Compare commits
3 Commits
main
...
feature/da
| Author | SHA1 | Date | |
|---|---|---|---|
| 223c9a6c6e | |||
| 0abf5618f7 | |||
| 71fc7eee13 |
@@ -1,4 +1,4 @@
|
|||||||
[/dms]
|
[/dms]
|
||||||
max_log = 496206
|
max_log = 498246
|
||||||
number = 3
|
number = 4
|
||||||
finish = 1
|
finish = 1
|
||||||
|
|||||||
3178
.qsync/meta/qmeta0
3178
.qsync/meta/qmeta0
File diff suppressed because it is too large
Load Diff
4871
.qsync/meta/qmeta1
4871
.qsync/meta/qmeta1
File diff suppressed because it is too large
Load Diff
5235
.qsync/meta/qmeta2
5235
.qsync/meta/qmeta2
File diff suppressed because it is too large
Load Diff
150
backend/src/db/sequelize.js
Normal file → Executable file
150
backend/src/db/sequelize.js
Normal file → Executable 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);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
61
backend/src/middleware/auth copy.js
Executable file
61
backend/src/middleware/auth copy.js
Executable 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
0
backend/src/middleware/auth.js
Normal file → Executable file
6
backend/src/middleware/index.js
Normal file → Executable file
6
backend/src/middleware/index.js
Normal file → Executable 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
0
backend/src/middleware/loadPrincipal.js
Normal file → Executable file
74
backend/src/routes/dashboard.js
Normal file → Executable file
74
backend/src/routes/dashboard.js
Normal file → Executable 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
130
backend/src/routes/rbac_admin.js
Normal file → Executable 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
205
backend/src/routes/users.js
Normal file → Executable 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;
|
||||||
22
frontend/components-Copy(1).json
Normal file
22
frontend/components-Copy(1).json
Normal 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": {}
|
||||||
|
}
|
||||||
99
frontend/components/ui/alert-dialog.jsx
Normal file
99
frontend/components/ui/alert-dialog.jsx
Normal 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,
|
||||||
|
}
|
||||||
24
frontend/components/ui/checkbox.jsx
Normal file
24
frontend/components/ui/checkbox.jsx
Normal 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 }
|
||||||
96
frontend/components/ui/dialog.jsx
Normal file
96
frontend/components/ui/dialog.jsx
Normal 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,
|
||||||
|
}
|
||||||
40
frontend/components/ui/scroll-area.jsx
Normal file
40
frontend/components/ui/scroll-area.jsx
Normal 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
38
frontend/lib/auth copy.js
Executable 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,
|
||||||
|
};
|
||||||
|
}
|
||||||
135
frontend/package-lock.json
generated
135
frontend/package-lock.json
generated
@@ -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",
|
||||||
|
|||||||
@@ -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",
|
||||||
|
|||||||
@@ -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.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -1,8 +0,0 @@
|
|||||||
1
|
|
||||||
/var/lib/postgresql/data
|
|
||||||
1759373409
|
|
||||||
5432
|
|
||||||
/var/run/postgresql
|
|
||||||
*
|
|
||||||
117512122 0
|
|
||||||
ready
|
|
||||||
Binary file not shown.
@@ -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;
|
|
||||||
}
|
|
||||||
@@ -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;
|
||||||
|
|||||||
@@ -1 +0,0 @@
|
|||||||
PMA_token |s:32:"6537407a672f376761784c234d79666a"; HMAC_secret |s:16:";_tF,Ps<jX+#Z=f>";errors|a:0:{}
|
|
||||||
@@ -1 +0,0 @@
|
|||||||
PMA_token |s:32:"34347471654523645a3b4a32556f6c2c"; HMAC_secret |s:16:"cxqQ@%S|9[H3Y6^%";errors|a:0:{}
|
|
||||||
@@ -1 +0,0 @@
|
|||||||
PMA_token |s:32:"263a746d69672f6a57305b5842603462"; HMAC_secret |s:16:".e@/Y$FkQ[hrnThJ";errors|a:0:{}
|
|
||||||
@@ -1 +0,0 @@
|
|||||||
PMA_token |s:32:"53754e49395b233f312a3e444e604c6c"; HMAC_secret |s:16:"'W%y<SKRJnU;}^!|";errors|a:0:{}
|
|
||||||
@@ -1 +0,0 @@
|
|||||||
PMA_token |s:32:"4156236d5e362168327e646d41463b24"; HMAC_secret |s:16:"0JzUDdkie;5^OD$x";errors|a:0:{}
|
|
||||||
@@ -1 +0,0 @@
|
|||||||
PMA_token |s:32:"397472635b712b6d2a40624244354f3f"; HMAC_secret |s:16:"MIW6@(E`(8hUZPvn";errors|a:0:{}
|
|
||||||
@@ -1 +0,0 @@
|
|||||||
PMA_token |s:32:"59692f53212c545c55273d71446d4a3a"; HMAC_secret |s:16:"-+x:]'T{Gq%(y'M)";errors|a:0:{}
|
|
||||||
@@ -1 +0,0 @@
|
|||||||
PMA_token |s:32:"4239422850276e464b7e426d556b5277"; HMAC_secret |s:16:"q]Ldh$];4JH,{.MX";errors|a:0:{}
|
|
||||||
@@ -1 +0,0 @@
|
|||||||
PMA_token |s:32:"24545877502f6d7d7967723b5e6c3a38"; HMAC_secret |s:16:"B%B#Rs(y)DGW?uv6";errors|a:0:{}
|
|
||||||
@@ -1 +0,0 @@
|
|||||||
PMA_token |s:32:"222b524d303848785047625636644350"; HMAC_secret |s:16:"r>gT.Yji&;>+(WBB";errors|a:0:{}
|
|
||||||
@@ -1 +0,0 @@
|
|||||||
PMA_token |s:32:"2f5e6c66684a7b414659303778513a25"; HMAC_secret |s:16:"32szCS_&Uq+0r$jk";errors|a:0:{}
|
|
||||||
@@ -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<>
|
|
||||||
@@ -1 +0,0 @@
|
|||||||
PMA_token |s:32:"32295c5e38423a7e2a4a237221505b3d"; HMAC_secret |s:16:"&:))STa2pQ/P6Z"h";errors|a:0:{}
|
|
||||||
@@ -1 +0,0 @@
|
|||||||
PMA_token |s:32:"2f472d2e70326a4f667d725e6e6c2e34"; HMAC_secret |s:16:"mAV2g;|NA4_6~ZyR";errors|a:0:{}
|
|
||||||
@@ -1 +0,0 @@
|
|||||||
PMA_token |s:32:"7057757d4e6d696d4e592a7b37584c55"; HMAC_secret |s:16:"A;qa<:i@!s2_vvR>";errors|a:0:{}
|
|
||||||
@@ -1 +0,0 @@
|
|||||||
PMA_token |s:32:"34247c3c42743a5d50762a54336f2355"; HMAC_secret |s:16:"vj[cT;Q$S4x)t]Qi";errors|a:0:{}
|
|
||||||
Reference in New Issue
Block a user