Files
dezky/apps/portal/components/RecordRow.vue
T
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

66 lines
2.1 KiB
Vue

<script setup lang="ts">
// One DNS record row used by the add-domain wizard: TYPE / HOST / VALUE with a
// live status badge and copy buttons. Values come straight from the server's
// expected records (Stalwart's authoritative zone), so the DKIM key etc. is real.
import type { DomainRecordView, RecordStatus } from '~/composables/useDomains'
const props = defineProps<{ rec: DomainRecordView }>()
const toast = useToast()
function badgeTone(status: RecordStatus): 'ok' | 'warn' | 'bad' {
return status === 'ok' ? 'ok' : status === 'bad' ? 'bad' : 'warn'
}
function copy(text: string) {
if (typeof navigator !== 'undefined' && navigator.clipboard) navigator.clipboard.writeText(text)
toast.ok('Copied to clipboard')
}
</script>
<template>
<div class="dns-row">
<div><Mono dim>TYPE</Mono><div class="dns-val">{{ rec.type }}</div></div>
<div>
<Mono dim>HOST</Mono>
<button class="dns-val link" @click="copy(rec.fqdn)" :title="rec.fqdn">{{ rec.fqdn }}</button>
</div>
<div>
<Mono dim>VALUE</Mono>
<button class="dns-val dim link" @click="copy(rec.expected)" :title="rec.expected">{{ rec.expected }}</button>
</div>
<div class="dns-right">
<Badge :tone="badgeTone(rec.status)" dot>{{ rec.status }}</Badge>
<span v-if="rec.priority !== undefined" class="prio"><Mono dim>prio {{ rec.priority }}</Mono></span>
</div>
</div>
</template>
<style scoped>
.dns-row {
display: grid;
grid-template-columns: 80px 220px 1fr 90px;
gap: 12px;
align-items: center;
padding: 12px 14px;
background: var(--bg);
border: 1px solid var(--border);
border-radius: 6px;
}
.dns-val { font-family: var(--font-mono); font-size: 13px; font-weight: 600; margin-top: 2px; }
.dns-val.dim { color: var(--text-dim); font-weight: 400; font-size: 12px; }
.dns-val.link {
background: transparent;
border: none;
padding: 0;
text-align: left;
cursor: pointer;
max-width: 100%;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
display: block;
}
.dns-val.link:hover { color: var(--text); }
.dns-right { display: flex; flex-direction: column; align-items: flex-end; gap: 6px; }
.prio { font-size: 11px; }
</style>