Refactor authentication flow and ticket handling in add-in
All checks were successful
Deploy eCourt Note Web to Server / Validate Database Schema (Drizzle) (push) Successful in 4s
Deploy eCourt Note Web to Server / Build Web Docker Image (push) Successful in 1m23s
Deploy eCourt Note Web to Server / Deploy eCourt Note Web to Server (push) Successful in 5s
Deploy eCourt Note Web to Server / Notify Deployment Status (push) Successful in 0s
All checks were successful
Deploy eCourt Note Web to Server / Validate Database Schema (Drizzle) (push) Successful in 4s
Deploy eCourt Note Web to Server / Build Web Docker Image (push) Successful in 1m23s
Deploy eCourt Note Web to Server / Deploy eCourt Note Web to Server (push) Successful in 5s
Deploy eCourt Note Web to Server / Notify Deployment Status (push) Successful in 0s
- Removed client-side HMAC verification; server now handles all HMAC checks. - Updated ticket processing logic to improve UX and security. - Adjusted API request handling to return discriminated results based on HTTP status. - Enhanced error messages for better clarity on authentication failures. - Added unit tests for the new token endpoint to ensure compliance with updated contract. - Cleaned up unused code and comments related to previous HMAC handling.
This commit is contained in:
parent
2e951f99f4
commit
c0833c00f7
24 changed files with 858 additions and 460 deletions
|
|
@ -11,10 +11,6 @@ export const config = {
|
|||
apiBaseUrl: process.env.NEXT_PUBLIC_ADDIN_API_BASE_URL ?? "",
|
||||
/** ชื่อผู้ใช้ของ mock auth — แสดงหน้า header + เป็น createdByUserId ของ version ที่ mock สร้าง */
|
||||
mockUsername: process.env.NEXT_PUBLIC_ADDIN_MOCK_USERNAME ?? "tester01",
|
||||
/**
|
||||
* secret ตรวจ HMAC ของ ticket ฝั่ง client (UX gate — ตัดเคสไฟล์ถูกแก้ก่อนยิง network;
|
||||
* ความมั่นคงจริงอยู่ที่ server ตรวจอีกชั้น เพราะค่าใน bundle สกัดได้) — ต้องตรงกับ
|
||||
* ตัว stamp ของ e-Court และ env ADDIN_TICKET_SECRET/SECRET ฝั่ง server (PLAN-ADDIN-AUTH.md §7)
|
||||
*/
|
||||
ticketSecret: process.env.NEXT_PUBLIC_ADDIN_TICKET_SECRET ?? "super_secret",
|
||||
// (ไม่มี secret ฝั่ง client อีกต่อไป — การตรวจ HMAC ของ ticket เป็นของ server อย่างเดียว
|
||||
// ตาม PLAN-SSO.md D13, ถอด NEXT_PUBLIC_ADDIN_TICKET_SECRET ออก 2026-11-04)
|
||||
};
|
||||
|
|
|
|||
|
|
@ -64,9 +64,9 @@ async function doFetch(
|
|||
|
||||
/**
|
||||
* request พร้อม Bearer — โดน 401 → ขอ token ใหม่จาก ticket ของเอกสารปัจจุบัน
|
||||
* (renewTokenFromTicket — ticket ยังไม่หมดอายุเท่านั้น) แล้ว retry 1 ครั้ง
|
||||
* ยัง 401 อีก / renew ไม่ได้ = AuthRequiredError (UI พาไป no-access ให้ขอไฟล์
|
||||
* ใหม่จาก e-Court) · Response ที่ได้คืนไปให้ caller เช็ค ok เอง
|
||||
* (renewTokenFromTicket — server เป็นคนตัดสิน: 200 = ออกให้ใหม่ · 403 = ticket
|
||||
* หมดอายุ → false) แล้ว retry 1 ครั้ง ยัง 401 อีก / renew ไม่ได้ = AuthRequiredError
|
||||
* (UI พาไป no-access ให้ขอไฟล์ใหม่จาก e-Court) · Response ที่ได้คืนไปให้ caller เช็ค ok เอง
|
||||
*/
|
||||
export async function apiFetch(path: string, options: ApiRequestOptions = {}): Promise<Response> {
|
||||
const accessToken = await authService.getAccessToken();
|
||||
|
|
|
|||
|
|
@ -1,21 +1,21 @@
|
|||
/**
|
||||
* Auth ของ add-in — e-Court ticket ทางเข้าเดียว (PLAN-ADDIN-AUTH.md §5 ตาม
|
||||
* docs/requirement/addin-workflow.md)
|
||||
* docs/requirement/addin-workflow.md — ปรับ flow 2026-11-04)
|
||||
*
|
||||
* ไม่มี login ใน add-in: ไฟล์ที่ e-Court stamp custom properties `ticket_data`
|
||||
* + `HmacSha256` มา (ผ่านกลุ่ม template) = ผ่านเข้าได้ — signInWithTicket ตรวจ
|
||||
* HMAC → session ใน localStorage ตาม uid/fid → ขอ access_token จาก
|
||||
* /api/documents/token เก็บตาม fid ("คีย์คือ fid") · exp ของ ticket ไม่ใช่
|
||||
* เกณฑ์ตัดสิทธิ์ฝั่ง client — server ออก token ให้แม้ ticket หมดอายุเมื่อ
|
||||
* เอกสารนี้เคยมี version โดย uid เดียวกัน (requirement ขั้น 7)
|
||||
* ฟอร์แม็ต/exp เป็น UX gate ฝั่ง client (ไม่มี secret ใน bundle อีกต่อไปตาม
|
||||
* PLAN-SSO.md D13 — การตรวจ HMAC เป็นของ server อย่างเดียว ยิง /api/documents/token
|
||||
* ทุกครั้งที่เปิดเอกสาร) → แยกทางตาม contract: 200 = ได้ JWT ใหม่ / 400 ฟอร์แม็ตพัง /
|
||||
* 401 ลายเซ็นไม่ถูก / 403 ลายเซ็นถูกแต่ ticket หมดอายุ (มี JWT ของ fid ใน session
|
||||
* ที่ยังไม่หมด = ใช้ต่อ ไม่มี = ขอไฟล์ใหม่จาก e-Court)
|
||||
*
|
||||
* token เก็บใน localStorage (ข้าม session ได้ — กลับมาเปิดเอกสารเดิมไม่ต้องขอใหม่
|
||||
* ตาม requirement ขั้น 7) ส่วน ticket ดิบของเอกสารปัจจุบันเก็บใน memory ไว้ renew
|
||||
* ตอน token หมดอายุกลาง session
|
||||
* token เก็บใน localStorage ตาม fid ("คีย์คือ fid") — อายุ 7 วัน แทนระบบ refresh
|
||||
* ส่วน ticket ดิบของเอกสารปัจจุบันเก็บใน memory ไว้ renew ตอนโดน 401 กลาง session
|
||||
*/
|
||||
|
||||
import { config } from "../config";
|
||||
import { isTicketExpired, parseDocverTicket } from "@/utils/docver-ticket";
|
||||
import { isTicketExpired, parseDocverTicket, type DocverTicket } from "@/utils/docver-ticket";
|
||||
import * as ticketService from "./ticketService";
|
||||
|
||||
/** สาเหตุที่เข้าไม่ได้ — UI แยกข้อความตาม reason (TaskpaneApp) */
|
||||
|
|
@ -46,13 +46,16 @@ export function isDemoMode(): boolean {
|
|||
}
|
||||
|
||||
/**
|
||||
* e-Court ticket — จุดเข้าเดียวของ auth ตอนเปิดเอกสาร (requirement ขั้น 1–7):
|
||||
* 1. ตรวจมี ticket_data + HmacSha256 ครบ
|
||||
* 2. parse + ตรวจ HMAC ฝั่ง client (UX gate)
|
||||
* 3. session ใน localStorage ตาม uid/fid — token ยังใช้ได้ = ผ่านเลย · ต้องขอ
|
||||
* token ใหม่ = ยิงขอเสมอแล้วให้ **server เป็นคนตัดสิน** (ticket หมดอายุแต่
|
||||
* เอกสารนี้เคยมี version โดย uid เดียวกัน = ออกให้ต่อ — requirement ขั้น 7
|
||||
* กลับมาทำเอกสารเดิมได้แม้ ticket หมดอายุ แม้ session หายไปจากเครื่อง)
|
||||
* e-Court ticket — จุดเข้าเดียวของ auth ตอนเปิดเอกสาร (flow 2026-11-04):
|
||||
* 1. ตรวจมี ticket_data + HmacSha256 ครบ (client) — ไม่ครบ = เตะเลย
|
||||
* 2. parse ฟอร์แม็ต (client UX gate) — พัง = เตะเลย
|
||||
* 3. pre-check: ticket หมดอายุ และ ไม่มี JWT ของ fid นี้ใน session (uid ตรงกัน)
|
||||
* = เตะเลย ไม่ยิง API (มีใน session = มีโอกาสเป็นไฟล์เก่าที่เคยแก้ → ไปให้
|
||||
* server ตัดสินที่ขั้นถัดไป)
|
||||
* 4. ที่เหลือยิง /api/documents/token **ทุกครั้ง** ให้ server ตรวจลายเซ็น (HMAC)
|
||||
* แล้วแยกทางตาม contract: 200 ได้ JWT ใหม่ (user ต่าง = เคลียร์ session ทั้งก้อน
|
||||
* สร้างใหม่) · 403 ลายเซ็นถูกแต่ ticket หมดอายุ — มี JWT ของ fid ที่ยังไม่หมด
|
||||
* ใน session ก็ใช้ต่อ · 400/401 ฟอร์แม็ต/ลายเซ็นพัง = เตะ (ไฟล์ถูกแก้)
|
||||
*/
|
||||
export async function signInWithTicket(input: {
|
||||
ticketData?: string;
|
||||
|
|
@ -74,14 +77,7 @@ export async function signInWithTicket(input: {
|
|||
return {
|
||||
ok: false,
|
||||
reason: "tampered",
|
||||
message: "ตรวจสอบ ticket ไม่ผ่าน (รูปแบบไม่ถูกต้อง) — ไฟล์ถูกแก้ไขหรือเสียหาย",
|
||||
};
|
||||
}
|
||||
if (!(await ticketService.verifyTicketHmacWeb(ticketData, ticketHmac, config.ticketSecret))) {
|
||||
return {
|
||||
ok: false,
|
||||
reason: "tampered",
|
||||
message: "ตรวจสอบลายเซ็น (HMAC) ของ ticket ไม่ผ่าน — ไฟล์ถูกแก้ไข",
|
||||
message: "รูปแบบ ticket ในไฟล์ไม่ถูกต้อง — ไฟล์ถูกแก้ไขหรือเสียหาย โปรดขอไฟล์ใหม่จาก e-Court",
|
||||
};
|
||||
}
|
||||
const ticket = parsed.ticket;
|
||||
|
|
@ -91,56 +87,68 @@ export async function signInWithTicket(input: {
|
|||
message: "ticket ของ e-Court หมดอายุ — โปรดขอไฟล์ใหม่จาก e-Court แล้วเปิดอีกครั้ง",
|
||||
};
|
||||
|
||||
// snapshot session ก่อนยิง — "มีใน session" = ยูสเซอร์ + ไฟล์ (uid+fid) ตรงกับ ticket
|
||||
const session = ticketService.loadSession();
|
||||
if (session === null || session.userId !== ticket.uid) {
|
||||
// ไม่มี session / user ใหม่ — ขอ token ใหม่เสมอ (แม้ ticket หมดอายุ: server
|
||||
// ออกให้ต่อเมื่อเอกสารนี้เคยทำงานโดย uid เดียวกัน ซึ่งช่วยเคส localStorage หาย
|
||||
// ของคนที่กลับมาทำเอกสารเดิมด้วย) แล้วเริ่ม session ใหม่ทั้งก้อน
|
||||
let entry: ticketService.SessionTokenEntry;
|
||||
try {
|
||||
entry = await ticketService.requestDocverToken(ticketData, ticketHmac);
|
||||
} catch {
|
||||
return isTicketExpired(ticket.exp)
|
||||
? expiredMessage
|
||||
: {
|
||||
ok: false,
|
||||
reason: "token-request-failed",
|
||||
message: "ขอ token จากระบบไม่สำเร็จ — ตรวจการเชื่อมต่อแล้วเปิดเอกสารอีกครั้ง",
|
||||
};
|
||||
}
|
||||
ticketService.clearSession();
|
||||
ticketService.saveSession({ userId: ticket.uid, userName: ticket.uname, tokens: { [ticket.fid]: entry } });
|
||||
} else {
|
||||
// คนเดิม — userName อัปเดตตามไฟล์ล่าสุด (ชื่อเปลี่ยนได้)
|
||||
session.userName = ticket.uname;
|
||||
let entry = session.tokens[ticket.fid] ?? null;
|
||||
if (entry !== null && ticketService.isTokenExpired(entry)) {
|
||||
// token หมดอายุ — ยิง renew เสมอแล้วให้ server ตัดสิน (ticket ยังไม่หมด =
|
||||
// ออกให้เสมอ · หมดแล้ว = ออกเฉพาะเอกสารนี้เคยทำงานโดย uid เดียวกัน)
|
||||
try {
|
||||
entry = await ticketService.requestDocverToken(ticketData, ticketHmac);
|
||||
session.tokens[ticket.fid] = entry;
|
||||
} catch {
|
||||
// ใช้ token เก่าต่อ — ถ้าเน่าจริง apiClient จะ 401 → renew-retry → no-access
|
||||
}
|
||||
}
|
||||
if (entry === null) {
|
||||
// เอกสารใหม่ของคนเดิม — ขอ token เก็บเพิ่ม (server ตัดสินเรื่อง ticket หมดอายุ)
|
||||
try {
|
||||
session.tokens[ticket.fid] = await ticketService.requestDocverToken(ticketData, ticketHmac);
|
||||
} catch {
|
||||
return isTicketExpired(ticket.exp)
|
||||
? expiredMessage
|
||||
: {
|
||||
ok: false,
|
||||
reason: "token-request-failed",
|
||||
message: "ขอ token จากระบบไม่สำเร็จ — ตรวจการเชื่อมต่อแล้วเปิดเอกสารอีกครั้ง",
|
||||
};
|
||||
}
|
||||
}
|
||||
ticketService.saveSession(session);
|
||||
const inSession =
|
||||
session !== null && session.userId === ticket.uid && session.tokens[ticket.fid] !== undefined;
|
||||
|
||||
// pre-check: หมดอายุ + ไม่มีใน session = เตะโดยไม่ยิง API (ลดการเรียกใช้ network)
|
||||
if (isTicketExpired(ticket.exp) && !inSession) {
|
||||
return expiredMessage;
|
||||
}
|
||||
|
||||
// ยิงทุกครั้งที่เปิดเอกสาร — signature ถูกตรวจที่ server เท่านั้น (ไม่มี secret ฝั่ง client)
|
||||
const result = await ticketService.requestDocverToken(ticketData, ticketHmac);
|
||||
if (!result.ok) {
|
||||
if (result.kind === "http" && result.status === 400) {
|
||||
return {
|
||||
ok: false,
|
||||
reason: "tampered",
|
||||
message: "รูปแบบ ticket ในไฟล์ไม่ถูกต้อง — ไฟล์ถูกแก้ไขหรือเสียหาย โปรดขอไฟล์ใหม่จาก e-Court",
|
||||
};
|
||||
}
|
||||
if (result.kind === "http" && result.status === 401) {
|
||||
return {
|
||||
ok: false,
|
||||
reason: "tampered",
|
||||
message: "ตรวจสอบลายเซ็น (HMAC) ของ ticket กับระบบไม่ผ่าน — ไฟล์ถูกแก้ไขหรือเสียหาย โปรดขอไฟล์ใหม่จาก e-Court",
|
||||
};
|
||||
}
|
||||
if (result.kind === "http" && result.status === 403) {
|
||||
// ลายเซ็นถูกแต่ ticket หมดอายุ — มี JWT ของ fid (uid ตรง) ที่ยังไม่หมด = ใช้ต่อ
|
||||
if (session !== null && session.userId === ticket.uid) {
|
||||
const entry = session.tokens[ticket.fid];
|
||||
if (entry !== undefined && !ticketService.isTokenExpired(entry)) {
|
||||
session.userName = ticket.uname; // refresh ชื่อตามไฟล์ล่าสุด
|
||||
ticketService.saveSession(session);
|
||||
return finishSignIn(ticket, ticketData, ticketHmac);
|
||||
}
|
||||
}
|
||||
return expiredMessage;
|
||||
}
|
||||
// network/malformed/5xx และอื่น ๆ — ไม่ใช่ความผิดของไฟล์ ลองใหม่ได้เมื่อเน็ตกลับมา
|
||||
return {
|
||||
ok: false,
|
||||
reason: "token-request-failed",
|
||||
message: "ขอ token จากระบบไม่สำเร็จ — ตรวจการเชื่อมต่อแล้วเปิดเอกสารอีกครั้ง",
|
||||
};
|
||||
}
|
||||
|
||||
// 200 — user ใหม่ = เคลียร์ session เก่าทั้งก้อน (requirement ขั้น 6) สร้างใหม่ ·
|
||||
// คนเดิม = update userName (ชื่อเปลี่ยนได้) + เก็บ token ใหม่ตาม fid
|
||||
if (session === null || session.userId !== ticket.uid) {
|
||||
ticketService.clearSession();
|
||||
ticketService.saveSession({ userId: ticket.uid, userName: ticket.uname, tokens: { [ticket.fid]: result.entry } });
|
||||
} else {
|
||||
session.userName = ticket.uname;
|
||||
session.tokens[ticket.fid] = result.entry;
|
||||
ticketService.saveSession(session);
|
||||
}
|
||||
return finishSignIn(ticket, ticketData, ticketHmac);
|
||||
}
|
||||
|
||||
/** ท้ายร่วมของทุกทางที่ผ่าน — ตั้ง module state ของ taskpane instance นี้ */
|
||||
function finishSignIn(ticket: DocverTicket, ticketData: string, ticketHmac: string): SignInResult {
|
||||
currentFid = ticket.fid;
|
||||
currentTicket = { data: ticketData, hmac: ticketHmac };
|
||||
username = ticket.uname;
|
||||
|
|
@ -151,26 +159,22 @@ export async function signInWithTicket(input: {
|
|||
/**
|
||||
* ขอ token ใหม่จาก ticket ของเอกสารปัจจุบัน — เรียกจาก apiClient ตอนโดน 401
|
||||
* ก่อน throw AuthRequiredError 1 รอบ · ไม่ตัดสิน exp ของ ticket ฝั่งนี้ (server
|
||||
* ออกให้ต่อได้แม้ ticket หมดอายุเมื่อเอกสารนี้เคยทำงานโดย uid เดียวกัน —
|
||||
* requirement ขั้น 7) 401 กลับมา = false ตามเดิม
|
||||
* เป็นคนตอบ: 200 = ออกให้ใหม่ · 403 = ticket หมดอายุ → false)
|
||||
* คืน true = ได้ token ใหม่เรียบร้อย (getAccessToken จะเห็นค่าใหม่)
|
||||
*/
|
||||
export async function renewTokenFromTicket(): Promise<boolean> {
|
||||
if (currentTicket === null || currentFid === null) return false;
|
||||
const parsed = parseDocverTicket(currentTicket.data);
|
||||
if (!parsed.ok) return false;
|
||||
try {
|
||||
const entry = await ticketService.requestDocverToken(currentTicket.data, currentTicket.hmac);
|
||||
const session =
|
||||
ticketService.loadSession() ??
|
||||
{ userId: parsed.ticket.uid, userName: parsed.ticket.uname, tokens: {} };
|
||||
session.userName = parsed.ticket.uname;
|
||||
session.tokens[parsed.ticket.fid] = entry;
|
||||
ticketService.saveSession(session);
|
||||
return true;
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
const result = await ticketService.requestDocverToken(currentTicket.data, currentTicket.hmac);
|
||||
if (!result.ok) return false;
|
||||
const session =
|
||||
ticketService.loadSession() ??
|
||||
{ userId: parsed.ticket.uid, userName: parsed.ticket.uname, tokens: {} };
|
||||
session.userName = parsed.ticket.uname;
|
||||
session.tokens[parsed.ticket.fid] = result.entry;
|
||||
ticketService.saveSession(session);
|
||||
return true;
|
||||
}
|
||||
|
||||
/** Bearer ของ apiClient — token ของ fid ปัจจุบัน (stale ได้ — apiClient เป็นคนตัดสิน 401) */
|
||||
|
|
|
|||
|
|
@ -1,17 +1,18 @@
|
|||
/**
|
||||
* Ticket ของ add-in — ของมือถือ (primitives) ให้ authService ประกอบเป็น flow
|
||||
* (PLAN-ADDIN-AUTH.md §5):
|
||||
* - verifyTicketHmacWeb: ตรวจ HMAC ฝั่ง client ด้วย Web Crypto (UX gate — ตัดเคส
|
||||
* ไฟล์ถูกแก้ก่อนยิง network ส่วนความมั่นคงจริงอยู่ที่ server ตรวจอีกชั้น)
|
||||
* - session store: localStorage key เดียว `wordaddin-auth` — userId/userName +
|
||||
* map token ต่อ fid · webview block localStorage = fallback memory ต่อ session
|
||||
* (pattern เดียวกับ versioningService)
|
||||
* - requestDocverToken: POST same-origin /api/documents/token แบบ body ticket —
|
||||
* ไม่ผูกกับ config.apiBaseUrl (endpoint อยู่แอปนี้ ใช้ได้ทั้งช่วง mock และจริง)
|
||||
* ไม่ผูกกับ config.apiBaseUrl (endpoint อยู่แอปนี้ ใช้ได้ทั้งช่วง mock และจริง) ·
|
||||
* คืน discriminated result แยก network/http status/malformed ให้ caller
|
||||
* ตัดสินตาม contract 400/401/403 (2026-11-04)
|
||||
* - (ไม่มีการตรวจ HMAC ฝั่ง client อีกต่อไป — secret เป็นของ server อย่างเดียว
|
||||
* ตาม PLAN-SSO.md D13, ถอด verifyTicketHmacWeb ออก 2026-11-04)
|
||||
*/
|
||||
|
||||
import { withBasePath } from "@/utils/base-path";
|
||||
import { normalizeHexDigest } from "@/utils/docver-ticket";
|
||||
|
||||
/** key เดียวของ session — v ไว้ migrate รูปในอนาคต */
|
||||
const SESSION_STORAGE_KEY = "wordaddin-auth";
|
||||
|
|
@ -91,38 +92,6 @@ export function isTokenExpired(entry: SessionTokenEntry): boolean {
|
|||
return entry.exp * 1000 <= Date.now() - 30_000;
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// HMAC ฝั่ง client — Web Crypto (crypto.subtle ให้ promise เสมอ)
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
/** คำนวณ HMAC-SHA256 คืน hex lowercase — รูปแบบเดียวกับ generateHmacSha256 ฝั่ง server */
|
||||
export async function hmacSha256HexWeb(message: string, secret: string): Promise<string> {
|
||||
const encoder = new TextEncoder();
|
||||
const key = await crypto.subtle.importKey(
|
||||
"raw",
|
||||
encoder.encode(secret),
|
||||
{ name: "HMAC", hash: "SHA-256" },
|
||||
false,
|
||||
["sign"],
|
||||
);
|
||||
const signature = await crypto.subtle.sign("HMAC", key, encoder.encode(message));
|
||||
return Array.from(new Uint8Array(signature), (byte) => byte.toString(16).padStart(2, "0")).join("");
|
||||
}
|
||||
|
||||
/**
|
||||
* ตรวจ HMAC ของ ticket ฝั่ง client — message คือ string ดิบของ ticket_data ตามที่อ่านจาก
|
||||
* property (ห้าม trim — ตัว stamp เซ็นบนค่าดิบ) · เทียบหลัง normalize lowercase
|
||||
*/
|
||||
export async function verifyTicketHmacWeb(ticketData: string, expectedHex: string, secret: string): Promise<boolean> {
|
||||
const expected = normalizeHexDigest(expectedHex);
|
||||
if (expected === null) return false;
|
||||
try {
|
||||
return (await hmacSha256HexWeb(ticketData, secret)) === expected;
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// ขอ token จาก /api/documents/token (ทางเข้า ticket — PLAN-ADDIN-AUTH.md §3)
|
||||
// ---------------------------------------------------------------------------
|
||||
|
|
@ -147,30 +116,50 @@ function jwtExp(token: string): number | undefined {
|
|||
}
|
||||
}
|
||||
|
||||
/** ผลของ requestDocverToken — แยกสาเหตุให้ caller ตัดสินตาม contract ไม่โยน exception */
|
||||
export type RequestDocverTokenResult =
|
||||
| { ok: true; entry: SessionTokenEntry }
|
||||
/** fetch reject — ไม่ถึง server (offline/dev server ตาย) */
|
||||
| { ok: false; kind: "network" }
|
||||
/** ได้ response แต่ไม่ใช่ 200 — 400 ฟอร์แม็ตพัง / 401 ลายเซ็นไม่ถูก / 403 ลายเซ็นถูกแต่ ticket หมดอายุ */
|
||||
| { ok: false; kind: "http"; status: number }
|
||||
/** ตอบ 200 แต่ body ใช้ไม่ได้ (ไม่ใช่ JSON / ไม่มี access_token) */
|
||||
| { ok: false; kind: "malformed" };
|
||||
|
||||
/**
|
||||
* ขอ access_token ด้วย ticket — โยน Error เมื่อ status ไม่สำเร็จ
|
||||
* (401 = ticket ไม่ผ่านการตรวจฝั่ง server — ให้ caller ไป no-access ตาม flow)
|
||||
* ขอ access_token ด้วย ticket — คืน discriminated result ไม่โยน Error:
|
||||
* caller (authService) แยกทางตาม contract ของทางเข้า ticket ของ
|
||||
* /api/documents/token (2026-11-04: 200 ผ่าน / 400 ฟอร์แม็ต / 401 ลายเซ็น /
|
||||
* 403 ลายเซ็นถูกแต่ ticket หมดอายุ — มี JWT เดิมของ fid ใน session ใช้ต่อได้)
|
||||
*/
|
||||
export async function requestDocverToken(ticketData: string, ticketHmac: string): Promise<SessionTokenEntry> {
|
||||
const response = await fetch(withBasePath("/api/documents/token"), {
|
||||
method: "POST",
|
||||
headers: { "content-type": "application/json" },
|
||||
body: JSON.stringify({ ticket_data: ticketData, HmacSha256: ticketHmac }),
|
||||
});
|
||||
export async function requestDocverToken(
|
||||
ticketData: string,
|
||||
ticketHmac: string,
|
||||
): Promise<RequestDocverTokenResult> {
|
||||
let response: Response;
|
||||
try {
|
||||
response = await fetch(withBasePath("/api/documents/token"), {
|
||||
method: "POST",
|
||||
headers: { "content-type": "application/json" },
|
||||
body: JSON.stringify({ ticket_data: ticketData, HmacSha256: ticketHmac }),
|
||||
});
|
||||
} catch {
|
||||
return { ok: false, kind: "network" };
|
||||
}
|
||||
if (!response.ok) {
|
||||
throw new Error(`ขอ token ไม่สำเร็จ (HTTP ${response.status})`);
|
||||
return { ok: false, kind: "http", status: response.status };
|
||||
}
|
||||
let data: TokenResponse;
|
||||
try {
|
||||
data = (await response.json()) as TokenResponse;
|
||||
} catch {
|
||||
throw new Error("ขอ token ไม่สำเร็จ (response ไม่ใช่ JSON)");
|
||||
return { ok: false, kind: "malformed" };
|
||||
}
|
||||
if (typeof data.access_token !== "string" || data.access_token === "") {
|
||||
throw new Error("ขอ token ไม่สำเร็จ (ไม่มี access_token)");
|
||||
return { ok: false, kind: "malformed" };
|
||||
}
|
||||
const exp =
|
||||
jwtExp(data.access_token) ??
|
||||
(typeof data.expires_in === "number" ? Math.floor(Date.now() / 1000) + data.expires_in : 0);
|
||||
return { token: data.access_token, exp };
|
||||
return { ok: true, entry: { token: data.access_token, exp } };
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue