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

353 lines
9.0 KiB
JavaScript

import { sessionCookie } from '../auth.mjs';
import { isDatabaseConfigured } from '../db.mjs';
import {
exchangeMiniProgramCode,
loadWechatMiniappConfig,
} from '../wechat-miniapp.mjs';
export function attachPortalCoreAuthRoutes({
app,
jsonBody,
userAuthReady = Promise.resolve(),
getUserAuth = () => null,
getLegacyAuth = () => null,
userToken,
legacySessionToken,
setUserLoginCookies,
isSecureRequest,
resolveSkillRuntimeForClient = async () => null,
resolveAgentCodeRunForClient = async () => null,
getPlazaSeo = () => null,
plazaClientIp = (req) => req.ip,
logger = console,
isDatabaseConfiguredFn = isDatabaseConfigured,
sessionCookieFn = sessionCookie,
loadWechatMiniappConfigFn =
loadWechatMiniappConfig,
exchangeMiniProgramCodeFn =
exchangeMiniProgramCode,
} = {}) {
if (
!app ||
typeof jsonBody !== 'function' ||
typeof userToken !== 'function' ||
typeof legacySessionToken !== 'function' ||
typeof setUserLoginCookies !== 'function' ||
typeof isSecureRequest !== 'function'
) {
throw new Error(
'attachPortalCoreAuthRoutes requires route dependencies',
);
}
app.get('/auth/status', async (req, res) => {
await userAuthReady;
const userAuth = getUserAuth();
if (userAuth) {
try {
const me = await userAuth.getMe(userToken(req));
if (!me) {
return res.json({
authenticated: false,
mode: 'user',
});
}
const row = await userAuth.getUserById(me.id);
const capabilityState =
await userAuth.resolveUserCapabilities(row);
const skillRuntime =
await resolveSkillRuntimeForClient();
const agentCodeRun =
await resolveAgentCodeRunForClient(me.id);
return res.json({
authenticated: true,
user: me,
mode: 'user',
capabilities: capabilityState.capabilities,
grantedSkills:
capabilityState.grantedSkills ?? [],
unrestricted: capabilityState.unrestricted,
skillRuntime,
agentCodeRun,
});
} catch (error) {
logger.error(
'[Auth] status failed:',
error instanceof Error
? error.message
: error,
);
return res.status(503).json({
authenticated: false,
mode: 'unavailable',
message: '用户认证服务不可用,请稍后重试',
});
}
}
if (isDatabaseConfiguredFn()) {
return res.status(503).json({
authenticated: false,
mode: 'unavailable',
message: '用户认证服务不可用,请稍后重试',
});
}
const legacyAuth = getLegacyAuth();
if (legacyAuth) {
return res.json({
authenticated: legacyAuth.verify(
legacySessionToken(req),
),
mode: 'legacy',
});
}
return res.json({
authenticated: false,
mode: 'none',
});
});
app.post(
'/auth/login',
jsonBody,
async (req, res) => {
await userAuthReady;
const secure = isSecureRequest(req);
const userAuth = getUserAuth();
if (userAuth) {
const { username, password } = req.body ?? {};
if (!username || !password) {
return res.status(400).json({
message: '用户名和密码不能为空',
});
}
const result = await userAuth.login({
username,
password,
ip: req.ip,
});
if (!result.ok) {
if (result.retryAfterMs > 0) {
res.set(
'Retry-After',
String(
Math.ceil(result.retryAfterMs / 1000),
),
);
return res.status(429).json({
message: result.message,
});
}
return res.status(401).json({
message: result.message,
});
}
setUserLoginCookies(
res,
req,
result.token,
);
return res.json({
authenticated: true,
user: result.user,
mode: 'user',
sessionToken: result.token,
});
}
const legacyAuth = getLegacyAuth();
if (!legacyAuth) {
return res.status(503).json({
message: '未配置用户数据库或访问密码',
});
}
const password =
typeof req.body?.password === 'string'
? req.body.password
: '';
const result = legacyAuth.login(password, req.ip);
if (!result.ok) {
if (result.retryAfterMs > 0) {
res.set(
'Retry-After',
String(
Math.ceil(result.retryAfterMs / 1000),
),
);
return res.status(429).json({
message: '尝试次数过多,请稍后再试',
});
}
return res.status(401).json({
message: '密码错误,请重试',
});
}
res.set(
'Set-Cookie',
sessionCookieFn(result.token, secure),
);
return res.json({
authenticated: true,
mode: 'legacy',
});
},
);
app.post(
'/auth/wechat-miniapp/login',
jsonBody,
async (req, res) => {
await userAuthReady;
const userAuth = getUserAuth();
if (!userAuth) {
return res.status(503).json({
message: '未启用用户系统',
});
}
const miniappConfig =
loadWechatMiniappConfigFn();
if (!miniappConfig.enabled) {
return res.status(503).json({
message: '小程序登录未配置,请联系管理员',
});
}
const code =
typeof req.body?.code === 'string'
? req.body.code.trim()
: '';
if (!code) {
return res.status(400).json({
message: '缺少微信登录 code',
});
}
try {
const session =
await exchangeMiniProgramCodeFn({
appId: miniappConfig.appId,
appSecret: miniappConfig.appSecret,
code,
});
const result =
await userAuth.loginByWechatMiniProgram({
appId: miniappConfig.appId,
openid: session.openid,
unionid: session.unionid,
});
if (!result.ok) {
return res.status(401).json({
message:
result.message || '微信登录失败',
});
}
setUserLoginCookies(
res,
req,
result.token,
);
return res.json({
authenticated: true,
user: result.user,
mode: 'user',
isNewUser: Boolean(result.isNewUser),
sessionToken: result.token,
});
} catch (error) {
const message =
error instanceof Error
? error.message
: '微信登录失败';
const status =
error?.code ===
'wechat_miniapp_code_failed'
? 401
: 400;
return res.status(status).json({ message });
}
},
);
app.post(
'/auth/register',
jsonBody,
async (req, res) => {
await userAuthReady;
const userAuth = getUserAuth();
if (!userAuth) {
return res.status(503).json({
message: '未启用用户注册',
});
}
const {
username,
password,
displayName,
email,
} = req.body ?? {};
const result = await userAuth.register({
username,
password,
displayName,
email,
});
if (!result.ok) {
const status = result.message.includes(
'已存在',
)
? 409
: 400;
return res.status(status).json({
message: result.message,
});
}
const plazaSeo = getPlazaSeo();
if (plazaSeo && req.body?.utm_source) {
void plazaSeo
.recordAttribution(
{
event_type: 'signup',
utm_source: req.body.utm_source,
utm_medium: req.body.utm_medium,
utm_campaign: req.body.utm_campaign,
ref_id:
req.body.ref ?? req.body.ref_id,
user_id: result.user?.id ?? null,
},
plazaClientIp(req),
)
.catch(() => {});
}
return res.json({
ok: true,
user: result.user,
});
},
);
app.post(
'/auth/reset-password',
jsonBody,
async (req, res) => {
await userAuthReady;
const userAuth = getUserAuth();
if (!userAuth) {
return res.status(503).json({
message: '未启用用户系统',
});
}
const { username, email, password } =
req.body ?? {};
const result = await userAuth.resetPassword({
username,
email,
password,
});
if (!result.ok) {
return res.status(400).json({
message: result.message,
});
}
return res.json({ ok: true });
},
);
}