From 0b9e43c9f82bd84d6390748ec8f55ecdc6791710 Mon Sep 17 00:00:00 2001 From: Hemachandar Date: Tue, 16 Sep 2025 00:58:23 +0530 Subject: [PATCH 1/9] Branded email for Console auth flows --- app/config/console.php | 40 ++++ .../locale/templates/email-auth-styled.tpl | 224 ++++++++++++++++++ .../locale/templates/email-mfa-challenge.tpl | 2 +- app/config/locale/templates/email-otp.tpl | 2 +- .../locale/templates/email-verification.tpl | 9 + app/controllers/api/account.php | 87 ++++++- 6 files changed, 361 insertions(+), 3 deletions(-) create mode 100644 app/config/locale/templates/email-auth-styled.tpl create mode 100644 app/config/locale/templates/email-verification.tpl diff --git a/app/config/console.php b/app/config/console.php index faacecaa08..164db597e9 100644 --- a/app/config/console.php +++ b/app/config/console.php @@ -9,6 +9,8 @@ use Appwrite\Network\Platform; use Utopia\Database\Helpers\ID; use Utopia\System\System; +$localeCodes = include __DIR__ . '/locale/codes.php'; + $console = [ '$id' => ID::custom('console'), '$sequence' => ID::custom('console'), @@ -49,6 +51,44 @@ $console = [ 'githubSecret' => System::getEnv('_APP_CONSOLE_GITHUB_SECRET', ''), 'githubAppid' => System::getEnv('_APP_CONSOLE_GITHUB_APP_ID', '') ], + 'templates' => [ + 'email.verification-en' => [ + 'subject' => 'Account Verification', + 'preview' => 'Verify your email to activate your {{project}} account.', + 'heading' => 'Verify your email to start using Appwrite Cloud', + 'hello' => 'Hello {{user}},', + 'body' => 'Thanks for signing up for Appwrite Cloud. Before you can get started, please verify your email address.', + 'footer' => 'If you didn’t create an account, you can ignore this email.', + 'buttonText' => 'Verify email', + 'thanks' => 'Thanks,', + "signature" => "{{project}} team", + ], + 'email.mfaChallenge-en' => [ + 'subject' => 'Verification Code for {{project}}', + 'preview' => 'Use code {{otp}} for two-step verification in {{project}}. Expires in 15 minutes.', + 'heading' => 'Complete two-step verification to use Appwrite Cloud', + 'hello' => 'Hello {{user}},', + 'body' => 'Enter the following code to confirm your two-step verification in {{b}}{{project}}{{/b}}. This code will expire in 15 minutes.', + 'thanks' => 'Thanks,', + "signature" => "{{project}} team", + ], + 'email.otpSession-en' => [ + 'subject' => 'OTP for {{project}} account login', + 'preview' => 'Use OTP {{otp}} to sign in to {{project}}. Expires in 15 minutes.', + 'heading' => 'Login with OTP to use Appwrite Cloud', + 'hello' => 'Hello {{user}},', + 'body' => 'Enter the following verification code when prompted to securely sign in to your {{b}}{{project}}{{/b}} account. This code will expire in 15 minutes.', + 'thanks' => 'Thanks,', + "signature" => "{{project}} team", + ], + ], + 'customEmails' => true, ]; +foreach ($localeCodes as $localeCode) { + $console['templates']['email.verification-'.$localeCode['code']] = $console['templates']['email.verification-en']; + $console['templates']['email.mfaChallenge-'.$localeCode['code']] = $console['templates']['email.mfaChallenge-en']; + $console['templates']['email.otpSession-'.$localeCode['code']] = $console['templates']['email.otpSession-en']; +} + return $console; diff --git a/app/config/locale/templates/email-auth-styled.tpl b/app/config/locale/templates/email-auth-styled.tpl new file mode 100644 index 0000000000..07572c84da --- /dev/null +++ b/app/config/locale/templates/email-auth-styled.tpl @@ -0,0 +1,224 @@ + + + + + + + + + + +
+ {{preview}} +
{{previewWhitespace}}
+
+ +
+ + + + +
+ +
+ + + + + +
+

{{heading}}

+
+ + + + + +
+{{body}} +
+ + + + + +
+ + + + + + + +
+ + + + + +
+ + + + + + +
Terms +
|
+
Privacy
+

+ © {{year}} Appwrite | 251 Little Falls Drive, Wilmington 19808, + Delaware, United States +

+
+ + \ No newline at end of file diff --git a/app/config/locale/templates/email-mfa-challenge.tpl b/app/config/locale/templates/email-mfa-challenge.tpl index 3e55227055..fdc0f4d498 100644 --- a/app/config/locale/templates/email-mfa-challenge.tpl +++ b/app/config/locale/templates/email-mfa-challenge.tpl @@ -5,7 +5,7 @@
-

{{otp}}

+
{{otp}}
diff --git a/app/config/locale/templates/email-otp.tpl b/app/config/locale/templates/email-otp.tpl index 84802c1603..e0d84005d6 100644 --- a/app/config/locale/templates/email-otp.tpl +++ b/app/config/locale/templates/email-otp.tpl @@ -5,7 +5,7 @@
-

{{otp}}

+
{{otp}}
diff --git a/app/config/locale/templates/email-verification.tpl b/app/config/locale/templates/email-verification.tpl new file mode 100644 index 0000000000..4b68f224db --- /dev/null +++ b/app/config/locale/templates/email-verification.tpl @@ -0,0 +1,9 @@ +

{{hello}}

+

{{body}}

+

{{buttonText}}

+

{{footer}}

+

+ {{thanks}} +
+ {{signature}} +

\ No newline at end of file diff --git a/app/controllers/api/account.php b/app/controllers/api/account.php index 8aaa5283c4..355ad2ec5d 100644 --- a/app/controllers/api/account.php +++ b/app/controllers/api/account.php @@ -2295,6 +2295,10 @@ App::post('/v1/account/tokens/email') $preview = $locale->getText("emails.otpSession.preview"); $customTemplate = $project->getAttribute('templates', [])['email.otpSession-' . $locale->default] ?? []; + $customEmails = $project->getAttribute('customEmails', false); + $bodyTemplate = ''; + $heading = ''; + $detector = new Detector($request->getUserAgent('UNKNOWN')); $agentOs = $detector->getOS(); $agentClient = $detector->getClient(); @@ -2360,6 +2364,21 @@ App::post('/v1/account/tokens/email') ->setSmtpReplyTo($replyTo) ->setSmtpSenderEmail($senderEmail) ->setSmtpSenderName($senderName); + } else if ($customEmails) { + $subject = $customTemplate['subject']; + $preview = $customTemplate['preview']; + $heading = $customTemplate['heading']; + + $message = Template::fromFile(__DIR__ . '/../../config/locale/templates/email-otp.tpl'); + $message + ->setParam('{{hello}}', $customTemplate['hello']) + ->setParam('{{description}}', $customTemplate['body'], escapeHtml: false) + ->setParam('{{thanks}}', $customTemplate['thanks']) + ->setParam('{{signature}}', $customTemplate['signature']) + ->setParam('{{clientInfo}}', ''); + + $body = $message->render(); + $bodyTemplate = __DIR__ . '/../../config/locale/templates/email-auth-styled.tpl'; } $emailVariables = [ @@ -2374,12 +2393,21 @@ App::post('/v1/account/tokens/email') 'phrase' => !empty($phrase) ? $phrase : '', // TODO: remove unnecessary team variable from this email 'team' => '', + 'heading' => $heading, + 'accentColor' => APP_EMAIL_ACCENT_COLOR, + 'logoUrl' => APP_EMAIL_LOGO_URL, + 'twitterUrl' => APP_SOCIAL_TWITTER, + 'discordUrl' => APP_SOCIAL_DISCORD, + 'githubUrl' => APP_SOCIAL_GITHUB_APPWRITE, + 'termsUrl' => APP_EMAIL_TERMS_URL, + 'privacyUrl' => APP_EMAIL_PRIVACY_URL, ]; $queueForMails ->setSubject($subject) ->setPreview($preview) ->setBody($body) + ->setBodyTemplate($bodyTemplate) ->setVariables($emailVariables) ->setRecipient($email) ->trigger(); @@ -3602,6 +3630,10 @@ App::post('/v1/account/verification') $senderName = System::getEnv('_APP_SYSTEM_EMAIL_NAME', APP_NAME . ' Server'); $replyTo = ""; + $customEmails = $project->getAttribute('customEmails', false); + $bodyTemplate = ''; + $heading = ''; + if ($smtpEnabled) { if (!empty($smtp['senderEmail'])) { $senderEmail = $smtp['senderEmail']; @@ -3639,6 +3671,22 @@ App::post('/v1/account/verification') ->setSmtpReplyTo($replyTo) ->setSmtpSenderEmail($senderEmail) ->setSmtpSenderName($senderName); + } else if ($customEmails) { + $subject = $customTemplate['subject']; + $preview = $customTemplate['preview']; + $heading = $customTemplate['heading']; + + $message = Template::fromFile(__DIR__ . '/../../config/locale/templates/email-verification.tpl'); + $message + ->setParam('{{hello}}', $customTemplate['hello']) + ->setParam('{{body}}', $customTemplate['body'], escapeHtml: false) + ->setParam('{{buttonText}}', $customTemplate['buttonText']) + ->setParam('{{footer}}', $customTemplate['footer']) + ->setParam('{{thanks}}', $customTemplate['thanks']) + ->setParam('{{signature}}', $customTemplate['signature']); + + $body = $message->render(); + $bodyTemplate = __DIR__ . '/../../config/locale/templates/email-auth-styled.tpl'; } $emailVariables = [ @@ -3649,12 +3697,21 @@ App::post('/v1/account/verification') 'project' => $projectName, // TODO: remove unnecessary team variable from this email 'team' => '', + 'heading' => $heading, + 'accentColor' => APP_EMAIL_ACCENT_COLOR, + 'logoUrl' => APP_EMAIL_LOGO_URL, + 'twitterUrl' => APP_SOCIAL_TWITTER, + 'discordUrl' => APP_SOCIAL_DISCORD, + 'githubUrl' => APP_SOCIAL_GITHUB_APPWRITE, + 'termsUrl' => APP_EMAIL_TERMS_URL, + 'privacyUrl' => APP_EMAIL_PRIVACY_URL, ]; $queueForMails ->setSubject($subject) ->setPreview($preview) ->setBody($body) + ->setBodyTemplate($bodyTemplate) ->setVariables($emailVariables) ->setRecipient($user->getAttribute('email')) ->setName($user->getAttribute('name') ?? '') @@ -4684,6 +4741,10 @@ App::post('/v1/account/mfa/challenge') $senderName = System::getEnv('_APP_SYSTEM_EMAIL_NAME', APP_NAME . ' Server'); $replyTo = ""; + $customEmails = $project->getAttribute('customEmails', false); + $bodyTemplate = ''; + $heading = ''; + if ($smtpEnabled) { if (!empty($smtp['senderEmail'])) { $senderEmail = $smtp['senderEmail']; @@ -4721,6 +4782,21 @@ App::post('/v1/account/mfa/challenge') ->setSmtpReplyTo($replyTo) ->setSmtpSenderEmail($senderEmail) ->setSmtpSenderName($senderName); + } else if ($customEmails) { + $subject = $customTemplate['subject']; + $preview = $customTemplate['preview']; + $heading = $customTemplate['heading']; + + $message = Template::fromFile(__DIR__ . '/../../config/locale/templates/email-mfa-challenge.tpl'); + $message + ->setParam('{{hello}}', $customTemplate['hello']) + ->setParam('{{description}}', $customTemplate['body'], escapeHtml: false) + ->setParam('{{thanks}}', $customTemplate['thanks']) + ->setParam('{{signature}}', $customTemplate['signature']) + ->setParam('{{clientInfo}}', ''); + + $body = $message->render(); + $bodyTemplate = __DIR__ . '/../../config/locale/templates/email-auth-styled.tpl'; } $emailVariables = [ @@ -4731,13 +4807,22 @@ App::post('/v1/account/mfa/challenge') 'otp' => $code, 'agentDevice' => $agentDevice['deviceBrand'] ?? $agentDevice['deviceBrand'] ?? 'UNKNOWN', 'agentClient' => $agentClient['clientName'] ?? 'UNKNOWN', - 'agentOs' => $agentOs['osName'] ?? 'UNKNOWN' + 'agentOs' => $agentOs['osName'] ?? 'UNKNOWN', + 'heading' => $heading, + 'accentColor' => APP_EMAIL_ACCENT_COLOR, + 'logoUrl' => APP_EMAIL_LOGO_URL, + 'twitterUrl' => APP_SOCIAL_TWITTER, + 'discordUrl' => APP_SOCIAL_DISCORD, + 'githubUrl' => APP_SOCIAL_GITHUB_APPWRITE, + 'termsUrl' => APP_EMAIL_TERMS_URL, + 'privacyUrl' => APP_EMAIL_PRIVACY_URL, ]; $queueForMails ->setSubject($subject) ->setPreview($preview) ->setBody($body) + ->setBodyTemplate($bodyTemplate) ->setVariables($emailVariables) ->setRecipient($user->getAttribute('email')) ->trigger(); From f8ab95b3e15636fa25cb8f346d516d2b5b43ec89 Mon Sep 17 00:00:00 2001 From: Hemachandar Date: Tue, 16 Sep 2025 11:54:01 +0530 Subject: [PATCH 2/9] tests & lint --- app/config/console.php | 12 +----------- app/controllers/api/account.php | 6 +++--- 2 files changed, 4 insertions(+), 14 deletions(-) diff --git a/app/config/console.php b/app/config/console.php index 164db597e9..9690cb8f28 100644 --- a/app/config/console.php +++ b/app/config/console.php @@ -71,16 +71,7 @@ $console = [ 'body' => 'Enter the following code to confirm your two-step verification in {{b}}{{project}}{{/b}}. This code will expire in 15 minutes.', 'thanks' => 'Thanks,', "signature" => "{{project}} team", - ], - 'email.otpSession-en' => [ - 'subject' => 'OTP for {{project}} account login', - 'preview' => 'Use OTP {{otp}} to sign in to {{project}}. Expires in 15 minutes.', - 'heading' => 'Login with OTP to use Appwrite Cloud', - 'hello' => 'Hello {{user}},', - 'body' => 'Enter the following verification code when prompted to securely sign in to your {{b}}{{project}}{{/b}} account. This code will expire in 15 minutes.', - 'thanks' => 'Thanks,', - "signature" => "{{project}} team", - ], + ] ], 'customEmails' => true, ]; @@ -88,7 +79,6 @@ $console = [ foreach ($localeCodes as $localeCode) { $console['templates']['email.verification-'.$localeCode['code']] = $console['templates']['email.verification-en']; $console['templates']['email.mfaChallenge-'.$localeCode['code']] = $console['templates']['email.mfaChallenge-en']; - $console['templates']['email.otpSession-'.$localeCode['code']] = $console['templates']['email.otpSession-en']; } return $console; diff --git a/app/controllers/api/account.php b/app/controllers/api/account.php index 355ad2ec5d..37ebceba17 100644 --- a/app/controllers/api/account.php +++ b/app/controllers/api/account.php @@ -2364,7 +2364,7 @@ App::post('/v1/account/tokens/email') ->setSmtpReplyTo($replyTo) ->setSmtpSenderEmail($senderEmail) ->setSmtpSenderName($senderName); - } else if ($customEmails) { + } elseif ($customEmails && !empty($customTemplate)) { $subject = $customTemplate['subject']; $preview = $customTemplate['preview']; $heading = $customTemplate['heading']; @@ -3671,7 +3671,7 @@ App::post('/v1/account/verification') ->setSmtpReplyTo($replyTo) ->setSmtpSenderEmail($senderEmail) ->setSmtpSenderName($senderName); - } else if ($customEmails) { + } elseif ($customEmails && !empty($customTemplate)) { $subject = $customTemplate['subject']; $preview = $customTemplate['preview']; $heading = $customTemplate['heading']; @@ -4782,7 +4782,7 @@ App::post('/v1/account/mfa/challenge') ->setSmtpReplyTo($replyTo) ->setSmtpSenderEmail($senderEmail) ->setSmtpSenderName($senderName); - } else if ($customEmails) { + } elseif ($customEmails && !empty($customTemplate)) { $subject = $customTemplate['subject']; $preview = $customTemplate['preview']; $heading = $customTemplate['heading']; From e6be9bac0774e38c4ddd9ef5660aa4e4932a0370 Mon Sep 17 00:00:00 2001 From: Hemachandar Date: Tue, 16 Sep 2025 11:55:43 +0530 Subject: [PATCH 3/9] cleanup --- app/config/locale/templates/email-auth-styled.tpl | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/config/locale/templates/email-auth-styled.tpl b/app/config/locale/templates/email-auth-styled.tpl index 07572c84da..544e58b7d8 100644 --- a/app/config/locale/templates/email-auth-styled.tpl +++ b/app/config/locale/templates/email-auth-styled.tpl @@ -38,7 +38,7 @@ background-color: #ffffff; margin: 0; padding: 0; - } 1Code has comments. Press enter to view. + } a { color: currentColor; word-break: break-all; From 2021396f39660bc915c7b873771e20c7c28cfa05 Mon Sep 17 00:00:00 2001 From: Hemachandar Date: Thu, 18 Sep 2025 12:33:42 +0530 Subject: [PATCH 4/9] email otp branded --- app/config/console.php | 16 ++++-- app/config/locale/templates/email-otp.tpl | 4 +- app/controllers/api/account.php | 59 +++++++++++++---------- 3 files changed, 49 insertions(+), 30 deletions(-) diff --git a/app/config/console.php b/app/config/console.php index 9690cb8f28..ae6c15fd64 100644 --- a/app/config/console.php +++ b/app/config/console.php @@ -71,14 +71,24 @@ $console = [ 'body' => 'Enter the following code to confirm your two-step verification in {{b}}{{project}}{{/b}}. This code will expire in 15 minutes.', 'thanks' => 'Thanks,', "signature" => "{{project}} team", - ] + ], + 'email.otpSession-en' => [ + 'subject' => 'OTP for {{project}} account login', + 'preview' => 'Use OTP {{otp}} to sign in to {{project}}. Expires in 15 minutes.', + 'heading' => 'Login with OTP to use Appwrite Cloud', + 'hello' => 'Hello {{user}},', + 'body' => 'Enter the following verification code when prompted to securely sign in to your {{b}}{{project}}{{/b}} account. This code will expire in 15 minutes.', + 'thanks' => 'Thanks,', + "signature" => "{{project}} team", + ], ], 'customEmails' => true, ]; foreach ($localeCodes as $localeCode) { - $console['templates']['email.verification-'.$localeCode['code']] = $console['templates']['email.verification-en']; - $console['templates']['email.mfaChallenge-'.$localeCode['code']] = $console['templates']['email.mfaChallenge-en']; + $console['templates']['email.verification-' . $localeCode['code']] = $console['templates']['email.verification-en']; + $console['templates']['email.mfaChallenge-' . $localeCode['code']] = $console['templates']['email.mfaChallenge-en']; + $console['templates']['email.otpSession-' . $localeCode['code']] = $console['templates']['email.otpSession-en']; } return $console; diff --git a/app/config/locale/templates/email-otp.tpl b/app/config/locale/templates/email-otp.tpl index e0d84005d6..aebc512e1b 100644 --- a/app/config/locale/templates/email-otp.tpl +++ b/app/config/locale/templates/email-otp.tpl @@ -5,7 +5,7 @@
-
{{otp}}
+
{{otp}}
@@ -15,6 +15,6 @@

{{thanks}}

{{signature}}

-
+

{{securityPhrase}}

\ No newline at end of file diff --git a/app/controllers/api/account.php b/app/controllers/api/account.php index 37ebceba17..bde3672774 100644 --- a/app/controllers/api/account.php +++ b/app/controllers/api/account.php @@ -165,7 +165,8 @@ function sendSessionAlert(Locale $locale, Document $user, Document $project, Doc ->setVariables($emailVariables) ->setRecipient($email) ->trigger(); -}; +} +; $createSession = function (string $userId, string $secret, Request $request, Response $response, Document $user, Database $dbForProject, Document $project, Locale $locale, Reader $geodb, Event $queueForEvents, Mail $queueForMails) { @@ -838,7 +839,7 @@ App::patch('/v1/account/sessions/:sessionId') $session ->setAttribute('providerAccessToken', $oauth2->getAccessToken('')) ->setAttribute('providerRefreshToken', $oauth2->getRefreshToken('')) - ->setAttribute('providerAccessTokenExpiry', DateTime::addSeconds(new \DateTime(), (int)$oauth2->getAccessTokenExpiry(''))); + ->setAttribute('providerAccessTokenExpiry', DateTime::addSeconds(new \DateTime(), (int) $oauth2->getAccessTokenExpiry(''))); } // Save changes @@ -982,9 +983,11 @@ App::post('/v1/account/sessions/email') ; if ($project->getAttribute('auths', [])['sessionAlerts'] ?? false) { - if ($dbForProject->count('sessions', [ - Query::equal('userId', [$user->getId()]), - ]) !== 1) { + if ( + $dbForProject->count('sessions', [ + Query::equal('userId', [$user->getId()]), + ]) !== 1 + ) { sendSessionAlert($locale, $user, $project, $session, $queueForMails); } } @@ -1098,7 +1101,7 @@ App::post('/v1/account/sessions/anonymous') Authorization::setRole(Role::user($user->getId())->toString()); - $session = $dbForProject->createDocument('sessions', $session-> setAttribute('$permissions', [ + $session = $dbForProject->createDocument('sessions', $session->setAttribute('$permissions', [ Permission::read(Role::user($user->getId())), Permission::update(Role::user($user->getId())), Permission::delete(Role::user($user->getId())), @@ -1659,13 +1662,13 @@ App::get('/v1/account/sessions/oauth2/:provider/redirect') 'providerEmail' => $email, 'providerAccessToken' => $accessToken, 'providerRefreshToken' => $refreshToken, - 'providerAccessTokenExpiry' => DateTime::addSeconds(new \DateTime(), (int)$accessTokenExpiry), + 'providerAccessTokenExpiry' => DateTime::addSeconds(new \DateTime(), (int) $accessTokenExpiry), ])); } else { $identity ->setAttribute('providerAccessToken', $accessToken) ->setAttribute('providerRefreshToken', $refreshToken) - ->setAttribute('providerAccessTokenExpiry', DateTime::addSeconds(new \DateTime(), (int)$accessTokenExpiry)); + ->setAttribute('providerAccessTokenExpiry', DateTime::addSeconds(new \DateTime(), (int) $accessTokenExpiry)); $dbForProject->updateDocument('identities', $identity->getId(), $identity); } @@ -1735,7 +1738,7 @@ App::get('/v1/account/sessions/oauth2/:provider/redirect') 'providerUid' => $oauth2ID, 'providerAccessToken' => $accessToken, 'providerRefreshToken' => $refreshToken, - 'providerAccessTokenExpiry' => DateTime::addSeconds(new \DateTime(), (int)$accessTokenExpiry), + 'providerAccessTokenExpiry' => DateTime::addSeconds(new \DateTime(), (int) $accessTokenExpiry), 'secret' => Auth::hash($secret), // One way hash encryption to protect DB leak 'userAgent' => $request->getUserAgent('UNKNOWN'), 'ip' => $request->getIP(), @@ -2314,8 +2317,10 @@ App::post('/v1/account/tokens/email') if (!empty($phrase)) { $message->setParam('{{securityPhrase}}', $locale->getText("emails.otpSession.securityPhrase")); + $message->setParam('{{securityPhraseDividerDisplay}}', 'block'); } else { $message->setParam('{{securityPhrase}}', ''); + $message->setParam('{{securityPhraseDividerDisplay}}', 'none'); } $body = $message->render(); @@ -2375,7 +2380,9 @@ App::post('/v1/account/tokens/email') ->setParam('{{description}}', $customTemplate['body'], escapeHtml: false) ->setParam('{{thanks}}', $customTemplate['thanks']) ->setParam('{{signature}}', $customTemplate['signature']) - ->setParam('{{clientInfo}}', ''); + ->setParam('{{clientInfo}}', '') + ->setParam('{{securityPhrase}}', '') + ->setParam('{{securityPhraseDividerDisplay}}', 'none'); $body = $message->render(); $bodyTemplate = __DIR__ . '/../../config/locale/templates/email-auth-styled.tpl'; @@ -2760,10 +2767,12 @@ App::post('/v1/account/jwts') $response ->setStatusCode(Response::STATUS_CODE_CREATED) - ->dynamic(new Document(['jwt' => $jwt->encode([ - 'userId' => $user->getId(), - 'sessionId' => $current->getId(), - ])]), Response::MODEL_JWT); + ->dynamic(new Document([ + 'jwt' => $jwt->encode([ + 'userId' => $user->getId(), + 'sessionId' => $current->getId(), + ]) + ]), Response::MODEL_JWT); }); App::get('/v1/account/prefs') @@ -3508,12 +3517,12 @@ App::put('/v1/account/recovery') $hooks->trigger('passwordValidator', [$dbForProject, $project, $password, &$user, true]); $profile = $dbForProject->updateDocument('users', $profile->getId(), $profile - ->setAttribute('password', $newPassword) - ->setAttribute('passwordHistory', $history) - ->setAttribute('passwordUpdate', DateTime::now()) - ->setAttribute('hash', Auth::DEFAULT_ALGO) - ->setAttribute('hashOptions', Auth::DEFAULT_ALGO_OPTIONS) - ->setAttribute('emailVerification', true)); + ->setAttribute('password', $newPassword) + ->setAttribute('passwordHistory', $history) + ->setAttribute('passwordUpdate', DateTime::now()) + ->setAttribute('hash', Auth::DEFAULT_ALGO) + ->setAttribute('hashOptions', Auth::DEFAULT_ALGO_OPTIONS) + ->setAttribute('emailVerification', true)); $user->setAttributes($profile->getArrayCopy()); @@ -4945,8 +4954,8 @@ App::put('/v1/account/mfa/challenge') $dbForProject->updateDocument('sessions', $session->getId(), $session); $queueForEvents - ->setParam('userId', $user->getId()) - ->setParam('sessionId', $session->getId()); + ->setParam('userId', $user->getId()) + ->setParam('sessionId', $session->getId()); $response->dynamic($session, Response::MODEL_SESSION); }); @@ -5009,7 +5018,7 @@ App::post('/v1/account/targets/push') ], 'providerId' => !empty($providerId) ? $providerId : null, 'providerInternalId' => !empty($providerId) ? $provider->getSequence() : null, - 'providerType' => MESSAGE_TYPE_PUSH, + 'providerType' => MESSAGE_TYPE_PUSH, 'userId' => $user->getId(), 'userInternalId' => $user->getSequence(), 'sessionId' => $session->getId(), @@ -5184,8 +5193,8 @@ App::get('/v1/account/identities') $queries[] = Query::equal('userInternalId', [$user->getSequence()]); /** - * Get cursor document if there was a cursor query, we use array_filter and reset for reference $cursor to $queries - */ + * Get cursor document if there was a cursor query, we use array_filter and reset for reference $cursor to $queries + */ $cursor = \array_filter($queries, function ($query) { return \in_array($query->getMethod(), [Query::TYPE_CURSOR_AFTER, Query::TYPE_CURSOR_BEFORE]); }); From 0812ac97a997012dfa3eaa817bd2f04027ade901 Mon Sep 17 00:00:00 2001 From: Hemachandar Date: Thu, 18 Sep 2025 14:09:42 +0530 Subject: [PATCH 5/9] tests --- app/config/console.php | 2 +- app/config/locale/templates/email-auth-styled.tpl | 1 + tests/e2e/Services/Account/AccountBase.php | 12 ++++++++++++ 3 files changed, 14 insertions(+), 1 deletion(-) diff --git a/app/config/console.php b/app/config/console.php index ae6c15fd64..6f44368060 100644 --- a/app/config/console.php +++ b/app/config/console.php @@ -73,7 +73,7 @@ $console = [ "signature" => "{{project}} team", ], 'email.otpSession-en' => [ - 'subject' => 'OTP for {{project}} account login', + 'subject' => 'OTP for {{project}} Login', 'preview' => 'Use OTP {{otp}} to sign in to {{project}}. Expires in 15 minutes.', 'heading' => 'Login with OTP to use Appwrite Cloud', 'hello' => 'Hello {{user}},', diff --git a/app/config/locale/templates/email-auth-styled.tpl b/app/config/locale/templates/email-auth-styled.tpl index 544e58b7d8..a826c62e95 100644 --- a/app/config/locale/templates/email-auth-styled.tpl +++ b/app/config/locale/templates/email-auth-styled.tpl @@ -144,6 +144,7 @@ Appwrite logo diff --git a/tests/e2e/Services/Account/AccountBase.php b/tests/e2e/Services/Account/AccountBase.php index 8813e2784f..6cf997e22c 100644 --- a/tests/e2e/Services/Account/AccountBase.php +++ b/tests/e2e/Services/Account/AccountBase.php @@ -152,6 +152,8 @@ trait AccountBase public function testEmailOTPSession(): void { + $isConsoleProject = $this->getProject()['$id'] === 'console'; + $response = $this->client->call(Client::METHOD_POST, '/account/tokens/email', array_merge([ 'origin' => 'http://localhost', 'content-type' => 'application/json', @@ -183,6 +185,16 @@ trait AccountBase $this->assertNotEmpty($code); $this->assertStringContainsStringIgnoringCase('Use OTP ' . $code . ' to sign in to '. $this->getProject()['name'] . '. Expires in 15 minutes.', $lastEmail['text']); + // Only Console project has branded logo in email. + if ($isConsoleProject) { + $this->assertStringContainsStringIgnoringCase('Appwrite logo', $lastEmail['html']); + } + + // TODO: Remove this once OTP login is supported for Console. + if ($isConsoleProject) { + return; + } + $response = $this->client->call(Client::METHOD_POST, '/account/sessions/token', array_merge([ 'origin' => 'http://localhost', 'content-type' => 'application/json', From 752368327f63694945de30f2661e1c041f24cb03 Mon Sep 17 00:00:00 2001 From: Hemachandar Date: Wed, 24 Sep 2025 13:06:20 +0530 Subject: [PATCH 6/9] feedback --- .../locale/templates/email-mfa-challenge.tpl | 2 +- app/config/locale/templates/email-otp.tpl | 2 +- app/controllers/api/account.php | 63 ++++++++++++------- tests/e2e/Services/Account/AccountBase.php | 2 + 4 files changed, 43 insertions(+), 26 deletions(-) diff --git a/app/config/locale/templates/email-mfa-challenge.tpl b/app/config/locale/templates/email-mfa-challenge.tpl index fdc0f4d498..a828e3d299 100644 --- a/app/config/locale/templates/email-mfa-challenge.tpl +++ b/app/config/locale/templates/email-mfa-challenge.tpl @@ -5,7 +5,7 @@
-
{{otp}}
+

{{otp}}

diff --git a/app/config/locale/templates/email-otp.tpl b/app/config/locale/templates/email-otp.tpl index aebc512e1b..e18a4ce725 100644 --- a/app/config/locale/templates/email-otp.tpl +++ b/app/config/locale/templates/email-otp.tpl @@ -5,7 +5,7 @@
-
{{otp}}
+

{{otp}}

diff --git a/app/controllers/api/account.php b/app/controllers/api/account.php index bde3672774..3aa9678a72 100644 --- a/app/controllers/api/account.php +++ b/app/controllers/api/account.php @@ -2400,16 +2400,21 @@ App::post('/v1/account/tokens/email') 'phrase' => !empty($phrase) ? $phrase : '', // TODO: remove unnecessary team variable from this email 'team' => '', - 'heading' => $heading, - 'accentColor' => APP_EMAIL_ACCENT_COLOR, - 'logoUrl' => APP_EMAIL_LOGO_URL, - 'twitterUrl' => APP_SOCIAL_TWITTER, - 'discordUrl' => APP_SOCIAL_DISCORD, - 'githubUrl' => APP_SOCIAL_GITHUB_APPWRITE, - 'termsUrl' => APP_EMAIL_TERMS_URL, - 'privacyUrl' => APP_EMAIL_PRIVACY_URL, ]; + if ($customEmails && !empty($customTemplate)) { + $emailVariables = array_merge($emailVariables, [ + 'heading' => $heading, + 'accentColor' => APP_EMAIL_ACCENT_COLOR, + 'logoUrl' => APP_EMAIL_LOGO_URL, + 'twitterUrl' => APP_SOCIAL_TWITTER, + 'discordUrl' => APP_SOCIAL_DISCORD, + 'githubUrl' => APP_SOCIAL_GITHUB_APPWRITE, + 'termsUrl' => APP_EMAIL_TERMS_URL, + 'privacyUrl' => APP_EMAIL_PRIVACY_URL, + ]); + } + $queueForMails ->setSubject($subject) ->setPreview($preview) @@ -3706,16 +3711,21 @@ App::post('/v1/account/verification') 'project' => $projectName, // TODO: remove unnecessary team variable from this email 'team' => '', - 'heading' => $heading, - 'accentColor' => APP_EMAIL_ACCENT_COLOR, - 'logoUrl' => APP_EMAIL_LOGO_URL, - 'twitterUrl' => APP_SOCIAL_TWITTER, - 'discordUrl' => APP_SOCIAL_DISCORD, - 'githubUrl' => APP_SOCIAL_GITHUB_APPWRITE, - 'termsUrl' => APP_EMAIL_TERMS_URL, - 'privacyUrl' => APP_EMAIL_PRIVACY_URL, ]; + if ($customEmails && !empty($customTemplate)) { + $emailVariables = array_merge($emailVariables, [ + 'heading' => $heading, + 'accentColor' => APP_EMAIL_ACCENT_COLOR, + 'logoUrl' => APP_EMAIL_LOGO_URL, + 'twitterUrl' => APP_SOCIAL_TWITTER, + 'discordUrl' => APP_SOCIAL_DISCORD, + 'githubUrl' => APP_SOCIAL_GITHUB_APPWRITE, + 'termsUrl' => APP_EMAIL_TERMS_URL, + 'privacyUrl' => APP_EMAIL_PRIVACY_URL, + ]); + } + $queueForMails ->setSubject($subject) ->setPreview($preview) @@ -4817,16 +4827,21 @@ App::post('/v1/account/mfa/challenge') 'agentDevice' => $agentDevice['deviceBrand'] ?? $agentDevice['deviceBrand'] ?? 'UNKNOWN', 'agentClient' => $agentClient['clientName'] ?? 'UNKNOWN', 'agentOs' => $agentOs['osName'] ?? 'UNKNOWN', - 'heading' => $heading, - 'accentColor' => APP_EMAIL_ACCENT_COLOR, - 'logoUrl' => APP_EMAIL_LOGO_URL, - 'twitterUrl' => APP_SOCIAL_TWITTER, - 'discordUrl' => APP_SOCIAL_DISCORD, - 'githubUrl' => APP_SOCIAL_GITHUB_APPWRITE, - 'termsUrl' => APP_EMAIL_TERMS_URL, - 'privacyUrl' => APP_EMAIL_PRIVACY_URL, ]; + if ($customEmails && !empty($customTemplate)) { + $emailVariables = array_merge($emailVariables, [ + 'heading' => $heading, + 'accentColor' => APP_EMAIL_ACCENT_COLOR, + 'logoUrl' => APP_EMAIL_LOGO_URL, + 'twitterUrl' => APP_SOCIAL_TWITTER, + 'discordUrl' => APP_SOCIAL_DISCORD, + 'githubUrl' => APP_SOCIAL_GITHUB_APPWRITE, + 'termsUrl' => APP_EMAIL_TERMS_URL, + 'privacyUrl' => APP_EMAIL_PRIVACY_URL, + ]); + } + $queueForMails ->setSubject($subject) ->setPreview($preview) diff --git a/tests/e2e/Services/Account/AccountBase.php b/tests/e2e/Services/Account/AccountBase.php index 6cf997e22c..46283e8f86 100644 --- a/tests/e2e/Services/Account/AccountBase.php +++ b/tests/e2e/Services/Account/AccountBase.php @@ -188,6 +188,8 @@ trait AccountBase // Only Console project has branded logo in email. if ($isConsoleProject) { $this->assertStringContainsStringIgnoringCase('Appwrite logo', $lastEmail['html']); + } else { + $this->assertStringNotContainsStringIgnoringCase('Appwrite logo', $lastEmail['html']); } // TODO: Remove this once OTP login is supported for Console. From 29dbe99840f0076562ba7fb969d368bbf3329cd4 Mon Sep 17 00:00:00 2001 From: Hemachandar Date: Mon, 6 Oct 2025 19:58:01 +0530 Subject: [PATCH 7/9] simplify config & templates --- app/config/console.php | 41 +--- .../locale/templates/email-auth-styled.tpl | 225 ------------------ .../locale/templates/email-base-styled.tpl | 5 +- app/config/locale/templates/email-base.tpl | 15 ++ .../locale/templates/email-inner-base.tpl | 2 +- .../locale/templates/email-magic-url.tpl | 2 +- .../locale/templates/email-verification.tpl | 9 - app/config/locale/translations/en.json | 3 + app/controllers/api/account.php | 78 ++---- src/Appwrite/Platform/Workers/Mails.php | 1 + tests/e2e/Services/Account/AccountBase.php | 5 - 11 files changed, 40 insertions(+), 346 deletions(-) delete mode 100644 app/config/locale/templates/email-auth-styled.tpl delete mode 100644 app/config/locale/templates/email-verification.tpl diff --git a/app/config/console.php b/app/config/console.php index 6f44368060..70bb3523e6 100644 --- a/app/config/console.php +++ b/app/config/console.php @@ -9,8 +9,6 @@ use Appwrite\Network\Platform; use Utopia\Database\Helpers\ID; use Utopia\System\System; -$localeCodes = include __DIR__ . '/locale/codes.php'; - $console = [ '$id' => ID::custom('console'), '$sequence' => ID::custom('console'), @@ -51,44 +49,7 @@ $console = [ 'githubSecret' => System::getEnv('_APP_CONSOLE_GITHUB_SECRET', ''), 'githubAppid' => System::getEnv('_APP_CONSOLE_GITHUB_APP_ID', '') ], - 'templates' => [ - 'email.verification-en' => [ - 'subject' => 'Account Verification', - 'preview' => 'Verify your email to activate your {{project}} account.', - 'heading' => 'Verify your email to start using Appwrite Cloud', - 'hello' => 'Hello {{user}},', - 'body' => 'Thanks for signing up for Appwrite Cloud. Before you can get started, please verify your email address.', - 'footer' => 'If you didn’t create an account, you can ignore this email.', - 'buttonText' => 'Verify email', - 'thanks' => 'Thanks,', - "signature" => "{{project}} team", - ], - 'email.mfaChallenge-en' => [ - 'subject' => 'Verification Code for {{project}}', - 'preview' => 'Use code {{otp}} for two-step verification in {{project}}. Expires in 15 minutes.', - 'heading' => 'Complete two-step verification to use Appwrite Cloud', - 'hello' => 'Hello {{user}},', - 'body' => 'Enter the following code to confirm your two-step verification in {{b}}{{project}}{{/b}}. This code will expire in 15 minutes.', - 'thanks' => 'Thanks,', - "signature" => "{{project}} team", - ], - 'email.otpSession-en' => [ - 'subject' => 'OTP for {{project}} Login', - 'preview' => 'Use OTP {{otp}} to sign in to {{project}}. Expires in 15 minutes.', - 'heading' => 'Login with OTP to use Appwrite Cloud', - 'hello' => 'Hello {{user}},', - 'body' => 'Enter the following verification code when prompted to securely sign in to your {{b}}{{project}}{{/b}} account. This code will expire in 15 minutes.', - 'thanks' => 'Thanks,', - "signature" => "{{project}} team", - ], - ], - 'customEmails' => true, + 'smtpBaseTemplate' => 'email-base-styled', ]; -foreach ($localeCodes as $localeCode) { - $console['templates']['email.verification-' . $localeCode['code']] = $console['templates']['email.verification-en']; - $console['templates']['email.mfaChallenge-' . $localeCode['code']] = $console['templates']['email.mfaChallenge-en']; - $console['templates']['email.otpSession-' . $localeCode['code']] = $console['templates']['email.otpSession-en']; -} - return $console; diff --git a/app/config/locale/templates/email-auth-styled.tpl b/app/config/locale/templates/email-auth-styled.tpl deleted file mode 100644 index a826c62e95..0000000000 --- a/app/config/locale/templates/email-auth-styled.tpl +++ /dev/null @@ -1,225 +0,0 @@ - - - - - - - - - - -
- {{preview}} -
{{previewWhitespace}}
-
- -
- - - - -
- Appwrite logo -
- - - - - -
-

{{heading}}

-
- - - - - -
-{{body}} -
- - - - - -
- - - - - - - -
- - - - - -
- - - - - - -
Terms -
|
-
Privacy
-

- © {{year}} Appwrite | 251 Little Falls Drive, Wilmington 19808, - Delaware, United States -

-
- - \ No newline at end of file diff --git a/app/config/locale/templates/email-base-styled.tpl b/app/config/locale/templates/email-base-styled.tpl index 16036e792c..3f3ba8dd9c 100644 --- a/app/config/locale/templates/email-base-styled.tpl +++ b/app/config/locale/templates/email-base-styled.tpl @@ -147,6 +147,7 @@ Appwrite logo @@ -155,12 +156,12 @@
-

{{subject}}

+

{{heading}}

- +
{{body}} diff --git a/app/config/locale/templates/email-base.tpl b/app/config/locale/templates/email-base.tpl index 8c94c3f63e..5153b3e4fc 100644 --- a/app/config/locale/templates/email-base.tpl +++ b/app/config/locale/templates/email-base.tpl @@ -44,6 +44,21 @@ color: currentColor; word-break: break-all; } + a.button { + box-sizing: border-box; + display: inline-block; + text-align: center; + text-decoration: none; + padding: 9px 14px; + color: #ffffff; + background-color: #2D2D31; + border: 1px solid #414146; + border-radius: 8px; + } + a.button:hover, + a.button:focus { + opacity: 0.8; + } table { width: 100%; border-spacing: 0 !important; diff --git a/app/config/locale/templates/email-inner-base.tpl b/app/config/locale/templates/email-inner-base.tpl index 677f70ce7d..4b68f224db 100644 --- a/app/config/locale/templates/email-inner-base.tpl +++ b/app/config/locale/templates/email-inner-base.tpl @@ -1,6 +1,6 @@

{{hello}}

{{body}}

-

{{buttonText}}

+

{{buttonText}}

{{footer}}

{{thanks}} diff --git a/app/config/locale/templates/email-magic-url.tpl b/app/config/locale/templates/email-magic-url.tpl index 21988c5bc1..618993e0e9 100644 --- a/app/config/locale/templates/email-magic-url.tpl +++ b/app/config/locale/templates/email-magic-url.tpl @@ -5,7 +5,7 @@
- {{buttonText}} + {{buttonText}}
diff --git a/app/config/locale/templates/email-verification.tpl b/app/config/locale/templates/email-verification.tpl deleted file mode 100644 index 4b68f224db..0000000000 --- a/app/config/locale/templates/email-verification.tpl +++ /dev/null @@ -1,9 +0,0 @@ -

{{hello}}

-

{{body}}

-

{{buttonText}}

-

{{footer}}

-

- {{thanks}} -
- {{signature}} -

\ No newline at end of file diff --git a/app/config/locale/translations/en.json b/app/config/locale/translations/en.json index e2ee20b2d7..69328e61a7 100644 --- a/app/config/locale/translations/en.json +++ b/app/config/locale/translations/en.json @@ -5,6 +5,7 @@ "emails.sender": "{{project}} Team", "emails.verification.subject": "Account Verification", "emails.verification.preview": "Verify your email to activate your {{project}} account.", + "emails.verification.heading": "Verify your email to start using Appwrite Cloud", "emails.verification.hello": "Hello {{user}},", "emails.verification.body": "Follow this link to verify your email address to your {{b}}{{project}}{{/b}} account.", "emails.verification.footer": "If you didn’t ask to verify this address, you can ignore this message.", @@ -33,6 +34,7 @@ "emails.sessionAlert.signature": "{{project}} team", "emails.otpSession.subject": "OTP for {{project}} Login", "emails.otpSession.preview": "Use OTP {{otp}} to sign in to {{project}}. Expires in 15 minutes.", + "emails.otpSession.heading": "Login with OTP to use Appwrite Cloud", "emails.otpSession.hello": "Hello {{user}},", "emails.otpSession.description": "Enter the following verification code when prompted to securely sign in to your {{b}}{{project}}{{/b}} account. This code will expire in 15 minutes.", "emails.otpSession.clientInfo": "This sign in was requested using {{b}}{{agentClient}}{{/b}} on {{b}}{{agentDevice}}{{/b}} {{b}}{{agentOs}}{{/b}}. If you didn't request the sign in, you can safely ignore this email.", @@ -41,6 +43,7 @@ "emails.otpSession.signature": "{{project}} team", "emails.mfaChallenge.subject": "Verification Code for {{project}}", "emails.mfaChallenge.preview": "Use code {{otp}} for two-step verification in {{project}}. Expires in 15 minutes.", + "emails.mfaChallenge.heading": "Complete two-step verification to use Appwrite Cloud", "emails.mfaChallenge.hello": "Hello {{user}},", "emails.mfaChallenge.description": "Enter the following code to confirm your two-step verification in {{b}}{{project}}{{/b}}. This code will expire in 15 minutes.", "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.", diff --git a/app/controllers/api/account.php b/app/controllers/api/account.php index 3aa9678a72..e98376dfc7 100644 --- a/app/controllers/api/account.php +++ b/app/controllers/api/account.php @@ -2296,11 +2296,11 @@ App::post('/v1/account/tokens/email') $subject = $locale->getText("emails.otpSession.subject"); $preview = $locale->getText("emails.otpSession.preview"); - $customTemplate = $project->getAttribute('templates', [])['email.otpSession-' . $locale->default] ?? []; + $heading = $locale->getText("emails.otpSession.heading"); - $customEmails = $project->getAttribute('customEmails', false); - $bodyTemplate = ''; - $heading = ''; + $customTemplate = $project->getAttribute('templates', [])['email.otpSession-' . $locale->default] ?? []; + $smtpBaseTemplate = $project->getAttribute('smtpBaseTemplate', 'email-base'); + $bodyTemplate = __DIR__ . '/../../config/locale/templates/' . $smtpBaseTemplate . '.tpl'; $detector = new Detector($request->getUserAgent('UNKNOWN')); $agentOs = $detector->getOS(); @@ -2369,23 +2369,6 @@ App::post('/v1/account/tokens/email') ->setSmtpReplyTo($replyTo) ->setSmtpSenderEmail($senderEmail) ->setSmtpSenderName($senderName); - } elseif ($customEmails && !empty($customTemplate)) { - $subject = $customTemplate['subject']; - $preview = $customTemplate['preview']; - $heading = $customTemplate['heading']; - - $message = Template::fromFile(__DIR__ . '/../../config/locale/templates/email-otp.tpl'); - $message - ->setParam('{{hello}}', $customTemplate['hello']) - ->setParam('{{description}}', $customTemplate['body'], escapeHtml: false) - ->setParam('{{thanks}}', $customTemplate['thanks']) - ->setParam('{{signature}}', $customTemplate['signature']) - ->setParam('{{clientInfo}}', '') - ->setParam('{{securityPhrase}}', '') - ->setParam('{{securityPhraseDividerDisplay}}', 'none'); - - $body = $message->render(); - $bodyTemplate = __DIR__ . '/../../config/locale/templates/email-auth-styled.tpl'; } $emailVariables = [ @@ -2402,7 +2385,7 @@ App::post('/v1/account/tokens/email') 'team' => '', ]; - if ($customEmails && !empty($customTemplate)) { + if ($smtpBaseTemplate === 'email-base-styled') { $emailVariables = array_merge($emailVariables, [ 'heading' => $heading, 'accentColor' => APP_EMAIL_ACCENT_COLOR, @@ -3624,7 +3607,11 @@ App::post('/v1/account/verification') $body = $locale->getText("emails.verification.body"); $preview = $locale->getText("emails.verification.preview"); $subject = $locale->getText("emails.verification.subject"); + $heading = $locale->getText("emails.verification.heading"); + $customTemplate = $project->getAttribute('templates', [])['email.verification-' . $locale->default] ?? []; + $smtpBaseTemplate = $project->getAttribute('smtpBaseTemplate', 'email-base'); + $bodyTemplate = __DIR__ . '/../../config/locale/templates/' . $smtpBaseTemplate . '.tpl'; $message = Template::fromFile(__DIR__ . '/../../config/locale/templates/email-inner-base.tpl'); $message @@ -3644,10 +3631,6 @@ App::post('/v1/account/verification') $senderName = System::getEnv('_APP_SYSTEM_EMAIL_NAME', APP_NAME . ' Server'); $replyTo = ""; - $customEmails = $project->getAttribute('customEmails', false); - $bodyTemplate = ''; - $heading = ''; - if ($smtpEnabled) { if (!empty($smtp['senderEmail'])) { $senderEmail = $smtp['senderEmail']; @@ -3685,22 +3668,6 @@ App::post('/v1/account/verification') ->setSmtpReplyTo($replyTo) ->setSmtpSenderEmail($senderEmail) ->setSmtpSenderName($senderName); - } elseif ($customEmails && !empty($customTemplate)) { - $subject = $customTemplate['subject']; - $preview = $customTemplate['preview']; - $heading = $customTemplate['heading']; - - $message = Template::fromFile(__DIR__ . '/../../config/locale/templates/email-verification.tpl'); - $message - ->setParam('{{hello}}', $customTemplate['hello']) - ->setParam('{{body}}', $customTemplate['body'], escapeHtml: false) - ->setParam('{{buttonText}}', $customTemplate['buttonText']) - ->setParam('{{footer}}', $customTemplate['footer']) - ->setParam('{{thanks}}', $customTemplate['thanks']) - ->setParam('{{signature}}', $customTemplate['signature']); - - $body = $message->render(); - $bodyTemplate = __DIR__ . '/../../config/locale/templates/email-auth-styled.tpl'; } $emailVariables = [ @@ -3713,7 +3680,7 @@ App::post('/v1/account/verification') 'team' => '', ]; - if ($customEmails && !empty($customTemplate)) { + if ($smtpBaseTemplate === 'email-base-styled') { $emailVariables = array_merge($emailVariables, [ 'heading' => $heading, 'accentColor' => APP_EMAIL_ACCENT_COLOR, @@ -4736,7 +4703,11 @@ App::post('/v1/account/mfa/challenge') $subject = $locale->getText("emails.mfaChallenge.subject"); $preview = $locale->getText("emails.mfaChallenge.preview"); + $heading = $locale->getText("emails.mfaChallenge.heading"); + $customTemplate = $project->getAttribute('templates', [])['email.mfaChallenge-' . $locale->default] ?? []; + $smtpBaseTemplate = $project->getAttribute('smtpBaseTemplate', 'email-base'); + $bodyTemplate = __DIR__ . '/../../config/locale/templates/' . $smtpBaseTemplate . '.tpl'; $detector = new Detector($request->getUserAgent('UNKNOWN')); $agentOs = $detector->getOS(); @@ -4760,10 +4731,6 @@ App::post('/v1/account/mfa/challenge') $senderName = System::getEnv('_APP_SYSTEM_EMAIL_NAME', APP_NAME . ' Server'); $replyTo = ""; - $customEmails = $project->getAttribute('customEmails', false); - $bodyTemplate = ''; - $heading = ''; - if ($smtpEnabled) { if (!empty($smtp['senderEmail'])) { $senderEmail = $smtp['senderEmail']; @@ -4801,21 +4768,6 @@ App::post('/v1/account/mfa/challenge') ->setSmtpReplyTo($replyTo) ->setSmtpSenderEmail($senderEmail) ->setSmtpSenderName($senderName); - } elseif ($customEmails && !empty($customTemplate)) { - $subject = $customTemplate['subject']; - $preview = $customTemplate['preview']; - $heading = $customTemplate['heading']; - - $message = Template::fromFile(__DIR__ . '/../../config/locale/templates/email-mfa-challenge.tpl'); - $message - ->setParam('{{hello}}', $customTemplate['hello']) - ->setParam('{{description}}', $customTemplate['body'], escapeHtml: false) - ->setParam('{{thanks}}', $customTemplate['thanks']) - ->setParam('{{signature}}', $customTemplate['signature']) - ->setParam('{{clientInfo}}', ''); - - $body = $message->render(); - $bodyTemplate = __DIR__ . '/../../config/locale/templates/email-auth-styled.tpl'; } $emailVariables = [ @@ -4829,7 +4781,7 @@ App::post('/v1/account/mfa/challenge') 'agentOs' => $agentOs['osName'] ?? 'UNKNOWN', ]; - if ($customEmails && !empty($customTemplate)) { + if ($smtpBaseTemplate === 'email-base-styled') { $emailVariables = array_merge($emailVariables, [ 'heading' => $heading, 'accentColor' => APP_EMAIL_ACCENT_COLOR, diff --git a/src/Appwrite/Platform/Workers/Mails.php b/src/Appwrite/Platform/Workers/Mails.php index 117b689863..efca484ebf 100644 --- a/src/Appwrite/Platform/Workers/Mails.php +++ b/src/Appwrite/Platform/Workers/Mails.php @@ -82,6 +82,7 @@ class Mails extends Action $preview = $payload['preview'] ?? ''; $variables['subject'] = $subject; + $variables['heading'] = $variables['heading'] ?? $subject; $variables['year'] = date("Y"); $attachment = $payload['attachment'] ?? []; diff --git a/tests/e2e/Services/Account/AccountBase.php b/tests/e2e/Services/Account/AccountBase.php index 46283e8f86..b2f85637a8 100644 --- a/tests/e2e/Services/Account/AccountBase.php +++ b/tests/e2e/Services/Account/AccountBase.php @@ -192,11 +192,6 @@ trait AccountBase $this->assertStringNotContainsStringIgnoringCase('Appwrite logo', $lastEmail['html']); } - // TODO: Remove this once OTP login is supported for Console. - if ($isConsoleProject) { - return; - } - $response = $this->client->call(Client::METHOD_POST, '/account/sessions/token', array_merge([ 'origin' => 'http://localhost', 'content-type' => 'application/json', From caf18372ce36eb777fdb894b0c0af368e27ed7bc Mon Sep 17 00:00:00 2001 From: Hemachandar Date: Mon, 6 Oct 2025 21:52:18 +0530 Subject: [PATCH 8/9] feedback --- app/config/console.php | 2 +- .../locale/templates/email-base-styled.tpl | 8 +++++ app/config/locale/templates/email-base.tpl | 7 +++- app/config/locale/templates/email-otp.tpl | 4 +-- app/config/locale/translations/en.json | 6 ++-- app/controllers/api/account.php | 35 ++++++++++++++----- app/init/constants.php | 1 + 7 files changed, 47 insertions(+), 16 deletions(-) diff --git a/app/config/console.php b/app/config/console.php index 70bb3523e6..f8f68a8039 100644 --- a/app/config/console.php +++ b/app/config/console.php @@ -49,7 +49,7 @@ $console = [ 'githubSecret' => System::getEnv('_APP_CONSOLE_GITHUB_SECRET', ''), 'githubAppid' => System::getEnv('_APP_CONSOLE_GITHUB_APP_ID', '') ], - 'smtpBaseTemplate' => 'email-base-styled', + 'smtpBaseTemplate' => APP_BRANDED_EMAIL_BASE_TEMPLATE, ]; return $console; diff --git a/app/config/locale/templates/email-base-styled.tpl b/app/config/locale/templates/email-base-styled.tpl index 3f3ba8dd9c..37ca630d43 100644 --- a/app/config/locale/templates/email-base-styled.tpl +++ b/app/config/locale/templates/email-base-styled.tpl @@ -131,6 +131,14 @@ .social-icon > img { margin: auto; } + p.security-phrase:not(:empty) { + opacity: 0.7; + margin: 0; + padding: 0; + margin-top: 32px; + padding-top: 32px; + border-top: 1px solid #e8e9f0; + } diff --git a/app/config/locale/templates/email-base.tpl b/app/config/locale/templates/email-base.tpl index 5153b3e4fc..de632d7838 100644 --- a/app/config/locale/templates/email-base.tpl +++ b/app/config/locale/templates/email-base.tpl @@ -109,10 +109,15 @@ h* { font-family: 'Poppins', sans-serif; } - p { margin-bottom: 10px; } + p.security-phrase:not(:empty) { + opacity: 0.7; + margin-top: 32px; + padding-top: 32px; + border-top: 1px solid #e8e9f0; + } diff --git a/app/config/locale/templates/email-otp.tpl b/app/config/locale/templates/email-otp.tpl index e18a4ce725..cfcdb8f7af 100644 --- a/app/config/locale/templates/email-otp.tpl +++ b/app/config/locale/templates/email-otp.tpl @@ -15,6 +15,4 @@

{{thanks}}

{{signature}}

-
- -

{{securityPhrase}}

\ No newline at end of file +

{{securityPhrase}}

\ No newline at end of file diff --git a/app/config/locale/translations/en.json b/app/config/locale/translations/en.json index 69328e61a7..0500c4c668 100644 --- a/app/config/locale/translations/en.json +++ b/app/config/locale/translations/en.json @@ -5,7 +5,7 @@ "emails.sender": "{{project}} Team", "emails.verification.subject": "Account Verification", "emails.verification.preview": "Verify your email to activate your {{project}} account.", - "emails.verification.heading": "Verify your email to start using Appwrite Cloud", + "emails.verification.heading": "Verify your email to start using {{project}}", "emails.verification.hello": "Hello {{user}},", "emails.verification.body": "Follow this link to verify your email address to your {{b}}{{project}}{{/b}} account.", "emails.verification.footer": "If you didn’t ask to verify this address, you can ignore this message.", @@ -34,7 +34,7 @@ "emails.sessionAlert.signature": "{{project}} team", "emails.otpSession.subject": "OTP for {{project}} Login", "emails.otpSession.preview": "Use OTP {{otp}} to sign in to {{project}}. Expires in 15 minutes.", - "emails.otpSession.heading": "Login with OTP to use Appwrite Cloud", + "emails.otpSession.heading": "Login with OTP to use {{project}}", "emails.otpSession.hello": "Hello {{user}},", "emails.otpSession.description": "Enter the following verification code when prompted to securely sign in to your {{b}}{{project}}{{/b}} account. This code will expire in 15 minutes.", "emails.otpSession.clientInfo": "This sign in was requested using {{b}}{{agentClient}}{{/b}} on {{b}}{{agentDevice}}{{/b}} {{b}}{{agentOs}}{{/b}}. If you didn't request the sign in, you can safely ignore this email.", @@ -43,7 +43,7 @@ "emails.otpSession.signature": "{{project}} team", "emails.mfaChallenge.subject": "Verification Code for {{project}}", "emails.mfaChallenge.preview": "Use code {{otp}} for two-step verification in {{project}}. Expires in 15 minutes.", - "emails.mfaChallenge.heading": "Complete two-step verification to use Appwrite Cloud", + "emails.mfaChallenge.heading": "Complete two-step verification to use {{project}}", "emails.mfaChallenge.hello": "Hello {{user}},", "emails.mfaChallenge.description": "Enter the following code to confirm your two-step verification in {{b}}{{project}}{{/b}}. This code will expire in 15 minutes.", "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.", diff --git a/app/controllers/api/account.php b/app/controllers/api/account.php index e98376dfc7..af9a772b17 100644 --- a/app/controllers/api/account.php +++ b/app/controllers/api/account.php @@ -69,6 +69,12 @@ use Utopia\Validator\WhiteList; $oauthDefaultSuccess = '/console/auth/oauth2/success'; $oauthDefaultFailure = '/console/auth/oauth2/failure'; +function containsDirectoryTraversal(string $path) +{ + // Matches '../', './', '/..', or absolute paths starting with '/' + return preg_match('/(\.\.\/|\.\/|\/\.\.|\/)/', $path); +} + function sendSessionAlert(Locale $locale, Document $user, Document $project, Document $session, Mail $queueForMails) { $subject = $locale->getText("emails.sessionAlert.subject"); @@ -2300,6 +2306,11 @@ App::post('/v1/account/tokens/email') $customTemplate = $project->getAttribute('templates', [])['email.otpSession-' . $locale->default] ?? []; $smtpBaseTemplate = $project->getAttribute('smtpBaseTemplate', 'email-base'); + + if (containsDirectoryTraversal($smtpBaseTemplate)) { + throw new Exception(Exception::GENERAL_BAD_REQUEST, 'Invalid template path'); + } + $bodyTemplate = __DIR__ . '/../../config/locale/templates/' . $smtpBaseTemplate . '.tpl'; $detector = new Detector($request->getUserAgent('UNKNOWN')); @@ -2317,10 +2328,8 @@ App::post('/v1/account/tokens/email') if (!empty($phrase)) { $message->setParam('{{securityPhrase}}', $locale->getText("emails.otpSession.securityPhrase")); - $message->setParam('{{securityPhraseDividerDisplay}}', 'block'); } else { $message->setParam('{{securityPhrase}}', ''); - $message->setParam('{{securityPhraseDividerDisplay}}', 'none'); } $body = $message->render(); @@ -2372,6 +2381,7 @@ App::post('/v1/account/tokens/email') } $emailVariables = [ + 'heading' => $heading, 'direction' => $locale->getText('settings.direction'), // {{user}}, {{project}} and {{otp}} are required in the templates 'user' => $user->getAttribute('name'), @@ -2385,9 +2395,8 @@ App::post('/v1/account/tokens/email') 'team' => '', ]; - if ($smtpBaseTemplate === 'email-base-styled') { + if ($smtpBaseTemplate === APP_BRANDED_EMAIL_BASE_TEMPLATE) { $emailVariables = array_merge($emailVariables, [ - 'heading' => $heading, 'accentColor' => APP_EMAIL_ACCENT_COLOR, 'logoUrl' => APP_EMAIL_LOGO_URL, 'twitterUrl' => APP_SOCIAL_TWITTER, @@ -3611,6 +3620,11 @@ App::post('/v1/account/verification') $customTemplate = $project->getAttribute('templates', [])['email.verification-' . $locale->default] ?? []; $smtpBaseTemplate = $project->getAttribute('smtpBaseTemplate', 'email-base'); + + if (containsDirectoryTraversal($smtpBaseTemplate)) { + throw new Exception(Exception::GENERAL_BAD_REQUEST, 'Invalid template path'); + } + $bodyTemplate = __DIR__ . '/../../config/locale/templates/' . $smtpBaseTemplate . '.tpl'; $message = Template::fromFile(__DIR__ . '/../../config/locale/templates/email-inner-base.tpl'); @@ -3671,6 +3685,7 @@ App::post('/v1/account/verification') } $emailVariables = [ + 'heading' => $heading, 'direction' => $locale->getText('settings.direction'), // {{user}}, {{redirect}} and {{project}} are required in default and custom templates 'user' => $user->getAttribute('name'), @@ -3680,9 +3695,8 @@ App::post('/v1/account/verification') 'team' => '', ]; - if ($smtpBaseTemplate === 'email-base-styled') { + if ($smtpBaseTemplate === APP_BRANDED_EMAIL_BASE_TEMPLATE) { $emailVariables = array_merge($emailVariables, [ - 'heading' => $heading, 'accentColor' => APP_EMAIL_ACCENT_COLOR, 'logoUrl' => APP_EMAIL_LOGO_URL, 'twitterUrl' => APP_SOCIAL_TWITTER, @@ -4707,6 +4721,11 @@ App::post('/v1/account/mfa/challenge') $customTemplate = $project->getAttribute('templates', [])['email.mfaChallenge-' . $locale->default] ?? []; $smtpBaseTemplate = $project->getAttribute('smtpBaseTemplate', 'email-base'); + + if (containsDirectoryTraversal($smtpBaseTemplate)) { + throw new Exception(Exception::GENERAL_BAD_REQUEST, 'Invalid template path'); + } + $bodyTemplate = __DIR__ . '/../../config/locale/templates/' . $smtpBaseTemplate . '.tpl'; $detector = new Detector($request->getUserAgent('UNKNOWN')); @@ -4771,6 +4790,7 @@ App::post('/v1/account/mfa/challenge') } $emailVariables = [ + 'heading' => $heading, 'direction' => $locale->getText('settings.direction'), // {{user}}, {{project}} and {{otp}} are required in the templates 'user' => $user->getAttribute('name'), @@ -4781,9 +4801,8 @@ App::post('/v1/account/mfa/challenge') 'agentOs' => $agentOs['osName'] ?? 'UNKNOWN', ]; - if ($smtpBaseTemplate === 'email-base-styled') { + if ($smtpBaseTemplate === APP_BRANDED_EMAIL_BASE_TEMPLATE) { $emailVariables = array_merge($emailVariables, [ - 'heading' => $heading, 'accentColor' => APP_EMAIL_ACCENT_COLOR, 'logoUrl' => APP_EMAIL_LOGO_URL, 'twitterUrl' => APP_SOCIAL_TWITTER, diff --git a/app/init/constants.php b/app/init/constants.php index 28cf8a4052..16ddcf5551 100644 --- a/app/init/constants.php +++ b/app/init/constants.php @@ -85,6 +85,7 @@ const APP_PLATFORM_CLIENT = 'client'; const APP_PLATFORM_CONSOLE = 'console'; const APP_VCS_GITHUB_USERNAME = 'Appwrite'; const APP_VCS_GITHUB_EMAIL = 'team@appwrite.io'; +const APP_BRANDED_EMAIL_BASE_TEMPLATE = 'email-base-styled'; // Database Reconnect const DATABASE_RECONNECT_SLEEP = 2; From 6e0d3767465716cbe0bb82110685a89622ac9980 Mon Sep 17 00:00:00 2001 From: Hemachandar Date: Tue, 7 Oct 2025 16:07:47 +0530 Subject: [PATCH 9/9] use filevalidator --- app/controllers/api/account.php | 16 +- composer.lock | 269 ++++++++++++++++---------------- 2 files changed, 144 insertions(+), 141 deletions(-) diff --git a/app/controllers/api/account.php b/app/controllers/api/account.php index af9a772b17..6836c6bfaf 100644 --- a/app/controllers/api/account.php +++ b/app/controllers/api/account.php @@ -58,6 +58,7 @@ use Utopia\Database\Validator\Query\Limit; use Utopia\Database\Validator\Query\Offset; use Utopia\Database\Validator\UID; use Utopia\Locale\Locale; +use Utopia\Storage\Validator\FileName; use Utopia\System\System; use Utopia\Validator\ArrayList; use Utopia\Validator\Assoc; @@ -69,12 +70,6 @@ use Utopia\Validator\WhiteList; $oauthDefaultSuccess = '/console/auth/oauth2/success'; $oauthDefaultFailure = '/console/auth/oauth2/failure'; -function containsDirectoryTraversal(string $path) -{ - // Matches '../', './', '/..', or absolute paths starting with '/' - return preg_match('/(\.\.\/|\.\/|\/\.\.|\/)/', $path); -} - function sendSessionAlert(Locale $locale, Document $user, Document $project, Document $session, Mail $queueForMails) { $subject = $locale->getText("emails.sessionAlert.subject"); @@ -2307,7 +2302,8 @@ App::post('/v1/account/tokens/email') $customTemplate = $project->getAttribute('templates', [])['email.otpSession-' . $locale->default] ?? []; $smtpBaseTemplate = $project->getAttribute('smtpBaseTemplate', 'email-base'); - if (containsDirectoryTraversal($smtpBaseTemplate)) { + $validator = new FileName(); + if (!$validator->isValid($smtpBaseTemplate)) { throw new Exception(Exception::GENERAL_BAD_REQUEST, 'Invalid template path'); } @@ -3621,7 +3617,8 @@ App::post('/v1/account/verification') $customTemplate = $project->getAttribute('templates', [])['email.verification-' . $locale->default] ?? []; $smtpBaseTemplate = $project->getAttribute('smtpBaseTemplate', 'email-base'); - if (containsDirectoryTraversal($smtpBaseTemplate)) { + $validator = new FileName(); + if (!$validator->isValid($smtpBaseTemplate)) { throw new Exception(Exception::GENERAL_BAD_REQUEST, 'Invalid template path'); } @@ -4722,7 +4719,8 @@ App::post('/v1/account/mfa/challenge') $customTemplate = $project->getAttribute('templates', [])['email.mfaChallenge-' . $locale->default] ?? []; $smtpBaseTemplate = $project->getAttribute('smtpBaseTemplate', 'email-base'); - if (containsDirectoryTraversal($smtpBaseTemplate)) { + $validator = new FileName(); + if (!$validator->isValid($smtpBaseTemplate)) { throw new Exception(Exception::GENERAL_BAD_REQUEST, 'Invalid template path'); } diff --git a/composer.lock b/composer.lock index bd70f229ad..8e600c326f 100644 --- a/composer.lock +++ b/composer.lock @@ -756,24 +756,21 @@ }, { "name": "google/protobuf", - "version": "v4.32.0", + "version": "v4.32.1", "source": { "type": "git", "url": "https://github.com/protocolbuffers/protobuf-php.git", - "reference": "9a9a92ecbe9c671dc1863f6d4a91ea3ea12c8646" + "reference": "c4ed1c1f9bbc1e91766e2cd6c0af749324fe87cb" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/protocolbuffers/protobuf-php/zipball/9a9a92ecbe9c671dc1863f6d4a91ea3ea12c8646", - "reference": "9a9a92ecbe9c671dc1863f6d4a91ea3ea12c8646", + "url": "https://api.github.com/repos/protocolbuffers/protobuf-php/zipball/c4ed1c1f9bbc1e91766e2cd6c0af749324fe87cb", + "reference": "c4ed1c1f9bbc1e91766e2cd6c0af749324fe87cb", "shasum": "" }, "require": { "php": ">=8.1.0" }, - "provide": { - "ext-protobuf": "*" - }, "require-dev": { "phpunit/phpunit": ">=5.0.0 <8.5.27" }, @@ -797,9 +794,9 @@ "proto" ], "support": { - "source": "https://github.com/protocolbuffers/protobuf-php/tree/v4.32.0" + "source": "https://github.com/protocolbuffers/protobuf-php/tree/v4.32.1" }, - "time": "2025-08-14T20:00:33+00:00" + "time": "2025-09-14T05:14:52+00:00" }, { "name": "league/csv", @@ -1162,20 +1159,20 @@ }, { "name": "open-telemetry/api", - "version": "1.5.0", + "version": "1.7.0", "source": { "type": "git", "url": "https://github.com/opentelemetry-php/api.git", - "reference": "7692075f486c14d8cfd37fba98a08a5667f089e5" + "reference": "610b79ad9d6d97e8368bcb6c4d42394fbb87b522" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/opentelemetry-php/api/zipball/7692075f486c14d8cfd37fba98a08a5667f089e5", - "reference": "7692075f486c14d8cfd37fba98a08a5667f089e5", + "url": "https://api.github.com/repos/opentelemetry-php/api/zipball/610b79ad9d6d97e8368bcb6c4d42394fbb87b522", + "reference": "610b79ad9d6d97e8368bcb6c4d42394fbb87b522", "shasum": "" }, "require": { - "open-telemetry/context": "^1.0", + "open-telemetry/context": "^1.4", "php": "^8.1", "psr/log": "^1.1|^2.0|^3.0", "symfony/polyfill-php82": "^1.26" @@ -1191,7 +1188,7 @@ ] }, "branch-alias": { - "dev-main": "1.4.x-dev" + "dev-main": "1.7.x-dev" } }, "autoload": { @@ -1228,20 +1225,20 @@ "issues": "https://github.com/open-telemetry/opentelemetry-php/issues", "source": "https://github.com/open-telemetry/opentelemetry-php" }, - "time": "2025-08-07T23:07:38+00:00" + "time": "2025-10-02T23:44:28+00:00" }, { "name": "open-telemetry/context", - "version": "1.3.1", + "version": "1.4.0", "source": { "type": "git", "url": "https://github.com/opentelemetry-php/context.git", - "reference": "438f71812242db3f196fb4c717c6f92cbc819be6" + "reference": "d4c4470b541ce72000d18c339cfee633e4c8e0cf" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/opentelemetry-php/context/zipball/438f71812242db3f196fb4c717c6f92cbc819be6", - "reference": "438f71812242db3f196fb4c717c6f92cbc819be6", + "url": "https://api.github.com/repos/opentelemetry-php/context/zipball/d4c4470b541ce72000d18c339cfee633e4c8e0cf", + "reference": "d4c4470b541ce72000d18c339cfee633e4c8e0cf", "shasum": "" }, "require": { @@ -1287,7 +1284,7 @@ "issues": "https://github.com/open-telemetry/opentelemetry-php/issues", "source": "https://github.com/open-telemetry/opentelemetry-php" }, - "time": "2025-08-13T01:12:00+00:00" + "time": "2025-09-19T00:05:49+00:00" }, { "name": "open-telemetry/exporter-otlp", @@ -1355,16 +1352,16 @@ }, { "name": "open-telemetry/gen-otlp-protobuf", - "version": "1.5.0", + "version": "1.8.0", "source": { "type": "git", "url": "https://github.com/opentelemetry-php/gen-otlp-protobuf.git", - "reference": "585bafddd4ae6565de154610b10a787a455c9ba0" + "reference": "673af5b06545b513466081884b47ef15a536edde" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/opentelemetry-php/gen-otlp-protobuf/zipball/585bafddd4ae6565de154610b10a787a455c9ba0", - "reference": "585bafddd4ae6565de154610b10a787a455c9ba0", + "url": "https://api.github.com/repos/opentelemetry-php/gen-otlp-protobuf/zipball/673af5b06545b513466081884b47ef15a536edde", + "reference": "673af5b06545b513466081884b47ef15a536edde", "shasum": "" }, "require": { @@ -1414,27 +1411,27 @@ "issues": "https://github.com/open-telemetry/opentelemetry-php/issues", "source": "https://github.com/open-telemetry/opentelemetry-php" }, - "time": "2025-01-15T23:07:07+00:00" + "time": "2025-09-17T23:10:12+00:00" }, { "name": "open-telemetry/sdk", - "version": "1.7.1", + "version": "1.9.0", "source": { "type": "git", "url": "https://github.com/opentelemetry-php/sdk.git", - "reference": "52690d4b37ae4f091af773eef3c238ed2bc0aa06" + "reference": "8986bcbcbea79cb1ba9e91c1d621541ad63d6b3e" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/opentelemetry-php/sdk/zipball/52690d4b37ae4f091af773eef3c238ed2bc0aa06", - "reference": "52690d4b37ae4f091af773eef3c238ed2bc0aa06", + "url": "https://api.github.com/repos/opentelemetry-php/sdk/zipball/8986bcbcbea79cb1ba9e91c1d621541ad63d6b3e", + "reference": "8986bcbcbea79cb1ba9e91c1d621541ad63d6b3e", "shasum": "" }, "require": { "ext-json": "*", "nyholm/psr7-server": "^1.1", - "open-telemetry/api": "^1.4", - "open-telemetry/context": "^1.0", + "open-telemetry/api": "^1.7", + "open-telemetry/context": "^1.4", "open-telemetry/sem-conv": "^1.0", "php": "^8.1", "php-http/discovery": "^1.14", @@ -1468,7 +1465,7 @@ ] }, "branch-alias": { - "dev-main": "1.0.x-dev" + "dev-main": "1.9.x-dev" } }, "autoload": { @@ -1511,7 +1508,7 @@ "issues": "https://github.com/open-telemetry/opentelemetry-php/issues", "source": "https://github.com/open-telemetry/opentelemetry-php" }, - "time": "2025-09-05T07:17:06+00:00" + "time": "2025-10-02T23:44:28+00:00" }, { "name": "open-telemetry/sem-conv", @@ -1572,16 +1569,16 @@ }, { "name": "paragonie/constant_time_encoding", - "version": "v2.7.0", + "version": "v2.8.2", "source": { "type": "git", "url": "https://github.com/paragonie/constant_time_encoding.git", - "reference": "52a0d99e69f56b9ec27ace92ba56897fe6993105" + "reference": "e30811f7bc69e4b5b6d5783e712c06c8eabf0226" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/paragonie/constant_time_encoding/zipball/52a0d99e69f56b9ec27ace92ba56897fe6993105", - "reference": "52a0d99e69f56b9ec27ace92ba56897fe6993105", + "url": "https://api.github.com/repos/paragonie/constant_time_encoding/zipball/e30811f7bc69e4b5b6d5783e712c06c8eabf0226", + "reference": "e30811f7bc69e4b5b6d5783e712c06c8eabf0226", "shasum": "" }, "require": { @@ -1635,7 +1632,7 @@ "issues": "https://github.com/paragonie/constant_time_encoding/issues", "source": "https://github.com/paragonie/constant_time_encoding" }, - "time": "2024-05-08T12:18:48+00:00" + "time": "2025-09-24T15:12:37+00:00" }, { "name": "paragonie/random_compat", @@ -1930,16 +1927,16 @@ }, { "name": "phpseclib/phpseclib", - "version": "3.0.46", + "version": "3.0.47", "source": { "type": "git", "url": "https://github.com/phpseclib/phpseclib.git", - "reference": "56483a7de62a6c2a6635e42e93b8a9e25d4f0ec6" + "reference": "9d6ca36a6c2dd434765b1071b2644a1c683b385d" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/phpseclib/phpseclib/zipball/56483a7de62a6c2a6635e42e93b8a9e25d4f0ec6", - "reference": "56483a7de62a6c2a6635e42e93b8a9e25d4f0ec6", + "url": "https://api.github.com/repos/phpseclib/phpseclib/zipball/9d6ca36a6c2dd434765b1071b2644a1c683b385d", + "reference": "9d6ca36a6c2dd434765b1071b2644a1c683b385d", "shasum": "" }, "require": { @@ -2020,7 +2017,7 @@ ], "support": { "issues": "https://github.com/phpseclib/phpseclib/issues", - "source": "https://github.com/phpseclib/phpseclib/tree/3.0.46" + "source": "https://github.com/phpseclib/phpseclib/tree/3.0.47" }, "funding": [ { @@ -2036,7 +2033,7 @@ "type": "tidelift" } ], - "time": "2025-06-26T16:29:55+00:00" + "time": "2025-10-06T01:07:24+00:00" }, { "name": "psr/container", @@ -2599,16 +2596,16 @@ }, { "name": "symfony/http-client", - "version": "v7.3.3", + "version": "v7.3.4", "source": { "type": "git", "url": "https://github.com/symfony/http-client.git", - "reference": "333b9bd7639cbdaecd25a3a48a9d2dcfaa86e019" + "reference": "4b62871a01c49457cf2a8e560af7ee8a94b87a62" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/http-client/zipball/333b9bd7639cbdaecd25a3a48a9d2dcfaa86e019", - "reference": "333b9bd7639cbdaecd25a3a48a9d2dcfaa86e019", + "url": "https://api.github.com/repos/symfony/http-client/zipball/4b62871a01c49457cf2a8e560af7ee8a94b87a62", + "reference": "4b62871a01c49457cf2a8e560af7ee8a94b87a62", "shasum": "" }, "require": { @@ -2675,7 +2672,7 @@ "http" ], "support": { - "source": "https://github.com/symfony/http-client/tree/v7.3.3" + "source": "https://github.com/symfony/http-client/tree/v7.3.4" }, "funding": [ { @@ -2695,7 +2692,7 @@ "type": "tidelift" } ], - "time": "2025-08-27T07:45:05+00:00" + "time": "2025-09-11T10:12:26+00:00" }, { "name": "symfony/http-client-contracts", @@ -3638,16 +3635,16 @@ }, { "name": "utopia-php/database", - "version": "1.4.1", + "version": "1.5.1", "source": { "type": "git", "url": "https://github.com/utopia-php/database.git", - "reference": "b5ea4d133a1a4e747b7522e61e072289129a06f4" + "reference": "56efe4daaf23abb753553acffccdcc04cd6178c9" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/utopia-php/database/zipball/b5ea4d133a1a4e747b7522e61e072289129a06f4", - "reference": "b5ea4d133a1a4e747b7522e61e072289129a06f4", + "url": "https://api.github.com/repos/utopia-php/database/zipball/56efe4daaf23abb753553acffccdcc04cd6178c9", + "reference": "56efe4daaf23abb753553acffccdcc04cd6178c9", "shasum": "" }, "require": { @@ -3688,9 +3685,9 @@ ], "support": { "issues": "https://github.com/utopia-php/database/issues", - "source": "https://github.com/utopia-php/database/tree/1.4.1" + "source": "https://github.com/utopia-php/database/tree/1.5.1" }, - "time": "2025-09-05T13:23:52+00:00" + "time": "2025-10-01T04:44:14+00:00" }, { "name": "utopia-php/detector", @@ -3795,16 +3792,16 @@ }, { "name": "utopia-php/domains", - "version": "0.8.0", + "version": "0.8.1", "source": { "type": "git", "url": "https://github.com/utopia-php/domains.git", - "reference": "650463d2a1525273eb03223c48da9fb1a768bbf7" + "reference": "d5f903e93c105407da6374e411c4805b7decd8a8" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/utopia-php/domains/zipball/650463d2a1525273eb03223c48da9fb1a768bbf7", - "reference": "650463d2a1525273eb03223c48da9fb1a768bbf7", + "url": "https://api.github.com/repos/utopia-php/domains/zipball/d5f903e93c105407da6374e411c4805b7decd8a8", + "reference": "d5f903e93c105407da6374e411c4805b7decd8a8", "shasum": "" }, "require": { @@ -3850,9 +3847,9 @@ ], "support": { "issues": "https://github.com/utopia-php/domains/issues", - "source": "https://github.com/utopia-php/domains/tree/0.8.0" + "source": "https://github.com/utopia-php/domains/tree/0.8.1" }, - "time": "2025-05-16T10:03:59+00:00" + "time": "2025-10-03T11:58:53+00:00" }, { "name": "utopia-php/dsn", @@ -3942,16 +3939,16 @@ }, { "name": "utopia-php/framework", - "version": "0.33.27", + "version": "0.33.28", "source": { "type": "git", "url": "https://github.com/utopia-php/http.git", - "reference": "d9d10a895e85c8c7675220347cc6109db9d3bd37" + "reference": "5aaa94d406577b0059ad28c78022606890dc6de0" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/utopia-php/http/zipball/d9d10a895e85c8c7675220347cc6109db9d3bd37", - "reference": "d9d10a895e85c8c7675220347cc6109db9d3bd37", + "url": "https://api.github.com/repos/utopia-php/http/zipball/5aaa94d406577b0059ad28c78022606890dc6de0", + "reference": "5aaa94d406577b0059ad28c78022606890dc6de0", "shasum": "" }, "require": { @@ -3983,9 +3980,9 @@ ], "support": { "issues": "https://github.com/utopia-php/http/issues", - "source": "https://github.com/utopia-php/http/tree/0.33.27" + "source": "https://github.com/utopia-php/http/tree/0.33.28" }, - "time": "2025-09-07T18:40:53+00:00" + "time": "2025-09-25T10:44:24+00:00" }, { "name": "utopia-php/image", @@ -4190,16 +4187,16 @@ }, { "name": "utopia-php/migration", - "version": "1.0.1", + "version": "1.2.0", "source": { "type": "git", "url": "https://github.com/utopia-php/migration.git", - "reference": "38171023efd3abe650d2abc5ac65f5df52311da6" + "reference": "42ff497c5231f5a727d1e229419ff1d2195d8093" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/utopia-php/migration/zipball/38171023efd3abe650d2abc5ac65f5df52311da6", - "reference": "38171023efd3abe650d2abc5ac65f5df52311da6", + "url": "https://api.github.com/repos/utopia-php/migration/zipball/42ff497c5231f5a727d1e229419ff1d2195d8093", + "reference": "42ff497c5231f5a727d1e229419ff1d2195d8093", "shasum": "" }, "require": { @@ -4240,9 +4237,9 @@ ], "support": { "issues": "https://github.com/utopia-php/migration/issues", - "source": "https://github.com/utopia-php/migration/tree/1.0.1" + "source": "https://github.com/utopia-php/migration/tree/1.2.0" }, - "time": "2025-08-28T13:41:25+00:00" + "time": "2025-09-24T10:32:24+00:00" }, { "name": "utopia-php/orchestration", @@ -4569,16 +4566,16 @@ }, { "name": "utopia-php/storage", - "version": "0.18.13", + "version": "0.18.14", "source": { "type": "git", "url": "https://github.com/utopia-php/storage.git", - "reference": "3d8ce53ae042173bf230445e996056c5f65ded22" + "reference": "4f14ec952c6f4006dd0613e55bbf7631814fbc00" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/utopia-php/storage/zipball/3d8ce53ae042173bf230445e996056c5f65ded22", - "reference": "3d8ce53ae042173bf230445e996056c5f65ded22", + "url": "https://api.github.com/repos/utopia-php/storage/zipball/4f14ec952c6f4006dd0613e55bbf7631814fbc00", + "reference": "4f14ec952c6f4006dd0613e55bbf7631814fbc00", "shasum": "" }, "require": { @@ -4621,9 +4618,9 @@ ], "support": { "issues": "https://github.com/utopia-php/storage/issues", - "source": "https://github.com/utopia-php/storage/tree/0.18.13" + "source": "https://github.com/utopia-php/storage/tree/0.18.14" }, - "time": "2025-05-26T13:10:35+00:00" + "time": "2025-10-07T10:21:47+00:00" }, { "name": "utopia-php/swoole", @@ -5007,16 +5004,16 @@ "packages-dev": [ { "name": "appwrite/sdk-generator", - "version": "1.3.2", + "version": "1.4.3", "source": { "type": "git", "url": "https://github.com/appwrite/sdk-generator.git", - "reference": "375a6c9b168db6fdf58fbe0d49d2261d80700b4a" + "reference": "e1ca749398189f36ec6d6afb8e9f64e9cb37e0a3" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/appwrite/sdk-generator/zipball/375a6c9b168db6fdf58fbe0d49d2261d80700b4a", - "reference": "375a6c9b168db6fdf58fbe0d49d2261d80700b4a", + "url": "https://api.github.com/repos/appwrite/sdk-generator/zipball/e1ca749398189f36ec6d6afb8e9f64e9cb37e0a3", + "reference": "e1ca749398189f36ec6d6afb8e9f64e9cb37e0a3", "shasum": "" }, "require": { @@ -5052,9 +5049,9 @@ "description": "Appwrite PHP library for generating API SDKs for multiple programming languages and platforms", "support": { "issues": "https://github.com/appwrite/sdk-generator/issues", - "source": "https://github.com/appwrite/sdk-generator/tree/1.3.2" + "source": "https://github.com/appwrite/sdk-generator/tree/1.4.3" }, - "time": "2025-09-05T15:50:35+00:00" + "time": "2025-10-01T06:25:19+00:00" }, { "name": "doctrine/annotations", @@ -5281,16 +5278,16 @@ }, { "name": "laravel/pint", - "version": "v1.24.0", + "version": "v1.25.1", "source": { "type": "git", "url": "https://github.com/laravel/pint.git", - "reference": "0345f3b05f136801af8c339f9d16ef29e6b4df8a" + "reference": "5016e263f95d97670d71b9a987bd8996ade6d8d9" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/laravel/pint/zipball/0345f3b05f136801af8c339f9d16ef29e6b4df8a", - "reference": "0345f3b05f136801af8c339f9d16ef29e6b4df8a", + "url": "https://api.github.com/repos/laravel/pint/zipball/5016e263f95d97670d71b9a987bd8996ade6d8d9", + "reference": "5016e263f95d97670d71b9a987bd8996ade6d8d9", "shasum": "" }, "require": { @@ -5301,9 +5298,9 @@ "php": "^8.2.0" }, "require-dev": { - "friendsofphp/php-cs-fixer": "^3.82.2", - "illuminate/view": "^11.45.1", - "larastan/larastan": "^3.5.0", + "friendsofphp/php-cs-fixer": "^3.87.2", + "illuminate/view": "^11.46.0", + "larastan/larastan": "^3.7.1", "laravel-zero/framework": "^11.45.0", "mockery/mockery": "^1.6.12", "nunomaduro/termwind": "^2.3.1", @@ -5314,9 +5311,6 @@ ], "type": "project", "autoload": { - "files": [ - "overrides/Runner/Parallel/ProcessFactory.php" - ], "psr-4": { "App\\": "app/", "Database\\Seeders\\": "database/seeders/", @@ -5346,7 +5340,7 @@ "issues": "https://github.com/laravel/pint/issues", "source": "https://github.com/laravel/pint" }, - "time": "2025-07-10T18:09:32+00:00" + "time": "2025-09-19T02:57:12+00:00" }, { "name": "matthiasmullie/minify", @@ -6236,16 +6230,16 @@ }, { "name": "phpunit/phpunit", - "version": "9.6.25", + "version": "9.6.29", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/phpunit.git", - "reference": "049c011e01be805202d8eebedef49f769a8ec7b7" + "reference": "9ecfec57835a5581bc888ea7e13b51eb55ab9dd3" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/phpunit/zipball/049c011e01be805202d8eebedef49f769a8ec7b7", - "reference": "049c011e01be805202d8eebedef49f769a8ec7b7", + "url": "https://api.github.com/repos/sebastianbergmann/phpunit/zipball/9ecfec57835a5581bc888ea7e13b51eb55ab9dd3", + "reference": "9ecfec57835a5581bc888ea7e13b51eb55ab9dd3", "shasum": "" }, "require": { @@ -6270,7 +6264,7 @@ "sebastian/comparator": "^4.0.9", "sebastian/diff": "^4.0.6", "sebastian/environment": "^5.1.5", - "sebastian/exporter": "^4.0.6", + "sebastian/exporter": "^4.0.8", "sebastian/global-state": "^5.0.8", "sebastian/object-enumerator": "^4.0.4", "sebastian/resource-operations": "^3.0.4", @@ -6319,7 +6313,7 @@ "support": { "issues": "https://github.com/sebastianbergmann/phpunit/issues", "security": "https://github.com/sebastianbergmann/phpunit/security/policy", - "source": "https://github.com/sebastianbergmann/phpunit/tree/9.6.25" + "source": "https://github.com/sebastianbergmann/phpunit/tree/9.6.29" }, "funding": [ { @@ -6343,7 +6337,7 @@ "type": "tidelift" } ], - "time": "2025-08-20T14:38:31+00:00" + "time": "2025-09-24T06:29:11+00:00" }, { "name": "psr/cache", @@ -6835,16 +6829,16 @@ }, { "name": "sebastian/exporter", - "version": "4.0.6", + "version": "4.0.8", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/exporter.git", - "reference": "78c00df8f170e02473b682df15bfcdacc3d32d72" + "reference": "14c6ba52f95a36c3d27c835d65efc7123c446e8c" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/exporter/zipball/78c00df8f170e02473b682df15bfcdacc3d32d72", - "reference": "78c00df8f170e02473b682df15bfcdacc3d32d72", + "url": "https://api.github.com/repos/sebastianbergmann/exporter/zipball/14c6ba52f95a36c3d27c835d65efc7123c446e8c", + "reference": "14c6ba52f95a36c3d27c835d65efc7123c446e8c", "shasum": "" }, "require": { @@ -6900,15 +6894,27 @@ ], "support": { "issues": "https://github.com/sebastianbergmann/exporter/issues", - "source": "https://github.com/sebastianbergmann/exporter/tree/4.0.6" + "source": "https://github.com/sebastianbergmann/exporter/tree/4.0.8" }, "funding": [ { "url": "https://github.com/sebastianbergmann", "type": "github" + }, + { + "url": "https://liberapay.com/sebastianbergmann", + "type": "liberapay" + }, + { + "url": "https://thanks.dev/u/gh/sebastianbergmann", + "type": "thanks_dev" + }, + { + "url": "https://tidelift.com/funding/github/packagist/sebastian/exporter", + "type": "tidelift" } ], - "time": "2024-03-02T06:33:00+00:00" + "time": "2025-09-24T06:03:27+00:00" }, { "name": "sebastian/global-state", @@ -7491,16 +7497,16 @@ }, { "name": "symfony/console", - "version": "v7.3.3", + "version": "v7.3.4", "source": { "type": "git", "url": "https://github.com/symfony/console.git", - "reference": "cb0102a1c5ac3807cf3fdf8bea96007df7fdbea7" + "reference": "2b9c5fafbac0399a20a2e82429e2bd735dcfb7db" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/console/zipball/cb0102a1c5ac3807cf3fdf8bea96007df7fdbea7", - "reference": "cb0102a1c5ac3807cf3fdf8bea96007df7fdbea7", + "url": "https://api.github.com/repos/symfony/console/zipball/2b9c5fafbac0399a20a2e82429e2bd735dcfb7db", + "reference": "2b9c5fafbac0399a20a2e82429e2bd735dcfb7db", "shasum": "" }, "require": { @@ -7565,7 +7571,7 @@ "terminal" ], "support": { - "source": "https://github.com/symfony/console/tree/v7.3.3" + "source": "https://github.com/symfony/console/tree/v7.3.4" }, "funding": [ { @@ -7585,7 +7591,7 @@ "type": "tidelift" } ], - "time": "2025-08-25T06:35:40+00:00" + "time": "2025-09-22T15:31:00+00:00" }, { "name": "symfony/filesystem", @@ -8128,16 +8134,16 @@ }, { "name": "symfony/process", - "version": "v7.3.3", + "version": "v7.3.4", "source": { "type": "git", "url": "https://github.com/symfony/process.git", - "reference": "32241012d521e2e8a9d713adb0812bb773b907f1" + "reference": "f24f8f316367b30810810d4eb30c543d7003ff3b" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/process/zipball/32241012d521e2e8a9d713adb0812bb773b907f1", - "reference": "32241012d521e2e8a9d713adb0812bb773b907f1", + "url": "https://api.github.com/repos/symfony/process/zipball/f24f8f316367b30810810d4eb30c543d7003ff3b", + "reference": "f24f8f316367b30810810d4eb30c543d7003ff3b", "shasum": "" }, "require": { @@ -8169,7 +8175,7 @@ "description": "Executes commands in sub-processes", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/process/tree/v7.3.3" + "source": "https://github.com/symfony/process/tree/v7.3.4" }, "funding": [ { @@ -8189,20 +8195,20 @@ "type": "tidelift" } ], - "time": "2025-08-18T09:42:54+00:00" + "time": "2025-09-11T10:12:26+00:00" }, { "name": "symfony/string", - "version": "v7.3.3", + "version": "v7.3.4", "source": { "type": "git", "url": "https://github.com/symfony/string.git", - "reference": "17a426cce5fd1f0901fefa9b2a490d0038fd3c9c" + "reference": "f96476035142921000338bad71e5247fbc138872" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/string/zipball/17a426cce5fd1f0901fefa9b2a490d0038fd3c9c", - "reference": "17a426cce5fd1f0901fefa9b2a490d0038fd3c9c", + "url": "https://api.github.com/repos/symfony/string/zipball/f96476035142921000338bad71e5247fbc138872", + "reference": "f96476035142921000338bad71e5247fbc138872", "shasum": "" }, "require": { @@ -8217,7 +8223,6 @@ }, "require-dev": { "symfony/emoji": "^7.1", - "symfony/error-handler": "^6.4|^7.0", "symfony/http-client": "^6.4|^7.0", "symfony/intl": "^6.4|^7.0", "symfony/translation-contracts": "^2.5|^3.0", @@ -8260,7 +8265,7 @@ "utf8" ], "support": { - "source": "https://github.com/symfony/string/tree/v7.3.3" + "source": "https://github.com/symfony/string/tree/v7.3.4" }, "funding": [ { @@ -8280,7 +8285,7 @@ "type": "tidelift" } ], - "time": "2025-08-25T06:35:40+00:00" + "time": "2025-09-11T14:36:48+00:00" }, { "name": "textalk/websocket", @@ -8512,7 +8517,7 @@ ], "aliases": [], "minimum-stability": "stable", - "stability-flags": [], + "stability-flags": {}, "prefer-stable": false, "prefer-lowest": false, "platform": { @@ -8536,5 +8541,5 @@ "platform-overrides": { "php": "8.3" }, - "plugin-api-version": "2.3.0" + "plugin-api-version": "2.6.0" }