Files
memind/server/portal-api-auth-middleware.mjs
T
2026-07-25 09:44:26 +08:00

157 lines
4.9 KiB
JavaScript

import {
comparePortalAccessPolicyShadow,
isPortalLegacyDirectGlobalAuthBypass,
isPortalPlazaOptionalUserPath,
PORTAL_ACCESS_POLICY_MODE,
resolvePortalAccessEnforcementDecision,
shouldOverridePortalLegacyGlobalAuth,
} from './portal-access-policy.mjs';
function resolveAuthMode({ userAuth, tkmindProxy, legacyAuth }) {
if (userAuth && tkmindProxy) return 'multi-user';
if (legacyAuth) return 'legacy';
return 'none';
}
export function createPortalApiAuthMiddleware({
waitForUserAuthReady = async () => {},
getUserAuth = () => null,
getTkmindProxy = () => null,
getLegacyAuth = () => null,
getLegacySessionToken = () => null,
isPageDataPublicPath = () => false,
isLegacyPageDataApiPath = () => false,
isProductAnalyticsPublicPath = () => false,
accessPolicyMode = PORTAL_ACCESS_POLICY_MODE.OFF,
accessEnforcementConfig = Object.freeze({
masterEnabled: false,
killSwitch: false,
enabled: false,
activeGroups: Object.freeze([]),
}),
accessShadowReporter = null,
logger = console,
} = {}) {
return async function portalApiAuthMiddleware(req, res, next) {
await waitForUserAuthReady();
const userAuth = getUserAuth();
const tkmindProxy = getTkmindProxy();
const legacyAuth = getLegacyAuth();
const authMode = resolveAuthMode({ userAuth, tkmindProxy, legacyAuth });
const requestAccess = { path: req.path, method: req.method };
const accessPredicates = {
isPageDataPublicPath,
isLegacyPageDataApiPath,
};
const comparison =
accessPolicyMode === PORTAL_ACCESS_POLICY_MODE.SHADOW ||
accessEnforcementConfig.enabled
? comparePortalAccessPolicyShadow(
requestAccess,
{
multiUserEnabled: Boolean(userAuth && tkmindProxy),
...accessPredicates,
},
)
: null;
if (accessPolicyMode === PORTAL_ACCESS_POLICY_MODE.SHADOW) {
accessShadowReporter?.record({
requestId: req.requestId,
authMode,
...comparison,
});
}
if (accessEnforcementConfig.enabled) {
const decision = resolvePortalAccessEnforcementDecision(
requestAccess,
accessPredicates,
accessEnforcementConfig,
);
if (decision.selected) {
logger.info('[Auth][PortalAccessEnforce]', JSON.stringify({
requestId: req.requestId,
authMode,
path: req.path,
method: req.method,
...decision,
legacyGlobalAuthBypass: comparison.currentGlobalAuthBypass,
behaviorChanged:
decision.bypassGlobalAuth !== comparison.currentGlobalAuthBypass,
}));
// Multi-user public routes must continue through the legacy branch so
// an optional authenticated identity is hydrated onto req.currentUser.
if (
shouldOverridePortalLegacyGlobalAuth(
decision,
comparison.currentGlobalAuthBypass,
)
) {
return next();
}
}
}
if (isPortalLegacyDirectGlobalAuthBypass(req.path, req.method)) {
return next();
}
const plazaPublic = isPortalPlazaOptionalUserPath(req.path, req.method);
const pageDataPublic = isPageDataPublicPath(req.path, req.method);
const productAnalyticsPublic = isProductAnalyticsPublicPath(
req.path,
req.method,
);
// The retired namespace must reach its explicit 410 route instead of
// being converted into a misleading global 401/403 response.
const legacyPageDataApi = isLegacyPageDataApiPath(req.path);
if (userAuth && tkmindProxy) {
if (req.userSessionError) {
if (
plazaPublic ||
pageDataPublic ||
legacyPageDataApi ||
productAnalyticsPublic
) {
return next();
}
return res.status(503).json({ message: '用户认证服务不可用,请稍后重试' });
}
try {
if (req.userSession) {
const me = await userAuth.getMe(req.userToken);
if (me) req.currentUser = me;
}
if (
plazaPublic ||
pageDataPublic ||
legacyPageDataApi ||
productAnalyticsPublic
) {
return next();
}
if (!req.userSession) {
return res.status(401).json({ message: '未授权,请重新登录' });
}
const me = await userAuth.getMe(req.userToken);
if (!me) return res.status(401).json({ message: '登录已过期' });
req.currentUser = me;
return next();
} catch (error) {
logger.error(
'[Auth] API auth failed:',
error instanceof Error ? error.message : error,
);
return res.status(503).json({ message: '用户认证服务不可用,请稍后重试' });
}
}
if (productAnalyticsPublic) return next();
if (legacyAuth?.verify(getLegacySessionToken(req))) return next();
return res.status(401).json({ message: '未授权,请重新登录' });
};
}