Commit Graph

22 Commits

Author SHA1 Message Date
Julien G. 126f2df21b chore: move i18n to shared package (#1066)
* chore: move i18n to shared package

* chore: move server translations to shared package and apply linter and prettier on entire shared package
2026-05-26 20:27:29 +02:00
Julien G. da3cba2de3 v3.0.19 Bug Fixes (#992)
* fix(mcp): replace relative oauth constent redirect by absolute redirect derived from APP_URL (#987)

* feat(journey): convert HEIC/HEIF uploads to JPEG for cross-platform compatibility

HEIC is an Apple-only format not recognised as an image by many browsers
and platforms. heic-to (lazy-loaded) now converts HEIC/HEIF files to JPEG
before upload in both the gallery and entry editor photo pickers.
Embedded metadata (EXIF, GPS) may be lost during conversion — documented
in the Journey Journal wiki page.

* fix(journey): skip heic-to import for non-HEIC files to avoid test env failures

* fix(notifications): prevent double-escaping HTML in password reset emails

buildPasswordResetHtml passed a pre-built HTML block to buildEmailHtml,
which then escaped it again — rendering raw tags as plain text in the email.
2026-05-13 10:13:17 +02:00
Julien G. de6c0fb781 fix: prevent Invalid URL crash when APP_URL lacks a protocol (#972)
* fix: prevent Invalid URL crash when APP_URL lacks a protocol (issue #970)

- Add getMcpSafeUrl() to notifications.ts: wraps getAppUrl() and
  guarantees a result that satisfies the MCP SDK's checkIssuerUrl
  requirement (https:// or http://localhost). Non-HTTPS, non-localhost
  URLs fall back to http://localhost:{PORT} instead of propagating an
  "Issuer URL must be HTTPS" error.
- Switch app.ts, mcp/index.ts, mcp/oauthProvider.ts, and oauthService.ts
  to import getMcpSafeUrl instead of getAppUrl for all MCP resource URL
  construction, so a misconfigured APP_URL never crashes the metadata
  router initialisation.
- Restrict the SDK metadata router middleware to /.well-known/* paths
  only. Previously it was invoked on every request; in production the
  lazy getMetaRouter() init ran on GET / and threw "Invalid URL" when
  APP_URL had no scheme, returning 500 for every page load.
- Log a startup warning when APP_URL is set but not usable, and include
  the resolved App URL in the startup banner so operators can confirm
  the correct value at a glance.
- Update oauth.test.ts mock to target notifications.getMcpSafeUrl.

* fix: show getAppUrl in banner and add two separate APP_URL startup checks

- Banner now displays getAppUrl() (the resolved app URL) rather than
  getMcpSafeUrl() so operators see the actual configured value
- Two independent startup warnings after the banner when APP_URL is set:
  1. whether APP_URL is a valid URL (parseable by new URL())
  2. whether APP_URL is MCP-safe (https:// or http://localhost)
- Fix getMcpSafeUrl() fallback port to use Number(PORT) || 3001,
  consistent with how index.ts parses PORT

* fix: update oidc.ts to import getAppUrl from notifications
2026-05-07 13:49:39 +02:00
Julien G. 25f326a659 v3.0.16 — bug fixes (#964)
* fix(mcp): MCP RFC compliant for more strict clients

* fix(mcp): serve flat /.well-known/oauth-protected-resource for ChatGPT reconnect

Clients such as ChatGPT probe the flat well-known URL on every fresh discovery
cycle (i.e. after a full disconnect/reconnect where cached OAuth state is cleared).
The SDK's mcpAuthMetadataRouter only serves the path-based form
/.well-known/oauth-protected-resource/mcp, so the flat probe returned 404.

Without the resource metadata, ChatGPT fell back to the issuer URL as the
resource parameter (https://…/ instead of https://…/mcp). The authorize handler
then rejected it with invalid_target and redirected back to ChatGPT's callback
with an error — showing the user the TREK home page instead of the consent form.

Add an explicit GET handler for the flat URL that returns the same protected
resource metadata, so the resource URI is discovered correctly on the first probe.

* fix(mcp): fix OAuth popup blank page — SW denylist and COOP header

Service worker was intercepting /oauth/authorize navigate requests
(not in denylist), serving index.html, and React Router's catch-all
redirected to / instead of the SDK authorize handler.

Helmet's default COOP: same-origin isolated the /oauth/consent popup
from its cross-origin opener, making window.opener null and breaking
the popup-based OAuth completion signal for ChatGPT and similar clients.

* fix(ntfy): encode non-Latin-1 header values with RFC 2047 to prevent ByteString crash

Todo/trip names containing chars like → or € (and non-Latin-1 locale templates
for Czech, Chinese, Russian, etc.) caused the Fetch API to throw when setting
the ntfy Title header. Apply RFC 2047 base64 encoded-word encoding for any
header value containing chars above U+00FF; ntfy decodes this automatically.

* docs(mcp): document Cloudflare bot detection blocking ChatGPT MCP requests

Add Cloudflare WAF note to MCP-Setup and a full troubleshooting entry covering
root cause (IP reputation + UA heuristics), free-plan limitation (disable Bot
Fight Mode entirely, with explicit warning), and paid-plan WAF skip rule with
the full expression syntax and path table for all MCP/OAuth/.well-known routes.

* fix(pwa): detect upstream proxy auth challenges and recover gracefully

Behind Cloudflare Zero Trust or Pangolin, cross-origin auth redirects on
/api/* calls surface as CORS errors (error.response === undefined) that
the existing 401 interceptor never catches, leaving the PWA stuck with
network-error toasts instead of re-authenticating.

New connectivity module probes /api/health every 30s using fetch with
cache:no-store and inspects Content-Type to reliably detect whether the
server is reachable vs intercepted by an upstream proxy.

axios interceptor changes:
- On !error.response + navigator.onLine: run probeNow(); if the health
  probe also fails (proxy is intercepting all requests), trigger a guarded
  window.location.reload() so the edge proxy can intercept the top-level
  navigation and run its auth flow (covers CF Access and Pangolin 302 mode)
- On error.response status 401 with text/html body: same reload path,
  covering Pangolin header-auth extended compatibility mode which returns
  401+HTML instead of a 302 redirect. TREK own 401s are always JSON so
  there is no collision with the existing AUTH_REQUIRED branch.
- sessionStorage flag prevents reload loops; cleared on any successful
  response so the guard resets after re-auth.

/api/health excluded from SW NetworkFirst cache (vite.config.js regex)
and Cache-Control: no-store added server-side so probes always hit the
network and cannot be served stale from the 24h api-data cache.

LoginPage caches last-known appConfig in localStorage so the SSO button
renders in OIDC+UN/PW dual mode even when the config fetch is intercepted
by the proxy. Auto-redirect to IdP skipped when config comes from cache
to avoid redirect loops while the proxy is challenging.

Fixes discussion #836.

* fix(files): add bottom-nav padding to files tab wrapper on mobile

* fix(budget): expose toolbar on mobile so users can add budget categories

* fix(pwa): unregister SW before proxy-reauth reload so Pangolin can challenge

WorkBox's NavigationRoute served the cached SPA shell on window.location.reload(),
meaning Pangolin/CF Access never saw the navigation and the app was left stuck
showing stale offline data. Unregistering the SW first lets the navigation reach
the network so the upstream proxy can run its auth flow.

Also rebuilds server/public with corrected sw.js (health excluded from
NetworkFirst, /oauth/ and /.well-known/ added to NavigationRoute denylist).

* chore: remove committed build artifacts from server/public

Dockerfile and Proxmox community script both rebuild client/dist and copy
it into server/public at build time — committed artifacts were never used.
Replace with .gitkeep and add server/public/* to .gitignore.

* chore: add build-from-sources script
2026-05-06 21:38:40 +02:00
Maurice d7a71c0572 feat(notifications): reminders for todos with upcoming due dates
Todos already support a due_date field but nothing notifies the user
when a deadline is approaching — you'd only remember if you happened
to look at the Lists tab. This wires a reminder into the existing
notification pipeline so due-date todos behave like trip-start
reminders.

Details:
- New `todo_due` event type alongside trip_reminder; all four channels
  (in-app, email, webhook, ntfy) supported and toggleable per user in
  Settings > Notifications.
- New daily scheduler task (9 AM local TZ) queries unchecked todos
  whose due_date is within the next 3 days. Each todo gets at most
  one reminder per 24 hours, tracked via a new todo_items.reminded_at
  column (migration 116).
- If the todo has an assigned user, only that user is reminded; if
  not, every member of the trip gets the notification.
- Strings added in all 15 UI languages and for all notification
  carriers.
- Gated by app_settings.notify_todo_due (default on) so admins can
  disable it globally.
2026-04-20 17:31:25 +02:00
Maurice 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).
2026-04-20 14:06:42 +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 191d59166c Merge remote-tracking branch 'origin/dev' into feat/indonesian-translation 2026-04-15 06:28:35 +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 0257e0d842 feat: route Synology session-cleared notification through unified send()
Replace direct createNotification() call with notificationService.send()
so the notification respects user preferences and reaches all enabled
channels (in-app, email, webhook) instead of only WebSocket.

Registers synology_session_cleared as a proper NotifEventType (inapp-only)
and adds localized text for all 14 supported languages.
2026-04-11 18:36:50 +02:00
xenocent 577f2b05ca feat(i18n): add Indonesian translation 2026-04-11 15:26:16 +07:00
Julien G. 9b11abbf4a Merge pull request #434 from jerryhuangyu/feat/support-zh
feat(i18n): add Traditional Chinese (zh-TW) language support
2026-04-05 21:18:02 +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
jerryhuangyu dd21074c27 feat: Add Traditional Chinese (zh-TW) translations support 2026-04-05 23:53:26 +08: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
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
jubnl d4e16ebe49 fix: use APP_URL is defined as base url in mails 2026-04-03 03:44:45 +02:00
jubnl 1b28bd96d4 fix: encrypt SMTP password at rest using AES-256-GCM
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.
2026-04-01 07:56:43 +02:00
Andrei Brebene 9b2f083e4b feat: notifications, audit logging, and admin improvements
- 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
2026-03-31 22:23:23 +03:00
Maurice b1138eb9db fix: shared page language redirect + skip TLS for self-signed certs — closes #163 #164
- 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
2026-03-30 22:26:09 +02:00
Maurice d189d6d776 feat: email notifications, webhook support, ICS export — closes #110
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
2026-03-30 17:07:33 +02:00