eCourt-note/lib/note-session.ts
Your Name d37bc2a3f0 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

74 lines
2.9 KiB
TypeScript

// 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}` },
});
}