104 lines
3.4 KiB
TypeScript
104 lines
3.4 KiB
TypeScript
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 }
|
|
);
|
|
}
|
|
}
|