refactor: 보안 취약점 제거 + 데드코드 정리 + 프론트엔드 중복 통합
- 인증 없는 임시 엔드포인트 삭제 (index.js, healthRoutes.js, publicPaths) - skipAuth 우회 라우트 삭제 (workAnalysis.js) - 하드코딩 유저 백도어 삭제 (routes/auth.js) - 안전체크 CRUD에 admin 권한 추가 (tbmRoutes.js) - deprecated shim 3개 삭제 + 8개 소비 파일 import 정리 (auth.js 직접 참조) - 미사용 pageAccessController, db.js, common/security.js 삭제 - escapeHtml() 5곳 로컬 중복 제거 → api-base.js 전역 사용 - userPageAccess_v2_v2 캐시 키 버그 수정 (app-init.js) - system3 .bak 파일 삭제, PROGRESS.md 업데이트 Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -9,7 +9,7 @@
|
||||
|
||||
const swaggerUi = require('swagger-ui-express');
|
||||
const swaggerSpec = require('./swagger');
|
||||
const { verifyToken } = require('../middlewares/authMiddleware');
|
||||
const { verifyToken } = require('../middlewares/auth');
|
||||
const { activityLogger } = require('../middlewares/activityLogger');
|
||||
const logger = require('../utils/logger');
|
||||
|
||||
@@ -107,10 +107,7 @@ function setupRoutes(app) {
|
||||
'/api/setup/migrate-existing-data',
|
||||
'/api/setup/check-data-status',
|
||||
'/api/monthly-status/calendar',
|
||||
'/api/monthly-status/daily-details',
|
||||
'/api/migrate-work-type-id', // 임시 마이그레이션 - 실행 후 삭제!
|
||||
'/api/diagnose-work-type-id', // 임시 진단 - 실행 후 삭제!
|
||||
'/api/test-analysis' // 임시 분석 테스트 - 실행 후 삭제!
|
||||
'/api/monthly-status/daily-details'
|
||||
];
|
||||
|
||||
// 인증 미들웨어 - 공개 경로를 제외한 모든 API (rate limiter보다 먼저 실행)
|
||||
|
||||
@@ -1,200 +0,0 @@
|
||||
// controllers/pageAccessController.js
|
||||
const PageAccessModel = require('../models/pageAccessModel');
|
||||
|
||||
const PageAccessController = {
|
||||
// 사용자의 페이지 권한 조회
|
||||
getUserPageAccess: (req, res) => {
|
||||
const userId = parseInt(req.params.userId);
|
||||
|
||||
if (isNaN(userId)) {
|
||||
return res.status(400).json({
|
||||
success: false,
|
||||
message: '유효하지 않은 사용자 ID입니다.'
|
||||
});
|
||||
}
|
||||
|
||||
PageAccessModel.getUserPageAccess(userId, (err, results) => {
|
||||
if (err) {
|
||||
console.error('페이지 권한 조회 오류:', err);
|
||||
return res.status(500).json({
|
||||
success: false,
|
||||
message: '페이지 권한 조회 중 오류가 발생했습니다.',
|
||||
error: err.message
|
||||
});
|
||||
}
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
data: results
|
||||
});
|
||||
});
|
||||
},
|
||||
|
||||
// 모든 페이지 목록 조회
|
||||
getAllPages: (req, res) => {
|
||||
PageAccessModel.getAllPages((err, results) => {
|
||||
if (err) {
|
||||
console.error('페이지 목록 조회 오류:', err);
|
||||
return res.status(500).json({
|
||||
success: false,
|
||||
message: '페이지 목록 조회 중 오류가 발생했습니다.',
|
||||
error: err.message
|
||||
});
|
||||
}
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
data: results
|
||||
});
|
||||
});
|
||||
},
|
||||
|
||||
// 페이지 권한 부여
|
||||
grantPageAccess: (req, res) => {
|
||||
const userId = parseInt(req.params.userId);
|
||||
const { pageId } = req.body;
|
||||
const grantedBy = req.user.user_id;
|
||||
|
||||
if (isNaN(userId) || !pageId) {
|
||||
return res.status(400).json({
|
||||
success: false,
|
||||
message: '필수 파라미터가 누락되었습니다.'
|
||||
});
|
||||
}
|
||||
|
||||
PageAccessModel.grantPageAccess(userId, pageId, grantedBy, (err, result) => {
|
||||
if (err) {
|
||||
console.error('페이지 권한 부여 오류:', err);
|
||||
return res.status(500).json({
|
||||
success: false,
|
||||
message: '페이지 권한 부여 중 오류가 발생했습니다.',
|
||||
error: err.message
|
||||
});
|
||||
}
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
message: '페이지 권한이 부여되었습니다.',
|
||||
data: result
|
||||
});
|
||||
});
|
||||
},
|
||||
|
||||
// 페이지 권한 회수
|
||||
revokePageAccess: (req, res) => {
|
||||
const userId = parseInt(req.params.userId);
|
||||
const pageId = parseInt(req.params.pageId);
|
||||
|
||||
if (isNaN(userId) || isNaN(pageId)) {
|
||||
return res.status(400).json({
|
||||
success: false,
|
||||
message: '유효하지 않은 파라미터입니다.'
|
||||
});
|
||||
}
|
||||
|
||||
PageAccessModel.revokePageAccess(userId, pageId, (err, result) => {
|
||||
if (err) {
|
||||
console.error('페이지 권한 회수 오류:', err);
|
||||
return res.status(500).json({
|
||||
success: false,
|
||||
message: '페이지 권한 회수 중 오류가 발생했습니다.',
|
||||
error: err.message
|
||||
});
|
||||
}
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
message: '페이지 권한이 회수되었습니다.',
|
||||
data: result
|
||||
});
|
||||
});
|
||||
},
|
||||
|
||||
// 사용자 페이지 권한 일괄 설정
|
||||
setUserPageAccess: (req, res) => {
|
||||
const userId = parseInt(req.params.userId);
|
||||
const { pageIds } = req.body;
|
||||
const grantedBy = req.user.user_id;
|
||||
|
||||
if (isNaN(userId)) {
|
||||
return res.status(400).json({
|
||||
success: false,
|
||||
message: '유효하지 않은 사용자 ID입니다.'
|
||||
});
|
||||
}
|
||||
|
||||
if (!Array.isArray(pageIds)) {
|
||||
return res.status(400).json({
|
||||
success: false,
|
||||
message: 'pageIds는 배열이어야 합니다.'
|
||||
});
|
||||
}
|
||||
|
||||
PageAccessModel.setUserPageAccess(userId, pageIds, grantedBy, (err, result) => {
|
||||
if (err) {
|
||||
console.error('페이지 권한 설정 오류:', err);
|
||||
return res.status(500).json({
|
||||
success: false,
|
||||
message: '페이지 권한 설정 중 오류가 발생했습니다.',
|
||||
error: err.message
|
||||
});
|
||||
}
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
message: '페이지 권한이 설정되었습니다.',
|
||||
data: result
|
||||
});
|
||||
});
|
||||
},
|
||||
|
||||
// 특정 페이지 접근 권한 확인
|
||||
checkPageAccess: (req, res) => {
|
||||
const userId = req.user.user_id;
|
||||
const { pageKey } = req.params;
|
||||
|
||||
if (!pageKey) {
|
||||
return res.status(400).json({
|
||||
success: false,
|
||||
message: '페이지 키가 필요합니다.'
|
||||
});
|
||||
}
|
||||
|
||||
PageAccessModel.checkPageAccess(userId, pageKey, (err, result) => {
|
||||
if (err) {
|
||||
console.error('페이지 접근 권한 확인 오류:', err);
|
||||
return res.status(500).json({
|
||||
success: false,
|
||||
message: '페이지 접근 권한 확인 중 오류가 발생했습니다.',
|
||||
error: err.message
|
||||
});
|
||||
}
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
data: result
|
||||
});
|
||||
});
|
||||
},
|
||||
|
||||
// 계정이 있는 사용자 목록 조회 (권한 관리용)
|
||||
getUsersWithAccounts: (req, res) => {
|
||||
PageAccessModel.getUsersWithAccounts((err, results) => {
|
||||
if (err) {
|
||||
console.error('사용자 목록 조회 오류:', err);
|
||||
return res.status(500).json({
|
||||
success: false,
|
||||
message: '사용자 목록 조회 중 오류가 발생했습니다.',
|
||||
error: err.message
|
||||
});
|
||||
}
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
data: results
|
||||
});
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
module.exports = PageAccessController;
|
||||
@@ -1,35 +0,0 @@
|
||||
require('dotenv').config();
|
||||
const mysql = require('mysql2/promise');
|
||||
const retry = require('async-retry');
|
||||
|
||||
// 초기화된 pool을 export 하기 위한 변수
|
||||
let pool = null;
|
||||
|
||||
const initPool = async () => {
|
||||
if (pool) return pool; // 이미 초기화된 경우 재사용
|
||||
|
||||
await retry(async () => {
|
||||
pool = mysql.createPool({
|
||||
host: process.env.DB_HOST,
|
||||
port: process.env.DB_PORT || 3306,
|
||||
user: process.env.DB_USER,
|
||||
password: process.env.DB_PASS,
|
||||
database: process.env.DB_NAME,
|
||||
waitForConnections: true,
|
||||
connectionLimit: 10,
|
||||
queueLimit: 0
|
||||
});
|
||||
|
||||
const conn = await pool.getConnection();
|
||||
await conn.query('SET FOREIGN_KEY_CHECKS = 1');
|
||||
console.log(`✅ MariaDB 연결 성공: ${process.env.DB_HOST}:${process.env.DB_PORT || 3306}/${process.env.DB_NAME}`);
|
||||
conn.release();
|
||||
}, {
|
||||
retries: 10,
|
||||
minTimeout: 3000
|
||||
});
|
||||
|
||||
return pool;
|
||||
};
|
||||
|
||||
module.exports = initPool;
|
||||
@@ -22,222 +22,6 @@ const PORT = process.env.PORT || 20005;
|
||||
// Trust proxy for accurate IP addresses
|
||||
app.set('trust proxy', 1);
|
||||
|
||||
// JSON body parser 미리 적용 (마이그레이션용)
|
||||
app.use(express.json());
|
||||
|
||||
// 임시 분석 테스트 엔드포인트 - 실행 후 삭제!
|
||||
app.get('/api/test-analysis', async (req, res) => {
|
||||
try {
|
||||
const { getDb } = require('./dbPool');
|
||||
const db = await getDb();
|
||||
|
||||
// 수정된 COALESCE 로직 테스트 (tasks 우선)
|
||||
const [results] = await db.query(`
|
||||
SELECT
|
||||
dwr.id,
|
||||
w.worker_name,
|
||||
dwr.report_date,
|
||||
dwr.work_type_id as original_work_type_id,
|
||||
COALESCE(
|
||||
CASE WHEN t.task_id IS NOT NULL THEN t.work_type_id ELSE NULL END,
|
||||
wt.id
|
||||
) as resolved_work_type_id,
|
||||
COALESCE(
|
||||
CASE WHEN t.task_id IS NOT NULL THEN wt2.name ELSE NULL END,
|
||||
wt.name
|
||||
) as work_type_name,
|
||||
t.task_name,
|
||||
wt.name as direct_match_work_type,
|
||||
wt2.name as task_work_type
|
||||
FROM daily_work_reports dwr
|
||||
LEFT JOIN workers w ON dwr.worker_id = w.worker_id
|
||||
LEFT JOIN work_types wt ON dwr.work_type_id = wt.id
|
||||
LEFT JOIN tasks t ON dwr.work_type_id = t.task_id
|
||||
LEFT JOIN work_types wt2 ON t.work_type_id = wt2.id
|
||||
WHERE w.worker_name LIKE '%조승민%' OR w.worker_name LIKE '%최광욱%'
|
||||
ORDER BY dwr.report_date DESC
|
||||
LIMIT 20
|
||||
`);
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
message: 'tasks 테이블 우선 조회 결과',
|
||||
data: results.map(r => ({
|
||||
id: r.id,
|
||||
worker: r.worker_name,
|
||||
date: r.report_date,
|
||||
original_id: r.original_work_type_id,
|
||||
resolved_work_type: r.work_type_name,
|
||||
task: r.task_name,
|
||||
note: `원래 ID ${r.original_work_type_id} → ${r.work_type_name}`
|
||||
}))
|
||||
});
|
||||
|
||||
} catch (error) {
|
||||
console.error('테스트 실패:', error);
|
||||
res.status(500).json({ success: false, error: error.message });
|
||||
}
|
||||
});
|
||||
|
||||
// 임시 진단 엔드포인트 - 실행 후 삭제!
|
||||
app.get('/api/diagnose-work-type-id', async (req, res) => {
|
||||
try {
|
||||
const { getDb } = require('./dbPool');
|
||||
const db = await getDb();
|
||||
|
||||
// 1. 전체 작업보고서 현황
|
||||
const [totalStats] = await db.query(`
|
||||
SELECT
|
||||
COUNT(*) as total_reports,
|
||||
COUNT(tbm_assignment_id) as tbm_reports,
|
||||
COUNT(CASE WHEN tbm_assignment_id IS NULL THEN 1 END) as non_tbm_reports
|
||||
FROM daily_work_reports
|
||||
`);
|
||||
|
||||
// 2. work_type_id 값 분포 (상위 20개)
|
||||
const [workTypeDistribution] = await db.query(`
|
||||
SELECT
|
||||
dwr.work_type_id,
|
||||
COUNT(*) as count,
|
||||
wt.name as if_work_type,
|
||||
t.task_name as if_task,
|
||||
wt2.name as task_work_type
|
||||
FROM daily_work_reports dwr
|
||||
LEFT JOIN work_types wt ON dwr.work_type_id = wt.id
|
||||
LEFT JOIN tasks t ON dwr.work_type_id = t.task_id
|
||||
LEFT JOIN work_types wt2 ON t.work_type_id = wt2.id
|
||||
GROUP BY dwr.work_type_id
|
||||
ORDER BY count DESC
|
||||
LIMIT 20
|
||||
`);
|
||||
|
||||
// 3. 특정 작업자 데이터 확인 (조승민, 최광욱)
|
||||
const [workerSamples] = await db.query(`
|
||||
SELECT
|
||||
dwr.id,
|
||||
w.worker_name,
|
||||
dwr.work_type_id,
|
||||
dwr.tbm_assignment_id,
|
||||
wt.name as direct_work_type,
|
||||
t.task_name,
|
||||
wt2.name as task_work_type,
|
||||
dwr.report_date
|
||||
FROM daily_work_reports dwr
|
||||
LEFT JOIN workers w ON dwr.worker_id = w.worker_id
|
||||
LEFT JOIN work_types wt ON dwr.work_type_id = wt.id
|
||||
LEFT JOIN tasks t ON dwr.work_type_id = t.task_id
|
||||
LEFT JOIN work_types wt2 ON t.work_type_id = wt2.id
|
||||
WHERE w.worker_name LIKE '%조승민%' OR w.worker_name LIKE '%최광욱%'
|
||||
ORDER BY dwr.report_date DESC
|
||||
LIMIT 20
|
||||
`);
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
data: {
|
||||
total_stats: totalStats[0],
|
||||
work_type_distribution: workTypeDistribution,
|
||||
worker_samples: workerSamples
|
||||
}
|
||||
});
|
||||
|
||||
} catch (error) {
|
||||
console.error('진단 실패:', error);
|
||||
res.status(500).json({ success: false, error: error.message });
|
||||
}
|
||||
});
|
||||
|
||||
// 임시 마이그레이션 엔드포인트 (인증 없이 실행) - 실행 후 삭제!
|
||||
app.post('/api/migrate-work-type-id', async (req, res) => {
|
||||
try {
|
||||
const { getDb } = require('./dbPool');
|
||||
const db = await getDb();
|
||||
|
||||
console.log('🔄 TBM 기반 작업보고서 work_type_id 수정 시작...');
|
||||
|
||||
// 1. 수정 대상 확인
|
||||
const [checkResult] = await db.query(`
|
||||
SELECT
|
||||
dwr.id,
|
||||
dwr.work_type_id as current_work_type_id,
|
||||
ta.task_id as correct_task_id,
|
||||
w.worker_name,
|
||||
dwr.report_date
|
||||
FROM daily_work_reports dwr
|
||||
INNER JOIN tbm_team_assignments ta ON dwr.tbm_assignment_id = ta.assignment_id
|
||||
INNER JOIN workers w ON dwr.worker_id = w.worker_id
|
||||
WHERE dwr.tbm_assignment_id IS NOT NULL
|
||||
AND ta.task_id IS NOT NULL
|
||||
AND dwr.work_type_id != ta.task_id
|
||||
ORDER BY dwr.report_date DESC
|
||||
`);
|
||||
|
||||
console.log('📊 수정 대상:', checkResult.length, '개 레코드');
|
||||
|
||||
if (checkResult.length === 0) {
|
||||
return res.json({
|
||||
success: true,
|
||||
message: '수정할 데이터가 없습니다.',
|
||||
data: { affected_rows: 0 }
|
||||
});
|
||||
}
|
||||
|
||||
// 2. 업데이트 실행
|
||||
const [updateResult] = await db.query(`
|
||||
UPDATE daily_work_reports dwr
|
||||
INNER JOIN tbm_team_assignments ta ON dwr.tbm_assignment_id = ta.assignment_id
|
||||
SET dwr.work_type_id = ta.task_id
|
||||
WHERE dwr.tbm_assignment_id IS NOT NULL
|
||||
AND ta.task_id IS NOT NULL
|
||||
AND dwr.work_type_id != ta.task_id
|
||||
`);
|
||||
|
||||
console.log('✅ 업데이트 완료:', updateResult.affectedRows, '개 레코드 수정됨');
|
||||
|
||||
// 3. 수정 후 확인
|
||||
const [samples] = await db.query(`
|
||||
SELECT
|
||||
dwr.id,
|
||||
dwr.work_type_id,
|
||||
t.task_name,
|
||||
wt.name as work_type_name,
|
||||
w.worker_name,
|
||||
dwr.report_date
|
||||
FROM daily_work_reports dwr
|
||||
INNER JOIN tbm_team_assignments ta ON dwr.tbm_assignment_id = ta.assignment_id
|
||||
LEFT JOIN tasks t ON dwr.work_type_id = t.task_id
|
||||
LEFT JOIN work_types wt ON t.work_type_id = wt.id
|
||||
LEFT JOIN workers w ON dwr.worker_id = w.worker_id
|
||||
WHERE dwr.tbm_assignment_id IS NOT NULL
|
||||
ORDER BY dwr.report_date DESC
|
||||
LIMIT 10
|
||||
`);
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
message: updateResult.affectedRows + '개 레코드가 수정되었습니다.',
|
||||
data: {
|
||||
affected_rows: updateResult.affectedRows,
|
||||
before_count: checkResult.length,
|
||||
samples: samples.map(s => ({
|
||||
id: s.id,
|
||||
worker: s.worker_name,
|
||||
date: s.report_date,
|
||||
task: s.task_name,
|
||||
work_type: s.work_type_name
|
||||
}))
|
||||
}
|
||||
});
|
||||
|
||||
} catch (error) {
|
||||
console.error('마이그레이션 실패:', error);
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
error: '마이그레이션 실패: ' + error.message
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
// 미들웨어 설정
|
||||
setupMiddlewares(app);
|
||||
|
||||
|
||||
@@ -1,9 +0,0 @@
|
||||
// utils/access.js
|
||||
exports.requireAccess = (...allowed) => {
|
||||
return (req, res, next) => {
|
||||
if (!req.user || !allowed.includes(req.user.access_level)) {
|
||||
return res.status(403).json({ error: '접근 권한이 없습니다' });
|
||||
}
|
||||
next();
|
||||
};
|
||||
};
|
||||
@@ -1,29 +0,0 @@
|
||||
/**
|
||||
* @deprecated 이 파일은 하위 호환성을 위해 유지됩니다.
|
||||
* 새로운 코드에서는 '../middlewares/auth'의 requireMinLevel을 사용하세요.
|
||||
*
|
||||
* @example
|
||||
* // 이전 방식 (deprecated)
|
||||
* const { requireAccess, ACCESS_LEVELS } = require('../middlewares/accessMiddleware');
|
||||
* router.get('/admin', requireAccess('admin'), handler);
|
||||
*
|
||||
* // 새로운 방식 (권장)
|
||||
* const { requireMinLevel, ACCESS_LEVELS } = require('../middlewares/auth');
|
||||
* router.get('/admin', requireAuth, requireMinLevel('admin'), handler);
|
||||
*/
|
||||
|
||||
const { requireMinLevel, ACCESS_LEVELS } = require('./auth');
|
||||
|
||||
/**
|
||||
* @deprecated requireMinLevel을 사용하세요
|
||||
*/
|
||||
const requireAccess = (requiredLevel) => {
|
||||
return requireMinLevel(requiredLevel);
|
||||
};
|
||||
|
||||
module.exports = {
|
||||
requireAccess,
|
||||
ACCESS_LEVELS,
|
||||
// 새로운 API
|
||||
requireMinLevel
|
||||
};
|
||||
@@ -1,36 +0,0 @@
|
||||
/**
|
||||
* @deprecated 이 파일은 하위 호환성을 위해 유지됩니다.
|
||||
* 새로운 코드에서는 './auth'를 직접 import하세요.
|
||||
*
|
||||
* @example
|
||||
* // 이전 방식 (deprecated)
|
||||
* const { verifyToken, requireAdmin } = require('../middlewares/authMiddleware');
|
||||
*
|
||||
* // 새로운 방식 (권장)
|
||||
* const { requireAuth, requireRole } = require('../middlewares/auth');
|
||||
*/
|
||||
|
||||
const {
|
||||
requireAuth,
|
||||
requireRole,
|
||||
requireMinLevel,
|
||||
requireOwnerOrAdmin,
|
||||
verifyToken,
|
||||
requireAdmin,
|
||||
requireSystem,
|
||||
ACCESS_LEVELS
|
||||
} = require('./auth');
|
||||
|
||||
module.exports = {
|
||||
// 레거시 별칭 (하위 호환성)
|
||||
verifyToken,
|
||||
requireAdmin,
|
||||
requireSystem,
|
||||
|
||||
// 새로운 API (권장)
|
||||
requireAuth,
|
||||
requireRole,
|
||||
requireMinLevel,
|
||||
requireOwnerOrAdmin,
|
||||
ACCESS_LEVELS
|
||||
};
|
||||
@@ -2,7 +2,7 @@
|
||||
const express = require('express');
|
||||
const router = express.Router();
|
||||
const { getAnalysisData } = require('../controllers/analysisController');
|
||||
const { verifyToken } = require('../middlewares/authMiddleware'); // 인증 미들웨어 추가
|
||||
const { verifyToken } = require('../middlewares/auth');
|
||||
|
||||
// GET /api/analysis?startDate=...&endDate=...
|
||||
router.get('/', verifyToken, getAnalysisData);
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
const express = require('express');
|
||||
const router = express.Router();
|
||||
const AttendanceController = require('../controllers/attendanceController');
|
||||
const { verifyToken } = require('../middlewares/authMiddleware');
|
||||
const { verifyToken } = require('../middlewares/auth');
|
||||
|
||||
// 모든 라우트에 인증 미들웨어 적용
|
||||
router.use(verifyToken);
|
||||
|
||||
@@ -1,215 +0,0 @@
|
||||
// routes/auth.js
|
||||
const express = require('express');
|
||||
const bcrypt = require('bcrypt');
|
||||
const jwt = require('jsonwebtoken');
|
||||
const { requireAuth, requireRole } = require('../middlewares/auth');
|
||||
const router = express.Router();
|
||||
|
||||
// 임시 사용자 데이터 (실제 운영 시 DB 사용 필수)
|
||||
// 비밀번호 해시는 bcrypt.hash('password', 10)으로 생성됨
|
||||
let users = [
|
||||
{
|
||||
user_id: 1,
|
||||
username: 'admin',
|
||||
password: '$2b$10$N9qo8uLOickgx2ZMRZoMyeIjZRGdjGj/n3.w7VtL.r8yR.nTfC7Hy', // 'password' 해시
|
||||
name: '관리자',
|
||||
access_level: 'admin',
|
||||
worker_id: null,
|
||||
created_at: new Date()
|
||||
},
|
||||
{
|
||||
user_id: 2,
|
||||
username: 'group_leader1',
|
||||
password: '$2b$10$N9qo8uLOickgx2ZMRZoMyeIjZRGdjGj/n3.w7VtL.r8yR.nTfC7Hy', // 'password' 해시
|
||||
name: '김그룹장',
|
||||
access_level: 'group_leader',
|
||||
worker_id: 1,
|
||||
created_at: new Date()
|
||||
}
|
||||
];
|
||||
|
||||
// 보안 경고: 운영 환경에서는 반드시 .env의 JWT_SECRET을 설정해야 합니다
|
||||
if (!process.env.JWT_SECRET) {
|
||||
console.warn('⚠️ WARNING: JWT_SECRET이 설정되지 않았습니다. 운영 환경에서는 반드시 설정하세요!');
|
||||
}
|
||||
|
||||
/**
|
||||
* 로그인
|
||||
*/
|
||||
router.post('/login', async (req, res) => {
|
||||
try {
|
||||
const { username, password } = req.body;
|
||||
|
||||
if (!username || !password) {
|
||||
return res.status(400).json({ error: '사용자명과 비밀번호를 입력해주세요.' });
|
||||
}
|
||||
|
||||
const user = users.find(u => u.username === username);
|
||||
if (!user) {
|
||||
return res.status(401).json({ error: '사용자를 찾을 수 없습니다.' });
|
||||
}
|
||||
|
||||
// 비밀번호 확인 (bcrypt.compare 사용)
|
||||
const isValid = await bcrypt.compare(password, user.password);
|
||||
if (!isValid) {
|
||||
return res.status(401).json({ error: '비밀번호가 올바르지 않습니다.' });
|
||||
}
|
||||
|
||||
// JWT 토큰 생성
|
||||
const token = jwt.sign(
|
||||
{
|
||||
user_id: user.user_id,
|
||||
username: user.username,
|
||||
access_level: user.access_level,
|
||||
worker_id: user.worker_id
|
||||
},
|
||||
process.env.JWT_SECRET,
|
||||
{ expiresIn: '24h' }
|
||||
);
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
token,
|
||||
user: {
|
||||
user_id: user.user_id,
|
||||
username: user.username,
|
||||
name: user.name,
|
||||
access_level: user.access_level,
|
||||
worker_id: user.worker_id
|
||||
}
|
||||
});
|
||||
|
||||
} catch (error) {
|
||||
console.error('Login error:', error);
|
||||
res.status(500).json({ error: '서버 오류가 발생했습니다.' });
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* 현재 사용자 정보 조회
|
||||
*/
|
||||
router.get('/me', requireAuth, (req, res) => {
|
||||
try {
|
||||
const userId = req.user.user_id;
|
||||
const user = users.find(u => u.user_id === userId);
|
||||
|
||||
if (!user) {
|
||||
return res.status(404).json({ error: '사용자를 찾을 수 없습니다.' });
|
||||
}
|
||||
|
||||
res.json({
|
||||
user_id: user.user_id,
|
||||
username: user.username,
|
||||
name: user.name,
|
||||
access_level: user.access_level,
|
||||
worker_id: user.worker_id
|
||||
});
|
||||
|
||||
} catch (error) {
|
||||
console.error('Get current user error:', error);
|
||||
res.status(500).json({ error: '서버 오류가 발생했습니다.' });
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* 사용자 등록 (관리자만)
|
||||
*/
|
||||
router.post('/register', requireAuth, requireRole('admin', 'system'), async (req, res) => {
|
||||
try {
|
||||
const { username, password, name, access_level, worker_id } = req.body;
|
||||
|
||||
if (!username || !password || !name || !access_level) {
|
||||
return res.status(400).json({ error: '필수 항목을 모두 입력해주세요.' });
|
||||
}
|
||||
|
||||
// 사용자명 중복 체크
|
||||
const existingUser = users.find(u => u.username === username);
|
||||
if (existingUser) {
|
||||
return res.status(409).json({ error: '이미 존재하는 사용자명입니다.' });
|
||||
}
|
||||
|
||||
// 비밀번호 해시
|
||||
const hashedPassword = await bcrypt.hash(password, 10);
|
||||
|
||||
const newUser = {
|
||||
user_id: users.length + 1,
|
||||
username,
|
||||
password: hashedPassword,
|
||||
name,
|
||||
access_level,
|
||||
worker_id: worker_id || null,
|
||||
created_at: new Date()
|
||||
};
|
||||
|
||||
users.push(newUser);
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
message: '사용자가 성공적으로 등록되었습니다.',
|
||||
user: {
|
||||
user_id: newUser.user_id,
|
||||
username: newUser.username,
|
||||
name: newUser.name,
|
||||
access_level: newUser.access_level,
|
||||
worker_id: newUser.worker_id
|
||||
}
|
||||
});
|
||||
|
||||
} catch (error) {
|
||||
console.error('Register error:', error);
|
||||
res.status(500).json({ error: '서버 오류가 발생했습니다.' });
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* 사용자 목록 조회 (관리자만)
|
||||
*/
|
||||
router.get('/users', requireAuth, requireRole('admin', 'system'), (req, res) => {
|
||||
try {
|
||||
const userList = users.map(user => ({
|
||||
user_id: user.user_id,
|
||||
username: user.username,
|
||||
name: user.name,
|
||||
access_level: user.access_level,
|
||||
worker_id: user.worker_id,
|
||||
created_at: user.created_at
|
||||
}));
|
||||
|
||||
res.json(userList);
|
||||
} catch (error) {
|
||||
console.error('Get users error:', error);
|
||||
res.status(500).json({ error: '서버 오류가 발생했습니다.' });
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* 사용자 삭제 (관리자만)
|
||||
*/
|
||||
router.delete('/users/:id', requireAuth, requireRole('admin', 'system'), (req, res) => {
|
||||
try {
|
||||
const userId = parseInt(req.params.id);
|
||||
|
||||
// 자기 자신 삭제 방지
|
||||
if (userId === req.user.user_id) {
|
||||
return res.status(400).json({ error: '자기 자신은 삭제할 수 없습니다.' });
|
||||
}
|
||||
|
||||
const userIndex = users.findIndex(u => u.user_id === userId);
|
||||
if (userIndex === -1) {
|
||||
return res.status(404).json({ error: '사용자를 찾을 수 없습니다.' });
|
||||
}
|
||||
|
||||
users.splice(userIndex, 1);
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
message: '사용자가 성공적으로 삭제되었습니다.'
|
||||
});
|
||||
|
||||
} catch (error) {
|
||||
console.error('Delete user error:', error);
|
||||
res.status(500).json({ error: '서버 오류가 발생했습니다.' });
|
||||
}
|
||||
});
|
||||
|
||||
module.exports = router;
|
||||
@@ -10,7 +10,7 @@ const express = require('express');
|
||||
const bcrypt = require('bcryptjs');
|
||||
const jwt = require('jsonwebtoken');
|
||||
const mysql = require('mysql2/promise');
|
||||
const { verifyToken } = require('../middlewares/authMiddleware');
|
||||
const { verifyToken } = require('../middlewares/auth');
|
||||
const { validatePassword, getPasswordError } = require('../utils/passwordValidator');
|
||||
const router = express.Router();
|
||||
const authController = require('../controllers/authController');
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
const express = require('express');
|
||||
const router = express.Router();
|
||||
const departmentController = require('../controllers/departmentController');
|
||||
const { requireAuth, requireRole } = require('../middlewares/authMiddleware');
|
||||
const { requireAuth, requireRole } = require('../middlewares/auth');
|
||||
|
||||
// 부서 목록 조회 (인증 필요)
|
||||
router.get('/', requireAuth, departmentController.getAll);
|
||||
|
||||
@@ -25,99 +25,4 @@ router.get('/detail', (req, res) => {
|
||||
});
|
||||
});
|
||||
|
||||
// 임시 마이그레이션 엔드포인트 - TBM work_type_id 수정
|
||||
// 실행 후 이 코드를 삭제하세요!
|
||||
router.post('/migrate-work-type-id', async (req, res) => {
|
||||
try {
|
||||
const { getDb } = require('../dbPool');
|
||||
const db = await getDb();
|
||||
|
||||
console.log('🔄 TBM 기반 작업보고서 work_type_id 수정 시작...');
|
||||
|
||||
// 1. 수정 대상 확인
|
||||
const [checkResult] = await db.query(`
|
||||
SELECT
|
||||
dwr.id,
|
||||
dwr.work_type_id as current_work_type_id,
|
||||
ta.task_id as correct_task_id,
|
||||
w.worker_name,
|
||||
dwr.report_date
|
||||
FROM daily_work_reports dwr
|
||||
INNER JOIN tbm_team_assignments ta ON dwr.tbm_assignment_id = ta.assignment_id
|
||||
INNER JOIN workers w ON dwr.worker_id = w.worker_id
|
||||
WHERE dwr.tbm_assignment_id IS NOT NULL
|
||||
AND ta.task_id IS NOT NULL
|
||||
AND dwr.work_type_id != ta.task_id
|
||||
ORDER BY dwr.report_date DESC
|
||||
`);
|
||||
|
||||
console.log(`📊 수정 대상: ${checkResult.length}개 레코드`);
|
||||
|
||||
if (checkResult.length === 0) {
|
||||
return res.json({
|
||||
success: true,
|
||||
message: '수정할 데이터가 없습니다.',
|
||||
data: { affected_rows: 0 }
|
||||
});
|
||||
}
|
||||
|
||||
// 수정 전 샘플 로깅
|
||||
console.log('수정 전 샘플:', checkResult.slice(0, 5));
|
||||
|
||||
// 2. 업데이트 실행
|
||||
const [updateResult] = await db.query(`
|
||||
UPDATE daily_work_reports dwr
|
||||
INNER JOIN tbm_team_assignments ta ON dwr.tbm_assignment_id = ta.assignment_id
|
||||
SET dwr.work_type_id = ta.task_id
|
||||
WHERE dwr.tbm_assignment_id IS NOT NULL
|
||||
AND ta.task_id IS NOT NULL
|
||||
AND dwr.work_type_id != ta.task_id
|
||||
`);
|
||||
|
||||
console.log(`✅ 업데이트 완료: ${updateResult.affectedRows}개 레코드 수정됨`);
|
||||
|
||||
// 3. 수정 후 확인
|
||||
const [samples] = await db.query(`
|
||||
SELECT
|
||||
dwr.id,
|
||||
dwr.work_type_id,
|
||||
t.task_name,
|
||||
wt.name as work_type_name,
|
||||
w.worker_name,
|
||||
dwr.report_date
|
||||
FROM daily_work_reports dwr
|
||||
INNER JOIN tbm_team_assignments ta ON dwr.tbm_assignment_id = ta.assignment_id
|
||||
LEFT JOIN tasks t ON dwr.work_type_id = t.task_id
|
||||
LEFT JOIN work_types wt ON t.work_type_id = wt.id
|
||||
LEFT JOIN workers w ON dwr.worker_id = w.worker_id
|
||||
WHERE dwr.tbm_assignment_id IS NOT NULL
|
||||
ORDER BY dwr.report_date DESC
|
||||
LIMIT 10
|
||||
`);
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
message: `${updateResult.affectedRows}개 레코드가 수정되었습니다.`,
|
||||
data: {
|
||||
affected_rows: updateResult.affectedRows,
|
||||
before_count: checkResult.length,
|
||||
samples: samples.map(s => ({
|
||||
id: s.id,
|
||||
worker: s.worker_name,
|
||||
date: s.report_date,
|
||||
task: s.task_name,
|
||||
work_type: s.work_type_name
|
||||
}))
|
||||
}
|
||||
});
|
||||
|
||||
} catch (error) {
|
||||
console.error('마이그레이션 실패:', error);
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
error: '마이그레이션 실패: ' + error.message
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
module.exports = router;
|
||||
@@ -4,7 +4,7 @@
|
||||
const express = require('express');
|
||||
const router = express.Router();
|
||||
const MonthlyStatusController = require('../controllers/monthlyStatusController');
|
||||
const { verifyToken } = require('../middlewares/authMiddleware');
|
||||
const { verifyToken } = require('../middlewares/auth');
|
||||
|
||||
// 모든 라우트에 인증 미들웨어 적용 (임시로 주석 처리 - 테스트용)
|
||||
// router.use(verifyToken);
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
const express = require('express');
|
||||
const router = express.Router();
|
||||
const notificationRecipientController = require('../controllers/notificationRecipientController');
|
||||
const { verifyToken, requireMinLevel } = require('../middlewares/authMiddleware');
|
||||
const { verifyToken, requireMinLevel } = require('../middlewares/auth');
|
||||
|
||||
// 모든 라우트에 인증 필요
|
||||
router.use(verifyToken);
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
const express = require('express');
|
||||
const router = express.Router();
|
||||
const TbmController = require('../controllers/tbmController');
|
||||
const { requireAuth } = require('../middlewares/auth');
|
||||
const { requireAuth, requireRole } = require('../middlewares/auth');
|
||||
|
||||
// ==================== TBM 세션 관련 ====================
|
||||
|
||||
@@ -56,13 +56,13 @@ router.delete('/sessions/:sessionId/team/:workerId', requireAuth, TbmController.
|
||||
router.get('/safety-checks', requireAuth, TbmController.getAllSafetyChecks);
|
||||
|
||||
// 안전 체크 항목 생성 (관리자용)
|
||||
router.post('/safety-checks', requireAuth, TbmController.createSafetyCheck);
|
||||
router.post('/safety-checks', requireAuth, requireRole('admin', 'system'), TbmController.createSafetyCheck);
|
||||
|
||||
// 안전 체크 항목 수정 (관리자용)
|
||||
router.put('/safety-checks/:checkId', requireAuth, TbmController.updateSafetyCheck);
|
||||
router.put('/safety-checks/:checkId', requireAuth, requireRole('admin', 'system'), TbmController.updateSafetyCheck);
|
||||
|
||||
// 안전 체크 항목 삭제 (관리자용)
|
||||
router.delete('/safety-checks/:checkId', requireAuth, TbmController.deleteSafetyCheck);
|
||||
router.delete('/safety-checks/:checkId', requireAuth, requireRole('admin', 'system'), TbmController.deleteSafetyCheck);
|
||||
|
||||
// TBM 세션의 안전 체크 기록 조회
|
||||
router.get('/sessions/:sessionId/safety', requireAuth, TbmController.getSafetyRecords);
|
||||
|
||||
@@ -10,7 +10,7 @@
|
||||
const express = require('express');
|
||||
const router = express.Router();
|
||||
const userController = require('../controllers/userController');
|
||||
const { verifyToken } = require('../middlewares/authMiddleware');
|
||||
const { verifyToken } = require('../middlewares/auth');
|
||||
const logger = require('../utils/logger');
|
||||
|
||||
/**
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
const express = require('express');
|
||||
const router = express.Router();
|
||||
const visitRequestController = require('../controllers/visitRequestController');
|
||||
const { verifyToken } = require('../middlewares/authMiddleware');
|
||||
const { verifyToken } = require('../middlewares/auth');
|
||||
|
||||
// 모든 라우트에 인증 미들웨어 적용
|
||||
router.use(verifyToken);
|
||||
|
||||
@@ -1,74 +0,0 @@
|
||||
// routes/workAnalysis.js
|
||||
|
||||
const express = require('express');
|
||||
const router = express.Router();
|
||||
const workAnalysisController = require('../controllers/workAnalysisController');
|
||||
|
||||
// 🔒 분석 기능은 admin 또는 system 권한만 접근 가능
|
||||
const requireAnalysisAccess = (req, res, next) => {
|
||||
if (!req.user) {
|
||||
return res.status(401).json({ error: '인증이 필요합니다.' });
|
||||
}
|
||||
|
||||
const allowedLevels = ['admin', 'system'];
|
||||
if (!allowedLevels.includes(req.user.access_level)) {
|
||||
return res.status(403).json({
|
||||
error: '분석 기능 접근 권한이 없습니다. 관리자 권한이 필요합니다.',
|
||||
required: 'admin 또는 system',
|
||||
current: req.user.access_level
|
||||
});
|
||||
}
|
||||
|
||||
console.log(`🔓 분석 기능 접근 허용: ${req.user.username} (${req.user.access_level})`);
|
||||
next();
|
||||
};
|
||||
|
||||
// 임시로 권한 체크 건너뛰기 (테스트용)
|
||||
const skipAuth = (req, res, next) => {
|
||||
console.log('⚠️ 임시로 권한 체크 건너뛰기');
|
||||
next();
|
||||
};
|
||||
|
||||
// 기본 통계 조회 - 임시로 권한 체크 비활성화
|
||||
router.get('/stats', skipAuth, workAnalysisController.getStats);
|
||||
|
||||
// 일별 작업시간 추이 - 임시로 권한 체크 비활성화
|
||||
router.get('/daily-trend', skipAuth, workAnalysisController.getDailyTrend);
|
||||
|
||||
// 작업자별 통계 - 임시로 권한 체크 비활성화
|
||||
router.get('/worker-stats', skipAuth, workAnalysisController.getWorkerStats);
|
||||
|
||||
// 프로젝트별 통계 - 임시로 권한 체크 비활성화
|
||||
router.get('/project-stats', skipAuth, workAnalysisController.getProjectStats);
|
||||
|
||||
// 작업유형별 통계 - 임시로 권한 체크 비활성화
|
||||
router.get('/worktype-stats', skipAuth, workAnalysisController.getWorkTypeStats);
|
||||
|
||||
// 최근 작업 현황 - 임시로 권한 체크 비활성화
|
||||
router.get('/recent-work', skipAuth, workAnalysisController.getRecentWork);
|
||||
|
||||
// 요일별 패턴 분석
|
||||
router.get('/weekday-pattern', requireAnalysisAccess, workAnalysisController.getWeekdayPattern);
|
||||
|
||||
// 에러 분석
|
||||
router.get('/error-analysis', requireAnalysisAccess, workAnalysisController.getErrorAnalysis);
|
||||
|
||||
// 월별 비교 분석
|
||||
router.get('/monthly-comparison', requireAnalysisAccess, workAnalysisController.getMonthlyComparison);
|
||||
|
||||
// 작업자별 전문분야 분석
|
||||
router.get('/worker-specialization', requireAnalysisAccess, workAnalysisController.getWorkerSpecialization);
|
||||
|
||||
// 대시보드용 종합 데이터 (한 번에 여러 데이터 조회)
|
||||
router.get('/dashboard', requireAnalysisAccess, workAnalysisController.getDashboardData);
|
||||
|
||||
// 헬스체크 - 인증 없이 접근 가능
|
||||
router.get('/health', (req, res) => {
|
||||
res.status(200).json({
|
||||
success: true,
|
||||
message: 'Work Analysis API is running',
|
||||
timestamp: new Date().toISOString()
|
||||
});
|
||||
});
|
||||
|
||||
module.exports = router;
|
||||
Reference in New Issue
Block a user