Final parity push (web-tier only): - REST API write + token auth: POST /api/tokens (issue a personal_access_token for the session user), Bearer auth via src/lib/api-auth.ts, POST /api/articles/[slug]/comment, GET/DELETE /api/me/tokens, full tickets API (/api/tickets +[id] +[id]/reply), radio current-dj/points/points-leaderboard/ embed-config + POST shouts, and a real-time /api/radio/stream (SSE). 31 public API routes total. - Pages: /draw-badge (buy a custom profile badge → credits + RCON), /me dashboard (stats + online friends + referral claim). Wired into the nav. - HTML sanitisation (sanitize-html) — the HTMLPurifier equivalent — applied to writeable boxes + article bodies before dangerouslySetInnerHTML. - "Dusk" dark theme preset + a default-dark site option honoured by the no-flash boot script. Verified live (prod, amx_test): token issue → Bearer endpoint 200, no-token 401; /api/me/tokens lists it; current-dj/leaderboard JSON; /me + /draw-badge 200; reverted the test user + tokens. tsc 0, vitest 49/49, next build 0.
66 lines
2.1 KiB
TypeScript
66 lines
2.1 KiB
TypeScript
// Public REST API — post a reply to a help-center ticket.
|
|
//
|
|
// Bearer-authed. POST inserts a reply ({ content }) authored by the current user
|
|
// into website_help_center_ticket_replies. The target ticket must exist and
|
|
// belong to the authed user. Fail-soft: never a 500.
|
|
|
|
import { apiError, apiJson } from "@/lib/api";
|
|
import { bearerUserId } from "@/lib/api-auth";
|
|
import { prisma } from "@/lib/prisma";
|
|
|
|
export const dynamic = "force-dynamic";
|
|
|
|
// POST /api/tickets/:id/reply body: { content }
|
|
export async function POST(req: Request, { params }: { params: Promise<{ id: string }> }) {
|
|
const uid = await bearerUserId(req);
|
|
if (!uid) return apiError("Unauthorized", 401);
|
|
|
|
const { id } = await params;
|
|
if (!/^\d+$/.test(id)) return apiError("Invalid ticket id");
|
|
const ticketId = BigInt(id);
|
|
|
|
const body = (await req.json().catch(() => ({}))) as { content?: unknown };
|
|
const content = String(body.content ?? "").trim().slice(0, 5000);
|
|
if (!content) return apiError("Content is required");
|
|
|
|
try {
|
|
// Ownership check — only the ticket owner may reply.
|
|
const ticket = await prisma.websiteHelpCenterTickets.findUnique({
|
|
where: { id: ticketId },
|
|
select: { id: true, userId: true },
|
|
});
|
|
if (!ticket || ticket.userId !== uid) return apiError("Ticket not found", 404);
|
|
|
|
const now = new Date();
|
|
const reply = await prisma.websiteHelpCenterTicketReplies.create({
|
|
data: {
|
|
ticketId,
|
|
userId: uid,
|
|
content,
|
|
createdAt: now,
|
|
updatedAt: now,
|
|
},
|
|
select: { id: true, userId: true, content: true, createdAt: true },
|
|
});
|
|
|
|
// Touch the parent ticket so its updatedAt reflects the latest activity.
|
|
prisma.websiteHelpCenterTickets
|
|
.update({ where: { id: ticketId }, data: { updatedAt: now }, select: { id: true } })
|
|
.catch(() => {});
|
|
|
|
return apiJson(
|
|
{
|
|
reply: {
|
|
id: reply.id,
|
|
userId: reply.userId,
|
|
content: reply.content,
|
|
createdAt: reply.createdAt,
|
|
},
|
|
},
|
|
{ status: 201 },
|
|
);
|
|
} catch {
|
|
return apiError("Failed to post reply", 503);
|
|
}
|
|
}
|