diff --git a/frontend/src/components/Layout.tsx b/frontend/src/components/Layout.tsx index fc9e880d..30b1b291 100644 --- a/frontend/src/components/Layout.tsx +++ b/frontend/src/components/Layout.tsx @@ -2,6 +2,7 @@ import { ReactNode, useState } from 'react' import { Link, useLocation } from 'react-router-dom' import { ThemeToggle } from './ThemeToggle' import { Button } from './ui/Button' +import { useAuth } from '../context/AuthContext' interface LayoutProps { children: ReactNode @@ -10,6 +11,7 @@ interface LayoutProps { export default function Layout({ children }: LayoutProps) { const location = useLocation() const [sidebarOpen, setSidebarOpen] = useState(false) + const { logout } = useAuth() const navigation = [ { name: 'Dashboard', path: '/', icon: '📊' }, @@ -63,6 +65,17 @@ export default function Layout({ children }: LayoutProps) { ) })} + +
diff --git a/frontend/src/components/__tests__/Layout.test.tsx b/frontend/src/components/__tests__/Layout.test.tsx index 88020954..4cc2c388 100644 --- a/frontend/src/components/__tests__/Layout.test.tsx +++ b/frontend/src/components/__tests__/Layout.test.tsx @@ -1,10 +1,17 @@ import { ReactNode } from 'react' -import { describe, it, expect } from 'vitest' +import { describe, it, expect, vi } from 'vitest' import { render, screen } from '@testing-library/react' import { BrowserRouter } from 'react-router-dom' import Layout from '../Layout' import { ThemeProvider } from '../../context/ThemeContext' +// Mock AuthContext +vi.mock('../../context/AuthContext', () => ({ + useAuth: () => ({ + logout: vi.fn(), + }), +})) + const renderWithProviders = (children: ReactNode) => { return render( diff --git a/frontend/src/context/AuthContext.tsx b/frontend/src/context/AuthContext.tsx index 0f30588a..e30ad4e4 100644 --- a/frontend/src/context/AuthContext.tsx +++ b/frontend/src/context/AuthContext.tsx @@ -1,6 +1,5 @@ import React, { createContext, useContext, useState, useEffect } from 'react'; import client from '../api/client'; -import { AxiosResponse } from 'axios'; interface User { user_id: number; @@ -9,7 +8,7 @@ interface User { interface AuthContextType { user: User | null; - login: () => void; + login: () => Promise; logout: () => void; isAuthenticated: boolean; isLoading: boolean; @@ -36,14 +35,16 @@ export const AuthProvider: React.FC<{ children: React.ReactNode }> = ({ children checkAuth(); }, []); - const login = () => { + const login = async () => { // Token is stored in cookie by backend, but we might want to store it in memory or trigger a re-fetch // Actually, if backend sets cookie, we just need to fetch /auth/me - client.get('/auth/me').then((response: AxiosResponse) => { - setUser(response.data); - }).catch(() => { - setUser(null); - }); + try { + const response = await client.get('/auth/me'); + setUser(response.data); + } catch (error) { + setUser(null); + throw error; + } }; const logout = async () => { @@ -55,6 +56,40 @@ export const AuthProvider: React.FC<{ children: React.ReactNode }> = ({ children setUser(null); }; + // Auto-logout logic + useEffect(() => { + if (!user) return; + + const TIMEOUT_MS = 15 * 60 * 1000; // 15 minutes + let timeoutId: ReturnType; + + const resetTimer = () => { + if (timeoutId) clearTimeout(timeoutId); + timeoutId = setTimeout(() => { + console.log('Auto-logging out due to inactivity'); + logout(); + }, TIMEOUT_MS); + }; + + // Initial timer start + resetTimer(); + + // Event listeners for activity + const events = ['mousedown', 'keydown', 'scroll', 'touchstart']; + const handleActivity = () => resetTimer(); + + events.forEach(event => { + window.addEventListener(event, handleActivity); + }); + + return () => { + if (timeoutId) clearTimeout(timeoutId); + events.forEach(event => { + window.removeEventListener(event, handleActivity); + }); + }; + }, [user]); + return ( {children} diff --git a/frontend/src/pages/Login.tsx b/frontend/src/pages/Login.tsx index af2bafc6..4bb99c2b 100644 --- a/frontend/src/pages/Login.tsx +++ b/frontend/src/pages/Login.tsx @@ -21,7 +21,7 @@ export default function Login() { try { await client.post('/auth/login', { email, password }) - login() + await login() toast.success('Logged in successfully') navigate('/') } catch (err: any) {