Files
company-celebration/packages/server/src/routes/wechat.routes.ts
empty 3246479643 feat: integrate WeChat Open Platform QR code login
- Add WeChat service for OAuth2 authentication flow
- Add WeChat routes (/api/wechat/login, /api/wechat/callback)
- Add WeChat types for login state and responses
- Update EntryQRCode component to support WeChat login
- Add WeChat config options (appId, appSecret, redirectUri)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-02-03 14:38:54 +08:00

146 lines
3.7 KiB
TypeScript

import { Router, Request, Response } from 'express';
import { wechatService } from '../services/wechat.service';
import { getIO } from '../socket';
import { logger } from '../utils/logger';
const router = Router();
/**
* GET /api/wechat/login
* Generate WeChat login QR code URL
*/
router.get('/login', async (req: Request, res: Response) => {
const { pcSocketId } = req.query;
if (!pcSocketId || typeof pcSocketId !== 'string') {
return res.status(400).json({
success: false,
error: 'pcSocketId is required',
});
}
const result = await wechatService.createLoginState(pcSocketId);
if (!result.success) {
return res.status(500).json(result);
}
res.json(result);
});
/**
* GET /api/wechat/callback
* WeChat OAuth2 callback handler
*/
router.get('/callback', async (req: Request, res: Response) => {
const { code, state } = req.query;
if (!code || !state || typeof code !== 'string' || typeof state !== 'string') {
logger.warn({ code, state }, 'Invalid WeChat callback parameters');
return res.status(400).send(renderCallbackPage(false, 'Invalid parameters'));
}
const result = await wechatService.handleCallback(code, state);
if (!result.success || !result.data) {
logger.error({ error: result.error }, 'WeChat callback failed');
return res.status(400).send(renderCallbackPage(false, result.error || 'Login failed'));
}
const { openid, pcSocketId, sessionToken, userId, userInfo } = result.data;
// Notify PC client via WebSocket
try {
const io = getIO();
io.to(pcSocketId).emit('wechat:login_success' as any, {
openid,
sessionToken,
userId,
userName: userInfo?.nickname || `微信用户_${openid.slice(-6)}`,
userInfo,
});
logger.info({ pcSocketId, userId }, 'WeChat login success notification sent');
} catch (error) {
logger.error({ error, pcSocketId }, 'Failed to notify PC client');
}
// Return success page
res.send(renderCallbackPage(true, 'Login successful'));
});
/**
* GET /api/wechat/status
* Check WeChat configuration status
*/
router.get('/status', (_req: Request, res: Response) => {
res.json({
configured: wechatService.isConfigured(),
});
});
/**
* Render callback result page
*/
function renderCallbackPage(success: boolean, message: string): string {
const color = success ? '#52c41a' : '#ff4d4f';
const icon = success ? '✓' : '✗';
return `
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>微信登录${success ? '成功' : '失败'}</title>
<style>
* { margin: 0; padding: 0; box-sizing: border-box; }
body {
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
background: linear-gradient(135deg, #1a1a2e 0%, #16213e 100%);
min-height: 100vh;
display: flex;
align-items: center;
justify-content: center;
color: #fff;
}
.container {
text-align: center;
padding: 40px;
}
.icon {
width: 80px;
height: 80px;
border-radius: 50%;
background: ${color};
display: flex;
align-items: center;
justify-content: center;
font-size: 40px;
margin: 0 auto 24px;
}
.message {
font-size: 24px;
margin-bottom: 16px;
}
.hint {
color: rgba(255,255,255,0.6);
font-size: 14px;
}
</style>
</head>
<body>
<div class="container">
<div class="icon">${icon}</div>
<div class="message">${message}</div>
<div class="hint">${success ? '请返回大屏查看' : '请重新扫码'}</div>
</div>
<script>
${success ? 'setTimeout(() => window.close(), 3000);' : ''}
</script>
</body>
</html>
`.trim();
}
export default router;