From e9b0fba22b3a3f5913939404eda7cb12b819620f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=CA=9C=C9=AA=C9=B4=E1=B4=87=C9=AA=20N=E1=B4=8F=E1=B4=9C?= =?UTF-8?q?=E1=B4=A2=E1=B4=87=C9=B4?= <157171073+Shineii86@users.noreply.github.com> Date: Mon, 8 Jun 2026 22:08:38 +0530 Subject: [PATCH] feat: add health, stats, OpenAPI, security headers, tests MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - /api/health — uptime, version, memory usage - /api/stats — request count, error rate, cache info - /api/openapi — OpenAPI 3.0.3 specification - Security headers: X-Frame-Options, X-Content-Type-Options, X-XSS-Protection - Request counter middleware - CONTRIBUTING.md — contribution guidelines - test.js — integration test suite (18 endpoints) --- CHANGELOG.md | 7 +++ CONTRIBUTING.md | 84 +++++++++++++++++++++++++ server.js | 10 +++ src/routes/apiRoutes.js | 134 ++++++++++++++++++++++++++++++++++++++-- test.js | 70 +++++++++++++++++++++ 5 files changed, 300 insertions(+), 5 deletions(-) create mode 100644 CONTRIBUTING.md create mode 100644 test.js diff --git a/CHANGELOG.md b/CHANGELOG.md index 8d36c7b..9c6ca7a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -14,6 +14,13 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - `docs/streaming.md` — Streaming flow guide (3-step with real data) - `docs/examples.md` — Code examples in cURL, JavaScript, Python, Node.js (all tested) - `docs/architecture.md` — Project structure, tech stack, design decisions +- `/api/health` — Health check endpoint (uptime, version, memory) +- `/api/stats` — Cache & API statistics (requests, errors, success rate) +- `/api/openapi` — OpenAPI 3.0.3 specification +- Security headers: X-Frame-Options, X-Content-Type-Options, X-XSS-Protection +- Request counter middleware for statistics +- `CONTRIBUTING.md` — Contribution guidelines +- `test.js` — Integration test suite (18 endpoints) ### Fixed - Clean URLs: `/tos` and `/privacy` instead of `/tos.html` and `/privacy.html` diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md new file mode 100644 index 0000000..1f267ee --- /dev/null +++ b/CONTRIBUTING.md @@ -0,0 +1,84 @@ +# Contributing to AniKotoAPI + +Thank you for your interest in contributing! Here's how you can help. + +## Getting Started + +1. Fork the repository +2. Clone your fork: `git clone https://github.com/YOUR_USERNAME/AniKotoAPI.git` +3. Create a branch: `git checkout -b feature/amazing-feature` +4. Make your changes +5. Commit: `git commit -m 'feat: add amazing feature'` +6. Push: `git push origin feature/amazing-feature` +7. Open a Pull Request + +## Development Setup + +```bash +# Install dependencies +npm install + +# Start development server +npm run dev + +# Server runs on http://localhost:4444 +``` + +## Project Structure + +``` +AniKotoAPI/ +├── server.js # Express entry point +├── src/ +│ ├── configs/ # URL patterns, headers, ID mappings +│ ├── controllers/ # Route handlers +│ ├── extractors/ # HTML scrapers (Cheerio) +│ ├── helper/ # Cache utilities +│ └── routes/ # Express routes +├── public/ # Static files (landing page) +└── docs/ # API documentation +``` + +## Adding a New Endpoint + +1. Create extractor in `src/extractors/yourExtractor.js` +2. Create controller in `src/controllers/yourController.js` +3. Add route in `src/routes/apiRoutes.js` +4. Update `docs/endpoints.md` with documentation +5. Test with `curl http://localhost:4444/api/your-endpoint` + +## Code Style + +- Use ES6+ features (import/export, async/await) +- Add JSDoc comments for new functions +- Follow existing patterns in the codebase +- Keep extractors focused on one task + +## Commit Messages + +Use [Conventional Commits](https://www.conventionalcommits.org/): + +- `feat:` new feature +- `fix:` bug fix +- `docs:` documentation changes +- `refactor:` code refactoring +- `test:` adding tests +- `chore:` maintenance + +## Pull Request Guidelines + +- Keep PRs focused on one feature or fix +- Update CHANGELOG.md with your changes +- Test your changes locally before submitting +- Follow existing code style + +## Reporting Issues + +- Use GitHub Issues for bug reports +- Include steps to reproduce +- Include expected vs actual behavior +- Include your Node.js version + +## License + +By contributing, you agree that your contributions will be licensed under the MIT License. diff --git a/server.js b/server.js index 1df7ca4..914df98 100644 --- a/server.js +++ b/server.js @@ -67,6 +67,16 @@ app.use((req, res, next) => { .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 // ══════════════════════════════════════════════════════════════ diff --git a/src/routes/apiRoutes.js b/src/routes/apiRoutes.js index 2dfb4bd..a6ecd27 100644 --- a/src/routes/apiRoutes.js +++ b/src/routes/apiRoutes.js @@ -321,12 +321,136 @@ const createApiRoutes = (app, jsonResponse, jsonError) => { } }); - // ══════════════════════════════════════════════════════════════ - // CATEGORY ROUTES - // ══════════════════════════════════════════════════════════════ +// ══════════════════════════════════════════════════════════════ +// CATEGORY ROUTES +// ══════════════════════════════════════════════════════════════ - // NOTE: Category routes (genre, type, status) are registered separately - categoryRoutes(app, jsonResponse, jsonError); +// NOTE: Category routes (genre, type, status) are registered separately +categoryRoutes(app, jsonResponse, jsonError); + +// ══════════════════════════════════════════════════════════════ +// HEALTH & STATS +// ══════════════════════════════════════════════════════════════ + +const startTime = Date.now(); +let requestCount = 0; +let errorCount = 0; + +// Middleware to count requests +app.use((req, res, next) => { + if (req.path.startsWith("/api")) { + requestCount++; + res.on("finish", () => { + if (res.statusCode >= 400) errorCount++; + }); + } + next(); +}); + +// ---- FEATURE: Health check endpoint ---- +app.get("/api/health", (req, res) => { + const uptime = Math.floor((Date.now() - startTime) / 1000); + const hours = Math.floor(uptime / 3600); + const minutes = Math.floor((uptime % 3600) / 60); + const seconds = uptime % 60; + + res.json({ + success: true, + results: { + status: "healthy", + version: "1.7.2", + uptime: `${hours}h ${minutes}m ${s}s`, + uptimeSeconds: uptime, + timestamp: new Date().toISOString(), + node: process.version, + memory: { + used: Math.round(process.memoryUsage().heapUsed / 1024 / 1024) + "MB", + total: Math.round(process.memoryUsage().heapTotal / 1024 / 1024) + "MB", + }, + }, + }); +}); + +// ---- FEATURE: Cache & API statistics ---- +app.get("/api/stats", (req, res) => { + const uptime = Math.floor((Date.now() - startTime) / 1000); + + res.json({ + success: true, + results: { + uptime: uptime + "s", + requests: { + total: requestCount, + errors: errorCount, + successRate: requestCount > 0 + ? ((1 - errorCount / requestCount) * 100).toFixed(1) + "%" + : "100%", + }, + cache: { + type: "in-memory", + ttl: "5 minutes", + description: "Map-based cache with TTL expiration", + }, + endpoints: 27, + timestamp: new Date().toISOString(), + }, + }); +}); + +// ---- FEATURE: OpenAPI specification ---- +app.get("/api/openapi", (req, res) => { + res.json({ + openapi: "3.0.3", + info: { + title: "AniKotoAPI", + description: "Free REST API for anime data from anikototv.to", + version: "1.7.2", + contact: { + name: "Shinei Nouzen", + url: "https://github.com/Shineii86/AniKotoAPI", + }, + }, + servers: [ + { url: "https://anikototvapi.vercel.app/api", description: "Production" }, + ], + paths: { + "/": { get: { summary: "Homepage data", tags: ["Home"] } }, + "/search": { get: { summary: "Search anime", tags: ["Search"] } }, + "/info": { get: { summary: "Anime info", tags: ["Anime"] } }, + "/episodes/{id}": { get: { summary: "Episode list", tags: ["Episodes"] } }, + "/servers": { get: { summary: "Server list", tags: ["Streaming"] } }, + "/stream": { get: { summary: "Stream URL", tags: ["Streaming"] } }, + "/schedule": { get: { summary: "Anime schedule", tags: ["Schedule"] } }, + "/random": { get: { summary: "Random anime", tags: ["Discovery"] } }, + "/top-ten": { get: { summary: "Top 10 rankings", tags: ["Rankings"] } }, + "/trending": { get: { summary: "Trending anime", tags: ["Discovery"] } }, + "/spotlight": { get: { summary: "Spotlight anime", tags: ["Discovery"] } }, + "/most-popular": { get: { summary: "Most popular", tags: ["Rankings"] } }, + "/new-release": { get: { summary: "New releases", tags: ["Releases"] } }, + "/filter": { get: { summary: "Advanced filter", tags: ["Filter"] } }, + "/genre/{name}": { get: { summary: "By genre", tags: ["Categories"] } }, + "/type/{name}": { get: { summary: "By type", tags: ["Categories"] } }, + "/status/{name}": { get: { summary: "By status", tags: ["Categories"] } }, + "/health": { get: { summary: "Health check", tags: ["System"] } }, + "/stats": { get: { summary: "API statistics", tags: ["System"] } }, + "/openapi": { get: { summary: "OpenAPI spec", tags: ["System"] } }, + }, + tags: [ + { name: "Home", description: "Homepage data" }, + { name: "Search", description: "Search & suggestions" }, + { name: "Anime", description: "Anime details" }, + { name: "Episodes", description: "Episode lists" }, + { name: "Streaming", description: "Stream URLs & servers" }, + { name: "Schedule", description: "Airing schedule" }, + { name: "Discovery", description: "Random, trending, spotlight" }, + { name: "Rankings", description: "Top 10, most popular" }, + { name: "Releases", description: "New releases" }, + { name: "Filter", description: "Advanced filtering" }, + { name: "Categories", description: "Genre, type, status" }, + { name: "System", description: "Health, stats, OpenAPI" }, + ], + }); +}); }; export { createApiRoutes }; diff --git a/test.js b/test.js new file mode 100644 index 0000000..71554aa --- /dev/null +++ b/test.js @@ -0,0 +1,70 @@ +/* + * ======= • ======= • ======= • ======= • =======• ======= + * AniKotoAPI — test.js + * Repository: https://github.com/Shineii86/AniKotoAPI + * + * @description + * Integration test suite for AniKotoAPI endpoints. + * Tests all major endpoints for correct response format. + * + * @author Shinei Nouzen + * @license MIT + * ======= • ======= • ======= • ======= • =======• ======= + */ + +const BASE = process.env.API_URL || "https://anikototvapi.vercel.app/api"; + +const tests = [ + { name: "Home", url: "/" }, + { name: "Search", url: "/search?keyword=naruto" }, + { name: "Info", url: "/info?id=road-of-naruto-ggjw8" }, + { name: "Episodes", url: "/episodes/road-of-naruto-ggjw8" }, + { name: "Suggestions", url: "/suggestions?keyword=naruto" }, + { name: "Spotlight", url: "/spotlight" }, + { name: "Trending", url: "/trending" }, + { name: "Top 10", url: "/top-ten" }, + { name: "Random", url: "/random" }, + { name: "Most Popular", url: "/most-popular" }, + { name: "New Release", url: "/new-release" }, + { name: "Schedule", url: "/schedule" }, + { name: "Genre", url: "/genre/action" }, + { name: "Type", url: "/type/tv" }, + { name: "Status", url: "/status/currently-airing" }, + { name: "Health", url: "/health" }, + { name: "Stats", url: "/stats" }, + { name: "OpenAPI", url: "/openapi" }, +]; + +let passed = 0; +let failed = 0; + +async function runTest(test) { + try { + const res = await fetch(`${BASE}${test.url}`); + const data = await res.json(); + + if (data.success === true && data.results) { + console.log(`✅ ${test.name}`); + passed++; + } else { + console.log(`❌ ${test.name} - Invalid response format`); + failed++; + } + } catch (error) { + console.log(`❌ ${test.name} - ${error.message}`); + failed++; + } +} + +async function runAll() { + console.log(`\n🧪 Running ${tests.length} tests...\n`); + + for (const test of tests) { + await runTest(test); + } + + console.log(`\n📊 Results: ${passed} passed, ${failed} failed, ${tests.length} total\n`); + process.exit(failed > 0 ? 1 : 0); +} + +runAll();