diff --git a/demo/nextjs/app/admin/page.tsx b/demo/nextjs/app/admin/page.tsx new file mode 100644 index 00000000..d8b645df --- /dev/null +++ b/demo/nextjs/app/admin/page.tsx @@ -0,0 +1,460 @@ +"use client"; + +import { useState } from "react"; +import { Button } from "@/components/ui/button"; +import { Input } from "@/components/ui/input"; +import { Label } from "@/components/ui/label"; +import { + Select, + SelectContent, + SelectItem, + SelectTrigger, + SelectValue, +} from "@/components/ui/select"; +import { + Table, + TableBody, + TableCell, + TableHead, + TableHeader, + TableRow, +} from "@/components/ui/table"; +import { toast, Toaster } from "sonner"; +import { client } from "@/lib/auth-client"; +import { useQuery, useQueryClient } from "@tanstack/react-query"; +import { useRouter } from "next/navigation"; +import { + Loader2, + Plus, + Trash, + RefreshCw, + UserCircle, + Calendar as CalendarIcon, +} from "lucide-react"; +import { + Dialog, + DialogContent, + DialogHeader, + DialogTitle, + DialogTrigger, +} from "@/components/ui/dialog"; +import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"; +import { Calendar } from "@/components/ui/calendar"; +import { + Popover, + PopoverContent, + PopoverTrigger, +} from "@/components/ui/popover"; +import { format } from "date-fns"; +import { cn } from "@/lib/utils"; +import { Badge } from "@/components/ui/badge"; + +type User = { + id: string; + email: string; + name: string; + role: "admin" | "user"; +}; + +export default function AdminDashboard() { + const queryClient = useQueryClient(); + const router = useRouter(); + const [isDialogOpen, setIsDialogOpen] = useState(false); + const [newUser, setNewUser] = useState({ + email: "", + password: "", + name: "", + role: "user" as const, + }); + const [isLoading, setIsLoading] = useState(); + const [isBanDialogOpen, setIsBanDialogOpen] = useState(false); + const [banForm, setBanForm] = useState({ + userId: "", + reason: "", + expirationDate: undefined as Date | undefined, + }); + + const { data: users, isLoading: isUsersLoading } = useQuery({ + queryKey: ["users"], + queryFn: () => + client.admin + .listUsers({ + query: { + limit: 10, + sortBy: "createdAt", + sortDirection: "desc", + }, + }) + .then((res) => res.data?.users ?? []), + }); + + const handleCreateUser = async (e: React.FormEvent) => { + e.preventDefault(); + setIsLoading("create"); + try { + await client.admin.createUser({ + email: newUser.email, + password: newUser.password, + name: newUser.name, + role: newUser.role, + }); + toast.success("User created successfully"); + setNewUser({ email: "", password: "", name: "", role: "user" }); + setIsDialogOpen(false); + queryClient.invalidateQueries({ + queryKey: ["users"], + }); + } catch (error: any) { + toast.error(error.message || "Failed to create user"); + } finally { + setIsLoading(undefined); + } + }; + + const handleDeleteUser = async (id: string) => { + setIsLoading(`delete-${id}`); + try { + await client.admin.removeUser({ userId: id }); + toast.success("User deleted successfully"); + queryClient.invalidateQueries({ + queryKey: ["users"], + }); + } catch (error: any) { + toast.error(error.message || "Failed to delete user"); + } finally { + setIsLoading(undefined); + } + }; + + const handleRevokeSessions = async (id: string) => { + setIsLoading(`revoke-${id}`); + try { + await client.admin.revokeUserSessions({ userId: id }); + toast.success("Sessions revoked for user"); + } catch (error: any) { + toast.error(error.message || "Failed to revoke sessions"); + } finally { + setIsLoading(undefined); + } + }; + + const handleImpersonateUser = async (id: string) => { + setIsLoading(`impersonate-${id}`); + try { + await client.admin.impersonateUser({ userId: id }); + toast.success("Impersonated user"); + router.push("/dashboard"); + } catch (error: any) { + toast.error(error.message || "Failed to impersonate user"); + } finally { + setIsLoading(undefined); + } + }; + + const handleBanUser = async (e: React.FormEvent) => { + e.preventDefault(); + setIsLoading(`ban-${banForm.userId}`); + try { + if (!banForm.expirationDate) { + throw new Error("Expiration date is required"); + } + await client.admin.banUser({ + userId: banForm.userId, + banReason: banForm.reason, + banExpiresIn: banForm.expirationDate.getTime() - new Date().getTime(), + }); + toast.success("User banned successfully"); + setIsBanDialogOpen(false); + queryClient.invalidateQueries({ + queryKey: ["users"], + }); + } catch (error: any) { + toast.error(error.message || "Failed to ban user"); + } finally { + setIsLoading(undefined); + } + }; + + return ( +
+ + + + Admin Dashboard + + + + + + + Create New User + +
+
+ + + setNewUser({ ...newUser, email: e.target.value }) + } + required + /> +
+
+ + + setNewUser({ ...newUser, password: e.target.value }) + } + required + /> +
+
+ + + setNewUser({ ...newUser, name: e.target.value }) + } + required + /> +
+
+ + +
+ +
+
+
+ + + + Ban User + +
+
+ + + setBanForm({ ...banForm, reason: e.target.value }) + } + required + /> +
+
+ + + + + + + + setBanForm({ ...banForm, expirationDate: date }) + } + initialFocus + /> + + +
+ +
+
+
+
+ + {isUsersLoading ? ( +
+ +
+ ) : ( + + + + Email + Name + Role + Banned + Actions + + + + {users?.map((user) => ( + + {user.email} + {user.name} + {user.role || "user"} + + {user.banned ? ( + Yes + ) : ( + No + )} + + +
+ + + + +
+
+
+ ))} +
+
+ )} +
+
+
+ ); +} diff --git a/demo/nextjs/app/dashboard/user-card.tsx b/demo/nextjs/app/dashboard/user-card.tsx index 0cb681f3..2e2d7d9c 100644 --- a/demo/nextjs/app/dashboard/user-card.tsx +++ b/demo/nextjs/app/dashboard/user-card.tsx @@ -216,7 +216,7 @@ export default function UserCard(props: {

Two Factor

- {session?.user.twoFactorEnabled && ( + {!!session?.user.twoFactorEnabled && (