Files
Epicnabbo-Catalogus-Updated…/src/lib/api-auth.ts
T
2026-07-13 21:57:41 +02:00

72 lines
2.1 KiB
TypeScript

import { createHash, randomBytes } from "node:crypto";
import { personalTokenScope } from "@/lib/auth/personal-token-scope";
import { databaseUserId } from "@/lib/auth/session-user";
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 …`.
*/
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),
OR: [{ expiresAt: null }, { expiresAt: { gt: new Date() } }],
},
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 databaseUserId(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: {
...personalTokenScope(userId),
name: name.slice(0, 100),
token: hashToken(plaintext),
abilities: '["*"]',
createdAt: new Date(),
updatedAt: new Date(),
},
select: { id: true },
});
return plaintext;
} catch {
return null;
}
}