Working UI with auth
This commit is contained in:
@@ -1,7 +1,8 @@
|
||||
// components/AppContent.tsx
|
||||
"use client";
|
||||
import React, { useState, useEffect } from 'react';
|
||||
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 BotsManagement from '@/components/BotsManagement';
|
||||
import SystemsManagement from '@/components/SystemsManagement';
|
||||
@@ -9,57 +10,68 @@ import { UserRoles } from '@/types';
|
||||
|
||||
const AppContent: React.FC = () => {
|
||||
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) {
|
||||
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 (
|
||||
<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">
|
||||
<h1 className="text-xl font-bold">Radio App Admin</h1>
|
||||
{user && (
|
||||
<div className="flex items-center space-x-4">
|
||||
<span className="text-sm">Logged in as: {user.username} ({user.role})</span>
|
||||
<Button onClick={() => {
|
||||
logout();
|
||||
setCurrentPage('login');
|
||||
}} variant="outline">Logout</Button>
|
||||
</div>
|
||||
)}
|
||||
<div className="flex items-center space-x-4">
|
||||
<span className="text-sm">Logged in as: {user.username} ({user.role})</span>
|
||||
<Button onClick={handleLogoutAndRedirect} variant="outline">Logout</Button>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<main className="p-6">
|
||||
{currentPage === 'login' && <LoginPage />}
|
||||
{currentPage === 'management' && user && token && hasPermission(UserRoles.MOD) && (
|
||||
<div className="space-y-8">
|
||||
<BotsManagement token={token} />
|
||||
<SystemsManagement token={token} />
|
||||
</div>
|
||||
)}
|
||||
{currentPage === 'management' && user && !hasPermission(UserRoles.MOD) && (
|
||||
<div className="text-center text-red-500 text-lg">
|
||||
You do not have permission to view this page. Your role: {user.role}. Required: {UserRoles.MOD}.
|
||||
<Button onClick={() => setCurrentPage('login')} className="mt-4">Go to Login</Button>
|
||||
</div>
|
||||
)}
|
||||
{currentPage === 'management' && !user && (
|
||||
<div className="text-center text-orange-500 text-lg">
|
||||
Session might have expired. Please login again.
|
||||
<Button onClick={() => setCurrentPage('login')} className="mt-4">Go to Login</Button>
|
||||
</div>
|
||||
)}
|
||||
<div className="mb-4">
|
||||
<Button
|
||||
variant={activeManagementTab === 'bots' ? 'default' : 'outline'}
|
||||
onClick={() => setActiveManagementTab('bots')}
|
||||
className="mr-2"
|
||||
>
|
||||
Bots Management
|
||||
</Button>
|
||||
<Button
|
||||
variant={activeManagementTab === 'systems' ? 'default' : 'outline'}
|
||||
onClick={() => setActiveManagementTab('systems')}
|
||||
>
|
||||
Systems Management
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
{activeManagementTab === 'bots' && <BotsManagement token={token} logoutUser={handleLogoutAndRedirect} />}
|
||||
{activeManagementTab === 'systems' && <SystemsManagement token={token} logoutUser={handleLogoutAndRedirect} />}
|
||||
</main>
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
// BotsManagement.tsx
|
||||
"use client";
|
||||
import React, { useState, useEffect } from 'react';
|
||||
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 { Checkbox } from '@/components/ui/checkbox';
|
||||
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 {
|
||||
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 [discordIds, setDiscordIds] = useState<DiscordId[]>([]);
|
||||
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 [isAddIdDialogOpen, setIsAddIdDialogOpen] = useState<boolean>(false);
|
||||
@@ -39,15 +42,22 @@ const BotsManagement: React.FC<BotsManagementProps> = ({ token }) => {
|
||||
const [assignDismissAction, setAssignDismissAction] = useState<'assign' | 'dismiss'>('assign');
|
||||
|
||||
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);
|
||||
setError('');
|
||||
let accumulatedError = '';
|
||||
|
||||
try {
|
||||
const [nodesRes, discordIdsRes, systemsRes] = await Promise.all([
|
||||
fetch(`${API_BASE_URL}/nodes/`, { headers: { Authorization: `Bearer ${token}` } }),
|
||||
fetch(`${API_BASE_URL}/bots/tokens/`, { headers: { Authorization: `Bearer ${token}` } }),
|
||||
fetch(`${API_BASE_URL}/systems/`, { headers: { Authorization: `Bearer ${token}` } }),
|
||||
authAwareFetch('/nodes/', { token }, logoutUser),
|
||||
authAwareFetch('/bots/tokens/', { token }, logoutUser),
|
||||
authAwareFetch('/systems/', { token }, logoutUser),
|
||||
]);
|
||||
|
||||
if (nodesRes.ok) {
|
||||
@@ -91,6 +101,11 @@ const BotsManagement: React.FC<BotsManagementProps> = ({ token }) => {
|
||||
}
|
||||
|
||||
} 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.');
|
||||
console.error(err);
|
||||
} finally {
|
||||
@@ -99,30 +114,43 @@ const BotsManagement: React.FC<BotsManagementProps> = ({ token }) => {
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
// This effect runs whenever 'token' changes.
|
||||
// If token becomes available, it triggers fetchData.
|
||||
if (token) {
|
||||
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> => {
|
||||
setError('');
|
||||
if (!token) {
|
||||
setError('Authentication token is missing. Please log in.');
|
||||
return;
|
||||
}
|
||||
try {
|
||||
const payload: Omit<DiscordId, '_id'> = {
|
||||
...newIdData,
|
||||
guild_ids: newIdData.guild_ids.split(',').map(id => id.trim()).filter(id => id),
|
||||
};
|
||||
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,
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
Authorization: `Bearer ${token}`,
|
||||
},
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify(payload),
|
||||
});
|
||||
token,
|
||||
}, logoutUser);
|
||||
|
||||
if (response.ok) {
|
||||
fetchData();
|
||||
@@ -138,6 +166,10 @@ const BotsManagement: React.FC<BotsManagementProps> = ({ token }) => {
|
||||
setError(errorMsg);
|
||||
}
|
||||
} 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.`);
|
||||
console.error(err);
|
||||
}
|
||||
@@ -146,11 +178,12 @@ const BotsManagement: React.FC<BotsManagementProps> = ({ token }) => {
|
||||
const handleDeleteId = async (id: string): Promise<void> => {
|
||||
if (!window.confirm('Are you sure you want to delete this Discord ID?')) return;
|
||||
setError('');
|
||||
if (!token) {
|
||||
setError('Authentication token is missing. Please log in.');
|
||||
return;
|
||||
}
|
||||
try {
|
||||
const response = await fetch(`${API_BASE_URL}/bots/token/${id}`, {
|
||||
method: 'DELETE',
|
||||
headers: { Authorization: `Bearer ${token}` },
|
||||
});
|
||||
const response = await authAwareFetch(`/bots/token/${id}`, { method: 'DELETE', token }, logoutUser);
|
||||
if (response.ok) {
|
||||
fetchData();
|
||||
} else {
|
||||
@@ -162,6 +195,10 @@ const BotsManagement: React.FC<BotsManagementProps> = ({ token }) => {
|
||||
setError(errorMsg);
|
||||
}
|
||||
} 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.');
|
||||
console.error(err);
|
||||
}
|
||||
@@ -173,16 +210,18 @@ const BotsManagement: React.FC<BotsManagementProps> = ({ token }) => {
|
||||
setError("Bot Client ID and System must be selected.");
|
||||
return;
|
||||
}
|
||||
if (!token) {
|
||||
setError('Authentication token is missing. Please log in.');
|
||||
return;
|
||||
}
|
||||
try {
|
||||
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',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
Authorization: `Bearer ${token}`,
|
||||
},
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ client_id: selectedBotClientId }),
|
||||
});
|
||||
token,
|
||||
}, logoutUser);
|
||||
if (response.ok) {
|
||||
fetchData();
|
||||
setIsAssignDismissDialogOpen(false);
|
||||
@@ -197,13 +236,21 @@ const BotsManagement: React.FC<BotsManagementProps> = ({ token }) => {
|
||||
setError(errorMsg);
|
||||
}
|
||||
} 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.`);
|
||||
console.error(err);
|
||||
}
|
||||
};
|
||||
|
||||
if (loading) return <p>Loading bots and Discord IDs...</p>;
|
||||
if (error && !loading) return <p className="text-red-500 whitespace-pre-line">{error}</p>;
|
||||
// Only show loading if actively fetching or waiting for token
|
||||
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 (
|
||||
@@ -252,8 +299,8 @@ const BotsManagement: React.FC<BotsManagementProps> = ({ token }) => {
|
||||
|
||||
<div>
|
||||
<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={() => setIsAssignDismissDialogOpen(true)} className="mb-4 ml-2">Assign/Dismiss Bot System</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" disabled={!token}>Assign/Dismiss Bot System</Button>
|
||||
|
||||
<Table>
|
||||
<TableHeader>
|
||||
@@ -287,10 +334,11 @@ const BotsManagement: React.FC<BotsManagementProps> = ({ token }) => {
|
||||
guild_ids: dId.guild_ids?.join(', '),
|
||||
});
|
||||
setIsAddIdDialogOpen(true);
|
||||
}}>
|
||||
}}
|
||||
disabled={!token}>
|
||||
Edit
|
||||
</Button>
|
||||
<Button variant="destructive" size="sm" onClick={() => handleDeleteId(dId._id)}>
|
||||
<Button variant="destructive" size="sm" onClick={() => handleDeleteId(dId._id)} disabled={!token}>
|
||||
Delete
|
||||
</Button>
|
||||
</TableCell>
|
||||
@@ -359,7 +407,9 @@ const BotsManagement: React.FC<BotsManagementProps> = ({ token }) => {
|
||||
</div>
|
||||
</div>
|
||||
<DialogFooter>
|
||||
<Button onClick={handleAddId}>{editingId ? 'Save Changes' : 'Add Discord ID'}</Button>
|
||||
<Button onClick={handleAddId} disabled={!token}>
|
||||
{editingId ? 'Save Changes' : 'Add Discord ID'}
|
||||
</Button>
|
||||
</DialogFooter>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
@@ -413,7 +463,7 @@ const BotsManagement: React.FC<BotsManagementProps> = ({ token }) => {
|
||||
</div>
|
||||
</div>
|
||||
<DialogFooter>
|
||||
<Button onClick={handleAssignDismiss} disabled={!selectedBotClientId || !selectedSystemId}>Perform Action</Button>
|
||||
<Button onClick={handleAssignDismiss} disabled={!selectedBotClientId || !selectedSystemId || !token}>Perform Action</Button>
|
||||
</DialogFooter>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
|
||||
@@ -9,16 +9,18 @@ import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogFooter } from '
|
||||
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select';
|
||||
import { Label } from '@/components/ui/label';
|
||||
import { Input } from '@/components/ui/input';
|
||||
import { authAwareFetch } from '@/utils/AuthAwareFetch';
|
||||
|
||||
interface IndividualClientPageProps {
|
||||
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 [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 [currentClientOp25Status, setCurrentClientOp25Status] = useState<string>('Unknown');
|
||||
|
||||
@@ -38,11 +40,15 @@ const IndividualClientPage: React.FC<IndividualClientPageProps> = ({ clientId, t
|
||||
|
||||
|
||||
const fetchClientStatus = async () => {
|
||||
if (!token) {
|
||||
setLoading(false);
|
||||
setCurrentClientDiscordStatus('Authentication token is missing.');
|
||||
setCurrentClientOp25Status('Authentication token is missing.');
|
||||
return;
|
||||
}
|
||||
setLoading(true);
|
||||
try {
|
||||
setLoading(true);
|
||||
const response = await fetch(`${API_BASE_URL}/nodes/${clientId}/status`, {
|
||||
headers: { Authorization: `Bearer ${token}` },
|
||||
});
|
||||
const response = await authAwareFetch(`/nodes/${clientId}/status`, { token }, logoutUser);
|
||||
if (response.ok) {
|
||||
const data: NodeStatusResponse = await response.json();
|
||||
console.log(data)
|
||||
@@ -53,6 +59,10 @@ const IndividualClientPage: React.FC<IndividualClientPageProps> = ({ clientId, t
|
||||
setCurrentClientOp25Status('Failed to fetch status');
|
||||
}
|
||||
} catch (err) {
|
||||
if (err.message === 'Unauthorized: Session expired or invalid token.') {
|
||||
setLoading(false); // already handled by authAwareFetch
|
||||
return;
|
||||
}
|
||||
setCurrentClientDiscordStatus('Error fetching status');
|
||||
setCurrentClientOp25Status('Error fetching status');
|
||||
} finally {
|
||||
@@ -62,13 +72,16 @@ const IndividualClientPage: React.FC<IndividualClientPageProps> = ({ clientId, t
|
||||
|
||||
const fetchSystems = async (): Promise<void> => {
|
||||
setError('');
|
||||
if (!token) {
|
||||
setLoading(false);
|
||||
setError('Authentication token is missing. Please log in.');
|
||||
return;
|
||||
}
|
||||
setLoading(true);
|
||||
try {
|
||||
const response = await fetch(`${API_BASE_URL}/systems/`, {
|
||||
headers: { Authorization: `Bearer ${token}` },
|
||||
});
|
||||
const response = await authAwareFetch('/systems/', { token }, logoutUser);
|
||||
if (response.ok) {
|
||||
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));
|
||||
setAvailableSystems(filteredSystems);
|
||||
} else {
|
||||
@@ -80,32 +93,47 @@ const IndividualClientPage: React.FC<IndividualClientPageProps> = ({ clientId, t
|
||||
setError(errorMsg);
|
||||
}
|
||||
} 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.');
|
||||
console.error(err);
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
useEffect(() => {
|
||||
fetchClientStatus();
|
||||
fetchSystems(); // Fetch systems when the component mounts or clientId/token changes
|
||||
}, [clientId, token]);
|
||||
if (token) {
|
||||
fetchClientStatus();
|
||||
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 (
|
||||
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 }
|
||||
): Promise<void> => {
|
||||
setMessage('');
|
||||
setError('');
|
||||
setLoading(true);
|
||||
|
||||
if (!token) {
|
||||
setError('Authentication token is missing. Please log in.');
|
||||
setLoading(false);
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const httpOptions: RequestInit = {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'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();
|
||||
|
||||
if (response.ok) {
|
||||
setMessage(`Client "${clientId}" successfully ${action === 'op25_set' ? 'set config for system ' + selectedSystem : action + 'ed'}.`);
|
||||
fetchClientStatus();
|
||||
setIsSetConfigDialogOpen(false); // Close set config dialog on success
|
||||
setIsJoinDiscordDialogOpen(false); // Close join discord dialog on success
|
||||
setIsLeaveDiscordDialogOpen(false); // Close leave discord dialog on success
|
||||
setIsSetConfigDialogOpen(false);
|
||||
setIsJoinDiscordDialogOpen(false);
|
||||
setIsLeaveDiscordDialogOpen(false);
|
||||
} else {
|
||||
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}`);
|
||||
}
|
||||
} 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}`);
|
||||
console.error(err);
|
||||
} finally {
|
||||
@@ -162,8 +194,8 @@ const IndividualClientPage: React.FC<IndividualClientPageProps> = ({ clientId, t
|
||||
};
|
||||
|
||||
const handleJoinClick = () => {
|
||||
setDiscordServerId(''); // Clear previous values
|
||||
setDiscordChannelId(''); // Clear previous values
|
||||
setDiscordServerId('');
|
||||
setDiscordChannelId('');
|
||||
setIsJoinDiscordDialogOpen(true);
|
||||
};
|
||||
|
||||
@@ -172,7 +204,7 @@ const IndividualClientPage: React.FC<IndividualClientPageProps> = ({ clientId, t
|
||||
};
|
||||
|
||||
const handleLeaveClick = () => {
|
||||
setLeaveGuildId(''); // Clear previous value
|
||||
setLeaveGuildId('');
|
||||
setIsLeaveDiscordDialogOpen(true);
|
||||
};
|
||||
|
||||
@@ -187,18 +219,22 @@ const IndividualClientPage: React.FC<IndividualClientPageProps> = ({ clientId, t
|
||||
<CardTitle>Manage Client: {clientId}</CardTitle>
|
||||
</CardHeader>
|
||||
<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>
|
||||
<div className="flex space-x-2">
|
||||
<Button
|
||||
onClick={handleJoinClick} // Open dialog for join
|
||||
disabled={loading}
|
||||
onClick={handleJoinClick}
|
||||
disabled={loading || !token}
|
||||
className="flex-1"
|
||||
>
|
||||
Join Client
|
||||
</Button>
|
||||
<Button
|
||||
onClick={handleLeaveClick} // Open dialog for leave
|
||||
disabled={loading}
|
||||
onClick={handleLeaveClick}
|
||||
disabled={loading || !token}
|
||||
variant="outline"
|
||||
className="flex-1"
|
||||
>
|
||||
@@ -209,15 +245,15 @@ const IndividualClientPage: React.FC<IndividualClientPageProps> = ({ clientId, t
|
||||
<div className="flex space-x-2">
|
||||
<Button
|
||||
onClick={() => handleAction('op25_start')}
|
||||
disabled={loading}
|
||||
disabled={loading || !token}
|
||||
className="flex-1"
|
||||
>
|
||||
{loading && message.includes('start') ? 'Starting...' : 'Start OP25'}
|
||||
</Button>
|
||||
|
||||
<Button
|
||||
onClick={() => setIsSetConfigDialogOpen(true)} // Open dialog for set config
|
||||
disabled={loading}
|
||||
onClick={() => setIsSetConfigDialogOpen(true)}
|
||||
disabled={loading || !token}
|
||||
className="flex-1"
|
||||
>
|
||||
Set OP25 Config
|
||||
@@ -225,7 +261,7 @@ const IndividualClientPage: React.FC<IndividualClientPageProps> = ({ clientId, t
|
||||
|
||||
<Button
|
||||
onClick={() => handleAction('op25_stop')}
|
||||
disabled={loading}
|
||||
disabled={loading || !token}
|
||||
variant="outline"
|
||||
className="flex-1"
|
||||
>
|
||||
@@ -233,8 +269,6 @@ const IndividualClientPage: React.FC<IndividualClientPageProps> = ({ clientId, t
|
||||
</Button>
|
||||
</div>
|
||||
{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>
|
||||
|
||||
{/* Set Config Dialog */}
|
||||
@@ -261,7 +295,7 @@ const IndividualClientPage: React.FC<IndividualClientPageProps> = ({ clientId, t
|
||||
</div>
|
||||
</div>
|
||||
<DialogFooter>
|
||||
<Button onClick={() => handleAction('op25_set')} disabled={!selectedSystem || loading}>
|
||||
<Button onClick={() => handleAction('op25_set')} disabled={!selectedSystem || loading || !token}>
|
||||
{loading ? 'Setting...' : 'Confirm Set Config'}
|
||||
</Button>
|
||||
<Button variant="outline" onClick={() => setIsSetConfigDialogOpen(false)}>
|
||||
@@ -300,7 +334,7 @@ const IndividualClientPage: React.FC<IndividualClientPageProps> = ({ clientId, t
|
||||
</div>
|
||||
</div>
|
||||
<DialogFooter>
|
||||
<Button onClick={handleConfirmJoin} disabled={!discordServerId || !discordChannelId || loading}>
|
||||
<Button onClick={handleConfirmJoin} disabled={!discordServerId || !discordChannelId || loading || !token}>
|
||||
{loading ? 'Joining...' : 'Confirm Join'}
|
||||
</Button>
|
||||
<Button variant="outline" onClick={() => setIsJoinDiscordDialogOpen(false)}>
|
||||
@@ -329,7 +363,7 @@ const IndividualClientPage: React.FC<IndividualClientPageProps> = ({ clientId, t
|
||||
</div>
|
||||
</div>
|
||||
<DialogFooter>
|
||||
<Button onClick={handleConfirmLeave} disabled={!leaveGuildId || loading}>
|
||||
<Button onClick={handleConfirmLeave} disabled={!leaveGuildId || loading || !token}>
|
||||
{loading ? 'Leaving...' : 'Confirm Leave'}
|
||||
</Button>
|
||||
<Button variant="outline" onClick={() => setIsLeaveDiscordDialogOpen(false)}>
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
// LoginPage.tsx
|
||||
"use client";
|
||||
import React, { useState } from 'react';
|
||||
import { Button } from '@/components/ui/button';
|
||||
@@ -10,14 +11,17 @@ const LoginPage: React.FC = () => {
|
||||
const [username, setUsername] = useState<string>('');
|
||||
const [password, setPassword] = 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> => {
|
||||
e.preventDefault();
|
||||
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) {
|
||||
setError('Invalid username or password. Please try again.');
|
||||
setError('Invalid username or password. Please try again.'); //
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
// SystemsManagement.tsx
|
||||
"use client";
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import { Button } from '@/components/ui/button';
|
||||
@@ -9,15 +10,17 @@ import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogFooter } from '
|
||||
import { Textarea } from '@/components/ui/textarea';
|
||||
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select';
|
||||
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 {
|
||||
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 [loading, setLoading] = useState<boolean>(true);
|
||||
const [loading, setLoading] = useState<boolean>(true); // Set to true initially
|
||||
const [error, setError] = useState<string>('');
|
||||
|
||||
const [isAddSystemDialogOpen, setIsAddSystemDialogOpen] = useState<boolean>(false);
|
||||
@@ -42,12 +45,16 @@ const SystemsManagement: React.FC<SystemsManagementProps> = ({ token }) => {
|
||||
const [editingSystem, setEditingSystem] = useState<System | null>(null);
|
||||
|
||||
const fetchSystems = async (): Promise<void> => {
|
||||
if (!token) {
|
||||
setLoading(false);
|
||||
setError('Authentication token is missing. Please log in.');
|
||||
return;
|
||||
}
|
||||
|
||||
setLoading(true);
|
||||
setError('');
|
||||
try {
|
||||
const response = await fetch(`${API_BASE_URL}/systems/`, {
|
||||
headers: { Authorization: `Bearer ${token}` },
|
||||
});
|
||||
const response = await authAwareFetch('/systems/', { token }, logoutUser);
|
||||
if (response.ok) {
|
||||
const data: System[] = await response.json();
|
||||
setSystems(data);
|
||||
@@ -60,6 +67,10 @@ const SystemsManagement: React.FC<SystemsManagementProps> = ({ token }) => {
|
||||
setError(errorMsg);
|
||||
}
|
||||
} 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.');
|
||||
console.error(err);
|
||||
} finally {
|
||||
@@ -70,11 +81,18 @@ const SystemsManagement: React.FC<SystemsManagementProps> = ({ token }) => {
|
||||
useEffect(() => {
|
||||
if (token) {
|
||||
fetchSystems();
|
||||
} else {
|
||||
setLoading(false);
|
||||
setError('Please log in to manage systems.');
|
||||
}
|
||||
}, [token]);
|
||||
|
||||
const handleAddSystem = async (): Promise<void> => {
|
||||
setError('');
|
||||
if (!token) {
|
||||
setError('Authentication token is missing. Please log in.');
|
||||
return;
|
||||
}
|
||||
try {
|
||||
let parsedTags: TalkgroupTag[] | undefined = undefined;
|
||||
try {
|
||||
@@ -110,18 +128,16 @@ const SystemsManagement: React.FC<SystemsManagementProps> = ({ token }) => {
|
||||
|
||||
|
||||
const method = editingSystem ? 'PUT' : 'POST';
|
||||
const url = editingSystem ? `${API_BASE_URL}/systems/${editingSystem._id}` :
|
||||
(payload._id ? `${API_BASE_URL}/systems/${payload._id}` : `${API_BASE_URL}/systems/`);
|
||||
const url = editingSystem ? `/systems/${editingSystem._id}` :
|
||||
(payload._id ? `/systems/${payload._id}` : `/systems/`);
|
||||
|
||||
|
||||
const response = await fetch(url, {
|
||||
const response = await authAwareFetch(url, {
|
||||
method,
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
Authorization: `Bearer ${token}`,
|
||||
},
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify(payload),
|
||||
});
|
||||
token,
|
||||
}, logoutUser);
|
||||
|
||||
if (response.ok) {
|
||||
fetchSystems();
|
||||
@@ -140,6 +156,10 @@ const SystemsManagement: React.FC<SystemsManagementProps> = ({ token }) => {
|
||||
setError(errorMsg);
|
||||
}
|
||||
} 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}`);
|
||||
console.error(err);
|
||||
}
|
||||
@@ -148,11 +168,12 @@ const SystemsManagement: React.FC<SystemsManagementProps> = ({ token }) => {
|
||||
const handleDeleteSystem = async (id: string): Promise<void> => {
|
||||
if (!window.confirm('Are you sure you want to delete this system?')) return;
|
||||
setError('');
|
||||
if (!token) {
|
||||
setError('Authentication token is missing. Please log in.');
|
||||
return;
|
||||
}
|
||||
try {
|
||||
const response = await fetch(`${API_BASE_URL}/systems/${id}`, {
|
||||
method: 'DELETE',
|
||||
headers: { Authorization: `Bearer ${token}` },
|
||||
});
|
||||
const response = await authAwareFetch(`/systems/${id}`, { method: 'DELETE', token }, logoutUser);
|
||||
if (response.ok) {
|
||||
fetchSystems();
|
||||
} else {
|
||||
@@ -164,13 +185,22 @@ const SystemsManagement: React.FC<SystemsManagementProps> = ({ token }) => {
|
||||
setError(errorMsg);
|
||||
}
|
||||
} 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.');
|
||||
console.error(err);
|
||||
}
|
||||
};
|
||||
|
||||
if (loading) return <p>Loading systems...</p>;
|
||||
if (error && !loading) return <p className="text-red-500 whitespace-pre-line">{error}</p>;
|
||||
// Only show loading if actively fetching or waiting for token
|
||||
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 (
|
||||
<Card className="w-full">
|
||||
@@ -188,7 +218,7 @@ const SystemsManagement: React.FC<SystemsManagementProps> = ({ token }) => {
|
||||
avail_on_nodes: '', description: '', tags: '[]', whitelist: '',
|
||||
});
|
||||
setIsAddSystemDialogOpen(true);
|
||||
}} className="mb-4">Add New System</Button>
|
||||
}} className="mb-4" disabled={!token}>Add New System</Button>
|
||||
|
||||
<Table>
|
||||
<TableHeader>
|
||||
@@ -226,10 +256,12 @@ const SystemsManagement: React.FC<SystemsManagementProps> = ({ token }) => {
|
||||
whitelist: system.whitelist ? system.whitelist.join(', ') : '',
|
||||
});
|
||||
setIsAddSystemDialogOpen(true);
|
||||
}}>
|
||||
}}
|
||||
disabled={!token}
|
||||
>
|
||||
Edit
|
||||
</Button>
|
||||
<Button variant="destructive" size="sm" onClick={() => handleDeleteSystem(system._id)}>
|
||||
<Button variant="destructive" size="sm" onClick={() => handleDeleteSystem(system._id)} disabled={!token}>
|
||||
Delete
|
||||
</Button>
|
||||
</TableCell>
|
||||
@@ -343,7 +375,7 @@ const SystemsManagement: React.FC<SystemsManagementProps> = ({ token }) => {
|
||||
</div>
|
||||
</div>
|
||||
<DialogFooter>
|
||||
<Button onClick={handleAddSystem}>{editingSystem ? 'Save Changes' : 'Add System'}</Button>
|
||||
<Button onClick={handleAddSystem} disabled={!token}>{editingSystem ? 'Save Changes' : 'Add System'}</Button>
|
||||
</DialogFooter>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
|
||||
@@ -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;
|
||||
};
|
||||
36
src/utils/AuthAwareFetch.ts
Normal file
36
src/utils/AuthAwareFetch.ts
Normal 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;
|
||||
};
|
||||
Reference in New Issue
Block a user