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:
parent
cab80e6aef
commit
cd5ad2e1e4
@ -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
|
91
frontend/docs/RESPONSIVE_DESIGN.md
Normal file
91
frontend/docs/RESPONSIVE_DESIGN.md
Normal 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
301
frontend/lib/api.ts
Normal 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,
|
||||||
|
};
|
145
frontend/lib/auth-context.tsx
Normal file
145
frontend/lib/auth-context.tsx
Normal 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
60
frontend/middleware.ts
Normal 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).*)',
|
||||||
|
],
|
||||||
|
};
|
@ -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"
|
||||||
|
Loading…
x
Reference in New Issue
Block a user