Add admin toggles for places_autocomplete_enabled and places_details_enabled
alongside the existing places_photos_enabled, all default ON.
- adminService: getPlacesAutocomplete/updatePlacesAutocomplete, getPlacesDetails/updatePlacesDetails
- admin routes: GET/PUT /admin/places-autocomplete, /admin/places-details
- maps routes: autocomplete returns { suggestions: [], source: 'disabled' } when off;
details returns { place: null, disabled: true } when off
- authService: both flags included in getAppConfig() response
- authStore: placesAutocompleteEnabled + placesDetailsEnabled state and setters
- App.tsx: wire both flags from app-config on load
- AdminPage: two new toggle rows using var(--text-primary)/var(--border-primary) consistent with rest of UI
- i18n: all 15 locales (en, de, ar, br, cs, es, fr, hu, id, it, nl, pl, ru, zh, zhTw)
P0 — stop the bleeding:
- Honor place.image_url in MapView and TripPlannerPage to skip redundant fetchPhoto calls
- Trim Place Details field mask (drop reviews/editorialSummary from default; new getPlaceDetailsExpanded for inspector)
- Admin toggle places_photos_enabled (default ON) to kill Google photo fetches under quota pressure; Wikimedia unaffected
- Return { photoUrl: null } instead of 204 so client handles disabled state cleanly
P1 — structural fix:
- New placePhotoCache service: persistent disk cache at uploads/photos/google/<sha1>.jpg, atomic writes, stampede dedup via in-flight Map
- Migrations 105-107: google_place_photo_meta table, place_details_cache table, backfill signed Google URLs to stable proxy URLs
- getPlacePhoto rewrites to fetch image bytes directly, store on disk, return /api/maps/place-photo/:id/bytes proxy URL
- Stable proxy URLs written to places.image_url — survive container restarts, no expiry
- New GET /api/maps/place-photo/:placeId/bytes route serving cached files with long-lived Cache-Control
- Place Details DB row cache with 7-day TTL; ?refresh=1 escape hatch
- photoService fast-path: proxy URLs bypass the mapsApi round-trip and go straight to urlToBase64
Bug fixes:
- MapView now requests base64 thumbs for places with proxy image_url (markers were showing color fallback)
- createPlaceIcon accepts /api/maps/place-photo/ URLs as interim fallback while thumb generates
- setSelectedAssignmentId ReferenceError in mobile day-detail handler (use selectAssignment)
- Remove redundant decodeURIComponent on already-decoded Express route param
- Use SHA1 hash for disk filenames to prevent coords:lat:lng pseudo-ID collisions
- Add checkSsrf guard to Wikimedia byte fetch
- Tighten migration 107 LIKE filter to avoid rewriting manually-pasted Google image URLs
- Validate enabled is boolean on PUT /admin/places-photos
- Drop aggressive iconCache.clear() on every thumb arrival
Observability:
- googleFetch() wrapper counts and debug-logs every outbound Google API call with running total
Allow admins to set instance-wide defaults for temperature unit, color
mode, time format, route calculation, blur booking codes, and map tile
URL via a new Admin > User Defaults tab. Defaults are stored in
app_settings (prefixed default_user_setting_*) and applied at read time
as a fallback — user's own explicit values always take priority.
Translations added for all 16 supported languages.
- Add missing admin.ntfy.hint translation key in all 15 languages
- Add admin ntfy server hint clarifying it is the default for users
- Expose admin_ntfy_server via PreferencesMatrix so user settings
placeholder reflects the admin-configured default
- Add clear token button to admin ntfy panel (same pattern as user settings)
- Extract common.clear from settings.ntfyUrl.clearToken across all 15 languages
Adds ntfy.sh (and self-hosted instances) as a new push notification
channel with full parity to the existing webhook channel.
- Backend: NtfyConfig type, getUserNtfyConfig, getAdminNtfyConfig,
resolveNtfyUrl, sendNtfy (header-based API with Title/Priority/Tags/
Click headers), testNtfy, NTFY_EVENT_META (priority + emoji tags per
event), SSRF guard via existing checkSsrf + createPinnedDispatcher
- notificationPreferencesService: ntfy added to NotifChannel union,
IMPLEMENTED_COMBOS, getActiveChannels parser, getAvailableChannels,
ADMIN_GLOBAL_CHANNELS, and AvailableChannels interface
- notificationService: per-user ntfy dispatch after webhook block;
admin-scoped ntfy via getAdminGlobalPref for version_available events
- Routes: POST /api/notifications/test-ntfy with saved-token fallback
- authService: admin_ntfy_server/topic/token in ADMIN_SETTINGS_KEYS,
masked + encrypted on read/write
- settingsService: ntfy_token added to ENCRYPTED_SETTING_KEYS
- Frontend: ntfy topic/server/token inputs + Save/Test/Clear buttons in
NotificationsTab; admin Ntfy panel in AdminPage; testNtfy API method
- i18n: full English strings; English placeholders in 14 other locales
- Tests: resolveNtfyUrl, sendNtfy, dispatch integration, UI tests,
MSW handler for test-ntfy endpoint
- testSmtp now surfaces real nodemailer error instead of generic 'SMTP not configured' on send failure
- admin webhook test button uses correct i18n key (was showing 'Test-E-Mail senden' in all languages)
- backup created_at uses stat.mtime instead of unreliable stat.birthtime on Linux
- Add docker-dev.yml: prerelease CI for dev branch with minor/major bump
inputs; auto-continues in-flight major line via existing pre tags;
publishes floating major-pre Docker tag (e.g. 2-pre)
- Rewrite docker.yml version-bump: tag-based versioning, manual bump
inputs (auto/patch/minor/major), major guarded by confirm_major=MAJOR,
auto-finalizes in-flight prereleases; publishes floating major tag (e.g. 2)
- Inject APP_VERSION build-arg through Dockerfile so the running container
knows its real version instead of reading package.json
- Server reads APP_VERSION env in authService/adminService; exposes
is_prerelease in app config and update-check response; prerelease builds
compare against GitHub prerelease releases rather than latest stable
- Client stores isPrerelease from config; navbar shows amber version badge
on prerelease builds (left of dark-mode toggle); GitHubPanel filters out
prerelease releases unless the running build is itself a prerelease
Replaces the coarse oidc_only + allow_registration settings with four
independent toggles: password_login, password_registration, oidc_login,
oidc_registration. Each can be enabled/disabled individually in
Admin > Settings without affecting the others.
- Add resolveAuthToggles() in authService.ts as the central resolver;
falls back to legacy oidc_only/allow_registration keys when new keys
are absent (backward compat)
- OIDC_ONLY env var still works and overrides DB toggles for password_*,
with a visual lock in the admin UI when active
- Server enforces lockout prevention: cannot disable all login methods
- oidc_login gate added to OIDC /login and /callback routes
- Remove oidc_only toggle from OIDC settings panel; replaced by the
granular toggles in the Settings tab
- Add 6 new resolveAuthToggles() unit tests; fix AUTH-DB-033 error
message assertion
- Update OIDC_ONLY descriptions in README, docker-compose, Helm values,
Unraid template, and .env.example to clarify override semantics
Closes#492
- Notifications: map raw avatar filename to /uploads/avatars/ URL in
getNotifications, createNotification broadcasts, and respond handler
- Admin listUsers: include avatar field in SELECT and map to avatar_url
- Admin page: render actual avatar image instead of initial letter only
- Budget loadItemMembers: map avatar to avatar_url (fixed in prior commit)
Fixes#507
Three distinct bugs caused infinite OIDC redirect loops:
1. After logout, navigating to /login with no signal to suppress the
auto-redirect caused the login page to immediately re-trigger the
OIDC flow. Fixed by passing `{ state: { noRedirect: true } }` via
React Router's navigation state (not URL params, which were fragile
due to async cleanup timing) from all logout call sites.
2. On the OIDC callback page (/login?oidc_code=...), App.tsx's
mount-level loadUser() fired concurrently with the LoginPage's
exchange fetch. The App-level call had no cookie yet and got a 401,
which (if it resolved after the successful exchange loadUser()) would
overwrite isAuthenticated back to false. Fixed by skipping loadUser()
in App.tsx when the initial path is /login.
3. React 18 StrictMode double-invokes useEffect. The first run called
window.history.replaceState to clean the oidc_code from the URL
before starting the async exchange, so the second run saw no
oidc_code and fell through to the getAppConfig auto-redirect, firing
window.location.href = '/api/auth/oidc/login' before the exchange
could complete. Fixed by adding a useRef guard to prevent
double-execution and moving replaceState into the fetch callbacks so
the URL is only cleaned after the exchange resolves.
Also adds login.oidcLoggedOut translation key in all 14 languages to
show "You have been logged out" instead of the generic OIDC-only
message when landing on /login after an intentional logout.
Closes#491
Encrypted webhook URLs are no longer returned to the frontend. Both user
and admin webhook fields now show '••••••••' as a placeholder when a URL
is already saved, and the sentinel value is skipped on save/test so the
stored secret is never exposed or accidentally overwritten.
Introduces a fully featured notification system with three delivery
channels (in-app, email, webhook), normalized per-user/per-event/
per-channel preferences, admin-scoped notifications, scheduled trip
reminders and version update alerts.
- New notificationService.send() as the single orchestration entry point
- In-app notifications with simple/boolean/navigate types and WebSocket push
- Per-user preference matrix with normalized notification_channel_preferences table
- Admin notification preferences stored globally in app_settings
- Migration 69 normalizes legacy notification_preferences table
- Scheduler hooks for daily trip reminders and version checks
- DevNotificationsPanel for testing in dev mode
- All new tests passing, covering dispatch, preferences, migration, boolean
responses, resilience, and full API integration (NSVC, NPREF, INOTIF,
MIGR, VNOTIF, NROUTE series)
- Previous tests passing
* add test suite, mostly covers integration testing, tests are only backend side
* workflow runs the correct script
* workflow runs the correct script
* workflow runs the correct script
* unit tests incoming
* Fix multer silent rejections and error handler info leak
- Revert cb(null, false) to cb(new Error(...)) in auth.ts, collab.ts,
and files.ts so invalid uploads return an error instead of silently
dropping the file
- Error handler in app.ts now always returns 500 / "Internal server
error" instead of forwarding err.message to the client
* Use statusCode consistently for multer errors and error handler
- Error handler in app.ts reads err.statusCode to forward the correct
HTTP status while keeping the response body generic
Introduces a full in-app notification system with three types (simple,
boolean with server-side callbacks, navigate), three scopes (user, trip,
admin), fan-out persistence per recipient, and real-time push via
WebSocket. Includes a notification bell in the navbar, dropdown, dedicated
/notifications page, and a dev-only admin tab for testing all notification
variants.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
The test-smtp button filtered out empty SMTP user/password values
before saving, preventing unauthenticated SMTP setups from working.
Changed filter from truthy check to !== undefined so empty strings
are properly persisted.
- MCP tokens tab only shown when MCP addon is enabled
- Permissions panel moved from own tab to users tab below invite links
- Fixed inconsistent dropdown widths in permissions panel
Replace duplicated inline validation with a shared validatePassword()
utility that checks minimum length (8), rejects repetitive and common
passwords, and requires uppercase, lowercase, a digit, and a special
character.
- Add server/src/services/passwordPolicy.ts as single source of truth
- Apply to registration, password change, and admin create/edit user
(admin routes previously had zero validation)
- Fix client min-length mismatch (6 vs 8) in RegisterPage and LoginPage
- Add client-side password length guard to AdminPage forms
- Update register.passwordTooShort and settings.passwordWeak i18n keys
in all 12 locales to reflect the corrected requirements
Introduces a dedicated ENCRYPTION_KEY for encrypting stored secrets
(API keys, MFA TOTP, SMTP password, OIDC client secret) so that
rotating the JWT signing secret no longer invalidates encrypted data,
and a compromised JWT_SECRET no longer exposes stored credentials.
- server/src/config.ts: add ENCRYPTION_KEY (auto-generated to
data/.encryption_key if not set, same pattern as JWT_SECRET);
switch JWT_SECRET to `export let` so updateJwtSecret() keeps the
CJS module binding live for all importers without restart
- apiKeyCrypto.ts, mfaCrypto.ts: derive encryption keys from
ENCRYPTION_KEY instead of JWT_SECRET
- admin POST /rotate-jwt-secret: generates a new 32-byte hex secret,
persists it to data/.jwt_secret, updates the live in-process binding
via updateJwtSecret(), and writes an audit log entry
- Admin panel (Settings → Danger Zone): "Rotate JWT Secret" button
with a confirmation modal warning that all sessions will be
invalidated; on success the acting admin is logged out immediately
- docker-compose.yml, .env.example, README, Helm chart (values.yaml,
secret.yaml, deployment.yaml, NOTES.txt, README): document
ENCRYPTION_KEY and its upgrade migration path
Addresses CWE-598: long-lived JWTs were exposed in WebSocket URLs, file
download links, and Immich asset proxy URLs, leaking into server logs,
browser history, and Referer headers.
- Add ephemeralTokens service: in-memory single-use tokens with per-purpose
TTLs (ws=30s, download/immich=60s), max 10k entries, periodic cleanup
- Add POST /api/auth/ws-token and POST /api/auth/resource-token endpoints
- WebSocket auth now consumes an ephemeral token instead of verifying the JWT
directly from the URL; client fetches a fresh token before each connect
- File download ?token= query param now accepts ephemeral tokens; Bearer
header path continues to accept JWTs for programmatic access
- Immich asset proxy replaces authFromQuery JWT injection with ephemeral token
consumption
- Client: new getAuthUrl() utility, AuthedImg/ImmichImg components, and async
onClick handlers replace the synchronous authUrl() pattern throughout
FileManager, PlaceInspector, and MemoriesPanel
- Add OIDC_DISCOVERY_URL env var and oidc_discovery_url DB setting to allow
overriding the auto-constructed discovery endpoint (required for Authentik
and similar providers); exposed in the admin UI and .env.example
The POST /api/admin/update endpoint ran git pull, npm install, and npm run build via execSync, potentially giving any compromised admin account full code execution on the host in case repository is compromised. TREK ships as a Docker image so runtime self-updating is unnecessary.
- Remove the /update route and child_process import from admin.ts
- Remove the installUpdate API client method
- Replace the live-update modal with an info-only panel showing docker pull instructions and a link to the GitHub release
- Drop the updating/updateResult state and handleInstallUpdate handler
Adds a full permissions management feature allowing admins to control
who can perform actions across the app (trip CRUD, files, places,
budget, packing, reservations, collab, members, share links).
- New server/src/services/permissions.ts: 16 configurable actions,
in-memory cache, checkPermission() helper, backwards-compatible
defaults matching upstream behaviour
- GET/PUT /admin/permissions endpoints; permissions loaded into
app-config response so clients have them on startup
- checkPermission() applied to all mutating route handlers across
10 server route files; getTripOwnerId() helper eliminates repeated
inline DB queries; trips.ts and files.ts now reuse canAccessTrip()
result to avoid redundant DB round-trips
- New client/src/store/permissionsStore.ts: Zustand store +
useCanDo() hook; TripOwnerContext type accepts both Trip and
DashboardTrip shapes without casting at call sites
- New client/src/components/Admin/PermissionsPanel.tsx: categorised
UI with per-action dropdowns, customised badge, save/reset
- AdminPage, DashboardPage, FileManager, PlacesSidebar,
TripMembersModal gated via useCanDo(); no prop drilling
- 46 perm.* translation keys added to all 12 language files
- Add configurable trip reminder days (1, 3, 9 or custom up to 30) settable by trip owner
- Grant administrators full access to edit, archive, delete, view and list all trips
- Show trip owner email in audit logs and docker logs when admin edits/deletes another user's trip
- Show target user email in audit logs when admin edits or deletes a user account
- Use email instead of username in all notifications (Discord/Slack/email) to avoid ambiguity
- Grey out notification event toggles when no SMTP/webhook is configured
- Grey out trip reminder selector when notifications are disabled
- Skip local admin account creation when OIDC_ONLY=true with OIDC configured
- Conditional scheduler logging: show disabled reason or active reminder count
- Log per-owner reminder creation/update in docker logs
- Demote 401/403 HTTP errors to DEBUG log level to reduce noise
- Hide edit/archive/delete buttons for non-owner invited users on trip cards
- Fix literal "0" rendering on trip cards from SQLite numeric is_owner field
- Add missing translation keys across all 14 language files
Made-with: Cursor
- Add centralized notification service with webhook (Discord/Slack) and
email (SMTP) support, triggered for trip invites, booking changes,
collab messages, and trip reminders
- Webhook sends one message per event (group channel); email sends
individually per trip member, excluding the actor
- Discord invite notifications now include the invited user's name
- Add LOG_LEVEL env var (info/debug) controlling console and file output
- INFO logs show user email, action, and IP for audit events; errors
for HTTP requests
- DEBUG logs show every request with full body/query (passwords redacted),
audit details, notification params, and webhook payloads
- Add persistent trek.log file logging with 10MB rotation (5 files)
in /app/data/logs/
- Color-coded log levels in Docker console output
- Timestamps without timezone name (user sets TZ via Docker)
- Add Test Webhook and Save buttons to admin notification settings
- Move notification event toggles to admin panel
- Add daily trip reminder scheduler (9 AM, timezone-aware)
- Wire up booking create/update/delete and collab message notifications
- Add i18n keys for notification UI across all 13 languages
Made-with: Cursor
Add an admin-controlled `require_mfa` policy in App Settings and expose it via `/auth/app-config` so the client can enforce it globally. Users without MFA are redirected to Settings after login and blocked from protected API/WebSocket access until setup is completed, while preserving MFA setup endpoints and admin recovery paths. Also prevent enabling the policy unless the acting admin already has MFA enabled, and block MFA disable while the policy is active. Includes UI toggle in Admin > Settings, required-policy notice in Settings, client-side 403 `MFA_REQUIRED` handling, and i18n updates for all supported locales.
- Language change on public shared page no longer triggers API call / login redirect
- New "Skip TLS certificate check" toggle in Admin > SMTP settings
- Also configurable via SMTP_SKIP_TLS_VERIFY=true env var
Audit logging for admin actions, backups, auth events.
New AuditLogPanel in Admin tab with pagination.
Dockerfile security: run as non-root user.
i18n keys for all 9 languages.
Thanks @fgbona for the implementation!
Email Notifications:
- SMTP configuration in Admin > Settings (host, port, user, pass, from)
- App URL setting for email CTA links
- Webhook URL support (Discord, Slack, custom)
- Test email button with SMTP validation
- Beautiful HTML email template with TREK logo, slogan, red heart footer
- All notification texts translated in 8 languages (en/de/fr/es/nl/ru/zh/ar)
- Emails sent in each user's language preference
Notification Events:
- Trip invitation (member added)
- Booking created (new reservation)
- Vacay fusion invite
- Photos shared (Immich)
- Collab chat message
- Packing list category assignment
User Notification Preferences:
- Per-user toggle for each event type in Settings
- Addon-aware: Vacay/Collab/Photos toggles hidden when addon disabled
- Webhook opt-in per user
ICS Calendar Export:
- Download button next to PDF in day plan header
- Exports trip dates + all reservations with details
- Compatible with Google Calendar, Apple Calendar, Outlook
Technical:
- Nodemailer for SMTP
- notification_preferences DB table with per-event columns
- GET/PUT /auth/app-settings for admin config persistence
- POST /notifications/test-smtp for validation
- Dynamic imports for non-blocking notification sends
- New display setting "Blur Booking Codes" (off by default)
- When enabled, confirmation codes are blurred across all views
(ReservationsPanel, DayDetailPanel, Transport detail modal)
- Hover or click reveals the code (click toggles on mobile)
- Settings page uses masonry two-column layout on desktop, single
column on mobile (<900px)
- Fix hardcoded admin page title to use i18n key
Add UI controls for configuring auto-backup schedule with hour, day of
week, and day of month pickers. The hour picker respects the user's
12h/24h time format preference from settings.
Add TZ environment variable support via docker-compose so the container
runs in the configured timezone. The timezone is passed to node-cron for
accurate scheduling and exposed via the API so the UI displays it.
Fix SQLite UTC timestamp handling by appending Z suffix to all timestamps
sent to the client, ensuring proper timezone conversion in the browser.
Made-with: Cursor
Audit log
- Add audit_log table (migration + schema) with index on created_at.
- Add auditLog service (writeAudit, getClientIp) and record events for backups
(create, restore, upload-restore, delete, auto-settings), admin actions
(users, OIDC, invites, system update, demo baseline, bag tracking, packing
template delete, addons), and auth (app settings, MFA enable/disable).
- Add GET /api/admin/audit-log with pagination; fix invite insert row id lookup.
- Add AuditLogPanel and Admin tab; adminApi.auditLog.
- Add admin.tabs.audit and admin.audit.* strings in all locale files.
Note: Rebase feature branches so new DB migrations stay after existing ones
(e.g. file_links) when merging upstream.
Admins can create one-time registration links (1–5× or unlimited uses)
with optional expiry (1d–14d or never). Recipients can register even
when public registration is disabled. Atomic usage counting prevents
race conditions, all endpoints are rate-limited.
When OIDC is configured, admins can now enable 'Disable password
authentication' in Admin → Settings → SSO. This blocks all password-
based login and registration, forcing users through the SSO identity
provider instead.
Backend:
- routes/admin.ts: expose oidc_only flag on GET /admin/oidc and accept
it on PUT /admin/oidc (persisted to app_settings)
- routes/auth.ts: add isOidcOnlyMode() helper; block POST /auth/login,
POST /auth/register (for non-first-user), and PUT /auth/me/password
with HTTP 403 when OIDC-only mode is active
- routes/auth.ts: expose oidc_only_mode boolean in GET /auth/app-config
Frontend:
- AdminPage: toggle in OIDC/SSO settings section (oidc_only saved with
rest of OIDC config on same Save button)
- LoginPage: when oidc_only_mode is active, replace form with a
single-button OIDC redirect; hide register toggle
- SettingsPage: hide password change section when oidc_only_mode is on
- i18n (en/de): admin.oidcOnlyMode, admin.oidcOnlyModeHint,
login.oidcOnly