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,64 @@
|
||||
import { createHash, randomBytes } from "node:crypto";
|
||||
import { prisma } from "@/lib/prisma";
|
||||
|
||||
/**
|
||||
* Bearer-token auth for the public REST API, backed by personal_access_tokens
|
||||
* (the Laravel Sanctum table that already exists in the emulator DB). Tokens are
|
||||
* stored as the sha256 of the plaintext; the client sends the plaintext (or the
|
||||
* Sanctum "{id}|{plaintext}" form) as `Authorization: Bearer …`.
|
||||
*
|
||||
* NOTE: the live amx_test table has NO expires_at column — never read/write it.
|
||||
*/
|
||||
const TOKENABLE_TYPE = "App\\Models\\User";
|
||||
|
||||
function hashToken(raw: string): string {
|
||||
return createHash("sha256").update(raw).digest("hex");
|
||||
}
|
||||
|
||||
/** Resolve the user id behind a Bearer token, or null. */
|
||||
export async function bearerUserId(req: Request): Promise<number | null> {
|
||||
const header = req.headers.get("authorization") ?? "";
|
||||
const m = header.match(/^Bearer\s+(.+)$/i);
|
||||
if (!m) return null;
|
||||
let raw = m[1].trim();
|
||||
const pipe = raw.indexOf("|");
|
||||
if (pipe >= 0) raw = raw.slice(pipe + 1); // Sanctum "{id}|{token}"
|
||||
if (!raw) return null;
|
||||
|
||||
try {
|
||||
const row = await prisma.personalAccessTokens.findFirst({
|
||||
where: { token: hashToken(raw) },
|
||||
select: { id: true, tokenableId: true },
|
||||
});
|
||||
if (!row) return null;
|
||||
// Best-effort last-used stamp (don't fail the request if it errors).
|
||||
prisma.personalAccessTokens
|
||||
.update({ where: { id: row.id }, data: { lastUsedAt: new Date() }, select: { id: true } })
|
||||
.catch(() => {});
|
||||
return Number(row.tokenableId);
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/** Mint a new token for a user. Returns the plaintext (shown once). */
|
||||
export async function issueToken(userId: number, name = "api"): Promise<string | null> {
|
||||
const plaintext = randomBytes(32).toString("hex");
|
||||
try {
|
||||
await prisma.personalAccessTokens.create({
|
||||
data: {
|
||||
tokenableId: BigInt(userId),
|
||||
tokenableType: TOKENABLE_TYPE,
|
||||
name: name.slice(0, 100),
|
||||
token: hashToken(plaintext),
|
||||
abilities: '["*"]',
|
||||
createdAt: new Date(),
|
||||
updatedAt: new Date(),
|
||||
},
|
||||
select: { id: true },
|
||||
});
|
||||
return plaintext;
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user