add-in: request docver token via /api/auth/token (short path)
All checks were successful
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) Successful in 1m22s
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 1s
All checks were successful
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) Successful in 1m22s
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 1s
- ticketService.requestDocverToken fetches withBasePath("/api/auth/token")
instead of /api/documents/token — same handler, shorter URL
- update referencing comments in authService/apiClient/config
- ticket-auth-flow test now asserts the actual URL hit, guarding a silent
revert to the old path
- versioning endpoints (/api/documents/versions*) unchanged — no short-path
equivalents exist
Co-Authored-By: Claude Code <noreply@anthropic.com>
This commit is contained in:
parent
545c9d5703
commit
3162b0d947
5 changed files with 14 additions and 10 deletions
|
|
@ -1,7 +1,7 @@
|
||||||
// @vitest-environment jsdom
|
// @vitest-environment jsdom
|
||||||
// ticket auth ของ add-in (PLAN-ADDIN-AUTH.md §5 — contract 2026-11-04) — คุมพฤติกรรม
|
// ticket auth ของ add-in (PLAN-ADDIN-AUTH.md §5 — contract 2026-11-04) — คุมพฤติกรรม
|
||||||
// service layer: pre-check ฝั่ง client (missing/ฟอร์แม็ต/หมดอายุ+ไม่มี session ไม่ยิง API),
|
// 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
|
// memory fallback ของ session, ทุก fail path ต้องคืน result ไม่ใช่ throw
|
||||||
// (ไม่มีการตรวจ HMAC ฝั่ง client อีกต่อไป — PLAN-SSO.md D13: secret เป็นของ server
|
// (ไม่มีการตรวจ HMAC ฝั่ง client อีกต่อไป — PLAN-SSO.md D13: secret เป็นของ server
|
||||||
// อย่างเดียว "ไฟล์ถูกแก้" ตรวจจาก 400/401 ของ 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 authService from "@/wordaddin/services/authService";
|
||||||
import * as ticketService from "@/wordaddin/services/ticketService";
|
import * as ticketService from "@/wordaddin/services/ticketService";
|
||||||
|
import { withBasePath } from "@/utils/base-path";
|
||||||
|
|
||||||
// จับ error ทุกทาง (window error / unhandled rejection) — flow ต้องไม่ปล่อยอะไรหลุดเลย
|
// จับ error ทุกทาง (window error / unhandled rejection) — flow ต้องไม่ปล่อยอะไรหลุดเลย
|
||||||
const errors: unknown[] = [];
|
const errors: unknown[] = [];
|
||||||
|
|
@ -157,13 +158,15 @@ describe("signInWithTicket — pre-check ฝั่ง client (เตะโดย
|
||||||
describe("signInWithTicket — ยิง /token ทุกครั้งที่เปิดเอกสาร", () => {
|
describe("signInWithTicket — ยิง /token ทุกครั้งที่เปิดเอกสาร", () => {
|
||||||
it("happy-path: ไม่มี session + 200 → ok และสร้าง session ใหม่ครบ userId/userName/tokens[fid]", async () => {
|
it("happy-path: ไม่มี session + 200 → ok และสร้าง session ใหม่ครบ userId/userName/tokens[fid]", async () => {
|
||||||
trapErrors();
|
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);
|
vi.stubGlobal("fetch", fetchMock);
|
||||||
|
|
||||||
const { ticketData, ticketHmac } = makeTicket();
|
const { ticketData, ticketHmac } = makeTicket();
|
||||||
const result = await authService.signInWithTicket({ ticketData, ticketHmac });
|
const result = await authService.signInWithTicket({ ticketData, ticketHmac });
|
||||||
expect(result.ok, JSON.stringify(result)).toBe(true);
|
expect(result.ok, JSON.stringify(result)).toBe(true);
|
||||||
expect(fetchMock).toHaveBeenCalledTimes(1);
|
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") ?? "{}");
|
const stored = JSON.parse(window.localStorage.getItem("wordaddin-auth") ?? "{}");
|
||||||
expect(stored.userId).toBe("oom");
|
expect(stored.userId).toBe("oom");
|
||||||
|
|
|
||||||
|
|
@ -4,7 +4,7 @@
|
||||||
*
|
*
|
||||||
* ช่วง mock: versioningService เป็น mock ไม่ใช้ apiBaseUrl —
|
* ช่วง mock: versioningService เป็น mock ไม่ใช้ apiBaseUrl —
|
||||||
* ตั้งค่านี้เมื่อปลด mock ต่อ backend จริง (ต้องเป็น HTTPS เสมอ — webview ของ Word บล็อก http)
|
* ตั้งค่านี้เมื่อปลด 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 = {
|
export const config = {
|
||||||
/** base URL ของ backend API (ไม่มี slash ท้าย) — ว่าง = ยังไม่ต่อ backend จริง */
|
/** base URL ของ backend API (ไม่มี slash ท้าย) — ว่าง = ยังไม่ต่อ backend จริง */
|
||||||
|
|
|
||||||
|
|
@ -4,7 +4,7 @@ import * as authService from "./authService";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* fetch wrapper ของ add-in — แนบ Bearer ทุก request (access_token ที่ 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:
|
* โดน 401 → ลองขอ token ใหม่จาก ticket แล้ว retry 1 ครั้ง ยังไม่ผ่าน → AuthRequiredError:
|
||||||
* UI จับ error นี้แล้วพาไปหน้า no-access ให้ขอไฟล์ใหม่จาก e-Court
|
* UI จับ error นี้แล้วพาไปหน้า no-access ให้ขอไฟล์ใหม่จาก e-Court
|
||||||
* พอร์ตจาก D:\word-addin-poc\addin\src\services\apiClient.ts
|
* พอร์ตจาก D:\word-addin-poc\addin\src\services\apiClient.ts
|
||||||
|
|
|
||||||
|
|
@ -5,7 +5,7 @@
|
||||||
* ไม่มี login ใน add-in: ไฟล์ที่ e-Court stamp custom properties `ticket_data`
|
* ไม่มี login ใน add-in: ไฟล์ที่ e-Court stamp custom properties `ticket_data`
|
||||||
* + `HmacSha256` มา (ผ่านกลุ่ม template) = ผ่านเข้าได้ — signInWithTicket ตรวจ
|
* + `HmacSha256` มา (ผ่านกลุ่ม template) = ผ่านเข้าได้ — signInWithTicket ตรวจ
|
||||||
* ฟอร์แม็ต/exp เป็น UX gate ฝั่ง client (ไม่มี secret ใน bundle อีกต่อไปตาม
|
* ฟอร์แม็ต/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 ฟอร์แม็ตพัง /
|
* ทุกครั้งที่เปิดเอกสาร) → แยกทางตาม contract: 200 = ได้ JWT ใหม่ / 400 ฟอร์แม็ตพัง /
|
||||||
* 401 ลายเซ็นไม่ถูก / 403 ลายเซ็นถูกแต่ ticket หมดอายุ (มี JWT ของ fid ใน session
|
* 401 ลายเซ็นไม่ถูก / 403 ลายเซ็นถูกแต่ ticket หมดอายุ (มี JWT ของ fid ใน session
|
||||||
* ที่ยังไม่หมด = ใช้ต่อ ไม่มี = ขอไฟล์ใหม่จาก e-Court)
|
* ที่ยังไม่หมด = ใช้ต่อ ไม่มี = ขอไฟล์ใหม่จาก e-Court)
|
||||||
|
|
@ -52,7 +52,7 @@ export function isDemoMode(): boolean {
|
||||||
* 3. pre-check: ticket หมดอายุ และ ไม่มี JWT ของ fid นี้ใน session (uid ตรงกัน)
|
* 3. pre-check: ticket หมดอายุ และ ไม่มี JWT ของ fid นี้ใน session (uid ตรงกัน)
|
||||||
* = เตะเลย ไม่ยิง API (มีใน session = มีโอกาสเป็นไฟล์เก่าที่เคยแก้ → ไปให้
|
* = เตะเลย ไม่ยิง API (มีใน session = มีโอกาสเป็นไฟล์เก่าที่เคยแก้ → ไปให้
|
||||||
* server ตัดสินที่ขั้นถัดไป)
|
* server ตัดสินที่ขั้นถัดไป)
|
||||||
* 4. ที่เหลือยิง /api/documents/token **ทุกครั้ง** ให้ server ตรวจลายเซ็น (HMAC)
|
* 4. ที่เหลือยิง /api/auth/token **ทุกครั้ง** ให้ server ตรวจลายเซ็น (HMAC)
|
||||||
* แล้วแยกทางตาม contract: 200 ได้ JWT ใหม่ (user ต่าง = เคลียร์ session ทั้งก้อน
|
* แล้วแยกทางตาม contract: 200 ได้ JWT ใหม่ (user ต่าง = เคลียร์ session ทั้งก้อน
|
||||||
* สร้างใหม่) · 403 ลายเซ็นถูกแต่ ticket หมดอายุ — มี JWT ของ fid ที่ยังไม่หมด
|
* สร้างใหม่) · 403 ลายเซ็นถูกแต่ ticket หมดอายุ — มี JWT ของ fid ที่ยังไม่หมด
|
||||||
* ใน session ก็ใช้ต่อ · 400/401 ฟอร์แม็ต/ลายเซ็นพัง = เตะ (ไฟล์ถูกแก้)
|
* ใน session ก็ใช้ต่อ · 400/401 ฟอร์แม็ต/ลายเซ็นพัง = เตะ (ไฟล์ถูกแก้)
|
||||||
|
|
|
||||||
|
|
@ -4,7 +4,8 @@
|
||||||
* - session store: localStorage key เดียว `wordaddin-auth` — userId/userName +
|
* - session store: localStorage key เดียว `wordaddin-auth` — userId/userName +
|
||||||
* map token ต่อ fid · webview block localStorage = fallback memory ต่อ session
|
* map token ต่อ fid · webview block localStorage = fallback memory ต่อ session
|
||||||
* (pattern เดียวกับ versioningService)
|
* (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 และจริง) ·
|
* ไม่ผูกกับ config.apiBaseUrl (endpoint อยู่แอปนี้ ใช้ได้ทั้งช่วง mock และจริง) ·
|
||||||
* คืน discriminated result แยก network/http status/malformed ให้ caller
|
* คืน discriminated result แยก network/http status/malformed ให้ caller
|
||||||
* ตัดสินตาม contract 400/401/403 (2026-11-04)
|
* ตัดสินตาม 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 {
|
interface TokenResponse {
|
||||||
|
|
@ -129,7 +130,7 @@ export type RequestDocverTokenResult =
|
||||||
/**
|
/**
|
||||||
* ขอ access_token ด้วย ticket — คืน discriminated result ไม่โยน Error:
|
* ขอ access_token ด้วย ticket — คืน discriminated result ไม่โยน Error:
|
||||||
* caller (authService) แยกทางตาม contract ของทางเข้า ticket ของ
|
* 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 ใช้ต่อได้)
|
* 403 ลายเซ็นถูกแต่ ticket หมดอายุ — มี JWT เดิมของ fid ใน session ใช้ต่อได้)
|
||||||
*/
|
*/
|
||||||
export async function requestDocverToken(
|
export async function requestDocverToken(
|
||||||
|
|
@ -138,7 +139,7 @@ export async function requestDocverToken(
|
||||||
): Promise<RequestDocverTokenResult> {
|
): Promise<RequestDocverTokenResult> {
|
||||||
let response: Response;
|
let response: Response;
|
||||||
try {
|
try {
|
||||||
response = await fetch(withBasePath("/api/documents/token"), {
|
response = await fetch(withBasePath("/api/auth/token"), {
|
||||||
method: "POST",
|
method: "POST",
|
||||||
headers: { "content-type": "application/json" },
|
headers: { "content-type": "application/json" },
|
||||||
body: JSON.stringify({ ticket_data: ticketData, HmacSha256: ticketHmac }),
|
body: JSON.stringify({ ticket_data: ticketData, HmacSha256: ticketHmac }),
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue