import { config } from "../config"; import { withBasePath } from "@/utils/base-path"; import * as authService from "./authService"; /** * 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 ใช้อยู่แล้ว) */ /** บอกว่า ticket ใช้ไม่ได้แล้ว (หมดอายุ / token ขอไม่ได้) — ต้องได้ไฟล์ใหม่จาก e-Court */ export class AuthRequiredError extends Error { constructor(message = "ticket ของ e-Court หมดอายุหรือไม่ถูกต้อง") { super(message); this.name = "AuthRequiredError"; } } /** backend ตอบมาแต่ status ไม่สำเร็จ — message อ่านได้จาก body ถ้ามี */ export class ApiError extends Error { readonly status: number; constructor(status: number, message: string) { super(message); this.name = "ApiError"; this.status = status; } } export interface ApiRequestOptions { method?: string; headers?: Record; body?: BodyInit | null; } /** * path เต็มเสมอ — ไม่ตั้ง `NEXT_PUBLIC_ADDIN_API_BASE_URL` = same-origin ผ่าน withBasePath * (ได้ /docedit/api/… ถูกต้องตาม invariant #12 — API ของกลุ่ม docver อยู่แอปนี้เอง) * · ตั้งค่า = ชี้ backend คนละ origin (ต้องรวม basePath แล้ว เช่น https://host/docedit — ใช้ตอน * deploy add-in กับ API คนละตัว) และต้องเป็น HTTPS เสมอ (webview ของ Word บล็อก http) */ function toUrl(path: string): string { if (path.startsWith("http")) return path; return config.apiBaseUrl ? `${config.apiBaseUrl}${path}` : withBasePath(path); } async function doFetch( path: string, options: ApiRequestOptions, accessToken: string ): Promise { const headers = new Headers(options.headers ?? {}); headers.set("Authorization", `Bearer ${accessToken}`); return fetch(toUrl(path), { method: options.method ?? "GET", headers, body: options.body, }); } /** * 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 { const accessToken = await authService.getAccessToken(); if (!accessToken) { throw new AuthRequiredError(); } const response = await doFetch(path, options, accessToken); if (response.status !== 401) { 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 */ export async function readErrorMessage(response: Response, fallback: string): Promise { try { const text = (await response.text()).trim(); if (!text) { return fallback; } try { const parsed = JSON.parse(text) as { detail?: unknown; message?: unknown; title?: unknown }; const detail = parsed.detail ?? parsed.message ?? parsed.title; if (typeof detail === "string" && detail) { return detail; } } catch { // plain text — ใช้ตรงๆ } return text; } catch { return fallback; } } /** GET แล้ว parse JSON — fail โยน ApiError พร้อม message จาก backend */ export async function apiGetJson(path: string, fallbackError: string): Promise { const response = await apiFetch(path); if (!response.ok) { throw new ApiError(response.status, await readErrorMessage(response, fallbackError)); } return (await response.json()) as T; } /** POST (form/json) แล้ว parse JSON — ใช้กับ upload version ที่ตอบ 201 */ export async function apiSendExpectJson( path: string, options: ApiRequestOptions, fallbackError: string ): Promise { const response = await apiFetch(path, options); if (!response.ok) { throw new ApiError(response.status, await readErrorMessage(response, fallbackError)); } return (await response.json()) as T; }