Close the web-feasible 100% gaps: REST write/token API, tickets, draw-badge, /me, sanitisation, dusk, radio SSE
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.
This commit is contained in:
1 parent
8cedf5614e
commit
f7b3845131
30 files changed
+1734
-10
No files matched your search
@@ -0,0 +1,61 @@
|
||||
// Public REST API — manage the SIGNED-IN user's personal access tokens.
|
||||
//
|
||||
// GET /api/me/tokens — list the current user's tokens (id, name,
|
||||
// lastUsedAt). The token hash is NEVER returned.
|
||||
// DELETE /api/me/tokens?id=42 — revoke one of the current user's tokens.
|
||||
//
|
||||
// Auth is the NextAuth web session (auth()), not a Bearer token. Tokens belong
|
||||
// to the user via personal_access_tokens.tokenable_id (a BigInt). NOTE: the live
|
||||
// table has no expires_at column, so it is never read or written here.
|
||||
|
||||
import { apiError, apiJson } from "@/lib/api";
|
||||
import { auth } from "@/lib/auth";
|
||||
import { prisma } from "@/lib/prisma";
|
||||
|
||||
export const dynamic = "force-dynamic";
|
||||
|
||||
async function currentUserId(): Promise<number | null> {
|
||||
const session = await auth();
|
||||
const id = session?.user?.id ? Number(session.user.id) : null;
|
||||
return id && !Number.isNaN(id) ? id : null;
|
||||
}
|
||||
|
||||
export async function GET(_req: Request) {
|
||||
const id = await currentUserId();
|
||||
if (!id) return apiError("Unauthorized", 401);
|
||||
|
||||
try {
|
||||
const tokens = await prisma.personalAccessTokens.findMany({
|
||||
where: { tokenableId: BigInt(id) },
|
||||
select: { id: true, name: true, lastUsedAt: true },
|
||||
orderBy: { id: "desc" },
|
||||
});
|
||||
// Never expose the token hash.
|
||||
return apiJson({ data: tokens });
|
||||
} catch {
|
||||
return apiJson({ data: [] });
|
||||
}
|
||||
}
|
||||
|
||||
export async function DELETE(req: Request) {
|
||||
const id = await currentUserId();
|
||||
if (!id) return apiError("Unauthorized", 401);
|
||||
|
||||
const tokenId = new URL(req.url).searchParams.get("id");
|
||||
if (!tokenId || !/^\d+$/.test(tokenId)) {
|
||||
return apiError("A valid token id is required", 422);
|
||||
}
|
||||
|
||||
try {
|
||||
// Scope the delete to the owner so users cannot revoke others' tokens.
|
||||
const result = await prisma.personalAccessTokens.deleteMany({
|
||||
where: { id: BigInt(tokenId), tokenableId: BigInt(id) },
|
||||
});
|
||||
if (result.count === 0) {
|
||||
return apiError("Token not found", 404);
|
||||
}
|
||||
return apiJson({ ok: true });
|
||||
} catch {
|
||||
return apiError("Could not revoke token", 400);
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user