// session ของแอป note ฝั่ง client (ฉบับ token 2026-10-30 ตามคำขอผู้ใช้ "note ใช้ // แบบ add-in" — pattern เดียวกับ wordaddin/services/authService): access_token // + identity เก็บใน localStorage ("note-auth") หลังแลกจาก POST /api/auth/session // (2026-11-09 — ย้ายมาเส้นสั้น, ทางเข้ารวมของระบบ) · การเรียก API ของแอปผ่าน // noteAuthFetch แนบ Authorization: Bearer อัตโนมัติ · token หมดอายุแล้วถือว่า // ไม่มี session (ล้างทิ้ง — token ที่ตายแล้วไม่มีทางฟื้น browser ไม่มี ticket // ให้ renew ต้องกลับ e-Court กดลิงก์ใหม่) · storage โยน (private mode/quota) มี // memory fallback ผ่าน utils/auth-store // // client-only — ห้าม import จาก server component/module ฝั่ง server import { createAuthStore, isExpired, jwtExp } from "@/utils/auth-store"; import { withBasePath } from "@/utils/base-path"; export type NoteSession = { token: string; userid: string; username: string; /** exp epoch มิลลิวินาที — พ้นเวลานี้ถือว่าไม่มี session */ expMs: number; }; const store = createAuthStore("note-auth"); /** message sentinel ของ requireUser (app/actions/annotations.ts) — Server Action * throw ข้าม network เหลือ message อย่างเดียว จึงตรวจด้วย prefix ของข้อความ */ const AUTH_EXPIRED_SENTINEL = "session หมดอายุหรือไม่ถูกต้อง"; /** action/API ล้มเพราะ token หมดอายุ/ไม่ผ่าน (เทียบ message ของ requireUser) — * caller (PDFViewer) จับได้ = ล้าง session แล้วพาไปหน้า gate ไม่ปล่อย error ค้าง */ export function isNoteAuthExpiredError(err: unknown): boolean { return ( err instanceof Error && err.message.startsWith(AUTH_EXPIRED_SENTINEL) ); } /** exp (มิลลิวินาที) ของ session ใหม่ — decode จาก JWT payload (นาฬิกา server * แม่นกว่า expires_in ที่คำนวณจากนาฬิกา client) · fallback expires_in เมื่อ decode * ไม่ได้ · expires_in ผิดปกติ (undefined/Infinity — NaN จะทำ isExpired เป็น false * เสมอจน session ไม่ตายตามอายุ) = คืน 0 ให้ถือว่าหมดอายุทันทีแทนค่าขยะ */ export function noteExpMs(token: string, expires_in: number): number { const exp = jwtExp(token); if (exp !== undefined) return exp * 1000; return Number.isFinite(expires_in) ? Date.now() + expires_in * 1000 : 0; } export function readNoteSession(): NoteSession | null { const raw = store.readRaw(); if (raw === null) return null; try { const parsed = JSON.parse(raw) as Partial | 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" ) { store.remove(); return null; } if (isExpired(Math.floor(parsed.expMs / 1000))) { store.remove(); return null; } return parsed as NoteSession; } catch { return null; } } export function saveNoteSession(session: NoteSession): void { store.writeRaw(JSON.stringify(session)); } export function clearNoteSession(): void { store.remove(); } /** fetch ของแอปพร้อม Bearer — path เป็น route API ของแอป ("/api/note/…") * ไม่มี session = throw "no-session" ให้ caller เปลี่ยนเป็นหน้า gate เอง */ export async function noteAuthFetch(path: string, init: RequestInit = {}): Promise { const session = readNoteSession(); if (session === null) throw new Error("no-session"); return fetch(withBasePath(path), { ...init, headers: { ...(init.headers ?? {}), authorization: `Bearer ${session.token}` }, }); }