Refactor Word Add-in authentication to use e-Court pass-through; remove Keycloak integration
- Updated documentation to reflect removal of login and transition to e-Court pass-through authentication. - Modified `authService` to handle authentication via custom properties in documents without user login. - Adjusted `apiClient` to immediately throw `AuthRequiredError` for invalid tokens, as refresh tokens are no longer applicable. - Revised UI flow to accommodate new authentication method, including changes to state management and error handling. - Added new requirements document outlining the add-in workflow and authentication process.
This commit is contained in:
parent
6ce071ee14
commit
50af6c10fa
9 changed files with 161 additions and 197 deletions
|
|
@ -2,17 +2,17 @@ import { config } from "../config";
|
|||
import * as authService from "./authService";
|
||||
|
||||
/**
|
||||
* fetch wrapper ของ add-in — แนบ Bearer ทุก request
|
||||
* โดน 401 → silent refresh แล้ว retry ครั้งเดียว; ยัง 401 → AuthRequiredError
|
||||
* (UI จับ error นี้แล้วพากลับหน้า sign-in อย่างเนียน — AC-9)
|
||||
* พอร์ตดิบจาก D:\word-addin-poc\addin\src\services\apiClient.ts
|
||||
* fetch wrapper ของ add-in — แนบ Bearer ทุก request (token จาก custom properties ของไฟล์)
|
||||
* โดน 401 → AuthRequiredError ทันที: token จากไฟล์ต่ออายุไม่ได้ (ไม่มี refresh token
|
||||
* ตั้งแต่ยกเลิก Keycloak) — UI จับ error นี้แล้วพาไปหน้า no-access ให้ขอไฟล์ใหม่จาก e-Court
|
||||
* พอร์ตจาก D:\word-addin-poc\addin\src\services\apiClient.ts
|
||||
* (ช่วง mock ไม่มี caller จริง — เตรียมไว้ให้ versioningService วันปลด mock + AuthRequiredError
|
||||
* ถูก TaskpaneApp ใช้อยู่แล้ว)
|
||||
*/
|
||||
|
||||
/** บอกว่าต้องกลับไป login ใหม่ (ไม่มี token / refresh ไม่ได้ / 401 ซ้ำ) */
|
||||
/** บอกว่า token จากไฟล์ใช้ไม่ได้ (ไม่มี / หมดอายุ / ไม่ถูกต้อง) — ต้องได้ไฟล์ใหม่จาก e-Court */
|
||||
export class AuthRequiredError extends Error {
|
||||
constructor(message = "เซสชันหมดอายุ — ต้อง login ใหม่") {
|
||||
constructor(message = "token ของ e-Court หมดอายุหรือไม่ถูกต้อง") {
|
||||
super(message);
|
||||
this.name = "AuthRequiredError";
|
||||
}
|
||||
|
|
@ -54,25 +54,17 @@ async function doFetch(
|
|||
}
|
||||
|
||||
/**
|
||||
* request พร้อม Bearer + retry-on-401 ครั้งเดียว
|
||||
* Response ที่ได้คืนไปให้ caller เช็ค ok เอง (ยกเว้น 401 ที่ refresh ไม่ช่วย → AuthRequiredError)
|
||||
* request พร้อม Bearer — 401 = token จากไฟล์หมดอายุ/ไม่ถูกต้อง → AuthRequiredError
|
||||
* Response ที่ได้คืนไปให้ caller เช็ค ok เอง
|
||||
*/
|
||||
export async function apiFetch(path: string, options: ApiRequestOptions = {}): Promise<Response> {
|
||||
const accessToken = await authService.getAccessToken();
|
||||
if (!accessToken) {
|
||||
throw new AuthRequiredError();
|
||||
}
|
||||
|
||||
let response = await doFetch(path, options, accessToken);
|
||||
const response = await doFetch(path, options, accessToken);
|
||||
if (response.status === 401) {
|
||||
const refreshedToken = await authService.refreshAccessToken();
|
||||
if (!refreshedToken) {
|
||||
throw new AuthRequiredError();
|
||||
}
|
||||
response = await doFetch(path, options, refreshedToken);
|
||||
if (response.status === 401) {
|
||||
throw new AuthRequiredError();
|
||||
}
|
||||
throw new AuthRequiredError();
|
||||
}
|
||||
return response;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,34 +1,21 @@
|
|||
/**
|
||||
* MOCK — แทน Keycloak OIDC/PKCE ของต้นฉบับ (ผู้ใช้งานเลือกไม่ใช้ Keycloak ช่วงพอร์ต)
|
||||
* ต้นฉบับ: D:\word-addin-poc\addin\src\services\authService.ts (login ผ่าน Office Dialog API,
|
||||
* token เก็บใน memory เท่านั้น, auto-refresh)
|
||||
* Auth ของ add-in — e-Court pass-through ทางเดียว (ยกเลิก Keycloak 2026-10-27 ตามคำขอผู้ใช้)
|
||||
* ไม่มีขั้นตอน login ใน add-in อีกต่อไป: ไฟล์ที่ e-Court stamp custom properties
|
||||
* `token`/`owner` มา (`readTrackedProperties` อ่านรวม) = ใช้งานได้ทันที ไม่ต้องกดปุ่มใด ๆ
|
||||
* ไฟล์ที่ไม่มีทั้งคู่ = phase no-access ของ TaskpaneApp (ขั้นตอนสำหรับเอกสารทั่วไปจะมีในอนาคต)
|
||||
*
|
||||
* วันสลับเป็น auth จริง: เขียน implementation ใหม่โดยคง export surface ด้านล่างครบทุกตัว
|
||||
* (เพิ่ม refreshAccessToken ที่ต้นฉบับมีอยู่แล้วสำหรับ apiClient) — โค้ดอื่นไม่ต้องแตะ
|
||||
* token เก็บใน memory เท่านั้น — getAccessToken คืน docToken ให้ apiClient แนบ Bearer
|
||||
* (ช่วงไม่ต่อ backend จริง versioningService เป็น mock ไม่เรียก apiClient อยู่แล้ว)
|
||||
*/
|
||||
|
||||
import { config } from "../config";
|
||||
|
||||
/** discriminated union เหมือนต้นฉบับ — ok:false ต้องมี error เสมอ */
|
||||
export type LoginResult = { ok: true } | { ok: false; error: string };
|
||||
|
||||
/** state เดียวของ mock — null = signed out */
|
||||
/** state เดียว — null = ยังไม่เคย pass-through สำเร็จ (จากไฟล์ หรือ ?addindemo) */
|
||||
let username: string | null = null;
|
||||
/** token จาก custom properties ของไฟล์ (e-Court pass-through) — เก็บใน memory เท่านั้น */
|
||||
let docToken: string | null = null;
|
||||
|
||||
function delay(ms: number): Promise<void> {
|
||||
return new Promise((resolve) => setTimeout(resolve, ms));
|
||||
}
|
||||
|
||||
/** จำลองเปิดหน้าต่าง login — สำเร็จทันที ตั้งชื่อจาก NEXT_PUBLIC_ADDIN_MOCK_USERNAME */
|
||||
export async function login(): Promise<LoginResult> {
|
||||
await delay(300);
|
||||
username = config.mockUsername;
|
||||
return { ok: true };
|
||||
}
|
||||
|
||||
/** mock-only — โหมด ?addindemo ใช้ตั้ง session ทันทีไม่ผ่านปุ่ม Sign in */
|
||||
/** dev-only — โหมด ?addindemo ของ WordAddinGate ใช้ตั้ง session ทันทีไม่อ่านไฟล์จริง */
|
||||
export function signInForDemo(): void {
|
||||
username = config.mockUsername;
|
||||
}
|
||||
|
|
@ -50,8 +37,8 @@ function decodeJwtField(token: string, field: string): string | undefined {
|
|||
}
|
||||
|
||||
/**
|
||||
* e-Court pass-through (ระยะแรก) — ไฟล์ที่ e-Court stamp `token`/`owner` ไว้ใน custom properties
|
||||
* ใช้งานได้เลยโดยไม่ต้องกด login: username จาก owner → uid ใน token → mockUsername
|
||||
* e-Court pass-through — จุดเข้าเดียวของ auth: ไฟล์ที่ stamp `token`/`owner` ไว้
|
||||
* username ได้จาก owner → uid ใน JWT → mockUsername (ตามลำดับ)
|
||||
* คืน true = ผ่านเข้าได้ (มี token หรือ owner อย่างน้อยหนึ่งตัว)
|
||||
*/
|
||||
export function signInFromDocument(input: { token?: string; owner?: string }): boolean {
|
||||
|
|
@ -65,33 +52,12 @@ export function signInFromDocument(input: { token?: string; owner?: string }): b
|
|||
return true;
|
||||
}
|
||||
|
||||
/** Bearer ของ apiClient — docToken จากไฟล์ (ส่วน demo/mock คืน token ปลอม) */
|
||||
export async function getAccessToken(): Promise<string | null> {
|
||||
if (docToken) return docToken;
|
||||
return username ? `mock-token-${username}` : null;
|
||||
}
|
||||
|
||||
/** mock: token ไม่มีวันหมดอายุจริง — คืนตัวเดิมหรือ null ตามสถานะ login */
|
||||
export async function refreshAccessToken(): Promise<string | null> {
|
||||
return getAccessToken();
|
||||
}
|
||||
|
||||
export function getUsername(): string | undefined {
|
||||
return username ?? undefined;
|
||||
}
|
||||
|
||||
export function isTokenValid(): boolean {
|
||||
return username !== null;
|
||||
}
|
||||
|
||||
export function hasRefreshToken(): boolean {
|
||||
return username !== null;
|
||||
}
|
||||
|
||||
export function startAutoRefresh(): void {
|
||||
// mock: ไม่มีอะไรต้อง refresh
|
||||
}
|
||||
|
||||
export function signOut(): void {
|
||||
username = null;
|
||||
docToken = null;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -23,7 +23,7 @@ export const LAST_VERSION_TIMESTAMP_PROPERTY = "LastVersionTimestamp";
|
|||
/** FR-16 — stamp เฉพาะไฟล์ร่าง (draft) ที่เว็บสร้าง: template ต้นทาง (ค่าเดียวกับ AddInMetadata.TemplateIdPropertyName ฝั่ง backend) */
|
||||
export const TEMPLATE_ID_PROPERTY = "TemplateId";
|
||||
|
||||
/** e-Court pass-through — ไฟล์ที่ e-Court stamp มาใน custom properties เข้าใช้ได้เลยไม่ต้องกด login */
|
||||
/** e-Court pass-through — ไฟล์ที่ e-Court stamp มาใน custom properties เข้าใช้ได้เลย (auth ทางเข้าเดียว) */
|
||||
export const ECOURT_TOKEN_PROPERTY = "token";
|
||||
export const ECOURT_OWNER_PROPERTY = "owner";
|
||||
|
||||
|
|
@ -35,7 +35,7 @@ export interface TrackedDocumentProperties {
|
|||
lastVersionTimestamp?: string;
|
||||
/** มีเฉพาะไฟล์ร่าง (FR-16) — ส่งต่อให้ backend ตอน Start Versioning เพื่อผูก Document กับ template */
|
||||
templateId?: string;
|
||||
/** e-Court pass-through — มีอย่างใดอย่างหนึ่ง = ข้ามหน้า login (username จาก owner หรือ uid ใน token) */
|
||||
/** e-Court pass-through — มีอย่างใดอย่างหนึ่ง = ใช้งานได้ทันที (username จาก owner หรือ uid ใน token) */
|
||||
ecourtToken?: string;
|
||||
ecourtOwner?: string;
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue