feat(sso): switch note session from cookies to token
เปลี่ยน session ของแอป note จาก cookie คู่ oacCase/oacCaseSig มาเป็น access_token แบบ add-in ตามคำขอผู้ใช้ (2026-10-30): - POST /api/note/auth/session แลก credential ของลิงก์ e-Court (JWT ทาง A / ticket ทาง B) เป็น access_token purpose=login อายุ 8 ชม. พร้อมลงทะเบียนเอกสาร N8 แบบ trusted ฝั่ง server (fileId จาก fid) - token เก็บ localStorage "note-auth" (lib/note-session.ts) + Bearer ผ่าน noteAuthFetch · Server Actions ทุกตัวรับ token เป็น param แรก และแกะ identity เอง (lib/note-token.ts) — ไม่เชื่อ userId จาก client - หน้า protected เป็น shell + client gate (NoteHome bootstrap ที่ /note, AnnotateView + GET /api/note/files/[id]/bootstrap) - ถอด: middleware.ts, lib/session.ts, utils/user.ts, action logout, GET /api/note/auth/e-court, entry /note/annotate?fileUrl= - ทาง ticket ปฏิเสธ secret default "super_secret" เฉพาะ production - GET /api/note/files/open: ลิงก์เปิดไฟล์ตัวอย่างแบบ /tmpl (path ใน เครื่อง /samples เท่านั้น) - หน้า /note/dev: ลิงก์ A Link Get(new) ที่ mint credential แล้วยิง เข้า flow SSO จริง (JWT/ticket สลับได้) + section Flow + เรียบเรียง ภาษาให้อ่านง่าย · หน้า /note เป็นหน้าข้อมูลระบบ - แก้ตาม code review: redo closure เรียก action ด้วย signature เก่า, bootstrap timeout + strip credential ที่ผิดรูป, แยก no-session ออก จาก network error, ห้ามอักขระ & = + % ใน identity, ตรวจ purpose ก่อน exp ของ login-token, audit log strip อักขระควบคุม - เอกสาร: docs/PLAN-SSO.md §0.5 (ฉบับ token), CLAUDE.md, .env.example ทดสอบ: tsc ผ่าน · vitest ผ่านทั้งชุด · ทดสอบจริงผ่าน Playwright (SSO → viewer → เปลี่ยนสี annotation → undo → redo ครบวงจร) Co-Authored-By: Claude Code <noreply@anthropic.com>
This commit is contained in:
parent
b1449ab668
commit
d37bc2a3f0
49 changed files with 2921 additions and 1307 deletions
74
lib/note-session.ts
Normal file
74
lib/note-session.ts
Normal file
|
|
@ -0,0 +1,74 @@
|
|||
// session ของแอป note ฝั่ง client (ฉบับ token 2026-10-30 ตามคำขอผู้ใช้ "note ใช้
|
||||
// แบบ add-in" — pattern เดียวกับ wordaddin/services/authService): access_token
|
||||
// + identity เก็บใน localStorage ("note-auth") หลังแลกจาก POST /api/note/auth/
|
||||
// session · การเรียก API ของแอปผ่าน noteAuthFetch แนบ Authorization: Bearer
|
||||
// อัตโนมัติ · token หมดอายุแล้วถือว่าไม่มี session (ล้างทิ้ง) — localStorage อาจ
|
||||
// โยน (private mode/quota) ทุกจุดจึง try/catch
|
||||
//
|
||||
// client-only — ห้าม import จาก server component/module ฝั่ง server
|
||||
import { withBasePath } from "@/utils/base-path";
|
||||
|
||||
export type NoteSession = {
|
||||
token: string;
|
||||
userid: string;
|
||||
username: string;
|
||||
/** exp epoch มิลลิวินาที — พ้นเวลานี้ถือว่าไม่มี session */
|
||||
expMs: number;
|
||||
};
|
||||
|
||||
const STORAGE_KEY = "note-auth";
|
||||
|
||||
export function readNoteSession(): NoteSession | null {
|
||||
try {
|
||||
const raw = window.localStorage.getItem(STORAGE_KEY);
|
||||
if (raw === null) return null;
|
||||
const parsed = JSON.parse(raw) as Partial<NoteSession> | null;
|
||||
if (
|
||||
parsed === null ||
|
||||
typeof parsed !== "object" ||
|
||||
typeof parsed.token !== "string" ||
|
||||
parsed.token === "" ||
|
||||
typeof parsed.userid !== "string" ||
|
||||
parsed.userid === "" ||
|
||||
typeof parsed.username !== "string" ||
|
||||
typeof parsed.expMs !== "number"
|
||||
) {
|
||||
window.localStorage.removeItem(STORAGE_KEY);
|
||||
return null;
|
||||
}
|
||||
if (parsed.expMs <= Date.now()) {
|
||||
window.localStorage.removeItem(STORAGE_KEY);
|
||||
return null;
|
||||
}
|
||||
return parsed as NoteSession;
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
export function saveNoteSession(session: NoteSession): void {
|
||||
try {
|
||||
window.localStorage.setItem(STORAGE_KEY, JSON.stringify(session));
|
||||
} catch {
|
||||
// private mode/quota — เงียบไว้: ผู้ใช้เข้าผ่านลิงก์ e-Court ใหม่ได้เสมอ
|
||||
}
|
||||
}
|
||||
|
||||
export function clearNoteSession(): void {
|
||||
try {
|
||||
window.localStorage.removeItem(STORAGE_KEY);
|
||||
} catch {
|
||||
// ignore
|
||||
}
|
||||
}
|
||||
|
||||
/** fetch ของแอปพร้อม Bearer — path เป็น route API ของแอป ("/api/note/…")
|
||||
* ไม่มี session = throw "no-session" ให้ caller เปลี่ยนเป็นหน้า gate เอง */
|
||||
export async function noteAuthFetch(path: string, init: RequestInit = {}): Promise<Response> {
|
||||
const session = readNoteSession();
|
||||
if (session === null) throw new Error("no-session");
|
||||
return fetch(withBasePath(path), {
|
||||
...init,
|
||||
headers: { ...(init.headers ?? {}), authorization: `Bearer ${session.token}` },
|
||||
});
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue