mirror of
https://github.com/lordmathis/lemma.git
synced 2025-11-06 07:54:22 +00:00
Add initial frontend auth implementation
This commit is contained in:
@@ -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 <Layout />;
|
||||
function AuthenticatedContent() {
|
||||
const { user, loading, initialized } = useAuth();
|
||||
|
||||
if (!initialized) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (loading) {
|
||||
return <div>Loading...</div>;
|
||||
}
|
||||
|
||||
if (!user) {
|
||||
return <LoginPage />;
|
||||
}
|
||||
|
||||
return (
|
||||
<WorkspaceProvider>
|
||||
<ModalProvider>
|
||||
<Layout />
|
||||
</ModalProvider>
|
||||
</WorkspaceProvider>
|
||||
);
|
||||
}
|
||||
|
||||
function App() {
|
||||
@@ -20,11 +42,9 @@ function App() {
|
||||
<MantineProvider defaultColorScheme="light">
|
||||
<Notifications />
|
||||
<ModalsProvider>
|
||||
<WorkspaceProvider>
|
||||
<ModalProvider>
|
||||
<AppContent />
|
||||
</ModalProvider>
|
||||
</WorkspaceProvider>
|
||||
<AuthProvider>
|
||||
<AuthenticatedContent />
|
||||
</AuthProvider>
|
||||
</ModalsProvider>
|
||||
</MantineProvider>
|
||||
</>
|
||||
|
||||
66
frontend/src/components/LoginPage.jsx
Normal file
66
frontend/src/components/LoginPage.jsx
Normal file
@@ -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 (
|
||||
<Container size={420} my={40}>
|
||||
<Title ta="center">Welcome to NovaMD</Title>
|
||||
<Text c="dimmed" size="sm" ta="center" mt={5}>
|
||||
Please sign in to continue
|
||||
</Text>
|
||||
|
||||
<Paper withBorder shadow="md" p={30} mt={30} radius="md">
|
||||
<form onSubmit={handleSubmit}>
|
||||
<Stack>
|
||||
<TextInput
|
||||
label="Email"
|
||||
placeholder="your@email.com"
|
||||
required
|
||||
value={email}
|
||||
onChange={(event) => setEmail(event.currentTarget.value)}
|
||||
/>
|
||||
|
||||
<PasswordInput
|
||||
label="Password"
|
||||
placeholder="Your password"
|
||||
required
|
||||
value={password}
|
||||
onChange={(event) => setPassword(event.currentTarget.value)}
|
||||
/>
|
||||
|
||||
<Button type="submit" loading={loading}>
|
||||
Sign in
|
||||
</Button>
|
||||
</Stack>
|
||||
</form>
|
||||
</Paper>
|
||||
</Container>
|
||||
);
|
||||
};
|
||||
|
||||
export default LoginPage;
|
||||
110
frontend/src/contexts/AuthContext.jsx
Normal file
110
frontend/src/contexts/AuthContext.jsx
Normal file
@@ -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 <AuthContext.Provider value={value}>{children}</AuthContext.Provider>;
|
||||
};
|
||||
|
||||
export const useAuth = () => {
|
||||
const context = useContext(AuthContext);
|
||||
if (!context) {
|
||||
throw new Error('useAuth must be used within an AuthProvider');
|
||||
}
|
||||
return context;
|
||||
};
|
||||
@@ -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`);
|
||||
|
||||
97
frontend/src/services/authApi.js
Normal file
97
frontend/src/services/authApi.js
Normal file
@@ -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();
|
||||
};
|
||||
Reference in New Issue
Block a user