149 lines
3.5 KiB
TypeScript
149 lines
3.5 KiB
TypeScript
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;
|