feat(wordaddin): add e-Court pass-through sign-in
All checks were successful
Deploy eCourt Note Web to Server / Validate Database Schema (Drizzle) (push) Successful in 4s
Deploy eCourt Note Web to Server / Build Web Docker Image (push) Successful in 1m8s
Deploy eCourt Note Web to Server / Deploy eCourt Note Web to Server (push) Successful in 6s
Deploy eCourt Note Web to Server / Notify Deployment Status (push) Successful in 0s
All checks were successful
Deploy eCourt Note Web to Server / Validate Database Schema (Drizzle) (push) Successful in 4s
Deploy eCourt Note Web to Server / Build Web Docker Image (push) Successful in 1m8s
Deploy eCourt Note Web to Server / Deploy eCourt Note Web to Server (push) Successful in 6s
Deploy eCourt Note Web to Server / Notify Deployment Status (push) Successful in 0s
ไฟล์ที่ e-Court stamp custom properties token/owner มาใช้งานได้เลย ไม่ต้องกด login — detect() ของ TaskpaneApp อ่านค่าทั้งสองตอนเปิด task pane แล้วเรียก signInFromDocument(): username จาก owner → uid ใน JWT payload → NEXT_PUBLIC_ADDIN_MOCK_USERNAME, token จาก ไฟล์เก็บใน memory ให้ getAccessToken() ใช้ต่อ decodeJwtField ถอด base64url เป็น bytes แล้วผ่าน TextDecoder ก่อน JSON.parse — uid ที่ไม่ใช่ ASCII (เช่น ชื่อไทย) ไม่เป็น mojibake อัปเดตเอกสาร CLAUDE.md + docs/WORDADDIN-DEV.md พร้อมวิธีทดสอบ เองใน Word ผ่าน Advanced Properties Co-Authored-By: Claude Code <noreply@anthropic.com>
This commit is contained in:
parent
dc851fbb73
commit
2b70a50c62
5 changed files with 78 additions and 9 deletions
|
|
@ -14,6 +14,8 @@ export type LoginResult = { ok: true } | { ok: false; error: string };
|
|||
|
||||
/** state เดียวของ mock — null = signed out */
|
||||
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));
|
||||
|
|
@ -31,7 +33,40 @@ export function signInForDemo(): void {
|
|||
username = config.mockUsername;
|
||||
}
|
||||
|
||||
/** ถอด field จาก JWT payload (base64url) โดยไม่ตรวจ signature — ใช้ดู uid ประกอบชื่อผู้ใช้เท่านั้น */
|
||||
function decodeJwtField(token: string, field: string): string | undefined {
|
||||
const payload = token.split(".")[1];
|
||||
if (!payload) return undefined;
|
||||
try {
|
||||
// atob คืน binary string (1 char = 1 byte) — ต้องถอด UTF-8 ด้วย TextDecoder ก่อน JSON.parse
|
||||
// ไม่งั้น uid ที่ไม่ใช่ ASCII (เช่น ชื่อไทย) จะกลายเป็น mojibake
|
||||
const bytes = Uint8Array.from(atob(payload.replace(/-/g, "+").replace(/_/g, "/")), (char) => char.charCodeAt(0));
|
||||
const json: unknown = JSON.parse(new TextDecoder().decode(bytes));
|
||||
const value = typeof json === "object" && json !== null ? (json as Record<string, unknown>)[field] : undefined;
|
||||
return typeof value === "string" && value !== "" ? value : undefined;
|
||||
} catch {
|
||||
return undefined;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* e-Court pass-through (ระยะแรก) — ไฟล์ที่ e-Court stamp `token`/`owner` ไว้ใน custom properties
|
||||
* ใช้งานได้เลยโดยไม่ต้องกด login: username จาก owner → uid ใน token → mockUsername
|
||||
* คืน true = ผ่านเข้าได้ (มี token หรือ owner อย่างน้อยหนึ่งตัว)
|
||||
*/
|
||||
export function signInFromDocument(input: { token?: string; owner?: string }): boolean {
|
||||
const token = input.token?.trim() || undefined;
|
||||
const owner = input.owner?.trim() || (token ? decodeJwtField(token, "uid") : undefined);
|
||||
if (!token && !owner) {
|
||||
return false;
|
||||
}
|
||||
username = owner ?? config.mockUsername;
|
||||
docToken = token ?? null;
|
||||
return true;
|
||||
}
|
||||
|
||||
export async function getAccessToken(): Promise<string | null> {
|
||||
if (docToken) return docToken;
|
||||
return username ? `mock-token-${username}` : null;
|
||||
}
|
||||
|
||||
|
|
@ -58,4 +93,5 @@ export function startAutoRefresh(): void {
|
|||
|
||||
export function signOut(): void {
|
||||
username = null;
|
||||
docToken = null;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -23,6 +23,10 @@ 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 */
|
||||
export const ECOURT_TOKEN_PROPERTY = "token";
|
||||
export const ECOURT_OWNER_PROPERTY = "owner";
|
||||
|
||||
/** ค่า property ที่หาไม่เจอเป็น `undefined` (ไม่ใช่ throw) */
|
||||
export interface TrackedDocumentProperties {
|
||||
savedByAddIn?: string;
|
||||
|
|
@ -31,6 +35,9 @@ export interface TrackedDocumentProperties {
|
|||
lastVersionTimestamp?: string;
|
||||
/** มีเฉพาะไฟล์ร่าง (FR-16) — ส่งต่อให้ backend ตอน Start Versioning เพื่อผูก Document กับ template */
|
||||
templateId?: string;
|
||||
/** e-Court pass-through — มีอย่างใดอย่างหนึ่ง = ข้ามหน้า login (username จาก owner หรือ uid ใน token) */
|
||||
ecourtToken?: string;
|
||||
ecourtOwner?: string;
|
||||
}
|
||||
|
||||
/** item ใน Custom XML Part `<VersionHistory>` */
|
||||
|
|
@ -41,7 +48,8 @@ export interface XmlVersionEntry {
|
|||
user: string;
|
||||
}
|
||||
|
||||
/** อ่าน custom properties ทั้ง 5 ตัวที่ระบบสนใจ (4 ตัวของ tracked + TemplateId ของไฟล์ร่าง FR-16) */
|
||||
/** อ่าน custom properties ทั้ง 7 ตัวที่ระบบสนใจ (4 ตัวของ tracked + TemplateId ของไฟล์ร่าง FR-16
|
||||
* + token/owner ที่ e-Court stamp มากับไฟล์) */
|
||||
export async function readTrackedProperties(): Promise<TrackedDocumentProperties> {
|
||||
return Word.run(async (context) => {
|
||||
const properties = context.document.properties.customProperties;
|
||||
|
|
@ -50,12 +58,16 @@ export async function readTrackedProperties(): Promise<TrackedDocumentProperties
|
|||
const lastVersionId = properties.getItemOrNullObject(LAST_VERSION_ID_PROPERTY);
|
||||
const lastVersionTimestamp = properties.getItemOrNullObject(LAST_VERSION_TIMESTAMP_PROPERTY);
|
||||
const templateId = properties.getItemOrNullObject(TEMPLATE_ID_PROPERTY);
|
||||
const ecourtToken = properties.getItemOrNullObject(ECOURT_TOKEN_PROPERTY);
|
||||
const ecourtOwner = properties.getItemOrNullObject(ECOURT_OWNER_PROPERTY);
|
||||
// load "value" ของ null-object proxy ก็ทำให้ isNullObject แม่นหลัง sync
|
||||
savedByAddIn.load("value");
|
||||
documentTrackingId.load("value");
|
||||
lastVersionId.load("value");
|
||||
lastVersionTimestamp.load("value");
|
||||
templateId.load("value");
|
||||
ecourtToken.load("value");
|
||||
ecourtOwner.load("value");
|
||||
await context.sync();
|
||||
|
||||
const read = (property: Word.CustomProperty): string | undefined =>
|
||||
|
|
@ -69,6 +81,8 @@ export async function readTrackedProperties(): Promise<TrackedDocumentProperties
|
|||
lastVersionId: read(lastVersionId),
|
||||
lastVersionTimestamp: read(lastVersionTimestamp),
|
||||
templateId: read(templateId),
|
||||
ecourtToken: read(ecourtToken),
|
||||
ecourtOwner: read(ecourtOwner),
|
||||
};
|
||||
});
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue