feat: serve media on /media/, not separate domain
This commit is contained in:
parent
7b7b0ca15b
commit
d3eaaaaa9d
15
README.md
15
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.
|
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
|
||||||
|
|
|
@ -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
|
||||||
}
|
}
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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: {
|
||||||
|
|
|
@ -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;
|
||||||
|
}
|
||||||
|
}
|
Loading…
Reference in New Issue