Files
memind/server/portal-wechat-auth-routes.mjs
T

662 lines
16 KiB
JavaScript

import {
isWechatUserAgent,
loadWechatOAuthConfig,
} from '../wechat-oauth.mjs';
import {
validateWechatShareSignatureUrl,
} from '../wechat-share.mjs';
export function attachPortalWechatAuthRoutes({
app,
jsonBody,
userAuthReady = Promise.resolve(),
getUserAuth = () => null,
getWechatOAuthService = () => null,
getWechatMpService = () => null,
wechatMpConfig,
userToken,
setUserLoginCookies,
isSecureRequest,
getPlazaSeo = () => null,
plazaClientIp = (req) => req.ip,
logger = console,
isWechatUserAgentFn = isWechatUserAgent,
loadWechatOAuthConfigFn = loadWechatOAuthConfig,
validateWechatShareSignatureUrlFn =
validateWechatShareSignatureUrl,
} = {}) {
if (
!app ||
typeof jsonBody !== 'function' ||
!wechatMpConfig ||
typeof userToken !== 'function' ||
typeof setUserLoginCookies !== 'function' ||
typeof isSecureRequest !== 'function'
) {
throw new Error(
'attachPortalWechatAuthRoutes requires route dependencies',
);
}
app.get('/auth/wechat/config', async (req, res) => {
await userAuthReady;
const wechatOAuthService =
getWechatOAuthService();
if (!wechatOAuthService?.enabled) {
return res.json({
enabled: false,
inWechat: isWechatUserAgentFn(
req.get('user-agent') || '',
),
scanEnabled: false,
});
}
return res.json(
wechatOAuthService.publicConfig(req),
);
});
app.get(
'/auth/wechat/js-sdk-signature',
async (req, res) => {
await userAuthReady;
const userAuth = getUserAuth();
const wechatMpService =
getWechatMpService();
if (!userAuth || !wechatMpService?.enabled) {
return res.status(503).json({
message: '微信 JS-SDK 未启用',
});
}
const me = await userAuth.getMe(userToken(req));
if (!me) {
return res.status(401).json({
message: '未登录',
});
}
const pageUrl = String(
req.query?.url ?? '',
).split('#')[0];
if (!pageUrl) {
return res.status(400).json({
message: '缺少 url',
});
}
try {
const normalizedUrl =
validateWechatShareSignatureUrlFn(pageUrl, {
publicBaseUrl:
wechatMpConfig.publicBaseUrl,
requestHost:
req.get('x-forwarded-host') ||
req.get('host') ||
'',
});
const payload =
await wechatMpService.createJsSdkSignature(
normalizedUrl,
);
return res.json(payload);
} catch (error) {
const message =
error instanceof Error
? error.message
: '微信 JS-SDK 签名失败';
logger.warn(
'WeChat JS-SDK signature failed:',
message,
);
return res.status(502).json({ message });
}
},
);
app.get(
'/auth/wechat/public-js-sdk-signature',
async (req, res) => {
await userAuthReady;
const wechatMpService =
getWechatMpService();
if (!wechatMpService?.enabled) {
return res.status(503).json({
message: '微信 JS-SDK 未启用',
});
}
const pageUrl = String(
req.query?.url ?? '',
).split('#')[0];
if (!pageUrl) {
return res.status(400).json({
message: '缺少 url',
});
}
try {
const normalizedUrl =
validateWechatShareSignatureUrlFn(pageUrl, {
publicBaseUrl:
wechatMpConfig.publicBaseUrl,
requestHost:
req.get('x-forwarded-host') ||
req.get('host') ||
'',
});
const payload =
await wechatMpService.createJsSdkSignature(
normalizedUrl,
);
return res.json(payload);
} catch (error) {
const message =
error instanceof Error
? error.message
: '微信 JS-SDK 签名失败';
logger.warn(
'WeChat public JS-SDK signature failed:',
message,
);
return res.status(502).json({ message });
}
},
);
app.get('/auth/wechat/status', async (req, res) => {
await userAuthReady;
const userAuth = getUserAuth();
const wechatOAuthService =
getWechatOAuthService();
if (!userAuth || !wechatOAuthService?.enabled) {
return res.json({
enabled: false,
bound: false,
});
}
const me = await userAuth.getMe(userToken(req));
if (!me) {
return res.status(401).json({
message: '未登录',
});
}
const config = loadWechatOAuthConfigFn();
const status =
await userAuth.getWechatBindingStatus(
me.id,
config.appId,
);
return res.json({
enabled: true,
...status,
});
});
if (wechatMpConfig.enabled) {
app.get(
'/auth/wechat/agent-route',
async (req, res) => {
await userAuthReady;
const userAuth = getUserAuth();
const wechatMpService =
getWechatMpService();
if (
!userAuth ||
!wechatMpService?.enabled
) {
return res.status(503).json({
message: '公众号 Agent 未启用',
});
}
const me = await userAuth.getMe(
userToken(req),
);
if (!me) {
return res.status(401).json({
message: '未登录',
});
}
return res.json(
await wechatMpService.getRouteStatusForUser(
me.id,
),
);
},
);
app.post(
'/auth/wechat/agent-route/reset',
async (req, res) => {
await userAuthReady;
const userAuth = getUserAuth();
const wechatMpService =
getWechatMpService();
if (
!userAuth ||
!wechatMpService?.enabled
) {
return res.status(503).json({
message: '公众号 Agent 未启用',
});
}
const me = await userAuth.getMe(
userToken(req),
);
if (!me) {
return res.status(401).json({
message: '未登录',
});
}
try {
const result =
await wechatMpService.recreateRouteForUser(
me.id,
);
if (!result.ok) {
return res.status(400).json({
message: result.message,
});
}
return res.json(result);
} catch (error) {
return res.status(500).json({
message:
error instanceof Error
? error.message
: '重建公众号 Agent 路由失败',
});
}
},
);
}
app.get(
'/auth/wechat/pending/:token',
async (req, res) => {
await userAuthReady;
const userAuth = getUserAuth();
if (!userAuth) {
return res.status(503).json({
message: '未启用用户系统',
});
}
const pending =
await userAuth.getWechatPendingBind(
req.params.token,
);
if (!pending) {
return res.status(404).json({
message: '绑定会话已过期,请重新微信登录',
});
}
return res.json({
nickname: pending.nickname,
avatarUrl: pending.avatar_url,
returnTo: pending.return_to || '/',
});
},
);
app.post(
'/auth/wechat/register',
jsonBody,
async (req, res) => {
await userAuthReady;
const secure = isSecureRequest(req);
const userAuth = getUserAuth();
if (!userAuth) {
return res.status(503).json({
message: '未启用用户系统',
});
}
const pendingToken =
typeof req.body?.pendingToken === 'string'
? req.body.pendingToken
: '';
if (!pendingToken) {
return res.status(400).json({
message: '缺少绑定会话',
});
}
const result =
await userAuth.completeWechatRegister({
pendingToken,
});
if (!result.ok) {
return res.status(400).json({
message: result.message,
});
}
const plazaSeo = getPlazaSeo();
if (result.isNewUser && plazaSeo) {
void plazaSeo
.recordAttribution(
{
event_type: 'signup',
utm_source:
result.utmSource || 'wechat',
utm_medium: result.utmMedium,
utm_campaign: result.utmCampaign,
user_id: result.user?.id ?? null,
},
plazaClientIp(req),
)
.catch(() => {});
}
void secure;
setUserLoginCookies(
res,
req,
result.token,
);
return res.json({
authenticated: true,
user: result.user,
returnTo: result.returnTo || '/',
});
},
);
app.post(
'/auth/wechat/bind',
jsonBody,
async (req, res) => {
await userAuthReady;
const secure = isSecureRequest(req);
const userAuth = getUserAuth();
if (!userAuth) {
return res.status(503).json({
message: '未启用用户系统',
});
}
const pendingToken =
typeof req.body?.pendingToken === 'string'
? req.body.pendingToken
: '';
const username =
typeof req.body?.username === 'string'
? req.body.username
: '';
const password =
typeof req.body?.password === 'string'
? req.body.password
: '';
if (!pendingToken) {
return res.status(400).json({
message: '缺少绑定会话',
});
}
if (!username || !password) {
return res.status(400).json({
message: '用户名和密码不能为空',
});
}
const result =
await userAuth.completeWechatBindAccount({
pendingToken,
username,
password,
ip: req.ip,
});
if (!result.ok) {
const status =
result.retryAfterMs > 0 ? 429 : 401;
if (result.retryAfterMs > 0) {
res.set(
'Retry-After',
String(
Math.ceil(result.retryAfterMs / 1000),
),
);
}
return res.status(status).json({
message: result.message,
});
}
void secure;
setUserLoginCookies(
res,
req,
result.token,
);
return res.json({
authenticated: true,
user: result.user,
bound: true,
returnTo: result.returnTo || '/',
});
},
);
app.post(
'/auth/wechat/scan/start',
async (req, res) => {
await userAuthReady;
const wechatOAuthService =
getWechatOAuthService();
if (!wechatOAuthService?.enabled) {
return res.status(503).json({
message: '微信登录未启用',
});
}
try {
const payload =
await wechatOAuthService.startScanLogin(
req,
);
return res.json(payload);
} catch (error) {
return res.status(503).json({
message:
error instanceof Error
? error.message
: '微信扫码登录不可用',
});
}
},
);
app.get(
'/auth/wechat/scan/poll',
async (req, res) => {
await userAuthReady;
const wechatOAuthService =
getWechatOAuthService();
if (!wechatOAuthService?.enabled) {
return res.status(503).json({
message: '微信登录未启用',
});
}
const state =
typeof req.query?.state === 'string'
? req.query.state
: '';
if (!state) {
return res.status(400).json({
message: '缺少扫码状态',
});
}
const result =
await wechatOAuthService.pollScanLogin(
state,
);
if (
result.status === 'complete' &&
result.token
) {
setUserLoginCookies(
res,
req,
result.token,
);
}
return res.json(result);
},
);
app.get(
'/auth/wechat/authorize',
async (req, res) => {
await userAuthReady;
const wechatOAuthService =
getWechatOAuthService();
if (!wechatOAuthService?.enabled) {
return res.status(503).json({
message: '微信登录未启用',
});
}
try {
const userAuth = getUserAuth();
let bindUserId = null;
const intent =
typeof req.query?.intent === 'string'
? req.query.intent
.trim()
.toLowerCase()
: 'login';
if (intent === 'bind' && userAuth) {
const me = await userAuth.getMe(
userToken(req),
);
if (!me) {
return res.status(401).json({
message: '请先登录后再绑定微信',
});
}
bindUserId = me.id;
}
const redirectUrl =
await wechatOAuthService.buildAuthorizeRedirect(
req,
{ bindUserId },
);
return res.redirect(302, redirectUrl);
} catch (error) {
logger.error(
'WeChat authorize failed:',
error,
);
return res.status(500).json({
message:
error instanceof Error
? error.message
: '微信授权失败',
});
}
},
);
app.get(
'/auth/wechat/callback',
async (req, res) => {
await userAuthReady;
const secure = isSecureRequest(req);
const wechatOAuthService =
getWechatOAuthService();
const userAuth = getUserAuth();
if (
!wechatOAuthService?.enabled ||
!userAuth
) {
return res.redirect(
302,
'/?wechat_error=unavailable',
);
}
try {
const result =
await wechatOAuthService.handleCallback({
code:
typeof req.query?.code === 'string'
? req.query.code
: '',
state:
typeof req.query?.state === 'string'
? req.query.state
: '',
ip: req.ip,
});
if (result.action === 'binding_gate') {
const params = new URLSearchParams();
params.set(
'wechat_pending',
result.pendingToken,
);
if (
result.returnTo &&
result.returnTo !== '/'
) {
params.set(
'return_to',
result.returnTo,
);
}
return res.redirect(
302,
`/?${params.toString()}`,
);
}
if (result.action === 'poll_error') {
return res.redirect(
302,
`/?wechat_error=${encodeURIComponent(
result.message || '微信登录失败',
)}`,
);
}
const plazaSeo = getPlazaSeo();
if (result.isNewUser && plazaSeo) {
void plazaSeo
.recordAttribution(
{
event_type: 'signup',
utm_source:
result.utmSource || 'wechat',
utm_medium: result.utmMedium,
utm_campaign:
result.utmCampaign,
user_id:
result.user?.id ?? null,
},
plazaClientIp(req),
)
.catch(() => {});
}
if (
result.authMode === 'open' ||
result.authMode === 'scan'
) {
return res.send(
'<!DOCTYPE html><html lang="zh-CN"><head><meta charset="utf-8"><title>微信登录</title></head><body><p>扫码登录成功,请返回电脑继续操作。</p></body></html>',
);
}
void secure;
setUserLoginCookies(
res,
req,
result.token,
);
return res.redirect(
302,
result.returnTo || '/',
);
} catch (error) {
logger.error(
'WeChat callback failed:',
error,
);
const message = encodeURIComponent(
error instanceof Error
? error.message
: '微信登录失败',
);
return res.redirect(
302,
`/?wechat_error=${message}`,
);
}
},
);
}