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 1m23s
Deploy eCourt Note Web to Server / Deploy eCourt Note Web to Server (push) Successful in 5s
Deploy eCourt Note Web to Server / Notify Deployment Status (push) Successful in 0s
- Removed client-side HMAC verification; server now handles all HMAC checks. - Updated ticket processing logic to improve UX and security. - Adjusted API request handling to return discriminated results based on HTTP status. - Enhanced error messages for better clarity on authentication failures. - Added unit tests for the new token endpoint to ensure compliance with updated contract. - Cleaned up unused code and comments related to previous HMAC handling.
258 lines
28 KiB
Markdown
258 lines
28 KiB
Markdown
# PLAN-ADDIN-AUTH — Add-in Ticket Authen (`ticket_data` + `HmacSha256`)
|
||
|
||
> วันที่ตกลงแผน: 2026-10-27 · สถานะ: แผนรออนุมัติ · requirement ต้นทาง: `docs/requirement/addin-workflow.md`
|
||
> **แก้ใหญ่ 2026-11-04** (ตาม code review ของผู้ใช้): ถอด client HMAC + `NEXT_PUBLIC_ADDIN_TICKET_SECRET`
|
||
> ออก (execute **D13 ของ PLAN-SSO.md** — secret ใน bundle สกัดได้ = ปลอม ticket ผ่าน server ได้จริง
|
||
> ไม่ใช่แค่ UX gate อย่าง D3 เดิม) · ทางเข้า ticket ของ /token แยก status ชัดเจน
|
||
> **200 / 400 ฟอร์แม็ตพัง / 401 ลายเซ็นไม่ถูก / 403 ลายเซ็นถูกแต่หมดอายุ** (เลิก hasVersionByUser
|
||
> ของ requirement ขั้น 7 ฉบับ 2026-10-28) · token TTL default 604800 (7 วัน) แทนระบบ refresh ·
|
||
> flow ฝั่ง addin เป็น pre-check + ยิง /token ทุกครั้งที่เปิดเอกสาร
|
||
|
||
## 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 ตรวจ **ฟอร์แม็ต + exp** เองฝั่ง client เป็น UX gate — การตรวจลายเซ็น (HMAC) เป็นของ
|
||
**server อย่างเดียว** ยิง /token ทุกครั้งที่เปิดเอกสาร (แก้ 2026-11-04 — เดิมตรวจ HMAC ฝั่ง client
|
||
ด้วย secret ใน bundle)
|
||
- **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`~~ **แทนที่ 2026-11-04 ตาม PLAN-SSO.md D13**: client ไม่ตรวจ HMAC — เหลือ parse + exp เป็น UX gate การตรวจ "ไฟล์ถูกแก้" เกิดตอน server ตอบ 401 · ถอด `NEXT_PUBLIC_ADDIN_TICKET_SECRET` ออกจาก bundle ทั้งหมด (secret ใน bundle ต้องตรงกับของ server เสมอจึงผ่าน gate = แกะ bundle ก็ปลอม ticket ผ่าน server ได้จริง) |
|
||
| 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 หมดอายุ | **แก้ 2026-11-04: TTL default 604800 (7 วัน)** แทนระบบ refresh/renew — renew จาก ticket (โดน 401 กลาง session → renew 1 ครั้งแล้ว retry) เหลือเป็นเส้นทางรองที่แทบไม่ถูกใช้ · ticket หมดอายุแล้ว server ตอบ 403 → renew ไม่ได้ → ใช้ JWT เดิมใน session ต่อ |
|
||
| 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 (ตัว stamp) + `ADDIN_TICKET_SECRET` (server
|
||
ของเรา ตัว verify) ต้องตรงกันสองฝั่ง** (แก้ 2026-11-04 — เดิมต้องตรงสามฝั่งรวม
|
||
`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}` ถูกทั้งคู่ | **ทางใหม่** — `verifyAddinTicketHmac` (HMAC+parse ไม่ตรวจ exp) → `isTicketExpired` → `issueDocverToken` → 200 response รูปเดียวกัน `{access_token, token_type, expires_in}` |
|
||
| body ไม่ใช่ JSON / ไม่ใช่ object / มี field อื่นปน / field ไม่ใช่ string / **ไม่มี ticket fields เลย (missing)** | **400** (ฟอร์แม็ตพัง — แก้ 2026-11-04: missing ย้ายจาก 401 มารวม 400) |
|
||
| HMAC ผิด / parse ticket ไม่ผ่าน | 401 `"ticket ไม่ถูกต้อง"` **ข้อความเดียวกันทุกสาเหตุ** (กัน oracle) |
|
||
| ticket **หมดอายุ** แต่ HMAC ถูก | **403 `"ticket ของ e-Court หมดอายุ"** (แก้ 2026-11-04 — เลิก requirement ขั้น 7 ฉบับ 2026-10-28: ถอด `hasVersionByUser` ออก ไม่ตรวจ DB อีกต่อไป เพราะ ticket ฝังในไฟล์ ใครได้ไฟล์ก็ได้ ticket การปล่อยให้ ticket เก่าแลก token ใหม่ได้ตลอดไปเท่ากับปล่อยสิทธิ์ตามการครอบครองไฟล์ · client ที่มี JWT ของ fid นี้ใน session ที่ยังไม่หมดใช้ต่อเอง) |
|
||
| `DOCVER_TOKEN_SECRET` ไม่ตั้ง | 500 (เดิม) |
|
||
|
||
- token TTL: env `DOCVER_TOKEN_TTL_SECONDS` **default 604800 (7 วัน)** (แก้ 2026-11-04 จาก 3600)
|
||
- route ไม่ import `@/db`/minio เลยหลังถอด DB check — unit test ได้โดยไม่ต้องตั้ง DATABASE_URL
|
||
(`tests/app/api/documents/token.test.ts`)
|
||
|
||
- การเทียบ 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}$ (ใช้ฝั่ง server)
|
||
```
|
||
|
||
(แก้ 2026-11-04: ถอด `timingSafeEqualHex` + `HmacSha256Hex` adapter ออก — client ไม่คำนวณ
|
||
HMAC แล้วจึงไม่ต้องฉีดตัวคำนวณ, timing-safe จริงอยู่ที่ `node:crypto` ฝั่ง server)
|
||
|
||
- `lib/` (server) กับ `wordaddin/` (client) ใช้ `parseDocverTicket`/`isTicketExpired` ตัวเดียวกัน —
|
||
schema drift กันได้จากฝั่งเดียว
|
||
|
||
## 5. ฝั่ง addin — flow ตอนเปิดเอกสาร (แก้ 2026-11-04 — pre-check + ยิง /token ทุกครั้ง)
|
||
|
||
`TaskpaneApp.detect()` → `authService.signInWithTicket({ticketData, ticketHmac})`:
|
||
|
||
```
|
||
1. อ่าน properties ticket_data + HmacSha256 (readTrackedProperties)
|
||
2. pre-check ฝั่ง client (ไม่ยิง network):
|
||
· ขาดข้อใดข้อหนึ่ง / ค่ายาวเกิน 255 → no-access "ไม่พบ ticket ของ e-Court ในไฟล์"
|
||
· parseDocverTicket ไม่ผ่าน → no-access "รูปแบบ ticket ในไฟล์ไม่ถูกต้อง"
|
||
· ticket หมดอายุ และ ไม่มี JWT ของ fid นี้ใน session (uid ตรงกัน) → no-access
|
||
"ticket หมดอายุ" (มีใน session = มีโอกาสเป็นไฟล์เก่าที่เคยแก้ → ไปให้ server ตัดสินต่อ)
|
||
3. ยิง POST /api/documents/token **ทุกครั้งที่เปิดเอกสาร** (ตรวจลายเซ็นที่ server —
|
||
client ไม่มี secret) แล้วแยกทางตาม status:
|
||
· 200 → ได้ JWT ใหม่: uid ≠ session.userId (user ใหม่) = เคลียร์ session เก่าทั้งก้อน
|
||
สร้างใหม่ · คนเดิม = update userName + tokens[fid]
|
||
· 403 (ลายเซ็นถูกแต่ ticket หมดอายุ) → มี JWT ของ fid ใน session ที่ยังไม่หมด
|
||
(uid ตรง) = refresh userName แล้ว**ใช้ JWT เดิมต่อ** · ไม่มี/หมดแล้ว = no-access
|
||
"ticket หมดอายุ"
|
||
· 400 → no-access "รูปแบบ ticket ไม่ถูกต้อง" · 401 → no-access "ตรวจสอบลายเซ็น (HMAC)
|
||
กับระบบไม่ผ่าน" (ทั้งคู่ = ไฟล์ถูกแก้/เสียหาย ต้องขอไฟล์ใหม่)
|
||
· network/5xx/ตอบผิดรูป → no-access "ขอ token จากระบบไม่สำเร็จ" (ลองใหม่ได้เมื่อเน็ตกลับ)
|
||
4. ตั้ง state ภายใน (fid/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)~~
|
||
(ถอด 2026-11-04), session store
|
||
(`loadSession`/`saveSession`/`clearSession` — try/catch + memory fallback), `requestDocverToken`
|
||
(POST `withBasePath("/api/documents/token")` body ticket — แก้ 2026-11-04: คืน discriminated
|
||
result แยก network/http status/malformed ไม่โยน Error)
|
||
- `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"`~~
|
||
(ถอด 2026-11-04 — เหลือ `apiBaseUrl` + `mockUsername`)
|
||
- `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 (แก้ 2026-11-04: pre-check + แยก status
|
||
200/400/401/403 ผ่าน result ของ requestDocverToken)
|
||
- `wordaddin/services/apiClient.ts` — renew-retry ตอน 401
|
||
- `components/wordaddin/TaskpaneApp.tsx` — `detect()` เรียก `signInWithTicket` + ข้อความ no-access
|
||
(แก้ 2026-11-04: no-access เคส "อ่านข้อมูลเอกสารไม่สำเร็จ" มีปุ่ม "ลองใหม่" เรียก detect() ซ้ำ)
|
||
- `components/wordaddin/WordAddinGate.tsx` — แก้ 2026-11-04: fallback onReady เดิม 5 วิแล้ว proceed
|
||
บอด ๆ ทำให้เครื่องช้าจบ no-access ไม่มีทางออก — ตอนนี้ poll ทุก 5 วิว่า global `Office` โผล่หรือยัง
|
||
(โผล่แล้ว = proceed ตามเจตนาเดิม · ยังไม่โผล่ = รอต่อ) จนถึง hard cap 60 วิ ค่อยไปหน้า error
|
||
- `lib/openapi.ts` — POST /token มี 2 รูป body (oneOf: identity+x-api-key / ticket) + คำอธิบาย
|
||
400/401/403 ตาม §3 (แก้ 2026-11-04)
|
||
- `tests/app/api/documents/token.test.ts` — ใหม่ 2026-11-04: route unit test ครอบ contract
|
||
(ทำได้เพราะ route ไม่ import @/db แล้ว)
|
||
- `.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`~~ | — | **ถอด 2026-11-04** (PLAN-SSO.md D13) — ฝั่ง add-in ไม่มี secret อีกต่อไป |
|
||
|
||
> หมายเหตุจากตอน 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 ไม่ผ่านอยู่แล้ว —
|
||
เช็คความยาวเพื่อข้อความที่ชัดกว่า)
|
||
- **เปิดเอกสาร = ต้อง online ทุกครั้ง** (ยิง /token เพื่อตรวจลายเซ็นที่ server — แก้ 2026-11-04)
|
||
— offline ได้ no-access "ขอ token ไม่สำเร็จ" · ระหว่าง session ใช้ต่อได้เพราะ JWT อายุ 7 วัน
|
||
(apiClient ไม่ยิง /token จนกว่าจะโดน 401)
|
||
- **ticket หมดอายุ + มี JWT ของ fid ใน session ที่ยังไม่หมด** → ผ่านด้วย JWT เดิม (server ตอบ 403)
|
||
· **ticket หมดอายุ + ไม่มี session / JWT หมดด้วย / localStorage หาย** → ต้องขอไฟล์ใหม่จาก
|
||
e-Court (แก้ 2026-11-04 — เดิม server ออก token ให้จากหลักฐาน DB `hasVersionByUser` ซึ่งถอดแล้ว)
|
||
- **localStorage ถูก block** (webview บางตัว/private mode) → session อยู่ใน memory ต่อ task pane
|
||
instance เดียว: ปิด-เปิดใหม่ + ticket หมดอายุ = no-access (ไม่มี JWT ในเครื่อง) · ticket ยังไม่หมด
|
||
= ขอใหม่ได้ปกติ
|
||
- **uname ไทย** — เป็น string ตรง ๆ ไม่ผ่าน base64 ไม่มีปัญหา mojibake (ต่างจาก decode JWT payload)
|
||
- **exp == now** → หมดอายุ
|
||
- **401 จาก endpoint อื่นระหว่างใช้งาน** → renew 1 ครั้ง (server ตอบ 200 = ได้ใหม่ · 403 = ticket
|
||
หมด → false) → retry → ยังไม่ผ่าน → no-access
|
||
- **เครื่องโหลดช้า จน office.js ยังไม่ evaluate ตอน taskpane เริ่ม** → WordAddinGate รอต่อ (poll
|
||
ทุก 5 วิ) จนมี global `Office` ถึง mount TaskpaneApp — ไม่ proceed บอด ๆ แล้ว (แก้ 2026-11-04);
|
||
ถ้า detect อ่านไฟล์ไม่ได้จริง หน้า no-access มีปุ่ม "ลองใหม่" เรียก detect() ซ้ำ
|
||
- **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/app/api/documents/token.test.ts` — route contract
|
||
400/401/403/200 ที่เพิ่ม 2026-11-04 และ `tests/wordaddin/ticket-auth-flow.test.ts` ฉบับใหม่)
|
||
2. `npx tsc --noEmit` ผ่าน (error เดิมของ `piscina`/`docx-templates` ไม่เกี่ยวกับแผนนี้)
|
||
3. curl regression ที่ /token: (ก) `x-api-key` + identity body ออก token ได้เหมือนเดิม
|
||
(ข) ticket body ออก token ได้ `expires_in` = 604800 (ค) HMAC ผิด = 401 ข้อความเดียว
|
||
(ง) body ปนกัน/ไม่ส่ง ticket = 400 (จ) exp อดีตลายเซ็นถูก = **403**
|
||
4. ทดสอบใน Word จริง (sideload ตาม `docs/WORDADDIN-DEV.md`) ด้วยไฟล์จาก make-ticket-docx:
|
||
ไฟล์ถูกต้อง → ใช้ได้ ชื่อผู้ใช้ = uname · **dev log เห็น POST /token ทุกครั้งที่เปิด task pane** ·
|
||
แก้ `ticket_data` 1 ตัวอักษร → no-access ข้อความลายเซ็น (server 401) ·
|
||
**ticket หมดอายุ + มี session (JWT 7 วันยังไม่หมด) → ผ่านด้วย JWT เดิม (Network เห็น 403)** ·
|
||
**ticket หมดอายุ + เคลียร์ localStorage `wordaddin-auth` → no-access "ticket หมดอายุ"** (แก้ 2026-11-04:
|
||
ไม่มีทางกลับจากหลักฐาน DB อีก) · หยุด dev server → no-access "ขอ token ไม่สำเร็จ" ·
|
||
ตั้ง `DOCVER_TOKEN_TTL_SECONDS` สั้น ๆ แล้วใช้จน JWT หมด → apiClient renew อัตโนมัติ (ticket ยังไม่หมด) ·
|
||
เปิดไฟล์ uid คนละคน → session เดิมถูกเคลียร์ · browser ธรรมดา → no-access "อ่านข้อมูลเอกสารไม่สำเร็จ"
|
||
+ กด "ลองใหม่" ได้
|
||
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`~~ (ทำแล้ว 2026-10-27 — `versioningService` ยิงกลุ่ม docver จริงผ่าน Bearer same-origin; mock เหลือเฉพาะ `?addindemo` ดู `docs/WORDADDIN-DEV.md`)
|
||
- ~~ความไม่ตรงกันของ `documentId` (GUID ที่ stamp) กับ `docId` (= fid)~~ (ยุติแล้ว 2026-10-27 — เอกสารระบุด้วย fid ใน token ฝั่งเดียว; พารามิเตอร์ documentId ของ versioningService เหลือตาม signature เดิมไม่ส่งถึง server)
|
||
- UI จัดการ session/ออกจากระบบ, refresh token แบบมี revoke, ฝั่ง e-Court stamp (เขาทำ — เราส่ง §2 ให้)
|