3 Commits

Author SHA1 Message Date
223c9a6c6e WIP: save before rebase 2025-10-05 11:21:51 +07:00
0abf5618f7 fronted build js-cookiep lock 2025-10-05 09:42:10 +07:00
71fc7eee13 backend: Mod 2025-10-04 23:55:15 +07:00
82 changed files with 7401 additions and 7146 deletions

View File

@@ -1,4 +1,4 @@
[/dms] [/dms]
max_log = 496206 max_log = 498246
number = 3 number = 4
finish = 1 finish = 1

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

150
backend/src/db/sequelize.js Normal file → Executable file
View File

@@ -5,11 +5,7 @@
import { Sequelize } from "sequelize"; import { Sequelize } from "sequelize";
import { config } from "../config.js"; import { config } from "../config.js";
export const sequelize = new Sequelize( export const sequelize = new Sequelize(config.DB.NAME, config.DB.USER, config.DB.PASS, {
config.DB.NAME,
config.DB.USER,
config.DB.PASS,
{
host: config.DB.HOST, host: config.DB.HOST,
port: config.DB.PORT, port: config.DB.PORT,
dialect: "mariadb", dialect: "mariadb",
@@ -17,79 +13,91 @@ export const sequelize = new Sequelize(
dialectOptions: { timezone: "Z" }, dialectOptions: { timezone: "Z" },
define: { freezeTableName: true, underscored: false, timestamps: false }, define: { freezeTableName: true, underscored: false, timestamps: false },
pool: { max: 10, min: 0, idle: 10000 }, pool: { max: 10, min: 0, idle: 10000 },
} });
);
// --- 1. ประกาศตัวแปรสำหรับ Export Model ทั้งหมด --- // --- 1. ประกาศตัวแปรสำหรับ Export Model ทั้งหมด ---
export let User = null; export let User, Role, Permission, Organization, Project, UserRole, RolePermission,
export let Role = null; UserProjectRole, Correspondence, CorrespondenceVersion, Document, CorrDocumentMap,
export let Permission = null; Drawing, DrawingRevision, FileObject, RFA, RFARevision, RfaDrawingMap,
export let UserRole = null; Transmittal, TransmittalItem, Volume, ContractDwg, SubCategory;
export let RolePermission = null;
export let Project = null; // <-- เพิ่มเข้ามา
export let UserProjectRole = null; // <-- เพิ่มเข้ามา
if (process.env.ENABLE_SEQUELIZE === "1") { if (process.env.ENABLE_SEQUELIZE === "1") {
// --- 2. โหลดโมเดลทั้งหมดแบบ on-demand --- // --- 2. สร้าง Object ของ Models ทั้งหมดที่จะโหลด ---
const mdlUser = await import("./models/User.js").catch(() => null); const modelsToLoad = {
const mdlRole = await import("./models/Role.js").catch(() => null); User: await import("./models/User.js").catch(() => null),
const mdlPerm = await import("./models/Permission.js").catch(() => null); Role: await import("./models/Role.js").catch(() => null),
const mdlUR = await import("./models/UserRole.js").catch(() => null); Permission: await import("./models/Permission.js").catch(() => null),
const mdlRP = await import("./models/RolePermission.js").catch(() => null); Organization: await import("./models/Organization.js").catch(() => null),
const mdlProj = await import("./models/Project.js").catch(() => null); // <-- เพิ่มเข้ามา Project: await import("./models/Project.js").catch(() => null),
const mdlUPR = await import("./models/UserProjectRole.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),
};
// --- 3. Initialize Model ทั้งหมด --- // --- 3. Initialize Model ทั้งหมด ---
if (mdlUser?.default) User = mdlUser.default(sequelize); User = modelsToLoad.User?.default ? modelsToLoad.User.default(sequelize) : null;
if (mdlRole?.default) Role = mdlRole.default(sequelize); Role = modelsToLoad.Role?.default ? modelsToLoad.Role.default(sequelize) : null;
if (mdlPerm?.default) Permission = mdlPerm.default(sequelize); Permission = modelsToLoad.Permission?.default ? modelsToLoad.Permission.default(sequelize) : null;
if (mdlUR?.default) UserRole = mdlUR.default(sequelize); Organization = modelsToLoad.Organization?.default ? modelsToLoad.Organization.default(sequelize) : null;
if (mdlRP?.default) RolePermission = mdlRP.default(sequelize); Project = modelsToLoad.Project?.default ? modelsToLoad.Project.default(sequelize) : null;
if (mdlProj?.default) Project = mdlProj.default(sequelize); // <-- เพิ่มเข้ามา UserRole = modelsToLoad.UserRole?.default ? modelsToLoad.UserRole.default(sequelize) : null;
if (mdlUPR?.default) UserProjectRole = mdlUPR.default(sequelize); // <-- เพิ่มเข้ามา 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;
// --- 4. สร้างความสัมพันธ์ (Associations) ทั้งหมด ---
if (User && Role && Permission && UserRole && RolePermission) {
// ความสัมพันธ์ระดับระบบ (System-level)
User.belongsToMany(Role, { through: UserRole, foreignKey: "user_id" });
Role.belongsToMany(User, { through: UserRole, foreignKey: "role_id" });
Role.belongsToMany(Permission, {
through: RolePermission,
foreignKey: "role_id",
});
Permission.belongsToMany(Role, {
through: RolePermission,
foreignKey: "permission_id",
});
}
// ความสัมพันธ์ระดับโปรเจกต์ (Project-level) - ส่วนที่เพิ่มเข้ามา // --- 4. สร้างความสัมพันธ์ (Associations) ---
if (User && Project && Role && UserProjectRole) { const loadedModels = { User, Role, Permission, Organization, Project, UserRole, RolePermission,
// ทำให้ UserProjectRole เป็นตัวกลางเชื่อม 3 ตาราง UserProjectRole, Correspondence, CorrespondenceVersion, Document, CorrDocumentMap,
UserProjectRole.belongsTo(User, { foreignKey: "user_id" }); Drawing, DrawingRevision, FileObject, RFA, RFARevision, RfaDrawingMap,
UserProjectRole.belongsTo(Project, { foreignKey: "project_id" }); Transmittal, TransmittalItem, Volume, ContractDwg, SubCategory };
UserProjectRole.belongsTo(Role, { foreignKey: "role_id" });
for (const modelName in loadedModels) {
// ทำให้สามารถ include ข้อมูลที่เกี่ยวข้องได้ง่ายขึ้น if (loadedModels[modelName] && loadedModels[modelName].associate) {
User.hasMany(UserProjectRole, { foreignKey: "user_id" }); loadedModels[modelName].associate(loadedModels);
Project.hasMany(UserProjectRole, { foreignKey: "project_id" }); }
Role.hasMany(UserProjectRole, { foreignKey: "role_id" }); }
}
} }
/**
* ฟังก์ชันสำหรับทดสอบการเชื่อมต่อ Sequelize
*/
export async function dbReady() { export async function dbReady() {
if (process.env.ENABLE_SEQUELIZE !== "1") { if (process.env.ENABLE_SEQUELIZE !== "1") {
console.log("Sequelize is disabled."); console.log("Sequelize is disabled.");
return Promise.resolve(); return Promise.resolve();
} }
try { try {
await sequelize.authenticate(); await sequelize.authenticate();
console.log("Sequelize connection has been established successfully."); console.log("Sequelize connection has been established successfully.");
} catch (error) { } catch (error) {
console.error("Unable to connect to the database via Sequelize:", error); console.error("Unable to connect to the database via Sequelize:", error);
return Promise.reject(error); return Promise.reject(error);
} }
} }

View File

@@ -0,0 +1,61 @@
// FILE: backend/src/middleware/auth.js
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,
});
}
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;
}
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" });
try {
req.user = jwt.verify(token, config.JWT.SECRET);
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);
}

0
backend/src/middleware/auth.js Normal file → Executable file
View File

6
backend/src/middleware/index.js Normal file → Executable file
View File

@@ -1,7 +1,7 @@
// File: backend/src/middleware/index.js // File: backend/src/middleware/index.js
import * as authJwt from "./authJwt.js";
import * as abac from "./abac.js"; import * as abac from "./abac.js";
import * as auth from "./auth.js"; import * as auth from "./auth.js";
import * as authJwt from "./authJwt.js";
import * as errorHandler from "./errorHandler.js"; import * as errorHandler from "./errorHandler.js";
import * as loadPrincipal from "./loadPrincipal.js"; import * as loadPrincipal from "./loadPrincipal.js";
import * as permGuard from "./permGuard.js"; import * as permGuard from "./permGuard.js";
@@ -12,9 +12,9 @@ import * as requirePerm from "./requirePerm.js";
// Export ทุกอย่างออกมาเป็น named exports // Export ทุกอย่างออกมาเป็น named exports
// เพื่อให้สามารถ import แบบ `import { authJwt, permGuard } from '../middleware';` ได้ // เพื่อให้สามารถ import แบบ `import { authJwt, permGuard } from '../middleware';` ได้
export { export {
authJwt,
abac, abac,
auth, auth,
authJwt,
errorHandler, errorHandler,
loadPrincipal, loadPrincipal,
permGuard, permGuard,
@@ -25,9 +25,9 @@ export {
// (Optional) สร้าง default export สำหรับกรณีที่ต้องการ import ทั้งหมดใน object เดียว // (Optional) สร้าง default export สำหรับกรณีที่ต้องการ import ทั้งหมดใน object เดียว
const middleware = { const middleware = {
authJwt,
abac, abac,
auth, auth,
authJwt,
errorHandler, errorHandler,
loadPrincipal, loadPrincipal,
permGuard, permGuard,

0
backend/src/middleware/loadPrincipal.js Normal file → Executable file
View File

74
backend/src/routes/dashboard.js Normal file → Executable file
View File

@@ -1,23 +1,63 @@
import { Router } from "express"; // backend/src/routes/dashboard.js
import { User } from "../db/index.js"; import { Router } from 'express';
import { authJwt } from "../middleware/index.js"; import { Op } from 'sequelize';
// 1. Import Middleware ที่ถูกต้อง
import { authJwt } from '../middleware/authJwt.js';
import { loadPrincipalMw } from '../middleware/loadPrincipal.js';
// 2. Import Sequelize Models จาก `sequelize.js` ไม่ใช่ `index.js`
import { Correspondence, Document, RFA, User } from '../db/sequelize.js';
const router = Router(); const router = Router();
router.use(authJwt.verifyToken); // 3. ใช้ Middleware Chain ที่ถูกต้อง 100%
router.use(authJwt(), loadPrincipalMw());
router.get("/users/summary", async (req, res, next) => {
try { // === API สำหรับ User Management Widget ===
const totalUsers = await User.count(); router.get('/users/summary', async (req, res, next) => {
const activeUsers = await User.count({ where: { is_active: true } }); try {
res.json({ // ตรวจสอบว่า Model ถูกโหลดแล้วหรือยัง (จำเป็นสำหรับโหมด lazy-load)
total: totalUsers, if (!User) {
active: activeUsers, return res.status(503).json({ message: 'Database models not available. Is ENABLE_SEQUELIZE=1 set?' });
inactive: totalUsers - activeUsers, }
}); const totalUsers = await User.count();
} catch (error) { const activeUsers = await User.count({ where: { is_active: true } });
next(error);
} res.json({
total: totalUsers,
active: activeUsers,
inactive: totalUsers - activeUsers,
});
} catch (error) {
next(error);
}
}); });
export default router;
// === API อื่นๆ สำหรับ Dashboard ที่เราคุยกันไว้ก่อนหน้า ===
router.get('/stats', async (req, res, next) => {
try {
if (!Document || !RFA) {
return res.status(503).json({ message: 'Database models not available. Is ENABLE_SEQUELIZE=1 set?' });
}
const sevenDaysAgo = new Date(new Date().setDate(new Date().getDate() - 7));
const totalDocuments = await Document.count();
const newThisWeek = await Document.count({ where: { createdAt: { [Op.gte]: sevenDaysAgo } } });
const pendingRfas = await RFA.count({ where: { status: 'pending' } }); // สมมติตาม status
res.json({
totalDocuments,
newThisWeek,
pendingRfas
});
} catch (error) {
next(error);
}
});
export default router;

130
backend/src/routes/rbac_admin.js Normal file → Executable file
View File

@@ -1,144 +1,88 @@
// FILE: backend/src/routes/rbac_admin.js // FILE: backend/src/routes/rbac_admin.js
import { Router } from "express"; import { Router } from "express";
import { Role, Permission, UserProjectRole, Project } from "../db/sequelize.js"; import { Role, Permission, UserProjectRole, Project } from "../db/sequelize.js";
import { authJwt, permGuard } from "../middleware/index.js"; import { authJwt } from "../middleware/authJwt.js";
import { loadPrincipalMw } from "../middleware/loadPrincipal.js"; // แก้ไข: import ให้ถูกต้อง
import { requirePerm } from "../middleware/requirePerm.js";
const router = Router(); const router = Router();
// กำหนดให้ทุก route ในไฟล์นี้ต้องมีสิทธิ์ 'manage_rbac' // Middleware Chain ที่ถูกต้อง 100% ตามสถาปัตยกรรมของคุณ
router.use(authJwt.verifyToken, permGuard("manage_rbac")); router.use(authJwt(), loadPrincipalMw());
// == ROLES Management == // == ROLES Management ==
router.get("/roles", async (req, res, next) => { router.get("/roles", requirePerm("roles.manage"), async (req, res, next) => {
try { try {
const roles = await Role.findAll({ const roles = await Role.findAll({
include: [ include: [{ model: Permission, attributes: ["id", "name"], through: { attributes: [] } }],
{
model: Permission,
attributes: ["id", "name"],
through: { attributes: [] },
},
],
order: [["name", "ASC"]], order: [["name", "ASC"]],
}); });
res.json(roles); res.json(roles);
} catch (error) { } catch (error) { next(error); }
next(error);
}
}); });
router.post("/roles", async (req, res, next) => { router.post("/roles", requirePerm("roles.manage"), async (req, res, next) => {
const { name, description } = req.body;
if (!name) return res.status(400).json({ message: "Role name is required." });
try { try {
const { name, description } = req.body;
if (!name)
return res.status(400).json({ message: "Role name is required." });
const newRole = await Role.create({ name, description }); const newRole = await Role.create({ name, description });
res.status(201).json(newRole); res.status(201).json(newRole);
} catch (error) { } catch (error) {
if (error.name === "SequelizeUniqueConstraintError") { if (error.name === "SequelizeUniqueConstraintError") {
return res return res.status(409).json({ message: `Role '${name}' already exists.` });
.status(409)
.json({ message: `Role '${name}' already exists.` });
} }
next(error); next(error);
} }
}); });
router.put("/roles/:id/permissions", async (req, res, next) => { router.put("/roles/:id/permissions", requirePerm("roles.manage"), async (req, res, next) => {
try {
const { permissionIds } = req.body; const { permissionIds } = req.body;
if (!Array.isArray(permissionIds)) if (!Array.isArray(permissionIds)) return res.status(400).json({ message: "permissionIds must be an array." });
return res try {
.status(400) const role = await Role.findByPk(req.params.id);
.json({ message: "permissionIds must be an array." }); if (!role) return res.status(404).json({ message: "Role not found." });
await role.setPermissions(permissionIds);
const role = await Role.findByPk(req.params.id); const updatedRole = await Role.findByPk(req.params.id, {
if (!role) return res.status(404).json({ message: "Role not found." }); include: [{ model: Permission, attributes: ['id', 'name'], through: { attributes: [] } }]
});
await role.setPermissions(permissionIds); res.json(updatedRole);
const updatedRole = await Role.findByPk(req.params.id, { } catch (error) { next(error); }
include: [
{
model: Permission,
attributes: ["id", "name"],
through: { attributes: [] },
},
],
});
res.json(updatedRole);
} catch (error) {
next(error);
}
});
// == PERMISSIONS Management ==
router.get("/permissions", async (req, res, next) => {
try {
const permissions = await Permission.findAll({ order: [["name", "ASC"]] });
res.json(permissions);
} catch (error) {
next(error);
}
}); });
// == USER-PROJECT-ROLES Management == // == USER-PROJECT-ROLES Management ==
router.get("/user-project-roles", async (req, res, next) => { router.get("/user-project-roles", requirePerm("users.manage"), async (req, res, next) => {
const { userId } = req.query; const { userId } = req.query;
if (!userId) if (!userId) return res.status(400).json({ message: "userId query parameter is required." });
return res
.status(400)
.json({ message: "userId query parameter is required." });
try { try {
const assignments = await UserProjectRole.findAll({ const assignments = await UserProjectRole.findAll({
where: { user_id: userId }, where: { user_id: userId },
include: [ include: [ { model: Project, attributes: ["id", "name"] }, { model: Role, attributes: ["id", "name"] } ],
{ model: Project, attributes: ["id", "name"] },
{ model: Role, attributes: ["id", "name"] },
],
}); });
res.json(assignments); res.json(assignments);
} catch (error) { } catch (error) { next(error); }
next(error);
}
}); });
router.post("/user-project-roles", async (req, res, next) => { router.post("/user-project-roles", requirePerm("users.manage"), async (req, res, next) => {
const { userId, projectId, roleId } = req.body; const { userId, projectId, roleId } = req.body;
if (!userId || !projectId || !roleId) if (!userId || !projectId || !roleId) return res.status(400).json({ message: "userId, projectId, and roleId are required." });
return res
.status(400)
.json({ message: "userId, projectId, and roleId are required." });
try { try {
const [assignment, created] = await UserProjectRole.findOrCreate({ const [assignment, created] = await UserProjectRole.findOrCreate({
where: { user_id: userId, project_id: projectId, role_id: roleId }, where: { user_id: userId, project_id: projectId, role_id: roleId },
defaults: { user_id: userId, project_id: projectId, role_id: roleId }, defaults: { user_id: userId, project_id: projectId, role_id: roleId },
}); });
if (!created) if (!created) return res.status(409).json({ message: "This assignment already exists." });
return res
.status(409)
.json({ message: "This assignment already exists." });
res.status(201).json(assignment); res.status(201).json(assignment);
} catch (error) { } catch (error) { next(error); }
next(error);
}
}); });
router.delete("/user-project-roles", async (req, res, next) => { router.delete("/user-project-roles", requirePerm("users.manage"), async (req, res, next) => {
const { userId, projectId, roleId } = req.body; const { userId, projectId, roleId } = req.body;
if (!userId || !projectId || !roleId) if (!userId || !projectId || !roleId) return res.status(400).json({ message: "userId, projectId, and roleId are required." });
return res
.status(400)
.json({ message: "userId, projectId, and roleId are required." });
try { try {
const deletedCount = await UserProjectRole.destroy({ const deletedCount = await UserProjectRole.destroy({ where: { user_id: userId, project_id: projectId, role_id: roleId } });
where: { user_id: userId, project_id: projectId, role_id: roleId }, if (deletedCount === 0) return res.status(404).json({ message: 'Assignment not found.' });
});
if (deletedCount === 0)
return res.status(404).json({ message: "Assignment not found." });
res.status(204).send(); res.status(204).send();
} catch (error) { } catch (error) { next(error); }
next(error);
}
}); });
export default router; export default router;

205
backend/src/routes/users.js Normal file → Executable file
View File

@@ -1,136 +1,99 @@
// File: backend/src/routes/users.js // File: backend/src/routes/users.js
import { Router } from "express"; import { Router } from 'express';
import { User, Role } from "../db/sequelize.js"; import { User, Role } from '../db/sequelize.js';
import { authJwt, permGuard } from "../middleware/index.js"; import { authJwt } from "../middleware/authJwt.js";
import { hashPassword } from "../utils/passwords.js"; import { loadPrincipalMw } from "../middleware/loadPrincipal.js"; // แก้ไข: import ให้ถูกต้อง
import { requirePerm } from '../middleware/requirePerm.js';
import { hashPassword } from '../utils/passwords.js';
const router = Router(); const router = Router();
// Middleware สำหรับทุก route ในไฟล์นี้ // Middleware Chain ที่ถูกต้อง 100%
router.use(authJwt.verifyToken); router.use(authJwt(), loadPrincipalMw());
// GET /api/users - ดึงรายชื่อผู้ใช้ทั้งหมด // GET /api/users
router.get( router.get('/', requirePerm('users.view'), async (req, res, next) => {
"/",
permGuard("manage_users"), // ตรวจสอบสิทธิ์
async (req, res, next) => {
try { try {
const users = await User.findAll({ const users = await User.findAll({
attributes: { exclude: ["password_hash"] }, // **สำคัญมาก: ห้ามส่ง password hash ออกไป** attributes: { exclude: ['password_hash'] },
include: [ include: [{ model: Role, attributes: ['id', 'name'], through: { attributes: [] } }],
{ order: [['username', 'ASC']]
model: Role, });
attributes: ["id", "name"], res.json(users);
through: { attributes: [] }, // ไม่ต้องเอาข้อมูลจากตาราง join (UserRoles) มา } catch (error) { next(error); }
}, });
],
order: [["username", "ASC"]], // POST /api/users
}); router.post('/', requirePerm('users.manage'), async (req, res, next) => {
res.json(users); 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' });
}
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,
});
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) { } catch (error) {
next(error); if (error.name === 'SequelizeUniqueConstraintError') {
return res.status(409).json({ message: 'Username or email already exists.' });
}
next(error);
} }
}
);
// POST /api/users - สร้างผู้ใช้ใหม่
router.post("/", permGuard("manage_users"), 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" });
}
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,
});
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 - อัปเดตข้อมูลผู้ใช้ // PUT /api/users/:id
router.put("/:id", permGuard("manage_users"), async (req, res, next) => { router.put('/:id', requirePerm('users.manage'), async (req, res, next) => {
const { id } = req.params; const { id } = req.params;
const { email, first_name, last_name, is_active, roles } = req.body; 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();
try { if (roles) {
const user = await User.findByPk(id); await user.setRoles(roles);
if (!user) { }
return res.status(404).json({ message: "User not found" }); const updatedUser = await User.findByPk(id, {
} attributes: { exclude: ['password_hash'] },
include: [{ model: Role, attributes: ['id', 'name'], through: { attributes: [] } }]
user.email = email ?? user.email; });
user.first_name = first_name ?? user.first_name; res.json(updatedUser);
user.last_name = last_name ?? user.last_name; } catch (error) { next(error); }
user.is_active = is_active ?? user.is_active;
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 - ลบผู้ใช้ (Soft Delete) // DELETE /api/users/:id
router.delete("/:id", permGuard("manage_users"), async (req, res, next) => { router.delete('/:id', requirePerm('users.manage'), async (req, res, next) => {
try { try {
const user = await User.findByPk(req.params.id); const user = await User.findByPk(req.params.id);
if (!user) { if (!user) {
return res.status(404).json({ message: "User not found" }); return res.status(404).json({ message: 'User not found' });
} }
user.is_active = false; // Soft Delete user.is_active = false;
await user.save(); user.updated_by = req.principal.user_id;
res.status(204).send(); await user.save();
} catch (error) { res.status(204).send();
next(error); } catch (error) { next(error); }
}
}); });
export default router; export default router;

View File

@@ -0,0 +1,22 @@
{
"$schema": "https://ui.shadcn.com/schema.json",
"style": "new-york",
"rsc": true,
"tsx": false,
"tailwind": {
"config": "tailwind.config.js",
"css": "app/globals.css",
"baseColor": "neutral",
"cssVariables": true,
"prefix": ""
},
"iconLibrary": "lucide",
"aliases": {
"components": "@/components",
"utils": "@/lib/utils",
"ui": "@/components/ui",
"lib": "@/lib",
"hooks": "@/hooks"
},
"registries": {}
}

View File

@@ -0,0 +1,99 @@
"use client"
import * as React from "react"
import * as AlertDialogPrimitive from "@radix-ui/react-alert-dialog"
import { cn } from "@/lib/utils"
import { buttonVariants } from "@/components/ui/button"
const AlertDialog = AlertDialogPrimitive.Root
const AlertDialogTrigger = AlertDialogPrimitive.Trigger
const AlertDialogPortal = AlertDialogPrimitive.Portal
const AlertDialogOverlay = React.forwardRef(({ className, ...props }, ref) => (
<AlertDialogPrimitive.Overlay
className={cn(
"fixed inset-0 z-50 bg-black/80 data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0",
className
)}
{...props}
ref={ref} />
))
AlertDialogOverlay.displayName = AlertDialogPrimitive.Overlay.displayName
const AlertDialogContent = React.forwardRef(({ className, ...props }, ref) => (
<AlertDialogPortal>
<AlertDialogOverlay />
<AlertDialogPrimitive.Content
ref={ref}
className={cn(
"fixed left-[50%] top-[50%] z-50 grid w-full max-w-lg translate-x-[-50%] translate-y-[-50%] gap-4 border bg-background p-6 shadow-lg duration-200 data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[state=closed]:slide-out-to-left-1/2 data-[state=closed]:slide-out-to-top-[48%] data-[state=open]:slide-in-from-left-1/2 data-[state=open]:slide-in-from-top-[48%] sm:rounded-lg",
className
)}
{...props} />
</AlertDialogPortal>
))
AlertDialogContent.displayName = AlertDialogPrimitive.Content.displayName
const AlertDialogHeader = ({
className,
...props
}) => (
<div
className={cn("flex flex-col space-y-2 text-center sm:text-left", className)}
{...props} />
)
AlertDialogHeader.displayName = "AlertDialogHeader"
const AlertDialogFooter = ({
className,
...props
}) => (
<div
className={cn("flex flex-col-reverse sm:flex-row sm:justify-end sm:space-x-2", className)}
{...props} />
)
AlertDialogFooter.displayName = "AlertDialogFooter"
const AlertDialogTitle = React.forwardRef(({ className, ...props }, ref) => (
<AlertDialogPrimitive.Title ref={ref} className={cn("text-lg font-semibold", className)} {...props} />
))
AlertDialogTitle.displayName = AlertDialogPrimitive.Title.displayName
const AlertDialogDescription = React.forwardRef(({ className, ...props }, ref) => (
<AlertDialogPrimitive.Description
ref={ref}
className={cn("text-sm text-muted-foreground", className)}
{...props} />
))
AlertDialogDescription.displayName =
AlertDialogPrimitive.Description.displayName
const AlertDialogAction = React.forwardRef(({ className, ...props }, ref) => (
<AlertDialogPrimitive.Action ref={ref} className={cn(buttonVariants(), className)} {...props} />
))
AlertDialogAction.displayName = AlertDialogPrimitive.Action.displayName
const AlertDialogCancel = React.forwardRef(({ className, ...props }, ref) => (
<AlertDialogPrimitive.Cancel
ref={ref}
className={cn(buttonVariants({ variant: "outline" }), "mt-2 sm:mt-0", className)}
{...props} />
))
AlertDialogCancel.displayName = AlertDialogPrimitive.Cancel.displayName
export {
AlertDialog,
AlertDialogPortal,
AlertDialogOverlay,
AlertDialogTrigger,
AlertDialogContent,
AlertDialogHeader,
AlertDialogFooter,
AlertDialogTitle,
AlertDialogDescription,
AlertDialogAction,
AlertDialogCancel,
}

View File

@@ -0,0 +1,24 @@
"use client"
import * as React from "react"
import * as CheckboxPrimitive from "@radix-ui/react-checkbox"
import { Check } from "lucide-react"
import { cn } from "@/lib/utils"
const Checkbox = React.forwardRef(({ className, ...props }, ref) => (
<CheckboxPrimitive.Root
ref={ref}
className={cn(
"peer h-4 w-4 shrink-0 rounded-sm border border-primary shadow focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:cursor-not-allowed disabled:opacity-50 data-[state=checked]:bg-primary data-[state=checked]:text-primary-foreground",
className
)}
{...props}>
<CheckboxPrimitive.Indicator className={cn("flex items-center justify-center text-current")}>
<Check className="h-4 w-4" />
</CheckboxPrimitive.Indicator>
</CheckboxPrimitive.Root>
))
Checkbox.displayName = CheckboxPrimitive.Root.displayName
export { Checkbox }

View File

@@ -0,0 +1,96 @@
"use client"
import * as React from "react"
import * as DialogPrimitive from "@radix-ui/react-dialog"
import { X } from "lucide-react"
import { cn } from "@/lib/utils"
const Dialog = DialogPrimitive.Root
const DialogTrigger = DialogPrimitive.Trigger
const DialogPortal = DialogPrimitive.Portal
const DialogClose = DialogPrimitive.Close
const DialogOverlay = React.forwardRef(({ className, ...props }, ref) => (
<DialogPrimitive.Overlay
ref={ref}
className={cn(
"fixed inset-0 z-50 bg-black/80 data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0",
className
)}
{...props} />
))
DialogOverlay.displayName = DialogPrimitive.Overlay.displayName
const DialogContent = React.forwardRef(({ className, children, ...props }, ref) => (
<DialogPortal>
<DialogOverlay />
<DialogPrimitive.Content
ref={ref}
className={cn(
"fixed left-[50%] top-[50%] z-50 grid w-full max-w-lg translate-x-[-50%] translate-y-[-50%] gap-4 border bg-background p-6 shadow-lg duration-200 data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[state=closed]:slide-out-to-left-1/2 data-[state=closed]:slide-out-to-top-[48%] data-[state=open]:slide-in-from-left-1/2 data-[state=open]:slide-in-from-top-[48%] sm:rounded-lg",
className
)}
{...props}>
{children}
<DialogPrimitive.Close
className="absolute right-4 top-4 rounded-sm opacity-70 ring-offset-background transition-opacity hover:opacity-100 focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2 disabled:pointer-events-none data-[state=open]:bg-accent data-[state=open]:text-muted-foreground">
<X className="h-4 w-4" />
<span className="sr-only">Close</span>
</DialogPrimitive.Close>
</DialogPrimitive.Content>
</DialogPortal>
))
DialogContent.displayName = DialogPrimitive.Content.displayName
const DialogHeader = ({
className,
...props
}) => (
<div
className={cn("flex flex-col space-y-1.5 text-center sm:text-left", className)}
{...props} />
)
DialogHeader.displayName = "DialogHeader"
const DialogFooter = ({
className,
...props
}) => (
<div
className={cn("flex flex-col-reverse sm:flex-row sm:justify-end sm:space-x-2", className)}
{...props} />
)
DialogFooter.displayName = "DialogFooter"
const DialogTitle = React.forwardRef(({ className, ...props }, ref) => (
<DialogPrimitive.Title
ref={ref}
className={cn("text-lg font-semibold leading-none tracking-tight", className)}
{...props} />
))
DialogTitle.displayName = DialogPrimitive.Title.displayName
const DialogDescription = React.forwardRef(({ className, ...props }, ref) => (
<DialogPrimitive.Description
ref={ref}
className={cn("text-sm text-muted-foreground", className)}
{...props} />
))
DialogDescription.displayName = DialogPrimitive.Description.displayName
export {
Dialog,
DialogPortal,
DialogOverlay,
DialogTrigger,
DialogClose,
DialogContent,
DialogHeader,
DialogFooter,
DialogTitle,
DialogDescription,
}

View File

@@ -0,0 +1,40 @@
"use client"
import * as React from "react"
import * as ScrollAreaPrimitive from "@radix-ui/react-scroll-area"
import { cn } from "@/lib/utils"
const ScrollArea = React.forwardRef(({ className, children, ...props }, ref) => (
<ScrollAreaPrimitive.Root
ref={ref}
className={cn("relative overflow-hidden", className)}
{...props}>
<ScrollAreaPrimitive.Viewport className="h-full w-full rounded-[inherit]">
{children}
</ScrollAreaPrimitive.Viewport>
<ScrollBar />
<ScrollAreaPrimitive.Corner />
</ScrollAreaPrimitive.Root>
))
ScrollArea.displayName = ScrollAreaPrimitive.Root.displayName
const ScrollBar = React.forwardRef(({ className, orientation = "vertical", ...props }, ref) => (
<ScrollAreaPrimitive.ScrollAreaScrollbar
ref={ref}
orientation={orientation}
className={cn(
"flex touch-none select-none transition-colors",
orientation === "vertical" &&
"h-full w-2.5 border-l border-l-transparent p-[1px]",
orientation === "horizontal" &&
"h-2.5 flex-col border-t border-t-transparent p-[1px]",
className
)}
{...props}>
<ScrollAreaPrimitive.ScrollAreaThumb className="relative flex-1 rounded-full bg-border" />
</ScrollAreaPrimitive.ScrollAreaScrollbar>
))
ScrollBar.displayName = ScrollAreaPrimitive.ScrollAreaScrollbar.displayName
export { ScrollArea, ScrollBar }

38
frontend/lib/auth copy.js Executable file
View File

@@ -0,0 +1,38 @@
// frontend/lib/auth.js
import { cookies } from "next/headers";
const COOKIE_NAME = "access_token";
/**
* Server-side session fetcher (ใช้ใน Server Components/Layouts)
* - อ่านคุกกี้แบบ async: await cookies()
* - ถ้าไม่มี token → return null
* - ถ้ามี → เรียก /api/auth/me ที่ backend เพื่อตรวจสอบ
*/
export async function getSession() {
// ✅ ต้อง await
const cookieStore = await cookies();
const token = cookieStore.get(COOKIE_NAME)?.value;
if (!token) return null;
// เรียก backend ตรวจ session (ปรับ endpoint ให้ตรงของคุณ)
const res = await fetch(`${process.env.NEXT_PUBLIC_API_BASE}/api/auth/me`, {
// ส่งต่อคุกกี้ไป backend (เลือกอย่างใดอย่างหนึ่ง)
// วิธี A: ส่ง header Cookie โดยตรง
headers: { Cookie: `${COOKIE_NAME}=${token}` },
// วิธี B: ถ้า proxy ผ่าน nginx ในโดเมนเดียวกัน ใช้ credentials รวมคุกกี้อัตโนมัติได้
// credentials: "include",
cache: "no-store",
});
if (!res.ok) return null;
const data = await res.json();
// คาดหวังโครงสร้าง { user, permissions } จาก backend
return {
user: data.user,
permissions: data.permissions || [],
token,
};
}

View File

@@ -8,9 +8,13 @@
"name": "dms-frontend", "name": "dms-frontend",
"version": "0.7.0", "version": "0.7.0",
"dependencies": { "dependencies": {
"@radix-ui/react-alert-dialog": "^1.1.15",
"@radix-ui/react-checkbox": "^1.3.3",
"@radix-ui/react-dialog": "^1.1.15",
"@radix-ui/react-dropdown-menu": "^2.1.16", "@radix-ui/react-dropdown-menu": "^2.1.16",
"@radix-ui/react-label": "^2.1.7", "@radix-ui/react-label": "^2.1.7",
"@radix-ui/react-progress": "^1.1.7", "@radix-ui/react-progress": "^1.1.7",
"@radix-ui/react-scroll-area": "^1.2.10",
"@radix-ui/react-slot": "^1.2.3", "@radix-ui/react-slot": "^1.2.3",
"@radix-ui/react-switch": "^1.2.6", "@radix-ui/react-switch": "^1.2.6",
"@radix-ui/react-tabs": "^1.1.13", "@radix-ui/react-tabs": "^1.1.13",
@@ -950,12 +954,46 @@
"node": ">=14" "node": ">=14"
} }
}, },
"node_modules/@radix-ui/number": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/@radix-ui/number/-/number-1.1.1.tgz",
"integrity": "sha512-MkKCwxlXTgz6CFoJx3pCwn07GKp36+aZyu/u2Ln2VrA5DcdyCZkASEDBTd8x5whTQQL5CiYf4prXKLcgQdv29g==",
"license": "MIT"
},
"node_modules/@radix-ui/primitive": { "node_modules/@radix-ui/primitive": {
"version": "1.1.3", "version": "1.1.3",
"resolved": "https://registry.npmjs.org/@radix-ui/primitive/-/primitive-1.1.3.tgz", "resolved": "https://registry.npmjs.org/@radix-ui/primitive/-/primitive-1.1.3.tgz",
"integrity": "sha512-JTF99U/6XIjCBo0wqkU5sK10glYe27MRRsfwoiq5zzOEZLHU3A3KCMa5X/azekYRCJ0HlwI0crAXS/5dEHTzDg==", "integrity": "sha512-JTF99U/6XIjCBo0wqkU5sK10glYe27MRRsfwoiq5zzOEZLHU3A3KCMa5X/azekYRCJ0HlwI0crAXS/5dEHTzDg==",
"license": "MIT" "license": "MIT"
}, },
"node_modules/@radix-ui/react-alert-dialog": {
"version": "1.1.15",
"resolved": "https://registry.npmjs.org/@radix-ui/react-alert-dialog/-/react-alert-dialog-1.1.15.tgz",
"integrity": "sha512-oTVLkEw5GpdRe29BqJ0LSDFWI3qu0vR1M0mUkOQWDIUnY/QIkLpgDMWuKxP94c2NAC2LGcgVhG1ImF3jkZ5wXw==",
"license": "MIT",
"dependencies": {
"@radix-ui/primitive": "1.1.3",
"@radix-ui/react-compose-refs": "1.1.2",
"@radix-ui/react-context": "1.1.2",
"@radix-ui/react-dialog": "1.1.15",
"@radix-ui/react-primitive": "2.1.3",
"@radix-ui/react-slot": "1.2.3"
},
"peerDependencies": {
"@types/react": "*",
"@types/react-dom": "*",
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
"react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
},
"peerDependenciesMeta": {
"@types/react": {
"optional": true
},
"@types/react-dom": {
"optional": true
}
}
},
"node_modules/@radix-ui/react-arrow": { "node_modules/@radix-ui/react-arrow": {
"version": "1.1.7", "version": "1.1.7",
"resolved": "https://registry.npmjs.org/@radix-ui/react-arrow/-/react-arrow-1.1.7.tgz", "resolved": "https://registry.npmjs.org/@radix-ui/react-arrow/-/react-arrow-1.1.7.tgz",
@@ -979,6 +1017,36 @@
} }
} }
}, },
"node_modules/@radix-ui/react-checkbox": {
"version": "1.3.3",
"resolved": "https://registry.npmjs.org/@radix-ui/react-checkbox/-/react-checkbox-1.3.3.tgz",
"integrity": "sha512-wBbpv+NQftHDdG86Qc0pIyXk5IR3tM8Vd0nWLKDcX8nNn4nXFOFwsKuqw2okA/1D/mpaAkmuyndrPJTYDNZtFw==",
"license": "MIT",
"dependencies": {
"@radix-ui/primitive": "1.1.3",
"@radix-ui/react-compose-refs": "1.1.2",
"@radix-ui/react-context": "1.1.2",
"@radix-ui/react-presence": "1.1.5",
"@radix-ui/react-primitive": "2.1.3",
"@radix-ui/react-use-controllable-state": "1.2.2",
"@radix-ui/react-use-previous": "1.1.1",
"@radix-ui/react-use-size": "1.1.1"
},
"peerDependencies": {
"@types/react": "*",
"@types/react-dom": "*",
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
"react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
},
"peerDependenciesMeta": {
"@types/react": {
"optional": true
},
"@types/react-dom": {
"optional": true
}
}
},
"node_modules/@radix-ui/react-collection": { "node_modules/@radix-ui/react-collection": {
"version": "1.1.7", "version": "1.1.7",
"resolved": "https://registry.npmjs.org/@radix-ui/react-collection/-/react-collection-1.1.7.tgz", "resolved": "https://registry.npmjs.org/@radix-ui/react-collection/-/react-collection-1.1.7.tgz",
@@ -1035,6 +1103,42 @@
} }
} }
}, },
"node_modules/@radix-ui/react-dialog": {
"version": "1.1.15",
"resolved": "https://registry.npmjs.org/@radix-ui/react-dialog/-/react-dialog-1.1.15.tgz",
"integrity": "sha512-TCglVRtzlffRNxRMEyR36DGBLJpeusFcgMVD9PZEzAKnUs1lKCgX5u9BmC2Yg+LL9MgZDugFFs1Vl+Jp4t/PGw==",
"license": "MIT",
"dependencies": {
"@radix-ui/primitive": "1.1.3",
"@radix-ui/react-compose-refs": "1.1.2",
"@radix-ui/react-context": "1.1.2",
"@radix-ui/react-dismissable-layer": "1.1.11",
"@radix-ui/react-focus-guards": "1.1.3",
"@radix-ui/react-focus-scope": "1.1.7",
"@radix-ui/react-id": "1.1.1",
"@radix-ui/react-portal": "1.1.9",
"@radix-ui/react-presence": "1.1.5",
"@radix-ui/react-primitive": "2.1.3",
"@radix-ui/react-slot": "1.2.3",
"@radix-ui/react-use-controllable-state": "1.2.2",
"aria-hidden": "^1.2.4",
"react-remove-scroll": "^2.6.3"
},
"peerDependencies": {
"@types/react": "*",
"@types/react-dom": "*",
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
"react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
},
"peerDependenciesMeta": {
"@types/react": {
"optional": true
},
"@types/react-dom": {
"optional": true
}
}
},
"node_modules/@radix-ui/react-direction": { "node_modules/@radix-ui/react-direction": {
"version": "1.1.1", "version": "1.1.1",
"resolved": "https://registry.npmjs.org/@radix-ui/react-direction/-/react-direction-1.1.1.tgz", "resolved": "https://registry.npmjs.org/@radix-ui/react-direction/-/react-direction-1.1.1.tgz",
@@ -1385,6 +1489,37 @@
} }
} }
}, },
"node_modules/@radix-ui/react-scroll-area": {
"version": "1.2.10",
"resolved": "https://registry.npmjs.org/@radix-ui/react-scroll-area/-/react-scroll-area-1.2.10.tgz",
"integrity": "sha512-tAXIa1g3sM5CGpVT0uIbUx/U3Gs5N8T52IICuCtObaos1S8fzsrPXG5WObkQN3S6NVl6wKgPhAIiBGbWnvc97A==",
"license": "MIT",
"dependencies": {
"@radix-ui/number": "1.1.1",
"@radix-ui/primitive": "1.1.3",
"@radix-ui/react-compose-refs": "1.1.2",
"@radix-ui/react-context": "1.1.2",
"@radix-ui/react-direction": "1.1.1",
"@radix-ui/react-presence": "1.1.5",
"@radix-ui/react-primitive": "2.1.3",
"@radix-ui/react-use-callback-ref": "1.1.1",
"@radix-ui/react-use-layout-effect": "1.1.1"
},
"peerDependencies": {
"@types/react": "*",
"@types/react-dom": "*",
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
"react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
},
"peerDependenciesMeta": {
"@types/react": {
"optional": true
},
"@types/react-dom": {
"optional": true
}
}
},
"node_modules/@radix-ui/react-slot": { "node_modules/@radix-ui/react-slot": {
"version": "1.2.3", "version": "1.2.3",
"resolved": "https://registry.npmjs.org/@radix-ui/react-slot/-/react-slot-1.2.3.tgz", "resolved": "https://registry.npmjs.org/@radix-ui/react-slot/-/react-slot-1.2.3.tgz",

View File

@@ -10,9 +10,13 @@
"format": "prettier --write ." "format": "prettier --write ."
}, },
"dependencies": { "dependencies": {
"@radix-ui/react-alert-dialog": "^1.1.15",
"@radix-ui/react-checkbox": "^1.3.3",
"@radix-ui/react-dialog": "^1.1.15",
"@radix-ui/react-dropdown-menu": "^2.1.16", "@radix-ui/react-dropdown-menu": "^2.1.16",
"@radix-ui/react-label": "^2.1.7", "@radix-ui/react-label": "^2.1.7",
"@radix-ui/react-progress": "^1.1.7", "@radix-ui/react-progress": "^1.1.7",
"@radix-ui/react-scroll-area": "^1.2.10",
"@radix-ui/react-slot": "^1.2.3", "@radix-ui/react-slot": "^1.2.3",
"@radix-ui/react-switch": "^1.2.6", "@radix-ui/react-switch": "^1.2.6",
"@radix-ui/react-tabs": "^1.1.13", "@radix-ui/react-tabs": "^1.1.13",

View File

@@ -7,12 +7,12 @@ services:
user: "${UID:-1000}:${GID:-1000}" user: "${UID:-1000}:${GID:-1000}"
command: > command: >
sh -c " sh -c "
echo '📦 Installing dependencies...' && echo 'ߓ栉nstalling dependencies...' &&
npm install && npm install &&
echo '🎨 Initializing shadcn/ui...' && echo 'ߎ蠉nitializing shadcn/ui...' &&
npx shadcn@latest init -y -d && npx shadcn@latest init -y -d &&
echo '📥 Adding components...' && echo 'ߓ堁dding components...' &&
npx shadcn@latest add -y button label input card badge tabs progress dropdown-menu tooltip switch && npx shadcn@latest add -y alert-dialog dialog checkbox scroll-area button label input card badge tabs progress dropdown-menu tooltip switch &&
echo '✅ Done! Check components/ui/ directory' echo '✅ Done! Check components/ui/ directory'
" "

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.

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.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

View File

@@ -1,8 +0,0 @@
1
/var/lib/postgresql/data
1759373409
5432
/var/run/postgresql
*
117512122 0
ready

View File

Binary file not shown.

View File

@@ -1,99 +0,0 @@
# ------------------------------------------------------------
# git.np-dms.work
# ------------------------------------------------------------
map $scheme $hsts_header {
https "max-age=63072000; preload";
}
server {
set $forward_scheme http;
set $server "gitea";
set $port 3000;
listen 80;
listen [::]:80;
listen 443 ssl;
listen [::]:443 ssl;
server_name git.np-dms.work;
http2 on;
# Let's Encrypt SSL
include conf.d/include/letsencrypt-acme-challenge.conf;
include conf.d/include/ssl-cache.conf;
include conf.d/include/ssl-ciphers.conf;
ssl_certificate /etc/letsencrypt/live/npm-10/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/npm-10/privkey.pem;
# Force SSL
include conf.d/include/force-ssl.conf;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection $http_connection;
proxy_http_version 1.1;
access_log /data/logs/proxy-host-5_access.log proxy;
error_log /data/logs/proxy-host-5_error.log warn;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_set_header Authorization $http_authorization;
proxy_connect_timeout 30s;
proxy_send_timeout 300s;
proxy_read_timeout 300s;
send_timeout 300s;
client_max_body_size 512m;
location / {
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection $http_connection;
proxy_http_version 1.1;
# Proxy!
include conf.d/include/proxy.conf;
}
# Custom
include /data/nginx/custom/server_proxy[.]conf;
}

View File

@@ -1,5 +1,5 @@
# ------------------------------------------------------------ # ------------------------------------------------------------
# pma.np-dms.work # git.np-dms.work
# ------------------------------------------------------------ # ------------------------------------------------------------
@@ -10,8 +10,8 @@ map $scheme $hsts_header {
server { server {
set $forward_scheme http; set $forward_scheme http;
set $server "phpmyadmin"; set $server "gitea";
set $port 80; set $port 3000;
listen 80; listen 80;
listen [::]:80; listen [::]:80;
@@ -20,16 +20,17 @@ listen 443 ssl;
listen [::]:443 ssl; listen [::]:443 ssl;
server_name pma.np-dms.work; server_name git.np-dms.work;
http2 off;
http2 on;
# Let's Encrypt SSL # Let's Encrypt SSL
include conf.d/include/letsencrypt-acme-challenge.conf; include conf.d/include/letsencrypt-acme-challenge.conf;
include conf.d/include/ssl-cache.conf; include conf.d/include/ssl-cache.conf;
include conf.d/include/ssl-ciphers.conf; include conf.d/include/ssl-ciphers.conf;
ssl_certificate /etc/letsencrypt/live/npm-11/fullchain.pem; ssl_certificate /etc/letsencrypt/live/npm-10/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/npm-11/privkey.pem; ssl_certificate_key /etc/letsencrypt/live/npm-10/privkey.pem;
@@ -45,28 +46,31 @@ http2 off;
# Force SSL
include conf.d/include/force-ssl.conf;
proxy_set_header Upgrade $http_upgrade; proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection $http_connection; proxy_set_header Connection $http_connection;
proxy_http_version 1.1; proxy_http_version 1.1;
access_log /data/logs/proxy-host-6_access.log proxy; access_log /data/logs/proxy-host-5_access.log proxy;
error_log /data/logs/proxy-host-6_error.log warn; error_log /data/logs/proxy-host-5_error.log warn;
client_max_body_size 128m;
add_header X-Frame-Options "SAMEORIGIN" always;
add_header X-Content-Type-Options "nosniff" always;
add_header Referrer-Policy "strict-origin-when-cross-origin" always;
add_header Permissions-Policy "camera=(), microphone=(), geolocation=()" always;
proxy_set_header Host $host; proxy_set_header Host $host;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_set_header Authorization $http_authorization;
proxy_buffering off; proxy_connect_timeout 30s;
proxy_send_timeout 300s;
proxy_read_timeout 300s;
send_timeout 300s;
client_max_body_size 512m;
@@ -80,6 +84,8 @@ proxy_buffering off;
proxy_set_header Upgrade $http_upgrade; proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection $http_connection; proxy_set_header Connection $http_connection;

View File

@@ -1 +0,0 @@
PMA_token |s:32:"6537407a672f376761784c234d79666a"; HMAC_secret |s:16:";_tF,Ps<jX+#Z=f>";errors|a:0:{}

View File

@@ -1 +0,0 @@
PMA_token |s:32:"34347471654523645a3b4a32556f6c2c"; HMAC_secret |s:16:"cxqQ@%S|9[H3Y6^%";errors|a:0:{}

View File

@@ -1 +0,0 @@
PMA_token |s:32:"263a746d69672f6a57305b5842603462"; HMAC_secret |s:16:".e@/Y$FkQ[hrnThJ";errors|a:0:{}

View File

@@ -1 +0,0 @@
PMA_token |s:32:"53754e49395b233f312a3e444e604c6c"; HMAC_secret |s:16:"'W%y<SKRJnU;}^!|";errors|a:0:{}

View File

@@ -1 +0,0 @@
PMA_token |s:32:"4156236d5e362168327e646d41463b24"; HMAC_secret |s:16:"0JzUDdkie;5^OD$x";errors|a:0:{}

View File

@@ -1 +0,0 @@
PMA_token |s:32:"397472635b712b6d2a40624244354f3f"; HMAC_secret |s:16:"MIW6@(E`(8hUZPvn";errors|a:0:{}

View File

@@ -1 +0,0 @@
PMA_token |s:32:"59692f53212c545c55273d71446d4a3a"; HMAC_secret |s:16:"-+x:]'T{Gq%(y'M)";errors|a:0:{}

View File

@@ -1 +0,0 @@
PMA_token |s:32:"4239422850276e464b7e426d556b5277"; HMAC_secret |s:16:"q]Ldh$];4JH,{.MX";errors|a:0:{}

View File

@@ -1 +0,0 @@
PMA_token |s:32:"24545877502f6d7d7967723b5e6c3a38"; HMAC_secret |s:16:"B%B#Rs(y)DGW?uv6";errors|a:0:{}

View File

@@ -1 +0,0 @@
PMA_token |s:32:"222b524d303848785047625636644350"; HMAC_secret |s:16:"r>gT.Yji&;>+(WBB";errors|a:0:{}

View File

@@ -1 +0,0 @@
PMA_token |s:32:"2f5e6c66684a7b414659303778513a25"; HMAC_secret |s:16:"32szCS_&Uq+0r$jk";errors|a:0:{}

View File

@@ -1 +0,0 @@
PMA_token |s:32:"637c736e2b30345b2f5e34516d2a5279"; HMAC_secret |s:16:"dwf#X!d2CIQn#30!";browser_access_time|a:0:{}encryption_key|s:32:"<22>S];W<>

View File

@@ -1 +0,0 @@
PMA_token |s:32:"32295c5e38423a7e2a4a237221505b3d"; HMAC_secret |s:16:"&:))STa2pQ/P6Z"h";errors|a:0:{}

View File

@@ -1 +0,0 @@
PMA_token |s:32:"2f472d2e70326a4f667d725e6e6c2e34"; HMAC_secret |s:16:"mAV2g;|NA4_6~ZyR";errors|a:0:{}

View File

@@ -1 +0,0 @@
PMA_token |s:32:"7057757d4e6d696d4e592a7b37584c55"; HMAC_secret |s:16:"A;qa<:i@!s2_vvR>";errors|a:0:{}

View File

@@ -1 +0,0 @@
PMA_token |s:32:"34247c3c42743a5d50762a54336f2355"; HMAC_secret |s:16:"vj[cT;Q$S4x)t]Qi";errors|a:0:{}