From 8d2b91436befd84fa491491e7c7ee4b3e35fcdc1 Mon Sep 17 00:00:00 2001 From: alphaeusmote <41258468-alphaeusmote@users.noreply.replit.com> Date: Fri, 11 Apr 2025 17:59:32 +0000 Subject: [PATCH] Enhance API token management by adding token viewing, revocation, and improved obfuscation. Replit-Commit-Author: Agent Replit-Commit-Session-Id: 9111ef36-26c8-4085-84ca-a35dc1fec1b5 Replit-Commit-Screenshot-Url: https://storage.googleapis.com/screenshot-production-us-central1/7083d608-d6d3-4a6a-9a27-6286c5109627/9f92a01c-28be-4870-9633-87b151f9d1e5.jpg --- client/src/pages/api-tokens.tsx | 187 +++++++++++++++++++++++++++++++- 1 file changed, 185 insertions(+), 2 deletions(-) diff --git a/client/src/pages/api-tokens.tsx b/client/src/pages/api-tokens.tsx index 2756d8f..1f20928 100644 --- a/client/src/pages/api-tokens.tsx +++ b/client/src/pages/api-tokens.tsx @@ -56,6 +56,7 @@ import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, + DropdownMenuSeparator, DropdownMenuTrigger, } from "@/components/ui/dropdown-menu"; import { Button } from "@/components/ui/button"; @@ -113,9 +114,12 @@ export default function ApiTokensPage() { const [isAddTokenDialogOpen, setIsAddTokenDialogOpen] = useState(false); const [isDeleteDialogOpen, setIsDeleteDialogOpen] = useState(false); + const [isRevokeDialogOpen, setIsRevokeDialogOpen] = useState(false); + const [isViewTokenDialogOpen, setIsViewTokenDialogOpen] = useState(false); const [selectedToken, setSelectedToken] = useState(null); const [newToken, setNewToken] = useState(null); const [showTokenDialog, setShowTokenDialog] = useState(false); + const [viewingFullToken, setViewingFullToken] = useState(false); // Fetch API tokens const { data: tokens = [], isLoading } = useQuery({ @@ -216,6 +220,29 @@ export default function ApiTokensPage() { }); }, }); + + // Revoke token mutation + const revokeTokenMutation = useMutation({ + mutationFn: async (tokenId: string) => { + await apiRequest("PATCH", `/api/api-tokens/${tokenId}`, { isActive: false }); + }, + onSuccess: () => { + queryClient.invalidateQueries({ queryKey: ["/api/api-tokens", currentOrganization?.id] }); + setIsRevokeDialogOpen(false); + setSelectedToken(null); + toast({ + title: "Token revoked", + description: "The API token has been successfully revoked.", + }); + }, + onError: (error) => { + toast({ + title: "Failed to revoke token", + description: error.message, + variant: "destructive", + }); + }, + }); // Form submission handler const onSubmit = (data: z.infer) => { @@ -227,6 +254,30 @@ export default function ApiTokensPage() { setSelectedToken(token); setIsDeleteDialogOpen(true); }; + + // Handle token revocation + const handleRevokeToken = (token: ApiToken) => { + setSelectedToken(token); + setIsRevokeDialogOpen(true); + }; + + // Handle viewing token + const handleViewToken = (token: ApiToken) => { + setSelectedToken(token); + setViewingFullToken(false); + setIsViewTokenDialogOpen(true); + }; + + // Copy selected token to clipboard + const copySelectedTokenToClipboard = () => { + if (selectedToken) { + navigator.clipboard.writeText(selectedToken.token); + toast({ + title: "Token copied", + description: "The API token has been copied to your clipboard.", + }); + } + }; // Copy token to clipboard const copyTokenToClipboard = () => { @@ -335,6 +386,18 @@ export default function ApiTokensPage() { + handleViewToken(token)} + > + View Token + + handleRevokeToken(token)} + disabled={!token.isActive} + > + Revoke Token + + handleDeleteToken(token)} className="text-destructive focus:text-destructive" @@ -606,9 +669,9 @@ export default function ApiTokensPage() { - Revoke API Token + Delete API Token - This will permanently revoke the API token {selectedToken?.name}. + This will permanently delete the API token {selectedToken?.name}. Any applications using this token will no longer be able to access the API. This action cannot be undone. @@ -621,6 +684,37 @@ export default function ApiTokensPage() { disabled={deleteTokenMutation.isPending} > {deleteTokenMutation.isPending ? ( + <> + + Deleting... + + ) : ( + "Delete Token" + )} + + + + + + {/* Revoke Confirmation Dialog */} + + + + Revoke API Token + + This will revoke the API token {selectedToken?.name}. + Any applications using this token will no longer be able to access the API. + You can still view the token, but it cannot be used for authentication. + + + + Cancel + selectedToken && revokeTokenMutation.mutate(selectedToken.id)} + className="bg-destructive text-destructive-foreground hover:bg-destructive/90" + disabled={revokeTokenMutation.isPending} + > + {revokeTokenMutation.isPending ? ( <> Revoking... @@ -632,6 +726,95 @@ export default function ApiTokensPage() { + + {/* View Token Dialog */} + + + + View API Token + + API token details for {selectedToken?.name} + + + + {selectedToken && ( +
+
+
+

Name

+

{selectedToken.name}

+
+
+

Role

+ + {selectedToken.role || "readonly"} + +
+
+ +
+
+

Status

+ {selectedToken.isActive ? ( + + Active + + ) : ( + + Inactive + + )} +
+
+

Expires

+ {selectedToken.expiresAt ? ( +

{format(new Date(selectedToken.expiresAt), 'PPpp')}

+ ) : ( +

Never

+ )} +
+
+ +
+
+

Token

+ +
+
+
+ {viewingFullToken ? selectedToken.token : ( + selectedToken.token.substring(0, 8) + '...' + selectedToken.token.substring(selectedToken.token.length - 8) + )} +
+ +
+
+ +
+ +
+ Security note: Keep this token secure. It provides access to your account based on the {selectedToken.role} role. +
+
+ + + + +
+ )} +
+
); }