backen: jwt

This commit is contained in:
2025-09-30 16:38:37 +07:00
parent 8b2dff8d1d
commit 5be0f5407b
396 changed files with 33377 additions and 4851 deletions

View File

@@ -1,4 +1,4 @@
[/dms]
max_log = 491862
number = 1
max_log = 494130
number = 3
finish = 1

File diff suppressed because it is too large Load Diff

13476
.qsync/meta/qmeta1 Normal file

File diff suppressed because it is too large Load Diff

7732
.qsync/meta/qmeta2 Normal file

File diff suppressed because it is too large Load Diff

483
Bearer-Token.patch.diff Executable file
View File

@@ -0,0 +1,483 @@
diff --git a/backend/src/middleware/requireBearer.js b/backend/src/middleware/requireBearer.js
new file mode 100644
index 0000000..1111111
--- /dev/null
+++ b/backend/src/middleware/requireBearer.js
@@ -0,0 +1,44 @@
+// backend/src/middleware/requireBearer.js
+import jwt from "jsonwebtoken";
+import { findUserById } from "../db/models/users.js";
+
+export async function requireBearer(req, res, next) {
+ const hdr = req.get("Authorization") || "";
+ const m = hdr.match(/^Bearer\s+(.+)$/i);
+ if (!m) return res.status(401).json({ error: "Unauthenticated" });
+ try {
+ const payload = jwt.verify(m[1], process.env.JWT_ACCESS_SECRET, {
+ issuer: "dms-backend",
+ });
+ const user = await findUserById(payload.user_id);
+ if (!user) return res.status(401).json({ error: "Unauthenticated" });
+ req.user = {
+ user_id: user.user_id,
+ username: user.username,
+ email: user.email,
+ first_name: user.first_name,
+ last_name: user.last_name,
+ };
+ next();
+ } catch {
+ return res.status(401).json({ error: "Unauthenticated" });
+ }
+}
diff --git a/backend/src/routes/auth.js b/backend/src/routes/auth.js
index 2222222..3333333 100644
--- a/backend/src/routes/auth.js
+++ b/backend/src/routes/auth.js
@@ -1,99 +1,109 @@
-// (เดิม) ผูกกับคุกกี้ / ส่ง ok:true ฯลฯ
+// backend/src/routes/auth.js — Bearer Token ล้วน
import { Router } from "express";
import jwt from "jsonwebtoken";
-import { findUserByUsername } from "../db/models/users.js";
+import { findUserByUsername, findUserById } from "../db/models/users.js";
import { verifyPassword } from "../utils/passwords.js";
-// NOTE: ลบการใช้งาน res.cookie(...) ทั้งหมด
+// NOTE: ไม่มีการใช้ res.cookie(...) อีกต่อไป
const router = Router();
function signAccessToken(user) {
return jwt.sign(
{ user_id: user.user_id, username: user.username },
process.env.JWT_ACCESS_SECRET,
- { issuer: "dms-backend", expiresIn: "30m" } // ปรับได้
+ { issuer: "dms-backend", expiresIn: "30m" }
);
}
function signRefreshToken(user) {
return jwt.sign(
- { user_id: user.user_id, username: user.username },
+ { user_id: user.user_id, username: user.username, t: "refresh" },
process.env.JWT_REFRESH_SECRET,
{ issuer: "dms-backend", expiresIn: "30d" }
);
}
router.post("/login", async (req, res) => {
const { username, password } = req.body || {};
const user = await findUserByUsername(username);
if (!user || !(await verifyPassword(password, user.password_hash))) {
return res.status(401).json({ error: "INVALID_CREDENTIALS" });
}
const token = signAccessToken(user);
const refresh_token = signRefreshToken(user);
return res.json({
token,
refresh_token,
user: {
user_id: user.user_id,
username: user.username,
email: user.email,
first_name: user.first_name,
last_name: user.last_name,
},
});
});
+router.post("/refresh", async (req, res) => {
+ const hdr = req.get("Authorization") || "";
+ const m = hdr.match(/^Bearer\s+(.+)$/i);
+ const r = m?.[1];
+ if (!r) return res.status(401).json({ error: "NO_REFRESH_TOKEN" });
+ try {
+ const payload = jwt.verify(r, process.env.JWT_REFRESH_SECRET, {
+ issuer: "dms-backend",
+ });
+ const user = await findUserById(payload.user_id);
+ if (!user) return res.status(401).json({ error: "USER_NOT_FOUND" });
+ const token = signAccessToken(user);
+ return res.json({ token });
+ } catch {
+ return res.status(401).json({ error: "INVALID_REFRESH_TOKEN" });
+ }
+});
+
export default router;
diff --git a/backend/src/index.js b/backend/src/index.js
index 4444444..5555555 100644
--- a/backend/src/index.js
+++ b/backend/src/index.js
@@ -1,60 +1,69 @@
import express from "express";
import cors from "cors";
import authRouter from "./routes/auth.js";
+import { requireBearer } from "./middleware/requireBearer.js";
-// import routers อื่น ๆ ตามจริง เช่น rfasRouter, transmittalsRouter
const app = express();
-// CORS เดิม (อาจมี credentials)
-app.use(cors({
- origin: true,
- credentials: true,
-}));
+// ✅ CORS สำหรับ Bearer: ไม่ต้อง credentials, อนุญาต Authorization header
+app.use(cors({
+ origin: [
+ "https://lcbp3.np-dms.work",
+ "http://localhost:3000"
+ ],
+ methods: ["GET","POST","PUT","PATCH","DELETE","OPTIONS"],
+ allowedHeaders: ["Authorization","Content-Type","Accept","Origin","Referer","User-Agent","X-Requested-With","Cache-Control","Pragma"],
+ exposedHeaders: ["Content-Disposition","Content-Length"]
+}));
app.use(express.json());
-// routes เดิม
-app.use("/api/auth", authRouter);
-// app.use("/api/rfas", rfasRouter);
-// app.use("/api/transmittals", transmittalsRouter);
+// ✅ เส้นทาง auth (ไม่ต้องมี token)
+app.use("/api/auth", authRouter);
+
+// ✅ ตั้ง guard สำหรับเส้นทางที่เหลือต้องล็อกอิน
+app.use("/api", requireBearer);
+// แล้วค่อย mount routers protected ใต้ /api
+// app.use("/api/rfas", rfasRouter);
+// app.use("/api/transmittals", transmittalsRouter);
app.use((err, _req, res, _next) => {
console.error(err);
res.status(500).json({ error: "INTERNAL_SERVER_ERROR" });
});
const port = process.env.PORT || 4000;
app.listen(port, () => console.log(`backend listening on :${port}`));
diff --git a/frontend/app/(auth)/login/page.jsx b/frontend/app/(auth)/login/page.jsx
index 6666666..7777777 100644
--- a/frontend/app/(auth)/login/page.jsx
+++ b/frontend/app/(auth)/login/page.jsx
@@ -1,200 +1,236 @@
// File: frontend/app/(auth)/login/page.jsx
"use client";
-// เวอร์ชันเดิม
+// ✅ Bearer-only + Debug toggle (NEXT_PUBLIC_DEBUG_AUTH)
import { useState, useMemo, Suspense } from "react";
import { useSearchParams, useRouter } from "next/navigation";
import {
Card, CardHeader, CardTitle, CardDescription, CardContent, CardFooter,
} from "@/components/ui/card";
import { Label } from "@/components/ui/label";
import { Input } from "@/components/ui/input";
import { Button } from "@/components/ui/button";
import { Alert, AlertDescription } from "@/components/ui/alert";
-const API_BASE = process.env.NEXT_PUBLIC_API_BASE?.replace(/\/$/, "") || "";
+const API_BASE = process.env.NEXT_PUBLIC_API_BASE?.replace(/\/$/, "") || "";
+const DEBUG =
+ String(process.env.NEXT_PUBLIC_DEBUG_AUTH || "").trim() !== "" &&
+ process.env.NEXT_PUBLIC_DEBUG_AUTH !== "0" &&
+ process.env.NEXT_PUBLIC_DEBUG_AUTH !== "false";
+function dlog(...args) {
+ if (DEBUG && typeof window !== "undefined") console.debug("[login]", ...args);
+}
function LoginForm() {
const router = useRouter();
const searchParams = useSearchParams();
const nextPath = useMemo(
() => searchParams.get("next") || "/dashboard",
[searchParams]
);
const [username, setUsername] = useState("");
const [password, setPassword] = useState("");
const [showPw, setShowPw] = useState(false);
const [remember, setRemember] = useState(false);
const [submitting, setSubmitting] = useState(false);
const [err, setErr] = useState("");
async function onSubmit(e) {
e.preventDefault();
setErr("");
if (!username.trim() || !password) {
setErr("กรอกชื่อผู้ใช้และรหัสผ่านให้ครบ");
return;
}
try {
setSubmitting(true);
+ dlog("API_BASE =", API_BASE || "(empty → relative)");
+ dlog("nextPath =", nextPath, "remember =", remember);
const res = await fetch(`${API_BASE}/api/auth/login`, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ username, password }),
cache: "no-store",
});
- const data = await res.json().catch(() => ({}));
+ dlog("response.status =", res.status);
+ dlog("response.headers.content-type =", res.headers.get("content-type"));
+ let data = {};
+ try { data = await res.json(); } catch (e) { dlog("response.json() error =", e); }
+ dlog("response.body =", data);
if (!res.ok) {
- setErr(data?.error || "เข้าสู่ระบบไม่สำเร็จ");
+ const msg =
+ data?.error === "INVALID_CREDENTIALS"
+ ? "ชื่อผู้ใช้หรือรหัสผ่านไม่ถูกต้อง"
+ : data?.error || `เข้าสู่ระบบไม่สำเร็จ (HTTP ${res.status})`;
+ dlog("login FAILED →", msg);
+ setErr(msg);
return;
}
+ if (!data?.token) {
+ dlog("login FAILED → data.token not found");
+ setErr("รูปแบบข้อมูลตอบกลับไม่ถูกต้อง (ไม่มี token)");
+ return;
+ }
const storage = remember ? window.localStorage : window.sessionStorage;
storage.setItem("dms.token", data.token);
storage.setItem("dms.refresh_token", data.refresh_token);
storage.setItem("dms.user", JSON.stringify(data.user || {}));
+ dlog("token stored in", remember ? "localStorage" : "sessionStorage");
try {
window.dispatchEvent(
new StorageEvent("storage", { key: "dms.auth", newValue: "login" })
);
} catch {}
- router.replace(nextPath);
+ dlog("navigating →", nextPath);
+ router.replace(nextPath);
} catch (e) {
+ dlog("exception =", e);
setErr("เชื่อมต่อเซิร์ฟเวอร์ไม่ได้ กรุณาลองใหม่");
} finally {
setSubmitting(false);
+ dlog("done");
}
}
return (
<div className="grid min-h-[calc(100vh-4rem)] place-items-center p-4">
<Card className="w-full max-w-md border-0 shadow-xl ring-1 ring-black/5 bg-white/90 backdrop-blur">
<CardHeader className="space-y-1">
<CardTitle className="text-2xl font-bold text-sky-800">เข้าสู่ระบบ</CardTitle>
<CardDescription className="text-sky-700">Document Management System • LCBP3</CardDescription>
</CardHeader>
<CardContent>
{err ? (
<Alert className="mb-4"><AlertDescription>{err}</AlertDescription></Alert>
) : null}
<form onSubmit={onSubmit} className="grid gap-4">
<div className="grid gap-2">
<Label htmlFor="username">ชื่อผู้ใช้</Label>
<Input id="username" autoFocus autoComplete="username" value={username}
onChange={(e) => setUsername(e.target.value)} placeholder="เช่น superadmin" disabled={submitting}/>
</div>
<div className="grid gap-2">
<Label htmlFor="password">รหัสผ่าน</Label>
<div className="relative">
<Input id="password" type={showPw ? "text" : "password"} autoComplete="current-password"
value={password} onChange={(e) => setPassword(e.target.value)} placeholder="••••••••"
disabled={submitting} className="pr-10"/>
<button type="button" onClick={() => setShowPw((v) => !v)}
className="absolute inset-y-0 px-2 my-auto text-xs bg-white border rounded-md right-2 hover:bg-slate-50"
aria-label={showPw ? "ซ่อนรหัสผ่าน" : "แสดงรหัสผ่าน"} disabled={submitting}>
{showPw ? "Hide" : "Show"}
</button>
</div>
</div>
<div className="flex items-center justify-between pt-1">
<label className="inline-flex items-center gap-2 text-sm text-slate-600">
<input type="checkbox" className="size-4 accent-sky-700"
checked={remember} onChange={(e) => setRemember(e.target.checked)} disabled={submitting}/>
จดจำฉันไว้ในเครื่องนี้
</label>
<a href="/forgot-password" className="text-sm text-sky-700 hover:text-sky-900 hover:underline">ลืมรหัสผ่าน?</a>
</div>
<Button type="submit" disabled={submitting} className="mt-2 bg-sky-700 hover:bg-sky-800">
{submitting ? (<span className="inline-flex items-center gap-2"><Spinner /> กำลังเข้าสู่ระบบ…</span>) : ("เข้าสู่ระบบ")}
</Button>
+ {DEBUG ? (
+ <p className="mt-2 text-xs text-slate-500">
+ DEBUG: NEXT_PUBLIC_API_BASE = <code>{API_BASE || "(empty)"}</code>
+ </p>
+ ) : null}
</form>
</CardContent>
<CardFooter className="text-xs text-center text-slate-500">
&copy; {new Date().getFullYear()} np-dms.work
</CardFooter>
</Card>
</div>
);
}
export default function LoginPage() {
return (
<Suspense fallback={<LoginPageSkeleton />}>
<LoginForm />
</Suspense>
);
}
function LoginPageSkeleton() {
return (
<div className="grid min-h-[calc(100vh-4rem)] place-items-center p-4">
<Card className="w-full max-w-md border-0 shadow-xl ring-1 ring-black/5 bg-white/90 backdrop-blur">
<CardHeader className="space-y-1">
<CardTitle className="text-2xl font-bold text-sky-800">เข้าสู่ระบบ</CardTitle>
<CardDescription className="text-sky-700">Document Management System • LCBP3</CardDescription>
</CardHeader>
<CardContent>
<div className="grid gap-4 animate-pulse">
<div className="h-10 rounded bg-slate-200"></div>
<div className="h-10 rounded bg-slate-200"></div>
<div className="h-10 rounded bg-slate-200"></div>
</div>
</CardContent>
</Card>
</div>
);
}
function Spinner() {
return (
<svg className="animate-spin size-4" viewBox="0 0 24 24" fill="none" aria-hidden="true">
<circle className="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" strokeWidth="4" />
<path className="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8v4a4 4 0 00-4 4H4z" />
</svg>
);
}
diff --git a/frontend/app/(protected)/layout.jsx b/frontend/app/(protected)/layout.jsx
new file mode 100644
index 0000000..8888888
--- /dev/null
+++ b/frontend/app/(protected)/layout.jsx
@@ -0,0 +1,38 @@
+"use client";
+import { useEffect, useState } from "react";
+import { usePathname, useRouter } from "next/navigation";
+
+export default function ProtectedLayout({ children }) {
+ const router = useRouter();
+ const pathname = usePathname();
+ const [ready, setReady] = useState(false);
+
+ useEffect(() => {
+ try {
+ const token =
+ (typeof window !== "undefined" &&
+ (localStorage.getItem("dms.token") ||
+ sessionStorage.getItem("dms.token"))) ||
+ null;
+ if (!token) {
+ const next = encodeURIComponent(pathname || "/dashboard");
+ router.replace(`/login?next=${next}`);
+ return;
+ }
+ } finally {
+ setReady(true);
+ }
+ }, [pathname, router]);
+
+ if (!ready) {
+ return (
+ <div className="grid min-h-[calc(100vh-4rem)] place-items-center p-6 text-slate-600">
+ กำลังตรวจสิทธิ์…
+ </div>
+ );
+ }
+ return <>{children}</>;
+}
diff --git a/frontend/lib/api.js b/frontend/lib/api.js
new file mode 100644
index 0000000..9999999
--- /dev/null
+++ b/frontend/lib/api.js
@@ -0,0 +1,45 @@
+// frontend/lib/api.js
+const API_BASE = process.env.NEXT_PUBLIC_API_BASE?.replace(/\/$/, "") || "";
+
+function getToken() {
+ if (typeof window === "undefined") return null;
+ return localStorage.getItem("dms.token") || sessionStorage.getItem("dms.token");
+}
+
+export async function apiFetch(path, options = {}) {
+ const token = getToken();
+ const headers = new Headers(options.headers || {});
+ headers.set("Accept", "application/json");
+ if (!headers.has("Content-Type")) headers.set("Content-Type", "application/json");
+ if (token) headers.set("Authorization", `Bearer ${token}`);
+
+ const res = await fetch(`${API_BASE}${path}`, { ...options, headers, cache: "no-store" });
+
+ if (res.status === 401) {
+ const refresh =
+ localStorage.getItem("dms.refresh_token") || sessionStorage.getItem("dms.refresh_token");
+ if (refresh) {
+ const r = await fetch(`${API_BASE}/api/auth/refresh`, {
+ method: "POST",
+ headers: { Authorization: `Bearer ${refresh}` },
+ });
+ if (r.ok) {
+ const { token: newToken } = await r.json();
+ const store = localStorage.getItem("dms.refresh_token") ? localStorage : sessionStorage;
+ store.setItem("dms.token", newToken);
+ const headers2 = new Headers(headers);
+ headers2.set("Authorization", `Bearer ${newToken}`);
+ return fetch(`${API_BASE}${path}`, { ...options, headers: headers2, cache: "no-store" });
+ }
+ }
+ }
+ return res;
+}
diff --git a/frontend/middleware.ts b/frontend/middleware.ts
index aaaaaaa..bbbbbbb 100644
--- a/frontend/middleware.ts
+++ b/frontend/middleware.ts
@@ -1,15 +1,14 @@
-import { NextResponse } from "next/server";
-import type { NextRequest } from "next/server";
-
-// เดิม: ตรวจคุกกี้แล้ว redirect /dashboard
-export function middleware(req: NextRequest) {
- // ... logic เดิมที่ใช้คุกกี้
- return NextResponse.next();
-}
-
-export const config = {
- matcher: ["/(protected/:path*)","/dashboard","/users/:path*","/api/:path*"],
-};
+import { NextResponse } from "next/server";
+// ✅ ไม่บล็อกเพจอีกต่อไป (Bearer อยู่ใน storage ฝั่ง client)
+export function middleware() {
+ return NextResponse.next();
+}
+// จำกัดให้ทำงานเฉพาะ /api ถ้าต้องการใช้ในอนาคต (ตอนนี้ผ่านเฉย ๆ)
+export const config = { matcher: ["/api/:path*"] };
diff --git a/frontend/app/(protected)/dashboard/page.jsx b/frontend/app/(protected)/dashboard/page.jsx
new file mode 100644
index 0000000..ccccccc
--- /dev/null
+++ b/frontend/app/(protected)/dashboard/page.jsx
@@ -0,0 +1,11 @@
+"use client";
+export default function DashboardPage() {
+ return (
+ <main className="p-6">
+ <h1 className="text-2xl font-semibold text-sky-800">Dashboard</h1>
+ <p className="text-slate-600 mt-2">
+ ยินดีต้อนรับสู่ DMS
+ </p>
+ </main>
+ );
+}

View File

@@ -0,0 +1,64 @@
diff --git a/src/index.js b/src/index.js
--- a/src/index.js
+++ b/src/index.js
@@ -1,9 +1,8 @@
import fs from "node:fs";
import path from "node:path";
import express from "express";
-import cookieParser from "cookie-parser";
import cors from "cors";
import sql from "./db/index.js";
import healthRouter from "./routes/health.js";
import { authJwt } from "./middleware/authJwt.js";
@@ -64,7 +63,7 @@
// ✅ อยู่หลัง NPM/Reverse proxy → ให้ trust proxy เพื่อให้ cookie secure / proto ทำงานถูก
app.set("trust proxy", 1);
-// CORS แบบกำหนด origin ตามรายการที่อนุญาต + อนุญาต credentials (จำเป็นสำหรับ cookie)
+// ✅ CORS สำหรับ Bearer token: ไม่ต้องใช้ credentials (ไม่มีคุกกี้)
app.use(
cors({
origin(origin, cb) {
if (!origin) return cb(null, true); // server-to-server / curl
return cb(null, ALLOW_ORIGINS.includes(origin));
},
- credentials: true,
+ credentials: false,
methods: ["GET", "POST", "PUT", "PATCH", "DELETE", "OPTIONS"],
- allowedHeaders: ["Content-Type", "Authorization", "X-Requested-With"],
+ allowedHeaders: [
+ "Content-Type",
+ "Authorization",
+ "X-Requested-With",
+ "Accept",
+ "Origin",
+ "Referer",
+ "User-Agent",
+ "Cache-Control",
+ "Pragma"
+ ],
exposedHeaders: ["Content-Disposition", "Content-Length"],
})
);
// preflight
app.options(
"*",
cors({
origin(origin, cb) {
if (!origin) return cb(null, true);
return cb(null, ALLOW_ORIGINS.includes(origin));
},
- credentials: true,
+ credentials: false,
})
);
-app.use(cookieParser());
+// ❌ ไม่ต้อง parse cookie แล้ว (เราไม่ใช้คุกกี้สำหรับ auth)
+// app.use(cookieParser());
// Payload limits
app.use(express.json({ limit: "10mb" }));
app.use(express.urlencoded({ extended: true, limit: "10mb" }));

View File

@@ -84,7 +84,7 @@ services:
JWT_SECRET: "8b0df02e4aee9f9f79a4f2d8ba77b0b82c1ee3446b68cb0bae94ab54d60f8d9e"
JWT_EXPIRES_IN: "12h"
PASSWORD_SALT_ROUNDS: "10"
FRONTEND_ORIGIN: "https://lcbp3.mycloudnas.com"
FRONTEND_ORIGIN: "https://lcbp3.np-dms.work"
CORS_ORIGINS: "https://backend.np-dms.work,http://localhost:3000,http://127.0.0.1:3000"
RATE_LIMIT_WINDOW_MS: "900000"
RATE_LIMIT_MAX: "200"
@@ -129,8 +129,11 @@ services:
# NEXT_PUBLIC_API_BASE: "/api"
CHOKIDAR_USEPOLLING: "1"
WATCHPACK_POLLING: "true"
NEXT_PUBLIC_API_BASE: "https://lcbp3.np-dms.work/api"
NEXT_PUBLIC_API_BASE: "https://lcbp3.np-dms.work"
NEXT_PUBLIC_DEBUG_AUTH: "1"
NEXT_TELEMETRY_DISABLED: "1"
JWT_ACCESS_SECRET: "change-this-access-secret"
JWT_REFRESH_SECRET: "change-this-refresh-secret"
expose:
- "3000"
networks: [dmsnet]

View File

@@ -8,6 +8,7 @@
// - ไม่ใช้ credentials: "include" อีกต่อไป
// - เอา RootLayout/metadata ออก เพราะไฟล์เพจเป็น client component
// - เพิ่มการอ่าน NEXT_PUBLIC_API_BASE และ error handling ให้ตรงกับ backend
// - เพิ่มโหมดดีบัก เปิดด้วย NEXT_PUBLIC_DEBUG_AUTH=1
import { useState, useMemo, Suspense } from "react";
import { useSearchParams, useRouter } from "next/navigation";
@@ -25,6 +26,16 @@ import { Button } from "@/components/ui/button";
import { Alert, AlertDescription } from "@/components/ui/alert";
const API_BASE = process.env.NEXT_PUBLIC_API_BASE?.replace(/\/$/, "") || "";
const DEBUG =
String(process.env.NEXT_PUBLIC_DEBUG_AUTH || "").trim() !== "" &&
process.env.NEXT_PUBLIC_DEBUG_AUTH !== "0" &&
process.env.NEXT_PUBLIC_DEBUG_AUTH !== "false";
function dlog(...args) {
if (DEBUG && typeof window !== "undefined") {
console.debug("[login]", ...args);
}
}
function LoginForm() {
const router = useRouter();
@@ -53,6 +64,12 @@ function LoginForm() {
try {
setSubmitting(true);
// ── DEBUG: ค่าเบื้องต้น
dlog("API_BASE =", API_BASE || "(empty → จะเรียก path relative)");
dlog("nextPath =", nextPath);
dlog("remember =", remember);
dlog("payload =", { username: "[hidden]", password: "[hidden]" });
const res = await fetch(`${API_BASE}/api/auth/login`, {
method: "POST",
headers: { "Content-Type": "application/json" },
@@ -60,14 +77,30 @@ function LoginForm() {
cache: "no-store",
});
const data = await res.json().catch(() => ({}));
dlog("response.status =", res.status);
dlog("response.headers.content-type =", res.headers.get("content-type"));
let data = {};
try {
data = await res.json();
} catch (e) {
dlog("response.json() error =", e);
}
dlog("response.body =", data);
if (!res.ok) {
// รองรับข้อความ error จาก backend เช่น INVALID_CREDENTIALS
setErr(
const msg =
data?.error === "INVALID_CREDENTIALS"
? "ชื่อผู้ใช้หรือรหัสผ่านไม่ถูกต้อง"
: data?.error || "เข้าสู่ระบบไม่สำเร็จ"
);
: data?.error || `เข้าสู่ระบบไม่สำเร็จ (HTTP ${res.status})`;
dlog("login FAILED →", msg);
setErr(msg);
return;
}
if (!data?.token) {
dlog("login FAILED → data.token not found");
setErr("รูปแบบข้อมูลตอบกลับไม่ถูกต้อง (ไม่มี token)");
return;
}
@@ -76,6 +109,7 @@ function LoginForm() {
storage.setItem("dms.token", data.token);
storage.setItem("dms.refresh_token", data.refresh_token);
storage.setItem("dms.user", JSON.stringify(data.user || {}));
dlog("token stored in", remember ? "localStorage" : "sessionStorage");
// (ออปชัน) เผยแพร่ event ให้แท็บอื่นทราบ
try {
@@ -84,11 +118,14 @@ function LoginForm() {
);
} catch {}
dlog("navigating →", nextPath);
router.replace(nextPath);
} catch (e) {
dlog("exception =", e);
setErr("เชื่อมต่อเซิร์ฟเวอร์ไม่ได้ กรุณาลองใหม่");
} finally {
setSubmitting(false);
dlog("done");
}
}
@@ -183,6 +220,12 @@ function LoginForm() {
"เข้าสู่ระบบ"
)}
</Button>
{DEBUG ? (
<p className="mt-2 text-xs text-slate-500">
DEBUG: NEXT_PUBLIC_API_BASE = <code>{API_BASE || "(empty)"}</code>
</p>
) : null}
</form>
</CardContent>
@@ -251,4 +294,4 @@ function Spinner() {
/>
</svg>
);
}
}

View File

@@ -19,6 +19,7 @@
"class-variance-authority": "^0.7.1",
"clsx": "^2.1.1",
"framer-motion": "^11.2.10",
"jsonwebtoken": "^9.0.2",
"lucide-react": "^0.451.0",
"next": "15.0.3",
"postcss": "8.4.47",
@@ -2742,6 +2743,12 @@
"node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7"
}
},
"node_modules/buffer-equal-constant-time": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/buffer-equal-constant-time/-/buffer-equal-constant-time-1.0.1.tgz",
"integrity": "sha512-zRpUiDwd/xk6ADqPMATG8vc9VPrkck7T07OIx0gnjmJAnHnTVXNQG3vfvWNuiZIkwu9KrKdA1iJKfsfTVxE6NA==",
"license": "BSD-3-Clause"
},
"node_modules/busboy": {
"version": "1.6.0",
"resolved": "https://registry.npmjs.org/busboy/-/busboy-1.6.0.tgz",
@@ -3191,6 +3198,15 @@
"integrity": "sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==",
"license": "MIT"
},
"node_modules/ecdsa-sig-formatter": {
"version": "1.0.11",
"resolved": "https://registry.npmjs.org/ecdsa-sig-formatter/-/ecdsa-sig-formatter-1.0.11.tgz",
"integrity": "sha512-nagl3RYrbNv6kQkeJIpt6NJZy8twLB/2vtz6yN9Z4vRKHN4/QZJIEbqohALSgwKdnksuY3k5Addp5lg8sVoVcQ==",
"license": "Apache-2.0",
"dependencies": {
"safe-buffer": "^5.0.1"
}
},
"node_modules/electron-to-chromium": {
"version": "1.5.227",
"resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.227.tgz",
@@ -4951,6 +4967,28 @@
"json5": "lib/cli.js"
}
},
"node_modules/jsonwebtoken": {
"version": "9.0.2",
"resolved": "https://registry.npmjs.org/jsonwebtoken/-/jsonwebtoken-9.0.2.tgz",
"integrity": "sha512-PRp66vJ865SSqOlgqS8hujT5U4AOgMfhrwYIuIhfKaoSCZcirrmASQr8CX7cUg+RMih+hgznrjp99o+W4pJLHQ==",
"license": "MIT",
"dependencies": {
"jws": "^3.2.2",
"lodash.includes": "^4.3.0",
"lodash.isboolean": "^3.0.3",
"lodash.isinteger": "^4.0.4",
"lodash.isnumber": "^3.0.3",
"lodash.isplainobject": "^4.0.6",
"lodash.isstring": "^4.0.1",
"lodash.once": "^4.0.0",
"ms": "^2.1.1",
"semver": "^7.5.4"
},
"engines": {
"node": ">=12",
"npm": ">=6"
}
},
"node_modules/jsx-ast-utils": {
"version": "3.3.5",
"resolved": "https://registry.npmjs.org/jsx-ast-utils/-/jsx-ast-utils-3.3.5.tgz",
@@ -4967,6 +5005,27 @@
"node": ">=4.0"
}
},
"node_modules/jwa": {
"version": "1.4.2",
"resolved": "https://registry.npmjs.org/jwa/-/jwa-1.4.2.tgz",
"integrity": "sha512-eeH5JO+21J78qMvTIDdBXidBd6nG2kZjg5Ohz/1fpa28Z4CcsWUzJ1ZZyFq/3z3N17aZy+ZuBoHljASbL1WfOw==",
"license": "MIT",
"dependencies": {
"buffer-equal-constant-time": "^1.0.1",
"ecdsa-sig-formatter": "1.0.11",
"safe-buffer": "^5.0.1"
}
},
"node_modules/jws": {
"version": "3.2.2",
"resolved": "https://registry.npmjs.org/jws/-/jws-3.2.2.tgz",
"integrity": "sha512-YHlZCB6lMTllWDtSPHz/ZXTsi8S00usEV6v1tjq8tOUZzw7DpSDWVXjXDre6ed1w/pd495ODpHZYSdkRTsa0HA==",
"license": "MIT",
"dependencies": {
"jwa": "^1.4.1",
"safe-buffer": "^5.0.1"
}
},
"node_modules/keyv": {
"version": "4.5.4",
"resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.4.tgz",
@@ -5042,6 +5101,42 @@
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/lodash.includes": {
"version": "4.3.0",
"resolved": "https://registry.npmjs.org/lodash.includes/-/lodash.includes-4.3.0.tgz",
"integrity": "sha512-W3Bx6mdkRTGtlJISOvVD/lbqjTlPPUDTMnlXZFnVwi9NKJ6tiAk6LVdlhZMm17VZisqhKcgzpO5Wz91PCt5b0w==",
"license": "MIT"
},
"node_modules/lodash.isboolean": {
"version": "3.0.3",
"resolved": "https://registry.npmjs.org/lodash.isboolean/-/lodash.isboolean-3.0.3.tgz",
"integrity": "sha512-Bz5mupy2SVbPHURB98VAcw+aHh4vRV5IPNhILUCsOzRmsTmSQ17jIuqopAentWoehktxGd9e/hbIXq980/1QJg==",
"license": "MIT"
},
"node_modules/lodash.isinteger": {
"version": "4.0.4",
"resolved": "https://registry.npmjs.org/lodash.isinteger/-/lodash.isinteger-4.0.4.tgz",
"integrity": "sha512-DBwtEWN2caHQ9/imiNeEA5ys1JoRtRfY3d7V9wkqtbycnAmTvRRmbHKDV4a0EYc678/dia0jrte4tjYwVBaZUA==",
"license": "MIT"
},
"node_modules/lodash.isnumber": {
"version": "3.0.3",
"resolved": "https://registry.npmjs.org/lodash.isnumber/-/lodash.isnumber-3.0.3.tgz",
"integrity": "sha512-QYqzpfwO3/CWf3XP+Z+tkQsfaLL/EnUlXWVkIk5FUPc4sBdTehEqZONuyRt2P67PXAk+NXmTBcc97zw9t1FQrw==",
"license": "MIT"
},
"node_modules/lodash.isplainobject": {
"version": "4.0.6",
"resolved": "https://registry.npmjs.org/lodash.isplainobject/-/lodash.isplainobject-4.0.6.tgz",
"integrity": "sha512-oSXzaWypCMHkPC3NvBEaPHf0KsA5mvPrOPgQWDsbg8n7orZ290M0BmC/jgRZ4vcJ6DTAhjrsSYgdsW/F+MFOBA==",
"license": "MIT"
},
"node_modules/lodash.isstring": {
"version": "4.0.1",
"resolved": "https://registry.npmjs.org/lodash.isstring/-/lodash.isstring-4.0.1.tgz",
"integrity": "sha512-0wJxfxH1wgO3GrbuP+dTTk7op+6L41QCXbGINEmD+ny/G/eCqGzxyCsh7159S+mgDDcoarnBw6PC1PS5+wUGgw==",
"license": "MIT"
},
"node_modules/lodash.merge": {
"version": "4.6.2",
"resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz",
@@ -5049,6 +5144,12 @@
"dev": true,
"license": "MIT"
},
"node_modules/lodash.once": {
"version": "4.1.1",
"resolved": "https://registry.npmjs.org/lodash.once/-/lodash.once-4.1.1.tgz",
"integrity": "sha512-Sb487aTOCr9drQVL8pIxOzVhafOjZN9UU54hiN8PU3uAiSV7lx1yYNpbNmex2PK6dSJoNTSJUUswT651yww3Mg==",
"license": "MIT"
},
"node_modules/loose-envify": {
"version": "1.4.0",
"resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz",
@@ -5159,7 +5260,6 @@
"version": "2.1.3",
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
"integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==",
"dev": true,
"license": "MIT"
},
"node_modules/mz": {
@@ -6066,6 +6166,26 @@
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/safe-buffer": {
"version": "5.2.1",
"resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz",
"integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==",
"funding": [
{
"type": "github",
"url": "https://github.com/sponsors/feross"
},
{
"type": "patreon",
"url": "https://www.patreon.com/feross"
},
{
"type": "consulting",
"url": "https://feross.org/support"
}
],
"license": "MIT"
},
"node_modules/safe-push-apply": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/safe-push-apply/-/safe-push-apply-1.0.0.tgz",
@@ -6114,7 +6234,6 @@
"version": "7.7.2",
"resolved": "https://registry.npmjs.org/semver/-/semver-7.7.2.tgz",
"integrity": "sha512-RF0Fw+rO5AMf9MAyaRXI4AV0Ulj5lMHqVxxdSgiVbixSCXoEmmX/jk0CuJw4+3SqroYO9VoUh+HcuJivvtJemA==",
"devOptional": true,
"license": "ISC",
"bin": {
"semver": "bin/semver.js"

View File

@@ -21,6 +21,7 @@
"class-variance-authority": "^0.7.1",
"clsx": "^2.1.1",
"framer-motion": "^11.2.10",
"jsonwebtoken": "^9.0.2",
"lucide-react": "^0.451.0",
"next": "15.0.3",
"postcss": "8.4.47",

View File

@@ -0,0 +1,6 @@
[mariadb-client]
port=3306
socket=/run/mysqld/mysqld.sock
user=healthcheck
password=jhc12PlUnlAebL|p7OxiYwMyuC<<E0D+

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

View File

@@ -0,0 +1,8 @@
TYPE=TRIGGERS
triggers='CREATE DEFINER=`root`@`localhost` TRIGGER trg_cir_users_bi_flags\nBEFORE INSERT ON cir_users\nFOR EACH ROW\nBEGIN\n SET NEW.is_main = IFNULL(NEW.is_main, 0);\n SET NEW.is_action = IFNULL(NEW.is_action, 0);\nEND' 'CREATE DEFINER=`root`@`localhost` TRIGGER trg_cir_users_bu_flags\nBEFORE UPDATE ON cir_users\nFOR EACH ROW\nBEGIN\n SET NEW.is_main = IFNULL(NEW.is_main, 0);\n SET NEW.is_action = IFNULL(NEW.is_action, 0);\nEND'
sql_modes=1411383296 1411383296
definers='root@localhost' 'root@localhost'
client_cs_names='utf8mb4' 'utf8mb4'
connection_cl_names='utf8mb4_general_ci' 'utf8mb4_general_ci'
db_cl_names='utf8mb4_general_ci' 'utf8mb4_general_ci'
created=1759220379993565 1759220380105898

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

View File

@@ -0,0 +1,8 @@
TYPE=TRIGGERS
triggers='CREATE DEFINER=`root`@`localhost` TRIGGER trg_cdwg_bi_normalize\nBEFORE INSERT ON contract_dwg\nFOR EACH ROW\nBEGIN\n SET NEW.condwg_no = TRIM(NEW.condwg_no);\n SET NEW.title = NULLIF(TRIM(NEW.title), \'\');\n\n IF NEW.project_id IS NULL OR NEW.sub_cat_id IS NULL THEN\n SIGNAL SQLSTATE \'45000\' SET MESSAGE_TEXT=\'project_id and sub_cat_id are required for contract_dwg\';\n END IF;\n IF NEW.condwg_no IS NULL OR NEW.condwg_no=\'\' THEN\n SIGNAL SQLSTATE \'45000\' SET MESSAGE_TEXT=\'condwg_no is required\';\n END IF;\nEND' 'CREATE DEFINER=`root`@`localhost` TRIGGER trg_dwg_before_insert\nBEFORE INSERT ON contract_dwg\nFOR EACH ROW\nBEGIN\n DECLARE v_cnt INT DEFAULT 0;\n\n \n SET NEW.condwg_no = TRIM(NEW.condwg_no);\n IF NEW.title IS NOT NULL THEN\n SET NEW.title = TRIM(NEW.title);\n END IF;\n IF NEW.remark IS NOT NULL THEN\n SET NEW.remark = TRIM(NEW.remark);\n END IF;\n\n \n SELECT COUNT(*) INTO v_cnt\n FROM contract_dwg_sub_cat sc\n WHERE sc.sub_cat_id = NEW.sub_cat_id\n AND sc.project_id = NEW.project_id;\n IF v_cnt = 0 THEN\n SIGNAL SQLSTATE \'45000\'\n SET MESSAGE_TEXT = \'Invalid sub_cat_id: must belong to same project\';\n END IF;\n\n \n SELECT COUNT(*) INTO v_cnt\n FROM contract_dwg_volume v\n WHERE v.volume_id = NEW.volume_id\n AND v.project_id = NEW.project_id;\n IF v_cnt = 0 THEN\n SIGNAL SQLSTATE \'45000\'\n SET MESSAGE_TEXT = \'Invalid volume_id: must belong to same project\';\n END IF;\n\n SET NEW.created_at = CURRENT_TIMESTAMP;\nEND' 'CREATE DEFINER=`root`@`localhost` TRIGGER trg_cdwg_bu_normalize\nBEFORE UPDATE ON contract_dwg\nFOR EACH ROW\nBEGIN\n SET NEW.condwg_no = TRIM(NEW.condwg_no);\n SET NEW.title = NULLIF(TRIM(NEW.title), \'\');\nEND' 'CREATE DEFINER=`root`@`localhost` TRIGGER trg_dwg_before_update\nBEFORE UPDATE ON contract_dwg\nFOR EACH ROW\nBEGIN\n DECLARE v_cnt INT DEFAULT 0;\n\n \n IF NEW.condwg_no IS NOT NULL THEN\n SET NEW.condwg_no = TRIM(NEW.condwg_no);\n END IF;\n IF NEW.title IS NOT NULL THEN\n SET NEW.title = TRIM(NEW.title);\n END IF;\n IF NEW.remark IS NOT NULL THEN\n SET NEW.remark = TRIM(NEW.remark);\n END IF;\n\n \n IF NEW.project_id <> OLD.project_id THEN\n SIGNAL SQLSTATE \'45000\'\n SET MESSAGE_TEXT = \'Changing drawing project_id is not allowed\';\n END IF;\n\n \n SELECT COUNT(*) INTO v_cnt\n FROM contract_dwg_sub_cat sc\n WHERE sc.sub_cat_id = NEW.sub_cat_id\n AND sc.project_id = NEW.project_id;\n IF v_cnt = 0 THEN\n SIGNAL SQLSTATE \'45000\'\n SET MESSAGE_TEXT = \'Invalid sub_cat_id after update: must be same project\';\n END IF;\n\n \n SELECT COUNT(*) INTO v_cnt\n FROM contract_dwg_volume v\n WHERE v.volume_id = NEW.volume_id\n AND v.project_id = NEW.project_id;\n IF v_cnt = 0 THEN\n SIGNAL SQLSTATE \'45000\'\n SET MESSAGE_TEXT = \'Invalid volume_id after update: must be same project\';\n END IF;\n\n SET NEW.updated_at = CURRENT_TIMESTAMP;\nEND' 'CREATE DEFINER=`root`@`localhost` TRIGGER trg_dwg_before_delete\nBEFORE DELETE ON contract_dwg\nFOR EACH ROW\nBEGIN\n \n \nEND'
sql_modes=1411383296 1411383296 1411383296 1411383296 1411383296
definers='root@localhost' 'root@localhost' 'root@localhost' 'root@localhost' 'root@localhost'
client_cs_names='utf8mb4' 'utf8mb4' 'utf8mb4' 'utf8mb4' 'utf8mb4'
connection_cl_names='utf8mb4_general_ci' 'utf8mb4_general_ci' 'utf8mb4_general_ci' 'utf8mb4_general_ci' 'utf8mb4_general_ci'
db_cl_names='utf8mb4_general_ci' 'utf8mb4_general_ci' 'utf8mb4_general_ci' 'utf8mb4_general_ci' 'utf8mb4_general_ci'
created=1759220379595746 1759220382757985 1759220379725392 1759220383012145 1759220383498581

Binary file not shown.

Binary file not shown.

View File

@@ -0,0 +1,8 @@
TYPE=TRIGGERS
triggers='CREATE DEFINER=`root`@`localhost` TRIGGER trg_cdwg_cat_bi_norm\nBEFORE INSERT ON contract_dwg_cat\nFOR EACH ROW\nBEGIN\n SET NEW.cat_name = TRIM(NEW.cat_name);\n IF NEW.cat_name IS NULL OR NEW.cat_name=\'\' THEN\n SIGNAL SQLSTATE \'45000\' SET MESSAGE_TEXT=\'cat_name is required\';\n END IF;\nEND' 'CREATE DEFINER=`root`@`localhost` TRIGGER trg_cat_before_insert\nBEFORE INSERT ON contract_dwg_cat\nFOR EACH ROW\nBEGIN\n \n SET NEW.cat_name = TRIM(NEW.cat_name);\n IF NEW.cat_code IS NOT NULL THEN\n SET NEW.cat_code = TRIM(NEW.cat_code);\n END IF;\n\n \n SET NEW.created_at = CURRENT_TIMESTAMP;\nEND' 'CREATE DEFINER=`root`@`localhost` TRIGGER trg_cat_before_update\nBEFORE UPDATE ON contract_dwg_cat\nFOR EACH ROW\nBEGIN\n DECLARE v_map_cnt INT DEFAULT 0;\n \n IF NEW.cat_name IS NOT NULL THEN\n SET NEW.cat_name = TRIM(NEW.cat_name);\n END IF;\n IF NEW.cat_code IS NOT NULL THEN\n SET NEW.cat_code = TRIM(NEW.cat_code);\n END IF;\n\n \n IF NEW.project_id <> OLD.project_id THEN\n SELECT COUNT(*) INTO v_map_cnt\n FROM contract_dwg_subcat_cat_map m\n WHERE m.project_id = OLD.project_id\n AND m.cat_id = OLD.cat_id;\n IF v_map_cnt > 0 THEN\n SIGNAL SQLSTATE \'45000\'\n SET MESSAGE_TEXT = \'Cannot change category project_id: mappings exist in subcat↔cat map\';\n END IF;\n END IF;\n\n \n SET NEW.updated_at = CURRENT_TIMESTAMP;\nEND' 'CREATE DEFINER=`root`@`localhost` TRIGGER trg_cat_before_delete\nBEFORE DELETE ON contract_dwg_cat\nFOR EACH ROW\nBEGIN\n \n DECLARE v_map_cnt INT DEFAULT 0;\n SELECT COUNT(*) INTO v_map_cnt\n FROM contract_dwg_subcat_cat_map m\n WHERE m.project_id = OLD.project_id\n AND m.cat_id = OLD.cat_id;\n\n IF v_map_cnt > 0 THEN\n SIGNAL SQLSTATE \'45000\'\n SET MESSAGE_TEXT = \'Cannot delete category: mapped by sub-categories in this project\';\n END IF;\nEND'
sql_modes=1411383296 1411383296 1411383296 1411383296
definers='root@localhost' 'root@localhost' 'root@localhost' 'root@localhost'
client_cs_names='utf8mb4' 'utf8mb4' 'utf8mb4' 'utf8mb4'
connection_cl_names='utf8mb4_general_ci' 'utf8mb4_general_ci' 'utf8mb4_general_ci' 'utf8mb4_general_ci'
db_cl_names='utf8mb4_general_ci' 'utf8mb4_general_ci' 'utf8mb4_general_ci' 'utf8mb4_general_ci'
created=1759220379895653 1759220380272498 1759220380492708 1759220380712245

Binary file not shown.

Binary file not shown.

View File

@@ -0,0 +1,8 @@
TYPE=TRIGGERS
triggers='CREATE DEFINER=`root`@`localhost` TRIGGER trg_subcat_before_insert\nBEFORE INSERT ON contract_dwg_sub_cat\nFOR EACH ROW\nBEGIN\n SET NEW.sub_cat_name = TRIM(NEW.sub_cat_name);\n IF NEW.sub_cat_code IS NOT NULL THEN\n SET NEW.sub_cat_code = TRIM(NEW.sub_cat_code);\n END IF;\n\n SET NEW.created_at = CURRENT_TIMESTAMP;\nEND' 'CREATE DEFINER=`root`@`localhost` TRIGGER trg_subcat_before_update\nBEFORE UPDATE ON contract_dwg_sub_cat\nFOR EACH ROW\nBEGIN\n DECLARE v_map_cnt INT DEFAULT 0;\n DECLARE v_dwg_cnt INT DEFAULT 0;\n IF NEW.sub_cat_name IS NOT NULL THEN\n SET NEW.sub_cat_name = TRIM(NEW.sub_cat_name);\n END IF;\n IF NEW.sub_cat_code IS NOT NULL THEN\n SET NEW.sub_cat_code = TRIM(NEW.sub_cat_code);\n END IF;\n\n \n IF NEW.project_id <> OLD.project_id THEN\n SELECT COUNT(*) INTO v_map_cnt\n FROM contract_dwg_subcat_cat_map m\n WHERE m.project_id = OLD.project_id\n AND m.sub_cat_id = OLD.sub_cat_id;\n\n SELECT COUNT(*) INTO v_dwg_cnt\n FROM contract_dwg d\n WHERE d.project_id = OLD.project_id\n AND d.sub_cat_id = OLD.sub_cat_id;\n\n IF v_map_cnt > 0 OR v_dwg_cnt > 0 THEN\n SIGNAL SQLSTATE \'45000\'\n SET MESSAGE_TEXT = \'Cannot change sub-category project_id: drawings or mappings exist\';\n END IF;\n END IF;\n\n SET NEW.updated_at = CURRENT_TIMESTAMP;\nEND' 'CREATE DEFINER=`root`@`localhost` TRIGGER trg_subcat_before_delete\nBEFORE DELETE ON contract_dwg_sub_cat\nFOR EACH ROW\nBEGIN\n DECLARE v_dwg_cnt INT DEFAULT 0;\n DECLARE v_map_cnt INT DEFAULT 0;\n\n SELECT COUNT(*) INTO v_dwg_cnt\n FROM contract_dwg d\n WHERE d.project_id = OLD.project_id\n AND d.sub_cat_id = OLD.sub_cat_id;\n\n SELECT COUNT(*) INTO v_map_cnt\n FROM contract_dwg_subcat_cat_map m\n WHERE m.project_id = OLD.project_id\n AND m.sub_cat_id = OLD.sub_cat_id;\n\n IF v_dwg_cnt > 0 THEN\n SIGNAL SQLSTATE \'45000\'\n SET MESSAGE_TEXT = \'Cannot delete sub-category: drawings exist in this sub-category\';\n END IF;\n\n IF v_map_cnt > 0 THEN\n SIGNAL SQLSTATE \'45000\'\n SET MESSAGE_TEXT = \'Cannot delete sub-category: it is mapped to categories\';\n END IF;\nEND'
sql_modes=1411383296 1411383296 1411383296
definers='root@localhost' 'root@localhost' 'root@localhost'
client_cs_names='utf8mb4' 'utf8mb4' 'utf8mb4'
connection_cl_names='utf8mb4_general_ci' 'utf8mb4_general_ci' 'utf8mb4_general_ci'
db_cl_names='utf8mb4_general_ci' 'utf8mb4_general_ci' 'utf8mb4_general_ci'
created=1759220381156227 1759220381265061 1759220381497493

Binary file not shown.

Binary file not shown.

View File

@@ -0,0 +1,8 @@
TYPE=TRIGGERS
triggers='CREATE DEFINER=`root`@`localhost` TRIGGER trg_map_before_insert\nBEFORE INSERT ON contract_dwg_subcat_cat_map\nFOR EACH ROW\nBEGIN\n DECLARE v_cnt INT DEFAULT 0;\n\n \n SELECT COUNT(*) INTO v_cnt\n FROM contract_dwg_sub_cat sc\n WHERE sc.sub_cat_id = NEW.sub_cat_id\n AND sc.project_id = NEW.project_id;\n IF v_cnt = 0 THEN\n SIGNAL SQLSTATE \'45000\'\n SET MESSAGE_TEXT = \'Invalid sub_cat_id: must belong to the same project (map)\';\n END IF;\n\n \n SELECT COUNT(*) INTO v_cnt\n FROM contract_dwg_cat c\n WHERE c.cat_id = NEW.cat_id\n AND c.project_id = NEW.project_id;\n IF v_cnt = 0 THEN\n SIGNAL SQLSTATE \'45000\'\n SET MESSAGE_TEXT = \'Invalid cat_id: must belong to the same project (map)\';\n END IF;\nEND' 'CREATE DEFINER=`root`@`localhost` TRIGGER trg_map_before_update\nBEFORE UPDATE ON contract_dwg_subcat_cat_map\nFOR EACH ROW\nBEGIN\n DECLARE v_cnt INT DEFAULT 0;\n\n \n IF NEW.project_id <> OLD.project_id THEN\n SIGNAL SQLSTATE \'45000\'\n SET MESSAGE_TEXT = \'Changing project_id on mapping is not allowed\';\n END IF;\n\n \n SELECT COUNT(*) INTO v_cnt\n FROM contract_dwg_sub_cat sc\n WHERE sc.sub_cat_id = NEW.sub_cat_id\n AND sc.project_id = NEW.project_id;\n IF v_cnt = 0 THEN\n SIGNAL SQLSTATE \'45000\'\n SET MESSAGE_TEXT = \'Invalid sub_cat_id after update: must be same project\';\n END IF;\n\n SELECT COUNT(*) INTO v_cnt\n FROM contract_dwg_cat c\n WHERE c.cat_id = NEW.cat_id\n AND c.project_id = NEW.project_id;\n IF v_cnt = 0 THEN\n SIGNAL SQLSTATE \'45000\'\n SET MESSAGE_TEXT = \'Invalid cat_id after update: must be same project\';\n END IF;\nEND' 'CREATE DEFINER=`root`@`localhost` TRIGGER trg_map_before_delete\nBEFORE DELETE ON contract_dwg_subcat_cat_map\nFOR EACH ROW\nBEGIN\n \n \nEND'
sql_modes=1411383296 1411383296 1411383296
definers='root@localhost' 'root@localhost' 'root@localhost'
client_cs_names='utf8mb4' 'utf8mb4' 'utf8mb4'
connection_cl_names='utf8mb4_general_ci' 'utf8mb4_general_ci' 'utf8mb4_general_ci'
db_cl_names='utf8mb4_general_ci' 'utf8mb4_general_ci' 'utf8mb4_general_ci'
created=1759220381718496 1759220381824403 1759220382028445

Binary file not shown.

Binary file not shown.

View File

@@ -0,0 +1,8 @@
TYPE=TRIGGERS
triggers='CREATE DEFINER=`root`@`localhost` TRIGGER trg_volume_before_insert\nBEFORE INSERT ON contract_dwg_volume\nFOR EACH ROW\nBEGIN\n SET NEW.volume_code = TRIM(NEW.volume_code);\n IF NEW.volume_name IS NOT NULL THEN\n SET NEW.volume_name = TRIM(NEW.volume_name);\n END IF;\n\n SET NEW.created_at = CURRENT_TIMESTAMP;\nEND' 'CREATE DEFINER=`root`@`localhost` TRIGGER trg_volume_before_update\nBEFORE UPDATE ON contract_dwg_volume\nFOR EACH ROW\nBEGIN\n DECLARE v_dwg_cnt INT DEFAULT 0;\n IF NEW.volume_code IS NOT NULL THEN\n SET NEW.volume_code = TRIM(NEW.volume_code);\n END IF;\n IF NEW.volume_name IS NOT NULL THEN\n SET NEW.volume_name = TRIM(NEW.volume_name);\n END IF;\n\n \n IF NEW.project_id <> OLD.project_id THEN \n SELECT COUNT(*) INTO v_dwg_cnt\n FROM contract_dwg d\n WHERE d.project_id = OLD.project_id\n AND d.volume_id = OLD.volume_id;\n\n IF v_dwg_cnt > 0 THEN\n SIGNAL SQLSTATE \'45000\'\n SET MESSAGE_TEXT = \'Cannot change volume project_id: drawings exist in this volume\';\n END IF;\n END IF;\n\n SET NEW.updated_at = CURRENT_TIMESTAMP;\nEND' 'CREATE DEFINER=`root`@`localhost` TRIGGER trg_volume_before_delete\nBEFORE DELETE ON contract_dwg_volume\nFOR EACH ROW\nBEGIN\n DECLARE v_dwg_cnt INT DEFAULT 0;\n SELECT COUNT(*) INTO v_dwg_cnt\n FROM contract_dwg d\n WHERE d.project_id = OLD.project_id\n AND d.volume_id = OLD.volume_id;\n\n IF v_dwg_cnt > 0 THEN\n SIGNAL SQLSTATE \'45000\'\n SET MESSAGE_TEXT = \'Cannot delete volume: drawings exist in this volume\';\n END IF;\nEND'
sql_modes=1411383296 1411383296 1411383296
definers='root@localhost' 'root@localhost' 'root@localhost'
client_cs_names='utf8mb4' 'utf8mb4' 'utf8mb4'
connection_cl_names='utf8mb4_general_ci' 'utf8mb4_general_ci' 'utf8mb4_general_ci'
db_cl_names='utf8mb4_general_ci' 'utf8mb4_general_ci' 'utf8mb4_general_ci'
created=1759220382195544 1759220382282307 1759220382459142

Binary file not shown.

Binary file not shown.

View File

@@ -0,0 +1,8 @@
TYPE=TRIGGERS
triggers='CREATE DEFINER=`root`@`localhost` TRIGGER trg_corcc_bi_guard_party\nBEFORE INSERT ON correspondence_cc_recipients\nFOR EACH ROW\nBEGIN\n DECLARE v_project_id INT;\n SELECT project_id INTO v_project_id FROM correspondences WHERE corr_id = NEW.corr_id;\n IF v_project_id IS NULL THEN\n SIGNAL SQLSTATE \'45000\' SET MESSAGE_TEXT=\'Correspondence not found\';\n END IF;\n\n IF NOT EXISTS (\n SELECT 1 FROM project_parties\n WHERE project_id = v_project_id AND org_id = NEW.org_id AND deleted_at IS NULL\n ) THEN\n SIGNAL SQLSTATE \'45000\' SET MESSAGE_TEXT=\'CC org is not in project parties\';\n END IF;\nEND' 'CREATE DEFINER=`root`@`localhost` TRIGGER trg_corcc_bu_guard_party\nBEFORE UPDATE ON correspondence_cc_recipients\nFOR EACH ROW\nBEGIN\n DECLARE v_project_id INT;\n SELECT project_id INTO v_project_id FROM correspondences WHERE corr_id = NEW.corr_id;\n IF v_project_id IS NULL THEN\n SIGNAL SQLSTATE \'45000\' SET MESSAGE_TEXT=\'Correspondence not found\';\n END IF;\n\n IF NOT EXISTS (\n SELECT 1 FROM project_parties\n WHERE project_id = v_project_id AND org_id = NEW.org_id AND deleted_at IS NULL\n ) THEN\n SIGNAL SQLSTATE \'45000\' SET MESSAGE_TEXT=\'CC org is not in project parties\';\n END IF;\nEND'
sql_modes=1411383296 1411383296
definers='root@localhost' 'root@localhost'
client_cs_names='utf8mb4' 'utf8mb4'
connection_cl_names='utf8mb4_general_ci' 'utf8mb4_general_ci'
db_cl_names='utf8mb4_general_ci' 'utf8mb4_general_ci'
created=1759220376112820 1759220376245144

Binary file not shown.

Binary file not shown.

View File

@@ -0,0 +1,8 @@
TYPE=TRIGGERS
triggers='CREATE DEFINER=`root`@`localhost` TRIGGER trg_correfl_bi_guard_self\nBEFORE INSERT ON correspondence_references\nFOR EACH ROW\nBEGIN\n IF NEW.src_corr_id = NEW.tgt_corr_id THEN\n SIGNAL SQLSTATE \'45000\' SET MESSAGE_TEXT=\'Cannot reference a correspondence to itself\';\n END IF;\nEND' 'CREATE DEFINER=`root`@`localhost` TRIGGER trg_correfl_bu_guard_self\nBEFORE UPDATE ON correspondence_references\nFOR EACH ROW\nBEGIN\n IF NEW.src_corr_id = NEW.tgt_corr_id THEN\n SIGNAL SQLSTATE \'45000\' SET MESSAGE_TEXT=\'Cannot reference a correspondence to itself\';\n END IF;\nEND'
sql_modes=1411383296 1411383296
definers='root@localhost' 'root@localhost'
client_cs_names='utf8mb4' 'utf8mb4'
connection_cl_names='utf8mb4_general_ci' 'utf8mb4_general_ci'
db_cl_names='utf8mb4_general_ci' 'utf8mb4_general_ci'
created=1759220376654188 1759220376770663

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

View File

@@ -0,0 +1,8 @@
TYPE=TRIGGERS
triggers='CREATE DEFINER=`root`@`localhost` TRIGGER trg_cor_bi_normalize\nBEFORE INSERT ON correspondences\nFOR EACH ROW\nBEGIN\n SET NEW.correspondence_number = TRIM(NEW.correspondence_number);\n SET NEW.title = TRIM(NEW.title);\n SET NEW.keywords = NULLIF(TRIM(NEW.keywords), \'\');\n SET NEW.revision = NULLIF(TRIM(NEW.revision), \'\');\n SET NEW.is_internal_communication = IFNULL(NEW.is_internal_communication, 0);\n\n IF NEW.project_id IS NULL OR NEW.correspondence_type_id IS NULL OR NEW.correspondence_status_id IS NULL THEN\n SIGNAL SQLSTATE \'45000\' SET MESSAGE_TEXT=\'project_id/type_id/status_id is required\';\n END IF;\n IF NEW.correspondence_number IS NULL OR NEW.correspondence_number=\'\' THEN\n SIGNAL SQLSTATE \'45000\' SET MESSAGE_TEXT=\'correspondence_number is required\';\n END IF;\n\n \nEND' 'CREATE DEFINER=`root`@`localhost` TRIGGER trg_cor_ai_set_root\nAFTER INSERT ON correspondences\nFOR EACH ROW\nBEGIN\n IF NEW.root_id IS NULL THEN\n UPDATE correspondences SET root_id = NEW.corr_id WHERE corr_id = NEW.corr_id;\n END IF;\nEND' 'CREATE DEFINER=`root`@`localhost` TRIGGER trg_cor_bu_normalize\nBEFORE UPDATE ON correspondences\nFOR EACH ROW\nBEGIN\n SET NEW.correspondence_number = TRIM(NEW.correspondence_number);\n SET NEW.title = TRIM(NEW.title);\n SET NEW.keywords = NULLIF(TRIM(NEW.keywords), \'\');\n SET NEW.revision = NULLIF(TRIM(NEW.revision), \'\');\n SET NEW.is_internal_communication = IFNULL(NEW.is_internal_communication, 0);\n\n \n IF NEW.root_id <> OLD.root_id THEN\n SIGNAL SQLSTATE \'45000\' SET MESSAGE_TEXT=\'root_id is immutable\';\n END IF;\nEND' 'CREATE DEFINER=`root`@`localhost` TRIGGER trg_cor_bd_no_delete_root_with_children\nBEFORE DELETE ON correspondences\nFOR EACH ROW\nBEGIN\n IF EXISTS (SELECT 1 FROM correspondences WHERE root_id = OLD.corr_id AND corr_id <> OLD.corr_id) THEN\n SIGNAL SQLSTATE \'45000\' SET MESSAGE_TEXT=\'Cannot delete root correspondence with existing revisions\';\n END IF;\nEND'
sql_modes=1411383296 1411383296 1411383296 1411383296
definers='root@localhost' 'root@localhost' 'root@localhost' 'root@localhost'
client_cs_names='utf8mb4' 'utf8mb4' 'utf8mb4' 'utf8mb4'
connection_cl_names='utf8mb4_general_ci' 'utf8mb4_general_ci' 'utf8mb4_general_ci' 'utf8mb4_general_ci'
db_cl_names='utf8mb4_general_ci' 'utf8mb4_general_ci' 'utf8mb4_general_ci' 'utf8mb4_general_ci'
created=1759220375434299 1759220375781124 1759220375559604 1759220375957839

Binary file not shown.

Binary file not shown.

2
mariadb/data/dms/db.opt Normal file
View File

@@ -0,0 +1,2 @@
default-character-set=utf8mb4
default-collation=utf8mb4_general_ci

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

View File

@@ -0,0 +1,8 @@
TYPE=TRIGGERS
triggers='CREATE DEFINER=`root`@`localhost` TRIGGER trg_scd_bi_guard\nBEFORE INSERT ON dwgdoc_contract_dwg\nFOR EACH ROW\nBEGIN\n DECLARE v_doc_proj INT;\n DECLARE v_doc_type_code VARCHAR(20);\n DECLARE v_dwg_proj INT;\n\n \n SELECT t.project_id, dt.code\n INTO v_doc_proj, v_doc_type_code\n FROM technicaldocs t\n JOIN document_types dt ON dt.document_types_id = t.document_type_id\n WHERE t.id = NEW.technical_doc_id;\n\n IF v_doc_proj IS NULL THEN\n SIGNAL SQLSTATE \'45000\' SET MESSAGE_TEXT=\'technical_doc not found\';\n END IF;\n IF v_doc_type_code <> \'DWG\' THEN\n SIGNAL SQLSTATE \'45000\' SET MESSAGE_TEXT=\'technical_doc must be type DWG\';\n END IF;\n\n \n SELECT project_id INTO v_dwg_proj FROM contract_dwg WHERE condwg_id = NEW.condwg_id;\n IF v_dwg_proj IS NULL THEN\n SIGNAL SQLSTATE \'45000\' SET MESSAGE_TEXT=\'contract_dwg not found\';\n END IF;\n IF v_dwg_proj <> v_doc_proj THEN\n SIGNAL SQLSTATE \'45000\' SET MESSAGE_TEXT=\'DWG doc and contract_dwg must be in the same project\';\n END IF;\nEND' 'CREATE DEFINER=`root`@`localhost` TRIGGER trg_scd_bu_block_update\nBEFORE UPDATE ON dwgdoc_contract_dwg\nFOR EACH ROW\nBEGIN\n SIGNAL SQLSTATE \'45000\' SET MESSAGE_TEXT=\'Update not allowed; delete then insert\';\nEND'
sql_modes=1411383296 1411383296
definers='root@localhost' 'root@localhost'
client_cs_names='utf8mb4' 'utf8mb4'
connection_cl_names='utf8mb4_general_ci' 'utf8mb4_general_ci'
db_cl_names='utf8mb4_general_ci' 'utf8mb4_general_ci'
created=1759220383708744 1759220383819565

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

View File

@@ -0,0 +1,8 @@
TYPE=TRIGGERS
triggers='CREATE DEFINER=`root`@`localhost` TRIGGER trg_gdr_bi_match_org_primary\nBEFORE INSERT ON global_default_roles\nFOR EACH ROW\nBEGIN\n DECLARE v_primary VARCHAR(20);\n SELECT primary_role INTO v_primary FROM organizations WHERE org_id = NEW.org_id;\n IF v_primary IS NULL THEN\n SIGNAL SQLSTATE \'45000\' SET MESSAGE_TEXT=\'Organization not found for global_default_roles\';\n END IF;\n IF NEW.role <> v_primary THEN\n SIGNAL SQLSTATE \'45000\' SET MESSAGE_TEXT=\'global_default_roles.role must match organizations.primary_role\';\n END IF;\nEND' 'CREATE DEFINER=`root`@`localhost` TRIGGER trg_gdr_bu_match_org_primary\nBEFORE UPDATE ON global_default_roles\nFOR EACH ROW\nBEGIN\n DECLARE v_primary VARCHAR(20);\n SELECT primary_role INTO v_primary FROM organizations WHERE org_id = NEW.org_id;\n IF v_primary IS NULL THEN\n SIGNAL SQLSTATE \'45000\' SET MESSAGE_TEXT=\'Organization not found for global_default_roles\';\n END IF;\n IF NEW.role <> v_primary THEN\n SIGNAL SQLSTATE \'45000\' SET MESSAGE_TEXT=\'global_default_roles.role must match organizations.primary_role\';\n END IF;\nEND'
sql_modes=1411383296 1411383296
definers='root@localhost' 'root@localhost'
client_cs_names='utf8mb4' 'utf8mb4'
connection_cl_names='utf8mb4_general_ci' 'utf8mb4_general_ci'
db_cl_names='utf8mb4_general_ci' 'utf8mb4_general_ci'
created=1759220379175294 1759220379347439

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

View File

@@ -0,0 +1,8 @@
TYPE=TRIGGERS
triggers='CREATE DEFINER=`root`@`localhost` TRIGGER trg_pp_bi_guard\nBEFORE INSERT ON project_parties\nFOR EACH ROW\nBEGIN\n SET NEW.role = UPPER(NEW.role);\n\n IF NEW.role = \'CONTRACTOR\' AND\n EXISTS (SELECT 1 FROM project_parties\n WHERE project_id = NEW.project_id\n AND role = \'CONTRACTOR\'\n AND deleted_at IS NULL) THEN\n SIGNAL SQLSTATE \'45000\' SET MESSAGE_TEXT=\'This project already has a CONTRACTOR\';\n END IF;\nEND' 'CREATE DEFINER=`root`@`localhost` TRIGGER trg_pp_bu_guard\nBEFORE UPDATE ON project_parties\nFOR EACH ROW\nBEGIN\n SET NEW.role = UPPER(NEW.role);\n\n IF NEW.role = \'CONTRACTOR\' AND\n EXISTS (SELECT 1 FROM project_parties\n WHERE project_id = NEW.project_id\n AND role = \'CONTRACTOR\'\n AND deleted_at IS NULL\n AND NOT (org_id = OLD.org_id AND role = \'CONTRACTOR\')) THEN\n SIGNAL SQLSTATE \'45000\' SET MESSAGE_TEXT=\'This project already has a CONTRACTOR\';\n END IF;\nEND'
sql_modes=1411383296 1411383296
definers='root@localhost' 'root@localhost'
client_cs_names='utf8mb4' 'utf8mb4'
connection_cl_names='utf8mb4_general_ci' 'utf8mb4_general_ci'
db_cl_names='utf8mb4_general_ci' 'utf8mb4_general_ci'
created=1759220378876780 1759220379009579

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

View File

@@ -0,0 +1,8 @@
TYPE=TRIGGERS
triggers='CREATE DEFINER=`root`@`localhost` TRIGGER trg_rfa_items_bi_guard\nBEFORE INSERT ON rfa_items\nFOR EACH ROW\nBEGIN\n DECLARE v_type_code VARCHAR(20);\n DECLARE v_hdr_proj INT;\n DECLARE v_doc_proj INT;\n\n SELECT t.type_code, c.project_id\n INTO v_type_code, v_hdr_proj\n FROM correspondences c\n JOIN correspondence_types t ON t.type_id = c.correspondence_type_id\n WHERE c.corr_id = NEW.rfa_corr_id;\n\n IF v_type_code IS NULL THEN\n SIGNAL SQLSTATE \'45000\' SET MESSAGE_TEXT=\'RFA header not found\';\n END IF;\n IF v_type_code <> \'RFA\' THEN\n SIGNAL SQLSTATE \'45000\' SET MESSAGE_TEXT=\'rfa_corr_id must be a correspondence of type RFA\';\n END IF;\n\n SELECT project_id INTO v_doc_proj FROM technicaldocs WHERE id = NEW.technical_doc_id;\n IF v_doc_proj IS NULL THEN\n SIGNAL SQLSTATE \'45000\' SET MESSAGE_TEXT=\'technical document not found\';\n END IF;\n\n IF v_hdr_proj <> v_doc_proj THEN\n SIGNAL SQLSTATE \'45000\' SET MESSAGE_TEXT=\'RFA header and technicaldoc must be in the same project\';\n END IF;\nEND' 'CREATE DEFINER=`root`@`localhost` TRIGGER trg_rfa_items_bu_guard\nBEFORE UPDATE ON rfa_items\nFOR EACH ROW\nBEGIN\n SIGNAL SQLSTATE \'45000\' SET MESSAGE_TEXT=\'Update on rfa_items is not allowed; delete and re-insert\';\nEND'
sql_modes=1411383296 1411383296
definers='root@localhost' 'root@localhost'
client_cs_names='utf8mb4' 'utf8mb4'
connection_cl_names='utf8mb4_general_ci' 'utf8mb4_general_ci'
db_cl_names='utf8mb4_general_ci' 'utf8mb4_general_ci'
created=1759220377624801 1759220377737882

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

BIN
mariadb/data/dms/roles.frm Normal file

Binary file not shown.

BIN
mariadb/data/dms/roles.ibd Normal file

Binary file not shown.

View File

@@ -0,0 +1,8 @@
TYPE=TRIGGERS
triggers='CREATE DEFINER=`root`@`localhost` TRIGGER trg_tdoc_bi_normalize\nBEFORE INSERT ON technicaldocs\nFOR EACH ROW\nBEGIN\n DECLARE v_orig_proj INT;\n\n SET NEW.title = TRIM(NEW.title);\n SET NEW.revision = NULLIF(TRIM(NEW.revision), \'\');\n SET NEW.pdf_path = NULLIF(TRIM(NEW.pdf_path), \'\');\n\n IF NEW.project_id IS NULL OR NEW.document_type_id IS NULL OR NEW.status_code_id IS NULL THEN\n SIGNAL SQLSTATE \'45000\' SET MESSAGE_TEXT=\'project_id/document_type_id/status_code_id is required\';\n END IF;\n IF NEW.revision IS NULL OR NEW.revision=\'\' THEN\n SIGNAL SQLSTATE \'45000\' SET MESSAGE_TEXT=\'technicaldoc revision is required\';\n END IF;\n\n SET v_orig_proj = NULL;\n IF NEW.original_id IS NOT NULL AND NEW.original_id <> 0 THEN\n SELECT project_id INTO v_orig_proj FROM technicaldocs WHERE id = NEW.original_id;\n IF v_orig_proj IS NULL THEN\n SIGNAL SQLSTATE \'45000\' SET MESSAGE_TEXT=\'original_id not found\';\n END IF;\n IF v_orig_proj <> NEW.project_id THEN\n SIGNAL SQLSTATE \'45000\' SET MESSAGE_TEXT=\'original and new revision must be in the same project\';\n END IF;\n END IF;\nEND' 'CREATE DEFINER=`root`@`localhost` TRIGGER trg_tdoc_ai_set_original\nAFTER INSERT ON technicaldocs\nFOR EACH ROW\nBEGIN\n IF NEW.original_id IS NULL OR NEW.original_id = 0 THEN\n UPDATE technicaldocs SET original_id = NEW.id WHERE id = NEW.id;\n END IF;\nEND' 'CREATE DEFINER=`root`@`localhost` TRIGGER trg_tdoc_bu_normalize\nBEFORE UPDATE ON technicaldocs\nFOR EACH ROW\nBEGIN\n SET NEW.title = TRIM(NEW.title);\n SET NEW.revision = NULLIF(TRIM(NEW.revision), \'\');\n SET NEW.pdf_path = NULLIF(TRIM(NEW.pdf_path), \'\');\n\n \n IF NEW.original_id <> OLD.original_id THEN\n SIGNAL SQLSTATE \'45000\' SET MESSAGE_TEXT=\'original_id is immutable\';\n END IF;\n IF NEW.project_id <> OLD.project_id THEN\n SIGNAL SQLSTATE \'45000\' SET MESSAGE_TEXT=\'project_id is immutable for a doc family\';\n END IF;\nEND'
sql_modes=1411383296 1411383296 1411383296
definers='root@localhost' 'root@localhost' 'root@localhost'
client_cs_names='utf8mb4' 'utf8mb4' 'utf8mb4'
connection_cl_names='utf8mb4_general_ci' 'utf8mb4_general_ci' 'utf8mb4_general_ci'
db_cl_names='utf8mb4_general_ci' 'utf8mb4_general_ci' 'utf8mb4_general_ci'
created=1759220377008102 1759220377345616 1759220377107766

Binary file not shown.

Binary file not shown.

View File

@@ -0,0 +1,8 @@
TYPE=TRIGGERS
triggers='CREATE DEFINER=`root`@`localhost` TRIGGER trg_tra_items_bi_guard\nBEFORE INSERT ON transmittal_items\nFOR EACH ROW\nBEGIN\n DECLARE v_type_code VARCHAR(20);\n DECLARE v_hdr_proj INT;\n DECLARE v_item_proj INT;\n\n IF NEW.transmittal_corr_id = NEW.item_corr_id THEN\n SIGNAL SQLSTATE \'45000\' SET MESSAGE_TEXT=\'transmittal cannot include itself as an item\';\n END IF;\n\n SELECT t.type_code, c.project_id\n INTO v_type_code, v_hdr_proj\n FROM correspondences c\n JOIN correspondence_types t ON t.type_id = c.correspondence_type_id\n WHERE c.corr_id = NEW.transmittal_corr_id;\n\n IF v_type_code IS NULL THEN\n SIGNAL SQLSTATE \'45000\' SET MESSAGE_TEXT=\'Transmittal header not found\';\n END IF;\n IF v_type_code <> \'TRANSMITTAL\' THEN\n SIGNAL SQLSTATE \'45000\' SET MESSAGE_TEXT=\'transmittal_corr_id must be type TRANSMITTAL\';\n END IF;\n\n SELECT project_id INTO v_item_proj FROM correspondences WHERE corr_id = NEW.item_corr_id;\n IF v_item_proj IS NULL THEN\n SIGNAL SQLSTATE \'45000\' SET MESSAGE_TEXT=\'item correspondence not found\';\n END IF;\n\n IF v_hdr_proj <> v_item_proj THEN\n SIGNAL SQLSTATE \'45000\' SET MESSAGE_TEXT=\'Transmittal header and item must be in the same project\';\n END IF;\nEND' 'CREATE DEFINER=`root`@`localhost` TRIGGER trg_tra_items_bu_guard\nBEFORE UPDATE ON transmittal_items\nFOR EACH ROW\nBEGIN\n SIGNAL SQLSTATE \'45000\' SET MESSAGE_TEXT=\'Update on transmittal_items is not allowed; delete and re-insert\';\nEND'
sql_modes=1411383296 1411383296
definers='root@localhost' 'root@localhost'
client_cs_names='utf8mb4' 'utf8mb4'
connection_cl_names='utf8mb4_general_ci' 'utf8mb4_general_ci'
db_cl_names='utf8mb4_general_ci' 'utf8mb4_general_ci'
created=1759220378602006 1759220378710787

Binary file not shown.

Some files were not shown because too many files have changed in this diff Show More