feat: Implement role-based access control (RBAC) with 3-tier authorization
- 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>
This commit is contained in:
@@ -1,7 +1,7 @@
|
||||
import express from 'express';
|
||||
import { query } from '../config.js';
|
||||
import { asyncHandler } from '../middleware/errorHandler.js';
|
||||
import { requireAuth, requireAdmin } from '../middleware/auth.js';
|
||||
import { requireAuth, requireSuperAdmin } from '../middleware/auth.js';
|
||||
import AuditLog from '../models/AuditLog.js';
|
||||
|
||||
const router = express.Router();
|
||||
@@ -12,7 +12,7 @@ const router = express.Router();
|
||||
*/
|
||||
router.get('/', requireAuth, asyncHandler(async (req, res) => {
|
||||
const configs = await query(
|
||||
`SELECT id, provider_name, model_name, is_active, created_at, updated_at
|
||||
`SELECT id, provider, api_url, model_name, is_active, created_at, updated_at
|
||||
FROM llm_configs
|
||||
ORDER BY is_active DESC, created_at DESC`
|
||||
);
|
||||
@@ -29,7 +29,7 @@ router.get('/', requireAuth, asyncHandler(async (req, res) => {
|
||||
*/
|
||||
router.get('/active', requireAuth, asyncHandler(async (req, res) => {
|
||||
const [config] = await query(
|
||||
`SELECT id, provider_name, api_endpoint, model_name, temperature, max_tokens, timeout_seconds
|
||||
`SELECT id, provider, api_url, model_name, temperature, max_tokens, timeout
|
||||
FROM llm_configs
|
||||
WHERE is_active = 1
|
||||
LIMIT 1`
|
||||
@@ -52,19 +52,19 @@ router.get('/active', requireAuth, asyncHandler(async (req, res) => {
|
||||
* POST /api/llm-config
|
||||
* 新增 LLM 配置(僅管理員)
|
||||
*/
|
||||
router.post('/', requireAdmin, asyncHandler(async (req, res) => {
|
||||
router.post('/', requireSuperAdmin, asyncHandler(async (req, res) => {
|
||||
const {
|
||||
provider_name,
|
||||
api_endpoint,
|
||||
provider,
|
||||
api_url,
|
||||
api_key,
|
||||
model_name,
|
||||
temperature,
|
||||
max_tokens,
|
||||
timeout_seconds
|
||||
timeout
|
||||
} = req.body;
|
||||
|
||||
// 驗證必填欄位
|
||||
if (!provider_name || !api_endpoint || !model_name) {
|
||||
if (!provider || !api_url || !model_name) {
|
||||
return res.status(400).json({
|
||||
success: false,
|
||||
error: '請填寫所有必填欄位'
|
||||
@@ -73,16 +73,16 @@ router.post('/', requireAdmin, asyncHandler(async (req, res) => {
|
||||
|
||||
const result = await query(
|
||||
`INSERT INTO llm_configs
|
||||
(provider_name, api_endpoint, api_key, model_name, temperature, max_tokens, timeout_seconds)
|
||||
(provider, api_url, api_key, model_name, temperature, max_tokens, timeout)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?)`,
|
||||
[
|
||||
provider_name,
|
||||
api_endpoint,
|
||||
provider,
|
||||
api_url,
|
||||
api_key || null,
|
||||
model_name,
|
||||
temperature || 0.7,
|
||||
max_tokens || 6000,
|
||||
timeout_seconds || 120
|
||||
timeout || 120000
|
||||
]
|
||||
);
|
||||
|
||||
@@ -91,7 +91,7 @@ router.post('/', requireAdmin, asyncHandler(async (req, res) => {
|
||||
req.session.userId,
|
||||
'llm_config',
|
||||
result.insertId,
|
||||
{ provider_name, model_name },
|
||||
{ provider, model_name },
|
||||
req.ip,
|
||||
req.get('user-agent')
|
||||
);
|
||||
@@ -107,20 +107,20 @@ router.post('/', requireAdmin, asyncHandler(async (req, res) => {
|
||||
* PUT /api/llm-config/:id
|
||||
* 更新 LLM 配置(僅管理員)
|
||||
*/
|
||||
router.put('/:id', requireAdmin, asyncHandler(async (req, res) => {
|
||||
router.put('/:id', requireSuperAdmin, asyncHandler(async (req, res) => {
|
||||
const configId = parseInt(req.params.id);
|
||||
const {
|
||||
provider_name,
|
||||
api_endpoint,
|
||||
provider,
|
||||
api_url,
|
||||
api_key,
|
||||
model_name,
|
||||
temperature,
|
||||
max_tokens,
|
||||
timeout_seconds
|
||||
timeout
|
||||
} = req.body;
|
||||
|
||||
// 驗證必填欄位
|
||||
if (!provider_name || !api_endpoint || !model_name) {
|
||||
if (!provider || !api_url || !model_name) {
|
||||
return res.status(400).json({
|
||||
success: false,
|
||||
error: '請填寫所有必填欄位'
|
||||
@@ -138,17 +138,17 @@ router.put('/:id', requireAdmin, asyncHandler(async (req, res) => {
|
||||
|
||||
await query(
|
||||
`UPDATE llm_configs
|
||||
SET provider_name = ?, api_endpoint = ?, api_key = ?, model_name = ?,
|
||||
temperature = ?, max_tokens = ?, timeout_seconds = ?, updated_at = NOW()
|
||||
SET provider = ?, api_url = ?, api_key = ?, model_name = ?,
|
||||
temperature = ?, max_tokens = ?, timeout = ?, updated_at = NOW()
|
||||
WHERE id = ?`,
|
||||
[
|
||||
provider_name,
|
||||
api_endpoint,
|
||||
provider,
|
||||
api_url,
|
||||
api_key || null,
|
||||
model_name,
|
||||
temperature || 0.7,
|
||||
max_tokens || 6000,
|
||||
timeout_seconds || 120,
|
||||
timeout || 120000,
|
||||
configId
|
||||
]
|
||||
);
|
||||
@@ -159,7 +159,7 @@ router.put('/:id', requireAdmin, asyncHandler(async (req, res) => {
|
||||
'llm_config',
|
||||
configId,
|
||||
{},
|
||||
{ provider_name, model_name },
|
||||
{ provider, model_name },
|
||||
req.ip,
|
||||
req.get('user-agent')
|
||||
);
|
||||
@@ -174,11 +174,11 @@ router.put('/:id', requireAdmin, asyncHandler(async (req, res) => {
|
||||
* PUT /api/llm-config/:id/activate
|
||||
* 啟用特定 LLM 配置(僅管理員)
|
||||
*/
|
||||
router.put('/:id/activate', requireAdmin, asyncHandler(async (req, res) => {
|
||||
router.put('/:id/activate', requireSuperAdmin, asyncHandler(async (req, res) => {
|
||||
const configId = parseInt(req.params.id);
|
||||
|
||||
// 檢查配置是否存在
|
||||
const [existing] = await query('SELECT id, provider_name FROM llm_configs WHERE id = ?', [configId]);
|
||||
const [existing] = await query('SELECT id, provider FROM llm_configs WHERE id = ?', [configId]);
|
||||
if (!existing) {
|
||||
return res.status(404).json({
|
||||
success: false,
|
||||
@@ -205,7 +205,7 @@ router.put('/:id/activate', requireAdmin, asyncHandler(async (req, res) => {
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
message: `已啟用 ${existing.provider_name} 配置`
|
||||
message: `已啟用 ${existing.provider} 配置`
|
||||
});
|
||||
}));
|
||||
|
||||
@@ -213,7 +213,7 @@ router.put('/:id/activate', requireAdmin, asyncHandler(async (req, res) => {
|
||||
* DELETE /api/llm-config/:id
|
||||
* 刪除 LLM 配置(僅管理員)
|
||||
*/
|
||||
router.delete('/:id', requireAdmin, asyncHandler(async (req, res) => {
|
||||
router.delete('/:id', requireSuperAdmin, asyncHandler(async (req, res) => {
|
||||
const configId = parseInt(req.params.id);
|
||||
|
||||
// 檢查是否為啟用中的配置
|
||||
@@ -254,10 +254,10 @@ router.delete('/:id', requireAdmin, asyncHandler(async (req, res) => {
|
||||
* POST /api/llm-config/test
|
||||
* 測試 LLM 配置連線(僅管理員)
|
||||
*/
|
||||
router.post('/test', requireAdmin, asyncHandler(async (req, res) => {
|
||||
const { api_endpoint, api_key, model_name } = req.body;
|
||||
router.post('/test', requireSuperAdmin, asyncHandler(async (req, res) => {
|
||||
const { api_url, api_key, model_name } = req.body;
|
||||
|
||||
if (!api_endpoint || !model_name) {
|
||||
if (!api_url || !model_name) {
|
||||
return res.status(400).json({
|
||||
success: false,
|
||||
error: '請提供 API 端點和模型名稱'
|
||||
@@ -268,7 +268,7 @@ router.post('/test', requireAdmin, asyncHandler(async (req, res) => {
|
||||
const axios = (await import('axios')).default;
|
||||
|
||||
const response = await axios.post(
|
||||
`${api_endpoint}/v1/chat/completions`,
|
||||
`${api_url}/v1/chat/completions`,
|
||||
{
|
||||
model: model_name,
|
||||
messages: [
|
||||
|
||||
Reference in New Issue
Block a user