Check for GitHub authentication in template create page and handle GitHub unauthorized errors (#173)

* Fix GitHub auth check on reloading template create page

* Fix error handling for unauthenticated GitHub token
This commit is contained in:
Nabarun Gogoi 2024-04-11 17:19:15 +05:30 committed by GitHub
parent 47231a6eab
commit cc8f9527da
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
5 changed files with 66 additions and 31 deletions

View File

@ -74,12 +74,12 @@ export const MockConnectGitCard = () => {
return ( return (
<div className="flex flex-col gap-2 relative z-0"> <div className="flex flex-col gap-2 relative z-0">
{IMPORT_CONTENT.map((repo, index) => ( {IMPORT_CONTENT.map((repo, index) => (
<> <React.Fragment key={index}>
<MockProjectCard key={index} {...repo} /> <MockProjectCard {...repo} />
{index !== IMPORT_CONTENT.length - 1 && ( {index !== IMPORT_CONTENT.length - 1 && (
<div className="border-b border-base-border" /> <div className="border-b border-base-border" />
)} )}
</> </React.Fragment>
))} ))}
</div> </div>
); );

View File

@ -1,5 +1,4 @@
import React, { useCallback, useEffect, useMemo, useState } from 'react'; import React, { useCallback, useEffect, useMemo, useState } from 'react';
import { Octokit } from 'octokit';
import assert from 'assert'; import assert from 'assert';
import { useDebounce } from 'usehooks-ts'; import { useDebounce } from 'usehooks-ts';
@ -14,20 +13,18 @@ import {
import { Select, SelectOption } from 'components/shared/Select'; import { Select, SelectOption } from 'components/shared/Select';
import { Input } from 'components/shared/Input'; import { Input } from 'components/shared/Input';
import { Button } from 'components/shared/Button'; import { Button } from 'components/shared/Button';
import { useOctokit } from 'context/OctokitContext';
const DEFAULT_SEARCHED_REPO = ''; const DEFAULT_SEARCHED_REPO = '';
const REPOS_PER_PAGE = 5; const REPOS_PER_PAGE = 5;
interface RepositoryListProps { export const RepositoryList = () => {
octokit: Octokit;
}
export const RepositoryList = ({ octokit }: RepositoryListProps) => {
const [searchedRepo, setSearchedRepo] = useState(DEFAULT_SEARCHED_REPO); const [searchedRepo, setSearchedRepo] = useState(DEFAULT_SEARCHED_REPO);
const [selectedAccount, setSelectedAccount] = useState<SelectOption>(); const [selectedAccount, setSelectedAccount] = useState<SelectOption>();
const [orgs, setOrgs] = useState<GitOrgDetails[]>([]); const [orgs, setOrgs] = useState<GitOrgDetails[]>([]);
// TODO: Add new type for Git user when required // TODO: Add new type for Git user when required
const [gitUser, setGitUser] = useState<GitOrgDetails>(); const [gitUser, setGitUser] = useState<GitOrgDetails>();
const { octokit, isAuth } = useOctokit();
const [repositoryDetails, setRepositoryDetails] = useState< const [repositoryDetails, setRepositoryDetails] = useState<
GitRepositoryDetails[] GitRepositoryDetails[]
@ -35,15 +32,23 @@ export const RepositoryList = ({ octokit }: RepositoryListProps) => {
useEffect(() => { useEffect(() => {
const fetchUserAndOrgs = async () => { const fetchUserAndOrgs = async () => {
const user = await octokit.rest.users.getAuthenticated(); try {
const orgs = await octokit.rest.orgs.listForAuthenticatedUser(); const user = await octokit.rest.users.getAuthenticated();
setOrgs(orgs.data); const orgs = await octokit.rest.orgs.listForAuthenticatedUser();
setGitUser(user.data); setOrgs(orgs.data);
setSelectedAccount({ label: user.data.login, value: user.data.login }); setGitUser(user.data);
setSelectedAccount({ label: user.data.login, value: user.data.login });
} catch (error) {
// Error handled by octokit error hook interceptor in Octokit context
console.error(error);
return;
}
}; };
fetchUserAndOrgs(); if (isAuth) {
}, [octokit]); fetchUserAndOrgs();
}
}, [octokit, isAuth]);
const debouncedSearchedRepo = useDebounce<string>(searchedRepo, 500); const debouncedSearchedRepo = useDebounce<string>(searchedRepo, 500);

View File

@ -8,8 +8,11 @@ import React, {
useEffect, useEffect,
} from 'react'; } from 'react';
import { Octokit, RequestError } from 'octokit'; import { Octokit, RequestError } from 'octokit';
import { useNavigate, useParams } from 'react-router-dom';
import { useDebounceCallback } from 'usehooks-ts';
import { useGQLClient } from './GQLClientContext'; import { useGQLClient } from './GQLClientContext';
import { useToast } from 'components/shared/Toast';
const UNAUTHORIZED_ERROR_CODE = 401; const UNAUTHORIZED_ERROR_CODE = 401;
@ -26,17 +29,17 @@ const OctokitContext = createContext<ContextValue>({
}); });
export const OctokitProvider = ({ children }: { children: ReactNode }) => { export const OctokitProvider = ({ children }: { children: ReactNode }) => {
const [authToken, setAuthToken] = useState<string>(''); const [authToken, setAuthToken] = useState<string | null>(null);
const [isAuth, setIsAuth] = useState(false); const [isAuth, setIsAuth] = useState(false);
const navigate = useNavigate();
const { orgSlug } = useParams();
const { toast, dismiss } = useToast();
const client = useGQLClient(); const client = useGQLClient();
const fetchUser = useCallback(async () => { const fetchUser = useCallback(async () => {
const { user } = await client.getUser(); const { user } = await client.getUser();
if (user.gitHubToken) { setAuthToken(user.gitHubToken);
setAuthToken(user.gitHubToken);
}
}, []); }, []);
const updateAuth = useCallback(() => { const updateAuth = useCallback(() => {
@ -57,6 +60,23 @@ export const OctokitProvider = ({ children }: { children: ReactNode }) => {
fetchUser(); fetchUser();
}, []); }, []);
const debouncedUnauthorizedGithubHandler = useDebounceCallback(
useCallback(
(error: RequestError) => {
toast({
id: 'unauthorized-github-token',
title: `GitHub authentication error: ${error.message}`,
variant: 'error',
onDismiss: dismiss,
});
navigate(`/${orgSlug}/projects/create`);
},
[toast, navigate, orgSlug],
),
500,
);
useEffect(() => { useEffect(() => {
// TODO: Handle React component error // TODO: Handle React component error
const interceptor = async (error: RequestError | Error) => { const interceptor = async (error: RequestError | Error) => {
@ -66,6 +86,8 @@ export const OctokitProvider = ({ children }: { children: ReactNode }) => {
) { ) {
await client.unauthenticateGithub(); await client.unauthenticateGithub();
await fetchUser(); await fetchUser();
debouncedUnauthorizedGithubHandler(error);
} }
throw error; throw error;
@ -77,7 +99,7 @@ export const OctokitProvider = ({ children }: { children: ReactNode }) => {
// Remove the interceptor when the component unmounts // Remove the interceptor when the component unmounts
octokit.hook.remove('request', interceptor); octokit.hook.remove('request', interceptor);
}; };
}, [octokit, client]); }, [octokit, client, debouncedUnauthorizedGithubHandler]);
return ( return (
<OctokitContext.Provider value={{ octokit, updateAuth, isAuth }}> <OctokitContext.Provider value={{ octokit, updateAuth, isAuth }}>

View File

@ -31,7 +31,7 @@ const NewProject = () => {
<Heading as="h3" className="font-medium text-lg mt-10 pl-1 mb-3"> <Heading as="h3" className="font-medium text-lg mt-10 pl-1 mb-3">
Import a repository Import a repository
</Heading> </Heading>
<RepositoryList octokit={octokit} /> <RepositoryList />
</> </>
) : ( ) : (
<ConnectAccount onAuth={updateAuth} /> <ConnectAccount onAuth={updateAuth} />

View File

@ -26,7 +26,7 @@ type SubmitRepoValues = {
}; };
const CreateRepo = () => { const CreateRepo = () => {
const { octokit } = useOctokit(); const { octokit, isAuth } = useOctokit();
const { template } = useOutletContext<{ template: Template }>(); const { template } = useOutletContext<{ template: Template }>();
const client = useGQLClient(); const client = useGQLClient();
@ -104,18 +104,26 @@ const CreateRepo = () => {
useEffect(() => { useEffect(() => {
const fetchUserAndOrgs = async () => { const fetchUserAndOrgs = async () => {
const user = await octokit?.rest.users.getAuthenticated(); try {
const orgs = await octokit?.rest.orgs.listForAuthenticatedUser(); const user = await octokit?.rest.users.getAuthenticated();
const orgs = await octokit?.rest.orgs.listForAuthenticatedUser();
if (user && orgs) { if (user && orgs) {
const orgsLoginArr = orgs.data.map((org) => org.login); const orgsLoginArr = orgs.data.map((org) => org.login);
setGitAccounts([user.data.login, ...orgsLoginArr]); setGitAccounts([user.data.login, ...orgsLoginArr]);
}
} catch (error) {
// Error handled by octokit error hook interceptor in Octokit context
console.error(error);
return;
} }
}; };
fetchUserAndOrgs(); if (isAuth) {
}, [octokit]); fetchUserAndOrgs();
}
}, [octokit, isAuth]);
const { handleSubmit, control, reset } = useForm<SubmitRepoValues>({ const { handleSubmit, control, reset } = useForm<SubmitRepoValues>({
defaultValues: { defaultValues: {