Update frontend login page.jsx และ backend
This commit is contained in:
@@ -7,7 +7,6 @@
|
||||
"engines": {
|
||||
"node": ">=20.0.0"
|
||||
},
|
||||
|
||||
"scripts": {
|
||||
"dev": "nodemon --watch src src/index.js",
|
||||
"dev:desktop": "node --watch src/index.js",
|
||||
@@ -16,7 +15,6 @@
|
||||
"health": "node -e \"fetch('http://localhost:'+ (process.env.BACKEND_PORT||3001) +'/health').then(r=>r.text()).then(console.log).catch(e=>{console.error(e);process.exit(1)})\"",
|
||||
"postinstall": "node -e \"console.log('Installed dms-backend %s','0.6.0')\""
|
||||
},
|
||||
|
||||
"dependencies": {
|
||||
"bcrypt": "5.1.1",
|
||||
"bcryptjs": "^2.4.3",
|
||||
|
||||
@@ -1,24 +1,39 @@
|
||||
// FILE: src/config.js
|
||||
// Purpose: Centralized configuration for the backend application
|
||||
// - Loads settings from environment variables with sensible defaults
|
||||
// - Exports a config object for use throughout the application
|
||||
// =============================================================
|
||||
// Load environment variables from .env file if present
|
||||
// (uncomment the next line if using dotenv)
|
||||
// import dotenv from 'dotenv'; dotenv.config();
|
||||
// (Make sure to install dotenv package if using this line)
|
||||
// =============================================================
|
||||
|
||||
export const config = {
|
||||
PORT: Number(process.env.BACKEND_PORT || 3001),
|
||||
DB: {
|
||||
HOST: process.env.DB_HOST || 'mariadb',
|
||||
HOST: process.env.DB_HOST || "mariadb",
|
||||
PORT: Number(process.env.DB_PORT || 3306),
|
||||
USER: process.env.DB_USER || 'center',
|
||||
PASS: process.env.DB_PASSWORD || 'Center#2025',
|
||||
NAME: process.env.DB_NAME || 'dms',
|
||||
USER: process.env.DB_USER || "center",
|
||||
PASS: process.env.DB_PASSWORD || "Center#2025",
|
||||
NAME: process.env.DB_NAME || "dms",
|
||||
},
|
||||
JWT: {
|
||||
SECRET: process.env.JWT_SECRET || '8b0df02e4aee9f9f79a4f2d8ba77b0b82c1ee3446b68cb0bae94ab54d60f8d9e',
|
||||
EXPIRES_IN: process.env.JWT_EXPIRES_IN || '8h',
|
||||
REFRESH_SECRET: process.env.JWT_REFRESH_SECRET || '31r2Wp59E8JukXjYiMopzoHDxJBLVefjR9YOOHjk62R5PbFVXzbx2Wjyc9weVDgK',
|
||||
REFRESH_EXPIRES_IN: process.env.JWT_REFRESH_EXPIRES_IN || '7d',
|
||||
SECRET:
|
||||
process.env.JWT_SECRET ||
|
||||
"8b0df02e4aee9f9f79a4f2d8ba77b0b82c1ee3446b68cb0bae94ab54d60f8d9e",
|
||||
EXPIRES_IN: process.env.JWT_EXPIRES_IN || "8h",
|
||||
REFRESH_SECRET:
|
||||
process.env.JWT_REFRESH_SECRET ||
|
||||
"31r2Wp59E8JukXjYiMopzoHDxJBLVefjR9YOOHjk62R5PbFVXzbx2Wjyc9weVDgK",
|
||||
REFRESH_EXPIRES_IN: process.env.JWT_REFRESH_EXPIRES_IN || "7d",
|
||||
},
|
||||
SECURITY: {
|
||||
RATE_LIMIT_WINDOW_MS: Number(process.env.RATE_LIMIT_WINDOW_MS || 60_000),
|
||||
RATE_LIMIT_MAX: Number(process.env.RATE_LIMIT_MAX || 100),
|
||||
},
|
||||
CORS_ORIGINS: (process.env.CORS_ALLOWLIST || '')
|
||||
.split(',')
|
||||
.map(s => s.trim())
|
||||
CORS_ORIGINS: (process.env.CORS_ALLOWLIST || "")
|
||||
.split(",")
|
||||
.map((s) => s.trim())
|
||||
.filter(Boolean),
|
||||
};
|
||||
|
||||
@@ -1,45 +1,156 @@
|
||||
// =============================================================
|
||||
// FILE: src/config/permissions.js
|
||||
// Purpose: Map permission_code to your seed naming convention.
|
||||
// - Choose one PROFILE (by SEED_PROFILE env) or edit inline to match exactly
|
||||
// what's in 06_dms_v0_5_0_data_v5_1_seed_users.sql
|
||||
// =============================================================
|
||||
|
||||
// Two built-in profiles:
|
||||
// - V5_DOT (default): permission codes with dot notation (e.g. rfa.create)
|
||||
// - V5_SNAKE: permission codes with snake_case (e.g. rfa_create)
|
||||
// =============================================================
|
||||
// You can also create your own profile by editing below or
|
||||
// setting SEED_PROFILE env variable to your custom profile name
|
||||
// and adding your custom profile object here.
|
||||
// =============================================================
|
||||
// Note: Changing permission codes after users/roles/permissions have been created
|
||||
// will require updating the database records accordingly.
|
||||
// =============================================================
|
||||
// Example: to use V5_SNAKE profile, set environment variable:
|
||||
// SEED_PROFILE=V5_SNAKE
|
||||
// =============================================================
|
||||
// Example: to use custom profile, set environment variable:
|
||||
// SEED_PROFILE=MY_CUSTOM_PROFILE
|
||||
// and add your custom profile object below.
|
||||
// =============================================================
|
||||
|
||||
const V5_DOT = {
|
||||
organization: { read: 'organization.read' },
|
||||
project: { read: 'project.read', create: 'project.create', update: 'project.update', delete: 'project.delete' },
|
||||
correspondence: { read: 'correspondence.read', create: 'correspondence.create', update: 'correspondence.update', delete: 'correspondence.delete', upload: 'correspondence.upload' },
|
||||
rfa: { read: 'rfa.read', create: 'rfa.create', update: 'rfa.update', delete: 'rfa.delete', upload: 'rfa.upload' },
|
||||
drawing: { read: 'drawing.read', create: 'drawing.create', update: 'drawing.update', delete: 'drawing.delete', upload: 'drawing.upload' },
|
||||
transmittal: { read: 'transmittal.read', create: 'transmittal.create', update: 'transmittal.update', delete: 'transmittal.delete', upload: 'transmittal.upload' },
|
||||
contract: { read: 'contract.read', create: 'contract.create', update: 'contract.update', delete: 'contract.delete' },
|
||||
contract_dwg: { read: 'contract_dwg.read', create: 'contract_dwg.create', update: 'contract_dwg.update', delete: 'contract_dwg.delete' },
|
||||
category: { read: 'category.read', create: 'category.create', update: 'category.update', delete: 'category.delete' },
|
||||
volume: { read: 'volume.read', create: 'volume.create', update: 'volume.update', delete: 'volume.delete' },
|
||||
permission: { read: 'permission.read' },
|
||||
user: { read: 'user.read' },
|
||||
organization: { read: "organization.read" },
|
||||
project: {
|
||||
read: "project.read",
|
||||
create: "project.create",
|
||||
update: "project.update",
|
||||
delete: "project.delete",
|
||||
},
|
||||
correspondence: {
|
||||
read: "correspondence.read",
|
||||
create: "correspondence.create",
|
||||
update: "correspondence.update",
|
||||
delete: "correspondence.delete",
|
||||
upload: "correspondence.upload",
|
||||
},
|
||||
rfa: {
|
||||
read: "rfa.read",
|
||||
create: "rfa.create",
|
||||
update: "rfa.update",
|
||||
delete: "rfa.delete",
|
||||
upload: "rfa.upload",
|
||||
},
|
||||
drawing: {
|
||||
read: "drawing.read",
|
||||
create: "drawing.create",
|
||||
update: "drawing.update",
|
||||
delete: "drawing.delete",
|
||||
upload: "drawing.upload",
|
||||
},
|
||||
transmittal: {
|
||||
read: "transmittal.read",
|
||||
create: "transmittal.create",
|
||||
update: "transmittal.update",
|
||||
delete: "transmittal.delete",
|
||||
upload: "transmittal.upload",
|
||||
},
|
||||
contract: {
|
||||
read: "contract.read",
|
||||
create: "contract.create",
|
||||
update: "contract.update",
|
||||
delete: "contract.delete",
|
||||
},
|
||||
contract_dwg: {
|
||||
read: "contract_dwg.read",
|
||||
create: "contract_dwg.create",
|
||||
update: "contract_dwg.update",
|
||||
delete: "contract_dwg.delete",
|
||||
},
|
||||
category: {
|
||||
read: "category.read",
|
||||
create: "category.create",
|
||||
update: "category.update",
|
||||
delete: "category.delete",
|
||||
},
|
||||
volume: {
|
||||
read: "volume.read",
|
||||
create: "volume.create",
|
||||
update: "volume.update",
|
||||
delete: "volume.delete",
|
||||
},
|
||||
permission: { read: "permission.read" },
|
||||
user: { read: "user.read" },
|
||||
};
|
||||
|
||||
|
||||
const V5_SNAKE = {
|
||||
organization: { read: 'organization_read' },
|
||||
project: { read: 'project_read', create: 'project_create', update: 'project_update', delete: 'project_delete' },
|
||||
correspondence: { read: 'correspondence_read', create: 'correspondence_create', update: 'correspondence_update', delete: 'correspondence_delete', upload: 'correspondence_upload' },
|
||||
rfa: { read: 'rfa_read', create: 'rfa_create', update: 'rfa_update', delete: 'rfa_delete', upload: 'rfa_upload' },
|
||||
drawing: { read: 'drawing_read', create: 'drawing_create', update: 'drawing_update', delete: 'drawing_delete', upload: 'drawing_upload' },
|
||||
transmittal: { read: 'transmittal_read', create: 'transmittal_create', update: 'transmittal_update', delete: 'transmittal_delete', upload: 'transmittal_upload' },
|
||||
contract: { read: 'contract_read', create: 'contract_create', update: 'contract_update', delete: 'contract_delete' },
|
||||
contract_dwg: { read: 'contract_dwg_read', create: 'contract_dwg_create', update: 'contract_dwg_update', delete: 'contract_dwg_delete' },
|
||||
category: { read: 'category_read', create: 'category_create', update: 'category_update', delete: 'category_delete' },
|
||||
volume: { read: 'volume_read', create: 'volume_create', update: 'volume_update', delete: 'volume_delete' },
|
||||
permission: { read: 'permission_read' },
|
||||
user: { read: 'user_read' },
|
||||
organization: { read: "organization_read" },
|
||||
project: {
|
||||
read: "project_read",
|
||||
create: "project_create",
|
||||
update: "project_update",
|
||||
delete: "project_delete",
|
||||
},
|
||||
correspondence: {
|
||||
read: "correspondence_read",
|
||||
create: "correspondence_create",
|
||||
update: "correspondence_update",
|
||||
delete: "correspondence_delete",
|
||||
upload: "correspondence_upload",
|
||||
},
|
||||
rfa: {
|
||||
read: "rfa_read",
|
||||
create: "rfa_create",
|
||||
update: "rfa_update",
|
||||
delete: "rfa_delete",
|
||||
upload: "rfa_upload",
|
||||
},
|
||||
drawing: {
|
||||
read: "drawing_read",
|
||||
create: "drawing_create",
|
||||
update: "drawing_update",
|
||||
delete: "drawing_delete",
|
||||
upload: "drawing_upload",
|
||||
},
|
||||
transmittal: {
|
||||
read: "transmittal_read",
|
||||
create: "transmittal_create",
|
||||
update: "transmittal_update",
|
||||
delete: "transmittal_delete",
|
||||
upload: "transmittal_upload",
|
||||
},
|
||||
contract: {
|
||||
read: "contract_read",
|
||||
create: "contract_create",
|
||||
update: "contract_update",
|
||||
delete: "contract_delete",
|
||||
},
|
||||
contract_dwg: {
|
||||
read: "contract_dwg_read",
|
||||
create: "contract_dwg_create",
|
||||
update: "contract_dwg_update",
|
||||
delete: "contract_dwg_delete",
|
||||
},
|
||||
category: {
|
||||
read: "category_read",
|
||||
create: "category_create",
|
||||
update: "category_update",
|
||||
delete: "category_delete",
|
||||
},
|
||||
volume: {
|
||||
read: "volume_read",
|
||||
create: "volume_create",
|
||||
update: "volume_update",
|
||||
delete: "volume_delete",
|
||||
},
|
||||
permission: { read: "permission_read" },
|
||||
user: { read: "user_read" },
|
||||
};
|
||||
|
||||
const PROFILE = (process.env.SEED_PROFILE || "V5_DOT").toUpperCase();
|
||||
|
||||
const PROFILE = (process.env.SEED_PROFILE || 'V5_DOT').toUpperCase();
|
||||
|
||||
|
||||
export const PERM = PROFILE === 'V5_SNAKE' ? V5_SNAKE : V5_DOT;
|
||||
export default PERM;
|
||||
export const PERM = PROFILE === "V5_SNAKE" ? V5_SNAKE : V5_DOT;
|
||||
export default PERM;
|
||||
|
||||
@@ -1,13 +1,26 @@
|
||||
// ESM
|
||||
import mysql from 'mysql2/promise';
|
||||
// FILE: src/db/index.js (ESM)
|
||||
// Database connection and query utility
|
||||
// - Uses mysql2/promise for connection pooling and async/await
|
||||
// - Exports a query function for executing SQL with parameters
|
||||
// - Connection settings are read from environment variables with defaults
|
||||
// - Uses named placeholders for query parameters
|
||||
// - Dates are handled as strings in UTC timezone to avoid timezone issues
|
||||
// - Connection pool is configured to handle multiple concurrent requests
|
||||
// =============================================================
|
||||
// Load environment variables from .env file if present
|
||||
// (uncomment the next line if using dotenv)
|
||||
// import dotenv from 'dotenv'; dotenv.config();
|
||||
// (Make sure to install dotenv package if using this line)
|
||||
|
||||
import mysql from "mysql2/promise";
|
||||
|
||||
const {
|
||||
DB_HOST = 'mariadb',
|
||||
DB_PORT = '3306',
|
||||
DB_USER = 'center',
|
||||
DB_PASSWORD = 'Center#2025',
|
||||
DB_NAME = 'dms',
|
||||
DB_CONN_LIMIT = '10',
|
||||
DB_HOST = "mariadb",
|
||||
DB_PORT = "3306",
|
||||
DB_USER = "center",
|
||||
DB_PASSWORD = "Center#2025",
|
||||
DB_NAME = "dms",
|
||||
DB_CONN_LIMIT = "10",
|
||||
} = process.env;
|
||||
|
||||
const pool = mysql.createPool({
|
||||
@@ -20,7 +33,7 @@ const pool = mysql.createPool({
|
||||
waitForConnections: true, // Recommended for handling connection spikes
|
||||
namedPlaceholders: true,
|
||||
dateStrings: true, // Keep dates as strings
|
||||
timezone: 'Z', // Store and retrieve dates in UTC
|
||||
timezone: "Z", // Store and retrieve dates in UTC
|
||||
});
|
||||
|
||||
/**
|
||||
|
||||
@@ -1,5 +1,12 @@
|
||||
import { Sequelize } from 'sequelize';
|
||||
import { config } from '../config.js';
|
||||
// FILE: src/db/sequelize.js
|
||||
// Sequelize initialization and model definitions
|
||||
// - Configured via config.js
|
||||
// - Defines User, Role, Permission, UserRole, RolePermission models
|
||||
// - Sets up associations between models
|
||||
// - Exports sequelize instance and models for use in other modules
|
||||
|
||||
import { Sequelize } from "sequelize";
|
||||
import { config } from "../config.js";
|
||||
|
||||
export const sequelize = new Sequelize(
|
||||
config.DB.NAME,
|
||||
@@ -8,9 +15,9 @@ export const sequelize = new Sequelize(
|
||||
{
|
||||
host: config.DB.HOST,
|
||||
port: config.DB.PORT,
|
||||
dialect: 'mariadb',
|
||||
dialect: "mariadb",
|
||||
logging: false,
|
||||
dialectOptions: { timezone: 'Z' },
|
||||
dialectOptions: { timezone: "Z" },
|
||||
define: {
|
||||
freezeTableName: true,
|
||||
underscored: false,
|
||||
@@ -20,11 +27,11 @@ export const sequelize = new Sequelize(
|
||||
}
|
||||
);
|
||||
|
||||
import UserModel from './models/User.js';
|
||||
import RoleModel from './models/Role.js';
|
||||
import PermissionModel from './models/Permission.js';
|
||||
import UserRoleModel from './models/UserRole.js';
|
||||
import RolePermissionModel from './models/RolePermission.js';
|
||||
import UserModel from "./models/User.js";
|
||||
import RoleModel from "./models/Role.js";
|
||||
import PermissionModel from "./models/Permission.js";
|
||||
import UserRoleModel from "./models/UserRole.js";
|
||||
import RolePermissionModel from "./models/RolePermission.js";
|
||||
|
||||
export const User = UserModel(sequelize);
|
||||
export const Role = RoleModel(sequelize);
|
||||
@@ -32,11 +39,27 @@ export const Permission = PermissionModel(sequelize);
|
||||
export const UserRole = UserRoleModel(sequelize);
|
||||
export const RolePermission = RolePermissionModel(sequelize);
|
||||
|
||||
User.belongsToMany(Role, { through: UserRole, foreignKey: 'user_id', otherKey: 'role_id' });
|
||||
Role.belongsToMany(User, { through: UserRole, foreignKey: 'role_id', otherKey: 'user_id' });
|
||||
User.belongsToMany(Role, {
|
||||
through: UserRole,
|
||||
foreignKey: "user_id",
|
||||
otherKey: "role_id",
|
||||
});
|
||||
Role.belongsToMany(User, {
|
||||
through: UserRole,
|
||||
foreignKey: "role_id",
|
||||
otherKey: "user_id",
|
||||
});
|
||||
|
||||
Role.belongsToMany(Permission, { through: RolePermission, foreignKey: 'role_id', otherKey: 'permission_id' });
|
||||
Permission.belongsToMany(Role, { through: RolePermission, foreignKey: 'permission_id', otherKey: 'role_id' });
|
||||
Role.belongsToMany(Permission, {
|
||||
through: RolePermission,
|
||||
foreignKey: "role_id",
|
||||
otherKey: "permission_id",
|
||||
});
|
||||
Permission.belongsToMany(Role, {
|
||||
through: RolePermission,
|
||||
foreignKey: "permission_id",
|
||||
otherKey: "role_id",
|
||||
});
|
||||
|
||||
export async function dbReady() {
|
||||
await sequelize.authenticate();
|
||||
|
||||
@@ -1,6 +1,67 @@
|
||||
// src/index.js (ESM)
|
||||
// -------------------
|
||||
// Node >= 18, Express 4/5 compatible
|
||||
// FILE: src/index.js (ESM)
|
||||
// Main entry point for the backend API server
|
||||
// - Sets up Express app with middleware, routes, error handling
|
||||
// - Connects to database
|
||||
// - Starts server and handles graceful shutdown
|
||||
// ==========================
|
||||
// Context:
|
||||
// - Node.js >= 18 (ESM)
|
||||
// - Express.js 4/5
|
||||
// - MySQL database (using mysql2/promise)
|
||||
// ==========================
|
||||
// Features:
|
||||
// - CORS with dynamic origin checking
|
||||
// - Cookie parsing
|
||||
// - JSON and URL-encoded body parsing
|
||||
// - Access logging
|
||||
// - Health, livez, readyz, info endpoints
|
||||
// - JWT authentication middleware
|
||||
// - Principal loading middleware
|
||||
// - Modular route handlers for various resources
|
||||
// - 404 and error handling middleware
|
||||
// - Graceful shutdown on SIGTERM/SIGINT
|
||||
// ==========================
|
||||
// Assumptions:
|
||||
// - Environment variables for configuration (e.g., PORT, DB connection, FRONTEND_ORIGIN)
|
||||
// - Database connection module at ./db/index.js
|
||||
// - Middleware modules for auth, permissions, principal loading
|
||||
// - Route modules for different API resources
|
||||
// - Logs directory exists or can be created
|
||||
// - Code is written in JavaScript (ESM) and runs in Node.js environment
|
||||
// - Uses ES6+ features for cleaner and more maintainable code
|
||||
// ==========================
|
||||
// Notes:
|
||||
// - Adjust CORS origins as needed for your frontend applications
|
||||
// - Ensure proper error handling and logging as per your requirements
|
||||
// - Customize middleware and routes as per your application's needs
|
||||
// ==========================
|
||||
// Best Practices Followed:
|
||||
// - Assumes existence of necessary database tables and columns
|
||||
// - Assumes existence of necessary middleware and utility functions
|
||||
// - Assumes Express.js app is set up to use this router for /api path
|
||||
// - Assumes existence of necessary environment variables
|
||||
// - Assumes existence of necessary directories and permissions for file storage
|
||||
// - Assumes multer is installed and configured
|
||||
// - Assumes fs and path modules are available for file system operations
|
||||
// - Assumes sql module is set up for database interactions
|
||||
// - Assumes middleware modules are correctly implemented and exported
|
||||
// - Assumes route modules are correctly implemented and exported
|
||||
// - Uses environment variables for configuration
|
||||
// - Uses middleware for modular functionality
|
||||
// - Uses async/await for asynchronous operations
|
||||
// - Uses try/catch for error handling in async functions (if needed)
|
||||
// - Uses parameterized queries to prevent SQL injection
|
||||
// - Uses HTTP status codes for responses (e.g., 404 for not found, 400 for bad request)
|
||||
// - Uses JSON responses for API endpoints
|
||||
// - Uses destructuring and default parameters for cleaner function signatures
|
||||
// - Uses best practices for Express.js route handling
|
||||
// - Uses modular code structure for maintainability
|
||||
// - Uses comments for documentation and clarity
|
||||
// - Uses ES6+ features for cleaner and more maintainable code
|
||||
// - Uses template literals for SQL query construction
|
||||
// - Uses array methods for filtering and joining conditions
|
||||
// - Uses utility functions for common tasks (e.g., building SQL WHERE clauses)
|
||||
// ==========================
|
||||
|
||||
import fs from "node:fs";
|
||||
import path from "node:path";
|
||||
|
||||
@@ -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" });
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
@@ -1,4 +1,12 @@
|
||||
// src/routes/admin.js
|
||||
// FILE: src/routes/admin.js
|
||||
// Admin routes
|
||||
// - System info (GET /api/admin/sysinfo)
|
||||
// - Maintenance tasks (POST /api/admin/maintenance/reindex)
|
||||
// - Permission matrix (GET /api/admin/perm-matrix?format=md|json)
|
||||
// - Requires appropriate permissions via requirePerm middleware
|
||||
// - Uses global scope for all permissions
|
||||
// - admin.read, admin.maintain
|
||||
|
||||
import { Router } from "express";
|
||||
import os from "node:os";
|
||||
import { dbReady, sequelize, Role, Permission } from "../db/sequelize.js";
|
||||
|
||||
@@ -1,62 +1,79 @@
|
||||
// src/routes/auth.js
|
||||
// FILE: src/routes/auth.js (ESM) — Bearer only, refresh via header/body, forgot/reset password
|
||||
// 03.2 เพิ่ม auth.js และ lookup.js ให้สอดคล้อง RBAC/permission_code
|
||||
// ตาม src/config/permissions.js) และอ่าน scope จาก DB เสมอ
|
||||
/*สมมติว่ามีตาราง password_resets สำหรับเก็บโทเคนรีเซ็ต:
|
||||
password_resets(
|
||||
id BIGINT PK, user_id BIGINT, token_hash CHAR(64),
|
||||
expires_at DATETIME, used_at DATETIME NULL,
|
||||
created_at DATETIME DEFAULT CURRENT_TIMESTAMP
|
||||
)
|
||||
INDEX(token_hash), INDEX(user_id), INDEX(expires_at)
|
||||
*/
|
||||
import { Router } from "express";
|
||||
import jwt from "jsonwebtoken";
|
||||
import sql from "../db/index.js";
|
||||
import crypto from "node:crypto"; // ถ้าต้องการ timingSafeEqual
|
||||
import bcrypt from "bcryptjs"; // ถ้า password_hash เป็น bcrypt (แนะนำ)
|
||||
import bcrypt from "bcryptjs";
|
||||
import crypto from "node:crypto";
|
||||
|
||||
const r = Router();
|
||||
|
||||
// ตั้งค่า JWT (อย่าใช้ .env ในโปรดักชันของคุณ → ใส่ผ่าน docker-compose)
|
||||
const JWT_ACCESS_SECRET = process.env.JWT_ACCESS_SECRET || "dev-access-secret";
|
||||
const JWT_REFRESH_SECRET =
|
||||
process.env.JWT_REFRESH_SECRET || "dev-refresh-secret";
|
||||
const ACCESS_TTL_MS = 30 * 60 * 1000; // 30 นาที
|
||||
const REFRESH_TTL_MS = 30 * 24 * 60 * 60 * 1000; // 30 วัน
|
||||
|
||||
function cookieOpts(maxAge) {
|
||||
return {
|
||||
httpOnly: true,
|
||||
secure: true, // ใช้งานจริงหลัง HTTPS
|
||||
sameSite: "lax",
|
||||
path: "/",
|
||||
maxAge,
|
||||
// domain: ".np-dms.work", // ถ้าต้องการใช้ข้าม subdomain ให้เปิด
|
||||
};
|
||||
}
|
||||
/* =========================
|
||||
* CONFIG & HELPERS
|
||||
* ========================= */
|
||||
// ใช้ค่าเดียวกับ middleware authJwt()
|
||||
const JWT_SECRET = process.env.JWT_SECRET || "dev-secret";
|
||||
const REFRESH_SECRET = process.env.REFRESH_SECRET || "dev-refresh-secret";
|
||||
const ACCESS_TTL = process.env.ACCESS_TTL || "30m";
|
||||
const REFRESH_TTL = process.env.REFRESH_TTL || "30d";
|
||||
// อายุของ reset token (นาที)
|
||||
const RESET_TTL_MIN = Number(process.env.RESET_TTL_MIN || 30);
|
||||
|
||||
function signAccessToken(user) {
|
||||
return jwt.sign(
|
||||
{ user_id: user.user_id, username: user.username },
|
||||
JWT_ACCESS_SECRET,
|
||||
{ expiresIn: "30m", issuer: "dms-backend" }
|
||||
JWT_SECRET,
|
||||
{ expiresIn: ACCESS_TTL, issuer: "dms-backend" }
|
||||
);
|
||||
}
|
||||
|
||||
function signRefreshToken(user) {
|
||||
return jwt.sign({ user_id: user.user_id }, JWT_REFRESH_SECRET, {
|
||||
expiresIn: "30d",
|
||||
issuer: "dms-backend",
|
||||
});
|
||||
return jwt.sign(
|
||||
{ user_id: user.user_id, username: user.username, t: "refresh" },
|
||||
REFRESH_SECRET,
|
||||
{ expiresIn: REFRESH_TTL, issuer: "dms-backend" }
|
||||
);
|
||||
}
|
||||
|
||||
function getBearer(req) {
|
||||
const h = req.headers.authorization || "";
|
||||
if (!h.startsWith("Bearer ")) return null;
|
||||
const token = h.slice(7).trim();
|
||||
return token || null;
|
||||
}
|
||||
|
||||
async function findUserByUsername(username) {
|
||||
const [rows] = await sql.query(
|
||||
"SELECT user_id, username, email, password_hash FROM users WHERE username=? LIMIT 1",
|
||||
`SELECT user_id, username, email, first_name, last_name, password_hash
|
||||
FROM users WHERE username=? LIMIT 1`,
|
||||
[username]
|
||||
);
|
||||
return rows?.[0] || null;
|
||||
}
|
||||
|
||||
async function verifyPassword(plain, hash) {
|
||||
// ถ้าใช้ bcrypt:
|
||||
try {
|
||||
return await bcrypt.compare(plain, hash);
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
// ถ้าระบบคุณใช้ hash แบบอื่น ให้สลับมาใช้วิธีที่ตรงกับของจริง
|
||||
async function findUserByEmail(email) {
|
||||
const [rows] = await sql.query(
|
||||
`SELECT user_id, username, email, first_name, last_name, password_hash
|
||||
FROM users WHERE email=? LIMIT 1`,
|
||||
[email]
|
||||
);
|
||||
return rows?.[0] || null;
|
||||
}
|
||||
|
||||
/* =========================
|
||||
* POST /api/auth/login
|
||||
* - รับ username/password
|
||||
* - ตรวจ bcrypt แล้วออก token+refresh_token (JSON)
|
||||
* ========================= */
|
||||
r.post("/login", async (req, res) => {
|
||||
const { username, password } = req.body || {};
|
||||
if (!username || !password) {
|
||||
@@ -66,62 +83,169 @@ r.post("/login", async (req, res) => {
|
||||
const user = await findUserByUsername(username);
|
||||
if (!user) return res.status(401).json({ error: "INVALID_CREDENTIALS" });
|
||||
|
||||
const ok = await verifyPassword(password, user.password_hash);
|
||||
const ok = await bcrypt.compare(password, user.password_hash || "");
|
||||
if (!ok) return res.status(401).json({ error: "INVALID_CREDENTIALS" });
|
||||
|
||||
const access = signAccessToken(user);
|
||||
const refresh = signRefreshToken(user);
|
||||
|
||||
res.cookie("access_token", access, cookieOpts(ACCESS_TTL_MS));
|
||||
res.cookie("refresh_token", refresh, cookieOpts(REFRESH_TTL_MS));
|
||||
const token = signAccessToken(user);
|
||||
const refresh_token = signRefreshToken(user);
|
||||
|
||||
return res.json({
|
||||
ok: true,
|
||||
token,
|
||||
refresh_token,
|
||||
user: {
|
||||
user_id: user.user_id,
|
||||
username: user.username,
|
||||
email: user.email,
|
||||
first_name: user.first_name,
|
||||
last_name: user.last_name,
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
/* =========================
|
||||
* POST /api/auth/refresh
|
||||
* - รองรับ refresh token จาก:
|
||||
* 1) Authorization: Bearer <refresh_token>
|
||||
* 2) req.body.refresh_token
|
||||
* - ออก token ใหม่ + refresh ใหม่ (rotation)
|
||||
* ========================= */
|
||||
r.post("/refresh", async (req, res) => {
|
||||
const refresh = req.cookies?.refresh_token || req.body?.refresh_token;
|
||||
if (!refresh)
|
||||
const fromHeader = getBearer(req);
|
||||
const fromBody = (req.body || {}).refresh_token;
|
||||
const refreshToken = fromHeader || fromBody;
|
||||
if (!refreshToken) {
|
||||
return res.status(400).json({ error: "REFRESH_TOKEN_REQUIRED" });
|
||||
}
|
||||
|
||||
try {
|
||||
const payload = jwt.verify(refresh, JWT_REFRESH_SECRET, {
|
||||
const payload = jwt.verify(refreshToken, REFRESH_SECRET, {
|
||||
issuer: "dms-backend",
|
||||
});
|
||||
// TODO: (ถ้ามี) ตรวจ blacklist/rotation store ของ refresh token
|
||||
if (payload.t !== "refresh") throw new Error("bad token type");
|
||||
|
||||
// คืน user จากฐานข้อมูลจริงตาม payload.user_id
|
||||
const [rows] = await sql.query(
|
||||
"SELECT user_id, username, email FROM users WHERE user_id=? LIMIT 1",
|
||||
const [[user]] = await sql.query(
|
||||
`SELECT user_id, username, email, first_name, last_name
|
||||
FROM users WHERE user_id=? LIMIT 1`,
|
||||
[payload.user_id]
|
||||
);
|
||||
const user = rows?.[0];
|
||||
if (!user) return res.status(401).json({ error: "USER_NOT_FOUND" });
|
||||
|
||||
// rotation: ออก access+refresh ใหม่
|
||||
const access = signAccessToken(user);
|
||||
const newRef = signRefreshToken(user);
|
||||
// rotation
|
||||
const token = signAccessToken(user);
|
||||
const new_refresh = signRefreshToken(user);
|
||||
|
||||
res.cookie("access_token", access, cookieOpts(ACCESS_TTL_MS));
|
||||
res.cookie("refresh_token", newRef, cookieOpts(REFRESH_TTL_MS));
|
||||
return res.json({ ok: true });
|
||||
} catch (e) {
|
||||
return res.json({
|
||||
token,
|
||||
refresh_token: new_refresh,
|
||||
user: {
|
||||
user_id: user.user_id,
|
||||
username: user.username,
|
||||
email: user.email,
|
||||
first_name: user.first_name,
|
||||
last_name: user.last_name,
|
||||
},
|
||||
});
|
||||
} catch {
|
||||
return res.status(401).json({ error: "INVALID_REFRESH_TOKEN" });
|
||||
}
|
||||
});
|
||||
|
||||
r.post("/logout", (req, res) => {
|
||||
res.clearCookie("access_token", { path: "/" });
|
||||
res.clearCookie("refresh_token", { path: "/" });
|
||||
/* =========================
|
||||
* POST /api/auth/forgot-password
|
||||
* - รับ username หรือ email อย่างใดอย่างหนึ่ง
|
||||
* - สร้าง reset token แบบสุ่ม, เก็บ hash ใน DB พร้อมหมดอายุ
|
||||
* - ส่งเสมอ {ok:true} เพื่อลด user enumeration
|
||||
* - การ “ส่งอีเมล/ลิงก์รีเซ็ต” ให้ทำนอกระบบนี้ (เช่น n8n)
|
||||
* ========================= */
|
||||
r.post("/forgot-password", async (req, res) => {
|
||||
const { username, email } = req.body || {};
|
||||
// หา user จาก username หรือ email (ถ้ามีทั้งสอง จะให้ username มาก่อน)
|
||||
let user = null;
|
||||
if (username) user = await findUserByUsername(username);
|
||||
if (!user && email) user = await findUserByEmail(email);
|
||||
|
||||
// สร้างโทเคน “เหมือนจริง” เสมอ (แต่ถ้าไม่เจอ user ก็ไม่บอก)
|
||||
if (user) {
|
||||
const raw = crypto.randomBytes(32).toString("hex"); // โทเคนดิบ (ส่งทางอีเมล)
|
||||
const hash = crypto.createHash("sha256").update(raw).digest("hex"); // เก็บใน DB
|
||||
const expires = new Date(Date.now() + RESET_TTL_MIN * 60 * 1000);
|
||||
|
||||
// ทำ invalid เก่า ๆ ของ user นี้ (optional)
|
||||
await sql.query(
|
||||
`UPDATE password_resets SET used_at=NOW()
|
||||
WHERE user_id=? AND used_at IS NULL AND expires_at < NOW()`,
|
||||
[user.user_id]
|
||||
);
|
||||
|
||||
// บันทึก token ใหม่
|
||||
await sql.query(
|
||||
`INSERT INTO password_resets (user_id, token_hash, expires_at)
|
||||
VALUES (?,?,?)`,
|
||||
[user.user_id, hash, expires]
|
||||
);
|
||||
|
||||
// TODO: ส่ง “raw token” ไปช่องทางปลอดภัย (เช่น n8n ส่งอีเมล)
|
||||
// ตัวอย่างลิงก์ที่ frontend จะใช้:
|
||||
// https://<frontend-domain>/reset-password?token=<raw>
|
||||
// คุณสามารถต่อ webhook ไป n8n ได้ที่นี่ถ้าต้องการ
|
||||
}
|
||||
|
||||
// ไม่บอกว่าเจอหรือไม่เจอ user
|
||||
return res.json({ ok: true });
|
||||
});
|
||||
|
||||
/* =========================
|
||||
* POST /api/auth/reset-password
|
||||
* - รับ token (จากลิงก์ในอีเมล) + new_password
|
||||
* - ตรวจ token_hash กับ DB, หมดอายุหรือถูกใช้ไปแล้วหรือยัง
|
||||
* - เปลี่ยนรหัสผ่าน/ปิดใช้ token
|
||||
* ========================= */
|
||||
r.post("/reset-password", async (req, res) => {
|
||||
const { token, new_password } = req.body || {};
|
||||
if (!token || !new_password) {
|
||||
return res.status(400).json({ error: "TOKEN_AND_NEW_PASSWORD_REQUIRED" });
|
||||
}
|
||||
|
||||
const token_hash = crypto.createHash("sha256").update(token).digest("hex");
|
||||
|
||||
const [[row]] = await sql.query(
|
||||
`SELECT id, user_id, expires_at, used_at
|
||||
FROM password_resets
|
||||
WHERE token_hash=? LIMIT 1`,
|
||||
[token_hash]
|
||||
);
|
||||
|
||||
if (!row) return res.status(400).json({ error: "INVALID_TOKEN" });
|
||||
if (row.used_at) return res.status(400).json({ error: "TOKEN_ALREADY_USED" });
|
||||
if (new Date(row.expires_at).getTime() < Date.now()) {
|
||||
return res.status(400).json({ error: "TOKEN_EXPIRED" });
|
||||
}
|
||||
|
||||
// เปลี่ยนรหัสผ่าน
|
||||
const salt = await bcrypt.genSalt(10);
|
||||
const hash = await bcrypt.hash(new_password, salt);
|
||||
await sql.query(`UPDATE users SET password_hash=? WHERE user_id=?`, [
|
||||
hash,
|
||||
row.user_id,
|
||||
]);
|
||||
|
||||
// ปิดใช้ token นี้
|
||||
await sql.query(`UPDATE password_resets SET used_at=NOW() WHERE id=?`, [
|
||||
row.id,
|
||||
]);
|
||||
|
||||
return res.json({ ok: true });
|
||||
});
|
||||
|
||||
/* =========================
|
||||
* POST /api/auth/logout — stateless
|
||||
* - frontend ลบ token เอง
|
||||
* ========================= */
|
||||
r.post("/logout", (_req, res) => {
|
||||
return res.json({ ok: true });
|
||||
});
|
||||
|
||||
export default r;
|
||||
|
||||
// หมายเหตุ: คุณอาจเพิ่ม /register, /forgot-password, /reset-password ตามต้องการ
|
||||
// แต่ในโปรเจกต์นี้จะให้แอดมินสร้างบัญชีผู้ใช้ผ่าน /api/users แทน
|
||||
|
||||
@@ -1,4 +1,8 @@
|
||||
// src/routes/auth_extras.js
|
||||
// FILE: src/routes/auth_extras.js
|
||||
// Extra auth-related middleware
|
||||
// - Simple JWT auth from httpOnly cookie
|
||||
// - Basic role check middleware (for simple cases, use requirePerm for flexibility)
|
||||
|
||||
import jwt from "jsonwebtoken";
|
||||
|
||||
const JWT_ACCESS_SECRET = process.env.JWT_ACCESS_SECRET || "dev-access-secret";
|
||||
|
||||
@@ -1,66 +1,86 @@
|
||||
import { Router } from 'express';
|
||||
import sql from '../db/index.js';
|
||||
import { requirePerm } from '../middleware/requirePerm.js';
|
||||
import PERM from '../config/permissions.js';
|
||||
// FILE: src/routes/categories.js
|
||||
// Categories and Subcategories routes
|
||||
// - CRUD operations for categories and subcategories
|
||||
// - Requires appropriate permissions via requirePerm middleware
|
||||
// - Uses global scope for all permissions
|
||||
// - category:read, category:create, category:update, category:delete
|
||||
// - Category fields: cat_id (PK), cat_code, cat_name
|
||||
// - Subcategory fields: sub_cat_id (PK), cat_id (FK), sub_cat_code, sub_cat_name
|
||||
// - cat_code and sub_cat_code are unique
|
||||
// - Basic validation: cat_code, cat_name required for category create; sub_cat_code, sub_cat_name, cat_id required for subcategory create
|
||||
|
||||
import { Router } from "express";
|
||||
import sql from "../db/index.js";
|
||||
import { requirePerm } from "../middleware/requirePerm.js";
|
||||
import PERM from "../config/permissions.js";
|
||||
|
||||
const r = Router();
|
||||
|
||||
|
||||
// Category LIST (global master, not scoped) – still require permission
|
||||
r.get('/categories',
|
||||
requirePerm(PERM.category.read, { scope: 'global' }),
|
||||
async (req, res) => {
|
||||
const [rows] = await sql.query('SELECT * FROM categories ORDER BY cat_id DESC');
|
||||
res.json(rows);
|
||||
}
|
||||
r.get(
|
||||
"/categories",
|
||||
requirePerm(PERM.category.read, { scope: "global" }),
|
||||
async (req, res) => {
|
||||
const [rows] = await sql.query(
|
||||
"SELECT * FROM categories ORDER BY cat_id DESC"
|
||||
);
|
||||
res.json(rows);
|
||||
}
|
||||
);
|
||||
|
||||
|
||||
r.post('/categories',
|
||||
requirePerm(PERM.category.create, { scope: 'global' }),
|
||||
async (req, res) => {
|
||||
const { cat_code, cat_name } = req.body;
|
||||
const [rs] = await sql.query('INSERT INTO categories (cat_code, cat_name) VALUES (?,?)', [cat_code, cat_name]);
|
||||
res.json({ cat_id: rs.insertId });
|
||||
}
|
||||
r.post(
|
||||
"/categories",
|
||||
requirePerm(PERM.category.create, { scope: "global" }),
|
||||
async (req, res) => {
|
||||
const { cat_code, cat_name } = req.body;
|
||||
const [rs] = await sql.query(
|
||||
"INSERT INTO categories (cat_code, cat_name) VALUES (?,?)",
|
||||
[cat_code, cat_name]
|
||||
);
|
||||
res.json({ cat_id: rs.insertId });
|
||||
}
|
||||
);
|
||||
|
||||
|
||||
r.put('/categories/:id',
|
||||
requirePerm(PERM.category.update, { scope: 'global' }),
|
||||
async (req, res) => {
|
||||
const id = Number(req.params.id);
|
||||
const { cat_name } = req.body;
|
||||
await sql.query('UPDATE categories SET cat_name=? WHERE cat_id=?', [cat_name, id]);
|
||||
res.json({ ok: 1 });
|
||||
}
|
||||
r.put(
|
||||
"/categories/:id",
|
||||
requirePerm(PERM.category.update, { scope: "global" }),
|
||||
async (req, res) => {
|
||||
const id = Number(req.params.id);
|
||||
const { cat_name } = req.body;
|
||||
await sql.query("UPDATE categories SET cat_name=? WHERE cat_id=?", [
|
||||
cat_name,
|
||||
id,
|
||||
]);
|
||||
res.json({ ok: 1 });
|
||||
}
|
||||
);
|
||||
|
||||
|
||||
r.delete('/categories/:id',
|
||||
requirePerm(PERM.category.delete, { scope: 'global' }),
|
||||
async (req, res) => {
|
||||
const id = Number(req.params.id);
|
||||
await sql.query('DELETE FROM categories WHERE cat_id=?', [id]);
|
||||
res.json({ ok: 1 });
|
||||
}
|
||||
r.delete(
|
||||
"/categories/:id",
|
||||
requirePerm(PERM.category.delete, { scope: "global" }),
|
||||
async (req, res) => {
|
||||
const id = Number(req.params.id);
|
||||
await sql.query("DELETE FROM categories WHERE cat_id=?", [id]);
|
||||
res.json({ ok: 1 });
|
||||
}
|
||||
);
|
||||
|
||||
|
||||
// Subcategories (belong to categories)
|
||||
r.get('/subcategories',
|
||||
requirePerm(PERM.category.read, { scope: 'global' }),
|
||||
async (req, res) => {
|
||||
const { cat_id } = req.query;
|
||||
let sqlText = 'SELECT * FROM subcategories';
|
||||
const params = [];
|
||||
if (cat_id) { sqlText += ' WHERE cat_id=?'; params.push(Number(cat_id)); }
|
||||
sqlText += ' ORDER BY sub_cat_id DESC';
|
||||
const [rows] = await sql.query(sqlText, params);
|
||||
res.json(rows);
|
||||
}
|
||||
r.get(
|
||||
"/subcategories",
|
||||
requirePerm(PERM.category.read, { scope: "global" }),
|
||||
async (req, res) => {
|
||||
const { cat_id } = req.query;
|
||||
let sqlText = "SELECT * FROM subcategories";
|
||||
const params = [];
|
||||
if (cat_id) {
|
||||
sqlText += " WHERE cat_id=?";
|
||||
params.push(Number(cat_id));
|
||||
}
|
||||
sqlText += " ORDER BY sub_cat_id DESC";
|
||||
const [rows] = await sql.query(sqlText, params);
|
||||
res.json(rows);
|
||||
}
|
||||
);
|
||||
|
||||
|
||||
export default r;
|
||||
export default r;
|
||||
|
||||
@@ -1,74 +1,147 @@
|
||||
import { Router } from 'express';
|
||||
import sql from '../db/index.js';
|
||||
import { requirePerm } from '../middleware/requirePerm.js';
|
||||
import { buildScopeWhere, ownerResolvers } from '../utils/scope.js';
|
||||
import PERM from '../config/permissions.js';
|
||||
// FILE: src/routes/contract_dwg.js
|
||||
// Contract Drawings routes
|
||||
// - CRUD operations for contract drawings
|
||||
// - Requires appropriate permissions via requirePerm middleware
|
||||
// - Uses scope-based access control (global, org, project) via requirePerm
|
||||
// - contract_dwg:read, contract_dwg:create, contract_dwg:update, contract_dwg:delete
|
||||
// - contract_dwg fields: id (PK), org_id, project_id, condwg_no, title, drawing_id, volume_id, sub_cat_id, sub_no, remark, created_by
|
||||
// - Basic filtering on list endpoint by project_id, org_id, condwg_no
|
||||
|
||||
import { Router } from "express";
|
||||
import sql from "../db/index.js";
|
||||
import { requirePerm } from "../middleware/requirePerm.js";
|
||||
import { buildScopeWhere, ownerResolvers } from "../utils/scope.js";
|
||||
import PERM from "../config/permissions.js";
|
||||
|
||||
const r = Router();
|
||||
const OWN = ownerResolvers(sql, 'contract_dwg', 'id');
|
||||
const OWN = ownerResolvers(sql, "contract_dwg", "id");
|
||||
|
||||
// LIST mappings
|
||||
r.get('/',
|
||||
requirePerm(PERM.contract_dwg.read, { scope: 'global' }),
|
||||
async (req, res) => {
|
||||
const { project_id, org_id, condwg_no, limit=50, offset=0 } = req.query;
|
||||
const base = buildScopeWhere(req.principal, { tableAlias: 'm', orgColumn: 'm.org_id', projectColumn: 'm.project_id', permCode: PERM.contract_dwg.read, preferProject: true });
|
||||
const extra = [];
|
||||
const params = { ...base.params, limit: Number(limit), offset: Number(offset) };
|
||||
if (project_id) { extra.push('m.project_id = :project_id'); params.project_id = Number(project_id); }
|
||||
if (org_id) { extra.push('m.org_id = :org_id'); params.org_id = Number(org_id); }
|
||||
if (condwg_no) { extra.push('m.condwg_no = :condwg_no'); params.condwg_no = condwg_no; }
|
||||
const where = [base.where, ...extra].filter(Boolean).join(' AND ');
|
||||
const [rows] = await sql.query(`SELECT m.* FROM contract_dwg m WHERE ${where} ORDER BY m.id DESC LIMIT :limit OFFSET :offset`, params);
|
||||
res.json(rows);
|
||||
}
|
||||
r.get(
|
||||
"/",
|
||||
requirePerm(PERM.contract_dwg.read, { scope: "global" }),
|
||||
async (req, res) => {
|
||||
const { project_id, org_id, condwg_no, limit = 50, offset = 0 } = req.query;
|
||||
const base = buildScopeWhere(req.principal, {
|
||||
tableAlias: "m",
|
||||
orgColumn: "m.org_id",
|
||||
projectColumn: "m.project_id",
|
||||
permCode: PERM.contract_dwg.read,
|
||||
preferProject: true,
|
||||
});
|
||||
const extra = [];
|
||||
const params = {
|
||||
...base.params,
|
||||
limit: Number(limit),
|
||||
offset: Number(offset),
|
||||
};
|
||||
if (project_id) {
|
||||
extra.push("m.project_id = :project_id");
|
||||
params.project_id = Number(project_id);
|
||||
}
|
||||
if (org_id) {
|
||||
extra.push("m.org_id = :org_id");
|
||||
params.org_id = Number(org_id);
|
||||
}
|
||||
if (condwg_no) {
|
||||
extra.push("m.condwg_no = :condwg_no");
|
||||
params.condwg_no = condwg_no;
|
||||
}
|
||||
const where = [base.where, ...extra].filter(Boolean).join(" AND ");
|
||||
const [rows] = await sql.query(
|
||||
`SELECT m.* FROM contract_dwg m WHERE ${where} ORDER BY m.id DESC LIMIT :limit OFFSET :offset`,
|
||||
params
|
||||
);
|
||||
res.json(rows);
|
||||
}
|
||||
);
|
||||
|
||||
|
||||
// GET mapping
|
||||
r.get('/:id',
|
||||
requirePerm(PERM.contract_dwg.read, { scope: 'org', getOrgId: OWN.getOrgIdById }),
|
||||
async (req, res) => {
|
||||
const id = Number(req.params.id);
|
||||
const [[row]] = await sql.query('SELECT * FROM contract_dwg WHERE id=?', [id]);
|
||||
if (!row) return res.status(404).json({ error: 'Not found' });
|
||||
res.json(row);
|
||||
}
|
||||
r.get(
|
||||
"/:id",
|
||||
requirePerm(PERM.contract_dwg.read, {
|
||||
scope: "org",
|
||||
getOrgId: OWN.getOrgIdById,
|
||||
}),
|
||||
async (req, res) => {
|
||||
const id = Number(req.params.id);
|
||||
const [[row]] = await sql.query("SELECT * FROM contract_dwg WHERE id=?", [
|
||||
id,
|
||||
]);
|
||||
if (!row) return res.status(404).json({ error: "Not found" });
|
||||
res.json(row);
|
||||
}
|
||||
);
|
||||
|
||||
|
||||
// CREATE mapping (1 drawing per contract or per rule)
|
||||
r.post('/',
|
||||
requirePerm(PERM.contract_dwg.create, { scope: 'org', getOrgId: async req => req.body?.org_id ?? null }),
|
||||
async (req, res) => {
|
||||
const { org_id, project_id, condwg_no, title, drawing_id, volume_id, sub_cat_id, sub_no, remark } = req.body;
|
||||
const [rs] = await sql.query(`INSERT INTO contract_dwg (org_id, project_id, condwg_no, title, drawing_id, volume_id, sub_cat_id, sub_no, remark, created_by) VALUES (?,?,?,?,?,?,?,?,?,?)`, [org_id, project_id, condwg_no, title, drawing_id, volume_id, sub_cat_id, sub_no, remark, req.principal.userId]);
|
||||
res.json({ id: rs.insertId });
|
||||
}
|
||||
r.post(
|
||||
"/",
|
||||
requirePerm(PERM.contract_dwg.create, {
|
||||
scope: "org",
|
||||
getOrgId: async (req) => req.body?.org_id ?? null,
|
||||
}),
|
||||
async (req, res) => {
|
||||
const {
|
||||
org_id,
|
||||
project_id,
|
||||
condwg_no,
|
||||
title,
|
||||
drawing_id,
|
||||
volume_id,
|
||||
sub_cat_id,
|
||||
sub_no,
|
||||
remark,
|
||||
} = req.body;
|
||||
const [rs] = await sql.query(
|
||||
`INSERT INTO contract_dwg (org_id, project_id, condwg_no, title, drawing_id, volume_id, sub_cat_id, sub_no, remark, created_by) VALUES (?,?,?,?,?,?,?,?,?,?)`,
|
||||
[
|
||||
org_id,
|
||||
project_id,
|
||||
condwg_no,
|
||||
title,
|
||||
drawing_id,
|
||||
volume_id,
|
||||
sub_cat_id,
|
||||
sub_no,
|
||||
remark,
|
||||
req.principal.userId,
|
||||
]
|
||||
);
|
||||
res.json({ id: rs.insertId });
|
||||
}
|
||||
);
|
||||
|
||||
|
||||
// UPDATE
|
||||
r.put('/:id',
|
||||
requirePerm(PERM.contract_dwg.update, { scope: 'org', getOrgId: OWN.getOrgIdById }),
|
||||
async (req, res) => {
|
||||
const id = Number(req.params.id);
|
||||
const { title, remark } = req.body;
|
||||
await sql.query('UPDATE contract_dwg SET title=?, remark=? WHERE id=?', [title, remark, id]);
|
||||
res.json({ ok: 1 });
|
||||
}
|
||||
r.put(
|
||||
"/:id",
|
||||
requirePerm(PERM.contract_dwg.update, {
|
||||
scope: "org",
|
||||
getOrgId: OWN.getOrgIdById,
|
||||
}),
|
||||
async (req, res) => {
|
||||
const id = Number(req.params.id);
|
||||
const { title, remark } = req.body;
|
||||
await sql.query("UPDATE contract_dwg SET title=?, remark=? WHERE id=?", [
|
||||
title,
|
||||
remark,
|
||||
id,
|
||||
]);
|
||||
res.json({ ok: 1 });
|
||||
}
|
||||
);
|
||||
|
||||
|
||||
// DELETE
|
||||
r.delete('/:id',
|
||||
requirePerm(PERM.contract_dwg.delete, { scope: 'org', getOrgId: OWN.getOrgIdById }),
|
||||
async (req, res) => {
|
||||
const id = Number(req.params.id);
|
||||
await sql.query('DELETE FROM contract_dwg WHERE id=?', [id]);
|
||||
res.json({ ok: 1 });
|
||||
}
|
||||
r.delete(
|
||||
"/:id",
|
||||
requirePerm(PERM.contract_dwg.delete, {
|
||||
scope: "org",
|
||||
getOrgId: OWN.getOrgIdById,
|
||||
}),
|
||||
async (req, res) => {
|
||||
const id = Number(req.params.id);
|
||||
await sql.query("DELETE FROM contract_dwg WHERE id=?", [id]);
|
||||
res.json({ ok: 1 });
|
||||
}
|
||||
);
|
||||
|
||||
|
||||
export default r;
|
||||
export default r;
|
||||
|
||||
@@ -1,72 +1,130 @@
|
||||
import { Router } from 'express';
|
||||
import sql from '../db/index.js';
|
||||
import { requirePerm } from '../middleware/requirePerm.js';
|
||||
import { buildScopeWhere, ownerResolvers } from '../utils/scope.js';
|
||||
import PERM from '../config/permissions.js';
|
||||
// FILE: src/routes/contracts.js
|
||||
// Contracts routes
|
||||
// - CRUD operations for contracts
|
||||
// - Requires appropriate permissions via requirePerm middleware
|
||||
// - Uses org scope for all permissions
|
||||
// - contract.read, contract.create, contract.update, contract.delete
|
||||
// - Contract fields: id (PK), org_id, project_id, contract_no, title, status, created_by
|
||||
// - Basic filtering on list endpoint by project_id, org_id, contract_no
|
||||
// - Uses async/await for asynchronous operations
|
||||
// - Middleware functions are used for permission checks
|
||||
// - Owner resolvers are used to fetch org_id for specific contract ids
|
||||
|
||||
import { Router } from "express";
|
||||
import sql from "../db/index.js";
|
||||
import { requirePerm } from "../middleware/requirePerm.js";
|
||||
import { buildScopeWhere, ownerResolvers } from "../utils/scope.js";
|
||||
import PERM from "../config/permissions.js";
|
||||
|
||||
const r = Router();
|
||||
const OWN = ownerResolvers(sql, 'contracts', 'id');
|
||||
const OWN = ownerResolvers(sql, "contracts", "id");
|
||||
|
||||
|
||||
r.get('/',
|
||||
requirePerm(PERM.contract.read, { scope: 'global' }),
|
||||
async (req, res) => {
|
||||
const { project_id, org_id, contract_no, q, limit = 50, offset = 0 } = req.query;
|
||||
const base = buildScopeWhere(req.principal, { tableAlias: 'c', orgColumn: 'c.org_id', projectColumn: 'c.project_id', permCode: PERM.contract.read, preferProject: true });
|
||||
const extra = [];
|
||||
const params = { ...base.params, limit: Number(limit), offset: Number(offset) };
|
||||
if (project_id) { extra.push('c.project_id = :project_id'); params.project_id = Number(project_id); }
|
||||
if (org_id) { extra.push('c.org_id = :org_id'); params.org_id = Number(org_id); }
|
||||
if (contract_no){ extra.push('c.contract_no = :contract_no'); params.contract_no = contract_no; }
|
||||
if (q) { extra.push('(c.contract_no LIKE :q OR c.title LIKE :q)'); params.q = `%${q}%`; }
|
||||
const where = [base.where, ...extra].filter(Boolean).join(' AND ');
|
||||
const [rows] = await sql.query(`SELECT c.* FROM contracts c WHERE ${where} ORDER BY c.id DESC LIMIT :limit OFFSET :offset`, params);
|
||||
res.json(rows);
|
||||
}
|
||||
r.get(
|
||||
"/",
|
||||
requirePerm(PERM.contract.read, { scope: "global" }),
|
||||
async (req, res) => {
|
||||
const {
|
||||
project_id,
|
||||
org_id,
|
||||
contract_no,
|
||||
q,
|
||||
limit = 50,
|
||||
offset = 0,
|
||||
} = req.query;
|
||||
const base = buildScopeWhere(req.principal, {
|
||||
tableAlias: "c",
|
||||
orgColumn: "c.org_id",
|
||||
projectColumn: "c.project_id",
|
||||
permCode: PERM.contract.read,
|
||||
preferProject: true,
|
||||
});
|
||||
const extra = [];
|
||||
const params = {
|
||||
...base.params,
|
||||
limit: Number(limit),
|
||||
offset: Number(offset),
|
||||
};
|
||||
if (project_id) {
|
||||
extra.push("c.project_id = :project_id");
|
||||
params.project_id = Number(project_id);
|
||||
}
|
||||
if (org_id) {
|
||||
extra.push("c.org_id = :org_id");
|
||||
params.org_id = Number(org_id);
|
||||
}
|
||||
if (contract_no) {
|
||||
extra.push("c.contract_no = :contract_no");
|
||||
params.contract_no = contract_no;
|
||||
}
|
||||
if (q) {
|
||||
extra.push("(c.contract_no LIKE :q OR c.title LIKE :q)");
|
||||
params.q = `%${q}%`;
|
||||
}
|
||||
const where = [base.where, ...extra].filter(Boolean).join(" AND ");
|
||||
const [rows] = await sql.query(
|
||||
`SELECT c.* FROM contracts c WHERE ${where} ORDER BY c.id DESC LIMIT :limit OFFSET :offset`,
|
||||
params
|
||||
);
|
||||
res.json(rows);
|
||||
}
|
||||
);
|
||||
|
||||
|
||||
r.get('/:id',
|
||||
requirePerm(PERM.contract.read, { scope: 'org', getOrgId: OWN.getOrgIdById }),
|
||||
async (req, res) => {
|
||||
const id = Number(req.params.id);
|
||||
const [[row]] = await sql.query('SELECT * FROM contracts WHERE id=?', [id]);
|
||||
if (!row) return res.status(404).json({ error: 'Not found' });
|
||||
res.json(row);
|
||||
}
|
||||
r.get(
|
||||
"/:id",
|
||||
requirePerm(PERM.contract.read, { scope: "org", getOrgId: OWN.getOrgIdById }),
|
||||
async (req, res) => {
|
||||
const id = Number(req.params.id);
|
||||
const [[row]] = await sql.query("SELECT * FROM contracts WHERE id=?", [id]);
|
||||
if (!row) return res.status(404).json({ error: "Not found" });
|
||||
res.json(row);
|
||||
}
|
||||
);
|
||||
|
||||
|
||||
r.post('/',
|
||||
requirePerm(PERM.contract.create, { scope: 'org', getOrgId: async req => req.body?.org_id ?? null }),
|
||||
async (req, res) => {
|
||||
const { org_id, project_id, contract_no, title, status } = req.body;
|
||||
const [rs] = await sql.query(`INSERT INTO contracts (org_id, project_id, contract_no, title, status, created_by) VALUES (?,?,?,?,?,?)`, [org_id, project_id, contract_no, title, status, req.principal.userId]);
|
||||
res.json({ id: rs.insertId });
|
||||
}
|
||||
r.post(
|
||||
"/",
|
||||
requirePerm(PERM.contract.create, {
|
||||
scope: "org",
|
||||
getOrgId: async (req) => req.body?.org_id ?? null,
|
||||
}),
|
||||
async (req, res) => {
|
||||
const { org_id, project_id, contract_no, title, status } = req.body;
|
||||
const [rs] = await sql.query(
|
||||
`INSERT INTO contracts (org_id, project_id, contract_no, title, status, created_by) VALUES (?,?,?,?,?,?)`,
|
||||
[org_id, project_id, contract_no, title, status, req.principal.userId]
|
||||
);
|
||||
res.json({ id: rs.insertId });
|
||||
}
|
||||
);
|
||||
|
||||
|
||||
r.put('/:id',
|
||||
requirePerm(PERM.contract.update, { scope: 'org', getOrgId: OWN.getOrgIdById }),
|
||||
async (req, res) => {
|
||||
const id = Number(req.params.id);
|
||||
const { title, status } = req.body;
|
||||
await sql.query('UPDATE contracts SET title=?, status=? WHERE id=?', [title, status, id]);
|
||||
res.json({ ok: 1 });
|
||||
}
|
||||
r.put(
|
||||
"/:id",
|
||||
requirePerm(PERM.contract.update, {
|
||||
scope: "org",
|
||||
getOrgId: OWN.getOrgIdById,
|
||||
}),
|
||||
async (req, res) => {
|
||||
const id = Number(req.params.id);
|
||||
const { title, status } = req.body;
|
||||
await sql.query("UPDATE contracts SET title=?, status=? WHERE id=?", [
|
||||
title,
|
||||
status,
|
||||
id,
|
||||
]);
|
||||
res.json({ ok: 1 });
|
||||
}
|
||||
);
|
||||
|
||||
|
||||
r.delete('/:id',
|
||||
requirePerm(PERM.contract.delete, { scope: 'org', getOrgId: OWN.getOrgIdById }),
|
||||
async (req, res) => {
|
||||
const id = Number(req.params.id);
|
||||
await sql.query('DELETE FROM contracts WHERE id=?', [id]);
|
||||
res.json({ ok: 1 });
|
||||
}
|
||||
r.delete(
|
||||
"/:id",
|
||||
requirePerm(PERM.contract.delete, {
|
||||
scope: "org",
|
||||
getOrgId: OWN.getOrgIdById,
|
||||
}),
|
||||
async (req, res) => {
|
||||
const id = Number(req.params.id);
|
||||
await sql.query("DELETE FROM contracts WHERE id=?", [id]);
|
||||
res.json({ ok: 1 });
|
||||
}
|
||||
);
|
||||
|
||||
|
||||
export default r;
|
||||
export default r;
|
||||
|
||||
@@ -1,74 +1,124 @@
|
||||
import { Router } from 'express';
|
||||
import sql from '../db/index.js';
|
||||
import { requirePerm } from '../middleware/requirePerm.js';
|
||||
import { buildScopeWhere, ownerResolvers } from '../utils/scope.js';
|
||||
import PERM from '../config/permissions.js';
|
||||
// FILE: src/routes/correspondences.js
|
||||
// 03.2 7) เพิ่ม routes/correspondences.js (ใหม่)
|
||||
// - ใช้ร่วมกับ requirePerm() และ buildScopeWhere()
|
||||
// - สำหรับจัดการ correspondences (ดู/เพิ่ม/แก้ไข/ลบ) ตามสิทธิ์ของผู้ใช้
|
||||
// Correspondences routes
|
||||
// - CRUD operations for correspondences
|
||||
// - Requires appropriate permissions via requirePerm middleware
|
||||
// - Uses org scope for all permissions
|
||||
// - correspondence:read, correspondence:create, correspondence:update, correspondence:delete
|
||||
// - Correspondence fields: id (PK), org_id, project_id, corr_no, subject, status, created_by
|
||||
// - Basic validation: org_id, corr_no, subject required for create
|
||||
|
||||
import { Router } from "express";
|
||||
import sql from "../db/index.js";
|
||||
import { requirePerm } from "../middleware/requirePerm.js";
|
||||
import { buildScopeWhere, ownerResolvers } from "../utils/scope.js";
|
||||
import PERM from "../config/permissions.js";
|
||||
|
||||
const r = Router();
|
||||
const OWN = ownerResolvers(sql, 'correspondences', 'id');
|
||||
const OWN = ownerResolvers(sql, "correspondences", "id");
|
||||
|
||||
|
||||
r.get('/',
|
||||
requirePerm(PERM.correspondence.read, { scope: 'global' }),
|
||||
async (req, res) => {
|
||||
const { project_id, org_id, q, limit = 50, offset = 0 } = req.query;
|
||||
const base = buildScopeWhere(req.principal, {
|
||||
tableAlias: 'c', orgColumn: 'c.org_id', projectColumn: 'c.project_id',
|
||||
permCode: PERM.correspondence.read, preferProject: true,
|
||||
});
|
||||
const extra = [];
|
||||
const params = { ...base.params, limit: Number(limit), offset: Number(offset) };
|
||||
if (project_id) { extra.push('c.project_id = :project_id'); params.project_id = Number(project_id); }
|
||||
if (org_id) { extra.push('c.org_id = :org_id'); params.org_id = Number(org_id); }
|
||||
if (q) { extra.push('(c.corr_no LIKE :q OR c.subject LIKE :q)'); params.q = `%${q}%`; }
|
||||
const where = [base.where, ...extra].join(' AND ');
|
||||
const [rows] = await sql.query(`SELECT c.* FROM correspondences c WHERE ${where} ORDER BY c.id DESC LIMIT :limit OFFSET :offset`, params);
|
||||
res.json(rows);
|
||||
}
|
||||
r.get(
|
||||
"/",
|
||||
requirePerm(PERM.correspondence.read, { scope: "global" }),
|
||||
async (req, res) => {
|
||||
const { project_id, org_id, q, limit = 50, offset = 0 } = req.query;
|
||||
const base = buildScopeWhere(req.principal, {
|
||||
tableAlias: "c",
|
||||
orgColumn: "c.org_id",
|
||||
projectColumn: "c.project_id",
|
||||
permCode: PERM.correspondence.read,
|
||||
preferProject: true,
|
||||
});
|
||||
const extra = [];
|
||||
const params = {
|
||||
...base.params,
|
||||
limit: Number(limit),
|
||||
offset: Number(offset),
|
||||
};
|
||||
if (project_id) {
|
||||
extra.push("c.project_id = :project_id");
|
||||
params.project_id = Number(project_id);
|
||||
}
|
||||
if (org_id) {
|
||||
extra.push("c.org_id = :org_id");
|
||||
params.org_id = Number(org_id);
|
||||
}
|
||||
if (q) {
|
||||
extra.push("(c.corr_no LIKE :q OR c.subject LIKE :q)");
|
||||
params.q = `%${q}%`;
|
||||
}
|
||||
const where = [base.where, ...extra].join(" AND ");
|
||||
const [rows] = await sql.query(
|
||||
`SELECT c.* FROM correspondences c WHERE ${where} ORDER BY c.id DESC LIMIT :limit OFFSET :offset`,
|
||||
params
|
||||
);
|
||||
res.json(rows);
|
||||
}
|
||||
);
|
||||
|
||||
|
||||
r.get('/:id',
|
||||
requirePerm(PERM.correspondence.read, { scope: 'org', getOrgId: OWN.getOrgIdById }),
|
||||
async (req, res) => {
|
||||
const id = Number(req.params.id);
|
||||
const [[row]] = await sql.query('SELECT * FROM correspondences WHERE id=?', [id]);
|
||||
if (!row) return res.status(404).json({ error: 'Not found' });
|
||||
res.json(row);
|
||||
}
|
||||
r.get(
|
||||
"/:id",
|
||||
requirePerm(PERM.correspondence.read, {
|
||||
scope: "org",
|
||||
getOrgId: OWN.getOrgIdById,
|
||||
}),
|
||||
async (req, res) => {
|
||||
const id = Number(req.params.id);
|
||||
const [[row]] = await sql.query(
|
||||
"SELECT * FROM correspondences WHERE id=?",
|
||||
[id]
|
||||
);
|
||||
if (!row) return res.status(404).json({ error: "Not found" });
|
||||
res.json(row);
|
||||
}
|
||||
);
|
||||
|
||||
|
||||
r.post('/',
|
||||
requirePerm(PERM.correspondence.create, { scope: 'org', getOrgId: async req => req.body?.org_id ?? null }),
|
||||
async (req, res) => {
|
||||
const { org_id, project_id, corr_no, subject, status } = req.body;
|
||||
const [rs] = await sql.query(`INSERT INTO correspondences (org_id, project_id, corr_no, subject, status, created_by) VALUES (?,?,?,?,?,?)`, [org_id, project_id, corr_no, subject, status, req.principal.userId]);
|
||||
res.json({ id: rs.insertId });
|
||||
}
|
||||
r.post(
|
||||
"/",
|
||||
requirePerm(PERM.correspondence.create, {
|
||||
scope: "org",
|
||||
getOrgId: async (req) => req.body?.org_id ?? null,
|
||||
}),
|
||||
async (req, res) => {
|
||||
const { org_id, project_id, corr_no, subject, status } = req.body;
|
||||
const [rs] = await sql.query(
|
||||
`INSERT INTO correspondences (org_id, project_id, corr_no, subject, status, created_by) VALUES (?,?,?,?,?,?)`,
|
||||
[org_id, project_id, corr_no, subject, status, req.principal.userId]
|
||||
);
|
||||
res.json({ id: rs.insertId });
|
||||
}
|
||||
);
|
||||
|
||||
|
||||
r.put('/:id',
|
||||
requirePerm(PERM.correspondence.update, { scope: 'org', getOrgId: OWN.getOrgIdById }),
|
||||
async (req, res) => {
|
||||
const id = Number(req.params.id);
|
||||
const { subject, status } = req.body;
|
||||
await sql.query('UPDATE correspondences SET subject=?, status=? WHERE id=?', [subject, status, id]);
|
||||
res.json({ ok: 1 });
|
||||
}
|
||||
r.put(
|
||||
"/:id",
|
||||
requirePerm(PERM.correspondence.update, {
|
||||
scope: "org",
|
||||
getOrgId: OWN.getOrgIdById,
|
||||
}),
|
||||
async (req, res) => {
|
||||
const id = Number(req.params.id);
|
||||
const { subject, status } = req.body;
|
||||
await sql.query(
|
||||
"UPDATE correspondences SET subject=?, status=? WHERE id=?",
|
||||
[subject, status, id]
|
||||
);
|
||||
res.json({ ok: 1 });
|
||||
}
|
||||
);
|
||||
|
||||
|
||||
r.delete('/:id',
|
||||
requirePerm(PERM.correspondence.delete, { scope: 'org', getOrgId: OWN.getOrgIdById }),
|
||||
async (req, res) => {
|
||||
const id = Number(req.params.id);
|
||||
await sql.query('DELETE FROM correspondences WHERE id=?', [id]);
|
||||
res.json({ ok: 1 });
|
||||
}
|
||||
r.delete(
|
||||
"/:id",
|
||||
requirePerm(PERM.correspondence.delete, {
|
||||
scope: "org",
|
||||
getOrgId: OWN.getOrgIdById,
|
||||
}),
|
||||
async (req, res) => {
|
||||
const id = Number(req.params.id);
|
||||
await sql.query("DELETE FROM correspondences WHERE id=?", [id]);
|
||||
res.json({ ok: 1 });
|
||||
}
|
||||
);
|
||||
|
||||
|
||||
export default r;
|
||||
export default r;
|
||||
|
||||
@@ -1,3 +1,12 @@
|
||||
// FILE: src/routes/documents.js
|
||||
// Documents routes
|
||||
// - CRUD operations for documents
|
||||
// - Requires appropriate permissions via requirePerm middleware
|
||||
// - Uses project scope for all permissions
|
||||
// - document:read, document:create, document:update, document:delete
|
||||
// - Document fields: document_id (PK), project_id, doc_no, title, category, status, created_by, updated_by
|
||||
// - Basic validation: project_id and doc_no required for create
|
||||
|
||||
import { Router } from 'express';
|
||||
import { requireAuth } from '../middleware/auth.js';
|
||||
import { enrichPermissions } from '../middleware/permissions.js';
|
||||
|
||||
@@ -1,31 +1,63 @@
|
||||
import { Router } from 'express';
|
||||
import sql from '../db/index.js';
|
||||
import { requirePerm } from '../middleware/requirePerm.js';
|
||||
import { buildScopeWhere, ownerResolvers } from '../utils/scope.js';
|
||||
import PERM from '../config/permissions.js';
|
||||
// FILE: src/routes/drawings.js
|
||||
// 03.2 9) เพิ่ม routes/drawings.js (ใหม่)
|
||||
// - ใช้ร่วมกับ requirePerm() และ buildScopeWhere()
|
||||
// - สำหรับจัดการ drawings (ดู/เพิ่ม/แก้ไข/ลบ) ตามสิทธิ์ของผู้ใช้
|
||||
// Drawings routes
|
||||
// - CRUD operations for drawings
|
||||
// - Requires appropriate permissions via requirePerm middleware
|
||||
// - Uses org scope for all permissions
|
||||
// - drawing:read, drawing:create, drawing:update, drawing:delete
|
||||
// - Drawing fields: id (PK), org_id, project_id, dwg_no, dwg_code, title, created_by
|
||||
// - Basic validation: org_id, dwg_no required for create
|
||||
|
||||
import { Router } from "express";
|
||||
import sql from "../db/index.js";
|
||||
import { requirePerm } from "../middleware/requirePerm.js";
|
||||
import { buildScopeWhere, ownerResolvers } from "../utils/scope.js";
|
||||
import PERM from "../config/permissions.js";
|
||||
|
||||
const r = Router();
|
||||
const OWN = ownerResolvers(sql, 'drawings', 'id');
|
||||
const OWN = ownerResolvers(sql, "drawings", "id");
|
||||
|
||||
// LIST
|
||||
r.get('/',
|
||||
requirePerm('drawing.read', { scope: 'global' }),
|
||||
r.get(
|
||||
"/",
|
||||
requirePerm("drawing.read", { scope: "global" }),
|
||||
async (req, res) => {
|
||||
const { project_id, org_id, code, q, limit = 50, offset = 0 } = req.query;
|
||||
|
||||
const base = buildScopeWhere(req.principal, {
|
||||
tableAlias: 'd', orgColumn: 'd.org_id', projectColumn: 'd.project_id',
|
||||
permCode: 'drawing.read', preferProject: true,
|
||||
tableAlias: "d",
|
||||
orgColumn: "d.org_id",
|
||||
projectColumn: "d.project_id",
|
||||
permCode: "drawing.read",
|
||||
preferProject: true,
|
||||
});
|
||||
|
||||
const extra = [];
|
||||
const params = { ...base.params, limit: Number(limit), offset: Number(offset) };
|
||||
if (project_id) { extra.push('d.project_id = :project_id'); params.project_id = Number(project_id); }
|
||||
if (org_id) { extra.push('d.org_id = :org_id'); params.org_id = Number(org_id); }
|
||||
if (code) { extra.push('d.dwg_code = :code'); params.code = code; }
|
||||
if (q) { extra.push('(d.dwg_no LIKE :q OR d.title LIKE :q)'); params.q = `%${q}%`; }
|
||||
const params = {
|
||||
...base.params,
|
||||
limit: Number(limit),
|
||||
offset: Number(offset),
|
||||
};
|
||||
if (project_id) {
|
||||
extra.push("d.project_id = :project_id");
|
||||
params.project_id = Number(project_id);
|
||||
}
|
||||
if (org_id) {
|
||||
extra.push("d.org_id = :org_id");
|
||||
params.org_id = Number(org_id);
|
||||
}
|
||||
if (code) {
|
||||
extra.push("d.dwg_code = :code");
|
||||
params.code = code;
|
||||
}
|
||||
if (q) {
|
||||
extra.push("(d.dwg_no LIKE :q OR d.title LIKE :q)");
|
||||
params.q = `%${q}%`;
|
||||
}
|
||||
|
||||
const where = [base.where, ...extra].filter(Boolean).join(' AND ');
|
||||
const where = [base.where, ...extra].filter(Boolean).join(" AND ");
|
||||
|
||||
const [rows] = await sql.query(
|
||||
`SELECT d.* FROM drawings d WHERE ${where}
|
||||
@@ -37,19 +69,24 @@ r.get('/',
|
||||
);
|
||||
|
||||
// GET
|
||||
r.get('/:id',
|
||||
requirePerm('drawing.read', { scope: 'org', getOrgId: OWN.getOrgIdById }),
|
||||
r.get(
|
||||
"/:id",
|
||||
requirePerm("drawing.read", { scope: "org", getOrgId: OWN.getOrgIdById }),
|
||||
async (req, res) => {
|
||||
const id = Number(req.params.id);
|
||||
const [[row]] = await sql.query('SELECT * FROM drawings WHERE id=?', [id]);
|
||||
if (!row) return res.status(404).json({ error: 'Not found' });
|
||||
const [[row]] = await sql.query("SELECT * FROM drawings WHERE id=?", [id]);
|
||||
if (!row) return res.status(404).json({ error: "Not found" });
|
||||
res.json(row);
|
||||
}
|
||||
);
|
||||
|
||||
// CREATE
|
||||
r.post('/',
|
||||
requirePerm('drawing.create', { scope: 'org', getOrgId: async req => req.body?.org_id ?? null }),
|
||||
r.post(
|
||||
"/",
|
||||
requirePerm("drawing.create", {
|
||||
scope: "org",
|
||||
getOrgId: async (req) => req.body?.org_id ?? null,
|
||||
}),
|
||||
async (req, res) => {
|
||||
const { org_id, project_id, dwg_no, dwg_code, title } = req.body;
|
||||
const [rs] = await sql.query(
|
||||
@@ -62,22 +99,24 @@ r.post('/',
|
||||
);
|
||||
|
||||
// UPDATE
|
||||
r.put('/:id',
|
||||
requirePerm('drawing.update', { scope: 'org', getOrgId: OWN.getOrgIdById }),
|
||||
r.put(
|
||||
"/:id",
|
||||
requirePerm("drawing.update", { scope: "org", getOrgId: OWN.getOrgIdById }),
|
||||
async (req, res) => {
|
||||
const id = Number(req.params.id);
|
||||
const { title } = req.body;
|
||||
await sql.query('UPDATE drawings SET title=? WHERE id=?', [title, id]);
|
||||
await sql.query("UPDATE drawings SET title=? WHERE id=?", [title, id]);
|
||||
res.json({ ok: 1 });
|
||||
}
|
||||
);
|
||||
|
||||
// DELETE
|
||||
r.delete('/:id',
|
||||
requirePerm('drawing.delete', { scope: 'org', getOrgId: OWN.getOrgIdById }),
|
||||
r.delete(
|
||||
"/:id",
|
||||
requirePerm("drawing.delete", { scope: "org", getOrgId: OWN.getOrgIdById }),
|
||||
async (req, res) => {
|
||||
const id = Number(req.params.id);
|
||||
await sql.query('DELETE FROM drawings WHERE id=?', [id]);
|
||||
await sql.query("DELETE FROM drawings WHERE id=?", [id]);
|
||||
res.json({ ok: 1 });
|
||||
}
|
||||
);
|
||||
|
||||
@@ -1,90 +1,150 @@
|
||||
import { Router } from 'express';
|
||||
import fs from 'fs';
|
||||
import path from 'path';
|
||||
import jwt from 'jsonwebtoken';
|
||||
import { requireAuth } from '../middleware/auth.js';
|
||||
import { enrichPermissions } from '../middleware/permissions.js';
|
||||
import { requireRole } from '../middleware/rbac.js';
|
||||
import { requirePerm } from '../middleware/permGuard.js';
|
||||
import { sequelize } from '../db/sequelize.js';
|
||||
import FileModel from '../db/models/FileObject.js';
|
||||
import { config } from '../config.js';
|
||||
// FILE: src/routes/files_extras.js
|
||||
// Extra file-related routes
|
||||
// - HEAD for file meta
|
||||
// - DELETE to delete a file (physical + record)
|
||||
// - POST to rename a file (meta only)
|
||||
// - POST to refresh signed download URL
|
||||
// - Requires authentication and appropriate permissions via requireAuth, requirePerm, and enrichPermissions middleware
|
||||
// - Uses project scope for file access permissions
|
||||
// - file:read, file:create, file:update, file:delete
|
||||
// - File fields: file_id (PK), module, ref_id, orig_name, disk_path, mime, size, created_by
|
||||
|
||||
import { Router } from "express";
|
||||
import fs from "fs";
|
||||
import path from "path";
|
||||
import jwt from "jsonwebtoken";
|
||||
import { requireAuth } from "../middleware/auth.js";
|
||||
import { enrichPermissions } from "../middleware/permissions.js";
|
||||
import { requireRole } from "../middleware/rbac.js";
|
||||
import { requirePerm } from "../middleware/permGuard.js";
|
||||
import { sequelize } from "../db/sequelize.js";
|
||||
import FileModel from "../db/models/FileObject.js";
|
||||
import { config } from "../config.js";
|
||||
|
||||
const r = Router();
|
||||
const Files = FileModel(sequelize);
|
||||
|
||||
async function projectForFile(rec) {
|
||||
const mod = rec.module; const refId = rec.ref_id;
|
||||
const mod = rec.module;
|
||||
const refId = rec.ref_id;
|
||||
switch (mod) {
|
||||
case 'rfa': { const M = (await import('../db/models/RFA.js')).default(sequelize); const row = await M.findByPk(refId); return row?.project_id||null; }
|
||||
case 'correspondence': { const M = (await import('../db/models/Correspondence.js')).default(sequelize); const row = await M.findByPk(refId); return row?.project_id||null; }
|
||||
case 'drawing': { const M = (await import('../db/models/Drawing.js')).default(sequelize); const row = await M.findByPk(refId); return row?.project_id||null; }
|
||||
case 'document': { const M = (await import('../db/models/Document.js')).default(sequelize); const row = await M.findByPk(refId); return row?.project_id||null; }
|
||||
case 'transmittal': { const M = (await import('../db/models/Transmittal.js')).default(sequelize); const row = await M.findByPk(refId); return row?.project_id||null; }
|
||||
default: return null;
|
||||
case "rfa": {
|
||||
const M = (await import("../db/models/RFA.js")).default(sequelize);
|
||||
const row = await M.findByPk(refId);
|
||||
return row?.project_id || null;
|
||||
}
|
||||
case "correspondence": {
|
||||
const M = (await import("../db/models/Correspondence.js")).default(
|
||||
sequelize
|
||||
);
|
||||
const row = await M.findByPk(refId);
|
||||
return row?.project_id || null;
|
||||
}
|
||||
case "drawing": {
|
||||
const M = (await import("../db/models/Drawing.js")).default(sequelize);
|
||||
const row = await M.findByPk(refId);
|
||||
return row?.project_id || null;
|
||||
}
|
||||
case "document": {
|
||||
const M = (await import("../db/models/Document.js")).default(sequelize);
|
||||
const row = await M.findByPk(refId);
|
||||
return row?.project_id || null;
|
||||
}
|
||||
case "transmittal": {
|
||||
const M = (await import("../db/models/Transmittal.js")).default(
|
||||
sequelize
|
||||
);
|
||||
const row = await M.findByPk(refId);
|
||||
return row?.project_id || null;
|
||||
}
|
||||
default:
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// HEAD meta only
|
||||
r.head('/files/:file_id', requireAuth, async (req, res) => {
|
||||
r.head("/files/:file_id", requireAuth, async (req, res) => {
|
||||
const rec = await Files.findByPk(Number(req.params.file_id));
|
||||
if (!rec) return res.status(404).end();
|
||||
res.setHeader('Content-Type', rec.mime || 'application/octet-stream');
|
||||
res.setHeader('Content-Length', String(rec.size || 0));
|
||||
res.setHeader("Content-Type", rec.mime || "application/octet-stream");
|
||||
res.setHeader("Content-Length", String(rec.size || 0));
|
||||
res.status(200).end();
|
||||
});
|
||||
|
||||
// delete (soft delete is recommended; here we do physical delete + record delete)
|
||||
r.delete('/files/:file_id', requireAuth, enrichPermissions(), requirePerm('file:delete'), async (req, res) => {
|
||||
const rec = await Files.findByPk(Number(req.params.file_id));
|
||||
if (!rec) return res.status(404).json({ error: 'Not found' });
|
||||
const pid = await projectForFile(rec);
|
||||
const roles = req.user?.roles || [];
|
||||
const isAdmin = roles.includes('Admin');
|
||||
if (!isAdmin) {
|
||||
const { getUserProjectIds } = await import('../middleware/abac.js');
|
||||
const memberProjects = await getUserProjectIds(req.user?.user_id);
|
||||
if (!memberProjects.includes(pid)) return res.status(403).json({ error: 'Forbidden: not a project member' });
|
||||
r.delete(
|
||||
"/files/:file_id",
|
||||
requireAuth,
|
||||
enrichPermissions(),
|
||||
requirePerm("file:delete"),
|
||||
async (req, res) => {
|
||||
const rec = await Files.findByPk(Number(req.params.file_id));
|
||||
if (!rec) return res.status(404).json({ error: "Not found" });
|
||||
const pid = await projectForFile(rec);
|
||||
const roles = req.user?.roles || [];
|
||||
const isAdmin = roles.includes("Admin");
|
||||
if (!isAdmin) {
|
||||
const { getUserProjectIds } = await import("../middleware/abac.js");
|
||||
const memberProjects = await getUserProjectIds(req.user?.user_id);
|
||||
if (!memberProjects.includes(pid))
|
||||
return res
|
||||
.status(403)
|
||||
.json({ error: "Forbidden: not a project member" });
|
||||
}
|
||||
try {
|
||||
fs.unlinkSync(rec.disk_path);
|
||||
} catch {}
|
||||
await rec.destroy();
|
||||
res.json({ ok: true });
|
||||
}
|
||||
try { fs.unlinkSync(rec.disk_path); } catch {}
|
||||
await rec.destroy();
|
||||
res.json({ ok: true });
|
||||
});
|
||||
);
|
||||
|
||||
// rename (meta only - keep disk file name)
|
||||
r.post('/files/:file_id/rename', requireAuth, enrichPermissions(), requirePerm('file:update'), async (req, res) => {
|
||||
const rec = await Files.findByPk(Number(req.params.file_id));
|
||||
if (!rec) return res.status(404).json({ error: 'Not found' });
|
||||
const pid = await projectForFile(rec);
|
||||
const roles = req.user?.roles || [];
|
||||
const isAdmin = roles.includes('Admin');
|
||||
if (!isAdmin) {
|
||||
const { getUserProjectIds } = await import('../middleware/abac.js');
|
||||
const memberProjects = await getUserProjectIds(req.user?.user_id);
|
||||
if (!memberProjects.includes(pid)) return res.status(403).json({ error: 'Forbidden: not a project member' });
|
||||
r.post(
|
||||
"/files/:file_id/rename",
|
||||
requireAuth,
|
||||
enrichPermissions(),
|
||||
requirePerm("file:update"),
|
||||
async (req, res) => {
|
||||
const rec = await Files.findByPk(Number(req.params.file_id));
|
||||
if (!rec) return res.status(404).json({ error: "Not found" });
|
||||
const pid = await projectForFile(rec);
|
||||
const roles = req.user?.roles || [];
|
||||
const isAdmin = roles.includes("Admin");
|
||||
if (!isAdmin) {
|
||||
const { getUserProjectIds } = await import("../middleware/abac.js");
|
||||
const memberProjects = await getUserProjectIds(req.user?.user_id);
|
||||
if (!memberProjects.includes(pid))
|
||||
return res
|
||||
.status(403)
|
||||
.json({ error: "Forbidden: not a project member" });
|
||||
}
|
||||
const { orig_name } = req.body || {};
|
||||
if (!orig_name)
|
||||
return res.status(400).json({ error: "orig_name required" });
|
||||
rec.orig_name = orig_name;
|
||||
await rec.save();
|
||||
res.json({ ok: true });
|
||||
}
|
||||
const { orig_name } = req.body || {};
|
||||
if (!orig_name) return res.status(400).json({ error: 'orig_name required' });
|
||||
rec.orig_name = orig_name;
|
||||
await rec.save();
|
||||
res.json({ ok: true });
|
||||
});
|
||||
);
|
||||
|
||||
// refresh signed download url
|
||||
r.post('/files/:file_id/refresh-url', requireAuth, async (req, res) => {
|
||||
r.post("/files/:file_id/refresh-url", requireAuth, async (req, res) => {
|
||||
const rec = await Files.findByPk(Number(req.params.file_id));
|
||||
if (!rec) return res.status(404).json({ error: 'Not found' });
|
||||
if (!rec) return res.status(404).json({ error: "Not found" });
|
||||
const pid = await projectForFile(rec);
|
||||
const roles = req.user?.roles || [];
|
||||
const isAdmin = roles.includes('Admin');
|
||||
const isAdmin = roles.includes("Admin");
|
||||
if (!isAdmin) {
|
||||
const { getUserProjectIds } = await import('../middleware/abac.js');
|
||||
const { getUserProjectIds } = await import("../middleware/abac.js");
|
||||
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" });
|
||||
}
|
||||
const expSec = Number(process.env.FILE_URL_EXPIRES || 600);
|
||||
const token = jwt.sign({ file_id: rec.file_id }, config.JWT.SECRET, { expiresIn: expSec });
|
||||
const token = jwt.sign({ file_id: rec.file_id }, config.JWT.SECRET, {
|
||||
expiresIn: expSec,
|
||||
});
|
||||
const download_url = `/api/v1/files/${rec.file_id}?token=${token}`;
|
||||
res.json({ download_url, expires_in: expSec });
|
||||
});
|
||||
|
||||
@@ -1,13 +1,18 @@
|
||||
import { Router } from 'express';
|
||||
import { sequelize } from '../db/sequelize.js';
|
||||
// FILE: src/routes/health.js
|
||||
// Health check route
|
||||
// - GET /health to check server and database status
|
||||
// - Requires appropriate permissions via requirePerm middleware
|
||||
|
||||
import { Router } from "express";
|
||||
import { sequelize } from "../db/sequelize.js";
|
||||
|
||||
const r = Router();
|
||||
r.get('/health', async (_req, res) => {
|
||||
r.get("/health", async (_req, res) => {
|
||||
try {
|
||||
await sequelize.query('SELECT 1 AS ok');
|
||||
res.status(200).json({ ok: true, db: 'up' });
|
||||
await sequelize.query("SELECT 1 AS ok");
|
||||
res.status(200).json({ ok: true, db: "up" });
|
||||
} catch (e) {
|
||||
res.status(500).json({ ok: false, db: 'down', error: String(e) });
|
||||
res.status(500).json({ ok: false, db: "down", error: String(e) });
|
||||
}
|
||||
});
|
||||
export default r;
|
||||
|
||||
@@ -1,9 +1,17 @@
|
||||
// src/routes/lookup.js (ESM)
|
||||
import { Router } from 'express';
|
||||
import sql from '../db/index.js';
|
||||
import { requirePerm } from '../middleware/requirePerm.js';
|
||||
import { buildScopeWhere } from '../utils/scope.js';
|
||||
import PERM from '../config/permissions.js';
|
||||
// FILE: src/routes/lookup.js
|
||||
// Lookup route
|
||||
// - GET /api/lookup to fetch various lookup data (organizations, projects, categories, subcategories, volumes, permissions)
|
||||
// - Requires appropriate permissions for each data type via requirePerm middleware
|
||||
// - Supports query parameter 'pick' to specify which data to include (comma-separated, e.g. ?pick=org,project)
|
||||
// - If 'pick' is not provided, returns all data types
|
||||
// - Organizations and Projects are scoped based on user's permissions
|
||||
// - Categories, Subcategories, Volumes, and Permissions are global master data
|
||||
|
||||
import { Router } from "express";
|
||||
import sql from "../db/index.js";
|
||||
import { requirePerm } from "../middleware/requirePerm.js";
|
||||
import { buildScopeWhere } from "../utils/scope.js";
|
||||
import PERM from "../config/permissions.js";
|
||||
|
||||
const r = Router();
|
||||
|
||||
@@ -13,34 +21,47 @@ const r = Router();
|
||||
function parsePick(qs) {
|
||||
if (!qs) return null;
|
||||
return String(qs)
|
||||
.split(',')
|
||||
.map(s => s.trim().toLowerCase())
|
||||
.split(",")
|
||||
.map((s) => s.trim().toLowerCase())
|
||||
.filter(Boolean);
|
||||
}
|
||||
|
||||
// GET /api/lookup?pick=org,project,category,subcategory,volume,permission
|
||||
r.get('/',
|
||||
r.get(
|
||||
"/",
|
||||
// ต้องเป็นผู้ใช้ที่ยืนยันตัวตนแล้ว (middleware authJwt+loadPrincipal อยู่ใน /api)
|
||||
// ไม่ require permission เดียว เพราะเราจะเช็คทีละชุดด้านล่าง
|
||||
async (req, res) => {
|
||||
const pick = new Set(parsePick(req.query.pick) || [
|
||||
'org', 'project', 'category', 'subcategory', 'volume', 'permission'
|
||||
]);
|
||||
const pick = new Set(
|
||||
parsePick(req.query.pick) || [
|
||||
"org",
|
||||
"project",
|
||||
"category",
|
||||
"subcategory",
|
||||
"volume",
|
||||
"permission",
|
||||
]
|
||||
);
|
||||
|
||||
const result = {};
|
||||
|
||||
// 1) Organizations (scoped list) — require organization.read
|
||||
if (pick.has('org')) {
|
||||
if (pick.has("org")) {
|
||||
// มีสิทธิ์ถึงจะดึง
|
||||
const canOrg = req.principal.isSuperAdmin || req.principal.perms.has(PERM.organization.read);
|
||||
const canOrg =
|
||||
req.principal.isSuperAdmin ||
|
||||
req.principal.perms.has(PERM.organization.read);
|
||||
if (canOrg) {
|
||||
const { where, params } = buildScopeWhere(req.principal, {
|
||||
tableAlias: 'o',
|
||||
orgColumn: 'o.org_id',
|
||||
projectColumn: 'NULL',
|
||||
tableAlias: "o",
|
||||
orgColumn: "o.org_id",
|
||||
projectColumn: "NULL",
|
||||
permCode: PERM.organization.read,
|
||||
});
|
||||
const [rows] = await sql.query(`SELECT o.org_id, o.org_name FROM organizations o WHERE ${where} ORDER BY o.org_name`, params);
|
||||
const [rows] = await sql.query(
|
||||
`SELECT o.org_id, o.org_name FROM organizations o WHERE ${where} ORDER BY o.org_name`,
|
||||
params
|
||||
);
|
||||
result.organizations = rows;
|
||||
} else {
|
||||
result.organizations = [];
|
||||
@@ -48,13 +69,15 @@ r.get('/',
|
||||
}
|
||||
|
||||
// 2) Projects (scoped list) — require project.read
|
||||
if (pick.has('project')) {
|
||||
const canPrj = req.principal.isSuperAdmin || req.principal.perms.has(PERM.project.read);
|
||||
if (pick.has("project")) {
|
||||
const canPrj =
|
||||
req.principal.isSuperAdmin ||
|
||||
req.principal.perms.has(PERM.project.read);
|
||||
if (canPrj) {
|
||||
const { where, params } = buildScopeWhere(req.principal, {
|
||||
tableAlias: 'p',
|
||||
orgColumn: 'p.org_id',
|
||||
projectColumn: 'p.project_id',
|
||||
tableAlias: "p",
|
||||
orgColumn: "p.org_id",
|
||||
projectColumn: "p.project_id",
|
||||
permCode: PERM.project.read,
|
||||
preferProject: true,
|
||||
});
|
||||
@@ -70,10 +93,14 @@ r.get('/',
|
||||
}
|
||||
|
||||
// 3) Categories (global master) — require category.read
|
||||
if (pick.has('category')) {
|
||||
const can = req.principal.isSuperAdmin || req.principal.perms.has(PERM.category.read);
|
||||
if (pick.has("category")) {
|
||||
const can =
|
||||
req.principal.isSuperAdmin ||
|
||||
req.principal.perms.has(PERM.category.read);
|
||||
if (can) {
|
||||
const [rows] = await sql.query('SELECT cat_id, cat_code, cat_name FROM categories ORDER BY cat_name');
|
||||
const [rows] = await sql.query(
|
||||
"SELECT cat_id, cat_code, cat_name FROM categories ORDER BY cat_name"
|
||||
);
|
||||
result.categories = rows;
|
||||
} else {
|
||||
result.categories = [];
|
||||
@@ -81,10 +108,14 @@ r.get('/',
|
||||
}
|
||||
|
||||
// 4) Subcategories (global master) — require category.read
|
||||
if (pick.has('subcategory')) {
|
||||
const can = req.principal.isSuperAdmin || req.principal.perms.has(PERM.category.read);
|
||||
if (pick.has("subcategory")) {
|
||||
const can =
|
||||
req.principal.isSuperAdmin ||
|
||||
req.principal.perms.has(PERM.category.read);
|
||||
if (can) {
|
||||
const [rows] = await sql.query('SELECT sub_cat_id, cat_id, sub_cat_name FROM subcategories ORDER BY sub_cat_name');
|
||||
const [rows] = await sql.query(
|
||||
"SELECT sub_cat_id, cat_id, sub_cat_name FROM subcategories ORDER BY sub_cat_name"
|
||||
);
|
||||
result.subcategories = rows;
|
||||
} else {
|
||||
result.subcategories = [];
|
||||
@@ -92,10 +123,13 @@ r.get('/',
|
||||
}
|
||||
|
||||
// 5) Volumes (global master) — require volume.read
|
||||
if (pick.has('volume')) {
|
||||
const can = req.principal.isSuperAdmin || req.principal.perms.has(PERM.volume.read);
|
||||
if (pick.has("volume")) {
|
||||
const can =
|
||||
req.principal.isSuperAdmin || req.principal.perms.has(PERM.volume.read);
|
||||
if (can) {
|
||||
const [rows] = await sql.query('SELECT volume_id, volume_code, volume_name FROM volumes ORDER BY volume_code');
|
||||
const [rows] = await sql.query(
|
||||
"SELECT volume_id, volume_code, volume_name FROM volumes ORDER BY volume_code"
|
||||
);
|
||||
result.volumes = rows;
|
||||
} else {
|
||||
result.volumes = [];
|
||||
@@ -103,10 +137,14 @@ r.get('/',
|
||||
}
|
||||
|
||||
// 6) Permissions (global master) — require permission.read
|
||||
if (pick.has('permission')) {
|
||||
const can = req.principal.isSuperAdmin || req.principal.perms.has(PERM.permission.read);
|
||||
if (pick.has("permission")) {
|
||||
const can =
|
||||
req.principal.isSuperAdmin ||
|
||||
req.principal.perms.has(PERM.permission.read);
|
||||
if (can) {
|
||||
const [rows] = await sql.query('SELECT permission_id, permission_code, description FROM permissions ORDER BY permission_code');
|
||||
const [rows] = await sql.query(
|
||||
"SELECT permission_id, permission_code, description FROM permissions ORDER BY permission_code"
|
||||
);
|
||||
result.permissions = rows;
|
||||
} else {
|
||||
result.permissions = [];
|
||||
|
||||
@@ -1,15 +1,23 @@
|
||||
import { Router } from 'express';
|
||||
import { requireAuth } from '../middleware/auth.js';
|
||||
import { enrichPermissions } from '../middleware/permissions.js';
|
||||
import { requireRole } from '../middleware/rbac.js';
|
||||
import { requirePerm } from '../middleware/permGuard.js';
|
||||
import { sequelize } from '../db/sequelize.js';
|
||||
import RfaModel from '../db/models/RFA.js';
|
||||
import DrawingModel from '../db/models/Drawing.js';
|
||||
import RfaDrawMapModel from '../db/models/RfaDrawingMap.js';
|
||||
import CorrModel from '../db/models/Correspondence.js';
|
||||
import DocModel from '../db/models/Document.js';
|
||||
import CorrDocMapModel from '../db/models/CorrDocumentMap.js';
|
||||
// FILE: src/routes/maps.js
|
||||
// Maps routes
|
||||
// - Manage relationships between RFAs and Drawings, Correspondences and Documents
|
||||
// - Requires appropriate permissions via requirePerm middleware
|
||||
// - Uses project scope for RFA-Drawing maps and Correspondence-Document maps
|
||||
// - rfa:update for RFA-Drawing maps
|
||||
// - correspondence:update for Correspondence-Document maps
|
||||
|
||||
import { Router } from "express";
|
||||
import { requireAuth } from "../middleware/auth.js";
|
||||
import { enrichPermissions } from "../middleware/permissions.js";
|
||||
import { requireRole } from "../middleware/rbac.js";
|
||||
import { requirePerm } from "../middleware/permGuard.js";
|
||||
import { sequelize } from "../db/sequelize.js";
|
||||
import RfaModel from "../db/models/RFA.js";
|
||||
import DrawingModel from "../db/models/Drawing.js";
|
||||
import RfaDrawMapModel from "../db/models/RfaDrawingMap.js";
|
||||
import CorrModel from "../db/models/Correspondence.js";
|
||||
import DocModel from "../db/models/Document.js";
|
||||
import CorrDocMapModel from "../db/models/CorrDocumentMap.js";
|
||||
|
||||
const r = Router();
|
||||
const RFA = RfaModel(sequelize);
|
||||
@@ -22,62 +30,121 @@ const CorrDoc = CorrDocMapModel(sequelize);
|
||||
async function ensureRfaMembership(req, res) {
|
||||
const rfaId = Number(req.params.rfa_id);
|
||||
const row = await RFA.findByPk(rfaId);
|
||||
if (!row) { res.status(404).json({ error:'RFA not found' }); return false; }
|
||||
const roles = req.user?.roles || []; const isAdmin = roles.includes('Admin');
|
||||
if (!row) {
|
||||
res.status(404).json({ error: "RFA not found" });
|
||||
return false;
|
||||
}
|
||||
const roles = req.user?.roles || [];
|
||||
const isAdmin = roles.includes("Admin");
|
||||
if (isAdmin) return true;
|
||||
const { getUserProjectIds } = await import('../middleware/abac.js');
|
||||
const { getUserProjectIds } = await import("../middleware/abac.js");
|
||||
const memberProjects = await getUserProjectIds(req.user?.user_id);
|
||||
if (!memberProjects.includes(Number(row.project_id))) { res.status(403).json({ error:'Forbidden: not a project member' }); return false; }
|
||||
if (!memberProjects.includes(Number(row.project_id))) {
|
||||
res.status(403).json({ error: "Forbidden: not a project member" });
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
async function ensureCorrMembership(req, res) {
|
||||
const corrId = Number(req.params.corr_id);
|
||||
const row = await Corr.findByPk(corrId);
|
||||
if (!row) { res.status(404).json({ error:'Correspondence not found' }); return false; }
|
||||
const roles = req.user?.roles || []; const isAdmin = roles.includes('Admin');
|
||||
if (!row) {
|
||||
res.status(404).json({ error: "Correspondence not found" });
|
||||
return false;
|
||||
}
|
||||
const roles = req.user?.roles || [];
|
||||
const isAdmin = roles.includes("Admin");
|
||||
if (isAdmin) return true;
|
||||
const { getUserProjectIds } = await import('../middleware/abac.js');
|
||||
const { getUserProjectIds } = await import("../middleware/abac.js");
|
||||
const memberProjects = await getUserProjectIds(req.user?.user_id);
|
||||
if (!memberProjects.includes(Number(row.project_id))) { res.status(403).json({ error:'Forbidden: not a project member' }); return false; }
|
||||
if (!memberProjects.includes(Number(row.project_id))) {
|
||||
res.status(403).json({ error: "Forbidden: not a project member" });
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
|
||||
// RFA <-> Drawing
|
||||
r.get('/maps/rfa/:rfa_id/drawings', requireAuth, async (req, res) => {
|
||||
const rows = await RfaDraw.findAll({ where: { rfa_id: Number(req.params.rfa_id) } });
|
||||
r.get("/maps/rfa/:rfa_id/drawings", requireAuth, async (req, res) => {
|
||||
const rows = await RfaDraw.findAll({
|
||||
where: { rfa_id: Number(req.params.rfa_id) },
|
||||
});
|
||||
res.json(rows);
|
||||
});
|
||||
r.post('/maps/rfa/:rfa_id/drawings/:drawing_id', requireAuth, enrichPermissions(), requirePerm('rfa:update'), async (req, res) => {
|
||||
if (!(await ensureRfaMembership(req, res))) return;
|
||||
const { rfa_id, drawing_id } = { rfa_id: Number(req.params.rfa_id), drawing_id: Number(req.params.drawing_id) };
|
||||
await RfaDraw.create({ rfa_id, drawing_id });
|
||||
res.status(201).json({ ok: true });
|
||||
});
|
||||
r.delete('/maps/rfa/:rfa_id/drawings/:drawing_id', requireAuth, enrichPermissions(), requirePerm('rfa:update'), async (req, res) => {
|
||||
if (!(await ensureRfaMembership(req, res))) return;
|
||||
const { rfa_id, drawing_id } = { rfa_id: Number(req.params.rfa_id), drawing_id: Number(req.params.drawing_id) };
|
||||
const count = await RfaDraw.destroy({ where: { rfa_id, drawing_id } });
|
||||
res.json({ ok: count > 0 });
|
||||
});
|
||||
r.post(
|
||||
"/maps/rfa/:rfa_id/drawings/:drawing_id",
|
||||
requireAuth,
|
||||
enrichPermissions(),
|
||||
requirePerm("rfa:update"),
|
||||
async (req, res) => {
|
||||
if (!(await ensureRfaMembership(req, res))) return;
|
||||
const { rfa_id, drawing_id } = {
|
||||
rfa_id: Number(req.params.rfa_id),
|
||||
drawing_id: Number(req.params.drawing_id),
|
||||
};
|
||||
await RfaDraw.create({ rfa_id, drawing_id });
|
||||
res.status(201).json({ ok: true });
|
||||
}
|
||||
);
|
||||
r.delete(
|
||||
"/maps/rfa/:rfa_id/drawings/:drawing_id",
|
||||
requireAuth,
|
||||
enrichPermissions(),
|
||||
requirePerm("rfa:update"),
|
||||
async (req, res) => {
|
||||
if (!(await ensureRfaMembership(req, res))) return;
|
||||
const { rfa_id, drawing_id } = {
|
||||
rfa_id: Number(req.params.rfa_id),
|
||||
drawing_id: Number(req.params.drawing_id),
|
||||
};
|
||||
const count = await RfaDraw.destroy({ where: { rfa_id, drawing_id } });
|
||||
res.json({ ok: count > 0 });
|
||||
}
|
||||
);
|
||||
|
||||
// Correspondence <-> Document
|
||||
r.get('/maps/correspondence/:corr_id/documents', requireAuth, async (req, res) => {
|
||||
const rows = await CorrDoc.findAll({ where: { correspondence_id: Number(req.params.corr_id) } });
|
||||
res.json(rows);
|
||||
});
|
||||
r.post('/maps/correspondence/:corr_id/documents/:doc_id', requireAuth, enrichPermissions(), requirePerm('correspondence:update'), async (req, res) => {
|
||||
if (!(await ensureCorrMembership(req, res))) return;
|
||||
const { corr_id, doc_id } = { corr_id: Number(req.params.corr_id), doc_id: Number(req.params.doc_id) };
|
||||
await CorrDoc.create({ correspondence_id: corr_id, document_id: doc_id });
|
||||
res.status(201).json({ ok: true });
|
||||
});
|
||||
r.delete('/maps/correspondence/:corr_id/documents/:doc_id', requireAuth, enrichPermissions(), requirePerm('correspondence:update'), async (req, res) => {
|
||||
if (!(await ensureCorrMembership(req, res))) return;
|
||||
const { corr_id, doc_id } = { corr_id: Number(req.params.corr_id), doc_id: Number(req.params.doc_id) };
|
||||
const count = await CorrDoc.destroy({ where: { correspondence_id: corr_id, document_id: doc_id } });
|
||||
res.json({ ok: count > 0 });
|
||||
});
|
||||
r.get(
|
||||
"/maps/correspondence/:corr_id/documents",
|
||||
requireAuth,
|
||||
async (req, res) => {
|
||||
const rows = await CorrDoc.findAll({
|
||||
where: { correspondence_id: Number(req.params.corr_id) },
|
||||
});
|
||||
res.json(rows);
|
||||
}
|
||||
);
|
||||
r.post(
|
||||
"/maps/correspondence/:corr_id/documents/:doc_id",
|
||||
requireAuth,
|
||||
enrichPermissions(),
|
||||
requirePerm("correspondence:update"),
|
||||
async (req, res) => {
|
||||
if (!(await ensureCorrMembership(req, res))) return;
|
||||
const { corr_id, doc_id } = {
|
||||
corr_id: Number(req.params.corr_id),
|
||||
doc_id: Number(req.params.doc_id),
|
||||
};
|
||||
await CorrDoc.create({ correspondence_id: corr_id, document_id: doc_id });
|
||||
res.status(201).json({ ok: true });
|
||||
}
|
||||
);
|
||||
r.delete(
|
||||
"/maps/correspondence/:corr_id/documents/:doc_id",
|
||||
requireAuth,
|
||||
enrichPermissions(),
|
||||
requirePerm("correspondence:update"),
|
||||
async (req, res) => {
|
||||
if (!(await ensureCorrMembership(req, res))) return;
|
||||
const { corr_id, doc_id } = {
|
||||
corr_id: Number(req.params.corr_id),
|
||||
doc_id: Number(req.params.doc_id),
|
||||
};
|
||||
const count = await CorrDoc.destroy({
|
||||
where: { correspondence_id: corr_id, document_id: doc_id },
|
||||
});
|
||||
res.json({ ok: count > 0 });
|
||||
}
|
||||
);
|
||||
|
||||
export default r;
|
||||
|
||||
@@ -1,16 +1,32 @@
|
||||
import { Router } from 'express';
|
||||
import { requireAuth } from '../middleware/auth.js';
|
||||
import { sequelize } from '../db/sequelize.js';
|
||||
import FileModel from '../db/models/FileObject.js';
|
||||
// FILE: src/routes/module_files.js
|
||||
// Module files routes
|
||||
// - GET /:module(s)/:id/files to list files for various modules (rfa, correspondence, drawing, document, transmittal)
|
||||
// - Requires authentication via requireAuth middleware
|
||||
// - Uses project scope for file access permissions
|
||||
// - file:read permission required
|
||||
|
||||
import { Router } from "express";
|
||||
import { requireAuth } from "../middleware/auth.js";
|
||||
import { sequelize } from "../db/sequelize.js";
|
||||
import FileModel from "../db/models/FileObject.js";
|
||||
|
||||
const r = Router();
|
||||
const Files = FileModel(sequelize);
|
||||
|
||||
async function listBy(mod, ref_id) {
|
||||
return Files.findAll({ where: { module: mod, ref_id }, order:[['created_at','DESC']] });
|
||||
return Files.findAll({
|
||||
where: { module: mod, ref_id },
|
||||
order: [["created_at", "DESC"]],
|
||||
});
|
||||
}
|
||||
|
||||
for (const mod of ['rfa','correspondence','drawing','document','transmittal']) {
|
||||
for (const mod of [
|
||||
"rfa",
|
||||
"correspondence",
|
||||
"drawing",
|
||||
"document",
|
||||
"transmittal",
|
||||
]) {
|
||||
r.get(`/${mod}s/:id/files`, requireAuth, async (req, res) => {
|
||||
const items = await listBy(mod, Number(req.params.id));
|
||||
res.json(items);
|
||||
|
||||
@@ -1,36 +1,71 @@
|
||||
// src/routes/map.js
|
||||
import { Router } from 'express';
|
||||
import sql from '../db/index.js';
|
||||
import { requirePerm } from '../middleware/requirePerm.js';
|
||||
import { buildScopeWhere, ownerResolvers } from '../utils/scope.js';
|
||||
import PERM from '../config/permissions.js';
|
||||
// FILE: src/routes/maps.js
|
||||
// Maps routes
|
||||
// - Manage relationships between RFAs and Drawings, Correspondences and Documents
|
||||
// - Requires appropriate permissions via requirePerm middleware
|
||||
// - Uses project scope for RFA-Drawing maps and Correspondence-Document maps
|
||||
// - rfa:update for RFA-Drawing maps
|
||||
// - correspondence:update for Correspondence-Document maps
|
||||
|
||||
import { Router } from "express";
|
||||
import sql from "../db/index.js";
|
||||
import { requirePerm } from "../middleware/requirePerm.js";
|
||||
import { buildScopeWhere, ownerResolvers } from "../utils/scope.js";
|
||||
import PERM from "../config/permissions.js";
|
||||
|
||||
const r = Router();
|
||||
const OWN = ownerResolvers(sql, 'entity_maps', 'id');
|
||||
const OWN = ownerResolvers(sql, "entity_maps", "id");
|
||||
|
||||
// LIST
|
||||
r.get('/',
|
||||
requirePerm(PERM.map.read, { scope: 'global' }),
|
||||
r.get(
|
||||
"/",
|
||||
requirePerm(PERM.map.read, { scope: "global" }),
|
||||
async (req, res) => {
|
||||
const { project_id, org_id, module, src_type, dst_type, limit = 100, offset = 0 } = req.query;
|
||||
const {
|
||||
project_id,
|
||||
org_id,
|
||||
module,
|
||||
src_type,
|
||||
dst_type,
|
||||
limit = 100,
|
||||
offset = 0,
|
||||
} = req.query;
|
||||
|
||||
const base = buildScopeWhere(req.principal, {
|
||||
tableAlias: 'm',
|
||||
orgColumn: 'm.org_id',
|
||||
projectColumn: 'm.project_id',
|
||||
tableAlias: "m",
|
||||
orgColumn: "m.org_id",
|
||||
projectColumn: "m.project_id",
|
||||
permCode: PERM.map.read,
|
||||
preferProject: true,
|
||||
});
|
||||
|
||||
const extra = [];
|
||||
const params = { ...base.params, limit: Number(limit), offset: Number(offset) };
|
||||
if (project_id) { extra.push('m.project_id = :project_id'); params.project_id = Number(project_id); }
|
||||
if (org_id) { extra.push('m.org_id = :org_id'); params.org_id = Number(org_id); }
|
||||
if (module) { extra.push('m.module = :module'); params.module = module; }
|
||||
if (src_type) { extra.push('m.src_type = :src_type'); params.src_type = src_type; }
|
||||
if (dst_type) { extra.push('m.dst_type = :dst_type'); params.dst_type = dst_type; }
|
||||
const params = {
|
||||
...base.params,
|
||||
limit: Number(limit),
|
||||
offset: Number(offset),
|
||||
};
|
||||
if (project_id) {
|
||||
extra.push("m.project_id = :project_id");
|
||||
params.project_id = Number(project_id);
|
||||
}
|
||||
if (org_id) {
|
||||
extra.push("m.org_id = :org_id");
|
||||
params.org_id = Number(org_id);
|
||||
}
|
||||
if (module) {
|
||||
extra.push("m.module = :module");
|
||||
params.module = module;
|
||||
}
|
||||
if (src_type) {
|
||||
extra.push("m.src_type = :src_type");
|
||||
params.src_type = src_type;
|
||||
}
|
||||
if (dst_type) {
|
||||
extra.push("m.dst_type = :dst_type");
|
||||
params.dst_type = dst_type;
|
||||
}
|
||||
|
||||
const where = [base.where, ...extra].filter(Boolean).join(' AND ');
|
||||
const where = [base.where, ...extra].filter(Boolean).join(" AND ");
|
||||
const [rows] = await sql.query(
|
||||
`SELECT m.* FROM entity_maps m
|
||||
WHERE ${where}
|
||||
@@ -42,25 +77,49 @@ r.get('/',
|
||||
);
|
||||
|
||||
// CREATE
|
||||
r.post('/',
|
||||
requirePerm(PERM.map.create, { scope: 'org', getOrgId: async req => req.body?.org_id ?? null }),
|
||||
r.post(
|
||||
"/",
|
||||
requirePerm(PERM.map.create, {
|
||||
scope: "org",
|
||||
getOrgId: async (req) => req.body?.org_id ?? null,
|
||||
}),
|
||||
async (req, res) => {
|
||||
const { org_id, project_id, module, src_type, src_id, dst_type, dst_id, remark } = req.body;
|
||||
const {
|
||||
org_id,
|
||||
project_id,
|
||||
module,
|
||||
src_type,
|
||||
src_id,
|
||||
dst_type,
|
||||
dst_id,
|
||||
remark,
|
||||
} = req.body;
|
||||
const [rs] = await sql.query(
|
||||
`INSERT INTO entity_maps (org_id, project_id, module, src_type, src_id, dst_type, dst_id, remark, created_by)
|
||||
VALUES (?,?,?,?,?,?,?,?,?)`,
|
||||
[org_id, project_id, module, src_type, Number(src_id), dst_type, Number(dst_id), remark ?? null, req.principal.userId]
|
||||
[
|
||||
org_id,
|
||||
project_id,
|
||||
module,
|
||||
src_type,
|
||||
Number(src_id),
|
||||
dst_type,
|
||||
Number(dst_id),
|
||||
remark ?? null,
|
||||
req.principal.userId,
|
||||
]
|
||||
);
|
||||
res.json({ id: rs.insertId });
|
||||
}
|
||||
);
|
||||
|
||||
// DELETE (by id)
|
||||
r.delete('/:id',
|
||||
requirePerm(PERM.map.delete, { scope: 'org', getOrgId: OWN.getOrgIdById }),
|
||||
r.delete(
|
||||
"/:id",
|
||||
requirePerm(PERM.map.delete, { scope: "org", getOrgId: OWN.getOrgIdById }),
|
||||
async (req, res) => {
|
||||
const id = Number(req.params.id);
|
||||
await sql.query('DELETE FROM entity_maps WHERE id=?', [id]);
|
||||
await sql.query("DELETE FROM entity_maps WHERE id=?", [id]);
|
||||
res.json({ ok: 1 });
|
||||
}
|
||||
);
|
||||
|
||||
@@ -1,28 +1,34 @@
|
||||
import { Router } from 'express';
|
||||
import { sequelize } from '../db/sequelize.js';
|
||||
import fs from 'fs';
|
||||
import path from 'path';
|
||||
// FILE: src/routes/ops.js
|
||||
// Ops routes
|
||||
// - GET /ready for readiness check (DB connection)
|
||||
// - GET /live for liveness check
|
||||
// - GET /version to get app version from package.json
|
||||
|
||||
import { Router } from "express";
|
||||
import { sequelize } from "../db/sequelize.js";
|
||||
import fs from "fs";
|
||||
import path from "path";
|
||||
|
||||
const r = Router();
|
||||
|
||||
r.get('/ready', async (_req, res) => {
|
||||
r.get("/ready", async (_req, res) => {
|
||||
try {
|
||||
await sequelize.query('SELECT 1');
|
||||
await sequelize.query("SELECT 1");
|
||||
return res.json({ ready: true });
|
||||
} catch {
|
||||
return res.status(500).json({ ready: false });
|
||||
}
|
||||
});
|
||||
|
||||
r.get('/live', (_req, res) => res.json({ live: true }));
|
||||
r.get("/live", (_req, res) => res.json({ live: true }));
|
||||
|
||||
r.get('/version', (_req, res) => {
|
||||
r.get("/version", (_req, res) => {
|
||||
try {
|
||||
const pkgPath = path.resolve(process.cwd(), 'package.json');
|
||||
const pkg = JSON.parse(fs.readFileSync(pkgPath, 'utf-8'));
|
||||
const pkgPath = path.resolve(process.cwd(), "package.json");
|
||||
const pkg = JSON.parse(fs.readFileSync(pkgPath, "utf-8"));
|
||||
res.json({ name: pkg.name, version: pkg.version });
|
||||
} catch {
|
||||
res.json({ name: 'dms-backend', version: 'unknown' });
|
||||
res.json({ name: "dms-backend", version: "unknown" });
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
@@ -1,18 +1,32 @@
|
||||
import { Router } from 'express';
|
||||
import sql from '../db/index.js';
|
||||
import { requirePerm } from '../middleware/requirePerm.js';
|
||||
import { buildScopeWhere } from '../utils/scope.js';
|
||||
import PERM from '../config/permissions.js';
|
||||
// FILE: src/routes/organizations.js
|
||||
// 03.2 5) เพิ่ม routes/organizations.js (ใหม่)
|
||||
// - ใช้ร่วมกับ requirePerm() และ buildScopeWhere()
|
||||
// - สำหรับจัดการ organizations (ดู/เพิ่ม/แก้ไข/ลบ) ตามสิทธิ์ของผู้ใช้
|
||||
// Organizations routes
|
||||
// - CRUD operations for organizations
|
||||
// - Requires appropriate permissions via requirePerm middleware
|
||||
// - Uses org scope for all permissions
|
||||
// - organization:read, organization:create, organization:update, organization:delete
|
||||
// - Organization fields: org_id (PK), org_name
|
||||
// - Basic validation: org_name required for create
|
||||
|
||||
import { Router } from "express";
|
||||
import sql from "../db/index.js";
|
||||
import { requirePerm } from "../middleware/requirePerm.js";
|
||||
import { buildScopeWhere } from "../utils/scope.js";
|
||||
|
||||
const r = Router();
|
||||
|
||||
// LIST (org) – ดูเฉพาะ org ใน scope
|
||||
r.get('/',
|
||||
requirePerm('organization.read', { scope: 'global' }),
|
||||
r.get(
|
||||
"/",
|
||||
requirePerm("organization.read", { scope: "global" }),
|
||||
async (req, res) => {
|
||||
const { where, params } = buildScopeWhere(req.principal, {
|
||||
tableAlias: 'o', orgColumn: 'o.org_id', projectColumn: 'NULL',
|
||||
permCode: 'organization.read',
|
||||
tableAlias: "o",
|
||||
orgColumn: "o.org_id",
|
||||
projectColumn: "NULL",
|
||||
permCode: "organization.read",
|
||||
});
|
||||
|
||||
const [rows] = await sql.query(
|
||||
@@ -24,15 +38,19 @@ r.get('/',
|
||||
);
|
||||
|
||||
// GET by id
|
||||
r.get('/:id',
|
||||
requirePerm('organization.read', {
|
||||
scope: 'org',
|
||||
getOrgId: async req => Number(req.params.id),
|
||||
r.get(
|
||||
"/:id",
|
||||
requirePerm("organization.read", {
|
||||
scope: "org",
|
||||
getOrgId: async (req) => Number(req.params.id),
|
||||
}),
|
||||
async (req, res) => {
|
||||
const id = Number(req.params.id);
|
||||
const [[row]] = await sql.query('SELECT * FROM organizations WHERE org_id=?', [id]);
|
||||
if (!row) return res.status(404).json({ error: 'Not found' });
|
||||
const [[row]] = await sql.query(
|
||||
"SELECT * FROM organizations WHERE org_id=?",
|
||||
[id]
|
||||
);
|
||||
if (!row) return res.status(404).json({ error: "Not found" });
|
||||
res.json(row);
|
||||
}
|
||||
);
|
||||
|
||||
@@ -1,14 +1,25 @@
|
||||
import { Router } from 'express';
|
||||
import sql from '../db/index.js';
|
||||
import { requirePerm } from '../middleware/requirePerm.js';
|
||||
import PERM from '../config/permissions.js';
|
||||
// FILE: src/routes/permissions.js
|
||||
// 03.2 12) เพิ่ม routes/permissions.js (ใหม่)
|
||||
// - ใช้ร่วมกับ requirePerm()
|
||||
// - สำหรับดูรายชื่อสิทธิ์ทั้งหมด
|
||||
// Permissions route
|
||||
// - GET /api/permissions to list all permissions (permission_id, permission_code, description)
|
||||
// - Requires global permission.read permission via requirePerm middleware
|
||||
|
||||
import { Router } from "express";
|
||||
import sql from "../db/index.js";
|
||||
import { requirePerm } from "../middleware/requirePerm.js";
|
||||
import PERM from "../config/permissions.js";
|
||||
|
||||
const r = Router();
|
||||
|
||||
r.get('/',
|
||||
requirePerm('permission.read', { scope: 'global' }),
|
||||
r.get(
|
||||
"/",
|
||||
requirePerm("permission.read", { scope: "global" }),
|
||||
async (req, res) => {
|
||||
const [rows] = await sql.query('SELECT permission_id, permission_code, description FROM permissions ORDER BY permission_code');
|
||||
const [rows] = await sql.query(
|
||||
"SELECT permission_id, permission_code, description FROM permissions ORDER BY permission_code"
|
||||
);
|
||||
res.json(rows);
|
||||
}
|
||||
);
|
||||
|
||||
@@ -1,18 +1,34 @@
|
||||
import { Router } from 'express';
|
||||
import sql from '../db/index.js';
|
||||
import { requirePerm } from '../middleware/requirePerm.js';
|
||||
import { buildScopeWhere } from '../utils/scope.js';
|
||||
import PERM from '../config/permissions.js';
|
||||
// FILE: src/routes/projects.js
|
||||
// 03.2 6) เพิ่ม routes/projects.js (ใหม่)
|
||||
// - ใช้ร่วมกับ requirePerm() และ buildScopeWhere()
|
||||
// - สำหรับจัดการ projects (ดู/เพิ่ม/แก้ไข/ลบ) ตามสิทธิ์ของผู้ใช้
|
||||
// Projects routes
|
||||
// - CRUD operations for projects
|
||||
// - Requires appropriate permissions via requirePerm middleware
|
||||
// - Uses org/project scope for all permissions
|
||||
// - project:read, project:create, project:update, project:delete
|
||||
// - Project fields: project_id (PK), org_id (FK), project_code, project_name
|
||||
// - project_code is unique
|
||||
// - Basic validation: org_id, project_code, project_name required for create
|
||||
|
||||
import { Router } from "express";
|
||||
import sql from "../db/index.js";
|
||||
import { requirePerm } from "../middleware/requirePerm.js";
|
||||
import { buildScopeWhere } from "../utils/scope.js";
|
||||
|
||||
const r = Router();
|
||||
|
||||
// LIST – จำกัดตาม org/project scope ของผู้ใช้
|
||||
r.get('/',
|
||||
requirePerm('project.read', { scope: 'global' }),
|
||||
r.get(
|
||||
"/",
|
||||
requirePerm("project.read", { scope: "global" }),
|
||||
async (req, res) => {
|
||||
const { where, params } = buildScopeWhere(req.principal, {
|
||||
tableAlias: 'p', orgColumn: 'p.org_id', projectColumn: 'p.project_id',
|
||||
permCode: 'project.read', preferProject: true,
|
||||
tableAlias: "p",
|
||||
orgColumn: "p.org_id",
|
||||
projectColumn: "p.project_id",
|
||||
permCode: "project.read",
|
||||
preferProject: true,
|
||||
});
|
||||
const [rows] = await sql.query(
|
||||
`SELECT p.* FROM projects p WHERE ${where}`,
|
||||
@@ -23,29 +39,34 @@ r.get('/',
|
||||
);
|
||||
|
||||
// GET
|
||||
r.get('/:id',
|
||||
requirePerm('project.read', {
|
||||
scope: 'project',
|
||||
getProjectId: async req => Number(req.params.id),
|
||||
r.get(
|
||||
"/:id",
|
||||
requirePerm("project.read", {
|
||||
scope: "project",
|
||||
getProjectId: async (req) => Number(req.params.id),
|
||||
}),
|
||||
async (req, res) => {
|
||||
const id = Number(req.params.id);
|
||||
const [[row]] = await sql.query('SELECT * FROM projects WHERE project_id=?', [id]);
|
||||
if (!row) return res.status(404).json({ error: 'Not found' });
|
||||
const [[row]] = await sql.query(
|
||||
"SELECT * FROM projects WHERE project_id=?",
|
||||
[id]
|
||||
);
|
||||
if (!row) return res.status(404).json({ error: "Not found" });
|
||||
res.json(row);
|
||||
}
|
||||
);
|
||||
|
||||
// CREATE
|
||||
r.post('/',
|
||||
requirePerm('project.create', {
|
||||
scope: 'org',
|
||||
getOrgId: async req => req.body?.org_id ?? null,
|
||||
r.post(
|
||||
"/",
|
||||
requirePerm("project.create", {
|
||||
scope: "org",
|
||||
getOrgId: async (req) => req.body?.org_id ?? null,
|
||||
}),
|
||||
async (req, res) => {
|
||||
const { org_id, project_code, project_name } = req.body;
|
||||
const [rs] = await sql.query(
|
||||
'INSERT INTO projects (org_id, project_code, project_name) VALUES (?,?,?)',
|
||||
"INSERT INTO projects (org_id, project_code, project_name) VALUES (?,?,?)",
|
||||
[org_id, project_code, project_name]
|
||||
);
|
||||
res.json({ project_id: rs.insertId });
|
||||
@@ -53,28 +74,33 @@ r.post('/',
|
||||
);
|
||||
|
||||
// UPDATE
|
||||
r.put('/:id',
|
||||
requirePerm('project.update', {
|
||||
scope: 'project',
|
||||
getProjectId: async req => Number(req.params.id),
|
||||
r.put(
|
||||
"/:id",
|
||||
requirePerm("project.update", {
|
||||
scope: "project",
|
||||
getProjectId: async (req) => Number(req.params.id),
|
||||
}),
|
||||
async (req, res) => {
|
||||
const { project_name } = req.body;
|
||||
const id = Number(req.params.id);
|
||||
await sql.query('UPDATE projects SET project_name=? WHERE project_id=?', [project_name, id]);
|
||||
await sql.query("UPDATE projects SET project_name=? WHERE project_id=?", [
|
||||
project_name,
|
||||
id,
|
||||
]);
|
||||
res.json({ ok: 1 });
|
||||
}
|
||||
);
|
||||
|
||||
// DELETE
|
||||
r.delete('/:id',
|
||||
requirePerm('project.delete', {
|
||||
scope: 'project',
|
||||
getProjectId: async req => Number(req.params.id),
|
||||
r.delete(
|
||||
"/:id",
|
||||
requirePerm("project.delete", {
|
||||
scope: "project",
|
||||
getProjectId: async (req) => Number(req.params.id),
|
||||
}),
|
||||
async (req, res) => {
|
||||
const id = Number(req.params.id);
|
||||
await sql.query('DELETE FROM projects WHERE project_id=?', [id]);
|
||||
await sql.query("DELETE FROM projects WHERE project_id=?", [id]);
|
||||
res.json({ ok: 1 });
|
||||
}
|
||||
);
|
||||
|
||||
@@ -1,101 +1,136 @@
|
||||
// src/routes/rbac_admin.js (ESM)
|
||||
import { Router } from 'express';
|
||||
import sql from '../db/index.js';
|
||||
import { requirePerm } from '../middleware/requirePerm.js';
|
||||
import PERM from '../config/permissions.js';
|
||||
// FILE: src/routes/rbac_admin.js
|
||||
// RBAC Admin routes
|
||||
// - Manage roles, permissions, user-role assignments
|
||||
// - Requires appropriate permissions via requirePerm middleware
|
||||
// - Uses global scope for all permissions
|
||||
// - rbac_admin.read, rbac_admin.assign_role, rbac_admin.grant_perm
|
||||
|
||||
import { Router } from "express";
|
||||
import sql from "../db/index.js";
|
||||
import { requirePerm } from "../middleware/requirePerm.js";
|
||||
import PERM from "../config/permissions.js";
|
||||
|
||||
const r = Router();
|
||||
|
||||
/** LIST: roles */
|
||||
r.get('/roles',
|
||||
requirePerm(PERM.rbac_admin.read, { scope: 'global' }),
|
||||
r.get(
|
||||
"/roles",
|
||||
requirePerm(PERM.rbac_admin.read, { scope: "global" }),
|
||||
async (req, res) => {
|
||||
const [rows] = await sql.query('SELECT role_id, role_code, role_name, description FROM roles ORDER BY role_code');
|
||||
const [rows] = await sql.query(
|
||||
"SELECT role_id, role_code, role_name, description FROM roles ORDER BY role_code"
|
||||
);
|
||||
res.json(rows);
|
||||
}
|
||||
);
|
||||
|
||||
/** LIST: permissions */
|
||||
r.get('/permissions',
|
||||
requirePerm(PERM.rbac_admin.read, { scope: 'global' }),
|
||||
r.get(
|
||||
"/permissions",
|
||||
requirePerm(PERM.rbac_admin.read, { scope: "global" }),
|
||||
async (req, res) => {
|
||||
const [rows] = await sql.query('SELECT permission_id, permission_code, description FROM permissions ORDER BY permission_code');
|
||||
const [rows] = await sql.query(
|
||||
"SELECT permission_id, permission_code, description FROM permissions ORDER BY permission_code"
|
||||
);
|
||||
res.json(rows);
|
||||
}
|
||||
);
|
||||
|
||||
/** LIST: role→permissions */
|
||||
r.get('/roles/:role_id/permissions',
|
||||
requirePerm(PERM.rbac_admin.read, { scope: 'global' }),
|
||||
r.get(
|
||||
"/roles/:role_id/permissions",
|
||||
requirePerm(PERM.rbac_admin.read, { scope: "global" }),
|
||||
async (req, res) => {
|
||||
const role_id = Number(req.params.role_id);
|
||||
const [rows] = await sql.query(
|
||||
`SELECT p.permission_id, p.permission_code, p.description
|
||||
FROM role_permissions rp
|
||||
JOIN permissions p ON p.permission_id = rp.permission_id
|
||||
WHERE rp.role_id=? ORDER BY p.permission_code`, [role_id]);
|
||||
WHERE rp.role_id=? ORDER BY p.permission_code`,
|
||||
[role_id]
|
||||
);
|
||||
res.json(rows);
|
||||
}
|
||||
);
|
||||
|
||||
/** MAP: role↔permission (grant/revoke) */
|
||||
r.post('/roles/:role_id/permissions',
|
||||
requirePerm(PERM.rbac_admin.grant_perm, { scope: 'global' }),
|
||||
r.post(
|
||||
"/roles/:role_id/permissions",
|
||||
requirePerm(PERM.rbac_admin.grant_perm, { scope: "global" }),
|
||||
async (req, res) => {
|
||||
const role_id = Number(req.params.role_id);
|
||||
const { permission_id } = req.body || {};
|
||||
await sql.query('INSERT IGNORE INTO role_permissions (role_id, permission_id) VALUES (?,?)',
|
||||
[role_id, Number(permission_id)]);
|
||||
res.json({ ok: 1 });
|
||||
}
|
||||
);
|
||||
|
||||
r.delete('/roles/:role_id/permissions/:permission_id',
|
||||
requirePerm(PERM.rbac_admin.grant_perm, { scope: 'global' }),
|
||||
async (req, res) => {
|
||||
const role_id = Number(req.params.role_id);
|
||||
const permission_id = Number(req.params.permission_id);
|
||||
await sql.query('DELETE FROM role_permissions WHERE role_id=? AND permission_id=?', [role_id, permission_id]);
|
||||
res.json({ ok: 1 });
|
||||
}
|
||||
);
|
||||
|
||||
/** LIST: user→roles(+scope) */
|
||||
r.get('/users/:user_id/roles',
|
||||
requirePerm(PERM.rbac_admin.read, { scope: 'global' }),
|
||||
async (req, res) => {
|
||||
const user_id = Number(req.params.user_id);
|
||||
const [rows] = await sql.query(
|
||||
`SELECT ur.user_id, ur.role_id, r.role_code, r.role_name, ur.org_id, ur.project_id
|
||||
FROM user_roles ur JOIN roles r ON r.role_id = ur.role_id
|
||||
WHERE ur.user_id=? ORDER BY r.role_code`, [user_id]);
|
||||
res.json(rows);
|
||||
}
|
||||
);
|
||||
|
||||
/** MAP: user↔role(+scope) (assign / revoke) */
|
||||
r.post('/users/:user_id/roles',
|
||||
requirePerm(PERM.rbac_admin.assign_role, { scope: 'global' }),
|
||||
async (req, res) => {
|
||||
const user_id = Number(req.params.user_id);
|
||||
const { role_id, org_id = null, project_id = null } = req.body || {};
|
||||
await sql.query(
|
||||
'INSERT INTO user_roles (user_id, role_id, org_id, project_id) VALUES (?,?,?,?)',
|
||||
[user_id, Number(role_id), org_id ? Number(org_id) : null, project_id ? Number(project_id) : null]
|
||||
"INSERT IGNORE INTO role_permissions (role_id, permission_id) VALUES (?,?)",
|
||||
[role_id, Number(permission_id)]
|
||||
);
|
||||
res.json({ ok: 1 });
|
||||
}
|
||||
);
|
||||
|
||||
r.delete('/users/:user_id/roles',
|
||||
requirePerm(PERM.rbac_admin.assign_role, { scope: 'global' }),
|
||||
r.delete(
|
||||
"/roles/:role_id/permissions/:permission_id",
|
||||
requirePerm(PERM.rbac_admin.grant_perm, { scope: "global" }),
|
||||
async (req, res) => {
|
||||
const role_id = Number(req.params.role_id);
|
||||
const permission_id = Number(req.params.permission_id);
|
||||
await sql.query(
|
||||
"DELETE FROM role_permissions WHERE role_id=? AND permission_id=?",
|
||||
[role_id, permission_id]
|
||||
);
|
||||
res.json({ ok: 1 });
|
||||
}
|
||||
);
|
||||
|
||||
/** LIST: user→roles(+scope) */
|
||||
r.get(
|
||||
"/users/:user_id/roles",
|
||||
requirePerm(PERM.rbac_admin.read, { scope: "global" }),
|
||||
async (req, res) => {
|
||||
const user_id = Number(req.params.user_id);
|
||||
const [rows] = await sql.query(
|
||||
`SELECT ur.user_id, ur.role_id, r.role_code, r.role_name, ur.org_id, ur.project_id
|
||||
FROM user_roles ur JOIN roles r ON r.role_id = ur.role_id
|
||||
WHERE ur.user_id=? ORDER BY r.role_code`,
|
||||
[user_id]
|
||||
);
|
||||
res.json(rows);
|
||||
}
|
||||
);
|
||||
|
||||
/** MAP: user↔role(+scope) (assign / revoke) */
|
||||
r.post(
|
||||
"/users/:user_id/roles",
|
||||
requirePerm(PERM.rbac_admin.assign_role, { scope: "global" }),
|
||||
async (req, res) => {
|
||||
const user_id = Number(req.params.user_id);
|
||||
const { role_id, org_id = null, project_id = null } = req.body || {};
|
||||
await sql.query(
|
||||
'DELETE FROM user_roles WHERE user_id=? AND role_id=? AND <=> org_id ? AND <=> project_id ?'
|
||||
.replace('<=> org_id ?', (org_id === null ? 'org_id IS ?' : 'org_id=?'))
|
||||
.replace('<=> project_id ?', (project_id === null ? 'project_id IS ?' : 'project_id=?')),
|
||||
"INSERT INTO user_roles (user_id, role_id, org_id, project_id) VALUES (?,?,?,?)",
|
||||
[
|
||||
user_id,
|
||||
Number(role_id),
|
||||
org_id ? Number(org_id) : null,
|
||||
project_id ? Number(project_id) : null,
|
||||
]
|
||||
);
|
||||
res.json({ ok: 1 });
|
||||
}
|
||||
);
|
||||
|
||||
r.delete(
|
||||
"/users/:user_id/roles",
|
||||
requirePerm(PERM.rbac_admin.assign_role, { scope: "global" }),
|
||||
async (req, res) => {
|
||||
const user_id = Number(req.params.user_id);
|
||||
const { role_id, org_id = null, project_id = null } = req.body || {};
|
||||
await sql.query(
|
||||
"DELETE FROM user_roles WHERE user_id=? AND role_id=? AND <=> org_id ? AND <=> project_id ?"
|
||||
.replace("<=> org_id ?", org_id === null ? "org_id IS ?" : "org_id=?")
|
||||
.replace(
|
||||
"<=> project_id ?",
|
||||
project_id === null ? "project_id IS ?" : "project_id=?"
|
||||
),
|
||||
[user_id, Number(role_id), org_id, project_id]
|
||||
);
|
||||
res.json({ ok: 1 });
|
||||
|
||||
@@ -1,34 +1,74 @@
|
||||
import { Router } from 'express';
|
||||
import { requireAuth } from '../middleware/auth.js';
|
||||
import { requirePermission } from '../middleware/perm.js';
|
||||
import { callProc } from '../db/index.js';
|
||||
// FILE: src/routes/rfa.js
|
||||
// RFA routes
|
||||
// - POST /create to create a new RFA with optional associated item documents
|
||||
// - POST /update-status to update the status of an existing RFA
|
||||
// - Requires authentication and appropriate permissions via requireAuth and requirePermission middleware
|
||||
// - Uses project scope for permissions
|
||||
// - RFA_CREATE permission required for creating RFAs
|
||||
// - RFA_STATUS_UPDATE permission required for updating RFA status
|
||||
|
||||
import { Router } from "express";
|
||||
import { requireAuth } from "../middleware/auth.js";
|
||||
import { requirePermission } from "../middleware/perm.js";
|
||||
import { callProc } from "../db/index.js";
|
||||
|
||||
const router = Router();
|
||||
|
||||
router.post('/create',
|
||||
router.post(
|
||||
"/create",
|
||||
requireAuth,
|
||||
requirePermission(['RFA_CREATE'], { projectRequired: true }),
|
||||
requirePermission(["RFA_CREATE"], { projectRequired: true }),
|
||||
async (req, res, next) => {
|
||||
try {
|
||||
const { project_id, cor_status_id, cor_no, title, originator_id, recipient_id, keywords = null, pdf_path = null, item_doc_ids = [] } = req.body || {};
|
||||
const {
|
||||
project_id,
|
||||
cor_status_id,
|
||||
cor_no,
|
||||
title,
|
||||
originator_id,
|
||||
recipient_id,
|
||||
keywords = null,
|
||||
pdf_path = null,
|
||||
item_doc_ids = [],
|
||||
} = req.body || {};
|
||||
const json = JSON.stringify(item_doc_ids.map(Number));
|
||||
await callProc('sp_rfa_create_with_items', [
|
||||
req.user.user_id, project_id, cor_status_id, cor_no, title, originator_id, recipient_id, keywords, pdf_path, json, null
|
||||
await callProc("sp_rfa_create_with_items", [
|
||||
req.user.user_id,
|
||||
project_id,
|
||||
cor_status_id,
|
||||
cor_no,
|
||||
title,
|
||||
originator_id,
|
||||
recipient_id,
|
||||
keywords,
|
||||
pdf_path,
|
||||
json,
|
||||
null,
|
||||
]);
|
||||
res.status(201).json({ ok: true });
|
||||
} catch (e) { next(e); }
|
||||
} catch (e) {
|
||||
next(e);
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
router.post('/update-status',
|
||||
router.post(
|
||||
"/update-status",
|
||||
requireAuth,
|
||||
requirePermission(['RFA_STATUS_UPDATE'], { projectRequired: true }),
|
||||
requirePermission(["RFA_STATUS_UPDATE"], { projectRequired: true }),
|
||||
async (req, res, next) => {
|
||||
try {
|
||||
const { rfa_corr_id, status_id, set_issue = 0 } = req.body || {};
|
||||
await callProc('sp_rfa_update_status', [req.user.user_id, rfa_corr_id, status_id, set_issue ? 1 : 0]);
|
||||
await callProc("sp_rfa_update_status", [
|
||||
req.user.user_id,
|
||||
rfa_corr_id,
|
||||
status_id,
|
||||
set_issue ? 1 : 0,
|
||||
]);
|
||||
res.json({ ok: true });
|
||||
} catch (e) { next(e); }
|
||||
} catch (e) {
|
||||
next(e);
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
|
||||
@@ -1,28 +1,43 @@
|
||||
// backend/src/routes/rfas.js (merged)
|
||||
// FILE: src/routes/rfas.js
|
||||
// 03.2 8) แก้ไข routes/rfas.js (ใหม่)
|
||||
// - ผสมผสานระหว่าง rfas.js เดิม + ฟีเจอร์ list/sort/paging/overdue จาก rfas-1.js
|
||||
// - ใช้ร่วมกับ requirePerm() และ buildScopeWhere()
|
||||
// - สำหรับจัดการ RFAs (ดู/เพิ่ม/แก้ไข/ลบ) ตามสิทธิ์ของผู้ใช้
|
||||
// RFAs routes
|
||||
// - Enhanced version of rfas.js with list/sort/paging/overdue from rfas-1.js
|
||||
// - Requires appropriate permissions via requirePerm middleware
|
||||
// - Uses project scope for rfa.read, org scope for rfa.create/update/delete
|
||||
// - GET /api/rfas for listing with faceted filters, sorting, and paging
|
||||
// - GET /api/rfas/:id for fetching a single RFA
|
||||
// - POST /api/rfas for creating a new RFA
|
||||
// - PUT /api/rfas/:id for updating an existing RFA (full update)
|
||||
// - PATCH /api/rfas/:id for partial updates
|
||||
// - DELETE /api/rfas/:id for deleting an RFA
|
||||
|
||||
// Base: rfas.js + enhanced list/sort/paging/overdue from rfas-1.js
|
||||
|
||||
import { Router } from 'express';
|
||||
import sql from '../db/index.js';
|
||||
import { requirePerm } from '../middleware/requirePerm.js';
|
||||
import { buildScopeWhere, ownerResolvers } from '../utils/scope.js';
|
||||
import { Router } from "express";
|
||||
import sql from "../db/index.js";
|
||||
import { requirePerm } from "../middleware/requirePerm.js";
|
||||
import { buildScopeWhere, ownerResolvers } from "../utils/scope.js";
|
||||
// import PERM from '../config/permissions.js'; // ถ้าไม่ได้ใช้ สามารถลบได้
|
||||
|
||||
const r = Router();
|
||||
const OWN = ownerResolvers(sql, 'rfas', 'id');
|
||||
const OWN = ownerResolvers(sql, "rfas", "id");
|
||||
|
||||
/* ----------------------------- Utilities ----------------------------- */
|
||||
// Allow-list สำหรับการ sort ป้องกัน SQL injection
|
||||
const ALLOWED_SORT = new Map([
|
||||
['updated_at', 'updated_at'],
|
||||
['due_date', 'due_date'],
|
||||
['created_at', 'created_at'],
|
||||
['id', 'id']
|
||||
["updated_at", "updated_at"],
|
||||
["due_date", "due_date"],
|
||||
["created_at", "created_at"],
|
||||
["id", "id"],
|
||||
]);
|
||||
|
||||
function parseSort(sort = 'updated_at:desc') {
|
||||
const [colRaw, dirRaw] = String(sort).split(':');
|
||||
const col = ALLOWED_SORT.get(colRaw) || 'updated_at';
|
||||
const dir = (dirRaw || 'desc').toLowerCase() === 'asc' ? 'ASC' : 'DESC';
|
||||
function parseSort(sort = "updated_at:desc") {
|
||||
const [colRaw, dirRaw] = String(sort).split(":");
|
||||
const col = ALLOWED_SORT.get(colRaw) || "updated_at";
|
||||
const dir = (dirRaw || "desc").toLowerCase() === "asc" ? "ASC" : "DESC";
|
||||
return `\`${col}\` ${dir}`;
|
||||
}
|
||||
|
||||
@@ -36,15 +51,29 @@ function parsePaging({ page = 1, pageSize = 20 }) {
|
||||
function buildExtraFilters({ q, status, overdue, project_id, org_id }) {
|
||||
const parts = [];
|
||||
const params = {};
|
||||
if (project_id) { parts.push('r.project_id = :project_id'); params.project_id = Number(project_id); }
|
||||
if (org_id) { parts.push('r.org_id = :org_id'); params.org_id = Number(org_id); }
|
||||
if (status) { parts.push('r.status = :status'); params.status = status; }
|
||||
if (q) { parts.push('(r.rfa_no LIKE :q OR r.title LIKE :q OR r.code LIKE :q)'); params.q = `%${q}%`; }
|
||||
if (String(overdue) === '1') {
|
||||
// overdue: due_date < TODAY และสถานะยังไม่ปิด
|
||||
parts.push("r.due_date IS NOT NULL AND r.due_date < CURDATE() AND r.status NOT IN ('Closed','Approved')");
|
||||
if (project_id) {
|
||||
parts.push("r.project_id = :project_id");
|
||||
params.project_id = Number(project_id);
|
||||
}
|
||||
return { where: parts.join(' AND '), params };
|
||||
if (org_id) {
|
||||
parts.push("r.org_id = :org_id");
|
||||
params.org_id = Number(org_id);
|
||||
}
|
||||
if (status) {
|
||||
parts.push("r.status = :status");
|
||||
params.status = status;
|
||||
}
|
||||
if (q) {
|
||||
parts.push("(r.rfa_no LIKE :q OR r.title LIKE :q OR r.code LIKE :q)");
|
||||
params.q = `%${q}%`;
|
||||
}
|
||||
if (String(overdue) === "1") {
|
||||
// overdue: due_date < TODAY และสถานะยังไม่ปิด
|
||||
parts.push(
|
||||
"r.due_date IS NOT NULL AND r.due_date < CURDATE() AND r.status NOT IN ('Closed','Approved')"
|
||||
);
|
||||
}
|
||||
return { where: parts.join(" AND "), params };
|
||||
}
|
||||
|
||||
/* -------------------------------- LIST --------------------------------
|
||||
@@ -52,63 +81,71 @@ function buildExtraFilters({ q, status, overdue, project_id, org_id }) {
|
||||
- คง requirePerm แบบ rfas.js (scope:global + project/org scope ผ่าน buildScopeWhere)
|
||||
- เพิ่ม faceted filters/sort/paging/overdue จาก rfas-1.js
|
||||
------------------------------------------------------------------------*/
|
||||
r.get('/',
|
||||
requirePerm('rfa.read', { scope: 'global' }),
|
||||
async (req, res) => {
|
||||
try {
|
||||
const { q, status, overdue, sort, page, pageSize, project_id, org_id } = req.query;
|
||||
const orderBy = parseSort(sort);
|
||||
const { limit, offset, page: p, pageSize: ps } = parsePaging({ page, pageSize });
|
||||
r.get("/", requirePerm("rfa.read", { scope: "global" }), async (req, res) => {
|
||||
try {
|
||||
const { q, status, overdue, sort, page, pageSize, project_id, org_id } =
|
||||
req.query;
|
||||
const orderBy = parseSort(sort);
|
||||
const {
|
||||
limit,
|
||||
offset,
|
||||
page: p,
|
||||
pageSize: ps,
|
||||
} = parsePaging({ page, pageSize });
|
||||
|
||||
// base scope จาก principal (org/project)
|
||||
const base = buildScopeWhere(req.principal, {
|
||||
tableAlias: 'r', orgColumn: 'r.org_id', projectColumn: 'r.project_id',
|
||||
permCode: 'rfa.read', preferProject: true,
|
||||
});
|
||||
// base scope จาก principal (org/project)
|
||||
const base = buildScopeWhere(req.principal, {
|
||||
tableAlias: "r",
|
||||
orgColumn: "r.org_id",
|
||||
projectColumn: "r.project_id",
|
||||
permCode: "rfa.read",
|
||||
preferProject: true,
|
||||
});
|
||||
|
||||
// extra filters
|
||||
const extra = buildExtraFilters({ q, status, overdue, project_id, org_id });
|
||||
// extra filters
|
||||
const extra = buildExtraFilters({ q, status, overdue, project_id, org_id });
|
||||
|
||||
// รวม where
|
||||
const where = [base.where, extra.where].filter(Boolean).join(' AND ') || '1=1';
|
||||
const params = { ...base.params, ...extra.params, limit, offset };
|
||||
// รวม where
|
||||
const where =
|
||||
[base.where, extra.where].filter(Boolean).join(" AND ") || "1=1";
|
||||
const params = { ...base.params, ...extra.params, limit, offset };
|
||||
|
||||
// total
|
||||
const [[{ cnt: total }]] = await sql.query(
|
||||
`SELECT COUNT(*) AS cnt FROM rfas r WHERE ${where}`,
|
||||
params
|
||||
);
|
||||
// total
|
||||
const [[{ cnt: total }]] = await sql.query(
|
||||
`SELECT COUNT(*) AS cnt FROM rfas r WHERE ${where}`,
|
||||
params
|
||||
);
|
||||
|
||||
// rows
|
||||
const [rows] = await sql.query(
|
||||
`SELECT r.id, r.code, r.rfa_no, r.title, r.status, r.discipline, r.due_date, r.owner_id, r.updated_at, r.project_id, r.org_id
|
||||
// rows
|
||||
const [rows] = await sql.query(
|
||||
`SELECT r.id, r.code, r.rfa_no, r.title, r.status, r.discipline, r.due_date, r.owner_id, r.updated_at, r.project_id, r.org_id
|
||||
FROM rfas r
|
||||
WHERE ${where}
|
||||
ORDER BY ${orderBy}
|
||||
LIMIT :limit OFFSET :offset`,
|
||||
params
|
||||
);
|
||||
params
|
||||
);
|
||||
|
||||
res.json({ data: rows, total: Number(total || 0), page: p, pageSize: ps });
|
||||
} catch (e) {
|
||||
res.status(500).json({ error: e.message || 'rfas/list failed' });
|
||||
}
|
||||
res.json({ data: rows, total: Number(total || 0), page: p, pageSize: ps });
|
||||
} catch (e) {
|
||||
res.status(500).json({ error: e.message || "rfas/list failed" });
|
||||
}
|
||||
);
|
||||
});
|
||||
|
||||
/* ------------------------------- GET ONE ------------------------------
|
||||
// ยึดรูปแบบตรวจสิทธิ์จาก rfas.js
|
||||
------------------------------------------------------------------------*/
|
||||
r.get('/:id',
|
||||
requirePerm('rfa.read', { scope: 'org', getOrgId: OWN.getOrgIdById }),
|
||||
r.get(
|
||||
"/:id",
|
||||
requirePerm("rfa.read", { scope: "org", getOrgId: OWN.getOrgIdById }),
|
||||
async (req, res) => {
|
||||
try {
|
||||
const id = Number(req.params.id);
|
||||
const [[row]] = await sql.query('SELECT * FROM rfas WHERE id=?', [id]);
|
||||
if (!row) return res.status(404).json({ error: 'Not found' });
|
||||
const [[row]] = await sql.query("SELECT * FROM rfas WHERE id=?", [id]);
|
||||
if (!row) return res.status(404).json({ error: "Not found" });
|
||||
res.json(row);
|
||||
} catch (e) {
|
||||
res.status(500).json({ error: e.message || 'rfas/detail failed' });
|
||||
res.status(500).json({ error: e.message || "rfas/detail failed" });
|
||||
}
|
||||
}
|
||||
);
|
||||
@@ -117,14 +154,19 @@ r.get('/:id',
|
||||
// ยึดรูปแบบฟิลด์ของ rfas.js (org_id, project_id, rfa_no, title, status)
|
||||
// เพิ่ม validation เบื้องต้น (title required)
|
||||
------------------------------------------------------------------------*/
|
||||
r.post('/',
|
||||
requirePerm('rfa.create', { scope: 'org', getOrgId: async req => req.body?.org_id ?? null }),
|
||||
r.post(
|
||||
"/",
|
||||
requirePerm("rfa.create", {
|
||||
scope: "org",
|
||||
getOrgId: async (req) => req.body?.org_id ?? null,
|
||||
}),
|
||||
async (req, res) => {
|
||||
try {
|
||||
const { org_id, project_id, rfa_no, title, status } = req.body || {};
|
||||
if (!title?.trim()) return res.status(400).json({ error: 'title is required' });
|
||||
if (!title?.trim())
|
||||
return res.status(400).json({ error: "title is required" });
|
||||
|
||||
const st = String(status || '').trim() || 'draft';
|
||||
const st = String(status || "").trim() || "draft";
|
||||
const [rs] = await sql.query(
|
||||
`INSERT INTO rfas (org_id, project_id, rfa_no, title, status, created_by, created_at, updated_at)
|
||||
VALUES (?,?,?,?,?,?,NOW(),NOW())`,
|
||||
@@ -132,7 +174,7 @@ r.post('/',
|
||||
);
|
||||
res.status(201).json({ id: rs.insertId });
|
||||
} catch (e) {
|
||||
res.status(500).json({ error: e.message || 'rfas/create failed' });
|
||||
res.status(500).json({ error: e.message || "rfas/create failed" });
|
||||
}
|
||||
}
|
||||
);
|
||||
@@ -141,64 +183,86 @@ r.post('/',
|
||||
// PUT: รูปแบบเดิมของ rfas.js (อัปเดต title, status)
|
||||
// PATCH: แบบยืดหยุ่น (บางฟิลด์) ผสานแนวทางจาก rfas-1.js
|
||||
------------------------------------------------------------------------*/
|
||||
r.put('/:id',
|
||||
requirePerm('rfa.update', { scope: 'org', getOrgId: OWN.getOrgIdById }),
|
||||
r.put(
|
||||
"/:id",
|
||||
requirePerm("rfa.update", { scope: "org", getOrgId: OWN.getOrgIdById }),
|
||||
async (req, res) => {
|
||||
try {
|
||||
const id = Number(req.params.id);
|
||||
const { title, status } = req.body || {};
|
||||
await sql.query('UPDATE rfas SET title=?, status=?, updated_at=NOW() WHERE id=?', [title, status, id]);
|
||||
await sql.query(
|
||||
"UPDATE rfas SET title=?, status=?, updated_at=NOW() WHERE id=?",
|
||||
[title, status, id]
|
||||
);
|
||||
res.json({ ok: 1, id });
|
||||
} catch (e) {
|
||||
res.status(500).json({ error: e.message || 'rfas/update failed' });
|
||||
res.status(500).json({ error: e.message || "rfas/update failed" });
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
// PATCH แบบ partial fields
|
||||
r.patch('/:id',
|
||||
requirePerm('rfa.update', { scope: 'org', getOrgId: OWN.getOrgIdById }),
|
||||
r.patch(
|
||||
"/:id",
|
||||
requirePerm("rfa.update", { scope: "org", getOrgId: OWN.getOrgIdById }),
|
||||
async (req, res) => {
|
||||
try {
|
||||
const id = Number(req.params.id);
|
||||
const allowed = ['code', 'rfa_no', 'title', 'discipline', 'due_date', 'description', 'status', 'owner_id'];
|
||||
const allowed = [
|
||||
"code",
|
||||
"rfa_no",
|
||||
"title",
|
||||
"discipline",
|
||||
"due_date",
|
||||
"description",
|
||||
"status",
|
||||
"owner_id",
|
||||
];
|
||||
const patch = {};
|
||||
for (const k of allowed) if (k in req.body) patch[k] = req.body[k];
|
||||
|
||||
if (Object.keys(patch).length === 0) {
|
||||
return res.status(400).json({ error: 'no fields to update' });
|
||||
return res.status(400).json({ error: "no fields to update" });
|
||||
}
|
||||
|
||||
if ('status' in patch) {
|
||||
if ("status" in patch) {
|
||||
const s = String(patch.status);
|
||||
const ok = ['draft','submitted','Pending','Review','Approved','Closed'].includes(s);
|
||||
if (!ok) return res.status(400).json({ error: 'invalid status' });
|
||||
const ok = [
|
||||
"draft",
|
||||
"submitted",
|
||||
"Pending",
|
||||
"Review",
|
||||
"Approved",
|
||||
"Closed",
|
||||
].includes(s);
|
||||
if (!ok) return res.status(400).json({ error: "invalid status" });
|
||||
}
|
||||
|
||||
const sets = Object.keys(patch).map(k => `\`${k}\` = :${k}`);
|
||||
const sets = Object.keys(patch).map((k) => `\`${k}\` = :${k}`);
|
||||
patch.id = id;
|
||||
|
||||
await sql.query(
|
||||
`UPDATE rfas SET ${sets.join(', ')}, updated_at=NOW() WHERE id=:id`,
|
||||
`UPDATE rfas SET ${sets.join(", ")}, updated_at=NOW() WHERE id=:id`,
|
||||
patch
|
||||
);
|
||||
res.json({ ok: 1, id });
|
||||
} catch (e) {
|
||||
res.status(500).json({ error: e.message || 'rfas/patch failed' });
|
||||
res.status(500).json({ error: e.message || "rfas/patch failed" });
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
/* ------------------------------- DELETE ------------------------------- */
|
||||
r.delete('/:id',
|
||||
requirePerm('rfa.delete', { scope: 'org', getOrgId: OWN.getOrgIdById }),
|
||||
r.delete(
|
||||
"/:id",
|
||||
requirePerm("rfa.delete", { scope: "org", getOrgId: OWN.getOrgIdById }),
|
||||
async (req, res) => {
|
||||
try {
|
||||
const id = Number(req.params.id);
|
||||
await sql.query('DELETE FROM rfas WHERE id=?', [id]);
|
||||
await sql.query("DELETE FROM rfas WHERE id=?", [id]);
|
||||
res.json({ ok: 1, id });
|
||||
} catch (e) {
|
||||
res.status(500).json({ error: e.message || 'rfas/delete failed' });
|
||||
res.status(500).json({ error: e.message || "rfas/delete failed" });
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
@@ -1,48 +1,93 @@
|
||||
import { Router } from 'express';
|
||||
import { requireAuth } from '../middleware/auth.js';
|
||||
import { enrichPermissions } from '../middleware/permissions.js';
|
||||
import { requireRole } from '../middleware/rbac.js';
|
||||
import { requirePerm } from '../middleware/permGuard.js';
|
||||
import { sequelize } from '../db/sequelize.js';
|
||||
import SubCatModel from '../db/models/SubCategory.js';
|
||||
// FILE: src/routes/subcategories.js
|
||||
// Subcategories routes
|
||||
// - CRUD operations for subcategories
|
||||
// - Requires appropriate permissions via requirePerm middleware
|
||||
// - Uses project scope for all permissions
|
||||
// - lookup:edit
|
||||
// - Subcategory fields: sub_cat_id (PK), project_id (FK), sub_cat_name, parent_cat_id (FK), code
|
||||
// - Basic validation: project_id, sub_cat_name required for create
|
||||
|
||||
import { Router } from "express";
|
||||
import { requireAuth } from "../middleware/auth.js";
|
||||
import { enrichPermissions } from "../middleware/permissions.js";
|
||||
import { requireRole } from "../middleware/rbac.js";
|
||||
import { requirePerm } from "../middleware/permGuard.js";
|
||||
import { sequelize } from "../db/sequelize.js";
|
||||
import SubCatModel from "../db/models/SubCategory.js";
|
||||
|
||||
const r = Router();
|
||||
const SubCat = SubCatModel(sequelize);
|
||||
|
||||
r.get('/sub_categories', requireAuth, async (req, res) => {
|
||||
const { q, project_id, page=1, page_size=50 } = req.query;
|
||||
const limit = Math.min(Number(page_size)||50, 200);
|
||||
const offset = (Math.max(Number(page)||1,1)-1) * limit;
|
||||
r.get("/sub_categories", requireAuth, async (req, res) => {
|
||||
const { q, project_id, page = 1, page_size = 50 } = req.query;
|
||||
const limit = Math.min(Number(page_size) || 50, 200);
|
||||
const offset = (Math.max(Number(page) || 1, 1) - 1) * limit;
|
||||
const where = {};
|
||||
if (project_id) where.project_id = project_id;
|
||||
if (q) where.sub_cat_name = sequelize.where(sequelize.fn('LOWER', sequelize.col('sub_cat_name')), 'LIKE', `%${String(q).toLowerCase()}%`);
|
||||
const { rows, count } = await SubCat.findAndCountAll({ where, limit, offset, order:[['sub_cat_name','ASC']] });
|
||||
if (q)
|
||||
where.sub_cat_name = sequelize.where(
|
||||
sequelize.fn("LOWER", sequelize.col("sub_cat_name")),
|
||||
"LIKE",
|
||||
`%${String(q).toLowerCase()}%`
|
||||
);
|
||||
const { rows, count } = await SubCat.findAndCountAll({
|
||||
where,
|
||||
limit,
|
||||
offset,
|
||||
order: [["sub_cat_name", "ASC"]],
|
||||
});
|
||||
res.json({ items: rows, total: count, page: Number(page), page_size: limit });
|
||||
});
|
||||
|
||||
r.post('/sub_categories', requireAuth, enrichPermissions(), requirePerm('lookup:edit'), async (req, res) => {
|
||||
const { project_id, sub_cat_name, parent_cat_id, code } = req.body || {};
|
||||
if (!project_id || !sub_cat_name) return res.status(400).json({ error: 'project_id and sub_cat_name required' });
|
||||
const created = await SubCat.create({ project_id, sub_cat_name, parent_cat_id, code });
|
||||
res.status(201).json({ sub_cat_id: created.sub_cat_id });
|
||||
});
|
||||
r.post(
|
||||
"/sub_categories",
|
||||
requireAuth,
|
||||
enrichPermissions(),
|
||||
requirePerm("lookup:edit"),
|
||||
async (req, res) => {
|
||||
const { project_id, sub_cat_name, parent_cat_id, code } = req.body || {};
|
||||
if (!project_id || !sub_cat_name)
|
||||
return res
|
||||
.status(400)
|
||||
.json({ error: "project_id and sub_cat_name required" });
|
||||
const created = await SubCat.create({
|
||||
project_id,
|
||||
sub_cat_name,
|
||||
parent_cat_id,
|
||||
code,
|
||||
});
|
||||
res.status(201).json({ sub_cat_id: created.sub_cat_id });
|
||||
}
|
||||
);
|
||||
|
||||
r.patch('/sub_categories/:id', requireAuth, enrichPermissions(), requirePerm('lookup:edit'), async (req, res) => {
|
||||
const row = await SubCat.findByPk(Number(req.params.id));
|
||||
if (!row) return res.status(404).json({ error: 'Not found' });
|
||||
const { sub_cat_name, parent_cat_id, code } = req.body || {};
|
||||
if (sub_cat_name !== undefined) row.sub_cat_name = sub_cat_name;
|
||||
if (parent_cat_id !== undefined) row.parent_cat_id = parent_cat_id;
|
||||
if (code !== undefined) row.code = code;
|
||||
await row.save();
|
||||
res.json({ ok: true });
|
||||
});
|
||||
r.patch(
|
||||
"/sub_categories/:id",
|
||||
requireAuth,
|
||||
enrichPermissions(),
|
||||
requirePerm("lookup:edit"),
|
||||
async (req, res) => {
|
||||
const row = await SubCat.findByPk(Number(req.params.id));
|
||||
if (!row) return res.status(404).json({ error: "Not found" });
|
||||
const { sub_cat_name, parent_cat_id, code } = req.body || {};
|
||||
if (sub_cat_name !== undefined) row.sub_cat_name = sub_cat_name;
|
||||
if (parent_cat_id !== undefined) row.parent_cat_id = parent_cat_id;
|
||||
if (code !== undefined) row.code = code;
|
||||
await row.save();
|
||||
res.json({ ok: true });
|
||||
}
|
||||
);
|
||||
|
||||
r.delete('/sub_categories/:id', requireAuth, enrichPermissions(), requirePerm('lookup:edit'), async (req, res) => {
|
||||
const row = await SubCat.findByPk(Number(req.params.id));
|
||||
if (!row) return res.status(404).json({ error: 'Not found' });
|
||||
await row.destroy();
|
||||
res.json({ ok: true });
|
||||
});
|
||||
r.delete(
|
||||
"/sub_categories/:id",
|
||||
requireAuth,
|
||||
enrichPermissions(),
|
||||
requirePerm("lookup:edit"),
|
||||
async (req, res) => {
|
||||
const row = await SubCat.findByPk(Number(req.params.id));
|
||||
if (!row) return res.status(404).json({ error: "Not found" });
|
||||
await row.destroy();
|
||||
res.json({ ok: true });
|
||||
}
|
||||
);
|
||||
|
||||
export default r;
|
||||
|
||||
@@ -1,4 +1,36 @@
|
||||
// src/routes/technicaldocs.js (ESM)
|
||||
// FILE: src/routes/technicaldocs.js
|
||||
// Technical Documents routes
|
||||
// - CRUD operations for technical documents
|
||||
// - Requires appropriate permissions via requirePerm middleware
|
||||
// - Supports filtering and pagination on list endpoint
|
||||
// - Uses ownerResolvers utility to determine org ownership for permission checks
|
||||
// - Permissions required are defined in config/permissions.js
|
||||
// - technicaldoc.read
|
||||
// - technicaldoc.create
|
||||
// - technicaldoc.update
|
||||
// - technicaldoc.delete
|
||||
// - Scope can be 'global' (list), 'org' (get/create/update/delete)
|
||||
// - List endpoint supports filtering by project_id, org_id, status, and search query (q)
|
||||
// - Pagination via limit and offset query parameters
|
||||
// - Results ordered by id DESC
|
||||
// - Error handling for not found and no fields to update scenarios
|
||||
// - Uses async/await for asynchronous operations
|
||||
// - SQL queries use parameterized queries to prevent SQL injection
|
||||
// - Responses are in JSON format
|
||||
// - Middleware functions are used for permission checks
|
||||
// - Owner resolvers are used to fetch org_id for specific document ids
|
||||
// - Code is modular and organized for maintainability
|
||||
// - Comments are provided for clarity/documentation
|
||||
// - Follows best practices for Express.js route handling
|
||||
// - Uses ES6+ features for cleaner code
|
||||
// - Assumes existence of technicaldocs table with appropriate columns
|
||||
// - Assumes existence of users table for created_by field
|
||||
// - Assumes existence of config/permissions.js with defined permission codes
|
||||
// - Assumes existence of utils/scope.js with buildScopeWhere and ownerResolvers functions
|
||||
// - Assumes existence of middleware/requirePerm.js for permission checks
|
||||
// - Assumes existence of db/index.js for database connection/querying
|
||||
// - Assumes Express.js app is set up to use this router for /api/technicaldocs path
|
||||
|
||||
import { Router } from 'express';
|
||||
import sql from '../db/index.js';
|
||||
import { requirePerm } from '../middleware/requirePerm.js';
|
||||
|
||||
@@ -1,32 +1,45 @@
|
||||
// backend/src/routes/transmittals.js (merged)
|
||||
// FILE: src/routes/transmittals.js
|
||||
// Transmittals routes
|
||||
// - Enhanced version of transmittals.js with list/sort/paging from transmittals-1.js
|
||||
// - Supports GET /transmittals with filtering, sorting, and pagination
|
||||
// - Requires appropriate permissions via requirePerm middleware
|
||||
// - GET by id, POST (create), PUT (update), PATCH (partial update), DELETE
|
||||
// - RBAC/Scope
|
||||
// - Global scope: list all transmittals user has access to (project/org scope applied)
|
||||
// - Org scope: get/create/update/delete transmittals within a specific org
|
||||
// - Permissions required:
|
||||
// - transmittal.read (global/org) for
|
||||
// - GET /transmittals (list)
|
||||
// - GET /transmittals/:id (get by id)
|
||||
|
||||
// Base: transmittals.js + list/sort/paging from transmittals-1.js
|
||||
// Notes:
|
||||
// - ยึด RBAC/Scope/Permissions แบบเดิมของ transmittals.js
|
||||
// - Faceted list -> ส่ง meta { data, total, page, pageSize }
|
||||
// - PATCH: อัปเดตบางฟิลด์อย่างปลอดภัย (ไม่บังคับให้มีคอลัมน์ใหม่ใน DB)
|
||||
|
||||
import { Router } from 'express';
|
||||
import sql from '../db/index.js';
|
||||
import { requirePerm } from '../middleware/requirePerm.js';
|
||||
import { buildScopeWhere, ownerResolvers } from '../utils/scope.js';
|
||||
import PERM from '../config/permissions.js';
|
||||
import { Router } from "express";
|
||||
import sql from "../db/index.js";
|
||||
import { requirePerm } from "../middleware/requirePerm.js";
|
||||
import { buildScopeWhere, ownerResolvers } from "../utils/scope.js";
|
||||
import PERM from "../config/permissions.js";
|
||||
|
||||
const r = Router();
|
||||
const OWN = ownerResolvers(sql, 'transmittals', 'id');
|
||||
const OWN = ownerResolvers(sql, "transmittals", "id");
|
||||
|
||||
/* ----------------------------- Utilities ----------------------------- */
|
||||
// จำกัดฟิลด์ที่อนุญาตให้ sort เพื่อลดความเสี่ยง SQLi
|
||||
const ALLOWED_SORT = new Map([
|
||||
['updated_at', 'updated_at'],
|
||||
['created_at', 'created_at'],
|
||||
['id', 'id'],
|
||||
['tr_no', 'tr_no'],
|
||||
['subject', 'subject'],
|
||||
["updated_at", "updated_at"],
|
||||
["created_at", "created_at"],
|
||||
["id", "id"],
|
||||
["tr_no", "tr_no"],
|
||||
["subject", "subject"],
|
||||
]);
|
||||
function parseSort(sort = 'updated_at:desc') {
|
||||
const [colRaw, dirRaw] = String(sort).split(':');
|
||||
const col = ALLOWED_SORT.get(colRaw) || 'updated_at';
|
||||
const dir = (dirRaw || 'desc').toLowerCase() === 'asc' ? 'ASC' : 'DESC';
|
||||
function parseSort(sort = "updated_at:desc") {
|
||||
const [colRaw, dirRaw] = String(sort).split(":");
|
||||
const col = ALLOWED_SORT.get(colRaw) || "updated_at";
|
||||
const dir = (dirRaw || "desc").toLowerCase() === "asc" ? "ASC" : "DESC";
|
||||
return `\`${col}\` ${dir}`;
|
||||
}
|
||||
function parsePaging({ page = 1, pageSize = 20 }) {
|
||||
@@ -38,15 +51,24 @@ function parsePaging({ page = 1, pageSize = 20 }) {
|
||||
function buildExtraFilters({ project_id, org_id, tr_no, q }) {
|
||||
const extra = [];
|
||||
const params = {};
|
||||
if (project_id) { extra.push('t.project_id = :project_id'); params.project_id = Number(project_id); }
|
||||
if (org_id) { extra.push('t.org_id = :org_id'); params.org_id = Number(org_id); }
|
||||
if (tr_no) { extra.push('t.tr_no = :tr_no'); params.tr_no = tr_no; }
|
||||
if (project_id) {
|
||||
extra.push("t.project_id = :project_id");
|
||||
params.project_id = Number(project_id);
|
||||
}
|
||||
if (org_id) {
|
||||
extra.push("t.org_id = :org_id");
|
||||
params.org_id = Number(org_id);
|
||||
}
|
||||
if (tr_no) {
|
||||
extra.push("t.tr_no = :tr_no");
|
||||
params.tr_no = tr_no;
|
||||
}
|
||||
if (q) {
|
||||
// ใช้ฟิลด์พื้นฐานที่ transmittals.js มีแน่นอน (tr_no, subject)
|
||||
extra.push('(t.tr_no LIKE :q OR t.subject LIKE :q)');
|
||||
extra.push("(t.tr_no LIKE :q OR t.subject LIKE :q)");
|
||||
params.q = `%${q}%`;
|
||||
}
|
||||
return { where: extra.join(' AND '), params };
|
||||
return { where: extra.join(" AND "), params };
|
||||
}
|
||||
|
||||
/* -------------------------------- LIST --------------------------------
|
||||
@@ -54,24 +76,31 @@ GET /transmittals
|
||||
- คง RBAC/Scope เดิม (global + project/org scope ผ่าน buildScopeWhere)
|
||||
- เพิ่ม sort/page/pageSize/q ตามสไตล์ transmittals-1.js และตอบ meta
|
||||
------------------------------------------------------------------------*/
|
||||
r.get('/',
|
||||
requirePerm(PERM.transmittal.read, { scope: 'global' }),
|
||||
r.get(
|
||||
"/",
|
||||
requirePerm(PERM.transmittal.read, { scope: "global" }),
|
||||
async (req, res) => {
|
||||
try {
|
||||
const { project_id, org_id, tr_no, q, sort, page, pageSize } = req.query;
|
||||
const orderBy = parseSort(sort);
|
||||
const { limit, offset, page: p, pageSize: ps } = parsePaging({ page, pageSize });
|
||||
const {
|
||||
limit,
|
||||
offset,
|
||||
page: p,
|
||||
pageSize: ps,
|
||||
} = parsePaging({ page, pageSize });
|
||||
|
||||
const base = buildScopeWhere(req.principal, {
|
||||
tableAlias: 't',
|
||||
orgColumn: 't.org_id',
|
||||
projectColumn: 't.project_id',
|
||||
tableAlias: "t",
|
||||
orgColumn: "t.org_id",
|
||||
projectColumn: "t.project_id",
|
||||
permCode: PERM.transmittal.read,
|
||||
preferProject: true
|
||||
preferProject: true,
|
||||
});
|
||||
|
||||
const extra = buildExtraFilters({ project_id, org_id, tr_no, q });
|
||||
const where = [base.where, extra.where].filter(Boolean).join(' AND ') || '1=1';
|
||||
const where =
|
||||
[base.where, extra.where].filter(Boolean).join(" AND ") || "1=1";
|
||||
const params = { ...base.params, ...extra.params, limit, offset };
|
||||
|
||||
// total
|
||||
@@ -90,31 +119,48 @@ r.get('/',
|
||||
params
|
||||
);
|
||||
|
||||
res.json({ data: rows, total: Number(total || 0), page: p, pageSize: ps });
|
||||
res.json({
|
||||
data: rows,
|
||||
total: Number(total || 0),
|
||||
page: p,
|
||||
pageSize: ps,
|
||||
});
|
||||
} catch (e) {
|
||||
res.status(500).json({ error: e.message || 'transmittals/list failed' });
|
||||
res.status(500).json({ error: e.message || "transmittals/list failed" });
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
/* ------------------------------- GET ONE ------------------------------ */
|
||||
r.get('/:id',
|
||||
requirePerm(PERM.transmittal.read, { scope: 'org', getOrgId: OWN.getOrgIdById }),
|
||||
r.get(
|
||||
"/:id",
|
||||
requirePerm(PERM.transmittal.read, {
|
||||
scope: "org",
|
||||
getOrgId: OWN.getOrgIdById,
|
||||
}),
|
||||
async (req, res) => {
|
||||
try {
|
||||
const id = Number(req.params.id);
|
||||
const [[row]] = await sql.query('SELECT * FROM transmittals WHERE id=?', [id]);
|
||||
if (!row) return res.status(404).json({ error: 'Not found' });
|
||||
const [[row]] = await sql.query("SELECT * FROM transmittals WHERE id=?", [
|
||||
id,
|
||||
]);
|
||||
if (!row) return res.status(404).json({ error: "Not found" });
|
||||
res.json(row);
|
||||
} catch (e) {
|
||||
res.status(500).json({ error: e.message || 'transmittals/detail failed' });
|
||||
res
|
||||
.status(500)
|
||||
.json({ error: e.message || "transmittals/detail failed" });
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
/* -------------------------------- CREATE ------------------------------ */
|
||||
r.post('/',
|
||||
requirePerm(PERM.transmittal.create, { scope: 'org', getOrgId: async req => req.body?.org_id ?? null }),
|
||||
r.post(
|
||||
"/",
|
||||
requirePerm(PERM.transmittal.create, {
|
||||
scope: "org",
|
||||
getOrgId: async (req) => req.body?.org_id ?? null,
|
||||
}),
|
||||
async (req, res) => {
|
||||
try {
|
||||
// ยึดสคีมาหลักจาก transmittals.js
|
||||
@@ -126,35 +172,50 @@ r.post('/',
|
||||
);
|
||||
res.status(201).json({ id: rs.insertId });
|
||||
} catch (e) {
|
||||
res.status(500).json({ error: e.message || 'transmittals/create failed' });
|
||||
res
|
||||
.status(500)
|
||||
.json({ error: e.message || "transmittals/create failed" });
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
/* -------------------------------- UPDATE ------------------------------ */
|
||||
// PUT: รูปแบบเดิม (อัปเดต subject, status)
|
||||
r.put('/:id',
|
||||
requirePerm(PERM.transmittal.update, { scope: 'org', getOrgId: OWN.getOrgIdById }),
|
||||
r.put(
|
||||
"/:id",
|
||||
requirePerm(PERM.transmittal.update, {
|
||||
scope: "org",
|
||||
getOrgId: OWN.getOrgIdById,
|
||||
}),
|
||||
async (req, res) => {
|
||||
try {
|
||||
const id = Number(req.params.id);
|
||||
const { subject, status } = req.body;
|
||||
await sql.query('UPDATE transmittals SET subject=?, status=? WHERE id=?', [subject, status, id]);
|
||||
await sql.query(
|
||||
"UPDATE transmittals SET subject=?, status=? WHERE id=?",
|
||||
[subject, status, id]
|
||||
);
|
||||
res.json({ ok: 1 });
|
||||
} catch (e) {
|
||||
res.status(500).json({ error: e.message || 'transmittals/update failed' });
|
||||
res
|
||||
.status(500)
|
||||
.json({ error: e.message || "transmittals/update failed" });
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
// PATCH: อัปเดตบางฟิลด์ (ไม่บังคับคอลัมน์ใหม่ใน DB — จะอัปเดตก็ต่อเมื่อ body ส่งมา)
|
||||
r.patch('/:id',
|
||||
requirePerm(PERM.transmittal.update, { scope: 'org', getOrgId: OWN.getOrgIdById }),
|
||||
r.patch(
|
||||
"/:id",
|
||||
requirePerm(PERM.transmittal.update, {
|
||||
scope: "org",
|
||||
getOrgId: OWN.getOrgIdById,
|
||||
}),
|
||||
async (req, res) => {
|
||||
try {
|
||||
const id = Number(req.params.id);
|
||||
// อนุญาตเฉพาะฟิลด์ที่คาดว่ามีในสคีมาหลัก
|
||||
const allowed = ['tr_no', 'subject', 'status'];
|
||||
const allowed = ["tr_no", "subject", "status"];
|
||||
|
||||
// ถ้าฐานข้อมูลของคุณมีคอลัมน์เพิ่ม เช่น to_party, sent_date, description
|
||||
// และต้องการอัปเดตด้วย ให้เพิ่มชื่อคอลัมน์เข้าไปใน allowed ได้
|
||||
@@ -164,39 +225,55 @@ r.patch('/:id',
|
||||
for (const k of allowed) if (k in req.body) patch[k] = req.body[k];
|
||||
|
||||
if (Object.keys(patch).length === 0) {
|
||||
return res.status(400).json({ error: 'no fields to update' });
|
||||
return res.status(400).json({ error: "no fields to update" });
|
||||
}
|
||||
|
||||
if ('status' in patch) {
|
||||
if ("status" in patch) {
|
||||
const s = String(patch.status);
|
||||
const ok = ['draft','submitted','Sent','Closed','Approved','Pending','Review'].includes(s);
|
||||
if (!ok) return res.status(400).json({ error: 'invalid status' });
|
||||
const ok = [
|
||||
"draft",
|
||||
"submitted",
|
||||
"Sent",
|
||||
"Closed",
|
||||
"Approved",
|
||||
"Pending",
|
||||
"Review",
|
||||
].includes(s);
|
||||
if (!ok) return res.status(400).json({ error: "invalid status" });
|
||||
}
|
||||
|
||||
const sets = Object.keys(patch).map(k => `\`${k}\` = :${k}`);
|
||||
const sets = Object.keys(patch).map((k) => `\`${k}\` = :${k}`);
|
||||
patch.id = id;
|
||||
|
||||
await sql.query(
|
||||
`UPDATE transmittals SET ${sets.join(', ')}, updated_at = NOW() WHERE id = :id`,
|
||||
`UPDATE transmittals SET ${sets.join(
|
||||
", "
|
||||
)}, updated_at = NOW() WHERE id = :id`,
|
||||
patch
|
||||
);
|
||||
res.json({ ok: 1, id });
|
||||
} catch (e) {
|
||||
res.status(500).json({ error: e.message || 'transmittals/patch failed' });
|
||||
res.status(500).json({ error: e.message || "transmittals/patch failed" });
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
/* -------------------------------- DELETE ------------------------------ */
|
||||
r.delete('/:id',
|
||||
requirePerm(PERM.transmittal.delete, { scope: 'org', getOrgId: OWN.getOrgIdById }),
|
||||
r.delete(
|
||||
"/:id",
|
||||
requirePerm(PERM.transmittal.delete, {
|
||||
scope: "org",
|
||||
getOrgId: OWN.getOrgIdById,
|
||||
}),
|
||||
async (req, res) => {
|
||||
try {
|
||||
const id = Number(req.params.id);
|
||||
await sql.query('DELETE FROM transmittals WHERE id=?', [id]);
|
||||
await sql.query("DELETE FROM transmittals WHERE id=?", [id]);
|
||||
res.json({ ok: 1 });
|
||||
} catch (e) {
|
||||
res.status(500).json({ error: e.message || 'transmittals/delete failed' });
|
||||
res
|
||||
.status(500)
|
||||
.json({ error: e.message || "transmittals/delete failed" });
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
@@ -1,67 +1,125 @@
|
||||
import { Router } from 'express';
|
||||
import multer from 'multer';
|
||||
import fs from 'node:fs';
|
||||
import path from 'node:path';
|
||||
import sql from '../db/index.js';
|
||||
import { requirePerm } from '../middleware/requirePerm.js';
|
||||
import PERM from '../config/permissions.js';
|
||||
// FILE: src/routes/uploads.js
|
||||
// 03.2 10) เพิ่ม routes/uploads.js (ใหม่)
|
||||
// - ใช้ร่วมกับ requirePerm()
|
||||
// - สำหรับอัพโหลดไฟล์แนบที่เกี่ยวข้องกับ module item ต่างๆ (เช่น correspondence, rfa, drawing) ตามสิทธิ์ของผู้ใช้
|
||||
// Uploads routes
|
||||
// - POST /:module/:id/file to upload a file associated with a module item (e.g. correspondence, rfa, drawing)
|
||||
// - Uses multer for file handling
|
||||
// - Stores files in structured directories based on org_id, project_id, and creation date
|
||||
// - Requires appropriate permissions via requirePerm middleware
|
||||
// - Supported modules: correspondences
|
||||
// - Requires appropriate permissions via requirePerm middleware
|
||||
// - Permissions are mapped in PERM_UPLOAD
|
||||
// - Ensure req.user.permissions is populated (e.g. via auth.js or authJwt.js with enrichment)
|
||||
// - Requires req.user to have the upload permission for the specific module and project scope
|
||||
// - Example: POST /correspondences/123/file with form-data including 'file' field
|
||||
// - Environment variable UPLOAD_BASE defines the base directory for uploads (default: /share/dms-data)
|
||||
// - Directory structure: UPLOAD_BASE/module/org_id/project_id/YYYY-MM
|
||||
// - Filename format: timestamp__originalname (with unsafe characters replaced by '_')
|
||||
// - Response: { ok: 1, module, ref_id, filename, path, size, mime }
|
||||
// - Assumes existence of necessary database tables and columns
|
||||
// - Assumes existence of necessary middleware and utility functions
|
||||
// - Assumes Express.js app is set up to use this router for /api/uploads path
|
||||
// - Assumes existence of necessary environment variables
|
||||
// - Assumes existence of necessary directories and permissions for file storage
|
||||
// - Assumes multer is installed and configured
|
||||
// - Assumes fs and path modules are available for file system operations
|
||||
// - Assumes sql module is set up for database interactions
|
||||
// - Assumes PERM constants are defined in config/permissions.js
|
||||
// - Assumes requirePerm middleware is defined in middleware/requirePerm.js
|
||||
// - Assumes Express.js app is set up to use this router for /api/uploads path
|
||||
// - Assumes multer is installed and configured
|
||||
// - Assumes fs and path modules are available for file system operations
|
||||
// - Assumes sql module is set up for database interactions
|
||||
|
||||
import { Router } from "express";
|
||||
import multer from "multer";
|
||||
import fs from "node:fs";
|
||||
import path from "node:path";
|
||||
import sql from "../db/index.js";
|
||||
import { requirePerm } from "../middleware/requirePerm.js";
|
||||
import PERM from "../config/permissions.js";
|
||||
|
||||
const r = Router();
|
||||
const UPLOAD_BASE = process.env.UPLOAD_BASE || '/share/dms-data';
|
||||
function ensureDir(p) { if (!fs.existsSync(p)) fs.mkdirSync(p, { recursive: true }); }
|
||||
|
||||
const UPLOAD_BASE = process.env.UPLOAD_BASE || "/share/dms-data";
|
||||
function ensureDir(p) {
|
||||
if (!fs.existsSync(p)) fs.mkdirSync(p, { recursive: true });
|
||||
}
|
||||
|
||||
const storage = multer.diskStorage({
|
||||
destination: async (req, file, cb) => {
|
||||
try {
|
||||
const { module, id } = req.params;
|
||||
const [[row]] = await sql.query(`SELECT org_id, project_id, created_at FROM ${module} WHERE id=?`, [Number(id)]);
|
||||
if (!row) return cb(new Error('Resource not found'));
|
||||
const dt = new Date(row.created_at || Date.now());
|
||||
const ym = `${dt.getUTCFullYear()}-${String(dt.getUTCMonth()+1).padStart(2,'0')}`;
|
||||
const dir = path.join(UPLOAD_BASE, module, String(row.org_id), String(row.project_id), ym);
|
||||
ensureDir(dir);
|
||||
cb(null, dir);
|
||||
} catch (e) { cb(e); }
|
||||
},
|
||||
filename: (req, file, cb) => {
|
||||
const ts = Date.now();
|
||||
const safe = file.originalname.replace(/[\^\w.\-]+/g, '_');
|
||||
cb(null, `${ts}__${safe}`);
|
||||
}
|
||||
destination: async (req, file, cb) => {
|
||||
try {
|
||||
const { module, id } = req.params;
|
||||
const [[row]] = await sql.query(
|
||||
`SELECT org_id, project_id, created_at FROM ${module} WHERE id=?`,
|
||||
[Number(id)]
|
||||
);
|
||||
if (!row) return cb(new Error("Resource not found"));
|
||||
const dt = new Date(row.created_at || Date.now());
|
||||
const ym = `${dt.getUTCFullYear()}-${String(
|
||||
dt.getUTCMonth() + 1
|
||||
).padStart(2, "0")}`;
|
||||
const dir = path.join(
|
||||
UPLOAD_BASE,
|
||||
module,
|
||||
String(row.org_id),
|
||||
String(row.project_id),
|
||||
ym
|
||||
);
|
||||
ensureDir(dir);
|
||||
cb(null, dir);
|
||||
} catch (e) {
|
||||
cb(e);
|
||||
}
|
||||
},
|
||||
filename: (req, file, cb) => {
|
||||
const ts = Date.now();
|
||||
const safe = file.originalname.replace(/[\^\w.\-]+/g, "_");
|
||||
cb(null, `${ts}__${safe}`);
|
||||
},
|
||||
});
|
||||
const upload = multer({ storage });
|
||||
|
||||
|
||||
const PERM_UPLOAD = {
|
||||
correspondences: PERM.correspondence.upload,
|
||||
rfas: PERM.rfa.upload,
|
||||
drawings: PERM.drawing.upload,
|
||||
transmittals: PERM.transmittal?.upload,
|
||||
correspondences: PERM.correspondence.upload,
|
||||
rfas: PERM.rfa.upload,
|
||||
drawings: PERM.drawing.upload,
|
||||
transmittals: PERM.transmittal?.upload,
|
||||
};
|
||||
|
||||
|
||||
async function getProjectIdByModule(req){
|
||||
const { module, id } = req.params;
|
||||
const [[row]] = await sql.query(`SELECT project_id FROM ${module} WHERE id=?`, [Number(id)]);
|
||||
return row?.project_id ?? null;
|
||||
async function getProjectIdByModule(req) {
|
||||
const { module, id } = req.params;
|
||||
const [[row]] = await sql.query(
|
||||
`SELECT project_id FROM ${module} WHERE id=?`,
|
||||
[Number(id)]
|
||||
);
|
||||
return row?.project_id ?? null;
|
||||
}
|
||||
|
||||
|
||||
r.post('/:module/:id/file',
|
||||
(req, res, next) => {
|
||||
const perm = PERM_UPLOAD[req.params.module];
|
||||
if (!perm) return res.status(400).json({ error: 'Unsupported module' });
|
||||
return requirePerm(perm, { scope: 'project', getProjectId: getProjectIdByModule })(req, res, next);
|
||||
},
|
||||
upload.single('file'),
|
||||
async (req, res) => {
|
||||
const { module, id } = req.params;
|
||||
const file = req.file;
|
||||
res.json({ ok: 1, module, ref_id: Number(id), filename: file.filename, path: file.path, size: file.size, mime: file.mimetype });
|
||||
}
|
||||
r.post(
|
||||
"/:module/:id/file",
|
||||
(req, res, next) => {
|
||||
const perm = PERM_UPLOAD[req.params.module];
|
||||
if (!perm) return res.status(400).json({ error: "Unsupported module" });
|
||||
return requirePerm(perm, {
|
||||
scope: "project",
|
||||
getProjectId: getProjectIdByModule,
|
||||
})(req, res, next);
|
||||
},
|
||||
upload.single("file"),
|
||||
async (req, res) => {
|
||||
const { module, id } = req.params;
|
||||
const file = req.file;
|
||||
res.json({
|
||||
ok: 1,
|
||||
module,
|
||||
ref_id: Number(id),
|
||||
filename: file.filename,
|
||||
path: file.path,
|
||||
size: file.size,
|
||||
mime: file.mimetype,
|
||||
});
|
||||
}
|
||||
);
|
||||
|
||||
|
||||
export default r;
|
||||
export default r;
|
||||
|
||||
@@ -1,32 +1,51 @@
|
||||
import { Router } from 'express';
|
||||
import sql from '../db/index.js';
|
||||
import { requirePerm } from '../middleware/requirePerm.js';
|
||||
import PERM from '../config/permissions.js';
|
||||
// FILE: src/routes/users.js
|
||||
// 03.2 11) เพิ่ม routes/users.js (ใหม่)
|
||||
// - ใช้ร่วมกับ requirePerm()
|
||||
// - สำหรับดูข้อมูลผู้ใช้ตัวเอง และรายชื่อผู้ใช้ (สำหรับ SUPER_ADMIN หรือ ADMIN เท่านั้น)
|
||||
// Users routes
|
||||
// - GET /me to get current user info and roles
|
||||
// - GET /api/users to list users (for SUPER_ADMIN or ADMIN only)
|
||||
// - Requires appropriate permissions via requirePerm middleware
|
||||
// - Uses req.principal loaded by loadPrincipal middleware
|
||||
// (make sure to use loadPrincipalMw() in app.js or the parent router)
|
||||
// (e.g. app.use('/api', requireAuth(), enrichPermissions(), loadPrincipalMw(), apiRouter);)
|
||||
// - req.principal has { userId, roleIds, roleCodes, permissions }
|
||||
// (see utils/rbac.js for details)
|
||||
// - Uses Sequelize ORM for DB access
|
||||
|
||||
import { Router } from "express";
|
||||
import sql from "../db/index.js";
|
||||
import { requirePerm } from "../middleware/requirePerm.js";
|
||||
import PERM from "../config/permissions.js";
|
||||
|
||||
const r = Router();
|
||||
|
||||
// ME
|
||||
r.get('/me', async (req, res) => {
|
||||
const [[u]] = await sql.query('SELECT user_id, username, email, first_name, last_name FROM users WHERE user_id=?',
|
||||
[req.principal.userId]);
|
||||
if (!u) return res.status(404).json({ error: 'User not found' });
|
||||
r.get("/me", async (req, res) => {
|
||||
const [[u]] = await sql.query(
|
||||
"SELECT user_id, username, email, first_name, last_name FROM users WHERE user_id=?",
|
||||
[req.principal.userId]
|
||||
);
|
||||
if (!u) return res.status(404).json({ error: "User not found" });
|
||||
|
||||
// roles in plain
|
||||
const [roles] = await sql.query(`
|
||||
const [roles] = await sql.query(
|
||||
`
|
||||
SELECT r.role_code, r.role_name, ur.org_id, ur.project_id
|
||||
FROM user_roles ur JOIN roles r ON r.role_id = ur.role_id
|
||||
WHERE ur.user_id=?`, [req.principal.userId]);
|
||||
WHERE ur.user_id=?`,
|
||||
[req.principal.userId]
|
||||
);
|
||||
|
||||
res.json({ ...u, roles, role_codes: [...req.principal.roleCodes] });
|
||||
});
|
||||
|
||||
// (optional) USERS LIST – ให้เฉพาะ SUPER_ADMIN หรือ ADMIN (ใน org ตัวเอง)
|
||||
r.get('/',
|
||||
requirePerm('user.read', { scope: 'global' }),
|
||||
async (req, res) => {
|
||||
const [rows] = await sql.query('SELECT user_id, username, email FROM users LIMIT 200');
|
||||
res.json(rows);
|
||||
}
|
||||
);
|
||||
r.get("/", requirePerm("user.read", { scope: "global" }), async (req, res) => {
|
||||
const [rows] = await sql.query(
|
||||
"SELECT user_id, username, email FROM users LIMIT 200"
|
||||
);
|
||||
res.json(rows);
|
||||
});
|
||||
|
||||
export default r;
|
||||
|
||||
@@ -1,28 +1,51 @@
|
||||
import { Router } from 'express';
|
||||
import { requireAuth } from '../middleware/auth.js';
|
||||
import { requireRole } from '../middleware/rbac.js';
|
||||
import { User } from '../db/sequelize.js';
|
||||
import { hashPassword } from '../utils/passwords.js';
|
||||
import { sequelize } from '../db/sequelize.js';
|
||||
import UPRModel from '../db/models/UserProjectRole.js';
|
||||
import ProjectModel from '../db/models/Project.js';
|
||||
// FILE: src/routes/users_extras.js
|
||||
// Users extra routes
|
||||
// - PATCH /users/:id/password to change user password (self or admin)
|
||||
// - GET /users/search for user search (admin only)
|
||||
// - GET /users/me/projects to list user's projects and roles
|
||||
// - Requires authentication and appropriate permissions/roles
|
||||
// via requireAuth and requireRole middleware
|
||||
// - Uses Sequelize ORM for DB access
|
||||
// - Passwords are hashed using bcrypt
|
||||
// - UserProjectRole and Project models are used for project-role listing
|
||||
// - Assumes User model is defined in Sequelize setup
|
||||
// - Assumes hashPassword utility function is defined for password hashing
|
||||
// - Assumes requireAuth and requireRole middleware are defined for auth
|
||||
// - Assumes sequelize instance is set up and connected to DB
|
||||
// - Assumes UserProjectRole and Project Sequelize models are defined
|
||||
// - Assumes User Sequelize model is defined
|
||||
// - Assumes hashPassword function is defined in utils/passwords.js
|
||||
// - Assumes requireAuth middleware is defined in middleware/auth.js
|
||||
// - Assumes requireRole middleware is defined in middleware/rbac.js
|
||||
// - Assumes sequelize instance is imported from db/sequelize.js
|
||||
// - Assumes UserProjectRole and Project models are imported from db/models/UserProjectRole.js and db/models/Project.js respectively
|
||||
|
||||
import { Router } from "express";
|
||||
import { requireAuth } from "../middleware/auth.js";
|
||||
import { requireRole } from "../middleware/rbac.js";
|
||||
import { User } from "../db/sequelize.js";
|
||||
import { hashPassword } from "../utils/passwords.js";
|
||||
import { sequelize } from "../db/sequelize.js";
|
||||
import UPRModel from "../db/models/UserProjectRole.js";
|
||||
import ProjectModel from "../db/models/Project.js";
|
||||
|
||||
const r = Router();
|
||||
const UPR = UPRModel(sequelize);
|
||||
const Project = ProjectModel(sequelize);
|
||||
|
||||
// self or admin change password
|
||||
r.patch('/users/:id/password', requireAuth, async (req, res) => {
|
||||
r.patch("/users/:id/password", requireAuth, async (req, res) => {
|
||||
const targetId = Number(req.params.id);
|
||||
const isSelf = req.user?.user_id === targetId;
|
||||
const isAdmin = (req.user?.roles || []).includes('Admin');
|
||||
if (!isSelf && !isAdmin) return res.status(403).json({ error: 'Forbidden' });
|
||||
const isAdmin = (req.user?.roles || []).includes("Admin");
|
||||
if (!isSelf && !isAdmin) return res.status(403).json({ error: "Forbidden" });
|
||||
|
||||
const { new_password } = req.body || {};
|
||||
if (!new_password) return res.status(400).json({ error: 'new_password required' });
|
||||
if (!new_password)
|
||||
return res.status(400).json({ error: "new_password required" });
|
||||
|
||||
const row = await User.findByPk(targetId);
|
||||
if (!row) return res.status(404).json({ error: 'Not found' });
|
||||
if (!row) return res.status(404).json({ error: "Not found" });
|
||||
|
||||
row.password_hash = await hashPassword(new_password);
|
||||
await row.save();
|
||||
@@ -30,25 +53,40 @@ r.patch('/users/:id/password', requireAuth, async (req, res) => {
|
||||
});
|
||||
|
||||
// user search (autocomplete)
|
||||
r.get('/users/search', requireAuth, requireRole('Admin'), async (req, res) => {
|
||||
const q = String(req.query.q || '').toLowerCase();
|
||||
const where = q ? {
|
||||
username: sequelize.where(sequelize.fn('LOWER', sequelize.col('username')), 'LIKE', `%${q}%`),
|
||||
} : {};
|
||||
const rows = await User.findAll({ where, limit: 20, order:[['username','ASC']], attributes:['user_id','username','first_name','last_name','email'] });
|
||||
r.get("/users/search", requireAuth, requireRole("Admin"), async (req, res) => {
|
||||
const q = String(req.query.q || "").toLowerCase();
|
||||
const where = q
|
||||
? {
|
||||
username: sequelize.where(
|
||||
sequelize.fn("LOWER", sequelize.col("username")),
|
||||
"LIKE",
|
||||
`%${q}%`
|
||||
),
|
||||
}
|
||||
: {};
|
||||
const rows = await User.findAll({
|
||||
where,
|
||||
limit: 20,
|
||||
order: [["username", "ASC"]],
|
||||
attributes: ["user_id", "username", "first_name", "last_name", "email"],
|
||||
});
|
||||
res.json(rows);
|
||||
});
|
||||
|
||||
// my projects/roles
|
||||
r.get('/users/me/projects', requireAuth, async (req, res) => {
|
||||
r.get("/users/me/projects", requireAuth, async (req, res) => {
|
||||
const user_id = req.user?.user_id;
|
||||
if (!user_id) return res.status(401).json({ error: 'Unauthorized' });
|
||||
if (!user_id) return res.status(401).json({ error: "Unauthorized" });
|
||||
const rows = await UPR.findAll({ where: { user_id } });
|
||||
// Optionally join project names
|
||||
const projectIds = [...new Set(rows.map(r => r.project_id))];
|
||||
const projectIds = [...new Set(rows.map((r) => r.project_id))];
|
||||
const projects = await Project.findAll({ where: { project_id: projectIds } });
|
||||
const map = new Map(projects.map(p => [p.project_id, p.project_name]));
|
||||
const result = rows.map(r => ({ project_id: r.project_id, role_name: r.role_name, project_name: map.get(r.project_id) || null }));
|
||||
const map = new Map(projects.map((p) => [p.project_id, p.project_name]));
|
||||
const result = rows.map((r) => ({
|
||||
project_id: r.project_id,
|
||||
role_name: r.role_name,
|
||||
project_name: map.get(r.project_id) || null,
|
||||
}));
|
||||
res.json(result);
|
||||
});
|
||||
|
||||
|
||||
@@ -1,86 +1,160 @@
|
||||
// src/routes/view.js
|
||||
import { Router } from 'express';
|
||||
import sql from '../db/index.js';
|
||||
import { requirePerm } from '../middleware/requirePerm.js';
|
||||
import { buildScopeWhere, ownerResolvers } from '../utils/scope.js';
|
||||
import PERM from '../config/permissions.js';
|
||||
// FILE: src/routes/view.js
|
||||
// Saved Views routes
|
||||
// - CRUD operations for saved views
|
||||
// - Requires appropriate permissions via requirePerm middleware
|
||||
// - Supports filtering and pagination on list endpoint
|
||||
// - Uses ownerResolvers utility to determine org ownership for permission checks
|
||||
// - Permissions required are defined in config/permissions.js
|
||||
// - savedview.read
|
||||
// - savedview.create
|
||||
// - savedview.update
|
||||
// - savedview.delete
|
||||
// - Scope can be 'global' (list), 'org' (get/create/update/delete)
|
||||
// - List endpoint supports filtering by project_id, org_id, shared flag, and search query (q)
|
||||
// - Pagination via limit and offset query parameters
|
||||
// - Results ordered by id DESC
|
||||
// - Error handling for not found and no fields to update scenarios
|
||||
// - Uses async/await for asynchronous operations
|
||||
// - SQL queries use parameterized queries to prevent SQL injection
|
||||
// - Responses are in JSON format
|
||||
// - Middleware functions are used for permission checks
|
||||
// - Owner resolvers are used to fetch org_id for specific view ids
|
||||
// - Code is modular and organized for maintainability
|
||||
// - Comments are provided for clarity/documentation
|
||||
// - Follows best practices for Express.js route handling
|
||||
// - Uses ES6+ features for cleaner code
|
||||
// - Assumes existence of saved_views table with appropriate columns
|
||||
// - Assumes existence of users table for owner
|
||||
// - Assumes existence of config/permissions.js with defined permission codes
|
||||
// - Assumes existence of utils/scope.js with buildScopeWhere and ownerResolvers functions
|
||||
// - Assumes existence of middleware/requirePerm.js for permission checks
|
||||
// - Assumes existence of db/index.js for database connection/querying
|
||||
// - Assumes Express.js app is set up to use this router for /api/saved_views path
|
||||
|
||||
import { Router } from "express";
|
||||
import sql from "../db/index.js";
|
||||
import { requirePerm } from "../middleware/requirePerm.js";
|
||||
import { buildScopeWhere, ownerResolvers } from "../utils/scope.js";
|
||||
import PERM from "../config/permissions.js";
|
||||
|
||||
const r = Router();
|
||||
const OWN = ownerResolvers(sql, 'saved_views', 'id');
|
||||
const OWN = ownerResolvers(sql, "saved_views", "id");
|
||||
|
||||
// LIST: GET /api/view?project_id=&org_id=&shared=1
|
||||
r.get('/',
|
||||
requirePerm(PERM.savedview.read, { scope: 'global' }),
|
||||
r.get(
|
||||
"/",
|
||||
requirePerm(PERM.savedview.read, { scope: "global" }),
|
||||
async (req, res) => {
|
||||
const { project_id, org_id, shared, q, limit = 50, offset = 0 } = req.query;
|
||||
|
||||
const base = buildScopeWhere(req.principal, {
|
||||
tableAlias: 'v',
|
||||
orgColumn: 'v.org_id',
|
||||
projectColumn: 'v.project_id',
|
||||
tableAlias: "v",
|
||||
orgColumn: "v.org_id",
|
||||
projectColumn: "v.project_id",
|
||||
permCode: PERM.savedview.read,
|
||||
preferProject: true,
|
||||
});
|
||||
|
||||
const extra = [];
|
||||
const params = { ...base.params, limit: Number(limit), offset: Number(offset), my: req.principal.userId };
|
||||
if (project_id) { extra.push('v.project_id = :project_id'); params.project_id = Number(project_id); }
|
||||
if (org_id) { extra.push('v.org_id = :org_id'); params.org_id = Number(org_id); }
|
||||
if (shared === '1') extra.push('v.is_shared = 1');
|
||||
if (q) { extra.push('(v.name LIKE :q)'); params.q = `%${q}%`; }
|
||||
const params = {
|
||||
...base.params,
|
||||
limit: Number(limit),
|
||||
offset: Number(offset),
|
||||
my: req.principal.userId,
|
||||
};
|
||||
if (project_id) {
|
||||
extra.push("v.project_id = :project_id");
|
||||
params.project_id = Number(project_id);
|
||||
}
|
||||
if (org_id) {
|
||||
extra.push("v.org_id = :org_id");
|
||||
params.org_id = Number(org_id);
|
||||
}
|
||||
if (shared === "1") extra.push("v.is_shared = 1");
|
||||
if (q) {
|
||||
extra.push("(v.name LIKE :q)");
|
||||
params.q = `%${q}%`;
|
||||
}
|
||||
|
||||
// ให้ผู้ใช้เห็นของตัวเองเสมอ + ของที่อยู่ใน scope
|
||||
const where = `(${base.where}) AND (v.is_shared=1 OR v.owner_user_id=:my${extra.length ? ' OR ' + extra.join(' AND ') : ''})`;
|
||||
const where = `(${base.where}) AND (v.is_shared=1 OR v.owner_user_id=:my${
|
||||
extra.length ? " OR " + extra.join(" AND ") : ""
|
||||
})`;
|
||||
|
||||
const [rows] = await sql.query(
|
||||
`SELECT v.* FROM saved_views v
|
||||
WHERE ${where}
|
||||
ORDER BY v.id DESC
|
||||
LIMIT :limit OFFSET :offset`, params
|
||||
LIMIT :limit OFFSET :offset`,
|
||||
params
|
||||
);
|
||||
res.json(rows);
|
||||
}
|
||||
);
|
||||
|
||||
// GET by id
|
||||
r.get('/:id',
|
||||
requirePerm(PERM.savedview.read, { scope: 'org', getOrgId: OWN.getOrgIdById }),
|
||||
r.get(
|
||||
"/:id",
|
||||
requirePerm(PERM.savedview.read, {
|
||||
scope: "org",
|
||||
getOrgId: OWN.getOrgIdById,
|
||||
}),
|
||||
async (req, res) => {
|
||||
const id = Number(req.params.id);
|
||||
const [[row]] = await sql.query('SELECT * FROM saved_views WHERE id=?', [id]);
|
||||
if (!row) return res.status(404).json({ error: 'Not found' });
|
||||
const [[row]] = await sql.query("SELECT * FROM saved_views WHERE id=?", [
|
||||
id,
|
||||
]);
|
||||
if (!row) return res.status(404).json({ error: "Not found" });
|
||||
res.json(row);
|
||||
}
|
||||
);
|
||||
|
||||
// CREATE
|
||||
r.post('/',
|
||||
requirePerm(PERM.savedview.create, { scope: 'org', getOrgId: async req => req.body?.org_id ?? null }),
|
||||
r.post(
|
||||
"/",
|
||||
requirePerm(PERM.savedview.create, {
|
||||
scope: "org",
|
||||
getOrgId: async (req) => req.body?.org_id ?? null,
|
||||
}),
|
||||
async (req, res) => {
|
||||
const { org_id, project_id, name, payload_json, is_shared = 0 } = req.body;
|
||||
const [rs] = await sql.query(
|
||||
`INSERT INTO saved_views (org_id, project_id, name, payload_json, is_shared, owner_user_id)
|
||||
VALUES (?,?,?,?,?,?)`,
|
||||
[org_id, project_id, name, JSON.stringify(payload_json ?? {}), Number(is_shared) ? 1 : 0, req.principal.userId]
|
||||
[
|
||||
org_id,
|
||||
project_id,
|
||||
name,
|
||||
JSON.stringify(payload_json ?? {}),
|
||||
Number(is_shared) ? 1 : 0,
|
||||
req.principal.userId,
|
||||
]
|
||||
);
|
||||
res.json({ id: rs.insertId });
|
||||
}
|
||||
);
|
||||
|
||||
// UPDATE (เฉพาะใน scope และถ้าเป็นของตนเอง หรือเป็นแอดมินตามนโยบาย)
|
||||
r.put('/:id',
|
||||
requirePerm(PERM.savedview.update, { scope: 'org', getOrgId: OWN.getOrgIdById }),
|
||||
r.put(
|
||||
"/:id",
|
||||
requirePerm(PERM.savedview.update, {
|
||||
scope: "org",
|
||||
getOrgId: OWN.getOrgIdById,
|
||||
}),
|
||||
async (req, res) => {
|
||||
const id = Number(req.params.id);
|
||||
const { name, payload_json, is_shared } = req.body;
|
||||
|
||||
// ตรวจ owner ถ้าต้องการบังคับเป็นของตัวเอง (option)
|
||||
const [[sv]] = await sql.query('SELECT owner_user_id FROM saved_views WHERE id=?', [id]);
|
||||
if (!sv) return res.status(404).json({ error: 'Not found' });
|
||||
const [[sv]] = await sql.query(
|
||||
"SELECT owner_user_id FROM saved_views WHERE id=?",
|
||||
[id]
|
||||
);
|
||||
if (!sv) return res.status(404).json({ error: "Not found" });
|
||||
// ถ้าจะจำกัดเฉพาะเจ้าของ: if (sv.owner_user_id !== req.principal.userId && !req.principal.isSuperAdmin) return res.status(403).json({ error: 'NOT_OWNER' });
|
||||
|
||||
await sql.query(
|
||||
'UPDATE saved_views SET name=?, payload_json=?, is_shared=? WHERE id=?',
|
||||
"UPDATE saved_views SET name=?, payload_json=?, is_shared=? WHERE id=?",
|
||||
[name, JSON.stringify(payload_json ?? {}), Number(is_shared) ? 1 : 0, id]
|
||||
);
|
||||
res.json({ ok: 1 });
|
||||
@@ -88,11 +162,15 @@ r.put('/:id',
|
||||
);
|
||||
|
||||
// DELETE
|
||||
r.delete('/:id',
|
||||
requirePerm(PERM.savedview.delete, { scope: 'org', getOrgId: OWN.getOrgIdById }),
|
||||
r.delete(
|
||||
"/:id",
|
||||
requirePerm(PERM.savedview.delete, {
|
||||
scope: "org",
|
||||
getOrgId: OWN.getOrgIdById,
|
||||
}),
|
||||
async (req, res) => {
|
||||
const id = Number(req.params.id);
|
||||
await sql.query('DELETE FROM saved_views WHERE id=?', [id]);
|
||||
await sql.query("DELETE FROM saved_views WHERE id=?", [id]);
|
||||
res.json({ ok: 1 });
|
||||
}
|
||||
);
|
||||
|
||||
@@ -1,28 +1,36 @@
|
||||
// src/routes/views.js (ESM)
|
||||
import { Router } from 'express';
|
||||
import sql from '../db/index.js';
|
||||
import { requirePerm } from '../middleware/requirePerm.js';
|
||||
import PERM from '../config/permissions.js';
|
||||
// FILE: src/routes/views.js
|
||||
// Views routes
|
||||
// - GET /api/views to list all views
|
||||
// - GET /api/views/:view_name to get view definition
|
||||
// - Requires appropriate permissions via requirePerm middleware
|
||||
|
||||
import { Router } from "express";
|
||||
import sql from "../db/index.js";
|
||||
import { requirePerm } from "../middleware/requirePerm.js";
|
||||
import PERM from "../config/permissions.js";
|
||||
|
||||
const r = Router();
|
||||
const DB_NAME = process.env.DB_NAME || 'dms_db';
|
||||
const DB_NAME = process.env.DB_NAME || "dms_db";
|
||||
|
||||
// LIST views
|
||||
r.get('/',
|
||||
requirePerm(PERM.viewdef.read, { scope: 'global' }),
|
||||
r.get(
|
||||
"/",
|
||||
requirePerm(PERM.viewdef.read, { scope: "global" }),
|
||||
async (req, res) => {
|
||||
const [rows] = await sql.query(
|
||||
`SELECT TABLE_SCHEMA AS db, TABLE_NAME AS view_name
|
||||
FROM information_schema.VIEWS
|
||||
WHERE TABLE_SCHEMA = ? ORDER BY TABLE_NAME`, [DB_NAME]
|
||||
WHERE TABLE_SCHEMA = ? ORDER BY TABLE_NAME`,
|
||||
[DB_NAME]
|
||||
);
|
||||
res.json(rows);
|
||||
}
|
||||
);
|
||||
|
||||
// GET view definition
|
||||
r.get('/:view_name',
|
||||
requirePerm(PERM.viewdef.read, { scope: 'global' }),
|
||||
r.get(
|
||||
"/:view_name",
|
||||
requirePerm(PERM.viewdef.read, { scope: "global" }),
|
||||
async (req, res) => {
|
||||
const viewName = req.params.view_name;
|
||||
const [[row]] = await sql.query(
|
||||
@@ -31,7 +39,7 @@ r.get('/:view_name',
|
||||
WHERE TABLE_SCHEMA=? AND TABLE_NAME=?`,
|
||||
[DB_NAME, viewName]
|
||||
);
|
||||
if (!row) return res.status(404).json({ error: 'Not found' });
|
||||
if (!row) return res.status(404).json({ error: "Not found" });
|
||||
res.json({ view: viewName, definition: row.definition });
|
||||
}
|
||||
);
|
||||
|
||||
@@ -1,50 +1,70 @@
|
||||
import { Router } from 'express';
|
||||
import sql from '../db/index.js';
|
||||
import { requirePerm } from '../middleware/requirePerm.js';
|
||||
import PERM from '../config/permissions.js';
|
||||
// FILE: src/routes/volumes.js
|
||||
// Volumes routes
|
||||
// - CRUD operations for volumes
|
||||
// - Requires appropriate permissions via requirePerm middleware
|
||||
// - Uses global scope for all permissions
|
||||
// - volume:read, volume:create, volume:update, volume:delete
|
||||
// - Volume fields: volume_id (PK), volume_code, volume_name
|
||||
// - volume_code is unique
|
||||
// - Basic validation: volume_code and volume_name required for create
|
||||
|
||||
import { Router } from "express";
|
||||
import sql from "../db/index.js";
|
||||
import { requirePerm } from "../middleware/requirePerm.js";
|
||||
import PERM from "../config/permissions.js";
|
||||
|
||||
const r = Router();
|
||||
|
||||
|
||||
r.get('/',
|
||||
requirePerm(PERM.volume.read, { scope: 'global' }),
|
||||
async (req, res) => {
|
||||
const [rows] = await sql.query('SELECT * FROM volumes ORDER BY volume_id DESC');
|
||||
res.json(rows);
|
||||
}
|
||||
// LIST: GET /api/volumes
|
||||
r.get(
|
||||
"/",
|
||||
requirePerm(PERM.volume.read, { scope: "global" }),
|
||||
async (req, res) => {
|
||||
const [rows] = await sql.query(
|
||||
"SELECT * FROM volumes ORDER BY volume_id DESC"
|
||||
);
|
||||
res.json(rows);
|
||||
}
|
||||
);
|
||||
|
||||
|
||||
r.post('/',
|
||||
requirePerm(PERM.volume.create, { scope: 'global' }),
|
||||
async (req, res) => {
|
||||
const { volume_code, volume_name } = req.body;
|
||||
const [rs] = await sql.query('INSERT INTO volumes (volume_code, volume_name) VALUES (?,?)', [volume_code, volume_name]);
|
||||
res.json({ volume_id: rs.insertId });
|
||||
}
|
||||
// CREATE
|
||||
r.post(
|
||||
"/",
|
||||
requirePerm(PERM.volume.create, { scope: "global" }),
|
||||
async (req, res) => {
|
||||
const { volume_code, volume_name } = req.body;
|
||||
const [rs] = await sql.query(
|
||||
"INSERT INTO volumes (volume_code, volume_name) VALUES (?,?)",
|
||||
[volume_code, volume_name]
|
||||
);
|
||||
res.json({ volume_id: rs.insertId });
|
||||
}
|
||||
);
|
||||
|
||||
|
||||
r.put('/:id',
|
||||
requirePerm(PERM.volume.update, { scope: 'global' }),
|
||||
async (req, res) => {
|
||||
const id = Number(req.params.id);
|
||||
const { volume_name } = req.body;
|
||||
await sql.query('UPDATE volumes SET volume_name=? WHERE volume_id=?', [volume_name, id]);
|
||||
res.json({ ok: 1 });
|
||||
}
|
||||
// UPDATE
|
||||
r.put(
|
||||
"/:id",
|
||||
requirePerm(PERM.volume.update, { scope: "global" }),
|
||||
async (req, res) => {
|
||||
const id = Number(req.params.id);
|
||||
const { volume_name } = req.body;
|
||||
await sql.query("UPDATE volumes SET volume_name=? WHERE volume_id=?", [
|
||||
volume_name,
|
||||
id,
|
||||
]);
|
||||
res.json({ ok: 1 });
|
||||
}
|
||||
);
|
||||
|
||||
|
||||
r.delete('/:id',
|
||||
requirePerm(PERM.volume.delete, { scope: 'global' }),
|
||||
async (req, res) => {
|
||||
const id = Number(req.params.id);
|
||||
await sql.query('DELETE FROM volumes WHERE volume_id=?', [id]);
|
||||
res.json({ ok: 1 });
|
||||
}
|
||||
// DELETE
|
||||
r.delete(
|
||||
"/:id",
|
||||
requirePerm(PERM.volume.delete, { scope: "global" }),
|
||||
async (req, res) => {
|
||||
const id = Number(req.params.id);
|
||||
await sql.query("DELETE FROM volumes WHERE volume_id=?", [id]);
|
||||
res.json({ ok: 1 });
|
||||
}
|
||||
);
|
||||
|
||||
|
||||
export default r;
|
||||
export default r;
|
||||
|
||||
@@ -1,4 +1,14 @@
|
||||
import bcrypt from 'bcrypt';
|
||||
// FILE: src/utils/passwords.js
|
||||
// Password hashing and verification utilities
|
||||
// - Uses bcrypt for secure password hashing
|
||||
// - Provides hashPassword(plain) and verifyPassword(plain, hash) functions
|
||||
// - hashPassword returns a promise that resolves to the hashed password
|
||||
// - verifyPassword returns a promise that resolves to true/false
|
||||
// - Uses 10 salt rounds for hashing
|
||||
// - Assumes bcrypt package is installed
|
||||
// - Suitable for user authentication systems
|
||||
// - Can be used in user registration and login flows
|
||||
import bcrypt from "bcrypt";
|
||||
export async function hashPassword(plain) {
|
||||
const saltRounds = 10;
|
||||
return bcrypt.hash(plain, saltRounds);
|
||||
|
||||
@@ -1,51 +1,69 @@
|
||||
// src/utils/rbac.js
|
||||
import sql from '../db/index.js';
|
||||
// FILE: src/utils/rbac.js
|
||||
// 03.2 2) เพิ่มตัวช่วย RBAC (ใหม่)
|
||||
// Role-Based Access Control (RBAC) utilities
|
||||
// - loadPrincipal(userId) to load user's roles, permissions, orgs, projects
|
||||
// - canPerform(principal, permCode, {scope, orgId, projectId}) to check permission
|
||||
// - Uses raw SQL queries via db/index.js
|
||||
// - Permissions can be global, org-scoped, or project-scoped
|
||||
// - Admin roles have special handling for org/project scope
|
||||
// - SUPER_ADMIN bypasses all checks
|
||||
|
||||
import sql from "../db/index.js";
|
||||
|
||||
/**
|
||||
* โหลด principal (บทบาท, องค์กร, โปรเจกต์) ให้ผู้ใช้
|
||||
*/
|
||||
export async function loadPrincipal(userId) {
|
||||
const [rolesRows] = await sql.query(/*sql*/`
|
||||
SELECT ur.user_id, r.role_code, ur.org_id, ur.project_id
|
||||
FROM user_roles ur
|
||||
JOIN roles r ON r.role_id = ur.role_id
|
||||
WHERE ur.user_id = ?
|
||||
`, [userId]);
|
||||
const [rolesRows] = await sql.query(
|
||||
/*sql*/ `
|
||||
SELECT ur.user_id, r.role_code, ur.org_id, ur.project_id
|
||||
FROM user_roles ur
|
||||
JOIN roles r ON r.role_id = ur.role_id
|
||||
WHERE ur.user_id = ?
|
||||
`,
|
||||
[userId]
|
||||
);
|
||||
|
||||
const [permRows] = await sql.query(
|
||||
/*sql*/ `
|
||||
SELECT ur.user_id, r.role_code, p.permission_code, ur.org_id, ur.project_id
|
||||
FROM user_roles ur
|
||||
JOIN roles r ON r.role_id = ur.role_id
|
||||
JOIN role_permissions rp ON rp.role_id = r.role_id
|
||||
JOIN permissions p ON p.permission_id = rp.permission_id
|
||||
WHERE ur.user_id = ?
|
||||
`,
|
||||
[userId]
|
||||
);
|
||||
|
||||
const [permRows] = await sql.query(/*sql*/`
|
||||
SELECT ur.user_id, r.role_code, p.permission_code, ur.org_id, ur.project_id
|
||||
FROM user_roles ur
|
||||
JOIN roles r ON r.role_id = ur.role_id
|
||||
JOIN role_permissions rp ON rp.role_id = r.role_id
|
||||
JOIN permissions p ON p.permission_id = rp.permission_id
|
||||
WHERE ur.user_id = ?
|
||||
`, [userId]);
|
||||
const roleCodes = new Set(rolesRows.map((r) => r.role_code));
|
||||
const isSuperAdmin = roleCodes.has("SUPER_ADMIN");
|
||||
|
||||
// set องค์กรที่ผู้ใช้อยู่ (ใช้สำหรับ ADMIN scope)
|
||||
const orgIds = new Set(
|
||||
rolesRows.filter((r) => r.org_id).map((r) => r.org_id)
|
||||
);
|
||||
const projectIds = new Set(
|
||||
rolesRows.filter((r) => r.project_id).map((r) => r.project_id)
|
||||
);
|
||||
|
||||
const roleCodes = new Set(rolesRows.map(r => r.role_code));
|
||||
const isSuperAdmin = roleCodes.has('SUPER_ADMIN');
|
||||
// map สิทธิเป็น: permCode -> { orgIds:Set, projectIds:Set }
|
||||
const perms = new Map();
|
||||
for (const r of permRows) {
|
||||
const key = r.permission_code;
|
||||
if (!perms.has(key))
|
||||
perms.set(key, { orgIds: new Set(), projectIds: new Set() });
|
||||
if (r.org_id) perms.get(key).orgIds.add(r.org_id);
|
||||
if (r.project_id) perms.get(key).projectIds.add(r.project_id);
|
||||
}
|
||||
|
||||
// set องค์กรที่ผู้ใช้อยู่ (ใช้สำหรับ ADMIN scope)
|
||||
const orgIds = new Set(rolesRows.filter(r => r.org_id).map(r => r.org_id));
|
||||
const projectIds = new Set(rolesRows.filter(r => r.project_id).map(r => r.project_id));
|
||||
|
||||
// map สิทธิเป็น: permCode -> { orgIds:Set, projectIds:Set }
|
||||
const perms = new Map();
|
||||
for (const r of permRows) {
|
||||
const key = r.permission_code;
|
||||
if (!perms.has(key)) perms.set(key, { orgIds: new Set(), projectIds: new Set() });
|
||||
if (r.org_id) perms.get(key).orgIds.add(r.org_id);
|
||||
if (r.project_id) perms.get(key).projectIds.add(r.project_id);
|
||||
}
|
||||
|
||||
return {
|
||||
return {
|
||||
userId,
|
||||
roleCodes, // Set<role_code>
|
||||
isSuperAdmin, // SUPER_ADMIN = true
|
||||
orgIds, // องค์กรของผู้ใช้ (จาก mapping)
|
||||
projectIds, // โปรเจกต์ของผู้ใช้ (จาก mapping)
|
||||
perms, // Map<permission_code, {orgIds:Set, projectIds:Set}>
|
||||
roleCodes, // Set<role_code>
|
||||
isSuperAdmin, // SUPER_ADMIN = true
|
||||
orgIds, // องค์กรของผู้ใช้ (จาก mapping)
|
||||
projectIds, // โปรเจกต์ของผู้ใช้ (จาก mapping)
|
||||
perms, // Map<permission_code, {orgIds:Set, projectIds:Set}>
|
||||
};
|
||||
}
|
||||
|
||||
@@ -55,30 +73,35 @@ return {
|
||||
* - ADMIN: ผ่านได้ "ภายใน org ของตัวเอง" เท่านั้น
|
||||
* - อื่น ๆ: ต้องถือ permission_code และเข้า scope ที่ถูกต้อง
|
||||
*/
|
||||
export function canPerform(principal, permCode, { scope = 'global', orgId = null, projectId = null } = {}) {
|
||||
if (!principal) return false;
|
||||
if (principal.isSuperAdmin) return true;
|
||||
const hasAdminRole = principal.roleCodes.has('ADMIN');
|
||||
export function canPerform(
|
||||
principal,
|
||||
permCode,
|
||||
{ scope = "global", orgId = null, projectId = null } = {}
|
||||
) {
|
||||
if (!principal) return false;
|
||||
if (principal.isSuperAdmin) return true;
|
||||
const hasAdminRole = principal.roleCodes.has("ADMIN");
|
||||
|
||||
if (scope === "global") return !!principal.perms.get(permCode);
|
||||
|
||||
if (scope === 'global') return !!principal.perms.get(permCode);
|
||||
if (scope === "org") {
|
||||
if (!orgId) return false;
|
||||
if (hasAdminRole && principal.orgIds.has(orgId))
|
||||
return !!principal.perms.get(permCode);
|
||||
const entry = principal.perms.get(permCode);
|
||||
return !!entry && (entry.orgIds.has(orgId) || entry.orgIds.size === 0);
|
||||
}
|
||||
|
||||
if (scope === "project") {
|
||||
if (!projectId) return false;
|
||||
if (hasAdminRole && principal.projectIds.has(projectId))
|
||||
return !!principal.perms.get(permCode);
|
||||
const entry = principal.perms.get(permCode);
|
||||
return (
|
||||
!!entry &&
|
||||
(entry.projectIds.has(projectId) || entry.projectIds.size === 0)
|
||||
);
|
||||
}
|
||||
|
||||
if (scope === 'org') {
|
||||
if (!orgId) return false;
|
||||
if (hasAdminRole && principal.orgIds.has(orgId)) return !!principal.perms.get(permCode);
|
||||
const entry = principal.perms.get(permCode);
|
||||
return !!entry && (entry.orgIds.has(orgId) || entry.orgIds.size === 0);
|
||||
return false;
|
||||
}
|
||||
|
||||
|
||||
if (scope === 'project') {
|
||||
if (!projectId) return false;
|
||||
if (hasAdminRole && principal.projectIds.has(projectId)) return !!principal.perms.get(permCode);
|
||||
const entry = principal.perms.get(permCode);
|
||||
return !!entry && (entry.projectIds.has(projectId) || entry.projectIds.size === 0);
|
||||
}
|
||||
|
||||
|
||||
return false;
|
||||
}
|
||||
@@ -1,3 +1,14 @@
|
||||
// FILE: src/utils/scope.js
|
||||
// 03.2 5) เพิ่ม utils/scope.js (ใหม่)
|
||||
// - ใช้ร่วมกับ requirePerm() และ loadPrincipal()
|
||||
// - สำหรับสร้าง SQL WHERE clause ในการ list entities ตาม scope ของผู้ใช้
|
||||
// Scope and permission utilities
|
||||
// - Functions to build SQL WHERE clauses based on user principal and permissions
|
||||
// - Used for filtering list queries according to user's
|
||||
// roles, permissions, and associated orgs/projects
|
||||
// - Works with rbac.js loadPrincipal() output
|
||||
// - Supports SUPER_ADMIN, ADMIN, and scoped permissions
|
||||
|
||||
/**
|
||||
* สร้าง WHERE fragment + params สำหรับ list ตาม principal
|
||||
* - SUPER_ADMIN: ไม่จำกัด
|
||||
@@ -12,17 +23,18 @@
|
||||
* permCode: permission_code ที่ใช้สำหรับ read list (เช่น 'correspondence.read')
|
||||
* preferProject: true -> บังคับต้อง match project scope ก่อน (ถ้ามี)
|
||||
*/
|
||||
export function buildScopeWhere(principal, {
|
||||
tableAlias, orgColumn, projectColumn, permCode, preferProject = false,
|
||||
}) {
|
||||
if (principal.isSuperAdmin) return { where: '1=1', params: {} };
|
||||
export function buildScopeWhere(
|
||||
principal,
|
||||
{ tableAlias, orgColumn, projectColumn, permCode, preferProject = false }
|
||||
) {
|
||||
if (principal.isSuperAdmin) return { where: "1=1", params: {} };
|
||||
|
||||
const perm = principal.perms.get(permCode);
|
||||
const orgIds = new Set(principal.orgIds);
|
||||
const projectIds = new Set(principal.projectIds);
|
||||
|
||||
// กรณี ADMIN: ให้ดูภายใน org/project ตัวเองได้ทั้งหมด (แต่ต้องถือ permCode)
|
||||
if (principal.roleCodes.has('ADMIN') && perm) {
|
||||
if (principal.roleCodes.has("ADMIN") && perm) {
|
||||
const orgList = [...orgIds];
|
||||
const prjList = [...projectIds];
|
||||
if (preferProject && prjList.length > 0) {
|
||||
@@ -38,11 +50,11 @@ export function buildScopeWhere(principal, {
|
||||
};
|
||||
}
|
||||
// ถ้าไม่มี mapping เลย ให้ไม่เห็นอะไร
|
||||
return { where: '1=0', params: {} };
|
||||
return { where: "1=0", params: {} };
|
||||
}
|
||||
|
||||
// บทบาทอื่น: อิงตาม perm scope
|
||||
if (!perm) return { where: '1=0', params: {} };
|
||||
if (!perm) return { where: "1=0", params: {} };
|
||||
|
||||
const permOrg = [...perm.orgIds];
|
||||
const permPrj = [...perm.projectIds];
|
||||
@@ -55,25 +67,31 @@ export function buildScopeWhere(principal, {
|
||||
}
|
||||
|
||||
// ถ้า perm ไม่มี scope ผูก (global grant) ให้ผ่านทั้งหมด
|
||||
return { where: '1=1', params: {} };
|
||||
return { where: "1=1", params: {} };
|
||||
}
|
||||
|
||||
/**
|
||||
* owner resolvers: อ่าน org_id/project_id จาก DB ด้วย id
|
||||
* ใช้กับ requirePerm(getOrgId/getProjectId) เสมอ
|
||||
*/
|
||||
export function ownerResolvers(sql, mainTable, idColumn = 'id') {
|
||||
export function ownerResolvers(sql, mainTable, idColumn = "id") {
|
||||
return {
|
||||
async getOrgIdById(req) {
|
||||
const id = Number(req.params.id ?? req.body?.id);
|
||||
if (!id) return null;
|
||||
const [[row]] = await sql.query(`SELECT org_id FROM ${mainTable} WHERE ${idColumn}=?`, [id]);
|
||||
const [[row]] = await sql.query(
|
||||
`SELECT org_id FROM ${mainTable} WHERE ${idColumn}=?`,
|
||||
[id]
|
||||
);
|
||||
return row?.org_id ?? null;
|
||||
},
|
||||
async getProjectIdById(req) {
|
||||
const id = Number(req.params.id ?? req.body?.id);
|
||||
if (!id) return null;
|
||||
const [[row]] = await sql.query(`SELECT project_id FROM ${mainTable} WHERE ${idColumn}=?`, [id]);
|
||||
const [[row]] = await sql.query(
|
||||
`SELECT project_id FROM ${mainTable} WHERE ${idColumn}=?`,
|
||||
[id]
|
||||
);
|
||||
return row?.project_id ?? null;
|
||||
},
|
||||
};
|
||||
|
||||
@@ -1,5 +1,14 @@
|
||||
// File: frontend/app/(auth)/login/page· typescript
|
||||
|
||||
"use client";
|
||||
|
||||
// ✅ ปรับให้ตรง backend: ใช้ Bearer token (ไม่ใช้ cookie)
|
||||
// - เรียก POST /api/auth/login → รับ { token, refresh_token, user }
|
||||
// - เก็บ token/refresh_token ใน localStorage (หรือ sessionStorage ถ้าไม่ติ๊กจำไว้)
|
||||
// - ไม่ใช้ credentials: "include" อีกต่อไป
|
||||
// - เอา RootLayout/metadata ออก เพราะไฟล์เพจเป็น client component
|
||||
// - เพิ่มการอ่าน NEXT_PUBLIC_API_BASE และ error handling ให้ตรงกับ backend
|
||||
|
||||
import { useState, useMemo } from "react";
|
||||
import { useSearchParams, useRouter } from "next/navigation";
|
||||
import {
|
||||
@@ -15,6 +24,8 @@ import { Input } from "@/components/ui/input";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Alert, AlertDescription } from "@/components/ui/alert";
|
||||
|
||||
const API_BASE = process.env.NEXT_PUBLIC_API_BASE?.replace(/\/$/, "") || "";
|
||||
|
||||
export default function LoginPage() {
|
||||
const router = useRouter();
|
||||
const searchParams = useSearchParams();
|
||||
@@ -42,25 +53,37 @@ export default function LoginPage() {
|
||||
try {
|
||||
setSubmitting(true);
|
||||
|
||||
// เรียก backend ให้ตั้ง HttpOnly cookie: access_token
|
||||
const res = await fetch(
|
||||
`${process.env.NEXT_PUBLIC_API_BASE}/api/auth/login`,
|
||||
{
|
||||
method: "POST",
|
||||
credentials: "include", // สำคัญ: รับ/ส่งคุกกี้
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({ username, password, remember }),
|
||||
cache: "no-store",
|
||||
}
|
||||
);
|
||||
const res = await fetch(`${API_BASE}/api/auth/login`, {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({ username, password }),
|
||||
cache: "no-store",
|
||||
});
|
||||
|
||||
const data = await res.json().catch(() => ({}));
|
||||
if (!res.ok) {
|
||||
const data = await res.json().catch(() => ({}));
|
||||
setErr(data?.message || "ชื่อผู้ใช้หรือรหัสผ่านไม่ถูกต้อง");
|
||||
// รองรับข้อความ error จาก backend เช่น INVALID_CREDENTIALS
|
||||
setErr(
|
||||
data?.error === "INVALID_CREDENTIALS"
|
||||
? "ชื่อผู้ใช้หรือรหัสผ่านไม่ถูกต้อง"
|
||||
: data?.error || "เข้าสู่ระบบไม่สำเร็จ"
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
// สำเร็จ → backend ตั้งคุกกี้แล้ว → redirect
|
||||
// ✅ เก็บ token ตามโหมดจำไว้/ไม่จำ
|
||||
const storage = remember ? window.localStorage : window.sessionStorage;
|
||||
storage.setItem("dms.token", data.token);
|
||||
storage.setItem("dms.refresh_token", data.refresh_token);
|
||||
storage.setItem("dms.user", JSON.stringify(data.user || {}));
|
||||
|
||||
// (ออปชัน) เผยแพร่ event ให้แท็บอื่นทราบ
|
||||
try {
|
||||
window.dispatchEvent(
|
||||
new StorageEvent("storage", { key: "dms.auth", newValue: "login" })
|
||||
);
|
||||
} catch {}
|
||||
|
||||
router.replace(nextPath);
|
||||
} catch (e) {
|
||||
setErr("เชื่อมต่อเซิร์ฟเวอร์ไม่ได้ กรุณาลองใหม่");
|
||||
@@ -70,102 +93,104 @@ export default function LoginPage() {
|
||||
}
|
||||
|
||||
return (
|
||||
<Card className="w-full max-w-md border-0 shadow-xl ring-1 ring-black/5 bg-white/90 backdrop-blur">
|
||||
<CardHeader className="space-y-1">
|
||||
<CardTitle className="text-2xl font-bold text-sky-800">
|
||||
เข้าสู่ระบบ
|
||||
</CardTitle>
|
||||
<CardDescription className="text-sky-700">
|
||||
Document Management System • LCBP3
|
||||
</CardDescription>
|
||||
</CardHeader>
|
||||
<div className="grid min-h-[calc(100vh-4rem)] place-items-center p-4">
|
||||
<Card className="w-full max-w-md border-0 shadow-xl ring-1 ring-black/5 bg-white/90 backdrop-blur">
|
||||
<CardHeader className="space-y-1">
|
||||
<CardTitle className="text-2xl font-bold text-sky-800">
|
||||
เข้าสู่ระบบ
|
||||
</CardTitle>
|
||||
<CardDescription className="text-sky-700">
|
||||
Document Management System • LCBP3
|
||||
</CardDescription>
|
||||
</CardHeader>
|
||||
|
||||
<CardContent>
|
||||
{err ? (
|
||||
<Alert className="mb-4">
|
||||
<AlertDescription>{err}</AlertDescription>
|
||||
</Alert>
|
||||
) : null}
|
||||
<CardContent>
|
||||
{err ? (
|
||||
<Alert className="mb-4">
|
||||
<AlertDescription>{err}</AlertDescription>
|
||||
</Alert>
|
||||
) : null}
|
||||
|
||||
<form onSubmit={onSubmit} className="grid gap-4">
|
||||
<div className="grid gap-2">
|
||||
<Label htmlFor="username">ชื่อผู้ใช้</Label>
|
||||
<Input
|
||||
id="username"
|
||||
autoFocus
|
||||
autoComplete="username"
|
||||
value={username}
|
||||
onChange={(e) => setUsername(e.target.value)}
|
||||
placeholder="เช่น superadmin"
|
||||
disabled={submitting}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="grid gap-2">
|
||||
<Label htmlFor="password">รหัสผ่าน</Label>
|
||||
<div className="relative">
|
||||
<form onSubmit={onSubmit} className="grid gap-4">
|
||||
<div className="grid gap-2">
|
||||
<Label htmlFor="username">ชื่อผู้ใช้</Label>
|
||||
<Input
|
||||
id="password"
|
||||
type={showPw ? "text" : "password"}
|
||||
autoComplete="current-password"
|
||||
value={password}
|
||||
onChange={(e) => setPassword(e.target.value)}
|
||||
placeholder="••••••••"
|
||||
id="username"
|
||||
autoFocus
|
||||
autoComplete="username"
|
||||
value={username}
|
||||
onChange={(e) => setUsername(e.target.value)}
|
||||
placeholder="เช่น superadmin"
|
||||
disabled={submitting}
|
||||
className="pr-10"
|
||||
/>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setShowPw((v) => !v)}
|
||||
className="absolute inset-y-0 px-2 my-auto text-xs bg-white border rounded-md right-2 hover:bg-slate-50"
|
||||
aria-label={showPw ? "ซ่อนรหัสผ่าน" : "แสดงรหัสผ่าน"}
|
||||
disabled={submitting}
|
||||
>
|
||||
{showPw ? "Hide" : "Show"}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center justify-between pt-1">
|
||||
<label className="inline-flex items-center gap-2 text-sm text-slate-600">
|
||||
<input
|
||||
type="checkbox"
|
||||
className="size-4 accent-sky-700"
|
||||
checked={remember}
|
||||
onChange={(e) => setRemember(e.target.checked)}
|
||||
disabled={submitting}
|
||||
/>
|
||||
จดจำฉันไว้ในเครื่องนี้
|
||||
</label>
|
||||
<div className="grid gap-2">
|
||||
<Label htmlFor="password">รหัสผ่าน</Label>
|
||||
<div className="relative">
|
||||
<Input
|
||||
id="password"
|
||||
type={showPw ? "text" : "password"}
|
||||
autoComplete="current-password"
|
||||
value={password}
|
||||
onChange={(e) => setPassword(e.target.value)}
|
||||
placeholder="••••••••"
|
||||
disabled={submitting}
|
||||
className="pr-10"
|
||||
/>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setShowPw((v) => !v)}
|
||||
className="absolute inset-y-0 px-2 my-auto text-xs bg-white border rounded-md right-2 hover:bg-slate-50"
|
||||
aria-label={showPw ? "ซ่อนรหัสผ่าน" : "แสดงรหัสผ่าน"}
|
||||
disabled={submitting}
|
||||
>
|
||||
{showPw ? "Hide" : "Show"}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<a
|
||||
href="/forgot"
|
||||
className="text-sm text-sky-700 hover:text-sky-900 hover:underline"
|
||||
<div className="flex items-center justify-between pt-1">
|
||||
<label className="inline-flex items-center gap-2 text-sm text-slate-600">
|
||||
<input
|
||||
type="checkbox"
|
||||
className="size-4 accent-sky-700"
|
||||
checked={remember}
|
||||
onChange={(e) => setRemember(e.target.checked)}
|
||||
disabled={submitting}
|
||||
/>
|
||||
จดจำฉันไว้ในเครื่องนี้
|
||||
</label>
|
||||
|
||||
<a
|
||||
href="/forgot-password"
|
||||
className="text-sm text-sky-700 hover:text-sky-900 hover:underline"
|
||||
>
|
||||
ลืมรหัสผ่าน?
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<Button
|
||||
type="submit"
|
||||
disabled={submitting}
|
||||
className="mt-2 bg-sky-700 hover:bg-sky-800"
|
||||
>
|
||||
ลืมรหัสผ่าน?
|
||||
</a>
|
||||
</div>
|
||||
{submitting ? (
|
||||
<span className="inline-flex items-center gap-2">
|
||||
<Spinner /> กำลังเข้าสู่ระบบ…
|
||||
</span>
|
||||
) : (
|
||||
"เข้าสู่ระบบ"
|
||||
)}
|
||||
</Button>
|
||||
</form>
|
||||
</CardContent>
|
||||
|
||||
<Button
|
||||
type="submit"
|
||||
disabled={submitting}
|
||||
className="mt-2 bg-sky-700 hover:bg-sky-800"
|
||||
>
|
||||
{submitting ? (
|
||||
<span className="inline-flex items-center gap-2">
|
||||
<Spinner /> กำลังเข้าสู่ระบบ…
|
||||
</span>
|
||||
) : (
|
||||
"เข้าสู่ระบบ"
|
||||
)}
|
||||
</Button>
|
||||
</form>
|
||||
</CardContent>
|
||||
|
||||
<CardFooter className="text-xs text-center text-slate-500">
|
||||
© {new Date().getFullYear()} np-dms.work
|
||||
</CardFooter>
|
||||
</Card>
|
||||
<CardFooter className="text-xs text-center text-slate-500">
|
||||
© {new Date().getFullYear()} np-dms.work
|
||||
</CardFooter>
|
||||
</Card>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "dms-frontend",
|
||||
"version": "0.5.0",
|
||||
"version": "0.6.0",
|
||||
"private": true,
|
||||
"engines": {
|
||||
"node": ">=20.0.0"
|
||||
|
||||
@@ -0,0 +1,31 @@
|
||||
-- 01_99_dms_data_v5_1_deploy_table_password_resets.sql
|
||||
-- Purpose: Table for password reset flow (forgot/reset password)
|
||||
-- Depends on: users
|
||||
|
||||
SET NAMES utf8mb4;
|
||||
SET time_zone = '+00:00';
|
||||
|
||||
-- ใช้ IF NOT EXISTS ป้องกันรันซ้ำ
|
||||
CREATE TABLE IF NOT EXISTS password_resets (
|
||||
id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT,
|
||||
user_id BIGINT UNSIGNED NOT NULL,
|
||||
token_hash CHAR(64) NOT NULL COMMENT 'SHA-256(hex) ของ token ดิบที่ส่งให้ผู้ใช้',
|
||||
expires_at DATETIME NOT NULL,
|
||||
used_at DATETIME NULL,
|
||||
created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
PRIMARY KEY (id),
|
||||
UNIQUE KEY uq_password_resets_token_hash (token_hash),
|
||||
KEY idx_password_resets_user_id (user_id),
|
||||
KEY idx_password_resets_expires (expires_at),
|
||||
CONSTRAINT fk_password_resets_user
|
||||
FOREIGN KEY (user_id) REFERENCES users(user_id)
|
||||
ON DELETE CASCADE ON UPDATE RESTRICT
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
|
||||
|
||||
-- (ออปชัน) กำจัด token ที่หมดอายุอัตโนมัติด้วย EVENT (หากเปิด event_scheduler)
|
||||
-- CREATE EVENT IF NOT EXISTS ev_prune_password_resets
|
||||
-- ON SCHEDULE EVERY 1 DAY
|
||||
-- DO DELETE FROM password_resets WHERE (used_at IS NOT NULL) OR (expires_at < NOW());
|
||||
|
||||
-- DOWN (rollback)
|
||||
-- DROP TABLE IF EXISTS password_resets;
|
||||
Reference in New Issue
Block a user