Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 5 additions & 4 deletions src/commands/gh/list.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,17 +28,17 @@ export default class List extends Command {
return;
}

const authSpinner = ora({
text: 'Getting user GitHub repos',
});
try {
let getMore = true;
let page = 1;
const repos: Repo[] = [];
const authSpinner = ora({
text: 'Getting user GitHub repos',
}).start();
do {
authSpinner.start();
const pageRepos = await getUserRepositories(page);
authSpinner.stop();

repos.push(...pageRepos);
this.log('');
printTable({
Expand Down Expand Up @@ -81,6 +81,7 @@ export default class List extends Command {
page++;
} while (getMore);
} catch (err) {
authSpinner.stop();
if (err instanceof Error) {
if (err.name === 'ExitPromptError') {
this.log('User exited');
Expand Down
2 changes: 2 additions & 0 deletions src/config/gh.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ const DEFAULT_GH_CLIENT_ID = 'Iv23liIePcrOHZ5tdjx7';
const DEFAULT_GH_SERVICE_NAME = '@herodevs/cli';
const DEFAULT_GH_ACCESS_KEY = 'gh_access_token';
const DEFAULT_GH_REFRESH_KEY = 'gh_refresh_token';
const DEFAULT_GH_TOKENS_URL = 'https://github.com/login/oauth/access_token';
const DEFAULT_GH_API_BASE_URL = 'https://api.github.com/';
const DEFAULT_GH_API_VERSION = '2022-11-28';
const DEFAULT_GH_REPOS_PER_PAGE = 5;
Expand All @@ -12,6 +13,7 @@ export const GH_CLIENT_ID = process.env.GH_CLIENT_ID ?? DEFAULT_GH_CLIENT_ID;
export const GH_SERVICE_NAME = process.env.GH_SERVICE_NAME ?? DEFAULT_GH_SERVICE_NAME;
export const GH_ACCESS_KEY = process.env.GH_ACCESS_KEY ?? DEFAULT_GH_ACCESS_KEY;
export const GH_REFRESH_KEY = process.env.GH_REFRESH_KEY ?? DEFAULT_GH_REFRESH_KEY;
export const GH_TOKENS_URL = process.env.GH_TOKENS_URL ?? DEFAULT_GH_TOKENS_URL;
export const GH_API_BASE_URL = process.env.GH_API_BASE_URL ?? DEFAULT_GH_API_BASE_URL;
export const GH_API_VERSION = process.env.GH_API_VERSION ?? DEFAULT_GH_API_VERSION;
export const GH_REPOS_PER_PAGE = Number(process.env.GH_REPOS_PER_PAGE ?? DEFAULT_GH_REPOS_PER_PAGE);
Expand Down
44 changes: 39 additions & 5 deletions src/service/gh.svc.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,15 +5,47 @@ import {
GH_API_BASE_URL,
GH_API_VERSION,
GH_APP_OAUTH_SETTINGS,
GH_CLIENT_ID,
GH_REFRESH_KEY,
GH_REPOS_PER_PAGE,
GH_SERVICE_NAME,
GH_TOKENS_URL,
} from '../config/gh.config.js';
import type { RefreshTokenResponse } from '../types/gh/refresh-token-response.js';
import type { Repo } from '../types/gh/repo.js';

const accessTokenEntry = new Entry(GH_SERVICE_NAME, GH_ACCESS_KEY);
const refreshTokenEntry = new Entry(GH_SERVICE_NAME, GH_REFRESH_KEY);

const refreshAccessToken = async () => {
if (!userRefreshToken()) {
return false;
}
const response = await fetch(
`${GH_TOKENS_URL}?client_id=${GH_CLIENT_ID}&client_secret=&grant_type=refresh_token&refresh_token=${userRefreshToken()}`,
{
method: 'POST',
headers: {
Accept: 'application/vnd.github+json',
'X-GitHub-Api-Version': GH_API_VERSION,
},
},
);
if (!response.ok) {
accessTokenEntry.deletePassword();
refreshTokenEntry.deletePassword();
return false;
}
const refreshedAccessTokenResponse = (await response.json()) as unknown as RefreshTokenResponse;
if ('error' in refreshedAccessTokenResponse) {
return false;
}
const { access_token, refresh_token } = refreshedAccessTokenResponse;
accessTokenEntry.setPassword(access_token);
refreshTokenEntry.setPassword(refresh_token);
return true;
};

const ghFetch = async <T>(endpoint: string, method: 'GET' | 'POST' | 'PUT' | 'DELETE', body?: unknown) => {
const response = await fetch(`${GH_API_BASE_URL}${endpoint}`, {
method,
Expand All @@ -24,13 +56,16 @@ const ghFetch = async <T>(endpoint: string, method: 'GET' | 'POST' | 'PUT' | 'DE
},
...(body ? { body: JSON.stringify(body) } : {}),
});

if (!response.ok) {
switch (response.status) {
case 401:
throw new Error(
`Unauthorized access to ${GH_API_BASE_URL}${endpoint}. Please authorize the CLI running the gh authorize command`,
);
if (await refreshAccessToken()) {
return await ghFetch(endpoint, method, body);
} else {
throw new Error(
`Unauthorized access to ${GH_API_BASE_URL}${endpoint}. Please authorize the CLI by running the gh authorize command`,
);
}
case 403:
throw new Error(`Forbidden access to ${GH_API_BASE_URL}${endpoint}`);
}
Expand All @@ -52,7 +87,6 @@ export const authenticateWithDeviceFlow = async (onVerification: GitHubAppStrate
...GH_APP_OAUTH_SETTINGS,
onVerification,
});

try {
const authResponse = await auth({
type: 'oauth',
Expand Down
9 changes: 9 additions & 0 deletions src/types/gh/refresh-token-response.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
export type RefreshTokenResponse = {
access_token: string;
expires_in: number;
refresh_token: string;
refresh_token_expires_in: number;
token_type: string;
scope?: string;
error?: string;
};