diff --git a/app/config/locale/templates.php b/app/config/locale/templates.php
index ac5a2acf1d..0e566e2450 100644
--- a/app/config/locale/templates.php
+++ b/app/config/locale/templates.php
@@ -6,7 +6,9 @@ return [
'magicSession',
'recovery',
'invitation',
- 'mfaChallenge'
+ 'mfaChallenge',
+ 'mfaCreated',
+ 'mfaRemoved',
],
'sms' => [
'verification',
diff --git a/app/config/locale/templates/email-mfa-altered.tpl b/app/config/locale/templates/email-mfa-altered.tpl
new file mode 100644
index 0000000000..320f5b757d
--- /dev/null
+++ b/app/config/locale/templates/email-mfa-altered.tpl
@@ -0,0 +1,11 @@
+{{body}}
+
+
+
+
\ No newline at end of file
diff --git a/app/config/locale/translations/en.json b/app/config/locale/translations/en.json
index 8c908ae5ab..fa5d142bc6 100644
--- a/app/config/locale/translations/en.json
+++ b/app/config/locale/translations/en.json
@@ -31,6 +31,20 @@
"emails.mfaChallenge.clientInfo": "This verification code was requested using {{b}}{{agentClient}}{{/b}} on {{b}}{{agentDevice}}{{/b}} {{b}}{{agentOs}}{{/b}}. If you didn't request the verification code, you can safely ignore this email.",
"emails.mfaChallenge.thanks": "Thanks,",
"emails.mfaChallenge.signature": "{{project}} team",
+ "emails.mfaCreated.subject": "Two-factor authentication is enabled in your {{project}} account",
+ "emails.mfaCreated.hello": "Hello {{user}}",
+ "emails.mfaCreated.body": "Two-factor authentication has been successfully enabled for your {{project}} account.
Please make sure to store your security codes safely, so you can use them if you lose or can’t access your mobile device. Each recovery code can only be used once, but you can re-generate a new set of 6 codes at any time.",
+ "emails.mfaCreated.footer": "If you didn’t enable two-factor authentication, please contact us immediately.",
+ "emails.mfaCreated.thanks": "Thanks,",
+ "emails.mfaCreated.signature": "{{project}} team",
+ "emails.mfaCreated.buttonText": "Visit your account",
+ "emails.mfaRemoved.subject": "Two-factor authentication is disabled in your {{project}} account",
+ "emails.mfaRemoved.hello": "Hello {{user}}",
+ "emails.mfaRemoved.body": "You've recently disabled two-factor authentication for your {{project}} account. You can always enable it again through your {{project}} account.",
+ "emails.mfaRemoved.footer": "If you didn’t disable two-factor authentication, please contact us immediately.",
+ "emails.mfaRemoved.thanks": "Thanks,",
+ "emails.mfaRemoved.signature": "{{project}} team",
+ "emails.mfaRemoved.buttonText": "Visit your account",
"emails.recovery.subject": "Password Reset",
"emails.recovery.hello": "Hello {{user}}",
"emails.recovery.body": "Follow this link to reset your {{b}}{{project}}{{/b}} password.",
diff --git a/app/controllers/api/account.php b/app/controllers/api/account.php
index 16538f68b6..2a251fbda6 100644
--- a/app/controllers/api/account.php
+++ b/app/controllers/api/account.php
@@ -3667,7 +3667,10 @@ App::put('/v1/account/mfa/authenticators/:type')
->inject('session')
->inject('dbForProject')
->inject('queueForEvents')
- ->action(function (string $type, string $otp, Response $response, Document $user, Document $session, Database $dbForProject, Event $queueForEvents) {
+ ->inject('project')
+ ->inject('locale')
+ ->inject('queueForMails')
+ ->action(function (string $type, string $otp, Response $response, Document $user, Document $session, Database $dbForProject, Event $queueForEvents, Document $project, Locale $locale, Mail $queueForMails) {
$authenticator = (match ($type) {
Type::TOTP => TOTP::getAuthenticatorFromUser($user),
@@ -3703,6 +3706,99 @@ App::put('/v1/account/mfa/authenticators/:type')
$session->setAttribute('factors', $factors);
$dbForProject->updateDocument('sessions', $session->getId(), $session);
+ // Send email to user informing that MFA is enabled
+ $subject = $locale->getText("emails.mfaCreated.subject");
+ $customTemplate = $project->getAttribute('templates', [])['email.mfaCreated-' . $locale->default] ?? [];
+
+ // Appwrite theming for console
+ if ($project->getInternalId() === 'console') {
+ $message = Template::fromFile(__DIR__ . '/../../config/locale/templates/email-base-styled.tpl');
+ $body = Template::fromFile(__DIR__ . '/../../config/locale/templates/email-mfa-altered.tpl');
+
+ $body
+ ->setParam('{{buttonText}}', $locale->getText("emails.mfaCreated.buttonText"))
+ ->setParam('{{redirect}}', App::getEnv('_APP_DOMAIN') . '/console/account')
+ ->setParam('{{body}}', $locale->getText("emails.mfaCreated.body"), escapeHtml: false)
+ ->setParam('{{project}}', $project->getAttribute('name'))
+ ->setParam('{{user}}', $user->getAttribute('name'));
+
+ $message->setParam('{{body}}', $body->render(), escapeHtml: false);
+ } else {
+ $message = Template::fromFile(__DIR__ . '/../../config/locale/templates/email-inner-base.tpl');
+
+ $message->setParam('{{body}}', $locale->getText("emails.mfaCreated.body"), escapeHtml: false);
+ }
+
+ $message
+ ->setParam('{{hello}}', $locale->getText("emails.mfaCreated.hello"))
+ ->setParam('{{footer}}', $locale->getText("emails.mfaCreated.footer"))
+ ->setParam('{{thanks}}', $locale->getText("emails.mfaCreated.thanks"))
+ ->setParam('{{signature}}', $locale->getText("emails.mfaCreated.signature"));
+
+ $body = $message->render();
+
+ $smtp = $project->getAttribute('smtp', []);
+ $smtpEnabled = $smtp['enabled'] ?? false;
+
+ $senderEmail = System::getEnv('_APP_SYSTEM_EMAIL_ADDRESS', APP_EMAIL_TEAM);
+ $senderName = System::getEnv('_APP_SYSTEM_EMAIL_NAME', APP_NAME . ' Server');
+ $replyTo = "";
+
+ if ($smtpEnabled) {
+ if (!empty($smtp['senderEmail'])) {
+ $senderEmail = $smtp['senderEmail'];
+ }
+ if (!empty($smtp['senderName'])) {
+ $senderName = $smtp['senderName'];
+ }
+ if (!empty($smtp['replyTo'])) {
+ $replyTo = $smtp['replyTo'];
+ }
+
+ $queueForMails
+ ->setSmtpHost($smtp['host'] ?? '')
+ ->setSmtpPort($smtp['port'] ?? '')
+ ->setSmtpUsername($smtp['username'] ?? '')
+ ->setSmtpPassword($smtp['password'] ?? '')
+ ->setSmtpSecure($smtp['secure'] ?? '');
+
+ if (!empty($customTemplate)) {
+ if (!empty($customTemplate['senderEmail'])) {
+ $senderEmail = $customTemplate['senderEmail'];
+ }
+ if (!empty($customTemplate['senderName'])) {
+ $senderName = $customTemplate['senderName'];
+ }
+ if (!empty($customTemplate['replyTo'])) {
+ $replyTo = $customTemplate['replyTo'];
+ }
+
+ $body = $customTemplate['message'] ?? '';
+ $subject = $customTemplate['subject'] ?? $subject;
+ }
+
+ $queueForMails
+ ->setSmtpReplyTo($replyTo)
+ ->setSmtpSenderEmail($senderEmail)
+ ->setSmtpSenderName($senderName);
+ }
+
+ $emailVariables = [
+ 'owner' => $user->getAttribute('name'),
+ 'direction' => $locale->getText('settings.direction'),
+ 'user' => $user->getAttribute('name'),
+ 'project' => $project->getAttribute('name')
+ ];
+
+ $queueForMails
+ ->setSubject($subject)
+ ->setBody($body)
+ ->setRecipient($user->getAttribute('email'))
+ ->setName($user->getAttribute('name'))
+ ->setVariables($emailVariables)
+ ->trigger()
+ ;
+
$queueForEvents->setParam('userId', $user->getId());
$response->dynamic($user, Response::MODEL_ACCOUNT);
@@ -3841,7 +3937,10 @@ App::delete('/v1/account/mfa/authenticators/:type')
->inject('user')
->inject('dbForProject')
->inject('queueForEvents')
- ->action(function (string $type, string $otp, Response $response, Document $user, Database $dbForProject, Event $queueForEvents) {
+ ->inject('project')
+ ->inject('locale')
+ ->inject('queueForMails')
+ ->action(function (string $type, string $otp, Response $response, Document $user, Database $dbForProject, Event $queueForEvents, Document $project, Locale $locale, Mail $queueForMails) {
$authenticator = (match ($type) {
Type::TOTP => TOTP::getAuthenticatorFromUser($user),
@@ -3876,6 +3975,99 @@ App::delete('/v1/account/mfa/authenticators/:type')
$dbForProject->deleteDocument('authenticators', $authenticator->getId());
$dbForProject->purgeCachedDocument('users', $user->getId());
+ // Send email to user informing that MFA is disabled
+ $subject = $locale->getText("emails.mfaRemoved.subject");
+ $customTemplate = $project->getAttribute('templates', [])['email.mfaRemoved-' . $locale->default] ?? [];
+
+ // Appwrite theming for console
+ if ($project->getInternalId() === 'console') {
+ $message = Template::fromFile(__DIR__ . '/../../config/locale/templates/email-base-styled.tpl');
+ $body = Template::fromFile(__DIR__ . '/../../config/locale/templates/email-mfa-altered.tpl');
+
+ $body
+ ->setParam('{{buttonText}}', $locale->getText("emails.mfaRemoved.buttonText"))
+ ->setParam('{{redirect}}', App::getEnv('_APP_DOMAIN') . '/console/account')
+ ->setParam('{{body}}', $locale->getText("emails.mfaRemoved.body"), escapeHtml: false)
+ ->setParam('{{project}}', $project->getAttribute('name'))
+ ->setParam('{{user}}', $user->getAttribute('name'));
+
+ $message->setParam('{{body}}', $body->render(), escapeHtml: false);
+ } else {
+ $message = Template::fromFile(__DIR__ . '/../../config/locale/templates/email-inner-base.tpl');
+
+ $message->setParam('{{body}}', $locale->getText("emails.mfaRemoved.body"), escapeHtml: false);
+ }
+
+ $message
+ ->setParam('{{hello}}', $locale->getText("emails.mfaRemoved.hello"))
+ ->setParam('{{footer}}', $locale->getText("emails.mfaRemoved.footer"))
+ ->setParam('{{thanks}}', $locale->getText("emails.mfaRemoved.thanks"))
+ ->setParam('{{signature}}', $locale->getText("emails.mfaRemoved.signature"));
+
+ $body = $message->render();
+
+ $smtp = $project->getAttribute('smtp', []);
+ $smtpEnabled = $smtp['enabled'] ?? false;
+
+ $senderEmail = System::getEnv('_APP_SYSTEM_EMAIL_ADDRESS', APP_EMAIL_TEAM);
+ $senderName = System::getEnv('_APP_SYSTEM_EMAIL_NAME', APP_NAME . ' Server');
+ $replyTo = "";
+
+ if ($smtpEnabled) {
+ if (!empty($smtp['senderEmail'])) {
+ $senderEmail = $smtp['senderEmail'];
+ }
+ if (!empty($smtp['senderName'])) {
+ $senderName = $smtp['senderName'];
+ }
+ if (!empty($smtp['replyTo'])) {
+ $replyTo = $smtp['replyTo'];
+ }
+
+ $queueForMails
+ ->setSmtpHost($smtp['host'] ?? '')
+ ->setSmtpPort($smtp['port'] ?? '')
+ ->setSmtpUsername($smtp['username'] ?? '')
+ ->setSmtpPassword($smtp['password'] ?? '')
+ ->setSmtpSecure($smtp['secure'] ?? '');
+
+ if (!empty($customTemplate)) {
+ if (!empty($customTemplate['senderEmail'])) {
+ $senderEmail = $customTemplate['senderEmail'];
+ }
+ if (!empty($customTemplate['senderName'])) {
+ $senderName = $customTemplate['senderName'];
+ }
+ if (!empty($customTemplate['replyTo'])) {
+ $replyTo = $customTemplate['replyTo'];
+ }
+
+ $body = $customTemplate['message'] ?? '';
+ $subject = $customTemplate['subject'] ?? $subject;
+ }
+
+ $queueForMails
+ ->setSmtpReplyTo($replyTo)
+ ->setSmtpSenderEmail($senderEmail)
+ ->setSmtpSenderName($senderName);
+ }
+
+ $emailVariables = [
+ 'owner' => $user->getAttribute('name'),
+ 'direction' => $locale->getText('settings.direction'),
+ 'user' => $user->getAttribute('name'),
+ 'project' => $project->getAttribute('name')
+ ];
+
+ $queueForMails
+ ->setSubject($subject)
+ ->setBody($body)
+ ->setRecipient($user->getAttribute('email'))
+ ->setName($user->getAttribute('name'))
+ ->setVariables($emailVariables)
+ ->trigger()
+ ;
+
$queueForEvents->setParam('userId', $user->getId());
$response->noContent();