From 195e2fc72d5b59c05c8717514d9be90509080014 Mon Sep 17 00:00:00 2001 From: Ophelia Goldstein <159258143+opheliagoldstein@users.noreply.github.com> Date: Tue, 7 Oct 2025 21:13:13 +0300 Subject: [PATCH] refactor: move access guards logic into reusable functions (#4535) * refactor: move access guards logic into reusable functions * Update src/common/access.js Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --------- Co-authored-by: Alexandr Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- api/gist.js | 31 +++++++++++----------- api/index.js | 49 +++++++++++------------------------ api/pin.js | 49 +++++++++++------------------------ api/top-langs.js | 49 +++++++++++------------------------ api/wakatime.js | 31 +++++++++++----------- src/common/access.js | 61 ++++++++++++++++++++++++++++++++++++++++++++ 6 files changed, 136 insertions(+), 134 deletions(-) create mode 100644 src/common/access.js diff --git a/api/gist.js b/api/gist.js index 435c6b72..aab85ba9 100644 --- a/api/gist.js +++ b/api/gist.js @@ -1,7 +1,6 @@ // @ts-check import { CONSTANTS, renderError, parseBoolean } from "../src/common/utils.js"; -import { gistWhitelist } from "../src/common/envs.js"; import { isLocaleAvailable } from "../src/translations.js"; import { renderGistCard } from "../src/cards/gist.js"; import { fetchGist } from "../src/fetchers/gist.js"; @@ -10,6 +9,7 @@ import { setCacheHeaders, setErrorCacheHeaders, } from "../src/common/cache.js"; +import { guardAccess } from "../src/common/access.js"; export default async (req, res) => { const { @@ -29,21 +29,20 @@ export default async (req, res) => { res.setHeader("Content-Type", "image/svg+xml"); - if (gistWhitelist && !gistWhitelist.includes(id)) { - return res.send( - renderError( - "This gist ID is not whitelisted", - "Please deploy your own instance", - { - title_color, - text_color, - bg_color, - border_color, - theme, - show_repo_link: false, - }, - ), - ); + const access = guardAccess({ + res, + id, + type: "gist", + colors: { + title_color, + text_color, + bg_color, + border_color, + theme, + }, + }); + if (!access.isPassed) { + return access.result; } if (locale && !isLocaleAvailable(locale)) { diff --git a/api/index.js b/api/index.js index 88ffd2db..a5887fb0 100644 --- a/api/index.js +++ b/api/index.js @@ -1,13 +1,12 @@ // @ts-check import { renderStatsCard } from "../src/cards/stats.js"; -import { blacklist } from "../src/common/blacklist.js"; +import { guardAccess } from "../src/common/access.js"; import { resolveCacheSeconds, setCacheHeaders, setErrorCacheHeaders, } from "../src/common/cache.js"; -import { whitelist } from "../src/common/envs.js"; import { CONSTANTS, parseArray, @@ -49,38 +48,20 @@ export default async (req, res) => { } = req.query; res.setHeader("Content-Type", "image/svg+xml"); - if (whitelist && !whitelist.includes(username)) { - return res.send( - renderError( - "This username is not whitelisted", - "Please deploy your own instance", - { - title_color, - text_color, - bg_color, - border_color, - theme, - show_repo_link: false, - }, - ), - ); - } - - if (whitelist === undefined && blacklist.includes(username)) { - return res.send( - renderError( - "This username is blacklisted", - "Please deploy your own instance", - { - title_color, - text_color, - bg_color, - border_color, - theme, - show_repo_link: false, - }, - ), - ); + const access = guardAccess({ + res, + id: username, + type: "username", + colors: { + title_color, + text_color, + bg_color, + border_color, + theme, + }, + }); + if (!access.isPassed) { + return access.result; } if (locale && !isLocaleAvailable(locale)) { diff --git a/api/pin.js b/api/pin.js index 2907774c..ebb0d8af 100644 --- a/api/pin.js +++ b/api/pin.js @@ -1,13 +1,12 @@ // @ts-check import { renderRepoCard } from "../src/cards/repo.js"; -import { blacklist } from "../src/common/blacklist.js"; +import { guardAccess } from "../src/common/access.js"; import { resolveCacheSeconds, setCacheHeaders, setErrorCacheHeaders, } from "../src/common/cache.js"; -import { whitelist } from "../src/common/envs.js"; import { CONSTANTS, parseBoolean, renderError } from "../src/common/utils.js"; import { fetchRepo } from "../src/fetchers/repo.js"; import { isLocaleAvailable } from "../src/translations.js"; @@ -32,38 +31,20 @@ export default async (req, res) => { res.setHeader("Content-Type", "image/svg+xml"); - if (whitelist && !whitelist.includes(username)) { - return res.send( - renderError( - "This username is not whitelisted", - "Please deploy your own instance", - { - title_color, - text_color, - bg_color, - border_color, - theme, - show_repo_link: false, - }, - ), - ); - } - - if (whitelist === undefined && blacklist.includes(username)) { - return res.send( - renderError( - "This username is blacklisted", - "Please deploy your own instance", - { - title_color, - text_color, - bg_color, - border_color, - theme, - show_repo_link: false, - }, - ), - ); + const access = guardAccess({ + res, + id: username, + type: "username", + colors: { + title_color, + text_color, + bg_color, + border_color, + theme, + }, + }); + if (!access.isPassed) { + return access.result; } if (locale && !isLocaleAvailable(locale)) { diff --git a/api/top-langs.js b/api/top-langs.js index 9e7df07f..f80528ae 100644 --- a/api/top-langs.js +++ b/api/top-langs.js @@ -1,13 +1,12 @@ // @ts-check import { renderTopLanguages } from "../src/cards/top-languages.js"; -import { blacklist } from "../src/common/blacklist.js"; +import { guardAccess } from "../src/common/access.js"; import { resolveCacheSeconds, setCacheHeaders, setErrorCacheHeaders, } from "../src/common/cache.js"; -import { whitelist } from "../src/common/envs.js"; import { CONSTANTS, parseArray, @@ -44,38 +43,20 @@ export default async (req, res) => { } = req.query; res.setHeader("Content-Type", "image/svg+xml"); - if (whitelist && !whitelist.includes(username)) { - return res.send( - renderError( - "This username is not whitelisted", - "Please deploy your own instance", - { - title_color, - text_color, - bg_color, - border_color, - theme, - show_repo_link: false, - }, - ), - ); - } - - if (whitelist === undefined && blacklist.includes(username)) { - return res.send( - renderError( - "This username is blacklisted", - "Please deploy your own instance", - { - title_color, - text_color, - bg_color, - border_color, - theme, - show_repo_link: false, - }, - ), - ); + const access = guardAccess({ + res, + id: username, + type: "username", + colors: { + title_color, + text_color, + bg_color, + border_color, + theme, + }, + }); + if (!access.isPassed) { + return access.result; } if (locale && !isLocaleAvailable(locale)) { diff --git a/api/wakatime.js b/api/wakatime.js index e3fedd5d..9cbf779f 100644 --- a/api/wakatime.js +++ b/api/wakatime.js @@ -7,7 +7,6 @@ import { parseBoolean, renderError, } from "../src/common/utils.js"; -import { whitelist } from "../src/common/envs.js"; import { fetchWakatimeStats } from "../src/fetchers/wakatime.js"; import { isLocaleAvailable } from "../src/translations.js"; import { @@ -15,6 +14,7 @@ import { setCacheHeaders, setErrorCacheHeaders, } from "../src/common/cache.js"; +import { guardAccess } from "../src/common/access.js"; export default async (req, res) => { const { @@ -44,21 +44,20 @@ export default async (req, res) => { res.setHeader("Content-Type", "image/svg+xml"); - if (whitelist && !whitelist.includes(username)) { - return res.send( - renderError( - "This username is not whitelisted", - "Please deploy your own instance", - { - title_color, - text_color, - bg_color, - border_color, - theme, - show_repo_link: false, - }, - ), - ); + const access = guardAccess({ + res, + id: username, + type: "wakatime", + colors: { + title_color, + text_color, + bg_color, + border_color, + theme, + }, + }); + if (!access.isPassed) { + return access.result; } if (locale && !isLocaleAvailable(locale)) { diff --git a/src/common/access.js b/src/common/access.js new file mode 100644 index 00000000..959e17af --- /dev/null +++ b/src/common/access.js @@ -0,0 +1,61 @@ +// @ts-check + +import { renderError } from "./utils.js"; +import { blacklist } from "./blacklist.js"; +import { whitelist, gistWhitelist } from "./envs.js"; + +const NOT_WHITELISTED_USERNAME_MESSAGE = "This username is not whitelisted"; +const NOT_WHITELISTED_GIST_MESSAGE = "This gist ID is not whitelisted"; +const BLACKLISTED_MESSAGE = "This username is blacklisted"; + +/** + * Guards access using whitelist/blacklist. + * + * @param {Object} args The parameters object. + * @param {Object} args.res The response object. + * @param {string} args.id Resource identifier (username or gist id). + * @param {"username"|"gist"|"wakatime"} args.type The type of identifier. + * @param {{ title_color?: string, text_color?: string, bg_color?: string, border_color?: string, theme?: string }} args.colors Color options for the error card. + * @returns {{ isPassed: boolean, result?: any }} The result object indicating success or failure. + */ +const guardAccess = ({ res, id, type, colors }) => { + if (!["username", "gist", "wakatime"].includes(type)) { + throw new Error( + 'Invalid type. Expected "username", "gist", or "wakatime".', + ); + } + + const currentWhitelist = type === "gist" ? gistWhitelist : whitelist; + const notWhitelistedMsg = + type === "gist" + ? NOT_WHITELISTED_GIST_MESSAGE + : NOT_WHITELISTED_USERNAME_MESSAGE; + + if (Array.isArray(currentWhitelist) && !currentWhitelist.includes(id)) { + const result = res.send( + renderError(notWhitelistedMsg, "Please deploy your own instance", { + ...colors, + show_repo_link: false, + }), + ); + return { isPassed: false, result }; + } + + if ( + type === "username" && + currentWhitelist === undefined && + blacklist.includes(id) + ) { + const result = res.send( + renderError(BLACKLISTED_MESSAGE, "Please deploy your own instance", { + ...colors, + show_repo_link: false, + }), + ); + return { isPassed: false, result }; + } + + return { isPassed: true }; +}; + +export { guardAccess };