mirror of
https://github.com/aaronpo97/the-biergarten-app.git
synced 2026-10-14 17:54:42 +00:00
Sign avatar download URLs against a browser-reachable endpoint
This commit is contained in:
@@ -260,17 +260,25 @@ and retrieving photos via an S3-compatible object store.
|
||||
|
||||
```bash
|
||||
SEAWEEDFS_SERVICE_URL=http://seaweedfs:8333 # Required, no default
|
||||
SEAWEEDFS_PUBLIC_URL=http://localhost:8333 # Optional, defaults to SEAWEEDFS_SERVICE_URL
|
||||
SEAWEEDFS_ACCESS_KEY_ID=admin # Required, no default
|
||||
SEAWEEDFS_SECRET_ACCESS_KEY=secret # Required, no default
|
||||
SEAWEEDFS_BUCKET=my-bucket # Required, no default
|
||||
```
|
||||
|
||||
- **Implementation**: `S3FileStorageProvider` throws on construction if any of
|
||||
these four values is missing
|
||||
the four required values is missing
|
||||
- **Local dev**: point at the `seaweedfs` Docker service (S3 API on port 8333)
|
||||
- **Production**: point at any S3-compatible endpoint (AWS S3, a hosted
|
||||
SeaweedFS cluster, and so on); `ForcePathStyle` is always enabled, which
|
||||
SeaweedFS requires and AWS S3 also accepts
|
||||
- **`SEAWEEDFS_PUBLIC_URL`**: the endpoint download URLs are signed against,
|
||||
used by `GetPresignedUrl` and reaching the API response as
|
||||
`PublicUserProfileDto.AvatarUrl`. A presigned URL is signed over its host, so
|
||||
this has to be the address the browser uses (`http://localhost:8333` with the
|
||||
dev compose file, which publishes the S3 API port), separate from the
|
||||
in-network `SEAWEEDFS_SERVICE_URL` the API uploads through. The two can be the
|
||||
same value wherever one address serves both
|
||||
|
||||
## Docker-specific variables
|
||||
|
||||
@@ -341,6 +349,7 @@ from the template in place; edit them manually if you go that route.
|
||||
| `SMTP_FROM_EMAIL` | ✓ | | ✓ | Yes | Email sender address |
|
||||
| `SMTP_FROM_NAME` | ✓ | | ✓ | No | Defaults to `The Biergarten` |
|
||||
| `SEAWEEDFS_SERVICE_URL` | ✓ | | ✓ | Yes | S3-compatible endpoint URL |
|
||||
| `SEAWEEDFS_PUBLIC_URL` | ✓ | | ✓ | No | Endpoint download URLs are signed against; defaults to `SEAWEEDFS_SERVICE_URL` |
|
||||
| `SEAWEEDFS_ACCESS_KEY_ID` | ✓ | | ✓ | Yes | S3 access key |
|
||||
| `SEAWEEDFS_SECRET_ACCESS_KEY` | ✓ | | ✓ | Yes | S3 secret key |
|
||||
| `SEAWEEDFS_BUCKET` | ✓ | | ✓ | Yes | S3 bucket name |
|
||||
@@ -415,6 +424,7 @@ SMTP_FROM_NAME=The Biergarten App
|
||||
|
||||
# SeaweedFS (S3-compatible storage)
|
||||
SEAWEEDFS_SERVICE_URL=http://seaweedfs:8333
|
||||
SEAWEEDFS_PUBLIC_URL=http://localhost:8333
|
||||
SEAWEEDFS_ACCESS_KEY_ID=<generated-with-openssl>
|
||||
SEAWEEDFS_SECRET_ACCESS_KEY=<generated-with-openssl>
|
||||
SEAWEEDFS_BUCKET=my-bucket
|
||||
|
||||
@@ -68,8 +68,12 @@ WEBSITE_BASE_URL=http://localhost:3000
|
||||
# ======================
|
||||
# SeaweedFS (S3) Configuration
|
||||
# ======================
|
||||
# Credentials and default bucket for the local SeaweedFS S3-compatible storage
|
||||
# Credentials and default bucket for the local SeaweedFS S3-compatible storage.
|
||||
# SEAWEEDFS_SERVICE_URL is the in-network endpoint the API uploads through;
|
||||
# SEAWEEDFS_PUBLIC_URL is the endpoint download URLs are signed against, so it has to be
|
||||
# reachable from the browser. It defaults to SEAWEEDFS_SERVICE_URL when left blank.
|
||||
SEAWEEDFS_SERVICE_URL=http://seaweedfs:8333
|
||||
SEAWEEDFS_PUBLIC_URL=http://localhost:8333
|
||||
SEAWEEDFS_ACCESS_KEY_ID=admin
|
||||
SEAWEEDFS_SECRET_ACCESS_KEY=secret
|
||||
SEAWEEDFS_BUCKET=my-bucket
|
||||
|
||||
@@ -23,6 +23,14 @@ public static class ConfigurationKeys
|
||||
public const string SmtpFromName = "SMTP_FROM_NAME";
|
||||
|
||||
public const string SeaweedFsServiceUrl = "SEAWEEDFS_SERVICE_URL";
|
||||
|
||||
/// <summary>
|
||||
/// The browser-reachable base URL of the object store, used to sign download URLs. Falls back to
|
||||
/// <see cref="SeaweedFsServiceUrl" /> when unset: SigV4 covers the host, so a URL signed against
|
||||
/// the container-internal endpoint is only usable by callers that can resolve that host.
|
||||
/// </summary>
|
||||
public const string SeaweedFsPublicUrl = "SEAWEEDFS_PUBLIC_URL";
|
||||
|
||||
public const string SeaweedFsAccessKeyId = "SEAWEEDFS_ACCESS_KEY_ID";
|
||||
public const string SeaweedFsSecretAccessKey = "SEAWEEDFS_SECRET_ACCESS_KEY";
|
||||
public const string SeaweedFsBucket = "SEAWEEDFS_BUCKET";
|
||||
|
||||
@@ -13,11 +13,13 @@ public class S3FileStorageProvider : IFileStorageProvider, IDisposable
|
||||
{
|
||||
private readonly string _bucket;
|
||||
private readonly IAmazonS3 _client;
|
||||
private readonly IAmazonS3 _presignClient;
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="S3FileStorageProvider" /> class, reading storage
|
||||
/// configuration (<c>SEAWEEDFS_SERVICE_URL</c>, <c>SEAWEEDFS_ACCESS_KEY_ID</c>,
|
||||
/// <c>SEAWEEDFS_SECRET_ACCESS_KEY</c>, <c>SEAWEEDFS_BUCKET</c>) from <paramref name="configuration" />.
|
||||
/// <c>SEAWEEDFS_SECRET_ACCESS_KEY</c>, <c>SEAWEEDFS_BUCKET</c>, and the optional
|
||||
/// <c>SEAWEEDFS_PUBLIC_URL</c>) from <paramref name="configuration" />.
|
||||
/// </summary>
|
||||
/// <exception cref="InvalidOperationException">
|
||||
/// Thrown when any of the required configuration values is not set.
|
||||
@@ -45,6 +47,23 @@ public class S3FileStorageProvider : IFileStorageProvider, IDisposable
|
||||
AmazonS3Config config = new() { ServiceURL = serviceUrl, ForcePathStyle = true };
|
||||
|
||||
_client = new AmazonS3Client(accessKeyId, secretAccessKey, config);
|
||||
|
||||
// A presigned URL is signed over its host, so one signed against the container-internal
|
||||
// endpoint is only usable from inside the network. Sign downloads against the public
|
||||
// endpoint instead, so a browser can fetch the object.
|
||||
string? configuredPublicUrl = configuration[ConfigurationKeys.SeaweedFsPublicUrl];
|
||||
string publicUrl = string.IsNullOrWhiteSpace(configuredPublicUrl)
|
||||
? serviceUrl
|
||||
: configuredPublicUrl;
|
||||
|
||||
_presignClient =
|
||||
publicUrl == serviceUrl
|
||||
? _client
|
||||
: new AmazonS3Client(
|
||||
accessKeyId,
|
||||
secretAccessKey,
|
||||
new AmazonS3Config { ServiceURL = publicUrl, ForcePathStyle = true }
|
||||
);
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
@@ -84,12 +103,15 @@ public class S3FileStorageProvider : IFileStorageProvider, IDisposable
|
||||
Expires = DateTime.UtcNow.Add(expiresIn),
|
||||
};
|
||||
|
||||
return _client.GetPreSignedURL(request);
|
||||
return _presignClient.GetPreSignedURL(request);
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public void Dispose()
|
||||
{
|
||||
if (!ReferenceEquals(_presignClient, _client))
|
||||
_presignClient.Dispose();
|
||||
|
||||
_client.Dispose();
|
||||
GC.SuppressFinalize(this);
|
||||
}
|
||||
|
||||
@@ -70,6 +70,7 @@ services:
|
||||
DB_USER: "${DB_USER}"
|
||||
DB_PASSWORD: "${DB_PASSWORD}"
|
||||
SEAWEEDFS_SERVICE_URL: "${SEAWEEDFS_SERVICE_URL}"
|
||||
SEAWEEDFS_PUBLIC_URL: "${SEAWEEDFS_PUBLIC_URL}"
|
||||
SEAWEEDFS_ACCESS_KEY_ID: "${SEAWEEDFS_ACCESS_KEY_ID}"
|
||||
SEAWEEDFS_SECRET_ACCESS_KEY: "${SEAWEEDFS_SECRET_ACCESS_KEY}"
|
||||
SEAWEEDFS_BUCKET: "${SEAWEEDFS_BUCKET}"
|
||||
|
||||
@@ -72,6 +72,7 @@ services:
|
||||
DB_USER: "${DB_USER}"
|
||||
DB_PASSWORD: "${DB_PASSWORD}"
|
||||
SEAWEEDFS_SERVICE_URL: "${SEAWEEDFS_SERVICE_URL}"
|
||||
SEAWEEDFS_PUBLIC_URL: "${SEAWEEDFS_PUBLIC_URL}"
|
||||
SEAWEEDFS_ACCESS_KEY_ID: "${SEAWEEDFS_ACCESS_KEY_ID}"
|
||||
SEAWEEDFS_SECRET_ACCESS_KEY: "${SEAWEEDFS_SECRET_ACCESS_KEY}"
|
||||
SEAWEEDFS_BUCKET: "${SEAWEEDFS_BUCKET}"
|
||||
@@ -110,6 +111,7 @@ services:
|
||||
CONFIRMATION_TOKEN_SECRET: "${CONFIRMATION_TOKEN_SECRET}"
|
||||
WEBSITE_BASE_URL: "${WEBSITE_BASE_URL}"
|
||||
SEAWEEDFS_SERVICE_URL: "${SEAWEEDFS_SERVICE_URL}"
|
||||
SEAWEEDFS_PUBLIC_URL: "${SEAWEEDFS_PUBLIC_URL}"
|
||||
SEAWEEDFS_ACCESS_KEY_ID: "${SEAWEEDFS_ACCESS_KEY_ID}"
|
||||
SEAWEEDFS_SECRET_ACCESS_KEY: "${SEAWEEDFS_SECRET_ACCESS_KEY}"
|
||||
SEAWEEDFS_BUCKET: "${SEAWEEDFS_BUCKET}"
|
||||
|
||||
@@ -84,6 +84,7 @@ services:
|
||||
DB_USER: "${DB_USER}"
|
||||
DB_PASSWORD: "${DB_PASSWORD}"
|
||||
SEAWEEDFS_SERVICE_URL: "${SEAWEEDFS_SERVICE_URL}"
|
||||
SEAWEEDFS_PUBLIC_URL: "${SEAWEEDFS_PUBLIC_URL}"
|
||||
SEAWEEDFS_ACCESS_KEY_ID: "${SEAWEEDFS_ACCESS_KEY_ID}"
|
||||
SEAWEEDFS_SECRET_ACCESS_KEY: "${SEAWEEDFS_SECRET_ACCESS_KEY}"
|
||||
SEAWEEDFS_BUCKET: "${SEAWEEDFS_BUCKET}"
|
||||
|
||||
@@ -19,6 +19,7 @@ services:
|
||||
DB_USER: "${DB_USER}"
|
||||
DB_PASSWORD: "${DB_PASSWORD}"
|
||||
SEAWEEDFS_SERVICE_URL: "${SEAWEEDFS_SERVICE_URL}"
|
||||
SEAWEEDFS_PUBLIC_URL: "${SEAWEEDFS_PUBLIC_URL}"
|
||||
SEAWEEDFS_ACCESS_KEY_ID: "${SEAWEEDFS_ACCESS_KEY_ID}"
|
||||
SEAWEEDFS_SECRET_ACCESS_KEY: "${SEAWEEDFS_SECRET_ACCESS_KEY}"
|
||||
SEAWEEDFS_BUCKET: "${SEAWEEDFS_BUCKET}"
|
||||
|
||||
@@ -51,6 +51,7 @@ services:
|
||||
CONFIRMATION_TOKEN_SECRET: "${CONFIRMATION_TOKEN_SECRET}"
|
||||
WEBSITE_BASE_URL: "${WEBSITE_BASE_URL}"
|
||||
SEAWEEDFS_SERVICE_URL: "${SEAWEEDFS_SERVICE_URL}"
|
||||
SEAWEEDFS_PUBLIC_URL: "${SEAWEEDFS_PUBLIC_URL}"
|
||||
SEAWEEDFS_ACCESS_KEY_ID: "${SEAWEEDFS_ACCESS_KEY_ID}"
|
||||
SEAWEEDFS_SECRET_ACCESS_KEY: "${SEAWEEDFS_SECRET_ACCESS_KEY}"
|
||||
SEAWEEDFS_BUCKET: "${SEAWEEDFS_BUCKET}"
|
||||
|
||||
@@ -70,6 +70,7 @@ services:
|
||||
DB_USER: "${DB_USER}"
|
||||
DB_PASSWORD: "${DB_PASSWORD}"
|
||||
SEAWEEDFS_SERVICE_URL: "${SEAWEEDFS_SERVICE_URL}"
|
||||
SEAWEEDFS_PUBLIC_URL: "${SEAWEEDFS_PUBLIC_URL}"
|
||||
SEAWEEDFS_ACCESS_KEY_ID: "${SEAWEEDFS_ACCESS_KEY_ID}"
|
||||
SEAWEEDFS_SECRET_ACCESS_KEY: "${SEAWEEDFS_SECRET_ACCESS_KEY}"
|
||||
SEAWEEDFS_BUCKET: "${SEAWEEDFS_BUCKET}"
|
||||
|
||||
Reference in New Issue
Block a user