From a174562c4e394698863ff359a5a95e54d856cc2d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Matej=20Ba=C4=8Do?= Date: Sun, 18 Jun 2023 13:38:37 +0200 Subject: [PATCH] vcs.createRepository() --- app/config/collections.php | 49 ++++++-- app/controllers/api/vcs.php | 172 ++++++++++++++++++++++------ app/init.php | 11 +- composer.lock | 4 +- docker-compose.yml | 1 + src/Appwrite/Auth/OAuth2/Github.php | 11 ++ 6 files changed, 196 insertions(+), 52 deletions(-) diff --git a/app/config/collections.php b/app/config/collections.php index 34fa7064f7..d754f50225 100644 --- a/app/config/collections.php +++ b/app/config/collections.php @@ -1534,7 +1534,40 @@ $collections = [ 'default' => null, 'array' => false, 'filters' => [], - ] + ], + [ + '$id' => ID::custom('vcsGithubAccessToken'), + 'type' => Database::VAR_STRING, + 'format' => '', + 'size' => 16384, + 'signed' => true, + 'required' => false, + 'default' => null, + 'array' => false, + 'filters' => ['encrypt'], + ], + [ + '$id' => ID::custom('vcsGithubAccessTokenExpiry'), + 'type' => Database::VAR_DATETIME, + 'format' => '', + 'size' => 0, + 'signed' => false, + 'required' => false, + 'default' => null, + 'array' => false, + 'filters' => ['datetime'], + ], + [ + '$id' => ID::custom('vcsGithubRefreshToken'), + 'type' => Database::VAR_STRING, + 'format' => '', + 'size' => 16384, + 'signed' => true, + 'required' => false, + 'default' => null, + 'array' => false, + 'filters' => ['encrypt'], + ], ], 'indexes' => [ [ @@ -2282,16 +2315,16 @@ $collections = [ 'filters' => [], ], [ - '$id' => ID::custom('accessToken'), - 'type' => Database::VAR_STRING, - 'format' => '', - 'size' => 16384, + '$id' => ID::custom('personal'), + 'type' => Database::VAR_BOOLEAN, 'signed' => true, + 'size' => 0, + 'format' => '', + 'filters' => [], 'required' => false, - 'default' => null, + 'default' => false, 'array' => false, - 'filters' => ['encrypt'] - ] + ], ], 'indexes' => [], ], diff --git a/app/controllers/api/vcs.php b/app/controllers/api/vcs.php index 143329a524..6acacb04c0 100644 --- a/app/controllers/api/vcs.php +++ b/app/controllers/api/vcs.php @@ -1,5 +1,6 @@ desc('Capture installation id and state after GitHub App Installation') +App::get('/v1/vcs/github/redirect') + ->desc('Capture installation and authorization from GitHub App') ->groups(['api', 'vcs']) ->label('scope', 'public') ->param('installation_id', '', new Text(256), 'GitHub installation ID') ->param('setup_action', '', new Text(256), 'GitHub setup actuon type') ->param('state', '', new Text(2048), 'GitHub state. Contains info sent when starting authorization flow.') + ->param('code', '', new Text(2048), 'OAuth2 code.', true) ->inject('gitHub') + ->inject('user') ->inject('project') ->inject('request') ->inject('response') ->inject('dbForConsole') - ->action(function (string $installationId, string $setupAction, string $state, GitHub $github, Document $project, Request $request, Response $response, Database $dbForConsole) { + ->action(function (string $installationId, string $setupAction, string $state, string $code, GitHub $github, Document $user, Document $project, Request $request, Response $response, Database $dbForConsole) { $state = \json_decode($state, true); $redirect = $state['redirect'] ?? ''; + $projectId = $state['projectId'] ?? ''; - $projectId = $project->getId(); + $project = $dbForConsole->getDocument('projects', $projectId); if (empty($redirect)) { $redirect = $request->getProtocol() . '://' . $request->getHostname() . "/console/project-$projectId/settings/git-installations"; @@ -94,38 +100,64 @@ App::get('/v1/vcs/github/incominginstallation') ->redirect($redirect); } - $privateKey = App::getEnv('VCS_GITHUB_PRIVATE_KEY'); - $githubAppId = App::getEnv('VCS_GITHUB_APP_ID'); - $github->initialiseVariables($installationId, $privateKey, $githubAppId); - $owner = $github->getOwnerName($installationId); + $personalSlug = ''; - $projectInternalId = $project->getInternalId(); + // OAuth Authroization + if (!empty($code)) { + $oauth2 = new OAuth2Github(App::getEnv('VCS_GITHUB_CLIENT_ID', ''), App::getEnv('VCS_GITHUB_CLIENT_SECRET', ''), ""); + $accessToken = $oauth2->getAccessToken($code); + $refreshToken = $oauth2->getRefreshToken($code); + $accessTokenExpiry = $oauth2->getAccessTokenExpiry($code); + $personalSlug = $oauth2->getUserSlug($accessToken); - $vcsInstallation = $dbForConsole->findOne('vcsInstallations', [ - Query::equal('installationId', [$installationId]), - Query::equal('projectInternalId', [$projectInternalId]) - ]); + $user = $user + ->setAttribute('vcsGithubAccessToken', $accessToken) + ->setAttribute('vcsGithubRefreshToken', $refreshToken) + ->setAttribute('vcsGithubAccessTokenExpiry', DateTime::addSeconds(new \DateTime(), (int)$accessTokenExpiry)); - if ($vcsInstallation === false || $vcsInstallation->isEmpty()) { - $vcsInstallation = new Document([ - '$id' => ID::unique(), - '$permissions' => [ - Permission::read(Role::any()), - Permission::update(Role::any()), - Permission::delete(Role::any()), - ], - 'installationId' => $installationId, - 'projectId' => $projectId, - 'projectInternalId' => $projectInternalId, - 'provider' => 'github', - 'organization' => $owner, - 'accessToken' => null + $dbForConsole->updateDocument('users', $user->getId(), $user); + } + + // Create / Update installation + if (!empty($installationId)) { + $privateKey = App::getEnv('VCS_GITHUB_PRIVATE_KEY'); + $githubAppId = App::getEnv('VCS_GITHUB_APP_ID'); + $github->initialiseVariables($installationId, $privateKey, $githubAppId); + $owner = $github->getOwnerName($installationId); + + $projectInternalId = $project->getInternalId(); + + $vcsInstallation = $dbForConsole->findOne('vcsInstallations', [ + Query::equal('installationId', [$installationId]), + Query::equal('projectInternalId', [$projectInternalId]) ]); - $vcsInstallation = $dbForConsole->createDocument('vcsInstallations', $vcsInstallation); - } else { - $vcsInstallation = $vcsInstallation->setAttribute('organization', $owner); - $vcsInstallation = $dbForConsole->updateDocument('vcsInstallations', $vcsInstallation->getId(), $vcsInstallation); + if ($vcsInstallation === false || $vcsInstallation->isEmpty()) { + $teamId = $project->getAttribute('teamId', ''); + + $vcsInstallation = new Document([ + '$id' => ID::unique(), + '$permissions' => [ + Permission::read(Role::team(ID::custom($teamId))), + Permission::update(Role::team(ID::custom($teamId), 'owner')), + Permission::update(Role::team(ID::custom($teamId), 'developer')), + Permission::delete(Role::team(ID::custom($teamId), 'owner')), + Permission::delete(Role::team(ID::custom($teamId), 'developer')), + ], + 'installationId' => $installationId, + 'projectId' => $projectId, + 'projectInternalId' => $projectInternalId, + 'provider' => 'github', + 'organization' => $owner, + 'personal' => $personalSlug === $owner + ]); + + $vcsInstallation = $dbForConsole->createDocument('vcsInstallations', $vcsInstallation); + } else { + $vcsInstallation = $vcsInstallation->setAttribute('organization', $owner); + $vcsInstallation = $vcsInstallation->setAttribute('personal', $personalSlug === $owner); + $vcsInstallation = $dbForConsole->updateDocument('vcsInstallations', $vcsInstallation->getId(), $vcsInstallation); + } } $response @@ -151,8 +183,6 @@ App::get('/v1/vcs/github/installations/:installationId/repositories') ->inject('project') ->inject('dbForConsole') ->action(function (string $vcsInstallationId, string $search, GitHub $github, Response $response, Document $project, Database $dbForConsole) { - $start = \microtime(true); - if (empty($search)) { $search = ""; } @@ -225,14 +255,84 @@ App::get('/v1/vcs/github/installations/:installationId/repositories') return new Document($repo); }, $repos); - \var_dump(\microtime(true) - $start); - $response->dynamic(new Document([ 'repositories' => $repos, 'total' => \count($repos), ]), Response::MODEL_REPOSITORY_LIST); }); +App::post('/v1/vcs/github/installations/:installationId/repositories') + ->desc('Create repository') + ->groups(['api', 'vcs']) + ->label('scope', 'public') + ->label('sdk.namespace', 'vcs') + ->label('sdk.method', 'createRepository') + ->label('sdk.description', '') + ->label('sdk.response.code', Response::STATUS_CODE_OK) + ->label('sdk.response.type', Response::CONTENT_TYPE_JSON) + ->label('sdk.response.model', Response::MODEL_REPOSITORY) + ->param('installationId', '', new Text(256), 'Installation Id') + ->param('name', '', new Text(256), 'Repository name (slug)') + ->param('private', '', new Boolean(false), 'Mark repository public or private') + ->inject('gitHub') + ->inject('user') + ->inject('response') + ->inject('project') + ->inject('dbForConsole') + ->action(function (string $vcsInstallationId, string $name, bool $private, GitHub $github, Document $user, Response $response, Document $project, Database $dbForConsole) { + $installation = $dbForConsole->getDocument('vcsInstallations', $vcsInstallationId, [ + Query::equal('projectInternalId', [$project->getInternalId()]) + ]); + + if ($installation->isEmpty()) { + throw new Exception(Exception::INSTALLATION_NOT_FOUND); + } + + if ($installation->getAttribute('personal', false) === true) { + $oauth2 = new OAuth2Github(App::getEnv('VCS_GITHUB_CLIENT_ID', ''), App::getEnv('VCS_GITHUB_CLIENT_SECRET', ''), ""); + + $accessToken = $user->getAttribute('vcsGithubAccessToken'); + $refreshToken = $user->getAttribute('vcsGithubRefreshToken'); + $accessTokenExpiry = $user->getAttribute('vcsGithubAccessTokenExpiry'); + + $isExpired = new \DateTime($accessTokenExpiry) < new \DateTime('now'); + if ($isExpired) { + $oauth2->refreshTokens($refreshToken); + + $accessToken = $oauth2->getAccessToken(''); + $refreshToken = $oauth2->getRefreshToken(''); + + $verificationId = $oauth2->getUserID($accessToken); + + if (empty($verificationId)) { + throw new Exception(Exception::GENERAL_RATE_LIMIT_EXCEEDED, "Another request is currently refreshing OAuth token. Please try again."); + } + + $user = $user + ->setAttribute('vcsGithubAccessToken', $accessToken) + ->setAttribute('vcsGithubRefreshToken', $refreshToken) + ->setAttribute('vcsGithubAccessTokenExpiry', DateTime::addSeconds(new \DateTime(), (int)$oauth2->getAccessTokenExpiry(''))); + + $dbForConsole->updateDocument('users', $user->getId(), $user); + } + + $repository = $oauth2->createRepository($accessToken, $name, $private); + } else { + $installationId = $installation->getAttribute('installationId'); + $privateKey = App::getEnv('VCS_GITHUB_PRIVATE_KEY'); + $githubAppId = App::getEnv('VCS_GITHUB_APP_ID'); + $github->initialiseVariables($installationId, $privateKey, $githubAppId); + $owner = $github->getOwnerName($installationId); + + $repository = $github->createRepository($owner, $name, $private); + } + + $repository['id'] = \strval($repository['id']); + $repository['pushedAt'] = $repository['pushed_at']; + + $response->dynamic(new Document($repository), Response::MODEL_REPOSITORY); + }); + App::get('/v1/vcs/github/installations/:installationId/repositories/:repositoryId') ->desc('Get repository') ->groups(['api', 'vcs']) @@ -371,7 +471,7 @@ $createGitDeployments = function (GitHub $github, string $installationId, string $repositoryName = $github->getRepositoryName($repositoryId); $comment = new Comment(); - // TODO: Add all builds reeeeee + // TODO: Add all builds $comment->addBuild($project, $function, 'waiting', $deploymentId); if (empty($latestCommentId)) { diff --git a/app/init.php b/app/init.php index 4d38eacbb1..1fb49ddd95 100644 --- a/app/init.php +++ b/app/init.php @@ -936,7 +936,11 @@ App::setResource('user', function ($mode, $project, $console, $request, $respons if ($project->isEmpty()) { $user = new Document(['$id' => ID::custom(''), '$collection' => 'users']); } else { - $user = $dbForProject->getDocument('users', Auth::$unique); + if($project->getId() === 'console') { + $user = $dbForConsole->getDocument('users', Auth::$unique); + } else { + $user = $dbForProject->getDocument('users', Auth::$unique); + } } } else { $user = $dbForConsole->getDocument('users', Auth::$unique); @@ -994,11 +998,6 @@ App::setResource('project', function ($dbForConsole, $request, $console) { $projectId = $request->getParam('project', ''); } elseif (!empty($request->getHeader('x-appwrite-project', ''))) { $projectId = $request->getHeader('x-appwrite-project', ''); - } elseif (!empty($request->getParam('state', ''))) { - $state = \json_decode($request->getParam('state', ''), true); - if (!empty($state['projectId'])) { - $projectId = $state['projectId']; - } } if ($projectId === 'console') { diff --git a/composer.lock b/composer.lock index 35f6ab87a6..c5f249afd5 100644 --- a/composer.lock +++ b/composer.lock @@ -2705,7 +2705,7 @@ "source": { "type": "git", "url": "https://github.com/utopia-php/vcs.git", - "reference": "2966e482920a9dd028a237083a81de723a9bcbf2" + "reference": "7fb5b733ab0676b257d8bc8fc4e0c4cafd4c756c" }, "require": { "adhocore/jwt": "^1.1", @@ -2750,7 +2750,7 @@ "utopia", "vcs" ], - "time": "2023-06-16T11:02:30+00:00" + "time": "2023-06-17T14:27:48+00:00" }, { "name": "utopia-php/websocket", diff --git a/docker-compose.yml b/docker-compose.yml index a61bcffeb2..f889350577 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -82,6 +82,7 @@ services: - traefik.http.routers.appwrite_api_https.tls.domains[0].main=$_APP_DOMAIN_FUNCTIONS - traefik.http.routers.appwrite_api_https.tls.domains[0].sans=*.$_APP_DOMAIN_FUNCTIONS volumes: + - ./vendor/utopia-php/vcs:/usr/src/code/vendor/utopia-php/vcs - appwrite-uploads:/storage/uploads:rw - appwrite-cache:/storage/cache:rw - appwrite-config:/storage/config:rw diff --git a/src/Appwrite/Auth/OAuth2/Github.php b/src/Appwrite/Auth/OAuth2/Github.php index 8b9208fc06..1cefc397c5 100644 --- a/src/Appwrite/Auth/OAuth2/Github.php +++ b/src/Appwrite/Auth/OAuth2/Github.php @@ -208,4 +208,15 @@ class Github extends OAuth2 return $this->user; } + + public function createRepository(string $accessToken, string $repositoryName, bool $private): array + { + $repository = $this->request('POST', 'https://api.github.com/user/repos', ['Authorization: token ' . \urlencode($accessToken)], \json_encode([ + 'name' => $repositoryName, + 'private' => $private + ])); + + $repository = \json_decode($repository, true); + return $repository; + } }