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: '未授权,请重新登录' }); }; }