SPB Git forge

spb/fetcha

Public
11commits 1branches 0releases
1.5 MBsize
maindefault branch
16 days agolast push
TypeScript 97.5% SQL 1.4% Python 0.8%
4.1 KB · 95 lines tsx
Raw Blame History
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