Files
dezky/apps/portal/composables/useDomains.ts
T
Ronni Baslund f6bac10ff3
ci / changes (push) Successful in 4s
ci / tc_booking (push) Has been skipped
ci / tc_operator (push) Has been skipped
ci / tc_website (push) Has been skipped
ci / tc_platform_api (push) Successful in 20s
ci / tc_portal (push) Failing after 27s
ci / build_booking (push) Has been skipped
ci / build_operator (push) Has been skipped
ci / build_portal (push) Has been skipped
ci / test_platform_api (push) Successful in 33s
ci / build_platform_api (push) Successful in 15s
ci / deploy (push) Failing after 3m5s
feat(domains): surface autodiscovery SRV records (RFC 6186)
Mail clients could never autoconfigure: Stalwart's zone file contains the
_imaps/_submissions/_pop3s SRV records but classify() dropped everything
except mx/spf/dkim/dmarc, so customers never saw them and every client
needed manual server entry. New 'autodiscovery' record kind: classified
from the zone (only the services actually reachable in prod — the
_jmap/_caldavs SRVs target :443 which Traefik owns, deferred to the
webmail story), verified via resolveSrv (missing=bad, wrong target=warn),
shown as an OPTIONAL slot on the portal Domains page that never gates the
domain status or the records-to-fix nag.

Also fixed on the live server via management JMAP (x:SystemSettings):
hostname was the machine name node1.dezky.eu from the v0.16 auto-bootstrap
— MX/SRV targets and the SMTP banner now say mail.dezky.eu, and the LE
x:Certificate is set as defaultCertificateId.
2026-06-10 22:11:34 +02:00

69 lines
2.2 KiB
TypeScript

// Customer-admin email-domain data + mutations, backed by platform-api's
// /api/tenants/:slug/domains endpoints. Reads use useFetch (SSR-friendly list);
// writes go through useApiFetch so a lapsed session refreshes silently instead
// of redirecting away mid-action. Mirrors the read/write split in
// pages/admin/security.vue.
export type RecordStatus = 'ok' | 'warn' | 'bad' | 'pending'
export type DomainStatus = 'pending' | 'verifying' | 'active' | 'error'
export type RecordKind = 'ownership' | 'mx' | 'spf' | 'dkim' | 'dmarc' | 'autodiscovery'
export type DmarcPolicy = 'none' | 'quarantine' | 'reject'
export interface DomainRecordView {
kind: RecordKind
type: string
host: string
fqdn: string
expected: string
priority?: number
observed?: string
status: RecordStatus
}
export interface DomainView {
id: string
domain: string
isPrimary: boolean
status: DomainStatus
ownershipVerified: boolean
verificationToken: string
dmarcPolicy: DmarcPolicy
stalwartProvisioned: boolean
stalwartError?: string
mailboxes: number
checks: Record<'ownership' | 'mx' | 'spf' | 'dkim' | 'dmarc', RecordStatus>
records: DomainRecordView[]
lastCheckedAt?: string
}
export function useDomains() {
const { tenant } = useTenant()
const slug = computed(() => tenant.value?.slug ?? '')
const { request } = useApiFetch()
const base = () => `/api/tenants/${slug.value}/domains`
const one = (domain: string) => `${base()}/${encodeURIComponent(domain)}`
const { data: domains, refresh, pending } = useFetch<DomainView[]>(base, {
key: 'admin-domains',
default: () => [],
immediate: !!slug.value,
watch: [slug],
})
const add = (domain: string) =>
request<DomainView>(base(), { method: 'POST', body: { domain } })
const getOne = (domain: string) => request<DomainView>(one(domain))
const recheck = (domain: string) =>
request<DomainView>(`${one(domain)}/recheck`, { method: 'POST' })
const setDmarcPolicy = (domain: string, dmarcPolicy: DmarcPolicy) =>
request<DomainView>(`${one(domain)}/dmarc`, { method: 'PATCH', body: { dmarcPolicy } })
const remove = (domain: string) => request(one(domain), { method: 'DELETE' })
return { domains, pending, refresh, slug, add, getOne, recheck, setDmarcPolicy, remove }
}