- 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>
306 lines
6.7 KiB
JavaScript
306 lines
6.7 KiB
JavaScript
import express from 'express';
|
|
import { query } from '../config.js';
|
|
import { asyncHandler } from '../middleware/errorHandler.js';
|
|
import { requireAuth, requireSuperAdmin } from '../middleware/auth.js';
|
|
import AuditLog from '../models/AuditLog.js';
|
|
|
|
const router = express.Router();
|
|
|
|
/**
|
|
* GET /api/llm-config
|
|
* 取得當前 LLM 配置(所有使用者可見)
|
|
*/
|
|
router.get('/', requireAuth, asyncHandler(async (req, res) => {
|
|
const configs = await query(
|
|
`SELECT id, provider, api_url, model_name, is_active, created_at, updated_at
|
|
FROM llm_configs
|
|
ORDER BY is_active DESC, created_at DESC`
|
|
);
|
|
|
|
res.json({
|
|
success: true,
|
|
data: configs
|
|
});
|
|
}));
|
|
|
|
/**
|
|
* GET /api/llm-config/active
|
|
* 取得當前啟用的 LLM 配置
|
|
*/
|
|
router.get('/active', requireAuth, asyncHandler(async (req, res) => {
|
|
const [config] = await query(
|
|
`SELECT id, provider, api_url, model_name, temperature, max_tokens, timeout
|
|
FROM llm_configs
|
|
WHERE is_active = 1
|
|
LIMIT 1`
|
|
);
|
|
|
|
if (!config) {
|
|
return res.status(404).json({
|
|
success: false,
|
|
error: '未找到啟用的 LLM 配置'
|
|
});
|
|
}
|
|
|
|
res.json({
|
|
success: true,
|
|
data: config
|
|
});
|
|
}));
|
|
|
|
/**
|
|
* POST /api/llm-config
|
|
* 新增 LLM 配置(僅管理員)
|
|
*/
|
|
router.post('/', requireSuperAdmin, asyncHandler(async (req, res) => {
|
|
const {
|
|
provider,
|
|
api_url,
|
|
api_key,
|
|
model_name,
|
|
temperature,
|
|
max_tokens,
|
|
timeout
|
|
} = req.body;
|
|
|
|
// 驗證必填欄位
|
|
if (!provider || !api_url || !model_name) {
|
|
return res.status(400).json({
|
|
success: false,
|
|
error: '請填寫所有必填欄位'
|
|
});
|
|
}
|
|
|
|
const result = await query(
|
|
`INSERT INTO llm_configs
|
|
(provider, api_url, api_key, model_name, temperature, max_tokens, timeout)
|
|
VALUES (?, ?, ?, ?, ?, ?, ?)`,
|
|
[
|
|
provider,
|
|
api_url,
|
|
api_key || null,
|
|
model_name,
|
|
temperature || 0.7,
|
|
max_tokens || 6000,
|
|
timeout || 120000
|
|
]
|
|
);
|
|
|
|
// 記錄稽核日誌
|
|
await AuditLog.logCreate(
|
|
req.session.userId,
|
|
'llm_config',
|
|
result.insertId,
|
|
{ provider, model_name },
|
|
req.ip,
|
|
req.get('user-agent')
|
|
);
|
|
|
|
res.json({
|
|
success: true,
|
|
message: '已新增 LLM 配置',
|
|
data: { id: result.insertId }
|
|
});
|
|
}));
|
|
|
|
/**
|
|
* PUT /api/llm-config/:id
|
|
* 更新 LLM 配置(僅管理員)
|
|
*/
|
|
router.put('/:id', requireSuperAdmin, asyncHandler(async (req, res) => {
|
|
const configId = parseInt(req.params.id);
|
|
const {
|
|
provider,
|
|
api_url,
|
|
api_key,
|
|
model_name,
|
|
temperature,
|
|
max_tokens,
|
|
timeout
|
|
} = req.body;
|
|
|
|
// 驗證必填欄位
|
|
if (!provider || !api_url || !model_name) {
|
|
return res.status(400).json({
|
|
success: false,
|
|
error: '請填寫所有必填欄位'
|
|
});
|
|
}
|
|
|
|
// 檢查配置是否存在
|
|
const [existing] = await query('SELECT id FROM llm_configs WHERE id = ?', [configId]);
|
|
if (!existing) {
|
|
return res.status(404).json({
|
|
success: false,
|
|
error: '找不到此 LLM 配置'
|
|
});
|
|
}
|
|
|
|
await query(
|
|
`UPDATE llm_configs
|
|
SET provider = ?, api_url = ?, api_key = ?, model_name = ?,
|
|
temperature = ?, max_tokens = ?, timeout = ?, updated_at = NOW()
|
|
WHERE id = ?`,
|
|
[
|
|
provider,
|
|
api_url,
|
|
api_key || null,
|
|
model_name,
|
|
temperature || 0.7,
|
|
max_tokens || 6000,
|
|
timeout || 120000,
|
|
configId
|
|
]
|
|
);
|
|
|
|
// 記錄稽核日誌
|
|
await AuditLog.logUpdate(
|
|
req.session.userId,
|
|
'llm_config',
|
|
configId,
|
|
{},
|
|
{ provider, model_name },
|
|
req.ip,
|
|
req.get('user-agent')
|
|
);
|
|
|
|
res.json({
|
|
success: true,
|
|
message: '已更新 LLM 配置'
|
|
});
|
|
}));
|
|
|
|
/**
|
|
* PUT /api/llm-config/:id/activate
|
|
* 啟用特定 LLM 配置(僅管理員)
|
|
*/
|
|
router.put('/:id/activate', requireSuperAdmin, asyncHandler(async (req, res) => {
|
|
const configId = parseInt(req.params.id);
|
|
|
|
// 檢查配置是否存在
|
|
const [existing] = await query('SELECT id, provider FROM llm_configs WHERE id = ?', [configId]);
|
|
if (!existing) {
|
|
return res.status(404).json({
|
|
success: false,
|
|
error: '找不到此 LLM 配置'
|
|
});
|
|
}
|
|
|
|
// 先停用所有配置
|
|
await query('UPDATE llm_configs SET is_active = 0');
|
|
|
|
// 啟用指定配置
|
|
await query('UPDATE llm_configs SET is_active = 1, updated_at = NOW() WHERE id = ?', [configId]);
|
|
|
|
// 記錄稽核日誌
|
|
await AuditLog.logUpdate(
|
|
req.session.userId,
|
|
'llm_config',
|
|
configId,
|
|
{ is_active: 0 },
|
|
{ is_active: 1 },
|
|
req.ip,
|
|
req.get('user-agent')
|
|
);
|
|
|
|
res.json({
|
|
success: true,
|
|
message: `已啟用 ${existing.provider} 配置`
|
|
});
|
|
}));
|
|
|
|
/**
|
|
* DELETE /api/llm-config/:id
|
|
* 刪除 LLM 配置(僅管理員)
|
|
*/
|
|
router.delete('/:id', requireSuperAdmin, asyncHandler(async (req, res) => {
|
|
const configId = parseInt(req.params.id);
|
|
|
|
// 檢查是否為啟用中的配置
|
|
const [existing] = await query('SELECT is_active FROM llm_configs WHERE id = ?', [configId]);
|
|
if (!existing) {
|
|
return res.status(404).json({
|
|
success: false,
|
|
error: '找不到此 LLM 配置'
|
|
});
|
|
}
|
|
|
|
if (existing.is_active) {
|
|
return res.status(400).json({
|
|
success: false,
|
|
error: '無法刪除啟用中的配置'
|
|
});
|
|
}
|
|
|
|
await query('DELETE FROM llm_configs WHERE id = ?', [configId]);
|
|
|
|
// 記錄稽核日誌
|
|
await AuditLog.logDelete(
|
|
req.session.userId,
|
|
'llm_config',
|
|
configId,
|
|
{},
|
|
req.ip,
|
|
req.get('user-agent')
|
|
);
|
|
|
|
res.json({
|
|
success: true,
|
|
message: '已刪除 LLM 配置'
|
|
});
|
|
}));
|
|
|
|
/**
|
|
* POST /api/llm-config/test
|
|
* 測試 LLM 配置連線(僅管理員)
|
|
*/
|
|
router.post('/test', requireSuperAdmin, asyncHandler(async (req, res) => {
|
|
const { api_url, api_key, model_name } = req.body;
|
|
|
|
if (!api_url || !model_name) {
|
|
return res.status(400).json({
|
|
success: false,
|
|
error: '請提供 API 端點和模型名稱'
|
|
});
|
|
}
|
|
|
|
try {
|
|
const axios = (await import('axios')).default;
|
|
|
|
const response = await axios.post(
|
|
`${api_url}/v1/chat/completions`,
|
|
{
|
|
model: model_name,
|
|
messages: [
|
|
{ role: 'user', content: 'Hello' }
|
|
],
|
|
max_tokens: 10
|
|
},
|
|
{
|
|
timeout: 10000,
|
|
headers: {
|
|
'Content-Type': 'application/json',
|
|
...(api_key && { 'Authorization': `Bearer ${api_key}` })
|
|
}
|
|
}
|
|
);
|
|
|
|
if (response.data && response.data.choices) {
|
|
res.json({
|
|
success: true,
|
|
message: 'LLM API 連線測試成功'
|
|
});
|
|
} else {
|
|
throw new Error('Invalid API response format');
|
|
}
|
|
} catch (error) {
|
|
res.status(500).json({
|
|
success: false,
|
|
error: 'LLM API 連線測試失敗',
|
|
message: error.message
|
|
});
|
|
}
|
|
}));
|
|
|
|
export default router;
|