From 37a7c70c2b4f507002ab492ba8909c477a03e8ac Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Matej=20Ba=C4=8Do?= Date: Thu, 19 Mar 2026 11:27:13 +0100 Subject: [PATCH 1/4] Fix webhook endpoints duplication --- app/controllers/api/projects.php | 310 ------------------ app/controllers/general.php | 2 +- app/init/constants.php | 2 +- app/init/resources.php | 40 ++- .../Modules/Webhooks/Http/Webhooks/Create.php | 1 + .../Modules/Webhooks/Http/Webhooks/Delete.php | 1 + .../Modules/Webhooks/Http/Webhooks/Get.php | 1 + .../Http/Webhooks/Signature/Update.php | 1 + .../Modules/Webhooks/Http/Webhooks/Update.php | 1 + .../Modules/Webhooks/Http/Webhooks/XList.php | 1 + src/Appwrite/Utopia/Request/Filters/V21.php | 11 +- tests/e2e/Scopes/ProjectCustom.php | 6 +- .../Services/ProjectWebhooks/WebhooksBase.php | 4 + 13 files changed, 59 insertions(+), 322 deletions(-) diff --git a/app/controllers/api/projects.php b/app/controllers/api/projects.php index 24a1b28cdd..cef64ffdeb 100644 --- a/app/controllers/api/projects.php +++ b/app/controllers/api/projects.php @@ -4,7 +4,6 @@ use Ahc\Jwt\JWT; use Appwrite\Auth\Validator\MockNumber; use Appwrite\Event\Delete; use Appwrite\Event\Mail; -use Appwrite\Event\Validator\Event; use Appwrite\Extend\Exception; use Appwrite\Network\Platform; use Appwrite\Network\Validator\Email; @@ -30,7 +29,6 @@ use Utopia\Database\Query; use Utopia\Database\Validator\Datetime as DatetimeValidator; use Utopia\Database\Validator\Query\Cursor; use Utopia\Database\Validator\UID; -use Utopia\Domains\Validator\PublicDomain; use Utopia\Http\Http; use Utopia\Locale\Locale; use Utopia\System\System; @@ -38,11 +36,9 @@ use Utopia\Validator\ArrayList; use Utopia\Validator\Boolean; use Utopia\Validator\Hostname; use Utopia\Validator\Integer; -use Utopia\Validator\Multiple; use Utopia\Validator\Nullable; use Utopia\Validator\Range; use Utopia\Validator\Text; -use Utopia\Validator\URL; use Utopia\Validator\WhiteList; Http::init() @@ -773,312 +769,6 @@ Http::delete('/v1/projects/:projectId') $response->noContent(); }); -// Webhooks - -Http::post('/v1/projects/:projectId/webhooks') - ->desc('Create webhook') - ->groups(['api', 'projects']) - ->label('scope', 'projects.write') - ->label('sdk', new Method( - namespace: 'projects', - group: 'webhooks', - name: 'createWebhook', - description: '/docs/references/projects/create-webhook.md', - auth: [AuthType::ADMIN], - responses: [ - new SDKResponse( - code: Response::STATUS_CODE_CREATED, - model: Response::MODEL_WEBHOOK, - ) - ] - )) - ->param('projectId', '', fn (Database $dbForPlatform) => new UID($dbForPlatform->getAdapter()->getMaxUIDLength()), 'Project unique ID.', false, ['dbForPlatform']) - ->param('name', null, new Text(128), 'Webhook name. Max length: 128 chars.') - ->param('enabled', true, new Boolean(true), 'Enable or disable a webhook.', true) - ->param('events', null, new ArrayList(new Event(), APP_LIMIT_ARRAY_PARAMS_SIZE), 'Events list. Maximum of ' . APP_LIMIT_ARRAY_PARAMS_SIZE . ' events are allowed.') - ->param('url', '', fn ($request) => new Multiple([new URL(['http', 'https']), new PublicDomain()], Multiple::TYPE_STRING), 'Webhook URL.', false, ['request']) - ->param('security', false, new Boolean(true), 'Certificate verification, false for disabled or true for enabled.') - ->param('httpUser', '', new Text(256), 'Webhook HTTP user. Max length: 256 chars.', true) - ->param('httpPass', '', new Text(256), 'Webhook HTTP password. Max length: 256 chars.', true) - ->inject('response') - ->inject('dbForPlatform') - ->action(function (string $projectId, string $name, bool $enabled, array $events, string $url, bool $security, string $httpUser, string $httpPass, Response $response, Database $dbForPlatform) { - - $project = $dbForPlatform->getDocument('projects', $projectId); - - if ($project->isEmpty()) { - throw new Exception(Exception::PROJECT_NOT_FOUND); - } - - $security = (bool) filter_var($security, FILTER_VALIDATE_BOOLEAN); - - $webhook = new Document([ - '$id' => ID::unique(), - '$permissions' => [ - Permission::read(Role::any()), - Permission::update(Role::any()), - Permission::delete(Role::any()), - ], - 'projectInternalId' => $project->getSequence(), - 'projectId' => $project->getId(), - 'name' => $name, - 'events' => $events, - 'url' => $url, - 'security' => $security, - 'httpUser' => $httpUser, - 'httpPass' => $httpPass, - 'signatureKey' => \bin2hex(\random_bytes(64)), - 'enabled' => $enabled, - ]); - - $webhook = $dbForPlatform->createDocument('webhooks', $webhook); - - $dbForPlatform->purgeCachedDocument('projects', $project->getId()); - - $response - ->setStatusCode(Response::STATUS_CODE_CREATED) - ->dynamic($webhook, Response::MODEL_WEBHOOK); - }); - -Http::get('/v1/projects/:projectId/webhooks') - ->desc('List webhooks') - ->groups(['api', 'projects']) - ->label('scope', 'projects.read') - ->label('sdk', new Method( - namespace: 'projects', - group: 'webhooks', - name: 'listWebhooks', - description: '/docs/references/projects/list-webhooks.md', - auth: [AuthType::ADMIN], - responses: [ - new SDKResponse( - code: Response::STATUS_CODE_OK, - model: Response::MODEL_WEBHOOK_LIST, - ) - ] - )) - ->param('projectId', '', fn (Database $dbForPlatform) => new UID($dbForPlatform->getAdapter()->getMaxUIDLength()), 'Project unique ID.', false, ['dbForPlatform']) - ->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') - ->action(function (string $projectId, bool $includeTotal, Response $response, Database $dbForPlatform) { - - $project = $dbForPlatform->getDocument('projects', $projectId); - - if ($project->isEmpty()) { - throw new Exception(Exception::PROJECT_NOT_FOUND); - } - - $webhooks = $dbForPlatform->find('webhooks', [ - Query::equal('projectInternalId', [$project->getSequence()]), - Query::limit(5000), - ]); - - $response->dynamic(new Document([ - 'webhooks' => $webhooks, - 'total' => $includeTotal ? count($webhooks) : 0, - ]), Response::MODEL_WEBHOOK_LIST); - }); - -Http::get('/v1/projects/:projectId/webhooks/:webhookId') - ->desc('Get webhook') - ->groups(['api', 'projects']) - ->label('scope', 'projects.read') - ->label('sdk', new Method( - namespace: 'projects', - group: 'webhooks', - name: 'getWebhook', - description: '/docs/references/projects/get-webhook.md', - auth: [AuthType::ADMIN], - responses: [ - new SDKResponse( - code: Response::STATUS_CODE_OK, - model: Response::MODEL_WEBHOOK, - ) - ] - )) - ->param('projectId', '', fn (Database $dbForPlatform) => new UID($dbForPlatform->getAdapter()->getMaxUIDLength()), 'Project unique ID.', false, ['dbForPlatform']) - ->param('webhookId', '', fn (Database $dbForPlatform) => new UID($dbForPlatform->getAdapter()->getMaxUIDLength()), 'Webhook unique ID.', false, ['dbForPlatform']) - ->inject('response') - ->inject('dbForPlatform') - ->action(function (string $projectId, string $webhookId, Response $response, Database $dbForPlatform) { - - $project = $dbForPlatform->getDocument('projects', $projectId); - - if ($project->isEmpty()) { - throw new Exception(Exception::PROJECT_NOT_FOUND); - } - - $webhook = $dbForPlatform->findOne('webhooks', [ - Query::equal('$id', [$webhookId]), - Query::equal('projectInternalId', [$project->getSequence()]), - ]); - - if ($webhook->isEmpty()) { - throw new Exception(Exception::WEBHOOK_NOT_FOUND); - } - - $response->dynamic($webhook, Response::MODEL_WEBHOOK); - }); - -Http::put('/v1/projects/:projectId/webhooks/:webhookId') - ->desc('Update webhook') - ->groups(['api', 'projects']) - ->label('scope', 'projects.write') - ->label('sdk', new Method( - namespace: 'projects', - group: 'webhooks', - name: 'updateWebhook', - description: '/docs/references/projects/update-webhook.md', - auth: [AuthType::ADMIN], - responses: [ - new SDKResponse( - code: Response::STATUS_CODE_OK, - model: Response::MODEL_WEBHOOK, - ) - ] - )) - ->param('projectId', '', fn (Database $dbForPlatform) => new UID($dbForPlatform->getAdapter()->getMaxUIDLength()), 'Project unique ID.', false, ['dbForPlatform']) - ->param('webhookId', '', fn (Database $dbForPlatform) => new UID($dbForPlatform->getAdapter()->getMaxUIDLength()), 'Webhook unique ID.', false, ['dbForPlatform']) - ->param('name', null, new Text(128), 'Webhook name. Max length: 128 chars.') - ->param('enabled', true, new Boolean(true), 'Enable or disable a webhook.', true) - ->param('events', null, new ArrayList(new Event(), APP_LIMIT_ARRAY_PARAMS_SIZE), 'Events list. Maximum of ' . APP_LIMIT_ARRAY_PARAMS_SIZE . ' events are allowed.') - ->param('url', '', fn ($request) => new Multiple([new URL(['http', 'https']), new PublicDomain()], Multiple::TYPE_STRING), 'Webhook URL.', false, ['request']) - ->param('security', false, new Boolean(true), 'Certificate verification, false for disabled or true for enabled.') - ->param('httpUser', '', new Text(256), 'Webhook HTTP user. Max length: 256 chars.', true) - ->param('httpPass', '', new Text(256), 'Webhook HTTP password. Max length: 256 chars.', true) - ->inject('response') - ->inject('dbForPlatform') - ->action(function (string $projectId, string $webhookId, string $name, bool $enabled, array $events, string $url, bool $security, string $httpUser, string $httpPass, Response $response, Database $dbForPlatform) { - - $project = $dbForPlatform->getDocument('projects', $projectId); - - if ($project->isEmpty()) { - throw new Exception(Exception::PROJECT_NOT_FOUND); - } - - $security = ($security === '1' || $security === 'true' || $security === 1 || $security === true); - - $webhook = $dbForPlatform->findOne('webhooks', [ - Query::equal('$id', [$webhookId]), - Query::equal('projectInternalId', [$project->getSequence()]), - ]); - - if ($webhook->isEmpty()) { - throw new Exception(Exception::WEBHOOK_NOT_FOUND); - } - - $webhook - ->setAttribute('name', $name) - ->setAttribute('events', $events) - ->setAttribute('url', $url) - ->setAttribute('security', $security) - ->setAttribute('httpUser', $httpUser) - ->setAttribute('httpPass', $httpPass) - ->setAttribute('enabled', $enabled); - - if ($enabled) { - $webhook->setAttribute('attempts', 0); - } - - $dbForPlatform->updateDocument('webhooks', $webhook->getId(), $webhook); - $dbForPlatform->purgeCachedDocument('projects', $project->getId()); - - $response->dynamic($webhook, Response::MODEL_WEBHOOK); - }); - -Http::patch('/v1/projects/:projectId/webhooks/:webhookId/signature') - ->desc('Update webhook signature key') - ->groups(['api', 'projects']) - ->label('scope', 'projects.write') - ->label('sdk', new Method( - namespace: 'projects', - group: 'webhooks', - name: 'updateWebhookSignature', - description: '/docs/references/projects/update-webhook-signature.md', - auth: [AuthType::ADMIN], - responses: [ - new SDKResponse( - code: Response::STATUS_CODE_OK, - model: Response::MODEL_WEBHOOK, - ) - ] - )) - ->param('projectId', '', fn (Database $dbForPlatform) => new UID($dbForPlatform->getAdapter()->getMaxUIDLength()), 'Project unique ID.', false, ['dbForPlatform']) - ->param('webhookId', '', fn (Database $dbForPlatform) => new UID($dbForPlatform->getAdapter()->getMaxUIDLength()), 'Webhook unique ID.', false, ['dbForPlatform']) - ->inject('response') - ->inject('dbForPlatform') - ->action(function (string $projectId, string $webhookId, Response $response, Database $dbForPlatform) { - - $project = $dbForPlatform->getDocument('projects', $projectId); - - if ($project->isEmpty()) { - throw new Exception(Exception::PROJECT_NOT_FOUND); - } - - $webhook = $dbForPlatform->findOne('webhooks', [ - Query::equal('$id', [$webhookId]), - Query::equal('projectInternalId', [$project->getSequence()]), - ]); - - if ($webhook->isEmpty()) { - throw new Exception(Exception::WEBHOOK_NOT_FOUND); - } - - $webhook->setAttribute('signatureKey', \bin2hex(\random_bytes(64))); - - $dbForPlatform->updateDocument('webhooks', $webhook->getId(), $webhook); - $dbForPlatform->purgeCachedDocument('projects', $project->getId()); - - $response->dynamic($webhook, Response::MODEL_WEBHOOK); - }); - -Http::delete('/v1/projects/:projectId/webhooks/:webhookId') - ->desc('Delete webhook') - ->groups(['api', 'projects']) - ->label('scope', 'projects.write') - ->label('sdk', new Method( - namespace: 'projects', - group: 'webhooks', - name: 'deleteWebhook', - description: '/docs/references/projects/delete-webhook.md', - auth: [AuthType::ADMIN], - responses: [ - new SDKResponse( - code: Response::STATUS_CODE_NOCONTENT, - model: Response::MODEL_NONE, - ) - ], - contentType: ContentType::NONE - )) - ->param('projectId', '', fn (Database $dbForPlatform) => new UID($dbForPlatform->getAdapter()->getMaxUIDLength()), 'Project unique ID.', false, ['dbForPlatform']) - ->param('webhookId', '', fn (Database $dbForPlatform) => new UID($dbForPlatform->getAdapter()->getMaxUIDLength()), 'Webhook unique ID.', false, ['dbForPlatform']) - ->inject('response') - ->inject('dbForPlatform') - ->action(function (string $projectId, string $webhookId, Response $response, Database $dbForPlatform) { - - $project = $dbForPlatform->getDocument('projects', $projectId); - - if ($project->isEmpty()) { - throw new Exception(Exception::PROJECT_NOT_FOUND); - } - - $webhook = $dbForPlatform->findOne('webhooks', [ - Query::equal('$id', [$webhookId]), - Query::equal('projectInternalId', [$project->getSequence()]), - ]); - - if ($webhook->isEmpty()) { - throw new Exception(Exception::WEBHOOK_NOT_FOUND); - } - - $dbForPlatform->deleteDocument('webhooks', $webhook->getId()); - - $dbForPlatform->purgeCachedDocument('projects', $project->getId()); - - $response->noContent(); - }); - // Keys Http::post('/v1/projects/:projectId/keys') diff --git a/app/controllers/general.php b/app/controllers/general.php index 1a099c4bde..341ac59f7a 100644 --- a/app/controllers/general.php +++ b/app/controllers/general.php @@ -888,7 +888,7 @@ Http::init() $dbForProject = $getProjectDB($project); $request->addFilter(new RequestV20($dbForProject, $route->getPathValues($request))); } - if (version_compare($requestFormat, '1.9.0', '<')) { + if (version_compare($requestFormat, '1.8.2', '<')) { $request->addFilter(new RequestV21()); } } diff --git a/app/init/constants.php b/app/init/constants.php index 7a484c7f4e..eab5ed91a4 100644 --- a/app/init/constants.php +++ b/app/init/constants.php @@ -47,7 +47,7 @@ const APP_RESOURCE_TOKEN_ACCESS = 24 * 60 * 60; // 24 hours const APP_FILE_ACCESS = 24 * 60 * 60; // 24 hours const APP_CACHE_UPDATE = 24 * 60 * 60; // 24 hours const APP_CACHE_BUSTER = 4321; -const APP_VERSION_STABLE = '1.8.1'; +const APP_VERSION_STABLE = '1.8.2'; const APP_DATABASE_ATTRIBUTE_EMAIL = 'email'; const APP_DATABASE_ATTRIBUTE_ENUM = 'enum'; const APP_DATABASE_ATTRIBUTE_IP = 'ip'; diff --git a/app/init/resources.php b/app/init/resources.php index 4ebe705e73..853353be79 100644 --- a/app/init/resources.php +++ b/app/init/resources.php @@ -476,7 +476,7 @@ Http::setResource('user', function (string $mode, Document $project, Document $c return $user; }, ['mode', 'project', 'console', 'request', 'response', 'dbForProject', 'dbForPlatform', 'store', 'proofForToken', 'authorization']); -Http::setResource('project', function ($dbForPlatform, $request, $console, $authorization) { +Http::setResource('project', function ($dbForPlatform, $request, $console, $authorization, Http $utopia) { /** @var Appwrite\Utopia\Request $request */ /** @var Utopia\Database\Database $dbForPlatform */ /** @var Utopia\Database\Document $console */ @@ -486,6 +486,27 @@ Http::setResource('project', function ($dbForPlatform, $request, $console, $auth $projectId = $request->getHeader('x-appwrite-project', ''); } + // Backwards compatibility for new services, originally project resources + // These endpoints moved from /v1/projects/:projectId/ to /v1/ + // When accessed via the old alias path, extract projectId from the URI + $isService = false; + $route = $utopia->match($request); + if (!empty($route)) { + foreach ([ + '/v1/webhooks' + ] as $path) { + if (\str_starts_with($route->getPath(), $path)) { + $isService = true; + break; + } + } + } + $isDeprecatedProjectEndpoint = \str_starts_with($request->getURI(), '/v1/projects/'); + + if ($isService && $isDeprecatedProjectEndpoint) { + $projectId = \explode('/', $request->getURI(), 5)[3] ?? ''; + } + if (empty($projectId) || $projectId === 'console') { return $console; } @@ -493,7 +514,7 @@ Http::setResource('project', function ($dbForPlatform, $request, $console, $auth $project = $authorization->skip(fn () => $dbForPlatform->getDocument('projects', $projectId)); return $project; -}, ['dbForPlatform', 'request', 'console', 'authorization']); +}, ['dbForPlatform', 'request', 'console', 'authorization', 'utopia']); Http::setResource('session', function (User $user, Store $store, Token $proofForToken) { if ($user->isEmpty()) { @@ -1072,16 +1093,21 @@ function getDevice(string $root, string $connection = ''): Device } } -Http::setResource('mode', function ($request) { - /** @var Appwrite\Utopia\Request $request */ - +Http::setResource('mode', function (Request $request, Document $project) { /** * Defines the mode for the request: * - 'default' => Requests for Client and Server Side * - 'admin' => Request from the Console on non-console projects */ - return $request->getParam('mode', $request->getHeader('x-appwrite-mode', APP_MODE_DEFAULT)); -}, ['request']); + $mode = $request->getParam('mode', $request->getHeader('x-appwrite-mode', APP_MODE_DEFAULT)); + + $projectId = $request->getParam('project', $request->getHeader('x-appwrite-project', '')); + if ($project->getId() !== $projectId) { + $mode = APP_MODE_ADMIN; + } + + return $mode; +}, ['request', 'project']); Http::setResource('geodb', function ($register) { /** @var Utopia\Registry\Registry $register */ diff --git a/src/Appwrite/Platform/Modules/Webhooks/Http/Webhooks/Create.php b/src/Appwrite/Platform/Modules/Webhooks/Http/Webhooks/Create.php index 261571a37b..91daf33b2b 100644 --- a/src/Appwrite/Platform/Modules/Webhooks/Http/Webhooks/Create.php +++ b/src/Appwrite/Platform/Modules/Webhooks/Http/Webhooks/Create.php @@ -39,6 +39,7 @@ class Create extends Base $this ->setHttpMethod(Action::HTTP_REQUEST_METHOD_POST) ->setHttpPath('/v1/webhooks') + ->httpAlias('/v1/projects/:projectId/webhooks') ->desc('Create webhook') ->groups(['api', 'webhooks']) ->label('scope', 'webhooks.write') diff --git a/src/Appwrite/Platform/Modules/Webhooks/Http/Webhooks/Delete.php b/src/Appwrite/Platform/Modules/Webhooks/Http/Webhooks/Delete.php index c63a558b06..7730e9fc2c 100644 --- a/src/Appwrite/Platform/Modules/Webhooks/Http/Webhooks/Delete.php +++ b/src/Appwrite/Platform/Modules/Webhooks/Http/Webhooks/Delete.php @@ -32,6 +32,7 @@ class Delete extends Base $this ->setHttpMethod(Action::HTTP_REQUEST_METHOD_DELETE) ->setHttpPath('/v1/webhooks/:webhookId') + ->httpAlias('/v1/projects/:projectId/webhooks/:webhookId') ->desc('Delete webhook') ->groups(['api', 'webhooks']) ->label('scope', 'webhooks.write') diff --git a/src/Appwrite/Platform/Modules/Webhooks/Http/Webhooks/Get.php b/src/Appwrite/Platform/Modules/Webhooks/Http/Webhooks/Get.php index 229db1924d..52ac455fc9 100644 --- a/src/Appwrite/Platform/Modules/Webhooks/Http/Webhooks/Get.php +++ b/src/Appwrite/Platform/Modules/Webhooks/Http/Webhooks/Get.php @@ -30,6 +30,7 @@ class Get extends Base $this ->setHttpMethod(Action::HTTP_REQUEST_METHOD_GET) ->setHttpPath('/v1/webhooks/:webhookId') + ->httpAlias('/v1/projects/:projectId/webhooks/:webhookId') ->desc('Get webhook') ->groups(['api', 'webhooks']) ->label('scope', 'webhooks.read') diff --git a/src/Appwrite/Platform/Modules/Webhooks/Http/Webhooks/Signature/Update.php b/src/Appwrite/Platform/Modules/Webhooks/Http/Webhooks/Signature/Update.php index 7995192bee..9b2612863f 100644 --- a/src/Appwrite/Platform/Modules/Webhooks/Http/Webhooks/Signature/Update.php +++ b/src/Appwrite/Platform/Modules/Webhooks/Http/Webhooks/Signature/Update.php @@ -30,6 +30,7 @@ class Update extends Base { $this->setHttpMethod(Action::HTTP_REQUEST_METHOD_PATCH) ->setHttpPath('/v1/webhooks/:webhookId/signature') + ->httpAlias('/v1/projects/:projectId/webhooks/:webhookId/signature') ->desc('Update webhook signature key') ->groups(['api', 'webhooks']) ->label('scope', 'webhooks.write') diff --git a/src/Appwrite/Platform/Modules/Webhooks/Http/Webhooks/Update.php b/src/Appwrite/Platform/Modules/Webhooks/Http/Webhooks/Update.php index abc2f2ef00..a1387c356c 100644 --- a/src/Appwrite/Platform/Modules/Webhooks/Http/Webhooks/Update.php +++ b/src/Appwrite/Platform/Modules/Webhooks/Http/Webhooks/Update.php @@ -37,6 +37,7 @@ class Update extends Base { $this->setHttpMethod(Action::HTTP_REQUEST_METHOD_PUT) ->setHttpPath('/v1/webhooks/:webhookId') + ->httpAlias('/v1/projects/:projectId/webhooks/:webhookId') ->desc('Update webhook') ->groups(['api', 'webhooks']) ->label('scope', 'webhooks.write') diff --git a/src/Appwrite/Platform/Modules/Webhooks/Http/Webhooks/XList.php b/src/Appwrite/Platform/Modules/Webhooks/Http/Webhooks/XList.php index 35bf762ce1..fae95d7c5d 100644 --- a/src/Appwrite/Platform/Modules/Webhooks/Http/Webhooks/XList.php +++ b/src/Appwrite/Platform/Modules/Webhooks/Http/Webhooks/XList.php @@ -34,6 +34,7 @@ class XList extends Base $this ->setHttpMethod(Action::HTTP_REQUEST_METHOD_GET) ->setHttpPath('/v1/webhooks') + ->httpAlias('/v1/projects/:projectId/webhooks') ->desc('List webhooks') ->groups(['api', 'webhooks']) ->label('scope', 'webhooks.read') diff --git a/src/Appwrite/Utopia/Request/Filters/V21.php b/src/Appwrite/Utopia/Request/Filters/V21.php index 4feb8e1926..14b8fa81bb 100644 --- a/src/Appwrite/Utopia/Request/Filters/V21.php +++ b/src/Appwrite/Utopia/Request/Filters/V21.php @@ -6,10 +6,13 @@ use Appwrite\Utopia\Request\Filter; class V21 extends Filter { - // Convert 1.8.0 params to 1.9.0 + // Convert 1.8.0 params to 1.8.2 public function parse(array $content, string $model): array { switch ($model) { + case 'webhooks.create': + $content = $this->fillWebhookid($content); + break; case 'functions.createTemplateDeployment': case 'sites.createTemplateDeployment': $content = $this->convertVersionToTypeAndReference($content); @@ -48,4 +51,10 @@ class V21 extends Filter return $content; } + + protected function fillWebhookid(array $content): array + { + $content['webhookId'] = 'unique()'; + return $content; + } } diff --git a/tests/e2e/Scopes/ProjectCustom.php b/tests/e2e/Scopes/ProjectCustom.php index c80ef20193..b8b6f38643 100644 --- a/tests/e2e/Scopes/ProjectCustom.php +++ b/tests/e2e/Scopes/ProjectCustom.php @@ -193,12 +193,14 @@ trait ProjectCustom $this->assertNotEmpty($devKey['body']); $this->assertNotEmpty($devKey['body']['secret']); - $webhook = $this->client->call(Client::METHOD_POST, '/projects/' . $project['body']['$id'] . '/webhooks', [ + $webhook = $this->client->call(Client::METHOD_POST, '/webhooks', [ 'origin' => 'http://localhost', 'content-type' => 'application/json', 'cookie' => 'a_session_console=' . $this->getRoot()['session'], - 'x-appwrite-project' => 'console', + 'x-appwrite-project' => $project['body']['$id'], + 'x-appwrite-mode' => 'admin' ], [ + 'webhookId' => 'unique()', 'name' => 'Webhook Test', 'events' => [ 'databases.*', diff --git a/tests/e2e/Services/ProjectWebhooks/WebhooksBase.php b/tests/e2e/Services/ProjectWebhooks/WebhooksBase.php index 6a251e2c52..0f1ff7eab3 100644 --- a/tests/e2e/Services/ProjectWebhooks/WebhooksBase.php +++ b/tests/e2e/Services/ProjectWebhooks/WebhooksBase.php @@ -1711,6 +1711,7 @@ trait WebhooksBase 'content-type' => 'application/json', 'cookie' => 'a_session_console=' . $this->getRoot()['session'], 'x-appwrite-project' => 'console', + 'X-Appwrite-Response-Format' => '1.8.0' ], [ 'name' => 'Webhook Test', 'enabled' => true, @@ -1740,6 +1741,7 @@ trait WebhooksBase 'content-type' => 'application/json', 'cookie' => 'a_session_console=' . $this->getRoot()['session'], 'x-appwrite-project' => 'console', + 'X-Appwrite-Response-Format' => '1.8.0' ], [ 'name' => 'Webhook Test', 'enabled' => true, @@ -1779,6 +1781,7 @@ trait WebhooksBase 'content-type' => 'application/json', 'cookie' => 'a_session_console=' . $this->getRoot()['session'], 'x-appwrite-project' => 'console', + 'X-Appwrite-Response-Format' => '1.8.0' ], [ 'name' => 'Webhook Test', 'enabled' => true, @@ -1824,6 +1827,7 @@ trait WebhooksBase 'content-type' => 'application/json', 'cookie' => 'a_session_console=' . $this->getRoot()['session'], 'x-appwrite-project' => 'console', + 'X-Appwrite-Response-Format' => '1.8.0' ])); // assert that the webhook is now disabled after 10 consecutive failures From 6f8a54273d2112159842e5af8137a62b91e3069b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Matej=20Ba=C4=8Do?= Date: Thu, 19 Mar 2026 11:47:02 +0100 Subject: [PATCH 2/4] AI review fixes --- app/init/resources.php | 21 +++++++-------------- src/Appwrite/Utopia/Request/Filters/V21.php | 2 +- 2 files changed, 8 insertions(+), 15 deletions(-) diff --git a/app/init/resources.php b/app/init/resources.php index 853353be79..2ed9e88e90 100644 --- a/app/init/resources.php +++ b/app/init/resources.php @@ -489,22 +489,15 @@ Http::setResource('project', function ($dbForPlatform, $request, $console, $auth // Backwards compatibility for new services, originally project resources // These endpoints moved from /v1/projects/:projectId/ to /v1/ // When accessed via the old alias path, extract projectId from the URI - $isService = false; + $deprecatedProjectPathPrefix = '/v1/projects/'; $route = $utopia->match($request); if (!empty($route)) { - foreach ([ - '/v1/webhooks' - ] as $path) { - if (\str_starts_with($route->getPath(), $path)) { - $isService = true; - break; - } - } - } - $isDeprecatedProjectEndpoint = \str_starts_with($request->getURI(), '/v1/projects/'); + $isDeprecatedAlias = \str_starts_with($request->getURI(), $deprecatedProjectPathPrefix) && + !\str_starts_with($route->getPath(), $deprecatedProjectPathPrefix); - if ($isService && $isDeprecatedProjectEndpoint) { - $projectId = \explode('/', $request->getURI(), 5)[3] ?? ''; + if ($isDeprecatedAlias) { + $projectId = \explode('/', $request->getURI(), 5)[3] ?? ''; + } } if (empty($projectId) || $projectId === 'console') { @@ -1102,7 +1095,7 @@ Http::setResource('mode', function (Request $request, Document $project) { $mode = $request->getParam('mode', $request->getHeader('x-appwrite-mode', APP_MODE_DEFAULT)); $projectId = $request->getParam('project', $request->getHeader('x-appwrite-project', '')); - if ($project->getId() !== $projectId) { + if (!empty($projectId) && $project->getId() !== $projectId) { $mode = APP_MODE_ADMIN; } diff --git a/src/Appwrite/Utopia/Request/Filters/V21.php b/src/Appwrite/Utopia/Request/Filters/V21.php index 14b8fa81bb..d51ec28a1e 100644 --- a/src/Appwrite/Utopia/Request/Filters/V21.php +++ b/src/Appwrite/Utopia/Request/Filters/V21.php @@ -54,7 +54,7 @@ class V21 extends Filter protected function fillWebhookid(array $content): array { - $content['webhookId'] = 'unique()'; + $content['webhookId'] = $content['webhookId'] ?? 'unique()'; return $content; } } From ab43d4995b05b0b33e0efd441509ab9edbc6cea2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Matej=20Ba=C4=8Do?= Date: Thu, 19 Mar 2026 12:20:33 +0100 Subject: [PATCH 3/4] Upgrade webhook tests --- src/Appwrite/Migration/Migration.php | 1 + tests/e2e/Services/Projects/ProjectsBase.php | 5 +- .../Projects/ProjectsConsoleClientTest.php | 89 ++++++++++++------- 3 files changed, 59 insertions(+), 36 deletions(-) diff --git a/src/Appwrite/Migration/Migration.php b/src/Appwrite/Migration/Migration.php index 519f05de2c..8fe35608be 100644 --- a/src/Appwrite/Migration/Migration.php +++ b/src/Appwrite/Migration/Migration.php @@ -91,6 +91,7 @@ abstract class Migration '1.7.4' => 'V22', '1.8.0' => 'V23', '1.8.1' => 'V23', + '1.8.2' => 'V23', ]; /** diff --git a/tests/e2e/Services/Projects/ProjectsBase.php b/tests/e2e/Services/Projects/ProjectsBase.php index dc31b7aa85..35b8b86404 100644 --- a/tests/e2e/Services/Projects/ProjectsBase.php +++ b/tests/e2e/Services/Projects/ProjectsBase.php @@ -81,10 +81,11 @@ trait ProjectsBase $projectData = $this->setupProjectData(); $id = $projectData['projectId']; - $response = $this->client->call(Client::METHOD_POST, '/projects/' . $id . '/webhooks', array_merge([ + $response = $this->client->call(Client::METHOD_POST, '/webhooks', array_merge([ 'content-type' => 'application/json', - 'x-appwrite-project' => $this->getProject()['$id'], + 'x-appwrite-project' => $id, ], $this->getHeaders()), [ + 'webhookId' => 'unique()', 'name' => 'Webhook Test', 'events' => ['users.*.create', 'users.*.update.email'], 'url' => 'https://appwrite.io', diff --git a/tests/e2e/Services/Projects/ProjectsConsoleClientTest.php b/tests/e2e/Services/Projects/ProjectsConsoleClientTest.php index 52e59f3e72..d4945f8407 100644 --- a/tests/e2e/Services/Projects/ProjectsConsoleClientTest.php +++ b/tests/e2e/Services/Projects/ProjectsConsoleClientTest.php @@ -2743,10 +2743,12 @@ class ProjectsConsoleClientTest extends Scope $data = $this->setupProjectData(); $id = $data['projectId']; - $response = $this->client->call(Client::METHOD_POST, '/projects/' . $id . '/webhooks', array_merge([ + $response = $this->client->call(Client::METHOD_POST, '/webhooks', array_merge([ 'content-type' => 'application/json', - 'x-appwrite-project' => $this->getProject()['$id'], + 'x-appwrite-project' => $id, + 'x-appwrite-mode' => 'admin' ], $this->getHeaders()), [ + 'webhookId' => 'unique()', 'name' => 'Webhook Test', 'events' => ['users.*.create', 'users.*.update.email'], 'url' => 'https://appwrite.io', @@ -2768,10 +2770,12 @@ class ProjectsConsoleClientTest extends Scope /** * Test for FAILURE */ - $response = $this->client->call(Client::METHOD_POST, '/projects/' . $id . '/webhooks', array_merge([ + $response = $this->client->call(Client::METHOD_POST, '/webhooks', array_merge([ 'content-type' => 'application/json', - 'x-appwrite-project' => $this->getProject()['$id'], + 'x-appwrite-project' => $id, + 'x-appwrite-mode' => 'admin' ], $this->getHeaders()), [ + 'webhookId' => 'unique()', 'name' => 'Webhook Test', 'events' => ['account.unknown', 'users.*.update.email'], 'url' => 'https://appwrite.io', @@ -2782,10 +2786,12 @@ class ProjectsConsoleClientTest extends Scope $this->assertEquals(400, $response['headers']['status-code']); - $response = $this->client->call(Client::METHOD_POST, '/projects/' . $id . '/webhooks', array_merge([ + $response = $this->client->call(Client::METHOD_POST, '/webhooks', array_merge([ 'content-type' => 'application/json', - 'x-appwrite-project' => $this->getProject()['$id'], + 'x-appwrite-project' => $id, + 'x-appwrite-mode' => 'admin' ], $this->getHeaders()), [ + 'webhookId' => 'unique()', 'name' => 'Webhook Test', 'events' => ['users.*.create', 'users.*.update.email'], 'url' => 'invalid://appwrite.io', @@ -2799,9 +2805,10 @@ class ProjectsConsoleClientTest extends Scope $data = $this->setupProjectWithWebhook(); $id = $data['projectId']; - $response = $this->client->call(Client::METHOD_GET, '/projects/' . $id . '/webhooks', array_merge([ + $response = $this->client->call(Client::METHOD_GET, '/webhooks', array_merge([ 'content-type' => 'application/json', - 'x-appwrite-project' => $this->getProject()['$id'], + 'x-appwrite-project' => $id, + 'x-appwrite-mode' => 'admin' ], $this->getHeaders()), []); $this->assertEquals(200, $response['headers']['status-code']); @@ -2819,9 +2826,10 @@ class ProjectsConsoleClientTest extends Scope $id = $data['projectId']; $webhookId = $data['webhookId']; - $response = $this->client->call(Client::METHOD_GET, '/projects/' . $id . '/webhooks/' . $webhookId, array_merge([ + $response = $this->client->call(Client::METHOD_GET, '/webhooks/' . $webhookId, array_merge([ 'content-type' => 'application/json', - 'x-appwrite-project' => $this->getProject()['$id'], + 'x-appwrite-project' => $id, + 'x-appwrite-mode' => 'admin' ], $this->getHeaders()), []); $this->assertEquals(200, $response['headers']['status-code']); @@ -2837,9 +2845,10 @@ class ProjectsConsoleClientTest extends Scope /** * Test for FAILURE */ - $response = $this->client->call(Client::METHOD_GET, '/projects/' . $id . '/webhooks/error', array_merge([ + $response = $this->client->call(Client::METHOD_GET, '/webhooks/error', array_merge([ 'content-type' => 'application/json', - 'x-appwrite-project' => $this->getProject()['$id'], + 'x-appwrite-project' => $id, + 'x-appwrite-mode' => 'admin' ], $this->getHeaders()), []); $this->assertEquals(404, $response['headers']['status-code']); @@ -2851,9 +2860,10 @@ class ProjectsConsoleClientTest extends Scope $id = $data['projectId']; $webhookId = $data['webhookId']; - $response = $this->client->call(Client::METHOD_PUT, '/projects/' . $id . '/webhooks/' . $webhookId, array_merge([ + $response = $this->client->call(Client::METHOD_PUT, '/webhooks/' . $webhookId, array_merge([ 'content-type' => 'application/json', - 'x-appwrite-project' => $this->getProject()['$id'], + 'x-appwrite-project' => $id, + 'x-appwrite-mode' => 'admin' ], $this->getHeaders()), [ 'name' => 'Webhook Test Update', 'events' => ['users.*.delete', 'users.*.sessions.*.delete', 'buckets.*.files.*.create'], @@ -2875,9 +2885,10 @@ class ProjectsConsoleClientTest extends Scope $this->assertEquals('', $response['body']['httpUser']); $this->assertEquals('', $response['body']['httpPass']); - $response = $this->client->call(Client::METHOD_GET, '/projects/' . $id . '/webhooks/' . $webhookId, array_merge([ + $response = $this->client->call(Client::METHOD_GET, '/webhooks/' . $webhookId, array_merge([ 'content-type' => 'application/json', - 'x-appwrite-project' => $this->getProject()['$id'], + 'x-appwrite-project' => $id, + 'x-appwrite-mode' => 'admin' ], $this->getHeaders()), []); $this->assertEquals(200, $response['headers']['status-code']); @@ -2897,9 +2908,10 @@ class ProjectsConsoleClientTest extends Scope /** * Test for FAILURE */ - $response = $this->client->call(Client::METHOD_PUT, '/projects/' . $id . '/webhooks/' . $webhookId, array_merge([ + $response = $this->client->call(Client::METHOD_PUT, '/webhooks/' . $webhookId, array_merge([ 'content-type' => 'application/json', - 'x-appwrite-project' => $this->getProject()['$id'], + 'x-appwrite-project' => $id, + 'x-appwrite-mode' => 'admin' ], $this->getHeaders()), [ 'name' => 'Webhook Test Update', 'events' => ['users.*.delete', 'users.*.sessions.*.delete', 'buckets.*.files.*.unknown'], @@ -2909,9 +2921,10 @@ class ProjectsConsoleClientTest extends Scope $this->assertEquals(400, $response['headers']['status-code']); - $response = $this->client->call(Client::METHOD_PUT, '/projects/' . $id . '/webhooks/' . $webhookId, array_merge([ + $response = $this->client->call(Client::METHOD_PUT, '/webhooks/' . $webhookId, array_merge([ 'content-type' => 'application/json', - 'x-appwrite-project' => $this->getProject()['$id'], + 'x-appwrite-project' => $id, + 'x-appwrite-mode' => 'admin' ], $this->getHeaders()), [ 'name' => 'Webhook Test Update', 'events' => ['users.*.delete', 'users.*.sessions.*.delete', 'buckets.*.files.*.create'], @@ -2921,9 +2934,10 @@ class ProjectsConsoleClientTest extends Scope $this->assertEquals(400, $response['headers']['status-code']); - $response = $this->client->call(Client::METHOD_PUT, '/projects/' . $id . '/webhooks/' . $webhookId, array_merge([ + $response = $this->client->call(Client::METHOD_PUT, '/webhooks/' . $webhookId, array_merge([ 'content-type' => 'application/json', - 'x-appwrite-project' => $this->getProject()['$id'], + 'x-appwrite-project' => $id, + 'x-appwrite-mode' => 'admin' ], $this->getHeaders()), [ 'name' => 'Webhook Test Update', 'events' => ['users.*.delete', 'users.*.sessions.*.delete', 'buckets.*.files.*.create'], @@ -2940,9 +2954,10 @@ class ProjectsConsoleClientTest extends Scope $webhookId = $data['webhookId']; $signatureKey = $data['signatureKey']; - $response = $this->client->call(Client::METHOD_PATCH, '/projects/' . $id . '/webhooks/' . $webhookId . '/signature', array_merge([ + $response = $this->client->call(Client::METHOD_PATCH, '/webhooks/' . $webhookId . '/signature', array_merge([ 'content-type' => 'application/json', - 'x-appwrite-project' => $this->getProject()['$id'], + 'x-appwrite-project' => $id, + 'x-appwrite-mode' => 'admin' ], $this->getHeaders())); $this->assertEquals(200, $response['headers']['status-code']); @@ -2957,10 +2972,12 @@ class ProjectsConsoleClientTest extends Scope $id = $projectData['projectId']; // Create a webhook to delete - $response = $this->client->call(Client::METHOD_POST, '/projects/' . $id . '/webhooks', array_merge([ + $response = $this->client->call(Client::METHOD_POST, '/webhooks', array_merge([ 'content-type' => 'application/json', - 'x-appwrite-project' => $this->getProject()['$id'], + 'x-appwrite-project' => $id, + 'x-appwrite-mode' => 'admin' ], $this->getHeaders()), [ + 'webhookId' => 'unique()', 'name' => 'Webhook To Delete', 'events' => ['users.*.create'], 'url' => 'https://appwrite.io', @@ -2972,17 +2989,19 @@ class ProjectsConsoleClientTest extends Scope $this->assertEquals(201, $response['headers']['status-code']); $webhookId = $response['body']['$id']; - $response = $this->client->call(Client::METHOD_DELETE, '/projects/' . $id . '/webhooks/' . $webhookId, array_merge([ + $response = $this->client->call(Client::METHOD_DELETE, '/webhooks/' . $webhookId, array_merge([ 'content-type' => 'application/json', - 'x-appwrite-project' => $this->getProject()['$id'], + 'x-appwrite-project' => $id, + 'x-appwrite-mode' => 'admin' ], $this->getHeaders()), []); $this->assertEquals(204, $response['headers']['status-code']); $this->assertEmpty($response['body']); - $response = $this->client->call(Client::METHOD_GET, '/projects/' . $id . '/webhooks/' . $webhookId, array_merge([ + $response = $this->client->call(Client::METHOD_GET, '/webhooks/' . $webhookId, array_merge([ 'content-type' => 'application/json', - 'x-appwrite-project' => $this->getProject()['$id'], + 'x-appwrite-project' => $id, + 'x-appwrite-mode' => 'admin' ], $this->getHeaders()), []); $this->assertEquals(404, $response['headers']['status-code']); @@ -2990,9 +3009,10 @@ class ProjectsConsoleClientTest extends Scope /** * Test for FAILURE */ - $response = $this->client->call(Client::METHOD_DELETE, '/projects/' . $id . '/webhooks/error', array_merge([ + $response = $this->client->call(Client::METHOD_DELETE, '/webhooks/error', array_merge([ 'content-type' => 'application/json', - 'x-appwrite-project' => $this->getProject()['$id'], + 'x-appwrite-project' => $id, + 'x-appwrite-mode' => 'admin' ], $this->getHeaders()), []); $this->assertEquals(404, $response['headers']['status-code']); @@ -4350,9 +4370,10 @@ class ProjectsConsoleClientTest extends Scope /** * Test for FAILURE */ - $response = $this->client->call(Client::METHOD_DELETE, '/projects/' . $id . '/webhooks/error', array_merge([ + $response = $this->client->call(Client::METHOD_DELETE, '/webhooks/error', array_merge([ 'content-type' => 'application/json', - 'x-appwrite-project' => $this->getProject()['$id'], + 'x-appwrite-project' => $id, + 'x-appwrite-mode' => 'admin' ], $this->getHeaders()), []); $this->assertEquals(404, $response['headers']['status-code']); From b80d76e287a2ff02b33f8f6f4f12a4e0e8d2dc32 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Matej=20Ba=C4=8Do?= Date: Thu, 19 Mar 2026 12:38:35 +0100 Subject: [PATCH 4/4] Fix failing test --- tests/e2e/Services/Projects/ProjectsBase.php | 1 + 1 file changed, 1 insertion(+) diff --git a/tests/e2e/Services/Projects/ProjectsBase.php b/tests/e2e/Services/Projects/ProjectsBase.php index 35b8b86404..231ec302de 100644 --- a/tests/e2e/Services/Projects/ProjectsBase.php +++ b/tests/e2e/Services/Projects/ProjectsBase.php @@ -84,6 +84,7 @@ trait ProjectsBase $response = $this->client->call(Client::METHOD_POST, '/webhooks', array_merge([ 'content-type' => 'application/json', 'x-appwrite-project' => $id, + 'x-appwrite-mode' => 'admin', ], $this->getHeaders()), [ 'webhookId' => 'unique()', 'name' => 'Webhook Test',