study / web /src /lib /use-auth-guard.ts
xiaolongmr
降低线上轮询压力:后台标签页暂停轮询、关闭 access log
d188720
Raw History Blame Contribute Delete
3.84 kB
"use client";
import { useEffect, useState } from "react";
import { useRouter } from "next/navigation";
import { getValidatedAuthSession } from "@/lib/auth-session";
import {
getDefaultRouteForRole,
type AuthRole,
type StoredAuthSession,
} from "@/store/auth";
type UseAuthGuardResult = {
isCheckingAuth: boolean;
session: StoredAuthSession | null;
};
/**
* 权限 / 登录态的兜底同步间隔。
*
* ⚠️ 原来是 5 秒一次 **force** 校验(绕过 10 秒本地缓存,每次都实打服务端),
* 这是全站最重的轮询接口 —— 线上实测 /auth/session 约 375 req/s,占全站请求 20%。
* 而权限和登录态本来就不会 5 秒一变,30 秒完全够用;
* 另外下面还监听 focus / visibilitychange,用户切回标签页会**立刻**强制校验一次,
* 所以真实感知比 30 秒更灵敏。
*/
const AUTH_PERMISSION_SYNC_INTERVAL_MS = 30_000;
function sameSession(left: StoredAuthSession | null, right: StoredAuthSession) {
return Boolean(
left
&& left.key === right.key
&& left.role === right.role
&& left.subjectId === right.subjectId
&& left.name === right.name
&& left.accountPoolEnabled === right.accountPoolEnabled
&& left.imageMemoryButtonEnabled === right.imageMemoryButtonEnabled
&& left.imageShareEvidenceEnabled === right.imageShareEvidenceEnabled
&& left.wechatOpenid === right.wechatOpenid
&& left.expiresAt === right.expiresAt,
);
}
export function useAuthGuard(allowedRoles?: AuthRole[]): UseAuthGuardResult {
const router = useRouter();
const [session, setSession] = useState<StoredAuthSession | null>(null);
const [isCheckingAuth, setIsCheckingAuth] = useState(true);
const allowedRolesKey = (allowedRoles || []).join(",");
useEffect(() => {
let active = true;
const load = async (force = false) => {
const roleList = allowedRolesKey ? (allowedRolesKey.split(",") as AuthRole[]) : [];
const storedSession = await getValidatedAuthSession({ force });
if (!active) {
return;
}
if (!storedSession) {
setSession(null);
setIsCheckingAuth(false);
router.replace("/login");
return;
}
if (roleList.length > 0 && !roleList.includes(storedSession.role)) {
setSession(storedSession);
setIsCheckingAuth(false);
router.replace(getDefaultRouteForRole(storedSession.role));
return;
}
setSession((current) => sameSession(current, storedSession) ? current : storedSession);
setIsCheckingAuth(false);
};
void load();
const syncPermissions = () => {
if (document.visibilityState === "visible") {
void load(true);
}
};
const intervalId = window.setInterval(syncPermissions, AUTH_PERMISSION_SYNC_INTERVAL_MS);
window.addEventListener("focus", syncPermissions);
document.addEventListener("visibilitychange", syncPermissions);
return () => {
active = false;
window.clearInterval(intervalId);
window.removeEventListener("focus", syncPermissions);
document.removeEventListener("visibilitychange", syncPermissions);
};
}, [allowedRolesKey, router]);
return { isCheckingAuth, session };
}
export function useRedirectIfAuthenticated() {
const router = useRouter();
const [isCheckingAuth, setIsCheckingAuth] = useState(true);
useEffect(() => {
let active = true;
const load = async () => {
const storedSession = await getValidatedAuthSession();
if (!active) {
return;
}
if (storedSession) {
router.replace(getDefaultRouteForRole(storedSession.role));
return;
}
setIsCheckingAuth(false);
};
void load();
return () => {
active = false;
};
}, [router]);
return { isCheckingAuth };
}