TypeScript 97.5%
SQL 1.4%
Python 0.8%
1"use client";2import * as React from "react";3import { useRouter } from "next/navigation";4import { Ban, BadgeCheck, ShieldCheck, ShieldOff, Undo2 } from "lucide-react";5import { banUser, unbanUser, setUserRole, forceVerifyEmail } from "@/actions/admin";6import { Button } from "@/components/ui/button";7import { Textarea } from "@/components/ui/input";8import { Field, Label, Hint } from "@/components/ui/label";9import { Dialog, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogTitle } from "@/components/ui/dialog";10import { ActionButton } from "./action-button";1112export function UserActions({ userId, banned, role, emailVerified, isSelf }: { userId: string; banned: boolean; role: string; emailVerified: boolean; isSelf: boolean }) {13 const router = useRouter();14 const [banOpen, setBanOpen] = React.useState(false);15 const [reason, setReason] = React.useState("");16 const [error, setError] = React.useState<string | null>(null);17 const [pending, start] = React.useTransition();1819 const doBan = () =>20 start(async () => {21 setError(null);22 const r = await banUser(userId, reason);23 if (!r.ok) return setError(r.error);24 setBanOpen(false);25 setReason("");26 router.refresh();27 });2829 return (30 <div className="flex flex-wrap items-center gap-2">31 {banned ? (32 <ActionButton variant="outline" size="sm" action={() => unbanUser(userId)}>33 <Undo2 className="size-3.5" /> Unban34 </ActionButton>35 ) : (36 <Button variant="danger" size="sm" onClick={() => setBanOpen(true)} disabled={isSelf} title={isSelf ? "You cannot ban yourself" : undefined}>37 <Ban className="size-3.5" /> Ban user38 </Button>39 )}40 {role === "admin" ? (41 <ActionButton42 variant="outline"43 size="sm"44 disabled={isSelf}45 action={() => setUserRole(userId, "user")}46 confirm={{ title: "Remove admin role?", description: "This user will lose access to /admin immediately.", confirmLabel: "Demote", variant: "danger" }}47 >48 <ShieldOff className="size-3.5" /> Demote to user49 </ActionButton>50 ) : (51 <ActionButton52 variant="outline"53 size="sm"54 action={() => setUserRole(userId, "admin")}55 confirm={{ title: "Grant admin role?", description: "Admins can see upstream provider names, costs and every customer's data. Only for Fetcha staff.", confirmLabel: "Promote", variant: "primary" }}56 >57 <ShieldCheck className="size-3.5" /> Promote to admin58 </ActionButton>59 )}60 {!emailVerified ? (61 <ActionButton variant="outline" size="sm" action={() => forceVerifyEmail(userId)} confirm={{ title: "Mark email as verified?", description: "Unlocks production API access without the user clicking the verification link.", confirmLabel: "Verify", variant: "primary" }}>62 <BadgeCheck className="size-3.5" /> Force verify email63 </ActionButton>64 ) : null}6566 <Dialog open={banOpen} onOpenChange={setBanOpen}>67 <DialogContent size="sm">68 <DialogHeader>69 <DialogTitle>Ban this user</DialogTitle>70 <DialogDescription>All active sessions are revoked and the user cannot sign in until unbanned. The reason is stored on the account and in the audit log.</DialogDescription>71 </DialogHeader>72 <Field>73 <Label htmlFor="ban-reason">Reason</Label>74 <Textarea id="ban-reason" value={reason} onChange={(e) => setReason(e.target.value)} placeholder="e.g. SSRF attempts against internal ranges (abuse_…)" />75 <Hint>Internal only — never shown to the user.</Hint>76 </Field>77 {error ? (78 <p role="alert" className="text-[13px] text-danger">79 {error}80 </p>81 ) : null}82 <DialogFooter>83 <Button variant="outline" onClick={() => setBanOpen(false)}>84 Cancel85 </Button>86 <Button variant="danger" loading={pending} onClick={doBan}>87 Ban user88 </Button>89 </DialogFooter>90 </DialogContent>91 </Dialog>92 </div>93 );94}95