mirror of
https://gitea.tendokyu.moe/pinapelz/Mirage.git
synced 2026-10-03 04:08:02 +03:00
admin: implement user deletion
This commit is contained in:
@@ -66,6 +66,7 @@ app.get('/api/scores/:chartId', requireAuth, scoreRoutes.handleGetScoresByChartI
|
||||
app.get('/api/allScores', requireAuth, scoreRoutes.handleGetAllGameScores);
|
||||
|
||||
app.post('/api/admin/createGame', requireAuth, adminRoutes.handleCreateGame);
|
||||
app.delete('/api/admin/user/:userId', requireAuth, adminRoutes.handleDeleteUser);
|
||||
|
||||
app.listen(port, () => {
|
||||
console.log(`Server listening on port ${port}`);
|
||||
|
||||
@@ -48,3 +48,63 @@ export const handleCreateGame = async (req: express.Request, res: express.Respon
|
||||
res.status(500).json({ error: 'Internal server error' });
|
||||
}
|
||||
}
|
||||
|
||||
export const handleDeleteUser = async (req: express.Request, res: express.Response) => {
|
||||
try {
|
||||
if (!req.session.userId) {
|
||||
return res.status(401).json({ error: 'Authentication required' });
|
||||
}
|
||||
|
||||
const user = await prisma.user.findUnique({
|
||||
where: { id: req.session.userId },
|
||||
select: { id: true, username: true, isAdmin: true }
|
||||
});
|
||||
|
||||
if (!user) {
|
||||
req.session.destroy((err) => {
|
||||
if (err) console.error('Session destroy error:', err);
|
||||
});
|
||||
return res.status(401).json({ error: 'Invalid session' });
|
||||
}
|
||||
|
||||
if (user.id !== 1 && !user.isAdmin) {
|
||||
return res.status(403).json({ error: 'Unauthorized. You are not an admin of this instance' });
|
||||
}
|
||||
|
||||
const { userId } = req.params;
|
||||
if (!userId) {
|
||||
return res.status(400).json({ error: 'User ID is required' });
|
||||
}
|
||||
|
||||
const targetUserId = parseInt(userId);
|
||||
if (isNaN(targetUserId) || targetUserId <= 0) {
|
||||
return res.status(400).json({ error: 'Invalid user ID' });
|
||||
}
|
||||
if (targetUserId === user.id) {
|
||||
return res.status(400).json({ error: 'Cannot delete your own account' });
|
||||
}
|
||||
const targetUser = await prisma.user.findUnique({
|
||||
where: { id: targetUserId },
|
||||
select: { id: true, username: true, isAdmin: true }
|
||||
});
|
||||
|
||||
if (!targetUser) {
|
||||
return res.status(404).json({ error: 'User not found' });
|
||||
}
|
||||
await prisma.user.delete({
|
||||
where: { id: targetUserId }
|
||||
});
|
||||
|
||||
return res.status(200).json({
|
||||
message: 'User deleted successfully',
|
||||
deletedUser: {
|
||||
id: targetUser.id,
|
||||
username: targetUser.username
|
||||
}
|
||||
});
|
||||
|
||||
} catch (error) {
|
||||
console.error('User deletion error:', error);
|
||||
res.status(500).json({ error: 'Internal server error' });
|
||||
}
|
||||
}
|
||||
|
||||
+2
-2
@@ -1,4 +1,4 @@
|
||||
import { ReactNode } from 'react';
|
||||
import type {ReactNode} from 'react';
|
||||
|
||||
interface CollapsibleSectionProps {
|
||||
title: string;
|
||||
@@ -37,4 +37,4 @@ const CollapsibleSection = ({ title, isOpen, onToggle, children }: CollapsibleSe
|
||||
);
|
||||
};
|
||||
|
||||
export default CollapsibleSection;
|
||||
export default CollapsibleSection;
|
||||
@@ -0,0 +1,218 @@
|
||||
import { useState } from 'react';
|
||||
|
||||
interface User {
|
||||
id: number;
|
||||
username: string;
|
||||
isAdmin: boolean;
|
||||
}
|
||||
|
||||
interface UserDeletionProps {
|
||||
onUserDeleted?: () => void;
|
||||
}
|
||||
|
||||
const UserDeletion = ({ onUserDeleted }: UserDeletionProps) => {
|
||||
const [userId, setUserId] = useState('');
|
||||
const [userToDelete, setUserToDelete] = useState<User | null>(null);
|
||||
const [isSearching, setIsSearching] = useState(false);
|
||||
const [isDeleting, setIsDeleting] = useState(false);
|
||||
const [showConfirmation, setShowConfirmation] = useState(false);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
|
||||
const handleSearchUser = async () => {
|
||||
if (!userId.trim()) {
|
||||
setError('Please enter a user ID');
|
||||
return;
|
||||
}
|
||||
|
||||
const id = parseInt(userId);
|
||||
if (isNaN(id) || id <= 0) {
|
||||
setError('Please enter a valid user ID');
|
||||
return;
|
||||
}
|
||||
|
||||
setIsSearching(true);
|
||||
setError(null);
|
||||
setUserToDelete(null);
|
||||
|
||||
try {
|
||||
const response = await fetch(`${import.meta.env.VITE_API_URL}/me?userId=${id}`, {
|
||||
method: 'GET',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
credentials: 'include',
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
if (response.status === 404) {
|
||||
throw new Error('User not found');
|
||||
}
|
||||
const error = await response.json();
|
||||
throw new Error(error.error || 'Failed to find user');
|
||||
}
|
||||
|
||||
const data = await response.json();
|
||||
setUserToDelete(data.user);
|
||||
} catch (error) {
|
||||
console.error('Failed to search user:', error);
|
||||
setError(error instanceof Error ? error.message : 'Failed to search user');
|
||||
} finally {
|
||||
setIsSearching(false);
|
||||
}
|
||||
};
|
||||
|
||||
const handleDeleteUser = async () => {
|
||||
if (!userToDelete) return;
|
||||
|
||||
setIsDeleting(true);
|
||||
setError(null);
|
||||
|
||||
try {
|
||||
const response = await fetch(`${import.meta.env.VITE_API_URL}/admin/user/${userToDelete.id}`, {
|
||||
method: 'DELETE',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
credentials: 'include',
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
const error = await response.json();
|
||||
throw new Error(error.error || 'Failed to delete user');
|
||||
}
|
||||
|
||||
alert(`User "${userToDelete.username}" has been successfully deleted.`);
|
||||
|
||||
// Reset form
|
||||
setUserId('');
|
||||
setUserToDelete(null);
|
||||
setShowConfirmation(false);
|
||||
|
||||
// Call callback if provided
|
||||
onUserDeleted?.();
|
||||
|
||||
} catch (error) {
|
||||
console.error('Failed to delete user:', error);
|
||||
setError(error instanceof Error ? error.message : 'Failed to delete user');
|
||||
} finally {
|
||||
setIsDeleting(false);
|
||||
}
|
||||
};
|
||||
|
||||
const resetForm = () => {
|
||||
setUserId('');
|
||||
setUserToDelete(null);
|
||||
setShowConfirmation(false);
|
||||
setError(null);
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<p className="text-slate-300 leading-relaxed mb-6 p-4 bg-slate-800/50 rounded-md border-l-4 border-red-500">
|
||||
<strong className="text-red-400">Warning:</strong> This action is irreversible. Deleting a user will permanently remove their account and all associated data including scores and statistics.
|
||||
</p>
|
||||
|
||||
{error && (
|
||||
<div className="mb-6 p-4 bg-red-900/30 border border-red-700 rounded-md">
|
||||
<p className="text-red-400">{error}</p>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="space-y-6">
|
||||
{/* Search User Section */}
|
||||
<div className="space-y-4">
|
||||
<div>
|
||||
<label htmlFor="userId" className="block text-sm font-medium text-slate-300 mb-2">
|
||||
User ID
|
||||
</label>
|
||||
<div className="flex gap-3">
|
||||
<input
|
||||
type="number"
|
||||
id="userId"
|
||||
value={userId}
|
||||
onChange={(e) => setUserId(e.target.value)}
|
||||
className="flex-1 px-3 py-2 bg-slate-800 border border-slate-600 rounded-md text-white placeholder-slate-400 focus:outline-none focus:ring-2 focus:ring-violet-500 focus:border-transparent"
|
||||
placeholder="Enter user ID to search"
|
||||
min="1"
|
||||
disabled={isSearching}
|
||||
/>
|
||||
<button
|
||||
onClick={handleSearchUser}
|
||||
disabled={isSearching || !userId.trim()}
|
||||
className="bg-violet-600 hover:bg-violet-700 disabled:bg-violet-800 disabled:cursor-not-allowed text-white font-medium px-4 py-2 rounded-md transition-colors"
|
||||
>
|
||||
{isSearching ? 'Searching...' : 'Search'}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* User Details Section */}
|
||||
{userToDelete && !showConfirmation && (
|
||||
<div className="p-4 bg-slate-800 border border-slate-600 rounded-md">
|
||||
<h3 className="text-lg font-semibold text-white mb-3">User Found</h3>
|
||||
<div className="space-y-2 text-sm">
|
||||
<p className="text-slate-300">
|
||||
<span className="text-slate-400">ID:</span> {userToDelete.id}
|
||||
</p>
|
||||
<p className="text-slate-300">
|
||||
<span className="text-slate-400">Username:</span> {userToDelete.username}
|
||||
</p>
|
||||
<p className="text-slate-300">
|
||||
<span className="text-slate-400">Admin Status:</span>{' '}
|
||||
<span className={userToDelete.isAdmin ? 'text-yellow-400' : 'text-green-400'}>
|
||||
{userToDelete.isAdmin ? 'Admin' : 'Regular User'}
|
||||
</span>
|
||||
</p>
|
||||
</div>
|
||||
<div className="flex gap-3 mt-4">
|
||||
<button
|
||||
onClick={() => setShowConfirmation(true)}
|
||||
className="bg-red-600 hover:bg-red-700 text-white font-medium px-4 py-2 rounded-md transition-colors"
|
||||
>
|
||||
Delete User
|
||||
</button>
|
||||
<button
|
||||
onClick={resetForm}
|
||||
className="bg-slate-600 hover:bg-slate-700 text-white font-medium px-4 py-2 rounded-md transition-colors"
|
||||
>
|
||||
Cancel
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Confirmation Section */}
|
||||
{showConfirmation && userToDelete && (
|
||||
<div className="p-4 bg-red-900/20 border border-red-600 rounded-md">
|
||||
<h3 className="text-lg font-semibold text-red-400 mb-3">Confirm Deletion</h3>
|
||||
<p className="text-slate-300 mb-4">
|
||||
Are you absolutely sure you want to delete the user <strong className="text-white">"{userToDelete.username}"</strong>?
|
||||
</p>
|
||||
<p className="text-sm text-red-300 mb-4">
|
||||
This action cannot be undone. All user data, scores, and statistics will be permanently deleted.
|
||||
</p>
|
||||
<div className="flex gap-3">
|
||||
<button
|
||||
onClick={handleDeleteUser}
|
||||
disabled={isDeleting}
|
||||
className="bg-red-600 hover:bg-red-700 disabled:bg-red-800 disabled:cursor-not-allowed text-white font-medium px-4 py-2 rounded-md transition-colors"
|
||||
>
|
||||
{isDeleting ? 'Deleting...' : 'Yes, Delete User'}
|
||||
</button>
|
||||
<button
|
||||
onClick={() => setShowConfirmation(false)}
|
||||
disabled={isDeleting}
|
||||
className="bg-slate-600 hover:bg-slate-700 disabled:bg-slate-700 disabled:cursor-not-allowed text-white font-medium px-4 py-2 rounded-md transition-colors"
|
||||
>
|
||||
Cancel
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default UserDeletion;
|
||||
@@ -3,9 +3,10 @@ import { NavBar } from "../components/NavBar";
|
||||
import { useAuth } from "../contexts/AuthContext";
|
||||
import SessionExpiredPopup from "../components/SessionExpiredPopup";
|
||||
import UnauthorizedAccess from "../components/UnauthorizedAccess";
|
||||
import CollapsibleSection from "../components/CollapsibleSection";
|
||||
import InviteCodeManager from "../components/InviteCodeManager";
|
||||
import GameManager from "../components/GameManager";
|
||||
import CollapsibleSection from "../components/admin/CollapsibleSection";
|
||||
import InviteCodeManager from "../components/admin/InviteCodeManager";
|
||||
import GameManager from "../components/admin/GameManager";
|
||||
import UserDeletion from "../components/admin/UserDeletion";
|
||||
import { useState } from "react";
|
||||
|
||||
interface GameFormData {
|
||||
@@ -23,6 +24,7 @@ const Admin = () => {
|
||||
const { user, isLoading, logout } = useAuth();
|
||||
const [showAddGame, setShowAddGame] = useState(false);
|
||||
const [showCreateInvite, setShowCreateInvite] = useState(false);
|
||||
const [showUserDeletion, setShowUserDeletion] = useState(false);
|
||||
const [isSubmitting, setIsSubmitting] = useState(false);
|
||||
const [isCreatingInvite, setIsCreatingInvite] = useState(false);
|
||||
const [createdInviteCode, setCreatedInviteCode] = useState<string | null>(null);
|
||||
@@ -107,6 +109,11 @@ const Admin = () => {
|
||||
}
|
||||
};
|
||||
|
||||
const handleUserDeleted = () => {
|
||||
// Optional: Add any additional logic after user deletion
|
||||
console.log('User deleted successfully');
|
||||
};
|
||||
|
||||
if (isLoading) {
|
||||
return <div className="min-h-screen bg-slate-950 flex items-center justify-center">
|
||||
<div className="text-center">
|
||||
@@ -161,6 +168,17 @@ const Admin = () => {
|
||||
isSubmitting={isSubmitting}
|
||||
/>
|
||||
</CollapsibleSection>
|
||||
|
||||
{/* User Deletion Section */}
|
||||
<CollapsibleSection
|
||||
title="Delete User"
|
||||
isOpen={showUserDeletion}
|
||||
onToggle={() => setShowUserDeletion(!showUserDeletion)}
|
||||
>
|
||||
<UserDeletion
|
||||
onUserDeleted={handleUserDeleted}
|
||||
/>
|
||||
</CollapsibleSection>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
Reference in New Issue
Block a user