Files
company-celebration/packages/server/src/routes/wechat-mp.routes.ts
2026-02-04 01:29:05 +08:00

149 lines
3.5 KiB
TypeScript
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.
import { Router, Request, Response } from 'express';
import { wechatMpService } from '../services/wechat-mp.service';
import { config } from '../config';
import { logger } from '../utils/logger';
const router = Router();
/**
* GET /api/mp/auth-url
* 获取公众号网页授权URL
* 前端在微信环境中调用此接口获取授权跳转URL
*/
router.get('/auth-url', async (req: Request, res: Response) => {
try {
if (!wechatMpService.isConfigured()) {
return res.json({
success: false,
error: 'WeChat MP not configured',
});
}
// 从query获取回调地址默认使用移动端URL
const redirectUri = (req.query.redirect_uri as string) || config.mobileClientUrl;
if (!isRedirectAllowed(redirectUri)) {
return res.status(400).json({
success: false,
error: 'Invalid redirect_uri',
});
}
// 生成随机state防止CSRF
const state = await wechatMpService.createState();
// 使用snsapi_base静默授权只获取openid
const scope = (req.query.scope as 'snsapi_base' | 'snsapi_userinfo') || 'snsapi_base';
const authUrl = wechatMpService.generateAuthUrl(redirectUri, state, scope);
logger.info({ redirectUri, scope }, 'Generated WeChat MP auth URL');
return res.json({
success: true,
data: {
authUrl,
state,
},
});
} catch (error) {
logger.error({ error }, 'Failed to generate auth URL');
return res.status(500).json({
success: false,
error: 'Internal server error',
});
}
});
/**
* POST /api/mp/login
* 用code完成登录
* 前端在授权回调后携带code调用此接口完成登录
*/
router.post('/login', async (req: Request, res: Response) => {
try {
const { code, state } = req.body;
if (!code) {
return res.status(400).json({
success: false,
error: 'Missing code parameter',
});
}
if (!state) {
return res.status(400).json({
success: false,
error: 'Missing state parameter',
});
}
if (!wechatMpService.isConfigured()) {
return res.json({
success: false,
error: 'WeChat MP not configured',
});
}
const isValidState = await wechatMpService.validateState(state);
if (!isValidState) {
return res.status(400).json({
success: false,
error: 'Invalid or expired state',
});
}
const result = await wechatMpService.login(code);
if (!result.success) {
return res.json({
success: false,
error: result.error,
});
}
return res.json({
success: true,
data: result.data,
});
} catch (error) {
logger.error({ error }, 'Failed to process MP login');
return res.status(500).json({
success: false,
error: 'Internal server error',
});
}
});
/**
* GET /api/mp/config
* 获取公众号配置状态(不返回敏感信息)
*/
router.get('/config', (_req: Request, res: Response) => {
return res.json({
success: true,
data: {
configured: wechatMpService.isConfigured(),
},
});
});
function isRedirectAllowed(redirectUri: string): boolean {
try {
const url = new URL(redirectUri);
if (!['http:', 'https:'].includes(url.protocol)) return false;
const allowlist = config.wechatMp.redirectAllowlist;
if (allowlist.length > 0) {
return allowlist.includes(url.host);
}
const fallbackHost = new URL(config.mobileClientUrl).host;
return url.host === fallbackHost;
} catch {
return false;
}
}
export default router;