Working UI with auth

This commit is contained in:
Logan Cusano
2025-05-26 02:43:35 -04:00
parent 7e55c120b1
commit f1d8012798
7 changed files with 426 additions and 188 deletions

View File

@@ -1,89 +1,159 @@
// context/AuthContext.tsx
"use client";
import React, { useState, useEffect, createContext, useContext, ReactNode } from 'react';
import { API_BASE_URL } from '@/constants/api';
import { UserDetails, UserRoles, ErrorResponse, AuthContextType } from '@/types'; // Import types
import React, {
createContext,
useContext,
useState,
useEffect,
ReactNode,
useCallback
} from 'react';
import { API_BASE_URL } from '@/constants/api'; // Assuming you have this
import { authAwareFetch } from '@/utils/AuthAwareFetch'; // Import authAwareFetch
export const AuthContext = createContext<AuthContextType | null>(null);
// Define your User and UserRoles types if not already in types.ts
interface User {
username: string;
role: string; // Or UserRoles enum
}
export enum UserRoles {
USER = 'user',
MOD = 'mod',
ADMIN = 'admin',
}
interface AuthContextType {
user: User | null;
token: string | null;
loading: boolean;
login: (username: string, password: string) => Promise<boolean>;
logout: () => void;
hasPermission: (requiredRole: UserRoles) => boolean;
}
const AuthContext = createContext<AuthContextType | undefined>(undefined);
interface AuthProviderProps {
children: ReactNode;
}
export const AuthProvider: React.FC<AuthProviderProps> = ({ children }) => {
const [user, setUser] = useState<User | null>(null);
const [token, setToken] = useState<string | null>(null);
const [user, setUser] = useState<UserDetails | null>(null);
const [loading, setLoading] = useState<boolean>(true);
const [loading, setLoading] = useState<boolean>(true); // Start as true to indicate initial check
useEffect(() => {
const storedToken = localStorage.getItem('jwt_token');
const storedUser = localStorage.getItem('user_data');
if (storedToken && storedUser) {
setToken(storedToken);
try {
setUser(JSON.parse(storedUser));
} catch (error) {
console.error("Failed to parse stored user data:", error);
localStorage.removeItem('user_data');
}
// Function to perform the actual logout steps
const performLogout = useCallback(() => {
setUser(null);
setToken(null);
if (typeof window !== 'undefined') { // Ensure localStorage is available
localStorage.removeItem('token');
localStorage.removeItem('user');
}
setLoading(false);
console.log("User logged out.");
}, []);
// Effect to load token from localStorage on initial mount
useEffect(() => {
let isMounted = true; // Flag to prevent state updates on unmounted component
if (typeof window !== 'undefined' && isMounted) { // Ensure localStorage is available
const storedToken = localStorage.getItem('token');
const storedUser = localStorage.getItem('user');
if (storedToken && storedUser) {
try {
const parsedUser: User = JSON.parse(storedUser);
setToken(storedToken);
setUser(parsedUser);
console.log("Token and user loaded from localStorage.");
} catch (e) {
console.error("Failed to parse user from localStorage, logging out:", e);
performLogout(); // Clear corrupted data
}
}
}
if (isMounted) { // Only set loading to false if component is still mounted
setLoading(false); // Authentication check is complete
}
return () => { isMounted = false; }; // Cleanup for unmount to prevent memory leaks
}, [performLogout]); // Dependency on performLogout for stable reference
const login = async (username: string, password: string): Promise<boolean> => {
setLoading(true);
try {
const response = await fetch(`${API_BASE_URL}/auth/login`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ username, password }),
});
const data = await response.json();
// Use authAwareFetch but explicitly mark as a login attempt
// This will prevent authAwareFetch from triggering logout if it receives a 401
// because that 401 is an expected outcome for invalid credentials.
const response = await authAwareFetch(
'/auth/login', // Your login endpoint
{
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({ username, password }),
isLoginAttempt: true, // Crucial: tell authAwareFetch this is a login attempt
},
performLogout // Pass the logout function
);
if (response.ok) {
setToken(data.access_token);
const tempUser: UserDetails = { id: data.user_id || 'some-id', username: data.username, role: data.role || UserRoles.USER };
setUser(tempUser);
localStorage.setItem('jwt_token', data.access_token);
localStorage.setItem('user_data', JSON.stringify(tempUser));
const data = await response.json();
const receivedToken = data.access_token; // Adjust based on your API response
const receivedUser: User = { // Adjust based on your API response
username: data.username,
role: data.role,
};
if (typeof window !== 'undefined') {
localStorage.setItem('token', receivedToken);
localStorage.setItem('user', JSON.stringify(receivedUser));
}
setToken(receivedToken);
setUser(receivedUser);
console.log("Login successful.");
return true;
} else {
const errorData = data as ErrorResponse;
console.error('Login failed:', errorData.message || errorData.detail || response.statusText);
const errorData = await response.json();
console.error('Login failed:', errorData.message || response.statusText);
return false;
}
} catch (error) {
console.error('Network error during login:', error);
} catch (err) {
console.error('Network error during login:', err);
return false;
} finally {
setLoading(false);
}
};
const logout = () => {
setToken(null);
setUser(null);
localStorage.removeItem('jwt_token');
localStorage.removeItem('user_data');
};
const logout = useCallback(() => {
performLogout();
}, [performLogout]);
const hasPermission = useCallback((requiredRole: UserRoles) => {
if (!user) return false;
// Simple role-based permission check. You might need a more complex hierarchy.
// Example: Admin has all permissions, Mod has Mod and User permissions.
const userRoleIndex = Object.values(UserRoles).indexOf(user.role as UserRoles);
const requiredRoleIndex = Object.values(UserRoles).indexOf(requiredRole);
console.log(userRoleIndex, requiredRoleIndex, user)
return userRoleIndex >= requiredRoleIndex;
}, [user]);
const hasPermission = (requiredRole: UserRoles): boolean => {
if (!user || !user.role) return false;
const roleOrder: Record<UserRoles, number> = {
[UserRoles.USER]: 0,
[UserRoles.MOD]: 1,
[UserRoles.ADMIN]: 2
};
return roleOrder[user.role] >= roleOrder[requiredRole];
};
return (
<AuthContext.Provider value={{ token, user, loading, login, logout, hasPermission }}>
<AuthContext.Provider value={{ user, token, loading, login, logout, hasPermission }}>
{children}
</AuthContext.Provider>
);
};
export const useAuth = (): AuthContextType => {
export const useAuth = () => {
const context = useContext(AuthContext);
if (context === null) {
throw new Error("useAuth must be used within an AuthProvider");
if (context === undefined) {
throw new Error('useAuth must be used within an AuthProvider');
}
return context;
};