diff --git a/README.md b/README.md index 1fdeff6..788b873 100644 --- a/README.md +++ b/README.md @@ -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. +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 Run `make backend` to build the API server, then run `yarn build` in `frontend/`. ## 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. -**Make sure to rewrite requests going to the API server to remove the starting `/api/`.** +Every path should be proxied to the frontend, except: + +- `/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 @@ -52,7 +59,7 @@ HMAC_KEY="`go run -v ./scripts/genkey`" DATABASE_URL=postgresql://:@localhost/ # PostgreSQL database URL REDIS=localhost:6379 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 diff --git a/backend/db/avatars.go b/backend/db/avatars.go index 11790ae..5fc7bc2 100644 --- a/backend/db/avatars.go +++ b/backend/db/avatars.go @@ -148,21 +148,23 @@ func (db *DB) WriteUserAvatar(ctx context.Context, jpegLocation string, 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", }) if err != nil { 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", }) if err != nil { 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, @@ -172,19 +174,21 @@ func (db *DB) WriteMemberAvatar(ctx context.Context, jpegLocation string, 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", }) if err != nil { 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", }) if err != nil { 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 } diff --git a/backend/db/db.go b/backend/db/db.go index 77c8a35..ed205af 100644 --- a/backend/db/db.go +++ b/backend/db/db.go @@ -4,6 +4,7 @@ import ( "context" "encoding/json" "fmt" + "net/url" "os" "codeberg.org/u1f320/pronouns.cc/backend/log" @@ -26,6 +27,7 @@ type DB struct { minio *minio.Client minioBucket string + baseURL *url.URL } func New() (*DB, error) { @@ -53,12 +55,18 @@ func New() (*DB, error) { 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{ Pool: pool, Redis: redis, minio: minioClient, minioBucket: os.Getenv("MINIO_BUCKET"), + baseURL: baseURL, } return db, nil diff --git a/frontend/next.config.js b/frontend/next.config.js index 77d540d..0334234 100755 --- a/frontend/next.config.js +++ b/frontend/next.config.js @@ -8,8 +8,12 @@ const nextConfig = { return [ { 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: { diff --git a/pronounscc.nginx b/pronounscc.nginx new file mode 100644 index 0000000..cd24039 --- /dev/null +++ b/pronounscc.nginx @@ -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; + } +}