Merge branch 'user-pats'
This commit is contained in:
@@ -1,4 +1,4 @@
|
||||
import { getUserAccess, deleteUser } from "../src/common/database.js";
|
||||
import { getUserAccessByKey, deleteUser } from "../src/common/database.js";
|
||||
import axios from "axios";
|
||||
import { logger } from "../src/common/log.js";
|
||||
|
||||
@@ -23,7 +23,7 @@ export default async (req, res) => {
|
||||
}
|
||||
|
||||
// get token and private access status
|
||||
const userAccess = await getUserAccess(user_key);
|
||||
const userAccess = await getUserAccessByKey(user_key);
|
||||
if (!userAccess) {
|
||||
res.statusCode = 404;
|
||||
res.send("user not found");
|
||||
|
||||
@@ -87,7 +87,7 @@ export default async (req, res) => {
|
||||
try {
|
||||
let PATsValid = true;
|
||||
try {
|
||||
await retryer(uptimeFetcher, {});
|
||||
await retryer(uptimeFetcher, null, {});
|
||||
} catch (err) {
|
||||
// Resolve eslint no-unused-vars
|
||||
err;
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { logger } from "../src/common/log.js";
|
||||
import { getUserAccess } from "../src/common/database.js";
|
||||
import { getUserAccessByKey } from "../src/common/database.js";
|
||||
|
||||
/**
|
||||
* @param {any} req The request.
|
||||
@@ -8,7 +8,7 @@ import { getUserAccess } from "../src/common/database.js";
|
||||
export default async (req, res) => {
|
||||
const { user_key } = req.query;
|
||||
try {
|
||||
const result = await getUserAccess(user_key);
|
||||
const result = await getUserAccessByKey(user_key);
|
||||
|
||||
if (!result) {
|
||||
res.statusCode = 404;
|
||||
|
||||
@@ -197,7 +197,7 @@ export async function deleteUser(userKey) {
|
||||
* @param {string} userKey user key of the user to fetch information for
|
||||
* @returns {Promise<{token: string, privateAccess: boolean} | null>} token and private access status, or null if user not found
|
||||
*/
|
||||
export async function getUserAccess(userKey) {
|
||||
export async function getUserAccessByKey(userKey) {
|
||||
if (!pool) {
|
||||
return null;
|
||||
}
|
||||
@@ -225,3 +225,38 @@ export async function getUserAccess(userKey) {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetches token and private access status for a given username.
|
||||
*
|
||||
* @param {string} userName GitHub username of the user to fetch information for
|
||||
* @returns {Promise<{token: string, privateAccess: boolean} | null>} token and private access status, or null if user not found
|
||||
*/
|
||||
export async function getUserAccessByName(userName) {
|
||||
if (!pool) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const query = `
|
||||
SELECT access_token, private_access
|
||||
FROM authenticated_users
|
||||
WHERE user_id = $1
|
||||
LIMIT 1
|
||||
`;
|
||||
try {
|
||||
const { rows } = await pool.query(query, [userName]);
|
||||
if (rows.length === 0) {
|
||||
return null;
|
||||
}
|
||||
return {
|
||||
token: rows[0].access_token,
|
||||
privateAccess: rows[0].private_access
|
||||
};
|
||||
} catch (err) {
|
||||
if (err.code === "42P01") {
|
||||
return null;
|
||||
} else {
|
||||
throw err;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,19 +2,12 @@
|
||||
|
||||
import { CustomError } from "./error.js";
|
||||
import { logger } from "./log.js";
|
||||
import { getUserAccessByKey, getUserAccessByName } from "./database.js";
|
||||
|
||||
function getRandomInt(max) {
|
||||
return Math.floor(Math.random() * max);
|
||||
}
|
||||
|
||||
// Script variables.
|
||||
|
||||
// Count the number of GitHub API tokens available.
|
||||
const PATs = Object.keys(process.env).filter((key) =>
|
||||
/PAT_\d*$/.exec(key),
|
||||
).length;
|
||||
const RETRIES = process.env.NODE_ENV === "test" ? 7 : PATs;
|
||||
|
||||
/**
|
||||
* @typedef {import("axios").AxiosResponse} AxiosResponse Axios response.
|
||||
* @typedef {(variables: any, token: string, retriesForTests?: number) => Promise<AxiosResponse>} FetcherFunction Fetcher function.
|
||||
@@ -24,22 +17,39 @@ const RETRIES = process.env.NODE_ENV === "test" ? 7 : PATs;
|
||||
* Try to execute the fetcher function until it succeeds or the max number of retries is reached.
|
||||
*
|
||||
* @param {FetcherFunction} fetcher The fetcher function.
|
||||
* @param username GitHub username of the user whose PAT to use, if available
|
||||
* @param {any} variables Object with arguments to pass to the fetcher function.
|
||||
* @returns {Promise<any>} The response from the fetcher function.
|
||||
*/
|
||||
const retryer = async (fetcher, variables) => {
|
||||
const retryer = async (fetcher, username, variables) => {
|
||||
let userPAT;
|
||||
if (username) {
|
||||
userPAT = await getUserAccessByName(username);
|
||||
}
|
||||
|
||||
let PATs;
|
||||
if (userPAT) {
|
||||
PATs = [userPAT.token];
|
||||
} else {
|
||||
// Count the number of GitHub API tokens available.
|
||||
PATs = Object.keys(process.env).filter((key) =>
|
||||
/PAT_\d*$/.exec(key),
|
||||
);
|
||||
}
|
||||
const RETRIES = process.env.NODE_ENV === "test" ? 7 : PATs.length;
|
||||
|
||||
if (!RETRIES) {
|
||||
throw new CustomError("No GitHub API tokens found", CustomError.NO_TOKENS);
|
||||
}
|
||||
const startPAT = getRandomInt(PATs);
|
||||
const startPAT = getRandomInt(PATs.length);
|
||||
|
||||
for (let retries = 0; retries < RETRIES; retries++) {
|
||||
const currentPAT = ((startPAT + retries) % PATs) + 1;
|
||||
const currentPAT = ((startPAT + retries) % PATs.length);
|
||||
try {
|
||||
let response = await fetcher(
|
||||
variables,
|
||||
// @ts-ignore
|
||||
process.env[`PAT_${currentPAT}`],
|
||||
PATs[currentPAT],
|
||||
// used in tests for faking rate limit
|
||||
retries,
|
||||
);
|
||||
@@ -89,5 +99,5 @@ const retryer = async (fetcher, variables) => {
|
||||
);
|
||||
};
|
||||
|
||||
export { retryer, RETRIES };
|
||||
export { retryer };
|
||||
export default retryer;
|
||||
|
||||
@@ -90,7 +90,7 @@ const fetchGist = async (id) => {
|
||||
if (!id) {
|
||||
throw new MissingParamError(["id"], "/api/gist?id=GIST_ID");
|
||||
}
|
||||
const res = await retryer(fetcher, { gistName: id });
|
||||
const res = await retryer(fetcher, null, { gistName: id });
|
||||
if (res.data.errors) {
|
||||
throw new Error(res.data.errors[0].message);
|
||||
}
|
||||
|
||||
@@ -99,7 +99,7 @@ const fetchRepo = async (
|
||||
throw new MissingParamError(["repo"], urlExample);
|
||||
}
|
||||
|
||||
let res = await retryer(fetcher, { login: owner, repo: reponame });
|
||||
let res = await retryer(fetcher, username, { login: owner, repo: reponame });
|
||||
|
||||
const data = res.data.data;
|
||||
|
||||
|
||||
@@ -139,7 +139,7 @@ const statsFetcher = async ({
|
||||
startTime,
|
||||
ownerAffiliations,
|
||||
};
|
||||
let res = await retryer(fetcher, variables);
|
||||
let res = await retryer(fetcher, username, variables);
|
||||
if (res.data.errors) {
|
||||
return res;
|
||||
}
|
||||
@@ -221,7 +221,7 @@ const totalItemsFetcher = async (username, repo, owner, type, filter) => {
|
||||
|
||||
let res;
|
||||
try {
|
||||
res = await retryer(fetchTotalItems, {
|
||||
res = await retryer(fetchTotalItems, username, {
|
||||
login: username,
|
||||
repo,
|
||||
owner,
|
||||
|
||||
@@ -73,7 +73,7 @@ const fetchTopLanguages = async (
|
||||
}
|
||||
ownerAffiliations = parseOwnerAffiliations(ownerAffiliations);
|
||||
|
||||
const res = await retryer(fetcher, { login: username, ownerAffiliations });
|
||||
const res = await retryer(fetcher, username, { login: username, ownerAffiliations });
|
||||
|
||||
if (res.data.errors) {
|
||||
logger.error(res.data.errors);
|
||||
|
||||
@@ -12,7 +12,7 @@
|
||||
## GitHub Stats Card
|
||||
|
||||
> [!WARNING]
|
||||
> By default, the stats card only shows statistics like stars, commits, and pull requests from public repositories. To show private statistics on the stats card, you should [deploy your own instance](deploy.md) using your own GitHub API token.
|
||||
> By default, the stats card only shows statistics like stars, commits, and pull requests from public repositories. To show private statistics on the stats card, [allow GitHub-Stats-Extended to access your private contributions](fork.md#private-contributions-support) or [deploy your own instance](deploy.md).
|
||||
|
||||
> [!NOTE]
|
||||
> Available ranks are S (top 1%), A+ (12.5%), A (25%), A- (37.5%), B+ (50%), B (62.5%), B- (75%), C+ (87.5%) and C (everyone). This ranking scheme is based on the [Japanese academic grading](https://wikipedia.org/wiki/Academic_grading_in_Japan) system. The global percentile is calculated as a weighted sum of percentiles for each statistic (number of commits, pull requests, reviews, issues, stars, and followers), based on the cumulative distribution function of the [exponential](https://wikipedia.org/wiki/exponential_distribution) and the [log-normal](https://wikipedia.org/wiki/Log-normal_distribution) distributions. The implementation can be investigated at [src/calculateRank.js](https://github.com/stats-organization/github-stats-extended/blob/master/backend/src/calculateRank.js). The circle around the rank shows 100 minus the global percentile.
|
||||
@@ -381,7 +381,7 @@ Use [show\_owner](#options-1) query option to include the gist's owner username
|
||||
The top languages card shows your most frequently used languages.
|
||||
|
||||
> [!WARNING]
|
||||
> By default, the language card shows language results only from public repositories. To include languages used in private repositories, you should [deploy your own instance](deploy.md) using your own GitHub API token.
|
||||
> By default, the language card shows language results only from public repositories. To include languages used in private repositories, [allow GitHub-Stats-Extended to access your private contributions](fork.md#private-contributions-support) or [deploy your own instance](deploy.md).
|
||||
|
||||
> [!WARNING]
|
||||
> This card shows language usage only inside your own non-forked repositories, not depending on who the author of the commits is. It does not include your contributions into another users/organizations repositories. Currently there are no way to get this data from GitHub API. If you want this behavior to be improved you can support [this feature request](https://github.com/orgs/community/discussions/18230) created by [@rickstaa](https://github.com/rickstaa) inside GitHub Community.
|
||||
|
||||
@@ -29,6 +29,10 @@ const SvgInline = (props) => {
|
||||
setShouldMock(stage === 0 || !isAuthenticated);
|
||||
}, [isAuthenticated, props.stage]);
|
||||
|
||||
useEffect(async () => {
|
||||
await axios.storage.clear();
|
||||
}, [userToken]);
|
||||
|
||||
useEffect(() => {
|
||||
const loadSvg = async () => {
|
||||
process.env.PAT_1 = userToken;
|
||||
|
||||
@@ -67,7 +67,7 @@ function App() {
|
||||
if (userKey && userKey.length > 0) {
|
||||
const userAccess = await getUserMetadata(userKey);
|
||||
if (userAccess === null) {
|
||||
dispatch(_logout());
|
||||
dispatch(_logout(userKey));
|
||||
} else {
|
||||
setUserAccess(userAccess);
|
||||
}
|
||||
|
||||
@@ -6,8 +6,8 @@ export function login(userId, userKey) {
|
||||
return { type: LOGIN, payload: { userId, userKey } };
|
||||
}
|
||||
|
||||
export function logout() {
|
||||
return { type: LOGOUT, payload: {} };
|
||||
export function logout(userKey = null) {
|
||||
return { type: LOGOUT, payload: { userKey: userKey } };
|
||||
}
|
||||
|
||||
export function setUserAccess(token, privateAccess) {
|
||||
|
||||
@@ -19,6 +19,12 @@ export default (state = initialState, action) => {
|
||||
userKey: action.payload.userKey,
|
||||
};
|
||||
case types.LOGOUT:
|
||||
if (
|
||||
action.payload.userKey === null ||
|
||||
action.payload.userKey !== localStorage.getItem('userKey')
|
||||
) {
|
||||
return;
|
||||
}
|
||||
localStorage.clear();
|
||||
return {
|
||||
userId: null,
|
||||
|
||||
Reference in New Issue
Block a user