diff --git a/api/index.js b/api/index.js index adfd3317..333b62dd 100644 --- a/api/index.js +++ b/api/index.js @@ -8,7 +8,36 @@ import { renderError, } from "../src/common/utils.js"; import { fetchStats } from "../src/fetchers/stats-fetcher.js"; -import { isLocaleAvailable } from "../src/translations.js"; +import { validateQueryStringParams } from "../src/common/validate.js"; + +const QUERYSTRING_PARAMS_DATA_TYPE_MAP = new Map([ + ["username", "string"], + ["hide", "enum-array"], + ["hide_title", "boolean"], + ["hide_border", "boolean"], + ["card_width", "number"], + ["hide_rank", "boolean"], + ["show_icons", "boolean"], + ["include_all_commits", "boolean"], + ["line_height", "number"], + ["title_color", "string"], + ["ring_color", "string"], + ["icon_color", "string"], + ["text_color", "string"], + ["text_bold", "boolean"], + ["bg_color", "string"], + ["theme", "enum"], + ["cache_seconds", "number"], + ["exclude_repo", "array"], + ["custom_title", "string"], + ["locale", "enum"], + ["disable_animations", "boolean"], + ["border_radius", "number"], + ["border_color", "string"], + ["number_format", "enum"], + ["rank_icon", "enum"], + ["show", "enum-array"], +]); export default async (req, res) => { const { @@ -45,11 +74,9 @@ export default async (req, res) => { return res.send(renderError("Something went wrong")); } - if (locale && !isLocaleAvailable(locale)) { - return res.send(renderError("Something went wrong", "Language not found")); - } - try { + validateQueryStringParams(req.query, QUERYSTRING_PARAMS_DATA_TYPE_MAP); + const showStats = parseArray(show); const stats = await fetchStats( username, diff --git a/src/common/validate.js b/src/common/validate.js new file mode 100644 index 00000000..5535eecc --- /dev/null +++ b/src/common/validate.js @@ -0,0 +1,126 @@ +// @ts-check + +import { availableLocales } from "../translations.js"; +import { themes } from "../../themes/index.js"; +import { parseArray } from "./utils.js"; + +/** + * Class for handling invalid query string param errors. + */ +class InvalidQueryStringParamsError extends Error { + /** + * Constructor for InvalidQueryStringParamsError. + * + * @param {string} message - The error message. + * @param {string} secondaryMessage - The secondary error message. + */ + constructor(message, secondaryMessage) { + super(message); + this.secondaryMessage = secondaryMessage; + } +} + +const QUERYSTRING_PARAMS_ENUM_VALUES = { + hide: ["stars", "commits", "prs", "issues", "contribs"], + theme: Object.keys(themes), + locale: availableLocales, + number_format: ["short", "long"], + rank_icon: ["github", "percentile", "default"], + show: [ + "reviews", + "discussions_started", + "discussions_answered", + "prs_merged", + "prs_merged_percentage", + ], +}; + +/** + * Returns the secondary error message for an invalid query string param. + * + * @param {string} param - The invalid query string param. + * @param {Map} queryStringParamsDataTypeMap - The query string params data type map. + * @returns {string} The secondary error message. + */ +const getInvalidQueryStringParamsErrorSecondaryMessage = ( + param, + queryStringParamsDataTypeMap, +) => { + const expectedDataType = queryStringParamsDataTypeMap.get(param); + if (expectedDataType === "enum" || expectedDataType === "enum-array") { + return `Expected: ${QUERYSTRING_PARAMS_ENUM_VALUES[param].join(", ")}`; + } else if (expectedDataType === "number") { + return "Expected: a number"; + } else if (expectedDataType === "boolean") { + return "Expected: true or false"; + } else if (expectedDataType === "array") { + return "Expected: an array"; + } else if (expectedDataType === "string") { + return "Expected: a string"; + } else { + throw new Error("Unexpected behavior"); + } +}; + +/** + * Validates the query string params. + * Throws an error if a query string param is invalid. + * Does not return anything. + * + * @param {object} queryStringParams - The query string params. + * @param {Map} queryStringParamsDataTypeMap - The query string params data type map. + * @returns {void} + */ +const validateQueryStringParams = ( + queryStringParams, + queryStringParamsDataTypeMap, +) => { + for (const [param, value] of Object.entries(queryStringParams)) { + const expectedDataType = queryStringParamsDataTypeMap.get(param); + if (!expectedDataType) { + throw new InvalidQueryStringParamsError( + `Invalid query string param: ${param}`, + "Expected: a valid query string param", + ); + } + if (expectedDataType === "enum") { + if (QUERYSTRING_PARAMS_ENUM_VALUES[param].includes(value)) { + continue; + } + } else if (expectedDataType === "enum-array") { + const values = parseArray(value); + if ( + values.every((value) => + QUERYSTRING_PARAMS_ENUM_VALUES[param].includes(value), + ) + ) { + continue; + } + } else if (expectedDataType === "number") { + if (!isNaN(value)) { + continue; + } + } else if (expectedDataType === "boolean") { + if (value === "true" || value === "false") { + continue; + } + } else if (expectedDataType === "array") { + if (Array.isArray(parseArray(value))) { + continue; + } + } else if (expectedDataType === "string") { + if (typeof value === "string") { + continue; + } + } + throw new InvalidQueryStringParamsError( + `Invalid query string param: ${param}`, + getInvalidQueryStringParamsErrorSecondaryMessage( + param, + queryStringParamsDataTypeMap, + ), + ); + } +}; + +export { validateQueryStringParams }; diff --git a/tests/api.test.js b/tests/api.test.js index a6ed04b5..2cc1854f 100644 --- a/tests/api.test.js +++ b/tests/api.test.js @@ -6,6 +6,7 @@ import { calculateRank } from "../src/calculateRank.js"; import { renderStatsCard } from "../src/cards/stats-card.js"; import { CONSTANTS, renderError } from "../src/common/utils.js"; import { expect, it, describe, afterEach } from "@jest/globals"; +import { availableLocales } from "../src/translations.js"; const stats = { name: "Anurag Hazra", @@ -125,8 +126,8 @@ describe("Test /api/", () => { { username: "anuraghazra", hide: "issues,prs,contribs", - show_icons: true, - hide_border: true, + show_icons: "true", + hide_border: "true", line_height: 100, title_color: "fff", icon_color: "fff", @@ -255,8 +256,8 @@ describe("Test /api/", () => { { username: "anuraghazra", hide: "issues,prs,contribs", - show_icons: true, - hide_border: true, + show_icons: "true", + hide_border: "true", line_height: 100, title_color: "fff", ring_color: "0000ff", @@ -301,7 +302,10 @@ describe("Test /api/", () => { expect(res.setHeader).toBeCalledWith("Content-Type", "image/svg+xml"); expect(res.send).toBeCalledWith( - renderError("Something went wrong", "Language not found"), + renderError( + "Invalid query string param: locale", + `Expected: ${availableLocales.join(", ")}`, + ), ); }); @@ -311,7 +315,7 @@ describe("Test /api/", () => { .reply(200, { error: "Some test error message" }); const { req, res } = faker( - { username: "anuraghazra", include_all_commits: true }, + { username: "anuraghazra", include_all_commits: "true" }, data_stats, );