diff --git a/packages/backend/environments/local.toml b/packages/backend/environments/local.toml index 1adbab8c..223979c4 100644 --- a/packages/backend/environments/local.toml +++ b/packages/backend/environments/local.toml @@ -17,6 +17,11 @@ clientId = "" clientSecret = "" +[gitea] + [gitea.oAuth] + clientId = "" + clientSecret = "" + [registryConfig] fetchDeploymentRecordDelay = 5000 restEndpoint = "http://localhost:1317" diff --git a/packages/backend/package.json b/packages/backend/package.json index f629dab9..d3f97868 100644 --- a/packages/backend/package.json +++ b/packages/backend/package.json @@ -21,6 +21,7 @@ "luxon": "^3.4.4", "nanoid": "3", "nanoid-dictionary": "^5.0.0-beta.1", + "node-fetch": "2", "octokit": "^3.1.2", "reflect-metadata": "^0.2.1", "semver": "^7.6.0", @@ -46,6 +47,7 @@ "devDependencies": { "@types/express-session": "^1.17.10", "@types/fs-extra": "^11.0.4", + "@types/node-fetch": "^2.6.11", "@typescript-eslint/eslint-plugin": "^6.18.1", "@typescript-eslint/parser": "^6.18.1", "better-sqlite3": "^9.2.2", diff --git a/packages/backend/src/resolvers.ts b/packages/backend/src/resolvers.ts index 3eeace3f..d28284a8 100644 --- a/packages/backend/src/resolvers.ts +++ b/packages/backend/src/resolvers.ts @@ -6,6 +6,7 @@ import { Permission } from './entity/ProjectMember'; import { Domain } from './entity/Domain'; import { Project } from './entity/Project'; import { EnvironmentVariable } from './entity/EnvironmentVariable'; +import { GitType } from './types'; const log = debug('snowball:resolver'); @@ -206,6 +207,15 @@ export const createResolvers = async (service: Service): Promise => { } }, + authenticateGit: async (_: any, { type, code }: { type: GitType, code: string }, context: any) => { + try { + return await service.authenticateGit(type, code, context.user); + } catch (err) { + log(err); + return false; + } + }, + unauthenticateGitHub: async (_: any, __: object, context: any) => { try { return service.unauthenticateGitHub(context.user, { gitHubToken: null }); diff --git a/packages/backend/src/schema.gql b/packages/backend/src/schema.gql index 620dec72..929589b0 100644 --- a/packages/backend/src/schema.gql +++ b/packages/backend/src/schema.gql @@ -26,6 +26,11 @@ enum DomainStatus { Pending } +enum GitType { + GitHub + Gitea +} + type User { id: String! name: String @@ -203,5 +208,6 @@ type Mutation { addDomain(projectId: String!, data: AddDomainInput!): Boolean! updateDomain(domainId: String!, data: UpdateDomainInput!): Boolean! authenticateGitHub(code: String!): AuthResult! + authenticateGit(type: GitType!, code: String!): AuthResult! unauthenticateGitHub: Boolean! } diff --git a/packages/backend/src/service.ts b/packages/backend/src/service.ts index a6ec94d8..662ad2d0 100644 --- a/packages/backend/src/service.ts +++ b/packages/backend/src/service.ts @@ -2,6 +2,7 @@ import assert from 'assert'; import debug from 'debug'; import { DeepPartial, FindOptionsWhere } from 'typeorm'; import { Octokit, RequestError } from 'octokit'; +import fetch from 'node-fetch'; import { OAuthApp } from '@octokit/oauth-app'; @@ -15,12 +16,13 @@ import { Permission, ProjectMember } from './entity/ProjectMember'; import { User } from './entity/User'; import { Registry } from './registry'; import { GitHubConfig, RegistryConfig } from './config'; -import { AppDeploymentRecord, GitPushEventPayload, PackageJSON } from './types'; +import { AppDeploymentRecord, GitPushEventPayload, GitType, PackageJSON } from './types'; import { Role } from './entity/UserOrganization'; const log = debug('snowball:service'); const GITHUB_UNIQUE_WEBHOOK_ERROR = 'Hook already exists on this repository'; +const GITEA_ACCESS_TOKEN_ENDPOINT = 'https://git.vdb.to/login/oauth/access_token'; interface Config { gitHubConfig: GitHubConfig @@ -701,6 +703,48 @@ export class Service { return { token }; } + async authenticateGit (type: GitType, code:string, user: User): Promise<{token: string}> { + let token: string; + + switch (type) { + case GitType.GitHub: + ({ authentication: { token } } = await this.oauthApp.createToken({ + code + })); + + break; + + case GitType.Gitea: { + const response = await fetch(GITEA_ACCESS_TOKEN_ENDPOINT, { + method: 'post', + body: JSON.stringify({ + // TODO: Fetch from config + client_id: '', + client_secret: '', + code, + grant_type: 'authorization_code', + // TODO: Get frontend app URL from config + redirect_uri: 'http://localhost:3000/organization/projects/create' + }), + headers: { 'Content-Type': 'application/json' } + }); + + assert(response.ok, `HTTP Error Response: ${response.status} ${response.statusText}`); + const data: any = await response.json(); + ({ access_token: token } = data); + + break; + } + + default: throw new Error(`Type ${type} not handled for Git authentication`); + } + + assert(token, `Access token is not set for type ${type}`); + await this.db.updateUser(user, { gitHubToken: token }); + + return { token }; + } + async unauthenticateGitHub (user: User, data: DeepPartial): Promise { return this.db.updateUser(user, data); } diff --git a/packages/backend/src/types.ts b/packages/backend/src/types.ts index e0cef120..a859b864 100644 --- a/packages/backend/src/types.ts +++ b/packages/backend/src/types.ts @@ -49,3 +49,8 @@ interface RegistryRecord { export interface AppDeploymentRecord extends RegistryRecord { attributes: AppDeploymentRecordAttributes } + +export enum GitType { + GitHub = 'GitHub', + Gitea = 'Gitea', +} diff --git a/packages/frontend/.env b/packages/frontend/.env index c73b8fe2..0ba4b838 100644 --- a/packages/frontend/.env +++ b/packages/frontend/.env @@ -3,4 +3,6 @@ REACT_APP_SERVER_URL = 'http://localhost:8000' REACT_APP_GITHUB_CLIENT_ID = REACT_APP_GITHUB_TEMPLATE_REPO = +REACT_APP_GITEA_CLIENT_ID = + REACT_APP_WALLET_CONNECT_ID = diff --git a/packages/frontend/src/components/projects/create/ConnectAccount.tsx b/packages/frontend/src/components/projects/create/ConnectAccount.tsx index 11a5e5d2..757fe246 100644 --- a/packages/frontend/src/components/projects/create/ConnectAccount.tsx +++ b/packages/frontend/src/components/projects/create/ConnectAccount.tsx @@ -1,15 +1,21 @@ -import { Button } from '@material-tailwind/react'; import React from 'react'; import OauthPopup from 'react-oauth-popup'; +import { GitType } from 'gql-client'; + +import { Button } from '@material-tailwind/react'; import { useGQLClient } from '../../../context/GQLClientContext'; import ConnectAccountTabPanel from './ConnectAccountTabPanel'; const SCOPES = 'repo user'; + const GITHUB_OAUTH_URL = `https://github.com/login/oauth/authorize?client_id=${ process.env.REACT_APP_GITHUB_CLIENT_ID }&scope=${encodeURIComponent(SCOPES)}`; +const REDIRECT_URI = `${window.location.origin}/organization/projects/create`; +const GITEA_OAUTH_URL = `https://git.vdb.to/login/oauth/authorize?client_id=${process.env.REACT_APP_GITEA_CLIENT_ID}&redirect_uri=${REDIRECT_URI}&response_type=code`; + interface ConnectAccountInterface { onAuth: (token: string) => void; } @@ -17,11 +23,13 @@ interface ConnectAccountInterface { const ConnectAccount = ({ onAuth: onToken }: ConnectAccountInterface) => { const client = useGQLClient(); - const handleCode = async (code: string) => { + const handleCode = async (type: GitType, code: string) => { // Pass code to backend and get access token const { - authenticateGitHub: { token }, - } = await client.authenticateGitHub(code); + authenticateGit: { token }, + } = await client.authenticateGit(type, code); + + // TODO: Handle token according to Git type onToken(token); }; @@ -40,7 +48,7 @@ const ConnectAccount = ({ onAuth: onToken }: ConnectAccountInterface) => {
handleCode(GitType.GitHub, code)} onClose={() => {}} title="Snowball" width={1000} @@ -48,7 +56,16 @@ const ConnectAccount = ({ onAuth: onToken }: ConnectAccountInterface) => { > - + handleCode(GitType.Gitea, code)} + onClose={() => {}} + title="Snowball" + width={1000} + height={1000} + > + +
diff --git a/packages/gql-client/src/client.ts b/packages/gql-client/src/client.ts index 74c7683e..073d4bfb 100644 --- a/packages/gql-client/src/client.ts +++ b/packages/gql-client/src/client.ts @@ -311,6 +311,18 @@ export class GQLClient { return data; } + async authenticateGit (type: types.GitType, code: string): Promise { + const { data } = await this.client.mutate({ + mutation: mutations.authenticateGit, + variables: { + type, + code + } + }); + + return data; + } + async unauthenticateGithub (): Promise { const { data } = await this.client.mutate({ mutation: mutations.unauthenticateGitHub diff --git a/packages/gql-client/src/mutations.ts b/packages/gql-client/src/mutations.ts index fd7a9c2e..347085eb 100644 --- a/packages/gql-client/src/mutations.ts +++ b/packages/gql-client/src/mutations.ts @@ -95,6 +95,13 @@ mutation ($code: String!) { } }`; +export const authenticateGit = gql` +mutation ($type: GitType!, $code: String!) { + authenticateGit(type: $type, code: $code) { + token + } +}`; + export const unauthenticateGitHub = gql` mutation { unauthenticateGitHub diff --git a/packages/gql-client/src/types.ts b/packages/gql-client/src/types.ts index 71c44c00..552ff8bb 100644 --- a/packages/gql-client/src/types.ts +++ b/packages/gql-client/src/types.ts @@ -28,6 +28,11 @@ export enum DomainStatus { Pending = 'Pending', } +export enum GitType { + GitHub = 'GitHub', + Gitea = 'Gitea', +} + export type EnvironmentVariable = { id: string environment: Environment @@ -288,6 +293,12 @@ export type AuthenticateGitHubResponse = { } } +export type AuthenticateGitResponse = { + authenticateGit: { + token: string + } +} + export type UnauthenticateGitHubResponse = { unauthenticateGitHub: boolean } diff --git a/yarn.lock b/yarn.lock index 84837876..847fa35a 100644 --- a/yarn.lock +++ b/yarn.lock @@ -4717,6 +4717,14 @@ resolved "https://registry.yarnpkg.com/@types/ms/-/ms-0.7.34.tgz#10964ba0dee6ac4cd462e2795b6bebd407303433" integrity sha512-nG96G3Wp6acyAgJqGasjODb+acrI7KltPiRxzHPXnP3NgI28bpQDRv53olbqGXbfcgF5aiiHmO3xpwEpS5Ld9g== +"@types/node-fetch@^2.6.11": + version "2.6.11" + resolved "https://registry.yarnpkg.com/@types/node-fetch/-/node-fetch-2.6.11.tgz#9b39b78665dae0e82a08f02f4967d62c66f95d24" + integrity sha512-24xFj9R5+rfQJLRyM56qh+wnVSYhyXC2tkoBndtY0U+vubqNsYXGjufB2nn8Q6gt0LrARwL6UBtMCSVCwl4B1g== + dependencies: + "@types/node" "*" + form-data "^4.0.0" + "@types/node-forge@^1.3.0": version "1.3.10" resolved "https://registry.npmjs.org/@types/node-forge/-/node-forge-1.3.10.tgz" @@ -13550,6 +13558,13 @@ node-fetch-native@^1.4.0, node-fetch-native@^1.4.1, node-fetch-native@^1.6.1: resolved "https://registry.yarnpkg.com/node-fetch-native/-/node-fetch-native-1.6.2.tgz#f439000d972eb0c8a741b65dcda412322955e1c6" integrity sha512-69mtXOFZ6hSkYiXAVB5SqaRvrbITC/NPyqv7yuu/qw0nmgPyYbIMYYNIDhNtwPrzk0ptrimrLz/hhjvm4w5Z+w== +node-fetch@2, node-fetch@^2.6.1, node-fetch@^2.6.12, node-fetch@^2.6.7: + version "2.7.0" + resolved "https://registry.yarnpkg.com/node-fetch/-/node-fetch-2.7.0.tgz#d0f0fa6e3e2dc1d27efcd8ad99d550bda94d187d" + integrity sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A== + dependencies: + whatwg-url "^5.0.0" + node-fetch@2.6.7: version "2.6.7" resolved "https://registry.yarnpkg.com/node-fetch/-/node-fetch-2.6.7.tgz#24de9fba827e3b4ae44dc8b20256a379160052ad" @@ -13557,13 +13572,6 @@ node-fetch@2.6.7: dependencies: whatwg-url "^5.0.0" -node-fetch@^2.6.1, node-fetch@^2.6.12, node-fetch@^2.6.7: - version "2.7.0" - resolved "https://registry.yarnpkg.com/node-fetch/-/node-fetch-2.7.0.tgz#d0f0fa6e3e2dc1d27efcd8ad99d550bda94d187d" - integrity sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A== - dependencies: - whatwg-url "^5.0.0" - node-forge@^1, node-forge@^1.3.1: version "1.3.1" resolved "https://registry.npmjs.org/node-forge/-/node-forge-1.3.1.tgz"