
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/f9ca419d-567c-41d0-a14d-f345868599e8.jpg
729 lines
23 KiB
TypeScript
729 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";
|
|
|
|
// IUCN API versions
|
|
const IUCN_V3_BASE_URL = "https://apiv3.iucnredlist.org/api/v3";
|
|
const IUCN_V4_BASE_URL = "https://apiv4.iucnredlist.org/api/v4";
|
|
|
|
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);
|
|
const savedToken = await storage.saveApiToken(tokenData);
|
|
|
|
// 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
|
|
}
|
|
});
|
|
|
|
// If IUCN token is provided, validate it as well
|
|
if (tokenData.iucnToken) {
|
|
try {
|
|
await axios.get(`${IUCN_V4_BASE_URL}/version`, {
|
|
headers: {
|
|
"Authorization": `Bearer ${tokenData.iucnToken}`
|
|
}
|
|
});
|
|
} catch (iucnError) {
|
|
return res.status(400).json({
|
|
success: false,
|
|
message: "Invalid IUCN API token. Please check and try again."
|
|
});
|
|
}
|
|
}
|
|
|
|
res.json({ success: true, token: savedToken });
|
|
} catch (error) {
|
|
if (axios.isAxiosError(error) && error.response) {
|
|
return res.status(400).json({
|
|
success: false,
|
|
message: "Invalid CITES API token. Please check and try again."
|
|
});
|
|
}
|
|
throw error;
|
|
}
|
|
} 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 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
|
|
app.get("/api/iucn/status", async (req: Request, res: Response) => {
|
|
try {
|
|
// Try V4 API first (with bearer token)
|
|
const activeToken = await storage.getActiveToken();
|
|
if (activeToken?.iucnToken) {
|
|
// Use the version endpoint which is the simplest endpoint
|
|
const versionUrl = `${IUCN_V4_BASE_URL}/version`;
|
|
|
|
try {
|
|
const response = await axios.get(versionUrl, {
|
|
headers: {
|
|
"Authorization": `Bearer ${activeToken.iucnToken}`
|
|
}
|
|
});
|
|
|
|
return res.json({
|
|
success: true,
|
|
connected: true,
|
|
apiVersion: "v4",
|
|
message: "IUCN API v4 is connected and responding"
|
|
});
|
|
} catch (error: any) {
|
|
// If V4 fails, fall back to checking V3 with environment API key
|
|
console.log("IUCN V4 API check failed, falling back to V3:", error.message);
|
|
}
|
|
}
|
|
|
|
// Fallback to V3 API (with query parameter token)
|
|
const apiKey = process.env.IUCN_API_KEY;
|
|
if (!apiKey) {
|
|
return res.status(401).json({
|
|
success: false,
|
|
connected: false,
|
|
message: "IUCN API key is not configured"
|
|
});
|
|
}
|
|
|
|
// Use the version endpoint for V3
|
|
const versionUrl = `${IUCN_V3_BASE_URL}/version`;
|
|
|
|
try {
|
|
// Make sure we're not including any parameters in the URL itself - only in params object
|
|
const response = await axios.get(versionUrl, {
|
|
params: { token: apiKey }
|
|
});
|
|
|
|
res.json({
|
|
success: true,
|
|
connected: true,
|
|
apiVersion: "v3",
|
|
message: "IUCN API v3 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 IUCN API: " + (error.response?.data?.message || error.message),
|
|
status: error.response.status
|
|
});
|
|
}
|
|
throw error;
|
|
}
|
|
} 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"
|
|
});
|
|
}
|
|
|
|
// To avoid 414 errors, we'll limit the name parameter to just the genus and species
|
|
// This will extract first two parts of scientific name (genus and species)
|
|
const nameParts = String(name).split(' ');
|
|
const simplifiedName = nameParts.slice(0, 2).join(' ');
|
|
const [genusName, speciesName] = nameParts;
|
|
|
|
// Try with v4 API first if we have a token
|
|
const activeToken = await storage.getActiveToken();
|
|
if (activeToken?.iucnToken) {
|
|
try {
|
|
// Use the v4 API with scientific name endpoint
|
|
const response = await axios.get(`${IUCN_V4_BASE_URL}/taxa/scientific_name`, {
|
|
headers: {
|
|
"Authorization": `Bearer ${activeToken.iucnToken}`
|
|
},
|
|
params: {
|
|
genus_name: genusName,
|
|
species_name: speciesName || ""
|
|
}
|
|
});
|
|
|
|
return res.json({
|
|
success: true,
|
|
data: response.data,
|
|
apiVersion: "v4"
|
|
});
|
|
} catch (error: any) {
|
|
console.log("IUCN V4 API species lookup failed, falling back to V3:", error.message);
|
|
// Continue to v3 fallback
|
|
}
|
|
}
|
|
|
|
// Fallback to v3 API
|
|
const apiKey = process.env.IUCN_API_KEY;
|
|
if (!apiKey) {
|
|
return res.status(401).json({
|
|
success: false,
|
|
connected: false,
|
|
message: "IUCN API key is not configured"
|
|
});
|
|
}
|
|
|
|
try {
|
|
const response = await axios.get(`${IUCN_V3_BASE_URL}/species/name/${encodeURIComponent(simplifiedName)}`, {
|
|
params: { token: apiKey }
|
|
});
|
|
|
|
res.json({
|
|
success: true,
|
|
data: response.data,
|
|
apiVersion: "v3"
|
|
});
|
|
} catch (error) {
|
|
if (axios.isAxiosError(error) && error.response) {
|
|
return res.status(error.response.status).json({
|
|
success: false,
|
|
message: error.response.data?.message || "Error from IUCN Red List API",
|
|
status: error.response.status
|
|
});
|
|
}
|
|
throw error;
|
|
}
|
|
} catch (error) {
|
|
res.status(500).json({
|
|
success: false,
|
|
message: "Failed to retrieve IUCN species data"
|
|
});
|
|
}
|
|
});
|
|
|
|
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"
|
|
});
|
|
}
|
|
|
|
// To avoid 414 errors, we'll limit the name parameter to just the genus and species
|
|
const nameParts = String(name).split(' ');
|
|
const simplifiedName = nameParts.slice(0, 2).join(' ');
|
|
const [genusName, speciesName] = nameParts;
|
|
|
|
// Try with v4 API first if we have a token
|
|
const activeToken = await storage.getActiveToken();
|
|
if (activeToken?.iucnToken) {
|
|
try {
|
|
// First, we need to find the species taxon ID using scientific name lookup
|
|
const taxaResponse = await axios.get(`${IUCN_V4_BASE_URL}/taxa/scientific_name`, {
|
|
headers: {
|
|
"Authorization": `Bearer ${activeToken.iucnToken}`
|
|
},
|
|
params: {
|
|
genus_name: genusName,
|
|
species_name: speciesName || ""
|
|
}
|
|
});
|
|
|
|
// Check if we found the species
|
|
if (taxaResponse.data && taxaResponse.data.result && taxaResponse.data.result.length > 0) {
|
|
const taxonId = taxaResponse.data.result[0].taxonid;
|
|
|
|
// Now retrieve the threats using the taxon ID
|
|
const threatsResponse = await axios.get(`${IUCN_V4_BASE_URL}/threats/species/id/${taxonId}`, {
|
|
headers: {
|
|
"Authorization": `Bearer ${activeToken.iucnToken}`
|
|
}
|
|
});
|
|
|
|
return res.json({
|
|
success: true,
|
|
data: threatsResponse.data,
|
|
apiVersion: "v4"
|
|
});
|
|
}
|
|
// If no species found, fall back to v3
|
|
console.log("IUCN V4 API: No species found with the given scientific name, falling back to V3");
|
|
} catch (error: any) {
|
|
console.log("IUCN V4 API threats lookup failed, falling back to V3:", error.message);
|
|
// Continue to v3 fallback
|
|
}
|
|
}
|
|
|
|
// Fallback to v3 API
|
|
const apiKey = process.env.IUCN_API_KEY;
|
|
if (!apiKey) {
|
|
return res.status(401).json({
|
|
success: false,
|
|
connected: false,
|
|
message: "IUCN API key is not configured"
|
|
});
|
|
}
|
|
|
|
try {
|
|
const response = await axios.get(`${IUCN_V3_BASE_URL}/threats/species/name/${encodeURIComponent(simplifiedName)}`, {
|
|
params: { token: apiKey }
|
|
});
|
|
|
|
res.json({
|
|
success: true,
|
|
data: response.data,
|
|
apiVersion: "v3"
|
|
});
|
|
} catch (error) {
|
|
if (axios.isAxiosError(error) && error.response) {
|
|
return res.status(error.response.status).json({
|
|
success: false,
|
|
message: error.response.data?.message || "Error from IUCN Red List API",
|
|
status: error.response.status
|
|
});
|
|
}
|
|
throw error;
|
|
}
|
|
} catch (error) {
|
|
res.status(500).json({
|
|
success: false,
|
|
message: "Failed to retrieve IUCN threats data"
|
|
});
|
|
}
|
|
});
|
|
|
|
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"
|
|
});
|
|
}
|
|
|
|
// To avoid 414 errors, we'll limit the name parameter to just the genus and species
|
|
const nameParts = String(name).split(' ');
|
|
const simplifiedName = nameParts.slice(0, 2).join(' ');
|
|
const [genusName, speciesName] = nameParts;
|
|
|
|
// Try with v4 API first if we have a token
|
|
const activeToken = await storage.getActiveToken();
|
|
if (activeToken?.iucnToken) {
|
|
try {
|
|
// First, we need to find the species taxon ID using scientific name lookup
|
|
const taxaResponse = await axios.get(`${IUCN_V4_BASE_URL}/taxa/scientific_name`, {
|
|
headers: {
|
|
"Authorization": `Bearer ${activeToken.iucnToken}`
|
|
},
|
|
params: {
|
|
genus_name: genusName,
|
|
species_name: speciesName || ""
|
|
}
|
|
});
|
|
|
|
// Check if we found the species
|
|
if (taxaResponse.data && taxaResponse.data.result && taxaResponse.data.result.length > 0) {
|
|
const taxonId = taxaResponse.data.result[0].taxonid;
|
|
|
|
// Now retrieve the habitats using the taxon ID
|
|
const habitatsResponse = await axios.get(`${IUCN_V4_BASE_URL}/habitats/species/id/${taxonId}`, {
|
|
headers: {
|
|
"Authorization": `Bearer ${activeToken.iucnToken}`
|
|
}
|
|
});
|
|
|
|
return res.json({
|
|
success: true,
|
|
data: habitatsResponse.data,
|
|
apiVersion: "v4"
|
|
});
|
|
}
|
|
// If no species found, fall back to v3
|
|
console.log("IUCN V4 API: No species found with the given scientific name, falling back to V3");
|
|
} catch (error: any) {
|
|
console.log("IUCN V4 API habitats lookup failed, falling back to V3:", error.message);
|
|
// Continue to v3 fallback
|
|
}
|
|
}
|
|
|
|
// Fallback to v3 API
|
|
const apiKey = process.env.IUCN_API_KEY;
|
|
if (!apiKey) {
|
|
return res.status(401).json({
|
|
success: false,
|
|
connected: false,
|
|
message: "IUCN API key is not configured"
|
|
});
|
|
}
|
|
|
|
try {
|
|
const response = await axios.get(`${IUCN_V3_BASE_URL}/habitats/species/name/${encodeURIComponent(simplifiedName)}`, {
|
|
params: { token: apiKey }
|
|
});
|
|
|
|
res.json({
|
|
success: true,
|
|
data: response.data,
|
|
apiVersion: "v3"
|
|
});
|
|
} catch (error) {
|
|
if (axios.isAxiosError(error) && error.response) {
|
|
return res.status(error.response.status).json({
|
|
success: false,
|
|
message: error.response.data?.message || "Error from IUCN Red List API",
|
|
status: error.response.status
|
|
});
|
|
}
|
|
throw error;
|
|
}
|
|
} catch (error) {
|
|
res.status(500).json({
|
|
success: false,
|
|
message: "Failed to retrieve IUCN habitats data"
|
|
});
|
|
}
|
|
});
|
|
|
|
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"
|
|
});
|
|
}
|
|
|
|
// To avoid 414 errors, we'll limit the name parameter to just the genus and species
|
|
const nameParts = String(name).split(' ');
|
|
const simplifiedName = nameParts.slice(0, 2).join(' ');
|
|
const [genusName, speciesName] = nameParts;
|
|
|
|
// Try with v4 API first if we have a token
|
|
const activeToken = await storage.getActiveToken();
|
|
if (activeToken?.iucnToken) {
|
|
try {
|
|
// First, we need to find the species taxon ID using scientific name lookup
|
|
const taxaResponse = await axios.get(`${IUCN_V4_BASE_URL}/taxa/scientific_name`, {
|
|
headers: {
|
|
"Authorization": `Bearer ${activeToken.iucnToken}`
|
|
},
|
|
params: {
|
|
genus_name: genusName,
|
|
species_name: speciesName || ""
|
|
}
|
|
});
|
|
|
|
// Check if we found the species
|
|
if (taxaResponse.data && taxaResponse.data.result && taxaResponse.data.result.length > 0) {
|
|
const taxonId = taxaResponse.data.result[0].taxonid;
|
|
|
|
// Now retrieve the conservation measures using the taxon ID
|
|
const measuresResponse = await axios.get(`${IUCN_V4_BASE_URL}/measures/species/id/${taxonId}`, {
|
|
headers: {
|
|
"Authorization": `Bearer ${activeToken.iucnToken}`
|
|
}
|
|
});
|
|
|
|
return res.json({
|
|
success: true,
|
|
data: measuresResponse.data,
|
|
apiVersion: "v4"
|
|
});
|
|
}
|
|
// If no species found, fall back to v3
|
|
console.log("IUCN V4 API: No species found with the given scientific name, falling back to V3");
|
|
} catch (error: any) {
|
|
console.log("IUCN V4 API conservation measures lookup failed, falling back to V3:", error.message);
|
|
// Continue to v3 fallback
|
|
}
|
|
}
|
|
|
|
// Fallback to v3 API
|
|
const apiKey = process.env.IUCN_API_KEY;
|
|
if (!apiKey) {
|
|
return res.status(401).json({
|
|
success: false,
|
|
connected: false,
|
|
message: "IUCN API key is not configured"
|
|
});
|
|
}
|
|
|
|
try {
|
|
const response = await axios.get(`${IUCN_V3_BASE_URL}/measures/species/name/${encodeURIComponent(simplifiedName)}`, {
|
|
params: { token: apiKey }
|
|
});
|
|
|
|
res.json({
|
|
success: true,
|
|
data: response.data,
|
|
apiVersion: "v3"
|
|
});
|
|
} catch (error) {
|
|
if (axios.isAxiosError(error) && error.response) {
|
|
return res.status(error.response.status).json({
|
|
success: false,
|
|
message: error.response.data?.message || "Error from IUCN Red List API",
|
|
status: error.response.status
|
|
});
|
|
}
|
|
throw error;
|
|
}
|
|
} catch (error) {
|
|
res.status(500).json({
|
|
success: false,
|
|
message: "Failed to retrieve IUCN conservation measures data"
|
|
});
|
|
}
|
|
});
|
|
|
|
const httpServer = createServer(app);
|
|
return httpServer;
|
|
}
|