feat: 飞书单点登录和通知功能

This commit is contained in:
2026-02-03 11:38:16 +08:00
parent c657fbe01a
commit 78ebc67e2a
18 changed files with 2136 additions and 105 deletions

View File

@@ -6,9 +6,12 @@ const config = require('./src/config/env');
const { getConfiguredRepos } = require('./src/config/mappings');
const { handleIssueEvent } = require('./src/logic/syncManager');
const { handleJiraHook } = require('./src/logic/jiraSyncManager');
const { notify, inferEventType } = require('./src/logic/larkNotifier');
const editorRoutes = require('./src/routes/control');
const logger = require('./src/utils/logger');
const { larkAuthMiddleware, loginHandler, callbackHandler } = require('./src/middleware/larkAuth');
const app = new Hono();
const requestCounts = new Map();
@@ -47,29 +50,25 @@ setInterval(() => {
}
}, 5 * 60 * 1000);
//内网访问控制中间件保护管理界面只允许dotenv配置的域名访问
const internalOnlyMiddleware = async (c, next) => {
const pathname = new URL(c.req.url).pathname;
//鉴权路由
app.get('/login', loginHandler);
app.get('/oauth/callback', callbackHandler);
if (pathname.startsWith('/hooks/')) {
return await next();
}
// 全局鉴权中间件 (飞书 OAuth)
app.use('*', larkAuthMiddleware);
const host = (c.req.header('host') || '').split(':')[0];
const allowedHosts = config.app.dashboardAllowedHosts;
// 获取当前用户信息
const { meHandler, adminMiddleware } = require('./src/middleware/larkAuth');
app.get('/api/me', meHandler);
if (!allowedHosts.some(allowed => host === allowed || host.endsWith('.' + allowed))) {
logger.security(`Blocked access from unauthorized host: ${host}`, {
path: pathname,
ip: c.req.header('x-forwarded-for') || c.req.header('x-real-ip') || 'unknown'
});
return c.text('Forbidden - Access denied from this domain', 403);
}
await next();
};
app.use('*', internalOnlyMiddleware);
// 敏感接口保护 (仅管理员可访问)
// 注意Hono 的中间件匹配顺序很重要specific routes should be handled or protected explicitly
app.use('/api/env', adminMiddleware);
app.use('/api/restart', adminMiddleware);
app.use('/api/logs', adminMiddleware);
app.use('/api/logs/clear', adminMiddleware);
app.use('/api/history', adminMiddleware);
app.use('/api/status', adminMiddleware);
//Gitea webhook处理入口
app.post('/hooks/gitea', rateLimiter, async (c) => {
@@ -100,16 +99,31 @@ app.post('/hooks/gitea', rateLimiter, async (c) => {
//解析JSON
const body = JSON.parse(rawBody);
//Payload结构验证
if (!body || !body.issue || !body.repository) {
logger.warn('Invalid payload structure', { ip });
return c.json({ error: 'Invalid payload structure' }, 400);
//触发飞书通知(所有事件类型)
const eventType = inferEventType(event, body);
if (eventType) {
notify(eventType, body).catch(err =>
logger.error('[LarkNotifier] Async notify error:', err.message)
);
}
//Issue 事件处理(原有逻辑)
if (event === 'issues' || event === 'issue_comment') {
//异步处理不阻塞webhook返回
handleIssueEvent(body).catch(err => logger.error('Async handler error', err.message));
} else {
if (body.issue && body.repository) {
handleIssueEvent(body).catch(err => logger.error('Async handler error', err.message));
} else {
logger.warn('Invalid payload structure for issue event', { ip });
}
}
//PR 事件 - 仅记录日志(未来可扩展)
else if (event === 'pull_request') {
logger.info(`[PR Event] ${body.action} - ${body.pull_request?.title || 'Unknown'}`, { ip });
}
//Release 事件 - 仅记录日志(未来可扩展)
else if (event === 'release') {
logger.info(`[Release Event] ${body.action} - ${body.release?.name || 'Unknown'}`, { ip });
}
else {
logger.info(`Ignored event type: ${event}`, { ip });
}
@@ -146,6 +160,7 @@ const configuredRepos = getConfiguredRepos();
//控制台首页
app.get('/', (c) => c.redirect('/dashboard'));
app.get('/dashboard', serveStatic({ path: './public/dashboard.html' }));
app.get('/error.html', serveStatic({ path: './public/error.html' }));
app.route('/api', editorRoutes);
app.route('/editor/api', editorRoutes);