Add IUCN API integration and improve authentication. This includes adding IUCN API token management, displaying API version, and updating the UI to reflect both CITES and IUCN API statuses.

Replit-Commit-Author: Agent
Replit-Commit-Session-Id: e931b5ab-041b-42e7-baf1-50017869cef6
Replit-Commit-Screenshot-Url: https://storage.googleapis.com/screenshot-production-us-central1/e19c6a51-7e4c-4bb8-a6a6-46dc00f0ec99/396f4320-2dbc-4ff6-85bf-f132094a1754.jpg
This commit is contained in:
Magnus-SmariSma
2025-03-20 23:02:43 +00:00
parent a0eaf166f0
commit 9ae5646a64
4 changed files with 142 additions and 48 deletions

View File

@ -11,6 +11,7 @@ interface ApiStatusProps {
export default function ApiStatus({ citesToken }: ApiStatusProps) { export default function ApiStatus({ citesToken }: ApiStatusProps) {
const [iucnStatus, setIucnStatus] = useState<'checking' | 'connected' | 'error'>('checking'); const [iucnStatus, setIucnStatus] = useState<'checking' | 'connected' | 'error'>('checking');
const [citesStatus, setCitesStatus] = useState<'checking' | 'connected' | 'error'>('checking'); const [citesStatus, setCitesStatus] = useState<'checking' | 'connected' | 'error'>('checking');
const [iucnApiVersion, setIucnApiVersion] = useState<string | null>(null);
// Check CITES API status // Check CITES API status
const { const {
@ -53,8 +54,10 @@ export default function ApiStatus({ citesToken }: ApiStatusProps) {
setIucnStatus('checking'); setIucnStatus('checking');
} else if (iucnApiData?.connected) { } else if (iucnApiData?.connected) {
setIucnStatus('connected'); setIucnStatus('connected');
setIucnApiVersion((iucnApiData as any)?.apiVersion || 'v3'); // Use v3 as default if not specified
} else { } else {
setIucnStatus('error'); setIucnStatus('error');
setIucnApiVersion(null);
} }
}, [isIucnLoading, iucnApiData]); }, [isIucnLoading, iucnApiData]);
@ -90,13 +93,17 @@ export default function ApiStatus({ citesToken }: ApiStatusProps) {
variant={iucnStatus === 'connected' ? 'default' : 'destructive'} variant={iucnStatus === 'connected' ? 'default' : 'destructive'}
className="text-xs px-2 py-0.5" className="text-xs px-2 py-0.5"
> >
IUCN API: {iucnStatus === 'checking' ? 'Checking...' : iucnStatus === 'connected' ? 'Connected' : 'Not Connected'} IUCN API: {iucnStatus === 'checking'
? 'Checking...'
: iucnStatus === 'connected'
? `Connected (${iucnApiVersion})`
: 'Not Connected'}
</Badge> </Badge>
</div> </div>
</TooltipTrigger> </TooltipTrigger>
<TooltipContent side="bottom"> <TooltipContent side="bottom">
{iucnStatus === 'connected' {iucnStatus === 'connected'
? 'IUCN Red List API is connected and working' ? `IUCN Red List API ${iucnApiVersion} is connected and working`
: iucnStatus === 'checking' : iucnStatus === 'checking'
? 'Checking IUCN Red List API connection...' ? 'Checking IUCN Red List API connection...'
: iucnApiData?.message || 'IUCN Red List API connection issue. Check the API key.'} : iucnApiData?.message || 'IUCN Red List API connection issue. Check the API key.'}

View File

@ -1,5 +1,5 @@
import { useState } from "react"; import { useState, useEffect } from "react";
import { useMutation } from "@tanstack/react-query"; import { useMutation, useQuery } from "@tanstack/react-query";
import { queryClient } from "@/lib/queryClient"; import { queryClient } from "@/lib/queryClient";
import { apiClient } from "@/lib/api"; import { apiClient } from "@/lib/api";
import { useToast } from "@/hooks/use-toast"; import { useToast } from "@/hooks/use-toast";
@ -7,6 +7,7 @@ import { Card, CardContent } from "@/components/ui/card";
import { Button } from "@/components/ui/button"; import { Button } from "@/components/ui/button";
import { Input } from "@/components/ui/input"; import { Input } from "@/components/ui/input";
import { Label } from "@/components/ui/label"; import { Label } from "@/components/ui/label";
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
interface AuthenticationPanelProps { interface AuthenticationPanelProps {
token: string; token: string;
@ -15,24 +16,43 @@ interface AuthenticationPanelProps {
export default function AuthenticationPanel({ token, onSave }: AuthenticationPanelProps) { export default function AuthenticationPanel({ token, onSave }: AuthenticationPanelProps) {
const { toast } = useToast(); const { toast } = useToast();
const [apiToken, setApiToken] = useState(token || ""); const [citesToken, setCitesToken] = useState(token || "");
const [iucnToken, setIucnToken] = useState("");
const [activeTab, setActiveTab] = useState("cites");
// Fetch both tokens from the server
const { data: tokenData } = useQuery({
queryKey: ['/api/token'],
queryFn: async () => {
return await apiClient.getTokens();
},
enabled: true, // Always fetch the tokens
});
// Update local state when token data is fetched
useEffect(() => {
if (tokenData) {
setCitesToken(tokenData.token || "");
setIucnToken(tokenData.iucnToken || "");
}
}, [tokenData]);
const saveTokenMutation = useMutation({ const saveTokenMutation = useMutation({
mutationFn: async (token: string) => { mutationFn: async (params: { citesToken: string; iucnToken?: string }) => {
return await apiClient.saveToken(token); return await apiClient.saveToken(params.citesToken, params.iucnToken);
}, },
onSuccess: (response) => { onSuccess: (response) => {
if (response.success) { if (response.success) {
queryClient.invalidateQueries({ queryKey: ['/api/token'] }); queryClient.invalidateQueries({ queryKey: ['/api/token'] });
toast({ toast({
title: "Success", title: "Success",
description: "API token has been saved successfully", description: "API tokens have been saved successfully",
}); });
onSave(); onSave();
} else { } else {
toast({ toast({
title: "Error", title: "Error",
description: response.message || "Failed to save API token", description: response.message || "Failed to save API tokens",
variant: "destructive", variant: "destructive",
}); });
} }
@ -40,29 +60,36 @@ export default function AuthenticationPanel({ token, onSave }: AuthenticationPan
onError: () => { onError: () => {
toast({ toast({
title: "Error", title: "Error",
description: "Failed to save API token", description: "Failed to save API tokens",
variant: "destructive", variant: "destructive",
}); });
} }
}); });
const handleSaveToken = () => { const handleSaveTokens = () => {
if (!apiToken.trim()) { if (!citesToken.trim()) {
toast({ toast({
title: "Validation Error", title: "Validation Error",
description: "Please enter an API token", description: "Please enter a CITES API token",
variant: "destructive", variant: "destructive",
}); });
return; return;
} }
saveTokenMutation.mutate(apiToken); saveTokenMutation.mutate({
citesToken,
iucnToken: iucnToken.trim() ? iucnToken : undefined
});
}; };
const handleGetNewToken = () => { const handleGetCitesToken = () => {
window.open("https://api.speciesplus.net/", "_blank"); window.open("https://api.speciesplus.net/", "_blank");
}; };
const handleGetIucnToken = () => {
window.open("https://apiv4.iucnredlist.org/api/v4/docs", "_blank");
};
return ( return (
<Card> <Card>
<CardContent className="pt-4"> <CardContent className="pt-4">
@ -81,37 +108,77 @@ export default function AuthenticationPanel({ token, onSave }: AuthenticationPan
</svg> </svg>
API Authentication API Authentication
</h2> </h2>
<div className="mb-4">
<Label htmlFor="apiToken" className="block text-sm font-medium text-gray-700 mb-1"> <Tabs value={activeTab} onValueChange={setActiveTab} className="mb-4">
CITES+ API Token <TabsList className="grid grid-cols-2">
</Label> <TabsTrigger value="cites">CITES+ API</TabsTrigger>
<Input <TabsTrigger value="iucn">IUCN Red List API</TabsTrigger>
type="password" </TabsList>
id="apiToken"
className="w-full" <TabsContent value="cites" className="pt-4">
placeholder="Enter your API token" <div className="mb-4">
value={apiToken} <Label htmlFor="citesToken" className="block text-sm font-medium text-gray-700 mb-1">
onChange={(e) => setApiToken(e.target.value)} CITES+ API Token (Required)
/> </Label>
</div> <Input
<div className="flex justify-between"> type="password"
id="citesToken"
className="w-full"
placeholder="Enter your CITES API token"
value={citesToken}
onChange={(e) => setCitesToken(e.target.value)}
/>
<div className="mt-2 text-xs text-gray-500">
The CITES+ API provides species listings, distributions, and legislation information.
</div>
<Button
variant="link"
className="text-primary text-sm px-0 py-1"
onClick={handleGetCitesToken}
>
Get a CITES+ API token
</Button>
</div>
</TabsContent>
<TabsContent value="iucn" className="pt-4">
<div className="mb-4">
<Label htmlFor="iucnToken" className="block text-sm font-medium text-gray-700 mb-1">
IUCN Red List API v4 Token (Optional)
</Label>
<Input
type="password"
id="iucnToken"
className="w-full"
placeholder="Enter your IUCN API token"
value={iucnToken}
onChange={(e) => setIucnToken(e.target.value)}
/>
<div className="mt-2 text-xs text-gray-500">
The IUCN Red List API provides conservation status, threats, habitats, and conservation measures.
</div>
<Button
variant="link"
className="text-primary text-sm px-0 py-1"
onClick={handleGetIucnToken}
>
Get an IUCN Red List API token
</Button>
</div>
</TabsContent>
</Tabs>
<div className="flex justify-between items-center">
<Button <Button
className="bg-primary text-white" className="bg-primary text-white"
onClick={handleSaveToken} onClick={handleSaveTokens}
disabled={saveTokenMutation.isPending} disabled={saveTokenMutation.isPending}
> >
{saveTokenMutation.isPending ? "Saving..." : "Save Token"} {saveTokenMutation.isPending ? "Saving..." : "Save Tokens"}
</Button> </Button>
<Button <div className="text-xs text-gray-500">
variant="link" Both tokens are stored securely.
className="text-primary text-sm" </div>
onClick={handleGetNewToken}
>
Need a token?
</Button>
</div>
<div className="mt-3 text-xs text-gray-500">
Your token is stored securely and is only sent to the CITES+ API.
</div> </div>
</CardContent> </CardContent>
</Card> </Card>

View File

@ -195,9 +195,13 @@ export const apiClient = {
}, },
// Token management // Token management
async saveToken(token: string): Promise<ApiResponse<any>> { async saveToken(token: string, iucnToken?: string): Promise<ApiResponse<any>> {
try { try {
const response = await axios.post<ApiResponse<any>>("/api/token", { token, isActive: true }); const response = await axios.post<ApiResponse<any>>("/api/token", {
token,
iucnToken,
isActive: true
});
return response.data; return response.data;
} catch (error: any) { } catch (error: any) {
return { return {
@ -207,6 +211,16 @@ export const apiClient = {
} }
}, },
async getTokens(): Promise<TokenResponse> {
try {
const response = await axios.get<TokenResponse>("/api/token");
return response.data;
} catch (error) {
console.error("Failed to retrieve API tokens:", error);
return { token: null, iucnToken: null };
}
},
async getToken(): Promise<string | null> { async getToken(): Promise<string | null> {
try { try {
const response = await axios.get<TokenResponse>("/api/token"); const response = await axios.get<TokenResponse>("/api/token");

View File

@ -14,9 +14,15 @@ export default function Home() {
const [showAuthPanel, setShowAuthPanel] = useState(false); const [showAuthPanel, setShowAuthPanel] = useState(false);
const [selectedSpecies, setSelectedSpecies] = useState<any>(null); const [selectedSpecies, setSelectedSpecies] = useState<any>(null);
// Get the active token // Get the active tokens
const { data: tokenData } = useQuery({ const { data: tokensData } = useQuery({
queryKey: ['/api/token'], queryKey: ['/api/token'],
queryFn: async () => await apiClient.getTokens(),
initialData: { token: null, iucnToken: null },
});
const { data: citesTokenData } = useQuery({
queryKey: ['/api/token/cites'],
queryFn: async () => await apiClient.getToken(), queryFn: async () => await apiClient.getToken(),
initialData: null, initialData: null,
}); });
@ -88,7 +94,7 @@ export default function Home() {
</svg> </svg>
<h1 className="text-xl font-bold">CITES+ Species Lookup</h1> <h1 className="text-xl font-bold">CITES+ Species Lookup</h1>
<div className="ml-6"> <div className="ml-6">
<ApiStatus citesToken={tokenData || null} /> <ApiStatus citesToken={tokensData.token || null} />
</div> </div>
</div> </div>
<div> <div>
@ -120,13 +126,13 @@ export default function Home() {
<div className="w-full md:w-1/3 space-y-4"> <div className="w-full md:w-1/3 space-y-4">
{showAuthPanel && ( {showAuthPanel && (
<AuthenticationPanel <AuthenticationPanel
token={tokenData || ""} token={tokensData.token || ""}
onSave={() => setShowAuthPanel(false)} onSave={() => setShowAuthPanel(false)}
/> />
)} )}
<SearchForm <SearchForm
token={tokenData || null} token={tokensData.token || null}
onSpeciesFound={setSelectedSpecies} onSpeciesFound={setSelectedSpecies}
/> />