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:
@ -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.'}
|
||||||
|
@ -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>
|
||||||
|
@ -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");
|
||||||
|
@ -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}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
Reference in New Issue
Block a user