Commit Graph

46 Commits

Author SHA1 Message Date
Julien G. 002ea91be8 Align dev (#869)
* chore: bump version to 3.0.0 [skip ci]

* fix: resolve dead wiki links across install and config pages

* fix(reservations): restore correct day assignment for non-transport bookings

v3.0.0 switched the planner from rendering reservations by
reservation_time to rendering them by day_id (commit 3f61e1c), but
migration 110 only backfilled day_id for transport types. Tours,
restaurants, events and 'other' bookings kept whatever day_id was
stored in the DB — often the trip's first day, from older code paths
that defaulted it there — so after the upgrade those rows all show
up on day 1 regardless of their actual reservation_time.

- Migration 122: for every non-hotel reservation, null out any
  day_id / end_day_id that does not match the reservation's time,
  then backfill it from reservation_time / reservation_end_time.
  Idempotent; leaves already-correct rows alone.
- reservationService.createReservation / updateReservation now
  derive day_id / end_day_id from reservation_time /
  reservation_end_time when the client didn't send one explicitly,
  so the mismatch cannot reappear on new or edited bookings.
  Hotels are skipped because they store their date range on the
  linked day_accommodation.

* chore: bump version to 3.0.1 [skip ci]

* fix(oidc): normalize discovery doc issuer before comparison

Trailing slash in doc.issuer (e.g. Authentik) caused a mismatch against
the already-normalized configured issuer, breaking OIDC login entirely.

Closes #834

* test(systemNotices): exclude v3 upgrade notices from login_count-only tests

Tests that expect an empty notice list were using first_seen_version='0.0.0'
(DB default), which matches the existingUserBeforeVersion('3.0.0') condition
now that the app is at 3.0.1. Set first_seen_version='3.0.0' so only the
firstLogin condition controls visibility in these tests.

* chore: bump version to 3.0.2 [skip ci]

* fix(oidc): normalize id_token iss claim before issuer comparison (#837)

jwt.verify does an exact string match on the issuer. Providers like
Authentik include a trailing slash in the id_token iss claim while the
configured issuer is already normalized (no trailing slash), causing
every login attempt to fail with jwt issuer invalid.

Move the issuer check out of jwt.verify options and apply the same
trailing-slash normalization used in the discovery doc validation.
Also adds OIDC-SVC-033–036 unit tests covering exact match, trailing
slash, wrong issuer, and wrong audience cases.

Closes #834

* chore: bump version to 3.0.3 [skip ci]

* fix(oidc,ui): restore Authentik login and fix mobile delete dialog (#845)

OIDC: when OIDC_DISCOVERY_URL is explicitly set, trust the discovery
doc's issuer for id_token comparison instead of rejecting a path
mismatch as an error. Authentik (and similar realm-path providers)
return a canonical issuer like /application/o/<slug>/ that differs
from the operator's base OIDC_ISSUER. Strict equality blocked login
in 3.x despite working in v2. Default discovery (no custom URL) keeps
the strict check. Adds OIDC-SVC-037/038/039.

UI: ConfirmDialog and CopyTripDialog lacked the --bottom-nav-h
paddingBottom offset that other overlays already use. On mobile portrait
the action buttons were hidden behind the sticky bottom nav bar.

Closes #843
Closes #844

* chore: bump version to 3.0.4 [skip ci]

* fix(files): open attachments only in new tab (#840)

window.open with noreferrer returns null, which triggered the popup-blocked download fallback in addition to the new-tab open. Use a target=_blank anchor click instead.

* chore: bump version to 3.0.5 [skip ci]

* fix(journey,pdf): journey reorder sort_order + PDF multi-day transport (#848)

* fix(journey): make sort_order authoritative for within-day entry ordering

Reorder buttons appeared broken because the server ORDER BY put entry_time
before sort_order, so entries synced from trip places with differing times
would always sort by time regardless of sort_order writes. The client store
mirrored the same comparator, making even the optimistic update invisible.

- Change ORDER BY to (entry_date, sort_order, id) in getJourneyFull and listEntries
- Fix syncTripPlaces and onPlaceCreated to assign MAX+1 sort_order per day instead of day_number/0
- Update client store comparator to match
- Add DB migration to backfill sort_order using old effective key (entry_time, id) so existing journeys retain their visual order
- Add tests: JOURNEY-SVC-089–093, FE-STORE-JOURNEY-018–019

Closes #846

* fix(pdf): include multi-day transport return/arrival in PDF itinerary (#847)

Reservations were matched to days by pickup date only, so the end-day
card (e.g. car Return, flight Arrival) was silently dropped from the PDF.
Add span-aware helpers mirroring DayPlanSidebar logic: match by day_id/end_day_id
span, show reservation_end_time on end days, prefix title with phase label
(Return/Arrival/etc.), and use per-day position for sort order.

* test(pdf): add missing day_id to transport reservation fixture

* chore: bump version to 3.0.6 [skip ci]

* [Snyk] Security upgrade uuid from 9.0.1 to 14.0.0 (#849)

* fix: server/package.json & server/package-lock.json to reduce vulnerabilities

The following vulnerabilities are fixed with an upgrade:
- https://snyk.io/vuln/SNYK-JS-UUID-16133035

* fix: bump fast-xml-parser version

---------

Co-authored-by: snyk-bot <snyk-bot@snyk.io>
Co-authored-by: jubnl <jgunther021@gmail.com>

* chore: bump version to 3.0.7 [skip ci]

* fix: hot fixes 23-04-2026 (#856)

* fix(packing): resolve avatar URL path in bag and category assignees (#854)

packingService was returning raw avatar filenames from the DB instead of
the full /uploads/avatars/<filename> path, causing broken profile images
for users with uploaded avatars.

* fix(budget): use Map.get() to fix category rename no-op (#855)

* fix(security): relax Referrer-Policy and document HSTS_INCLUDE_SUBDOMAINS (#862) (#863)

- Change Helmet default from no-referrer to strict-origin-when-cross-origin
  so browsers send the origin on cross-origin requests, allowing Google Maps
  API key restrictions by HTTP referrer to work correctly
- Document HSTS_INCLUDE_SUBDOMAINS in all deployment artifacts:
  .env.example, docker-compose.yml, README.md, unraid-template.xml,
  charts/values.yaml, charts/configmap.yaml, wiki/Environment-Variables.md

* fix(planner): prefetch budget items on trip page mount (#864)

Loads budgetItems alongside reservations when TripPlannerPage mounts so
the Budget category dropdown in ReservationModal and TransportModal shows
pre-existing categories on first open, regardless of whether the Budget
tab has been visited.

Closes #861

* fix(reservations): prevent Invalid Date when end time is set without end date (#866)

When reservation_end_time held a bare time string ("HH:MM"), fmtDate()
produced Invalid Date on the reservation card.

- Modal: when end date is blank but end time is filled, construct a
  same-day ISO datetime using the start date (prevents time-only strings
  from ever being persisted)
- Panel: derive endDatePart via regex so date-only end values ("YYYY-MM-DD")
  still show the multi-day range, while bare time strings are skipped and
  handled correctly by the existing time column logic

Closes #860

* fix(planner): format reservation end time instead of rendering raw ISO string (#867)

Closes #859

* fix(planner): wire Route toggle into mobile day sidebar (#850) (#868)

The per-booking Route icon was missing on mobile because the mobile
DayPlanSidebar invocation in TripPlannerPage didn't pass
visibleConnectionIds or onToggleConnection. Mobile PWA users couldn't
activate reservation map overlays without forcing desktop mode.

Also corrects the Map-Features wiki: fixes the setting name
("Booking route labels" not "Show connection labels"), documents the
route_calculation requirement for travel-time pills, and explains that
overlays are off by default and must be toggled per reservation.

* chore: bump version to 3.0.8 [skip ci]

---------

Co-authored-by: Maurice <61554723+mauriceboe@users.noreply.github.com>
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
Co-authored-by: Maurice <mauriceboe@icloud.com>
Co-authored-by: Xre0uS <36565320+Xre0uS@users.noreply.github.com>
Co-authored-by: snyk-bot <snyk-bot@snyk.io>
2026-04-23 19:57:50 +02:00
jubnl ba7b99fb7d fix: update backend tests and service bugs for gallery 1-to-N schema
updatePhoto: write sort_order to journey_entry_photos (junction) not journey_photos,
since JP_SELECT reads jep.sort_order — updating the gallery row had no visible effect.

deletePhoto: include id in return value so callers that check deleted.id still work.

Tests updated for new schema:
- journeyShareService: insertJourneyPhoto helper now inserts into journey_photos
  (keyed by journey_id) + journey_entry_photos junction instead of the old
  entry_id-keyed table
- SVC-081: deleteEntry cascades junction rows (journey_entry_photos), not gallery
  rows (journey_photos); assert junction is gone, gallery is preserved
- SVC-086: syncTripPhotos now populates the gallery directly — no [Trip Photos]
  wrapper entry; assert journey_photos gallery row instead
- INT-028: error message updated to 'journey_photo_id required'
2026-04-22 16:05:18 +02:00
jubnl 5eaf7492dc fix(backups,files): auto-backups rejected by validator; trip file download broken after cookie migration
Fixes #773: isValidBackupFilename regex anchored to ^backup- rejected all
auto-backup-* filenames, causing 400 on download/restore/delete. Broadened
to ^(?:auto-)?backup-.

Fixes #774: three regressions in the trip Files tab —
- openFile import shadowed by a local function of the same name inside
  FileManager; PDF preview modal was calling the local with a URL string,
  corrupting state and crashing on the second click (mime_type read on
  undefined). Fixed by aliasing the import as openFileUrl.
- GET /:id/download used a bespoke authenticateDownload that checked only
  Bearer header and ?token= query param, ignoring the trek_session cookie.
  After the JWT-to-cookie migration the client sends cookies only, so every
  download silently 401-ed. Extended authenticateDownload to accept req and
  check cookie → Bearer → query token in priority order.
- files.download and files.openError translation keys were missing from all
  15 locale files; t() was returning the raw key as a truthy string,
  defeating the || 'Download' fallback.
2026-04-21 11:18:17 +02:00
jubnl edf14e2ebc test(maps): update getPlacePhoto stubs to use text() instead of json()
mapsService now reads the details response body via .text() before parsing,
so test stubs need text() rather than json().
2026-04-21 00:16:54 +02:00
Maurice c857d38bcd test(notifications): bump event_types count to 9 after adding todo_due 2026-04-20 17:38:25 +02:00
jubnl dd90c6d424 fix(mcp): add RFC 9728 PRM, RFC 8707 audience binding, and collab sub-feature gating
Root cause: claude.ai's MCP connector (spec 2025-06-18) requires the resource server
to publish Protected Resource Metadata and return WWW-Authenticate on 401s to bind
the /mcp endpoint to its AS. Without these, it silently shows no tools after OAuth.

- Add /.well-known/oauth-protected-resource (RFC 9728) with addon gating
- Emit WWW-Authenticate: Bearer resource_metadata=... on 401/auth-failure 403s
- Open CORS (origin: *) on both .well-known/* endpoints per RFC 8414/9728
- Accept resource parameter at authorize + token endpoints (RFC 8707)
- Store audience on oauth_tokens; validate on every MCP request
- Refresh tokens inherit audience; add resource_parameter_supported to AS metadata
- DB migration: ADD COLUMN audience TEXT to oauth_tokens
- Gate collab MCP tools/resources by chat/notes/polls sub-features individually
- Invalidate MCP sessions when collab sub-features are toggled in admin
- Update test mocks and MCP.md
2026-04-20 07:34:38 +02:00
Maurice 4974013995 fix journey bugs reported by roel-de-vries (#722-#736)
Mobile UI:
- #722 timeline carousel no longer cut off by BottomNav (uses --bottom-nav-h var)
- #723 scroll-snap-type relaxed to proximity so small swipes no longer skip entries
- #724 defensive padding-bottom fix in JourneySettingsDialog for iOS PWA
- #725 add back/settings buttons + journey title subtitle to mobile activity view
- #726 active entry re-centers after scroll settle; tap inactive card activates
  it (does not jump straight into editor)

Entry editor flow:
- #727 photo uploads queue locally until Save for existing entries too
  (previously fired upload immediately; Cancel silently kept the new photo)
- #728 Cancel/Close with unsaved changes now requires confirm (window.confirm)
- #729 linking a Gallery photo into an entry now copies the row (old MOVE
  behavior meant Remove-from-Entry also nuked the Gallery original)
- #731 addPhoto / addProviderPhoto / linkPhotoToEntry promote skeleton
  entries to concrete 'entry' type when content is added

Permissions:
- #732 updateJourney switched from canEdit to isOwner — editors can still
  edit entries and photos, just not the journey shell (title, cover, status)
- #733 Contributors list gains a per-row remove (X) control with confirm
- #734 my_role is computed server-side and returned with the journey; UI
  gates Settings/Add/Edit/Delete controls based on role
- #736 createOrUpdateJourneyShareLink + deleteJourneyShareLink now require
  isOwner (previously NO permission check at all — anyone authenticated
  could publish or unpublish a journey)

Immich upload (#730):
- migration 111: add users.immich_auto_upload (default 0)
- migration 112: seed provider_field for the toggle (idempotent, FK-safe)
- journey photo upload only mirrors to Immich when the user has opted in
- Settings UI gets a "Mirror journey photos to Immich on upload" checkbox

Test updates:
- JOURNEY-SVC-019 inverted to assert editor cannot update journey settings
- JOURNEY-SHARE-007 now passes userId (owner) to deleteJourneyShareLink
- FE-PAGE-JOURNEYDETAIL-148 inverted to assert photos stay pending until Save
- client/tests still green (2676/2676)

Also fixed en route: gallery entry title is now the literal 'Gallery' on the
wire (used to send the translated label, which broke server-side title === 'Gallery'
checks in non-English locales); confirm interpolation uses {username} single
braces matching the existing i18n runtime; Settings footer uses icon-only
delete/archive buttons on mobile so the row doesn't wrap.
2026-04-18 19:11:16 +02:00
Maurice 777b68f87b fix tests for sidebar/settings refactor + weather archive fallback
- DayPlanSidebar: add aria-label to undo button, replace title with aria-label
  so tests can still locate buttons by accessible name after tooltip refactor
- tests: switch getByTitle("Add Note") to getByLabelText
- tests: find undo button via aria-label (new expand/collapse button also uses
  width:30, breaking the old style-based lookup)
- PlacesSidebar tests: loosen "All" button regex to account for count badge
- DisplaySettingsTab tests: use getByRole for Auto button (two "Auto" spans
  coexist for mobile/desktop); handle multiple English matches in lang test
- weatherService tests: past-date case now expects an archive fetch instead
  of an immediate no_forecast error
2026-04-18 11:45:19 +02:00
jubnl 6a718fccea feat(import): selective GPX/KML element import and performance improvements
Add type-selector UI in the file import modal letting users choose which
GPX elements (waypoints, routes, tracks) or KML/KMZ elements (points,
paths) to import. KML LineString placemarks are now imported as path
places with route_geometry.

Performance improvements:
- Extract MemoPlaceRow with React.memo and contentVisibility:auto to cut
  unnecessary re-renders in PlacesSidebar
- Add weatherQueue to cap concurrent weather fetches at 3
- Replace sequential per-place deletes with a single bulkDelete API call
  (new DELETE /places/bulk endpoint + deletePlacesMany service)
- Memoize atlas/photo/weather service calls to avoid redundant requests
- Add multi-select mode to PlacesSidebar for bulk operations

Add large GPX/KML/KMZ fixtures for integration/perf testing and two
profiler analysis scripts under scripts/.
2026-04-18 01:28:37 +02:00
jubnl 9c2decb095 fix(maps): reduce Google Places API quota usage with persistent caching
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
2026-04-17 19:07:39 +02:00
jubnl 3b94727c07 fix(journey): fix issue #704 — active logic, archive, places rename, search, trip reminders
- Derive journey lifecycle from linked trip dates (live/upcoming/completed/draft)
  instead of relying solely on status field; status=archived always wins
- Add Archive/Restore Journey action in journey settings dialog
- Rename cities → places end-to-end (SQL alias, TS types, stats field, all locales)
- Wire up search icon: toggles inline input, filters by title+subtitle client-side
- Fix channelConfigured check: trip reminders enabled by default since inapp is
  always available; remove channel check, controlled solely by admin setting
- Expose notify_trip_reminder toggle in Admin → Settings → Notifications
- Add trip_date_min/trip_date_max to listJourneys SQL for client-side lifecycle
- Add archived status to Journey type (server + client)
- Update all 15 locale files with new keys (search, archive, places, trip reminders)
2026-04-17 16:59:23 +02:00
jubnl 5046e1a2e0 fix(synology): wire shared-album passphrase through journey-entry add flow
Thread selectedAlbumPassphrase from ProviderPicker through onAdd →
journeyApi.addProviderPhotos → POST /entries/:entryId/provider-photos →
addProviderPhoto service → getOrCreateTrekPhoto so shared-album photos
have their passphrase encrypted and persisted on trek_photos at add-time,
enabling streamPhoto to forward it to Synology correctly (#689).
2026-04-17 15:33:05 +02:00
jubnl da70388f4b fix(journey): resolve Immich photos on public share by matching trek_photos.id
validateShareTokenForPhoto was querying journey_photos by jp.id but the
public page sends p.photo_id (trek_photos.id) in the URL. In a fresh
database the IDs coincidentally match, masking the bug. In production
instances with many Immich-synced photos the trek_photos autoincrement
is far ahead of journey_photos, causing a 404 for every Immich photo
on the public share page.

Fix: change the lookup to jp.photo_id = ? so validation is keyed on
trek_photos.id, which is what the client sends and what streamPhoto
needs. Updated the test helper to return trekId and added a regression
test that pre-populates trek_photos to produce diverging IDs. Closes #675.
2026-04-16 15:37:24 +02:00
jubnl bfe84b3016 feat(notifications): add ntfy as a first-class notification channel
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
2026-04-15 13:59:25 +02:00
jubnl 875c91e5ff feat(places): unified file import modal with drag-and-drop and deduplication
- Replace separate GPX and KML/KMZ import buttons with a single "Import
  file" modal accepting all three formats, with a drag-and-drop drop zone
- Support dragging files directly onto the Places sidebar panel; overlay
  appears on hover and pre-loads the file into the modal on drop
- Fix [object Object] description bug in KML imports caused by
  fast-xml-parser returning mixed-content nodes as objects; add stopNodes
  config and object guard in asTrimmedString
- Fix CDATA sections leaking into descriptions (e.g. "text.]]>") by
  unwrapping CDATA markers before tag stripping
- Add import deduplication across all import paths (GPX, KML/KMZ, Google
  list, Naver list): reimporting skips places already in the trip by name
  (case-insensitive) or by coordinates (within ~11 m tolerance), with
  intra-batch dedup so duplicate placemarks within the same file are
  also collapsed
- Fix KML route returning 400 "No valid Placemarks found" when all
  placemarks were valid but deduplicated; 400 now only fires when the
  file contains zero placemarks
- Show a warning toast "All places were already in the trip" instead of
  a misleading success toast when a reimport produces zero new places
  (GPX, KML/KMZ, Google list, Naver list)
- Add 8 new i18n keys across all 14 locales; remove 11 keys made unused
  by the modal consolidation
2026-04-15 06:07:26 +02:00
jubnl 801ffbfb7b fix(kml-import): address PR #488 review issues
- Strip BOM (U+FEFF) from 14 translation files injected by editor
- Guard KMZ unpack against zip-bomb: check entry.uncompressedSize against
  50 MB cap (KMZ_DECOMPRESSED_SIZE_LIMIT) before calling .buffer();
  limit is an exported constant so tests can override it
- Fix non-BMP HTML entity decoding: replace String.fromCharCode with
  String.fromCodePoint + 0x10FFFF bounds check so emoji like &#128512;
  round-trip correctly
- Switch KML namespace stripping from regex to fast-xml-parser's
  removeNSPrefix option; XMLValidator accepts namespaced XML natively,
  making the pre-strip step unnecessary
- Remove dead skippedCount overwrite after transaction; per-loop
  increment already tracks it alongside per-item error messages
- Type multer req.file as Express.Multer.File on both /import/gpx
  and /import/map routes instead of (req as any).file
- Add unit tests: emoji entity decoding (decimal + hex), KMZ zip-bomb
  rejection, KMZ-with-no-KML rejection
2026-04-15 05:16:47 +02:00
jubnl a1a7795945 Merge PR #488: KMZ/KML place import
Resolves conflicts with Naver list import (PR #662) — kept both unified
list-import dialog and new KMZ/KML dialog. Dropped duplicate react-dom
import and unused CustomSelect import from PlacesSidebar.
2026-04-15 05:09:45 +02:00
jubnl 607498cabe fix(search-autocomplete): address PR #542 review issues
- Fix race condition: AbortController cancels in-flight autocomplete
  requests on each keystroke; stale responses no longer overwrite fresh ones
- Remove acTrigger state hack; onFocus calls fetchSuggestions directly
- Cap autocomplete input at 200 chars server-side (400 on violation)
- Filter Nominatim suggestions with empty osm_id segments
- Revert getPlaceDetails OSM branch from unconditional parallel fetch to
  conditional serial: Nominatim called only when Overpass lacks coords/address
- Wire places.loadingDetails i18n key to Loader2 spinner via aria-label/role
- Add tests: MAPS-017, MAPS-040c, MAPS-093, FE-MAPS-004
2026-04-15 04:16:56 +02:00
jubnl 35321076cf Merge branch 'review/pr-542' into feat/search-autocomplete 2026-04-15 04:02:08 +02:00
jubnl a438652a50 fix(trips): preserve day content when trip date range changes
Rewrites generateDays to remap days positionally by day_number instead
of matching by date identity. Previously any date range shift with no
overlap would cascade-delete all day_assignments, day_notes, and
day_accommodations.

New behaviour:
- Shift/partial overlap: existing days remapped to new dates in order
- Shrink: overflow days become dateless (date=NULL) instead of deleted,
  preserving all child data for manual reassignment
- Grow: existing days kept, new empty days appended
- Clear dates: all days nullified, content intact

Also fixes a UNIQUE(trip_id, day_number) collision that would occur when
spare dateless days remained after growing into a partially-dateless trip
(maxAssigned base was wrong).

Closes #646
2026-04-15 01:28:53 +02:00
Maurice b3571f391a Fix skeleton entry deletion and add hide suggestions toggle (#619)
- Revert filled skeleton entries back to skeleton on delete instead of permanently removing them
- Add per-user hide_skeletons preference on journey_contributors (migration 99)
- Add PATCH /journeys/:id/preferences endpoint for toggling skeleton visibility
- Add Eye/EyeOff toggle button with custom tooltip in journey detail header
- Filter skeleton entries from timeline when hidden
- Add i18n keys for all 14 languages
2026-04-14 19:58:13 +02:00
jubnl 6a23118342 fix(notifications): fix SMTP error surfacing, webhook button label, backup timestamp
- 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
2026-04-14 16:20:52 +02:00
jubnl 714e2ad703 fix(tests): update test helpers and assertions for migration-98 photo schema
trek_photos is now the central registry; trip_photos and journey_photos
reference it via photo_id FK. Updated all affected test helpers and
direct-SQL assertions to join trek_photos instead of querying stale
columns (asset_id, provider, owner_id) on the leaf tables.

Also fix ATLAS-UNIT-019: getVisitedRegions now fires background geocoding
and returns immediately, so the test must call it twice — once to trigger
the fill, once after advancing fake timers to read cached results.
2026-04-14 13:54:48 +02:00
Ben Haas 479ab49d67 Merge branch 'dev' into search-auto-complete 2026-04-13 08:47:36 -07:00
Ben Haas 7fca16d866 Switch location bias from a point to a bounding box for improved autocomplete accuracy and validation. 2026-04-13 07:53:40 -07:00
jubnl e629548a42 fix(tests): align tests to actual working code 2026-04-13 14:48:25 +02:00
jubnl ad27c5f6be fix: restore broken tests after prerelease workflow refactor
- Export __clearVersionCacheForTests() from adminService; call in
  versionNotification beforeEach to reset module-scoped cache between
  tests (VNOTIF-002..006 failed because VNOTIF-001 cached
  update_available:false, short-circuiting all subsequent test fetches)
- Seed appVersion:'2.9.10' in Navbar test authStore; appVersion moved
  from local useEffect state to authStore in last commit so the test
  render no longer fetches it independently (FE-COMP-NAVBAR-016)
- Add data-testid="weekend-days" to VacaySettings weekend-days
  container; use within() in tests to scope button count to that
  section, fixing false positives from the week-start buttons which
  share the same inline styles (FE-COMP-VACAYSETTINGS-003/004)
- Pass isPrerelease={true} in GitHubPanel FE-ADMIN-GH-007; component
  filters out prerelease releases when isPrerelease=false so the badge
  was never rendered (pre-existing, unrelated to last commit)
2026-04-12 17:19:24 +02:00
Maurice de157cb87b test: comprehensive Journey test suite — 89.5% new code coverage
Server (172 tests):
- journeyService unit tests (87 tests): CRUD, access control, sync, photos, contributors
- journeyShareService unit tests (20 tests): share links, token validation, public access
- journey integration tests (45 tests): all API routes, auth, permissions, edge cases
- Test helpers: journey factories, RESET_TABLES updated

Client (340+ tests):
- journeyStore tests (15 tests): all store actions and state management
- JourneyPage tests (20 tests): frontpage, create flow, suggestions, navigation
- JourneyDetailPage tests (94 tests): all sub-components, entry editor, settings,
  share links, contributors, gallery, map, trip linking
- JourneyPublicPage tests (18 tests): public view, tabs, restricted access
- JourneyBookPDF tests (6 tests): PDF generation
- BottomNav tests (9 tests): profile sheet, navigation
- PhotoLightbox tests (8 tests): keyboard nav, counter
- JourneyMap tests (12 tests): markers, polylines, zoom
- Component tests: moodConfig, stripMarkdown, MarkdownToolbar, JournalBody, MobileTopHeader
- DashboardPage tests (32 tests): spotlight card, quick actions, widget settings

SonarQube: exclude unused MemoriesPanel from coverage (dead code, moved to Journey)
2026-04-12 01:19:53 +02:00
jubnl 47d9cce936 fix(tests): update tests for granular auth toggles
- Add new fields to AppConfig type and buildAppConfig factory
- Update FE-PAGE-ADMIN-018: heading changed to "Authentication Methods"
- Update FE-PAGE-ADMIN-053: oidc_only toggle removed from OIDC panel
- Update FE-PAGE-LOGIN-007/017: mocks now include password_login/oidc_login
- Update ADMIN-SVC-049: updateOidcSettings no longer writes oidc_only
2026-04-11 20:33:51 +02:00
jubnl bfd2553d1e feat(auth): split OIDC_ONLY into granular auth toggles
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
2026-04-11 20:21:36 +02:00
jubnl eff3fcfe10 test: update expected event_types count after adding synology_session_cleared 2026-04-11 18:44:40 +02:00
jubnl 7a22d742ab test: add comprehensive coverage for OAuth scopes, MCP, and core services
Adds new and expanded test suites across client and server to cover the
OAuth 2.1 scope system, MCP session manager, collab service, unified
memories helpers, OIDC service, budget slice, and OAuth authorize page.
Also extends SonarQube coverage exclusions to include bootstrapping files
(migrations, scheduler, main.tsx, types.ts) that are not meaningfully
testable.
2026-04-11 14:08:09 +02:00
jubnl 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
2026-04-10 02:03:27 +02:00
Ben Haas 583ac6d4d9 Add tests for mapsApi.autocomplete and autocompletePlaces service interactions 2026-04-09 16:02:10 -07:00
jubnl f2908fdd65 test(mcp): add tests for OAuth 2.1, addon gating, and budget reorder
Covers OAuth integration flow, scope enforcement, addon-gated tool access,
oauthService unit tests, and budget reorder/permission/reservation-sync scenarios.
2026-04-09 23:12:59 +02:00
Yannis Biasutti 0e3e6df1f0 Merge remote-tracking branch 'origin/dev' into feat/places-kmz-kml-import
# Conflicts:
#	server/tests/integration/places.test.ts
2026-04-07 21:07:42 +02:00
Yannis Biasutti 8c8bd5bc37 refactor(server): consolidate KML import utilities 2026-04-06 21:27:10 +02:00
jubnl 781861f799 test: relax ReDoS timing thresholds for CI compatibility
MAPS-024 and MAPS-026 were asserting < 100ms on adversarial regex input,
which passed locally but flaked on CI runners (~150-170ms). These are not
cases of catastrophic backtracking — true ReDoS would take seconds, not
~150ms. Raise the threshold to 500ms to remain meaningful while being
reliable across environments.
2026-04-06 20:12:52 +02:00
jubnl b4922322ae test: expand test suite to 87.3% backend coverage
Add new integration test files covering previously untested routes:
- categories.test.ts — GET /api/categories
- oidc.test.ts — full OIDC login flow (callback, state, errors)
- settings.test.ts — GET/PUT /api/settings, bulk save
- tags.test.ts — CRUD for trip tags
- todo.test.ts — todo items CRUD and reorder

Add new unit test files covering service-layer logic:
- adminService.test.ts — user/invite management, packing templates, OIDC settings
- atlasService.test.ts — atlas search and place enrichment
- authServiceDb.test.ts — DB-backed auth helpers (login, register, MFA)
- backupService.test.ts — export/import/restore logic
- categoryService.test.ts — category CRUD
- dayService.test.ts — day management and accommodation helpers
- mapsService.test.ts — route/directions helpers
- oidcService.test.ts — OIDC state, auth code, role resolution, user upsert
- packingService.test.ts — packing item/bag/template operations
- placeService.test.ts — place CRUD and tag attachment
- settingsService.test.ts — settings get/set/bulk
- tagService.test.ts — tag CRUD
- todoService.test.ts — todo CRUD and reorder
- tripService.test.ts — trip CRUD, member management, archiving
- vacayService.test.ts — vacay integration helpers
- tripAccess.test.ts (middleware) — requireTripAccess middleware

Expand existing integration and unit test files with additional cases
across admin, atlas, auth, backup, collab, days, files, maps, memories
(Immich/Synology), notifications, places, reservations, share, vacay,
weather, auth middleware, ephemeral tokens, notification preferences,
permissions, SSRF guard, and WebSocket connection tests.

Update test helpers (factories.ts, test-db.ts) with new factory
functions and seed data required by the expanded suite.

Fix minor issues in server/src/routes/reservations.ts and
server/src/services/atlasService.ts surfaced by new test coverage.

Update sonar-project.properties to reflect new coverage thresholds.
2026-04-06 20:08:30 +02:00
jubnl 5bcadb3cc6 test: apply suite review improvements (01–11)
- Fix SEC-005: rewrite path traversal test to upload a real file, inject
  traversal filename into DB, and assert the download does not succeed
- Fix SEC-007: rename misleading test description to reflect it tests
  rejection of an invalid token, not acceptance of a valid one
- Delete health.test.ts: all 3 tests were exact duplicates of auth.test.ts
  and misc.test.ts
- Remove duplicate describe blocks from misc.test.ts: Categories endpoint
  (duplicate of categories.test.ts) and App config (duplicate of auth.test.ts)
- Remove TRIP-016 from trips.test.ts: weaker duplicate of TRIP-007 (no body
  assertion)
- Remove API Keys describe block from profile.test.ts: canonical copy lives
  in security.test.ts where it belongs
- Remove avatarUrl describe block from budgetService.test.ts: identical tests
  already exist in authService.test.ts; drop now-unused import
- Add DB verification to ASSIGN-007 and PACK-006 reorder tests: query
  day_assignments / packing_items after PUT reorder to confirm order changed
- Strengthen BUDGET-007/008/009: add member/payer setup and assert concrete
  values (total_paid, per-user balance, flow direction and amount)
- Remove 6 pointless Map-semantics tests from inAppNotificationActions.test.ts;
  keep only the two built-in registration checks
- Remove 5 passthrough tests from queryHelpers.test.ts; keep the 4 tests that
  cover actual flat-to-nested transformation logic
2026-04-06 20:08:13 +02:00
Yannis Biasutti 5271463064 feat(server): add KML and KMZ place import pipeline 2026-04-06 18:31:47 +02:00
jubnl 5cc81ae4b0 refactor(server): replace node-fetch with native fetch + undici, fix photo integrations
Replace node-fetch v2 with Node 22's built-in fetch API across the entire server.
Add undici as an explicit dependency to provide the dispatcher API needed for
DNS pinning (SSRF rebinding prevention) in ssrfGuard.ts. All seven service files
that used a plain `import fetch from 'node-fetch'` are updated to use the global.
The ssrfGuard safeFetch/createPinnedAgent is rewritten as createPinnedDispatcher
using an undici Agent, with correct handling of the `all: true` lookup callback
required by Node 18+. The collabService dynamic require() and notifications agent
option are updated to use the dispatcher pattern. Test mocks are migrated from
vi.mock('node-fetch') to vi.stubGlobal('fetch'), and streaming test fixtures are
updated to use Web ReadableStream instead of Node Readable.

Fix several bugs in the Synology and Immich photo integrations:
- pipeAsset: guard against setting headers after stream has already started
- _getSynologySession: clear stale SID and re-login when decrypt_api_key returns null
  instead of propagating success(null) downstream
- _requestSynologyApi: return retrySession error (not stale session) on retry failure;
  also retry on error codes 106 (timeout) and 107 (duplicate login), not only 119
- searchSynologyPhotos: fix incorrect total field type (Synology list_item returns no
  total); hasMore correctly uses allItems.length === limit
- _splitPackedSynologyId: validate cache_key format before use; callers return 400
- getImmichCredentials / _getSynologyCredentials: treat null from decrypt_api_key as
  a missing-credentials condition rather than casting null to string
- Synology size param: enforce allowlist ['sm', 'm', 'xl'] per API documentation
2026-04-05 21:12:51 +02:00
jubnl 7b37d337c1 fix(security): address notification system security audit findings
- SSRF: guard sendWebhook() with checkSsrf() + createPinnedAgent() to block
  requests to loopback, link-local, private network, and cloud metadata endpoints
- XSS: escape subject, body, and ctaHref in buildEmailHtml() via escapeHtml()
  to prevent HTML injection through user-controlled params (actor, preview, etc.)
- Encrypt webhook URLs at rest: apply maybe_encrypt_api_key on save
  (settingsService for user URLs, authService for admin URL) and decrypt_api_key
  on read in getUserWebhookUrl() / getAdminWebhookUrl()
- Log failed channel dispatches: inspect Promise.allSettled() results and log
  rejections via logError instead of silently dropping them
- Log admin webhook failures: replace fire-and-forget .catch(() => {}) with
  .catch(err => logError(...)) and await the call
- Migration 69: guard against missing notification_preferences table on fresh installs
- Migration 70: drop the now-unused notification_preferences table
- Refactor: extract applyUserChannelPrefs() helper to deduplicate
  setPreferences / setAdminPreferences logic
- Tests: add SEC-016 (XSS, 5 cases) and SEC-017 (SSRF, 6 cases) test suites;
  mock ssrfGuard in notificationService tests
2026-04-05 03:36:50 +02:00
jubnl fc29c5f7d0 feat(notifications): add unified multi-channel notification system
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
2026-04-05 01:22:18 +02:00
Maurice 501bab0f69 test: update cookie test to match sameSite lax change 2026-04-03 14:42:48 +02:00
Julien G. 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
2026-04-03 13:17:53 +02:00