کلیدهای API
پیشنمایش تمامصفحهجدولِ کلیدها با پیشوندِ ماسکشده، دسترسی و آخرین استفاده؛ ساختِ کلید با نام و دسترسیها، نمایشِ یکبارهٔ کلید با کپی و هشدار، و باطلکردن با تأییدِ دوباره.
۱. نصب کامپوننتها
این بلاک از ۱۳ آیتم دیگ استفاده میکند: 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.jsonpnpm 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.jsonyarn 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.jsonbunx --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>
)
}