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%
5.3 KB · 115 lines tsx
Raw Blame History
1"use client";2import * as React from "react";3import { useRouter } from "next/navigation";4import { Check, Plus, Undo2 } from "lucide-react";5import { recordAbuseEvent, resolveAbuseEvent } from "@/actions/admin";6import { Button } from "@/components/ui/button";7import { Input, NativeSelect, Textarea } from "@/components/ui/input";8import { Field, Hint, Label } from "@/components/ui/label";9import { Dialog, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogTitle } from "@/components/ui/dialog";10import { ActionButton } from "./action-button";1112export function ResolveAbuseButton({ id, resolved }: { id: string; resolved: boolean }) {13  return resolved ? (14    <ActionButton variant="ghost" size="xs" action={() => resolveAbuseEvent(id, false)}>15      <Undo2 className="size-3" /> Reopen16    </ActionButton>17  ) : (18    <ActionButton variant="outline" size="xs" action={() => resolveAbuseEvent(id, true)}>19      <Check className="size-3" /> Resolve20    </ActionButton>21  );22}2324export function RecordAbuseDialog({ defaultOrgId }: { defaultOrgId?: string }) {25  const router = useRouter();26  const [open, setOpen] = React.useState(false);27  const [pending, start] = React.useTransition();28  const [error, setError] = React.useState<string | null>(null);29  const [form, setForm] = React.useState({ kind: "manual", severity: "medium", organizationId: defaultOrgId ?? "", projectId: "", requestId: "", detail: "" });30  const set = (k: keyof typeof form) => (e: React.ChangeEvent<HTMLInputElement | HTMLSelectElement | HTMLTextAreaElement>) => setForm((f) => ({ ...f, [k]: e.target.value }));31  return (32    <>33      <Button variant="primary" size="sm" onClick={() => setOpen(true)}>34        <Plus className="size-3.5" /> Record abuse event35      </Button>36      <Dialog open={open} onOpenChange={setOpen}>37        <DialogContent size="md">38          <DialogHeader>39            <DialogTitle>Record an abuse event</DialogTitle>40            <DialogDescription>Manual entries appear alongside automatically detected events and are linked to the organization when an id is given.</DialogDescription>41          </DialogHeader>42          <form43            className="grid gap-3"44            onSubmit={(e) => {45              e.preventDefault();46              start(async () => {47                setError(null);48                const r = await recordAbuseEvent(form as Parameters<typeof recordAbuseEvent>[0]);49                if (!r.ok) return setError(r.error);50                setOpen(false);51                setForm({ kind: "manual", severity: "medium", organizationId: defaultOrgId ?? "", projectId: "", requestId: "", detail: "" });52                router.refresh();53              });54            }}55          >56            <div className="grid gap-3 sm:grid-cols-2">57              <Field>58                <Label htmlFor="ab-kind">Kind</Label>59                <NativeSelect id="ab-kind" value={form.kind} onChange={set("kind")}>60                  <option value="manual">manual</option>61                  <option value="ssrf_attempt">ssrf_attempt</option>62                  <option value="prohibited_target">prohibited_target</option>63                  <option value="rate_abuse">rate_abuse</option>64                  <option value="credential_abuse">credential_abuse</option>65                </NativeSelect>66              </Field>67              <Field>68                <Label htmlFor="ab-sev">Severity</Label>69                <NativeSelect id="ab-sev" value={form.severity} onChange={set("severity")}>70                  <option value="low">low</option>71                  <option value="medium">medium</option>72                  <option value="high">high</option>73                  <option value="critical">critical</option>74                </NativeSelect>75              </Field>76            </div>77            <Field>78              <Label htmlFor="ab-org">Organization id</Label>79              <Input id="ab-org" value={form.organizationId} onChange={set("organizationId")} placeholder="org_…" className="font-mono" />80            </Field>81            <div className="grid gap-3 sm:grid-cols-2">82              <Field>83                <Label htmlFor="ab-proj">Project id</Label>84                <Input id="ab-proj" value={form.projectId} onChange={set("projectId")} placeholder="proj_… (optional)" className="font-mono" />85              </Field>86              <Field>87                <Label htmlFor="ab-req">Request id</Label>88                <Input id="ab-req" value={form.requestId} onChange={set("requestId")} placeholder="req_… (optional)" className="font-mono" />89              </Field>90            </div>91            <Field>92              <Label htmlFor="ab-detail">Detail</Label>93              <Textarea id="ab-detail" value={form.detail} onChange={set("detail")} placeholder="What happened, evidence, links to requests…" required />94              <Hint>Internal only.</Hint>95            </Field>96            {error ? (97              <p role="alert" className="text-[13px] text-danger">98                {error}99              </p>100            ) : null}101            <DialogFooter>102              <Button type="button" variant="outline" onClick={() => setOpen(false)}>103                Cancel104              </Button>105              <Button type="submit" variant="primary" loading={pending}>106                Record107              </Button>108            </DialogFooter>109          </form>110        </DialogContent>111      </Dialog>112    </>113  );114}115