From e71bd6768e84c127ea4abb3ed33abcf02d334c7d Mon Sep 17 00:00:00 2001 From: jubnl Date: Wed, 1 Apr 2026 20:37:01 +0200 Subject: [PATCH 1/4] fix: show actual backend error messages on login page and add missing db import - LoginPage now uses getApiErrorMessage() instead of err.message so backend validation errors (e.g. "Password must be at least 8 characters") are displayed instead of the generic "Request failed with status code 400" - Add missing db import in server/src/index.ts Co-Authored-By: Claude Sonnet 4.6 --- client/src/pages/LoginPage.tsx | 3 ++- server/src/index.ts | 2 +- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/client/src/pages/LoginPage.tsx b/client/src/pages/LoginPage.tsx index 1474838..fd48341 100644 --- a/client/src/pages/LoginPage.tsx +++ b/client/src/pages/LoginPage.tsx @@ -4,6 +4,7 @@ import { useAuthStore } from '../store/authStore' import { useSettingsStore } from '../store/settingsStore' import { SUPPORTED_LANGUAGES, useTranslation } from '../i18n' import { authApi } from '../api/client' +import { getApiErrorMessage } from '../types' import { Plane, Eye, EyeOff, Mail, Lock, MapPin, Calendar, Package, User, Globe, Zap, Users, Wallet, Map, CheckSquare, BookMarked, FolderOpen, Route, Shield, KeyRound } from 'lucide-react' interface AppConfig { @@ -171,7 +172,7 @@ export default function LoginPage(): React.ReactElement { setShowTakeoff(true) setTimeout(() => navigate('/dashboard'), 2600) } catch (err: unknown) { - setError(err instanceof Error ? err.message : t('login.error')) + setError(getApiErrorMessage(err, t('login.error'))) setIsLoading(false) } } diff --git a/server/src/index.ts b/server/src/index.ts index 84a1f55..328387d 100644 --- a/server/src/index.ts +++ b/server/src/index.ts @@ -203,7 +203,7 @@ app.use('/api/admin', adminRoutes); // Public addons endpoint (authenticated but not admin-only) import { authenticate as addonAuth } from './middleware/auth'; -import { db as addonDb } from './db/database'; +import {db, db as addonDb} from './db/database'; import { Addon } from './types'; app.get('/api/addons', addonAuth, (req: Request, res: Response) => { const addons = addonDb.prepare('SELECT id, name, type, icon, enabled FROM addons WHERE enabled = 1 ORDER BY sort_order').all() as Pick[]; From fabf5a7e26f7f4ee233a05bf8febcfb564636522 Mon Sep 17 00:00:00 2001 From: jubnl Date: Wed, 1 Apr 2026 20:38:25 +0200 Subject: [PATCH 2/4] fix: remove redundant db import alias in index.ts db was already imported as addonDb; the extra db named import was unnecessary. Updated the one stray db.prepare call at line 155 to use addonDb consistently. Co-Authored-By: Claude Sonnet 4.6 --- server/src/index.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/server/src/index.ts b/server/src/index.ts index 328387d..c43faf5 100644 --- a/server/src/index.ts +++ b/server/src/index.ts @@ -152,7 +152,7 @@ app.get('/uploads/photos/:filename', (req: Request, res: Response) => { jwt.verify(token, process.env.JWT_SECRET || require('./config').JWT_SECRET); } catch { // Check if it's a share token - const shareRow = db.prepare('SELECT id FROM share_tokens WHERE token = ?').get(token); + const shareRow = addonDb.prepare('SELECT id FROM share_tokens WHERE token = ?').get(token); if (!shareRow) return res.status(401).send('Authentication required'); } res.sendFile(resolved); @@ -203,7 +203,7 @@ app.use('/api/admin', adminRoutes); // Public addons endpoint (authenticated but not admin-only) import { authenticate as addonAuth } from './middleware/auth'; -import {db, db as addonDb} from './db/database'; +import {db as addonDb} from './db/database'; import { Addon } from './types'; app.get('/api/addons', addonAuth, (req: Request, res: Response) => { const addons = addonDb.prepare('SELECT id, name, type, icon, enabled FROM addons WHERE enabled = 1 ORDER BY sort_order').all() as Pick[]; From 1a4c04e239865e406d2c9844c889677716978224 Mon Sep 17 00:00:00 2001 From: jubnl Date: Wed, 1 Apr 2026 21:19:53 +0200 Subject: [PATCH 3/4] fix: resolve Immich 401 passthrough causing spurious login redirects - Auth middleware now tags its 401s with code: AUTH_REQUIRED so the client interceptor only redirects to /login on genuine session failures, not on upstream API errors - Fix /albums and album sync routes using raw encrypted API key instead of getImmichCredentials() (which decrypts it), causing Immich to reject requests with 401 - Add toast error notifications for all Immich operations in MemoriesPanel that previously swallowed errors silently Co-Authored-By: Claude Sonnet 4.6 --- client/src/api/client.ts | 2 +- .../src/components/Memories/MemoriesPanel.tsx | 17 ++++++++++------- client/src/i18n/translations/en.ts | 8 ++++++++ server/src/middleware/auth.ts | 6 +++--- server/src/routes/immich.ts | 16 ++++++++-------- 5 files changed, 30 insertions(+), 19 deletions(-) diff --git a/client/src/api/client.ts b/client/src/api/client.ts index 7992005..a901a5b 100644 --- a/client/src/api/client.ts +++ b/client/src/api/client.ts @@ -25,7 +25,7 @@ apiClient.interceptors.request.use( apiClient.interceptors.response.use( (response) => response, (error) => { - if (error.response?.status === 401) { + if (error.response?.status === 401 && (error.response?.data as { code?: string } | undefined)?.code === 'AUTH_REQUIRED') { if (!window.location.pathname.includes('/login') && !window.location.pathname.includes('/register')) { window.location.href = '/login' } diff --git a/client/src/components/Memories/MemoriesPanel.tsx b/client/src/components/Memories/MemoriesPanel.tsx index b45c82e..9dd1ed4 100644 --- a/client/src/components/Memories/MemoriesPanel.tsx +++ b/client/src/components/Memories/MemoriesPanel.tsx @@ -4,6 +4,7 @@ import apiClient from '../../api/client' import { useAuthStore } from '../../store/authStore' import { useTranslation } from '../../i18n' import { getAuthUrl } from '../../api/authUrl' +import { useToast } from '../shared/Toast' function ImmichImg({ baseUrl, style, loading }: { baseUrl: string; style?: React.CSSProperties; loading?: 'lazy' | 'eager' }) { const [src, setSrc] = useState('') @@ -40,6 +41,7 @@ interface MemoriesPanelProps { export default function MemoriesPanel({ tripId, startDate, endDate }: MemoriesPanelProps) { const { t } = useTranslation() + const toast = useToast() const currentUser = useAuthStore(s => s.user) const [connected, setConnected] = useState(false) @@ -81,7 +83,7 @@ export default function MemoriesPanel({ tripId, startDate, endDate }: MemoriesPa try { const res = await apiClient.get('/integrations/immich/albums') setAlbums(res.data.albums || []) - } catch { setAlbums([]) } + } catch { setAlbums([]); toast.error(t('memories.error.loadAlbums')) } finally { setAlbumsLoading(false) } } @@ -94,14 +96,14 @@ export default function MemoriesPanel({ tripId, startDate, endDate }: MemoriesPa const linksRes = await apiClient.get(`/integrations/immich/trips/${tripId}/album-links`) const newLink = (linksRes.data.links || []).find((l: any) => l.immich_album_id === albumId) if (newLink) await syncAlbum(newLink.id) - } catch {} + } catch { toast.error(t('memories.error.linkAlbum')) } } const unlinkAlbum = async (linkId: number) => { try { await apiClient.delete(`/integrations/immich/trips/${tripId}/album-links/${linkId}`) loadAlbumLinks() - } catch {} + } catch { toast.error(t('memories.error.unlinkAlbum')) } } const syncAlbum = async (linkId: number) => { @@ -110,7 +112,7 @@ export default function MemoriesPanel({ tripId, startDate, endDate }: MemoriesPa await apiClient.post(`/integrations/immich/trips/${tripId}/album-links/${linkId}/sync`) await loadAlbumLinks() await loadPhotos() - } catch {} + } catch { toast.error(t('memories.error.syncAlbum')) } finally { setSyncing(null) } } @@ -178,6 +180,7 @@ export default function MemoriesPanel({ tripId, startDate, endDate }: MemoriesPa setPickerPhotos(res.data.assets || []) } catch { setPickerPhotos([]) + toast.error(t('memories.error.loadPhotos')) } finally { setPickerLoading(false) } @@ -206,7 +209,7 @@ export default function MemoriesPanel({ tripId, startDate, endDate }: MemoriesPa }) setShowPicker(false) loadInitial() - } catch {} + } catch { toast.error(t('memories.error.addPhotos')) } } // ── Remove photo ────────────────────────────────────────────────────────── @@ -215,7 +218,7 @@ export default function MemoriesPanel({ tripId, startDate, endDate }: MemoriesPa try { await apiClient.delete(`/integrations/immich/trips/${tripId}/photos/${assetId}`) setTripPhotos(prev => prev.filter(p => p.immich_asset_id !== assetId)) - } catch {} + } catch { toast.error(t('memories.error.removePhoto')) } } // ── Toggle sharing ──────────────────────────────────────────────────────── @@ -226,7 +229,7 @@ export default function MemoriesPanel({ tripId, startDate, endDate }: MemoriesPa setTripPhotos(prev => prev.map(p => p.immich_asset_id === assetId ? { ...p, shared: shared ? 1 : 0 } : p )) - } catch {} + } catch { toast.error(t('memories.error.toggleSharing')) } } // ── Helpers ─────────────────────────────────────────────────────────────── diff --git a/client/src/i18n/translations/en.ts b/client/src/i18n/translations/en.ts index 632a8c3..336ea72 100644 --- a/client/src/i18n/translations/en.ts +++ b/client/src/i18n/translations/en.ts @@ -1363,6 +1363,14 @@ const en: Record = { 'memories.confirmShareTitle': 'Share with trip members?', 'memories.confirmShareHint': '{count} photos will be visible to all members of this trip. You can make individual photos private later.', 'memories.confirmShareButton': 'Share photos', + 'memories.error.loadAlbums': 'Failed to load albums', + 'memories.error.linkAlbum': 'Failed to link album', + 'memories.error.unlinkAlbum': 'Failed to unlink album', + 'memories.error.syncAlbum': 'Failed to sync album', + 'memories.error.loadPhotos': 'Failed to load photos', + 'memories.error.addPhotos': 'Failed to add photos', + 'memories.error.removePhoto': 'Failed to remove photo', + 'memories.error.toggleSharing': 'Failed to update sharing', // Collab Addon 'collab.tabs.chat': 'Chat', diff --git a/server/src/middleware/auth.ts b/server/src/middleware/auth.ts index b2a7807..2fc2b53 100644 --- a/server/src/middleware/auth.ts +++ b/server/src/middleware/auth.ts @@ -16,7 +16,7 @@ const authenticate = (req: Request, res: Response, next: NextFunction): void => const token = extractToken(req); if (!token) { - res.status(401).json({ error: 'Access token required' }); + res.status(401).json({ error: 'Access token required', code: 'AUTH_REQUIRED' }); return; } @@ -26,13 +26,13 @@ const authenticate = (req: Request, res: Response, next: NextFunction): void => 'SELECT id, username, email, role FROM users WHERE id = ?' ).get(decoded.id) as User | undefined; if (!user) { - res.status(401).json({ error: 'User not found' }); + res.status(401).json({ error: 'User not found', code: 'AUTH_REQUIRED' }); return; } (req as AuthRequest).user = user; next(); } catch (err: unknown) { - res.status(401).json({ error: 'Invalid or expired token' }); + res.status(401).json({ error: 'Invalid or expired token', code: 'AUTH_REQUIRED' }); } }; diff --git a/server/src/routes/immich.ts b/server/src/routes/immich.ts index c39a7ec..9b63da4 100644 --- a/server/src/routes/immich.ts +++ b/server/src/routes/immich.ts @@ -359,12 +359,12 @@ router.get('/assets/:assetId/original', authFromQuery, async (req: Request, res: // List user's Immich albums router.get('/albums', authenticate, async (req: Request, res: Response) => { const authReq = req as AuthRequest; - const user = db.prepare('SELECT immich_url, immich_api_key FROM users WHERE id = ?').get(authReq.user.id) as any; - if (!user?.immich_url || !user?.immich_api_key) return res.status(400).json({ error: 'Immich not configured' }); + const creds = getImmichCredentials(authReq.user.id); + if (!creds) return res.status(400).json({ error: 'Immich not configured' }); try { - const resp = await fetch(`${user.immich_url}/api/albums`, { - headers: { 'x-api-key': user.immich_api_key, 'Accept': 'application/json' }, + const resp = await fetch(`${creds.immich_url}/api/albums`, { + headers: { 'x-api-key': creds.immich_api_key, 'Accept': 'application/json' }, signal: AbortSignal.timeout(10000), }); if (!resp.ok) return res.status(resp.status).json({ error: 'Failed to fetch albums' }); @@ -431,12 +431,12 @@ router.post('/trips/:tripId/album-links/:linkId/sync', authenticate, async (req: .get(linkId, tripId, authReq.user.id) as any; if (!link) return res.status(404).json({ error: 'Album link not found' }); - const user = db.prepare('SELECT immich_url, immich_api_key FROM users WHERE id = ?').get(authReq.user.id) as any; - if (!user?.immich_url || !user?.immich_api_key) return res.status(400).json({ error: 'Immich not configured' }); + const creds = getImmichCredentials(authReq.user.id); + if (!creds) return res.status(400).json({ error: 'Immich not configured' }); try { - const resp = await fetch(`${user.immich_url}/api/albums/${link.immich_album_id}`, { - headers: { 'x-api-key': user.immich_api_key, 'Accept': 'application/json' }, + const resp = await fetch(`${creds.immich_url}/api/albums/${link.immich_album_id}`, { + headers: { 'x-api-key': creds.immich_api_key, 'Accept': 'application/json' }, signal: AbortSignal.timeout(15000), }); if (!resp.ok) return res.status(resp.status).json({ error: 'Failed to fetch album' }); From ae040714664446a8142d6ee366606a9289628f49 Mon Sep 17 00:00:00 2001 From: jubnl Date: Wed, 1 Apr 2026 21:44:02 +0200 Subject: [PATCH 4/4] docs: document COOKIE_SECURE and OIDC_DISCOVERY_URL across all config files Adds COOKIE_SECURE (fixes login loop on plain-HTTP setups) and the previously undocumented OIDC_DISCOVERY_URL to .env.example, docker-compose.yml, README.md, chart/values.yaml, chart/templates/configmap.yaml, and chart/README.md. Co-Authored-By: Claude Sonnet 4.6 --- README.md | 19 +++++++++++++++++-- chart/README.md | 2 ++ chart/templates/configmap.yaml | 6 ++++++ chart/values.yaml | 4 ++++ docker-compose.yml | 12 +++++++----- server/.env.example | 3 ++- server/src/services/cookie.ts | 2 +- 7 files changed, 39 insertions(+), 9 deletions(-) diff --git a/README.md b/README.md index 5644377..bdd5e3f 100644 --- a/README.md +++ b/README.md @@ -139,10 +139,23 @@ services: - NODE_ENV=production - PORT=3000 - ENCRYPTION_KEY=${ENCRYPTION_KEY:-} # Recommended. Generate with: openssl rand -hex 32. If unset, falls back to data/.jwt_secret (existing installs) or auto-generates a key (fresh installs). - - ALLOWED_ORIGINS=${ALLOWED_ORIGINS:-} # Comma-separated origins for CORS and email notification links - TZ=${TZ:-UTC} # Timezone for logs, reminders and scheduled tasks (e.g. Europe/Berlin) - LOG_LEVEL=${LOG_LEVEL:-info} # info = concise user actions; debug = verbose admin-level details - # - ALLOW_INTERNAL_NETWORK=true # Uncomment if Immich is on your local network (RFC-1918 IPs) + - ALLOWED_ORIGINS=${ALLOWED_ORIGINS:-} # Comma-separated origins for CORS and email notification links + - FORCE_HTTPS=true # Redirect HTTP to HTTPS when behind a TLS-terminating proxy + # - COOKIE_SECURE=false # Uncomment if accessing over plain HTTP (no HTTPS). Not recommended for production. + - TRUST_PROXY=1 # Number of trusted proxies for X-Forwarded-For + # - ALLOW_INTERNAL_NETWORK=true # Uncomment if Immich or other services are on your local network (RFC-1918 IPs) + - APP_URL=${APP_URL:-} # Base URL of this instance — required when OIDC is enabled; must match the redirect URI registered with your IdP + # - OIDC_ISSUER=https://auth.example.com # OpenID Connect provider URL + # - OIDC_CLIENT_ID=trek # OpenID Connect client ID + # - OIDC_CLIENT_SECRET=supersecret # OpenID Connect client secret + # - OIDC_DISPLAY_NAME=SSO # Label shown on the SSO login button + # - OIDC_ONLY=false # Set to true to disable local password auth entirely (SSO only) + # - OIDC_ADMIN_CLAIM=groups # OIDC claim used to identify admin users + # - OIDC_ADMIN_VALUE=app-trek-admins # Value of the OIDC claim that grants admin role + # - OIDC_DISCOVERY_URL= # Override the OIDC discovery endpoint for providers with non-standard paths (e.g. Authentik) + # - DEMO_MODE=false # Enable demo mode (resets data hourly) volumes: - ./data:/app/data - ./uploads:/app/uploads @@ -265,6 +278,7 @@ trek.yourdomain.com { | `LOG_LEVEL` | `info` = concise user actions, `debug` = verbose details | `info` | | `ALLOWED_ORIGINS` | Comma-separated origins for CORS and email links | same-origin | | `FORCE_HTTPS` | Redirect HTTP to HTTPS behind a TLS-terminating proxy | `false` | +| `COOKIE_SECURE` | Set to `false` to allow session cookies over plain HTTP (e.g. accessing via IP without HTTPS). Defaults to `true` in production. **Not recommended to disable in production.** | `true` | | `TRUST_PROXY` | Number of trusted reverse proxies for `X-Forwarded-For` | `1` | | `ALLOW_INTERNAL_NETWORK` | Allow outbound requests to private/RFC-1918 IP addresses. Set to `true` if Immich or other integrated services are hosted on your local network. Loopback (`127.x`) and link-local/metadata addresses (`169.254.x`) are always blocked regardless of this setting. | `false` | | **OIDC / SSO** | | | @@ -273,6 +287,7 @@ trek.yourdomain.com { | `OIDC_CLIENT_SECRET` | OIDC client secret | — | | `OIDC_DISPLAY_NAME` | Label shown on the SSO login button | `SSO` | | `OIDC_ONLY` | Disable local password auth entirely (first SSO login becomes admin) | `false` | +| `OIDC_DISCOVERY_URL` | Override the auto-constructed OIDC discovery endpoint. Useful for providers that expose it at a non-standard path (e.g. Authentik: `https://auth.example.com/application/o/trek/.well-known/openid-configuration`) | — | | **Other** | | | | `DEMO_MODE` | Enable demo mode (hourly data resets) | `false` | diff --git a/chart/README.md b/chart/README.md index 54e8b90..87fc2fc 100644 --- a/chart/README.md +++ b/chart/README.md @@ -32,3 +32,5 @@ See `values.yaml` for more options. - `ENCRYPTION_KEY` encrypts stored secrets (API keys, MFA, SMTP, OIDC) at rest. Recommended: set via `secretEnv.ENCRYPTION_KEY` or `existingSecret`. If left empty, the server falls back automatically: existing installs use `data/.jwt_secret` (no action needed on upgrade); fresh installs auto-generate a key persisted to the data PVC. - If using ingress, you must manually keep `env.ALLOWED_ORIGINS` and `ingress.hosts` in sync to ensure CORS works correctly. The chart does not sync these automatically. - Set `env.ALLOW_INTERNAL_NETWORK: "true"` if Immich or other integrated services are hosted on a private/RFC-1918 address (e.g. a pod on the same cluster or a NAS on your LAN). Loopback (`127.x`) and link-local/metadata addresses (`169.254.x`) remain blocked regardless. +- Set `env.COOKIE_SECURE: "false"` only if your deployment has no TLS (e.g. during local testing without ingress). Session cookies require HTTPS in all other cases. +- Set `env.OIDC_DISCOVERY_URL` to override the auto-constructed OIDC discovery endpoint. Required for providers (e.g. Authentik) that expose it at a non-standard path. diff --git a/chart/templates/configmap.yaml b/chart/templates/configmap.yaml index 7e0a5a3..a7a4eb7 100644 --- a/chart/templates/configmap.yaml +++ b/chart/templates/configmap.yaml @@ -13,3 +13,9 @@ data: {{- if .Values.env.ALLOW_INTERNAL_NETWORK }} ALLOW_INTERNAL_NETWORK: {{ .Values.env.ALLOW_INTERNAL_NETWORK | quote }} {{- end }} + {{- if .Values.env.COOKIE_SECURE }} + COOKIE_SECURE: {{ .Values.env.COOKIE_SECURE | quote }} + {{- end }} + {{- if .Values.env.OIDC_DISCOVERY_URL }} + OIDC_DISCOVERY_URL: {{ .Values.env.OIDC_DISCOVERY_URL | quote }} + {{- end }} diff --git a/chart/values.yaml b/chart/values.yaml index 07164e3..2f82f4f 100644 --- a/chart/values.yaml +++ b/chart/values.yaml @@ -20,6 +20,10 @@ env: # ALLOW_INTERNAL_NETWORK: "false" # Set to "true" if Immich or other integrated services are hosted on a private/RFC-1918 network address. # Loopback (127.x) and link-local/metadata addresses (169.254.x) are always blocked. + # COOKIE_SECURE: "true" + # Set to "false" to allow session cookies over plain HTTP (e.g. no ingress TLS). Not recommended for production. + # OIDC_DISCOVERY_URL: "" + # Override the OIDC discovery endpoint for providers with non-standard paths (e.g. Authentik). # Secret environment variables stored in a Kubernetes Secret. diff --git a/docker-compose.yml b/docker-compose.yml index 6c2ccba..731a77c 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -23,13 +23,15 @@ services: - LOG_LEVEL=${LOG_LEVEL:-info} # info = concise user actions; debug = verbose admin-level details - ALLOWED_ORIGINS=${ALLOWED_ORIGINS:-} # Comma-separated origins for CORS and email notification links - FORCE_HTTPS=true # Redirect HTTP to HTTPS when behind a TLS-terminating proxy +# - COOKIE_SECURE=false # Uncomment if accessing over plain HTTP (no HTTPS). Not recommended for production. - TRUST_PROXY=1 # Number of trusted proxies (for X-Forwarded-For / real client IP) - ALLOW_INTERNAL_NETWORK=false # Set to true if Immich or other services are hosted on your local network (RFC-1918 IPs). Loopback and link-local addresses remain blocked regardless. - - OIDC_ISSUER=https://auth.example.com # OpenID Connect provider URL - - OIDC_CLIENT_ID=trek # OpenID Connect client ID - - OIDC_CLIENT_SECRET=supersecret # OpenID Connect client secret - - OIDC_DISPLAY_NAME=SSO # Label shown on the SSO login button - - OIDC_ONLY=false # Set true to disable local password auth entirely (SSO only) +# - OIDC_ISSUER=https://auth.example.com # OpenID Connect provider URL +# - OIDC_CLIENT_ID=trek # OpenID Connect client ID +# - OIDC_CLIENT_SECRET=supersecret # OpenID Connect client secret +# - OIDC_DISPLAY_NAME=SSO # Label shown on the SSO login button +# - OIDC_ONLY=false # Set true to disable local password auth entirely (SSO only) +# - OIDC_DISCOVERY_URL= # Override the OIDC discovery endpoint for providers with non-standard paths (e.g. Authentik) volumes: - ./data:/app/data - ./uploads:/app/uploads diff --git a/server/.env.example b/server/.env.example index 1dcabc0..35be0dd 100644 --- a/server/.env.example +++ b/server/.env.example @@ -10,6 +10,7 @@ LOG_LEVEL=info # info = concise user actions; debug = verbose admin-level detail ALLOWED_ORIGINS=https://trek.example.com # Comma-separated origins for CORS and email links FORCE_HTTPS=false # Redirect HTTP → HTTPS behind a TLS proxy +COOKIE_SECURE=true # Set to false to allow session cookies over HTTP (e.g. plain-IP or non-HTTPS setups). Defaults to true in production. TRUST_PROXY=1 # Number of trusted proxies for X-Forwarded-For ALLOW_INTERNAL_NETWORK=false # Allow outbound requests to private/RFC1918 IPs (e.g. Immich hosted on your LAN). Loopback and link-local addresses are always blocked. @@ -22,6 +23,6 @@ OIDC_DISPLAY_NAME=SSO # Label shown on the SSO login button OIDC_ONLY=true # Disable local password auth entirely (SSO only) OIDC_ADMIN_CLAIM=groups # OIDC claim used to identify admin users OIDC_ADMIN_VALUE=app-trek-admins # Value of the OIDC claim that grants admin role -OIDC_DISCOVERY_URL= # Override the auto-constructed discovery endpoint (e.g. Authentik: https://auth.example.com/application/o/trek/.well-known/openid-configuration) +OIDC_DISCOVERY_URL= # Override the auto-constructed OIDC discovery endpoint. Useful for providers (e.g. Authentik) that expose it at a non-standard path. Example: https://auth.example.com/application/o/trek/.well-known/openid-configuration DEMO_MODE=false # Demo mode - resets data hourly diff --git a/server/src/services/cookie.ts b/server/src/services/cookie.ts index 448e25c..2111221 100644 --- a/server/src/services/cookie.ts +++ b/server/src/services/cookie.ts @@ -3,7 +3,7 @@ import { Response } from 'express'; const COOKIE_NAME = 'trek_session'; function cookieOptions(clear = false) { - const secure = process.env.NODE_ENV === 'production' || process.env.FORCE_HTTPS === 'true'; + const secure = process.env.COOKIE_SECURE !== 'false' && (process.env.NODE_ENV === 'production' || process.env.FORCE_HTTPS === 'true'); return { httpOnly: true, secure,