This commit is contained in:
2026-03-15 21:01:29 -04:00
parent d40f8fc375
commit 4306d86080
73 changed files with 13106 additions and 63 deletions

View File

@@ -0,0 +1,107 @@
import { createContext, useContext, useState, useEffect, ReactNode } from 'react';
import { useNavigate } from 'react-router-dom';
import { useLogin, useLogout, useGetMe } from '../generated/anthoLumeAPIV1';
interface AuthState {
isAuthenticated: boolean;
user: { username: string; is_admin: boolean } | null;
token: string | null;
}
interface AuthContextType extends AuthState {
login: (username: string, password: string) => Promise<void>;
logout: () => void;
}
const AuthContext = createContext<AuthContextType | undefined>(undefined);
const TOKEN_KEY = 'antholume_token';
export function AuthProvider({ children }: { children: ReactNode }) {
const [authState, setAuthState] = useState<AuthState>({
isAuthenticated: false,
user: null,
token: null,
});
const loginMutation = useLogin();
const logoutMutation = useLogout();
const { data: meData } = useGetMe(authState.isAuthenticated ? {} : undefined);
const navigate = useNavigate();
// Check for existing token on mount
useEffect(() => {
const token = localStorage.getItem(TOKEN_KEY);
if (token) {
setAuthState((prev) => ({ ...prev, token, isAuthenticated: true }));
}
}, []);
// Fetch user data when authenticated
useEffect(() => {
if (meData?.data && authState.isAuthenticated) {
setAuthState((prev) => ({
...prev,
user: meData.data,
}));
}
}, [meData, authState.isAuthenticated]);
const login = async (username: string, password: string) => {
try {
loginMutation.mutate({
data: {
username,
password,
},
}, {
onSuccess: () => {
const token = localStorage.getItem(TOKEN_KEY) || 'authenticated';
localStorage.setItem(TOKEN_KEY, token);
setAuthState({
isAuthenticated: true,
user: null,
token,
});
navigate('/');
},
onError: () => {
throw new Error('Login failed');
},
});
} catch (err) {
throw err;
}
};
const logout = () => {
logoutMutation.mutate(undefined, {
onSuccess: () => {
localStorage.removeItem(TOKEN_KEY);
setAuthState({
isAuthenticated: false,
user: null,
token: null,
});
navigate('/login');
},
});
};
return (
<AuthContext.Provider value={{ ...authState, login, logout }}>
{children}
</AuthContext.Provider>
);
}
export function useAuth() {
const context = useContext(AuthContext);
if (context === undefined) {
throw new Error('useAuth must be used within an AuthProvider');
}
return context;
}

View File

@@ -0,0 +1,18 @@
import { Navigate, useLocation } from 'react-router-dom';
import { useAuth } from './AuthContext';
interface ProtectedRouteProps {
children: React.ReactNode;
}
export function ProtectedRoute({ children }: ProtectedRouteProps) {
const { isAuthenticated } = useAuth();
const location = useLocation();
if (!isAuthenticated) {
// Redirect to login with the current location saved
return <Navigate to="/login" state={{ from: location }} replace />;
}
return children;
}

View File

@@ -0,0 +1,35 @@
import axios from 'axios';
const TOKEN_KEY = 'antholume_token';
// Request interceptor to add auth token to requests
axios.interceptors.request.use(
(config) => {
const token = localStorage.getItem(TOKEN_KEY);
if (token && config.headers) {
config.headers.Authorization = `Bearer ${token}`;
}
return config;
},
(error) => {
return Promise.reject(error);
}
);
// Response interceptor to handle auth errors
axios.interceptors.response.use(
(response) => {
return response;
},
(error) => {
if (error.response?.status === 401) {
// Clear token on auth failure
localStorage.removeItem(TOKEN_KEY);
// Optionally redirect to login
// window.location.href = '/login';
}
return Promise.reject(error);
}
);
export default axios;