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
|
|
|
|
import Link from "next/link";
|
2026-11-02 12:19:38 +07:00
|
|
|
|
import { notFound } from "next/navigation";
|
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
|
|
|
|
|
|
|
|
|
|
import AppNav from "@/components/AppNav";
|
|
|
|
|
|
import DevLoginLinkForm from "@/components/DevLoginLinkForm";
|
2026-11-02 12:19:38 +07:00
|
|
|
|
import MockEcourtForm from "@/components/MockEcourtForm";
|
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
|
|
|
|
import { devMintEnabled } from "@/lib/note-auth";
|
|
|
|
|
|
|
|
|
|
|
|
export const dynamic = "force-dynamic";
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* /note/dev — เครื่องมือทดสอบ (ย้ายมาจากหน้าแรก 2026-10-30 ตอน /note เปลี่ยนเป็น
|
|
|
|
|
|
* landing เรียบ): เปิดเอกสารตัวอย่าง (D14) + คู่มือทางเข้า SSO 2 แบบ + ตัวช่วย mint
|
2026-11-02 12:19:38 +07:00
|
|
|
|
* ลิงก์ทดสอบ · **เปิดด้วย env NOTE_SSO_DEV_MINT=1 อย่างเดียว — 0/ไม่ตั้ง = 404
|
|
|
|
|
|
* ทั้งหน้า** (คำขอผู้ใช้ 2026-11-02, ใช้ได้ทุก environment รวม production ที่ตั้ง env
|
|
|
|
|
|
* ไว้) · ตัวอย่างลิงก์เป็น "รูปแบบ + ขั้นตอน" เท่านั้น ไม่มี secret ในหน้า
|
|
|
|
|
|
* · ไม่ query DB · แบนเนอร์ ?sso= และ bootstrap ของ N8 ?fileUrl= อยู่ที่หน้าแรก /note
|
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
|
|
|
|
*/
|
|
|
|
|
|
|
|
|
|
|
|
/** รายการเอกสาร fixed ในโค้ด (D14) — จาก PDF ตัวอย่างที่มีอยู่จริงของแอป
|
|
|
|
|
|
* (public/note/samples/ — ยังไม่ seed กดเปิดแล้ว N8 จะตอบ error เองที่หน้าแรก) */
|
|
|
|
|
|
const SAMPLE_DOCS: Record<string, string[]> = {
|
|
|
|
|
|
"คำสั่งศาล — คดี 01012-672427": [
|
|
|
|
|
|
"01012-672427-1f-690609-0000840223.pdf",
|
|
|
|
|
|
"01012-672427-4f-690609-0000840227.pdf",
|
|
|
|
|
|
],
|
|
|
|
|
|
"คำสั่งศาล — ตัวอย่าง": [
|
|
|
|
|
|
"court_command_11_123 1.pdf",
|
|
|
|
|
|
"court_command_1_2547 1.pdf",
|
|
|
|
|
|
],
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
/** code inline สไตล์เดียวกันทั้งหน้า */
|
|
|
|
|
|
function Code({ children }: { children: React.ReactNode }) {
|
|
|
|
|
|
return (
|
|
|
|
|
|
<code className="rounded bg-ink/[0.05] px-1 py-0.5 text-[13px] text-ink">
|
|
|
|
|
|
{children}
|
|
|
|
|
|
</code>
|
|
|
|
|
|
);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/** บล็อกคำสั่ง/URL ตัวอย่าง — อ่านง่ายบนจอแคบ (scroll แนวนอนได้) */
|
|
|
|
|
|
function CodeBlock({ children }: { children: string }) {
|
|
|
|
|
|
return (
|
|
|
|
|
|
<pre className="mt-2 overflow-x-auto rounded-lg border border-line bg-canvas px-3 py-2.5 text-[12.5px] leading-relaxed text-ink">
|
|
|
|
|
|
{children}
|
|
|
|
|
|
</pre>
|
|
|
|
|
|
);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
export default async function DevToolsPage({
|
|
|
|
|
|
searchParams,
|
|
|
|
|
|
}: {
|
|
|
|
|
|
searchParams: Record<string, string | string[] | undefined>;
|
|
|
|
|
|
}) {
|
2026-11-02 12:19:38 +07:00
|
|
|
|
// ทางเข้าหน้านี้คุมด้วย env ตัวเดียวกับ dev-link (2026-11-02) — ปิด = 404
|
|
|
|
|
|
// เหมือนไม่มีหน้านี้ ไม่เหลือแค่ข้อความแนะนำ
|
|
|
|
|
|
if (!devMintEnabled()) notFound();
|
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
|
|
|
|
// โหมด credential ของลิงก์ตัวอย่าง — jwt (default) | ticket สลับผ่าน ?sampleMode=
|
|
|
|
|
|
const sampleMode = searchParams.sampleMode === "ticket" ? "ticket" : "jwt";
|
|
|
|
|
|
const mintOn = devMintEnabled();
|
|
|
|
|
|
return (
|
|
|
|
|
|
<>
|
|
|
|
|
|
<AppNav />
|
|
|
|
|
|
<main className="mx-auto max-w-4xl px-4 pb-16 pt-10 sm:px-6">
|
|
|
|
|
|
<h1 className="font-display text-3xl tracking-tight text-ink sm:text-headline">
|
|
|
|
|
|
เครื่องมือทดสอบ
|
|
|
|
|
|
</h1>
|
|
|
|
|
|
<p className="mt-2 text-body text-ink-soft">
|
|
|
|
|
|
หน้านี้สำหรับผู้พัฒนาและผู้ทดสอบ — ลองเปิดเอกสารตัวอย่าง
|
|
|
|
|
|
และดูวิธีเข้าระบบผ่านลิงก์ของ e-Court
|
|
|
|
|
|
</p>
|
|
|
|
|
|
|
|
|
|
|
|
{/* ── เปิดเอกสารตัวอย่าง (D14) — A Link Get(new) แบบ /tmpl ─────────── */}
|
|
|
|
|
|
<section className="mt-10">
|
|
|
|
|
|
<div className="border-b border-line pb-3">
|
|
|
|
|
|
<h2 className="font-display text-section-title text-ink">เปิดเอกสารตัวอย่าง</h2>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<p className="mt-3 text-sm text-ink-soft">
|
|
|
|
|
|
กดชื่อไฟล์แล้วระบบจะสร้างลิงก์แบบเดียวกับที่ e-Court ส่งมาให้
|
|
|
|
|
|
(แบบ{sampleMode === "jwt" ? " JWT" : " ticket"}) แล้วพาเข้าไปอ่านเอกสารนั้น
|
|
|
|
|
|
ทันที — ดูแถบที่อยู่ของ browser จะเห็นการทำงานทีละขั้นจริง ๆ
|
|
|
|
|
|
ตั้งแต่เข้าระบบจนถึงหน้าอ่านเอกสาร
|
|
|
|
|
|
</p>
|
|
|
|
|
|
{mintOn ? (
|
|
|
|
|
|
<div className="mt-4 flex flex-wrap items-center gap-2 text-sm">
|
|
|
|
|
|
<span className="text-ink-soft">รูปแบบลิงก์:</span>
|
|
|
|
|
|
{(["jwt", "ticket"] as const).map((m) => (
|
|
|
|
|
|
<Link
|
|
|
|
|
|
key={m}
|
|
|
|
|
|
href={m === "jwt" ? "/note/dev" : "/note/dev?sampleMode=ticket"}
|
|
|
|
|
|
aria-current={sampleMode === m ? "page" : undefined}
|
|
|
|
|
|
className={`focus-ring rounded-md border px-2.5 py-1 font-medium transition ${
|
|
|
|
|
|
sampleMode === m
|
|
|
|
|
|
? "border-primary bg-primary/[0.08] text-primary"
|
|
|
|
|
|
: "border-line bg-white text-ink-soft hover:border-ink-faint"
|
|
|
|
|
|
}`}
|
|
|
|
|
|
>
|
|
|
|
|
|
{m === "jwt" ? "JWT (ทาง A)" : "ticket (ทาง B)"}
|
|
|
|
|
|
</Link>
|
|
|
|
|
|
))}
|
|
|
|
|
|
</div>
|
|
|
|
|
|
) : (
|
|
|
|
|
|
<p className="mt-3 rounded-lg border border-line bg-canvas p-3 text-xs leading-relaxed text-ink-soft">
|
2026-11-02 12:19:38 +07:00
|
|
|
|
ลิงก์ด้านล่างเปิดเอกสารด้วย session ที่มีอยู่เท่านั้น — ยังไม่มี session
|
|
|
|
|
|
ใช้ฟอร์ม "จำลอง e-Court" ด้านล่างแทน · dev server ที่ต้องการ mint
|
|
|
|
|
|
ให้อัตโนมัติ (ไม่ต้องใส่ key) เพิ่ม <Code>NOTE_SSO_DEV_MINT=1</Code>{" "}
|
|
|
|
|
|
แล้วรีสตาร์ท
|
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
|
|
|
|
</p>
|
|
|
|
|
|
)}
|
|
|
|
|
|
<div className="mt-5 max-w-xl rounded-xl border border-line bg-white p-6">
|
|
|
|
|
|
<h3 className="font-display text-subhead text-primary">A Link Get(new)</h3>
|
|
|
|
|
|
{Object.entries(SAMPLE_DOCS).map(([group, files]) => (
|
|
|
|
|
|
<div key={group} className="mt-3">
|
|
|
|
|
|
<p className="text-xs font-medium tracking-wide text-ink-faint">{group}</p>
|
|
|
|
|
|
<ul className="mt-1 space-y-1">
|
|
|
|
|
|
{files.map((file) => (
|
|
|
|
|
|
<li key={file}>
|
|
|
|
|
|
<Link
|
|
|
|
|
|
href={
|
|
|
|
|
|
mintOn
|
|
|
|
|
|
? `/api/note/auth/dev-link?file=${encodeURIComponent(file)}&mode=${sampleMode}`
|
|
|
|
|
|
: `/api/note/files/open?fileUrl=${encodeURIComponent(`/samples/${file}`)}`
|
|
|
|
|
|
}
|
|
|
|
|
|
className="focus-ring rounded text-primary transition hover:underline"
|
|
|
|
|
|
>
|
|
|
|
|
|
{file}
|
|
|
|
|
|
</Link>
|
|
|
|
|
|
</li>
|
|
|
|
|
|
))}
|
|
|
|
|
|
</ul>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
))}
|
|
|
|
|
|
</div>
|
2026-11-02 12:19:38 +07:00
|
|
|
|
|
|
|
|
|
|
{/* จำลอง e-Court ด้วย API key — แสดงทุก environment (คำขอผู้ใช้ 2026-11-02:
|
|
|
|
|
|
ใช้ทางเข้าจริงเหมือน e-Court ต้องถือ DOCVER_API_KEY จึงไม่เป็นประตูหลัง) */}
|
|
|
|
|
|
<div className="mt-5 max-w-xl rounded-xl border border-line bg-white p-6">
|
|
|
|
|
|
<h3 className="font-display text-subhead text-primary">
|
|
|
|
|
|
จำลอง e-Court — ขอ token ด้วย API key
|
|
|
|
|
|
</h3>
|
|
|
|
|
|
<p className="mt-1.5 text-sm text-ink-soft">
|
|
|
|
|
|
ใส่ API key (<Code>DOCVER_API_KEY</Code>) เพื่อขอ token ผ่าน{" "}
|
|
|
|
|
|
<Code>POST /api/note/auth/token</Code> แบบเดียวกับที่ e-Court ทำจริง
|
|
|
|
|
|
แล้วระบบจะพาเข้าอ่านเอกสารที่เลือกเลย — ได้ session 8 ชม.
|
|
|
|
|
|
(ใช้ได้ทั้ง dev และ production)
|
|
|
|
|
|
</p>
|
|
|
|
|
|
<div className="mt-4">
|
|
|
|
|
|
<MockEcourtForm files={Object.values(SAMPLE_DOCS).flat()} />
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
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
|
|
|
|
</section>
|
|
|
|
|
|
|
|
|
|
|
|
{/* ── Flow ─────────────────────────────────────────────────────────── */}
|
|
|
|
|
|
<section className="mt-10">
|
|
|
|
|
|
<div className="border-b border-line pb-3">
|
|
|
|
|
|
<h2 className="font-display text-section-title text-ink">Flow</h2>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<div className="mt-5 max-w-xl rounded-xl border border-line bg-white p-6 text-center text-sm font-medium leading-relaxed text-ink">
|
|
|
|
|
|
e-Court สร้างลิงก์เข้าใช้ (มีตัวยืนยันตัวตน + ที่อยู่ไฟล์)
|
|
|
|
|
|
<br />↓<br />
|
|
|
|
|
|
ผู้ใช้กดลิงก์ → ระบบตรวจแล้วออก session ให้ (ใช้ได้ 8 ชม.)
|
|
|
|
|
|
<br />↓<br />
|
|
|
|
|
|
เข้าหน้าอ่านเอกสาร — มาร์กข้อความ วาดเส้น คอมเมนต์ คั่นหน้า
|
|
|
|
|
|
<br />↓<br />
|
|
|
|
|
|
บันทึกทันทีทุกครั้งที่แก้ คนอื่นที่เปิดไฟล์เดียวกันเห็นสด ๆ
|
|
|
|
|
|
<br />↓<br />
|
|
|
|
|
|
ดูรวมทั้งหมดได้ที่หน้า "สรุปหมายเหตุ" และ "เอกสารในกลุ่ม"
|
|
|
|
|
|
<br />↓<br />
|
|
|
|
|
|
ระบบอื่น (เช่น e-Court) ดึงข้อมูลไปใช้ต่อได้ผ่าน API
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</section>
|
|
|
|
|
|
|
|
|
|
|
|
{/* ── ทางเข้าระบบผ่านลิงก์ e-Court (คู่มือ 2 ทาง ตามแผน SSO) ────────── */}
|
|
|
|
|
|
<section className="mt-12">
|
|
|
|
|
|
<div className="border-b border-line pb-3">
|
|
|
|
|
|
<h2 className="font-display text-section-title text-ink">
|
|
|
|
|
|
ทางเข้าระบบผ่านลิงก์ e-Court (SSO)
|
|
|
|
|
|
</h2>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<p className="mt-3 text-sm text-ink-soft">
|
|
|
|
|
|
แอปนี้ไม่มีหน้าล็อกอิน — ผู้ใช้เข้าได้ทางลิงก์ที่ e-Court สร้างให้อย่างเดียว
|
|
|
|
|
|
ลิงก์มี 2 แบบ (ใช้แบบใดแบบหนึ่ง — ห้ามใส่ครบทั้งสองแบบในลิงก์เดียว)
|
|
|
|
|
|
</p>
|
|
|
|
|
|
|
|
|
|
|
|
<div className="mt-5 grid gap-5 lg:grid-cols-2">
|
|
|
|
|
|
{/* ทาง A — JWT */}
|
|
|
|
|
|
<article className="flex flex-col rounded-xl border border-line bg-white p-5">
|
|
|
|
|
|
<div className="flex items-center gap-2">
|
|
|
|
|
|
<h3 className="font-display text-subhead text-ink">ทาง A — ขอ token (JWT)</h3>
|
|
|
|
|
|
<span className="rounded bg-primary/[0.08] px-1.5 py-0.5 text-[11px] font-medium text-primary">
|
|
|
|
|
|
แนะนำ
|
|
|
|
|
|
</span>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<p className="mt-1.5 text-sm text-ink-soft">
|
|
|
|
|
|
เหมาะเมื่อสร้างลิงก์ให้ผู้ใช้ได้ตอนนั้นเลย (ตอนเปิดหน้า) — คีย์ที่ใช้เซ็น
|
|
|
|
|
|
อยู่ฝั่ง server เท่านั้น ปลอมตัวเป็นผู้อื่นไม่ได้
|
|
|
|
|
|
</p>
|
|
|
|
|
|
<ol className="mt-4 list-decimal space-y-1.5 pl-5 text-sm text-ink">
|
|
|
|
|
|
<li>
|
|
|
|
|
|
ฝั่ง e-Court เรียก <Code>POST /api/note/auth/token</Code> พร้อมรหัส{" "}
|
|
|
|
|
|
<Code>x-api-key</Code> (ค่าจาก env <Code>DOCVER_API_KEY</Code>) และข้อมูล:
|
|
|
|
|
|
</li>
|
|
|
|
|
|
</ol>
|
|
|
|
|
|
<CodeBlock>{`POST {BASE}/api/note/auth/token
|
|
|
|
|
|
x-api-key: <DOCVER_API_KEY>
|
|
|
|
|
|
|
|
|
|
|
|
{"userid":"<u_userid>","username":"<u_userlogin>",
|
|
|
|
|
|
"fid":"<fileId>","ttl_seconds":300}`}</CodeBlock>
|
|
|
|
|
|
<ol start={2} className="mt-3 list-decimal space-y-1.5 pl-5 text-sm text-ink">
|
|
|
|
|
|
<li>
|
|
|
|
|
|
ได้ <Code>access_token</Code> กลับมา (อายุ 1–10 นาที) — นำไปใส่ในลิงก์
|
|
|
|
|
|
เป็นพารามิเตอร์ <Code>t</Code> ให้ผู้ใช้กด:
|
|
|
|
|
|
</li>
|
|
|
|
|
|
</ol>
|
|
|
|
|
|
<CodeBlock>{`{BASE}/note
|
|
|
|
|
|
?fileUrl=<urlencode>&typeId=<กลุ่ม ทางเลือก>&t=<access_token>`}</CodeBlock>
|
|
|
|
|
|
<ol start={3} className="mt-3 list-decimal space-y-1.5 pl-5 text-sm text-ink">
|
|
|
|
|
|
<li>
|
|
|
|
|
|
ผู้ใช้กดลิงก์ → ระบบตรวจแล้วออก session ให้ (เก็บในเครื่องของผู้ใช้
|
|
|
|
|
|
อายุ 8 ชม.) → พาเข้าหน้าอ่านเอกสาร และตัดค่า token ออกจากลิงก์ทันที
|
|
|
|
|
|
</li>
|
|
|
|
|
|
</ol>
|
|
|
|
|
|
<p className="mt-4 rounded-lg border border-line bg-canvas p-3 text-xs leading-relaxed text-ink-soft">
|
|
|
|
|
|
ข้อควรระวัง: สร้าง token ตอนแสดงหน้าให้ผู้ใช้คนนั้น ๆ เท่านั้น และ{" "}
|
|
|
|
|
|
<strong className="font-medium text-ink-soft">
|
|
|
|
|
|
ห้ามเก็บ token ไว้ในที่อื่น
|
|
|
|
|
|
</strong>{" "}
|
|
|
|
|
|
(อายุสั้นมาก) · token นี้ใช้เข้าแอปนี้อย่างเดียว เอาไปเรียก API
|
|
|
|
|
|
กลุ่ม documents ไม่ได้
|
|
|
|
|
|
</p>
|
|
|
|
|
|
</article>
|
|
|
|
|
|
|
|
|
|
|
|
{/* ทาง B — ticket */}
|
|
|
|
|
|
<article className="flex flex-col rounded-xl border border-line bg-white p-5">
|
|
|
|
|
|
<div className="flex items-center gap-2">
|
|
|
|
|
|
<h3 className="font-display text-subhead text-ink">ทาง B — ticket</h3>
|
|
|
|
|
|
<span className="rounded bg-ink/[0.05] px-1.5 py-0.5 text-[11px] font-medium text-ink-soft">
|
|
|
|
|
|
สร้างล่วงหน้า
|
|
|
|
|
|
</span>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<p className="mt-1.5 text-sm text-ink-soft">
|
|
|
|
|
|
เหมาะเมื่อสร้างลิงก์เตรียมไว้ล่วงหน้าได้ ไม่ต้องเรียก API ตอนแสดงหน้า —
|
|
|
|
|
|
ใช้คู่ <Code>ticket_data</Code> + <Code>HmacSha256</Code> ชุดเดียวกับที่ฝัง
|
|
|
|
|
|
ไว้ในไฟล์ .docx ของ add-in
|
|
|
|
|
|
</p>
|
|
|
|
|
|
<ol className="mt-4 list-decimal space-y-1.5 pl-5 text-sm text-ink">
|
|
|
|
|
|
<li>
|
|
|
|
|
|
e-Court เตรียม <Code>ticket_data</Code> (JSON มี{" "}
|
|
|
|
|
|
<Code>exp/fid/uid/uname</Code>) แล้วเซ็น <Code>HmacSha256</Code> ด้วยคีย์{" "}
|
|
|
|
|
|
<Code>ADDIN_TICKET_SECRET</Code>
|
|
|
|
|
|
</li>
|
|
|
|
|
|
<li>นำมาใส่ในลิงก์ให้ผู้ใช้กด:</li>
|
|
|
|
|
|
</ol>
|
|
|
|
|
|
<CodeBlock>{`{BASE}/note
|
|
|
|
|
|
?fileUrl=<urlencode>&typeId=<ทางเลือก>
|
|
|
|
|
|
&ticket_data=<JSON urlencode>&HmacSha256=<hex 64 ตัว>`}</CodeBlock>
|
|
|
|
|
|
<ol start={3} className="mt-3 list-decimal space-y-1.5 pl-5 text-sm text-ink">
|
|
|
|
|
|
<li>
|
|
|
|
|
|
ผู้ใช้กดลิงก์ → ระบบตรวจลายเซ็นและวันหมดอายุ → ออก session 8 ชม. →
|
|
|
|
|
|
พาเข้าหน้าอ่านเอกสาร
|
|
|
|
|
|
</li>
|
|
|
|
|
|
</ol>
|
|
|
|
|
|
<p className="mt-4 rounded-lg border border-line bg-canvas p-3 text-xs leading-relaxed text-ink-soft">
|
|
|
|
|
|
ข้อควรระวัง: ตั้ง <Code>exp</Code> สั้น ๆ (ไม่เกิน ~10 นาที เพราะค่าเหล่านี้
|
|
|
|
|
|
อยู่ในลิงก์) · ค่าผู้ใช้ (<Code>userid/username</Code> ทาง A,{" "}
|
|
|
|
|
|
<Code>uid/uname</Code> ทาง B) ต้องเป็นคนเดียวกับ{" "}
|
|
|
|
|
|
<Code>u_userid/u_userlogin</Code> ของ e-Court — ใส่ผิดคน = ข้อมูล
|
|
|
|
|
|
ผูกกับคนผิด
|
|
|
|
|
|
</p>
|
|
|
|
|
|
</article>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
|
|
{/* dev tool — ฟอร์ม mint ลิงก์ (เปิดด้วย env NOTE_SSO_DEV_MINT=1 เท่านั้น:
|
|
|
|
|
|
ตัวช่วยทดสอบ ไม่ใช่ทางเข้าจริง และตายเสมอใน production build) */}
|
|
|
|
|
|
{devMintEnabled() && (
|
|
|
|
|
|
<div className="mt-5 rounded-xl border border-dashed border-line bg-white/60 p-5">
|
|
|
|
|
|
<h4 className="font-display text-sm font-semibold text-ink">
|
|
|
|
|
|
สร้างลิงก์ทดสอบเอง (dev)
|
|
|
|
|
|
</h4>
|
|
|
|
|
|
<p className="mt-1.5 text-sm text-ink-soft">
|
|
|
|
|
|
กรอกชื่อผู้ใช้แล้วกดสร้าง — ระบบจะสร้างลิงก์ให้จากคีย์ของตัวเอง
|
|
|
|
|
|
(ค่าลับไม่โผล่ที่หน้าเว็บ) ได้ลิงก์มาแล้วกด "เปิดลิงก์"
|
|
|
|
|
|
เพื่อทดสอบเข้าจริงทั้งสองแบบ
|
|
|
|
|
|
</p>
|
|
|
|
|
|
<div className="mt-4">
|
|
|
|
|
|
<DevLoginLinkForm />
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
)}
|
|
|
|
|
|
|
|
|
|
|
|
{/* dev tool — สคริปต์มือ (ใช้ได้เสมอ ไม่ขึ้นกับ env) */}
|
|
|
|
|
|
<div className="mt-5 rounded-xl border border-dashed border-line bg-white/60 p-5">
|
|
|
|
|
|
<h4 className="font-display text-sm font-semibold text-ink">
|
|
|
|
|
|
ทดสอบบนเครื่อง (dev)
|
|
|
|
|
|
</h4>
|
|
|
|
|
|
<p className="mt-1.5 text-sm text-ink-soft">
|
|
|
|
|
|
สร้างลิงก์เองบนเครื่องโดยไม่ต้องรอ e-Court — สคริปต์อ่านค่าจากไฟล์{" "}
|
|
|
|
|
|
<Code>.env</Code> แล้วพิมพ์ลิงก์เต็มให้ก๊อปไปเปิดใน browser:
|
|
|
|
|
|
</p>
|
|
|
|
|
|
<CodeBlock>{`# ทาง A (default)
|
|
|
|
|
|
node scripts/make-login-link.mjs --uid oom --uname "สรวิชญ์" --fid F123 \\
|
|
|
|
|
|
--fileUrl /samples/01012-672427-1f-690609-0000840223.pdf \\
|
|
|
|
|
|
--base http://localhost:3000/docedit
|
|
|
|
|
|
|
|
|
|
|
|
# ทาง B
|
|
|
|
|
|
node scripts/make-login-link.mjs --mode ticket --uid oom --uname "สรวิชญ์" --fid F123 \\
|
|
|
|
|
|
--fileUrl /samples/01012-672427-1f-690609-0000840223.pdf \\
|
|
|
|
|
|
--base http://localhost:3000/docedit`}</CodeBlock>
|
|
|
|
|
|
<p className="mt-3 text-xs text-ink-soft">
|
|
|
|
|
|
ถ้าลิงก์หมดอายุหรือไม่ถูกต้อง ระบบจะพากลับหน้าแรกพร้อมข้อความบอกสาเหตุ —
|
|
|
|
|
|
สร้างลิงก์ใหม่แล้วกดอีกครั้งได้เลย
|
|
|
|
|
|
</p>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</section>
|
|
|
|
|
|
</main>
|
|
|
|
|
|
</>
|
|
|
|
|
|
);
|
|
|
|
|
|
}
|