From 0687561f5bf1067424dfa2cbd74cf1b48471244b Mon Sep 17 00:00:00 2001 From: Anish Sarkar <104695310+AnishSarkar22@users.noreply.github.com> Date: Fri, 19 Jun 2026 20:29:03 +0530 Subject: [PATCH] feat: add personal access token settings UI --- .../components/ApiKeyContent.tsx | 238 ++++++++++++------ surfsense_web/messages/en.json | 4 +- surfsense_web/messages/es.json | 4 +- surfsense_web/messages/hi.json | 4 +- surfsense_web/messages/pt.json | 4 +- surfsense_web/messages/zh.json | 4 +- 6 files changed, 173 insertions(+), 85 deletions(-) diff --git a/surfsense_web/app/dashboard/[search_space_id]/user-settings/components/ApiKeyContent.tsx b/surfsense_web/app/dashboard/[search_space_id]/user-settings/components/ApiKeyContent.tsx index 47cdf8f2d..5ac7e83b8 100644 --- a/surfsense_web/app/dashboard/[search_space_id]/user-settings/components/ApiKeyContent.tsx +++ b/surfsense_web/app/dashboard/[search_space_id]/user-settings/components/ApiKeyContent.tsx @@ -1,109 +1,197 @@ "use client"; -import { Check, Copy, Info } from "lucide-react"; -import { useTranslations } from "next-intl"; -import { useCallback, useRef, useState } from "react"; +import { Check, Copy, Info, Plus, Trash2 } from "lucide-react"; +import { useCallback, useMemo, useState } from "react"; import { Alert, AlertDescription } from "@/components/ui/alert"; +import { Badge } from "@/components/ui/badge"; import { Button } from "@/components/ui/button"; +import { + Dialog, + DialogContent, + DialogDescription, + DialogFooter, + DialogHeader, + DialogTitle, +} from "@/components/ui/dialog"; +import { Input } from "@/components/ui/input"; +import { Label } from "@/components/ui/label"; import { Skeleton } from "@/components/ui/skeleton"; -import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from "@/components/ui/tooltip"; -import { useApiKey } from "@/hooks/use-api-key"; +import { usePats } from "@/hooks/use-pats"; import { copyToClipboard as copyToClipboardUtil } from "@/lib/utils"; export function ApiKeyContent() { - const t = useTranslations("userSettings"); - const { apiKey, isLoading, copied, copyToClipboard } = useApiKey(); - const [copiedUsage, setCopiedUsage] = useState(false); - const usageCopyTimeoutRef = useRef>(null); + const { tokens, createdToken, setCreatedToken, isLoading, isMutating, createToken, deleteToken } = + usePats(); + const [createOpen, setCreateOpen] = useState(false); + const [label, setLabel] = useState(""); + const [expiresInDays, setExpiresInDays] = useState(""); + const [copiedToken, setCopiedToken] = useState(false); - const copyUsageToClipboard = useCallback(async () => { - const text = `Authorization: Bearer ${apiKey || "YOUR_API_KEY"}`; - const success = await copyToClipboardUtil(text); + const sortedTokens = useMemo(() => tokens, [tokens]); + + const handleCreate = useCallback(async () => { + const trimmedLabel = label.trim(); + if (!trimmedLabel) return; + + await createToken({ + label: trimmedLabel, + expires_in_days: expiresInDays ? Number(expiresInDays) : null, + }); + setLabel(""); + setExpiresInDays(""); + setCreateOpen(false); + }, [createToken, expiresInDays, label]); + + const copyCreatedToken = useCallback(async () => { + if (!createdToken) return; + const success = await copyToClipboardUtil(createdToken.token); if (success) { - setCopiedUsage(true); - if (usageCopyTimeoutRef.current) clearTimeout(usageCopyTimeoutRef.current); - usageCopyTimeoutRef.current = setTimeout(() => setCopiedUsage(false), 2000); + setCopiedToken(true); + setTimeout(() => setCopiedToken(false), 2000); } - }, [apiKey]); + }, [createdToken]); + + const handleDelete = useCallback( + async (id: number, tokenLabel: string) => { + if (!window.confirm(`Delete personal access token "${tokenLabel}"? This cannot be undone.`)) { + return; + } + await deleteToken(id); + }, + [deleteToken] + ); return (
- {t("api_key_warning_description")} + + Personal access tokens are long-lived credentials for extensions, Obsidian, and + programmatic API clients. Copy a token when you create it; it is shown only once. + -
-

{t("your_api_key")}

+
+
+

Personal access tokens

+

+ Expired tokens stay listed until you delete them. +

+
+ +
+ +
{isLoading ? ( -
-
- -
-
+
+ +
- ) : apiKey ? ( -
-
-

- {apiKey} -

-
- - - + ) : sortedTokens.length > 0 ? ( +
+ {sortedTokens.map((token) => { + const expiresAt = token.expires_at ? new Date(token.expires_at) : null; + const isExpired = expiresAt ? expiresAt.getTime() <= Date.now() : false; + return ( +
+
+
+

{token.label}

+ {isExpired ? Expired : null} +
+

{token.prefix}...

+

+ Expires: {expiresAt ? expiresAt.toLocaleDateString() : "Never"} · Last used:{" "} + {token.last_used_at + ? new Date(token.last_used_at).toLocaleString() + : "Never"} +

+
- - {copied ? t("copied") : t("copy")} - - +
+ ); + })}
) : ( -

{t("no_api_key")}

+

+ No personal access tokens yet. +

)}
-
-

{t("usage_title")}

-

{t("usage_description")}

-
-
-
-							Authorization: Bearer {apiKey || "YOUR_API_KEY"}
-						
+ + + + Create personal access token + + Name this token so you can recognize where it is used later. + + +
+
+ + setLabel(event.target.value)} + placeholder="Obsidian vault" + /> +
+
+ + setExpiresInDays(event.target.value)} + placeholder="Never expires" + /> +
- - - - - - {copiedUsage ? t("copied") : t("copy")} - - -
-
+ + + + + + + + !open && setCreatedToken(null)}> + + + Copy your token now + + This token is shown only once. Store it somewhere secure before closing this + dialog. + + +
+ + {createdToken?.token} + + +
+ + + +
+
); } diff --git a/surfsense_web/messages/en.json b/surfsense_web/messages/en.json index 866ba4844..6cfee4edf 100644 --- a/surfsense_web/messages/en.json +++ b/surfsense_web/messages/en.json @@ -119,9 +119,9 @@ "profile_save": "Save Changes", "profile_saved": "Profile updated successfully", "profile_save_error": "Failed to update profile", - "api_key_nav_label": "API Key", + "api_key_nav_label": "API Access", "api_key_nav_description": "Manage your API access token", - "api_key_title": "API Key", + "api_key_title": "API Access", "api_key_description": "Use this key to authenticate API requests", "api_key_warning_description": "Your API key grants full access to your account. Never share it publicly or commit it to version control.", "your_api_key": "Your API Key", diff --git a/surfsense_web/messages/es.json b/surfsense_web/messages/es.json index f7755b47e..06b309df1 100644 --- a/surfsense_web/messages/es.json +++ b/surfsense_web/messages/es.json @@ -119,9 +119,9 @@ "profile_save": "Guardar cambios", "profile_saved": "Perfil actualizado correctamente", "profile_save_error": "Error al actualizar el perfil", - "api_key_nav_label": "Clave API", + "api_key_nav_label": "Acceso API", "api_key_nav_description": "Administra tu token de acceso a la API", - "api_key_title": "Clave API", + "api_key_title": "Acceso API", "api_key_description": "Usa esta clave para autenticar las solicitudes de la API", "api_key_warning_description": "Tu clave API otorga acceso completo a tu cuenta. Nunca la compartas públicamente ni la incluyas en el control de versiones.", "your_api_key": "Tu clave API", diff --git a/surfsense_web/messages/hi.json b/surfsense_web/messages/hi.json index 038555f1e..73a025803 100644 --- a/surfsense_web/messages/hi.json +++ b/surfsense_web/messages/hi.json @@ -119,9 +119,9 @@ "profile_save": "परिवर्तन सहेजें", "profile_saved": "प्रोफ़ाइल सफलतापूर्वक अपडेट की गई", "profile_save_error": "प्रोफ़ाइल अपडेट करने में विफल", - "api_key_nav_label": "API कुंजी", + "api_key_nav_label": "API एक्सेस", "api_key_nav_description": "अपना API एक्सेस टोकन प्रबंधित करें", - "api_key_title": "API कुंजी", + "api_key_title": "API एक्सेस", "api_key_description": "API अनुरोधों को प्रमाणित करने के लिए इस कुंजी का उपयोग करें", "api_key_warning_description": "आपकी API कुंजी आपके खाते तक पूर्ण पहुंच प्रदान करती है। इसे कभी सार्वजनिक रूप से साझा न करें या संस्करण नियंत्रण में शामिल न करें।", "your_api_key": "आपकी API कुंजी", diff --git a/surfsense_web/messages/pt.json b/surfsense_web/messages/pt.json index bcba8f70c..00b8242f7 100644 --- a/surfsense_web/messages/pt.json +++ b/surfsense_web/messages/pt.json @@ -119,9 +119,9 @@ "profile_save": "Salvar alterações", "profile_saved": "Perfil atualizado com sucesso", "profile_save_error": "Falha ao atualizar o perfil", - "api_key_nav_label": "Chave API", + "api_key_nav_label": "Acesso API", "api_key_nav_description": "Gerencie seu token de acesso à API", - "api_key_title": "Chave API", + "api_key_title": "Acesso API", "api_key_description": "Use esta chave para autenticar solicitações da API", "api_key_warning_description": "Sua chave API concede acesso total à sua conta. Nunca a compartilhe publicamente nem a inclua no controle de versão.", "your_api_key": "Sua chave API", diff --git a/surfsense_web/messages/zh.json b/surfsense_web/messages/zh.json index 5fea60eb8..fd4147e66 100644 --- a/surfsense_web/messages/zh.json +++ b/surfsense_web/messages/zh.json @@ -119,9 +119,9 @@ "profile_save": "保存更改", "profile_saved": "个人资料已成功更新", "profile_save_error": "无法更新个人资料", - "api_key_nav_label": "API密钥", + "api_key_nav_label": "API访问", "api_key_nav_description": "管理您的API访问令牌", - "api_key_title": "API密钥", + "api_key_title": "API访问", "api_key_description": "使用此密钥验证API请求", "api_key_warning_description": "您的API密钥可以完全访问您的账户。请勿公开分享或提交到版本控制。", "your_api_key": "您的API密钥",