diff --git a/.env b/.env index 43c198a683..c7c3476b06 100644 --- a/.env +++ b/.env @@ -47,6 +47,8 @@ _APP_SMTP_PORT=1025 _APP_SMTP_SECURE= _APP_SMTP_USERNAME= _APP_SMTP_PASSWORD= +_APP_HAMSTER_RECIPIENTS= +_APP_HAMSTER_INTERVAL=86400 _APP_SMS_PROVIDER=sms://username:password@mock _APP_SMS_FROM=+123456789 _APP_STORAGE_LIMIT=30000000 diff --git a/CHANGES.md b/CHANGES.md index 6a51a738e3..7a553d5d5e 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -4,7 +4,6 @@ - Increase Traefik TCP + file limits [#4673](https://github.com/appwrite/appwrite/pull/4673) - Fix invited account verified status [#4776](https://github.com/appwrite/appwrite/pull/4776) - Get default region from environment on project create [#4780](https://github.com/appwrite/appwrite/pull/4780) -- Store build output file size [#4844](https://github.com/appwrite/appwrite/pull/4844) - Fix max mimetype size [#4814](https://github.com/appwrite/appwrite/pull/4814) # Version 1.1.2 diff --git a/Dockerfile b/Dockerfile index 7723a65bb5..b87d6e912c 100755 --- a/Dockerfile +++ b/Dockerfile @@ -165,6 +165,7 @@ RUN chmod +x /usr/local/bin/doctor && \ chmod +x /usr/local/bin/sdks && \ chmod +x /usr/local/bin/specs && \ chmod +x /usr/local/bin/ssl && \ + chmod +x /usr/local/bin/hamster && \ chmod +x /usr/local/bin/test && \ chmod +x /usr/local/bin/vars && \ chmod +x /usr/local/bin/worker-audits && \ diff --git a/app/config/collections.php b/app/config/collections.php index 9b54b61c6f..454ed006eb 100644 --- a/app/config/collections.php +++ b/app/config/collections.php @@ -2672,6 +2672,17 @@ $collections = [ 'array' => false, 'filters' => ['datetime'], ], + [ + '$id' => ID::custom('endTime'), + 'type' => Database::VAR_DATETIME, + 'format' => '', + 'size' => 0, + 'signed' => false, + 'required' => false, + 'default' => null, + 'array' => false, + 'filters' => ['datetime'], + ], [ '$id' => ID::custom('duration'), 'type' => Database::VAR_INTEGER, @@ -2684,7 +2695,18 @@ $collections = [ 'filters' => [], ], [ - '$id' => 'deploymentInternalId', + '$id' => ID::custom('size'), + 'type' => Database::VAR_INTEGER, + 'format' => '', + 'size' => 0, + 'signed' => true, + 'required' => false, + 'default' => null, + 'array' => false, + 'filters' => [], + ], + [ + '$id' => ID::custom('deploymentInternalId'), 'type' => Database::VAR_STRING, 'format' => '', 'size' => Database::LENGTH_KEY, @@ -2738,17 +2760,6 @@ $collections = [ 'array' => false, 'filters' => [], ], - [ - '$id' => ID::custom('size'), - 'type' => Database::VAR_INTEGER, - 'format' => '', - 'size' => 0, - 'signed' => true, - 'required' => false, - 'default' => null, - 'array' => false, - 'filters' => [], - ], [ '$id' => ID::custom('stderr'), 'type' => Database::VAR_STRING, diff --git a/app/config/errors.php b/app/config/errors.php index eeeffcd9ea..a13acabe89 100644 --- a/app/config/errors.php +++ b/app/config/errors.php @@ -88,6 +88,11 @@ return [ 'description' => 'The request cannot be fulfilled with the current protocol. Please check the value of the _APP_OPTIONS_FORCE_HTTPS environment variable.', 'code' => 500, ], + Exception::GENERAL_CODES_DISABLED => [ + 'name' => Exception::GENERAL_CODES_DISABLED, + 'description' => 'Invitation codes are disabled on this server. Please contact the server administrator.', + 'code' => 500, + ], /** User Errors */ Exception::USER_COUNT_EXCEEDED => [ @@ -125,8 +130,8 @@ return [ 'description' => 'Console registration is restricted to specific emails. Contact your administrator for more information.', 'code' => 401, ], - Exception::USER_CODE_INVALID => [ - 'name' => Exception::USER_CODE_INVALID, + Exception::USER_INVALID_CODE => [ + 'name' => Exception::USER_INVALID_CODE, 'description' => 'The specified code is not valid. Contact your administrator for more information.', 'code' => 401, ], diff --git a/app/controllers/api/account.php b/app/controllers/api/account.php index e04b86e57e..d5d9539a5f 100644 --- a/app/controllers/api/account.php +++ b/app/controllers/api/account.php @@ -82,8 +82,12 @@ App::post('/v1/account/invite') $whitelistCodes = (!empty(App::getEnv('_APP_CONSOLE_WHITELIST_CODES', null))) ? \explode(',', App::getEnv('_APP_CONSOLE_WHITELIST_CODES', null)) : []; + if (empty($whitelistCodes)) { + throw new Exception(Exception::GENERAL_CODES_DISABLED); + } + if (!empty($whitelistCodes) && !\in_array($code, $whitelistCodes)) { - throw new Exception(Exception::USER_CODE_INVALID); + throw new Exception(Exception::USER_INVALID_CODE); } $limit = $project->getAttribute('auths', [])['limit'] ?? 0; @@ -170,7 +174,7 @@ App::post('/v1/account') $whitelistEmails = $project->getAttribute('authWhitelistEmails'); $whitelistIPs = $project->getAttribute('authWhitelistIPs'); - if (!empty($whitelistEmails) && !\in_array($email, $whitelistEmails)) { + if (!empty($whitelistEmails) && !\in_array($email, $whitelistEmails) && !\in_array(strtoupper($email), $whitelistEmails)) { throw new Exception(Exception::USER_EMAIL_NOT_WHITELISTED); } diff --git a/app/workers/builds.php b/app/workers/builds.php index 31417b7a3a..b3a9974c92 100644 --- a/app/workers/builds.php +++ b/app/workers/builds.php @@ -101,13 +101,14 @@ class BuildsV1 extends Worker 'deploymentId' => $deployment->getId(), 'status' => 'processing', 'path' => '', - 'size' => 0, 'runtime' => $function->getAttribute('runtime'), 'source' => $deployment->getAttribute('path'), 'sourceType' => $device, 'stdout' => '', 'stderr' => '', - 'duration' => 0 + 'endTime' => null, + 'duration' => 0, + 'size' => 0 ])); $deployment->setAttribute('buildId', $build->getId()); $deployment->setAttribute('buildInternalId', $build->getInternalId()); @@ -189,8 +190,11 @@ class BuildsV1 extends Worker ] ); + $endTime = DateTime::now(); + /** Update the build document */ $build->setAttribute('startTime', DateTime::format((new \DateTime())->setTimestamp($response['startTime']))); + $build->setAttribute('endTime', $endTime); $build->setAttribute('duration', \intval(\ceil($response['duration']))); $build->setAttribute('status', 'ready'); $build->setAttribute('path', $response['path']); @@ -226,7 +230,7 @@ class BuildsV1 extends Worker } catch (\Throwable $th) { $endTime = DateTime::now(); $interval = (new \DateTime($endTime))->diff(new \DateTime($startTime)); - + $build->setAttribute('endTime', $endTime); $build->setAttribute('duration', $interval->format('%s') + 0); $build->setAttribute('status', 'failed'); $build->setAttribute('stderr', $th->getMessage()); @@ -262,7 +266,6 @@ class BuildsV1 extends Worker ->setParam('builds.{scope}.compute', 1) ->setParam('buildStatus', $build->getAttribute('status', '')) ->setParam('buildTime', $build->getAttribute('duration')) - ->setParam('buildSize', $build->getAttribute('size')) ->setParam('networkRequestSize', 0) ->setParam('networkResponseSize', 0) ->submit(); diff --git a/bin/hamster b/bin/hamster new file mode 100644 index 0000000000..dcc7ed308d --- /dev/null +++ b/bin/hamster @@ -0,0 +1,3 @@ +#!/bin/sh + +php /usr/src/code/app/cli.php hamster $@ \ No newline at end of file diff --git a/composer.json b/composer.json index 3429fb0253..bd7be22322 100644 --- a/composer.json +++ b/composer.json @@ -73,7 +73,8 @@ "phpmailer/phpmailer": "6.6.0", "chillerlan/php-qrcode": "4.3.3", "adhocore/jwt": "1.1.2", - "slickdeals/statsd": "3.1.0" + "slickdeals/statsd": "3.1.0", + "league/csv": "^9.0.0" }, "repositories": [ { diff --git a/composer.lock b/composer.lock index 24fb668be4..4bdf4a404d 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": "e8e64d67c559b26264b6a89ebab1ab72", + "content-hash": "d388807afe22fb473b5136a75cb3d7e7", "packages": [ { "name": "adhocore/jwt", @@ -870,6 +870,93 @@ }, "time": "2022-11-29T16:25:20+00:00" }, + { + "name": "league/csv", + "version": "9.9.0", + "source": { + "type": "git", + "url": "https://github.com/thephpleague/csv.git", + "reference": "b4418ede47fbd88facc34e40a16c8ce9153b961b" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/thephpleague/csv/zipball/b4418ede47fbd88facc34e40a16c8ce9153b961b", + "reference": "b4418ede47fbd88facc34e40a16c8ce9153b961b", + "shasum": "" + }, + "require": { + "ext-json": "*", + "ext-mbstring": "*", + "php": "^8.1.2" + }, + "require-dev": { + "doctrine/collections": "^2.1.2", + "ext-dom": "*", + "ext-xdebug": "*", + "friendsofphp/php-cs-fixer": "^v3.14.3", + "phpbench/phpbench": "^1.2.8", + "phpstan/phpstan": "^1.10.4", + "phpstan/phpstan-deprecation-rules": "^1.1.2", + "phpstan/phpstan-phpunit": "^1.3.10", + "phpstan/phpstan-strict-rules": "^1.5.0", + "phpunit/phpunit": "^10.0.14" + }, + "suggest": { + "ext-dom": "Required to use the XMLConverter and the HTMLConverter classes", + "ext-iconv": "Needed to ease transcoding CSV using iconv stream filters" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "9.x-dev" + } + }, + "autoload": { + "files": [ + "src/functions_include.php" + ], + "psr-4": { + "League\\Csv\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Ignace Nyamagana Butera", + "email": "nyamsprod@gmail.com", + "homepage": "https://github.com/nyamsprod/", + "role": "Developer" + } + ], + "description": "CSV data manipulation made easy in PHP", + "homepage": "https://csv.thephpleague.com", + "keywords": [ + "convert", + "csv", + "export", + "filter", + "import", + "read", + "transform", + "write" + ], + "support": { + "docs": "https://csv.thephpleague.com", + "issues": "https://github.com/thephpleague/csv/issues", + "rss": "https://github.com/thephpleague/csv/releases.atom", + "source": "https://github.com/thephpleague/csv" + }, + "funding": [ + { + "url": "https://github.com/sponsors/nyamsprod", + "type": "github" + } + ], + "time": "2023-03-11T15:57:12+00:00" + }, { "name": "matomo/device-detector", "version": "6.0.0", diff --git a/docker-compose.yml b/docker-compose.yml index 2359e60da9..e6a12a2129 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -59,7 +59,7 @@ services: DEBUG: false TESTING: true VERSION: dev - VITE_CONSOLE_MODE: self-hosted + VITE_CONSOLE_MODE: cloud ports: - 9501:80 networks: @@ -145,6 +145,7 @@ services: - _APP_SMTP_SECURE - _APP_SMTP_USERNAME - _APP_SMTP_PASSWORD + - _APP_HAMSTER_RECIPIENTS - _APP_USAGE_STATS - _APP_INFLUXDB_HOST - _APP_INFLUXDB_PORT @@ -554,6 +555,39 @@ services: - _APP_LOGGING_PROVIDER - _APP_LOGGING_CONFIG + appwrite-hamster: + entrypoint: hamster + <<: *x-logging + container_name: appwrite-hamster + image: appwrite-dev + networks: + - appwrite + volumes: + - ./app:/usr/src/code/app + - ./src:/usr/src/code/src + depends_on: + - redis + environment: + - _APP_ENV + - _APP_WORKER_PER_CORE + - _APP_CONNECTIONS_MAX + - _APP_POOL_CLIENTS + - _APP_OPENSSL_KEY_V1 + - _APP_DB_HOST + - _APP_DB_PORT + - _APP_DB_SCHEMA + - _APP_DB_USER + - _APP_DB_PASS + - _APP_REDIS_HOST + - _APP_REDIS_PORT + - _APP_REDIS_USER + - _APP_REDIS_PASS + - _APP_CONNECTIONS_DB_CONSOLE + - _APP_CONNECTIONS_DB_PROJECT + - _APP_CONNECTIONS_CACHE + - _APP_HAMSTER_RECIPIENTS + - _APP_HAMSTER_INTERVAL + appwrite-maintenance: entrypoint: maintenance <<: *x-logging diff --git a/src/Appwrite/Extend/Exception.php b/src/Appwrite/Extend/Exception.php index f77dc50e29..bd2e63e8a6 100644 --- a/src/Appwrite/Extend/Exception.php +++ b/src/Appwrite/Extend/Exception.php @@ -50,6 +50,7 @@ class Exception extends \Exception public const GENERAL_CURSOR_NOT_FOUND = 'general_cursor_not_found'; public const GENERAL_SERVER_ERROR = 'general_server_error'; public const GENERAL_PROTOCOL_UNSUPPORTED = 'general_protocol_unsupported'; + public const GENERAL_CODES_DISABLED = 'general_codes_disabled'; /** Users */ public const USER_COUNT_EXCEEDED = 'user_count_exceeded'; @@ -60,7 +61,7 @@ class Exception extends \Exception public const USER_PASSWORD_RESET_REQUIRED = 'user_password_reset_required'; public const USER_EMAIL_NOT_WHITELISTED = 'user_email_not_whitelisted'; public const USER_IP_NOT_WHITELISTED = 'user_ip_not_whitelisted'; - public const USER_CODE_INVALID = 'user_code_invalid'; + public const USER_INVALID_CODE = 'user_invalid_code'; public const USER_INVALID_CREDENTIALS = 'user_invalid_credentials'; public const USER_ANONYMOUS_CONSOLE_PROHIBITED = 'user_anonymous_console_prohibited'; public const USER_SESSION_ALREADY_EXISTS = 'user_session_already_exists'; @@ -186,6 +187,7 @@ class Exception extends \Exception public const PLATFORM_NOT_FOUND = 'platform_not_found'; protected $type = ''; + protected $errors = []; public function __construct(string $type = Exception::GENERAL_UNKNOWN, string $message = null, int $code = null, \Throwable $previous = null) { diff --git a/src/Appwrite/Migration/Version/V17.php b/src/Appwrite/Migration/Version/V17.php index f841fcab52..dac6b740c6 100644 --- a/src/Appwrite/Migration/Version/V17.php +++ b/src/Appwrite/Migration/Version/V17.php @@ -46,17 +46,6 @@ class V17 extends Migration $this->projectDB->setNamespace("_{$this->project->getInternalId()}"); switch ($id) { - case 'files': - try { - /** - * Update 'mimeType' attribute size (127->255) - */ - $this->projectDB->updateAttribute($id, 'mimeType', Database::VAR_STRING, 255, true, false); - $this->projectDB->deleteCachedCollection($id); - } catch (\Throwable $th) { - Console::warning("'mimeType' from {$id}: {$th->getMessage()}"); - } - break; case 'builds': try { /** @@ -68,6 +57,18 @@ class V17 extends Migration Console::warning("'size' from {$id}: {$th->getMessage()}"); } + break; + case 'files': + try { + /** + * Update 'mimeType' attribute size (127->255) + */ + $this->projectDB->updateAttribute($id, 'mimeType', Database::VAR_STRING, 255, true, false); + $this->projectDB->deleteCachedCollection($id); + } catch (\Throwable $th) { + Console::warning("'mimeType' from {$id}: {$th->getMessage()}"); + } + try { /** * Delete 'endTime' attribute (use startTime+duration if needed) diff --git a/src/Appwrite/Platform/Services/Tasks.php b/src/Appwrite/Platform/Services/Tasks.php index 2e15cd015c..00cf3f89c5 100644 --- a/src/Appwrite/Platform/Services/Tasks.php +++ b/src/Appwrite/Platform/Services/Tasks.php @@ -12,6 +12,7 @@ use Appwrite\Platform\Tasks\PatchCreateMissingSchedules; use Appwrite\Platform\Tasks\SDKs; use Appwrite\Platform\Tasks\Specs; use Appwrite\Platform\Tasks\SSL; +use Appwrite\Platform\Tasks\Hamster; use Appwrite\Platform\Tasks\Usage; use Appwrite\Platform\Tasks\Vars; use Appwrite\Platform\Tasks\Version; @@ -27,6 +28,7 @@ class Tasks extends Service ->addAction(Usage::getName(), new Usage()) ->addAction(Vars::getName(), new Vars()) ->addAction(SSL::getName(), new SSL()) + ->addAction(Hamster::getName(), new Hamster()) ->addAction(Doctor::getName(), new Doctor()) ->addAction(Install::getName(), new Install()) ->addAction(Maintenance::getName(), new Maintenance()) diff --git a/src/Appwrite/Platform/Tasks/Hamster.php b/src/Appwrite/Platform/Tasks/Hamster.php new file mode 100644 index 0000000000..723f3e0e47 --- /dev/null +++ b/src/Appwrite/Platform/Tasks/Hamster.php @@ -0,0 +1,271 @@ + 'files.$all.count.total', + 'Buckets' => 'buckets.$all.count.total', + 'Databases' => 'databases.$all.count.total', + 'Documents' => 'documents.$all.count.total', + 'Collections' => 'collections.$all.count.total', + 'Storage' => 'project.$all.storage.size', + 'Requests' => 'project.$all.network.requests', + 'Bandwidth' => 'project.$all.network.bandwidth', + 'Users' => 'users.$all.count.total', + 'Sessions' => 'sessions.$all.requests.create', + 'Executions' => 'executions.$all.compute.total', + ]; + + protected string $directory = '/usr/local'; + protected string $path; + + protected string $date; + + public static function getName(): string + { + return 'hamster'; + } + + public function __construct() + { + $this + ->desc('Get stats for projects') + ->inject('register') + ->inject('pools') + ->inject('cache') + ->inject('dbForConsole') + ->callback(function (Registry $register, Group $pools, Cache $cache, Database $dbForConsole) { + $this->action($register, $pools, $cache, $dbForConsole); + }); + } + + private function getStats(Database $dbForConsole, Database $dbForProject, Document $project): array + { + $stats = []; + + /** Get Project ID */ + $stats['Project ID'] = $project->getId(); + + /** Get Project Name */ + $stats['Project Name'] = $project->getAttribute('name'); + + /** Get Total Functions */ + $stats['Functions'] = $dbForProject->count('functions', [], APP_LIMIT_COUNT); + + /** Get Total Deployments */ + $stats['Deployments'] = $dbForProject->count('deployments', [], APP_LIMIT_COUNT); + + /** Get Total Members */ + $teamInternalId = $project->getAttribute('teamInternalId', null); + if ($teamInternalId) { + $stats['Members'] = $dbForConsole->count('memberships', [ + Query::equal('teamInternalId', [$teamInternalId]) + ], APP_LIMIT_COUNT); + } else { + $stats['Members'] = 0; + } + + /** Get Domains */ + $stats['Domains'] = $dbForProject->count('domains', [], APP_LIMIT_COUNT); + + /** Get Usage stats */ + $range = '90d'; + $periods = [ + '90d' => [ + 'period' => '1d', + 'limit' => 90, + ], + ]; + + $metrics = array_values($this->usageStats); + Authorization::skip(function () use ($dbForProject, $periods, $range, $metrics, &$stats) { + foreach ($metrics as $metric) { + $limit = $periods[$range]['limit']; + $period = $periods[$range]['period']; + + $requestDocs = $dbForProject->find('stats', [ + Query::equal('period', [$period]), + Query::equal('metric', [$metric]), + Query::limit($limit), + Query::orderDesc('time'), + ]); + + $stats[$metric] = []; + foreach ($requestDocs as $requestDoc) { + $stats[$metric][] = [ + 'value' => $requestDoc->getAttribute('value'), + 'date' => $requestDoc->getAttribute('time'), + ]; + } + + $stats[$metric] = array_reverse($stats[$metric]); + // Calculate aggregate of each metric + $stats[$metric] = array_sum(array_column($stats[$metric], 'value')); + } + }); + + return $stats; + } + + public function action(Registry $register, Group $pools, Cache $cache, Database $dbForConsole): void + { + + Console::title('Cloud Hamster V1'); + Console::success(APP_NAME . ' cloud hamster process v1 has started'); + + $interval = (int) App::getEnv('_APP_HAMSTER_INTERVAL', '30'); // 30 seconds (by default) + + Console::loop(function () use ($register, $pools, $cache, $dbForConsole, $interval) { + + $now = date('d-m-Y H:i:s', time()); + Console::info("[{$now}] Getting Cloud Usage Stats every {$interval} seconds"); + $loopStart = microtime(true); + + /* Initialise new Utopia app */ + $app = new App('UTC'); + $console = $app->getResource('console'); + + /** CSV stuff */ + $this->date = date('Y-m-d'); + $this->path = "{$this->directory}/stats_{$this->date}.csv"; + $csv = Writer::createFromPath($this->path, 'w'); + $csv->insertOne($this->columns); + + /** Database connections */ + $totalProjects = $dbForConsole->count('projects') + 1; + Console::success("Found a total of: {$totalProjects} projects"); + + $projects = [$console]; + $count = 0; + $limit = 30; + $sum = 30; + $offset = 0; + while (!empty($projects)) { + foreach ($projects as $project) { + /** + * Skip user projects with id 'console' + */ + if ($project->getId() === 'console') { + continue; + } + + Console::info("Getting stats for {$project->getId()}"); + + try { + $db = $project->getAttribute('database'); + $adapter = $pools + ->get($db) + ->pop() + ->getResource(); + + $dbForProject = new Database($adapter, $cache); + $dbForProject->setDefaultDatabase('appwrite'); + $dbForProject->setNamespace('_' . $project->getInternalId()); + + $statsPerProject = $this->getStats($dbForConsole, $dbForProject, $project); + $csv->insertOne(array_values($statsPerProject)); + } catch (\Throwable $th) { + throw $th; + Console::error('Failed to update project ("' . $project->getId() . '") version with error: ' . $th->getMessage()); + } finally { + $pools + ->get($db) + ->reclaim(); + } + } + + $sum = \count($projects); + + $projects = $dbForConsole->find('projects', [ + Query::limit($limit), + Query::offset($offset), + ]); + + $offset = $offset + $limit; + $count = $count + $sum; + + Console::log('Iterated through ' . $count . '/' . $totalProjects . ' projects...'); + } + + $this->sendEmail($register); + + $pools + ->get('console') + ->reclaim(); + + $loopTook = microtime(true) - $loopStart; + $now = date('d-m-Y H:i:s', time()); + Console::info("[{$now}] Cloud Stats took {$loopTook} seconds"); + }, $interval); + } + + private function sendEmail(Registry $register) + { + /** @var \PHPMailer\PHPMailer\PHPMailer $mail */ + $mail = $register->get('smtp'); + + $mail->clearAddresses(); + $mail->clearAllRecipients(); + $mail->clearReplyTos(); + $mail->clearAttachments(); + $mail->clearBCCs(); + $mail->clearCCs(); + + try { + /** Addresses */ + $mail->setFrom(App::getEnv('_APP_SYSTEM_EMAIL_ADDRESS', APP_EMAIL_TEAM), 'Appwrite Cloud Hamster'); + $recipients = explode(',', App::getEnv('_APP_HAMSTER_RECIPIENTS', '')); + foreach ($recipients as $recipient) { + $mail->addAddress($recipient); + } + + /** Attachments */ + $mail->addAttachment($this->path); + + /** Content */ + $mail->Subject = "Cloud Report for {$this->date}"; + $mail->Body = "Please find the daily cloud report atttached"; + + $mail->send(); + Console::success('Email has been sent!'); + } catch (Exception $e) { + Console::error("Message could not be sent. Mailer Error: {$mail->ErrorInfo}"); + } + } +} diff --git a/src/Appwrite/Utopia/Response/Model/Build.php b/src/Appwrite/Utopia/Response/Model/Build.php index 4d7a6cd27c..d80c17645a 100644 --- a/src/Appwrite/Utopia/Response/Model/Build.php +++ b/src/Appwrite/Utopia/Response/Model/Build.php @@ -51,6 +51,12 @@ class Build extends Model 'default' => '', 'example' => self::TYPE_DATETIME_EXAMPLE, ]) + ->addRule('endTime', [ + 'type' => self::TYPE_DATETIME, + 'description' => 'The time the build was finished in ISO 8601 format.', + 'default' => '', + 'example' => self::TYPE_DATETIME_EXAMPLE, + ]) ->addRule('duration', [ 'type' => self::TYPE_INTEGER, 'description' => 'The build duration in seconds.', diff --git a/tests/e2e/Services/Account/AccountConsoleClientTest.php b/tests/e2e/Services/Account/AccountConsoleClientTest.php index 4258004ecf..69bb503428 100644 --- a/tests/e2e/Services/Account/AccountConsoleClientTest.php +++ b/tests/e2e/Services/Account/AccountConsoleClientTest.php @@ -38,7 +38,7 @@ class AccountConsoleClientTest extends Scope ]); $this->assertEquals($response['headers']['status-code'], 401); - $this->assertEquals($response['body']['type'], Exception::USER_CODE_INVALID); + $this->assertEquals($response['body']['type'], Exception::USER_INVALID_CODE); $response = $this->client->call(Client::METHOD_POST, '/account/invite', array_merge([ 'origin' => 'http://localhost', @@ -52,7 +52,7 @@ class AccountConsoleClientTest extends Scope ]); $this->assertEquals($response['headers']['status-code'], 401); - $this->assertEquals($response['body']['type'], Exception::USER_CODE_INVALID); + $this->assertEquals($response['body']['type'], Exception::USER_INVALID_CODE); /** * Test for SUCCESS