import { useState } from "react"; import { useQuery, useMutation } from "@tanstack/react-query"; import { MainLayout } from "@/components/layouts/main-layout"; import { ApiToken, InsertApiToken, systemRoles, CustomRole } from "@shared/schema"; import { useAuth } from "@/hooks/use-auth"; import { useOrganization } from "@/context/organization-context"; import { apiRequest, queryClient } from "@/lib/queryClient"; import { useToast } from "@/hooks/use-toast"; import { useForm } from "react-hook-form"; import { zodResolver } from "@hookform/resolvers/zod"; import { z } from "zod"; import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow, } from "@/components/ui/table"; import { Dialog, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogTitle, } from "@/components/ui/dialog"; import { Form, FormControl, FormField, FormItem, FormLabel, FormMessage, FormDescription, } from "@/components/ui/form"; import { AlertDialog, AlertDialogAction, AlertDialogCancel, AlertDialogContent, AlertDialogDescription, AlertDialogFooter, AlertDialogHeader, AlertDialogTitle, } from "@/components/ui/alert-dialog"; import { Card, CardContent, CardDescription, CardHeader, CardTitle, } from "@/components/ui/card"; import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuTrigger, } from "@/components/ui/dropdown-menu"; import { Button } from "@/components/ui/button"; import { Input } from "@/components/ui/input"; import { Badge } from "@/components/ui/badge"; import { Checkbox } from "@/components/ui/checkbox"; import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue, } from "@/components/ui/select"; import { Loader2, Key, MoreVertical, Calendar, Copy, Info } from "lucide-react"; import { formatDistanceToNow, format } from "date-fns"; // Token form schema const tokenFormSchema = z.object({ name: z.string().min(1, "Token name is required"), role: z.string().min(1, "Role is required"), expiresIn: z.string().optional(), customDate: z.date().optional(), customTime: z.string().optional(), }); export default function ApiTokensPage() { const { toast } = useToast(); const { user } = useAuth(); const { currentOrganization } = useOrganization(); const [isAddTokenDialogOpen, setIsAddTokenDialogOpen] = useState(false); const [isDeleteDialogOpen, setIsDeleteDialogOpen] = useState(false); const [selectedToken, setSelectedToken] = useState(null); const [newToken, setNewToken] = useState(null); const [showTokenDialog, setShowTokenDialog] = useState(false); // Fetch API tokens const { data: tokens = [], isLoading } = useQuery({ queryKey: ["/api/api-tokens", currentOrganization?.id], enabled: !!currentOrganization?.id, }); // Fetch custom roles const { data: customRoles = [] } = useQuery({ queryKey: ["/api/roles/custom"], }); // Form for adding a token const form = useForm>({ resolver: zodResolver(tokenFormSchema), defaultValues: { name: "", role: "readonly", expiresIn: "never", }, }); // Add token mutation const addTokenMutation = useMutation({ mutationFn: async (data: z.infer) => { const tokenData: Partial = { name: data.name, organizationId: currentOrganization?.id || "", role: data.role, isActive: true, }; // Calculate expiration date based on selection if (data.expiresIn && data.expiresIn !== 'never') { const now = new Date(); switch (data.expiresIn) { case '1hour': tokenData.expiresAt = new Date(now.getTime() + 60 * 60 * 1000); break; case '1day': tokenData.expiresAt = new Date(now.getTime() + 24 * 60 * 60 * 1000); break; case '7days': tokenData.expiresAt = new Date(now.getTime() + 7 * 24 * 60 * 60 * 1000); break; case '30days': tokenData.expiresAt = new Date(now.getTime() + 30 * 24 * 60 * 60 * 1000); break; case '90days': tokenData.expiresAt = new Date(now.getTime() + 90 * 24 * 60 * 60 * 1000); break; case '1year': tokenData.expiresAt = new Date(now.getTime() + 365 * 24 * 60 * 60 * 1000); break; } } const res = await apiRequest("POST", "/api/api-tokens", tokenData); return await res.json(); }, onSuccess: (data) => { queryClient.invalidateQueries({ queryKey: ["/api/api-tokens", currentOrganization?.id] }); setIsAddTokenDialogOpen(false); setNewToken(data.token); setShowTokenDialog(true); form.reset(); toast({ title: "Token created", description: "The API token has been successfully created.", }); }, onError: (error) => { toast({ title: "Failed to create token", description: error.message, variant: "destructive", }); }, }); // Delete token mutation const deleteTokenMutation = useMutation({ mutationFn: async (tokenId: string) => { await apiRequest("DELETE", `/api/api-tokens/${tokenId}`); }, onSuccess: () => { queryClient.invalidateQueries({ queryKey: ["/api/api-tokens", currentOrganization?.id] }); setIsDeleteDialogOpen(false); setSelectedToken(null); toast({ title: "Token deleted", description: "The API token has been successfully deleted.", }); }, onError: (error) => { toast({ title: "Failed to delete token", description: error.message, variant: "destructive", }); }, }); // Form submission handler const onSubmit = (data: z.infer) => { addTokenMutation.mutate(data); }; // Handle token deletion const handleDeleteToken = (token: ApiToken) => { setSelectedToken(token); setIsDeleteDialogOpen(true); }; // Copy token to clipboard const copyTokenToClipboard = () => { if (newToken) { navigator.clipboard.writeText(newToken); toast({ title: "Token copied", description: "The API token has been copied to your clipboard.", }); } }; return (

Your API Tokens

Create and manage API tokens for secure access to the DynamoDNS API.

{/* Tokens Table */} {isLoading ? (
) : tokens.length === 0 ? (

No API tokens found

Create your first API token to enable programmatic access.

) : ( Name Token Role Expires Status Actions {tokens.map((token) => ( {token.name} {token.token.substring(0, 8)}... {token.role || "readonly"} {token.expiresAt ? (
{formatDistanceToNow(new Date(token.expiresAt), { addSuffix: true })}
) : ( Never )}
{token.isActive ? ( Active ) : ( Inactive )} handleDeleteToken(token)} className="text-destructive focus:text-destructive" > Delete
))}
)}
{/* Add Token Dialog */} Create API Token Generate a new API token for programmatic access to the DynamoDNS API.
( Token Name A descriptive name to identify this token )} /> ( Role Determines the level of access for this token )} /> ( Expiration Select how long this token should remain valid )} />
{/* New Token Display Dialog */} Your New API Token Please copy your API token now. For security reasons, it won't be displayed again.
{newToken}
Security note: Store this token securely. It provides access to your account based on the role you selected.
{/* Delete Confirmation Dialog */} Revoke API Token This will permanently revoke the API token {selectedToken?.name}. Any applications using this token will no longer be able to access the API. This action cannot be undone. Cancel selectedToken && deleteTokenMutation.mutate(selectedToken.id)} className="bg-destructive text-destructive-foreground hover:bg-destructive/90" disabled={deleteTokenMutation.isPending} > {deleteTokenMutation.isPending ? ( <> Revoking... ) : ( "Revoke Token" )}
); }