This commit is contained in:
103
src/app/api/support/tickets/route.ts
Normal file
103
src/app/api/support/tickets/route.ts
Normal file
@@ -0,0 +1,103 @@
|
||||
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 }
|
||||
);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user