/* * ======= • ======= • ======= • ======= • =======• ======= * AniKotoAPI — server.js * Repository: https://github.com/Shineii86/AniKotoAPI * * @description * Main entry point for the AniKotoAPI Express server. * Configures CORS, middleware, static files, API routes, * and 404 handling. Starts the server on the configured port. * * @exports * None (side-effect: starts Express server) * * @author Shinei Nouzen * @license MIT * ======= • ======= • ======= • ======= • =======• ======= */ import dotenv from "dotenv"; import express from "express"; import cors from "cors"; import path from "path"; import fs from "fs"; import { fileURLToPath } from "url"; import { dirname } from "path"; import { createApiRoutes } from "./src/routes/apiRoutes.js"; dotenv.config(); // ══════════════════════════════════════════════════════════════ // SERVER CONFIGURATION // ══════════════════════════════════════════════════════════════ const app = express(); const PORT = process.env.PORT || 4444; const __filename = fileURLToPath(import.meta.url); const publicDir = path.join(process.cwd(), "public"); const allowedOrigins = process.env.ALLOWED_ORIGINS?.split(","); // ══════════════════════════════════════════════════════════════ // CORS MIDDLEWARE // ══════════════════════════════════════════════════════════════ // NOTE: CORS is configured to only allow GET requests from specified origins app.use( cors({ origin: allowedOrigins?.includes("*") ? "*" : allowedOrigins || [], methods: ["GET"], }) ); // ---- FEATURE: Custom origin validation middleware ---- app.use((req, res, next) => { const origin = req.headers.origin; if ( !allowedOrigins || allowedOrigins.includes("*") || (origin && allowedOrigins.includes(origin)) ) { res.setHeader("Access-Control-Allow-Origin", origin || "*"); res.setHeader("Access-Control-Allow-Methods", "GET"); res.setHeader("Access-Control-Allow-Headers", "Content-Type"); return next(); } res .status(403) .json({ success: false, message: "Forbidden: Origin not allowed" }); }); // ---- FEATURE: Security headers ---- app.use((req, res, next) => { res.setHeader("X-Frame-Options", "DENY"); res.setHeader("X-Content-Type-Options", "nosniff"); res.setHeader("X-XSS-Protection", "1; mode=block"); res.setHeader("Referrer-Policy", "strict-origin-when-cross-origin"); res.setHeader("Permissions-Policy", "camera=(), microphone=(), geolocation=()"); next(); }); // ══════════════════════════════════════════════════════════════ // STATIC FILES // ══════════════════════════════════════════════════════════════ // NOTE: redirect: false prevents automatic redirects to index.html app.use(express.static(publicDir, { redirect: false })); // ══════════════════════════════════════════════════════════════ // CLEAN URL ROUTES // ══════════════════════════════════════════════════════════════ // Serve HTML files without .html extension app.get("/tos", (req, res) => { res.sendFile(path.join(publicDir, "tos.html")); }); app.get("/privacy", (req, res) => { res.sendFile(path.join(publicDir, "privacy.html")); }); // ══════════════════════════════════════════════════════════════ // RESPONSE HELPERS // ══════════════════════════════════════════════════════════════ // ---- FEATURE: Standardized JSON response wrapper ---- /** * Wraps data in a standardized success JSON response. * * @param {object} res - Express response object * @param {*} data - The data to return in the response * @param {number} status - HTTP status code (default: 200) */ const jsonResponse = (res, data, status = 200) => res.status(status).json({ success: true, results: data }); // ---- FEATURE: Standardized error response wrapper ---- /** * Returns a standardized error JSON response. * * @param {object} res - Express response object * @param {string} message - Error message to return (default: "Internal server error") * @param {number} status - HTTP status code (default: 500) */ const jsonError = (res, message = "Internal server error", status = 500) => res.status(status).json({ success: false, message }); // ══════════════════════════════════════════════════════════════ // API ROUTES // ══════════════════════════════════════════════════════════════ createApiRoutes(app, jsonResponse, jsonError); // ══════════════════════════════════════════════════════════════ // 404 HANDLER // ══════════════════════════════════════════════════════════════ // ---- FEATURE: Catch-all 404 handler for undefined routes ---- app.use((req, res) => { const filePath = path.join(publicDir, "404.html"); if (fs.existsSync(filePath)) { res.status(404).sendFile(filePath); } else { res.status(404).json({ success: false, message: "Endpoint not found" }); } }); // ══════════════════════════════════════════════════════════════ // SERVER START // ══════════════════════════════════════════════════════════════ app.listen(PORT, () => { console.info(`AniKotoAPI listening at ${PORT}`); }); // ══════════════════════════════════════════════════════════════ END: server.js