Fix session-expiry redirect losing return-to-app + wire up inactivity timer
Login.tsx only read the return path from router state, which a hard cross-document navigation (a child app bouncing the whole shell here on session expiry) can never carry — so re-authenticating always landed on the Dashboard instead of back in whatever app you were using. Login.tsx now also accepts a ?from= query param for that case. Also wired up AuthGate's inactivity auto-logout timer, which was defined (getInactivityMs) but never actually called — so the per-device timeout configurable in Admin Settings had no effect. Disabled for installed PWAs as intended. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
parent
58b64c5450
commit
d2603bca99
2 changed files with 36 additions and 1 deletions
|
|
@ -19,6 +19,7 @@ export function AuthGate({ children }: Props) {
|
||||||
const navigate = useNavigate()
|
const navigate = useNavigate()
|
||||||
const location = useLocation()
|
const location = useLocation()
|
||||||
const userRef = useRef<User | null>(null)
|
const userRef = useRef<User | null>(null)
|
||||||
|
const timerRef = useRef<ReturnType<typeof setTimeout> | null>(null)
|
||||||
|
|
||||||
function fetchMe(isInitial = false) {
|
function fetchMe(isInitial = false) {
|
||||||
fetch('/api/auth/me', { credentials: 'include' })
|
fetch('/api/auth/me', { credentials: 'include' })
|
||||||
|
|
@ -58,6 +59,35 @@ export function AuthGate({ children }: Props) {
|
||||||
}
|
}
|
||||||
}, [])
|
}, [])
|
||||||
|
|
||||||
|
// Inactivity auto-logout — disabled for installed PWAs; configurable per
|
||||||
|
// device (Admin Settings → Device) for shared/front-desk browser sessions.
|
||||||
|
useEffect(() => {
|
||||||
|
const ms = getInactivityMs()
|
||||||
|
if (!user || !ms) return
|
||||||
|
const timeoutMs: number = ms
|
||||||
|
|
||||||
|
async function forceLogout() {
|
||||||
|
await fetch('/api/auth/logout', { method: 'POST', credentials: 'include' }).catch(() => {})
|
||||||
|
userRef.current = null
|
||||||
|
setUser(null)
|
||||||
|
navigate('/login', { replace: true, state: { from: location.pathname } })
|
||||||
|
}
|
||||||
|
|
||||||
|
function reset() {
|
||||||
|
if (timerRef.current) clearTimeout(timerRef.current)
|
||||||
|
timerRef.current = setTimeout(forceLogout, timeoutMs)
|
||||||
|
}
|
||||||
|
|
||||||
|
const events = ['mousemove', 'keydown', 'click', 'touchstart'] as const
|
||||||
|
events.forEach(e => window.addEventListener(e, reset, { passive: true }))
|
||||||
|
reset()
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
if (timerRef.current) clearTimeout(timerRef.current)
|
||||||
|
events.forEach(e => window.removeEventListener(e, reset))
|
||||||
|
}
|
||||||
|
}, [user])
|
||||||
|
|
||||||
if (checking) {
|
if (checking) {
|
||||||
return (
|
return (
|
||||||
<div style={{ height: '100dvh', display: 'flex', alignItems: 'center', justifyContent: 'center' }}>
|
<div style={{ height: '100dvh', display: 'flex', alignItems: 'center', justifyContent: 'center' }}>
|
||||||
|
|
|
||||||
|
|
@ -9,7 +9,12 @@ export function Login() {
|
||||||
const [loading, setLoading] = useState(false)
|
const [loading, setLoading] = useState(false)
|
||||||
const navigate = useNavigate()
|
const navigate = useNavigate()
|
||||||
const location = useLocation()
|
const location = useLocation()
|
||||||
const from = (location.state as { from?: string })?.from || '/'
|
// `state.from` covers portal-internal redirects (client-side nav preserves
|
||||||
|
// router state); `?from=` covers a child app bouncing the whole shell here
|
||||||
|
// via a hard page navigation, which can't carry router state.
|
||||||
|
const from = (location.state as { from?: string })?.from
|
||||||
|
|| new URLSearchParams(location.search).get('from')
|
||||||
|
|| '/'
|
||||||
|
|
||||||
async function submit(e: React.FormEvent) {
|
async function submit(e: React.FormEvent) {
|
||||||
e.preventDefault()
|
e.preventDefault()
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue