refactor(auth): consolidate auth under /api/auth/* endpoints
Some checks failed
Deploy eCourt Note Web to Server / Validate Database Schema (Drizzle) (push) Successful in 3s
Deploy eCourt Note Web to Server / Build Web Docker Image (push) Failing after 30s
Deploy eCourt Note Web to Server / Deploy eCourt Note Web to Server (push) Has been skipped
Deploy eCourt Note Web to Server / Notify Deployment Status (push) Failing after 1s

- ยุบ route auth ที่เคย copy กันจน drift (flow N11 หายจาก /api/auth/session)
  เหลือโค้ด inline สองเส้นเดียว: POST /api/auth/token (ทางเข้า 3: x-api-key
  docver / x-api-key purpose:"login" mint ลิงก์ note / ticket ของ add-in)
  + POST /api/auth/session (แลก credential + register N8 + N11 fid-only)
- POST /api/documents/token คงไว้เป็น alias re-export (กลุ่ม docver/e-Court
  เดิม — contract เดิมเป๊ะ, มี test toBe กัน copy แยกชุดกลับมา)
- ถอดเส้นฝั่ง note ทั้งสอบ: /api/note/auth/token + /api/note/auth/session
  (e-Court ต้องย้ายมาทางเข้ารวม — แจ้งทีม e-Court ก่อน deploy)
- utils/auth-store.ts: primitives กลางฝั่ง client (jwtExp/isExpired/
  localStorage store + memory fallback) — lib/note-session ใช้ร่วม
  (expMs decode จาก JWT payload = นาฬิกา server) และวางไว้ให้ wordaddin
  ย้ายมาใช้รอบหน้าได้ (รอบนี้ไม่แต้ wordaddin ตามข้อตกลง)
- นโยบาย token หมดอายุกลาง session: PDFViewer reportActionError จับ
  auth-error ของ Server Action ทุกจุด → ล้าง session แล้วพาไป
  /note?sso=expired (จบที่ gate เดียวกับ bootstrap 401)
- openapi: ทางเข้า purpose:"login" + schema LoginTokenRequest + ลบ path
  เส้นที่ถอด + test กันเส้นตายหลุดค้างใน spec
- เอกสาร sync: PLAN-SSO §0.6, CLAUDE.md, PLAN-STAGED, .env.example, /note/dev
- tests: ย้าย/เพิ่มชุด auth (50 เคสกลุ่ม auth ผ่าน, ทั้งชุด 444 ผ่าน) +
  e2e ครบทั้ง API (mint/alias/ticket/404 เส้นเก่า/N11/D4) และ browser

Co-Authored-By: Claude Code <noreply@anthropic.com>
This commit is contained in:
Your Name 2026-11-09 14:30:29 +07:00
parent c57a75e36a
commit 5121970f31
26 changed files with 625 additions and 523 deletions

View file

@ -1,7 +1,8 @@
// PLAN-SSO.md §4 — ส่วนกลางของ SSO ของแอป note (ฉบับ token 2026-10-30 ตามคำขอ
// ผู้ใช้ "note ใช้แบบ add-in" — เลิก cookie ของรอบ D15/D16): ลิงก์ e-Court พา
// browser มา `/note` พร้อม credential (`t` หรือ `ticket_data`+`HmacSha256`) →
// client แลกเป็น access_token ที่ POST /api/note/auth/session → เก็บใน
// client แลกเป็น access_token ที่ POST /api/auth/session (ทางเข้ารวม 2026-11-09
// ตาม PLAN-SSO.md §0.6 — เส้นเดิม /api/note/auth/session ถอดแล้ว) → เก็บใน
// localStorage ("note-auth") แล้วแนบ Authorization: Bearer กับทุก API และส่ง
// token เข้าทุก Server Action (identity แกะฝั่ง server — ห้ามเชื่อ userId จาก
// client ตรง ๆ)
@ -80,7 +81,7 @@ export function parseSsoLinkParams(url: URL): SsoLinkParams {
}
// ---------------------------------------------------------------------------
// body ของ POST /api/note/auth/session (แลก credential → access_token)
// body ของ POST /api/auth/session (แลก credential → access_token — เดิม /api/note/auth/session)
// ---------------------------------------------------------------------------
const SESSION_EXCHANGE_LIMITS = SSO_PARAM_LIMITS;
@ -154,7 +155,8 @@ export function parseSessionExchangeBody(raw: unknown): SessionExchangeBody {
}
// ---------------------------------------------------------------------------
// body ของ POST /api/note/auth/token (เส้น mint login-token — D1/D5)
// body ของทางเข้า mint login-token (ทาง purpose:"login" ของ POST /api/auth/token —
// เส้นเดิม POST /api/note/auth/token ถอดแล้ว · D1/D5, PLAN-SSO.md §0.6)
// ---------------------------------------------------------------------------
/** ttl_seconds ทางเลือก — ช่วง 60–600 วินาที default 300 (credential ใน URL

View file

@ -1,11 +1,14 @@
// session ของแอป note ฝั่ง client (ฉบับ token 2026-10-30 ตามคำขอผู้ใช้ "note ใช้
// แบบ add-in" — pattern เดียวกับ wordaddin/services/authService): access_token
// + identity เก็บใน localStorage ("note-auth") หลังแลกจาก POST /api/note/auth/
// session · การเรียก API ของแอปผ่าน noteAuthFetch แนบ Authorization: Bearer
// อัตโนมัติ · token หมดอายุแล้วถือว่าไม่มี session (ล้างทิ้ง) — localStorage อาจ
// โยน (private mode/quota) ทุกจุดจึง try/catch
// + identity เก็บใน localStorage ("note-auth") หลังแลกจาก POST /api/auth/session
// (2026-11-09 — ย้ายมาเส้นสั้น, ทางเข้ารวมของระบบ) · การเรียก API ของแอปผ่าน
// noteAuthFetch แนบ Authorization: Bearer อัตโนมัติ · token หมดอายุแล้วถือว่า
// ไม่มี session (ล้างทิ้ง — token ที่ตายแล้วไม่มีทางฟื้น browser ไม่มี ticket
// ให้ renew ต้องกลับ e-Court กดลิงก์ใหม่) · storage โยน (private mode/quota) มี
// memory fallback ผ่าน utils/auth-store
//
// client-only — ห้าม import จาก server component/module ฝั่ง server
import { createAuthStore, isExpired, jwtExp } from "@/utils/auth-store";
import { withBasePath } from "@/utils/base-path";
export type NoteSession = {
@ -16,12 +19,34 @@ export type NoteSession = {
expMs: number;
};
const STORAGE_KEY = "note-auth";
const store = createAuthStore("note-auth");
/** message sentinel ของ requireUser (app/actions/annotations.ts) — Server Action
* throw ข้าม network เหลือ message อย่างเดียว จึงตรวจด้วย prefix ของข้อความ */
const AUTH_EXPIRED_SENTINEL = "session หมดอายุหรือไม่ถูกต้อง";
/** action/API ล้มเพราะ token หมดอายุ/ไม่ผ่าน (เทียบ message ของ requireUser) —
* caller (PDFViewer) จับได้ = ล้าง session แล้วพาไปหน้า gate ไม่ปล่อย error ค้าง */
export function isNoteAuthExpiredError(err: unknown): boolean {
return (
err instanceof Error && err.message.startsWith(AUTH_EXPIRED_SENTINEL)
);
}
/** exp (มิลลิวินาที) ของ session ใหม่ — decode จาก JWT payload (นาฬิกา server
* แม่นกว่า expires_in ที่คำนวณจากนาฬิกา client) · fallback expires_in เมื่อ decode
* ไม่ได้ · expires_in ผิดปกติ (undefined/Infinity — NaN จะทำ isExpired เป็น false
* เสมอจน session ไม่ตายตามอายุ) = คืน 0 ให้ถือว่าหมดอายุทันทีแทนค่าขยะ */
export function noteExpMs(token: string, expires_in: number): number {
const exp = jwtExp(token);
if (exp !== undefined) return exp * 1000;
return Number.isFinite(expires_in) ? Date.now() + expires_in * 1000 : 0;
}
export function readNoteSession(): NoteSession | null {
const raw = store.readRaw();
if (raw === null) return null;
try {
const raw = window.localStorage.getItem(STORAGE_KEY);
if (raw === null) return null;
const parsed = JSON.parse(raw) as Partial<NoteSession> | null;
if (
parsed === null ||
@ -33,11 +58,11 @@ export function readNoteSession(): NoteSession | null {
typeof parsed.username !== "string" ||
typeof parsed.expMs !== "number"
) {
window.localStorage.removeItem(STORAGE_KEY);
store.remove();
return null;
}
if (parsed.expMs <= Date.now()) {
window.localStorage.removeItem(STORAGE_KEY);
if (isExpired(Math.floor(parsed.expMs / 1000))) {
store.remove();
return null;
}
return parsed as NoteSession;
@ -47,19 +72,11 @@ export function readNoteSession(): NoteSession | null {
}
export function saveNoteSession(session: NoteSession): void {
try {
window.localStorage.setItem(STORAGE_KEY, JSON.stringify(session));
} catch {
// private mode/quota — เงียบไว้: ผู้ใช้เข้าผ่านลิงก์ e-Court ใหม่ได้เสมอ
}
store.writeRaw(JSON.stringify(session));
}
export function clearNoteSession(): void {
try {
window.localStorage.removeItem(STORAGE_KEY);
} catch {
// ignore
}
store.remove();
}
/** fetch ของแอปพร้อม Bearer — path เป็น route API ของแอป ("/api/note/…")

View file

@ -1,6 +1,7 @@
// session ของแอป note ฝั่ง server (ฉบับ token 2026-10-30 ตามคำขอผู้ใช้ "note ใช้
// แบบ add-in") — แกะ identity จาก access_token (purpose="login" ออกที่
// POST /api/note/auth/session อายุ NOTE_SESSION_TTL) ให้ Server Actions และ API
// POST /api/auth/session — เส้นเดิม /api/note/auth/session ถอดแล้ว 2026-11-09
// อายุ NOTE_SESSION_TTL) ให้ Server Actions และ API
// ของแอปใช้ร่วม: token ผิด/หมดอายุ/secret ไม่ตั้ง = null เสมอ (fail closed)
// · ห้าม log ค่า token ดิบ
//

View file

@ -1,7 +1,9 @@
// OpenAPI 3.0 spec ของ API ทดสอบทั้งหมดของ judes-note — เส้นเดียวกับที่ Swagger UI
// แสดงผล: กลุ่ม template (DOCX Content Control) + N10 external API + docver
// (จัด version เอกสาร) + auth (ทางเข้า path สั้น /api/auth/* — alias ของ
// /api/documents/token กับ /api/note/auth/session เพิ่ม 2026-11-05)
// (จัด version เอกสาร) + auth (ทางเข้ารวม /api/auth/* — 2026-11-09 รอบสอง:
// /api/auth/token รวมทางเข้า mint login-token ด้วย purpose:"login" ·
// /api/note/auth/token กับ /api/note/auth/session ถอดแล้ว · /api/documents/token
// ของกลุ่ม docver คงไว้เป็น alias ของ /api/auth/token)
// route handler GET /api/swagger (app/api/swagger/route.ts) เป็นคนเดียวที่ใช้
// — inline เป็น JSON ลงใน HTML ตอน respond (ไม่มี endpoint spec แยก)
//
@ -64,11 +66,15 @@ export const openApiSpec = {
{
name: "auth",
description:
"ทางเข้า auth แบบ **path สั้น** `/api/auth/*` (เพิ่ม 2026-11-05) — endpoint " +
"ก้อนเดียวกับของเดิมคนละ URL เลือกใช้ทางใดทางหนึ่งได้:\n" +
"- `POST /api/auth/token` = `POST /api/documents/token` — ออก access_token ของกลุ่ม documents (docver)\n" +
"- `POST /api/auth/session` = `POST /api/note/auth/session` — แลก credential ของลิงก์ e-Court " +
"เป็น session token ของแอป note",
"**ทางเข้า auth รวม** `/api/auth/*` (2026-11-09 รอบสอง — เส้นของฝั่ง note " +
"กลุ่ม `note/auth/*` ทั้งหมดถอดแล้ว ผู้เรียกเก่าต้องย้ายมาที่นี่):\n" +
"- `POST /api/auth/token` — ออก access_token: x-api-key + body เดิม = " +
"docver-token 7 วัน · x-api-key + `purpose:\"login\"` = login-token อายุสั้น " +
"(mint ลิงก์ `/note?t=`) · ticket ของ add-in\n" +
"- `POST /api/auth/session` — แลก credential ของลิงก์ e-Court เป็น session token " +
"ของแอป note\n" +
"- `POST /api/documents/token` — alias ของกลุ่ม docver (e-Court เดิม คงไว้ — " +
"function เดียวกับ `/api/auth/token`)",
},
],
paths: {
@ -630,74 +636,6 @@ export const openApiSpec = {
},
},
},
"/api/note/auth/token": {
post: {
tags: ["Judge's Note"],
summary: "mint login-token สำหรับลิงก์ SSO เข้าแอป note (PLAN-SSO.md)",
description:
"ออก `access_token` ชนิด `purpose=login` ไว้ฝังเป็น param เดียว `t=` ของลิงก์ " +
"`/note?fileUrl=…&t=<access_token>` ที่ให้ผู้ใช้คลิกจากหน้า e-Court " +
"(ทางเข้า session ทางเดียวของแอป note — หน้าแรกแลก credential เป็น session " +
"token ที่ POST /api/note/auth/session แบบ add-in)\n\n" +
"- token ใช้ได้กับ**การเข้าแอป note เท่านั้น** — เอาไปยิง `/api/documents/*` " +
"ของกลุ่ม docver ไม่ได้ (401)\n" +
"- แนะนำให้ mint แบบ real-time ตอน render หน้า/ปุ่มให้ user ปัจจุบันเท่านั้น — " +
"**ห้ามเก็บ token ลง DB / ฝังอีเมล / แชท** (อายุสั้นตาม ttl_seconds)\n" +
"- `userid`/`username` = ค่าเดียวกับ `u_userid`/`u_userlogin` ของ e-Court " +
"(สิทธิ์เจ้าของ annotation ของแอป note ผูกกับ userid — ส่งผิดคน = ข้อมูลผูกคนผิด)\n" +
"- `fileUrl` ของลิงก์ปลายทางต้องผ่าน allowlist ของ N8 (admincourt.go.th + `EXTERNAL_FILE_HOSTS`)",
security: [{ ApiKeyAuth: [] }],
requestBody: {
required: true,
content: {
"application/json": {
schema: {
type: "object",
required: ["userid", "username", "fid"],
properties: {
userid: { type: "string", description: "u_userid ของ e-Court (1–100 ตัวอักษร)" },
username: { type: "string", description: "u_userlogin ของ e-Court (1–200 ตัวอักษร)" },
fid: { type: "string", description: "fileId ของ e-Court — ใช้เป็น fileId ของ N8 เสมอ (1–200 ตัวอักษร)" },
ttl_seconds: {
type: "integer",
minimum: 60,
maximum: 600,
default: 300,
description: "อายุ token ทางเลือกช่วง 60–600 วินาที (credential อยู่ใน URL ต้องอายุสั้น)",
},
},
},
examples: {
"mint ให้ผู้ใช้ oom เปิดไฟล์ F123": {
value: { userid: "oom", username: "สรวิชญ์", fid: "F123", ttl_seconds: 300 },
},
},
},
},
},
responses: {
200: {
description: "login-token พร้อมฝังเป็น param t ของลิงก์ e-court",
content: {
"application/json": {
schema: { $ref: "#/components/schemas/TokenResponse" },
},
},
},
400: { $ref: "#/components/responses/BadRequest" },
401: {
description:
"ไม่ได้ส่ง header x-api-key หรือค่าไม่ถูกต้อง (env DOCVER_API_KEY — ข้อความเดียวกันทุกสาเหตุ)",
content: {
"application/json": {
schema: { $ref: "#/components/schemas/Error" },
},
},
},
500: { $ref: "#/components/responses/ServerError" },
},
},
},
"/api/note/files/stage": {
post: {
tags: ["Judge's Note"],
@ -708,8 +646,8 @@ export const openApiSpec = {
"(TTL 8 ชม. นับจากการเข้าใช้ล่าสุด — ห้ามเก็บไฟล์ถาวรทุกรูปแบบตามโจทย์) " +
"annotation/bookmark เก็บถาวรใน DB ตามระบบเดิม\n\n" +
"ได้ `{fileId}` แล้วเอา `fid` **เดิม** ไป mint login token ที่ " +
"`POST /api/note/auth/token` → ลิงก์ `/note?t=<access_token>` (ไม่ต้องมี " +
"`fileUrl`) ให้ผู้ใช้คลิกตามเส้นทางเดิม\n\n" +
"`POST /api/auth/token` แบบ `purpose:\"login\"` → ลิงก์ `/note?t=<access_token>` " +
"(ไม่ต้องมี `fileUrl`) ให้ผู้ใช้คลิกตามเส้นทางเดิม\n\n" +
"- auth: header `x-api-key` (env `DOCVER_API_KEY` — ตัวเดียวกับทางเข้า mint)\n" +
"- `data`: **base64 มาตรฐานต่อเนื่อง** — ห้าม whitespace/line break และไม่รับ " +
"base64url (`-`/`_`)\n" +
@ -748,7 +686,7 @@ export const openApiSpec = {
description:
"รหัสกลุ่มเอกสาร N9 (ทางเลือก) — ส่งมา = ย้ายกลุ่มตามค่าล่าสุด ไม่ส่ง = คงเดิม · " +
"⚠️ ของเอกสาร staging รับทางเส้นนี้**ทางเดียว** — typeId ที่แนบมากับ body ของ " +
"`POST /api/note/auth/session` (ตอนแลกลิงก์ fid-only) ไม่ถูกใช้กับกลุ่มของไฟล์",
"`POST /api/auth/session` (ตอนแลกลิงก์ fid-only) ไม่ถูกใช้กับกลุ่มของไฟล์",
},
title: {
type: "string",
@ -841,18 +779,25 @@ export const openApiSpec = {
"/api/auth/token": {
post: {
tags: ["auth"],
summary: "ขอ access_token ของกลุ่ม documents (docver) — path สั้นของ /api/documents/token",
summary: "ขอ access_token — ทางเข้ารวมของทั้งระบบ (docver / login ลิงก์ note / ticket add-in)",
description:
"ทางเข้า **path สั้น** ของ `POST /api/documents/token` — endpoint ก้อนเดียวกัน " +
"พฤติกรรมเหมือนกันทุกประการ (เพิ่ม 2026-11-05)\n\n" +
"ออก `access_token` (JWT HS256) ไว้เรียก endpoint อื่นของกลุ่ม documents — " +
"token ฝัง claims `{userid, username, fid, template}` อายุตาม env " +
"`DOCVER_TOKEN_TTL_SECONDS` (default 604800 วินาที = 7 วัน — token อายุยาวแทน" +
"ระบบ refresh/renew ของ add-in)\n\n" +
"**ทางเข้า 2 แบบ** — เลือกตามว่าใครเป็นคนเรียก (ดูตัวอย่างใน Request body ได้ทั้งคู่):\n\n" +
"**ทางเข้า token รวมของทั้งระบบ** (2026-11-09 รอบสอง — ถอดเส้น mint เดิมของฝั่ง note " +
"ทาง mint login-token จึงรวมอยู่ที่นี่ทางเดียวผ่าน `purpose:\"login\"` · " +
"`/api/documents/token` ของกลุ่ม docver เป็น alias ของเส้นนี้ — function เดียวกัน)\n\n" +
"ออก `access_token` (JWT HS256) — token ฝัง claims `{userid, username, fid, " +
"template}` อายุตามชนิดของ token (D4 แยกสิทธิ์ด้วย claim `purpose`)\n\n" +
"**ทางเข้า 3 แบบ** — เลือกตามว่าใครเป็นคนเรียก (ดูตัวอย่างใน Request body):\n\n" +
"1. **ระบบภายนอก (server-to-server)** — แนบ header `x-api-key` (ค่าจาก env " +
"`DOCVER_API_KEY`) แล้วส่ง claims ตรง ๆ ใน body ตาม schema `TokenRequest`\n" +
"2. **add-in ใน Word** — ไม่ต้องมี `x-api-key` ส่ง body `{ticket_data, HmacSha256}` " +
"`DOCVER_API_KEY`) แล้วส่ง claims ตรง ๆ ใน body ตาม schema `TokenRequest` → " +
"docver-token อายุตาม env `DOCVER_TOKEN_TTL_SECONDS` (default 604800 วินาที = " +
"7 วัน — token อายุยาวแทนระบบ refresh/renew ของ add-in) ใช้กับ endpoint กลุ่ม " +
"documents\n" +
"2. **e-Court mint ลิงก์ SSO ของแอป note** — `x-api-key` เช่นเดียวกัน + body ตาม " +
'schema `LoginTokenRequest` (บอกด้วย `purpose:"login"` + `ttl_seconds` 60–600 ' +
"default 300) → login-token อายุสั้นไว้ฝังเป็น param `t=` ของลิงก์ `/note?t=…` " +
"(credential อยู่ใน URL ต้องอายุสั้น — D5) · ไม่ส่ง `purpose` = ทางเข้า 1 ตามเดิม " +
"พอดี\n" +
"3. **add-in ใน Word** — ไม่ต้องมี `x-api-key` ส่ง body `{ticket_data, HmacSha256}` " +
"ตาม schema `TicketTokenRequest` (คู่ค่าที่ e-Court stamp ไว้ใน custom properties " +
"ของไฟล์ .docx) — server ตรวจลายเซ็น HMAC-SHA256 ด้วย env `ADDIN_TICKET_SECRET` " +
'(ไม่ตั้ง → env `SECRET` → `"super_secret"`) แล้วออก token จาก claims ที่อยู่ในตัว ticket เอง ' +
@ -868,6 +813,7 @@ export const openApiSpec = {
schema: {
oneOf: [
{ $ref: "#/components/schemas/TokenRequest" },
{ $ref: "#/components/schemas/LoginTokenRequest" },
{ $ref: "#/components/schemas/TicketTokenRequest" },
],
},
@ -883,6 +829,11 @@ export const openApiSpec = {
"server-to-server ขั้นต่ำ (template เป็นทางเลือก)": {
value: { userid: "123", username: "Sorawit Bholsithi", fid: "3" },
},
"mint login-token ลิงก์ SSO ของแอป note (purpose=login)": {
value: { userid: "oom", username: "สรวิชญ์", fid: "F123", ttl_seconds: 300, purpose: "login" },
description:
"ออก login-token อายุสั้น — ฝังเป็น t= ของลิงก์ /note?fileUrl=…&t=…",
},
"add-in ด้วย ticket จากไฟล์ (ไม่ต้องมี x-api-key)": {
value: {
ticket_data:
@ -934,20 +885,25 @@ export const openApiSpec = {
tags: ["auth"],
summary: "แลก credential ของลิงก์ e-Court เป็น session token ของแอป note (SSO)",
description:
"ทางเข้า **path สั้น** ของ `POST /api/note/auth/session` — endpoint ก้อนเดียวกัน " +
"พฤติกรรมเหมือนกันทุกประการ (เพิ่ม 2026-11-05) · flow SSO ของแอป note " +
"**ทางเข้าหลัก (และเดียว) ของแอป note** (2026-11-09 รอบสอง — เส้น session เดิมของฝั่ง note " +
"ถอดแล้ว ผู้เรียกเก่าต้องย้ายมาเส้นนี้) · flow SSO ของแอป note " +
"(PLAN-SSO.md §3.2 ฉบับ token): ลิงก์ e-Court พา browser มาหน้า `/note` พร้อม " +
"credential → client เรียกเส้นนี้แลกเป็น access_token เก็บ localStorage " +
'("note-auth") แล้วแนบ `Authorization: Bearer` กับทุก API/Server Action ของแอป\n\n' +
"- ส่ง credential **ทางเดียว**: `t` (login-token จาก `POST /api/note/auth/token`) " +
"หรือ `ticket_data`+`HmacSha256` (คู่ค่าชุดเดียวกับที่ e-Court stamp ใน custom " +
"properties ของไฟล์ .docx ของ add-in) — สองชุดพร้อมกัน = 400 (D12)\n" +
"- ส่ง credential **ทางเดียว**: `t` (login-token จาก `POST /api/auth/token` " +
'แบบ `purpose:"login"`) หรือ `ticket_data`+`HmacSha256` (คู่ค่าชุดเดียวกับที่ e-Court ' +
"stamp ใน custom properties ของไฟล์ .docx ของ add-in) — สองชุดพร้อมกัน = 400 (D12)\n" +
"- token ที่ได้เป็นชนิด `purpose=login` อายุ 8 ชม. (28800 วินาที) — ใช้ได้กับแอป " +
"note เท่านั้น เอาไปยิง `/api/documents/*` ของกลุ่ม docver ไม่ได้ (401)\n" +
"note เท่านั้น เอาไปยิง `/api/documents/*` ของกลุ่ม docver ไม่ได้ (401) · หมด " +
"กลาง session แล้ว client ล้างทิ้งแล้วพากลับ e-Court (token ที่ตายแล้วไม่มีทางฟื้น)\n" +
"- ส่ง `fileUrl` มาด้วย = ลงทะเบียนเอกสาร N8 แบบ trusted ฝั่ง server ทันที " +
"(fid จาก credential ที่ verify แล้ว) — ล้มก็ยังได้ token พร้อม `fileError` " +
"ให้หน้าแรกแสดงสาเหตุ · `fileUrl` ต้องผ่าน allowlist ของ N8 " +
"(admincourt.go.th + env `EXTERNAL_FILE_HOSTS`) หรือเป็น path `/samples/…`\n" +
"- **ไม่ส่ง `fileUrl` (ลิงก์ fid-only — เอกสาร staging ของ N11)**: ค้นเอกสารของ " +
"`fid` ตั้งแต่ตอนแลก token — staging ที่ยังมีเนื้อ (RAM) หรือเอกสาร N8 ที่รู้จัก " +
"แล้ว = ได้ `fileId` เข้า viewer ได้เลย · ไม่เจอ/พ้นช่วงจัดเตรียม = `fileError` " +
"ให้กลับ e-Court กดลิงก์ใหม่ (ผู้ใช้ยังได้ token ตามปกติทุกกรณี)\n" +
"- **ทาง ticket ใน production**: secret ต้องตั้งจริง (env `ADDIN_TICKET_SECRET` " +
'→ `SECRET`) — ค่า default `"super_secret"` ที่รู้กันทั้งระบบถูกปฏิเสธ (500)',
requestBody: {
@ -1018,6 +974,9 @@ export const openApiSpec = {
tags: ["documents"],
summary: "ขอ access_token ของกลุ่ม documents (docver)",
description:
"**alias ของ `POST /api/auth/token`** — function เดียวกันเป๊ะ (re-export · " +
"2026-11-09: คงไว้ตามคำขอผู้ใช้เพราะเป็นทางเข้าของกลุ่ม docver/e-Court เดิม; " +
"contract เดิมคงทั้งหมด — body ไม่ส่ง `purpose` = docver-token)\n\n" +
"ออก `access_token` (JWT HS256) ไว้เรียก endpoint อื่นของกลุ่ม documents — " +
"token ฝัง claims `{userid, username, fid, template}` อายุตาม env " +
"`DOCVER_TOKEN_TTL_SECONDS` (default 604800 วินาที = 7 วัน — token อายุยาวแทน" +
@ -1464,6 +1423,36 @@ export const openApiSpec = {
},
},
},
LoginTokenRequest: {
type: "object",
description:
"ทางเข้า mint login-token ของแอป note (x-api-key เช่นเดียวกับ TokenRequest — " +
"บอกความต้องการด้วย purpose:\"login\") · ไม่ส่ง purpose = ทางเข้า docver ของ " +
"TokenRequest ข้อ 1 · key อื่นนอกจาก 5 ตัวนี้ = 400 · token ที่ได้เป็น " +
"purpose=login ใช้ได้กับการเข้าแอป note เท่านั้น (D4)",
required: ["userid", "username", "fid", "purpose"],
properties: {
userid: { type: "string", description: "u_userid ของ e-Court (1–100 ตัวอักษร)" },
username: { type: "string", description: "u_userlogin ของ e-Court (1–200 ตัวอักษร)" },
fid: {
type: "string",
description:
"fileId ของ e-Court — ใช้เป็น fileId ของ N8 เสมอ (1–200 ตัวอักษร)",
},
ttl_seconds: {
type: "integer",
minimum: 60,
maximum: 600,
default: 300,
description: "อายุ token ทางเลือกช่วง 60–600 วินาที (credential อยู่ใน URL ต้องอายุสั้น)",
},
purpose: {
type: "string",
enum: ["login"],
description: 'ค่าเดียวที่รับ — "login" (ค่าอื่น = 400)',
},
},
},
TokenResponse: {
type: "object",
description: "access_token ของกลุ่ม documents (JWT HS256)",
@ -1486,7 +1475,7 @@ export const openApiSpec = {
t: {
type: "string",
description:
"login-token (JWT purpose=login) จาก POST /api/note/auth/token — ≤ 8192 ตัวอักษร",
'login-token (JWT purpose=login) จาก POST /api/auth/token แบบ purpose:"login" — ≤ 8192 ตัวอักษร',
},
ticket_data: {
type: "string",
@ -1532,14 +1521,14 @@ export const openApiSpec = {
type: "string",
format: "uuid",
description:
"มีเมื่อส่ง fileUrl และลงทะเบียนสำเร็จ — id ของเอกสารในระบบนี้ " +
"(ใช้เข้าหน้าอ่าน /note/annotate/<fileId>)",
"มีเมื่อส่ง fileUrl และลงทะเบียนสำเร็จ หรือ (ลิงก์ fid-only ของ N11) เจอเอกสารของ " +
"fid ที่พร้อมใช้ — id ของเอกสารในระบบนี้ (ใช้เข้าหน้าอ่าน /note/annotate/<fileId>)",
},
fileError: {
type: "string",
description:
"มีเมื่อลงทะเบียน fileUrl ไม่สำเร็จ (ไม่ผ่าน allowlist ฯลฯ) — token ยังออกให้ " +
"หน้าแรกแสดงสาเหตุ",
"มีเมื่อลงทะเบียน fileUrl ไม่สำเร็จ (ไม่ผ่าน allowlist ฯลฯ) หรือลิงก์ fid-only " +
"ไม่เจอเอกสาร/พ้นช่วงจัดเตรียม — token ยังออกให้ หน้าแรกแสดงสาเหตุ",
},
},
},