Before swapping in a restored database, run PRAGMA integrity_check and
verify the five core TREK tables (users, trips, trip_members, places,
days) are present. This blocks restoring corrupt, empty, or unrelated
SQLite files that would otherwise crash the app immediately after swap,
and prevents a malicious admin from hot-swapping a crafted database with
forged users or permissions.
Per-user Immich API keys were stored as plaintext in the users table,
giving any attacker with DB read access full control over each user's
Immich photo server. Keys are now encrypted on write with
maybe_encrypt_api_key() and decrypted at the point of use via a shared
getImmichCredentials() helper. A new migration (index 66) back-fills
encryption for any existing plaintext values on startup.
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 OIDC login route silently fell back to building the redirect URI from
X-Forwarded-Host/X-Forwarded-Proto when APP_URL was not configured. An
attacker could set X-Forwarded-Host: attacker.example.com to redirect the
authorization code to their own server after the user authenticates.
Remove the header-derived fallback entirely. If APP_URL is not set (via env
or the app_url DB setting), the OIDC login endpoint now returns a 500 error
rather than trusting attacker-controlled request headers. Document APP_URL
in .env.example as required for OIDC use.
The smtp_pass setting was stored as plaintext in app_settings, exposing
SMTP credentials to anyone with database read access. Apply the same
encrypt_api_key/decrypt_api_key pattern already used for OIDC client
secrets and API keys. A new migration transparently re-encrypts any
existing plaintext value on startup; decrypt_api_key handles legacy
plaintext gracefully so in-flight reads remain safe during upgrade.
The oidc_client_secret was written to app_settings as plaintext,
unlike Maps and OpenWeather API keys which are protected with
apiKeyCrypto. An attacker with read access to the SQLite file
(e.g. via a backup download) could obtain the secret and
impersonate the application with the identity provider.
- Encrypt on write in PUT /api/admin/oidc via maybe_encrypt_api_key
- Decrypt on read in GET /api/admin/oidc and in getOidcConfig()
(oidc.ts) before passing the secret to the OIDC client library
- Add a startup migration that encrypts any existing plaintext value
already present in the database
getPlaceWithTags, canAccessTrip, and isOwner were calling _db! directly,
bypassing the Proxy that guards against null-dereference during a backup
restore. When the restore handler briefly sets _db = null, any concurrent
request hitting these helpers would crash with an unhandled TypeError
instead of receiving a clean 503-style error.
Replace all four _db! accesses with the exported db proxy so the guard
("Database connection is not available") fires consistently.
Previously, the migration runner called each migration function directly with no transaction wrapping and updated schema_version only after all pending migrations had run. A mid-migration failure (e.g. disk full after ALTER TABLE but before CREATE INDEX) would leave the schema in a partially-applied state with no rollback path. On the next restart the broken migration would be skipped — because schema_version had not advanced — but only if the failure was noticed at all.
~43 catch {} blocks silently discarded every error, including non-idempotency errors such as disk-full or corruption, making it impossible to know a migration had failed.
Changes:
- Each migration now runs inside db.transaction(); better-sqlite3 rolls back automatically on throw.
- schema_version is updated after every individual migration succeeds, so a failure does not cause already-applied migrations to re-run.
- A migration that throws after rollback logs FATAL and calls process.exit(1), refusing to start with a broken schema.
- All catch {} blocks on ALTER TABLE ADD COLUMN re-throw any error that is not "duplicate column name", so only the expected idempotency case is swallowed.
- Genuinely optional steps (INSERT OR IGNORE, UPDATE data-copy, DROP TABLE IF EXISTS) now log a warning instead of discarding the error entirely.
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
On mobile/touch devices, Leaflet tooltips disappear immediately on tap
since there is no hover state. This makes the info bubble permanent for
the selected marker on touch devices so it stays readable.
Fixes#249
- Suppress note context menu when canEditDays is false instead of
showing empty menu
- Untie poll voting from collab_edit — voting is participation, not
editing; any trip member can vote
- Restore NoteFormModal props (note, tripId) to required; remove
leftover canUploadFiles prop in favor of direct zustand hook
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
Bounding boxes overlap for neighboring countries (e.g. Munich matched
Austria instead of Germany). Now uses Nominatim reverse geocoding with
in-memory cache as primary fallback, bounding boxes only as last resort.
- Block direct access to /uploads/files (401), serve via authenticated
/api/trips/:tripId/files/:id/download with JWT verification
- Client passes auth token as query parameter for direct links
- Atlas country search now uses Intl.DisplayNames (user language) instead
of English GeoJSON names
- Atlas search results use flagcdn.com flag images instead of emoji
- 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