Add WeChat login binding gate and context-aware auth UX.
Stop auto-creating duplicate accounts on OAuth, add bind-or-register gate, PC scan login, mobile open-in-WeChat guide, and fix localhost session cookies. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
+169
-5
@@ -15,6 +15,7 @@ import { createTkmindProxy } from './tkmind-proxy.mjs';
|
||||
import {
|
||||
clearUserSessionCookie,
|
||||
createUserAuth,
|
||||
resolveCookieDomainForRequest,
|
||||
USER_COOKIE,
|
||||
userLoginCookies,
|
||||
userSessionCookie,
|
||||
@@ -340,6 +341,20 @@ function userToken(req) {
|
||||
return parseCookies(req.get('cookie'))[USER_COOKIE];
|
||||
}
|
||||
|
||||
function setUserLoginCookies(res, req, token) {
|
||||
res.set(
|
||||
'Set-Cookie',
|
||||
userLoginCookies(token, isSecureRequest(req), resolveCookieDomainForRequest(req)),
|
||||
);
|
||||
}
|
||||
|
||||
function clearUserLoginCookies(res, req) {
|
||||
res.set(
|
||||
'Set-Cookie',
|
||||
clearUserSessionCookie(isSecureRequest(req), resolveCookieDomainForRequest(req)),
|
||||
);
|
||||
}
|
||||
|
||||
async function attachUserSession(req, _res, next) {
|
||||
if (!userAuth) return next();
|
||||
const token = userToken(req);
|
||||
@@ -394,7 +409,7 @@ app.post('/auth/login', jsonBody, async (req, res) => {
|
||||
}
|
||||
return res.status(401).json({ message: result.message });
|
||||
}
|
||||
res.set('Set-Cookie', userLoginCookies(result.token, secure));
|
||||
setUserLoginCookies(res, req, result.token);
|
||||
return res.json({ authenticated: true, user: result.user, mode: 'user' });
|
||||
}
|
||||
|
||||
@@ -460,18 +475,145 @@ app.post('/auth/reset-password', jsonBody, async (req, res) => {
|
||||
app.get('/auth/wechat/config', async (req, res) => {
|
||||
await userAuthReady;
|
||||
if (!wechatOAuthService?.enabled) {
|
||||
return res.json({ enabled: false, inWechat: isWechatUserAgent(req.get('user-agent') || '') });
|
||||
return res.json({
|
||||
enabled: false,
|
||||
inWechat: isWechatUserAgent(req.get('user-agent') || ''),
|
||||
scanEnabled: false,
|
||||
});
|
||||
}
|
||||
return res.json(wechatOAuthService.publicConfig(req));
|
||||
});
|
||||
|
||||
app.get('/auth/wechat/status', async (req, res) => {
|
||||
await userAuthReady;
|
||||
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 = loadWechatOAuthConfig();
|
||||
const status = await userAuth.getWechatBindingStatus(me.id, config.appId);
|
||||
return res.json({ enabled: true, ...status });
|
||||
});
|
||||
|
||||
app.get('/auth/wechat/pending/:token', async (req, res) => {
|
||||
await userAuthReady;
|
||||
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);
|
||||
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 });
|
||||
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(() => {});
|
||||
}
|
||||
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);
|
||||
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 });
|
||||
}
|
||||
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;
|
||||
if (!wechatOAuthService?.enabled) {
|
||||
return res.status(503).json({ message: '微信登录未启用' });
|
||||
}
|
||||
try {
|
||||
const payload = await wechatOAuthService.startScanLogin(req);
|
||||
return res.json(payload);
|
||||
} catch (err) {
|
||||
return res.status(503).json({
|
||||
message: err instanceof Error ? err.message : '微信扫码登录不可用',
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
app.get('/auth/wechat/scan/poll', async (req, res) => {
|
||||
await userAuthReady;
|
||||
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;
|
||||
if (!wechatOAuthService?.enabled) {
|
||||
return res.status(503).json({ message: '微信登录未启用' });
|
||||
}
|
||||
try {
|
||||
const redirectUrl = await wechatOAuthService.buildAuthorizeRedirect(req);
|
||||
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 (err) {
|
||||
console.error('WeChat authorize failed:', err);
|
||||
@@ -491,6 +633,23 @@ app.get('/auth/wechat/callback', async (req, res) => {
|
||||
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 || '微信登录失败')}`,
|
||||
);
|
||||
}
|
||||
|
||||
if (result.isNewUser && plazaSeo) {
|
||||
void plazaSeo
|
||||
.recordAttribution(
|
||||
@@ -505,7 +664,12 @@ app.get('/auth/wechat/callback', async (req, res) => {
|
||||
)
|
||||
.catch(() => {});
|
||||
}
|
||||
res.set('Set-Cookie', userLoginCookies(result.token, secure));
|
||||
|
||||
if (result.authMode === 'open') {
|
||||
return res.send(`<!DOCTYPE html><html lang="zh-CN"><head><meta charset="utf-8"><title>微信登录</title></head><body><p>扫码登录成功,请返回电脑继续操作。</p></body></html>`);
|
||||
}
|
||||
|
||||
setUserLoginCookies(res, req, result.token);
|
||||
return res.redirect(302, result.returnTo || '/');
|
||||
} catch (err) {
|
||||
console.error('WeChat callback failed:', err);
|
||||
@@ -537,7 +701,7 @@ app.post('/auth/logout', async (req, res) => {
|
||||
const secure = isSecureRequest(req);
|
||||
if (userAuth) {
|
||||
await userAuth.revoke(userToken(req));
|
||||
res.set('Set-Cookie', clearUserSessionCookie(secure));
|
||||
clearUserLoginCookies(res, req);
|
||||
}
|
||||
if (legacyAuth) {
|
||||
legacyAuth.revoke(legacySessionToken(req));
|
||||
|
||||
Reference in New Issue
Block a user