Commit Graph

160 Commits

Author SHA1 Message Date
Ronni Baslund 8a9fd36f33 feat(ocis): dezky whitelabel theme for the files web UI
ci / typecheck (map[dir:apps/booking name:booking]) (push) Has been cancelled
ci / typecheck (map[dir:apps/portal name:portal]) (push) Has been cancelled
ci / typecheck (map[dir:apps/website name:website]) (push) Has been cancelled
ci / typecheck (map[dir:services/platform-api name:platform-api]) (push) Has been cancelled
ci / test (push) Has been cancelled
Skin OCIS web in the dezky brand so users don't see ownCloud/Infinite Scale.

- Custom theme.json (WEB_UI_THEME_PATH + WEB_ASSET_THEMES_PATH): dezky name,
  slogan, logos (light wordmark for the dark top bar, dark wordmark for the
  light login, favicon), and the full dezky palette — carbon chrome, signal
  yellow as a sparing accent, paper/bone surfaces, dezky semantic colours
- Pin the light theme as default (single variant) so OS-dark / auto-system
  always resolves to it
- Override only index.html via WEB_ASSET_CORE_PATH (OCIS falls back to the
  embedded core per-file): hide the ".versions" footer ("Infinite Scale … /
  ownCloud Web UI …") and set the pre-hydration <title>/theme-color to dezky

Apache-2.0 lets us drop the ownCloud marks without trademark fees. NOTE:
index.html pins the built bundle hashes — refresh it after an OCIS image bump.
2026-06-07 12:14:04 +02:00
Ronni Baslund b7f10eb092 fix(portal): app launcher opens real per-service hosts
The "Jump to" launcher only navigated for the internal tiles (Personal /
Admin / Partner); every external app (Mail, Drev, Møder, …) just fired a
toast and never opened. Hosts were also hardcoded to *.dezky.com, with Drev
pointing at a vanity drev. subdomain instead of the real OCIS host.

- Open external apps in a new tab at https://<host>.<baseDomain>
- Derive the base domain from the portal's own hostname so links resolve in
  every environment (app.dezky.local → dezky.local, app.dezky.com → dezky.com)
- Map Drev → files (OCIS); mail/meet/chat/cal/contacts/docs use their service
  subdomain
2026-06-07 12:13:59 +02:00
Ronni Baslund 98e49bfe34 feat(admin/users): editable member drawer + mailbox & ownership management
ci / typecheck (map[dir:apps/booking name:booking]) (push) Has been cancelled
ci / typecheck (map[dir:apps/portal name:portal]) (push) Has been cancelled
ci / typecheck (map[dir:apps/website name:website]) (push) Has been cancelled
ci / typecheck (map[dir:services/platform-api name:platform-api]) (push) Has been cancelled
ci / test (push) Has been cancelled
Rebuild the /admin/users detail drawer from a read-only profile into an
editable, Office 365-style panel with four sections:

- Username & mail: read-only primary for mailbox users; editable sign-in
  (Authentik-only) for mailbox-less identities; "Create mailbox" provisions
  a Stalwart inbox for an external-login admin
- Aliases: list/add/remove mailbox aliases (Stalwart), domain-scoped
- Role: member/admin toggle with a primary-account lock (owner, mailbox-less
  bootstrap admin, self) and a last-admin guard
- Contact information: display name, first/last name, phone, alternative
  email — mirrored best-effort to Authentik attributes + mailbox name

Ownership transfer: "Make owner" (row menu + drawer) plus an owner-side
"Transfer ownership" picker, gated to tenant admins / platform admins so a
departed owner can be replaced; promotes the target and demotes the prior
owner to admin.

Backend (platform-api): contact fields on User; AuthentikClient.updateUser;
StalwartClient.setMailboxName; UsersService updateTenantMember,
changeMemberPrimaryEmail, list/add/removeMemberAlias, createMailboxForMember,
transferOwnership; new DTOs and tenant-member routes. All mutations audited.

Portal: Nuxt proxies for the new endpoints + extended TenantUserDoc.
2026-06-07 10:34:53 +02:00
Ronni Baslund 90e8a22de4 feat(scheduling): calendar_failed badge + admin "retry now" action
ci / typecheck (map[dir:apps/booking name:booking]) (push) Has been cancelled
ci / typecheck (map[dir:apps/portal name:portal]) (push) Has been cancelled
ci / typecheck (map[dir:apps/website name:website]) (push) Has been cancelled
ci / typecheck (map[dir:services/platform-api name:platform-api]) (push) Has been cancelled
ci / test (push) Has been cancelled
Surface pending/calendar_failed booking states in the admin bookings list with
proper status badges (failed shows the last calendar error as a tooltip), and
add an operator "Retry now" action. The retry re-drives the same Stalwart
calendar write (confirm + attendee email on success); for a terminal
calendar_failed booking it re-claims the slot lock atomically first and refuses
if the time was taken in the meantime, so a manual retry can never double-book.
2026-06-07 09:39:42 +02:00
Ronni Baslund 35bc7b6c31 chore(infra): production manifests + CI for scheduling apps
ci / typecheck (map[dir:apps/booking name:booking]) (push) Has been cancelled
ci / typecheck (map[dir:apps/portal name:portal]) (push) Has been cancelled
ci / typecheck (map[dir:apps/website name:website]) (push) Has been cancelled
ci / typecheck (map[dir:services/platform-api name:platform-api]) (push) Has been cancelled
ci / test (push) Has been cancelled
2026-06-07 09:27:44 +02:00
Ronni Baslund b2c2650af9 test(scheduling): property-based slot tests + guarded Stalwart integration test 2026-06-07 09:23:16 +02:00
Ronni Baslund 8bbb7881a4 feat(scheduling): tenant scheduling overview/analytics 2026-06-07 09:17:01 +02:00
Ronni Baslund 95cbdc4e3d feat(scheduling): round-robin team event types 2026-06-07 09:14:08 +02:00
Ronni Baslund b9b4d56a2d feat(scheduling): tenant webhooks for booking lifecycle 2026-06-07 09:08:45 +02:00
Ronni Baslund e33b7f18a3 feat(scheduling): pluggable captcha (Turnstile) on public booking 2026-06-07 09:02:35 +02:00
Ronni Baslund e1a77b085f feat(scheduling): optional JWT-authed dezky Meet rooms 2026-06-07 08:58:00 +02:00
Ronni Baslund 851018f481 feat(scheduling): date-overrides UI for availability 2026-06-07 08:55:52 +02:00
Ronni Baslund f41475ac3b feat(scheduling): ignoreAllDayEvents option 2026-06-07 08:53:31 +02:00
Ronni Baslund 2cb13a1a14 feat(scheduling): retry calendar writes for pending bookings
A failed Stalwart calendar write during confirmation no longer deletes the
booking + SlotLock. The booking stays 'pending' with its lock retained, and a
new @Cron worker (every 2 min, max 5 attempts by default) re-drives the write:
on success it promotes to 'confirmed' and sends the confirmation email; after
the cap it moves to the terminal 'calendar_failed' state and releases the lock.

Tracks calendarWriteAttempts + lastCalendarError on the Booking. The public
confirm endpoint still throws 503 on a failed first write (preserving the DoD:
never surface a confirmed booking without a calendar event); the pending row is
left for the background retry to finish.
2026-06-07 08:49:53 +02:00
Ronni Baslund 9e1defa946 feat(scheduling): booking reminder emails 2026-06-07 00:31:33 +02:00
Ronni Baslund 3831c85285 feat(infra): production host bootstrap and bare-metal Stalwart scaffolding
Host provisioning for the single-server production target: SSH + firewall
hardening (nftables allowlist), k3s node registration, bare-metal Stalwart
install with systemd units and TLS cert-sync from the cluster secret, and
Restic encrypted backup/restore (primary + DR) with timer units. Host-specific
secrets live in config.env (gitignored); config.env.example is the template.
Also gitignores MemPalace per-project files.
2026-06-07 00:19:48 +02:00
Ronni Baslund 5ed3d2bc5f feat(scheduling): dezky Scheduling — Calendly-style booking on Stalwart calendars
First-party booking system on top of Stalwart calendars (no third-party
scheduling dependency). Hosts expose public booking pages; visitors pick a
slot computed from the host's live Stalwart free/busy, and confirming writes
the event to the host's calendar and sends a dezky-branded confirmation with
an .ics.

platform-api (services/platform-api/src/scheduling):
- Schemas: Host, StalwartCredential (AES-256-GCM at rest), AvailabilitySchedule,
  EventType, Booking, SlotLock (unique (hostId,startUtc) + TTL).
- StalwartCalendarModule: JMAP gateway (free/busy via Principal/getAvailability,
  event create/delete, scheduleAgent=client) + on-behalf app-password
  provisioning. CredentialCipher for at-rest encryption.
- DST-correct slot engine (Luxon) with unit tests; two-layer double-booking
  guard (atomic SlotLock + live free/busy re-check).
- Booking confirm/cancel/reschedule, branded email + .ics via JMAP submission,
  self-service manage tokens. /api/v1 public + tenant-gated admin routes,
  per-IP rate limiting.

apps/booking: standalone public, whitelabel booking app (booking.dezky.eu) —
path-based tenant resolution, per-tenant brand colour, booking + manage flows.

apps/portal: admin scheduling page (hosts, event types, availability, bookings
with edit/delete + admin cancel/reschedule) and proxy routes.

infra: booking dev service in docker-compose; scheduling env vars.
2026-06-07 00:17:36 +02:00
Ronni Baslund aee8f13899 feat(mail): tenant alias and distribution-list management via Stalwart
Customer-admin Mail settings backed by Stalwart JMAP: per-tenant aliases
(extra addresses routing to a mailbox) and distribution lists (one address
fanning out to many recipients). Adds StalwartClient x:Alias/x:MailingList
methods, a tenant-scoped MailController/MailService, the portal Mail settings
page and its proxy routes, and the mailboxAddress field on TenantUserDoc.
Removes the old mock mail data now that the page reads live data.
2026-06-07 00:16:30 +02:00
Ronni Baslund 04191193c2 feat(website): gated coming-soon holding page with email signup
Add a standalone bilingual /coming-soon page (branded, dark, email signup
via mailto:info@dezky.eu, fires a waitlist-signup Umami event, noindex) plus
a global middleware that redirects every route to the locale-correct holding
page while NUXT_PUBLIC_COMING_SOON=true.

- Env-gated (default off, so dev and the live site are unaffected); flip the
  env in Coolify to show/hide the holding page with no code change.
- Preview the real site behind the gate via ?preview=<previewToken>
  (NUXT_PUBLIC_PREVIEW_TOKEN), which sets a 7-day cookie.
- Locale-preserving redirects (/da/* -> /da/coming-soon), no loops.
2026-06-06 21:51:26 +02:00
Ronni Baslund c9e22ec117 build(website): Dockerfile + production start script for Coolify
Make the marketing site deployable standalone from apps/website (the repo
has no root package.json, so the build must run in this subdir — set Base
Directory to apps/website in Coolify).

- Add a multi-stage Dockerfile (pnpm build -> node .output/server/index.mjs,
  port 3000, NUXT_PUBLIC_SITE_URL default) + .dockerignore.
- Add a "start" script for the Nixpacks path (Nuxt SSR has no default start).
- Guard the bind-mount-only /shared-packages components dir with existsSync
  so standalone builds don't warn on the missing path (the site uses no
  shared component).
2026-06-06 21:33:41 +02:00
Ronni Baslund 554cb99f2c feat(website): Umami analytics (conversion + CTA events)
Add the Umami tracker (cookieless, no consent banner) in the document head,
limited to the production hostnames via data-domains so dev traffic doesn't
pollute the stats. Pageviews are auto-tracked per page and locale.

Custom events on the key funnel:
- demo-request (demo form submit, with teamSize)
- partner-application (partner form submit, with type)
- book-demo (every "Book a demo" CTA click) via data-umami-event
- login (clicks through to the app)

Also fix the mobile nav menu links, which weren't localized (would drop
Danish visitors back to English).
2026-06-06 21:14:42 +02:00
Ronni Baslund 7bee161ac1 feat(website): bilingual i18n (English default, Danish at /da) + SEO
Add @nuxtjs/i18n: English is the default locale (no prefix), Danish lives
under /da (prefix_except_default). Both server-rendered and indexed with
hreflang alternates + per-locale canonical (useLocaleHead in app.vue).
First-visit browser language is auto-detected and remembered in the
i18n_redirected cookie (redirectOn root).

- Keep the hand-authored COPY object; useLang/useCopy now read the i18n
  locale; useLocalizeHref/useLangToggle added. Every internal link is
  localized so navigation stays in-locale.
- Clear segmented EN|DA language switcher (active segment filled) replacing
  the ambiguous "en · da" pill.
- SEO: useSeoMeta defaults are locale-aware; og:image switches per locale
  (English / Danish share card); favicon links; robots.txt + sitemap.xml;
  env-aware siteUrl/baseUrl (localhost in dev, dezky.eu in prod).
- Update the cookie policy (dezky-lang -> i18n_redirected).
- Gate the Traefik wss:443 HMR behind DEZKY_TRAEFIK so localhost dev/DevTools
  connect over plain ws (fixes the DevTools disconnect loop).
2026-06-06 20:46:26 +02:00
Ronni Baslund 3f0298e011 feat(website): favicon & social share image assets
Add the dezky node-mark favicon (SVG + 32px / apple-touch PNG fallbacks)
and 1200x630 OpenGraph share cards — English (og-image.png, the default
locale) and Danish (og-image-da.png), on-dark mark + hero headline.
2026-06-06 20:45:57 +02:00
Ronni Baslund bd96ef0a33 feat(website): partner application form on /partners
Replace the bare partner CTA with a "Become a partner" section: a pitch
column (with a secondary "book a partner call" link to /demo) plus an
application form — name, company, work email, website, partnership type
(reseller / white-label / not sure) and message. On submit it composes a
prefilled email to info@dezky.eu (interim, no backend), to be swapped for
a real intake/CRM later. Bilingual and responsive.
2026-06-06 19:28:35 +02:00
Ronni Baslund 1ace447f47 style(website): lowercase brand name, info@ email, European tagline
- Write the brand name as lowercase "dezky" across all user-facing copy
  (the legal entity "Dezky ApS" stays capitalised).
- Change the general contact email kontakt@dezky.eu -> info@dezky.eu.
- Footer tagline now "European" rather than only Danish business.
- Compare table header logo uses the on-dark treatment (signal-green
  squircle + carbon d) instead of a low-contrast green d on a light chip.
2026-06-06 19:19:26 +02:00
Ronni Baslund d2096eb847 feat(website): book-a-demo & status pages, expand roadmap/contact
- /demo: book-a-demo page with a what-to-expect column + a form that
  composes a prefilled email to info@dezky.eu (interim, no backend); built
  to swap for a self-hosted scheduler later. Wire every "Book a demo" CTA
  (nav, hero, pricing, the previously-dead final-CTA button, and the
  contact/partners/migration/coming-soon CTAs) to /demo.
- /status: manually-maintained system-status page (overall banner,
  per-service rows, incident history). Live modules operational; Video/Chat
  marked coming soon.
- Roadmap: expand the board (5 items/column) + a "the bigger picture"
  themes grid + a "suggest a feature" CTA + a directional-timelines note.
- Contact: purpose-specific channels (info@ / legal@ / privacy@), a
  response-time note, and a company + "see it live" demo block.
- Drop /status from the [slug].vue stub map; tidy now-unused imports.
2026-06-06 19:19:19 +02:00
Ronni Baslund 3347fa9265 feat(website): expand about page (problem + founders)
Extend /about with the "why": a "the problem" section (jurisdiction vs
geography, creeping prices, lock-in, vendor sprawl) as a 2x2 pain grid, and
a "we're founders ourselves" section. Both bilingual and responsive.

Also point the footer partner link to /partners (was the homepage
#whitelabel section).
2026-06-06 16:47:47 +02:00
Ronni Baslund 97728cb09e feat(website): SLA & cookie policy pages
Replace the /sla and /cookies stubs with real pages (da + en), matching
the DPA/terms/privacy layout. Drop both from the [slug].vue stub map.

- SLA: 99% uptime target framed as a transparency-backed commitment (no
  public service credits); contractual credits offered only via Enterprise
  agreements. Support first-response targets table; status-page reference.
- Cookie policy: strictly-necessary + language preference only, cookie-free
  analytics (Umami), no third-party tracking, "no banner needed"; cookie
  table; final (v1.0), no draft banner.
2026-06-06 16:47:47 +02:00
Ronni Baslund 1686b71411 feat(website): persist language choice in a cookie
useLang now seeds from a first-party `dezky-lang` cookie on SSR init and
toggleLang writes it (12 months, SameSite=Lax). The chosen language now
survives reloads/return visits (fixes the reset-to-Danish quirk) and makes
the cookie-policy entry accurate.
2026-06-06 16:42:46 +02:00
Ronni Baslund 3123d8a5a1 feat(website): terms of service page
Replace the /terms stub with a real Servicevilkår page (da + en),
mirroring the DPA/privacy layout and draft / legal-review banner.

- 12 clauses tailored to the system: the service, per-user accounts,
  acceptable use, customer data (EU-hosted, DPA ref, export/no lock-in),
  pricing & payment (49 kr/user/mo excl. VAT), term/renewal/cancellation,
  uptime & SLA ref, IP, liability cap (last 12 months' fees), changes,
  Danish law / venue Esbjerg
- Drop 'terms' from the [slug].vue stub map so the real page wins
- Contact: legal@dezky.eu

Still a 0.1 draft pending legal review (flagged on the page).
2026-06-06 16:04:29 +02:00
Ronni Baslund d668b1b6a6 feat(website): responsive / mobile layouts
Make the marketing site mobile-friendly across every page and section.
Desktop appearance is unchanged; all breakpoint logic targets <=768px.

- Fluid section padding via clamp(); equal grids use auto-fit/minmax,
  asymmetric grids stack to one column via scoped-CSS media queries
- Nav: real hamburger menu on mobile (links, lang toggle, login, CTA)
- ProductMockup: scales the whole dashboard to fit (zoom) instead of
  reflowing its internals into a tall stack
- Lower oversized heading clamp() minimums so titles no longer overflow
  at ~390px (hero, page headers, final CTA, brand cover/chapter)
- HowItWorks: row-gap when steps stack so node markers clear the text
- Compare + partners tables: stacked rows now label each value with its
  column (Dezky vs hyperscaler / CSP) instead of an ambiguous header
- Footer columns, tiers, calculator and tables stack cleanly on mobile
2026-06-06 15:55:35 +02:00
Ronni Baslund bc0697c3e8 feat(website): real DPA + privacy policy pages
Replace the /dpa and /privacy placeholder stubs with full pages (da + en):
- /dpa — GDPR Art. 28 data processing agreement: roles, subject matter, data
  categories, processor obligations, sub-processors (Hetzner, Stripe — EU),
  no third-country transfer, 72h breach notice, audits, deletion, Danish law.
- /privacy — controller privacy notice: data collected, legal basis, cookieless
  Umami analytics, retention, recipients, data-subject rights + Datatilsynet.
Both carry a "draft — pending legal review" banner. Removes their stub entries.
2026-06-05 22:10:11 +02:00
Ronni Baslund 80c8a23688 chore(website): reword partner-tiers heading to "Skalér med os." 2026-06-05 22:09:53 +02:00
Ronni Baslund f447b13c83 feat(website): brand guide page + NodeMark/NodeLockup variants
Replace the /brand stub with the full Brand Guide from the Claude Design
handoff (cover, logo, color, typography, voice, applications), rendered inside
the site layout, English-only, with illustrative copy adapted to Dezky's real
EU-sovereign voice. Extend NodeMark with a `variant` (donut/solid/outline) and
NodeLockup with a separate `wordmark` colour so the mark reads correctly on
dark surfaces. Drops the old brand stub copy.
2026-06-05 22:09:36 +02:00
Ronni Baslund ed660b9a81 chore(website): gitignore stray npm package-lock.json (project uses pnpm) 2026-06-05 16:02:44 +02:00
Ronni Baslund 41af70d57b chore(website): pin TMPDIR in dev script and fix brand domain
Set TMPDIR=/tmp in the dev script so the Nuxt vite-node Unix socket path stays
under macOS's 104-char limit (fixes "Failed to restrict vite-node socket
permissions" on local runs; harmless in the Linux container). Also fix the
package.json description: dezky.com → dezky.eu.
2026-06-05 15:59:23 +02:00
Ronni Baslund bf183fce07 feat(website): tier-driven progressive partner margin calculator
The /partners margin calculator now derives margin from the user count using
progressive brackets (like tax brackets): first 500 users at 15%, 501–1000 at
30%, 1001+ at 40%, off the 49 kr/user/mo list price. Replaces the manual margin
slider with a live per-bracket breakdown. Rates are read from the tier copy;
tier thresholds aligned to 501 / 1.001 so the cards and calculator agree.
2026-06-05 15:59:19 +02:00
Ronni Baslund 6d82502e7b chore(website): coming-soon badges, standards reframe, pricing, company info
- Suite: "coming soon" badge + dimmed glyph on Meet & Chat (data-driven `soon` flag)
- Stack (section 07): reframe from a vendor shopping-list to open standards +
  portability (no vendor names exposed; keeps the no-lock-in message)
- Pricing: 69 → 49 kr/user/mo
- Company info (footer + contact): Åtoften 33, 6710 Esbjerg V; CVR 43 14 18 21
2026-06-05 14:46:35 +02:00
Ronni Baslund 2e400d86c5 feat(website): partner program page + reseller conversion sections
Add the /partners page rendering the partner pitch: benefits, an interactive
margin calculator (seats × margin → monthly/annual, off the 49 kr list price),
a reseller-facing "CSP vs Dezky" comparison, partner tiers, a 3-step "get
started", and a partner FAQ. Wire the section-06 "see the partner program"
button to it, and align the whitelabel margin bullet to 15–40%.
2026-06-05 14:46:22 +02:00
Ronni Baslund 0a35d9deb6 feat(website): footer sub-pages + shared page layout
Wire every footer link to a real route. Adds a shared `page` layout (Nav +
content + Footer), reusable PageHeader/ComingSoon components, six content pages
(about, contact, brand, roadmap, changelog, migration), and a dynamic [slug]
catch-all for the not-yet-built pages — unknown slugs 404, legal slugs get a
distinct "contact us" body.

Footer links repointed from dead "#" to real paths; section anchors ("/#suite")
smooth-scroll on the homepage and route home + scroll from a sub-page; logo
links home. Page copy (da + en) added under COPY.pages.
2026-06-05 14:40:36 +02:00
Ronni Baslund 4c57d41350 feat(website): rewrite hero headline and switch brand domain to .eu
Hero headline was a broken comma-splice ("Den produktivitetssuite, dine data
bliver i Danmark med."); replace with a clean two-sentence line in both
languages:
  da: "Din digitale arbejdsplads. Data der bliver i EU."
  en: "Your digital workplace. Data that stays in the EU."

Also move all brand references off the unowned dezky.com to dezky.eu
(status page, app dashboard mockup, config/comments). External domains
(zulip.com, Google Fonts) are left untouched.
2026-06-05 12:29:50 +02:00
Ronni Baslund a0f79ab852 chore(scripts): configure git remote in bootstrap
Add a "Configure git remote" step that points origin at the Gitea host
(git@git.lastcloud.io) and pins the host to port 22222 in ~/.ssh/config so
git doesn't default to port 22 and get rejected by the agent offering too many
keys. Idempotent: reuses existing config on re-run. Also adds git to the
prerequisite checks and renumbers the steps to 1-7.
2026-06-05 12:18:07 +02:00
Ronni Baslund 4c3c47cc87 feat(website): localize whitelabel partner cards (da/en)
Partner demo cards in section 06 were hardcoded Danish strings, so they
stayed Danish in EN mode. Move name + subtitle into COPY.whitelabel.partners
for both languages and render them via a mapped computed; per-card accent and
the placeholder style remain presentational config in the component.

Also harden PartnerCard's avatar-initial against an empty name to satisfy
noUncheckedIndexedAccess.
2026-06-05 12:08:57 +02:00
Ronni Baslund c9911cc262 feat(website): add Nuxt 4 marketing landing page
New standalone apps/website (Nuxt 4) serving the public marketing site at
dezky.local / www.dezky.local. The customer portal moves off the root domain
to app.dezky.local only.

Landing page ported from the Dezky design handoff: light theme, Danish
default, hero variant A, with a working da/en toggle. Self-contained colour
system threaded through components (utils/landingTokens.ts), full bilingual
copy (utils/landingCopy.ts), and shared state (composables/useLanding.ts).
Sections live under components/landing/* with the Node logo under
components/brand/*.

Wired into docker-compose (website service, volume, Traefik labels, network
aliases) and bootstrap.sh (hosts list + service URLs).
2026-06-05 10:58:25 +02:00
Ronni Baslund 47eb9502f8 feat(platform): real email domains, mailboxes & member lifecycle
Wire the mail/identity stack to real Stalwart/Authentik/OCIS provisioning,
replacing the mocked Domains and Users pages.

Domains (customer-admin):
- StalwartClient: real JMAP management (v0.16 dropped REST) — create/list/delete
  email domains via x:Domain at the internal http://stalwart:8080 listener;
  DKIM auto-generated; the records to publish are read from the domain's
  dnsZoneFile. Gated by STALWART_PROVISIONING_ENABLED.
- New Domain collection + DomainsModule: add/list/recheck/set-DMARC/remove,
  tenant-membership-gated and audited.
- DnsVerifierService: verifies MX/SPF/DKIM/DMARC/ownership against a public
  resolver (1.1.1.1/8.8.8.8) and diffs them against the expected records.
- Remove is guarded: refuses while accounts/aliases/mailing lists still use the
  domain (via Stalwart referential integrity).
- Domains page + add wizard on real data; sidebar badge counts domains needing
  attention.

Users & groups (customer-admin):
- Create a member provisioned across Authentik SSO, a Stalwart mailbox on the
  tenant's primary domain, and OCIS — returning a one-time password.
- Lifecycle: suspend/resume (Authentik is_active + freeze the mailbox via
  account permissions, original password preserved), force-logout (terminate
  sessions, filtered client-side so it can never end other users' sessions),
  reset password (new one-time password on SSO + mailbox), and remove (tear down
  mailbox + SSO identity + OCIS + doc; mailbox-in-use aware for multi-tenant
  users). Self-suspend / self-force-logout are blocked.

Infra: point platform-api at the internal Stalwart listener; document the new
STALWART_/provisioning vars in .env.example.
2026-06-01 21:19:42 +02:00
Ronni Baslund 2a43a7bbf3 feat(operator): show per-tenant role in tenant users list
GET /tenants/:slug/users now returns a tenant-scoped `tenantRole`
(resolved server-side via roleForTenant), and the operator tenant page
displays it instead of the global `role` — so a user who is admin here
but member elsewhere reads correctly in this tenant's context. The
global `role` field is kept intact for other consumers.
2026-05-31 21:31:51 +02:00
Ronni Baslund f094158334 fix(api): report tenant-scoped role in tenant users list
A user who is admin in one tenant but member in another must read
'admin' for this tenant — use roleForTenant() rather than the global
u.role fallback when building the tenant users list.
2026-05-31 21:30:08 +02:00
Ronni Baslund f8618b2bbc feat(portal): real OCIS storage data via refresh-token service auth
The Storage page + endpoint landed earlier but had no working OCIS
backend credential. OCIS has no service-account/client-credentials grant
and trusts a single issuer, and basic auth resolves no user in our
external-IdP setup — so authenticate OcisClient via an OIDC
refresh-token bootstrap instead:

- One-time headless login of svc-platform-api against the ocis provider
  (public client ocis-web, issuer .../o/ocis/) yields a refresh token,
  persisted in Mongo (ocis_credentials) and rotated on every use.
- OcisClient mints access tokens with the refresh_token grant; the
  service user holds the OCIS admin role (OCIS_ADMIN_USER_ID) so
  libregraph ListAllDrives works.
- scripts/bootstrap-ocis.mjs re-runs the bootstrap if the token lapses.
- Dashboard Plan card gains a storage capacity bar beside seats;
  hidden when storage is unavailable.
- compose + .env.example: OCIS service OIDC env and admin user id.
- docs/NEXT-STEPS: document the mechanism and the dead-end alternatives.
2026-05-31 21:29:17 +02:00
Ronni Baslund 559348f6bc feat(portal): real Security & audit page (+ bundled Storage / per-tenant-roles WIP)
Security & audit (admin)
- Audit log: real, tenant-scoped — widened GET /tenants/:slug/audit with
  q/action/outcome/actorEmail/since/before; UI gains search, outcome + time
  filters, action chips, cursor pagination, and client-side CSV export.
- Security policy: new tenant.securityPolicy (mfaMode, session idle/absolute,
  allowedCountries, ipAllowlist) + PATCH /tenants/:slug/security-policy
  (membership-gated, audited). Editable, labelled by enforcement status.
- MFA: live enrollment overview via GET /tenants/:slug/mfa-status
  (Authentik countAuthenticators per member).
- SSO apps (Dezky as IdP): real Authentik OIDC provider + application CRUD,
  scoped to the tenant group. New AuthentikClient methods (provider/app/binding
  + flow/key/scope discovery), TenantSsoApp schema, TenantSsoService (rollback
  on partial failure; client secret never stored), GET/POST/DELETE
  /tenants/:slug/sso-apps. Validated end-to-end against live Authentik.
- Deferred: shared-flow MFA/geo/session enforcement (global auth-flow blast
  radius) — to be done as its own reviewed change.

Bundled in-progress work that shares the same files (kept together so the tree
stays green):
- Storage page: StorageService + GET /tenants/:slug/storage (OCIS-backed),
  storage.get proxy, storage.vue.
- Per-tenant roles: User.tenantRoles + MeProfile.tenantRoles plumbing.
2026-05-31 17:20:36 +02:00
Ronni Baslund 3288fde693 feat(portal): customer-admin surface on real data + Stripe billing + session resilience
Access & navigation
- Gate partner-mode strictly to partner staff so admins/end-users never inherit
  leftover partner-view state; purge stale session entry on hydrate.
- Role-driven admin entry: useMe.isTenantAdmin, Admin/Personal tiles in the app
  launcher, and an /admin route guard in the global middleware (fail closed).
- Drop the duplicate user identity block from the sidebar footer.

Admin pages on real data
- New tenant-scoped, membership-gated endpoints: GET /tenants/:slug/{audit,users,
  invoices}; useTenant composable resolves the active workspace + subscription.
- Dashboard: real seats, spend (cycle-normalized + minor-units), plan, renewal,
  and recent audit; unbacked sections removed.
- Users & groups: real members; Groups/Invitations/Service accounts shown as
  honest "coming soon".
- Subscription & invoices: real plan hero, invoice history, and billing details.

Stripe payment method (Elements + SetupIntent)
- StripeClient: publishable key + getDefaultCard/createSetupIntent/setDefaultCard.
- CustomerBillingController + BillingService methods (ensure-customer on demand).
- Portal: PaymentMethodModal, useStripeJs (CDN load), proxies; hidePostalCode.

Editable billing details & whitelabel branding
- PATCH /tenants/:slug/billing-info (narrow: company/VAT/country/email).
- TenantBranding schema/service + GET/PUT /tenants/:slug/branding: real product
  name, accent colour, and per-tenant email-template overrides.
- Branding preview + sidebar workspace mark wired to real name/plan/seats/colour
  with YIQ auto-contrast (readableOn util).

Session resilience
- Request offline_access so Authentik issues a refresh token (automaticRefresh).
- Silent refresh + single retry on 401 for writes (useApiFetch, incl. partner
  pages) and reads (useMe.fetchMe) — no redirect, no lost input.
- Modal backdrop closes only on press+release on the backdrop (no more
  drag-select-to-close).
2026-05-31 00:19:34 +02:00