Working UI with auth
This commit is contained in:
@@ -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>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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)}>
|
||||||
|
|||||||
@@ -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.'); //
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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;
|
||||||
};
|
};
|
||||||
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