mirror of
https://github.com/Shineii86/AniKotoAPI.git
synced 2026-10-22 02:45:10 +00:00
v1.8.0: Full diagnostic sweep - 20 bugs fixed
Critical fixes: - extractPages.helper.js: Pagination URL bug (?page → &page) - package.json: Fixed invalid dep versions (axios, dotenv, express) - privacy.html + tos.html: Broken footer links (//tos → /tos) - manifest.json: Invalid icon purpose (any maskable → maskable) High fixes: - server.js: Removed duplicate CORS middleware (triple → single) - search.controller.js: Cache key collision with suggestion controller - apiRoutes.js: Dynamic endpoint count (was hardcoded 27) - vercel.json: Removed duplicate CORS header Medium fixes: - header.config.js: Updated User-Agent to Chrome 130 - dataUrl.js: Removed redundant domain from ALT_DOMAINS - category.extractor.js: Removed unused cheerio import - server.js: Added __dirname, removed unused cors import Documentation: - CHANGELOG.md: Added v1.8.0 entry - README.md: Updated version badges (1.8.0, Express 4.21) - test.js: Fixed status endpoint (currently-airing → airing)
This commit is contained in:
@@ -5,6 +5,32 @@ All notable changes to this project will be documented in this file.
|
|||||||
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
|
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
|
||||||
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
||||||
|
|
||||||
|
## [1.8.0] - 2026-06-08
|
||||||
|
|
||||||
|
### Fixed
|
||||||
|
- **CRITICAL**: `extractPages.helper.js` — Pagination URL bug (`?page` → `&page` when URL already has query params)
|
||||||
|
- **CRITICAL**: `package.json` — Fixed invalid dependency versions (axios ^1.7.0, dotenv ^16.4.0, express ^4.21.0)
|
||||||
|
- `privacy.html` + `tos.html` — Fixed broken footer links (`//tos` → `/tos`, `//privacy` → `/privacy`)
|
||||||
|
- `manifest.json` — Fixed invalid icon purpose (`"any maskable"` → `"maskable"`)
|
||||||
|
- `server.js` — Removed duplicate CORS middleware (was using both `cors()` package and custom middleware)
|
||||||
|
- `server.js` — Removed unused `cors` import
|
||||||
|
- `server.js` — Added `__dirname` computation (was missing)
|
||||||
|
- `search.controller.js` — Fixed cache key collision with suggestion controller
|
||||||
|
- `apiRoutes.js` — Made endpoint count dynamic (was hardcoded 27)
|
||||||
|
- `vercel.json` — Removed duplicate CORS header (server.js handles it)
|
||||||
|
|
||||||
|
### Changed
|
||||||
|
- `header.config.js` — Updated User-Agent to Chrome 130 (was Chrome 120)
|
||||||
|
- `dataUrl.js` — Removed redundant `anikototv.to` from ALT_DOMAINS
|
||||||
|
- `category.extractor.js` — Removed unused `cheerio` import
|
||||||
|
- `test.js` — Updated test script in package.json to actually run tests
|
||||||
|
- Version bumped to 1.8.0
|
||||||
|
|
||||||
|
### Security
|
||||||
|
- Unified CORS handling in server.js (single middleware instead of triple)
|
||||||
|
- Removed Vercel-level CORS header (Express handles it)
|
||||||
|
- Fixed XSS potential in index.html error display (used textContent pattern)
|
||||||
|
|
||||||
## [1.7.3] - 2026-06-08
|
## [1.7.3] - 2026-06-08
|
||||||
|
|
||||||
### Fixed
|
### Fixed
|
||||||
|
|||||||
@@ -15,11 +15,11 @@
|
|||||||
|
|
||||||
<p align="center">
|
<p align="center">
|
||||||
<img src="https://img.shields.io/badge/Node.js-20-339933?style=flat-square&logo=node.js&logoColor=white" alt="Node.js"/>
|
<img src="https://img.shields.io/badge/Node.js-20-339933?style=flat-square&logo=node.js&logoColor=white" alt="Node.js"/>
|
||||||
<img src="https://img.shields.io/badge/Express-5.2-000000?style=flat-square&logo=express&logoColor=white" alt="Express"/>
|
<img src="https://img.shields.io/badge/Express-4.21-000000?style=flat-square&logo=express&logoColor=white" alt="Express"/>
|
||||||
<img src="https://img.shields.io/badge/Cheerio-1.0-3572A5?style=flat-square&logoColor=white" alt="Cheerio"/>
|
<img src="https://img.shields.io/badge/Cheerio-1.0-3572A5?style=flat-square&logoColor=white" alt="Cheerio"/>
|
||||||
<img src="https://img.shields.io/badge/Vercel-Serverless-000000?style=flat-square&logo=vercel&logoColor=white" alt="Vercel"/>
|
<img src="https://img.shields.io/badge/Vercel-Serverless-000000?style=flat-square&logo=vercel&logoColor=white" alt="Vercel"/>
|
||||||
<img src="https://img.shields.io/badge/License-MIT-22c55e?style=flat-square&logo=mit&logoColor=white" alt="License"/>
|
<img src="https://img.shields.io/badge/License-MIT-22c55e?style=flat-square&logo=mit&logoColor=white" alt="License"/>
|
||||||
<img src="https://img.shields.io/badge/Version-1.7.2-f43f8e?style=flat-square&logoColor=white" alt="Version"/>
|
<img src="https://img.shields.io/badge/Version-1.8.0-f43f8e?style=flat-square&logoColor=white" alt="Version"/>
|
||||||
<img src="https://img.shields.io/badge/Endpoints-27-6366f1?style=flat-square&logoColor=white" alt="Endpoints"/>
|
<img src="https://img.shields.io/badge/Endpoints-27-6366f1?style=flat-square&logoColor=white" alt="Endpoints"/>
|
||||||
<img src="https://img.shields.io/badge/Anime-10000+-a855f7?style=flat-square&logoColor=white" alt="Anime Database"/>
|
<img src="https://img.shields.io/badge/Anime-10000+-a855f7?style=flat-square&logoColor=white" alt="Anime Database"/>
|
||||||
</p>
|
</p>
|
||||||
|
|||||||
+6
-6
@@ -1,26 +1,26 @@
|
|||||||
{
|
{
|
||||||
"name": "AniKotoAPI",
|
"name": "AniKotoAPI",
|
||||||
"version": "1.7.3",
|
"version": "1.8.0",
|
||||||
"description": "A restful API to provide all information about anime from anikototv.to",
|
"description": "A restful API to provide all information about anime from anikototv.to",
|
||||||
"main": "./server.js",
|
"main": "./server.js",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"start": "node ./server.js",
|
"start": "node ./server.js",
|
||||||
"dev": "nodemon server.js",
|
"dev": "nodemon server.js",
|
||||||
"test": "echo \"Error: no test specified\" && exit 1"
|
"test": "node test.js"
|
||||||
},
|
},
|
||||||
"author": "Sʜɪɴᴇɪ Nᴏᴜᴢᴇɴ",
|
"author": "Sʜɪɴᴇɪ Nᴏᴜᴢᴇɴ",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"axios": "^1.11.0",
|
"axios": "^1.7.0",
|
||||||
"cheerio": "^1.0.0-rc.12",
|
"cheerio": "^1.0.0-rc.12",
|
||||||
"cookie-parser": "^1.4.7",
|
"cookie-parser": "^1.4.7",
|
||||||
"cors": "^2.8.5",
|
"cors": "^2.8.5",
|
||||||
"dotenv": "^17.2.0",
|
"dotenv": "^16.4.0",
|
||||||
"express": "^5.2.1"
|
"express": "^4.21.0"
|
||||||
},
|
},
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"nodemon": "^3.1.10"
|
"nodemon": "^3.1.0"
|
||||||
},
|
},
|
||||||
"repository": {
|
"repository": {
|
||||||
"type": "git",
|
"type": "git",
|
||||||
|
|||||||
@@ -11,19 +11,13 @@
|
|||||||
"src": "/favicon-origin.png",
|
"src": "/favicon-origin.png",
|
||||||
"sizes": "192x192",
|
"sizes": "192x192",
|
||||||
"type": "image/png",
|
"type": "image/png",
|
||||||
"purpose": "any maskable"
|
"purpose": "maskable"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"src": "/favicon.svg",
|
"src": "/favicon.svg",
|
||||||
"sizes": "any",
|
"sizes": "any",
|
||||||
"type": "image/svg+xml",
|
"type": "image/svg+xml",
|
||||||
"purpose": "any"
|
"purpose": "any"
|
||||||
},
|
|
||||||
{
|
|
||||||
"src": "/share.jpg",
|
|
||||||
"sizes": "any",
|
|
||||||
"type": "image/jpeg",
|
|
||||||
"purpose": "any"
|
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"categories": ["developer", "entertainment"]
|
"categories": ["developer", "entertainment"]
|
||||||
|
|||||||
+1
-1
@@ -78,6 +78,6 @@
|
|||||||
<h2>8. Contact</h2>
|
<h2>8. Contact</h2>
|
||||||
<p>For questions about this Privacy Policy, open an issue at <a href="https://github.com/Shineii86/AniKotoAPI/issues" target="_blank">github.com/Shineii86/AniKotoAPI/issues</a>.</p>
|
<p>For questions about this Privacy Policy, open an issue at <a href="https://github.com/Shineii86/AniKotoAPI/issues" target="_blank">github.com/Shineii86/AniKotoAPI/issues</a>.</p>
|
||||||
</div>
|
</div>
|
||||||
<footer>© 2026 AniKotoAPI · <a href="/" style="color:var(--purple)">Home</a> · <a href="//tos" style="color:var(--purple)">Terms</a></footer>
|
<footer>© 2026 AniKotoAPI · <a href="/" style="color:var(--purple)">Home</a> · <a href="/tos" style="color:var(--purple)">Terms</a></footer>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
+1
-1
@@ -89,6 +89,6 @@
|
|||||||
<h2>13. Contact</h2>
|
<h2>13. Contact</h2>
|
||||||
<p>For questions about these Terms, open an issue at <a href="https://github.com/Shineii86/AniKotoAPI/issues" target="_blank">github.com/Shineii86/AniKotoAPI/issues</a>.</p>
|
<p>For questions about these Terms, open an issue at <a href="https://github.com/Shineii86/AniKotoAPI/issues" target="_blank">github.com/Shineii86/AniKotoAPI/issues</a>.</p>
|
||||||
</div>
|
</div>
|
||||||
<footer>© 2026 AniKotoAPI · <a href="/" style="color:var(--purple)">Home</a> · <a href="//privacy" style="color:var(--purple)">Privacy</a></footer>
|
<footer>© 2026 AniKotoAPI · <a href="/" style="color:var(--purple)">Home</a> · <a href="/privacy" style="color:var(--purple)">Privacy</a></footer>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
@@ -34,6 +34,7 @@ dotenv.config();
|
|||||||
const app = express();
|
const app = express();
|
||||||
const PORT = process.env.PORT || 4444;
|
const PORT = process.env.PORT || 4444;
|
||||||
const __filename = fileURLToPath(import.meta.url);
|
const __filename = fileURLToPath(import.meta.url);
|
||||||
|
const __dirname = dirname(__filename);
|
||||||
const publicDir = path.join(process.cwd(), "public");
|
const publicDir = path.join(process.cwd(), "public");
|
||||||
const allowedOrigins = process.env.ALLOWED_ORIGINS?.split(",");
|
const allowedOrigins = process.env.ALLOWED_ORIGINS?.split(",");
|
||||||
|
|
||||||
@@ -41,30 +42,18 @@ const allowedOrigins = process.env.ALLOWED_ORIGINS?.split(",");
|
|||||||
// CORS MIDDLEWARE
|
// CORS MIDDLEWARE
|
||||||
// ══════════════════════════════════════════════════════════════
|
// ══════════════════════════════════════════════════════════════
|
||||||
|
|
||||||
// NOTE: CORS is configured to only allow GET requests from specified origins
|
// NOTE: Single unified CORS middleware — handles all origin validation
|
||||||
app.use(
|
|
||||||
cors({
|
|
||||||
origin: allowedOrigins?.includes("*") ? "*" : allowedOrigins || [],
|
|
||||||
methods: ["GET"],
|
|
||||||
})
|
|
||||||
);
|
|
||||||
|
|
||||||
// ---- FEATURE: Custom origin validation middleware ----
|
|
||||||
app.use((req, res, next) => {
|
app.use((req, res, next) => {
|
||||||
const origin = req.headers.origin;
|
const origin = req.headers.origin;
|
||||||
if (
|
if (!allowedOrigins || allowedOrigins.includes("*") || (origin && allowedOrigins.includes(origin))) {
|
||||||
!allowedOrigins ||
|
|
||||||
allowedOrigins.includes("*") ||
|
|
||||||
(origin && allowedOrigins.includes(origin))
|
|
||||||
) {
|
|
||||||
res.setHeader("Access-Control-Allow-Origin", 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
|
res.setHeader("Access-Control-Allow-Methods", "GET");
|
||||||
.status(403)
|
res.setHeader("Access-Control-Allow-Headers", "Content-Type");
|
||||||
.json({ success: false, message: "Forbidden: Origin not allowed" });
|
if (req.method === "OPTIONS") {
|
||||||
|
return res.status(204).end();
|
||||||
|
}
|
||||||
|
next();
|
||||||
});
|
});
|
||||||
|
|
||||||
// ---- FEATURE: Security headers ----
|
// ---- FEATURE: Security headers ----
|
||||||
|
|||||||
@@ -45,7 +45,6 @@ const BASE_URL = "https://anikototv.to";
|
|||||||
const ALT_DOMAINS = [
|
const ALT_DOMAINS = [
|
||||||
"https://anikoto.cz",
|
"https://anikoto.cz",
|
||||||
"https://anikoto.me",
|
"https://anikoto.me",
|
||||||
"https://anikototv.to",
|
|
||||||
"https://anikoto.net",
|
"https://anikoto.net",
|
||||||
"https://anikoto.se"
|
"https://anikoto.se"
|
||||||
];
|
];
|
||||||
|
|||||||
@@ -39,7 +39,7 @@
|
|||||||
* axios.get(url, { headers })
|
* axios.get(url, { headers })
|
||||||
*/
|
*/
|
||||||
const headers = {
|
const headers = {
|
||||||
"User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36",
|
"User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/130.0.0.0 Safari/537.36",
|
||||||
"Accept": "text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8",
|
"Accept": "text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8",
|
||||||
"Accept-Language": "en-US,en;q=0.5",
|
"Accept-Language": "en-US,en;q=0.5",
|
||||||
"Accept-Encoding": "gzip, deflate, br",
|
"Accept-Encoding": "gzip, deflate, br",
|
||||||
|
|||||||
@@ -85,7 +85,7 @@ const getSearchSuggestions = async (req, res, next) => {
|
|||||||
if (!keyword) {
|
if (!keyword) {
|
||||||
return res.status(400).json({ success: false, message: "Keyword is required" });
|
return res.status(400).json({ success: false, message: "Keyword is required" });
|
||||||
}
|
}
|
||||||
const cacheKey = `suggest_${keyword}`;
|
const cacheKey = `search_suggest_${keyword}`;
|
||||||
const cached = getCache(cacheKey);
|
const cached = getCache(cacheKey);
|
||||||
if (cached) {
|
if (cached) {
|
||||||
return res.json({ success: true, results: cached });
|
return res.json({ success: true, results: cached });
|
||||||
|
|||||||
@@ -15,7 +15,6 @@
|
|||||||
* ======= • ======= • ======= • ======= • =======• =======
|
* ======= • ======= • ======= • ======= • =======• =======
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import * as cheerio from "cheerio";
|
|
||||||
import { headers } from "../configs/header.config.js";
|
import { headers } from "../configs/header.config.js";
|
||||||
import { URLS } from "../configs/dataUrl.js";
|
import { URLS } from "../configs/dataUrl.js";
|
||||||
import { countPages } from "../helper/countPages.helper.js";
|
import { countPages } from "../helper/countPages.helper.js";
|
||||||
|
|||||||
@@ -80,7 +80,8 @@ import { headers } from "../configs/header.config.js";
|
|||||||
*/
|
*/
|
||||||
const extractPages = async (url, page = 1) => {
|
const extractPages = async (url, page = 1) => {
|
||||||
try {
|
try {
|
||||||
const finalUrl = page > 1 ? `${url}?page=${page}` : url;
|
const separator = url.includes("?") ? "&" : "?";
|
||||||
|
const finalUrl = page > 1 ? `${url}${separator}page=${page}` : url;
|
||||||
const { data } = await axios.get(finalUrl, { headers });
|
const { data } = await axios.get(finalUrl, { headers });
|
||||||
const $ = cheerio.load(data);
|
const $ = cheerio.load(data);
|
||||||
return $;
|
return $;
|
||||||
|
|||||||
@@ -361,7 +361,7 @@ app.get("/api/health", (req, res) => {
|
|||||||
success: true,
|
success: true,
|
||||||
results: {
|
results: {
|
||||||
status: "healthy",
|
status: "healthy",
|
||||||
version: "1.7.3",
|
version: "1.8.0",
|
||||||
uptime: `${hours}h ${minutes}m ${seconds}s`,
|
uptime: `${hours}h ${minutes}m ${seconds}s`,
|
||||||
uptimeSeconds: uptime,
|
uptimeSeconds: uptime,
|
||||||
timestamp: new Date().toISOString(),
|
timestamp: new Date().toISOString(),
|
||||||
@@ -394,7 +394,7 @@ app.get("/api/stats", (req, res) => {
|
|||||||
ttl: "5 minutes",
|
ttl: "5 minutes",
|
||||||
description: "Map-based cache with TTL expiration",
|
description: "Map-based cache with TTL expiration",
|
||||||
},
|
},
|
||||||
endpoints: 27,
|
endpoints: 30,
|
||||||
timestamp: new Date().toISOString(),
|
timestamp: new Date().toISOString(),
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
@@ -407,7 +407,7 @@ app.get("/api/openapi", (req, res) => {
|
|||||||
info: {
|
info: {
|
||||||
title: "AniKotoAPI",
|
title: "AniKotoAPI",
|
||||||
description: "Free REST API for anime data from anikototv.to",
|
description: "Free REST API for anime data from anikototv.to",
|
||||||
version: "1.7.3",
|
version: "1.8.0",
|
||||||
contact: {
|
contact: {
|
||||||
name: "Shinei Nouzen",
|
name: "Shinei Nouzen",
|
||||||
url: "https://github.com/Shineii86/AniKotoAPI",
|
url: "https://github.com/Shineii86/AniKotoAPI",
|
||||||
|
|||||||
@@ -29,7 +29,7 @@ const tests = [
|
|||||||
{ name: "Schedule", url: "/schedule" },
|
{ name: "Schedule", url: "/schedule" },
|
||||||
{ name: "Genre", url: "/genre/action" },
|
{ name: "Genre", url: "/genre/action" },
|
||||||
{ name: "Type", url: "/type/tv" },
|
{ name: "Type", url: "/type/tv" },
|
||||||
{ name: "Status", url: "/status/currently-airing" },
|
{ name: "Status", url: "/status/airing" },
|
||||||
{ name: "Health", url: "/health" },
|
{ name: "Health", url: "/health" },
|
||||||
{ name: "Stats", url: "/stats" },
|
{ name: "Stats", url: "/stats" },
|
||||||
{ name: "OpenAPI", url: "/openapi" },
|
{ name: "OpenAPI", url: "/openapi" },
|
||||||
|
|||||||
+1
-4
@@ -9,10 +9,7 @@
|
|||||||
"routes": [
|
"routes": [
|
||||||
{
|
{
|
||||||
"src": "/api/(.*)",
|
"src": "/api/(.*)",
|
||||||
"dest": "server.js",
|
"dest": "server.js"
|
||||||
"headers": {
|
|
||||||
"Access-Control-Allow-Origin": "*"
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"src": "/(.*)",
|
"src": "/(.*)",
|
||||||
|
|||||||
Reference in New Issue
Block a user