93 lines
3.0 KiB
TypeScript
93 lines
3.0 KiB
TypeScript
"use client";
|
|
|
|
import { useEffect, useMemo, useState } from "react";
|
|
import { signIn } from "next-auth/react";
|
|
import AccessTimeOutlinedIcon from "@mui/icons-material/AccessTimeOutlined";
|
|
import {
|
|
Alert,
|
|
Button,
|
|
Dialog,
|
|
DialogActions,
|
|
DialogContent,
|
|
DialogTitle,
|
|
Stack,
|
|
Typography,
|
|
} from "@mui/material";
|
|
import { useTheme } from "@mui/material/styles";
|
|
|
|
import { useAuthStore } from "@/store/authStore";
|
|
|
|
const WARNING_WINDOW_MS = 15 * 60 * 1000;
|
|
|
|
type SessionExpiryDialogProps = {
|
|
expiresAt?: number;
|
|
};
|
|
|
|
export const SessionExpiryDialog = ({
|
|
expiresAt,
|
|
}: SessionExpiryDialogProps) => {
|
|
const theme = useTheme();
|
|
const sessionExpired = useAuthStore((state) => state.sessionExpired);
|
|
const reason = useAuthStore((state) => state.sessionExpiryReason);
|
|
const [now, setNow] = useState(() => Date.now());
|
|
const [warningDismissed, setWarningDismissed] = useState(false);
|
|
|
|
useEffect(() => {
|
|
const timer = window.setInterval(() => setNow(Date.now()), 30_000);
|
|
return () => window.clearInterval(timer);
|
|
}, []);
|
|
|
|
const isExpiringSoon = useMemo(
|
|
() =>
|
|
!sessionExpired &&
|
|
!warningDismissed &&
|
|
typeof expiresAt === "number" &&
|
|
expiresAt > now &&
|
|
expiresAt - now <= WARNING_WINDOW_MS,
|
|
[expiresAt, now, sessionExpired, warningDismissed],
|
|
);
|
|
|
|
const handleReauthenticate = () => {
|
|
const callbackUrl = `${window.location.pathname}${window.location.search}`;
|
|
void signIn("keycloak", { callbackUrl, redirect: true });
|
|
};
|
|
|
|
const isOpen = sessionExpired || isExpiringSoon;
|
|
const title = sessionExpired ? "登录已过期" : "登录即将到期";
|
|
const detail = sessionExpired
|
|
? reason === "session_max_age"
|
|
? "已达到 12 小时的最长连续登录时间。请重新认证后继续。"
|
|
: "无法续期当前登录。请重新认证后继续。"
|
|
: "当前登录将在 15 分钟内到期。请先保存正在编辑的内容。";
|
|
|
|
return (
|
|
<Dialog
|
|
open={isOpen}
|
|
style={{ zIndex: theme.zIndex.tooltip + 1 }}
|
|
disableEscapeKeyDown={sessionExpired}
|
|
onClose={sessionExpired ? undefined : () => setWarningDismissed(true)}
|
|
aria-labelledby="session-expiry-dialog-title"
|
|
>
|
|
<DialogTitle id="session-expiry-dialog-title">{title}</DialogTitle>
|
|
<DialogContent>
|
|
<Stack spacing={2} sx={{ pt: 0.5 }}>
|
|
<Alert icon={<AccessTimeOutlinedIcon />} severity={sessionExpired ? "warning" : "info"}>
|
|
{detail}
|
|
</Alert>
|
|
<Typography variant="body2" color="text.secondary">
|
|
重新认证不会自动重放已失败的写入请求;请在返回后确认内容并再次提交。
|
|
</Typography>
|
|
</Stack>
|
|
</DialogContent>
|
|
<DialogActions>
|
|
{!sessionExpired && (
|
|
<Button onClick={() => setWarningDismissed(true)}>稍后处理</Button>
|
|
)}
|
|
<Button variant="contained" onClick={handleReauthenticate}>
|
|
重新认证
|
|
</Button>
|
|
</DialogActions>
|
|
</Dialog>
|
|
);
|
|
};
|