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, useWatch } from "react-hook-form"; import { Popover, PopoverContent, PopoverTrigger } from "@/components/ui/popover"; 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 { Calendar } from "@/components/ui/calendar"; import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue, } from "@/components/ui/select"; import { Loader2, Key, MoreVertical, CalendarIcon, Copy, Info, Clock } 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().regex(/^([01]?[0-9]|2[0-3]):[0-5][0-9]$/, { message: "Time must be in 24-hour format HH:MM" }).optional(), }).refine((data) => { // If custom expiration is selected, a date must be provided if (data.expiresIn === 'custom' && !data.customDate) { return false; } return true; }, { message: "Please select a date for the custom expiration", path: ["customDate"] }).refine((data) => { // If a custom date is provided, validate it's in the future if (data.customDate) { const now = new Date(); const dateOnly = new Date(data.customDate); dateOnly.setHours(0, 0, 0, 0); now.setHours(0, 0, 0, 0); return dateOnly >= now; } return true; }, { message: "Expiration date must be in the future", path: ["customDate"] }); 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) => { // Instead of using expiresAt directly, send the form data and let the server handle it const tokenData: any = { name: data.name, organizationId: currentOrganization?.id || "1", role: data.role, isActive: true, expiresIn: data.expiresIn || 'never', }; // Add custom date and time fields if present if (data.expiresIn === 'custom') { // For custom date/time, pass as separate fields for the server to process if (data.customDate) { // Format the date as YYYY-MM-DD for the server tokenData.customDate = format(data.customDate, 'yyyy-MM-dd'); console.log("Sending customDate:", tokenData.customDate); // Add custom time if present if (data.customTime) { tokenData.customTime = data.customTime; console.log("Sending customTime:", data.customTime); } else { // Default to end of day if no time tokenData.customTime = "23:59"; console.log("Using default end of day time"); } } } console.log("Sending token data to server:", tokenData); 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 )} /> {/* Custom date and time fields - show only when custom expiration is selected */} {form.watch("expiresIn") === "custom" && (
( Custom Date date < new Date()} initialFocus /> The date when the token will expire )} /> ( Custom Time
{field.value ? field.value : "23:59"}
The exact time (HH:MM) when the token will expire
)} />
)}
{/* 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" )}
); }