Files
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

210 lines
21 KiB
TypeScript
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// The "current workspace" for prototype purposes. Replaced by platform-api
// lookup in production. Holds the brand identity that the BrandingScreen
// lives in front of.
export interface Workspace {
id: string
name: string
productName: string
domain: string
plan: 'starter' | 'business' | 'enterprise'
seats: { used: number; total: number }
storage: { usedGb: number; totalGb: number }
mailFlow: { deliveredPct: number; trend: 'up' | 'down' | 'flat' }
monthlySpendDkk: number
status: 'active' | 'trial' | 'past_due' | 'suspended'
}
export const workspace: Workspace = {
id: 'ws-baslund',
name: 'Baslund',
productName: 'Baslund Workspace',
domain: 'baslund.dk',
plan: 'business',
seats: { used: 11, total: 25 },
storage: { usedGb: 142, totalGb: 500 },
mailFlow: { deliveredPct: 99.4, trend: 'up' },
monthlySpendDkk: 1940,
status: 'active',
}
export const sampleUsers = [
{ id: 'u-1', name: 'Anne Hansen', email: 'anne@baslund.dk', role: 'owner', groups: ['Leadership', 'Finance'], mfa: 'totp', lastLogin: '2 min ago', status: 'active', storageMb: 8200, license: 'business' },
{ id: 'u-2', name: 'Mikkel Sørensen', email: 'mikkel@baslund.dk', role: 'admin', groups: ['Engineering'], mfa: 'webauthn', lastLogin: '14 min ago', status: 'active', storageMb: 14210, license: 'business' },
{ id: 'u-3', name: 'Sofie Lund', email: 'sofie@baslund.dk', role: 'admin', groups: ['Design'], mfa: 'totp', lastLogin: '1 hour ago', status: 'active', storageMb: 6804, license: 'business' },
{ id: 'u-4', name: 'Frederik Holm', email: 'frederik@baslund.dk', role: 'user', groups: ['Finance'], mfa: 'totp', lastLogin: '3 hours ago', status: 'active', storageMb: 2401, license: 'business' },
{ id: 'u-5', name: 'Caroline Bjerg', email: 'caroline@baslund.dk', role: 'user', groups: ['Sales'], mfa: 'none', lastLogin: 'Yesterday', status: 'active', storageMb: 1880, license: 'business' },
{ id: 'u-6', name: 'Johan Olesen', email: 'johan@baslund.dk', role: 'user', groups: ['Engineering'], mfa: 'webauthn', lastLogin: '2 days ago', status: 'active', storageMb: 4250, license: 'business' },
{ id: 'u-7', name: 'Maria Petersen', email: 'maria@baslund.dk', role: 'user', groups: ['Design'], mfa: 'totp', lastLogin: '5 days ago', status: 'active', storageMb: 3120, license: 'business' },
{ id: 'u-8', name: 'Henrik Schmidt', email: 'henrik@baslund.dk', role: 'user', groups: ['Engineering'], mfa: 'totp', lastLogin: '1 week ago', status: 'active', storageMb: 9540, license: 'business' },
{ id: 'u-9', name: 'Trine Madsen', email: 'trine@baslund.dk', role: 'user', groups: ['Sales'], mfa: 'none', lastLogin: 'never', status: 'invited', storageMb: 0, license: 'business' },
{ id: 'u-10', name: 'Lars Engelbrecht', email: 'lars@baslund.dk', role: 'user', groups: ['Engineering'], mfa: 'totp', lastLogin: '12 days ago', status: 'suspended', storageMb: 5102, license: 'business' },
{ id: 'u-11', name: 'Bo Christensen', email: 'bo@baslund.dk', role: 'user', groups: ['Sales'], mfa: 'totp', lastLogin: '3 hours ago', status: 'active', storageMb: 1242, license: 'starter' },
]
export const sampleGroups = [
{ id: 'g-1', name: 'Leadership', description: 'Owners + admins', members: 3, owner: 'Anne Hansen', resources: ['leadership@', 'Drev/Leadership'] },
{ id: 'g-2', name: 'Engineering', description: 'Backend, frontend, infrastructure', members: 4, owner: 'Mikkel Sørensen', resources: ['eng@', 'Drev/Engineering', '#eng'] },
{ id: 'g-3', name: 'Design', description: 'Brand, product, marketing visuals', members: 2, owner: 'Sofie Lund', resources: ['design@', 'Drev/Design', '#design'] },
{ id: 'g-4', name: 'Finance', description: 'Bookkeeping, AP/AR, payroll', members: 2, owner: 'Frederik Holm', resources: ['finance@', 'Drev/Finance'] },
{ id: 'g-5', name: 'Sales', description: 'Outbound + customer success', members: 3, owner: 'Bo Christensen', resources: ['sales@', 'Drev/Sales', '#sales'] },
]
export const sampleInvoices = [
{ id: 'inv-2026-001247', date: '2026-05-01', period: '2026-05', amount: 1940, status: 'paid', method: 'MobilePay' },
{ id: 'inv-2026-001112', date: '2026-04-01', period: '2026-04', amount: 1940, status: 'paid', method: 'MobilePay' },
{ id: 'inv-2026-000984', date: '2026-03-01', period: '2026-03', amount: 1940, status: 'paid', method: 'MobilePay' },
{ id: 'inv-2026-000855', date: '2026-02-01', period: '2026-02', amount: 1940, status: 'paid', method: 'MobilePay' },
{ id: 'inv-2026-000721', date: '2026-01-01', period: '2026-01', amount: 1820, status: 'paid', method: 'Card · VISA 4242' },
]
// Audit events — strict port of project/platform-screens.jsx SAMPLE_AUDIT
// (line 29). Same 8 rows, same wording, same tones. The AdminDashboard slices
// the first 6, the SecurityScreen audit log uses all of them.
export const sampleAudit = [
{ id: 'a1', when: '14:02:11', actor: 'Anne Baslund', action: 'user.invited', target: 'magnus@dezky.com', ip: '92.43.118.4', tone: 'info' as const },
{ id: 'a2', when: '13:48:02', actor: 'Mikkel Nørgaard', action: 'billing.plan_changed', target: 'Team → Business', ip: '92.43.118.4', tone: 'info' as const },
{ id: 'a3', when: '13:21:55', actor: 'system', action: 'domain.dns_check', target: 'baslund.dk', ip: '—', tone: 'warn' as const },
{ id: 'a4', when: '12:09:30', actor: 'Sofie Lindberg', action: 'user.suspended', target: 'tina@dezky.com', ip: '78.110.4.92', tone: 'warn' as const },
{ id: 'a5', when: '11:44:00', actor: 'Anne Baslund', action: 'mfa.policy_updated', target: 'require for admins', ip: '92.43.118.4', tone: 'info' as const },
{ id: 'a6', when: '10:12:08', actor: 'Lars Holm', action: 'session.terminated', target: 'iPad — Safari', ip: '78.110.4.10', tone: 'info' as const },
{ id: 'a7', when: '09:55:41', actor: 'system', action: 'auth.failed_login', target: 'oliver@dezky.com', ip: '203.0.113.4', tone: 'bad' as const },
{ id: 'a8', when: '09:30:00', actor: 'Anne Baslund', action: 'branding.color_set', target: '#D4FF3A', ip: '92.43.118.4', tone: 'info' as const },
]
// Org-wide mail aliases — strict port of platform-admin.jsx ORG_ALIASES (line 9)
export const orgAliases = [
{ alias: 'info@dezky.com', dest: 'Distribution · Everyone', active: true, created: '14 Jan 2026' },
{ alias: 'sales@dezky.com', dest: 'frederik@dezky.com', active: true, created: '22 Jan 2026' },
{ alias: 'support@dezky.com', dest: 'sofie@dezky.com', active: true, created: '22 Jan 2026' },
{ alias: 'no-reply@dezky.com', dest: '(discard)', active: true, created: '14 Jan 2026' },
{ alias: 'careers@dezky.com', dest: 'anne@dezky.com', active: true, created: '04 Mar 2026' },
{ alias: 'legal@dezky.com', dest: 'mikkel@dezky.com', active: false, created: '12 Apr 2026' },
]
// Forwarding rules — strict port of platform-admin.jsx FORWARDING_RULES (line 18)
export const forwardingRules = [
{ name: 'Out-of-hours to on-call', match: 'support@dezky.com · 18:0008:00 CET', fwd: 'oncall@dezky.com', enabled: true },
{ name: 'Vendor invoices', match: 'subject: "invoice" · from: *@vendors', fwd: 'finance@dezky.com', enabled: true },
{ name: 'Legal threads', match: 'cc: legal@*', fwd: 'mikkel@dezky.com', enabled: false },
]
// Distribution lists — strict port of platform-admin.jsx DISTRIBUTION_LISTS (line 24)
export const distributionLists = [
{ name: 'Everyone', alias: 'everyone@dezky.com', members: 11, owner: 'Anne Baslund', moderation: 'open' as const, external: false },
{ name: 'Engineering', alias: 'eng@dezky.com', members: 4, owner: 'Anne Baslund', moderation: 'closed' as const, external: false },
{ name: 'Leadership', alias: 'leads@dezky.com', members: 3, owner: 'Anne Baslund', moderation: 'closed' as const, external: false },
{ name: 'Customers VIP', alias: 'vip-customers@dezky.com', members: 0, owner: 'Frederik Madsen', moderation: 'closed' as const, external: true },
]
// Anti-spam content filters — strict port of platform-admin.jsx ANTI_SPAM_FILTERS (line 31)
export const antiSpamFilters = [
{ name: 'Block executable attachments', match: 'attachment ext in (.exe, .scr, .bat, .cmd)', action: 'reject' as const, enabled: true },
{ name: 'Quarantine cryptocurrency mail', match: 'body contains "wallet address"', action: 'quarantine' as const, enabled: true },
{ name: 'Tag external mail', match: 'from outside @dezky.com', action: 'add tag' as const, enabled: true },
]
// Full groups list — strict port of platform-admin.jsx GROUPS_FULL (line 64)
export const groupsFull = [
{ id: 'g_eng', name: 'Engineering', alias: 'engineering@dezky.com', members: 4, description: 'Product engineering team', created: '14 Jan 2026', owner: 'Anne Baslund' },
{ id: 'g_des', name: 'Design', alias: 'design@dezky.com', members: 2, description: 'Brand and product designers', created: '02 Feb 2026', owner: 'Sofie Lindberg' },
{ id: 'g_ops', name: 'Operations', alias: 'ops@dezky.com', members: 2, description: 'Operations and on-call', created: '14 Jan 2026', owner: 'Mikkel Nørgaard' },
{ id: 'g_fin', name: 'Finance', alias: 'finance@dezky.com', members: 2, description: 'Finance and accounting', created: '22 Jan 2026', owner: 'Astrid Vinther' },
{ id: 'g_sales', name: 'Sales', alias: 'sales@dezky.com', members: 2, description: 'Customer sales', created: '14 Jan 2026', owner: 'Frederik Madsen' },
{ id: 'g_all', name: 'All-hands', alias: 'everyone@dezky.com', members: 11, description: 'Everyone in the workspace', created: '14 Jan 2026', owner: 'Anne Baslund' },
]
// Source-fidelity users (project/platform-screens.jsx SAMPLE_USERS line 8) —
// flat shape used by UsersScreen, BillingScreen seats math and StorageScreen
// "top users" list. Kept in sync with the visual mock — these have `last`,
// `group` (singular), `storage` (GB) rather than the multi-group `sampleUsers`
// above.
export const sampleUsersFlat = [
{ id: 'u_1ph2', name: 'Anne Baslund', email: 'anne@dezky.com', role: 'Owner', status: 'active', last: '2 min ago', group: 'Engineering', storage: 12.4 },
{ id: 'u_4kx9', name: 'Mikkel Nørgaard', email: 'mikkel@dezky.com', role: 'Admin', status: 'active', last: '14 min ago', group: 'Operations', storage: 8.1 },
{ id: 'u_88aw', name: 'Sofie Lindberg', email: 'sofie@dezky.com', role: 'Admin', status: 'active', last: '1 h ago', group: 'Finance', storage: 3.2 },
{ id: 'u_d12k', name: 'Lars Holm', email: 'lars@dezky.com', role: 'Member', status: 'active', last: '3 h ago', group: 'Engineering', storage: 22.7 },
{ id: 'u_9rqo', name: 'Emma Skov', email: 'emma@dezky.com', role: 'Member', status: 'invited', last: '—', group: 'Design', storage: 0 },
{ id: 'u_3vbn', name: 'Jonas Berg', email: 'jonas@dezky.com', role: 'Member', status: 'active', last: '2 d ago', group: 'Engineering', storage: 14.0 },
{ id: 'u_g51e', name: 'Tina Falkenberg', email: 'tina@dezky.com', role: 'Member', status: 'suspended', last: '11 d ago', group: 'Finance', storage: 5.6 },
{ id: 'u_kk7n', name: 'Oliver Schmidt', email: 'oliver@dezky.com', role: 'Member', status: 'active', last: '4 h ago', group: 'Design', storage: 9.3 },
{ id: 'u_pp01', name: 'Frederik Madsen', email: 'frederik@dezky.com', role: 'Member', status: 'active', last: 'Yesterday', group: 'Sales', storage: 6.8 },
{ id: 'u_qq22', name: 'Astrid Vinther', email: 'astrid@dezky.com', role: 'Admin', status: 'active', last: '32 min ago', group: 'Operations', storage: 4.1 },
{ id: 'u_rr44', name: 'Magnus Eriksen', email: 'magnus@dezky.com', role: 'Member', status: 'invited', last: '—', group: 'Engineering', storage: 0 },
{ id: 'u_tt55', name: 'Clara Bjerre', email: 'clara@dezky.com', role: 'Member', status: 'active', last: '5 d ago', group: 'Sales', storage: 2.0 },
]
// Meeting rooms — strict port of platform-collab.jsx MEETING_ROOMS (line 8)
export const meetingRooms = [
{ id: 'r_eng', name: 'Engineering standup', alias: 'eng-standup', type: 'recurring' as const, when: 'Daily · 09:30', owner: 'Mikkel Nørgaard', members: 4, recording: 'auto' as const, protected: false },
{ id: 'r_lead', name: 'Leadership weekly', alias: 'leadership', type: 'recurring' as const, when: 'Mondays · 14:00', owner: 'Anne Baslund', members: 3, recording: 'manual' as const, protected: true },
{ id: 'r_allh', name: 'All-hands', alias: 'allhands', type: 'recurring' as const, when: '2nd Friday · 15:00', owner: 'Anne Baslund', members: 11, recording: 'auto' as const, protected: false },
{ id: 'r_inter', name: 'Interview room A', alias: 'interview-a', type: 'ad-hoc' as const, when: 'Persistent', owner: 'Sofie Lindberg', members: 2, recording: 'off' as const, protected: true },
{ id: 'r_cust', name: 'Customer demos', alias: 'demo', type: 'shared' as const, when: 'On-demand', owner: 'Frederik Madsen', members: 2, recording: 'manual' as const, protected: false },
{ id: 'r_focus', name: 'Focus · pair', alias: 'pair', type: 'ad-hoc' as const, when: 'Persistent', owner: 'Lars Holm', members: 1, recording: 'off' as const, protected: false },
]
// Meeting recordings — strict port of platform-collab.jsx MEETING_RECORDINGS (line 17)
export const meetingRecordings = [
{ id: 'rec1', title: 'All-hands · May edition', date: '12 May 2026', dur: '52 min', size: '184 MB', host: 'Anne Baslund', views: 11, retention: '365 d', legal: false },
{ id: 'rec2', title: 'Customer demo · Novo Holding', date: '11 May 2026', dur: '38 min', size: '142 MB', host: 'Frederik Madsen', views: 4, retention: '90 d', legal: false },
{ id: 'rec3', title: 'Engineering · Sprint review 24', date: '09 May 2026', dur: '46 min', size: '168 MB', host: 'Mikkel Nørgaard', views: 6, retention: '365 d', legal: false },
{ id: 'rec4', title: 'Board update · Q1 close', date: '02 May 2026', dur: '28 min', size: '102 MB', host: 'Anne Baslund', views: 3, retention: 'forever', legal: true },
{ id: 'rec5', title: 'New hire onboarding · Magnus', date: '29 Apr 2026', dur: '24 min', size: '88 MB', host: 'Sofie Lindberg', views: 2, retention: '90 d', legal: false },
{ id: 'rec6', title: 'Engineering · Sprint review 23', date: '25 Apr 2026', dur: '41 min', size: '154 MB', host: 'Mikkel Nørgaard', views: 5, retention: '365 d', legal: false },
{ id: 'rec7', title: 'Customer call · Aalborg Logistik', date: '22 Apr 2026', dur: '34 min', size: '128 MB', host: 'Frederik Madsen', views: 3, retention: '90 d', legal: false },
]
// Chat workspaces — strict port of platform-collab.jsx CHAT_WORKSPACES (line 27)
export const chatWorkspaces = [
{ id: 'cw_main', name: 'baslund · main', url: 'baslund.chat.dezky.com', members: 11, channels: 24, messages30d: 8420, status: 'active' as const, primary: true },
{ id: 'cw_eng', name: 'engineering', url: 'eng.chat.dezky.com', members: 4, channels: 11, messages30d: 12480, status: 'active' as const, primary: false },
]
// Chat channels — strict port of platform-collab.jsx CHAT_CHANNELS (line 32)
export const chatChannels = [
{ name: 'general', type: 'public' as const, members: 11, messages30d: 1840, owner: 'Anne Baslund', topic: 'Workspace-wide announcements' },
{ name: 'engineering', type: 'public' as const, members: 4, messages30d: 3220, owner: 'Mikkel Nørgaard', topic: 'Eng standups, deploys, code questions' },
{ name: 'design', type: 'public' as const, members: 2, messages30d: 480, owner: 'Sofie Lindberg', topic: 'Brand, product design, reviews' },
{ name: 'sales-customers', type: 'private' as const, members: 3, messages30d: 720, owner: 'Frederik Madsen', topic: 'Customer accounts and pipeline' },
{ name: 'random', type: 'public' as const, members: 9, messages30d: 1240, owner: 'Anne Baslund', topic: 'Off-topic' },
{ name: 'incidents', type: 'public' as const, members: 5, messages30d: 84, owner: 'Mikkel Nørgaard', topic: 'Service alerts and incident response' },
{ name: 'dezky-roadmap', type: 'private' as const, members: 4, messages30d: 410, owner: 'Anne Baslund', topic: 'Product planning, not for everyone' },
{ name: 'finance', type: 'private' as const, members: 2, messages30d: 96, owner: 'Astrid Vinther', topic: 'Invoicing, accounting' },
]
// Integrations marketplace — strict port of platform-collab.jsx INTEGRATIONS (line 43)
export interface Integration {
id: string
name: string
cat: 'Productivity' | 'Storage' | 'CRM' | 'Accounting' | 'Operations'
desc: string
connected: boolean
users?: number
kind: string
icon: string
color: string
accent: string
danish?: boolean
}
export const integrations: Integration[] = [
{ id: 'notion', name: 'Notion', cat: 'Productivity', desc: 'Docs and wikis. Sign in with dezky.', connected: true, users: 11, kind: 'SSO', icon: '◧', color: '#000000', accent: '#FFFFFF' },
{ id: 'figma', name: 'Figma', cat: 'Productivity', desc: 'Design files with single sign-on.', connected: true, users: 6, kind: 'SSO', icon: 'F', color: '#F24E1E', accent: '#FFFFFF' },
{ id: 'linear', name: 'Linear', cat: 'Productivity', desc: 'Issue tracking for engineering.', connected: true, users: 4, kind: 'SSO + provisioning', icon: 'L', color: '#5E6AD2', accent: '#FFFFFF' },
{ id: 'github', name: 'GitHub', cat: 'Productivity', desc: 'Source control. SAML enforcement.', connected: false, kind: 'SSO', icon: '⌬', color: '#181717', accent: '#FFFFFF' },
{ id: 'sketch', name: 'Sketch', cat: 'Productivity', desc: 'Design tool with team libraries.', connected: false, kind: 'SSO', icon: '◆', color: '#FDB300', accent: '#0A0A0A' },
{ id: 'gdrive', name: 'Google Drive', cat: 'Storage', desc: 'Mount Google Drive folders inside Drev.', connected: false, kind: 'OAuth', icon: 'G', color: '#1A73E8', accent: '#FFFFFF' },
{ id: 'dropbox', name: 'Dropbox', cat: 'Storage', desc: 'Mirror Dropbox into Drev. Read-only.', connected: false, kind: 'OAuth', icon: 'D', color: '#0061FF', accent: '#FFFFFF' },
{ id: 'hubspot', name: 'HubSpot', cat: 'CRM', desc: 'Customer pipeline + email sync.', connected: true, users: 3, kind: 'API', icon: 'H', color: '#FF7A59', accent: '#FFFFFF' },
{ id: 'pipedrive', name: 'Pipedrive', cat: 'CRM', desc: 'Sales CRM. Sign in with dezky.', connected: false, kind: 'SSO', icon: 'P', color: '#1A1A1A', accent: '#FFFFFF' },
{ id: 'economic', name: 'e-conomic', cat: 'Accounting', desc: 'Sync invoices to e-conomic. Danish CVR aware.', connected: true, users: 2, kind: 'API', icon: 'e', color: '#0091CD', accent: '#FFFFFF', danish: true },
{ id: 'billy', name: 'Billy', cat: 'Accounting', desc: 'Danish accounting platform. Auto-bookkeeping.', connected: false, kind: 'API', icon: 'B', color: '#0EBC81', accent: '#FFFFFF', danish: true },
{ id: 'dinero', name: 'Dinero', cat: 'Accounting', desc: 'Send invoices, sync ledger.', connected: false, kind: 'API', icon: 'D', color: '#FFCB05', accent: '#0A0A0A', danish: true },
{ id: 'pleo', name: 'Pleo', cat: 'Accounting', desc: 'Company cards. Auto-reconcile receipts.', connected: false, kind: 'API', icon: 'p', color: '#1B1B1B', accent: '#D4FF3A', danish: true },
{ id: 'pagerduty', name: 'PagerDuty', cat: 'Operations', desc: 'Page on-call from dezky incidents.', connected: false, kind: 'Webhook', icon: 'P', color: '#06AC38', accent: '#FFFFFF' },
{ id: 'slack', name: 'Slack', cat: 'Operations', desc: 'Mirror Chat to Slack (read-only).', connected: false, kind: 'OAuth', icon: 'S', color: '#4A154B', accent: '#FFFFFF' },
]
export const integrationCategories = ['All', 'Productivity', 'Storage', 'CRM', 'Accounting', 'Operations'] as const