/** * POST /_emdash/api/auth/invite * * Create an invite for a new user. Admin only. * * When an email provider is configured (via the plugin email pipeline), * the invite email is sent automatically. * When no provider is configured, returns the invite URL for the admin * to share manually (copy-link fallback). */ import type { APIRoute } from "astro"; export const prerender = false; import { builtinRoleForLevel, createInvite, InviteError, Role } from "@premium-cms/auth"; import { createKyselyAdapter } from "@premium-cms/auth/adapters/kysely"; import { requirePerm } from "#api/authorize.js"; import { apiError, apiSuccess, handleError } from "#api/error.js"; import { isParseError, parseBody } from "#api/parse.js"; import { inviteCreateBody } from "#api/schemas.js"; import { getSiteBaseUrl } from "#api/site-url.js"; import { AuthzRepository } from "#db/repositories/authz.js"; import { OptionsRepository } from "#db/repositories/options.js"; export const POST: APIRoute = async ({ request, locals }) => { const { emdash, user } = locals; if (!emdash?.db) { return apiError("NOT_CONFIGURED", "EmDash is not initialized", 500); } const denied = requirePerm(user, "users:invite"); if (denied) return denied; const adapter = createKyselyAdapter(emdash.db); try { const body = await parseBody(request, inviteCreateBody); if (isParseError(body)) return body; // Resolve the invited role: `roleId` (id or slug) wins, else the legacy // level maps to its built-in role. Default is Author, as before. let role = body.role ?? Role.AUTHOR; let roleId: string; if (body.roleId !== undefined) { const summary = await new AuthzRepository(emdash.db).getRole(body.roleId); if (!summary) return apiError("UNKNOWN_ROLE", "Role not found", 400); roleId = summary.id; role = summary.level as typeof role; } else { roleId = `role:${builtinRoleForLevel(role).slug}`; } // Get site config for invite email const options = new OptionsRepository(emdash.db); const siteName = (await options.get("emdash:site_title")) || "EmDash"; // Use the configured site URL (stored option as fallback) to prevent Host header spoofing in invite emails const baseUrl = await getSiteBaseUrl(emdash.db, request, emdash.config); // Build email sender from the plugin pipeline (if available) const emailSend = emdash.email?.isAvailable() ? (message: { to: string; subject: string; text: string; html?: string }) => emdash.email!.send(message, "system") : undefined; const result = await createInvite( { baseUrl, siteName, email: emailSend, }, adapter, body.email, role, user!.id, roleId, ); if (emailSend) { // Email was sent return apiSuccess({ success: true, message: `Invite sent to ${body.email}`, }); } // No email provider — return the invite URL for manual sharing return apiSuccess( { success: true, message: "Invite created. No email provider configured — share the link manually.", inviteUrl: result.url, }, 200, ); } catch (error) { if (error instanceof InviteError) { const statusMap: Record = { user_exists: 409, invalid_token: 400, token_expired: 400, }; return apiError(error.code.toUpperCase(), error.message, statusMap[error.code] ?? 400); } return handleError(error, "Failed to create invite", "INVITE_CREATE_ERROR"); } };