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
+
+
+
+
+
+
+ );
+};
+
+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();
+};