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,7 +1,8 @@
// components/AppContent.tsx
"use client"; "use client";
import React, { useState, useEffect } from 'react'; import React, { useState, useEffect } from 'react';
import { Button } from '@/components/ui/button'; import { Button } from '@/components/ui/button';
import { useAuth } from '@/context/AuthContext'; import { useAuth } from '@/context/AuthContext'; // Correct path to useAuth
import LoginPage from '@/components/LoginPage'; import LoginPage from '@/components/LoginPage';
import BotsManagement from '@/components/BotsManagement'; import BotsManagement from '@/components/BotsManagement';
import SystemsManagement from '@/components/SystemsManagement'; import SystemsManagement from '@/components/SystemsManagement';
@@ -9,57 +10,68 @@ import { UserRoles } from '@/types';
const AppContent: React.FC = () => { const AppContent: React.FC = () => {
const { user, loading, logout, hasPermission, token } = useAuth(); const { user, loading, logout, hasPermission, token } = useAuth();
const [currentPage, setCurrentPage] = useState<'login' | 'management'>(loading ? 'login' : (user ? 'management' : 'login')); const [activeManagementTab, setActiveManagementTab] = useState<'bots' | 'systems'>('bots');
useEffect(() => {
if (!loading) {
if (user && hasPermission(UserRoles.MOD)) {
setCurrentPage('management');
} else {
setCurrentPage('login');
}
}
}, [user, loading, hasPermission]);
// Function to handle logout and redirect to login page
const handleLogoutAndRedirect = () => {
logout();
// No need to set currentPage, as the AppContent will re-render based on useAuth().user becoming null
};
// Display a loading indicator while AuthContext is determining authentication status
if (loading) { if (loading) {
return <div className="flex items-center justify-center min-h-screen bg-gray-100 dark:bg-gray-900">Loading Authentication...</div>; return <div className="flex items-center justify-center min-h-screen bg-gray-100 dark:bg-gray-900">Loading Authentication...</div>;
} }
// Once loading is false, if no user is authenticated, display the LoginPage
if (!user) {
return <LoginPage />;
}
// If a user is authenticated but lacks the required permission, display an access denied message
if (user && !hasPermission(UserRoles.MOD)) {
return (
<div className="min-h-screen flex flex-col items-center justify-center bg-gray-100 dark:bg-gray-900 text-gray-900 dark:text-gray-100 font-sans p-6">
<h2 className="text-xl font-bold text-red-500 mb-4">Access Denied</h2>
<p className="text-lg text-center mb-6">
You do not have sufficient permissions to view this page. Your role: {user.role}. Required: {UserRoles.MOD}.
</p>
<Button onClick={handleLogoutAndRedirect} className="mt-4">Logout</Button>
</div>
);
}
// If loading is false, and we have a user with MOD permission and a token, render the main app content
return ( return (
<div className="min-h-screen bg-gray-100 dark:bg-gray-900 text-gray-900 dark:text-gray-100 font-sans"> <div className="min-h-screen bg-gray-100 dark:bg-gray-900 text-gray-900 dark:text-gray-100 font-sans">
<header className="flex justify-between items-center p-4 bg-white dark:bg-gray-800 shadow-md"> <header className="flex justify-between items-center p-4 bg-white dark:bg-gray-800 shadow-md">
<h1 className="text-xl font-bold">Radio App Admin</h1> <h1 className="text-xl font-bold">Radio App Admin</h1>
{user && ( <div className="flex items-center space-x-4">
<div className="flex items-center space-x-4"> <span className="text-sm">Logged in as: {user.username} ({user.role})</span>
<span className="text-sm">Logged in as: {user.username} ({user.role})</span> <Button onClick={handleLogoutAndRedirect} variant="outline">Logout</Button>
<Button onClick={() => { </div>
logout();
setCurrentPage('login');
}} variant="outline">Logout</Button>
</div>
)}
</header> </header>
<main className="p-6"> <main className="p-6">
{currentPage === 'login' && <LoginPage />} <div className="mb-4">
{currentPage === 'management' && user && token && hasPermission(UserRoles.MOD) && ( <Button
<div className="space-y-8"> variant={activeManagementTab === 'bots' ? 'default' : 'outline'}
<BotsManagement token={token} /> onClick={() => setActiveManagementTab('bots')}
<SystemsManagement token={token} /> className="mr-2"
</div> >
)} Bots Management
{currentPage === 'management' && user && !hasPermission(UserRoles.MOD) && ( </Button>
<div className="text-center text-red-500 text-lg"> <Button
You do not have permission to view this page. Your role: {user.role}. Required: {UserRoles.MOD}. variant={activeManagementTab === 'systems' ? 'default' : 'outline'}
<Button onClick={() => setCurrentPage('login')} className="mt-4">Go to Login</Button> onClick={() => setActiveManagementTab('systems')}
</div> >
)} Systems Management
{currentPage === 'management' && !user && ( </Button>
<div className="text-center text-orange-500 text-lg"> </div>
Session might have expired. Please login again.
<Button onClick={() => setCurrentPage('login')} className="mt-4">Go to Login</Button> {activeManagementTab === 'bots' && <BotsManagement token={token} logoutUser={handleLogoutAndRedirect} />}
</div> {activeManagementTab === 'systems' && <SystemsManagement token={token} logoutUser={handleLogoutAndRedirect} />}
)}
</main> </main>
</div> </div>
); );

View File

@@ -1,3 +1,4 @@
// BotsManagement.tsx
"use client"; "use client";
import React, { useState, useEffect } from 'react'; import React, { useState, useEffect } from 'react';
import Link from 'next/link'; import Link from 'next/link';
@@ -10,17 +11,19 @@ import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogFooter } from '
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select'; import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select';
import { Checkbox } from '@/components/ui/checkbox'; import { Checkbox } from '@/components/ui/checkbox';
import { API_BASE_URL } from '@/constants/api'; import { API_BASE_URL } from '@/constants/api';
import { DiscordId, System, ErrorResponse } from '@/types'; // Import types import { DiscordId, System, ErrorResponse } from '@/types';
import { authAwareFetch } from '@/utils/AuthAwareFetch';
interface BotsManagementProps { interface BotsManagementProps {
token: string; token: string | null; // Allow token to be null initially
logoutUser: () => void;
} }
const BotsManagement: React.FC<BotsManagementProps> = ({ token }) => { const BotsManagement: React.FC<BotsManagementProps> = ({ token, logoutUser }) => {
const [bots, setBots] = useState<string[]>([]); const [bots, setBots] = useState<string[]>([]);
const [discordIds, setDiscordIds] = useState<DiscordId[]>([]); const [discordIds, setDiscordIds] = useState<DiscordId[]>([]);
const [systems, setSystems] = useState<System[]>([]); const [systems, setSystems] = useState<System[]>([]);
const [loading, setLoading] = useState<boolean>(true); const [loading, setLoading] = useState<boolean>(true); // Set to true initially
const [error, setError] = useState<string>(''); const [error, setError] = useState<string>('');
const [isAddIdDialogOpen, setIsAddIdDialogOpen] = useState<boolean>(false); const [isAddIdDialogOpen, setIsAddIdDialogOpen] = useState<boolean>(false);
@@ -39,15 +42,22 @@ const BotsManagement: React.FC<BotsManagementProps> = ({ token }) => {
const [assignDismissAction, setAssignDismissAction] = useState<'assign' | 'dismiss'>('assign'); const [assignDismissAction, setAssignDismissAction] = useState<'assign' | 'dismiss'>('assign');
const fetchData = async (): Promise<void> => { const fetchData = async (): Promise<void> => {
// Only attempt to fetch if a token is present
if (!token) {
setLoading(false); // Stop loading if no token
setError('Authentication token is missing. Please log in.');
return;
}
setLoading(true); setLoading(true);
setError(''); setError('');
let accumulatedError = ''; let accumulatedError = '';
try { try {
const [nodesRes, discordIdsRes, systemsRes] = await Promise.all([ const [nodesRes, discordIdsRes, systemsRes] = await Promise.all([
fetch(`${API_BASE_URL}/nodes/`, { headers: { Authorization: `Bearer ${token}` } }), authAwareFetch('/nodes/', { token }, logoutUser),
fetch(`${API_BASE_URL}/bots/tokens/`, { headers: { Authorization: `Bearer ${token}` } }), authAwareFetch('/bots/tokens/', { token }, logoutUser),
fetch(`${API_BASE_URL}/systems/`, { headers: { Authorization: `Bearer ${token}` } }), authAwareFetch('/systems/', { token }, logoutUser),
]); ]);
if (nodesRes.ok) { if (nodesRes.ok) {
@@ -91,6 +101,11 @@ const BotsManagement: React.FC<BotsManagementProps> = ({ token }) => {
} }
} catch (err: any) { } catch (err: any) {
// Catch specific unauthorized errors and let authAwareFetch handle logout
if (err.message === 'Unauthorized: Session expired or invalid token.') {
setLoading(false); // already handled by authAwareFetch
return;
}
setError('Failed to fetch data. Check server connection or console.'); setError('Failed to fetch data. Check server connection or console.');
console.error(err); console.error(err);
} finally { } finally {
@@ -99,30 +114,43 @@ const BotsManagement: React.FC<BotsManagementProps> = ({ token }) => {
}; };
useEffect(() => { useEffect(() => {
// This effect runs whenever 'token' changes.
// If token becomes available, it triggers fetchData.
if (token) { if (token) {
fetchData(); fetchData();
} else {
// If token is explicitly null (e.g., after logout or initial load without a token)
setLoading(false);
// No need to set error "Please log in" here, as AppContent handles the redirect
// This is a component internal message if it somehow renders without a token when it shouldn't
setError('Authentication required to load data.');
setBots([]); // Clear data if token is gone
setDiscordIds([]);
setSystems([]);
} }
}, [token]); }, [token]); // Dependency on token ensures fetchData is called when token becomes available
const handleAddId = async (): Promise<void> => { const handleAddId = async (): Promise<void> => {
setError(''); setError('');
if (!token) {
setError('Authentication token is missing. Please log in.');
return;
}
try { try {
const payload: Omit<DiscordId, '_id'> = { const payload: Omit<DiscordId, '_id'> = {
...newIdData, ...newIdData,
guild_ids: newIdData.guild_ids.split(',').map(id => id.trim()).filter(id => id), guild_ids: newIdData.guild_ids.split(',').map(id => id.trim()).filter(id => id),
}; };
const method = editingId ? 'PUT' : 'POST'; const method = editingId ? 'PUT' : 'POST';
const url = editingId ? `${API_BASE_URL}/bots/token/${editingId._id}` : `${API_BASE_URL}/bots/token`; const url = editingId ? `/bots/token/${editingId._id}` : `/bots/token`;
const response = await fetch(url, { const response = await authAwareFetch(url, {
method, method,
headers: { headers: { 'Content-Type': 'application/json' },
'Content-Type': 'application/json',
Authorization: `Bearer ${token}`,
},
body: JSON.stringify(payload), body: JSON.stringify(payload),
}); token,
}, logoutUser);
if (response.ok) { if (response.ok) {
fetchData(); fetchData();
@@ -138,6 +166,10 @@ const BotsManagement: React.FC<BotsManagementProps> = ({ token }) => {
setError(errorMsg); setError(errorMsg);
} }
} catch (err: any) { } catch (err: any) {
if (err.message === 'Unauthorized: Session expired or invalid token.') {
setLoading(false); // already handled by authAwareFetch
return;
}
setError(`Network error during ${editingId ? 'update' : 'add'} Discord ID.`); setError(`Network error during ${editingId ? 'update' : 'add'} Discord ID.`);
console.error(err); console.error(err);
} }
@@ -146,11 +178,12 @@ const BotsManagement: React.FC<BotsManagementProps> = ({ token }) => {
const handleDeleteId = async (id: string): Promise<void> => { const handleDeleteId = async (id: string): Promise<void> => {
if (!window.confirm('Are you sure you want to delete this Discord ID?')) return; if (!window.confirm('Are you sure you want to delete this Discord ID?')) return;
setError(''); setError('');
if (!token) {
setError('Authentication token is missing. Please log in.');
return;
}
try { try {
const response = await fetch(`${API_BASE_URL}/bots/token/${id}`, { const response = await authAwareFetch(`/bots/token/${id}`, { method: 'DELETE', token }, logoutUser);
method: 'DELETE',
headers: { Authorization: `Bearer ${token}` },
});
if (response.ok) { if (response.ok) {
fetchData(); fetchData();
} else { } else {
@@ -162,6 +195,10 @@ const BotsManagement: React.FC<BotsManagementProps> = ({ token }) => {
setError(errorMsg); setError(errorMsg);
} }
} catch (err: any) { } catch (err: any) {
if (err.message === 'Unauthorized: Session expired or invalid token.') {
setLoading(false); // already handled by authAwareFetch
return;
}
setError('Network error during delete Discord ID.'); setError('Network error during delete Discord ID.');
console.error(err); console.error(err);
} }
@@ -173,16 +210,18 @@ const BotsManagement: React.FC<BotsManagementProps> = ({ token }) => {
setError("Bot Client ID and System must be selected."); setError("Bot Client ID and System must be selected.");
return; return;
} }
if (!token) {
setError('Authentication token is missing. Please log in.');
return;
}
try { try {
const endpoint = assignDismissAction === 'assign' ? 'assign' : 'dismiss'; const endpoint = assignDismissAction === 'assign' ? 'assign' : 'dismiss';
const response = await fetch(`${API_BASE_URL}/systems/${selectedSystemId}/${endpoint}`, { const response = await authAwareFetch(`/systems/${selectedSystemId}/${endpoint}`, {
method: 'POST', method: 'POST',
headers: { headers: { 'Content-Type': 'application/json' },
'Content-Type': 'application/json',
Authorization: `Bearer ${token}`,
},
body: JSON.stringify({ client_id: selectedBotClientId }), body: JSON.stringify({ client_id: selectedBotClientId }),
}); token,
}, logoutUser);
if (response.ok) { if (response.ok) {
fetchData(); fetchData();
setIsAssignDismissDialogOpen(false); setIsAssignDismissDialogOpen(false);
@@ -197,13 +236,21 @@ const BotsManagement: React.FC<BotsManagementProps> = ({ token }) => {
setError(errorMsg); setError(errorMsg);
} }
} catch (err: any) { } catch (err: any) {
if (err.message === 'Unauthorized: Session expired or invalid token.') {
setLoading(false); // already handled by authAwareFetch
return;
}
setError(`Network error during ${assignDismissAction} bot.`); setError(`Network error during ${assignDismissAction} bot.`);
console.error(err); console.error(err);
} }
}; };
if (loading) return <p>Loading bots and Discord IDs...</p>; // Only show loading if actively fetching or waiting for token
if (error && !loading) return <p className="text-red-500 whitespace-pre-line">{error}</p>; if (loading && token) return <p>Loading bots and Discord IDs...</p>;
// Show error if there's an error
if (error) return <p className="text-red-500 whitespace-pre-line">{error}</p>;
// If not loading and no token, it implies no action can be taken or a login is needed
if (!loading && !token) return <p className="text-orange-500">Please log in to view and manage bots.</p>;
return ( return (
@@ -252,8 +299,8 @@ const BotsManagement: React.FC<BotsManagementProps> = ({ token }) => {
<div> <div>
<h3 className="text-lg font-semibold mb-2">Manage Discord IDs</h3> <h3 className="text-lg font-semibold mb-2">Manage Discord IDs</h3>
<Button onClick={() => { setEditingId(null); setNewIdData({ discord_id: '', name: '', token: '', active: false, guild_ids: '' }); setIsAddIdDialogOpen(true);}} className="mb-4">Add New Discord ID</Button> <Button onClick={() => { setEditingId(null); setNewIdData({ discord_id: '', name: '', token: '', active: false, guild_ids: '' }); setIsAddIdDialogOpen(true);}} className="mb-4" disabled={!token}>Add New Discord ID</Button>
<Button onClick={() => setIsAssignDismissDialogOpen(true)} className="mb-4 ml-2">Assign/Dismiss Bot System</Button> <Button onClick={() => setIsAssignDismissDialogOpen(true)} className="mb-4 ml-2" disabled={!token}>Assign/Dismiss Bot System</Button>
<Table> <Table>
<TableHeader> <TableHeader>
@@ -287,10 +334,11 @@ const BotsManagement: React.FC<BotsManagementProps> = ({ token }) => {
guild_ids: dId.guild_ids?.join(', '), guild_ids: dId.guild_ids?.join(', '),
}); });
setIsAddIdDialogOpen(true); setIsAddIdDialogOpen(true);
}}> }}
disabled={!token}>
Edit Edit
</Button> </Button>
<Button variant="destructive" size="sm" onClick={() => handleDeleteId(dId._id)}> <Button variant="destructive" size="sm" onClick={() => handleDeleteId(dId._id)} disabled={!token}>
Delete Delete
</Button> </Button>
</TableCell> </TableCell>
@@ -359,7 +407,9 @@ const BotsManagement: React.FC<BotsManagementProps> = ({ token }) => {
</div> </div>
</div> </div>
<DialogFooter> <DialogFooter>
<Button onClick={handleAddId}>{editingId ? 'Save Changes' : 'Add Discord ID'}</Button> <Button onClick={handleAddId} disabled={!token}>
{editingId ? 'Save Changes' : 'Add Discord ID'}
</Button>
</DialogFooter> </DialogFooter>
</DialogContent> </DialogContent>
</Dialog> </Dialog>
@@ -413,7 +463,7 @@ const BotsManagement: React.FC<BotsManagementProps> = ({ token }) => {
</div> </div>
</div> </div>
<DialogFooter> <DialogFooter>
<Button onClick={handleAssignDismiss} disabled={!selectedBotClientId || !selectedSystemId}>Perform Action</Button> <Button onClick={handleAssignDismiss} disabled={!selectedBotClientId || !selectedSystemId || !token}>Perform Action</Button>
</DialogFooter> </DialogFooter>
</DialogContent> </DialogContent>
</Dialog> </Dialog>

View File

@@ -9,16 +9,18 @@ import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogFooter } from '
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select'; import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select';
import { Label } from '@/components/ui/label'; import { Label } from '@/components/ui/label';
import { Input } from '@/components/ui/input'; import { Input } from '@/components/ui/input';
import { authAwareFetch } from '@/utils/AuthAwareFetch';
interface IndividualClientPageProps { interface IndividualClientPageProps {
clientId: string; clientId: string;
token: string; token: string | null; // Allow token to be null initially
logoutUser: () => void;
} }
const IndividualClientPage: React.FC<IndividualClientPageProps> = ({ clientId, token }) => { const IndividualClientPage: React.FC<IndividualClientPageProps> = ({ clientId, token, logoutUser }) => {
const [message, setMessage] = useState<string>(''); const [message, setMessage] = useState<string>('');
const [error, setError] = useState<string>(''); const [error, setError] = useState<string>('');
const [loading, setLoading] = useState<boolean>(false); const [loading, setLoading] = useState<boolean>(true); // Set to true initially
const [currentClientDiscordStatus, setCurrentClientDiscordStatus] = useState<string>('Unknown'); const [currentClientDiscordStatus, setCurrentClientDiscordStatus] = useState<string>('Unknown');
const [currentClientOp25Status, setCurrentClientOp25Status] = useState<string>('Unknown'); const [currentClientOp25Status, setCurrentClientOp25Status] = useState<string>('Unknown');
@@ -38,11 +40,15 @@ const IndividualClientPage: React.FC<IndividualClientPageProps> = ({ clientId, t
const fetchClientStatus = async () => { const fetchClientStatus = async () => {
if (!token) {
setLoading(false);
setCurrentClientDiscordStatus('Authentication token is missing.');
setCurrentClientOp25Status('Authentication token is missing.');
return;
}
setLoading(true);
try { try {
setLoading(true); const response = await authAwareFetch(`/nodes/${clientId}/status`, { token }, logoutUser);
const response = await fetch(`${API_BASE_URL}/nodes/${clientId}/status`, {
headers: { Authorization: `Bearer ${token}` },
});
if (response.ok) { if (response.ok) {
const data: NodeStatusResponse = await response.json(); const data: NodeStatusResponse = await response.json();
console.log(data) console.log(data)
@@ -53,6 +59,10 @@ const IndividualClientPage: React.FC<IndividualClientPageProps> = ({ clientId, t
setCurrentClientOp25Status('Failed to fetch status'); setCurrentClientOp25Status('Failed to fetch status');
} }
} catch (err) { } catch (err) {
if (err.message === 'Unauthorized: Session expired or invalid token.') {
setLoading(false); // already handled by authAwareFetch
return;
}
setCurrentClientDiscordStatus('Error fetching status'); setCurrentClientDiscordStatus('Error fetching status');
setCurrentClientOp25Status('Error fetching status'); setCurrentClientOp25Status('Error fetching status');
} finally { } finally {
@@ -62,13 +72,16 @@ const IndividualClientPage: React.FC<IndividualClientPageProps> = ({ clientId, t
const fetchSystems = async (): Promise<void> => { const fetchSystems = async (): Promise<void> => {
setError(''); setError('');
if (!token) {
setLoading(false);
setError('Authentication token is missing. Please log in.');
return;
}
setLoading(true);
try { try {
const response = await fetch(`${API_BASE_URL}/systems/`, { const response = await authAwareFetch('/systems/', { token }, logoutUser);
headers: { Authorization: `Bearer ${token}` },
});
if (response.ok) { if (response.ok) {
const data: System[] = await response.json(); const data: System[] = await response.json();
// Filter systems to those available on the current clientId
const filteredSystems = data.filter(system => system.avail_on_nodes.includes(clientId)); const filteredSystems = data.filter(system => system.avail_on_nodes.includes(clientId));
setAvailableSystems(filteredSystems); setAvailableSystems(filteredSystems);
} else { } else {
@@ -80,32 +93,47 @@ const IndividualClientPage: React.FC<IndividualClientPageProps> = ({ clientId, t
setError(errorMsg); setError(errorMsg);
} }
} catch (err: any) { } catch (err: any) {
if (err.message === 'Unauthorized: Session expired or invalid token.') {
setLoading(false); // already handled by authAwareFetch
return;
}
setError('Failed to fetch systems. Check server connection or console.'); setError('Failed to fetch systems. Check server connection or console.');
console.error(err); console.error(err);
} finally {
setLoading(false);
} }
}; };
useEffect(() => { useEffect(() => {
fetchClientStatus(); if (token) {
fetchSystems(); // Fetch systems when the component mounts or clientId/token changes fetchClientStatus();
}, [clientId, token]); fetchSystems();
} else {
setLoading(false);
setError('Please log in to manage this client.');
}
}, [clientId, token, logoutUser]); // Added logoutUser to dependencies to avoid lint warnings
const handleAction = async ( const handleAction = async (
action: 'join' | 'leave' | 'op25_start' | 'op25_stop' | 'op25_set', action: 'join' | 'leave' | 'op25_start' | 'op25_stop' | 'op25_set',
// Optional parameters for 'join' and 'leave' actions
payloadData?: { server_id?: string; channel_id?: string; guild_id?: string } payloadData?: { server_id?: string; channel_id?: string; guild_id?: string }
): Promise<void> => { ): Promise<void> => {
setMessage(''); setMessage('');
setError(''); setError('');
setLoading(true); setLoading(true);
if (!token) {
setError('Authentication token is missing. Please log in.');
setLoading(false);
return;
}
try { try {
const httpOptions: RequestInit = { const httpOptions: RequestInit = {
method: 'POST', method: 'POST',
headers: { headers: {
'Content-Type': 'application/json', 'Content-Type': 'application/json',
Authorization: `Bearer ${token}`,
} }
} }
@@ -139,21 +167,25 @@ const IndividualClientPage: React.FC<IndividualClientPageProps> = ({ clientId, t
}); });
} }
const response = await fetch(`${API_BASE_URL}/nodes/${clientId}/${action}`, httpOptions); const response = await authAwareFetch(`/nodes/${clientId}/${action}`, { ...httpOptions, token }, logoutUser);
const data = await response.json(); const data = await response.json();
if (response.ok) { if (response.ok) {
setMessage(`Client "${clientId}" successfully ${action === 'op25_set' ? 'set config for system ' + selectedSystem : action + 'ed'}.`); setMessage(`Client "${clientId}" successfully ${action === 'op25_set' ? 'set config for system ' + selectedSystem : action + 'ed'}.`);
fetchClientStatus(); fetchClientStatus();
setIsSetConfigDialogOpen(false); // Close set config dialog on success setIsSetConfigDialogOpen(false);
setIsJoinDiscordDialogOpen(false); // Close join discord dialog on success setIsJoinDiscordDialogOpen(false);
setIsLeaveDiscordDialogOpen(false); // Close leave discord dialog on success setIsLeaveDiscordDialogOpen(false);
} else { } else {
const errorData = data as ErrorResponse; const errorData = data as ErrorResponse;
setError(`Failed to ${action} client "${clientId}": ${errorData.message || (typeof errorData.detail === 'string' ? errorData.detail : JSON.stringify(errorData.detail)) || response.statusText}`); setError(`Failed to ${action} client "${clientId}": ${errorData.message || (typeof errorData.detail === 'string' ? errorData.detail : JSON.stringify(errorData.detail)) || response.statusText}`);
} }
} catch (err: any) { } catch (err: any) {
if (err.message === 'Unauthorized: Session expired or invalid token.') {
setLoading(false); // already handled by authAwareFetch
return;
}
setError(`Network error during ${action} client: ${err.message}`); setError(`Network error during ${action} client: ${err.message}`);
console.error(err); console.error(err);
} finally { } finally {
@@ -162,8 +194,8 @@ const IndividualClientPage: React.FC<IndividualClientPageProps> = ({ clientId, t
}; };
const handleJoinClick = () => { const handleJoinClick = () => {
setDiscordServerId(''); // Clear previous values setDiscordServerId('');
setDiscordChannelId(''); // Clear previous values setDiscordChannelId('');
setIsJoinDiscordDialogOpen(true); setIsJoinDiscordDialogOpen(true);
}; };
@@ -172,7 +204,7 @@ const IndividualClientPage: React.FC<IndividualClientPageProps> = ({ clientId, t
}; };
const handleLeaveClick = () => { const handleLeaveClick = () => {
setLeaveGuildId(''); // Clear previous value setLeaveGuildId('');
setIsLeaveDiscordDialogOpen(true); setIsLeaveDiscordDialogOpen(true);
}; };
@@ -187,18 +219,22 @@ const IndividualClientPage: React.FC<IndividualClientPageProps> = ({ clientId, t
<CardTitle>Manage Client: {clientId}</CardTitle> <CardTitle>Manage Client: {clientId}</CardTitle>
</CardHeader> </CardHeader>
<CardContent className="space-y-4"> <CardContent className="space-y-4">
{error && <p className="text-red-500 text-sm">{error}</p>}
{loading && <p className="text-blue-500 text-sm">Processing request...</p>}
{!loading && !token && <p className="text-orange-500 text-sm">Please log in to manage this client.</p>}
<p>Current Discord Status: {currentClientDiscordStatus}</p> <p>Current Discord Status: {currentClientDiscordStatus}</p>
<div className="flex space-x-2"> <div className="flex space-x-2">
<Button <Button
onClick={handleJoinClick} // Open dialog for join onClick={handleJoinClick}
disabled={loading} disabled={loading || !token}
className="flex-1" className="flex-1"
> >
Join Client Join Client
</Button> </Button>
<Button <Button
onClick={handleLeaveClick} // Open dialog for leave onClick={handleLeaveClick}
disabled={loading} disabled={loading || !token}
variant="outline" variant="outline"
className="flex-1" className="flex-1"
> >
@@ -209,15 +245,15 @@ const IndividualClientPage: React.FC<IndividualClientPageProps> = ({ clientId, t
<div className="flex space-x-2"> <div className="flex space-x-2">
<Button <Button
onClick={() => handleAction('op25_start')} onClick={() => handleAction('op25_start')}
disabled={loading} disabled={loading || !token}
className="flex-1" className="flex-1"
> >
{loading && message.includes('start') ? 'Starting...' : 'Start OP25'} {loading && message.includes('start') ? 'Starting...' : 'Start OP25'}
</Button> </Button>
<Button <Button
onClick={() => setIsSetConfigDialogOpen(true)} // Open dialog for set config onClick={() => setIsSetConfigDialogOpen(true)}
disabled={loading} disabled={loading || !token}
className="flex-1" className="flex-1"
> >
Set OP25 Config Set OP25 Config
@@ -225,7 +261,7 @@ const IndividualClientPage: React.FC<IndividualClientPageProps> = ({ clientId, t
<Button <Button
onClick={() => handleAction('op25_stop')} onClick={() => handleAction('op25_stop')}
disabled={loading} disabled={loading || !token}
variant="outline" variant="outline"
className="flex-1" className="flex-1"
> >
@@ -233,8 +269,6 @@ const IndividualClientPage: React.FC<IndividualClientPageProps> = ({ clientId, t
</Button> </Button>
</div> </div>
{message && <p className="text-green-500 text-sm">{message}</p>} {message && <p className="text-green-500 text-sm">{message}</p>}
{error && <p className="text-red-500 text-sm">{error}</p>}
{loading && <p className="text-blue-500 text-sm">Processing request...</p>}
</CardContent> </CardContent>
{/* Set Config Dialog */} {/* Set Config Dialog */}
@@ -261,7 +295,7 @@ const IndividualClientPage: React.FC<IndividualClientPageProps> = ({ clientId, t
</div> </div>
</div> </div>
<DialogFooter> <DialogFooter>
<Button onClick={() => handleAction('op25_set')} disabled={!selectedSystem || loading}> <Button onClick={() => handleAction('op25_set')} disabled={!selectedSystem || loading || !token}>
{loading ? 'Setting...' : 'Confirm Set Config'} {loading ? 'Setting...' : 'Confirm Set Config'}
</Button> </Button>
<Button variant="outline" onClick={() => setIsSetConfigDialogOpen(false)}> <Button variant="outline" onClick={() => setIsSetConfigDialogOpen(false)}>
@@ -300,7 +334,7 @@ const IndividualClientPage: React.FC<IndividualClientPageProps> = ({ clientId, t
</div> </div>
</div> </div>
<DialogFooter> <DialogFooter>
<Button onClick={handleConfirmJoin} disabled={!discordServerId || !discordChannelId || loading}> <Button onClick={handleConfirmJoin} disabled={!discordServerId || !discordChannelId || loading || !token}>
{loading ? 'Joining...' : 'Confirm Join'} {loading ? 'Joining...' : 'Confirm Join'}
</Button> </Button>
<Button variant="outline" onClick={() => setIsJoinDiscordDialogOpen(false)}> <Button variant="outline" onClick={() => setIsJoinDiscordDialogOpen(false)}>
@@ -329,7 +363,7 @@ const IndividualClientPage: React.FC<IndividualClientPageProps> = ({ clientId, t
</div> </div>
</div> </div>
<DialogFooter> <DialogFooter>
<Button onClick={handleConfirmLeave} disabled={!leaveGuildId || loading}> <Button onClick={handleConfirmLeave} disabled={!leaveGuildId || loading || !token}>
{loading ? 'Leaving...' : 'Confirm Leave'} {loading ? 'Leaving...' : 'Confirm Leave'}
</Button> </Button>
<Button variant="outline" onClick={() => setIsLeaveDiscordDialogOpen(false)}> <Button variant="outline" onClick={() => setIsLeaveDiscordDialogOpen(false)}>

View File

@@ -1,3 +1,4 @@
// LoginPage.tsx
"use client"; "use client";
import React, { useState } from 'react'; import React, { useState } from 'react';
import { Button } from '@/components/ui/button'; import { Button } from '@/components/ui/button';
@@ -10,14 +11,17 @@ const LoginPage: React.FC = () => {
const [username, setUsername] = useState<string>(''); const [username, setUsername] = useState<string>('');
const [password, setPassword] = useState<string>(''); const [password, setPassword] = useState<string>('');
const [error, setError] = useState<string>(''); const [error, setError] = useState<string>('');
const { login } = useAuth(); const { login } = useAuth(); // Assuming login function is now capable of handling 401 internally or through a mechanism
const handleSubmit = async (e: React.FormEvent): Promise<void> => { const handleSubmit = async (e: React.FormEvent): Promise<void> => {
e.preventDefault(); e.preventDefault();
setError(''); setError('');
const success = await login(username, password); // The login function in AuthContext should be updated to use authAwareFetch with isLoginAttempt: true
// This example assumes useAuth().login handles the network call and error setting.
// If not, you'd directly use authAwareFetch here and manage isLoginAttempt.
const success = await login(username, password); //
if (!success) { if (!success) {
setError('Invalid username or password. Please try again.'); setError('Invalid username or password. Please try again.'); //
} }
}; };

View File

@@ -1,3 +1,4 @@
// SystemsManagement.tsx
"use client"; "use client";
import React, { useState, useEffect } from 'react'; import React, { useState, useEffect } from 'react';
import { Button } from '@/components/ui/button'; import { Button } from '@/components/ui/button';
@@ -9,15 +10,17 @@ import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogFooter } from '
import { Textarea } from '@/components/ui/textarea'; import { Textarea } from '@/components/ui/textarea';
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select'; import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select';
import { API_BASE_URL } from '@/constants/api'; import { API_BASE_URL } from '@/constants/api';
import { DemodTypes, TalkgroupTag, System, ErrorResponse } from '@/types'; // Import types import { DemodTypes, TalkgroupTag, System, ErrorResponse } from '@/types';
import { authAwareFetch } from '@/utils/AuthAwareFetch';
interface SystemsManagementProps { interface SystemsManagementProps {
token: string; token: string | null; // Allow token to be null initially
logoutUser: () => void;
} }
const SystemsManagement: React.FC<SystemsManagementProps> = ({ token }) => { const SystemsManagement: React.FC<SystemsManagementProps> = ({ token, logoutUser }) => {
const [systems, setSystems] = useState<System[]>([]); const [systems, setSystems] = useState<System[]>([]);
const [loading, setLoading] = useState<boolean>(true); const [loading, setLoading] = useState<boolean>(true); // Set to true initially
const [error, setError] = useState<string>(''); const [error, setError] = useState<string>('');
const [isAddSystemDialogOpen, setIsAddSystemDialogOpen] = useState<boolean>(false); const [isAddSystemDialogOpen, setIsAddSystemDialogOpen] = useState<boolean>(false);
@@ -42,12 +45,16 @@ const SystemsManagement: React.FC<SystemsManagementProps> = ({ token }) => {
const [editingSystem, setEditingSystem] = useState<System | null>(null); const [editingSystem, setEditingSystem] = useState<System | null>(null);
const fetchSystems = async (): Promise<void> => { const fetchSystems = async (): Promise<void> => {
if (!token) {
setLoading(false);
setError('Authentication token is missing. Please log in.');
return;
}
setLoading(true); setLoading(true);
setError(''); setError('');
try { try {
const response = await fetch(`${API_BASE_URL}/systems/`, { const response = await authAwareFetch('/systems/', { token }, logoutUser);
headers: { Authorization: `Bearer ${token}` },
});
if (response.ok) { if (response.ok) {
const data: System[] = await response.json(); const data: System[] = await response.json();
setSystems(data); setSystems(data);
@@ -60,6 +67,10 @@ const SystemsManagement: React.FC<SystemsManagementProps> = ({ token }) => {
setError(errorMsg); setError(errorMsg);
} }
} catch (err: any) { } catch (err: any) {
if (err.message === 'Unauthorized: Session expired or invalid token.') {
setLoading(false); // already handled by authAwareFetch
return;
}
setError('Failed to fetch systems. Check server connection or console.'); setError('Failed to fetch systems. Check server connection or console.');
console.error(err); console.error(err);
} finally { } finally {
@@ -70,11 +81,18 @@ const SystemsManagement: React.FC<SystemsManagementProps> = ({ token }) => {
useEffect(() => { useEffect(() => {
if (token) { if (token) {
fetchSystems(); fetchSystems();
} else {
setLoading(false);
setError('Please log in to manage systems.');
} }
}, [token]); }, [token]);
const handleAddSystem = async (): Promise<void> => { const handleAddSystem = async (): Promise<void> => {
setError(''); setError('');
if (!token) {
setError('Authentication token is missing. Please log in.');
return;
}
try { try {
let parsedTags: TalkgroupTag[] | undefined = undefined; let parsedTags: TalkgroupTag[] | undefined = undefined;
try { try {
@@ -110,18 +128,16 @@ const SystemsManagement: React.FC<SystemsManagementProps> = ({ token }) => {
const method = editingSystem ? 'PUT' : 'POST'; const method = editingSystem ? 'PUT' : 'POST';
const url = editingSystem ? `${API_BASE_URL}/systems/${editingSystem._id}` : const url = editingSystem ? `/systems/${editingSystem._id}` :
(payload._id ? `${API_BASE_URL}/systems/${payload._id}` : `${API_BASE_URL}/systems/`); (payload._id ? `/systems/${payload._id}` : `/systems/`);
const response = await fetch(url, { const response = await authAwareFetch(url, {
method, method,
headers: { headers: { 'Content-Type': 'application/json' },
'Content-Type': 'application/json',
Authorization: `Bearer ${token}`,
},
body: JSON.stringify(payload), body: JSON.stringify(payload),
}); token,
}, logoutUser);
if (response.ok) { if (response.ok) {
fetchSystems(); fetchSystems();
@@ -140,6 +156,10 @@ const SystemsManagement: React.FC<SystemsManagementProps> = ({ token }) => {
setError(errorMsg); setError(errorMsg);
} }
} catch (err: any) { } catch (err: any) {
if (err.message === 'Unauthorized: Session expired or invalid token.') {
setLoading(false); // already handled by authAwareFetch
return;
}
setError(`Network error during ${editingSystem ? 'update' : 'add'} system. ${err.message}`); setError(`Network error during ${editingSystem ? 'update' : 'add'} system. ${err.message}`);
console.error(err); console.error(err);
} }
@@ -148,11 +168,12 @@ const SystemsManagement: React.FC<SystemsManagementProps> = ({ token }) => {
const handleDeleteSystem = async (id: string): Promise<void> => { const handleDeleteSystem = async (id: string): Promise<void> => {
if (!window.confirm('Are you sure you want to delete this system?')) return; if (!window.confirm('Are you sure you want to delete this system?')) return;
setError(''); setError('');
if (!token) {
setError('Authentication token is missing. Please log in.');
return;
}
try { try {
const response = await fetch(`${API_BASE_URL}/systems/${id}`, { const response = await authAwareFetch(`/systems/${id}`, { method: 'DELETE', token }, logoutUser);
method: 'DELETE',
headers: { Authorization: `Bearer ${token}` },
});
if (response.ok) { if (response.ok) {
fetchSystems(); fetchSystems();
} else { } else {
@@ -164,13 +185,22 @@ const SystemsManagement: React.FC<SystemsManagementProps> = ({ token }) => {
setError(errorMsg); setError(errorMsg);
} }
} catch (err: any) { } catch (err: any) {
if (err.message === 'Unauthorized: Session expired or invalid token.') {
setLoading(false); // already handled by authAwareFetch
return;
}
setError('Network error during delete system.'); setError('Network error during delete system.');
console.error(err); console.error(err);
} }
}; };
if (loading) return <p>Loading systems...</p>; // Only show loading if actively fetching or waiting for token
if (error && !loading) return <p className="text-red-500 whitespace-pre-line">{error}</p>; if (loading && token) return <p>Loading systems...</p>;
// Show error if there's an error OR if no token and not loading
if (error) return <p className="text-red-500 whitespace-pre-line">{error}</p>;
// If not loading and no token, it implies no action can be taken or a login is needed
if (!loading && !token) return <p className="text-orange-500">Please log in to view and manage systems.</p>;
return ( return (
<Card className="w-full"> <Card className="w-full">
@@ -188,7 +218,7 @@ const SystemsManagement: React.FC<SystemsManagementProps> = ({ token }) => {
avail_on_nodes: '', description: '', tags: '[]', whitelist: '', avail_on_nodes: '', description: '', tags: '[]', whitelist: '',
}); });
setIsAddSystemDialogOpen(true); setIsAddSystemDialogOpen(true);
}} className="mb-4">Add New System</Button> }} className="mb-4" disabled={!token}>Add New System</Button>
<Table> <Table>
<TableHeader> <TableHeader>
@@ -226,10 +256,12 @@ const SystemsManagement: React.FC<SystemsManagementProps> = ({ token }) => {
whitelist: system.whitelist ? system.whitelist.join(', ') : '', whitelist: system.whitelist ? system.whitelist.join(', ') : '',
}); });
setIsAddSystemDialogOpen(true); setIsAddSystemDialogOpen(true);
}}> }}
disabled={!token}
>
Edit Edit
</Button> </Button>
<Button variant="destructive" size="sm" onClick={() => handleDeleteSystem(system._id)}> <Button variant="destructive" size="sm" onClick={() => handleDeleteSystem(system._id)} disabled={!token}>
Delete Delete
</Button> </Button>
</TableCell> </TableCell>
@@ -343,7 +375,7 @@ const SystemsManagement: React.FC<SystemsManagementProps> = ({ token }) => {
</div> </div>
</div> </div>
<DialogFooter> <DialogFooter>
<Button onClick={handleAddSystem}>{editingSystem ? 'Save Changes' : 'Add System'}</Button> <Button onClick={handleAddSystem} disabled={!token}>{editingSystem ? 'Save Changes' : 'Add System'}</Button>
</DialogFooter> </DialogFooter>
</DialogContent> </DialogContent>
</Dialog> </Dialog>

View File

@@ -1,89 +1,159 @@
// context/AuthContext.tsx
"use client"; "use client";
import React, { useState, useEffect, createContext, useContext, ReactNode } from 'react'; import React, {
import { API_BASE_URL } from '@/constants/api'; createContext,
import { UserDetails, UserRoles, ErrorResponse, AuthContextType } from '@/types'; // Import types 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 { interface AuthProviderProps {
children: ReactNode; children: ReactNode;
} }
export const AuthProvider: React.FC<AuthProviderProps> = ({ children }) => { export const AuthProvider: React.FC<AuthProviderProps> = ({ children }) => {
const [user, setUser] = useState<User | null>(null);
const [token, setToken] = useState<string | null>(null); const [token, setToken] = useState<string | null>(null);
const [user, setUser] = useState<UserDetails | null>(null); const [loading, setLoading] = useState<boolean>(true); // Start as true to indicate initial check
const [loading, setLoading] = useState<boolean>(true);
useEffect(() => { // Function to perform the actual logout steps
const storedToken = localStorage.getItem('jwt_token'); const performLogout = useCallback(() => {
const storedUser = localStorage.getItem('user_data'); setUser(null);
if (storedToken && storedUser) { setToken(null);
setToken(storedToken); if (typeof window !== 'undefined') { // Ensure localStorage is available
try { localStorage.removeItem('token');
setUser(JSON.parse(storedUser)); localStorage.removeItem('user');
} catch (error) {
console.error("Failed to parse stored user data:", error);
localStorage.removeItem('user_data');
}
} }
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> => { const login = async (username: string, password: string): Promise<boolean> => {
setLoading(true);
try { try {
const response = await fetch(`${API_BASE_URL}/auth/login`, { // Use authAwareFetch but explicitly mark as a login attempt
method: 'POST', // This will prevent authAwareFetch from triggering logout if it receives a 401
headers: { 'Content-Type': 'application/json' }, // because that 401 is an expected outcome for invalid credentials.
body: JSON.stringify({ username, password }), const response = await authAwareFetch(
}); '/auth/login', // Your login endpoint
const data = await response.json(); {
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) { if (response.ok) {
setToken(data.access_token); const data = await response.json();
const tempUser: UserDetails = { id: data.user_id || 'some-id', username: data.username, role: data.role || UserRoles.USER }; const receivedToken = data.access_token; // Adjust based on your API response
setUser(tempUser); const receivedUser: User = { // Adjust based on your API response
localStorage.setItem('jwt_token', data.access_token); username: data.username,
localStorage.setItem('user_data', JSON.stringify(tempUser)); 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; return true;
} else { } else {
const errorData = data as ErrorResponse; const errorData = await response.json();
console.error('Login failed:', errorData.message || errorData.detail || response.statusText); console.error('Login failed:', errorData.message || response.statusText);
return false; return false;
} }
} catch (error) { } catch (err) {
console.error('Network error during login:', error); console.error('Network error during login:', err);
return false; return false;
} finally {
setLoading(false);
} }
}; };
const logout = () => { const logout = useCallback(() => {
setToken(null); performLogout();
setUser(null); }, [performLogout]);
localStorage.removeItem('jwt_token');
localStorage.removeItem('user_data'); 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 ( return (
<AuthContext.Provider value={{ token, user, loading, login, logout, hasPermission }}> <AuthContext.Provider value={{ user, token, loading, login, logout, hasPermission }}>
{children} {children}
</AuthContext.Provider> </AuthContext.Provider>
); );
}; };
export const useAuth = (): AuthContextType => { export const useAuth = () => {
const context = useContext(AuthContext); const context = useContext(AuthContext);
if (context === null) { if (context === undefined) {
throw new Error("useAuth must be used within an AuthProvider"); throw new Error('useAuth must be used within an AuthProvider');
} }
return context; return context;
}; };

View File

@@ -0,0 +1,36 @@
// utils/AuthAwareFetch.ts
import { API_BASE_URL } from '@/constants/api'; // Make sure this path is correct
interface AuthAwareFetchOptions extends RequestInit {
token?: string;
isLoginAttempt?: boolean; // New option to bypass 401 handling for login
}
export const authAwareFetch = async (
url: string,
options: AuthAwareFetchOptions = {},
logoutUser: () => void // Function to call on 401
): Promise<Response> => {
const { token, isLoginAttempt, headers, ...rest } = options;
const requestHeaders = {
...headers,
...(token && { Authorization: `Bearer ${token}` }),
};
console.log(requestHeaders, rest, url)
const response = await fetch(`${API_BASE_URL}${url}`, {
headers: requestHeaders,
...rest,
});
if (response.status === 401 && !isLoginAttempt) {
console.error('Authentication failed (401). Logging out...');
logoutUser();
// Potentially throw an error or return a specific response to indicate logout
throw new Error('Unauthorized: Session expired or invalid token.');
}
return response;
};