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
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:
parent
c57a75e36a
commit
5121970f31
26 changed files with 625 additions and 523 deletions
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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/…")
|
||||
|
|
|
|||
|
|
@ -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 ดิบ
|
||||
//
|
||||
|
|
|
|||
187
lib/openapi.ts
187
lib/openapi.ts
|
|
@ -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 ยังออกให้ หน้าแรกแสดงสาเหตุ",
|
||||
},
|
||||
},
|
||||
},
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue