diff --git a/app/config/locale/templates.php b/app/config/locale/templates.php index 0e566e2450..bf6540dfab 100644 --- a/app/config/locale/templates.php +++ b/app/config/locale/templates.php @@ -7,8 +7,8 @@ return [ 'recovery', 'invitation', 'mfaChallenge', - 'mfaCreated', - 'mfaRemoved', + 'mfaEnabled', + 'mfaDisabled', ], 'sms' => [ 'verification', diff --git a/app/config/locale/translations/en.json b/app/config/locale/translations/en.json index fa5d142bc6..9543f155b7 100644 --- a/app/config/locale/translations/en.json +++ b/app/config/locale/translations/en.json @@ -31,20 +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.mfaEnabled.subject": "Two-factor authentication is enabled in your {{project}} account", + "emails.mfaEnabled.hello": "Hello {{user}}", + "emails.mfaEnabled.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.mfaEnabled.footer": "If you didn’t enable two-factor authentication, please contact us immediately.", + "emails.mfaEnabled.thanks": "Thanks", + "emails.mfaEnabled.signature": "{{project}} team", + "emails.mfaEnabled.buttonText": "Visit your account", + "emails.mfaDisabled.subject": "Two-factor authentication is disabled in your {{project}} account", + "emails.mfaDisabled.hello": "Hello {{user}}", + "emails.mfaDisabled.body": "You've disabled two-factor authentication for your {{project}} account. You can always enable it again through your {{project}} account.", + "emails.mfaDisabled.footer": "If you didn’t disable two-factor authentication, please contact us immediately.", + "emails.mfaDisabled.thanks": "Thanks", + "emails.mfaDisabled.signature": "{{project}} team", + "emails.mfaDisabled.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 7bd33dc8ca..c77857c388 100644 --- a/app/controllers/api/account.php +++ b/app/controllers/api/account.php @@ -166,6 +166,101 @@ $createSession = function (string $userId, string $secret, Request $request, Res $response->dynamic($session, Response::MODEL_SESSION); }; +$sendMFAAlert = function (Document $project, Document $user, Locale $locale, Mail $queueForMails, string $type, string $redirect) { + $subject = $locale->getText("emails.{$type}.subject"); + $customTemplate = $project->getAttribute('templates', [])["email.{$type}-" . $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.{$type}.buttonText")) + ->setParam('{{redirect}}', 'http://' . App::getEnv('_APP_DOMAIN') . '/console/account') + ->setParam('{{body}}', $locale->getText("emails.{$type}.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.{$type}.body"), escapeHtml: false); + } + + $message + ->setParam('{{hello}}', $locale->getText("emails.{$type}.hello")) + ->setParam('{{footer}}', $locale->getText("emails.{$type}.footer")) + ->setParam('{{thanks}}', $locale->getText("emails.{$type}.thanks")) + ->setParam('{{signature}}', $locale->getText("emails.{$type}.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'), + 'redirect' => $redirect + ]; + + $queueForMails + ->setSubject($subject) + ->setBody($body) + ->setRecipient($user->getAttribute('email')) + ->setName($user->getAttribute('name')) + ->setVariables($emailVariables) + ->trigger() + ; +}; + App::post('/v1/account') ->desc('Create account') ->groups(['api', 'account', 'auth']) @@ -3505,12 +3600,25 @@ App::patch('/v1/account/mfa') ->label('sdk.offline.key', 'current') ->param('mfa', null, new Boolean(), 'Enable or disable MFA.') ->inject('requestTimestamp') + ->inject('request') ->inject('response') ->inject('user') + ->inject('project') ->inject('session') + ->inject('locale') ->inject('dbForProject') ->inject('queueForEvents') - ->action(function (bool $mfa, ?\DateTime $requestTimestamp, Response $response, Document $user, Document $session, Database $dbForProject, Event $queueForEvents) { + ->inject('queueForMails') + ->action(function (bool $mfa, ?\DateTime $requestTimestamp, Request $request, Response $response, Document $user, Document $project, Document $session, Locale $locale, Database $dbForProject, Event $queueForEvents, Mail $queueForMails) use ($sendMFAAlert) { + // If MFA Changes then we need to send a email + if ($mfa !== $user->getAttribute('mfa')) { + $domain = $request->getHostname(); + $protocol = $request->getProtocol(); + + $redirect = $protocol . '://' . $domain . '/console/account'; + + $sendMFAAlert($project, $user, $locale, $queueForMails, $mfa ? 'mfaEnabled' : 'mfaDisabled', $redirect); + } $user->setAttribute('mfa', $mfa); @@ -3667,11 +3775,7 @@ App::put('/v1/account/mfa/authenticators/:type') ->inject('session') ->inject('dbForProject') ->inject('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) { - + ->action(function (string $type, string $otp, Response $response, Document $user, Document $session, Database $dbForProject, Event $queueForEvents) { $authenticator = (match ($type) { Type::TOTP => TOTP::getAuthenticatorFromUser($user), default => null @@ -3706,99 +3810,6 @@ 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}}', 'http://' . 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); @@ -3937,11 +3948,7 @@ App::delete('/v1/account/mfa/authenticators/:type') ->inject('user') ->inject('dbForProject') ->inject('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) { - + ->action(function (string $type, string $otp, Response $response, Document $user, Database $dbForProject, Event $queueForEvents) { $authenticator = (match ($type) { Type::TOTP => TOTP::getAuthenticatorFromUser($user), default => null @@ -3975,99 +3982,6 @@ 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}}', 'http://' . 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();