353 lines
9.0 KiB
JavaScript
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 });
|
|
},
|
|
);
|
|
}
|