Update frontend login page.jsx และ backend
This commit is contained in:
@@ -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();
|
||||
};
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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" });
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
@@ -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" });
|
||||
}
|
||||
|
||||
@@ -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" });
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
@@ -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();
|
||||
};
|
||||
}
|
||||
|
||||
@@ -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))];
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -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();
|
||||
};
|
||||
}
|
||||
|
||||
@@ -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" });
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user