mirror of
https://github.com/Shineii86/AniKotoAPI.git
synced 2026-10-22 02:45:10 +00:00
feat: add health, stats, OpenAPI, security headers, tests
- /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)
This commit is contained in:
@@ -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/streaming.md` — Streaming flow guide (3-step with real data)
|
||||||
- `docs/examples.md` — Code examples in cURL, JavaScript, Python, Node.js (all tested)
|
- `docs/examples.md` — Code examples in cURL, JavaScript, Python, Node.js (all tested)
|
||||||
- `docs/architecture.md` — Project structure, tech stack, design decisions
|
- `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
|
### Fixed
|
||||||
- Clean URLs: `/tos` and `/privacy` instead of `/tos.html` and `/privacy.html`
|
- Clean URLs: `/tos` and `/privacy` instead of `/tos.html` and `/privacy.html`
|
||||||
|
|||||||
@@ -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.
|
||||||
@@ -67,6 +67,16 @@ app.use((req, res, next) => {
|
|||||||
.json({ success: false, message: "Forbidden: Origin not allowed" });
|
.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
|
// STATIC FILES
|
||||||
// ══════════════════════════════════════════════════════════════
|
// ══════════════════════════════════════════════════════════════
|
||||||
|
|||||||
+129
-5
@@ -321,12 +321,136 @@ const createApiRoutes = (app, jsonResponse, jsonError) => {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
// ══════════════════════════════════════════════════════════════
|
// ══════════════════════════════════════════════════════════════
|
||||||
// CATEGORY ROUTES
|
// CATEGORY ROUTES
|
||||||
// ══════════════════════════════════════════════════════════════
|
// ══════════════════════════════════════════════════════════════
|
||||||
|
|
||||||
// NOTE: Category routes (genre, type, status) are registered separately
|
// NOTE: Category routes (genre, type, status) are registered separately
|
||||||
categoryRoutes(app, jsonResponse, jsonError);
|
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 };
|
export { createApiRoutes };
|
||||||
|
|||||||
@@ -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();
|
||||||
Reference in New Issue
Block a user