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.
This commit is contained in:
@@ -0,0 +1,22 @@
|
||||
// Set the DMARC policy for a domain (wizard step 5). Proxies
|
||||
// PATCH /tenants/:slug/domains/:domain/dmarc with { dmarcPolicy };
|
||||
// platform-api updates the expected record, re-verifies, and enforces membership.
|
||||
|
||||
import { getUserSession } from 'nuxt-oidc-auth/runtime/server/utils/session.js'
|
||||
|
||||
export default defineEventHandler(async (event) => {
|
||||
const session = await getUserSession(event).catch(() => null)
|
||||
const accessToken = (session as { accessToken?: string } | null)?.accessToken
|
||||
if (!accessToken) {
|
||||
throw createError({ statusCode: 401, statusMessage: 'Not signed in' })
|
||||
}
|
||||
const slug = getRouterParam(event, 'slug')
|
||||
const domain = getRouterParam(event, 'domain')
|
||||
const body = await readBody(event)
|
||||
const base = process.env.PLATFORM_API_INTERNAL_URL ?? 'http://platform-api:3001'
|
||||
return $fetch(`${base}/tenants/${slug}/domains/${encodeURIComponent(domain ?? '')}/dmarc`, {
|
||||
method: 'PATCH',
|
||||
headers: { Authorization: `Bearer ${accessToken}` },
|
||||
body,
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,20 @@
|
||||
// Remove a domain. Proxies DELETE /tenants/:slug/domains/:domain; platform-api
|
||||
// deletes it from Stalwart (DKIM sigs first) and enforces tenant membership.
|
||||
|
||||
import { getUserSession } from 'nuxt-oidc-auth/runtime/server/utils/session.js'
|
||||
|
||||
export default defineEventHandler(async (event) => {
|
||||
const session = await getUserSession(event).catch(() => null)
|
||||
const accessToken = (session as { accessToken?: string } | null)?.accessToken
|
||||
if (!accessToken) {
|
||||
throw createError({ statusCode: 401, statusMessage: 'Not signed in' })
|
||||
}
|
||||
const slug = getRouterParam(event, 'slug')
|
||||
const domain = getRouterParam(event, 'domain')
|
||||
const base = process.env.PLATFORM_API_INTERNAL_URL ?? 'http://platform-api:3001'
|
||||
await $fetch(`${base}/tenants/${slug}/domains/${encodeURIComponent(domain ?? '')}`, {
|
||||
method: 'DELETE',
|
||||
headers: { Authorization: `Bearer ${accessToken}` },
|
||||
})
|
||||
return { ok: true }
|
||||
})
|
||||
@@ -0,0 +1,18 @@
|
||||
// Single domain detail (expected + observed records). Proxies
|
||||
// GET /tenants/:slug/domains/:domain; platform-api enforces tenant membership.
|
||||
|
||||
import { getUserSession } from 'nuxt-oidc-auth/runtime/server/utils/session.js'
|
||||
|
||||
export default defineEventHandler(async (event) => {
|
||||
const session = await getUserSession(event).catch(() => null)
|
||||
const accessToken = (session as { accessToken?: string } | null)?.accessToken
|
||||
if (!accessToken) {
|
||||
throw createError({ statusCode: 401, statusMessage: 'Not signed in' })
|
||||
}
|
||||
const slug = getRouterParam(event, 'slug')
|
||||
const domain = getRouterParam(event, 'domain')
|
||||
const base = process.env.PLATFORM_API_INTERNAL_URL ?? 'http://platform-api:3001'
|
||||
return $fetch(`${base}/tenants/${slug}/domains/${encodeURIComponent(domain ?? '')}`, {
|
||||
headers: { Authorization: `Bearer ${accessToken}` },
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,20 @@
|
||||
// Re-run the live DNS checks for a domain. Proxies
|
||||
// POST /tenants/:slug/domains/:domain/recheck; platform-api re-verifies
|
||||
// MX/SPF/DKIM/DMARC/ownership against public DNS and enforces membership.
|
||||
|
||||
import { getUserSession } from 'nuxt-oidc-auth/runtime/server/utils/session.js'
|
||||
|
||||
export default defineEventHandler(async (event) => {
|
||||
const session = await getUserSession(event).catch(() => null)
|
||||
const accessToken = (session as { accessToken?: string } | null)?.accessToken
|
||||
if (!accessToken) {
|
||||
throw createError({ statusCode: 401, statusMessage: 'Not signed in' })
|
||||
}
|
||||
const slug = getRouterParam(event, 'slug')
|
||||
const domain = getRouterParam(event, 'domain')
|
||||
const base = process.env.PLATFORM_API_INTERNAL_URL ?? 'http://platform-api:3001'
|
||||
return $fetch(`${base}/tenants/${slug}/domains/${encodeURIComponent(domain ?? '')}/recheck`, {
|
||||
method: 'POST',
|
||||
headers: { Authorization: `Bearer ${accessToken}` },
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,18 @@
|
||||
// List the workspace's email domains (Domains page + sidebar badge). Proxies
|
||||
// GET /tenants/:slug/domains with the signed-in user's access token;
|
||||
// platform-api enforces tenant membership.
|
||||
|
||||
import { getUserSession } from 'nuxt-oidc-auth/runtime/server/utils/session.js'
|
||||
|
||||
export default defineEventHandler(async (event) => {
|
||||
const session = await getUserSession(event).catch(() => null)
|
||||
const accessToken = (session as { accessToken?: string } | null)?.accessToken
|
||||
if (!accessToken) {
|
||||
throw createError({ statusCode: 401, statusMessage: 'Not signed in' })
|
||||
}
|
||||
const slug = getRouterParam(event, 'slug')
|
||||
const base = process.env.PLATFORM_API_INTERNAL_URL ?? 'http://platform-api:3001'
|
||||
return $fetch(`${base}/tenants/${slug}/domains`, {
|
||||
headers: { Authorization: `Bearer ${accessToken}` },
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,21 @@
|
||||
// Add an email domain. Proxies POST /tenants/:slug/domains with { domain };
|
||||
// platform-api provisions it in Stalwart (auto-generating DKIM), seeds the
|
||||
// expected records, runs an initial DNS check, and enforces tenant membership.
|
||||
|
||||
import { getUserSession } from 'nuxt-oidc-auth/runtime/server/utils/session.js'
|
||||
|
||||
export default defineEventHandler(async (event) => {
|
||||
const session = await getUserSession(event).catch(() => null)
|
||||
const accessToken = (session as { accessToken?: string } | null)?.accessToken
|
||||
if (!accessToken) {
|
||||
throw createError({ statusCode: 401, statusMessage: 'Not signed in' })
|
||||
}
|
||||
const slug = getRouterParam(event, 'slug')
|
||||
const body = await readBody(event)
|
||||
const base = process.env.PLATFORM_API_INTERNAL_URL ?? 'http://platform-api:3001'
|
||||
return $fetch(`${base}/tenants/${slug}/domains`, {
|
||||
method: 'POST',
|
||||
headers: { Authorization: `Bearer ${accessToken}` },
|
||||
body,
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,21 @@
|
||||
// Create a workspace member. Proxies POST /tenants/:slug/users; platform-api
|
||||
// provisions the user across Authentik (SSO), Stalwart (mailbox on the default
|
||||
// domain) and OCIS, then returns the email + one-time temp password.
|
||||
|
||||
import { getUserSession } from 'nuxt-oidc-auth/runtime/server/utils/session.js'
|
||||
|
||||
export default defineEventHandler(async (event) => {
|
||||
const session = await getUserSession(event).catch(() => null)
|
||||
const accessToken = (session as { accessToken?: string } | null)?.accessToken
|
||||
if (!accessToken) {
|
||||
throw createError({ statusCode: 401, statusMessage: 'Not signed in' })
|
||||
}
|
||||
const slug = getRouterParam(event, 'slug')
|
||||
const body = await readBody(event)
|
||||
const base = process.env.PLATFORM_API_INTERNAL_URL ?? 'http://platform-api:3001'
|
||||
return $fetch(`${base}/tenants/${slug}/users`, {
|
||||
method: 'POST',
|
||||
headers: { Authorization: `Bearer ${accessToken}` },
|
||||
body,
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,21 @@
|
||||
// Remove a workspace member. Proxies DELETE /tenants/:slug/users/:userId;
|
||||
// platform-api tears down the mailbox, OCIS account and (if it was their last
|
||||
// workspace) the SSO identity. Enforces tenant membership + blocks self-removal.
|
||||
|
||||
import { getUserSession } from 'nuxt-oidc-auth/runtime/server/utils/session.js'
|
||||
|
||||
export default defineEventHandler(async (event) => {
|
||||
const session = await getUserSession(event).catch(() => null)
|
||||
const accessToken = (session as { accessToken?: string } | null)?.accessToken
|
||||
if (!accessToken) {
|
||||
throw createError({ statusCode: 401, statusMessage: 'Not signed in' })
|
||||
}
|
||||
const slug = getRouterParam(event, 'slug')
|
||||
const userId = getRouterParam(event, 'userId')
|
||||
const base = process.env.PLATFORM_API_INTERNAL_URL ?? 'http://platform-api:3001'
|
||||
await $fetch(`${base}/tenants/${slug}/users/${userId}`, {
|
||||
method: 'DELETE',
|
||||
headers: { Authorization: `Bearer ${accessToken}` },
|
||||
})
|
||||
return { ok: true }
|
||||
})
|
||||
@@ -0,0 +1,16 @@
|
||||
// Force-logout a member (terminate their SSO sessions). Proxies POST
|
||||
// /tenants/:slug/users/:userId/force-logout.
|
||||
import { getUserSession } from 'nuxt-oidc-auth/runtime/server/utils/session.js'
|
||||
|
||||
export default defineEventHandler(async (event) => {
|
||||
const session = await getUserSession(event).catch(() => null)
|
||||
const accessToken = (session as { accessToken?: string } | null)?.accessToken
|
||||
if (!accessToken) throw createError({ statusCode: 401, statusMessage: 'Not signed in' })
|
||||
const slug = getRouterParam(event, 'slug')
|
||||
const userId = getRouterParam(event, 'userId')
|
||||
const base = process.env.PLATFORM_API_INTERNAL_URL ?? 'http://platform-api:3001'
|
||||
return $fetch(`${base}/tenants/${slug}/users/${userId}/force-logout`, {
|
||||
method: 'POST',
|
||||
headers: { Authorization: `Bearer ${accessToken}` },
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,16 @@
|
||||
// Reset a member's password (new one-time password on SSO + mailbox). Proxies
|
||||
// POST /tenants/:slug/users/:userId/reset-password and returns { email, tempPassword }.
|
||||
import { getUserSession } from 'nuxt-oidc-auth/runtime/server/utils/session.js'
|
||||
|
||||
export default defineEventHandler(async (event) => {
|
||||
const session = await getUserSession(event).catch(() => null)
|
||||
const accessToken = (session as { accessToken?: string } | null)?.accessToken
|
||||
if (!accessToken) throw createError({ statusCode: 401, statusMessage: 'Not signed in' })
|
||||
const slug = getRouterParam(event, 'slug')
|
||||
const userId = getRouterParam(event, 'userId')
|
||||
const base = process.env.PLATFORM_API_INTERNAL_URL ?? 'http://platform-api:3001'
|
||||
return $fetch(`${base}/tenants/${slug}/users/${userId}/reset-password`, {
|
||||
method: 'POST',
|
||||
headers: { Authorization: `Bearer ${accessToken}` },
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,16 @@
|
||||
// Resume a suspended member. Proxies POST /tenants/:slug/users/:userId/resume.
|
||||
import { getUserSession } from 'nuxt-oidc-auth/runtime/server/utils/session.js'
|
||||
|
||||
export default defineEventHandler(async (event) => {
|
||||
const session = await getUserSession(event).catch(() => null)
|
||||
const accessToken = (session as { accessToken?: string } | null)?.accessToken
|
||||
if (!accessToken) throw createError({ statusCode: 401, statusMessage: 'Not signed in' })
|
||||
const slug = getRouterParam(event, 'slug')
|
||||
const userId = getRouterParam(event, 'userId')
|
||||
const base = process.env.PLATFORM_API_INTERNAL_URL ?? 'http://platform-api:3001'
|
||||
await $fetch(`${base}/tenants/${slug}/users/${userId}/resume`, {
|
||||
method: 'POST',
|
||||
headers: { Authorization: `Bearer ${accessToken}` },
|
||||
})
|
||||
return { ok: true }
|
||||
})
|
||||
@@ -0,0 +1,17 @@
|
||||
// Suspend a member (freeze SSO + mailbox). Proxies POST
|
||||
// /tenants/:slug/users/:userId/suspend.
|
||||
import { getUserSession } from 'nuxt-oidc-auth/runtime/server/utils/session.js'
|
||||
|
||||
export default defineEventHandler(async (event) => {
|
||||
const session = await getUserSession(event).catch(() => null)
|
||||
const accessToken = (session as { accessToken?: string } | null)?.accessToken
|
||||
if (!accessToken) throw createError({ statusCode: 401, statusMessage: 'Not signed in' })
|
||||
const slug = getRouterParam(event, 'slug')
|
||||
const userId = getRouterParam(event, 'userId')
|
||||
const base = process.env.PLATFORM_API_INTERNAL_URL ?? 'http://platform-api:3001'
|
||||
await $fetch(`${base}/tenants/${slug}/users/${userId}/suspend`, {
|
||||
method: 'POST',
|
||||
headers: { Authorization: `Bearer ${accessToken}` },
|
||||
})
|
||||
return { ok: true }
|
||||
})
|
||||
Reference in New Issue
Block a user