Working UI with auth

This commit is contained in:
Logan Cusano
2025-05-26 02:43:35 -04:00
parent 7e55c120b1
commit f1d8012798
7 changed files with 426 additions and 188 deletions

View File

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