diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index 9c9b678302..2cc4c700f7 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -141,7 +141,7 @@ jobs: run: | docker load --input /tmp/${{ env.IMAGE }}.tar docker compose up -d - sleep 10 + sleep 25 - name: Run ${{matrix.service}} Tests run: docker compose exec -T appwrite test /usr/src/code/tests/e2e/Services/${{matrix.service}} --debug diff --git a/.gitmodules b/.gitmodules index bed812bea0..e259782156 100644 --- a/.gitmodules +++ b/.gitmodules @@ -1,4 +1,4 @@ [submodule "app/console"] path = app/console url = https://github.com/appwrite/console - branch = chore-update-sdk + branch = 1.5.x diff --git a/Dockerfile b/Dockerfile index ccb8f8bac1..d7343dd71d 100755 --- a/Dockerfile +++ b/Dockerfile @@ -29,7 +29,7 @@ ENV VITE_APPWRITE_GROWTH_ENDPOINT=$VITE_APPWRITE_GROWTH_ENDPOINT RUN npm ci RUN npm run build -FROM appwrite/base:0.4.4 as final +FROM appwrite/base:0.9.0 as final LABEL maintainer="team@appwrite.io" diff --git a/app/config/locale/templates.php b/app/config/locale/templates.php index f2672c04a0..ac5a2acf1d 100644 --- a/app/config/locale/templates.php +++ b/app/config/locale/templates.php @@ -6,10 +6,12 @@ return [ 'magicSession', 'recovery', 'invitation', + 'mfaChallenge' ], 'sms' => [ 'verification', 'login', - 'invitation' + 'invitation', + 'mfaChallenge' ] ]; diff --git a/app/config/locale/templates/email-magic-url.tpl b/app/config/locale/templates/email-magic-url.tpl index 21988c5bc1..def1ea2395 100644 --- a/app/config/locale/templates/email-magic-url.tpl +++ b/app/config/locale/templates/email-magic-url.tpl @@ -1,4 +1,4 @@ -

{{hello}}

+

{{hello}},

{{optionButton}}

diff --git a/app/config/locale/templates/email-mfa-challenge.tpl b/app/config/locale/templates/email-mfa-challenge.tpl new file mode 100644 index 0000000000..cf09448ca5 --- /dev/null +++ b/app/config/locale/templates/email-mfa-challenge.tpl @@ -0,0 +1,16 @@ +

{{hello}},

+ +

{{description}}

+ + + + + +
+

{{otp}}

+
+ +

{{clientInfo}}

+ +

{{thanks}}

+

{{signature}}

diff --git a/app/config/locale/templates/email-otp.tpl b/app/config/locale/templates/email-otp.tpl index 84802c1603..9552185f84 100644 --- a/app/config/locale/templates/email-otp.tpl +++ b/app/config/locale/templates/email-otp.tpl @@ -1,4 +1,4 @@ -

{{hello}}

+

{{hello}},

{{description}}

diff --git a/app/config/locale/translations/en.json b/app/config/locale/translations/en.json index dfa5ebe32a..22a132964e 100644 --- a/app/config/locale/translations/en.json +++ b/app/config/locale/translations/en.json @@ -4,36 +4,42 @@ "settings.direction": "ltr", "emails.sender": "%s Team", "emails.verification.subject": "Account Verification", - "emails.verification.hello": "Hey {{user}}", - "emails.verification.body": "Follow this link to verify your email address.", + "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.", "emails.verification.thanks": "Thanks", "emails.verification.signature": "{{project}} team", "emails.magicSession.subject": "{{project}} Login", - "emails.magicSession.hello": "Hello,", - "emails.magicSession.optionButton": "Click the button below to securely sign in to your {{project}} account. This link will expire in 1 hour.", + "emails.magicSession.hello": "Hello {{user}}", + "emails.magicSession.optionButton": "Click the button below to securely sign in to your {{b}}{{project}}{{/b}} account. This link will expire in 1 hour.", "emails.magicSession.buttonText": "Sign in to {{project}}", "emails.magicSession.optionUrl": "If you are unable to sign in using the button above, please visit the following link:", - "emails.magicSession.clientInfo": "This sign in was requested using {{agentClient}} on {{agentDevice}} {{agentOs}}. If you didn't request the sign in, you can safely ignore this email.", - "emails.magicSession.securityPhrase": "Security phrase for this email is {{phrase}}. You can trust this email if this phrase matches the phrase shown during sign in.", + "emails.magicSession.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.", + "emails.magicSession.securityPhrase": "Security phrase for this email is {{b}}{{phrase}}{{/b}}. You can trust this email if this phrase matches the phrase shown during sign in.", "emails.magicSession.thanks": "Thanks,", "emails.magicSession.signature": "{{project}} team", "emails.otpSession.subject": "OTP for {{project}} Login", - "emails.otpSession.hello": "Hello,", - "emails.otpSession.description": "Enter the following verification code when prompted to securely sign in to your {{project}} account. This code will expire in 15 minutes.", - "emails.otpSession.clientInfo": "This sign in was requested using {{agentClient}} on {{agentDevice}} {{agentOs}}. If you didn't request the sign in, you can safely ignore this email.", - "emails.otpSession.securityPhrase": "Security phrase for this email is {{phrase}}. You can trust this email if this phrase matches the phrase shown during sign in.", + "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.", + "emails.otpSession.securityPhrase": "Security phrase for this email is {{b}}{{phrase}}{{/b}}. You can trust this email if this phrase matches the phrase shown during sign in.", "emails.otpSession.thanks": "Thanks,", "emails.otpSession.signature": "{{project}} team", + "emails.mfaChallenge.subject": "Verification Code for {{project}}", + "emails.mfaChallenge.hello": "Hello {{user}}", + "emails.mfaChallenge.description": "Enter the following verification code to verify your email and activate 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.", + "emails.mfaChallenge.thanks": "Thanks,", + "emails.mfaChallenge.signature": "{{project}} team", "emails.recovery.subject": "Password Reset", "emails.recovery.hello": "Hello {{user}}", - "emails.recovery.body": "Follow this link to reset your {{project}} password.", + "emails.recovery.body": "Follow this link to reset your {{b}}{{project}}{{/b}} password.", "emails.recovery.footer": "If you didn’t ask to reset your password, you can ignore this message.", "emails.recovery.thanks": "Thanks", "emails.recovery.signature": "{{project}} team", "emails.invitation.subject": "Invitation to %s Team at %s", - "emails.invitation.hello": "Hello", - "emails.invitation.body": "This mail was sent to you because {{owner}} wanted to invite you to become a member of the {{team}} team at {{project}}.", + "emails.invitation.hello": "Hello {{user}}", + "emails.invitation.body": "This mail was sent to you because {{b}}{{owner}}{{/b}} wanted to invite you to become a member of the {{b}}{{team}}{{/b}} team at {{b}}{{project}}{{/b}}.", "emails.invitation.footer": "If you are not interested, you can ignore this message.", "emails.invitation.thanks": "Thanks", "emails.invitation.signature": "{{project}} team", diff --git a/app/console b/app/console index 44edd461c6..c72ba12e47 160000 --- a/app/console +++ b/app/console @@ -1 +1 @@ -Subproject commit 44edd461c6036cb462047c1424b80f0903cdc15e +Subproject commit c72ba12e479b0d3d9b3f4e48e01625c12a38fd7f diff --git a/app/controllers/api/account.php b/app/controllers/api/account.php index 22d2776daf..eec259f321 100644 --- a/app/controllers/api/account.php +++ b/app/controllers/api/account.php @@ -1259,15 +1259,16 @@ App::post('/v1/account/tokens/magic-url') $emailVariables = [ 'direction' => $locale->getText('settings.direction'), - /* {{user}}, {{team}}, {{redirect}} and {{project}} are required in default and custom templates */ - 'user' => '', - 'team' => '', + // {{user}}, {{redirect}} and {{project}} are required in default and custom templates + 'user' => $user->getAttribute('name'), 'project' => $project->getAttribute('name'), 'redirect' => $url, - 'agentDevice' => '' . ( $agentDevice['deviceBrand'] ?? $agentDevice['deviceBrand'] ?? 'UNKNOWN') . '', - 'agentClient' => '' . ($agentClient['clientName'] ?? 'UNKNOWN') . '', - 'agentOs' => '' . ($agentOs['osName'] ?? 'UNKNOWN') . '', - 'phrase' => '' . (!empty($phrase) ? $phrase : '') . '' + 'agentDevice' => $agentDevice['deviceBrand'] ?? $agentDevice['deviceBrand'] ?? 'UNKNOWN', + 'agentClient' => $agentClient['clientName'] ?? 'UNKNOWN', + 'agentOs' => $agentOs['osName'] ?? 'UNKNOWN', + 'phrase' => !empty($phrase) ? $phrase : '', + // TODO: remove unnecessary team variable from this email + 'team' => '', ]; $queueForMails @@ -1487,15 +1488,16 @@ App::post('/v1/account/tokens/email') $emailVariables = [ 'direction' => $locale->getText('settings.direction'), - /* {{user}} ,{{team}}, {{project}} and {{otp}} are required in the templates */ - 'user' => '', - 'team' => '', + // {{user}}, {{project}} and {{otp}} are required in the templates + 'user' => $user->getAttribute('name'), 'project' => $project->getAttribute('name'), 'otp' => $tokenSecret, - 'agentDevice' => '' . ( $agentDevice['deviceBrand'] ?? $agentDevice['deviceBrand'] ?? 'UNKNOWN') . '', - 'agentClient' => '' . ($agentClient['clientName'] ?? 'UNKNOWN') . '', - 'agentOs' => '' . ($agentOs['osName'] ?? 'UNKNOWN') . '', - 'phrase' => '' . (!empty($phrase) ? $phrase : '') . '' + 'agentDevice' => $agentDevice['deviceBrand'] ?? $agentDevice['deviceBrand'] ?? 'UNKNOWN', + 'agentClient' => $agentClient['clientName'] ?? 'UNKNOWN', + 'agentOs' => $agentOs['osName'] ?? 'UNKNOWN', + 'phrase' => !empty($phrase) ? $phrase : '', + // TODO: remove unnecessary team variable from this email + 'team' => '', ]; $queueForMails @@ -2953,11 +2955,12 @@ App::post('/v1/account/recovery') $emailVariables = [ 'direction' => $locale->getText('settings.direction'), - /* {{user}}, {{team}}, {{redirect}} and {{project}} are required in default and custom templates */ + // {{user}}, {{redirect}} and {{project}} are required in default and custom templates 'user' => $profile->getAttribute('name'), - 'team' => '', 'redirect' => $url, - 'project' => $projectName + 'project' => $projectName, + // TODO: remove unnecessary team variable from this email + 'team' => '' ]; $queueForMails @@ -3200,11 +3203,12 @@ App::post('/v1/account/verification') $emailVariables = [ 'direction' => $locale->getText('settings.direction'), - /* {{user}}, {{team}}, {{redirect}} and {{project}} are required in default and custom templates */ + // {{user}}, {{redirect}} and {{project}} are required in default and custom templates 'user' => $user->getAttribute('name'), - 'team' => '', 'redirect' => $url, - 'project' => $projectName + 'project' => $projectName, + // TODO: remove unnecessary team variable from this email + 'team' => '', ]; $queueForMails @@ -3696,7 +3700,7 @@ App::post('/v1/account/mfa/challenge') ->label('audits.userId', '{response.userId}') ->label('sdk.auth', []) ->label('sdk.namespace', 'account') - ->label('sdk.method', 'create2FAChallenge') + ->label('sdk.method', 'createChallenge') ->label('sdk.description', '/docs/references/account/create-2fa-challenge.md') ->label('sdk.response.code', Response::STATUS_CODE_CREATED) ->label('sdk.response.type', Response::CONTENT_TYPE_JSON) @@ -3707,11 +3711,13 @@ App::post('/v1/account/mfa/challenge') ->inject('response') ->inject('dbForProject') ->inject('user') + ->inject('locale') + ->inject('project') + ->inject('request') ->inject('queueForEvents') ->inject('queueForMessaging') ->inject('queueForMails') - ->inject('locale') - ->action(function (string $factor, Response $response, Database $dbForProject, Document $user, Event $queueForEvents, Messaging $queueForMessaging, Mail $queueForMails, Locale $locale) { + ->action(function (string $factor, Response $response, Database $dbForProject, Document $user, Locale $locale, Document $project, Request $request, Event $queueForEvents, Messaging $queueForMessaging, Mail $queueForMails) { $expire = DateTime::addSeconds(new \DateTime(), Auth::TOKEN_EXPIRATION_CONFIRM); $code = Auth::codeGenerator(); @@ -3743,6 +3749,22 @@ App::post('/v1/account/mfa/challenge') throw new Exception(Exception::USER_PHONE_NOT_VERIFIED); } + $message = Template::fromFile(__DIR__ . '/../../config/locale/templates/sms-base.tpl'); + + $customTemplate = $project->getAttribute('templates', [])['sms.mfaChallenge-' . $locale->default] ?? []; + if (!empty($customTemplate)) { + $message = $customTemplate['message'] ?? $message; + } + + $messageContent = Template::fromString($locale->getText("sms.verification.body")); + $messageContent + ->setParam('{{project}}', $project->getAttribute('name')) + ->setParam('{{secret}}', $code); + $messageContent = \strip_tags($messageContent->render()); + $message = $message->setParam('{{token}}', $messageContent); + + $message = $message->render(); + $queueForMessaging ->setType(MESSAGE_SEND_TYPE_INTERNAL) ->setMessage(new Document([ @@ -3751,7 +3773,8 @@ App::post('/v1/account/mfa/challenge') 'content' => $code, ], ])) - ->setRecipients([$user->getAttribute('phone')]); + ->setRecipients([$user->getAttribute('phone')]) + ->setProviderType(MESSAGE_TYPE_SMS); break; case 'email': if (empty(App::getEnv('_APP_SMTP_HOST'))) { @@ -3764,9 +3787,85 @@ App::post('/v1/account/mfa/challenge') throw new Exception(Exception::USER_EMAIL_NOT_VERIFIED); } + $subject = $locale->getText("emails.mfaChallenge.subject"); + $customTemplate = $project->getAttribute('templates', [])['email.mfaChallenge-' . $locale->default] ?? []; + + $detector = new Detector($request->getUserAgent('UNKNOWN')); + $agentOs = $detector->getOS(); + $agentClient = $detector->getClient(); + $agentDevice = $detector->getDevice(); + + $message = Template::fromFile(__DIR__ . '/../../config/locale/templates/email-mfa-challenge.tpl'); + $message + ->setParam('{{hello}}', $locale->getText("emails.mfaChallenge.hello")) + ->setParam('{{description}}', $locale->getText("emails.mfaChallenge.description")) + ->setParam('{{clientInfo}}', $locale->getText("emails.mfaChallenge.clientInfo")) + ->setParam('{{thanks}}', $locale->getText("emails.mfaChallenge.thanks")) + ->setParam('{{signature}}', $locale->getText("emails.mfaChallenge.signature")); + + $body = $message->render(); + + $smtp = $project->getAttribute('smtp', []); + $smtpEnabled = $smtp['enabled'] ?? false; + + $senderEmail = App::getEnv('_APP_SYSTEM_EMAIL_ADDRESS', APP_EMAIL_TEAM); + $senderName = App::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 = [ + 'direction' => $locale->getText('settings.direction'), + // {{user}}, {{project}} and {{otp}} are required in the templates + 'user' => $user->getAttribute('name'), + 'project' => $project->getAttribute('name'), + 'otp' => $code, + 'agentDevice' => $agentDevice['deviceBrand'] ?? $agentDevice['deviceBrand'] ?? 'UNKNOWN', + 'agentClient' => $agentClient['clientName'] ?? 'UNKNOWN', + 'agentOs' => $agentOs['osName'] ?? 'UNKNOWN' + ]; + $queueForMails - ->setSubject("{$code} is your 6-digit code") - ->setBody($code) + ->setSubject($subject) + ->setBody($body) + ->setVariables($emailVariables) ->setRecipient($user->getAttribute('email')) ->trigger(); break; @@ -3841,6 +3940,9 @@ App::put('/v1/account/mfa/challenge') $dbForProject->updateDocument('sessions', $sessionId, $session->setAttribute('factors', $provider, Document::SET_TYPE_APPEND)); + $queueForEvents + ->setParam('userId', $user->getId()); + $response->dynamic($session, Response::MODEL_SESSION); }); diff --git a/app/controllers/api/messaging.php b/app/controllers/api/messaging.php index 1c30e07ba5..7e051ad828 100644 --- a/app/controllers/api/messaging.php +++ b/app/controllers/api/messaging.php @@ -766,11 +766,12 @@ App::post('/v1/messaging/providers/apns') ->param('authKeyId', '', new Text(0), 'APNS authentication key ID.', true) ->param('teamId', '', new Text(0), 'APNS team ID.', true) ->param('bundleId', '', new Text(0), 'APNS bundle ID.', true) + ->param('sandbox', false, new Boolean(), 'Use APNS sandbox environment.', true) ->param('enabled', null, new Boolean(), 'Set as enabled.', true) ->inject('queueForEvents') ->inject('dbForProject') ->inject('response') - ->action(function (string $providerId, string $name, string $authKey, string $authKeyId, string $teamId, string $bundleId, ?bool $enabled, Event $queueForEvents, Database $dbForProject, Response $response) { + ->action(function (string $providerId, string $name, string $authKey, string $authKeyId, string $teamId, string $bundleId, bool $sandbox, ?bool $enabled, Event $queueForEvents, Database $dbForProject, Response $response) { $providerId = $providerId == 'unique()' ? ID::unique() : $providerId; $credentials = []; @@ -803,6 +804,10 @@ App::post('/v1/messaging/providers/apns') $enabled = false; } + $options = [ + 'sandbox' => $sandbox + ]; + $provider = new Document([ '$id' => $providerId, 'name' => $name, @@ -810,6 +815,7 @@ App::post('/v1/messaging/providers/apns') 'type' => MESSAGE_TYPE_PUSH, 'enabled' => $enabled, 'credentials' => $credentials, + 'options' => $options ]); try { @@ -1808,10 +1814,11 @@ App::patch('/v1/messaging/providers/apns/:providerId') ->param('authKeyId', '', new Text(0), 'APNS authentication key ID.', true) ->param('teamId', '', new Text(0), 'APNS team ID.', true) ->param('bundleId', '', new Text(0), 'APNS bundle ID.', true) + ->param('sandbox', null, new Boolean(), 'Use APNS sandbox environment.', true) ->inject('queueForEvents') ->inject('dbForProject') ->inject('response') - ->action(function (string $providerId, string $name, ?bool $enabled, string $authKey, string $authKeyId, string $teamId, string $bundleId, Event $queueForEvents, Database $dbForProject, Response $response) { + ->action(function (string $providerId, string $name, ?bool $enabled, string $authKey, string $authKeyId, string $teamId, string $bundleId, ?bool $sandbox, Event $queueForEvents, Database $dbForProject, Response $response) { $provider = $dbForProject->getDocument('providers', $providerId); if ($provider->isEmpty()) { @@ -1847,6 +1854,14 @@ App::patch('/v1/messaging/providers/apns/:providerId') $provider->setAttribute('credentials', $credentials); + $options = $provider->getAttribute('options'); + + if (!\is_null($sandbox)) { + $options['sandbox'] = $sandbox; + } + + $provider->setAttribute('options', $options); + if (!\is_null($enabled)) { if ($enabled) { if ( @@ -2133,21 +2148,26 @@ App::patch('/v1/messaging/topics/:topicId') ->label('sdk.response.type', Response::CONTENT_TYPE_JSON) ->label('sdk.response.model', Response::MODEL_TOPIC) ->param('topicId', '', new UID(), 'Topic ID.') - ->param('name', '', new Text(128), 'Topic Name.', true) + ->param('name', null, new Text(128), 'Topic Name.', true) + ->param('subscribe', null, new Roles(APP_LIMIT_ARRAY_PARAMS_SIZE), 'An array of role strings with subscribe permission. By default all users are granted with any subscribe permission. [learn more about roles](https://appwrite.io/docs/permissions#permission-roles). Maximum of ' . APP_LIMIT_ARRAY_PARAMS_SIZE . ' roles are allowed, each 64 characters long.', true) ->inject('queueForEvents') ->inject('dbForProject') ->inject('response') - ->action(function (string $topicId, string $name, Event $queueForEvents, Database $dbForProject, Response $response) { + ->action(function (string $topicId, ?string $name, ?array $subscribe, Event $queueForEvents, Database $dbForProject, Response $response) { $topic = $dbForProject->getDocument('topics', $topicId); if ($topic->isEmpty()) { throw new Exception(Exception::TOPIC_NOT_FOUND); } - if (!empty($name)) { + if (!\is_null($name)) { $topic->setAttribute('name', $name); } + if (!\is_null($subscribe)) { + $topic->setAttribute('subscribe', $subscribe); + } + $topic = $dbForProject->updateDocument('topics', $topicId, $topic); $queueForEvents diff --git a/app/controllers/api/users.php b/app/controllers/api/users.php index e2c41d9ed1..987c146e0d 100644 --- a/app/controllers/api/users.php +++ b/app/controllers/api/users.php @@ -513,7 +513,7 @@ App::post('/v1/users/:userId/targets') Permission::delete(Role::user($user->getId())), ], 'providerId' => $providerId ?? null, - 'providerInternalId' => $provider->getInternalId() ?? null, + 'providerInternalId' => $provider->isEmpty() ? null : $provider->getInternalId(), 'providerType' => $providerType, 'userId' => $userId, 'userInternalId' => $user->getInternalId(), @@ -1662,7 +1662,7 @@ App::post('/v1/users/:userId/sessions') ->inject('queueForEvents') ->action(function (string $userId, Request $request, Response $response, Database $dbForProject, Document $project, Locale $locale, Reader $geodb, Event $queueForEvents) { $user = $dbForProject->getDocument('users', $userId); - if ($user === false || $user->isEmpty()) { + if ($user->isEmpty()) { throw new Exception(Exception::USER_NOT_FOUND); } @@ -1731,7 +1731,7 @@ App::post('/v1/users/:userId/tokens') ->action(function (string $userId, int $length, int $expire, Request $request, Response $response, Database $dbForProject, Event $queueForEvents) { $user = $dbForProject->getDocument('users', $userId); - if ($user === false || $user->isEmpty()) { + if ($user->isEmpty()) { throw new Exception(Exception::USER_NOT_FOUND); } diff --git a/composer.json b/composer.json index c0afd636f7..90cf585739 100644 --- a/composer.json +++ b/composer.json @@ -50,7 +50,7 @@ "utopia-php/cache": "0.9.*", "utopia-php/cli": "0.15.*", "utopia-php/config": "0.2.*", - "utopia-php/database": "0.48.2", + "utopia-php/database": "0.48.*", "utopia-php/domains": "0.5.*", "utopia-php/dsn": "0.2.*", "utopia-php/framework": "0.33.*", diff --git a/composer.lock b/composer.lock index 447a047d30..b7bb031c3c 100644 --- a/composer.lock +++ b/composer.lock @@ -4,7 +4,7 @@ "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "This file is @generated automatically" ], - "content-hash": "9e0e07159d27e4b86511aaab851532de", + "content-hash": "f19c09e7e233fe0f767bf5255fb46b86", "packages": [ { "name": "adhocore/jwt", @@ -65,16 +65,16 @@ }, { "name": "appwrite/appwrite", - "version": "10.1.0", + "version": "10.0.0", "source": { "type": "git", "url": "https://github.com/appwrite/sdk-for-php.git", - "reference": "da579af70723cfc117b5af84375bdef117e27312" + "reference": "461eedf4efd502dc905c3055f36f0e3583f67390" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/appwrite/sdk-for-php/zipball/da579af70723cfc117b5af84375bdef117e27312", - "reference": "da579af70723cfc117b5af84375bdef117e27312", + "url": "https://api.github.com/repos/appwrite/sdk-for-php/zipball/461eedf4efd502dc905c3055f36f0e3583f67390", + "reference": "461eedf4efd502dc905c3055f36f0e3583f67390", "shasum": "" }, "require": { @@ -99,10 +99,10 @@ "support": { "email": "team@appwrite.io", "issues": "https://github.com/appwrite/sdk-for-php/issues", - "source": "https://github.com/appwrite/sdk-for-php/tree/10.1.0", + "source": "https://github.com/appwrite/sdk-for-php/tree/10.0.0", "url": "https://appwrite.io/support" }, - "time": "2023-11-20T09:56:12+00:00" + "time": "2023-09-07T23:28:31+00:00" }, { "name": "appwrite/php-clamav", @@ -1552,16 +1552,16 @@ }, { "name": "utopia-php/database", - "version": "0.48.2", + "version": "0.48.4", "source": { "type": "git", "url": "https://github.com/utopia-php/database.git", - "reference": "0a231a2874fdbc0cf2ae2170b3f132fdee0ddfd4" + "reference": "02f20bd901b8fab26d7dc2c58f7da1d6a08d21c0" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/utopia-php/database/zipball/0a231a2874fdbc0cf2ae2170b3f132fdee0ddfd4", - "reference": "0a231a2874fdbc0cf2ae2170b3f132fdee0ddfd4", + "url": "https://api.github.com/repos/utopia-php/database/zipball/02f20bd901b8fab26d7dc2c58f7da1d6a08d21c0", + "reference": "02f20bd901b8fab26d7dc2c58f7da1d6a08d21c0", "shasum": "" }, "require": { @@ -1569,7 +1569,7 @@ "ext-pdo": "*", "php": ">=8.0", "utopia-php/cache": "0.9.*", - "utopia-php/framework": "0.*.*", + "utopia-php/framework": "0.33.*", "utopia-php/mongo": "0.3.*" }, "require-dev": { @@ -1602,9 +1602,9 @@ ], "support": { "issues": "https://github.com/utopia-php/database/issues", - "source": "https://github.com/utopia-php/database/tree/0.48.2" + "source": "https://github.com/utopia-php/database/tree/0.48.4" }, - "time": "2024-02-02T14:10:14+00:00" + "time": "2024-02-23T03:22:55+00:00" }, { "name": "utopia-php/domains", @@ -1962,20 +1962,20 @@ }, { "name": "utopia-php/migration", - "version": "dev-dev", + "version": "0.3.6", "source": { "type": "git", "url": "https://github.com/utopia-php/migration.git", - "reference": "0847fad35006c16f2aa572c4fa890cc8a0e7f8f2" + "reference": "f78273b38bade23db5866e5c7cb5f55427ba82af" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/utopia-php/migration/zipball/0847fad35006c16f2aa572c4fa890cc8a0e7f8f2", - "reference": "0847fad35006c16f2aa572c4fa890cc8a0e7f8f2", + "url": "https://api.github.com/repos/utopia-php/migration/zipball/f78273b38bade23db5866e5c7cb5f55427ba82af", + "reference": "f78273b38bade23db5866e5c7cb5f55427ba82af", "shasum": "" }, "require": { - "appwrite/appwrite": "10.1.0", + "appwrite/appwrite": "10.0.*", "php": "8.*", "utopia-php/cli": "0.*" }, @@ -1990,19 +1990,7 @@ "Utopia\\Migration\\": "src/Migration" } }, - "autoload-dev": { - "psr-4": { - "Utopia\\Tests\\": "tests/Migration" - } - }, - "scripts": { - "lint": [ - "./vendor/bin/pint --test" - ], - "format": [ - "./vendor/bin/pint" - ] - }, + "notification-url": "https://packagist.org/downloads/", "license": [ "MIT" ], @@ -2015,10 +2003,10 @@ "utopia" ], "support": { - "source": "https://github.com/utopia-php/migration/tree/dev", - "issues": "https://github.com/utopia-php/migration/issues" + "issues": "https://github.com/utopia-php/migration/issues", + "source": "https://github.com/utopia-php/migration/tree/0.3.6" }, - "time": "2024-02-23T12:02:32+00:00" + "time": "2023-11-02T15:13:03+00:00" }, { "name": "utopia-php/mongo", @@ -5461,9 +5449,7 @@ ], "aliases": [], "minimum-stability": "stable", - "stability-flags": { - "utopia-php/migration": 20 - }, + "stability-flags": [], "prefer-stable": false, "prefer-lowest": false, "platform": { diff --git a/src/Appwrite/Migration/Version/V20.php b/src/Appwrite/Migration/Version/V20.php index 0e7434c56c..0ef899588e 100644 --- a/src/Appwrite/Migration/Version/V20.php +++ b/src/Appwrite/Migration/Version/V20.php @@ -30,31 +30,34 @@ class V20 extends Migration foreach (['subQueryIndexes', 'subQueryPlatforms', 'subQueryDomains', 'subQueryKeys', 'subQueryWebhooks', 'subQuerySessions', 'subQueryTokens', 'subQueryMemberships', 'subQueryVariables', 'subQueryChallenges', 'subQueryProjectVariables', 'subQueryTargets', 'subQueryTopicTargets'] as $name) { Database::addFilter( $name, - fn() => null, - fn() => [] + fn () => null, + fn () => [] ); } - $this->migrateUsageMetrics('project.$all.network.requests', 'network.requests'); - $this->migrateUsageMetrics('project.$all.network.outbound', 'network.outbound'); - $this->migrateUsageMetrics('project.$all.network.inbound', 'network.inbound'); - $this->migrateUsageMetrics('users.$all.count.total', 'users'); - $this->migrateSessionsMetric(); - Console::log('Migrating Project: ' . $this->project->getAttribute('name') . ' (' . $this->project->getId() . ')'); $this->projectDB->setNamespace("_{$this->project->getInternalId()}"); Console::info('Migrating Collections'); $this->migrateCollections(); - Console::info('Migrating Functions'); - $this->migrateFunctions(); + // No need to migrate stats for console + if ($this->project->getInternalId() !== 'console') { + $this->migrateUsageMetrics('project.$all.network.requests', 'network.requests'); + $this->migrateUsageMetrics('project.$all.network.outbound', 'network.outbound'); + $this->migrateUsageMetrics('project.$all.network.inbound', 'network.inbound'); + $this->migrateUsageMetrics('users.$all.count.total', 'users'); + $this->migrateSessionsMetric(); - Console::info('Migrating Databases'); - $this->migrateDatabases(); + Console::info('Migrating Functions'); + $this->migrateFunctions(); - Console::info('Migrating Buckets'); - $this->migrateBuckets(); + Console::info('Migrating Databases'); + $this->migrateDatabases(); + + Console::info('Migrating Buckets'); + $this->migrateBuckets(); + } Console::info('Migrating Documents'); $this->forEachDocument([$this, 'fixDocument']); @@ -75,25 +78,27 @@ class V20 extends Migration }; // Support database array type migration (user collections) - foreach ( - $this->documentsIterator('attributes', [ - Query::equal('array', [true]), - ]) as $attribute - ) { - $foundIndex = false; + if ($collectionType === 'projects') { foreach ( - $this->documentsIterator('indexes', [ - Query::equal('databaseInternalId', [$attribute['databaseInternalId']]), - Query::equal('collectionInternalId', [$attribute['collectionInternalId']]), - ]) as $index + $this->documentsIterator('attributes', [ + Query::equal('array', [true]), + ]) as $attribute ) { - if (in_array($attribute['key'], $index['attributes'])) { - $this->projectDB->deleteIndex($index['collectionId'], $index['$id']); - $foundIndex = true; + $foundIndex = false; + foreach ( + $this->documentsIterator('indexes', [ + Query::equal('databaseInternalId', [$attribute['databaseInternalId']]), + Query::equal('collectionInternalId', [$attribute['collectionInternalId']]), + ]) as $index + ) { + if (in_array($attribute['key'], $index['attributes'])) { + $this->projectDB->deleteIndex($index['collectionId'], $index['$id']); + $foundIndex = true; + } + } + if ($foundIndex === true) { + $this->projectDB->updateAttribute($attribute['collectionInternalId'], $attribute['key'], $attribute['type']); } - } - if ($foundIndex === true) { - $this->projectDB->updateAttribute($attribute['collectionInternalId'], $attribute['key'], $attribute['type']); } } @@ -323,7 +328,6 @@ class V20 extends Migration */ protected function createInfMetric(string $metric, int $value): void { - try { /** * Creating inf metric @@ -351,7 +355,6 @@ class V20 extends Migration */ protected function migrateUsageMetrics(string $from, string $to): void { - /** * inf metric */ @@ -411,7 +414,6 @@ class V20 extends Migration */ private function migrateFunctions(): void { - $this->migrateUsageMetrics('deployment.$all.storage.size', 'deployments.storage'); $this->migrateUsageMetrics('builds.$all.compute.total', 'builds'); $this->migrateUsageMetrics('builds.$all.compute.time', 'builds.compute'); @@ -539,6 +541,14 @@ class V20 extends Migration $duration = $this->project->getAttribute('auths', [])['duration'] ?? Auth::TOKEN_EXPIRATION_LOGIN_LONG; $expire = DateTime::addSeconds(new \DateTime(), $duration); $document->setAttribute('expire', $expire); + + $factors = match ($document->getAttribute('provider')) { + Auth::SESSION_PROVIDER_ANONYMOUS => ['anonymous'], + Auth::SESSION_PROVIDER_PHONE => ['phone'], + default => ['password'], + }; + + $document->setAttribute('factors', $factors); break; } return $document; diff --git a/src/Appwrite/Platform/Workers/Mails.php b/src/Appwrite/Platform/Workers/Mails.php index ac9c44c3b9..57d1baa978 100644 --- a/src/Appwrite/Platform/Workers/Mails.php +++ b/src/Appwrite/Platform/Workers/Mails.php @@ -32,6 +32,14 @@ class Mails extends Action ->callback(fn (Message $message, Registry $register, Log $log) => $this->action($message, $register, $log)); } + /** + * @var array + */ + protected array $richTextParams = [ + 'b' => '', + '/b' => '', + ]; + /** * @param Message $message * @param Registry $register @@ -81,6 +89,9 @@ class Mails extends Action // TODO: hotfix for redirect param $bodyTemplate->setParam('{{' . $key . '}}', $value, escapeHtml: $key !== 'redirect'); } + foreach ($this->richTextParams as $key => $value) { + $bodyTemplate->setParam('{{' . $key . '}}', $value, escapeHtml: false); + } $body = $bodyTemplate->render(); $subjectTemplate = Template::fromString($subject); diff --git a/src/Appwrite/Platform/Workers/Messaging.php b/src/Appwrite/Platform/Workers/Messaging.php index 62e58db11a..aacd49f94a 100644 --- a/src/Appwrite/Platform/Workers/Messaging.php +++ b/src/Appwrite/Platform/Workers/Messaging.php @@ -469,6 +469,7 @@ class Messaging extends Action private function getPushAdapter(Document $provider): ?PushAdapter { $credentials = $provider->getAttribute('credentials'); + $options = $provider->getAttribute('options'); return match ($provider->getAttribute('provider')) { 'mock' => new Mock('username', 'password'), @@ -477,6 +478,7 @@ class Messaging extends Action $credentials['authKeyId'], $credentials['teamId'], $credentials['bundleId'], + $options['sandbox'] ), 'fcm' => new FCM(\json_encode($credentials['serviceAccountJSON'])), default => null diff --git a/src/Appwrite/Specification/Format.php b/src/Appwrite/Specification/Format.php index c58d69fcb9..70a86e4f5a 100644 --- a/src/Appwrite/Specification/Format.php +++ b/src/Appwrite/Specification/Format.php @@ -116,6 +116,7 @@ abstract class Format case 'account': switch ($method) { case 'createOAuth2Session': + case 'createOAuth2Token': switch ($param) { case 'provider': return 'OAuthProvider'; @@ -219,11 +220,11 @@ abstract class Format return 'MessageStatus'; } break; - case 'createSMTPProvider': - case 'updateSMTPProvider': + case 'createSmtpProvider': + case 'updateSmtpProvider': switch ($param) { case 'encryption': - return 'SMTPEncryption'; + return 'SmtpEncryption'; } break; } diff --git a/src/Appwrite/Utopia/Request/Filters/V17.php b/src/Appwrite/Utopia/Request/Filters/V17.php index 246b52deae..d7a4039d7e 100644 --- a/src/Appwrite/Utopia/Request/Filters/V17.php +++ b/src/Appwrite/Utopia/Request/Filters/V17.php @@ -59,6 +59,10 @@ class V17 extends Filter private function convertOldQueries(array $content): array { + if (!isset($content['queries'])) { + return $content; + } + $parsed = []; foreach ($content['queries'] as $query) { try { diff --git a/tests/e2e/Services/Account/AccountCustomClientTest.php b/tests/e2e/Services/Account/AccountCustomClientTest.php index ba525f556e..d3fc13bd17 100644 --- a/tests/e2e/Services/Account/AccountCustomClientTest.php +++ b/tests/e2e/Services/Account/AccountCustomClientTest.php @@ -2226,7 +2226,7 @@ class AccountCustomClientTest extends Scope /** * @depends testUpdatePhone */ - #[Retry(count: 2)] + #[Retry(count: 3)] public function testPhoneVerification(array $data): array { $session = $data['session'] ?? ''; diff --git a/tests/e2e/Services/Databases/DatabasesBase.php b/tests/e2e/Services/Databases/DatabasesBase.php index 7f23705a1a..a0823f054f 100644 --- a/tests/e2e/Services/Databases/DatabasesBase.php +++ b/tests/e2e/Services/Databases/DatabasesBase.php @@ -2973,7 +2973,7 @@ trait DatabasesBase $this->assertEquals('Invalid document structure: Attribute "floatRange" has invalid format. Value must be a valid range between 1 and 1', $badFloatRange['body']['message']); $this->assertEquals('Invalid document structure: Attribute "probability" has invalid format. Value must be a valid range between 0 and 1', $badProbability['body']['message']); $this->assertEquals('Invalid document structure: Attribute "upperBound" has invalid format. Value must be a valid range between -9,223,372,036,854,775,808 and 10', $tooHigh['body']['message']); - $this->assertEquals('Invalid document structure: Attribute "lowerBound" has invalid format. Value must be a valid range between 5 and 9,223,372,036,854,775,808', $tooLow['body']['message']); + $this->assertEquals('Invalid document structure: Attribute "lowerBound" has invalid format. Value must be a valid range between 5 and 9,223,372,036,854,775,807', $tooLow['body']['message']); } /** diff --git a/tests/e2e/Services/Functions/FunctionsCustomClientTest.php b/tests/e2e/Services/Functions/FunctionsCustomClientTest.php index 8a5031f6ae..12dd7cda59 100644 --- a/tests/e2e/Services/Functions/FunctionsCustomClientTest.php +++ b/tests/e2e/Services/Functions/FunctionsCustomClientTest.php @@ -135,7 +135,7 @@ class FunctionsCustomClientTest extends Scope \sleep(1); } - $this->assertEquals('ready', $deployment['body']['status']); + $this->assertEquals('ready', $deployment['body']['status'], \json_encode($deployment['body'])); $function = $this->client->call(Client::METHOD_PATCH, '/functions/' . $function['body']['$id'] . '/deployments/' . $deploymentId, [ 'content-type' => 'application/json', @@ -266,7 +266,7 @@ class FunctionsCustomClientTest extends Scope \sleep(1); } - $this->assertEquals('ready', $deployment['body']['status']); + $this->assertEquals('ready', $deployment['body']['status'], \json_encode($deployment['body'])); $function = $this->client->call(Client::METHOD_PATCH, '/functions/' . $functionId . '/deployments/' . $deploymentId, [ 'content-type' => 'application/json', diff --git a/tests/e2e/Services/GraphQL/Base.php b/tests/e2e/Services/GraphQL/Base.php index 01153cdd04..f8c61186ab 100644 --- a/tests/e2e/Services/GraphQL/Base.php +++ b/tests/e2e/Services/GraphQL/Base.php @@ -1812,8 +1812,8 @@ trait Base } }'; case self::$CREATE_SMTP_PROVIDER: - return 'mutation createSMTPProvider($providerId: String!, $name: String!, $host: String!, $port: Int!, $username: String!, $password: String!, $encryption: String!, $autoTLS: Boolean! $fromName: String!, $fromEmail: String!, $replyToName: String, $replyToEmail: String) { - messagingCreateSMTPProvider(providerId: $providerId, name: $name, host: $host, port: $port, username: $username, password: $password, encryption: $encryption, autoTLS: $autoTLS, fromName: $fromName, fromEmail: $fromEmail, replyToName: $replyToName, replyToEmail: $replyToEmail) { + return 'mutation createSmtpProvider($providerId: String!, $name: String!, $host: String!, $port: Int!, $username: String!, $password: String!, $encryption: String!, $autoTLS: Boolean! $fromName: String!, $fromEmail: String!, $replyToName: String, $replyToEmail: String) { + messagingCreateSmtpProvider(providerId: $providerId, name: $name, host: $host, port: $port, username: $username, password: $password, encryption: $encryption, autoTLS: $autoTLS, fromName: $fromName, fromEmail: $fromEmail, replyToName: $replyToName, replyToEmail: $replyToEmail) { _id name provider @@ -1936,8 +1936,8 @@ trait Base } }'; case self::$UPDATE_SMTP_PROVIDER: - return 'mutation updateSMTPProvider($providerId: String!, $name: String!, $host: String!, $port: Int!, $username: String!, $password: String!, $encryption: String!, $autoTLS: Boolean!, $fromName: String, $fromEmail: String, $enabled: Boolean) { - messagingUpdateSMTPProvider(providerId: $providerId, name: $name, host: $host, port: $port, username: $username, password: $password, encryption: $encryption, autoTLS: $autoTLS, fromName: $fromName, fromEmail: $fromEmail, enabled: $enabled) { + return 'mutation updateSmtpProvider($providerId: String!, $name: String!, $host: String!, $port: Int!, $username: String!, $password: String!, $encryption: String!, $autoTLS: Boolean!, $fromName: String, $fromEmail: String, $enabled: Boolean) { + messagingUpdateSmtpProvider(providerId: $providerId, name: $name, host: $host, port: $port, username: $username, password: $password, encryption: $encryption, autoTLS: $autoTLS, fromName: $fromName, fromEmail: $fromEmail, enabled: $enabled) { _id name provider diff --git a/tests/e2e/Services/Messaging/MessagingBase.php b/tests/e2e/Services/Messaging/MessagingBase.php index 25bf985692..be8499ce07 100644 --- a/tests/e2e/Services/Messaging/MessagingBase.php +++ b/tests/e2e/Services/Messaging/MessagingBase.php @@ -98,16 +98,22 @@ trait MessagingBase ]; $providers = []; - foreach (\array_keys($providersParams) as $key) { + foreach ($providersParams as $key => $params) { $response = $this->client->call(Client::METHOD_POST, '/messaging/providers/' . $key, \array_merge([ 'content-type' => 'application/json', 'x-appwrite-project' => $this->getProject()['$id'], 'x-appwrite-key' => $this->getProject()['apiKey'], - ]), $providersParams[$key]); + ]), $params); $this->assertEquals(201, $response['headers']['status-code']); - $this->assertEquals($providersParams[$key]['name'], $response['body']['name']); - \array_push($providers, $response['body']); + $this->assertEquals($params['name'], $response['body']['name']); + $providers[] = $response['body']; + + switch ($key) { + case 'apns': + $this->assertEquals(false, $response['body']['options']['sandbox']); + break; + } } return $providers; @@ -341,6 +347,17 @@ trait MessagingBase $this->assertEquals(200, $response['headers']['status-code']); $this->assertEquals('android-app', $response['body']['name']); + $response2 = $this->client->call(Client::METHOD_PATCH, '/messaging/topics/' . $topics['private']['$id'], [ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + 'x-appwrite-key' => $this->getProject()['apiKey'], + ], [ + 'name' => 'ios-app', + 'subscribe' => [Role::user('some-user')->toString()], + ]); + $this->assertEquals(200, $response2['headers']['status-code']); + $this->assertEquals('ios-app', $response2['body']['name']); + return $response['body']['$id']; } diff --git a/tests/unit/General/ExtensionsTest.php b/tests/unit/General/ExtensionsTest.php index d638cfffa6..f415816e56 100644 --- a/tests/unit/General/ExtensionsTest.php +++ b/tests/unit/General/ExtensionsTest.php @@ -21,11 +21,6 @@ class ExtensionsTest extends TestCase $this->assertEquals(true, extension_loaded('yaml')); } - public function testOPCache(): void - { - $this->assertEquals(true, extension_loaded('Zend OPcache')); - } - public function testDOM(): void { $this->assertEquals(true, extension_loaded('dom'));