zhizhi/backend/api-server/routes/auth.js

224 lines
6.2 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

/**
* /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 clientIp = req.ip || req.connection.remoteAddress || 'unknown';
if (!checkRateLimit(clientIp)) {
return res.status(429).json({ valid: false, message: '请求过于频繁' });
}
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;