From 932e257152b9338ab95c70756ac5b3853d745ba7 Mon Sep 17 00:00:00 2001 From: Adam French Date: Thu, 26 Mar 2026 11:18:32 +0000 Subject: [PATCH] Add HTTPS support in dev mode and fix mobile layout issues Generate self-signed certs for local HTTPS, add port 443 and full SSL server block to dev nginx config, add Spotify redirect URI env var, improve Spotify token error handling, and fix Chat/Steam mobile sizing. Co-Authored-By: Claude Opus 4.6 --- backend/handlers/handle_spotify.go | 5 +- backend/services/spotify.go | 5 ++ docker-compose.dev.yml | 4 ++ nginx/Dockerfile | 2 +- nginx/entrypoint.sh | 10 +++- nginx/nginx_dev.conf.template | 94 ++++++++++++++++++++++++++++++ vue/src/components/util/Chat.vue | 3 +- vue/src/views/home/Steam.vue | 7 +++ 8 files changed, 126 insertions(+), 4 deletions(-) diff --git a/backend/handlers/handle_spotify.go b/backend/handlers/handle_spotify.go index 852a339..7217b06 100644 --- a/backend/handlers/handle_spotify.go +++ b/backend/handlers/handle_spotify.go @@ -21,7 +21,10 @@ func (store *Store) CompleteSpotifyAuth(ctx *gin.Context) { return } - services.SaveSpotifyToken(services.SPOTIFY_TOKEN_JSON_PATH, token) + if err := services.SaveSpotifyToken(services.SPOTIFY_TOKEN_JSON_PATH, token); err != nil { + ctx.String(http.StatusInternalServerError, "Failed to save token: %v", err) + return + } client := spotify.New(store.SpotifyAuth.Client(c, token)) diff --git a/backend/services/spotify.go b/backend/services/spotify.go index 6808d72..24fe31c 100644 --- a/backend/services/spotify.go +++ b/backend/services/spotify.go @@ -5,6 +5,7 @@ import ( "encoding/json" "fmt" "os" + "path/filepath" "time" "github.com/zmb3/spotify/v2" @@ -34,6 +35,10 @@ func SaveSpotifyToken(path string, tok *oauth2.Token) error { Expiry: tok.Expiry, } + if err := os.MkdirAll(filepath.Dir(path), 0755); err != nil { + return fmt.Errorf("creating token directory: %w", err) + } + jsonBytes, err := json.MarshalIndent(data, "", " ") if err != nil { return err diff --git a/docker-compose.dev.yml b/docker-compose.dev.yml index 51c0387..c6b03f6 100644 --- a/docker-compose.dev.yml +++ b/docker-compose.dev.yml @@ -6,12 +6,16 @@ services: - /app/node_modules environment: - NODE_ENV=development + backend: + environment: + - SPOTIFY_REDIRECT_URI=https://localhost/api/spotify/callback nginx: environment: - DEV_MODE=true - SEED_DB=true ports: - 80:80 + - 443:443 certbot: profiles: - disabled diff --git a/nginx/Dockerfile b/nginx/Dockerfile index c75cab9..747f14c 100644 --- a/nginx/Dockerfile +++ b/nginx/Dockerfile @@ -1,6 +1,6 @@ FROM nginx:latest RUN rm -rf /etc/nginx/html/* && \ - apt-get update && apt-get install -y gettext-base && \ + apt-get update && apt-get install -y gettext-base openssl && \ rm -rf /var/lib/apt/lists/* COPY nginx.conf.template /etc/nginx/nginx.conf.template COPY nginx_setup.conf.template /etc/nginx/nginx_setup.conf.template diff --git a/nginx/entrypoint.sh b/nginx/entrypoint.sh index 83fa286..46f991d 100755 --- a/nginx/entrypoint.sh +++ b/nginx/entrypoint.sh @@ -3,7 +3,15 @@ set -e # Check if dev mode, certificate exists, or setup mode if [ "$DEV_MODE" = "true" ]; then - echo "Dev mode. Using HTTP-only nginx config." + echo "Dev mode. Generating self-signed certificate for HTTPS." + CERT_DIR="/etc/letsencrypt/live/localhost" + if [ ! -f "$CERT_DIR/fullchain.pem" ]; then + mkdir -p "$CERT_DIR" + openssl req -x509 -nodes -days 365 -newkey rsa:2048 \ + -keyout "$CERT_DIR/privkey.pem" \ + -out "$CERT_DIR/fullchain.pem" \ + -subj "/CN=localhost" 2>/dev/null + fi envsubst '${DOMAIN} ${BACKEND_HOST} ${BACKEND_PORT} ${BACKEND_ENDPOINT} ${ICECAST_HOST} ${ICECAST_PORT} ${GITEA_HOST} ${GITEA_PORT}' \ /etc/nginx/nginx.conf diff --git a/nginx/nginx_dev.conf.template b/nginx/nginx_dev.conf.template index 7fa11e3..6d4ab3b 100644 --- a/nginx/nginx_dev.conf.template +++ b/nginx/nginx_dev.conf.template @@ -131,4 +131,98 @@ http { } + server { + listen 443 ssl; + server_name $DOMAIN www.$DOMAIN; + + ssl_certificate /etc/letsencrypt/live/localhost/fullchain.pem; + ssl_certificate_key /etc/letsencrypt/live/localhost/privkey.pem; + + location /uploads/ { + alias /uploads/; + add_header X-Content-Type-Options nosniff always; + add_header Content-Disposition "inline" always; + add_header Content-Security-Policy "default-src 'none'; img-src 'self'; style-src 'none'; script-src 'none'" always; + } + + location / { + proxy_pass http://vue:5173; + proxy_http_version 1.1; + proxy_set_header Upgrade $http_upgrade; + proxy_set_header Connection "upgrade"; + proxy_set_header Host $host; + } + + location $BACKEND_ENDPOINT { + return 301 $BACKEND_ENDPOINT/; + } + + location $BACKEND_ENDPOINT/ws { + rewrite ^$BACKEND_ENDPOINT/(.*)$ /$1 break; + proxy_pass http://$BACKEND_HOST:$BACKEND_PORT/; + proxy_http_version 1.1; + proxy_set_header Upgrade $http_upgrade; + proxy_set_header Connection "upgrade"; + proxy_set_header Host $host; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header X-Forwarded-Proto $scheme; + } + + location $BACKEND_ENDPOINT/auth/login { + limit_req zone=login burst=3 nodelay; + rewrite ^$BACKEND_ENDPOINT/(.*)$ /$1 break; + proxy_pass http://$BACKEND_HOST:$BACKEND_PORT/; + proxy_set_header Host $host; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header X-Forwarded-Proto $scheme; + } + + location $BACKEND_ENDPOINT/messages/upload { + limit_req zone=upload burst=3 nodelay; + rewrite ^$BACKEND_ENDPOINT/(.*)$ /$1 break; + proxy_pass http://$BACKEND_HOST:$BACKEND_PORT/; + proxy_set_header Host $host; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header X-Forwarded-Proto $scheme; + } + + location $BACKEND_ENDPOINT/ { + limit_req zone=api burst=20 nodelay; + rewrite ^$BACKEND_ENDPOINT/(.*)$ /$1 break; + proxy_pass http://$BACKEND_HOST:$BACKEND_PORT/; + proxy_set_header Host $host; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header X-Forwarded-Proto $scheme; + } + + location /radio { + return 301 /radio/; + } + + location /radio/ { + proxy_pass http://$ICECAST_HOST:$ICECAST_PORT/; + proxy_set_header Host $host; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header X-Forwarded-Proto $scheme; + } + + location /gitea { + return 301 /gitea/; + } + + location /gitea/ { + proxy_pass http://$GITEA_HOST:$GITEA_PORT/; + proxy_set_header Host $host; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header X-Forwarded-Proto $scheme; + } + + } + } diff --git a/vue/src/components/util/Chat.vue b/vue/src/components/util/Chat.vue index eb2c847..de4b33a 100644 --- a/vue/src/components/util/Chat.vue +++ b/vue/src/components/util/Chat.vue @@ -195,7 +195,8 @@ onUnmounted(() => { diff --git a/vue/src/views/home/Steam.vue b/vue/src/views/home/Steam.vue index 2f4b6eb..a93a687 100644 --- a/vue/src/views/home/Steam.vue +++ b/vue/src/views/home/Steam.vue @@ -120,4 +120,11 @@ p { .fade-leave-to { opacity: 0; } + +@media (max-width: 850px) { + .steam-wrapper { + height: auto; + min-height: 120px; + } +}