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:
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;
|
||||
}
|
||||
Reference in New Issue
Block a user