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
|
|
@ -23,16 +23,15 @@ const DOC_ID_RE = /^[A-Za-z0-9._:-]{1,200}$/;
|
|||
|
||||
const UUID_RE = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
|
||||
|
||||
/** field ที่รับใน multipart ของ POST …/versions — อะไรนอกนี้ = 400 กัน typo เงียบ ๆ */
|
||||
/** field ที่รับใน multipart ของ POST …/versions — อะไรนอกนี้ = 400 กัน typo เงียบ ๆ
|
||||
* (userId/userName/templateId ถอดออกแล้ว — identity/template มาจาก Bearer token
|
||||
* ผ่าน lib/docver-token.ts ส่ง field เก่ามา = 400 ตาม convention) */
|
||||
const FORM_FIELDS = [
|
||||
"file",
|
||||
"pdf",
|
||||
"userId",
|
||||
"userName",
|
||||
"clientVersionId",
|
||||
"fields",
|
||||
"message",
|
||||
"templateId",
|
||||
"fileName",
|
||||
] as const;
|
||||
|
||||
|
|
@ -160,11 +159,8 @@ export function parseFieldsJson(raw: string):
|
|||
}
|
||||
|
||||
export type ParsedVersionUploadForm = {
|
||||
userId: string;
|
||||
userName: string;
|
||||
clientVersionId: string | null;
|
||||
message: string | null;
|
||||
templateId: string | null;
|
||||
/** ชื่อไฟล์ดั้งเดิมจาก field "fileName" (แยกจากชื่อของ File เอง — add-in ส่งมาเอง
|
||||
* เพราะ desktop Word ได้ชื่อจาก path จริง) — strip path + cap แล้ว */
|
||||
fileName: string | null;
|
||||
|
|
@ -172,7 +168,8 @@ export type ParsedVersionUploadForm = {
|
|||
};
|
||||
|
||||
/** ตรวจ text field ทั้งหมดของ POST …/versions (file/pdf เช็คที่ route เพราะต้องอ่าน
|
||||
* size ก่อน buffer) — userId/userName บังคับเพราะระบบ stand-alone ไม่มี session */
|
||||
* size ก่อน buffer) — identity (userId/userName) กับ templateId ไม่รับใน form
|
||||
* แล้ว: route แกะจาก Bearer token ผ่าน lib/docver-token.ts ก่อนเรียกตรงนี้ */
|
||||
export function parseVersionUploadForm(form: FormData):
|
||||
| { ok: true; form: ParsedVersionUploadForm }
|
||||
| DocverError {
|
||||
|
|
@ -187,36 +184,12 @@ export function parseVersionUploadForm(form: FormData):
|
|||
}
|
||||
}
|
||||
|
||||
const userId = formString(form, "userId");
|
||||
if (!userId.ok) return userId;
|
||||
if (userId.value === null) {
|
||||
return { ok: false, error: "ต้องระบุ userId (ระบบ stand-alone — บอกผู้อัปโหลดเอง)" };
|
||||
}
|
||||
if (userId.value.length > 100) {
|
||||
return { ok: false, error: "userId ยาวเกิน 100 ตัวอักษร" };
|
||||
}
|
||||
|
||||
const userName = formString(form, "userName");
|
||||
if (!userName.ok) return userName;
|
||||
if (userName.value === null) {
|
||||
return { ok: false, error: "ต้องระบุ userName (ระบบ stand-alone — บอกผู้อัปโหลดเอง)" };
|
||||
}
|
||||
if (userName.value.length > 200) {
|
||||
return { ok: false, error: "userName ยาวเกิน 200 ตัวอักษร" };
|
||||
}
|
||||
|
||||
const clientVersionId = formString(form, "clientVersionId");
|
||||
if (!clientVersionId.ok) return clientVersionId;
|
||||
if (clientVersionId.value && clientVersionId.value.length > 100) {
|
||||
return { ok: false, error: "clientVersionId ยาวเกิน 100 ตัวอักษร" };
|
||||
}
|
||||
|
||||
const templateId = formString(form, "templateId");
|
||||
if (!templateId.ok) return templateId;
|
||||
if (templateId.value && templateId.value.length > 100) {
|
||||
return { ok: false, error: "templateId ยาวเกิน 100 ตัวอักษร" };
|
||||
}
|
||||
|
||||
const message = formString(form, "message");
|
||||
if (!message.ok) return message;
|
||||
if (message.value && message.value.length > DOCVER_MESSAGE_MAX) {
|
||||
|
|
@ -238,11 +211,8 @@ export function parseVersionUploadForm(form: FormData):
|
|||
return {
|
||||
ok: true,
|
||||
form: {
|
||||
userId: userId.value,
|
||||
userName: userName.value,
|
||||
clientVersionId: clientVersionId.value,
|
||||
message: message.value,
|
||||
templateId: templateId.value,
|
||||
fileName: sanitizeDisplayFileName(rawFileName.value),
|
||||
fields,
|
||||
},
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue