Initial commit: API Debug Tool

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
jason
2026-03-12 04:32:35 +08:00
commit 96bdc292bb
42 changed files with 8577 additions and 0 deletions

86
server/routes/auth.js Normal file
View File

@@ -0,0 +1,86 @@
const express = require('express');
const router = express.Router();
const bcrypt = require('bcryptjs');
const jwt = require('jsonwebtoken');
const db = require('../db');
// JWT Secret from env or default
const JWT_SECRET = process.env.JWT_SECRET || 'super_secret_api_debug_key';
// 用户登录
router.post('/login', async (req, res) => {
try {
const { username, password } = req.body;
if (!username || !password) {
return res.status(400).json({ error: '请输入用户名和密码' });
}
const [users] = await db.query('SELECT * FROM users WHERE username = ?', [username]);
if (users.length === 0) {
return res.status(401).json({ error: '用户名或密码错误' });
}
const user = users[0];
const isMatch = await bcrypt.compare(password, user.password);
if (!isMatch) {
return res.status(401).json({ error: '用户名或密码错误' });
}
// 签发 Token
const token = jwt.sign(
{ id: user.id, username: user.username },
JWT_SECRET,
{ expiresIn: '7d' } // 7天有效
);
res.json({
success: true,
token,
user: { id: user.id, username: user.username }
});
} catch (err) {
console.error('Login error:', err);
res.status(500).json({ error: '服务器内部错误' });
}
});
// 用户注册 (仅供演示,生产环境应限制注册)
router.post('/register', async (req, res) => {
try {
const { username, password } = req.body;
if (!username || !password) {
return res.status(400).json({ error: '请输入用户名和设置密码' });
}
// 检查用户是否已存在
const [existing] = await db.query('SELECT id FROM users WHERE username = ?', [username]);
if (existing.length > 0) {
return res.status(400).json({ error: '该用户名已被使用' });
}
// 密码加密
const salt = await bcrypt.genSalt(10);
const hashedPassword = await bcrypt.hash(password, salt);
// 插入数据库
const [result] = await db.query(
'INSERT INTO users (username, password) VALUES (?, ?)',
[username, hashedPassword]
);
res.status(201).json({
success: true,
message: '注册成功,请去登录'
});
} catch (err) {
console.error('Registration error:', err);
res.status(500).json({ error: '服务器内部错误' });
}
});
module.exports = router;

214
server/routes/debug.js Normal file
View File

@@ -0,0 +1,214 @@
const express = require('express');
const router = express.Router();
const db = require('../db');
const axios = require('axios');
const crypto = require('crypto');
const auth = require('../middleware/auth');
router.use(auth);
/**
* 将私钥(裸 base64 PKCS8 DER 或 PEM加载为 KeyObject
*/
function loadPrivateKey(raw) {
if (!raw) return null;
if (raw.includes('-----BEGIN')) {
return crypto.createPrivateKey(raw);
}
const derBuf = Buffer.from(raw.trim(), 'base64');
return crypto.createPrivateKey({ key: derBuf, format: 'der', type: 'pkcs8' });
}
// 获取 DingTalk Access Token
async function getAccessToken(appKey, appSecret) {
// 1. 检查缓存
const [rows] = await db.query(
'SELECT * FROM token_cache WHERE app_key = ? AND expires_at > NOW()',
[appKey]
);
if (rows.length > 0) {
return rows[0].access_token;
}
// 2. 调用钉钉接口获取
try {
const response = await axios.post('https://api.dingtalk.com/v1.0/oauth2/accessToken', {
appKey,
appSecret
});
const { accessToken, expireIn } = response.data;
// 计算过期时间 (提前 5 分钟过期以保证安全)
const expiresAt = new Date(Date.now() + (expireIn - 300) * 1000);
// 3. 更新缓存
await db.query(
'INSERT INTO token_cache (app_key, access_token, expires_at) VALUES (?, ?, ?) ON DUPLICATE KEY UPDATE access_token = ?, expires_at = ?',
[appKey, accessToken, expiresAt, accessToken, expiresAt]
);
return accessToken;
} catch (err) {
console.error('获取钉钉 Token 失败:', err.response ? err.response.data : err.message);
throw new Error('获取 AccessToken 失败: ' + (err.response?.data?.message || err.message));
}
}
// 旗舰版PP 接口调试转发
router.post('/execute-pp', async (req, res) => {
try {
const { tenantId, endpointId, body, queryParams } = req.body;
if (!tenantId || !endpointId) {
return res.status(400).json({ error: 'tenantId and endpointId are required' });
}
// 1. 获取租户和接口信息(验证归属当前用户)
const [[tenantInfo]] = await db.query('SELECT * FROM tenants WHERE id = ? AND user_id = ?', [tenantId, req.user.username]);
const [[endpointInfo]] = await db.query('SELECT * FROM endpoints WHERE id = ? AND user_id = ?', [endpointId, req.user.username]);
if (!tenantInfo || !endpointInfo) {
return res.status(404).json({ error: '租户或接口信息不存在' });
}
// The endpoint URL is an absolute URL (e.g., https://api.dingtalk.com/...)
if (!endpointInfo.url) {
return res.status(400).json({ error: 'Endpoint url is missing' });
}
let finalUrl = endpointInfo.url;
const headers = {
'Content-Type': 'application/json',
'Accept': 'application/json'
};
let requestConfig = {
method: endpointInfo.method.toLowerCase(),
url: finalUrl,
headers,
timeout: 15000 // 15s timeout
};
// Standard PP Logic
if (tenantInfo.type === '标品PP') {
// 1. Basic Auth using API Key
if (tenantInfo.api_key) {
const encodedKey = Buffer.from(`${tenantInfo.api_key}:`).toString('base64');
headers['Authorization'] = `Basic ${encodedKey}`;
}
// 2. Prepare Query Parameters for Signature
const mergedParams = { ...(queryParams || {}) };
// Auto inject missing parameters
if (!mergedParams.timestamp) mergedParams.timestamp = Date.now().toString();
if (!mergedParams.nonce) {
// 文档要求:字母数字,最多 8 字符
const chars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789';
mergedParams.nonce = Array.from({ length: 8 }, () => chars[Math.floor(Math.random() * chars.length)]).join('');
}
if (!mergedParams.entCode && tenantInfo.ent_code) mergedParams.entCode = tenantInfo.ent_code;
if (!mergedParams.apiCode && endpointInfo.api_code) mergedParams.apiCode = endpointInfo.api_code;
// userName 为非必填,有值时加入签名(文档示例:...&userName=xiao@qq.com
if (!mergedParams.userName && endpointInfo.user_name) mergedParams.userName = endpointInfo.user_name;
// 3. RSA-MD5 Signature Generation
if (tenantInfo.private_key) {
// Collect keys and sort alphabetically
const keys = Object.keys(mergedParams).sort();
const signParts = keys.map(k => `${k}=${mergedParams[k]}`);
const signString = signParts.join('&');
try {
const privateKey = loadPrivateKey(tenantInfo.private_key);
const signGenerator = crypto.createSign('RSA-MD5');
signGenerator.update(signString, 'utf8');
const signatureBase64 = signGenerator.sign(privateKey, 'base64');
// Add signature to params (axios params will URL-encode it automatically)
mergedParams.sign = signatureBase64;
} catch (signErr) {
console.error('Failed to generate signature via RSA-MD5', signErr);
// Continue anyway, it might fail on server but we let them debug it
}
}
requestConfig.params = mergedParams;
} else if (tenantInfo.type === '旗舰版PP') {
// DingTalk specific logic
// 2. 获取 Token
const token = (await getAccessToken(tenantInfo.app_key, tenantInfo.app_secret)).trim();
headers['x-acs-dingtalk-access-token'] = token;
} else {
return res.status(400).json({ error: `不支持的租户类型: ${tenantInfo.type}` });
}
// Apply Data Body
if (requestConfig.method !== 'get' && body && Object.keys(body).length > 0) {
requestConfig.data = body;
}
// 仅供复制 CURL 使用 (近似的命令)
let curlCmd = `curl -X ${endpointInfo.method.toUpperCase()} '${finalUrl}${requestConfig.params ? '?' + new URLSearchParams(requestConfig.params).toString() : ''}'`;
Object.entries(headers).forEach(([k, v]) => {
curlCmd += ` \\\n -H '${k}: ${v}'`;
});
if (requestConfig.data) {
// 格式化输出 JSON
curlCmd += ` \\\n -d '${JSON.stringify(requestConfig.data, null, 2)}'`;
}
console.log('--- [TRACE] 发送请求详情 ---');
console.log('URL:', requestConfig.url);
console.log('Headers:', JSON.stringify(requestConfig.headers, null, 2));
if (requestConfig.params) console.log('Query Params:', JSON.stringify(requestConfig.params, null, 2));
if (requestConfig.data) console.log('Payload:', JSON.stringify(requestConfig.data, null, 2));
console.log('等效 CURL 命令:\n', curlCmd);
// 3. 执行转发请求
const response = await axios(requestConfig);
// 4. 保存历史记录
await db.query(
'INSERT INTO debug_history (tenant_id, endpoint_id, url, curl_cmd, response_data) VALUES (?, ?, ?, ?, ?)',
[tenantInfo.id, endpointInfo.id, finalUrl, curlCmd, JSON.stringify(response.data)]
);
console.log('--- [TRACE] 接口响应成功并已保存历史 ---');
res.json({
curlCmd: curlCmd,
data: response.data
});
} catch (err) {
const errorMsg = err.response ? err.response.data : err.message;
console.error('--- 调试请求失败 ---');
console.error('错误信息:', err.response ? JSON.stringify(errorMsg, null, 2) : errorMsg);
// 失败也保存历史以便排查 (可选,这里为保持简单先不存,或可根据需求扩展)
res.status(err.response ? err.response.status : 500).json({
error: errorMsg
});
}
});
// 获取调试历史
router.get('/history', async (req, res) => {
try {
const [rows] = await db.query(
`SELECT dh.*, t.name as tenant_name, e.name as endpoint_name
FROM debug_history dh
LEFT JOIN tenants t ON dh.tenant_id = t.id
LEFT JOIN endpoints e ON dh.endpoint_id = e.id
WHERE t.user_id = ?
ORDER BY dh.created_at DESC LIMIT 50`,
[req.user.username]
);
res.json(rows);
} catch (err) {
res.status(500).json({ error: err.message });
}
});
module.exports = router;

View File

@@ -0,0 +1,68 @@
const express = require('express');
const router = express.Router();
const db = require('../db');
const auth = require('../middleware/auth');
router.use(auth);
// 获取所有接口 (含搜索)
router.get('/', async (req, res) => {
try {
const { search } = req.query;
let query = 'SELECT * FROM endpoints WHERE user_id = ?';
let params = [req.user.username];
if (search) {
query += ' AND name LIKE ?';
params.push(`%${search}%`);
}
query += ' ORDER BY created_at DESC';
const [rows] = await db.query(query, params);
res.json(rows);
} catch (err) {
res.status(500).json({ error: err.message });
}
});
// 新增接口
router.post('/', async (req, res) => {
try {
const { name, url, method, body, category, api_code, user_name, module } = req.body;
const [result] = await db.query(
'INSERT INTO endpoints (user_id, name, url, method, body, category, api_code, user_name, module) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)',
[req.user.username, name, url, method, body || null, category || '旗舰版PP', api_code || null, user_name || null, module || null]
);
res.json({ id: result.insertId, name, url, method, body, category: category || '旗舰版PP', api_code: api_code || null, user_name: user_name || null, module: module || null });
} catch (err) {
res.status(500).json({ error: err.message });
}
});
// 更新接口
router.put('/:id', async (req, res) => {
try {
const { id } = req.params;
const { name, url, method, body, category, api_code, user_name, module } = req.body;
await db.query(
'UPDATE endpoints SET name = ?, url = ?, method = ?, body = ?, category = ?, api_code = ?, user_name = ?, module = ? WHERE id = ? AND user_id = ?',
[name, url, method, body || null, category || '旗舰版PP', api_code || null, user_name || null, module || null, id, req.user.username]
);
res.json({ message: 'Updated successfully' });
} catch (err) {
res.status(500).json({ error: err.message });
}
});
// 删除接口
router.delete('/:id', async (req, res) => {
try {
const { id } = req.params;
await db.query('DELETE FROM endpoints WHERE id = ? AND user_id = ?', [id, req.user.username]);
res.json({ message: 'Deleted successfully' });
} catch (err) {
res.status(500).json({ error: err.message });
}
});
module.exports = router;

71
server/routes/settings.js Normal file
View File

@@ -0,0 +1,71 @@
const express = require('express');
const router = express.Router();
const db = require('../db');
const auth = require('../middleware/auth');
router.use(auth);
const DEFAULT_CATEGORIES = ['旗舰版PP', '旗舰版ATS', '标品ATS', '标品PP', '国际版ATS'];
const DEFAULT_MODULES = ['组织接口', '职位职务接口', '人事接口', '假勤接口', '薪酬接口', '绩效接口', '自定义分组接口', 'BI 报表接口'];
async function getSetting(key, defaults) {
const [[row]] = await db.query('SELECT value FROM settings WHERE `key` = ?', [key]);
if (!row) return defaults;
try {
const extra = JSON.parse(row.value);
// 合并:默认值在前,自定义值去重追加
return [...defaults, ...extra.filter(v => !defaults.includes(v))];
} catch { return defaults; }
}
// 获取所有设置
router.get('/', async (req, res) => {
try {
const [categories, modules] = await Promise.all([
getSetting('categories', DEFAULT_CATEGORIES),
getSetting('modules', DEFAULT_MODULES),
]);
res.json({
categories,
modules,
defaultCategories: DEFAULT_CATEGORIES,
defaultModules: DEFAULT_MODULES,
});
} catch (err) {
res.status(500).json({ error: err.message });
}
});
// 更新分类(仅保存自定义项,默认项不入库)
router.put('/categories', async (req, res) => {
try {
const { items } = req.body; // 完整列表(含默认+自定义)
const extra = items.filter(v => !DEFAULT_CATEGORIES.includes(v));
await db.query(
'INSERT INTO settings (`key`, value) VALUES (?, ?) ON DUPLICATE KEY UPDATE value = ?',
['categories', JSON.stringify(extra), JSON.stringify(extra)]
);
res.json({ message: 'OK' });
} catch (err) {
res.status(500).json({ error: err.message });
}
});
// 更新业务模块
router.put('/modules', async (req, res) => {
try {
const { items } = req.body;
const extra = items.filter(v => !DEFAULT_MODULES.includes(v));
await db.query(
'INSERT INTO settings (`key`, value) VALUES (?, ?) ON DUPLICATE KEY UPDATE value = ?',
['modules', JSON.stringify(extra), JSON.stringify(extra)]
);
res.json({ message: 'OK' });
} catch (err) {
res.status(500).json({ error: err.message });
}
});
module.exports = router;
module.exports.DEFAULT_CATEGORIES = DEFAULT_CATEGORIES;
module.exports.DEFAULT_MODULES = DEFAULT_MODULES;

144
server/routes/tenants.js Normal file
View File

@@ -0,0 +1,144 @@
const express = require('express');
const router = express.Router();
const db = require('../db');
const auth = require('../middleware/auth');
router.use(auth);
// 获取所有租户 (含搜索)
router.get('/', async (req, res) => {
try {
const { search } = req.query;
let query = 'SELECT * FROM tenants WHERE user_id = ?';
let params = [req.user.username];
if (search) {
query += ' AND name LIKE ?';
params.push(`%${search}%`);
}
query += ' ORDER BY created_at DESC';
const [rows] = await db.query(query, params);
res.json(rows);
} catch (err) {
res.status(500).json({ error: err.message });
}
});
// 新增租户
router.post('/', async (req, res) => {
try {
const tenant = req.body;
// 特殊逻辑标品PP下 entCode 默认等于 apiKey
if (tenant.type === '标品PP' && !tenant.entCode) {
tenant.entCode = tenant.apiKey;
}
const [result] = await db.query(
'INSERT INTO tenants (user_id, name, type, app_key, app_secret, api_key, ent_code, bu_id, ent_id, private_key, public_key) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)',
[
req.user.username,
tenant.name,
tenant.type,
tenant.appKey || null,
tenant.appSecret || null,
tenant.apiKey || null,
tenant.entCode || null,
tenant.buId || null,
tenant.entId || null,
tenant.privateKey || null,
tenant.publicKey || null
]
);
res.json({ id: result.insertId, ...tenant });
} catch (err) {
res.status(500).json({ error: err.message });
}
});
// 更新租户
router.put('/:id', async (req, res) => {
try {
const { id } = req.params;
const tenant = req.body;
await db.query(
`UPDATE tenants SET
name = ?,
type = ?,
app_key = ?,
app_secret = ?,
api_key = ?,
ent_code = ?,
bu_id = ?,
ent_id = ?,
private_key = ?,
public_key = ?
WHERE id = ? AND user_id = ?`,
[
tenant.name,
tenant.type,
tenant.appKey || null,
tenant.appSecret || null,
tenant.apiKey || null,
tenant.entCode || null,
tenant.buId || null,
tenant.entId || null,
tenant.privateKey || null,
tenant.publicKey || null,
id,
req.user.username
]
);
res.json({ message: 'Updated successfully' });
} catch (err) {
res.status(500).json({ error: err.message });
}
});
// 删除租户
router.delete('/:id', async (req, res) => {
try {
const { id } = req.params;
await db.query('DELETE FROM tenants WHERE id = ? AND user_id = ?', [id, req.user.username]);
res.json({ message: 'Deleted successfully' });
} catch (err) {
res.status(500).json({ error: err.message });
}
});
// 复制租户
router.post('/copy/:id', async (req, res) => {
try {
const { id } = req.params;
const [rows] = await db.query('SELECT * FROM tenants WHERE id = ? AND user_id = ?', [id, req.user.username]);
if (rows.length === 0) {
return res.status(404).json({ error: 'Tenant not found' });
}
const tenant = rows[0];
const [result] = await db.query(
'INSERT INTO tenants (user_id, name, type, app_key, app_secret, api_key, ent_code, bu_id, ent_id, private_key, public_key) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)',
[
req.user.username,
`${tenant.name} (Copy)`,
tenant.type,
tenant.app_key,
tenant.app_secret,
tenant.api_key,
tenant.ent_code,
tenant.bu_id,
tenant.ent_id,
tenant.private_key,
tenant.public_key
]
);
res.json({ id: result.insertId, message: 'Copied successfully' });
} catch (err) {
res.status(500).json({ error: err.message });
}
});
module.exports = router;