diff --git a/packages/backend/src/database.ts b/packages/backend/src/database.ts index 702842f6..7ccd1c1d 100644 --- a/packages/backend/src/database.ts +++ b/packages/backend/src/database.ts @@ -1,6 +1,7 @@ import { DataSource, DeepPartial } from 'typeorm'; import path from 'path'; import debug from 'debug'; +import assert from 'assert'; import { DatabaseConfig } from './config'; import { User } from './entity/User'; @@ -14,6 +15,7 @@ import { Domain } from './entity/Domain'; const log = debug('snowball:database'); +// TODO: Fix order of methods export class Database { private dataSource: DataSource; @@ -41,6 +43,14 @@ export class Database { return user; } + async updateUser (userId: number, data: DeepPartial): Promise { + const userRepository = this.dataSource.getRepository(User); + const updateResult = await userRepository.update({ id: Number(userId) }, data); + assert(updateResult.affected); + + return updateResult.affected > 0; + } + async getOrganizationsByUserId (userId: number): Promise { const userOrganizationRepository = this.dataSource.getRepository(UserOrganization); diff --git a/packages/backend/src/entity/User.ts b/packages/backend/src/entity/User.ts index a25ec81e..35fb5f72 100644 --- a/packages/backend/src/entity/User.ts +++ b/packages/backend/src/entity/User.ts @@ -18,6 +18,9 @@ export class User { @Column() email!: string; + @Column('varchar', { nullable: true }) + gitHubToken!: string | null; + @CreateDateColumn() createdAt!: Date; diff --git a/packages/backend/src/resolvers.ts b/packages/backend/src/resolvers.ts index 45434a3c..3565a84b 100644 --- a/packages/backend/src/resolvers.ts +++ b/packages/backend/src/resolvers.ts @@ -245,15 +245,24 @@ export const createResolvers = async (db: Database, app: OAuthApp): Promise } }, - authenticateGithub: async (_: any, { code }: { code: string }) => { + authenticateGitHub: async (_: any, { code }: { code: string }, context: any) => { // TOO: Move to Service class const { authentication: { token } } = await app.createToken({ code }); - // TODO: Save bearer token in DB for user + await db.updateUser(context.userId, { gitHubToken: token }); return { token }; + }, + + unauthenticateGitHub: async (_: any, __: object, context: any) => { + try { + return db.updateUser(context.userId, { gitHubToken: null }); + } catch (err) { + log(err); + return false; + } } } }; diff --git a/packages/backend/src/schema.gql b/packages/backend/src/schema.gql index 9a788032..298e9d7e 100644 --- a/packages/backend/src/schema.gql +++ b/packages/backend/src/schema.gql @@ -34,6 +34,7 @@ type User { projects: [Project!] createdAt: String! updatedAt: String! + gitHubToken: String } type Organization { @@ -143,7 +144,8 @@ type Mutation { rollbackDeployment(projectId: String!, deploymentId: String!): Boolean! addDomain(projectId: String!, domainDetails: AddDomainInput!): Boolean! updateDomain(domainId: String!, domainDetails: UpdateDomainInput!): Boolean! - authenticateGithub(code: String!): AuthResult! + authenticateGitHub(code: String!): AuthResult! + unauthenticateGitHub: Boolean! } input AddEnvironmentVariableInput { diff --git a/packages/frontend/src/components/projects/create/ConnectAccount.tsx b/packages/frontend/src/components/projects/create/ConnectAccount.tsx index 76889ecc..02187a56 100644 --- a/packages/frontend/src/components/projects/create/ConnectAccount.tsx +++ b/packages/frontend/src/components/projects/create/ConnectAccount.tsx @@ -19,8 +19,8 @@ const ConnectAccount = ({ onToken }: ConnectAccountInterface) => { const handleCode = async (code: string) => { // Pass code to backend and get access token const { - authenticateGithub: { token }, - } = await client.authenticateGithub(code); + authenticateGitHub: { token }, + } = await client.authenticateGitHub(code); onToken(token); }; diff --git a/packages/frontend/src/components/projects/create/RepositoryList.tsx b/packages/frontend/src/components/projects/create/RepositoryList.tsx index d9d17f1f..6c5c5049 100644 --- a/packages/frontend/src/components/projects/create/RepositoryList.tsx +++ b/packages/frontend/src/components/projects/create/RepositoryList.tsx @@ -14,12 +14,12 @@ const REPOS_PER_PAGE = 5; interface RepositoryListProps { repoSelectionHandler: (repo: GitRepositoryDetails) => void; - token: string; + octokit: Octokit; } const RepositoryList = ({ repoSelectionHandler, - token, + octokit, }: RepositoryListProps) => { const [searchedRepo, setSearchedRepo] = useState(DEFAULT_SEARCHED_REPO); const [selectedAccount, setSelectedAccount] = useState(''); @@ -31,11 +31,6 @@ const RepositoryList = ({ GitRepositoryDetails[] >([]); - const octokit = useMemo(() => { - // TODO: Create github/octokit context - return new Octokit({ auth: token }); - }, [token]); - useEffect(() => { const fetchUserAndOrgs = async () => { const user = await octokit.rest.users.getAuthenticated(); @@ -45,9 +40,7 @@ const RepositoryList = ({ setSelectedAccount(user.data.login); }; - if (token) { - fetchUserAndOrgs(); - } + fetchUserAndOrgs(); }, [octokit]); const debouncedSearchedRepo = useDebounce(searchedRepo, 500); diff --git a/packages/frontend/src/components/projects/create/TemplateCard.tsx b/packages/frontend/src/components/projects/create/TemplateCard.tsx index edb467fb..46ce1bde 100644 --- a/packages/frontend/src/components/projects/create/TemplateCard.tsx +++ b/packages/frontend/src/components/projects/create/TemplateCard.tsx @@ -1,4 +1,5 @@ import React from 'react'; +import toast from 'react-hot-toast'; import { IconButton, Typography } from '@material-tailwind/react'; @@ -10,25 +11,40 @@ interface TemplateDetails { } interface TemplateCardProps { framework: TemplateDetails; + isGitAuth: boolean; } -const TemplateCard: React.FC = ({ framework }) => { +const CardDetails = ({ framework }: { framework: TemplateDetails }) => { return ( - -
- - {framework.icon} {framework.framework} - -
- - {'>'} - -
+
+ + {framework.icon} {framework.framework} + +
+ + {'>'} +
+
+ ); +}; + +const TemplateCard: React.FC = ({ + framework, + isGitAuth, +}) => { + return isGitAuth ? ( + + + ) : ( + + toast.error('Connect Git account to start with a template') + } + > + + ); }; diff --git a/packages/frontend/src/components/projects/project/settings/GitTabPanel.tsx b/packages/frontend/src/components/projects/project/settings/GitTabPanel.tsx index c50de747..34cd4c36 100644 --- a/packages/frontend/src/components/projects/project/settings/GitTabPanel.tsx +++ b/packages/frontend/src/components/projects/project/settings/GitTabPanel.tsx @@ -4,25 +4,14 @@ import toast from 'react-hot-toast'; import { Button, Input, Switch, Typography } from '@material-tailwind/react'; -import RepositoryList from '../../create/RepositoryList'; -import RepoConnectedSection from './RepoConnectedSection'; -import GitSelectionSection from './GitSelectionSection'; -import { GitRepositoryDetails, GitSelect } from '../../../../types/project'; +import { GitRepositoryDetails } from '../../../../types/project'; import WebhookCard from './WebhookCard'; const GitTabPanel = () => { - const [gitSelect, setGitSelect] = useState('none'); - const [linkedRepo, setLinkedRepo] = useState(); + // TODO: Get linked repo from project + const [linkedRepo] = useState(); const [webhooksArray, setWebhooksArray] = useState>([]); - const gitSelectionHandler = (git: GitSelect) => { - setGitSelect(git); - }; - - const repoSelectionHandler = (repoDetails: GitRepositoryDetails) => { - setLinkedRepo(repoDetails); - }; - const { register, handleSubmit, @@ -44,22 +33,8 @@ const GitTabPanel = () => { <>
- Connect Git repository + Git repository - - Create deployments for any commits pushed to your Git repository. - - {linkedRepo && } - {!linkedRepo && - (GitSelect.NONE === gitSelect ? ( - - ) : ( - - ))}
diff --git a/packages/frontend/src/pages/projects/create/index.tsx b/packages/frontend/src/pages/projects/create/index.tsx index 8ec221da..15608bf8 100644 --- a/packages/frontend/src/pages/projects/create/index.tsx +++ b/packages/frontend/src/pages/projects/create/index.tsx @@ -1,31 +1,83 @@ -import React, { useCallback, useState } from 'react'; +import React, { useCallback, useEffect, useMemo, useState } from 'react'; +import { User } from 'gql-client'; +import { Octokit, RequestError } from 'octokit'; import templateDetails from '../../../assets/templates.json'; import TemplateCard from '../../../components/projects/create/TemplateCard'; import RepositoryList from '../../../components/projects/create/RepositoryList'; import ConnectAccount from '../../../components/projects/create/ConnectAccount'; +import { useGQLClient } from '../../../context/GQLClientContext'; + +const UNAUTHORIZED_ERROR_CODE = 401; const NewProject = () => { - const [isGitAuth, setIsGitAuth] = useState(false); - const [gitToken, setGitToken] = useState(''); - // TODO: Get DB user details for checking if already authenticated to Github + const client = useGQLClient(); + const [user, setUser] = useState(); - const handleToken = useCallback((token: string) => { - setGitToken(token); - setIsGitAuth(true); + const octokit = useMemo(() => { + if (!user?.gitHubToken) { + return; + } + + // TODO: Create github/octokit context + return new Octokit({ auth: user.gitHubToken }); + }, [user]); + + const fetchUser = useCallback(async () => { + const { user } = await client.getUser(); + setUser(user); }, []); + const handleToken = useCallback(() => { + fetchUser(); + }, []); + + useEffect(() => { + fetchUser(); + }, []); + + useEffect(() => { + if (!octokit) { + return; + } + + const interceptor = async (error: RequestError | Error) => { + if ( + error instanceof RequestError && + error.status === UNAUTHORIZED_ERROR_CODE + ) { + await client.unauthenticateGithub(); + await fetchUser(); + } + + throw error; + }; + + octokit.hook.error('request', interceptor); + + return () => { + // Remove the interceptor when the component unmounts + octokit.hook.remove('request', interceptor); + }; + }, [octokit, client]); + return ( <>
Start with template
{templateDetails.map((framework, key) => { - return ; + return ( + + ); })}
Import a repository
- {isGitAuth ? ( - {}} /> + {Boolean(octokit) ? ( + {}} /> ) : ( )} diff --git a/packages/gql-client/src/client.ts b/packages/gql-client/src/client.ts index 4c12c41f..37e08e04 100644 --- a/packages/gql-client/src/client.ts +++ b/packages/gql-client/src/client.ts @@ -1,8 +1,8 @@ import { ApolloClient, DefaultOptions, InMemoryCache, NormalizedCacheObject } from '@apollo/client'; import { getUser, getOrganizations, getDeployments, getProjectMembers, searchProjects, getEnvironmentVariables, getProject, getDomains, getProjectsInOrganization } from './queries'; -import { AddEnvironmentVariableInput, AddEnvironmentVariablesResponse, GetDeploymentsResponse, GetEnvironmentVariablesResponse, GetOrganizationsResponse, GetProjectMembersResponse, SearchProjectsResponse, GetUserResponse, UpdateDeploymentToProdResponse, GetProjectResponse, UpdateProjectResponse, UpdateProjectInput, RedeployToProdResponse, DeleteProjectResponse, GetProjectsInOrganizationResponse, RollbackDeploymentResponse, AddDomainInput, AddDomainResponse, GetDomainsResponse, UpdateDomainInput, UpdateDomainResponse, AuthenticateGithubResponse, UpdateEnvironmentVariableResponse, UpdateEnvironmentVariableInput, RemoveEnvironmentVariableResponse, UpdateProjectMemberInput, RemoveProjectMemberResponse, UpdateProjectMemberResponse } from './types'; -import { removeProjectMember, addEnvironmentVariables, updateDeploymentToProd, updateProjectMutation, redeployToProd, deleteProject, addDomain, rollbackDeployment, updateDomainMutation, authenticateGithub, updateEnvironmentVariable, removeEnvironmentVariable, updateProjectMember } from './mutations'; +import { AddEnvironmentVariableInput, AddEnvironmentVariablesResponse, GetDeploymentsResponse, GetEnvironmentVariablesResponse, GetOrganizationsResponse, GetProjectMembersResponse, SearchProjectsResponse, GetUserResponse, UpdateDeploymentToProdResponse, GetProjectResponse, UpdateProjectResponse, UpdateProjectInput, RedeployToProdResponse, DeleteProjectResponse, GetProjectsInOrganizationResponse, RollbackDeploymentResponse, AddDomainInput, AddDomainResponse, GetDomainsResponse, UpdateDomainInput, UpdateDomainResponse, AuthenticateGitHubResponse, UnauthenticateGitHubResponse, UpdateEnvironmentVariableResponse, UpdateEnvironmentVariableInput, RemoveEnvironmentVariableResponse, UpdateProjectMemberInput, RemoveProjectMemberResponse, UpdateProjectMemberResponse } from './types'; +import { removeProjectMember, addEnvironmentVariables, updateDeploymentToProd, updateProjectMutation, redeployToProd, deleteProject, addDomain, rollbackDeployment, updateDomainMutation, authenticateGitHub, unauthenticateGitHub, updateEnvironmentVariable, removeEnvironmentVariable, updateProjectMember } from './mutations'; export interface GraphQLConfig { gqlEndpoint: string; @@ -263,9 +263,9 @@ export class GQLClient { return data; } - async authenticateGithub (code: string): Promise { + async authenticateGitHub (code: string): Promise { const { data } = await this.client.mutate({ - mutation: authenticateGithub, + mutation: authenticateGitHub, variables: { code } @@ -273,4 +273,12 @@ export class GQLClient { return data; } + + async unauthenticateGithub (): Promise { + const { data } = await this.client.mutate({ + mutation: unauthenticateGitHub + }); + + return data; + } } diff --git a/packages/gql-client/src/mutations.ts b/packages/gql-client/src/mutations.ts index 3840380d..ea3e571f 100644 --- a/packages/gql-client/src/mutations.ts +++ b/packages/gql-client/src/mutations.ts @@ -70,9 +70,14 @@ mutation ($projectId: String!, $domainDetails: AddDomainInput!) { } `; -export const authenticateGithub = gql` +export const authenticateGitHub = gql` mutation ($code: String!) { - authenticateGithub(code: $code) { + authenticateGitHub(code: $code) { token } }`; + +export const unauthenticateGitHub = gql` +mutation { + unauthenticateGitHub +}`; diff --git a/packages/gql-client/src/queries.ts b/packages/gql-client/src/queries.ts index 0ccf5022..f002ebae 100644 --- a/packages/gql-client/src/queries.ts +++ b/packages/gql-client/src/queries.ts @@ -8,6 +8,7 @@ query { email createdAt updatedAt + gitHubToken } } `; diff --git a/packages/gql-client/src/types.ts b/packages/gql-client/src/types.ts index 47177aaa..96469a18 100644 --- a/packages/gql-client/src/types.ts +++ b/packages/gql-client/src/types.ts @@ -53,6 +53,7 @@ export type User = { email: string createdAt: string updatedAt: string + gitHubToken: string | null } export type Deployment = { @@ -244,8 +245,12 @@ export type AddDomainResponse = { addDomain: true } -export type AuthenticateGithubResponse = { - authenticateGithub: { +export type AuthenticateGitHubResponse = { + authenticateGitHub: { token: string } } + +export type UnauthenticateGitHubResponse = { + unauthenticateGitHub: boolean +}