feat(docver): add access token endpoint and Bearer auth on all endpoints
- POST /api/documents/token issues JWT HS256 (claims userid/username/fid/template) after x-api-key check against new env DOCVER_API_KEY (fail-closed, timing-safe, uniform 401) - all docver endpoints now require Authorization: Bearer, verified with env DOCVER_TOKEN_SECRET (TTL via DOCVER_TOKEN_TTL_SECONDS, default 3600); secret unset = 401 on verify / 500 on issue - identity (userId/userName) + templateId extracted from token claims instead of request payload; removed from multipart form (legacy fields now 400) - lib/docver-token.ts: hand-rolled JWT via node:crypto only — alg pinned to HS256 (anti alg-confusion), timing-safe compare with both sides hashed, exp enforced, identical 401 for all bearer failure modes - openapi spec: /api/documents/token path + DocverBearer scheme; PLAN-DOCVER.md / CLAUDE.md / .env.example updated - tests: new tests/lib/docver-token.test.ts + adjusted parseVersionUploadForm tests (258 pass, no DB env needed) - package-lock.json: sync minio (^8.0.7) that was missing from the earlier docver commit Co-Authored-By: Claude Code <noreply@anthropic.com>
This commit is contained in:
parent
280cc2da40
commit
98b370c925
13 changed files with 932 additions and 131 deletions
47
app/api/documents/token/route.ts
Normal file
47
app/api/documents/token/route.ts
Normal file
|
|
@ -0,0 +1,47 @@
|
|||
// PLAN-DOCVER.md §auth — POST /api/documents/token ออก access_token ของกลุ่ม docver
|
||||
// ยืนยันด้วย header "x-api-key" เทียบ env DOCVER_API_KEY (fail closed เมื่อ env ไม่
|
||||
// ตั้ง/ว่าง) — token เป็น JWT HS256 เซ็นด้วย env DOCVER_TOKEN_SECRET อายุตาม env
|
||||
// DOCVER_TOKEN_TTL_SECONDS (default 3600) แล้วนำไปแนบ Authorization: Bearer กับ
|
||||
// endpoint อื่นของกลุ่ม (lib/docver-token.ts) · segment คงที่ "token" ไม่ชน [docId]
|
||||
// (Next ให้ static ชนะ) และ middleware ยกเว้น api/documents/* อยู่แล้ว
|
||||
import { docverError } from "@/lib/document-versioning";
|
||||
import {
|
||||
issueDocverToken,
|
||||
parseTokenRequestBody,
|
||||
requireDocverApiKey,
|
||||
} from "@/lib/docver-token";
|
||||
|
||||
export const runtime = "nodejs";
|
||||
export const dynamic = "force-dynamic";
|
||||
|
||||
export async function POST(request: Request) {
|
||||
const apiKeyFail = requireDocverApiKey(request);
|
||||
if (apiKeyFail !== null) return apiKeyFail;
|
||||
|
||||
let raw: unknown;
|
||||
try {
|
||||
raw = await request.json();
|
||||
} catch {
|
||||
return docverError("body ต้องเป็น JSON", 400);
|
||||
}
|
||||
const parsed = parseTokenRequestBody(raw);
|
||||
if (!parsed.ok) return docverError(parsed.error, 400);
|
||||
|
||||
// ความผิด config ฝั่ง server ไม่ใช่ความผิดของผู้เรียก — ไม่ตั้ง = 500 (ห้าม log ค่า secret)
|
||||
const secret = process.env.DOCVER_TOKEN_SECRET?.trim();
|
||||
if (secret === undefined || secret === "") {
|
||||
return docverError("DOCVER_TOKEN_SECRET ยังไม่ถูกตั้งค่า", 500);
|
||||
}
|
||||
const ttlRaw = Number(process.env.DOCVER_TOKEN_TTL_SECONDS);
|
||||
const ttlSeconds = Number.isFinite(ttlRaw) && ttlRaw > 0 ? Math.floor(ttlRaw) : 3600;
|
||||
|
||||
const accessToken = issueDocverToken(parsed.claims, secret, ttlSeconds);
|
||||
return new Response(
|
||||
JSON.stringify({
|
||||
access_token: accessToken,
|
||||
token_type: "Bearer",
|
||||
expires_in: ttlSeconds,
|
||||
}),
|
||||
{ headers: { "content-type": "application/json; charset=utf-8" } },
|
||||
);
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue