mirror of
https://github.com/Shineii86/MiruroAPI.git
synced 2026-10-22 10:15:10 +00:00
- Full index.html rewrite with AniKotoAPI-style features: * Canvas particle effects (cyan particles) * Scroll-reveal animations (IntersectionObserver) * Animated stat counters (count up on scroll) * Infinite scrolling marquee (8 feature badges) * Live interactive API console (5 presets: Search, Trending, Episodes, Info, Health) * Sidebar endpoint explorer (16 endpoints, multi-language tabs: cURL/JS/Python) * 4 interactive playground cards (Search, Info, Episodes, Trending) * Custom scrollbar, JetBrains Mono font * 4-column grid footer with API/Resources/Legal * Fixed header with scroll-aware glass blur * Ambient orb gradient animations * CTA section with gradient overlay - Added public/404.html (glitch animation error page) - Added public/manifest.json (PWA, theme: #38bdf8) - Added public/robots.txt (Allow /, Disallow /api) - Added public/sitemap.xml (4 URLs) - Added public/tos.html (13 sections) - Added public/privacy.html (8 sections) - Added .env.example, CONTRIBUTING.md, LICENSE, render.yaml, test.js - Added docs/ (5 files: index, endpoints, streaming, examples, architecture) - Updated server.js: clean URL routes (/tos, /privacy), 404.html fallback - Updated package.json: devDependencies nodemon, repository, dev script - All branding: AniKotoAPI → MiruroAPI, cyan color scheme (#38bdf8)
200 lines
8.7 KiB
JavaScript
200 lines
8.7 KiB
JavaScript
/*
|
|
* ======= • ======= • ======= • ======= • =======• =======
|
|
* MiruroAPI — server.js
|
|
* Repository: https://github.com/Shineii86/MiruroAPI
|
|
*
|
|
* @description
|
|
* Main entry point for the MiruroAPI Express server.
|
|
* Configures CORS, middleware, static files, Swagger docs,
|
|
* API routes, and 404 handling. Starts the server on the
|
|
* configured port.
|
|
*
|
|
* @exports
|
|
* None (side-effect: starts Express server)
|
|
*
|
|
* @author Shinei Nouzen
|
|
* @license MIT
|
|
* ======= • ======= • ======= • ======= • =======• =======
|
|
*/
|
|
|
|
const express = require("express");
|
|
const cors = require("cors");
|
|
const path = require("path");
|
|
require("dotenv").config();
|
|
|
|
// ══════════════════════════════════════════════════════════════
|
|
// SERVER CONFIGURATION
|
|
// ══════════════════════════════════════════════════════════════
|
|
|
|
const app = express();
|
|
const PORT = process.env.PORT || 3000;
|
|
const allowedOrigins = process.env.ALLOWED_ORIGINS
|
|
? process.env.ALLOWED_ORIGINS.split(",")
|
|
: null;
|
|
|
|
// ══════════════════════════════════════════════════════════════
|
|
// CORS MIDDLEWARE
|
|
// ══════════════════════════════════════════════════════════════
|
|
|
|
// NOTE: Single unified CORS middleware — handles all origin validation
|
|
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");
|
|
if (req.method === "OPTIONS") {
|
|
return res.status(204).end();
|
|
}
|
|
next();
|
|
});
|
|
|
|
// ---- 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
|
|
// ══════════════════════════════════════════════════════════════
|
|
|
|
app.use(express.static(path.join(__dirname, "public"), { redirect: false }));
|
|
|
|
// ══════════════════════════════════════════════════════════════
|
|
// CLEAN URL ROUTES
|
|
// ══════════════════════════════════════════════════════════════
|
|
|
|
// ---- FEATURE: Clean URL routes (no .html extension) ----
|
|
const fs = require("fs");
|
|
const publicDir = path.join(__dirname, "public");
|
|
|
|
app.get("/tos", (req, res) => {
|
|
res.sendFile(path.join(publicDir, "tos.html"));
|
|
});
|
|
|
|
app.get("/privacy", (req, res) => {
|
|
res.sendFile(path.join(publicDir, "privacy.html"));
|
|
});
|
|
|
|
// ══════════════════════════════════════════════════════════════
|
|
// SWAGGER UI DOCS
|
|
// ══════════════════════════════════════════════════════════════
|
|
|
|
// ---- FEATURE: Interactive Swagger UI documentation at /docs ----
|
|
app.get("/docs", (req, res) => {
|
|
res.sendFile(path.join(publicDir, "docs.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
|
|
* @param {number} status - HTTP status code (default: 500)
|
|
*/
|
|
const jsonError = (res, message = "Internal server error", status = 500) =>
|
|
res.status(status).json({ success: false, message });
|
|
|
|
// ══════════════════════════════════════════════════════════════
|
|
// RATE LIMITING
|
|
// ══════════════════════════════════════════════════════════════
|
|
|
|
// ---- FEATURE: Rate limiting (100 requests per minute per IP) ----
|
|
const requestCounts = new Map();
|
|
app.use((req, res, next) => {
|
|
const ip = req.ip || req.connection.remoteAddress;
|
|
const now = Date.now();
|
|
const windowMs = 60000;
|
|
const maxRequests = 100;
|
|
|
|
if (!requestCounts.has(ip)) {
|
|
requestCounts.set(ip, []);
|
|
}
|
|
|
|
const timestamps = requestCounts.get(ip).filter((t) => now - t < windowMs);
|
|
requestCounts.set(ip, timestamps);
|
|
|
|
if (timestamps.length >= maxRequests) {
|
|
return res.status(429).json({
|
|
success: false,
|
|
message: "Rate limit exceeded. Try again later.",
|
|
});
|
|
}
|
|
|
|
timestamps.push(now);
|
|
next();
|
|
});
|
|
|
|
// ══════════════════════════════════════════════════════════════
|
|
// API ROUTES
|
|
// ══════════════════════════════════════════════════════════════
|
|
|
|
const apiRoutes = require("./src/routes/apiRoutes");
|
|
app.use("/api", apiRoutes(jsonResponse, jsonError));
|
|
|
|
// ══════════════════════════════════════════════════════════════
|
|
// 404 HANDLER
|
|
// ══════════════════════════════════════════════════════════════
|
|
|
|
// ---- FEATURE: Global error handler ----
|
|
app.use((err, req, res, next) => {
|
|
console.error(err.stack);
|
|
res.status(err.status || 500).json({
|
|
success: false,
|
|
message: err.message || "Internal server error",
|
|
});
|
|
});
|
|
|
|
// ---- FEATURE: Catch-all 404 handler for undefined routes ----
|
|
app.use((req, res) => {
|
|
if (req.path.startsWith("/api/")) {
|
|
return res.status(404).json({
|
|
success: false,
|
|
message: "Endpoint not found",
|
|
});
|
|
}
|
|
const notFoundPath = path.join(publicDir, "404.html");
|
|
if (fs.existsSync(notFoundPath)) {
|
|
return res.status(404).sendFile(notFoundPath);
|
|
}
|
|
res.sendFile(path.join(publicDir, "index.html"));
|
|
});
|
|
|
|
// ══════════════════════════════════════════════════════════════
|
|
// SERVER START
|
|
// ══════════════════════════════════════════════════════════════
|
|
|
|
app.listen(PORT, () => {
|
|
console.info(`MiruroAPI listening at ${PORT}`);
|
|
});
|
|
|
|
module.exports = app;
|
|
|
|
// ══════════════════════════════════════════════════════════════ END: server.js
|