Initial commit: API Debug Tool
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
5
server/.env.example
Normal file
5
server/.env.example
Normal file
@@ -0,0 +1,5 @@
|
||||
PORT=5001
|
||||
DB_HOST=localhost
|
||||
DB_USER=root
|
||||
DB_PASSWORD=your_password_here
|
||||
DB_NAME=api_debug
|
||||
14
server/db.js
Normal file
14
server/db.js
Normal file
@@ -0,0 +1,14 @@
|
||||
const mysql = require('mysql2/promise');
|
||||
require('dotenv').config();
|
||||
|
||||
const pool = mysql.createPool({
|
||||
host: process.env.DB_HOST || 'localhost',
|
||||
user: process.env.DB_USER || 'root',
|
||||
password: process.env.DB_PASSWORD || '',
|
||||
database: process.env.DB_NAME || 'api_debug',
|
||||
waitForConnections: true,
|
||||
connectionLimit: 10,
|
||||
queueLimit: 0
|
||||
});
|
||||
|
||||
module.exports = pool;
|
||||
30
server/index.js
Normal file
30
server/index.js
Normal file
@@ -0,0 +1,30 @@
|
||||
const express = require('express');
|
||||
const cors = require('cors');
|
||||
const bodyParser = require('body-parser');
|
||||
require('dotenv').config();
|
||||
|
||||
const tenantRoutes = require('./routes/tenants');
|
||||
const endpointRoutes = require('./routes/endpoints');
|
||||
const debugRoutes = require('./routes/debug');
|
||||
const authRoutes = require('./routes/auth');
|
||||
const settingsRoutes = require('./routes/settings');
|
||||
|
||||
const app = express();
|
||||
const PORT = process.env.PORT || 5001;
|
||||
|
||||
app.use(cors());
|
||||
app.use(bodyParser.json());
|
||||
|
||||
app.use('/api/auth', authRoutes);
|
||||
app.use('/api/tenants', tenantRoutes);
|
||||
app.use('/api/endpoints', endpointRoutes);
|
||||
app.use('/api/debug', debugRoutes);
|
||||
app.use('/api/settings', settingsRoutes);
|
||||
|
||||
app.get('/', (req, res) => {
|
||||
res.send('API Debug Tool Backend is running...');
|
||||
});
|
||||
|
||||
app.listen(PORT, () => {
|
||||
process.stdout.write(`Server is running on port ${PORT}\n`);
|
||||
});
|
||||
20
server/init.sql
Normal file
20
server/init.sql
Normal file
@@ -0,0 +1,20 @@
|
||||
CREATE DATABASE IF NOT EXISTS api_debug;
|
||||
|
||||
USE api_debug;
|
||||
|
||||
CREATE TABLE IF NOT EXISTS tenants (
|
||||
id INT AUTO_INCREMENT PRIMARY KEY,
|
||||
user_id VARCHAR(50) DEFAULT 'admin', -- 暂时默认为 admin,后续可扩展用户系统
|
||||
name VARCHAR(100) NOT NULL,
|
||||
type ENUM('旗舰版PP', '旗舰版ATS', '标品ATS', '标品PP', '国际版ATS') NOT NULL,
|
||||
app_key VARCHAR(100),
|
||||
app_secret VARCHAR(100),
|
||||
api_key VARCHAR(100),
|
||||
ent_code VARCHAR(100),
|
||||
bu_id VARCHAR(100),
|
||||
ent_id VARCHAR(100),
|
||||
private_key TEXT,
|
||||
public_key TEXT,
|
||||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP
|
||||
);
|
||||
17
server/middleware/auth.js
Normal file
17
server/middleware/auth.js
Normal file
@@ -0,0 +1,17 @@
|
||||
const jwt = require('jsonwebtoken');
|
||||
|
||||
const JWT_SECRET = process.env.JWT_SECRET || 'super_secret_api_debug_key';
|
||||
|
||||
module.exports = (req, res, next) => {
|
||||
const authHeader = req.headers['authorization'];
|
||||
const token = authHeader && authHeader.split(' ')[1];
|
||||
|
||||
if (!token) return res.status(401).json({ error: '未登录,请先登录' });
|
||||
|
||||
try {
|
||||
req.user = jwt.verify(token, JWT_SECRET);
|
||||
next();
|
||||
} catch {
|
||||
res.status(401).json({ error: 'Token 无效或已过期,请重新登录' });
|
||||
}
|
||||
};
|
||||
44
server/migrate.js
Normal file
44
server/migrate.js
Normal file
@@ -0,0 +1,44 @@
|
||||
const pool = require('./db');
|
||||
const bcrypt = require('bcryptjs');
|
||||
|
||||
async function migrate() {
|
||||
try {
|
||||
console.log('Starting migration...');
|
||||
|
||||
// 1. Create users table
|
||||
await pool.query(`
|
||||
CREATE TABLE IF NOT EXISTS users (
|
||||
id INT AUTO_INCREMENT PRIMARY KEY,
|
||||
username VARCHAR(50) NOT NULL UNIQUE,
|
||||
password VARCHAR(255) NOT NULL,
|
||||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
|
||||
)
|
||||
`);
|
||||
console.log('users table ensured.');
|
||||
|
||||
// 2. Add default admin user if no users exist
|
||||
const [users] = await pool.query('SELECT * FROM users');
|
||||
if (users.length === 0) {
|
||||
const hashedPassword = await bcrypt.hash('123456', 10);
|
||||
await pool.query('INSERT INTO users (username, password) VALUES (?, ?)', ['admin', hashedPassword]);
|
||||
console.log('Default admin user created.');
|
||||
}
|
||||
|
||||
// 3. Add category column to endpoints if it doesn't exist
|
||||
const [columns] = await pool.query(`SHOW COLUMNS FROM endpoints LIKE 'category'`);
|
||||
if (columns.length === 0) {
|
||||
await pool.query(`ALTER TABLE endpoints ADD COLUMN category VARCHAR(50) DEFAULT '旗舰版PP' AFTER name`);
|
||||
console.log('Added category column to endpoints table.');
|
||||
} else {
|
||||
console.log('category column already exists in endpoints.');
|
||||
}
|
||||
|
||||
console.log('Migration completed successfully.');
|
||||
process.exit(0);
|
||||
} catch (error) {
|
||||
console.error('Migration failed:', error);
|
||||
process.exit(1);
|
||||
}
|
||||
}
|
||||
|
||||
migrate();
|
||||
1603
server/package-lock.json
generated
Normal file
1603
server/package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load Diff
28
server/package.json
Normal file
28
server/package.json
Normal file
@@ -0,0 +1,28 @@
|
||||
{
|
||||
"dependencies": {
|
||||
"axios": "^1.13.6",
|
||||
"bcryptjs": "^3.0.3",
|
||||
"body-parser": "^2.2.2",
|
||||
"cors": "^2.8.6",
|
||||
"dotenv": "^17.3.1",
|
||||
"express": "^5.2.1",
|
||||
"jsonwebtoken": "^9.0.3",
|
||||
"mysql2": "^3.19.0"
|
||||
},
|
||||
"name": "server",
|
||||
"version": "1.0.0",
|
||||
"description": "",
|
||||
"main": "index.js",
|
||||
"scripts": {
|
||||
"start": "node index.js",
|
||||
"dev": "nodemon index.js",
|
||||
"test": "echo \"Error: no test specified\" && exit 1"
|
||||
},
|
||||
"keywords": [],
|
||||
"author": "",
|
||||
"license": "ISC",
|
||||
"type": "commonjs",
|
||||
"devDependencies": {
|
||||
"nodemon": "^3.1.14"
|
||||
}
|
||||
}
|
||||
86
server/routes/auth.js
Normal file
86
server/routes/auth.js
Normal 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
214
server/routes/debug.js
Normal 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;
|
||||
68
server/routes/endpoints.js
Normal file
68
server/routes/endpoints.js
Normal 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
71
server/routes/settings.js
Normal 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
144
server/routes/tenants.js
Normal 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;
|
||||
20
server/test-rsa.js
Normal file
20
server/test-rsa.js
Normal file
@@ -0,0 +1,20 @@
|
||||
const crypto = require('crypto');
|
||||
const { generateKeyPairSync } = crypto;
|
||||
|
||||
const { privateKey, publicKey } = generateKeyPairSync('rsa', {
|
||||
modulusLength: 2048,
|
||||
publicKeyEncoding: {
|
||||
type: 'spki',
|
||||
format: 'pem'
|
||||
},
|
||||
privateKeyEncoding: {
|
||||
type: 'pkcs8',
|
||||
format: 'pem'
|
||||
}
|
||||
});
|
||||
|
||||
const data = "apiCode=0001&entCode=1&nonce=999×tamp=1565244098737";
|
||||
const sign = crypto.createSign('RSA-MD5');
|
||||
sign.update(data);
|
||||
const signature = sign.sign(privateKey, 'base64');
|
||||
console.log('Signature:', signature);
|
||||
16
server/test_debug.js
Normal file
16
server/test_debug.js
Normal file
@@ -0,0 +1,16 @@
|
||||
const axios = require('axios');
|
||||
|
||||
async function test() {
|
||||
try {
|
||||
const res = await axios.post('http://localhost:5001/api/debug/execute-pp', {
|
||||
tenantId: 1, // Assume 1 is a valid tenant
|
||||
endpointId: 1, // Assume 1 is a valid endpoint
|
||||
body: { "test": "data" },
|
||||
queryParams: { "hello": "world" }
|
||||
});
|
||||
console.log("Success:", res.data);
|
||||
} catch (e) {
|
||||
console.error("Error:", e.response ? e.response.data : e.message);
|
||||
}
|
||||
}
|
||||
test();
|
||||
145
server/tmp_test/test_standard_pp.js
Normal file
145
server/tmp_test/test_standard_pp.js
Normal file
@@ -0,0 +1,145 @@
|
||||
/**
|
||||
* 标品PP 真实接口调试脚本(从 DB 读取完整数据)
|
||||
* 使用租户 id=5 (名称:895) + 接口 id=4 (标品部门数据)
|
||||
*/
|
||||
|
||||
const crypto = require('crypto');
|
||||
const axios = require('axios');
|
||||
const db = require('../db');
|
||||
|
||||
// ─── 工具函数 ─────────────────────────────────────────────────────────────
|
||||
|
||||
/**
|
||||
* 将私钥(多种格式)解析为 Node.js crypto 可用的 KeyObject
|
||||
* 支持:
|
||||
* - 裸 base64 PKCS8 DER(Java 导出格式)
|
||||
* - PEM 格式
|
||||
*/
|
||||
function loadPrivateKey(raw) {
|
||||
if (raw.includes('-----BEGIN')) {
|
||||
return crypto.createPrivateKey(raw);
|
||||
}
|
||||
const derBuf = Buffer.from(raw.trim(), 'base64');
|
||||
return crypto.createPrivateKey({ key: derBuf, format: 'der', type: 'pkcs8' });
|
||||
}
|
||||
|
||||
function generateNonce(length = 8) {
|
||||
const chars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789';
|
||||
return Array.from({ length }, () => chars[Math.floor(Math.random() * chars.length)]).join('');
|
||||
}
|
||||
|
||||
function generateSign(params, privateKey) {
|
||||
const sortedKeys = Object.keys(params).sort();
|
||||
const signString = sortedKeys.map(k => `${k}=${params[k]}`).join('&');
|
||||
console.log('[签名] 待签名字符串:', signString);
|
||||
|
||||
const signer = crypto.createSign('RSA-MD5');
|
||||
signer.update(signString, 'utf8');
|
||||
const signature = signer.sign(privateKey, 'base64');
|
||||
console.log('[签名] Base64 (前30):', signature.substring(0, 30) + '...');
|
||||
return signature;
|
||||
}
|
||||
|
||||
// ─── 主流程 ────────────────────────────────────────────────────────────────
|
||||
|
||||
async function main() {
|
||||
console.log('='.repeat(60));
|
||||
console.log('标品PP 真实接口调试:标品部门数据');
|
||||
console.log('='.repeat(60));
|
||||
|
||||
// 从 DB 读取完整数据
|
||||
const [[tenant]] = await db.query('SELECT * FROM tenants WHERE id = ?', [5]);
|
||||
const [[endpoint]] = await db.query('SELECT * FROM endpoints WHERE id = ?', [4]);
|
||||
|
||||
if (!tenant || !endpoint) {
|
||||
console.error('❌ 租户或接口不存在');
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
console.log('\n[租户] 名称:', tenant.name, '| 类型:', tenant.type);
|
||||
console.log('[接口] 名称:', endpoint.name, '| URL:', endpoint.url);
|
||||
console.log('[私钥] 长度:', tenant.private_key?.length, '字符');
|
||||
|
||||
// 1. 加载私钥
|
||||
let privateKey;
|
||||
try {
|
||||
privateKey = loadPrivateKey(tenant.private_key);
|
||||
console.log('✅ 私钥加载成功(类型:', privateKey.asymmetricKeyType, ')');
|
||||
} catch (e) {
|
||||
console.error('❌ 私钥加载失败:', e.message);
|
||||
// 尝试输出私钥前后片段以诊断
|
||||
console.log(' 私钥开头:', tenant.private_key.substring(0, 50));
|
||||
console.log(' 私钥结尾:', tenant.private_key.slice(-50));
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
// 2. 构建 query 参数(不含 sign)
|
||||
const params = {
|
||||
entCode: tenant.ent_code,
|
||||
apiCode: endpoint.api_code,
|
||||
timestamp: Date.now().toString(),
|
||||
nonce: generateNonce(8),
|
||||
};
|
||||
|
||||
console.log('\n[参数] 基础参数:', JSON.stringify(params));
|
||||
|
||||
// 3. 生成签名
|
||||
let signature;
|
||||
try {
|
||||
signature = generateSign(params, privateKey);
|
||||
params.sign = signature;
|
||||
console.log('✅ 签名生成成功');
|
||||
} catch (e) {
|
||||
console.error('❌ 签名失败:', e.message);
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
// 4. 构建请求
|
||||
const encodedKey = Buffer.from(`${tenant.api_key}:`).toString('base64');
|
||||
const headers = {
|
||||
'Authorization': `Basic ${encodedKey}`,
|
||||
'Content-Type': 'application/json',
|
||||
};
|
||||
|
||||
const body = JSON.parse(endpoint.body || '{}');
|
||||
|
||||
// 5. CURL 命令
|
||||
const urlObj = new URL(endpoint.url);
|
||||
Object.entries(params).forEach(([k, v]) => urlObj.searchParams.set(k, v));
|
||||
let curlCmd = `curl -X ${endpoint.method} '${urlObj.toString()}'`;
|
||||
Object.entries(headers).forEach(([k, v]) => {
|
||||
curlCmd += ` \\\n -H '${k}: ${v}'`;
|
||||
});
|
||||
curlCmd += ` \\\n -d '${JSON.stringify(body)}'`;
|
||||
console.log('\n[CURL]\n' + curlCmd);
|
||||
|
||||
// 6. 发送请求
|
||||
console.log('\n[请求] 发送中...');
|
||||
try {
|
||||
const response = await axios({
|
||||
method: endpoint.method.toLowerCase(),
|
||||
url: endpoint.url,
|
||||
headers,
|
||||
params,
|
||||
data: body,
|
||||
timeout: 15000,
|
||||
});
|
||||
|
||||
console.log('\n✅ 请求成功!');
|
||||
console.log('[响应] 状态:', response.status);
|
||||
console.log('[响应] 数据:');
|
||||
console.log(JSON.stringify(response.data, null, 2).substring(0, 1000));
|
||||
} catch (err) {
|
||||
console.error('\n❌ 请求失败!');
|
||||
if (err.response) {
|
||||
console.error('[错误] HTTP 状态:', err.response.status);
|
||||
console.error('[错误] 响应:', JSON.stringify(err.response.data, null, 2));
|
||||
} else {
|
||||
console.error('[错误]', err.message);
|
||||
}
|
||||
}
|
||||
|
||||
process.exit(0);
|
||||
}
|
||||
|
||||
main().catch(e => { console.error(e); process.exit(1); });
|
||||
20
server/update_endpoints_table.js
Normal file
20
server/update_endpoints_table.js
Normal file
@@ -0,0 +1,20 @@
|
||||
const { pool } = require('./db');
|
||||
|
||||
async function migrate() {
|
||||
try {
|
||||
console.log('Checking endpoints table for query_params column...');
|
||||
const [columns] = await pool.query(`SHOW COLUMNS FROM endpoints LIKE 'query_params'`);
|
||||
if (columns.length === 0) {
|
||||
console.log('Adding query_params column...');
|
||||
await pool.query(`ALTER TABLE endpoints ADD COLUMN query_params JSON`);
|
||||
console.log('query_params column added successfully.');
|
||||
} else {
|
||||
console.log('query_params column already exists.');
|
||||
}
|
||||
} catch (err) {
|
||||
console.error('Migration failed:', err);
|
||||
} finally {
|
||||
process.exit();
|
||||
}
|
||||
}
|
||||
migrate();
|
||||
Reference in New Issue
Block a user