+ {sessions.map((session) => (
+
+
+
+
-
-
-
- {session.name} • {session.currentEntity}
-
- {session.id === sessionId && (
-
- Current
-
+
+
+
+ {session.name} • {session.currentEntity}
+
+ {session.id === sessionId && (
+
+ Current
+
+ )}
+
+
+
+
+
+
+ Last active {formatDistanceToNow(session.lastActivityTimestamp, { addSuffix: true })}
+
+
+
+ Logged in {formatDistanceToNow(session.loginTimestamp, { addSuffix: true })}
+
+
+
+ {session.expiresAt && (
+
+ Expires {formatDistanceToNow(session.expiresAt, { addSuffix: true })}
+
)}
-
-
-
-
- Last active {formatDistanceToNow(session.lastActivityTimestamp, { addSuffix: true })}
-
-
-
- Logged in {formatDistanceToNow(session.loginTimestamp, { addSuffix: true })}
-
-
-
- {session.expiresAt && (
-
- Expires {formatDistanceToNow(session.expiresAt, { addSuffix: true })}
-
- )}
+
handleEndSession(session.id)}
+ aria-label={session.id === sessionId ? "Sign out" : "End this session"}
+ >
+
+
+ ))}
+
+ {sessions.length === 0 && (
+
+
No active sessions found
+
+ )}
+
+ {sessions.length > 1 && (
+
handleEndSession(session.id)}
- aria-label={session.id === sessionId ? "Sign out" : "End this session"}
+ onClick={handleEndAllOtherSessions}
+ disabled={sessions.length <= 1}
>
-
+ End All Other Sessions
+
+
+ Clear All Sessions
- ))}
-
+ )}
+
+
- {sessions.length === 0 && (
-
-
No active sessions found
+
+
+
+
+ Session Security
- )}
+
+ Automatic timeout and security settings
+
+
+
+
+
+ Auto-Logout Protection
+
+
+ For your security, you will be automatically logged out after 30 minutes of inactivity.
+
+
+ You'll receive a warning 5 minutes before your session expires, giving you the option to extend it.
+
+
+
- {sessions.length > 1 && (
-
-
- End All Other Sessions
-
-
- Clear All Sessions
-
+
+
+
+
+ Sessions are automatically extended when you interact with the application
+
+
+
+
+
+ All sessions are stored securely in your browser's local database
+
+
+
+
+
+ Close your browser to end all sessions immediately
+
+
- )}
-
-
+
+
+
)
}
diff --git a/src/components/ui/NEW_COMPONENTS_LATEST.md b/src/components/ui/NEW_COMPONENTS_LATEST.md
index 434195a..47f62dd 100644
--- a/src/components/ui/NEW_COMPONENTS_LATEST.md
+++ b/src/components/ui/NEW_COMPONENTS_LATEST.md
@@ -2,6 +2,50 @@
This document lists all newly added UI components to the library.
+## Security & Session Components
+
+### `SessionExpiryDialog`
+Modal dialog that warns users when their session is about to expire with a countdown timer.
+
+```tsx
+ extendSession()}
+ onLogout={() => handleLogout()}
+/>
+```
+
+**Props:**
+- `open`: boolean - controls dialog visibility
+- `timeRemaining`: number - seconds until auto-logout
+- `totalWarningTime`: number - total warning period for progress bar
+- `onExtend`: () => void - callback to extend session
+- `onLogout`: () => void - callback for manual logout
+
+**Features:**
+- Live countdown timer with minutes:seconds format
+- Visual progress bar showing time remaining
+- Clear call-to-action buttons
+- Accessible with ARIA live regions
+- Auto-focuses "Stay Logged In" button
+
+### `SessionManager`
+Comprehensive session management interface showing active sessions and security settings.
+
+```tsx
+
+```
+
+**Features:**
+- List of all active sessions with timestamps
+- Last activity tracking
+- Manual session termination
+- Bulk session management
+- Security information panel
+- Auto-logout configuration details
+
## Layout Components
### `Container`
diff --git a/src/components/views/profile-view.tsx b/src/components/views/profile-view.tsx
index e9ef92d..d2f743a 100644
--- a/src/components/views/profile-view.tsx
+++ b/src/components/views/profile-view.tsx
@@ -1,4 +1,4 @@
-import { useState } from 'react'
+import { useState, useEffect } from 'react'
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card'
import { Button } from '@/components/ui/button'
import { Input } from '@/components/ui/input'
@@ -11,6 +11,7 @@ import { Switch } from '@/components/ui/switch'
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select'
import { SessionManager } from '@/components/SessionManager'
import { useAuth } from '@/hooks/use-auth'
+import { useSessionTimeoutPreferences } from '@/hooks/use-session-timeout-preferences'
import { toast } from 'sonner'
import {
User,
@@ -28,6 +29,7 @@ import {
export function ProfileView() {
const { user } = useAuth()
const [isEditing, setIsEditing] = useState(false)
+ const { preferences: timeoutPrefs, updateTimeout } = useSessionTimeoutPreferences()
const [profileData, setProfileData] = useState({
name: user?.name || '',
@@ -57,6 +59,15 @@ export function ProfileView() {
loginAlerts: true,
})
+ useEffect(() => {
+ if (timeoutPrefs) {
+ setSecurity(prev => ({
+ ...prev,
+ sessionTimeout: String(timeoutPrefs.timeoutMinutes)
+ }))
+ }
+ }, [timeoutPrefs])
+
const getUserInitials = () => {
if (!user) return 'U'
return user.name
@@ -89,7 +100,10 @@ export function ProfileView() {
}
const handleSaveSecurity = () => {
- toast.success('Security settings updated')
+ updateTimeout(Number(security.sessionTimeout))
+ toast.success('Security settings updated', {
+ description: `Session timeout set to ${security.sessionTimeout} minutes`,
+ })
}
return (
@@ -459,7 +473,13 @@ export function ProfileView() {
-
Session Timeout (minutes)
+
+
+ Session Timeout
+
+
+ Automatically log out after this period of inactivity
+
setSecurity({ ...security, sessionTimeout: value })}
@@ -469,9 +489,10 @@ export function ProfileView() {
15 minutes
- 30 minutes
+ 30 minutes (Recommended)
1 hour
2 hours
+ 4 hours
diff --git a/src/hooks/NEW_HOOKS_LATEST.md b/src/hooks/NEW_HOOKS_LATEST.md
index 7969bda..5125d6f 100644
--- a/src/hooks/NEW_HOOKS_LATEST.md
+++ b/src/hooks/NEW_HOOKS_LATEST.md
@@ -2,6 +2,56 @@
This document lists all newly added custom hooks to the library.
+## Security & Session Management Hooks
+
+### `useSessionTimeout`
+Monitors user activity and automatically logs out users after a period of inactivity with configurable warnings.
+
+```tsx
+const {
+ isWarningShown,
+ timeRemaining,
+ isTimedOut,
+ extendSession,
+ resetActivity,
+ config
+} = useSessionTimeout({
+ timeoutMinutes: 30,
+ warningMinutes: 5,
+ checkIntervalSeconds: 30
+})
+
+// Shows warning dialog when isWarningShown is true
+// timeRemaining counts down in seconds
+// Call extendSession() to reset the timeout
+// Automatically logs out when timeRemaining reaches 0
+```
+
+### `useSessionTimeoutPreferences`
+Manages persistent user preferences for session timeout configuration.
+
+```tsx
+const {
+ preferences,
+ updateTimeout,
+ updateWarning,
+ toggleEnabled,
+ resetToDefaults
+} = useSessionTimeoutPreferences()
+
+// Update timeout duration
+updateTimeout(60) // 60 minutes
+
+// Update warning period
+updateWarning(10) // 10 minutes before timeout
+
+// Enable/disable auto-logout
+toggleEnabled(true)
+
+// Reset to default settings
+resetToDefaults()
+```
+
## Business Logic Hooks (Specialized)
### `useInvoicing`
diff --git a/src/hooks/index.ts b/src/hooks/index.ts
index 05662c5..57346cf 100644
--- a/src/hooks/index.ts
+++ b/src/hooks/index.ts
@@ -103,6 +103,8 @@ export { useKeyboardShortcuts } from './use-keyboard-shortcuts'
export { useSkipLink } from './use-skip-link'
export { useSessionStorage } from './use-session-storage'
+export { useSessionTimeout } from './use-session-timeout'
+export { useSessionTimeoutPreferences } from './use-session-timeout-preferences'
export { useIndexedDBState, useIndexedDBCache } from './use-indexed-db-state'
export type { AsyncState } from './use-async'
@@ -156,3 +158,5 @@ export type { UseAsyncActionResult } from './use-async-action'
export type { UseMutationOptions, UseMutationResult } from './use-mutation'
export type { UseFavoritesOptions, UseFavoritesResult } from './use-favorites'
export type { UseClipboardResult } from './use-clipboard-copy'
+export type { SessionTimeoutConfig, SessionTimeoutState } from './use-session-timeout'
+export type { SessionTimeoutPreferences } from './use-session-timeout-preferences'
diff --git a/src/hooks/use-session-timeout-preferences.ts b/src/hooks/use-session-timeout-preferences.ts
new file mode 100644
index 0000000..14d8d35
--- /dev/null
+++ b/src/hooks/use-session-timeout-preferences.ts
@@ -0,0 +1,53 @@
+import { useKV } from '@github/spark/hooks'
+
+export interface SessionTimeoutPreferences {
+ timeoutMinutes: number
+ warningMinutes: number
+ enabled: boolean
+}
+
+const DEFAULT_PREFERENCES: SessionTimeoutPreferences = {
+ timeoutMinutes: 30,
+ warningMinutes: 5,
+ enabled: true,
+}
+
+export function useSessionTimeoutPreferences() {
+ const [preferences, setPreferences] = useKV
(
+ 'session-timeout-preferences',
+ DEFAULT_PREFERENCES
+ )
+
+ const updateTimeout = (minutes: number) => {
+ setPreferences((current) => ({
+ ...(current || DEFAULT_PREFERENCES),
+ timeoutMinutes: minutes,
+ }))
+ }
+
+ const updateWarning = (minutes: number) => {
+ setPreferences((current) => ({
+ ...(current || DEFAULT_PREFERENCES),
+ warningMinutes: minutes,
+ }))
+ }
+
+ const toggleEnabled = (enabled: boolean) => {
+ setPreferences((current) => ({
+ ...(current || DEFAULT_PREFERENCES),
+ enabled,
+ }))
+ }
+
+ const resetToDefaults = () => {
+ setPreferences(DEFAULT_PREFERENCES)
+ }
+
+ return {
+ preferences,
+ updateTimeout,
+ updateWarning,
+ toggleEnabled,
+ resetToDefaults,
+ }
+}
diff --git a/src/hooks/use-session-timeout.ts b/src/hooks/use-session-timeout.ts
new file mode 100644
index 0000000..12e83cf
--- /dev/null
+++ b/src/hooks/use-session-timeout.ts
@@ -0,0 +1,173 @@
+import { useState, useEffect, useCallback, useRef } from 'react'
+import { useAppSelector, useAppDispatch } from '@/store/hooks'
+import { logout } from '@/store/slices/authSlice'
+import { toast } from 'sonner'
+
+export interface SessionTimeoutConfig {
+ timeoutMinutes: number
+ warningMinutes: number
+ checkIntervalSeconds: number
+}
+
+export interface SessionTimeoutState {
+ isWarningShown: boolean
+ timeRemaining: number
+ lastActivity: number
+ isTimedOut: boolean
+}
+
+const DEFAULT_CONFIG: SessionTimeoutConfig = {
+ timeoutMinutes: 30,
+ warningMinutes: 5,
+ checkIntervalSeconds: 30,
+}
+
+export function useSessionTimeout(config: Partial = {}) {
+ const fullConfig = { ...DEFAULT_CONFIG, ...config }
+ const { timeoutMinutes, warningMinutes, checkIntervalSeconds } = fullConfig
+
+ const dispatch = useAppDispatch()
+ const isAuthenticated = useAppSelector(state => state.auth.isAuthenticated)
+
+ const [isWarningShown, setIsWarningShown] = useState(false)
+ const [timeRemaining, setTimeRemaining] = useState(timeoutMinutes * 60)
+ const [isTimedOut, setIsTimedOut] = useState(false)
+ const lastActivityRef = useRef(Date.now())
+ const checkIntervalRef = useRef(undefined)
+ const countdownIntervalRef = useRef(undefined)
+
+ const resetActivity = useCallback(() => {
+ lastActivityRef.current = Date.now()
+ setTimeRemaining(timeoutMinutes * 60)
+ setIsWarningShown(false)
+ setIsTimedOut(false)
+ }, [timeoutMinutes])
+
+ const handleLogout = useCallback(() => {
+ setIsTimedOut(true)
+ setIsWarningShown(false)
+ dispatch(logout())
+
+ if (checkIntervalRef.current) {
+ clearInterval(checkIntervalRef.current)
+ }
+ if (countdownIntervalRef.current) {
+ clearInterval(countdownIntervalRef.current)
+ }
+ }, [dispatch])
+
+ const extendSession = useCallback(() => {
+ resetActivity()
+ toast.success('Session Extended', {
+ description: 'Your session has been extended.',
+ duration: 3000,
+ })
+ }, [resetActivity])
+
+ const checkTimeout = useCallback(() => {
+ if (!isAuthenticated) return
+
+ const now = Date.now()
+ const elapsedSeconds = Math.floor((now - lastActivityRef.current) / 1000)
+ const remaining = (timeoutMinutes * 60) - elapsedSeconds
+
+ setTimeRemaining(remaining)
+
+ if (remaining <= 0) {
+ handleLogout()
+ toast.error('Session Expired', {
+ description: 'You have been logged out due to inactivity.',
+ duration: 5000,
+ })
+ } else if (remaining <= warningMinutes * 60 && !isWarningShown) {
+ setIsWarningShown(true)
+ toast.warning('Session Expiring Soon', {
+ description: `Your session will expire in ${warningMinutes} minutes due to inactivity.`,
+ duration: 10000,
+ })
+ }
+ }, [isAuthenticated, timeoutMinutes, warningMinutes, isWarningShown, handleLogout])
+
+ useEffect(() => {
+ if (!isAuthenticated) {
+ if (checkIntervalRef.current) {
+ clearInterval(checkIntervalRef.current)
+ }
+ if (countdownIntervalRef.current) {
+ clearInterval(countdownIntervalRef.current)
+ }
+ setIsWarningShown(false)
+ setIsTimedOut(false)
+ return
+ }
+
+ resetActivity()
+
+ checkIntervalRef.current = setInterval(() => {
+ checkTimeout()
+ }, checkIntervalSeconds * 1000)
+
+ const activityEvents = ['mousedown', 'keydown', 'scroll', 'touchstart', 'click']
+ const handleActivity = () => {
+ if (!isWarningShown) {
+ resetActivity()
+ }
+ }
+
+ activityEvents.forEach(event => {
+ window.addEventListener(event, handleActivity, { passive: true })
+ })
+
+ return () => {
+ if (checkIntervalRef.current) {
+ clearInterval(checkIntervalRef.current)
+ }
+ if (countdownIntervalRef.current) {
+ clearInterval(countdownIntervalRef.current)
+ }
+ activityEvents.forEach(event => {
+ window.removeEventListener(event, handleActivity)
+ })
+ }
+ }, [isAuthenticated, checkIntervalSeconds, checkTimeout, resetActivity, isWarningShown])
+
+ useEffect(() => {
+ if (isWarningShown && !countdownIntervalRef.current) {
+ countdownIntervalRef.current = setInterval(() => {
+ setTimeRemaining(prev => {
+ const newValue = prev - 1
+ if (newValue <= 0) {
+ if (countdownIntervalRef.current) {
+ clearInterval(countdownIntervalRef.current)
+ countdownIntervalRef.current = undefined
+ }
+ handleLogout()
+ return 0
+ }
+ return newValue
+ })
+ }, 1000)
+ }
+
+ if (!isWarningShown && countdownIntervalRef.current) {
+ clearInterval(countdownIntervalRef.current)
+ countdownIntervalRef.current = undefined
+ }
+
+ return () => {
+ if (countdownIntervalRef.current) {
+ clearInterval(countdownIntervalRef.current)
+ countdownIntervalRef.current = undefined
+ }
+ }
+ }, [isWarningShown, handleLogout])
+
+ return {
+ isWarningShown,
+ timeRemaining,
+ isTimedOut,
+ extendSession,
+ resetActivity,
+ config: fullConfig,
+ }
+}