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:
2025-05-16 14:43:56 +02:00
parent cab80e6aef
commit cd5ad2e1e4
6 changed files with 680 additions and 23 deletions

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;
}