forked from cerc-io/snowballtools-base
Save gitHub token in DB and unauthenticate on expiry (#58)
This commit is contained in:
parent
0dd6c7702a
commit
3a7f16467e
@ -1,6 +1,7 @@
|
|||||||
import { DataSource, DeepPartial } from 'typeorm';
|
import { DataSource, DeepPartial } from 'typeorm';
|
||||||
import path from 'path';
|
import path from 'path';
|
||||||
import debug from 'debug';
|
import debug from 'debug';
|
||||||
|
import assert from 'assert';
|
||||||
|
|
||||||
import { DatabaseConfig } from './config';
|
import { DatabaseConfig } from './config';
|
||||||
import { User } from './entity/User';
|
import { User } from './entity/User';
|
||||||
@ -14,6 +15,7 @@ import { Domain } from './entity/Domain';
|
|||||||
|
|
||||||
const log = debug('snowball:database');
|
const log = debug('snowball:database');
|
||||||
|
|
||||||
|
// TODO: Fix order of methods
|
||||||
export class Database {
|
export class Database {
|
||||||
private dataSource: DataSource;
|
private dataSource: DataSource;
|
||||||
|
|
||||||
@ -41,6 +43,14 @@ export class Database {
|
|||||||
return user;
|
return user;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async updateUser (userId: number, data: DeepPartial<User>): Promise<boolean> {
|
||||||
|
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<Organization[]> {
|
async getOrganizationsByUserId (userId: number): Promise<Organization[]> {
|
||||||
const userOrganizationRepository = this.dataSource.getRepository(UserOrganization);
|
const userOrganizationRepository = this.dataSource.getRepository(UserOrganization);
|
||||||
|
|
||||||
|
@ -18,6 +18,9 @@ export class User {
|
|||||||
@Column()
|
@Column()
|
||||||
email!: string;
|
email!: string;
|
||||||
|
|
||||||
|
@Column('varchar', { nullable: true })
|
||||||
|
gitHubToken!: string | null;
|
||||||
|
|
||||||
@CreateDateColumn()
|
@CreateDateColumn()
|
||||||
createdAt!: Date;
|
createdAt!: Date;
|
||||||
|
|
||||||
|
@ -245,15 +245,24 @@ export const createResolvers = async (db: Database, app: OAuthApp): Promise<any>
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
authenticateGithub: async (_: any, { code }: { code: string }) => {
|
authenticateGitHub: async (_: any, { code }: { code: string }, context: any) => {
|
||||||
// TOO: Move to Service class
|
// TOO: Move to Service class
|
||||||
const { authentication: { token } } = await app.createToken({
|
const { authentication: { token } } = await app.createToken({
|
||||||
code
|
code
|
||||||
});
|
});
|
||||||
|
|
||||||
// TODO: Save bearer token in DB for user
|
await db.updateUser(context.userId, { gitHubToken: token });
|
||||||
|
|
||||||
return { token };
|
return { token };
|
||||||
|
},
|
||||||
|
|
||||||
|
unauthenticateGitHub: async (_: any, __: object, context: any) => {
|
||||||
|
try {
|
||||||
|
return db.updateUser(context.userId, { gitHubToken: null });
|
||||||
|
} catch (err) {
|
||||||
|
log(err);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
@ -34,6 +34,7 @@ type User {
|
|||||||
projects: [Project!]
|
projects: [Project!]
|
||||||
createdAt: String!
|
createdAt: String!
|
||||||
updatedAt: String!
|
updatedAt: String!
|
||||||
|
gitHubToken: String
|
||||||
}
|
}
|
||||||
|
|
||||||
type Organization {
|
type Organization {
|
||||||
@ -143,7 +144,8 @@ type Mutation {
|
|||||||
rollbackDeployment(projectId: String!, deploymentId: String!): Boolean!
|
rollbackDeployment(projectId: String!, deploymentId: String!): Boolean!
|
||||||
addDomain(projectId: String!, domainDetails: AddDomainInput!): Boolean!
|
addDomain(projectId: String!, domainDetails: AddDomainInput!): Boolean!
|
||||||
updateDomain(domainId: String!, domainDetails: UpdateDomainInput!): Boolean!
|
updateDomain(domainId: String!, domainDetails: UpdateDomainInput!): Boolean!
|
||||||
authenticateGithub(code: String!): AuthResult!
|
authenticateGitHub(code: String!): AuthResult!
|
||||||
|
unauthenticateGitHub: Boolean!
|
||||||
}
|
}
|
||||||
|
|
||||||
input AddEnvironmentVariableInput {
|
input AddEnvironmentVariableInput {
|
||||||
|
@ -19,8 +19,8 @@ const ConnectAccount = ({ onToken }: ConnectAccountInterface) => {
|
|||||||
const handleCode = async (code: string) => {
|
const handleCode = async (code: string) => {
|
||||||
// Pass code to backend and get access token
|
// Pass code to backend and get access token
|
||||||
const {
|
const {
|
||||||
authenticateGithub: { token },
|
authenticateGitHub: { token },
|
||||||
} = await client.authenticateGithub(code);
|
} = await client.authenticateGitHub(code);
|
||||||
onToken(token);
|
onToken(token);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -14,12 +14,12 @@ const REPOS_PER_PAGE = 5;
|
|||||||
|
|
||||||
interface RepositoryListProps {
|
interface RepositoryListProps {
|
||||||
repoSelectionHandler: (repo: GitRepositoryDetails) => void;
|
repoSelectionHandler: (repo: GitRepositoryDetails) => void;
|
||||||
token: string;
|
octokit: Octokit;
|
||||||
}
|
}
|
||||||
|
|
||||||
const RepositoryList = ({
|
const RepositoryList = ({
|
||||||
repoSelectionHandler,
|
repoSelectionHandler,
|
||||||
token,
|
octokit,
|
||||||
}: RepositoryListProps) => {
|
}: RepositoryListProps) => {
|
||||||
const [searchedRepo, setSearchedRepo] = useState(DEFAULT_SEARCHED_REPO);
|
const [searchedRepo, setSearchedRepo] = useState(DEFAULT_SEARCHED_REPO);
|
||||||
const [selectedAccount, setSelectedAccount] = useState('');
|
const [selectedAccount, setSelectedAccount] = useState('');
|
||||||
@ -31,11 +31,6 @@ const RepositoryList = ({
|
|||||||
GitRepositoryDetails[]
|
GitRepositoryDetails[]
|
||||||
>([]);
|
>([]);
|
||||||
|
|
||||||
const octokit = useMemo(() => {
|
|
||||||
// TODO: Create github/octokit context
|
|
||||||
return new Octokit({ auth: token });
|
|
||||||
}, [token]);
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const fetchUserAndOrgs = async () => {
|
const fetchUserAndOrgs = async () => {
|
||||||
const user = await octokit.rest.users.getAuthenticated();
|
const user = await octokit.rest.users.getAuthenticated();
|
||||||
@ -45,9 +40,7 @@ const RepositoryList = ({
|
|||||||
setSelectedAccount(user.data.login);
|
setSelectedAccount(user.data.login);
|
||||||
};
|
};
|
||||||
|
|
||||||
if (token) {
|
fetchUserAndOrgs();
|
||||||
fetchUserAndOrgs();
|
|
||||||
}
|
|
||||||
}, [octokit]);
|
}, [octokit]);
|
||||||
|
|
||||||
const debouncedSearchedRepo = useDebounce<string>(searchedRepo, 500);
|
const debouncedSearchedRepo = useDebounce<string>(searchedRepo, 500);
|
||||||
|
@ -1,4 +1,5 @@
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
|
import toast from 'react-hot-toast';
|
||||||
|
|
||||||
import { IconButton, Typography } from '@material-tailwind/react';
|
import { IconButton, Typography } from '@material-tailwind/react';
|
||||||
|
|
||||||
@ -10,25 +11,40 @@ interface TemplateDetails {
|
|||||||
}
|
}
|
||||||
interface TemplateCardProps {
|
interface TemplateCardProps {
|
||||||
framework: TemplateDetails;
|
framework: TemplateDetails;
|
||||||
|
isGitAuth: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
const TemplateCard: React.FC<TemplateCardProps> = ({ framework }) => {
|
const CardDetails = ({ framework }: { framework: TemplateDetails }) => {
|
||||||
return (
|
return (
|
||||||
<Link to={'/projects/create/template'}>
|
<div className="h-14 group bg-gray-200 border-gray-200 rounded-lg shadow p-4 flex items-center justify-between">
|
||||||
<div className="h-14 group bg-gray-200 border-gray-200 rounded-lg shadow p-4 flex items-center justify-between">
|
<Typography className="grow">
|
||||||
<Typography className="grow">
|
{framework.icon} {framework.framework}
|
||||||
{framework.icon} {framework.framework}
|
</Typography>
|
||||||
</Typography>
|
<div>
|
||||||
<div>
|
<IconButton size="sm" className="rounded-full hidden group-hover:block">
|
||||||
<IconButton
|
{'>'}
|
||||||
size="sm"
|
</IconButton>
|
||||||
className="rounded-full hidden group-hover:block"
|
|
||||||
>
|
|
||||||
{'>'}
|
|
||||||
</IconButton>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const TemplateCard: React.FC<TemplateCardProps> = ({
|
||||||
|
framework,
|
||||||
|
isGitAuth,
|
||||||
|
}) => {
|
||||||
|
return isGitAuth ? (
|
||||||
|
<Link to="/projects/create/template">
|
||||||
|
<CardDetails framework={framework} />
|
||||||
</Link>
|
</Link>
|
||||||
|
) : (
|
||||||
|
<a
|
||||||
|
onClick={() =>
|
||||||
|
toast.error('Connect Git account to start with a template')
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<CardDetails framework={framework} />
|
||||||
|
</a>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -4,25 +4,14 @@ import toast from 'react-hot-toast';
|
|||||||
|
|
||||||
import { Button, Input, Switch, Typography } from '@material-tailwind/react';
|
import { Button, Input, Switch, Typography } from '@material-tailwind/react';
|
||||||
|
|
||||||
import RepositoryList from '../../create/RepositoryList';
|
import { GitRepositoryDetails } from '../../../../types/project';
|
||||||
import RepoConnectedSection from './RepoConnectedSection';
|
|
||||||
import GitSelectionSection from './GitSelectionSection';
|
|
||||||
import { GitRepositoryDetails, GitSelect } from '../../../../types/project';
|
|
||||||
import WebhookCard from './WebhookCard';
|
import WebhookCard from './WebhookCard';
|
||||||
|
|
||||||
const GitTabPanel = () => {
|
const GitTabPanel = () => {
|
||||||
const [gitSelect, setGitSelect] = useState('none');
|
// TODO: Get linked repo from project
|
||||||
const [linkedRepo, setLinkedRepo] = useState<GitRepositoryDetails>();
|
const [linkedRepo] = useState<GitRepositoryDetails>();
|
||||||
const [webhooksArray, setWebhooksArray] = useState<Array<string>>([]);
|
const [webhooksArray, setWebhooksArray] = useState<Array<string>>([]);
|
||||||
|
|
||||||
const gitSelectionHandler = (git: GitSelect) => {
|
|
||||||
setGitSelect(git);
|
|
||||||
};
|
|
||||||
|
|
||||||
const repoSelectionHandler = (repoDetails: GitRepositoryDetails) => {
|
|
||||||
setLinkedRepo(repoDetails);
|
|
||||||
};
|
|
||||||
|
|
||||||
const {
|
const {
|
||||||
register,
|
register,
|
||||||
handleSubmit,
|
handleSubmit,
|
||||||
@ -44,22 +33,8 @@ const GitTabPanel = () => {
|
|||||||
<>
|
<>
|
||||||
<div className="mb-2 p-2">
|
<div className="mb-2 p-2">
|
||||||
<Typography variant="h6" className="text-black">
|
<Typography variant="h6" className="text-black">
|
||||||
Connect Git repository
|
Git repository
|
||||||
</Typography>
|
</Typography>
|
||||||
<Typography variant="small">
|
|
||||||
Create deployments for any commits pushed to your Git repository.
|
|
||||||
</Typography>
|
|
||||||
{linkedRepo && <RepoConnectedSection linkedRepo={linkedRepo} />}
|
|
||||||
{!linkedRepo &&
|
|
||||||
(GitSelect.NONE === gitSelect ? (
|
|
||||||
<GitSelectionSection gitSelectionHandler={gitSelectionHandler} />
|
|
||||||
) : (
|
|
||||||
<RepositoryList
|
|
||||||
repoSelectionHandler={repoSelectionHandler}
|
|
||||||
// TODO: Pass Github access token after authentication
|
|
||||||
token=""
|
|
||||||
/>
|
|
||||||
))}
|
|
||||||
|
|
||||||
<div className="flex justify-between mt-4">
|
<div className="flex justify-between mt-4">
|
||||||
<div>
|
<div>
|
||||||
|
@ -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 templateDetails from '../../../assets/templates.json';
|
||||||
import TemplateCard from '../../../components/projects/create/TemplateCard';
|
import TemplateCard from '../../../components/projects/create/TemplateCard';
|
||||||
import RepositoryList from '../../../components/projects/create/RepositoryList';
|
import RepositoryList from '../../../components/projects/create/RepositoryList';
|
||||||
import ConnectAccount from '../../../components/projects/create/ConnectAccount';
|
import ConnectAccount from '../../../components/projects/create/ConnectAccount';
|
||||||
|
import { useGQLClient } from '../../../context/GQLClientContext';
|
||||||
|
|
||||||
|
const UNAUTHORIZED_ERROR_CODE = 401;
|
||||||
|
|
||||||
const NewProject = () => {
|
const NewProject = () => {
|
||||||
const [isGitAuth, setIsGitAuth] = useState(false);
|
const client = useGQLClient();
|
||||||
const [gitToken, setGitToken] = useState('');
|
const [user, setUser] = useState<User>();
|
||||||
// TODO: Get DB user details for checking if already authenticated to Github
|
|
||||||
|
|
||||||
const handleToken = useCallback((token: string) => {
|
const octokit = useMemo(() => {
|
||||||
setGitToken(token);
|
if (!user?.gitHubToken) {
|
||||||
setIsGitAuth(true);
|
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 (
|
return (
|
||||||
<>
|
<>
|
||||||
<h5 className="mt-4 ml-4">Start with template</h5>
|
<h5 className="mt-4 ml-4">Start with template</h5>
|
||||||
<div className="grid grid-cols-3 p-4 gap-4">
|
<div className="grid grid-cols-3 p-4 gap-4">
|
||||||
{templateDetails.map((framework, key) => {
|
{templateDetails.map((framework, key) => {
|
||||||
return <TemplateCard framework={framework} key={key} />;
|
return (
|
||||||
|
<TemplateCard
|
||||||
|
isGitAuth={Boolean(user?.gitHubToken)}
|
||||||
|
framework={framework}
|
||||||
|
key={key}
|
||||||
|
/>
|
||||||
|
);
|
||||||
})}
|
})}
|
||||||
</div>
|
</div>
|
||||||
<h5 className="mt-4 ml-4">Import a repository</h5>
|
<h5 className="mt-4 ml-4">Import a repository</h5>
|
||||||
{isGitAuth ? (
|
{Boolean(octokit) ? (
|
||||||
<RepositoryList token={gitToken} repoSelectionHandler={() => {}} />
|
<RepositoryList octokit={octokit!} repoSelectionHandler={() => {}} />
|
||||||
) : (
|
) : (
|
||||||
<ConnectAccount onToken={handleToken} />
|
<ConnectAccount onToken={handleToken} />
|
||||||
)}
|
)}
|
||||||
|
@ -1,8 +1,8 @@
|
|||||||
import { ApolloClient, DefaultOptions, InMemoryCache, NormalizedCacheObject } from '@apollo/client';
|
import { ApolloClient, DefaultOptions, InMemoryCache, NormalizedCacheObject } from '@apollo/client';
|
||||||
|
|
||||||
import { getUser, getOrganizations, getDeployments, getProjectMembers, searchProjects, getEnvironmentVariables, getProject, getDomains, getProjectsInOrganization } from './queries';
|
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 { 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, updateEnvironmentVariable, removeEnvironmentVariable, updateProjectMember } from './mutations';
|
import { removeProjectMember, addEnvironmentVariables, updateDeploymentToProd, updateProjectMutation, redeployToProd, deleteProject, addDomain, rollbackDeployment, updateDomainMutation, authenticateGitHub, unauthenticateGitHub, updateEnvironmentVariable, removeEnvironmentVariable, updateProjectMember } from './mutations';
|
||||||
|
|
||||||
export interface GraphQLConfig {
|
export interface GraphQLConfig {
|
||||||
gqlEndpoint: string;
|
gqlEndpoint: string;
|
||||||
@ -263,9 +263,9 @@ export class GQLClient {
|
|||||||
return data;
|
return data;
|
||||||
}
|
}
|
||||||
|
|
||||||
async authenticateGithub (code: string): Promise<AuthenticateGithubResponse> {
|
async authenticateGitHub (code: string): Promise<AuthenticateGitHubResponse> {
|
||||||
const { data } = await this.client.mutate({
|
const { data } = await this.client.mutate({
|
||||||
mutation: authenticateGithub,
|
mutation: authenticateGitHub,
|
||||||
variables: {
|
variables: {
|
||||||
code
|
code
|
||||||
}
|
}
|
||||||
@ -273,4 +273,12 @@ export class GQLClient {
|
|||||||
|
|
||||||
return data;
|
return data;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async unauthenticateGithub (): Promise<UnauthenticateGitHubResponse> {
|
||||||
|
const { data } = await this.client.mutate({
|
||||||
|
mutation: unauthenticateGitHub
|
||||||
|
});
|
||||||
|
|
||||||
|
return data;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -70,9 +70,14 @@ mutation ($projectId: String!, $domainDetails: AddDomainInput!) {
|
|||||||
}
|
}
|
||||||
`;
|
`;
|
||||||
|
|
||||||
export const authenticateGithub = gql`
|
export const authenticateGitHub = gql`
|
||||||
mutation ($code: String!) {
|
mutation ($code: String!) {
|
||||||
authenticateGithub(code: $code) {
|
authenticateGitHub(code: $code) {
|
||||||
token
|
token
|
||||||
}
|
}
|
||||||
}`;
|
}`;
|
||||||
|
|
||||||
|
export const unauthenticateGitHub = gql`
|
||||||
|
mutation {
|
||||||
|
unauthenticateGitHub
|
||||||
|
}`;
|
||||||
|
@ -8,6 +8,7 @@ query {
|
|||||||
email
|
email
|
||||||
createdAt
|
createdAt
|
||||||
updatedAt
|
updatedAt
|
||||||
|
gitHubToken
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
`;
|
`;
|
||||||
|
@ -53,6 +53,7 @@ export type User = {
|
|||||||
email: string
|
email: string
|
||||||
createdAt: string
|
createdAt: string
|
||||||
updatedAt: string
|
updatedAt: string
|
||||||
|
gitHubToken: string | null
|
||||||
}
|
}
|
||||||
|
|
||||||
export type Deployment = {
|
export type Deployment = {
|
||||||
@ -244,8 +245,12 @@ export type AddDomainResponse = {
|
|||||||
addDomain: true
|
addDomain: true
|
||||||
}
|
}
|
||||||
|
|
||||||
export type AuthenticateGithubResponse = {
|
export type AuthenticateGitHubResponse = {
|
||||||
authenticateGithub: {
|
authenticateGitHub: {
|
||||||
token: string
|
token: string
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export type UnauthenticateGitHubResponse = {
|
||||||
|
unauthenticateGitHub: boolean
|
||||||
|
}
|
||||||
|
Loading…
Reference in New Issue
Block a user