Press n or j to go to the next uncovered block, b, p or k for the previous block.
| 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 | 1x 1x 1x | import { useEffect, useRef, useState, useCallback } from 'react'
import { useNavigate } from 'react-router-dom'
import { useAuthStore } from './authStore'
import { Button } from '@/components/ui/button'
const WARNING_BEFORE_MS = 2 * 60 * 1000
const REFRESH_BEFORE_MS = 30 * 1000
const CHECK_INTERVAL_MS = 1000
export function SessionMonitor() {
const navigate = useNavigate()
const expiresAt = useAuthStore((s) => s.expiresAt)
const idleExpiresAt = useAuthStore((s) => s.idleExpiresAt)
const isAuthenticated = useAuthStore((s) => s.isAuthenticated)
const setAuth = useAuthStore((s) => s.setAuth)
const clearAuth = useAuthStore((s) => s.clearAuth)
const user = useAuthStore((s) => s.user)
const [remaining, setRemaining] = useState<number | null>(null)
const hasRefreshed = useRef(false)
const doClearAndRedirect = useCallback(() => {
const path = window.location.hash.replace('#', '') || '/'
sessionStorage.setItem('returnUrl', path)
clearAuth()
navigate('/login?expired=true', { replace: true })
}, [clearAuth, navigate])
useEffect(() => {
if (!isAuthenticated || !idleExpiresAt || !expiresAt) {
setRemaining(null)
return
}
const interval = setInterval(() => {
const now = Date.now()
const idleDiff = new Date(idleExpiresAt).getTime() - now
const absDiff = new Date(expiresAt).getTime() - now
if (absDiff <= 0) {
doClearAndRedirect()
clearInterval(interval)
return
}
if (idleDiff <= 0) {
doClearAndRedirect()
clearInterval(interval)
return
}
if (idleDiff <= REFRESH_BEFORE_MS && !hasRefreshed.current) {
hasRefreshed.current = true
fetch('/api/auth/refresh', { method: 'POST' })
.then((res) => {
if (!res.ok) throw new Error('Refresh failed')
return res.json()
})
.then((data) => {
if (user) {
setAuth(user, data.expiresAt, data.idleExpiresAt)
}
hasRefreshed.current = false
})
.catch(() => {
doClearAndRedirect()
})
}
if (idleDiff <= WARNING_BEFORE_MS) {
setRemaining(idleDiff)
} else {
setRemaining(null)
}
}, CHECK_INTERVAL_MS)
return () => clearInterval(interval)
}, [isAuthenticated, idleExpiresAt, expiresAt, doClearAndRedirect, setAuth, user])
function handleStayLoggedIn() {
fetch('/api/auth/refresh', { method: 'POST' })
.then((res) => {
if (!res.ok) throw new Error('Refresh failed')
return res.json()
})
.then((data) => {
if (user) {
setAuth(user, data.expiresAt, data.idleExpiresAt)
}
setRemaining(null)
hasRefreshed.current = false
})
.catch(() => {
doClearAndRedirect()
})
}
if (!isAuthenticated || remaining === null || remaining > WARNING_BEFORE_MS) return null
const seconds = Math.ceil(remaining / 1000)
const minutes = Math.floor(seconds / 60)
const secs = seconds % 60
return (
<div
role="dialog"
className="fixed inset-0 z-50 flex items-center justify-center bg-black/50"
>
<div className="w-80 rounded-lg border border-border bg-card p-6 shadow-lg">
<h2 className="mb-2 text-lg font-semibold">Session Timeout</h2>
<p className="mb-4 text-sm text-muted-foreground">
Your session will expire in {minutes}:{secs.toString().padStart(2, '0')}
</p>
<div className="mb-4 h-2 w-full overflow-hidden rounded-full bg-muted">
<div
className="h-full rounded-full bg-primary transition-all duration-1000"
style={{
width: `${(remaining / WARNING_BEFORE_MS) * 100}%`,
}}
/>
</div>
<Button className="w-full" onClick={handleStayLoggedIn}>
Stay logged in
</Button>
</div>
</div>
)
}
|