From b30d9eb8ad904b257ba95df0f05eb8c1694b338f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Matej=20Ba=C4=8Do?= Date: Sat, 15 Feb 2025 01:19:21 +0100 Subject: [PATCH 01/20] Initial screenshot implementation --- app/config/collections/projects.php | 13 ++- app/http.php | 36 +++++++- docker-compose.yml | 8 ++ .../Modules/Functions/Workers/Builds.php | 90 ++++++++++++++++++- 4 files changed, 141 insertions(+), 6 deletions(-) diff --git a/app/config/collections/projects.php b/app/config/collections/projects.php index b31681fd64..9674b1f993 100644 --- a/app/config/collections/projects.php +++ b/app/config/collections/projects.php @@ -1528,7 +1528,18 @@ return [ 'default' => false, 'array' => false, 'filters' => [], - ] + ], + [ + '$id' => ID::custom('screenshot'), // File ID from 'screenshots' Console bucket + 'type' => Database::VAR_STRING, + 'format' => '', + 'size' => Database::LENGTH_KEY, + 'signed' => false, + 'required' => false, + 'default' => null, + 'array' => false, + 'filters' => [], + ], ], 'indexes' => [ [ diff --git a/app/http.php b/app/http.php index 608ac2ec12..8562962977 100644 --- a/app/http.php +++ b/app/http.php @@ -13,6 +13,7 @@ use Swoole\Table; use Utopia\App; use Utopia\Audit\Audit; use Utopia\CLI\Console; +use Utopia\Compression\Compression; use Utopia\Config\Config; use Utopia\Database\Database; use Utopia\Database\DateTime; @@ -217,7 +218,7 @@ $http->on(Constant::EVENT_START, function (Server $http) use ($payloadSize, $reg $dbForPlatform->createCollection($key, $attributes, $indexes); } - if ($dbForPlatform->getDocument('buckets', 'default')->isEmpty() && !$dbForPlatform->exists($dbForPlatform->getDatabase(), 'bucket_1')) { + if ($dbForPlatform->getDocument('buckets', 'default')->isEmpty() ) { Console::success('[Setup] - Creating default bucket...'); $dbForPlatform->createDocument('buckets', new Document([ '$id' => ID::custom('default'), @@ -253,6 +254,39 @@ $http->on(Constant::EVENT_START, function (Server $http) use ($payloadSize, $reg $dbForPlatform->createCollection('bucket_' . $bucket->getInternalId(), $attributes, $indexes); } + if ($dbForPlatform->getDocument('buckets', 'screenshots')->isEmpty()) { + Console::success('[Setup] - Creating screenshots bucket...'); + $dbForPlatform->createDocument('buckets', new Document([ + '$id' => ID::custom('screenshots'), + '$collection' => ID::custom('buckets'), + 'name' => 'Screenshots', + 'maximumFileSize' => (int) System::getEnv('_APP_STORAGE_LIMIT', 0), // 10MB + 'allowedFileExtensions' => [ 'png' ], + 'enabled' => true, + 'compression' => Compression::GZIP, + 'encryption' => false, + 'antivirus' => false, + 'fileSecurity' => true, + '$permissions' => [ + Permission::create(Role::any()), + ], + 'search' => 'buckets Screenshots', + ])); + + $bucket = $dbForPlatform->getDocument('buckets', 'screenshots'); + + Console::success('[Setup] - Creating files collection for screenshots bucket...'); + $files = $collections['buckets']['files'] ?? []; + if (empty($files)) { + throw new Exception('Files collection is not configured.'); + } + + $attributes = \array_map(fn ($attribute) => new Document($attribute), $files['attributes']); + $indexes = \array_map(fn (array $index) => new Document($index), $files['indexes']); + + $dbForPlatform->createCollection('bucket_' . $bucket->getInternalId(), $attributes, $indexes); + } + $projectCollections = $collections['projects']; $sharedTables = \explode(',', System::getEnv('_APP_DATABASE_SHARED_TABLES', '')); $sharedTablesV1 = \explode(',', System::getEnv('_APP_DATABASE_SHARED_TABLES_V1', '')); diff --git a/docker-compose.yml b/docker-compose.yml index c827ec2108..277f82aafb 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -880,6 +880,14 @@ services: environment: - _APP_ASSISTANT_OPENAI_API_KEY + appwrite-browser: + container_name: appwrite-browser + image: appwrite/browser:0.1.0 + networks: + - appwrite + environment: + - APPWRITE_BROWSER_SECRET=$_APP_OPENSSL_KEY_V1 + openruntimes-executor: container_name: openruntimes-executor hostname: exc1 diff --git a/src/Appwrite/Platform/Modules/Functions/Workers/Builds.php b/src/Appwrite/Platform/Modules/Functions/Workers/Builds.php index 809cb96cc2..916c987da5 100644 --- a/src/Appwrite/Platform/Modules/Functions/Workers/Builds.php +++ b/src/Appwrite/Platform/Modules/Functions/Workers/Builds.php @@ -7,11 +7,15 @@ use Appwrite\Event\Event; use Appwrite\Event\Func; use Appwrite\Event\Usage; use Appwrite\Messaging\Adapter\Realtime; +use Appwrite\Permission; +use Appwrite\Role; use Appwrite\Utopia\Response\Model\Deployment; use Appwrite\Vcs\Comment; use Exception; use Executor\Executor; use Swoole\Coroutine as Co; +use Tests\E2E\Client; +use Utopia\App; use Utopia\Cache\Cache; use Utopia\CLI\Console; use Utopia\Config\Config; @@ -24,9 +28,11 @@ use Utopia\Database\Exception\Structure; use Utopia\Database\Helpers\ID; use Utopia\Database\Query; use Utopia\Database\Validator\Authorization; +use Utopia\Fetch\Client as FetchClient; use Utopia\Logger\Log; use Utopia\Platform\Action; use Utopia\Queue\Message; +use Utopia\Storage\Compression\Compression; use Utopia\Storage\Device; use Utopia\Storage\Device\Local; use Utopia\System\System; @@ -55,8 +61,9 @@ class Builds extends Action ->inject('cache') ->inject('dbForProject') ->inject('deviceForFunctions') + ->inject('deviceForFiles') ->inject('log') - ->callback(fn ($message, Document $project, Database $dbForPlatform, Event $queueForEvents, Func $queueForFunctions, Usage $usage, Cache $cache, Database $dbForProject, Device $deviceForFunctions, Log $log) => $this->action($message, $project, $dbForPlatform, $queueForEvents, $queueForFunctions, $usage, $cache, $dbForProject, $deviceForFunctions, $log)); + ->callback(fn ($message, Document $project, Database $dbForPlatform, Event $queueForEvents, Func $queueForFunctions, Usage $usage, Cache $cache, Database $dbForProject, Device $deviceForFunctions, Device $deviceForFiles, Log $log) => $this->action($message, $project, $dbForPlatform, $queueForEvents, $queueForFunctions, $usage, $cache, $dbForProject, $deviceForFunctions, $deviceForFiles, $log)); } /** @@ -69,11 +76,12 @@ class Builds extends Action * @param Cache $cache * @param Database $dbForProject * @param Device $deviceForFunctions + * @param Device $deviceForFiles * @param Log $log * @return void * @throws \Utopia\Database\Exception */ - public function action(Message $message, Document $project, Database $dbForPlatform, Event $queueForEvents, Func $queueForFunctions, Usage $queueForUsage, Cache $cache, Database $dbForProject, Device $deviceForFunctions, Log $log): void + public function action(Message $message, Document $project, Database $dbForPlatform, Event $queueForEvents, Func $queueForFunctions, Usage $queueForUsage, Cache $cache, Database $dbForProject, Device $deviceForFunctions, Device $deviceForFiles, Log $log): void { $payload = $message->getPayload() ?? []; @@ -94,7 +102,7 @@ class Builds extends Action case BUILD_TYPE_RETRY: Console::info('Creating build for deployment: ' . $deployment->getId()); $github = new GitHub($cache); - $this->buildDeployment($deviceForFunctions, $queueForFunctions, $queueForEvents, $queueForUsage, $dbForPlatform, $dbForProject, $github, $project, $resource, $deployment, $template, $log); + $this->buildDeployment($deviceForFunctions, $deviceForFiles, $queueForFunctions, $queueForEvents, $queueForUsage, $dbForPlatform, $dbForProject, $github, $project, $resource, $deployment, $template, $log); break; default: @@ -104,6 +112,7 @@ class Builds extends Action /** * @param Device $deviceForFunctions + * @param Device $deviceForFiles * @param Func $queueForFunctions * @param Event $queueForEvents * @param Usage $queueForUsage @@ -120,7 +129,7 @@ class Builds extends Action * * @throws Exception */ - protected function buildDeployment(Device $deviceForFunctions, Func $queueForFunctions, Event $queueForEvents, Usage $queueForUsage, Database $dbForPlatform, Database $dbForProject, GitHub $github, Document $project, Document $resource, Document $deployment, Document $template, Log $log): void + protected function buildDeployment(Device $deviceForFunctions, Device $deviceForFiles, Func $queueForFunctions, Event $queueForEvents, Usage $queueForUsage, Database $dbForPlatform, Database $dbForProject, GitHub $github, Document $project, Document $resource, Document $deployment, Document $template, Log $log): void { $resourceKey = match($resource->getCollection()) { 'functions' => 'functionId', @@ -700,6 +709,79 @@ class Builds extends Action Console::success("Build id: $buildId created"); + if ($resource->getCollection() === 'sites' && $build->getAttribute('status') === 'ready') { + try { + $rule = Authorization::skip(fn () => $dbForPlatform->findOne('rules', [ + Query::equal("projectInternalId", [$project->getInternalId()]), + Query::equal("resourceType", ["deployment"]), + Query::equal("resourceInternalId", [$deployment->getInternalId()]) + ])); + + if(!$rule->isEmpty()) { + throw new \Exception("Rule for build not found"); + } + + + $client = new FetchClient(); + $client->addHeader('Authorization', 'Bearer ' . App::getEnv('_APP_OPENSSL_KEY_V1', '')); + $response = $client->fetch('http://appwrite-browser/screenshot', query: [ + 'url' => 'http://' . $rule->getAttribute('domain') . '/' + ]); + $screenshot = $response->getBody(); + + $bucket = $dbForPlatform->getDocument('buckets', 'screenshots'); + + // TODO: @Khushboo replace with deviceForSites + $fileId = ID::unique(); + $fileName = $fileId . '.png'; + $path = $deviceForFiles->getPath($fileName); + $path = str_ireplace($deviceForFiles->getRoot(), $deviceForFiles->getRoot() . DIRECTORY_SEPARATOR . $bucket->getId(), $path); // Add bucket id to path after root + $success = $deviceForFiles->write($path, $screenshot, "image/png"); + + if(!$success) { + throw new \Exception("Screenshot failed to save"); + } + + $teamId = $project->getAttribute('teamId', ''); + $file = new Document([ + '$id' => $fileId, + '$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')), + ], + 'bucketId' => $bucket->getId(), + 'bucketInternalId' => $bucket->getInternalId(), + 'name' => $fileName, + 'path' => $path, + 'signature' => $deviceForFiles->getFileHash($path), + 'mimeType' => $deviceForFiles->getFileMimeType($path), + 'sizeOriginal' => \strlen($screenshot), + 'sizeActual' => $deviceForFiles->getFileSize($path), + 'algorithm' => Compression::GZIP, + 'comment' => '', + 'chunksTotal' => 1, + 'chunksUploaded' => 1, + 'openSSLVersion' => null, + 'openSSLCipher' => null, + 'openSSLTag' => null, + 'openSSLIV' => null, + 'search' => implode(' ', [$fileId, $fileName]), + 'metadata' => ['content_type' => $deviceForFiles->getFileMimeType($fileName)], + ]); + $file = $dbForProject->createDocument('bucket_' . $bucket->getInternalId(), $file); + + $deployment->setAttribute('screenshot', $fileId); + $dbForProject->updateDocument('deployments', $deployment->getId(), $deployment); + } catch(\Throwable $th) { + Console::warning("Screenshot failed to generate:"); + Console::warning($th->getMessage()); + Console::warning($th->getTraceAsString()); + } + } + /** Set auto deploy */ if ($deployment->getAttribute('activate') === true) { $resource->setAttribute('deploymentInternalId', $deployment->getInternalId()); From a6bcc3ce97f3e9e3183d0053ed1e1638fa9ba2b3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Matej=20Ba=C4=8Do?= Date: Sat, 15 Feb 2025 01:57:41 +0100 Subject: [PATCH 02/20] Fix bugs --- app/controllers/general.php | 2 +- app/http.php | 3 +++ app/init.php | 3 ++- docker-compose.yml | 1 + .../Modules/Functions/Workers/Builds.php | 18 ++++++++++++------ 5 files changed, 19 insertions(+), 8 deletions(-) diff --git a/app/controllers/general.php b/app/controllers/general.php index c9efc90426..eb9a02f8ca 100644 --- a/app/controllers/general.php +++ b/app/controllers/general.php @@ -585,7 +585,7 @@ App::init() */ App::init() - ->groups(['database', 'functions', 'storage', 'messaging']) + ->groups(['database', 'functions', 'messaging']) ->inject('project') ->inject('request') ->action(function (Document $project, Request $request) { diff --git a/app/http.php b/app/http.php index 8562962977..b8852e9f74 100644 --- a/app/http.php +++ b/app/http.php @@ -269,6 +269,9 @@ $http->on(Constant::EVENT_START, function (Server $http) use ($payloadSize, $reg 'fileSecurity' => true, '$permissions' => [ Permission::create(Role::any()), + Permission::read(Role::any()), + Permission::update(Role::any()), + Permission::delete(Role::any()), ], 'search' => 'buckets Screenshots', ])); diff --git a/app/init.php b/app/init.php index fbffc4b3ad..971986920b 100644 --- a/app/init.php +++ b/app/init.php @@ -1911,8 +1911,9 @@ App::setResource( ); App::setResource('previewHostname', function (Request $request) { + // TODO: @Meldiron Allow in production too for internal communication (authorized with secret) if (App::isDevelopment()) { - $host = $request->getQuery('appwrite-hostname') ?? ''; + $host = $request->getQuery('appwrite-hostname', $request->getHeader('x-appwrite-hostname', '')); if (!empty($host)) { return $host; } diff --git a/docker-compose.yml b/docker-compose.yml index 277f82aafb..a8caddfeae 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -433,6 +433,7 @@ services: volumes: - appwrite-functions:/storage/functions:rw - appwrite-builds:/storage/builds:rw + - appwrite-uploads:/storage/uploads:rw - ./app:/usr/src/code/app - ./src:/usr/src/code/src depends_on: diff --git a/src/Appwrite/Platform/Modules/Functions/Workers/Builds.php b/src/Appwrite/Platform/Modules/Functions/Workers/Builds.php index 916c987da5..809f3f4ce9 100644 --- a/src/Appwrite/Platform/Modules/Functions/Workers/Builds.php +++ b/src/Appwrite/Platform/Modules/Functions/Workers/Builds.php @@ -717,16 +717,22 @@ class Builds extends Action Query::equal("resourceInternalId", [$deployment->getInternalId()]) ])); - if(!$rule->isEmpty()) { + if($rule->isEmpty()) { throw new \Exception("Rule for build not found"); } $client = new FetchClient(); $client->addHeader('Authorization', 'Bearer ' . App::getEnv('_APP_OPENSSL_KEY_V1', '')); - $response = $client->fetch('http://appwrite-browser/screenshot', query: [ - 'url' => 'http://' . $rule->getAttribute('domain') . '/' + $response = $client->fetch('http://appwrite-browser:3000/screenshot', query: [ + 'hostname' => $rule->getAttribute('domain'), + 'path' => '/', ]); + + if($response->getStatusCode() >= 400) { + throw new \Exception("Screenshot failed to generate: " . $response->getBody()); + } + $screenshot = $response->getBody(); $bucket = $dbForPlatform->getDocument('buckets', 'screenshots'); @@ -736,7 +742,7 @@ class Builds extends Action $fileName = $fileId . '.png'; $path = $deviceForFiles->getPath($fileName); $path = str_ireplace($deviceForFiles->getRoot(), $deviceForFiles->getRoot() . DIRECTORY_SEPARATOR . $bucket->getId(), $path); // Add bucket id to path after root - $success = $deviceForFiles->write($path, $screenshot, "image/png"); + $success = $deviceForFiles->write($path, $screenshot, "image/png"); if(!$success) { throw new \Exception("Screenshot failed to save"); @@ -769,9 +775,9 @@ class Builds extends Action 'openSSLTag' => null, 'openSSLIV' => null, 'search' => implode(' ', [$fileId, $fileName]), - 'metadata' => ['content_type' => $deviceForFiles->getFileMimeType($fileName)], + 'metadata' => ['content_type' => $deviceForFiles->getFileMimeType($path)], ]); - $file = $dbForProject->createDocument('bucket_' . $bucket->getInternalId(), $file); + $file = Authorization::skip(fn () => $dbForPlatform->createDocument('bucket_' . $bucket->getInternalId(), $file)); $deployment->setAttribute('screenshot', $fileId); $dbForProject->updateDocument('deployments', $deployment->getId(), $deployment); From b7466429799225fd2f5c9083384cc91b8fc77386 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Matej=20Ba=C4=8Do?= Date: Sat, 15 Feb 2025 02:01:11 +0100 Subject: [PATCH 03/20] Fix missing response --- src/Appwrite/Utopia/Response/Model/Deployment.php | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/src/Appwrite/Utopia/Response/Model/Deployment.php b/src/Appwrite/Utopia/Response/Model/Deployment.php index 0e49c82f82..f5352711c9 100644 --- a/src/Appwrite/Utopia/Response/Model/Deployment.php +++ b/src/Appwrite/Utopia/Response/Model/Deployment.php @@ -76,6 +76,11 @@ class Deployment extends Model 'default' => false, 'example' => true, ]) + ->addRule('screenshot', [ + 'type' => self::TYPE_STRING, + 'description' => 'Screenshot file ID.', + 'default' => '', + ]) ->addRule('status', [ 'type' => self::TYPE_STRING, 'description' => 'The deployment status. Possible values are "processing", "building", "waiting", "ready", and "failed".', From fb2649ab41c278089c8f3374469b51a01ebb3be0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Matej=20Ba=C4=8Do?= Date: Sat, 15 Feb 2025 03:44:40 +0100 Subject: [PATCH 04/20] Add leftover --- src/Appwrite/Utopia/Response/Model/Deployment.php | 1 + 1 file changed, 1 insertion(+) diff --git a/src/Appwrite/Utopia/Response/Model/Deployment.php b/src/Appwrite/Utopia/Response/Model/Deployment.php index f5352711c9..c37f256768 100644 --- a/src/Appwrite/Utopia/Response/Model/Deployment.php +++ b/src/Appwrite/Utopia/Response/Model/Deployment.php @@ -80,6 +80,7 @@ class Deployment extends Model 'type' => self::TYPE_STRING, 'description' => 'Screenshot file ID.', 'default' => '', + 'example' => '5e5ea5c16897e', ]) ->addRule('status', [ 'type' => self::TYPE_STRING, From 81a7ba536c128e3957947633bc57035f8380676f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Matej=20Ba=C4=8Do?= Date: Sat, 15 Feb 2025 04:36:38 +0100 Subject: [PATCH 05/20] Initial branch rule implementation --- app/controllers/api/vcs.php | 28 +++++++++++++++++-- app/controllers/general.php | 2 +- .../Platform/Modules/Compute/Base.php | 2 +- .../Modules/Functions/Workers/Builds.php | 16 +++++++++++ 4 files changed, 43 insertions(+), 5 deletions(-) diff --git a/app/controllers/api/vcs.php b/app/controllers/api/vcs.php index 921a840c0a..72377ecf74 100644 --- a/app/controllers/api/vcs.php +++ b/app/controllers/api/vcs.php @@ -19,6 +19,7 @@ use Utopia\Config\Config; use Utopia\Database\Database; use Utopia\Database\DateTime; use Utopia\Database\Document; +use Utopia\Database\Exception\Duplicate; use Utopia\Database\Exception\Query as QueryException; use Utopia\Database\Helpers\ID; use Utopia\Database\Helpers\Permission; @@ -233,10 +234,8 @@ $createGitDeployments = function (GitHub $github, string $providerInstallationId // Preview deployments for sites if ($resource->getCollection() === 'sites') { - $projectId = $project->getId(); - $sitesDomain = System::getEnv('_APP_DOMAIN_SITES', ''); - $domain = "{$deploymentId}-{$projectId}.{$sitesDomain}"; + $domain = "{$deploymentId}-{$resourceId}.{$sitesDomain}"; $ruleId = md5($domain); $rule = Authorization::skip( @@ -252,6 +251,29 @@ $createGitDeployments = function (GitHub $github, string $providerInstallationId 'certificateId' => '', ])) ); + + // Branch preview + if (!empty($providerBranch)) { + $domain = "git-{$providerBranch}-{$resource->getId()}.{$sitesDomain}"; + $ruleId = md5($domain); + try { + Authorization::skip( + fn () => $dbForPlatform->createDocument('rules', new Document([ + '$id' => $ruleId, + 'projectId' => $project->getId(), + 'projectInternalId' => $project->getInternalId(), + 'domain' => $domain, + 'resourceType' => 'deployment', + 'resourceId' => $deployment->getId(), + 'resourceInternalId' => $deployment->getInternalId(), + 'status' => 'verified', + 'certificateId' => '', + ])) + ); + } catch (Duplicate $err) { + // Ignore, rule already exists; will be updated by builds worker + } + } } if (!empty($providerCommitHash) && $resource->getAttribute('providerSilentMode', false) === false) { diff --git a/app/controllers/general.php b/app/controllers/general.php index eb9a02f8ca..5fb7496af0 100644 --- a/app/controllers/general.php +++ b/app/controllers/general.php @@ -739,7 +739,7 @@ App::init() $refDomainOrigin = $origin; } else { // Auto-allow domains with linked rule - $rule = Authorization::skip(fn () => $dbForPlatform->getDocument('rules', md5($origin))); + $rule = Authorization::skip(fn () => $dbForPlatform->getDocument('rules', md5($origin ?? ''))); if (!$rule->isEmpty() && $rule->getAttribute('projectInternalId') === $project->getInternalId()) { $refDomainOrigin = $origin; } diff --git a/src/Appwrite/Platform/Modules/Compute/Base.php b/src/Appwrite/Platform/Modules/Compute/Base.php index 9ea0ef86c5..89fe070723 100644 --- a/src/Appwrite/Platform/Modules/Compute/Base.php +++ b/src/Appwrite/Platform/Modules/Compute/Base.php @@ -165,7 +165,7 @@ class Base extends Action $projectId = $project->getId(); $sitesDomain = System::getEnv('_APP_DOMAIN_SITES', ''); - $domain = "{$deploymentId}-{$projectId}.{$sitesDomain}"; + $domain = "{$deploymentId}-{$site->getId()}-{$projectId}.{$sitesDomain}"; $ruleId = md5($domain); $rule = Authorization::skip( diff --git a/src/Appwrite/Platform/Modules/Functions/Workers/Builds.php b/src/Appwrite/Platform/Modules/Functions/Workers/Builds.php index 809f3f4ce9..96e647b70b 100644 --- a/src/Appwrite/Platform/Modules/Functions/Workers/Builds.php +++ b/src/Appwrite/Platform/Modules/Functions/Workers/Builds.php @@ -23,6 +23,7 @@ use Utopia\Database\Database; use Utopia\Database\DateTime; use Utopia\Database\Document; use Utopia\Database\Exception\Conflict; +use Utopia\Database\Exception\Duplicate; use Utopia\Database\Exception\Restricted; use Utopia\Database\Exception\Structure; use Utopia\Database\Helpers\ID; @@ -788,6 +789,21 @@ class Builds extends Action } } + // Git branch preview + $providerBranch = $deployment->getAttribute('providerBranch', ''); + if(!empty($providerBranch)) { + $sitesDomain = System::getEnv('_APP_DOMAIN_SITES', ''); + $domain = "git-{$providerBranch}-{$resource->getId()}.{$sitesDomain}"; + $ruleId = md5($domain); + $rule = Authorization::skip(fn () => $dbForPlatform->getDocument('rules', $ruleId)); + if(!$rule->isEmpty()) { + $rule = $rule + ->setAttribute('resourceId', $deployment->getId()) + ->setAttribute('resourceInternalId', $deployment->getInternalId()); + Authorization::skip(fn () => $dbForPlatform->updateDocument('rules', $rule->getId(), $rule)); + } + } + /** Set auto deploy */ if ($deployment->getAttribute('activate') === true) { $resource->setAttribute('deploymentInternalId', $deployment->getInternalId()); From 51e89f51100d25ccf5604259559f15252d60e318 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Matej=20Ba=C4=8Do?= Date: Sat, 15 Feb 2025 04:38:21 +0100 Subject: [PATCH 06/20] Revert "Initial branch rule implementation" This reverts commit 81a7ba536c128e3957947633bc57035f8380676f. --- app/controllers/api/vcs.php | 28 ++----------------- app/controllers/general.php | 2 +- .../Platform/Modules/Compute/Base.php | 2 +- .../Modules/Functions/Workers/Builds.php | 16 ----------- 4 files changed, 5 insertions(+), 43 deletions(-) diff --git a/app/controllers/api/vcs.php b/app/controllers/api/vcs.php index 72377ecf74..921a840c0a 100644 --- a/app/controllers/api/vcs.php +++ b/app/controllers/api/vcs.php @@ -19,7 +19,6 @@ use Utopia\Config\Config; use Utopia\Database\Database; use Utopia\Database\DateTime; use Utopia\Database\Document; -use Utopia\Database\Exception\Duplicate; use Utopia\Database\Exception\Query as QueryException; use Utopia\Database\Helpers\ID; use Utopia\Database\Helpers\Permission; @@ -234,8 +233,10 @@ $createGitDeployments = function (GitHub $github, string $providerInstallationId // Preview deployments for sites if ($resource->getCollection() === 'sites') { + $projectId = $project->getId(); + $sitesDomain = System::getEnv('_APP_DOMAIN_SITES', ''); - $domain = "{$deploymentId}-{$resourceId}.{$sitesDomain}"; + $domain = "{$deploymentId}-{$projectId}.{$sitesDomain}"; $ruleId = md5($domain); $rule = Authorization::skip( @@ -251,29 +252,6 @@ $createGitDeployments = function (GitHub $github, string $providerInstallationId 'certificateId' => '', ])) ); - - // Branch preview - if (!empty($providerBranch)) { - $domain = "git-{$providerBranch}-{$resource->getId()}.{$sitesDomain}"; - $ruleId = md5($domain); - try { - Authorization::skip( - fn () => $dbForPlatform->createDocument('rules', new Document([ - '$id' => $ruleId, - 'projectId' => $project->getId(), - 'projectInternalId' => $project->getInternalId(), - 'domain' => $domain, - 'resourceType' => 'deployment', - 'resourceId' => $deployment->getId(), - 'resourceInternalId' => $deployment->getInternalId(), - 'status' => 'verified', - 'certificateId' => '', - ])) - ); - } catch (Duplicate $err) { - // Ignore, rule already exists; will be updated by builds worker - } - } } if (!empty($providerCommitHash) && $resource->getAttribute('providerSilentMode', false) === false) { diff --git a/app/controllers/general.php b/app/controllers/general.php index 5fb7496af0..eb9a02f8ca 100644 --- a/app/controllers/general.php +++ b/app/controllers/general.php @@ -739,7 +739,7 @@ App::init() $refDomainOrigin = $origin; } else { // Auto-allow domains with linked rule - $rule = Authorization::skip(fn () => $dbForPlatform->getDocument('rules', md5($origin ?? ''))); + $rule = Authorization::skip(fn () => $dbForPlatform->getDocument('rules', md5($origin))); if (!$rule->isEmpty() && $rule->getAttribute('projectInternalId') === $project->getInternalId()) { $refDomainOrigin = $origin; } diff --git a/src/Appwrite/Platform/Modules/Compute/Base.php b/src/Appwrite/Platform/Modules/Compute/Base.php index 89fe070723..9ea0ef86c5 100644 --- a/src/Appwrite/Platform/Modules/Compute/Base.php +++ b/src/Appwrite/Platform/Modules/Compute/Base.php @@ -165,7 +165,7 @@ class Base extends Action $projectId = $project->getId(); $sitesDomain = System::getEnv('_APP_DOMAIN_SITES', ''); - $domain = "{$deploymentId}-{$site->getId()}-{$projectId}.{$sitesDomain}"; + $domain = "{$deploymentId}-{$projectId}.{$sitesDomain}"; $ruleId = md5($domain); $rule = Authorization::skip( diff --git a/src/Appwrite/Platform/Modules/Functions/Workers/Builds.php b/src/Appwrite/Platform/Modules/Functions/Workers/Builds.php index 96e647b70b..809f3f4ce9 100644 --- a/src/Appwrite/Platform/Modules/Functions/Workers/Builds.php +++ b/src/Appwrite/Platform/Modules/Functions/Workers/Builds.php @@ -23,7 +23,6 @@ use Utopia\Database\Database; use Utopia\Database\DateTime; use Utopia\Database\Document; use Utopia\Database\Exception\Conflict; -use Utopia\Database\Exception\Duplicate; use Utopia\Database\Exception\Restricted; use Utopia\Database\Exception\Structure; use Utopia\Database\Helpers\ID; @@ -789,21 +788,6 @@ class Builds extends Action } } - // Git branch preview - $providerBranch = $deployment->getAttribute('providerBranch', ''); - if(!empty($providerBranch)) { - $sitesDomain = System::getEnv('_APP_DOMAIN_SITES', ''); - $domain = "git-{$providerBranch}-{$resource->getId()}.{$sitesDomain}"; - $ruleId = md5($domain); - $rule = Authorization::skip(fn () => $dbForPlatform->getDocument('rules', $ruleId)); - if(!$rule->isEmpty()) { - $rule = $rule - ->setAttribute('resourceId', $deployment->getId()) - ->setAttribute('resourceInternalId', $deployment->getInternalId()); - Authorization::skip(fn () => $dbForPlatform->updateDocument('rules', $rule->getId(), $rule)); - } - } - /** Set auto deploy */ if ($deployment->getAttribute('activate') === true) { $resource->setAttribute('deploymentInternalId', $deployment->getInternalId()); From e1dedd2fbbd9ed6a13d068c06ef117b46a25c737 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Matej=20Ba=C4=8Do?= Date: Sat, 15 Feb 2025 04:41:36 +0100 Subject: [PATCH 07/20] Remove unnessessary check --- src/Appwrite/Platform/Modules/Functions/Workers/Builds.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Appwrite/Platform/Modules/Functions/Workers/Builds.php b/src/Appwrite/Platform/Modules/Functions/Workers/Builds.php index 809f3f4ce9..b3737f2c4f 100644 --- a/src/Appwrite/Platform/Modules/Functions/Workers/Builds.php +++ b/src/Appwrite/Platform/Modules/Functions/Workers/Builds.php @@ -709,7 +709,7 @@ class Builds extends Action Console::success("Build id: $buildId created"); - if ($resource->getCollection() === 'sites' && $build->getAttribute('status') === 'ready') { + if ($resource->getCollection() === 'sites') { try { $rule = Authorization::skip(fn () => $dbForPlatform->findOne('rules', [ Query::equal("projectInternalId", [$project->getInternalId()]), From d1978b102f927b0d78d4f4e9b7930d1bdc7a878c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Matej=20Ba=C4=8Do?= Date: Sat, 15 Feb 2025 22:27:16 +0100 Subject: [PATCH 08/20] Dark screenshot, upgrade screnshot image, fix redeploy missing domain --- app/config/collections/projects.php | 13 +- app/http.php | 2 +- docker-compose.yml | 2 - .../Modules/Functions/Workers/Builds.php | 123 ++++++++++-------- .../Sites/Http/Deployments/Builds/Create.php | 28 +++- .../Utopia/Response/Model/Deployment.php | 6 + 6 files changed, 114 insertions(+), 60 deletions(-) diff --git a/app/config/collections/projects.php b/app/config/collections/projects.php index 9674b1f993..3df48fcd55 100644 --- a/app/config/collections/projects.php +++ b/app/config/collections/projects.php @@ -1533,7 +1533,18 @@ return [ '$id' => ID::custom('screenshot'), // File ID from 'screenshots' Console bucket 'type' => Database::VAR_STRING, 'format' => '', - 'size' => Database::LENGTH_KEY, + 'size' => 32, + 'signed' => false, + 'required' => false, + 'default' => null, + 'array' => false, + 'filters' => [], + ], + [ + '$id' => ID::custom('screenshotDark'), // File ID from 'screenshots' Console bucket + 'type' => Database::VAR_STRING, + 'format' => '', + 'size' => 32, 'signed' => false, 'required' => false, 'default' => null, diff --git a/app/http.php b/app/http.php index b8852e9f74..7e49000734 100644 --- a/app/http.php +++ b/app/http.php @@ -218,7 +218,7 @@ $http->on(Constant::EVENT_START, function (Server $http) use ($payloadSize, $reg $dbForPlatform->createCollection($key, $attributes, $indexes); } - if ($dbForPlatform->getDocument('buckets', 'default')->isEmpty() ) { + if ($dbForPlatform->getDocument('buckets', 'default')->isEmpty()) { Console::success('[Setup] - Creating default bucket...'); $dbForPlatform->createDocument('buckets', new Document([ '$id' => ID::custom('default'), diff --git a/docker-compose.yml b/docker-compose.yml index a8caddfeae..e3b4533c65 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -886,8 +886,6 @@ services: image: appwrite/browser:0.1.0 networks: - appwrite - environment: - - APPWRITE_BROWSER_SECRET=$_APP_OPENSSL_KEY_V1 openruntimes-executor: container_name: openruntimes-executor diff --git a/src/Appwrite/Platform/Modules/Functions/Workers/Builds.php b/src/Appwrite/Platform/Modules/Functions/Workers/Builds.php index b3737f2c4f..bc577a7d08 100644 --- a/src/Appwrite/Platform/Modules/Functions/Workers/Builds.php +++ b/src/Appwrite/Platform/Modules/Functions/Workers/Builds.php @@ -14,7 +14,6 @@ use Appwrite\Vcs\Comment; use Exception; use Executor\Executor; use Swoole\Coroutine as Co; -use Tests\E2E\Client; use Utopia\App; use Utopia\Cache\Cache; use Utopia\CLI\Console; @@ -717,71 +716,85 @@ class Builds extends Action Query::equal("resourceInternalId", [$deployment->getInternalId()]) ])); - if($rule->isEmpty()) { + if ($rule->isEmpty()) { throw new \Exception("Rule for build not found"); } - $client = new FetchClient(); - $client->addHeader('Authorization', 'Bearer ' . App::getEnv('_APP_OPENSSL_KEY_V1', '')); - $response = $client->fetch('http://appwrite-browser:3000/screenshot', query: [ - 'hostname' => $rule->getAttribute('domain'), - 'path' => '/', - ]); - - if($response->getStatusCode() >= 400) { - throw new \Exception("Screenshot failed to generate: " . $response->getBody()); - } - - $screenshot = $response->getBody(); + $client->addHeader('content-type', FetchClient::CONTENT_TYPE_APPLICATION_JSON); $bucket = $dbForPlatform->getDocument('buckets', 'screenshots'); - // TODO: @Khushboo replace with deviceForSites - $fileId = ID::unique(); - $fileName = $fileId . '.png'; - $path = $deviceForFiles->getPath($fileName); - $path = str_ireplace($deviceForFiles->getRoot(), $deviceForFiles->getRoot() . DIRECTORY_SEPARATOR . $bucket->getId(), $path); // Add bucket id to path after root - $success = $deviceForFiles->write($path, $screenshot, "image/png"); + $configs = [ + 'screenshot' => [ + 'headers' => [ 'x-appwrite-hostname' => $rule->getAttribute('domain') ], + 'url' => 'http://traefik/', + 'color' => 'light' + ], + 'screenshotDark' => [ + 'headers' => [ 'x-appwrite-hostname' => $rule->getAttribute('domain') ], + 'url' => 'http://traefik/', + 'color' => 'dark' + ], + ]; - if(!$success) { - throw new \Exception("Screenshot failed to save"); + // TODO: @Meldiron if becomes too slow, do concurrently + foreach ($configs as $key => $config) { + $response = $client->fetch( + url: 'http://appwrite-browser:3000/v1/screenshots', + method: 'POST', + body: $config + ); + + if ($response->getStatusCode() >= 400) { + throw new \Exception("Screenshot failed to generate: " . $response->getBody()); + } + + $screenshot = $response->getBody(); + + // TODO: @Khushboo replace with deviceForSites + $fileId = ID::unique(); + $fileName = $fileId . '.png'; + $path = $deviceForFiles->getPath($fileName); + $path = str_ireplace($deviceForFiles->getRoot(), $deviceForFiles->getRoot() . DIRECTORY_SEPARATOR . $bucket->getId(), $path); // Add bucket id to path after root + $success = $deviceForFiles->write($path, $screenshot, "image/png"); + + if (!$success) { + throw new \Exception("Screenshot failed to save"); + } + + $teamId = $project->getAttribute('teamId', ''); + $file = new Document([ + '$id' => $fileId, + '$permissions' => [ + Permission::read(Role::team(ID::custom($teamId))), + ], + 'bucketId' => $bucket->getId(), + 'bucketInternalId' => $bucket->getInternalId(), + 'name' => $fileName, + 'path' => $path, + 'signature' => $deviceForFiles->getFileHash($path), + 'mimeType' => $deviceForFiles->getFileMimeType($path), + 'sizeOriginal' => \strlen($screenshot), + 'sizeActual' => $deviceForFiles->getFileSize($path), + 'algorithm' => Compression::GZIP, + 'comment' => '', + 'chunksTotal' => 1, + 'chunksUploaded' => 1, + 'openSSLVersion' => null, + 'openSSLCipher' => null, + 'openSSLTag' => null, + 'openSSLIV' => null, + 'search' => implode(' ', [$fileId, $fileName]), + 'metadata' => ['content_type' => $deviceForFiles->getFileMimeType($path)], + ]); + $file = Authorization::skip(fn () => $dbForPlatform->createDocument('bucket_' . $bucket->getInternalId(), $file)); + + $deployment->setAttribute($key, $fileId); } - $teamId = $project->getAttribute('teamId', ''); - $file = new Document([ - '$id' => $fileId, - '$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')), - ], - 'bucketId' => $bucket->getId(), - 'bucketInternalId' => $bucket->getInternalId(), - 'name' => $fileName, - 'path' => $path, - 'signature' => $deviceForFiles->getFileHash($path), - 'mimeType' => $deviceForFiles->getFileMimeType($path), - 'sizeOriginal' => \strlen($screenshot), - 'sizeActual' => $deviceForFiles->getFileSize($path), - 'algorithm' => Compression::GZIP, - 'comment' => '', - 'chunksTotal' => 1, - 'chunksUploaded' => 1, - 'openSSLVersion' => null, - 'openSSLCipher' => null, - 'openSSLTag' => null, - 'openSSLIV' => null, - 'search' => implode(' ', [$fileId, $fileName]), - 'metadata' => ['content_type' => $deviceForFiles->getFileMimeType($path)], - ]); - $file = Authorization::skip(fn () => $dbForPlatform->createDocument('bucket_' . $bucket->getInternalId(), $file)); - - $deployment->setAttribute('screenshot', $fileId); $dbForProject->updateDocument('deployments', $deployment->getId(), $deployment); - } catch(\Throwable $th) { + } catch (\Throwable $th) { Console::warning("Screenshot failed to generate:"); Console::warning($th->getMessage()); Console::warning($th->getTraceAsString()); diff --git a/src/Appwrite/Platform/Modules/Sites/Http/Deployments/Builds/Create.php b/src/Appwrite/Platform/Modules/Sites/Http/Deployments/Builds/Create.php index 0c5cfc34e4..c28a0be4e5 100644 --- a/src/Appwrite/Platform/Modules/Sites/Http/Deployments/Builds/Create.php +++ b/src/Appwrite/Platform/Modules/Sites/Http/Deployments/Builds/Create.php @@ -10,11 +10,14 @@ use Appwrite\SDK\Method; use Appwrite\SDK\Response as SDKResponse; use Appwrite\Utopia\Response; use Utopia\Database\Database; +use Utopia\Database\Document; use Utopia\Database\Helpers\ID; +use Utopia\Database\Validator\Authorization; use Utopia\Database\Validator\UID; use Utopia\Platform\Action; use Utopia\Platform\Scope\HTTP; use Utopia\Storage\Device; +use Utopia\System\System; class Create extends Action { @@ -53,7 +56,9 @@ class Create extends Action ->param('siteId', '', new UID(), 'Site ID.') ->param('deploymentId', '', new UID(), 'Deployment ID.') ->inject('response') + ->inject('project') ->inject('dbForProject') + ->inject('dbForPlatform') ->inject('queueForEvents') ->inject('queueForBuilds') ->inject('deviceForSites') @@ -61,7 +66,7 @@ class Create extends Action ->callback([$this, 'action']); } - public function action(string $siteId, string $deploymentId, Response $response, Database $dbForProject, Event $queueForEvents, Build $queueForBuilds, Device $deviceForSites, Device $deviceForFunctions) + public function action(string $siteId, string $deploymentId, Response $response, Document $project, Database $dbForProject, Database $dbForPlatform, Event $queueForEvents, Build $queueForBuilds, Device $deviceForSites, Device $deviceForFunctions) { $site = $dbForProject->getDocument('sites', $siteId); @@ -97,6 +102,27 @@ class Create extends Action 'search' => implode(' ', [$deploymentId]), ])); + // Preview deployments for sites + $projectId = $project->getId(); + + $sitesDomain = System::getEnv('_APP_DOMAIN_SITES', ''); + $domain = "{$deploymentId}-{$projectId}.{$sitesDomain}"; + $ruleId = md5($domain); + + $rule = Authorization::skip( + fn () => $dbForPlatform->createDocument('rules', new Document([ + '$id' => $ruleId, + 'projectId' => $project->getId(), + 'projectInternalId' => $project->getInternalId(), + 'domain' => $domain, + 'resourceType' => 'deployment', + 'resourceId' => $deploymentId, + 'resourceInternalId' => $deployment->getInternalId(), + 'status' => 'verified', + 'certificateId' => '', + ])) + ); + $queueForBuilds ->setType(BUILD_TYPE_DEPLOYMENT) ->setResource($site) diff --git a/src/Appwrite/Utopia/Response/Model/Deployment.php b/src/Appwrite/Utopia/Response/Model/Deployment.php index c37f256768..3f226d72ea 100644 --- a/src/Appwrite/Utopia/Response/Model/Deployment.php +++ b/src/Appwrite/Utopia/Response/Model/Deployment.php @@ -82,6 +82,12 @@ class Deployment extends Model 'default' => '', 'example' => '5e5ea5c16897e', ]) + ->addRule('screenshotDark', [ + 'type' => self::TYPE_STRING, + 'description' => 'Screenshot with dark theme prefference file ID.', + 'default' => '', + 'example' => '5e5ea5c16897e', + ]) ->addRule('status', [ 'type' => self::TYPE_STRING, 'description' => 'The deployment status. Possible values are "processing", "building", "waiting", "ready", and "failed".', From e5e7c50ca4c7cbaf890c136f550911e5c3381fa5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Matej=20Ba=C4=8Do?= Date: Mon, 17 Feb 2025 11:54:07 +0100 Subject: [PATCH 09/20] previewHostname security --- app/init.php | 37 +++++++++++++++++-- .../Modules/Functions/Workers/Builds.php | 9 +++++ 2 files changed, 42 insertions(+), 4 deletions(-) diff --git a/app/init.php b/app/init.php index 971986920b..55781a89b1 100644 --- a/app/init.php +++ b/app/init.php @@ -1910,9 +1910,38 @@ App::setResource( fn () => fn (Document $project, string $resourceType, ?string $resourceId) => false ); -App::setResource('previewHostname', function (Request $request) { - // TODO: @Meldiron Allow in production too for internal communication (authorized with secret) - if (App::isDevelopment()) { +/** + * JWT key from x-appwrite-key header. + * + * @return array Decoded key-value pair from JWT + */ +App::setResource('dynamicKey', function (Request $request) { + $apiKey = $request->getHeader('x-appwrite-key', ''); + + if (empty($apiKey) || !\str_contains($apiKey, '_')) { + return []; + } + + [ $keyType, $authKey ] = \explode('_', $apiKey, 2); + + if($keyType !== API_KEY_DYNAMIC) { + return []; + } + + $jwtObj = new JWT(System::getEnv('_APP_OPENSSL_KEY_V1'), 'HS256', 86400, 0); + + try { + $payload = $jwtObj->decode($authKey); + } catch (JWTException $error) { + return []; + } + + return $payload; + +}, ['request']); + +App::setResource('previewHostname', function (Request $request, array $dynamicKey) { + if (App::isDevelopment() || $dynamicKey['overrideHostname'] ?? false) { $host = $request->getQuery('appwrite-hostname', $request->getHeader('x-appwrite-hostname', '')); if (!empty($host)) { return $host; @@ -1920,4 +1949,4 @@ App::setResource('previewHostname', function (Request $request) { } return ''; -}, ['request']); +}, ['request', 'dynamicKey']); diff --git a/src/Appwrite/Platform/Modules/Functions/Workers/Builds.php b/src/Appwrite/Platform/Modules/Functions/Workers/Builds.php index bc577a7d08..eb8d31ea59 100644 --- a/src/Appwrite/Platform/Modules/Functions/Workers/Builds.php +++ b/src/Appwrite/Platform/Modules/Functions/Workers/Builds.php @@ -738,8 +738,17 @@ class Builds extends Action ], ]; + $jwtObj = new JWT(System::getEnv('_APP_OPENSSL_KEY_V1'), 'HS256', 900, 0); + $apiKey = $jwtObj->encode([ + 'overrideHostname' => true + ]); + // TODO: @Meldiron if becomes too slow, do concurrently foreach ($configs as $key => $config) { + $config['headers'] = \array_merge($config['headers'] ?? [], [ + 'x-appwrite-key' => API_KEY_DYNAMIC . '_' . $apiKey + ]); + $response = $client->fetch( url: 'http://appwrite-browser:3000/v1/screenshots', method: 'POST', From c2a60a8569d250502cbaeb8e9cc65cdf9314ae9a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Matej=20Ba=C4=8Do?= Date: Mon, 17 Feb 2025 13:03:56 +0100 Subject: [PATCH 10/20] Screenshot deletion in worker --- app/init.php | 4 +- .../Modules/Functions/Workers/Builds.php | 1 - src/Appwrite/Platform/Workers/Deletes.php | 55 ++++++++++++++++++- 3 files changed, 54 insertions(+), 6 deletions(-) diff --git a/app/init.php b/app/init.php index 55781a89b1..b27d72bbc8 100644 --- a/app/init.php +++ b/app/init.php @@ -1912,7 +1912,7 @@ App::setResource( /** * JWT key from x-appwrite-key header. - * + * * @return array Decoded key-value pair from JWT */ App::setResource('dynamicKey', function (Request $request) { @@ -1924,7 +1924,7 @@ App::setResource('dynamicKey', function (Request $request) { [ $keyType, $authKey ] = \explode('_', $apiKey, 2); - if($keyType !== API_KEY_DYNAMIC) { + if ($keyType !== API_KEY_DYNAMIC) { return []; } diff --git a/src/Appwrite/Platform/Modules/Functions/Workers/Builds.php b/src/Appwrite/Platform/Modules/Functions/Workers/Builds.php index eb8d31ea59..44585bb45d 100644 --- a/src/Appwrite/Platform/Modules/Functions/Workers/Builds.php +++ b/src/Appwrite/Platform/Modules/Functions/Workers/Builds.php @@ -14,7 +14,6 @@ use Appwrite\Vcs\Comment; use Exception; use Executor\Executor; use Swoole\Coroutine as Co; -use Utopia\App; use Utopia\Cache\Cache; use Utopia\CLI\Console; use Utopia\Config\Config; diff --git a/src/Appwrite/Platform/Workers/Deletes.php b/src/Appwrite/Platform/Workers/Deletes.php index 09c71dc004..7e663f139e 100644 --- a/src/Appwrite/Platform/Workers/Deletes.php +++ b/src/Appwrite/Platform/Workers/Deletes.php @@ -21,6 +21,7 @@ use Utopia\Database\Exception\Conflict; use Utopia\Database\Exception\Restricted; use Utopia\Database\Exception\Structure; use Utopia\Database\Query; +use Utopia\Database\Validator\Authorization as ValidatorAuthorization; use Utopia\DSN\DSN; use Utopia\Logger\Log; use Utopia\Platform\Action; @@ -91,7 +92,7 @@ class Deletes extends Action $this->deleteProject($dbForPlatform, $getProjectDB, $deviceForFiles, $deviceForSites, $deviceForFunctions, $deviceForBuilds, $deviceForCache, $certificates, $document); break; case DELETE_TYPE_SITES: - $this->deleteSite($dbForPlatform, $getProjectDB, $deviceForSites, $deviceForFunctions, $deviceForBuilds, $document, $certificates, $project); + $this->deleteSite($dbForPlatform, $getProjectDB, $deviceForSites, $deviceForFunctions, $deviceForBuilds, $deviceForFiles, $document, $certificates, $project); break; case DELETE_TYPE_FUNCTIONS: $this->deleteFunction($dbForPlatform, $getProjectDB, $deviceForFunctions, $deviceForBuilds, $certificates, $document, $project); @@ -747,7 +748,7 @@ class Deletes extends Action * @return void * @throws Exception */ - private function deleteSite(Database $dbForPlatform, callable $getProjectDB, Device $deviceForSites, Device $deviceForFunctions, Device $deviceForBuilds, Document $document, CertificatesAdapter $certificates, Document $project): void + private function deleteSite(Database $dbForPlatform, callable $getProjectDB, Device $deviceForSites, Device $deviceForFunctions, Device $deviceForBuilds, Device $deviceForFiles, Document $document, CertificatesAdapter $certificates, Document $project): void { $dbForProject = $getProjectDB($project); $siteId = $document->getId(); @@ -781,9 +782,10 @@ class Deletes extends Action $deploymentInternalIds = []; $this->deleteByGroup('deployments', [ Query::equal('resourceInternalId', [$siteInternalId]) - ], $dbForProject, function (Document $document) use ($deviceForFunctions, &$deploymentInternalIds) { + ], $dbForProject, function (Document $document) use ($deviceForFunctions, $deviceForFiles, $dbForPlatform, &$deploymentInternalIds) { $deploymentInternalIds[] = $document->getInternalId(); $this->deleteDeploymentFiles($deviceForFunctions, $document); + $this->deleteDeploymentScreenshots($deviceForFiles, $dbForPlatform, $document); }); /** @@ -926,6 +928,53 @@ class Deletes extends Action $this->deleteRuntimes($getProjectDB, $document, $project); } + private function deleteDeploymentScreenshots(Device $deviceForFiles, Database $dbForPlatform, Document $deployment): void + { + $screenshotIds = []; + if (!empty($deployment->getAttribute('screenshot', ''))) { + $screenshotIds[] = $deployment->getAttribute('screenshot', ''); + } + if (!empty($deployment->getAttribute('screenshotDark', ''))) { + $screenshotIds[] = $deployment->getAttribute('screenshotDark', ''); + } + + if (empty($screenshotIds)) { + return; + } + Console::info("Deleting screenshots for deployment " . $deployment->getId()); + + $bucket = ValidatorAuthorization::skip(fn () => $dbForPlatform->getDocument('buckets', 'screenshots')); + if ($bucket->isEmpty()) { + Console::error('Failed to get bucket for deployment screenshots'); + return; + } + + foreach ($screenshotIds as $id) { + $file = ValidatorAuthorization::skip(fn () => $dbForPlatform->getDocument('bucket_' . $bucket->getInternalId(), $id)); + + if ($file->isEmpty()) { + Console::error('Failed to get deployment screenshot: ' . $id); + continue; + } + + $path = $file->getAttribute('path', ''); + + try { + if ($deviceForFiles->delete($path, true)) { + Console::success('Deleted deployment screenshot: ' . $path); + } else { + Console::error('Failed to delete deployment screenshot: ' . $path); + } + } catch (\Throwable $th) { + Console::error('Failed to delete deployment screenshot: ' . $path); + Console::error('[Error] Type: ' . get_class($th)); + Console::error('[Error] Message: ' . $th->getMessage()); + Console::error('[Error] File: ' . $th->getFile()); + Console::error('[Error] Line: ' . $th->getLine()); + } + } + } + /** * @param Device $device * @param Document $deployment From 3ccd49e4a5b9c3f87de370cb090d0c264c725f44 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Matej=20Ba=C4=8Do?= Date: Thu, 20 Feb 2025 13:44:22 +0100 Subject: [PATCH 11/20] Rework screenshots to use api key resource from recent 1.6.x merge --- app/http.php | 4 +- app/init.php | 38 ++++--------------- docker-compose.yml | 2 +- src/Appwrite/Auth/Key.php | 11 +++++- .../Modules/Functions/Workers/Builds.php | 3 +- 5 files changed, 22 insertions(+), 36 deletions(-) diff --git a/app/http.php b/app/http.php index b5a214b753..d00c33c217 100644 --- a/app/http.php +++ b/app/http.php @@ -274,7 +274,7 @@ $http->on(Constant::EVENT_START, function (Server $http) use ($payloadSize, $reg ])); $bucket = $dbForPlatform->getDocument('buckets', 'default'); - + Console::info(" └── Creating files collection for default bucket..."); $files = $collections['buckets']['files'] ?? []; if (empty($files)) { @@ -328,7 +328,7 @@ $http->on(Constant::EVENT_START, function (Server $http) use ($payloadSize, $reg ])); $bucket = $dbForPlatform->getDocument('buckets', 'screenshots'); - + Console::info(" └── Creating files collection for screenshots bucket..."); $files = $collections['buckets']['files'] ?? []; if (empty($files)) { diff --git a/app/init.php b/app/init.php index dceb04fc30..585dfc0616 100644 --- a/app/init.php +++ b/app/init.php @@ -1961,38 +1961,16 @@ App::setResource( fn () => fn (Document $project, string $resourceType, ?string $resourceId) => false ); -/** - * JWT key from x-appwrite-key header. - * - * @return array Decoded key-value pair from JWT - */ -App::setResource('dynamicKey', function (Request $request) { - $apiKey = $request->getHeader('x-appwrite-key', ''); +App::setResource('previewHostname', function (Request $request, ?Key $apiKey) { + $allowed = false; - if (empty($apiKey) || !\str_contains($apiKey, '_')) { - return []; + if(App::isDevelopment()) { + $allowed = true; + } else if(!is_null($apiKey) && $apiKey->getHostnameOverride() === true) { + $allowed = true; } - [ $keyType, $authKey ] = \explode('_', $apiKey, 2); - - if ($keyType !== API_KEY_DYNAMIC) { - return []; - } - - $jwtObj = new JWT(System::getEnv('_APP_OPENSSL_KEY_V1'), 'HS256', 86400, 0); - - try { - $payload = $jwtObj->decode($authKey); - } catch (JWTException $error) { - return []; - } - - return $payload; - -}, ['request']); - -App::setResource('previewHostname', function (Request $request, array $dynamicKey) { - if (App::isDevelopment() || $dynamicKey['overrideHostname'] ?? false) { + if($allowed === true) { $host = $request->getQuery('appwrite-hostname', $request->getHeader('x-appwrite-hostname', '')); if (!empty($host)) { return $host; @@ -2000,7 +1978,7 @@ App::setResource('previewHostname', function (Request $request, array $dynamicKe } return ''; -}, ['request', 'dynamicKey']); +}, ['request', 'apiKey']); App::setResource('apiKey', function (Request $request, Document $project): ?Key { $key = $request->getHeader('x-appwrite-key'); diff --git a/docker-compose.yml b/docker-compose.yml index 887ca98f34..04d623df35 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -205,7 +205,7 @@ services: appwrite-console: <<: *x-logging container_name: appwrite-console - image: appwrite/console:5.3.0-sites-rc.9 + image: appwrite/console:5.3.0-sites-rc.10 restart: unless-stopped networks: - appwrite diff --git a/src/Appwrite/Auth/Key.php b/src/Appwrite/Auth/Key.php index 1c40b35f54..2845a879a5 100644 --- a/src/Appwrite/Auth/Key.php +++ b/src/Appwrite/Auth/Key.php @@ -20,6 +20,7 @@ class Key protected string $name, protected bool $expired = false, protected array $disabledMetrics = [], + protected bool $hostnameOverride = false, ) { } @@ -58,6 +59,12 @@ class Key return $this->disabledMetrics; } + + public function getHostnameOverride(): bool + { + return $this->hostnameOverride; + } + /** * Decode the given secret key into a Key object, containing the project ID, type, role, scopes, and name. * Can be a stored API key or a dynamic key (JWT). @@ -109,6 +116,7 @@ class Key $name = $payload['name'] ?? 'Dynamic Key'; $projectId = $payload['projectId'] ?? ''; $disabledMetrics = $payload['disabledMetrics'] ?? []; + $hostnameOverride = $payload['hostnameOverride'] ?? false; $scopes = \array_merge($payload['scopes'] ?? [], $scopes); if ($projectId !== $project->getId()) { @@ -122,7 +130,8 @@ class Key $scopes, $name, $expired, - $disabledMetrics + $disabledMetrics, + $hostnameOverride ); case API_KEY_STANDARD: $key = $project->find( diff --git a/src/Appwrite/Platform/Modules/Functions/Workers/Builds.php b/src/Appwrite/Platform/Modules/Functions/Workers/Builds.php index 274c3aa06b..7501b60aae 100644 --- a/src/Appwrite/Platform/Modules/Functions/Workers/Builds.php +++ b/src/Appwrite/Platform/Modules/Functions/Workers/Builds.php @@ -752,7 +752,7 @@ class Builds extends Action $jwtObj = new JWT(System::getEnv('_APP_OPENSSL_KEY_V1'), 'HS256', 900, 0); $apiKey = $jwtObj->encode([ - 'overrideHostname' => true + 'hostnameOverride' => true ]); // TODO: @Meldiron if becomes too slow, do concurrently @@ -773,7 +773,6 @@ class Builds extends Action $screenshot = $response->getBody(); - // TODO: @Khushboo replace with deviceForSites $fileId = ID::unique(); $fileName = $fileId . '.png'; $path = $deviceForFiles->getPath($fileName); From d941d18e8cbb0b52ef4fde2d332599f428b0c5cd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Matej=20Ba=C4=8Do?= Date: Thu, 20 Feb 2025 13:44:31 +0100 Subject: [PATCH 12/20] WIP: screenshot tests --- .../Services/Sites/SitesCustomServerTest.php | 96 +++++++++++++------ .../resources/sites/static-themed/index.html | 23 +++++ 2 files changed, 91 insertions(+), 28 deletions(-) create mode 100644 tests/resources/sites/static-themed/index.html diff --git a/tests/e2e/Services/Sites/SitesCustomServerTest.php b/tests/e2e/Services/Sites/SitesCustomServerTest.php index eefa0ddcbb..da8d9ebd8f 100644 --- a/tests/e2e/Services/Sites/SitesCustomServerTest.php +++ b/tests/e2e/Services/Sites/SitesCustomServerTest.php @@ -313,10 +313,7 @@ class SitesCustomServerTest extends Scope $proxyClient = new Client(); $proxyClient->setEndpoint('http://' . $domain); - $response = $proxyClient->call(Client::METHOD_GET, '/', array_merge([ - 'content-type' => 'application/json', - 'x-appwrite-project' => $this->getProject()['$id'], - ])); + $response = $proxyClient->call(Client::METHOD_GET, '/'); $this->assertEquals(200, $response['headers']['status-code']); $this->assertStringContainsString("Env variable is Appwrite", $response['body']); @@ -1249,19 +1246,13 @@ class SitesCustomServerTest extends Scope $proxyClient = new Client(); $proxyClient->setEndpoint('http://' . $domain); - $response = $proxyClient->call(Client::METHOD_GET, '/', array_merge([ - 'content-type' => 'application/json', - 'x-appwrite-project' => $this->getProject()['$id'], - ])); + $response = $proxyClient->call(Client::METHOD_GET, '/'); $this->assertEquals(200, $response['headers']['status-code']); $this->assertStringContainsString("Astro Blog", $response['body']); $this->assertStringContainsString("Hello, Astronaut!", $response['body']); - $response = $proxyClient->call(Client::METHOD_GET, '/about', array_merge([ - 'content-type' => 'application/json', - 'x-appwrite-project' => $this->getProject()['$id'], - ])); + $response = $proxyClient->call(Client::METHOD_GET, '/about'); $this->assertEquals(200, $response['headers']['status-code']); $this->assertStringContainsString("Astro Blog", $response['body']); @@ -1300,10 +1291,7 @@ class SitesCustomServerTest extends Scope $proxyClient = new Client(); $proxyClient->setEndpoint('http://' . $domain); - $response = $proxyClient->call(Client::METHOD_GET, '/', array_merge([ - 'content-type' => 'application/json', - 'x-appwrite-project' => $this->getProject()['$id'], - ])); + $response = $proxyClient->call(Client::METHOD_GET, '/'); $this->assertEquals(200, $response['headers']['status-code']); $this->assertStringNotContainsString("This domain is not connected to any Appwrite resource yet", $response['body']); @@ -1350,10 +1338,7 @@ class SitesCustomServerTest extends Scope $this->assertEquals(0, $rules['body']['total']); }, 50000, 500); - $response = $proxyClient->call(Client::METHOD_GET, '/', array_merge([ - 'content-type' => 'application/json', - 'x-appwrite-project' => $this->getProject()['$id'], - ])); + $response = $proxyClient->call(Client::METHOD_GET, '/'); $this->assertEquals(401, $response['headers']['status-code']); $this->assertStringContainsString("This domain is not connected to any Appwrite resource yet", $response['body']); @@ -1416,10 +1401,7 @@ class SitesCustomServerTest extends Scope $proxyClient = new Client(); $proxyClient->setEndpoint('http://' . $domain); - $response = $proxyClient->call(Client::METHOD_GET, '/', array_merge([ - 'content-type' => 'application/json', - 'x-appwrite-project' => $this->getProject()['$id'], - ])); + $response = $proxyClient->call(Client::METHOD_GET, '/'); $this->assertEquals(200, $response['headers']['status-code']); $this->assertStringContainsString("Hello Appwrite", $response['body']); @@ -1430,10 +1412,7 @@ class SitesCustomServerTest extends Scope $proxyClient = new Client(); $proxyClient->setEndpoint('http://' . $previewDomain); - $response = $proxyClient->call(Client::METHOD_GET, '/', array_merge([ - 'content-type' => 'application/json', - 'x-appwrite-project' => $this->getProject()['$id'], - ])); + $response = $proxyClient->call(Client::METHOD_GET, '/'); $this->assertEquals(200, $response['headers']['status-code']); $this->assertStringContainsString("Hello Appwrite", $response['body']); @@ -1500,5 +1479,66 @@ class SitesCustomServerTest extends Scope $this->assertEquals('http://localhost', $response['headers']['access-control-allow-origin']); } + public function testSiteScreenshot(): void + { + $siteId = $this->setupSite([ + 'siteId' => ID::unique(), + 'name' => 'Themed site', + 'framework' => 'other', + 'adapter' => 'static', + 'buildRuntime' => 'static-1', + 'outputDirectory' => './', + 'buildCommand' => '', + 'installCommand' => '', + 'fallbackFile' => '', + ]); + + $this->assertNotEmpty($siteId); + + $domain = $this->setupSiteDomain($siteId); + + $deploymentId = $this->setupDeployment($siteId, [ + 'code' => $this->packageSite('static-themed'), + 'activate' => 'true' + ]); + + $this->assertNotEmpty($deploymentId); + + $domain = $this->getSiteDomain($siteId); + $this->assertNotEmpty($domain); + + $proxyClient = new Client(); + $proxyClient->setEndpoint('http://' . $domain); + + \var_dump($domain); + + $response = $proxyClient->call(Client::METHOD_GET, '/'); + + $this->assertEquals(200, $response['headers']['status-code']); + $this->assertStringContainsString("Themed website", $response['body']); + $this->assertStringContainsString("@media (prefers-color-scheme: dark)", $response['body']); + + $deployment = $this->getDeployment($siteId, $deploymentId); + + $this->assertEquals(200, $deployment['headers']['status-code']); + $this->assertNotEmpty($deployment['body']['screenshot']); + $this->assertNotEmpty($deployment['body']['screenshotDark']); + + $screenshotId = $deployment['body']['screenshot']; + $file = $this->client->call(Client::METHOD_GET, "/storage/buckets/screenshots/files/$screenshotId/view?project=console&mode=admin", array_merge([ + ], $this->getHeaders())); + + \var_dump($file['headers']); + \var_dump(\strlen($file['body'])); + + $this->assertEquals(200, $file['headers']['status-code']); + $this->assertNotEmpty(200, $file['body']); + + // TODO: Dark file screenshot + // TODO: md5 different + + $this->cleanupSite($siteId); + } + // TODO: Add tests for deletion of resources when site is deleted } diff --git a/tests/resources/sites/static-themed/index.html b/tests/resources/sites/static-themed/index.html new file mode 100644 index 0000000000..955696b473 --- /dev/null +++ b/tests/resources/sites/static-themed/index.html @@ -0,0 +1,23 @@ + + + + + + Themed website + + + + From 732e74f114ed602bf89939e45a1981d503b81879 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Matej=20Ba=C4=8Do?= Date: Thu, 20 Feb 2025 14:22:30 +0100 Subject: [PATCH 13/20] Finalize screenshot tests --- app/controllers/api/vcs.php | 5 ++--- app/init.php | 6 +++--- composer.lock | 12 ++++++------ src/Appwrite/Platform/Modules/Compute/Base.php | 8 ++------ .../Modules/Functions/Workers/Builds.php | 4 ++-- .../Sites/Http/Deployments/Builds/Create.php | 7 ++----- .../Modules/Sites/Http/Deployments/Create.php | 16 ++++------------ .../Sites/Http/Deployments/Template/Create.php | 13 +++++-------- tests/e2e/Services/Sites/SitesBase.php | 12 ++++++++++++ 9 files changed, 38 insertions(+), 45 deletions(-) diff --git a/app/controllers/api/vcs.php b/app/controllers/api/vcs.php index 921a840c0a..d8394a3b4f 100644 --- a/app/controllers/api/vcs.php +++ b/app/controllers/api/vcs.php @@ -236,10 +236,9 @@ $createGitDeployments = function (GitHub $github, string $providerInstallationId $projectId = $project->getId(); $sitesDomain = System::getEnv('_APP_DOMAIN_SITES', ''); - $domain = "{$deploymentId}-{$projectId}.{$sitesDomain}"; + $domain = ID::unique() . "." . $sitesDomain; $ruleId = md5($domain); - - $rule = Authorization::skip( + Authorization::skip( fn () => $dbForPlatform->createDocument('rules', new Document([ '$id' => $ruleId, 'projectId' => $project->getId(), diff --git a/app/init.php b/app/init.php index 585dfc0616..53c190d78e 100644 --- a/app/init.php +++ b/app/init.php @@ -1964,13 +1964,13 @@ App::setResource( App::setResource('previewHostname', function (Request $request, ?Key $apiKey) { $allowed = false; - if(App::isDevelopment()) { + if (App::isDevelopment()) { $allowed = true; - } else if(!is_null($apiKey) && $apiKey->getHostnameOverride() === true) { + } elseif (!is_null($apiKey) && $apiKey->getHostnameOverride() === true) { $allowed = true; } - if($allowed === true) { + if ($allowed === true) { $host = $request->getQuery('appwrite-hostname', $request->getHeader('x-appwrite-hostname', '')); if (!empty($host)) { return $host; diff --git a/composer.lock b/composer.lock index 51535b220d..223a6a7a4e 100644 --- a/composer.lock +++ b/composer.lock @@ -6190,16 +6190,16 @@ }, { "name": "phpstan/phpdoc-parser", - "version": "2.0.2", + "version": "2.1.0", "source": { "type": "git", "url": "https://github.com/phpstan/phpdoc-parser.git", - "reference": "51087f87dcce2663e1fed4dfd4e56eccd580297e" + "reference": "9b30d6fd026b2c132b3985ce6b23bec09ab3aa68" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/phpstan/phpdoc-parser/zipball/51087f87dcce2663e1fed4dfd4e56eccd580297e", - "reference": "51087f87dcce2663e1fed4dfd4e56eccd580297e", + "url": "https://api.github.com/repos/phpstan/phpdoc-parser/zipball/9b30d6fd026b2c132b3985ce6b23bec09ab3aa68", + "reference": "9b30d6fd026b2c132b3985ce6b23bec09ab3aa68", "shasum": "" }, "require": { @@ -6231,9 +6231,9 @@ "description": "PHPDoc parser with support for nullable, intersection and generic types", "support": { "issues": "https://github.com/phpstan/phpdoc-parser/issues", - "source": "https://github.com/phpstan/phpdoc-parser/tree/2.0.2" + "source": "https://github.com/phpstan/phpdoc-parser/tree/2.1.0" }, - "time": "2025-02-17T20:25:51+00:00" + "time": "2025-02-19T13:28:12+00:00" }, { "name": "phpstan/phpstan", diff --git a/src/Appwrite/Platform/Modules/Compute/Base.php b/src/Appwrite/Platform/Modules/Compute/Base.php index d31b731407..81e20b240f 100644 --- a/src/Appwrite/Platform/Modules/Compute/Base.php +++ b/src/Appwrite/Platform/Modules/Compute/Base.php @@ -172,14 +172,10 @@ class Base extends Action 'activate' => $activate, ])); - // Preview deployments for sites - $projectId = $project->getId(); - $sitesDomain = System::getEnv('_APP_DOMAIN_SITES', ''); - $domain = "{$deploymentId}-{$projectId}.{$sitesDomain}"; + $domain = ID::unique() . "." . $sitesDomain; $ruleId = md5($domain); - - $rule = Authorization::skip( + Authorization::skip( fn () => $dbForPlatform->createDocument('rules', new Document([ '$id' => $ruleId, 'projectId' => $project->getId(), diff --git a/src/Appwrite/Platform/Modules/Functions/Workers/Builds.php b/src/Appwrite/Platform/Modules/Functions/Workers/Builds.php index 7501b60aae..a8b94666b0 100644 --- a/src/Appwrite/Platform/Modules/Functions/Workers/Builds.php +++ b/src/Appwrite/Platform/Modules/Functions/Workers/Builds.php @@ -740,12 +740,12 @@ class Builds extends Action $configs = [ 'screenshot' => [ 'headers' => [ 'x-appwrite-hostname' => $rule->getAttribute('domain') ], - 'url' => 'http://traefik/', + 'url' => 'http://traefik/?appwrite-preview=1&appwrite-theme=light', 'color' => 'light' ], 'screenshotDark' => [ 'headers' => [ 'x-appwrite-hostname' => $rule->getAttribute('domain') ], - 'url' => 'http://traefik/', + 'url' => 'http://traefik/?appwrite-preview=1&appwrite-theme=dark', 'color' => 'dark' ], ]; diff --git a/src/Appwrite/Platform/Modules/Sites/Http/Deployments/Builds/Create.php b/src/Appwrite/Platform/Modules/Sites/Http/Deployments/Builds/Create.php index c28a0be4e5..c76a4c3ffe 100644 --- a/src/Appwrite/Platform/Modules/Sites/Http/Deployments/Builds/Create.php +++ b/src/Appwrite/Platform/Modules/Sites/Http/Deployments/Builds/Create.php @@ -103,13 +103,10 @@ class Create extends Action ])); // Preview deployments for sites - $projectId = $project->getId(); - $sitesDomain = System::getEnv('_APP_DOMAIN_SITES', ''); - $domain = "{$deploymentId}-{$projectId}.{$sitesDomain}"; + $domain = ID::unique() . "." . $sitesDomain; $ruleId = md5($domain); - - $rule = Authorization::skip( + Authorization::skip( fn () => $dbForPlatform->createDocument('rules', new Document([ '$id' => $ruleId, 'projectId' => $project->getId(), diff --git a/src/Appwrite/Platform/Modules/Sites/Http/Deployments/Create.php b/src/Appwrite/Platform/Modules/Sites/Http/Deployments/Create.php index 38b9e2aeba..fb536a3123 100644 --- a/src/Appwrite/Platform/Modules/Sites/Http/Deployments/Create.php +++ b/src/Appwrite/Platform/Modules/Sites/Http/Deployments/Create.php @@ -228,14 +228,10 @@ class Create extends Action 'type' => $type ])); - // Preview deployments for sites - $projectId = $project->getId(); - $sitesDomain = System::getEnv('_APP_DOMAIN_SITES', ''); - $domain = "{$deploymentId}-{$projectId}.{$sitesDomain}"; + $domain = ID::unique() . "." . $sitesDomain; $ruleId = md5($domain); - - $rule = Authorization::skip( + Authorization::skip( fn () => $dbForPlatform->createDocument('rules', new Document([ '$id' => $ruleId, 'projectId' => $project->getId(), @@ -283,14 +279,10 @@ class Create extends Action 'type' => $type ])); - // Preview deployments for sites - $projectId = $project->getId(); - $sitesDomain = System::getEnv('_APP_DOMAIN_SITES', ''); - $domain = "{$deploymentId}-{$projectId}.{$sitesDomain}"; + $domain = ID::unique() . "." . $sitesDomain; $ruleId = md5($domain); - - $rule = Authorization::skip( + Authorization::skip( fn () => $dbForPlatform->createDocument('rules', new Document([ '$id' => $ruleId, 'projectId' => $project->getId(), diff --git a/src/Appwrite/Platform/Modules/Sites/Http/Deployments/Template/Create.php b/src/Appwrite/Platform/Modules/Sites/Http/Deployments/Template/Create.php index ec75740759..08be94776d 100644 --- a/src/Appwrite/Platform/Modules/Sites/Http/Deployments/Template/Create.php +++ b/src/Appwrite/Platform/Modules/Sites/Http/Deployments/Template/Create.php @@ -139,18 +139,15 @@ class Create extends Base 'activate' => $activate, ])); - // Preview deployments url - $projectId = $project->getId(); - $sitesDomain = System::getEnv('_APP_DOMAIN_SITES', ''); - $previewDomain = "{$deploymentId}-{$projectId}.{$sitesDomain}"; - - $rule = Authorization::skip( + $domain = ID::unique() . "." . $sitesDomain; + $ruleId = md5($domain); + Authorization::skip( fn () => $dbForPlatform->createDocument('rules', new Document([ - '$id' => \md5($previewDomain), + '$id' => $ruleId, 'projectId' => $project->getId(), 'projectInternalId' => $project->getInternalId(), - 'domain' => $previewDomain, + 'domain' => $domain, 'resourceType' => 'deployment', 'resourceId' => $deploymentId, 'resourceInternalId' => $deployment->getInternalId(), diff --git a/tests/e2e/Services/Sites/SitesBase.php b/tests/e2e/Services/Sites/SitesBase.php index 4fcd34572d..4c11e78d76 100644 --- a/tests/e2e/Services/Sites/SitesBase.php +++ b/tests/e2e/Services/Sites/SitesBase.php @@ -51,6 +51,18 @@ trait SitesBase $this->assertEquals('ready', $deployment['body']['status'], 'Deployment status is not ready, deployment: ' . json_encode($deployment['body'], JSON_PRETTY_PRINT)); }, 100000, 500); + // Not === so multipart/form-data works fine too + if (($params['activate'] ?? false) == true) { + $this->assertEventually(function () use ($siteId, $deploymentId) { + $site = $this->client->call(Client::METHOD_GET, '/sites/' . $siteId, array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + 'x-appwrite-key' => $this->getProject()['apiKey'], + ])); + $this->assertEquals($deploymentId, $site['body']['deploymentId'], 'Deployment is not activated, deployment: ' . json_encode($site['body'], JSON_PRETTY_PRINT)); + }, 100000, 500); + } + return $deploymentId; } From c3df0a71e322928d5be4b796da3a950fadb5188c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Matej=20Ba=C4=8Do?= Date: Thu, 20 Feb 2025 14:34:59 +0100 Subject: [PATCH 14/20] Address leftover --- .../Services/Sites/SitesCustomServerTest.php | 23 ++++++++++++++----- 1 file changed, 17 insertions(+), 6 deletions(-) diff --git a/tests/e2e/Services/Sites/SitesCustomServerTest.php b/tests/e2e/Services/Sites/SitesCustomServerTest.php index da8d9ebd8f..c06aade655 100644 --- a/tests/e2e/Services/Sites/SitesCustomServerTest.php +++ b/tests/e2e/Services/Sites/SitesCustomServerTest.php @@ -1510,8 +1510,6 @@ class SitesCustomServerTest extends Scope $proxyClient = new Client(); $proxyClient->setEndpoint('http://' . $domain); - \var_dump($domain); - $response = $proxyClient->call(Client::METHOD_GET, '/'); $this->assertEquals(200, $response['headers']['status-code']); @@ -1528,14 +1526,27 @@ class SitesCustomServerTest extends Scope $file = $this->client->call(Client::METHOD_GET, "/storage/buckets/screenshots/files/$screenshotId/view?project=console&mode=admin", array_merge([ ], $this->getHeaders())); - \var_dump($file['headers']); - \var_dump(\strlen($file['body'])); + $this->assertEquals(200, $file['headers']['status-code']); + $this->assertNotEmpty(200, $file['body']); + $this->assertGreaterThan(4096, $file['headers']['content-length']); + $this->assertEquals('image/png', $file['headers']['content-type']); + + $screenshotHash = \md5($file['body']); + $this->assertNotEmpty($screenshotHash); + + $screenshotId = $deployment['body']['screenshotDark']; + $file = $this->client->call(Client::METHOD_GET, "/storage/buckets/screenshots/files/$screenshotId/view?project=console&mode=admin", array_merge([ + ], $this->getHeaders())); $this->assertEquals(200, $file['headers']['status-code']); $this->assertNotEmpty(200, $file['body']); + $this->assertGreaterThan(4096, $file['headers']['content-length']); + $this->assertEquals('image/png', $file['headers']['content-type']); - // TODO: Dark file screenshot - // TODO: md5 different + $screenshotDarkHash = \md5($file['body']); + $this->assertNotEmpty($screenshotDarkHash); + + $this->assertNotEquals($screenshotDarkHash, $screenshotHash); $this->cleanupSite($siteId); } From 54d877fe3ef9c32c819f508d760acc444e201f94 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Matej=20Ba=C4=8Do?= Date: Thu, 20 Feb 2025 16:49:20 +0100 Subject: [PATCH 15/20] PR review fixes --- docker-compose.yml | 2 +- .../Platform/Modules/Functions/Workers/Builds.php | 4 ++-- src/Appwrite/Platform/Workers/Deletes.php | 9 +++++++-- src/Appwrite/Utopia/Response/Model/Deployment.php | 2 +- 4 files changed, 11 insertions(+), 6 deletions(-) diff --git a/docker-compose.yml b/docker-compose.yml index 04d623df35..f4f93da9f2 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -950,7 +950,7 @@ services: appwrite-browser: container_name: appwrite-browser - image: appwrite/browser:0.1.0 + image: appwrite/browser:0.2.0 networks: - appwrite diff --git a/src/Appwrite/Platform/Modules/Functions/Workers/Builds.php b/src/Appwrite/Platform/Modules/Functions/Workers/Builds.php index a8b94666b0..fe925750e0 100644 --- a/src/Appwrite/Platform/Modules/Functions/Workers/Builds.php +++ b/src/Appwrite/Platform/Modules/Functions/Workers/Builds.php @@ -741,12 +741,12 @@ class Builds extends Action 'screenshot' => [ 'headers' => [ 'x-appwrite-hostname' => $rule->getAttribute('domain') ], 'url' => 'http://traefik/?appwrite-preview=1&appwrite-theme=light', - 'color' => 'light' + 'theme' => 'light' ], 'screenshotDark' => [ 'headers' => [ 'x-appwrite-hostname' => $rule->getAttribute('domain') ], 'url' => 'http://traefik/?appwrite-preview=1&appwrite-theme=dark', - 'color' => 'dark' + 'theme' => 'dark' ], ]; diff --git a/src/Appwrite/Platform/Workers/Deletes.php b/src/Appwrite/Platform/Workers/Deletes.php index 461ce9af88..e2e6fc5545 100644 --- a/src/Appwrite/Platform/Workers/Deletes.php +++ b/src/Appwrite/Platform/Workers/Deletes.php @@ -99,7 +99,7 @@ class Deletes extends Action $this->deleteFunction($dbForPlatform, $getProjectDB, $deviceForFunctions, $deviceForBuilds, $certificates, $document, $project); break; case DELETE_TYPE_DEPLOYMENTS: - $this->deleteDeployment($dbForPlatform, $getProjectDB, $deviceForFunctions, $deviceForBuilds, $document, $certificates, $project); + $this->deleteDeployment($dbForPlatform, $getProjectDB, $deviceForFunctions, $deviceForBuilds, $deviceForFiles, $document, $certificates, $project); break; case DELETE_TYPE_USERS: $this->deleteUser($getProjectDB, $document, $project); @@ -1048,7 +1048,7 @@ class Deletes extends Action * @return void * @throws Exception */ - private function deleteDeployment(Database $dbForPlatform, callable $getProjectDB, Device $deviceForFunctions, Device $deviceForBuilds, Document $document, CertificatesAdapter $certificates, Document $project): void + private function deleteDeployment(Database $dbForPlatform, callable $getProjectDB, Device $deviceForFunctions, Device $deviceForBuilds, Device $deviceForFiles, Document $document, CertificatesAdapter $certificates, Document $project): void { $projectId = $project->getId(); $dbForProject = $getProjectDB($project); @@ -1060,6 +1060,11 @@ class Deletes extends Action */ $this->deleteDeploymentFiles($deviceForFunctions, $document); //TODO: For sites, this should be deviceForSites + /** + * Delete deployment screenshots + */ + $this->deleteDeploymentScreenshots($deviceForFiles, $dbForPlatform, $document); + /** * Delete builds */ diff --git a/src/Appwrite/Utopia/Response/Model/Deployment.php b/src/Appwrite/Utopia/Response/Model/Deployment.php index 3f226d72ea..f5deb7ac6d 100644 --- a/src/Appwrite/Utopia/Response/Model/Deployment.php +++ b/src/Appwrite/Utopia/Response/Model/Deployment.php @@ -84,7 +84,7 @@ class Deployment extends Model ]) ->addRule('screenshotDark', [ 'type' => self::TYPE_STRING, - 'description' => 'Screenshot with dark theme prefference file ID.', + 'description' => 'Screenshot with dark theme preference file ID.', 'default' => '', 'example' => '5e5ea5c16897e', ]) From 1c6204118f46acab80455c6f2d1beda3906f021d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Matej=20Ba=C4=8Do?= Date: Fri, 21 Feb 2025 10:09:35 +0100 Subject: [PATCH 16/20] PR code review --- app/config/collections/projects.php | 2 +- app/http.php | 2 +- src/Appwrite/Platform/Modules/Functions/Workers/Builds.php | 6 +++--- src/Appwrite/Platform/Workers/Deletes.php | 4 ++-- src/Appwrite/Utopia/Response/Model/Deployment.php | 4 ++-- tests/e2e/Services/Sites/SitesCustomServerTest.php | 4 ++-- 6 files changed, 11 insertions(+), 11 deletions(-) diff --git a/app/config/collections/projects.php b/app/config/collections/projects.php index 3df48fcd55..597f02f7d6 100644 --- a/app/config/collections/projects.php +++ b/app/config/collections/projects.php @@ -1530,7 +1530,7 @@ return [ 'filters' => [], ], [ - '$id' => ID::custom('screenshot'), // File ID from 'screenshots' Console bucket + '$id' => ID::custom('screenshotLight'), // File ID from 'screenshots' Console bucket 'type' => Database::VAR_STRING, 'format' => '', 'size' => 32, diff --git a/app/http.php b/app/http.php index d00c33c217..ed903fc48b 100644 --- a/app/http.php +++ b/app/http.php @@ -311,7 +311,7 @@ $http->on(Constant::EVENT_START, function (Server $http) use ($payloadSize, $reg '$id' => ID::custom('screenshots'), '$collection' => ID::custom('buckets'), 'name' => 'Screenshots', - 'maximumFileSize' => (int) System::getEnv('_APP_STORAGE_LIMIT', 0), // 10MB + 'maximumFileSize' => 5000000, // ~5MB 'allowedFileExtensions' => [ 'png' ], 'enabled' => true, 'compression' => Compression::GZIP, diff --git a/src/Appwrite/Platform/Modules/Functions/Workers/Builds.php b/src/Appwrite/Platform/Modules/Functions/Workers/Builds.php index fe925750e0..43cac3bcc6 100644 --- a/src/Appwrite/Platform/Modules/Functions/Workers/Builds.php +++ b/src/Appwrite/Platform/Modules/Functions/Workers/Builds.php @@ -738,14 +738,14 @@ class Builds extends Action $bucket = $dbForPlatform->getDocument('buckets', 'screenshots'); $configs = [ - 'screenshot' => [ + 'screenshotLight' => [ 'headers' => [ 'x-appwrite-hostname' => $rule->getAttribute('domain') ], - 'url' => 'http://traefik/?appwrite-preview=1&appwrite-theme=light', + 'url' => 'http://appwrite/?appwrite-preview=1&appwrite-theme=light', 'theme' => 'light' ], 'screenshotDark' => [ 'headers' => [ 'x-appwrite-hostname' => $rule->getAttribute('domain') ], - 'url' => 'http://traefik/?appwrite-preview=1&appwrite-theme=dark', + 'url' => 'http://appwrite/?appwrite-preview=1&appwrite-theme=dark', 'theme' => 'dark' ], ]; diff --git a/src/Appwrite/Platform/Workers/Deletes.php b/src/Appwrite/Platform/Workers/Deletes.php index e2e6fc5545..cfc9a3ff79 100644 --- a/src/Appwrite/Platform/Workers/Deletes.php +++ b/src/Appwrite/Platform/Workers/Deletes.php @@ -933,8 +933,8 @@ class Deletes extends Action private function deleteDeploymentScreenshots(Device $deviceForFiles, Database $dbForPlatform, Document $deployment): void { $screenshotIds = []; - if (!empty($deployment->getAttribute('screenshot', ''))) { - $screenshotIds[] = $deployment->getAttribute('screenshot', ''); + if (!empty($deployment->getAttribute('screenshotLight', ''))) { + $screenshotIds[] = $deployment->getAttribute('screenshotLight', ''); } if (!empty($deployment->getAttribute('screenshotDark', ''))) { $screenshotIds[] = $deployment->getAttribute('screenshotDark', ''); diff --git a/src/Appwrite/Utopia/Response/Model/Deployment.php b/src/Appwrite/Utopia/Response/Model/Deployment.php index f5deb7ac6d..6c13fd4b9e 100644 --- a/src/Appwrite/Utopia/Response/Model/Deployment.php +++ b/src/Appwrite/Utopia/Response/Model/Deployment.php @@ -76,9 +76,9 @@ class Deployment extends Model 'default' => false, 'example' => true, ]) - ->addRule('screenshot', [ + ->addRule('screenshotLight', [ 'type' => self::TYPE_STRING, - 'description' => 'Screenshot file ID.', + 'description' => 'Screenshot with light theme preference file ID.', 'default' => '', 'example' => '5e5ea5c16897e', ]) diff --git a/tests/e2e/Services/Sites/SitesCustomServerTest.php b/tests/e2e/Services/Sites/SitesCustomServerTest.php index c06aade655..6a86f64a88 100644 --- a/tests/e2e/Services/Sites/SitesCustomServerTest.php +++ b/tests/e2e/Services/Sites/SitesCustomServerTest.php @@ -1519,10 +1519,10 @@ class SitesCustomServerTest extends Scope $deployment = $this->getDeployment($siteId, $deploymentId); $this->assertEquals(200, $deployment['headers']['status-code']); - $this->assertNotEmpty($deployment['body']['screenshot']); + $this->assertNotEmpty($deployment['body']['screenshotLight']); $this->assertNotEmpty($deployment['body']['screenshotDark']); - $screenshotId = $deployment['body']['screenshot']; + $screenshotId = $deployment['body']['screenshotLight']; $file = $this->client->call(Client::METHOD_GET, "/storage/buckets/screenshots/files/$screenshotId/view?project=console&mode=admin", array_merge([ ], $this->getHeaders())); From 2917c66202157c3dd8c61006a9c0dda5df793efd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Matej=20Ba=C4=8Do?= Date: Fri, 21 Feb 2025 20:21:13 +0100 Subject: [PATCH 17/20] Remove unnessessary complexity --- app/http.php | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/app/http.php b/app/http.php index ed903fc48b..2b1f038777 100644 --- a/app/http.php +++ b/app/http.php @@ -250,8 +250,7 @@ $http->on(Constant::EVENT_START, function (Server $http) use ($payloadSize, $reg $audit->setup(); } - if ($dbForPlatform->getDocument('buckets', 'default')->isEmpty() && - !$dbForPlatform->exists($dbForPlatform->getDatabase(), 'bucket_1')) { + if ($dbForPlatform->getDocument('buckets', 'default')->isEmpty()) { Console::info(" └── Creating default bucket..."); $dbForPlatform->createDocument('buckets', new Document([ '$id' => ID::custom('default'), @@ -304,8 +303,7 @@ $http->on(Constant::EVENT_START, function (Server $http) use ($payloadSize, $reg $dbForPlatform->createCollection('bucket_' . $bucket->getInternalId(), $attributes, $indexes); } - if ($dbForPlatform->getDocument('buckets', 'screenshots')->isEmpty() && - !$dbForPlatform->exists($dbForPlatform->getDatabase(), 'bucket_2')) { + if ($dbForPlatform->getDocument('buckets', 'screenshots')->isEmpty()) { Console::info(" └── Creating screenshots bucket..."); $dbForPlatform->createDocument('buckets', new Document([ '$id' => ID::custom('screenshots'), From 8661ae684078428f89040ebe441b56e083378c54 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Matej=20Ba=C4=8Do?= Date: Fri, 21 Feb 2025 20:50:43 +0100 Subject: [PATCH 18/20] Disable banner for screenshots --- app/controllers/general.php | 44 +++++++++++-------- app/init.php | 4 +- src/Appwrite/Auth/Key.php | 21 ++++++++- .../Modules/Functions/Workers/Builds.php | 4 +- 4 files changed, 49 insertions(+), 24 deletions(-) diff --git a/app/controllers/general.php b/app/controllers/general.php index 9dff0b1de8..564125c79e 100644 --- a/app/controllers/general.php +++ b/app/controllers/general.php @@ -4,6 +4,7 @@ require_once __DIR__ . '/../init.php'; use Ahc\Jwt\JWT; use Appwrite\Auth\Auth; +use Appwrite\Auth\Key; use Appwrite\Event\Certificate; use Appwrite\Event\Event; use Appwrite\Event\Func; @@ -54,7 +55,7 @@ Config::setParam('domainVerification', false); Config::setParam('cookieDomain', 'localhost'); Config::setParam('cookieSamesite', Response::COOKIE_SAMESITE_NONE); -function router(App $utopia, Database $dbForPlatform, callable $getProjectDB, SwooleRequest $swooleRequest, Request $request, Response $response, Event $queueForEvents, StatsUsage $queueForStatsUsage, Func $queueForFunctions, Reader $geodb, callable $isResourceBlocked, string $previewHostname) +function router(App $utopia, Database $dbForPlatform, callable $getProjectDB, SwooleRequest $swooleRequest, Request $request, Response $response, Event $queueForEvents, StatsUsage $queueForStatsUsage, Func $queueForFunctions, Reader $geodb, callable $isResourceBlocked, string $previewHostname, ?Key $apiKey) { $utopia->getRoute()?->label('error', __DIR__ . '/../views/general/error.phtml'); @@ -448,18 +449,19 @@ function router(App $utopia, Database $dbForPlatform, callable $getProjectDB, Sw requestTimeout: 30 ); - // Branded banner for previews - $transformation = new Transformation(); - $transformation->addAdapter(new Preview()); - $transformation->setInput($executionResponse['body']); - $transformation->setTraits($executionResponse['headers']); - if ($type === 'deployment' && $transformation->transform()) { - $executionResponse['body'] = $transformation->getOutput(); + if (\is_null($apiKey) || $apiKey->isBannerDisabled() === false) { + $transformation = new Transformation(); + $transformation->addAdapter(new Preview()); + $transformation->setInput($executionResponse['body']); + $transformation->setTraits($executionResponse['headers']); + if ($type === 'deployment' && $transformation->transform()) { + $executionResponse['body'] = $transformation->getOutput(); - foreach ($executionResponse['headers'] as $key => $value) { - if (\strtolower($key) === 'content-length') { - $executionResponse['headers'][$key] = \strlen($executionResponse['body']); + foreach ($executionResponse['headers'] as $key => $value) { + if (\strtolower($key) === 'content-length') { + $executionResponse['headers'][$key] = \strlen($executionResponse['body']); + } } } } @@ -617,7 +619,8 @@ App::init() ->inject('queueForFunctions') ->inject('isResourceBlocked') ->inject('previewHostname') - ->action(function (App $utopia, SwooleRequest $swooleRequest, Request $request, Response $response, Document $console, Document $project, Database $dbForPlatform, callable $getProjectDB, Locale $locale, array $localeCodes, array $clients, Reader $geodb, StatsUsage $queueForStatsUsage, Event $queueForEvents, Certificate $queueForCertificates, Func $queueForFunctions, callable $isResourceBlocked, string $previewHostname) { + ->inject('apiKey') + ->action(function (App $utopia, SwooleRequest $swooleRequest, Request $request, Response $response, Document $console, Document $project, Database $dbForPlatform, callable $getProjectDB, Locale $locale, array $localeCodes, array $clients, Reader $geodb, StatsUsage $queueForStatsUsage, Event $queueForEvents, Certificate $queueForCertificates, Func $queueForFunctions, callable $isResourceBlocked, string $previewHostname, ?Key $apiKey) { /* * Appwrite Router */ @@ -625,7 +628,7 @@ App::init() $mainDomain = System::getEnv('_APP_DOMAIN', ''); // Only run Router when external domain if ($host !== $mainDomain || !empty($previewHostname)) { - if (router($utopia, $dbForPlatform, $getProjectDB, $swooleRequest, $request, $response, $queueForEvents, $queueForStatsUsage, $queueForFunctions, $geodb, $isResourceBlocked, $previewHostname)) { + if (router($utopia, $dbForPlatform, $getProjectDB, $swooleRequest, $request, $response, $queueForEvents, $queueForStatsUsage, $queueForFunctions, $geodb, $isResourceBlocked, $previewHostname, $apiKey)) { $utopia->getRoute()?->label('router', true); } } @@ -865,7 +868,8 @@ App::options() ->inject('geodb') ->inject('isResourceBlocked') ->inject('previewHostname') - ->action(function (App $utopia, SwooleRequest $swooleRequest, Request $request, Response $response, Database $dbForPlatform, callable $getProjectDB, Event $queueForEvents, StatsUsage $queueForStatsUsage, Func $queueForFunctions, Reader $geodb, callable $isResourceBlocked, string $previewHostname) { + ->inject('apiKey') + ->action(function (App $utopia, SwooleRequest $swooleRequest, Request $request, Response $response, Database $dbForPlatform, callable $getProjectDB, Event $queueForEvents, StatsUsage $queueForStatsUsage, Func $queueForFunctions, Reader $geodb, callable $isResourceBlocked, string $previewHostname, ?Key $apiKey) { /* * Appwrite Router */ @@ -873,7 +877,7 @@ App::options() $mainDomain = System::getEnv('_APP_DOMAIN', ''); // Only run Router when external domain if ($host !== $mainDomain || !empty($previewHostname)) { - if (router($utopia, $dbForPlatform, $getProjectDB, $swooleRequest, $request, $response, $queueForEvents, $queueForStatsUsage, $queueForFunctions, $geodb, $isResourceBlocked, $previewHostname)) { + if (router($utopia, $dbForPlatform, $getProjectDB, $swooleRequest, $request, $response, $queueForEvents, $queueForStatsUsage, $queueForFunctions, $geodb, $isResourceBlocked, $previewHostname, $apiKey)) { $utopia->getRoute()?->label('router', true); } } @@ -1173,7 +1177,8 @@ App::get('/robots.txt') ->inject('geodb') ->inject('isResourceBlocked') ->inject('previewHostname') - ->action(function (App $utopia, SwooleRequest $swooleRequest, Request $request, Response $response, Database $dbForPlatform, callable $getProjectDB, Event $queueForEvents, StatsUsage $queueForStatsUsage, Func $queueForFunctions, Reader $geodb, callable $isResourceBlocked, string $previewHostname) { + ->inject('apiKey') + ->action(function (App $utopia, SwooleRequest $swooleRequest, Request $request, Response $response, Database $dbForPlatform, callable $getProjectDB, Event $queueForEvents, StatsUsage $queueForStatsUsage, Func $queueForFunctions, Reader $geodb, callable $isResourceBlocked, string $previewHostname, ?Key $apiKey) { $host = $request->getHostname() ?? ''; $mainDomain = System::getEnv('_APP_DOMAIN', ''); @@ -1181,7 +1186,7 @@ App::get('/robots.txt') $template = new View(__DIR__ . '/../views/general/robots.phtml'); $response->text($template->render(false)); } else { - if (router($utopia, $dbForPlatform, $getProjectDB, $swooleRequest, $request, $response, $queueForEvents, $queueForStatsUsage, $queueForFunctions, $geodb, $isResourceBlocked, $previewHostname)) { + if (router($utopia, $dbForPlatform, $getProjectDB, $swooleRequest, $request, $response, $queueForEvents, $queueForStatsUsage, $queueForFunctions, $geodb, $isResourceBlocked, $previewHostname, $apiKey)) { $utopia->getRoute()?->label('router', true); } } @@ -1203,7 +1208,8 @@ App::get('/humans.txt') ->inject('geodb') ->inject('isResourceBlocked') ->inject('previewHostname') - ->action(function (App $utopia, SwooleRequest $swooleRequest, Request $request, Response $response, Database $dbForPlatform, callable $getProjectDB, Event $queueForEvents, StatsUsage $queueForStatsUsage, Func $queueForFunctions, Reader $geodb, callable $isResourceBlocked, string $previewHostname) { + ->inject('apiKey') + ->action(function (App $utopia, SwooleRequest $swooleRequest, Request $request, Response $response, Database $dbForPlatform, callable $getProjectDB, Event $queueForEvents, StatsUsage $queueForStatsUsage, Func $queueForFunctions, Reader $geodb, callable $isResourceBlocked, string $previewHostname, ?Key $apiKey) { $host = $request->getHostname() ?? ''; $mainDomain = System::getEnv('_APP_DOMAIN', ''); @@ -1211,7 +1217,7 @@ App::get('/humans.txt') $template = new View(__DIR__ . '/../views/general/humans.phtml'); $response->text($template->render(false)); } else { - if (router($utopia, $dbForPlatform, $getProjectDB, $swooleRequest, $request, $response, $queueForEvents, $queueForStatsUsage, $queueForFunctions, $geodb, $isResourceBlocked, $previewHostname)) { + if (router($utopia, $dbForPlatform, $getProjectDB, $swooleRequest, $request, $response, $queueForEvents, $queueForStatsUsage, $queueForFunctions, $geodb, $isResourceBlocked, $previewHostname, $apiKey)) { $utopia->getRoute()?->label('router', true); } } diff --git a/app/init.php b/app/init.php index 53c190d78e..4e36c91cb1 100644 --- a/app/init.php +++ b/app/init.php @@ -1966,11 +1966,11 @@ App::setResource('previewHostname', function (Request $request, ?Key $apiKey) { if (App::isDevelopment()) { $allowed = true; - } elseif (!is_null($apiKey) && $apiKey->getHostnameOverride() === true) { + } elseif (!\is_null($apiKey) && $apiKey->getHostnameOverride() === true) { $allowed = true; } - if ($allowed === true) { + if ($allowed) { $host = $request->getQuery('appwrite-hostname', $request->getHeader('x-appwrite-hostname', '')); if (!empty($host)) { return $host; diff --git a/src/Appwrite/Auth/Key.php b/src/Appwrite/Auth/Key.php index 2845a879a5..83f8dd408d 100644 --- a/src/Appwrite/Auth/Key.php +++ b/src/Appwrite/Auth/Key.php @@ -21,6 +21,8 @@ class Key protected bool $expired = false, protected array $disabledMetrics = [], protected bool $hostnameOverride = false, + protected bool $bannerDisabled = false, + protected bool $projectCheckDisabled = false, ) { } @@ -65,6 +67,17 @@ class Key return $this->hostnameOverride; } + + public function isBannerDisabled(): bool + { + return $this->bannerDisabled; + } + + public function isProjectCheckDisabled(): bool + { + return $this->projectCheckDisabled; + } + /** * Decode the given secret key into a Key object, containing the project ID, type, role, scopes, and name. * Can be a stored API key or a dynamic key (JWT). @@ -117,9 +130,11 @@ class Key $projectId = $payload['projectId'] ?? ''; $disabledMetrics = $payload['disabledMetrics'] ?? []; $hostnameOverride = $payload['hostnameOverride'] ?? false; + $bannerDisabled = $payload['bannerDisabled'] ?? false; + $projectCheckDisabled = $payload['projectCheckDisabled'] ?? false; $scopes = \array_merge($payload['scopes'] ?? [], $scopes); - if ($projectId !== $project->getId()) { + if (!$projectCheckDisabled && $projectId !== $project->getId()) { return $guestKey; } @@ -131,7 +146,9 @@ class Key $name, $expired, $disabledMetrics, - $hostnameOverride + $hostnameOverride, + $bannerDisabled, + $projectCheckDisabled ); case API_KEY_STANDARD: $key = $project->find( diff --git a/src/Appwrite/Platform/Modules/Functions/Workers/Builds.php b/src/Appwrite/Platform/Modules/Functions/Workers/Builds.php index 43cac3bcc6..15e5daff40 100644 --- a/src/Appwrite/Platform/Modules/Functions/Workers/Builds.php +++ b/src/Appwrite/Platform/Modules/Functions/Workers/Builds.php @@ -752,7 +752,9 @@ class Builds extends Action $jwtObj = new JWT(System::getEnv('_APP_OPENSSL_KEY_V1'), 'HS256', 900, 0); $apiKey = $jwtObj->encode([ - 'hostnameOverride' => true + 'hostnameOverride' => true, + 'bannerDisabled' => true, + 'projectCheckDisabled' => true ]); // TODO: @Meldiron if becomes too slow, do concurrently From 7397d52c0c2ce42981413acad653d4dd8eae9779 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Matej=20Ba=C4=8Do?= Date: Fri, 21 Feb 2025 21:41:39 +0100 Subject: [PATCH 19/20] Fix tests --- app/config/site-templates.php | 45 +++++++++++++++++++ .../Services/Sites/SitesCustomServerTest.php | 4 +- 2 files changed, 47 insertions(+), 2 deletions(-) diff --git a/app/config/site-templates.php b/app/config/site-templates.php index fbd4732573..b22b559a15 100644 --- a/app/config/site-templates.php +++ b/app/config/site-templates.php @@ -539,4 +539,49 @@ return [ 'providerVersion' => '0.2.*', 'variables' => [], ], + [ + 'key' => 'nextjs-starter', + 'name' => 'Next.js starter website', + 'useCases' => ['starter'], + 'frameworks' => [ + getFramework('NEXTJS', [ + 'providerRootDirectory' => './nextjs/starter', + ]), + ], + 'vcsProvider' => 'github', + 'providerRepositoryId' => 'templates-for-sites', + 'providerOwner' => 'appwrite', + 'providerVersion' => '0.2.*', + 'variables' => [], + ], + [ + 'key' => 'nuxt-starter', + 'name' => 'Nuxt starter website', + 'useCases' => ['starter'], + 'frameworks' => [ + getFramework('NUXT', [ + 'providerRootDirectory' => './nuxt/starter', + ]), + ], + 'vcsProvider' => 'github', + 'providerRepositoryId' => 'templates-for-sites', + 'providerOwner' => 'appwrite', + 'providerVersion' => '0.2.*', + 'variables' => [], + ], + [ + 'key' => 'sveltekit-starter', + 'name' => 'SvelteKit starter website', + 'useCases' => ['starter'], + 'frameworks' => [ + getFramework('SVELTEKIT', [ + 'providerRootDirectory' => './sveltekit/starter', + ]), + ], + 'vcsProvider' => 'github', + 'providerRepositoryId' => 'templates-for-sites', + 'providerOwner' => 'appwrite', + 'providerVersion' => '0.2.*', + 'variables' => [], + ], ]; diff --git a/tests/e2e/Services/Sites/SitesCustomServerTest.php b/tests/e2e/Services/Sites/SitesCustomServerTest.php index 6a86f64a88..1961e40ce5 100644 --- a/tests/e2e/Services/Sites/SitesCustomServerTest.php +++ b/tests/e2e/Services/Sites/SitesCustomServerTest.php @@ -1528,7 +1528,7 @@ class SitesCustomServerTest extends Scope $this->assertEquals(200, $file['headers']['status-code']); $this->assertNotEmpty(200, $file['body']); - $this->assertGreaterThan(4096, $file['headers']['content-length']); + $this->assertGreaterThan(1, $file['headers']['content-length']); $this->assertEquals('image/png', $file['headers']['content-type']); $screenshotHash = \md5($file['body']); @@ -1540,7 +1540,7 @@ class SitesCustomServerTest extends Scope $this->assertEquals(200, $file['headers']['status-code']); $this->assertNotEmpty(200, $file['body']); - $this->assertGreaterThan(4096, $file['headers']['content-length']); + $this->assertGreaterThan(1, $file['headers']['content-length']); $this->assertEquals('image/png', $file['headers']['content-type']); $screenshotDarkHash = \md5($file['body']); From f9bc5d0cce1db4414f7c8c8c576609c6f49eebb0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Matej=20Ba=C4=8Do?= Date: Fri, 21 Feb 2025 22:10:39 +0100 Subject: [PATCH 20/20] Fix tests --- app/controllers/general.php | 4 ++-- tests/e2e/Services/Functions/FunctionsBase.php | 12 ++++++++++++ 2 files changed, 14 insertions(+), 2 deletions(-) diff --git a/app/controllers/general.php b/app/controllers/general.php index 564125c79e..6d201d4a78 100644 --- a/app/controllers/general.php +++ b/app/controllers/general.php @@ -270,11 +270,11 @@ function router(App $utopia, Database $dbForPlatform, callable $getProjectDB, Sw if ($type === 'function') { $jwtExpiry = $resource->getAttribute('timeout', 900); $jwtObj = new JWT(System::getEnv('_APP_OPENSSL_KEY_V1'), 'HS256', $jwtExpiry, 0); - $apiKey = $jwtObj->encode([ + $jwtKey = $jwtObj->encode([ 'projectId' => $project->getId(), 'scopes' => $resource->getAttribute('scopes', []) ]); - $headers['x-appwrite-key'] = API_KEY_DYNAMIC . '_' . $apiKey; + $headers['x-appwrite-key'] = API_KEY_DYNAMIC . '_' . $jwtKey; $headers['x-appwrite-trigger'] = 'http'; $headers['x-appwrite-user-jwt'] = ''; } diff --git a/tests/e2e/Services/Functions/FunctionsBase.php b/tests/e2e/Services/Functions/FunctionsBase.php index ce5df8b746..b7624c24a1 100644 --- a/tests/e2e/Services/Functions/FunctionsBase.php +++ b/tests/e2e/Services/Functions/FunctionsBase.php @@ -51,6 +51,18 @@ trait FunctionsBase $this->assertEquals('ready', $deployment['body']['status'], 'Deployment status is not ready, deployment: ' . json_encode($deployment['body'], JSON_PRETTY_PRINT)); }, 50000, 500); + // Not === so multipart/form-data works fine too + if (($params['activate'] ?? false) == true) { + $this->assertEventually(function () use ($functionId, $deploymentId) { + $function = $this->client->call(Client::METHOD_GET, '/functions/' . $functionId, array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + 'x-appwrite-key' => $this->getProject()['apiKey'], + ])); + $this->assertEquals($deploymentId, $function['body']['deployment'], 'Deployment is not activated, deployment: ' . json_encode($function['body'], JSON_PRETTY_PRINT)); + }, 100000, 500); + } + return $deploymentId; }