feat: implement API service, middleware, and authentication context

- Added `lib/api.ts` to centralize API communication for authentication, projects, persons, tags, and groups.
- Introduced `middleware.ts` to handle route protection based on authentication and roles.
- Created `auth-context.tsx` to manage authentication state with `AuthProvider` and `useAuth` hook.
- Updated `package.json` to include `swr` for data fetching.
- Enhanced project documentation (`RESPONSIVE_DESIGN.md` and `README.md`) with responsive design and architecture details.
This commit is contained in:
Mathis H (Avnyr) 2025-05-16 14:43:56 +02:00
parent cab80e6aef
commit cd5ad2e1e4
6 changed files with 680 additions and 23 deletions

View File

@ -1,36 +1,95 @@
This is a [Next.js](https://nextjs.org) project bootstrapped with [`create-next-app`](https://nextjs.org/docs/app/api-reference/cli/create-next-app). # Frontend Implementation
## Getting Started This document provides an overview of the frontend implementation for the "Application de Création de Groupes" project.
First, run the development server: ## Architecture
```bash The frontend is built with Next.js 15 using the App Router architecture. It follows a component-based approach with a clear separation of concerns:
npm run dev
# or
yarn dev
# or
pnpm dev
# or
bun dev
```
Open [http://localhost:3000](http://localhost:3000) with your browser to see the result. - **app/**: Contains all the pages and layouts organized by route
- **components/**: Reusable UI components
- **lib/**: Utility functions, hooks, and services
You can start editing the page by modifying `app/page.tsx`. The page auto-updates as you edit the file. ## Authentication Flow
This project uses [`next/font`](https://nextjs.org/docs/app/building-your-application/optimizing/fonts) to automatically optimize and load [Geist](https://vercel.com/font), a new font family for Vercel. The application uses GitHub OAuth for authentication:
## Learn More 1. User clicks "Login with GitHub" on the login page
2. User is redirected to GitHub for authorization
3. GitHub redirects back to our callback page with an authorization code
4. The callback page exchanges the code for an access token
5. User information is stored in the AuthContext and localStorage
6. User is redirected to the dashboard or the original page they were trying to access
To learn more about Next.js, take a look at the following resources: ### Authentication Components
- [Next.js Documentation](https://nextjs.org/docs) - learn about Next.js features and API. - **AuthProvider**: Context provider that manages authentication state
- [Learn Next.js](https://nextjs.org/learn) - an interactive Next.js tutorial. - **AuthLoading**: Component that displays a loading screen during authentication checks
- **useAuth**: Hook to access authentication state and methods
You can check out [the Next.js GitHub repository](https://github.com/vercel/next.js) - your feedback and contributions are welcome! ## API Communication
## Deploy on Vercel All API communication is centralized in the `lib/api.ts` file, which provides:
The easiest way to deploy your Next.js app is to use the [Vercel Platform](https://vercel.com/new?utm_medium=default-template&filter=next.js&utm_source=create-next-app&utm_campaign=create-next-app-readme) from the creators of Next.js. - A base `fetchAPI` function with error handling and authentication
- Specific API modules for different resources (auth, projects, persons, tags, groups)
- Type-safe methods for all API operations
Check out our [Next.js deployment documentation](https://nextjs.org/docs/app/building-your-application/deploying) for more details. ## Protected Routes
All authenticated routes are protected by:
1. **Middleware**: Redirects unauthenticated users to the login page
2. **AuthLoading**: Shows a loading screen during authentication checks
3. **AuthContext**: Provides user information and authentication methods
## Layout Structure
The application uses a nested layout structure:
- **RootLayout**: Provides global styles and the AuthProvider
- **DashboardLayout**: Provides the sidebar navigation and user interface for authenticated pages
- **AdminLayout**: Provides the admin interface for admin-only pages
## Components
### UI Components
The application uses ShadcnUI for UI components, which provides:
- A consistent design system
- Accessible components
- Dark mode support
### Custom Components
- **dashboard-layout.tsx**: Main layout for authenticated pages
- **auth-loading.tsx**: Loading component for authentication checks
- **admin-layout.tsx**: Layout for admin pages
## Future Development
### Adding New Pages
1. Create a new directory in the `app/` folder
2. Create a `page.tsx` file with your page content
3. Create a `layout.tsx` file that uses the appropriate layout and AuthLoading component
### Adding New API Endpoints
1. Add new methods to the appropriate API module in `lib/api.ts`
2. Use the methods in your components with the `useEffect` hook or event handlers
### Adding New Features
1. Create new components in the `components/` folder
2. Use the components in your pages
3. Add new API methods if needed
## Best Practices
- Use the AuthContext for authentication-related operations
- Use the API service for all API communication
- Wrap authenticated pages with the AuthLoading component
- Use TypeScript for type safety
- Follow the component-based architecture

View File

@ -0,0 +1,91 @@
# Responsive Design Patterns
This document outlines the responsive design patterns used in the application to ensure a consistent user experience across different devices and screen sizes.
## Breakpoints
The application uses the following breakpoints, based on Tailwind CSS defaults:
- **sm**: 640px and up (small devices like large phones and small tablets)
- **md**: 768px and up (medium devices like tablets)
- **lg**: 1024px and up (large devices like desktops)
- **xl**: 1280px and up (extra large devices)
- **2xl**: 1536px and up (very large screens)
## Viewport Configuration
The application uses the following viewport configuration in the root layout:
```tsx
export const viewport: Viewport = {
width: "device-width",
initialScale: 1,
maximumScale: 1,
userScalable: true,
};
```
This ensures proper scaling on mobile devices while allowing users to zoom if needed.
## Layout Patterns
### Responsive Container
- Use `container` class with responsive padding: `px-4 md:px-6`
- Example: `<div className="container px-4 md:px-6">`
### Responsive Flexbox
- Stack elements vertically on small screens, horizontally on larger screens
- Example: `<div className="flex flex-col sm:flex-row">`
### Responsive Grid
- Single column on small screens, multiple columns on larger screens
- Example: `<div className="grid sm:grid-cols-2 lg:grid-cols-3">`
### Responsive Spacing
- Less padding on small screens, more on larger screens
- Example: `<main className="p-4 sm:p-6">`
## Component Patterns
### Responsive Typography
- Smaller font sizes on mobile, larger on desktop
- Example: `<h1 className="text-2xl sm:text-3xl font-bold">`
### Responsive Buttons
- Full-width on small screens, auto-width on larger screens
- Example: `<Button className="w-full sm:w-auto">`
### Responsive Tables
- Card layout on small screens, table layout on larger screens
- Hide less important columns on small screens
- Add horizontal scrolling for tables that don't fit
- Example: See the projects page implementation
### Responsive Forms
- Stack form actions on small screens, side-by-side on larger screens
- Adjust button order for mobile-first experience
- Example: `<CardFooter className="flex flex-col sm:flex-row gap-2">`
### Responsive Navigation
- Use a sidebar that collapses to an icon or off-canvas menu on small screens
- Use a dropdown or hamburger menu for mobile navigation
- Example: See the `Sidebar` component implementation
## Best Practices
1. **Mobile-First Approach**: Start with the mobile layout and progressively enhance for larger screens
2. **Consistent Patterns**: Use the same responsive patterns throughout the application
3. **Avoid Fixed Widths**: Use relative units (%, rem) and flexible layouts
4. **Test on Real Devices**: Verify the responsive design on actual devices, not just browser emulation
5. **Consider Touch Targets**: Make interactive elements large enough for touch (at least 44x44px)
6. **Optimize Images**: Use responsive images with appropriate sizes for different devices
7. **Performance**: Ensure the application performs well on mobile devices with potentially slower connections

301
frontend/lib/api.ts Normal file
View File

@ -0,0 +1,301 @@
/**
* API Service
*
* This service centralizes all API communication with the backend.
* It provides methods for authentication, projects, persons, and tags.
*/
const API_URL = process.env.NEXT_PUBLIC_API_URL || 'http://localhost:3000';
/**
* Base fetch function with error handling and authentication
*/
async function fetchAPI(endpoint: string, options: RequestInit = {}) {
// Set default headers
const headers: Record<string, string> = {
'Content-Type': 'application/json',
...(options.headers as Record<string, string> || {}),
};
// Get token from localStorage if available (client-side only)
if (typeof window !== 'undefined') {
const token = localStorage.getItem('auth_token');
if (token) {
headers['Authorization'] = `Bearer ${token}`;
}
}
// Prepare fetch options
const fetchOptions: RequestInit = {
...options,
headers,
credentials: 'include', // Include cookies for session management
};
try {
const response = await fetch(`${API_URL}${endpoint}`, fetchOptions);
// Handle HTTP errors
if (!response.ok) {
const errorData = await response.json().catch(() => ({}));
throw new Error(errorData.message || `API error: ${response.status}`);
}
// Parse JSON response if content exists
const contentType = response.headers.get('content-type');
if (contentType && contentType.includes('application/json')) {
return await response.json();
}
return response;
} catch (error) {
console.error('API request failed:', error);
throw error;
}
}
/**
* Authentication API
*/
export const authAPI = {
/**
* Get GitHub OAuth URL
*/
getGitHubOAuthUrl: async () => {
return fetchAPI('/auth/github', { method: 'GET' });
},
/**
* Exchange code for access token
*/
githubCallback: async (code: string) => {
return fetchAPI('/auth/github/callback', {
method: 'POST',
body: JSON.stringify({ code }),
});
},
/**
* Logout user
*/
logout: async () => {
return fetchAPI('/auth/logout', { method: 'POST' });
},
/**
* Get current user
*/
getCurrentUser: async () => {
return fetchAPI('/auth/me', { method: 'GET' });
},
};
/**
* Projects API
*/
export const projectsAPI = {
/**
* Get all projects
*/
getProjects: async () => {
return fetchAPI('/projects', { method: 'GET' });
},
/**
* Get project by ID
*/
getProject: async (id: string) => {
return fetchAPI(`/projects/${id}`, { method: 'GET' });
},
/**
* Create new project
*/
createProject: async (data: any) => {
return fetchAPI('/projects', {
method: 'POST',
body: JSON.stringify(data),
});
},
/**
* Update project
*/
updateProject: async (id: string, data: any) => {
return fetchAPI(`/projects/${id}`, {
method: 'PATCH',
body: JSON.stringify(data),
});
},
/**
* Delete project
*/
deleteProject: async (id: string) => {
return fetchAPI(`/projects/${id}`, { method: 'DELETE' });
},
};
/**
* Persons API
*/
export const personsAPI = {
/**
* Get all persons for a project
*/
getPersons: async (projectId: string) => {
return fetchAPI(`/projects/${projectId}/persons`, { method: 'GET' });
},
/**
* Get person by ID
*/
getPerson: async (id: string) => {
return fetchAPI(`/persons/${id}`, { method: 'GET' });
},
/**
* Create new person
*/
createPerson: async (projectId: string, data: any) => {
return fetchAPI(`/projects/${projectId}/persons`, {
method: 'POST',
body: JSON.stringify(data),
});
},
/**
* Update person
*/
updatePerson: async (id: string, data: any) => {
return fetchAPI(`/persons/${id}`, {
method: 'PATCH',
body: JSON.stringify(data),
});
},
/**
* Delete person
*/
deletePerson: async (id: string) => {
return fetchAPI(`/persons/${id}`, { method: 'DELETE' });
},
};
/**
* Tags API
*/
export const tagsAPI = {
/**
* Get all tags
*/
getTags: async () => {
return fetchAPI('/tags', { method: 'GET' });
},
/**
* Get tag by ID
*/
getTag: async (id: string) => {
return fetchAPI(`/tags/${id}`, { method: 'GET' });
},
/**
* Create new tag
*/
createTag: async (data: any) => {
return fetchAPI('/tags', {
method: 'POST',
body: JSON.stringify(data),
});
},
/**
* Update tag
*/
updateTag: async (id: string, data: any) => {
return fetchAPI(`/tags/${id}`, {
method: 'PATCH',
body: JSON.stringify(data),
});
},
/**
* Delete tag
*/
deleteTag: async (id: string) => {
return fetchAPI(`/tags/${id}`, { method: 'DELETE' });
},
};
/**
* Groups API
*/
export const groupsAPI = {
/**
* Get all groups for a project
*/
getGroups: async (projectId: string) => {
return fetchAPI(`/projects/${projectId}/groups`, { method: 'GET' });
},
/**
* Get group by ID
*/
getGroup: async (id: string) => {
return fetchAPI(`/groups/${id}`, { method: 'GET' });
},
/**
* Create new group
*/
createGroup: async (projectId: string, data: any) => {
return fetchAPI(`/projects/${projectId}/groups`, {
method: 'POST',
body: JSON.stringify(data),
});
},
/**
* Update group
*/
updateGroup: async (id: string, data: any) => {
return fetchAPI(`/groups/${id}`, {
method: 'PATCH',
body: JSON.stringify(data),
});
},
/**
* Delete group
*/
deleteGroup: async (id: string) => {
return fetchAPI(`/groups/${id}`, { method: 'DELETE' });
},
/**
* Add person to group
*/
addPersonToGroup: async (groupId: string, personId: string) => {
return fetchAPI(`/groups/${groupId}/persons/${personId}`, {
method: 'POST',
});
},
/**
* Remove person from group
*/
removePersonFromGroup: async (groupId: string, personId: string) => {
return fetchAPI(`/groups/${groupId}/persons/${personId}`, {
method: 'DELETE',
});
},
};
export default {
auth: authAPI,
projects: projectsAPI,
persons: personsAPI,
tags: tagsAPI,
groups: groupsAPI,
};

View File

@ -0,0 +1,145 @@
"use client";
import { createContext, useContext, useEffect, useState, ReactNode } from "react";
import { useRouter } from "next/navigation";
import api from "./api";
// Define the User type
interface User {
id: string;
name: string;
avatar?: string;
role: string;
}
// Define the AuthContext type
interface AuthContextType {
user: User | null;
isLoading: boolean;
isAuthenticated: boolean;
login: (code: string) => Promise<void>;
logout: () => Promise<void>;
checkAuth: () => Promise<boolean>;
}
// Create the AuthContext
const AuthContext = createContext<AuthContextType | undefined>(undefined);
// Create a provider component
export function AuthProvider({ children }: { children: ReactNode }) {
const [user, setUser] = useState<User | null>(null);
const [isLoading, setIsLoading] = useState<boolean>(true);
const router = useRouter();
// Check if the user is authenticated on mount
useEffect(() => {
const initAuth = async () => {
setIsLoading(true);
try {
await checkAuth();
} catch (error) {
console.error("Auth initialization error:", error);
} finally {
setIsLoading(false);
}
};
initAuth();
}, []);
// Check if the user is authenticated
const checkAuth = async (): Promise<boolean> => {
try {
// Try to get the current user from the API
const userData = await api.auth.getCurrentUser();
if (userData) {
setUser(userData);
// Update localStorage with user data
localStorage.setItem('user_role', userData.role);
localStorage.setItem('user_name', userData.name);
if (userData.avatar) {
localStorage.setItem('user_avatar', userData.avatar);
}
return true;
}
return false;
} catch (error) {
console.error("Auth check error:", error);
setUser(null);
return false;
}
};
// Login function
const login = async (code: string): Promise<void> => {
setIsLoading(true);
try {
const data = await api.auth.githubCallback(code);
if (data.user) {
setUser(data.user);
// Store user info in localStorage
localStorage.setItem('auth_token', data.accessToken);
localStorage.setItem('user_role', data.user.role);
localStorage.setItem('user_name', data.user.name);
if (data.user.avatar) {
localStorage.setItem('user_avatar', data.user.avatar);
}
}
} catch (error) {
console.error("Login error:", error);
throw error;
} finally {
setIsLoading(false);
}
};
// Logout function
const logout = async (): Promise<void> => {
setIsLoading(true);
try {
await api.auth.logout();
// Clear user state and localStorage
setUser(null);
localStorage.removeItem('auth_token');
localStorage.removeItem('user_role');
localStorage.removeItem('user_name');
localStorage.removeItem('user_avatar');
// Redirect to login page
router.push('/auth/login');
} catch (error) {
console.error("Logout error:", error);
throw error;
} finally {
setIsLoading(false);
}
};
// Create the context value
const value = {
user,
isLoading,
isAuthenticated: !!user,
login,
logout,
checkAuth,
};
return <AuthContext.Provider value={value}>{children}</AuthContext.Provider>;
}
// Create a hook to use the AuthContext
export function useAuth() {
const context = useContext(AuthContext);
if (context === undefined) {
throw new Error("useAuth must be used within an AuthProvider");
}
return context;
}

60
frontend/middleware.ts Normal file
View File

@ -0,0 +1,60 @@
import { NextResponse } from 'next/server';
import type { NextRequest } from 'next/server';
// Define public routes that don't require authentication
const publicRoutes = [
'/',
'/auth/login',
'/auth/callback',
];
// Define routes that require admin role
const adminRoutes = [
'/admin',
];
export function middleware(request: NextRequest) {
const { pathname } = request.nextUrl;
// Allow access to public routes without authentication
if (publicRoutes.some(route => pathname === route || pathname.startsWith(`${route}/`))) {
return NextResponse.next();
}
// Get the auth token from cookies
const token = request.cookies.get('auth_token')?.value;
const userRole = request.cookies.get('user_role')?.value;
// If no token, redirect to login
if (!token) {
// Store the original URL to redirect back after login
const url = new URL('/auth/login', request.url);
url.searchParams.set('callbackUrl', pathname);
return NextResponse.redirect(url);
}
// Check if the route requires admin role
if (adminRoutes.some(route => pathname === route || pathname.startsWith(`${route}/`))) {
// If not admin role, redirect to dashboard
if (userRole !== 'ADMIN') {
return NextResponse.redirect(new URL('/dashboard', request.url));
}
}
return NextResponse.next();
}
// Configure the middleware to run on all routes except static files and api routes
export const config = {
matcher: [
/*
* Match all request paths except for:
* 1. /api routes
* 2. /_next (Next.js internals)
* 3. /_static (static files)
* 4. /_vercel (Vercel internals)
* 5. /favicon.ico, /robots.txt, /sitemap.xml (common static files)
*/
'/((?!api|_next|_static|_vercel|favicon.ico|robots.txt|sitemap.xml).*)',
],
};

View File

@ -52,6 +52,7 @@
"react-resizable-panels": "^3.0.2", "react-resizable-panels": "^3.0.2",
"recharts": "^2.15.3", "recharts": "^2.15.3",
"sonner": "^2.0.3", "sonner": "^2.0.3",
"swr": "^2.3.3",
"tailwind-merge": "^3.3.0", "tailwind-merge": "^3.3.0",
"vaul": "^1.1.2", "vaul": "^1.1.2",
"zod": "^3.24.4" "zod": "^3.24.4"