เปลี่ยน 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>
74 lines
2.9 KiB
TypeScript
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}` },
|
|
});
|
|
}
|