autosnippet
Version:
Extract code patterns into a knowledge base for AI coding assistants
125 lines (124 loc) • 5.18 kB
JavaScript
/**
* 认证 API 路由
*
* 提供简单的用户名/密码登录。凭证通过环境变量配置:
* ASD_AUTH_USERNAME (默认 admin)
* ASD_AUTH_PASSWORD (默认 autosnippet)
*
* 仅在前端 VITE_AUTH_ENABLED=true 时由 Dashboard 调用。
* 使用 HMAC-SHA256 签发简单 JWT-like token(无第三方依赖)。
*/
import crypto from 'node:crypto';
import express from 'express';
import { AuthLoginBody } from '../../shared/schemas/http-requests.js';
import { validate } from '../middleware/validate.js';
const router = express.Router();
// ═══════════════════════════════════════════════════════
// Configuration
// ═══════════════════════════════════════════════════════
const AUTH_USERNAME = process.env.ASD_AUTH_USERNAME || 'admin';
const AUTH_PASSWORD = process.env.ASD_AUTH_PASSWORD || 'autosnippet';
const TOKEN_SECRET = process.env.ASD_AUTH_SECRET || crypto.randomBytes(32).toString('hex');
// 安全警告:仅在认证启用且使用默认凭据时提示
const authEnabled = process.env.VITE_AUTH_ENABLED === 'true' || process.env.ASD_AUTH_ENABLED === 'true';
if (authEnabled && (!process.env.ASD_AUTH_USERNAME || !process.env.ASD_AUTH_PASSWORD)) {
console.warn('[auth] WARNING: Using default credentials (admin/autosnippet). ' +
'Set ASD_AUTH_USERNAME and ASD_AUTH_PASSWORD environment variables for production.');
}
const TOKEN_TTL = 7 * 24 * 60 * 60 * 1000; // 7 天
// 将 secret 写回环境变量,供 roleResolver 等模块共享
if (!process.env.ASD_AUTH_SECRET) {
process.env.ASD_AUTH_SECRET = TOKEN_SECRET;
}
// ═══════════════════════════════════════════════════════
// Token helpers
// ═══════════════════════════════════════════════════════
function createToken(username) {
const payload = {
sub: username,
role: 'developer',
iat: Date.now(),
exp: Date.now() + TOKEN_TTL,
};
const payloadB64 = Buffer.from(JSON.stringify(payload)).toString('base64url');
const sig = crypto.createHmac('sha256', TOKEN_SECRET).update(payloadB64).digest('base64url');
return `${payloadB64}.${sig}`;
}
function verifyToken(token) {
if (!token || typeof token !== 'string') {
return null;
}
const [payloadB64, sig] = token.split('.');
if (!payloadB64 || !sig) {
return null;
}
const expectedSig = crypto
.createHmac('sha256', TOKEN_SECRET)
.update(payloadB64)
.digest('base64url');
if (!crypto.timingSafeEqual(Buffer.from(sig), Buffer.from(expectedSig))) {
return null;
}
try {
const payload = JSON.parse(Buffer.from(payloadB64, 'base64url').toString());
if (payload.exp && payload.exp < Date.now()) {
return null; // 已过期
}
return payload;
}
catch {
return null;
}
}
// ═══════════════════════════════════════════════════════
// Routes
// ═══════════════════════════════════════════════════════
/**
* POST /auth/login
* Body: { username, password }
*/
router.post('/login', validate(AuthLoginBody), async (req, res) => {
const { username, password } = req.body;
// 恒时比较防止时序攻击
const userOk = username.length === AUTH_USERNAME.length &&
crypto.timingSafeEqual(Buffer.from(username), Buffer.from(AUTH_USERNAME));
const passOk = password.length === AUTH_PASSWORD.length &&
crypto.timingSafeEqual(Buffer.from(password), Buffer.from(AUTH_PASSWORD));
if (!userOk || !passOk) {
return void res.status(401).json({
success: false,
error: { code: 'UNAUTHORIZED', message: '用户名或密码错误' },
});
}
const token = createToken(username);
return void res.json({
success: true,
data: {
token,
user: { username, role: 'developer' },
},
});
});
/**
* GET /auth/me
* Header: Authorization: Bearer <token>
*/
router.get('/me', async (req, res) => {
const authHeader = req.headers.authorization || '';
const token = authHeader.startsWith('Bearer ') ? authHeader.slice(7) : '';
const payload = verifyToken(token);
if (!payload) {
return void res.status(401).json({
success: false,
error: { code: 'UNAUTHORIZED', message: 'Token 无效或已过期' },
});
}
return void res.json({
success: true,
data: {
user: { username: payload.sub, role: payload.role },
},
});
});
export { verifyToken };
export default router;