feat(addin): implement e-Court ticket auth (ticket_data + HmacSha256)
Replace the token/owner pass-through with the ticket flow from
docs/requirement/addin-workflow.md (full plan: docs/PLAN-ADDIN-AUTH.md):
- utils/docver-ticket.ts: shared pure ticket parse + hex digest helpers,
client/server inject their own crypto (Web Crypto / node:crypto)
- lib/addin-ticket.ts + POST /api/documents/token second entry: body
{ticket_data, HmacSha256} verified server-side (HMAC timing-safe + exp,
secret ADDIN_TICKET_SECRET -> SECRET -> "super_secret"); x-api-key path
unchanged
- wordaddin: ticketService (Web Crypto HMAC, localStorage session
wordaddin-auth keyed by fid, same-origin token request), authService
rewrite (7-step flow, silent renew), apiClient 401 renew-retry once,
metadataService reads ticket_data/HmacSha256, no-access messages per
cause
- scripts/make-ticket-docx.mjs: dev tool to stamp tickets onto any .docx
for testing in real Word
- app/wordaddin/error.tsx: error boundary showing the real error + stack
in the pane (webviews have no devtools)
- tests: docver-ticket + addin-ticket unit suites (21 cases) + temporary
crash repro suite for the ongoing client-crash debug session
- docs sync: PLAN-DOCVER, WORDADDIN-DEV, addin-ui-flow, openapi (two
request bodies for /token), CLAUDE.md, .env.example
Co-Authored-By: Claude Code <noreply@anthropic.com>
This commit is contained in:
parent
8ebe5f8644
commit
cffc571da5
21 changed files with 1725 additions and 124 deletions
|
|
@ -2,17 +2,18 @@ import { config } from "../config";
|
|||
import * as authService from "./authService";
|
||||
|
||||
/**
|
||||
* fetch wrapper ของ add-in — แนบ Bearer ทุก request (token จาก custom properties ของไฟล์)
|
||||
* โดน 401 → AuthRequiredError ทันที: token จากไฟล์ต่ออายุไม่ได้ (ไม่มี refresh token
|
||||
* ตั้งแต่ยกเลิก Keycloak) — UI จับ error นี้แล้วพาไปหน้า no-access ให้ขอไฟล์ใหม่จาก e-Court
|
||||
* fetch wrapper ของ add-in — แนบ Bearer ทุก request (access_token ที่ authService
|
||||
* ขอจาก /api/documents/token ด้วย ticket ของไฟล์ — PLAN-ADDIN-AUTH.md)
|
||||
* โดน 401 → ลองขอ token ใหม่จาก ticket แล้ว retry 1 ครั้ง ยังไม่ผ่าน → AuthRequiredError:
|
||||
* UI จับ error นี้แล้วพาไปหน้า no-access ให้ขอไฟล์ใหม่จาก e-Court
|
||||
* พอร์ตจาก D:\word-addin-poc\addin\src\services\apiClient.ts
|
||||
* (ช่วง mock ไม่มี caller จริง — เตรียมไว้ให้ versioningService วันปลด mock + AuthRequiredError
|
||||
* ถูก TaskpaneApp ใช้อยู่แล้ว)
|
||||
*/
|
||||
|
||||
/** บอกว่า token จากไฟล์ใช้ไม่ได้ (ไม่มี / หมดอายุ / ไม่ถูกต้อง) — ต้องได้ไฟล์ใหม่จาก e-Court */
|
||||
/** บอกว่า ticket ใช้ไม่ได้แล้ว (หมดอายุ / token ขอไม่ได้) — ต้องได้ไฟล์ใหม่จาก e-Court */
|
||||
export class AuthRequiredError extends Error {
|
||||
constructor(message = "token ของ e-Court หมดอายุหรือไม่ถูกต้อง") {
|
||||
constructor(message = "ticket ของ e-Court หมดอายุหรือไม่ถูกต้อง") {
|
||||
super(message);
|
||||
this.name = "AuthRequiredError";
|
||||
}
|
||||
|
|
@ -54,8 +55,10 @@ async function doFetch(
|
|||
}
|
||||
|
||||
/**
|
||||
* request พร้อม Bearer — 401 = token จากไฟล์หมดอายุ/ไม่ถูกต้อง → AuthRequiredError
|
||||
* Response ที่ได้คืนไปให้ caller เช็ค ok เอง
|
||||
* request พร้อม Bearer — โดน 401 → ขอ token ใหม่จาก ticket ของเอกสารปัจจุบัน
|
||||
* (renewTokenFromTicket — ticket ยังไม่หมดอายุเท่านั้น) แล้ว 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();
|
||||
|
|
@ -63,10 +66,19 @@ export async function apiFetch(path: string, options: ApiRequestOptions = {}): P
|
|||
throw new AuthRequiredError();
|
||||
}
|
||||
const response = await doFetch(path, options, accessToken);
|
||||
if (response.status === 401) {
|
||||
throw new AuthRequiredError();
|
||||
if (response.status !== 401) {
|
||||
return response;
|
||||
}
|
||||
return response;
|
||||
if (await authService.renewTokenFromTicket()) {
|
||||
const retryToken = await authService.getAccessToken();
|
||||
if (retryToken) {
|
||||
const retry = await doFetch(path, options, retryToken);
|
||||
if (retry.status !== 401) {
|
||||
return retry;
|
||||
}
|
||||
}
|
||||
}
|
||||
throw new AuthRequiredError();
|
||||
}
|
||||
|
||||
/** อ่าน error message จาก response body — backend error ส่วนใหญ่เป็น plain text, บางทีเป็น JSON problem details */
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue