eCourt-note/lib/note-session.ts

75 lines
2.9 KiB
TypeScript
Raw Normal View History

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>
2026-10-30 18:05:36 +07:00
// 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}` },
});
}