security: external API - only ID+Secret auth, remove legacy key & JWT
- removed x-external-key (single key) auth path + getExternalKey - removed JWT passthrough in clientAuth (Bearer no longer accepted) - removed /auth/login (JWT endpoint) and /my-tickets (JWT-only) - clientAuth now mandatory: missing/invalid/disabled client -> 401 (closed the 'no config = allow all' authorization bypass) - moved /auth/register BEHIND clientAuth (was anonymous abuse surface) - clients mgmt endpoints keep authenticate + role check (admin UI) - docs + UI copy updated to single auth method - verified: 30 checks incl. full-tree scan for legacy key refs
This commit is contained in:
@@ -2,46 +2,31 @@ const express = require('express');
|
||||
const bcrypt = require('bcryptjs');
|
||||
const crypto = require('crypto');
|
||||
const { v4: uuid } = require('uuid');
|
||||
const { query, getRow, getConfig } = require('../db');
|
||||
const { generateToken, authenticate } = require('../middleware/auth');
|
||||
const { query, getRow } = require('../db');
|
||||
const { authenticate } = require('../middleware/auth');
|
||||
const { sendEmail } = require('../mailer');
|
||||
const { logSystem } = require('../logger');
|
||||
|
||||
const router = express.Router();
|
||||
|
||||
// ============ 鉴权 ============
|
||||
// 支持三种方式(免用户登录):
|
||||
// 1. x-external-key: 旧版单 key(config.external_api_key)
|
||||
// 2. x-api-client-id + x-api-secret: api_clients 表多客户端(推荐)
|
||||
// 3. Authorization: Bearer <jwt>: 用户登录态(插件用)
|
||||
|
||||
function getExternalKey() {
|
||||
try { return getConfig()?.external_api_key || process.env.EXTERNAL_API_KEY || ''; } catch { return process.env.EXTERNAL_API_KEY || ''; }
|
||||
}
|
||||
// 外部 API 唯一鉴权方式: ID + Secret(api_clients 表)
|
||||
// 无凭据 / 凭据错误 / 客户端停用 → 一律 401(不允许匿名访问)
|
||||
|
||||
async function clientAuth(req, res, next) {
|
||||
// 方式1: 旧版单 key
|
||||
const legacy = getExternalKey();
|
||||
if (legacy && req.headers['x-external-key'] === legacy) return next();
|
||||
|
||||
// 方式2: client_id + secret
|
||||
const cid = req.headers['x-api-client-id'];
|
||||
const secret = req.headers['x-api-secret'];
|
||||
if (cid && secret) {
|
||||
try {
|
||||
const client = await getRow('SELECT * FROM api_clients WHERE client_id = ?', [cid]);
|
||||
if (client && client.active && bcrypt.compareSync(secret, client.secret_hash)) return next();
|
||||
} catch {}
|
||||
return res.status(401).json({ error: '客户端鉴权失败' });
|
||||
if (!cid || !secret) return res.status(401).json({ error: '未授权: 缺少 x-api-client-id / x-api-secret' });
|
||||
try {
|
||||
const client = await getRow('SELECT * FROM api_clients WHERE client_id = ?', [cid]);
|
||||
if (!client || !client.active || !bcrypt.compareSync(secret, client.secret_hash)) {
|
||||
return res.status(401).json({ error: '客户端鉴权失败' });
|
||||
}
|
||||
req.apiClient = client;
|
||||
next();
|
||||
} catch (e) {
|
||||
res.status(500).json({ error: '鉴权服务异常' });
|
||||
}
|
||||
|
||||
// 方式3: 用户 JWT(交给 authenticate 处理, 这里放行)
|
||||
if (req.headers['authorization']?.startsWith('Bearer ')) return next();
|
||||
|
||||
// 方式1 配置了 key 但没带 → 拒绝(避免裸奔)
|
||||
if (legacy) return res.status(401).json({ error: '未授权' });
|
||||
// 什么都没配置: 允许(兼容旧部署)
|
||||
return next();
|
||||
}
|
||||
|
||||
// 服务器 alias 解析: '分组/服务器' 或 alias 或 server_name
|
||||
@@ -97,7 +82,10 @@ router.delete('/clients/:id', authenticate, async (req, res) => {
|
||||
} catch (e) { res.status(500).json({ error: e.message }); }
|
||||
});
|
||||
|
||||
// ============ Auth (no external key needed) ============
|
||||
// ---- 以下接口: 外部鉴权(client_id + secret, 唯一方式) ----
|
||||
router.use(clientAuth);
|
||||
|
||||
// ============ 外部注册(需客户端鉴权) ============
|
||||
router.post('/auth/register', async (req, res) => {
|
||||
try {
|
||||
const { username, password, email, game_name, game_uid, source } = req.body;
|
||||
@@ -125,21 +113,6 @@ router.post('/auth/register', async (req, res) => {
|
||||
} catch (e) { res.status(500).json({ error: '服务器错误' }); }
|
||||
});
|
||||
|
||||
router.post('/auth/login', async (req, res) => {
|
||||
try {
|
||||
const { username, password } = req.body;
|
||||
if (!username || !password) return res.status(400).json({ error: '请输入用户名和密码' });
|
||||
const user = await getRow('SELECT * FROM users WHERE username = ?', [username]);
|
||||
if (!user || !(await bcrypt.compare(password, user.password))) return res.status(401).json({ error: '用户名或密码错误' });
|
||||
if (!user.active) return res.status(403).json({ error: '账号未激活' });
|
||||
const token = generateToken(user);
|
||||
res.json({ token, user: { id:user.id, username:user.username, game_name:user.game_name, game_uid:user.game_uid, role:user.role, source:user.source } });
|
||||
} catch (e) { res.status(500).json({ error: '服务器错误' }); }
|
||||
});
|
||||
|
||||
// ---- 以下接口: 免用户登录(client_id+secret / x-external-key / JWT) ----
|
||||
router.use(clientAuth);
|
||||
|
||||
// ============ 工单: 提交 ============
|
||||
// server: '分组/服务器名' 或 alias 或 server_name(可选)
|
||||
router.post('/tickets', async (req, res) => {
|
||||
@@ -177,21 +150,6 @@ router.get('/tickets/track', async (req, res) => {
|
||||
} catch (e) { res.status(500).json({ error: e.message }); }
|
||||
});
|
||||
|
||||
// 我的工单(JWT 用户)
|
||||
router.get('/my-tickets', authenticate, async (req, res) => {
|
||||
const { limit } = req.query;
|
||||
const rows = await query(`SELECT id, type, title, status, priority, server_name, created_at, updated_at FROM tickets WHERE user_id = ? ORDER BY updated_at DESC LIMIT ?`, [req.user.id, Math.min(parseInt(limit)||10, 50)]);
|
||||
res.json({ list: rows });
|
||||
});
|
||||
|
||||
router.get('/my-tickets/:id', authenticate, async (req, res) => {
|
||||
const t = await getRow(`SELECT id, type, title, status, priority, reporter_game_name, reporter_game_uid,
|
||||
target_game_name, target_game_uid, reason, description, assigned_to, claim_note, server_name, created_at, updated_at
|
||||
FROM tickets WHERE id = ? AND user_id = ?`, [req.params.id, req.user.id]);
|
||||
if (!t) return res.status(404).json({ error: '工单不存在' });
|
||||
res.json(t);
|
||||
});
|
||||
|
||||
// ============ 工单列表(带 server 过滤) ============
|
||||
router.get('/all-tickets', async (req, res) => {
|
||||
const { type, status, server, page, limit } = req.query;
|
||||
|
||||
Reference in New Issue
Block a user