Files
lcbp3.np-dms.work/backend/src/index.js

174 lines
5.2 KiB
JavaScript

// FILE: backend/src/index.js (ESM) ไฟล์ฉบับ “Bearer-only”
// FILE: src/index.js (ESM)
import fs from "node:fs";
import express from "express";
import cors from "cors";
import sql from "./db/index.js";
import healthRouter from "./routes/health.js";
import { authJwt } from "./middleware/authJwt.js";
import { loadPrincipalMw } from "./middleware/loadPrincipal.js";
// ROUTES
import authRoutes from "./routes/auth.js";
import lookupRoutes from "./routes/lookup.js";
import organizationsRoutes from "./routes/organizations.js";
import projectsRoutes from "./routes/projects.js";
import correspondencesRoutes from "./routes/correspondences.js";
import rfasRoutes from "./routes/rfas.js";
import drawingsRoutes from "./routes/drawings.js";
import transmittalsRoutes from "./routes/transmittals.js";
import contractsRoutes from "./routes/contracts.js";
import contractDwgRoutes from "./routes/contract_dwg.js";
import categoriesRoutes from "./routes/categories.js";
import volumesRoutes from "./routes/volumes.js";
import uploadsRoutes from "./routes/uploads.js";
import usersRoutes from "./routes/users.js";
import permissionsRoutes from "./routes/permissions.js";
const PORT = Number(process.env.PORT || 3001);
const NODE_ENV = process.env.NODE_ENV || "development";
const FRONTEND_ORIGIN =
process.env.FRONTEND_ORIGIN || "https://lcbp3.np-dms.work";
const ALLOW_ORIGINS = [
"http://localhost:3000",
"http://127.0.0.1:3000",
FRONTEND_ORIGIN,
...(process.env.CORS_ALLOWLIST
? process.env.CORS_ALLOWLIST.split(",").map((x) => x.trim()).filter(Boolean)
: []),
].filter(Boolean);
const LOG_DIR = process.env.BACKEND_LOG_DIR || "/app/logs";
try {
if (!fs.existsSync(LOG_DIR)) fs.mkdirSync(LOG_DIR, { recursive: true });
} catch (e) {
console.warn("[WARN] Cannot ensure LOG_DIR:", LOG_DIR, e?.message);
}
const app = express();
app.set("trust proxy", 1);
// CORS: allow list
app.use(
cors({
origin(origin, cb) {
if (!origin) return cb(null, true); // server-to-server / curl
cb(null, ALLOW_ORIGINS.includes(origin));
},
credentials: false, // Bearer-only
methods: ["GET", "POST", "PUT", "PATCH", "DELETE", "OPTIONS"],
allowedHeaders: [
"Content-Type",
"Authorization",
"X-Requested-With",
"Accept",
"Origin",
"Referer",
"User-Agent",
"Cache-Control",
"Pragma",
],
exposedHeaders: ["Content-Disposition", "Content-Length"],
})
);
app.options(
"*",
cors({
origin(origin, cb) {
if (!origin) return cb(null, true);
cb(null, ALLOW_ORIGINS.includes(origin));
},
credentials: false,
})
);
app.use(express.json({ limit: "10mb" }));
app.use(express.urlencoded({ extended: true, limit: "10mb" }));
// minimal access log
app.use((req, _res, next) => {
console.log(`[REQ] ${req.method} ${req.originalUrl}`);
next();
});
// health/info (เปิดทั้ง /health, /livez, /readyz, /info)
app.get("/health", async (_req, res) => {
try {
const [[{ now }]] = await sql.query("SELECT NOW() AS now");
res.json({ status: "ok", db: "ok", now });
} catch (e) {
res.status(500).json({ status: "degraded", db: "fail", error: e?.message });
}
});
app.get("/livez", (_req, res) => res.send("ok"));
app.get("/readyz", async (_req, res) => {
try { await sql.query("SELECT 1"); res.send("ready"); }
catch { res.status(500).send("not-ready"); }
});
app.get("/info", (_req, res) =>
res.json({
name: "dms-backend",
env: NODE_ENV,
version: process.env.APP_VERSION || "0.5.0",
commit: process.env.GIT_COMMIT || undefined,
})
);
// ---------- Public (no auth) ----------
app.use("/api", healthRouter);
app.use("/api/auth", authRoutes);
// ---------- Protected (Bearer + Principal) ----------
app.use("/api", authJwt(), loadPrincipalMw());
app.use("/api/lookup", lookupRoutes);
app.use("/api/organizations", organizationsRoutes);
app.use("/api/projects", projectsRoutes);
app.use("/api/correspondences", correspondencesRoutes);
app.use("/api/rfas", rfasRoutes);
app.use("/api/drawings", drawingsRoutes);
app.use("/api/transmittals", transmittalsRoutes);
app.use("/api/contracts", contractsRoutes);
app.use("/api/contract-dwg", contractDwgRoutes);
app.use("/api/categories", categoriesRoutes);
app.use("/api/volumes", volumesRoutes);
app.use("/api/uploads", uploadsRoutes);
app.use("/api/users", usersRoutes);
app.use("/api/permissions", permissionsRoutes);
// 404 / error
app.use((req, res) =>
res.status(404).json({ error: "NOT_FOUND", path: req.originalUrl })
);
// eslint-disable-next-line no-unused-vars
app.use((err, _req, res, _next) => {
console.error("[UNHANDLED ERROR]", err);
res.status(err?.status || 500).json({ error: "SERVER_ERROR" });
});
// START
const server = app.listen(PORT, () => {
console.log(`Backend API listening on ${PORT} (env=${NODE_ENV})`);
});
// Shutdown
async function shutdown(signal) {
try {
console.log(`[SHUTDOWN] ${signal} received`);
await new Promise((resolve) => server.close(resolve));
try { await sql.end(); } catch {}
console.log("[SHUTDOWN] complete");
process.exit(0);
} catch (e) {
console.error("[SHUTDOWN] error", e);
process.exit(1);
}
}
process.on("SIGTERM", () => shutdown("SIGTERM"));
process.on("SIGINT", () => shutdown("SIGINT"));
export default app;