mirror of
https://github.com/appwrite/appwrite
synced 2026-05-23 08:58:35 +00:00
Merge pull request #7128 from appwrite/fix-limit-failed-webhook-attempts
Limit webhook failure attempts to 10
This commit is contained in:
commit
0206e4ee0c
13 changed files with 385 additions and 49 deletions
|
|
@ -1486,7 +1486,7 @@ $commonCollections = [
|
|||
[
|
||||
'$id' => ID::custom('_key_enabled_type'),
|
||||
'type' => Database::INDEX_KEY,
|
||||
'attributes' => ['enabled','type'],
|
||||
'attributes' => ['enabled', 'type'],
|
||||
'lengths' => [],
|
||||
'orders' => [Database::ORDER_ASC],
|
||||
],
|
||||
|
|
@ -4577,6 +4577,39 @@ $consoleCollections = array_merge([
|
|||
'array' => false,
|
||||
'filters' => [],
|
||||
],
|
||||
[
|
||||
'$id' => ID::custom('enabled'),
|
||||
'type' => Database::VAR_BOOLEAN,
|
||||
'signed' => true,
|
||||
'size' => 0,
|
||||
'format' => '',
|
||||
'filters' => [],
|
||||
'required' => false,
|
||||
'default' => true,
|
||||
'array' => false,
|
||||
],
|
||||
[
|
||||
'$id' => ID::custom('logs'),
|
||||
'type' => Database::VAR_STRING,
|
||||
'format' => '',
|
||||
'size' => 1000000,
|
||||
'signed' => true,
|
||||
'required' => false,
|
||||
'default' => '',
|
||||
'array' => false,
|
||||
'filters' => [],
|
||||
],
|
||||
[
|
||||
'$id' => ID::custom('attempts'),
|
||||
'type' => Database::VAR_INTEGER,
|
||||
'format' => '',
|
||||
'size' => 0,
|
||||
'signed' => true,
|
||||
'required' => false,
|
||||
'default' => 0,
|
||||
'array' => false,
|
||||
'filters' => [],
|
||||
],
|
||||
],
|
||||
'indexes' => [
|
||||
[
|
||||
|
|
|
|||
|
|
@ -6,6 +6,11 @@
|
|||
rel="stylesheet"
|
||||
/>
|
||||
<style>
|
||||
@media (max-width:500px) {
|
||||
.mobile-full-width {
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
.main a {
|
||||
color: currentColor;
|
||||
}
|
||||
|
|
@ -169,7 +174,7 @@
|
|||
</tr>
|
||||
</table>
|
||||
|
||||
<table style="margin-top: 40px">
|
||||
<table style="margin-top: 32px">
|
||||
<tr>
|
||||
<td>{{message}}</td>
|
||||
</tr>
|
||||
|
|
|
|||
20
app/config/locale/templates/email-webhook-failed.tpl
Normal file
20
app/config/locale/templates/email-webhook-failed.tpl
Normal file
|
|
@ -0,0 +1,20 @@
|
|||
<p>Hi <strong>{{user}}</strong>,</p>
|
||||
<p>Your webhook <strong>{{webhook}}</strong> on project <strong>{{project}}</strong> has been paused after {{attempts}} consecutive failures.</p>
|
||||
<p>Webhook Endpoint: <strong>{{url}}</strong></p>
|
||||
<p>Error: <strong>{{error}}</strong></p>
|
||||
<p>To restore your webhook's functionality and reset attempts, we suggest to follow the below steps:</p>
|
||||
<ol>
|
||||
<li>Examine the logs of both Appwrite Console and your webhook server to identify the issue.</li>
|
||||
<li>Investigate potential network issues and use webhook testing tools to verify expected behaviour.</li>
|
||||
<li>Ensure the webhook endpoint is reachable and configured to accept incoming POST requests.</li>
|
||||
<li>Confirm that the webhook doesn't return error status codes such as 400 or 500.</li>
|
||||
</ol>
|
||||
<p>After the issue is resolved, please make sure to re-enable the webhook directly through the webhook settings.</p>
|
||||
|
||||
<table border="0" cellspacing="0" cellpadding="0" style="padding-top: 10px; padding-bottom: 10px; margin-top: 32px">
|
||||
<tr>
|
||||
<td style="border-radius: 8px; display: block; width: 100%;">
|
||||
<a class="mobile-full-width" rel="noopener" target="_blank" href="{{protocol}}://{{hostname}}{{redirect}}" style="font-size: 14px; font-family: Inter; color: #ffffff; text-decoration: none; background-color: #FD366E; border-radius: 8px; padding: 9px 14px; border: 1px solid #FD366E; display: inline-block; text-align:center; box-sizing: border-box;">Webhook settings</a>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
|
|
@ -27,6 +27,7 @@ use Utopia\Database\Query;
|
|||
use Utopia\Database\Validator\Authorization;
|
||||
use Utopia\Database\Validator\Datetime as DatetimeValidator;
|
||||
use Utopia\Database\Validator\UID;
|
||||
use Utopia\Domains\Validator\PublicDomain;
|
||||
use Utopia\Locale\Locale;
|
||||
use Utopia\Pools\Group;
|
||||
use Utopia\Registry\Registry;
|
||||
|
|
@ -34,6 +35,7 @@ use Utopia\Validator\ArrayList;
|
|||
use Utopia\Validator\Boolean;
|
||||
use Utopia\Validator\Hostname;
|
||||
use Utopia\Validator\Integer;
|
||||
use Utopia\Validator\Multiple;
|
||||
use Utopia\Validator\Range;
|
||||
use Utopia\Validator\Text;
|
||||
use Utopia\Validator\URL;
|
||||
|
|
@ -897,14 +899,15 @@ App::post('/v1/projects/:projectId/webhooks')
|
|||
->label('sdk.response.model', Response::MODEL_WEBHOOK)
|
||||
->param('projectId', '', new UID(), 'Project unique ID.')
|
||||
->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', null, new URL(['http', 'https']), 'Webhook URL.')
|
||||
->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('dbForConsole')
|
||||
->action(function (string $projectId, string $name, array $events, string $url, bool $security, string $httpUser, string $httpPass, Response $response, Database $dbForConsole) {
|
||||
->action(function (string $projectId, string $name, bool $enabled, array $events, string $url, bool $security, string $httpUser, string $httpPass, Response $response, Database $dbForConsole) {
|
||||
|
||||
$project = $dbForConsole->getDocument('projects', $projectId);
|
||||
|
||||
|
|
@ -930,6 +933,7 @@ App::post('/v1/projects/:projectId/webhooks')
|
|||
'httpUser' => $httpUser,
|
||||
'httpPass' => $httpPass,
|
||||
'signatureKey' => \bin2hex(\random_bytes(64)),
|
||||
'enabled' => $enabled,
|
||||
]);
|
||||
|
||||
$webhook = $dbForConsole->createDocument('webhooks', $webhook);
|
||||
|
|
@ -1020,14 +1024,15 @@ App::put('/v1/projects/:projectId/webhooks/:webhookId')
|
|||
->param('projectId', '', new UID(), 'Project unique ID.')
|
||||
->param('webhookId', '', new UID(), 'Webhook unique ID.')
|
||||
->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', null, new URL(['http', 'https']), 'Webhook URL.')
|
||||
->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('dbForConsole')
|
||||
->action(function (string $projectId, string $webhookId, string $name, array $events, string $url, bool $security, string $httpUser, string $httpPass, Response $response, Database $dbForConsole) {
|
||||
->action(function (string $projectId, string $webhookId, string $name, bool $enabled, array $events, string $url, bool $security, string $httpUser, string $httpPass, Response $response, Database $dbForConsole) {
|
||||
|
||||
$project = $dbForConsole->getDocument('projects', $projectId);
|
||||
|
||||
|
|
@ -1052,7 +1057,12 @@ App::put('/v1/projects/:projectId/webhooks/:webhookId')
|
|||
->setAttribute('url', $url)
|
||||
->setAttribute('security', $security)
|
||||
->setAttribute('httpUser', $httpUser)
|
||||
->setAttribute('httpPass', $httpPass);
|
||||
->setAttribute('httpPass', $httpPass)
|
||||
->setAttribute('enabled', $enabled);
|
||||
|
||||
if ($enabled) {
|
||||
$webhook->setAttribute('attempts', 0);
|
||||
}
|
||||
|
||||
$dbForConsole->updateDocument('webhooks', $webhook->getId(), $webhook);
|
||||
$dbForConsole->deleteCachedDocument('projects', $project->getId());
|
||||
|
|
|
|||
|
|
@ -79,6 +79,7 @@ use Utopia\Validator\IP;
|
|||
use Utopia\Validator\URL;
|
||||
use Utopia\Validator\WhiteList;
|
||||
use Utopia\CLI\Console;
|
||||
use Utopia\Domains\Validator\PublicDomain;
|
||||
|
||||
const APP_NAME = 'Appwrite';
|
||||
const APP_DOMAIN = 'appwrite.io';
|
||||
|
|
@ -230,6 +231,12 @@ $register = new Registry();
|
|||
|
||||
App::setMode(App::getEnv('_APP_ENV', App::MODE_TYPE_PRODUCTION));
|
||||
|
||||
if (!App::isProduction()) {
|
||||
// Allow specific domains to skip public domain validation in dev environment
|
||||
// Useful for existing tests involving webhooks
|
||||
PublicDomain::allow(['request-catcher']);
|
||||
}
|
||||
|
||||
/*
|
||||
* ENV vars
|
||||
*/
|
||||
|
|
|
|||
|
|
@ -240,7 +240,10 @@ $worker
|
|||
->inject('error')
|
||||
->inject('logger')
|
||||
->inject('log')
|
||||
->action(function (Throwable $error, ?Logger $logger, Log $log) use ($queueName) {
|
||||
->inject('pools')
|
||||
->action(function (Throwable $error, ?Logger $logger, Log $log, Group $pools) use ($queueName) {
|
||||
$pools->reclaim();
|
||||
|
||||
$version = App::getEnv('_APP_VERSION', 'UNKNOWN');
|
||||
|
||||
if ($error instanceof PDOException) {
|
||||
|
|
|
|||
|
|
@ -50,9 +50,9 @@
|
|||
"utopia-php/cli": "0.15.*",
|
||||
"utopia-php/config": "0.2.*",
|
||||
"utopia-php/database": "0.45.*",
|
||||
"utopia-php/domains": "0.3.*",
|
||||
"utopia-php/domains": "0.5.*",
|
||||
"utopia-php/dsn": "0.1.*",
|
||||
"utopia-php/framework": "0.31.1",
|
||||
"utopia-php/framework": "0.32.*",
|
||||
"utopia-php/image": "0.5.*",
|
||||
"utopia-php/locale": "0.4.*",
|
||||
"utopia-php/logger": "0.3.*",
|
||||
|
|
|
|||
54
composer.lock
generated
54
composer.lock
generated
|
|
@ -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": "0922b311842c222911fce1ae3e3b352e",
|
||||
"content-hash": "b493981ce1e062708a4f86b0ceff315c",
|
||||
"packages": [
|
||||
{
|
||||
"name": "adhocore/jwt",
|
||||
|
|
@ -1964,16 +1964,16 @@
|
|||
},
|
||||
{
|
||||
"name": "utopia-php/domains",
|
||||
"version": "0.3.2",
|
||||
"version": "0.5.0",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/utopia-php/domains.git",
|
||||
"reference": "aaa8c9a96c69ccb397997b1f4f2299c66f77eefb"
|
||||
"reference": "bf07f60326f8389f378ddf6fcde86217e5cfe18c"
|
||||
},
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/utopia-php/domains/zipball/aaa8c9a96c69ccb397997b1f4f2299c66f77eefb",
|
||||
"reference": "aaa8c9a96c69ccb397997b1f4f2299c66f77eefb",
|
||||
"url": "https://api.github.com/repos/utopia-php/domains/zipball/bf07f60326f8389f378ddf6fcde86217e5cfe18c",
|
||||
"reference": "bf07f60326f8389f378ddf6fcde86217e5cfe18c",
|
||||
"shasum": ""
|
||||
},
|
||||
"require": {
|
||||
|
|
@ -2018,9 +2018,9 @@
|
|||
],
|
||||
"support": {
|
||||
"issues": "https://github.com/utopia-php/domains/issues",
|
||||
"source": "https://github.com/utopia-php/domains/tree/0.3.2"
|
||||
"source": "https://github.com/utopia-php/domains/tree/0.5.0"
|
||||
},
|
||||
"time": "2023-07-19T16:39:24+00:00"
|
||||
"time": "2024-01-03T22:04:27+00:00"
|
||||
},
|
||||
{
|
||||
"name": "utopia-php/dsn",
|
||||
|
|
@ -2071,16 +2071,16 @@
|
|||
},
|
||||
{
|
||||
"name": "utopia-php/framework",
|
||||
"version": "0.31.1",
|
||||
"version": "0.32.0",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/utopia-php/http.git",
|
||||
"reference": "e50d2d16f4bc31319043f3f6d3dbea36c6fd6b68"
|
||||
"reference": "ad6f7e6d6b38cf5bed4e3af9a1394c59d4bb9225"
|
||||
},
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/utopia-php/http/zipball/e50d2d16f4bc31319043f3f6d3dbea36c6fd6b68",
|
||||
"reference": "e50d2d16f4bc31319043f3f6d3dbea36c6fd6b68",
|
||||
"url": "https://api.github.com/repos/utopia-php/http/zipball/ad6f7e6d6b38cf5bed4e3af9a1394c59d4bb9225",
|
||||
"reference": "ad6f7e6d6b38cf5bed4e3af9a1394c59d4bb9225",
|
||||
"shasum": ""
|
||||
},
|
||||
"require": {
|
||||
|
|
@ -2110,9 +2110,9 @@
|
|||
],
|
||||
"support": {
|
||||
"issues": "https://github.com/utopia-php/http/issues",
|
||||
"source": "https://github.com/utopia-php/http/tree/0.31.1"
|
||||
"source": "https://github.com/utopia-php/http/tree/0.32.0"
|
||||
},
|
||||
"time": "2023-12-08T18:47:29+00:00"
|
||||
"time": "2023-12-26T14:18:36+00:00"
|
||||
},
|
||||
{
|
||||
"name": "utopia-php/image",
|
||||
|
|
@ -3140,16 +3140,16 @@
|
|||
"packages-dev": [
|
||||
{
|
||||
"name": "appwrite/sdk-generator",
|
||||
"version": "0.36.0",
|
||||
"version": "0.36.1",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/appwrite/sdk-generator.git",
|
||||
"reference": "3a10f1f895ed71120442ff71eb6adec3fd6b4e8a"
|
||||
"reference": "ca4700bfbbb8bcf1c0d5a49fc5efc38da98d0992"
|
||||
},
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/appwrite/sdk-generator/zipball/3a10f1f895ed71120442ff71eb6adec3fd6b4e8a",
|
||||
"reference": "3a10f1f895ed71120442ff71eb6adec3fd6b4e8a",
|
||||
"url": "https://api.github.com/repos/appwrite/sdk-generator/zipball/ca4700bfbbb8bcf1c0d5a49fc5efc38da98d0992",
|
||||
"reference": "ca4700bfbbb8bcf1c0d5a49fc5efc38da98d0992",
|
||||
"shasum": ""
|
||||
},
|
||||
"require": {
|
||||
|
|
@ -3185,9 +3185,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/0.36.0"
|
||||
"source": "https://github.com/appwrite/sdk-generator/tree/0.36.1"
|
||||
},
|
||||
"time": "2023-11-20T10:03:06+00:00"
|
||||
"time": "2024-01-18T06:24:47+00:00"
|
||||
},
|
||||
{
|
||||
"name": "doctrine/deprecations",
|
||||
|
|
@ -5380,16 +5380,16 @@
|
|||
},
|
||||
{
|
||||
"name": "squizlabs/php_codesniffer",
|
||||
"version": "3.8.0",
|
||||
"version": "3.8.1",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/PHPCSStandards/PHP_CodeSniffer.git",
|
||||
"reference": "5805f7a4e4958dbb5e944ef1e6edae0a303765e7"
|
||||
"reference": "14f5fff1e64118595db5408e946f3a22c75807f7"
|
||||
},
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/PHPCSStandards/PHP_CodeSniffer/zipball/5805f7a4e4958dbb5e944ef1e6edae0a303765e7",
|
||||
"reference": "5805f7a4e4958dbb5e944ef1e6edae0a303765e7",
|
||||
"url": "https://api.github.com/repos/PHPCSStandards/PHP_CodeSniffer/zipball/14f5fff1e64118595db5408e946f3a22c75807f7",
|
||||
"reference": "14f5fff1e64118595db5408e946f3a22c75807f7",
|
||||
"shasum": ""
|
||||
},
|
||||
"require": {
|
||||
|
|
@ -5399,11 +5399,11 @@
|
|||
"php": ">=5.4.0"
|
||||
},
|
||||
"require-dev": {
|
||||
"phpunit/phpunit": "^4.0 || ^5.0 || ^6.0 || ^7.0 || ^8.0 || ^9.0"
|
||||
"phpunit/phpunit": "^4.0 || ^5.0 || ^6.0 || ^7.0 || ^8.0 || ^9.3.4"
|
||||
},
|
||||
"bin": [
|
||||
"bin/phpcs",
|
||||
"bin/phpcbf"
|
||||
"bin/phpcbf",
|
||||
"bin/phpcs"
|
||||
],
|
||||
"type": "library",
|
||||
"extra": {
|
||||
|
|
@ -5456,7 +5456,7 @@
|
|||
"type": "open_collective"
|
||||
}
|
||||
],
|
||||
"time": "2023-12-08T12:32:31+00:00"
|
||||
"time": "2024-01-11T20:47:48+00:00"
|
||||
},
|
||||
{
|
||||
"name": "swoole/ide-helper",
|
||||
|
|
|
|||
|
|
@ -192,6 +192,7 @@ services:
|
|||
- _APP_MESSAGE_SMS_TEST_DSN
|
||||
- _APP_MESSAGE_EMAIL_TEST_DSN
|
||||
- _APP_MESSAGE_PUSH_TEST_DSN
|
||||
|
||||
appwrite-realtime:
|
||||
entrypoint: realtime
|
||||
<<: *x-logging
|
||||
|
|
@ -289,6 +290,11 @@ services:
|
|||
- _APP_WORKER_PER_CORE
|
||||
- _APP_OPENSSL_KEY_V1
|
||||
- _APP_SYSTEM_SECURITY_EMAIL_ADDRESS
|
||||
- _APP_DB_HOST
|
||||
- _APP_DB_PORT
|
||||
- _APP_DB_SCHEMA
|
||||
- _APP_DB_USER
|
||||
- _APP_DB_PASS
|
||||
- _APP_REDIS_HOST
|
||||
- _APP_REDIS_PORT
|
||||
- _APP_REDIS_USER
|
||||
|
|
@ -554,6 +560,8 @@ services:
|
|||
- _APP_SMTP_PASSWORD
|
||||
- _APP_LOGGING_PROVIDER
|
||||
- _APP_LOGGING_CONFIG
|
||||
- _APP_DOMAIN
|
||||
- _APP_OPTIONS_FORCE_HTTPS
|
||||
|
||||
appwrite-worker-messaging:
|
||||
entrypoint: worker-messaging
|
||||
|
|
|
|||
|
|
@ -29,7 +29,7 @@ class Mails extends Action
|
|||
->inject('message')
|
||||
->inject('register')
|
||||
->inject('log')
|
||||
->callback(fn(Message $message, Registry $register, Log $log) => $this->action($message, $register, $log));
|
||||
->callback(fn (Message $message, Registry $register, Log $log) => $this->action($message, $register, $log));
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -63,6 +63,11 @@ class Mails extends Action
|
|||
$name = $payload['name'];
|
||||
$body = $payload['body'];
|
||||
|
||||
$protocol = App::getEnv('_APP_OPTIONS_FORCE_HTTPS') == 'disabled' ? 'http' : 'https';
|
||||
$hostname = App::getEnv('_APP_DOMAIN');
|
||||
|
||||
$body = str_replace(['{{protocol}}', '{{hostname}}'], [$protocol, $hostname], $body);
|
||||
|
||||
$bodyTemplate = Template::fromFile(__DIR__ . '/../../../../app/config/locale/templates/email-base.tpl');
|
||||
$bodyTemplate->setParam('{{body}}', $body);
|
||||
foreach ($variables as $key => $value) {
|
||||
|
|
|
|||
|
|
@ -2,9 +2,13 @@
|
|||
|
||||
namespace Appwrite\Platform\Workers;
|
||||
|
||||
use Appwrite\Event\Mail;
|
||||
use Appwrite\Template\Template;
|
||||
use Exception;
|
||||
use Utopia\App;
|
||||
use Utopia\Database\Document;
|
||||
use Utopia\Database\Database;
|
||||
use Utopia\Database\Query;
|
||||
use Utopia\Logger\Log;
|
||||
use Utopia\Platform\Action;
|
||||
use Utopia\Queue\Message;
|
||||
|
|
@ -12,6 +16,8 @@ use Utopia\Queue\Message;
|
|||
class Webhooks extends Action
|
||||
{
|
||||
private array $errors = [];
|
||||
private const MAX_FAILED_ATTEMPTS = 10;
|
||||
private const MAX_FILE_SIZE = 5242880; // 5 MB
|
||||
|
||||
public static function getName(): string
|
||||
{
|
||||
|
|
@ -26,18 +32,23 @@ class Webhooks extends Action
|
|||
$this
|
||||
->desc('Webhooks worker')
|
||||
->inject('message')
|
||||
->inject('dbForConsole')
|
||||
->inject('queueForMails')
|
||||
->inject('log')
|
||||
->callback(fn (Message $message, Log $log) => $this->action($message, $log));
|
||||
->callback(fn (Message $message, Database $dbForConsole, Mail $queueForMails, Log $log) => $this->action($message, $dbForConsole, $queueForMails, $log));
|
||||
}
|
||||
|
||||
/**
|
||||
* @param Message $message
|
||||
* @param Database $dbForConsole
|
||||
* @param Mail $queueForMails
|
||||
* @param Log $log
|
||||
* @return void
|
||||
* @throws Exception
|
||||
*/
|
||||
public function action(Message $message, Log $log): void
|
||||
public function action(Message $message, Database $dbForConsole, Mail $queueForMails, Log $log): void
|
||||
{
|
||||
$this->errors = [];
|
||||
$payload = $message->getPayload() ?? [];
|
||||
|
||||
if (empty($payload)) {
|
||||
|
|
@ -53,7 +64,7 @@ class Webhooks extends Action
|
|||
|
||||
foreach ($project->getAttribute('webhooks', []) as $webhook) {
|
||||
if (array_intersect($webhook->getAttribute('events', []), $events)) {
|
||||
$this->execute($events, $webhookPayload, $webhook, $user, $project);
|
||||
$this->execute($events, $webhookPayload, $webhook, $user, $project, $dbForConsole, $queueForMails);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -68,10 +79,15 @@ class Webhooks extends Action
|
|||
* @param Document $webhook
|
||||
* @param Document $user
|
||||
* @param Document $project
|
||||
* @param Database $dbForConsole
|
||||
* @param Mail $queueForMails
|
||||
* @return void
|
||||
*/
|
||||
private function execute(array $events, string $payload, Document $webhook, Document $user, Document $project): void
|
||||
private function execute(array $events, string $payload, Document $webhook, Document $user, Document $project, Database $dbForConsole, Mail $queueForMails): void
|
||||
{
|
||||
if ($webhook->getAttribute('enabled') !== true) {
|
||||
return;
|
||||
}
|
||||
|
||||
$url = \rawurldecode($webhook->getAttribute('url'));
|
||||
$signatureKey = $webhook->getAttribute('signatureKey');
|
||||
|
|
@ -83,9 +99,9 @@ class Webhooks extends Action
|
|||
\curl_setopt($ch, CURLOPT_CUSTOMREQUEST, 'POST');
|
||||
\curl_setopt($ch, CURLOPT_POSTFIELDS, $payload);
|
||||
\curl_setopt($ch, CURLOPT_HEADER, 0);
|
||||
\curl_setopt($ch, CURLOPT_RETURNTRANSFER, 0);
|
||||
\curl_setopt($ch, CURLOPT_RETURNTRANSFER, 1);
|
||||
\curl_setopt($ch, CURLOPT_TIMEOUT, 15);
|
||||
\curl_setopt($ch, CURLOPT_MAXFILESIZE, 5242880);
|
||||
\curl_setopt($ch, CURLOPT_MAXFILESIZE, self::MAX_FILE_SIZE);
|
||||
\curl_setopt($ch, CURLOPT_USERAGENT, \sprintf(
|
||||
APP_USERAGENT,
|
||||
App::getEnv('_APP_VERSION', 'UNKNOWN'),
|
||||
|
|
@ -117,10 +133,98 @@ class Webhooks extends Action
|
|||
\curl_setopt($ch, CURLOPT_HTTPAUTH, CURLAUTH_BASIC);
|
||||
}
|
||||
|
||||
if (false === \curl_exec($ch)) {
|
||||
$this->errors[] = \curl_error($ch) . ' in events ' . implode(', ', $events) . ' for webhook ' . $webhook->getAttribute('name');
|
||||
}
|
||||
|
||||
$responseBody = \curl_exec($ch);
|
||||
$curlError = \curl_error($ch);
|
||||
$statusCode = curl_getinfo($ch, CURLINFO_RESPONSE_CODE);
|
||||
\curl_close($ch);
|
||||
|
||||
if (!empty($curlError) || $statusCode >= 400) {
|
||||
$dbForConsole->increaseDocumentAttribute('webhooks', $webhook->getId(), 'attempts', 1);
|
||||
$webhook = $dbForConsole->getDocument('webhooks', $webhook->getId());
|
||||
$attempts = $webhook->getAttribute('attempts');
|
||||
|
||||
$logs = '';
|
||||
$logs .= 'URL: ' . $webhook->getAttribute('url') . "\n";
|
||||
$logs .= 'Method: ' . 'POST' . "\n";
|
||||
|
||||
if (!empty($curlError)) {
|
||||
$logs .= 'CURL Error: ' . $curlError . "\n";
|
||||
$logs .= 'Events: ' . implode(', ', $events) . "\n";
|
||||
} else {
|
||||
$logs .= 'Status code: ' . $statusCode . "\n";
|
||||
$logs .= 'Body: ' . "\n" . \mb_strcut($responseBody, 0, 10000) . "\n"; // Limit to 10kb
|
||||
}
|
||||
|
||||
$webhook->setAttribute('logs', $logs);
|
||||
|
||||
if ($attempts >= self::MAX_FAILED_ATTEMPTS) {
|
||||
$webhook->setAttribute('enabled', false);
|
||||
$this->sendEmailAlert($attempts, $statusCode, $webhook, $project, $dbForConsole, $queueForMails);
|
||||
}
|
||||
|
||||
$dbForConsole->updateDocument('webhooks', $webhook->getId(), $webhook);
|
||||
$dbForConsole->deleteCachedDocument('projects', $project->getId());
|
||||
|
||||
$this->errors[] = $logs;
|
||||
} else {
|
||||
$webhook->setAttribute('attempts', 0); // Reset attempts on success
|
||||
$dbForConsole->updateDocument('webhooks', $webhook->getId(), $webhook);
|
||||
$dbForConsole->deleteCachedDocument('projects', $project->getId());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param int $attempts
|
||||
* @param mixed $statusCode
|
||||
* @param Document $webhook
|
||||
* @param Document $project
|
||||
* @param Database $dbForConsole
|
||||
* @param Mail $queueForMails
|
||||
* @return void
|
||||
*/
|
||||
public function sendEmailAlert(int $attempts, mixed $statusCode, Document $webhook, Document $project, Database $dbForConsole, Mail $queueForMails): void
|
||||
{
|
||||
$memberships = $dbForConsole->find('memberships', [
|
||||
Query::equal('teamInternalId', [$project->getAttribute('teamInternalId')]),
|
||||
Query::limit(APP_LIMIT_SUBQUERY)
|
||||
]);
|
||||
|
||||
$userIds = array_column(\array_map(fn ($membership) => $membership->getArrayCopy(), $memberships), 'userId');
|
||||
|
||||
$users = $dbForConsole->find('users', [
|
||||
Query::equal('$id', $userIds),
|
||||
]);
|
||||
|
||||
$projectId = $project->getId();
|
||||
$webhookId = $webhook->getId();
|
||||
|
||||
$template = Template::fromFile(__DIR__ . '/../../../../app/config/locale/templates/email-webhook-failed.tpl');
|
||||
|
||||
$template->setParam('{{webhook}}', $webhook->getAttribute('name'));
|
||||
$template->setParam('{{project}}', $project->getAttribute('name'));
|
||||
$template->setParam('{{url}}', $webhook->getAttribute('url'));
|
||||
$template->setParam('{{error}}', $curlError ?? 'The server returned ' . $statusCode . ' status code');
|
||||
$template->setParam('{{redirect}}', "/console/project-$projectId/settings/webhooks/$webhookId");
|
||||
$template->setParam('{{attempts}}', $attempts);
|
||||
|
||||
$subject = 'Webhook deliveries have been paused';
|
||||
$body = Template::fromFile(__DIR__ . '/../../../../app/config/locale/templates/email-base-styled.tpl');
|
||||
|
||||
$body
|
||||
->setParam('{{subject}}', $subject)
|
||||
->setParam('{{message}}', $template->render())
|
||||
->setParam('{{year}}', date("Y"));
|
||||
|
||||
$queueForMails
|
||||
->setSubject($subject)
|
||||
->setBody($body->render());
|
||||
|
||||
foreach ($users as $user) {
|
||||
$queueForMails
|
||||
->setVariables(['user' => $user->getAttribute('name', '')])
|
||||
->setName($user->getAttribute('name', ''))
|
||||
->setRecipient($user->getAttribute('email'))
|
||||
->trigger();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -76,7 +76,24 @@ class Webhook extends Model
|
|||
'default' => '',
|
||||
'example' => 'ad3d581ca230e2b7059c545e5a',
|
||||
])
|
||||
;
|
||||
->addRule('enabled', [
|
||||
'type' => self::TYPE_BOOLEAN,
|
||||
'description' => 'Indicates if this webhook is enabled.',
|
||||
'default' => true,
|
||||
'example' => true,
|
||||
])
|
||||
->addRule('logs', [
|
||||
'type' => self::TYPE_STRING,
|
||||
'description' => 'Webhook error logs from the most recent failure.',
|
||||
'default' => '',
|
||||
'example' => 'Failed to connect to remote server.',
|
||||
])
|
||||
->addRule('attempts', [
|
||||
'type' => self::TYPE_INTEGER,
|
||||
'description' => 'Number of consecutive failed webhook attempts.',
|
||||
'default' => 0,
|
||||
'example' => 10,
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
|||
|
|
@ -997,4 +997,128 @@ trait WebhooksBase
|
|||
$this->assertEquals(true, (new DatetimeValidator())->isValid($webhook['data']['invited']));
|
||||
$this->assertEquals(('server' === $this->getSide()), $webhook['data']['confirm']);
|
||||
}
|
||||
|
||||
public function testCreateWebhookWithPrivateDomain(): void
|
||||
{
|
||||
/**
|
||||
* Test for FAILURE
|
||||
*/
|
||||
$projectId = $this->getProject()['$id'];
|
||||
$webhook = $this->client->call(Client::METHOD_POST, '/projects/' . $projectId . '/webhooks', [
|
||||
'origin' => 'http://localhost',
|
||||
'content-type' => 'application/json',
|
||||
'cookie' => 'a_session_console=' . $this->getRoot()['session'],
|
||||
'x-appwrite-project' => 'console',
|
||||
], [
|
||||
'name' => 'Webhook Test',
|
||||
'enabled' => true,
|
||||
'events' => [
|
||||
'databases.*',
|
||||
'functions.*',
|
||||
'buckets.*',
|
||||
'teams.*',
|
||||
'users.*'
|
||||
],
|
||||
'url' => 'http://localhost/webhook', // private domains not allowed
|
||||
'security' => false,
|
||||
]);
|
||||
|
||||
$this->assertEquals(400, $webhook['headers']['status-code']);
|
||||
}
|
||||
|
||||
public function testUpdateWebhookWithPrivateDomain(): void
|
||||
{
|
||||
/**
|
||||
* Test for FAILURE
|
||||
*/
|
||||
$projectId = $this->getProject()['$id'];
|
||||
$webhookId = $this->getProject()['webhookId'];
|
||||
$webhook = $this->client->call(Client::METHOD_PUT, '/projects/' . $projectId . '/webhooks/' . $webhookId, [
|
||||
'origin' => 'http://localhost',
|
||||
'content-type' => 'application/json',
|
||||
'cookie' => 'a_session_console=' . $this->getRoot()['session'],
|
||||
'x-appwrite-project' => 'console',
|
||||
], [
|
||||
'name' => 'Webhook Test',
|
||||
'enabled' => true,
|
||||
'events' => [
|
||||
'databases.*',
|
||||
'functions.*',
|
||||
'buckets.*',
|
||||
'teams.*',
|
||||
'users.*'
|
||||
],
|
||||
'url' => 'http://localhost/webhook', // private domains not allowed
|
||||
'security' => false,
|
||||
]);
|
||||
|
||||
$this->assertEquals(400, $webhook['headers']['status-code']);
|
||||
}
|
||||
|
||||
/**
|
||||
* @depends testCreateCollection
|
||||
*/
|
||||
public function testWebhookAutoDisable(array $data): void
|
||||
{
|
||||
$projectId = $this->getProject()['$id'];
|
||||
$webhookId = $this->getProject()['webhookId'];
|
||||
$databaseId = $data['databaseId'];
|
||||
|
||||
$webhook = $this->client->call(Client::METHOD_PUT, '/projects/' . $projectId . '/webhooks/' . $webhookId, [
|
||||
'origin' => 'http://localhost',
|
||||
'content-type' => 'application/json',
|
||||
'cookie' => 'a_session_console=' . $this->getRoot()['session'],
|
||||
'x-appwrite-project' => 'console',
|
||||
], [
|
||||
'name' => 'Webhook Test',
|
||||
'enabled' => true,
|
||||
'events' => [
|
||||
'databases.*',
|
||||
'functions.*',
|
||||
'buckets.*',
|
||||
'teams.*',
|
||||
'users.*'
|
||||
],
|
||||
'url' => 'http://appwrite-non-existing-domain.com', // set non-existent URL
|
||||
'security' => false,
|
||||
]);
|
||||
|
||||
$this->assertEquals(200, $webhook['headers']['status-code']);
|
||||
$this->assertNotEmpty($webhook['body']);
|
||||
|
||||
// trigger webhook for failure event 10 times
|
||||
for ($i = 0; $i < 10; $i++) {
|
||||
$newCollection = $this->client->call(Client::METHOD_POST, '/databases/' . $databaseId . '/collections', array_merge([
|
||||
'content-type' => 'application/json',
|
||||
'x-appwrite-project' => $this->getProject()['$id'],
|
||||
'x-appwrite-key' => $this->getProject()['apiKey']
|
||||
]), [
|
||||
'collectionId' => ID::unique(),
|
||||
'name' => 'newCollection' . $i,
|
||||
'permissions' => [
|
||||
Permission::read(Role::any()),
|
||||
Permission::create(Role::any()),
|
||||
Permission::update(Role::any()),
|
||||
Permission::delete(Role::any()),
|
||||
],
|
||||
'documentSecurity' => true,
|
||||
]);
|
||||
|
||||
$this->assertEquals($newCollection['headers']['status-code'], 201);
|
||||
$this->assertNotEmpty($newCollection['body']['$id']);
|
||||
}
|
||||
|
||||
sleep(10);
|
||||
|
||||
$webhook = $this->client->call(Client::METHOD_GET, '/projects/' . $projectId . '/webhooks/' . $webhookId, array_merge([
|
||||
'origin' => 'http://localhost',
|
||||
'content-type' => 'application/json',
|
||||
'cookie' => 'a_session_console=' . $this->getRoot()['session'],
|
||||
'x-appwrite-project' => 'console',
|
||||
]));
|
||||
|
||||
// assert that the webhook is now disabled after 10 consecutive failures
|
||||
$this->assertEquals($webhook['body']['enabled'], false);
|
||||
$this->assertEquals($webhook['body']['attempts'], 10);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in a new issue