can.school shows "Approve as school" (§16). ?html=<messageId> returns that| Location | Name | Required | Meaning |
|---|---|---|---|
| path | id | Yes | Use the ID from an authorized staff response; source validators reject malformed IDs. |
| query | html | Conditional / optional | {"type":"string"} |
can.school shows "Approve as school" (§16). ?html=<messageId> returns that{error:{code,message}} for CloudError failures, attaches Request-Id, and disables caching. Handle 401 by signing in; show role refusal for 403 forbidden, reauthenticate only for step_up_required, and collect a reason for 400 reason_required. A CSRF failure needs a current page/session token, not blind retries. Operation-specific 404/409/413/422/502/503 errors retain their status. Honor Retry-After on 429./**
* GET /api/admin/support/tickets/:id (support.read): the ticket and its thread (inbound mail as text) plus the staff
* directory for the assignee picker; `can.school` shows "Approve as school" (§16). `?html=<messageId>` returns that
* inbound message's stored HTML instead, which the desk shows only inside a sandboxed, network-less iframe.
*/
export const GET = adminRoute("support.read", async (request, ctx, { params }: Context) => {
const id = ticketIdParam((await params).id);
const html = new URL(request.url).searchParams.get("html");
if (html !== null) {
if (!/^msg_[A-Za-z0-9_-]{18}$/.test(html)) throw invalid("Unknown message.");
return adminJson({ html: await htmlOf(id, html) });
}
const [thread, staff] = await Promise.all([getThread(ctx, id), staffDirectory()]);
return adminJson({ ...thread, staff, can: ticketPermissions(ctx.admin.role) });
});