feat: implement whitelist for self-hosted instances (#3939)

* whitelist for username based endpoints

* added gist whitelist

* review

* fix

---------

Co-authored-by: Alexandr <qwerty541zxc@gmail.com>
This commit is contained in:
Kyle Upton
2025-09-15 23:13:42 +03:00
committed by GitHub
co-authored by Alexandr
parent 70add4cfcf
commit 3987991058
11 changed files with 168 additions and 32 deletions
+18
View File
@@ -4,6 +4,7 @@ import {
renderError,
parseBoolean,
} from "../src/common/utils.js";
import { gistWhitelist } from "../src/common/whitelist.js";
import { isLocaleAvailable } from "../src/translations.js";
import { renderGistCard } from "../src/cards/gist.js";
import { fetchGist } from "../src/fetchers/gist.js";
@@ -26,6 +27,23 @@ 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,
},
),
);
}
if (locale && !isLocaleAvailable(locale)) {
return res.send(
renderError("Something went wrong", "Language not found", {
+31 -8
View File
@@ -1,5 +1,6 @@
import { renderStatsCard } from "../src/cards/stats.js";
import { blacklist } from "../src/common/blacklist.js";
import { whitelist } from "../src/common/whitelist.js";
import {
clampValue,
CONSTANTS,
@@ -41,15 +42,37 @@ export default async (req, res) => {
} = req.query;
res.setHeader("Content-Type", "image/svg+xml");
if (blacklist.includes(username)) {
if (whitelist && !whitelist.includes(username)) {
return res.send(
renderError("Something went wrong", "This username is blacklisted", {
title_color,
text_color,
bg_color,
border_color,
theme,
}),
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,
},
),
);
}
+31 -8
View File
@@ -1,5 +1,6 @@
import { renderRepoCard } from "../src/cards/repo.js";
import { blacklist } from "../src/common/blacklist.js";
import { whitelist } from "../src/common/whitelist.js";
import {
clampValue,
CONSTANTS,
@@ -29,15 +30,37 @@ export default async (req, res) => {
res.setHeader("Content-Type", "image/svg+xml");
if (blacklist.includes(username)) {
if (whitelist && !whitelist.includes(username)) {
return res.send(
renderError("Something went wrong", "This username is blacklisted", {
title_color,
text_color,
bg_color,
border_color,
theme,
}),
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,
},
),
);
}
+31 -8
View File
@@ -1,5 +1,6 @@
import { renderTopLanguages } from "../src/cards/top-languages.js";
import { blacklist } from "../src/common/blacklist.js";
import { whitelist } from "../src/common/whitelist.js";
import {
clampValue,
CONSTANTS,
@@ -36,15 +37,37 @@ export default async (req, res) => {
} = req.query;
res.setHeader("Content-Type", "image/svg+xml");
if (blacklist.includes(username)) {
if (whitelist && !whitelist.includes(username)) {
return res.send(
renderError("Something went wrong", "This username is blacklisted", {
title_color,
text_color,
bg_color,
border_color,
theme,
}),
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,
},
),
);
}
+18
View File
@@ -6,6 +6,7 @@ import {
parseBoolean,
renderError,
} from "../src/common/utils.js";
import { whitelist } from "../src/common/whitelist.js";
import { fetchWakatimeStats } from "../src/fetchers/wakatime.js";
import { isLocaleAvailable } from "../src/translations.js";
@@ -36,6 +37,23 @@ 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 (locale && !isLocaleAvailable(locale)) {
return res.send(
renderError("Something went wrong", "Language not found", {
+6 -4
View File
@@ -86,7 +86,7 @@
- [On Vercel](#on-vercel)
- [:film\_projector: Check Out Step By Step Video Tutorial By @codeSTACKr](#film_projector-check-out-step-by-step-video-tutorial-by-codestackr)
- [On other platforms](#on-other-platforms)
- [Disable rate limit protections](#disable-rate-limit-protections)
- [Available environment variables](#available-environment-variables)
- [Keep your fork up to date](#keep-your-fork-up-to-date)
- [:sparkling\_heart: Support the project](#sparkling_heart-support-the-project)
</details>
@@ -797,11 +797,13 @@ Since the GitHub API only allows 5k requests per hour, my `https://github-readme
5. You're done 🎉
</details>
## Disable rate limit protections
## Available environment variables
GitHub Readme Stats contains several Vercel environment variables that can be used to remove the rate limit protections:
GitHub Readme Stats provides several environment variables that can be used to customize the behavior of your self-hosted instance. These include:
* `CACHE_SECONDS`: This environment variable takes precedence over our cache minimum and maximum values and can circumvent these values for self-hosted Vercel instances.
* `CACHE_SECONDS`: This takes precedence over our cache minimum and maximum values and can circumvent these values for self-hosted instances.
* `WHITELIST`: A comma-separated list of GitHub usernames that are allowed to access your instance. If this variable is not set, all usernames are allowed.
* `GIST_WHITELIST`: A comma-separated list of GitHub gist IDs that are allowed to be accessed on your instance. If this variable is not set, all gist IDs are allowed.
See [the Vercel documentation](https://vercel.com/docs/concepts/projects/environment-variables) on adding these environment variables to your Vercel instance.
+8 -1
View File
@@ -355,6 +355,12 @@ const UPSTREAM_API_ERRORS = [
* @param {string} message Main error message.
* @param {string} secondaryMessage The secondary error message.
* @param {object} options Function options.
* @param {string=} options.title_color Card title color.
* @param {string=} options.text_color Card text color.
* @param {string=} options.bg_color Card background color.
* @param {string=} options.border_color Card border color.
* @param {string=} options.theme Card theme.
* @param {boolean=} options.show_repo_link Whether to show repo link or not.
* @returns {string} The SVG markup.
*/
const renderError = (message, secondaryMessage = "", options = {}) => {
@@ -364,6 +370,7 @@ const renderError = (message, secondaryMessage = "", options = {}) => {
bg_color,
border_color,
theme = "default",
show_repo_link = true,
} = options;
// returns theme based colors with proper overrides and defaults
@@ -388,7 +395,7 @@ const renderError = (message, secondaryMessage = "", options = {}) => {
ERROR_CARD_LENGTH - 1
}" height="99%" rx="4.5" fill="${bgColor}" stroke="${borderColor}"/>
<text x="25" y="45" class="text">Something went wrong!${
UPSTREAM_API_ERRORS.includes(secondaryMessage)
UPSTREAM_API_ERRORS.includes(secondaryMessage) || !show_repo_link
? ""
: " file an issue at https://tiny.one/readme-stats"
}</text>
+10
View File
@@ -0,0 +1,10 @@
const whitelist = process.env.WHITELIST
? process.env.WHITELIST.split(",")
: undefined;
const gistWhitelist = process.env.GIST_WHITELIST
? process.env.GIST_WHITELIST.split(",")
: undefined;
export { whitelist, gistWhitelist };
export default whitelist;
+5 -1
View File
@@ -307,7 +307,11 @@ describe("Test /api/", () => {
expect(res.setHeader).toBeCalledWith("Content-Type", "image/svg+xml");
expect(res.send).toBeCalledWith(
renderError("Something went wrong", "This username is blacklisted"),
renderError(
"This username is blacklisted",
"Please deploy your own instance",
{ show_repo_link: false },
),
);
});
+5 -1
View File
@@ -156,7 +156,11 @@ describe("Test /api/pin", () => {
expect(res.setHeader).toBeCalledWith("Content-Type", "image/svg+xml");
expect(res.send).toBeCalledWith(
renderError("Something went wrong", "This username is blacklisted"),
renderError(
"This username is blacklisted",
"Please deploy your own instance",
{ show_repo_link: false },
),
);
});
+5 -1
View File
@@ -184,7 +184,11 @@ describe("Test /api/top-langs", () => {
expect(res.setHeader).toBeCalledWith("Content-Type", "image/svg+xml");
expect(res.send).toBeCalledWith(
renderError("Something went wrong", "This username is blacklisted"),
renderError(
"This username is blacklisted",
"Please deploy your own instance",
{ show_repo_link: false },
),
);
});