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