Configurable inactivity timeout — disabled automatically in PWA/standalone mode
This commit is contained in:
parent
f8847c0726
commit
ad7d43f720
2 changed files with 53 additions and 34 deletions
|
|
@ -1,10 +1,12 @@
|
||||||
import { useEffect, useRef, useState } from 'react'
|
import { useEffect, useRef, useState } from 'react'
|
||||||
import type { User } from '../types'
|
import type { User } from '../types'
|
||||||
|
|
||||||
const SHARED_TIMEOUT_MS = 10 * 60 * 1000
|
function getInactivityMs(): number | undefined {
|
||||||
|
if (window.matchMedia('(display-mode: standalone)').matches) return undefined
|
||||||
function isSharedDevice() {
|
const c = document.cookie.split(';').map(s => s.trim()).find(s => s.startsWith('hnf_inactivity_mins='))
|
||||||
return document.cookie.split(';').some(c => c.trim() === 'hnf_shared_device=1')
|
if (!c) return undefined
|
||||||
|
const mins = parseInt(c.split('=')[1])
|
||||||
|
return isNaN(mins) || mins <= 0 ? undefined : mins * 60 * 1000
|
||||||
}
|
}
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
|
|
@ -41,7 +43,8 @@ export function AuthGate({ children }: Props) {
|
||||||
}, [])
|
}, [])
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (state !== 'authed' || !isSharedDevice()) return
|
const ms = getInactivityMs()
|
||||||
|
if (state !== 'authed' || !ms) return
|
||||||
|
|
||||||
async function forceLogout() {
|
async function forceLogout() {
|
||||||
await fetch('/api/auth/logout', { method: 'POST', credentials: 'include' }).catch(() => {})
|
await fetch('/api/auth/logout', { method: 'POST', credentials: 'include' }).catch(() => {})
|
||||||
|
|
@ -51,7 +54,7 @@ export function AuthGate({ children }: Props) {
|
||||||
|
|
||||||
function reset() {
|
function reset() {
|
||||||
if (timerRef.current) clearTimeout(timerRef.current)
|
if (timerRef.current) clearTimeout(timerRef.current)
|
||||||
timerRef.current = setTimeout(forceLogout, SHARED_TIMEOUT_MS)
|
timerRef.current = setTimeout(forceLogout, ms)
|
||||||
}
|
}
|
||||||
|
|
||||||
const events = ['mousemove', 'keydown', 'click', 'touchstart'] as const
|
const events = ['mousemove', 'keydown', 'click', 'touchstart'] as const
|
||||||
|
|
|
||||||
|
|
@ -5,14 +5,23 @@ import { can, fmtGBP, type User } from '../types'
|
||||||
|
|
||||||
const DEFAULT_MACHINE_NAMES = ['Front Desk', 'Restaurant / Bar']
|
const DEFAULT_MACHINE_NAMES = ['Front Desk', 'Restaurant / Bar']
|
||||||
|
|
||||||
function isSharedDevice() {
|
const TIMEOUT_OPTIONS = [
|
||||||
return document.cookie.split(';').some(c => c.trim() === 'hnf_shared_device=1')
|
{ label: 'Off', value: 0 },
|
||||||
|
{ label: '5 min', value: 5 },
|
||||||
|
{ label: '10 min', value: 10 },
|
||||||
|
{ label: '15 min', value: 15 },
|
||||||
|
{ label: '30 min', value: 30 },
|
||||||
|
]
|
||||||
|
|
||||||
|
function getInactivityMins(): number {
|
||||||
|
const c = document.cookie.split(';').map(s => s.trim()).find(s => s.startsWith('hnf_inactivity_mins='))
|
||||||
|
return c ? parseInt(c.split('=')[1]) || 0 : 0
|
||||||
}
|
}
|
||||||
|
|
||||||
function setSharedDeviceCookie(shared: boolean) {
|
function setInactivityMins(mins: number) {
|
||||||
document.cookie = shared
|
document.cookie = mins > 0
|
||||||
? 'hnf_shared_device=1; max-age=31536000; path=/; SameSite=Strict'
|
? `hnf_inactivity_mins=${mins}; max-age=31536000; path=/; SameSite=Strict`
|
||||||
: 'hnf_shared_device=; max-age=0; path=/; SameSite=Strict'
|
: 'hnf_inactivity_mins=; max-age=0; path=/; SameSite=Strict'
|
||||||
}
|
}
|
||||||
|
|
||||||
interface SettingsData {
|
interface SettingsData {
|
||||||
|
|
@ -52,7 +61,8 @@ export function SettingsPage({ user }: { user: User }) {
|
||||||
const [testing, setTesting] = useState(false)
|
const [testing, setTesting] = useState(false)
|
||||||
const [refreshing, setRefreshing] = useState(false)
|
const [refreshing, setRefreshing] = useState(false)
|
||||||
const [msg, setMsg] = useState<{ text: string; ok: boolean } | null>(null)
|
const [msg, setMsg] = useState<{ text: string; ok: boolean } | null>(null)
|
||||||
const [isShared, setIsShared] = useState(isSharedDevice)
|
const [inactivityMins, setInactivityMinsState] = useState(getInactivityMins)
|
||||||
|
const isPwa = window.matchMedia('(display-mode: standalone)').matches
|
||||||
|
|
||||||
function flash(text: string, ok = true) {
|
function flash(text: string, ok = true) {
|
||||||
setMsg({ text, ok })
|
setMsg({ text, ok })
|
||||||
|
|
@ -344,27 +354,33 @@ export function SettingsPage({ user }: { user: User }) {
|
||||||
)}
|
)}
|
||||||
|
|
||||||
<Card style={{ marginBottom: '1rem' }}>
|
<Card style={{ marginBottom: '1rem' }}>
|
||||||
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center' }}>
|
<h2 style={{ fontSize: '1rem', fontWeight: 700, marginBottom: '0.4rem' }}>This Device</h2>
|
||||||
<div>
|
<p style={{ fontSize: '0.8rem', color: 'var(--text-mid)', marginBottom: '0.75rem' }}>
|
||||||
<h2 style={{ fontSize: '1rem', fontWeight: 700 }}>This Device</h2>
|
{isPwa
|
||||||
<p style={{ fontSize: '0.8rem', color: 'var(--text-mid)', marginTop: '0.25rem' }}>
|
? 'Running as an installed app — inactivity timeout is disabled. Sessions last until the 30-day limit.'
|
||||||
{isShared
|
: 'Set an inactivity timeout to automatically sign out after a period of no use. Use on shared or front-desk computers.'}
|
||||||
? 'Marked as a shared computer — sessions time out after 10 minutes of inactivity.'
|
|
||||||
: 'Mark this as a shared computer to automatically log out after 10 minutes of inactivity.'}
|
|
||||||
</p>
|
</p>
|
||||||
</div>
|
<div style={{ display: 'flex', gap: '0.5rem', flexWrap: 'wrap' }}>
|
||||||
<Btn
|
{TIMEOUT_OPTIONS.map(opt => (
|
||||||
variant={isShared ? 'danger' : 'secondary'}
|
<button
|
||||||
small
|
key={opt.value}
|
||||||
|
disabled={isPwa}
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
const next = !isShared
|
setInactivityMins(opt.value)
|
||||||
setSharedDeviceCookie(next)
|
setInactivityMinsState(opt.value)
|
||||||
setIsShared(next)
|
flash(opt.value > 0 ? `Inactivity timeout set to ${opt.label}.` : 'Inactivity timeout disabled.')
|
||||||
flash(next ? 'This device is now marked as shared.' : 'Shared device mode removed.')
|
}}
|
||||||
|
style={{
|
||||||
|
padding: '0.35rem 0.85rem', borderRadius: '6px', fontSize: '0.82rem', fontWeight: 600,
|
||||||
|
cursor: isPwa ? 'not-allowed' : 'pointer', opacity: isPwa ? 0.4 : 1,
|
||||||
|
border: `1px solid ${inactivityMins === opt.value ? 'var(--gold)' : 'var(--card-border)'}`,
|
||||||
|
background: inactivityMins === opt.value ? 'var(--gold)' : 'var(--body-bg)',
|
||||||
|
color: inactivityMins === opt.value ? '#fff' : 'var(--text-primary)',
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{isShared ? 'Remove Shared Status' : 'Mark as Shared'}
|
{opt.label}
|
||||||
</Btn>
|
</button>
|
||||||
|
))}
|
||||||
</div>
|
</div>
|
||||||
</Card>
|
</Card>
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue