diff --git a/src/commands/gh/list.ts b/src/commands/gh/list.ts index f5d296e4..2e576341 100644 --- a/src/commands/gh/list.ts +++ b/src/commands/gh/list.ts @@ -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({ @@ -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'); diff --git a/src/config/gh.config.ts b/src/config/gh.config.ts index 1c027b13..f6341ef1 100644 --- a/src/config/gh.config.ts +++ b/src/config/gh.config.ts @@ -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; @@ -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); diff --git a/src/service/gh.svc.ts b/src/service/gh.svc.ts index 4f55fe63..3338bc06 100644 --- a/src/service/gh.svc.ts +++ b/src/service/gh.svc.ts @@ -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 (endpoint: string, method: 'GET' | 'POST' | 'PUT' | 'DELETE', body?: unknown) => { const response = await fetch(`${GH_API_BASE_URL}${endpoint}`, { method, @@ -24,13 +56,16 @@ const ghFetch = async (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}`); } @@ -52,7 +87,6 @@ export const authenticateWithDeviceFlow = async (onVerification: GitHubAppStrate ...GH_APP_OAUTH_SETTINGS, onVerification, }); - try { const authResponse = await auth({ type: 'oauth', diff --git a/src/types/gh/refresh-token-response.ts b/src/types/gh/refresh-token-response.ts new file mode 100644 index 00000000..8aa3b846 --- /dev/null +++ b/src/types/gh/refresh-token-response.ts @@ -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; +};