eCourt-note/app/api/note/files/[id]/bootstrap/route.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

81 lines
2.9 KiB
TypeScript

// GET /api/note/files/[id]/bootstrap — ข้อมูลเริ่มต้นของ viewer (file + annotations
// + bookmarks + replies เป็น DTO) ให้ client gate ของหน้า /note/annotate/[id]
// ดึงหลังมี access_token (ฉบับ token 2026-10-30 — เดิมหน้านี้เป็น server component
// อ่าน cookie ผ่าน getSessionUser) · Bearer บังคับ (401) · ไม่ publish SSE ·
// query ชุดเดียวกับหน้าเดิมตอนเป็น server component
import { asc, eq, inArray } from "drizzle-orm";
import { NextResponse } from "next/server";
import { db } from "@/db";
import {
annotationReplies,
annotations,
bookmarks,
files,
toAnnotationDTO,
toBookmarkDTO,
toReplyDTO,
} from "@/db/schema";
import { noteUserFromRequest } from "@/lib/note-token";
export const runtime = "nodejs";
export const dynamic = "force-dynamic";
const UUID_RE =
/^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
export async function GET(
request: Request,
{ params }: { params: { id: string } },
): Promise<Response> {
const user = noteUserFromRequest(request);
if (user === null) {
return NextResponse.json(
{ error: "session หมดอายุ — กรุณากลับ e-Court กดลิงก์เปิดเอกสารใหม่" },
{ status: 401, headers: { "cache-control": "no-store" } },
);
}
const { id } = params;
if (!UUID_RE.test(id)) {
return NextResponse.json({ error: "ไม่พบไฟล์นี้" }, { status: 404 });
}
const [file] = await db.select().from(files).where(eq(files.id, id));
if (!file) {
return NextResponse.json({ error: "ไม่พบไฟล์นี้" }, { status: 404 });
}
const [annotationRows, bookmarkRows] = await Promise.all([
db
.select()
.from(annotations)
.where(eq(annotations.fileId, id))
.orderBy(asc(annotations.createdAt)),
db
.select()
.from(bookmarks)
.where(eq(bookmarks.fileId, id))
.orderBy(asc(bookmarks.createdAt)),
]);
// replies ของทุก comment ในไฟล์นี้ (F9) — แม่ถูก CASCADE ไปแล้ว row ก็หายตาม
const annotationIds = annotationRows.map((r) => r.id);
const replyRows = annotationIds.length
? await db
.select()
.from(annotationReplies)
.where(inArray(annotationReplies.annotationId, annotationIds))
.orderBy(asc(annotationReplies.createdAt))
: [];
return NextResponse.json(
{
file: { id: file.id, title: file.title, pdfUrl: file.pdfUrl },
annotations: annotationRows.map(toAnnotationDTO),
bookmarks: bookmarkRows.map(toBookmarkDTO),
replies: replyRows.map(toReplyDTO),
user: { userId: user.userid, userName: user.username },
},
{ headers: { "cache-control": "no-store" } },
);
}