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
|
|
@ -1,12 +1,14 @@
|
|||
// PLAN-DOCVER.md §1 — GET /api/documents/{docId}/versions/{versionId}/download-link?format=docx|pdf
|
||||
// คืน presigned URL อายุสั้น (env DOCVER_LINK_TTL_SECONDS) ให้ UI เปิดในเบราว์เซอร์จริง
|
||||
// ของผู้ใช้ — เช็คทั้ง docId และ versionId พร้อมกันกันโยน id ข้ามเอกสาร
|
||||
// auth: Authorization: Bearer (access_token จาก POST /api/documents/token — ทุก endpoint ของกลุ่ม)
|
||||
import {
|
||||
docverError,
|
||||
parseDocId,
|
||||
parseDownloadFormat,
|
||||
parseVersionIdParam,
|
||||
} from "@/lib/document-versioning";
|
||||
import { requireDocverBearer } from "@/lib/docver-token";
|
||||
import { findVersionForDoc, presignVersionUrl } from "@/lib/docver-store";
|
||||
|
||||
export const runtime = "nodejs";
|
||||
|
|
@ -16,6 +18,9 @@ export async function GET(
|
|||
request: Request,
|
||||
{ params }: { params: { docId: string; versionId: string } },
|
||||
) {
|
||||
const auth = requireDocverBearer(request);
|
||||
if (!auth.ok) return auth.response;
|
||||
|
||||
const docId = parseDocId(params.docId);
|
||||
if (!docId.ok) return docverError(docId.error, 400);
|
||||
const versionId = parseVersionIdParam(params.versionId);
|
||||
|
|
|
|||
|
|
@ -1,6 +1,8 @@
|
|||
// PLAN-DOCVER.md §1 — POST อัปโหลด version ใหม่ + GET history ของ /api/documents/{docId}/versions
|
||||
// กลุ่ม API stand-alone ไม่มี auth (เปิดกว้างเหมือน api/template — intranet) — ผู้เรียกบอก
|
||||
// identity เองผ่าน field userId/userName เพราะไม่มี session ร่วมกับใคร
|
||||
// กลุ่ม API stand-alone ไม่มี session — auth ด้วย access_token ของกลุ่มนี้: ขอที่
|
||||
// POST /api/documents/token (ยืนยัน x-api-key กับ env DOCVER_API_KEY) แล้วแนบ
|
||||
// Authorization: Bearer ทุก request — identity (userid/username) และ templateId
|
||||
// แกะจาก claims ของ token ไม่รับจาก multipart form อีก (lib/docver-token.ts)
|
||||
import { createHash, randomUUID } from "node:crypto";
|
||||
|
||||
import { toDocumentVersionDTO } from "@/db/schema";
|
||||
|
|
@ -14,6 +16,7 @@ import {
|
|||
parseVersionUploadForm,
|
||||
sanitizeFilePartName,
|
||||
} from "@/lib/document-versioning";
|
||||
import { requireDocverBearer } from "@/lib/docver-token";
|
||||
import {
|
||||
findDocumentByDocId,
|
||||
insertVersionRecord,
|
||||
|
|
@ -27,11 +30,14 @@ export const runtime = "nodejs";
|
|||
export const dynamic = "force-dynamic";
|
||||
|
||||
/** POST — อัปโหลด version ใหม่ (เอกสารสร้างแบบ implicit ถ้าเป็น version แรก)
|
||||
* ลำดับ: validate text field ก่อนแตะไฟล์ → cap ขนาดก่อน buffer → magic bytes →
|
||||
* ลำดับ: Bearer token → validate text field ก่อนแตะไฟล์ → cap ขนาดก่อน buffer → magic bytes →
|
||||
* putObject MinIO → insert DB (tx) — DB ล้ม = เก็บกวาด object (pattern N4)
|
||||
* response = UploadVersionResult เป๊ะ โดย versionNumber เป็นค่า authoritative
|
||||
* จาก server (client เดา latest+1 แล้วแก้เองเมื่ออัปโหลดชนกัน) */
|
||||
export async function POST(request: Request, { params }: { params: { docId: string } }) {
|
||||
const auth = requireDocverBearer(request);
|
||||
if (!auth.ok) return auth.response;
|
||||
|
||||
const docId = parseDocId(params.docId);
|
||||
if (!docId.ok) return docverError(docId.error, 400);
|
||||
|
||||
|
|
@ -114,14 +120,14 @@ export async function POST(request: Request, { params }: { params: { docId: stri
|
|||
contentHash,
|
||||
message: parsed.form.message,
|
||||
fields: parsed.form.fields,
|
||||
templateId: parsed.form.templateId,
|
||||
templateId: auth.claims.template,
|
||||
fileName: docxDisplayName,
|
||||
docxObjectKey: keys.docx,
|
||||
pdfObjectKey: pdfBytes ? keys.pdf : null,
|
||||
docxSize: docxBytes.length,
|
||||
pdfSize: pdfBytes?.length ?? null,
|
||||
userId: parsed.form.userId,
|
||||
userName: parsed.form.userName,
|
||||
userId: auth.claims.userid,
|
||||
userName: auth.claims.username,
|
||||
});
|
||||
} catch (err) {
|
||||
// DB ล้มหลังเขียน object — เก็บกวาดก่อนตอบ กัน object กำพร้าไม่มี row ชี้
|
||||
|
|
@ -142,7 +148,10 @@ export async function POST(request: Request, { params }: { params: { docId: stri
|
|||
|
||||
/** GET — history ของเอกสาร (ใหม่ → เก่า) · docId ที่ยังไม่เคยมี version = 404
|
||||
* (สัญญาณ REST ตรง ๆ แบบ N10 — UI ฝั่ง add-in แสดง error + ปุ่มลองใหม่ ไม่พัง) */
|
||||
export async function GET(_request: Request, { params }: { params: { docId: string } }) {
|
||||
export async function GET(request: Request, { params }: { params: { docId: string } }) {
|
||||
const auth = requireDocverBearer(request);
|
||||
if (!auth.ok) return auth.response;
|
||||
|
||||
const docId = parseDocId(params.docId);
|
||||
if (!docId.ok) return docverError(docId.error, 400);
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue