diff --git a/CHANGES.md b/CHANGES.md index e680482ce1..0fd4bc1bca 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -2,15 +2,23 @@ ## Features -- New route in Locale API to fetch a list of languages -- Added option to force HTTPS connection to the Appwrite server (_APP_OPTIONS_FORCE_HTTPS) +- New route in Locale API to fetch a list of languages (@TorstenDittmann) - Added Google Fonts to Appwrite for offline availability - Added a new route in the Avatars API to get user initials avatar - Added option to delete team from the console - Added option to view team members from the console - Added option to join a user to any team from the console -- Added support for Brotli compression -- UI performance & accessibility improvments +- Added support for Brotli compression (@PedroCisnerosSantana, @Rohitub222) +- New UI micro-interactions and CSS fixes (@AnatoleLucet) +- UI performance & accessibility improvments (#406) +- Updated ClamAV conntainer to version 1.0.9 +- New Doctor CLI to debug the Appwrite server ([#415](https://github.com/appwrite/appwrite/issues/415)) +- All emails are now sent asynchronously for improved performance (@TorstenDittmann) +- Updated grid for OAuth2 providers list in the console +- Upgraded Redis Resque queue library to version 1.3.6 +- Added container names to docker-compose.yml (@drandell) +- Upgraded ClamAV container image to version 1.0.9 +- Optimised function execution by using fully-qualified function calls ## Bug Fixes @@ -20,10 +28,18 @@ - Fixed a UI bug preventing float values in numeric fields - Fixed scroll positioning when moving rules order up & down - Fixed missing validation for database documents key length (32 chars) +- Grammer fix for pt-br email templates (@rubensdemelo) +- Fixed update form labels and tooltips for Flutter Android apps +- Fixed missing custom scopes param for OAuth2 session create API route +- Fixed wrong JSON validation when creating and updating database documnets +- Fixed bug where max file size was limited to max of 10MB +- Fixed bug preventing the deletion of the project logo ## Security - Access to Health API now requires authentication with an API Key with access to `health.read` scope allowed +- Added option to force HTTPS connection to the Appwrite server (_APP_OPTIONS_FORCE_HTTPS) +- Now using your `_APP_SYSTEM_EMAIL_ADDRESS` as the email address for issuing and renewing SSL certificates # Version 0.6.2 (PRE-RELEASE) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 39d9a574c2..65e3446335 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -159,6 +159,26 @@ To run tests manually, run phpunit from your command line: docker exec appwrite test ``` +## Code Maintenance + +We use some automation tools to help us keep a healthy code base. + +Improve PHP exeution time by using [fully-qualified function calls](https://veewee.github.io/blog/optimizing-php-performance-by-fq-function-calls/): + +```bash +php-cs-fixer fix src/ --rules=native_function_invocation --allow-risky=yes +``` + +Coding Standards: + +```bash +php-cs-fixer fix app/controllers --rules='{"braces": {"allow_single_line_closure": true}}' +``` + +```bash +php-cs-fixer fix src --rules='{"braces": {"allow_single_line_closure": true}}' +``` + ## Tutorials From time to time, our team will add tutorials that will help contributors find their way in the Appwrite source code. Below is a list of currently available tutorials: diff --git a/Dockerfile b/Dockerfile index e4ffd73eb9..867812cbbd 100644 --- a/Dockerfile +++ b/Dockerfile @@ -139,13 +139,12 @@ RUN \ # Set Upload Limit (default to 100MB) RUN echo "upload_max_filesize = ${_APP_STORAGE_LIMIT}" >> /etc/php/$PHP_VERSION/fpm/conf.d/appwrite.ini RUN echo "post_max_size = ${_APP_STORAGE_LIMIT}" >> /etc/php/$PHP_VERSION/fpm/conf.d/appwrite.ini -RUN echo "env[TESTME] = your-secret-key" >> /etc/php/$PHP_VERSION/fpm/conf.d/appwrite.ini # Add logs file RUN echo "" >> /var/log/appwrite.log # Nginx Configuration (with self-signed ssl certificates) -COPY ./docker/nginx.conf /etc/nginx/nginx.conf +COPY ./docker/nginx.conf.template /etc/nginx/nginx.conf.template COPY ./docker/ssl/cert.pem /etc/nginx/ssl/cert.pem COPY ./docker/ssl/key.pem /etc/nginx/ssl/key.pem @@ -175,6 +174,7 @@ COPY ./docker/supervisord.conf /etc/supervisord.conf # Executables RUN chmod +x /usr/local/bin/start +RUN chmod +x /usr/local/bin/doctor RUN chmod +x /usr/local/bin/migrate RUN chmod +x /usr/local/bin/test diff --git a/README.md b/README.md index bc78047f0f..d0900ade4f 100644 --- a/README.md +++ b/README.md @@ -8,7 +8,7 @@

-[![Discord](https://badgen.net/badge/discord/chat/green)](https://discord.gg/GSeTUeA) +[![Discord](https://img.shields.io/discord/564160730845151244?label=discord)](https://discord.gg/GSeTUeA) [![Docker Pulls](https://badgen.net/docker/pulls/appwrite/appwrite)](https://travis-ci.org/appwrite/appwrite) [![Travis CI](https://badgen.net/travis/appwrite/appwrite?label=build)](https://travis-ci.org/appwrite/appwrite) [![Twitter Account](https://badgen.net/twitter/follow/appwrite_io?label=twitter)](https://twitter.com/appwrite_io) diff --git a/app/app.php b/app/app.php index edd0a79921..e62bddd807 100644 --- a/app/app.php +++ b/app/app.php @@ -1,9 +1,8 @@ getAttribute('platforms', []), function ($node) { + }, \array_filter($console->getAttribute('platforms', []), function ($node) { if (isset($node['type']) && $node['type'] === 'web' && isset($node['hostname']) && !empty($node['hostname'])) { return true; } @@ -44,9 +41,9 @@ $clientsConsole = array_map(function ($node) { return false; })); -$clients = array_unique(array_merge($clientsConsole, array_map(function ($node) { +$clients = \array_unique(\array_merge($clientsConsole, \array_map(function ($node) { return $node['hostname']; - }, array_filter($project->getAttribute('platforms', []), function ($node) { + }, \array_filter($project->getAttribute('platforms', []), function ($node) { if (isset($node['type']) && $node['type'] === 'web' && isset($node['hostname']) && !empty($node['hostname'])) { return true; } @@ -54,7 +51,7 @@ $clients = array_unique(array_merge($clientsConsole, array_map(function ($node) return false; })))); -$utopia->init(function () use ($utopia, $request, $response, &$user, $project, $console, $roles, $webhook, $audit, $usage, $clients) { +$utopia->init(function () use ($utopia, $request, $response, &$user, $project, $console, $webhook, $mail, $audit, $usage, $clients) { $route = $utopia->match($request); @@ -63,11 +60,11 @@ $utopia->init(function () use ($utopia, $request, $response, &$user, $project, $ } $referrer = $request->getServer('HTTP_REFERER', ''); - $origin = parse_url($request->getServer('HTTP_ORIGIN', $referrer), PHP_URL_HOST); - $protocol = parse_url($request->getServer('HTTP_ORIGIN', $referrer), PHP_URL_SCHEME); - $port = parse_url($request->getServer('HTTP_ORIGIN', $referrer), PHP_URL_PORT); + $origin = \parse_url($request->getServer('HTTP_ORIGIN', $referrer), PHP_URL_HOST); + $protocol = \parse_url($request->getServer('HTTP_ORIGIN', $referrer), PHP_URL_SCHEME); + $port = \parse_url($request->getServer('HTTP_ORIGIN', $referrer), PHP_URL_PORT); - $refDomain = $protocol.'://'.((in_array($origin, $clients)) + $refDomain = $protocol.'://'.((\in_array($origin, $clients)) ? $origin : 'localhost') . (!empty($port) ? ':'.$port : ''); $selfDomain = new Domain(Config::getParam('hostname')); @@ -83,7 +80,7 @@ $utopia->init(function () use ($utopia, $request, $response, &$user, $project, $ * As recommended at: * @see https://www.owasp.org/index.php/List_of_useful_HTTP_headers */ - if ($request->getServer('_APP_OPTIONS_FORCE_HTTPS', 'disabled') === 'enabled') { // Force HTTPS + if ($utopia->getEnv('_APP_OPTIONS_FORCE_HTTPS', 'disabled') === 'enabled') { // Force HTTPS if(Config::getParam('protocol') !== 'https') { return $response->redirect('https://' . Config::getParam('domain').$request->getServer('REQUEST_URI')); } @@ -93,7 +90,7 @@ $utopia->init(function () use ($utopia, $request, $response, &$user, $project, $ $response ->addHeader('Server', 'Appwrite') - ->addHeader('X-XSS-Protection', '1; mode=block; report=/v1/xss?url='.urlencode($request->getServer('REQUEST_URI'))) + ->addHeader('X-XSS-Protection', '1; mode=block; report=/v1/xss?url='.\urlencode($request->getServer('REQUEST_URI'))) //->addHeader('X-Frame-Options', ($refDomain == 'http://localhost') ? 'SAMEORIGIN' : 'ALLOW-FROM ' . $refDomain) ->addHeader('X-Content-Type-Options', 'nosniff') ->addHeader('Access-Control-Allow-Methods', 'GET, POST, PUT, PATCH, DELETE') @@ -109,10 +106,10 @@ $utopia->init(function () use ($utopia, $request, $response, &$user, $project, $ * Skip this check for non-web platforms which are not requiredto send an origin header */ $origin = $request->getServer('HTTP_ORIGIN', $request->getServer('HTTP_REFERER', '')); - $originValidator = new Origin(array_merge($project->getAttribute('platforms', []), $console->getAttribute('platforms', []))); + $originValidator = new Origin(\array_merge($project->getAttribute('platforms', []), $console->getAttribute('platforms', []))); if(!$originValidator->isValid($origin) - && in_array($request->getMethod(), [Request::METHOD_POST, Request::METHOD_PUT, Request::METHOD_PATCH, Request::METHOD_DELETE]) + && \in_array($request->getMethod(), [Request::METHOD_POST, Request::METHOD_PUT, Request::METHOD_PATCH, Request::METHOD_DELETE]) && $route->getLabel('origin', false) !== '*' && empty($request->getHeader('X-Appwrite-Key', ''))) { throw new Exception($originValidator->getDescription(), 403); @@ -142,6 +139,7 @@ $utopia->init(function () use ($utopia, $request, $response, &$user, $project, $ } } + $roles = Config::getParam('roles', []); $scope = $route->getLabel('scope', 'none'); // Allowed scope for chosen route $scopes = $roles[$role]['scopes']; // Allowed scopes for user role @@ -162,7 +160,7 @@ $utopia->init(function () use ($utopia, $request, $response, &$user, $project, $ ]); $role = Auth::USER_ROLE_APP; - $scopes = array_merge($roles[$role]['scopes'], $key->getAttribute('scopes', [])); + $scopes = \array_merge($roles[$role]['scopes'], $key->getAttribute('scopes', [])); Authorization::setDefaultStatus(false); // Cancel security segmentation for API keys. } @@ -170,7 +168,7 @@ $utopia->init(function () use ($utopia, $request, $response, &$user, $project, $ Authorization::setRole('user:'.$user->getId()); Authorization::setRole('role:'.$role); - array_map(function ($node) { + \array_map(function ($node) { if (isset($node['teamId']) && isset($node['roles'])) { Authorization::setRole('team:'.$node['teamId']); @@ -182,12 +180,12 @@ $utopia->init(function () use ($utopia, $request, $response, &$user, $project, $ // TDOO Check if user is god - if (!in_array($scope, $scopes)) { + if (!\in_array($scope, $scopes)) { if (empty($project->getId()) || Database::SYSTEM_COLLECTION_PROJECTS !== $project->getCollection()) { // Check if permission is denied because project is missing throw new Exception('Project not found', 404); } - throw new Exception($user->getAttribute('email', 'Guest').' (role: '.strtolower($roles[$role]['label']).') missing scope ('.$scope.')', 401); + throw new Exception($user->getAttribute('email', 'User').' (role: '.\strtolower($roles[$role]['label']).') missing scope ('.$scope.')', 401); } if (Auth::USER_STATUS_BLOCKED == $user->getAttribute('status')) { // Account has not been activated @@ -293,7 +291,7 @@ $utopia->error(function ($error /* @var $error Exception */) use ($request, $res $_SERVER = []; // Reset before reporting to error log to avoid keys being compromised - $output = ((App::ENV_TYPE_DEVELOPMENT == $env)) ? [ + $output = ((App::MODE_TYPE_DEVELOPMENT == $env)) ? [ 'message' => $error->getMessage(), 'code' => $error->getCode(), 'file' => $error->getFile(), @@ -397,9 +395,9 @@ $utopia->get('/.well-known/acme-challenge') ->label('docs', false) ->action( function () use ($request, $response) { - $base = realpath(APP_STORAGE_CERTIFICATES); - $path = str_replace('/.well-known/acme-challenge/', '', $request->getParam('q')); - $absolute = realpath($base.'/.well-known/acme-challenge/'.$path); + $base = \realpath(APP_STORAGE_CERTIFICATES); + $path = \str_replace('/.well-known/acme-challenge/', '', $request->getParam('q')); + $absolute = \realpath($base.'/.well-known/acme-challenge/'.$path); if(!$base) { throw new Exception('Storage error', 500); @@ -409,15 +407,15 @@ $utopia->get('/.well-known/acme-challenge') throw new Exception('Unknown path', 404); } - if(!substr($absolute, 0, strlen($base)) === $base) { + if(!\substr($absolute, 0, \strlen($base)) === $base) { throw new Exception('Invalid path', 401); } - if(!file_exists($absolute)) { + if(!\file_exists($absolute)) { throw new Exception('Unknown path', 404); } - $content = @file_get_contents($absolute); + $content = @\file_get_contents($absolute); if(!$content) { throw new Exception('Failed to get contents', 500); @@ -427,14 +425,11 @@ $utopia->get('/.well-known/acme-challenge') } ); -$name = APP_NAME; +include_once __DIR__ . '/controllers/shared/api.php'; +include_once __DIR__ . '/controllers/shared/web.php'; -if (array_key_exists($service, $services)) { /** @noinspection PhpIncludeInspection */ - include_once $services[$service]['controller']; - $name = APP_NAME.' '.ucfirst($services[$service]['name']); -} else { - /** @noinspection PhpIncludeInspection */ - include_once $services['/']['controller']; +foreach(Config::getParam('services', []) as $service) { + include_once $service['controller']; } $utopia->run($request, $response); \ No newline at end of file diff --git a/app/config/collections.php b/app/config/collections.php index 9b708ae8e1..0870919870 100644 --- a/app/config/collections.php +++ b/app/config/collections.php @@ -41,7 +41,7 @@ $collections = [ '$collection' => Database::SYSTEM_COLLECTION_PLATFORMS, 'name' => 'Current Host', 'type' => 'web', - 'hostname' => parse_url('https://'.$request->getServer('HTTP_HOST'), PHP_URL_HOST), + 'hostname' => \parse_url('https://'.$request->getServer('HTTP_HOST'), PHP_URL_HOST), ], ], 'legalName' => '', @@ -50,9 +50,9 @@ $collections = [ 'legalCity' => '', 'legalAddress' => '', 'legalTaxId' => '', - 'authWhitelistEmails' => (!empty($request->getServer('_APP_CONSOLE_WHITELIST_EMAILS', null))) ? explode(',', $request->getServer('_APP_CONSOLE_WHITELIST_EMAILS', null)) : [], - 'authWhitelistIPs' => (!empty($request->getServer('_APP_CONSOLE_WHITELIST_IPS', null))) ? explode(',', $request->getServer('_APP_CONSOLE_WHITELIST_IPS', null)) : [], - 'authWhitelistDomains' => (!empty($request->getServer('_APP_CONSOLE_WHITELIST_DOMAINS', null))) ? explode(',', $request->getServer('_APP_CONSOLE_WHITELIST_DOMAINS', null)) : [], + 'authWhitelistEmails' => (!empty($request->getServer('_APP_CONSOLE_WHITELIST_EMAILS', null))) ? \explode(',', $request->getServer('_APP_CONSOLE_WHITELIST_EMAILS', null)) : [], + 'authWhitelistIPs' => (!empty($request->getServer('_APP_CONSOLE_WHITELIST_IPS', null))) ? \explode(',', $request->getServer('_APP_CONSOLE_WHITELIST_IPS', null)) : [], + 'authWhitelistDomains' => (!empty($request->getServer('_APP_CONSOLE_WHITELIST_DOMAINS', null))) ? \explode(',', $request->getServer('_APP_CONSOLE_WHITELIST_DOMAINS', null)) : [], ], Database::SYSTEM_COLLECTION_COLLECTIONS => [ '$collection' => Database::SYSTEM_COLLECTION_COLLECTIONS, @@ -1434,8 +1434,8 @@ foreach ($providers as $index => $provider) { $collections[Database::SYSTEM_COLLECTION_PROJECTS]['rules'][] = [ '$collection' => Database::SYSTEM_COLLECTION_RULES, - 'label' => 'OAuth2 '.ucfirst($index).' ID', - 'key' => 'usersOauth2'.ucfirst($index).'Appid', + 'label' => 'OAuth2 '.\ucfirst($index).' ID', + 'key' => 'usersOauth2'.\ucfirst($index).'Appid', 'type' => Database::SYSTEM_VAR_TYPE_TEXT, 'default' => '', 'required' => false, @@ -1444,8 +1444,8 @@ foreach ($providers as $index => $provider) { $collections[Database::SYSTEM_COLLECTION_PROJECTS]['rules'][] = [ '$collection' => Database::SYSTEM_COLLECTION_RULES, - 'label' => 'OAuth2 '.ucfirst($index).' Secret', - 'key' => 'usersOauth2'.ucfirst($index).'Secret', + 'label' => 'OAuth2 '.\ucfirst($index).' Secret', + 'key' => 'usersOauth2'.\ucfirst($index).'Secret', 'type' => Database::SYSTEM_VAR_TYPE_TEXT, 'default' => '', 'required' => false, @@ -1454,8 +1454,8 @@ foreach ($providers as $index => $provider) { $collections[Database::SYSTEM_COLLECTION_USERS]['rules'][] = [ '$collection' => Database::SYSTEM_COLLECTION_RULES, - 'label' => 'OAuth2 '.ucfirst($index).' ID', - 'key' => 'oauth2'.ucfirst($index), + 'label' => 'OAuth2 '.\ucfirst($index).' ID', + 'key' => 'oauth2'.\ucfirst($index), 'type' => Database::SYSTEM_VAR_TYPE_TEXT, 'default' => '', 'required' => false, @@ -1464,8 +1464,8 @@ foreach ($providers as $index => $provider) { $collections[Database::SYSTEM_COLLECTION_USERS]['rules'][] = [ '$collection' => Database::SYSTEM_COLLECTION_RULES, - 'label' => 'OAuth2 '.ucfirst($index).' Access Token', - 'key' => 'oauth2'.ucfirst($index).'AccessToken', + 'label' => 'OAuth2 '.\ucfirst($index).' Access Token', + 'key' => 'oauth2'.\ucfirst($index).'AccessToken', 'type' => Database::SYSTEM_VAR_TYPE_TEXT, 'default' => '', 'required' => false, diff --git a/app/config/eu.php b/app/config/eu.php index d63ebef707..fbefd23694 100644 --- a/app/config/eu.php +++ b/app/config/eu.php @@ -30,7 +30,7 @@ $list = [ 'SE', // Sweden ]; -if (time() < strtotime('2020-01-31')) { // @see https://en.wikipedia.org/wiki/Brexit +if (\time() < \strtotime('2020-01-31')) { // @see https://en.wikipedia.org/wiki/Brexit $list[] = 'GB'; // // United Kingdom } diff --git a/app/config/locales/templates/_base.tpl b/app/config/locales/templates/_base.tpl new file mode 100644 index 0000000000..561ce73855 --- /dev/null +++ b/app/config/locales/templates/_base.tpl @@ -0,0 +1,238 @@ + + + + + + + {{title}} + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/config/locales/templates/_cta.tpl b/app/config/locales/templates/_cta.tpl new file mode 100644 index 0000000000..634609a09e --- /dev/null +++ b/app/config/locales/templates/_cta.tpl @@ -0,0 +1,26 @@ + + + + + + + +

+ + + {{redirect}} + + +

\ No newline at end of file diff --git a/app/config/locales/templates/af.email.auth.confirm.tpl b/app/config/locales/templates/af.email.auth.confirm.tpl index e0a6c40356..a9dbb2af46 100644 --- a/app/config/locales/templates/af.email.auth.confirm.tpl +++ b/app/config/locales/templates/af.email.auth.confirm.tpl @@ -1,24 +1,15 @@ - - -
+

Hallo {{name}}, -
-
+

+

Om jou epos adres te verifieer, kliek op die web adres hier: -
- {{redirect}} -
-
+

+{{cta}} +

As jy nie aangevra het om jou epos adres te verifieer nie, kan jy die boodskap ignoreer. -
-
+

+

Baie dankie,
{{project}} span -

\ No newline at end of file +

\ No newline at end of file diff --git a/app/config/locales/templates/af.email.auth.invitation.tpl b/app/config/locales/templates/af.email.auth.invitation.tpl index eed6cf69d1..0c1c72c13a 100644 --- a/app/config/locales/templates/af.email.auth.invitation.tpl +++ b/app/config/locales/templates/af.email.auth.invitation.tpl @@ -1,27 +1,18 @@ - - -
+

Hallo, -
-
+

+

Hierdie epos is vir jou gestuur omdat {{owner}} jou graag wou nooi om 'n spanlid te word van {{team}} by {{project}}. -
-
+

+

Om by die {{team}} span aan te sluit: -
- {{redirect}} -
-
+

+{{cta}} +

As jy nie belangstel nie, kan jy die boodskap ignoreer. -
-
+

+

Baie dankie,
{{project}} span -

+

diff --git a/app/config/locales/templates/af.email.auth.recovery.tpl b/app/config/locales/templates/af.email.auth.recovery.tpl index 598c4c1d96..925baaf290 100644 --- a/app/config/locales/templates/af.email.auth.recovery.tpl +++ b/app/config/locales/templates/af.email.auth.recovery.tpl @@ -1,24 +1,15 @@ - - -
+

Hallo {{name}}, -
-
+

+

Volg hierdie web adres om jou wagwoord te verander vir {{project}}. -
- {{redirect}} -
-
+

+{{cta}} +

As jy nie gevra het om you wagwoord te verander nie, kan jy hierdie boodskap ignoreer. -
-
+

+

Baie dankie,
{{project}} span -

+

diff --git a/app/config/locales/templates/alb.email.auth.confirm.tpl b/app/config/locales/templates/alb.email.auth.confirm.tpl index 549340eca9..f350c13fa9 100644 --- a/app/config/locales/templates/alb.email.auth.confirm.tpl +++ b/app/config/locales/templates/alb.email.auth.confirm.tpl @@ -1,24 +1,15 @@ - - -
+

Hej {{name}}, -
-
+

+

Ndiqni këtë lidhje për të konfirmuar adresën tuaj të emailit. -
- {{redirect}} -
-
+

+{{cta}} +

Nëse nuk keni kërkuar të konfirmoni këtë adresë, mund ta injoroni këtë mesazh. -
-
+

+

Faleminderit,
Ekipi i tij {{project}} -

+

diff --git a/app/config/locales/templates/alb.email.auth.invitation.tpl b/app/config/locales/templates/alb.email.auth.invitation.tpl index fc75e51825..b734162067 100644 --- a/app/config/locales/templates/alb.email.auth.invitation.tpl +++ b/app/config/locales/templates/alb.email.auth.invitation.tpl @@ -1,27 +1,18 @@ - - -
+

Ηej, -
-
+

+

Ju e morët këtë email sepse {{owner}} ju ftoi të bashkoheni në ekip {{team}} në {{project}}. -
-
+

+

Ndiqni këtë lidhje për t'u bashkuar me grupin {{team}}: -
- {{redirect}} -
-
+

+{{cta}} +

Nëse nuk jeni të interesuar, mund ta injoroni këtë mesazh. -
-
+

+

Faleminderit,
Ekipi i tij {{project}} -

+

diff --git a/app/config/locales/templates/alb.email.auth.recovery.tpl b/app/config/locales/templates/alb.email.auth.recovery.tpl index d8e0bf867c..044cb963ae 100644 --- a/app/config/locales/templates/alb.email.auth.recovery.tpl +++ b/app/config/locales/templates/alb.email.auth.recovery.tpl @@ -1,24 +1,15 @@ - - -
+

Hej {{name}}, -
-
+

+

Ndiqni këtë lidhje për të rivendosur fjalëkalimin tuaj ajo {{project}}. -
- {{redirect}} -
-
+

+{{cta}} +

Nëse nuk keni kërkuar një ndryshim të fjalëkalimit, mund ta injoroni këtë mesazh. -
-
+

+

Faleminderit,
Ekipi i tij {{project}} -

+

diff --git a/app/config/locales/templates/ar.email.auth.confirm.tpl b/app/config/locales/templates/ar.email.auth.confirm.tpl index 8cd1929dd9..a924e75be8 100644 --- a/app/config/locales/templates/ar.email.auth.confirm.tpl +++ b/app/config/locales/templates/ar.email.auth.confirm.tpl @@ -1,24 +1,15 @@ - - -
+

مرحبا {{name}}, -
-
+

+

اتبع هذا الرابط للتحقق من عنوان بريدك الإلكتروني. -
- {{redirect}} -
-
+

+{{cta}} +

إذا لم تطلب التحقق من هذا العنوان، فيمكنك تجاهل هذه الرسالة. -
-
+

+

Thanks,
فريق {{project}} -

\ No newline at end of file +

\ No newline at end of file diff --git a/app/config/locales/templates/ar.email.auth.invitation.tpl b/app/config/locales/templates/ar.email.auth.invitation.tpl index fba4a731e3..78a1321dab 100644 --- a/app/config/locales/templates/ar.email.auth.invitation.tpl +++ b/app/config/locales/templates/ar.email.auth.invitation.tpl @@ -1,27 +1,18 @@ - - -
+

مرحبا، -
-
+

+

تم إرسال هذه الرسالة إليك لأن {{owner}} أراد دعوتك لتصبح عضوًا في فريق {{team}} في {{project}}. -
-
+

+

اتبع هذا الرابط للانضمام إلى فريق {{team}}: -
- {{redirect}} -
-
+

+{{cta}} +

إذا لم تكن مهتمًا، يمكنك تجاهل هذه الرسالة. -
-
+

+

شكرا،
فريق {{project}} -

+

diff --git a/app/config/locales/templates/ar.email.auth.recovery.tpl b/app/config/locales/templates/ar.email.auth.recovery.tpl index 06ca47f35e..53c4537e3c 100644 --- a/app/config/locales/templates/ar.email.auth.recovery.tpl +++ b/app/config/locales/templates/ar.email.auth.recovery.tpl @@ -1,24 +1,15 @@ - - -
+

مرحبا {{name}}، -
-
+

+

اتبع هذا الرابط لإعادة تعيين كلمة مرور {{project}}. -
- {{redirect}} -
-
+

+{{cta}} +

إذا لم تطلب إعادة تعيين كلمة المرور الخاصة بك، فيمكنك تجاهل هذه الرسالة. -
-
+

+

شكرا،
فريق {{project}} -

+

diff --git a/app/config/locales/templates/bn.email.auth.confirm.tpl b/app/config/locales/templates/bn.email.auth.confirm.tpl index dabf4f1222..12be7cecdb 100644 --- a/app/config/locales/templates/bn.email.auth.confirm.tpl +++ b/app/config/locales/templates/bn.email.auth.confirm.tpl @@ -1,24 +1,15 @@ - - -
+

হ্যালো {{name}}, -
-
+

+

এই লিংকটি অনুসরণ করুন আপনার ইমেইল এড্রেস যাচাই করতে | -
- {{redirect}} -
-
+

+{{cta}} +

আপনি যদি আপনার ইমেইল এড্রেস যাচাই করতে অনুরোধ করেননি, আপনি এই মেসেজটি অগ্রাহ্য করতে পারেন | -
-
+

+

ধন্যবাদান্তে,
{{project}} টীম -

+

diff --git a/app/config/locales/templates/bn.email.auth.invitation.tpl b/app/config/locales/templates/bn.email.auth.invitation.tpl index f09c23adf9..b0e1395a5f 100644 --- a/app/config/locales/templates/bn.email.auth.invitation.tpl +++ b/app/config/locales/templates/bn.email.auth.invitation.tpl @@ -1,27 +1,18 @@ - - -
+

Hello, -
-
+

+

এই মেইলটি আপনাকে পাঠানো হয়েছে কারণ {{owner}} আমন্ত্রণ করেছেন আপনাকে {{team}} এই টীমর মেম্বার হতে যেটি {{project}} এই প্রজেক্টর অন্তর্গত | -
-
+

+

এই লিংকটি অনুসরণ করুন {{team}} এই টীম-এ যোগ দিতে: -
- {{redirect}} -
-
+

+{{cta}} +

আপনি যদি অনাগ্রহী হন, এই মেসেজটি অগ্রাহ্য করতে পারেন | -
-
+

+

ধন্যবাদান্তে,
{{project}} টীম -

+

diff --git a/app/config/locales/templates/bn.email.auth.recovery.tpl b/app/config/locales/templates/bn.email.auth.recovery.tpl index 4a356d9889..0dbe9bc643 100644 --- a/app/config/locales/templates/bn.email.auth.recovery.tpl +++ b/app/config/locales/templates/bn.email.auth.recovery.tpl @@ -1,24 +1,15 @@ - - -
+

হ্যালো {{name}}, -
-
+

+

এই লিংকটি অনুসরণ করুন আপনার {{project}} পাসওয়ার্ড রিসেট করতে | -
- {{redirect}} -
-
+

+{{cta}} +

আপনি যদি পাসওয়ার্ড রিসেট করতে অনুরোধ করেননি, আপনি এই মেসেজটি অগ্রাহ্য করতে পারেন | -
-
+

+

ধন্যবাদান্তে,
{{project}} টীম -

+

diff --git a/app/config/locales/templates/cat.email.auth.confirm.tpl b/app/config/locales/templates/cat.email.auth.confirm.tpl index 3761912010..c6987289eb 100644 --- a/app/config/locales/templates/cat.email.auth.confirm.tpl +++ b/app/config/locales/templates/cat.email.auth.confirm.tpl @@ -1,24 +1,15 @@ - - -
+

Hola {{name}}, -
-
+

+

Segueix aquest enllaç per verificar la teva direcció de correu: -
- {{redirect}} -
-
+

+{{cta}} +

Si no has solicitat verificar aquesta direcció, pots ignorar aquest missatge. -
-
+

+

Gràcies,
Equip {{project}} -

+

diff --git a/app/config/locales/templates/cat.email.auth.invitation.tpl b/app/config/locales/templates/cat.email.auth.invitation.tpl index 69687693a2..1a8b062531 100644 --- a/app/config/locales/templates/cat.email.auth.invitation.tpl +++ b/app/config/locales/templates/cat.email.auth.invitation.tpl @@ -1,28 +1,18 @@ - - -
+

Hola, -
-
- T'hem enviat aquest correu perquè {{owner}} et vol convidar a formar part - de l'equip {{team}} a {{project}}. -
-
+

+

+ T'hem enviat aquest correu perquè {{owner}} et vol convidar a formar part de l'equip {{team}} a {{project}}. +

+

Segueix aquest enllaç per unir-te a l'equip {{team}}: -
- {{redirect}} -
-
+

+{{cta}} +

Si no estàs interessat, pots ignorar aquest missatge. -
-
+

+

Gràcies,
Equip {{project}} -

+

diff --git a/app/config/locales/templates/cat.email.auth.recovery.tpl b/app/config/locales/templates/cat.email.auth.recovery.tpl index 2c17989674..856b92c457 100644 --- a/app/config/locales/templates/cat.email.auth.recovery.tpl +++ b/app/config/locales/templates/cat.email.auth.recovery.tpl @@ -1,24 +1,15 @@ - - -
+

Hola {{name}}, -
-
+

+

Segueix aquest enllaç per restablir la teva contrasenya de {{project}}. -
- {{redirect}} -
-
+

+{{cta}} +

Si no has demanat restablir la teva contrasenya, pots ignorar aquest missatge. -
-
+

+

Gràcies,
Equip {{project}} -

+

diff --git a/app/config/locales/templates/cz.email.auth.confirm.tpl b/app/config/locales/templates/cz.email.auth.confirm.tpl index 481b440ab9..e72b357a48 100644 --- a/app/config/locales/templates/cz.email.auth.confirm.tpl +++ b/app/config/locales/templates/cz.email.auth.confirm.tpl @@ -1,24 +1,15 @@ - - -
+

Ahoj {{name}}, -
-
+

+

Kliknutím na tento odkaz ověřte svou e-mailovou adresu. -
- {{redirect}} -
-
+

+{{cta}} +

Pokud jste nepožádali o ověření této adresy, můžete tuto zprávu ignorovat. -
-
+

+

dík,
{{project}} tým -

+

diff --git a/app/config/locales/templates/cz.email.auth.invitation.tpl b/app/config/locales/templates/cz.email.auth.invitation.tpl index 8e8a8dc5ce..d5353e71cf 100644 --- a/app/config/locales/templates/cz.email.auth.invitation.tpl +++ b/app/config/locales/templates/cz.email.auth.invitation.tpl @@ -1,27 +1,18 @@ - - -
+

Ahoj, -
-
+

+

Tento e-mail vám byl zaslán, protože vás {{owner}} chtěl pozvat, abyste se stali členem týmu v týmu {{team}} v {{project}}. -
-
+

+

Klepnutím na tento odkaz se připojíte k týmu {{team}}: -
- {{redirect}} -
-
+

+{{cta}} +

Pokud vás nezajímá, můžete tuto zprávu ignorovat. -
-
+

+

Dík,
{{project}} tým -

+

diff --git a/app/config/locales/templates/cz.email.auth.recovery.tpl b/app/config/locales/templates/cz.email.auth.recovery.tpl index d8a3e6a2b1..04b947d7ad 100644 --- a/app/config/locales/templates/cz.email.auth.recovery.tpl +++ b/app/config/locales/templates/cz.email.auth.recovery.tpl @@ -1,24 +1,15 @@ - - -
+

Ahoj {{name}}, -
-
+

+

Pomocí tohoto odkazu obnovte své heslo {{project}}. -
- {{redirect}} -
-
+

+{{cta}} +

Pokud jste nepožádali o resetování hesla, můžete tuto zprávu ignorovat. -
-
+

+

Dík,
{{project}} tým -

+

diff --git a/app/config/locales/templates/de.email.auth.confirm.tpl b/app/config/locales/templates/de.email.auth.confirm.tpl index 2a3211bbab..2780869778 100644 --- a/app/config/locales/templates/de.email.auth.confirm.tpl +++ b/app/config/locales/templates/de.email.auth.confirm.tpl @@ -1,24 +1,15 @@ - - -
+

Hallo {{name}}, -
-
+

+

bitte folge diesem Link um deine E-Mail Adresse zu verifizieren. -
- {{redirect}} -
-
+

+{{cta}} +

Bitte ignoriere diese Nachricht, wenn du das Verifizieren deiner E-Mail Adresse nicht beantragt hast. -
-
+

+

Vielen Dank,
{{project}} Team -

\ No newline at end of file +

\ No newline at end of file diff --git a/app/config/locales/templates/de.email.auth.invitation.tpl b/app/config/locales/templates/de.email.auth.invitation.tpl index c6d9162730..ae55630797 100644 --- a/app/config/locales/templates/de.email.auth.invitation.tpl +++ b/app/config/locales/templates/de.email.auth.invitation.tpl @@ -1,27 +1,18 @@ - - -
+

Hallo, -
-
+

+

diese E-Mail wurde dir geschickt, weil {{owner}} dich eingeladen hat Teammitglied im Team {{team}} bei {{project}} zu werden. -
-
+

+

Folge diesem Link um dem Team {{team}} beizutreten: -
- {{redirect}} -
-
+

+{{cta}} +

Wenn du daran nicht interessiert bist, kannst du diese Nachricht ignorieren. -
-
+

+

Vielen Dank,
{{project}} Team -

+

diff --git a/app/config/locales/templates/de.email.auth.recovery.tpl b/app/config/locales/templates/de.email.auth.recovery.tpl index 6d2ccd7bae..8ca8140aec 100644 --- a/app/config/locales/templates/de.email.auth.recovery.tpl +++ b/app/config/locales/templates/de.email.auth.recovery.tpl @@ -1,24 +1,15 @@ - - -
+

Hallo {{name}}, -
-
+

+

Folge diesem Link um dein Passwort für {{project}} zurückzusetzen. -
- {{redirect}} -
-
+

+{{cta}} +

Bitte ignoriere diese Nachricht, wenn du das Zurücksetzen deines Passworts nicht beantragt hast. -
-
+

+

Vielen Dank,
{{project}} Team -

+

diff --git a/app/config/locales/templates/en.email.auth.confirm.tpl b/app/config/locales/templates/en.email.auth.confirm.tpl index 92078114e3..799832da56 100644 --- a/app/config/locales/templates/en.email.auth.confirm.tpl +++ b/app/config/locales/templates/en.email.auth.confirm.tpl @@ -1,24 +1,15 @@ - - -
+

Hello {{name}}, -
-
+

+

Follow this link to verify your email address. -
- {{redirect}} -
-
+

+{{cta}} +

If you didn’t ask to verify this address, you can ignore this message. -
-
+

+

Thanks,
{{project}} team -

\ No newline at end of file +

\ No newline at end of file diff --git a/app/config/locales/templates/en.email.auth.invitation.tpl b/app/config/locales/templates/en.email.auth.invitation.tpl index fda2bbcce0..70ea922c0c 100644 --- a/app/config/locales/templates/en.email.auth.invitation.tpl +++ b/app/config/locales/templates/en.email.auth.invitation.tpl @@ -1,27 +1,14 @@ - - -
- Hello, -
-
+

+ Hello, +

+

This mail was sent to you because {{owner}} wanted to invite you to become a team member at the {{team}} team over at {{project}}. -
-
- Follow this link to join the {{team}} team: -
- {{redirect}} -
-
- If you are not interested, you can ignore this message. -
-
+

+{{cta}} +

+ If you are not interested, you can ignore this message.

+

Thanks,
{{project}} team -

+

\ No newline at end of file diff --git a/app/config/locales/templates/en.email.auth.recovery.tpl b/app/config/locales/templates/en.email.auth.recovery.tpl index 82b365546a..a4a982f46f 100644 --- a/app/config/locales/templates/en.email.auth.recovery.tpl +++ b/app/config/locales/templates/en.email.auth.recovery.tpl @@ -1,24 +1,15 @@ - - -
+

Hello {{name}}, -
-
+

+

Follow this link to reset your {{project}} password. -
- {{redirect}} -
-
- If you didn't ask to reset your password, you can ignore this message. -
-
+

+{{cta}} +

+ If you didn’t ask to verify this address, you can ignore this message. +

+

Thanks,
{{project}} team -

+

\ No newline at end of file diff --git a/app/config/locales/templates/es.email.auth.confirm.tpl b/app/config/locales/templates/es.email.auth.confirm.tpl index a5b9235be8..2c728ddce1 100644 --- a/app/config/locales/templates/es.email.auth.confirm.tpl +++ b/app/config/locales/templates/es.email.auth.confirm.tpl @@ -1,24 +1,15 @@ - - -
+

Hola {{name}}, -
-
+

+

Sigue este enlace para verificar tu dirección de correo. -
- {{redirect}} -
-
+

+{{cta}} +

Si no has solicitado verificar esta dirección, puedes ignorar este mensaje. -
-
+

+

Gracias,
Equipo {{project}} -

+

diff --git a/app/config/locales/templates/es.email.auth.invitation.tpl b/app/config/locales/templates/es.email.auth.invitation.tpl index aa0469d9c6..d07b7ffb55 100644 --- a/app/config/locales/templates/es.email.auth.invitation.tpl +++ b/app/config/locales/templates/es.email.auth.invitation.tpl @@ -1,27 +1,18 @@ - - -
+

Hola, -
-
+

+

Te hemos enviamos este correo porque {{owner}} quiere invitarte a formar parte del equipo {{team}} en {{project}}. -
-
+

+

Sigue este enlace para unirte al equipo {{team}}: -
- {{redirect}} -
-
- Si no ests interesado, puedes ignorar este mensaje. -
-
+

+{{cta}} +

+ Si no estás interesado, puedes ignorar este mensaje. +

+

Gracias,
Equipo {{project}} -

+

diff --git a/app/config/locales/templates/es.email.auth.recovery.tpl b/app/config/locales/templates/es.email.auth.recovery.tpl index 6f78fffa7c..0f1cfaa434 100644 --- a/app/config/locales/templates/es.email.auth.recovery.tpl +++ b/app/config/locales/templates/es.email.auth.recovery.tpl @@ -1,24 +1,15 @@ - - -
+

Hola {{name}}, -
-
+

+

Sigue este enlace para reestablecer tu contraseña de {{project}}. -
- {{redirect}} -
-
+

+{{cta}} +

Si no has pedido reestablecer tu contraseña, puedes ignorar este mensaje. -
-
+

+

Gracias,
Equipo {{project}} -

+

diff --git a/app/config/locales/templates/fi.email.auth.confirm.tpl b/app/config/locales/templates/fi.email.auth.confirm.tpl index 1354e7a5d6..d14b0e32b0 100644 --- a/app/config/locales/templates/fi.email.auth.confirm.tpl +++ b/app/config/locales/templates/fi.email.auth.confirm.tpl @@ -1,24 +1,15 @@ - - -
+

Hei {{name}}, -
-
+

+

Varmista sähköpostiosoite tästä linkistä. -
- {{redirect}} -
-
+

+{{cta}} +

Jos et kysynyt tämän sähköpostiosoitteen varmistamista, voit sivuuttaa tämän viestin. -
-
+

+

kiitos,
{{project}} tiimi -

\ No newline at end of file +

\ No newline at end of file diff --git a/app/config/locales/templates/fi.email.auth.invitation.tpl b/app/config/locales/templates/fi.email.auth.invitation.tpl index 7e955d0216..6680259aa7 100644 --- a/app/config/locales/templates/fi.email.auth.invitation.tpl +++ b/app/config/locales/templates/fi.email.auth.invitation.tpl @@ -1,27 +1,18 @@ - - -
+

Hei, -
-
+

+

Sait tämän sähköpostin koska {{owner}} halusi kutsua sinut jäseneksi {{team}} tiimiin, täällä {{project}}. -
-
+

+

Käytä tätä linkkiä liittyäksesi {{team}} tiimiin: -
- {{redirect}} -
-
+

+{{cta}} +

Jos et ole kiinnostunut, voit sivuuttaa tämän viestin. -
-
+

+

kiitos,
{{project}} tiimi -

+

diff --git a/app/config/locales/templates/fi.email.auth.recovery.tpl b/app/config/locales/templates/fi.email.auth.recovery.tpl index b273c65617..34c3177418 100644 --- a/app/config/locales/templates/fi.email.auth.recovery.tpl +++ b/app/config/locales/templates/fi.email.auth.recovery.tpl @@ -1,24 +1,15 @@ - - -
+

Hei {{name}}, -
-
+

+

Resetoi {{project}} salasana tästä linkistä. -
- {{redirect}} -
-
+

+{{cta}} +

Jos et pyytänyt salasanan nollaamista, voit sivuuttaa tämän viestin- -
-
+

+

Kiitos,
{{project}} tiimi -

\ No newline at end of file +

\ No newline at end of file diff --git a/app/config/locales/templates/fo.email.auth.confirm.tpl b/app/config/locales/templates/fo.email.auth.confirm.tpl index 9af52651b8..5400c2ab01 100644 --- a/app/config/locales/templates/fo.email.auth.confirm.tpl +++ b/app/config/locales/templates/fo.email.auth.confirm.tpl @@ -1,24 +1,15 @@ - - -
+

Halló {{name}}, -
-
+

+

Fylg hesa lenka fyrið at verifisera teldupostur -
- {{redirect}} -
-
+

+{{cta}} +

Om tú ikke spurdi om at verifisera hesa teldupostur, kannst tú ignorera hesa boð. -
-
+

+

Takk,
{{project}} lið -

\ No newline at end of file +

\ No newline at end of file diff --git a/app/config/locales/templates/fo.email.auth.invitation.tpl b/app/config/locales/templates/fo.email.auth.invitation.tpl index 586daac0e4..c264c7516f 100644 --- a/app/config/locales/templates/fo.email.auth.invitation.tpl +++ b/app/config/locales/templates/fo.email.auth.invitation.tpl @@ -1,27 +1,18 @@ - - -
+

Halló, -
-
+

+

Hesa teldupost var send til tín tí {{owner}} vildi bjóða tær at vera eitt lið limur hjá {{team}} á {{project}} -
-
+

+

Fylg hettar lenka fyrið at bliva vi {{team}} lið: -
- {{redirect}} -
-
+

+{{cta}} +

Om tú ikke er áhugaður, kannst tú ignorera hesa boð -
-
+

+

Takk,
{{project}} lið -

+

diff --git a/app/config/locales/templates/fo.email.auth.recovery.tpl b/app/config/locales/templates/fo.email.auth.recovery.tpl index 8c916a723f..e2f17313ee 100644 --- a/app/config/locales/templates/fo.email.auth.recovery.tpl +++ b/app/config/locales/templates/fo.email.auth.recovery.tpl @@ -1,24 +1,15 @@ - - -
+

Halló {{name}}, -
-
+

+

Fylg hettar lenka fyrið at resette títt {{project}} passord. -
- {{redirect}} -
-
+

+{{cta}} +

Om tú ikke spurdi om at verifisera hesa telduposturin, kannst du ignorera hesa boð. -
-
+

+

Takk,
{{project}} lið -

+

diff --git a/app/config/locales/templates/fr.email.auth.confirm.tpl b/app/config/locales/templates/fr.email.auth.confirm.tpl index 21621e237a..ad0d242740 100644 --- a/app/config/locales/templates/fr.email.auth.confirm.tpl +++ b/app/config/locales/templates/fr.email.auth.confirm.tpl @@ -1,24 +1,15 @@ - - -
+

Bonjour {{name}}, -
-
+

+

Cliquez sur le lien suivant pour vérifier votre adresse mail. -
- {{redirect}} -
-
+

+{{cta}} +

Si vous n'avez pas demandé une vérification de cette adresse mail, vous pouvez ignorer ce message. -
-
+

+

Merci,
L'équipe {{project}} -

+

diff --git a/app/config/locales/templates/fr.email.auth.invitation.tpl b/app/config/locales/templates/fr.email.auth.invitation.tpl index 3b793bb25c..c2d8f8cd1b 100644 --- a/app/config/locales/templates/fr.email.auth.invitation.tpl +++ b/app/config/locales/templates/fr.email.auth.invitation.tpl @@ -1,27 +1,18 @@ - - -
+

Bonjour, -
-
+

+

Ce mail vous a été envoyé car {{owner}} vous invite à devenir membre de l'équipe {{team}} sur le projet {{project}}. -
-
+

+

Cliquez sur le lien suivant pour rejoindre l'équipe {{team}}: -
- {{redirect}} -
-
+

+{{cta}} +

Si vous n'êtes pas intéressé, vous pouvez ignorer ce message. -
-
+

+

Merci,
L'équipe {{project}} -

+

diff --git a/app/config/locales/templates/fr.email.auth.recovery.tpl b/app/config/locales/templates/fr.email.auth.recovery.tpl index d3ab2a305f..597b421bf3 100644 --- a/app/config/locales/templates/fr.email.auth.recovery.tpl +++ b/app/config/locales/templates/fr.email.auth.recovery.tpl @@ -1,24 +1,15 @@ - - -
+

Bonjour {{name}}, -
-
+

+

Cliquez sur le lien suivant pour réinitialiser votre mot de passe pour le projet {{project}}. -
- {{redirect}} -
-
+

+{{cta}} +

Si vous n'êtes pas à l'origine de cette demande de réinitialisation de mot de passe, vous pouvez ignorer ce message. -
-
+

+

Merci,
L'équipe {{project}} -

+

diff --git a/app/config/locales/templates/gr.email.auth.confirm.tpl b/app/config/locales/templates/gr.email.auth.confirm.tpl index a62445476b..e52209c9db 100644 --- a/app/config/locales/templates/gr.email.auth.confirm.tpl +++ b/app/config/locales/templates/gr.email.auth.confirm.tpl @@ -1,24 +1,15 @@ - - -
+

Γεια σου {{name}}, -
-
+

+

Ακολούθησε αυτό τον σύνδεσμο για να επιβεβαιώσεις τη διεύθυνση email σου. -
- {{redirect}} -
-
+

+{{cta}} +

Αν δεν ζήτησες να επιβεβαιώσεις αυτή τη διεύθυνση, μπορείς να αγνοήσεις αυτό το μήνυμα. -
-
+

+

Ευχαριστούμε,
Η ομάδα του {{project}} -

\ No newline at end of file +

\ No newline at end of file diff --git a/app/config/locales/templates/gr.email.auth.invitation.tpl b/app/config/locales/templates/gr.email.auth.invitation.tpl index 101a1c673c..a7c0743ff6 100644 --- a/app/config/locales/templates/gr.email.auth.invitation.tpl +++ b/app/config/locales/templates/gr.email.auth.invitation.tpl @@ -1,27 +1,18 @@ - - -
+

Γεια, -
-
+

+

Έλαβες αυτό το email επειδή ο {{owner}} σε προσκάλεσε να γίνεις μέλος της ομάδας {{team}} στο {{project}}. -
-
+

+

Ακολούθησε αυτό τον σύνδεσμο για να γίνεις μέλος της ομάδας {{team}}: -
- {{redirect}} -
-
+

+{{cta}} +

Άν δεν ενδιαφέρεσαι, μπορείς να αγνοήσεις αυτό το μήνυμα. -
-
+

+

Ευχαριστούμε,
Η ομάδα του {{project}} -

+

diff --git a/app/config/locales/templates/gr.email.auth.recovery.tpl b/app/config/locales/templates/gr.email.auth.recovery.tpl index cae164df22..fb18c16ef3 100644 --- a/app/config/locales/templates/gr.email.auth.recovery.tpl +++ b/app/config/locales/templates/gr.email.auth.recovery.tpl @@ -1,24 +1,15 @@ - - -
+

Γεια σου {{name}}, -
-
+

+

Ακολούθησε αυτό τον σύνδεσμο για να επαναφέρεις τον κωδικό πρόσβασής σου για το {{project}}. -
- {{redirect}} -
-
+

+{{cta}} +

Άν δεν ζήτησες αλλαγή κωδικού πρόσβασης, μπορείς να αγνοήσεις αυτο το μήνυμα. -
-
+

+

Ευχαριστούμε,
Η ομάδα του {{project}} -

+

diff --git a/app/config/locales/templates/he.email.auth.confirm.tpl b/app/config/locales/templates/he.email.auth.confirm.tpl index bc9189a16f..0c1f2a36d3 100644 --- a/app/config/locales/templates/he.email.auth.confirm.tpl +++ b/app/config/locales/templates/he.email.auth.confirm.tpl @@ -1,24 +1,15 @@ - - -
+

שלום {{name}}, -
-
+

+

נא ללחוץ על הקישור שלהלן כדי לאמת את החשבון שלך. -
- {{redirect}} -
-
+

+{{cta}} +

אם לא ביקשת לאמת את כתובת הדוא״ל, ניתן להתעלם מההודעה זו. -
-
+

+

בברכה,
צוות {{project}} -

\ No newline at end of file +

\ No newline at end of file diff --git a/app/config/locales/templates/he.email.auth.invitation.tpl b/app/config/locales/templates/he.email.auth.invitation.tpl index a7514affda..9a9226a5ba 100644 --- a/app/config/locales/templates/he.email.auth.invitation.tpl +++ b/app/config/locales/templates/he.email.auth.invitation.tpl @@ -1,27 +1,18 @@ - - -
+

שלום, -
-
+

+

הודעת דוא״ל זו נשלחה אליך כי {{owner}} ביקש להזמינך להצטרף לצוות {{team}} ב־{{project}}. -
-
+

+

כדי להצטרף לצוות {{team}}, נא ללחוץ על הקישור: -
- {{redirect}} -
-
+

+{{cta}} +

אם איך לך עניין להצטרף לצוות, ניתן להתעלם מהודעת דוא״ל זו. -
-
+

+

בברכה,
צוות {{project}} -

+

diff --git a/app/config/locales/templates/he.email.auth.recovery.tpl b/app/config/locales/templates/he.email.auth.recovery.tpl index e349365f07..e46bf1990e 100644 --- a/app/config/locales/templates/he.email.auth.recovery.tpl +++ b/app/config/locales/templates/he.email.auth.recovery.tpl @@ -1,24 +1,15 @@ - - -
+

שלום {{name}}, -
-
+

+

נא ללחוץ על הקישור שלהלן כדי לאפס את הסיסמה שלך ב־{{project}}. -
- {{redirect}} -
-
+

+{{cta}} +

אם לא ביקשת לאפס את סיסמתך, ניתן להתעלם מהודעת דוא״ל זו. -
-
+

+

בברכה,
צוות {{project}} -

+

diff --git a/app/config/locales/templates/hi.email.auth.confirm.tpl b/app/config/locales/templates/hi.email.auth.confirm.tpl index a8b2f10086..ec71210dce 100644 --- a/app/config/locales/templates/hi.email.auth.confirm.tpl +++ b/app/config/locales/templates/hi.email.auth.confirm.tpl @@ -1,25 +1,16 @@ - - -
+

नमस्ते {{name}}, -
-
+

+

अपना ईमेल पता वेरीफाई करने के लिए इस लिंक पर क्लिक करे। -
- {{redirect}} -
-
+

+{{cta}} +

यदि आपने इस ईमेल को वेरीफाई करने के लिए नहीं कहा है, तो आप इस संदेश को अनदेखा कर सकते हैं। -
-
+

+

धन्यवाद,
{{project}} टीम -

+

diff --git a/app/config/locales/templates/hi.email.auth.invitation.tpl b/app/config/locales/templates/hi.email.auth.invitation.tpl index 53eea9b70a..f4b5255038 100644 --- a/app/config/locales/templates/hi.email.auth.invitation.tpl +++ b/app/config/locales/templates/hi.email.auth.invitation.tpl @@ -1,27 +1,18 @@ - - -
+

नमस्ते, -
-
+

+

यह मेल आपको इसलिए भेजा गया था क्योंकि {{owner}} आपको {{project}} के लिए {{team}} टीम में टीम मेंबर बनने के लिए आमंत्रित करना चाहते थे। -
-
+

+

टीम {{team}} ज्वाइन करने के लिए इस लिंक पर क्लिक करे : -
- {{redirect}} -
-
+

+{{cta}} +

यदि आप रुचि नहीं रखते हैं, तो आप इस संदेश को अनदेखा कर सकते हैं। -
-
+

+

धन्यवाद,
{{project}} टीम -

+

diff --git a/app/config/locales/templates/hi.email.auth.recovery.tpl b/app/config/locales/templates/hi.email.auth.recovery.tpl index 697c839480..0dbeb3fd9a 100644 --- a/app/config/locales/templates/hi.email.auth.recovery.tpl +++ b/app/config/locales/templates/hi.email.auth.recovery.tpl @@ -1,24 +1,15 @@ - - -
+

नमस्ते {{name}}, -
-
+

+

{{project}} का पासवर्ड रिसेट करने के लिए इस लिंक पर क्लिक करे। -
- {{redirect}} -
-
+

+{{cta}} +

यदि आपने अपना पासवर्ड रीसेट करने के लिए नहीं कहा है, तो आप इस संदेश को अनदेखा कर सकते हैं। -
-
+

+

धन्यवाद,
{{project}} टीम -

+

diff --git a/app/config/locales/templates/hu.email.auth.confirm.tpl b/app/config/locales/templates/hu.email.auth.confirm.tpl index 680e0da2ec..259c7ceb9b 100644 --- a/app/config/locales/templates/hu.email.auth.confirm.tpl +++ b/app/config/locales/templates/hu.email.auth.confirm.tpl @@ -1,24 +1,15 @@ - - -
+

Szia {{name}}, -
-
+

+

Kattints erre a linkre, hogy megerősítsd az e-mail címed. -
- {{redirect}} -
-
+

+{{cta}} +

Ha nem kérelmezted, hogy megerősítsük ezt a címet, ignoráld ezt a levelet. -
-
+

+

Köszönettel,
{{project}} csapat -

+

diff --git a/app/config/locales/templates/hu.email.auth.invitation.tpl b/app/config/locales/templates/hu.email.auth.invitation.tpl index 6dce55a639..5c8d90b86a 100644 --- a/app/config/locales/templates/hu.email.auth.invitation.tpl +++ b/app/config/locales/templates/hu.email.auth.invitation.tpl @@ -1,27 +1,18 @@ - - -
+

Szia, -
-
+

+

Azért küldtük ezt az e-mailt {{owner}} mert meg szeretnénk hívni a {{team}} csapatba a következő projektre {{project}}. -
-
+

+

Kattints erre a linkre, hogy a {{team}} csapat tagja legyél: -
- {{redirect}} -
-
+

+{{cta}} +

Ha nem vegy ebben érdekelt ignoráld ezt az üzenetet. -
-
+

+

Köszönettel,
{{project}} csapat -

+

diff --git a/app/config/locales/templates/hu.email.auth.recovery.tpl b/app/config/locales/templates/hu.email.auth.recovery.tpl index 564d55bd0b..b5e915516a 100644 --- a/app/config/locales/templates/hu.email.auth.recovery.tpl +++ b/app/config/locales/templates/hu.email.auth.recovery.tpl @@ -1,24 +1,15 @@ - - -
+

Szia {{name}}, -
-
+

+

Kattints erre a linkre, hogy visszaállítsuk a {{project}} jelszavad. -
- {{redirect}} -
-
+

+{{cta}} +

Ha nem kérvényezted, hogy visszaállítsuk a jelszavad ignoráld ezt a levelet. -
-
+

+

Köszönettel,
{{project}} csapat -

+

diff --git a/app/config/locales/templates/hy.email.auth.confirm.tpl b/app/config/locales/templates/hy.email.auth.confirm.tpl index f8c8f99670..dd8666c3a3 100644 --- a/app/config/locales/templates/hy.email.auth.confirm.tpl +++ b/app/config/locales/templates/hy.email.auth.confirm.tpl @@ -1,24 +1,15 @@ - - -
+

Ողջույն, {{name}}, -
-
+

+

Անցեք հղումով, որպեսզի հաստատեք Ձեր էլեկտրոնային հասցեն։ -
- {{redirect}} -
-
+

+{{cta}} +

Եթե չեք պահանջել էլեկտրոնային հասցեի հաստատում, պարզապես արհամարհեք այս նամակը։ -
-
+

+

Շնորհակալություն,
{{project}} թիմ -

\ No newline at end of file +

\ No newline at end of file diff --git a/app/config/locales/templates/hy.email.auth.invitation.tpl b/app/config/locales/templates/hy.email.auth.invitation.tpl index e471c81b63..2d0f125403 100644 --- a/app/config/locales/templates/hy.email.auth.invitation.tpl +++ b/app/config/locales/templates/hy.email.auth.invitation.tpl @@ -1,27 +1,18 @@ - - -
+

Ողջույն, -
-
+

+

Դուք ստացել եք այս նամակը, քանի որ {{owner}}-ը հրավիրում է Ձեզ {{team}} խումբ, {{project}} պրոեկտում։ -
-
+

+

Անցեք հղումով, որ միանաք {{team}} թիմին՝ -
- {{redirect}} -
-
+

+{{cta}} +

Եթե հետաքրքրված չեք դրանով, պարզապես արհամարհեք այս նամակը։ -
-
+

+

Շնորհակալություն,
{{project}} թիմ -

+

diff --git a/app/config/locales/templates/hy.email.auth.recovery.tpl b/app/config/locales/templates/hy.email.auth.recovery.tpl index d08068efab..f51dcc96fd 100644 --- a/app/config/locales/templates/hy.email.auth.recovery.tpl +++ b/app/config/locales/templates/hy.email.auth.recovery.tpl @@ -1,24 +1,15 @@ - - -
+

Ողջույն, {{name}}, -
-
+

+

Անցեք հղումով, որպեսզի փոխեք գաղտնաբառը {{project}} պրոեկտի համար։ -
- {{redirect}} -
-
+

+{{cta}} +

Եթե չեք պահանջել գաղտնաբառի փոփոխություն, արհամարհեք այս նամակը։ -
-
+

+

Շնորհակալություն
{{project}} թիմ -

+

diff --git a/app/config/locales/templates/id.email.auth.confirm.tpl b/app/config/locales/templates/id.email.auth.confirm.tpl index a6049cb8f6..a5f3427c35 100644 --- a/app/config/locales/templates/id.email.auth.confirm.tpl +++ b/app/config/locales/templates/id.email.auth.confirm.tpl @@ -1,24 +1,15 @@ - - -
+

Halo {{name}}, -
-
+

+

Ikuti link ini untuk memverifikasi alamat email Anda. -
- {{redirect}} -
-
+

+{{cta}} +

Jika Anda tidak meminta untuk memverifikasi alamat ini, Anda dapat mengabaikan pesan ini. -
-
+

+

Terima kasih,
Tim {{project}} -

\ No newline at end of file +

\ No newline at end of file diff --git a/app/config/locales/templates/id.email.auth.invitation.tpl b/app/config/locales/templates/id.email.auth.invitation.tpl index 7ab49b2002..d81a4a5d83 100644 --- a/app/config/locales/templates/id.email.auth.invitation.tpl +++ b/app/config/locales/templates/id.email.auth.invitation.tpl @@ -1,27 +1,18 @@ - - -
+

Halo, -
-
+

+

Email ini dikirimkan kepada Anda karena {{owner}} ingin mengundang Anda untuk menjadi anggota tim {{team}} di {{project}}. -
-
+

+

Ikuti link ini untuk bergabung dengan tim {{team}}: -
- {{redirect}} -
-
+

+{{cta}} +

Jika Anda tidak tertarik, Anda dapat mengabaikan pesan ini. -
-
+

+

Terima kasih,
Tim {{project}} -

+

diff --git a/app/config/locales/templates/id.email.auth.recovery.tpl b/app/config/locales/templates/id.email.auth.recovery.tpl index e0b953f224..6a7d2596c6 100644 --- a/app/config/locales/templates/id.email.auth.recovery.tpl +++ b/app/config/locales/templates/id.email.auth.recovery.tpl @@ -1,24 +1,15 @@ - - -
+

Halo {{name}}, -
-
+

+

Ikuti link ini untuk mereset kata sandi {{project}} Anda. -
- {{redirect}} -
-
+

+{{cta}} +

Jika Anda tidak meminta untuk mereset kata sandi Anda, Anda dapat mengabaikan pesan ini. -
-
+

+

Terima kasih,
Tim {{project}} -

+

diff --git a/app/config/locales/templates/is.email.auth.confirm.tpl b/app/config/locales/templates/is.email.auth.confirm.tpl index e9733cd47d..c6b9778a45 100644 --- a/app/config/locales/templates/is.email.auth.confirm.tpl +++ b/app/config/locales/templates/is.email.auth.confirm.tpl @@ -1,24 +1,15 @@ - - -
+

Halló {{name}}, -
-
+

+

Fylgdu þessum tengli til að staðfesta netfangið þitt. -
- {{redirect}} -
-
+

+{{cta}} +

Ef þú baðst ekki um að staðfesta þetta netfang geturðu hunsað þessi skilaboð. -
-
+

+

Takk,
{{project}} Teymi -

+

diff --git a/app/config/locales/templates/is.email.auth.invitation.tpl b/app/config/locales/templates/is.email.auth.invitation.tpl index 2738fc2025..83e1e5696d 100644 --- a/app/config/locales/templates/is.email.auth.invitation.tpl +++ b/app/config/locales/templates/is.email.auth.invitation.tpl @@ -1,27 +1,18 @@ - - -
+

Halló, -
-
+

+

Þessi póstur var sendur til þín vegna þess að {{owner}} vildi bjóða þér að gerast liðsmaður í {{team}} teymi í {{project}}. -
-
+

+

Fylgdu þessum hlekk til að ganga í {{team}} liðið: -
- {{redirect}} -
-
+

+{{cta}} +

Ef þú hefur ekki áhuga geturðu hunsað þessi skilaboð. -
-
+

+

Takk,
{{project}} Teymi -

\ No newline at end of file +

\ No newline at end of file diff --git a/app/config/locales/templates/is.email.auth.recovery.tpl b/app/config/locales/templates/is.email.auth.recovery.tpl index fc3bc57722..edddba1ffb 100644 --- a/app/config/locales/templates/is.email.auth.recovery.tpl +++ b/app/config/locales/templates/is.email.auth.recovery.tpl @@ -1,24 +1,15 @@ - - -
+

Halló {{name}}, -
-
+

+

Fylgdu þessum tengli til að núllstilla lykilorð {{project}}. -
- {{redirect}} -
-
+

+{{cta}} +

Ef þú baðst ekki um að endurstilla lykilorðið þitt geturðu hunsað þessi skilaboð. -
-
+

+

Takk,
{{project}} Teymi -

\ No newline at end of file +

\ No newline at end of file diff --git a/app/config/locales/templates/it.email.auth.confirm.tpl b/app/config/locales/templates/it.email.auth.confirm.tpl index 93ee5e006f..ec7837ec3d 100644 --- a/app/config/locales/templates/it.email.auth.confirm.tpl +++ b/app/config/locales/templates/it.email.auth.confirm.tpl @@ -1,25 +1,16 @@ - - -
+

Ciao {{name}}, -
-
+

+

Segui questo link per verificare il tuo indirizzo email. -
- {{redirect}} -
-
+

+{{cta}} +

Se non hai chiesto di verificare questo indirizzo, puoi ignorare questo messaggio. -
-
+

+

Grazie,
Il team di {{project}} -

\ No newline at end of file +

\ No newline at end of file diff --git a/app/config/locales/templates/it.email.auth.invitation.tpl b/app/config/locales/templates/it.email.auth.invitation.tpl index e6eddb5d1b..f4b8e4f93b 100644 --- a/app/config/locales/templates/it.email.auth.invitation.tpl +++ b/app/config/locales/templates/it.email.auth.invitation.tpl @@ -1,28 +1,19 @@ - - -
+

Ciao, -
-
+

+

Questa mail ti è stata inviata perchè {{owner}} vuole invitarti a diventare un membro del team {{team}} del progetto {{project}}. -
-
+

+

Segui questo link per unirti al team {{team}}: -
- {{redirect}} -
-
+

+{{cta}} +

Se non sei interessato, puoi ignorare questo messaggio. -
-
+

+

Grazie,
Il team di {{project}} -

\ No newline at end of file +

\ No newline at end of file diff --git a/app/config/locales/templates/it.email.auth.recovery.tpl b/app/config/locales/templates/it.email.auth.recovery.tpl index 64e865fe76..870b3ab0f5 100644 --- a/app/config/locales/templates/it.email.auth.recovery.tpl +++ b/app/config/locales/templates/it.email.auth.recovery.tpl @@ -1,24 +1,15 @@ - - -
+

Ciao {{name}}, -
-
+

+

Segui questo link per reimpostare la tua password per {{project}}. -
- {{redirect}} -
-
+

+{{cta}} +

Se non hai chiesto di reimpostare la password, puoi ignorare questo messaggio. -
-
+

+

Grazie,
Il team di {{project}} -

\ No newline at end of file +

\ No newline at end of file diff --git a/app/config/locales/templates/ja.email.auth.confirm.tpl b/app/config/locales/templates/ja.email.auth.confirm.tpl index 6c7a89f355..a06f96cd83 100644 --- a/app/config/locales/templates/ja.email.auth.confirm.tpl +++ b/app/config/locales/templates/ja.email.auth.confirm.tpl @@ -1,24 +1,15 @@ - - -
+

{{name}}さん こんにちは。 -
-
+

+

下記のリンクからメールアドレスを認証してください。 -
- {{redirect}} -
-
+

+{{cta}} +

お手数ですが、心当たりがない場合このメールを破棄してください。 -
-
+

+

ありがとうございます。
{{project}} チーム -

\ No newline at end of file +

\ No newline at end of file diff --git a/app/config/locales/templates/ja.email.auth.invitation.tpl b/app/config/locales/templates/ja.email.auth.invitation.tpl index ed59caff07..26d5de981b 100644 --- a/app/config/locales/templates/ja.email.auth.invitation.tpl +++ b/app/config/locales/templates/ja.email.auth.invitation.tpl @@ -1,27 +1,18 @@ - - -
+

こんにちは。 -
-
+

+

{{owner}} さんから {{project}} プロジェクトの {{team}} チームへの参加招待が届きました。 -
-
+

+

下記のリンクから {{team}} へ参加してください。 -
- {{redirect}} -
-
+

+{{cta}} +

お手数ですが、心当たりがない場合このメールを破棄してください。 -
-
+

+

ありがとうございます。
{{project}} チーム -

+

diff --git a/app/config/locales/templates/ja.email.auth.recovery.tpl b/app/config/locales/templates/ja.email.auth.recovery.tpl index 108c7aebee..0b0260c586 100644 --- a/app/config/locales/templates/ja.email.auth.recovery.tpl +++ b/app/config/locales/templates/ja.email.auth.recovery.tpl @@ -1,24 +1,15 @@ - - -
+

{{name}}さん こんにちは。 -
-
+

+

下記のリンクから {{project}} プロジェクトのパスワードを再設定してください。 -
- {{redirect}} -
-
+

+{{cta}} +

お手数ですが、心当たりがない場合このメールを破棄してください。 -
-
+

+

ありがとうございます。
{{project}} チーム -

+

diff --git a/app/config/locales/templates/jv.email.auth.confirm.tpl b/app/config/locales/templates/jv.email.auth.confirm.tpl index c1afdc69a9..71279a3c89 100644 --- a/app/config/locales/templates/jv.email.auth.confirm.tpl +++ b/app/config/locales/templates/jv.email.auth.confirm.tpl @@ -1,24 +1,15 @@ - - -
+

Halo, {{name}}, -
-
+

+

Tindakake tautan iki kanggo verifikasi alamat email sampeyan. -
- {{redirect}} -
-
+

+{{cta}} +

Yen sampeyan ora njaluk verifikasi alamat iki, sampeyan bisa nglalekake pesen iki. -
-
+

+

Matur suwun,
tim {{project}} -

+

diff --git a/app/config/locales/templates/jv.email.auth.invitation.tpl b/app/config/locales/templates/jv.email.auth.invitation.tpl index 8c5de0f419..807cc3f478 100644 --- a/app/config/locales/templates/jv.email.auth.invitation.tpl +++ b/app/config/locales/templates/jv.email.auth.invitation.tpl @@ -1,28 +1,19 @@ - - -
+

Halo, -
-
+

+

Email iki dikirim menyang sampeyan amarga {{owner}} pengin ngajak sampeyan dadi anggota tim ing {{team}} tim ing {{project}}. -
-
+

+

Tindakake link iki kanggo gabung ing {{team}}: -
- {{redirect}} -
-
+

+{{cta}} +

Yen sampeyan ora kasengsem, sampeyan bisa nglirwakake pesen iki. -
-
+

+

Matur suwun,
tim {{project}} -

\ No newline at end of file +

\ No newline at end of file diff --git a/app/config/locales/templates/jv.email.auth.recovery.tpl b/app/config/locales/templates/jv.email.auth.recovery.tpl index ed6c879aeb..1efd86bc77 100644 --- a/app/config/locales/templates/jv.email.auth.recovery.tpl +++ b/app/config/locales/templates/jv.email.auth.recovery.tpl @@ -1,24 +1,15 @@ - - -
+

Halo {{name}}, -
-
+

+

Tindakake link iki kanggo ngreset {{project}} sandhi. -
- {{redirect}} -
-
+

+{{cta}} +

Yen sampeyan ora njaluk ngreset sandhi, sampeyan bisa nglalekake pesen iki. -
-
+

+

Matur suwun,
tim {{project}} -

+

diff --git a/app/config/locales/templates/km.email.auth.confirm.tpl b/app/config/locales/templates/km.email.auth.confirm.tpl index c2c94d08c0..193508c9bd 100644 --- a/app/config/locales/templates/km.email.auth.confirm.tpl +++ b/app/config/locales/templates/km.email.auth.confirm.tpl @@ -1,24 +1,15 @@ - - -
+

សួស្ដី {{name}}, -
-
+

+

តាមតំណនេះដើម្បីផ្ទៀងផ្ទាត់អាសយដ្ឋានសារអេឡិចត្រូនិចរបស់អ្នក។ -
- {{redirect}} -
-
+

+{{cta}} +

ប្រសិនបើអ្នកពុំបានស្នើសុំផ្ទៀងផ្ទាត់អាសយដ្ឋាននេះទេ អ្នកអាចមិនអើពើនឹងសារនេះបាន។ -
-
+

+

អរគុណ,
ក្រុម {{project}} -

\ No newline at end of file +

\ No newline at end of file diff --git a/app/config/locales/templates/km.email.auth.invitation.tpl b/app/config/locales/templates/km.email.auth.invitation.tpl index 910a14451f..a366ed73e9 100644 --- a/app/config/locales/templates/km.email.auth.invitation.tpl +++ b/app/config/locales/templates/km.email.auth.invitation.tpl @@ -1,27 +1,18 @@ - - -
+

សួស្ដី, -
-
+

+

សារអេឡិចត្រូនិចនេះត្រូវបានផ្ញើទៅអ្នកដោយសារ {{owner}} ចង់អញ្ជើញអ្នកឲ្យក្លាយជាសមាជិកនៅក្រុម {{team}} លើ {{project}}។ -
-
+

+

តាមតំណនេះដើម្បីចូលរួមក្នុងក្រុម {{team}}៖ -
- {{redirect}} -
-
+

+{{cta}} +

ប្រសិនបើអ្នកមិនចាប់អារម្មណ៍ អ្នកអាចមិនអើពើនឹងសារនេះបាន។ -
-
+

+

អរគុណ,
ក្រុម {{project}} -

+

diff --git a/app/config/locales/templates/km.email.auth.recovery.tpl b/app/config/locales/templates/km.email.auth.recovery.tpl index 138997623b..578fe15d14 100644 --- a/app/config/locales/templates/km.email.auth.recovery.tpl +++ b/app/config/locales/templates/km.email.auth.recovery.tpl @@ -1,24 +1,15 @@ - - -
+

សួស្ដី {{name}}, -
-
+

+

តាមតំណនេះដើម្បីកំណត់ពាក្យសម្ងាត់នៃ {{project}} របស់អ្នកឡើងវិញ។ -
- {{redirect}} -
-
+

+{{cta}} +

ប្រសិនបើអ្នកពុំបានស្នើសុំកំណត់ពាក្យសម្ងាត់របស់អ្នកឡើងវិញទេ អ្នកអាចមិនអើពើនឹងសារនេះបាន។ -
-
+

+

អរគុណ,
ក្រុម {{project}} -

+

diff --git a/app/config/locales/templates/ko.email.auth.confirm.tpl b/app/config/locales/templates/ko.email.auth.confirm.tpl index e3c6070d55..bf9bfd1c08 100644 --- a/app/config/locales/templates/ko.email.auth.confirm.tpl +++ b/app/config/locales/templates/ko.email.auth.confirm.tpl @@ -1,24 +1,15 @@ - - -
+

안녕하세요 {{name}}님, -
-
+

+

회원님 이메일을 인증하러 아래 링크를 클릭하세요. -
- {{redirect}} -
-
+

+{{cta}} +

만약 인증 요청하지 않으셨다면 이 이메일을 무시하세요. -
-
+

+

감사합니다!
{{project}}팀 드림 -

\ No newline at end of file +

\ No newline at end of file diff --git a/app/config/locales/templates/ko.email.auth.invitation.tpl b/app/config/locales/templates/ko.email.auth.invitation.tpl index 4fc62b8516..21ed74de68 100644 --- a/app/config/locales/templates/ko.email.auth.invitation.tpl +++ b/app/config/locales/templates/ko.email.auth.invitation.tpl @@ -1,27 +1,18 @@ - - -
+

안녕하세요, -
-
+

+

{{owner}}님이 {{project}}프로젝트의 {{team}}팀에 초대했습니다. -
-
+

+

아래 링크를 통하여 {{team}}팀에 합류해주시면 됩니다. -
- {{redirect}} -
-
+

+{{cta}} +

만약 합류에 관심 없으시면 이 이메일을 무시하세요. -
-
+

+

감사합니다!
{{project}}팀 드림 -

+

diff --git a/app/config/locales/templates/ko.email.auth.recovery.tpl b/app/config/locales/templates/ko.email.auth.recovery.tpl index bf73a93058..5445ef6a3e 100644 --- a/app/config/locales/templates/ko.email.auth.recovery.tpl +++ b/app/config/locales/templates/ko.email.auth.recovery.tpl @@ -1,24 +1,15 @@ - - -
+

안녕하세요 {{name}}님, -
-
+

+

{{project}} 비밀번호 재설정하러 아래 링크를 클릭하세요. -
- {{redirect}} -
-
+

+{{cta}} +

만약 회원님이 비밀번호 재설정 요청하지 않으셨다면 이 이메일을 무시하세요. -
-
+

+

감사합니다!
{{project}}팀 드림 -

+

diff --git a/app/config/locales/templates/lt.email.auth.confirm.tpl b/app/config/locales/templates/lt.email.auth.confirm.tpl index f1bc439b11..d7514cc25b 100644 --- a/app/config/locales/templates/lt.email.auth.confirm.tpl +++ b/app/config/locales/templates/lt.email.auth.confirm.tpl @@ -1,24 +1,15 @@ - - -
+

Sveiki, {{name}}, -
-
+

+

Paspauskite nuorodą, kad patvirtinti savo el. paštą. -
- {{redirect}} -
-
+

+{{cta}} +

Jei neprašėte el. pašto patvirtinimo, ignoruokite šį laišką -
-
+

+

Ačiū,
komanda {{project}} -

+

diff --git a/app/config/locales/templates/lt.email.auth.invitation.tpl b/app/config/locales/templates/lt.email.auth.invitation.tpl index 0c40a3cbc3..2eb630b5bd 100644 --- a/app/config/locales/templates/lt.email.auth.invitation.tpl +++ b/app/config/locales/templates/lt.email.auth.invitation.tpl @@ -1,27 +1,18 @@ - - -
+

Sveiki, -
-
+

+

Šis laiškas buvo išsiųstas dėl to, kad {{owner}} kviečia tapti komandos {{team}} nariu projekte {{project}}. -
-
+

+

Paspauskite nuorodą, kad prisijungti prie komandos {{team}} : -
- {{redirect}} -
-
+

+{{cta}} +

Jei Jums neįdomu, ignoruokite šį laišką. -
-
+

+

Ačiū,
komanda {{project}} -

+

diff --git a/app/config/locales/templates/lt.email.auth.recovery.tpl b/app/config/locales/templates/lt.email.auth.recovery.tpl index 7aba4cd2ce..0ed8354918 100644 --- a/app/config/locales/templates/lt.email.auth.recovery.tpl +++ b/app/config/locales/templates/lt.email.auth.recovery.tpl @@ -1,24 +1,15 @@ - - -
+

Sveiki, {{name}}, -
-
+

+

Paspauskite nuorodą, kad pakeisti slaptožodį projektui {{project}}. -
- {{redirect}} -
-
+

+{{cta}} +

Jei neprašėte atkurti slaptažodžio, ignoruokite šį laišką. -
-
+

+

Ačiū,
komanda {{project}} -

+

diff --git a/app/config/locales/templates/ml.email.auth.confirm.tpl b/app/config/locales/templates/ml.email.auth.confirm.tpl index 14746f04a5..b4f0724d70 100644 --- a/app/config/locales/templates/ml.email.auth.confirm.tpl +++ b/app/config/locales/templates/ml.email.auth.confirm.tpl @@ -1,28 +1,19 @@ - - -
+

നമസ്കാരം {{name}}, -
-
+

+

താങ്കളുടെ ഇമെയിൽ ഐഡി വെരിഫൈ ചെയ്യുന്നതിന് താഴെ കാണുന്ന ലിങ്കിൽ ക്ലിക്ക് ചെയ്യൂ -
- {{redirect}} -
-
+

+{{cta}} +

ഇമെയിൽ ഐഡി വെരിഫൈ ചെയ്യേണ്ടെങ്കിൽ ഈ മെസ്സേജ് ഇഗ്നോർ ചെയ്യാം -
-
+

+

നന്ദി
{{project}} ടീം. -

+

diff --git a/app/config/locales/templates/ml.email.auth.invitation.tpl b/app/config/locales/templates/ml.email.auth.invitation.tpl index 5f16374dcd..06b5bdf08e 100644 --- a/app/config/locales/templates/ml.email.auth.invitation.tpl +++ b/app/config/locales/templates/ml.email.auth.invitation.tpl @@ -1,27 +1,18 @@ - - -
+

നമസ്കാരം, -
-
+

+

{{owner}} താങ്കളെ {{project}} പ്രോജക്ടിലെ {{team}} ടീമിൽ അംഗമാവാൻ ക്ഷണിച്ചിരിക്കുന്നു. -
-
+

+

{{team}} ടീമിൽ ചേരുവാൻ താഴെ കാണുന്ന ലിങ്കിൽ ക്ലിക്ക് ചെയ്യൂ. -
- {{redirect}} -
-
+

+{{cta}} +

താൽപര്യമില്ലെങ്കിൽ ഈ മെസ്സേജ് ഇഗ്നോർ ചെയ്യാം -
-
+

+

നന്ദി,
{{project}} ടീം. -

+

diff --git a/app/config/locales/templates/ml.email.auth.recovery.tpl b/app/config/locales/templates/ml.email.auth.recovery.tpl index ee1e73a52b..cf82e5c489 100644 --- a/app/config/locales/templates/ml.email.auth.recovery.tpl +++ b/app/config/locales/templates/ml.email.auth.recovery.tpl @@ -1,24 +1,15 @@ - - -
+

നമസ്കാരം {{name}}, -
-
+

+

താങ്കളുടെ {{project}} പാസ്‌വേഡ് റീസെറ്റ് ചെയ്യാൻ താഴെ കാണുന്ന ലിങ്കിൽ ക്ലിക്ക് ചെയ്യുക -
- {{redirect}} -
-
+

+{{cta}} +

പാസ്‌വേഡ് റീസെറ്റ് ചെയ്യാൻ താങ്കൾ റിക്വസ്റ്റ് ചെയ്തിട്ടില്ലെങ്കിൽ ദയവായി ഈ മെസ്സേജ് ഇഗ്നോർ ചെയ്യുക -
-
+

+

നന്ദി,
{{project}} ടീം -

+

diff --git a/app/config/locales/templates/my.email.auth.confirm.tpl b/app/config/locales/templates/my.email.auth.confirm.tpl index 65a224c51f..174b100e02 100644 --- a/app/config/locales/templates/my.email.auth.confirm.tpl +++ b/app/config/locales/templates/my.email.auth.confirm.tpl @@ -1,24 +1,15 @@ - - -
+

Hello {{name}}, -
-
+

+

Ikuti pautan ini untuk mengesahkan alamat e-mel anda. -
- {{redirect}} -
-
+

+{{cta}} +

Jika anda tidak meminta untuk mengesahkan alamat ini, anda boleh mengabaikan mesej ini. -
-
+

+

Terima kasih,
Kumpulan {{project}} -

\ No newline at end of file +

\ No newline at end of file diff --git a/app/config/locales/templates/my.email.auth.invitation.tpl b/app/config/locales/templates/my.email.auth.invitation.tpl index 891e1eb4de..66e38bb608 100644 --- a/app/config/locales/templates/my.email.auth.invitation.tpl +++ b/app/config/locales/templates/my.email.auth.invitation.tpl @@ -1,27 +1,18 @@ - - -
+

Hello, -
-
+

+

E-mel ini dihantar kepada anda kerana {{owner}}ingin mengundang Anda untuk menjadi anggota kumpulan {{team}} di dalam {{project}}. -
-
+

+

Ikuti pautan ini untuk menyertai kumpulan {{team}}: -
- {{redirect}} -
-
+

+{{cta}} +

Sekiranya anda tidak berminat,anda boleh mengabaikan mesej ini. -
-
+

+

Terima Kasih,
Kumpulan {{project}} -

+

diff --git a/app/config/locales/templates/my.email.auth.recovery.tpl b/app/config/locales/templates/my.email.auth.recovery.tpl index 25f81408d7..a14c78cb59 100644 --- a/app/config/locales/templates/my.email.auth.recovery.tpl +++ b/app/config/locales/templates/my.email.auth.recovery.tpl @@ -1,24 +1,15 @@ - - -
+

Hello {{name}}, -
-
+

+

Ikuti pautan ini sekiranya anda mahu menetapkan semula kata laluan {{project}} anda. -
- {{redirect}} -
-
+

+{{cta}} +

Sekiranya anda tidak meminta untuk menetapkan semula kata laluan anda,anda boleh sahaja mengabaikan mesej ini. -
-
+

+

Terima Kasih,
Kumpulan {{project}} -

+

diff --git a/app/config/locales/templates/nl.email.auth.confirm.tpl b/app/config/locales/templates/nl.email.auth.confirm.tpl index 513593bfc6..04f6a14ccf 100644 --- a/app/config/locales/templates/nl.email.auth.confirm.tpl +++ b/app/config/locales/templates/nl.email.auth.confirm.tpl @@ -1,24 +1,15 @@ - - -
+

Hallo {{name}}, -
-
+

+

Klink op deze link om uw emailadres te valideren. -
- {{redirect}} -
-
+

+{{cta}} +

Als u niet heeft gevraagd om dit adres te verifiëren, kunt u dit bericht negeren. -
-
+

+

Bedankt,
{{project}} team -

\ No newline at end of file +

\ No newline at end of file diff --git a/app/config/locales/templates/nl.email.auth.invitation.tpl b/app/config/locales/templates/nl.email.auth.invitation.tpl index 252e104aff..75ac6f4510 100644 --- a/app/config/locales/templates/nl.email.auth.invitation.tpl +++ b/app/config/locales/templates/nl.email.auth.invitation.tpl @@ -1,27 +1,18 @@ - - -
+

Hallo, -
-
+

+

Deze mail is naar je gestuurd omdat {{owner}} wilde dat u een lid zou worden in het {{team}} team voor {{project}}. -
-
+

+

Volg deze link om het team te joinen {{team}}: -
- {{redirect}} -
-
+

+{{cta}} +

ALs u niet geïnteresseerd bent, kunt u dit bericht negeren. -
-
+

+

Bedankt,
{{project}} team -

+

diff --git a/app/config/locales/templates/nl.email.auth.recovery.tpl b/app/config/locales/templates/nl.email.auth.recovery.tpl index 2c4d5952a2..4c7ac87961 100644 --- a/app/config/locales/templates/nl.email.auth.recovery.tpl +++ b/app/config/locales/templates/nl.email.auth.recovery.tpl @@ -1,24 +1,15 @@ - - -
+

Hallo {{name}}, -
-
+

+

Klik op deze link om uw {{project}} wachtwoord te resetten. -
- {{redirect}} -
-
+

+{{cta}} +

Als u niet gevraagd heeft om uw wachtwoord te resetten, kunt u dit bericht negeren. -
-
+

+

Bedankt,
{{project}} team -

+

diff --git a/app/config/locales/templates/no.email.auth.confirm.tpl b/app/config/locales/templates/no.email.auth.confirm.tpl index abe17ff65f..63c906cdf5 100644 --- a/app/config/locales/templates/no.email.auth.confirm.tpl +++ b/app/config/locales/templates/no.email.auth.confirm.tpl @@ -1,24 +1,15 @@ - - -
+

Hei {{name}}, -
-
+

+

Følg denne lenken for å verifisere din e-postadresse. -
- {{redirect}} -
-
+

+{{cta}} +

Hvis du ikke har spurt om å verifisere din e-post, kan du ignorere denne meldingen. -
-
+

+

Hilsen,
{{project}}-teamet -

\ No newline at end of file +

\ No newline at end of file diff --git a/app/config/locales/templates/no.email.auth.invitation.tpl b/app/config/locales/templates/no.email.auth.invitation.tpl index c56e913d3d..00cd9defe5 100644 --- a/app/config/locales/templates/no.email.auth.invitation.tpl +++ b/app/config/locales/templates/no.email.auth.invitation.tpl @@ -1,28 +1,19 @@ - - -
+

Hei, -
-
+

+

Denne mailen ble sendt til deg fordi {{owner}} har invitert deg til å bli medlem av {{team}}-teamet på {{project}}. -
-
+

+

Follow this link to join the {{team}} team: Følg denne lenken for å bli med på {{team}}-teamet: -
- {{redirect}} -
-
+

+{{cta}} +

Hvis du ikke er interresert kan du ignorere denne meldingen. -
-
+

+

Hilsen,
{{project}}-teamet -

+

diff --git a/app/config/locales/templates/no.email.auth.recovery.tpl b/app/config/locales/templates/no.email.auth.recovery.tpl index 75b8be0e96..472674be31 100644 --- a/app/config/locales/templates/no.email.auth.recovery.tpl +++ b/app/config/locales/templates/no.email.auth.recovery.tpl @@ -1,25 +1,16 @@ - - -
+

Hei {{name}}, -
-
+

+

Follow this link to reset your {{project}} password. Følg denne lenken for å tilbakestille ditt {{project}}-passord. -
- {{redirect}} -
-
+

+{{cta}} +

Hvis du ikke har spurt om å tilbakestille passordet ditt, kan du ignorere denne meldingen. -
-
+

+

Hilsen,
{{project}}-teamet -

+

diff --git a/app/config/locales/templates/ph.email.auth.confirm.tpl b/app/config/locales/templates/ph.email.auth.confirm.tpl index 08a9b6d639..63e5db58db 100644 --- a/app/config/locales/templates/ph.email.auth.confirm.tpl +++ b/app/config/locales/templates/ph.email.auth.confirm.tpl @@ -1,24 +1,15 @@ - - -
+

Kamusta {{name}}, -
-
+

+

Sundun ang link na ito upang ma-verify ang iyong email address. -
- {{redirect}} -
-
+

+{{cta}} +

Kung hindi mo hiniling na i-verify ang address na ito, maaari mong balewalain ang mensaheng ito. -
-
+

+

Salamat,
Pangkat ng {{project}} -

\ No newline at end of file +

\ No newline at end of file diff --git a/app/config/locales/templates/ph.email.auth.invitation.tpl b/app/config/locales/templates/ph.email.auth.invitation.tpl index f8a8668e50..23916efb0c 100644 --- a/app/config/locales/templates/ph.email.auth.invitation.tpl +++ b/app/config/locales/templates/ph.email.auth.invitation.tpl @@ -1,27 +1,18 @@ - - -
+

Kamusta, -
-
+

+

Ipinadala ang email na ito sa iyo dahil nais kang anyayahan ni {{ owner }} upang maging isang kasapi ng pangkat ng {{ team }} sa {{ project }}. -
-
+

+

Sundan ang link na ito upang sumali sa pangkat ng {{team}}: -
- {{redirect}} -
-
+

+{{cta}} +

Kung hindi ka interesado, maari mong balewalain ang mensahing ito. -
-
+

+

Salamat,
Pangkat ng {{project}} -

+

diff --git a/app/config/locales/templates/ph.email.auth.recovery.tpl b/app/config/locales/templates/ph.email.auth.recovery.tpl index abf1b780ef..c9096076a9 100644 --- a/app/config/locales/templates/ph.email.auth.recovery.tpl +++ b/app/config/locales/templates/ph.email.auth.recovery.tpl @@ -1,24 +1,15 @@ - - -
+

Kamusta {{name}}, -
-
+

+

Sundan ang link na ito updang i-reset ang iyong password sa {{ project }}. -
- {{redirect}} -
-
+

+{{cta}} +

Kung hindi mo hiniling na i-reset ang iyong password, maaari mong balewalain ang mensahe na ito. -
-
+

+

Salamat,
Pangkat ng {{project}} -

+

diff --git a/app/config/locales/templates/pl.email.auth.confirm.tpl b/app/config/locales/templates/pl.email.auth.confirm.tpl index e46187999f..5b797d331b 100644 --- a/app/config/locales/templates/pl.email.auth.confirm.tpl +++ b/app/config/locales/templates/pl.email.auth.confirm.tpl @@ -1,24 +1,15 @@ - - -
+

Witaj {{name}}, -
-
+

+

Kliknij ten link, aby zweryfikować swój adres e-mail. -
- {{redirect}} -
-
+

+{{cta}} +

Jeśli nie prosiłeś o weryfikację tego adresu, możesz zignorować tę wiadomość. -
-
+

+

Dziękujemy,
zespół {{project}} -

\ No newline at end of file +

\ No newline at end of file diff --git a/app/config/locales/templates/pl.email.auth.invitation.tpl b/app/config/locales/templates/pl.email.auth.invitation.tpl index d11586de75..387b2e4ae8 100644 --- a/app/config/locales/templates/pl.email.auth.invitation.tpl +++ b/app/config/locales/templates/pl.email.auth.invitation.tpl @@ -1,27 +1,18 @@ - - -
+

Witaj, -
-
+

+

Ta wiadomość została do Ciebie wysłana, ponieważ {{owner}} chciałby zaprosić Cię do dołączenia do zespołu {{team}} w {{project}}. -
-
+

+

Kliknij ten link, aby dołączyć do zespołu {{team}}: -
- {{redirect}} -
-
+

+{{cta}} +

Jeśli nie jesteś zainteresowany, możesz zignorować tę wiadomość. -
-
+

+

Dziękujemy,
zaspół {{project}} -

+

diff --git a/app/config/locales/templates/pl.email.auth.recovery.tpl b/app/config/locales/templates/pl.email.auth.recovery.tpl index 78066ee064..5b79846ae6 100644 --- a/app/config/locales/templates/pl.email.auth.recovery.tpl +++ b/app/config/locales/templates/pl.email.auth.recovery.tpl @@ -1,24 +1,15 @@ - - -
+

Witaj {{name}}, -
-
+

+

Kliknij ten link, aby zresetować hasło do {{project}}. -
- {{redirect}} -
-
+

+{{cta}} +

Jeśli nie prosiłeś o zresetowanie hasła, możesz zignorować tę wiadomość. -
-
+

+

Dziękujemy,
zaspół {{project}} -

+

diff --git a/app/config/locales/templates/pn.email.auth.confirm.tpl b/app/config/locales/templates/pn.email.auth.confirm.tpl index 3b31381808..c96d27dee1 100644 --- a/app/config/locales/templates/pn.email.auth.confirm.tpl +++ b/app/config/locales/templates/pn.email.auth.confirm.tpl @@ -1,24 +1,15 @@ - - -
+

ਹੈਲੋ {{name}}, -
-
+

+

ਆਪਣੇ ਈਮੇਲ ਪਤੇ ਦੀ ਪੁਸ਼ਟੀ ਕਰਨ ਲਈ ਇਸ ਲਿੰਕ ਦਾ ਪਾਲਣ ਕਰੋ: -
- {{redirect}} -
-
+

+{{cta}} +

ਜੇ ਤੁਸੀਂ ਇਸ ਪਤੇ ਨੂੰ ਪ੍ਰਮਾਣਿਤ ਕਰਨ ਲਈ ਨਹੀਂ ਕਿਹਾ, ਤਾਂ ਤੁਸੀਂ ਇਸ ਸੁਨੇਹੇ ਨੂੰ ਨਜ਼ਰ ਅੰਦਾਜ਼ ਕਰ ਸਕਦੇ ਹੋ. -
-
+

+

ਧੰਨਵਾਦ,
{{project}} ਟੀਮ -

+

diff --git a/app/config/locales/templates/pn.email.auth.invitation.tpl b/app/config/locales/templates/pn.email.auth.invitation.tpl index 2b0a5fd279..6280d951a1 100644 --- a/app/config/locales/templates/pn.email.auth.invitation.tpl +++ b/app/config/locales/templates/pn.email.auth.invitation.tpl @@ -1,27 +1,18 @@ - - -
+

ਸਤ ਸ੍ਰੀ ਅਕਾਲ, -
-
+

+

ਇਹ ਮੇਲ ਤੁਹਾਨੂੰ ਇਸ ਲਈ ਭੇਜਿਆ ਗਿਆ ਸੀ ਕਿਉਂਕਿ {{owner}} ਤੁਹਾਨੂੰ {{team} at ਟੀਮ {{project}} 'ਤੇ ਟੀਮ ਦੇ ਮੈਂਬਰ ਬਣਨ ਲਈ ਸੱਦਾ ਦੇਣਾ ਚਾਹੁੰਦਾ ਸੀ. -
-
+

+

{{team}} ਟੀਮ ਵਿੱਚ ਸ਼ਾਮਲ ਹੋਣ ਲਈ ਇਸ ਲਿੰਕ ਦਾ ਪਾਲਣ ਕਰੋ: -
- {{redirect}} -
-
+

+{{cta}} +

ਜੇ ਤੁਸੀਂ ਦਿਲਚਸਪੀ ਨਹੀਂ ਰੱਖਦੇ, ਤਾਂ ਤੁਸੀਂ ਇਸ ਸੰਦੇਸ਼ ਨੂੰ ਨਜ਼ਰ ਅੰਦਾਜ਼ ਕਰ ਸਕਦੇ ਹੋ. -
-
+

+

ਧੰਨਵਾਦ,
{{project}} ਟੀਮ -

+

diff --git a/app/config/locales/templates/pn.email.auth.recovery.tpl b/app/config/locales/templates/pn.email.auth.recovery.tpl index e85026a203..cdb4fd069d 100644 --- a/app/config/locales/templates/pn.email.auth.recovery.tpl +++ b/app/config/locales/templates/pn.email.auth.recovery.tpl @@ -1,24 +1,15 @@ - - -
+

ਸਤ ਸ੍ਰੀ ਅਕਾਲ {{name}}, -
-
+

+

ਆਪਣੇ {{project}} ਪਾਸਵਰਡ ਨੂੰ ਰੀਸੈਟ ਕਰਨ ਲਈ ਇਸ ਲਿੰਕ ਦਾ ਪਾਲਣ ਕਰੋ. -
- {{redirect}} -
-
+

+{{cta}} +

ਜੇ ਤੁਸੀਂ ਆਪਣਾ ਪਾਸਵਰਡ ਰੀਸੈਟ ਕਰਨ ਲਈ ਨਹੀਂ ਕਿਹਾ, ਤਾਂ ਤੁਸੀਂ ਇਸ ਸੁਨੇਹੇ ਨੂੰ ਨਜ਼ਰ ਅੰਦਾਜ਼ ਕਰ ਸਕਦੇ ਹੋ. -
-
+

+

ਧੰਨਵਾਦ,
{{project}} ਟੀਮ -

+

diff --git a/app/config/locales/templates/pt-br.email.auth.confirm.tpl b/app/config/locales/templates/pt-br.email.auth.confirm.tpl index 7cb959914e..f7ffdd8c38 100644 --- a/app/config/locales/templates/pt-br.email.auth.confirm.tpl +++ b/app/config/locales/templates/pt-br.email.auth.confirm.tpl @@ -1,24 +1,15 @@ - - -
+

Olá {{name}}, -
-
+

+

Por favor, confirme o seu email acessando o link abaixo. -
- {{redirect}} -
-
+

+{{cta}} +

Caso a confirmação de email não foi solicitada por você, ignore esta mensagem. -
-
+

+

Atenciosamente,
Equipe {{project}} -

+

diff --git a/app/config/locales/templates/pt-br.email.auth.invitation.tpl b/app/config/locales/templates/pt-br.email.auth.invitation.tpl index f4255ffafa..b0de23b3fd 100644 --- a/app/config/locales/templates/pt-br.email.auth.invitation.tpl +++ b/app/config/locales/templates/pt-br.email.auth.invitation.tpl @@ -1,27 +1,18 @@ - - -
+

Olá, -
-
+

+

Este email foi enviado a você porque
{{owner}} deseja convidá-lo para se tornar membro da equipe {{team}} no {{project}}. -
-
+

+

Entre no link abaixo para se juntar a equipe {{team}}: -
- {{redirect}} -
-
+

+{{cta}} +

Caso não estiver interessado, por favor ignore esta mensagem. -
-
+

+

Atenciosamente,
Equipe {{project}} -

+

diff --git a/app/config/locales/templates/pt-br.email.auth.recovery.tpl b/app/config/locales/templates/pt-br.email.auth.recovery.tpl index e2d710f38f..5393316918 100644 --- a/app/config/locales/templates/pt-br.email.auth.recovery.tpl +++ b/app/config/locales/templates/pt-br.email.auth.recovery.tpl @@ -1,24 +1,15 @@ - - -
+

Olá {{name}}, -
-
+

+

Acesse o link abaixo para redefinir sua senha do {{project}}. -
- {{redirect}} -
-
- Caso você não solicitou a redefinição de senha, por favor ignore esta mensagem. -
-
+

+{{cta}} +

+ Caso não tenha solicitado a redefinição de senha, por favor ignore esta mensagem. +

+

Atenciosamente,
Equipe {{project}} -

+

diff --git a/app/config/locales/templates/pt-pt.email.auth.confirm.tpl b/app/config/locales/templates/pt-pt.email.auth.confirm.tpl index d2209883cc..a2d8998f3c 100644 --- a/app/config/locales/templates/pt-pt.email.auth.confirm.tpl +++ b/app/config/locales/templates/pt-pt.email.auth.confirm.tpl @@ -1,24 +1,15 @@ - - -
+

Olá {{name}}, -
-
+

+

Por favor, confirme o seu email através do link abaixo. -
- {{redirect}} -
-
+

+{{cta}} +

Se não solicitou a confirmação de email, por favor ignore esta mensagem. -
-
+

+

Com os melhores cumprimentos,
Equipa {{project}} -

\ No newline at end of file +

\ No newline at end of file diff --git a/app/config/locales/templates/pt-pt.email.auth.invitation.tpl b/app/config/locales/templates/pt-pt.email.auth.invitation.tpl index 429a30be9a..c0bdfa4136 100644 --- a/app/config/locales/templates/pt-pt.email.auth.invitation.tpl +++ b/app/config/locales/templates/pt-pt.email.auth.invitation.tpl @@ -1,27 +1,18 @@ - - -
+

Olá, -
-
+

+

Recebeu este email porque
{{owner}} deseja convidá-lo a tornar-se membro da equipa {{team}} no {{project}}. -
-
+

+

Use este link para se juntar à equipa {{team}}: -
- {{redirect}} -
-
+

+{{cta}} +

Se não estiver interessado, por favor ignore esta mensagem. -
-
+

+

Com os melhores cumprimentos,
Equipa {{project}} -

+

diff --git a/app/config/locales/templates/pt-pt.email.auth.recovery.tpl b/app/config/locales/templates/pt-pt.email.auth.recovery.tpl index 7c89f1b729..ae09ac0998 100644 --- a/app/config/locales/templates/pt-pt.email.auth.recovery.tpl +++ b/app/config/locales/templates/pt-pt.email.auth.recovery.tpl @@ -1,24 +1,15 @@ - - -
+

Olá {{name}}, -
-
+

+

Use este link para repor a sua palavra-passe {{project}}. -
- {{redirect}} -
-
+

+{{cta}} +

Se não solicitou a reposição da sua palavra-passe, por favor ignore esta mensagem. -
-
+

+

Com os melhores cumprimentos,
Equipa {{project}} -

+

diff --git a/app/config/locales/templates/ro.email.auth.confirm.tpl b/app/config/locales/templates/ro.email.auth.confirm.tpl index e5672f6d9a..133fe2a189 100644 --- a/app/config/locales/templates/ro.email.auth.confirm.tpl +++ b/app/config/locales/templates/ro.email.auth.confirm.tpl @@ -1,24 +1,15 @@ - - -
+

Salut {{name}}, -
-
+

+

Accesează link-ul următor pentru a-ți confirma mail-ul. -
- {{redirect}} -
-
+

+{{cta}} +

Dacă nu ai solicitat acest mail, te rugăm frumos să îl ignori. -
-
+

+

Mulțumim,
Echipa {{project}} -

+

diff --git a/app/config/locales/templates/ro.email.auth.invitation.tpl b/app/config/locales/templates/ro.email.auth.invitation.tpl index 477ecfe84b..04cdf9e0bd 100644 --- a/app/config/locales/templates/ro.email.auth.invitation.tpl +++ b/app/config/locales/templates/ro.email.auth.invitation.tpl @@ -1,27 +1,18 @@ - - -
+

Salut, -
-
+

+

Ai primit acest mail deoarece {{owner}} te-a invitat să faci parte din echipa {{team}} în proiectul {{project}}. -
-
+

+

Accesează link-ul următor pentru a accepta invitația în echipa {{team}}: -
- {{redirect}} -
-
+

+{{cta}} +

Dacă nu ești interesat de această invitație, te rugăm frumos să o ignori. -
-
+

+

Cu drag,
Echipa {{project}} -

+

diff --git a/app/config/locales/templates/ro.email.auth.recovery.tpl b/app/config/locales/templates/ro.email.auth.recovery.tpl index 2e934194bb..9e5f24cbea 100644 --- a/app/config/locales/templates/ro.email.auth.recovery.tpl +++ b/app/config/locales/templates/ro.email.auth.recovery.tpl @@ -1,24 +1,15 @@ - - -
+

Salut {{name}}, -
-
+

+

Accesează link-ul următor pentru a-ți reseta parola din proiectul {{project}}. -
- {{redirect}} -
-
+

+{{cta}} +

Dacă nu ai solicitat acest mail, te rugăm frumos să îl ignori. -
-
+

+

Cu drag,
Echipa {{project}} -

+

diff --git a/app/config/locales/templates/ru.email.auth.confirm.tpl b/app/config/locales/templates/ru.email.auth.confirm.tpl index 29993e988f..7e1c976c63 100644 --- a/app/config/locales/templates/ru.email.auth.confirm.tpl +++ b/app/config/locales/templates/ru.email.auth.confirm.tpl @@ -1,24 +1,15 @@ - - -
+

Здравствуйте, {{name}}, -
-
+

+

Перейдите по ссылке, чтобы подтвердить свой адрес электронной почты. -
- {{redirect}} -
-
+

+{{cta}} +

Если вы не запрашивали подтверждение этого адреса, проигнорируйте это сообщение. -
-
+

+

Спасибо,
команда {{project}} -

\ No newline at end of file +

\ No newline at end of file diff --git a/app/config/locales/templates/ru.email.auth.invitation.tpl b/app/config/locales/templates/ru.email.auth.invitation.tpl index 41de21d09f..7dc637a3de 100644 --- a/app/config/locales/templates/ru.email.auth.invitation.tpl +++ b/app/config/locales/templates/ru.email.auth.invitation.tpl @@ -1,27 +1,18 @@ - - -
+

Здравствуйте, -
-
+

+

Это письмо отправлено вам, потому что {{owner}} приглашает стать членом команды {{team}} в проекте {{project}}. -
-
+

+

Перейдите по ссылке, чтобы присоединиться к команде {{team}} : -
- {{redirect}} -
-
+

+{{cta}} +

Если вы не заинтересованы, проигнорируйте это сообщение. -
-
+

+

Спасибо,
команда {{project}} -

+

diff --git a/app/config/locales/templates/ru.email.auth.recovery.tpl b/app/config/locales/templates/ru.email.auth.recovery.tpl index 680278e42c..cdcc25ac05 100644 --- a/app/config/locales/templates/ru.email.auth.recovery.tpl +++ b/app/config/locales/templates/ru.email.auth.recovery.tpl @@ -1,24 +1,15 @@ - - -
+

Здравствуйте, {{name}}, -
-
+

+

Перейдите по ссылке, чтобы сбросить пароль для проекта {{project}}. -
- {{redirect}} -
-
+

+{{cta}} +

Если вы не запрашивали сброс пароля, проигнорируйте это сообщение. -
-
+

+

Спасибо,
команда {{project}} -

+

diff --git a/app/config/locales/templates/si.email.auth.confirm.tpl b/app/config/locales/templates/si.email.auth.confirm.tpl index c7be6ec681..aa1bc4f273 100644 --- a/app/config/locales/templates/si.email.auth.confirm.tpl +++ b/app/config/locales/templates/si.email.auth.confirm.tpl @@ -1,25 +1,16 @@ - - -
+

ආයුබෝවන් {{name}}, -
-
+

+

ඔබගේ විද්‍යුත් තැපැල් ලිපිනය සත්‍යාපනය කිරීමට මෙම සබැඳිය අනුගමනය කරන්න. -
- {{redirect}} -
-
+

+{{cta}} +

ඔබ මෙම ලිපිනය සත්‍යාපනය කිරීමට ඉල්ලා නොසිටියේ නම්, ඔබට මෙම පණිවිඩය නොසලකා හැරිය හැක. -
-
+

+

ස්තූතියි,
{{project}} කණ්ඩායම -

\ No newline at end of file +

\ No newline at end of file diff --git a/app/config/locales/templates/si.email.auth.invitation.tpl b/app/config/locales/templates/si.email.auth.invitation.tpl index 1a0842d2e8..2fa979751e 100644 --- a/app/config/locales/templates/si.email.auth.invitation.tpl +++ b/app/config/locales/templates/si.email.auth.invitation.tpl @@ -1,31 +1,22 @@ - - -
+

ආයුබෝවන්, -
-
+

+

මෙම තැපෑල ඔබ වෙත එවනු ලැබුවේ {{owner}}ට ඔබව , {{project}} ව්‍යාපෘතියෙහි {{team}} කණ්ඩායමේ කණ්ඩායම් සාමාජිකයෙකු වීමට ආරාධනා කිරීමට අවශ්‍ය වූ නිසාය. -
-
+

+

{{team}} කණ්ඩායමට එක්වීමට මෙම සබැඳිය අනුගමනය කරන්න: -
- {{redirect}} -
-
+

+{{cta}} +

ඔබ උනන්දුවක් නොදක්වන්නේ නම්, ඔබට මෙම පණිවිඩය නොසලකා හැරිය හැකිය. -
-
+

+

ස්තූතියි,
{{project}} කණ්ඩායම. -

\ No newline at end of file +

\ No newline at end of file diff --git a/app/config/locales/templates/si.email.auth.recovery.tpl b/app/config/locales/templates/si.email.auth.recovery.tpl index bcf32b61ba..4df6902070 100644 --- a/app/config/locales/templates/si.email.auth.recovery.tpl +++ b/app/config/locales/templates/si.email.auth.recovery.tpl @@ -1,25 +1,15 @@ - - -
+

ආයුබෝවන් {{name}}, -
-
+

+

ඔබගේ {{project}} ව්‍යාපෘතියෙහි මුරපදය නැවත සැකසීමට මෙම සබැඳිය අනුගමනය කරන්න - -
- {{redirect}} -
-
+

+{{cta}} +

ඔබගේ මුරපදය නැවත සැකසීමට ඔබ ඉල්ලා නොසිටියේ නම්, ඔබට මෙම පණිවිඩය නොසලකා හැරිය හැක. -
-
+

+

ස්තූතියි,
{{project}} කණ්ඩායම -

\ No newline at end of file +

\ No newline at end of file diff --git a/app/config/locales/templates/sl.email.auth.confirm.tpl b/app/config/locales/templates/sl.email.auth.confirm.tpl index e7f15272da..fb6c4a59d1 100644 --- a/app/config/locales/templates/sl.email.auth.confirm.tpl +++ b/app/config/locales/templates/sl.email.auth.confirm.tpl @@ -1,24 +1,15 @@ - - -
+

Pozdravljeni {{name}}, -
-
+

+

Sledite tej povezavi za potrditev vašega email naslova. -
- {{redirect}} -
-
+

+{{cta}} +

Če niste zahtevali potrditve tega naslova, lahko to sporočilo prezrete. -
-
+

+

Hvala,
{{project}} ekipa -

\ No newline at end of file +

\ No newline at end of file diff --git a/app/config/locales/templates/sl.email.auth.invitation.tpl b/app/config/locales/templates/sl.email.auth.invitation.tpl index 13467789a3..0dc64594b0 100644 --- a/app/config/locales/templates/sl.email.auth.invitation.tpl +++ b/app/config/locales/templates/sl.email.auth.invitation.tpl @@ -1,27 +1,18 @@ - - -
+

Pozdravljeni, -
-
+

+

To sporočilo vam je bilo posredovano, ker vas je {{owner}} povabil/a, da postanete član {{team}} ekipe za {{project}}. -
-
+

+

Sledite tej povezavi, da se pridružite {{team}} ekipi: -
- {{redirect}} -
-
+

+{{cta}} +

Če vas ne zanima, lahko to sporočilo prezrete. -
-
+

+

Hvala,
{{project}} ekipa -

+

diff --git a/app/config/locales/templates/sl.email.auth.recovery.tpl b/app/config/locales/templates/sl.email.auth.recovery.tpl index 701805e12e..46b0e4ee5e 100644 --- a/app/config/locales/templates/sl.email.auth.recovery.tpl +++ b/app/config/locales/templates/sl.email.auth.recovery.tpl @@ -1,24 +1,15 @@ - - -
+

Pozdravljeni {{name}}, -
-
+

+

Za ponastavitev vašega {{project}} gesla sledite tej povezavi. -
- {{redirect}} -
-
+

+{{cta}} +

Če niste zahtevali ponastavitve gesla, lahko to sporočilo prezrete. -
-
+

+

Hvala,
{{project}} ekipa -

+

diff --git a/app/config/locales/templates/sv.email.auth.confirm.tpl b/app/config/locales/templates/sv.email.auth.confirm.tpl index 0b1e4451ff..dc406e1857 100644 --- a/app/config/locales/templates/sv.email.auth.confirm.tpl +++ b/app/config/locales/templates/sv.email.auth.confirm.tpl @@ -1,24 +1,15 @@ - - -
+

Hej {{name}}, -
-
+

+

Vänligen följ länken nedan för att verifiera din epostadress. -
- {{redirect}} -
-
+

+{{cta}} +

Om du inte vill verifiera din epostadress så kan du ignorera detta meddelande. -
-
+

+

Tack,
{{project}}-teamet -

\ No newline at end of file +

\ No newline at end of file diff --git a/app/config/locales/templates/sv.email.auth.invitation.tpl b/app/config/locales/templates/sv.email.auth.invitation.tpl index 604e94227d..12f4ddf858 100644 --- a/app/config/locales/templates/sv.email.auth.invitation.tpl +++ b/app/config/locales/templates/sv.email.auth.invitation.tpl @@ -1,27 +1,18 @@ - - -
+

Hej, -
-
+

+

{{owner}} vill bjuda in dig att bli del av {{team}}-teamet inom {{project}}. -
-
+

+

Följ denna länk för att bli del av {{team}}-teamet: -
- {{redirect}} -
-
+

+{{cta}} +

Om du inte är intresserad så kan du ignorera detta meddelande. -
-
+

+

Tack,
{{project}}-teamet -

+

diff --git a/app/config/locales/templates/sv.email.auth.recovery.tpl b/app/config/locales/templates/sv.email.auth.recovery.tpl index 673d914bd1..d781ed1075 100644 --- a/app/config/locales/templates/sv.email.auth.recovery.tpl +++ b/app/config/locales/templates/sv.email.auth.recovery.tpl @@ -1,24 +1,15 @@ - - -
+

Hej {{name}}, -
-
+

+

Följ denna länk för att ändra ditt {{project}} lösenord. -
- {{redirect}} -
-
+

+{{cta}} +

Om du inte bett om att ändra ditt lösenord så kan du ignorera detta meddelande. -
-
+

+

Tack,
{{project}}-teamet -

+

diff --git a/app/config/locales/templates/ta.email.auth.confirm.tpl b/app/config/locales/templates/ta.email.auth.confirm.tpl index 25a34d067c..843ce248e1 100644 --- a/app/config/locales/templates/ta.email.auth.confirm.tpl +++ b/app/config/locales/templates/ta.email.auth.confirm.tpl @@ -1,24 +1,15 @@ - - -
+

வணக்கம் {{name}}, -
-
+

+

இந்த இணைப்பைப் பின்தொடர்ந்து உங்கள் மின்னஞ்சல் முகவரியை சரிபார்க்கவும். -
- {{redirect}} -
-
+

+{{cta}} +

இந்த முகவரியை சரிபார்க்க நீங்கள் கேட்கவில்லை என்றால், இந்த செய்தியை நீங்கள் புறக்கணிக்கலாம். -
-
+

+

நன்றி,
{{project}} குழு -

+

diff --git a/app/config/locales/templates/ta.email.auth.invitation.tpl b/app/config/locales/templates/ta.email.auth.invitation.tpl index b9c417c269..00ec55d08a 100644 --- a/app/config/locales/templates/ta.email.auth.invitation.tpl +++ b/app/config/locales/templates/ta.email.auth.invitation.tpl @@ -1,27 +1,18 @@ - - -
+

வணக்கம், -
-
+

+

இந்த மின்னஞ்சல் உங்களுக்கு அனுப்பப்பட்டது, ஏனெனில் {{owner}} உங்களை {{team}} குழுவின் {{project}} திட்டத்தில் உறுப்பினராக அழைக்க விரும்பினார்கள். -
-
+

+

இந்த இணைப்பைப் பின்தொடர்ந்து {{team}} குழுவில் சேரவும்: -
- {{redirect}} -
-
+

+{{cta}} +

இந்த முகவரியை சரிபார்க்க நீங்கள் கேட்கவில்லை என்றால், இந்த செய்தியை நீங்கள் புறக்கணிக்கலாம். -
-
+

+

நன்றி,
{{project}} குழு -

+

diff --git a/app/config/locales/templates/ta.email.auth.recovery.tpl b/app/config/locales/templates/ta.email.auth.recovery.tpl index 455a6cf25b..36e9707f8d 100644 --- a/app/config/locales/templates/ta.email.auth.recovery.tpl +++ b/app/config/locales/templates/ta.email.auth.recovery.tpl @@ -1,24 +1,15 @@ - - -
+

வணக்கம் {{name}}, -
-
+

+

இந்த இணைப்பைப் பின்தொடர்ந்து உங்கள் {{project}} திட்டத்தின் கடவுச்சொல்லை மீட்கவும். -
- {{redirect}} -
-
+

+{{cta}} +

உங்கள் கடவுச்சொல்லை மீட்டமைக்க நீங்கள் கேட்கவில்லை என்றால், இந்த செய்தியை நீங்கள் புறக்கணிக்கலாம். -
-
+

+

நன்றி,
{{project}} குழு -

+

diff --git a/app/config/locales/templates/th.email.auth.confirm.tpl b/app/config/locales/templates/th.email.auth.confirm.tpl index 99a6268ea7..c45fbf7d5d 100644 --- a/app/config/locales/templates/th.email.auth.confirm.tpl +++ b/app/config/locales/templates/th.email.auth.confirm.tpl @@ -1,24 +1,15 @@ - - -
+

สวัสดี {{name}}, -
-
+

+

ตามไปยังลิงค์นี้เพื่อยืนยันที่อยู่อีเมลของคุณ -
- {{redirect}} -
-
+

+{{cta}} +

หากคุณไม่ได้ขอให้ยืนยันที่อยู่นี้คุณสามารถเพิกเฉยต่อข้อความนี้ได้ -
-
+

+

ขอบคุณ,
ทีม {{project}} -

+

diff --git a/app/config/locales/templates/th.email.auth.invitation.tpl b/app/config/locales/templates/th.email.auth.invitation.tpl index 399e47c686..918b28cc8f 100644 --- a/app/config/locales/templates/th.email.auth.invitation.tpl +++ b/app/config/locales/templates/th.email.auth.invitation.tpl @@ -1,28 +1,18 @@ - - -
+

สวัสดี, -
-
- +

+

เมลนี้ส่งถึงคุณเพราะ {{owner}} ต้องการเชิญคุณเป็นสมาชิกในทีม {{team}} ของ {{project}}. -
-
+

+

ตามลิงค์นี้เพื่อเข้าร่วม {{team}} ทีม: -
- {{redirect}} -
-
+

+{{cta}} +

หากคุณไม่สนใจคุณสามารถละเว้นข้อความนี้ -
-
+

+

ขอบคุณ,
{{project}} ทีม -

+

diff --git a/app/config/locales/templates/th.email.auth.recovery.tpl b/app/config/locales/templates/th.email.auth.recovery.tpl index d835481dc4..220a193e19 100644 --- a/app/config/locales/templates/th.email.auth.recovery.tpl +++ b/app/config/locales/templates/th.email.auth.recovery.tpl @@ -1,24 +1,15 @@ - - -
+

สวัสดี {{name}}, -
-
+

+

ตามไปยังลิงค์นี้เพื่อรีเซ็ตรหัสผ่าน {{project}}. -
- {{redirect}} -
-
+

+{{cta}} +

หากคุณไม่ได้ขอให้ตั้งรหัสผ่านใหม่คุณสามารถเพิกเฉยต่อข้อความนี้ได้ -
-
+

+

ขอบคุณ,
ทีม {{project}} -

+

diff --git a/app/config/locales/templates/tr.email.auth.confirm.tpl b/app/config/locales/templates/tr.email.auth.confirm.tpl index d83bda1e67..0ed2fa54c4 100644 --- a/app/config/locales/templates/tr.email.auth.confirm.tpl +++ b/app/config/locales/templates/tr.email.auth.confirm.tpl @@ -1,24 +1,15 @@ - - -
+

Merhaba {{name}}, -
-
+

+

Aşağıdaki bağlantıyı takip ederek email hesabınızı doğrulayın. -
- {{redirect}} -
-
+

+{{cta}} +

Bu adresi doğrulamayı istemediyseniz, bu mesajı yok sayabilirsiniz. -
-
+

+

Teşekkürler,
{{project}} takımı -

\ No newline at end of file +

\ No newline at end of file diff --git a/app/config/locales/templates/tr.email.auth.invitation.tpl b/app/config/locales/templates/tr.email.auth.invitation.tpl index 07e8f06db4..c3c35b6bbd 100644 --- a/app/config/locales/templates/tr.email.auth.invitation.tpl +++ b/app/config/locales/templates/tr.email.auth.invitation.tpl @@ -1,27 +1,18 @@ - - -
+

Merhaba, -
-
+

+

Bu posta size gönderildi, çünkü {{owner}} sizi {{project}} için {{team}} ekibinde takım üyesi olmaya davet etmek istedi. -
-
+

+

Aşağıdaki bağlantıyı takip ederek {{team}} takımına takılın: -
- {{redirect}} -
-
+

+{{cta}} +

Eğer ilgilenmiyorsanız, bu mesajı yok sayabilirsiniz. -
-
+

+

Teşekkürler,
{{project}} takımı -

+

diff --git a/app/config/locales/templates/tr.email.auth.recovery.tpl b/app/config/locales/templates/tr.email.auth.recovery.tpl index d04ffce5ee..4e7ce73d8e 100644 --- a/app/config/locales/templates/tr.email.auth.recovery.tpl +++ b/app/config/locales/templates/tr.email.auth.recovery.tpl @@ -1,24 +1,15 @@ - - -
+

Merhaba {{name}}, -
-
+

+

Aşağıdaki bağlantıyı takip ederek {{project}} şifresini değiştirin. -
- {{redirect}} -
-
+

+{{cta}} +

Eğer şifrenizi sıfırlamayı istemediyseniz, bu mesajı yok sayabilirsiniz. -
-
+

+

Teşekkürler,
{{project}} takımı -

+

diff --git a/app/config/locales/templates/ua.email.auth.confirm.tpl b/app/config/locales/templates/ua.email.auth.confirm.tpl index 8d135457f0..ea628111cd 100644 --- a/app/config/locales/templates/ua.email.auth.confirm.tpl +++ b/app/config/locales/templates/ua.email.auth.confirm.tpl @@ -1,24 +1,15 @@ - - -
+

Вітаємо {{name}}, -
-
+

+

Перейдіть за цим посиланням, та підтвердіть свою електронну адресу -
- {{redirect}} -
-
+

+{{cta}} +

Якщо ви не запитували підтвердження цієї адреси, проігноруйте це повідомлення. -
-
+

+

Дякуємо,
команда {{project}} -

\ No newline at end of file +

\ No newline at end of file diff --git a/app/config/locales/templates/ua.email.auth.invitation.tpl b/app/config/locales/templates/ua.email.auth.invitation.tpl index 8daaecbaaf..dc4d3aed02 100644 --- a/app/config/locales/templates/ua.email.auth.invitation.tpl +++ b/app/config/locales/templates/ua.email.auth.invitation.tpl @@ -1,27 +1,18 @@ - - -
+

Вітаємо, -
-
+

+

Цей лист був надісланий вам тому що {{owner}} хоче запросити вас стати членом команди {{team}} у {{project}}. -
-
+

+

Перейдіть за цим посиланням щоб приєднатись до команди {{team}} : -
- {{redirect}} -
-
+

+{{cta}} +

Якщо ви не зацікавлені, проігноруйте це повідомлення. -
-
+

+

Дякуємо,
Команда {{project}} -

+

diff --git a/app/config/locales/templates/ua.email.auth.recovery.tpl b/app/config/locales/templates/ua.email.auth.recovery.tpl index b833a6575a..0e1431bad9 100644 --- a/app/config/locales/templates/ua.email.auth.recovery.tpl +++ b/app/config/locales/templates/ua.email.auth.recovery.tpl @@ -1,24 +1,15 @@ - - -
+

Вітаємо, {{name}}, -
-
+

+

Перейдіть за цим посиланням для того щоб скинути свій пароль для {{project}} . -
- {{redirect}} -
-
+

+{{cta}} +

Якщо ви не запитували скидання паролю, проігноруйте це повідомлення. -
-
+

+

Дякуємо,
команда {{project}} -

+

diff --git a/app/config/locales/templates/vi.email.auth.confirm.tpl b/app/config/locales/templates/vi.email.auth.confirm.tpl index 40c7d496e1..463d26b1f3 100644 --- a/app/config/locales/templates/vi.email.auth.confirm.tpl +++ b/app/config/locales/templates/vi.email.auth.confirm.tpl @@ -1,24 +1,15 @@ - - -
+

Xin chào {{name}}, -
-
+

+

Hãy vào liên kết này để xác nhận địa chỉ email của bạn. -
- {{redirect}} -
-
+

+{{cta}} +

Xin hãy bỏ qua email này nếu bạn không yêu cầu xác nhận địa chỉ này. -
-
+

+

Xin cảm ơn,
{{project}} team -

\ No newline at end of file +

\ No newline at end of file diff --git a/app/config/locales/templates/vi.email.auth.invitation.tpl b/app/config/locales/templates/vi.email.auth.invitation.tpl index 387c537f55..8d222ea8c7 100644 --- a/app/config/locales/templates/vi.email.auth.invitation.tpl +++ b/app/config/locales/templates/vi.email.auth.invitation.tpl @@ -1,27 +1,18 @@ - - -
+

Xin chào, -
-
+

+

Bạn nhận được email này vì {{owner}} muốn mời bạn tham gia {{project}} cùng với {{team}} team. -
-
+

+

Hãy theo liên kết này để tham gia vào {{team}} team: -
- {{redirect}} -
-
+

+{{cta}} +

Nếu bạn không thích tham gia, hãy bỏ qua lời nhắn này. -
-
+

+

Xin cảm ơn,
{{project}} team -

+

diff --git a/app/config/locales/templates/vi.email.auth.recovery.tpl b/app/config/locales/templates/vi.email.auth.recovery.tpl index 57c704f0cd..9c7feb4900 100644 --- a/app/config/locales/templates/vi.email.auth.recovery.tpl +++ b/app/config/locales/templates/vi.email.auth.recovery.tpl @@ -1,24 +1,15 @@ - - -
+

Xin chào {{name}}, -
-
+

+

Hãy theo liên kết này để khôi phục mật khẩu của bạn ở {{project}}. -
- {{redirect}} -
-
+

+{{cta}} +

Hãy bỏ qua lời nhắn này nếu bạn không yêu cầu khôi phục mật khẩu. -
-
+

+

Xin cảm ơn,
{{project}} team -

+

diff --git a/app/config/locales/templates/zh-cn.email.auth.confirm.tpl b/app/config/locales/templates/zh-cn.email.auth.confirm.tpl index a557d451c7..9a0d7a3c8c 100644 --- a/app/config/locales/templates/zh-cn.email.auth.confirm.tpl +++ b/app/config/locales/templates/zh-cn.email.auth.confirm.tpl @@ -1,24 +1,15 @@ - - -
+

{{name}} 你好, -
-
+

+

请点击下方的链接验证你的电子邮箱地址。 -
- {{redirect}} -
-
+

+{{cta}} +

如果你没有请求验证本邮箱,请忽略这份邮件。 -
-
+

+

谢谢。
来自 {{project}} -

\ No newline at end of file +

\ No newline at end of file diff --git a/app/config/locales/templates/zh-cn.email.auth.invitation.tpl b/app/config/locales/templates/zh-cn.email.auth.invitation.tpl index 7ee56a401a..4ef8caba5c 100644 --- a/app/config/locales/templates/zh-cn.email.auth.invitation.tpl +++ b/app/config/locales/templates/zh-cn.email.auth.invitation.tpl @@ -1,27 +1,18 @@ - - -
+

你好, -
-
+

+

{{owner}}邀请您加入{{project}}项目的{{team}}小组。 -
-
+

+

请点击下方的链接加入{{team}}小组: -
- {{redirect}} -
-
+

+{{cta}} +

如果您没有加入该小组的计划,请忽略本邮件。 -
-
+

+

谢谢。
来自 {{project}} -

+

diff --git a/app/config/locales/templates/zh-cn.email.auth.recovery.tpl b/app/config/locales/templates/zh-cn.email.auth.recovery.tpl index 8a473b5537..1144b36b3c 100644 --- a/app/config/locales/templates/zh-cn.email.auth.recovery.tpl +++ b/app/config/locales/templates/zh-cn.email.auth.recovery.tpl @@ -1,25 +1,16 @@ - - -
+

{{name}} 你好, -
-
+

+

请点击下方的链接重新设置{{project}}的密码。 Follow this link to reset your {{project}} password. -
- {{redirect}} -
-
+

+{{cta}} +

如果您未曾申请重设密码,请忽略本邮件。 -
-
+

+

谢谢。
来自 {{project}} -

+

diff --git a/app/config/locales/templates/zh-tw.email.auth.confirm.tpl b/app/config/locales/templates/zh-tw.email.auth.confirm.tpl index 9ba081bb91..7a19636dbb 100644 --- a/app/config/locales/templates/zh-tw.email.auth.confirm.tpl +++ b/app/config/locales/templates/zh-tw.email.auth.confirm.tpl @@ -1,24 +1,15 @@ - - -
+

{{name}} 你好, -
-
+

+

請點擊下方的鏈接驗證你的電子郵箱地址。 -
- {{redirect}} -
-
+

+{{cta}} +

如果你沒有請求驗證本郵箱,請忽略這份郵件。 -
-
+

+

謝謝。
來自 {{project}} -

\ No newline at end of file +

\ No newline at end of file diff --git a/app/config/locales/templates/zh-tw.email.auth.invitation.tpl b/app/config/locales/templates/zh-tw.email.auth.invitation.tpl index 08fb921fd7..b5e6cc8225 100644 --- a/app/config/locales/templates/zh-tw.email.auth.invitation.tpl +++ b/app/config/locales/templates/zh-tw.email.auth.invitation.tpl @@ -1,27 +1,18 @@ - - -
+

你好, -
-
+

+

{{owner}}邀請您加入{{project}}項目的{{team}}小組。 -
-
+

+

請點擊下方的鏈接加入{{team}}小組: -
- {{redirect}} -
-
+

+{{cta}} +

如果您沒有加入該小組的計劃,請忽略本郵件。 -
-
+

+

謝謝。
來自 {{project}} -

\ No newline at end of file +

\ No newline at end of file diff --git a/app/config/locales/templates/zh-tw.email.auth.recovery.tpl b/app/config/locales/templates/zh-tw.email.auth.recovery.tpl index 8a473b5537..1144b36b3c 100644 --- a/app/config/locales/templates/zh-tw.email.auth.recovery.tpl +++ b/app/config/locales/templates/zh-tw.email.auth.recovery.tpl @@ -1,25 +1,16 @@ - - -
+

{{name}} 你好, -
-
+

+

请点击下方的链接重新设置{{project}}的密码。 Follow this link to reset your {{project}} password. -
- {{redirect}} -
-
+

+{{cta}} +

如果您未曾申请重设密码,请忽略本邮件。 -
-
+

+

谢谢。
来自 {{project}} -

+

diff --git a/app/config/platforms.php b/app/config/platforms.php index f05dcde39b..a0b4955a8e 100644 --- a/app/config/platforms.php +++ b/app/config/platforms.php @@ -21,7 +21,7 @@ return [ 'beta' => false, 'family' => APP_PLATFORM_CLIENT, 'prism' => 'javascript', - 'source' => realpath(__DIR__ . '/../sdks/client-web'), + 'source' => \realpath(__DIR__ . '/../sdks/client-web'), 'gitUrl' => 'git@github.com:appwrite/sdk-for-js.git', 'gitRepoName' => 'sdk-for-js', 'gitUserName' => 'appwrite', @@ -35,7 +35,7 @@ return [ 'beta' => true, 'family' => APP_PLATFORM_CLIENT, 'prism' => 'dart', - 'source' => realpath(__DIR__ . '/../sdks/client-flutter'), + 'source' => \realpath(__DIR__ . '/../sdks/client-flutter'), 'gitUrl' => 'git@github.com:appwrite/sdk-for-flutter.git', 'gitRepoName' => 'sdk-for-flutter', 'gitUserName' => 'appwrite', @@ -110,7 +110,7 @@ return [ 'beta' => false, 'family' => APP_PLATFORM_CONSOLE, 'prism' => 'console', - 'source' => realpath(__DIR__ . '/../sdks/console-web'), + 'source' => \realpath(__DIR__ . '/../sdks/console-web'), 'gitUrl' => null, 'gitRepoName' => 'sdk-for-console', 'gitUserName' => 'appwrite', @@ -134,7 +134,7 @@ return [ 'beta' => false, 'family' => APP_PLATFORM_SERVER, 'prism' => 'javascript', - 'source' => realpath(__DIR__ . '/../sdks/server-nodejs'), + 'source' => \realpath(__DIR__ . '/../sdks/server-nodejs'), 'gitUrl' => 'git@github.com:appwrite/sdk-for-node.git', 'gitRepoName' => 'sdk-for-node', 'gitUserName' => 'appwrite', @@ -148,7 +148,7 @@ return [ 'beta' => true, 'family' => APP_PLATFORM_SERVER, 'prism' => 'typescript', - 'source' => realpath(__DIR__ . '/../sdks/server-deno'), + 'source' => \realpath(__DIR__ . '/../sdks/server-deno'), 'gitUrl' => 'git@github.com:appwrite/sdk-for-deno.git', 'gitRepoName' => 'sdk-for-deno', 'gitUserName' => 'appwrite', @@ -162,7 +162,7 @@ return [ 'beta' => false, 'family' => APP_PLATFORM_SERVER, 'prism' => 'php', - 'source' => realpath(__DIR__ . '/../sdks/server-php'), + 'source' => \realpath(__DIR__ . '/../sdks/server-php'), 'gitUrl' => 'git@github.com:appwrite/sdk-for-php.git', 'gitRepoName' => 'sdk-for-php', 'gitUserName' => 'appwrite', @@ -176,7 +176,7 @@ return [ 'beta' => true, 'family' => APP_PLATFORM_SERVER, 'prism' => 'python', - 'source' => realpath(__DIR__ . '/../sdks/server-python'), + 'source' => \realpath(__DIR__ . '/../sdks/server-python'), 'gitUrl' => 'git@github.com:appwrite/sdk-for-python.git', 'gitRepoName' => 'sdk-for-python', 'gitUserName' => 'appwrite', @@ -190,7 +190,7 @@ return [ 'beta' => true, 'family' => APP_PLATFORM_SERVER, 'prism' => 'ruby', - 'source' => realpath(__DIR__ . '/../sdks/server-ruby'), + 'source' => \realpath(__DIR__ . '/../sdks/server-ruby'), 'gitUrl' => 'git@github.com:appwrite/sdk-for-ruby.git', 'gitRepoName' => 'sdk-for-ruby', 'gitUserName' => 'appwrite', @@ -204,7 +204,7 @@ return [ 'beta' => true, 'family' => APP_PLATFORM_SERVER, 'prism' => 'go', - 'source' => realpath(__DIR__ . '/../sdks/server-go'), + 'source' => \realpath(__DIR__ . '/../sdks/server-go'), 'gitUrl' => 'git@github.com:appwrite/sdk-for-go.git', 'gitRepoName' => 'sdk-for-go', 'gitUserName' => 'appwrite', @@ -218,7 +218,7 @@ return [ 'beta' => true, 'family' => APP_PLATFORM_SERVER, 'prism' => 'java', - 'source' => realpath(__DIR__ . '/../sdks/server-java'), + 'source' => \realpath(__DIR__ . '/../sdks/server-java'), 'gitUrl' => 'git@github.com:appwrite/sdk-for-java.git', 'gitRepoName' => 'sdk-for-java', 'gitUserName' => 'appwrite', @@ -232,7 +232,7 @@ return [ 'beta' => true, 'family' => APP_PLATFORM_SERVER, 'prism' => 'java', - 'source' => realpath(__DIR__ . '/../sdks/server-dart'), + 'source' => \realpath(__DIR__ . '/../sdks/server-dart'), 'gitUrl' => 'git@github.com:appwrite/sdk-for-dart.git', 'gitRepoName' => 'sdk-for-dart', 'gitUserName' => 'appwrite', diff --git a/app/config/providers.php b/app/config/providers.php index b1b086a865..505c2898c4 100644 --- a/app/config/providers.php +++ b/app/config/providers.php @@ -74,7 +74,7 @@ return [ // Ordered by ABC. 'mock' => false, ], 'google' => [ - 'developers' => 'https://developers.google.com/', + 'developers' => 'https://support.google.com/googleapi/answer/6158849', 'icon' => 'icon-google', 'enabled' => true, 'form' => false, diff --git a/app/config/roles.php b/app/config/roles.php index 9bd12bdf22..0a4ec4369c 100644 --- a/app/config/roles.php +++ b/app/config/roles.php @@ -67,19 +67,19 @@ return [ ], ROLE_MEMBER => [ 'label' => 'Member', - 'scopes' => array_merge($logged, []), + 'scopes' => \array_merge($logged, []), ], ROLE_ADMIN => [ 'label' => 'Admin', - 'scopes' => array_merge($admins, []), + 'scopes' => \array_merge($admins, []), ], ROLE_DEVELOPER => [ 'label' => 'Developer', - 'scopes' => array_merge($admins, []), + 'scopes' => \array_merge($admins, []), ], ROLE_OWNER => [ 'label' => 'Owner', - 'scopes' => array_merge($logged, $admins, []), + 'scopes' => \array_merge($logged, $admins, []), ], ROLE_APP => [ 'label' => 'Application', diff --git a/app/controllers/api/account.php b/app/controllers/api/account.php index 282a5d9a22..3bce54eafa 100644 --- a/app/controllers/api/account.php +++ b/app/controllers/api/account.php @@ -1,7 +1,7 @@ getServer('_APP_HOME').'/auth/oauth2/success'; $oauthDefaultFailure = $request->getServer('_APP_HOME').'/auth/oauth2/failure'; @@ -41,14 +40,14 @@ $utopia->init(function() use (&$oauth2Keys) { continue; } - $oauth2Keys[] = 'oauth2'.ucfirst($key); - $oauth2Keys[] = 'oauth2'.ucfirst($key).'AccessToken'; + $oauth2Keys[] = 'oauth2'.\ucfirst($key); + $oauth2Keys[] = 'oauth2'.\ucfirst($key).'AccessToken'; } - -}); +}, 'account'); $utopia->post('/v1/account') ->desc('Create Account') + ->groups(['api', 'account']) ->label('webhook', 'account.create') ->label('scope', 'public') ->label('sdk.platform', [APP_PLATFORM_CLIENT]) @@ -60,21 +59,21 @@ $utopia->post('/v1/account') ->param('password', '', function () { return new Password(); }, 'User password. Must be between 6 to 32 chars.') ->param('name', '', function () { return new Text(100); }, 'User name.', true) ->action( - function ($email, $password, $name) use ($register, $request, $response, $audit, $projectDB, $project, $webhook, $oauth2Keys) { + function ($email, $password, $name) use ($request, $response, $audit, $projectDB, $project, $webhook, $oauth2Keys) { if ('console' === $project->getId()) { $whitlistEmails = $project->getAttribute('authWhitelistEmails'); $whitlistIPs = $project->getAttribute('authWhitelistIPs'); $whitlistDomains = $project->getAttribute('authWhitelistDomains'); - if (!empty($whitlistEmails) && !in_array($email, $whitlistEmails)) { + if (!empty($whitlistEmails) && !\in_array($email, $whitlistEmails)) { throw new Exception('Console registration is restricted to specific emails. Contact your administrator for more information.', 401); } - if (!empty($whitlistIPs) && !in_array($request->getIP(), $whitlistIPs)) { + if (!empty($whitlistIPs) && !\in_array($request->getIP(), $whitlistIPs)) { throw new Exception('Console registration is restricted to specific IPs. Contact your administrator for more information.', 401); } - if (!empty($whitlistDomains) && !in_array(substr(strrchr($email, '@'), 1), $whitlistDomains)) { + if (!empty($whitlistDomains) && !\in_array(\substr(\strrchr($email, '@'), 1), $whitlistDomains)) { throw new Exception('Console registration is restricted to specific domains. Contact your administrator for more information.', 401); } } @@ -105,8 +104,8 @@ $utopia->post('/v1/account') 'emailVerification' => false, 'status' => Auth::USER_STATUS_UNACTIVATED, 'password' => Auth::passwordHash($password), - 'password-update' => time(), - 'registration' => time(), + 'password-update' => \time(), + 'registration' => \time(), 'reset' => false, 'name' => $name, ], ['email' => $email]); @@ -135,7 +134,7 @@ $utopia->post('/v1/account') $response ->setStatusCode(Response::STATUS_CODE_CREATED) - ->json(array_merge($user->getArrayCopy(array_merge( + ->json(\array_merge($user->getArrayCopy(\array_merge( [ '$id', 'email', @@ -149,6 +148,7 @@ $utopia->post('/v1/account') $utopia->post('/v1/account/sessions') ->desc('Create Account Session') + ->groups(['api', 'account']) ->label('webhook', 'account.sessions.create') ->label('scope', 'public') ->label('sdk.platform', [APP_PLATFORM_CLIENT]) @@ -181,7 +181,7 @@ $utopia->post('/v1/account/sessions') throw new Exception('Invalid credentials', 401); // Wrong password or username } - $expiry = time() + Auth::TOKEN_EXPIRATION_LOGIN_LONG; + $expiry = \time() + Auth::TOKEN_EXPIRATION_LOGIN_LONG; $secret = Auth::tokenGenerator(); $session = new Document([ '$collection' => Database::SYSTEM_COLLECTION_TOKENS, @@ -222,9 +222,9 @@ $utopia->post('/v1/account/sessions') ->setParam('resource', 'users/'.$profile->getId()) ; - if(!Config::getParam('domainVerification')) { + if (!Config::getParam('domainVerification')) { $response - ->addHeader('X-Fallback-Cookies', json_encode([Auth::$cookieName => Auth::encodeSession($profile->getId(), $secret)])) + ->addHeader('X-Fallback-Cookies', \json_encode([Auth::$cookieName => Auth::encodeSession($profile->getId(), $secret)])) ; } @@ -239,6 +239,7 @@ $utopia->post('/v1/account/sessions') $utopia->get('/v1/account/sessions/oauth2/:provider') ->desc('Create Account Session with OAuth2') + ->groups(['api', 'account']) ->label('error', __DIR__.'/../../views/general/error.phtml') ->label('scope', 'public') ->label('sdk.platform', [APP_PLATFORM_CLIENT]) @@ -250,34 +251,35 @@ $utopia->get('/v1/account/sessions/oauth2/:provider') ->label('sdk.methodType', 'webAuth') ->label('abuse-limit', 50) ->label('abuse-key', 'ip:{ip}') - ->param('provider', '', function () { return new WhiteList(array_keys(Config::getParam('providers'))); }, 'OAuth2 Provider. Currently, supported providers are: ' . implode(', ', array_keys(array_filter(Config::getParam('providers'), function($node) {return (!$node['mock']);}))).'.') + ->param('provider', '', function () { return new WhiteList(\array_keys(Config::getParam('providers'))); }, 'OAuth2 Provider. Currently, supported providers are: ' . \implode(', ', \array_keys(\array_filter(Config::getParam('providers'), function($node) {return (!$node['mock']);}))).'.') ->param('success', $oauthDefaultSuccess, function () use ($clients) { return new Host($clients); }, 'URL to redirect back to your app after a successful login attempt. Only URLs from hostnames in your project platform list are allowed. This requirement helps to prevent an [open redirect](https://cheatsheetseries.owasp.org/cheatsheets/Unvalidated_Redirects_and_Forwards_Cheat_Sheet.html) attack against your project API.', true) ->param('failure', $oauthDefaultFailure, function () use ($clients) { return new Host($clients); }, 'URL to redirect back to your app after a failed login attempt. Only URLs from hostnames in your project platform list are allowed. This requirement helps to prevent an [open redirect](https://cheatsheetseries.owasp.org/cheatsheets/Unvalidated_Redirects_and_Forwards_Cheat_Sheet.html) attack against your project API.', true) + ->param('scopes', [], function () { return new ArrayList(new Text(128)); }, 'A list of custom OAuth2 scopes. Check each provider internal docs for a list of supported scopes.', true) ->action( - function ($provider, $success, $failure) use ($response, $request, $project) { + function ($provider, $success, $failure, $scopes) use ($response, $request, $project) { $protocol = Config::getParam('protocol'); $callback = $protocol.'://'.$request->getServer('HTTP_HOST').'/v1/account/sessions/oauth2/callback/'.$provider.'/'.$project->getId(); - $appId = $project->getAttribute('usersOauth2'.ucfirst($provider).'Appid', ''); - $appSecret = $project->getAttribute('usersOauth2'.ucfirst($provider).'Secret', '{}'); + $appId = $project->getAttribute('usersOauth2'.\ucfirst($provider).'Appid', ''); + $appSecret = $project->getAttribute('usersOauth2'.\ucfirst($provider).'Secret', '{}'); - $appSecret = json_decode($appSecret, true); + $appSecret = \json_decode($appSecret, true); if (!empty($appSecret) && isset($appSecret['version'])) { $key = $request->getServer('_APP_OPENSSL_KEY_V'.$appSecret['version']); - $appSecret = OpenSSL::decrypt($appSecret['data'], $appSecret['method'], $key, 0, hex2bin($appSecret['iv']), hex2bin($appSecret['tag'])); + $appSecret = OpenSSL::decrypt($appSecret['data'], $appSecret['method'], $key, 0, \hex2bin($appSecret['iv']), \hex2bin($appSecret['tag'])); } if (empty($appId) || empty($appSecret)) { throw new Exception('This provider is disabled. Please configure the provider app ID and app secret key from your '.APP_NAME.' console to continue.', 412); } - $classname = 'Appwrite\\Auth\\OAuth2\\'.ucfirst($provider); + $classname = 'Appwrite\\Auth\\OAuth2\\'.\ucfirst($provider); - if (!class_exists($classname)) { + if (!\class_exists($classname)) { throw new Exception('Provider is not supported', 501); } - $oauth2 = new $classname($appId, $appSecret, $callback, ['success' => $success, 'failure' => $failure]); + $oauth2 = new $classname($appId, $appSecret, $callback, ['success' => $success, 'failure' => $failure], $scopes); $response ->addHeader('Cache-Control', 'no-store, no-cache, must-revalidate, max-age=0') @@ -288,11 +290,12 @@ $utopia->get('/v1/account/sessions/oauth2/:provider') $utopia->get('/v1/account/sessions/oauth2/callback/:provider/:projectId') ->desc('OAuth2 Callback') + ->groups(['api', 'account']) ->label('error', __DIR__.'/../../views/general/error.phtml') ->label('scope', 'public') ->label('docs', false) ->param('projectId', '', function () { return new Text(1024); }, 'Project unique ID.') - ->param('provider', '', function () { return new WhiteList(array_keys(Config::getParam('providers'))); }, 'OAuth2 provider.') + ->param('provider', '', function () { return new WhiteList(\array_keys(Config::getParam('providers'))); }, 'OAuth2 provider.') ->param('code', '', function () { return new Text(1024); }, 'OAuth2 code.') ->param('state', '', function () { return new Text(2048); }, 'Login state params.', true) ->action( @@ -304,18 +307,19 @@ $utopia->get('/v1/account/sessions/oauth2/callback/:provider/:projectId') ->addHeader('Cache-Control', 'no-store, no-cache, must-revalidate, max-age=0') ->addHeader('Pragma', 'no-cache') ->redirect($protocol.'://'.$domain.'/v1/account/sessions/oauth2/'.$provider.'/redirect?' - .http_build_query(['project' => $projectId, 'code' => $code, 'state' => $state])); + .\http_build_query(['project' => $projectId, 'code' => $code, 'state' => $state])); } ); $utopia->post('/v1/account/sessions/oauth2/callback/:provider/:projectId') ->desc('OAuth2 Callback') + ->groups(['api', 'account']) ->label('error', __DIR__.'/../../views/general/error.phtml') ->label('scope', 'public') ->label('origin', '*') ->label('docs', false) ->param('projectId', '', function () { return new Text(1024); }, 'Project unique ID.') - ->param('provider', '', function () { return new WhiteList(array_keys(Config::getParam('providers'))); }, 'OAuth2 provider.') + ->param('provider', '', function () { return new WhiteList(\array_keys(Config::getParam('providers'))); }, 'OAuth2 provider.') ->param('code', '', function () { return new Text(1024); }, 'OAuth2 code.') ->param('state', '', function () { return new Text(2048); }, 'Login state params.', true) ->action( @@ -327,19 +331,20 @@ $utopia->post('/v1/account/sessions/oauth2/callback/:provider/:projectId') ->addHeader('Cache-Control', 'no-store, no-cache, must-revalidate, max-age=0') ->addHeader('Pragma', 'no-cache') ->redirect($protocol.'://'.$domain.'/v1/account/sessions/oauth2/'.$provider.'/redirect?' - .http_build_query(['project' => $projectId, 'code' => $code, 'state' => $state])); + .\http_build_query(['project' => $projectId, 'code' => $code, 'state' => $state])); } ); $utopia->get('/v1/account/sessions/oauth2/:provider/redirect') ->desc('OAuth2 Redirect') + ->groups(['api', 'account']) ->label('error', __DIR__.'/../../views/general/error.phtml') ->label('webhook', 'account.sessions.create') ->label('scope', 'public') ->label('abuse-limit', 50) ->label('abuse-key', 'ip:{ip}') ->label('docs', false) - ->param('provider', '', function () { return new WhiteList(array_keys(Config::getParam('providers'))); }, 'OAuth2 provider.') + ->param('provider', '', function () { return new WhiteList(\array_keys(Config::getParam('providers'))); }, 'OAuth2 provider.') ->param('code', '', function () { return new Text(1024); }, 'OAuth2 code.') ->param('state', '', function () { return new Text(2048); }, 'OAuth2 state params.', true) ->action( @@ -349,19 +354,19 @@ $utopia->get('/v1/account/sessions/oauth2/:provider/redirect') $defaultState = ['success' => $project->getAttribute('url', ''), 'failure' => '']; $validateURL = new URL(); - $appId = $project->getAttribute('usersOauth2'.ucfirst($provider).'Appid', ''); - $appSecret = $project->getAttribute('usersOauth2'.ucfirst($provider).'Secret', '{}'); + $appId = $project->getAttribute('usersOauth2'.\ucfirst($provider).'Appid', ''); + $appSecret = $project->getAttribute('usersOauth2'.\ucfirst($provider).'Secret', '{}'); - $appSecret = json_decode($appSecret, true); + $appSecret = \json_decode($appSecret, true); if (!empty($appSecret) && isset($appSecret['version'])) { $key = $request->getServer('_APP_OPENSSL_KEY_V'.$appSecret['version']); - $appSecret = OpenSSL::decrypt($appSecret['data'], $appSecret['method'], $key, 0, hex2bin($appSecret['iv']), hex2bin($appSecret['tag'])); + $appSecret = OpenSSL::decrypt($appSecret['data'], $appSecret['method'], $key, 0, \hex2bin($appSecret['iv']), \hex2bin($appSecret['tag'])); } - $classname = 'Appwrite\\Auth\\OAuth2\\'.ucfirst($provider); + $classname = 'Appwrite\\Auth\\OAuth2\\'.\ucfirst($provider); - if (!class_exists($classname)) { + if (!\class_exists($classname)) { throw new Exception('Provider is not supported', 501); } @@ -369,7 +374,7 @@ $utopia->get('/v1/account/sessions/oauth2/:provider/redirect') if (!empty($state)) { try { - $state = array_merge($defaultState, $oauth2->parseState($state)); + $state = \array_merge($defaultState, $oauth2->parseState($state)); } catch (\Exception $exception) { throw new Exception('Failed to parse login state params as passed from OAuth2 provider'); } @@ -417,7 +422,7 @@ $utopia->get('/v1/account/sessions/oauth2/:provider/redirect') 'first' => true, 'filters' => [ '$collection='.Database::SYSTEM_COLLECTION_USERS, - 'oauth2'.ucfirst($provider).'='.$oauth2ID, + 'oauth2'.\ucfirst($provider).'='.$oauth2ID, ], ]) : $user; @@ -445,8 +450,8 @@ $utopia->get('/v1/account/sessions/oauth2/:provider/redirect') 'emailVerification' => true, 'status' => Auth::USER_STATUS_ACTIVATED, // Email should already be authenticated by OAuth2 provider 'password' => Auth::passwordHash(Auth::passwordGenerator()), - 'password-update' => time(), - 'registration' => time(), + 'password-update' => \time(), + 'registration' => \time(), 'reset' => false, 'name' => $name, ], ['email' => $email]); @@ -465,7 +470,7 @@ $utopia->get('/v1/account/sessions/oauth2/:provider/redirect') // Create session token, verify user account and update OAuth2 ID and Access Token $secret = Auth::tokenGenerator(); - $expiry = time() + Auth::TOKEN_EXPIRATION_LOGIN_LONG; + $expiry = \time() + Auth::TOKEN_EXPIRATION_LOGIN_LONG; $session = new Document([ '$collection' => Database::SYSTEM_COLLECTION_TOKENS, '$permissions' => ['read' => ['user:'.$user['$id']], 'write' => ['user:'.$user['$id']]], @@ -477,8 +482,8 @@ $utopia->get('/v1/account/sessions/oauth2/:provider/redirect') ]); $user - ->setAttribute('oauth2'.ucfirst($provider), $oauth2ID) - ->setAttribute('oauth2'.ucfirst($provider).'AccessToken', $accessToken) + ->setAttribute('oauth2'.\ucfirst($provider), $oauth2ID) + ->setAttribute('oauth2'.\ucfirst($provider).'AccessToken', $accessToken) ->setAttribute('status', Auth::USER_STATUS_ACTIVATED) ->setAttribute('tokens', $session, Document::SET_TYPE_APPEND) ; @@ -498,13 +503,13 @@ $utopia->get('/v1/account/sessions/oauth2/:provider/redirect') ->setParam('data', ['provider' => $provider]) ; - if(!Config::getParam('domainVerification')) { + if (!Config::getParam('domainVerification')) { $response - ->addHeader('X-Fallback-Cookies', json_encode([Auth::$cookieName => Auth::encodeSession($user->getId(), $secret)])) + ->addHeader('X-Fallback-Cookies', \json_encode([Auth::$cookieName => Auth::encodeSession($user->getId(), $secret)])) ; } - if($state['success'] === $oauthDefaultSuccess) { // Add keys for non-web platforms + if ($state['success'] === $oauthDefaultSuccess) { // Add keys for non-web platforms $state['success'] = URLParser::parse($state['success']); $query = URLParser::parseQuery($state['success']['query']); $query['project'] = $project->getId(); @@ -527,6 +532,7 @@ $utopia->get('/v1/account/sessions/oauth2/:provider/redirect') $utopia->get('/v1/account') ->desc('Get Account') + ->groups(['api', 'account']) ->label('scope', 'account') ->label('sdk.platform', [APP_PLATFORM_CLIENT]) ->label('sdk.namespace', 'account') @@ -535,7 +541,7 @@ $utopia->get('/v1/account') ->label('sdk.response', ['200' => 'user']) ->action( function () use ($response, &$user, $oauth2Keys) { - $response->json(array_merge($user->getArrayCopy(array_merge( + $response->json(\array_merge($user->getArrayCopy(\array_merge( [ '$id', 'email', @@ -550,6 +556,7 @@ $utopia->get('/v1/account') $utopia->get('/v1/account/prefs') ->desc('Get Account Preferences') + ->groups(['api', 'account']) ->label('scope', 'account') ->label('sdk.platform', [APP_PLATFORM_CLIENT]) ->label('sdk.namespace', 'account') @@ -560,7 +567,7 @@ $utopia->get('/v1/account/prefs') $prefs = $user->getAttribute('prefs', '{}'); try { - $prefs = json_decode($prefs, true); + $prefs = \json_decode($prefs, true); $prefs = ($prefs) ? $prefs : []; } catch (\Exception $error) { throw new Exception('Failed to parse prefs', 500); @@ -572,6 +579,7 @@ $utopia->get('/v1/account/prefs') $utopia->get('/v1/account/sessions') ->desc('Get Account Sessions') + ->groups(['api', 'account']) ->label('scope', 'account') ->label('sdk.platform', [APP_PLATFORM_CLIENT]) ->label('sdk.namespace', 'account') @@ -614,7 +622,7 @@ $utopia->get('/v1/account/sessions') try { $record = $reader->country($token->getAttribute('ip', '')); - $sessions[$index]['geo']['isoCode'] = strtolower($record->country->isoCode); + $sessions[$index]['geo']['isoCode'] = \strtolower($record->country->isoCode); $sessions[$index]['geo']['country'] = (isset($countries[$record->country->isoCode])) ? $countries[$record->country->isoCode] : Locale::getText('locale.country.unknown'); } catch (\Exception $e) { $sessions[$index]['geo']['isoCode'] = '--'; @@ -630,6 +638,7 @@ $utopia->get('/v1/account/sessions') $utopia->get('/v1/account/logs') ->desc('Get Account Logs') + ->groups(['api', 'account']) ->label('scope', 'account') ->label('sdk.platform', [APP_PLATFORM_CLIENT]) ->label('sdk.namespace', 'account') @@ -676,7 +685,7 @@ $utopia->get('/v1/account/logs') $output[$i] = [ 'event' => $log['event'], 'ip' => $log['ip'], - 'time' => strtotime($log['time']), + 'time' => \strtotime($log['time']), 'OS' => $dd->getOs(), 'client' => $dd->getClient(), 'device' => $dd->getDevice(), @@ -687,7 +696,7 @@ $utopia->get('/v1/account/logs') try { $record = $reader->country($log['ip']); - $output[$i]['geo']['isoCode'] = strtolower($record->country->isoCode); + $output[$i]['geo']['isoCode'] = \strtolower($record->country->isoCode); $output[$i]['geo']['country'] = $record->country->name; $output[$i]['geo']['country'] = (isset($countries[$record->country->isoCode])) ? $countries[$record->country->isoCode] : Locale::getText('locale.country.unknown'); } catch (\Exception $e) { @@ -702,6 +711,7 @@ $utopia->get('/v1/account/logs') $utopia->patch('/v1/account/name') ->desc('Update Account Name') + ->groups(['api', 'account']) ->label('webhook', 'account.update.name') ->label('scope', 'account') ->label('sdk.platform', [APP_PLATFORM_CLIENT]) @@ -711,7 +721,7 @@ $utopia->patch('/v1/account/name') ->param('name', '', function () { return new Text(100); }, 'User name.') ->action( function ($name) use ($response, $user, $projectDB, $audit, $oauth2Keys) { - $user = $projectDB->updateDocument(array_merge($user->getArrayCopy(), [ + $user = $projectDB->updateDocument(\array_merge($user->getArrayCopy(), [ 'name' => $name, ])); @@ -725,7 +735,7 @@ $utopia->patch('/v1/account/name') ->setParam('resource', 'users/'.$user->getId()) ; - $response->json(array_merge($user->getArrayCopy(array_merge( + $response->json(\array_merge($user->getArrayCopy(\array_merge( [ '$id', 'email', @@ -739,6 +749,7 @@ $utopia->patch('/v1/account/name') $utopia->patch('/v1/account/password') ->desc('Update Account Password') + ->groups(['api', 'account']) ->label('webhook', 'account.update.password') ->label('scope', 'account') ->label('sdk.platform', [APP_PLATFORM_CLIENT]) @@ -753,7 +764,7 @@ $utopia->patch('/v1/account/password') throw new Exception('Invalid credentials', 401); } - $user = $projectDB->updateDocument(array_merge($user->getArrayCopy(), [ + $user = $projectDB->updateDocument(\array_merge($user->getArrayCopy(), [ 'password' => Auth::passwordHash($password), ])); @@ -767,7 +778,7 @@ $utopia->patch('/v1/account/password') ->setParam('resource', 'users/'.$user->getId()) ; - $response->json(array_merge($user->getArrayCopy(array_merge( + $response->json(\array_merge($user->getArrayCopy(\array_merge( [ '$id', 'email', @@ -781,6 +792,7 @@ $utopia->patch('/v1/account/password') $utopia->patch('/v1/account/email') ->desc('Update Account Email') + ->groups(['api', 'account']) ->label('webhook', 'account.update.email') ->label('scope', 'account') ->label('sdk.platform', [APP_PLATFORM_CLIENT]) @@ -810,7 +822,7 @@ $utopia->patch('/v1/account/email') // TODO after this user needs to confirm mail again - $user = $projectDB->updateDocument(array_merge($user->getArrayCopy(), [ + $user = $projectDB->updateDocument(\array_merge($user->getArrayCopy(), [ 'email' => $email, 'emailVerification' => false, ])); @@ -825,7 +837,7 @@ $utopia->patch('/v1/account/email') ->setParam('resource', 'users/'.$user->getId()) ; - $response->json(array_merge($user->getArrayCopy(array_merge( + $response->json(\array_merge($user->getArrayCopy(\array_merge( [ '$id', 'email', @@ -839,6 +851,7 @@ $utopia->patch('/v1/account/email') $utopia->patch('/v1/account/prefs') ->desc('Update Account Preferences') + ->groups(['api', 'account']) ->label('webhook', 'account.update.prefs') ->label('scope', 'account') ->label('sdk.platform', [APP_PLATFORM_CLIENT]) @@ -848,11 +861,11 @@ $utopia->patch('/v1/account/prefs') ->label('sdk.description', '/docs/references/account/update-prefs.md') ->action( function ($prefs) use ($response, $user, $projectDB, $audit) { - $old = json_decode($user->getAttribute('prefs', '{}'), true); + $old = \json_decode($user->getAttribute('prefs', '{}'), true); $old = ($old) ? $old : []; - $user = $projectDB->updateDocument(array_merge($user->getArrayCopy(), [ - 'prefs' => json_encode(array_merge($old, $prefs)), + $user = $projectDB->updateDocument(\array_merge($user->getArrayCopy(), [ + 'prefs' => \json_encode(\array_merge($old, $prefs)), ])); if (false === $user) { @@ -867,7 +880,7 @@ $utopia->patch('/v1/account/prefs') $prefs = $user->getAttribute('prefs', '{}'); try { - $prefs = json_decode($prefs, true); + $prefs = \json_decode($prefs, true); $prefs = ($prefs) ? $prefs : []; } catch (\Exception $error) { throw new Exception('Failed to parse prefs', 500); @@ -879,6 +892,7 @@ $utopia->patch('/v1/account/prefs') $utopia->delete('/v1/account') ->desc('Delete Account') + ->groups(['api', 'account']) ->label('webhook', 'account.delete') ->label('scope', 'account') ->label('sdk.platform', [APP_PLATFORM_CLIENT]) @@ -888,7 +902,7 @@ $utopia->delete('/v1/account') ->action( function () use ($response, $user, $projectDB, $audit, $webhook) { $protocol = Config::getParam('protocol'); - $user = $projectDB->updateDocument(array_merge($user->getArrayCopy(), [ + $user = $projectDB->updateDocument(\array_merge($user->getArrayCopy(), [ 'status' => Auth::USER_STATUS_BLOCKED, ])); @@ -918,15 +932,15 @@ $utopia->delete('/v1/account') ]) ; - if(!Config::getParam('domainVerification')) { + if (!Config::getParam('domainVerification')) { $response - ->addHeader('X-Fallback-Cookies', json_encode([])) + ->addHeader('X-Fallback-Cookies', \json_encode([])) ; } $response - ->addCookie(Auth::$cookieName.'_legacy', '', time() - 3600, '/', COOKIE_DOMAIN, ('https' == $protocol), true, null) - ->addCookie(Auth::$cookieName, '', time() - 3600, '/', COOKIE_DOMAIN, ('https' == $protocol), true, COOKIE_SAMESITE) + ->addCookie(Auth::$cookieName.'_legacy', '', \time() - 3600, '/', COOKIE_DOMAIN, ('https' == $protocol), true, null) + ->addCookie(Auth::$cookieName, '', \time() - 3600, '/', COOKIE_DOMAIN, ('https' == $protocol), true, COOKIE_SAMESITE) ->noContent() ; } @@ -934,6 +948,7 @@ $utopia->delete('/v1/account') $utopia->delete('/v1/account/sessions/:sessionId') ->desc('Delete Account Session') + ->groups(['api', 'account']) ->label('scope', 'account') ->label('webhook', 'account.sessions.delete') ->label('sdk.platform', [APP_PLATFORM_CLIENT]) @@ -970,16 +985,16 @@ $utopia->delete('/v1/account/sessions/:sessionId') ]) ; - if(!Config::getParam('domainVerification')) { + if (!Config::getParam('domainVerification')) { $response - ->addHeader('X-Fallback-Cookies', json_encode([])) + ->addHeader('X-Fallback-Cookies', \json_encode([])) ; } if ($token->getAttribute('secret') == Auth::hash(Auth::$secret)) { // If current session delete the cookies too $response - ->addCookie(Auth::$cookieName.'_legacy', '', time() - 3600, '/', COOKIE_DOMAIN, ('https' == $protocol), true, null) - ->addCookie(Auth::$cookieName, '', time() - 3600, '/', COOKIE_DOMAIN, ('https' == $protocol), true, COOKIE_SAMESITE) + ->addCookie(Auth::$cookieName.'_legacy', '', \time() - 3600, '/', COOKIE_DOMAIN, ('https' == $protocol), true, null) + ->addCookie(Auth::$cookieName, '', \time() - 3600, '/', COOKIE_DOMAIN, ('https' == $protocol), true, COOKIE_SAMESITE) ; } @@ -993,6 +1008,7 @@ $utopia->delete('/v1/account/sessions/:sessionId') $utopia->delete('/v1/account/sessions') ->desc('Delete All Account Sessions') + ->groups(['api', 'account']) ->label('scope', 'account') ->label('webhook', 'account.sessions.delete') ->label('sdk.platform', [APP_PLATFORM_CLIENT]) @@ -1023,16 +1039,16 @@ $utopia->delete('/v1/account/sessions') ]) ; - if(!Config::getParam('domainVerification')) { + if (!Config::getParam('domainVerification')) { $response - ->addHeader('X-Fallback-Cookies', json_encode([])) + ->addHeader('X-Fallback-Cookies', \json_encode([])) ; } if ($token->getAttribute('secret') == Auth::hash(Auth::$secret)) { // If current session delete the cookies too $response - ->addCookie(Auth::$cookieName.'_legacy', '', time() - 3600, '/', COOKIE_DOMAIN, ('https' == $protocol), true, null) - ->addCookie(Auth::$cookieName, '', time() - 3600, '/', COOKIE_DOMAIN, ('https' == $protocol), true, COOKIE_SAMESITE) + ->addCookie(Auth::$cookieName.'_legacy', '', \time() - 3600, '/', COOKIE_DOMAIN, ('https' == $protocol), true, null) + ->addCookie(Auth::$cookieName, '', \time() - 3600, '/', COOKIE_DOMAIN, ('https' == $protocol), true, COOKIE_SAMESITE) ; } } @@ -1043,6 +1059,7 @@ $utopia->delete('/v1/account/sessions') $utopia->post('/v1/account/recovery') ->desc('Create Password Recovery') + ->groups(['api', 'account']) ->label('scope', 'public') ->label('sdk.platform', [APP_PLATFORM_CLIENT]) ->label('sdk.namespace', 'account') @@ -1053,7 +1070,7 @@ $utopia->post('/v1/account/recovery') ->param('email', '', function () { return new Email(); }, 'User email.') ->param('url', '', function () use ($clients) { return new Host($clients); }, 'URL to redirect the user back to your app from the recovery email. Only URLs from hostnames in your project platform list are allowed. This requirement helps to prevent an [open redirect](https://cheatsheetseries.owasp.org/cheatsheets/Unvalidated_Redirects_and_Forwards_Cheat_Sheet.html) attack against your project API.') ->action( - function ($email, $url) use ($request, $response, $projectDB, $register, $audit, $project) { + function ($email, $url) use ($request, $response, $projectDB, $mail, $audit, $project) { $profile = $projectDB->getCollection([ // Get user by email address 'limit' => 1, 'first' => true, @@ -1073,7 +1090,7 @@ $utopia->post('/v1/account/recovery') '$permissions' => ['read' => ['user:'.$profile->getId()], 'write' => ['user:'.$profile->getId()]], 'type' => Auth::TOKEN_TYPE_RECOVERY, 'secret' => Auth::hash($secret), // On way hash encryption to protect DB leak - 'expire' => time() + Auth::TOKEN_EXPIRATION_RECOVERY, + 'expire' => \time() + Auth::TOKEN_EXPIRATION_RECOVERY, 'userAgent' => $request->getServer('HTTP_USER_AGENT', 'UNKNOWN'), 'ip' => $request->getIP(), ]); @@ -1098,27 +1115,34 @@ $utopia->post('/v1/account/recovery') $url['query'] = Template::mergeQuery(((isset($url['query'])) ? $url['query'] : ''), ['userId' => $profile->getId(), 'secret' => $secret]); $url = Template::unParseURL($url); - $body = new Template(__DIR__.'/../../config/locales/templates/'.Locale::getText('account.emails.recovery.body')); + $body = new Template(__DIR__.'/../../config/locales/templates/_base.tpl'); + $content = new Template(__DIR__.'/../../config/locales/templates/'.Locale::getText('account.emails.recovery.body')); + $cta = new Template(__DIR__.'/../../config/locales/templates/_cta.tpl'); + $body + ->setParam('{{content}}', $content->render()) + ->setParam('{{cta}}', $cta->render()) + ->setParam('{{title}}', Locale::getText('account.emails.recovery.title')) ->setParam('{{direction}}', Locale::getText('settings.direction')) ->setParam('{{project}}', $project->getAttribute('name', ['[APP-NAME]'])) ->setParam('{{name}}', $profile->getAttribute('name')) ->setParam('{{redirect}}', $url) + ->setParam('{{bg-body}}', '#f6f6f6') + ->setParam('{{bg-content}}', '#ffffff') + ->setParam('{{bg-cta}}', '#3498db') + ->setParam('{{bg-cta-hover}}', '#34495e') + ->setParam('{{text-content}}', '#000000') + ->setParam('{{text-cta}}', '#ffffff') ; - $mail = $register->get('smtp'); /* @var $mail \PHPMailer\PHPMailer\PHPMailer */ - - $mail->addAddress($profile->getAttribute('email', ''), $profile->getAttribute('name', '')); - - $mail->Subject = Locale::getText('account.emails.recovery.title'); - $mail->Body = $body->render(); - $mail->AltBody = strip_tags($body->render()); - - try { - $mail->send(); - } catch (\Exception $error) { - throw new Exception('Error sending mail: ' . $error->getMessage(), 500); - } + $mail + ->setParam('event', 'account.recovery.create') + ->setParam('recipient', $profile->getAttribute('email', '')) + ->setParam('name', $profile->getAttribute('name', '')) + ->setParam('subject', Locale::getText('account.emails.recovery.title')) + ->setParam('body', $body->render()) + ->trigger(); + ; $audit ->setParam('userId', $profile->getId()) @@ -1135,6 +1159,7 @@ $utopia->post('/v1/account/recovery') $utopia->put('/v1/account/recovery') ->desc('Complete Password Recovery') + ->groups(['api', 'account']) ->label('scope', 'public') ->label('sdk.platform', [APP_PLATFORM_CLIENT]) ->label('sdk.namespace', 'account') @@ -1173,9 +1198,9 @@ $utopia->put('/v1/account/recovery') Authorization::setRole('user:'.$profile->getId()); - $profile = $projectDB->updateDocument(array_merge($profile->getArrayCopy(), [ + $profile = $projectDB->updateDocument(\array_merge($profile->getArrayCopy(), [ 'password' => Auth::passwordHash($password), - 'password-update' => time(), + 'password-update' => \time(), 'emailVerification' => true, ])); @@ -1205,6 +1230,7 @@ $utopia->put('/v1/account/recovery') $utopia->post('/v1/account/verification') ->desc('Create Email Verification') + ->groups(['api', 'account']) ->label('scope', 'account') ->label('sdk.platform', [APP_PLATFORM_CLIENT]) ->label('sdk.namespace', 'account') @@ -1214,7 +1240,7 @@ $utopia->post('/v1/account/verification') ->label('abuse-key', 'url:{url},email:{param-email}') ->param('url', '', function () use ($clients) { return new Host($clients); }, 'URL to redirect the user back to your app from the verification email. Only URLs from hostnames in your project platform list are allowed. This requirement helps to prevent an [open redirect](https://cheatsheetseries.owasp.org/cheatsheets/Unvalidated_Redirects_and_Forwards_Cheat_Sheet.html) attack against your project API.') // TODO add built-in confirm page ->action( - function ($url) use ($request, $response, $register, $user, $project, $projectDB, $audit) { + function ($url) use ($request, $response, $mail, $user, $project, $projectDB, $audit) { $verificationSecret = Auth::tokenGenerator(); $verification = new Document([ @@ -1222,7 +1248,7 @@ $utopia->post('/v1/account/verification') '$permissions' => ['read' => ['user:'.$user->getId()], 'write' => ['user:'.$user->getId()]], 'type' => Auth::TOKEN_TYPE_VERIFICATION, 'secret' => Auth::hash($verificationSecret), // On way hash encryption to protect DB leak - 'expire' => time() + Auth::TOKEN_EXPIRATION_CONFIRM, + 'expire' => \time() + Auth::TOKEN_EXPIRATION_CONFIRM, 'userAgent' => $request->getServer('HTTP_USER_AGENT', 'UNKNOWN'), 'ip' => $request->getIP(), ]); @@ -1247,27 +1273,34 @@ $utopia->post('/v1/account/verification') $url['query'] = Template::mergeQuery(((isset($url['query'])) ? $url['query'] : ''), ['userId' => $user->getId(), 'secret' => $verificationSecret]); $url = Template::unParseURL($url); - $body = new Template(__DIR__.'/../../config/locales/templates/'.Locale::getText('account.emails.verification.body')); + $body = new Template(__DIR__.'/../../config/locales/templates/_base.tpl'); + $content = new Template(__DIR__.'/../../config/locales/templates/'.Locale::getText('account.emails.verification.body')); + $cta = new Template(__DIR__.'/../../config/locales/templates/_cta.tpl'); + $body + ->setParam('{{content}}', $content->render()) + ->setParam('{{cta}}', $cta->render()) + ->setParam('{{title}}', Locale::getText('account.emails.verification.title')) ->setParam('{{direction}}', Locale::getText('settings.direction')) ->setParam('{{project}}', $project->getAttribute('name', ['[APP-NAME]'])) ->setParam('{{name}}', $user->getAttribute('name')) ->setParam('{{redirect}}', $url) + ->setParam('{{bg-body}}', '#f6f6f6') + ->setParam('{{bg-content}}', '#ffffff') + ->setParam('{{bg-cta}}', '#3498db') + ->setParam('{{bg-cta-hover}}', '#34495e') + ->setParam('{{text-content}}', '#000000') + ->setParam('{{text-cta}}', '#ffffff') ; - $mail = $register->get('smtp'); /* @var $mail \PHPMailer\PHPMailer\PHPMailer */ - - $mail->addAddress($user->getAttribute('email'), $user->getAttribute('name')); - - $mail->Subject = Locale::getText('account.emails.verification.title'); - $mail->Body = $body->render(); - $mail->AltBody = strip_tags($body->render()); - - try { - $mail->send(); - } catch (\Exception $error) { - throw new Exception('Problem sending mail: ' . $error->getMessage(), 500); - } + $mail + ->setParam('event', 'account.verification.create') + ->setParam('recipient', $user->getAttribute('email')) + ->setParam('name', $user->getAttribute('name')) + ->setParam('subject', Locale::getText('account.emails.verification.title')) + ->setParam('body', $body->render()) + ->trigger() + ; $audit ->setParam('userId', $user->getId()) @@ -1284,6 +1317,7 @@ $utopia->post('/v1/account/verification') $utopia->put('/v1/account/verification') ->desc('Complete Email Verification') + ->groups(['api', 'account']) ->label('scope', 'public') ->label('sdk.platform', [APP_PLATFORM_CLIENT]) ->label('sdk.namespace', 'account') @@ -1316,7 +1350,7 @@ $utopia->put('/v1/account/verification') Authorization::setRole('user:'.$profile->getId()); - $profile = $projectDB->updateDocument(array_merge($profile->getArrayCopy(), [ + $profile = $projectDB->updateDocument(\array_merge($profile->getArrayCopy(), [ 'emailVerification' => true, ])); diff --git a/app/controllers/api/avatars.php b/app/controllers/api/avatars.php index b1c8d352b3..90f8ac34c8 100644 --- a/app/controllers/api/avatars.php +++ b/app/controllers/api/avatars.php @@ -18,37 +18,35 @@ use BaconQrCode\Writer; use Utopia\Config\Config; use Utopia\Validator\HexColor; -include_once __DIR__ . '/../shared/api.php'; - $types = [ 'browsers' => include __DIR__.'/../../config/avatars/browsers.php', 'credit-cards' => include __DIR__.'/../../config/avatars/credit-cards.php', 'flags' => include __DIR__.'/../../config/avatars/flags.php', ]; -$avatarCallback = function ($type, $code, $width, $height, $quality) use ($types, $response, $request) { - $code = strtolower($code); - $type = strtolower($type); +$avatarCallback = function ($type, $code, $width, $height, $quality) use ($types, $response) { + $code = \strtolower($code); + $type = \strtolower($type); - if (!array_key_exists($type, $types)) { + if (!\array_key_exists($type, $types)) { throw new Exception('Avatar set not found', 404); } - if (!array_key_exists($code, $types[$type])) { + if (!\array_key_exists($code, $types[$type])) { throw new Exception('Avatar not found', 404); } - if (!extension_loaded('imagick')) { + if (!\extension_loaded('imagick')) { throw new Exception('Imagick extension is missing', 500); } $output = 'png'; - $date = date('D, d M Y H:i:s', time() + (60 * 60 * 24 * 45)).' GMT'; // 45 days cache - $key = md5('/v1/avatars/:type/:code-'.$code.$width.$height.$quality.$output); + $date = \date('D, d M Y H:i:s', \time() + (60 * 60 * 24 * 45)).' GMT'; // 45 days cache + $key = \md5('/v1/avatars/:type/:code-'.$code.$width.$height.$quality.$output); $path = $types[$type][$code]; $type = 'png'; - if (!is_readable($path)) { + if (!\is_readable($path)) { throw new Exception('File not readable in '.$path, 500); } @@ -66,7 +64,7 @@ $avatarCallback = function ($type, $code, $width, $height, $quality) use ($types ; } - $resize = new Resize(file_get_contents($path)); + $resize = new Resize(\file_get_contents($path)); $resize->crop((int) $width, (int) $height); @@ -86,13 +84,12 @@ $avatarCallback = function ($type, $code, $width, $height, $quality) use ($types echo $data; unset($resize); - - exit(0); }; $utopia->get('/v1/avatars/credit-cards/:code') ->desc('Get Credit Card Icon') - ->param('code', '', function () use ($types) { return new WhiteList(array_keys($types['credit-cards'])); }, 'Credit Card Code. Possible values: '.implode(', ', array_keys($types['credit-cards'])).'.') + ->groups(['api', 'avatars']) + ->param('code', '', function () use ($types) { return new WhiteList(\array_keys($types['credit-cards'])); }, 'Credit Card Code. Possible values: '.\implode(', ', \array_keys($types['credit-cards'])).'.') ->param('width', 100, function () { return new Range(0, 2000); }, 'Image width. Pass an integer between 0 to 2000. Defaults to 100.', true) ->param('height', 100, function () { return new Range(0, 2000); }, 'Image height. Pass an integer between 0 to 2000. Defaults to 100.', true) ->param('quality', 100, function () { return new Range(0, 100); }, 'Image quality. Pass an integer between 0 to 100. Defaults to 100.', true) @@ -102,12 +99,14 @@ $utopia->get('/v1/avatars/credit-cards/:code') ->label('sdk.method', 'getCreditCard') ->label('sdk.methodType', 'location') ->label('sdk.description', '/docs/references/avatars/get-credit-card.md') - ->action(function ($code, $width, $height, $quality) use ($avatarCallback) { return $avatarCallback('credit-cards', $code, $width, $height, $quality); + ->action(function ($code, $width, $height, $quality) use ($avatarCallback) { + return $avatarCallback('credit-cards', $code, $width, $height, $quality); }); $utopia->get('/v1/avatars/browsers/:code') ->desc('Get Browser Icon') - ->param('code', '', function () use ($types) { return new WhiteList(array_keys($types['browsers'])); }, 'Browser Code.') + ->groups(['api', 'avatars']) + ->param('code', '', function () use ($types) { return new WhiteList(\array_keys($types['browsers'])); }, 'Browser Code.') ->param('width', 100, function () { return new Range(0, 2000); }, 'Image width. Pass an integer between 0 to 2000. Defaults to 100.', true) ->param('height', 100, function () { return new Range(0, 2000); }, 'Image height. Pass an integer between 0 to 2000. Defaults to 100.', true) ->param('quality', 100, function () { return new Range(0, 100); }, 'Image quality. Pass an integer between 0 to 100. Defaults to 100.', true) @@ -117,12 +116,14 @@ $utopia->get('/v1/avatars/browsers/:code') ->label('sdk.method', 'getBrowser') ->label('sdk.methodType', 'location') ->label('sdk.description', '/docs/references/avatars/get-browser.md') - ->action(function ($code, $width, $height, $quality) use ($avatarCallback) { return $avatarCallback('browsers', $code, $width, $height, $quality); + ->action(function ($code, $width, $height, $quality) use ($avatarCallback) { + return $avatarCallback('browsers', $code, $width, $height, $quality); }); $utopia->get('/v1/avatars/flags/:code') ->desc('Get Country Flag') - ->param('code', '', function () use ($types) { return new WhiteList(array_keys($types['flags'])); }, 'Country Code. ISO Alpha-2 country code format.') + ->groups(['api', 'avatars']) + ->param('code', '', function () use ($types) { return new WhiteList(\array_keys($types['flags'])); }, 'Country Code. ISO Alpha-2 country code format.') ->param('width', 100, function () { return new Range(0, 2000); }, 'Image width. Pass an integer between 0 to 2000. Defaults to 100.', true) ->param('height', 100, function () { return new Range(0, 2000); }, 'Image height. Pass an integer between 0 to 2000. Defaults to 100.', true) ->param('quality', 100, function () { return new Range(0, 100); }, 'Image quality. Pass an integer between 0 to 100. Defaults to 100.', true) @@ -132,11 +133,13 @@ $utopia->get('/v1/avatars/flags/:code') ->label('sdk.method', 'getFlag') ->label('sdk.methodType', 'location') ->label('sdk.description', '/docs/references/avatars/get-flag.md') - ->action(function ($code, $width, $height, $quality) use ($avatarCallback) { return $avatarCallback('flags', $code, $width, $height, $quality); + ->action(function ($code, $width, $height, $quality) use ($avatarCallback) { + return $avatarCallback('flags', $code, $width, $height, $quality); }); $utopia->get('/v1/avatars/image') ->desc('Get Image from URL') + ->groups(['api', 'avatars']) ->param('url', '', function () { return new URL(); }, 'Image URL which you want to crop.') ->param('width', 400, function () { return new Range(0, 2000); }, 'Resize preview image width, Pass an integer between 0 to 2000.', true) ->param('height', 400, function () { return new Range(0, 2000); }, 'Resize preview image height, Pass an integer between 0 to 2000.', true) @@ -150,8 +153,8 @@ $utopia->get('/v1/avatars/image') function ($url, $width, $height) use ($response) { $quality = 80; $output = 'png'; - $date = date('D, d M Y H:i:s', time() + (60 * 60 * 24 * 45)).' GMT'; // 45 days cache - $key = md5('/v2/avatars/images-'.$url.'-'.$width.'/'.$height.'/'.$quality); + $date = \date('D, d M Y H:i:s', \time() + (60 * 60 * 24 * 45)).' GMT'; // 45 days cache + $key = \md5('/v2/avatars/images-'.$url.'-'.$width.'/'.$height.'/'.$quality); $type = 'png'; $cache = new Cache(new Filesystem(APP_STORAGE_CACHE.'/app-0')); // Limit file number or size $data = $cache->load($key, 60 * 60 * 24 * 7 /* 1 week */); @@ -165,11 +168,11 @@ $utopia->get('/v1/avatars/image') ; } - if (!extension_loaded('imagick')) { + if (!\extension_loaded('imagick')) { throw new Exception('Imagick extension is missing', 500); } - $fetch = @file_get_contents($url, false); + $fetch = @\file_get_contents($url, false); if (!$fetch) { throw new Exception('Image not found', 404); @@ -199,13 +202,12 @@ $utopia->get('/v1/avatars/image') echo $data; unset($resize); - - exit(0); } ); $utopia->get('/v1/avatars/favicon') ->desc('Get Favicon') + ->groups(['api', 'avatars']) ->param('url', '', function () { return new URL(); }, 'Website URL which you want to fetch the favicon from.') ->label('scope', 'avatars.read') ->label('sdk.platform', [APP_PLATFORM_CLIENT, APP_PLATFORM_SERVER]) @@ -219,8 +221,8 @@ $utopia->get('/v1/avatars/favicon') $height = 56; $quality = 80; $output = 'png'; - $date = date('D, d M Y H:i:s', time() + (60 * 60 * 24 * 45)).' GMT'; // 45 days cache - $key = md5('/v2/avatars/favicon-'.$url); + $date = \date('D, d M Y H:i:s', \time() + (60 * 60 * 24 * 45)).' GMT'; // 45 days cache + $key = \md5('/v2/avatars/favicon-'.$url); $type = 'png'; $cache = new Cache(new Filesystem(APP_STORAGE_CACHE.'/app-0')); // Limit file number or size $data = $cache->load($key, 60 * 60 * 24 * 30 * 3 /* 3 months */); @@ -234,26 +236,26 @@ $utopia->get('/v1/avatars/favicon') ; } - if (!extension_loaded('imagick')) { + if (!\extension_loaded('imagick')) { throw new Exception('Imagick extension is missing', 500); } - $curl = curl_init(); + $curl = \curl_init(); - curl_setopt_array($curl, [ + \curl_setopt_array($curl, [ CURLOPT_RETURNTRANSFER => 1, CURLOPT_FOLLOWLOCATION => true, CURLOPT_MAXREDIRS => 3, CURLOPT_URL => $url, - CURLOPT_USERAGENT => sprintf(APP_USERAGENT, + CURLOPT_USERAGENT => \sprintf(APP_USERAGENT, Config::getParam('version'), $request->getServer('_APP_SYSTEM_SECURITY_EMAIL_ADDRESS', APP_EMAIL_SECURITY) ), ]); - $html = curl_exec($curl); + $html = \curl_exec($curl); - curl_close($curl); + \curl_close($curl); if (!$html) { throw new Exception('Failed to fetch remote URL', 404); @@ -272,20 +274,20 @@ $utopia->get('/v1/avatars/favicon') $href = $link->getAttribute('href'); $rel = $link->getAttribute('rel'); $sizes = $link->getAttribute('sizes'); - $absolute = URLParse::unparse(array_merge(parse_url($url), parse_url($href))); + $absolute = URLParse::unparse(\array_merge(\parse_url($url), \parse_url($href))); - switch (strtolower($rel)) { + switch (\strtolower($rel)) { case 'icon': case 'shortcut icon': //case 'apple-touch-icon': - $ext = pathinfo(parse_url($absolute, PHP_URL_PATH), PATHINFO_EXTENSION); + $ext = \pathinfo(\parse_url($absolute, PHP_URL_PATH), PATHINFO_EXTENSION); switch ($ext) { case 'ico': case 'png': case 'jpg': case 'jpeg': - $size = explode('x', strtolower($sizes)); + $size = \explode('x', \strtolower($sizes)); $sizeWidth = (isset($size[0])) ? (int) $size[0] : 0; $sizeHeight = (isset($size[1])) ? (int) $size[1] : 0; @@ -304,16 +306,16 @@ $utopia->get('/v1/avatars/favicon') } if (empty($outputHref) || empty($outputExt)) { - $default = parse_url($url); + $default = \parse_url($url); $outputHref = $default['scheme'].'://'.$default['host'].'/favicon.ico'; $outputExt = 'ico'; } if ('ico' == $outputExt) { // Skip crop, Imagick isn\'t supporting icon files - $data = @file_get_contents($outputHref, false); + $data = @\file_get_contents($outputHref, false); - if (empty($data) || (mb_substr($data, 0, 5) === 'get('/v1/avatars/favicon') ; } - $fetch = @file_get_contents($outputHref, false); + $fetch = @\file_get_contents($outputHref, false); if (!$fetch) { throw new Exception('Icon not found', 404); @@ -353,13 +355,12 @@ $utopia->get('/v1/avatars/favicon') echo $data; unset($resize); - - exit(0); } ); $utopia->get('/v1/avatars/qr') ->desc('Get QR Code') + ->groups(['api', 'avatars']) ->param('text', '', function () { return new Text(512); }, 'Plain text to be converted to QR code image.') ->param('size', 400, function () { return new Range(0, 1000); }, 'QR code size. Pass an integer between 0 to 1000. Defaults to 400.', true) ->param('margin', 1, function () { return new Range(0, 10); }, 'Margin from edge. Pass an integer between 0 to 10. Defaults to 1.', true) @@ -384,7 +385,7 @@ $utopia->get('/v1/avatars/qr') } $response - ->addHeader('Expires', date('D, d M Y H:i:s', time() + (60 * 60 * 24 * 45)).' GMT') // 45 days cache + ->addHeader('Expires', \date('D, d M Y H:i:s', \time() + (60 * 60 * 24 * 45)).' GMT') // 45 days cache ->setContentType('image/png') ->send($writer->writeString($text)) ; @@ -393,6 +394,7 @@ $utopia->get('/v1/avatars/qr') $utopia->get('/v1/avatars/initials') ->desc('Get User Initials') + ->groups(['api', 'avatars']) ->param('name', '', function () { return new Text(512); }, 'Full Name. When empty, current user name or email will be used.', true) ->param('width', 500, function () { return new Range(0, 2000); }, 'Image width. Pass an integer between 0 to 2000. Defaults to 100.', true) ->param('height', 500, function () { return new Range(0, 2000); }, 'Image height. Pass an integer between 0 to 2000. Defaults to 100.', true) @@ -419,30 +421,30 @@ $utopia->get('/v1/avatars/initials') ['color' => '#610008', 'background' => '#f6d2d5'] // RED ]; - $rand = rand(0, count($themes)-1); + $rand = \rand(0, \count($themes)-1); $name = (!empty($name)) ? $name : $user->getAttribute('name', $user->getAttribute('email', '')); - $words = explode(' ', strtoupper($name)); + $words = \explode(' ', \strtoupper($name)); $initials = null; $code = 0; foreach ($words as $key => $w) { $initials .= (isset($w[0])) ? $w[0] : ''; - $code += (isset($w[0])) ? ord($w[0]) : 0; + $code += (isset($w[0])) ? \ord($w[0]) : 0; - if($key == 1) { + if ($key == 1) { break; } } - $length = count($words); - $rand = substr($code,-1); + $length = \count($words); + $rand = \substr($code,-1); $background = (!empty($background)) ? '#'.$background : $themes[$rand]['background']; $color = (!empty($color)) ? '#'.$color : $themes[$rand]['color']; $image = new \Imagick(); $draw = new \ImagickDraw(); - $fontSize = min($width, $height) / 2; + $fontSize = \min($width, $height) / 2; $draw->setFont(__DIR__."/../../../public/fonts/poppins-v9-latin-500.ttf"); $image->setFont(__DIR__."/../../../public/fonts/poppins-v9-latin-500.ttf"); @@ -460,7 +462,7 @@ $utopia->get('/v1/avatars/initials') //$image->setImageCompressionQuality(9 - round(($quality / 100) * 9)); $response - ->addHeader('Expires', date('D, d M Y H:i:s', time() + (60 * 60 * 24 * 45)).' GMT') // 45 days cache + ->addHeader('Expires', \date('D, d M Y H:i:s', \time() + (60 * 60 * 24 * 45)).' GMT') // 45 days cache ->setContentType('image/png') ->send($image->getImageBlob()) ; diff --git a/app/controllers/api/database.php b/app/controllers/api/database.php index 69d01e9dc7..fafed2624b 100644 --- a/app/controllers/api/database.php +++ b/app/controllers/api/database.php @@ -9,6 +9,7 @@ use Utopia\Validator\Range; use Utopia\Validator\WhiteList; use Utopia\Validator\Text; use Utopia\Validator\ArrayList; +use Utopia\Validator\JSON; use Utopia\Locale\Locale; use Utopia\Audit\Audit; use Utopia\Audit\Adapters\MySQL as AuditAdapter; @@ -24,12 +25,9 @@ use Appwrite\Database\Exception\Structure as StructureException; use DeviceDetector\DeviceDetector; use GeoIp2\Database\Reader; -include_once __DIR__ . '/../shared/api.php'; - -$isDev = (App::ENV_TYPE_PRODUCTION !== $utopia->getEnv()); - $utopia->post('/v1/database/collections') ->desc('Create Collection') + ->groups(['api', 'database']) ->label('webhook', 'database.collections.create') ->label('scope', 'collections.write') ->label('sdk.namespace', 'database') @@ -45,7 +43,7 @@ $utopia->post('/v1/database/collections') $parsedRules = []; foreach ($rules as &$rule) { - $parsedRules[] = array_merge([ + $parsedRules[] = \array_merge([ '$collection' => Database::SYSTEM_COLLECTION_RULES, '$permissions' => [ 'read' => $read, @@ -58,8 +56,8 @@ $utopia->post('/v1/database/collections') $data = $projectDB->createDocument([ '$collection' => Database::SYSTEM_COLLECTION_COLLECTIONS, 'name' => $name, - 'dateCreated' => time(), - 'dateUpdated' => time(), + 'dateCreated' => \time(), + 'dateUpdated' => \time(), 'structure' => true, '$permissions' => [ 'read' => $read, @@ -103,6 +101,7 @@ $utopia->post('/v1/database/collections') $utopia->get('/v1/database/collections') ->desc('List Collections') + ->groups(['api', 'database']) ->label('scope', 'collections.read') ->label('sdk.namespace', 'database') ->label('sdk.platform', [APP_PLATFORM_SERVER]) @@ -151,6 +150,7 @@ $utopia->get('/v1/database/collections') $utopia->get('/v1/database/collections/:collectionId') ->desc('Get Collection') + ->groups(['api', 'database']) ->label('scope', 'collections.read') ->label('sdk.namespace', 'database') ->label('sdk.platform', [APP_PLATFORM_SERVER]) @@ -169,72 +169,74 @@ $utopia->get('/v1/database/collections/:collectionId') } ); -$utopia->get('/v1/database/collections/:collectionId/logs') - ->desc('Get Collection Logs') - ->label('scope', 'collections.read') - ->label('sdk.platform', [APP_PLATFORM_SERVER]) - ->label('sdk.namespace', 'database') - ->label('sdk.method', 'getCollectionLogs') - ->label('sdk.description', '/docs/references/database/get-collection-logs.md') - ->param('collectionId', '', function () { return new UID(); }, 'Collection unique ID.') - ->action( - function ($collectionId) use ($response, $register, $projectDB, $project) { - $collection = $projectDB->getDocument($collectionId); +// $utopia->get('/v1/database/collections/:collectionId/logs') +// ->desc('Get Collection Logs') +// ->groups(['api', 'database']) +// ->label('scope', 'collections.read') +// ->label('sdk.platform', [APP_PLATFORM_SERVER]) +// ->label('sdk.namespace', 'database') +// ->label('sdk.method', 'getCollectionLogs') +// ->label('sdk.description', '/docs/references/database/get-collection-logs.md') +// ->param('collectionId', '', function () { return new UID(); }, 'Collection unique ID.') +// ->action( +// function ($collectionId) use ($response, $register, $projectDB, $project) { +// $collection = $projectDB->getDocument($collectionId, false); - if (empty($collection->getId()) || Database::SYSTEM_COLLECTION_COLLECTIONS != $collection->getCollection()) { - throw new Exception('Collection not found', 404); - } +// if (empty($collection->getId()) || Database::SYSTEM_COLLECTION_COLLECTIONS != $collection->getCollection()) { +// throw new Exception('Collection not found', 404); +// } - $adapter = new AuditAdapter($register->get('db')); - $adapter->setNamespace('app_'.$project->getId()); +// $adapter = new AuditAdapter($register->get('db')); +// $adapter->setNamespace('app_'.$project->getId()); - $audit = new Audit($adapter); +// $audit = new Audit($adapter); - $countries = Locale::getText('countries'); +// $countries = Locale::getText('countries'); - $logs = $audit->getLogsByResource('database/collection/'.$collection->getId()); +// $logs = $audit->getLogsByResource('database/collection/'.$collection->getId()); - $reader = new Reader(__DIR__.'/../../db/DBIP/dbip-country-lite-2020-01.mmdb'); - $output = []; +// $reader = new Reader(__DIR__.'/../../db/DBIP/dbip-country-lite-2020-01.mmdb'); +// $output = []; - foreach ($logs as $i => &$log) { - $log['userAgent'] = (!empty($log['userAgent'])) ? $log['userAgent'] : 'UNKNOWN'; +// foreach ($logs as $i => &$log) { +// $log['userAgent'] = (!empty($log['userAgent'])) ? $log['userAgent'] : 'UNKNOWN'; - $dd = new DeviceDetector($log['userAgent']); +// $dd = new DeviceDetector($log['userAgent']); - $dd->skipBotDetection(); // OPTIONAL: If called, bot detection will completely be skipped (bots will be detected as regular devices then) +// $dd->skipBotDetection(); // OPTIONAL: If called, bot detection will completely be skipped (bots will be detected as regular devices then) - $dd->parse(); +// $dd->parse(); - $output[$i] = [ - 'event' => $log['event'], - 'ip' => $log['ip'], - 'time' => strtotime($log['time']), - 'OS' => $dd->getOs(), - 'client' => $dd->getClient(), - 'device' => $dd->getDevice(), - 'brand' => $dd->getBrand(), - 'model' => $dd->getModel(), - 'geo' => [], - ]; +// $output[$i] = [ +// 'event' => $log['event'], +// 'ip' => $log['ip'], +// 'time' => strtotime($log['time']), +// 'OS' => $dd->getOs(), +// 'client' => $dd->getClient(), +// 'device' => $dd->getDevice(), +// 'brand' => $dd->getBrand(), +// 'model' => $dd->getModel(), +// 'geo' => [], +// ]; - try { - $record = $reader->country($log['ip']); - $output[$i]['geo']['isoCode'] = strtolower($record->country->isoCode); - $output[$i]['geo']['country'] = $record->country->name; - $output[$i]['geo']['country'] = (isset($countries[$record->country->isoCode])) ? $countries[$record->country->isoCode] : Locale::getText('locale.country.unknown'); - } catch (\Exception $e) { - $output[$i]['geo']['isoCode'] = '--'; - $output[$i]['geo']['country'] = Locale::getText('locale.country.unknown'); - } - } +// try { +// $record = $reader->country($log['ip']); +// $output[$i]['geo']['isoCode'] = strtolower($record->country->isoCode); +// $output[$i]['geo']['country'] = $record->country->name; +// $output[$i]['geo']['country'] = (isset($countries[$record->country->isoCode])) ? $countries[$record->country->isoCode] : Locale::getText('locale.country.unknown'); +// } catch (\Exception $e) { +// $output[$i]['geo']['isoCode'] = '--'; +// $output[$i]['geo']['country'] = Locale::getText('locale.country.unknown'); +// } +// } - $response->json($output); - } - ); +// $response->json($output); +// } +// ); $utopia->put('/v1/database/collections/:collectionId') ->desc('Update Collection') + ->groups(['api', 'database']) ->label('scope', 'collections.write') ->label('webhook', 'database.collections.update') ->label('sdk.namespace', 'database') @@ -257,7 +259,7 @@ $utopia->put('/v1/database/collections/:collectionId') $parsedRules = []; foreach ($rules as &$rule) { - $parsedRules[] = array_merge([ + $parsedRules[] = \array_merge([ '$collection' => Database::SYSTEM_COLLECTION_RULES, '$permissions' => [ 'read' => $read, @@ -267,10 +269,10 @@ $utopia->put('/v1/database/collections/:collectionId') } try { - $collection = $projectDB->updateDocument(array_merge($collection->getArrayCopy(), [ + $collection = $projectDB->updateDocument(\array_merge($collection->getArrayCopy(), [ 'name' => $name, 'structure' => true, - 'dateUpdated' => time(), + 'dateUpdated' => \time(), '$permissions' => [ 'read' => $read, 'write' => $write, @@ -307,6 +309,7 @@ $utopia->put('/v1/database/collections/:collectionId') $utopia->delete('/v1/database/collections/:collectionId') ->desc('Delete Collection') + ->groups(['api', 'database']) ->label('scope', 'collections.write') ->label('webhook', 'database.collections.delete') ->label('sdk.namespace', 'database') @@ -344,14 +347,15 @@ $utopia->delete('/v1/database/collections/:collectionId') $utopia->post('/v1/database/collections/:collectionId/documents') ->desc('Create Document') + ->groups(['api', 'database']) ->label('webhook', 'database.documents.create') ->label('scope', 'documents.write') ->label('sdk.namespace', 'database') ->label('sdk.platform', [APP_PLATFORM_CLIENT, APP_PLATFORM_SERVER]) ->label('sdk.method', 'createDocument') ->label('sdk.description', '/docs/references/database/create-document.md') - ->param('collectionId', null, function () { return new UID(); }, 'Collection unique ID. You can create a new collection with validation rules using the Database service [server integration](/docs/database?platform=server#createCollection).') - ->param('data', [], function () { return new \Utopia\Validator\Mock(); }, 'Document data as JSON object.') + ->param('collectionId', null, function () { return new UID(); }, 'Collection unique ID. You can create a new collection with validation rules using the Database service [server integration](/docs/server/database#createCollection).') + ->param('data', [], function () { return new JSON(); }, 'Document data as JSON object.') ->param('read', [], function () { return new ArrayList(new Text(64)); }, 'An array of strings with read permissions. By default no user is granted with any read permissions. [learn more about permissions](/docs/permissions) and get a full list of available permissions.') ->param('write', [], function () { return new ArrayList(new Text(64)); }, 'An array of strings with write permissions. By default no user is granted with any write permissions. [learn more about permissions](/docs/permissions) and get a full list of available permissions.') ->param('parentDocument', '', function () { return new UID(); }, 'Parent document unique ID. Use when you want your new document to be a child of a parent document.', true) @@ -359,8 +363,8 @@ $utopia->post('/v1/database/collections/:collectionId/documents') ->param('parentPropertyType', Document::SET_TYPE_ASSIGN, function () { return new WhiteList([Document::SET_TYPE_ASSIGN, Document::SET_TYPE_APPEND, Document::SET_TYPE_PREPEND]); }, 'Parent document property connection type. You can set this value to **assign**, **append** or **prepend**, default value is assign. Use when you want your new document to be a child of a parent document.', true) ->action( function ($collectionId, $data, $read, $write, $parentDocument, $parentProperty, $parentPropertyType) use ($response, $projectDB, $webhook, $audit) { - $data = (is_string($data) && $result = json_decode($data, true)) ? $result : $data; // Cast to JSON array - + $data = (\is_string($data)) ? \json_decode($data, true) : $data; // Cast to JSON array + if (empty($data)) { throw new Exception('Missing payload', 400); } @@ -369,9 +373,9 @@ $utopia->post('/v1/database/collections/:collectionId/documents') throw new Exception('$id is not allowed for creating new documents, try update instead', 400); } - $collection = $projectDB->getDocument($collectionId/*, $isDev*/); + $collection = $projectDB->getDocument($collectionId, false); - if (is_null($collection->getId()) || Database::SYSTEM_COLLECTION_COLLECTIONS != $collection->getCollection()) { + if (\is_null($collection->getId()) || Database::SYSTEM_COLLECTION_COLLECTIONS != $collection->getCollection()) { throw new Exception('Collection not found', 404); } @@ -384,7 +388,7 @@ $utopia->post('/v1/database/collections/:collectionId/documents') // Read parent document + validate not 404 + validate read / write permission like patch method // Add payload to parent document property if ((!empty($parentDocument)) && (!empty($parentProperty))) { - $parentDocument = $projectDB->getDocument($parentDocument); + $parentDocument = $projectDB->getDocument($parentDocument, false); if (empty($parentDocument->getArrayCopy())) { // Check empty throw new Exception('No parent document found', 404); @@ -424,7 +428,7 @@ $utopia->post('/v1/database/collections/:collectionId/documents') $key = (isset($rule['key'])) ? $rule['key'] : ''; $default = (isset($rule['default'])) ? $rule['default'] : null; - if(!isset($data[$key])) { + if (!isset($data[$key])) { $data[$key] = $default; } } @@ -463,12 +467,13 @@ $utopia->post('/v1/database/collections/:collectionId/documents') $utopia->get('/v1/database/collections/:collectionId/documents') ->desc('List Documents') + ->groups(['api', 'database']) ->label('scope', 'documents.read') ->label('sdk.namespace', 'database') ->label('sdk.platform', [APP_PLATFORM_CLIENT, APP_PLATFORM_SERVER]) ->label('sdk.method', 'listDocuments') ->label('sdk.description', '/docs/references/database/list-documents.md') - ->param('collectionId', null, function () { return new UID(); }, 'Collection unique ID. You can create a new collection with validation rules using the Database service [server integration](/docs/database?platform=server#createCollection).') + ->param('collectionId', null, function () { return new UID(); }, 'Collection unique ID. You can create a new collection with validation rules using the Database service [server integration](/docs/server/database#createCollection).') ->param('filters', [], function () { return new ArrayList(new Text(128)); }, 'Array of filter strings. Each filter is constructed from a key name, comparison operator (=, !=, >, <, <=, >=) and a value. You can also use a dot (.) separator in attribute names to filter by child document attributes. Examples: \'name=John Doe\' or \'category.$id>=5bed2d152c362\'.', true) ->param('offset', 0, function () { return new Range(0, 900000000); }, 'Offset value. Use this value to manage pagination.', true) ->param('limit', 50, function () { return new Range(0, 1000); }, 'Maximum number of documents to return in response. Use this value to manage pagination.', true) @@ -479,10 +484,10 @@ $utopia->get('/v1/database/collections/:collectionId/documents') ->param('first', 0, function () { return new Range(0, 1); }, 'Return only the first document. Pass 1 for true or 0 for false. The default value is 0.', true) ->param('last', 0, function () { return new Range(0, 1); }, 'Return only the last document. Pass 1 for true or 0 for false. The default value is 0.', true) ->action( - function ($collectionId, $filters, $offset, $limit, $orderField, $orderType, $orderCast, $search, $first, $last) use ($response, $projectDB, $isDev) { - $collection = $projectDB->getDocument($collectionId, $isDev); + function ($collectionId, $filters, $offset, $limit, $orderField, $orderType, $orderCast, $search, $first, $last) use ($response, $projectDB, $utopia) { + $collection = $projectDB->getDocument($collectionId, false); - if (is_null($collection->getId()) || Database::SYSTEM_COLLECTION_COLLECTIONS != $collection->getCollection()) { + if (\is_null($collection->getId()) || Database::SYSTEM_COLLECTION_COLLECTIONS != $collection->getCollection()) { throw new Exception('Collection not found', 404); } @@ -495,7 +500,7 @@ $utopia->get('/v1/database/collections/:collectionId/documents') 'search' => $search, 'first' => (bool) $first, 'last' => (bool) $last, - 'filters' => array_merge($filters, [ + 'filters' => \array_merge($filters, [ '$collection='.$collectionId, ]), ]); @@ -503,7 +508,7 @@ $utopia->get('/v1/database/collections/:collectionId/documents') if ($first || $last) { $response->json((!empty($list) ? $list->getArrayCopy() : [])); } else { - if ($isDev) { + if ($utopia->isDevelopment()) { $collection ->setAttribute('debug', $projectDB->getDebug()) ->setAttribute('limit', $limit) @@ -530,17 +535,18 @@ $utopia->get('/v1/database/collections/:collectionId/documents') $utopia->get('/v1/database/collections/:collectionId/documents/:documentId') ->desc('Get Document') + ->groups(['api', 'database']) ->label('scope', 'documents.read') ->label('sdk.namespace', 'database') ->label('sdk.platform', [APP_PLATFORM_CLIENT, APP_PLATFORM_SERVER]) ->label('sdk.method', 'getDocument') ->label('sdk.description', '/docs/references/database/get-document.md') - ->param('collectionId', null, function () { return new UID(); }, 'Collection unique ID. You can create a new collection with validation rules using the Database service [server integration](/docs/database?platform=server#createCollection).') + ->param('collectionId', null, function () { return new UID(); }, 'Collection unique ID. You can create a new collection with validation rules using the Database service [server integration](/docs/server/database#createCollection).') ->param('documentId', null, function () { return new UID(); }, 'Document unique ID.') ->action( - function ($collectionId, $documentId) use ($response, $request, $projectDB, $isDev) { - $document = $projectDB->getDocument($documentId, $isDev); - $collection = $projectDB->getDocument($collectionId, $isDev); + function ($collectionId, $documentId) use ($response, $request, $projectDB) { + $document = $projectDB->getDocument($documentId, false); + $collection = $projectDB->getDocument($collectionId, false); if (empty($document->getArrayCopy()) || $document->getCollection() != $collection->getId()) { // Check empty throw new Exception('No document found', 404); @@ -548,20 +554,20 @@ $utopia->get('/v1/database/collections/:collectionId/documents/:documentId') $output = $document->getArrayCopy(); - $paths = explode('/', $request->getParam('q', '')); - $paths = array_slice($paths, 7, count($paths)); + $paths = \explode('/', $request->getParam('q', '')); + $paths = \array_slice($paths, 7, \count($paths)); - if (count($paths) > 0) { - if (count($paths) % 2 == 1) { - $output = $document->getAttribute(implode('.', $paths)); + if (\count($paths) > 0) { + if (\count($paths) % 2 == 1) { + $output = $document->getAttribute(\implode('.', $paths)); } else { - $id = (int) array_pop($paths); - $output = $document->search('$id', $id, $document->getAttribute(implode('.', $paths))); + $id = (int) \array_pop($paths); + $output = $document->search('$id', $id, $document->getAttribute(\implode('.', $paths))); } $output = ($output instanceof Document) ? $output->getArrayCopy() : $output; - if (!is_array($output)) { + if (!\is_array($output)) { throw new Exception('No document found', 404); } } @@ -575,29 +581,30 @@ $utopia->get('/v1/database/collections/:collectionId/documents/:documentId') $utopia->patch('/v1/database/collections/:collectionId/documents/:documentId') ->desc('Update Document') + ->groups(['api', 'database']) ->label('webhook', 'database.documents.update') ->label('scope', 'documents.write') ->label('sdk.namespace', 'database') ->label('sdk.platform', [APP_PLATFORM_CLIENT, APP_PLATFORM_SERVER]) ->label('sdk.method', 'updateDocument') ->label('sdk.description', '/docs/references/database/update-document.md') - ->param('collectionId', null, function () { return new UID(); }, 'Collection unique ID. You can create a new collection with validation rules using the Database service [server integration](/docs/database?platform=server#createCollection).') + ->param('collectionId', null, function () { return new UID(); }, 'Collection unique ID. You can create a new collection with validation rules using the Database service [server integration](/docs/server/database#createCollection).') ->param('documentId', null, function () { return new UID(); }, 'Document unique ID.') - ->param('data', [], function () { return new \Utopia\Validator\Mock(); }, 'Document data as JSON object.') + ->param('data', [], function () { return new JSON(); }, 'Document data as JSON object.') ->param('read', [], function () { return new ArrayList(new Text(64)); }, 'An array of strings with read permissions. By default no user is granted with any read permissions. [learn more about permissions](/docs/permissions) and get a full list of available permissions.') ->param('write', [], function () { return new ArrayList(new Text(64)); }, 'An array of strings with write permissions. By default no user is granted with any write permissions. [learn more about permissions](/docs/permissions) and get a full list of available permissions.') ->action( - function ($collectionId, $documentId, $data, $read, $write) use ($response, $projectDB, &$output, $webhook, $audit, $isDev) { - $collection = $projectDB->getDocument($collectionId/*, $isDev*/); - $document = $projectDB->getDocument($documentId, $isDev); + function ($collectionId, $documentId, $data, $read, $write) use ($response, $projectDB, $webhook, $audit) { + $collection = $projectDB->getDocument($collectionId, false); + $document = $projectDB->getDocument($documentId, false); - $data = (is_string($data) && $result = json_decode($data, true)) ? $result : $data; // Cast to JSON array + $data = (\is_string($data)) ? \json_decode($data, true) : $data; // Cast to JSON array - if (!is_array($data)) { + if (!\is_array($data)) { throw new Exception('Data param should be a valid JSON', 400); } - if (is_null($collection->getId()) || Database::SYSTEM_COLLECTION_COLLECTIONS != $collection->getCollection()) { + if (\is_null($collection->getId()) || Database::SYSTEM_COLLECTION_COLLECTIONS != $collection->getCollection()) { throw new Exception('Collection not found', 404); } @@ -615,7 +622,7 @@ $utopia->patch('/v1/database/collections/:collectionId/documents/:documentId') $data['$permissions']['write'] = $read; } - $data = array_merge($document->getArrayCopy(), $data); + $data = \array_merge($document->getArrayCopy(), $data); $data['$collection'] = $collection->getId(); // Make sure user don't switch collectionID $data['$id'] = $document->getId(); // Make sure user don't switch document unique ID @@ -654,24 +661,25 @@ $utopia->patch('/v1/database/collections/:collectionId/documents/:documentId') $utopia->delete('/v1/database/collections/:collectionId/documents/:documentId') ->desc('Delete Document') + ->groups(['api', 'database']) ->label('scope', 'documents.write') ->label('webhook', 'database.documents.delete') ->label('sdk.namespace', 'database') ->label('sdk.platform', [APP_PLATFORM_CLIENT, APP_PLATFORM_SERVER]) ->label('sdk.method', 'deleteDocument') ->label('sdk.description', '/docs/references/database/delete-document.md') - ->param('collectionId', null, function () { return new UID(); }, 'Collection unique ID. You can create a new collection with validation rules using the Database service [server integration](/docs/database?platform=server#createCollection).') + ->param('collectionId', null, function () { return new UID(); }, 'Collection unique ID. You can create a new collection with validation rules using the Database service [server integration](/docs/server/database#createCollection).') ->param('documentId', null, function () { return new UID(); }, 'Document unique ID.') ->action( - function ($collectionId, $documentId) use ($response, $projectDB, $audit, $webhook, $isDev) { - $collection = $projectDB->getDocument($collectionId, $isDev); - $document = $projectDB->getDocument($documentId, $isDev); + function ($collectionId, $documentId) use ($response, $projectDB, $audit, $webhook) { + $collection = $projectDB->getDocument($collectionId, false); + $document = $projectDB->getDocument($documentId, false); if (empty($document->getArrayCopy()) || $document->getCollection() != $collectionId) { // Check empty throw new Exception('No document found', 404); } - if (is_null($collection->getId()) || Database::SYSTEM_COLLECTION_COLLECTIONS != $collection->getCollection()) { + if (\is_null($collection->getId()) || Database::SYSTEM_COLLECTION_COLLECTIONS != $collection->getCollection()) { throw new Exception('Collection not found', 404); } diff --git a/app/controllers/api/graphql.php b/app/controllers/api/graphql.php index ce39d8e799..0eecb23e15 100644 --- a/app/controllers/api/graphql.php +++ b/app/controllers/api/graphql.php @@ -14,6 +14,7 @@ global $utopia; $utopia->post('/v1/graphql') ->desc('GraphQL Endpoint') + ->groups(['api', 'graphql']) ->label('scope', 'public') ->action( function () { diff --git a/app/controllers/api/health.php b/app/controllers/api/health.php index db95fe5db1..76727563ae 100644 --- a/app/controllers/api/health.php +++ b/app/controllers/api/health.php @@ -9,6 +9,7 @@ use Appwrite\ClamAV\Network; $utopia->get('/v1/health') ->desc('Get HTTP') + ->groups(['api', 'health']) ->label('scope', 'health.read') ->label('sdk.platform', [APP_PLATFORM_SERVER]) ->label('sdk.namespace', 'health') @@ -20,8 +21,19 @@ $utopia->get('/v1/health') } ); +$utopia->get('/v1/health/version') + ->desc('Get Version') + ->groups(['api', 'health']) + ->label('scope', 'public') + ->action( + function () use ($response) { + $response->json(['version' => APP_VERSION_STABLE]); + } + ); + $utopia->get('/v1/health/db') ->desc('Get DB') + ->groups(['api', 'health']) ->label('scope', 'health.read') ->label('sdk.platform', [APP_PLATFORM_SERVER]) ->label('sdk.namespace', 'health') @@ -37,6 +49,7 @@ $utopia->get('/v1/health/db') $utopia->get('/v1/health/cache') ->desc('Get Cache') + ->groups(['api', 'health']) ->label('scope', 'health.read') ->label('sdk.platform', [APP_PLATFORM_SERVER]) ->label('sdk.namespace', 'health') @@ -52,6 +65,7 @@ $utopia->get('/v1/health/cache') $utopia->get('/v1/health/time') ->desc('Get Time') + ->groups(['api', 'health']) ->label('scope', 'health.read') ->label('sdk.platform', [APP_PLATFORM_SERVER]) ->label('sdk.namespace', 'health') @@ -66,39 +80,40 @@ $utopia->get('/v1/health/time') $gap = 60; // Allow [X] seconds gap /* Create a socket and connect to NTP server */ - $sock = socket_create(AF_INET, SOCK_DGRAM, SOL_UDP); + $sock = \socket_create(AF_INET, SOCK_DGRAM, SOL_UDP); - socket_connect($sock, $host, 123); + \socket_connect($sock, $host, 123); /* Send request */ - $msg = "\010".str_repeat("\0", 47); + $msg = "\010".\str_repeat("\0", 47); - socket_send($sock, $msg, strlen($msg), 0); + \socket_send($sock, $msg, \strlen($msg), 0); /* Receive response and close socket */ - socket_recv($sock, $recv, 48, MSG_WAITALL); - socket_close($sock); + \socket_recv($sock, $recv, 48, MSG_WAITALL); + \socket_close($sock); /* Interpret response */ - $data = unpack('N12', $recv); - $timestamp = sprintf('%u', $data[9]); + $data = \unpack('N12', $recv); + $timestamp = \sprintf('%u', $data[9]); /* NTP is number of seconds since 0000 UT on 1 January 1900 Unix time is seconds since 0000 UT on 1 January 1970 */ $timestamp -= 2208988800; - $diff = ($timestamp - time()); + $diff = ($timestamp - \time()); if ($diff > $gap || $diff < ($gap * -1)) { throw new Exception('Server time gaps detected'); } - $response->json(['remote' => $timestamp, 'local' => time(), 'diff' => $diff]); + $response->json(['remote' => $timestamp, 'local' => \time(), 'diff' => $diff]); } ); $utopia->get('/v1/health/queue/webhooks') ->desc('Get Webhooks Queue') + ->groups(['api', 'health']) ->label('scope', 'health.read') ->label('sdk.platform', [APP_PLATFORM_SERVER]) ->label('sdk.namespace', 'health') @@ -112,6 +127,7 @@ $utopia->get('/v1/health/queue/webhooks') $utopia->get('/v1/health/queue/tasks') ->desc('Get Tasks Queue') + ->groups(['api', 'health']) ->label('scope', 'health.read') ->label('sdk.platform', [APP_PLATFORM_SERVER]) ->label('sdk.namespace', 'health') @@ -124,7 +140,8 @@ $utopia->get('/v1/health/queue/tasks') ); $utopia->get('/v1/health/queue/logs') -->desc('Get Logs Queue') + ->desc('Get Logs Queue') + ->groups(['api', 'health']) ->label('scope', 'health.read') ->label('sdk.platform', [APP_PLATFORM_SERVER]) ->label('sdk.namespace', 'health') @@ -138,6 +155,7 @@ $utopia->get('/v1/health/queue/logs') $utopia->get('/v1/health/queue/usage') ->desc('Get Usage Queue') + ->groups(['api', 'health']) ->label('scope', 'health.read') ->label('sdk.platform', [APP_PLATFORM_SERVER]) ->label('sdk.namespace', 'health') @@ -151,6 +169,7 @@ $utopia->get('/v1/health/queue/usage') $utopia->get('/v1/health/queue/certificates') ->desc('Get Certificate Queue') + ->groups(['api', 'health']) ->label('scope', 'health.read') ->label('sdk.platform', [APP_PLATFORM_SERVER]) ->label('sdk.namespace', 'health') @@ -164,6 +183,7 @@ $utopia->get('/v1/health/queue/certificates') $utopia->get('/v1/health/queue/functions') ->desc('Get Functions Queue') + ->groups(['api', 'health']) ->label('scope', 'health.read') ->label('sdk.platform', [APP_PLATFORM_SERVER]) ->label('sdk.namespace', 'health') @@ -177,6 +197,7 @@ $utopia->get('/v1/health/queue/functions') $utopia->get('/v1/health/storage/local') ->desc('Get Local Storage') + ->groups(['api', 'health']) ->label('scope', 'health.read') ->label('sdk.platform', [APP_PLATFORM_SERVER]) ->label('sdk.namespace', 'health') @@ -184,26 +205,21 @@ $utopia->get('/v1/health/storage/local') ->label('sdk.description', '/docs/references/health/get-storage-local.md') ->action( function () use ($response) { - $device = new Local(APP_STORAGE_UPLOADS.'/'); + foreach ([ + 'Uploads' => APP_STORAGE_UPLOADS, + 'Cache' => APP_STORAGE_CACHE, + 'Config' => APP_STORAGE_CONFIG, + 'Certs' => APP_STORAGE_CERTIFICATES + ] as $key => $volume) { + $device = new Local($volume); - if (!is_readable($device->getRoot().'/..')) { - throw new Exception('Device is not readable'); - } - - if (!is_writable($device->getRoot().'/../uploads')) { - throw new Exception('Device uploads dir is not writable'); - } - - if (!is_writable($device->getRoot().'/../cache')) { - throw new Exception('Device cache dir is not writable'); - } + if (!\is_readable($device->getRoot())) { + throw new Exception('Device '.$key.' dir is not readable'); + } - if (!is_writable($device->getRoot().'/../config')) { - throw new Exception('Device config dir is not writable'); - } - - if (!is_writable($device->getRoot().'/../certificates')) { - throw new Exception('Device certificates dir is not writable'); + if (!\is_writable($device->getRoot())) { + throw new Exception('Device '.$key.' dir is not writable'); + } } $response->json(['status' => 'OK']); @@ -212,6 +228,7 @@ $utopia->get('/v1/health/storage/local') $utopia->get('/v1/health/anti-virus') ->desc('Get Anti virus') + ->groups(['api', 'health']) ->label('scope', 'health.read') ->label('sdk.platform', [APP_PLATFORM_SERVER]) ->label('sdk.namespace', 'health') @@ -219,7 +236,7 @@ $utopia->get('/v1/health/anti-virus') ->label('sdk.description', '/docs/references/health/get-storage-anti-virus.md') ->action( function () use ($request, $response) { - if($request->getServer('_APP_STORAGE_ANTIVIRUS') === 'disabled') { // Check if scans are enabled + if ($request->getServer('_APP_STORAGE_ANTIVIRUS') === 'disabled') { // Check if scans are enabled throw new Exception('Anitvirus is disabled'); } @@ -234,6 +251,7 @@ $utopia->get('/v1/health/anti-virus') $utopia->get('/v1/health/stats') // Currently only used internally ->desc('Get System Stats') + ->groups(['api', 'health']) ->label('scope', 'god') // ->label('sdk.platform', [APP_PLATFORM_SERVER]) // ->label('sdk.namespace', 'health') @@ -250,7 +268,7 @@ $utopia->get('/v1/health/stats') // Currently only used internally ->json([ 'server' => [ 'name' => 'nginx', - 'version' => shell_exec('nginx -v 2>&1'), + 'version' => \shell_exec('nginx -v 2>&1'), ], 'storage' => [ 'used' => Storage::human($device->getDirectorySize($device->getRoot().'/')), diff --git a/app/controllers/api/locale.php b/app/controllers/api/locale.php index 64978ac355..148ee3e8e9 100644 --- a/app/controllers/api/locale.php +++ b/app/controllers/api/locale.php @@ -6,10 +6,9 @@ use Utopia\App; use Utopia\Locale\Locale; use GeoIp2\Database\Reader; -include_once __DIR__ . '/../shared/api.php'; - $utopia->get('/v1/locale') ->desc('Get User Locale') + ->groups(['api', 'locale']) ->label('scope', 'locale.read') ->label('sdk.platform', [APP_PLATFORM_CLIENT, APP_PLATFORM_SERVER]) ->label('sdk.namespace', 'locale') @@ -26,7 +25,7 @@ $utopia->get('/v1/locale') $countries = Locale::getText('countries'); $continents = Locale::getText('continents'); - if (App::ENV_TYPE_PRODUCTION !== $utopia->getEnv()) { + if (App::MODE_TYPE_PRODUCTION !== $utopia->getMode()) { $ip = '79.177.241.94'; } @@ -41,10 +40,10 @@ $utopia->get('/v1/locale') //$output['countryTimeZone'] = DateTimeZone::listIdentifiers(DateTimeZone::PER_COUNTRY, $record->country->isoCode); $output['continent'] = (isset($continents[$record->continent->code])) ? $continents[$record->continent->code] : Locale::getText('locale.country.unknown'); $output['continentCode'] = $record->continent->code; - $output['eu'] = (in_array($record->country->isoCode, $eu)) ? true : false; + $output['eu'] = (\in_array($record->country->isoCode, $eu)) ? true : false; foreach ($currencies as $code => $element) { - if (isset($element['locations']) && isset($element['code']) && in_array($record->country->isoCode, $element['locations'])) { + if (isset($element['locations']) && isset($element['code']) && \in_array($record->country->isoCode, $element['locations'])) { $currency = $element['code']; } } @@ -61,13 +60,14 @@ $utopia->get('/v1/locale') $response ->addHeader('Cache-Control', 'public, max-age='.$time) - ->addHeader('Expires', date('D, d M Y H:i:s', time() + $time).' GMT') // 45 days cache + ->addHeader('Expires', \date('D, d M Y H:i:s', \time() + $time).' GMT') // 45 days cache ->json($output); } ); $utopia->get('/v1/locale/countries') ->desc('List Countries') + ->groups(['api', 'locale']) ->label('scope', 'locale.read') ->label('sdk.platform', [APP_PLATFORM_CLIENT, APP_PLATFORM_SERVER]) ->label('sdk.namespace', 'locale') @@ -77,7 +77,7 @@ $utopia->get('/v1/locale/countries') function () use ($response) { $list = Locale::getText('countries'); /* @var $list array */ - asort($list); + \asort($list); $response->json($list); } @@ -85,6 +85,7 @@ $utopia->get('/v1/locale/countries') $utopia->get('/v1/locale/countries/eu') ->desc('List EU Countries') + ->groups(['api', 'locale']) ->label('scope', 'locale.read') ->label('sdk.platform', [APP_PLATFORM_CLIENT, APP_PLATFORM_SERVER]) ->label('sdk.namespace', 'locale') @@ -97,12 +98,12 @@ $utopia->get('/v1/locale/countries/eu') $list = []; foreach ($eu as $code) { - if (array_key_exists($code, $countries)) { + if (\array_key_exists($code, $countries)) { $list[$code] = $countries[$code]; } } - asort($list); + \asort($list); $response->json($list); } @@ -110,6 +111,7 @@ $utopia->get('/v1/locale/countries/eu') $utopia->get('/v1/locale/countries/phones') ->desc('List Countries Phone Codes') + ->groups(['api', 'locale']) ->label('scope', 'locale.read') ->label('sdk.platform', [APP_PLATFORM_CLIENT, APP_PLATFORM_SERVER]) ->label('sdk.namespace', 'locale') @@ -122,12 +124,12 @@ $utopia->get('/v1/locale/countries/phones') $countries = Locale::getText('countries'); /* @var $countries array */ foreach ($list as $code => $name) { - if (array_key_exists($code, $countries)) { + if (\array_key_exists($code, $countries)) { $list[$code] = '+'.$list[$code]; } } - asort($list); + \asort($list); $response->json($list); } @@ -135,6 +137,7 @@ $utopia->get('/v1/locale/countries/phones') $utopia->get('/v1/locale/continents') ->desc('List Continents') + ->groups(['api', 'locale']) ->label('scope', 'locale.read') ->label('sdk.platform', [APP_PLATFORM_CLIENT, APP_PLATFORM_SERVER]) ->label('sdk.namespace', 'locale') @@ -144,7 +147,7 @@ $utopia->get('/v1/locale/continents') function () use ($response) { $list = Locale::getText('continents'); /* @var $list array */ - asort($list); + \asort($list); $response->json($list); } @@ -153,6 +156,7 @@ $utopia->get('/v1/locale/continents') $utopia->get('/v1/locale/currencies') ->desc('List Currencies') + ->groups(['api', 'locale']) ->label('scope', 'locale.read') ->label('sdk.platform', [APP_PLATFORM_CLIENT, APP_PLATFORM_SERVER]) ->label('sdk.namespace', 'locale') @@ -169,6 +173,7 @@ $utopia->get('/v1/locale/currencies') $utopia->get('/v1/locale/languages') ->desc('List Languages') + ->groups(['api', 'locale']) ->label('scope', 'locale.read') ->label('sdk.platform', [APP_PLATFORM_CLIENT, APP_PLATFORM_SERVER]) ->label('sdk.namespace', 'locale') diff --git a/app/controllers/api/projects.php b/app/controllers/api/projects.php index 766b2f48fb..89f26feb15 100644 --- a/app/controllers/api/projects.php +++ b/app/controllers/api/projects.php @@ -21,12 +21,11 @@ use Appwrite\OpenSSL\OpenSSL; use Appwrite\Network\Validator\CNAME; use Cron\CronExpression; -include_once __DIR__ . '/../shared/api.php'; - $scopes = include __DIR__.'/../../../app/config/scopes.php'; $utopia->post('/v1/projects') ->desc('Create Project') + ->groups(['api', 'projects']) ->label('scope', 'projects.write') ->label('sdk.namespace', 'projects') ->label('sdk.method', 'create') @@ -89,6 +88,7 @@ $utopia->post('/v1/projects') $utopia->get('/v1/projects') ->desc('List Projects') + ->groups(['api', 'projects']) ->label('scope', 'projects.read') ->label('sdk.namespace', 'projects') ->label('sdk.method', 'list') @@ -107,11 +107,11 @@ $utopia->get('/v1/projects') foreach ($results as $project) { foreach (Config::getParam('providers') as $provider => $node) { - $secret = json_decode($project->getAttribute('usersOauth2'.ucfirst($provider).'Secret', '{}'), true); + $secret = \json_decode($project->getAttribute('usersOauth2'.\ucfirst($provider).'Secret', '{}'), true); if (!empty($secret) && isset($secret['version'])) { $key = $request->getServer('_APP_OPENSSL_KEY_V'.$secret['version']); - $project->setAttribute('usersOauth2'.ucfirst($provider).'Secret', OpenSSL::decrypt($secret['data'], $secret['method'], $key, 0, hex2bin($secret['iv']), hex2bin($secret['tag']))); + $project->setAttribute('usersOauth2'.\ucfirst($provider).'Secret', OpenSSL::decrypt($secret['data'], $secret['method'], $key, 0, \hex2bin($secret['iv']), \hex2bin($secret['tag']))); } } } @@ -122,6 +122,7 @@ $utopia->get('/v1/projects') $utopia->get('/v1/projects/:projectId') ->desc('Get Project') + ->groups(['api', 'projects']) ->label('scope', 'projects.read') ->label('sdk.namespace', 'projects') ->label('sdk.method', 'get') @@ -135,11 +136,11 @@ $utopia->get('/v1/projects/:projectId') } foreach (Config::getParam('providers') as $provider => $node) { - $secret = json_decode($project->getAttribute('usersOauth2'.ucfirst($provider).'Secret', '{}'), true); + $secret = \json_decode($project->getAttribute('usersOauth2'.\ucfirst($provider).'Secret', '{}'), true); if (!empty($secret) && isset($secret['version'])) { $key = $request->getServer('_APP_OPENSSL_KEY_V'.$secret['version']); - $project->setAttribute('usersOauth2'.ucfirst($provider).'Secret', OpenSSL::decrypt($secret['data'], $secret['method'], $key, 0, hex2bin($secret['iv']), hex2bin($secret['tag']))); + $project->setAttribute('usersOauth2'.\ucfirst($provider).'Secret', OpenSSL::decrypt($secret['data'], $secret['method'], $key, 0, \hex2bin($secret['iv']), \hex2bin($secret['tag']))); } } @@ -149,6 +150,7 @@ $utopia->get('/v1/projects/:projectId') $utopia->get('/v1/projects/:projectId/usage') ->desc('Get Project') + ->groups(['api', 'projects']) ->label('scope', 'projects.read') ->label('sdk.namespace', 'projects') ->label('sdk.method', 'getUsage') @@ -164,23 +166,23 @@ $utopia->get('/v1/projects/:projectId/usage') $period = [ 'daily' => [ - 'start' => DateTime::createFromFormat('U', strtotime('today')), - 'end' => DateTime::createFromFormat('U', strtotime('tomorrow')), + 'start' => DateTime::createFromFormat('U', \strtotime('today')), + 'end' => DateTime::createFromFormat('U', \strtotime('tomorrow')), 'group' => '1m', ], 'monthly' => [ - 'start' => DateTime::createFromFormat('U', strtotime('midnight first day of this month')), - 'end' => DateTime::createFromFormat('U', strtotime('midnight last day of this month')), + 'start' => DateTime::createFromFormat('U', \strtotime('midnight first day of this month')), + 'end' => DateTime::createFromFormat('U', \strtotime('midnight last day of this month')), 'group' => '1d', ], 'last30' => [ - 'start' => DateTime::createFromFormat('U', strtotime('-30 days')), - 'end' => DateTime::createFromFormat('U', strtotime('tomorrow')), + 'start' => DateTime::createFromFormat('U', \strtotime('-30 days')), + 'end' => DateTime::createFromFormat('U', \strtotime('tomorrow')), 'group' => '1d', ], 'last90' => [ - 'start' => DateTime::createFromFormat('U', strtotime('-90 days')), - 'end' => DateTime::createFromFormat('U', strtotime('today')), + 'start' => DateTime::createFromFormat('U', \strtotime('-90 days')), + 'end' => DateTime::createFromFormat('U', \strtotime('today')), 'group' => '1d', ], // 'yearly' => [ @@ -207,7 +209,7 @@ $utopia->get('/v1/projects/:projectId/usage') foreach ($points as $point) { $requests[] = [ 'value' => (!empty($point['value'])) ? $point['value'] : 0, - 'date' => strtotime($point['time']), + 'date' => \strtotime($point['time']), ]; } @@ -218,7 +220,7 @@ $utopia->get('/v1/projects/:projectId/usage') foreach ($points as $point) { $network[] = [ 'value' => (!empty($point['value'])) ? $point['value'] : 0, - 'date' => strtotime($point['time']), + 'date' => \strtotime($point['time']), ]; } } @@ -262,18 +264,18 @@ $utopia->get('/v1/projects/:projectId/usage') } // Tasks - $tasksTotal = count($project->getAttribute('tasks', [])); + $tasksTotal = \count($project->getAttribute('tasks', [])); $response->json([ 'requests' => [ 'data' => $requests, - 'total' => array_sum(array_map(function ($item) { + 'total' => \array_sum(\array_map(function ($item) { return $item['value']; }, $requests)), ], 'network' => [ - 'data' => array_map(function ($value) {return ['value' => round($value['value'] / 1000000, 2), 'date' => $value['date']];}, $network), // convert bytes to mb - 'total' => array_sum(array_map(function ($item) { + 'data' => \array_map(function ($value) {return ['value' => \round($value['value'] / 1000000, 2), 'date' => $value['date']];}, $network), // convert bytes to mb + 'total' => \array_sum(\array_map(function ($item) { return $item['value']; }, $network)), ], @@ -283,7 +285,7 @@ $utopia->get('/v1/projects/:projectId/usage') ], 'documents' => [ 'data' => $documents, - 'total' => array_sum(array_map(function ($item) { + 'total' => \array_sum(\array_map(function ($item) { return $item['total']; }, $documents)), ], @@ -310,6 +312,7 @@ $utopia->get('/v1/projects/:projectId/usage') $utopia->patch('/v1/projects/:projectId') ->desc('Update Project') + ->groups(['api', 'projects']) ->label('scope', 'projects.write') ->label('sdk.namespace', 'projects') ->label('sdk.method', 'update') @@ -332,7 +335,7 @@ $utopia->patch('/v1/projects/:projectId') throw new Exception('Project not found', 404); } - $project = $consoleDB->updateDocument(array_merge($project->getArrayCopy(), [ + $project = $consoleDB->updateDocument(\array_merge($project->getArrayCopy(), [ 'name' => $name, 'description' => $description, 'logo' => $logo, @@ -355,11 +358,12 @@ $utopia->patch('/v1/projects/:projectId') $utopia->patch('/v1/projects/:projectId/oauth2') ->desc('Update Project OAuth2') + ->groups(['api', 'projects']) ->label('scope', 'projects.write') ->label('sdk.namespace', 'projects') ->label('sdk.method', 'updateOAuth2') ->param('projectId', '', function () { return new UID(); }, 'Project unique ID.') - ->param('provider', '', function () { return new WhiteList(array_keys(Config::getParam('providers'))); }, 'Provider Name', false) + ->param('provider', '', function () { return new WhiteList(\array_keys(Config::getParam('providers'))); }, 'Provider Name', false) ->param('appId', '', function () { return new Text(256); }, 'Provider app ID.', true) ->param('secret', '', function () { return new text(512); }, 'Provider secret key.', true) ->action( @@ -373,17 +377,17 @@ $utopia->patch('/v1/projects/:projectId/oauth2') $key = $request->getServer('_APP_OPENSSL_KEY_V1'); $iv = OpenSSL::randomPseudoBytes(OpenSSL::cipherIVLength(OpenSSL::CIPHER_AES_128_GCM)); $tag = null; - $secret = json_encode([ + $secret = \json_encode([ 'data' => OpenSSL::encrypt($secret, OpenSSL::CIPHER_AES_128_GCM, $key, 0, $iv, $tag), 'method' => OpenSSL::CIPHER_AES_128_GCM, - 'iv' => bin2hex($iv), - 'tag' => bin2hex($tag), + 'iv' => \bin2hex($iv), + 'tag' => \bin2hex($tag), 'version' => '1', ]); - $project = $consoleDB->updateDocument(array_merge($project->getArrayCopy(), [ - 'usersOauth2'.ucfirst($provider).'Appid' => $appId, - 'usersOauth2'.ucfirst($provider).'Secret' => $secret, + $project = $consoleDB->updateDocument(\array_merge($project->getArrayCopy(), [ + 'usersOauth2'.\ucfirst($provider).'Appid' => $appId, + 'usersOauth2'.\ucfirst($provider).'Secret' => $secret, ])); if (false === $project) { @@ -396,6 +400,7 @@ $utopia->patch('/v1/projects/:projectId/oauth2') $utopia->delete('/v1/projects/:projectId') ->desc('Delete Project') + ->groups(['api', 'projects']) ->label('scope', 'projects.write') ->label('sdk.namespace', 'projects') ->label('sdk.method', 'delete') @@ -415,7 +420,7 @@ $utopia->delete('/v1/projects/:projectId') $deletes->setParam('document', $project->getArrayCopy()); - foreach(['keys', 'webhooks', 'tasks', 'platforms', 'domains'] as $key) { // Delete all children (keys, webhooks, tasks [stop tasks?], platforms) + foreach (['keys', 'webhooks', 'tasks', 'platforms', 'domains'] as $key) { // Delete all children (keys, webhooks, tasks [stop tasks?], platforms) $list = $project->getAttribute('webhooks', []); foreach ($list as $document) { /* @var $document Document */ @@ -441,6 +446,7 @@ $utopia->delete('/v1/projects/:projectId') $utopia->post('/v1/projects/:projectId/webhooks') ->desc('Create Webhook') + ->groups(['api', 'projects']) ->label('scope', 'projects.write') ->label('sdk.namespace', 'projects') ->label('sdk.method', 'createWebhook') @@ -462,11 +468,11 @@ $utopia->post('/v1/projects/:projectId/webhooks') $key = $request->getServer('_APP_OPENSSL_KEY_V1'); $iv = OpenSSL::randomPseudoBytes(OpenSSL::cipherIVLength(OpenSSL::CIPHER_AES_128_GCM)); $tag = null; - $httpPass = json_encode([ + $httpPass = \json_encode([ 'data' => OpenSSL::encrypt($httpPass, OpenSSL::CIPHER_AES_128_GCM, $key, 0, $iv, $tag), 'method' => OpenSSL::CIPHER_AES_128_GCM, - 'iv' => bin2hex($iv), - 'tag' => bin2hex($tag), + 'iv' => \bin2hex($iv), + 'tag' => \bin2hex($tag), 'version' => '1', ]); @@ -505,6 +511,7 @@ $utopia->post('/v1/projects/:projectId/webhooks') $utopia->get('/v1/projects/:projectId/webhooks') ->desc('List Webhooks') + ->groups(['api', 'projects']) ->label('scope', 'projects.read') ->label('sdk.namespace', 'projects') ->label('sdk.method', 'listWebhooks') @@ -520,7 +527,7 @@ $utopia->get('/v1/projects/:projectId/webhooks') $webhooks = $project->getAttribute('webhooks', []); foreach ($webhooks as $webhook) { /* @var $webhook Document */ - $httpPass = json_decode($webhook->getAttribute('httpPass', '{}'), true); + $httpPass = \json_decode($webhook->getAttribute('httpPass', '{}'), true); if (empty($httpPass) || !isset($httpPass['version'])) { continue; @@ -528,7 +535,7 @@ $utopia->get('/v1/projects/:projectId/webhooks') $key = $request->getServer('_APP_OPENSSL_KEY_V'.$httpPass['version']); - $webhook->setAttribute('httpPass', OpenSSL::decrypt($httpPass['data'], $httpPass['method'], $key, 0, hex2bin($httpPass['iv']), hex2bin($httpPass['tag']))); + $webhook->setAttribute('httpPass', OpenSSL::decrypt($httpPass['data'], $httpPass['method'], $key, 0, \hex2bin($httpPass['iv']), \hex2bin($httpPass['tag']))); } $response->json($webhooks); @@ -537,6 +544,7 @@ $utopia->get('/v1/projects/:projectId/webhooks') $utopia->get('/v1/projects/:projectId/webhooks/:webhookId') ->desc('Get Webhook') + ->groups(['api', 'projects']) ->label('scope', 'projects.read') ->label('sdk.namespace', 'projects') ->label('sdk.method', 'getWebhook') @@ -556,11 +564,11 @@ $utopia->get('/v1/projects/:projectId/webhooks/:webhookId') throw new Exception('Webhook not found', 404); } - $httpPass = json_decode($webhook->getAttribute('httpPass', '{}'), true); + $httpPass = \json_decode($webhook->getAttribute('httpPass', '{}'), true); if (!empty($httpPass) && isset($httpPass['version'])) { $key = $request->getServer('_APP_OPENSSL_KEY_V'.$httpPass['version']); - $webhook->setAttribute('httpPass', OpenSSL::decrypt($httpPass['data'], $httpPass['method'], $key, 0, hex2bin($httpPass['iv']), hex2bin($httpPass['tag']))); + $webhook->setAttribute('httpPass', OpenSSL::decrypt($httpPass['data'], $httpPass['method'], $key, 0, \hex2bin($httpPass['iv']), \hex2bin($httpPass['tag']))); } $response->json($webhook->getArrayCopy()); @@ -570,6 +578,7 @@ $utopia->get('/v1/projects/:projectId/webhooks/:webhookId') $utopia->put('/v1/projects/:projectId/webhooks/:webhookId') ->desc('Update Webhook') + ->groups(['api', 'projects']) ->label('scope', 'projects.write') ->label('sdk.namespace', 'projects') ->label('sdk.method', 'updateWebhook') @@ -592,11 +601,11 @@ $utopia->put('/v1/projects/:projectId/webhooks/:webhookId') $key = $request->getServer('_APP_OPENSSL_KEY_V1'); $iv = OpenSSL::randomPseudoBytes(OpenSSL::cipherIVLength(OpenSSL::CIPHER_AES_128_GCM)); $tag = null; - $httpPass = json_encode([ + $httpPass = \json_encode([ 'data' => OpenSSL::encrypt($httpPass, OpenSSL::CIPHER_AES_128_GCM, $key, 0, $iv, $tag), 'method' => OpenSSL::CIPHER_AES_128_GCM, - 'iv' => bin2hex($iv), - 'tag' => bin2hex($tag), + 'iv' => \bin2hex($iv), + 'tag' => \bin2hex($tag), 'version' => '1', ]); @@ -625,6 +634,7 @@ $utopia->put('/v1/projects/:projectId/webhooks/:webhookId') $utopia->delete('/v1/projects/:projectId/webhooks/:webhookId') ->desc('Delete Webhook') + ->groups(['api', 'projects']) ->label('scope', 'projects.write') ->label('sdk.namespace', 'projects') ->label('sdk.method', 'deleteWebhook') @@ -656,6 +666,7 @@ $utopia->delete('/v1/projects/:projectId/webhooks/:webhookId') $utopia->post('/v1/projects/:projectId/keys') ->desc('Create Key') + ->groups(['api', 'projects']) ->label('scope', 'projects.write') ->label('sdk.namespace', 'projects') ->label('sdk.method', 'createKey') @@ -678,7 +689,7 @@ $utopia->post('/v1/projects/:projectId/keys') ], 'name' => $name, 'scopes' => $scopes, - 'secret' => bin2hex(random_bytes(128)), + 'secret' => \bin2hex(\random_bytes(128)), ]); if (false === $key) { @@ -702,6 +713,7 @@ $utopia->post('/v1/projects/:projectId/keys') $utopia->get('/v1/projects/:projectId/keys') ->desc('List Keys') + ->groups(['api', 'projects']) ->label('scope', 'projects.read') ->label('sdk.namespace', 'projects') ->label('sdk.method', 'listKeys') @@ -720,6 +732,7 @@ $utopia->get('/v1/projects/:projectId/keys') $utopia->get('/v1/projects/:projectId/keys/:keyId') ->desc('Get Key') + ->groups(['api', 'projects']) ->label('scope', 'projects.read') ->label('sdk.namespace', 'projects') ->label('sdk.method', 'getKey') @@ -745,6 +758,7 @@ $utopia->get('/v1/projects/:projectId/keys/:keyId') $utopia->put('/v1/projects/:projectId/keys/:keyId') ->desc('Update Key') + ->groups(['api', 'projects']) ->label('scope', 'projects.write') ->label('sdk.namespace', 'projects') ->label('sdk.method', 'updateKey') @@ -781,6 +795,7 @@ $utopia->put('/v1/projects/:projectId/keys/:keyId') $utopia->delete('/v1/projects/:projectId/keys/:keyId') ->desc('Delete Key') + ->groups(['api', 'projects']) ->label('scope', 'projects.write') ->label('sdk.namespace', 'projects') ->label('sdk.method', 'deleteKey') @@ -812,6 +827,7 @@ $utopia->delete('/v1/projects/:projectId/keys/:keyId') $utopia->post('/v1/projects/:projectId/tasks') ->desc('Create Task') + ->groups(['api', 'projects']) ->label('scope', 'projects.write') ->label('sdk.namespace', 'projects') ->label('sdk.method', 'createTask') @@ -839,11 +855,11 @@ $utopia->post('/v1/projects/:projectId/tasks') $key = $request->getServer('_APP_OPENSSL_KEY_V1'); $iv = OpenSSL::randomPseudoBytes(OpenSSL::cipherIVLength(OpenSSL::CIPHER_AES_128_GCM)); $tag = null; - $httpPass = json_encode([ + $httpPass = \json_encode([ 'data' => OpenSSL::encrypt($httpPass, OpenSSL::CIPHER_AES_128_GCM, $key, 0, $iv, $tag), 'method' => OpenSSL::CIPHER_AES_128_GCM, - 'iv' => bin2hex($iv), - 'tag' => bin2hex($tag), + 'iv' => \bin2hex($iv), + 'tag' => \bin2hex($tag), 'version' => '1', ]); @@ -856,7 +872,7 @@ $utopia->post('/v1/projects/:projectId/tasks') 'name' => $name, 'status' => $status, 'schedule' => $schedule, - 'updated' => time(), + 'updated' => \time(), 'previous' => null, 'next' => $next, 'security' => (int) $security, @@ -894,6 +910,7 @@ $utopia->post('/v1/projects/:projectId/tasks') $utopia->get('/v1/projects/:projectId/tasks') ->desc('List Tasks') + ->groups(['api', 'projects']) ->label('scope', 'projects.read') ->label('sdk.namespace', 'projects') ->label('sdk.method', 'listTasks') @@ -909,7 +926,7 @@ $utopia->get('/v1/projects/:projectId/tasks') $tasks = $project->getAttribute('tasks', []); foreach ($tasks as $task) { /* @var $task Document */ - $httpPass = json_decode($task->getAttribute('httpPass', '{}'), true); + $httpPass = \json_decode($task->getAttribute('httpPass', '{}'), true); if (empty($httpPass) || !isset($httpPass['version'])) { continue; @@ -917,7 +934,7 @@ $utopia->get('/v1/projects/:projectId/tasks') $key = $request->getServer('_APP_OPENSSL_KEY_V'.$httpPass['version']); - $task->setAttribute('httpPass', OpenSSL::decrypt($httpPass['data'], $httpPass['method'], $key, 0, hex2bin($httpPass['iv']), hex2bin($httpPass['tag']))); + $task->setAttribute('httpPass', OpenSSL::decrypt($httpPass['data'], $httpPass['method'], $key, 0, \hex2bin($httpPass['iv']), \hex2bin($httpPass['tag']))); } $response->json($tasks); @@ -926,6 +943,7 @@ $utopia->get('/v1/projects/:projectId/tasks') $utopia->get('/v1/projects/:projectId/tasks/:taskId') ->desc('Get Task') + ->groups(['api', 'projects']) ->label('scope', 'projects.read') ->label('sdk.namespace', 'projects') ->label('sdk.method', 'getTask') @@ -945,11 +963,11 @@ $utopia->get('/v1/projects/:projectId/tasks/:taskId') throw new Exception('Task not found', 404); } - $httpPass = json_decode($task->getAttribute('httpPass', '{}'), true); + $httpPass = \json_decode($task->getAttribute('httpPass', '{}'), true); if (!empty($httpPass) && isset($httpPass['version'])) { $key = $request->getServer('_APP_OPENSSL_KEY_V'.$httpPass['version']); - $task->setAttribute('httpPass', OpenSSL::decrypt($httpPass['data'], $httpPass['method'], $key, 0, hex2bin($httpPass['iv']), hex2bin($httpPass['tag']))); + $task->setAttribute('httpPass', OpenSSL::decrypt($httpPass['data'], $httpPass['method'], $key, 0, \hex2bin($httpPass['iv']), \hex2bin($httpPass['tag']))); } $response->json($task->getArrayCopy()); @@ -958,6 +976,7 @@ $utopia->get('/v1/projects/:projectId/tasks/:taskId') $utopia->put('/v1/projects/:projectId/tasks/:taskId') ->desc('Update Task') + ->groups(['api', 'projects']) ->label('scope', 'projects.write') ->label('sdk.namespace', 'projects') ->label('sdk.method', 'updateTask') @@ -992,11 +1011,11 @@ $utopia->put('/v1/projects/:projectId/tasks/:taskId') $key = $request->getServer('_APP_OPENSSL_KEY_V1'); $iv = OpenSSL::randomPseudoBytes(OpenSSL::cipherIVLength(OpenSSL::CIPHER_AES_128_GCM)); $tag = null; - $httpPass = json_encode([ + $httpPass = \json_encode([ 'data' => OpenSSL::encrypt($httpPass, OpenSSL::CIPHER_AES_128_GCM, $key, 0, $iv, $tag), 'method' => OpenSSL::CIPHER_AES_128_GCM, - 'iv' => bin2hex($iv), - 'tag' => bin2hex($tag), + 'iv' => \bin2hex($iv), + 'tag' => \bin2hex($tag), 'version' => '1', ]); @@ -1004,7 +1023,7 @@ $utopia->put('/v1/projects/:projectId/tasks/:taskId') ->setAttribute('name', $name) ->setAttribute('status', $status) ->setAttribute('schedule', $schedule) - ->setAttribute('updated', time()) + ->setAttribute('updated', \time()) ->setAttribute('next', $next) ->setAttribute('security', (int) $security) ->setAttribute('httpMethod', $httpMethod) @@ -1028,6 +1047,7 @@ $utopia->put('/v1/projects/:projectId/tasks/:taskId') $utopia->delete('/v1/projects/:projectId/tasks/:taskId') ->desc('Delete Task') + ->groups(['api', 'projects']) ->label('scope', 'projects.write') ->label('sdk.namespace', 'projects') ->label('sdk.method', 'deleteTask') @@ -1059,6 +1079,7 @@ $utopia->delete('/v1/projects/:projectId/tasks/:taskId') $utopia->post('/v1/projects/:projectId/platforms') ->desc('Create Platform') + ->groups(['api', 'projects']) ->label('scope', 'projects.write') ->label('sdk.namespace', 'projects') ->label('sdk.method', 'createPlatform') @@ -1087,8 +1108,8 @@ $utopia->post('/v1/projects/:projectId/platforms') 'key' => $key, 'store' => $store, 'hostname' => $hostname, - 'dateCreated' => time(), - 'dateUpdated' => time(), + 'dateCreated' => \time(), + 'dateUpdated' => \time(), ]); if (false === $platform) { @@ -1112,6 +1133,7 @@ $utopia->post('/v1/projects/:projectId/platforms') $utopia->get('/v1/projects/:projectId/platforms') ->desc('List Platforms') + ->groups(['api', 'projects']) ->label('scope', 'projects.read') ->label('sdk.namespace', 'projects') ->label('sdk.method', 'listPlatforms') @@ -1132,6 +1154,7 @@ $utopia->get('/v1/projects/:projectId/platforms') $utopia->get('/v1/projects/:projectId/platforms/:platformId') ->desc('Get Platform') + ->groups(['api', 'projects']) ->label('scope', 'projects.read') ->label('sdk.namespace', 'projects') ->label('sdk.method', 'getPlatform') @@ -1157,6 +1180,7 @@ $utopia->get('/v1/projects/:projectId/platforms/:platformId') $utopia->put('/v1/projects/:projectId/platforms/:platformId') ->desc('Update Platform') + ->groups(['api', 'projects']) ->label('scope', 'projects.write') ->label('sdk.namespace', 'projects') ->label('sdk.method', 'updatePlatform') @@ -1182,7 +1206,7 @@ $utopia->put('/v1/projects/:projectId/platforms/:platformId') $platform ->setAttribute('name', $name) - ->setAttribute('dateUpdated', time()) + ->setAttribute('dateUpdated', \time()) ->setAttribute('key', $key) ->setAttribute('store', $store) ->setAttribute('hostname', $hostname) @@ -1198,6 +1222,7 @@ $utopia->put('/v1/projects/:projectId/platforms/:platformId') $utopia->delete('/v1/projects/:projectId/platforms/:platformId') ->desc('Delete Platform') + ->groups(['api', 'projects']) ->label('scope', 'projects.write') ->label('sdk.namespace', 'projects') ->label('sdk.method', 'deletePlatform') @@ -1229,6 +1254,7 @@ $utopia->delete('/v1/projects/:projectId/platforms/:platformId') $utopia->post('/v1/projects/:projectId/domains') ->desc('Create Domain') + ->groups(['api', 'projects']) ->label('scope', 'projects.write') ->label('sdk.namespace', 'projects') ->label('sdk.method', 'createDomain') @@ -1250,7 +1276,7 @@ $utopia->post('/v1/projects/:projectId/domains') $target = new Domain($request->getServer('_APP_DOMAIN_TARGET', '')); - if(!$target->isKnown() || $target->isTest()) { + if (!$target->isKnown() || $target->isTest()) { throw new Exception('Unreachable CNAME target ('.$target->get().'), plesse use a domain with a public suffix.', 500); } @@ -1262,7 +1288,7 @@ $utopia->post('/v1/projects/:projectId/domains') 'read' => ['team:'.$project->getAttribute('teamId', null)], 'write' => ['team:'.$project->getAttribute('teamId', null).'/owner', 'team:'.$project->getAttribute('teamId', null).'/developer'], ], - 'updated' => time(), + 'updated' => \time(), 'domain' => $domain->get(), 'tld' => $domain->getSuffix(), 'registerable' => $domain->getRegisterable(), @@ -1291,6 +1317,7 @@ $utopia->post('/v1/projects/:projectId/domains') $utopia->get('/v1/projects/:projectId/domains') ->desc('List Domains') + ->groups(['api', 'projects']) ->label('scope', 'projects.read') ->label('sdk.namespace', 'projects') ->label('sdk.method', 'listDomains') @@ -1311,6 +1338,7 @@ $utopia->get('/v1/projects/:projectId/domains') $utopia->get('/v1/projects/:projectId/domains/:domainId') ->desc('Get Domain') + ->groups(['api', 'projects']) ->label('scope', 'projects.read') ->label('sdk.namespace', 'projects') ->label('sdk.method', 'getDomain') @@ -1336,6 +1364,7 @@ $utopia->get('/v1/projects/:projectId/domains/:domainId') $utopia->patch('/v1/projects/:projectId/domains/:domainId/verification') ->desc('Update Domain Verification Status') + ->groups(['api', 'projects']) ->label('scope', 'projects.write') ->label('sdk.namespace', 'projects') ->label('sdk.method', 'updateDomainVerification') @@ -1357,18 +1386,18 @@ $utopia->patch('/v1/projects/:projectId/domains/:domainId/verification') $target = new Domain($request->getServer('_APP_DOMAIN_TARGET', '')); - if(!$target->isKnown() || $target->isTest()) { + if (!$target->isKnown() || $target->isTest()) { throw new Exception('Unreachable CNAME target ('.$target->get().'), plesse use a domain with a public suffix.', 500); } - if($domain->getAttribute('verification') === true) { + if ($domain->getAttribute('verification') === true) { return $response->json($domain->getArrayCopy()); } // Verify Domain with DNS records $validator = new CNAME($target->get()); - if(!$validator->isValid($domain->getAttribute('domain', ''))) { + if (!$validator->isValid($domain->getAttribute('domain', ''))) { throw new Exception('Failed to verify domain', 401); } @@ -1392,6 +1421,7 @@ $utopia->patch('/v1/projects/:projectId/domains/:domainId/verification') $utopia->delete('/v1/projects/:projectId/domains/:domainId') ->desc('Delete Domain') + ->groups(['api', 'projects']) ->label('scope', 'projects.write') ->label('sdk.namespace', 'projects') ->label('sdk.method', 'deleteDomain') diff --git a/app/controllers/api/storage.php b/app/controllers/api/storage.php index ebcb85f32d..9a12b56048 100644 --- a/app/controllers/api/storage.php +++ b/app/controllers/api/storage.php @@ -23,8 +23,6 @@ use Appwrite\Storage\Compression\Algorithms\GZIP; use Appwrite\Resize\Resize; use Appwrite\OpenSSL\OpenSSL; -include_once __DIR__ . '/../shared/api.php'; - Storage::addDevice('local', new Local(APP_STORAGE_UPLOADS.'/app-'.$project->getId())); $fileLogos = [ // Based on this list @see http://stackoverflow.com/a/4212908/2299554 @@ -135,6 +133,7 @@ $mimes = [ $utopia->post('/v1/storage/files') ->desc('Create File') + ->groups(['api', 'storage']) ->label('scope', 'files.write') ->label('webhook', 'storage.files.create') ->label('sdk.platform', [APP_PLATFORM_CLIENT, APP_PLATFORM_SERVER]) @@ -165,9 +164,9 @@ $utopia->post('/v1/storage/files') } // Make sure we handle a single file and multiple files the same way - $file['name'] = (is_array($file['name']) && isset($file['name'][0])) ? $file['name'][0] : $file['name']; - $file['tmp_name'] = (is_array($file['tmp_name']) && isset($file['tmp_name'][0])) ? $file['tmp_name'][0] : $file['tmp_name']; - $file['size'] = (is_array($file['size']) && isset($file['size'][0])) ? $file['size'][0] : $file['size']; + $file['name'] = (\is_array($file['name']) && isset($file['name'][0])) ? $file['name'][0] : $file['name']; + $file['tmp_name'] = (\is_array($file['tmp_name']) && isset($file['tmp_name'][0])) ? $file['tmp_name'][0] : $file['tmp_name']; + $file['size'] = (\is_array($file['size']) && isset($file['size'][0])) ? $file['size'][0] : $file['size']; // Check if file type is allowed (feature for project settings?) //if (!$fileType->isValid($file['tmp_name'])) { @@ -190,7 +189,7 @@ $utopia->post('/v1/storage/files') // Save to storage $size = $device->getFileSize($file['tmp_name']); - $path = $device->getPath(uniqid().'.'.pathinfo($file['name'], PATHINFO_EXTENSION)); + $path = $device->getPath(\uniqid().'.'.\pathinfo($file['name'], PATHINFO_EXTENSION)); if (!$device->upload($file['tmp_name'], $path)) { // TODO deprecate 'upload' and replace with 'move' throw new Exception('Failed moving file', 500); @@ -198,7 +197,7 @@ $utopia->post('/v1/storage/files') $mimeType = $device->getFileMimeType($path); // Get mime-type before compression and encryption - if($request->getServer('_APP_STORAGE_ANTIVIRUS') === 'enabled') { // Check if scans are enabled + if ($request->getServer('_APP_STORAGE_ANTIVIRUS') === 'enabled') { // Check if scans are enabled $antiVirus = new Network('clamav', 3310); // Check if file size is exceeding allowed limit @@ -216,7 +215,7 @@ $utopia->post('/v1/storage/files') $iv = OpenSSL::randomPseudoBytes(OpenSSL::cipherIVLength(OpenSSL::CIPHER_AES_128_GCM)); $data = OpenSSL::encrypt($data, OpenSSL::CIPHER_AES_128_GCM, $key, 0, $iv, $tag); - if(!$device->write($path, $data)) { + if (!$device->write($path, $data)) { throw new Exception('Failed to save file', 500); } @@ -228,7 +227,7 @@ $utopia->post('/v1/storage/files') 'read' => $read, 'write' => $write, ], - 'dateCreated' => time(), + 'dateCreated' => \time(), 'folderId' => $folderId, 'name' => $file['name'], 'path' => $path, @@ -237,12 +236,12 @@ $utopia->post('/v1/storage/files') 'sizeOriginal' => $size, 'sizeActual' => $sizeActual, 'algorithm' => $compressor->getName(), - 'token' => bin2hex(random_bytes(64)), + 'token' => \bin2hex(\random_bytes(64)), 'comment' => '', 'fileOpenSSLVersion' => '1', 'fileOpenSSLCipher' => OpenSSL::CIPHER_AES_128_GCM, - 'fileOpenSSLTag' => bin2hex($tag), - 'fileOpenSSLIV' => bin2hex($iv), + 'fileOpenSSLTag' => \bin2hex($tag), + 'fileOpenSSLIV' => \bin2hex($iv), ]); if (false === $file) { @@ -271,6 +270,7 @@ $utopia->post('/v1/storage/files') $utopia->get('/v1/storage/files') ->desc('List Files') + ->groups(['api', 'storage']) ->label('scope', 'files.read') ->label('sdk.platform', [APP_PLATFORM_CLIENT, APP_PLATFORM_SERVER]) ->label('sdk.namespace', 'storage') @@ -294,7 +294,7 @@ $utopia->get('/v1/storage/files') ], ]); - $results = array_map(function ($value) { /* @var $value \Database\Document */ + $results = \array_map(function ($value) { /* @var $value \Database\Document */ return $value->getArrayCopy(['$id', '$permissions', 'name', 'dateCreated', 'signature', 'mimeType', 'sizeOriginal']); }, $results); @@ -304,6 +304,7 @@ $utopia->get('/v1/storage/files') $utopia->get('/v1/storage/files/:fileId') ->desc('Get File') + ->groups(['api', 'storage']) ->label('scope', 'files.read') ->label('sdk.platform', [APP_PLATFORM_CLIENT, APP_PLATFORM_SERVER]) ->label('sdk.namespace', 'storage') @@ -324,6 +325,7 @@ $utopia->get('/v1/storage/files/:fileId') $utopia->get('/v1/storage/files/:fileId/preview') ->desc('Get File Preview') + ->groups(['api', 'storage']) ->label('scope', 'files.read') ->label('sdk.platform', [APP_PLATFORM_CLIENT, APP_PLATFORM_SERVER]) ->label('sdk.namespace', 'storage') @@ -336,14 +338,12 @@ $utopia->get('/v1/storage/files/:fileId/preview') ->param('height', 0, function () { return new Range(0, 4000); }, 'Resize preview image height, Pass an integer between 0 to 4000.', true) ->param('quality', 100, function () { return new Range(0, 100); }, 'Preview image quality. Pass an integer between 0 to 100. Defaults to 100.', true) ->param('background', '', function () { return new HexColor(); }, 'Preview image background color. Only works with transparent images (png). Use a valid HEX color, no # is needed for prefix.', true) - ->param('output', null, function () use ($outputs) { return new WhiteList(array_merge(array_keys($outputs), [null])); }, 'Output format type (jpeg, jpg, png, gif and webp).', true) - //->param('storage', 'local', function () {return new WhiteList(array('local'));}, 'Selected storage device. defaults to local') - //->param('token', '', function () {return new Text(128);}, 'Preview token', true) + ->param('output', null, function () use ($outputs) { return new WhiteList(\array_merge(\array_keys($outputs), [null])); }, 'Output format type (jpeg, jpg, png, gif and webp).', true) ->action( function ($fileId, $width, $height, $quality, $background, $output) use ($request, $response, $projectDB, $project, $inputs, $outputs, $fileLogos) { $storage = 'local'; - if (!extension_loaded('imagick')) { + if (!\extension_loaded('imagick')) { throw new Exception('Imagick extension is missing', 500); } @@ -351,12 +351,12 @@ $utopia->get('/v1/storage/files/:fileId/preview') throw new Exception('No such storage device', 400); } - if ((strpos($request->getServer('HTTP_ACCEPT'), 'image/webp') === false) && ('webp' == $output)) { // Fallback webp to jpeg when no browser support + if ((\strpos($request->getServer('HTTP_ACCEPT'), 'image/webp') === false) && ('webp' == $output)) { // Fallback webp to jpeg when no browser support $output = 'jpg'; } - $date = date('D, d M Y H:i:s', time() + (60 * 60 * 24 * 45)).' GMT'; // 45 days cache - $key = md5($fileId.$width.$height.$quality.$background.$storage.$output); + $date = \date('D, d M Y H:i:s', \time() + (60 * 60 * 24 * 45)).' GMT'; // 45 days cache + $key = \md5($fileId.$width.$height.$quality.$background.$storage.$output); $file = $projectDB->getDocument($fileId); @@ -365,24 +365,24 @@ $utopia->get('/v1/storage/files/:fileId/preview') } $path = $file->getAttribute('path'); - $type = strtolower(pathinfo($path, PATHINFO_EXTENSION)); + $type = \strtolower(\pathinfo($path, PATHINFO_EXTENSION)); $algorithm = $file->getAttribute('algorithm'); $cipher = $file->getAttribute('fileOpenSSLCipher'); $mime = $file->getAttribute('mimeType'); - if(!in_array($mime, $inputs)) { - $path = (array_key_exists($mime, $fileLogos)) ? $fileLogos[$mime] : $fileLogos['default']; + if (!\in_array($mime, $inputs)) { + $path = (\array_key_exists($mime, $fileLogos)) ? $fileLogos[$mime] : $fileLogos['default']; $algorithm = null; $cipher = null; $background = (empty($background)) ? 'eceff1' : $background; - $type = strtolower(pathinfo($path, PATHINFO_EXTENSION)); - $key = md5($path.$width.$height.$quality.$background.$storage.$output); + $type = \strtolower(\pathinfo($path, PATHINFO_EXTENSION)); + $key = \md5($path.$width.$height.$quality.$background.$storage.$output); } $compressor = new GZIP(); $device = Storage::getDevice('local'); - if (!file_exists($path)) { + if (!\file_exists($path)) { throw new Exception('File not found', 404); } @@ -393,7 +393,7 @@ $utopia->get('/v1/storage/files/:fileId/preview') $output = (empty($output)) ? $type : $output; $response - ->setContentType((in_array($output, $outputs)) ? $outputs[$output] : $outputs['jpg']) + ->setContentType((\in_array($output, $outputs)) ? $outputs[$output] : $outputs['jpg']) ->addHeader('Expires', $date) ->addHeader('X-Appwrite-Cache', 'hit') ->send($data) @@ -410,8 +410,8 @@ $utopia->get('/v1/storage/files/:fileId/preview') $file->getAttribute('fileOpenSSLCipher'), $request->getServer('_APP_OPENSSL_KEY_V'.$file->getAttribute('fileOpenSSLVersion')), 0, - hex2bin($file->getAttribute('fileOpenSSLIV')), - hex2bin($file->getAttribute('fileOpenSSLTag')) + \hex2bin($file->getAttribute('fileOpenSSLIV')), + \hex2bin($file->getAttribute('fileOpenSSLTag')) ); } @@ -448,6 +448,7 @@ $utopia->get('/v1/storage/files/:fileId/preview') $utopia->get('/v1/storage/files/:fileId/download') ->desc('Get File for Download') + ->groups(['api', 'storage']) ->label('scope', 'files.read') ->label('sdk.platform', [APP_PLATFORM_CLIENT, APP_PLATFORM_SERVER]) ->label('sdk.namespace', 'storage') @@ -466,7 +467,7 @@ $utopia->get('/v1/storage/files/:fileId/download') $path = $file->getAttribute('path', ''); - if (!file_exists($path)) { + if (!\file_exists($path)) { throw new Exception('File not found in '.$path, 404); } @@ -481,8 +482,8 @@ $utopia->get('/v1/storage/files/:fileId/download') $file->getAttribute('fileOpenSSLCipher'), $request->getServer('_APP_OPENSSL_KEY_V'.$file->getAttribute('fileOpenSSLVersion')), 0, - hex2bin($file->getAttribute('fileOpenSSLIV')), - hex2bin($file->getAttribute('fileOpenSSLTag')) + \hex2bin($file->getAttribute('fileOpenSSLIV')), + \hex2bin($file->getAttribute('fileOpenSSLTag')) ); } @@ -492,8 +493,8 @@ $utopia->get('/v1/storage/files/:fileId/download') $response ->setContentType($file->getAttribute('mimeType')) ->addHeader('Content-Disposition', 'attachment; filename="'.$file->getAttribute('name', '').'"') - ->addHeader('Expires', date('D, d M Y H:i:s', time() + (60 * 60 * 24 * 45)).' GMT') // 45 days cache - ->addHeader('X-Peak', memory_get_peak_usage()) + ->addHeader('Expires', \date('D, d M Y H:i:s', \time() + (60 * 60 * 24 * 45)).' GMT') // 45 days cache + ->addHeader('X-Peak', \memory_get_peak_usage()) ->send($source) ; } @@ -501,6 +502,7 @@ $utopia->get('/v1/storage/files/:fileId/download') $utopia->get('/v1/storage/files/:fileId/view') ->desc('Get File for View') + ->groups(['api', 'storage']) ->label('scope', 'files.read') ->label('sdk.platform', [APP_PLATFORM_CLIENT, APP_PLATFORM_SERVER]) ->label('sdk.namespace', 'storage') @@ -520,7 +522,7 @@ $utopia->get('/v1/storage/files/:fileId/view') $path = $file->getAttribute('path', ''); - if (!file_exists($path)) { + if (!\file_exists($path)) { throw new Exception('File not found in '.$path, 404); } @@ -529,7 +531,7 @@ $utopia->get('/v1/storage/files/:fileId/view') $contentType = 'text/plain'; - if (in_array($file->getAttribute('mimeType'), $mimes)) { + if (\in_array($file->getAttribute('mimeType'), $mimes)) { $contentType = $file->getAttribute('mimeType'); } @@ -541,8 +543,8 @@ $utopia->get('/v1/storage/files/:fileId/view') $file->getAttribute('fileOpenSSLCipher'), $request->getServer('_APP_OPENSSL_KEY_V'.$file->getAttribute('fileOpenSSLVersion')), 0, - hex2bin($file->getAttribute('fileOpenSSLIV')), - hex2bin($file->getAttribute('fileOpenSSLTag')) + \hex2bin($file->getAttribute('fileOpenSSLIV')), + \hex2bin($file->getAttribute('fileOpenSSLTag')) ); } @@ -554,7 +556,7 @@ $utopia->get('/v1/storage/files/:fileId/view') 'text' => 'text/plain', ]; - $contentType = (array_key_exists($as, $contentTypes)) ? $contentTypes[$as] : $contentType; + $contentType = (\array_key_exists($as, $contentTypes)) ? $contentTypes[$as] : $contentType; // Response $response @@ -562,8 +564,8 @@ $utopia->get('/v1/storage/files/:fileId/view') ->addHeader('Content-Security-Policy', 'script-src none;') ->addHeader('X-Content-Type-Options', 'nosniff') ->addHeader('Content-Disposition', 'inline; filename="'.$fileName.'"') - ->addHeader('Expires', date('D, d M Y H:i:s', time() + (60 * 60 * 24 * 45)).' GMT') // 45 days cache - ->addHeader('X-Peak', memory_get_peak_usage()) + ->addHeader('Expires', \date('D, d M Y H:i:s', \time() + (60 * 60 * 24 * 45)).' GMT') // 45 days cache + ->addHeader('X-Peak', \memory_get_peak_usage()) ->send($output) ; } @@ -571,6 +573,7 @@ $utopia->get('/v1/storage/files/:fileId/view') $utopia->put('/v1/storage/files/:fileId') ->desc('Update File') + ->groups(['api', 'storage']) ->label('scope', 'files.write') ->label('webhook', 'storage.files.update') ->label('sdk.platform', [APP_PLATFORM_CLIENT, APP_PLATFORM_SERVER]) @@ -589,7 +592,7 @@ $utopia->put('/v1/storage/files/:fileId') throw new Exception('File not found', 404); } - $file = $projectDB->updateDocument(array_merge($file->getArrayCopy(), [ + $file = $projectDB->updateDocument(\array_merge($file->getArrayCopy(), [ '$permissions' => [ 'read' => $read, 'write' => $write, @@ -616,6 +619,7 @@ $utopia->put('/v1/storage/files/:fileId') $utopia->delete('/v1/storage/files/:fileId') ->desc('Delete File') + ->groups(['api', 'storage']) ->label('scope', 'files.write') ->label('webhook', 'storage.files.delete') ->label('sdk.platform', [APP_PLATFORM_CLIENT, APP_PLATFORM_SERVER]) @@ -658,6 +662,7 @@ $utopia->delete('/v1/storage/files/:fileId') // $utopia->get('/v1/storage/files/:fileId/scan') // ->desc('Scan Storage') +// ->groups(['api', 'storage']) // ->label('scope', 'god') // ->label('sdk.platform', [APP_PLATFORM_CLIENT, APP_PLATFORM_SERVER]) // ->label('sdk.namespace', 'storage') diff --git a/app/controllers/api/teams.php b/app/controllers/api/teams.php index ded2445faa..67b4599b2d 100644 --- a/app/controllers/api/teams.php +++ b/app/controllers/api/teams.php @@ -1,6 +1,6 @@ post('/v1/teams') ->desc('Create Team') + ->groups(['api', 'teams']) ->label('scope', 'teams.write') ->label('sdk.platform', [APP_PLATFORM_CLIENT, APP_PLATFORM_SERVER]) ->label('sdk.namespace', 'teams') @@ -43,7 +42,7 @@ $utopia->post('/v1/teams') ], 'name' => $name, 'sum' => ($mode !== APP_MODE_ADMIN && $user->getId()) ? 1 : 0, - 'dateCreated' => time(), + 'dateCreated' => \time(), ]); Authorization::reset(); @@ -62,8 +61,8 @@ $utopia->post('/v1/teams') 'userId' => $user->getId(), 'teamId' => $team->getId(), 'roles' => $roles, - 'invited' => time(), - 'joined' => time(), + 'invited' => \time(), + 'joined' => \time(), 'confirm' => true, 'secret' => '', ]); @@ -87,6 +86,7 @@ $utopia->post('/v1/teams') $utopia->get('/v1/teams') ->desc('List Teams') + ->groups(['api', 'teams']) ->label('scope', 'teams.read') ->label('sdk.platform', [APP_PLATFORM_CLIENT, APP_PLATFORM_SERVER]) ->label('sdk.namespace', 'teams') @@ -116,6 +116,7 @@ $utopia->get('/v1/teams') $utopia->get('/v1/teams/:teamId') ->desc('Get Team') + ->groups(['api', 'teams']) ->label('scope', 'teams.read') ->label('sdk.platform', [APP_PLATFORM_CLIENT, APP_PLATFORM_SERVER]) ->label('sdk.namespace', 'teams') @@ -136,6 +137,7 @@ $utopia->get('/v1/teams/:teamId') $utopia->put('/v1/teams/:teamId') ->desc('Update Team') + ->groups(['api', 'teams']) ->label('scope', 'teams.write') ->label('sdk.platform', [APP_PLATFORM_CLIENT, APP_PLATFORM_SERVER]) ->label('sdk.namespace', 'teams') @@ -151,7 +153,7 @@ $utopia->put('/v1/teams/:teamId') throw new Exception('Team not found', 404); } - $team = $projectDB->updateDocument(array_merge($team->getArrayCopy(), [ + $team = $projectDB->updateDocument(\array_merge($team->getArrayCopy(), [ 'name' => $name, ])); @@ -165,6 +167,7 @@ $utopia->put('/v1/teams/:teamId') $utopia->delete('/v1/teams/:teamId') ->desc('Delete Team') + ->groups(['api', 'teams']) ->label('scope', 'teams.write') ->label('sdk.platform', [APP_PLATFORM_CLIENT, APP_PLATFORM_SERVER]) ->label('sdk.namespace', 'teams') @@ -204,6 +207,7 @@ $utopia->delete('/v1/teams/:teamId') $utopia->post('/v1/teams/:teamId/memberships') ->desc('Create Team Membership') + ->groups(['api', 'teams']) ->label('scope', 'teams.write') ->label('sdk.platform', [APP_PLATFORM_CLIENT, APP_PLATFORM_SERVER]) ->label('sdk.namespace', 'teams') @@ -215,7 +219,7 @@ $utopia->post('/v1/teams/:teamId/memberships') ->param('roles', [], function () { return new ArrayList(new Text(128)); }, 'Array of strings. Use this param to set the user roles in the team. A role can be any string. Learn more about [roles and permissions](/docs/permissions).') ->param('url', '', function () use ($clients) { return new Host($clients); }, 'URL to redirect the user back to your app from the invitation email. Only URLs from hostnames in your project platform list are allowed. This requirement helps to prevent an [open redirect](https://cheatsheetseries.owasp.org/cheatsheets/Unvalidated_Redirects_and_Forwards_Cheat_Sheet.html) attack against your project API.') // TODO add our own built-in confirm page ->action( - function ($teamId, $email, $name, $roles, $url) use ($response, $register, $project, $user, $audit, $projectDB, $mode) { + function ($teamId, $email, $name, $roles, $url) use ($response, $mail, $project, $user, $audit, $projectDB, $mode) { $name = (empty($name)) ? $email : $name; $team = $projectDB->getDocument($teamId); @@ -256,8 +260,8 @@ $utopia->post('/v1/teams/:teamId/memberships') 'emailVerification' => false, 'status' => Auth::USER_STATUS_UNACTIVATED, 'password' => Auth::passwordHash(Auth::passwordGenerator()), - 'password-update' => time(), - 'registration' => time(), + 'password-update' => \time(), + 'registration' => \time(), 'reset' => false, 'name' => $name, 'tokens' => [], @@ -280,7 +284,7 @@ $utopia->post('/v1/teams/:teamId/memberships') throw new Exception('User has already been invited or is already a member of this team', 409); } - if ($member->getAttribute('userId') == $user->getId() && in_array('owner', $member->getAttribute('roles', []))) { + if ($member->getAttribute('userId') == $user->getId() && \in_array('owner', $member->getAttribute('roles', []))) { $isOwner = true; } } @@ -300,18 +304,17 @@ $utopia->post('/v1/teams/:teamId/memberships') 'userId' => $invitee->getId(), 'teamId' => $team->getId(), 'roles' => $roles, - 'invited' => time(), + 'invited' => \time(), 'joined' => 0, 'confirm' => (APP_MODE_ADMIN === $mode), 'secret' => Auth::hash($secret), ]); - if(APP_MODE_ADMIN === $mode) { // Allow admin to create membership + if (APP_MODE_ADMIN === $mode) { // Allow admin to create membership Authorization::disable(); $membership = $projectDB->createDocument($membership->getArrayCopy()); Authorization::reset(); - } - else { + } else { $membership = $projectDB->createDocument($membership->getArrayCopy()); } @@ -323,29 +326,36 @@ $utopia->post('/v1/teams/:teamId/memberships') $url['query'] = Template::mergeQuery(((isset($url['query'])) ? $url['query'] : ''), ['inviteId' => $membership->getId(), 'teamId' => $team->getId(), 'userId' => $invitee->getId(), 'secret' => $secret, 'teamId' => $teamId]); $url = Template::unParseURL($url); - $body = new Template(__DIR__.'/../../config/locales/templates/'.Locale::getText('account.emails.invitation.body')); + $body = new Template(__DIR__.'/../../config/locales/templates/_base.tpl'); + $content = new Template(__DIR__.'/../../config/locales/templates/'.Locale::getText('account.emails.invitation.body')); + $cta = new Template(__DIR__.'/../../config/locales/templates/_cta.tpl'); + $body + ->setParam('{{content}}', $content->render()) + ->setParam('{{cta}}', $cta->render()) + ->setParam('{{title}}', Locale::getText('account.emails.invitation.title')) ->setParam('{{direction}}', Locale::getText('settings.direction')) ->setParam('{{project}}', $project->getAttribute('name', ['[APP-NAME]'])) ->setParam('{{team}}', $team->getAttribute('name', '[TEAM-NAME]')) ->setParam('{{owner}}', $user->getAttribute('name', '')) ->setParam('{{redirect}}', $url) + ->setParam('{{bg-body}}', '#f6f6f6') + ->setParam('{{bg-content}}', '#ffffff') + ->setParam('{{bg-cta}}', '#3498db') + ->setParam('{{bg-cta-hover}}', '#34495e') + ->setParam('{{text-content}}', '#000000') + ->setParam('{{text-cta}}', '#ffffff') ; - $mail = $register->get('smtp'); /* @var $mail \PHPMailer\PHPMailer\PHPMailer */ - - $mail->addAddress($email, $name); - - $mail->Subject = sprintf(Locale::getText('account.emails.invitation.title'), $team->getAttribute('name', '[TEAM-NAME]'), $project->getAttribute('name', ['[APP-NAME]'])); - $mail->Body = $body->render(); - $mail->AltBody = strip_tags($body->render()); - - try { - if(APP_MODE_ADMIN !== $mode) { // No need in comfirmation when in admin mode - $mail->send(); - } - } catch (\Exception $error) { - throw new Exception('Error sending mail: ' . $error->getMessage(), 500); + if (APP_MODE_ADMIN !== $mode) { // No need in comfirmation when in admin mode + $mail + ->setParam('event', 'teams.membership.create') + ->setParam('recipient', $email) + ->setParam('name', $name) + ->setParam('subject', \sprintf(Locale::getText('account.emails.invitation.title'), $team->getAttribute('name', '[TEAM-NAME]'), $project->getAttribute('name', ['[APP-NAME]']))) + ->setParam('body', $body->render()) + ->trigger(); + ; } $audit @@ -356,7 +366,7 @@ $utopia->post('/v1/teams/:teamId/memberships') $response ->setStatusCode(Response::STATUS_CODE_CREATED) // TODO change response of this endpoint - ->json(array_merge($membership->getArrayCopy([ + ->json(\array_merge($membership->getArrayCopy([ '$id', 'userId', 'teamId', @@ -374,6 +384,7 @@ $utopia->post('/v1/teams/:teamId/memberships') $utopia->get('/v1/teams/:teamId/memberships') ->desc('Get Team Memberships') + ->groups(['api', 'teams']) ->label('scope', 'teams.read') ->label('sdk.platform', [APP_PLATFORM_CLIENT, APP_PLATFORM_SERVER]) ->label('sdk.namespace', 'teams') @@ -414,7 +425,7 @@ $utopia->get('/v1/teams/:teamId/memberships') $temp = $projectDB->getDocument($membership->getAttribute('userId', null))->getArrayCopy(['email', 'name']); - $users[] = array_merge($temp, $membership->getArrayCopy([ + $users[] = \array_merge($temp, $membership->getArrayCopy([ '$id', 'userId', 'teamId', @@ -426,12 +437,12 @@ $utopia->get('/v1/teams/:teamId/memberships') } $response->json(['sum' => $projectDB->getSum(), 'memberships' => $users]); - } ); $utopia->patch('/v1/teams/:teamId/memberships/:inviteId/status') ->desc('Update Team Membership Status') + ->groups(['api', 'teams']) ->label('scope', 'public') ->label('sdk.platform', [APP_PLATFORM_CLIENT]) ->label('sdk.namespace', 'teams') @@ -488,7 +499,7 @@ $utopia->patch('/v1/teams/:teamId/memberships/:inviteId/status') } $membership // Attach user to team - ->setAttribute('joined', time()) + ->setAttribute('joined', \time()) ->setAttribute('confirm', true) ; @@ -498,7 +509,7 @@ $utopia->patch('/v1/teams/:teamId/memberships/:inviteId/status') ; // Log user in - $expiry = time() + Auth::TOKEN_EXPIRATION_LOGIN_LONG; + $expiry = \time() + Auth::TOKEN_EXPIRATION_LOGIN_LONG; $secret = Auth::tokenGenerator(); $user->setAttribute('tokens', new Document([ @@ -521,7 +532,7 @@ $utopia->patch('/v1/teams/:teamId/memberships/:inviteId/status') Authorization::disable(); - $team = $projectDB->updateDocument(array_merge($team->getArrayCopy(), [ + $team = $projectDB->updateDocument(\array_merge($team->getArrayCopy(), [ 'sum' => $team->getAttribute('sum', 0) + 1, ])); @@ -537,16 +548,16 @@ $utopia->patch('/v1/teams/:teamId/memberships/:inviteId/status') ->setParam('resource', 'teams/'.$teamId) ; - if(!Config::getParam('domainVerification')) { + if (!Config::getParam('domainVerification')) { $response - ->addHeader('X-Fallback-Cookies', json_encode([Auth::$cookieName => Auth::encodeSession($user->getId(), $secret)])) + ->addHeader('X-Fallback-Cookies', \json_encode([Auth::$cookieName => Auth::encodeSession($user->getId(), $secret)])) ; } $response ->addCookie(Auth::$cookieName.'_legacy', Auth::encodeSession($user->getId(), $secret), $expiry, '/', COOKIE_DOMAIN, ('https' == $protocol), true, null) ->addCookie(Auth::$cookieName, Auth::encodeSession($user->getId(), $secret), $expiry, '/', COOKIE_DOMAIN, ('https' == $protocol), true, COOKIE_SAMESITE) - ->json(array_merge($membership->getArrayCopy([ + ->json(\array_merge($membership->getArrayCopy([ '$id', 'userId', 'teamId', @@ -564,6 +575,7 @@ $utopia->patch('/v1/teams/:teamId/memberships/:inviteId/status') $utopia->delete('/v1/teams/:teamId/memberships/:inviteId') ->desc('Delete Team Membership') + ->groups(['api', 'teams']) ->label('scope', 'teams.write') ->label('sdk.platform', [APP_PLATFORM_CLIENT, APP_PLATFORM_SERVER]) ->label('sdk.namespace', 'teams') @@ -594,7 +606,7 @@ $utopia->delete('/v1/teams/:teamId/memberships/:inviteId') } if ($membership->getAttribute('confirm')) { // Count only confirmed members - $team = $projectDB->updateDocument(array_merge($team->getArrayCopy(), [ + $team = $projectDB->updateDocument(\array_merge($team->getArrayCopy(), [ 'sum' => $team->getAttribute('sum', 0) - 1, ])); } diff --git a/app/controllers/api/users.php b/app/controllers/api/users.php index 3eee6f5b02..c7be03e95b 100644 --- a/app/controllers/api/users.php +++ b/app/controllers/api/users.php @@ -21,10 +21,9 @@ use Appwrite\Database\Validator\UID; use DeviceDetector\DeviceDetector; use GeoIp2\Database\Reader; -include_once __DIR__ . '/../shared/api.php'; - $utopia->post('/v1/users') ->desc('Create User') + ->groups(['api', 'users']) ->label('scope', 'users.write') ->label('sdk.platform', [APP_PLATFORM_SERVER]) ->label('sdk.namespace', 'users') @@ -59,8 +58,8 @@ $utopia->post('/v1/users') 'emailVerification' => false, 'status' => Auth::USER_STATUS_UNACTIVATED, 'password' => Auth::passwordHash($password), - 'password-update' => time(), - 'registration' => time(), + 'password-update' => \time(), + 'registration' => \time(), 'reset' => false, 'name' => $name, ], ['email' => $email]); @@ -75,13 +74,13 @@ $utopia->post('/v1/users') continue; } - $oauth2Keys[] = 'oauth2'.ucfirst($key); - $oauth2Keys[] = 'oauth2'.ucfirst($key).'AccessToken'; + $oauth2Keys[] = 'oauth2'.\ucfirst($key); + $oauth2Keys[] = 'oauth2'.\ucfirst($key).'AccessToken'; } $response ->setStatusCode(Response::STATUS_CODE_CREATED) - ->json(array_merge($user->getArrayCopy(array_merge([ + ->json(\array_merge($user->getArrayCopy(\array_merge([ '$id', 'status', 'email', @@ -94,6 +93,7 @@ $utopia->post('/v1/users') $utopia->get('/v1/users') ->desc('List Users') + ->groups(['api', 'users']) ->label('scope', 'users.read') ->label('sdk.platform', [APP_PLATFORM_SERVER]) ->label('sdk.namespace', 'users') @@ -124,12 +124,12 @@ $utopia->get('/v1/users') continue; } - $oauth2Keys[] = 'oauth2'.ucfirst($key); - $oauth2Keys[] = 'oauth2'.ucfirst($key).'AccessToken'; + $oauth2Keys[] = 'oauth2'.\ucfirst($key); + $oauth2Keys[] = 'oauth2'.\ucfirst($key).'AccessToken'; } - $results = array_map(function ($value) use ($oauth2Keys) { /* @var $value \Database\Document */ - return $value->getArrayCopy(array_merge( + $results = \array_map(function ($value) use ($oauth2Keys) { /* @var $value \Database\Document */ + return $value->getArrayCopy(\array_merge( [ '$id', 'status', @@ -148,6 +148,7 @@ $utopia->get('/v1/users') $utopia->get('/v1/users/:userId') ->desc('Get User') + ->groups(['api', 'users']) ->label('scope', 'users.read') ->label('sdk.platform', [APP_PLATFORM_SERVER]) ->label('sdk.namespace', 'users') @@ -169,11 +170,11 @@ $utopia->get('/v1/users/:userId') continue; } - $oauth2Keys[] = 'oauth2'.ucfirst($key); - $oauth2Keys[] = 'oauth2'.ucfirst($key).'AccessToken'; + $oauth2Keys[] = 'oauth2'.\ucfirst($key); + $oauth2Keys[] = 'oauth2'.\ucfirst($key).'AccessToken'; } - $response->json(array_merge($user->getArrayCopy(array_merge( + $response->json(\array_merge($user->getArrayCopy(\array_merge( [ '$id', 'status', @@ -189,6 +190,7 @@ $utopia->get('/v1/users/:userId') $utopia->get('/v1/users/:userId/prefs') ->desc('Get User Preferences') + ->groups(['api', 'users']) ->label('scope', 'users.read') ->label('sdk.platform', [APP_PLATFORM_SERVER]) ->label('sdk.namespace', 'users') @@ -206,7 +208,7 @@ $utopia->get('/v1/users/:userId/prefs') $prefs = $user->getAttribute('prefs', ''); try { - $prefs = json_decode($prefs, true); + $prefs = \json_decode($prefs, true); $prefs = ($prefs) ? $prefs : []; } catch (\Exception $error) { throw new Exception('Failed to parse prefs', 500); @@ -218,6 +220,7 @@ $utopia->get('/v1/users/:userId/prefs') $utopia->get('/v1/users/:userId/sessions') ->desc('Get User Sessions') + ->groups(['api', 'users']) ->label('scope', 'users.read') ->label('sdk.platform', [APP_PLATFORM_SERVER]) ->label('sdk.namespace', 'users') @@ -265,7 +268,7 @@ $utopia->get('/v1/users/:userId/sessions') try { $record = $reader->country($token->getAttribute('ip', '')); - $sessions[$index]['geo']['isoCode'] = strtolower($record->country->isoCode); + $sessions[$index]['geo']['isoCode'] = \strtolower($record->country->isoCode); $sessions[$index]['geo']['country'] = (isset($countries[$record->country->isoCode])) ? $countries[$record->country->isoCode] : Locale::getText('locale.country.unknown'); } catch (\Exception $e) { $sessions[$index]['geo']['isoCode'] = '--'; @@ -281,6 +284,7 @@ $utopia->get('/v1/users/:userId/sessions') $utopia->get('/v1/users/:userId/logs') ->desc('Get User Logs') + ->groups(['api', 'users']) ->label('scope', 'users.read') ->label('sdk.platform', [APP_PLATFORM_SERVER]) ->label('sdk.namespace', 'users') @@ -335,7 +339,7 @@ $utopia->get('/v1/users/:userId/logs') $output[$i] = [ 'event' => $log['event'], 'ip' => $log['ip'], - 'time' => strtotime($log['time']), + 'time' => \strtotime($log['time']), 'OS' => $dd->getOs(), 'client' => $dd->getClient(), 'device' => $dd->getDevice(), @@ -346,7 +350,7 @@ $utopia->get('/v1/users/:userId/logs') try { $record = $reader->country($log['ip']); - $output[$i]['geo']['isoCode'] = strtolower($record->country->isoCode); + $output[$i]['geo']['isoCode'] = \strtolower($record->country->isoCode); $output[$i]['geo']['country'] = $record->country->name; $output[$i]['geo']['country'] = (isset($countries[$record->country->isoCode])) ? $countries[$record->country->isoCode] : Locale::getText('locale.country.unknown'); } catch (\Exception $e) { @@ -361,6 +365,7 @@ $utopia->get('/v1/users/:userId/logs') $utopia->patch('/v1/users/:userId/status') ->desc('Update User Status') + ->groups(['api', 'users']) ->label('scope', 'users.write') ->label('sdk.platform', [APP_PLATFORM_SERVER]) ->label('sdk.namespace', 'users') @@ -376,7 +381,7 @@ $utopia->patch('/v1/users/:userId/status') throw new Exception('User not found', 404); } - $user = $projectDB->updateDocument(array_merge($user->getArrayCopy(), [ + $user = $projectDB->updateDocument(\array_merge($user->getArrayCopy(), [ 'status' => (int)$status, ])); @@ -391,12 +396,12 @@ $utopia->patch('/v1/users/:userId/status') continue; } - $oauth2Keys[] = 'oauth2'.ucfirst($key); - $oauth2Keys[] = 'oauth2'.ucfirst($key).'AccessToken'; + $oauth2Keys[] = 'oauth2'.\ucfirst($key); + $oauth2Keys[] = 'oauth2'.\ucfirst($key).'AccessToken'; } $response - ->json(array_merge($user->getArrayCopy(array_merge([ + ->json(\array_merge($user->getArrayCopy(\array_merge([ '$id', 'status', 'email', @@ -409,6 +414,7 @@ $utopia->patch('/v1/users/:userId/status') $utopia->patch('/v1/users/:userId/prefs') ->desc('Update User Preferences') + ->groups(['api', 'users']) ->label('scope', 'users.write') ->label('sdk.platform', [APP_PLATFORM_SERVER]) ->label('sdk.namespace', 'users') @@ -424,11 +430,11 @@ $utopia->patch('/v1/users/:userId/prefs') throw new Exception('User not found', 404); } - $old = json_decode($user->getAttribute('prefs', '{}'), true); + $old = \json_decode($user->getAttribute('prefs', '{}'), true); $old = ($old) ? $old : []; - $user = $projectDB->updateDocument(array_merge($user->getArrayCopy(), [ - 'prefs' => json_encode(array_merge($old, $prefs)), + $user = $projectDB->updateDocument(\array_merge($user->getArrayCopy(), [ + 'prefs' => \json_encode(\array_merge($old, $prefs)), ])); if (false === $user) { @@ -438,7 +444,7 @@ $utopia->patch('/v1/users/:userId/prefs') $prefs = $user->getAttribute('prefs', ''); try { - $prefs = json_decode($prefs, true); + $prefs = \json_decode($prefs, true); $prefs = ($prefs) ? $prefs : []; } catch (\Exception $error) { throw new Exception('Failed to parse prefs', 500); @@ -450,6 +456,7 @@ $utopia->patch('/v1/users/:userId/prefs') $utopia->delete('/v1/users/:userId/sessions/:sessionId') ->desc('Delete User Session') + ->groups(['api', 'users']) ->label('scope', 'users.write') ->label('sdk.platform', [APP_PLATFORM_SERVER]) ->label('sdk.namespace', 'users') @@ -482,6 +489,7 @@ $utopia->delete('/v1/users/:userId/sessions/:sessionId') $utopia->delete('/v1/users/:userId/sessions') ->desc('Delete User Sessions') + ->groups(['api', 'users']) ->label('scope', 'users.write') ->label('sdk.platform', [APP_PLATFORM_SERVER]) ->label('sdk.namespace', 'users') diff --git a/app/controllers/mock.php b/app/controllers/mock.php index 1600fa857e..097501fbba 100644 --- a/app/controllers/mock.php +++ b/app/controllers/mock.php @@ -22,7 +22,6 @@ $utopia->get('/v1/mock/tests/foo') ->param('z', null, function () { return new ArrayList(new Text(256)); }, 'Sample array param') ->action( function ($x, $y, $z) { - } ); @@ -37,7 +36,6 @@ $utopia->post('/v1/mock/tests/foo') ->param('z', null, function () { return new ArrayList(new Text(256)); }, 'Sample array param') ->action( function ($x, $y, $z) { - } ); @@ -52,7 +50,6 @@ $utopia->patch('/v1/mock/tests/foo') ->param('z', null, function () { return new ArrayList(new Text(256)); }, 'Sample array param') ->action( function ($x, $y, $z) { - } ); @@ -67,7 +64,6 @@ $utopia->put('/v1/mock/tests/foo') ->param('z', null, function () { return new ArrayList(new Text(256)); }, 'Sample array param') ->action( function ($x, $y, $z) { - } ); @@ -82,7 +78,6 @@ $utopia->delete('/v1/mock/tests/foo') ->param('z', null, function () { return new ArrayList(new Text(256)); }, 'Sample array param') ->action( function ($x, $y, $z) { - } ); @@ -97,7 +92,6 @@ $utopia->get('/v1/mock/tests/bar') ->param('z', null, function () { return new ArrayList(new Text(256)); }, 'Sample array param') ->action( function ($x, $y, $z) { - } ); @@ -112,7 +106,6 @@ $utopia->post('/v1/mock/tests/bar') ->param('z', null, function () { return new ArrayList(new Text(256)); }, 'Sample array param') ->action( function ($x, $y, $z) { - } ); @@ -127,7 +120,6 @@ $utopia->patch('/v1/mock/tests/bar') ->param('z', null, function () { return new ArrayList(new Text(256)); }, 'Sample array param') ->action( function ($x, $y, $z) { - } ); @@ -142,7 +134,6 @@ $utopia->put('/v1/mock/tests/bar') ->param('z', null, function () { return new ArrayList(new Text(256)); }, 'Sample array param') ->action( function ($x, $y, $z) { - } ); @@ -157,7 +148,6 @@ $utopia->delete('/v1/mock/tests/bar') ->param('z', null, function () { return new ArrayList(new Text(256)); }, 'Sample array param') ->action( function ($x, $y, $z) { - } ); @@ -175,24 +165,24 @@ $utopia->post('/v1/mock/tests/general/upload') ->action( function ($x, $y, $z, $file) use ($request) { $file = $request->getFiles('file'); - $file['tmp_name'] = (is_array($file['tmp_name'])) ? $file['tmp_name'] : [$file['tmp_name']]; - $file['name'] = (is_array($file['name'])) ? $file['name'] : [$file['name']]; - $file['size'] = (is_array($file['size'])) ? $file['size'] : [$file['size']]; + $file['tmp_name'] = (\is_array($file['tmp_name'])) ? $file['tmp_name'] : [$file['tmp_name']]; + $file['name'] = (\is_array($file['name'])) ? $file['name'] : [$file['name']]; + $file['size'] = (\is_array($file['size'])) ? $file['size'] : [$file['size']]; foreach ($file['name'] as $i => $name) { - if($name !== 'file.png') { + if ($name !== 'file.png') { throw new Exception('Wrong file name', 400); } } foreach ($file['size'] as $i => $size) { - if($size !== 38756) { + if ($size !== 38756) { throw new Exception('Wrong file size', 400); } } foreach ($file['tmp_name'] as $i => $tmpName) { - if(md5(file_get_contents($tmpName)) !== 'd80e7e6999a3eb2ae0d631a96fe135a4') { + if (\md5(\file_get_contents($tmpName)) !== 'd80e7e6999a3eb2ae0d631a96fe135a4') { throw new Exception('Wrong file uploaded', 400); } } @@ -230,7 +220,7 @@ $utopia->get('/v1/mock/tests/general/set-cookie') ->label('sdk.description', 'Mock a set cookie request for SDK tests') ->action( function () use ($response) { - $response->addCookie('cookieName', 'cookieValue', time() + 31536000, '/', 'localhost', true, true); + $response->addCookie('cookieName', 'cookieValue', \time() + 31536000, '/', 'localhost', true, true); } ); @@ -242,7 +232,7 @@ $utopia->get('/v1/mock/tests/general/get-cookie') ->label('sdk.description', 'Mock a get cookie request for SDK tests') ->action( function () use ($request) { - if($request->getCookie('cookieName', '') !== 'cookieValue') { + if ($request->getCookie('cookieName', '') !== 'cookieValue') { throw new Exception('Missing cookie value', 400); } } @@ -271,7 +261,7 @@ $utopia->get('/v1/mock/tests/general/oauth2') ->param('state', '', function () { return new Text(1024); }, 'OAuth2 state.') ->action( function ($clientId, $redirectURI, $scope, $state) use ($response) { - $response->redirect($redirectURI.'?'.http_build_query(['code' => 'abcdef', 'state' => $state])); + $response->redirect($redirectURI.'?'.\http_build_query(['code' => 'abcdef', 'state' => $state])); } ); @@ -285,15 +275,15 @@ $utopia->get('/v1/mock/tests/general/oauth2/token') ->param('code', '', function () { return new Text(100); }, 'OAuth2 state.') ->action( function ($clientId, $redirectURI, $clientSecret, $code) use ($response) { - if($clientId != '1') { + if ($clientId != '1') { throw new Exception('Invalid client ID'); } - if($clientSecret != '123456') { + if ($clientSecret != '123456') { throw new Exception('Invalid client secret'); } - if($code != 'abcdef') { + if ($code != 'abcdef') { throw new Exception('Invalid token'); } @@ -308,7 +298,7 @@ $utopia->get('/v1/mock/tests/general/oauth2/user') ->param('token', '', function () { return new Text(100); }, 'OAuth2 Access Token.') ->action( function ($token) use ($response) { - if($token != '123456') { + if ($token != '123456') { throw new Exception('Invalid token'); } @@ -345,22 +335,21 @@ $utopia->get('/v1/mock/tests/general/oauth2/failure') ); $utopia->shutdown(function() use ($response, $request, &$result, $utopia) { - $route = $utopia->match($request); $path = APP_STORAGE_CACHE.'/tests.json'; - $tests = (file_exists($path)) ? json_decode(file_get_contents($path), true) : []; + $tests = (\file_exists($path)) ? \json_decode(\file_get_contents($path), true) : []; - if(!is_array($tests)) { + if (!\is_array($tests)) { throw new Exception('Failed to read results', 500); } $result[$route->getMethod() . ':' . $route->getURL()] = true; - $tests = array_merge($tests, $result); + $tests = \array_merge($tests, $result); - if(!file_put_contents($path, json_encode($tests), LOCK_EX)) { + if (!\file_put_contents($path, \json_encode($tests), LOCK_EX)) { throw new Exception('Failed to save resutls', 500); } $response->json(['result' => $route->getMethod() . ':' . $route->getURL() . ':passed']); -}); \ No newline at end of file +}, 'mock'); \ No newline at end of file diff --git a/app/controllers/shared/api.php b/app/controllers/shared/api.php index 0430212be7..e911c2c556 100644 --- a/app/controllers/shared/api.php +++ b/app/controllers/shared/api.php @@ -7,9 +7,12 @@ use Utopia\Abuse\Adapters\TimeLimit; global $utopia, $request, $response, $register, $user, $project; $utopia->init(function () use ($utopia, $request, $response, $register, $user, $project) { - $route = $utopia->match($request); + if (empty($project->getId()) && $route->getLabel('abuse-limit', 0) > 0) { // Abuse limit requires an active project scope + throw new Exception('Missing or unknown project ID', 400); + } + /* * Abuse Check */ @@ -27,7 +30,7 @@ $utopia->init(function () use ($utopia, $request, $response, $register, $user, $ //TODO make sure we get array here foreach ($request->getParams() as $key => $value) { // Set request params as potential abuse keys - $timeLimit->setParam('{param-'.$key.'}', (is_array($value)) ? json_encode($value) : $value); + $timeLimit->setParam('{param-'.$key.'}', (\is_array($value)) ? \json_encode($value) : $value); } $abuse = new Abuse($timeLimit); @@ -43,4 +46,4 @@ $utopia->init(function () use ($utopia, $request, $response, $register, $user, $ if ($abuse->check() && $request->getServer('_APP_OPTIONS_ABUSE', 'enabled') !== 'disabled') { throw new Exception('Too many requests', 429); } -}); \ No newline at end of file +}, 'api'); \ No newline at end of file diff --git a/app/controllers/shared/web.php b/app/controllers/shared/web.php index 42afb283e6..a2f85bcb0a 100644 --- a/app/controllers/shared/web.php +++ b/app/controllers/shared/web.php @@ -2,42 +2,37 @@ use Utopia\View; use Utopia\Config\Config; -use Utopia\Locale\Locale; - -Locale::$exceptions = false; - -$roles = [ - ['type' => 'owner', 'label' => 'Owner'], - ['type' => 'developer', 'label' => 'Developer'], - ['type' => 'admin', 'label' => 'Admin'], -]; $layout = new View(__DIR__.'/../../views/layouts/default.phtml'); -/* AJAX check */ -if (!empty($request->getQuery('version', ''))) { - $layout->setPath(__DIR__.'/../../views/layouts/empty.phtml'); -} - -$layout - ->setParam('title', APP_NAME) - ->setParam('protocol', Config::getParam('protocol')) - ->setParam('domain', $domain) - ->setParam('home', $request->getServer('_APP_HOME')) - ->setParam('setup', $request->getServer('_APP_SETUP')) - ->setParam('class', 'unknown') - ->setParam('icon', '/images/favicon.png') - ->setParam('roles', $roles) - ->setParam('env', $utopia->getEnv()) -; - $utopia->init(function () use ($utopia, $response, $request, $layout) { + + /* AJAX check */ + if (!empty($request->getQuery('version', ''))) { + $layout->setPath(__DIR__.'/../../views/layouts/empty.phtml'); + } + $layout + ->setParam('title', APP_NAME) + ->setParam('protocol', Config::getParam('protocol')) + ->setParam('domain', Config::getParam('domain')) + ->setParam('home', $request->getServer('_APP_HOME')) + ->setParam('setup', $request->getServer('_APP_SETUP')) + ->setParam('class', 'unknown') + ->setParam('icon', '/images/favicon.png') + ->setParam('roles', [ + ['type' => 'owner', 'label' => 'Owner'], + ['type' => 'developer', 'label' => 'Developer'], + ['type' => 'admin', 'label' => 'Admin'], + ]) + ->setParam('env', $utopia->getMode()) + ; + $time = (60 * 60 * 24 * 45); // 45 days cache - $isDev = (\Utopia\App::ENV_TYPE_DEVELOPMENT == Config::getParam('env')); + $isDev = (\Utopia\App::MODE_TYPE_DEVELOPMENT == Config::getParam('env')); $response ->addHeader('Cache-Control', 'public, max-age='.$time) - ->addHeader('Expires', date('D, d M Y H:i:s', time() + $time).' GMT') // 45 days cache + ->addHeader('Expires', \date('D, d M Y H:i:s', \time() + $time).' GMT') // 45 days cache ->addHeader('X-UA-Compatible', 'IE=Edge'); // Deny IE browsers from going into quirks mode $route = $utopia->match($request); @@ -47,4 +42,4 @@ $utopia->init(function () use ($utopia, $response, $request, $layout) { ->setParam('isDev', $isDev) ->setParam('class', $scope) ; -}); +}, 'web'); diff --git a/app/controllers/web/console.php b/app/controllers/web/console.php index 5ef32d1f4d..71f7a94f1a 100644 --- a/app/controllers/web/console.php +++ b/app/controllers/web/console.php @@ -1,7 +1,5 @@ init(function () use ($layout) { ->setParam('description', 'Appwrite Console allows you to easily manage, monitor, and control your entire backend API and tools.') ->setParam('analytics', 'UA-26264668-5') ; -}); +}, 'console'); $utopia->shutdown(function () use ($response, $request, $layout) { $header = new View(__DIR__.'/../../views/console/comps/header.phtml'); @@ -34,10 +32,10 @@ $utopia->shutdown(function () use ($response, $request, $layout) { ; $response->send($layout->render()); -}); +}, 'console'); $utopia->get('/error/:code') - ->desc('Error page') + ->groups(['web', 'console']) ->label('permission', 'public') ->label('scope', 'home') ->param('code', null, new \Utopia\Validator\Numeric(), 'Valid status code number', false) @@ -54,6 +52,7 @@ $utopia->get('/error/:code') }); $utopia->get('/console') + ->groups(['web', 'console']) ->label('permission', 'public') ->label('scope', 'console') ->action(function () use ($layout, $request) { @@ -69,6 +68,7 @@ $utopia->get('/console') }); $utopia->get('/console/account') + ->groups(['web', 'console']) ->label('permission', 'public') ->label('scope', 'console') ->action(function () use ($layout) { @@ -86,7 +86,7 @@ $utopia->get('/console/account') }); $utopia->get('/console/notifications') - ->desc('Platform console notifications') + ->groups(['web', 'console']) ->label('permission', 'public') ->label('scope', 'console') ->action(function () use ($layout) { @@ -98,7 +98,7 @@ $utopia->get('/console/notifications') }); $utopia->get('/console/home') - ->desc('Platform console project home') + ->groups(['web', 'console']) ->label('permission', 'public') ->label('scope', 'console') ->action(function () use ($layout) { @@ -110,7 +110,7 @@ $utopia->get('/console/home') }); $utopia->get('/console/settings') - ->desc('Platform console project settings') + ->groups(['web', 'console']) ->label('permission', 'public') ->label('scope', 'console') ->action(function () use ($request, $layout) { @@ -129,7 +129,7 @@ $utopia->get('/console/settings') }); $utopia->get('/console/webhooks') - ->desc('Platform console project webhooks') + ->groups(['web', 'console']) ->label('permission', 'public') ->label('scope', 'console') ->action(function () use ($layout) { @@ -145,7 +145,7 @@ $utopia->get('/console/webhooks') }); $utopia->get('/console/keys') - ->desc('Platform console project keys') + ->groups(['web', 'console']) ->label('permission', 'public') ->label('scope', 'console') ->action(function () use ($layout) { @@ -160,7 +160,7 @@ $utopia->get('/console/keys') }); $utopia->get('/console/tasks') - ->desc('Platform console project tasks') + ->groups(['web', 'console']) ->label('permission', 'public') ->label('scope', 'console') ->action(function () use ($layout) { @@ -172,7 +172,7 @@ $utopia->get('/console/tasks') }); $utopia->get('/console/database') - ->desc('Platform console project settings') + ->groups(['web', 'console']) ->label('permission', 'public') ->label('scope', 'console') ->action(function () use ($layout) { @@ -184,7 +184,7 @@ $utopia->get('/console/database') }); $utopia->get('/console/database/collection') - ->desc('Platform console project database collection') + ->groups(['web', 'console']) ->label('permission', 'public') ->label('scope', 'console') ->param('id', '', function () { return new UID(); }, 'Collection unique ID.') @@ -213,11 +213,10 @@ $utopia->get('/console/database/collection') ->addHeader('Expires', 0) ->addHeader('Pragma', 'no-cache') ; - }); $utopia->get('/console/database/document') - ->desc('Platform console project database document') + ->groups(['web', 'console']) ->label('permission', 'public') ->label('scope', 'console') ->param('collection', '', function () { return new UID(); }, 'Collection unique ID.') @@ -247,7 +246,7 @@ $utopia->get('/console/database/document') }); $utopia->get('/console/storage') - ->desc('Platform console project settings') + ->groups(['web', 'console']) ->label('permission', 'public') ->label('scope', 'console') ->action(function () use ($request, $layout) { @@ -265,7 +264,7 @@ $utopia->get('/console/storage') }); $utopia->get('/console/users') - ->desc('Platform console project settings') + ->groups(['web', 'console']) ->label('permission', 'public') ->label('scope', 'console') ->action(function () use ($layout) { @@ -279,7 +278,7 @@ $utopia->get('/console/users') }); $utopia->get('/console/users/user') - ->desc('Platform console project user') + ->groups(['web', 'console']) ->label('permission', 'public') ->label('scope', 'console') ->action(function () use ($layout) { @@ -291,7 +290,7 @@ $utopia->get('/console/users/user') }); $utopia->get('/console/users/teams/team') - ->desc('Platform console project team') + ->groups(['web', 'console']) ->label('permission', 'public') ->label('scope', 'console') ->action(function () use ($layout) { diff --git a/app/controllers/web/home.php b/app/controllers/web/home.php index 093faf3eb7..65097d36c3 100644 --- a/app/controllers/web/home.php +++ b/app/controllers/web/home.php @@ -1,7 +1,5 @@ init(function () use ($layout) { + $header = new View(__DIR__.'/../../views/home/comps/header.phtml'); + $footer = new View(__DIR__.'/../../views/home/comps/footer.phtml'); -$footer - ->setParam('version', Config::getParam('version')) -; + $footer + ->setParam('version', Config::getParam('version')) + ; -$layout - ->setParam('title', APP_NAME) - ->setParam('description', '') - ->setParam('class', 'home') - ->setParam('platforms', Config::getParam('platforms')) - ->setParam('header', [$header]) - ->setParam('footer', [$footer]) -; + $layout + ->setParam('title', APP_NAME) + ->setParam('description', '') + ->setParam('class', 'home') + ->setParam('platforms', Config::getParam('platforms')) + ->setParam('header', [$header]) + ->setParam('footer', [$footer]) + ; +}, 'home'); $utopia->shutdown(function () use ($response, $layout) { $response->send($layout->render()); -}); +}, 'home'); $utopia->get('/') + ->groups(['web', 'home']) ->label('permission', 'public') ->label('scope', 'home') ->action( @@ -39,7 +40,7 @@ $utopia->get('/') ); $utopia->get('/auth/signin') - ->desc('Login page') + ->groups(['web', 'home']) ->label('permission', 'public') ->label('scope', 'home') ->action(function () use ($layout) { @@ -51,7 +52,7 @@ $utopia->get('/auth/signin') }); $utopia->get('/auth/signup') - ->desc('Registration page') + ->groups(['web', 'home']) ->label('permission', 'public') ->label('scope', 'home') ->action(function () use ($layout) { @@ -63,7 +64,7 @@ $utopia->get('/auth/signup') }); $utopia->get('/auth/recovery') - ->desc('Password recovery page') + ->groups(['web', 'home']) ->label('permission', 'public') ->label('scope', 'home') ->action(function () use ($request, $layout) { @@ -75,7 +76,7 @@ $utopia->get('/auth/recovery') }); $utopia->get('/auth/confirm') - ->desc('Account confirmation page') + ->groups(['web', 'home']) ->label('permission', 'public') ->label('scope', 'home') ->action(function () use ($layout) { @@ -87,7 +88,7 @@ $utopia->get('/auth/confirm') }); $utopia->get('/auth/join') - ->desc('Account team join page') + ->groups(['web', 'home']) ->label('permission', 'public') ->label('scope', 'home') ->action(function () use ($layout) { @@ -99,7 +100,7 @@ $utopia->get('/auth/join') }); $utopia->get('/auth/recovery/reset') - ->desc('Password recovery page') + ->groups(['web', 'home']) ->label('permission', 'public') ->label('scope', 'home') ->action(function () use ($layout) { @@ -112,7 +113,7 @@ $utopia->get('/auth/recovery/reset') $utopia->get('/auth/oauth2/success') - ->desc('Registration page') + ->groups(['web', 'home']) ->label('permission', 'public') ->label('scope', 'home') ->action(function () use ($layout) { @@ -127,7 +128,7 @@ $utopia->get('/auth/oauth2/success') }); $utopia->get('/auth/oauth2/failure') - ->desc('Registration page') + ->groups(['web', 'home']) ->label('permission', 'public') ->label('scope', 'home') ->action(function () use ($layout) { @@ -142,7 +143,7 @@ $utopia->get('/auth/oauth2/failure') }); $utopia->get('/error/:code') - ->desc('Error page') + ->groups(['web', 'home']) ->label('permission', 'public') ->label('scope', 'home') ->param('code', null, new \Utopia\Validator\Numeric(), 'Valid status code number', false) @@ -159,35 +160,38 @@ $utopia->get('/error/:code') }); $utopia->get('/open-api-2.json') + ->groups(['web', 'home']) ->label('scope', 'public') ->label('docs', false) ->param('platform', APP_PLATFORM_CLIENT, function () {return new WhiteList([APP_PLATFORM_CLIENT, APP_PLATFORM_SERVER, APP_PLATFORM_CONSOLE]);}, 'Choose target platform.', true) ->param('extensions', 0, function () {return new Range(0, 1);}, 'Show extra data.', true) ->param('tests', 0, function () {return new Range(0, 1);}, 'Include only test services.', true) ->action( - function ($platform, $extensions, $tests) use ($response, $request, $utopia, $services) { + function ($platform, $extensions, $tests) use ($response, $request, $utopia) { + $services = Config::getParam('services', []); + function fromCamelCase($input) { - preg_match_all('!([A-Z][A-Z0-9]*(?=$|[A-Z][a-z0-9])|[A-Za-z][a-z0-9]+)!', $input, $matches); + \preg_match_all('!([A-Z][A-Z0-9]*(?=$|[A-Z][a-z0-9])|[A-Za-z][a-z0-9]+)!', $input, $matches); $ret = $matches[0]; foreach ($ret as &$match) { - $match = $match == strtoupper($match) ? strtolower($match) : lcfirst($match); + $match = $match == \strtoupper($match) ? \strtolower($match) : \lcfirst($match); } - return implode('_', $ret); + return \implode('_', $ret); } function fromCamelCaseToDash($input) { - return str_replace([' ', '_'], '-', strtolower(preg_replace('/([a-zA-Z])(?=[A-Z])/', '$1-', $input))); + return \str_replace([' ', '_'], '-', \strtolower(\preg_replace('/([a-zA-Z])(?=[A-Z])/', '$1-', $input))); } foreach ($services as $service) { /* @noinspection PhpIncludeInspection */ - if($tests && !isset($service['tests'])) { + if ($tests && !isset($service['tests'])) { continue; } - if($tests && !$service['tests']) { + if ($tests && !$service['tests']) { continue; } @@ -196,7 +200,7 @@ $utopia->get('/open-api-2.json') } /** @noinspection PhpIncludeInspection */ - include_once realpath(__DIR__.'/../../'.$service['controller']); + include_once \realpath(__DIR__.'/../../'.$service['controller']); } $security = [ @@ -295,7 +299,7 @@ $utopia->get('/open-api-2.json') 'url' => 'https://raw.githubusercontent.com/appwrite/appwrite/master/LICENSE', ], ], - 'host' => parse_url($request->getServer('_APP_HOME', Config::getParam('domain')), PHP_URL_HOST), + 'host' => \parse_url($request->getServer('_APP_HOME', Config::getParam('domain')), PHP_URL_HOST), 'basePath' => '/v1', 'schemes' => ['https'], 'consumes' => ['application/json', 'multipart/form-data'], @@ -347,19 +351,19 @@ $utopia->get('/open-api-2.json') ]; if ($extensions) { - if(isset($output['securityDefinitions']['Project'])) { + if (isset($output['securityDefinitions']['Project'])) { $output['securityDefinitions']['Project']['extensions'] = ['demo' => '5df5acd0d48c2']; } - if(isset($output['securityDefinitions']['Key'])) { + if (isset($output['securityDefinitions']['Key'])) { $output['securityDefinitions']['Key']['extensions'] = ['demo' => '919c2d18fb5d4...a2ae413da83346ad2']; } - if(isset($output['securityDefinitions']['Locale'])) { + if (isset($output['securityDefinitions']['Locale'])) { $output['securityDefinitions']['Locale']['extensions'] = ['demo' => 'en']; } - if(isset($output['securityDefinitions']['Mode'])) { + if (isset($output['securityDefinitions']['Mode'])) { $output['securityDefinitions']['Mode']['extensions'] = ['demo' => '']; } } @@ -374,11 +378,11 @@ $utopia->get('/open-api-2.json') continue; } - if($platform !== APP_PLATFORM_CONSOLE && !in_array($platforms[$platform], $route->getLabel('sdk.platform', []))) { + if ($platform !== APP_PLATFORM_CONSOLE && !\in_array($platforms[$platform], $route->getLabel('sdk.platform', []))) { continue; } - $url = str_replace('/v1', '', $route->getURL()); + $url = \str_replace('/v1', '', $route->getURL()); $scope = $route->getLabel('scope', ''); $hide = $route->getLabel('sdk.hide', false); $consumes = ['application/json']; @@ -387,14 +391,14 @@ $utopia->get('/open-api-2.json') continue; } - $desc = (!empty($route->getLabel('sdk.description', ''))) ? realpath('../'.$route->getLabel('sdk.description', '')) : null; + $desc = (!empty($route->getLabel('sdk.description', ''))) ? \realpath('../'.$route->getLabel('sdk.description', '')) : null; $temp = [ 'summary' => $route->getDesc(), - 'operationId' => $route->getLabel('sdk.method', uniqid()), + 'operationId' => $route->getLabel('sdk.method', \uniqid()), 'consumes' => [], 'tags' => [$route->getLabel('sdk.namespace', 'default')], - 'description' => ($desc) ? file_get_contents($desc) : '', + 'description' => ($desc) ? \file_get_contents($desc) : '', // 'responses' => [ // 200 => [ @@ -440,7 +444,7 @@ $utopia->get('/open-api-2.json') ]; foreach ($route->getParams() as $name => $param) { - $validator = (is_callable($param['validator'])) ? $param['validator']() : $param['validator']; /* @var $validator \Utopia\Validator */ + $validator = (\is_callable($param['validator'])) ? $param['validator']() : $param['validator']; /* @var $validator \Utopia\Validator */ $node = [ 'name' => $name, @@ -448,14 +452,14 @@ $utopia->get('/open-api-2.json') 'required' => !$param['optional'], ]; - switch ((!empty($validator)) ? get_class($validator) : '') { + switch ((!empty($validator)) ? \get_class($validator) : '') { case 'Utopia\Validator\Text': $node['type'] = 'string'; - $node['x-example'] = '['.strtoupper(fromCamelCase($node['name'])).']'; + $node['x-example'] = '['.\strtoupper(fromCamelCase($node['name'])).']'; break; case 'Appwrite\Database\Validator\UID': $node['type'] = 'string'; - $node['x-example'] = '['.strtoupper(fromCamelCase($node['name'])).']'; + $node['x-example'] = '['.\strtoupper(fromCamelCase($node['name'])).']'; break; case 'Utopia\Validator\Email': $node['type'] = 'string'; @@ -517,11 +521,11 @@ $utopia->get('/open-api-2.json') break; } - if ($param['optional'] && !is_null($param['default'])) { // Param has default value + if ($param['optional'] && !\is_null($param['default'])) { // Param has default value $node['default'] = $param['default']; } - if (false !== strpos($url, ':'.$name)) { // Param is in URL path + if (false !== \strpos($url, ':'.$name)) { // Param is in URL path $node['in'] = 'path'; $temp['parameters'][] = $node; } elseif ($key == 'GET') { // Param is in query @@ -537,12 +541,12 @@ $utopia->get('/open-api-2.json') } } - $url = str_replace(':'.$name, '{'.$name.'}', $url); + $url = \str_replace(':'.$name, '{'.$name.'}', $url); } $temp['consumes'] = $consumes; - $output['paths'][$url][strtolower($route->getMethod())] = $temp; + $output['paths'][$url][\strtolower($route->getMethod())] = $temp; } } @@ -550,7 +554,7 @@ $utopia->get('/open-api-2.json') var_dump($mock['name']); }*/ - ksort($output['paths']); + \ksort($output['paths']); $response ->json($output); diff --git a/app/init.php b/app/init.php index 62ea435166..ffa2171472 100644 --- a/app/init.php +++ b/app/init.php @@ -7,7 +7,7 @@ * Set configuration, framework resources, app constants * */ -if (file_exists(__DIR__.'/../vendor/autoload.php')) { +if (\file_exists(__DIR__.'/../vendor/autoload.php')) { require_once __DIR__.'/../vendor/autoload.php'; } @@ -51,6 +51,9 @@ const APP_SOCIAL_DEV = 'https://dev.to/appwrite'; $register = new Registry(); $request = new Request(); $response = new Response(); +$utopia = new App('Asia/Tel_Aviv'); + +$utopia->setMode($utopia->getEnv('_APP_ENV', App::MODE_TYPE_PRODUCTION)); /* * ENV vars @@ -61,30 +64,30 @@ Config::load('platforms', __DIR__.'/../app/config/platforms.php'); Config::load('locales', __DIR__.'/../app/config/locales.php'); Config::load('collections', __DIR__.'/../app/config/collections.php'); Config::load('environments', __DIR__.'/../app/config/environments.php'); +Config::load('roles', __DIR__.'/../app/config/roles.php'); // User roles and scopes +Config::load('services', __DIR__.'/../app/config/services.php'); // List of services -Config::setParam('env', $request->getServer('_APP_ENV', App::ENV_TYPE_PRODUCTION)); +Config::setParam('env', $utopia->getMode()); Config::setParam('domain', $request->getServer('HTTP_HOST', '')); Config::setParam('domainVerification', false); Config::setParam('version', $request->getServer('_APP_VERSION', 'UNKNOWN')); Config::setParam('protocol', $request->getServer('HTTP_X_FORWARDED_PROTO', $request->getServer('REQUEST_SCHEME', 'https'))); -Config::setParam('port', (string) parse_url(Config::getParam('protocol').'://'.$request->getServer('HTTP_HOST', ''), PHP_URL_PORT)); -Config::setParam('hostname', parse_url(Config::getParam('protocol').'://'.$request->getServer('HTTP_HOST', null), PHP_URL_HOST)); - -$utopia = new App('Asia/Tel_Aviv', Config::getParam('env')); +Config::setParam('port', (string) \parse_url(Config::getParam('protocol').'://'.$request->getServer('HTTP_HOST', ''), PHP_URL_PORT)); +Config::setParam('hostname', \parse_url(Config::getParam('protocol').'://'.$request->getServer('HTTP_HOST', null), PHP_URL_HOST)); Resque::setBackend($request->getServer('_APP_REDIS_HOST', '') .':'.$request->getServer('_APP_REDIS_PORT', '')); -define('COOKIE_DOMAIN', +\define('COOKIE_DOMAIN', ( $request->getServer('HTTP_HOST', null) === 'localhost' || $request->getServer('HTTP_HOST', null) === 'localhost:'.Config::getParam('port') || - (filter_var(Config::getParam('hostname'), FILTER_VALIDATE_IP) !== false) + (\filter_var(Config::getParam('hostname'), FILTER_VALIDATE_IP) !== false) ) ? null : '.'.Config::getParam('hostname') ); -define('COOKIE_SAMESITE', Response::COOKIE_SAMESITE_NONE); +\define('COOKIE_SAMESITE', Response::COOKIE_SAMESITE_NONE); /* * Registry @@ -151,8 +154,9 @@ $register->set('smtp', function () use ($request) { $mail->Password = $password; $mail->SMTPSecure = $request->getServer('_APP_SMTP_SECURE', false); $mail->SMTPAutoTLS = false; + $mail->CharSet = 'UTF-8'; - $from = urldecode($request->getServer('_APP_SYSTEM_EMAIL_NAME', APP_NAME.' Server')); + $from = \urldecode($request->getServer('_APP_SYSTEM_EMAIL_NAME', APP_NAME.' Server')); $email = $request->getServer('_APP_SYSTEM_EMAIL_ADDRESS', APP_EMAIL_TEAM); $mail->setFrom($email, $from); @@ -166,7 +170,7 @@ $register->set('smtp', function () use ($request) { /* * Localization */ -$locale = $request->getParam('locale', $request->getHeader('X-Appwrite-Locale', null)); +$locale = $request->getParam('locale', $request->getHeader('X-Appwrite-Locale', '')); Locale::$exceptions = false; @@ -219,14 +223,14 @@ Locale::setLanguage('zh-tw', include __DIR__.'/config/locales/zh-tw.php'); Locale::setDefault('en'); -if (in_array($locale, Config::getParam('locales'))) { +if (\in_array($locale, Config::getParam('locales'))) { Locale::setDefault($locale); } -stream_context_set_default([ // Set global user agent and http settings +\stream_context_set_default([ // Set global user agent and http settings 'http' => [ 'method' => 'GET', - 'user_agent' => sprintf(APP_USERAGENT, + 'user_agent' => \sprintf(APP_USERAGENT, Config::getParam('version'), $request->getServer('_APP_SYSTEM_SECURITY_EMAIL_ADDRESS', APP_EMAIL_SECURITY)), 'timeout' => 2, @@ -280,7 +284,7 @@ $consoleDB->setNamespace('app_console'); // Should be replaced with param if we $consoleDB->setMocks(Config::getParam('collections', [])); Authorization::disable(); -$project = $consoleDB->getDocument($request->getParam('project', $request->getHeader('X-Appwrite-Project', null))); +$project = $consoleDB->getDocument($request->getParam('project', $request->getHeader('X-Appwrite-Project', ''))); Authorization::enable(); @@ -304,8 +308,8 @@ $response->addHeader('X-Debug-Fallback', 'false'); if(empty($session['id']) && empty($session['secret'])) { $response->addHeader('X-Debug-Fallback', 'true'); - $fallback = $request->getHeader('X-Fallback-Cookies', null); - $fallback = json_decode($fallback, true); + $fallback = $request->getHeader('X-Fallback-Cookies', ''); + $fallback = \json_decode($fallback, true); $session = Auth::decodeSession(((isset($fallback[Auth::$cookieName])) ? $fallback[Auth::$cookieName] : '')); } @@ -317,9 +321,10 @@ $projectDB->setAdapter(new RedisAdapter(new MySQLAdapter($register), $register)) $projectDB->setNamespace('app_'.$project->getId()); $projectDB->setMocks(Config::getParam('collections', [])); -$user = $projectDB->getDocument(Auth::$unique); - -if (APP_MODE_ADMIN === $mode) { +if (APP_MODE_ADMIN !== $mode) { + $user = $projectDB->getDocument(Auth::$unique); +} +else { $user = $consoleDB->getDocument(Auth::$unique); $user @@ -346,7 +351,7 @@ $register->get('smtp') ->setFrom( $request->getServer('_APP_SYSTEM_EMAIL_ADDRESS', APP_EMAIL_TEAM), ($project->getId() === 'console') - ? urldecode($request->getServer('_APP_SYSTEM_EMAIL_NAME', APP_NAME.' Server')) - : sprintf(Locale::getText('account.emails.team'), $project->getAttribute('name') + ? \urldecode($request->getServer('_APP_SYSTEM_EMAIL_NAME', APP_NAME.' Server')) + : \sprintf(Locale::getText('account.emails.team'), $project->getAttribute('name') ) ); \ No newline at end of file diff --git a/app/sdks/client-flutter/README.md b/app/sdks/client-flutter/README.md index b0c261c115..9a5c8c5c1a 100644 --- a/app/sdks/client-flutter/README.md +++ b/app/sdks/client-flutter/README.md @@ -2,9 +2,9 @@ [![pub package](https://img.shields.io/pub/v/appwrite.svg)](https://pub.dartlang.org/packages/appwrite) ![License](https://img.shields.io/github/license/appwrite/sdk-for-flutter.svg?v=1) -![Version](https://img.shields.io/badge/api%20version-0.6.1-blue.svg?v=1) +![Version](https://img.shields.io/badge/api%20version-0.6.2-blue.svg?v=1) -**This SDK is compatible with Appwrite server version 0.6.1. For older versions, please check previous releases.** +**This SDK is compatible with Appwrite server version 0.6.2. For older versions, please check previous releases.** Appwrite is an open-source backend as a service server that abstract and simplify complex and repetitive development tasks behind a very simple to use REST API. Appwrite aims to help you develop your apps faster and in a more secure way. Use the Flutter SDK to integrate your app with the Appwrite server to easily start interacting with all of Appwrite backend APIs and tools. diff --git a/app/sdks/client-flutter/docs/examples/account/create-o-auth2session.md b/app/sdks/client-flutter/docs/examples/account/create-o-auth2session.md index c617c34d4d..0e26dd27af 100644 --- a/app/sdks/client-flutter/docs/examples/account/create-o-auth2session.md +++ b/app/sdks/client-flutter/docs/examples/account/create-o-auth2session.md @@ -10,7 +10,7 @@ void main() { // Init SDK ; Future result = account.createOAuth2Session( - provider: 'bitbucket', + provider: 'amazon', ); result diff --git a/app/sdks/client-flutter/docs/examples/avatars/get-initials.md b/app/sdks/client-flutter/docs/examples/avatars/get-initials.md new file mode 100644 index 0000000000..b2e70788d6 --- /dev/null +++ b/app/sdks/client-flutter/docs/examples/avatars/get-initials.md @@ -0,0 +1,16 @@ +import 'package:appwrite/appwrite.dart'; + +void main() { // Init SDK + Client client = Client(); + Avatars avatars = Avatars(client); + + client + .setEndpoint('https://[HOSTNAME_OR_IP]/v1') // Your API Endpoint + .setProject('5df5acd0d48c2') // Your project ID + ; + + String result = avatars.getInitials( + ); + + print(result); // Resource URL string +} \ No newline at end of file diff --git a/app/sdks/client-flutter/docs/examples/locale/get-languages.md b/app/sdks/client-flutter/docs/examples/locale/get-languages.md new file mode 100644 index 0000000000..a4124d3cbd --- /dev/null +++ b/app/sdks/client-flutter/docs/examples/locale/get-languages.md @@ -0,0 +1,20 @@ +import 'package:appwrite/appwrite.dart'; + +void main() { // Init SDK + Client client = Client(); + Locale locale = Locale(client); + + client + .setEndpoint('https://[HOSTNAME_OR_IP]/v1') // Your API Endpoint + .setProject('5df5acd0d48c2') // Your project ID + ; + + Future result = locale.getLanguages( ); + + result + .then((response) { + print(response); + }).catchError((error) { + print(error.response); + }); +} \ No newline at end of file diff --git a/app/sdks/client-flutter/lib/services/account.dart b/app/sdks/client-flutter/lib/services/account.dart index 8cc237d104..c2083da526 100644 --- a/app/sdks/client-flutter/lib/services/account.dart +++ b/app/sdks/client-flutter/lib/services/account.dart @@ -33,10 +33,10 @@ class Account extends Service { /// /// Use this endpoint to allow a new user to register a new account in your /// project. After the user registration completes successfully, you can use - /// the [/account/verfication](/docs/account#createVerification) route to start - /// verifying the user email address. To allow your new user to login to his - /// new account, you need to create a new [account - /// session](/docs/account#createSession). + /// the [/account/verfication](/docs/client/account#createVerification) route + /// to start verifying the user email address. To allow your new user to login + /// to his new account, you need to create a new [account + /// session](/docs/client/account#createSession). /// Future create({@required String email, @required String password, String name = ''}) { final String path = '/account'; @@ -195,7 +195,7 @@ class Account extends Service { /// When the user clicks the confirmation link he is redirected back to your /// app password reset URL with the secret key and email address values /// attached to the URL query string. Use the query string params to submit a - /// request to the [PUT /account/recovery](/docs/account#updateRecovery) + /// request to the [PUT /account/recovery](/docs/client/account#updateRecovery) /// endpoint to complete the process. /// Future createRecovery({@required String email, @required String url}) { @@ -218,7 +218,7 @@ class Account extends Service { /// Use this endpoint to complete the user account password reset. Both the /// **userId** and **secret** arguments will be passed as query parameters to /// the redirect URL you have provided when sending your request to the [POST - /// /account/recovery](/docs/account#createRecovery) endpoint. + /// /account/recovery](/docs/client/account#createRecovery) endpoint. /// /// Please note that in order to avoid a [Redirect /// Attack](https://github.com/OWASP/CheatSheetSeries/blob/master/cheatsheets/Unvalidated_Redirects_and_Forwards_Cheat_Sheet.md) @@ -305,21 +305,36 @@ class Account extends Service { /// first. Use the success and failure arguments to provide a redirect URL's /// back to your app when login is completed. /// - Future createOAuth2Session({@required String provider, String success = 'https://appwrite.io/auth/oauth2/success', String failure = 'https://appwrite.io/auth/oauth2/failure'}) { + Future createOAuth2Session({@required String provider, String success = 'https://appwrite.io/auth/oauth2/success', String failure = 'https://appwrite.io/auth/oauth2/failure', List scopes = const []}) { final String path = '/account/sessions/oauth2/{provider}'.replaceAll(RegExp('{provider}'), provider); final Map params = { 'success': success, 'failure': failure, + 'scopes': scopes, 'project': client.config['project'], }; + + final List query = []; + + params.forEach((key, value) { + if (value is List) { + for (var item in value) { + query.add(Uri.encodeComponent(key + '[]') + '=' + Uri.encodeComponent(item)); + } + } + else { + query.add(Uri.encodeComponent(key) + '=' + Uri.encodeComponent(value)); + } + }); + Uri endpoint = Uri.parse(client.endPoint); Uri url = new Uri(scheme: endpoint.scheme, host: endpoint.host, port: endpoint.port, path: endpoint.path + path, - queryParameters:params, + query: query.join('&') ); return FlutterWebAuth.authenticate( @@ -361,7 +376,7 @@ class Account extends Service { /// should redirect the user back for your app and allow you to complete the /// verification process by verifying both the **userId** and **secret** /// parameters. Learn more about how to [complete the verification - /// process](/docs/account#updateAccountVerification). + /// process](/docs/client/account#updateAccountVerification). /// /// Please note that in order to avoid a [Redirect /// Attack](https://github.com/OWASP/CheatSheetSeries/blob/master/cheatsheets/Unvalidated_Redirects_and_Forwards_Cheat_Sheet.md) diff --git a/app/sdks/client-flutter/lib/services/avatars.dart b/app/sdks/client-flutter/lib/services/avatars.dart index fc9c403d90..16c7ce59ff 100644 --- a/app/sdks/client-flutter/lib/services/avatars.dart +++ b/app/sdks/client-flutter/lib/services/avatars.dart @@ -145,6 +145,42 @@ class Avatars extends Service { return location.toString(); } + /// Get User Initials + /// + /// Use this endpoint to show your user initials avatar icon on your website or + /// app. By default, this route will try to print your logged-in user name or + /// email initials. You can also overwrite the user name if you pass the 'name' + /// parameter. If no name is given and no user is logged, an empty avatar will + /// be returned. + /// + /// You can use the color and background params to change the avatar colors. By + /// default, a random theme will be selected. The random theme will persist for + /// the user's initials when reloading the same theme will always return for + /// the same initials. + /// + String getInitials({String name = '', int width = 500, int height = 500, String color = '', String background = ''}) { + final String path = '/avatars/initials'; + + final Map params = { + 'name': name, + 'width': width, + 'height': height, + 'color': color, + 'background': background, + 'project': client.config['project'], + }; + + Uri endpoint = Uri.parse(client.endPoint); + Uri location = new Uri(scheme: endpoint.scheme, + host: endpoint.host, + port: endpoint.port, + path: endpoint.path + path, + queryParameters:params, + ); + + return location.toString(); + } + /// Get QR Code /// /// Converts a given plain text to a QR code image. You can use the query diff --git a/app/sdks/client-flutter/lib/services/database.dart b/app/sdks/client-flutter/lib/services/database.dart index 1163c9dbb5..88082feef7 100644 --- a/app/sdks/client-flutter/lib/services/database.dart +++ b/app/sdks/client-flutter/lib/services/database.dart @@ -41,7 +41,10 @@ class Database extends Service { /// Create Document /// - /// Create a new Document. + /// Create a new Document. Before using this route, you should create a new + /// collection resource using either a [server + /// integration](/docs/server/database?sdk=nodejs#createCollection) API or + /// directly from your database console. /// Future createDocument({@required String collectionId, @required dynamic data, @required List read, @required List write, String parentDocument = '', String parentProperty = '', String parentPropertyType = 'assign'}) { final String path = '/database/collections/{collectionId}/documents'.replaceAll(RegExp('{collectionId}'), collectionId); diff --git a/app/sdks/client-flutter/lib/services/locale.dart b/app/sdks/client-flutter/lib/services/locale.dart index 91ea1567ef..095af4792c 100644 --- a/app/sdks/client-flutter/lib/services/locale.dart +++ b/app/sdks/client-flutter/lib/services/locale.dart @@ -106,9 +106,9 @@ class Locale extends Service { /// List Currencies /// - /// List of all currencies, including currency symol, name, plural, and decimal - /// digits for all major and minor currencies. You can use the locale header to - /// get the data in a supported language. + /// List of all currencies, including currency symbol, name, plural, and + /// decimal digits for all major and minor currencies. You can use the locale + /// header to get the data in a supported language. /// Future getCurrencies() { final String path = '/locale/currencies'; @@ -120,6 +120,24 @@ class Locale extends Service { 'content-type': 'application/json', }; + return client.call(HttpMethod.get, path: path, params: params, headers: headers); + } + + /// List Languages + /// + /// List of all languages classified by ISO 639-1 including 2-letter code, name + /// in English, and name in the respective language. + /// + Future getLanguages() { + final String path = '/locale/languages'; + + final Map params = { + }; + + final Map headers = { + 'content-type': 'application/json', + }; + return client.call(HttpMethod.get, path: path, params: params, headers: headers); } } \ No newline at end of file diff --git a/app/sdks/client-flutter/lib/services/teams.dart b/app/sdks/client-flutter/lib/services/teams.dart index 56e0731687..b9205da455 100644 --- a/app/sdks/client-flutter/lib/services/teams.dart +++ b/app/sdks/client-flutter/lib/services/teams.dart @@ -115,10 +115,14 @@ class Teams extends Service { /// Get team members by the team unique ID. All team members have read access /// for this list of resources. /// - Future getMemberships({@required String teamId}) { + Future getMemberships({@required String teamId, String search = '', int limit = 25, int offset = 0, OrderType orderType = OrderType.asc}) { final String path = '/teams/{teamId}/memberships'.replaceAll(RegExp('{teamId}'), teamId); final Map params = { + 'search': search, + 'limit': limit, + 'offset': offset, + 'orderType': orderType.name(), }; final Map headers = { @@ -136,8 +140,8 @@ class Teams extends Service { /// /// Use the 'URL' parameter to redirect the user from the invitation email back /// to your app. When the user is redirected, use the [Update Team Membership - /// Status](/docs/teams#updateMembershipStatus) endpoint to allow the user to - /// accept the invitation to the team. + /// Status](/docs/client/teams#updateMembershipStatus) endpoint to allow the + /// user to accept the invitation to the team. /// /// Please note that in order to avoid a [Redirect /// Attacks](https://github.com/OWASP/CheatSheetSeries/blob/master/cheatsheets/Unvalidated_Redirects_and_Forwards_Cheat_Sheet.md) diff --git a/app/sdks/client-web/docs/examples/account/create-o-auth2session.md b/app/sdks/client-web/docs/examples/account/create-o-auth2session.md index 0c7430741c..25b02e632e 100644 --- a/app/sdks/client-web/docs/examples/account/create-o-auth2session.md +++ b/app/sdks/client-web/docs/examples/account/create-o-auth2session.md @@ -5,10 +5,6 @@ sdk .setProject('5df5acd0d48c2') // Your project ID ; -let promise = sdk.account.createOAuth2Session('amazon'); +// Go to OAuth provider login page +sdk.account.createOAuth2Session('amazon'); -promise.then(function (response) { - console.log(response); // Success -}, function (error) { - console.log(error); // Failure -}); \ No newline at end of file diff --git a/app/sdks/client-web/docs/examples/avatars/get-browser.md b/app/sdks/client-web/docs/examples/avatars/get-browser.md index 86d0df4a23..0855209f58 100644 --- a/app/sdks/client-web/docs/examples/avatars/get-browser.md +++ b/app/sdks/client-web/docs/examples/avatars/get-browser.md @@ -7,4 +7,4 @@ sdk let result = sdk.avatars.getBrowser('aa'); -console.log(result); // Resource URL +console.log(result); // Resource URL \ No newline at end of file diff --git a/app/sdks/client-web/docs/examples/avatars/get-credit-card.md b/app/sdks/client-web/docs/examples/avatars/get-credit-card.md index 83a1381adf..1aa595bd65 100644 --- a/app/sdks/client-web/docs/examples/avatars/get-credit-card.md +++ b/app/sdks/client-web/docs/examples/avatars/get-credit-card.md @@ -7,4 +7,4 @@ sdk let result = sdk.avatars.getCreditCard('amex'); -console.log(result); // Resource URL +console.log(result); // Resource URL \ No newline at end of file diff --git a/app/sdks/client-web/docs/examples/avatars/get-favicon.md b/app/sdks/client-web/docs/examples/avatars/get-favicon.md index c0ff93307a..a945e50bb2 100644 --- a/app/sdks/client-web/docs/examples/avatars/get-favicon.md +++ b/app/sdks/client-web/docs/examples/avatars/get-favicon.md @@ -7,4 +7,4 @@ sdk let result = sdk.avatars.getFavicon('https://example.com'); -console.log(result); // Resource URL +console.log(result); // Resource URL \ No newline at end of file diff --git a/app/sdks/client-web/docs/examples/avatars/get-flag.md b/app/sdks/client-web/docs/examples/avatars/get-flag.md index 762aeb348e..222b388afc 100644 --- a/app/sdks/client-web/docs/examples/avatars/get-flag.md +++ b/app/sdks/client-web/docs/examples/avatars/get-flag.md @@ -7,4 +7,4 @@ sdk let result = sdk.avatars.getFlag('af'); -console.log(result); // Resource URL +console.log(result); // Resource URL \ No newline at end of file diff --git a/app/sdks/client-web/docs/examples/avatars/get-image.md b/app/sdks/client-web/docs/examples/avatars/get-image.md index 58f0eb8077..3f15981c71 100644 --- a/app/sdks/client-web/docs/examples/avatars/get-image.md +++ b/app/sdks/client-web/docs/examples/avatars/get-image.md @@ -7,4 +7,4 @@ sdk let result = sdk.avatars.getImage('https://example.com'); -console.log(result); // Resource URL +console.log(result); // Resource URL \ No newline at end of file diff --git a/app/sdks/client-web/docs/examples/avatars/get-q-r.md b/app/sdks/client-web/docs/examples/avatars/get-q-r.md index fb17032949..9d9ee3fea2 100644 --- a/app/sdks/client-web/docs/examples/avatars/get-q-r.md +++ b/app/sdks/client-web/docs/examples/avatars/get-q-r.md @@ -7,4 +7,4 @@ sdk let result = sdk.avatars.getQR('[TEXT]'); -console.log(result); // Resource URL +console.log(result); // Resource URL \ No newline at end of file diff --git a/app/sdks/client-web/docs/examples/storage/get-file-download.md b/app/sdks/client-web/docs/examples/storage/get-file-download.md index ae3a643529..1323b8b8df 100644 --- a/app/sdks/client-web/docs/examples/storage/get-file-download.md +++ b/app/sdks/client-web/docs/examples/storage/get-file-download.md @@ -7,4 +7,4 @@ sdk let result = sdk.storage.getFileDownload('[FILE_ID]'); -console.log(result); // Resource URL +console.log(result); // Resource URL \ No newline at end of file diff --git a/app/sdks/client-web/docs/examples/storage/get-file-preview.md b/app/sdks/client-web/docs/examples/storage/get-file-preview.md index 30461e4d9a..5ede9fdb93 100644 --- a/app/sdks/client-web/docs/examples/storage/get-file-preview.md +++ b/app/sdks/client-web/docs/examples/storage/get-file-preview.md @@ -7,4 +7,4 @@ sdk let result = sdk.storage.getFilePreview('[FILE_ID]'); -console.log(result); // Resource URL +console.log(result); // Resource URL \ No newline at end of file diff --git a/app/sdks/client-web/docs/examples/storage/get-file-view.md b/app/sdks/client-web/docs/examples/storage/get-file-view.md index 341dd502de..53877ae352 100644 --- a/app/sdks/client-web/docs/examples/storage/get-file-view.md +++ b/app/sdks/client-web/docs/examples/storage/get-file-view.md @@ -7,4 +7,4 @@ sdk let result = sdk.storage.getFileView('[FILE_ID]'); -console.log(result); // Resource URL +console.log(result); // Resource URL \ No newline at end of file diff --git a/app/sdks/client-web/src/sdk.js b/app/sdks/client-web/src/sdk.js index 35f9ef352f..4bc64c99d2 100644 --- a/app/sdks/client-web/src/sdk.js +++ b/app/sdks/client-web/src/sdk.js @@ -190,28 +190,27 @@ } request.onload = function () { + let data = request.response; + let contentType = this.getResponseHeader('content-type') || ''; + contentType = contentType.substring(0, contentType.indexOf(';')); + + switch (contentType) { + case 'application/json': + data = JSON.parse(data); + break; + } + + let cookieFallback = this.getResponseHeader('X-Fallback-Cookies') || ''; + + if(window.localStorage && cookieFallback) { + window.console.warn('Appwrite is using localStorage for session management. Increase your security by adding a custom domain as your API endpoint.'); + window.localStorage.setItem('cookieFallback', cookieFallback); + } + if (4 === request.readyState && 399 >= request.status) { - let data = request.response; - let contentType = this.getResponseHeader('content-type') || ''; - contentType = contentType.substring(0, contentType.indexOf(';')); - - switch (contentType) { - case 'application/json': - data = JSON.parse(data); - break; - } - - let cookieFallback = this.getResponseHeader('X-Fallback-Cookies') || ''; - - if(window.localStorage && cookieFallback) { - window.console.warn('Appwrite is using localStorage for session management. Increase your security by adding a custom domain as your API endpoint.'); - window.localStorage.setItem('cookieFallback', cookieFallback); - } - resolve(data); - } else { - reject(new Error(request.statusText)); + reject(data); } }; @@ -709,10 +708,11 @@ * @param {string} provider * @param {string} success * @param {string} failure + * @param {string[]} scopes * @throws {Error} * @return {Promise} */ - createOAuth2Session: function(provider, success = 'https://appwrite.io/auth/oauth2/success', failure = 'https://appwrite.io/auth/oauth2/failure') { + createOAuth2Session: function(provider, success = 'https://appwrite.io/auth/oauth2/success', failure = 'https://appwrite.io/auth/oauth2/failure', scopes = []) { if(provider === undefined) { throw new Error('Missing required parameter: "provider"'); } @@ -729,9 +729,28 @@ payload['failure'] = failure; } + if(scopes) { + payload['scopes'] = scopes; + } + payload['project'] = config.project; - let query = Object.keys(payload).map(key => key + '=' + encodeURIComponent(payload[key])).join('&'); + + let query = []; + + for (let p in payload) { + if(Array.isArray(payload[p])) { + for (let index = 0; index < payload[p].length; index++) { + let param = payload[p][index]; + query.push(encodeURIComponent(p + '[]') + "=" + encodeURIComponent(param)); + } + } + else { + query.push(encodeURIComponent(p) + "=" + encodeURIComponent(payload[p])); + } + } + + query = query.join("&"); window.location = config.endpoint + path + ((query) ? '?' + query : ''); }, @@ -883,7 +902,22 @@ payload['project'] = config.project; - let query = Object.keys(payload).map(key => key + '=' + encodeURIComponent(payload[key])).join('&'); + + let query = []; + + for (let p in payload) { + if(Array.isArray(payload[p])) { + for (let index = 0; index < payload[p].length; index++) { + let param = payload[p][index]; + query.push(encodeURIComponent(p + '[]') + "=" + encodeURIComponent(param)); + } + } + else { + query.push(encodeURIComponent(p) + "=" + encodeURIComponent(payload[p])); + } + } + + query = query.join("&"); return config.endpoint + path + ((query) ? '?' + query : ''); }, @@ -926,7 +960,22 @@ payload['project'] = config.project; - let query = Object.keys(payload).map(key => key + '=' + encodeURIComponent(payload[key])).join('&'); + + let query = []; + + for (let p in payload) { + if(Array.isArray(payload[p])) { + for (let index = 0; index < payload[p].length; index++) { + let param = payload[p][index]; + query.push(encodeURIComponent(p + '[]') + "=" + encodeURIComponent(param)); + } + } + else { + query.push(encodeURIComponent(p) + "=" + encodeURIComponent(payload[p])); + } + } + + query = query.join("&"); return config.endpoint + path + ((query) ? '?' + query : ''); }, @@ -956,7 +1005,22 @@ payload['project'] = config.project; - let query = Object.keys(payload).map(key => key + '=' + encodeURIComponent(payload[key])).join('&'); + + let query = []; + + for (let p in payload) { + if(Array.isArray(payload[p])) { + for (let index = 0; index < payload[p].length; index++) { + let param = payload[p][index]; + query.push(encodeURIComponent(p + '[]') + "=" + encodeURIComponent(param)); + } + } + else { + query.push(encodeURIComponent(p) + "=" + encodeURIComponent(payload[p])); + } + } + + query = query.join("&"); return config.endpoint + path + ((query) ? '?' + query : ''); }, @@ -998,7 +1062,22 @@ payload['project'] = config.project; - let query = Object.keys(payload).map(key => key + '=' + encodeURIComponent(payload[key])).join('&'); + + let query = []; + + for (let p in payload) { + if(Array.isArray(payload[p])) { + for (let index = 0; index < payload[p].length; index++) { + let param = payload[p][index]; + query.push(encodeURIComponent(p + '[]') + "=" + encodeURIComponent(param)); + } + } + else { + query.push(encodeURIComponent(p) + "=" + encodeURIComponent(payload[p])); + } + } + + query = query.join("&"); return config.endpoint + path + ((query) ? '?' + query : ''); }, @@ -1040,7 +1119,91 @@ payload['project'] = config.project; - let query = Object.keys(payload).map(key => key + '=' + encodeURIComponent(payload[key])).join('&'); + + let query = []; + + for (let p in payload) { + if(Array.isArray(payload[p])) { + for (let index = 0; index < payload[p].length; index++) { + let param = payload[p][index]; + query.push(encodeURIComponent(p + '[]') + "=" + encodeURIComponent(param)); + } + } + else { + query.push(encodeURIComponent(p) + "=" + encodeURIComponent(payload[p])); + } + } + + query = query.join("&"); + + return config.endpoint + path + ((query) ? '?' + query : ''); + }, + + /** + * Get User Initials + * + * Use this endpoint to show your user initials avatar icon on your website or + * app. By default, this route will try to print your logged-in user name or + * email initials. You can also overwrite the user name if you pass the 'name' + * parameter. If no name is given and no user is logged, an empty avatar will + * be returned. + * + * You can use the color and background params to change the avatar colors. By + * default, a random theme will be selected. The random theme will persist for + * the user's initials when reloading the same theme will always return for + * the same initials. + * + * @param {string} name + * @param {number} width + * @param {number} height + * @param {string} color + * @param {string} background + * @throws {Error} + * @return {string} + */ + getInitials: function(name = '', width = 500, height = 500, color = '', background = '') { + let path = '/avatars/initials'; + + let payload = {}; + + if(name) { + payload['name'] = name; + } + + if(width) { + payload['width'] = width; + } + + if(height) { + payload['height'] = height; + } + + if(color) { + payload['color'] = color; + } + + if(background) { + payload['background'] = background; + } + + payload['project'] = config.project; + + + let query = []; + + for (let p in payload) { + if(Array.isArray(payload[p])) { + for (let index = 0; index < payload[p].length; index++) { + let param = payload[p][index]; + query.push(encodeURIComponent(p + '[]') + "=" + encodeURIComponent(param)); + } + } + else { + query.push(encodeURIComponent(p) + "=" + encodeURIComponent(payload[p])); + } + } + + query = query.join("&"); return config.endpoint + path + ((query) ? '?' + query : ''); }, @@ -1139,7 +1302,22 @@ payload['project'] = config.project; - let query = Object.keys(payload).map(key => key + '=' + encodeURIComponent(payload[key])).join('&'); + + let query = []; + + for (let p in payload) { + if(Array.isArray(payload[p])) { + for (let index = 0; index < payload[p].length; index++) { + let param = payload[p][index]; + query.push(encodeURIComponent(p + '[]') + "=" + encodeURIComponent(param)); + } + } + else { + query.push(encodeURIComponent(p) + "=" + encodeURIComponent(payload[p])); + } + } + + query = query.join("&"); return config.endpoint + path + ((query) ? '?' + query : ''); } @@ -1759,7 +1937,22 @@ payload['project'] = config.project; - let query = Object.keys(payload).map(key => key + '=' + encodeURIComponent(payload[key])).join('&'); + + let query = []; + + for (let p in payload) { + if(Array.isArray(payload[p])) { + for (let index = 0; index < payload[p].length; index++) { + let param = payload[p][index]; + query.push(encodeURIComponent(p + '[]') + "=" + encodeURIComponent(param)); + } + } + else { + query.push(encodeURIComponent(p) + "=" + encodeURIComponent(payload[p])); + } + } + + query = query.join("&"); return config.endpoint + path + ((query) ? '?' + query : ''); }, @@ -1812,7 +2005,22 @@ payload['project'] = config.project; - let query = Object.keys(payload).map(key => key + '=' + encodeURIComponent(payload[key])).join('&'); + + let query = []; + + for (let p in payload) { + if(Array.isArray(payload[p])) { + for (let index = 0; index < payload[p].length; index++) { + let param = payload[p][index]; + query.push(encodeURIComponent(p + '[]') + "=" + encodeURIComponent(param)); + } + } + else { + query.push(encodeURIComponent(p) + "=" + encodeURIComponent(payload[p])); + } + } + + query = query.join("&"); return config.endpoint + path + ((query) ? '?' + query : ''); }, @@ -1843,7 +2051,22 @@ payload['project'] = config.project; - let query = Object.keys(payload).map(key => key + '=' + encodeURIComponent(payload[key])).join('&'); + + let query = []; + + for (let p in payload) { + if(Array.isArray(payload[p])) { + for (let index = 0; index < payload[p].length; index++) { + let param = payload[p][index]; + query.push(encodeURIComponent(p + '[]') + "=" + encodeURIComponent(param)); + } + } + else { + query.push(encodeURIComponent(p) + "=" + encodeURIComponent(payload[p])); + } + } + + query = query.join("&"); return config.endpoint + path + ((query) ? '?' + query : ''); } diff --git a/app/sdks/client-web/src/sdk.min.js b/app/sdks/client-web/src/sdk.min.js index fa08385239..a19dd9b755 100644 --- a/app/sdks/client-web/src/sdk.min.js +++ b/app/sdks/client-web/src/sdk.min.js @@ -9,9 +9,9 @@ if(method==='GET'){for(let param in params){if(param.hasOwnProperty(key)){path=a switch(headers['content-type']){case 'application/json':params=JSON.stringify(params);break;case 'multipart/form-data':let formData=new FormData();Object.keys(params).forEach(function(key){let param=params[key];formData.append(key+(Array.isArray(param)?'[]':''),param)});params=formData;break} return new Promise(function(resolve,reject){let request=new XMLHttpRequest(),key;request.withCredentials=!0;request.open(method,path,!0);for(key in headers){if(headers.hasOwnProperty(key)){if(key==='content-type'&&headers[key]==='multipart/form-data'){continue} request.setRequestHeader(key,headers[key])}} -request.onload=function(){if(4===request.readyState&&399>=request.status){let data=request.response;let contentType=this.getResponseHeader('content-type')||'';contentType=contentType.substring(0,contentType.indexOf(';'));switch(contentType){case 'application/json':data=JSON.parse(data);break} +request.onload=function(){let data=request.response;let contentType=this.getResponseHeader('content-type')||'';contentType=contentType.substring(0,contentType.indexOf(';'));switch(contentType){case 'application/json':data=JSON.parse(data);break} let cookieFallback=this.getResponseHeader('X-Fallback-Cookies')||'';if(window.localStorage&&cookieFallback){window.console.warn('Appwrite is using localStorage for session management. Increase your security by adding a custom domain as your API endpoint.');window.localStorage.setItem('cookieFallback',cookieFallback)} -resolve(data)}else{reject(new Error(request.statusText))}};if(progress){request.addEventListener('progress',progress);request.upload.addEventListener('progress',progress,!1)} +if(4===request.readyState&&399>=request.status){resolve(data)}else{reject(data)}};if(progress){request.addEventListener('progress',progress);request.upload.addEventListener('progress',progress,!1)} request.onerror=function(){reject(new Error("Network Error"))};request.send(params)})};return{'get':function(path,headers={},params={}){return call('GET',path+((Object.keys(params).length>0)?'?'+buildQuery(params):''),headers,{})},'post':function(path,headers={},params={},progress=null){return call('POST',path,headers,params,progress)},'put':function(path,headers={},params={},progress=null){return call('PUT',path,headers,params,progress)},'patch':function(path,headers={},params={},progress=null){return call('PATCH',path,headers,params,progress)},'delete':function(path,headers={},params={},progress=null){return call('DELETE',path,headers,params,progress)},'addGlobalParam':addGlobalParam,'addGlobalHeader':addGlobalHeader}}(window.document);let account={get:function(){let path='/account';let payload={};return http.get(path,{'content-type':'application/json',},payload)},create:function(email,password,name=''){if(email===undefined){throw new Error('Missing required parameter: "email"')} if(password===undefined){throw new Error('Missing required parameter: "password"')} let path='/account';let payload={};if(email){payload.email=email} @@ -45,10 +45,12 @@ return http.put(path,{'content-type':'application/json',},payload)},getSessions: if(password===undefined){throw new Error('Missing required parameter: "password"')} let path='/account/sessions';let payload={};if(email){payload.email=email} if(password){payload.password=password} -return http.post(path,{'content-type':'application/json',},payload)},deleteSessions:function(){let path='/account/sessions';let payload={};return http.delete(path,{'content-type':'application/json',},payload)},createOAuth2Session:function(provider,success='https://appwrite.io/auth/oauth2/success',failure='https://appwrite.io/auth/oauth2/failure'){if(provider===undefined){throw new Error('Missing required parameter: "provider"')} +return http.post(path,{'content-type':'application/json',},payload)},deleteSessions:function(){let path='/account/sessions';let payload={};return http.delete(path,{'content-type':'application/json',},payload)},createOAuth2Session:function(provider,success='https://appwrite.io/auth/oauth2/success',failure='https://appwrite.io/auth/oauth2/failure',scopes=[]){if(provider===undefined){throw new Error('Missing required parameter: "provider"')} let path='/account/sessions/oauth2/{provider}'.replace(new RegExp('{provider}','g'),provider);let payload={};if(success){payload.success=success} if(failure){payload.failure=failure} -payload.project=config.project;let query=Object.keys(payload).map(key=>key+'='+encodeURIComponent(payload[key])).join('&');window.location=config.endpoint+path+((query)?'?'+query:'')},deleteSession:function(sessionId){if(sessionId===undefined){throw new Error('Missing required parameter: "sessionId"')} +if(scopes){payload.scopes=scopes} +payload.project=config.project;let query=[];for(let p in payload){if(Array.isArray(payload[p])){for(let index=0;indexkey+'='+encodeURIComponent(payload[key])).join('&');return config.endpoint+path+((query)?'?'+query:'')},getCreditCard:function(code,width=100,height=100,quality=100){if(code===undefined){throw new Error('Missing required parameter: "code"')} +payload.project=config.project;let query=[];for(let p in payload){if(Array.isArray(payload[p])){for(let index=0;indexkey+'='+encodeURIComponent(payload[key])).join('&');return config.endpoint+path+((query)?'?'+query:'')},getFavicon:function(url){if(url===undefined){throw new Error('Missing required parameter: "url"')} +payload.project=config.project;let query=[];for(let p in payload){if(Array.isArray(payload[p])){for(let index=0;indexkey+'='+encodeURIComponent(payload[key])).join('&');return config.endpoint+path+((query)?'?'+query:'')},getFlag:function(code,width=100,height=100,quality=100){if(code===undefined){throw new Error('Missing required parameter: "code"')} +payload.project=config.project;let query=[];for(let p in payload){if(Array.isArray(payload[p])){for(let index=0;indexkey+'='+encodeURIComponent(payload[key])).join('&');return config.endpoint+path+((query)?'?'+query:'')},getImage:function(url,width=400,height=400){if(url===undefined){throw new Error('Missing required parameter: "url"')} +payload.project=config.project;let query=[];for(let p in payload){if(Array.isArray(payload[p])){for(let index=0;indexkey+'='+encodeURIComponent(payload[key])).join('&');return config.endpoint+path+((query)?'?'+query:'')}};let database={listDocuments:function(collectionId,filters=[],offset=0,limit=50,orderField='$id',orderType='ASC',orderCast='string',search='',first=0,last=0){if(collectionId===undefined){throw new Error('Missing required parameter: "collectionId"')} +payload.project=config.project;let query=[];for(let p in payload){if(Array.isArray(payload[p])){for(let index=0;indexkey+'='+encodeURIComponent(payload[key])).join('&');return config.endpoint+path+((query)?'?'+query:'')},getFilePreview:function(fileId,width=0,height=0,quality=100,background='',output=''){if(fileId===undefined){throw new Error('Missing required parameter: "fileId"')} +let path='/storage/files/{fileId}/download'.replace(new RegExp('{fileId}','g'),fileId);let payload={};payload.project=config.project;let query=[];for(let p in payload){if(Array.isArray(payload[p])){for(let index=0;indexkey+'='+encodeURIComponent(payload[key])).join('&');return config.endpoint+path+((query)?'?'+query:'')},getFileView:function(fileId,as=''){if(fileId===undefined){throw new Error('Missing required parameter: "fileId"')} +payload.project=config.project;let query=[];for(let p in payload){if(Array.isArray(payload[p])){for(let index=0;indexkey+'='+encodeURIComponent(payload[key])).join('&');return config.endpoint+path+((query)?'?'+query:'')}};let teams={list:function(search='',limit=25,offset=0,orderType='ASC'){let path='/teams';let payload={};if(search){payload.search=search} +payload.project=config.project;let query=[];for(let p in payload){if(Array.isArray(payload[p])){for(let index=0;index; + createOAuth2Session(provider: string, success: string, failure: string, scopes: string[]): Promise; /** * Delete Account Session diff --git a/app/sdks/console-web/docs/examples/account/create-o-auth2session.md b/app/sdks/console-web/docs/examples/account/create-o-auth2session.md index 4a3523693f..3a2dcba651 100644 --- a/app/sdks/console-web/docs/examples/account/create-o-auth2session.md +++ b/app/sdks/console-web/docs/examples/account/create-o-auth2session.md @@ -6,10 +6,6 @@ sdk .setKey('919c2d18fb5d4...a2ae413da83346ad2') // Your secret API key ; -let promise = sdk.account.createOAuth2Session('amazon'); +// Go to OAuth provider login page +sdk.account.createOAuth2Session('amazon'); -promise.then(function (response) { - console.log(response); // Success -}, function (error) { - console.log(error); // Failure -}); \ No newline at end of file diff --git a/app/sdks/console-web/docs/examples/avatars/get-browser.md b/app/sdks/console-web/docs/examples/avatars/get-browser.md index cd89107b91..bbbdb9738f 100644 --- a/app/sdks/console-web/docs/examples/avatars/get-browser.md +++ b/app/sdks/console-web/docs/examples/avatars/get-browser.md @@ -8,4 +8,4 @@ sdk let result = sdk.avatars.getBrowser('aa'); -console.log(result); // Resource URL +console.log(result); // Resource URL \ No newline at end of file diff --git a/app/sdks/console-web/docs/examples/avatars/get-credit-card.md b/app/sdks/console-web/docs/examples/avatars/get-credit-card.md index 4906a4127c..e1fab01cf2 100644 --- a/app/sdks/console-web/docs/examples/avatars/get-credit-card.md +++ b/app/sdks/console-web/docs/examples/avatars/get-credit-card.md @@ -8,4 +8,4 @@ sdk let result = sdk.avatars.getCreditCard('amex'); -console.log(result); // Resource URL +console.log(result); // Resource URL \ No newline at end of file diff --git a/app/sdks/console-web/docs/examples/avatars/get-favicon.md b/app/sdks/console-web/docs/examples/avatars/get-favicon.md index ec53212364..397986b11a 100644 --- a/app/sdks/console-web/docs/examples/avatars/get-favicon.md +++ b/app/sdks/console-web/docs/examples/avatars/get-favicon.md @@ -8,4 +8,4 @@ sdk let result = sdk.avatars.getFavicon('https://example.com'); -console.log(result); // Resource URL +console.log(result); // Resource URL \ No newline at end of file diff --git a/app/sdks/console-web/docs/examples/avatars/get-flag.md b/app/sdks/console-web/docs/examples/avatars/get-flag.md index 87122bddb3..6e90b72feb 100644 --- a/app/sdks/console-web/docs/examples/avatars/get-flag.md +++ b/app/sdks/console-web/docs/examples/avatars/get-flag.md @@ -8,4 +8,4 @@ sdk let result = sdk.avatars.getFlag('af'); -console.log(result); // Resource URL +console.log(result); // Resource URL \ No newline at end of file diff --git a/app/sdks/console-web/docs/examples/avatars/get-image.md b/app/sdks/console-web/docs/examples/avatars/get-image.md index d6789db732..afd1a17cb5 100644 --- a/app/sdks/console-web/docs/examples/avatars/get-image.md +++ b/app/sdks/console-web/docs/examples/avatars/get-image.md @@ -8,4 +8,4 @@ sdk let result = sdk.avatars.getImage('https://example.com'); -console.log(result); // Resource URL +console.log(result); // Resource URL \ No newline at end of file diff --git a/app/sdks/console-web/docs/examples/avatars/get-q-r.md b/app/sdks/console-web/docs/examples/avatars/get-q-r.md index 93d3438746..31f490cedd 100644 --- a/app/sdks/console-web/docs/examples/avatars/get-q-r.md +++ b/app/sdks/console-web/docs/examples/avatars/get-q-r.md @@ -8,4 +8,4 @@ sdk let result = sdk.avatars.getQR('[TEXT]'); -console.log(result); // Resource URL +console.log(result); // Resource URL \ No newline at end of file diff --git a/app/sdks/console-web/docs/examples/storage/get-file-download.md b/app/sdks/console-web/docs/examples/storage/get-file-download.md index 5be3f81694..0a4677fe80 100644 --- a/app/sdks/console-web/docs/examples/storage/get-file-download.md +++ b/app/sdks/console-web/docs/examples/storage/get-file-download.md @@ -8,4 +8,4 @@ sdk let result = sdk.storage.getFileDownload('[FILE_ID]'); -console.log(result); // Resource URL +console.log(result); // Resource URL \ No newline at end of file diff --git a/app/sdks/console-web/docs/examples/storage/get-file-preview.md b/app/sdks/console-web/docs/examples/storage/get-file-preview.md index 737ead95c3..a44cb6479c 100644 --- a/app/sdks/console-web/docs/examples/storage/get-file-preview.md +++ b/app/sdks/console-web/docs/examples/storage/get-file-preview.md @@ -8,4 +8,4 @@ sdk let result = sdk.storage.getFilePreview('[FILE_ID]'); -console.log(result); // Resource URL +console.log(result); // Resource URL \ No newline at end of file diff --git a/app/sdks/console-web/docs/examples/storage/get-file-view.md b/app/sdks/console-web/docs/examples/storage/get-file-view.md index c9f73b7d28..adfb6320c3 100644 --- a/app/sdks/console-web/docs/examples/storage/get-file-view.md +++ b/app/sdks/console-web/docs/examples/storage/get-file-view.md @@ -8,4 +8,4 @@ sdk let result = sdk.storage.getFileView('[FILE_ID]'); -console.log(result); // Resource URL +console.log(result); // Resource URL \ No newline at end of file diff --git a/app/sdks/console-web/src/sdk.js b/app/sdks/console-web/src/sdk.js index a8076af4de..376406f959 100644 --- a/app/sdks/console-web/src/sdk.js +++ b/app/sdks/console-web/src/sdk.js @@ -226,28 +226,27 @@ } request.onload = function () { + let data = request.response; + let contentType = this.getResponseHeader('content-type') || ''; + contentType = contentType.substring(0, contentType.indexOf(';')); + + switch (contentType) { + case 'application/json': + data = JSON.parse(data); + break; + } + + let cookieFallback = this.getResponseHeader('X-Fallback-Cookies') || ''; + + if(window.localStorage && cookieFallback) { + window.console.warn('Appwrite is using localStorage for session management. Increase your security by adding a custom domain as your API endpoint.'); + window.localStorage.setItem('cookieFallback', cookieFallback); + } + if (4 === request.readyState && 399 >= request.status) { - let data = request.response; - let contentType = this.getResponseHeader('content-type') || ''; - contentType = contentType.substring(0, contentType.indexOf(';')); - - switch (contentType) { - case 'application/json': - data = JSON.parse(data); - break; - } - - let cookieFallback = this.getResponseHeader('X-Fallback-Cookies') || ''; - - if(window.localStorage && cookieFallback) { - window.console.warn('Appwrite is using localStorage for session management. Increase your security by adding a custom domain as your API endpoint.'); - window.localStorage.setItem('cookieFallback', cookieFallback); - } - resolve(data); - } else { - reject(new Error(request.statusText)); + reject(data); } }; @@ -745,10 +744,11 @@ * @param {string} provider * @param {string} success * @param {string} failure + * @param {string[]} scopes * @throws {Error} * @return {Promise} */ - createOAuth2Session: function(provider, success = 'https://appwrite.io/auth/oauth2/success', failure = 'https://appwrite.io/auth/oauth2/failure') { + createOAuth2Session: function(provider, success = 'https://appwrite.io/auth/oauth2/success', failure = 'https://appwrite.io/auth/oauth2/failure', scopes = []) { if(provider === undefined) { throw new Error('Missing required parameter: "provider"'); } @@ -765,11 +765,30 @@ payload['failure'] = failure; } + if(scopes) { + payload['scopes'] = scopes; + } + payload['project'] = config.project; payload['key'] = config.key; - let query = Object.keys(payload).map(key => key + '=' + encodeURIComponent(payload[key])).join('&'); + + let query = []; + + for (let p in payload) { + if(Array.isArray(payload[p])) { + for (let index = 0; index < payload[p].length; index++) { + let param = payload[p][index]; + query.push(encodeURIComponent(p + '[]') + "=" + encodeURIComponent(param)); + } + } + else { + query.push(encodeURIComponent(p) + "=" + encodeURIComponent(payload[p])); + } + } + + query = query.join("&"); window.location = config.endpoint + path + ((query) ? '?' + query : ''); }, @@ -923,7 +942,22 @@ payload['key'] = config.key; - let query = Object.keys(payload).map(key => key + '=' + encodeURIComponent(payload[key])).join('&'); + + let query = []; + + for (let p in payload) { + if(Array.isArray(payload[p])) { + for (let index = 0; index < payload[p].length; index++) { + let param = payload[p][index]; + query.push(encodeURIComponent(p + '[]') + "=" + encodeURIComponent(param)); + } + } + else { + query.push(encodeURIComponent(p) + "=" + encodeURIComponent(payload[p])); + } + } + + query = query.join("&"); return config.endpoint + path + ((query) ? '?' + query : ''); }, @@ -968,7 +1002,22 @@ payload['key'] = config.key; - let query = Object.keys(payload).map(key => key + '=' + encodeURIComponent(payload[key])).join('&'); + + let query = []; + + for (let p in payload) { + if(Array.isArray(payload[p])) { + for (let index = 0; index < payload[p].length; index++) { + let param = payload[p][index]; + query.push(encodeURIComponent(p + '[]') + "=" + encodeURIComponent(param)); + } + } + else { + query.push(encodeURIComponent(p) + "=" + encodeURIComponent(payload[p])); + } + } + + query = query.join("&"); return config.endpoint + path + ((query) ? '?' + query : ''); }, @@ -1000,7 +1049,22 @@ payload['key'] = config.key; - let query = Object.keys(payload).map(key => key + '=' + encodeURIComponent(payload[key])).join('&'); + + let query = []; + + for (let p in payload) { + if(Array.isArray(payload[p])) { + for (let index = 0; index < payload[p].length; index++) { + let param = payload[p][index]; + query.push(encodeURIComponent(p + '[]') + "=" + encodeURIComponent(param)); + } + } + else { + query.push(encodeURIComponent(p) + "=" + encodeURIComponent(payload[p])); + } + } + + query = query.join("&"); return config.endpoint + path + ((query) ? '?' + query : ''); }, @@ -1044,7 +1108,22 @@ payload['key'] = config.key; - let query = Object.keys(payload).map(key => key + '=' + encodeURIComponent(payload[key])).join('&'); + + let query = []; + + for (let p in payload) { + if(Array.isArray(payload[p])) { + for (let index = 0; index < payload[p].length; index++) { + let param = payload[p][index]; + query.push(encodeURIComponent(p + '[]') + "=" + encodeURIComponent(param)); + } + } + else { + query.push(encodeURIComponent(p) + "=" + encodeURIComponent(payload[p])); + } + } + + query = query.join("&"); return config.endpoint + path + ((query) ? '?' + query : ''); }, @@ -1088,7 +1167,93 @@ payload['key'] = config.key; - let query = Object.keys(payload).map(key => key + '=' + encodeURIComponent(payload[key])).join('&'); + + let query = []; + + for (let p in payload) { + if(Array.isArray(payload[p])) { + for (let index = 0; index < payload[p].length; index++) { + let param = payload[p][index]; + query.push(encodeURIComponent(p + '[]') + "=" + encodeURIComponent(param)); + } + } + else { + query.push(encodeURIComponent(p) + "=" + encodeURIComponent(payload[p])); + } + } + + query = query.join("&"); + + return config.endpoint + path + ((query) ? '?' + query : ''); + }, + + /** + * Get User Initials + * + * Use this endpoint to show your user initials avatar icon on your website or + * app. By default, this route will try to print your logged-in user name or + * email initials. You can also overwrite the user name if you pass the 'name' + * parameter. If no name is given and no user is logged, an empty avatar will + * be returned. + * + * You can use the color and background params to change the avatar colors. By + * default, a random theme will be selected. The random theme will persist for + * the user's initials when reloading the same theme will always return for + * the same initials. + * + * @param {string} name + * @param {number} width + * @param {number} height + * @param {string} color + * @param {string} background + * @throws {Error} + * @return {string} + */ + getInitials: function(name = '', width = 500, height = 500, color = '', background = '') { + let path = '/avatars/initials'; + + let payload = {}; + + if(name) { + payload['name'] = name; + } + + if(width) { + payload['width'] = width; + } + + if(height) { + payload['height'] = height; + } + + if(color) { + payload['color'] = color; + } + + if(background) { + payload['background'] = background; + } + + payload['project'] = config.project; + + payload['key'] = config.key; + + + let query = []; + + for (let p in payload) { + if(Array.isArray(payload[p])) { + for (let index = 0; index < payload[p].length; index++) { + let param = payload[p][index]; + query.push(encodeURIComponent(p + '[]') + "=" + encodeURIComponent(param)); + } + } + else { + query.push(encodeURIComponent(p) + "=" + encodeURIComponent(payload[p])); + } + } + + query = query.join("&"); return config.endpoint + path + ((query) ? '?' + query : ''); }, @@ -1191,7 +1356,22 @@ payload['key'] = config.key; - let query = Object.keys(payload).map(key => key + '=' + encodeURIComponent(payload[key])).join('&'); + + let query = []; + + for (let p in payload) { + if(Array.isArray(payload[p])) { + for (let index = 0; index < payload[p].length; index++) { + let param = payload[p][index]; + query.push(encodeURIComponent(p + '[]') + "=" + encodeURIComponent(param)); + } + } + else { + query.push(encodeURIComponent(p) + "=" + encodeURIComponent(payload[p])); + } + } + + query = query.join("&"); return config.endpoint + path + ((query) ? '?' + query : ''); } @@ -4034,7 +4214,22 @@ payload['key'] = config.key; - let query = Object.keys(payload).map(key => key + '=' + encodeURIComponent(payload[key])).join('&'); + + let query = []; + + for (let p in payload) { + if(Array.isArray(payload[p])) { + for (let index = 0; index < payload[p].length; index++) { + let param = payload[p][index]; + query.push(encodeURIComponent(p + '[]') + "=" + encodeURIComponent(param)); + } + } + else { + query.push(encodeURIComponent(p) + "=" + encodeURIComponent(payload[p])); + } + } + + query = query.join("&"); return config.endpoint + path + ((query) ? '?' + query : ''); }, @@ -4089,7 +4284,22 @@ payload['key'] = config.key; - let query = Object.keys(payload).map(key => key + '=' + encodeURIComponent(payload[key])).join('&'); + + let query = []; + + for (let p in payload) { + if(Array.isArray(payload[p])) { + for (let index = 0; index < payload[p].length; index++) { + let param = payload[p][index]; + query.push(encodeURIComponent(p + '[]') + "=" + encodeURIComponent(param)); + } + } + else { + query.push(encodeURIComponent(p) + "=" + encodeURIComponent(payload[p])); + } + } + + query = query.join("&"); return config.endpoint + path + ((query) ? '?' + query : ''); }, @@ -4122,7 +4332,22 @@ payload['key'] = config.key; - let query = Object.keys(payload).map(key => key + '=' + encodeURIComponent(payload[key])).join('&'); + + let query = []; + + for (let p in payload) { + if(Array.isArray(payload[p])) { + for (let index = 0; index < payload[p].length; index++) { + let param = payload[p][index]; + query.push(encodeURIComponent(p + '[]') + "=" + encodeURIComponent(param)); + } + } + else { + query.push(encodeURIComponent(p) + "=" + encodeURIComponent(payload[p])); + } + } + + query = query.join("&"); return config.endpoint + path + ((query) ? '?' + query : ''); } diff --git a/app/sdks/console-web/src/sdk.min.js b/app/sdks/console-web/src/sdk.min.js index 8cda389241..7a4cd20f7a 100644 --- a/app/sdks/console-web/src/sdk.min.js +++ b/app/sdks/console-web/src/sdk.min.js @@ -9,9 +9,9 @@ if(method==='GET'){for(let param in params){if(param.hasOwnProperty(key)){path=a switch(headers['content-type']){case 'application/json':params=JSON.stringify(params);break;case 'multipart/form-data':let formData=new FormData();Object.keys(params).forEach(function(key){let param=params[key];formData.append(key+(Array.isArray(param)?'[]':''),param)});params=formData;break} return new Promise(function(resolve,reject){let request=new XMLHttpRequest(),key;request.withCredentials=!0;request.open(method,path,!0);for(key in headers){if(headers.hasOwnProperty(key)){if(key==='content-type'&&headers[key]==='multipart/form-data'){continue} request.setRequestHeader(key,headers[key])}} -request.onload=function(){if(4===request.readyState&&399>=request.status){let data=request.response;let contentType=this.getResponseHeader('content-type')||'';contentType=contentType.substring(0,contentType.indexOf(';'));switch(contentType){case 'application/json':data=JSON.parse(data);break} +request.onload=function(){let data=request.response;let contentType=this.getResponseHeader('content-type')||'';contentType=contentType.substring(0,contentType.indexOf(';'));switch(contentType){case 'application/json':data=JSON.parse(data);break} let cookieFallback=this.getResponseHeader('X-Fallback-Cookies')||'';if(window.localStorage&&cookieFallback){window.console.warn('Appwrite is using localStorage for session management. Increase your security by adding a custom domain as your API endpoint.');window.localStorage.setItem('cookieFallback',cookieFallback)} -resolve(data)}else{reject(new Error(request.statusText))}};if(progress){request.addEventListener('progress',progress);request.upload.addEventListener('progress',progress,!1)} +if(4===request.readyState&&399>=request.status){resolve(data)}else{reject(data)}};if(progress){request.addEventListener('progress',progress);request.upload.addEventListener('progress',progress,!1)} request.onerror=function(){reject(new Error("Network Error"))};request.send(params)})};return{'get':function(path,headers={},params={}){return call('GET',path+((Object.keys(params).length>0)?'?'+buildQuery(params):''),headers,{})},'post':function(path,headers={},params={},progress=null){return call('POST',path,headers,params,progress)},'put':function(path,headers={},params={},progress=null){return call('PUT',path,headers,params,progress)},'patch':function(path,headers={},params={},progress=null){return call('PATCH',path,headers,params,progress)},'delete':function(path,headers={},params={},progress=null){return call('DELETE',path,headers,params,progress)},'addGlobalParam':addGlobalParam,'addGlobalHeader':addGlobalHeader}}(window.document);let account={get:function(){let path='/account';let payload={};return http.get(path,{'content-type':'application/json',},payload)},create:function(email,password,name=''){if(email===undefined){throw new Error('Missing required parameter: "email"')} if(password===undefined){throw new Error('Missing required parameter: "password"')} let path='/account';let payload={};if(email){payload.email=email} @@ -45,10 +45,12 @@ return http.put(path,{'content-type':'application/json',},payload)},getSessions: if(password===undefined){throw new Error('Missing required parameter: "password"')} let path='/account/sessions';let payload={};if(email){payload.email=email} if(password){payload.password=password} -return http.post(path,{'content-type':'application/json',},payload)},deleteSessions:function(){let path='/account/sessions';let payload={};return http.delete(path,{'content-type':'application/json',},payload)},createOAuth2Session:function(provider,success='https://appwrite.io/auth/oauth2/success',failure='https://appwrite.io/auth/oauth2/failure'){if(provider===undefined){throw new Error('Missing required parameter: "provider"')} +return http.post(path,{'content-type':'application/json',},payload)},deleteSessions:function(){let path='/account/sessions';let payload={};return http.delete(path,{'content-type':'application/json',},payload)},createOAuth2Session:function(provider,success='https://appwrite.io/auth/oauth2/success',failure='https://appwrite.io/auth/oauth2/failure',scopes=[]){if(provider===undefined){throw new Error('Missing required parameter: "provider"')} let path='/account/sessions/oauth2/{provider}'.replace(new RegExp('{provider}','g'),provider);let payload={};if(success){payload.success=success} if(failure){payload.failure=failure} -payload.project=config.project;payload.key=config.key;let query=Object.keys(payload).map(key=>key+'='+encodeURIComponent(payload[key])).join('&');window.location=config.endpoint+path+((query)?'?'+query:'')},deleteSession:function(sessionId){if(sessionId===undefined){throw new Error('Missing required parameter: "sessionId"')} +if(scopes){payload.scopes=scopes} +payload.project=config.project;payload.key=config.key;let query=[];for(let p in payload){if(Array.isArray(payload[p])){for(let index=0;indexkey+'='+encodeURIComponent(payload[key])).join('&');return config.endpoint+path+((query)?'?'+query:'')},getCreditCard:function(code,width=100,height=100,quality=100){if(code===undefined){throw new Error('Missing required parameter: "code"')} +payload.project=config.project;payload.key=config.key;let query=[];for(let p in payload){if(Array.isArray(payload[p])){for(let index=0;indexkey+'='+encodeURIComponent(payload[key])).join('&');return config.endpoint+path+((query)?'?'+query:'')},getFavicon:function(url){if(url===undefined){throw new Error('Missing required parameter: "url"')} +payload.project=config.project;payload.key=config.key;let query=[];for(let p in payload){if(Array.isArray(payload[p])){for(let index=0;indexkey+'='+encodeURIComponent(payload[key])).join('&');return config.endpoint+path+((query)?'?'+query:'')},getFlag:function(code,width=100,height=100,quality=100){if(code===undefined){throw new Error('Missing required parameter: "code"')} +payload.project=config.project;payload.key=config.key;let query=[];for(let p in payload){if(Array.isArray(payload[p])){for(let index=0;indexkey+'='+encodeURIComponent(payload[key])).join('&');return config.endpoint+path+((query)?'?'+query:'')},getImage:function(url,width=400,height=400){if(url===undefined){throw new Error('Missing required parameter: "url"')} +payload.project=config.project;payload.key=config.key;let query=[];for(let p in payload){if(Array.isArray(payload[p])){for(let index=0;indexkey+'='+encodeURIComponent(payload[key])).join('&');return config.endpoint+path+((query)?'?'+query:'')}};let database={listCollections:function(search='',limit=25,offset=0,orderType='ASC'){let path='/database/collections';let payload={};if(search){payload.search=search} +payload.project=config.project;payload.key=config.key;let query=[];for(let p in payload){if(Array.isArray(payload[p])){for(let index=0;indexkey+'='+encodeURIComponent(payload[key])).join('&');return config.endpoint+path+((query)?'?'+query:'')},getFilePreview:function(fileId,width=0,height=0,quality=100,background='',output=''){if(fileId===undefined){throw new Error('Missing required parameter: "fileId"')} +let path='/storage/files/{fileId}/download'.replace(new RegExp('{fileId}','g'),fileId);let payload={};payload.project=config.project;payload.key=config.key;let query=[];for(let p in payload){if(Array.isArray(payload[p])){for(let index=0;indexkey+'='+encodeURIComponent(payload[key])).join('&');return config.endpoint+path+((query)?'?'+query:'')},getFileView:function(fileId,as=''){if(fileId===undefined){throw new Error('Missing required parameter: "fileId"')} +payload.project=config.project;payload.key=config.key;let query=[];for(let p in payload){if(Array.isArray(payload[p])){for(let index=0;indexkey+'='+encodeURIComponent(payload[key])).join('&');return config.endpoint+path+((query)?'?'+query:'')}};let teams={list:function(search='',limit=25,offset=0,orderType='ASC'){let path='/teams';let payload={};if(search){payload.search=search} +payload.project=config.project;payload.key=config.key;let query=[];for(let p in payload){if(Array.isArray(payload[p])){for(let index=0;index; + createOAuth2Session(provider: string, success: string, failure: string, scopes: string[]): Promise; /** * Delete Account Session diff --git a/app/tasks/init.php b/app/tasks/init.php index 71619381d0..a6e6af101d 100644 --- a/app/tasks/init.php +++ b/app/tasks/init.php @@ -5,8 +5,12 @@ require_once __DIR__.'/../init.php'; global $request; +use Appwrite\ClamAV\Network; +use Appwrite\Storage\Device\Local; +use Appwrite\Storage\Storage; use Utopia\CLI\CLI; use Utopia\CLI\Console; +use Utopia\Domains\Domain; $cli = new CLI(); @@ -18,7 +22,7 @@ $cli Console::log('Issue a TLS certificate for master domain ('.$domain.')'); - ResqueScheduler::enqueueAt(time() + 30, 'v1-certificates', 'CertificatesV1', [ + ResqueScheduler::enqueueAt(\time() + 30, 'v1-certificates', 'CertificatesV1', [ 'document' => [], 'domain' => $domain, 'validateTarget' => false, @@ -26,4 +30,230 @@ $cli ]); }); +$cli + ->task('doctor') + ->desc('Validate server health') + ->action(function () use ($request, $register) { + Console::log(" __ ____ ____ _ _ ____ __ ____ ____ __ __ + / _\ ( _ \( _ \/ )( \( _ \( )(_ _)( __) ( )/ \ +/ \ ) __/ ) __/\ /\ / ) / )( )( ) _) _ )(( O ) +\_/\_/(__) (__) (_/\_)(__\_)(__) (__) (____)(_)(__)\__/ "); + + Console::log("\n".'👩‍⚕️ Running '.APP_NAME.' Doctor for version '.$request->getServer('_APP_VERSION', 'UNKNOWN').' ...'."\n"); + + Console::log('Checking for production best practices...'); + + $domain = new Domain($request->getServer('_APP_DOMAIN')); + + if(!$domain->isKnown() || $domain->isTest()) { + Console::log('🔴 Hostname has a public suffix'); + } + else { + Console::log('🟢 Hostname has a public suffix'); + } + + $domain = new Domain($request->getServer('_APP_DOMAIN_TARGET')); + + if(!$domain->isKnown() || $domain->isTest()) { + Console::log('🔴 CNAME target has a public suffix'); + } + else { + Console::log('🟢 CNAME target has a public suffix'); + } + + if($request->getServer('_APP_OPENSSL_KEY_V1', 'your-secret-key') === 'your-secret-key') { + Console::log('🔴 Using a unique secret key for encryption'); + } + else { + Console::log('🟢 Using a unique secret key for encryption'); + } + + if($request->getServer('_APP_ENV', 'development') === 'development') { + Console::log('🔴 App enviornment is set for production'); + } + else { + Console::log('🟢 App enviornment is set for production'); + } + + if($request->getServer('_APP_OPTIONS_ABUSE', 'disabled') === 'disabled') { + Console::log('🔴 Abuse protection is enabled'); + } + else { + Console::log('🟢 Abuse protection is enabled'); + } + + $authWhitelistEmails = $request->getServer('_APP_CONSOLE_WHITELIST_EMAILS', null); + $authWhitelistIPs = $request->getServer('_APP_CONSOLE_WHITELIST_IPS', null); + $authWhitelistDomains = $request->getServer('_APP_CONSOLE_WHITELIST_DOMAINS', null); + + if(empty($authWhitelistEmails) + && empty($authWhitelistDomains) + && empty($authWhitelistIPs) + ) { + Console::log('🔴 Console access limits are disabled'); + } + else { + Console::log('🟢 Console access limits are enabled'); + } + + if(empty($request->getServer('_APP_OPTIONS_FORCE_HTTPS', null))) { + Console::log('🔴 HTTP force option is disabled'); + } + else { + Console::log('🟢 HTTP force option is enabled'); + } + + \sleep(0.2); + + try { + Console::log("\n".'Checking connectivity...'); + } catch (\Throwable $th) { + //throw $th; + } + + try { + $register->get('db'); /* @var $db PDO */ + Console::success('Database............connected 👍'); + } catch (\Throwable $th) { + Console::error('Database.........disconnected 👎'); + } + + try { + $register->get('cache'); + Console::success('Queue...............connected 👍'); + } catch (\Throwable $th) { + Console::error('Queue............disconnected 👎'); + } + + try { + $register->get('cache'); + Console::success('Cache...............connected 👍'); + } catch (\Throwable $th) { + Console::error('Cache............disconnected 👎'); + } + + if($request->getServer('_APP_STORAGE_ANTIVIRUS') === 'enabled') { // Check if scans are enabled + try { + $antiVirus = new Network('clamav', 3310); + + if((@$antiVirus->ping())) { + Console::success('AntiVirus...........connected 👍'); + } + else { + Console::error('AntiVirus........disconnected 👎'); + } + } catch (\Throwable $th) { + Console::error('AntiVirus........disconnected 👎'); + } + } + + try { + $mail = $register->get('smtp'); /* @var $mail \PHPMailer\PHPMailer\PHPMailer */ + + $mail->addAddress('demo@example.com', 'Example.com'); + $mail->Subject = 'Test SMTP Connection'; + $mail->Body = 'Hello World'; + $mail->AltBody = 'Hello World'; + + $mail->send(); + Console::success('SMTP................connected 👍'); + } catch (\Throwable $th) { + Console::error('SMTP.............disconnected 👎'); + } + + $host = $request->getServer('_APP_STATSD_HOST', 'telegraf'); + $port = $request->getServer('_APP_STATSD_PORT', 8125); + + if($fp = @\fsockopen('udp://'.$host, $port, $errCode, $errStr, 2)){ + Console::success('StatsD..............connected 👍'); + \fclose($fp); + } else { + Console::error('StatsD...........disconnected 👎'); + } + + $host = $request->getServer('_APP_INFLUXDB_HOST', ''); + $port = $request->getServer('_APP_INFLUXDB_PORT', ''); + + if($fp = @\fsockopen($host, $port, $errCode, $errStr, 2)){ + Console::success('InfluxDB............connected 👍'); + \fclose($fp); + } else { + Console::error('InfluxDB.........disconnected 👎'); + } + + \sleep(0.2); + + Console::log(''); + Console::log('Checking volumes...'); + + foreach ([ + 'Uploads' => APP_STORAGE_UPLOADS, + 'Cache' => APP_STORAGE_CACHE, + 'Config' => APP_STORAGE_CONFIG, + 'Certs' => APP_STORAGE_CERTIFICATES + ] as $key => $volume) { + $device = new Local($volume); + + if (\is_readable($device->getRoot())) { + Console::success('🟢 '.$key.' Volume is readable'); + } + else { + Console::error('🔴 '.$key.' Volume is unreadable'); + } + + if (\is_writable($device->getRoot())) { + Console::success('🟢 '.$key.' Volume is writeable'); + } + else { + Console::error('🔴 '.$key.' Volume is unwriteable'); + } + } + + \sleep(0.2); + + Console::log(''); + Console::log('Checking disk space usage...'); + + foreach ([ + 'Uploads' => APP_STORAGE_UPLOADS, + 'Cache' => APP_STORAGE_CACHE, + 'Config' => APP_STORAGE_CONFIG, + 'Certs' => APP_STORAGE_CERTIFICATES + ] as $key => $volume) { + $device = new Local($volume); + + $percentage = (($device->getPartitionTotalSpace() - $device->getPartitionFreeSpace()) + / $device->getPartitionTotalSpace()) * 100; + + $message = $key.' Volume has '.Storage::human($device->getPartitionFreeSpace()) . ' free space ('.\round($percentage, 2).'% used)'; + + if ($percentage < 80) { + Console::success('🟢 ' . $message); + } + else { + Console::error('🔴 ' . $message); + } + } + + + try { + Console::log(''); + $version = \json_decode(@\file_get_contents($request->getServer('_APP_HOME', 'http://localhost').'/v1/health/version'), true); + + if($version && isset($version['version'])) { + if(\version_compare($version['version'], $request->getServer('_APP_VERSION', 'UNKNOWN')) === 0) { + Console::info('You are running the latest version of '.APP_NAME.'! 🥳'); + } + else { + Console::info('A new version ('.$version['version'].') is available! 🥳'."\n"); + } + } + else { + Console::error('Failed to check for a newer version'."\n"); + } + } catch (\Throwable $th) { + Console::error('Failed to check for a newer version'."\n"); + } + }); + $cli->run(); diff --git a/app/tasks/migrate.php b/app/tasks/migrate.php index e05d1bc0ff..fe833e27f6 100644 --- a/app/tasks/migrate.php +++ b/app/tasks/migrate.php @@ -38,7 +38,7 @@ $callbacks = [ 'orderCast' => 'string', ]); - $sum = count($all); + $sum = \count($all); Console::log('Migrating: '.$offset.' / '.$projectDB->getSum()); @@ -97,17 +97,17 @@ function fixDocument(Document $document) { if($document->getAttribute('$collection') === Database::SYSTEM_COLLECTION_PROJECTS){ foreach($providers as $key => $provider) { - if(!empty($document->getAttribute('usersOauth'.ucfirst($key).'Appid'))) { + if(!empty($document->getAttribute('usersOauth'.\ucfirst($key).'Appid'))) { $document - ->setAttribute('usersOauth2'.ucfirst($key).'Appid', $document->getAttribute('usersOauth'.ucfirst($key).'Appid', '')) - ->removeAttribute('usersOauth'.ucfirst($key).'Appid') + ->setAttribute('usersOauth2'.\ucfirst($key).'Appid', $document->getAttribute('usersOauth'.\ucfirst($key).'Appid', '')) + ->removeAttribute('usersOauth'.\ucfirst($key).'Appid') ; } - if(!empty($document->getAttribute('usersOauth'.ucfirst($key).'Secret'))) { + if(!empty($document->getAttribute('usersOauth'.\ucfirst($key).'Secret'))) { $document - ->setAttribute('usersOauth2'.ucfirst($key).'Secret', $document->getAttribute('usersOauth'.ucfirst($key).'Secret', '')) - ->removeAttribute('usersOauth'.ucfirst($key).'Secret') + ->setAttribute('usersOauth2'.\ucfirst($key).'Secret', $document->getAttribute('usersOauth'.\ucfirst($key).'Secret', '')) + ->removeAttribute('usersOauth'.\ucfirst($key).'Secret') ; } } @@ -115,17 +115,17 @@ function fixDocument(Document $document) { if($document->getAttribute('$collection') === Database::SYSTEM_COLLECTION_USERS) { foreach($providers as $key => $provider) { - if(!empty($document->getAttribute('oauth'.ucfirst($key)))) { + if(!empty($document->getAttribute('oauth'.\ucfirst($key)))) { $document - ->setAttribute('oauth2'.ucfirst($key), $document->getAttribute('oauth'.ucfirst($key), '')) - ->removeAttribute('oauth'.ucfirst($key)) + ->setAttribute('oauth2'.\ucfirst($key), $document->getAttribute('oauth'.\ucfirst($key), '')) + ->removeAttribute('oauth'.\ucfirst($key)) ; } - if(!empty($document->getAttribute('oauth'.ucfirst($key).'AccessToken'))) { + if(!empty($document->getAttribute('oauth'.\ucfirst($key).'AccessToken'))) { $document - ->setAttribute('oauth2'.ucfirst($key).'AccessToken', $document->getAttribute('oauth'.ucfirst($key).'AccessToken', '')) - ->removeAttribute('oauth'.ucfirst($key).'AccessToken') + ->setAttribute('oauth2'.\ucfirst($key).'AccessToken', $document->getAttribute('oauth'.\ucfirst($key).'AccessToken', '')) + ->removeAttribute('oauth'.\ucfirst($key).'AccessToken') ; } } @@ -141,7 +141,7 @@ function fixDocument(Document $document) { if($document->getAttribute('$collection') === Database::SYSTEM_COLLECTION_PLATFORMS) { if($document->getAttribute('url', null) !== null) { $document - ->setAttribute('hostname', parse_url($document->getAttribute('url', $document->getAttribute('hostname', '')), PHP_URL_HOST)) + ->setAttribute('hostname', \parse_url($document->getAttribute('url', $document->getAttribute('hostname', '')), PHP_URL_HOST)) ->removeAttribute('url') ; } @@ -159,7 +159,7 @@ function fixDocument(Document $document) { $attr = fixDocument($attr); } - if(is_array($attr)) { + if(\is_array($attr)) { foreach($attr as &$child) { if($child instanceof Document) { $child = fixDocument($child); @@ -207,7 +207,7 @@ $cli ], ]); - $sum = count($projects); + $sum = \count($projects); $offset = $offset + $limit; Console::log('Fetched '.$sum.' projects...'); diff --git a/app/tasks/sdks.php b/app/tasks/sdks.php index 2843443924..0753c4dfef 100644 --- a/app/tasks/sdks.php +++ b/app/tasks/sdks.php @@ -29,20 +29,20 @@ $cli ->action(function () use ($warning, $version) { function getSSLPage($url) { - $ch = curl_init(); - curl_setopt($ch, CURLOPT_HEADER, false); - curl_setopt($ch, CURLOPT_URL, $url); - curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, false); - curl_setopt($ch, CURLOPT_SSL_VERIFYHOST, false); - curl_setopt($ch, CURLOPT_RETURNTRANSFER, true); - $result = curl_exec($ch); - curl_close($ch); + $ch = \curl_init(); + \curl_setopt($ch, CURLOPT_HEADER, false); + \curl_setopt($ch, CURLOPT_URL, $url); + \curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, false); + \curl_setopt($ch, CURLOPT_SSL_VERIFYHOST, false); + \curl_setopt($ch, CURLOPT_RETURNTRANSFER, true); + $result = \curl_exec($ch); + \curl_close($ch); return $result; } $platforms = Config::getParam('platforms'); - $selected = strtolower(Console::confirm('Choose SDK ("*" for all):')); + $selected = \strtolower(Console::confirm('Choose SDK ("*" for all):')); $message = Console::confirm('Please enter your commit message:'); $production = (Console::confirm('Type "Appwrite" to deploy for production') == 'Appwrite'); @@ -63,14 +63,14 @@ $cli $spec = getSSLPage('https://appwrite.io/v1/open-api-2.json?extensions=1&platform='.$language['family']); $spec = getSSLPage('https://localhost/v1/open-api-2.json?extensions=1&platform='.$language['family']); - $result = realpath(__DIR__.'/..').'/sdks/'.$key.'-'.$language['key']; - $target = realpath(__DIR__.'/..').'/sdks/git/'.$language['key'].'/'; - $readme = realpath(__DIR__ . '/../../docs/sdks/'.$language['key'].'/README.md'); - $readme = ($readme) ? file_get_contents($readme) : ''; - $examples = realpath(__DIR__ . '/../../docs/sdks/'.$language['key'].'/EXAMPLES.md'); - $examples = ($examples) ? file_get_contents($examples) : ''; - $changelog = realpath(__DIR__ . '/../../docs/sdks/'.$language['key'].'/CHANGELOG.md'); - $changelog = ($changelog) ? file_get_contents($changelog) : '# Change Log'; + $result = \realpath(__DIR__.'/..').'/sdks/'.$key.'-'.$language['key']; + $target = \realpath(__DIR__.'/..').'/sdks/git/'.$language['key'].'/'; + $readme = \realpath(__DIR__ . '/../../docs/sdks/'.$language['key'].'/README.md'); + $readme = ($readme) ? \file_get_contents($readme) : ''; + $examples = \realpath(__DIR__ . '/../../docs/sdks/'.$language['key'].'/EXAMPLES.md'); + $examples = ($examples) ? \file_get_contents($examples) : ''; + $changelog = \realpath(__DIR__ . '/../../docs/sdks/'.$language['key'].'/CHANGELOG.md'); + $changelog = ($changelog) ? \file_get_contents($changelog) : '# Change Log'; $warning = ($language['beta']) ? '**This SDK is compatible with Appwrite server version ' . $version . '. For older versions, please check previous releases.**' : ''; $license = 'BSD-3-Clause'; $licenseContent = 'Copyright (c) 2019 Appwrite (https://appwrite.io) and individual contributors. @@ -186,7 +186,7 @@ THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND $gitUrl = 'git@github.com:aw-tests/'.$language['gitRepoName'].'.git'; } - exec('rm -rf '.$target.' && \ + \exec('rm -rf '.$target.' && \ mkdir -p '.$target.' && \ cd '.$target.' && \ git init && \ @@ -201,7 +201,7 @@ THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND Console::success("Pushed {$language['name']} SDK to {$gitUrl}"); - exec('rm -rf '.$target); + \exec('rm -rf '.$target); Console::success("Remove temp directory '{$target}' for {$language['name']} SDK"); } diff --git a/app/views/console/comps/footer.phtml b/app/views/console/comps/footer.phtml index c966839392..26493f30cb 100644 --- a/app/views/console/comps/footer.phtml +++ b/app/views/console/comps/footer.phtml @@ -5,21 +5,21 @@ $version = $this->getParam('version', '').'.'.APP_CACHE_BUSTER;