// POST /api/note/auth/session — แลก credential ของลิงก์ e-Court เป็น access_token // (ฉบับ token 2026-10-30 ตามคำขอผู้ใช้ "note ใช้แบบ add-in" — แทน GET e-court ที่ // set cookie คู่ oacCase ซึ่งถอดแล้ว): body {t | ticket_data+HmacSha256, fileUrl?, // typeId?} → verify → ลงทะเบียนเอกสาร N8 แบบ trusted ฝั่ง server เลย (fileId จาก // fid ที่เซ็นมา — ไม่ต้องมี link proof อีก) → ออก login-token อายุ NOTE_SESSION_TTL // (8 ชม.) ให้ client เก็บ localStorage "note-auth" // · 400 body ผิด · 401 {error, reason: expired|invalid} · 500 secret ไม่ตั้ง // · audit log เฉพาะ userid (strip อักขระควบคุม) — ห้าม log token/query ดิบ import { NextResponse } from "next/server"; import { addinTicketSecret, verifyAddinTicket } from "@/lib/addin-ticket"; import { issueDocverToken, verifyDocverLoginToken } from "@/lib/docver-token"; import { registerExternalFile } from "@/lib/external-import"; import { parseSessionExchangeBody } from "@/lib/note-auth"; import { NOTE_SESSION_TTL } from "@/lib/note-token"; export const runtime = "nodejs"; export const dynamic = "force-dynamic"; function json(body: unknown, status = 200): NextResponse { return NextResponse.json(body, { status, headers: { "cache-control": "no-store" } }); } export async function POST(request: Request): Promise { let raw: unknown; try { raw = await request.json(); } catch { raw = null; } const parsed = parseSessionExchangeBody(raw); if (!parsed.ok) return json({ error: parsed.error }, 400); const secret = process.env.DOCVER_TOKEN_SECRET?.trim(); if (secret === undefined || secret === "") { console.error("[sso] DOCVER_TOKEN_SECRET ยังไม่ถูกตั้งค่า"); return json({ error: "ระบบยังไม่พร้อมให้เข้าสู่ระบบ" }, 500); } let identity: { userid: string; username: string; fid: string }; if (parsed.credential.kind === "jwt") { const verified = verifyDocverLoginToken(parsed.credential.token, secret); if (!verified.ok) { return json( { error: "ลิงก์เข้าสู่ระบบหมดอายุหรือไม่ถูกต้อง", reason: verified.reason }, 401, ); } identity = verified.claims; } else { // ค่า default "super_secret" ของ addinTicketSecret() เป็นที่รู้กันทั้ง repo/ // .env.example — ใครก็ปลอม ticket ของ uid ใครก็ได้ จึงปฏิเสธใน production // (fail-closed — กลุ่ม docver/add-in อนุญาต default ตาม requirement อยู่แล้ว // คนละประตู) · **dev ยอมรับค่า default** เพื่อให้ทดสอบทาง ticket รอบเครื่องได้ // โดยไม่ต้องตั้ง env ใหม่ — เครื่องมือ dev (make-login-link/make-ticket-docx/ // dev-link) ก็ default ค่าเดียวกัน (review 2026-10-30 รอบสอง) if (process.env.NODE_ENV === "production" && addinTicketSecret() === "super_secret") { console.error("[sso] ticket secret ยังเป็นค่า default (ADDIN_TICKET_SECRET/SECRET) — ปฏิเสธทาง ticket"); return json({ error: "ระบบยังไม่พร้อมให้เข้าสู่ระบบ" }, 500); } const verified = verifyAddinTicket( parsed.credential.ticketData, parsed.credential.hmacHex, addinTicketSecret(), ); if (!verified.ok) { return json({ error: "ลิงก์เข้าสู่ระบบหมดอายุหรือไม่ถูกต้อง", reason: "invalid" }, 401); } identity = { userid: verified.ticket.uid, username: verified.ticket.uname, fid: verified.ticket.fid }; } // N8: ลงทะเบียนเอกสารตอนแลก token เลย — ฝั่ง server เชื่อ fid จาก credential // ที่ verify แล้วเท่านั้น (trusted) ส่วน ?fileUrl พิมพ์เองบนหน้าเว็บไม่มีทาง // เข้ามาถึงจุดนี้ · ล้ม = ยังออก token ให้ + fileError ให้หน้าแรกแสดง (คู่ขนาน // พฤติกรรมสมัย cookie: session ตั้งก่อน แล้วค่อยแจ้ง error ของไฟล์) let fileId: string | undefined; let fileError: string | undefined; if (parsed.fileUrl !== undefined) { const result = await registerExternalFile(parsed.fileUrl, identity.fid, parsed.typeId, { trusted: true, }); if (result.ok) fileId = result.fileId; else fileError = result.error; } const access_token = issueDocverToken( { userid: identity.userid, username: identity.username, fid: identity.fid, template: null }, secret, NOTE_SESSION_TTL, "login", ); // audit: เฉพาะ userid + เวลา — ห้าม log token/query ดิบ · strip อักขระควบคุม // ก่อนพิมพ์ (claims ไม่จำกัด charset — กันฝัง \n ปลอมบรรทัด log) const auditId = identity.userid.replace(/[\u0000-\u001f\u007f-\u009f]/g, ""); console.log(`[sso] e-Court login ok: userid=${auditId}`); return json({ access_token, token_type: "Bearer", expires_in: NOTE_SESSION_TTL, userid: identity.userid, username: identity.username, ...(fileId !== undefined ? { fileId } : {}), ...(fileError !== undefined ? { fileError } : {}), }); }