diff --git a/src/components/AppContent.tsx b/src/components/AppContent.tsx
index f974cb3..5c2998d 100644
--- a/src/components/AppContent.tsx
+++ b/src/components/AppContent.tsx
@@ -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
Loading Authentication...
;
}
+ // Once loading is false, if no user is authenticated, display the LoginPage
+ if (!user) {
+ return ;
+ }
+
+ // If a user is authenticated but lacks the required permission, display an access denied message
+ if (user && !hasPermission(UserRoles.MOD)) {
+ return (
+
+
Access Denied
+
+ You do not have sufficient permissions to view this page. Your role: {user.role}. Required: {UserRoles.MOD}.
+
+
Logout
+
+ );
+ }
+
+ // If loading is false, and we have a user with MOD permission and a token, render the main app content
return (
Radio App Admin
- {user && (
-
- Logged in as: {user.username} ({user.role})
- {
- logout();
- setCurrentPage('login');
- }} variant="outline">Logout
-
- )}
+
+ Logged in as: {user.username} ({user.role})
+ Logout
+
- {currentPage === 'login' && }
- {currentPage === 'management' && user && token && hasPermission(UserRoles.MOD) && (
-
-
-
-
- )}
- {currentPage === 'management' && user && !hasPermission(UserRoles.MOD) && (
-
- You do not have permission to view this page. Your role: {user.role}. Required: {UserRoles.MOD}.
- setCurrentPage('login')} className="mt-4">Go to Login
-
- )}
- {currentPage === 'management' && !user && (
-
- Session might have expired. Please login again.
- setCurrentPage('login')} className="mt-4">Go to Login
-
- )}
+
+ setActiveManagementTab('bots')}
+ className="mr-2"
+ >
+ Bots Management
+
+ setActiveManagementTab('systems')}
+ >
+ Systems Management
+
+
+
+ {activeManagementTab === 'bots' && }
+ {activeManagementTab === 'systems' && }
);
diff --git a/src/components/BotsManagement.tsx b/src/components/BotsManagement.tsx
index ad7cf55..bd742fc 100644
--- a/src/components/BotsManagement.tsx
+++ b/src/components/BotsManagement.tsx
@@ -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 = ({ token }) => {
+const BotsManagement: React.FC = ({ token, logoutUser }) => {
const [bots, setBots] = useState([]);
const [discordIds, setDiscordIds] = useState([]);
const [systems, setSystems] = useState([]);
- const [loading, setLoading] = useState(true);
+ const [loading, setLoading] = useState(true); // Set to true initially
const [error, setError] = useState('');
const [isAddIdDialogOpen, setIsAddIdDialogOpen] = useState(false);
@@ -39,15 +42,22 @@ const BotsManagement: React.FC = ({ token }) => {
const [assignDismissAction, setAssignDismissAction] = useState<'assign' | 'dismiss'>('assign');
const fetchData = async (): Promise => {
+ // 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 = ({ 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 = ({ 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 => {
setError('');
+ if (!token) {
+ setError('Authentication token is missing. Please log in.');
+ return;
+ }
try {
const payload: Omit = {
...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 = ({ 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 = ({ token }) => {
const handleDeleteId = async (id: string): Promise => {
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 = ({ 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 = ({ 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 = ({ 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 Loading bots and Discord IDs...
;
- if (error && !loading) return {error}
;
+ // Only show loading if actively fetching or waiting for token
+ if (loading && token) return Loading bots and Discord IDs...
;
+ // Show error if there's an error
+ if (error) return {error}
;
+ // If not loading and no token, it implies no action can be taken or a login is needed
+ if (!loading && !token) return Please log in to view and manage bots.
;
return (
@@ -252,8 +299,8 @@ const BotsManagement: React.FC = ({ token }) => {
Manage Discord IDs
-
{ setEditingId(null); setNewIdData({ discord_id: '', name: '', token: '', active: false, guild_ids: '' }); setIsAddIdDialogOpen(true);}} className="mb-4">Add New Discord ID
-
setIsAssignDismissDialogOpen(true)} className="mb-4 ml-2">Assign/Dismiss Bot System
+
{ setEditingId(null); setNewIdData({ discord_id: '', name: '', token: '', active: false, guild_ids: '' }); setIsAddIdDialogOpen(true);}} className="mb-4" disabled={!token}>Add New Discord ID
+
setIsAssignDismissDialogOpen(true)} className="mb-4 ml-2" disabled={!token}>Assign/Dismiss Bot System
@@ -287,10 +334,11 @@ const BotsManagement: React.FC = ({ token }) => {
guild_ids: dId.guild_ids?.join(', '),
});
setIsAddIdDialogOpen(true);
- }}>
+ }}
+ disabled={!token}>
Edit
- handleDeleteId(dId._id)}>
+ handleDeleteId(dId._id)} disabled={!token}>
Delete
@@ -359,7 +407,9 @@ const BotsManagement: React.FC = ({ token }) => {
- {editingId ? 'Save Changes' : 'Add Discord ID'}
+
+ {editingId ? 'Save Changes' : 'Add Discord ID'}
+
@@ -413,7 +463,7 @@ const BotsManagement: React.FC = ({ token }) => {
- Perform Action
+ Perform Action
diff --git a/src/components/IndividualClientPage.tsx b/src/components/IndividualClientPage.tsx
index 0ed9803..20854b0 100644
--- a/src/components/IndividualClientPage.tsx
+++ b/src/components/IndividualClientPage.tsx
@@ -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 = ({ clientId, token }) => {
+const IndividualClientPage: React.FC = ({ clientId, token, logoutUser }) => {
const [message, setMessage] = useState('');
const [error, setError] = useState('');
- const [loading, setLoading] = useState(false);
+ const [loading, setLoading] = useState(true); // Set to true initially
const [currentClientDiscordStatus, setCurrentClientDiscordStatus] = useState('Unknown');
const [currentClientOp25Status, setCurrentClientOp25Status] = useState('Unknown');
@@ -38,11 +40,15 @@ const IndividualClientPage: React.FC = ({ 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 = ({ 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 = ({ clientId, t
const fetchSystems = async (): Promise => {
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 = ({ 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 => {
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 = ({ 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 = ({ clientId, t
};
const handleJoinClick = () => {
- setDiscordServerId(''); // Clear previous values
- setDiscordChannelId(''); // Clear previous values
+ setDiscordServerId('');
+ setDiscordChannelId('');
setIsJoinDiscordDialogOpen(true);
};
@@ -172,7 +204,7 @@ const IndividualClientPage: React.FC = ({ clientId, t
};
const handleLeaveClick = () => {
- setLeaveGuildId(''); // Clear previous value
+ setLeaveGuildId('');
setIsLeaveDiscordDialogOpen(true);
};
@@ -187,18 +219,22 @@ const IndividualClientPage: React.FC = ({ clientId, t
Manage Client: {clientId}
+ {error && {error}
}
+ {loading && Processing request...
}
+ {!loading && !token && Please log in to manage this client.
}
+
Current Discord Status: {currentClientDiscordStatus}
Join Client
@@ -209,15 +245,15 @@ const IndividualClientPage: React.FC = ({ clientId, t
handleAction('op25_start')}
- disabled={loading}
+ disabled={loading || !token}
className="flex-1"
>
{loading && message.includes('start') ? 'Starting...' : 'Start OP25'}
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 = ({ clientId, t
handleAction('op25_stop')}
- disabled={loading}
+ disabled={loading || !token}
variant="outline"
className="flex-1"
>
@@ -233,8 +269,6 @@ const IndividualClientPage: React.FC = ({ clientId, t
{message && {message}
}
- {error && {error}
}
- {loading && Processing request...
}
{/* Set Config Dialog */}
@@ -261,7 +295,7 @@ const IndividualClientPage: React.FC = ({ clientId, t
- handleAction('op25_set')} disabled={!selectedSystem || loading}>
+ handleAction('op25_set')} disabled={!selectedSystem || loading || !token}>
{loading ? 'Setting...' : 'Confirm Set Config'}
setIsSetConfigDialogOpen(false)}>
@@ -300,7 +334,7 @@ const IndividualClientPage: React.FC = ({ clientId, t
-
+
{loading ? 'Joining...' : 'Confirm Join'}
setIsJoinDiscordDialogOpen(false)}>
@@ -329,7 +363,7 @@ const IndividualClientPage: React.FC = ({ clientId, t
-
+
{loading ? 'Leaving...' : 'Confirm Leave'}
setIsLeaveDiscordDialogOpen(false)}>
diff --git a/src/components/LoginPage.tsx b/src/components/LoginPage.tsx
index bb01d47..7435c0a 100644
--- a/src/components/LoginPage.tsx
+++ b/src/components/LoginPage.tsx
@@ -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('');
const [password, setPassword] = useState('');
const [error, setError] = useState('');
- 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 => {
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.'); //
}
};
diff --git a/src/components/SystemsManagement.tsx b/src/components/SystemsManagement.tsx
index efd66de..6afccbb 100644
--- a/src/components/SystemsManagement.tsx
+++ b/src/components/SystemsManagement.tsx
@@ -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 = ({ token }) => {
+const SystemsManagement: React.FC = ({ token, logoutUser }) => {
const [systems, setSystems] = useState([]);
- const [loading, setLoading] = useState(true);
+ const [loading, setLoading] = useState(true); // Set to true initially
const [error, setError] = useState('');
const [isAddSystemDialogOpen, setIsAddSystemDialogOpen] = useState(false);
@@ -42,12 +45,16 @@ const SystemsManagement: React.FC = ({ token }) => {
const [editingSystem, setEditingSystem] = useState(null);
const fetchSystems = async (): Promise => {
+ 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 = ({ 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 = ({ token }) => {
useEffect(() => {
if (token) {
fetchSystems();
+ } else {
+ setLoading(false);
+ setError('Please log in to manage systems.');
}
}, [token]);
const handleAddSystem = async (): Promise => {
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 = ({ 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 = ({ 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 = ({ token }) => {
const handleDeleteSystem = async (id: string): Promise => {
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 = ({ 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 Loading systems...
;
- if (error && !loading) return {error}
;
+ // Only show loading if actively fetching or waiting for token
+ if (loading && token) return Loading systems...
;
+ // Show error if there's an error OR if no token and not loading
+ if (error) return {error}
;
+ // If not loading and no token, it implies no action can be taken or a login is needed
+ if (!loading && !token) return Please log in to view and manage systems.
;
+
return (
@@ -188,7 +218,7 @@ const SystemsManagement: React.FC = ({ token }) => {
avail_on_nodes: '', description: '', tags: '[]', whitelist: '',
});
setIsAddSystemDialogOpen(true);
- }} className="mb-4">Add New System
+ }} className="mb-4" disabled={!token}>Add New System
@@ -226,10 +256,12 @@ const SystemsManagement: React.FC = ({ token }) => {
whitelist: system.whitelist ? system.whitelist.join(', ') : '',
});
setIsAddSystemDialogOpen(true);
- }}>
+ }}
+ disabled={!token}
+ >
Edit
- handleDeleteSystem(system._id)}>
+ handleDeleteSystem(system._id)} disabled={!token}>
Delete
@@ -343,7 +375,7 @@ const SystemsManagement: React.FC = ({ token }) => {
- {editingSystem ? 'Save Changes' : 'Add System'}
+ {editingSystem ? 'Save Changes' : 'Add System'}
diff --git a/src/context/AuthContext.tsx b/src/context/AuthContext.tsx
index 862d428..58525ea 100644
--- a/src/context/AuthContext.tsx
+++ b/src/context/AuthContext.tsx
@@ -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(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;
+ logout: () => void;
+ hasPermission: (requiredRole: UserRoles) => boolean;
+}
+
+const AuthContext = createContext(undefined);
interface AuthProviderProps {
children: ReactNode;
}
export const AuthProvider: React.FC = ({ children }) => {
+ const [user, setUser] = useState(null);
const [token, setToken] = useState(null);
- const [user, setUser] = useState(null);
- const [loading, setLoading] = useState(true);
+ const [loading, setLoading] = useState(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 => {
+ 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.USER]: 0,
- [UserRoles.MOD]: 1,
- [UserRoles.ADMIN]: 2
- };
- return roleOrder[user.role] >= roleOrder[requiredRole];
- };
return (
-
+
{children}
);
};
-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;
};
\ No newline at end of file
diff --git a/src/utils/AuthAwareFetch.ts b/src/utils/AuthAwareFetch.ts
new file mode 100644
index 0000000..bf6544f
--- /dev/null
+++ b/src/utils/AuthAwareFetch.ts
@@ -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 => {
+ 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;
+};
\ No newline at end of file