Move VCS auth & callback APIs to Modules (#11274)

* Move VCS auth & callback APIs to Modules

* lint

* rename
This commit is contained in:
Hemachandar
2026-02-09 15:55:37 +05:30
committed by GitHub
parent f7ab8e46d3
commit 353b7f2a49
4 changed files with 278 additions and 193 deletions
-193
View File
@@ -1,19 +1,15 @@
<?php
use Appwrite\Auth\OAuth2\Github as OAuth2Github;
use Appwrite\Event\Build;
use Appwrite\Extend\Exception;
use Appwrite\SDK\AuthType;
use Appwrite\SDK\ContentType;
use Appwrite\SDK\Method;
use Appwrite\SDK\MethodType;
use Appwrite\SDK\Response as SDKResponse;
use Appwrite\Utopia\Request;
use Appwrite\Utopia\Response;
use Appwrite\Vcs\Comment;
use Utopia\CLI\Console;
use Utopia\Database\Database;
use Utopia\Database\DateTime;
use Utopia\Database\Document;
use Utopia\Database\Exception\Duplicate;
use Utopia\Database\Helpers\ID;
@@ -472,195 +468,6 @@ $createGitDeployments = function (GitHub $github, string $providerInstallationId
}
};
Http::get('/v1/vcs/github/authorize')
->desc('Create GitHub app installation')
->groups(['api', 'vcs'])
->label('scope', 'vcs.read')
->label('error', __DIR__ . '/../../views/general/error.phtml')
->label('sdk', new Method(
namespace: 'vcs',
group: 'installations',
name: 'createGitHubInstallation',
description: '/docs/references/vcs/create-github-installation.md',
auth: [AuthType::ADMIN],
responses: [
new SDKResponse(
code: Response::STATUS_CODE_MOVED_PERMANENTLY,
model: Response::MODEL_NONE,
)
],
contentType: ContentType::HTML,
type: MethodType::WEBAUTH,
hide: true,
))
->param('success', '', fn ($redirectValidator) => $redirectValidator, 'URL to redirect back to console after a successful installation attempt.', true, ['redirectValidator'])
->param('failure', '', fn ($redirectValidator) => $redirectValidator, 'URL to redirect back to console after a failed installation attempt.', true, ['redirectValidator'])
->inject('response')
->inject('project')
->inject('platform')
->action(function (string $success, string $failure, Response $response, Document $project, array $platform) {
$state = \json_encode([
'projectId' => $project->getId(),
'success' => $success,
'failure' => $failure,
]);
$appName = System::getEnv('_APP_VCS_GITHUB_APP_NAME');
$protocol = System::getEnv('_APP_OPTIONS_FORCE_HTTPS') === 'disabled' ? 'http' : 'https';
$hostname = $platform['consoleHostname'] ?? '';
if (empty($appName)) {
throw new Exception(Exception::GENERAL_SERVER_ERROR, 'GitHub App name is not configured. Please configure VCS (Version Control System) variables in .env file.');
}
$url = "https://github.com/apps/$appName/installations/new?" . \http_build_query([
'state' => $state,
'redirect_uri' => $protocol . '://' . $hostname . "/v1/vcs/github/callback"
]);
$response
->addHeader('Cache-Control', 'no-store, no-cache, must-revalidate, max-age=0')
->addHeader('Pragma', 'no-cache')
->redirect($url);
});
Http::get('/v1/vcs/github/callback')
->desc('Get installation and authorization from GitHub app')
->groups(['api', 'vcs'])
->label('scope', 'public')
->label('error', __DIR__ . '/../../views/general/error.phtml')
->param('installation_id', '', new Text(256, 0), 'GitHub installation ID', true)
->param('setup_action', '', new Text(256, 0), 'GitHub setup action type', true)
->param('state', '', new Text(2048), 'GitHub state. Contains info sent when starting authorization flow.', true)
->param('code', '', new Text(2048, 0), 'OAuth2 code. This is a temporary code that the will be later exchanged for an access token.', true)
->inject('gitHub')
->inject('user')
->inject('project')
->inject('response')
->inject('dbForPlatform')
->inject('platform')
->action(function (string $providerInstallationId, string $setupAction, string $state, string $code, GitHub $github, Document $user, Document $project, Response $response, Database $dbForPlatform, array $platform) {
if (empty($state)) {
$error = 'Installation requests from organisation members for the Appwrite GitHub App are currently unsupported. To proceed with the installation, login to the Appwrite Console and install the GitHub App.';
throw new Exception(Exception::GENERAL_ARGUMENT_INVALID, $error);
}
$state = \json_decode($state, true);
$projectId = $state['projectId'] ?? '';
$project = $dbForPlatform->getDocument('projects', $projectId);
if ($project->isEmpty()) {
$error = 'Project with the ID from state could not be found.';
if (!empty($redirectFailure)) {
$separator = \str_contains($redirectFailure, '?') ? '&' : ':';
return $response
->addHeader('Cache-Control', 'no-store, no-cache, must-revalidate, max-age=0')
->addHeader('Pragma', 'no-cache')
->redirect($redirectFailure . $separator . \http_build_query(['error' => $error]));
}
throw new Exception(Exception::PROJECT_NOT_FOUND, $error);
}
$region = $project->getAttribute('region', 'default');
$protocol = System::getEnv('_APP_OPTIONS_FORCE_HTTPS') === 'disabled' ? 'http' : 'https';
$hostname = $platform['consoleHostname'] ?? '';
$defaultState = [
'success' => $protocol . '://' . $hostname . "/console/project-$region-$projectId/settings/git-installations",
'failure' => $protocol . '://' . $hostname . "/console/project-$region-$projectId/settings/git-installations",
];
$state = \array_merge($defaultState, $state ?? []);
$redirectSuccess = $state['success'] ?? '';
$redirectFailure = $state['failure'] ?? '';
// Create / Update installation
if (!empty($providerInstallationId)) {
$privateKey = System::getEnv('_APP_VCS_GITHUB_PRIVATE_KEY');
$githubAppId = System::getEnv('_APP_VCS_GITHUB_APP_ID');
$github->initializeVariables($providerInstallationId, $privateKey, $githubAppId);
$owner = $github->getOwnerName($providerInstallationId) ?? '';
$projectInternalId = $project->getSequence();
$installation = $dbForPlatform->findOne('installations', [
Query::equal('providerInstallationId', [$providerInstallationId]),
Query::equal('projectInternalId', [$projectInternalId])
]);
$personal = false;
$refreshToken = null;
$accessToken = null;
$accessTokenExpiry = null;
if (!empty($code)) {
$oauth2 = new OAuth2Github(System::getEnv('_APP_VCS_GITHUB_CLIENT_ID', ''), System::getEnv('_APP_VCS_GITHUB_CLIENT_SECRET', ''), "");
$accessToken = $oauth2->getAccessToken($code) ?? '';
$refreshToken = $oauth2->getRefreshToken($code) ?? '';
$accessTokenExpiry = DateTime::addSeconds(new \DateTime(), \intval($oauth2->getAccessTokenExpiry($code)));
$personalSlug = $oauth2->getUserSlug($accessToken) ?? '';
$personal = $personalSlug === $owner;
}
if ($installation->isEmpty()) {
$teamId = $project->getAttribute('teamId', '');
$installation = 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')),
],
'providerInstallationId' => $providerInstallationId,
'projectId' => $projectId,
'projectInternalId' => $projectInternalId,
'provider' => 'github',
'organization' => $owner,
'personal' => $personal,
'personalRefreshToken' => $refreshToken,
'personalAccessToken' => $accessToken,
'personalAccessTokenExpiry' => $accessTokenExpiry,
]);
$installation = $dbForPlatform->createDocument('installations', $installation);
} else {
$installation = $installation
->setAttribute('organization', $owner)
->setAttribute('personal', $personal)
->setAttribute('personalRefreshToken', $refreshToken)
->setAttribute('personalAccessToken', $accessToken)
->setAttribute('personalAccessTokenExpiry', $accessTokenExpiry);
$installation = $dbForPlatform->updateDocument('installations', $installation->getId(), $installation);
}
} else {
$error = 'Installation of the Appwrite GitHub App on organization accounts is restricted to organization owners. As a member of the organization, you do not have the necessary permissions to install this GitHub App. Please contact the organization owner to create the installation from the Appwrite console.';
if (!empty($redirectFailure)) {
$separator = \str_contains($redirectFailure, '?') ? '&' : ':';
return $response
->addHeader('Cache-Control', 'no-store, no-cache, must-revalidate, max-age=0')
->addHeader('Pragma', 'no-cache')
->redirect($redirectFailure . $separator . \http_build_query(['error' => $error]));
}
throw new Exception(Exception::GENERAL_ARGUMENT_INVALID, $error);
}
$response
->addHeader('Cache-Control', 'no-store, no-cache, must-revalidate, max-age=0')
->addHeader('Pragma', 'no-cache')
->redirect($redirectSuccess);
});
Http::post('/v1/vcs/github/events')
->desc('Create event')
->groups(['api', 'vcs'])
@@ -0,0 +1,90 @@
<?php
namespace Appwrite\Platform\Modules\VCS\Http\GitHub\Authorize;
use Appwrite\Extend\Exception;
use Appwrite\Platform\Action;
use Appwrite\SDK\AuthType;
use Appwrite\SDK\ContentType;
use Appwrite\SDK\Method;
use Appwrite\SDK\MethodType;
use Appwrite\SDK\Response as SDKResponse;
use Appwrite\Utopia\Response;
use Utopia\Database\Document;
use Utopia\Platform\Scope\HTTP;
use Utopia\System\System;
class Get extends Action
{
use HTTP;
public static function getName()
{
return 'getVCSGitHubAuthorize';
}
public function __construct()
{
$this
->setHttpMethod(Action::HTTP_REQUEST_METHOD_GET)
->setHttpPath('/v1/vcs/github/authorize')
->desc('Create GitHub app installation')
->groups(['api', 'vcs'])
->label('scope', 'vcs.read')
->label('error', __DIR__ . '/../../views/general/error.phtml')
->label('sdk', new Method(
namespace: 'vcs',
group: 'installations',
name: 'createGitHubInstallation',
description: '/docs/references/vcs/create-github-installation.md',
auth: [AuthType::ADMIN],
responses: [
new SDKResponse(
code: Response::STATUS_CODE_MOVED_PERMANENTLY,
model: Response::MODEL_NONE,
)
],
contentType: ContentType::HTML,
type: MethodType::WEBAUTH,
hide: true,
))
->param('success', '', fn ($redirectValidator) => $redirectValidator, 'URL to redirect back to console after a successful installation attempt.', true, ['redirectValidator'])
->param('failure', '', fn ($redirectValidator) => $redirectValidator, 'URL to redirect back to console after a failed installation attempt.', true, ['redirectValidator'])
->inject('response')
->inject('project')
->inject('platform')
->callback($this->action(...));
}
public function action(
string $success,
string $failure,
Response $response,
Document $project,
array $platform
) {
$state = \json_encode([
'projectId' => $project->getId(),
'success' => $success,
'failure' => $failure,
]);
$appName = System::getEnv('_APP_VCS_GITHUB_APP_NAME');
$protocol = System::getEnv('_APP_OPTIONS_FORCE_HTTPS') === 'disabled' ? 'http' : 'https';
$hostname = $platform['consoleHostname'] ?? '';
if (empty($appName)) {
throw new Exception(Exception::GENERAL_SERVER_ERROR, 'GitHub App name is not configured. Please configure VCS (Version Control System) variables in .env file.');
}
$url = "https://github.com/apps/$appName/installations/new?" . \http_build_query([
'state' => $state,
'redirect_uri' => $protocol . '://' . $hostname . "/v1/vcs/github/callback"
]);
$response
->addHeader('Cache-Control', 'no-store, no-cache, must-revalidate, max-age=0')
->addHeader('Pragma', 'no-cache')
->redirect($url);
}
}
@@ -0,0 +1,182 @@
<?php
namespace Appwrite\Platform\Modules\VCS\Http\GitHub\Callback;
use Appwrite\Auth\OAuth2\Github as OAuth2Github;
use Appwrite\Extend\Exception;
use Appwrite\Utopia\Response;
use Utopia\Database\Database;
use Utopia\Database\DateTime;
use Utopia\Database\Document;
use Utopia\Database\Helpers\ID;
use Utopia\Database\Helpers\Permission;
use Utopia\Database\Helpers\Role;
use Utopia\Database\Query;
use Utopia\Platform\Action;
use Utopia\Platform\Scope\HTTP;
use Utopia\System\System;
use Utopia\Validator\Text;
use Utopia\VCS\Adapter\Git\GitHub;
class Get extends Action
{
use HTTP;
public static function getName()
{
return 'getVCSGitHubCallback';
}
public function __construct()
{
$this
->setHttpMethod(Action::HTTP_REQUEST_METHOD_GET)
->setHttpPath('/v1/vcs/github/callback')
->desc('Get installation and authorization from GitHub app')
->groups(['api', 'vcs'])
->label('scope', 'public')
->label('error', __DIR__ . '/../../views/general/error.phtml')
->param('installation_id', '', new Text(256, 0), 'GitHub installation ID', true)
->param('setup_action', '', new Text(256, 0), 'GitHub setup action type', true)
->param('state', '', new Text(2048), 'GitHub state. Contains info sent when starting authorization flow.', true)
->param('code', '', new Text(2048, 0), 'OAuth2 code. This is a temporary code that the will be later exchanged for an access token.', true)
->inject('gitHub')
->inject('project')
->inject('response')
->inject('dbForPlatform')
->inject('platform')
->callback($this->action(...));
}
public function action(
string $providerInstallationId,
string $setupAction,
string $state,
string $code,
GitHub $github,
Document $project,
Response $response,
Database $dbForPlatform,
array $platform
) {
if (empty($state)) {
$error = 'Installation requests from organisation members for the Appwrite GitHub App are currently unsupported. To proceed with the installation, login to the Appwrite Console and install the GitHub App.';
throw new Exception(Exception::GENERAL_ARGUMENT_INVALID, $error);
}
$state = \json_decode($state, true);
$projectId = $state['projectId'] ?? '';
$project = $dbForPlatform->getDocument('projects', $projectId);
if ($project->isEmpty()) {
$error = 'Project with the ID from state could not be found.';
if (!empty($redirectFailure)) {
$separator = \str_contains($redirectFailure, '?') ? '&' : ':';
return $response
->addHeader('Cache-Control', 'no-store, no-cache, must-revalidate, max-age=0')
->addHeader('Pragma', 'no-cache')
->redirect($redirectFailure . $separator . \http_build_query(['error' => $error]));
}
throw new Exception(Exception::PROJECT_NOT_FOUND, $error);
}
$region = $project->getAttribute('region', 'default');
$protocol = System::getEnv('_APP_OPTIONS_FORCE_HTTPS') === 'disabled' ? 'http' : 'https';
$hostname = $platform['consoleHostname'] ?? '';
$defaultState = [
'success' => $protocol . '://' . $hostname . "/console/project-$region-$projectId/settings/git-installations",
'failure' => $protocol . '://' . $hostname . "/console/project-$region-$projectId/settings/git-installations",
];
$state = \array_merge($defaultState, $state ?? []);
$redirectSuccess = $state['success'] ?? '';
$redirectFailure = $state['failure'] ?? '';
// Create / Update installation
if (!empty($providerInstallationId)) {
$privateKey = System::getEnv('_APP_VCS_GITHUB_PRIVATE_KEY');
$githubAppId = System::getEnv('_APP_VCS_GITHUB_APP_ID');
$github->initializeVariables($providerInstallationId, $privateKey, $githubAppId);
$owner = $github->getOwnerName($providerInstallationId) ?? '';
$projectInternalId = $project->getSequence();
$installation = $dbForPlatform->findOne('installations', [
Query::equal('providerInstallationId', [$providerInstallationId]),
Query::equal('projectInternalId', [$projectInternalId])
]);
$personal = false;
$refreshToken = null;
$accessToken = null;
$accessTokenExpiry = null;
if (!empty($code)) {
$oauth2 = new OAuth2Github(System::getEnv('_APP_VCS_GITHUB_CLIENT_ID', ''), System::getEnv('_APP_VCS_GITHUB_CLIENT_SECRET', ''), "");
$accessToken = $oauth2->getAccessToken($code) ?? '';
$refreshToken = $oauth2->getRefreshToken($code) ?? '';
$accessTokenExpiry = DateTime::addSeconds(new \DateTime(), \intval($oauth2->getAccessTokenExpiry($code)));
$personalSlug = $oauth2->getUserSlug($accessToken) ?? '';
$personal = $personalSlug === $owner;
}
if ($installation->isEmpty()) {
$teamId = $project->getAttribute('teamId', '');
$installation = 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')),
],
'providerInstallationId' => $providerInstallationId,
'projectId' => $projectId,
'projectInternalId' => $projectInternalId,
'provider' => 'github',
'organization' => $owner,
'personal' => $personal,
'personalRefreshToken' => $refreshToken,
'personalAccessToken' => $accessToken,
'personalAccessTokenExpiry' => $accessTokenExpiry,
]);
$installation = $dbForPlatform->createDocument('installations', $installation);
} else {
$installation = $installation
->setAttribute('organization', $owner)
->setAttribute('personal', $personal)
->setAttribute('personalRefreshToken', $refreshToken)
->setAttribute('personalAccessToken', $accessToken)
->setAttribute('personalAccessTokenExpiry', $accessTokenExpiry);
$installation = $dbForPlatform->updateDocument('installations', $installation->getId(), $installation);
}
} else {
$error = 'Installation of the Appwrite GitHub App on organization accounts is restricted to organization owners. As a member of the organization, you do not have the necessary permissions to install this GitHub App. Please contact the organization owner to create the installation from the Appwrite console.';
if (!empty($redirectFailure)) {
$separator = \str_contains($redirectFailure, '?') ? '&' : ':';
return $response
->addHeader('Cache-Control', 'no-store, no-cache, must-revalidate, max-age=0')
->addHeader('Pragma', 'no-cache')
->redirect($redirectFailure . $separator . \http_build_query(['error' => $error]));
}
throw new Exception(Exception::GENERAL_ARGUMENT_INVALID, $error);
}
$response
->addHeader('Cache-Control', 'no-store, no-cache, must-revalidate, max-age=0')
->addHeader('Pragma', 'no-cache')
->redirect($redirectSuccess);
}
}
@@ -2,6 +2,8 @@
namespace Appwrite\Platform\Modules\VCS\Services;
use Appwrite\Platform\Modules\VCS\Http\GitHub\Authorize\Get as GetGitHubAuthorize;
use Appwrite\Platform\Modules\VCS\Http\GitHub\Callback\Get as GetGitHubCallback;
use Appwrite\Platform\Modules\VCS\Http\Installations\Delete as DeleteInstallation;
use Appwrite\Platform\Modules\VCS\Http\Installations\Get as GetInstallation;
use Appwrite\Platform\Modules\VCS\Http\Installations\Repositories\Branches\XList as ListRepositoryBranches;
@@ -19,6 +21,10 @@ class Http extends Service
{
$this->type = Service::TYPE_HTTP;
// GitHub Authorization & Callback
$this->addAction(GetGitHubAuthorize::getName(), new GetGitHubAuthorize());
$this->addAction(GetGitHubCallback::getName(), new GetGitHubCallback());
// Installations
$this->addAction(GetInstallation::getName(), new GetInstallation());
$this->addAction(ListInstallations::getName(), new ListInstallations());