- 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>
146 lines
3.7 KiB
TypeScript
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;
|