App with a login page

This commit is contained in:
2025-09-26 23:58:50 +02:00
parent 4de732b0ae
commit fc2e9e621e
21 changed files with 2251 additions and 180 deletions

View File

@@ -348,14 +348,6 @@ class ProcessingJob(BaseModel):
- **Rationale**: Full compatibility with Celery workers and simplified workflow
- **Implementation**: All repositories and services operate synchronously for seamless integration
### Implementation Status
1. ✅ Pydantic models for MongoDB collections
2. ✅ Repository layer for data access (files + processing_jobs + users + documents) - synchronous
3. ✅ Service layer for business logic (auth, user, document, job) - synchronous
4. ✅ Celery tasks for document processing
5. ✅ Watchdog file monitoring implementation
6. ✅ FastAPI integration and startup coordination
## Job Management Layer
@@ -493,15 +485,88 @@ src/file-processor/app/
### Next Implementation Steps
1. **TODO**: Complete file processing pipeline =>
1. ✅ Create Pydantic models for files and processing_jobs collections
2. ✅ Implement repository layer for file and processing job data access (synchronous)
3. ✅ Implement service layer for business logic (synchronous)
4. ✅ Create Celery tasks for document processing (.txt, .pdf, .docx)
5. ✅ Implement Watchdog file monitoring with dedicated observer
6. ✅ Integrate file watcher with FastAPI startup
2. Create protected API routes for user management
3. Build React monitoring interface with authentication
1. Build React Login Page
2. Build React Registration Page
3. Build React Default Dashboard
4. Build React User Management Pages
#### Validated Folders and files
```
src/frontend/src/
├── components/
│ ├── auth/
│ │ ├── LoginForm.jsx # Composant formulaire de login => Done
│ │ └── AuthLayout.jsx # Layout pour les pages d'auth => Done
│ └── common/
│ ├── Header.jsx # Header commun => TODO
│ ├── Layout.jsx # Header commun => TODO
│ └── ProtectedRoutes.jsx # Done
├── contexts/
│ └── AuthContext.jsx # Done
├── pages/
│ ├── LoginPage.jsx # Page complète de login => Done
│ └── DashboardPage.jsx # Page tableau de bord (exemple) => TODO
├── services/
│ └── authService.js # Service API pour auth => Done
├── hooks/
│ └── useAuth.js # Hook React pour gestion auth => TODO
├── utils/
│ └── api.js # Configuration axios/fetch => Done
├── App.jsx # Needs to be updated => TODO
```
#### Choices already made
* Pour la gestion des requêtes API et de l'état d'authentification, je propose
* axios (plus de fonctionnalités) :
* Installation d'axios pour les requêtes HTTP
* Intercepteurs pour gestion automatique du token
* Gestion d'erreurs centralisée
* Pour la gestion de l'état d'authentification et la navigation : Option A + C en même temps
* Option A - Context React + React Router :
* React Context pour l'état global d'auth (user, token, isAuthenticated)
* React Router pour la navigation entre pages
* Routes protégées automatiques
* Option C - Context + localStorage pour persistance :
* Token sauvegardé en localStorage pour rester connecté
* Context qui se recharge au démarrage de l'app
* CSS : Utilisation de daisyUI
#### Package.json
```
{
"name": "frontend",
"private": true,
"version": "0.0.0",
"type": "module",
"scripts": {
"dev": "vite",
"build": "vite build",
"lint": "eslint .",
"preview": "vite preview"
},
"dependencies": {
"@tailwindcss/vite": "^4.1.13",
"axios": "^1.12.2",
"react": "^19.1.1",
"react-dom": "^19.1.1",
"react-router-dom": "^7.9.3"
},
"devDependencies": {
"@eslint/js": "^9.33.0",
"@types/react": "^19.1.10",
"@types/react-dom": "^19.1.7",
"@vitejs/plugin-react": "^5.0.0",
"autoprefixer": "^10.4.21",
"daisyui": "^5.1.23",
"eslint": "^9.33.0",
"eslint-plugin-react-hooks": "^5.2.0",
"eslint-plugin-react-refresh": "^0.4.20",
"globals": "^16.3.0",
"postcss": "^8.5.6",
"tailwindcss": "^4.1.13",
"vite": "^7.1.2"
}
}
```
## Annexes

View File

@@ -47,6 +47,7 @@ pytest-mock==3.15.1
python-dateutil==2.9.0.post0
python-dotenv==1.1.1
python-magic==0.4.27
python-multipart==0.0.20
pytz==2025.2
PyYAML==6.0.2
redis==6.4.0

View File

@@ -8,6 +8,7 @@ motor==3.7.1
pydantic==2.11.9
PyJWT==2.10.1
pymongo==4.15.0
python-multipart==0.0.20
redis==6.4.0
uvicorn==0.35.0
python-magic==0.4.27

File diff suppressed because it is too large Load Diff

View File

@@ -10,18 +10,25 @@
"preview": "vite preview"
},
"dependencies": {
"@tailwindcss/vite": "^4.1.13",
"axios": "^1.12.2",
"react": "^19.1.1",
"react-dom": "^19.1.1"
"react-dom": "^19.1.1",
"react-router-dom": "^7.9.3"
},
"devDependencies": {
"@eslint/js": "^9.33.0",
"@types/react": "^19.1.10",
"@types/react-dom": "^19.1.7",
"@vitejs/plugin-react": "^5.0.0",
"autoprefixer": "^10.4.21",
"daisyui": "^5.1.23",
"eslint": "^9.33.0",
"eslint-plugin-react-hooks": "^5.2.0",
"eslint-plugin-react-refresh": "^0.4.20",
"globals": "^16.3.0",
"postcss": "^8.5.6",
"tailwindcss": "^4.1.13",
"vite": "^7.1.2"
}
}

View File

@@ -1,3 +1,6 @@
@import "tailwindcss";
@plugin "daisyui";
#root {
max-width: 1280px;
margin: 0 auto;

View File

@@ -1,35 +1,34 @@
import { useState } from 'react'
import reactLogo from './assets/react.svg'
import viteLogo from '/vite.svg'
import './App.css'
import { BrowserRouter as Router, Routes, Route, Navigate } from 'react-router-dom';
import { AuthProvider } from './contexts/AuthContext';
import ProtectedRoute from './components/common/ProtectedRoute';
import Layout from './components/common/Layout';
import LoginPage from './pages/LoginPage';
import DashboardPage from './pages/DashboardPage';
function App() {
const [count, setCount] = useState(0)
return (
<>
<div>
<a href="https://vite.dev" target="_blank">
<img src={viteLogo} className="logo" alt="Vite logo" />
</a>
<a href="https://react.dev" target="_blank">
<img src={reactLogo} className="logo react" alt="React logo" />
</a>
<AuthProvider>
<Router>
<div className="App">
<Routes>
{/* Public Routes */}
<Route path="/login" element={<LoginPage />} />
{/* Protected Routes */}
<Route path="/" element={<ProtectedRoute><Layout /></ProtectedRoute>}>
<Route index element={<Navigate to="/dashboard" replace />} />
<Route path="dashboard" element={<DashboardPage />} />
<Route path="documents" element={<div>Documents Page - Coming Soon</div>} />
<Route path="users" element={<div>User Management - Coming Soon</div>} />
</Route>
{/* Catch all route */}
<Route path="*" element={<Navigate to="/dashboard" replace />} />
</Routes>
</div>
<h1>Vite + React</h1>
<div className="card">
<button onClick={() => setCount((count) => count + 1)}>
count is {count}
</button>
<p>
Edit <code>src/App.jsx</code> and save to test HMR
</p>
</div>
<p className="read-the-docs">
Click on the Vite and React logos to learn more
</p>
</>
)
</Router>
</AuthProvider>
);
}
export default App
export default App;

View File

@@ -0,0 +1,33 @@
import React from 'react';
/**
* AuthLayout component for authentication pages
* Provides centered layout with background and responsive design
*
* @param {Object} props - Component props
* @param {React.ReactNode} props.children - Child components to render
*/
function AuthLayout({children}) {
return (
<div className="min-h-screen bg-gradient-to-br from-primary/10 via-base-200 to-secondary/10">
{/* Main container with flex centering */}
<div className="min-h-screen flex items-center justify-center p-4">
{/* Content wrapper for responsive spacing */}
<div className="w-full max-w-md">
{children}
</div>
</div>
{/* Optional decorative elements */}
<div className="fixed top-0 left-0 w-full h-full pointer-events-none overflow-hidden -z-10">
{/* Subtle geometric background pattern */}
<div className="absolute top-20 left-10 w-32 h-32 bg-primary/5 rounded-full blur-2xl"></div>
<div className="absolute bottom-20 right-10 w-40 h-40 bg-secondary/5 rounded-full blur-2xl"></div>
<div
className="absolute top-1/2 left-1/2 transform -translate-x-1/2 -translate-y-1/2 w-60 h-60 bg-accent/3 rounded-full blur-3xl"></div>
</div>
</div>
);
}
export default AuthLayout;

View File

@@ -0,0 +1,203 @@
import React, {useEffect, useState} from 'react';
import {useAuth} from '../../contexts/AuthContext';
/**
* LoginForm component with DaisyUI styling
* Handles user authentication with form validation and error display
*/
function LoginForm() {
const {login, loading, error, clearError} = useAuth();
const [formData, setFormData] = useState({
username: '',
password: '',
});
const [formErrors, setFormErrors] = useState({});
// Clear errors when component mounts or form data changes
useEffect(() => {
if (error) {
const timer = setTimeout(() => {
clearError();
}, 5000); // Clear error after 5 seconds
return () => clearTimeout(timer);
}
}, [error, clearError]);
/**
* Handle input changes and clear related errors
* @param {Event} e - Input change event
*/
const handleInputChange = (e) => {
const {name, value} = e.target;
setFormData(prev => ({
...prev,
[name]: value,
}));
// Clear field error when user starts typing
if (formErrors[name]) {
setFormErrors(prev => ({
...prev,
[name]: '',
}));
}
// Clear global error when user modifies form
if (error) {
clearError();
}
};
/**
* Validate form data before submission
* @returns {boolean} True if form is valid
*/
const validateForm = () => {
const errors = {};
if (!formData.username.trim()) {
errors.username = 'Username is required';
}
if (!formData.password.trim()) {
errors.password = 'Password is required';
} else if (formData.password.length < 3) {
errors.password = 'Password must be at least 3 characters';
}
setFormErrors(errors);
return Object.keys(errors).length === 0;
};
/**
* Handle form submission
* @param {Event} e - Form submission event
*/
const handleSubmit = async (e) => {
e.preventDefault();
if (!validateForm()) {
return;
}
const success = await login(formData.username, formData.password);
if (success) {
// Reset form on successful login
setFormData({username: '', password: ''});
setFormErrors({});
}
};
return (
<div className="card w-full max-w-md shadow-xl bg-base-100">
<div className="card-body">
{/* Card Header */}
<div className="text-center mb-6">
<h1 className="text-3xl font-bold text-primary">MyDocManager</h1>
<p className="text-base-content/70 mt-2">Sign in to your account</p>
</div>
{/* Global Error Alert */}
{error && (
<div className="alert alert-error mb-4">
<svg
xmlns="http://www.w3.org/2000/svg"
className="stroke-current shrink-0 h-6 w-6"
fill="none"
viewBox="0 0 24 24"
>
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth="2"
d="M10 14l2-2m0 0l2-2m-2 2l-2-2m2 2l2 2m7-2a9 9 0 11-18 0 9 9 0 0118 0z"
/>
</svg>
<span>{error}</span>
</div>
)}
{/* Login Form */}
<form onSubmit={handleSubmit} className="space-y-4">
{/* Username Field */}
<div className="form-control">
<label className="label">
<span className="label-text font-medium">Username</span>
</label>
<input
type="text"
name="username"
value={formData.username}
onChange={handleInputChange}
placeholder="Enter your username"
className={`input input-bordered w-full ${
formErrors.username ? 'input-error' : ''
}`}
disabled={loading}
autoComplete="username"
/>
{formErrors.username && (
<label className="label">
<span className="label-text-alt text-error">{formErrors.username}</span>
</label>
)}
</div>
{/* Password Field */}
<div className="form-control">
<label className="label">
<span className="label-text font-medium">Password</span>
</label>
<input
type="password"
name="password"
value={formData.password}
onChange={handleInputChange}
placeholder="Enter your password"
className={`input input-bordered w-full ${
formErrors.password ? 'input-error' : ''
}`}
disabled={loading}
autoComplete="current-password"
/>
{formErrors.password && (
<label className="label">
<span className="label-text-alt text-error">{formErrors.password}</span>
</label>
)}
</div>
{/* Submit Button */}
<div className="form-control mt-6">
<button
type="submit"
className={`btn btn-primary w-full btn-hover-effect ${loading ? 'loading' : ''}`}
disabled={loading}
>
{loading ? (
<>
<span className="loading loading-spinner loading-sm"></span>
Signing in...
</>
) : (
'Sign In'
)}
</button>
</div>
</form>
{/* Additional Info */}
<div className="text-center mt-4">
<p className="text-sm text-base-content/60">
Don't have an account? Contact your administrator.
</p>
</div>
</div>
</div>
);
}
export default LoginForm;

View File

@@ -0,0 +1,70 @@
import { useAuth } from '../../hooks/useAuth';
import { useNavigate } from 'react-router-dom';
const Header = () => {
const { user, logout } = useAuth();
const navigate = useNavigate();
const handleLogout = async () => {
await logout();
navigate('/login');
};
return (
<div className="navbar bg-base-100 shadow-lg">
<div className="navbar-start">
<div className="dropdown">
<div tabIndex={0} role="button" className="btn btn-ghost lg:hidden">
<svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M4 6h16M4 12h8m-8 6h16" />
</svg>
</div>
<ul tabIndex={0} className="menu menu-sm dropdown-content bg-base-100 rounded-box z-[1] mt-3 w-52 p-2 shadow">
<li><a href="/dashboard">Dashboard</a></li>
<li><a href="/documents">Documents</a></li>
{user?.role === 'admin' && (
<li><a href="/users">User Management</a></li>
)}
</ul>
</div>
<a className="btn btn-ghost text-xl" href="/dashboard">
MyDocManager
</a>
</div>
<div className="navbar-center hidden lg:flex">
<ul className="menu menu-horizontal px-1">
<li><a href="/dashboard">Dashboard</a></li>
<li><a href="/documents">Documents</a></li>
{user?.role === 'admin' && (
<li><a href="/users">User Management</a></li>
)}
</ul>
</div>
<div className="navbar-end">
<div className="dropdown dropdown-end">
<div tabIndex={0} role="button" className="btn btn-ghost btn-circle avatar">
<div className="w-10 rounded-full bg-primary text-primary-content flex items-center justify-center">
<span className="text-sm font-medium">
{user?.username?.charAt(0).toUpperCase()}
</span>
</div>
</div>
<ul tabIndex={0} className="menu menu-sm dropdown-content bg-base-100 rounded-box z-[1] mt-3 w-52 p-2 shadow">
<li>
<div className="justify-between">
Profile
<span className="badge badge-sm">{user?.role}</span>
</div>
</li>
<li><a>Settings</a></li>
<li><button onClick={handleLogout}>Logout</button></li>
</ul>
</div>
</div>
</div>
);
};
export default Header;

View File

@@ -0,0 +1,15 @@
import Header from './Header';
import {Outlet} from 'react-router-dom';
const Layout = () => {
return (
<div className="min-h-screen bg-base-200">
<Header/>
<main className="container mx-auto px-4 py-8">
<Outlet/>
</main>
</div>
);
};
export default Layout;

View File

@@ -0,0 +1,69 @@
import React from 'react';
import {Navigate, useLocation} from 'react-router-dom';
import {useAuth} from '../../contexts/AuthContext';
/**
* ProtectedRoute component to guard routes that require authentication
* Redirects to login if user is not authenticated, preserving intended destination
*
* @param {Object} props - Component props
* @param {React.ReactNode} props.children - Child components to render if authenticated
* @param {string[]} props.allowedRoles - Array of roles allowed to access this route (optional)
*/
function ProtectedRoute({children, allowedRoles = []}) {
const {isAuthenticated, loading, user} = useAuth();
const location = useLocation();
// Show loading spinner while checking authentication
if (loading) {
return (
<div className="min-h-screen flex items-center justify-center bg-base-200">
<div className="text-center">
<span className="loading loading-spinner loading-lg text-primary"></span>
<p className="text-base-content/70 mt-4">Checking authentication...</p>
</div>
</div>
);
}
// Redirect to login if not authenticated
if (!isAuthenticated) {
return (
<Navigate
to="/login"
state={{from: location}}
replace
/>
);
}
// Check role-based access if allowedRoles is specified
if (allowedRoles.length > 0 && user && !allowedRoles.includes(user.role)) {
return (
<div className="min-h-screen flex items-center justify-center bg-base-200">
<div className="card w-full max-w-md shadow-xl bg-base-100">
<div className="card-body text-center">
<div className="text-6xl mb-4">🚫</div>
<h2 className="card-title justify-center text-error">Access Denied</h2>
<p className="text-base-content/70 mb-4">
You don't have permission to access this page.
</p>
<div className="card-actions justify-center">
<button
className="btn btn-primary"
onClick={() => window.history.back()}
>
Go Back
</button>
</div>
</div>
</div>
</div>
);
}
// User is authenticated and authorized, render children
return children;
}
export default ProtectedRoute;

View File

@@ -0,0 +1,205 @@
import React, {createContext, useContext, useEffect, useReducer} from 'react';
import authService from '../services/authService';
// Auth state actions
const AUTH_ACTIONS = {
LOGIN_START: 'LOGIN_START',
LOGIN_SUCCESS: 'LOGIN_SUCCESS',
LOGIN_FAILURE: 'LOGIN_FAILURE',
LOGOUT: 'LOGOUT',
LOAD_USER: 'LOAD_USER',
CLEAR_ERROR: 'CLEAR_ERROR',
};
// Initial state
const initialState = {
user: null,
token: null,
isAuthenticated: false,
loading: true, // Loading true initially to check stored auth
error: null,
};
// Auth reducer to manage state transitions
function authReducer(state, action) {
switch (action.type) {
case AUTH_ACTIONS.LOGIN_START:
return {
...state,
loading: true,
error: null,
};
case AUTH_ACTIONS.LOGIN_SUCCESS:
return {
...state,
user: action.payload.user,
token: action.payload.token,
isAuthenticated: true,
loading: false,
error: null,
};
case AUTH_ACTIONS.LOGIN_FAILURE:
return {
...state,
user: null,
token: null,
isAuthenticated: false,
loading: false,
error: action.payload.error,
};
case AUTH_ACTIONS.LOGOUT:
return {
...state,
user: null,
token: null,
isAuthenticated: false,
loading: false,
error: null,
};
case AUTH_ACTIONS.LOAD_USER:
return {
...state,
user: action.payload.user,
token: action.payload.token,
isAuthenticated: !!action.payload.token,
loading: false,
error: null,
};
case AUTH_ACTIONS.CLEAR_ERROR:
return {
...state,
error: null,
};
default:
return state;
}
}
// Create context
const AuthContext = createContext(null);
/**
* AuthProvider component to wrap the app and provide authentication state
* @param {Object} props - Component props
* @param {React.ReactNode} props.children - Child components
*/
export function AuthProvider({children}) {
const [state, dispatch] = useReducer(authReducer, initialState);
// Load stored authentication data on app startup
useEffect(() => {
const loadStoredAuth = () => {
const token = authService.getStoredToken();
const user = authService.getStoredUser();
dispatch({
type: AUTH_ACTIONS.LOAD_USER,
payload: {user, token},
});
};
loadStoredAuth();
}, []);
/**
* Login function to authenticate user
* @param {string} username - User's username
* @param {string} password - User's password
* @returns {Promise<boolean>} True if login successful
*/
const login = async (username, password) => {
try {
dispatch({type: AUTH_ACTIONS.LOGIN_START});
const {access_token, user} = await authService.login(username, password);
dispatch({
type: AUTH_ACTIONS.LOGIN_SUCCESS,
payload: {user, token: access_token},
});
return true;
} catch (error) {
dispatch({
type: AUTH_ACTIONS.LOGIN_FAILURE,
payload: {error: error.message},
});
return false;
}
};
/**
* Logout function to clear authentication state
*/
const logout = () => {
authService.logout();
dispatch({type: AUTH_ACTIONS.LOGOUT});
};
/**
* Clear error message from state
*/
const clearError = () => {
dispatch({type: AUTH_ACTIONS.CLEAR_ERROR});
};
/**
* Refresh user data from API
*/
const refreshUser = async () => {
try {
const user = await authService.getCurrentUser();
dispatch({
type: AUTH_ACTIONS.LOGIN_SUCCESS,
payload: {user, token: state.token},
});
} catch (error) {
console.error('Failed to refresh user data:', error);
// Don't logout on refresh failure, just log error
}
};
// Context value object
const value = {
// State
user: state.user,
token: state.token,
isAuthenticated: state.isAuthenticated,
loading: state.loading,
error: state.error,
// Actions
login,
logout,
clearError,
refreshUser,
};
return (
<AuthContext.Provider value={value}>
{children}
</AuthContext.Provider>
);
}
/**
* Custom hook to use authentication context
* @returns {Object} Auth context value
* @throws {Error} If used outside AuthProvider
*/
export function useAuth() {
const context = useContext(AuthContext);
if (!context) {
throw new Error('useAuth must be used within an AuthProvider');
}
return context;
}
export { AuthContext };

View File

@@ -0,0 +1,12 @@
import {useContext} from 'react';
import {AuthContext} from '../contexts/AuthContext';
export const useAuth = () => {
const context = useContext(AuthContext);
if (!context) {
throw new Error('useAuth must be used within an AuthProvider');
}
return context;
};

View File

@@ -1,68 +1,10 @@
:root {
font-family: system-ui, Avenir, Helvetica, Arial, sans-serif;
line-height: 1.5;
font-weight: 400;
color-scheme: light dark;
color: rgba(255, 255, 255, 0.87);
background-color: #242424;
font-synthesis: none;
text-rendering: optimizeLegibility;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
}
a {
font-weight: 500;
color: #646cff;
text-decoration: inherit;
}
a:hover {
color: #535bf2;
}
@tailwind base;
@tailwind components;
@tailwind utilities;
@plugin "daisyui";
/* Custom styles for the application */
body {
margin: 0;
display: flex;
place-items: center;
min-width: 320px;
min-height: 100vh;
}
h1 {
font-size: 3.2em;
line-height: 1.1;
}
button {
border-radius: 8px;
border: 1px solid transparent;
padding: 0.6em 1.2em;
font-size: 1em;
font-weight: 500;
font-family: inherit;
background-color: #1a1a1a;
cursor: pointer;
transition: border-color 0.25s;
}
button:hover {
border-color: #646cff;
}
button:focus,
button:focus-visible {
outline: 4px auto -webkit-focus-ring-color;
}
@media (prefers-color-scheme: light) {
:root {
color: #213547;
background-color: #ffffff;
}
a:hover {
color: #747bff;
}
button {
background-color: #f9f9f9;
}
@apply bg-base-100 text-base-content;
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif;
}

View File

@@ -0,0 +1,239 @@
import {useEffect, useState} from 'react';
import {useAuth} from '../hooks/useAuth';
const DashboardPage = () => {
const {user} = useAuth();
const [stats, setStats] = useState({
totalDocuments: 0,
processingJobs: 0,
completedJobs: 0,
failedJobs: 0
});
const [recentFiles, setRecentFiles] = useState([]);
const [loading, setLoading] = useState(true);
useEffect(() => {
// Simulate API calls for dashboard data
const fetchDashboardData = async () => {
try {
// TODO: Replace with actual API calls
setTimeout(() => {
setStats({
totalDocuments: 42,
processingJobs: 3,
completedJobs: 38,
failedJobs: 1
});
setRecentFiles([
{
id: 1,
filename: 'invoice_2024.pdf',
status: 'completed',
processedAt: '2024-01-15 14:30:00',
fileType: 'pdf'
},
{
id: 2,
filename: 'contract_draft.docx',
status: 'processing',
processedAt: '2024-01-15 14:25:00',
fileType: 'docx'
},
{
id: 3,
filename: 'receipt_scan.jpg',
status: 'completed',
processedAt: '2024-01-15 14:20:00',
fileType: 'image'
}
]);
setLoading(false);
}, 1000);
} catch (error) {
console.error('Error fetching dashboard data:', error);
setLoading(false);
}
};
fetchDashboardData();
}, []);
const getStatusBadge = (status) => {
const statusColors = {
completed: 'badge-success',
processing: 'badge-warning',
failed: 'badge-error',
pending: 'badge-info'
};
return `badge ${statusColors[status] || 'badge-neutral'}`;
};
const getFileTypeIcon = (fileType) => {
const icons = {
pdf: '📄',
docx: '📝',
image: '🖼️',
txt: '📄'
};
return icons[fileType] || '📄';
};
if (loading) {
return (
<div className="flex justify-center items-center h-64">
<span className="loading loading-spinner loading-lg"></span>
</div>
);
}
return (
<div className="space-y-6">
{/* Welcome Header */}
<div className="bg-base-100 rounded-lg shadow p-6">
<h1 className="text-3xl font-bold text-base-content">
Welcome back, {user?.username}!
</h1>
<p className="text-base-content/60 mt-2">
Here's your document processing overview
</p>
</div>
{/* Stats Cards */}
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-4">
<div className="stat bg-base-100 rounded-lg shadow">
<div className="stat-figure text-primary">
<svg className="w-8 h-8" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2"
d="M9 12h6m-6 4h6m2 5H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z"/>
</svg>
</div>
<div className="stat-title">Total Documents</div>
<div className="stat-value text-primary">{stats.totalDocuments}</div>
</div>
<div className="stat bg-base-100 rounded-lg shadow">
<div className="stat-figure text-warning">
<svg className="w-8 h-8" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2"
d="M12 8v4l3 3m6-3a9 9 0 11-18 0 9 9 0 0118 0z"/>
</svg>
</div>
<div className="stat-title">Processing</div>
<div className="stat-value text-warning">{stats.processingJobs}</div>
</div>
<div className="stat bg-base-100 rounded-lg shadow">
<div className="stat-figure text-success">
<svg className="w-8 h-8" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2"
d="M9 12l2 2 4-4m6 2a9 9 0 11-18 0 9 9 0 0118 0z"/>
</svg>
</div>
<div className="stat-title">Completed</div>
<div className="stat-value text-success">{stats.completedJobs}</div>
</div>
<div className="stat bg-base-100 rounded-lg shadow">
<div className="stat-figure text-error">
<svg className="w-8 h-8" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2"
d="M12 8v4m0 4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z"/>
</svg>
</div>
<div className="stat-title">Failed</div>
<div className="stat-value text-error">{stats.failedJobs}</div>
</div>
</div>
{/* Recent Files */}
<div className="bg-base-100 rounded-lg shadow">
<div className="p-6 border-b border-base-300">
<h2 className="text-xl font-semibold">Recent Files</h2>
</div>
<div className="overflow-x-auto">
<table className="table table-zebra">
<thead>
<tr>
<th>File</th>
<th>Type</th>
<th>Status</th>
<th>Processed At</th>
<th>Actions</th>
</tr>
</thead>
<tbody>
{recentFiles.map((file) => (
<tr key={file.id}>
<td>
<div className="flex items-center space-x-3">
<div className="text-2xl">
{getFileTypeIcon(file.fileType)}
</div>
<div className="font-medium">{file.filename}</div>
</div>
</td>
<td>
<span className="badge badge-outline">
{file.fileType.toUpperCase()}
</span>
</td>
<td>
<span className={getStatusBadge(file.status)}>
{file.status.charAt(0).toUpperCase() + file.status.slice(1)}
</span>
</td>
<td>{file.processedAt}</td>
<td>
<div className="flex space-x-2">
<button className="btn btn-sm btn-ghost">View</button>
<button className="btn btn-sm btn-ghost">Download</button>
</div>
</td>
</tr>
))}
</tbody>
</table>
</div>
</div>
{/* Quick Actions */}
<div className="bg-base-100 rounded-lg shadow p-6">
<h2 className="text-xl font-semibold mb-4">Quick Actions</h2>
<div className="flex flex-wrap gap-4">
<button className="btn btn-primary">
<svg className="w-5 h-5 mr-2" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2"
d="M7 16a4 4 0 01-.88-7.903A5 5 0 1115.9 6L16 6a5 5 0 011 9.9M15 13l-3-3m0 0l-3 3m3-3v12"/>
</svg>
Upload Documents
</button>
<button className="btn btn-outline">
<svg className="w-5 h-5 mr-2" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2"
d="M9 17v-2m3 2v-4m3 4v-6m2 10H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z"/>
</svg>
View Reports
</button>
{user?.role === 'admin' && (
<button className="btn btn-outline">
<svg className="w-5 h-5 mr-2" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2"
d="M12 4.354a4 4 0 110 5.292M15 21H3v-1a6 6 0 0112 0v1zm0 0h6v-1a6 6 0 00-9-5.197m13.5-9a2.5 2.5 0 11-5 0 2.5 2.5 0 015 0z"/>
</svg>
Manage Users
</button>
)}
</div>
</div>
</div>
);
};
export default DashboardPage;

View File

@@ -0,0 +1,48 @@
import React, {useEffect} from 'react';
import {useNavigate} from 'react-router-dom';
import {useAuth} from '../contexts/AuthContext';
import AuthLayout from '../components/auth/AuthLayout';
import LoginForm from '../components/auth/LoginForm';
/**
* LoginPage component
* Full page component that handles login functionality and redirects
*/
function LoginPage() {
const {isAuthenticated, loading} = useAuth();
const navigate = useNavigate();
// Redirect to dashboard if already authenticated
useEffect(() => {
if (!loading && isAuthenticated) {
navigate('/dashboard', {replace: true});
}
}, [isAuthenticated, loading, navigate]);
// Show loading spinner while checking authentication
if (loading) {
return (
<AuthLayout>
<div className="card w-full max-w-md shadow-xl bg-base-100">
<div className="card-body items-center">
<span className="loading loading-spinner loading-lg text-primary"></span>
<p className="text-base-content/70 mt-4">Loading...</p>
</div>
</div>
</AuthLayout>
);
}
// Don't render login form if user is authenticated (prevents flash)
if (isAuthenticated) {
return null;
}
return (
<AuthLayout>
<LoginForm/>
</AuthLayout>
);
}
export default LoginPage;

View File

@@ -0,0 +1,101 @@
import api from '../utils/api';
/**
* Authentication service for handling login, logout, and user profile operations
*/
class AuthService {
/**
* Login user with username and password
* @param {string} username - User's username
* @param {string} password - User's password
* @returns {Promise<{access_token: string, user: Object}>} Login response with token and user data
*/
async login(username, password) {
try {
// FastAPI expects form data for OAuth2PasswordRequestForm
const formData = new FormData();
formData.append('username', username);
formData.append('password', password);
const response = await api.post('/auth/login', formData, {
headers: {
'Content-Type': 'multipart/form-data',
},
});
const {access_token, user} = response.data;
// Store token and user data in localStorage
localStorage.setItem('access_token', access_token);
localStorage.setItem('user', JSON.stringify(user));
return {access_token, user};
} catch (error) {
// Extract error message from response
const errorMessage = error.response?.data?.detail || 'Login failed';
throw new Error(errorMessage);
}
}
/**
* Logout user by clearing stored data
*/
logout() {
localStorage.removeItem('access_token');
localStorage.removeItem('user');
}
/**
* Get current user profile from API
* @returns {Promise<Object>} Current user profile
*/
async getCurrentUser() {
try {
const response = await api.get('/auth/me');
const user = response.data;
// Update stored user data
localStorage.setItem('user', JSON.stringify(user));
return user;
} catch (error) {
const errorMessage = error.response?.data?.detail || 'Failed to get user profile';
throw new Error(errorMessage);
}
}
/**
* Check if user is authenticated by verifying token existence
* @returns {boolean} True if user has valid token
*/
isAuthenticated() {
const token = localStorage.getItem('access_token');
return !!token;
}
/**
* Get stored user data from localStorage
* @returns {Object|null} User data or null if not found
*/
getStoredUser() {
try {
const userStr = localStorage.getItem('user');
return userStr ? JSON.parse(userStr) : null;
} catch (error) {
console.error('Error parsing stored user data:', error);
return null;
}
}
/**
* Get stored access token from localStorage
* @returns {string|null} Access token or null if not found
*/
getStoredToken() {
return localStorage.getItem('access_token');
}
}
// Export singleton instance
const authService = new AuthService();
export default authService;

View File

@@ -0,0 +1,55 @@
import axios from 'axios';
// Base API configuration
const API_BASE_URL = 'http://localhost:8000';
// Create axios instance with default configuration
const api = axios.create({
baseURL: API_BASE_URL,
timeout: 10000, // 10 seconds timeout
headers: {
'Content-Type': 'application/json',
},
});
// Request interceptor to add authentication token
api.interceptors.request.use(
(config) => {
// Get token from localStorage
const token = localStorage.getItem('access_token');
if (token) {
config.headers.Authorization = `Bearer ${token}`;
}
return config;
},
(error) => {
return Promise.reject(error);
}
);
// Response interceptor to handle common errors
api.interceptors.response.use(
(response) => {
return response;
},
(error) => {
// Handle 401 errors (unauthorized)
if (error.response?.status === 401) {
// Clear token from localStorage on 401
localStorage.removeItem('access_token');
localStorage.removeItem('user');
// Redirect to login page
window.location.href = '/login';
}
// Handle other common errors
if (error.response?.status >= 500) {
console.error('Server error:', error.response.data);
}
return Promise.reject(error);
}
);
export default api;

View File

@@ -0,0 +1,14 @@
/** @type {import('tailwindcss').Config} */
export default {
content: [
"./index.html",
"./src/**/*.{js,ts,jsx,tsx}",
],
theme: {
extend: {},
},
plugins: [require("daisyui")],
daisyui: {
themes: ["light", "dark"],
},
}

View File

@@ -1,7 +1,10 @@
import { defineConfig } from 'vite'
import {defineConfig} from 'vite'
import react from '@vitejs/plugin-react'
import tailwindcss from '@tailwindcss/vite'
// https://vite.dev/config/
export default defineConfig({
plugins: [react()],
plugins: [react(),
tailwindcss(),
],
})