mirror of
https://github.com/appwrite/appwrite.git
synced 2026-05-26 13:51:13 +00:00
Merge branch '1.8.x' of https://github.com/appwrite/appwrite into joins2
# Conflicts: # composer.lock
This commit is contained in:
+1
-1
@@ -227,7 +227,7 @@ return [
|
||||
[
|
||||
'key' => 'cli',
|
||||
'name' => 'Command Line',
|
||||
'version' => '13.3.0',
|
||||
'version' => '13.3.1',
|
||||
'url' => 'https://github.com/appwrite/sdk-for-cli',
|
||||
'package' => 'https://www.npmjs.com/package/appwrite-cli',
|
||||
'enabled' => true,
|
||||
|
||||
@@ -1467,13 +1467,14 @@ Http::get('/v1/account/sessions/oauth2/:provider/redirect')
|
||||
->inject('devKey')
|
||||
->inject('user')
|
||||
->inject('dbForProject')
|
||||
->inject('dbForPlatform')
|
||||
->inject('geodb')
|
||||
->inject('queueForEvents')
|
||||
->inject('store')
|
||||
->inject('proofForPassword')
|
||||
->inject('proofForToken')
|
||||
->inject('authorization')
|
||||
->action(function (string $provider, string $code, string $state, string $error, string $error_description, Request $request, Response $response, Document $project, Validator $redirectValidator, Document $devKey, User $user, Database $dbForProject, Reader $geodb, Event $queueForEvents, Store $store, ProofsPassword $proofForPassword, ProofsToken $proofForToken, Authorization $authorization) use ($oauthDefaultSuccess) {
|
||||
->action(function (string $provider, string $code, string $state, string $error, string $error_description, Request $request, Response $response, Document $project, Validator $redirectValidator, Document $devKey, User $user, Database $dbForProject, Database $dbForPlatform, Reader $geodb, Event $queueForEvents, Store $store, ProofsPassword $proofForPassword, ProofsToken $proofForToken, Authorization $authorization) use ($oauthDefaultSuccess) {
|
||||
$protocol = System::getEnv('_APP_OPTIONS_FORCE_HTTPS') === 'disabled' ? 'http' : 'https';
|
||||
$port = $request->getPort();
|
||||
$callbackBase = $protocol . '://' . $request->getHostname();
|
||||
@@ -1510,6 +1511,25 @@ Http::get('/v1/account/sessions/oauth2/:provider/redirect')
|
||||
$state = $defaultState;
|
||||
}
|
||||
|
||||
// Allow redirect to rule URL if related to project
|
||||
//Check if $redirectValidator is instance of Redirect class
|
||||
if ($redirectValidator instanceof Redirect) {
|
||||
$rules = $authorization->skip(fn () => $dbForPlatform->find('rules', [
|
||||
Query::equal('domain', [
|
||||
parse_url($state['success'], PHP_URL_HOST),
|
||||
parse_url($state['failure'], PHP_URL_HOST)
|
||||
]),
|
||||
Query::equal('projectInternalId', [$project->getSequence()]),
|
||||
Query::limit(2)
|
||||
]));
|
||||
|
||||
foreach ($rules as $rule) {
|
||||
$allowedHostnames = $redirectValidator->getAllowedHostnames();
|
||||
$allowedHostnames[] = $rule->getAttribute('domain', '');
|
||||
$redirectValidator->setAllowedHostnames($allowedHostnames);
|
||||
}
|
||||
}
|
||||
|
||||
if ($devKey->isEmpty() && !$redirectValidator->isValid($state['success'])) {
|
||||
throw new Exception(Exception::PROJECT_INVALID_SUCCESS_URL);
|
||||
}
|
||||
|
||||
@@ -14,6 +14,7 @@ use Appwrite\SDK\Deprecated;
|
||||
use Appwrite\SDK\Method;
|
||||
use Appwrite\SDK\Response as SDKResponse;
|
||||
use Appwrite\Template\Template;
|
||||
use Appwrite\Utopia\Database\Validator\CustomId;
|
||||
use Appwrite\Utopia\Response;
|
||||
use PHPMailer\PHPMailer\PHPMailer;
|
||||
use Utopia\Config\Config;
|
||||
@@ -1094,12 +1095,15 @@ Http::post('/v1/projects/:projectId/keys')
|
||||
]
|
||||
))
|
||||
->param('projectId', '', new UID(), 'Project unique ID.')
|
||||
// TODO: When migrating to Platform API, mark keyId required for consistency
|
||||
->param('keyId', 'unique()', new CustomId(), 'Key ID. Choose a custom ID or generate a random ID with `ID.unique()`. Valid chars are a-z, A-Z, 0-9, period, hyphen, and underscore. Can\'t start with a special char. Max length is 36 chars.', true)
|
||||
->param('name', null, new Text(128), 'Key name. Max length: 128 chars.')
|
||||
->param('scopes', null, new Nullable(new ArrayList(new WhiteList(array_keys(Config::getParam('projectScopes')), true), APP_LIMIT_ARRAY_PARAMS_SIZE)), 'Key scopes list. Maximum of ' . APP_LIMIT_ARRAY_PARAMS_SIZE . ' scopes are allowed.')
|
||||
->param('expire', null, new Nullable(new DatetimeValidator()), 'Expiration time in [ISO 8601](https://www.iso.org/iso-8601-date-and-time-format.html) format. Use null for unlimited expiration.', true)
|
||||
->inject('response')
|
||||
->inject('dbForPlatform')
|
||||
->action(function (string $projectId, string $name, array $scopes, ?string $expire, Response $response, Database $dbForPlatform) {
|
||||
->action(function (string $projectId, string $keyId, string $name, array $scopes, ?string $expire, Response $response, Database $dbForPlatform) {
|
||||
$keyId = $keyId == 'unique()' ? ID::unique() : $keyId;
|
||||
|
||||
$project = $dbForPlatform->getDocument('projects', $projectId);
|
||||
|
||||
@@ -1108,7 +1112,7 @@ Http::post('/v1/projects/:projectId/keys')
|
||||
}
|
||||
|
||||
$key = new Document([
|
||||
'$id' => ID::unique(),
|
||||
'$id' => $keyId,
|
||||
'$permissions' => [
|
||||
Permission::read(Role::any()),
|
||||
Permission::update(Role::any()),
|
||||
|
||||
@@ -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'])
|
||||
|
||||
+20
-4
@@ -198,15 +198,26 @@ Http::setResource('allowedHostnames', function (array $platform, Document $proje
|
||||
}
|
||||
|
||||
$originHostname = parse_url($request->getOrigin(), PHP_URL_HOST);
|
||||
$refererHostname = parse_url($request->getReferer(), PHP_URL_HOST);
|
||||
|
||||
$hostname = $originHostname;
|
||||
if (empty($hostname)) {
|
||||
$hostname = $refererHostname;
|
||||
}
|
||||
|
||||
/* Add request hostname for preflight requests */
|
||||
if ($request->getMethod() === 'OPTIONS') {
|
||||
$allowed[] = $originHostname;
|
||||
$allowed[] = $hostname;
|
||||
}
|
||||
|
||||
/* Allow the request origin if a dev key or rule is found */
|
||||
if ((!$rule->isEmpty() || !$devKey->isEmpty()) && !empty($originHostname)) {
|
||||
$allowed[] = $originHostname;
|
||||
/* Allow the request origin of rule */
|
||||
if (!$rule->isEmpty() && !empty($rule->getAttribute('domain', ''))) {
|
||||
$allowed[] = $rule->getAttribute('domain', '');
|
||||
}
|
||||
|
||||
/* Allow the request origin if a dev key is found */
|
||||
if (!$devKey->isEmpty() && !empty($hostname)) {
|
||||
$allowed[] = $hostname;
|
||||
}
|
||||
|
||||
return array_unique($allowed);
|
||||
@@ -237,6 +248,11 @@ Http::setResource('allowedSchemes', function (Document $project) {
|
||||
*/
|
||||
Http::setResource('rule', function (Request $request, Database $dbForPlatform, Document $project, Authorization $authorization) {
|
||||
$domain = \parse_url($request->getOrigin(), PHP_URL_HOST);
|
||||
|
||||
if (empty($domain)) {
|
||||
$domain = \parse_url($request->getReferer(), PHP_URL_HOST);
|
||||
}
|
||||
|
||||
if (empty($domain)) {
|
||||
return new Document();
|
||||
}
|
||||
|
||||
Generated
+36
-53
@@ -4,7 +4,7 @@
|
||||
"Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies",
|
||||
"This file is @generated automatically"
|
||||
],
|
||||
"content-hash": "de8ef00a3aaec540c28f7b1aeb98d294",
|
||||
"content-hash": "917f9050c673379c91e46814c7c07f64",
|
||||
"packages": [
|
||||
{
|
||||
"name": "adhocore/jwt",
|
||||
@@ -3795,16 +3795,16 @@
|
||||
},
|
||||
{
|
||||
"name": "utopia-php/database",
|
||||
"version": "dev-joins8",
|
||||
"version": "5.0.2",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/utopia-php/database.git",
|
||||
"reference": "5564abe0598cc1c388eee615a2af41bd95c4c90a"
|
||||
"reference": "aa80f86f5bf3f0d8c13abd3213bf1649f542d366"
|
||||
},
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/utopia-php/database/zipball/5564abe0598cc1c388eee615a2af41bd95c4c90a",
|
||||
"reference": "5564abe0598cc1c388eee615a2af41bd95c4c90a",
|
||||
"url": "https://api.github.com/repos/utopia-php/database/zipball/aa80f86f5bf3f0d8c13abd3213bf1649f542d366",
|
||||
"reference": "aa80f86f5bf3f0d8c13abd3213bf1649f542d366",
|
||||
"shasum": ""
|
||||
},
|
||||
"require": {
|
||||
@@ -3847,9 +3847,9 @@
|
||||
],
|
||||
"support": {
|
||||
"issues": "https://github.com/utopia-php/database/issues",
|
||||
"source": "https://github.com/utopia-php/database/tree/joins8"
|
||||
"source": "https://github.com/utopia-php/database/tree/5.0.2"
|
||||
},
|
||||
"time": "2026-02-10T10:01:39+00:00"
|
||||
"time": "2026-02-08T05:23:42+00:00"
|
||||
},
|
||||
{
|
||||
"name": "utopia-php/detector",
|
||||
@@ -4112,20 +4112,21 @@
|
||||
},
|
||||
{
|
||||
"name": "utopia-php/emails",
|
||||
"version": "0.6.8",
|
||||
"version": "0.6.6",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/utopia-php/emails.git",
|
||||
"reference": "25dfcd46ed47b862d2a7e7c98d92a3a4680b6f1b"
|
||||
"reference": "354f7fe591e1fba7736afada558cb3b02ec03fea"
|
||||
},
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/utopia-php/emails/zipball/25dfcd46ed47b862d2a7e7c98d92a3a4680b6f1b",
|
||||
"reference": "25dfcd46ed47b862d2a7e7c98d92a3a4680b6f1b",
|
||||
"url": "https://api.github.com/repos/utopia-php/emails/zipball/354f7fe591e1fba7736afada558cb3b02ec03fea",
|
||||
"reference": "354f7fe591e1fba7736afada558cb3b02ec03fea",
|
||||
"shasum": ""
|
||||
},
|
||||
"require": {
|
||||
"php": ">=8.0",
|
||||
"utopia-php/cli": "^0.15",
|
||||
"utopia-php/domains": "^1.0",
|
||||
"utopia-php/fetch": "^0.5",
|
||||
"utopia-php/validators": "0.*"
|
||||
@@ -4133,9 +4134,7 @@
|
||||
"require-dev": {
|
||||
"laravel/pint": "1.25.*",
|
||||
"phpstan/phpstan": "^1.10",
|
||||
"phpunit/phpunit": "^9.3",
|
||||
"utopia-php/cli": "^0.22",
|
||||
"utopia-php/console": "0.*"
|
||||
"phpunit/phpunit": "^9.3"
|
||||
},
|
||||
"type": "library",
|
||||
"autoload": {
|
||||
@@ -4167,9 +4166,9 @@
|
||||
],
|
||||
"support": {
|
||||
"issues": "https://github.com/utopia-php/emails/issues",
|
||||
"source": "https://github.com/utopia-php/emails/tree/0.6.8"
|
||||
"source": "https://github.com/utopia-php/emails/tree/0.6.6"
|
||||
},
|
||||
"time": "2026-02-09T12:31:56+00:00"
|
||||
"time": "2026-02-02T10:41:22+00:00"
|
||||
},
|
||||
{
|
||||
"name": "utopia-php/fetch",
|
||||
@@ -4461,16 +4460,16 @@
|
||||
},
|
||||
{
|
||||
"name": "utopia-php/migration",
|
||||
"version": "dev-joins",
|
||||
"version": "1.5.1",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/utopia-php/migration.git",
|
||||
"reference": "d115b61ddde14d4dcd52bfae7ad210273ddffbef"
|
||||
"reference": "b5fe19804b41d5bdd85571e7cdb83a268b6859e2"
|
||||
},
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/utopia-php/migration/zipball/d115b61ddde14d4dcd52bfae7ad210273ddffbef",
|
||||
"reference": "d115b61ddde14d4dcd52bfae7ad210273ddffbef",
|
||||
"url": "https://api.github.com/repos/utopia-php/migration/zipball/b5fe19804b41d5bdd85571e7cdb83a268b6859e2",
|
||||
"reference": "b5fe19804b41d5bdd85571e7cdb83a268b6859e2",
|
||||
"shasum": ""
|
||||
},
|
||||
"require": {
|
||||
@@ -4480,7 +4479,7 @@
|
||||
"halaxa/json-machine": "^1.2",
|
||||
"php": ">=8.1",
|
||||
"utopia-php/console": "0.0.*",
|
||||
"utopia-php/database": "dev-joins8 as 5.0.0",
|
||||
"utopia-php/database": "5.*",
|
||||
"utopia-php/dsn": "0.2.*",
|
||||
"utopia-php/storage": "0.18.*"
|
||||
},
|
||||
@@ -4511,9 +4510,9 @@
|
||||
],
|
||||
"support": {
|
||||
"issues": "https://github.com/utopia-php/migration/issues",
|
||||
"source": "https://github.com/utopia-php/migration/tree/joins"
|
||||
"source": "https://github.com/utopia-php/migration/tree/1.5.1"
|
||||
},
|
||||
"time": "2026-02-08T12:53:41+00:00"
|
||||
"time": "2026-02-05T11:32:03+00:00"
|
||||
},
|
||||
{
|
||||
"name": "utopia-php/mongo",
|
||||
@@ -4904,16 +4903,16 @@
|
||||
},
|
||||
{
|
||||
"name": "utopia-php/servers",
|
||||
"version": "0.2.5",
|
||||
"version": "0.2.4",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/utopia-php/servers.git",
|
||||
"reference": "4770e879a90685af4ba14e7e5d95d0a17c7fdf03"
|
||||
"reference": "8675d32f4315e91cdb7757a829356030029ed4f5"
|
||||
},
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/utopia-php/servers/zipball/4770e879a90685af4ba14e7e5d95d0a17c7fdf03",
|
||||
"reference": "4770e879a90685af4ba14e7e5d95d0a17c7fdf03",
|
||||
"url": "https://api.github.com/repos/utopia-php/servers/zipball/8675d32f4315e91cdb7757a829356030029ed4f5",
|
||||
"reference": "8675d32f4315e91cdb7757a829356030029ed4f5",
|
||||
"shasum": ""
|
||||
},
|
||||
"require": {
|
||||
@@ -4952,9 +4951,9 @@
|
||||
],
|
||||
"support": {
|
||||
"issues": "https://github.com/utopia-php/servers/issues",
|
||||
"source": "https://github.com/utopia-php/servers/tree/0.2.5"
|
||||
"source": "https://github.com/utopia-php/servers/tree/0.2.4"
|
||||
},
|
||||
"time": "2026-02-10T04:21:53+00:00"
|
||||
"time": "2026-02-04T07:15:26+00:00"
|
||||
},
|
||||
{
|
||||
"name": "utopia-php/span",
|
||||
@@ -5489,16 +5488,16 @@
|
||||
"packages-dev": [
|
||||
{
|
||||
"name": "appwrite/sdk-generator",
|
||||
"version": "1.8.27",
|
||||
"version": "1.8.26",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/appwrite/sdk-generator.git",
|
||||
"reference": "9c6927336a163954c0ed6d8208af8d5b83761762"
|
||||
"reference": "ce65854069a1af8ef0757650da5848168cca5f02"
|
||||
},
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/appwrite/sdk-generator/zipball/9c6927336a163954c0ed6d8208af8d5b83761762",
|
||||
"reference": "9c6927336a163954c0ed6d8208af8d5b83761762",
|
||||
"url": "https://api.github.com/repos/appwrite/sdk-generator/zipball/ce65854069a1af8ef0757650da5848168cca5f02",
|
||||
"reference": "ce65854069a1af8ef0757650da5848168cca5f02",
|
||||
"shasum": ""
|
||||
},
|
||||
"require": {
|
||||
@@ -5534,9 +5533,9 @@
|
||||
"description": "Appwrite PHP library for generating API SDKs for multiple programming languages and platforms",
|
||||
"support": {
|
||||
"issues": "https://github.com/appwrite/sdk-generator/issues",
|
||||
"source": "https://github.com/appwrite/sdk-generator/tree/1.8.27"
|
||||
"source": "https://github.com/appwrite/sdk-generator/tree/1.8.26"
|
||||
},
|
||||
"time": "2026-02-10T06:10:29+00:00"
|
||||
"time": "2026-02-08T07:41:27+00:00"
|
||||
},
|
||||
{
|
||||
"name": "doctrine/annotations",
|
||||
@@ -8993,25 +8992,9 @@
|
||||
"time": "2024-03-07T20:33:40+00:00"
|
||||
}
|
||||
],
|
||||
"aliases": [
|
||||
{
|
||||
"package": "utopia-php/database",
|
||||
"version": "dev-joins8",
|
||||
"alias": "5.0.0",
|
||||
"alias_normalized": "5.0.0.0"
|
||||
},
|
||||
{
|
||||
"package": "utopia-php/migration",
|
||||
"version": "dev-joins",
|
||||
"alias": "1.5.0",
|
||||
"alias_normalized": "1.5.0.0"
|
||||
}
|
||||
],
|
||||
"aliases": [],
|
||||
"minimum-stability": "stable",
|
||||
"stability-flags": {
|
||||
"utopia-php/database": 20,
|
||||
"utopia-php/migration": 20
|
||||
},
|
||||
"stability-flags": {},
|
||||
"prefer-stable": false,
|
||||
"prefer-lowest": false,
|
||||
"platform": {
|
||||
|
||||
@@ -1,5 +1,9 @@
|
||||
# Change Log
|
||||
|
||||
## 13.3.1
|
||||
|
||||
- Fix generated TS imports to auto-detect ESM vs non-ESM
|
||||
|
||||
## 13.3.0
|
||||
|
||||
- Support type generation for text/varchar/mediumtext/longtext attributes
|
||||
|
||||
@@ -22,6 +22,27 @@ class Origin extends Validator
|
||||
{
|
||||
}
|
||||
|
||||
public function setAllowedHostnames(array $allowedHostnames): self
|
||||
{
|
||||
$this->allowedHostnames = $allowedHostnames;
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function setAllowedSchemes(array $allowedSchemes): self
|
||||
{
|
||||
$this->allowedSchemes = $allowedSchemes;
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function getAllowedHostnames(): array
|
||||
{
|
||||
return $this->allowedHostnames;
|
||||
}
|
||||
|
||||
public function getAllowedSchemes(): array
|
||||
{
|
||||
return $this->allowedSchemes;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if Origin is valid.
|
||||
|
||||
@@ -109,6 +109,12 @@ class Base extends Action
|
||||
} catch (\Throwable $error) {
|
||||
// Ignore; deployment can continue
|
||||
}
|
||||
} else {
|
||||
// Fallback till we have tag support here
|
||||
// Goal is to set providerBranch, so build worker knows what to clone as base
|
||||
// Without this, clone command would be cloning empty branch, and failing
|
||||
$providerBranch = $function->getAttribute('providerBranch', 'main');
|
||||
$branchUrl = "https://github.com/$owner/$repositoryName/tree/$providerBranch";
|
||||
}
|
||||
|
||||
$repositoryUrl = "https://github.com/$owner/$repositoryName";
|
||||
@@ -199,6 +205,12 @@ class Base extends Action
|
||||
} catch (\Throwable $error) {
|
||||
// Ignore; deployment can continue
|
||||
}
|
||||
} else {
|
||||
// Fallback till we have tag support here
|
||||
// Goal is to set providerBranch, so build worker knows what to clone as base
|
||||
// Without this, clone command would be cloning empty branch, and failing
|
||||
$providerBranch = $site->getAttribute('providerBranch', 'main');
|
||||
$branchUrl = "https://github.com/$owner/$repositoryName/tree/$providerBranch";
|
||||
}
|
||||
|
||||
$repositoryUrl = "https://github.com/$owner/$repositoryName";
|
||||
|
||||
@@ -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());
|
||||
|
||||
@@ -177,7 +177,10 @@ class Certificates extends Action
|
||||
Console::success('Domain verification succeeded.');
|
||||
} catch (AppwriteException $err) {
|
||||
Console::warning('Domain verification failed: ' . $err->getMessage());
|
||||
$rule->setAttribute('logs', $err->getMessage());
|
||||
$date = \date('H:i:s');
|
||||
$logs = "\033[90m[{$date}] \033[31mDNS verification failed: \033[0m\n";
|
||||
$logs .= \mb_strcut($err->getMessage(), 0, 500000); // Limit to 500kb
|
||||
$rule->setAttribute('logs', $logs);
|
||||
} finally {
|
||||
// Update rule and emit events
|
||||
$this->updateRuleAndSendEvents($rule, $dbForPlatform, $queueForEvents, $queueForWebhooks, $queueForFunctions, $queueForRealtime);
|
||||
@@ -474,11 +477,20 @@ class Certificates extends Action
|
||||
{
|
||||
$mainDomain = $validationDomain ?? $this->getMainDomain();
|
||||
$isMainDomain = !isset($mainDomain) || $domain->get() === $mainDomain;
|
||||
if (!$isMainDomain) {
|
||||
$this->verifyRule($rule, $log);
|
||||
} else {
|
||||
|
||||
if ($isMainDomain) {
|
||||
// Main domain validation
|
||||
// TODO: Would be awesome to check A/AAAA record here. Maybe dry run?
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
$this->verifyRule($rule, $log);
|
||||
} catch (AppwriteException $err) {
|
||||
$msg = $err->getMessage() . "\n";
|
||||
$msg .= "Verify your DNS records are correctly configured and try again.\n";
|
||||
$msg .= "If they're correct and it still fails, please retry after sometime. DNS records can take up to 48 hours to propagate.\n";
|
||||
throw new AppwriteException($err->getType(), $msg);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -60,6 +60,7 @@ trait ProjectCustom
|
||||
'cookie' => 'a_session_console=' . $this->getRoot()['session'],
|
||||
'x-appwrite-project' => 'console',
|
||||
], [
|
||||
'keyId' => ID::unique(),
|
||||
'name' => 'Demo Project Key',
|
||||
'scopes' => [
|
||||
'users.read',
|
||||
@@ -194,6 +195,7 @@ trait ProjectCustom
|
||||
'cookie' => 'a_session_console=' . $this->getRoot()['session'],
|
||||
'x-appwrite-project' => 'console',
|
||||
], [
|
||||
'keyId' => ID::unique(),
|
||||
'name' => 'Demo Project Key',
|
||||
'scopes' => $scopes,
|
||||
]);
|
||||
|
||||
@@ -2825,6 +2825,7 @@ class ProjectsConsoleClientTest extends Scope
|
||||
'x-appwrite-project' => $this->getProject()['$id'],
|
||||
'cookie' => 'a_session_console=' . $this->getRoot()['session'],
|
||||
]), [
|
||||
'keyId' => ID::unique(),
|
||||
'name' => 'Key Test',
|
||||
'scopes' => ['functions.read', 'teams.write'],
|
||||
]);
|
||||
@@ -3174,6 +3175,7 @@ class ProjectsConsoleClientTest extends Scope
|
||||
'content-type' => 'application/json',
|
||||
'x-appwrite-project' => $this->getProject()['$id'],
|
||||
], $this->getHeaders()), [
|
||||
'keyId' => ID::unique(),
|
||||
'name' => 'Key Test',
|
||||
'scopes' => ['teams.read', 'teams.write'],
|
||||
]);
|
||||
@@ -3189,6 +3191,52 @@ class ProjectsConsoleClientTest extends Scope
|
||||
$this->assertArrayHasKey('accessedAt', $response['body']);
|
||||
$this->assertEmpty($response['body']['accessedAt']);
|
||||
|
||||
/**
|
||||
* Test for SUCCESS without key ID
|
||||
*/
|
||||
$response = $this->client->call(Client::METHOD_POST, '/projects/' . $id . '/keys', array_merge([
|
||||
'content-type' => 'application/json',
|
||||
'x-appwrite-project' => $this->getProject()['$id'],
|
||||
], $this->getHeaders()), [
|
||||
'name' => 'Key Custom',
|
||||
'scopes' => ['teams.read', 'teams.write'],
|
||||
]);
|
||||
|
||||
$this->assertEquals(201, $response['headers']['status-code']);
|
||||
$this->assertNotEmpty($response['body']['$id']);
|
||||
|
||||
/**
|
||||
* Test for SUCCESS with custom ID
|
||||
*/
|
||||
$customKeyId = 'key-with-custom-id';
|
||||
$response = $this->client->call(Client::METHOD_POST, '/projects/' . $id . '/keys', array_merge([
|
||||
'content-type' => 'application/json',
|
||||
'x-appwrite-project' => $this->getProject()['$id'],
|
||||
], $this->getHeaders()), [
|
||||
'keyId' => $customKeyId,
|
||||
'name' => 'Key Custom',
|
||||
'scopes' => ['teams.read', 'teams.write'],
|
||||
]);
|
||||
|
||||
$this->assertEquals(201, $response['headers']['status-code']);
|
||||
$this->assertSame($customKeyId, $response['body']['$id']);
|
||||
|
||||
/**
|
||||
* Test for SUCCESS with magic string ID
|
||||
*/
|
||||
$response = $this->client->call(Client::METHOD_POST, '/projects/' . $id . '/keys', array_merge([
|
||||
'content-type' => 'application/json',
|
||||
'x-appwrite-project' => $this->getProject()['$id'],
|
||||
], $this->getHeaders()), [
|
||||
'keyId' => 'unique()',
|
||||
'name' => 'Key Custom',
|
||||
'scopes' => ['teams.read', 'teams.write'],
|
||||
]);
|
||||
|
||||
$this->assertEquals(201, $response['headers']['status-code']);
|
||||
$this->assertNotEmpty($response['body']['$id']);
|
||||
$this->assertNotSame('unique()', $response['body']['$id']);
|
||||
|
||||
$data = array_merge($data, [
|
||||
'keyId' => $response['body']['$id'],
|
||||
'secret' => $response['body']['secret']
|
||||
@@ -3201,6 +3249,7 @@ class ProjectsConsoleClientTest extends Scope
|
||||
'content-type' => 'application/json',
|
||||
'x-appwrite-project' => $this->getProject()['$id'],
|
||||
], $this->getHeaders()), [
|
||||
'keyId' => ID::unique(),
|
||||
'name' => 'Key Test',
|
||||
'scopes' => ['unknown'],
|
||||
]);
|
||||
@@ -3225,7 +3274,7 @@ class ProjectsConsoleClientTest extends Scope
|
||||
|
||||
|
||||
$this->assertEquals(200, $response['headers']['status-code']);
|
||||
$this->assertEquals(1, $response['body']['total']);
|
||||
$this->assertEquals(4, $response['body']['total']);
|
||||
|
||||
/**
|
||||
* Test for FAILURE
|
||||
@@ -3251,7 +3300,7 @@ class ProjectsConsoleClientTest extends Scope
|
||||
$this->assertEquals(200, $response['headers']['status-code']);
|
||||
$this->assertNotEmpty($response['body']['$id']);
|
||||
$this->assertEquals($keyId, $response['body']['$id']);
|
||||
$this->assertEquals('Key Test', $response['body']['name']);
|
||||
$this->assertEquals('Key Custom', $response['body']['name']);
|
||||
$this->assertContains('teams.read', $response['body']['scopes']);
|
||||
$this->assertContains('teams.write', $response['body']['scopes']);
|
||||
$this->assertCount(2, $response['body']['scopes']);
|
||||
@@ -3291,6 +3340,7 @@ class ProjectsConsoleClientTest extends Scope
|
||||
'content-type' => 'application/json',
|
||||
'x-appwrite-project' => $this->getProject()['$id'],
|
||||
], $this->getHeaders()), [
|
||||
'keyId' => ID::unique(),
|
||||
'name' => 'Key Test',
|
||||
'scopes' => ['users.write'],
|
||||
'expire' => DateTime::addSeconds(new \DateTime(), 3600),
|
||||
@@ -3311,6 +3361,7 @@ class ProjectsConsoleClientTest extends Scope
|
||||
'content-type' => 'application/json',
|
||||
'x-appwrite-project' => $this->getProject()['$id'],
|
||||
], $this->getHeaders()), [
|
||||
'keyId' => ID::unique(),
|
||||
'name' => 'Key Test',
|
||||
'scopes' => ['health.read'],
|
||||
'expire' => null,
|
||||
@@ -3333,6 +3384,7 @@ class ProjectsConsoleClientTest extends Scope
|
||||
'content-type' => 'application/json',
|
||||
'x-appwrite-project' => $this->getProject()['$id'],
|
||||
], $this->getHeaders()), [
|
||||
'keyId' => ID::unique(),
|
||||
'name' => 'Key Test',
|
||||
'scopes' => ['health.read'],
|
||||
'expire' => DateTime::addSeconds(new \DateTime(), -3600),
|
||||
@@ -3374,6 +3426,7 @@ class ProjectsConsoleClientTest extends Scope
|
||||
'content-type' => 'application/json',
|
||||
'x-appwrite-project' => $this->getProject()['$id'],
|
||||
], $this->getHeaders()), [
|
||||
'keyId' => ID::unique(),
|
||||
'name' => 'Key Test',
|
||||
'scopes' => ['teams.read'],
|
||||
'expire' => DateTime::addSeconds(new \DateTime(), 3600),
|
||||
@@ -3406,6 +3459,7 @@ class ProjectsConsoleClientTest extends Scope
|
||||
'content-type' => 'application/json',
|
||||
'x-appwrite-project' => $this->getProject()['$id'],
|
||||
], $this->getHeaders()), [
|
||||
'keyId' => ID::unique(),
|
||||
'name' => 'Key Test',
|
||||
'scopes' => ['health.read'],
|
||||
'expire' => DateTime::addSeconds(new \DateTime(), 3600),
|
||||
@@ -4415,6 +4469,7 @@ class ProjectsConsoleClientTest extends Scope
|
||||
'content-type' => 'application/json',
|
||||
'x-appwrite-project' => $this->getProject()['$id'],
|
||||
], $this->getHeaders()), [
|
||||
'keyId' => ID::unique(),
|
||||
'name' => 'Key Test',
|
||||
'scopes' => ['users.read', 'users.write'],
|
||||
]);
|
||||
@@ -4435,6 +4490,7 @@ class ProjectsConsoleClientTest extends Scope
|
||||
'content-type' => 'application/json',
|
||||
'x-appwrite-project' => $this->getProject()['$id'],
|
||||
], $this->getHeaders()), [
|
||||
'keyId' => ID::unique(),
|
||||
'name' => 'Key Test',
|
||||
'scopes' => ['users.read', 'users.write'],
|
||||
]);
|
||||
@@ -5143,6 +5199,31 @@ class ProjectsConsoleClientTest extends Scope
|
||||
]);
|
||||
$this->assertEquals(200, $response['headers']['status-code']);
|
||||
|
||||
/** Ensure any hostname is allowed */
|
||||
$response = $this->client->call(Client::METHOD_GET, '/account/sessions/oauth2/' . $provider, [
|
||||
'content-type' => 'application/json',
|
||||
'x-appwrite-project' => $projectId,
|
||||
'x-appwrite-dev-key' => $devKey['secret'],
|
||||
'origin' => '',
|
||||
'referer' => 'https://domain-without-rule.com'
|
||||
], [
|
||||
'success' => 'https://domain-without-rule.com',
|
||||
'failure' => 'https://domain-without-rule.com'
|
||||
], followRedirects: false);
|
||||
$this->assertEquals(301, $response['headers']['status-code']);
|
||||
|
||||
$response = $this->client->call(Client::METHOD_GET, '/account/sessions/oauth2/' . $provider, [
|
||||
'content-type' => 'application/json',
|
||||
'x-appwrite-project' => $projectId,
|
||||
'x-appwrite-dev-key' => $devKey['secret'],
|
||||
'referer' => '',
|
||||
'origin' => 'https://domain-without-rule.com'
|
||||
], [
|
||||
'success' => 'https://domain-without-rule.com',
|
||||
'failure' => 'https://domain-without-rule.com'
|
||||
], followRedirects: false);
|
||||
$this->assertEquals(301, $response['headers']['status-code']);
|
||||
|
||||
/** Test hostname in Magic URL */
|
||||
$response = $this->client->call(Client::METHOD_POST, '/account/sessions/magic-url', [
|
||||
'content-type' => 'application/json',
|
||||
@@ -5167,6 +5248,131 @@ class ProjectsConsoleClientTest extends Scope
|
||||
$this->assertEquals(201, $response['headers']['status-code']);
|
||||
}
|
||||
|
||||
public function testRuleOAuthRedirect(): void
|
||||
{
|
||||
// Prepare project
|
||||
$projectId = $this->setupProject([
|
||||
'projectId' => ID::unique(),
|
||||
'name' => 'testRuleOAuthRedirect',
|
||||
'region' => System::getEnv('_APP_REGION', 'default')
|
||||
]);
|
||||
|
||||
$provider = 'mock';
|
||||
$appId = '1';
|
||||
$secret = '123456';
|
||||
|
||||
// Prepare OAuth provider
|
||||
$response = $this->client->call(Client::METHOD_PATCH, '/projects/' . $projectId . '/oauth2', array_merge([
|
||||
'origin' => 'http://localhost',
|
||||
'content-type' => 'application/json',
|
||||
'x-appwrite-project' => $this->getProject()['$id'],
|
||||
], $this->getHeaders()), [
|
||||
'provider' => $provider,
|
||||
'appId' => $appId,
|
||||
'secret' => $secret,
|
||||
'enabled' => true,
|
||||
]);
|
||||
$this->assertEquals(200, $response['headers']['status-code']);
|
||||
|
||||
// Prepare rule. In reality this is site rule, but for testing, API rule is enough, and faster to prepare
|
||||
$domain = \uniqid() . '-with-rule.custom.localhost';
|
||||
$rule = $this->client->call(Client::METHOD_POST, '/proxy/rules/api', array_merge([
|
||||
'content-type' => 'application/json',
|
||||
'x-appwrite-project' => $projectId,
|
||||
'x-appwrite-mode' => 'admin',
|
||||
], $this->getHeaders()), [
|
||||
'domain' => $domain
|
||||
]);
|
||||
|
||||
$this->assertEquals(201, $rule['headers']['status-code']);
|
||||
|
||||
// Ensure unknown domain cannot be redirect URL
|
||||
$response = $this->client->call(Client::METHOD_GET, '/account/sessions/oauth2/' . $provider, [
|
||||
'content-type' => 'application/json',
|
||||
'x-appwrite-project' => $projectId,
|
||||
'referer' => 'https://' . $domain,
|
||||
'origin' => '',
|
||||
], [
|
||||
'success' => 'https://domain-without-rule.com',
|
||||
'failure' => 'https://domain-without-rule.com'
|
||||
], followRedirects: false);
|
||||
$this->assertEquals(400, $response['headers']['status-code']);
|
||||
|
||||
// Also ensure final step blocks unknown redirect URL
|
||||
$response = $this->client->call(Client::METHOD_GET, '/account/sessions/oauth2/' . $provider . '/redirect', [
|
||||
'content-type' => 'application/json',
|
||||
'x-appwrite-project' => $projectId,
|
||||
'origin' => '',
|
||||
'referer' => 'https://mockserver.com',
|
||||
], [
|
||||
'code' => 'any-code',
|
||||
'state' => \json_encode([
|
||||
'success' => 'https://domain-without-rule.com',
|
||||
'failure' => 'https://domain-without-rule.com'
|
||||
]),
|
||||
'error' => '',
|
||||
'error_description' => '',
|
||||
], followRedirects: false);
|
||||
$this->assertEquals(400, $response['headers']['status-code']);
|
||||
$this->assertStringContainsString('project_invalid_success_url', $response['body']);
|
||||
|
||||
// Ensure rule's domain can be redirect URL
|
||||
$response = $this->client->call(Client::METHOD_GET, '/account/sessions/oauth2/' . $provider, [
|
||||
'content-type' => 'application/json',
|
||||
'x-appwrite-project' => $projectId,
|
||||
'referer' => 'https://' . $domain,
|
||||
'origin' => '',
|
||||
], [
|
||||
'success' => 'https://' . $domain,
|
||||
'failure' => 'https://' . $domain
|
||||
], followRedirects: false);
|
||||
$this->assertEquals(301, $response['headers']['status-code']);
|
||||
|
||||
// Also ensure final step allows redirect URL
|
||||
$response = $this->client->call(Client::METHOD_GET, '/account/sessions/oauth2/' . $provider . '/redirect', [
|
||||
'content-type' => 'application/json',
|
||||
'x-appwrite-project' => $projectId,
|
||||
'origin' => '',
|
||||
'referer' => 'https://mockserver.com',
|
||||
], [
|
||||
'code' => 'any-code',
|
||||
'state' => \json_encode([
|
||||
'success' => 'https://' . $domain,
|
||||
'failure' => 'https://' . $domain
|
||||
]),
|
||||
'error' => '',
|
||||
'error_deescription' => '',
|
||||
], followRedirects: false);
|
||||
$this->assertEquals(301, $response['headers']['status-code']);
|
||||
$this->assertStringContainsString('https://' . $domain, $response['headers']['location']);
|
||||
|
||||
// Ensure unknown domain cannot be redirect URL
|
||||
$response = $this->client->call(Client::METHOD_POST, '/account/sessions/magic-url', [
|
||||
'content-type' => 'application/json',
|
||||
'x-appwrite-project' => $projectId,
|
||||
'referer' => 'https://' . $domain,
|
||||
'origin' => '',
|
||||
], [
|
||||
'userId' => ID::unique(),
|
||||
'email' => 'user@appwrite.io',
|
||||
'url' => 'https://domain-without-rule.com',
|
||||
]);
|
||||
$this->assertEquals(400, $response['headers']['status-code']);
|
||||
|
||||
// Ensure rule's domain can be redirect URL
|
||||
$response = $this->client->call(Client::METHOD_POST, '/account/sessions/magic-url', [
|
||||
'content-type' => 'application/json',
|
||||
'x-appwrite-project' => $projectId,
|
||||
'referer' => 'https://' . $domain,
|
||||
'origin' => '',
|
||||
], [
|
||||
'userId' => ID::unique(),
|
||||
'email' => 'user@appwrite.io',
|
||||
'url' => 'https://' . $domain,
|
||||
]);
|
||||
$this->assertEquals(201, $response['headers']['status-code']);
|
||||
}
|
||||
|
||||
/**
|
||||
* @group abuseEnabled
|
||||
*/
|
||||
|
||||
@@ -74,4 +74,60 @@ class OriginTest extends TestCase
|
||||
$this->assertEquals(false, $validator->isValid('random-scheme://localhost'));
|
||||
$this->assertEquals('Invalid Scheme. The scheme used (random-scheme) in the Origin (random-scheme://localhost) is not supported. If you are using a custom scheme, please change it to `appwrite-callback-<PROJECT_ID>`', $validator->getDescription());
|
||||
}
|
||||
|
||||
public function testGetAllowedHostnames(): void
|
||||
{
|
||||
$validator = new Origin(
|
||||
allowedHostnames: ['appwrite.io', 'localhost'],
|
||||
allowedSchemes: ['exp']
|
||||
);
|
||||
|
||||
$this->assertEquals(['appwrite.io', 'localhost'], $validator->getAllowedHostnames());
|
||||
}
|
||||
|
||||
public function testGetAllowedSchemes(): void
|
||||
{
|
||||
$validator = new Origin(
|
||||
allowedHostnames: ['appwrite.io'],
|
||||
allowedSchemes: ['exp', 'appwrite-callback-123']
|
||||
);
|
||||
|
||||
$this->assertEquals(['exp', 'appwrite-callback-123'], $validator->getAllowedSchemes());
|
||||
}
|
||||
|
||||
public function testSetAllowedHostnames(): void
|
||||
{
|
||||
$validator = new Origin(
|
||||
allowedHostnames: ['appwrite.io'],
|
||||
allowedSchemes: ['exp']
|
||||
);
|
||||
|
||||
$this->assertEquals(true, $validator->isValid('https://appwrite.io'));
|
||||
$this->assertEquals(false, $validator->isValid('https://example.com'));
|
||||
|
||||
$result = $validator->setAllowedHostnames(['example.com']);
|
||||
|
||||
$this->assertSame($validator, $result);
|
||||
$this->assertEquals(['example.com'], $validator->getAllowedHostnames());
|
||||
$this->assertEquals(true, $validator->isValid('https://example.com'));
|
||||
$this->assertEquals(false, $validator->isValid('https://appwrite.io'));
|
||||
}
|
||||
|
||||
public function testSetAllowedSchemes(): void
|
||||
{
|
||||
$validator = new Origin(
|
||||
allowedHostnames: ['appwrite.io'],
|
||||
allowedSchemes: ['exp']
|
||||
);
|
||||
|
||||
$this->assertEquals(true, $validator->isValid('exp://'));
|
||||
$this->assertEquals(false, $validator->isValid('appwrite-callback-456://'));
|
||||
|
||||
$result = $validator->setAllowedSchemes(['appwrite-callback-456']);
|
||||
|
||||
$this->assertSame($validator, $result);
|
||||
$this->assertEquals(['appwrite-callback-456'], $validator->getAllowedSchemes());
|
||||
$this->assertEquals(true, $validator->isValid('appwrite-callback-456://'));
|
||||
$this->assertEquals(false, $validator->isValid('exp://'));
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user