import { NextResponse } from 'next/server'; import { z } from 'zod'; import { withAuth, withPermission } from '@/lib/api/helpers'; import { parseBody } from '@/lib/api/route-helpers'; import { errorResponse } from '@/lib/errors'; import { createPortalUser, resendActivation } from '@/lib/services/portal-auth.service'; import { db } from '@/lib/db'; import { eq } from 'drizzle-orm'; import { portalUsers } from '@/lib/db/schema/portal'; const inviteSchema = z.object({ email: z.string().email(), name: z.string().min(1).max(200).optional(), }); /** * POST /api/v1/clients/:id/portal-user * * Admin creates a portal account for a client and triggers the activation * email. Idempotent in spirit: if a portal user already exists for the * email, returns 409 — the admin can resend the activation via * ?action=resend. */ export const POST = withAuth( withPermission('clients', 'edit', async (req, ctx, params) => { try { const url = new URL(req.url); const action = url.searchParams.get('action'); if (action === 'resend') { // Body is optional in resend mode; the portal user id is the path id // in this case (not the client id). Looking up by client+email so // admins don't have to track portal-user ids. const body = await parseBody(req, inviteSchema); const existing = await db.query.portalUsers.findFirst({ where: eq(portalUsers.email, body.email.toLowerCase().trim()), }); if (!existing) { return NextResponse.json({ error: 'Portal user not found' }, { status: 404 }); } await resendActivation(existing.id, ctx.portId); return NextResponse.json({ success: true }); } const body = await parseBody(req, inviteSchema); const result = await createPortalUser({ clientId: params.id!, portId: ctx.portId, email: body.email, name: body.name, createdBy: ctx.userId, }); return NextResponse.json({ data: result }, { status: 201 }); } catch (err) { return errorResponse(err); } }), );