feat: serve media on /media/, not separate domain

This commit is contained in:
Sam 2022-12-22 15:42:43 +01:00
parent 7b7b0ca15b
commit d3eaaaaa9d
No known key found for this signature in database
GPG key ID: B4EF20DDE721CAA1
5 changed files with 109 additions and 11 deletions

View file

@ -14,16 +14,23 @@ A work-in-progress site to share your names, pronouns, and other preferred terms
When working on the frontend, run the API and then use `yarn dev` in `frontend/` for hot reloading. When working on the frontend, run the API and then use `yarn dev` in `frontend/` for hot reloading.
Note that the Next.js server assumes that the backend listens on `:8080` and MinIO listens on `:9000`.
If these ports differ on your development environment, you must edit `next.config.js`.
## Building ## Building
Run `make backend` to build the API server, then run `yarn build` in `frontend/`. Run `make backend` to build the API server, then run `yarn build` in `frontend/`.
## Running ## Running
Both the backend and frontend are expected to run behind a reverse proxy such as [Caddy](https://caddyserver.com/). Both the backend and frontend are expected to run behind a reverse proxy such as [Caddy](https://caddyserver.com/) or nginx.
The frontend should serve every possible path _except_ anything starting with `/api/`, which should be routed to the backend instead. Every path should be proxied to the frontend, except:
**Make sure to rewrite requests going to the API server to remove the starting `/api/`.**
- `/api/`: this should be proxied to the backend, with the URL being rewritten to remove `/api`
(for example, a request to `$DOMAIN/api/v1/users/@me` should be proxied to `localhost:8080/v1/users/@me`)
- `/media/`: this should be proxied to your object storage.
Make sure to rewrite `/media` into your storage bucket's name.
## Development ## Development
@ -52,7 +59,7 @@ HMAC_KEY="`go run -v ./scripts/genkey`"
DATABASE_URL=postgresql://<username>:<pass>@localhost/<database> # PostgreSQL database URL DATABASE_URL=postgresql://<username>:<pass>@localhost/<database> # PostgreSQL database URL
REDIS=localhost:6379 REDIS=localhost:6379
PORT=8080 # Port the API will listen on. Default is 8080, this is also default for the backend. PORT=8080 # Port the API will listen on. Default is 8080, this is also default for the backend.
MINIO_ENDPOINT=localhost:9000 # This always needs to be set, it *does not* need to point to a MinIO server. MINIO_ENDPOINT=localhost:9000 # This always needs to be set, it *does not* need to point to a running MinIO server.
``` ```
## License ## License

View file

@ -148,21 +148,23 @@ func (db *DB) WriteUserAvatar(ctx context.Context,
jpegLocation string, jpegLocation string,
err error, err error,
) { ) {
webpInfo, err := db.minio.PutObject(ctx, db.minioBucket, "/users/"+userID.String()+".webp", webp, -1, minio.PutObjectOptions{ _, err = db.minio.PutObject(ctx, db.minioBucket, "/users/"+userID.String()+".webp", webp, -1, minio.PutObjectOptions{
ContentType: "image/webp", ContentType: "image/webp",
}) })
if err != nil { if err != nil {
return "", "", errors.Wrap(err, "uploading webp avatar") return "", "", errors.Wrap(err, "uploading webp avatar")
} }
jpegInfo, err := db.minio.PutObject(ctx, db.minioBucket, "/users/"+userID.String()+".jpg", jpeg, -1, minio.PutObjectOptions{ _, err = db.minio.PutObject(ctx, db.minioBucket, "/users/"+userID.String()+".jpg", jpeg, -1, minio.PutObjectOptions{
ContentType: "image/jpeg", ContentType: "image/jpeg",
}) })
if err != nil { if err != nil {
return "", "", errors.Wrap(err, "uploading jpeg avatar") return "", "", errors.Wrap(err, "uploading jpeg avatar")
} }
return webpInfo.Location, jpegInfo.Location, nil return db.baseURL.JoinPath("/media/users/" + userID.String() + ".webp").String(),
db.baseURL.JoinPath("/media/users/" + userID.String() + ".jpg").String(),
nil
} }
func (db *DB) WriteMemberAvatar(ctx context.Context, func (db *DB) WriteMemberAvatar(ctx context.Context,
@ -172,19 +174,21 @@ func (db *DB) WriteMemberAvatar(ctx context.Context,
jpegLocation string, jpegLocation string,
err error, err error,
) { ) {
webpInfo, err := db.minio.PutObject(ctx, db.minioBucket, "/members/"+memberID.String()+".webp", webp, -1, minio.PutObjectOptions{ _, err = db.minio.PutObject(ctx, db.minioBucket, "/members/"+memberID.String()+".webp", webp, -1, minio.PutObjectOptions{
ContentType: "image/webp", ContentType: "image/webp",
}) })
if err != nil { if err != nil {
return "", "", errors.Wrap(err, "uploading webp avatar") return "", "", errors.Wrap(err, "uploading webp avatar")
} }
jpegInfo, err := db.minio.PutObject(ctx, db.minioBucket, "/members/"+memberID.String()+".jpg", jpeg, -1, minio.PutObjectOptions{ _, err = db.minio.PutObject(ctx, db.minioBucket, "/members/"+memberID.String()+".jpg", jpeg, -1, minio.PutObjectOptions{
ContentType: "image/jpeg", ContentType: "image/jpeg",
}) })
if err != nil { if err != nil {
return "", "", errors.Wrap(err, "uploading jpeg avatar") return "", "", errors.Wrap(err, "uploading jpeg avatar")
} }
return webpInfo.Location, jpegInfo.Location, nil return db.baseURL.JoinPath("/media/members/" + memberID.String() + ".webp").String(),
db.baseURL.JoinPath("/media/members/" + memberID.String() + ".jpg").String(),
nil
} }

View file

@ -4,6 +4,7 @@ import (
"context" "context"
"encoding/json" "encoding/json"
"fmt" "fmt"
"net/url"
"os" "os"
"codeberg.org/u1f320/pronouns.cc/backend/log" "codeberg.org/u1f320/pronouns.cc/backend/log"
@ -26,6 +27,7 @@ type DB struct {
minio *minio.Client minio *minio.Client
minioBucket string minioBucket string
baseURL *url.URL
} }
func New() (*DB, error) { func New() (*DB, error) {
@ -53,12 +55,18 @@ func New() (*DB, error) {
return nil, errors.Wrap(err, "creating minio client") return nil, errors.Wrap(err, "creating minio client")
} }
baseURL, err := url.Parse(os.Getenv("BASE_URL"))
if err != nil {
return nil, errors.Wrap(err, "parsing base URL")
}
db := &DB{ db := &DB{
Pool: pool, Pool: pool,
Redis: redis, Redis: redis,
minio: minioClient, minio: minioClient,
minioBucket: os.Getenv("MINIO_BUCKET"), minioBucket: os.Getenv("MINIO_BUCKET"),
baseURL: baseURL,
} }
return db, nil return db, nil

View file

@ -8,8 +8,12 @@ const nextConfig = {
return [ return [
{ {
source: "/api/:path*", source: "/api/:path*",
destination: "http://localhost:8080/:path*", // Proxy to Backend destination: "http://localhost:8080/:path*", // proxy to backend
}, },
{
source: "/media/:path*",
destination: "http://localhost:9000/pronouns.cc/:path*", // proxy to media server
}
]; ];
}, },
sentry: { sentry: {

75
pronounscc.nginx Normal file
View file

@ -0,0 +1,75 @@
server {
server_name example.tld;
listen 80;
listen [::]:80;
# For SSL domain validation
root /var/www/html;
location /.well-known/acme-challenge/ { allow all; }
location /.well-known/pki-validation/ { allow all; }
location / { return 301 https://$server_name$request_uri; }
}
# For media proxy
proxy_cache_path /tmp/pronouns-media-cache levels=1:2 keys_zone=pronouns_media_cache:10m max_size=1g
inactive=720m use_temp_path=off;
server {
listen 443 ssl http2;
listen [::]:443 ssl http2;
server_name example.tld;
ssl_session_timeout 1d;
ssl_session_cache shared:ssl_session_cache:10m;
ssl_session_tickets off;
ssl_protocols TLSv1.2 TLSv1.3;
ssl_ciphers ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384:ECDHE-ECDSA-CHACHA20-POLY1305:ECDHE-RSA-CHACHA20-POLY1305:DHE-RSA-AES128-GCM-SHA256:DHE-RSA-AES256-GCM-SHA384;
ssl_prefer_server_ciphers off;
ssl_stapling on;
ssl_stapling_verify on;
# To use a Let's Encrypt certificate
ssl_trusted_certificate /etc/letsencrypt/live/example.tld/chain.pem;
ssl_certificate /etc/letsencrypt/live/example.tld/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/example.tld/privkey.pem;
# To use Debian/Ubuntu's self-signed certificate (For testing or before issuing a certificate)
#ssl_certificate /etc/ssl/certs/ssl-cert-snakeoil.pem;
#ssl_certificate_key /etc/ssl/private/ssl-cert-snakeoil.key;
client_max_body_size 8m;
location ~ ^/api {
rewrite ^/api(.*) $1 break;
proxy_pass http://127.0.0.1:8080;
}
location ~ ^/media {
proxy_cache pronouns_media_cache;
slice 1m;
proxy_cache_key $host$uri$is_args$args$slice_range;
proxy_set_header Range $slice_range;
proxy_cache_valid 200 206 301 304 1h;
proxy_cache_lock on;
proxy_ignore_client_abort on;
proxy_buffering on;
chunked_transfer_encoding on;
# Rewrite URL to remove /media/ and add bucket
rewrite ^/media/(.*) /pronouns/$1 break;
proxy_pass http://127.0.0.1:9000;
}
location / {
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection 'upgrade';
proxy_set_header Host $host;
proxy_cache_bypass $http_upgrade;
proxy_pass http://127.0.0.1:3000;
}
}