mirror of
https://github.com/freedbygrace/DynamoDNS.git
synced 2026-08-28 03:07:25 +00:00
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
This commit is contained in:
@@ -56,6 +56,7 @@ import {
|
|||||||
DropdownMenu,
|
DropdownMenu,
|
||||||
DropdownMenuContent,
|
DropdownMenuContent,
|
||||||
DropdownMenuItem,
|
DropdownMenuItem,
|
||||||
|
DropdownMenuSeparator,
|
||||||
DropdownMenuTrigger,
|
DropdownMenuTrigger,
|
||||||
} from "@/components/ui/dropdown-menu";
|
} from "@/components/ui/dropdown-menu";
|
||||||
import { Button } from "@/components/ui/button";
|
import { Button } from "@/components/ui/button";
|
||||||
@@ -113,9 +114,12 @@ export default function ApiTokensPage() {
|
|||||||
|
|
||||||
const [isAddTokenDialogOpen, setIsAddTokenDialogOpen] = useState(false);
|
const [isAddTokenDialogOpen, setIsAddTokenDialogOpen] = useState(false);
|
||||||
const [isDeleteDialogOpen, setIsDeleteDialogOpen] = useState(false);
|
const [isDeleteDialogOpen, setIsDeleteDialogOpen] = useState(false);
|
||||||
|
const [isRevokeDialogOpen, setIsRevokeDialogOpen] = useState(false);
|
||||||
|
const [isViewTokenDialogOpen, setIsViewTokenDialogOpen] = useState(false);
|
||||||
const [selectedToken, setSelectedToken] = useState<ApiToken | null>(null);
|
const [selectedToken, setSelectedToken] = useState<ApiToken | null>(null);
|
||||||
const [newToken, setNewToken] = useState<string | null>(null);
|
const [newToken, setNewToken] = useState<string | null>(null);
|
||||||
const [showTokenDialog, setShowTokenDialog] = useState(false);
|
const [showTokenDialog, setShowTokenDialog] = useState(false);
|
||||||
|
const [viewingFullToken, setViewingFullToken] = useState(false);
|
||||||
|
|
||||||
// Fetch API tokens
|
// Fetch API tokens
|
||||||
const { data: tokens = [], isLoading } = useQuery<ApiToken[]>({
|
const { data: tokens = [], isLoading } = useQuery<ApiToken[]>({
|
||||||
@@ -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
|
// Form submission handler
|
||||||
const onSubmit = (data: z.infer<typeof tokenFormSchema>) => {
|
const onSubmit = (data: z.infer<typeof tokenFormSchema>) => {
|
||||||
@@ -227,6 +254,30 @@ export default function ApiTokensPage() {
|
|||||||
setSelectedToken(token);
|
setSelectedToken(token);
|
||||||
setIsDeleteDialogOpen(true);
|
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
|
// Copy token to clipboard
|
||||||
const copyTokenToClipboard = () => {
|
const copyTokenToClipboard = () => {
|
||||||
@@ -335,6 +386,18 @@ export default function ApiTokensPage() {
|
|||||||
</Button>
|
</Button>
|
||||||
</DropdownMenuTrigger>
|
</DropdownMenuTrigger>
|
||||||
<DropdownMenuContent align="end">
|
<DropdownMenuContent align="end">
|
||||||
|
<DropdownMenuItem
|
||||||
|
onClick={() => handleViewToken(token)}
|
||||||
|
>
|
||||||
|
View Token
|
||||||
|
</DropdownMenuItem>
|
||||||
|
<DropdownMenuItem
|
||||||
|
onClick={() => handleRevokeToken(token)}
|
||||||
|
disabled={!token.isActive}
|
||||||
|
>
|
||||||
|
Revoke Token
|
||||||
|
</DropdownMenuItem>
|
||||||
|
<DropdownMenuSeparator />
|
||||||
<DropdownMenuItem
|
<DropdownMenuItem
|
||||||
onClick={() => handleDeleteToken(token)}
|
onClick={() => handleDeleteToken(token)}
|
||||||
className="text-destructive focus:text-destructive"
|
className="text-destructive focus:text-destructive"
|
||||||
@@ -606,9 +669,9 @@ export default function ApiTokensPage() {
|
|||||||
<AlertDialog open={isDeleteDialogOpen} onOpenChange={setIsDeleteDialogOpen}>
|
<AlertDialog open={isDeleteDialogOpen} onOpenChange={setIsDeleteDialogOpen}>
|
||||||
<AlertDialogContent>
|
<AlertDialogContent>
|
||||||
<AlertDialogHeader>
|
<AlertDialogHeader>
|
||||||
<AlertDialogTitle>Revoke API Token</AlertDialogTitle>
|
<AlertDialogTitle>Delete API Token</AlertDialogTitle>
|
||||||
<AlertDialogDescription>
|
<AlertDialogDescription>
|
||||||
This will permanently revoke the API token <strong>{selectedToken?.name}</strong>.
|
This will permanently delete the API token <strong>{selectedToken?.name}</strong>.
|
||||||
Any applications using this token will no longer be able to access the API.
|
Any applications using this token will no longer be able to access the API.
|
||||||
This action cannot be undone.
|
This action cannot be undone.
|
||||||
</AlertDialogDescription>
|
</AlertDialogDescription>
|
||||||
@@ -621,6 +684,37 @@ export default function ApiTokensPage() {
|
|||||||
disabled={deleteTokenMutation.isPending}
|
disabled={deleteTokenMutation.isPending}
|
||||||
>
|
>
|
||||||
{deleteTokenMutation.isPending ? (
|
{deleteTokenMutation.isPending ? (
|
||||||
|
<>
|
||||||
|
<Loader2 className="mr-2 h-4 w-4 animate-spin" />
|
||||||
|
Deleting...
|
||||||
|
</>
|
||||||
|
) : (
|
||||||
|
"Delete Token"
|
||||||
|
)}
|
||||||
|
</AlertDialogAction>
|
||||||
|
</AlertDialogFooter>
|
||||||
|
</AlertDialogContent>
|
||||||
|
</AlertDialog>
|
||||||
|
|
||||||
|
{/* Revoke Confirmation Dialog */}
|
||||||
|
<AlertDialog open={isRevokeDialogOpen} onOpenChange={setIsRevokeDialogOpen}>
|
||||||
|
<AlertDialogContent>
|
||||||
|
<AlertDialogHeader>
|
||||||
|
<AlertDialogTitle>Revoke API Token</AlertDialogTitle>
|
||||||
|
<AlertDialogDescription>
|
||||||
|
This will revoke the API token <strong>{selectedToken?.name}</strong>.
|
||||||
|
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.
|
||||||
|
</AlertDialogDescription>
|
||||||
|
</AlertDialogHeader>
|
||||||
|
<AlertDialogFooter>
|
||||||
|
<AlertDialogCancel>Cancel</AlertDialogCancel>
|
||||||
|
<AlertDialogAction
|
||||||
|
onClick={() => selectedToken && revokeTokenMutation.mutate(selectedToken.id)}
|
||||||
|
className="bg-destructive text-destructive-foreground hover:bg-destructive/90"
|
||||||
|
disabled={revokeTokenMutation.isPending}
|
||||||
|
>
|
||||||
|
{revokeTokenMutation.isPending ? (
|
||||||
<>
|
<>
|
||||||
<Loader2 className="mr-2 h-4 w-4 animate-spin" />
|
<Loader2 className="mr-2 h-4 w-4 animate-spin" />
|
||||||
Revoking...
|
Revoking...
|
||||||
@@ -632,6 +726,95 @@ export default function ApiTokensPage() {
|
|||||||
</AlertDialogFooter>
|
</AlertDialogFooter>
|
||||||
</AlertDialogContent>
|
</AlertDialogContent>
|
||||||
</AlertDialog>
|
</AlertDialog>
|
||||||
|
|
||||||
|
{/* View Token Dialog */}
|
||||||
|
<Dialog open={isViewTokenDialogOpen} onOpenChange={setIsViewTokenDialogOpen}>
|
||||||
|
<DialogContent>
|
||||||
|
<DialogHeader>
|
||||||
|
<DialogTitle>View API Token</DialogTitle>
|
||||||
|
<DialogDescription>
|
||||||
|
API token details for <strong>{selectedToken?.name}</strong>
|
||||||
|
</DialogDescription>
|
||||||
|
</DialogHeader>
|
||||||
|
|
||||||
|
{selectedToken && (
|
||||||
|
<div className="space-y-4">
|
||||||
|
<div className="grid grid-cols-2 gap-4">
|
||||||
|
<div>
|
||||||
|
<h4 className="text-sm font-medium mb-1">Name</h4>
|
||||||
|
<p className="text-sm">{selectedToken.name}</p>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<h4 className="text-sm font-medium mb-1">Role</h4>
|
||||||
|
<Badge variant="outline" className="text-xs capitalize">
|
||||||
|
{selectedToken.role || "readonly"}
|
||||||
|
</Badge>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="grid grid-cols-2 gap-4">
|
||||||
|
<div>
|
||||||
|
<h4 className="text-sm font-medium mb-1">Status</h4>
|
||||||
|
{selectedToken.isActive ? (
|
||||||
|
<Badge variant="outline" className="bg-success/10 text-success border-success/20">
|
||||||
|
Active
|
||||||
|
</Badge>
|
||||||
|
) : (
|
||||||
|
<Badge variant="outline" className="bg-muted/10 text-muted-foreground">
|
||||||
|
Inactive
|
||||||
|
</Badge>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<h4 className="text-sm font-medium mb-1">Expires</h4>
|
||||||
|
{selectedToken.expiresAt ? (
|
||||||
|
<p className="text-sm">{format(new Date(selectedToken.expiresAt), 'PPpp')}</p>
|
||||||
|
) : (
|
||||||
|
<p className="text-sm text-muted-foreground">Never</p>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<div className="flex justify-between items-center mb-1">
|
||||||
|
<h4 className="text-sm font-medium">Token</h4>
|
||||||
|
<Button variant="ghost" size="sm" onClick={() => setViewingFullToken(!viewingFullToken)}>
|
||||||
|
{viewingFullToken ? "Hide" : "Show"}
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
<div className="bg-muted p-4 rounded-md relative">
|
||||||
|
<div className="font-mono text-sm break-all">
|
||||||
|
{viewingFullToken ? selectedToken.token : (
|
||||||
|
selectedToken.token.substring(0, 8) + '...' + selectedToken.token.substring(selectedToken.token.length - 8)
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
<Button
|
||||||
|
variant="ghost"
|
||||||
|
size="icon"
|
||||||
|
className="absolute top-2 right-2"
|
||||||
|
onClick={copySelectedTokenToClipboard}
|
||||||
|
>
|
||||||
|
<Copy className="h-4 w-4" />
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="flex items-start mt-2 p-4 bg-amber-50 dark:bg-amber-950/30 text-amber-800 dark:text-amber-200 rounded-md">
|
||||||
|
<Info className="h-5 w-5 mr-2 flex-shrink-0 mt-0.5" />
|
||||||
|
<div className="text-sm">
|
||||||
|
<strong>Security note:</strong> Keep this token secure. It provides access to your account based on the {selectedToken.role} role.
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<DialogFooter>
|
||||||
|
<Button onClick={() => setIsViewTokenDialogOpen(false)}>
|
||||||
|
Close
|
||||||
|
</Button>
|
||||||
|
</DialogFooter>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</DialogContent>
|
||||||
|
</Dialog>
|
||||||
</MainLayout>
|
</MainLayout>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user