ecourt+groups: N8 เปิดเอกสารจากลิงก์ ?fileUrl=&fileId=&typeId= (metadata-only + allowlist + ดึงไฟล์สด + login วนกลับ + การ์ดจำลองลิงก์ + path dev /samples) + N9 กลุ่มเอกสารตาม typeId — หน้ารายการจัด section ต่อกลุ่ม + "ไม่มีกลุ่ม" ท้ายสุด + หน้า /groups/[typeId] พร้อมหมายเหตุ "อาจไม่ครบทุกฉบับ" + docs sync ครบ (2026-09-23)
Co-Authored-By: Claude Code <noreply@anthropic.com>
This commit is contained in:
parent
0aef3ff028
commit
329ae8e956
31 changed files with 2766 additions and 155 deletions
|
|
@ -3,5 +3,7 @@
|
|||
// plain <a href>/workerSrc/img src — ต่างจาก <Link>/router/redirect ของ next
|
||||
// ที่เติมให้อัตโนมัติ (pdfUrl ใน DB เก็บ path ไม่รวม basePath เติมตอนใช้เท่านั้น)
|
||||
export function withBasePath(path: string): string {
|
||||
// absolute http(s) ผ่านไปเฉย ๆ (N8: pdf_url เก็บ URL รีโมตได้ — ห้าม prefix ทับ)
|
||||
if (/^https?:\/\//i.test(path)) return path;
|
||||
return `${process.env.NEXT_PUBLIC_BASE_PATH ?? ""}${path}`;
|
||||
}
|
||||
|
|
|
|||
92
utils/external-file.ts
Normal file
92
utils/external-file.ts
Normal file
|
|
@ -0,0 +1,92 @@
|
|||
// N8 (PLAN-ECOURT.md D2) — validation และตัวช่วยของลิงก์ไฟล์ภายนอก (pure module ไม่แตะ fs/db
|
||||
// เพื่อให้ middleware ใช้ร่วมได้ในอนาคต) + ตัวช่วยแยกไฟล์ในเครื่องกับไฟล์รีโมต
|
||||
|
||||
export type ExternalFileUrlError =
|
||||
| "invalid_url"
|
||||
| "not_https"
|
||||
| "has_credentials"
|
||||
| "host_not_allowed";
|
||||
|
||||
/** Next 14: searchParams เป็น plain object ค่า string | string[] — เอาตัวแรกของแต่ละ key
|
||||
* (ใช้ร่วมระหว่าง entry point หน้า "/", "/annotate" และหน้า login — วางที่ pure module
|
||||
* เพื่อไม่ลาก db/fs ของ lib/external-import ตามเข้าหน้าที่ไม่แตะ DB) */
|
||||
export function firstSearchParam(v: string | string[] | undefined): string | undefined {
|
||||
return Array.isArray(v) ? v[0] : v;
|
||||
}
|
||||
|
||||
/** allowlist โฮสต์รีโมต — default "admincourt.go.th" ครอบทั้ง admcasedey. และ judgement-api.
|
||||
* (suffix match), override ด้วย env EXTERNAL_FILE_HOSTS (comma-separated) —
|
||||
* อ่านตอน call ไม่ใช่ตอน module load เพื่อ test ได้ */
|
||||
export function externalHosts(): string[] {
|
||||
const raw = process.env.EXTERNAL_FILE_HOSTS;
|
||||
if (!raw) return ["admincourt.go.th"];
|
||||
const list = raw
|
||||
.split(",")
|
||||
.map((h) => h.trim().toLowerCase())
|
||||
.filter(Boolean);
|
||||
return list.length > 0 ? list : ["admincourt.go.th"];
|
||||
}
|
||||
|
||||
/** suffix match กันโฮสต์ปลอม: host === entry หรือ host.endsWith("." + entry)
|
||||
* (ไม่ match "admincourt.go.th.evil.com" และ "xadmincourt.go.th") */
|
||||
export function isAllowedExternalHost(host: string): boolean {
|
||||
const h = host.toLowerCase();
|
||||
return externalHosts().some((entry) => h === entry || h.endsWith("." + entry));
|
||||
}
|
||||
|
||||
export function validateExternalFileUrl(
|
||||
raw: string,
|
||||
): { ok: true; url: URL } | { ok: false; error: ExternalFileUrlError } {
|
||||
let url: URL;
|
||||
try {
|
||||
url = new URL(raw.trim());
|
||||
} catch {
|
||||
return { ok: false, error: "invalid_url" };
|
||||
}
|
||||
if (url.protocol !== "https:") return { ok: false, error: "not_https" };
|
||||
if (url.username || url.password) {
|
||||
return { ok: false, error: "has_credentials" };
|
||||
}
|
||||
if (!isAllowedExternalHost(url.hostname)) {
|
||||
return { ok: false, error: "host_not_allowed" };
|
||||
}
|
||||
return { ok: true, url };
|
||||
}
|
||||
|
||||
/** absolute http(s):// หรือไม่ — ใช้ทั้งฝั่ง server แยกว่า pdf_url เป็นรีโมต
|
||||
* และฝั่ง client เลือกทางโหลดรูปภาพ */
|
||||
export function isHttpUrl(s: string): boolean {
|
||||
return /^https?:\/\//i.test(s.trim());
|
||||
}
|
||||
|
||||
/** title จาก basename ของ URL (decode แล้ว ตัดนามสกุล cap 200) —
|
||||
* path ลงท้าย "/" หรือ decode ไม่ได้/ไม่มีชื่อ ใช้ fallbackId หรือชื่อกลาง */
|
||||
export function titleFromExternalUrl(url: URL, fallbackId?: string): string {
|
||||
const base = url.pathname.split("/").pop() ?? "";
|
||||
let name = base;
|
||||
try {
|
||||
name = decodeURIComponent(base);
|
||||
} catch {
|
||||
// ชื่อ encode เสีย — ใช้ base ดิบ
|
||||
}
|
||||
const dot = name.lastIndexOf(".");
|
||||
if (dot > 0) name = name.slice(0, dot);
|
||||
name = name.trim().slice(0, 200);
|
||||
return name || fallbackId?.trim() || "เอกสารจาก e-Court";
|
||||
}
|
||||
|
||||
/** dedupe key ของเอกสารนำเข้า — "ecourt:<fileId>" เมื่อมี id ของ e-Court,
|
||||
* ไม่มี fallback เป็น "url:<href>" (id เปลี่ยน = ไฟล์ใหม่ ตามหลักของแผน) */
|
||||
export function externalIdFor(fileId: string | undefined, url: URL): string {
|
||||
const id = fileId?.trim();
|
||||
return id ? `ecourt:${id}` : `url:${url.href}`;
|
||||
}
|
||||
|
||||
/** path วนกลับหลัง login (N8 D5) — ต้องขึ้นต้น "/" และไม่ใช่ "//" หรือ "/\"
|
||||
* (กัน open-redirect: browser ตีความ "//evil.com" และ "/\evil.com" เป็น absolute
|
||||
* URL ไป host อื่น) — ค่า next มาจาก query ที่ใครก็แก้ได้ ต้อง sanitize ก่อนใช้เสมอ;
|
||||
* ไม่ผ่านคืน null ให้ caller ใช้ "/" */
|
||||
export function sanitizeNextPath(s: string): string | null {
|
||||
if (!s.startsWith("/") || s.startsWith("//") || s.startsWith("/\\")) return null;
|
||||
return s;
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue