feat(addin): implement e-Court ticket auth (ticket_data + HmacSha256)
Replace the token/owner pass-through with the ticket flow from
docs/requirement/addin-workflow.md (full plan: docs/PLAN-ADDIN-AUTH.md):
- utils/docver-ticket.ts: shared pure ticket parse + hex digest helpers,
client/server inject their own crypto (Web Crypto / node:crypto)
- lib/addin-ticket.ts + POST /api/documents/token second entry: body
{ticket_data, HmacSha256} verified server-side (HMAC timing-safe + exp,
secret ADDIN_TICKET_SECRET -> SECRET -> "super_secret"); x-api-key path
unchanged
- wordaddin: ticketService (Web Crypto HMAC, localStorage session
wordaddin-auth keyed by fid, same-origin token request), authService
rewrite (7-step flow, silent renew), apiClient 401 renew-retry once,
metadataService reads ticket_data/HmacSha256, no-access messages per
cause
- scripts/make-ticket-docx.mjs: dev tool to stamp tickets onto any .docx
for testing in real Word
- app/wordaddin/error.tsx: error boundary showing the real error + stack
in the pane (webviews have no devtools)
- tests: docver-ticket + addin-ticket unit suites (21 cases) + temporary
crash repro suite for the ongoing client-crash debug session
- docs sync: PLAN-DOCVER, WORDADDIN-DEV, addin-ui-flow, openapi (two
request bodies for /token), CLAUDE.md, .env.example
Co-Authored-By: Claude Code <noreply@anthropic.com>
This commit is contained in:
parent
8ebe5f8644
commit
cffc571da5
21 changed files with 1725 additions and 124 deletions
215
docs/PLAN-ADDIN-AUTH.md
Normal file
215
docs/PLAN-ADDIN-AUTH.md
Normal file
|
|
@ -0,0 +1,215 @@
|
|||
# PLAN-ADDIN-AUTH — Add-in Ticket Authen (`ticket_data` + `HmacSha256`)
|
||||
|
||||
> วันที่ตกลงแผน: 2026-10-27 · สถานะ: แผนรออนุมัติ · requirement ต้นทาง: `docs/requirement/addin-workflow.md`
|
||||
|
||||
## 0. บริบท/เป้าหมาย
|
||||
|
||||
เปลี่ยน auth ของ wordaddin จาก e-Court pass-through แบบเดิม (e-Court ขอ token เองแล้ว stamp
|
||||
`token`/`owner` ลงไฟล์) เป็น **ticket** ตาม requirement:
|
||||
|
||||
- e-Court stamp custom properties 2 ค่าในไฟล์ .docx: `ticket_data` (JSON) + `HmacSha256`
|
||||
- addin ตรวจ HMAC เองฝั่ง client (gate เชิง UX — ตัดเคสไฟล์ถูกแก้ก่อนยิง network)
|
||||
- **addin เป็นฝ่ายขอ access_token** ที่ `POST /api/documents/token` เก็บใน localStorage
|
||||
ตาม fid (session ต่อ userId) — ไม่ใช่ e-Court อีกต่อไป
|
||||
- ถอดระบบเก่า `token`/`owner` ออกทั้งหมด (implement ไว้ 2026-10-27 ยังไม่มีไฟล์จริงใช้ —
|
||||
ไม่ต้องรองรับสองแบบพร้อมกัน)
|
||||
|
||||
## 1. ข้อตกลงที่ตัดสินใจแล้ว (2026-10-27)
|
||||
|
||||
| # | เรื่อง | ข้อตกลง |
|
||||
|---|---|---|
|
||||
| D1 | addin ยืนยันกับ `/token` อย่างไร | **ขยาย `POST /api/documents/token` รับ ticket เพิ่ม** — body `{ticket_data, HmacSha256}` server ตรวจ HMAC+exp ด้วย secret ใน env ฝั่ง server ก่อนออก token · ทางเข้า `x-api-key` เดิม (e-Court server-to-server) ใช้ต่อคู่กัน |
|
||||
| D2 | โค้ดซ้ำสองฝั่ง | แชร์ส่วน pure ผ่าน **`utils/docver-ticket.ts`** (โซนกลาง client ใช้ได้ — ไม่แตะกติกา "wordaddin ห้าม import จาก `lib/`") · ฝั่ง crypto ฉีดเข้าทาง parameter: client = Web Crypto, server = `node:crypto` |
|
||||
| D3 | client-side HMAC | ทำตาม requirement (sign เทียบเองก่อน) โดย secret มาจาก `NEXT_PUBLIC_ADDIN_TICKET_SECRET` — **ยอมรับว่าค่าใน bundle สกัดได้** จึงถือเป็นแค่ UX gate, ความมั่นคงจริงอยู่ที่ server verify |
|
||||
| D4 | รูปแบบ HMAC | sign บน **string ดิบของ `ticket_data` ตามที่อ่านได้จาก property** (ห้าม trim/normalize) · digest เป็น **hex lowercase 64 ตัว** — การเทียบ normalize เป็น lowercase ก่อน (ฝั่ง e-Court stamp ตัวพิมพ์ใหญ่ก็ผ่าน) |
|
||||
| D5 | `tid` | = `template` ของ claims (`tid` optional → `template: null`) · map `uid→userid`, `uname→username`, `fid` ตรง ๆ |
|
||||
| D6 | token หมดอายุ (TTL ~1 ชม.) แต่ ticket ยังไม่หมด | เก็บ `exp` ของ token (decode จาก JWT) — ก่อนใช้ถ้าหมดอายุและ ticket ยังไม่หมด → **renew อัตโนมัติ** · โดน 401 กลาง session → renew จาก ticket 1 ครั้งแล้ว retry ก่อนไป no-access |
|
||||
| D7 | localStorage | key เดียว `wordaddin-auth` เก็บ JSON `{v:1, userId, userName, tokens: {[fid]: {token, exp}}}` · เจอ user ใหม่ = **เคลียร์ทั้งก้อน** (รวม token ทุก fid ของ user เก่า) · webview block localStorage = fallback memory (pattern `versioningService`) |
|
||||
| D8 | "disable Add-in ทั้งหมด" | = phase `no-access` ที่มีอยู่ โดยแยกข้อความตามสาเหตุ: ไม่พบ ticket / ไฟล์ถูกแก้ไข (HMAC) / ticket หมดอายุ |
|
||||
| D9 | ช่วง mock (`NEXT_PUBLIC_ADDIN_API_BASE_URL` ว่าง) | addin ยิงขอ token จริงที่ same-origin `withBasePath("/api/documents/token")` (ไม่ผูกกับ apiBaseUrl ของ versioning) — versioning ยัง mock ต่อ |
|
||||
| D10 | เครื่องมือทดสอบ | มีสคริปต์ stamp ticket ลงไฟล์ .docx ตัวอย่าง (`scripts/make-ticket-docx.mjs`) — ไม่งั้นทดสอบ flow นี้ใน Word จริงไม่ได้เลย |
|
||||
|
||||
## 2. Contract ของ ticket (ต้องส่งให้ทีม e-Court ฝั่ง stamp)
|
||||
|
||||
Custom properties ของไฟล์ .docx (ชื่อ key ต้องตรงเป๊ะ — case-sensitive):
|
||||
|
||||
```
|
||||
ticket_data = {"exp":1793067433,"fid":"T12_FD155_3s-long_ZZZ","tid":"T12","uid":"oom","uname":"สรวิชญ์"}
|
||||
HmacSha256 = <hex 64 ตัว lowercase> = HMAC-SHA256(secret, ticket_data ดิบทั้ง string ไม่ trim)
|
||||
```
|
||||
|
||||
| field | type | เงื่อนไข |
|
||||
|---|---|---|
|
||||
| `exp` | int (unix วินาที) | บังคับ · `exp <= now` = หมดอายุ |
|
||||
| `fid` | string | บังคับ · trim 1–200 · เป็น `docId` ของทุก operation ของกลุ่ม docver |
|
||||
| `uid` | string | บังคับ · trim 1–100 |
|
||||
| `uname` | string | บังคับ · trim 1–200 |
|
||||
| `tid` | string | optional · ≤100 · template ต้นทาง (ไม่มี = `template: null`) |
|
||||
|
||||
- field อื่นปนมา = reject (allowlist ตาม convention ของ repo)
|
||||
- secret ปัจจุบัน = `"super_secret"` — **e-Court + `ADDIN_TICKET_SECRET` (server) +
|
||||
`NEXT_PUBLIC_ADDIN_TICKET_SECRET` (client) ต้องตรงกันทั้งสาม**
|
||||
- ⚠️ **custom property ของ Word จำกัด value ~255 ตัวอักษร** — JSON ของ `ticket_data`
|
||||
ต้องออกแบบสั้น (fid/tid/uname ยาวมากจะทะลุ; ฝั่ง addin เจอค่าเกิน = ถือว่าไฟล์เสียหาย → no-access)
|
||||
|
||||
## 3. ฝั่ง server — `POST /api/documents/token` รับ ticket เพิ่ม
|
||||
|
||||
Branch ตาม header (ตัวตั้ง = `x-api-key`):
|
||||
|
||||
| เคส | พฤติกรรม |
|
||||
|---|---|
|
||||
| มี header `x-api-key` | **ทางเดิมทุกอย่าง** — `requireDocverApiKey` + `parseTokenRequestBody` (body allowlist `userid`/`username`/`fid`/`template`, ส่ง field ticket ปนมา = 400 ตาม allowlist เดิม) |
|
||||
| ไม่มี `x-api-key` + body `{ticket_data: string, HmacSha256: string}` | **ทางใหม่** — `verifyAddinTicket` → map claims → `issueDocverToken` → response รูปเดียวกัน `{access_token, token_type, expires_in}` |
|
||||
| ไม่มี `x-api-key` + body ไม่ใช่ object / มี field อื่นปน / field ไม่ใช่ string | 400 (รูปผิดก่อนถึงการ verify) |
|
||||
| HMAC ผิด / parse ticket ไม่ผ่าน / `exp` หมดอายุ | 401 **ข้อความเดียวกันทุกสาเหตุ** (กัน oracle — addin รู้สาเหตุจริงจากการตรวจเองฝั่ง client อยู่แล้ว) |
|
||||
| ไม่มี `x-api-key` และไม่มี ticket fields เลย | 401 |
|
||||
| `DOCVER_TOKEN_SECRET` ไม่ตั้ง | 500 (เดิม) |
|
||||
|
||||
- การเทียบ digest ฝั่ง server: hash ทั้งสองฝั่งด้วย SHA-256 แล้ว `timingSafeEqual` — pattern
|
||||
`verifyDocverToken` ของ `lib/docver-token.ts` เป๊ะ
|
||||
- middleware ยกเว้น `api/documents/*` อยู่แล้ว — ไม่ต้องแต้
|
||||
|
||||
## 4. โค้ดแชร์ — `utils/docver-ticket.ts` (pure, ไม่มี dependency เลย)
|
||||
|
||||
```ts
|
||||
export interface DocverTicket { exp: number; fid: string; tid: string | null; uid: string; uname: string }
|
||||
export function parseDocverTicket(raw: string | unknown): { ok: true; ticket: DocverTicket } | { ok: false; error: string }
|
||||
export function isTicketExpired(exp: number, nowSec?: number): boolean // exp <= now
|
||||
export function normalizeHexDigest(value: string): string | null // trim+lowercase+ตรวจ ^[0-9a-f]{64}$
|
||||
export function timingSafeEqualHex(a: string, b: string): boolean // constant-time บน hex ที่ normalize แล้ว
|
||||
export type HmacSha256Hex = (message: string, secret: string) => Promise<string> // adapter ที่สองฝั่งฉีด
|
||||
```
|
||||
|
||||
- `lib/` (server) กับ `wordaddin/` (client) ใช้ `parseDocverTicket`/`isTicketExpired` ตัวเดียวกัน —
|
||||
schema drift กันได้จากฝั่งเดียว
|
||||
|
||||
## 5. ฝั่ง addin — flow ตอนเปิดเอกสาร (map ขั้นตอน 1–7 ของ requirement)
|
||||
|
||||
`TaskpaneApp.detect()` → `authService.signInWithTicket({ticketData, ticketHmac})`:
|
||||
|
||||
```
|
||||
1. อ่าน properties ticket_data + HmacSha256 (readTrackedProperties)
|
||||
2. ขาดข้อใดข้อหนึ่ง / ค่ายาวเกิน 255 → no-access "ไม่พบ ticket ของ e-Court ในไฟล์"
|
||||
parseDocverTicket ไม่ผ่าน → no-access "ไฟล์ถูกแก้ไข"
|
||||
HMAC (Web Crypto + NEXT_PUBLIC_ADDIN_TICKET_SECRET) ไม่ตรง → no-access "ไฟล์ถูกแก้ไข"
|
||||
3. session = localStorage["wordaddin-auth"] (fallback memory)
|
||||
4. ไม่มี session หรือ uid ≠ session.userId (user ใหม่):
|
||||
· ticket หมดอายุ → no-access "ticket หมดอายุ"
|
||||
· เคลียร์ session ทั้งก้อน → ขอ token (ทาง ticket ของ /token) → fail → no-access "ขอ token ไม่สำเร็จ"
|
||||
· เก็บ session ใหม่ {userId: uid, userName: uname, tokens: {[fid]: {token, exp}}}
|
||||
5. uid ตรงกัน (คนเดิม):
|
||||
· tokens[fid] มี (กลับมาทำเอกสารเดิม) → ผ่านเลย ไม่ตรวจ ticket exp (ตามเอกสาร)
|
||||
— token หมดอายุ + ticket ยังไม่หมด → renew เงียบ ๆ ก่อน (D6)
|
||||
— token หมดอายุ + ticket หมดด้วย → เข้า UI ได้แต่ API จะ 401 → renew fail → no-access ตอนใช้งาน
|
||||
· tokens[fid] ไม่มี (เอกสารใหม่) → ตรวจ exp แล้วขอ token เก็บเหมือนข้อ 4
|
||||
6. ตั้ง state ภายใน (fid/token/username/ticket ดิบไว้ renew) → ไหลต่อ phase
|
||||
untracked/tracking เดิม (SavedByAddIn → history) ไม่เปลี่ยน
|
||||
```
|
||||
|
||||
- `authService` surface ใหม่: `signInWithTicket` / `getAccessToken` (token ของ fid ปัจจุบัน) /
|
||||
`renewTokenFromTicket` / `getUsername` — ถอด `signInFromDocument` + `decodeJwtField` สำหรับ uid
|
||||
(ticket มี uid/uname ตรง ๆ แล้ว เหลือ decode เฉพาะ `exp` ของ JWT ตอนเก็บ token)
|
||||
- `apiClient` ตอนโดน 401: `renewTokenFromTicket()` → สำเร็จ = retry request 1 ครั้ง → ยัง 401/ไม่มี
|
||||
ticket = `AuthRequiredError` (UI ไป no-access — เดิมมีอยู่แล้ว)
|
||||
- `?addindemo` ยังใช้ได้ (`signInForDemo` คงเดิม)
|
||||
|
||||
## 6. ไฟล์ที่สร้าง/แต้/ถอด
|
||||
|
||||
**สร้าง**
|
||||
- `utils/docver-ticket.ts` — §4
|
||||
- `lib/addin-ticket.ts` — server-only: `addinTicketSecret()` (env `ADDIN_TICKET_SECRET` trim หรือ default
|
||||
`"super_secret"`) + `verifyAddinTicket(ticketData, hmacHex, secret, nowSec?)` (HMAC `node:crypto` +
|
||||
timing-safe + parse + exp) + `ticketToDocverClaims(ticket)` — ห้าม import `@/db`/minio (unit test รันได้เปล่า ๆ)
|
||||
- `wordaddin/services/ticketService.ts` — client: `verifyTicketHmacWeb` (crypto.subtle), session store
|
||||
(`loadSession`/`saveSession`/`clearSession` — try/catch + memory fallback), `requestDocverToken`
|
||||
(POST `withBasePath("/api/documents/token")` body ticket)
|
||||
- `scripts/make-ticket-docx.mjs` — §9
|
||||
- `tests/utils/docver-ticket.test.ts`, `tests/lib/addin-ticket.test.ts` (vitest — pattern `tests/lib/docver-token.test.ts`)
|
||||
|
||||
**แต้**
|
||||
- `app/api/documents/token/route.ts` — branch ตาม §3
|
||||
- `wordaddin/config.ts` — `ticketSecret: process.env.NEXT_PUBLIC_ADDIN_TICKET_SECRET ?? "super_secret"`
|
||||
- `wordaddin/services/metadataService.ts` — ถอด `ECOURT_TOKEN_PROPERTY`/`ECOURT_OWNER_PROPERTY` →
|
||||
`TICKET_DATA_PROPERTY = "ticket_data"` + `TICKET_HMAC_PROPERTY = "HmacSha256"` (`readTrackedProperties`
|
||||
อ่านคู่นี้แทน `ecourtToken`/`ecourtOwner`)
|
||||
- `wordaddin/services/authService.ts` — rewrite ตาม §5
|
||||
- `wordaddin/services/apiClient.ts` — renew-retry ตอน 401
|
||||
- `components/wordaddin/TaskpaneApp.tsx` — `detect()` เรียก `signInWithTicket` + ข้อความ no-access 3 แบบ
|
||||
- `lib/openapi.ts` — POST /token มี 2 รูป body (oneOf: identity+x-api-key / ticket)
|
||||
- `.env.example`, `docs/WORDADDIN-DEV.md`, `docs/addin-ui-flow.md`, `docs/PLAN-DOCVER.md` (§token ชี้มาที่นี่), `CLAUDE.md`
|
||||
|
||||
**ถอด**
|
||||
- ระบบ `token`/`owner` ทั้งเส้น (properties + `signInFromDocument` + `decodeJwtField` ส่วน uid) —
|
||||
แทนด้วย ticket เต็มตัว
|
||||
|
||||
## 7. Env
|
||||
|
||||
| env | default | หมายเหตุ |
|
||||
|---|---|---|
|
||||
| `ADDIN_TICKET_SECRET` | `SECRET` ของกลุ่ม template → `"super_secret"` | server ตรวจ HMAC ทาง ticket ของ /token — อ่านตามลำดับ `ADDIN_TICKET_SECRET` → `SECRET` (deployment เดียวกับตัว stamp ของกลุ่ม template มักตั้งไว้แล้ว) → `"super_secret"` · **production ต้องตั้งค่าใหม่** (ค่า default รู้กันหมด = ปลอม ticket ได้) และต้องตรงกับฝั่ง e-Court · ห้าม log ค่าดิบ |
|
||||
| `NEXT_PUBLIC_ADDIN_TICKET_SECRET` | `"super_secret"` | ฝั่ง addin (UX gate) — inline ลง bundle · ต้องตรงกับฝั่ง e-Court เช่นกัน |
|
||||
|
||||
> หมายเหตุจากตอน implement: ตัว stamp ticket จริงคือกลุ่ม template
|
||||
> (`app/api/template/render/route.ts` → `applyTemplate` — sort key ตามตัวอักษรหลังเติม
|
||||
> `exp` แล้ว HMAC hex lowercase ผ่าน `lib/lib-utils.ts` + stamp ลงไฟล์ด้วย
|
||||
> `setCustomDocProps` ของ `lib/lib-tmpl.ts` อ่าน env `SECRET` กับ `TICKET_SEC` default
|
||||
> 300 วิ — ไฟล์ยังมี property `Ed25519` เพิ่มด้วยแต่ out of scope ของแผนนี้)
|
||||
|
||||
(เลือก default แทน fail-closed ตาม requirement ที่ระบุค่าปัจจุบันไว้ — dev ใช้ได้ทันที
|
||||
แต่ comment เตือน production ชัด ๆ ทั้ง `.env.example` และแผนนี้)
|
||||
|
||||
## 8. Edge cases
|
||||
|
||||
- **property ยาวเกิน 255** → ไฟล์เสียหาย → no-access (Word ตัดค่าทิ้งจะ parse/HMAC ไม่ผ่านอยู่แล้ว —
|
||||
เช็คความยาวเพื่อข้อความที่ชัดกว่า)
|
||||
- **localStorage ถูก block** (webview บางตัว/private mode) → session อยู่ใน memory ต่อ session เดียว
|
||||
(เปิดใหม่เริ่มใหม่ ขอ token ใหม่ — ยอมรับได้)
|
||||
- **uname ไทย** — เป็น string ตรง ๆ ไม่ผ่าน base64 ไม่มีปัญหา mojibake (ต่างจาก decode JWT payload)
|
||||
- **exp == now** → หมดอายุ
|
||||
- **401 จาก endpoint อื่นระหว่างใช้งาน** → renew 1 ครั้ง → retry → ยังไม่ผ่าน → no-access
|
||||
- **Word เปิดหลายเอกสารพร้อมกัน (user เดียวกัน fid ต่างกัน)** — tokens map รองรับ; current fid เป็น
|
||||
state ต่อ taskpane instance (memory) — แยกกันอยู่แล้ว
|
||||
- **ไฟล์ทั่วไปไม่มี ticket** → no-access เหมือนเดิม (ข้อความเดิมปรับคำ)
|
||||
|
||||
## 9. สคริปต์ทดสอบ — `scripts/make-ticket-docx.mjs`
|
||||
|
||||
Dev tool รันมือ `node scripts/make-ticket-docx.mjs <file.docx> [options]`:
|
||||
|
||||
- option: `--fid --uid --uname --tid --exp <unix|+seconds> --secret <hmac-secret>` และ
|
||||
`--tracked` (stamp `SavedByAddIn`+`DocumentTrackingId` เพื่อทดสอบ phase tracking ด้วย)
|
||||
- ทำงาน: สร้าง JSON ticket → HMAC-SHA256 hex → เขียน custom properties ลง `docProps/custom.xml`
|
||||
ผ่าน **pizzip** (dependency อยู่แล้วจากกลุ่ม template) — สร้าง custom.xml เองถ้าไฟล์ยังไม่มี
|
||||
(พร้อม override ใน `[Content_Types].xml`) · เขียนทับไฟล์เดิม**เฉพาะเมื่อส่ง `--in-place`**
|
||||
ไม่งั้นเขียนไฟล์ใหม่ `<name>.ticket.docx` (กฎ non-destructive ผูกกับ `public/` เท่านั้นแต่ถือหลักเดียวกัน)
|
||||
- default secret/exp สอดคล้อง .env ตัวอย่าง
|
||||
|
||||
## 10. Verify criteria
|
||||
|
||||
1. `npx vitest run` ผ่านทั้งชุด (ไฟล์ใหม่ + เดิม — โดยเฉพาะ `tests/lib/docver-token.test.ts` ไม่พัง)
|
||||
2. `npx tsc --noEmit` ผ่าน
|
||||
3. curl regression ที่ /token: (ก) `x-api-key` + identity body ออก token ได้เหมือนเดิม
|
||||
(ข) ticket body ออก token ได้ (ค) HMAC ผิดกับ exp หมด = 401 ข้อความเดียวกัน (ง) body ปนกัน = 400
|
||||
4. ทดสอบใน Word จริง (sideload ตาม `docs/WORDADDIN-DEV.md`) ด้วยไฟล์จาก make-ticket-docx:
|
||||
ไฟล์ถูกต้อง → ใช้ได้ ชื่อผู้ใช้ = uname · แก้ `ticket_data` 1 ตัวอักษร → no-access "ไฟล์ถูกแก้ไข" ·
|
||||
exp อดีต → no-access "ticket หมดอายุ" · เปิดเอกสารเดิมซ้ำ → ไม่ยิง /token ซ้ำ (ใช้ token เดิม) ·
|
||||
ตั้ง `DOCVER_TOKEN_TTL_SECONDS` สั้น ๆ แล้วเปิดซ้ำหลังหมดอายุ → renew อัตโนมัติใช้ต่อได้ ·
|
||||
เปิดไฟล์ uid คนละคน → session เดิมถูกเคลียร์
|
||||
5. รัน `plan-reviewer` + `code-reviewer` ตามกติกา `.claude/`
|
||||
|
||||
## 11. ลำดับงาน
|
||||
|
||||
1. `utils/docver-ticket.ts` + `tests/utils/docver-ticket.test.ts`
|
||||
2. `lib/addin-ticket.ts` + `tests/lib/addin-ticket.test.ts`
|
||||
3. `app/api/documents/token/route.ts` + `lib/openapi.ts` → verify 10.3 ด้วย curl
|
||||
4. `wordaddin/config.ts` + `metadataService.ts` (properties)
|
||||
5. `ticketService.ts` + `authService.ts` (rewrite)
|
||||
6. `TaskpaneApp.tsx` (detect + ข้อความ) + `apiClient.ts` (renew-retry)
|
||||
7. `scripts/make-ticket-docx.mjs`
|
||||
8. env/docs/CLAUDE.md sync → verify 10.1–10.5
|
||||
|
||||
## 12. Out of scope
|
||||
|
||||
- ปลด mock `versioningService` (ยัง mock ต่อ — token/flow ที่ทำรอบนี้พร้อมใช้วันปลด)
|
||||
- ความไม่ตรงกันของ `documentId` (GUID ที่ stamp) กับ `docId` (= fid) ของกลุ่ม docver —
|
||||
เป็นเรื่องของวันปลด mock (token ผูก fid แต่ mock ยังเรียกด้วย GUID — ไม่กระทบ auth)
|
||||
- UI จัดการ session/ออกจากระบบ, refresh token แบบมี revoke, ฝั่ง e-Court stamp (เขาทำ — เราส่ง §2 ให้)
|
||||
|
|
@ -41,6 +41,14 @@ middleware เหมือน `api/template`)
|
|||
auth: header `x-api-key` เทียบ env `DOCVER_API_KEY` (ไม่ตั้ง/ว่าง = 401 fail closed ·
|
||||
ไม่ใส่/ผิด = 401 ข้อความเดียวกันเป๊ะ กัน enumerate) — ตรวจก่อนอ่าน body เสมอ
|
||||
|
||||
**ทางเข้าที่สอง (2026-10-27, add-in ใน Word — PLAN-ADDIN-AUTH.md):** ไม่มี header
|
||||
`x-api-key` + body `{ticket_data, HmacSha256}` (ชื่อ field เหมือน custom properties
|
||||
ในไฟล์) — server ตรวจ HMAC-SHA256(secret, ticket_data ดิบ) + exp ผ่าน
|
||||
`lib/addin-ticket.ts` (env `ADDIN_TICKET_SECRET` → `SECRET` → default `"super_secret"`)
|
||||
แล้วออก token จาก claims ใน ticket (`uid→userid`, `uname→username`, `fid`, `tid→template`)
|
||||
· response/error รูปเดียวกับทางแรกทุกอย่าง (HMAC ผิด/หมดอายุ/ไม่ส่งมา = 401 ข้อความ
|
||||
เดียวกัน · body ผิดรูป = 400)
|
||||
|
||||
JSON body (allowlist — key อื่น = 400 กัน typo):
|
||||
|
||||
| field | type | หมายเหตุ |
|
||||
|
|
|
|||
|
|
@ -4,7 +4,7 @@ Taskpane ของ Word add-in "e-Court Documents Versioning" พอร์ตม
|
|||
|
||||
**ทางเข้า:** เปิด `/docedit` เปล่า ๆ จะ redirect มาที่ taskpane ทันที (ไม่ผ่าน login — คำขอผู้ใช้ 2026-10-14) ยกเว้นลิงก์ e-Court `/docedit/?fileUrl=&fileId=&typeId=` ที่ยังส่งต่อให้ `/note` ตามสัญญา N8 · manifest ชี้ `/docedit/wordaddin` ตรง ๆ (ไม่ว่าผ่าน root เพื่อลด hop ใน webview)
|
||||
|
||||
สถานะช่วงนี้: **ไม่มี login แล้ว** (ยกเลิก Keycloak 2026-10-27 — auth เข้าทางเดียวคือ e-Court pass-through จาก token/owner ในไฟล์ ดูหัว "ทดสอบ e-Court pass-through") ส่วน **API ยังเป็น mock** (version store เก็บ metadata ค้างข้าม session ใน localStorage ของ taskpane เพื่อทดสอบวงปิดเอกสาร→เปิดใหม่ — สลับกลับของจริงด้วยการลบ `wordaddin-mock-versions` หรือเคลียร์ storage ของ origin) รอ backend จริง — ดูหัว "การสลับกลับของจริง" ด้านล่าง
|
||||
สถานะช่วงนี้: **ไม่มี login แล้ว** (ยกเลิก Keycloak 2026-10-27 — auth เข้าทางเดียวคือ **ticket ของ e-Court** (`ticket_data` + `HmacSha256` ในไฟล์): add-in ตรวจ HMAC เอง → ขอ token ที่ `/api/documents/token` → เก็บใน localStorage ตาม fid ดูหัว "ทดสอบ e-Court ticket" และแผน `docs/PLAN-ADDIN-AUTH.md`) ส่วน **API ยังเป็น mock** (version store เก็บ metadata ค้างข้าม session ใน localStorage ของ taskpane เพื่อทดสอบวงปิดเอกสาร→เปิดใหม่ — สลับกลับของจริงด้วยการลบ `wordaddin-mock-versions` หรือเคลียร์ storage ของ origin) รอ backend จริง — ดูหัว "การสลับกลับของจริง" ด้านล่าง
|
||||
|
||||
## ไฟล์หลัก
|
||||
|
||||
|
|
@ -13,7 +13,7 @@ Taskpane ของ Word add-in "e-Court Documents Versioning" พอร์ตม
|
|||
| `app/wordaddin/page.tsx` + `components/wordaddin/WordAddinGate.tsx` | gate โหลด Office.js (self-host) → `Office.onReady` → mount `TaskpaneApp` |
|
||||
| `app/wordaddin/manifest.xml/route.ts` | serve manifest พร้อม **rewrite origin ตามโฮสต์ที่ขอ** (อ่าน `x-forwarded-proto`/`x-forwarded-host` ก่อนแล้ว fallback host ตรง ผ่าน `utils/request-origin.ts` ที่แชร์กับ origin ของ deep link N10 — โฮสต์ที่ไม่ใช่ localhost บังคับ https เพราะ Office รับ https เท่านั้น) |
|
||||
| `components/wordaddin/TaskpaneApp.tsx` | state machine `checking/no-access/untracked/tracking` + ทุก handler |
|
||||
| `wordaddin/services/*` | ชั้น Office.js พอร์ตดิบจากต้นฉบับ + `versioningService` เป็น mock (`authService` = pass-through จริง ไม่มี login) |
|
||||
| `wordaddin/services/*` | ชั้น Office.js พอร์ตดิบจากต้นฉบับ + `versioningService` เป็น mock (`authService` + `ticketService` = ticket auth จริง ไม่มี login — PLAN-ADDIN-AUTH.md) |
|
||||
| `public/wordaddin/manifest.template.xml` | template ของ manifest (ค่า default dev `https://localhost:3000`) — serve ผ่าน route ด้านบนเท่านั้น ไม่ได้ใช้ตรง ๆ |
|
||||
| `public/wordaddin/office-js/` | Office.js runtime — สร้างโดย postinstall `scripts/fetch-office-js.mjs` ดาวน์โหลดฉบับล่าสุดจาก CDN ของ Microsoft (**gitignore** — รัน `npm install` ทุกครั้งหลัง clone เครื่องต้องออกอินเทอร์เน็ตตอนติดตั้ง ส่วนตัวระบบ deploy ใช้ intranet ล้วน) |
|
||||
| `wordaddin/config.ts` | env `NEXT_PUBLIC_ADDIN_*` (ตารางด้านล่าง) |
|
||||
|
|
@ -22,8 +22,9 @@ Taskpane ของ Word add-in "e-Court Documents Versioning" พอร์ตม
|
|||
|
||||
| ตัวแปร | default | ใช้เมื่อไหร่ |
|
||||
|---|---|---|
|
||||
| `NEXT_PUBLIC_ADDIN_API_BASE_URL` | `""` (ว่าง) | ปลด mock ต่อ backend จริง (ต้องเป็น HTTPS) |
|
||||
| `NEXT_PUBLIC_ADDIN_MOCK_USERNAME` | `"tester01"` | ชื่อ fallback — ใช้ตอนไฟล์มี token แต่ไม่มี owner/uid และโหมด `?addindemo` (แสดง header + เป็น `createdByUserId` ของ mock) |
|
||||
| `NEXT_PUBLIC_ADDIN_API_BASE_URL` | `""` (ว่าง) | ปลด mock ต่อ backend จริง (ต้องเป็น HTTPS) — ไม่เกี่ยวกับการขอ token (ยิง same-origin `/docedit/api/documents/token` เสมอ) |
|
||||
| `NEXT_PUBLIC_ADDIN_MOCK_USERNAME` | `"tester01"` | ชื่อ fallback ของโหมด `?addindemo` (แสดง header + เป็น `createdByUserId` ของ mock) |
|
||||
| `NEXT_PUBLIC_ADDIN_TICKET_SECRET` | `"super_secret"` | secret ตรวจ HMAC ของ ticket ฝั่ง client (UX gate) — ต้องตรงกับตัว stamp ของ e-Court และ `ADDIN_TICKET_SECRET`/`SECRET` ฝั่ง server (PLAN-ADDIN-AUTH.md §7) · **production ต้องตั้งค่าจริงทั้งสองฝั่ง** |
|
||||
|
||||
## รัน dev ด้วย HTTPS (จำเป็นสำหรับ sideload ใน Word)
|
||||
|
||||
|
|
@ -56,15 +57,28 @@ Deploy จริง (intranet): **ไม่ต้อง rewrite อะไรแ
|
|||
- **`/docedit/wordaddin?addindemo=untracked`** / **`?addindemo=tracking`** — dev-only: ข้าม Office.js + ตั้ง session mock ทันที + ตั้ง phase ตามค่า (tracking มี history ตัวอย่าง 3 รายการ) — ทุกปุ่มกดได้ แต่ flow ที่แตะ Word จริง (Start Versioning / Save & Version / บันทึก) จะจบที่ error ตาม expected
|
||||
- หมายเหตุ: ใน browser ปกติ (ไม่ใช่ demo) banner "บันทึกอัตโนมัติไม่สำเร็จ" จะโผล่หลัง ~30 วิ เพราะ auto-save (FR-17) พยายาม save ทุก 30 วิแต่ไม่มี Word — ถือว่าถูกต้อง
|
||||
|
||||
## ทดสอบ e-Court pass-through (auth ทางเข้าเดียว)
|
||||
## ทดสอบ e-Court ticket (auth ทางเข้าเดียว)
|
||||
|
||||
ไฟล์ที่ e-Court stamp custom properties `token` และ/หรือ `owner` ไว้ จะใช้ได้เลย — ไม่มี login ให้กดตั้งแต่ยกเลิก Keycloak (2026-10-27): `detect()` อ่านค่าทั้งสองตอนเปิด task pane แล้วเรียก `authService.signInFromDocument()` (username ได้จาก `owner` → ถอด `uid` ใน JWT payload → `NEXT_PUBLIC_ADDIN_MOCK_USERNAME`, token จากไฟล์เก็บใน memory ให้ `getAccessToken()` ใช้ต่อ) · ไฟล์ที่ไม่มีทั้งคู่ = phase `no-access` (ขั้นตอนสำหรับเอกสารทั่วไปจะมีในอนาคต)
|
||||
ไฟล์ที่ e-Court stamp custom properties **`ticket_data`** (JSON `exp`/`fid`/`tid`/`uid`/`uname`) + **`HmacSha256`** (HMAC-SHA256 ของ string ดิบ ด้วย secret ปัจจุบัน `super_secret`) ไว้ จะใช้ได้เลย — ไม่มี login ให้กด: `detect()` อ่านคู่นี้ตอนเปิด task pane แล้วเรียก `authService.signInWithTicket()` ตาม flow ของ `docs/requirement/addin-workflow.md` (ตรวจ HMAC ฝั่ง client → ตรวจ exp → ดู session ใน localStorage `wordaddin-auth` ตาม uid/fid → ขอ access_token ที่ `/docedit/api/documents/token` แบบ body ticket → เก็บ token ตาม fid) · ไฟล์ที่ไม่มีคู่นี้ / HMAC ไม่ตรง / ticket หมดอายุ = phase `no-access` แยกข้อความตามสาเหตุ
|
||||
|
||||
ทดสอบเองใน Word โดยไม่ต้องรอ e-Court: เปิดไฟล์ → **File → Info → Properties → Advanced Properties → แท็บ Custom** → เพิ่ม property ชื่อ `token` (ค่า JWT ตัวอย่าง `eyJ...` — payload มี `uid` ก็แสดงเป็นชื่อผู้ใช้ได้) และ/หรือ `owner` (เช่น `u1001`) → เปิด task pane ใหม่ → ต้องเข้าเฟส untracked/tracking ทันทีไม่ผ่านหน้า no-access · ไฟล์ที่ไม่ใส่อะไรเลย = หน้า no-access
|
||||
ทดสอบเองใน Word โดยไม่ต้องรอ e-Court — สร้างไฟล์ทดสอบด้วยสคริปต์ (คำนวณ HMAC ตรงกับตัว stamp จริงของกลุ่ม template):
|
||||
|
||||
```bash
|
||||
# สร้าง <file>.ticket.docx (ไม่แตะไฟล์เดิม) exp +5 นาที เท่า TICKET_SEC ของตัว stamp
|
||||
node scripts/make-ticket-docx.mjs path/to/file.docx --fid DEMO_FID_001 --uid oom --uname "สรวิชญ์" --tid T12
|
||||
# ดู custom properties ที่มีในไฟล์
|
||||
node scripts/make-ticket-docx.mjs path/to/file.ticket.docx --list
|
||||
# ทดสอบ phase tracking ด้วย (stamp SavedByAddIn + DocumentTrackingId เพิ่ม)
|
||||
node scripts/make-ticket-docx.mjs path/to/file.docx --tracked --in-place
|
||||
```
|
||||
|
||||
แล้วเปิดไฟล์ใน Word ที่ sideload แล้ว → ต้องเข้าเฟส untracked (หรือ tracking เมื่อ `--tracked`) ทันที ชื่อผู้ใช้ = `uname` · ทดสอบ fail-case: แก้ `ticket_data` ผ่าน **File → Info → Properties → Advanced Properties → Custom** สัก 1 ตัวอักษร → no-access "ไฟล์ถูกแก้ไข" · สร้างด้วย `--exp +1` รอ 1 นาทีแล้วเปิด → no-access "ticket หมดอายุ" · เปิดเอกสารเดิมซ้ำ (จน ticket หมดอายุแต่ token ยังอยู่ใน localStorage) → ยังใช้ได้ตาม requirement ขั้น 7
|
||||
|
||||
หมายเหตุ: การขอ token ยิง same-origin `/docedit/api/documents/token` จริง (ไม่ผูก `NEXT_PUBLIC_ADDIN_API_BASE_URL`) — dev server ต้องรันอยู่และ secret ฝั่ง server (`ADDIN_TICKET_SECRET` → `SECRET` → default `super_secret`) ตรงกับ `--secret` ของสคริปต์
|
||||
|
||||
## การสลับกลับของจริง (วันหน้า)
|
||||
|
||||
- **Auth:** ไม่มี login ให้สลับแล้ว — **ยกเลิก Keycloak ถาวร 2026-10-27** (ต้นฉบับ OIDC/PKCE + Office Dialog API ของ poc เป็น historical record ที่ `D:\word-addin-poc\addin` เท่านั้น) · auth ของ add-in คือ e-Court pass-through ผ่าน `signInFromDocument` (token/owner ในไฟล์) — งานต่อยอดคือรูปแบบ/วิธี verify ของ credential ที่ e-Court stamp (ร่าง: `docs/requirement/addin-workflow.md`) · โดน 401 กลาง flow = `AuthRequiredError` → หน้า `no-access` (token จากไฟล์ต่ออายุไม่ได้ — ต้องได้ไฟล์ใหม่จาก e-Court)
|
||||
- **Auth:** ไม่มี login ให้สลับแล้ว — **ยกเลิก Keycloak ถาวร 2026-10-27** · auth ของ add-in คือ ticket ของ e-Court ผ่าน `signInWithTicket` (ticket_data + HmacSha256 ในไฟล์ — แผนเต็ม `docs/PLAN-ADDIN-AUTH.md`): add-in ตรวจ HMAC ฝั่ง client (UX gate) แล้วขอ access_token ที่ `/api/documents/token` ซึ่ง server ตรวจ HMAC+exp อีกชั้นด้วย secret ฝั่ง server · โดน 401 กลาง flow = ลอง renew จาก ticket 1 ครั้ง ยังไม่ผ่าน = `AuthRequiredError` → หน้า `no-access` (ticket ต่ออายุไม่ได้ — ต้องได้ไฟล์ใหม่จาก e-Court)
|
||||
- **API:** `wordaddin/services/versioningService.ts` เก็บชื่อ/ลายเซ็นเหมือนต้นฉบับเป๊ะ — ก้อบ body เดิม (multipart POST ผ่าน `apiClient` ซึ่งพอร์ตไว้แล้ว) กลับมา + ตั้ง `NEXT_PUBLIC_ADDIN_API_BASE_URL` (contract ปัจจุบันของกลุ่ม docver สรุปไว้ใน docblock ของไฟล์นั้น + docs/PLAN-DOCVER.md — identity/docId แกะจาก Bearer token ทั้งหมด)
|
||||
- ชุด unit test ของ services (jest → vitest) ยังไม่พอร์ต — ต้นฉบับอยู่ที่ `D:\word-addin-poc\addin\src\services\*.test.ts` + mock ที่ `src/test/`
|
||||
|
||||
|
|
|
|||
|
|
@ -12,19 +12,19 @@ Task pane ทั้งหมดเป็น state machine เดียว **ไ
|
|||
|
||||
ตั้งแต่ 2026-10-19 taskpane มี **2 แท็บ** (toggle กลุ่มปุ่ม `aria-pressed` ใต้ header — แสดงเฉพาะ phase `untracked`/`tracking`): **Versioning** คือทุกอย่างในเอกสารนี้ตามเดิม ส่วน **เครื่องมือ Template** (ดูหัวข้อ 10) คือเครื่องมือจัดการ Content Controls ฝั่ง Office.js ล้วน ไม่ยิง backend จึงใช้ได้ทั้งสอง phase ปุ่ม toggle **ทั้งสองปิดระหว่าง flow ที่เขียนเอกสาร** — `saving || starting` (versioning), `saveBusy` (บันทึก local/ปิดเอกสาร), `templateBusy` (operation ของแท็บ template รายงานผ่าน `onBusyChange` — กันสลับกลาง operation แล้ว flow versioning มา stamp/capture ซ้อน หรือกลับกัน; code review 2026-10-20) · สถานะแท็บรีเซ็ตกลับ `versioning` ตอนเข้า `no-access` กัน surprise · banner auto-save error (FR-17) ยกไว้เหนือ toggle ให้เห็นทั้งสองแท็บ
|
||||
|
||||
**ไม่มี login ตั้งแต่ 2026-10-27** (ยกเลิก Keycloak ตามคำขอผู้ใช้) — auth ทางเข้าเดียวคือ e-Court pass-through: `detect()` อ่าน custom properties `token`/`owner` ที่ e-Court stamp มากับไฟล์ แล้วเรียก `authService.signInFromDocument()` ผ่านเข้าเลย
|
||||
**ไม่มี login ตั้งแต่ 2026-10-27** (ยกเลิก Keycloak ตามคำขอผู้ใช้) — auth ทางเข้าเดียวคือ **ticket ของ e-Court**: `detect()` อ่าน custom properties `ticket_data` + `HmacSha256` ที่ e-Court stamp มากับไฟล์ แล้วเรียก `authService.signInWithTicket()` — ตรวจ HMAC ฝั่ง client → ตรวจ exp → ดู session ใน localStorage ตาม uid/fid → ขอ access_token ที่ `/api/documents/token` เก็บตาม fid (flow เต็ม: `docs/requirement/addin-workflow.md` + แผน `docs/PLAN-ADDIN-AUTH.md`)
|
||||
|
||||
```
|
||||
checking ──(ไม่มี token/owner ในไฟล์)──→ no-access
|
||||
checking ──(ไม่มี ticket / HMAC ไม่ตรง / ticket หมดอายุ / ขอ token ไม่สำเร็จ)──→ no-access
|
||||
│
|
||||
├─(มี token + ไม่ใช่ tracked doc)──→ untracked
|
||||
└─(มี token + tracked doc)────────→ tracking
|
||||
├─(ticket ผ่าน + ไม่ใช่ tracked doc)──→ untracked
|
||||
└─(ticket ผ่าน + tracked doc)────────→ tracking
|
||||
```
|
||||
|
||||
การเปลี่ยน phase อื่นๆ:
|
||||
|
||||
- `untracked` → (Start Versioning สำเร็จ) → `tracking`
|
||||
- ทุก phase → (ไม่มี token ตอน detect ใหม่ / 401 = token หมดอายุไม่มีทางต่ออายุ) → `no-access`
|
||||
- ทุก phase → (ไม่มี ticket ตอน detect ใหม่ / 401 หลังพยายาม renew จาก ticket แล้ว = ticket หมดอายุไม่มีทางต่ออายุ) → `no-access`
|
||||
- ปุ่ม "ลองใหม่" มีจริงแค่ 2 จุด: `bootError` ใน `checking` และ history error ใน `VersionHistoryPanel` — `untracked`/`tracking` **ไม่มี**ปุ่มลองใหม่แยกต่างหาก (เริ่ม flow ใหม่ด้วยการกดปุ่ม flow ซ้ำ)
|
||||
|
||||
State ที่ UI ถือนอกเหนือจาก phase:
|
||||
|
|
@ -32,11 +32,11 @@ State ที่ UI ถือนอกเหนือจาก phase:
|
|||
| State | ความหมาย |
|
||||
|---|---|
|
||||
| `activeTab` | แท็บที่เปิดอยู่ `versioning` (default) / `template` — ดูหัวข้อ 10 |
|
||||
| `username` | ชื่อเจ้าของไฟล์จาก pass-through (owner → uid ใน token → fallback) — แสดงเป็น chip เฉย ๆ ใน header |
|
||||
| `username` | ชื่อจาก `uname` ของ ticket — แสดงเป็น chip เฉย ๆ ใน header |
|
||||
| `documentId` | tracking id ของเอกสาร (มีเฉพาะ `tracking`) |
|
||||
| `documentTemplateId` | template ต้นทางของ**ไฟล์ร่าง** (มีเฉพาะ `untracked` ที่เป็นไฟล์ร่าง — FR-16) |
|
||||
| `versions` + `historyLoading` + `historyError` | version history ของ `documentId` |
|
||||
| `noAccessNotice` | ข้อความบนหน้า `no-access` — ต่างกันตามสาเหตุ (ไม่มี token/owner ในไฟล์ หรือ token หมดอายุจาก 401) |
|
||||
| `noAccessNotice` | ข้อความบนหน้า `no-access` — ต่างกันตามสาเหตุ (ไม่มี ticket ในไฟล์ / HMAC ไม่ตรง = ไฟล์ถูกแก้ / ticket หมดอายุ / ขอ token ไม่สำเร็จ) |
|
||||
| `starting`, `saving`, `progressMessage` | busy + ข้อความ progress ของ flow Start Versioning / Save & Version |
|
||||
| `confirmOpen`, `startConfirmOpen` | เปิด dialog ยืนยันของ flow ไหน |
|
||||
| `flowError`, `flowWarning`, `flowSuccess` | MessageBar 3 แบบของ flow หลัก (แสดงเฉพาะ `tracking`) |
|
||||
|
|
@ -54,7 +54,7 @@ State ที่ UI ถือนอกเหนือจาก phase:
|
|||
### Header (ทุก phase)
|
||||
|
||||
- ชื่อแอป: **e-Court Documents Versioning** (semibold)
|
||||
- ฝั่งขวา (เมื่อผ่าน pass-through แล้วและไม่ใช่ `no-access`/`checking`): chip เล็กพื้น `canvas` แสดง `username` เฉย ๆ — ไม่ใช่ปุ่ม (ไม่มี login จึงไม่มีออกจากระบบ) · จอแคบตัดด้วย truncate ชื่อเต็มอยู่ใน `title`
|
||||
- ฝั่งขวา (เมื่อผ่าน ticket auth แล้วและไม่ใช่ `no-access`/`checking`): chip เล็กพื้น `canvas` แสดง `username` เฉย ๆ — ไม่ใช่ปุ่ม (ไม่มี login จึงไม่มีออกจากระบบ) · จอแคบตัดด้วย truncate ชื่อเต็มอยู่ใน `title`
|
||||
|
||||
### `checking`
|
||||
|
||||
|
|
@ -65,8 +65,10 @@ State ที่ UI ถือนอกเหนือจาก phase:
|
|||
|
||||
- ข้อความ: "เอกสารนี้ยังใช้งาน add-in ไม่ได้"
|
||||
- คำอธิบายจางๆ (`noAccessNotice` ตามสาเหตุ):
|
||||
- ตอน detect ไม่เจอข้อมูลในไฟล์: "ไม่พบ token/owner ของ e-Court ในไฟล์ — ขณะนี้ add-in ใช้ได้กับเอกสารที่ส่งมาจาก e-Court เท่านั้น"
|
||||
- ตอนโดน 401 กลาง flow: "token ของ e-Court หมดอายุหรือไม่ถูกต้อง — โปรดขอไฟล์ใหม่จาก e-Court แล้วเปิดอีกครั้ง"
|
||||
- ไม่มี ticket ในไฟล์: "ไม่พบ ticket ของ e-Court ในไฟล์ — add-in ใช้ได้กับเอกสารที่ส่งมาจาก e-Court เท่านั้น"
|
||||
- ticket รูปผิด / HMAC ไม่ตรง: "ตรวจสอบ ticket ไม่ผ่าน — ไฟล์ถูกแก้ไขหรือเสียหาย"
|
||||
- ticket หมดอายุ (ตอนเปิด หรือโดน 401 กลาง flow หลังพยายาม renew): "ticket ของ e-Court หมดอายุ — โปรดขอไฟล์ใหม่จาก e-Court แล้วเปิดอีกครั้ง"
|
||||
- ขอ token ไม่สำเร็จ (server ลง/network): "ขอ token จากระบบไม่สำเร็จ — ตรวจการเชื่อมต่อแล้วเปิดเอกสารอีกครั้ง"
|
||||
- **ไม่มีปุ่มใด ๆ** — ไม่มี login ให้กด (ยกเลิก Keycloak 2026-10-27; ขั้นตอนสำหรับเอกสารทั่วไปจะมีในอนาคต) ผู้ใช้ได้ไฟล์ใหม่จาก e-Court แล้วเปิดใหม่เท่านั้น
|
||||
|
||||
### `untracked`
|
||||
|
|
@ -164,8 +166,8 @@ Dialog เดียวใช้ 2 flow (ปรับข้อความด้
|
|||
|
||||
### Detect (FR-1) — ตอนเปิด task pane / กดลองใหม่
|
||||
|
||||
1. อ่าน custom properties ครั้งเดียวทั้งชุด: `token`, `owner`, `SavedByAddIn`, `DocumentTrackingId`, `LastVersionId`, `LastVersionTimestamp`, `TemplateId`
|
||||
2. e-Court pass-through: ไม่มี `token`/`owner` เลย (หรืออ่านไฟล์ไม่ได้) → `no-access` · มีอย่างน้อยหนึ่งตัว → `signInFromDocument` (username จาก owner → uid ใน token) แล้วไปต่อ
|
||||
1. อ่าน custom properties ครั้งเดียวทั้งชุด: `ticket_data`, `HmacSha256`, `SavedByAddIn`, `DocumentTrackingId`, `LastVersionId`, `LastVersionTimestamp`, `TemplateId`
|
||||
2. ticket auth (`signInWithTicket` — PLAN-ADDIN-AUTH.md §5): อ่านไฟล์ไม่ได้ / ไม่มี ticket / HMAC ไม่ตรง / ticket หมดอายุ (ตามเงื่อนไข uid/fid ของ session) / ขอ token ไม่สำเร็จ → `no-access` แยกข้อความตามสาเหตุ · ผ่าน = มี access_token ของ fid นี้เก็บใน localStorage `wordaddin-auth` แล้วไปต่อ
|
||||
3. `tracked = (SavedByAddIn === GUID ของ add-in)`:
|
||||
- tracked **และมี** `DocumentTrackingId` → `tracking` + โหลด history
|
||||
- ไม่ track หรือ track แต่ id หาย → `untracked` (เก็บ `TemplateId` ไว้ใช้ตอน Start Versioning — ไฟล์ร่างอยู่กลุ่มนี้)
|
||||
|
|
@ -251,17 +253,17 @@ interface VersionRecord {
|
|||
|
||||
response: `{ url: string, expiresAt: string }` — UI เปิด `url` ในเบราว์เซอร์เครื่องผู้ใช้เท่านั้น
|
||||
|
||||
### Auth — e-Court pass-through (ไม่มี login)
|
||||
### Auth — e-Court ticket (ไม่มี login)
|
||||
|
||||
ยกเลิก Keycloak OIDC/PKCE แล้ว (2026-10-27) — ไม่มี Office Dialog ไม่มีหน้า sign-in: e-Court stamp custom properties `token`/`owner` ลงไฟล์ตอนสร้าง แล้ว `signInFromDocument()` ใช้ได้ทันที · token เก็บ **ใน memory เท่านั้น** (ห้าม localStorage) และ**ต่ออายุไม่ได้** — โดน 401 = `AuthRequiredError` → หน้า `no-access` ให้ขอไฟล์ใหม่จาก e-Court · token นี้คือสิ่งที่ `apiClient` แนบเป็น `Authorization: Bearer` ทุก request ตอนต่อ backend จริง (กลุ่ม docver บังคับ Bearer — docs/PLAN-DOCVER.md) · **รูปแบบ/วิธี verify ของ credential ที่ e-Court stamp ลงไฟล์ยังเป็นงานออกแบบต่อ** (ร่าง requirement: `docs/requirement/addin-workflow.md`)
|
||||
ยกเลิก Keycloak OIDC/PKCE แล้ว (2026-10-27) — ไม่มี Office Dialog ไม่มีหน้า sign-in: e-Court stamp custom properties **`ticket_data`** (JSON `exp`/`fid`/`tid`/`uid`/`uname`) + **`HmacSha256`** (HMAC-SHA256 ของ string ดิบ ด้วย secret ที่ config ได้ — ปัจจุบัน `super_secret`) ลงไฟล์ตอนสร้าง แล้ว `signInWithTicket()` ใช้ได้ทันที · flow ตาม `docs/requirement/addin-workflow.md` (แผนเต็ม `docs/PLAN-ADDIN-AUTH.md`): ตรวจ HMAC ฝั่ง client (UX gate) → ตรวจ exp → session ใน localStorage `wordaddin-auth` ตาม uid (user ใหม่ = เคลียร์ session เก่าทั้งก้อน) → **ขอ access_token ที่ `POST /api/documents/token`** แบบ body `{ticket_data, HmacSha256}` (server ตรวจ HMAC+exp อีกชั้นด้วย secret ฝั่ง server) → เก็บ token ตาม fid ("คีย์คือ fid") — กลับมาเปิดเอกสารเดิม = ใช้ token เดิมไม่ต้องขอใหม่ · token หมดอายุกลางใช้งาน (โดน 401) = ลองขอใหม่จาก ticket 1 ครั้ง (ticket ต้องยังไม่หมดอายุ) ยังไม่ผ่าน = `AuthRequiredError` → หน้า `no-access` ให้ขอไฟล์ใหม่จาก e-Court · token นี้คือสิ่งที่ `apiClient` แนบเป็น `Authorization: Bearer` ทุก request ตอนต่อ backend จริง (กลุ่ม docver บังคับ Bearer — docs/PLAN-DOCVER.md)
|
||||
|
||||
> **หมายเหตุสถานะปัจจุบัน:** UI นี้ถูก implement แล้วใน repo นี้ที่ route `/docedit/wordaddin` (ดู `components/wordaddin/` + `wordaddin/`) — auth เป็น e-Court pass-through จริง (`wordaddin/services/authService.ts` — ไม่มี mock ให้ login แล้ว) ส่วน API ทั้งสามข้างบนยังเป็น in-memory mock (`wordaddin/services/versioningService.ts`) · วิธีสลับกลับของจริง: `docs/WORDADDIN-DEV.md`
|
||||
> **หมายเหตุสถานะปัจจุบัน:** UI นี้ถูก implement แล้วใน repo นี้ที่ route `/docedit/wordaddin` (ดู `components/wordaddin/` + `wordaddin/`) — auth เป็น ticket auth จริงครบ flow (`wordaddin/services/authService.ts` + `ticketService.ts` + ทางเข้า ticket ของ `/api/documents/token`) ส่วน API ทั้งสามข้างบนยังเป็น in-memory mock (`wordaddin/services/versioningService.ts`) · วิธีสลับกลับของจริง: `docs/WORDADDIN-DEV.md` · สร้างไฟล์ทดสอบพร้อม ticket: `node scripts/make-ticket-docx.mjs`
|
||||
|
||||
---
|
||||
|
||||
## 8. กติกา error ที่ UI ต้องรองรับ
|
||||
|
||||
1. **`AuthRequiredError` (ไม่มี token / 401)** — ทุก flow ต้อง catch แล้ว: กลับ `no-access` + เคลียร์ `documentId`/`documentTemplateId`/`versions` + ตั้ง `noAccessNotice` = "token ของ e-Court หมดอายุหรือไม่ถูกต้อง — โปรดขอไฟล์ใหม่จาก e-Court แล้วเปิดอีกครั้ง" — **ไม่ throw ต่อ ไม่โชว์ error ทับ phase**
|
||||
1. **`AuthRequiredError` (ไม่มี token / 401 หลัง renew-retry)** — ทุก flow ต้อง catch แล้ว: กลับ `no-access` + เคลียร์ `documentId`/`documentTemplateId`/`versions` + ตั้ง `noAccessNotice` = "ticket ของ e-Court หมดอายุหรือไม่ถูกต้อง — โปรดขอไฟล์ใหม่จาก e-Court แล้วเปิดอีกครั้ง" — **ไม่ throw ต่อ ไม่โชว์ error ทับ phase**
|
||||
2. error อื่น → MessageBar ณ จุดที่เกิด (login/history/flow/download แยกกัน) — ห้ามปล่อย unhandled rejection
|
||||
3. แยก error ของ "ตัว version" ออกจาก "ปิดเอกสารไม่สำเร็จ" — อันหลังเป็น warning เพราะข้อมูลปลอดภัยแล้ว
|
||||
4. PDF เป็น best-effort เสมอ — ล้มแค่เตือน ไม่ fail flow
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue