219 lines
6.0 KiB
JavaScript
219 lines
6.0 KiB
JavaScript
/**
|
||
* /api/auth — 开发者编号免配置登录路由
|
||
*
|
||
* 开发者输入编号(如 DEV-002)即可直接进入对话界面。
|
||
* 系统自动匹配团队配置的 API,人不需要设置任何东西。
|
||
* API Key 只在服务器端使用,前端永远看不到。
|
||
*
|
||
* 版权:国作登字-2026-A-00037559
|
||
*/
|
||
|
||
'use strict';
|
||
|
||
var express = require('express');
|
||
var crypto = require('crypto');
|
||
var router = express.Router();
|
||
|
||
// 团队共享密钥(存在服务器环境变量,前端不可见)
|
||
var TEAM_API_KEY = process.env.TEAM_API_KEY || '';
|
||
|
||
// 会话 token 签名密钥
|
||
var SESSION_SECRET = process.env.SESSION_SECRET || crypto.randomBytes(32).toString('hex');
|
||
|
||
// 简易速率限制(防暴力枚举)
|
||
var loginAttempts = new Map();
|
||
var RATE_LIMIT_WINDOW_MS = 60 * 1000; // 1分钟窗口
|
||
var RATE_LIMIT_MAX = 10; // 每分钟最多10次
|
||
|
||
function checkRateLimit(ip) {
|
||
var now = Date.now();
|
||
var entry = loginAttempts.get(ip);
|
||
if (!entry || now - entry.windowStart > RATE_LIMIT_WINDOW_MS) {
|
||
loginAttempts.set(ip, { windowStart: now, count: 1 });
|
||
return true;
|
||
}
|
||
entry.count++;
|
||
if (entry.count > RATE_LIMIT_MAX) {
|
||
return false;
|
||
}
|
||
return true;
|
||
}
|
||
|
||
// 开发者数据库(后续对接 Notion)
|
||
var DEV_DATABASE = {
|
||
'DEV-000': { name: '冰朔', level: 3, channel: 'system' },
|
||
'DEV-001': { name: '页页', level: 1, channel: '小坍缩核线' },
|
||
'DEV-002': { name: '肥猫', level: 2, channel: '男频' },
|
||
'DEV-003': { name: 'Awen', level: 1, channel: '知秋线' },
|
||
'DEV-004': { name: '之之', level: 2, channel: '秋秋线' },
|
||
'DEV-005': { name: '时雨', level: 1, channel: '知秋线' },
|
||
'DEV-006': { name: '匆匆那年', level: 1, channel: '霜砚线' },
|
||
'DEV-007': { name: '小兴', level: 1, channel: '霜砚线' },
|
||
'DEV-008': { name: '花尔', level: 1, channel: '糖星云线' },
|
||
'DEV-009': { name: '小草莓', level: 1, channel: '欧诺弥亚线' },
|
||
'DEV-010': { name: '桔子', level: 2, channel: '女频' },
|
||
'DEV-011': { name: '燕樊', level: 1, channel: '寂曜线' }
|
||
};
|
||
|
||
// 活跃会话存储(内存级,重启清空)
|
||
var activeSessions = new Map();
|
||
|
||
/**
|
||
* 生成会话 token(有效期 24h)
|
||
* @param {string} devId
|
||
* @returns {string}
|
||
*/
|
||
function generateSessionToken(devId) {
|
||
var payload = devId + ':' + Date.now();
|
||
var hmac = crypto.createHmac('sha256', SESSION_SECRET);
|
||
hmac.update(payload);
|
||
var token = payload + ':' + hmac.digest('hex');
|
||
var tokenBase64 = Buffer.from(token).toString('base64');
|
||
|
||
activeSessions.set(tokenBase64, {
|
||
devId: devId,
|
||
createdAt: Date.now(),
|
||
expiresAt: Date.now() + 24 * 60 * 60 * 1000
|
||
});
|
||
|
||
return tokenBase64;
|
||
}
|
||
|
||
/**
|
||
* 验证会话 token
|
||
* @param {string} token
|
||
* @returns {{ valid: boolean, devId?: string }}
|
||
*/
|
||
function verifySessionToken(token) {
|
||
var session = activeSessions.get(token);
|
||
if (!session) {
|
||
return { valid: false };
|
||
}
|
||
if (Date.now() > session.expiresAt) {
|
||
activeSessions.delete(token);
|
||
return { valid: false };
|
||
}
|
||
return { valid: true, devId: session.devId };
|
||
}
|
||
|
||
/**
|
||
* 会话验证中间件
|
||
*/
|
||
function requireSession(req, res, next) {
|
||
var authHeader = req.headers['authorization'] || '';
|
||
var token = authHeader.replace(/^Bearer\s+/i, '');
|
||
|
||
if (!token) {
|
||
return res.status(401).json({
|
||
error: true,
|
||
code: 'SESSION_REQUIRED',
|
||
message: '请先登录。需要携带有效的会话 token。'
|
||
});
|
||
}
|
||
|
||
var result = verifySessionToken(token);
|
||
if (!result.valid) {
|
||
return res.status(401).json({
|
||
error: true,
|
||
code: 'SESSION_EXPIRED',
|
||
message: '会话已过期,请重新登录。'
|
||
});
|
||
}
|
||
|
||
req.sessionDevId = result.devId;
|
||
req.sessionDev = DEV_DATABASE[result.devId] || null;
|
||
next();
|
||
}
|
||
|
||
/**
|
||
* POST /api/auth/team-login
|
||
* 开发者编号免配置登录
|
||
*/
|
||
router.post('/auth/team-login', function(req, res) {
|
||
// 速率限制检查
|
||
var clientIp = req.ip || req.connection.remoteAddress || 'unknown';
|
||
if (!checkRateLimit(clientIp)) {
|
||
return res.status(429).json({
|
||
success: false,
|
||
message: '⚠️ 登录请求过于频繁,请稍后再试。'
|
||
});
|
||
}
|
||
|
||
var devId = (req.body.devId || '').trim().toUpperCase();
|
||
|
||
if (!devId || !/^DEV-\d{3}$/.test(devId)) {
|
||
return res.json({
|
||
success: false,
|
||
message: '❌ 无效的开发者编号格式。请使用 DEV-XXX 格式。'
|
||
});
|
||
}
|
||
|
||
var dev = DEV_DATABASE[devId];
|
||
|
||
if (!dev) {
|
||
return res.json({
|
||
success: false,
|
||
message: '❌ 开发者编号 ' + devId + ' 不存在。请确认你的编号。'
|
||
});
|
||
}
|
||
|
||
// 生成会话 token(有效期 24h)
|
||
var sessionToken = generateSessionToken(devId);
|
||
|
||
res.json({
|
||
success: true,
|
||
devName: dev.name,
|
||
level: dev.level,
|
||
channel: dev.channel,
|
||
sessionToken: sessionToken
|
||
});
|
||
|
||
// 审计日志
|
||
console.log('[AUTH] ' + devId + ' (' + dev.name + ') logged in at ' + new Date().toISOString());
|
||
});
|
||
|
||
/**
|
||
* GET /api/auth/verify
|
||
* 验证当前会话是否有效
|
||
*/
|
||
router.get('/auth/verify', function(req, res) {
|
||
var authHeader = req.headers['authorization'] || '';
|
||
var token = authHeader.replace(/^Bearer\s+/i, '');
|
||
var result = verifySessionToken(token);
|
||
|
||
if (result.valid) {
|
||
var dev = DEV_DATABASE[result.devId] || {};
|
||
res.json({
|
||
valid: true,
|
||
devId: result.devId,
|
||
devName: dev.name || '',
|
||
level: dev.level || 0,
|
||
channel: dev.channel || ''
|
||
});
|
||
} else {
|
||
res.json({ valid: false });
|
||
}
|
||
});
|
||
|
||
/**
|
||
* POST /api/auth/logout
|
||
* 注销会话
|
||
*/
|
||
router.post('/auth/logout', function(req, res) {
|
||
var authHeader = req.headers['authorization'] || '';
|
||
var token = authHeader.replace(/^Bearer\s+/i, '');
|
||
|
||
if (token && activeSessions.has(token)) {
|
||
var session = activeSessions.get(token);
|
||
activeSessions.delete(token);
|
||
console.log('[AUTH] ' + session.devId + ' logged out at ' + new Date().toISOString());
|
||
}
|
||
|
||
res.json({ success: true, message: '已注销' });
|
||
});
|
||
|
||
module.exports = router;
|
||
module.exports.requireSession = requireSession;
|
||
module.exports.verifySessionToken = verifySessionToken;
|
||
module.exports.DEV_DATABASE = DEV_DATABASE;
|