Complete backend infrastructure and authentication system

Co-authored-by: mblanke <9078342+mblanke@users.noreply.github.com>
This commit is contained in:
copilot-swe-agent[bot]
2025-12-09 14:29:06 +00:00
parent af23e610b2
commit 961946026a
47 changed files with 2337 additions and 1 deletions

29
frontend/src/App.tsx Normal file
View File

@@ -0,0 +1,29 @@
import React from 'react';
import { BrowserRouter as Router, Routes, Route, Navigate } from 'react-router-dom';
import { AuthProvider } from './context/AuthContext';
import PrivateRoute from './components/PrivateRoute';
import Login from './pages/Login';
import Dashboard from './pages/Dashboard';
const App: React.FC = () => {
return (
<Router>
<AuthProvider>
<Routes>
<Route path="/login" element={<Login />} />
<Route
path="/"
element={
<PrivateRoute>
<Dashboard />
</PrivateRoute>
}
/>
<Route path="*" element={<Navigate to="/" />} />
</Routes>
</AuthProvider>
</Router>
);
};
export default App;

View File

@@ -0,0 +1,37 @@
import React from 'react';
import { Navigate } from 'react-router-dom';
import { useAuth } from '../context/AuthContext';
interface PrivateRouteProps {
children: React.ReactNode;
}
const PrivateRoute: React.FC<PrivateRouteProps> = ({ children }) => {
const { isAuthenticated, loading } = useAuth();
if (loading) {
return (
<div style={styles.container}>
<div style={styles.spinner}>Loading...</div>
</div>
);
}
return isAuthenticated ? <>{children}</> : <Navigate to="/login" />;
};
const styles: { [key: string]: React.CSSProperties } = {
container: {
display: 'flex',
justifyContent: 'center',
alignItems: 'center',
minHeight: '100vh',
backgroundColor: '#f5f5f5',
},
spinner: {
fontSize: '18px',
color: '#666',
},
};
export default PrivateRoute;

View File

@@ -0,0 +1,85 @@
import React, { createContext, useState, useContext, useEffect, ReactNode } from 'react';
import { authAPI } from '../utils/api';
interface User {
id: number;
username: string;
role: string;
tenant_id: number;
is_active: boolean;
created_at: string;
}
interface AuthContextType {
user: User | null;
loading: boolean;
login: (username: string, password: string) => Promise<void>;
logout: () => void;
isAuthenticated: boolean;
}
const AuthContext = createContext<AuthContextType | undefined>(undefined);
export const useAuth = () => {
const context = useContext(AuthContext);
if (context === undefined) {
throw new Error('useAuth must be used within an AuthProvider');
}
return context;
};
interface AuthProviderProps {
children: ReactNode;
}
export const AuthProvider: React.FC<AuthProviderProps> = ({ children }) => {
const [user, setUser] = useState<User | null>(null);
const [loading, setLoading] = useState<boolean>(true);
useEffect(() => {
// Check if user is already logged in on mount
const checkAuth = async () => {
const token = localStorage.getItem('access_token');
if (token) {
try {
const userData = await authAPI.getCurrentUser();
setUser(userData);
} catch (error) {
console.error('Failed to fetch user:', error);
localStorage.removeItem('access_token');
}
}
setLoading(false);
};
checkAuth();
}, []);
const login = async (username: string, password: string) => {
try {
const response = await authAPI.login(username, password);
localStorage.setItem('access_token', response.access_token);
// Fetch user data after login
const userData = await authAPI.getCurrentUser();
setUser(userData);
} catch (error) {
throw error;
}
};
const logout = () => {
localStorage.removeItem('access_token');
setUser(null);
};
const value: AuthContextType = {
user,
loading,
login,
logout,
isAuthenticated: !!user,
};
return <AuthContext.Provider value={value}>{children}</AuthContext.Provider>;
};

13
frontend/src/index.tsx Normal file
View File

@@ -0,0 +1,13 @@
import React from 'react';
import ReactDOM from 'react-dom/client';
import App from './App';
const root = ReactDOM.createRoot(
document.getElementById('root') as HTMLElement
);
root.render(
<React.StrictMode>
<App />
</React.StrictMode>
);

View File

@@ -0,0 +1,81 @@
import React from 'react';
import { useAuth } from '../context/AuthContext';
const Dashboard: React.FC = () => {
const { user, logout } = useAuth();
return (
<div style={styles.container}>
<div style={styles.header}>
<h1 style={styles.title}>VelociCompanion Dashboard</h1>
<button onClick={logout} style={styles.logoutButton}>
Logout
</button>
</div>
<div style={styles.content}>
<div style={styles.card}>
<h2>Welcome, {user?.username}!</h2>
<p><strong>Role:</strong> {user?.role}</p>
<p><strong>Tenant ID:</strong> {user?.tenant_id}</p>
<p><strong>Status:</strong> {user?.is_active ? 'Active' : 'Inactive'}</p>
</div>
<div style={styles.card}>
<h3>Getting Started</h3>
<p>Your authentication system is now set up and working!</p>
<ul>
<li> JWT authentication</li>
<li> Multi-tenancy support</li>
<li> Role-based access control</li>
<li> Protected routes</li>
</ul>
</div>
</div>
</div>
);
};
const styles: { [key: string]: React.CSSProperties } = {
container: {
minHeight: '100vh',
backgroundColor: '#f5f5f5',
},
header: {
backgroundColor: 'white',
padding: '20px 40px',
boxShadow: '0 2px 4px rgba(0,0,0,0.1)',
display: 'flex',
justifyContent: 'space-between',
alignItems: 'center',
},
title: {
margin: 0,
fontSize: '24px',
color: '#333',
},
logoutButton: {
padding: '10px 20px',
fontSize: '14px',
fontWeight: '500',
color: 'white',
backgroundColor: '#dc3545',
border: 'none',
borderRadius: '4px',
cursor: 'pointer',
},
content: {
padding: '40px',
maxWidth: '1200px',
margin: '0 auto',
},
card: {
backgroundColor: 'white',
padding: '30px',
borderRadius: '8px',
boxShadow: '0 2px 4px rgba(0,0,0,0.1)',
marginBottom: '20px',
},
};
export default Dashboard;

View File

@@ -0,0 +1,148 @@
import React, { useState } from 'react';
import { useNavigate } from 'react-router-dom';
import { useAuth } from '../context/AuthContext';
const Login: React.FC = () => {
const [username, setUsername] = useState('');
const [password, setPassword] = useState('');
const [error, setError] = useState('');
const [loading, setLoading] = useState(false);
const navigate = useNavigate();
const { login } = useAuth();
const handleSubmit = async (e: React.FormEvent) => {
e.preventDefault();
setError('');
setLoading(true);
try {
await login(username, password);
navigate('/');
} catch (err: any) {
setError(err.response?.data?.detail || 'Login failed. Please check your credentials.');
} finally {
setLoading(false);
}
};
return (
<div style={styles.container}>
<div style={styles.loginBox}>
<h1 style={styles.title}>VelociCompanion</h1>
<h2 style={styles.subtitle}>Login</h2>
<form onSubmit={handleSubmit} style={styles.form}>
{error && <div style={styles.error}>{error}</div>}
<div style={styles.formGroup}>
<label htmlFor="username" style={styles.label}>Username</label>
<input
id="username"
type="text"
value={username}
onChange={(e) => setUsername(e.target.value)}
required
style={styles.input}
disabled={loading}
/>
</div>
<div style={styles.formGroup}>
<label htmlFor="password" style={styles.label}>Password</label>
<input
id="password"
type="password"
value={password}
onChange={(e) => setPassword(e.target.value)}
required
style={styles.input}
disabled={loading}
/>
</div>
<button
type="submit"
style={styles.button}
disabled={loading}
>
{loading ? 'Logging in...' : 'Login'}
</button>
</form>
</div>
</div>
);
};
const styles: { [key: string]: React.CSSProperties } = {
container: {
display: 'flex',
justifyContent: 'center',
alignItems: 'center',
minHeight: '100vh',
backgroundColor: '#f5f5f5',
},
loginBox: {
backgroundColor: 'white',
padding: '40px',
borderRadius: '8px',
boxShadow: '0 2px 10px rgba(0,0,0,0.1)',
width: '100%',
maxWidth: '400px',
},
title: {
margin: '0 0 10px 0',
fontSize: '28px',
color: '#333',
textAlign: 'center',
},
subtitle: {
margin: '0 0 30px 0',
fontSize: '20px',
color: '#666',
textAlign: 'center',
fontWeight: 'normal',
},
form: {
display: 'flex',
flexDirection: 'column',
},
formGroup: {
marginBottom: '20px',
},
label: {
display: 'block',
marginBottom: '8px',
color: '#333',
fontSize: '14px',
fontWeight: '500',
},
input: {
width: '100%',
padding: '12px',
fontSize: '14px',
border: '1px solid #ddd',
borderRadius: '4px',
boxSizing: 'border-box',
},
button: {
padding: '12px',
fontSize: '16px',
fontWeight: '500',
color: 'white',
backgroundColor: '#007bff',
border: 'none',
borderRadius: '4px',
cursor: 'pointer',
marginTop: '10px',
},
error: {
padding: '12px',
marginBottom: '20px',
backgroundColor: '#fee',
color: '#c33',
borderRadius: '4px',
fontSize: '14px',
},
};
export default Login;

1
frontend/src/react-app-env.d.ts vendored Normal file
View File

@@ -0,0 +1 @@
/// <reference types="react-scripts" />

75
frontend/src/utils/api.ts Normal file
View File

@@ -0,0 +1,75 @@
import axios, { AxiosInstance } from 'axios';
const API_URL = process.env.REACT_APP_API_URL || 'http://localhost:8000';
// Create axios instance with default config
const api: AxiosInstance = axios.create({
baseURL: API_URL,
headers: {
'Content-Type': 'application/json',
},
});
// Request interceptor to add JWT token
api.interceptors.request.use(
(config) => {
const token = localStorage.getItem('access_token');
if (token) {
config.headers.Authorization = `Bearer ${token}`;
}
return config;
},
(error) => {
return Promise.reject(error);
}
);
// Response interceptor to handle 401 errors
api.interceptors.response.use(
(response) => response,
(error) => {
if (error.response?.status === 401) {
// Token expired or invalid, clear storage and redirect to login
localStorage.removeItem('access_token');
window.location.href = '/login';
}
return Promise.reject(error);
}
);
export default api;
// Auth API functions
export const authAPI = {
login: async (username: string, password: string) => {
const formData = new FormData();
formData.append('username', username);
formData.append('password', password);
const response = await api.post('/api/auth/login', formData, {
headers: {
'Content-Type': 'application/x-www-form-urlencoded',
},
});
return response.data;
},
register: async (username: string, password: string, tenantId?: number) => {
const response = await api.post('/api/auth/register', {
username,
password,
tenant_id: tenantId,
});
return response.data;
},
getCurrentUser: async () => {
const response = await api.get('/api/auth/me');
return response.data;
},
updateProfile: async (data: { username?: string; password?: string }) => {
const response = await api.put('/api/auth/me', data);
return response.data;
},
};