diff --git a/app/config/collections/projects.php b/app/config/collections/projects.php index 933de12290..120c9704ce 100644 --- a/app/config/collections/projects.php +++ b/app/config/collections/projects.php @@ -841,6 +841,28 @@ return [ 'array' => true, 'filters' => [], ], + [ + '$id' => ID::custom('providerBranches'), + 'type' => Database::VAR_STRING, + 'format' => '', + 'size' => 128, + 'signed' => true, + 'required' => false, + 'default' => [], + 'array' => true, + 'filters' => [], + ], + [ + '$id' => ID::custom('providerPaths'), + 'type' => Database::VAR_STRING, + 'format' => '', + 'size' => 128, + 'signed' => true, + 'required' => false, + 'default' => [], + 'array' => true, + 'filters' => [], + ], ], 'indexes' => [ [ @@ -1320,6 +1342,28 @@ return [ 'array' => false, 'filters' => [], ], + [ + '$id' => ID::custom('providerBranches'), + 'type' => Database::VAR_STRING, + 'format' => '', + 'size' => 128, + 'signed' => true, + 'required' => false, + 'default' => [], + 'array' => true, + 'filters' => [], + ], + [ + '$id' => ID::custom('providerPaths'), + 'type' => Database::VAR_STRING, + 'format' => '', + 'size' => 128, + 'signed' => true, + 'required' => false, + 'default' => [], + 'array' => true, + 'filters' => [], + ], ], 'indexes' => [ [ diff --git a/app/config/cors.php b/app/config/cors.php index 0454a24495..8147ec8fe5 100644 --- a/app/config/cors.php +++ b/app/config/cors.php @@ -22,6 +22,7 @@ return [ 'X-Appwrite-Locale', 'X-Appwrite-Mode', 'X-Appwrite-JWT', + 'X-Appwrite-Organization', 'X-Appwrite-Response-Format', 'X-Appwrite-Timeout', 'X-Appwrite-ID', diff --git a/app/config/roles.php b/app/config/roles.php index abb8d4481f..d5ae7c1331 100644 --- a/app/config/roles.php +++ b/app/config/roles.php @@ -150,7 +150,7 @@ return [ 'label' => 'Owner', 'scopes' => \array_merge($member, $admins), ], - User::ROLE_APPS => [ + User::ROLE_KEYS => [ 'label' => 'Applications', 'scopes' => ['global', 'health.read', 'graphql'], ], diff --git a/app/controllers/api/graphql.php b/app/controllers/api/graphql.php index 9ec2479749..4a509aefdd 100644 --- a/app/controllers/api/graphql.php +++ b/app/controllers/api/graphql.php @@ -39,7 +39,7 @@ Http::init() if ( array_key_exists('graphql', $project->getAttribute('apis', [])) && !$project->getAttribute('apis', [])['graphql'] - && !($user->isPrivileged($authorization->getRoles()) || $user->isApp($authorization->getRoles())) + && !($user->isPrivileged($authorization->getRoles()) || $user->isKey($authorization->getRoles())) ) { throw new AppwriteException(AppwriteException::GENERAL_API_DISABLED); } diff --git a/app/controllers/general.php b/app/controllers/general.php index b39c2e2623..219c14774f 100644 --- a/app/controllers/general.php +++ b/app/controllers/general.php @@ -835,7 +835,8 @@ Http::init() ->inject('authorization') ->inject('publisherForDeletes') ->inject('executionsRetentionCount') - ->action(function (Http $utopia, SwooleRequest $swooleRequest, Request $request, Response $response, Log $log, Document $project, Database $dbForPlatform, callable $getProjectDB, Locale $locale, array $localeCodes, Reader $geodb, Event $queueForEvents, Bus $bus, Executor $executor, array $platform, callable $isResourceBlocked, string $previewHostname, Document $devKey, ?Key $apiKey, Cors $cors, Authorization $authorization, DeletePublisher $publisherForDeletes, int $executionsRetentionCount) { + ->inject('params') + ->action(function (Http $utopia, SwooleRequest $swooleRequest, Request $request, Response $response, Log $log, Document $project, Database $dbForPlatform, callable $getProjectDB, Locale $locale, array $localeCodes, Reader $geodb, Event $queueForEvents, Bus $bus, Executor $executor, array $platform, callable $isResourceBlocked, string $previewHostname, Document $devKey, ?Key $apiKey, Cors $cors, Authorization $authorization, DeletePublisher $publisherForDeletes, int $executionsRetentionCount, array $params) { /* * Appwrite Router */ @@ -844,14 +845,14 @@ Http::init() // Only run Router when external domain if (!\in_array($hostname, $platformHostnames) || !empty($previewHostname)) { if (router($utopia, $dbForPlatform, $getProjectDB, $swooleRequest, $request, $response, $log, $queueForEvents, $bus, $executor, $geodb, $isResourceBlocked, $platform, $previewHostname, $authorization, $apiKey, $publisherForDeletes, $executionsRetentionCount)) { - $utopia->getRoute()?->label('router', true); + $utopia->match($request)?->route->label('router', true); } } /* * Request format */ - $route = $utopia->getRoute(); + $route = $utopia->match($request)?->route; $request->setRoute($route); if ($route === null) { @@ -876,7 +877,7 @@ Http::init() } if (version_compare($requestFormat, '1.8.0', '<')) { $dbForProject = $getProjectDB($project); - $request->addFilter(new RequestV20($dbForProject, $route->getPathValues($request))); + $request->addFilter(new RequestV20($dbForProject, $params)); } if (version_compare($requestFormat, '1.9.0', '<')) { $request->addFilter(new RequestV21()); @@ -1154,7 +1155,7 @@ Http::options() // Only run Router when external domain if (!in_array($request->getHostname(), $platformHostnames) || !empty($previewHostname)) { if (router($utopia, $dbForPlatform, $getProjectDB, $swooleRequest, $request, $response, $log, $queueForEvents, $bus, $executor, $geodb, $isResourceBlocked, $platform, $previewHostname, $authorization, $apiKey, $publisherForDeletes, $executionsRetentionCount)) { - $utopia->getRoute()?->label('router', true); + $utopia->match($request)?->route->label('router', true); } } @@ -1189,7 +1190,7 @@ Http::error() ->inject('authorization') ->action(function (Throwable $error, Http $utopia, Request $request, Response $response, Document $project, ?Logger $logger, Log $log, Bus $bus, Document $devKey, Authorization $authorization) { $version = System::getEnv('_APP_VERSION', 'UNKNOWN'); - $route = $utopia->getRoute(); + $route = $utopia->match($request)?->route; $class = \get_class($error); $code = $error->getCode(); $message = $error->getMessage(); @@ -1555,7 +1556,7 @@ Http::get('/robots.txt') $response->text($template->render(false)); } else { if (router($utopia, $dbForPlatform, $getProjectDB, $swooleRequest, $request, $response, $log, $queueForEvents, $bus, $executor, $geodb, $isResourceBlocked, $platform, $previewHostname, $authorization, $apiKey, $publisherForDeletes, $executionsRetentionCount)) { - $utopia->getRoute()?->label('router', true); + $utopia->match($request)?->route->label('router', true); } } }); @@ -1589,7 +1590,7 @@ Http::get('/humans.txt') $response->text($template->render(false)); } else { if (router($utopia, $dbForPlatform, $getProjectDB, $swooleRequest, $request, $response, $log, $queueForEvents, $bus, $executor, $geodb, $isResourceBlocked, $platform, $previewHostname, $authorization, $apiKey, $publisherForDeletes, $executionsRetentionCount)) { - $utopia->getRoute()?->label('router', true); + $utopia->match($request)?->route->label('router', true); } } }); diff --git a/app/controllers/mock.php b/app/controllers/mock.php index 4e92b3482d..00389085af 100644 --- a/app/controllers/mock.php +++ b/app/controllers/mock.php @@ -13,6 +13,7 @@ use Utopia\Database\Helpers\Permission; use Utopia\Database\Helpers\Role; use Utopia\Database\Validator\UID; use Utopia\Http\Http; +use Utopia\Http\Route; use Utopia\Locale\Locale; use Utopia\System\System; use Utopia\Validator\Text; @@ -283,13 +284,11 @@ Http::get('/v1/mock/github/callback') Http::shutdown() ->groups(['mock']) - ->inject('utopia') ->inject('response') - ->inject('request') - ->action(function (Http $utopia, Response $response, Request $request) { + ->inject('route') + ->action(function (Response $response, Route $route) { $result = []; - $route = $utopia->getRoute(); $path = APP_STORAGE_CACHE . '/tests.json'; $tests = (\file_exists($path)) ? \json_decode(\file_get_contents($path), true) : []; diff --git a/app/controllers/shared/api.php b/app/controllers/shared/api.php index 6e5167660a..494bd3da28 100644 --- a/app/controllers/shared/api.php +++ b/app/controllers/shared/api.php @@ -37,6 +37,7 @@ use Utopia\Database\Validator\Authorization; use Utopia\Database\Validator\Authorization\Input; use Utopia\Database\Validator\Roles; use Utopia\Http\Http; +use Utopia\Http\Route; use Utopia\Span\Span; use Utopia\System\System; use Utopia\Telemetry\Adapter as Telemetry; @@ -85,7 +86,7 @@ $parseLabel = function (string $label, array $responsePayload, array $requestPar Http::init() ->groups(['api']) - ->inject('utopia') + ->inject('route') ->inject('request') ->inject('dbForPlatform') ->inject('dbForProject') @@ -98,11 +99,7 @@ Http::init() ->inject('team') ->inject('apiKey') ->inject('authorization') - ->action(function (Http $utopia, Request $request, Database $dbForPlatform, Database $dbForProject, AuditContext $auditContext, Document $project, User $user, ?Document $session, array $servers, string $mode, Document $team, ?Key $apiKey, Authorization $authorization) { - $route = $utopia->getRoute(); - if ($route === null) { - throw new AppwriteException(AppwriteException::GENERAL_ROUTE_NOT_FOUND); - } + ->action(function (Route $route, Request $request, Database $dbForPlatform, Database $dbForProject, AuditContext $auditContext, Document $project, User $user, ?Document $session, array $servers, string $mode, Document $team, ?Key $apiKey, Authorization $authorization) { /** * Handle user authentication and session validation. @@ -178,8 +175,8 @@ Http::init() $role = $apiKey->getRole(); $scopes = $apiKey->getScopes(); - // Handle special app role case - if ($apiKey->getRole() === User::ROLE_APPS) { + // Handle special key role case + if ($apiKey->getRole() === User::ROLE_KEYS) { // Disable authorization checks for project API keys // Dynamic supported for backwards compatibility if (($apiKey->getType() === API_KEY_STANDARD || $apiKey->getType() === API_KEY_EPHEMERAL || $apiKey->getType() === 'dynamic') && $apiKey->getProjectId() === $project->getId()) { @@ -189,7 +186,7 @@ Http::init() $user = new User([ '$id' => '', 'status' => true, - 'type' => ACTIVITY_TYPE_KEY_PROJECT, + 'type' => ACTOR_TYPE_KEY_PROJECT, 'email' => 'app.' . $project->getId() . '@service.' . $request->getHostname(), 'password' => '', 'name' => $apiKey->getName(), @@ -261,9 +258,9 @@ Http::init() $userClone = clone $user; $userClone->setAttribute('type', match ($apiKey->getType()) { - API_KEY_STANDARD => ACTIVITY_TYPE_KEY_PROJECT, - API_KEY_ACCOUNT => ACTIVITY_TYPE_KEY_ACCOUNT, - default => ACTIVITY_TYPE_KEY_ORGANIZATION, + API_KEY_STANDARD => ACTOR_TYPE_KEY_PROJECT, + API_KEY_ACCOUNT => ACTOR_TYPE_KEY_ACCOUNT, + default => ACTOR_TYPE_KEY_ORGANIZATION, }); $auditContext->user = $userClone; } @@ -428,7 +425,7 @@ Http::init() if ( array_key_exists($namespace, $project->getAttribute('services', [])) && ! $project->getAttribute('services', [])[$namespace] - && ! ($user->isPrivileged($authorization->getRoles()) || $user->isApp($authorization->getRoles())) + && ! ($user->isPrivileged($authorization->getRoles()) || $user->isKey($authorization->getRoles())) ) { throw new Exception(Exception::GENERAL_SERVICE_DISABLED); } @@ -438,7 +435,7 @@ Http::init() if ( array_key_exists('rest', $project->getAttribute('apis', [])) && ! $project->getAttribute('apis', [])['rest'] - && ! ($user->isPrivileged($authorization->getRoles()) || $user->isApp($authorization->getRoles())) + && ! ($user->isPrivileged($authorization->getRoles()) || $user->isKey($authorization->getRoles())) ) { throw new AppwriteException(AppwriteException::GENERAL_API_DISABLED); } @@ -477,7 +474,7 @@ Http::init() Http::init() ->groups(['api']) - ->inject('utopia') + ->inject('route') ->inject('request') ->inject('response') ->inject('project') @@ -485,21 +482,16 @@ Http::init() ->inject('timelimit') ->inject('devKey') ->inject('authorization') - ->action(function (Http $utopia, Request $request, Response $response, Document $project, User $user, callable $timelimit, Document $devKey, Authorization $authorization) { + ->action(function (Route $route, Request $request, Response $response, Document $project, User $user, callable $timelimit, Document $devKey, Authorization $authorization) { $response->setUser($user); $request->setUser($user); $roles = $authorization->getRoles(); $shouldCheckAbuse = System::getEnv('_APP_OPTIONS_ABUSE', 'enabled') !== 'disabled' - && ! $user->isApp($roles) + && ! $user->isKey($roles) && ! $user->isPrivileged($roles) && $devKey->isEmpty(); - $route = $utopia->getRoute(); - if ($route === null) { - throw new AppwriteException(AppwriteException::GENERAL_ROUTE_NOT_FOUND); - } - $abuseKeyLabel = $route->getLabel('abuse-key', 'url:{url},ip:{ip}'); $abuseKeyLabel = (! is_array($abuseKeyLabel)) ? [$abuseKeyLabel] : $abuseKeyLabel; $closestLimit = null; @@ -556,7 +548,7 @@ Http::init() Http::init() ->groups(['api']) - ->inject('utopia') + ->inject('route') ->inject('request') ->inject('response') ->inject('project') @@ -574,17 +566,12 @@ Http::init() ->inject('platform') ->inject('authorization') ->inject('cacheControlForStorage') - ->action(function (Http $utopia, Request $request, Response $response, Document $project, User $user, Event $queueForEvents, AuditContext $auditContext, Context $usage, FunctionPublisher $publisherForFunctions, Database $dbForProject, Document $resourceToken, string $mode, ?Key $apiKey, array $plan, Telemetry $telemetry, array $platform, Authorization $authorization, callable $cacheControlForStorage) { + ->action(function (Route $route, Request $request, Response $response, Document $project, User $user, Event $queueForEvents, AuditContext $auditContext, Context $usage, FunctionPublisher $publisherForFunctions, Database $dbForProject, Document $resourceToken, string $mode, ?Key $apiKey, array $plan, Telemetry $telemetry, array $platform, Authorization $authorization, callable $cacheControlForStorage) { $response->setUser($user); $request->setUser($user); - $route = $utopia->getRoute(); - if ($route === null) { - throw new AppwriteException(AppwriteException::GENERAL_ROUTE_NOT_FOUND); - } - - $path = $route->getMatchedPath(); + $path = $route->getPath(); $databaseType = match (true) { str_contains($path, '/documentsdb') => DATABASE_TYPE_DOCUMENTSDB, str_contains($path, '/vectorsdb') => DATABASE_TYPE_VECTORSDB, @@ -615,7 +602,7 @@ Http::init() $userClone = clone $user; // $user doesn't support `type` and can cause unintended effects. if (empty($user->getAttribute('type'))) { - $userClone->setAttribute('type', $mode === APP_MODE_ADMIN ? ACTIVITY_TYPE_ADMIN : ACTIVITY_TYPE_USER); + $userClone->setAttribute('type', $mode === APP_MODE_ADMIN ? ACTOR_TYPE_ADMIN : ACTOR_TYPE_USER); } $auditContext->user = $userClone; } @@ -623,9 +610,8 @@ Http::init() $useCache = $route->getLabel('cache', false); $storageCacheOperationsCounter = $telemetry->createCounter('storage.cache.operations.load'); if ($useCache) { - $route = $utopia->match($request); $roles = $authorization->getRoles(); - $isAppUser = $user->isApp($roles); + $isAppUser = $user->isKey($roles); $isImageTransformation = $route->getPath() === '/v1/storage/buckets/:bucketId/files/:fileId/preview'; $isDisabled = isset($plan['imageTransformations']) && $plan['imageTransformations'] === -1 && ! $user->isPrivileged($roles); @@ -761,12 +747,11 @@ Http::init() */ Http::shutdown() ->groups(['session']) - ->inject('utopia') ->inject('request') ->inject('response') ->inject('project') ->inject('dbForProject') - ->action(function (Http $utopia, Request $request, Response $response, Document $project, Database $dbForProject) { + ->action(function (Request $request, Response $response, Document $project, Database $dbForProject) { $sessionLimit = $project->getAttribute('auths', [])['maxSessions'] ?? 0; if ($sessionLimit === 0) { @@ -800,7 +785,7 @@ Http::shutdown() Http::shutdown() ->groups(['api']) - ->inject('utopia') + ->inject('route') ->inject('request') ->inject('response') ->inject('project') @@ -820,7 +805,7 @@ Http::shutdown() ->inject('bus') ->inject('apiKey') ->inject('mode') - ->action(function (Http $utopia, Request $request, Response $response, Document $project, User $user, Event $queueForEvents, AuditContext $auditContext, Audit $publisherForAudits, Context $usage, UsagePublisher $publisherForUsage, FunctionPublisher $publisherForFunctions, Event $queueForWebhooks, Realtime $queueForRealtime, Database $dbForProject, Authorization $authorization, callable $timelimit, EventProcessor $eventProcessor, Bus $bus, ?Key $apiKey, string $mode) use ($parseLabel) { + ->action(function (Route $route, Request $request, Response $response, Document $project, User $user, Event $queueForEvents, AuditContext $auditContext, Audit $publisherForAudits, Context $usage, UsagePublisher $publisherForUsage, FunctionPublisher $publisherForFunctions, Event $queueForWebhooks, Realtime $queueForRealtime, Database $dbForProject, Authorization $authorization, callable $timelimit, EventProcessor $eventProcessor, Bus $bus, ?Key $apiKey, string $mode) use ($parseLabel) { $responsePayload = $response->getPayload(); @@ -876,7 +861,6 @@ Http::shutdown() } } - $route = $utopia->getRoute(); $requestParams = $route->getParamsValues(); /** @@ -929,7 +913,7 @@ Http::shutdown() $userClone = clone $user; // $user doesn't support `type` and can cause unintended effects. if (empty($user->getAttribute('type'))) { - $userClone->setAttribute('type', $mode === APP_MODE_ADMIN ? ACTIVITY_TYPE_ADMIN : ACTIVITY_TYPE_USER); + $userClone->setAttribute('type', $mode === APP_MODE_ADMIN ? ACTOR_TYPE_ADMIN : ACTOR_TYPE_USER); } $auditContext->user = $userClone; } elseif ($auditContext->user === null || $auditContext->user->isEmpty()) { @@ -944,7 +928,7 @@ Http::shutdown() $user = new User([ '$id' => '', 'status' => true, - 'type' => ACTIVITY_TYPE_GUEST, + 'type' => ACTOR_TYPE_GUEST, 'email' => 'guest.' . $project->getId() . '@service.' . $request->getHostname(), 'password' => '', 'name' => 'Guest', diff --git a/app/controllers/shared/api/auth.php b/app/controllers/shared/api/auth.php index db98d97bf5..dfb384f893 100644 --- a/app/controllers/shared/api/auth.php +++ b/app/controllers/shared/api/auth.php @@ -9,6 +9,7 @@ use Utopia\Database\DateTime; use Utopia\Database\Document; use Utopia\Database\Validator\Authorization; use Utopia\Http\Http; +use Utopia\Http\Route; use Utopia\System\System; Http::init() @@ -32,13 +33,13 @@ Http::init() Http::init() ->groups(['auth']) - ->inject('utopia') + ->inject('route') ->inject('request') ->inject('project') ->inject('geodb') ->inject('user') ->inject('authorization') - ->action(function (Http $utopia, Request $request, Document $project, Reader $geodb, User $user, Authorization $authorization) { + ->action(function (Route $route, Request $request, Document $project, Reader $geodb, User $user, Authorization $authorization) { $denylist = System::getEnv('_APP_CONSOLE_COUNTRIES_DENYLIST', ''); if (!empty($denylist && $project->getId() === 'console')) { $countries = explode(',', $denylist); @@ -49,10 +50,8 @@ Http::init() } } - $route = $utopia->match($request); - $isPrivilegedUser = $user->isPrivileged($authorization->getRoles()); - $isAppUser = $user->isApp($authorization->getRoles()); + $isAppUser = $user->isKey($authorization->getRoles()); if ($isAppUser || $isPrivilegedUser) { // Skip limits for app and console devs return; diff --git a/app/http.php b/app/http.php index 6dc415f000..6226026a16 100644 --- a/app/http.php +++ b/app/http.php @@ -539,7 +539,7 @@ $swoole->onRequest(function ($utopiaRequest, $utopiaResponse) use ($files, $swoo $app->run($request, $response); - $route = $app->getRoute(); + $route = $app->match($request)?->route; Span::add('http.path', $route?->getPath() ?? 'unknown'); } catch (\Throwable $th) { Span::error($th); @@ -555,7 +555,7 @@ $swoole->onRequest(function ($utopiaRequest, $utopiaResponse) use ($files, $swoo // All good, user is optional information for logger } - $route = $app->getRoute(); + $route = $app->match($request)?->route; $log = $app->context()->get("log"); diff --git a/app/init/constants.php b/app/init/constants.php index b271b56a14..4bb607e2fb 100644 --- a/app/init/constants.php +++ b/app/init/constants.php @@ -159,14 +159,14 @@ const SESSION_PROVIDER_TOKEN = 'token'; const SESSION_PROVIDER_SERVER = 'server'; /** - * Activity associated with user or the app. + * Actor that performed the request (user, admin, guest, or API key). */ -const ACTIVITY_TYPE_USER = 'user'; -const ACTIVITY_TYPE_ADMIN = 'admin'; -const ACTIVITY_TYPE_GUEST = 'guest'; -const ACTIVITY_TYPE_KEY_PROJECT = 'keyProject'; -const ACTIVITY_TYPE_KEY_ACCOUNT = 'keyAccount'; -const ACTIVITY_TYPE_KEY_ORGANIZATION = 'keyOrganization'; +const ACTOR_TYPE_USER = 'user'; +const ACTOR_TYPE_ADMIN = 'admin'; +const ACTOR_TYPE_GUEST = 'guest'; +const ACTOR_TYPE_KEY_PROJECT = 'keyProject'; +const ACTOR_TYPE_KEY_ACCOUNT = 'keyAccount'; +const ACTOR_TYPE_KEY_ORGANIZATION = 'keyOrganization'; /** * MFA diff --git a/app/init/models.php b/app/init/models.php index 0d1cb061ea..08ebc3af23 100644 --- a/app/init/models.php +++ b/app/init/models.php @@ -266,7 +266,7 @@ Response::setModel(new BaseList('Frameworks List', Response::MODEL_FRAMEWORK_LIS Response::setModel(new BaseList('Runtimes List', Response::MODEL_RUNTIME_LIST, 'runtimes', Response::MODEL_RUNTIME)); Response::setModel(new BaseList('Deployments List', Response::MODEL_DEPLOYMENT_LIST, 'deployments', Response::MODEL_DEPLOYMENT)); Response::setModel(new BaseList('Executions List', Response::MODEL_EXECUTION_LIST, 'executions', Response::MODEL_EXECUTION)); -Response::setModel(new BaseList('Projects List', Response::MODEL_PROJECT_LIST, 'projects', Response::MODEL_PROJECT, true, false)); +Response::setModel(new BaseList('Projects List', Response::MODEL_PROJECT_LIST, 'projects', Response::MODEL_PROJECT, true, true)); Response::setModel(new BaseList('Webhooks List', Response::MODEL_WEBHOOK_LIST, 'webhooks', Response::MODEL_WEBHOOK, true, true)); Response::setModel(new BaseList('API Keys List', Response::MODEL_KEY_LIST, 'keys', Response::MODEL_KEY, true, true)); Response::setModel(new BaseList('Dev Keys List', Response::MODEL_DEV_KEY_LIST, 'devKeys', Response::MODEL_DEV_KEY, true, false)); diff --git a/app/init/resources/request.php b/app/init/resources/request.php index 85d8db3698..8c55eaf3e0 100644 --- a/app/init/resources/request.php +++ b/app/init/resources/request.php @@ -596,7 +596,7 @@ return function (Container $context): void { // These endpoints moved from /v1/projects/:projectId/ to /v1/ // When accessed via the old alias path, extract projectId from the URI $deprecatedProjectPathPrefix = '/v1/projects/'; - $route = $utopia->match($request); + $route = $utopia->match($request)?->route; if (!empty($route)) { $isDeprecatedAlias = \str_starts_with($request->getURI(), $deprecatedProjectPathPrefix) && !\str_starts_with($route->getPath(), $deprecatedProjectPathPrefix); @@ -1093,7 +1093,7 @@ return function (Container $context): void { if ($project->getId() !== 'console') { $teamInternalId = $project->getAttribute('teamInternalId', ''); } else { - $route = $utopia->match($request); + $route = $utopia->match($request)?->route; $path = ! empty($route) ? $route->getPath() : $request->getURI(); $orgHeader = $request->getHeader('x-appwrite-organization', ''); if (str_starts_with($path, '/v1/projects/:projectId')) { diff --git a/app/realtime.php b/app/realtime.php index d8b70960b8..ce2dc41e54 100644 --- a/app/realtime.php +++ b/app/realtime.php @@ -858,7 +858,7 @@ $server->onOpen(function (int $connection, SwooleRequest $request) use ($server, $websocketEnabled = $apis['websocket'] ?? $apis['realtime'] ?? true; if ( !$websocketEnabled - && !($user->isPrivileged($authorization->getRoles()) || $user->isApp($authorization->getRoles())) + && !($user->isPrivileged($authorization->getRoles()) || $user->isKey($authorization->getRoles())) ) { throw new AppwriteException(AppwriteException::GENERAL_API_DISABLED); } diff --git a/composer.json b/composer.json index d4472c6868..3ddd6ae119 100644 --- a/composer.json +++ b/composer.json @@ -54,7 +54,7 @@ "utopia-php/abuse": "1.3.*", "utopia-php/agents": "1.2.*", "utopia-php/analytics": "0.15.*", - "utopia-php/audit": "2.3.*", + "utopia-php/audit": "^2.4", "utopia-php/auth": "0.5.*", "utopia-php/cache": "^3.0", "utopia-php/cli": "0.23.*", @@ -67,7 +67,7 @@ "utopia-php/emails": "0.7.*", "utopia-php/dns": "1.7.*", "utopia-php/dsn": "0.2.1", - "utopia-php/http": "^2.0@RC", + "utopia-php/http": "2.0.0-rc2", "utopia-php/fetch": "^1.1", "utopia-php/validators": "0.2.*", "utopia-php/image": "0.8.*", diff --git a/composer.lock b/composer.lock index a954399162..54a6faab3a 100644 --- a/composer.lock +++ b/composer.lock @@ -4,7 +4,7 @@ "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "This file is @generated automatically" ], - "content-hash": "b645c7da1728536497fe8186b158f9c6", + "content-hash": "b092fffec11494aea10b0c823b7837b8", "packages": [ { "name": "adhocore/jwt", @@ -161,16 +161,16 @@ }, { "name": "appwrite/php-runtimes", - "version": "0.20.0", + "version": "0.20.1", "source": { "type": "git", "url": "https://github.com/appwrite/runtimes.git", - "reference": "7d9b7f4eef5c0a142a60907b06de2219d025c5c3" + "reference": "e9213dfe9fff1b67de77aa61dbcae5f4ca10b6d6" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/appwrite/runtimes/zipball/7d9b7f4eef5c0a142a60907b06de2219d025c5c3", - "reference": "7d9b7f4eef5c0a142a60907b06de2219d025c5c3", + "url": "https://api.github.com/repos/appwrite/runtimes/zipball/e9213dfe9fff1b67de77aa61dbcae5f4ca10b6d6", + "reference": "e9213dfe9fff1b67de77aa61dbcae5f4ca10b6d6", "shasum": "" }, "require": { @@ -210,9 +210,9 @@ ], "support": { "issues": "https://github.com/appwrite/runtimes/issues", - "source": "https://github.com/appwrite/runtimes/tree/0.20.0" + "source": "https://github.com/appwrite/runtimes/tree/0.20.1" }, - "time": "2026-05-01T07:47:07+00:00" + "time": "2026-05-24T03:00:39+00:00" }, { "name": "brick/math", @@ -3510,16 +3510,16 @@ }, { "name": "utopia-php/audit", - "version": "2.3.2", + "version": "2.4.1", "source": { "type": "git", "url": "https://github.com/utopia-php/audit.git", - "reference": "e7b4049fc2ee9be34bcc18771fa593db3b0e9fe3" + "reference": "eddd79d93f23ed2851c0df2b1e2e2dfb25ba06c6" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/utopia-php/audit/zipball/e7b4049fc2ee9be34bcc18771fa593db3b0e9fe3", - "reference": "e7b4049fc2ee9be34bcc18771fa593db3b0e9fe3", + "url": "https://api.github.com/repos/utopia-php/audit/zipball/eddd79d93f23ed2851c0df2b1e2e2dfb25ba06c6", + "reference": "eddd79d93f23ed2851c0df2b1e2e2dfb25ba06c6", "shasum": "" }, "require": { @@ -3554,9 +3554,9 @@ ], "support": { "issues": "https://github.com/utopia-php/audit/issues", - "source": "https://github.com/utopia-php/audit/tree/2.3.2" + "source": "https://github.com/utopia-php/audit/tree/2.4.1" }, - "time": "2026-05-14T04:00:37+00:00" + "time": "2026-05-20T06:25:45+00:00" }, { "name": "utopia-php/auth", @@ -4245,16 +4245,16 @@ }, { "name": "utopia-php/emails", - "version": "0.7.0", + "version": "0.7.1", "source": { "type": "git", "url": "https://github.com/utopia-php/emails.git", - "reference": "115e24aa908e2b1f06c7ff3b94434a0bdbed9107" + "reference": "a5f1d111e5023918731f2de96d348f5b6a0de143" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/utopia-php/emails/zipball/115e24aa908e2b1f06c7ff3b94434a0bdbed9107", - "reference": "115e24aa908e2b1f06c7ff3b94434a0bdbed9107", + "url": "https://api.github.com/repos/utopia-php/emails/zipball/a5f1d111e5023918731f2de96d348f5b6a0de143", + "reference": "a5f1d111e5023918731f2de96d348f5b6a0de143", "shasum": "" }, "require": { @@ -4300,9 +4300,9 @@ ], "support": { "issues": "https://github.com/utopia-php/emails/issues", - "source": "https://github.com/utopia-php/emails/tree/0.7.0" + "source": "https://github.com/utopia-php/emails/tree/0.7.1" }, - "time": "2026-05-13T05:01:26+00:00" + "time": "2026-05-20T13:05:30+00:00" }, { "name": "utopia-php/fetch", @@ -4346,16 +4346,16 @@ }, { "name": "utopia-php/http", - "version": "2.0.0-rc1", + "version": "2.0.0-rc2", "source": { "type": "git", "url": "https://github.com/utopia-php/http.git", - "reference": "3e3b431d443844c6bf810120dee735f45880856f" + "reference": "17f3d5e966ada8a5c041717436f069f269aef2b3" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/utopia-php/http/zipball/3e3b431d443844c6bf810120dee735f45880856f", - "reference": "3e3b431d443844c6bf810120dee735f45880856f", + "url": "https://api.github.com/repos/utopia-php/http/zipball/17f3d5e966ada8a5c041717436f069f269aef2b3", + "reference": "17f3d5e966ada8a5c041717436f069f269aef2b3", "shasum": "" }, "require": { @@ -4396,9 +4396,9 @@ ], "support": { "issues": "https://github.com/utopia-php/http/issues", - "source": "https://github.com/utopia-php/http/tree/2.0.0-rc1" + "source": "https://github.com/utopia-php/http/tree/2.0.0-rc2" }, - "time": "2026-05-05T15:00:03+00:00" + "time": "2026-05-20T11:13:49+00:00" }, { "name": "utopia-php/image", @@ -5355,16 +5355,16 @@ }, { "name": "utopia-php/validators", - "version": "0.2.3", + "version": "0.2.4", "source": { "type": "git", "url": "https://github.com/utopia-php/validators.git", - "reference": "9770269c8ed8e6909934965fa8722103c7434c23" + "reference": "b4ee60db4dbae5ffbe53968d01f69b6941251576" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/utopia-php/validators/zipball/9770269c8ed8e6909934965fa8722103c7434c23", - "reference": "9770269c8ed8e6909934965fa8722103c7434c23", + "url": "https://api.github.com/repos/utopia-php/validators/zipball/b4ee60db4dbae5ffbe53968d01f69b6941251576", + "reference": "b4ee60db4dbae5ffbe53968d01f69b6941251576", "shasum": "" }, "require": { @@ -5394,9 +5394,9 @@ ], "support": { "issues": "https://github.com/utopia-php/validators/issues", - "source": "https://github.com/utopia-php/validators/tree/0.2.3" + "source": "https://github.com/utopia-php/validators/tree/0.2.4" }, - "time": "2026-05-14T08:05:44+00:00" + "time": "2026-05-21T12:47:43+00:00" }, { "name": "utopia-php/vcs", @@ -5638,16 +5638,16 @@ "packages-dev": [ { "name": "appwrite/sdk-generator", - "version": "1.31.0", + "version": "1.31.1", "source": { "type": "git", "url": "https://github.com/appwrite/sdk-generator.git", - "reference": "a7119db15696131a86d477b3bed348beda85523f" + "reference": "5699f6da951aef9378fabdcf12f40a9a54fb3128" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/appwrite/sdk-generator/zipball/a7119db15696131a86d477b3bed348beda85523f", - "reference": "a7119db15696131a86d477b3bed348beda85523f", + "url": "https://api.github.com/repos/appwrite/sdk-generator/zipball/5699f6da951aef9378fabdcf12f40a9a54fb3128", + "reference": "5699f6da951aef9378fabdcf12f40a9a54fb3128", "shasum": "" }, "require": { @@ -5656,7 +5656,7 @@ "ext-mbstring": "*", "matthiasmullie/minify": "1.3.*", "php": ">=8.3", - "twig/twig": "3.14.*" + "twig/twig": "3.26.*" }, "require-dev": { "brianium/paratest": "7.*", @@ -5683,9 +5683,9 @@ "description": "Appwrite PHP library for generating API SDKs for multiple programming languages and platforms", "support": { "issues": "https://github.com/appwrite/sdk-generator/issues", - "source": "https://github.com/appwrite/sdk-generator/tree/1.31.0" + "source": "https://github.com/appwrite/sdk-generator/tree/1.31.1" }, - "time": "2026-05-20T11:16:09+00:00" + "time": "2026-05-20T22:22:59+00:00" }, { "name": "brianium/paratest", @@ -8213,86 +8213,6 @@ ], "time": "2024-09-09T11:45:10+00:00" }, - { - "name": "symfony/polyfill-php81", - "version": "v1.37.0", - "source": { - "type": "git", - "url": "https://github.com/symfony/polyfill-php81.git", - "reference": "4a4cfc2d253c21a5ad0e53071df248ed48c6ce5c" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/symfony/polyfill-php81/zipball/4a4cfc2d253c21a5ad0e53071df248ed48c6ce5c", - "reference": "4a4cfc2d253c21a5ad0e53071df248ed48c6ce5c", - "shasum": "" - }, - "require": { - "php": ">=7.2" - }, - "type": "library", - "extra": { - "thanks": { - "url": "https://github.com/symfony/polyfill", - "name": "symfony/polyfill" - } - }, - "autoload": { - "files": [ - "bootstrap.php" - ], - "psr-4": { - "Symfony\\Polyfill\\Php81\\": "" - }, - "classmap": [ - "Resources/stubs" - ] - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "authors": [ - { - "name": "Nicolas Grekas", - "email": "p@tchwork.com" - }, - { - "name": "Symfony Community", - "homepage": "https://symfony.com/contributors" - } - ], - "description": "Symfony polyfill backporting some PHP 8.1+ features to lower PHP versions", - "homepage": "https://symfony.com", - "keywords": [ - "compatibility", - "polyfill", - "portable", - "shim" - ], - "support": { - "source": "https://github.com/symfony/polyfill-php81/tree/v1.37.0" - }, - "funding": [ - { - "url": "https://symfony.com/sponsor", - "type": "custom" - }, - { - "url": "https://github.com/fabpot", - "type": "github" - }, - { - "url": "https://github.com/nicolas-grekas", - "type": "github" - }, - { - "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", - "type": "tidelift" - } - ], - "time": "2024-09-09T11:45:10+00:00" - }, { "name": "symfony/process", "version": "v8.0.11", @@ -8549,26 +8469,27 @@ }, { "name": "twig/twig", - "version": "3.14.x-dev", + "version": "v3.26.0", "source": { "type": "git", "url": "https://github.com/twigphp/Twig.git", - "reference": "0b6f9d8370bb3b7f1ce5313ed8feb0fafd6e399a" + "reference": "1fcae487b180d78e6351f4e0afa91f9eab96a2bc" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/twigphp/Twig/zipball/0b6f9d8370bb3b7f1ce5313ed8feb0fafd6e399a", - "reference": "0b6f9d8370bb3b7f1ce5313ed8feb0fafd6e399a", + "url": "https://api.github.com/repos/twigphp/Twig/zipball/1fcae487b180d78e6351f4e0afa91f9eab96a2bc", + "reference": "1fcae487b180d78e6351f4e0afa91f9eab96a2bc", "shasum": "" }, "require": { - "php": ">=8.0.2", + "php": ">=8.1.0", "symfony/deprecation-contracts": "^2.5|^3", "symfony/polyfill-ctype": "^1.8", - "symfony/polyfill-mbstring": "^1.3", - "symfony/polyfill-php81": "^1.29" + "symfony/polyfill-mbstring": "^1.3" }, "require-dev": { + "php-cs-fixer/shim": "^3.0@stable", + "phpstan/phpstan": "^2.0@stable", "psr/container": "^1.0|^2.0", "symfony/phpunit-bridge": "^5.4.9|^6.4|^7.0" }, @@ -8612,7 +8533,7 @@ ], "support": { "issues": "https://github.com/twigphp/Twig/issues", - "source": "https://github.com/twigphp/Twig/tree/v3.14.2" + "source": "https://github.com/twigphp/Twig/tree/v3.26.0" }, "funding": [ { @@ -8624,14 +8545,12 @@ "type": "tidelift" } ], - "time": "2024-11-07T12:36:22+00:00" + "time": "2026-05-20T07:31:59+00:00" } ], "aliases": [], "minimum-stability": "dev", - "stability-flags": { - "utopia-php/http": 5 - }, + "stability-flags": {}, "prefer-stable": true, "prefer-lowest": false, "platform": { diff --git a/docs/references/projects/create-jwt.md b/docs/references/projects/create-jwt.md deleted file mode 100644 index 9a6f8ebf6b..0000000000 --- a/docs/references/projects/create-jwt.md +++ /dev/null @@ -1 +0,0 @@ -Create a new JWT token. This token can be used to authenticate users with custom scopes and expiration time. \ No newline at end of file diff --git a/docs/references/projects/create-smtp-test.md b/docs/references/projects/create-smtp-test.md deleted file mode 100644 index 63cea9d21f..0000000000 --- a/docs/references/projects/create-smtp-test.md +++ /dev/null @@ -1 +0,0 @@ -Send a test email to verify SMTP configuration. \ No newline at end of file diff --git a/docs/references/projects/create.md b/docs/references/projects/create.md deleted file mode 100644 index d502c269ef..0000000000 --- a/docs/references/projects/create.md +++ /dev/null @@ -1 +0,0 @@ -Create a new project. You can create a maximum of 100 projects per account. \ No newline at end of file diff --git a/docs/references/projects/delete-email-template.md b/docs/references/projects/delete-email-template.md deleted file mode 100644 index 332b1d6117..0000000000 --- a/docs/references/projects/delete-email-template.md +++ /dev/null @@ -1 +0,0 @@ -Reset a custom email template to its default value. This endpoint removes any custom content and restores the template to its original state. \ No newline at end of file diff --git a/docs/references/projects/delete.md b/docs/references/projects/delete.md deleted file mode 100644 index 4a8070c082..0000000000 --- a/docs/references/projects/delete.md +++ /dev/null @@ -1 +0,0 @@ -Delete a project by its unique ID. \ No newline at end of file diff --git a/docs/references/projects/get-email-template.md b/docs/references/projects/get-email-template.md deleted file mode 100644 index 6119a0a183..0000000000 --- a/docs/references/projects/get-email-template.md +++ /dev/null @@ -1 +0,0 @@ -Get a custom email template for the specified locale and type. This endpoint returns the template content, subject, and other configuration details. \ No newline at end of file diff --git a/docs/references/projects/get.md b/docs/references/projects/get.md deleted file mode 100644 index b7a1165adc..0000000000 --- a/docs/references/projects/get.md +++ /dev/null @@ -1 +0,0 @@ -Get a project by its unique ID. This endpoint allows you to retrieve the project's details, including its name, description, team, region, and other metadata. \ No newline at end of file diff --git a/docs/references/projects/update-auth-status.md b/docs/references/projects/update-auth-status.md deleted file mode 100644 index 5d39ec29c4..0000000000 --- a/docs/references/projects/update-auth-status.md +++ /dev/null @@ -1 +0,0 @@ -Update the status of a specific authentication method. Use this endpoint to enable or disable different authentication methods such as email, magic urls or sms in your project. \ No newline at end of file diff --git a/docs/references/projects/update-email-template.md b/docs/references/projects/update-email-template.md deleted file mode 100644 index d2bf124541..0000000000 --- a/docs/references/projects/update-email-template.md +++ /dev/null @@ -1 +0,0 @@ -Update a custom email template for the specified locale and type. Use this endpoint to modify the content of your email templates. \ No newline at end of file diff --git a/docs/references/projects/update-mock-numbers.md b/docs/references/projects/update-mock-numbers.md deleted file mode 100644 index 7fa92455c1..0000000000 --- a/docs/references/projects/update-mock-numbers.md +++ /dev/null @@ -1 +0,0 @@ -Update the list of mock phone numbers for testing. Use these numbers to bypass SMS verification in development. \ No newline at end of file diff --git a/docs/references/projects/update-oauth2.md b/docs/references/projects/update-oauth2.md deleted file mode 100644 index 2285135991..0000000000 --- a/docs/references/projects/update-oauth2.md +++ /dev/null @@ -1 +0,0 @@ -Update the OAuth2 provider configurations. Use this endpoint to set up or update the OAuth2 provider credentials or enable/disable providers. \ No newline at end of file diff --git a/docs/references/projects/update-smtp.md b/docs/references/projects/update-smtp.md deleted file mode 100644 index 7d898e1ed1..0000000000 --- a/docs/references/projects/update-smtp.md +++ /dev/null @@ -1 +0,0 @@ -Update the SMTP configuration for your project. Use this endpoint to configure your project's SMTP provider with your custom settings for sending transactional emails. \ No newline at end of file diff --git a/docs/references/projects/update.md b/docs/references/projects/update.md deleted file mode 100644 index 60c072c477..0000000000 --- a/docs/references/projects/update.md +++ /dev/null @@ -1 +0,0 @@ -Update a project by its unique ID. \ No newline at end of file diff --git a/src/Appwrite/Auth/Key.php b/src/Appwrite/Auth/Key.php index 0cbaefa4b3..f33f591d52 100644 --- a/src/Appwrite/Auth/Key.php +++ b/src/Appwrite/Auth/Key.php @@ -122,9 +122,9 @@ class Key $secret = $key; } - $role = User::ROLE_APPS; + $role = User::ROLE_KEYS; $roles = Config::getParam('roles', []); - $scopes = $roles[User::ROLE_APPS]['scopes'] ?? []; + $scopes = $roles[User::ROLE_KEYS]['scopes'] ?? []; $expired = false; $guestKey = new Key( @@ -270,7 +270,7 @@ class Key $name = $key->getAttribute('name', 'UNKNOWN'); - $role = User::ROLE_APPS; + $role = User::ROLE_KEYS; $scopes = $key->getAttribute('scopes', []); diff --git a/src/Appwrite/GraphQL/Resolvers.php b/src/Appwrite/GraphQL/Resolvers.php index 4471ab53a7..ab98e6df0c 100644 --- a/src/Appwrite/GraphQL/Resolvers.php +++ b/src/Appwrite/GraphQL/Resolvers.php @@ -342,7 +342,6 @@ class Resolvers $lock->acquire(); - $original = $utopia->getRoute(); try { $request = clone $request; $request->addHeader('x-appwrite-source', 'graphql'); @@ -363,10 +362,9 @@ class Resolvers $resolverResponse->setContentType(Response::CONTENT_TYPE_NULL); $resolverResponse->setSent(false); - $route = $utopia->match($request, fresh: true); - $request->setRoute($route); + $request->setRoute($utopia->match($request)?->route); - $utopia->execute($route, $request, $resolverResponse); + $utopia->execute($request, $resolverResponse); self::mergeResponseSideEffects($resolverResponse, $response); @@ -385,10 +383,6 @@ class Resolvers $reject($e); return; } finally { - if ($original !== null) { - $utopia->setRoute($original); - } - $lock->release(); unset(self::$locks[\spl_object_hash($utopia)]); } diff --git a/src/Appwrite/Platform/Appwrite.php b/src/Appwrite/Platform/Appwrite.php index 310d59615d..4bf3ed4768 100644 --- a/src/Appwrite/Platform/Appwrite.php +++ b/src/Appwrite/Platform/Appwrite.php @@ -11,6 +11,7 @@ use Appwrite\Platform\Modules\Databases; use Appwrite\Platform\Modules\Functions; use Appwrite\Platform\Modules\Health; use Appwrite\Platform\Modules\Migrations; +use Appwrite\Platform\Modules\Organization; use Appwrite\Platform\Modules\Presences; use Appwrite\Platform\Modules\Project; use Appwrite\Platform\Modules\Projects; @@ -44,6 +45,7 @@ class Appwrite extends Platform $this->addModule(new VCS\Module()); $this->addModule(new Webhooks\Module()); $this->addModule(new Migrations\Module()); + $this->addModule(new Organization\Module()); $this->addModule(new Project\Module()); $this->addModule(new Advisor\Module()); } diff --git a/src/Appwrite/Platform/Modules/Databases/Http/Databases/Collections/Documents/Attribute/Decrement.php b/src/Appwrite/Platform/Modules/Databases/Http/Databases/Collections/Documents/Attribute/Decrement.php index e0464f7e52..9319cffc57 100644 --- a/src/Appwrite/Platform/Modules/Databases/Http/Databases/Collections/Documents/Attribute/Decrement.php +++ b/src/Appwrite/Platform/Modules/Databases/Http/Databases/Collections/Documents/Attribute/Decrement.php @@ -93,7 +93,7 @@ class Decrement extends Action public function action(string $databaseId, string $collectionId, string $documentId, string $attribute, int|float $value, int|float|null $min, ?string $transactionId, UtopiaResponse $response, Database $dbForProject, callable $getDatabasesDB, Event $queueForEvents, Context $usage, array $plan, Authorization $authorization, User $user): void { - $isAPIKey = $user->isApp($authorization->getRoles()); + $isAPIKey = $user->isKey($authorization->getRoles()); $isPrivilegedUser = $user->isPrivileged($authorization->getRoles()); $database = $authorization->skip(fn () => $dbForProject->getDocument('databases', $databaseId)); diff --git a/src/Appwrite/Platform/Modules/Databases/Http/Databases/Collections/Documents/Attribute/Increment.php b/src/Appwrite/Platform/Modules/Databases/Http/Databases/Collections/Documents/Attribute/Increment.php index de090f9882..ba74545342 100644 --- a/src/Appwrite/Platform/Modules/Databases/Http/Databases/Collections/Documents/Attribute/Increment.php +++ b/src/Appwrite/Platform/Modules/Databases/Http/Databases/Collections/Documents/Attribute/Increment.php @@ -93,7 +93,7 @@ class Increment extends Action public function action(string $databaseId, string $collectionId, string $documentId, string $attribute, int|float $value, int|float|null $max, ?string $transactionId, UtopiaResponse $response, Database $dbForProject, callable $getDatabasesDB, Event $queueForEvents, Context $usage, array $plan, Authorization $authorization, User $user): void { - $isAPIKey = $user->isApp($authorization->getRoles()); + $isAPIKey = $user->isKey($authorization->getRoles()); $isPrivilegedUser = $user->isPrivileged($authorization->getRoles()); $database = $authorization->skip(fn () => $dbForProject->getDocument('databases', $databaseId)); diff --git a/src/Appwrite/Platform/Modules/Databases/Http/Databases/Collections/Documents/Create.php b/src/Appwrite/Platform/Modules/Databases/Http/Databases/Collections/Documents/Create.php index 2ade0b2b79..2e861dfcce 100644 --- a/src/Appwrite/Platform/Modules/Databases/Http/Databases/Collections/Documents/Create.php +++ b/src/Appwrite/Platform/Modules/Databases/Http/Databases/Collections/Documents/Create.php @@ -201,7 +201,7 @@ class Create extends Action $documents = [$data]; } - $isAPIKey = $user->isApp($authorization->getRoles()); + $isAPIKey = $user->isKey($authorization->getRoles()); $isPrivilegedUser = $user->isPrivileged($authorization->getRoles()); if ($isBulk && !$isAPIKey && !$isPrivilegedUser) { diff --git a/src/Appwrite/Platform/Modules/Databases/Http/Databases/Collections/Documents/Delete.php b/src/Appwrite/Platform/Modules/Databases/Http/Databases/Collections/Documents/Delete.php index ecc5b152ec..e0863b8ac7 100644 --- a/src/Appwrite/Platform/Modules/Databases/Http/Databases/Collections/Documents/Delete.php +++ b/src/Appwrite/Platform/Modules/Databases/Http/Databases/Collections/Documents/Delete.php @@ -107,7 +107,7 @@ class Delete extends Action ): void { $database = $authorization->skip(fn () => $dbForProject->getDocument('databases', $databaseId)); - $isAPIKey = $user->isApp($authorization->getRoles()); + $isAPIKey = $user->isKey($authorization->getRoles()); $isPrivilegedUser = $user->isPrivileged($authorization->getRoles()); if ($database->isEmpty() || (!$database->getAttribute('enabled', false) && !$isAPIKey && !$isPrivilegedUser)) { diff --git a/src/Appwrite/Platform/Modules/Databases/Http/Databases/Collections/Documents/Get.php b/src/Appwrite/Platform/Modules/Databases/Http/Databases/Collections/Documents/Get.php index 06f0e9cf1c..c40e70d667 100644 --- a/src/Appwrite/Platform/Modules/Databases/Http/Databases/Collections/Documents/Get.php +++ b/src/Appwrite/Platform/Modules/Databases/Http/Databases/Collections/Documents/Get.php @@ -78,7 +78,7 @@ class Get extends Action public function action(string $databaseId, string $collectionId, string $documentId, array $queries, ?string $transactionId, UtopiaResponse $response, Database $dbForProject, callable $getDatabasesDB, Context $usage, TransactionState $transactionState, Authorization $authorization, User $user): void { - $isAPIKey = $user->isApp($authorization->getRoles()); + $isAPIKey = $user->isKey($authorization->getRoles()); $isPrivilegedUser = $user->isPrivileged($authorization->getRoles()); $database = $authorization->skip(fn () => $dbForProject->getDocument('databases', $databaseId)); diff --git a/src/Appwrite/Platform/Modules/Databases/Http/Databases/Collections/Documents/Update.php b/src/Appwrite/Platform/Modules/Databases/Http/Databases/Collections/Documents/Update.php index b86d934ffb..4a675615da 100644 --- a/src/Appwrite/Platform/Modules/Databases/Http/Databases/Collections/Documents/Update.php +++ b/src/Appwrite/Platform/Modules/Databases/Http/Databases/Collections/Documents/Update.php @@ -103,7 +103,7 @@ class Update extends Action $database = $authorization->skip(fn () => $dbForProject->getDocument('databases', $databaseId)); - $isAPIKey = $user->isApp($authorization->getRoles()); + $isAPIKey = $user->isKey($authorization->getRoles()); $isPrivilegedUser = $user->isPrivileged($authorization->getRoles()); if ($database->isEmpty() || (!$database->getAttribute('enabled', false) && !$isAPIKey && !$isPrivilegedUser)) { diff --git a/src/Appwrite/Platform/Modules/Databases/Http/Databases/Collections/Documents/Upsert.php b/src/Appwrite/Platform/Modules/Databases/Http/Databases/Collections/Documents/Upsert.php index fb3d414097..ae940456f0 100644 --- a/src/Appwrite/Platform/Modules/Databases/Http/Databases/Collections/Documents/Upsert.php +++ b/src/Appwrite/Platform/Modules/Databases/Http/Databases/Collections/Documents/Upsert.php @@ -108,7 +108,7 @@ class Upsert extends Action throw new Exception($this->getMissingPayloadException()); } - $isAPIKey = $user->isApp($authorization->getRoles()); + $isAPIKey = $user->isKey($authorization->getRoles()); $isPrivilegedUser = $user->isPrivileged($authorization->getRoles()); $database = $authorization->skip(fn () => $dbForProject->getDocument('databases', $databaseId)); diff --git a/src/Appwrite/Platform/Modules/Databases/Http/Databases/Collections/Documents/XList.php b/src/Appwrite/Platform/Modules/Databases/Http/Databases/Collections/Documents/XList.php index fdcbced6f3..afde3d0baa 100644 --- a/src/Appwrite/Platform/Modules/Databases/Http/Databases/Collections/Documents/XList.php +++ b/src/Appwrite/Platform/Modules/Databases/Http/Databases/Collections/Documents/XList.php @@ -88,7 +88,7 @@ class XList extends Action public function action(string $databaseId, string $collectionId, array $queries, ?string $transactionId, bool $includeTotal, int $ttl, UtopiaResponse $response, Database $dbForProject, User $user, callable $getDatabasesDB, Context $usage, TransactionState $transactionState, Authorization $authorization, ?Http $utopia = null): void { - $isAPIKey = $user->isApp($authorization->getRoles()); + $isAPIKey = $user->isKey($authorization->getRoles()); $isPrivilegedUser = $user->isPrivileged($authorization->getRoles()); $database = $authorization->skip(fn () => $dbForProject->getDocument('databases', $databaseId)); diff --git a/src/Appwrite/Platform/Modules/Databases/Http/Databases/Transactions/Operations/Create.php b/src/Appwrite/Platform/Modules/Databases/Http/Databases/Transactions/Operations/Create.php index f06feccdee..8e085a9481 100644 --- a/src/Appwrite/Platform/Modules/Databases/Http/Databases/Transactions/Operations/Create.php +++ b/src/Appwrite/Platform/Modules/Databases/Http/Databases/Transactions/Operations/Create.php @@ -75,7 +75,7 @@ class Create extends Action throw new Exception(Exception::GENERAL_BAD_REQUEST, 'Operations array cannot be empty'); } - $isAPIKey = $user->isApp($authorization->getRoles()); + $isAPIKey = $user->isKey($authorization->getRoles()); $isPrivilegedUser = $user->isPrivileged($authorization->getRoles()); // API keys and admins can read any transaction, regular users need permissions diff --git a/src/Appwrite/Platform/Modules/Databases/Http/Databases/Transactions/Update.php b/src/Appwrite/Platform/Modules/Databases/Http/Databases/Transactions/Update.php index fe2ad8dbae..de057ae15e 100644 --- a/src/Appwrite/Platform/Modules/Databases/Http/Databases/Transactions/Update.php +++ b/src/Appwrite/Platform/Modules/Databases/Http/Databases/Transactions/Update.php @@ -120,7 +120,7 @@ class Update extends Action throw new Exception(Exception::GENERAL_BAD_REQUEST, 'Cannot commit and rollback at the same time'); } - $isAPIKey = $user->isApp($authorization->getRoles()); + $isAPIKey = $user->isKey($authorization->getRoles()); $isPrivilegedUser = $user->isPrivileged($authorization->getRoles()); $transaction = ($isAPIKey || $isPrivilegedUser) diff --git a/src/Appwrite/Platform/Modules/Functions/Http/Deployments/Create.php b/src/Appwrite/Platform/Modules/Functions/Http/Deployments/Create.php index 9af5491598..25863d424c 100644 --- a/src/Appwrite/Platform/Modules/Functions/Http/Deployments/Create.php +++ b/src/Appwrite/Platform/Modules/Functions/Http/Deployments/Create.php @@ -227,6 +227,7 @@ class Create extends Action } if ($completed) { + $queueForEvents->reset(); return; } @@ -249,6 +250,8 @@ class Create extends Action $metadata = \array_merge($deployment->getAttribute('sourceMetadata', []), $metadata); if ($uploaded === $chunks) { + $queueForEvents->reset(); + $response ->setStatusCode(Response::STATUS_CODE_ACCEPTED) ->dynamic($deployment, Response::MODEL_DEPLOYMENT); diff --git a/src/Appwrite/Platform/Modules/Functions/Http/Executions/Create.php b/src/Appwrite/Platform/Modules/Functions/Http/Executions/Create.php index 35264730f8..1089d815b5 100644 --- a/src/Appwrite/Platform/Modules/Functions/Http/Executions/Create.php +++ b/src/Appwrite/Platform/Modules/Functions/Http/Executions/Create.php @@ -161,7 +161,7 @@ class Create extends Base /* @var Document $function */ $function = $authorization->skip(fn () => $dbForProject->getDocument('functions', $functionId)); - $isAPIKey = $user->isApp($authorization->getRoles()); + $isAPIKey = $user->isKey($authorization->getRoles()); $isPrivilegedUser = $user->isPrivileged($authorization->getRoles()); if ($function->isEmpty() || (!$function->getAttribute('enabled') && !$isAPIKey && !$isPrivilegedUser)) { diff --git a/src/Appwrite/Platform/Modules/Functions/Http/Executions/Get.php b/src/Appwrite/Platform/Modules/Functions/Http/Executions/Get.php index 0a9dd01b7e..9908669f84 100644 --- a/src/Appwrite/Platform/Modules/Functions/Http/Executions/Get.php +++ b/src/Appwrite/Platform/Modules/Functions/Http/Executions/Get.php @@ -67,7 +67,7 @@ class Get extends Base ) { $function = $authorization->skip(fn () => $dbForProject->getDocument('functions', $functionId)); - $isAPIKey = $user->isApp($authorization->getRoles()); + $isAPIKey = $user->isKey($authorization->getRoles()); $isPrivilegedUser = $user->isPrivileged($authorization->getRoles()); if ($function->isEmpty() || (!$function->getAttribute('enabled') && !$isAPIKey && !$isPrivilegedUser)) { diff --git a/src/Appwrite/Platform/Modules/Functions/Http/Executions/XList.php b/src/Appwrite/Platform/Modules/Functions/Http/Executions/XList.php index 6ad2a5ae55..bf31fa0ced 100644 --- a/src/Appwrite/Platform/Modules/Functions/Http/Executions/XList.php +++ b/src/Appwrite/Platform/Modules/Functions/Http/Executions/XList.php @@ -77,7 +77,7 @@ class XList extends Base ) { $function = $authorization->skip(fn () => $dbForProject->getDocument('functions', $functionId)); - $isAPIKey = $user->isApp($authorization->getRoles()); + $isAPIKey = $user->isKey($authorization->getRoles()); $isPrivilegedUser = $user->isPrivileged($authorization->getRoles()); if ($function->isEmpty() || (!$function->getAttribute('enabled') && !$isAPIKey && !$isPrivilegedUser)) { diff --git a/src/Appwrite/Platform/Modules/Functions/Http/Functions/Create.php b/src/Appwrite/Platform/Modules/Functions/Http/Functions/Create.php index 148f0945ac..3f980275a2 100644 --- a/src/Appwrite/Platform/Modules/Functions/Http/Functions/Create.php +++ b/src/Appwrite/Platform/Modules/Functions/Http/Functions/Create.php @@ -95,6 +95,8 @@ class Create extends Base ->param('providerBranch', '', new Text(128, 0), 'Production branch for the repo linked to the function.', true) ->param('providerSilentMode', false, new Boolean(), 'Is the VCS (Version Control System) connection in silent mode for the repo linked to the function? In silent mode, comments will not be made on commits and pull requests.', true) ->param('providerRootDirectory', '', new Text(128, 0), 'Path to function code in the linked repo.', true) + ->param('providerBranches', [], new ArrayList(new Text(128), APP_LIMIT_ARRAY_PARAMS_SIZE), 'List of branch name patterns to trigger automatic deployments. Supports wildcards. Leave empty to deploy on all branches.', true) + ->param('providerPaths', [], new ArrayList(new Text(128), APP_LIMIT_ARRAY_PARAMS_SIZE), 'List of file path patterns to trigger automatic deployments. Supports wildcards. Leave empty to deploy on all file changes.', true) ->param('buildSpecification', fn (array $plan) => $this->getDefaultSpecification($plan), fn (array $plan) => new Specification( $plan, Config::getParam('specifications', []), @@ -147,6 +149,8 @@ class Create extends Base string $providerBranch, bool $providerSilentMode, string $providerRootDirectory, + array $providerBranches, + array $providerPaths, string $buildSpecification, string $runtimeSpecification, string $templateRepository, @@ -248,6 +252,8 @@ class Create extends Base 'providerBranch' => $providerBranch, 'providerRootDirectory' => $providerRootDirectory, 'providerSilentMode' => $providerSilentMode, + 'providerBranches' => $providerBranches, + 'providerPaths' => $providerPaths, 'buildSpecification' => $buildSpecification, 'runtimeSpecification' => $runtimeSpecification, ])); diff --git a/src/Appwrite/Platform/Modules/Functions/Http/Functions/Update.php b/src/Appwrite/Platform/Modules/Functions/Http/Functions/Update.php index e8713a179d..aca4ead98e 100644 --- a/src/Appwrite/Platform/Modules/Functions/Http/Functions/Update.php +++ b/src/Appwrite/Platform/Modules/Functions/Http/Functions/Update.php @@ -87,6 +87,8 @@ class Update extends Base ->param('providerBranch', '', new Text(128, 0), 'Production branch for the repo linked to the function', true) ->param('providerSilentMode', false, new Boolean(), 'Is the VCS (Version Control System) connection in silent mode for the repo linked to the function? In silent mode, comments will not be made on commits and pull requests.', true) ->param('providerRootDirectory', '', new Text(128, 0), 'Path to function code in the linked repo.', true) + ->param('providerBranches', null, new Nullable(new ArrayList(new Text(128), APP_LIMIT_ARRAY_PARAMS_SIZE)), 'List of branch name patterns to trigger automatic deployments. Supports wildcards. Leave empty to deploy on all branches.', true) + ->param('providerPaths', null, new Nullable(new ArrayList(new Text(128), APP_LIMIT_ARRAY_PARAMS_SIZE)), 'List of file path patterns to trigger automatic deployments. Supports wildcards. Leave empty to deploy on all file changes.', true) ->param('buildSpecification', fn (array $plan) => $this->getDefaultSpecification($plan), fn (array $plan) => new Specification( $plan, Config::getParam('specifications', []), @@ -132,6 +134,8 @@ class Update extends Base string $providerBranch, bool $providerSilentMode, string $providerRootDirectory, + ?array $providerBranches, + ?array $providerPaths, string $buildSpecification, string $runtimeSpecification, int $deploymentRetention, @@ -276,6 +280,8 @@ class Update extends Base 'providerBranch' => $providerBranch, 'providerRootDirectory' => $providerRootDirectory, 'providerSilentMode' => $providerSilentMode, + 'providerBranches' => $providerBranches ?? $function->getAttribute('providerBranches', []), + 'providerPaths' => $providerPaths ?? $function->getAttribute('providerPaths', []), 'buildSpecification' => $buildSpecification, 'runtimeSpecification' => $runtimeSpecification, 'search' => implode(' ', [$functionId, $name, $runtime]), diff --git a/src/Appwrite/Platform/Modules/Functions/Workers/Builds.php b/src/Appwrite/Platform/Modules/Functions/Workers/Builds.php index 5aa95d3bf2..1d4a8dd689 100644 --- a/src/Appwrite/Platform/Modules/Functions/Workers/Builds.php +++ b/src/Appwrite/Platform/Modules/Functions/Workers/Builds.php @@ -104,8 +104,6 @@ class Builds extends Action Executor $executor, array $plan ): void { - Console::log('Build action started'); - $payload = $message->getPayload(); if (empty($payload)) { @@ -113,6 +111,8 @@ class Builds extends Action } $type = $payload['type'] ?? ''; + Span::add('build.type', $type); + $resource = new Document($payload['resource'] ?? []); $deployment = new Document($payload['deployment'] ?? []); $template = new Document($payload['template'] ?? []); @@ -124,7 +124,6 @@ class Builds extends Action switch ($type) { case BUILD_TYPE_DEPLOYMENT: case BUILD_TYPE_RETRY: - Console::info('Creating build for deployment: ' . $deployment->getId()); $github = new GitHub($cache); $this->buildDeployment( $deviceForFunctions, @@ -193,8 +192,6 @@ class Builds extends Action Span::add('deployment.id', $deployment->getId()); Span::add('build.timeout', $timeout); - Console::info('Deployment action started'); - $startTime = DateTime::now(); $durationStart = \microtime(true); @@ -268,7 +265,7 @@ class Builds extends Action $resource = $dbForProject->updateDocument($resource->getCollection(), $resource->getId(), new Document(['latestDeploymentStatus' => $deployment->getAttribute('status', '')])); } - Console::log('Status marked as processing'); + Span::add('deployment.status', 'processing'); $queueForRealtime ->setPayload($deployment->getArrayCopy()) @@ -359,7 +356,7 @@ class Builds extends Action ->setPayload($deployment->getArrayCopy()) ->trigger(); - Console::log('Template cloned'); + Span::add('build.source_size', $deployment->getAttribute('sourceSize')); } } elseif ($isVcsEnabled) { // VCS and VCS+Temaplte @@ -403,8 +400,6 @@ class Builds extends Action throw new \Exception('Unable to clone code repository: ' . $stderr); } - Console::log('Git repository cloned'); - // Local refactoring for function folder with spaces if (str_contains($rootDirectory, ' ')) { $rootDirectoryWithoutSpaces = str_replace(' ', '', $rootDirectory); @@ -478,8 +473,6 @@ class Builds extends Action $queueForRealtime ->setPayload($deployment->getArrayCopy()) ->trigger(); - - Console::log('Git template pushed'); } $tmpPath = '/tmp/builds/' . $deploymentId; @@ -531,18 +524,17 @@ class Builds extends Action ->setPayload($deployment->getArrayCopy()) ->trigger(); - Console::log('Git source uploaded'); + Span::add('build.source_size', $deployment->getAttribute('sourceSize')); $this->runGitAction('processing', $github, $providerCommitHash, $owner, $repositoryName, $project, $resource, $deployment->getId(), $dbForProject, $dbForPlatform, $queueForRealtime, $platform); } - Console::log('Status marked as building'); - /** Request the executor to build the code... */ $deployment->setAttribute('status', 'building'); $deployment = $dbForProject->updateDocument('deployments', $deployment->getId(), new Document([ 'status' => 'building', ])); + Span::add('deployment.status', 'building'); if ($deployment->getSequence() === $resource->getAttribute('latestDeploymentInternalId', '')) { $resource = $dbForProject->updateDocument($resource->getCollection(), $resource->getId(), new Document(['latestDeploymentStatus' => $deployment->getAttribute('status', '')])); @@ -687,11 +679,10 @@ class Builds extends Action } $isCanceled = false; - - Console::log('Runtime creation started'); + $span = Span::current(); Co::join([ - Co\go(function () use ($executor, &$response, $project, $deployment, $source, $resource, $runtime, $vars, $command, $cpus, $memory, $timeout, &$err, $version) { + Co\go(function () use ($executor, &$response, $project, $deployment, $source, $resource, $runtime, $vars, $command, $cpus, $memory, $timeout, &$err, $version, $span) { try { if ($version === 'v2') { $command = 'tar -zxf /tmp/code.tar.gz -C /usr/code && cd /usr/local/src/ && ./build.sh'; @@ -739,16 +730,18 @@ class Builds extends Action outputDirectory: $outputDirectory ?? '' ); - Console::log('createRuntime finished'); } catch (ExecutorTimeout $error) { - Console::warning('createRuntime timed out'); + $span?->set('build.runtime.timed_out', true); + $span?->set('build.runtime.error_type', $error::class); + $span?->set('build.runtime.error_message', $error->getMessage()); $err = new AppwriteException(AppwriteException::BUILD_TIMEOUT, previous: $error); } catch (\Throwable $error) { - Console::warning('createRuntime failed'); + $span?->set('build.runtime.error_type', $error::class); + $span?->set('build.runtime.error_message', $error->getMessage()); $err = $error; } }), - Co\go(function () use ($executor, $project, &$deployment, &$response, $dbForProject, $timeout, &$err, $queueForRealtime, &$isCanceled) { + Co\go(function () use ($executor, $project, &$deployment, &$response, $dbForProject, $timeout, &$err, $queueForRealtime, &$isCanceled, $span) { try { $insideSeparation = false; @@ -756,7 +749,7 @@ class Builds extends Action deploymentId: $deployment->getId(), projectId: $project->getId(), timeout: $timeout, - callback: function ($logs) use (&$response, &$err, $dbForProject, &$isCanceled, &$deployment, $queueForRealtime, &$insideSeparation) { + callback: function ($logs) use (&$response, &$err, $dbForProject, &$isCanceled, &$deployment, $queueForRealtime, &$insideSeparation, $span) { if ($isCanceled) { return; } @@ -767,7 +760,7 @@ class Builds extends Action if ($deployment->getAttribute('status') === 'canceled') { $isCanceled = true; - Console::info('Ignoring realtime logs because build has been canceled'); + $span?->set('build.logs.ignored_reason', 'canceled'); return; } @@ -836,9 +829,10 @@ class Builds extends Action } } ); - Console::warning('listLogs finished'); + $span?->set('build.logs.finished', true); } catch (\Throwable $error) { - Console::warning('listLogs failed'); + $span?->set('build.logs.error_type', $error::class); + $span?->set('build.logs.error_message', $error->getMessage()); if (empty($err)) { $err = $error; } @@ -846,8 +840,6 @@ class Builds extends Action }), ]); - Console::log('Runtime creation finished'); - $latestDeployment = $dbForProject->getDocument('deployments', $deploymentId); if ($latestDeployment->getAttribute('status') === 'canceled') { $this->cancelDeployment($deployment->getId(), $dbForProject, $queueForRealtime); @@ -870,6 +862,8 @@ class Builds extends Action $deployment->setAttribute('buildPath', $response['path']); $deployment->setAttribute('buildSize', $response['size']); $deployment->setAttribute('totalSize', $deployment->getAttribute('buildSize', 0) + $deployment->getAttribute('sourceSize', 0)); + Span::add('build.size', $deployment->getAttribute('buildSize')); + Span::add('build.total_size', $deployment->getAttribute('totalSize')); $logs = ''; foreach ($response['output'] as $log) { @@ -908,8 +902,8 @@ class Builds extends Action $deployment->setAttribute('adapter', $detection->getName()); $deployment->setAttribute('fallbackFile', $detection->getFallbackFile() ?? ''); - - Console::log('Adapter detected'); + Span::add('build.adapter', $deployment->getAttribute('adapter')); + Span::add('build.fallback_file', $deployment->getAttribute('fallbackFile')); } elseif ($adapter === 'ssr' && $detection->getName() === 'static') { throw new \Exception('Adapter mismatch. Detected: ' . $detection->getName() . ' does not match with the set adapter: ' . $adapter); } @@ -927,8 +921,6 @@ class Builds extends Action ->setPayload($deployment->getArrayCopy()) ->trigger(); - Console::log('Build details stored'); - $this->afterBuildSuccess($queueForRealtime, $dbForProject, $deployment, $runtime, $adapter); $logs = $deployment->getAttribute('buildLogs', ''); @@ -942,8 +934,7 @@ class Builds extends Action 'buildLogs' => $deployment->getAttribute('buildLogs'), 'status' => 'ready', ])); - - Console::log('Status marked as ready'); + Span::add('deployment.status', 'ready'); if ($deployment->getSequence() === $resource->getAttribute('latestDeploymentInternalId', '')) { $resource = $dbForProject->updateDocument($resource->getCollection(), $resource->getId(), new Document(['latestDeploymentStatus' => $deployment->getAttribute('status', '')])); @@ -969,7 +960,7 @@ class Builds extends Action if ($currentActiveStartTime < $deploymentStartTime) { $activateBuild = true; } else { - Console::info('Skipping auto-activation as current deployment is more recent'); + Span::add('build.auto_activation.skipped_reason', 'current_deployment_newer'); } } } else { @@ -1031,7 +1022,7 @@ class Builds extends Action break; } - Console::log('Deployment activated'); + Span::add('build.activated', true); } $this->afterDeploymentSuccess( @@ -1099,7 +1090,7 @@ class Builds extends Action ])); }, $queries); - Console::log('Preview rule created'); + Span::add('build.preview_rule_created', true); } } @@ -1109,6 +1100,7 @@ class Builds extends Action 'buildEndedAt' => $endTime, 'buildDuration' => \intval(\ceil($durationEnd - $durationStart)), ])); + Span::add('build.duration', $deployment->getAttribute('buildDuration')); $queueForRealtime ->setPayload($deployment->getArrayCopy()) ->trigger(); @@ -1119,8 +1111,6 @@ class Builds extends Action return; } - Console::log('Build duration updated'); - /** Update function schedule */ // Inform scheduler if function is still active @@ -1144,23 +1134,21 @@ class Builds extends Action deploymentId: $deployment->getId(), )); - Console::log('Site screenshot queued'); + Span::add('build.screenshot_queued', true); } - - Console::info('Deployment action finished'); } catch (\Throwable $th) { - Console::warning('Build failed:'); - Console::error($th->getMessage()); - Console::error($th->getFile()); - Console::error($th->getLine()); - Console::error($th->getTraceAsString()); - if ($dbForProject->getDocument('deployments', $deploymentId)->getAttribute('status') === 'canceled') { $this->cancelDeployment($deployment->getId(), $dbForProject, $queueForRealtime); return; } + Span::add('build.error.stage', 'deployment'); + Span::add('build.error.type', $th::class); + Span::add('build.error.message', $th->getMessage()); + Span::add('build.error.file', $th->getFile()); + Span::add('build.error.line', $th->getLine()); + // Color message red $message = $th->getMessage(); if (! \str_contains($message, '')) { @@ -1182,6 +1170,8 @@ class Builds extends Action $deployment->setAttribute('buildEndedAt', $endTime); $deployment->setAttribute('buildDuration', \intval(\ceil($durationEnd - $durationStart))); $deployment->setAttribute('status', 'failed'); + Span::add('deployment.status', 'failed'); + Span::add('build.duration', $deployment->getAttribute('buildDuration')); $deployment->setAttribute('buildLogs', $message); $deployment = $dbForProject->updateDocument('deployments', $deploymentId, new Document([ @@ -1200,7 +1190,7 @@ class Builds extends Action ->trigger(); if ($isVcsEnabled) { - $this->runGitAction('failed', $github, $providerCommitHash, $owner, $repositoryName, $project, $resource, $deployment->getId(), $dbForProject, $dbForPlatform, $queueForRealtime, $platform); + $this->runGitAction('failed', $github, $providerCommitHash, $owner, $repositoryName, $project, $resource, $deployment->getId(), $dbForProject, $dbForPlatform, $queueForRealtime, $platform, true); } } finally { $queueForRealtime @@ -1360,7 +1350,8 @@ class Builds extends Action Database $dbForProject, Database $dbForPlatform, Realtime $queueForRealtime, - array $platform + array $platform, + bool $secondaryError = false ): void { $deployment = new Document(); @@ -1456,9 +1447,14 @@ class Builds extends Action } } } catch (\Throwable $th) { - Console::warning('Git action failed:'); - Console::warning($th->getMessage()); - Console::warning($th->getTraceAsString()); + $span = Span::current(); + $errorPrefix = $secondaryError ? 'build.error.secondary' : 'build.git_action.error'; + $span?->set("{$errorPrefix}.stage", 'git_action'); + $span?->set("{$errorPrefix}.status", $status); + $span?->set("{$errorPrefix}.type", $th::class); + $span?->set("{$errorPrefix}.message", $th->getMessage()); + $span?->set("{$errorPrefix}.file", $th->getFile()); + $span?->set("{$errorPrefix}.line", $th->getLine()); $logs = $deployment->getAttribute('buildLogs', ''); $date = \date('H:i:s'); @@ -1477,7 +1473,7 @@ class Builds extends Action private function cancelDeployment(string $deploymentId, Database $dbForProject, Realtime $queueForRealtime) { - Console::info('Build has been canceled'); + Span::add('deployment.status', 'canceled'); $deployment = $dbForProject->getDocument('deployments', $deploymentId); diff --git a/src/Appwrite/Platform/Modules/Organization/Http/Init.php b/src/Appwrite/Platform/Modules/Organization/Http/Init.php new file mode 100644 index 0000000000..56eb6db3a0 --- /dev/null +++ b/src/Appwrite/Platform/Modules/Organization/Http/Init.php @@ -0,0 +1,28 @@ +setType(Action::TYPE_INIT) + ->groups(['organization']) + ->inject('team') + ->callback(function (Document $team) { + if ($team->isEmpty()) { + throw new Exception(Exception::TEAM_NOT_FOUND); + } + }); + } +} diff --git a/src/Appwrite/Platform/Modules/Organization/Http/Projects/Action.php b/src/Appwrite/Platform/Modules/Organization/Http/Projects/Action.php new file mode 100644 index 0000000000..0160e2aa04 --- /dev/null +++ b/src/Appwrite/Platform/Modules/Organization/Http/Projects/Action.php @@ -0,0 +1,11 @@ +setHttpMethod(Action::HTTP_REQUEST_METHOD_POST) + ->setHttpPath('/v1/organization/projects') + ->desc('Create organization project') + ->groups(['api', 'organization']) + ->label('audits.event', 'projects.create') + ->label('audits.resource', 'project/{response.$id}') + ->label('scope', 'projects.write') + ->label('sdk', new Method( + namespace: 'organization', + group: 'projects', + name: 'createProject', + description: <<param('projectId', '', new ProjectId(), 'Unique Id. Choose a custom ID or generate a random ID with `ID.unique()`. Valid chars are a-z, and hyphen. Can\'t start with a special char. Max length is 36 chars.') + ->param('name', null, new Text(128), 'Project name. Max length: 128 chars.') + ->param('region', System::getEnv('_APP_REGION', 'default'), new WhiteList(array_keys(array_filter(Config::getParam('regions'), fn ($config) => !$config['disabled']))), 'Project Region.', true) + ->inject('response') + ->inject('dbForPlatform') + ->inject('cache') + ->inject('pools') + ->inject('hooks') + ->inject('team') + ->callback($this->action(...)); + } + + public function action(string $projectId, string $name, string $region, Response $response, Database $dbForPlatform, Cache $cache, Group $pools, Hooks $hooks, Document $team) + { + $allowList = \array_filter(\explode(',', System::getEnv('_APP_PROJECT_REGIONS', ''))); + + if (!empty($allowList) && !\in_array($region, $allowList)) { + throw new Exception(Exception::PROJECT_REGION_UNSUPPORTED, 'Region "' . $region . '" is not supported'); + } + + $auth = Config::getParam('auth', []); + $auths = [ + 'limit' => 0, + 'maxSessions' => 0, + 'passwordHistory' => 0, + 'passwordDictionary' => false, + 'duration' => TOKEN_EXPIRATION_LOGIN_LONG, + 'personalDataCheck' => false, + 'disposableEmails' => false, + 'canonicalEmails' => false, + 'freeEmails' => false, + 'mockNumbers' => [], + 'sessionAlerts' => false, + 'membershipsUserName' => false, + 'membershipsUserEmail' => false, + 'membershipsMfa' => false, + 'membershipsUserId' => false, + 'membershipsUserPhone' => false, + 'invalidateSessions' => true + ]; + + foreach ($auth as $method) { + $auths[$method['key'] ?? ''] = true; + } + + $projectId = ($projectId == 'unique()') ? ID::unique() : $projectId; + + if ($projectId === 'console') { + throw new Exception(Exception::PROJECT_RESERVED_PROJECT, "'console' is a reserved project."); + } + + $databases = Config::getParam('pools-database', []); + + if ($region !== 'default') { + $databaseKeys = System::getEnv('_APP_DATABASE_KEYS', ''); + $keys = explode(',', $databaseKeys); + $databases = array_filter($keys, function ($value) use ($region) { + return str_contains($value, $region); + }); + } + + $databaseOverride = System::getEnv('_APP_DATABASE_OVERRIDE'); + $index = \array_search($databaseOverride, $databases); + if ($index !== false) { + $dsn = $databases[$index]; + } else { + $dsn = $databases[array_rand($databases)]; + } + + // TODO: Temporary until all projects are using shared tables. + $sharedTables = \explode(',', System::getEnv('_APP_DATABASE_SHARED_TABLES', '')); + + if (\in_array($dsn, $sharedTables)) { + $schema = 'appwrite'; + $database = 'appwrite'; + $namespace = System::getEnv('_APP_DATABASE_SHARED_NAMESPACE', ''); + $dsn = $schema . '://' . $dsn . '?database=' . $database; + + if (!empty($namespace)) { + $dsn .= '&namespace=' . $namespace; + } + } + + try { + $project = $dbForPlatform->createDocument('projects', new Document([ + '$id' => $projectId, + '$permissions' => $this->getPermissions($team->getId(), $projectId), + 'name' => $name, + 'teamInternalId' => $team->getSequence(), + 'teamId' => $team->getId(), + 'region' => $region, + 'version' => APP_VERSION_STABLE, + 'services' => new \stdClass(), + 'platforms' => null, + 'oAuthProviders' => [], + 'webhooks' => null, + 'keys' => null, + 'auths' => $auths, + 'accessedAt' => DateTime::now(), + 'search' => implode(' ', [$projectId, $name]), + 'database' => $dsn, + 'labels' => [], + 'status' => PROJECT_STATUS_ACTIVE, + ])); + } catch (Duplicate) { + throw new Exception(Exception::PROJECT_ALREADY_EXISTS); + } + + try { + $dsn = new DSN($dsn); + } catch (\InvalidArgumentException) { + // TODO: Temporary until all projects are using shared tables + $dsn = new DSN('mysql://' . $dsn); + } + + $sharedTables = \explode(',', System::getEnv('_APP_DATABASE_SHARED_TABLES', '')); + $projectTables = !\in_array($dsn->getHost(), $sharedTables); + + if ($projectTables) { + $adapter = new DatabasePool($pools->get($dsn->getHost())); + $dbForProject = new Database($adapter, $cache); + $dbForProject + ->setDatabase(APP_DATABASE) + ->setSharedTables(false) + ->setTenant(null) + ->setNamespace('_' . $project->getSequence()); + + $create = true; + + try { + $dbForProject->create(); + } catch (Duplicate) { + $create = false; + } + + $adapter = new AdapterDatabase($dbForProject); + $audit = new Audit($adapter); + $audit->setup(); + + if ($create) { + /** @var array $collections */ + $collections = Config::getParam('collections', [])['projects'] ?? []; + + foreach ($collections as $key => $collection) { + if (($collection['$collection'] ?? '') !== Database::METADATA) { + continue; + } + + $attributes = \array_map(fn ($attribute) => new Document($attribute), $collection['attributes']); + $indexes = \array_map(fn (array $index) => new Document($index), $collection['indexes']); + + try { + $dbForProject->createCollection($key, $attributes, $indexes); + } catch (Duplicate) { + // Collection already exists + } + } + } + } + + // Hook allowing instant project mirroring during migration + // Outside of migration, hook is not registered and has no effect + $hooks->trigger('afterProjectCreation', [$project, $pools, $cache]); + + $response + ->setStatusCode(Response::STATUS_CODE_CREATED) + ->dynamic($project, Response::MODEL_PROJECT); + } +} diff --git a/src/Appwrite/Platform/Modules/Organization/Http/Projects/Delete.php b/src/Appwrite/Platform/Modules/Organization/Http/Projects/Delete.php new file mode 100644 index 0000000000..fc8d5cccfc --- /dev/null +++ b/src/Appwrite/Platform/Modules/Organization/Http/Projects/Delete.php @@ -0,0 +1,93 @@ +setHttpMethod(Action::HTTP_REQUEST_METHOD_DELETE) + ->setHttpPath('/v1/organization/projects/:projectId') + ->desc('Delete organization project') + ->groups(['api', 'organization']) + ->label('scope', 'projects.write') + ->label('audits.event', 'projects.delete') + ->label('audits.resource', 'project/{request.projectId}') + ->label('sdk', new Method( + namespace: 'organization', + group: 'projects', + name: 'deleteProject', + description: <<param('projectId', '', new UID(), 'Project unique ID.') + ->inject('response') + ->inject('dbForPlatform') + ->inject('publisherForDeletes') + ->inject('authorization') + ->inject('team') + ->callback($this->action(...)); + } + + public function action( + string $projectId, + Response $response, + Database $dbForPlatform, + DeletePublisher $publisherForDeletes, + Authorization $authorization, + Document $team, + ) { + $project = $dbForPlatform->getDocument('projects', $projectId); + + if ($project->isEmpty()) { + throw new Exception(Exception::PROJECT_NOT_FOUND); + } + + if ($project->getAttribute('teamInternalId') !== $team->getSequence()) { + throw new Exception(Exception::PROJECT_NOT_FOUND); + } + + if (!$authorization->skip(fn () => $dbForPlatform->deleteDocument('projects', $project->getId()))) { + throw new Exception(Exception::GENERAL_SERVER_ERROR, 'Failed to remove project from DB'); + } + + $publisherForDeletes->enqueue(new DeleteMessage( + project: $project, + type: DELETE_TYPE_DOCUMENT, + document: $project, + )); + + $response->noContent(); + } +} diff --git a/src/Appwrite/Platform/Modules/Organization/Http/Projects/Get.php b/src/Appwrite/Platform/Modules/Organization/Http/Projects/Get.php new file mode 100644 index 0000000000..37f2dd417a --- /dev/null +++ b/src/Appwrite/Platform/Modules/Organization/Http/Projects/Get.php @@ -0,0 +1,76 @@ +setHttpMethod(Action::HTTP_REQUEST_METHOD_GET) + ->setHttpPath('/v1/organization/projects/:projectId') + ->desc('Get organization project') + ->groups(['api', 'organization']) + ->label('scope', 'projects.read') + ->label('sdk', new Method( + namespace: 'organization', + group: 'projects', + name: 'getProject', + description: <<param('projectId', '', new UID(), 'Project unique ID.') + ->inject('response') + ->inject('dbForPlatform') + ->inject('team') + ->callback($this->action(...)); + } + + public function action( + string $projectId, + Response $response, + Database $dbForPlatform, + Document $team, + ) { + $project = $dbForPlatform->getDocument('projects', $projectId); + + if ($project->isEmpty()) { + throw new Exception(Exception::PROJECT_NOT_FOUND); + } + + if ($project->getAttribute('teamInternalId') !== $team->getSequence()) { + throw new Exception(Exception::PROJECT_NOT_FOUND); + } + + $response + ->setStatusCode(Response::STATUS_CODE_OK) + ->dynamic($project, Response::MODEL_PROJECT); + } +} diff --git a/src/Appwrite/Platform/Modules/Organization/Http/Projects/Update.php b/src/Appwrite/Platform/Modules/Organization/Http/Projects/Update.php new file mode 100644 index 0000000000..c364a5d6df --- /dev/null +++ b/src/Appwrite/Platform/Modules/Organization/Http/Projects/Update.php @@ -0,0 +1,81 @@ +setHttpMethod(Action::HTTP_REQUEST_METHOD_PATCH) + ->setHttpPath('/v1/organization/projects/:projectId') + ->desc('Update organization project') + ->groups(['api', 'organization']) + ->label('scope', 'projects.write') + ->label('audits.event', 'projects.update') + ->label('audits.resource', 'project/{response.$id}') + ->label('sdk', new Method( + namespace: 'organization', + group: 'projects', + name: 'updateProject', + description: <<param('projectId', '', new UID(), 'Project unique ID.') + ->param('name', null, new Text(128), 'Project name. Max length: 128 chars.') + ->inject('response') + ->inject('dbForPlatform') + ->inject('team') + ->callback($this->action(...)); + } + + public function action(string $projectId, string $name, Response $response, Database $dbForPlatform, Document $team) + { + $project = $dbForPlatform->getDocument('projects', $projectId); + + if ($project->isEmpty()) { + throw new Exception(Exception::PROJECT_NOT_FOUND); + } + + if ($project->getAttribute('teamInternalId') !== $team->getSequence()) { + throw new Exception(Exception::PROJECT_NOT_FOUND); + } + + $project = $dbForPlatform->updateDocument('projects', $project->getId(), new Document([ + 'name' => $name, + 'search' => implode(' ', [$projectId, $name]), + ])); + + $response + ->setStatusCode(Response::STATUS_CODE_OK) + ->dynamic($project, Response::MODEL_PROJECT); + } +} diff --git a/src/Appwrite/Platform/Modules/Organization/Http/Projects/XList.php b/src/Appwrite/Platform/Modules/Organization/Http/Projects/XList.php new file mode 100644 index 0000000000..6b45d92175 --- /dev/null +++ b/src/Appwrite/Platform/Modules/Organization/Http/Projects/XList.php @@ -0,0 +1,196 @@ +setHttpMethod(Action::HTTP_REQUEST_METHOD_GET) + ->setHttpPath('/v1/organization/projects') + ->desc('List organization projects') + ->groups(['api', 'organization']) + ->label('scope', 'projects.read') + ->label('sdk', new Method( + namespace: 'organization', + group: 'projects', + name: 'listProjects', + description: <<param('queries', [], $this->getQueriesValidator(), 'Array of query strings generated using the Query class provided by the SDK. [Learn more about queries](https://appwrite.io/docs/queries). Maximum of ' . APP_LIMIT_ARRAY_PARAMS_SIZE . ' queries are allowed, each ' . APP_LIMIT_ARRAY_ELEMENT_SIZE . ' characters long. You may filter on the following attributes: ' . implode(', ', Projects::ALLOWED_ATTRIBUTES), true) + ->param('search', '', new Text(256), 'Search term to filter your list results. Max length: 256 chars.', true) + ->param('total', true, new Boolean(true), 'When set to false, the total count returned will be 0 and will not be calculated.', true) + ->inject('response') + ->inject('dbForPlatform') + ->inject('team') + ->callback($this->action(...)); + } + + public function action(array $queries, string $search, bool $includeTotal, Response $response, Database $dbForPlatform, Document $team) + { + try { + $queries = Query::parseQueries($queries); + } catch (QueryException $e) { + throw new Exception(Exception::GENERAL_QUERY_INVALID, $e->getMessage()); + } + + if (!empty($search)) { + $queries[] = Query::search('search', $search); + } + + $queries[] = Query::equal('teamInternalId', [$team->getSequence()]); + + $cursor = Query::getCursorQueries($queries, false); + $cursor = \reset($cursor); + + if ($cursor !== false) { + $validator = new Cursor(); + if (!$validator->isValid($cursor)) { + throw new Exception(Exception::GENERAL_QUERY_INVALID, $validator->getDescription()); + } + + $projectId = $cursor->getValue(); + $cursorDocument = $dbForPlatform->getDocument('projects', $projectId); + + if ($cursorDocument->isEmpty()) { + throw new Exception(Exception::GENERAL_CURSOR_NOT_FOUND, "Project '{$projectId}' for the 'cursor' value not found."); + } + + $cursor->setValue($cursorDocument); + } + + try { + $selectQueries = Query::groupByType($queries)['selections']; + $filterQueries = Query::groupByType($queries)['filters']; + + $projects = $this->find($dbForPlatform, $queries, $selectQueries); + $total = $includeTotal ? $dbForPlatform->count('projects', $filterQueries, APP_LIMIT_COUNT) : 0; + } catch (Order $e) { + throw new Exception(Exception::DATABASE_QUERY_ORDER_NULL, "The order attribute '{$e->getAttribute()}' had a null value. Cursor pagination requires all documents order attribute values are non-null."); + } + + $response->addFilter(new ListSelection($selectQueries, 'projects')); + + $response + ->setStatusCode(Response::STATUS_CODE_OK) + ->dynamic(new Document([ + 'projects' => $projects, + 'total' => $total, + ]), Response::MODEL_PROJECT_LIST); + } + + // Build mapping of columns to their subQuery filters + private static function getAttributeToSubQueryFilters(): array + { + if (self::$attributeToSubQueryFilters !== null) { + return self::$attributeToSubQueryFilters; + } + + self::$attributeToSubQueryFilters = []; + + $collections = Config::getParam('collections', []); + $projectAttributes = $collections['platform']['projects']['attributes'] ?? []; + + foreach ($projectAttributes as $attribute) { + $attributeId = $attribute['$id'] ?? null; + $filters = $attribute['filters'] ?? []; + + if ($attributeId === null || empty($filters)) { + continue; + } + + // extract only subQuery filters + $subQueryFilters = \array_filter($filters, function ($filter) { + return \str_starts_with($filter, 'subQuery'); + }); + + if (!empty($subQueryFilters)) { + self::$attributeToSubQueryFilters[$attributeId] = \array_values($subQueryFilters); + } + } + + return self::$attributeToSubQueryFilters; + } + + private function find(Database $dbForPlatform, array $queries, array $selectQueries): array + { + if (empty($selectQueries)) { + return $dbForPlatform->find('projects', $queries); + } + + $selectedAttributes = []; + foreach ($selectQueries as $query) { + foreach ($query->getValues() as $value) { + $selectedAttributes[] = $value; + } + } + + if (\in_array('*', $selectedAttributes)) { + return $dbForPlatform->find('projects', $queries); + } + + $filtersToSkipMap = []; + $selectedAttributesMap = \array_flip($selectedAttributes); + $attributeToSubQueryFilters = self::getAttributeToSubQueryFilters(); + + foreach ($attributeToSubQueryFilters as $attributeName => $subQueryFilters) { + if (!isset($selectedAttributesMap[$attributeName])) { + foreach ($subQueryFilters as $filter) { + $filtersToSkipMap[$filter] = true; + } + } + } + + $filtersToSkip = \array_keys($filtersToSkipMap); + + return empty($filtersToSkip) + ? $dbForPlatform->find('projects', $queries) + : $dbForPlatform->skipFilters(fn () => $dbForPlatform->find('projects', $queries), $filtersToSkip); + } +} diff --git a/src/Appwrite/Platform/Modules/Organization/Module.php b/src/Appwrite/Platform/Modules/Organization/Module.php new file mode 100644 index 0000000000..eb7a2dc433 --- /dev/null +++ b/src/Appwrite/Platform/Modules/Organization/Module.php @@ -0,0 +1,14 @@ +addService('http', new Http()); + } +} diff --git a/src/Appwrite/Platform/Modules/Organization/Services/Http.php b/src/Appwrite/Platform/Modules/Organization/Services/Http.php new file mode 100644 index 0000000000..49a8f7d832 --- /dev/null +++ b/src/Appwrite/Platform/Modules/Organization/Services/Http.php @@ -0,0 +1,29 @@ +type = Service::TYPE_HTTP; + + // Init hook + $this->addAction(Init::getName(), new Init()); + + // Projects + $this->addAction(CreateProject::getName(), new CreateProject()); + $this->addAction(ListProjects::getName(), new ListProjects()); + $this->addAction(GetProject::getName(), new GetProject()); + $this->addAction(UpdateProject::getName(), new UpdateProject()); + $this->addAction(DeleteProject::getName(), new DeleteProject()); + } +} diff --git a/src/Appwrite/Platform/Modules/Presences/HTTP/Update.php b/src/Appwrite/Platform/Modules/Presences/HTTP/Update.php index 5387d3a91e..376359717b 100644 --- a/src/Appwrite/Platform/Modules/Presences/HTTP/Update.php +++ b/src/Appwrite/Platform/Modules/Presences/HTTP/Update.php @@ -128,7 +128,7 @@ class Update extends PlatformAction Event $queueForEvents ): void { $presenceState = new PresenceState(); - $isAPIKey = $user->isApp($authorization->getRoles()); + $isAPIKey = $user->isKey($authorization->getRoles()); $isPrivilegedUser = $user->isPrivileged($authorization->getRoles()); if ($userId && !$isAPIKey && !$isPrivilegedUser) { diff --git a/src/Appwrite/Platform/Modules/Presences/HTTP/Upsert.php b/src/Appwrite/Platform/Modules/Presences/HTTP/Upsert.php index c85cb15f17..be658adbd1 100644 --- a/src/Appwrite/Platform/Modules/Presences/HTTP/Upsert.php +++ b/src/Appwrite/Platform/Modules/Presences/HTTP/Upsert.php @@ -128,7 +128,7 @@ class Upsert extends PlatformAction Event $queueForEvents, Context $usage ): void { - $isAPIKey = $user->isApp($authorization->getRoles()); + $isAPIKey = $user->isKey($authorization->getRoles()); $isPrivilegedUser = $user->isPrivileged($authorization->getRoles()); if ($userId && !$isAPIKey && !$isPrivilegedUser) { throw new Exception(Exception::GENERAL_UNAUTHORIZED_SCOPE, "userId is not allowed for non-API key and non-privileged users"); diff --git a/src/Appwrite/Platform/Modules/Project/Http/Project/SMTP/Tests/Create.php b/src/Appwrite/Platform/Modules/Project/Http/Project/SMTP/Tests/Create.php index 8c87a41475..fa35b8d6d5 100644 --- a/src/Appwrite/Platform/Modules/Project/Http/Project/SMTP/Tests/Create.php +++ b/src/Appwrite/Platform/Modules/Project/Http/Project/SMTP/Tests/Create.php @@ -69,7 +69,7 @@ class Create extends Action ->inject('response') ->inject('project') ->inject('publisherForMails') - ->inject('plan') + ->inject('platform') ->callback($this->action(...)); } @@ -89,7 +89,7 @@ class Create extends Action Response $response, Document $project, MailPublisher $publisherForMails, - array $plan + array $platform, ): void { // Backwards compatibility: use inline params if provided, otherwise fall back to project SMTP config. // When inline params are provided they are treated as self-contained — project config is ignored @@ -144,14 +144,7 @@ class Create extends Action $template = Template::fromFile(APP_CE_CONFIG_DIR . '/locale/templates/email-smtp-test.tpl'); $template ->setParam('{{from}}', "{$senderName} ({$senderEmail})") - ->setParam('{{replyTo}}', "{$replyToNameDisplay} ({$replyToEmailDisplay})") - ->setParam('{{logoUrl}}', $plan['logoUrl'] ?? APP_EMAIL_LOGO_URL) - ->setParam('{{accentColor}}', $plan['accentColor'] ?? APP_EMAIL_ACCENT_COLOR) - ->setParam('{{twitterUrl}}', $plan['twitterUrl'] ?? APP_SOCIAL_TWITTER) - ->setParam('{{discordUrl}}', $plan['discordUrl'] ?? APP_SOCIAL_DISCORD) - ->setParam('{{githubUrl}}', $plan['githubUrl'] ?? APP_SOCIAL_GITHUB_APPWRITE) - ->setParam('{{termsUrl}}', $plan['termsUrl'] ?? APP_EMAIL_TERMS_URL) - ->setParam('{{privacyUrl}}', $plan['privacyUrl'] ?? APP_EMAIL_PRIVACY_URL); + ->setParam('{{replyTo}}', "{$replyToNameDisplay} ({$replyToEmailDisplay})"); foreach ($emails as $email) { $publisherForMails->enqueue(new MailMessage( @@ -171,6 +164,17 @@ class Create extends Action 'senderEmail' => $senderEmail, 'senderName' => $senderName, ], + variables: [ + 'platform' => $platform['platformName'] ?? APP_NAME, + 'logoUrl' => $platform['logoUrl'] ?? APP_EMAIL_LOGO_URL, + 'accentColor' => $platform['accentColor'] ?? APP_EMAIL_ACCENT_COLOR, + 'twitter' => $platform['twitterUrl'] ?? APP_SOCIAL_TWITTER, + 'discord' => $platform['discordUrl'] ?? APP_SOCIAL_DISCORD, + 'github' => $platform['githubUrl'] ?? APP_SOCIAL_GITHUB_APPWRITE, + 'terms' => $platform['termsUrl'] ?? APP_EMAIL_TERMS_URL, + 'privacy' => $platform['privacyUrl'] ?? APP_EMAIL_PRIVACY_URL, + ], + platform: $platform, )); } diff --git a/src/Appwrite/Platform/Modules/Project/Http/Project/SMTP/Update.php b/src/Appwrite/Platform/Modules/Project/Http/Project/SMTP/Update.php index ef2e478d96..b99a9db3c2 100644 --- a/src/Appwrite/Platform/Modules/Project/Http/Project/SMTP/Update.php +++ b/src/Appwrite/Platform/Modules/Project/Http/Project/SMTP/Update.php @@ -121,7 +121,7 @@ class Update extends Action // Validate when the caller is explicitly enabling or hasn't expressed a preference // (so a credentials-only PATCH can auto-enable). Skip only when the caller is // explicitly keeping/turning SMTP off. - if (\is_null($enabled) || $enabled === true) { + if ((\is_null($enabled) || $enabled === true) && !empty($smtp['senderEmail'] ?? '')) { $mail = new PHPMailer(true); $mail->isSMTP(); diff --git a/src/Appwrite/Platform/Modules/Projects/Http/Projects/Create.php b/src/Appwrite/Platform/Modules/Projects/Http/Projects/Create.php index d2c92fc65c..18250cb140 100644 --- a/src/Appwrite/Platform/Modules/Projects/Http/Projects/Create.php +++ b/src/Appwrite/Platform/Modules/Projects/Http/Projects/Create.php @@ -4,9 +4,6 @@ namespace Appwrite\Platform\Modules\Projects\Http\Projects; use Appwrite\Extend\Exception; use Appwrite\Hooks\Hooks; -use Appwrite\SDK\AuthType; -use Appwrite\SDK\Method; -use Appwrite\SDK\Response as SDKResponse; use Appwrite\Utopia\Database\Validator\ProjectId; use Appwrite\Utopia\Database\Validator\Queries\Projects; use Appwrite\Utopia\Request; @@ -54,19 +51,6 @@ class Create extends Action ->label('audits.event', 'projects.create') ->label('audits.resource', 'project/{response.$id}') ->label('scope', 'projects.write') - ->label('sdk', new Method( - namespace: 'projects', - group: 'projects', - name: 'create', - description: '/docs/references/projects/create.md', - auth: [AuthType::ADMIN], - responses: [ - new SDKResponse( - code: Response::STATUS_CODE_CREATED, - model: Response::MODEL_PROJECT, - ) - ] - )) ->param('projectId', '', new ProjectId(), 'Unique Id. Choose a custom ID or generate a random ID with `ID.unique()`. Valid chars are a-z, and hyphen. Can\'t start with a special char. Max length is 36 chars.') ->param('name', null, new Text(128), 'Project name. Max length: 128 chars.') ->param('teamId', '', new UID(), 'Team unique ID.') diff --git a/src/Appwrite/Platform/Modules/Projects/Http/Projects/Update.php b/src/Appwrite/Platform/Modules/Projects/Http/Projects/Update.php index 29c26b33ea..f6df843d07 100644 --- a/src/Appwrite/Platform/Modules/Projects/Http/Projects/Update.php +++ b/src/Appwrite/Platform/Modules/Projects/Http/Projects/Update.php @@ -3,9 +3,6 @@ namespace Appwrite\Platform\Modules\Projects\Http\Projects; use Appwrite\Extend\Exception; -use Appwrite\SDK\AuthType; -use Appwrite\SDK\Method; -use Appwrite\SDK\Response as SDKResponse; use Appwrite\Utopia\Database\Validator\Queries\Projects; use Appwrite\Utopia\Response; use Utopia\Database\Database; @@ -39,19 +36,6 @@ class Update extends Action ->label('scope', 'projects.write') ->label('audits.event', 'projects.update') ->label('audits.resource', 'project/{request.projectId}') - ->label('sdk', new Method( - namespace: 'projects', - group: 'projects', - name: 'update', - description: '/docs/references/projects/update.md', - auth: [AuthType::ADMIN], - responses: [ - new SDKResponse( - code: Response::STATUS_CODE_OK, - model: Response::MODEL_PROJECT, - ) - ] - )) ->param('projectId', '', new UID(), 'Project unique ID.') ->param('name', null, new Text(128), 'Project name. Max length: 128 chars.') ->param('description', '', new Text(256), 'Project description. Max length: 256 chars.', true) diff --git a/src/Appwrite/Platform/Modules/Projects/Http/Projects/XList.php b/src/Appwrite/Platform/Modules/Projects/Http/Projects/XList.php index 0d2a951388..b967c29451 100644 --- a/src/Appwrite/Platform/Modules/Projects/Http/Projects/XList.php +++ b/src/Appwrite/Platform/Modules/Projects/Http/Projects/XList.php @@ -4,10 +4,6 @@ namespace Appwrite\Platform\Modules\Projects\Http\Projects; use Appwrite\Extend\Exception; use Appwrite\Platform\Action; -use Appwrite\SDK\AuthType; -use Appwrite\SDK\ContentType; -use Appwrite\SDK\Method; -use Appwrite\SDK\Response as SDKResponse; use Appwrite\Utopia\Database\Validator\Queries\Projects; use Appwrite\Utopia\Response; use Appwrite\Utopia\Response\Filters\ListSelection; @@ -48,22 +44,6 @@ class XList extends Action ->desc('List projects') ->groups(['api', 'projects']) ->label('scope', 'projects.read') - ->label('sdk', new Method( - namespace: 'projects', - group: 'projects', - name: 'list', - description: <<param('queries', [], $this->getQueriesValidator(), 'Array of query strings generated using the Query class provided by the SDK. [Learn more about queries](https://appwrite.io/docs/queries). Maximum of ' . APP_LIMIT_ARRAY_PARAMS_SIZE . ' queries are allowed, each ' . APP_LIMIT_ARRAY_ELEMENT_SIZE . ' characters long. You may filter on the following attributes: ' . implode(', ', Projects::ALLOWED_ATTRIBUTES), true) ->param('search', '', new Text(256), 'Search term to filter your list results. Max length: 256 chars.', true) ->param('total', true, new Boolean(true), 'When set to false, the total count returned will be 0 and will not be calculated.', true) diff --git a/src/Appwrite/Platform/Modules/Sites/Http/Deployments/Create.php b/src/Appwrite/Platform/Modules/Sites/Http/Deployments/Create.php index d27755d106..5fd724346c 100644 --- a/src/Appwrite/Platform/Modules/Sites/Http/Deployments/Create.php +++ b/src/Appwrite/Platform/Modules/Sites/Http/Deployments/Create.php @@ -227,6 +227,7 @@ class Create extends Action } if ($completed) { + $queueForEvents->reset(); return; } @@ -257,6 +258,8 @@ class Create extends Action $metadata = \array_merge($deployment->getAttribute('sourceMetadata', []), $metadata); if ($uploaded === $chunks) { + $queueForEvents->reset(); + $response ->setStatusCode(Response::STATUS_CODE_ACCEPTED) ->dynamic($deployment, Response::MODEL_DEPLOYMENT); diff --git a/src/Appwrite/Platform/Modules/Sites/Http/Sites/Create.php b/src/Appwrite/Platform/Modules/Sites/Http/Sites/Create.php index d01d0d8ca7..9f3d46ffd0 100644 --- a/src/Appwrite/Platform/Modules/Sites/Http/Sites/Create.php +++ b/src/Appwrite/Platform/Modules/Sites/Http/Sites/Create.php @@ -19,6 +19,7 @@ use Utopia\Database\Helpers\ID; use Utopia\Platform\Action; use Utopia\Platform\Scope\HTTP; use Utopia\System\System; +use Utopia\Validator\ArrayList; use Utopia\Validator\Boolean; use Utopia\Validator\Range; use Utopia\Validator\Text; @@ -78,6 +79,8 @@ class Create extends Base ->param('providerBranch', '', new Text(128, 0), 'Production branch for the repo linked to the site.', true) ->param('providerSilentMode', false, new Boolean(), 'Is the VCS (Version Control System) connection in silent mode for the repo linked to the site? In silent mode, comments will not be made on commits and pull requests.', true) ->param('providerRootDirectory', '', new Text(128, 0), 'Path to site code in the linked repo.', true) + ->param('providerBranches', [], new ArrayList(new Text(128), APP_LIMIT_ARRAY_PARAMS_SIZE), 'List of branch name patterns to trigger automatic deployments. Supports wildcards. Leave empty to deploy on all branches.', true) + ->param('providerPaths', [], new ArrayList(new Text(128), APP_LIMIT_ARRAY_PARAMS_SIZE), 'List of file path patterns to trigger automatic deployments. Supports wildcards. Leave empty to deploy on all file changes.', true) ->param('buildSpecification', fn (array $plan) => $this->getDefaultSpecification($plan), fn (array $plan) => new Specification( $plan, Config::getParam('specifications', []), @@ -118,6 +121,8 @@ class Create extends Base string $providerBranch, bool $providerSilentMode, string $providerRootDirectory, + array $providerBranches, + array $providerPaths, string $buildSpecification, string $runtimeSpecification, int $deploymentRetention, @@ -173,6 +178,8 @@ class Create extends Base 'providerBranch' => $providerBranch, 'providerRootDirectory' => $providerRootDirectory, 'providerSilentMode' => $providerSilentMode, + 'providerBranches' => $providerBranches, + 'providerPaths' => $providerPaths, 'buildSpecification' => $buildSpecification, 'runtimeSpecification' => $runtimeSpecification, 'buildRuntime' => $buildRuntime, diff --git a/src/Appwrite/Platform/Modules/Sites/Http/Sites/Update.php b/src/Appwrite/Platform/Modules/Sites/Http/Sites/Update.php index 2aee03265e..bfd8c9f198 100644 --- a/src/Appwrite/Platform/Modules/Sites/Http/Sites/Update.php +++ b/src/Appwrite/Platform/Modules/Sites/Http/Sites/Update.php @@ -22,7 +22,9 @@ use Utopia\Http\Adapter\Swoole\Request; use Utopia\Platform\Action; use Utopia\Platform\Scope\HTTP; use Utopia\System\System; +use Utopia\Validator\ArrayList; use Utopia\Validator\Boolean; +use Utopia\Validator\Nullable; use Utopia\Validator\Range; use Utopia\Validator\Text; use Utopia\Validator\WhiteList; @@ -81,6 +83,8 @@ class Update extends Base ->param('providerBranch', '', new Text(128, 0), 'Production branch for the repo linked to the site.', true) ->param('providerSilentMode', false, new Boolean(), 'Is the VCS (Version Control System) connection in silent mode for the repo linked to the site? In silent mode, comments will not be made on commits and pull requests.', true) ->param('providerRootDirectory', '', new Text(128, 0), 'Path to site code in the linked repo.', true) + ->param('providerBranches', null, new Nullable(new ArrayList(new Text(128), APP_LIMIT_ARRAY_PARAMS_SIZE)), 'List of branch name patterns to trigger automatic deployments. Supports wildcards. Leave empty to deploy on all branches.', true) + ->param('providerPaths', null, new Nullable(new ArrayList(new Text(128), APP_LIMIT_ARRAY_PARAMS_SIZE)), 'List of file path patterns to trigger automatic deployments. Supports wildcards. Leave empty to deploy on all file changes.', true) ->param('buildSpecification', fn (array $plan) => $this->getDefaultSpecification($plan), fn (array $plan) => new Specification( $plan, Config::getParam('specifications', []), @@ -126,6 +130,8 @@ class Update extends Base string $providerBranch, bool $providerSilentMode, string $providerRootDirectory, + ?array $providerBranches, + ?array $providerPaths, string $buildSpecification, string $runtimeSpecification, int $deploymentRetention, @@ -271,6 +277,8 @@ class Update extends Base 'providerBranch' => $providerBranch, 'providerRootDirectory' => $providerRootDirectory, 'providerSilentMode' => $providerSilentMode, + 'providerBranches' => $providerBranches ?? $site->getAttribute('providerBranches', []), + 'providerPaths' => $providerPaths ?? $site->getAttribute('providerPaths', []), 'buildSpecification' => $buildSpecification, 'runtimeSpecification' => $runtimeSpecification, 'search' => implode(' ', [$siteId, $name, $framework]), diff --git a/src/Appwrite/Platform/Modules/Storage/Http/Buckets/Files/Create.php b/src/Appwrite/Platform/Modules/Storage/Http/Buckets/Files/Create.php index 8530475f0c..348b19c039 100644 --- a/src/Appwrite/Platform/Modules/Storage/Http/Buckets/Files/Create.php +++ b/src/Appwrite/Platform/Modules/Storage/Http/Buckets/Files/Create.php @@ -115,7 +115,7 @@ class Create extends Action ) { $bucket = $authorization->skip(fn () => $dbForProject->getDocument('buckets', $bucketId)); - $isAPIKey = $user->isApp($authorization->getRoles()); + $isAPIKey = $user->isKey($authorization->getRoles()); $isPrivilegedUser = $user->isPrivileged($authorization->getRoles()); if ($bucket->isEmpty() || (!$bucket->getAttribute('enabled') && !$isAPIKey && !$isPrivilegedUser)) { @@ -320,6 +320,7 @@ class Create extends Action } if ($completed) { + $queueForEvents->reset(); return; } @@ -337,6 +338,8 @@ class Create extends Action throw new Exception(Exception::STORAGE_FILE_ALREADY_EXISTS); } + $queueForEvents->reset(); + $response ->setStatusCode(Response::STATUS_CODE_OK) ->dynamic($file, Response::MODEL_FILE); diff --git a/src/Appwrite/Platform/Modules/Storage/Http/Buckets/Files/Delete.php b/src/Appwrite/Platform/Modules/Storage/Http/Buckets/Files/Delete.php index 6d8781d484..c16b374c78 100644 --- a/src/Appwrite/Platform/Modules/Storage/Http/Buckets/Files/Delete.php +++ b/src/Appwrite/Platform/Modules/Storage/Http/Buckets/Files/Delete.php @@ -84,7 +84,7 @@ class Delete extends Action ) { $bucket = $authorization->skip(fn () => $dbForProject->getDocument('buckets', $bucketId)); - $isAPIKey = $user->isApp($authorization->getRoles()); + $isAPIKey = $user->isKey($authorization->getRoles()); $isPrivilegedUser = $user->isPrivileged($authorization->getRoles()); if ($bucket->isEmpty() || (!$bucket->getAttribute('enabled') && !$isAPIKey && !$isPrivilegedUser)) { diff --git a/src/Appwrite/Platform/Modules/Storage/Http/Buckets/Files/Download/Get.php b/src/Appwrite/Platform/Modules/Storage/Http/Buckets/Files/Download/Get.php index c876004319..7e5d7d6879 100644 --- a/src/Appwrite/Platform/Modules/Storage/Http/Buckets/Files/Download/Get.php +++ b/src/Appwrite/Platform/Modules/Storage/Http/Buckets/Files/Download/Get.php @@ -90,7 +90,7 @@ class Get extends Action /* @type Document $bucket */ $bucket = $authorization->skip(fn () => $dbForProject->getDocument('buckets', $bucketId)); - $isAPIKey = $user->isApp($authorization->getRoles()); + $isAPIKey = $user->isKey($authorization->getRoles()); $isPrivilegedUser = $user->isPrivileged($authorization->getRoles()); if ($bucket->isEmpty() || (!$bucket->getAttribute('enabled') && !$isAPIKey && !$isPrivilegedUser)) { diff --git a/src/Appwrite/Platform/Modules/Storage/Http/Buckets/Files/Get.php b/src/Appwrite/Platform/Modules/Storage/Http/Buckets/Files/Get.php index c9ce5796eb..d997fe3cc0 100644 --- a/src/Appwrite/Platform/Modules/Storage/Http/Buckets/Files/Get.php +++ b/src/Appwrite/Platform/Modules/Storage/Http/Buckets/Files/Get.php @@ -65,7 +65,7 @@ class Get extends Action ) { $bucket = $authorization->skip(fn () => $dbForProject->getDocument('buckets', $bucketId)); - $isAPIKey = $user->isApp($authorization->getRoles()); + $isAPIKey = $user->isKey($authorization->getRoles()); $isPrivilegedUser = $user->isPrivileged($authorization->getRoles()); if ($bucket->isEmpty() || (!$bucket->getAttribute('enabled') && !$isAPIKey && !$isPrivilegedUser)) { diff --git a/src/Appwrite/Platform/Modules/Storage/Http/Buckets/Files/Preview/Get.php b/src/Appwrite/Platform/Modules/Storage/Http/Buckets/Files/Preview/Get.php index 68bc2cabae..0cca87c646 100644 --- a/src/Appwrite/Platform/Modules/Storage/Http/Buckets/Files/Preview/Get.php +++ b/src/Appwrite/Platform/Modules/Storage/Http/Buckets/Files/Preview/Get.php @@ -133,7 +133,7 @@ class Get extends Action $bucket = $authorization->skip(fn () => $dbForProject->getDocument('buckets', $bucketId)); - $isAPIKey = $user->isApp($authorization->getRoles()); + $isAPIKey = $user->isKey($authorization->getRoles()); $isPrivilegedUser = $user->isPrivileged($authorization->getRoles()); if ($bucket->isEmpty() || (!$bucket->getAttribute('enabled') && !$isAPIKey && !$isPrivilegedUser)) { diff --git a/src/Appwrite/Platform/Modules/Storage/Http/Buckets/Files/Push/Get.php b/src/Appwrite/Platform/Modules/Storage/Http/Buckets/Files/Push/Get.php index 5b3fd02370..f069b1cc2a 100644 --- a/src/Appwrite/Platform/Modules/Storage/Http/Buckets/Files/Push/Get.php +++ b/src/Appwrite/Platform/Modules/Storage/Http/Buckets/Files/Push/Get.php @@ -90,7 +90,7 @@ class Get extends Action $disposition = $decoded['disposition'] ?? 'inline'; $dbForProject = $isInternal ? $dbForPlatform : $dbForProject; - $isAPIKey = $user->isApp($authorization->getRoles()); + $isAPIKey = $user->isKey($authorization->getRoles()); $isPrivilegedUser = $user->isPrivileged($authorization->getRoles()); $bucket = $authorization->skip(fn () => $dbForProject->getDocument('buckets', $bucketId)); diff --git a/src/Appwrite/Platform/Modules/Storage/Http/Buckets/Files/Update.php b/src/Appwrite/Platform/Modules/Storage/Http/Buckets/Files/Update.php index 407f3766df..fb1245d29c 100644 --- a/src/Appwrite/Platform/Modules/Storage/Http/Buckets/Files/Update.php +++ b/src/Appwrite/Platform/Modules/Storage/Http/Buckets/Files/Update.php @@ -81,7 +81,7 @@ class Update extends Action ) { $bucket = $authorization->skip(fn () => $dbForProject->getDocument('buckets', $bucketId)); - $isAPIKey = $user->isApp($authorization->getRoles()); + $isAPIKey = $user->isKey($authorization->getRoles()); $isPrivilegedUser = $user->isPrivileged($authorization->getRoles()); if ($bucket->isEmpty() || (!$bucket->getAttribute('enabled') && !$isAPIKey && !$isPrivilegedUser)) { @@ -110,7 +110,7 @@ class Update extends Action // Users can only manage their own roles, API keys and Admin users can manage any $roles = $authorization->getRoles(); - if (!$user->isApp($roles) && !$user->isPrivileged($roles) && !\is_null($permissions)) { + if (!$user->isKey($roles) && !$user->isPrivileged($roles) && !\is_null($permissions)) { foreach (Database::PERMISSIONS as $type) { foreach ($permissions as $permission) { $permission = Permission::parse($permission); diff --git a/src/Appwrite/Platform/Modules/Storage/Http/Buckets/Files/View/Get.php b/src/Appwrite/Platform/Modules/Storage/Http/Buckets/Files/View/Get.php index b2f00da6d2..b78d582c47 100644 --- a/src/Appwrite/Platform/Modules/Storage/Http/Buckets/Files/View/Get.php +++ b/src/Appwrite/Platform/Modules/Storage/Http/Buckets/Files/View/Get.php @@ -91,7 +91,7 @@ class Get extends Action /* @type Document $bucket */ $bucket = $authorization->skip(fn () => $dbForProject->getDocument('buckets', $bucketId)); - $isAPIKey = $user->isApp($authorization->getRoles()); + $isAPIKey = $user->isKey($authorization->getRoles()); $isPrivilegedUser = $user->isPrivileged($authorization->getRoles()); if ($bucket->isEmpty() || (!$bucket->getAttribute('enabled') && !$isAPIKey && !$isPrivilegedUser)) { diff --git a/src/Appwrite/Platform/Modules/Storage/Http/Buckets/Files/XList.php b/src/Appwrite/Platform/Modules/Storage/Http/Buckets/Files/XList.php index 945c4bfd7c..28dfa87c5d 100644 --- a/src/Appwrite/Platform/Modules/Storage/Http/Buckets/Files/XList.php +++ b/src/Appwrite/Platform/Modules/Storage/Http/Buckets/Files/XList.php @@ -80,7 +80,7 @@ class XList extends Action ) { $bucket = $authorization->skip(fn () => $dbForProject->getDocument('buckets', $bucketId)); - $isAPIKey = $user->isApp($authorization->getRoles()); + $isAPIKey = $user->isKey($authorization->getRoles()); $isPrivilegedUser = $user->isPrivileged($authorization->getRoles()); if ($bucket->isEmpty() || (!$bucket->getAttribute('enabled') && !$isAPIKey && !$isPrivilegedUser)) { diff --git a/src/Appwrite/Platform/Modules/Teams/Http/Memberships/Create.php b/src/Appwrite/Platform/Modules/Teams/Http/Memberships/Create.php index 5500a56cbc..3bffb091ba 100644 --- a/src/Appwrite/Platform/Modules/Teams/Http/Memberships/Create.php +++ b/src/Appwrite/Platform/Modules/Teams/Http/Memberships/Create.php @@ -103,7 +103,7 @@ class Create extends Action public function action(string $teamId, string $email, string $userId, string $phone, array $roles, string $url, string $name, Response $response, Document $project, User $user, Database $dbForProject, Authorization $authorization, Locale $locale, MailPublisher $publisherForMails, MessagingPublisher $publisherForMessaging, Event $queueForEvents, callable $timelimit, Context $usage, array $plan, array $platform, Password $proofForPassword, Token $proofForToken) { - $isAppUser = $user->isApp($authorization->getRoles()); + $isAppUser = $user->isKey($authorization->getRoles()); $isPrivilegedUser = $user->isPrivileged($authorization->getRoles()); $invitee = new Document(); $hash = ''; diff --git a/src/Appwrite/Platform/Modules/Teams/Http/Memberships/Get.php b/src/Appwrite/Platform/Modules/Teams/Http/Memberships/Get.php index ef8d130855..556f6de52c 100644 --- a/src/Appwrite/Platform/Modules/Teams/Http/Memberships/Get.php +++ b/src/Appwrite/Platform/Modules/Teams/Http/Memberships/Get.php @@ -81,7 +81,7 @@ class Get extends Action $roles = $authorization->getRoles(); $isPrivilegedUser = $user->isPrivileged($roles); - $isAppUser = $user->isApp($roles); + $isAppUser = $user->isKey($roles); $membershipsPrivacy = array_map(function ($privacy) use ($isPrivilegedUser, $isAppUser) { return $privacy || $isPrivilegedUser || $isAppUser; diff --git a/src/Appwrite/Platform/Modules/Teams/Http/Memberships/Update.php b/src/Appwrite/Platform/Modules/Teams/Http/Memberships/Update.php index 540dc8a871..2198531e64 100644 --- a/src/Appwrite/Platform/Modules/Teams/Http/Memberships/Update.php +++ b/src/Appwrite/Platform/Modules/Teams/Http/Memberships/Update.php @@ -84,7 +84,7 @@ class Update extends Action } $isPrivilegedUser = $user->isPrivileged($authorization->getRoles()); - $isAppUser = $user->isApp($authorization->getRoles()); + $isAppUser = $user->isKey($authorization->getRoles()); $isOwner = $authorization->hasRole('team:' . $team->getId() . '/owner'); if ($project->getId() === 'console') { diff --git a/src/Appwrite/Platform/Modules/Teams/Http/Memberships/XList.php b/src/Appwrite/Platform/Modules/Teams/Http/Memberships/XList.php index 7835c8051f..45f7eb33aa 100644 --- a/src/Appwrite/Platform/Modules/Teams/Http/Memberships/XList.php +++ b/src/Appwrite/Platform/Modules/Teams/Http/Memberships/XList.php @@ -134,7 +134,7 @@ class XList extends Action $roles = $authorization->getRoles(); $isPrivilegedUser = $user->isPrivileged($roles); - $isAppUser = $user->isApp($roles); + $isAppUser = $user->isKey($roles); $membershipsPrivacy = array_map(function ($privacy) use ($isPrivilegedUser, $isAppUser) { return $privacy || $isPrivilegedUser || $isAppUser; diff --git a/src/Appwrite/Platform/Modules/Teams/Http/Teams/Create.php b/src/Appwrite/Platform/Modules/Teams/Http/Teams/Create.php index 0d20a58b6b..222b0968be 100644 --- a/src/Appwrite/Platform/Modules/Teams/Http/Teams/Create.php +++ b/src/Appwrite/Platform/Modules/Teams/Http/Teams/Create.php @@ -71,7 +71,7 @@ class Create extends Action public function action(string $teamId, string $name, array $roles, Response $response, User $user, Database $dbForProject, Authorization $authorization, Event $queueForEvents) { $isPrivilegedUser = $user->isPrivileged($authorization->getRoles()); - $isAppUser = $user->isApp($authorization->getRoles()); + $isAppUser = $user->isKey($authorization->getRoles()); $teamId = $teamId == 'unique()' ? ID::unique() : $teamId; diff --git a/src/Appwrite/Platform/Modules/Tokens/Http/Tokens/Buckets/Files/Action.php b/src/Appwrite/Platform/Modules/Tokens/Http/Tokens/Buckets/Files/Action.php index 934074d3c2..85247678f8 100644 --- a/src/Appwrite/Platform/Modules/Tokens/Http/Tokens/Buckets/Files/Action.php +++ b/src/Appwrite/Platform/Modules/Tokens/Http/Tokens/Buckets/Files/Action.php @@ -15,7 +15,7 @@ class Action extends UtopiaAction { $bucket = $authorization->skip(fn () => $dbForProject->getDocument('buckets', $bucketId)); - $isAPIKey = $user->isApp($authorization->getRoles()); + $isAPIKey = $user->isKey($authorization->getRoles()); $isPrivilegedUser = $user->isPrivileged($authorization->getRoles()); if ($bucket->isEmpty() || (!$bucket->getAttribute('enabled') && !$isAPIKey && !$isPrivilegedUser)) { diff --git a/src/Appwrite/Platform/Modules/VCS/Http/GitHub/Authorize/External/Update.php b/src/Appwrite/Platform/Modules/VCS/Http/GitHub/Authorize/External/Update.php index a40d7fc6b9..993740c61a 100644 --- a/src/Appwrite/Platform/Modules/VCS/Http/GitHub/Authorize/External/Update.php +++ b/src/Appwrite/Platform/Modules/VCS/Http/GitHub/Authorize/External/Update.php @@ -130,7 +130,14 @@ class Update extends Action $providerCommitAuthor = $commitDetails["commitAuthor"] ?? ''; $providerCommitAuthorUrl = $commitDetails["commitAuthorUrl"] ?? ''; - $this->createGitDeployments($github, $providerInstallationId, $repositories, $providerBranch, $providerBranchUrl, $providerRepositoryName, $providerRepositoryUrl, $providerRepositoryOwner, $providerCommitHash, $providerCommitAuthor, $providerCommitAuthorUrl, $providerCommitMessage, $providerCommitUrl, $providerPullRequestId, true, $dbForPlatform, $authorization, $publisherForBuilds, $getProjectDB, $platform); + $prFiles = $github->getPullRequestFiles($owner, $providerRepositoryName, $providerPullRequestId); + $providerAffectedFiles = [ + ...array_column($prFiles, 'filename'), + // Only renamed files include previous_filename; skip missing values from other file changes. + ...array_filter(array_column($prFiles, 'previous_filename')) + ]; + + $this->createGitDeployments($github, $providerInstallationId, $repositories, $providerBranch, $providerBranchUrl, $providerRepositoryName, $providerRepositoryUrl, $providerRepositoryOwner, $providerCommitHash, $providerCommitAuthor, $providerCommitAuthorUrl, $providerCommitMessage, $providerCommitUrl, $providerPullRequestId, $providerAffectedFiles, true, $dbForPlatform, $authorization, $publisherForBuilds, $getProjectDB, $platform); $response->noContent(); } diff --git a/src/Appwrite/Platform/Modules/VCS/Http/GitHub/Deployment.php b/src/Appwrite/Platform/Modules/VCS/Http/GitHub/Deployment.php index a6f0e7fd6d..27c4eacba3 100644 --- a/src/Appwrite/Platform/Modules/VCS/Http/GitHub/Deployment.php +++ b/src/Appwrite/Platform/Modules/VCS/Http/GitHub/Deployment.php @@ -22,6 +22,7 @@ use Utopia\DSN\DSN; use Utopia\Span\Span; use Utopia\System\System; use Utopia\Validator\Contains; +use Utopia\Validator\Globstar; use Utopia\VCS\Adapter\Git\GitHub; use Utopia\VCS\Exception\RepositoryNotFound; @@ -42,6 +43,7 @@ trait Deployment string $providerCommitMessage, string $providerCommitUrl, string $providerPullRequestId, + array $providerAffectedFiles, bool $external, Database $dbForPlatform, Authorization $authorization, @@ -103,6 +105,32 @@ trait Deployment continue; } + // Skip deployments when the branch or affected files do not match configured build triggers. + $branchTrigger = new Globstar($resource->getAttribute('providerBranches', [])); + if (!$branchTrigger->isValid($providerBranch)) { + Span::add("{$logBase}.build.skipped.reason", 'branch'); + Span::add("{$logBase}.build.skipped", 'true'); + continue; + } + + $providerPaths = $resource->getAttribute('providerPaths', []); + if (!empty($providerPaths) && !empty($providerAffectedFiles)) { + $pathTrigger = new Globstar($providerPaths); + $pathMatched = false; + foreach ($providerAffectedFiles as $file) { + if ($pathTrigger->isValid($file)) { + $pathMatched = true; + break; + } + } + + if (!$pathMatched) { + Span::add("{$logBase}.build.skipped.reason", 'path'); + Span::add("{$logBase}.build.skipped", 'true'); + continue; + } + } + $deploymentId = ID::unique(); $repositoryId = $repository->getId(); $repositoryInternalId = $repository->getSequence(); diff --git a/src/Appwrite/Platform/Modules/VCS/Http/GitHub/Events/Create.php b/src/Appwrite/Platform/Modules/VCS/Http/GitHub/Events/Create.php index 0b81504309..c79df05f8a 100644 --- a/src/Appwrite/Platform/Modules/VCS/Http/GitHub/Events/Create.php +++ b/src/Appwrite/Platform/Modules/VCS/Http/GitHub/Events/Create.php @@ -133,7 +133,6 @@ class Create extends Action callable $getProjectDB, array $platform, ) { - $providerBranchCreated = $parsedPayload["branchCreated"] ?? false; $providerBranchDeleted = $parsedPayload["branchDeleted"] ?? false; $providerBranch = $parsedPayload["branch"] ?? ''; $providerBranchUrl = $parsedPayload["branchUrl"] ?? ''; @@ -164,7 +163,8 @@ class Create extends Action // Create new deployment only on push (not committed by us) and not when branch is deleted if ($providerCommitAuthorEmail !== APP_VCS_GITHUB_EMAIL && !$providerBranchDeleted) { - $this->createGitDeployments($github, $providerInstallationId, $repositories, $providerBranch, $providerBranchUrl, $providerRepositoryName, $providerRepositoryUrl, $providerRepositoryOwner, $providerCommitHash, $providerCommitAuthorName, $providerCommitAuthorUrl, $providerCommitMessage, $providerCommitUrl, '', false, $dbForPlatform, $authorization, $publisherForBuilds, $getProjectDB, $platform); + $providerAffectedFiles = $parsedPayload['affectedFiles'] ?? []; + $this->createGitDeployments($github, $providerInstallationId, $repositories, $providerBranch, $providerBranchUrl, $providerRepositoryName, $providerRepositoryUrl, $providerRepositoryOwner, $providerCommitHash, $providerCommitAuthorName, $providerCommitAuthorUrl, $providerCommitMessage, $providerCommitUrl, '', $providerAffectedFiles, false, $dbForPlatform, $authorization, $publisherForBuilds, $getProjectDB, $platform); } } @@ -211,12 +211,19 @@ class Create extends Action $providerCommitAuthor = $commitDetails["commitAuthor"] ?? ''; $providerCommitMessage = $commitDetails["commitMessage"] ?? ''; + $prFiles = $github->getPullRequestFiles($providerRepositoryOwner, $providerRepositoryName, $providerPullRequestId); + $providerAffectedFiles = [ + ...array_column($prFiles, 'filename'), + // Only renamed files include previous_filename; skip missing values from other file changes. + ...array_filter(array_column($prFiles, 'previous_filename')) + ]; + $repositories = $authorization->skip(fn () => $dbForPlatform->find('repositories', [ Query::equal('providerRepositoryId', [$providerRepositoryId]), Query::orderDesc('$createdAt') ])); - $this->createGitDeployments($github, $providerInstallationId, $repositories, $providerBranch, $providerBranchUrl, $providerRepositoryName, $providerRepositoryUrl, $providerRepositoryOwner, $providerCommitHash, $providerCommitAuthor, $providerCommitAuthorUrl, $providerCommitMessage, $providerCommitUrl, $providerPullRequestId, $external, $dbForPlatform, $authorization, $publisherForBuilds, $getProjectDB, $platform); + $this->createGitDeployments($github, $providerInstallationId, $repositories, $providerBranch, $providerBranchUrl, $providerRepositoryName, $providerRepositoryUrl, $providerRepositoryOwner, $providerCommitHash, $providerCommitAuthor, $providerCommitAuthorUrl, $providerCommitMessage, $providerCommitUrl, $providerPullRequestId, $providerAffectedFiles, $external, $dbForPlatform, $authorization, $publisherForBuilds, $getProjectDB, $platform); } elseif ($action == "closed") { // Allowed external contributions cleanup diff --git a/src/Appwrite/Platform/Workers/Audits.php b/src/Appwrite/Platform/Workers/Audits.php index f6b0345381..09863b8626 100644 --- a/src/Appwrite/Platform/Workers/Audits.php +++ b/src/Appwrite/Platform/Workers/Audits.php @@ -91,7 +91,7 @@ class Audits extends Action $actorUserEmail = $impersonatorUserId ? $user->getAttribute('impersonatorUserEmail', '') : $user->getAttribute('email', ''); - $userType = $user->getAttribute('type', ACTIVITY_TYPE_USER); + $userType = $user->getAttribute('type', ACTOR_TYPE_USER); // Create event data $eventData = [ @@ -100,7 +100,6 @@ class Audits extends Action 'resource' => $resource, 'userAgent' => $userAgent, 'ip' => $ip, - 'location' => '', 'data' => [ 'userId' => $actorUserId, 'userName' => $actorUserName, diff --git a/src/Appwrite/Presences/State.php b/src/Appwrite/Presences/State.php index 19e7dc98b7..e7c51dec87 100644 --- a/src/Appwrite/Presences/State.php +++ b/src/Appwrite/Presences/State.php @@ -48,7 +48,7 @@ class State $permissions[] = (new Permission($permission, 'user', $ownerOverride))->toString(); } } else { - $isAPIKey = $user->isApp($authorization->getRoles()); + $isAPIKey = $user->isKey($authorization->getRoles()); $isPrivilegedUser = $user->isPrivileged($authorization->getRoles()); $permissions = Permission::aggregate($permissions, $allowedPermissions); diff --git a/src/Appwrite/Utopia/Database/Documents/User.php b/src/Appwrite/Utopia/Database/Documents/User.php index 211c6449dc..3004efe382 100644 --- a/src/Appwrite/Utopia/Database/Documents/User.php +++ b/src/Appwrite/Utopia/Database/Documents/User.php @@ -17,7 +17,7 @@ class User extends Document public const ROLE_ADMIN = 'admin'; public const ROLE_DEVELOPER = 'developer'; public const ROLE_OWNER = 'owner'; - public const ROLE_APPS = 'apps'; + public const ROLE_KEYS = 'keys'; public const ROLE_SYSTEM = 'system'; public function getEmail(): ?string @@ -39,7 +39,7 @@ class User extends Document { $roles = []; - if (!$this->isApp($authorization->getRoles())) { + if (!$this->isKey($authorization->getRoles())) { if ($this->getId()) { $roles[] = Role::user($this->getId())->toString(); $roles[] = Role::users()->toString(); @@ -115,15 +115,15 @@ class User extends Document } /** - * Is App User? + * Is Key User? * * @param array $roles * * @return bool */ - public function isApp(array $roles): bool + public function isKey(array $roles): bool { - if (in_array(self::ROLE_APPS, $roles)) { + if (in_array(self::ROLE_KEYS, $roles)) { return true; } diff --git a/src/Appwrite/Utopia/Request.php b/src/Appwrite/Utopia/Request.php index 24803eeaa7..1070d993c8 100644 --- a/src/Appwrite/Utopia/Request.php +++ b/src/Appwrite/Utopia/Request.php @@ -240,7 +240,7 @@ class Request extends UtopiaRequest $forwardedUserAgent = $this->getHeader('x-forwarded-user-agent'); if (!empty($forwardedUserAgent)) { $roles = $this->authorization->getRoles(); - $isAppUser = $this->user?->isApp($roles) ?? false; + $isAppUser = $this->user?->isKey($roles) ?? false; if ($isAppUser) { return $forwardedUserAgent; diff --git a/src/Appwrite/Utopia/Response.php b/src/Appwrite/Utopia/Response.php index 66e4a5f4ae..c8b573fc49 100644 --- a/src/Appwrite/Utopia/Response.php +++ b/src/Appwrite/Utopia/Response.php @@ -591,7 +591,7 @@ class Response extends SwooleResponse $roles = $this->authorization->getRoles(); $user = $this->user ?? new DBUser(); $isPrivilegedUser = $user->isPrivileged($roles); - $isAppUser = $user->isApp($roles); + $isAppUser = $user->isKey($roles); if ((!$isPrivilegedUser && !$isAppUser) && !$this->showSensitive) { $data->setAttribute($key, ''); diff --git a/src/Appwrite/Utopia/Response/Model/Func.php b/src/Appwrite/Utopia/Response/Model/Func.php index 3aea364fe5..fa6040904c 100644 --- a/src/Appwrite/Utopia/Response/Model/Func.php +++ b/src/Appwrite/Utopia/Response/Model/Func.php @@ -182,6 +182,20 @@ class Func extends Model 'default' => false, 'example' => false, ]) + ->addRule('providerBranches', [ + 'type' => self::TYPE_STRING, + 'description' => 'List of branch name patterns that trigger automatic deployments. Supports glob wildcards. Empty list deploys on all branches.', + 'default' => [], + 'example' => ['main', 'feat/*'], + 'array' => true, + ]) + ->addRule('providerPaths', [ + 'type' => self::TYPE_STRING, + 'description' => 'List of file path patterns that trigger automatic deployments. Supports glob wildcards. Empty list deploys on all file changes.', + 'default' => [], + 'example' => ['src/**', '!docs/**'], + 'array' => true, + ]) ->addRule('buildSpecification', [ 'type' => self::TYPE_STRING, 'description' => 'Machine specification for deployment builds.', diff --git a/src/Appwrite/Utopia/Response/Model/Project.php b/src/Appwrite/Utopia/Response/Model/Project.php index af2a21d551..c788833e88 100644 --- a/src/Appwrite/Utopia/Response/Model/Project.php +++ b/src/Appwrite/Utopia/Response/Model/Project.php @@ -94,7 +94,7 @@ class Project extends Model ->addRule('smtpPort', [ 'type' => self::TYPE_INTEGER, 'description' => 'SMTP server port', - 'default' => '', + 'default' => 0, 'example' => 25, ]) ->addRule('smtpUsername', [ @@ -225,7 +225,7 @@ class Project extends Model $document->setAttribute('smtpReplyToEmail', $smtp['replyToEmail'] ?? $smtp['replyTo'] ?? ''); // Includes backwards compatibility $document->setAttribute('smtpReplyToName', $smtp['replyToName'] ?? ''); $document->setAttribute('smtpHost', $smtp['host'] ?? ''); - $document->setAttribute('smtpPort', $smtp['port'] ?? ''); + $document->setAttribute('smtpPort', (int) ($smtp['port'] ?? 0)); $document->setAttribute('smtpUsername', $smtp['username'] ?? ''); $document->setAttribute('smtpPassword', ''); // Write-only: never expose the stored value $document->setAttribute('smtpSecure', $smtp['secure'] ?? ''); diff --git a/src/Appwrite/Utopia/Response/Model/Site.php b/src/Appwrite/Utopia/Response/Model/Site.php index 941b6104df..330dd1c777 100644 --- a/src/Appwrite/Utopia/Response/Model/Site.php +++ b/src/Appwrite/Utopia/Response/Model/Site.php @@ -173,6 +173,20 @@ class Site extends Model 'default' => false, 'example' => false, ]) + ->addRule('providerBranches', [ + 'type' => self::TYPE_STRING, + 'description' => 'List of branch name patterns that trigger automatic deployments. Supports glob wildcards. Empty list deploys on all branches.', + 'default' => [], + 'example' => ['main', 'feat/*'], + 'array' => true, + ]) + ->addRule('providerPaths', [ + 'type' => self::TYPE_STRING, + 'description' => 'List of file path patterns that trigger automatic deployments. Supports glob wildcards. Empty list deploys on all file changes.', + 'default' => [], + 'example' => ['src/**', '!docs/**'], + 'array' => true, + ]) ->addRule('buildSpecification', [ 'type' => self::TYPE_STRING, 'description' => 'Machine specification for deployment builds.', diff --git a/tests/e2e/General/HTTPTest.php b/tests/e2e/General/HTTPTest.php index 450e4f2378..0358281eb7 100644 --- a/tests/e2e/General/HTTPTest.php +++ b/tests/e2e/General/HTTPTest.php @@ -101,7 +101,7 @@ class HTTPTest extends Scope $body = $response['body']; $this->assertEquals(200, $response['headers']['status-code']); $this->assertIsString($body['server']); - $this->assertIsString($body['client-web']); + $this->assertIsString($body['server-web']); $this->assertIsString($body['client-flutter']); $this->assertIsString($body['console-web']); $this->assertIsString($body['server-nodejs']); diff --git a/tests/e2e/Services/Account/AccountCustomClientTest.php b/tests/e2e/Services/Account/AccountCustomClientTest.php index 160ee39e21..5b0d947198 100644 --- a/tests/e2e/Services/Account/AccountCustomClientTest.php +++ b/tests/e2e/Services/Account/AccountCustomClientTest.php @@ -1026,123 +1026,101 @@ class AccountCustomClientTest extends Scope // Use fresh account for predictable log count $data = $this->createFreshAccountWithSession(); $session = $data['session']; + $headers = array_merge([ + 'origin' => 'http://localhost', + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + 'cookie' => 'a_session_' . $this->getProject()['$id'] . '=' . $session, + ]); /** * Test for SUCCESS */ - $response = $this->client->call(Client::METHOD_GET, '/account/logs', array_merge([ - 'origin' => 'http://localhost', - 'content-type' => 'application/json', - 'x-appwrite-project' => $this->getProject()['$id'], - 'cookie' => 'a_session_' . $this->getProject()['$id'] . '=' . $session, - ])); + $this->assertEventually(function () use ($headers) { + $response = $this->client->call(Client::METHOD_GET, '/account/logs', $headers); - $this->assertEquals(200, $response['headers']['status-code']); - $this->assertIsArray($response['body']['logs']); - $this->assertNotEmpty($response['body']['logs']); - // Fresh account: session.create is always logged. user.create audit may or may not - // be present depending on async audit processing timing. - $logCount = count($response['body']['logs']); - $this->assertContains($logCount, [1, 2]); - $this->assertIsNumeric($response['body']['total']); + $this->assertEquals(200, $response['headers']['status-code']); + $this->assertIsArray($response['body']['logs']); + $this->assertNotEmpty($response['body']['logs']); + $logCount = count($response['body']['logs']); + $this->assertContains($logCount, [1, 2]); + $this->assertIsNumeric($response['body']['total']); - // Check session.create log (logs[0] - most recent) - $this->assertEquals('Windows', $response['body']['logs'][0]['osName']); - $this->assertEquals('WIN', $response['body']['logs'][0]['osCode']); - $this->assertEquals('10', $response['body']['logs'][0]['osVersion']); + $this->assertEquals('session.create', $response['body']['logs'][0]['event']); + $this->assertEquals('Windows', $response['body']['logs'][0]['osName']); + $this->assertEquals('WIN', $response['body']['logs'][0]['osCode']); + $this->assertEquals('10', $response['body']['logs'][0]['osVersion']); - $this->assertEquals('browser', $response['body']['logs'][0]['clientType']); - $this->assertEquals('Chrome', $response['body']['logs'][0]['clientName']); - $this->assertEquals('CH', $response['body']['logs'][0]['clientCode']); - $this->assertEquals('70.0', $response['body']['logs'][0]['clientVersion']); - $this->assertEquals('Blink', $response['body']['logs'][0]['clientEngine']); + $this->assertEquals('browser', $response['body']['logs'][0]['clientType']); + $this->assertEquals('Chrome', $response['body']['logs'][0]['clientName']); + $this->assertEquals('CH', $response['body']['logs'][0]['clientCode']); + $this->assertEquals('70.0', $response['body']['logs'][0]['clientVersion']); + $this->assertEquals('Blink', $response['body']['logs'][0]['clientEngine']); - $this->assertEquals('desktop', $response['body']['logs'][0]['deviceName']); - $this->assertEquals('', $response['body']['logs'][0]['deviceBrand']); - $this->assertEquals('', $response['body']['logs'][0]['deviceModel']); - $this->assertEquals(filter_var($response['body']['logs'][0]['ip'], FILTER_VALIDATE_IP), $response['body']['logs'][0]['ip']); + $this->assertEquals('desktop', $response['body']['logs'][0]['deviceName']); + $this->assertEquals('', $response['body']['logs'][0]['deviceBrand']); + $this->assertEquals('', $response['body']['logs'][0]['deviceModel']); + $this->assertEquals(filter_var($response['body']['logs'][0]['ip'], FILTER_VALIDATE_IP), $response['body']['logs'][0]['ip']); - $this->assertEquals('--', $response['body']['logs'][0]['countryCode']); - $this->assertEquals('Unknown', $response['body']['logs'][0]['countryName']); + $this->assertEquals('--', $response['body']['logs'][0]['countryCode']); + $this->assertEquals('Unknown', $response['body']['logs'][0]['countryName']); - if ($logCount === 2) { - // Check user.create log (logs[1] - oldest) - $this->assertEquals('user.create', $response['body']['logs'][1]['event']); - $this->assertEquals(filter_var($response['body']['logs'][1]['ip'], FILTER_VALIDATE_IP), $response['body']['logs'][1]['ip']); - $this->assertTrue((new DatetimeValidator())->isValid($response['body']['logs'][1]['time'])); - } + if ($logCount === 2) { + $this->assertEquals('user.create', $response['body']['logs'][1]['event']); + $this->assertEquals(filter_var($response['body']['logs'][1]['ip'], FILTER_VALIDATE_IP), $response['body']['logs'][1]['ip']); + $this->assertTrue((new DatetimeValidator())->isValid($response['body']['logs'][1]['time'])); + } - $responseLimit = $this->client->call(Client::METHOD_GET, '/account/logs', array_merge([ - 'origin' => 'http://localhost', - 'content-type' => 'application/json', - 'x-appwrite-project' => $this->getProject()['$id'], - 'cookie' => 'a_session_' . $this->getProject()['$id'] . '=' . $session, - ]), [ - 'queries' => [ - Query::limit(1)->toString() - ] - ]); + $responseLimit = $this->client->call(Client::METHOD_GET, '/account/logs', $headers, [ + 'queries' => [ + Query::limit(1)->toString() + ] + ]); - $this->assertEquals(200, $responseLimit['headers']['status-code']); - $this->assertIsArray($responseLimit['body']['logs']); - $this->assertNotEmpty($responseLimit['body']['logs']); - $this->assertCount(1, $responseLimit['body']['logs']); - $this->assertIsNumeric($responseLimit['body']['total']); + $this->assertEquals(200, $responseLimit['headers']['status-code']); + $this->assertIsArray($responseLimit['body']['logs']); + $this->assertNotEmpty($responseLimit['body']['logs']); + $this->assertCount(1, $responseLimit['body']['logs']); + $this->assertIsNumeric($responseLimit['body']['total']); - $this->assertEquals($response['body']['logs'][0], $responseLimit['body']['logs'][0]); + $this->assertEquals($response['body']['logs'][0], $responseLimit['body']['logs'][0]); - $responseOffset = $this->client->call(Client::METHOD_GET, '/account/logs', array_merge([ - 'origin' => 'http://localhost', - 'content-type' => 'application/json', - 'x-appwrite-project' => $this->getProject()['$id'], - 'cookie' => 'a_session_' . $this->getProject()['$id'] . '=' . $session, - ]), [ - 'queries' => [ - Query::offset(1)->toString() - ] - ]); + $responseOffset = $this->client->call(Client::METHOD_GET, '/account/logs', $headers, [ + 'queries' => [ + Query::offset(1)->toString() + ] + ]); - $this->assertEquals($responseOffset['headers']['status-code'], 200); - $this->assertIsArray($responseOffset['body']['logs']); - // With offset(1), remaining logs = logCount - 1 - $this->assertCount($logCount - 1, $responseOffset['body']['logs']); - $this->assertIsNumeric($responseOffset['body']['total']); + $this->assertEquals(200, $responseOffset['headers']['status-code']); + $this->assertIsArray($responseOffset['body']['logs']); + $this->assertCount($logCount - 1, $responseOffset['body']['logs']); + $this->assertIsNumeric($responseOffset['body']['total']); - if ($logCount === 2) { - $this->assertEquals($response['body']['logs'][1], $responseOffset['body']['logs'][0]); - } + if ($logCount === 2) { + $this->assertEquals($response['body']['logs'][1], $responseOffset['body']['logs'][0]); + } - $responseLimitOffset = $this->client->call(Client::METHOD_GET, '/account/logs', array_merge([ - 'origin' => 'http://localhost', - 'content-type' => 'application/json', - 'x-appwrite-project' => $this->getProject()['$id'], - 'cookie' => 'a_session_' . $this->getProject()['$id'] . '=' . $session, - ]), [ - 'queries' => [ - Query::offset(1)->toString(), - Query::limit(1)->toString() - ] - ]); + $responseLimitOffset = $this->client->call(Client::METHOD_GET, '/account/logs', $headers, [ + 'queries' => [ + Query::offset(1)->toString(), + Query::limit(1)->toString() + ] + ]); - $this->assertEquals(200, $responseLimitOffset['headers']['status-code']); - $this->assertIsArray($responseLimitOffset['body']['logs']); - // With offset(1)+limit(1), remaining logs = min(1, logCount - 1) - $this->assertCount(min(1, $logCount - 1), $responseLimitOffset['body']['logs']); - $this->assertIsNumeric($responseLimitOffset['body']['total']); + $this->assertEquals(200, $responseLimitOffset['headers']['status-code']); + $this->assertIsArray($responseLimitOffset['body']['logs']); + $this->assertCount(min(1, $logCount - 1), $responseLimitOffset['body']['logs']); + $this->assertIsNumeric($responseLimitOffset['body']['total']); - if ($logCount === 2) { - $this->assertEquals($response['body']['logs'][1], $responseLimitOffset['body']['logs'][0]); - } + if ($logCount === 2) { + $this->assertEquals($response['body']['logs'][1], $responseLimitOffset['body']['logs'][0]); + } + }); /** * Test for total=false */ - $logsWithIncludeTotalFalse = $this->client->call(Client::METHOD_GET, '/account/logs', array_merge([ - 'origin' => 'http://localhost', - 'content-type' => 'application/json', - 'x-appwrite-project' => $this->getProject()['$id'], - 'cookie' => 'a_session_' . $this->getProject()['$id'] . '=' . $session, - ]), [ + $logsWithIncludeTotalFalse = $this->client->call(Client::METHOD_GET, '/account/logs', $headers, [ 'total' => false ]); diff --git a/tests/e2e/Services/Functions/FunctionsCustomServerTest.php b/tests/e2e/Services/Functions/FunctionsCustomServerTest.php index b1f07c3f9d..44d5d274da 100644 --- a/tests/e2e/Services/Functions/FunctionsCustomServerTest.php +++ b/tests/e2e/Services/Functions/FunctionsCustomServerTest.php @@ -2778,17 +2778,17 @@ class FunctionsCustomServerTest extends Scope $this->assertEmpty($executions['body']['executions'][0]['logs']); $this->assertEmpty($executions['body']['executions'][0]['errors']); - // Ensure executions count - $executions = $this->listExecutions($functionId); + $this->assertEventually(function () use ($functionId) { + $executions = $this->listExecutions($functionId); - $this->assertEquals(200, $executions['headers']['status-code']); - $this->assertCount(3, $executions['body']['executions']); + $this->assertEquals(200, $executions['headers']['status-code']); + $this->assertCount(3, $executions['body']['executions']); - // Double check logs and errors are empty - foreach ($executions['body']['executions'] as $execution) { - $this->assertEmpty($execution['logs']); - $this->assertEmpty($execution['errors']); - } + foreach ($executions['body']['executions'] as $execution) { + $this->assertEmpty($execution['logs']); + $this->assertEmpty($execution['errors']); + } + }, 10000, 500); $this->cleanupFunction($functionId); } diff --git a/tests/e2e/Services/Organization/ProjectsBase.php b/tests/e2e/Services/Organization/ProjectsBase.php new file mode 100644 index 0000000000..4e18050670 --- /dev/null +++ b/tests/e2e/Services/Organization/ProjectsBase.php @@ -0,0 +1,484 @@ +client->call(Client::METHOD_POST, '/teams', array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + ], $this->getHeaders()), [ + 'teamId' => $teamId, + 'name' => 'Organization Test', + ]); + if (\in_array($team['headers']['status-code'], [201, 409])) { + break; + } + \usleep(500000); + } + $this->assertContains($team['headers']['status-code'], [201, 409], 'Setup organization (team) failed'); + + self::$cachedOrganization = [ + 'teamId' => $team['body']['$id'] ?? $teamId, + ]; + + return self::$cachedOrganization; + } + + protected function getOrganizationHeaders(): array + { + $organization = $this->setupOrganization(); + + return array_merge($this->getHeaders(), [ + 'x-appwrite-organization' => $organization['teamId'], + ]); + } + + /** + * Setup and cache a project created via organization endpoints. + */ + protected function setupOrganizationProject(): array + { + if (!empty(self::$cachedProjectData)) { + return self::$cachedProjectData; + } + + $project = null; + for ($i = 0; $i < 3; $i++) { + $project = $this->client->call(Client::METHOD_POST, '/v1/organization/projects', array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + ], $this->getOrganizationHeaders()), [ + 'projectId' => ID::unique(), + 'name' => 'Organization Project Test', + 'region' => System::getEnv('_APP_REGION', 'default'), + ]); + if ($project['headers']['status-code'] === 201) { + break; + } + \usleep(500000); + } + $this->assertEquals(201, $project['headers']['status-code'], 'Setup organization project failed'); + + self::$cachedProjectData = [ + 'projectId' => $project['body']['$id'], + 'teamId' => $this->setupOrganization()['teamId'], + ]; + + return self::$cachedProjectData; + } + + public function testCreateProject(): void + { + $organization = $this->setupOrganization(); + $teamId = $organization['teamId']; + + /** + * Test for SUCCESS + */ + $response = $this->client->call(Client::METHOD_POST, '/v1/organization/projects', array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + ], $this->getOrganizationHeaders()), [ + 'projectId' => ID::unique(), + 'name' => 'Organization Project Test', + 'region' => System::getEnv('_APP_REGION', 'default'), + ]); + + $this->assertEquals(201, $response['headers']['status-code']); + $this->assertNotEmpty($response['body']['$id']); + $this->assertEquals('Organization Project Test', $response['body']['name']); + $this->assertEquals($teamId, $response['body']['teamId']); + $this->assertEquals(PROJECT_STATUS_ACTIVE, $response['body']['status']); + $this->assertArrayHasKey('platforms', $response['body']); + $this->assertArrayHasKey('webhooks', $response['body']); + $this->assertArrayHasKey('keys', $response['body']); + + /** + * Test for FAILURE - missing organization header + */ + $response = $this->client->call(Client::METHOD_POST, '/v1/organization/projects', array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + ], $this->getHeaders()), [ + 'projectId' => ID::unique(), + 'name' => 'Organization Project Test', + 'region' => System::getEnv('_APP_REGION', 'default'), + ]); + + $this->assertEquals(404, $response['headers']['status-code']); + + /** + * Test for FAILURE - empty name + */ + $response = $this->client->call(Client::METHOD_POST, '/v1/organization/projects', array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + ], $this->getOrganizationHeaders()), [ + 'projectId' => ID::unique(), + 'name' => '', + 'region' => System::getEnv('_APP_REGION', 'default'), + ]); + + $this->assertEquals(400, $response['headers']['status-code']); + } + + public function testCreateDuplicateProject(): void + { + $organization = $this->setupOrganization(); + $projectId = ID::unique(); + + $response = $this->client->call(Client::METHOD_POST, '/v1/organization/projects', array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + ], $this->getOrganizationHeaders()), [ + 'projectId' => $projectId, + 'name' => 'Original Organization Project', + 'region' => System::getEnv('_APP_REGION', 'default'), + ]); + + $this->assertEquals(201, $response['headers']['status-code']); + + /** + * Test for FAILURE - duplicate project ID + */ + $response = $this->client->call(Client::METHOD_POST, '/v1/organization/projects', array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + ], $this->getOrganizationHeaders()), [ + 'projectId' => $projectId, + 'name' => 'Duplicate Organization Project', + 'region' => System::getEnv('_APP_REGION', 'default'), + ]); + + $this->assertEquals(409, $response['headers']['status-code']); + $this->assertEquals(409, $response['body']['code']); + $this->assertEquals(Exception::PROJECT_ALREADY_EXISTS, $response['body']['type']); + } + + public function testGetProject(): void + { + $data = $this->setupOrganizationProject(); + $projectId = $data['projectId']; + + /** + * Test for SUCCESS + */ + $response = $this->client->call(Client::METHOD_GET, '/v1/organization/projects/' . $projectId, array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + ], $this->getOrganizationHeaders())); + + $this->assertEquals(200, $response['headers']['status-code']); + $this->assertNotEmpty($response['body']['$id']); + $this->assertEquals($projectId, $response['body']['$id']); + $this->assertEquals('Organization Project Test', $response['body']['name']); + $this->assertEquals(PROJECT_STATUS_ACTIVE, $response['body']['status']); + $this->assertArrayHasKey('platforms', $response['body']); + $this->assertArrayHasKey('webhooks', $response['body']); + $this->assertArrayHasKey('keys', $response['body']); + + /** + * Test for FAILURE - project not found + */ + $response = $this->client->call(Client::METHOD_GET, '/v1/organization/projects/' . ID::unique(), array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + ], $this->getOrganizationHeaders())); + + $this->assertEquals(404, $response['headers']['status-code']); + + /** + * Test for FAILURE - project from different organization + */ + $otherTeam = $this->client->call(Client::METHOD_POST, '/teams', array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + ], $this->getHeaders()), [ + 'teamId' => ID::unique(), + 'name' => 'Other Organization', + ]); + $this->assertContains($otherTeam['headers']['status-code'], [201, 409]); + $otherTeamId = $otherTeam['body']['$id'] ?? $otherTeam['body']['teamId']; + + $otherProject = $this->client->call(Client::METHOD_POST, '/v1/organization/projects', array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + ], array_merge($this->getHeaders(), [ + 'x-appwrite-organization' => $otherTeamId, + ])), [ + 'projectId' => ID::unique(), + 'name' => 'Other Organization Project', + 'region' => System::getEnv('_APP_REGION', 'default'), + ]); + $this->assertEquals(201, $otherProject['headers']['status-code']); + $otherProjectId = $otherProject['body']['$id']; + + $response = $this->client->call(Client::METHOD_GET, '/v1/organization/projects/' . $otherProjectId, array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + ], $this->getOrganizationHeaders())); + + $this->assertEquals(404, $response['headers']['status-code']); + } + + public function testUpdateProject(): void + { + $data = $this->setupOrganizationProject(); + $projectId = $data['projectId']; + + /** + * Test for SUCCESS + */ + $response = $this->client->call(Client::METHOD_PATCH, '/v1/organization/projects/' . $projectId, array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + ], $this->getOrganizationHeaders()), [ + 'name' => 'Updated Organization Project', + ]); + + $this->assertEquals(200, $response['headers']['status-code']); + $this->assertEquals($projectId, $response['body']['$id']); + $this->assertEquals('Updated Organization Project', $response['body']['name']); + + /** + * Test for FAILURE - project not found + */ + $response = $this->client->call(Client::METHOD_PATCH, '/v1/organization/projects/' . ID::unique(), array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + ], $this->getOrganizationHeaders()), [ + 'name' => 'Should Fail', + ]); + + $this->assertEquals(404, $response['headers']['status-code']); + + /** + * Test for FAILURE - empty name + */ + $response = $this->client->call(Client::METHOD_PATCH, '/v1/organization/projects/' . $projectId, array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + ], $this->getOrganizationHeaders()), [ + 'name' => '', + ]); + + $this->assertEquals(400, $response['headers']['status-code']); + } + + public function testDeleteProject(): void + { + $organization = $this->setupOrganization(); + + $project = $this->client->call(Client::METHOD_POST, '/v1/organization/projects', array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + ], $this->getOrganizationHeaders()), [ + 'projectId' => ID::unique(), + 'name' => 'Project To Delete', + 'region' => System::getEnv('_APP_REGION', 'default'), + ]); + + $this->assertEquals(201, $project['headers']['status-code']); + $projectId = $project['body']['$id']; + + /** + * Test for SUCCESS + */ + $response = $this->client->call(Client::METHOD_DELETE, '/v1/organization/projects/' . $projectId, array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + ], $this->getOrganizationHeaders())); + + $this->assertEquals(204, $response['headers']['status-code']); + + // Verify project is actually deleted + $response = $this->client->call(Client::METHOD_GET, '/v1/organization/projects/' . $projectId, array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + ], $this->getOrganizationHeaders())); + + $this->assertEquals(404, $response['headers']['status-code']); + + /** + * Test for FAILURE - project not found (already deleted) + */ + $response = $this->client->call(Client::METHOD_DELETE, '/v1/organization/projects/' . $projectId, array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + ], $this->getOrganizationHeaders())); + + $this->assertEquals(404, $response['headers']['status-code']); + } + + public function testListProjects(): void + { + $organization = $this->setupOrganization(); + $teamId = $organization['teamId']; + + // Create a second project in the same organization + $project2 = $this->client->call(Client::METHOD_POST, '/v1/organization/projects', array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + ], $this->getOrganizationHeaders()), [ + 'projectId' => ID::unique(), + 'name' => 'Second Organization Project', + 'region' => System::getEnv('_APP_REGION', 'default'), + ]); + + $this->assertEquals(201, $project2['headers']['status-code']); + $project2Id = $project2['body']['$id']; + + /** + * Test for SUCCESS - basic list + */ + $response = $this->client->call(Client::METHOD_GET, '/v1/organization/projects', array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + ], $this->getOrganizationHeaders())); + + $this->assertEquals(200, $response['headers']['status-code']); + $this->assertNotEmpty($response['body']); + $this->assertGreaterThan(0, count($response['body']['projects'])); + $this->assertGreaterThan(0, $response['body']['total']); + + /** + * Test search queries + */ + $response = $this->client->call(Client::METHOD_GET, '/v1/organization/projects', array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + ], $this->getOrganizationHeaders(), [ + 'search' => 'Second Organization Project', + ])); + + $this->assertEquals(200, $response['headers']['status-code']); + $this->assertGreaterThan(0, $response['body']['total']); + $this->assertIsArray($response['body']['projects']); + $this->assertEquals('Second Organization Project', $response['body']['projects'][0]['name']); + + /** + * Test pagination with limit + */ + $response = $this->client->call(Client::METHOD_GET, '/v1/organization/projects', array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + ], $this->getOrganizationHeaders()), [ + 'queries' => [ + Query::limit(1)->toString(), + ], + ]); + + $this->assertEquals(200, $response['headers']['status-code']); + $this->assertCount(1, $response['body']['projects']); + + /** + * Test pagination with offset + */ + $response = $this->client->call(Client::METHOD_GET, '/v1/organization/projects', array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + ], $this->getOrganizationHeaders()), [ + 'queries' => [ + Query::offset(1)->toString(), + ], + ]); + + $this->assertEquals(200, $response['headers']['status-code']); + $this->assertNotEmpty($response['body']); + + /** + * Test query by name + */ + $response = $this->client->call(Client::METHOD_GET, '/v1/organization/projects', array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + ], $this->getOrganizationHeaders()), [ + 'queries' => [ + Query::equal('name', ['Second Organization Project'])->toString(), + ], + ]); + + $this->assertEquals(200, $response['headers']['status-code']); + $this->assertGreaterThanOrEqual(1, count($response['body']['projects'])); + $this->assertEquals('Second Organization Project', $response['body']['projects'][0]['name']); + + /** + * Test cursor pagination + */ + $response = $this->client->call(Client::METHOD_GET, '/v1/organization/projects', array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + ], $this->getOrganizationHeaders())); + + $this->assertEquals(200, $response['headers']['status-code']); + $this->assertNotEmpty($response['body']['projects']); + + $response = $this->client->call(Client::METHOD_GET, '/v1/organization/projects', array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + ], $this->getOrganizationHeaders()), [ + 'queries' => [ + Query::cursorAfter(new Document(['$id' => $response['body']['projects'][0]['$id']]))->toString(), + ], + ]); + + $this->assertEquals(200, $response['headers']['status-code']); + $this->assertNotEmpty($response['body']); + + /** + * Test for FAILURE - invalid cursor + */ + $response = $this->client->call(Client::METHOD_GET, '/v1/organization/projects', array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + ], $this->getOrganizationHeaders()), [ + 'queries' => [ + Query::cursorAfter(new Document(['$id' => 'unknown']))->toString(), + ], + ]); + + $this->assertEquals(400, $response['headers']['status-code']); + } + + public function testListProjectsQuerySelect(): void + { + $data = $this->setupOrganizationProject(); + $projectId = $data['projectId']; + + $response = $this->client->call(Client::METHOD_GET, '/v1/organization/projects', array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + ], $this->getOrganizationHeaders()), [ + 'queries' => [ + Query::select(['name'])->toString(), + ], + ]); + + $this->assertEquals(200, $response['headers']['status-code']); + $this->assertNotEmpty($response['body']['projects']); + $this->assertEquals('Organization Project Test', $response['body']['projects'][0]['name']); + } +} diff --git a/tests/e2e/Services/Organization/ProjectsConsoleClientTest.php b/tests/e2e/Services/Organization/ProjectsConsoleClientTest.php new file mode 100644 index 0000000000..5d016eff01 --- /dev/null +++ b/tests/e2e/Services/Organization/ProjectsConsoleClientTest.php @@ -0,0 +1,14 @@ +assertSame('', $response['body']['smtpReplyToEmail']); $this->assertSame('', $response['body']['smtpReplyToName']); $this->assertSame('', $response['body']['smtpHost']); - $this->assertSame('', $response['body']['smtpPort']); + $this->assertSame(0, $response['body']['smtpPort']); $this->assertSame('', $response['body']['smtpUsername']); $this->assertSame('', $response['body']['smtpPassword']); $this->assertSame('', $response['body']['smtpSecure']); diff --git a/tests/e2e/Services/Projects/ProjectsConsoleClientTest.php b/tests/e2e/Services/Projects/ProjectsConsoleClientTest.php index aa5e6911f1..7b44b0485d 100644 --- a/tests/e2e/Services/Projects/ProjectsConsoleClientTest.php +++ b/tests/e2e/Services/Projects/ProjectsConsoleClientTest.php @@ -1802,7 +1802,13 @@ class ProjectsConsoleClientTest extends Scope $this->assertEquals('en-us', $response['body']['locale']); /** Update Email template, fail due to SMTP disabled */ - $response = $this->client->call(Client::METHOD_PATCH, '/projects/' . $id . '/templates/email/verification/en-us', array_merge([ + $projectWithoutSmtp = $this->setupProject([ + 'projectId' => ID::unique(), + 'name' => 'Project Without SMTP', + 'region' => System::getEnv('_APP_REGION', 'default') + ]); + + $response = $this->client->call(Client::METHOD_PATCH, '/projects/' . $projectWithoutSmtp . '/templates/email/verification/en-us', array_merge([ 'content-type' => 'application/json', 'x-appwrite-project' => $this->getProject()['$id'], 'x-appwrite-response-format' => '1.9.1', diff --git a/tests/unit/Auth/KeyTest.php b/tests/unit/Auth/KeyTest.php index bcdb46180f..674fdf09aa 100644 --- a/tests/unit/Auth/KeyTest.php +++ b/tests/unit/Auth/KeyTest.php @@ -22,7 +22,7 @@ class KeyTest extends TestCase 'collections.read', 'documents.read', ]; - $roleScopes = Config::getParam('roles', [])[User::ROLE_APPS]['scopes']; + $roleScopes = Config::getParam('roles', [])[User::ROLE_KEYS]['scopes']; $guestRoleScopes = Config::getParam('roles', [])[User::ROLE_GUESTS]['scopes']; $key = self::generateKey($projectId, $usage, $scopes); @@ -37,7 +37,7 @@ class KeyTest extends TestCase $this->assertEquals('', $decoded->getTeamId()); $this->assertEquals('', $decoded->getUserId()); $this->assertEquals(API_KEY_EPHEMERAL, $decoded->getType()); - $this->assertEquals(User::ROLE_APPS, $decoded->getRole()); + $this->assertEquals(User::ROLE_KEYS, $decoded->getRole()); $this->assertEquals(\array_merge($scopes, $roleScopes), $decoded->getScopes()); $this->assertEquals('Ephemeral Key', $decoded->getName()); @@ -61,7 +61,7 @@ class KeyTest extends TestCase $this->assertEquals('', $decoded->getTeamId()); $this->assertEquals('', $decoded->getUserId()); $this->assertEquals(API_KEY_EPHEMERAL, $decoded->getType()); - $this->assertEquals(User::ROLE_APPS, $decoded->getRole()); + $this->assertEquals(User::ROLE_KEYS, $decoded->getRole()); $this->assertEquals(\array_merge($scopes, $roleScopes), $decoded->getScopes()); $this->assertEquals('Ephemeral Key', $decoded->getName()); $this->assertEquals(['metric123'], $decoded->getDisabledMetrics()); @@ -123,7 +123,7 @@ class KeyTest extends TestCase $this->assertEquals('', $decoded->getTeamId()); $this->assertEquals('', $decoded->getUserId()); $this->assertEquals(API_KEY_STANDARD, $decoded->getType()); - $this->assertEquals(User::ROLE_APPS, $decoded->getRole()); + $this->assertEquals(User::ROLE_KEYS, $decoded->getRole()); $this->assertEquals(\array_merge($scopes, $roleScopes), $decoded->getScopes()); $this->assertEquals('Standard key', $decoded->getName()); @@ -146,7 +146,7 @@ class KeyTest extends TestCase $this->assertEquals('', $decoded->getTeamId()); $this->assertEquals('', $decoded->getUserId()); $this->assertEquals(API_KEY_STANDARD, $decoded->getType()); - $this->assertEquals(User::ROLE_APPS, $decoded->getRole()); + $this->assertEquals(User::ROLE_KEYS, $decoded->getRole()); $this->assertEquals(\array_merge($scopes, $roleScopes), $decoded->getScopes()); $this->assertEquals('Standard key', $decoded->getName()); @@ -194,7 +194,7 @@ class KeyTest extends TestCase $this->assertEquals('', $decoded->getTeamId()); $this->assertEquals('', $decoded->getUserId()); $this->assertEquals(API_KEY_STANDARD, $decoded->getType()); - $this->assertEquals(User::ROLE_APPS, $decoded->getRole()); + $this->assertEquals(User::ROLE_KEYS, $decoded->getRole()); $this->assertEquals(\array_merge($scopes, $roleScopes), $decoded->getScopes()); $this->assertEquals('Standard key', $decoded->getName()); @@ -289,7 +289,7 @@ class KeyTest extends TestCase $this->assertEquals($teamId, $decoded->getTeamId()); $this->assertEquals('', $decoded->getUserId()); $this->assertEquals(API_KEY_ORGANIZATION, $decoded->getType()); - $this->assertEquals(User::ROLE_APPS, $decoded->getRole()); + $this->assertEquals(User::ROLE_KEYS, $decoded->getRole()); $this->assertEquals($scopes, $decoded->getScopes()); $this->assertEquals('Organization key', $decoded->getName()); @@ -336,7 +336,7 @@ class KeyTest extends TestCase $this->assertEquals($teamId, $decoded->getTeamId()); $this->assertEquals('', $decoded->getUserId()); $this->assertEquals(API_KEY_ORGANIZATION, $decoded->getType()); - $this->assertEquals(User::ROLE_APPS, $decoded->getRole()); + $this->assertEquals(User::ROLE_KEYS, $decoded->getRole()); $this->assertEquals($scopes, $decoded->getScopes()); $this->assertEquals('Organization key', $decoded->getName()); } diff --git a/tests/unit/Utopia/Database/Documents/UserTest.php b/tests/unit/Utopia/Database/Documents/UserTest.php index b3638e7d3a..5bd73db132 100644 --- a/tests/unit/Utopia/Database/Documents/UserTest.php +++ b/tests/unit/Utopia/Database/Documents/UserTest.php @@ -179,11 +179,11 @@ class UserTest extends TestCase $this->assertEquals(true, $user->isPrivileged([User::ROLE_ADMIN])); $this->assertEquals(true, $user->isPrivileged([User::ROLE_DEVELOPER])); $this->assertEquals(true, $user->isPrivileged([User::ROLE_OWNER])); - $this->assertEquals(false, $user->isPrivileged([User::ROLE_APPS])); + $this->assertEquals(false, $user->isPrivileged([User::ROLE_KEYS])); $this->assertEquals(false, $user->isPrivileged([User::ROLE_SYSTEM])); - $this->assertEquals(false, $user->isPrivileged([User::ROLE_APPS, User::ROLE_APPS])); - $this->assertEquals(false, $user->isPrivileged([User::ROLE_APPS, Role::guests()->toString()])); + $this->assertEquals(false, $user->isPrivileged([User::ROLE_KEYS, User::ROLE_KEYS])); + $this->assertEquals(false, $user->isPrivileged([User::ROLE_KEYS, Role::guests()->toString()])); $this->assertEquals(true, $user->isPrivileged([User::ROLE_OWNER, Role::guests()->toString()])); $this->assertEquals(true, $user->isPrivileged([User::ROLE_OWNER, User::ROLE_ADMIN, User::ROLE_DEVELOPER])); } @@ -192,19 +192,19 @@ class UserTest extends TestCase { $user = new User(); - $this->assertEquals(false, $user->isApp([])); - $this->assertEquals(false, $user->isApp([Role::guests()->toString()])); - $this->assertEquals(false, $user->isApp([Role::users()->toString()])); - $this->assertEquals(false, $user->isApp([User::ROLE_ADMIN])); - $this->assertEquals(false, $user->isApp([User::ROLE_DEVELOPER])); - $this->assertEquals(false, $user->isApp([User::ROLE_OWNER])); - $this->assertEquals(true, $user->isApp([User::ROLE_APPS])); - $this->assertEquals(false, $user->isApp([User::ROLE_SYSTEM])); + $this->assertEquals(false, $user->isKey([])); + $this->assertEquals(false, $user->isKey([Role::guests()->toString()])); + $this->assertEquals(false, $user->isKey([Role::users()->toString()])); + $this->assertEquals(false, $user->isKey([User::ROLE_ADMIN])); + $this->assertEquals(false, $user->isKey([User::ROLE_DEVELOPER])); + $this->assertEquals(false, $user->isKey([User::ROLE_OWNER])); + $this->assertEquals(true, $user->isKey([User::ROLE_KEYS])); + $this->assertEquals(false, $user->isKey([User::ROLE_SYSTEM])); - $this->assertEquals(true, $user->isApp([User::ROLE_APPS, User::ROLE_APPS])); - $this->assertEquals(true, $user->isApp([User::ROLE_APPS, Role::guests()->toString()])); - $this->assertEquals(false, $user->isApp([User::ROLE_OWNER, Role::guests()->toString()])); - $this->assertEquals(false, $user->isApp([User::ROLE_OWNER, User::ROLE_ADMIN, User::ROLE_DEVELOPER])); + $this->assertEquals(true, $user->isKey([User::ROLE_KEYS, User::ROLE_KEYS])); + $this->assertEquals(true, $user->isKey([User::ROLE_KEYS, Role::guests()->toString()])); + $this->assertEquals(false, $user->isKey([User::ROLE_OWNER, Role::guests()->toString()])); + $this->assertEquals(false, $user->isKey([User::ROLE_OWNER, User::ROLE_ADMIN, User::ROLE_DEVELOPER])); } public function testGuestRoles(): void @@ -327,7 +327,7 @@ class UserTest extends TestCase public function testAppUserRoles(): void { - $this->getAuthorization()->addRole(User::ROLE_APPS); + $this->getAuthorization()->addRole(User::ROLE_KEYS); $user = new User([ '$id' => ID::custom('123'), 'memberships' => [