Merge branch 'forgejo-dev' into newfeat

Co-Authored-By: Claude Code <noreply@anthropic.com>
This commit is contained in:
Your Name 2026-11-09 10:15:38 +07:00
commit c57a75e36a
50 changed files with 2760 additions and 961 deletions

View file

@ -0,0 +1,106 @@
// POST /api/auth/session (path สั้นของ POST /api/note/auth/session — โค้ดก้อนเดียวกัน
// copy มา 2026-11-05) — แลก 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<Response> {
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 } : {}),
});
}

113
app/api/auth/token/route.ts Normal file
View file

@ -0,0 +1,113 @@
// PLAN-DOCVER.md §auth — POST /api/auth/token (path สั้นของ POST /api/documents/token
// — โค้ดก้อนเดียวกัน copy มา 2026-11-05) ออก access_token ของกลุ่ม docver
// ทางเข้า 2 ทาง (PLAN-ADDIN-AUTH.md §3 — header x-api-key เป็นตัวตั้ง):
// 1) e-Court server-to-server: header "x-api-key" เทียบ env DOCVER_API_KEY
// (fail closed เมื่อ env ไม่ตั้ง/ว่าง) + body claims ตรง ๆ (เดิมทุกอย่าง)
// 2) add-in ใน Word: ไม่มี x-api-key + body {ticket_data, HmacSha256} — server
// ตรวจ HMAC ด้วย env ฝั่ง server (ADDIN_TICKET_SECRET → SECRET →
// "super_secret", lib/addin-ticket.ts) แล้วออก token จาก claims ของ ticket
// (secret ตัวเซ็น JWT ไม่มีทางไปอยู่ใน bundle ของ add-in — client ไม่ตรวจ HMAC
// เองแล้วตาม PLAN-SSO.md D13) · contract 2026-11-04: 400 = ฟอร์แม็ตพัง
// (รวม missing) / 401 = ลายเซ็นไม่ถูก / 403 = ลายเซ็นถูกแต่ ticket หมดอายุ
// (client ที่มี JWT เดิมของ fid นี้ใน session ใช้ต่อเอง — ไม่มีการออกซ้ำ
// จากหลักฐาน DB อีกต่อไป เลิก requirement ขั้น 7 ฉบับ 2026-10-28)
// ทั้งสองทางออก JWT HS256 เซ็นด้วย env DOCVER_TOKEN_SECRET อายุตาม env
// DOCVER_TOKEN_TTL_SECONDS (default 604800 = 7 วัน — token อายุยาวแทนระบบ
// refresh/renew) แล้วนำไปแนบ Authorization: Bearer กับ endpoint อื่นของกลุ่ม
// (lib/docver-token.ts) · segment คงที่ "token" ไม่ชน [docId]
// (Next ให้ static ชนะ) และ middleware ยกเว้น api/documents/* อยู่แล้ว
import { docverError } from "@/lib/document-versioning";
import {
addinTicketSecret,
parseTicketTokenBody,
ticketToDocverClaims,
verifyAddinTicketHmac,
} from "@/lib/addin-ticket";
import { isTicketExpired } from "@/utils/docver-ticket";
import {
issueDocverToken,
parseTokenRequestBody,
requireDocverApiKey,
type DocverTokenClaims,
} from "@/lib/docver-token";
export const runtime = "nodejs";
export const dynamic = "force-dynamic";
/** 401 ของทางเข้า ticket — ข้อความเดียวทุกสาเหตุ (HMAC ผิด/รูปผิด) กัน oracle · หมดอายุแยกเป็น 403 */
const TICKET_DENIED = "ticket ไม่ถูกต้อง";
/** 403 ของทางเข้า ticket — ลายเซ็นถูกแต่หมดอายุ (client มี JWT เดิมใน session ก็ใช้ต่อได้) */
const TICKET_EXPIRED = "ticket ของ e-Court หมดอายุ";
export async function POST(request: Request) {
return request.headers.get("x-api-key") !== null
? postWithApiKey(request)
: postWithTicket(request);
}
/** ทางเข้า 1 (เดิม) — e-Court server-to-server */
async function postWithApiKey(request: Request): Promise<Response> {
const apiKeyFail = requireDocverApiKey(request);
if (apiKeyFail !== null) return apiKeyFail;
let raw: unknown;
try {
raw = await request.json();
} catch {
return docverError("body ต้องเป็น JSON", 400);
}
const parsed = parseTokenRequestBody(raw);
if (!parsed.ok) return docverError(parsed.error, 400);
return issueToken(parsed.claims);
}
/** ทางเข้า 2 — add-in ส่ง ticket ที่ e-Court stamp มากับไฟล์ */
async function postWithTicket(request: Request): Promise<Response> {
let raw: unknown;
try {
raw = await request.json();
} catch {
return docverError("body ต้องเป็น JSON", 400);
}
const parsed = parseTicketTokenBody(raw);
if (!parsed.ok) {
// contract 2026-11-04: ฟอร์แม็ตพัง = 400 ทั้ง missing/invalid (เดิม missing เป็น 401)
return docverError(parsed.error, 400);
}
// ตรวจ HMAC ก่อนเสมอ (ตัวตนของผู้ถือไฟล์) — exp ไม่ใช่เกณฑ์ตัดสิทธิ์ ณ จุดนี้
const verified = verifyAddinTicketHmac(parsed.ticketData, parsed.hmacHex, addinTicketSecret());
if (!verified.ok) return docverError(TICKET_DENIED, 401);
// ลายเซ็นถูกแต่ ticket หมดอายุ → 403 เสมอ (contract 2026-11-04 — เลิก requirement
// ขั้น 7 ฉบับ 2026-10-28: ไม่ตรวจ hasVersionByUser อีกต่อไป เพราะ ticket ฝังอยู่ใน
// ไฟล์ ใครได้ไฟล์ก็ได้ ticket การปล่อยให้ ticket เก่าแลก token ใหม่ได้ตลอดไป
// เท่ากันคือปล่อยสิทธิ์ตามการครอบครองไฟล์ — ฝั่ง client ที่มี JWT ของ fid นี้
// ใน session ที่ยังไม่หมด (7 วัน) ใช้ต่อเอง ไม่มี = ขอไฟล์ใหม่จาก e-Court)
if (isTicketExpired(verified.ticket.exp)) {
return docverError(TICKET_EXPIRED, 403);
}
return issueToken(ticketToDocverClaims(verified.ticket));
}
/** ออก JWT ของ claims ที่ผ่านการยืนยันแล้วจากทั้งสองทาง — ส่วนกลาง */
async function issueToken(claims: DocverTokenClaims): Promise<Response> {
// ความผิด config ฝั่ง server ไม่ใช่ความผิดของผู้เรียก — ไม่ตั้ง = 500 (ห้าม log ค่า secret)
const secret = process.env.DOCVER_TOKEN_SECRET?.trim();
if (secret === undefined || secret === "") {
return docverError("DOCVER_TOKEN_SECRET ยังไม่ถูกตั้งค่า", 500);
}
const ttlRaw = Number(process.env.DOCVER_TOKEN_TTL_SECONDS);
// default 7 วัน (2026-11-04) — token อายุยาวแทนระบบ refresh/renew ของ add-in
const ttlSeconds =
Number.isFinite(ttlRaw) && ttlRaw > 0 ? Math.floor(ttlRaw) : 604_800;
const accessToken = issueDocverToken(claims, secret, ttlSeconds);
return new Response(
JSON.stringify({
access_token: accessToken,
token_type: "Bearer",
expires_in: ttlSeconds,
}),
{ headers: { "content-type": "application/json; charset=utf-8" } },
);
}

View file

@ -5,11 +5,15 @@
// 2) add-in ใน Word: ไม่มี x-api-key + body {ticket_data, HmacSha256} — server
// ตรวจ HMAC ด้วย env ฝั่ง server (ADDIN_TICKET_SECRET → SECRET →
// "super_secret", lib/addin-ticket.ts) แล้วออก token จาก claims ของ ticket
// (secret ตัวเซ็น JWT ไม่มีทางไปอยู่ใน bundle ของ add-in) — ticket หมดอายุ
// ยังออกให้ต่อเมื่อ fid นี้เคยมี version โดย uid เดียวกัน (requirement ขั้น 7)
// (secret ตัวเซ็น JWT ไม่มีทางไปอยู่ใน bundle ของ add-in — client ไม่ตรวจ HMAC
// เองแล้วตาม PLAN-SSO.md D13) · contract 2026-11-04: 400 = ฟอร์แม็ตพัง
// (รวม missing) / 401 = ลายเซ็นไม่ถูก / 403 = ลายเซ็นถูกแต่ ticket หมดอายุ
// (client ที่มี JWT เดิมของ fid นี้ใน session ใช้ต่อเอง — ไม่มีการออกซ้ำ
// จากหลักฐาน DB อีกต่อไป เลิก requirement ขั้น 7 ฉบับ 2026-10-28)
// ทั้งสองทางออก JWT HS256 เซ็นด้วย env DOCVER_TOKEN_SECRET อายุตาม env
// DOCVER_TOKEN_TTL_SECONDS (default 3600) แล้วนำไปแนบ Authorization: Bearer กับ
// endpoint อื่นของกลุ่ม (lib/docver-token.ts) · segment คงที่ "token" ไม่ชน [docId]
// DOCVER_TOKEN_TTL_SECONDS (default 604800 = 7 วัน — token อายุยาวแทนระบบ
// refresh/renew) แล้วนำไปแนบ Authorization: Bearer กับ endpoint อื่นของกลุ่ม
// (lib/docver-token.ts) · segment คงที่ "token" ไม่ชน [docId]
// (Next ให้ static ชนะ) และ middleware ยกเว้น api/documents/* อยู่แล้ว
import { docverError } from "@/lib/document-versioning";
import {
@ -18,7 +22,6 @@ import {
ticketToDocverClaims,
verifyAddinTicketHmac,
} from "@/lib/addin-ticket";
import { hasVersionByUser } from "@/lib/docver-store";
import { isTicketExpired } from "@/utils/docver-ticket";
import {
issueDocverToken,
@ -30,8 +33,10 @@ import {
export const runtime = "nodejs";
export const dynamic = "force-dynamic";
/** 401 ของทางเข้า ticket — ข้อความเดียวทุกสาเหตุ (HMAC ผิด/รูปผิด/หมดอายุ/ไม่ส่งมา) กัน oracle */
const TICKET_DENIED = "ticket ไม่ถูกต้องหรือหมดอายุ";
/** 401 ของทางเข้า ticket — ข้อความเดียวทุกสาเหตุ (HMAC ผิด/รูปผิด) กัน oracle · หมดอายุแยกเป็น 403 */
const TICKET_DENIED = "ticket ไม่ถูกต้อง";
/** 403 ของทางเข้า ticket — ลายเซ็นถูกแต่หมดอายุ (client มี JWT เดิมใน session ก็ใช้ต่อได้) */
const TICKET_EXPIRED = "ticket ของ e-Court หมดอายุ";
export async function POST(request: Request) {
return request.headers.get("x-api-key") !== null
@ -65,25 +70,20 @@ async function postWithTicket(request: Request): Promise<Response> {
}
const parsed = parseTicketTokenBody(raw);
if (!parsed.ok) {
// missing = ไม่มี ticket มาเล่น ๆ (401 เดียวกับตอน verify ไม่ผ่าน)
// invalid = รูป body ผิดจริง ๆ ให้ 400 ตาม convention กัน typo เงียบ ๆ
return docverError(parsed.error, parsed.kind === "invalid" ? 400 : 401);
// contract 2026-11-04: ฟอร์แม็ตพัง = 400 ทั้ง missing/invalid (เดิม missing เป็น 401)
return docverError(parsed.error, 400);
}
// ตรวจ HMAC ก่อนเสมอ (ตัวตนของผู้ถือไฟล์) — exp ไม่ใช่เกณฑ์ตัดสิทธิ์ ณ จุดนี้
const verified = verifyAddinTicketHmac(parsed.ticketData, parsed.hmacHex, addinTicketSecret());
if (!verified.ok) return docverError(TICKET_DENIED, 401);
// ticket หมดอายุ → ยังออก token ให้ต่อเมื่อ "เอกสารนี้เคยทำงานโดย uid เดียวกัน"
// (requirement ขั้น 7: กลับมาทำเอกสารเดิมผ่านได้แม้ ticket หมดอายุ — พิสูจน์จาก
// document_versions ของกลุ่มนี้เอง) · DB พัง = fail closed เป็น 401 เหมือนไม่มีสิทธิ์
// ลายเซ็นถูกแต่ ticket หมดอายุ → 403 เสมอ (contract 2026-11-04 — เลิก requirement
// ขั้น 7 ฉบับ 2026-10-28: ไม่ตรวจ hasVersionByUser อีกต่อไป เพราะ ticket ฝังอยู่ใน
// ไฟล์ ใครได้ไฟล์ก็ได้ ticket การปล่อยให้ ticket เก่าแลก token ใหม่ได้ตลอดไป
// เท่ากันคือปล่อยสิทธิ์ตามการครอบครองไฟล์ — ฝั่ง client ที่มี JWT ของ fid นี้
// ใน session ที่ยังไม่หมด (7 วัน) ใช้ต่อเอง ไม่มี = ขอไฟล์ใหม่จาก e-Court)
if (isTicketExpired(verified.ticket.exp)) {
let workedBefore = false;
try {
workedBefore = await hasVersionByUser(verified.ticket.fid, verified.ticket.uid);
} catch {
workedBefore = false;
}
if (!workedBefore) return docverError(TICKET_DENIED, 401);
return docverError(TICKET_EXPIRED, 403);
}
return issueToken(ticketToDocverClaims(verified.ticket));
}
@ -96,8 +96,9 @@ async function issueToken(claims: DocverTokenClaims): Promise<Response> {
return docverError("DOCVER_TOKEN_SECRET ยังไม่ถูกตั้งค่า", 500);
}
const ttlRaw = Number(process.env.DOCVER_TOKEN_TTL_SECONDS);
// default 7 วัน (2026-11-04) — token อายุยาวแทนระบบ refresh/renew ของ add-in
const ttlSeconds =
Number.isFinite(ttlRaw) && ttlRaw > 0 ? Math.floor(ttlRaw) : 3600;
Number.isFinite(ttlRaw) && ttlRaw > 0 ? Math.floor(ttlRaw) : 604_800;
const accessToken = issueDocverToken(claims, secret, ttlSeconds);
return new Response(

View file

@ -1,88 +1,129 @@
// app/page.tsx (ระบบ B)
'use client';
export const dynamic = 'force-dynamic'
export const revalidate = 0;
import fs from 'fs';
import path from 'path';
import Link from "next/link";
import type { ChangeEvent } from 'react';
import DependentDropdown, { DropdownData } from '@/components/utils/DependentDropdown';
import TmplHome from "@/components/TmplHome";
import { DropdownData } from '@/components/utils/DependentDropdown';
import {
Bookmark,ToyBrick,FilePenLine,BicepsFlexed,Rocket,Workflow, Files,Blocks,FileQuestionMark,
ExternalLink,
FileText,
Highlighter,
MessagesSquare,
ToyBrick, FilePenLine, Rocket, Workflow, NotepadTextDashed,
Blocks, FileQuestionMark, ShieldLock, FileUp, GitMerge,
FileText
} from "lucide-react";
export default function HomePage() {
const templateData: DropdownData = {
T_X: ['T_no_warning', 'TF_no_warning_P2', 'TS_P2_IN'],
T12_X: ['T12_b2_IN', 'T12_FD155_3s-long'],
};
const handleTmplSelect = (template: string, data: string) => {
//console.log('Location Selected:', { template, data });
};
const handleInput = (e: ChangeEvent<HTMLInputElement>) => {
// console.log(e.target)
return true;
// อ่านไฟล์จากโฟลเดอร์ public/tmpl บน Server
const tmplDirectory = path.join(process.cwd(), 'public/tmpl');
let filenames: string[] = [];
try {
filenames = fs.readdirSync(tmplDirectory);
} catch (error) {
console.error("ไม่พบโฟลเดอร์ public/tmpl", error);
}
// กรองเฉพาะไฟล์ .docx และ .json
const docxFiles = filenames.filter(file => file.endsWith('.docx'));
const jsonFiles = filenames.filter(file => file.endsWith('.json'));
// สร้างโครงสร้างข้อมูลสำหรับ DependentDropdown แบบอัตโนมัติ (หรือปรับเงื่อนไขการจัดกลุ่มตามต้องการ)
// ตัวอย่างนี้จัดกลุ่มตามชื่อไฟล์ตั้งต้น
const templateData: DropdownData = {};
docxFiles.forEach(docx => {
const key = docx.replace('.docx', '');
// หาไฟล์ json ที่ชื่อขึ้นต้นตรงกันมาใส่เป็นตัวเลือกย่อย
const matchedJsons = jsonFiles
.filter(json => json.startsWith(key))
.map(json => json.replace('.json', ''));
templateData[key] = matchedJsons
// templateData[key] = matchedJsons.length > 0 ? matchedJsons : ['default'];
});
return (
<div style={{ padding: '2rem' }}>
<div className="mx-auto max-w-4xl px-4 pb-16 pt-12 sm:px-6">
<h1 className="font-display text-3xl tracking-tight text-ink sm:text-headline">
eCourt Template
DOCEDIT Template & Add-ins
</h1>
<p className="mt-2 text-body text-ink-soft">
ระบบสร้างเทมเพลตเอกสารแบบใหม่ MS Word จากไฟล์ JSON และ Template docx
ชุดพัฒนาระบบสร้างเอกสาร docx จากข้อมูลและเทมเพลต นำไปแก้ไขจัดหน้าต่อบน MS Word โดยที่ข้อมูลในฟอร์มเอกสารสามารถสะกัดออกมาใช้งานต่อได้ ควบคุมรุ่นของเอกสารด้วย Add-ins สามารถผสานกับระบบงานเดิมได้แบบไร้รอยต่อ
</p>
{/* ส่วนเนื้อหาคงที่ (Features, Flow, Add-ins) */}
<section className="mt-10">
<div className="border-b border-line pb-3">
<h2 className="font-display text-section-title text-ink">
ระบบนี้ทำอะไรได้
คุณสมบัติของระบบ
</h2>
</div>
<div className="mt-5 grid gap-5 sm:grid-cols-2">
{[
{
icon: FileText,
title: "DOCX Template",
title: "ออกแบบเทมเพลตด้วย Microsoft Word",
detail:
"ออกแบบหน้าของเอกสารจาก Microsoft Word โดยไม่ต้องใช้เครื่องมือพิเศษ " +
"สามารถนำเอกสารตัวเดิมมาใช้ออกแบบได้เลย " +
"ไม่จำเป็นต้องมีความรู้ด้านการเขียนโปรแกรมหรือ XML ก็สามารถสร้างเทมเพลตได้",
"สร้างเทมเพลตเอกสารได้ง่าย ๆ ด้วย Microsoft Word เครื่องมือที่ทุกคนคุ้นเคย " +
"ไม่จำเป็นต้องติดตั้งโปรแกรมเพิ่มเติมหรือเรียนรู้เครื่องมือเฉพาะทาง สามารถนำเอกสาร Word ที่มีอยู่เดิมมาปรับใช้เป็นเทมเพลตได้ทันที " +
"ไแม้ไม่มีทักษะด้านการเขียนโปรแกรม ก็สามารถออกแบบและสร้างเทมเพลตเอกสารได้ด้วยตนเอง ",
},
{
icon: NotepadTextDashed,
title: "รองรับเอกสารที่มีเงื่อนไขซับซ้อน",
detail:
"รองรับการสร้างเอกสารที่มีโครงสร้างและเงื่อนไขซับซ้อน ไม่ว่าจะเป็นข้อมูลจำนวนไม่แน่นอน การแสดงผลตามเงื่อนไข " +
"หรือการจัดรูปแบบเนื้อหาแบบไดนามิก สามารถเพิ่มชุดคำสั่งและเขียนโปรแกรมลงในเทมเพลตได้ เช่น FOR, IF, " +
"การแทรกโค้ด และฟังก์ชันเพิ่มเติม พร้อม API สำหรับนักพัฒนา เพื่อทดสอบการสร้างเอกสารจากเทมเพลต DOCX ร่วมกับชุดข้อมูล JSON ได้อย่างสะดวก "
},
{
icon: FilePenLine,
title: "Fill Form",
title: "เปลี่ยนเอกสาร Word ให้เป็นแบบฟอร์มอัจฉริยะ",
detail:
"รอรับ Rich และ Plain Text Content Control " +
"สามารถสร้างฟอร์มสำหรับกรอกข้อมูลพร้อมข้อมูลตั้งต้น " +
"ผู้ใช้สามารถนำไปกรอกข้อมูลใน Word ได้เลย และสามารถเใช้โปรแกรมดึงข้อมูลกับมาเก็บในฐานข้อมูลได้",
"รองรับ Rich Text และ Plain Text Content Controls ใน Microsoft Word " +
"สามารถนำไฟล์ DOCX มาสร้างเป็นแบบฟอร์มสำหรับกรอกข้อมูลได้ทันที โดยไม่ต้องเสียเวลาออกแบบฟอร์มใหม่บน " +
"Web Application พร้อม API สำหรับดึงข้อมูลที่ผู้ใช้กรอกกลับมาในรูปแบบ JSON เพื่อนำไปประมวลผล "+
"จัดเก็บลงฐานข้อมูล หรือเชื่อมต่อกับกระบวนการทำงานอื่น ๆ ได้อย่างยืดหยุ่น ",
},
{
icon: ToyBrick,
title: "Integrate with Other System",
title: "เชื่อมต่อระบบงานเดิมได้อย่างอิสร",
detail:
"มี Web API สำหรับสร้างเอกสารจากเทมเพลต(DOCX) กับข้อมูล(JSON) " +
"มี API Key เพื่อจำกัดการใช้งาน สามารถออกบัตรผ่านที่ลงลายเซ็นต์ด้วย HmacSha256" +
"เพื่อให้ผสานกับระบบงานอื่นๆ ได้อย่างปลอดภัย",
"ออกแบบให้ทำงานเป็นระบบอิสระ สามารถเชื่อมต่อกับระบบงานที่มีอยู่เดิมผ่าน API ได้อย่างยืดหยุ่น รองรับการออก Access Token " +
"สำหรับกำหนดสิทธิ์และควบคุมการเข้าถึง โดยไม่จำเป็นต้องพึ่งพาระบบ Single Sign-On (SSO) " +
"หรือเชื่อมต่อเข้าถึงฐานข้อมูลของระบบงานเดิม ช่วยลดความซับซ้อนในการบูรณาการระบบและเพิ่มความคล่องตัวในการพัฒนา ",
},
{
icon: FileText,
title: "Dynamic Document",
icon: GitMerge,
title: "จัดการเวอร์ชันเอกสารอย่างเป็นระบบ",
detail:
"รองรับเทมเพลตที่ซับซ้อน ข้อมูลที่มีไม่แน่นอนอย่างอาเรย์ การแสดงผลตามเงื่อนไขแบบต่างๆ " +
"สามารถเขียนโปรแกรมเพิ่มลงในเทมเพลตได้ รองรับ FOR , IF แทรกโค้ด " +
"ฟังก์ชั่นในตัวเพื่อรองรับการสร้าง Content Control Bookmark ฯลฯ",
"รองรับการจัดเก็บและบริหารจัดการเวอร์ชันของเอกสารร่วมกันผ่าน Microsoft Word Add-ins " +
"สามารถบันทึกเวอร์ชันใหม่ เรียกดูประวัติการแก้ไข และเรียกคืนเวอร์ชันที่ต้องการกลับมาแก้ไขต่อได้ " +
"ช่วยให้การทำงานร่วมกันเป็นระบบ ลดความสับสนจากการจัดเก็บไฟล์หลายฉบับ และสามารถตรวจสอบย้อนกลับไปยังเวอร์ชันก่อนหน้าได้",
},
{
icon: Rocket,
title: " High Performance",
title: "ประสิทธิภาพสูง พร้อมรองรับการขยายระบบ",
detail:
"containerization ขยายระบบได้ง่าย, ระบบเทมเพลตใช้การ XML โดยตรง " +
"การสร้าง template เป็นแบบ Multi-Thread รองรับ CPU หลายแกน " +
"",
"พัฒนาด้วยสถาปัตยกรรมแบบ Container รองรับการขยายระบบ (Scalability) ได้อย่างยืดหยุ่น " +
"กระบวนการสร้างเอกสารจากเทมเพลตทำงานแบบ Multi-threading สามารถใช้ประโยชน์จาก CPU หลายแกนประมวลผลพร้อมกัน " +
"รองรับการจัดเก็บไฟล์ผ่าน Object Storage ที่มีความยืดหยุ่นและรองรับการขยายพื้นที่จัดเก็บ ช่วยเพิ่มประสิทธิภาพและรองรับปริมาณงานที่เพิ่มขึ้นในอนาคต ",
},
{
icon: FileUp,
title: "ส่งออก PDF ด้วยรูปแบบที่ตรงกับต้นฉบับ",
detail:
"สร้างไฟล์ PDF โดยอัตโนมัติทุกครั้งที่บันทึกเวอร์ชันเอกสาร ผ่าน Microsoft Word โดยตรง ทำให้รูปแบบและการจัดวางหน้าของ PDF " +
"สอดคล้องกับเอกสารต้นฉบับ ลดปัญหาความคลาดเคลื่อนของรูปแบบเอกสารจากความแตกต่างของ Microsoft Office แต่ละเวอร์ชัน " +
"นอกจากนี้ยังประมวลผลบนเครื่องผู้ใช้งาน (Client-side Processing) ช่วยลดภาระการประมวลผลและการใช้ทรัพยากรของเซิร์ฟเวอร์",
},
{
icon: ShieldLock,
title: "ปลอดภัยด้วยมาตรฐานการสื่อสารและการควบคุมการเข้าถึง",
detail:
"ให้ความสำคัญกับความปลอดภัยของข้อมูล ด้วยการเข้ารหัสการสื่อสารผ่าน HTTPS และควบคุมการเข้าถึงไฟล์ด้วย Access Token " +
"ที่ผ่านการลงลายมือชื่อดิจิทัลด้วย HMAC-SHA256 ช่วยป้องกันการปลอมแปลง Token และจำกัดการเข้าถึงทรัพยากรตามสิทธิ์ที่ได้รับ ลดความเสี่ยงจากการเข้าถึงข้อมูลโดยไม่ได้รับอนุญาต",
},
].map(({ icon: Icon, title, detail }) => (
<div
@ -101,95 +142,41 @@ export default function HomePage() {
</div>
))}
</div>
</section>
{/* ส่วนตัวอย่างการใช้งานที่ต้องการให้อัปเดตอัตโนมัติ */}
<section className="mt-10">
<div className="border-b border-line pb-3">
<h2 className="font-display text-section-title text-ink">
ตัวอย่างการใช้งาน
</h2>
</div>
<TmplHome templateData={templateData} />
</section>
<section className="mt-10">
<div className="mt-5 grid gap-5 sm:grid-cols-2">
<div className="flex gap-4 rounded-xl border border-line bg-white p-6">
<span className="flex h-10 w-10 shrink-0 items-center justify-center rounded-lg bg-primary/[0.08] text-primary">
<ExternalLink className="h-5 w-5" aria-hidden />
<FileQuestionMark className="h-5 w-5" aria-hidden />
</span>
<div>
<h3 className="font-display text-subhead text-ink">Link (GET)</h3>
<h3 className="font-display text-subhead text-ink">วิธีการใช้งาน</h3>
<p className="mt-1.5 text-sm leading-relaxed text-ink-soft">
<a href="/docedit/api/template/download-docx?tid=T_X&fid=T_no_warning&uid=john&uname=John%20Doe">T_no_warning_john_xxx.docx</a><br />
<a href="/docedit/api/template/download-docx?tid=T_X&fid=TF_no_warning_P2&uid=john&uname=John%20Doe">TF_no_warning_P2_john_xxx.docx</a><br />
<a href="/docedit/api/template/download-docx?tid=T_X&fid=TS_P2_IN&uid=john&uname=John%20Doe">TS_P2_IN_john_xxx.docx</a><br />
<a href="/docedit/api/template/download-docx?tid=T12_X&fid=T12_b2_IN&uid=john&uname=John%20Doe">T12_b2_IN_john_xxx.docx</a><br />
<a href="/docedit/api/template/download-docx?tid=T12_X&fid=T12_FD155_3s-long&uid=john&uname=John%20Doe">T12_FD155_3s-long_john_xxx.docx</a><br />
<a target="_blank" href="https://www.youtube.com/watch?v=QxMitSk3S4E" className="text-primary transition hover:underline">
แนะนำเทมเพลตของ Docedit เบื้องต้น</a><br />
<a target="_blank" href="https://www.youtube.com/watch?v=kOBoVN-KQzU" className="text-primary transition hover:underline">
ติดตั้ง MS Word Add-ins</a><br />
<a target="_blank" href="https://www.youtube.com/watch?v=P1-xe24nOUo" className="text-primary transition hover:underline">
ใช้งาน DOCEDIT Add-ins</a><br />
</p>
</div>
</div>
<div className="flex gap-4 rounded-xl border border-line bg-white p-6">
<span className="flex h-10 w-10 shrink-0 items-center justify-center rounded-lg bg-primary/[0.08] text-primary">
<Files className="h-5 w-5" aria-hidden />
</span>
<div>
<h3 className="font-display text-subhead text-ink">Template & Data</h3>
<p className="mt-1.5 text-sm leading-relaxed text-ink-soft">
Template: <Link href="/tmpl/T_X.docx">T_X.docx</Link>, &nbsp;
<Link href="/tmpl/T12_X.docx">T12_X.docx</Link><br />
Data: <Link href="tmpl/TF_no_warning_P2.json">TF_no_warning_P2.json</Link>, &nbsp;
<Link href="/tmpl/T_no_warning.json">T_no_warning.json</Link>, &nbsp;
<Link href="/tmpl/TS_P2_IN.json">TS_P2_IN.json</Link><br />
<Link href="/tmpl/T12_b2_IN.json">T12_b2_IN.json</Link>, &nbsp;
<Link href="/tmpl/T12_FD155_3s-long.json">T12_FD155_3s-long.json</Link>
</p>
</div>
</div>
<div className="flex gap-4 rounded-xl border border-line bg-white p-6">
<span className="flex h-10 w-10 shrink-0 items-center justify-center rounded-lg bg-primary/[0.08] text-primary">
<FilePenLine className="h-5 w-5" aria-hidden />
</span>
<div>
<h3 className="font-display text-subhead text-ink">Form (POST)</h3>
<div className="mt-1.5 text-sm leading-relaxed text-ink-soft">
<form action="/docedit/api/template/download-docx" method="POST">
<DependentDropdown data={templateData} name1='tid' name2='fid'
label1="Template(tid):" label2="ข้อมูล(fid):" onSelectChange={handleTmplSelect} />
<div className="mb-2"></div>
<div>
<label htmlFor="uid">uid: &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;
<input type="text" name="uid" className="p-2 mb-2 border border-gray-300 rounded-md" defaultValue="john" />
</label>
</div>
<div>
<label htmlFor="uname">uname:
<input type="text" name="uname" className="p-2 mb-2 border border-gray-300 rounded-md" defaultValue="John Doe" />
</label>
</div>
<button
type="submit"
style={{
padding: '10px 20px',
backgroundColor: '#0070f3',
color: '#fff',
border: 'none',
borderRadius: '5px',
cursor: 'pointer',
}}
>
ดาวน์โหลดเอกสาร MS Word
</button>
</form>
</div>
</div>
</div>
<div className="flex gap-4 rounded-xl border border-line bg-white p-6">
<span className="flex h-10 w-10 shrink-0 items-center justify-center rounded-lg bg-primary/[0.08] text-primary">
<Workflow className="h-5 w-5" aria-hidden />
@ -197,7 +184,7 @@ export default function HomePage() {
<div>
<h3 className="font-display text-subhead text-ink">Flow</h3>
<div className="mt-1.5 text-sm leading-relaxed text-ink-soft">
<div className="text-xl font-bold text-center">
<div className="text-center">
ลิงค์เลือกไฟล์<br />
&darr; <br />
ตรวจสิทธิ์ สร้าง docx <br />
@ -214,23 +201,11 @@ export default function HomePage() {
</div>
</div>
<div className="flex gap-4 rounded-xl border border-line bg-white p-6">
<span className="flex h-10 w-10 shrink-0 items-center justify-center rounded-lg bg-primary/[0.08] text-primary">
<FileQuestionMark className="h-5 w-5" aria-hidden />
</span>
<div>
<h3 className="font-display text-subhead text-ink">วิธีการใช้งาน</h3>
<p className="mt-1.5 text-sm leading-relaxed text-ink-soft">
<Link href="https://www.youtube.com/watch?v=QxMitSk3S4E" className="focus-ring -m-1 inline-block rounded-md px-1 py-0.5 font-medium text-primary transition hover:underline">
แนะนำเทมเพลตของ Docedit เบื้องต้น</Link><br />
<Link href="https://www.youtube.com/watch?v=3emGNqRZgMs" className="focus-ring -m-1 inline-block rounded-md px-1 py-0.5 font-medium text-primary transition hover:underline">
Sideload Office add-ins</Link>
</p>
</div>
</div>
</div>
</section>
<section className="mt-10">
<div className="border-b border-line pb-3">
<h2 className="font-display text-section-title text-ink">
@ -245,24 +220,48 @@ export default function HomePage() {
<h3 className="font-display text-subhead text-ink">
Word Add-ins<br />
</h3>
<p className="mt-1.5 text-sm leading-relaxed text-ink-soft">
ดาวน์โหลด&nbsp;
<Link href="/wordaddin/manifest.xml" className="focus-ring -m-1 inline-block rounded-md px-1 py-0.5 font-medium text-primary transition hover:underline">
manifest.xml </Link>&nbsp;
ไว้ที่ SharePoint แชร์เน็ตเวิร์ก หรือดาวน์โหลดสคริปต์ PowerShell(Dev Install) &nbsp;
<Link href="/wordaddin/install-word-addins.ps1" className="focus-ring -m-1 inline-block rounded-md px-1 py-0.5 font-medium text-primary transition hover:underline">
install-word-addins.ps1 </Link> &nbsp;
แล้วใช้คำสั่ง PowerShell ด้านล่างเพื่อติดตั้ง Add-ins ใน Word<br />
<div className="mt-1.5 text-sm leading-relaxed text-ink-soft">
การนำไปเผยแพร่สามารถติดตั้งได้หลายวิธี (แนะนำ Shared Folder Sideloading)
<ol>
<li>1. ติดตั้งผ่าน Office Store การเผยแพร่ต้องสมัครบัญชีนักพัฒนาใช้ Microsoft Partner Center ลงทะเบียนในโปรแกรม Office Store</li>
<li>2. การติดตั้งระดับองค์กร (ผ่าน Microsoft 365 Admin Center) ทำตาม
<a target="_blank" className="text-primary transition hover:underline" href="https://www.youtube.com/watch?v=KB_KRfAfJzY">
วีดีโอนี้</a></li>
<li>3. Sideloading ด้วยไฟล์ <Link target="_blank" href="/wordaddin/manifest.xml" className="text-primary transition hover:underline">
manifest.xml </Link>(Office 365) Insert&gt;Add-ins&gt;My Add-ins คลิ้ก "Upload My Add-in"</li>
<li>4. Shared Folder Sideloading(Office 2019+ รุ่นเดสท็อป) นำไฟล์ <Link target="_blank" href="/wordaddin/manifest.xml" className="text-primary transition hover:underline">
manifest.xml </Link>&nbsp;ไปทำตามวีดีโอ <a target="_blank" href="https://www.youtube.com/watch?v=kOBoVN-KQzU" className="text-primary transition hover:underline">
ติดตั้ง MS Word Add-ins</a> </li>
<li>5. นักพัฒนา ใช้คำสั่ง reg.exe ร่วมกับ <Link target="_blank" href="/wordaddin/manifest.xml" className="text-primary transition hover:underline">
manifest.xml </Link>&nbsp;ไปทำตามวีดีโอ <a target="_blank" href="https://www.youtube.com/watch?v=kOBoVN-KQzU" className="text-primary transition hover:underline">
ติดตั้ง MS Word Add-ins</a> </li>
<li>6. นักพัฒนาสร้างสคริปต์
<Link href="/wordaddin/install-word-addins.ps1" className="text-primary transition hover:underline">
install-word-addins.ps1 (เรียกใช้ reg.exe)</Link>&nbsp;
สำหรับ Power Shell จะต้องให้สิทธิ์ในการเรียกใช้ สามารถเรียกคำสั่งด้านล่างเพื่อติดตั้ง<br />
<br />
<b className="whitespace-nowrap">Unblock-File .\install-word-addins.ps1</b><br />
<b className="whitespace-nowrap">Set-ExecutionPolicy -Scope Process -ExecutionPolicy RemoteSigned -Force</b><br />
<b className="whitespace-nowrap">.\install-word-addins.ps1 -Install</b><br /></li>
</ol>
<br />
<b className="whitespace-nowrap">Unblock-File .\install-word-addins.ps1</b>
<b className="whitespace-nowrap">Set-ExecutionPolicy -Scope Process -ExecutionPolicy RemoteSigned -Force</b>
<b className="whitespace-nowrap">.\install-word-addins.ps1 -Install</b>
</p>
<b>หมายเหตุ:</b>
<i>
Add-ins เป็นรุ่นใหม่(Office.js) มีความเป็นไปได้ที่จะรองรับรุ่น Desktop(Windows/Mac), Web และ iPad
ด้วยข้อกำหนดของ TOR ให้รองรับ Word 2019(Desktop) บนวินโดว์ 10 เป็นขั้นต่ำ ทำให้ถูกจำกัดที่ WordApi 1.3 เท่านั้น
ฟึเจอร์บางอย่างบน Add-ins จะถูกจำกัดตามความสามารถตามรุ่นของ WordApi การทดสอบต่างๆตามที่กำหนดใน TOR
<br />
สำหรับ Microsoft Office เวอร์ชันขายขาดแบบเก่า (เช่น Office 2016, 2019, 2021 หรือ 2024) จะไม่มีปุ่มอัปโหลดไฟล์ XML
บนหน้าต่างเมนูโดยตรงเหมือน Microsoft 365 หากต้องการติดตั้งไฟล์ manifest.xml ในรุ่นดังกล่าว
นะนำให้ใช้วิธีแชร์โฟลเดอร์เครือข่าย (Shared Folder Sideloading) หรือผ่าน Microsoft 365 Admin Center
<br />
</i>
</div>
</div>
</div>
</section>
</div>
</div>
);