feat: 구매/안전 시스템 전면 개편 — tkpurchase 개편 + tksafety 신규 + 권한 보강
Phase 1: tkuser 협력업체 CRUD 이관 (읽기전용 → 전체 CRUD) Phase 2: tkpurchase 개편 — 일용공 신청/확정, 작업일정, 업무현황, 계정관리, 협력업체 포털 Phase 3: tksafety 신규 시스템 — 방문관리 + 안전교육 신고 Phase 4: SSO 인증 보강 (partner_company_id JWT, 만료일 체크), 권한 테이블 기반 접근 제어 Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
101
tksafety/api/middleware/auth.js
Normal file
101
tksafety/api/middleware/auth.js
Normal file
@@ -0,0 +1,101 @@
|
||||
const jwt = require('jsonwebtoken');
|
||||
const mysql = require('mysql2/promise');
|
||||
|
||||
const JWT_SECRET = process.env.SSO_JWT_SECRET;
|
||||
|
||||
let pool;
|
||||
function getPool() {
|
||||
if (!pool) {
|
||||
pool = mysql.createPool({
|
||||
host: process.env.DB_HOST || 'mariadb',
|
||||
port: parseInt(process.env.DB_PORT) || 3306,
|
||||
user: process.env.DB_USER || 'hyungi_user',
|
||||
password: process.env.DB_PASSWORD,
|
||||
database: process.env.DB_NAME || 'hyungi',
|
||||
waitForConnections: true,
|
||||
connectionLimit: 5,
|
||||
queueLimit: 0
|
||||
});
|
||||
}
|
||||
return pool;
|
||||
}
|
||||
|
||||
function extractToken(req) {
|
||||
const authHeader = req.headers['authorization'];
|
||||
if (authHeader && authHeader.startsWith('Bearer ')) {
|
||||
return authHeader.split(' ')[1];
|
||||
}
|
||||
if (req.cookies && req.cookies.sso_token) {
|
||||
return req.cookies.sso_token;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
function requireAuth(req, res, next) {
|
||||
const token = extractToken(req);
|
||||
if (!token) {
|
||||
return res.status(401).json({ success: false, error: '인증이 필요합니다' });
|
||||
}
|
||||
try {
|
||||
const decoded = jwt.verify(token, JWT_SECRET);
|
||||
req.user = decoded;
|
||||
next();
|
||||
} catch {
|
||||
return res.status(401).json({ success: false, error: '유효하지 않은 토큰입니다' });
|
||||
}
|
||||
}
|
||||
|
||||
function requireAdmin(req, res, next) {
|
||||
const token = extractToken(req);
|
||||
if (!token) {
|
||||
return res.status(401).json({ success: false, error: '인증이 필요합니다' });
|
||||
}
|
||||
try {
|
||||
const decoded = jwt.verify(token, JWT_SECRET);
|
||||
if (!['admin', 'system'].includes((decoded.role || '').toLowerCase())) {
|
||||
return res.status(403).json({ success: false, error: '관리자 권한이 필요합니다' });
|
||||
}
|
||||
req.user = decoded;
|
||||
next();
|
||||
} catch {
|
||||
return res.status(401).json({ success: false, error: '유효하지 않은 토큰입니다' });
|
||||
}
|
||||
}
|
||||
|
||||
function requirePage(pageName) {
|
||||
return async (req, res, next) => {
|
||||
const userId = req.user.user_id || req.user.id;
|
||||
const role = (req.user.role || '').toLowerCase();
|
||||
if (role === 'admin' || role === 'system') return next();
|
||||
|
||||
try {
|
||||
const db = getPool();
|
||||
// 1. 개인 권한
|
||||
const [rows] = await db.query(
|
||||
'SELECT can_access FROM user_page_permissions WHERE user_id = ? AND page_name = ?',
|
||||
[userId, pageName]
|
||||
);
|
||||
if (rows.length > 0) {
|
||||
return rows[0].can_access ? next() : res.status(403).json({ success: false, error: '접근 권한이 없습니다' });
|
||||
}
|
||||
// 2. 부서 권한
|
||||
const [userRows] = await db.query('SELECT department_id FROM sso_users WHERE user_id = ?', [userId]);
|
||||
if (userRows.length > 0 && userRows[0].department_id) {
|
||||
const [deptRows] = await db.query(
|
||||
'SELECT can_access FROM department_page_permissions WHERE department_id = ? AND page_name = ?',
|
||||
[userRows[0].department_id, pageName]
|
||||
);
|
||||
if (deptRows.length > 0) {
|
||||
return deptRows[0].can_access ? next() : res.status(403).json({ success: false, error: '접근 권한이 없습니다' });
|
||||
}
|
||||
}
|
||||
// 3. 기본 거부
|
||||
return res.status(403).json({ success: false, error: '접근 권한이 없습니다' });
|
||||
} catch (err) {
|
||||
console.error('Permission check error:', err);
|
||||
return res.status(500).json({ success: false, error: '권한 확인 실패' });
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
module.exports = { getPool, extractToken, requireAuth, requireAdmin, requirePage };
|
||||
Reference in New Issue
Block a user