feat: 시스템 관리자 대시보드 개선
- 시스템 관리자 전용 웹페이지 구현 (system.html) - 깔끔한 흰색 배경의 올드스쿨 스타일 적용 - 반응형 그리드 레이아웃으로 카드 배치 개선 - ES6 모듈 방식으로 JavaScript 구조 개선 - 이벤트 리스너 방식으로 버튼 클릭 처리 변경 - 시스템 상태, 사용자 통계, 계정 관리 기능 구현 - 시스템 로그 조회 기능 추가 - 나머지 관리 기능들 스켈레톤 구현 (개발 중 상태) - 인코딩 문제 해결을 위한 영어 로그 메시지 적용 - hyungi 계정을 system 권한으로 설정 - JWT 토큰에 role 필드 추가 - 시스템 전용 API 엔드포인트 구현 주요 변경사항: - web-ui/pages/dashboard/system.html: 시스템 관리자 전용 페이지 - web-ui/css/system-dashboard.css: 시스템 대시보드 전용 스타일 - web-ui/js/system-dashboard.js: 시스템 대시보드 로직 - api.hyungi.net/controllers/systemController.js: 시스템 API 컨트롤러 - api.hyungi.net/routes/systemRoutes.js: 시스템 API 라우트 - api.hyungi.net/controllers/authController.js: 시스템 권한 로그인 처리 - api.hyungi.net/services/auth.service.js: JWT 토큰에 role 필드 추가
This commit is contained in:
@@ -1,5 +1,5 @@
|
||||
# .env
|
||||
PORT=3005
|
||||
PORT=20005
|
||||
|
||||
# MariaDB 컨테이너 초기화용 루트 패스워드
|
||||
DB_ROOT_PASSWORD=matxAc-jutty1-ruhsoc
|
||||
|
||||
@@ -24,8 +24,10 @@ const login = async (req, res) => {
|
||||
let redirectUrl;
|
||||
|
||||
switch (user.role) {
|
||||
case 'system': // 시스템 계정 전용 대시보드
|
||||
redirectUrl = '/pages/dashboard/system.html';
|
||||
break;
|
||||
case 'admin':
|
||||
case 'system': // 'system'도 관리자로 취급
|
||||
redirectUrl = '/pages/dashboard/admin.html';
|
||||
break;
|
||||
case 'leader':
|
||||
@@ -69,7 +71,7 @@ exports.register = async (req, res) => {
|
||||
|
||||
// 중복 아이디 확인
|
||||
const [existing] = await db.query(
|
||||
'SELECT user_id FROM users WHERE username = ?',
|
||||
'SELECT user_id FROM Users WHERE username = ?',
|
||||
[username]
|
||||
);
|
||||
|
||||
@@ -86,7 +88,7 @@ exports.register = async (req, res) => {
|
||||
// role 설정 (access_level에 따라)
|
||||
const roleMap = {
|
||||
'admin': 'admin',
|
||||
'system': 'admin',
|
||||
'system': 'system', // 시스템 계정은 system role로 설정
|
||||
'group_leader': 'leader',
|
||||
'support_team': 'support',
|
||||
'worker': 'user'
|
||||
@@ -95,7 +97,7 @@ exports.register = async (req, res) => {
|
||||
|
||||
// 사용자 등록
|
||||
const [result] = await db.query(
|
||||
`INSERT INTO users (username, password, name, role, access_level, worker_id)
|
||||
`INSERT INTO Users (username, password, name, role, access_level, worker_id)
|
||||
VALUES (?, ?, ?, ?, ?, ?)`,
|
||||
[username, hashedPassword, name, role, access_level, worker_id]
|
||||
);
|
||||
@@ -126,7 +128,7 @@ exports.deleteUser = async (req, res) => {
|
||||
|
||||
// 사용자 존재 확인
|
||||
const [user] = await db.query(
|
||||
'SELECT user_id FROM users WHERE user_id = ?',
|
||||
'SELECT user_id FROM Users WHERE user_id = ?',
|
||||
[id]
|
||||
);
|
||||
|
||||
@@ -138,7 +140,7 @@ exports.deleteUser = async (req, res) => {
|
||||
}
|
||||
|
||||
// 사용자 삭제
|
||||
await db.query('DELETE FROM users WHERE user_id = ?', [id]);
|
||||
await db.query('DELETE FROM Users WHERE user_id = ?', [id]);
|
||||
|
||||
console.log('[사용자 삭제 성공] ID:', id);
|
||||
|
||||
@@ -165,7 +167,7 @@ exports.getAllUsers = async (req, res) => {
|
||||
// 비밀번호 제외하고 조회
|
||||
const [rows] = await db.query(
|
||||
`SELECT user_id, username, name, role, access_level, worker_id, created_at
|
||||
FROM users
|
||||
FROM Users
|
||||
ORDER BY created_at DESC`
|
||||
);
|
||||
|
||||
|
||||
506
api.hyungi.net/controllers/systemController.js
Normal file
506
api.hyungi.net/controllers/systemController.js
Normal file
@@ -0,0 +1,506 @@
|
||||
// 시스템 관리 컨트롤러
|
||||
const { getDb } = require('../dbPool');
|
||||
const bcrypt = require('bcryptjs');
|
||||
|
||||
/**
|
||||
* 시스템 상태 확인
|
||||
*/
|
||||
exports.getSystemStatus = async (req, res) => {
|
||||
try {
|
||||
const db = await getDb();
|
||||
|
||||
// 데이터베이스 연결 상태 확인
|
||||
const [dbStatus] = await db.query('SELECT 1 as status');
|
||||
|
||||
// 시스템 상태 정보
|
||||
const systemStatus = {
|
||||
server: 'online',
|
||||
database: dbStatus.length > 0 ? 'online' : 'offline',
|
||||
timestamp: new Date().toISOString(),
|
||||
uptime: process.uptime(),
|
||||
memory: process.memoryUsage(),
|
||||
version: process.version
|
||||
};
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
data: systemStatus
|
||||
});
|
||||
|
||||
} catch (error) {
|
||||
console.error('시스템 상태 확인 오류:', error);
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
error: '시스템 상태를 확인할 수 없습니다.'
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* 데이터베이스 상태 확인
|
||||
*/
|
||||
exports.getDatabaseStatus = async (req, res) => {
|
||||
try {
|
||||
const db = await getDb();
|
||||
|
||||
// 데이터베이스 연결 수 확인
|
||||
const [connections] = await db.query('SHOW STATUS LIKE "Threads_connected"');
|
||||
const [maxConnections] = await db.query('SHOW VARIABLES LIKE "max_connections"');
|
||||
|
||||
// 데이터베이스 크기 확인
|
||||
const [dbSize] = await db.query(`
|
||||
SELECT
|
||||
ROUND(SUM(data_length + index_length) / 1024 / 1024, 2) AS size_mb
|
||||
FROM information_schema.tables
|
||||
WHERE table_schema = DATABASE()
|
||||
`);
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
data: {
|
||||
status: 'online',
|
||||
connections: parseInt(connections[0]?.Value || 0),
|
||||
max_connections: parseInt(maxConnections[0]?.Value || 0),
|
||||
size_mb: dbSize[0]?.size_mb || 0
|
||||
}
|
||||
});
|
||||
|
||||
} catch (error) {
|
||||
console.error('데이터베이스 상태 확인 오류:', error);
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
error: '데이터베이스 상태를 확인할 수 없습니다.'
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* 시스템 알림 조회
|
||||
*/
|
||||
exports.getSystemAlerts = async (req, res) => {
|
||||
try {
|
||||
const db = await getDb();
|
||||
|
||||
// 최근 실패한 로그인 시도
|
||||
const [failedLogins] = await db.query(`
|
||||
SELECT COUNT(*) as count
|
||||
FROM login_logs
|
||||
WHERE login_status = 'failed'
|
||||
AND login_time > DATE_SUB(NOW(), INTERVAL 1 HOUR)
|
||||
`);
|
||||
|
||||
// 비활성 사용자 수
|
||||
const [inactiveusers] = await db.query(`
|
||||
SELECT COUNT(*) as count
|
||||
FROM users
|
||||
WHERE is_active = 0
|
||||
`);
|
||||
|
||||
const alerts = [];
|
||||
|
||||
if (failedLogins[0]?.count > 5) {
|
||||
alerts.push({
|
||||
type: 'security',
|
||||
level: 'warning',
|
||||
message: `최근 1시간 동안 ${failedLogins[0].count}회의 로그인 실패가 발생했습니다.`,
|
||||
timestamp: new Date().toISOString()
|
||||
});
|
||||
}
|
||||
|
||||
if (inactiveusers[0]?.count > 0) {
|
||||
alerts.push({
|
||||
type: 'user',
|
||||
level: 'info',
|
||||
message: `${inactiveusers[0].count}명의 비활성 사용자가 있습니다.`,
|
||||
timestamp: new Date().toISOString()
|
||||
});
|
||||
}
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
alerts: alerts
|
||||
});
|
||||
|
||||
} catch (error) {
|
||||
console.error('시스템 알림 조회 오류:', error);
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
error: '시스템 알림을 조회할 수 없습니다.'
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* 최근 시스템 활동 조회
|
||||
*/
|
||||
exports.getRecentActivities = async (req, res) => {
|
||||
try {
|
||||
const db = await getDb();
|
||||
|
||||
// 최근 로그인 활동
|
||||
const [loginActivities] = await db.query(`
|
||||
SELECT
|
||||
ll.login_time as created_at,
|
||||
u.name as user_name,
|
||||
ll.login_status,
|
||||
ll.ip_address,
|
||||
'login' as activity_type
|
||||
FROM login_logs ll
|
||||
LEFT JOIN users u ON ll.user_id = u.user_id
|
||||
ORDER BY ll.login_time DESC
|
||||
LIMIT 10
|
||||
`);
|
||||
|
||||
// 비밀번호 변경 활동
|
||||
const [passwordActivities] = await db.query(`
|
||||
SELECT
|
||||
pcl.changed_at as created_at,
|
||||
u.name as user_name,
|
||||
pcl.change_type,
|
||||
'password_change' as activity_type
|
||||
FROM password_change_logs pcl
|
||||
LEFT JOIN users u ON pcl.user_id = u.user_id
|
||||
ORDER BY pcl.changed_at DESC
|
||||
LIMIT 5
|
||||
`);
|
||||
|
||||
// 활동 통합 및 정렬
|
||||
const activities = [
|
||||
...loginActivities.map(activity => ({
|
||||
type: activity.login_status === 'success' ? 'login' : 'login_failed',
|
||||
title: activity.login_status === 'success'
|
||||
? `${activity.user_name || '알 수 없는 사용자'} 로그인`
|
||||
: `로그인 실패 (${activity.ip_address})`,
|
||||
description: activity.login_status === 'success'
|
||||
? `IP: ${activity.ip_address}`
|
||||
: `사용자: ${activity.user_name || '알 수 없음'}`,
|
||||
created_at: activity.created_at
|
||||
})),
|
||||
...passwordActivities.map(activity => ({
|
||||
type: 'password_change',
|
||||
title: `${activity.user_name || '알 수 없는 사용자'} 비밀번호 변경`,
|
||||
description: `변경 유형: ${activity.change_type}`,
|
||||
created_at: activity.created_at
|
||||
}))
|
||||
];
|
||||
|
||||
// 시간순 정렬
|
||||
activities.sort((a, b) => new Date(b.created_at) - new Date(a.created_at));
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
data: activities.slice(0, 15)
|
||||
});
|
||||
|
||||
} catch (error) {
|
||||
console.error('최근 활동 조회 오류:', error);
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
error: '최근 활동을 조회할 수 없습니다.'
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* 사용자 통계 조회
|
||||
*/
|
||||
exports.getUserStats = async (req, res) => {
|
||||
try {
|
||||
const db = await getDb();
|
||||
|
||||
// 전체 사용자 수
|
||||
const [totalusers] = await db.query('SELECT COUNT(*) as count FROM users');
|
||||
|
||||
// 활성 사용자 수
|
||||
const [activeusers] = await db.query('SELECT COUNT(*) as count FROM users WHERE is_active = 1');
|
||||
|
||||
// 최근 24시간 로그인 사용자 수
|
||||
const [recentLogins] = await db.query(`
|
||||
SELECT COUNT(DISTINCT user_id) as count
|
||||
FROM login_logs
|
||||
WHERE login_status = 'success'
|
||||
AND login_time > DATE_SUB(NOW(), INTERVAL 24 HOUR)
|
||||
`);
|
||||
|
||||
// 권한별 사용자 수
|
||||
const [roleStats] = await db.query(`
|
||||
SELECT role, COUNT(*) as count
|
||||
FROM users
|
||||
WHERE is_active = 1
|
||||
GROUP BY role
|
||||
`);
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
data: {
|
||||
total: totalusers[0]?.count || 0,
|
||||
active: activeusers[0]?.count || 0,
|
||||
recent_logins: recentLogins[0]?.count || 0,
|
||||
by_role: roleStats
|
||||
}
|
||||
});
|
||||
|
||||
} catch (error) {
|
||||
console.error('사용자 통계 조회 오류:', error);
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
error: '사용자 통계를 조회할 수 없습니다.'
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* 모든 사용자 목록 조회 (시스템 관리자용)
|
||||
*/
|
||||
exports.getAllUsers = async (req, res) => {
|
||||
try {
|
||||
const db = await getDb();
|
||||
|
||||
const [users] = await db.query(`
|
||||
SELECT
|
||||
user_id,
|
||||
username,
|
||||
name,
|
||||
email,
|
||||
role,
|
||||
access_level,
|
||||
worker_id,
|
||||
is_active,
|
||||
last_login_at,
|
||||
failed_login_attempts,
|
||||
locked_until,
|
||||
created_at,
|
||||
updated_at
|
||||
FROM users
|
||||
ORDER BY created_at DESC
|
||||
`);
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
data: users
|
||||
});
|
||||
|
||||
} catch (error) {
|
||||
console.error('사용자 목록 조회 오류:', error);
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
error: '사용자 목록을 조회할 수 없습니다.'
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* 사용자 생성
|
||||
*/
|
||||
exports.createUser = async (req, res) => {
|
||||
try {
|
||||
const { username, password, name, email, role, access_level, worker_id } = req.body;
|
||||
const db = await getDb();
|
||||
|
||||
// 필수 필드 검증
|
||||
if (!username || !password || !name || !role) {
|
||||
return res.status(400).json({
|
||||
success: false,
|
||||
error: '필수 정보가 누락되었습니다.'
|
||||
});
|
||||
}
|
||||
|
||||
// 사용자명 중복 확인
|
||||
const [existing] = await db.query('SELECT user_id FROM users WHERE username = ?', [username]);
|
||||
if (existing.length > 0) {
|
||||
return res.status(409).json({
|
||||
success: false,
|
||||
error: '이미 존재하는 사용자명입니다.'
|
||||
});
|
||||
}
|
||||
|
||||
// 이메일 중복 확인 (이메일이 제공된 경우)
|
||||
if (email) {
|
||||
const [existingEmail] = await db.query('SELECT user_id FROM users WHERE email = ?', [email]);
|
||||
if (existingEmail.length > 0) {
|
||||
return res.status(409).json({
|
||||
success: false,
|
||||
error: '이미 사용 중인 이메일입니다.'
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// 비밀번호 해시화
|
||||
const hashedPassword = await bcrypt.hash(password, 10);
|
||||
|
||||
// 사용자 생성
|
||||
const [result] = await db.query(`
|
||||
INSERT INTO users (username, password, name, email, role, access_level, worker_id, is_active, created_at, password_changed_at)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?, 1, NOW(), NOW())
|
||||
`, [username, hashedPassword, name, email || null, role, access_level || role, worker_id || null]);
|
||||
|
||||
// 비밀번호 변경 로그 기록
|
||||
await db.query(`
|
||||
INSERT INTO password_change_logs (user_id, changed_by_user_id, changed_at, change_type)
|
||||
VALUES (?, ?, NOW(), 'initial')
|
||||
`, [result.insertId, req.user.user_id]);
|
||||
|
||||
res.status(201).json({
|
||||
success: true,
|
||||
message: '사용자가 성공적으로 생성되었습니다.',
|
||||
user_id: result.insertId
|
||||
});
|
||||
|
||||
} catch (error) {
|
||||
console.error('사용자 생성 오류:', error);
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
error: '사용자 생성 중 오류가 발생했습니다.'
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* 사용자 수정
|
||||
*/
|
||||
exports.updateUser = async (req, res) => {
|
||||
try {
|
||||
const { id } = req.params;
|
||||
const { name, email, role, access_level, is_active, worker_id } = req.body;
|
||||
const db = await getDb();
|
||||
|
||||
// 사용자 존재 확인
|
||||
const [user] = await db.query('SELECT user_id FROM users WHERE user_id = ?', [id]);
|
||||
if (user.length === 0) {
|
||||
return res.status(404).json({
|
||||
success: false,
|
||||
error: '해당 사용자를 찾을 수 없습니다.'
|
||||
});
|
||||
}
|
||||
|
||||
// 이메일 중복 확인 (다른 사용자가 사용 중인지)
|
||||
if (email) {
|
||||
const [existingEmail] = await db.query(
|
||||
'SELECT user_id FROM users WHERE email = ? AND user_id != ?',
|
||||
[email, id]
|
||||
);
|
||||
if (existingEmail.length > 0) {
|
||||
return res.status(409).json({
|
||||
success: false,
|
||||
error: '이미 사용 중인 이메일입니다.'
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// 사용자 정보 업데이트
|
||||
await db.query(`
|
||||
UPDATE users
|
||||
SET name = ?, email = ?, role = ?, access_level = ?, is_active = ?, worker_id = ?, updated_at = NOW()
|
||||
WHERE user_id = ?
|
||||
`, [name, email || null, role, access_level || role, is_active ? 1 : 0, worker_id || null, id]);
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
message: '사용자 정보가 성공적으로 업데이트되었습니다.'
|
||||
});
|
||||
|
||||
} catch (error) {
|
||||
console.error('사용자 수정 오류:', error);
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
error: '사용자 수정 중 오류가 발생했습니다.'
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* 사용자 삭제
|
||||
*/
|
||||
exports.deleteUser = async (req, res) => {
|
||||
try {
|
||||
const { id } = req.params;
|
||||
const db = await getDb();
|
||||
|
||||
// 자기 자신 삭제 방지
|
||||
if (parseInt(id) === req.user.user_id) {
|
||||
return res.status(400).json({
|
||||
success: false,
|
||||
error: '자기 자신은 삭제할 수 없습니다.'
|
||||
});
|
||||
}
|
||||
|
||||
// 사용자 존재 확인
|
||||
const [user] = await db.query('SELECT user_id, username FROM users WHERE user_id = ?', [id]);
|
||||
if (user.length === 0) {
|
||||
return res.status(404).json({
|
||||
success: false,
|
||||
error: '해당 사용자를 찾을 수 없습니다.'
|
||||
});
|
||||
}
|
||||
|
||||
// 사용자 삭제 (관련 로그는 유지)
|
||||
await db.query('DELETE FROM users WHERE user_id = ?', [id]);
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
message: `사용자 '${user[0].username}'가 성공적으로 삭제되었습니다.`
|
||||
});
|
||||
|
||||
} catch (error) {
|
||||
console.error('사용자 삭제 오류:', error);
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
error: '사용자 삭제 중 오류가 발생했습니다.'
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* 사용자 비밀번호 재설정
|
||||
*/
|
||||
exports.resetUserPassword = async (req, res) => {
|
||||
try {
|
||||
const { id } = req.params;
|
||||
const { new_password } = req.body;
|
||||
const db = await getDb();
|
||||
|
||||
if (!new_password || new_password.length < 6) {
|
||||
return res.status(400).json({
|
||||
success: false,
|
||||
error: '비밀번호는 최소 6자 이상이어야 합니다.'
|
||||
});
|
||||
}
|
||||
|
||||
// 사용자 존재 확인
|
||||
const [user] = await db.query('SELECT user_id, username FROM users WHERE user_id = ?', [id]);
|
||||
if (user.length === 0) {
|
||||
return res.status(404).json({
|
||||
success: false,
|
||||
error: '해당 사용자를 찾을 수 없습니다.'
|
||||
});
|
||||
}
|
||||
|
||||
// 비밀번호 해시화
|
||||
const hashedPassword = await bcrypt.hash(new_password, 10);
|
||||
|
||||
// 비밀번호 업데이트
|
||||
await db.query(`
|
||||
UPDATE users
|
||||
SET password = ?, password_changed_at = NOW(), failed_login_attempts = 0, locked_until = NULL
|
||||
WHERE user_id = ?
|
||||
`, [hashedPassword, id]);
|
||||
|
||||
// 비밀번호 변경 로그 기록
|
||||
await db.query(`
|
||||
INSERT INTO password_change_logs (user_id, changed_by_user_id, changed_at, change_type)
|
||||
VALUES (?, ?, NOW(), 'admin')
|
||||
`, [id, req.user.user_id]);
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
message: `사용자 '${user[0].username}'의 비밀번호가 재설정되었습니다.`
|
||||
});
|
||||
|
||||
} catch (error) {
|
||||
console.error('비밀번호 재설정 오류:', error);
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
error: '비밀번호 재설정 중 오류가 발생했습니다.'
|
||||
});
|
||||
}
|
||||
};
|
||||
@@ -16,7 +16,7 @@ services:
|
||||
- db_data:/var/lib/mysql
|
||||
- ./migrations:/docker-entrypoint-initdb.d # SQL 마이그레이션 자동 실행
|
||||
ports:
|
||||
- "3306:3306" # 개발 시 외부 접속용 (운영 시 제거)
|
||||
- "20306:3306" # RULES.md 준수: DB 포트 20306
|
||||
healthcheck:
|
||||
test: ["CMD", "mysqladmin", "ping", "-h", "localhost"]
|
||||
timeout: 20s
|
||||
@@ -32,7 +32,7 @@ services:
|
||||
condition: service_healthy # DB가 준비된 후 시작
|
||||
restart: unless-stopped
|
||||
ports:
|
||||
- "${PORT:-3005}:3005"
|
||||
- "20005:3005" # RULES.md 준수: API 포트 20005
|
||||
env_file:
|
||||
- ./.env
|
||||
environment:
|
||||
@@ -58,7 +58,7 @@ services:
|
||||
- db
|
||||
restart: unless-stopped
|
||||
ports:
|
||||
- "18080:80"
|
||||
- "20080:80" # RULES.md 준수: phpMyAdmin 포트 20080
|
||||
env_file:
|
||||
- ./.env
|
||||
environment:
|
||||
|
||||
@@ -192,7 +192,8 @@ const healthRoutes = require('./routes/healthRoutes');
|
||||
const pipeSpecRoutes = require('./routes/pipeSpecRoutes');
|
||||
const dailyWorkReportRoutes = require('./routes/dailyWorkReportRoutes');
|
||||
const workAnalysisRoutes = require('./routes/workAnalysisRoutes');
|
||||
const analysisRoutes = require('./routes/analysisRoutes'); // 새로운 분석 라우트
|
||||
const analysisRoutes = require('./routes/analysisRoutes');
|
||||
const systemRoutes = require('./routes/systemRoutes'); // 새로운 분석 라우트
|
||||
|
||||
// 🔒 인증 미들웨어 가져오기
|
||||
const { verifyToken } = require('./middlewares/authMiddleware');
|
||||
@@ -307,6 +308,9 @@ app.use('/api/analysis', analysisRoutes); // 새로운 분석 라우트 등록
|
||||
|
||||
// 📊 리포트 및 분석
|
||||
app.use('/api/workreports', workReportRoutes);
|
||||
|
||||
// 🔧 시스템 관리 (시스템 권한만)
|
||||
app.use('/api/system', systemRoutes);
|
||||
app.use('/api/uploads', uploadRoutes);
|
||||
|
||||
// ⚙️ 시스템 데이터들 (모든 인증된 사용자)
|
||||
|
||||
0
api.hyungi.net/migrations/01_schema.sql
Normal file
0
api.hyungi.net/migrations/01_schema.sql
Normal file
45
api.hyungi.net/migrations/update_hyungi_system_role.sql
Normal file
45
api.hyungi.net/migrations/update_hyungi_system_role.sql
Normal file
@@ -0,0 +1,45 @@
|
||||
-- hyungi 계정을 시스템 권한으로 업데이트
|
||||
-- 실행 날짜: 2025-01-XX
|
||||
|
||||
-- hyungi 계정의 role을 system으로 변경
|
||||
UPDATE Users
|
||||
SET
|
||||
role = 'system',
|
||||
access_level = 'system',
|
||||
name = '시스템 관리자',
|
||||
updated_at = NOW()
|
||||
WHERE username = 'hyungi';
|
||||
|
||||
-- 변경 결과 확인
|
||||
SELECT
|
||||
user_id,
|
||||
username,
|
||||
name,
|
||||
role,
|
||||
access_level,
|
||||
is_active,
|
||||
updated_at
|
||||
FROM Users
|
||||
WHERE username = 'hyungi';
|
||||
|
||||
-- 시스템 권한 확인을 위한 쿼리
|
||||
SELECT
|
||||
username,
|
||||
name,
|
||||
role,
|
||||
access_level,
|
||||
CASE
|
||||
WHEN role = 'system' THEN '✅ 시스템 관리자'
|
||||
WHEN role = 'admin' THEN '🔧 관리자'
|
||||
WHEN role = 'leader' THEN '👨🏫 그룹장'
|
||||
ELSE '👤 일반 사용자'
|
||||
END as permission_level
|
||||
FROM Users
|
||||
ORDER BY
|
||||
CASE role
|
||||
WHEN 'system' THEN 1
|
||||
WHEN 'admin' THEN 2
|
||||
WHEN 'leader' THEN 3
|
||||
ELSE 4
|
||||
END,
|
||||
username;
|
||||
@@ -97,7 +97,7 @@ router.post('/refresh-token', async (req, res) => {
|
||||
|
||||
// 사용자 정보 조회
|
||||
const [users] = await connection.execute(
|
||||
'SELECT * FROM users WHERE user_id = ? AND is_active = TRUE',
|
||||
'SELECT * FROM Users WHERE user_id = ? AND is_active = TRUE',
|
||||
[decoded.user_id]
|
||||
);
|
||||
|
||||
@@ -176,7 +176,7 @@ router.post('/change-password', verifyToken, async (req, res) => {
|
||||
|
||||
// 현재 사용자의 비밀번호 조회
|
||||
const [users] = await connection.execute(
|
||||
'SELECT password FROM users WHERE user_id = ?',
|
||||
'SELECT password FROM Users WHERE user_id = ?',
|
||||
[userId]
|
||||
);
|
||||
|
||||
@@ -283,7 +283,7 @@ router.post('/admin/change-password', verifyToken, async (req, res) => {
|
||||
|
||||
// 대상 사용자 확인
|
||||
const [users] = await connection.execute(
|
||||
'SELECT username, name FROM users WHERE user_id = ?',
|
||||
'SELECT username, name FROM Users WHERE user_id = ?',
|
||||
[userId]
|
||||
);
|
||||
|
||||
@@ -400,7 +400,7 @@ router.get('/me', verifyToken, async (req, res) => {
|
||||
|
||||
connection = await mysql.createConnection(dbConfig);
|
||||
const [rows] = await connection.execute(
|
||||
'SELECT user_id, username, name, email, access_level, worker_id, last_login_at, created_at FROM users WHERE user_id = ?',
|
||||
'SELECT user_id, username, name, email, access_level, worker_id, last_login_at, created_at FROM Users WHERE user_id = ?',
|
||||
[userId]
|
||||
);
|
||||
|
||||
@@ -466,7 +466,7 @@ router.post('/register', verifyToken, async (req, res) => {
|
||||
|
||||
// 사용자명 중복 체크
|
||||
const [existing] = await connection.execute(
|
||||
'SELECT user_id FROM users WHERE username = ?',
|
||||
'SELECT user_id FROM Users WHERE username = ?',
|
||||
[username]
|
||||
);
|
||||
|
||||
@@ -480,7 +480,7 @@ router.post('/register', verifyToken, async (req, res) => {
|
||||
// 이메일 중복 체크 (이메일이 제공된 경우)
|
||||
if (email) {
|
||||
const [existingEmail] = await connection.execute(
|
||||
'SELECT user_id FROM users WHERE email = ?',
|
||||
'SELECT user_id FROM Users WHERE email = ?',
|
||||
[email]
|
||||
);
|
||||
|
||||
@@ -561,7 +561,7 @@ router.get('/users', verifyToken, async (req, res) => {
|
||||
is_active,
|
||||
last_login_at,
|
||||
created_at
|
||||
FROM users
|
||||
FROM Users
|
||||
WHERE 1=1
|
||||
`;
|
||||
|
||||
@@ -638,7 +638,7 @@ router.put('/users/:id', verifyToken, async (req, res) => {
|
||||
|
||||
// 사용자 존재 확인
|
||||
const [existing] = await connection.execute(
|
||||
'SELECT user_id, username FROM users WHERE user_id = ?',
|
||||
'SELECT user_id, username FROM Users WHERE user_id = ?',
|
||||
[userId]
|
||||
);
|
||||
|
||||
@@ -662,7 +662,7 @@ router.put('/users/:id', verifyToken, async (req, res) => {
|
||||
// 이메일 중복 체크
|
||||
if (email) {
|
||||
const [emailCheck] = await connection.execute(
|
||||
'SELECT user_id FROM users WHERE email = ? AND user_id != ?',
|
||||
'SELECT user_id FROM Users WHERE email = ? AND user_id != ?',
|
||||
[email, userId]
|
||||
);
|
||||
|
||||
@@ -732,7 +732,7 @@ router.put('/users/:id', verifyToken, async (req, res) => {
|
||||
|
||||
// 업데이트된 사용자 정보 조회
|
||||
const [updated] = await connection.execute(
|
||||
'SELECT user_id, username, name, email, access_level, worker_id, is_active FROM users WHERE user_id = ?',
|
||||
'SELECT user_id, username, name, email, access_level, worker_id, is_active FROM Users WHERE user_id = ?',
|
||||
[userId]
|
||||
);
|
||||
|
||||
@@ -786,7 +786,7 @@ router.delete('/users/:id', verifyToken, async (req, res) => {
|
||||
|
||||
// 사용자 존재 확인
|
||||
const [existing] = await connection.execute(
|
||||
'SELECT username FROM users WHERE user_id = ?',
|
||||
'SELECT username FROM Users WHERE user_id = ?',
|
||||
[userId]
|
||||
);
|
||||
|
||||
|
||||
297
api.hyungi.net/routes/systemRoutes.js
Normal file
297
api.hyungi.net/routes/systemRoutes.js
Normal file
@@ -0,0 +1,297 @@
|
||||
// 시스템 관리 라우트
|
||||
const express = require('express');
|
||||
const router = express.Router();
|
||||
const systemController = require('../controllers/systemController');
|
||||
const { verifyToken } = require('../middlewares/authMiddleware');
|
||||
|
||||
// 시스템 권한 확인 미들웨어
|
||||
const requireSystemAccess = (req, res, next) => {
|
||||
if (!req.user || req.user.role !== 'system') {
|
||||
return res.status(403).json({
|
||||
success: false,
|
||||
error: '시스템 관리자 권한이 필요합니다.'
|
||||
});
|
||||
}
|
||||
next();
|
||||
};
|
||||
|
||||
// 모든 라우트에 인증 및 시스템 권한 확인 적용
|
||||
router.use(verifyToken);
|
||||
router.use(requireSystemAccess);
|
||||
|
||||
// ===== 시스템 상태 관련 =====
|
||||
|
||||
/**
|
||||
* GET /api/system/status
|
||||
* 시스템 전체 상태 확인
|
||||
*/
|
||||
router.get('/status', systemController.getSystemStatus);
|
||||
|
||||
/**
|
||||
* GET /api/system/db-status
|
||||
* 데이터베이스 상태 확인
|
||||
*/
|
||||
router.get('/db-status', systemController.getDatabaseStatus);
|
||||
|
||||
/**
|
||||
* GET /api/system/alerts
|
||||
* 시스템 알림 조회
|
||||
*/
|
||||
router.get('/alerts', systemController.getSystemAlerts);
|
||||
|
||||
/**
|
||||
* GET /api/system/recent-activities
|
||||
* 최근 시스템 활동 조회
|
||||
*/
|
||||
router.get('/recent-activities', systemController.getRecentActivities);
|
||||
|
||||
// ===== 사용자 관리 관련 =====
|
||||
|
||||
/**
|
||||
* GET /api/system/users/stats
|
||||
* 사용자 통계 조회
|
||||
*/
|
||||
router.get('/users/stats', systemController.getUserStats);
|
||||
|
||||
/**
|
||||
* GET /api/system/users
|
||||
* 모든 사용자 목록 조회
|
||||
*/
|
||||
router.get('/users', systemController.getAllUsers);
|
||||
|
||||
/**
|
||||
* POST /api/system/users
|
||||
* 새 사용자 생성
|
||||
*/
|
||||
router.post('/users', systemController.createUser);
|
||||
|
||||
/**
|
||||
* PUT /api/system/users/:id
|
||||
* 사용자 정보 수정
|
||||
*/
|
||||
router.put('/users/:id', systemController.updateUser);
|
||||
|
||||
/**
|
||||
* DELETE /api/system/users/:id
|
||||
* 사용자 삭제
|
||||
*/
|
||||
router.delete('/users/:id', systemController.deleteUser);
|
||||
|
||||
/**
|
||||
* POST /api/system/users/:id/reset-password
|
||||
* 사용자 비밀번호 재설정
|
||||
*/
|
||||
router.post('/users/:id/reset-password', systemController.resetUserPassword);
|
||||
|
||||
// ===== 시스템 로그 관련 =====
|
||||
|
||||
/**
|
||||
* GET /api/system/logs/login
|
||||
* 로그인 로그 조회
|
||||
*/
|
||||
router.get('/logs/login', async (req, res) => {
|
||||
try {
|
||||
const { getDb } = require('../dbPool');
|
||||
const db = await getDb();
|
||||
|
||||
const { page = 1, limit = 50, status, user_id, start_date, end_date } = req.query;
|
||||
const offset = (page - 1) * limit;
|
||||
|
||||
let whereClause = '1=1';
|
||||
const params = [];
|
||||
|
||||
if (status) {
|
||||
whereClause += ' AND ll.login_status = ?';
|
||||
params.push(status);
|
||||
}
|
||||
|
||||
if (user_id) {
|
||||
whereClause += ' AND ll.user_id = ?';
|
||||
params.push(user_id);
|
||||
}
|
||||
|
||||
if (start_date) {
|
||||
whereClause += ' AND ll.login_time >= ?';
|
||||
params.push(start_date);
|
||||
}
|
||||
|
||||
if (end_date) {
|
||||
whereClause += ' AND ll.login_time <= ?';
|
||||
params.push(end_date);
|
||||
}
|
||||
|
||||
const [logs] = await db.query(`
|
||||
SELECT
|
||||
ll.log_id,
|
||||
ll.user_id,
|
||||
u.username,
|
||||
u.name,
|
||||
ll.login_time,
|
||||
ll.ip_address,
|
||||
ll.user_agent,
|
||||
ll.login_status,
|
||||
ll.failure_reason
|
||||
FROM login_logs ll
|
||||
LEFT JOIN Users u ON ll.user_id = u.user_id
|
||||
WHERE ${whereClause}
|
||||
ORDER BY ll.login_time DESC
|
||||
LIMIT ? OFFSET ?
|
||||
`, [...params, parseInt(limit), parseInt(offset)]);
|
||||
|
||||
const [totalCount] = await db.query(`
|
||||
SELECT COUNT(*) as count
|
||||
FROM login_logs ll
|
||||
WHERE ${whereClause}
|
||||
`, params);
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
data: {
|
||||
logs,
|
||||
pagination: {
|
||||
page: parseInt(page),
|
||||
limit: parseInt(limit),
|
||||
total: totalCount[0].count,
|
||||
pages: Math.ceil(totalCount[0].count / limit)
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
} catch (error) {
|
||||
console.error('로그인 로그 조회 오류:', error);
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
error: '로그인 로그를 조회할 수 없습니다.'
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* GET /api/system/logs/password-changes
|
||||
* 비밀번호 변경 로그 조회
|
||||
*/
|
||||
router.get('/logs/password-changes', async (req, res) => {
|
||||
try {
|
||||
const { getDb } = require('../dbPool');
|
||||
const db = await getDb();
|
||||
|
||||
const { page = 1, limit = 50 } = req.query;
|
||||
const offset = (page - 1) * limit;
|
||||
|
||||
const [logs] = await db.query(`
|
||||
SELECT
|
||||
pcl.log_id,
|
||||
pcl.user_id,
|
||||
u.username,
|
||||
u.name,
|
||||
pcl.changed_by_user_id,
|
||||
admin.username as changed_by_username,
|
||||
admin.name as changed_by_name,
|
||||
pcl.changed_at,
|
||||
pcl.change_type,
|
||||
pcl.ip_address
|
||||
FROM password_change_logs pcl
|
||||
LEFT JOIN Users u ON pcl.user_id = u.user_id
|
||||
LEFT JOIN Users admin ON pcl.changed_by_user_id = admin.user_id
|
||||
ORDER BY pcl.changed_at DESC
|
||||
LIMIT ? OFFSET ?
|
||||
`, [parseInt(limit), parseInt(offset)]);
|
||||
|
||||
const [totalCount] = await db.query('SELECT COUNT(*) as count FROM password_change_logs');
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
data: {
|
||||
logs,
|
||||
pagination: {
|
||||
page: parseInt(page),
|
||||
limit: parseInt(limit),
|
||||
total: totalCount[0].count,
|
||||
pages: Math.ceil(totalCount[0].count / limit)
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
} catch (error) {
|
||||
console.error('비밀번호 변경 로그 조회 오류:', error);
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
error: '비밀번호 변경 로그를 조회할 수 없습니다.'
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* GET /api/system/logs/activity
|
||||
* 활동 로그 조회 (activity_logs 테이블이 있는 경우)
|
||||
*/
|
||||
router.get('/logs/activity', async (req, res) => {
|
||||
try {
|
||||
const { getDb } = require('../dbPool');
|
||||
const db = await getDb();
|
||||
|
||||
const { page = 1, limit = 50, activity_type, user_id } = req.query;
|
||||
const offset = (page - 1) * limit;
|
||||
|
||||
let whereClause = '1=1';
|
||||
const params = [];
|
||||
|
||||
if (activity_type) {
|
||||
whereClause += ' AND al.activity_type = ?';
|
||||
params.push(activity_type);
|
||||
}
|
||||
|
||||
if (user_id) {
|
||||
whereClause += ' AND al.user_id = ?';
|
||||
params.push(user_id);
|
||||
}
|
||||
|
||||
const [logs] = await db.query(`
|
||||
SELECT
|
||||
al.log_id,
|
||||
al.user_id,
|
||||
u.username,
|
||||
u.name,
|
||||
al.activity_type,
|
||||
al.table_name,
|
||||
al.record_id,
|
||||
al.action,
|
||||
al.ip_address,
|
||||
al.user_agent,
|
||||
al.created_at
|
||||
FROM activity_logs al
|
||||
LEFT JOIN Users u ON al.user_id = u.user_id
|
||||
WHERE ${whereClause}
|
||||
ORDER BY al.created_at DESC
|
||||
LIMIT ? OFFSET ?
|
||||
`, [...params, parseInt(limit), parseInt(offset)]);
|
||||
|
||||
const [totalCount] = await db.query(`
|
||||
SELECT COUNT(*) as count
|
||||
FROM activity_logs al
|
||||
WHERE ${whereClause}
|
||||
`, params);
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
data: {
|
||||
logs,
|
||||
pagination: {
|
||||
page: parseInt(page),
|
||||
limit: parseInt(limit),
|
||||
total: totalCount[0].count,
|
||||
pages: Math.ceil(totalCount[0].count / limit)
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
} catch (error) {
|
||||
console.error('활동 로그 조회 오류:', error);
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
error: '활동 로그를 조회할 수 없습니다.'
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
module.exports = router;
|
||||
@@ -57,7 +57,7 @@ const loginService = async (username, password, ipAddress, userAgent) => {
|
||||
|
||||
|
||||
const token = jwt.sign(
|
||||
{ user_id: user.user_id, username: user.username, access_level: user.access_level, worker_id: user.worker_id, name: user.name || user.username },
|
||||
{ user_id: user.user_id, username: user.username, role: user.role, access_level: user.access_level, worker_id: user.worker_id, name: user.name || user.username },
|
||||
process.env.JWT_SECRET || 'your-secret-key',
|
||||
{ expiresIn: process.env.JWT_EXPIRES_IN || '24h' }
|
||||
);
|
||||
@@ -80,6 +80,7 @@ const loginService = async (username, password, ipAddress, userAgent) => {
|
||||
user_id: user.user_id,
|
||||
username: user.username,
|
||||
name: user.name || user.username,
|
||||
role: user.role,
|
||||
access_level: user.access_level,
|
||||
worker_id: user.worker_id
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user