fix: tailwind v4 postcss, auth-server session, eslint cleanups
This commit is contained in:
BIN
backend/backend_tree.txt
Executable file
BIN
backend/backend_tree.txt
Executable file
Binary file not shown.
77
backend/docker-compose.yml
Executable file
77
backend/docker-compose.yml
Executable file
@@ -0,0 +1,77 @@
|
||||
# File: backend/docker-compose.yml
|
||||
# DMS Container v0_8_0 แยก service/ lcbp3-backend
|
||||
x-restart: &restart_policy
|
||||
restart: unless-stopped
|
||||
|
||||
x-logging: &default_logging
|
||||
logging:
|
||||
driver: "json-file"
|
||||
options:
|
||||
max-size: "10m"
|
||||
max-file: "5"
|
||||
volumes:
|
||||
backend_node_modules:
|
||||
services:
|
||||
backend:
|
||||
<<: [*restart_policy, *default_logging]
|
||||
image: dms-backend:dev
|
||||
# pull_policy: never # <-- FINAL FIX ADDED HERE
|
||||
container_name: dms_backend
|
||||
stdin_open: true
|
||||
tty: true
|
||||
#user: "node"
|
||||
user: "1000:1000"
|
||||
working_dir: /app
|
||||
deploy:
|
||||
resources:
|
||||
limits:
|
||||
cpus: "2.0"
|
||||
memory: 1G
|
||||
reservations:
|
||||
cpus: "0.25"
|
||||
memory: 256M
|
||||
environment:
|
||||
TZ: "Asia/Bangkok"
|
||||
CHOKIDAR_USEPOLLING: "1"
|
||||
CHOKIDAR_INTERVAL: "300"
|
||||
WATCHPACK_POLLING: "true"
|
||||
# NODE_ENV: "production"
|
||||
NODE_ENV: "development"
|
||||
PORT: "3001"
|
||||
DB_HOST: "mariadb"
|
||||
DB_PORT: "3306"
|
||||
DB_USER: "center"
|
||||
DB_PASSWORD: "Center#2025"
|
||||
DB_NAME: "dms"
|
||||
JWT_SECRET: "9a6d8705a6695ab9bae4ca1cd46c72a6379aa72404b96e2c5b59af881bb55c639dd583afdce5a885c68e188da55ce6dbc1fb4aa9cd4055ceb51507e56204e4ca"
|
||||
JWT_ACCESS_SECRET: "9a6d8705a6695ab9bae4ca1cd46c72a6379aa72404b96e2c5b59af881bb55c639dd583afdce5a885c68e188da55ce6dbc1fb4aa9cd4055ceb51507e56204e4ca"
|
||||
JWT_REFRESH_SECRET: "743e798bb10d6aba168bf68fc3cf8eff103c18bd34f1957a3906dc87987c0df139ab72498f2fe20d6c4c580f044ccba7d7bfa4393ee6035b73ba038f28d7480c"
|
||||
ACCESS_TTL_MS: "900000"
|
||||
REFRESH_TTL_MS: "604800000"
|
||||
JWT_EXPIRES_IN: "12h"
|
||||
PASSWORD_SALT_ROUNDS: "10"
|
||||
FRONTEND_ORIGIN: "https://lcbp3.np-dms.work"
|
||||
CORS_ORIGINS: "https://lcbp3.np-dms.work,http://localhost:3000,http://127.0.0.1:3000"
|
||||
COOKIE_DOMAIN: ".np-dms.work"
|
||||
RATE_LIMIT_WINDOW_MS: "900000"
|
||||
RATE_LIMIT_MAX: "200"
|
||||
BACKEND_LOG_DIR: "/app/logs"
|
||||
networks:
|
||||
lcbp3: {}
|
||||
volumes:
|
||||
- "/share/Container/dms/backend/src:/app/src:rw"
|
||||
# - "/share/Container/dms/backend/package.json:/app/package.json"
|
||||
# - "/share/Container/dms/backend/package-lock.json:/app/package-lock.json"
|
||||
- "/share/dms-data:/share/dms-data:rw"
|
||||
- "/share/Container/dms/logs/backend:/app/logs:rw"
|
||||
# - "/share/Container/dms/backend/node_modules:/app/node_modules"
|
||||
- "backend_node_modules:/app/node_modules"
|
||||
healthcheck:
|
||||
test: ["CMD", "wget", "-qO-", "http://127.0.0.1:3001/health"]
|
||||
interval: 10s
|
||||
timeout: 5s
|
||||
retries: 30
|
||||
|
||||
networks:
|
||||
lcbp3:
|
||||
external: true
|
||||
@@ -5,7 +5,11 @@
|
||||
import { Sequelize } from "sequelize";
|
||||
import { config } from "../config.js";
|
||||
|
||||
export const sequelize = new Sequelize(config.DB.NAME, config.DB.USER, config.DB.PASS, {
|
||||
export const sequelize = new Sequelize(
|
||||
config.DB.NAME,
|
||||
config.DB.USER,
|
||||
config.DB.PASS,
|
||||
{
|
||||
host: config.DB.HOST,
|
||||
port: config.DB.PORT,
|
||||
dialect: "mariadb",
|
||||
@@ -13,91 +17,55 @@ export const sequelize = new Sequelize(config.DB.NAME, config.DB.USER, config.DB
|
||||
dialectOptions: { timezone: "Z" },
|
||||
define: { freezeTableName: true, underscored: false, timestamps: false },
|
||||
pool: { max: 10, min: 0, idle: 10000 },
|
||||
});
|
||||
}
|
||||
);
|
||||
|
||||
// --- 1. ประกาศตัวแปรสำหรับ Export Model ทั้งหมด ---
|
||||
export let User, Role, Permission, Organization, Project, UserRole, RolePermission,
|
||||
UserProjectRole, Correspondence, CorrespondenceVersion, Document, CorrDocumentMap,
|
||||
Drawing, DrawingRevision, FileObject, RFA, RFARevision, RfaDrawingMap,
|
||||
Transmittal, TransmittalItem, Volume, ContractDwg, SubCategory;
|
||||
export let User = null;
|
||||
export let Role = null;
|
||||
export let Permission = null;
|
||||
export let UserRole = null;
|
||||
export let RolePermission = null;
|
||||
|
||||
if (process.env.ENABLE_SEQUELIZE === "1") {
|
||||
// --- 2. สร้าง Object ของ Models ทั้งหมดที่จะโหลด ---
|
||||
const modelsToLoad = {
|
||||
User: await import("./models/User.js").catch(() => null),
|
||||
Role: await import("./models/Role.js").catch(() => null),
|
||||
Permission: await import("./models/Permission.js").catch(() => null),
|
||||
Organization: await import("./models/Organization.js").catch(() => null),
|
||||
Project: await import("./models/Project.js").catch(() => null),
|
||||
UserRole: await import("./models/UserRole.js").catch(() => null),
|
||||
RolePermission: await import("./models/RolePermission.js").catch(() => null),
|
||||
UserProjectRole: await import("./models/UserProjectRole.js").catch(() => null),
|
||||
Correspondence: await import("./models/Correspondence.js").catch(() => null),
|
||||
CorrespondenceVersion: await import("./models/CorrespondenceVersion.js").catch(() => null),
|
||||
Document: await import("./models/Document.js").catch(() => null),
|
||||
CorrDocumentMap: await import("./models/CorrDocumentMap.js").catch(() => null),
|
||||
Drawing: await import("./models/Drawing.js").catch(() => null),
|
||||
DrawingRevision: await import("./models/DrawingRevision.js").catch(() => null),
|
||||
FileObject: await import("./models/FileObject.js").catch(() => null),
|
||||
RFA: await import("./models/RFA.js").catch(() => null),
|
||||
RFARevision: await import("./models/RFARevision.js").catch(() => null),
|
||||
RfaDrawingMap: await import("./models/RfaDrawingMap.js").catch(() => null),
|
||||
Transmittal: await import("./models/Transmittal.js").catch(() => null),
|
||||
TransmittalItem: await import("./models/TransmittalItem.js").catch(() => null),
|
||||
Volume: await import("./models/Volume.js").catch(() => null),
|
||||
ContractDwg: await import("./models/ContractDwg.js").catch(() => null),
|
||||
SubCategory: await import("./models/SubCategory.js").catch(() => null),
|
||||
};
|
||||
// โหลดโมเดลแบบ on-demand เพื่อลดความเสี่ยง runtime หากไฟล์โมเดลไม่มี
|
||||
const mdlUser = await import("./models/User.js").catch(() => null);
|
||||
const mdlRole = await import("./models/Role.js").catch(() => null);
|
||||
const mdlPerm = await import("./models/Permission.js").catch(() => null);
|
||||
const mdlUR = await import("./models/UserRole.js").catch(() => null);
|
||||
const mdlRP = await import("./models/RolePermission.js").catch(() => null);
|
||||
|
||||
// --- 3. Initialize Model ทั้งหมด ---
|
||||
User = modelsToLoad.User?.default ? modelsToLoad.User.default(sequelize) : null;
|
||||
Role = modelsToLoad.Role?.default ? modelsToLoad.Role.default(sequelize) : null;
|
||||
Permission = modelsToLoad.Permission?.default ? modelsToLoad.Permission.default(sequelize) : null;
|
||||
Organization = modelsToLoad.Organization?.default ? modelsToLoad.Organization.default(sequelize) : null;
|
||||
Project = modelsToLoad.Project?.default ? modelsToLoad.Project.default(sequelize) : null;
|
||||
UserRole = modelsToLoad.UserRole?.default ? modelsToLoad.UserRole.default(sequelize) : null;
|
||||
RolePermission = modelsToLoad.RolePermission?.default ? modelsToLoad.RolePermission.default(sequelize) : null;
|
||||
UserProjectRole = modelsToLoad.UserProjectRole?.default ? modelsToLoad.UserProjectRole.default(sequelize) : null;
|
||||
Correspondence = modelsToLoad.Correspondence?.default ? modelsToLoad.Correspondence.default(sequelize) : null;
|
||||
CorrespondenceVersion = modelsToLoad.CorrespondenceVersion?.default ? modelsToLoad.CorrespondenceVersion.default(sequelize) : null;
|
||||
Document = modelsToLoad.Document?.default ? modelsToLoad.Document.default(sequelize) : null;
|
||||
CorrDocumentMap = modelsToLoad.CorrDocumentMap?.default ? modelsToLoad.CorrDocumentMap.default(sequelize) : null;
|
||||
Drawing = modelsToLoad.Drawing?.default ? modelsToLoad.Drawing.default(sequelize) : null;
|
||||
DrawingRevision = modelsToLoad.DrawingRevision?.default ? modelsToLoad.DrawingRevision.default(sequelize) : null;
|
||||
FileObject = modelsToLoad.FileObject?.default ? modelsToLoad.FileObject.default(sequelize) : null;
|
||||
RFA = modelsToLoad.RFA?.default ? modelsToLoad.RFA.default(sequelize) : null;
|
||||
RFARevision = modelsToLoad.RFARevision?.default ? modelsToLoad.RFARevision.default(sequelize) : null;
|
||||
RfaDrawingMap = modelsToLoad.RfaDrawingMap?.default ? modelsToLoad.RfaDrawingMap.default(sequelize) : null;
|
||||
Transmittal = modelsToLoad.Transmittal?.default ? modelsToLoad.Transmittal.default(sequelize) : null;
|
||||
TransmittalItem = modelsToLoad.TransmittalItem?.default ? modelsToLoad.TransmittalItem.default(sequelize) : null;
|
||||
Volume = modelsToLoad.Volume?.default ? modelsToLoad.Volume.default(sequelize) : null;
|
||||
ContractDwg = modelsToLoad.ContractDwg?.default ? modelsToLoad.ContractDwg.default(sequelize) : null;
|
||||
SubCategory = modelsToLoad.SubCategory?.default ? modelsToLoad.SubCategory.default(sequelize) : null;
|
||||
if (mdlUser?.default) User = mdlUser.default(sequelize);
|
||||
if (mdlRole?.default) Role = mdlRole.default(sequelize);
|
||||
if (mdlPerm?.default) Permission = mdlPerm.default(sequelize);
|
||||
if (mdlUR?.default) UserRole = mdlUR.default(sequelize);
|
||||
if (mdlRP?.default) RolePermission = mdlRP.default(sequelize);
|
||||
|
||||
if (User && Role && Permission && UserRole && RolePermission) {
|
||||
User.belongsToMany(Role, {
|
||||
through: UserRole,
|
||||
foreignKey: "user_id",
|
||||
otherKey: "role_id",
|
||||
});
|
||||
Role.belongsToMany(User, {
|
||||
through: UserRole,
|
||||
foreignKey: "role_id",
|
||||
otherKey: "user_id",
|
||||
});
|
||||
|
||||
// --- 4. สร้างความสัมพันธ์ (Associations) ---
|
||||
const loadedModels = { User, Role, Permission, Organization, Project, UserRole, RolePermission,
|
||||
UserProjectRole, Correspondence, CorrespondenceVersion, Document, CorrDocumentMap,
|
||||
Drawing, DrawingRevision, FileObject, RFA, RFARevision, RfaDrawingMap,
|
||||
Transmittal, TransmittalItem, Volume, ContractDwg, SubCategory };
|
||||
|
||||
for (const modelName in loadedModels) {
|
||||
if (loadedModels[modelName] && loadedModels[modelName].associate) {
|
||||
loadedModels[modelName].associate(loadedModels);
|
||||
}
|
||||
}
|
||||
Role.belongsToMany(Permission, {
|
||||
through: RolePermission,
|
||||
foreignKey: "role_id",
|
||||
otherKey: "permission_id",
|
||||
});
|
||||
Permission.belongsToMany(Role, {
|
||||
through: RolePermission,
|
||||
foreignKey: "permission_id",
|
||||
otherKey: "role_id",
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
export async function dbReady() {
|
||||
if (process.env.ENABLE_SEQUELIZE !== "1") {
|
||||
console.log("Sequelize is disabled.");
|
||||
return Promise.resolve();
|
||||
}
|
||||
try {
|
||||
await sequelize.authenticate();
|
||||
console.log("Sequelize connection has been established successfully.");
|
||||
} catch (error) {
|
||||
console.error("Unable to connect to the database via Sequelize:", error);
|
||||
return Promise.reject(error);
|
||||
}
|
||||
}
|
||||
// โหมดเบา ๆ: แค่ทดสอบเชื่อมต่อ
|
||||
await sequelize.authenticate();
|
||||
}
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
// 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 cookieParser from "cookie-parser"; // added
|
||||
|
||||
import sql from "./db/index.js";
|
||||
import healthRouter from "./routes/health.js";
|
||||
@@ -10,9 +10,6 @@ import { authJwt } from "./middleware/authJwt.js";
|
||||
import { loadPrincipalMw } from "./middleware/loadPrincipal.js";
|
||||
|
||||
// ROUTES
|
||||
import usersRoutes from "./routes/users.js";
|
||||
import rbacAdminRoutes from "./routes/rbac_admin.js";
|
||||
import dashboardRoutes from "./routes/dashboard.js";
|
||||
import authRoutes from "./routes/auth.js";
|
||||
import lookupRoutes from "./routes/lookup.js";
|
||||
import organizationsRoutes from "./routes/organizations.js";
|
||||
@@ -26,6 +23,7 @@ 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);
|
||||
@@ -39,9 +37,7 @@ const ALLOW_ORIGINS = [
|
||||
"http://127.0.0.1:3000",
|
||||
FRONTEND_ORIGIN,
|
||||
...(process.env.CORS_ALLOWLIST
|
||||
? process.env.CORS_ALLOWLIST.split(",")
|
||||
.map((x) => x.trim())
|
||||
.filter(Boolean)
|
||||
? process.env.CORS_ALLOWLIST.split(",").map((x) => x.trim()).filter(Boolean)
|
||||
: []),
|
||||
].filter(Boolean);
|
||||
|
||||
@@ -78,10 +74,6 @@ app.use(
|
||||
exposedHeaders: ["Content-Disposition", "Content-Length"],
|
||||
})
|
||||
);
|
||||
|
||||
// parse cookies สำหรับ access_token / refresh_token
|
||||
app.use(cookieParser()); // added
|
||||
|
||||
app.options(
|
||||
"*",
|
||||
cors({
|
||||
@@ -113,12 +105,8 @@ app.get("/health", async (_req, res) => {
|
||||
});
|
||||
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");
|
||||
}
|
||||
try { await sql.query("SELECT 1"); res.send("ready"); }
|
||||
catch { res.status(500).send("not-ready"); }
|
||||
});
|
||||
app.get("/info", (_req, res) =>
|
||||
res.json({
|
||||
@@ -150,8 +138,6 @@ app.use("/api/volumes", volumesRoutes);
|
||||
app.use("/api/uploads", uploadsRoutes);
|
||||
app.use("/api/users", usersRoutes);
|
||||
app.use("/api/permissions", permissionsRoutes);
|
||||
app.use("/api/rbac", rbacAdminRoutes);
|
||||
app.use("/api/dashboard", dashboardRoutes);
|
||||
|
||||
// 404 / error
|
||||
app.use((req, res) =>
|
||||
@@ -173,9 +159,7 @@ async function shutdown(signal) {
|
||||
try {
|
||||
console.log(`[SHUTDOWN] ${signal} received`);
|
||||
await new Promise((resolve) => server.close(resolve));
|
||||
try {
|
||||
await sql.end();
|
||||
} catch {}
|
||||
try { await sql.end(); } catch {}
|
||||
console.log("[SHUTDOWN] complete");
|
||||
process.exit(0);
|
||||
} catch (e) {
|
||||
|
||||
@@ -3,118 +3,41 @@
|
||||
// - Project-scoped access control base on user_project_roles + permissions
|
||||
// - Requires req.user.roles and req.user.permissions to be populated (e.g. via auth.js with enrichment)
|
||||
// - Uses UserProjectRole model to check project membership
|
||||
// Helper ABAC เสริมบางเคส (ถ้าต้องการฟิลเตอร์/บังคับ project_id ตรง ๆ)
|
||||
// หมายเหตุ: โดยหลักแล้วคุณควรใช้ requirePerm() ที่บังคับ ABAC อัตโนมัติจาก permissions.scope_level
|
||||
|
||||
import { sequelize } from "../db/sequelize.js";
|
||||
import UPRModel from "../db/models/UserProjectRole.js";
|
||||
|
||||
/**
|
||||
* ดึง project_id ที่ผู้ใช้เข้าถึงได้ (จาก user_project_roles)
|
||||
*/
|
||||
export async function getUserProjectIds(user_id) {
|
||||
const UPR = UPRModel(sequelize);
|
||||
const rows = await UPR.findAll({ where: { user_id } });
|
||||
return [...new Set(rows.map((r) => r.project_id))];
|
||||
}
|
||||
|
||||
/**
|
||||
* projectScopedView(moduleName) -> middleware
|
||||
* - ต้องมี permission '<module>:view' หรือ
|
||||
* - เป็นสมาชิกของโปรเจ็กต์ (ผ่าน user_project_roles)
|
||||
* Behavior:
|
||||
* - ถ้า query ไม่มี project_id และผู้ใช้ไม่ใช่ Admin:
|
||||
* จำกัดผลลัพธ์ให้เฉพาะโปรเจ็กต์ที่ผู้ใช้เป็นสมาชิก
|
||||
* - ถ้ามี project_id: บังคับตรวจสิทธิ์การเป็นสมาชิกของโปรเจ็กต์นั้น (เว้นแต่เป็น Admin)
|
||||
*/
|
||||
export function projectScopedView(moduleName) {
|
||||
export function projectScopedViewFallback(moduleName) {
|
||||
// ใช้ในเคส legacy เท่านั้น
|
||||
return async (req, res, next) => {
|
||||
const roles = req.user?.roles || [];
|
||||
const isAdmin = roles.includes("Admin");
|
||||
const permName = `${moduleName}:view`;
|
||||
const hasViewPerm = (req.user?.permissions || []).includes(permName);
|
||||
const p = req.principal;
|
||||
if (!p) return res.status(401).json({ error: "Unauthenticated" });
|
||||
|
||||
// Admin ผ่านได้เสมอ
|
||||
if (isAdmin) return next();
|
||||
const hasViewPerm = p.can?.(`${moduleName}.view`) || p.permissions?.has?.(`${moduleName}.view`);
|
||||
if (p.is_superadmin) return next();
|
||||
|
||||
const qProjectId = req.query?.project_id
|
||||
? Number(req.query.project_id)
|
||||
: null;
|
||||
const memberProjects = await getUserProjectIds(req.user?.user_id);
|
||||
const qProjectId = req.query?.project_id ? Number(req.query.project_id) : null;
|
||||
|
||||
if (qProjectId) {
|
||||
// ต้องเป็นสมาชิกโปรเจ็กต์นั้น หรือมี perm view
|
||||
if (hasViewPerm || memberProjects.includes(qProjectId)) return next();
|
||||
return res
|
||||
.status(403)
|
||||
.json({ error: "Forbidden: not a member of project" });
|
||||
if (hasViewPerm || p.inProject(qProjectId)) return next();
|
||||
return res.status(403).json({ error: "FORBIDDEN_PROJECT" });
|
||||
} else {
|
||||
// ไม่มี project_id: ถ้ามี perm view → อนุญาตทั้งหมด
|
||||
// ถ้าไม่มี perm view → จำกัดด้วยรายการโปรเจ็กต์ที่เป็นสมาชิก (บันทึกไว้ใน req.abac.filterProjectIds)
|
||||
if (hasViewPerm) return next();
|
||||
if (!memberProjects.length)
|
||||
return res
|
||||
.status(403)
|
||||
.json({ error: "Forbidden: no accessible projects" });
|
||||
if (!p.project_ids?.length) return res.status(403).json({ error: "FORBIDDEN_PROJECT" });
|
||||
req.abac = req.abac || {};
|
||||
req.abac.filterProjectIds = memberProjects;
|
||||
req.abac.filterProjectIds = p.project_ids;
|
||||
return next();
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* บังคับเป็นสมาชิกโปรเจ็กต์จากค่า project_id ใน body
|
||||
* ใช้กับ create endpoints
|
||||
*/
|
||||
export function requireProjectMembershipFromBody() {
|
||||
return async (req, res, next) => {
|
||||
const roles = req.user?.roles || [];
|
||||
const isAdmin = roles.includes("Admin");
|
||||
if (isAdmin) return next();
|
||||
const pid = Number(req.body?.project_id);
|
||||
if (!pid) return res.status(400).json({ error: "project_id required" });
|
||||
const memberProjects = await getUserProjectIds(req.user?.user_id);
|
||||
if (!memberProjects.includes(pid))
|
||||
return res.status(403).json({ error: "Forbidden: not a project member" });
|
||||
next();
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* บังคับเป็นสมาชิกโปรเจ็กต์โดยอ้างอิงจากเรคคอร์ด (ใช้กับ update/delete)
|
||||
* opts: { modelLoader: (sequelize)=>Model, idParam: 'id', projectField: 'project_id' }
|
||||
*/
|
||||
export function requireProjectMembershipByRecord(opts) {
|
||||
const { modelLoader, idParam = "id", projectField = "project_id" } = opts;
|
||||
return async (req, res, next) => {
|
||||
const roles = req.user?.roles || [];
|
||||
const isAdmin = roles.includes("Admin");
|
||||
if (isAdmin) return next();
|
||||
const id = Number(req.params[idParam]);
|
||||
if (!id) return res.status(400).json({ error: "Invalid id" });
|
||||
const Model = modelLoader(sequelize);
|
||||
const row = await Model.findByPk(id);
|
||||
if (!row) return res.status(404).json({ error: "Not found" });
|
||||
const pid = Number(row[projectField]);
|
||||
const memberProjects = await getUserProjectIds(req.user?.user_id);
|
||||
if (!memberProjects.includes(pid))
|
||||
return res.status(403).json({ error: "Forbidden: not a project member" });
|
||||
next();
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* บังคับให้ view ทุกอันต้องส่ง project_id (ยกเว้น Admin)
|
||||
*/
|
||||
export function requireProjectIdQuery() {
|
||||
return async (req, res, next) => {
|
||||
const roles = req.user?.roles || [];
|
||||
const isAdmin = roles.includes("Admin");
|
||||
if (isAdmin) return next();
|
||||
const qProjectId = req.query?.project_id
|
||||
? Number(req.query.project_id)
|
||||
: null;
|
||||
if (!qProjectId)
|
||||
return res.status(400).json({ error: "project_id query required" });
|
||||
return (req, res, next) => {
|
||||
const p = req.principal;
|
||||
if (!p) return res.status(401).json({ error: "Unauthenticated" });
|
||||
if (p.is_superadmin) return next();
|
||||
const qProjectId = req.query?.project_id ? Number(req.query.project_id) : null;
|
||||
if (!qProjectId) return res.status(400).json({ error: "project_id query required" });
|
||||
next();
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -1,61 +1,30 @@
|
||||
// FILE: backend/src/middleware/auth.js
|
||||
|
||||
// (ถ้ายังใช้อยู่) ปรับให้สอดคล้อง Bearer + principal
|
||||
import jwt from "jsonwebtoken";
|
||||
import { config } from "../config.js";
|
||||
import { User, Role, UserRole } from "../db/sequelize.js";
|
||||
|
||||
export function signAccessToken(payload) {
|
||||
return jwt.sign(payload, config.JWT.SECRET, {
|
||||
expiresIn: config.JWT.EXPIRES_IN,
|
||||
});
|
||||
const { JWT_SECRET = "dev-secret", JWT_EXPIRES_IN = "30m" } = process.env;
|
||||
return jwt.sign(payload, JWT_SECRET, { expiresIn: JWT_EXPIRES_IN, issuer: "dms-backend" });
|
||||
}
|
||||
export function signRefreshToken(payload) {
|
||||
return jwt.sign(payload, config.JWT.REFRESH_SECRET, {
|
||||
expiresIn: config.JWT.REFRESH_EXPIRES_IN,
|
||||
});
|
||||
}
|
||||
|
||||
export function extractToken(req) {
|
||||
// ให้คุกกี้มาก่อน แล้วค่อย Bearer (รองรับทั้งสองทาง)
|
||||
const cookieTok = req.cookies?.access_token || null;
|
||||
if (cookieTok) return cookieTok;
|
||||
const hdr = req.headers.authorization || "";
|
||||
return hdr.startsWith("Bearer ") ? hdr.slice(7) : null;
|
||||
const { JWT_REFRESH_SECRET = "dev-refresh", JWT_REFRESH_EXPIRES_IN = "30d" } = process.env;
|
||||
return jwt.sign({ ...payload, t: "refresh" }, JWT_REFRESH_SECRET, { expiresIn: JWT_REFRESH_EXPIRES_IN, issuer: "dms-backend" });
|
||||
}
|
||||
|
||||
// ถ้าจะใช้ standalone (ไม่แนะนำถ้ามี authJwt แล้ว)
|
||||
export function requireAuth(req, res, next) {
|
||||
if (req.path === "/health") return next(); // อนุญาต health เสมอ
|
||||
const token = extractToken(req);
|
||||
if (!token) return res.status(401).json({ error: "Missing token" });
|
||||
|
||||
const h = req.headers.authorization || "";
|
||||
const m = /^Bearer\s+(.+)$/i.exec(h || "");
|
||||
if (!m) return res.status(401).json({ error: "Missing token" });
|
||||
try {
|
||||
req.user = jwt.verify(token, config.JWT.SECRET);
|
||||
const { JWT_SECRET = "dev-secret" } = process.env;
|
||||
const payload = jwt.verify(m[1], JWT_SECRET, { issuer: "dms-backend" });
|
||||
req.auth = { user_id: payload.user_id, username: payload.username };
|
||||
req.user = req.user || {};
|
||||
req.user.user_id = payload.user_id;
|
||||
req.user.username = payload.username;
|
||||
next();
|
||||
} catch {
|
||||
return res.status(401).json({ error: "Invalid/Expired token" });
|
||||
}
|
||||
}
|
||||
// ใช้กับเส้นทางที่ login แล้วจะ enrich ต่อได้ แต่ไม่บังคับ
|
||||
export function optionalAuth(req, _res, next) {
|
||||
const token = extractToken(req);
|
||||
if (!token) return next();
|
||||
try {
|
||||
req.user = jwt.verify(token, config.JWT.SECRET);
|
||||
} catch {}
|
||||
next();
|
||||
}
|
||||
|
||||
export async function enrichRoles(req, _res, next) {
|
||||
if (!req.user?.user_id) return next();
|
||||
const rows = await UserRole.findAll({
|
||||
where: { user_id: req.user.user_id },
|
||||
include: [{ model: Role }],
|
||||
}).catch(() => []);
|
||||
req.user.roles = rows.map((r) => r.role?.role_name).filter(Boolean);
|
||||
next();
|
||||
}
|
||||
|
||||
export function hasPerm(req, perm) {
|
||||
const set = new Set(req?.user?.permissions || []);
|
||||
return set.has(perm);
|
||||
}
|
||||
}
|
||||
@@ -25,6 +25,10 @@ export function authJwt() {
|
||||
// แนบข้อมูลขั้นต่ำให้ middleware ถัดไป
|
||||
req.auth = { user_id: payload.user_id, username: payload.username };
|
||||
//req.user = { user_id: payload.user_id, username: payload.username };
|
||||
// เผื่อโค้ดเก่าอ้างอิง req.user
|
||||
req.user = req.user || {};
|
||||
req.user.user_id = payload.user_id;
|
||||
req.user.username = payload.username;
|
||||
next();
|
||||
} catch (e) {
|
||||
return res.status(401).json({ error: "Unauthenticated" });
|
||||
|
||||
@@ -5,15 +5,90 @@
|
||||
// - Uses rbac.js utility to load principal info
|
||||
// - Attaches to req.principal
|
||||
// - Requires req.user.user_id to be populated (e.g. via auth.js or authJwt.js)
|
||||
// โหลด principal จาก DB แล้วแนบไว้ใน req.principal
|
||||
// NOTE: ตรงนี้สมมุติว่าคุณมี service/query ฝั่ง DB อยู่แล้ว (เช่น sql/Sequelize)
|
||||
// ถ้าคุณมีฟังก์ชันโหลด principal อยู่ที่อื่น ให้แทน logic DB ตรง FIXME ด้านล่าง
|
||||
// ใช้ req.auth.user_id และตั้ง req.principal ให้ครบ (RBAC + ABAC)
|
||||
|
||||
import { loadPrincipal } from "../utils/rbac.js";
|
||||
import sql from "../db/index.js";
|
||||
|
||||
export function loadPrincipalMw() {
|
||||
return async (req, res, next) => {
|
||||
try {
|
||||
if (!req.user?.user_id)
|
||||
return res.status(401).json({ error: "Unauthenticated" });
|
||||
req.principal = await loadPrincipal(req.user.user_id);
|
||||
const uid = req?.auth?.user_id || req?.user?.user_id;
|
||||
if (!uid) return res.status(401).json({ error: "Unauthenticated" });
|
||||
|
||||
// --- 1) users (รวม org_id)
|
||||
const [[u]] = await sql.query(
|
||||
`SELECT user_id, username, email, first_name, last_name, org_id, is_active
|
||||
FROM users WHERE user_id=? LIMIT 1`,
|
||||
[uid]
|
||||
);
|
||||
if (!u || u.is_active === 0) return res.status(401).json({ error: "Unauthenticated" });
|
||||
|
||||
// --- 2) roles (global)
|
||||
const [roleRows] = await sql.query(
|
||||
`SELECT r.role_id, r.role_code, r.role_name
|
||||
FROM user_roles ur
|
||||
JOIN roles r ON r.role_id = ur.role_id
|
||||
WHERE ur.user_id=?`,
|
||||
[uid]
|
||||
);
|
||||
const roleCodes = new Set(roleRows.map(r => r.role_code));
|
||||
const is_superadmin = roleCodes.has("SUPER_ADMIN");
|
||||
|
||||
// --- 3) permissions (ผ่าน role_permissions)
|
||||
const [permRows] = await sql.query(
|
||||
`SELECT DISTINCT p.perm_code
|
||||
FROM user_roles ur
|
||||
JOIN role_permissions rp ON rp.role_id = ur.role_id
|
||||
JOIN permissions p ON p.permission_id = rp.permission_id AND p.is_active=1
|
||||
WHERE ur.user_id=?`,
|
||||
[uid]
|
||||
);
|
||||
const permSet = new Set(permRows.map(x => x.perm_code));
|
||||
|
||||
// --- 4) project scope (user_project_roles)
|
||||
const [projRows] = await sql.query(
|
||||
`SELECT DISTINCT project_id FROM user_project_roles WHERE user_id=?`,
|
||||
[uid]
|
||||
);
|
||||
const project_ids = projRows.map(r => r.project_id);
|
||||
|
||||
// --- 5) org scope: users.org_id + orgs จาก project_parties ของโปรเจ็คที่เข้าถึง
|
||||
const baseOrgIds = u.org_id ? [u.org_id] : [];
|
||||
let projOrgIds = [];
|
||||
if (project_ids.length) {
|
||||
const [rows] = await sql.query(
|
||||
`SELECT DISTINCT org_id FROM project_parties WHERE project_id IN (?)`,
|
||||
[project_ids]
|
||||
);
|
||||
projOrgIds = rows.map(r => r.org_id);
|
||||
}
|
||||
const org_ids = Array.from(new Set([...baseOrgIds, ...projOrgIds]));
|
||||
|
||||
req.principal = {
|
||||
user_id: u.user_id,
|
||||
username: u.username,
|
||||
email: u.email,
|
||||
first_name: u.first_name,
|
||||
last_name: u.last_name,
|
||||
org_id: u.org_id || null,
|
||||
|
||||
roles: roleRows.map(r => ({ role_id: r.role_id, role_code: r.role_code, role_name: r.role_name })),
|
||||
permissions: permSet, // Set ของ perm_code
|
||||
project_ids,
|
||||
org_ids,
|
||||
is_superadmin,
|
||||
|
||||
// helpers
|
||||
can: (code) => is_superadmin || permSet.has(code),
|
||||
canAny: (codes=[]) => is_superadmin || codes.some(c => permSet.has(c)),
|
||||
canAll: (codes=[]) => is_superadmin || codes.every(c => permSet.has(c)),
|
||||
inProject: (pid) => is_superadmin || project_ids.includes(Number(pid)),
|
||||
inOrg: (oid) => is_superadmin || org_ids.includes(Number(oid)),
|
||||
};
|
||||
|
||||
next();
|
||||
} catch (err) {
|
||||
console.error("loadPrincipal error", err);
|
||||
|
||||
@@ -2,16 +2,14 @@
|
||||
// Permission guard middleware
|
||||
// - Checks if user has required permissions
|
||||
// - Requires req.user.permissions to be populated (e.g. via auth.js or authJwt.js with enrichment)
|
||||
// เปลี่ยนให้เป็น wrapper ที่เรียก req.principal (ทางเก่ายังใช้ได้)**
|
||||
|
||||
/**
|
||||
* requirePerm('rfa:create') => ตรวจว่ามี permission นี้ใน req.user.permissions
|
||||
* ต้องแน่ใจว่าเรียก enrichPermissions() มาก่อน หรือคำนวณที่จุดเข้าใช้งาน
|
||||
*/
|
||||
export function requirePerm(...allowedPerms) {
|
||||
return (req, res, next) => {
|
||||
const perms = req.user?.permissions || [];
|
||||
const ok = perms.some((p) => allowedPerms.includes(p));
|
||||
if (!ok) return res.status(403).json({ error: "Forbidden" });
|
||||
const p = req.principal;
|
||||
if (!p) return res.status(401).json({ error: "Unauthenticated" });
|
||||
const ok = p.is_superadmin || allowedPerms.some((code) => p.permissions?.has?.(code));
|
||||
if (!ok) return res.status(403).json({ error: "FORBIDDEN", need_any_of: allowedPerms });
|
||||
next();
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -2,39 +2,40 @@
|
||||
// Permission calculation and enrichment middleware
|
||||
// - Computes effective permissions for a user based on their roles
|
||||
// - Attaches permissions to req.user.permissions
|
||||
// ใช้เฉพาะกรณีที่คุณยังมี stack Sequelize เดิมอยู่ และอยาก enrich จาก Role/Permission model
|
||||
// โดยทั่วไป ถ้าคุณใช้ loadPrincipalMw() อยู่แล้ว สามารถไม่ใช้ไฟล์นี้ได้
|
||||
|
||||
import { Role, Permission, UserRole, RolePermission } from "../db/sequelize.js";
|
||||
import { Permission, UserRole, RolePermission } from "../db/sequelize.js";
|
||||
|
||||
/**
|
||||
* คืนชุด permission (string[]) ของ user_id
|
||||
*/
|
||||
export async function computeEffectivePermissions(user_id) {
|
||||
// ดึง roles ของผู้ใช้
|
||||
const userRoles = await UserRole.findAll({ where: { user_id } });
|
||||
const roleIds = userRoles.map((r) => r.role_id);
|
||||
if (!roleIds.length) return [];
|
||||
|
||||
// ดึง permission ผ่าน role_permissions
|
||||
const rp = await RolePermission.findAll({ where: { role_id: roleIds } });
|
||||
const permIds = [...new Set(rp.map((x) => x.permission_id))];
|
||||
if (!permIds.length) return [];
|
||||
|
||||
const perms = await Permission.findAll({ where: { permission_id: permIds } });
|
||||
return [...new Set(perms.map((p) => p.permission_name))];
|
||||
// ใช้ perm_code ให้สอดคล้อง seed
|
||||
return [...new Set(perms.map((p) => p.perm_code))];
|
||||
}
|
||||
|
||||
/**
|
||||
* middleware: เติม permissions ลง req.user.permissions
|
||||
*/
|
||||
export function enrichPermissions() {
|
||||
return async (req, _res, next) => {
|
||||
if (!req.user?.user_id) return next();
|
||||
const uid = req?.auth?.user_id || req?.user?.user_id;
|
||||
if (!uid) return next();
|
||||
try {
|
||||
const perms = await computeEffectivePermissions(req.user.user_id);
|
||||
const perms = await computeEffectivePermissions(uid);
|
||||
// อัปเดตทั้ง req.principal และ req.user (เผื่อโค้ดเก่า)
|
||||
req.principal = req.principal || {};
|
||||
req.principal.permissions = new Set(perms);
|
||||
req.user = req.user || {};
|
||||
req.user.permissions = perms;
|
||||
} catch (e) {
|
||||
req.user.permissions = [];
|
||||
} catch {
|
||||
if (req.principal) req.principal.permissions = new Set();
|
||||
if (req.user) req.user.permissions = [];
|
||||
}
|
||||
next();
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -5,18 +5,19 @@
|
||||
|
||||
export function requireRole(...allowed) {
|
||||
return (req, res, next) => {
|
||||
const roles = req.user?.roles || [];
|
||||
const ok = roles.some((r) => allowed.includes(r));
|
||||
if (!ok) return res.status(403).json({ error: "Forbidden" });
|
||||
const roles = (req.principal?.roles || []).map(r => r.role_code);
|
||||
const ok = roles.some((r) => allowed.includes(r)) || req.principal?.is_superadmin;
|
||||
if (!ok) return res.status(403).json({ error: "FORBIDDEN_ROLE", need_any_of: allowed });
|
||||
next();
|
||||
};
|
||||
}
|
||||
|
||||
export function requirePermission(...allowedPerms) {
|
||||
export function requirePermissionCode(...codes) {
|
||||
return (req, res, next) => {
|
||||
const perms = req.user?.permissions || [];
|
||||
const ok = perms.some((p) => allowedPerms.includes(p));
|
||||
if (!ok) return res.status(403).json({ error: "Forbidden" });
|
||||
const p = req.principal;
|
||||
if (!p) return res.status(401).json({ error: "Unauthenticated" });
|
||||
const ok = p.is_superadmin || codes.some((c) => p.permissions?.has?.(c));
|
||||
if (!ok) return res.status(403).json({ error: "FORBIDDEN", need_any_of: codes });
|
||||
next();
|
||||
};
|
||||
}
|
||||
|
||||
18
backend/src/middleware/requireBearer.js
Executable file
18
backend/src/middleware/requireBearer.js
Executable file
@@ -0,0 +1,18 @@
|
||||
// FILE: 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" });
|
||||
}
|
||||
}
|
||||
@@ -6,32 +6,59 @@
|
||||
// - Uses canPerform() utility from rbac.js
|
||||
// - Supports global, org, and project scopes
|
||||
// - Requires req.principal to be populated (e.g. via loadPrincipal middleware)
|
||||
// เช็คตาม perm_code + ABAC อัตโนมัติจาก permissions.scope_level
|
||||
import sql from "../db/index.js";
|
||||
|
||||
import { canPerform } from "../utils/rbac.js";
|
||||
let _permMap = null;
|
||||
let _loadedAt = 0;
|
||||
const TTL_MS = 60_000;
|
||||
|
||||
async function getPermRegistry() {
|
||||
const now = Date.now();
|
||||
if (_permMap && now - _loadedAt < TTL_MS) return _permMap;
|
||||
const [rows] = await sql.query(
|
||||
`SELECT perm_code, scope_level FROM permissions WHERE is_active=1`
|
||||
);
|
||||
_permMap = new Map(rows.map(r => [r.perm_code, r.scope_level])); // GLOBAL | ORG | PROJECT
|
||||
_loadedAt = now;
|
||||
return _permMap;
|
||||
}
|
||||
|
||||
/**
|
||||
* requirePerm('correspondence.create', { scope: 'org', getOrgId: req => ... })
|
||||
* scope: 'global' | 'org' | 'project'
|
||||
* requirePerm('rfas.view', { projectParam: 'project_id', orgParam: 'org_id' })
|
||||
* - GLOBAL: แค่มี perm ก็ผ่าน
|
||||
* - ORG: ต้องมี perm + อยู่ใน org scope (อ่าน org_id จาก param หากระบุ; ไม่ระบุจะใช้ req.principal.org_id)
|
||||
* - PROJECT:ต้องมี perm + อยู่ใน project scope (อ่าน project_id จาก param)
|
||||
*/
|
||||
export function requirePerm(
|
||||
permCode,
|
||||
{ scope = "global", getOrgId = null, getProjectId = null } = {}
|
||||
) {
|
||||
export function requirePerm(permCode, { projectParam, orgParam } = {}) {
|
||||
return async (req, res, next) => {
|
||||
try {
|
||||
const orgId = getOrgId ? await getOrgId(req) : null;
|
||||
const projectId = getProjectId ? await getProjectId(req) : null;
|
||||
const p = req.principal;
|
||||
if (!p) return res.status(401).json({ error: "Unauthenticated" });
|
||||
|
||||
if (canPerform(req.principal, permCode, { scope, orgId, projectId }))
|
||||
return next();
|
||||
|
||||
return res.status(403).json({
|
||||
error: "FORBIDDEN",
|
||||
message: `Require ${permCode} (${scope}-scoped)`,
|
||||
});
|
||||
} catch (e) {
|
||||
console.error("requirePerm error", e);
|
||||
res.status(500).json({ error: "Permission check error" });
|
||||
if (!(p.is_superadmin || p.permissions?.has?.(permCode))) {
|
||||
return res.status(403).json({ error: "FORBIDDEN", need: permCode });
|
||||
}
|
||||
|
||||
const registry = await getPermRegistry();
|
||||
const scope = registry.get(permCode) || "GLOBAL";
|
||||
|
||||
const readParam = (name) => req.params?.[name] ?? req.query?.[name] ?? req.body?.[name];
|
||||
|
||||
if (scope === "PROJECT") {
|
||||
const pid = Number(projectParam ? readParam(projectParam) : undefined);
|
||||
if (!p.is_superadmin) {
|
||||
if (!pid || !p.inProject(pid)) {
|
||||
return res.status(403).json({ error: "FORBIDDEN_PROJECT", project_id: pid || null });
|
||||
}
|
||||
}
|
||||
} else if (scope === "ORG") {
|
||||
const oid = Number(orgParam ? readParam(orgParam) : p.org_id);
|
||||
if (!p.is_superadmin) {
|
||||
if (!oid || !p.inOrg(oid)) {
|
||||
return res.status(403).json({ error: "FORBIDDEN_ORG", org_id: oid || null });
|
||||
}
|
||||
}
|
||||
}
|
||||
next();
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -1,4 +1,6 @@
|
||||
// FILE: src/routes/categories.js
|
||||
// อ่าน: ใช้ organizations.view (GLOBAL)
|
||||
// สร้าง/แก้/ลบ: ใช้ settings.manage (GLOBAL)
|
||||
import { Router } from "express";
|
||||
import sql from "../db/index.js";
|
||||
import { requirePerm } from "../middleware/requirePerm.js";
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
// FILE: src/routes/contract_dwg.js
|
||||
// ใน seed ยังไม่มี contract_dwg.* → ผูกชั่วคราวกับสิทธิ์กลุ่ม drawings:
|
||||
// read → drawings.view, create/update/delete → drawings.upload/delete (PROJECT scope)
|
||||
import { Router } from "express";
|
||||
import sql from "../db/index.js";
|
||||
import { requirePerm } from "../middleware/requirePerm.js";
|
||||
|
||||
@@ -1,4 +1,7 @@
|
||||
// FILE: src/routes/contracts.js
|
||||
// ไม่มี contract.* ใน seed → map เป็นงานดูแลองค์กร/โปรเจ็กต์:
|
||||
// list/get → projects.view (ORG)
|
||||
// create/update/delete → projects.manage (ORG)
|
||||
import { Router } from "express";
|
||||
import sql from "../db/index.js";
|
||||
import { requirePerm } from "../middleware/requirePerm.js";
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
// FILE: backend/src/routes/mvp.js
|
||||
// (generic entity maps — ใช้ ‘projects.view’ อ่าน และ ‘projects.manage’ เขียน/ลบ)
|
||||
import { Router } from "express";
|
||||
import sql from "../db/index.js";
|
||||
import { requirePerm } from "../middleware/requirePerm.js";
|
||||
|
||||
@@ -1,99 +1,55 @@
|
||||
// File: backend/src/routes/users.js
|
||||
import { Router } from 'express';
|
||||
import { User, Role } from '../db/sequelize.js';
|
||||
import { authJwt } from "../middleware/authJwt.js";
|
||||
import { loadPrincipalMw } from "../middleware/loadPrincipal.js"; // แก้ไข: import ให้ถูกต้อง
|
||||
import { requirePerm } from '../middleware/requirePerm.js';
|
||||
import { hashPassword } from '../utils/passwords.js';
|
||||
// FILE: backend/src/routes/users.js
|
||||
import { Router } from "express";
|
||||
import sql from "../db/index.js";
|
||||
import { requirePerm } from "../middleware/requirePerm.js";
|
||||
|
||||
const router = Router();
|
||||
const r = Router();
|
||||
|
||||
// Middleware Chain ที่ถูกต้อง 100%
|
||||
router.use(authJwt(), loadPrincipalMw());
|
||||
|
||||
// GET /api/users
|
||||
router.get('/', requirePerm('users.view'), async (req, res, next) => {
|
||||
try {
|
||||
const users = await User.findAll({
|
||||
attributes: { exclude: ['password_hash'] },
|
||||
include: [{ model: Role, attributes: ['id', 'name'], through: { attributes: [] } }],
|
||||
order: [['username', 'ASC']]
|
||||
});
|
||||
res.json(users);
|
||||
} catch (error) { next(error); }
|
||||
// ME (ทุกคน)
|
||||
r.get("/me", async (req, res) => {
|
||||
const p = req.principal;
|
||||
const [[u]] = await sql.query(
|
||||
`SELECT user_id, username, email, first_name, last_name, org_id FROM users WHERE user_id=?`,
|
||||
[p.user_id]
|
||||
);
|
||||
if (!u) return res.status(404).json({ error: "User not found" });
|
||||
const [roles] = await sql.query(
|
||||
`SELECT r.role_code, r.role_name, ur.org_id, ur.project_id
|
||||
FROM user_roles ur JOIN roles r ON r.role_id = ur.role_id
|
||||
WHERE ur.user_id=?`,
|
||||
[p.user_id]
|
||||
);
|
||||
res.json({
|
||||
...u,
|
||||
roles,
|
||||
role_codes: roles.map((r) => r.role_code),
|
||||
permissions: [...(p.permissions || [])],
|
||||
project_ids: p.project_ids,
|
||||
org_ids: p.org_ids,
|
||||
is_superadmin: p.is_superadmin,
|
||||
});
|
||||
});
|
||||
|
||||
// POST /api/users
|
||||
router.post('/', requirePerm('users.manage'), async (req, res, next) => {
|
||||
const { username, email, password, first_name, last_name, is_active, roles } = req.body;
|
||||
if (!username || !email || !password) {
|
||||
return res.status(400).json({ message: 'Username, email, and password are required' });
|
||||
// USERS LIST (ORG scope) — admin.access
|
||||
r.get(
|
||||
"/",
|
||||
requirePerm("admin.access", { orgParam: "org_id" }),
|
||||
async (req, res) => {
|
||||
const P = req.principal;
|
||||
let rows = [];
|
||||
if (P.is_superadmin) {
|
||||
[rows] = await sql.query(
|
||||
"SELECT user_id, username, email, org_id FROM users ORDER BY user_id DESC LIMIT 500"
|
||||
);
|
||||
} else if (P.org_ids?.length) {
|
||||
const inSql = P.org_ids.map(() => "?").join(",");
|
||||
[rows] = await sql.query(
|
||||
`SELECT user_id, username, email, org_id FROM users WHERE org_id IN (${inSql}) ORDER BY user_id DESC LIMIT 500`,
|
||||
P.org_ids
|
||||
);
|
||||
}
|
||||
try {
|
||||
const password_hash = await hashPassword(password);
|
||||
const newUser = await User.create({
|
||||
username, email, password_hash, first_name, last_name, is_active: is_active !== false,
|
||||
created_by: req.principal.user_id,
|
||||
updated_by: req.principal.user_id,
|
||||
org_id: req.principal.org_ids[0] || null,
|
||||
});
|
||||
res.json(rows);
|
||||
}
|
||||
);
|
||||
|
||||
if (roles && roles.length > 0) {
|
||||
await newUser.setRoles(roles);
|
||||
}
|
||||
|
||||
const userWithRoles = await User.findByPk(newUser.id, {
|
||||
attributes: { exclude: ['password_hash'] },
|
||||
include: [{ model: Role, attributes: ['id', 'name'], through: { attributes: [] } }]
|
||||
});
|
||||
res.status(201).json(userWithRoles);
|
||||
} catch (error) {
|
||||
if (error.name === 'SequelizeUniqueConstraintError') {
|
||||
return res.status(409).json({ message: 'Username or email already exists.' });
|
||||
}
|
||||
next(error);
|
||||
}
|
||||
});
|
||||
|
||||
// PUT /api/users/:id
|
||||
router.put('/:id', requirePerm('users.manage'), async (req, res, next) => {
|
||||
const { id } = req.params;
|
||||
const { email, first_name, last_name, is_active, roles } = req.body;
|
||||
try {
|
||||
const user = await User.findByPk(id);
|
||||
if (!user) {
|
||||
return res.status(404).json({ message: 'User not found' });
|
||||
}
|
||||
user.email = email ?? user.email;
|
||||
user.first_name = first_name ?? user.first_name;
|
||||
user.last_name = last_name ?? user.last_name;
|
||||
user.is_active = is_active ?? user.is_active;
|
||||
user.updated_by = req.principal.user_id;
|
||||
await user.save();
|
||||
|
||||
if (roles) {
|
||||
await user.setRoles(roles);
|
||||
}
|
||||
const updatedUser = await User.findByPk(id, {
|
||||
attributes: { exclude: ['password_hash'] },
|
||||
include: [{ model: Role, attributes: ['id', 'name'], through: { attributes: [] } }]
|
||||
});
|
||||
res.json(updatedUser);
|
||||
} catch (error) { next(error); }
|
||||
});
|
||||
|
||||
// DELETE /api/users/:id
|
||||
router.delete('/:id', requirePerm('users.manage'), async (req, res, next) => {
|
||||
try {
|
||||
const user = await User.findByPk(req.params.id);
|
||||
if (!user) {
|
||||
return res.status(404).json({ message: 'User not found' });
|
||||
}
|
||||
user.is_active = false;
|
||||
user.updated_by = req.principal.user_id;
|
||||
await user.save();
|
||||
res.status(204).send();
|
||||
} catch (error) { next(error); }
|
||||
});
|
||||
|
||||
export default router;
|
||||
export default r;
|
||||
|
||||
Reference in New Issue
Block a user