import { NextRequest, NextResponse } from "next/server"; import { z } from "zod"; import { getSessionUser } from "@/lib/session"; import { createSupportTicket, listSupportTicketsForUser, listAllSupportTickets, } from "@/lib/db"; import { sendSupportTicketCreatedEmail, sendSupportAdminNotificationEmail, } from "@/lib/email"; import { safeError } from "@/lib/errors"; /** * Support tickets API (Feature 5). * * Visibility: tickets are scoped strictly per-user (zitadel_user_id). * Coworkers in the same org cannot see each other's tickets — this * is the team's design choice for privacy. Platform admins see * everything (the admin queue lives at the same UI but pulls from * a different list). * * GET — for platform users, returns all tickets across all users. * For everyone else, returns only the caller's own tickets. The * client decides the rendering based on user role; we just return * the right list. * * POST — creates a ticket, sends a confirmation email to the * customer and a notification email to the admin distribution list. */ const createSchema = z.object({ title: z.string().trim().min(3, "required").max(200), description: z.string().trim().min(10, "required").max(10_000), category: z.enum(["bug", "feature_request", "question", "billing", "other"]), }); export async function GET() { const user = await getSessionUser(); if (!user) { return NextResponse.json({ error: "Unauthorized" }, { status: 401 }); } // Platform admins get the global queue; everyone else sees their // own tickets only. Visibility-by-default-deny: even an org owner // doesn't see their coworkers' tickets, by Feature 5 design. const tickets = user.isPlatform ? await listAllSupportTickets() : await listSupportTicketsForUser(user.id); return NextResponse.json({ tickets }); } export async function POST(req: NextRequest) { const user = await getSessionUser(); if (!user) { return NextResponse.json({ error: "Unauthorized" }, { status: 401 }); } const body = await req.json().catch(() => null); const parsed = createSchema.safeParse(body); if (!parsed.success) { return NextResponse.json( { error: "Invalid input", details: parsed.error.flatten() }, { status: 400 } ); } try { const ticket = await createSupportTicket({ zitadelOrgId: user.orgId, zitadelUserId: user.id, title: parsed.data.title, description: parsed.data.description, category: parsed.data.category, contactName: user.name, contactEmail: user.email, }); // Fire-and-log email notifications. Both are best-effort; // failure to send doesn't roll back the ticket creation. sendSupportTicketCreatedEmail({ to: user.email, contactName: user.name, ticketId: ticket.id, title: ticket.title, }).catch((e) => console.error("ticket created email:", e)); sendSupportAdminNotificationEmail({ reason: "created", ticketId: ticket.id, title: ticket.title, contactName: user.name, contactEmail: user.email, body: ticket.description, category: ticket.category, }).catch((e) => console.error("admin notification:", e)); return NextResponse.json({ ticket }, { status: 201 }); } catch (e: any) { console.error("Failed to create support ticket:", e); return NextResponse.json( { error: safeError(e, "Failed to create ticket") }, { status: 500 } ); } }