feat(auth): 登录态改为滑动续期(后台 24h/7d,打手 7d/30d)
This commit is contained in:
@@ -46,11 +46,14 @@ import {
|
||||
} from '@/services/admin'
|
||||
import {
|
||||
clearAdminSession,
|
||||
expireAdminSession,
|
||||
getAdminDisplayName,
|
||||
getAdminRole,
|
||||
getAdminSessionExpiryDelay,
|
||||
getAdminToken,
|
||||
getAdminTokenExpiresAt,
|
||||
getAdminUsername,
|
||||
isAdminSessionStorageKey,
|
||||
setAdminDisplayName,
|
||||
setAdminPermissions,
|
||||
} from '@/utils/admin-auth'
|
||||
@@ -77,7 +80,7 @@ export default function AdminLayout() {
|
||||
)
|
||||
const role = getAdminRole()
|
||||
const username = getAdminUsername() || 'admin'
|
||||
const expiresAt = getAdminTokenExpiresAt()
|
||||
const [expiresAt, setExpiresAt] = useState(() => getAdminTokenExpiresAt())
|
||||
const [soundEnabled, setSoundEnabled] = useState(
|
||||
() => window.localStorage.getItem(NOTIFICATION_SOUND_KEY) === '1',
|
||||
)
|
||||
@@ -96,6 +99,43 @@ export default function AdminLayout() {
|
||||
}
|
||||
}, [adminSessionQuery.data])
|
||||
|
||||
useEffect(() => {
|
||||
const navigateToLogin = () => {
|
||||
if (!window.location.hash.startsWith('#/admin/login')) {
|
||||
void navigate('/admin/login', { replace: true })
|
||||
}
|
||||
}
|
||||
const handleSessionExpired = () => navigateToLogin()
|
||||
const handleSessionRenewed = () => setExpiresAt(getAdminTokenExpiresAt())
|
||||
const handleStorage = (event: StorageEvent) => {
|
||||
if (!isAdminSessionStorageKey(event.key)) return
|
||||
if (event.newValue === null) {
|
||||
expireAdminSession()
|
||||
navigateToLogin()
|
||||
return
|
||||
}
|
||||
handleSessionRenewed()
|
||||
}
|
||||
|
||||
window.addEventListener('admin-session-expired', handleSessionExpired)
|
||||
window.addEventListener('admin-session-renewed', handleSessionRenewed)
|
||||
window.addEventListener('storage', handleStorage)
|
||||
return () => {
|
||||
window.removeEventListener('admin-session-expired', handleSessionExpired)
|
||||
window.removeEventListener('admin-session-renewed', handleSessionRenewed)
|
||||
window.removeEventListener('storage', handleStorage)
|
||||
}
|
||||
}, [navigate])
|
||||
|
||||
useEffect(() => {
|
||||
const delay = getAdminSessionExpiryDelay(expiresAt)
|
||||
if (delay === null) return
|
||||
const timer = window.setTimeout(() => {
|
||||
expireAdminSession()
|
||||
}, delay)
|
||||
return () => window.clearTimeout(timer)
|
||||
}, [expiresAt])
|
||||
|
||||
const displayName =
|
||||
adminSessionQuery.data?.data.user?.displayName || getAdminDisplayName() || username
|
||||
|
||||
@@ -121,7 +161,7 @@ export default function AdminLayout() {
|
||||
onEvent: (event) => applyAdminRealtimeEvent(event, queryClient),
|
||||
// 会话过期时清登录态回登录页,避免 SSE 带着失效 token 无限重连刷 401。
|
||||
onAuthExpired: () => {
|
||||
clearAdminSession()
|
||||
expireAdminSession()
|
||||
void navigate('/admin/login', { replace: true, state: { from: location } })
|
||||
},
|
||||
})
|
||||
|
||||
@@ -9,7 +9,7 @@ import {
|
||||
import { App, Button, Layout, Menu, Typography } from 'antd'
|
||||
import { useQuery, useQueryClient } from '@tanstack/react-query'
|
||||
import type { MenuProps } from 'antd'
|
||||
import { useEffect, useMemo } from 'react'
|
||||
import { useEffect, useMemo, useState } from 'react'
|
||||
import { Outlet, useLocation, useNavigate } from 'react-router'
|
||||
|
||||
import { fetchWorkerAfterSalesCases, fetchWorkerProfile, logoutWorker } from '@/services/worker'
|
||||
@@ -29,6 +29,7 @@ import {
|
||||
getWorkerToken,
|
||||
getWorkerTokenExpiresAt,
|
||||
getWorkerUsername,
|
||||
isWorkerSessionExpiryKey,
|
||||
isWorkerSessionStorageKey,
|
||||
} from '@/utils/worker-auth'
|
||||
import { useRealtimeEvents, type RealtimeEvent } from '@/hooks/useRealtimeEvents'
|
||||
@@ -100,6 +101,8 @@ export default function WorkerLayout() {
|
||||
[afterSalesPendingCount],
|
||||
)
|
||||
|
||||
const [sessionExpiresAt, setSessionExpiresAt] = useState(() => getWorkerTokenExpiresAt())
|
||||
|
||||
useEffect(() => {
|
||||
const navigateToLogin = () => {
|
||||
if (window.location.pathname !== '/') {
|
||||
@@ -107,30 +110,38 @@ export default function WorkerLayout() {
|
||||
}
|
||||
}
|
||||
const handleSessionExpired = () => navigateToLogin()
|
||||
const handleSessionRenewed = () => setSessionExpiresAt(getWorkerTokenExpiresAt())
|
||||
const handleStorage = (event: StorageEvent) => {
|
||||
if (isWorkerSessionStorageKey(event.key) && event.newValue === null) {
|
||||
if (!isWorkerSessionStorageKey(event.key)) return
|
||||
if (event.newValue === null) {
|
||||
expireWorkerSession()
|
||||
navigateToLogin()
|
||||
return
|
||||
}
|
||||
if (isWorkerSessionExpiryKey(event.key)) {
|
||||
handleSessionRenewed()
|
||||
}
|
||||
}
|
||||
|
||||
window.addEventListener('worker-session-expired', handleSessionExpired)
|
||||
window.addEventListener('worker-session-renewed', handleSessionRenewed)
|
||||
window.addEventListener('storage', handleStorage)
|
||||
return () => {
|
||||
window.removeEventListener('worker-session-expired', handleSessionExpired)
|
||||
window.removeEventListener('worker-session-renewed', handleSessionRenewed)
|
||||
window.removeEventListener('storage', handleStorage)
|
||||
}
|
||||
}, [navigate])
|
||||
|
||||
useEffect(() => {
|
||||
const delay = getWorkerSessionExpiryDelay(getWorkerTokenExpiresAt())
|
||||
const delay = getWorkerSessionExpiryDelay(sessionExpiresAt)
|
||||
if (delay === null) return
|
||||
|
||||
const timer = window.setTimeout(() => {
|
||||
expireWorkerSession()
|
||||
}, delay)
|
||||
return () => window.clearTimeout(timer)
|
||||
}, [])
|
||||
}, [sessionExpiresAt])
|
||||
|
||||
useRealtimeEvents({
|
||||
url: '/api/v1/worker/realtime',
|
||||
|
||||
Reference in New Issue
Block a user