Implement Gitea authentication and get access token
This commit is contained in:
parent
6126c03eee
commit
c96385f40c
@ -17,6 +17,11 @@
|
|||||||
clientId = ""
|
clientId = ""
|
||||||
clientSecret = ""
|
clientSecret = ""
|
||||||
|
|
||||||
|
[gitea]
|
||||||
|
[gitea.oAuth]
|
||||||
|
clientId = ""
|
||||||
|
clientSecret = ""
|
||||||
|
|
||||||
[registryConfig]
|
[registryConfig]
|
||||||
fetchDeploymentRecordDelay = 5000
|
fetchDeploymentRecordDelay = 5000
|
||||||
restEndpoint = "http://localhost:1317"
|
restEndpoint = "http://localhost:1317"
|
||||||
|
@ -21,6 +21,7 @@
|
|||||||
"luxon": "^3.4.4",
|
"luxon": "^3.4.4",
|
||||||
"nanoid": "3",
|
"nanoid": "3",
|
||||||
"nanoid-dictionary": "^5.0.0-beta.1",
|
"nanoid-dictionary": "^5.0.0-beta.1",
|
||||||
|
"node-fetch": "2",
|
||||||
"octokit": "^3.1.2",
|
"octokit": "^3.1.2",
|
||||||
"reflect-metadata": "^0.2.1",
|
"reflect-metadata": "^0.2.1",
|
||||||
"semver": "^7.6.0",
|
"semver": "^7.6.0",
|
||||||
@ -46,6 +47,7 @@
|
|||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@types/express-session": "^1.17.10",
|
"@types/express-session": "^1.17.10",
|
||||||
"@types/fs-extra": "^11.0.4",
|
"@types/fs-extra": "^11.0.4",
|
||||||
|
"@types/node-fetch": "^2.6.11",
|
||||||
"@typescript-eslint/eslint-plugin": "^6.18.1",
|
"@typescript-eslint/eslint-plugin": "^6.18.1",
|
||||||
"@typescript-eslint/parser": "^6.18.1",
|
"@typescript-eslint/parser": "^6.18.1",
|
||||||
"better-sqlite3": "^9.2.2",
|
"better-sqlite3": "^9.2.2",
|
||||||
|
@ -6,6 +6,7 @@ import { Permission } from './entity/ProjectMember';
|
|||||||
import { Domain } from './entity/Domain';
|
import { Domain } from './entity/Domain';
|
||||||
import { Project } from './entity/Project';
|
import { Project } from './entity/Project';
|
||||||
import { EnvironmentVariable } from './entity/EnvironmentVariable';
|
import { EnvironmentVariable } from './entity/EnvironmentVariable';
|
||||||
|
import { GitType } from './types';
|
||||||
|
|
||||||
const log = debug('snowball:resolver');
|
const log = debug('snowball:resolver');
|
||||||
|
|
||||||
@ -206,6 +207,15 @@ export const createResolvers = async (service: Service): Promise<any> => {
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
|
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) => {
|
unauthenticateGitHub: async (_: any, __: object, context: any) => {
|
||||||
try {
|
try {
|
||||||
return service.unauthenticateGitHub(context.user, { gitHubToken: null });
|
return service.unauthenticateGitHub(context.user, { gitHubToken: null });
|
||||||
|
@ -26,6 +26,11 @@ enum DomainStatus {
|
|||||||
Pending
|
Pending
|
||||||
}
|
}
|
||||||
|
|
||||||
|
enum GitType {
|
||||||
|
GitHub
|
||||||
|
Gitea
|
||||||
|
}
|
||||||
|
|
||||||
type User {
|
type User {
|
||||||
id: String!
|
id: String!
|
||||||
name: String
|
name: String
|
||||||
@ -203,5 +208,6 @@ type Mutation {
|
|||||||
addDomain(projectId: String!, data: AddDomainInput!): Boolean!
|
addDomain(projectId: String!, data: AddDomainInput!): Boolean!
|
||||||
updateDomain(domainId: String!, data: UpdateDomainInput!): Boolean!
|
updateDomain(domainId: String!, data: UpdateDomainInput!): Boolean!
|
||||||
authenticateGitHub(code: String!): AuthResult!
|
authenticateGitHub(code: String!): AuthResult!
|
||||||
|
authenticateGit(type: GitType!, code: String!): AuthResult!
|
||||||
unauthenticateGitHub: Boolean!
|
unauthenticateGitHub: Boolean!
|
||||||
}
|
}
|
||||||
|
@ -2,6 +2,7 @@ import assert from 'assert';
|
|||||||
import debug from 'debug';
|
import debug from 'debug';
|
||||||
import { DeepPartial, FindOptionsWhere } from 'typeorm';
|
import { DeepPartial, FindOptionsWhere } from 'typeorm';
|
||||||
import { Octokit, RequestError } from 'octokit';
|
import { Octokit, RequestError } from 'octokit';
|
||||||
|
import fetch from 'node-fetch';
|
||||||
|
|
||||||
import { OAuthApp } from '@octokit/oauth-app';
|
import { OAuthApp } from '@octokit/oauth-app';
|
||||||
|
|
||||||
@ -15,12 +16,13 @@ import { Permission, ProjectMember } from './entity/ProjectMember';
|
|||||||
import { User } from './entity/User';
|
import { User } from './entity/User';
|
||||||
import { Registry } from './registry';
|
import { Registry } from './registry';
|
||||||
import { GitHubConfig, RegistryConfig } from './config';
|
import { GitHubConfig, RegistryConfig } from './config';
|
||||||
import { AppDeploymentRecord, GitPushEventPayload, PackageJSON } from './types';
|
import { AppDeploymentRecord, GitPushEventPayload, GitType, PackageJSON } from './types';
|
||||||
import { Role } from './entity/UserOrganization';
|
import { Role } from './entity/UserOrganization';
|
||||||
|
|
||||||
const log = debug('snowball:service');
|
const log = debug('snowball:service');
|
||||||
|
|
||||||
const GITHUB_UNIQUE_WEBHOOK_ERROR = 'Hook already exists on this repository';
|
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 {
|
interface Config {
|
||||||
gitHubConfig: GitHubConfig
|
gitHubConfig: GitHubConfig
|
||||||
@ -701,6 +703,48 @@ export class Service {
|
|||||||
return { token };
|
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<User>): Promise<boolean> {
|
async unauthenticateGitHub (user: User, data: DeepPartial<User>): Promise<boolean> {
|
||||||
return this.db.updateUser(user, data);
|
return this.db.updateUser(user, data);
|
||||||
}
|
}
|
||||||
|
@ -49,3 +49,8 @@ interface RegistryRecord {
|
|||||||
export interface AppDeploymentRecord extends RegistryRecord {
|
export interface AppDeploymentRecord extends RegistryRecord {
|
||||||
attributes: AppDeploymentRecordAttributes
|
attributes: AppDeploymentRecordAttributes
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export enum GitType {
|
||||||
|
GitHub = 'GitHub',
|
||||||
|
Gitea = 'Gitea',
|
||||||
|
}
|
||||||
|
@ -3,4 +3,6 @@ REACT_APP_SERVER_URL = 'http://localhost:8000'
|
|||||||
REACT_APP_GITHUB_CLIENT_ID =
|
REACT_APP_GITHUB_CLIENT_ID =
|
||||||
REACT_APP_GITHUB_TEMPLATE_REPO =
|
REACT_APP_GITHUB_TEMPLATE_REPO =
|
||||||
|
|
||||||
|
REACT_APP_GITEA_CLIENT_ID =
|
||||||
|
|
||||||
REACT_APP_WALLET_CONNECT_ID =
|
REACT_APP_WALLET_CONNECT_ID =
|
||||||
|
@ -1,15 +1,21 @@
|
|||||||
import { Button } from '@material-tailwind/react';
|
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import OauthPopup from 'react-oauth-popup';
|
import OauthPopup from 'react-oauth-popup';
|
||||||
|
import { GitType } from 'gql-client';
|
||||||
|
|
||||||
|
import { Button } from '@material-tailwind/react';
|
||||||
|
|
||||||
import { useGQLClient } from '../../../context/GQLClientContext';
|
import { useGQLClient } from '../../../context/GQLClientContext';
|
||||||
import ConnectAccountTabPanel from './ConnectAccountTabPanel';
|
import ConnectAccountTabPanel from './ConnectAccountTabPanel';
|
||||||
|
|
||||||
const SCOPES = 'repo user';
|
const SCOPES = 'repo user';
|
||||||
|
|
||||||
const GITHUB_OAUTH_URL = `https://github.com/login/oauth/authorize?client_id=${
|
const GITHUB_OAUTH_URL = `https://github.com/login/oauth/authorize?client_id=${
|
||||||
process.env.REACT_APP_GITHUB_CLIENT_ID
|
process.env.REACT_APP_GITHUB_CLIENT_ID
|
||||||
}&scope=${encodeURIComponent(SCOPES)}`;
|
}&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 {
|
interface ConnectAccountInterface {
|
||||||
onAuth: (token: string) => void;
|
onAuth: (token: string) => void;
|
||||||
}
|
}
|
||||||
@ -17,11 +23,13 @@ interface ConnectAccountInterface {
|
|||||||
const ConnectAccount = ({ onAuth: onToken }: ConnectAccountInterface) => {
|
const ConnectAccount = ({ onAuth: onToken }: ConnectAccountInterface) => {
|
||||||
const client = useGQLClient();
|
const client = useGQLClient();
|
||||||
|
|
||||||
const handleCode = async (code: string) => {
|
const handleCode = async (type: GitType, code: string) => {
|
||||||
// Pass code to backend and get access token
|
// Pass code to backend and get access token
|
||||||
const {
|
const {
|
||||||
authenticateGitHub: { token },
|
authenticateGit: { token },
|
||||||
} = await client.authenticateGitHub(code);
|
} = await client.authenticateGit(type, code);
|
||||||
|
|
||||||
|
// TODO: Handle token according to Git type
|
||||||
onToken(token);
|
onToken(token);
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -40,7 +48,7 @@ const ConnectAccount = ({ onAuth: onToken }: ConnectAccountInterface) => {
|
|||||||
<div className="mt-2 flex">
|
<div className="mt-2 flex">
|
||||||
<OauthPopup
|
<OauthPopup
|
||||||
url={GITHUB_OAUTH_URL}
|
url={GITHUB_OAUTH_URL}
|
||||||
onCode={handleCode}
|
onCode={(code) => handleCode(GitType.GitHub, code)}
|
||||||
onClose={() => {}}
|
onClose={() => {}}
|
||||||
title="Snowball"
|
title="Snowball"
|
||||||
width={1000}
|
width={1000}
|
||||||
@ -48,7 +56,16 @@ const ConnectAccount = ({ onAuth: onToken }: ConnectAccountInterface) => {
|
|||||||
>
|
>
|
||||||
<Button className="rounded-full mx-2">Connect to Github</Button>
|
<Button className="rounded-full mx-2">Connect to Github</Button>
|
||||||
</OauthPopup>
|
</OauthPopup>
|
||||||
|
<OauthPopup
|
||||||
|
url={GITEA_OAUTH_URL}
|
||||||
|
onCode={(code) => handleCode(GitType.Gitea, code)}
|
||||||
|
onClose={() => {}}
|
||||||
|
title="Snowball"
|
||||||
|
width={1000}
|
||||||
|
height={1000}
|
||||||
|
>
|
||||||
<Button className="rounded-full mx-2">Connect to Gitea</Button>
|
<Button className="rounded-full mx-2">Connect to Gitea</Button>
|
||||||
|
</OauthPopup>
|
||||||
</div>
|
</div>
|
||||||
<ConnectAccountTabPanel />
|
<ConnectAccountTabPanel />
|
||||||
</div>
|
</div>
|
||||||
|
@ -311,6 +311,18 @@ export class GQLClient {
|
|||||||
return data;
|
return data;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async authenticateGit (type: types.GitType, code: string): Promise<types.AuthenticateGitResponse> {
|
||||||
|
const { data } = await this.client.mutate({
|
||||||
|
mutation: mutations.authenticateGit,
|
||||||
|
variables: {
|
||||||
|
type,
|
||||||
|
code
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return data;
|
||||||
|
}
|
||||||
|
|
||||||
async unauthenticateGithub (): Promise<types.UnauthenticateGitHubResponse> {
|
async unauthenticateGithub (): Promise<types.UnauthenticateGitHubResponse> {
|
||||||
const { data } = await this.client.mutate({
|
const { data } = await this.client.mutate({
|
||||||
mutation: mutations.unauthenticateGitHub
|
mutation: mutations.unauthenticateGitHub
|
||||||
|
@ -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`
|
export const unauthenticateGitHub = gql`
|
||||||
mutation {
|
mutation {
|
||||||
unauthenticateGitHub
|
unauthenticateGitHub
|
||||||
|
@ -28,6 +28,11 @@ export enum DomainStatus {
|
|||||||
Pending = 'Pending',
|
Pending = 'Pending',
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export enum GitType {
|
||||||
|
GitHub = 'GitHub',
|
||||||
|
Gitea = 'Gitea',
|
||||||
|
}
|
||||||
|
|
||||||
export type EnvironmentVariable = {
|
export type EnvironmentVariable = {
|
||||||
id: string
|
id: string
|
||||||
environment: Environment
|
environment: Environment
|
||||||
@ -288,6 +293,12 @@ export type AuthenticateGitHubResponse = {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export type AuthenticateGitResponse = {
|
||||||
|
authenticateGit: {
|
||||||
|
token: string
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
export type UnauthenticateGitHubResponse = {
|
export type UnauthenticateGitHubResponse = {
|
||||||
unauthenticateGitHub: boolean
|
unauthenticateGitHub: boolean
|
||||||
}
|
}
|
||||||
|
22
yarn.lock
22
yarn.lock
@ -4717,6 +4717,14 @@
|
|||||||
resolved "https://registry.yarnpkg.com/@types/ms/-/ms-0.7.34.tgz#10964ba0dee6ac4cd462e2795b6bebd407303433"
|
resolved "https://registry.yarnpkg.com/@types/ms/-/ms-0.7.34.tgz#10964ba0dee6ac4cd462e2795b6bebd407303433"
|
||||||
integrity sha512-nG96G3Wp6acyAgJqGasjODb+acrI7KltPiRxzHPXnP3NgI28bpQDRv53olbqGXbfcgF5aiiHmO3xpwEpS5Ld9g==
|
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":
|
"@types/node-forge@^1.3.0":
|
||||||
version "1.3.10"
|
version "1.3.10"
|
||||||
resolved "https://registry.npmjs.org/@types/node-forge/-/node-forge-1.3.10.tgz"
|
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"
|
resolved "https://registry.yarnpkg.com/node-fetch-native/-/node-fetch-native-1.6.2.tgz#f439000d972eb0c8a741b65dcda412322955e1c6"
|
||||||
integrity sha512-69mtXOFZ6hSkYiXAVB5SqaRvrbITC/NPyqv7yuu/qw0nmgPyYbIMYYNIDhNtwPrzk0ptrimrLz/hhjvm4w5Z+w==
|
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:
|
node-fetch@2.6.7:
|
||||||
version "2.6.7"
|
version "2.6.7"
|
||||||
resolved "https://registry.yarnpkg.com/node-fetch/-/node-fetch-2.6.7.tgz#24de9fba827e3b4ae44dc8b20256a379160052ad"
|
resolved "https://registry.yarnpkg.com/node-fetch/-/node-fetch-2.6.7.tgz#24de9fba827e3b4ae44dc8b20256a379160052ad"
|
||||||
@ -13557,13 +13572,6 @@ node-fetch@2.6.7:
|
|||||||
dependencies:
|
dependencies:
|
||||||
whatwg-url "^5.0.0"
|
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:
|
node-forge@^1, node-forge@^1.3.1:
|
||||||
version "1.3.1"
|
version "1.3.1"
|
||||||
resolved "https://registry.npmjs.org/node-forge/-/node-forge-1.3.1.tgz"
|
resolved "https://registry.npmjs.org/node-forge/-/node-forge-1.3.1.tgz"
|
||||||
|
Loading…
Reference in New Issue
Block a user