mirror of
https://github.com/appwrite/appwrite.git
synced 2026-05-26 13:51:13 +00:00
Merge branch 'feat-project-templates-api' into feat-project-smtp-endpoints
This commit is contained in:
@@ -692,9 +692,8 @@ jobs:
|
||||
|
||||
- name: Installing latest version
|
||||
run: |
|
||||
rm docker-compose.yml
|
||||
# TODO: Minify docker-compose; remove development tooling
|
||||
rm .env
|
||||
curl https://appwrite.io/install/compose -o docker-compose.yml
|
||||
curl https://appwrite.io/install/env -o .env
|
||||
sed -i 's/_APP_OPTIONS_ABUSE=enabled/_APP_OPTIONS_ABUSE=disabled/g' .env
|
||||
docker compose up -d
|
||||
|
||||
+3
-3
@@ -72,7 +72,7 @@ docker run -it --rm \
|
||||
--volume /var/run/docker.sock:/var/run/docker.sock \
|
||||
--volume "$(pwd)"/appwrite:/usr/src/code/appwrite:rw \
|
||||
--entrypoint="install" \
|
||||
appwrite/appwrite:1.9.1
|
||||
appwrite/appwrite:1.9.0
|
||||
```
|
||||
|
||||
### Windows
|
||||
@@ -84,7 +84,7 @@ docker run -it --rm ^
|
||||
--volume //var/run/docker.sock:/var/run/docker.sock ^
|
||||
--volume "%cd%"/appwrite:/usr/src/code/appwrite:rw ^
|
||||
--entrypoint="install" ^
|
||||
appwrite/appwrite:1.9.1
|
||||
appwrite/appwrite:1.9.0
|
||||
```
|
||||
|
||||
#### PowerShell
|
||||
@@ -94,7 +94,7 @@ docker run -it --rm `
|
||||
--volume /var/run/docker.sock:/var/run/docker.sock `
|
||||
--volume ${pwd}/appwrite:/usr/src/code/appwrite:rw `
|
||||
--entrypoint="install" `
|
||||
appwrite/appwrite:1.9.1
|
||||
appwrite/appwrite:1.9.0
|
||||
```
|
||||
|
||||
运行后,可以在浏览器上访问 http://localhost 找到 Appwrite 控制台。在非 Linux 的本机主机上完成安装后,服务器可能需要几分钟才能启动。
|
||||
|
||||
@@ -75,7 +75,7 @@ docker run -it --rm \
|
||||
--volume /var/run/docker.sock:/var/run/docker.sock \
|
||||
--volume "$(pwd)"/appwrite:/usr/src/code/appwrite:rw \
|
||||
--entrypoint="install" \
|
||||
appwrite/appwrite:1.9.1
|
||||
appwrite/appwrite:1.9.0
|
||||
```
|
||||
|
||||
### Windows
|
||||
@@ -88,7 +88,7 @@ docker run -it --rm ^
|
||||
--volume //var/run/docker.sock:/var/run/docker.sock ^
|
||||
--volume "%cd%"/appwrite:/usr/src/code/appwrite:rw ^
|
||||
--entrypoint="install" ^
|
||||
appwrite/appwrite:1.9.1
|
||||
appwrite/appwrite:1.9.0
|
||||
```
|
||||
|
||||
#### PowerShell
|
||||
@@ -99,7 +99,7 @@ docker run -it --rm `
|
||||
--volume /var/run/docker.sock:/var/run/docker.sock `
|
||||
--volume ${pwd}/appwrite:/usr/src/code/appwrite:rw `
|
||||
--entrypoint="install" `
|
||||
appwrite/appwrite:1.9.1
|
||||
appwrite/appwrite:1.9.0
|
||||
```
|
||||
|
||||
Once the Docker installation is complete, go to http://localhost to access the Appwrite console from your browser. Please note that on non-Linux native hosts, the server might take a few minutes to start after completing the installation.
|
||||
|
||||
@@ -55,6 +55,8 @@ $admins = [
|
||||
'tables.write',
|
||||
'platforms.read',
|
||||
'platforms.write',
|
||||
'templates.read',
|
||||
'templates.write',
|
||||
'projects.write',
|
||||
'keys.read',
|
||||
'keys.write',
|
||||
|
||||
@@ -204,4 +204,12 @@ return [ // List of publicly visible scopes
|
||||
"description" =>
|
||||
"Access to create, update, and delete project\'s platforms",
|
||||
],
|
||||
"templates.read" => [
|
||||
"description" =>
|
||||
"Access to read project\'s templates",
|
||||
],
|
||||
"templates.write" => [
|
||||
"description" =>
|
||||
"Access to create, update, and delete project\'s templates",
|
||||
],
|
||||
];
|
||||
|
||||
@@ -453,7 +453,7 @@ Http::delete('/v1/account')
|
||||
->groups(['api', 'account'])
|
||||
->label('scope', 'account')
|
||||
->label('audits.event', 'user.delete')
|
||||
->label('audits.resource', 'user/{response.$id}')
|
||||
->label('audits.resource', 'user/{user.$id}')
|
||||
->label('sdk', new Method(
|
||||
namespace: 'account',
|
||||
group: 'account',
|
||||
@@ -4665,7 +4665,7 @@ Http::delete('/v1/account/targets/:targetId/push')
|
||||
->groups(['api', 'account'])
|
||||
->label('scope', 'targets.write')
|
||||
->label('audits.event', 'target.delete')
|
||||
->label('audits.resource', 'target/response.$id')
|
||||
->label('audits.resource', 'target/{request.targetId}')
|
||||
->label('event', 'users.[userId].targets.[targetId].delete')
|
||||
->label('sdk', new Method(
|
||||
namespace: 'account',
|
||||
|
||||
@@ -2252,8 +2252,8 @@ Http::delete('/v1/users/:userId/mfa/authenticators/:type')
|
||||
->label('event', 'users.[userId].delete.mfa')
|
||||
->label('scope', 'users.write')
|
||||
->label('audits.event', 'user.update')
|
||||
->label('audits.resource', 'user/{response.$id}')
|
||||
->label('audits.userId', '{response.$id}')
|
||||
->label('audits.resource', 'user/{request.userId}')
|
||||
->label('audits.userId', '{request.userId}')
|
||||
->label('usage.metric', 'users.{scope}.requests.update')
|
||||
->label('sdk', [
|
||||
new Method(
|
||||
|
||||
@@ -26,6 +26,7 @@ use Appwrite\Utopia\Request\Filters\V19 as RequestV19;
|
||||
use Appwrite\Utopia\Request\Filters\V20 as RequestV20;
|
||||
use Appwrite\Utopia\Request\Filters\V21 as RequestV21;
|
||||
use Appwrite\Utopia\Request\Filters\V22 as RequestV22;
|
||||
use Appwrite\Utopia\Request\Filters\V23 as RequestV23;
|
||||
use Appwrite\Utopia\Response;
|
||||
use Appwrite\Utopia\Response\Filters\V16 as ResponseV16;
|
||||
use Appwrite\Utopia\Response\Filters\V17 as ResponseV17;
|
||||
@@ -34,6 +35,7 @@ use Appwrite\Utopia\Response\Filters\V19 as ResponseV19;
|
||||
use Appwrite\Utopia\Response\Filters\V20 as ResponseV20;
|
||||
use Appwrite\Utopia\Response\Filters\V21 as ResponseV21;
|
||||
use Appwrite\Utopia\Response\Filters\V22 as ResponseV22;
|
||||
use Appwrite\Utopia\Response\Filters\V23 as ResponseV23;
|
||||
use Appwrite\Utopia\View;
|
||||
use Executor\Executor;
|
||||
use MaxMind\Db\Reader;
|
||||
@@ -897,6 +899,9 @@ Http::init()
|
||||
if (version_compare($requestFormat, '1.9.1', '<')) {
|
||||
$request->addFilter(new RequestV22());
|
||||
}
|
||||
if (version_compare($requestFormat, '1.9.2', '<')) {
|
||||
$request->addFilter(new RequestV23());
|
||||
}
|
||||
}
|
||||
|
||||
$localeParam = (string) $request->getParam('locale', $request->getHeader('x-appwrite-locale', ''));
|
||||
@@ -921,6 +926,9 @@ Http::init()
|
||||
*/
|
||||
$responseFormat = $request->getHeader('x-appwrite-response-format', System::getEnv('_APP_SYSTEM_RESPONSE_FORMAT', ''));
|
||||
if ($responseFormat) {
|
||||
if (version_compare($responseFormat, '1.9.2', '<')) {
|
||||
$response->addFilter(new ResponseV23());
|
||||
}
|
||||
if (version_compare($responseFormat, '1.9.1', '<')) {
|
||||
$response->addFilter(new ResponseV22());
|
||||
}
|
||||
|
||||
@@ -46,8 +46,8 @@ const APP_PROJECT_ACCESS = 24 * 60 * 60; // 24 hours
|
||||
const APP_RESOURCE_TOKEN_ACCESS = 24 * 60 * 60; // 24 hours
|
||||
const APP_FILE_ACCESS = 24 * 60 * 60; // 24 hours
|
||||
const APP_CACHE_UPDATE = 24 * 60 * 60; // 24 hours
|
||||
const APP_CACHE_BUSTER = 4322;
|
||||
const APP_VERSION_STABLE = '1.9.1';
|
||||
const APP_CACHE_BUSTER = 4323;
|
||||
const APP_VERSION_STABLE = '1.9.2';
|
||||
const APP_DATABASE_ATTRIBUTE_EMAIL = 'email';
|
||||
const APP_DATABASE_ATTRIBUTE_ENUM = 'enum';
|
||||
const APP_DATABASE_ATTRIBUTE_IP = 'ip';
|
||||
|
||||
@@ -94,6 +94,7 @@ abstract class Migration
|
||||
'1.8.1' => 'V23',
|
||||
'1.9.0' => 'V24',
|
||||
'1.9.1' => 'V24',
|
||||
'1.9.2' => 'V24',
|
||||
];
|
||||
|
||||
/**
|
||||
|
||||
@@ -37,8 +37,8 @@ class Delete extends Action
|
||||
->label('event', 'users.[userId].delete.mfa')
|
||||
->label('scope', 'account')
|
||||
->label('audits.event', 'user.update')
|
||||
->label('audits.resource', 'user/{response.$id}')
|
||||
->label('audits.userId', '{response.$id}')
|
||||
->label('audits.resource', 'user/{user.$id}')
|
||||
->label('audits.userId', '{user.$id}')
|
||||
->label('sdk', [
|
||||
new Method(
|
||||
namespace: 'account',
|
||||
|
||||
@@ -36,7 +36,7 @@ class Delete extends Action
|
||||
->label('scope', 'platforms.write')
|
||||
->label('event', 'platforms.[platformId].delete')
|
||||
->label('audits.event', 'project.platform.delete')
|
||||
->label('audits.resource', 'project.platform/{response.$id}')
|
||||
->label('audits.resource', 'project.platform/{request.platformId}')
|
||||
->label('sdk', new Method(
|
||||
namespace: 'project',
|
||||
group: 'platforms',
|
||||
|
||||
@@ -0,0 +1,109 @@
|
||||
<?php
|
||||
|
||||
namespace Appwrite\Platform\Modules\Project\Http\Project\Templates\Email;
|
||||
|
||||
use Appwrite\Event\Event as QueueEvent;
|
||||
use Appwrite\Extend\Exception;
|
||||
use Appwrite\SDK\AuthType;
|
||||
use Appwrite\SDK\Method;
|
||||
use Appwrite\SDK\Response as SDKResponse;
|
||||
use Appwrite\Utopia\Response;
|
||||
use Utopia\Config\Config;
|
||||
use Utopia\Database\Database;
|
||||
use Utopia\Database\Document;
|
||||
use Utopia\Database\Validator\Authorization;
|
||||
use Utopia\Locale\Locale;
|
||||
use Utopia\Platform\Action;
|
||||
use Utopia\Platform\Scope\HTTP;
|
||||
use Utopia\System\System;
|
||||
use Utopia\Validator\WhiteList;
|
||||
|
||||
class Delete extends Action
|
||||
{
|
||||
use HTTP;
|
||||
|
||||
public static function getName()
|
||||
{
|
||||
return 'deleteProjectEmailTemplate';
|
||||
}
|
||||
|
||||
public function __construct()
|
||||
{
|
||||
$this->setHttpMethod(Action::HTTP_REQUEST_METHOD_DELETE)
|
||||
->setHttpPath('/v1/project/templates/email')
|
||||
->httpAlias('/v1/projects/:projectId/templates/email')
|
||||
->httpAlias('/v1/projects/:projectId/templates/email/:templateId/:locale')
|
||||
->desc('Delete project email template')
|
||||
->groups(['api', 'project'])
|
||||
->label('scope', 'templates.write')
|
||||
->label('event', 'templates.[templateType].delete')
|
||||
->label('audits.event', 'project.template.delete')
|
||||
->label('audits.resource', 'project.template/{request.templateId}')
|
||||
->label('sdk', new Method(
|
||||
namespace: 'project',
|
||||
group: 'templates',
|
||||
name: 'deleteEmailTemplate',
|
||||
description: <<<EOT
|
||||
Reset a custom email template to its default value. This endpoint removes any custom content and restores the template to its original state.
|
||||
EOT,
|
||||
auth: [AuthType::ADMIN, AuthType::KEY],
|
||||
responses: [
|
||||
new SDKResponse(
|
||||
code: Response::STATUS_CODE_OK,
|
||||
model: Response::MODEL_EMAIL_TEMPLATE,
|
||||
)
|
||||
]
|
||||
))
|
||||
->param('templateId', '', new WhiteList(Config::getParam('locale-templates')['email'] ?? [], true), 'Custom email template type. Can be one of: '.\implode(', ', Config::getParam('locale-templates')['email'] ?? []))
|
||||
->param('locale', '', fn ($localeCodes) => new WhiteList($localeCodes), 'Custom email template locale. If left empty, the fallback locale (en) will be used.', optional: true, injections: ['localeCodes'])
|
||||
->inject('response')
|
||||
->inject('queueForEvents')
|
||||
->inject('dbForPlatform')
|
||||
->inject('authorization')
|
||||
->inject('project')
|
||||
->inject('locale')
|
||||
->callback($this->action(...));
|
||||
}
|
||||
|
||||
public function action(
|
||||
string $templateId,
|
||||
string $locale,
|
||||
Response $response,
|
||||
QueueEvent $queueForEvents,
|
||||
Database $dbForPlatform,
|
||||
Authorization $authorization,
|
||||
Document $project,
|
||||
Locale $localeObject,
|
||||
) {
|
||||
$locale = $locale ?: System::getEnv('_APP_LOCALE', 'en');
|
||||
|
||||
$templates = $project->getAttribute('templates', []);
|
||||
$template = $templates['email.' . $templateId . '-' . $locale] ?? null;
|
||||
|
||||
if (is_null($template)) {
|
||||
throw new Exception(Exception::PROJECT_TEMPLATE_DEFAULT_DELETION);
|
||||
}
|
||||
|
||||
unset($templates['email.' . $templateId . '-' . $locale]);
|
||||
|
||||
$updates = new Document([
|
||||
'templates' => $templates,
|
||||
]);
|
||||
|
||||
$project = $authorization->skip(fn () => $dbForPlatform->updateDocument('projects', $project->getId(), $updates));
|
||||
|
||||
$queueForEvents->setParam('templateType', $templateId);
|
||||
|
||||
$response->dynamic(new Document([
|
||||
'templateId' => $templateId,
|
||||
'locale' => $locale,
|
||||
'senderName' => $template['senderName'] ?? '',
|
||||
'senderEmail' => $template['senderEmail'] ?? '',
|
||||
'subject' => $template['subject'] ?? '',
|
||||
'replyToEmail' => $template['replyToEmail'] ?? $template['replyTo'] ?? '', // Includes backwards compatibility
|
||||
'replyToName' => $template['replyToName'] ?? '',
|
||||
'message' => $template['message'] ?? '',
|
||||
'custom' => true,
|
||||
]), Response::MODEL_EMAIL_TEMPLATE);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,146 @@
|
||||
<?php
|
||||
|
||||
namespace Appwrite\Platform\Modules\Project\Http\Project\Templates\Email;
|
||||
|
||||
use Appwrite\SDK\AuthType;
|
||||
use Appwrite\SDK\Method;
|
||||
use Appwrite\SDK\Response as SDKResponse;
|
||||
use Appwrite\Template\Template;
|
||||
use Appwrite\Utopia\Response;
|
||||
use Utopia\Config\Config;
|
||||
use Utopia\Database\Document;
|
||||
use Utopia\Locale\Locale;
|
||||
use Utopia\Platform\Action;
|
||||
use Utopia\Platform\Scope\HTTP;
|
||||
use Utopia\System\System;
|
||||
use Utopia\Validator\WhiteList;
|
||||
|
||||
class Get extends Action
|
||||
{
|
||||
use HTTP;
|
||||
|
||||
public static function getName()
|
||||
{
|
||||
return 'getProjectEmailTemplate';
|
||||
}
|
||||
|
||||
public function __construct()
|
||||
{
|
||||
$this->setHttpMethod(Action::HTTP_REQUEST_METHOD_GET)
|
||||
->setHttpPath('/v1/project/templates/email/:templateId')
|
||||
->httpAlias('/v1/projects/:projectId/templates/email/:templateId/:locale')
|
||||
->desc('Get project email template')
|
||||
->groups(['api', 'project'])
|
||||
->label('scope', 'templates.read')
|
||||
->label('sdk', new Method(
|
||||
namespace: 'project',
|
||||
group: 'templates',
|
||||
name: 'getEmailTemplate',
|
||||
description: <<<EOT
|
||||
Get a custom email template for the specified locale and type. This endpoint returns the template content, subject, and other configuration details.
|
||||
EOT,
|
||||
auth: [AuthType::ADMIN, AuthType::KEY],
|
||||
responses: [
|
||||
new SDKResponse(
|
||||
code: Response::STATUS_CODE_OK,
|
||||
model: Response::MODEL_EMAIL_TEMPLATE,
|
||||
)
|
||||
]
|
||||
))
|
||||
->param('templateId', '', new WhiteList(Config::getParam('locale-templates')['email'] ?? [], true), 'Custom email template type. Can be one of: '.\implode(', ', Config::getParam('locale-templates')['email'] ?? []))
|
||||
->param('locale', '', fn ($localeCodes) => new WhiteList($localeCodes), 'Custom email template locale. If left empty, the fallback locale (en) will be used.', optional: true, injections: ['localeCodes'])
|
||||
->inject('response')
|
||||
->inject('project')
|
||||
->inject('locale')
|
||||
->callback($this->action(...));
|
||||
}
|
||||
|
||||
public function action(
|
||||
string $templateId,
|
||||
string $locale,
|
||||
Response $response,
|
||||
Document $project,
|
||||
Locale $localeObject,
|
||||
) {
|
||||
$locale = $locale ?: System::getEnv('_APP_LOCALE', 'en');
|
||||
|
||||
$templates = $project->getAttribute('templates', []);
|
||||
$template = $templates['email.' . $templateId . '-' . $locale] ?? null;
|
||||
|
||||
// Includes backwards compatibility: fall back to legacy `replyTo` key
|
||||
if (!is_null($template)) {
|
||||
$template['replyToEmail'] = $template['replyToEmail'] ?? $template['replyTo'] ?? '';
|
||||
$template['replyToName'] = $template['replyToName'] ?? '';
|
||||
}
|
||||
|
||||
$localeObj = new Locale($locale);
|
||||
$localeObj->setFallback(System::getEnv('_APP_LOCALE', 'en'));
|
||||
|
||||
if (is_null($template)) {
|
||||
/**
|
||||
* different templates, different placeholders.
|
||||
*/
|
||||
$templateConfigs = [
|
||||
'magicSession' => [
|
||||
'file' => 'email-magic-url.tpl',
|
||||
'placeholders' => ['optionButton', 'buttonText', 'optionUrl', 'clientInfo', 'securityPhrase']
|
||||
],
|
||||
'mfaChallenge' => [
|
||||
'file' => 'email-mfa-challenge.tpl',
|
||||
'placeholders' => ['description', 'clientInfo']
|
||||
],
|
||||
'otpSession' => [
|
||||
'file' => 'email-otp.tpl',
|
||||
'placeholders' => ['description', 'clientInfo', 'securityPhrase']
|
||||
],
|
||||
'sessionAlert' => [
|
||||
'file' => 'email-session-alert.tpl',
|
||||
'placeholders' => ['body', 'listDevice', 'listIpAddress', 'listCountry', 'footer']
|
||||
],
|
||||
];
|
||||
|
||||
// fallback to the base template.
|
||||
$config = $templateConfigs[$templateId] ?? [
|
||||
'file' => 'email-inner-base.tpl',
|
||||
'placeholders' => ['buttonText', 'body', 'footer']
|
||||
];
|
||||
|
||||
$templateString = file_get_contents(APP_CE_CONFIG_DIR . '/locale/templates/' . $config['file']);
|
||||
|
||||
// We use `fromString` due to the replace above
|
||||
$message = Template::fromString($templateString);
|
||||
|
||||
// Set type-specific parameters
|
||||
foreach ($config['placeholders'] as $param) {
|
||||
$escapeHtml = !in_array($param, ['clientInfo', 'body', 'footer', 'description']);
|
||||
$message->setParam("{{{$param}}}", $localeObj->getText("emails.{$templateId}.{$param}"), escapeHtml: $escapeHtml);
|
||||
}
|
||||
|
||||
$message
|
||||
// common placeholders on all the templates
|
||||
->setParam('{{hello}}', $localeObj->getText("emails.{$templateId}.hello"))
|
||||
->setParam('{{thanks}}', $localeObj->getText("emails.{$templateId}.thanks"))
|
||||
->setParam('{{signature}}', $localeObj->getText("emails.{$templateId}.signature"));
|
||||
|
||||
// `useContent: false` will strip new lines!
|
||||
$message = $message->render(useContent: true);
|
||||
|
||||
$template = [
|
||||
'message' => $message,
|
||||
'subject' => $localeObj->getText('emails.' . $templateId . '.subject'),
|
||||
'senderEmail' => '',
|
||||
'senderName' => '',
|
||||
'replyToEmail' => '',
|
||||
'replyToName' => '',
|
||||
'custom' => false,
|
||||
];
|
||||
} else {
|
||||
$template['custom'] = true;
|
||||
}
|
||||
|
||||
$template['templateId'] = $templateId;
|
||||
$template['locale'] = $locale;
|
||||
|
||||
$response->dynamic(new Document($template), Response::MODEL_EMAIL_TEMPLATE);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,125 @@
|
||||
<?php
|
||||
|
||||
namespace Appwrite\Platform\Modules\Project\Http\Project\Templates\Email;
|
||||
|
||||
use Appwrite\Event\Event as QueueEvent;
|
||||
use Appwrite\SDK\AuthType;
|
||||
use Appwrite\SDK\Method;
|
||||
use Appwrite\SDK\Response as SDKResponse;
|
||||
use Appwrite\Utopia\Response;
|
||||
use Utopia\Config\Config;
|
||||
use Utopia\Database\Database;
|
||||
use Utopia\Database\Document;
|
||||
use Utopia\Database\Validator\Authorization;
|
||||
use Utopia\Emails\Validator\Email;
|
||||
use Utopia\Locale\Locale;
|
||||
use Utopia\Platform\Action;
|
||||
use Utopia\Platform\Scope\HTTP;
|
||||
use Utopia\System\System;
|
||||
use Utopia\Validator\Text;
|
||||
use Utopia\Validator\WhiteList;
|
||||
|
||||
class Update extends Action
|
||||
{
|
||||
use HTTP;
|
||||
|
||||
public static function getName()
|
||||
{
|
||||
return 'updateProjectEmailTemplate';
|
||||
}
|
||||
|
||||
public function __construct()
|
||||
{
|
||||
$this->setHttpMethod(Action::HTTP_REQUEST_METHOD_PATCH)
|
||||
->setHttpPath('/v1/project/templates/email')
|
||||
->httpAlias('/v1/projects/:projectId/templates/email')
|
||||
->httpAlias('/v1/projects/:projectId/templates/email/:templateId/:locale')
|
||||
->desc('Update project email template')
|
||||
->groups(['api', 'project'])
|
||||
->label('scope', 'templates.write')
|
||||
->label('event', 'templates.[templateType].update')
|
||||
->label('audits.event', 'project.template.update')
|
||||
->label('audits.resource', 'project.template/{response.templateId}')
|
||||
->label('sdk', new Method(
|
||||
namespace: 'project',
|
||||
group: 'templates',
|
||||
name: 'updateEmailTemplate',
|
||||
description: <<<EOT
|
||||
Update a custom email template for the specified locale and type. Use this endpoint to modify the content of your email templates.
|
||||
EOT,
|
||||
auth: [AuthType::ADMIN, AuthType::KEY],
|
||||
responses: [
|
||||
new SDKResponse(
|
||||
code: Response::STATUS_CODE_OK,
|
||||
model: Response::MODEL_EMAIL_TEMPLATE,
|
||||
)
|
||||
]
|
||||
))
|
||||
->param('templateId', '', new WhiteList(Config::getParam('locale-templates')['email'] ?? [], true), 'Custom email template type. Can be one of: '.\implode(', ', Config::getParam('locale-templates')['email'] ?? []))
|
||||
->param('locale', '', fn ($localeCodes) => new WhiteList($localeCodes), 'Custom email template locale. If left empty, the fallback locale (en) will be used.', optional: true, injections: ['localeCodes'])
|
||||
->param('subject', '', new Text(255), 'Subject of the email template. Can be up to 255 characters.')
|
||||
->param('message', '', new Text(10485760), 'Plain or HTML body of the email template message. Can be up to 10MB of content.')
|
||||
->param('senderName', '', new Text(255, 0), 'Name of the email sender.', true)
|
||||
->param('senderEmail', '', new Email(), 'Email of the sender.', true)
|
||||
->param('replyToEmail', '', new Email(), 'Reply to email.', true)
|
||||
->param('replyToName', '', new Text(255, 0), 'Reply to name.', true)
|
||||
->inject('response')
|
||||
->inject('queueForEvents')
|
||||
->inject('dbForPlatform')
|
||||
->inject('authorization')
|
||||
->inject('project')
|
||||
->inject('locale')
|
||||
->callback($this->action(...));
|
||||
}
|
||||
|
||||
public function action(
|
||||
string $templateId,
|
||||
string $locale,
|
||||
string $subject,
|
||||
string $message,
|
||||
string $senderName,
|
||||
string $senderEmail,
|
||||
string $replyToEmail,
|
||||
string $replyToName,
|
||||
Response $response,
|
||||
QueueEvent $queueForEvents,
|
||||
Database $dbForPlatform,
|
||||
Authorization $authorization,
|
||||
Document $project,
|
||||
Locale $localeObject,
|
||||
) {
|
||||
$locale = $locale ?: System::getEnv('_APP_LOCALE', 'en');
|
||||
|
||||
$template = [
|
||||
'senderName' => $senderName,
|
||||
'senderEmail' => $senderEmail,
|
||||
'subject' => $subject,
|
||||
'replyToEmail' => $replyToEmail,
|
||||
'replyToName' => $replyToName,
|
||||
'message' => $message
|
||||
];
|
||||
|
||||
$templates = $project->getAttribute('templates', []);
|
||||
$templates['email.' . $templateId . '-' . $locale] = $template;
|
||||
|
||||
$updates = new Document([
|
||||
'templates' => $templates,
|
||||
]);
|
||||
|
||||
$project = $authorization->skip(fn () => $dbForPlatform->updateDocument('projects', $project->getId(), $updates));
|
||||
|
||||
$queueForEvents->setParam('templateType', $templateId);
|
||||
|
||||
$response->dynamic(new Document([
|
||||
'templateId' => $templateId,
|
||||
'locale' => $locale,
|
||||
'senderName' => $template['senderName'],
|
||||
'senderEmail' => $template['senderEmail'],
|
||||
'subject' => $template['subject'],
|
||||
'replyToEmail' => $template['replyToEmail'],
|
||||
'replyToName' => $template['replyToName'],
|
||||
'message' => $template['message'],
|
||||
'custom' => true,
|
||||
]), Response::MODEL_EMAIL_TEMPLATE);
|
||||
}
|
||||
}
|
||||
@@ -26,6 +26,9 @@ use Appwrite\Platform\Modules\Project\Http\Project\Protocols\Update as UpdatePro
|
||||
use Appwrite\Platform\Modules\Project\Http\Project\Services\Update as UpdateProjectService;
|
||||
use Appwrite\Platform\Modules\Project\Http\Project\SMTP\Tests\Create as CreateSMTPTest;
|
||||
use Appwrite\Platform\Modules\Project\Http\Project\SMTP\Update as UpdateSMTP;
|
||||
use Appwrite\Platform\Modules\Project\Http\Project\Templates\Email\Delete as DeleteTemplate;
|
||||
use Appwrite\Platform\Modules\Project\Http\Project\Templates\Email\Get as GetTemplate;
|
||||
use Appwrite\Platform\Modules\Project\Http\Project\Templates\Email\Update as UpdateTemplate;
|
||||
use Appwrite\Platform\Modules\Project\Http\Project\Variables\Create as CreateVariable;
|
||||
use Appwrite\Platform\Modules\Project\Http\Project\Variables\Delete as DeleteVariable;
|
||||
use Appwrite\Platform\Modules\Project\Http\Project\Variables\Get as GetVariable;
|
||||
@@ -50,6 +53,10 @@ class Http extends Service
|
||||
// SMTP
|
||||
$this->addAction(UpdateSMTP::getName(), new UpdateSMTP());
|
||||
$this->addAction(CreateSMTPTest::getName(), new CreateSMTPTest());
|
||||
// Templates
|
||||
$this->addAction(GetTemplate::getName(), new GetTemplate());
|
||||
$this->addAction(DeleteTemplate::getName(), new DeleteTemplate());
|
||||
$this->addAction(UpdateTemplate::getName(), new UpdateTemplate());
|
||||
|
||||
// Variables
|
||||
$this->addAction(CreateVariable::getName(), new CreateVariable());
|
||||
|
||||
@@ -376,6 +376,8 @@ class Migrations extends Action
|
||||
'keys.write',
|
||||
'platforms.read',
|
||||
'platforms.write',
|
||||
'templates.read',
|
||||
'templates.write',
|
||||
]
|
||||
]);
|
||||
|
||||
|
||||
@@ -0,0 +1,31 @@
|
||||
<?php
|
||||
|
||||
namespace Appwrite\Utopia\Request\Filters;
|
||||
|
||||
use Appwrite\Utopia\Request\Filter;
|
||||
|
||||
class V23 extends Filter
|
||||
{
|
||||
// Convert 1.9.1 params to 1.9.2
|
||||
protected function parseEmailTemplate(array $content): array
|
||||
{
|
||||
if (isset($content['type'])) {
|
||||
$content['templateId'] = $content['type'];
|
||||
unset($content['type']);
|
||||
}
|
||||
|
||||
return $content;
|
||||
}
|
||||
|
||||
public function parse(array $content, string $model): array
|
||||
{
|
||||
switch ($model) {
|
||||
case 'project.getEmailTemplate':
|
||||
case 'project.updateEmailTemplate':
|
||||
case 'project.deleteEmailTemplate':
|
||||
$content = $this->parseEmailTemplate($content);
|
||||
break;
|
||||
}
|
||||
return $content;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
<?php
|
||||
|
||||
namespace Appwrite\Utopia\Response\Filters;
|
||||
|
||||
use Appwrite\Utopia\Response;
|
||||
use Appwrite\Utopia\Response\Filter;
|
||||
|
||||
// Convert 1.9.2 Data format to 1.9.1 format
|
||||
class V23 extends Filter
|
||||
{
|
||||
public function parse(array $content, string $model): array
|
||||
{
|
||||
return match ($model) {
|
||||
Response::MODEL_EMAIL_TEMPLATE => $this->parseEmailTemplate($content),
|
||||
default => $content,
|
||||
};
|
||||
}
|
||||
|
||||
private function parseEmailTemplate(array $content): array
|
||||
{
|
||||
if (isset($content['templateId'])) {
|
||||
$content['type'] = $content['templateId'];
|
||||
unset($content['templateId']);
|
||||
}
|
||||
|
||||
return $content;
|
||||
}
|
||||
}
|
||||
@@ -1,32 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace Appwrite\Utopia\Response\Model;
|
||||
|
||||
use Appwrite\Utopia\Response\Model;
|
||||
|
||||
abstract class Template extends Model
|
||||
{
|
||||
public function __construct()
|
||||
{
|
||||
$this
|
||||
->addRule('type', [
|
||||
'type' => self::TYPE_STRING,
|
||||
'description' => 'Template type',
|
||||
'default' => '',
|
||||
'example' => 'verification',
|
||||
])
|
||||
->addRule('locale', [
|
||||
'type' => self::TYPE_STRING,
|
||||
'description' => 'Template locale',
|
||||
'default' => '',
|
||||
'example' => 'en_us',
|
||||
])
|
||||
->addRule('message', [
|
||||
'type' => self::TYPE_STRING,
|
||||
'description' => 'Template message',
|
||||
'default' => '',
|
||||
'example' => 'Click on the link to verify your account.',
|
||||
])
|
||||
;
|
||||
}
|
||||
}
|
||||
@@ -3,13 +3,31 @@
|
||||
namespace Appwrite\Utopia\Response\Model;
|
||||
|
||||
use Appwrite\Utopia\Response;
|
||||
use Appwrite\Utopia\Response\Model;
|
||||
|
||||
class TemplateEmail extends Template
|
||||
class TemplateEmail extends Model
|
||||
{
|
||||
public function __construct()
|
||||
{
|
||||
parent::__construct();
|
||||
$this
|
||||
->addRule('templateId', [
|
||||
'type' => self::TYPE_STRING,
|
||||
'description' => 'Template type',
|
||||
'default' => '',
|
||||
'example' => 'verification',
|
||||
])
|
||||
->addRule('locale', [
|
||||
'type' => self::TYPE_STRING,
|
||||
'description' => 'Template locale',
|
||||
'default' => '',
|
||||
'example' => 'en_us',
|
||||
])
|
||||
->addRule('message', [
|
||||
'type' => self::TYPE_STRING,
|
||||
'description' => 'Template message',
|
||||
'default' => '',
|
||||
'example' => 'Click on the link to verify your account.',
|
||||
])
|
||||
->addRule('senderName', [
|
||||
'type' => self::TYPE_STRING,
|
||||
'description' => 'Name of the sender',
|
||||
@@ -40,6 +58,12 @@ class TemplateEmail extends Template
|
||||
'default' => '',
|
||||
'example' => 'Please verify your email address',
|
||||
])
|
||||
->addRule('custom', [
|
||||
'type' => self::TYPE_BOOLEAN,
|
||||
'description' => 'Whether the template has been customized for the project. Non-custom templates render from defaults.',
|
||||
'default' => false,
|
||||
'example' => false,
|
||||
])
|
||||
;
|
||||
}
|
||||
|
||||
|
||||
@@ -169,6 +169,8 @@ trait ProjectCustom
|
||||
'keys.write',
|
||||
'platforms.read',
|
||||
'platforms.write',
|
||||
'templates.read',
|
||||
'templates.write',
|
||||
],
|
||||
]);
|
||||
|
||||
|
||||
@@ -0,0 +1,530 @@
|
||||
<?php
|
||||
|
||||
namespace Tests\E2E\Services\Project;
|
||||
|
||||
use Tests\E2E\Client;
|
||||
|
||||
trait TemplatesBase
|
||||
{
|
||||
// =========================================================================
|
||||
// Get email template tests
|
||||
// =========================================================================
|
||||
|
||||
public function testGetEmailTemplateDefault(): void
|
||||
{
|
||||
$template = $this->getEmailTemplate('verification', 'en');
|
||||
|
||||
$this->assertSame(200, $template['headers']['status-code']);
|
||||
$this->assertSame('verification', $template['body']['templateId']);
|
||||
$this->assertSame('en', $template['body']['locale']);
|
||||
$this->assertFalse($template['body']['custom']);
|
||||
$this->assertNotEmpty($template['body']['subject']);
|
||||
$this->assertNotEmpty($template['body']['message']);
|
||||
}
|
||||
|
||||
public function testGetEmailTemplateDefaultLocale(): void
|
||||
{
|
||||
$template = $this->getEmailTemplate('verification');
|
||||
|
||||
$this->assertSame(200, $template['headers']['status-code']);
|
||||
$this->assertSame('verification', $template['body']['templateId']);
|
||||
$this->assertSame('en', $template['body']['locale']);
|
||||
$this->assertFalse($template['body']['custom']);
|
||||
}
|
||||
|
||||
public function testGetEmailTemplateCustom(): void
|
||||
{
|
||||
$update = $this->updateEmailTemplate('magicSession', 'en', 'Magic Subject', 'Magic Body');
|
||||
$this->assertSame(200, $update['headers']['status-code']);
|
||||
|
||||
$get = $this->getEmailTemplate('magicSession', 'en');
|
||||
|
||||
$this->assertSame(200, $get['headers']['status-code']);
|
||||
$this->assertSame('magicSession', $get['body']['templateId']);
|
||||
$this->assertSame('en', $get['body']['locale']);
|
||||
$this->assertTrue($get['body']['custom']);
|
||||
$this->assertSame('Magic Subject', $get['body']['subject']);
|
||||
$this->assertSame('Magic Body', $get['body']['message']);
|
||||
|
||||
// Cleanup
|
||||
$this->deleteEmailTemplate('magicSession', 'en');
|
||||
}
|
||||
|
||||
public function testGetEmailTemplateInvalidType(): void
|
||||
{
|
||||
$template = $this->getEmailTemplate('notATemplate', 'en');
|
||||
|
||||
$this->assertSame(400, $template['headers']['status-code']);
|
||||
}
|
||||
|
||||
public function testGetEmailTemplateInvalidLocale(): void
|
||||
{
|
||||
$template = $this->getEmailTemplate('verification', 'not-a-locale');
|
||||
|
||||
$this->assertSame(400, $template['headers']['status-code']);
|
||||
}
|
||||
|
||||
public function testGetEmailTemplateWithoutAuthentication(): void
|
||||
{
|
||||
$template = $this->getEmailTemplate('verification', 'en', false);
|
||||
|
||||
$this->assertSame(401, $template['headers']['status-code']);
|
||||
}
|
||||
|
||||
// =========================================================================
|
||||
// Update email template tests
|
||||
// =========================================================================
|
||||
|
||||
public function testUpdateEmailTemplate(): void
|
||||
{
|
||||
$update = $this->updateEmailTemplate(
|
||||
'verification',
|
||||
'en',
|
||||
'Please verify your email',
|
||||
'Click here to verify: {{url}}',
|
||||
);
|
||||
|
||||
$this->assertSame(200, $update['headers']['status-code']);
|
||||
$this->assertSame('verification', $update['body']['templateId']);
|
||||
$this->assertSame('en', $update['body']['locale']);
|
||||
$this->assertSame('Please verify your email', $update['body']['subject']);
|
||||
$this->assertSame('Click here to verify: {{url}}', $update['body']['message']);
|
||||
$this->assertTrue($update['body']['custom']);
|
||||
|
||||
// Verify persisted via GET
|
||||
$get = $this->getEmailTemplate('verification', 'en');
|
||||
$this->assertSame(200, $get['headers']['status-code']);
|
||||
$this->assertSame('Please verify your email', $get['body']['subject']);
|
||||
$this->assertSame('Click here to verify: {{url}}', $get['body']['message']);
|
||||
$this->assertTrue($get['body']['custom']);
|
||||
|
||||
// Cleanup
|
||||
$this->deleteEmailTemplate('verification', 'en');
|
||||
}
|
||||
|
||||
public function testUpdateEmailTemplateWithOptionalFields(): void
|
||||
{
|
||||
$update = $this->updateEmailTemplate(
|
||||
'invitation',
|
||||
'en',
|
||||
'Team invitation',
|
||||
'You have been invited',
|
||||
'Appwrite Team',
|
||||
'team@appwrite.io',
|
||||
'reply@appwrite.io',
|
||||
);
|
||||
|
||||
$this->assertSame(200, $update['headers']['status-code']);
|
||||
$this->assertSame('Team invitation', $update['body']['subject']);
|
||||
$this->assertSame('You have been invited', $update['body']['message']);
|
||||
$this->assertSame('Appwrite Team', $update['body']['senderName']);
|
||||
$this->assertSame('team@appwrite.io', $update['body']['senderEmail']);
|
||||
$this->assertSame('reply@appwrite.io', $update['body']['replyToEmail']);
|
||||
|
||||
// Cleanup
|
||||
$this->deleteEmailTemplate('invitation', 'en');
|
||||
}
|
||||
|
||||
public function testUpdateEmailTemplateDefaultLocale(): void
|
||||
{
|
||||
$update = $this->updateEmailTemplate(
|
||||
'sessionAlert',
|
||||
null,
|
||||
'Session alert',
|
||||
'Someone signed in',
|
||||
);
|
||||
|
||||
$this->assertSame(200, $update['headers']['status-code']);
|
||||
$this->assertSame('sessionAlert', $update['body']['templateId']);
|
||||
$this->assertSame('en', $update['body']['locale']);
|
||||
|
||||
// Cleanup
|
||||
$this->deleteEmailTemplate('sessionAlert', 'en');
|
||||
}
|
||||
|
||||
public function testUpdateEmailTemplateOverwrite(): void
|
||||
{
|
||||
$this->updateEmailTemplate('otpSession', 'en', 'First', 'First body');
|
||||
|
||||
$second = $this->updateEmailTemplate('otpSession', 'en', 'Second', 'Second body');
|
||||
|
||||
$this->assertSame(200, $second['headers']['status-code']);
|
||||
$this->assertSame('Second', $second['body']['subject']);
|
||||
$this->assertSame('Second body', $second['body']['message']);
|
||||
|
||||
$get = $this->getEmailTemplate('otpSession', 'en');
|
||||
$this->assertSame('Second', $get['body']['subject']);
|
||||
|
||||
// Cleanup
|
||||
$this->deleteEmailTemplate('otpSession', 'en');
|
||||
}
|
||||
|
||||
public function testUpdateEmailTemplateInvalidType(): void
|
||||
{
|
||||
$update = $this->updateEmailTemplate('notATemplate', 'en', 'Subject', 'Message');
|
||||
|
||||
$this->assertSame(400, $update['headers']['status-code']);
|
||||
}
|
||||
|
||||
public function testUpdateEmailTemplateMissingSubject(): void
|
||||
{
|
||||
$update = $this->updateEmailTemplate('verification', 'en', null, 'Message only');
|
||||
|
||||
$this->assertSame(400, $update['headers']['status-code']);
|
||||
}
|
||||
|
||||
public function testUpdateEmailTemplateMissingMessage(): void
|
||||
{
|
||||
$update = $this->updateEmailTemplate('verification', 'en', 'Subject only', null);
|
||||
|
||||
$this->assertSame(400, $update['headers']['status-code']);
|
||||
}
|
||||
|
||||
public function testUpdateEmailTemplateInvalidSenderEmail(): void
|
||||
{
|
||||
$update = $this->updateEmailTemplate(
|
||||
'verification',
|
||||
'en',
|
||||
'Subject',
|
||||
'Message',
|
||||
'Sender',
|
||||
'not-an-email',
|
||||
);
|
||||
|
||||
$this->assertSame(400, $update['headers']['status-code']);
|
||||
}
|
||||
|
||||
public function testUpdateEmailTemplateInvalidReplyTo(): void
|
||||
{
|
||||
$update = $this->updateEmailTemplate(
|
||||
'verification',
|
||||
'en',
|
||||
'Subject',
|
||||
'Message',
|
||||
null,
|
||||
null,
|
||||
'not-an-email',
|
||||
);
|
||||
|
||||
$this->assertSame(400, $update['headers']['status-code']);
|
||||
}
|
||||
|
||||
public function testUpdateEmailTemplateWithoutAuthentication(): void
|
||||
{
|
||||
$update = $this->updateEmailTemplate(
|
||||
'verification',
|
||||
'en',
|
||||
'Subject',
|
||||
'Message',
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
false,
|
||||
);
|
||||
|
||||
$this->assertSame(401, $update['headers']['status-code']);
|
||||
}
|
||||
|
||||
// =========================================================================
|
||||
// Delete email template tests
|
||||
// =========================================================================
|
||||
|
||||
public function testDeleteEmailTemplate(): void
|
||||
{
|
||||
$update = $this->updateEmailTemplate('mfaChallenge', 'en', 'MFA', 'Enter code');
|
||||
$this->assertSame(200, $update['headers']['status-code']);
|
||||
|
||||
$customBefore = $this->getEmailTemplate('mfaChallenge', 'en');
|
||||
$this->assertTrue($customBefore['body']['custom']);
|
||||
|
||||
$delete = $this->deleteEmailTemplate('mfaChallenge', 'en');
|
||||
$this->assertSame(204, $delete['headers']['status-code']);
|
||||
$this->assertEmpty($delete['body']);
|
||||
|
||||
// Verify reset back to default
|
||||
$after = $this->getEmailTemplate('mfaChallenge', 'en');
|
||||
$this->assertSame(200, $after['headers']['status-code']);
|
||||
$this->assertFalse($after['body']['custom']);
|
||||
$this->assertNotSame('MFA', $after['body']['subject']);
|
||||
}
|
||||
|
||||
public function testDeleteEmailTemplateDefault(): void
|
||||
{
|
||||
// Attempt to delete a template that was never customized
|
||||
$delete = $this->deleteEmailTemplate('verification', 'fr');
|
||||
|
||||
$this->assertSame(401, $delete['headers']['status-code']);
|
||||
$this->assertSame('project_template_default_deletion', $delete['body']['type']);
|
||||
}
|
||||
|
||||
public function testDeleteEmailTemplateInvalidType(): void
|
||||
{
|
||||
$delete = $this->deleteEmailTemplate('notATemplate', 'en');
|
||||
|
||||
$this->assertSame(400, $delete['headers']['status-code']);
|
||||
}
|
||||
|
||||
public function testDeleteEmailTemplateWithoutAuthentication(): void
|
||||
{
|
||||
$update = $this->updateEmailTemplate('recovery', 'en', 'Recovery', 'Reset password');
|
||||
$this->assertSame(200, $update['headers']['status-code']);
|
||||
|
||||
$delete = $this->deleteEmailTemplate('recovery', 'en', false);
|
||||
|
||||
$this->assertSame(401, $delete['headers']['status-code']);
|
||||
|
||||
// Verify still customized
|
||||
$get = $this->getEmailTemplate('recovery', 'en');
|
||||
$this->assertTrue($get['body']['custom']);
|
||||
|
||||
// Cleanup
|
||||
$this->deleteEmailTemplate('recovery', 'en');
|
||||
}
|
||||
|
||||
// =========================================================================
|
||||
// Legacy response format tests (request + response filters)
|
||||
// =========================================================================
|
||||
|
||||
public function testGetEmailTemplateLegacyResponseFormat(): void
|
||||
{
|
||||
$headers = \array_merge([
|
||||
'content-type' => 'application/json',
|
||||
'x-appwrite-project' => $this->getProject()['$id'],
|
||||
'x-appwrite-response-format' => '1.9.1',
|
||||
], $this->getHeaders());
|
||||
|
||||
$template = $this->client->call(
|
||||
Client::METHOD_GET,
|
||||
'/project/templates/email/verification',
|
||||
$headers,
|
||||
);
|
||||
|
||||
$this->assertSame(200, $template['headers']['status-code']);
|
||||
// Response filter should rename templateId -> type for < 1.9.2 clients.
|
||||
$this->assertArrayHasKey('type', $template['body']);
|
||||
$this->assertArrayNotHasKey('templateId', $template['body']);
|
||||
$this->assertSame('verification', $template['body']['type']);
|
||||
$this->assertSame('en', $template['body']['locale']);
|
||||
}
|
||||
|
||||
public function testUpdateEmailTemplateLegacyResponseFormat(): void
|
||||
{
|
||||
$headers = \array_merge([
|
||||
'content-type' => 'application/json',
|
||||
'x-appwrite-project' => $this->getProject()['$id'],
|
||||
'x-appwrite-response-format' => '1.9.1',
|
||||
], $this->getHeaders());
|
||||
|
||||
// Request filter should accept legacy `type` and map it to `templateId`.
|
||||
$update = $this->client->call(
|
||||
Client::METHOD_PATCH,
|
||||
'/project/templates/email',
|
||||
$headers,
|
||||
[
|
||||
'type' => 'magicSession',
|
||||
'locale' => 'en',
|
||||
'subject' => 'Legacy Subject',
|
||||
'message' => 'Legacy Body',
|
||||
],
|
||||
);
|
||||
|
||||
$this->assertSame(200, $update['headers']['status-code']);
|
||||
// Response filter should rename templateId -> type for < 1.9.2 clients.
|
||||
$this->assertArrayHasKey('type', $update['body']);
|
||||
$this->assertArrayNotHasKey('templateId', $update['body']);
|
||||
$this->assertSame('magicSession', $update['body']['type']);
|
||||
$this->assertSame('Legacy Subject', $update['body']['subject']);
|
||||
$this->assertSame('Legacy Body', $update['body']['message']);
|
||||
$this->assertTrue($update['body']['custom']);
|
||||
|
||||
// Verify persisted, then cleanup via legacy DELETE with `type`.
|
||||
$get = $this->getEmailTemplate('magicSession', 'en');
|
||||
$this->assertSame(200, $get['headers']['status-code']);
|
||||
$this->assertTrue($get['body']['custom']);
|
||||
|
||||
$delete = $this->client->call(
|
||||
Client::METHOD_DELETE,
|
||||
'/project/templates/email',
|
||||
$headers,
|
||||
[
|
||||
'type' => 'magicSession',
|
||||
'locale' => 'en',
|
||||
],
|
||||
);
|
||||
$this->assertSame(204, $delete['headers']['status-code']);
|
||||
|
||||
$after = $this->getEmailTemplate('magicSession', 'en');
|
||||
$this->assertFalse($after['body']['custom']);
|
||||
}
|
||||
|
||||
public function testDeleteEmailTemplateLegacyResponseFormat(): void
|
||||
{
|
||||
// Seed a custom template using the current API.
|
||||
$update = $this->updateEmailTemplate('otpSession', 'en', 'Legacy OTP', 'Legacy OTP body');
|
||||
$this->assertSame(200, $update['headers']['status-code']);
|
||||
|
||||
$headers = \array_merge([
|
||||
'content-type' => 'application/json',
|
||||
'x-appwrite-project' => $this->getProject()['$id'],
|
||||
'x-appwrite-response-format' => '1.9.1',
|
||||
], $this->getHeaders());
|
||||
|
||||
// Request filter should accept legacy `type` and map it to `templateId`.
|
||||
$delete = $this->client->call(
|
||||
Client::METHOD_DELETE,
|
||||
'/project/templates/email',
|
||||
$headers,
|
||||
[
|
||||
'type' => 'otpSession',
|
||||
'locale' => 'en',
|
||||
],
|
||||
);
|
||||
|
||||
$this->assertSame(204, $delete['headers']['status-code']);
|
||||
$this->assertEmpty($delete['body']);
|
||||
|
||||
// Verify reset back to default.
|
||||
$after = $this->getEmailTemplate('otpSession', 'en');
|
||||
$this->assertSame(200, $after['headers']['status-code']);
|
||||
$this->assertFalse($after['body']['custom']);
|
||||
$this->assertNotSame('Legacy OTP', $after['body']['subject']);
|
||||
}
|
||||
|
||||
public function testDeleteEmailTemplateLegacyInvalidType(): void
|
||||
{
|
||||
$headers = \array_merge([
|
||||
'content-type' => 'application/json',
|
||||
'x-appwrite-project' => $this->getProject()['$id'],
|
||||
'x-appwrite-response-format' => '1.9.1',
|
||||
], $this->getHeaders());
|
||||
|
||||
$delete = $this->client->call(
|
||||
Client::METHOD_DELETE,
|
||||
'/project/templates/email',
|
||||
$headers,
|
||||
[
|
||||
'type' => 'notATemplate',
|
||||
'locale' => 'en',
|
||||
],
|
||||
);
|
||||
|
||||
$this->assertSame(400, $delete['headers']['status-code']);
|
||||
}
|
||||
|
||||
public function testUpdateEmailTemplateLegacyInvalidType(): void
|
||||
{
|
||||
$headers = \array_merge([
|
||||
'content-type' => 'application/json',
|
||||
'x-appwrite-project' => $this->getProject()['$id'],
|
||||
'x-appwrite-response-format' => '1.9.1',
|
||||
], $this->getHeaders());
|
||||
|
||||
$update = $this->client->call(
|
||||
Client::METHOD_PATCH,
|
||||
'/project/templates/email',
|
||||
$headers,
|
||||
[
|
||||
'type' => 'notATemplate',
|
||||
'locale' => 'en',
|
||||
'subject' => 'Subject',
|
||||
'message' => 'Message',
|
||||
],
|
||||
);
|
||||
|
||||
$this->assertSame(400, $update['headers']['status-code']);
|
||||
}
|
||||
|
||||
// =========================================================================
|
||||
// Helpers
|
||||
// =========================================================================
|
||||
|
||||
protected function getEmailTemplate(string $type, ?string $locale = null, bool $authenticated = true): mixed
|
||||
{
|
||||
$headers = [
|
||||
'content-type' => 'application/json',
|
||||
'x-appwrite-project' => $this->getProject()['$id'],
|
||||
];
|
||||
|
||||
if ($authenticated) {
|
||||
$headers = \array_merge($headers, $this->getHeaders());
|
||||
}
|
||||
|
||||
$params = [];
|
||||
if ($locale !== null) {
|
||||
$params['locale'] = $locale;
|
||||
}
|
||||
|
||||
return $this->client->call(Client::METHOD_GET, '/project/templates/email/' . $type, $headers, $params);
|
||||
}
|
||||
|
||||
protected function updateEmailTemplate(
|
||||
string $type,
|
||||
?string $locale,
|
||||
?string $subject,
|
||||
?string $message,
|
||||
?string $senderName = null,
|
||||
?string $senderEmail = null,
|
||||
?string $replyToEmail = null,
|
||||
?string $replyToName = null,
|
||||
bool $authenticated = true,
|
||||
): mixed {
|
||||
$params = [
|
||||
'templateId' => $type,
|
||||
];
|
||||
|
||||
if ($locale !== null) {
|
||||
$params['locale'] = $locale;
|
||||
}
|
||||
if ($subject !== null) {
|
||||
$params['subject'] = $subject;
|
||||
}
|
||||
if ($message !== null) {
|
||||
$params['message'] = $message;
|
||||
}
|
||||
if ($senderName !== null) {
|
||||
$params['senderName'] = $senderName;
|
||||
}
|
||||
if ($senderEmail !== null) {
|
||||
$params['senderEmail'] = $senderEmail;
|
||||
}
|
||||
if ($replyToEmail !== null) {
|
||||
$params['replyToEmail'] = $replyToEmail;
|
||||
}
|
||||
if ($replyToName !== null) {
|
||||
$params['replyToName'] = $replyToName;
|
||||
}
|
||||
|
||||
$headers = [
|
||||
'content-type' => 'application/json',
|
||||
'x-appwrite-project' => $this->getProject()['$id'],
|
||||
];
|
||||
|
||||
if ($authenticated) {
|
||||
$headers = \array_merge($headers, $this->getHeaders());
|
||||
}
|
||||
|
||||
return $this->client->call(Client::METHOD_PATCH, '/project/templates/email', $headers, $params);
|
||||
}
|
||||
|
||||
protected function deleteEmailTemplate(string $type, ?string $locale = null, bool $authenticated = true): mixed
|
||||
{
|
||||
$params = [
|
||||
'templateId' => $type,
|
||||
];
|
||||
|
||||
if ($locale !== null) {
|
||||
$params['locale'] = $locale;
|
||||
}
|
||||
|
||||
$headers = [
|
||||
'content-type' => 'application/json',
|
||||
'x-appwrite-project' => $this->getProject()['$id'],
|
||||
];
|
||||
|
||||
if ($authenticated) {
|
||||
$headers = \array_merge($headers, $this->getHeaders());
|
||||
}
|
||||
|
||||
return $this->client->call(Client::METHOD_DELETE, '/project/templates/email', $headers, $params);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
<?php
|
||||
|
||||
namespace Tests\E2E\Services\Project;
|
||||
|
||||
use Tests\E2E\Scopes\ProjectCustom;
|
||||
use Tests\E2E\Scopes\Scope;
|
||||
use Tests\E2E\Scopes\SideConsole;
|
||||
|
||||
class TemplatesConsoleClientTest extends Scope
|
||||
{
|
||||
use TemplatesBase;
|
||||
use ProjectCustom;
|
||||
use SideConsole;
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
<?php
|
||||
|
||||
namespace Tests\E2E\Services\Project;
|
||||
|
||||
use Tests\E2E\Scopes\ProjectCustom;
|
||||
use Tests\E2E\Scopes\Scope;
|
||||
use Tests\E2E\Scopes\SideServer;
|
||||
|
||||
class TemplatesCustomServerTest extends Scope
|
||||
{
|
||||
use TemplatesBase;
|
||||
use ProjectCustom;
|
||||
use SideServer;
|
||||
}
|
||||
@@ -1121,6 +1121,7 @@ class ProjectsConsoleClientTest extends Scope
|
||||
$response = $this->client->call(Client::METHOD_GET, '/projects/' . $id . '/templates/email/verification/en-us', array_merge([
|
||||
'content-type' => 'application/json',
|
||||
'x-appwrite-project' => $this->getProject()['$id'],
|
||||
'x-appwrite-response-format' => '1.9.1',
|
||||
], $this->getHeaders()));
|
||||
|
||||
$this->assertEquals(200, $response['headers']['status-code']);
|
||||
@@ -1133,6 +1134,7 @@ class ProjectsConsoleClientTest extends Scope
|
||||
$response = $this->client->call(Client::METHOD_PATCH, '/projects/' . $id . '/templates/email/verification/en-us', array_merge([
|
||||
'content-type' => 'application/json',
|
||||
'x-appwrite-project' => $this->getProject()['$id'],
|
||||
'x-appwrite-response-format' => '1.9.1',
|
||||
], $this->getHeaders()), [
|
||||
'subject' => 'Please verify your email',
|
||||
'message' => 'Please verify your email {{url}}',
|
||||
@@ -1152,6 +1154,7 @@ class ProjectsConsoleClientTest extends Scope
|
||||
$response = $this->client->call(Client::METHOD_GET, '/projects/' . $id . '/templates/email/verification/en-us', array_merge([
|
||||
'content-type' => 'application/json',
|
||||
'x-appwrite-project' => $this->getProject()['$id'],
|
||||
'x-appwrite-response-format' => '1.9.1',
|
||||
], $this->getHeaders()));
|
||||
|
||||
$this->assertEquals(200, $response['headers']['status-code']);
|
||||
@@ -1219,6 +1222,7 @@ class ProjectsConsoleClientTest extends Scope
|
||||
$response = $this->client->call(Client::METHOD_PATCH, '/projects/' . $projectId . '/templates/email', array_merge([
|
||||
'content-type' => 'application/json',
|
||||
'x-appwrite-project' => $this->getProject()['$id'],
|
||||
'x-appwrite-response-format' => '1.9.1',
|
||||
], $this->getHeaders()), [
|
||||
'type' => 'sessionAlert',
|
||||
// Intentionally no locale
|
||||
@@ -1237,6 +1241,7 @@ class ProjectsConsoleClientTest extends Scope
|
||||
$response = $this->client->call(Client::METHOD_PATCH, '/projects/' . $projectId . '/templates/email', array_merge([
|
||||
'content-type' => 'application/json',
|
||||
'x-appwrite-project' => $this->getProject()['$id'],
|
||||
'x-appwrite-response-format' => '1.9.1',
|
||||
], $this->getHeaders()), [
|
||||
'type' => 'sessionAlert',
|
||||
'locale' => 'sk',
|
||||
|
||||
Reference in New Issue
Block a user