From 9cdbf9fec811a90ce7ef4c0d56eae1fe444acc27 Mon Sep 17 00:00:00 2001 From: LordMathis Date: Sun, 3 Nov 2024 23:16:57 +0100 Subject: [PATCH] Add initial frontend auth implementation --- frontend/src/App.jsx | 34 ++++++-- frontend/src/components/LoginPage.jsx | 66 ++++++++++++++++ frontend/src/contexts/AuthContext.jsx | 110 ++++++++++++++++++++++++++ frontend/src/services/api.js | 19 +---- frontend/src/services/authApi.js | 97 +++++++++++++++++++++++ 5 files changed, 302 insertions(+), 24 deletions(-) create mode 100644 frontend/src/components/LoginPage.jsx create mode 100644 frontend/src/contexts/AuthContext.jsx create mode 100644 frontend/src/services/authApi.js diff --git a/frontend/src/App.jsx b/frontend/src/App.jsx index 2ba651a..75040d1 100644 --- a/frontend/src/App.jsx +++ b/frontend/src/App.jsx @@ -3,14 +3,36 @@ import { MantineProvider, ColorSchemeScript } from '@mantine/core'; import { Notifications } from '@mantine/notifications'; import { ModalsProvider } from '@mantine/modals'; import Layout from './components/Layout'; +import LoginPage from './components/LoginPage'; import { WorkspaceProvider } from './contexts/WorkspaceContext'; import { ModalProvider } from './contexts/ModalContext'; +import { AuthProvider, useAuth } from './contexts/AuthContext'; import '@mantine/core/styles.css'; import '@mantine/notifications/styles.css'; import './App.scss'; -function AppContent() { - return ; +function AuthenticatedContent() { + const { user, loading, initialized } = useAuth(); + + if (!initialized) { + return null; + } + + if (loading) { + return
Loading...
; + } + + if (!user) { + return ; + } + + return ( + + + + + + ); } function App() { @@ -20,11 +42,9 @@ function App() { - - - - - + + + diff --git a/frontend/src/components/LoginPage.jsx b/frontend/src/components/LoginPage.jsx new file mode 100644 index 0000000..c5c55f1 --- /dev/null +++ b/frontend/src/components/LoginPage.jsx @@ -0,0 +1,66 @@ +import React, { useState } from 'react'; +import { + TextInput, + PasswordInput, + Paper, + Title, + Container, + Button, + Text, + Stack, +} from '@mantine/core'; +import { useAuth } from '../contexts/AuthContext'; + +const LoginPage = () => { + const [email, setEmail] = useState(''); + const [password, setPassword] = useState(''); + const [loading, setLoading] = useState(false); + const { login } = useAuth(); + + const handleSubmit = async (e) => { + e.preventDefault(); + setLoading(true); + try { + await login(email, password); + } finally { + setLoading(false); + } + }; + + return ( + + Welcome to NovaMD + + Please sign in to continue + + + +
+ + setEmail(event.currentTarget.value)} + /> + + setPassword(event.currentTarget.value)} + /> + + + +
+
+
+ ); +}; + +export default LoginPage; diff --git a/frontend/src/contexts/AuthContext.jsx b/frontend/src/contexts/AuthContext.jsx new file mode 100644 index 0000000..2a716d8 --- /dev/null +++ b/frontend/src/contexts/AuthContext.jsx @@ -0,0 +1,110 @@ +import React, { + createContext, + useContext, + useState, + useCallback, + useEffect, +} from 'react'; +import { notifications } from '@mantine/notifications'; +import * as authApi from '../services/authApi'; + +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 storedToken = localStorage.getItem('accessToken'); + if (storedToken) { + authApi.setAuthToken(storedToken); + const userData = await authApi.getCurrentUser(); + setUser(userData); + } + } catch (error) { + console.error('Failed to initialize auth:', error); + localStorage.removeItem('accessToken'); + authApi.clearAuthToken(); + } finally { + setLoading(false); + setInitialized(true); + } + }; + + initializeAuth(); + }, []); + + const login = useCallback(async (email, password) => { + try { + const { accessToken, user: userData } = await authApi.login( + email, + password + ); + localStorage.setItem('accessToken', accessToken); + authApi.setAuthToken(accessToken); + 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 { + localStorage.removeItem('accessToken'); + authApi.clearAuthToken(); + setUser(null); + } + }, []); + + const refreshToken = useCallback(async () => { + try { + const { accessToken } = await authApi.refreshToken(); + localStorage.setItem('accessToken', accessToken); + authApi.setAuthToken(accessToken); + return true; + } catch (error) { + console.error('Token refresh failed:', error); + await logout(); + return false; + } + }, [logout]); + + const value = { + user, + loading, + initialized, + login, + logout, + refreshToken, + }; + + 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/frontend/src/services/api.js b/frontend/src/services/api.js index 15c5ca5..fdeeabb 100644 --- a/frontend/src/services/api.js +++ b/frontend/src/services/api.js @@ -1,20 +1,5 @@ -const API_BASE_URL = window.API_BASE_URL; - -const apiCall = async (url, options = {}) => { - try { - const response = await fetch(url, options); - if (!response.ok) { - const errorData = await response.json().catch(() => null); - throw new Error( - errorData?.message || `HTTP error! status: ${response.status}` - ); - } - return response; - } catch (error) { - console.error(`API call failed: ${error.message}`); - throw error; - } -}; +import { API_BASE_URL } from '../utils/constants'; +import { apiCall } from './authApi'; export const fetchLastWorkspaceId = async () => { const response = await apiCall(`${API_BASE_URL}/workspaces/last`); diff --git a/frontend/src/services/authApi.js b/frontend/src/services/authApi.js new file mode 100644 index 0000000..9b1ae31 --- /dev/null +++ b/frontend/src/services/authApi.js @@ -0,0 +1,97 @@ +import { API_BASE_URL } from '../utils/constants'; + +let authToken = null; + +export const setAuthToken = (token) => { + authToken = token; +}; + +export const clearAuthToken = () => { + authToken = null; +}; + +export const getAuthHeaders = () => { + const headers = { + 'Content-Type': 'application/json', + }; + + if (authToken) { + headers['Authorization'] = `Bearer ${authToken}`; + } + + return headers; +}; + +// Update the existing apiCall function to include auth headers +export const apiCall = async (url, options = {}) => { + try { + const headers = { + ...getAuthHeaders(), + ...options.headers, + }; + + const response = await fetch(url, { + ...options, + headers, + }); + + // Handle 401 responses + if (response.status === 401) { + const isRefreshEndpoint = url.endsWith('/auth/refresh'); + if (!isRefreshEndpoint) { + // Attempt token refresh and retry the request + const refreshSuccess = await refreshToken(); + if (refreshSuccess) { + // Retry the original request with the new token + return apiCall(url, options); + } + } + throw new Error('Authentication failed'); + } + + if (!response.ok) { + const errorData = await response.json().catch(() => null); + throw new Error( + errorData?.message || `HTTP error! status: ${response.status}` + ); + } + + return response; + } catch (error) { + console.error(`API call failed: ${error.message}`); + throw error; + } +}; + +// Authentication endpoints +export const login = async (email, password) => { + const response = await apiCall(`${API_BASE_URL}/auth/login`, { + method: 'POST', + body: JSON.stringify({ email, password }), + }); + return response.json(); +}; + +export const logout = async () => { + const sessionId = localStorage.getItem('sessionId'); + await apiCall(`${API_BASE_URL}/auth/logout`, { + method: 'POST', + headers: { + 'X-Session-ID': sessionId, + }, + }); +}; + +export const refreshToken = async () => { + const refreshToken = localStorage.getItem('refreshToken'); + const response = await apiCall(`${API_BASE_URL}/auth/refresh`, { + method: 'POST', + body: JSON.stringify({ refreshToken }), + }); + return response.json(); +}; + +export const getCurrentUser = async () => { + const response = await apiCall(`${API_BASE_URL}/auth/me`); + return response.json(); +};