mirror of
https://github.com/mauriceboe/TREK.git
synced 2026-06-20 13:51:45 +00:00
abe1c549bded6d691e6af6f040ba96d0faf6624c
15 Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
20791a29a7 |
Migrate TREK 3 to NestJS + React 19 (shared Zod contracts) (#1087)
* Migrate TREK 3 to NestJS + React 19 with a shared Zod contract layer
Brownfield strangler migration of the backend onto NestJS modules
(auth, trips, days, places, assignments, packing, todo, budget,
reservations, collab, files, photos, journey, share, settings, backup,
oidc, oauth, admin, atlas, vacay, weather, airports, maps, categories,
tags, notifications, system-notices) served through a per-prefix
dispatcher, keeping the existing SQLite/better-sqlite3 DB and JWT
httpOnly cookie auth, with behavioural parity for every route.
Client: React 19 upgrade, "page = wiring container + data hook"
pattern across all pages, per-domain Zustand stores bound to
@trek/shared contracts, and decomposition of the large components
(DayPlanSidebar, PackingListPanel, CollabNotes, FileManager,
MemoriesPanel, PlacesSidebar, CollabChat, SystemNoticeModal,
BudgetPanel, PlaceFormModal, ...) into focused render units backed by
in-file hooks.
Apply the shared global request pipeline (helmet/CSP, CORS, HSTS,
forced HTTPS, the global MFA policy and request logging) to the NestJS
instance as well, so a migrated route is protected identically to the
legacy fallback rather than bypassing it.
* Finish the NestJS migration — drop the legacy Express app
NestJS now serves the whole surface: every /api domain plus the platform
routes (uploads, /mcp, the OAuth/MCP SDK + /.well-known metadata and the
production SPA fallback). Removed server/src/app.ts, all of
server/src/routes/* and the strangler dispatcher; index.ts and the
integration suite share a single buildApp() bootstrap so prod and tests
can't drift.
- Platform/transport routes extracted to nest/platform/platform.routes.ts
and mounted before app.init() — Nest's router answers an unmatched
request with a 404, so a route registered after init is never reached.
The SPA fallback is a NotFoundException filter and the catch-all uses a
RegExp (Express 5's path-to-regexp rejects a bare '*').
- New modules: memories (/api/integrations/memories — the Journey
gallery's Immich/Synology proxy), addons (GET /api/addons) and the
cross-trip GET /api/reservations/upcoming.
- TrekExceptionFilter reproduces the old multer / err.statusCode handling
so upload rejections keep their 400/413 { error } body and non-ASCII
filenames survive (defParamCharset).
- addTripToJourney and the MCP get_journey_share_link tool gained the
trip-access check they were missing.
- Re-pointed the 34 integration tests + the websocket test onto the Nest
app; removed the now-meaningless Express-vs-Nest parity tests and a few
orphaned client components.
* Restore the reset-password rate limit and fix copyTrip reservation links
Two correctness/security gaps the NestJS migration introduced:
- POST /api/auth/reset-password lost its per-IP rate limiter. Restore it
(5 attempts / 15 min on a dedicated bucket, same as the old resetLimiter)
so reset tokens can't be brute-forced unthrottled. Covered by AUTH-019.
- copyTripById did not copy reservations.end_day_id (a day reference — now
remapped through dayMap like day_id) or needs_review, so a duplicated trip
lost multi-day transport end-day links and reset the review flag.
* Clean up dead code, dedupe helpers, fix the reset-password contract
- Remove server exports orphaned by the Express removal: the immich
album-link helpers, seven route-only service exports, getFileByIdFull;
de-export internal-only helpers (utcSuffix).
- De-duplicate verifyTripAccess (9 identical copies -> services/tripAccess.ts)
and avatarUrl (3 -> services/avatarUrl.ts); name the bcrypt cost
(BCRYPT_COST) and the email regex (EMAIL_REGEX). Public API unchanged.
- resetPasswordRequestSchema declared `password`, but the client sends and
the service reads `new_password` — rename it so the contract matches and
the client types resolve.
- Make ATLAS-013 deterministic: stub the admin-1 GeoJSON download instead of
fetching ~4600 features from GitHub during the test (it hung the suite).
* Make the client typecheck runnable (vitest/vite ambient types)
The client had no `typecheck` script and tsc couldn't even start (the
baseUrl deprecation errored out, same as server/shared already silence).
Add `ignoreDeprecations: "6.0"` to match the other workspaces, a `typecheck`
npm script, and a src/vite-env.d.ts referencing vite/client + vitest/globals
so tsc knows the test globals (describe/it/expect/vi). This turns ~3600
phantom "Cannot find name" errors into a real, measurable count (~590 actual
type errors remain, to be worked down). Type-only; no runtime change.
* Derive client domain types from the shared schema contracts
Add entity/response Zod schemas to @trek/shared (place, trip, assignment, day, budget, packing, reservation), each matched against the producing server service, and re-export them from client types.ts instead of the hand-written duplicates that had drifted (name/title, amount/total_price, owner_id/user_id, cover_url/cover_image, ...). Updates the call sites and test fixtures the corrected types surfaced; type-only, no runtime behaviour change.
* chore(db): log swallowed errors in addon-disable migration + guard against destructive migrations
The migration that disables the legacy "memories" addon swallowed any
error in an empty catch, as did ~30 other catch blocks in the migration
runner (column adds, the journey rebuild, index probes). Replace each
silent catch with the existing console.warn('[migrations] ...') log so
failures are visible. Control flow is unchanged: every step stays
non-fatal, nothing new is thrown.
Add a static guardrail test that scans the migration source and fails
when a new destructive statement (DROP TABLE / DROP COLUMN / TRUNCATE /
DELETE FROM / ALTER ... DROP) appears outside a reviewed allowlist, and
when an empty/silent catch block is reintroduced. The existing
destructive statements are all legitimate table rebuilds or
bounded cleanups and are recorded in the allowlist with a reason.
* Re-check SSRF on every redirect hop when resolving short links
Replace the one-shot checkSsrf + fetch(redirect:'follow') in the maps and place short-link resolvers with safeFetchFollow, which follows redirects manually and re-runs checkSsrf against the DNS-pinned IP of each hop (max 5). A redirect to an internal/loopback address is now blocked even when the initial URL is public, while legitimate cross-host redirects (goo.gl -> maps.google.com) still resolve.
* Reject WebSocket tokens minted before a password change
Stamp the user's password_version onto the ephemeral ws token and verify it on connect, closing the socket (4001) when it no longer matches, so a token issued before a password reset can't be replayed. Tokens minted without a version are treated as version 0, matching the JWT pv-claim semantics.
* fix(i18n): guard locale key parity and finish the OAuth consent page strings
Every non-en locale now exposes the exact same flat key set as en. Keys that
had drifted out of sync are backfilled with the English source value (tagged
en-fallback) so t() resolves a real string instead of relying on the silent
runtime fallback; no existing translation was touched and no key was removed.
Add a parity test that imports each aggregated locale bundle and asserts its
key set matches en, with a diagnostic listing of any missing/extra keys. This
complements the file-level check in shared/scripts by guarding the merged
export the app actually serves.
Finish internationalising OAuthAuthorizePage: the ~15 remaining hardcoded
English chrome strings now go through oauth.authorize.* keys (English source
in en, en-fallback placeholders elsewhere). Markup and behaviour are unchanged.
* Add semantic theme color tokens to Tailwind
Map the CSS theme variables from src/index.css (:root light / .dark dark) to named Tailwind utilities — bg-surface, text-content, border-edge, bg-accent and their variants. This gives components a Tailwind-native target for the theme colors so we can replace inline `style={{ ... 'var(--...)' }}` with utility classes without changing the rendered values.
* Surface silent store failures to the user and validate API responses in dev
Reservation toggle, todo/packing toggle and budget reorder were swallowing API errors after rolling back, so the user saw the change silently snap back with no explanation. Route those failures through the existing toast channel (new store/notify.ts bridges to window.__addToast, the same channel SystemNoticeBanner uses); the reservation toggle re-throws so ReservationsPanel's own translated toast finally fires. Also wire the existing parseInDev/checkInDev response validation into the maps and notification-test endpoints to catch contract drift in dev.
* Migrate static theme inline styles to Tailwind utilities and extract page sub-components
Replace the static, color-only inline `style={{ ... 'var(--bg-primary)' ... }}` props with the new semantic Tailwind utilities (bg-surface, text-content, border-edge, ...) wherever the result is byte-identical; dynamic/conditional theme styles and hardcoded status colors are left inline. Extract the Atlas country-search autocomplete, the Admin update banner, and two Journey dialogs into their own presentational components to shrink the oversized page files, keeping behaviour and markup identical.
* Remove the unrouted photos page and its dead photo components
PhotosPage was never wired into the router and its usePhotos hook read a tripStore photos slice that was never implemented; the Photos gallery, lightbox and upload components were only reachable through it. Per-trip photos now live in the Journey gallery (Immich/Synology). Removed the dead page, hook and components — the live Journey PhotoLightbox is a separate component and stays.
* Resolve the remaining client type errors and the trip.title navbar bug
Drive the client typecheck to zero without any/ts-ignore: convert the tripId route param to a number once at the page boundary so it matches the numeric props and store actions it feeds, fix trip.name -> trip.title (the wire field is title, so the old read rendered blank in the files/offline views), and tighten the scattered handler-arity, DOM-cast and untyped-payload sites. No runtime behaviour change.
* Convert the remaining dynamic and hardcoded inline styles to Tailwind utilities
Second styling pass over the components and pages: move conditional theme colors into className ternaries (bg-accent / bg-surface-hover etc.), turn reused CSSProperties constants into className constants, and express static hardcoded hex/rgba colors as Tailwind arbitrary values so the exact rendered colour is preserved. Truly dynamic styling (computed geometry, gradients, multi-part shadows, data-driven colours, the undefined --sidebar/--nav layout vars) stays inline as it cannot be expressed as a static class. Updated three component tests that asserted the old inline active-state styles to assert the equivalent utility class instead.
Verified: client typecheck 0, full client suite green, and a live light/dark render check in the dev server confirms the semantic theme tokens resolve correctly (the earlier 'transparent popups' were a stale dev server that pre-dated the tailwind.config token addition, not a code issue).
* Add eslint flat-config for client and server and gate typecheck, lint and pages in CI
client and server had lint scripts but no eslint config (only shared was linted in CI). Add flat configs mirroring shared's stack (js + typescript-eslint recommended + eslint-config-prettier) plus the client's react-hooks/react-refresh plugins. Pre-existing patterns in this never-linted code (explicit any, require() in the CommonJS server, empty catches, exhaustive-deps) are set to 'warn' rather than 'error' so the gate passes at 0 errors without a repo-wide reformat — these can be ratcheted to errors over time. Wire blocking typecheck + lint + lint:pages steps into the client and server CI jobs (now that both typechecks are clean) and promote the server typecheck from informational to blocking.
* Decompose the remaining God Components into hooks, helpers and sub-components
FE6: split the oversized page and panel components into thin layout shells plus colocated use<Component> hooks, .constants.ts, .helpers.ts (with tests) and presentational sub-components, following the established 'logic in a hook, render in slices' pattern. Behaviour, markup, classes and effect order are unchanged. Largest reductions: PackingListPanel 1598->42, FileManager 1055->36, AdminPage 1525->167, BudgetPanel 1266->146, JourneyDetailPage 2822->547, PlacesSidebar 945->66, CollabChat 861->106, CollabNotes 1417->532. DayPlanSidebar's drag-and-drop render body was left intact (ref-identity sensitive) and only its toolbar/modals/constants were extracted.
* Fix duplicate React keys in the file-assign place list
When a place is assigned to the same day more than once it appeared twice in a day's list, so the place-button key={p.id} collided and React warned about duplicate keys. Key by place id + render index so siblings stay unique. Pre-existing in the old FileManager; behaviour unchanged.
* Format the shared package and drop an unused import to satisfy the lint gate
The i18n and schema changes added code that wasn't prettier-formatted, and place.schema.ts imported categorySchema without using it. Run prettier over shared and remove the import so 'npm run lint' + 'format:check' pass.
* Install all workspaces in the server CI job so SWC's native binary is present
The server vitest config transforms via unplugin-swc, which needs @swc/core's platform-specific native binary. A workspace-scoped 'npm ci --workspace server' skips that optional dependency, so vitest failed to load the config on the Linux runner. Use a full 'npm ci'.
* Re-resolve dependencies with npm install in the server CI job for SWC
Full 'npm ci' still skipped @swc/core's Linux native binary because the committed lockfile was generated on Windows and lacks the Linux optional-dep install metadata. 'npm install' re-resolves and fetches the platform-matching binary, which the server's unplugin-swc transform needs to load vitest.config.ts.
* Install @swc/core's Linux binary explicitly in the server CI job
Neither npm ci nor npm install fetched @swc/core-linux-x64-gnu on the Linux runner because the lockfile was generated on Windows and lacks the Linux optional-dep metadata. Add a step that installs the matching @swc/core-linux-x64-gnu version (no-save, no-lockfile) so unplugin-swc can load the server's vitest config.
* Use legacy-peer-deps when installing the SWC Linux binary in CI
The explicit @swc/core-linux-x64-gnu install re-resolved the tree and hit the pre-existing lucide-react/react-19 peer conflict that the lockfile was generated around. Add --legacy-peer-deps so the step matches the project's resolution and installs the binary.
* Keep the lockfile when installing the SWC binary so other deps stay pinned
Dropping --no-package-lock made npm re-resolve the whole tree and upgrade eslint, whose newer recommended config flagged no-useless-assignment as an error in the server lint step. Keep the lockfile so only @swc/core-linux-x64-gnu is added and every other dependency (incl. eslint) stays at its locked version.
|
||
|
|
1f5deeba6c |
Bug fixes - April 27th 2026 (#907)
* fix: clean up dangling FK references before deleting a user Resolves FOREIGN KEY constraint failed (500) on DELETE /api/admin/users/:id and DELETE /api/auth/me when the target user had rows in trip_members.invited_by, share_tokens.created_by, budget_items.paid_by_user_id, journeys.user_id, journey_entries.author_id, journey_contributors.user_id, or journey_share_tokens.created_by — none of which had ON DELETE clauses. Introduces deleteUserCompletely() in userCleanupService.ts which wraps all cleanup and the final DELETE FROM users in a single transaction. Both adminService.deleteUser and authService.deleteAccount now call it instead of the bare DELETE. Tests ADMIN-005b and AUTH-040 cover all reference types including notification sender/recipient and notice dismissals. * test: extend FK deletion tests to cover journeys, files, and photos ADMIN-005b and AUTH-040 now also seed and assert: - owned journey with entries (cascade-deleted via journeys.user_id cleanup) - trip_files.uploaded_by (SET NULL — file survives, attribution cleared) - trek_photos.owner_id (SET NULL — photo record survives, owner cleared) - trip_photos.user_id (CASCADE — photo association removed) * test: extend user deletion tests to cover all FK relationships ADMIN-005b and AUTH-040 now seed and assert every user FK relationship: CASCADE (row deleted): trips, trip_members, tags, mcp_tokens, oauth_tokens, oauth_consents, vacay_plans, vacay_plan_members, bucket_list, visited_countries, visited_regions, packing_templates, invite_tokens, collab_notes, settings, password_reset_tokens, notification_channel_preferences SET NULL (row survives, column nulled): categories, todo_items.assigned_user_id, packing_bags, audit_log Caught and fixed: notification_preferences was dropped in migration 72; correct table is notification_channel_preferences. * fix: preserve URL hash and OIDC redirect target through login flow - Include location.hash in redirect param at all three producer sites (ProtectedRoute, axios 401 interceptor, OAuthAuthorizePage) so hash fragments survive the login bounce - Stash redirectTarget in sessionStorage before any OIDC provider redirect and restore it after the code exchange, since the IdP strips the original ?redirect= param during the roundtrip - Clear sessionStorage on OIDC error to avoid stale state - Add tests covering sessionStorage stash on mount, navigate to saved redirect after OIDC exchange, fallback to /dashboard, and cleanup on error * fix: use day position instead of ID for accommodation date range clamping Math.min/Math.max over raw day IDs breaks the start/end picker when a trip's day IDs are non-monotonic relative to day_number (normal after repeated generateDays extend/shrink cycles). Replaced with findIndex lookups so clamping is always based on positional order. Closes #889 * fix: normalize env var comparisons to be case-insensitive All NODE_ENV, DEMO_MODE, OIDC_ONLY, FORCE_HTTPS, COOKIE_SECURE, and ALLOW_INTERNAL_NETWORK checks now use .toLowerCase() so values like 'Production' or 'True' behave identically to their lowercase forms. Also adds APP_VERSION to the startup banner. * fix: delete surplus days when shortening a trip When shrinking a trip's date range, surplus days are now deleted along with their assignments, notes, and accommodations (cascade). Places remain in the trip pool; reservations keep their day reference nulled by the existing ON DELETE SET NULL constraint (issue #909). Updates TRIP-SVC-011 to reflect the new behaviour; adds TRIP-SVC-016 as a regression test for the empty-day case. * fix: auto-backup retention deletes itself and manual backups on Docker Two bugs in cleanupOldBackups: 1. Filter was .endsWith('.zip') — swept manual backup-*.zip files too. Now restricted to auto-backup-* prefix. 2. Age was derived from stat.birthtimeMs, which is 0 on overlayfs (Docker default), making every backup appear epoch-old and get deleted immediately. Age is now parsed from the filename timestamp and falls back to mtimeMs (reliable on overlayfs). Also converts inline require('./services/auditLog') calls to a static import throughout scheduler.ts, and adds 8 unit tests covering the fixed retention logic including the overlayfs regression case. * test: update TRIP-024 to match delete behavior on trip shrink * feat: add bypass-branch-check label to skip branch enforcement |
||
|
|
292e443dbe |
security: address silent-failure review findings on top of batch 1
Second-pass fixes caught by a self-review after the initial commit — each one would have undermined a fix from the previous commit. - mfaPolicy now goes through `verifyJwtAndLoadUser` too. Without this, a JWT stolen before a password reset still satisfied `require_mfa` until its natural 24h expiry, defeating the whole point of the password_version bump. - Drop the `?? keys[0]` fallback in OIDC JWKS key selection. When the token carries a `kid` that is not in the current JWKS, refuse outright instead of picking an arbitrary key and letting the signature check produce a generic failure — the real failure mode deserves a specific error code. - Tighten OAuth DCR custom-scheme rule so `javascript:`, `data:`, `vbscript:`, `file:`, `blob:`, `about:`, `chrome:` are all rejected. Previously the catch-all "not http/https" check admitted them; the authorize flow later 302s the browser to whatever is registered, which with a `javascript:` URI would execute attacker script on redirect. Also require the private-use scheme body to be reverse-DNS (contain a dot), matching RFC 8252 §7.1. - permanentDeleteFile / emptyTrash only delete the trip_files row when the on-disk unlink actually succeeded. Previously Promise.all swallowed individual unlink failures and DELETE ran unconditionally, so a permission / ENOSPC failure would orphan bytes on disk. - restoreFromZip also invalidates the permissions cache in the outer catch. If extraction threw before the DB swap even started, the cache wasn't stale, but belt-and-braces is cheap and guarantees no failed-restore path leaves stale cache behind. |
||
|
|
2d0414b4a3 |
security: internal audit — batch 1
Fixes the critical + high + medium findings from our internal security
review. Bundled into one PR because the changes overlap heavily (JWT
verification unifies across three call sites; backup-code hashing and
demo-email handling cross-cut several services); splitting them out
would mean redundant reviews of the same files.
Critical
- CI-C1 — .github/workflows/test.yml: restore actions/{checkout,setup-
node,upload-artifact} to @v4. The @v6 refs don't exist, so the test
workflow was errorring before a single test ran.
- SEC-C1 — mfaPolicy now extracts the token via extractToken() (cookie-
first, Bearer fallback). Previously it only read Authorization, so
every cookie-authenticated SPA session bypassed require_mfa entirely.
- SEC-C2/C4/C6 — all JWT verification paths (MCP bearer, file download,
photo route) now go through the shared verifyJwtAndLoadUser that
checks password_version. resetPassword additionally deletes every
mcp_tokens row and marks outstanding oauth_tokens revoked, so a
password reset invalidates ALL credential classes — not just the
cookie JWT.
High
- SEC-H2 — reset email URL is built from server-side APP_URL /
ALLOWED_ORIGINS (via existing getAppUrl()), not request headers.
Closes the host-header-injection vector into reset links.
- SEC-H3 — OIDC findOrCreateUser wraps the invite-redemption UPDATE +
user INSERT in a transaction. The UPDATE is the capacity check; if
a concurrent callback takes the last slot, the whole transaction
aborts with registration_disabled instead of double-creating users.
- SEC-H4 — new verifyIdToken() performs full JWT signature
verification via the provider's JWKS (Node's crypto.createPublicKey
accepts JWK directly — no extra dependency), plus iss/aud/exp
checks. The callback also rejects the login when userinfo.sub does
not match id_token.sub.
- SEC-H5 — OAuth DCR now validates redirect_uris against an allowlist
of schemes: https, http-loopback, or a private custom scheme. Plain
http://non-loopback is rejected.
- SEC-H6 — oauthService audience defaults to mcpResource when the
`resource` parameter is missing, so tokens are always audience-bound
to /mcp instead of being issued with audience=null.
- SEC-H7 — HSTS is enabled any time NODE_ENV=production (previously
required FORCE_HTTPS=true), includeSubDomains defaults on and can
be disabled with HSTS_INCLUDE_SUBDOMAINS=false.
- SEC-H8 — trek_session cookie Secure flag is also driven by
req.secure (which Express resolves from X-Forwarded-Proto once
trust proxy is set), so instances behind a TLS-terminating proxy
get Secure cookies without needing FORCE_HTTPS.
Medium
- SEC-M1 — permanentDeleteFile / emptyTrash / avatar unlink now use
fs.promises.rm with { force: true } (one async op vs the previous
existsSync + unlinkSync pair per file).
- SEC-M2 — invalidatePermissionsCache() is called inside restoreFromZip
so a restored DB with different permission rows is honoured
immediately.
- SEC-M3 + C1 — idempotency store bounds the key at 128 chars, caches
only responses ≤ 256 KiB, and scopes the lookup by (key, user_id,
method, path) rather than (key, user_id). Same key replayed against
a different endpoint no longer returns a stale unrelated body.
- SEC-M4 — share_tokens gets an expires_at column; new tokens default
to 90-day TTL, expired tokens are denied at lookup. Existing tokens
stay NULL = no expiry so already-published links don't break.
- SEC-M5 — /uploads/photos/:filename now resolves the photo to its
trip_id and requires the share token to cover THAT trip. Previously
any share token for any trip would unlock any photo filename.
- SEC-M6 — BLOCKED_EXTENSIONS is the single source of truth shared
between fileService and collab uploads. The '*' allowed_file_types
wildcard now still rejects executables/scripts.
- SEC-M7 — single DEMO_EMAILS constant (services/demo.ts) used by
demoUploadBlock, mfaPolicy, and every demo-mode guard in
authService. The old demoUploadBlock only matched 'demo@nomad.app'
so the seed 'demo@trek.app' could in fact upload in demo mode.
- SEC-M8 — MFA backup codes are now bcrypt-hashed at rest
(hashBackupCodeBcrypt). matchBackupCode accepts both bcrypt and
legacy SHA-256 hex hashes, so existing installs keep working until
the user regenerates codes via enableMfa.
- SEC-M9 — document the "security via UUID v4 filename" model for
/uploads/avatars|covers|journey. Requires no code change but
captures the decision so future reviewers don't re-flag it.
- SEC-M10 — already covered by the resetPassword revocation logic
above: mcp_tokens DELETE + oauth_tokens UPDATE … SET revoked_at.
Performance
- PERF-H1 — new migration adds the indexes flagged in the audit:
trips(user_id), trips(created_at DESC), photos(day_id),
photos(place_id), reservations(day_id), share_tokens(token), plus
conditional day_accommodations and notifications indexes depending
on which columns are present.
Tests
- tests/integration/oidc.test.ts now mocks verifyIdToken and passes
an id_token in the exchangeCodeForToken stub for the three flows
that exercise a successful callback. The three remaining failures
tests pointed out were all pre-existing (file-upload flakes +
notificationPreferences event_types count drift), none introduced
by this PR.
|
||
|
|
51387b0af1 |
feat(auth): add email-based password reset with MFA + session invalidation
Adds /auth/forgot-password and /auth/reset-password endpoints plus two new
client pages. When SMTP is configured the user receives a branded, i18n-aware
reset email; when it isn't the reset link is logged to the server console in
a clearly-fenced block so self-hosters can relay it manually.
Security properties:
- 256-bit cryptographically-random tokens, only SHA-256 hashes stored in DB
- 60 min expiry, single-use, prior unconsumed tokens auto-invalidated
- Enumeration-safe: /forgot-password always responds {ok:true} with a minimum
latency pad so timing doesn't leak account existence
- Per-IP rate limit (3/15min on forgot, 5/15min on reset) + per-email throttle
- If the user has MFA enabled, a valid TOTP or backup code is required at
reset-complete time — a compromised mailbox alone cannot take over a
2FA-protected account
- New users.password_version column + JWT "pv" claim: bumping it on reset
invalidates every live session immediately
- Full audit-log coverage (user.password_reset_request/_success/_fail)
- Forgot-page shows a visible hint when SMTP is unconfigured
Migration 115 adds users.password_version and password_reset_tokens
(user_id, token_hash UNIQUE, expires_at, consumed_at, created_ip).
|
||
|
|
b194e8317d |
feat(pwa): implement real offline mode with IndexedDB sync
Add genuine offline read/write capability for trips: - Dexie IndexedDB schema (trips, places, packing, todo, budget, reservations, files, mutationQueue, syncMeta, blobCache) - Repo layer for all domains: offline reads from Dexie, writes optimistically to Dexie and enqueue mutations for later replay - Mutation queue with UUID idempotency keys (X-Idempotency-Key), FIFO flush, temp-ID reconciliation on 2xx, fail-and-continue on 4xx - Trip sync manager: caches all trips with end_date >= today or null, auto-evicts 7d after end_date, fetches bundle endpoint in one request - Map tile prefetcher: bbox from place coords, zooms 10-16, 50MB cap, warms SW cache via fetch - Sync triggers: network online → flush + syncAll; WS reconnect → flush only (rate-limiter safe); visibilitychange/30s → flush only - WS remoteEventHandler writes through to Dexie on every event - Server idempotency middleware + idempotency_keys table (migration 100, 24h TTL nightly cleanup) - GET /api/trips/:id/bundle endpoint for efficient single-request sync - OfflineBanner component: amber (offline) / blue (syncing) / hidden - OfflineTab in Settings: cached trip list, re-sync and clear actions - usePendingMutations hook for per-item pending indicators Closes #505 #541 |
||
|
|
7c0a0d5f39 |
security(oauth): harden OAuth 2.1/MCP implementation (Critical + High + Medium findings)
Address 14 security findings from internal review of the OAuth 2.1 + MCP layer: Critical: - C1: Scope-gate all MCP resources (trips, budget, packing, collab, atlas, vacay, etc.) - C2: Wire token/session revocation into active MCP session lifecycle per (user, client_id) - C3: Refresh-token replay detection via parent_token_id chain + cascade revoke on replay High: - H1: Validate PKCE code_challenge (43-char base64url) and code_verifier (43–128 chars) format - H2: Rate-limit /oauth/token (30/min), /authorize/validate (30/min), /oauth/revoke (10/min) - H3: Strip client metadata from unauthenticated /authorize/validate responses (oracle prevention) - H4: Constant-time secret comparison via crypto.timingSafeEqual (prevents timing attacks) - H5: Collapse all invalid_grant cases to a single generic message; log specifics server-side Medium: - M1: Set Cache-Control: no-store + Pragma: no-cache on token endpoint responses - M2: Return 404 (not 200/403) on discovery + revoke endpoints when MCP addon is disabled - M4: Audit-log all OAuth lifecycle events (create, consent, issue, refresh, revoke, replay) - M5: Union consent scopes on re-authorization instead of replacing existing grants - M7: Require httpOnly cookie auth (not Bearer JWT) on all state-mutating OAuth endpoints - M8: Strict Bearer scheme check in MCP token verification Refactoring: - Extract MCP session management (sessions Map, revokeUserSessions, revokeUserSessionsForClient) into mcp/sessionManager.ts to break the circular dependency between oauthService and mcp/index - Extract verifyJwtAndLoadUser helper in auth middleware, shared by authenticate and new requireCookieAuth middleware Tests: - Fix all existing integration tests broken by the security hardening (OAUTH-019 to OAUTH-032) - Add 13 new integration tests covering M1, M2, H1, H3, H5, M5, M7, C3 - Add 14 new unit tests covering C2, C3, H1, H3, M5 behaviors in oauthService |
||
|
|
905c7d460b |
Add comprehensive backend test suite (#339)
* 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 |
||
|
|
1a4c04e239 |
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 <noreply@anthropic.com> |
||
|
|
add0b17e04 |
feat(auth): migrate JWT storage from localStorage to httpOnly cookies
Eliminates XSS token theft risk by storing session JWTs in an httpOnly cookie (trek_session) instead of localStorage, making them inaccessible to JavaScript entirely. - Add cookie-parser middleware and setAuthCookie/clearAuthCookie helpers - Set trek_session cookie on login, register, demo-login, MFA verify, OIDC exchange - Auth middleware reads cookie first, falls back to Authorization: Bearer (MCP unchanged) - Add POST /api/auth/logout to clear the cookie server-side - Remove all localStorage auth_token reads/writes from client - Axios uses withCredentials; raw fetch calls use credentials: include - WebSocket ws-token exchange uses credentials: include (no JWT param) - authStore initialises isLoading: true so ProtectedRoute waits for /api/auth/me Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> |
||
|
|
fedd559fd6 |
fix: pin JWT algorithm to HS256 and harden token security
- Add { algorithms: ['HS256'] } to all jwt.verify() calls to prevent
algorithm confusion attacks (including the 'none' algorithm)
- Add { algorithm: 'HS256' } to all jwt.sign() calls for consistency
- Reduce OIDC token payload to only { id } (was leaking username, email, role)
- Validate OIDC redirect URI against APP_URL env var when configured
- Add startup warning when JWT_SECRET is auto-generated
https://claude.ai/code/session_01SoQKcF5Rz9Y8Nzo4PzkxY8
|
||
|
|
66f5ea50c5 |
feat(require-mfa): #155 enforce MFA via admin policy toggle across app access
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. |
||
|
|
8396a75223 | refactoring: TypeScript migration, security fixes, | ||
|
|
c582a7b6c8 |
Block uploads for demo user, restore PDF preview modal (v2.2.3)
- Demo user gets 403 on all upload endpoints (files, photos, cover, avatar) - Admin uploads still work normally - PDF export back in modal popup using srcdoc iframe - Zero behavior change when DEMO_MODE is not set |
||
|
|
cb1e217bbe |
Initial commit — NOMAD (Navigation Organizer for Maps, Activities & Destinations)
Self-hosted travel planner with Express.js, SQLite, React & Tailwind CSS. |