Commit Graph

972 Commits

Author SHA1 Message Date
Paul Nothaft b00a16159e fix(security): harden .picpeak restore operator-preservation (GHSA-qxfx follow-up)
The req.admin.id fix activated reinjectCurrentAdmin(); hardening its preservation
logic (found across Codex review rounds of #811):

- MFA hijack: reinject wrote back only password_hash/is_active/
  must_change_password, leaving a crafted backup's two_factor_* on the
  operator's row — it could strip or replace their second factor. The email-
  matched row is now updated with the operator's full AUTH set (login identity,
  password, and all two_factor_* columns). Relationship/audit FKs (role_id,
  created_by) are deliberately NOT forced from the snapshot: on a cross-instance
  restore those pre-restore ids may be absent from the backup and would dangle
  the FK (SQLite rolls back at commit); the restored row keeps its own valid
  values.

- Cross-instance restore rollback / FK safety: reinject matched only by email,
  so a backup shipping a different admin with the default `admin` username hit
  UNIQUE(username) and rolled the whole restore back; email and username could
  even collide on two different rows. Reconciliation is now non-destructive:
  the email-matching row is updated in place (id preserved → restored FKs like
  events.created_by stay valid); any different row holding the operator's
  username is RENAMED, not deleted (deletion would fire ON DELETE actions /
  dangle references); only when no row has the operator's email is a fresh row
  inserted, with created_by nulled and an explicit max(id)+1 id (batchInsert
  left the Postgres identity sequence unadvanced, so a sequence-based insert
  could collide).

- Stale session after restore: admin_users ids shift on restore, but the
  operator's live JWT is bound only to decoded.id (IP logged not enforced; the
  backup controls password_changed_at). The route now revokes the token (result
  checked and logged) and clears the admin cookie; the client redirects to a
  fresh login via a sessionInvalidated flag. Cookie clear is the unconditional
  guarantee.

Adds SQLite-backed reinject regression tests (in-place login/MFA restore with id
and FK columns preserved, username-only rename, email+username on different rows,
clean insert with created_by nulled) and the frontend redirect on
sessionInvalidated.

Deferred (design decisions / pre-existing, need a Postgres test env — see PR
discussion): global "invalidate all pre-restore sessions" cutoff; preserving the
operator's ROLE semantics across an RBAC-table replace; and resyncing Postgres
identity sequences after any restore (batchInsert leaves them behind max(id) —
pre-existing, affects every restored table).
2026-07-16 12:31:28 +02:00
Paul Nothaft caa9fe5d56 chore(stable): release 3.45.0 (#777)
Build and Push Docker Images / build-backend (linux/amd64, ubuntu-latest) (push) Has been cancelled
Build and Push Docker Images / build-backend (linux/arm64, ubuntu-24.04-arm) (push) Has been cancelled
Build and Push Docker Images / merge-backend (push) Has been cancelled
Build and Push Docker Images / build-frontend (linux/amd64, ubuntu-latest) (push) Has been cancelled
Build and Push Docker Images / build-frontend (linux/arm64, ubuntu-24.04-arm) (push) Has been cancelled
Build and Push Docker Images / merge-frontend (push) Has been cancelled
Build and Push Docker Images / summary (push) Has been cancelled
2026-07-09 11:22:46 +00:00
Paul Nothaft c29ad747f2 chore(main): release 3.83.0-beta.0 (#770)
Build and Push Docker Images / build-backend (linux/amd64, ubuntu-latest) (push) Has been cancelled
Build and Push Docker Images / build-backend (linux/arm64, ubuntu-24.04-arm) (push) Has been cancelled
Build and Push Docker Images / merge-backend (push) Has been cancelled
Build and Push Docker Images / build-frontend (linux/amd64, ubuntu-latest) (push) Has been cancelled
Build and Push Docker Images / build-frontend (linux/arm64, ubuntu-24.04-arm) (push) Has been cancelled
Build and Push Docker Images / merge-frontend (push) Has been cancelled
Build and Push Docker Images / summary (push) Has been cancelled
2026-07-08 10:42:24 +00:00
Paul Nothaft a71b9b5ed7 Merge pull request #769 from Luca-Timo/feat/messages-email-client
feat(messages): unified Messages email client (flag-gated, default off)
2026-07-08 12:38:58 +02:00
Luca 1e08a4fb15 fix(messages): PR #769 nits — server-side search, bare-email recipient, DE i18n
- Search now hits the backend (debounced) so results aren't truncated to the
  first loaded page: /received gains a `q` filter (sender/subject); the frontend
  passes the debounced term to every list query. The instant client-side filter
  stays for responsiveness.
- Reply/compose recipient extracts the bare address from a "Name <addr>" From
  header (extractEmail) — also used for the customer-lookup key.
- Added the full de + en `messages.*` and `email.customerMailbox.*` translation
  namespaces (were English inline-fallbacks only). Swiss-German spelling.
2026-07-07 18:39:04 +02:00
Luca bb235e72e5 fix(messages): PR #769 review — escape reply sender (XSS), gate backend routes, exact customer match
- BLOCKER: stored XSS via inbound sender display name. The reply stub built raw
  HTML with the unsanitized From name and set it as innerHTML on the composer's
  contentEditable (admin origin) → onerror JS ran on Reply. Now HTML-escape
  from_address in the stub AND DOMPurify-sanitize the composer body before
  innerHTML (defense in depth).
- Gate the NEW Messages routes with requireFeatureFlag('messaging') per-route
  (queue/:id, received/:id, item/*, identities, accounts, accounts/test, send)
  — NOT the shared /email mount, so the pre-existing email-config endpoints stay
  ungated.
- DocumentActionModal auto-picks a customer only on an EXACT email match
  (customer search is prefix/fuzzy), else leaves the picker to the admin.
2026-07-07 18:28:26 +02:00
Luca 99d5996561 feat(messages): search bar + Archive/Delete with Archived & Deleted folders
- Search box in the header filters the current folder's list (sender/subject),
  client-side; works across the merged Archived/Deleted views too.
- Archive and Delete are now implemented as soft moves: migration 157 adds
  mailbox_state ('active'|'archived'|'deleted') to email_queue + received_emails.
  Archive → 'archived', Delete → 'deleted' (trash). Restore → 'active'. Deleting
  FROM the Deleted folder is permanent (hard row delete).
- New cross-account system folders Archived + Deleted (merge sent + received of
  that state, sorted by date). Normal folders now exclude archived/deleted.
- Backend: /queue + /received gain a `state` filter (default active + legacy
  NULL); new POST /item/:kind/:id/state (archive/delete/restore) and DELETE
  /item/:kind/:id (purge, email.edit).
- Toolbar Archive/Delete wired; Restore + "Delete permanently" shown in the
  system folders.

Frontend build + migration boot (157) verified.
2026-07-07 16:16:51 +02:00
Luca c8cb4c88ca harden(messages): SSRF guard on mailbox host, strict sandbox + sanitizer, per-account TLS
Pre-upstream review hardening:
- /accounts + /accounts/test now reject private/internal IMAP/SMTP hosts via
  isPrivateIP(), matching /config + /incoming-config (SSRF).
- QueueDetail body iframe uses sandbox="" (script-less, no same-origin) like the
  inbound pane, instead of allow-same-origin.
- /send sanitizer drops the <style> tag + data: scheme to match the stricter
  inbound sanitizeBody allowlist.
- Per-account SMTP transport sets tls.rejectUnauthorized explicitly.
2026-07-07 16:04:11 +02:00
Luca 2c5c1d561b fix(messages): show the resolved customer's name in the doc-action modal
CustomerPicker uses its 'label' prop as the selected-customer chip text, so
passing the static 'Customer' string hid the actual name. Pass the resolved
customer's name as label and add a separate field heading.
2026-07-07 15:45:59 +02:00
Luca 0dbf863f60 feat(messages): create/select quote, contract, invoice, gallery from a message
The toolbar doc buttons now open a real document-action flow instead of just
loading an email template:

- DocumentActionModal resolves the customer from the message's sender address
  (customers/search); if no match, the CustomerPicker lets you search or create
  a passive customer inline.
- Create new -> jumps to the real editor prefilled with the customer
  (quotes/contracts/bills ?customerAccountId=), so numbering, line items and PDF
  all come from the existing CRM. Gallery opens the event editor.
- Select existing -> lists that customer's quotes/contracts/invoices and drops
  the chosen document number into a reply composer.
- Toolbar buttons are gated by the global feature flags (quotes/contracts/bills).

Adds the missing customerAccountId prefill to ContractEditorPage (quotes + bills
already had it). Frontend-only; reuses existing endpoints. Build verified.
2026-07-07 13:16:18 +02:00
Paul Nothaft 72cbb95b44 chore(main): release 3.82.6-beta.0 (#768)
Build and Push Docker Images / build-backend (linux/amd64, ubuntu-latest) (push) Has been cancelled
Build and Push Docker Images / build-backend (linux/arm64, ubuntu-24.04-arm) (push) Has been cancelled
Build and Push Docker Images / merge-backend (push) Has been cancelled
Build and Push Docker Images / build-frontend (linux/amd64, ubuntu-latest) (push) Has been cancelled
Build and Push Docker Images / build-frontend (linux/arm64, ubuntu-24.04-arm) (push) Has been cancelled
Build and Push Docker Images / merge-frontend (push) Has been cancelled
Build and Push Docker Images / summary (push) Has been cancelled
2026-07-07 10:35:31 +00:00
Luca 88fe9f9844 fix(messages): show only the mailbox local part in the sidebar (full address on hover) 2026-07-07 12:32:08 +02:00
Luca f9c2b4ed75 fix(messages): dynamic addresses, branding accent, compose/sync, per-identity SMTP
Addresses dev-test feedback:
- Sidebar + reading-pane addresses (rechnungen@ / hello@ / no-reply@) are now
  read from the mail config via GET /admin/email/identities, not hardcoded.
- Highlight/selection now uses the branding accent (bg-accent-soft /
  text-on-accent-soft / accent-dark) instead of hardcoded blue, so it follows
  the admin's CI colour like the sidebar.
- Header gains "New message" (compose) and "Sync" (poll mailboxes now) buttons.
- Composer modal enlarged (920px, taller editable body).
- Customer mailbox (hello@) now has BOTH incoming (IMAP) and outgoing (SMTP)
  settings — migration 156 adds smtp_* + from_* to mail_accounts;
  emailProcessor.sendRawEmail takes an accountKey and sends via that mailbox's
  SMTP identity (falls back to the global from). Manual/reply sends from the
  Messages UI use the 'customers' identity, so replies come from hello@.

Frontend build + migration boot (156) verified.
2026-07-07 11:58:41 +02:00
Luca 976d52280e Merge pull request #356 from Luca-Timo/feat/messages-email-client
Feat/messages email client
2026-07-07 11:11:21 +02:00
Luca 0ed3bbefa1 chore(messages): describe the feature as 'unified' rather than by a third-party product name 2026-07-07 11:10:00 +02:00
Luca b96ad36f5d fix(messages): make the Messaging feature flag toggleable
The Messaging FeatureCard was a hardcoded-disabled 'roadmap' placeholder
(no-op toggle), so the messaging flag could never be turned on — the Messages
sidebar item + page stayed hidden. Wire the toggle to setFlag, mark it 'new',
and describe the actual admin Messages client.
2026-07-07 11:06:16 +02:00
Luca d0bdcb1a6a Merge pull request #355 from Luca-Timo/feat/messages-email-client
feat(messages): Outlook-style Messages email client (3 phases, flag-gated)
2026-07-07 10:48:46 +02:00
Luca 768e84711f feat(messages): Phase 3 — editable-template composer, reply + create actions
The CRM action buttons and Reply now open a send-composer, not a silent
templated send.

- New send-composer (MessageComposer): loads the rendered template (via
  previewTemplate) or a reply stub into a fully-editable body — the admin can
  rewrite it or drop a note anywhere before sending. On send it goes out as-is
  (server-sanitized), no template re-render.
- Backend: emailProcessor.sendRawEmail() sends admin-edited HTML via the
  configured SMTP identity; POST /admin/email/send sanitizes + sends + records
  the message in email_queue as a 'manual' send.
- Migration 155: email_queue.origin ('system' default | 'manual'). The Sent
  stream now splits by origin — Automated ▸ Sent = system, Customers ▸ Sent =
  the human/edited messages (which finally populates that folder). /queue gains
  an origin filter + returns origin.
- Toolbar wired: Reply enabled on inbound customer mail (prefilled + quoted);
  Create Quote/Contract/Invoice open the composer with that template loaded;
  Gallery opens a blank compose. Accounting/Forward/Archive/Delete stay disabled
  (later phases). After send, jumps to Customers ▸ Sent.

Deferred to a later phase: two-way IMAP write-back; per-identity SMTP (manual
sends currently use the global from address). Frontend build + migration boot
verified.
2026-07-07 10:42:54 +02:00
Luca ee46cf2125 feat(messages): Phase 2 — customer (hello@) mailbox + inbound body capture
Second inbound mailbox and real message bodies for the Messages viewer.

Backend:
- Migration 154: mail_accounts table (additional inbound mailboxes beyond the
  primary accounting IMAP) + received_emails.{account_key,to_address,body_html,
  body_text}. Additive/guarded.
- emailIntakeService now polls the accounting mailbox AND every enabled
  mail_accounts row. Extracted pollAccountOnce(cfg, {accountKey, routeToExpenses});
  accounting keeps its exact attachment->expenses behavior, customer mail is
  logged with its body and NOT routed to accounting. Inbound HTML is sanitized
  server-side (sanitize-html) on ingest.
- adminEmail: /received gains an account filter + returns account_key/to_address
  (bodies excluded from the list); new GET /received/:id returns the body;
  GET/POST /accounts + /accounts/test manage the extra mailboxes.

Frontend:
- Customers inbox now pulls the hello@ mailbox; reading pane renders the
  sanitized body in a strict (script-less, no same-origin) sandboxed iframe.
  Accounting inbox shows bodies too. Toolbar context keys off the mailbox.
- CustomerMailboxCard in Settings -> Email (behind the messaging flag) to
  configure + test the hello@ IMAP box.

No behavior change to the existing accounting inbound flow. Frontend build +
migration boot verified.
2026-07-07 10:11:58 +02:00
Paul Nothaft c0ac5c36a4 chore(main): release 3.82.5-beta.0 (#767)
Build and Push Docker Images / build-backend (linux/amd64, ubuntu-latest) (push) Has been cancelled
Build and Push Docker Images / build-backend (linux/arm64, ubuntu-24.04-arm) (push) Has been cancelled
Build and Push Docker Images / merge-backend (push) Has been cancelled
Build and Push Docker Images / build-frontend (linux/amd64, ubuntu-latest) (push) Has been cancelled
Build and Push Docker Images / build-frontend (linux/arm64, ubuntu-24.04-arm) (push) Has been cancelled
Build and Push Docker Images / merge-frontend (push) Has been cancelled
Build and Push Docker Images / summary (push) Has been cancelled
2026-07-07 07:08:02 +00:00
Paul Nothaft 945026d446 fix(admin): stop the event-date field crashing the page on backspace
Repro: create an event, click into the date field, backspace a day digit.
The whole page white-screened and needed a reload.

Root cause: LocalizedDateInput's `toIso` only checked the day/month were
1-2 digits, not that they formed a real date — so a mid-backspace value
like "0/07/2026" was coerced to the string "2026-07-00" and committed to
`event_date`. CreateEventPage then rendered
`format(addDays(new Date('2026-07-00'), days))`, and date-fns `format`
throws RangeError on an Invalid Date — thrown during render, so React
tore the tree down to the error boundary.

Two complementary fixes:
- `toIso` round-trips the parsed y/m/d through `Date` and rejects
  impossible dates (day 00, month 13, 31 Feb…), so the field never
  commits a value that isn't a real calendar date.
- `useLocalizedDate.format`/`formatDistanceToNow` guard with `isValid`
  and return '' instead of throwing — defence in depth for the ~57 call
  sites that could otherwise white-screen on a bad date.

Verified live: backspacing to a partial/invalid date no longer crashes
(the form stays rendered), a valid date still commits + the expiry
preview renders. Adds a LocalizedDateInput regression test; tsc + build
green.
2026-07-07 08:56:04 +02:00
Paul Nothaft 2522d7e1ce chore(main): release 3.82.4-beta.0 (#765)
Build and Push Docker Images / build-backend (linux/amd64, ubuntu-latest) (push) Has been cancelled
Build and Push Docker Images / build-backend (linux/arm64, ubuntu-24.04-arm) (push) Has been cancelled
Build and Push Docker Images / merge-backend (push) Has been cancelled
Build and Push Docker Images / build-frontend (linux/amd64, ubuntu-latest) (push) Has been cancelled
Build and Push Docker Images / build-frontend (linux/arm64, ubuntu-24.04-arm) (push) Has been cancelled
Build and Push Docker Images / merge-frontend (push) Has been cancelled
Build and Push Docker Images / summary (push) Has been cancelled
2026-07-07 05:13:53 +00:00
Luca 26eeb76197 feat(messages): Phase 1 read-only Messages viewer (email client shell)
New admin "Messages" page — a three-pane mail viewer over the mail picpeak
already stores, feature-flagged behind `messaging` (default off):

- Sidebar account tree: All mail / Customers (hello@) / Accounting (rechnungen@)
  / Automated (no-reply@), matching the agreed IA.
- Automated + All Sent = email_queue (listQueue); Accounting + All Inbox =
  received_emails (listReceived). Customers folders show an explanatory empty
  state pending the hello@ mailbox (Phase 2).
- Reading pane renders the sent body from rendered_html (migration 119) in a
  sandboxed iframe; new GET /admin/email/queue/:id returns body + cc +
  attachment filenames (disk paths never exposed).
- Received supplier invoices: envelope + rasterized PDF viewer reusing the
  accounting inbound blob endpoint, plus "Open in Accounting inbox".
- Context toolbar (Reply/Forward/Create Quote-Contract-Gallery-Invoice /
  Book-as-expense-Re-bill) present but disabled — wired in later phases.

Reuses email.service, accounting inbound blob endpoint, RequireFeature +
PermissionGate (email.view), Tailwind dark: theming. No schema change.
2026-07-07 03:15:41 +02:00
Luca fcc3e9195d fix(email,ui): billing emails follow customer language + readable payment-check confirmation
- Billing/dunning emails no longer render in the gallery event's language.
  emailProcessor now honors an explicit `__language` in the email data
  (else falls back to the event-first recipient resolution), and the invoice
  reminder passes the customer/invoice locale (customer.preferred_language
  || invoice.language || 'de'). Fixes German customers getting English
  dunning notices. (#760)
- Payment-check confirmation card ("Action recorded") is now theme-adaptive
  (green tint + readable text on both light and dark surfaces) instead of a
  hardcoded light-green mix + dark-green title that vanished in dark mode. (#759)
- The per-customer "Preferred language" field already exists
  (CustomerDetailPage) plus the business-profile default; updated the helper
  text to note billing emails now honor it too. (#761)
2026-07-06 19:27:57 +02:00
Paul Nothaft a52317d8a5 chore(main): release 3.82.3-beta.0 (#758)
Build and Push Docker Images / build-backend (linux/amd64, ubuntu-latest) (push) Has been cancelled
Build and Push Docker Images / build-backend (linux/arm64, ubuntu-24.04-arm) (push) Has been cancelled
Build and Push Docker Images / merge-backend (push) Has been cancelled
Build and Push Docker Images / build-frontend (linux/amd64, ubuntu-latest) (push) Has been cancelled
Build and Push Docker Images / build-frontend (linux/arm64, ubuntu-24.04-arm) (push) Has been cancelled
Build and Push Docker Images / merge-frontend (push) Has been cancelled
Build and Push Docker Images / summary (push) Has been cancelled
2026-07-06 09:53:29 +00:00
Paul Nothaft a88da99c8d Merge pull request #757 from PicPeak/fix/hero-logo-global-inherit-756
fix(branding): make 'Show logo in hero' a true global toggle with per-event override (#756)
2026-07-06 11:50:06 +02:00
Paul Nothaft 60b03b1728 fix(branding): unify hero logo SIZE the same way as visibility (#756)
Follow-on to the visibility fix in this PR — hero_logo_size had the same
split-brain: GalleryLayout read the global branding_logo_size live while
the hero-header path used the per-event snapshot, so a hero logo could
render at different sizes on different layouts and the global size didn't
reach hero-header galleries.

Now mirrored on the visibility model: NULL per-event hero_logo_size =
inherit branding_logo_size; explicit = override.

- Migration 153: hero_logo_size nullable + backfill NULL so existing
  galleries inherit the global size (restores GalleryLayout's prior
  live-global behaviour and fixes the hero-header staleness).
- Creation stores NULL unless explicit; gallery.js resolves
  per-event ?? global and sends the effective size.
- GalleryLayout now consumes that resolved size for the hero logo (new
  heroLogoSize prop) instead of the global — both render paths match.
- Admin size control gains a 'Use branding default' (inherit) option.

Verified: migration on SQLite + PG; live resolution (inherit follows
global both ways, override wins); creation stores NULL on PG; tsc clean,
106 adminEvents+gallery tests pass, build green.
2026-07-06 11:44:33 +02:00
Paul Nothaft 96fe478bf8 fix(branding): make 'Show logo in hero' a true global toggle with per-event override (#756)
Before: the global branding_logo_display_hero toggle was only a
creation-time default — snapshotted into each event's hero_logo_visible
column at creation and never consulted again. Disabling it did nothing
to existing galleries (the reporter's bug), and the two gallery render
paths disagreed (GalleryLayout read the global, HeroHeader read the
per-event snapshot).

Now: NULL per-event hero_logo_visible = 'inherit the global toggle';
an explicit true/false is a per-gallery override.

- Migration 152: make events.hero_logo_visible nullable and NULL out the
  defaulted  rows so existing galleries follow the global going
  forward. Deliberate per-gallery hides () are preserved.
- Creation stores NULL unless the admin explicitly sets it; the update
  path preserves NULL.
- gallery.js resolves per-event ?? global (branding_logo_display_hero,
  default true) and sends the EFFECTIVE value on both gallery responses.
- Both frontend render paths now consume that resolved value
  (GalleryLayout gets it via a new heroLogoVisible prop).
- Admin per-event control is now tri-state: Use branding default /
  Always show / Always hide (en + de).

Verified: SQLite migration + live resolution (inherit follows global
both ways; override wins both ways); PG migration SQL dry-run; the admin
tri-state renders 'Use branding default' for an inherited event; tsc
clean, 106 adminEvents+gallery tests pass, build green.
2026-07-06 11:20:36 +02:00
Paul Nothaft 43213b50ce chore(main): release 3.82.2-beta.0 (#755)
Build and Push Docker Images / build-backend (linux/amd64, ubuntu-latest) (push) Has been cancelled
Build and Push Docker Images / build-backend (linux/arm64, ubuntu-24.04-arm) (push) Has been cancelled
Build and Push Docker Images / merge-backend (push) Has been cancelled
Build and Push Docker Images / build-frontend (linux/amd64, ubuntu-latest) (push) Has been cancelled
Build and Push Docker Images / build-frontend (linux/arm64, ubuntu-24.04-arm) (push) Has been cancelled
Build and Push Docker Images / merge-frontend (push) Has been cancelled
Build and Push Docker Images / summary (push) Has been cancelled
2026-07-05 21:43:21 +00:00
Paul Nothaft a0a28a4777 fix(og): broaden social-crawler coverage (Bluesky Cardyb, WeChat-scraper, fediverse, etc.)
From alexvaltchev's field UA list on #699. Adds CRAWLER-EXCLUSIVE tokens
to both the nginx UA regex and SOCIAL_CRAWLER_PATTERNS (kept in sync):
Cardyb (Bluesky's actual link-card fetcher), facebookcatalog, Signal,
Misskey, Pleroma, Synapse, Nextcloud, Rocket.Chat, kakaotalk-scrap,
Google-PageRenderer, OdklBot, ZoomBot.

Deliberately NOT added: UAs shared with real human in-app browsers
(WeChat MicroMessenger, LINE 'Line/', Zalo) and broad strings
('InAppBrowser', 'preview', 'unfurl', 'XING' → matches 'boxing'). Our OG
response is meta-only with no redirect, so matching those would serve a
human the bare stub. New negative test locks that exclusion in.

Verified: nginx -t passes; live harness confirms the new tokens rewrite
to /og while the in-app-browser UAs still get the SPA. Backend suite 15/15.
2026-07-05 23:37:16 +02:00
Paul Nothaft 0dffe0ce92 fix(og): route branded short URLs + slideshow links to OG, add Viber (#699)
Follow-up to #699/#700/#702 — the OG SSR handler existed but three link
shapes never reached it behind the frontend nginx:

- Branded short URLs (/s/<slug>, #702) had NO nginx location, so they fell
  through to the SPA — which has no /s/ route. Dead for humans (no 302
  redirect) and crawlers (no OG). Add an ^~ /s/ proxy to the backend, whose
  /s/:shortSlug route already handles both.
- Slideshow links (/gallery/<slug>/show/<token>) have TWO extra path
  segments; the crawler-detect location regex allowed only one, so they
  never rewrote to /og and got generic site-wide OG. Widen to {0,2} extra
  segments (quoted regex — the braces would otherwise be parsed as nginx
  config delimiters). client-access still matches (its token is in ?query,
  one path segment).
- Viber's preview fetcher wasn't in either UA list, so Viber shares showed
  no preview. Add it to nginx + SOCIAL_CRAWLER_PATTERNS (kept in sync).

Verified end-to-end: nginx -t passes; a live nginx+mock-backend harness
confirms /s/ proxies to the backend, slideshow + Viber + share-token +
client-access crawler UAs all rewrite to /og/gallery/<slug>, and browsers
still get the SPA. Backend isSocialCrawler test extended for Viber.
2026-07-05 21:19:53 +02:00
Paul Nothaft f15e104702 chore(main): release 3.82.1-beta.0 (#753)
Build and Push Docker Images / build-backend (linux/amd64, ubuntu-latest) (push) Has been cancelled
Build and Push Docker Images / build-backend (linux/arm64, ubuntu-24.04-arm) (push) Has been cancelled
Build and Push Docker Images / merge-backend (push) Has been cancelled
Build and Push Docker Images / build-frontend (linux/amd64, ubuntu-latest) (push) Has been cancelled
Build and Push Docker Images / build-frontend (linux/arm64, ubuntu-24.04-arm) (push) Has been cancelled
Build and Push Docker Images / merge-frontend (push) Has been cancelled
Build and Push Docker Images / summary (push) Has been cancelled
2026-07-05 17:27:41 +00:00
Paul Nothaft c3ed7f1693 chore(main): release 3.82.0-beta.0 (#742)
Build and Push Docker Images / build-backend (linux/amd64, ubuntu-latest) (push) Has been cancelled
Build and Push Docker Images / build-backend (linux/arm64, ubuntu-24.04-arm) (push) Has been cancelled
Build and Push Docker Images / merge-backend (push) Has been cancelled
Build and Push Docker Images / build-frontend (linux/amd64, ubuntu-latest) (push) Has been cancelled
Build and Push Docker Images / build-frontend (linux/arm64, ubuntu-24.04-arm) (push) Has been cancelled
Build and Push Docker Images / merge-frontend (push) Has been cancelled
Build and Push Docker Images / summary (push) Has been cancelled
2026-07-03 11:10:31 +00:00
Paul Nothaft dadaaeea77 feat(setup): final community/thank-you step (#732); fix create-admin button overflow (#730)
#730 — the account step's Create-admin button shared a flex row with Back;
its label + loading spinner exceeded the card width, so the button
overflowed the card outline while submitting (and was fragile for longer
i18n labels). Stack both buttons full-width — the primary always has room
for the spinner now, matching every other wizard step.

#732 — add a final 'community' step, shown once on first-run after
config / no-config, before entering the app. Mission line + four link
cards (report a bug, request a feature, star/share, Buy Me a Coffee),
all target=_blank rel=noopener, and a Finish → Dashboard button. Fully
i18n (en + de). Restore keeps its reload flow (a restored instance is no
longer first-run, so it never reaches this step). Adds .github/FUNDING.yml
so GitHub renders a Sponsor button too.

Verified live: drove the real first-run wizard end to end — stacked
account buttons render inside the card, community step shows the mission
+ all four links, Finish lands on the dashboard.
2026-07-03 12:29:58 +02:00
Paul Nothaft c76877c0c4 chore(main): release 3.81.0-beta.0 (#740)
Build and Push Docker Images / build-backend (linux/amd64, ubuntu-latest) (push) Has been cancelled
Build and Push Docker Images / build-backend (linux/arm64, ubuntu-24.04-arm) (push) Has been cancelled
Build and Push Docker Images / merge-backend (push) Has been cancelled
Build and Push Docker Images / build-frontend (linux/amd64, ubuntu-latest) (push) Has been cancelled
Build and Push Docker Images / build-frontend (linux/arm64, ubuntu-24.04-arm) (push) Has been cancelled
Build and Push Docker Images / merge-frontend (push) Has been cancelled
Build and Push Docker Images / summary (push) Has been cancelled
2026-07-03 10:08:29 +00:00
Paul Nothaft f564b38c5a Merge remote-tracking branch 'origin/main' into refactor/codebase-cleanup
# Conflicts:
#	backend/src/routes/adminEvents.js
#	backend/src/routes/protectedImages.js
#	frontend/src/pages/admin/EventDetailsPage.tsx
2026-07-03 11:54:01 +02:00
Paul Nothaft cf073615ef Merge pull request #739 from PicPeak/feat/admin-mfa
feat: admin two-factor authentication (TOTP) with recovery codes + CLI reset
2026-07-03 11:49:07 +02:00
Paul Nothaft b732974779 Merge pull request #737 from PicPeak/fix/auth-access-control
fix(security): cross-event thumbnail leak, bulk-op ownership bypass + auth hardening
2026-07-03 11:47:25 +02:00
Paul Nothaft b187f588b4 Merge pull request #734 from PicPeak/fix/event-create-nan-and-bool-render
fix: event creation 500s on PostgreSQL (NaN slideshow seed) + stray "0" boolean renders
2026-07-03 11:46:58 +02:00
Paul Nothaft 96e3c68b9d feat(admin-ui): TOTP MFA enrollment + two-step login; remove stub 2FA toggle
Frontend for #738.
- mfa.service.ts + MfaSettingsCard (Settings → General → Admin Account):
  per-user setup (QR + manual secret + verify), recovery codes shown once
  (copy/download/confirm), status, regenerate, disable. Renders for
  super_admin (closes #735).
- Two-step login in AdminLoginPage: on {mfaRequired,mfaToken} swap to a
  code step (TOTP or recovery), call /auth/admin/login/mfa; handle
  MFA_INVALID / MFA_SESSION_EXPIRED / 423 lockout.
- Removed the non-functional global enable_2fa checkbox from SecurityTab
  (and its persistence) — replaced with a note pointing to per-user setup.
- en + de i18n.

Verified live in-browser: enroll (QR→code→recovery codes), logout, and
the two-step challenge into the dashboard as super_admin.
2026-07-03 11:44:07 +02:00
Paul Nothaft 081f3edcdf fix(security): close cross-event thumbnail leak, bulk-op ownership bypass, + hardening
Auth/access-control audit fixes (all pre-existing on main; none are
regressions). Verified end-to-end where noted.

HIGH
- Thumbnail enumeration: photoAuth granted any gallery token access to any
  flat /thumbnails/thumb_* file, so a visitor to one gallery could
  enumerate another (password-protected) gallery's entire thumbnail set.
  Scope thumbnail access to the token's event via photos.thumbnail_path.
  Live-verified: cross-event fetch now 404s, own-event still 200s.
- Bulk ownership bypass: bulk-archive/bulk-delete acted on body-supplied
  event ids with no owner filter (single-event routes enforce
  requireEventOwnership), letting admin/editor archive or cascade-delete
  any event. Add filterOwnedEventIds; also guard rename + import-external;
  tighten photo-retry to scope admin (not just editor). Fix misleading
  bulk-delete comment.

MED
- verifyGalleryAccess never checked decoded.type — assert 'gallery'
  instead of relying on other token types incidentally lacking eventId.
- secure-images generate-token/secure-download missing denySlideshowToken
  (#646 bypass): a leaked slideshow token could download originals.
- Frontend: AuthenticatedImage + api.ts attached the gallery bearer token
  to absolute/external URLs — only attach to relative same-app paths.

LOW hardening
- Pin algorithms:['HS256'] on all auth-boundary jwt.verify calls.
- crypto.timingSafeEqual for share-token + HMAC compares (utils/timingSafe).
- Remove dead photoAuth import in galleryFeedback.

Tests: new regression suites for thumbnail scoping + filterOwnedEventIds;
fixed verifyGalleryAccess.customerRevoke fixture (real customer tokens
carry type:'gallery'). Full backend suite at the pre-existing baseline
(5 suites/27 tests fail on main too), zero new failures.
2026-07-03 10:27:28 +02:00
Paul Nothaft 5b26dbd935 chore(main): release 3.80.0-beta.0 (#736)
Build and Push Docker Images / build-backend (linux/amd64, ubuntu-latest) (push) Has been cancelled
Build and Push Docker Images / build-backend (linux/arm64, ubuntu-24.04-arm) (push) Has been cancelled
Build and Push Docker Images / merge-backend (push) Has been cancelled
Build and Push Docker Images / build-frontend (linux/amd64, ubuntu-latest) (push) Has been cancelled
Build and Push Docker Images / build-frontend (linux/arm64, ubuntu-24.04-arm) (push) Has been cancelled
Build and Push Docker Images / merge-frontend (push) Has been cancelled
Build and Push Docker Images / summary (push) Has been cancelled
2026-07-03 08:05:34 +00:00
Paul Nothaft e513e8345b Merge pull request #731 from Luca-Timo/feat/setup-wizard-and-backup-roundtrip
feat: first-run setup wizard (feature selection + config) and portable .picpeak backup roundtrip
2026-07-03 10:03:22 +02:00
Paul Nothaft 766351b588 fix: mirror #734 onto decomposed files (PG NaN slideshow seed, SQLite bool renders)
Same two pre-existing-on-main bugs, at their post-decomposition
locations: clampIntOrUndefined in adminEvents/crud.js slideshow seed;
!! coercion in EventDetailsHeader, EventInformationCard,
ClientAccessCard. Keeps this branch correct in either merge order with
#734 — when merging main afterwards, resolve the adminEvents.js
modify/delete conflict by keeping the deletion.
2026-07-03 09:01:01 +02:00
Paul Nothaft 760c3d7b67 fix(admin): stray literal "0" rendered from SQLite integer booleans
On SQLite deployments boolean event columns come back as 0/1, and
{event.is_draft && ...} renders the 0 as a literal text node. Visible on
the event details page in three spots: above the tab bar (is_draft),
in the download-protection badge row (disable_right_click /
enable_devtools_protection / watermark_downloads), and in the Client
Access card (client_access_enabled). Coerce with !! at the render sites.
2026-07-03 08:57:48 +02:00
Paul Nothaft 2ea26a4962 fix: conform moved code to eslint indent/quotes, 4-arg mutation callbacks
- eslint --fix on branch-changed backend files (indent shift from the
  module-wrapper nesting in decomposed files); backend lint now 904
  errors vs 1,315 on main
- useMutationWithToast forwards all four TanStack v5 callback args
  (tsc -b strict build flagged the 3-arg passthrough)
2026-07-03 08:17:14 +02:00
Paul Nothaft b5eafc52bd refactor(frontend): decompose EventDetailsPage and ThemeCustomizerEnhanced
Move-code split, entry paths/exports unchanged:
- EventDetailsPage.tsx (2,697 -> 679) + pages/admin/event-details/* (16 files)
- ThemeCustomizerEnhanced.tsx (1,541 -> 349) + admin/theme-customizer/* (11)
Known ephemeral-UI delta: widget-local state (copied-link flags, unsaved
PIN input, modal selection) now resets when a tab unmounts.
2026-07-03 08:10:25 +02:00
Paul Nothaft 51f827774a refactor(frontend): extract shared PhotoCard from gallery layouts
One card implementation (hover overlay, download/expand/select, likes,
identity flow, lazy render) replaces per-layout copies in Masonry,
Justified, Grid, Mosaic, Timeline (-1270/+395 in layouts). Carousel,
Premium, Story stay bespoke — different DOM/design by intent.
2026-07-03 07:49:45 +02:00
Paul Nothaft 0f230f53fb refactor(frontend): useMutationWithToast + useModal hooks, migrate admin surfaces
- 92 mutations across 40 files moved to useMutationWithToast
  (success/error toast + invalidateKeys); complex flows left as-is
- 24 boolean modal flags moved to useModal
- Mutations without an original onError intentionally not migrated
  to avoid introducing new error toasts
2026-07-03 07:49:45 +02:00
Paul Nothaft eb71fcf209 refactor: remove dead files, dedupe formatBytes and document numbering helpers
- Delete unused adminEvents-enhanced.js, backupService.original.js,
  databaseBackup.example.js, s3Storage.example.js, ThemeCustomizer.tsx
- Extract shared formatBytes to utils/formatBytes.js (was copied 4x)
- Centralize formatNumberInTemplate + next-document-number logic in
  utils/documentSequences.js (was copied in invoice/quote/contract services)
2026-07-03 07:25:07 +02:00