- Add 3 user roles: user, admin, super_admin - Restrict LLM config management to super_admin only - Restrict audit logs and statistics to super_admin only - Update AdminPage with role-based tab visibility - Add complete 5 Why prompt from 5why-analyzer.jsx - Add system documentation and authorization guide - Add ErrorModal component and seed test users script 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
282 lines
6.4 KiB
JavaScript
282 lines
6.4 KiB
JavaScript
import express from 'express';
|
|
import User from '../models/User.js';
|
|
import Analysis from '../models/Analysis.js';
|
|
import AuditLog from '../models/AuditLog.js';
|
|
import { asyncHandler } from '../middleware/errorHandler.js';
|
|
import { requireAdmin, requireSuperAdmin } from '../middleware/auth.js';
|
|
|
|
const router = express.Router();
|
|
|
|
/**
|
|
* GET /api/admin/dashboard
|
|
* 管理者儀表板統計
|
|
*/
|
|
router.get('/dashboard', requireAdmin, asyncHandler(async (req, res) => {
|
|
const stats = await Analysis.getStatistics();
|
|
const userStats = await User.getAll(1, 1000); // 取得所有使用者
|
|
|
|
const totalUsers = userStats.pagination.total;
|
|
const activeUsers = userStats.data.filter(u => u.is_active).length;
|
|
|
|
res.json({
|
|
success: true,
|
|
data: {
|
|
totalUsers,
|
|
activeUsers,
|
|
totalAnalyses: stats.total,
|
|
completedAnalyses: stats.completed,
|
|
failedAnalyses: stats.failed,
|
|
processingAnalyses: stats.processing,
|
|
avgProcessingTime: Math.round(stats.avg_processing_time) || 0,
|
|
successRate: stats.total > 0 ? ((stats.completed / stats.total) * 100).toFixed(1) : 0
|
|
}
|
|
});
|
|
}));
|
|
|
|
/**
|
|
* GET /api/admin/users
|
|
* 取得所有使用者
|
|
*/
|
|
router.get('/users', requireAdmin, asyncHandler(async (req, res) => {
|
|
const page = parseInt(req.query.page) || 1;
|
|
const limit = parseInt(req.query.limit) || 10;
|
|
const filters = {
|
|
role: req.query.role,
|
|
is_active: req.query.is_active !== undefined ? req.query.is_active === 'true' : undefined,
|
|
search: req.query.search
|
|
};
|
|
|
|
const result = await User.getAll(page, limit, filters);
|
|
|
|
res.json({
|
|
success: true,
|
|
...result
|
|
});
|
|
}));
|
|
|
|
/**
|
|
* POST /api/admin/users
|
|
* 建立新使用者
|
|
*/
|
|
router.post('/users', requireAdmin, asyncHandler(async (req, res) => {
|
|
const { employee_id, username, email, password, role, department, position } = req.body;
|
|
|
|
// 驗證必填欄位
|
|
if (!employee_id || !username || !email || !password) {
|
|
return res.status(400).json({
|
|
success: false,
|
|
error: '請填寫所有必填欄位'
|
|
});
|
|
}
|
|
|
|
// 驗證 role
|
|
const validRoles = ['user', 'admin', 'super_admin'];
|
|
if (role && !validRoles.includes(role)) {
|
|
return res.status(400).json({
|
|
success: false,
|
|
error: '無效的權限等級'
|
|
});
|
|
}
|
|
|
|
const newUser = await User.create({
|
|
employee_id,
|
|
username,
|
|
email,
|
|
password,
|
|
role: role || 'user',
|
|
department,
|
|
position
|
|
});
|
|
|
|
// 記錄稽核日誌
|
|
await AuditLog.logCreate(
|
|
req.session.userId,
|
|
'user',
|
|
newUser.id,
|
|
{ username, email, role: newUser.role },
|
|
req.ip,
|
|
req.get('user-agent')
|
|
);
|
|
|
|
res.status(201).json({
|
|
success: true,
|
|
message: '使用者已建立',
|
|
data: newUser
|
|
});
|
|
}));
|
|
|
|
/**
|
|
* PUT /api/admin/users/:id
|
|
* 更新使用者
|
|
*/
|
|
router.put('/users/:id', requireAdmin, asyncHandler(async (req, res) => {
|
|
const userId = parseInt(req.params.id);
|
|
const { username, email, role, department, position, is_active } = req.body;
|
|
|
|
const oldUser = await User.findById(userId);
|
|
if (!oldUser) {
|
|
return res.status(404).json({
|
|
success: false,
|
|
error: '使用者不存在'
|
|
});
|
|
}
|
|
|
|
const updatedUser = await User.update(userId, {
|
|
username,
|
|
email,
|
|
role,
|
|
department,
|
|
position,
|
|
is_active
|
|
});
|
|
|
|
// 記錄稽核日誌
|
|
await AuditLog.logUpdate(
|
|
req.session.userId,
|
|
'user',
|
|
userId,
|
|
{ username: oldUser.username, role: oldUser.role },
|
|
{ username, role },
|
|
req.ip,
|
|
req.get('user-agent')
|
|
);
|
|
|
|
res.json({
|
|
success: true,
|
|
message: '使用者已更新',
|
|
data: updatedUser
|
|
});
|
|
}));
|
|
|
|
/**
|
|
* DELETE /api/admin/users/:id
|
|
* 停用/刪除使用者
|
|
*/
|
|
router.delete('/users/:id', requireSuperAdmin, asyncHandler(async (req, res) => {
|
|
const userId = parseInt(req.params.id);
|
|
const hard = req.query.hard === 'true';
|
|
|
|
const user = await User.findById(userId);
|
|
if (!user) {
|
|
return res.status(404).json({
|
|
success: false,
|
|
error: '使用者不存在'
|
|
});
|
|
}
|
|
|
|
// 不能刪除自己
|
|
if (userId === req.session.userId) {
|
|
return res.status(400).json({
|
|
success: false,
|
|
error: '無法刪除自己的帳號'
|
|
});
|
|
}
|
|
|
|
if (hard) {
|
|
await User.hardDelete(userId);
|
|
} else {
|
|
await User.delete(userId);
|
|
}
|
|
|
|
// 記錄稽核日誌
|
|
await AuditLog.logDelete(
|
|
req.session.userId,
|
|
'user',
|
|
userId,
|
|
{ username: user.username, email: user.email },
|
|
req.ip,
|
|
req.get('user-agent')
|
|
);
|
|
|
|
res.json({
|
|
success: true,
|
|
message: hard ? '使用者已刪除' : '使用者已停用'
|
|
});
|
|
}));
|
|
|
|
/**
|
|
* GET /api/admin/analyses
|
|
* 取得所有分析記錄
|
|
*/
|
|
router.get('/analyses', requireAdmin, asyncHandler(async (req, res) => {
|
|
const page = parseInt(req.query.page) || 1;
|
|
const limit = parseInt(req.query.limit) || 10;
|
|
const filters = {
|
|
status: req.query.status,
|
|
user_id: req.query.user_id,
|
|
search: req.query.search
|
|
};
|
|
|
|
const result = await Analysis.getAll(page, limit, filters);
|
|
|
|
res.json({
|
|
success: true,
|
|
...result
|
|
});
|
|
}));
|
|
|
|
/**
|
|
* GET /api/admin/audit-logs
|
|
* 取得稽核日誌
|
|
*/
|
|
router.get('/audit-logs', requireSuperAdmin, asyncHandler(async (req, res) => {
|
|
const page = parseInt(req.query.page) || 1;
|
|
const limit = parseInt(req.query.limit) || 50;
|
|
const filters = {
|
|
user_id: req.query.user_id,
|
|
action: req.query.action,
|
|
entity_type: req.query.entity_type,
|
|
status: req.query.status,
|
|
date_from: req.query.date_from,
|
|
date_to: req.query.date_to
|
|
};
|
|
|
|
const result = await AuditLog.getAll(page, limit, filters);
|
|
|
|
res.json({
|
|
success: true,
|
|
...result
|
|
});
|
|
}));
|
|
|
|
/**
|
|
* GET /api/admin/statistics
|
|
* 取得完整統計資料
|
|
*/
|
|
router.get('/statistics', requireSuperAdmin, asyncHandler(async (req, res) => {
|
|
const overallStats = await Analysis.getStatistics();
|
|
const users = await User.getAll(1, 1000);
|
|
|
|
// 計算各部門統計
|
|
const departmentStats = users.data.reduce((acc, user) => {
|
|
const dept = user.department || '未分類';
|
|
if (!acc[dept]) {
|
|
acc[dept] = { total: 0, active: 0 };
|
|
}
|
|
acc[dept].total++;
|
|
if (user.is_active) acc[dept].active++;
|
|
return acc;
|
|
}, {});
|
|
|
|
// 計算權限統計
|
|
const roleStats = users.data.reduce((acc, user) => {
|
|
const role = user.role || 'user';
|
|
acc[role] = (acc[role] || 0) + 1;
|
|
return acc;
|
|
}, {});
|
|
|
|
res.json({
|
|
success: true,
|
|
data: {
|
|
overall: overallStats,
|
|
users: {
|
|
total: users.pagination.total,
|
|
byDepartment: departmentStats,
|
|
byRole: roleStats
|
|
}
|
|
}
|
|
});
|
|
}));
|
|
|
|
export default router;
|