Update frontend login page.jsx และ backend

This commit is contained in:
admin
2025-09-29 13:25:09 +07:00
parent aca3667a9d
commit 7dd5ce8015
52 changed files with 2903 additions and 1289 deletions

View File

@@ -1,5 +1,11 @@
import { sequelize } from '../db/sequelize.js';
import UPRModel from '../db/models/UserProjectRole.js';
// FILE: src/middleware/abac.js
// ABAC: Attribute-Based Access Control middleware helpers
// - Project-scoped access control base on user_project_roles + permissions
// - Requires req.user.roles and req.user.permissions to be populated (e.g. via auth.js with enrichment)
// - Uses UserProjectRole model to check project membership
import { sequelize } from "../db/sequelize.js";
import UPRModel from "../db/models/UserProjectRole.js";
/**
* ดึง project_id ที่ผู้ใช้เข้าถึงได้ (จาก user_project_roles)
@@ -7,7 +13,7 @@ import UPRModel from '../db/models/UserProjectRole.js';
export async function getUserProjectIds(user_id) {
const UPR = UPRModel(sequelize);
const rows = await UPR.findAll({ where: { user_id } });
return [...new Set(rows.map(r => r.project_id))];
return [...new Set(rows.map((r) => r.project_id))];
}
/**
@@ -22,25 +28,32 @@ export async function getUserProjectIds(user_id) {
export function projectScopedView(moduleName) {
return async (req, res, next) => {
const roles = req.user?.roles || [];
const isAdmin = roles.includes('Admin');
const isAdmin = roles.includes("Admin");
const permName = `${moduleName}:view`;
const hasViewPerm = (req.user?.permissions || []).includes(permName);
// Admin ผ่านได้เสมอ
if (isAdmin) return next();
const qProjectId = req.query?.project_id ? Number(req.query.project_id) : null;
const qProjectId = req.query?.project_id
? Number(req.query.project_id)
: null;
const memberProjects = await getUserProjectIds(req.user?.user_id);
if (qProjectId) {
// ต้องเป็นสมาชิกโปรเจ็กต์นั้น หรือมี perm view
if (hasViewPerm || memberProjects.includes(qProjectId)) return next();
return res.status(403).json({ error: 'Forbidden: not a member of project' });
return res
.status(403)
.json({ error: "Forbidden: not a member of project" });
} else {
// ไม่มี project_id: ถ้ามี perm view → อนุญาตทั้งหมด
// ถ้าไม่มี perm view → จำกัดด้วยรายการโปรเจ็กต์ที่เป็นสมาชิก (บันทึกไว้ใน req.abac.filterProjectIds)
if (hasViewPerm) return next();
if (!memberProjects.length) return res.status(403).json({ error: 'Forbidden: no accessible projects' });
if (!memberProjects.length)
return res
.status(403)
.json({ error: "Forbidden: no accessible projects" });
req.abac = req.abac || {};
req.abac.filterProjectIds = memberProjects;
return next();
@@ -48,7 +61,6 @@ export function projectScopedView(moduleName) {
};
}
/**
* บังคับเป็นสมาชิกโปรเจ็กต์จากค่า project_id ใน body
* ใช้กับ create endpoints
@@ -56,12 +68,13 @@ export function projectScopedView(moduleName) {
export function requireProjectMembershipFromBody() {
return async (req, res, next) => {
const roles = req.user?.roles || [];
const isAdmin = roles.includes('Admin');
const isAdmin = roles.includes("Admin");
if (isAdmin) return next();
const pid = Number(req.body?.project_id);
if (!pid) return res.status(400).json({ error: 'project_id required' });
if (!pid) return res.status(400).json({ error: "project_id required" });
const memberProjects = await getUserProjectIds(req.user?.user_id);
if (!memberProjects.includes(pid)) return res.status(403).json({ error: 'Forbidden: not a project member' });
if (!memberProjects.includes(pid))
return res.status(403).json({ error: "Forbidden: not a project member" });
next();
};
}
@@ -71,19 +84,20 @@ export function requireProjectMembershipFromBody() {
* opts: { modelLoader: (sequelize)=>Model, idParam: 'id', projectField: 'project_id' }
*/
export function requireProjectMembershipByRecord(opts) {
const { modelLoader, idParam='id', projectField='project_id' } = opts;
const { modelLoader, idParam = "id", projectField = "project_id" } = opts;
return async (req, res, next) => {
const roles = req.user?.roles || [];
const isAdmin = roles.includes('Admin');
const isAdmin = roles.includes("Admin");
if (isAdmin) return next();
const id = Number(req.params[idParam]);
if (!id) return res.status(400).json({ error: 'Invalid id' });
if (!id) return res.status(400).json({ error: "Invalid id" });
const Model = modelLoader(sequelize);
const row = await Model.findByPk(id);
if (!row) return res.status(404).json({ error: 'Not found' });
if (!row) return res.status(404).json({ error: "Not found" });
const pid = Number(row[projectField]);
const memberProjects = await getUserProjectIds(req.user?.user_id);
if (!memberProjects.includes(pid)) return res.status(403).json({ error: 'Forbidden: not a project member' });
if (!memberProjects.includes(pid))
return res.status(403).json({ error: "Forbidden: not a project member" });
next();
};
}
@@ -94,10 +108,13 @@ export function requireProjectMembershipByRecord(opts) {
export function requireProjectIdQuery() {
return async (req, res, next) => {
const roles = req.user?.roles || [];
const isAdmin = roles.includes('Admin');
const isAdmin = roles.includes("Admin");
if (isAdmin) return next();
const qProjectId = req.query?.project_id ? Number(req.query.project_id) : null;
if (!qProjectId) return res.status(400).json({ error: 'project_id query required' });
const qProjectId = req.query?.project_id
? Number(req.query.project_id)
: null;
if (!qProjectId)
return res.status(400).json({ error: "project_id query required" });
next();
};
}

View File

@@ -1,37 +1,50 @@
import jwt from 'jsonwebtoken';
import { config } from '../config.js';
import { User, Role, UserRole } from '../db/sequelize.js';
// FILE: src/middleware/auth.js
// Authentication & Authorization middleware
// - JWT-based authentication
// - Role & Permission enrichment
// - RBAC (Role-Based Access Control) helpers
// - Requires User, Role, Permission, UserRole, RolePermission models
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 });
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 });
return jwt.sign(payload, config.JWT.REFRESH_SECRET, {
expiresIn: config.JWT.REFRESH_EXPIRES_IN,
});
}
export function requireAuth(req, res, next) {
if (req.path === '/health') return next(); // อนุญาต health เสมอ
const hdr = req.headers.authorization || '';
const token = hdr.startsWith('Bearer ') ? hdr.slice(7) : null;
if (!token) return res.status(401).json({ error: 'Missing token' });
if (req.path === "/health") return next(); // อนุญาต health เสมอ
const hdr = req.headers.authorization || "";
const token = hdr.startsWith("Bearer ") ? hdr.slice(7) : null;
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' });
return res.status(401).json({ error: "Invalid/Expired token" });
}
}
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);
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);
}
}

View File

@@ -1,18 +1,28 @@
// FILE: src/middleware/authJwt.js
// 03.2 4) เพิ่ม middleware authJwt (ใหม่)
// นำ middleware นี้ไปใส่ หลัง verify JWT เสมอ เช่น app.use('/api', authJwt(), routes)
// Simple JWT authentication middleware example
// - For demonstration or simple use cases
// - Not as feature-rich as auth.js (no role/permission enrichment)
// - Can be used standalone or alongside auth.js
// authJwt.js สมมติคุณมี JWT อยู่แล้ว (ปรับ verify ตามที่ใช้จริง)
import jwt from 'jsonwebtoken';
const { JWT_SECRET = 'dev-secret' } = process.env;
// - ตรวจ token และเติม req.user
// - ไม่ได้เติม roles/permissions (ถ้าต้องการให้ใช้ auth.js แทนหรือร่วมกัน)
import jwt from "jsonwebtoken";
const { JWT_SECRET = "dev-secret" } = process.env;
export function authJwt() {
return (req, res, next) => {
const h = req.headers.authorization || '';
const token = h.startsWith('Bearer ') ? h.slice(7) : null;
if (!token) return res.status(401).json({ error: 'Unauthenticated' });
const h = req.headers.authorization || "";
const token = h.startsWith("Bearer ") ? h.slice(7) : null;
if (!token) return res.status(401).json({ error: "Unauthenticated" });
try {
const payload = jwt.verify(token, JWT_SECRET);
req.user = { user_id: payload.user_id, username: payload.username };
next();
} catch (e) {
return res.status(401).json({ error: 'Invalid token' });
return res.status(401).json({ error: "Invalid token" });
}
};
}

View File

@@ -1,7 +1,13 @@
// FILE: src/middleware/errorHandler.js
// Error handling middleware
// - 404 Not Found handler
// - General error handler
// - Should be the last middleware added
export function notFound(_req, res, _next) {
res.status(404).json({ error: 'Not Found' });
res.status(404).json({ error: "Not Found" });
}
export function errorHandler(err, _req, res, _next) {
console.error(err);
res.status(500).json({ error: 'Internal Server Error' });
res.status(500).json({ error: "Internal Server Error" });
}

View File

@@ -1,15 +1,23 @@
// loadPrincipal.js
import { loadPrincipal } from '../utils/rbac.js';
// FILE: src/middleware/loadPrincipal.js
// 03.2 4) เพิ่ม middleware loadPrincipal (ใหม่)
// นำ middleware นี้ไปใส่ หลัง verify JWT เสมอ เช่น app.use('/api', authJwt(), loadPrincipalMw(), routes)
// Load principal (roles + permissions) middleware
// - Uses rbac.js utility to load principal info
// - Attaches to req.principal
// - Requires req.user.user_id to be populated (e.g. via auth.js or authJwt.js)
import { loadPrincipal } from "../utils/rbac.js";
export function loadPrincipalMw() {
return async (req, res, next) => {
try {
if (!req.user?.user_id) return res.status(401).json({ error: 'Unauthenticated' });
if (!req.user?.user_id)
return res.status(401).json({ error: "Unauthenticated" });
req.principal = await loadPrincipal(req.user.user_id);
next();
} catch (err) {
console.error('loadPrincipal error', err);
res.status(500).json({ error: 'Failed to load principal' });
console.error("loadPrincipal error", err);
res.status(500).json({ error: "Failed to load principal" });
}
};
}

View File

@@ -1,3 +1,8 @@
// FILE: src/middleware/permGuard.js
// Permission guard middleware
// - Checks if user has required permissions
// - Requires req.user.permissions to be populated (e.g. via auth.js or authJwt.js with enrichment)
/**
* requirePerm('rfa:create') => ตรวจว่ามี permission นี้ใน req.user.permissions
* ต้องแน่ใจว่าเรียก enrichPermissions() มาก่อน หรือคำนวณที่จุดเข้าใช้งาน
@@ -5,8 +10,8 @@
export function requirePerm(...allowedPerms) {
return (req, res, next) => {
const perms = req.user?.permissions || [];
const ok = perms.some(p => allowedPerms.includes(p));
if (!ok) return res.status(403).json({ error: 'Forbidden' });
const ok = perms.some((p) => allowedPerms.includes(p));
if (!ok) return res.status(403).json({ error: "Forbidden" });
next();
};
}

View File

@@ -1,4 +1,9 @@
import { Role, Permission, UserRole, RolePermission } from '../db/sequelize.js';
// FILE: src/middleware/permissions.js
// Permission calculation and enrichment middleware
// - Computes effective permissions for a user based on their roles
// - Attaches permissions to req.user.permissions
import { Role, Permission, UserRole, RolePermission } from "../db/sequelize.js";
/**
* คืนชุด permission (string[]) ของ user_id
@@ -6,16 +11,16 @@ import { Role, Permission, UserRole, RolePermission } from '../db/sequelize.js';
export async function computeEffectivePermissions(user_id) {
// ดึง roles ของผู้ใช้
const userRoles = await UserRole.findAll({ where: { user_id } });
const roleIds = userRoles.map(r => r.role_id);
const roleIds = userRoles.map((r) => r.role_id);
if (!roleIds.length) return [];
// ดึง permission ผ่าน role_permissions
const rp = await RolePermission.findAll({ where: { role_id: roleIds } });
const permIds = [...new Set(rp.map(x => x.permission_id))];
const permIds = [...new Set(rp.map((x) => x.permission_id))];
if (!permIds.length) return [];
const perms = await Permission.findAll({ where: { permission_id: permIds } });
return [...new Set(perms.map(p => p.permission_name))];
return [...new Set(perms.map((p) => p.permission_name))];
}
/**

View File

@@ -1,8 +1,13 @@
// FILE: src/middleware/rbac.js
// RBAC: Role-Based Access Control middleware helpers
// - Role and Permission guard middleware
// - Requires req.user.roles and req.user.permissions to be populated (e.g. via auth.js with enrichment)
export function requireRole(...allowed) {
return (req, res, next) => {
const roles = req.user?.roles || [];
const ok = roles.some(r => allowed.includes(r));
if (!ok) return res.status(403).json({ error: 'Forbidden' });
const ok = roles.some((r) => allowed.includes(r));
if (!ok) return res.status(403).json({ error: "Forbidden" });
next();
};
}
@@ -10,8 +15,8 @@ export function requireRole(...allowed) {
export function requirePermission(...allowedPerms) {
return (req, res, next) => {
const perms = req.user?.permissions || [];
const ok = perms.some(p => allowedPerms.includes(p));
if (!ok) return res.status(403).json({ error: 'Forbidden' });
const ok = perms.some((p) => allowedPerms.includes(p));
if (!ok) return res.status(403).json({ error: "Forbidden" });
next();
};
}

View File

@@ -1,25 +1,37 @@
// src/middleware/requirePerm.js
import { canPerform } from '../utils/rbac.js';
// FILE: src/middleware/requirePerm.js
// 03.2 4) เพิ่ม middleware requirePerm (ใหม่)
// นำ middleware นี้ไปใส่ หลัง loadPrincipal เสมอ เช่น app.use('/api', authJwt(), loadPrincipalMw(), requirePerm('correspondence.create', {scope:'org', getOrgId: req=>...}), routes)
// หรือใส่ใน route เดี่ยวๆ ก็ได้ เช่น router.post('/', requirePerm('correspondence.create', {scope:'org', getOrgId: req=>...}), (req,res)=>{...})
// Permission requirement middleware with scope support
// - Uses canPerform() utility from rbac.js
// - Supports global, org, and project scopes
// - Requires req.principal to be populated (e.g. via loadPrincipal middleware)
import { canPerform } from "../utils/rbac.js";
/**
* requirePerm('correspondence.create', { scope: 'org', getOrgId: req => ... })
* scope: 'global' | 'org' | 'project'
*/
export function requirePerm(permCode, { scope = 'global', getOrgId = null, getProjectId = null } = {}) {
export function requirePerm(
permCode,
{ scope = "global", getOrgId = null, getProjectId = null } = {}
) {
return async (req, res, next) => {
try {
const orgId = getOrgId ? await getOrgId(req) : null;
const projectId = getProjectId ? await getProjectId(req) : null;
if (canPerform(req.principal, permCode, { scope, orgId, projectId })) return next();
if (canPerform(req.principal, permCode, { scope, orgId, projectId }))
return next();
return res.status(403).json({
error: 'FORBIDDEN',
error: "FORBIDDEN",
message: `Require ${permCode} (${scope}-scoped)`,
});
} catch (e) {
console.error('requirePerm error', e);
res.status(500).json({ error: 'Permission check error' });
console.error("requirePerm error", e);
res.status(500).json({ error: "Permission check error" });
}
};
}