diff --git a/tests/wordaddin/ticket-auth-flow.test.ts b/tests/wordaddin/ticket-auth-flow.test.ts index 5cea20c..b9e0bc0 100644 --- a/tests/wordaddin/ticket-auth-flow.test.ts +++ b/tests/wordaddin/ticket-auth-flow.test.ts @@ -1,7 +1,7 @@ // @vitest-environment jsdom // ticket auth ของ add-in (PLAN-ADDIN-AUTH.md §5 — contract 2026-11-04) — คุมพฤติกรรม // service layer: pre-check ฝั่ง client (missing/ฟอร์แม็ต/หมดอายุ+ไม่มี session ไม่ยิง API), -// ยิง /api/documents/token ทุกครั้งที่เปิดเอกสารแล้วแยกทางตาม status (200/400/401/403), +// ยิง /api/auth/token ทุกครั้งที่เปิดเอกสารแล้วแยกทางตาม status (200/400/401/403), // memory fallback ของ session, ทุก fail path ต้องคืน result ไม่ใช่ throw // (ไม่มีการตรวจ HMAC ฝั่ง client อีกต่อไป — PLAN-SSO.md D13: secret เป็นของ server // อย่างเดียว "ไฟล์ถูกแก้" ตรวจจาก 400/401 ของ server) @@ -10,6 +10,7 @@ import { afterEach, describe, expect, it, vi } from "vitest"; import * as authService from "@/wordaddin/services/authService"; import * as ticketService from "@/wordaddin/services/ticketService"; +import { withBasePath } from "@/utils/base-path"; // จับ error ทุกทาง (window error / unhandled rejection) — flow ต้องไม่ปล่อยอะไรหลุดเลย const errors: unknown[] = []; @@ -157,13 +158,15 @@ describe("signInWithTicket — pre-check ฝั่ง client (เตะโดย describe("signInWithTicket — ยิง /token ทุกครั้งที่เปิดเอกสาร", () => { it("happy-path: ไม่มี session + 200 → ok และสร้าง session ใหม่ครบ userId/userName/tokens[fid]", async () => { trapErrors(); - const fetchMock = vi.fn(async () => tokenResponse("token-1")); + const fetchMock = vi.fn(async (_url: string | URL | Request) => tokenResponse("token-1")); vi.stubGlobal("fetch", fetchMock); const { ticketData, ticketHmac } = makeTicket(); const result = await authService.signInWithTicket({ ticketData, ticketHmac }); expect(result.ok, JSON.stringify(result)).toBe(true); expect(fetchMock).toHaveBeenCalledTimes(1); + // endpoint ที่ยิงจริง = path สั้น /api/auth/token (เปลี่ยนจาก /api/documents/token 2026-11-05) + expect(fetchMock.mock.calls[0][0]).toBe(withBasePath("/api/auth/token")); const stored = JSON.parse(window.localStorage.getItem("wordaddin-auth") ?? "{}"); expect(stored.userId).toBe("oom"); diff --git a/wordaddin/config.ts b/wordaddin/config.ts index 6c8e663..accbc65 100644 --- a/wordaddin/config.ts +++ b/wordaddin/config.ts @@ -4,7 +4,7 @@ * * ช่วง mock: versioningService เป็น mock ไม่ใช้ apiBaseUrl — * ตั้งค่านี้เมื่อปลด mock ต่อ backend จริง (ต้องเป็น HTTPS เสมอ — webview ของ Word บล็อก http) - * ส่วนการขอ token (ticket auth) ยิง same-origin /api/documents/token เสมอ ไม่ผูกกับ apiBaseUrl + * ส่วนการขอ token (ticket auth) ยิง same-origin /api/auth/token เสมอ ไม่ผูกกับ apiBaseUrl */ export const config = { /** base URL ของ backend API (ไม่มี slash ท้าย) — ว่าง = ยังไม่ต่อ backend จริง */ diff --git a/wordaddin/services/apiClient.ts b/wordaddin/services/apiClient.ts index 9640483..de94e21 100644 --- a/wordaddin/services/apiClient.ts +++ b/wordaddin/services/apiClient.ts @@ -4,7 +4,7 @@ import * as authService from "./authService"; /** * fetch wrapper ของ add-in — แนบ Bearer ทุก request (access_token ที่ authService - * ขอจาก /api/documents/token ด้วย ticket ของไฟล์ — PLAN-ADDIN-AUTH.md) + * ขอจาก /api/auth/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 diff --git a/wordaddin/services/authService.ts b/wordaddin/services/authService.ts index 3ab193b..8aa0876 100644 --- a/wordaddin/services/authService.ts +++ b/wordaddin/services/authService.ts @@ -5,7 +5,7 @@ * ไม่มี login ใน add-in: ไฟล์ที่ e-Court stamp custom properties `ticket_data` * + `HmacSha256` มา (ผ่านกลุ่ม template) = ผ่านเข้าได้ — signInWithTicket ตรวจ * ฟอร์แม็ต/exp เป็น UX gate ฝั่ง client (ไม่มี secret ใน bundle อีกต่อไปตาม - * PLAN-SSO.md D13 — การตรวจ HMAC เป็นของ server อย่างเดียว ยิง /api/documents/token + * PLAN-SSO.md D13 — การตรวจ HMAC เป็นของ server อย่างเดียว ยิง /api/auth/token * ทุกครั้งที่เปิดเอกสาร) → แยกทางตาม contract: 200 = ได้ JWT ใหม่ / 400 ฟอร์แม็ตพัง / * 401 ลายเซ็นไม่ถูก / 403 ลายเซ็นถูกแต่ ticket หมดอายุ (มี JWT ของ fid ใน session * ที่ยังไม่หมด = ใช้ต่อ ไม่มี = ขอไฟล์ใหม่จาก e-Court) @@ -52,7 +52,7 @@ export function isDemoMode(): boolean { * 3. pre-check: ticket หมดอายุ และ ไม่มี JWT ของ fid นี้ใน session (uid ตรงกัน) * = เตะเลย ไม่ยิง API (มีใน session = มีโอกาสเป็นไฟล์เก่าที่เคยแก้ → ไปให้ * server ตัดสินที่ขั้นถัดไป) - * 4. ที่เหลือยิง /api/documents/token **ทุกครั้ง** ให้ server ตรวจลายเซ็น (HMAC) + * 4. ที่เหลือยิง /api/auth/token **ทุกครั้ง** ให้ server ตรวจลายเซ็น (HMAC) * แล้วแยกทางตาม contract: 200 ได้ JWT ใหม่ (user ต่าง = เคลียร์ session ทั้งก้อน * สร้างใหม่) · 403 ลายเซ็นถูกแต่ ticket หมดอายุ — มี JWT ของ fid ที่ยังไม่หมด * ใน session ก็ใช้ต่อ · 400/401 ฟอร์แม็ต/ลายเซ็นพัง = เตะ (ไฟล์ถูกแก้) diff --git a/wordaddin/services/ticketService.ts b/wordaddin/services/ticketService.ts index 349a721..0cc3d89 100644 --- a/wordaddin/services/ticketService.ts +++ b/wordaddin/services/ticketService.ts @@ -4,7 +4,8 @@ * - 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 — + * - requestDocverToken: POST same-origin /api/auth/token แบบ body ticket + * (path สั้นของ /api/documents/token — endpoint ก้อนเดียวกัน เปลี่ยนมาใช้ 2026-11-05) — * ไม่ผูกกับ config.apiBaseUrl (endpoint อยู่แอปนี้ ใช้ได้ทั้งช่วง mock และจริง) · * คืน discriminated result แยก network/http status/malformed ให้ caller * ตัดสินตาม contract 400/401/403 (2026-11-04) @@ -93,7 +94,7 @@ export function isTokenExpired(entry: SessionTokenEntry): boolean { } // --------------------------------------------------------------------------- -// ขอ token จาก /api/documents/token (ทางเข้า ticket — PLAN-ADDIN-AUTH.md §3) +// ขอ token จาก /api/auth/token (ทางเข้า ticket — PLAN-ADDIN-AUTH.md §3) // --------------------------------------------------------------------------- interface TokenResponse { @@ -129,7 +130,7 @@ export type RequestDocverTokenResult = /** * ขอ access_token ด้วย ticket — คืน discriminated result ไม่โยน Error: * caller (authService) แยกทางตาม contract ของทางเข้า ticket ของ - * /api/documents/token (2026-11-04: 200 ผ่าน / 400 ฟอร์แม็ต / 401 ลายเซ็น / + * /api/auth/token (2026-11-04: 200 ผ่าน / 400 ฟอร์แม็ต / 401 ลายเซ็น / * 403 ลายเซ็นถูกแต่ ticket หมดอายุ — มี JWT เดิมของ fid ใน session ใช้ต่อได้) */ export async function requestDocverToken( @@ -138,7 +139,7 @@ export async function requestDocverToken( ): Promise { let response: Response; try { - response = await fetch(withBasePath("/api/documents/token"), { + response = await fetch(withBasePath("/api/auth/token"), { method: "POST", headers: { "content-type": "application/json" }, body: JSON.stringify({ ticket_data: ticketData, HmacSha256: ticketHmac }),