Magnus-SmariSma 052f9fbbf0 Refactor: Prioritize IUCN API key from environment variables.
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/96599b53-5e69-4e62-a096-dfed59479efa.jpg
2025-03-20 23:27:41 +00:00

731 lines
23 KiB
TypeScript

import type { Express, Request, Response, NextFunction } from "express";
import { createServer, type Server } from "http";
import { storage } from "./storage";
import axios from "axios";
import { insertSpeciesSchema, insertSearchSchema, insertApiTokenSchema } from "@shared/schema";
import { ZodError } from "zod";
import { fromZodError } from "zod-validation-error";
const CITES_BASE_URL = "https://api.speciesplus.net/api/v1";
export async function registerRoutes(app: Express): Promise<Server> {
// API Token routes
app.post("/api/token", async (req: Request, res: Response) => {
try {
const tokenData = insertApiTokenSchema.parse(req.body);
let citesValid = false;
let iucnValid = false;
let warnings = [];
// Validate the CITES token by making a test request to CITES API
try {
await axios.get(`${CITES_BASE_URL}/taxon_concepts`, {
headers: {
"X-Authentication-Token": tokenData.token
}
});
citesValid = true;
} catch (error) {
return res.status(400).json({
success: false,
message: "Invalid CITES API token. Please check and try again."
});
}
// If IUCN token is provided, try to validate it
if (tokenData.iucnToken) {
try {
await axios.get("https://apiv4.iucnredlist.org/api/v4/version", {
headers: {
"Authorization": `Bearer ${tokenData.iucnToken}`
}
});
iucnValid = true;
} catch (iucnError) {
warnings.push("The IUCN v4 token could not be validated. Please check your token and try again.");
// Don't store the invalid token
tokenData.iucnToken = null;
}
}
// Save the validated token
const savedToken = await storage.saveApiToken(tokenData);
// Return appropriate response based on validation results
if (warnings.length > 0) {
return res.json({
success: true,
token: savedToken,
warnings,
message: "CITES API token validated and saved. The IUCN v4 token validation failed."
});
}
res.json({
success: true,
token: savedToken,
message: tokenData.iucnToken
? "Both CITES and IUCN API tokens validated and saved successfully!"
: "CITES API token validated and saved successfully!"
});
} catch (error) {
if (error instanceof ZodError) {
return res.status(400).json({
success: false,
message: fromZodError(error).message
});
}
console.error("Error saving API token:", error);
res.status(500).json({
success: false,
message: "Failed to save API token"
});
}
});
app.get("/api/token", async (req: Request, res: Response) => {
try {
const token = await storage.getActiveToken();
res.json({
token: token?.token || null,
iucnToken: token?.iucnToken || null
});
} catch (error) {
res.status(500).json({
success: false,
message: "Failed to retrieve API token"
});
}
});
// Species search routes
app.get("/api/species/search", async (req: Request, res: Response) => {
try {
const { query, format = "json" } = req.query;
if (!query) {
return res.status(400).json({
success: false,
message: "Search query is required"
});
}
// Get the API token
const tokenData = await storage.getActiveToken();
if (!tokenData) {
return res.status(401).json({
success: false,
message: "API token is not configured. Please set your CITES+ API token."
});
}
// Record the search query
await storage.addSearch({ query: query.toString() });
// Make request to CITES API
try {
const response = await axios.get(`${CITES_BASE_URL}/taxon_concepts.${format}`, {
params: { name: query },
headers: {
"X-Authentication-Token": tokenData.token
}
});
res.json({
success: true,
data: response.data
});
} catch (error) {
if (axios.isAxiosError(error) && error.response) {
return res.status(error.response.status).json({
success: false,
message: error.response.data?.message || "Error from CITES+ API",
status: error.response.status
});
}
throw error;
}
} catch (error) {
res.status(500).json({
success: false,
message: "Failed to search species"
});
}
});
app.get("/api/species/:id", async (req: Request, res: Response) => {
try {
const { id } = req.params;
const { endpoint, format = "json" } = req.query;
const tokenData = await storage.getActiveToken();
if (!tokenData) {
return res.status(401).json({
success: false,
message: "API token is not configured"
});
}
// Make request to CITES API for specific data about the taxon
try {
let apiUrl = `${CITES_BASE_URL}/taxon_concepts/${id}`;
if (endpoint) {
apiUrl += `/${endpoint}`;
}
apiUrl += `.${format}`;
const response = await axios.get(apiUrl, {
headers: {
"X-Authentication-Token": tokenData.token
}
});
res.json({
success: true,
data: response.data
});
} catch (error) {
if (axios.isAxiosError(error) && error.response) {
return res.status(error.response.status).json({
success: false,
message: error.response.data?.message || "Error from CITES+ API",
status: error.response.status
});
}
throw error;
}
} catch (error) {
res.status(500).json({
success: false,
message: "Failed to get species details"
});
}
});
// Save species to database
app.post("/api/species", async (req: Request, res: Response) => {
try {
const speciesData = insertSpeciesSchema.parse(req.body);
const savedSpecies = await storage.saveSpecies(speciesData);
res.json({ success: true, species: savedSpecies });
} catch (error) {
if (error instanceof ZodError) {
return res.status(400).json({
success: false,
message: fromZodError(error).message
});
}
res.status(500).json({
success: false,
message: "Failed to save species data"
});
}
});
// Get all saved species
app.get("/api/species", async (req: Request, res: Response) => {
try {
const allSpecies = await storage.getAllSpecies();
res.json({ success: true, species: allSpecies });
} catch (error) {
res.status(500).json({
success: false,
message: "Failed to retrieve saved species"
});
}
});
// Get recent searches
app.get("/api/searches", async (req: Request, res: Response) => {
try {
const { limit = 10 } = req.query;
const recentSearches = await storage.getRecentSearches(Number(limit));
res.json({ success: true, searches: recentSearches });
} catch (error) {
res.status(500).json({
success: false,
message: "Failed to retrieve recent searches"
});
}
});
// CITES API Status check endpoint
app.get("/api/cites/status", async (req: Request, res: Response) => {
try {
const activeToken = await storage.getActiveToken();
if (!activeToken || !activeToken.token) {
return res.status(401).json({
success: false,
connected: false,
message: "CITES API token is not configured"
});
}
try {
// Make a request to the CITES API to verify the token
const response = await axios.get(`${CITES_BASE_URL}/taxon_concepts/search`, {
params: {
page: 1,
per_page: 1
},
headers: {
'X-Authentication-Token': activeToken.token
}
});
res.json({
success: true,
connected: true,
message: "CITES API is connected and responding"
});
} catch (error) {
if (axios.isAxiosError(error) && error.response) {
return res.status(error.response.status).json({
success: false,
connected: false,
message: "Failed to connect to CITES API or invalid token",
status: error.response.status
});
}
throw error;
}
} catch (error) {
res.status(500).json({
success: false,
connected: false,
message: "Error checking CITES API status"
});
}
});
// IUCN API Status check endpoint - v4 only
app.get("/api/iucn/status", async (req: Request, res: Response) => {
try {
// First try to use environment variable for API key
let iucnToken = process.env.IUCN_API_KEY || null;
// If not available in env, try to get from storage
if (!iucnToken) {
const activeToken = await storage.getActiveToken();
iucnToken = activeToken?.iucnToken || null;
}
if (!iucnToken) {
return res.status(401).json({
success: false,
connected: false,
message: "IUCN API v4 token is not configured. Please set your token in the API Token panel."
});
}
try {
// Check if the API is working by hitting the version endpoint
const response = await axios.get("https://apiv4.iucnredlist.org/api/v4/version", {
headers: {
"Authorization": `Bearer ${iucnToken}`
}
});
console.log("IUCN API v4 version check response:", response.data);
return res.json({
success: true,
connected: true,
apiVersion: "v4",
message: "IUCN API v4 is connected and responding"
});
} catch (error: any) {
console.log("IUCN API check failed:", error.message);
// Return useful error messages
if (error.response?.status === 401) {
return res.status(401).json({
success: false,
connected: false,
message: "IUCN API v4 token is invalid. Please check your token and try again."
});
}
// For network errors or other issues
return res.status(error.response?.status || 500).json({
success: false,
connected: false,
message: "Failed to connect to IUCN API: " + (error.response?.data?.message || error.message)
});
}
} catch (error) {
const errorMessage = error instanceof Error ? error.message : String(error);
res.status(500).json({
success: false,
connected: false,
message: "Error checking IUCN API status: " + errorMessage
});
}
});
// IUCN Red List API routes
app.get("/api/iucn/species", async (req: Request, res: Response) => {
try {
const { name } = req.query;
if (!name) {
return res.status(400).json({
success: false,
message: "Species name is required"
});
}
// Extract genus and species for v4 API
const nameParts = String(name).split(' ');
const [genusName, speciesName] = nameParts;
// Try to get IUCN token from environment variable first
let iucnToken = process.env.IUCN_API_KEY || null;
// If not available in env, try to get from storage
if (!iucnToken) {
const activeToken = await storage.getActiveToken();
iucnToken = activeToken?.iucnToken || null;
}
if (!iucnToken) {
return res.status(401).json({
success: false,
message: "IUCN API v4 token is not configured. Please set your token in the API Token panel."
});
}
try {
// Use the v4 API with scientific name endpoint
const response = await axios.get("https://apiv4.iucnredlist.org/api/v4/taxa/scientific_name", {
headers: {
"Authorization": `Bearer ${iucnToken}`
},
params: {
genus_name: genusName,
species_name: speciesName || ""
}
});
// Debug the response structure
console.log("IUCN API response structure:",
Object.keys(response.data),
response.data.result ? `Result has ${response.data.result.length} items` : "No result property");
return res.json({
success: true,
data: response.data,
apiVersion: "v4"
});
} catch (error: any) {
console.log("IUCN API species lookup failed:", error.message);
if (error.response?.status === 401) {
return res.status(401).json({
success: false,
message: "IUCN API v4 token is invalid. Please check your token and try again."
});
}
return res.status(error.response?.status || 500).json({
success: false,
message: error.response?.data?.message || "Error from IUCN Red List API: " + error.message,
status: error.response?.status
});
}
} catch (error) {
const errorMessage = error instanceof Error ? error.message : String(error);
res.status(500).json({
success: false,
message: "Failed to retrieve IUCN species data: " + errorMessage
});
}
});
app.get("/api/iucn/threats", async (req: Request, res: Response) => {
try {
const { name } = req.query;
if (!name) {
return res.status(400).json({
success: false,
message: "Species name is required"
});
}
// Extract genus and species for v4 API query
const nameParts = String(name).split(' ');
const [genusName, speciesName] = nameParts;
// Try to get IUCN token from environment variable first
let iucnToken = process.env.IUCN_API_KEY || null;
// If not available in env, try to get from storage
if (!iucnToken) {
const activeToken = await storage.getActiveToken();
iucnToken = activeToken?.iucnToken || null;
}
if (!iucnToken) {
return res.status(401).json({
success: false,
message: "IUCN API v4 token is not configured. Please set your token in the API Token panel."
});
}
try {
// First, we need to find the species taxon ID using scientific name lookup
const taxaResponse = await axios.get("https://apiv4.iucnredlist.org/api/v4/taxa/scientific_name", {
headers: {
"Authorization": `Bearer ${iucnToken}`
},
params: {
genus_name: genusName,
species_name: speciesName || ""
}
});
// Check if we found the species
if (!taxaResponse.data?.result || taxaResponse.data.result.length === 0) {
return res.status(404).json({
success: false,
message: `No species found with the name "${name}" in the IUCN Red List database.`
});
}
const taxonId = taxaResponse.data.result[0].taxonid;
// Now retrieve the threats using the taxon ID
const threatsResponse = await axios.get(`https://apiv4.iucnredlist.org/api/v4/threats`, {
headers: {
"Authorization": `Bearer ${iucnToken}`
},
params: {
taxonid: taxonId
}
});
// Debug the response structure
console.log("IUCN Threats API response structure:",
Object.keys(threatsResponse.data),
threatsResponse.data.result ? `Result has ${threatsResponse.data.result.length} items` : "No result property");
return res.json({
success: true,
data: threatsResponse.data,
apiVersion: "v4"
});
} catch (error: any) {
console.log("IUCN API threats lookup failed:", error.message);
if (error.response?.status === 401) {
return res.status(401).json({
success: false,
message: "IUCN API v4 token is invalid. Please check your token and try again."
});
}
return res.status(error.response?.status || 500).json({
success: false,
message: error.response?.data?.message || "Error from IUCN Red List API: " + error.message,
status: error.response?.status
});
}
} catch (error) {
const errorMessage = error instanceof Error ? error.message : String(error);
res.status(500).json({
success: false,
message: "Failed to retrieve IUCN threats data: " + errorMessage
});
}
});
app.get("/api/iucn/habitats", async (req: Request, res: Response) => {
try {
const { name } = req.query;
if (!name) {
return res.status(400).json({
success: false,
message: "Species name is required"
});
}
// Extract genus and species for v4 API query
const nameParts = String(name).split(' ');
const [genusName, speciesName] = nameParts;
// Get IUCN token
const activeToken = await storage.getActiveToken();
if (!activeToken?.iucnToken) {
return res.status(401).json({
success: false,
message: "IUCN API v4 token is not configured. Please set your token in the API Token panel."
});
}
try {
// First, we need to find the species taxon ID using scientific name lookup
const taxaResponse = await axios.get("https://apiv4.iucnredlist.org/api/v4/taxa/scientific_name", {
headers: {
"Authorization": `Bearer ${activeToken.iucnToken}`
},
params: {
genus_name: genusName,
species_name: speciesName || ""
}
});
// Check if we found the species
if (!taxaResponse.data?.result || taxaResponse.data.result.length === 0) {
return res.status(404).json({
success: false,
message: `No species found with the name "${name}" in the IUCN Red List database.`
});
}
const taxonId = taxaResponse.data.result[0].taxonid;
// Now retrieve the habitats using the taxon ID
const habitatsResponse = await axios.get(`https://apiv4.iucnredlist.org/api/v4/habitats`, {
headers: {
"Authorization": `Bearer ${activeToken.iucnToken}`
},
params: {
taxonid: taxonId
}
});
// Debug the response structure
console.log("IUCN Habitats API response structure:",
Object.keys(habitatsResponse.data),
habitatsResponse.data.result ? `Result has ${habitatsResponse.data.result.length} items` : "No result property");
return res.json({
success: true,
data: habitatsResponse.data,
apiVersion: "v4"
});
} catch (error: any) {
console.log("IUCN API habitats lookup failed:", error.message);
if (error.response?.status === 401) {
return res.status(401).json({
success: false,
message: "IUCN API v4 token is invalid. Please check your token and try again."
});
}
return res.status(error.response?.status || 500).json({
success: false,
message: error.response?.data?.message || "Error from IUCN Red List API: " + error.message,
status: error.response?.status
});
}
} catch (error) {
const errorMessage = error instanceof Error ? error.message : String(error);
res.status(500).json({
success: false,
message: "Failed to retrieve IUCN habitats data: " + errorMessage
});
}
});
app.get("/api/iucn/measures", async (req: Request, res: Response) => {
try {
const { name } = req.query;
if (!name) {
return res.status(400).json({
success: false,
message: "Species name is required"
});
}
// Extract genus and species for v4 API query
const nameParts = String(name).split(' ');
const [genusName, speciesName] = nameParts;
// Get IUCN token
const activeToken = await storage.getActiveToken();
if (!activeToken?.iucnToken) {
return res.status(401).json({
success: false,
message: "IUCN API v4 token is not configured. Please set your token in the API Token panel."
});
}
try {
// First, we need to find the species taxon ID using scientific name lookup
const taxaResponse = await axios.get("https://apiv4.iucnredlist.org/api/v4/taxa/scientific_name", {
headers: {
"Authorization": `Bearer ${activeToken.iucnToken}`
},
params: {
genus_name: genusName,
species_name: speciesName || ""
}
});
// Check if we found the species
if (!taxaResponse.data?.result || taxaResponse.data.result.length === 0) {
return res.status(404).json({
success: false,
message: `No species found with the name "${name}" in the IUCN Red List database.`
});
}
const taxonId = taxaResponse.data.result[0].taxonid;
// Now retrieve the conservation measures using the taxon ID
const measuresResponse = await axios.get(`https://apiv4.iucnredlist.org/api/v4/measures`, {
headers: {
"Authorization": `Bearer ${activeToken.iucnToken}`
},
params: {
taxonid: taxonId
}
});
// Debug the response structure
console.log("IUCN Measures API response structure:",
Object.keys(measuresResponse.data),
measuresResponse.data.result ? `Result has ${measuresResponse.data.result.length} items` : "No result property");
return res.json({
success: true,
data: measuresResponse.data,
apiVersion: "v4"
});
} catch (error: any) {
console.log("IUCN API conservation measures lookup failed:", error.message);
if (error.response?.status === 401) {
return res.status(401).json({
success: false,
message: "IUCN API v4 token is invalid. Please check your token and try again."
});
}
return res.status(error.response?.status || 500).json({
success: false,
message: error.response?.data?.message || "Error from IUCN Red List API: " + error.message,
status: error.response?.status
});
}
} catch (error) {
const errorMessage = error instanceof Error ? error.message : String(error);
res.status(500).json({
success: false,
message: "Failed to retrieve IUCN conservation measures data: " + errorMessage
});
}
});
const httpServer = createServer(app);
return httpServer;
}