214 lines
6.5 KiB
TypeScript
214 lines
6.5 KiB
TypeScript
|
|
"use client";
|
||
|
|
|
||
|
|
import { useState } from "react";
|
||
|
|
import { useRouter } from "next/navigation";
|
||
|
|
import { MoreHorizontal, Pencil, Ban, Trash2, Key, UserX, UserCheck } from "lucide-react";
|
||
|
|
import { toast } from "sonner";
|
||
|
|
import {
|
||
|
|
DropdownMenu,
|
||
|
|
DropdownMenuContent,
|
||
|
|
DropdownMenuItem,
|
||
|
|
DropdownMenuLabel,
|
||
|
|
DropdownMenuSeparator,
|
||
|
|
DropdownMenuTrigger,
|
||
|
|
} from "@/components/ui/dropdown-menu";
|
||
|
|
import {
|
||
|
|
AlertDialog,
|
||
|
|
AlertDialogAction,
|
||
|
|
AlertDialogCancel,
|
||
|
|
AlertDialogContent,
|
||
|
|
AlertDialogDescription,
|
||
|
|
AlertDialogFooter,
|
||
|
|
AlertDialogHeader,
|
||
|
|
AlertDialogTitle,
|
||
|
|
} from "@/components/ui/alert-dialog";
|
||
|
|
import { Button } from "@/components/ui/button";
|
||
|
|
|
||
|
|
interface User {
|
||
|
|
id: string;
|
||
|
|
name: string;
|
||
|
|
email: string;
|
||
|
|
role: string;
|
||
|
|
banned: boolean;
|
||
|
|
}
|
||
|
|
|
||
|
|
interface UserActionsProps {
|
||
|
|
user: User;
|
||
|
|
currentUserId?: string;
|
||
|
|
onRefresh?: () => void;
|
||
|
|
}
|
||
|
|
|
||
|
|
export function UserActions({ user, currentUserId, onRefresh }: UserActionsProps) {
|
||
|
|
const router = useRouter();
|
||
|
|
const [showDeleteDialog, setShowDeleteDialog] = useState(false);
|
||
|
|
const [showBanDialog, setShowBanDialog] = useState(false);
|
||
|
|
const [isLoading, setIsLoading] = useState(false);
|
||
|
|
|
||
|
|
const isCurrentUser = user.id === currentUserId;
|
||
|
|
|
||
|
|
async function handleBanToggle() {
|
||
|
|
setIsLoading(true);
|
||
|
|
try {
|
||
|
|
const response = await fetch(`/api/admin/users/${user.id}`, {
|
||
|
|
method: "PATCH",
|
||
|
|
headers: { "Content-Type": "application/json" },
|
||
|
|
body: JSON.stringify({ banned: !user.banned }),
|
||
|
|
});
|
||
|
|
|
||
|
|
if (!response.ok) {
|
||
|
|
const data = await response.json();
|
||
|
|
throw new Error(data.error || "Failed to update user");
|
||
|
|
}
|
||
|
|
|
||
|
|
toast.success(user.banned ? "User unbanned" : "User banned");
|
||
|
|
onRefresh?.();
|
||
|
|
} catch (error) {
|
||
|
|
toast.error(error instanceof Error ? error.message : "An error occurred");
|
||
|
|
} finally {
|
||
|
|
setIsLoading(false);
|
||
|
|
setShowBanDialog(false);
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
async function handleDelete() {
|
||
|
|
setIsLoading(true);
|
||
|
|
try {
|
||
|
|
const response = await fetch(`/api/admin/users/${user.id}`, {
|
||
|
|
method: "DELETE",
|
||
|
|
});
|
||
|
|
|
||
|
|
if (!response.ok) {
|
||
|
|
const data = await response.json();
|
||
|
|
throw new Error(data.error || "Failed to delete user");
|
||
|
|
}
|
||
|
|
|
||
|
|
toast.success("User deleted");
|
||
|
|
onRefresh?.();
|
||
|
|
} catch (error) {
|
||
|
|
toast.error(error instanceof Error ? error.message : "An error occurred");
|
||
|
|
} finally {
|
||
|
|
setIsLoading(false);
|
||
|
|
setShowDeleteDialog(false);
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
async function handleRevokeSessions() {
|
||
|
|
setIsLoading(true);
|
||
|
|
try {
|
||
|
|
const response = await fetch(`/api/admin/users/${user.id}/sessions`, {
|
||
|
|
method: "DELETE",
|
||
|
|
});
|
||
|
|
|
||
|
|
if (!response.ok) {
|
||
|
|
const data = await response.json();
|
||
|
|
throw new Error(data.error || "Failed to revoke sessions");
|
||
|
|
}
|
||
|
|
|
||
|
|
toast.success("All sessions revoked");
|
||
|
|
onRefresh?.();
|
||
|
|
} catch (error) {
|
||
|
|
toast.error(error instanceof Error ? error.message : "An error occurred");
|
||
|
|
} finally {
|
||
|
|
setIsLoading(false);
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
return (
|
||
|
|
<>
|
||
|
|
<DropdownMenu>
|
||
|
|
<DropdownMenuTrigger asChild>
|
||
|
|
<Button variant="ghost" className="h-8 w-8 p-0">
|
||
|
|
<span className="sr-only">Open menu</span>
|
||
|
|
<MoreHorizontal className="h-4 w-4" />
|
||
|
|
</Button>
|
||
|
|
</DropdownMenuTrigger>
|
||
|
|
<DropdownMenuContent align="end">
|
||
|
|
<DropdownMenuLabel>Actions</DropdownMenuLabel>
|
||
|
|
<DropdownMenuItem onClick={() => router.push(`/admin/users/${user.id}`)}>
|
||
|
|
<Pencil className="mr-2 h-4 w-4" />
|
||
|
|
Edit
|
||
|
|
</DropdownMenuItem>
|
||
|
|
<DropdownMenuItem onClick={handleRevokeSessions} disabled={isLoading}>
|
||
|
|
<Key className="mr-2 h-4 w-4" />
|
||
|
|
Revoke Sessions
|
||
|
|
</DropdownMenuItem>
|
||
|
|
<DropdownMenuSeparator />
|
||
|
|
{!isCurrentUser && (
|
||
|
|
<>
|
||
|
|
<DropdownMenuItem
|
||
|
|
onClick={() => setShowBanDialog(true)}
|
||
|
|
disabled={isLoading}
|
||
|
|
>
|
||
|
|
{user.banned ? (
|
||
|
|
<>
|
||
|
|
<UserCheck className="mr-2 h-4 w-4" />
|
||
|
|
Unban User
|
||
|
|
</>
|
||
|
|
) : (
|
||
|
|
<>
|
||
|
|
<Ban className="mr-2 h-4 w-4" />
|
||
|
|
Ban User
|
||
|
|
</>
|
||
|
|
)}
|
||
|
|
</DropdownMenuItem>
|
||
|
|
<DropdownMenuItem
|
||
|
|
onClick={() => setShowDeleteDialog(true)}
|
||
|
|
disabled={isLoading}
|
||
|
|
className="text-destructive focus:text-destructive"
|
||
|
|
>
|
||
|
|
<Trash2 className="mr-2 h-4 w-4" />
|
||
|
|
Delete User
|
||
|
|
</DropdownMenuItem>
|
||
|
|
</>
|
||
|
|
)}
|
||
|
|
</DropdownMenuContent>
|
||
|
|
</DropdownMenu>
|
||
|
|
|
||
|
|
{/* Ban Confirmation Dialog */}
|
||
|
|
<AlertDialog open={showBanDialog} onOpenChange={setShowBanDialog}>
|
||
|
|
<AlertDialogContent>
|
||
|
|
<AlertDialogHeader>
|
||
|
|
<AlertDialogTitle>
|
||
|
|
{user.banned ? "Unban User" : "Ban User"}
|
||
|
|
</AlertDialogTitle>
|
||
|
|
<AlertDialogDescription>
|
||
|
|
{user.banned
|
||
|
|
? `Are you sure you want to unban ${user.name}? They will be able to sign in again.`
|
||
|
|
: `Are you sure you want to ban ${user.name}? They will be signed out and unable to sign in.`}
|
||
|
|
</AlertDialogDescription>
|
||
|
|
</AlertDialogHeader>
|
||
|
|
<AlertDialogFooter>
|
||
|
|
<AlertDialogCancel disabled={isLoading}>Cancel</AlertDialogCancel>
|
||
|
|
<AlertDialogAction onClick={handleBanToggle} disabled={isLoading}>
|
||
|
|
{user.banned ? "Unban" : "Ban"}
|
||
|
|
</AlertDialogAction>
|
||
|
|
</AlertDialogFooter>
|
||
|
|
</AlertDialogContent>
|
||
|
|
</AlertDialog>
|
||
|
|
|
||
|
|
{/* Delete Confirmation Dialog */}
|
||
|
|
<AlertDialog open={showDeleteDialog} onOpenChange={setShowDeleteDialog}>
|
||
|
|
<AlertDialogContent>
|
||
|
|
<AlertDialogHeader>
|
||
|
|
<AlertDialogTitle>Delete User</AlertDialogTitle>
|
||
|
|
<AlertDialogDescription>
|
||
|
|
Are you sure you want to permanently delete {user.name}? This action
|
||
|
|
cannot be undone.
|
||
|
|
</AlertDialogDescription>
|
||
|
|
</AlertDialogHeader>
|
||
|
|
<AlertDialogFooter>
|
||
|
|
<AlertDialogCancel disabled={isLoading}>Cancel</AlertDialogCancel>
|
||
|
|
<AlertDialogAction
|
||
|
|
onClick={handleDelete}
|
||
|
|
disabled={isLoading}
|
||
|
|
className="bg-destructive text-destructive-foreground hover:bg-destructive/90"
|
||
|
|
>
|
||
|
|
Delete
|
||
|
|
</AlertDialogAction>
|
||
|
|
</AlertDialogFooter>
|
||
|
|
</AlertDialogContent>
|
||
|
|
</AlertDialog>
|
||
|
|
</>
|
||
|
|
);
|
||
|
|
}
|