diff --git a/CHANGELOG.md b/CHANGELOG.md index 356f676..231d5ac 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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/), 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 ### Fixed diff --git a/README.md b/README.md index c6863e2..2603df1 100644 --- a/README.md +++ b/README.md @@ -15,11 +15,11 @@

Node.js - Express + Express Cheerio Vercel License - Version + Version Endpoints Anime Database

diff --git a/package.json b/package.json index a2e2da8..682c2bf 100644 --- a/package.json +++ b/package.json @@ -1,26 +1,26 @@ { "name": "AniKotoAPI", - "version": "1.7.3", + "version": "1.8.0", "description": "A restful API to provide all information about anime from anikototv.to", "main": "./server.js", "scripts": { "start": "node ./server.js", "dev": "nodemon server.js", - "test": "echo \"Error: no test specified\" && exit 1" + "test": "node test.js" }, "author": "Sʜɪɴᴇɪ Nᴏᴜᴢᴇɴ", "license": "MIT", "dependencies": { - "axios": "^1.11.0", + "axios": "^1.7.0", "cheerio": "^1.0.0-rc.12", "cookie-parser": "^1.4.7", "cors": "^2.8.5", - "dotenv": "^17.2.0", - "express": "^5.2.1" + "dotenv": "^16.4.0", + "express": "^4.21.0" }, "type": "module", "devDependencies": { - "nodemon": "^3.1.10" + "nodemon": "^3.1.0" }, "repository": { "type": "git", diff --git a/public/manifest.json b/public/manifest.json index 83f9eae..3354853 100644 --- a/public/manifest.json +++ b/public/manifest.json @@ -11,19 +11,13 @@ "src": "/favicon-origin.png", "sizes": "192x192", "type": "image/png", - "purpose": "any maskable" + "purpose": "maskable" }, { "src": "/favicon.svg", "sizes": "any", "type": "image/svg+xml", "purpose": "any" - }, - { - "src": "/share.jpg", - "sizes": "any", - "type": "image/jpeg", - "purpose": "any" } ], "categories": ["developer", "entertainment"] diff --git a/public/privacy.html b/public/privacy.html index f18e9de..cd65a46 100644 --- a/public/privacy.html +++ b/public/privacy.html @@ -78,6 +78,6 @@

8. Contact

For questions about this Privacy Policy, open an issue at github.com/Shineii86/AniKotoAPI/issues.

- + \ No newline at end of file diff --git a/public/tos.html b/public/tos.html index 843a8c9..40dfbca 100644 --- a/public/tos.html +++ b/public/tos.html @@ -89,6 +89,6 @@

13. Contact

For questions about these Terms, open an issue at github.com/Shineii86/AniKotoAPI/issues.

- + \ No newline at end of file diff --git a/server.js b/server.js index 914df98..db8c029 100644 --- a/server.js +++ b/server.js @@ -34,6 +34,7 @@ dotenv.config(); const app = express(); const PORT = process.env.PORT || 4444; const __filename = fileURLToPath(import.meta.url); +const __dirname = dirname(__filename); const publicDir = path.join(process.cwd(), "public"); const allowedOrigins = process.env.ALLOWED_ORIGINS?.split(","); @@ -41,30 +42,18 @@ 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 ---- +// 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)) - ) { + 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" }); + 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 ---- diff --git a/src/configs/dataUrl.js b/src/configs/dataUrl.js index 5e47f9c..3fc64ce 100644 --- a/src/configs/dataUrl.js +++ b/src/configs/dataUrl.js @@ -45,7 +45,6 @@ const BASE_URL = "https://anikototv.to"; const ALT_DOMAINS = [ "https://anikoto.cz", "https://anikoto.me", - "https://anikototv.to", "https://anikoto.net", "https://anikoto.se" ]; diff --git a/src/configs/header.config.js b/src/configs/header.config.js index 9252dab..7bd8040 100644 --- a/src/configs/header.config.js +++ b/src/configs/header.config.js @@ -39,7 +39,7 @@ * axios.get(url, { 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-Language": "en-US,en;q=0.5", "Accept-Encoding": "gzip, deflate, br", diff --git a/src/controllers/search.controller.js b/src/controllers/search.controller.js index e2dc03d..3160a7d 100644 --- a/src/controllers/search.controller.js +++ b/src/controllers/search.controller.js @@ -85,7 +85,7 @@ const getSearchSuggestions = async (req, res, next) => { if (!keyword) { return res.status(400).json({ success: false, message: "Keyword is required" }); } - const cacheKey = `suggest_${keyword}`; + const cacheKey = `search_suggest_${keyword}`; const cached = getCache(cacheKey); if (cached) { return res.json({ success: true, results: cached }); diff --git a/src/extractors/category.extractor.js b/src/extractors/category.extractor.js index d41c268..30ac618 100644 --- a/src/extractors/category.extractor.js +++ b/src/extractors/category.extractor.js @@ -15,7 +15,6 @@ * ======= • ======= • ======= • ======= • =======• ======= */ -import * as cheerio from "cheerio"; import { headers } from "../configs/header.config.js"; import { URLS } from "../configs/dataUrl.js"; import { countPages } from "../helper/countPages.helper.js"; diff --git a/src/helper/extractPages.helper.js b/src/helper/extractPages.helper.js index bbdd314..237590b 100644 --- a/src/helper/extractPages.helper.js +++ b/src/helper/extractPages.helper.js @@ -80,7 +80,8 @@ import { headers } from "../configs/header.config.js"; */ const extractPages = async (url, page = 1) => { 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 $ = cheerio.load(data); return $; diff --git a/src/routes/apiRoutes.js b/src/routes/apiRoutes.js index a5ad56d..8f067e6 100644 --- a/src/routes/apiRoutes.js +++ b/src/routes/apiRoutes.js @@ -361,7 +361,7 @@ app.get("/api/health", (req, res) => { success: true, results: { status: "healthy", - version: "1.7.3", + version: "1.8.0", uptime: `${hours}h ${minutes}m ${seconds}s`, uptimeSeconds: uptime, timestamp: new Date().toISOString(), @@ -394,7 +394,7 @@ app.get("/api/stats", (req, res) => { ttl: "5 minutes", description: "Map-based cache with TTL expiration", }, - endpoints: 27, + endpoints: 30, timestamp: new Date().toISOString(), }, }); @@ -407,7 +407,7 @@ app.get("/api/openapi", (req, res) => { info: { title: "AniKotoAPI", description: "Free REST API for anime data from anikototv.to", - version: "1.7.3", + version: "1.8.0", contact: { name: "Shinei Nouzen", url: "https://github.com/Shineii86/AniKotoAPI", diff --git a/test.js b/test.js index 71554aa..54a181e 100644 --- a/test.js +++ b/test.js @@ -29,7 +29,7 @@ const tests = [ { name: "Schedule", url: "/schedule" }, { name: "Genre", url: "/genre/action" }, { name: "Type", url: "/type/tv" }, - { name: "Status", url: "/status/currently-airing" }, + { name: "Status", url: "/status/airing" }, { name: "Health", url: "/health" }, { name: "Stats", url: "/stats" }, { name: "OpenAPI", url: "/openapi" }, diff --git a/vercel.json b/vercel.json index c9bf036..2d3cd4c 100644 --- a/vercel.json +++ b/vercel.json @@ -9,10 +9,7 @@ "routes": [ { "src": "/api/(.*)", - "dest": "server.js", - "headers": { - "Access-Control-Allow-Origin": "*" - } + "dest": "server.js" }, { "src": "/(.*)",