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 { Notifications } from '@mantine/notifications';
|
||||||
import { ModalsProvider } from '@mantine/modals';
|
import { ModalsProvider } from '@mantine/modals';
|
||||||
import Layout from './components/Layout';
|
import Layout from './components/Layout';
|
||||||
|
import LoginPage from './components/LoginPage';
|
||||||
import { WorkspaceProvider } from './contexts/WorkspaceContext';
|
import { WorkspaceProvider } from './contexts/WorkspaceContext';
|
||||||
import { ModalProvider } from './contexts/ModalContext';
|
import { ModalProvider } from './contexts/ModalContext';
|
||||||
|
import { AuthProvider, useAuth } from './contexts/AuthContext';
|
||||||
import '@mantine/core/styles.css';
|
import '@mantine/core/styles.css';
|
||||||
import '@mantine/notifications/styles.css';
|
import '@mantine/notifications/styles.css';
|
||||||
import './App.scss';
|
import './App.scss';
|
||||||
|
|
||||||
function AppContent() {
|
function AuthenticatedContent() {
|
||||||
return <Layout />;
|
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() {
|
function App() {
|
||||||
@@ -20,11 +42,9 @@ function App() {
|
|||||||
<MantineProvider defaultColorScheme="light">
|
<MantineProvider defaultColorScheme="light">
|
||||||
<Notifications />
|
<Notifications />
|
||||||
<ModalsProvider>
|
<ModalsProvider>
|
||||||
<WorkspaceProvider>
|
<AuthProvider>
|
||||||
<ModalProvider>
|
<AuthenticatedContent />
|
||||||
<AppContent />
|
</AuthProvider>
|
||||||
</ModalProvider>
|
|
||||||
</WorkspaceProvider>
|
|
||||||
</ModalsProvider>
|
</ModalsProvider>
|
||||||
</MantineProvider>
|
</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;
|
import { API_BASE_URL } from '../utils/constants';
|
||||||
|
import { apiCall } from './authApi';
|
||||||
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;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
export const fetchLastWorkspaceId = async () => {
|
export const fetchLastWorkspaceId = async () => {
|
||||||
const response = await apiCall(`${API_BASE_URL}/workspaces/last`);
|
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