بلاک‌ها · پنل مدیریت

جدولِ کلیدها با پیشوندِ ماسک‌شده، دسترسی و آخرین استفاده؛ ساختِ کلید با نام و دسترسی‌ها، نمایشِ یک‌بارهٔ کلید با کپی و هشدار، و باطل‌کردن با تأییدِ دوباره.

۱. نصب کامپوننت‌ها

این بلاک از ۱۳ آیتم دیگ استفاده می‌کند: alert-dialogbadgebuttoncheckboxcontrol-sizedialogdropdown-menuiconslabelpersiantabletext-fieldtoast

پکیج‌منیجر پیش‌فرض Node.jsنصب سریع با لینک سخت و دیسک مشترکYarn نسخهٔ ۲ به بالا (Berry)رانتایم و پکیج‌منیجر Bun
npx digdesign@latest add https://docs.digdesign.ir/r/alert-dialog.json https://docs.digdesign.ir/r/badge.json https://docs.digdesign.ir/r/button.json https://docs.digdesign.ir/r/checkbox.json https://docs.digdesign.ir/r/control-size.json https://docs.digdesign.ir/r/dialog.json https://docs.digdesign.ir/r/dropdown-menu.json https://docs.digdesign.ir/r/icons.json https://docs.digdesign.ir/r/label.json https://docs.digdesign.ir/r/persian.json https://docs.digdesign.ir/r/table.json https://docs.digdesign.ir/r/text-field.json https://docs.digdesign.ir/r/toast.json
pnpm dlx digdesign@latest add https://docs.digdesign.ir/r/alert-dialog.json https://docs.digdesign.ir/r/badge.json https://docs.digdesign.ir/r/button.json https://docs.digdesign.ir/r/checkbox.json https://docs.digdesign.ir/r/control-size.json https://docs.digdesign.ir/r/dialog.json https://docs.digdesign.ir/r/dropdown-menu.json https://docs.digdesign.ir/r/icons.json https://docs.digdesign.ir/r/label.json https://docs.digdesign.ir/r/persian.json https://docs.digdesign.ir/r/table.json https://docs.digdesign.ir/r/text-field.json https://docs.digdesign.ir/r/toast.json
yarn dlx digdesign@latest add https://docs.digdesign.ir/r/alert-dialog.json https://docs.digdesign.ir/r/badge.json https://docs.digdesign.ir/r/button.json https://docs.digdesign.ir/r/checkbox.json https://docs.digdesign.ir/r/control-size.json https://docs.digdesign.ir/r/dialog.json https://docs.digdesign.ir/r/dropdown-menu.json https://docs.digdesign.ir/r/icons.json https://docs.digdesign.ir/r/label.json https://docs.digdesign.ir/r/persian.json https://docs.digdesign.ir/r/table.json https://docs.digdesign.ir/r/text-field.json https://docs.digdesign.ir/r/toast.json
bunx --bun digdesign@latest add https://docs.digdesign.ir/r/alert-dialog.json https://docs.digdesign.ir/r/badge.json https://docs.digdesign.ir/r/button.json https://docs.digdesign.ir/r/checkbox.json https://docs.digdesign.ir/r/control-size.json https://docs.digdesign.ir/r/dialog.json https://docs.digdesign.ir/r/dropdown-menu.json https://docs.digdesign.ir/r/icons.json https://docs.digdesign.ir/r/label.json https://docs.digdesign.ir/r/persian.json https://docs.digdesign.ir/r/table.json https://docs.digdesign.ir/r/text-field.json https://docs.digdesign.ir/r/toast.json

۲. کد صفحه

کد را در app/<route>/page.tsx بگذارید و متن‌ها، داده و هندلرها را با محصول خودتان عوض کنید.

api-keys.tsx
"use client"

import * as React from "react"

import { AlertTriangleIcon, CheckIcon, CopyIcon, MoreHorizontalIcon, PlusIcon } from "@/components/icons"
import {
  AlertDialog,
  AlertDialogAction,
  AlertDialogCancel,
  AlertDialogContent,
  AlertDialogDescription,
  AlertDialogFooter,
  AlertDialogHeader,
  AlertDialogTitle,
} from "@/components/ui/alert-dialog"
import { Badge } from "@/components/ui/badge"
import { Button } from "@/components/ui/button"
import { Checkbox } from "@/components/ui/checkbox"
import { ControlSizeProvider } from "@/components/ui/control-size"
import { Dialog, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogTitle, DialogTrigger } from "@/components/ui/dialog"
import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuTrigger } from "@/components/ui/dropdown-menu"
import { Label } from "@/components/ui/label"
import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "@/components/ui/table"
import { TextField } from "@/components/ui/text-field"
import { toast } from "@/components/ui/toast"
import { formatJalali, formatRelativeTime, toFaDigits } from "@/lib/persian"

type Key = { id: string; name: string; prefix: string; scopes: string[]; created: string; used?: string }

const INITIAL: Key[] = [
  { id: "k1", name: "سرورِ فروشگاه", prefix: "dg_live_4f2a", scopes: ["خواندن", "نوشتن"], created: "2026-08-12", used: "2026-10-06T08:40:00" },
  { id: "k2", name: "گزارشِ داخلی", prefix: "dg_live_91cd", scopes: ["خواندن"], created: "2026-09-03", used: "2026-10-01T13:00:00" },
  { id: "k3", name: "تستِ اپ", prefix: "dg_test_7b10", scopes: ["خواندن", "نوشتن"], created: "2026-09-20" },
]
const SCOPES = [
  { id: "read", label: "خواندن", text: "دیدنِ فاکتورها، مشتریان و گزارش‌ها" },
  { id: "write", label: "نوشتن", text: "ساخت و ویرایشِ فاکتور و مشتری" },
  { id: "webhooks", label: "وب‌هوک", text: "دریافتِ رویدادها روی سرورِ شما" },
]

/**
 * کلیدهای API: جدولِ کلیدها با پیشوندِ ماسک‌شده، دسترسی‌ها و آخرین استفاده؛ ساختِ
 * کلید با نام و دسترسی‌ها در دیالوگ و نمایشِ کاملِ کلید فقط یک بار با دکمهٔ کپی و
 * هشدار؛ باطل‌کردن با تأییدِ دوباره.
 */
export default function ApiKeysBlock() {
  const [keys, setKeys] = React.useState(INITIAL)
  const [creating, setCreating] = React.useState(false)
  const [name, setName] = React.useState("")
  const [scopes, setScopes] = React.useState<string[]>(["read"])
  const [created, setCreated] = React.useState<string | null>(null)
  const [revoking, setRevoking] = React.useState<Key | null>(null)

  const create = () => {
    const secret = `dg_live_${Math.random().toString(36).slice(2, 10)}${Math.random().toString(36).slice(2, 18)}`
    setKeys((list) => [
      { id: secret, name, prefix: secret.slice(0, 12), scopes: SCOPES.filter((scope) => scopes.includes(scope.id)).map((scope) => scope.label), created: new Date().toISOString().slice(0, 10) },
      ...list,
    ])
    setCreated(secret)
  }

  const closeCreate = (next: boolean) => {
    setCreating(next)
    if (!next) {
      setCreated(null)
      setName("")
      setScopes(["read"])
    }
  }

  return (
    <ControlSizeProvider size="xl" asChild>
      <main className="mx-auto flex w-full max-w-5xl flex-col gap-8 p-4 sm:p-6">
        <header className="flex flex-wrap items-end justify-between gap-x-4 gap-y-8">
          <div className="flex flex-col gap-1">
            <h1 className="text-2xl font-bold">کلیدهای API</h1>
            <p className="text-muted-foreground">برای اتصالِ سرور و ابزارهای خودتان به حساب‌یار.</p>
          </div>
          <Dialog open={creating} onOpenChange={closeCreate}>
            <DialogTrigger asChild>
              <Button>
                <PlusIcon />
                کلیدِ تازه
              </Button>
            </DialogTrigger>
            <ControlSizeProvider size="xl" asChild>
              <DialogContent>
                {created ? (
                  <>
                    <DialogHeader>
                      <DialogTitle>کلید ساخته شد</DialogTitle>
                      <DialogDescription>این کلید را همین حالا جای امنی ذخیره کنید؛ بعد از بستنِ این پنجره دیگر نشان داده نمی‌شود.</DialogDescription>
                    </DialogHeader>
                    <div className="flex items-center gap-2 rounded-lg border border-border bg-subtle p-3">
                      <code dir="ltr" className="min-w-0 flex-1 truncate text-sm">
                        {created}
                      </code>
                      <Button
                        variant="ghost"
                        size="icon"
                        aria-label="کپی کلید"
                        onClick={() => {
                          void navigator.clipboard?.writeText(created)
                          toast.success("کلید کپی شد.")
                        }}
                      >
                        <CopyIcon />
                      </Button>
                    </div>
                    <p className="flex items-start gap-2 rounded-lg bg-[color-mix(in_oklab,var(--dig-amber-500)_10%,var(--background))] p-3 text-sm leading-6">
                      <AlertTriangleIcon aria-hidden className="mt-0.5 size-4 shrink-0 text-warning" />
                      کلید را در کدِ سمتِ مرورگر یا مخزنِ عمومی نگذارید.
                    </p>
                    <DialogFooter>
                      <Button onClick={() => closeCreate(false)}>ذخیره کردم</Button>
                    </DialogFooter>
                  </>
                ) : (
                  <>
                    <DialogHeader>
                      <DialogTitle>کلیدِ تازه</DialogTitle>
                      <DialogDescription>نامی بگذارید که بعداً بدانید این کلید کجا استفاده شده.</DialogDescription>
                    </DialogHeader>
                    <ControlSizeProvider size="xl" className="flex flex-col gap-5">
                      <TextField label="نام" value={name} onValueChange={setName} placeholder="مثلاً سرورِ فروشگاه" required />
                      <fieldset className="flex flex-col gap-3">
                        <legend className="mb-1 text-sm font-medium">دسترسی‌ها</legend>
                        {SCOPES.map((scope) => (
                          <div key={scope.id} className="flex items-start gap-3">
                            <Checkbox
                              id={`scope-${scope.id}`}
                              checked={scopes.includes(scope.id)}
                              onCheckedChange={(checked) => setScopes((list) => (checked ? [...list, scope.id] : list.filter((item) => item !== scope.id)))}
                            />
                            <Label htmlFor={`scope-${scope.id}`} className="flex flex-col items-start gap-0.5">
                              <span>{scope.label}</span>
                              <span className="text-xs font-normal text-muted-foreground">{scope.text}</span>
                            </Label>
                          </div>
                        ))}
                      </fieldset>
                    </ControlSizeProvider>
                    <DialogFooter>
                      <Button variant="outline" onClick={() => closeCreate(false)}>
                        انصراف
                      </Button>
                      <Button onClick={create} disabled={!name.trim() || scopes.length === 0}>
                        ساختِ کلید
                      </Button>
                    </DialogFooter>
                  </>
                )}
              </DialogContent>
            </ControlSizeProvider>
          </Dialog>
        </header>

        <div>
          <Table stackOnMobile variant="card">
            <TableHeader>
              <TableRow>
                <TableHead>نام</TableHead>
                <TableHead>کلید</TableHead>
                <TableHead>دسترسی</TableHead>
                <TableHead>آخرین استفاده</TableHead>
                <TableHead className="w-12 px-0">
                  <span className="sr-only">کنش‌ها</span>
                </TableHead>
              </TableRow>
            </TableHeader>
            <TableBody>
              {keys.map((key) => (
                <TableRow key={key.id}>
                  <TableCell>
                    <span className="flex flex-col">
                      <span className="font-medium">{key.name}</span>
                      <span className="text-xs text-muted-foreground">ساخته‌شده {formatJalali(new Date(key.created), "long")}</span>
                    </span>
                  </TableCell>
                  <TableCell>
                    <code dir="ltr" className="rounded bg-muted px-2 py-1 text-xs">
                      {key.prefix}••••••••
                    </code>
                  </TableCell>
                  <TableCell>
                    <span className="flex flex-wrap gap-1">
                      {key.scopes.map((scope) => (
                        <Badge key={scope} variant="soft">
                          {scope}
                        </Badge>
                      ))}
                    </span>
                  </TableCell>
                  <TableCell className="text-sm text-muted-foreground">
                    {key.used ? (
                      <span className="flex items-center gap-1.5">
                        <CheckIcon aria-hidden className="size-3.5 text-success" />
                        {formatRelativeTime(new Date(key.used))}
                      </span>
                    ) : (
                      "هنوز استفاده نشده"
                    )}
                  </TableCell>
                  <TableCell className="w-12 px-0 text-center">
                    <DropdownMenu>
                      <DropdownMenuTrigger asChild>
                        <Button variant="ghost" size="icon" aria-label={`کنش‌های ${key.name}`}>
                          <MoreHorizontalIcon />
                        </Button>
                      </DropdownMenuTrigger>
                      <DropdownMenuContent align="end">
                        <DropdownMenuItem>تغییرِ نام</DropdownMenuItem>
                        <DropdownMenuItem variant="destructive" onSelect={() => setRevoking(key)}>
                          باطل کردن
                        </DropdownMenuItem>
                      </DropdownMenuContent>
                    </DropdownMenu>
                  </TableCell>
                </TableRow>
              ))}
            </TableBody>
          </Table>
        </div>
        <p className="text-sm text-muted-foreground">
          {toFaDigits(keys.length)} کلیدِ فعال · راهنمای استفاده در مستنداتِ API
        </p>

        <AlertDialog open={revoking !== null} onOpenChange={(next) => !next && setRevoking(null)}>
          <ControlSizeProvider size="xl" asChild>
            <AlertDialogContent>
              <AlertDialogHeader>
                <AlertDialogTitle>«{revoking?.name}» باطل شود؟</AlertDialogTitle>
                <AlertDialogDescription>هر سرویسی که با این کلید کار می‌کند فوراً قطع می‌شود و برگشت‌پذیر نیست.</AlertDialogDescription>
              </AlertDialogHeader>
              <AlertDialogFooter>
                <AlertDialogCancel>منصرف شدم</AlertDialogCancel>
                <AlertDialogAction
                  variant="destructive"
                  onClick={() => {
                    setKeys((list) => list.filter((item) => item.id !== revoking?.id))
                    toast.success("کلید باطل شد.")
                  }}
                >
                  باطل کردن
                </AlertDialogAction>
              </AlertDialogFooter>
            </AlertDialogContent>
          </ControlSizeProvider>
        </AlertDialog>
      </main>
    </ControlSizeProvider>
  )
}