refactor(auth): consolidate auth under /api/auth/* endpoints
Some checks failed
Deploy eCourt Note Web to Server / Validate Database Schema (Drizzle) (push) Successful in 3s
Deploy eCourt Note Web to Server / Build Web Docker Image (push) Failing after 30s
Deploy eCourt Note Web to Server / Deploy eCourt Note Web to Server (push) Has been skipped
Deploy eCourt Note Web to Server / Notify Deployment Status (push) Failing after 1s
Some checks failed
Deploy eCourt Note Web to Server / Validate Database Schema (Drizzle) (push) Successful in 3s
Deploy eCourt Note Web to Server / Build Web Docker Image (push) Failing after 30s
Deploy eCourt Note Web to Server / Deploy eCourt Note Web to Server (push) Has been skipped
Deploy eCourt Note Web to Server / Notify Deployment Status (push) Failing after 1s
- ยุบ route auth ที่เคย copy กันจน drift (flow N11 หายจาก /api/auth/session) เหลือโค้ด inline สองเส้นเดียว: POST /api/auth/token (ทางเข้า 3: x-api-key docver / x-api-key purpose:"login" mint ลิงก์ note / ticket ของ add-in) + POST /api/auth/session (แลก credential + register N8 + N11 fid-only) - POST /api/documents/token คงไว้เป็น alias re-export (กลุ่ม docver/e-Court เดิม — contract เดิมเป๊ะ, มี test toBe กัน copy แยกชุดกลับมา) - ถอดเส้นฝั่ง note ทั้งสอบ: /api/note/auth/token + /api/note/auth/session (e-Court ต้องย้ายมาทางเข้ารวม — แจ้งทีม e-Court ก่อน deploy) - utils/auth-store.ts: primitives กลางฝั่ง client (jwtExp/isExpired/ localStorage store + memory fallback) — lib/note-session ใช้ร่วม (expMs decode จาก JWT payload = นาฬิกา server) และวางไว้ให้ wordaddin ย้ายมาใช้รอบหน้าได้ (รอบนี้ไม่แต้ wordaddin ตามข้อตกลง) - นโยบาย token หมดอายุกลาง session: PDFViewer reportActionError จับ auth-error ของ Server Action ทุกจุด → ล้าง session แล้วพาไป /note?sso=expired (จบที่ gate เดียวกับ bootstrap 401) - openapi: ทางเข้า purpose:"login" + schema LoginTokenRequest + ลบ path เส้นที่ถอด + test กันเส้นตายหลุดค้างใน spec - เอกสาร sync: PLAN-SSO §0.6, CLAUDE.md, PLAN-STAGED, .env.example, /note/dev - tests: ย้าย/เพิ่มชุด auth (50 เคสกลุ่ม auth ผ่าน, ทั้งชุด 444 ผ่าน) + e2e ครบทั้ง API (mint/alias/ticket/404 เส้นเก่า/N11/D4) และ browser Co-Authored-By: Claude Code <noreply@anthropic.com>
This commit is contained in:
parent
c57a75e36a
commit
5121970f31
26 changed files with 625 additions and 523 deletions
|
|
@ -37,7 +37,7 @@ credential อายุสั้น ฝังอีเมล/แชท/เก็
|
|||
|
||||
```
|
||||
ลิงก์ e-Court ชี้ตรงมา /note?fileUrl=&typeId=&t=… (หรือ ticket_data+HmacSha256)
|
||||
→ NoteHome (client bootstrap หน้าแรก) POST /api/note/auth/session
|
||||
→ NoteHome (client bootstrap หน้าแรก) POST /api/auth/session (§0.6 — เดิม /api/note/auth/session)
|
||||
→ server verify credential (ทาง A JWT purpose=login / ทาง B ticket + fail-closed
|
||||
เมื่อ secret เป็นค่า default "super_secret")
|
||||
→ ลงทะเบียนเอกสาร N8 แบบ trusted เลย (fid จาก credential — ไม่ต้องมี link proof)
|
||||
|
|
@ -53,6 +53,17 @@ credential อายุสั้น ฝังอีเมล/แชท/เก็
|
|||
- **คง status quo เส้น content ที่ browser เปิดแบบไม่ผ่าน fetch()** (ตามคำตอบผู้ใช้ 2026-10-30): SSE (EventSource) / `/api/note/pdf` + `<img>` N3 / export PDF — ไม่บังคับ Bearer (เดิมก็ไม่มี auth) · หน้าสรุป/หน้ากลุ่มยังเป็น server component จึงกลายเป็น public ไปด้วย — ยอมรับชั่วคราวตามมาตรฐานเดียวกัน (จะเปลี่ยนเป็น client+API เมื่อเรียกข้อเกิน)
|
||||
- **contract กับ e-Court เปลี่ยนเฉพาะปลายทางลิงก์**: `{BASE}/note?…` แทน `{BASE}/api/note/auth/e-court?…` (mint ทาง A และ ticket ทาง B ไม่เปลี่ยน) — ส่วนที่เหลือของแผน (§1 D-rows, §3.1, §6, §8) ให้อ่านผ่านเลนส์ของ §0.5 นี้
|
||||
|
||||
## 0.6 ฉบับทางเข้ารวม `/api/auth/*` (2026-11-09 — คำขอผู้ใช้ "note กับ add-in ใช้ auth เดียวกัน")
|
||||
|
||||
**ยุบ route คู่ที่เคย copy กันให้เป็นทางเข้าเดียว** (จุดกระตุ้น: `/api/auth/session` ที่ copy มา 2026-11-05 ตัด flow N11 ของลิงก์ fid-only ทิ้ง — drift จริง) · **รอบสอง (วันเดียวกัน ตามคำขอผู้ใช้ "ทำให้มีแค่ /api/auth/token + /api/auth/session" + "ของที่เกี่ยวกับ add-in ไม่ต้องลบ")**: **ถอดเส้นฝั่ง note ทั้งสอง (`/api/note/auth/token`, `/api/note/auth/session`) ออกถาวร** — โค้ดกลับมา inline ใน route ของ `/api/auth/*` แบบเดิม (ไม่มี lib handler แยก) · **`/api/documents/token` ของกลุ่ม docver คงไว้** เป็น alias re-export ของ `/api/auth/token` (function เดียวกันเป๊ะ — ของ add-in/e-Court เดิมไม่ถอด) · e-Court ที่ยังยิงเส้น note เก่าต้องย้าย:
|
||||
|
||||
- **`POST /api/auth/token` = ทางเข้า token เดียวของทั้งระบบ** (โค้ดเต็มใน route): x-api-key + body เดิม = docver-token 7 วัน (alias `/api/documents/token` — contract เดิมเป๊ะ) · x-api-key + **`purpose:"login"`** (+`ttl_seconds` 60–600) = login-token อายุสั้น (แทนเส้น mint เดิมของ §3.1) · ticket ของ add-in เหมือนเดิม
|
||||
- **`POST /api/auth/session` = ทางเข้า session เดียวของแอป note** (โค้ดเต็มใน route ฉบับครบ flow N11) · `NoteHome` ยิงเส้นนี้
|
||||
- **TTL ยังแยกตาม purpose** (ตามข้อตกลงกับผู้ใช้ "แยกไว้ก่อน"): login ลิงก์ 60–600 วิ · session 8 ชม. · docver 7 วัน — secret เดียวกัน `DOCVER_TOKEN_SECRET` แยกสิทธิ์ด้วย `purpose` (D4) · response shape `{access_token, token_type, expires_in}` ตรง OAuth2 อยู่แล้ว วันหนึ่งเพิ่ม grant `refresh_token` ได้โดยไม่ break (จดใน §11)
|
||||
- **โค้ด client กลาง**: `utils/auth-store.ts` (pure — localStorage store + memory fallback + `jwtExp`/`isExpired`) — ฝั่ง note ใช้ผ่าน `lib/note-session.ts` (exp ตอนนี้ decode จาก JWT payload = นาฬิกา server) · **วางไว้ที่ `utils/` เพื่อให้ wordaddin import ได้ตามกติกา client-only zone** — add-in ย้าย `ticketService` มาใช้รอบหน้า
|
||||
- **นโยบาย token หมดอายุ (คำถามผู้ใช้): เอาออกจาก session เลย** — token ที่ตายแล้วไม่มีทางฟื้น (browser ไม่มี ticket ให้ renew แบบ add-in; renew ของ add-in ก็ไปทาง ticket ไม่ใช่ token เก่า): `readNoteSession` ล้างทิ้งตอนอ่าน (เดิม) + **กลาง session: Server Action ล้มด้วย auth (message sentinel ของ `requireUser` — helper `isNoteAuthExpiredError` + `reportActionError` ใน PDFViewer) = `clearNoteSession` + พาไป `/note?sso=expired`** จบที่แบนเนอร์เดียวกับตอน bootstrap 401 ไม่ปล่อย error toast ค้างให้กดอะไรไม่ได้อีก
|
||||
- กัน drift กลับมา: `/api/documents/token` เป็น re-export ของ `/api/auth/token` (function เดียวกันเป๊ะ — มี test `toBe` ยึดไว้ใน `tests/app/api/auth/token.test.ts` กัน copy แยกชุดอีก)
|
||||
|
||||
## 1. ข้อตกลงที่ตัดสินใจแล้ว (2026-10-28)
|
||||
|
||||
| # | เรื่อง | ข้อตกลง |
|
||||
|
|
@ -60,7 +71,7 @@ credential อายุสั้น ฝังอีเมล/แชท/เก็
|
|||
| D1 | ตัวพา identity | 2 รูปแบบ (D12): **(A)** JWT จากทางเข้า `x-api-key` ของ **`POST /api/note/auth/token` — เส้น mint ใหม่ของแผนนี้ แยกจาก `/api/documents/token` ของกลุ่ม docver** (คำขอผู้ใช้ 2026-10-29 — ไม่แต้ contract docver ใช้ lib `docver-token` เดิม) ฝังเป็น param เดียว `t=` · **(B)** `ticket_data`+`HmacSha256` (ชุดเดียวกับตัว stamp .docx ของ add-in) |
|
||||
| D2 | จุดลงจอ | **route handler `GET /api/note/auth/e-court`** — set cookie ได้ตามกติกา Next (page render ทำไม่ได้) + middleware ยกเว้น `api/note` อยู่แล้ว (route นี้เองไม่ต้องแต้ middleware — ชุดที่แต้ตาม D15 เป็นเรื่องหน้าแรก public คนละเรื่อง) |
|
||||
| D3 | session | แปลงเป็น cookie `oacCase` รูปเดิมผ่าน `buildOacCaseCookie(userid, username)` (identity ไหลใน cookie เดิม: `userid`→u_userid, `username`→u_userlogin) **+ คู่ `oacCaseSig` ตาม D16 เสมอ** — cookie เดิมลอยตัวไม่มีลายเซ็น = ใครก็ปลอม `u_userid` ข้าม SSO ได้ทั้งหมด · `lib/session.ts` ต้องแต้แล้ว (verify signature — ดู D16) |
|
||||
| D4 | แยกสิทธิ์ token | เพิ่ม claim `purpose: "docver" \| "login"` เซ็นด้วย secret เดียวกัน (`DOCVER_TOKEN_SECRET`) — **mint login token ได้จาก `POST /api/note/auth/token` เท่านั้น** ส่วน `/api/documents/token` ไม่รับ/ไม่ออก `purpose` อีก (ส่ง field มา = 400 ตาม allowlist เดิม) · `requireDocverBearer` รับเฉพาะ `docver` (undefined = docver เพื่อ token เดิมในมือ user ตอน roll ไม่ตาย), landing route รับเฉพาะ `login` — กัน token ใน URL ถูกจับไปใช้กับ API versioning (scope escalation) |
|
||||
| D4 | แยกสิทธิ์ token | เพิ่ม claim `purpose: "docver" \| "login"` เซ็นด้วย secret เดียวกัน (`DOCVER_TOKEN_SECRET`) — **mint login token ได้จาก `POST /api/note/auth/token` เท่านั้น** ส่วน `/api/documents/token` ไม่รับ/ไม่ออก `purpose` อีก (ส่ง field มา = 400 ตาม allowlist เดิม) · `requireDocverBearer` รับเฉพาะ `docver` (undefined = docver เพื่อ token เดิมในมือ user ตอน roll ไม่ตาย), landing route รับเฉพาะ `login` — กัน token ใน URL ถูกจับไปใช้กับ API versioning (scope escalation) · **อัปเดต 2026-11-09 (§0.6): mint login token ทาง `POST /api/auth/token` ด้วย `purpose:"login"` ใน body ได้เพิ่ม (ค่าอื่น = 400) — เส้น mint เดิม `/api/note/auth/token` ถอดแล้วในรอบสอง** |
|
||||
| D5 | TTL ของ login token | body `ttl_seconds` ทางเลือกช่วง 60–600 default 300 (ของเส้น mint ใหม่ — เป็น login-token เอกสิทธิ์ ไม่ต้องส่ง `purpose` ใน body) — credential ใน URL ต้องอายุสั้น · ทางเข้า docver TTL ตาม env เดิมไม่เปลี่ยน |
|
||||
| D6 | อายุ session | cookie 8 ชม. เหมือน mock login — token สั้นเฉพาะตอน "ขอเข้า" (แบบ SSO code flow) · ไม่มี revoke (เท่า status quo ปัจจุบัน) |
|
||||
| D7 | mock login | **ถอดออกทั้งเส้น** (MOCK_USERS + Demo1234 + LoginForm + action `login`) — ลิงก์ e-Court คือทางเข้าเดียวของ production · **หน้า `/note/login` ถอดทั้งหน้า ไม่เหลือหน้าข้อมูล** (ปรับแผน 2026-10-29 — ปลายทางข้อความต่าง ๆ ย้ายไปแบนเนอร์ของหน้าแรก D14) · dev/ทดสอบ mint ลิงก์ด้วยสคริปต์ D10 |
|
||||
|
|
@ -106,16 +117,16 @@ credential อายุสั้น ฝังอีเมล/แชท/เก็
|
|||
**ขั้น 1 (ทาง A) — mint token (server-to-server ของ backend e-Court):**
|
||||
|
||||
```
|
||||
POST {BASE}/api/note/auth/token
|
||||
POST {BASE}/api/auth/token
|
||||
x-api-key: <DOCVER_API_KEY ที่ใช้อยู่>
|
||||
content-type: application/json
|
||||
|
||||
{"userid":"<u_userid>","username":"<u_userlogin>","fid":"<fileId ของ e-Court>","ttl_seconds":300}
|
||||
{"userid":"<u_userid>","username":"<u_userlogin>","fid":"<fileId ของ e-Court>","ttl_seconds":300,"purpose":"login"}
|
||||
|
||||
→ 200 {"access_token":"…","token_type":"Bearer","expires_in":300}
|
||||
```
|
||||
|
||||
(เส้นนี้เป็น login-token เอกสิทธิ์ — ไม่ต้องส่ง `purpose` ใน body · แยกจาก `/api/documents/token` ของกลุ่ม docver ไม่ปนกัน)
|
||||
(ทางเข้ารวม §0.6 — `purpose:"login"` บอกว่าต้องการ login-token · **เส้น mint เดิม `POST /api/note/auth/token` ถอดแล้ว (2026-11-09 รอบสอง) — e-Court ที่ยังยิง URL เดิมต้องย้ายมาเส้นนี้** · ไม่ส่ง `purpose` ที่ `/api/auth/token` = docver-token ของกลุ่ม documents — อย่าสับสนสองอย่างนี้ใน codebase เดียวกัน)
|
||||
|
||||
**ขั้น 2 — ประกอบลิงก์ให้ผู้ใช้คลิก (เลือกทางใดทางหนึ่ง — ห้ามส่งสองชุดพร้อมกัน):**
|
||||
|
||||
|
|
@ -143,7 +154,7 @@ content-type: application/json
|
|||
|
||||
## 3. ฝั่ง server — endpoint ที่เปลี่ยน/เพิ่ม
|
||||
|
||||
### 3.1 `POST /api/note/auth/token` (ใหม่ — mint login token ทางเข้า x-api-key, ไม่ import `@/db`)
|
||||
### 3.1 `POST /api/note/auth/token` (mint login token — **ถอดแล้ว 2026-11-09 รอบสอง**: ทางเข้ารวมอยู่ที่ `POST /api/auth/token` แบบ `purpose:"login"` ตาม §0.6 · เนื้อหาข้างล่างเป็น contract ที่ย้ายไปเส้นรวมทั้งหมด)
|
||||
|
||||
| เคส | พฤติกรรม |
|
||||
|---|---|
|
||||
|
|
@ -155,7 +166,7 @@ content-type: application/json
|
|||
- reuse จาก lib เดิมทั้งหมด: `requireDocverApiKey` (เทียบ `DOCVER_API_KEY`) + `issueDocverToken` (ส่ง purpose="login" + ttl เข้าไป) — **ห้าม log ค่า key/body ดิบ**
|
||||
- **`POST /api/documents/token` ของกลุ่ม docver ไม่ถูกแต้จากแผนนี้แล้ว** — contract เดิมคงเดิมทั้งหมด (ส่ง `purpose`/`ttl_seconds` มา = 400 ตาม allowlist เดิม) เหลือแค่ระดับ lib: `issueDocverToken` default แนบ `purpose:"docver"` ให้ token ใหม่โดย route docver ไม่ต้องแต้ และ `requireDocverBearer` (ทุก endpoint กลุ่ม docver) reject `purpose === "login"` — 401 ข้อความเดิม (กันเอา login token ไปยิง API versioning — D4)
|
||||
|
||||
### 3.2 `POST /api/note/auth/session` (ฉบับ token 2026-10-30 — แทน GET e-court ที่ถอดแล้ว)
|
||||
### 3.2 `POST /api/auth/session` (ฉบับ token 2026-10-30 — แทน GET e-court ที่ถอดแล้ว · **เส้นเดิม `/api/note/auth/session` ถอดแล้ว 2026-11-09 รอบสอง** — ทางเข้า session เดียว)
|
||||
|
||||
body: `{t} | {ticket_data, HmacSha256}` + optional `{fileUrl, typeId}` (allowlist 5 field — `parseSessionExchangeBody`)
|
||||
|
||||
|
|
@ -197,19 +208,21 @@ readNoteSession() / saveNoteSession() / clearNoteSession() / noteAuthFetch(path,
|
|||
|
||||
```
|
||||
ผู้ใช้คลิกลิงก์ใน e-Court → browser ไป /note?fileUrl=&typeId=&t=… (หรือ ticket)
|
||||
→ NoteHome (client bootstrap) POST /api/note/auth/session {credential + fileUrl + typeId}
|
||||
→ NoteHome (client bootstrap) POST /api/auth/session {credential + fileUrl + typeId} (§0.6 เส้นสั้น)
|
||||
→ server: verify (A: verifyDocverLoginToken / B: verifyAddinTicket + fail-closed default secret)
|
||||
→ registerExternalFile(fileUrl, fid, typeId, {trusted:true}) → fileId
|
||||
→ 200 {access_token (8 ชม.), userid, username, fileId?}
|
||||
→ client: saveNoteSession (localStorage "note-auth") + strip credential ออกจาก URL
|
||||
→ client: saveNoteSession (localStorage "note-auth" — expMs จาก jwtExp ของ JWT payload) + strip credential ออกจาก URL
|
||||
→ มี fileId = /note/annotate/<rowId> → AnnotateView (client gate)
|
||||
→ GET /api/note/files/<id>/bootstrap (Bearer) → PDFViewer (prop token)
|
||||
→ ทุก Server Action รับ token เป็น param แรก — identity แกะฝั่ง server เสมอ
|
||||
```
|
||||
|
||||
- ผู้ใช้มี session token เก่าอยู่ในเครื่อง → **ลิงก์ชนะ** (bootstrap เขียนทับใหม่เสมอ — e-Court
|
||||
เป็นผู้ยืนยันตัวตนปัจจุบัน) · token หมดอายุกลางวัน = action โยน error/หน้า 401 → กลับ e-Court
|
||||
กดลิงก์ใหม่ (ไม่มี renew — เว็บไม่มี ticket ฝังไฟล์แบบ add-in)
|
||||
เป็นผู้ยืนยันตัวตนปัจจุบัน) · token หมดอายุกลางวัน = **§0.6: PDFViewer จับ auth-error
|
||||
ของ action (`isNoteAuthExpiredError` ผ่าน `reportActionError`) → ล้าง session + พาไป
|
||||
`/note?sso=expired`** → กลับ e-Court กดลิงก์ใหม่ (ไม่มี renew — เว็บไม่มี ticket
|
||||
ฝังไฟล์แบบ add-in)
|
||||
- ไม่มี credential ใน URL → `/note` = landing public (แบนเนอร์ `?sso=` ของลิงก์เก่ายังรองรับ)
|
||||
- samples หน้า `/note/dev` → `GET /api/note/files/open?fileUrl=/samples/…` (ลิงก์แบบ /tmpl, trusted:false — แถว `url:…` เท่านั้น)
|
||||
- **ลิงก์ fid-only ของเอกสาร staging (N11 2026-11-04):** e-Court POST เนื้อ base64 ที่
|
||||
|
|
@ -221,7 +234,8 @@ readNoteSession() / saveNoteSession() / clearNoteSession() / noteAuthFetch(path,
|
|||
|
||||
**สร้าง**
|
||||
|
||||
- `app/api/note/auth/token/route.ts` — §3.1 mint login token (runtime nodejs, force-dynamic)
|
||||
- **(§0.6 2026-11-09 — รอบสอง)** `app/api/auth/token/route.ts` + `app/api/auth/session/route.ts` โค้ดเต็ม inline (เส้นฝั่ง note `/api/note/auth/token` + `/api/note/auth/session` **ถอดทั้งคู่**; lib handler ที่ใช้ชั่วระหว่างรอบแรกถอดตาม) · `app/api/documents/token/route.ts` = alias re-export ของ `/api/auth/token` (กลุ่ม docver คงไว้ตามคำขอผู้ใช้) · `utils/auth-store.ts` — primitives กลางฝั่ง client (localStorage + memory fallback + `jwtExp`/`isExpired`) + `tests/utils/auth-store.test.ts` + `tests/app/api/auth/token.test.ts` + `tests/api/auth-session-staged.test.ts`
|
||||
- `app/api/note/auth/token/route.ts` — §3.1 mint login token (runtime nodejs, force-dynamic) — **ถอดแล้ว 2026-11-09 §0.6 รอบสอง (ทางเข้ารวมที่ `/api/auth/token`)**
|
||||
- `app/api/note/auth/e-court/route.ts` — §3.2 (runtime nodejs, force-dynamic)
|
||||
- `lib/note-auth.ts` — §4 (รวม `signOacSession`/`verifyOacSession` ของ D16) + `tests/lib/note-auth.test.ts` (รวมเคส: คู่ถูกต้องผ่าน · แก้ payload 1 ตัวอักษร/แก้ exp เฉย ๆ ไม่ผ่าน · exp อดีตไม่ผ่าน · ไม่มี secret = false)
|
||||
- `scripts/make-login-link.mjs` — `node scripts/make-login-link.mjs --uid oom --uname สรวิชญ์ --fid F123 --fileUrl https://… --typeId T12 [--ttl 300] [--mode jwt|ticket] [--secret <ตาม mode: DOCVER_TOKEN_SECRET | ADDIN_TICKET_SECRET>]` → พิมพ์ลิงก์เต็ม (jwt = node:crypto HS256 ตรงตาม `lib/docver-token.ts` · ticket = HMAC ตรงตาม `lib/addin-ticket.ts` โดย exp มาจาก `--ttl` default 300)
|
||||
|
|
@ -288,7 +302,7 @@ readNoteSession() / saveNoteSession() / clearNoteSession() / noteAuthFetch(path,
|
|||
1. `npx vitest run` ผ่านทั้งชุด (เพิ่ม `tests/lib/note-auth.test.ts` + เคสใหม่ของ `docver-token` — เคสเดิมไม่พัง)
|
||||
2. `npx tsc --noEmit` ผ่าน
|
||||
3. curl regression:
|
||||
- (ก) mint: `x-api-key` + `{userid,username,fid,ttl_seconds:300}` ที่ **`/api/note/auth/token`** → ได้ token ที่ decode แล้วมี `purpose:"login"` · ส่ง `purpose` มาด้วย = 400
|
||||
- (ก) mint: `x-api-key` + `{userid,username,fid,ttl_seconds:300,purpose:"login"}` ที่ **`/api/auth/token`** → ได้ token ที่ decode แล้วมี `purpose:"login"` (§0.6 — เส้น mint เดิม `/api/note/auth/token` ถอดแล้ว)
|
||||
- (ข) `GET /api/note/auth/e-court?fileUrl=…&t=<login token>` → 302 + `Set-Cookie oacCase` + Location เป็น `/note?…` **ไม่มี `t`** + `Cache-Control: no-store`
|
||||
- (ค) รอให้ TTL หมด (ตั้ง 60 วิ) → 302 `/note?sso=expired`
|
||||
- (ง) เอา token purpose=docver มาใช้ที่ route → `sso=invalid` · เอา login token ไป `GET /api/documents/versions` → 401
|
||||
|
|
@ -299,20 +313,23 @@ readNoteSession() / saveNoteSession() / clearNoteSession() / noteAuthFetch(path,
|
|||
- (ฎ) หน้าแรกใหม่ D14/D15 (ปรับตาม 2026-10-30): ไม่มี session เปิด `/note` = เห็น landing เรียบ และ `/note/dev` = เห็น dropdown 2 ระดับ (public ทั้งคู่ ไม่โดน redirect) · เลือกกลุ่ม → ไฟล์ แล้วกด "เปิดเอกสาร" บน `/note/dev` โดยไม่มี session = เด้งกลับ `/note?sso=required` มีแบนเนอร์ · มี session = ลง viewer ของไฟล์ที่เลือก · middleware ไม่จับ `/note` และ `/note/dev` แต่ยังจับ `/note/annotate/…` · **ไม่มี session เปิด `/note?fileUrl=…&fileId=<ที่มีอยู่แล้ว>` ตรง ๆ = redirect `sso=required` และ DB ไม่เปลี่ยน** (ไม่มี row ใหม่, `pdf_url`/`type_id` เดิม — เทียบก่อน/หลัง)
|
||||
- (ฏ) D16 session signing: ผ่านลิงก์จริงแล้วตั้ง cookie เดียวแบบเดิม (`oacCase` ไม่มี `oacCaseSig` — คัดลอกค่าจาก DevTools ใส่เอง) → เข้า annotate ไม่ได้ redirect `sso=required` · มีคู่ครบแต่แก้ `u_userid` ใน `oacCase` 1 ตัวอักษร → ไม่ผ่านเช่นกัน · แก้เฉพาะ `exp` ใน `oacCaseSig` ยืดอายุออก → ไม่ผ่าน (exp ถูกเซ็น) · response ของ auth route มี `Set-Cookie` **สองตัว** + `Secure` เมื่อรัน production (ตรวจจาก response header) · `NOTE_SESSION_SECRET` ว่าง → auth route ตอบ `sso=error` และทุกหน้ามองเป็นไม่มี session · `getSessionUser` async แล้ว `npx tsc --noEmit` ผ่าน (call site ครบ)
|
||||
4. ทดสอบเบราว์เซอร์ด้วย `scripts/make-login-link.mjs`: วางลิงก์ → ลง viewer ชื่อผู้ใช้ = `uname` · เปิดลิงก์ uid คนละคนต่อ → session สลับตามลิงก์ · ลิงก์หมดอายุ → กลับหน้าแรกพร้อมแบนเนอร์ "ลิงก์หมดอายุ — กลับ e-Court กดเปิดใหม่" · เข้า `/note` ตรง ๆ ไม่มี session → landing เรียบ (public ไม่มีแบนเนอร์) · ที่ `/note/dev` กดปุ่ม "เปิดเอกสาร" → แบนเนอร์ "กรุณาเข้าสู่ระบบผ่าน e-Court" (`sso=required`)
|
||||
5. รัน `plan-reviewer` + `code-reviewer` ตามกติกา `.claude/`
|
||||
5. **(§0.6)** ทางเข้ารวม: mint `purpose:"login"` ที่ `/api/auth/token` → ลิงก์ `/note?t=` → NoteHome แลกที่ `/api/auth/session` (network tab) → เข้า viewer · alias `/api/documents/token` body เดิมยังผ่าน (กลุ่ม docver คงไว้) · **เส้น note เก่า `/api/note/auth/token` + `/api/note/auth/session` ตอบ 404** · `/api/auth/session` กับลิงก์ fid-only ของ staging → ได้ `fileId` (N11 ครบ) · **หมดอายุกลาง session**: แก้ `expMs` ใน localStorage เป็นอดีต → สร้าง annotation → ถูกพาไป `/note?sso=expired` + localStorage `note-auth` ถูกล้าง (ไม่ใช่ error toast เปล่า ๆ)
|
||||
6. รัน `plan-reviewer` + `code-reviewer` ตามกติกา `.claude/`
|
||||
|
||||
## 10. ลำดับงาน
|
||||
|
||||
0. **add-in D13** — ~~ถอด client HMAC check + `NEXT_PUBLIC_ADDIN_TICKET_SECRET` ออกจาก bundle (+ ปรับ tests/docs)~~ **✅ ทำแล้ว 2026-11-04** (พร้อม refactor ใหญ่ของ PLAN-ADDIN-AUTH.md: contract 400/401/403 + TTL 7 วัน + fallback onReady) → verify 9.3 (ฐ)
|
||||
1. `lib/docver-token.ts` (purpose + ttl) + tests → verify 9.1 บางส่วน
|
||||
2. `lib/note-auth.ts` + tests (รวม `signOacSession`/`verifyOacSession` ของ D16)
|
||||
3. `app/api/note/auth/token/route.ts` (mint) → verify 9.3 (ก)(จ) แล้ว `app/api/note/auth/e-court/route.ts` → verify 9.3 (ข–ง)
|
||||
3. `app/api/note/auth/token/route.ts` (mint — ภายหลังถอดตาม §0.6 รอบสอง) → verify 9.3 (ก)(จ) แล้ว `app/api/note/auth/e-court/route.ts` → verify 9.3 (ข–ง)
|
||||
4. ถอด mock login ทั้งเส้น (`LoginForm`/action `login`/`MOCK_USERS` + **หน้า `/note/login` ทั้งหน้า**) + rework หน้าแรก `/note` ใหม่ (D14) + middleware (D15) + งาน session signing ของ D16 (`utils/user.ts`/`lib/session.ts`/logout/หน้า caller `await`) → verify 9.3 (ฉ)(ฎ)(ฏ) + 9.4
|
||||
5. `scripts/make-login-link.mjs` → verify 9.4
|
||||
6. `lib/openapi.ts` + `.env.example` + `CLAUDE.md` sync → verify 9.1–9.5
|
||||
|
||||
## 11. Out of scope
|
||||
|
||||
- **add-in ย้ายมาใช้โค้ดกลางของ §0.6** (รอบหน้า — ผู้ใช้กำหนด "รอบนี้เอาแค่ของ note มาใช้ก่อน"): `wordaddin/services/ticketService.ts` internals → `utils/auth-store.ts` + prune token หมดอายุออกจาก map ตอนโหลด + ลบ entry ของ fid หลัง renew จาก ticket ล้ม — wire contract/flow/UI ไม่เปลี่ยน (`utils/auth-store` ออกแบบรองรับอยู่แล้ว)
|
||||
- **refresh token** — วันหนึ่งอยากให้ browser/add-in ต่ออายุโดยไม่กลับ e-Court: เพิ่ม grant ใหม่ที่ `/api/auth/token` (มาตรฐาน OAuth2 + rotation) — response shape ปัจจุบันรองรับอยู่แล้ว · ข้อควรชั่ง: refresh token ใน localStorage ของ browser = ยืด session โดยไม่มีผู้ใช้ยืนยัน ขัดเหตุผลความปลอดภัยของ TTL 8 ชม. เดิม
|
||||
- ~~แก้ secret รั่ยใน bundle ของ add-in~~ — ย้ายเข้าแผนนี้เป็น **D13** แล้ว (ทาง ticket ทำให้ secret ตัวนี้กลายเป็นจุดอ่อนของ login โดยตรง จึงตัดออกก่อนเปิดทาง ticket)
|
||||
- revoke/refresh token, logout ระดับ e-Court, roles/สิทธิ์จริง, ผูก session กับ device
|
||||
- OAuth/OIDC เต็มรูปแบบ (e-Court เป็น IdP) — ไปทางนี้เมื่อมีหลายแอปของศาลต้อง SSO
|
||||
|
|
|
|||
|
|
@ -13,7 +13,7 @@
|
|||
3. **ไม่มี dropzone/ลากไฟล์บนแอป** — ผู้พิพากษาเปิดเอกสารผ่าน e-Court ทางเดียว (ตัดทางเลือก `local:<hash>` ทั้งหมดที่เคยพิจารณา)
|
||||
4. **แอปมีหน้าที่แค่ "รองรับได้"** — เปิด endpoint + contract ให้ e-Court เรียก ไม่รับผิดชอบ flow ฝั่ง e-Court (เรียกตอนไหน/กี่ครั้ง) · contract ต้องเข้า Swagger UI (`lib/openapi.ts`) เหมือนกลุ่มอื่น
|
||||
5. **เก็บ bytes ใน RAM ของ process เท่านั้น** (globalThis แบบ `lib/annotate-events.ts`) — sliding TTL + LRU + cap · **ห้าม**ใช้ MinIO/disk แม้แต่แบบมี TTL (ความหมาย "ไม่เก็บไฟล์" เคร่งครัด) · **รันได้เฉพาะ single process — ดู D7 deploy**
|
||||
6. **auth ใช้ `x-api-key` เทียบ env `DOCVER_API_KEY`** ผ่าน `requireDocverApiKey` (`lib/docver-token.ts` — timing-safe แล้ว: SHA-256 ทั้งสองฝั่ง + `timingSafeEqual`, fail-closed + ข้อความ 401 เดียวกันทั้งไม่ใส่/ผิด) — กลุ่มเดียวกับทางเข้า mint ของ e-Court (`POST /api/note/auth/token`) · (ทางเลือก `EXTERNAL_API_KEY` ของ N10 เป็นฝั่งอ่าน ไม่ใช้กับฝั่งเขียน)
|
||||
6. **auth ใช้ `x-api-key` เทียบ env `DOCVER_API_KEY`** ผ่าน `requireDocverApiKey` (`lib/docver-token.ts` — timing-safe แล้ว: SHA-256 ทั้งสองฝั่ง + `timingSafeEqual`, fail-closed + ข้อความ 401 เดียวกันทั้งไม่ใส่/ผิด) — กลุ่มเดียวกับทางเข้า mint ของ e-Court (`POST /api/auth/token` แบบ `purpose:"login"` — ทางเข้ารวม 2026-11-09 ตาม PLAN-SSO.md §0.6, เส้นเดิม `/api/note/auth/token` ถอดแล้ว) · (ทางเลือก `EXTERNAL_API_KEY` ของ N10 เป็นฝั่งอ่าน ไม่ใช้กับฝั่งเขียน)
|
||||
7. **ไม่มี schema migration** — ไม่เพิ่ม column: ใช้ sentinel ใน `pdf_url` คือ **`staged:<uuid>.<ext>`** (มี extension ติดไปเสมอ) เพื่อให้ `fileKindFromUrl`/`imageFormatFromUrl`/`imageContentTypeFromUrl` ทำงานทุกจุดโดยไม่แก้ (extension คือ single source of truth ของชนิดไฟล์ตาม N3)
|
||||
|
||||
## Design
|
||||
|
|
@ -91,7 +91,7 @@
|
|||
3. **`PDFViewer.tsx`** — (ก) branch รูปภาพ `:1744`: `isHttpUrl(pdfUrl)` → เพิ่ม `|| pdfUrl.startsWith("staged:")` ให้โหลดผ่าน `/api/note/pdf?fileId=` (PDF branch ไม่ต้องแตะ — fetch ผ่าน route นี้อยู่แล้ว) · (ข) **ตัด fallback** `fetch(withBasePath(file.pdfUrl))` เมื่อ `staged:` (โหลดจาก sentinel ตรง ๆ ไม่มีความหมาย) — ให้ error จาก route (410) ลอยขึ้นเป็นข้อความ gate ไทยแทน garbage fetch
|
||||
4. **`lib/server-pdf.ts` — `resolvePublicPdfPath` เพิ่ม guard ต้นฟังก์ชัน:** `pdfUrl.startsWith("staged:")` → `throw new PdfPathError("เอกสารนี้ไม่มีไฟล์บนดิสก์ (staging พ้นช่วงจัดเตรียม)")` — **ผลตรวจโค้ดจริง: วันนี้ `"staged:x.pdf"` ผ่าน guard** (`path.resolve(publicDir, …)` ตกใน publicDir) **แล้วตายด้วย ENOENT เป็น 500 ดิบ** (Windows อาจ EINVAL เพราะ colon ในชื่อ) — เป็น defense in depth ให้ทุก caller ในอนาคตที่ลืมแยกทาง
|
||||
|
||||
### D5 — Session route รับลิงก์ fid-only + เช็ค staging ตั้งแต่ต้น (`app/api/note/auth/session/route.ts`)
|
||||
### D5 — Session route รับลิงก์ fid-only + เช็ค staging ตั้งแต่ต้น (`/api/auth/session` ทางเข้าเดียว — เส้นเดิม `/api/note/auth/session` ถอดแล้ว 2026-11-09 ตาม PLAN-SSO.md §0.6 รอบสอง)
|
||||
|
||||
ตอนนี้ `parsed.fileUrl !== undefined` เท่านั้นที่ลงทะเบียน — เอกสาร staging ลิงก์มีแค่ `t=` (fid อยู่ใน credential): เติม branch **`fileUrl` ไม่มี + มี `fid`** → select row `ecourt:<hash ของ fid>` (lib/ecourt-fid):
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue