diff --git a/package-lock.json b/package-lock.json index 7df7652..f284c09 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,6 +1,6 @@ { "name": "jetsetradio-api", - "version": "1.1.0", + "version": "1.1.1", "lockfileVersion": 1, "requires": true, "dependencies": { diff --git a/package.json b/package.json index 266eed1..84a3e68 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "jetsetradio-api", - "version": "1.1.0", + "version": "1.1.1", "description": "A Data Provider relating to the JSR/JSRF universe", "type": "module", "main": "src/app.js", diff --git a/src/controllers/characterController.js b/src/controllers/characterController.js index 1e307f2..5d6fda0 100644 --- a/src/controllers/characterController.js +++ b/src/controllers/characterController.js @@ -1,3 +1,4 @@ +import {all} from "axios"; import { performJSRAction, performJSRFAction, @@ -30,6 +31,28 @@ export const getAllCharacters = async (req, res) => { } }; +export const getRandomCharacter = async (req, res) => { + try { + const jsrCharacters = await fetchJSRCharacters(req); + const jsrfCharacters = await fetchJSRFCharacters(req); + const brcCharacters = await fetchBRCCharacters(req); + + const allCharacters = [ + ...jsrCharacters, + ...jsrfCharacters, + ...brcCharacters, + ]; + + const randomCharacter = + allCharacters[Math.floor(Math.random() * allCharacters.length)]; + + res.json(randomCharacter); + } catch (err) { + LOGGER.error(`Could not fetch random character \n${err}`); + res.status(500).json({error: "Failed to fetch random character"}); + } +}; + export const getJSRCharacters = async (req, res) => { try { res.send(await fetchJSRCharacters(req)); diff --git a/src/managers/MiddlewareManager.js b/src/managers/MiddlewareManager.js index 5928f22..7bf2a73 100644 --- a/src/managers/MiddlewareManager.js +++ b/src/managers/MiddlewareManager.js @@ -1,111 +1,143 @@ -import express from 'express'; -import listEndpoints from 'express-list-endpoints'; -import { fileURLToPath } from 'url'; -import path, { dirname } from 'path'; -import cors from 'cors'; -import * as bcrypt from 'bcrypt'; -import swaggerUi from 'swagger-ui-express'; -import rateLimit from 'express-rate-limit'; -import MemoryCache from 'memory-cache'; -import favicon from 'serve-favicon'; -import { createRequire } from 'module'; +import express from "express"; +import listEndpoints from "express-list-endpoints"; +import {fileURLToPath} from "url"; +import path, {dirname} from "path"; +import cors from "cors"; +import * as bcrypt from "bcrypt"; +import swaggerUi from "swagger-ui-express"; +import rateLimit from "express-rate-limit"; +import MemoryCache from "memory-cache"; +import favicon from "serve-favicon"; +import {createRequire} from "module"; const require = createRequire(import.meta.url); -const data = require('../utils/swagger-docs.json'); -import dotenv from 'dotenv'; +const data = require("../utils/swagger-docs.json"); +import dotenv from "dotenv"; dotenv.config(); -import Constants from '../constants/dbConstants.js'; -import HealthCheckManager from './HealthCheckManager.js'; -import router from '../routes/router.js'; -import { renderHome, renderDocs } from '../controllers/indexController.js'; -import { listCollections } from '../config/db.js'; -import LOGGER from '../utils/logger.js'; -import { Actions } from '../config/dbActions.js'; -import { performCoreAdminAction } from '../config/db.js'; - +import Constants from "../constants/dbConstants.js"; +import HealthCheckManager from "./HealthCheckManager.js"; +import router from "../routes/router.js"; +import {renderHome, renderDocs} from "../controllers/indexController.js"; +import {listCollections} from "../config/db.js"; +import LOGGER from "../utils/logger.js"; +import {Actions} from "../config/dbActions.js"; +import {performCoreAdminAction} from "../config/db.js"; const cache = new MemoryCache.Cache(); const __dirname = dirname(fileURLToPath(import.meta.url)); const healthCheckManager = new HealthCheckManager(); -const { CORE_DB, JSR_DB, JSRF_DB } = Constants; +const {CORE_DB, JSR_DB, JSRF_DB, BRC_DB} = Constants; class MiddlewareManager { - setMiddleware(app) { - app.set('views', path.join(__dirname, '..', 'views')); - app.set('view engine', 'ejs'); - + app.set("views", path.join(__dirname, "..", "views")); + app.set("view engine", "ejs"); + app.use(cors()); - app.use(express.static(path.join(__dirname, '..', 'public'))); - app.use(express.urlencoded({ extended: true })); + app.use(express.static(path.join(__dirname, "..", "public"))); + app.use(express.urlencoded({extended: true})); app.use(express.json()); - app.use(favicon(path.join(__dirname, '..', 'public', 'img', 'favicon.ico'))); - - app.get('/', (req, res) => renderHome(req, res)); - app.get('/docs', (req, res) => renderDocs(req, res)); - app.get('/health', (req, res) => res.send(healthCheckManager.getAppHealth())); - app.get('/endpoints', async (req, res) => res.send(await filterPipeRoutes(req, listEndpoints(app)))); - app.post('/cache/clear', async (req, res) => await clearCache(req, res)); - app.use('/v1/api', [limiter, cacheMiddleware], router); - app.use('/api-docs', swaggerUi.serve, swaggerUi.setup(data)) + app.use( + favicon(path.join(__dirname, "..", "public", "img", "favicon.ico")) + ); + + app.get("/", (req, res) => renderHome(req, res)); + app.get("/docs", (req, res) => renderDocs(req, res)); + app.get("/health", (req, res) => + res.send(healthCheckManager.getAppHealth()) + ); + app.get("/endpoints", async (req, res) => + res.send(await filterPipeRoutes(req, listEndpoints(app))) + ); + app.post("/cache/clear", async (req, res) => await clearCache(req, res)); + app.use("/v1/api", [limiter, cacheMiddleware], router); + app.use("/api-docs", swaggerUi.serve, swaggerUi.setup(data)); } - } /** * Return a list of Available Endpoints * Pass pipe=true to filter the results to just the routes needed to populate a development database - * + * * @param {*} req : the express request object * @param {*} endpoints : the unfiltered endpoints object list - * @returns + * @returns */ const filterPipeRoutes = async (req, endpoints) => { const pipe = req?.query?.pipe; const coreCollectionsObj = await listCollections(CORE_DB); const jsrCollectionsObj = await listCollections(JSR_DB); const jsrfCollectionsObj = await listCollections(JSRF_DB); - const coreCollections = coreCollectionsObj.map(col => { return `${col.name.toLowerCase()}s` }); - const jsrCollections = jsrCollectionsObj.map(col => { return `${col.name.toLowerCase()}s` }); - const jsrfCollections = jsrfCollectionsObj.map(col => { return `${col.name.toLowerCase()}s` }); + const brcCollectionsObj = await listCollections(BRC_DB); + + const coreCollections = coreCollectionsObj.map((col) => { + return `${col.name.toLowerCase()}s`; + }); + const jsrCollections = jsrCollectionsObj.map((col) => { + return `${col.name.toLowerCase()}s`; + }); + const jsrfCollections = jsrfCollectionsObj.map((col) => { + return `${col.name.toLowerCase()}s`; + }); + const brcCollections = brcCollectionsObj.map((col) => { + return `${col.name.toLowerCase()}s`; + }); endpoints = endpoints - .filter(endpoint => pipe - ? !endpoint.path.includes(':') && endpoint.path.includes('/v1/api') - : endpoint) - .map(endpoint => { return endpoint.path }) + .filter((endpoint) => + pipe + ? !endpoint.path.includes(":") && endpoint.path.includes("/v1/api") + : endpoint + ) + .map((endpoint) => { + return endpoint.path; + }); if (pipe) { const filteredEndpoints = []; for (const endpoint of endpoints) { - const model = endpoint.split('/')[3].replace('-', ''); + const model = endpoint.split("/")[3].replace("-", ""); + console.log("processing model: ", model, " and endpoint: ", endpoint); if (coreCollections.includes(model)) { filteredEndpoints.push(endpoint); } - if (jsrCollections.includes(model) && endpoint.includes('jsr')) { + if (jsrCollections.includes(model) && endpoint.includes("jsr")) { filteredEndpoints.push(endpoint); } - if (jsrfCollections.includes(model) && endpoint.includes('jsrf')) { + if (jsrfCollections.includes(model) && endpoint.includes("jsrf")) { + filteredEndpoints.push(endpoint); + } + if ( + brcCollections.includes(model) && + (endpoint.includes("brc") || endpoint.includes("collectibles")) + ) { filteredEndpoints.push(endpoint); } } endpoints = filteredEndpoints; } return [...new Set(endpoints)]; -} +}; +/* Rate Limiting */ const limiter = rateLimit({ windowMs: 60 * 60 * 1000, // 1 hour time range max: 1000, // 1000 requests limit - keyGenerator: (req) => { + keyGenerator: (req) => { return req.ip; }, }); +/* Set Cache */ const cacheMiddleware = (req, res, next) => { - const clientIp = req.ip; + const clientIp = req.ip; const cacheKey = `cache_${clientIp}_${req.originalUrl || req.url}`; + /* Don't cache 'Random' routes */ + if (req?.path.includes("random")) { + return next(); + } + const cachedData = cache.get(cacheKey); if (cachedData) { LOGGER.info(`Cache hit for url ${req.url}`); @@ -128,26 +160,27 @@ const cacheMiddleware = (req, res, next) => { next(); }; +/* Clear Cache */ const clearCache = async (req, res) => { const username = req?.body?.username; const password = req?.body?.password; const adminUser = await performCoreAdminAction(Actions.fetchAdmin, username); if (!adminUser) { - LOGGER.error('Admin User Not Found'); - return res.status(400).send() + LOGGER.error("Admin User Not Found"); + return res.status(400).send(); } const authenticated = await validatePassword(password, adminUser?.password); if (!authenticated) { - LOGGER.error('Invalid Admin Creds!'); - return res.status(401).send('Unauthorized'); + LOGGER.error("Invalid Admin Creds!"); + return res.status(401).send("Unauthorized"); } if (adminUser && authenticated) { cache.clear(); - LOGGER.info('All Caches Cleared'); - return res.send('Cache Cleared'); + LOGGER.info("All Caches Cleared"); + return res.send("Cache Cleared"); } - res.status(500).send('Unexpected Behavior'); -} + res.status(500).send("Unexpected Behavior"); +}; const validatePassword = async (password, hashedPassword) => { try { @@ -155,6 +188,6 @@ const validatePassword = async (password, hashedPassword) => { } catch (err) { LOGGER.warn(`Error validating Admin password ${err}`); } -} +}; -export default MiddlewareManager; \ No newline at end of file +export default MiddlewareManager; diff --git a/src/routes/characterRouter.js b/src/routes/characterRouter.js index 10ac696..02d6739 100644 --- a/src/routes/characterRouter.js +++ b/src/routes/characterRouter.js @@ -1,10 +1,11 @@ import express from 'express'; -import { getAllCharacters, getJSRCharacters, getJSRFCharacters, getJSRCharacterById, getJSRFCharacterById, getBRCCharacters, getBRCCharacterById } from '../controllers/characterController.js'; +import { getAllCharacters, getRandomCharacter, getJSRCharacters, getJSRFCharacters, getJSRCharacterById, getJSRFCharacterById, getBRCCharacters, getBRCCharacterById } from '../controllers/characterController.js'; const characters = express.Router(); characters.get('/', async (req, res) => /* #swagger.tags = ['Characters'] */ await getAllCharacters(req, res)); +characters.get('/random', async (req, res) => /* #swagger.tags = ['Characters'] */ await getRandomCharacter(req, res)); characters.get('/jsr', async (req, res) => /* #swagger.tags = ['Characters'] */ await getJSRCharacters(req, res)); characters.get('/jsr/:id', async (req, res) => /* #swagger.tags = ['Characters'] */ await getJSRCharacterById(req, res)); characters.get('/jsrf', async (req, res) => /* #swagger.tags = ['Characters'] */ await getJSRFCharacters(req, res)); diff --git a/src/routes/collectibleRouter.js b/src/routes/collectibleRouter.js index 4d95bce..fdc2832 100644 --- a/src/routes/collectibleRouter.js +++ b/src/routes/collectibleRouter.js @@ -6,7 +6,7 @@ import { const collectibles = express.Router(); -collectibles.get("/", async (req, res) => /* #swagger.tags = ['Collectible'] */ await getAllCollectibles(req, res)); -collectibles.get("/:id", async (req, res) => /* #swagger.tags = ['Collectible'] */ await getBRCCollectibleById(req, res)); +collectibles.get("/", async (req, res) => /* #swagger.tags = ['Collectibles'] */ await getAllCollectibles(req, res)); +collectibles.get("/:id", async (req, res) => /* #swagger.tags = ['Collectibles'] */ await getBRCCollectibleById(req, res)); export default collectibles; diff --git a/src/utils/swagger-docs.json b/src/utils/swagger-docs.json index c652381..4f96a3b 100644 --- a/src/utils/swagger-docs.json +++ b/src/utils/swagger-docs.json @@ -2,7 +2,7 @@ "swagger": "2.0", "info": { "title": "JetSetRadio-API", - "version": "1.0.4", + "version": "1.1.0", "description": "Providing data for all things JSR and JSRF!" }, "host": "localhost:9005", @@ -37,16 +37,11 @@ "description": "Artist Data from JSR and JSRF" } ], - "schemes": [ - "https", - "http" - ], + "schemes": ["https", "http"], "paths": { "/games/": { "get": { - "tags": [ - "Games" - ], + "tags": ["Games"], "description": "", "responses": { "200": { @@ -60,9 +55,7 @@ }, "/games/{id}": { "get": { - "tags": [ - "Games" - ], + "tags": ["Games"], "description": "", "parameters": [ { @@ -81,9 +74,7 @@ }, "/songs/": { "get": { - "tags": [ - "Songs" - ], + "tags": ["Songs"], "description": "", "responses": { "200": { @@ -97,9 +88,7 @@ }, "/songs/jsr": { "get": { - "tags": [ - "Songs" - ], + "tags": ["Songs"], "description": "", "responses": { "200": { @@ -116,9 +105,7 @@ }, "/songs/jsr/{id}": { "get": { - "tags": [ - "Songs" - ], + "tags": ["Songs"], "description": "", "parameters": [ { @@ -143,9 +130,7 @@ }, "/songs/jsrf": { "get": { - "tags": [ - "Songs" - ], + "tags": ["Songs"], "description": "", "responses": { "200": { @@ -162,9 +147,7 @@ }, "/songs/jsrf/{id}": { "get": { - "tags": [ - "Songs" - ], + "tags": ["Songs"], "description": "", "parameters": [ { @@ -189,9 +172,7 @@ }, "/songs/brc": { "get": { - "tags": [ - "Songs" - ], + "tags": ["Songs"], "description": "", "responses": { "200": { @@ -208,9 +189,7 @@ }, "/songs/brc/{id}": { "get": { - "tags": [ - "Songs" - ], + "tags": ["Songs"], "description": "", "parameters": [ { @@ -235,9 +214,7 @@ }, "/artists/": { "get": { - "tags": [ - "Artists" - ], + "tags": ["Artists"], "description": "", "responses": { "200": { @@ -254,9 +231,7 @@ }, "/artists/{id}": { "get": { - "tags": [ - "Artists" - ], + "tags": ["Artists"], "description": "", "parameters": [ { @@ -281,9 +256,7 @@ }, "/artists/{id}/songs": { "get": { - "tags": [ - "Artists" - ], + "tags": ["Artists"], "description": "", "parameters": [ { @@ -308,9 +281,7 @@ }, "/graffiti-tags/": { "get": { - "tags": [ - "GraffitiTags" - ], + "tags": ["GraffitiTags"], "description": "", "responses": { "200": { @@ -321,9 +292,7 @@ }, "/graffiti-tags/jsr": { "get": { - "tags": [ - "GraffitiTags" - ], + "tags": ["GraffitiTags"], "description": "", "responses": { "200": { @@ -334,9 +303,7 @@ }, "/graffiti-tags/jsr/{id}": { "get": { - "tags": [ - "GraffitiTags" - ], + "tags": ["GraffitiTags"], "description": "", "parameters": [ { @@ -355,9 +322,7 @@ }, "/graffiti-tags/jsrf": { "get": { - "tags": [ - "GraffitiTags" - ], + "tags": ["GraffitiTags"], "description": "", "responses": { "200": { @@ -368,9 +333,7 @@ }, "/graffiti-tags/jsrf/{id}": { "get": { - "tags": [ - "GraffitiTags" - ], + "tags": ["GraffitiTags"], "description": "", "parameters": [ { @@ -389,22 +352,32 @@ }, "/characters/": { "get": { - "tags": [ - "Characters" - ], + "tags": ["Characters"], + "description": "", + "responses": { + "200": { + "description": "OK" + } + } + } + }, + "/characters/random": { + "get": { + "tags": ["Characters"], "description": "", "responses": { "200": { "description": "OK" + }, + "500": { + "description": "Internal Server Error" } } } }, "/characters/jsr": { "get": { - "tags": [ - "Characters" - ], + "tags": ["Characters"], "description": "", "responses": { "200": { @@ -415,9 +388,7 @@ }, "/characters/jsr/{id}": { "get": { - "tags": [ - "Characters" - ], + "tags": ["Characters"], "description": "", "parameters": [ { @@ -436,9 +407,7 @@ }, "/characters/jsrf": { "get": { - "tags": [ - "Characters" - ], + "tags": ["Characters"], "description": "", "responses": { "200": { @@ -449,9 +418,7 @@ }, "/characters/jsrf/{id}": { "get": { - "tags": [ - "Characters" - ], + "tags": ["Characters"], "description": "", "parameters": [ { @@ -470,9 +437,7 @@ }, "/characters/brc": { "get": { - "tags": [ - "Characters" - ], + "tags": ["Characters"], "description": "", "responses": { "200": { @@ -483,9 +448,7 @@ }, "/characters/brc/{id}": { "get": { - "tags": [ - "Characters" - ], + "tags": ["Characters"], "description": "", "parameters": [ { @@ -504,9 +467,7 @@ }, "/locations/": { "get": { - "tags": [ - "Locations" - ], + "tags": ["Locations"], "description": "", "responses": { "200": { @@ -517,9 +478,7 @@ }, "/locations/jsr": { "get": { - "tags": [ - "Locations" - ], + "tags": ["Locations"], "description": "", "responses": { "200": { @@ -530,9 +489,7 @@ }, "/locations/jsr/{id}": { "get": { - "tags": [ - "Locations" - ], + "tags": ["Locations"], "description": "", "parameters": [ { @@ -551,9 +508,7 @@ }, "/locations/jsrf": { "get": { - "tags": [ - "Locations" - ], + "tags": ["Locations"], "description": "", "responses": { "200": { @@ -564,9 +519,7 @@ }, "/locations/jsrf/{id}": { "get": { - "tags": [ - "Locations" - ], + "tags": ["Locations"], "description": "", "parameters": [ { @@ -585,9 +538,7 @@ }, "/locations/brc": { "get": { - "tags": [ - "Locations" - ], + "tags": ["Locations"], "description": "", "responses": { "200": { @@ -598,9 +549,7 @@ }, "/locations/brc/{id}": { "get": { - "tags": [ - "Locations" - ], + "tags": ["Locations"], "description": "", "parameters": [ { @@ -619,9 +568,7 @@ }, "/levels/": { "get": { - "tags": [ - "Levels" - ], + "tags": ["Levels"], "description": "", "responses": { "200": { @@ -632,9 +579,7 @@ }, "/levels/{id}": { "get": { - "tags": [ - "Levels" - ], + "tags": ["Levels"], "description": "", "parameters": [ { @@ -653,9 +598,7 @@ }, "/collectibles/": { "get": { - "tags": [ - "Collectible" - ], + "tags": ["Collectibles"], "description": "", "responses": { "200": { @@ -666,9 +609,7 @@ }, "/collectibles/{id}": { "get": { - "tags": [ - "Collectible" - ], + "tags": ["Collectibles"], "description": "", "parameters": [ { @@ -686,4 +627,4 @@ } } } -} \ No newline at end of file +}