From c98c2b53bdd1c56cf2d7cbf400c1dbd849429ab7 Mon Sep 17 00:00:00 2001 From: lyannne Date: Wed, 25 Feb 2026 16:05:52 -0500 Subject: [PATCH 1/6] implemented auto logout with useeffect --- frontend/src/context/auth/authContext.tsx | 36 ++++++++++++++++++++++- 1 file changed, 35 insertions(+), 1 deletion(-) diff --git a/frontend/src/context/auth/authContext.tsx b/frontend/src/context/auth/authContext.tsx index bd83417..26a5b6e 100644 --- a/frontend/src/context/auth/authContext.tsx +++ b/frontend/src/context/auth/authContext.tsx @@ -1,4 +1,4 @@ -import { useContext, createContext, ReactNode } from 'react'; +import { useContext, createContext, ReactNode, useEffect, useRef } from 'react'; import { getAppStore } from '../../external/bcanSatchel/store'; import { setAuthState, logoutUser } from '../../external/bcanSatchel/actions'; import { observer } from 'mobx-react-lite'; @@ -31,6 +31,11 @@ export const useAuthContext = () => { export const AuthProvider = observer(({ children }: { children: ReactNode }) => { const store = getAppStore(); + const logoutTimerRef = useRef(null); + + // Auto-logout timeout duration (in milliseconds) + // 8 hours = 8 * 60 * 60 * 1000 + const SESSION_TIMEOUT = 8 * 60 * 60 * 1000; /** Attempt to log in the user */ const login = async (email: string, password: string): Promise => { @@ -100,8 +105,37 @@ export const AuthProvider = observer(({ children }: { children: ReactNode }) => const logout = () => { api('/auth/logout', { method: 'POST' }); logoutUser(); + // Clear the logout timer when user manually logs out + if (logoutTimerRef.current) { + clearTimeout(logoutTimerRef.current); + logoutTimerRef.current = null; + } }; + /** Start the auto-logout timer (8 hours from now) */ + useEffect(() => { + if (!store.isAuthenticated) { + // Clear timer if user is not authenticated + if (logoutTimerRef.current) { + clearTimeout(logoutTimerRef.current); + logoutTimerRef.current = null; + } + return; + } + + // Start the 8-hour timer when user logs in + logoutTimerRef.current = setTimeout(() => { + logout(); + }, SESSION_TIMEOUT); + + // Cleanup: clear timer on unmount or logout + return () => { + if (logoutTimerRef.current) { + clearTimeout(logoutTimerRef.current); + } + }; + }, [store.isAuthenticated]); + /** Restore user session on refresh */ // useEffect(() => { // api('/auth/session') From ba577a05780f8dccb38c4cb76e92e0695289c1ca Mon Sep 17 00:00:00 2001 From: lyannne Date: Wed, 25 Feb 2026 17:25:15 -0500 Subject: [PATCH 2/6] added logout backend route --- backend/src/auth/auth.controller.ts | 38 +++++++++++++++++++++++++++++ backend/src/auth/auth.service.ts | 17 +++++++++++++ 2 files changed, 55 insertions(+) diff --git a/backend/src/auth/auth.controller.ts b/backend/src/auth/auth.controller.ts index b240452..34ef3df 100644 --- a/backend/src/auth/auth.controller.ts +++ b/backend/src/auth/auth.controller.ts @@ -81,6 +81,44 @@ export class AuthController { return { message: 'User registered successfully' }; } + /** + * Logs out a user by clearing authentication cookies + */ + @Post('logout') + @ApiResponse({ + status: 200, + description: "User logged out successfully" + }) + @ApiResponse({ + status: 500, + description: "Internal Server Error" + }) + async logout( + @Res({ passthrough: true }) response: Response, + @Req() req: any + ): Promise<{ message: string }> { + // Try to invalidate Cognito session if access token is available + try { + const authHeader = req.headers['authorization'] || req.headers['Authorization']; + if (authHeader) { + const token = authHeader.startsWith('Bearer ') + ? authHeader.substring(7) + : authHeader; + await this.authService.logout(token); + } + } catch (error) { + // Log error but continue - we'll clear cookies anyway + console.error('Error invalidating Cognito session:', error); + } + + // Clear all cookies + response.clearCookie('access_token', { path: '/' }); + response.clearCookie('refresh_token', { path: '/auth/refresh' }); + response.clearCookie('id_token', { path: '/' }); + + return { message: 'Logged out successfully' }; + } + /** * Logs in a user */ diff --git a/backend/src/auth/auth.service.ts b/backend/src/auth/auth.service.ts index 4b9f2d0..84f2671 100644 --- a/backend/src/auth/auth.service.ts +++ b/backend/src/auth/auth.service.ts @@ -291,6 +291,23 @@ export class AuthService { return emailRegex.test(email); } + // purpose statement: logs out a user by invalidating their Cognito session + async logout(accessToken: string): Promise { + try { + await this.cognito + .globalSignOut({ + AccessToken: accessToken, + }) + .promise(); + + this.logger.log('User signed out successfully from Cognito'); + } catch (error) { + this.logger.error('Error during Cognito sign out:', error); + // Don't throw error since we still clear cookies in controller + // This handles cases where token is already expired + } + } + // purpose statement: logs in an user via cognito and retrieves user data from dynamodb // use case: employee is trying to access the app, needs to have an account already From 432d64a232a61365d9b0a7a411d62a4d69e703af Mon Sep 17 00:00:00 2001 From: lyannne Date: Wed, 25 Feb 2026 16:05:52 -0500 Subject: [PATCH 3/6] implemented auto logout with useeffect --- frontend/src/context/auth/authContext.tsx | 36 ++++++++++++++++++++++- 1 file changed, 35 insertions(+), 1 deletion(-) diff --git a/frontend/src/context/auth/authContext.tsx b/frontend/src/context/auth/authContext.tsx index bd83417..26a5b6e 100644 --- a/frontend/src/context/auth/authContext.tsx +++ b/frontend/src/context/auth/authContext.tsx @@ -1,4 +1,4 @@ -import { useContext, createContext, ReactNode } from 'react'; +import { useContext, createContext, ReactNode, useEffect, useRef } from 'react'; import { getAppStore } from '../../external/bcanSatchel/store'; import { setAuthState, logoutUser } from '../../external/bcanSatchel/actions'; import { observer } from 'mobx-react-lite'; @@ -31,6 +31,11 @@ export const useAuthContext = () => { export const AuthProvider = observer(({ children }: { children: ReactNode }) => { const store = getAppStore(); + const logoutTimerRef = useRef(null); + + // Auto-logout timeout duration (in milliseconds) + // 8 hours = 8 * 60 * 60 * 1000 + const SESSION_TIMEOUT = 8 * 60 * 60 * 1000; /** Attempt to log in the user */ const login = async (email: string, password: string): Promise => { @@ -100,8 +105,37 @@ export const AuthProvider = observer(({ children }: { children: ReactNode }) => const logout = () => { api('/auth/logout', { method: 'POST' }); logoutUser(); + // Clear the logout timer when user manually logs out + if (logoutTimerRef.current) { + clearTimeout(logoutTimerRef.current); + logoutTimerRef.current = null; + } }; + /** Start the auto-logout timer (8 hours from now) */ + useEffect(() => { + if (!store.isAuthenticated) { + // Clear timer if user is not authenticated + if (logoutTimerRef.current) { + clearTimeout(logoutTimerRef.current); + logoutTimerRef.current = null; + } + return; + } + + // Start the 8-hour timer when user logs in + logoutTimerRef.current = setTimeout(() => { + logout(); + }, SESSION_TIMEOUT); + + // Cleanup: clear timer on unmount or logout + return () => { + if (logoutTimerRef.current) { + clearTimeout(logoutTimerRef.current); + } + }; + }, [store.isAuthenticated]); + /** Restore user session on refresh */ // useEffect(() => { // api('/auth/session') From 1e2f3ee40e7844a0980bbf130f16aaeb9ef88e35 Mon Sep 17 00:00:00 2001 From: lyannne Date: Wed, 25 Feb 2026 17:25:15 -0500 Subject: [PATCH 4/6] added logout backend route --- backend/src/auth/auth.controller.ts | 38 +++++++++++++++++++++++++++++ backend/src/auth/auth.service.ts | 17 +++++++++++++ 2 files changed, 55 insertions(+) diff --git a/backend/src/auth/auth.controller.ts b/backend/src/auth/auth.controller.ts index b240452..34ef3df 100644 --- a/backend/src/auth/auth.controller.ts +++ b/backend/src/auth/auth.controller.ts @@ -81,6 +81,44 @@ export class AuthController { return { message: 'User registered successfully' }; } + /** + * Logs out a user by clearing authentication cookies + */ + @Post('logout') + @ApiResponse({ + status: 200, + description: "User logged out successfully" + }) + @ApiResponse({ + status: 500, + description: "Internal Server Error" + }) + async logout( + @Res({ passthrough: true }) response: Response, + @Req() req: any + ): Promise<{ message: string }> { + // Try to invalidate Cognito session if access token is available + try { + const authHeader = req.headers['authorization'] || req.headers['Authorization']; + if (authHeader) { + const token = authHeader.startsWith('Bearer ') + ? authHeader.substring(7) + : authHeader; + await this.authService.logout(token); + } + } catch (error) { + // Log error but continue - we'll clear cookies anyway + console.error('Error invalidating Cognito session:', error); + } + + // Clear all cookies + response.clearCookie('access_token', { path: '/' }); + response.clearCookie('refresh_token', { path: '/auth/refresh' }); + response.clearCookie('id_token', { path: '/' }); + + return { message: 'Logged out successfully' }; + } + /** * Logs in a user */ diff --git a/backend/src/auth/auth.service.ts b/backend/src/auth/auth.service.ts index 4b9f2d0..84f2671 100644 --- a/backend/src/auth/auth.service.ts +++ b/backend/src/auth/auth.service.ts @@ -291,6 +291,23 @@ export class AuthService { return emailRegex.test(email); } + // purpose statement: logs out a user by invalidating their Cognito session + async logout(accessToken: string): Promise { + try { + await this.cognito + .globalSignOut({ + AccessToken: accessToken, + }) + .promise(); + + this.logger.log('User signed out successfully from Cognito'); + } catch (error) { + this.logger.error('Error during Cognito sign out:', error); + // Don't throw error since we still clear cookies in controller + // This handles cases where token is already expired + } + } + // purpose statement: logs in an user via cognito and retrieves user data from dynamodb // use case: employee is trying to access the app, needs to have an account already From 13e81f2959f938f9e7da2a85ed638227bfa5314c Mon Sep 17 00:00:00 2001 From: lyannne Date: Thu, 26 Feb 2026 14:04:35 -0500 Subject: [PATCH 5/6] logout tests --- .../src/auth/__test__/auth.service.spec.ts | 31 +++++++++++++++++++ 1 file changed, 31 insertions(+) diff --git a/backend/src/auth/__test__/auth.service.spec.ts b/backend/src/auth/__test__/auth.service.spec.ts index 7eca97a..a20bf08 100644 --- a/backend/src/auth/__test__/auth.service.spec.ts +++ b/backend/src/auth/__test__/auth.service.spec.ts @@ -20,6 +20,7 @@ const mockAdminDeleteUser = vi.fn(() => ({ promise: mockCognitoPromise const mockInitiateAuth = vi.fn(() => ({ promise: mockCognitoPromise })); const mockGetUser = vi.fn(() => ({ promise: mockCognitoPromise })); const mockRespondToAuthChallenge = vi.fn(() => ({ promise: mockCognitoPromise })); +const mockGlobalSignOut = vi.fn(() => ({ promise: mockCognitoPromise })); // ─── DynamoDB mocks ─────────────────────────────────────────────────────────── const mockDynamoPromise = vi.fn(); @@ -41,6 +42,7 @@ vi.mock('aws-sdk', () => { initiateAuth: mockInitiateAuth, getUser: mockGetUser, respondToAuthChallenge: mockRespondToAuthChallenge, + globalSignOut: mockGlobalSignOut, }; }); @@ -86,6 +88,7 @@ describe('AuthService', () => { mockInitiateAuth.mockReturnValue({ promise: mockCognitoPromise }); mockGetUser.mockReturnValue({ promise: mockCognitoPromise }); mockRespondToAuthChallenge.mockReturnValue({ promise: mockCognitoPromise }); + mockGlobalSignOut.mockReturnValue({ promise: mockCognitoPromise }); mockDynamoGet.mockReturnValue({ promise: mockDynamoPromise }); mockDynamoPut.mockReturnValue({ promise: mockDynamoPromise }); @@ -474,4 +477,32 @@ describe('AuthService', () => { await expect(service.validateSession('bad-token')).rejects.toThrow('Failed to validate session'); }); }); + + describe('logout', () => { + it('should successfully sign out user from Cognito', async () => { + mockCognitoPromise.mockResolvedValueOnce({}); + + await expect(service.logout('valid-access-token')).resolves.toBeUndefined(); + + expect(mockGlobalSignOut).toHaveBeenCalledWith({ + AccessToken: 'valid-access-token', + }); + }); + + it('should not throw error when Cognito sign out fails', async () => { + mockCognitoPromise.mockRejectedValueOnce(new Error('Cognito error')); + + await expect(service.logout('some-token')).resolves.toBeUndefined(); + expect(mockGlobalSignOut).toHaveBeenCalled(); + }); + + it('should not throw error when token is already expired', async () => { + mockCognitoPromise.mockRejectedValueOnce({ + code: 'NotAuthorizedException', + message: 'Token expired' + }); + + await expect(service.logout('expired-token')).resolves.toBeUndefined(); + }); + }); }); \ No newline at end of file From 61d3e81ee6e59d1bf18d94d53c63a50d68764ab9 Mon Sep 17 00:00:00 2001 From: lyannne Date: Thu, 26 Feb 2026 14:41:14 -0500 Subject: [PATCH 6/6] copilot comments --- backend/src/auth/auth.controller.ts | 27 +++++++++++++---------- frontend/src/context/auth/authContext.tsx | 3 +-- 2 files changed, 16 insertions(+), 14 deletions(-) diff --git a/backend/src/auth/auth.controller.ts b/backend/src/auth/auth.controller.ts index 34ef3df..94db957 100644 --- a/backend/src/auth/auth.controller.ts +++ b/backend/src/auth/auth.controller.ts @@ -97,18 +97,21 @@ export class AuthController { @Res({ passthrough: true }) response: Response, @Req() req: any ): Promise<{ message: string }> { - // Try to invalidate Cognito session if access token is available - try { - const authHeader = req.headers['authorization'] || req.headers['Authorization']; - if (authHeader) { - const token = authHeader.startsWith('Bearer ') - ? authHeader.substring(7) - : authHeader; - await this.authService.logout(token); - } - } catch (error) { - // Log error but continue - we'll clear cookies anyway - console.error('Error invalidating Cognito session:', error); + const cookieToken = req.cookies?.access_token; + let token: string | undefined = cookieToken; + + if (!token) { + const authHeader = req.headers['authorization'] || req.headers['Authorization']; + if (authHeader && typeof authHeader === 'string') { + token = authHeader.startsWith('Bearer ') + ? authHeader.substring(7) + : authHeader; + } + } + + // Logout user in Cognito + if (token) { + await this.authService.logout(token); } // Clear all cookies diff --git a/frontend/src/context/auth/authContext.tsx b/frontend/src/context/auth/authContext.tsx index 26a5b6e..82cd7eb 100644 --- a/frontend/src/context/auth/authContext.tsx +++ b/frontend/src/context/auth/authContext.tsx @@ -31,8 +31,7 @@ export const useAuthContext = () => { export const AuthProvider = observer(({ children }: { children: ReactNode }) => { const store = getAppStore(); - const logoutTimerRef = useRef(null); - + const logoutTimerRef = useRef | null>(null); // Auto-logout timeout duration (in milliseconds) // 8 hours = 8 * 60 * 60 * 1000 const SESSION_TIMEOUT = 8 * 60 * 60 * 1000;