diff --git a/app/src/api/auth.ts b/app/src/api/auth.ts index e76aaf3..3fb3ff5 100644 --- a/app/src/api/auth.ts +++ b/app/src/api/auth.ts @@ -1,25 +1,15 @@ -import { - API_BASE_URL, - User, - LoginRequest, - LoginResponse, - isLoginResponse, - isUser, -} from '../types/authApi'; +import { API_BASE_URL, User, LoginRequest, isUser } from '../types/authApi'; import { apiCall } from './api'; /** * Logs in a user with email and password * @param {string} email - The user's email * @param {string} password - The user's password - * @returns {Promise} A promise that resolves to the login response + * @returns {Promise} A promise that resolves to the user * @throws {Error} If the API call fails or returns an invalid response * @throws {Error} If the login fails */ -export const login = async ( - email: string, - password: string -): Promise => { +export const login = async (email: string, password: string): Promise => { const loginData: LoginRequest = { email, password }; const response = await apiCall(`${API_BASE_URL}/auth/login`, { method: 'POST', @@ -27,11 +17,11 @@ export const login = async ( }); const data = await response.json(); - if (!isLoginResponse(data)) { - throw new Error('Invalid login response received from API'); + if (!('user' in data) || !isUser(data.user)) { + throw new Error('Invalid login response from API'); } - return data; + return data.user; }; /** diff --git a/app/src/contexts/AuthContext.jsx b/app/src/contexts/AuthContext.jsx deleted file mode 100644 index 20faa34..0000000 --- a/app/src/contexts/AuthContext.jsx +++ /dev/null @@ -1,108 +0,0 @@ -import React, { - createContext, - useContext, - useState, - useCallback, - useEffect, -} from 'react'; -import { notifications } from '@mantine/notifications'; -import * as authApi from '../api/auth'; - -const AuthContext = createContext(null); - -export const AuthProvider = ({ children }) => { - const [user, setUser] = useState(null); - const [loading, setLoading] = useState(true); - const [initialized, setInitialized] = useState(false); - - // Load user data on mount - useEffect(() => { - const initializeAuth = async () => { - try { - const userData = await authApi.getCurrentUser(); - setUser(userData); - } catch (error) { - console.error('Failed to initialize auth:', error); - } finally { - setLoading(false); - setInitialized(true); - } - }; - - initializeAuth(); - }, []); - - const login = useCallback(async (email, password) => { - try { - const { user: userData } = await authApi.login(email, password); - setUser(userData); - notifications.show({ - title: 'Success', - message: 'Logged in successfully', - color: 'green', - }); - return true; - } catch (error) { - console.error('Login failed:', error); - notifications.show({ - title: 'Error', - message: error.message || 'Login failed', - color: 'red', - }); - return false; - } - }, []); - - const logout = useCallback(async () => { - try { - await authApi.logout(); - } catch (error) { - console.error('Logout failed:', error); - } finally { - setUser(null); - } - }, []); - - const refreshToken = useCallback(async () => { - try { - const success = await authApi.refreshToken(); - if (!success) { - await logout(); - } - return success; - } catch (error) { - console.error('Token refresh failed:', error); - await logout(); - return false; - } - }, [logout]); - - const refreshUser = useCallback(async () => { - try { - const userData = await authApi.getCurrentUser(); - setUser(userData); - } catch (error) { - console.error('Failed to refresh user data:', error); - } - }, []); - - const value = { - user, - loading, - initialized, - login, - logout, - refreshToken, - refreshUser, - }; - - return {children}; -}; - -export const useAuth = () => { - const context = useContext(AuthContext); - if (!context) { - throw new Error('useAuth must be used within an AuthProvider'); - } - return context; -}; diff --git a/app/src/contexts/AuthContext.tsx b/app/src/contexts/AuthContext.tsx new file mode 100644 index 0000000..42c771e --- /dev/null +++ b/app/src/contexts/AuthContext.tsx @@ -0,0 +1,131 @@ +import React, { + createContext, + useContext, + useState, + useCallback, + useEffect, +} from 'react'; +import { notifications } from '@mantine/notifications'; +import { + login as apiLogin, + logout as apiLogout, + refreshToken as apiRefreshToken, + getCurrentUser, +} from '@/api/auth'; +import { User } from '@/types/authApi'; + +interface AuthContextType { + user: User | null; + loading: boolean; + initialized: boolean; + login: (email: string, password: string) => Promise; + logout: () => Promise; + refreshToken: () => Promise; + refreshUser: () => Promise; +} + +const AuthContext = createContext(null); + +interface AuthProviderProps { + children: React.ReactNode; +} + +export const AuthProvider: React.FC = ({ children }) => { + const [user, setUser] = useState(null); + const [loading, setLoading] = useState(true); + const [initialized, setInitialized] = useState(false); + + // Load user data on mount + useEffect(() => { + const initializeAuth = async (): Promise => { + try { + const userData = await getCurrentUser(); + setUser(userData); + } catch (error) { + console.error('Failed to initialize auth:', error); + } finally { + setLoading(false); + setInitialized(true); + } + }; + + initializeAuth(); + }, []); + + const login = useCallback( + async (email: string, password: string): Promise => { + try { + const userData = await apiLogin(email, password); + setUser(userData); + notifications.show({ + title: 'Success', + message: 'Logged in successfully', + color: 'green', + }); + return true; + } catch (error) { + console.error('Login failed:', error); + notifications.show({ + title: 'Error', + message: error instanceof Error ? error.message : 'Login failed', + color: 'red', + }); + return false; + } + }, + [] + ); + + const logout = useCallback(async (): Promise => { + try { + await apiLogout(); + } catch (error) { + console.error('Logout failed:', error); + } finally { + setUser(null); + } + }, []); + + const refreshToken = useCallback(async (): Promise => { + try { + const success = await apiRefreshToken(); + if (!success) { + await logout(); + } + return success; + } catch (error) { + console.error('Token refresh failed:', error); + await logout(); + return false; + } + }, [logout]); + + const refreshUser = useCallback(async (): Promise => { + try { + const userData = await getCurrentUser(); + setUser(userData); + } catch (error) { + console.error('Failed to refresh user data:', error); + } + }, []); + + const value: AuthContextType = { + user, + loading, + initialized, + login, + logout, + refreshToken, + refreshUser, + }; + + return {children}; +}; + +export const useAuth = (): AuthContextType => { + const context = useContext(AuthContext); + if (!context) { + throw new Error('useAuth must be used within an AuthProvider'); + } + return context; +}; diff --git a/app/src/types/authApi.ts b/app/src/types/authApi.ts index ca5337a..3e40dce 100644 --- a/app/src/types/authApi.ts +++ b/app/src/types/authApi.ts @@ -72,31 +72,6 @@ export interface LoginRequest { password: string; } -/** - * Login response from the API - */ -export interface LoginResponse { - user: User; - sessionId: string; - expiresAt: string; -} - -/** - * Type guard to check if a value is a valid LoginResponse - */ -export function isLoginResponse(value: unknown): value is LoginResponse { - return ( - typeof value === 'object' && - value !== null && - 'user' in value && - isUser((value as LoginResponse).user) && - 'sessionId' in value && - typeof (value as LoginResponse).sessionId === 'string' && - 'expiresAt' in value && - typeof (value as LoginResponse).expiresAt === 'string' - ); -} - /** * API call options extending the standard RequestInit */