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
|
|
@ -3,7 +3,7 @@
|
|||
"use client";
|
||||
|
||||
import { useCallback, useEffect, useState } from "react";
|
||||
import { FileUp, GitCommitHorizontal, LogOut } from "lucide-react";
|
||||
import { FileUp, GitCommitHorizontal } from "lucide-react";
|
||||
|
||||
import { autoSaveService } from "@/wordaddin/services/autoSaveService";
|
||||
import * as authService from "@/wordaddin/services/authService";
|
||||
|
|
@ -23,22 +23,25 @@ import TemplateToolsPanel from "./TemplateToolsPanel";
|
|||
import VersionHistoryPanel from "./VersionHistoryPanel";
|
||||
|
||||
/**
|
||||
* Task pane หลัก — state machine ตาม Phase 5:
|
||||
* checking → (ไม่มี token) signed-out
|
||||
* → (มี token + ไม่ใช่ tracked document) untracked
|
||||
* → (มี token + tracked document) tracking
|
||||
* พอร์ตจาก App.tsx ต้นฉบับ (D:\word-addin-poc\addin) — state/handlers คงเดิมทุกตัว
|
||||
* เปลี่ยนเฉพาะ: import path, JSX → Genesis/Tailwind, ถอด FluentProvider/makeStyles,
|
||||
* Task pane หลัก — state machine:
|
||||
* checking → (ไม่มี token/owner ในไฟล์) no-access
|
||||
* → (มี token/owner + ไม่ใช่ tracked document) untracked
|
||||
* → (มี token/owner + tracked document) tracking
|
||||
* ไม่มี login ตั้งแต่ยกเลิก Keycloak (2026-10-27) — auth เข้าทางเดียวคือ e-Court
|
||||
* pass-through: `detect()` อ่าน token/owner จาก custom properties แล้วเรียก
|
||||
* `authService.signInFromDocument` ผ่านเลย
|
||||
* พอร์ตจาก App.tsx ต้นฉบับ (D:\word-addin-poc\addin) — เปลี่ยน: import path,
|
||||
* JSX → Genesis/Tailwind, ถอด FluentProvider/makeStyles, ถอดหน้า login,
|
||||
* เพิ่ม prop demo (โหมด ?addindemo ของ WordAddinGate — ตั้ง phase ตรง ไม่ detect จริง)
|
||||
*
|
||||
* กติกา: ทุก async ต้อง catch — AuthRequiredError → กลับ signed-out อย่างเนียน (AC-9)
|
||||
* กติกา: ทุก async ต้อง catch — AuthRequiredError → หน้า no-access ให้ขอไฟล์ใหม่จาก e-Court
|
||||
* error อื่นแสดง MessageBar ไม่ปล่อย unhandled rejection
|
||||
*
|
||||
* FR-17 — auto-save (local เท่านั้น) เริ่มตอน mount ไม่รอ sign-in; FR-3 reminder
|
||||
* FR-17 — auto-save (local เท่านั้น) เริ่มตอน mount ไม่ผูกกับ phase; FR-3 reminder
|
||||
* กลายเป็น flag `uncommittedSinceVersion` ที่ periodic tick ของ autoSaveService ตั้งค่า
|
||||
*/
|
||||
|
||||
type Phase = "checking" | "signed-out" | "untracked" | "tracking";
|
||||
type Phase = "checking" | "no-access" | "untracked" | "tracking";
|
||||
|
||||
/** แท็บของ taskpane — versioning คือทุกอย่างเดิม, template คือเครื่องมือจัดการ Content Controls
|
||||
* (Office.js ล้วน ไม่ยิง backend จึงใช้ได้ทั้ง untracked และ tracking) */
|
||||
|
|
@ -108,8 +111,8 @@ const TaskpaneApp: React.FC<{ demo?: "untracked" | "tracking" }> = ({ demo }) =>
|
|||
const [historyLoading, setHistoryLoading] = useState(false);
|
||||
const [historyError, setHistoryError] = useState<string | null>(null);
|
||||
|
||||
const [signingIn, setSigningIn] = useState(false);
|
||||
const [loginError, setLoginError] = useState<string | null>(null);
|
||||
/** ข้อความบนหน้า no-access — ต่างกันตามสาเหตุ (ไม่มี token ในไฟล์ / token หมดอายุ) */
|
||||
const [noAccessNotice, setNoAccessNotice] = useState<string | null>(null);
|
||||
|
||||
const [starting, setStarting] = useState(false);
|
||||
const [saving, setSaving] = useState(false);
|
||||
|
|
@ -135,15 +138,16 @@ const TaskpaneApp: React.FC<{ demo?: "untracked" | "tracking" }> = ({ demo }) =>
|
|||
/** FR-3 — periodic tick เห็นการแก้ไขหลัง version ล่าสุด (แสดงเฉพาะตอน tracking) */
|
||||
const [hasUncommitted, setHasUncommitted] = useState(false);
|
||||
|
||||
/** 401 ที่ refresh ไม่ช่วย → กลับหน้า sign-in อย่างเนียน (AC-9) */
|
||||
/** AuthRequiredError (ไม่มี token / 401) → หน้า no-access — token จากไฟล์ต่ออายุไม่ได้
|
||||
* ผู้ใช้ต้องขอไฟล์ใหม่จาก e-Court จึงรีเซ็ต state ทั้งชุดกันใช้ของเก่าต่อ */
|
||||
const handleAuthRequired = useCallback((error: unknown): boolean => {
|
||||
if (error instanceof AuthRequiredError) {
|
||||
setPhase("signed-out");
|
||||
setPhase("no-access");
|
||||
setActiveTab("versioning");
|
||||
setDocumentId(null);
|
||||
setDocumentTemplateId(null);
|
||||
setVersions([]);
|
||||
setLoginError("เซสชันหมดอายุ — กรุณา login ใหม่");
|
||||
setNoAccessNotice("token ของ e-Court หมดอายุหรือไม่ถูกต้อง — โปรดขอไฟล์ใหม่จาก e-Court แล้วเปิดอีกครั้ง");
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
|
|
@ -166,32 +170,31 @@ const TaskpaneApp: React.FC<{ demo?: "untracked" | "tracking" }> = ({ demo }) =>
|
|||
[handleAuthRequired],
|
||||
);
|
||||
|
||||
/** ตรวจตอนเปิด task pane (FR-1): token → SavedByAddIn → history
|
||||
/** ตรวจตอนเปิด task pane (FR-1): token/owner ในไฟล์ → SavedByAddIn → history
|
||||
* FR-16 — อ่าน properties ครั้งเดียวทั้งชุดแล้ว derive tracked เอง เพราะไฟล์ร่าง (untracked)
|
||||
* ต้องได้อ่าน TemplateId ไปใช้ตอน Start Versioning ด้วย (ข้ามทั้งหมดในโหมด demo)
|
||||
* e-Court pass-through — ยังไม่มี session แต่ไฟล์ stamp token/owner มา = ผ่านเข้าเลยไม่ต้องกด login */
|
||||
* e-Court pass-through — auth ทางเข้าเดียว: ไฟล์ที่ stamp token/owner มา = ผ่านเข้าเลย
|
||||
* ไม่มีขั้นตอน login (นอก Word เช่น browser ธรรมดา → อ่านไม่ได้ = ไป no-access) */
|
||||
const detect = useCallback(async () => {
|
||||
setPhase("checking");
|
||||
setBootError(null);
|
||||
try {
|
||||
let properties: metadataService.TrackedDocumentProperties | null = null;
|
||||
if (!(await authService.getAccessToken())) {
|
||||
// e-Court pass-through — อ่านไฟล์ก่อนตัดสิน: ไฟล์ที่ stamp token/owner มา = ผ่านเลยไม่ต้องกด login
|
||||
// (นอก Word เช่น browser ธรรมดา → อ่านไม่ได้ = ไป signed-out ตามเดิม)
|
||||
try {
|
||||
properties = await metadataService.readTrackedProperties();
|
||||
} catch {
|
||||
properties = null;
|
||||
}
|
||||
if (
|
||||
!properties ||
|
||||
!authService.signInFromDocument({ token: properties.ecourtToken, owner: properties.ecourtOwner })
|
||||
) {
|
||||
setPhase("signed-out");
|
||||
return;
|
||||
}
|
||||
try {
|
||||
properties = await metadataService.readTrackedProperties();
|
||||
} catch {
|
||||
properties = null;
|
||||
}
|
||||
if (
|
||||
!properties ||
|
||||
!authService.signInFromDocument({ token: properties.ecourtToken, owner: properties.ecourtOwner })
|
||||
) {
|
||||
setNoAccessNotice(
|
||||
"ไม่พบ token/owner ของ e-Court ในไฟล์ — ขณะนี้ add-in ใช้ได้กับเอกสารที่ส่งมาจาก e-Court เท่านั้น",
|
||||
);
|
||||
setPhase("no-access");
|
||||
return;
|
||||
}
|
||||
authService.startAutoRefresh();
|
||||
setUsername(authService.getUsername() ?? null);
|
||||
|
||||
properties ??= await metadataService.readTrackedProperties();
|
||||
|
|
@ -217,7 +220,7 @@ const TaskpaneApp: React.FC<{ demo?: "untracked" | "tracking" }> = ({ demo }) =>
|
|||
void detect();
|
||||
}, [demo, detect]);
|
||||
|
||||
/** FR-17 — เริ่ม auto-save ตั้งแต่ mount ไม่รอ sign-in (ข้ามใน demo กัน error banner จาก Word ที่ไม่มี)
|
||||
/** FR-17 — เริ่ม auto-save ตั้งแต่ mount ไม่ผูกกับ phase (ข้ามใน demo กัน error banner จาก Word ที่ไม่มี)
|
||||
* ผูก listener ให้ React state: error → banner, dirty→save → flag FR-3 */
|
||||
useEffect(() => {
|
||||
autoSaveService.onError = (error) =>
|
||||
|
|
@ -235,34 +238,6 @@ const TaskpaneApp: React.FC<{ demo?: "untracked" | "tracking" }> = ({ demo }) =>
|
|||
};
|
||||
}, [demo]);
|
||||
|
||||
const handleSignIn = async () => {
|
||||
setSigningIn(true);
|
||||
setLoginError(null);
|
||||
try {
|
||||
const result = await authService.login();
|
||||
if (result.ok) {
|
||||
await detect();
|
||||
} else {
|
||||
setLoginError(result.error);
|
||||
}
|
||||
} catch (error) {
|
||||
setLoginError(getErrorMessage(error, "เข้าสู่ระบบไม่สำเร็จ"));
|
||||
} finally {
|
||||
setSigningIn(false);
|
||||
}
|
||||
};
|
||||
|
||||
const handleSignOut = () => {
|
||||
authService.signOut();
|
||||
setUsername(null);
|
||||
setDocumentId(null);
|
||||
setDocumentTemplateId(null);
|
||||
setVersions([]);
|
||||
setLoginError(null);
|
||||
setPhase("signed-out");
|
||||
setActiveTab("versioning");
|
||||
};
|
||||
|
||||
/** FR-18 [บันทึก] — local save เดี่ยวผ่าน lock ของ autoSaveService (ไม่ stamp metadata อะไร) */
|
||||
const handleSaveOnly = async () => {
|
||||
setSaveBusy(true);
|
||||
|
|
@ -408,18 +383,15 @@ const TaskpaneApp: React.FC<{ demo?: "untracked" | "tracking" }> = ({ demo }) =>
|
|||
<div className="flex min-h-screen flex-col gap-2 p-4">
|
||||
<div className="flex items-center justify-between gap-2 border-b border-line pb-2">
|
||||
<span className="font-display text-sm font-semibold leading-tight text-ink">e-Court Documents Versioning</span>
|
||||
{phase !== "signed-out" && phase !== "checking" && username ? (
|
||||
/* จอแคบ: username ย้ายไป title/aria-label — ไม่กินพื้นที่แถบหัวจนตัวชื่อแอป wrap 3-4 บรรทัด */
|
||||
<button
|
||||
type="button"
|
||||
className="focus-ring inline-flex shrink-0 items-center gap-1 rounded-md px-2 py-1.5 text-xs text-ink-soft transition hover:text-primary"
|
||||
onClick={handleSignOut}
|
||||
aria-label={`ออกจากระบบ (${username})`}
|
||||
title={`${username} — ออกจากระบบ`}
|
||||
{phase !== "no-access" && phase !== "checking" && username ? (
|
||||
/* ไม่มีปุ่มออกจากระบบ (ไม่มี login ให้ออก) — แสดงชื่อเจ้าของไฟล์จาก pass-through เฉย ๆ
|
||||
จอแคบ: ตัดด้วย truncate + เต็ม ๆ ใน title ไม่กินพื้นที่แถบหัวจนตัวชื่อแอป wrap */
|
||||
<span
|
||||
className="shrink-0 max-w-40 truncate rounded-md bg-canvas px-2 py-1.5 text-xs text-ink-soft"
|
||||
title={username}
|
||||
>
|
||||
<LogOut className="size-3.5" aria-hidden />
|
||||
ออกจากระบบ
|
||||
</button>
|
||||
{username}
|
||||
</span>
|
||||
) : null}
|
||||
</div>
|
||||
|
||||
|
|
@ -430,7 +402,7 @@ const TaskpaneApp: React.FC<{ demo?: "untracked" | "tracking" }> = ({ demo }) =>
|
|||
<MessageBar intent="warning">{autoSaveError}</MessageBar>
|
||||
) : null}
|
||||
|
||||
{/* toggle แท็บ — แสดงเฉพาะ phase ที่เข้าถึงเอกสารแล้ว (checking/signed-out ยังไม่มีอะไรให้จัดการ)
|
||||
{/* toggle แท็บ — แสดงเฉพาะ phase ที่เข้าถึงเอกสารแล้ว (checking/no-access ยังไม่มีอะไรให้จัดการ)
|
||||
ปิดทั้งสองปุ่มระหว่าง flow ที่เขียนเอกสาร: saving/starting ของ versioning (กัน progress หาย
|
||||
กลาง flow — handler อยู่ที่ component นี้ flow ยังรันจนจบ แค่หน้าตา), saveBusy ของบันทึก
|
||||
local/ปิดเอกสาร และ templateBusy ที่ panel รายงานผ่าน onBusyChange — กันเครื่องมือ template
|
||||
|
|
@ -481,21 +453,13 @@ const TaskpaneApp: React.FC<{ demo?: "untracked" | "tracking" }> = ({ demo }) =>
|
|||
</div>
|
||||
) : null}
|
||||
|
||||
{phase === "signed-out" ? (
|
||||
{phase === "no-access" ? (
|
||||
/* ไม่มี login ให้กด — บอกสาเหตุและจบที่นั้น (ขั้นตอนสำหรับเอกสารทั่วไปจะมีในอนาคต) */
|
||||
<div className="flex flex-col gap-2 pt-4">
|
||||
<p className="text-base text-ink">เข้าสู่ระบบเพื่อเริ่มใช้งาน versioning</p>
|
||||
<p className="text-base text-ink">เอกสารนี้ยังใช้งาน add-in ไม่ได้</p>
|
||||
<p className="text-sm text-ink-soft">
|
||||
ใช้บัญชีของ realm <code>word-versioning</code> — หน้าต่าง login จะเปิดแยกต่างหาก
|
||||
{noAccessNotice ?? "ไม่พบ token/owner ของ e-Court ในไฟล์ — ขณะนี้ add-in ใช้ได้กับเอกสารที่ส่งมาจาก e-Court เท่านั้น"}
|
||||
</p>
|
||||
<button
|
||||
type="button"
|
||||
className={`${BTN_PRIMARY} mt-2 w-full justify-center`}
|
||||
disabled={signingIn}
|
||||
onClick={() => void handleSignIn()}
|
||||
>
|
||||
{signingIn ? "กำลังเข้าสู่ระบบ…" : "เข้าสู่ระบบ"}
|
||||
</button>
|
||||
{loginError ? <MessageBar intent="warning">{loginError}</MessageBar> : null}
|
||||
</div>
|
||||
) : null}
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue