mirror of
https://github.com/appwrite/appwrite.git
synced 2026-05-26 13:51:13 +00:00
Merge branch '1.5.x' of github.com:appwrite/appwrite into 1.5.x-response-request-models
This commit is contained in:
@@ -99,3 +99,4 @@ _APP_ASSISTANT_OPENAI_API_KEY=
|
||||
_APP_MESSAGE_SMS_TEST_DSN=
|
||||
_APP_MESSAGE_EMAIL_TEST_DSN=
|
||||
_APP_MESSAGE_PUSH_TEST_DSN=
|
||||
_APP_WEBHOOK_MAX_FAILED_ATTEMPTS=10
|
||||
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@@ -8,7 +8,7 @@ use Appwrite\Auth\Validator\Phone;
|
||||
use Appwrite\Detector\Detector;
|
||||
use Appwrite\Event\Event;
|
||||
use Appwrite\Event\Mail;
|
||||
use Appwrite\Auth\SecurityPhrase;
|
||||
use Appwrite\Auth\Phrase;
|
||||
use Appwrite\Extend\Exception;
|
||||
use Appwrite\Network\Validator\Email;
|
||||
use Utopia\Validator\Host;
|
||||
@@ -990,7 +990,7 @@ App::post('/v1/account/tokens/magic-url')
|
||||
->param('userId', '', new CustomId(), 'User ID. Choose a custom ID or generate a random ID with `ID.unique()`. Valid chars are a-z, A-Z, 0-9, period, hyphen, and underscore. Can\'t start with a special char. Max length is 36 chars.')
|
||||
->param('email', '', new Email(), 'User email.')
|
||||
->param('url', '', fn($clients) => new Host($clients), 'URL to redirect the user back to your app from the magic URL login. Only URLs from hostnames in your project platform list are allowed. This requirement helps to prevent an [open redirect](https://cheatsheetseries.owasp.org/cheatsheets/Unvalidated_Redirects_and_Forwards_Cheat_Sheet.html) attack against your project API.', true, ['clients'])
|
||||
->param('securityPhrase', false, new Boolean(), 'Toggle for security phrase. If enabled, email will be send with a randomly generated phrase and the phrase will also be included in the response. Confirming phrases match increases the security of authentication flow.', true)
|
||||
->param('phrase', false, new Boolean(), 'Toggle for security phrase. If enabled, email will be send with a randomly generated phrase and the phrase will also be included in the response. Confirming phrases match increases the security of your authentication flow.', true)
|
||||
->inject('request')
|
||||
->inject('response')
|
||||
->inject('user')
|
||||
@@ -999,14 +999,14 @@ App::post('/v1/account/tokens/magic-url')
|
||||
->inject('locale')
|
||||
->inject('queueForEvents')
|
||||
->inject('queueForMails')
|
||||
->action(function (string $userId, string $email, string $url, bool $securityPhrase, Request $request, Response $response, Document $user, Document $project, Database $dbForProject, Locale $locale, Event $queueForEvents, Mail $queueForMails) {
|
||||
->action(function (string $userId, string $email, string $url, bool $phrase, Request $request, Response $response, Document $user, Document $project, Database $dbForProject, Locale $locale, Event $queueForEvents, Mail $queueForMails) {
|
||||
|
||||
if (empty(App::getEnv('_APP_SMTP_HOST'))) {
|
||||
throw new Exception(Exception::GENERAL_SMTP_DISABLED, 'SMTP disabled');
|
||||
}
|
||||
|
||||
if ($securityPhrase === true) {
|
||||
$securityPhrase = SecurityPhrase::generate();
|
||||
if ($phrase === true) {
|
||||
$phrase = Phrase::generate();
|
||||
}
|
||||
|
||||
$roles = Authorization::getRoles();
|
||||
@@ -1116,7 +1116,7 @@ App::post('/v1/account/tokens/magic-url')
|
||||
->setParam('{{thanks}}', $locale->getText("emails.magicSession.thanks"))
|
||||
->setParam('{{signature}}', $locale->getText("emails.magicSession.signature"));
|
||||
|
||||
if (!empty($securityPhrase)) {
|
||||
if (!empty($phrase)) {
|
||||
$message->setParam('{{securityPhrase}}', $locale->getText("emails.magicSession.securityPhrase"));
|
||||
} else {
|
||||
$message->setParam('{{securityPhrase}}', '');
|
||||
@@ -1180,7 +1180,7 @@ App::post('/v1/account/tokens/magic-url')
|
||||
'agentDevice' => '<strong>' . ( $agentDevice['deviceBrand'] ?? $agentDevice['deviceBrand'] ?? 'UNKNOWN') . '</strong>',
|
||||
'agentClient' => '<strong>' . ($agentClient['clientName'] ?? 'UNKNOWN') . '</strong>',
|
||||
'agentOs' => '<strong>' . ($agentOs['osName'] ?? 'UNKNOWN') . '</strong>',
|
||||
'phrase' => '<strong>' . (!empty($securityPhrase) ? $securityPhrase : '') . '</strong>'
|
||||
'phrase' => '<strong>' . (!empty($phrase) ? $phrase : '') . '</strong>'
|
||||
];
|
||||
|
||||
$queueForMails
|
||||
@@ -1200,8 +1200,8 @@ App::post('/v1/account/tokens/magic-url')
|
||||
// Hide secret for clients
|
||||
$token->setAttribute('secret', ($isPrivilegedUser || $isAppUser) ? $tokenSecret : '');
|
||||
|
||||
if (!empty($securityPhrase)) {
|
||||
$token->setAttribute('securityPhrase', $securityPhrase);
|
||||
if (!empty($phrase)) {
|
||||
$token->setAttribute('phrase', $phrase);
|
||||
}
|
||||
|
||||
$response
|
||||
@@ -1229,7 +1229,7 @@ App::post('/v1/account/tokens/email')
|
||||
->label('abuse-key', 'url:{url},email:{param-email}')
|
||||
->param('userId', '', new CustomId(), 'User ID. Choose a custom ID or generate a random ID with `ID.unique()`. Valid chars are a-z, A-Z, 0-9, period, hyphen, and underscore. Can\'t start with a special char. Max length is 36 chars.')
|
||||
->param('email', '', new Email(), 'User email.')
|
||||
->param('securityPhrase', false, new Boolean(), 'Toggle for security phrase. If enabled, email will be send with a randomly generated phrase and the phrase will also be included in the response. Confirming phrases match increases the security of authentication flow.', true)
|
||||
->param('phrase', false, new Boolean(), 'Toggle for security phrase. If enabled, email will be send with a randomly generated phrase and the phrase will also be included in the response. Confirming phrases match increases the security of your authentication flow.', true)
|
||||
->inject('request')
|
||||
->inject('response')
|
||||
->inject('user')
|
||||
@@ -1238,13 +1238,13 @@ App::post('/v1/account/tokens/email')
|
||||
->inject('locale')
|
||||
->inject('queueForEvents')
|
||||
->inject('queueForMails')
|
||||
->action(function (string $userId, string $email, bool $securityPhrase, Request $request, Response $response, Document $user, Document $project, Database $dbForProject, Locale $locale, Event $queueForEvents, Mail $queueForMails) {
|
||||
->action(function (string $userId, string $email, bool $phrase, Request $request, Response $response, Document $user, Document $project, Database $dbForProject, Locale $locale, Event $queueForEvents, Mail $queueForMails) {
|
||||
if (empty(App::getEnv('_APP_SMTP_HOST'))) {
|
||||
throw new Exception(Exception::GENERAL_SMTP_DISABLED, 'SMTP disabled');
|
||||
}
|
||||
|
||||
if ($securityPhrase === true) {
|
||||
$securityPhrase = SecurityPhrase::generate();
|
||||
if ($phrase === true) {
|
||||
$phrase = Phrase::generate();
|
||||
}
|
||||
|
||||
$roles = Authorization::getRoles();
|
||||
@@ -1344,7 +1344,7 @@ App::post('/v1/account/tokens/email')
|
||||
->setParam('{{thanks}}', $locale->getText("emails.otpSession.thanks"))
|
||||
->setParam('{{signature}}', $locale->getText("emails.otpSession.signature"));
|
||||
|
||||
if (!empty($securityPhrase)) {
|
||||
if (!empty($phrase)) {
|
||||
$message->setParam('{{securityPhrase}}', $locale->getText("emails.otpSession.securityPhrase"));
|
||||
} else {
|
||||
$message->setParam('{{securityPhrase}}', '');
|
||||
@@ -1408,7 +1408,7 @@ App::post('/v1/account/tokens/email')
|
||||
'agentDevice' => '<strong>' . ( $agentDevice['deviceBrand'] ?? $agentDevice['deviceBrand'] ?? 'UNKNOWN') . '</strong>',
|
||||
'agentClient' => '<strong>' . ($agentClient['clientName'] ?? 'UNKNOWN') . '</strong>',
|
||||
'agentOs' => '<strong>' . ($agentOs['osName'] ?? 'UNKNOWN') . '</strong>',
|
||||
'phrase' => '<strong>' . (!empty($securityPhrase) ? $securityPhrase : '') . '</strong>'
|
||||
'phrase' => '<strong>' . (!empty($phrase) ? $phrase : '') . '</strong>'
|
||||
];
|
||||
|
||||
$queueForMails
|
||||
@@ -1428,8 +1428,8 @@ App::post('/v1/account/tokens/email')
|
||||
// Hide secret for clients
|
||||
$token->setAttribute('secret', ($isPrivilegedUser || $isAppUser) ? $tokenSecret : '');
|
||||
|
||||
if (!empty($securityPhrase)) {
|
||||
$token->setAttribute('securityPhrase', $securityPhrase);
|
||||
if (!empty($phrase)) {
|
||||
$token->setAttribute('phrase', $phrase);
|
||||
}
|
||||
|
||||
$response
|
||||
@@ -2035,6 +2035,9 @@ App::get('/v1/account')
|
||||
->inject('response')
|
||||
->inject('user')
|
||||
->action(function (Response $response, Document $user) {
|
||||
if ($user->isEmpty()) {
|
||||
throw new Exception(Exception::USER_NOT_FOUND);
|
||||
}
|
||||
|
||||
$response->dynamic($user, Response::MODEL_ACCOUNT);
|
||||
});
|
||||
|
||||
@@ -35,6 +35,7 @@ use Utopia\Validator\ArrayList;
|
||||
use Utopia\Validator\Boolean;
|
||||
use Utopia\Validator\Integer;
|
||||
use Utopia\Validator\JSON;
|
||||
use Utopia\Validator\Range;
|
||||
use Utopia\Validator\Text;
|
||||
use MaxMind\Db\Reader;
|
||||
use Utopia\Database\DateTime;
|
||||
@@ -61,30 +62,20 @@ App::post('/v1/messaging/providers/mailgun')
|
||||
->param('apiKey', '', new Text(0), 'Mailgun API Key.', true)
|
||||
->param('domain', '', new Text(0), 'Mailgun Domain.', true)
|
||||
->param('isEuRegion', null, new Boolean(), 'Set as EU region.', true)
|
||||
->param('enabled', null, new Boolean(), 'Set as enabled.', true)
|
||||
->param('fromName', '', new Text(128, 0), 'Sender Name.', true)
|
||||
->param('fromEmail', '', new Email(), 'Sender email address.', true)
|
||||
->param('replyToName', '', new Text(128, 0), 'Name set in the reply to field for the mail. Default value is sender name. Reply to name must have reply to email as well.', true)
|
||||
->param('replyToEmail', '', new Email(), 'Email set in the reply to field for the mail. Default value is sender email. Reply to email must have reply to name as well.', true)
|
||||
->param('enabled', null, new Boolean(), 'Set as enabled.', true)
|
||||
->inject('queueForEvents')
|
||||
->inject('dbForProject')
|
||||
->inject('response')
|
||||
->action(function (string $providerId, string $name, string $apiKey, string $domain, ?bool $isEuRegion, ?bool $enabled, string $fromName, string $fromEmail, string $replyToName, string $replyToEmail, Event $queueForEvents, Database $dbForProject, Response $response) {
|
||||
->action(function (string $providerId, string $name, string $apiKey, string $domain, ?bool $isEuRegion, string $fromName, string $fromEmail, string $replyToName, string $replyToEmail, ?bool $enabled, Event $queueForEvents, Database $dbForProject, Response $response) {
|
||||
$providerId = $providerId == 'unique()' ? ID::unique() : $providerId;
|
||||
|
||||
$options = [
|
||||
'fromName' => $fromName,
|
||||
'fromEmail' => $fromEmail,
|
||||
];
|
||||
|
||||
if (!empty($replyToName) && !empty($replyToEmail)) {
|
||||
$options['replyToName'] = $replyToName;
|
||||
$options['replyToEmail'] = $replyToEmail;
|
||||
}
|
||||
|
||||
$credentials = [];
|
||||
|
||||
if ($isEuRegion === true || $isEuRegion === false) {
|
||||
if (!\is_null($isEuRegion)) {
|
||||
$credentials['isEuRegion'] = $isEuRegion;
|
||||
}
|
||||
|
||||
@@ -96,12 +87,19 @@ App::post('/v1/messaging/providers/mailgun')
|
||||
$credentials['domain'] = $domain;
|
||||
}
|
||||
|
||||
$options = [
|
||||
'fromName' => $fromName,
|
||||
'fromEmail' => $fromEmail,
|
||||
'replyToName' => $replyToName,
|
||||
'replyToEmail' => $replyToEmail,
|
||||
];
|
||||
|
||||
if (
|
||||
$enabled === true &&
|
||||
\array_key_exists('isEuRegion', $credentials) &&
|
||||
\array_key_exists('apiKey', $credentials) &&
|
||||
\array_key_exists('domain', $credentials) &&
|
||||
\array_key_exists('fromEmail', $options)
|
||||
$enabled === true
|
||||
&& !empty($fromEmail)
|
||||
&& \array_key_exists('isEuRegion', $credentials)
|
||||
&& \array_key_exists('apiKey', $credentials)
|
||||
&& \array_key_exists('domain', $credentials)
|
||||
) {
|
||||
$enabled = true;
|
||||
} else {
|
||||
@@ -149,37 +147,34 @@ App::post('/v1/messaging/providers/sendgrid')
|
||||
->param('providerId', '', new CustomId(), 'Provider ID. Choose a custom ID or generate a random ID with `ID.unique()`. Valid chars are a-z, A-Z, 0-9, period, hyphen, and underscore. Can\'t start with a special char. Max length is 36 chars.')
|
||||
->param('name', '', new Text(128), 'Provider name.')
|
||||
->param('apiKey', '', new Text(0), 'Sendgrid API key.', true)
|
||||
->param('enabled', null, new Boolean(), 'Set as enabled.', true)
|
||||
->param('fromName', '', new Text(128, 0), 'Sender Name.', true)
|
||||
->param('fromEmail', '', new Email(), 'Sender email address.', true)
|
||||
->param('replyToName', '', new Text(128, 0), 'Name set in the reply to field for the mail. Default value is sender name.', true)
|
||||
->param('replyToEmail', '', new Email(), 'Email set in the reply to field for the mail. Default value is sender email.', true)
|
||||
->param('enabled', null, new Boolean(), 'Set as enabled.', true)
|
||||
->inject('queueForEvents')
|
||||
->inject('dbForProject')
|
||||
->inject('response')
|
||||
->action(function (string $providerId, string $name, string $apiKey, ?bool $enabled, string $fromName, string $fromEmail, string $replyToName, string $replyToEmail, Event $queueForEvents, Database $dbForProject, Response $response) {
|
||||
->action(function (string $providerId, string $name, string $apiKey, string $fromName, string $fromEmail, string $replyToName, string $replyToEmail, ?bool $enabled, Event $queueForEvents, Database $dbForProject, Response $response) {
|
||||
$providerId = $providerId == 'unique()' ? ID::unique() : $providerId;
|
||||
|
||||
$options = [
|
||||
'fromName' => $fromName,
|
||||
'fromEmail' => $fromEmail,
|
||||
];
|
||||
|
||||
if (!empty($replyToName) && !empty($replyToEmail)) {
|
||||
$options['replyToName'] = $replyToName;
|
||||
$options['replyToEmail'] = $replyToEmail;
|
||||
}
|
||||
|
||||
$credentials = [];
|
||||
|
||||
if (!empty($apiKey)) {
|
||||
$credentials['apiKey'] = $apiKey;
|
||||
}
|
||||
|
||||
$options = [
|
||||
'fromName' => $fromName,
|
||||
'fromEmail' => $fromEmail,
|
||||
'replyToName' => $replyToName,
|
||||
'replyToEmail' => $replyToEmail,
|
||||
];
|
||||
|
||||
if (
|
||||
$enabled === true
|
||||
&& !empty($fromEmail)
|
||||
&& \array_key_exists('apiKey', $credentials)
|
||||
&& \array_key_exists('fromEmail', $options)
|
||||
) {
|
||||
$enabled = true;
|
||||
} else {
|
||||
@@ -210,6 +205,94 @@ App::post('/v1/messaging/providers/sendgrid')
|
||||
->dynamic($provider, Response::MODEL_PROVIDER);
|
||||
});
|
||||
|
||||
App::post('/v1/messaging/providers/smtp')
|
||||
->desc('Create SMTP provider')
|
||||
->groups(['api', 'messaging'])
|
||||
->label('audits.event', 'provider.create')
|
||||
->label('audits.resource', 'provider/{response.$id}')
|
||||
->label('event', 'providers.[providerId].create')
|
||||
->label('scope', 'providers.write')
|
||||
->label('sdk.auth', [APP_AUTH_TYPE_ADMIN, APP_AUTH_TYPE_KEY])
|
||||
->label('sdk.namespace', 'messaging')
|
||||
->label('sdk.method', 'createSMTPProvider')
|
||||
->label('sdk.description', '/docs/references/messaging/create-smtp-provider.md')
|
||||
->label('sdk.response.code', Response::STATUS_CODE_CREATED)
|
||||
->label('sdk.response.type', Response::CONTENT_TYPE_JSON)
|
||||
->label('sdk.response.model', Response::MODEL_PROVIDER)
|
||||
->param('providerId', '', new CustomId(), 'Provider ID. Choose a custom ID or generate a random ID with `ID.unique()`. Valid chars are a-z, A-Z, 0-9, period, hyphen, and underscore. Can\'t start with a special char. Max length is 36 chars.')
|
||||
->param('name', '', new Text(128), 'Provider name.')
|
||||
->param('host', '', new Text(0), 'SMTP hosts. Either a single hostname or multiple semicolon-delimited hostnames. You can also specify a different port for each host by using this format: [hostname:port] (e.g. "smtp1.example.com:25;smtp2.example.com"). You can also specify encryption type, for example: (e.g. "tls://smtp1.example.com:587;ssl://smtp2.example.com:465"). Hosts will be tried in order.')
|
||||
->param('port', 587, new Range(1, 65535), 'The default SMTP server port.', true)
|
||||
->param('username', '', new Text(0), 'Authentication username.', true)
|
||||
->param('password', '', new Text(0), 'Authentication password.', true)
|
||||
->param('encryption', '', new WhiteList(['none', 'ssl', 'tls']), 'Encryption type. Can be omitted, \'ssl\', or \'tls\'', true)
|
||||
->param('autoTLS', true, new Boolean(), 'Enable SMTP AutoTLS feature.', true)
|
||||
->param('mailer', '', new Text(0), 'The value to use for the X-Mailer header.', true)
|
||||
->param('fromName', '', new Text(128, 0), 'Sender Name.', true)
|
||||
->param('fromEmail', '', new Email(), 'Sender email address.', true)
|
||||
->param('replyToName', '', new Text(128, 0), 'Name set in the reply to field for the mail. Default value is sender name.', true)
|
||||
->param('replyToEmail', '', new Email(), 'Email set in the reply to field for the mail. Default value is sender email.', true)
|
||||
->param('enabled', null, new Boolean(), 'Set as enabled.', true)
|
||||
->inject('queueForEvents')
|
||||
->inject('dbForProject')
|
||||
->inject('response')
|
||||
->action(function (string $providerId, string $name, string $host, int $port, string $username, string $password, string $encryption, bool $autoTLS, string $mailer, string $fromName, string $fromEmail, string $replyToName, string $replyToEmail, ?bool $enabled, Event $queueForEvents, Database $dbForProject, Response $response) {
|
||||
$providerId = $providerId == 'unique()' ? ID::unique() : $providerId;
|
||||
|
||||
$credentials = [
|
||||
'port' => $port,
|
||||
'username' => $username,
|
||||
'password' => $password,
|
||||
];
|
||||
|
||||
if (!empty($host)) {
|
||||
$credentials['host'] = $host;
|
||||
}
|
||||
|
||||
$options = [
|
||||
'fromName' => $fromName,
|
||||
'fromEmail' => $fromEmail,
|
||||
'replyToName' => $replyToName,
|
||||
'replyToEmail' => $replyToEmail,
|
||||
'encryption' => $encryption === 'none' ? '' : $encryption,
|
||||
'autoTLS' => $autoTLS,
|
||||
'mailer' => $mailer,
|
||||
];
|
||||
|
||||
if (
|
||||
$enabled === true
|
||||
&& !empty($fromEmail)
|
||||
&& \array_key_exists('host', $credentials)
|
||||
) {
|
||||
$enabled = true;
|
||||
} else {
|
||||
$enabled = false;
|
||||
}
|
||||
|
||||
$provider = new Document([
|
||||
'$id' => $providerId,
|
||||
'name' => $name,
|
||||
'provider' => 'smtp',
|
||||
'type' => MESSAGE_TYPE_EMAIL,
|
||||
'enabled' => $enabled,
|
||||
'credentials' => $credentials,
|
||||
'options' => $options,
|
||||
]);
|
||||
|
||||
try {
|
||||
$provider = $dbForProject->createDocument('providers', $provider);
|
||||
} catch (DuplicateException) {
|
||||
throw new Exception(Exception::PROVIDER_ALREADY_EXISTS);
|
||||
}
|
||||
|
||||
$queueForEvents
|
||||
->setParam('providerId', $provider->getId());
|
||||
|
||||
$response
|
||||
->setStatusCode(Response::STATUS_CODE_CREATED)
|
||||
->dynamic($provider, Response::MODEL_PROVIDER);
|
||||
});
|
||||
|
||||
App::post('/v1/messaging/providers/msg91')
|
||||
->desc('Create Msg91 provider')
|
||||
->groups(['api', 'messaging'])
|
||||
@@ -1085,6 +1168,125 @@ App::patch('/v1/messaging/providers/sendgrid/:providerId')
|
||||
->dynamic($provider, Response::MODEL_PROVIDER);
|
||||
});
|
||||
|
||||
App::patch('/v1/messaging/providers/smtp/:providerId')
|
||||
->desc('Update SMTP provider')
|
||||
->groups(['api', 'messaging'])
|
||||
->label('audits.event', 'provider.update')
|
||||
->label('audits.resource', 'provider/{response.$id}')
|
||||
->label('event', 'providers.[providerId].update')
|
||||
->label('scope', 'providers.write')
|
||||
->label('sdk.auth', [APP_AUTH_TYPE_ADMIN, APP_AUTH_TYPE_KEY])
|
||||
->label('sdk.namespace', 'messaging')
|
||||
->label('sdk.method', 'updateSMTPProvider')
|
||||
->label('sdk.description', '/docs/references/messaging/update-smtp-provider.md')
|
||||
->label('sdk.response.code', Response::STATUS_CODE_OK)
|
||||
->label('sdk.response.type', Response::CONTENT_TYPE_JSON)
|
||||
->label('sdk.response.model', Response::MODEL_PROVIDER)
|
||||
->param('providerId', '', new UID(), 'Provider ID.')
|
||||
->param('name', '', new Text(128), 'Provider name.', true)
|
||||
->param('host', '', new Text(0), 'SMTP hosts. Either a single hostname or multiple semicolon-delimited hostnames. You can also specify a different port for each host by using this format: [hostname:port] (e.g. "smtp1.example.com:25;smtp2.example.com"). You can also specify encryption type, for example: (e.g. "tls://smtp1.example.com:587;ssl://smtp2.example.com:465"). Hosts will be tried in order.', true)
|
||||
->param('port', null, new Range(1, 65535), 'SMTP port.', true)
|
||||
->param('username', '', new Text(0), 'Authentication username.', true)
|
||||
->param('password', '', new Text(0), 'Authentication password.', true)
|
||||
->param('encryption', '', new WhiteList(['none', 'ssl', 'tls']), 'Encryption type. Can be \'ssl\' or \'tls\'', true)
|
||||
->param('autoTLS', null, new Boolean(), 'Enable SMTP AutoTLS feature.', true)
|
||||
->param('fromName', '', new Text(128), 'Sender Name.', true)
|
||||
->param('fromEmail', '', new Email(), 'Sender email address.', true)
|
||||
->param('replyToName', '', new Text(128), 'Name set in the Reply To field for the mail. Default value is Sender Name.', true)
|
||||
->param('replyToEmail', '', new Text(128), 'Email set in the Reply To field for the mail. Default value is Sender Email.', true)
|
||||
->param('enabled', null, new Boolean(), 'Set as enabled.', true)
|
||||
->inject('queueForEvents')
|
||||
->inject('dbForProject')
|
||||
->inject('response')
|
||||
->action(function (string $providerId, string $name, string $host, ?int $port, string $username, string $password, string $encryption, ?bool $autoTLS, string $fromName, string $fromEmail, string $replyToName, string $replyToEmail, ?bool $enabled, Event $queueForEvents, Database $dbForProject, Response $response) {
|
||||
$provider = $dbForProject->getDocument('providers', $providerId);
|
||||
|
||||
if ($provider->isEmpty()) {
|
||||
throw new Exception(Exception::PROVIDER_NOT_FOUND);
|
||||
}
|
||||
|
||||
$providerAttr = $provider->getAttribute('provider');
|
||||
|
||||
if ($providerAttr !== 'smtp') {
|
||||
throw new Exception(Exception::PROVIDER_INCORRECT_TYPE);
|
||||
}
|
||||
|
||||
if (!empty($name)) {
|
||||
$provider->setAttribute('name', $name);
|
||||
}
|
||||
|
||||
$options = $provider->getAttribute('options');
|
||||
|
||||
if (!empty($fromName)) {
|
||||
$options['fromName'] = $fromName;
|
||||
}
|
||||
|
||||
if (!empty($fromEmail)) {
|
||||
$options['fromEmail'] = $fromEmail;
|
||||
}
|
||||
|
||||
if (!empty($replyToName)) {
|
||||
$options['replyToName'] = $replyToName;
|
||||
}
|
||||
|
||||
if (!empty($replyToEmail)) {
|
||||
$options['replyToEmail'] = $replyToEmail;
|
||||
}
|
||||
|
||||
$provider->setAttribute('options', $options);
|
||||
|
||||
$credentials = $provider->getAttribute('credentials');
|
||||
|
||||
if (!empty($host)) {
|
||||
$credentials['host'] = $host;
|
||||
}
|
||||
|
||||
if (!\is_null($port)) {
|
||||
$credentials['port'] = $port;
|
||||
}
|
||||
|
||||
if (!empty($username)) {
|
||||
$credentials['username'] = $username;
|
||||
}
|
||||
|
||||
if (!empty($password)) {
|
||||
$credentials['password'] = $password;
|
||||
}
|
||||
|
||||
if (!empty($encryption)) {
|
||||
$credentials['encryption'] = $encryption === 'none' ? '' : $encryption;
|
||||
}
|
||||
|
||||
if (!\is_null($autoTLS)) {
|
||||
$credentials['autoTLS'] = $autoTLS;
|
||||
}
|
||||
|
||||
$provider->setAttribute('credentials', $credentials);
|
||||
|
||||
if (!\is_null($enabled)) {
|
||||
if ($enabled) {
|
||||
if (
|
||||
!empty($options['fromEmail'])
|
||||
&& \array_key_exists('host', $credentials)
|
||||
) {
|
||||
$provider->setAttribute('enabled', true);
|
||||
} else {
|
||||
throw new Exception(Exception::PROVIDER_MISSING_CREDENTIALS);
|
||||
}
|
||||
} else {
|
||||
$provider->setAttribute('enabled', false);
|
||||
}
|
||||
}
|
||||
|
||||
$provider = $dbForProject->updateDocument('providers', $provider->getId(), $provider);
|
||||
|
||||
$queueForEvents
|
||||
->setParam('providerId', $provider->getId());
|
||||
|
||||
$response
|
||||
->dynamic($provider, Response::MODEL_PROVIDER);
|
||||
});
|
||||
|
||||
App::patch('/v1/messaging/providers/msg91/:providerId')
|
||||
->desc('Update Msg91 provider')
|
||||
->groups(['api', 'messaging'])
|
||||
|
||||
@@ -1417,76 +1417,6 @@ App::patch('/v1/users/:userId/targets/:targetId')
|
||||
->dynamic($target, Response::MODEL_TARGET);
|
||||
});
|
||||
|
||||
App::post('/v1/users/:userId/sessions')
|
||||
->desc('Create session')
|
||||
->groups(['api', 'users'])
|
||||
->label('event', 'users.[userId].sessions.[sessionId].create')
|
||||
->label('scope', 'users.write')
|
||||
->label('audits.event', 'session.create')
|
||||
->label('audits.resource', 'user/{request.userId}')
|
||||
->label('usage.metric', 'sessions.{scope}.requests.create')
|
||||
->label('sdk.auth', [APP_AUTH_TYPE_KEY])
|
||||
->label('sdk.namespace', 'users')
|
||||
->label('sdk.method', 'createSession')
|
||||
->label('sdk.description', '/docs/references/users/create-session.md')
|
||||
->label('sdk.response.code', Response::STATUS_CODE_CREATED)
|
||||
->label('sdk.response.type', Response::CONTENT_TYPE_JSON)
|
||||
->label('sdk.response.model', Response::MODEL_SESSION)
|
||||
->param('userId', '', new CustomId(), 'User ID. Choose a custom ID or generate a random ID with `ID.unique()`. Valid chars are a-z, A-Z, 0-9, period, hyphen, and underscore. Can\'t start with a special char. Max length is 36 chars.')
|
||||
->inject('request')
|
||||
->inject('response')
|
||||
->inject('dbForProject')
|
||||
->inject('project')
|
||||
->inject('locale')
|
||||
->inject('geodb')
|
||||
->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()) {
|
||||
throw new Exception(Exception::USER_NOT_FOUND);
|
||||
}
|
||||
|
||||
$secret = Auth::codeGenerator();
|
||||
$detector = new Detector($request->getUserAgent('UNKNOWN'));
|
||||
$record = $geodb->get($request->getIP());
|
||||
|
||||
$duration = $project->getAttribute('auths', [])['duration'] ?? Auth::TOKEN_EXPIRATION_LOGIN_LONG;
|
||||
$expire = DateTime::formatTz(DateTime::addSeconds(new \DateTime(), $duration));
|
||||
|
||||
$session = new Document(array_merge(
|
||||
[
|
||||
'$id' => ID::unique(),
|
||||
'userId' => $user->getId(),
|
||||
'userInternalId' => $user->getInternalId(),
|
||||
'provider' => Auth::SESSION_PROVIDER_SERVER,
|
||||
'secret' => Auth::hash($secret), // One way hash encryption to protect DB leak
|
||||
'userAgent' => $request->getUserAgent('UNKNOWN'),
|
||||
'ip' => $request->getIP(),
|
||||
'countryCode' => ($record) ? \strtolower($record['country']['iso_code']) : '--',
|
||||
],
|
||||
$detector->getOS(),
|
||||
$detector->getClient(),
|
||||
$detector->getDevice()
|
||||
));
|
||||
|
||||
$countryName = $locale->getText('countries.' . strtolower($session->getAttribute('countryCode')), $locale->getText('locale.country.unknown'));
|
||||
|
||||
$session = $dbForProject->createDocument('sessions', $session);
|
||||
$session
|
||||
->setAttribute('secret', $secret)
|
||||
->setAttribute('expire', $expire)
|
||||
->setAttribute('countryName', $countryName);
|
||||
|
||||
$queueForEvents
|
||||
->setParam('userId', $user->getId())
|
||||
->setParam('sessionId', $session->getId())
|
||||
->setPayload($response->output($session, Response::MODEL_SESSION));
|
||||
|
||||
return $response
|
||||
->setStatusCode(Response::STATUS_CODE_CREATED)
|
||||
->dynamic($session, Response::MODEL_SESSION);
|
||||
});
|
||||
|
||||
App::post('/v1/users/:userId/tokens')
|
||||
->desc('Create token')
|
||||
->groups(['api', 'users'])
|
||||
|
||||
@@ -297,6 +297,7 @@ services:
|
||||
- _APP_REDIS_PASS
|
||||
- _APP_LOGGING_PROVIDER
|
||||
- _APP_LOGGING_CONFIG
|
||||
- _APP_WEBHOOK_MAX_FAILED_ATTEMPTS
|
||||
|
||||
appwrite-worker-deletes:
|
||||
entrypoint: worker-deletes
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
namespace Appwrite\Auth;
|
||||
|
||||
class SecurityPhrase
|
||||
class Phrase
|
||||
{
|
||||
public static function generate(): string
|
||||
{
|
||||
@@ -18,6 +18,7 @@ use Utopia\Database\Query;
|
||||
use Utopia\Messaging\Adapter\Email as EmailAdapter;
|
||||
use Utopia\Messaging\Adapter\Email\Mailgun;
|
||||
use Utopia\Messaging\Adapter\Email\Sendgrid;
|
||||
use Utopia\Messaging\Adapter\Email\SMTP;
|
||||
use Utopia\Messaging\Adapter\Push as PushAdapter;
|
||||
use Utopia\Messaging\Adapter\Push\APNS;
|
||||
use Utopia\Messaging\Adapter\Push\FCM;
|
||||
@@ -216,8 +217,8 @@ class Messaging extends Action
|
||||
$batches = \array_chunk($identifiers, $maxBatchSize);
|
||||
$batchIndex = 0;
|
||||
|
||||
return batch(\array_map(function ($batch) use ($message, $provider, $adapter, $batchIndex, $dbForProject) {
|
||||
return function () use ($batch, $message, $provider, $adapter, $batchIndex, $dbForProject) {
|
||||
return batch(\array_map(function ($batch) use ($message, $provider, $adapter, &$batchIndex, $dbForProject) {
|
||||
return function () use ($batch, $message, $provider, $adapter, &$batchIndex, $dbForProject) {
|
||||
$deliveredTotal = 0;
|
||||
$deliveryErrors = [];
|
||||
$messageData = clone $message;
|
||||
@@ -407,10 +408,24 @@ class Messaging extends Action
|
||||
|
||||
private function email(Document $provider): ?EmailAdapter
|
||||
{
|
||||
$credentials = $provider->getAttribute('credentials');
|
||||
$credentials = $provider->getAttribute('credentials', []);
|
||||
$options = $provider->getAttribute('options', []);
|
||||
return match ($provider->getAttribute('provider')) {
|
||||
'mock' => new Mock('username', 'password'),
|
||||
'mailgun' => new Mailgun($credentials['apiKey'], $credentials['domain'], $credentials['isEuRegion']),
|
||||
'smtp' => new SMTP(
|
||||
$credentials['host'],
|
||||
$credentials['port'],
|
||||
$credentials['username'],
|
||||
$credentials['password'],
|
||||
$options['encryption'],
|
||||
$options['autoTLS'],
|
||||
$options['mailer'],
|
||||
),
|
||||
'mailgun' => new Mailgun(
|
||||
$credentials['apiKey'],
|
||||
$credentials['domain'],
|
||||
$credentials['isEuRegion']
|
||||
),
|
||||
'sendgrid' => new Sendgrid($credentials['apiKey']),
|
||||
default => null
|
||||
};
|
||||
@@ -418,16 +433,10 @@ class Messaging extends Action
|
||||
|
||||
private function buildEmailMessage(Database $dbForProject, Document $message, Document $provider): Email
|
||||
{
|
||||
$fromName = $provider['options']['fromName'];
|
||||
$fromEmail = $provider['options']['fromEmail'];
|
||||
$replyToEmail = null;
|
||||
$replyToName = null;
|
||||
|
||||
if (isset($provider['options']['replyToName']) && isset($provider['options']['replyToEmail'])) {
|
||||
$replyToName = $provider['options']['replyToName'];
|
||||
$replyToEmail = $provider['options']['replyToEmail'];
|
||||
}
|
||||
|
||||
$fromName = $provider['options']['fromName'] ?? null;
|
||||
$fromEmail = $provider['options']['fromEmail'] ?? null;
|
||||
$replyToEmail = $provider['options']['replyToEmail'] ?? null;
|
||||
$replyToName = $provider['options']['replyToName'] ?? null;
|
||||
$data = $message['data'] ?? [];
|
||||
$ccTargets = $data['cc'] ?? [];
|
||||
$bccTargets = $data['bcc'] ?? [];
|
||||
|
||||
@@ -16,7 +16,6 @@ use Utopia\Queue\Message;
|
||||
class Webhooks extends Action
|
||||
{
|
||||
private array $errors = [];
|
||||
private const MAX_FAILED_ATTEMPTS = 10;
|
||||
private const MAX_FILE_SIZE = 5242880; // 5 MB
|
||||
|
||||
public static function getName(): string
|
||||
@@ -157,7 +156,7 @@ class Webhooks extends Action
|
||||
|
||||
$webhook->setAttribute('logs', $logs);
|
||||
|
||||
if ($attempts >= self::MAX_FAILED_ATTEMPTS) {
|
||||
if ($attempts >= \intval(App::getEnv('_APP_WEBHOOK_MAX_FAILED_ATTEMPTS', '10'))) {
|
||||
$webhook->setAttribute('enabled', false);
|
||||
$this->sendEmailAlert($attempts, $statusCode, $webhook, $project, $dbForConsole, $queueForMails);
|
||||
}
|
||||
|
||||
@@ -40,7 +40,7 @@ class Token extends Model
|
||||
'default' => '',
|
||||
'example' => self::TYPE_DATETIME_EXAMPLE,
|
||||
])
|
||||
->addRule('securityPhrase', [
|
||||
->addRule('phrase', [
|
||||
'type' => self::TYPE_STRING,
|
||||
'description' => 'Security phrase of a token. Empty if security phrase was not requested when creating a token. It includes randomly generated phrase which is also sent in the external resource such as email.',
|
||||
'default' => '',
|
||||
|
||||
@@ -148,7 +148,7 @@ trait AccountBase
|
||||
$this->assertNotEmpty($response['body']['userId']);
|
||||
$this->assertNotEmpty($response['body']['expire']);
|
||||
$this->assertEmpty($response['body']['secret']);
|
||||
$this->assertEmpty($response['body']['securityPhrase']);
|
||||
$this->assertEmpty($response['body']['phrase']);
|
||||
|
||||
$userId = $response['body']['userId'];
|
||||
|
||||
@@ -208,21 +208,21 @@ trait AccountBase
|
||||
]), [
|
||||
'userId' => ID::unique(),
|
||||
'email' => 'otpuser@appwrite.io',
|
||||
'securityPhrase' => true
|
||||
'phrase' => true
|
||||
]);
|
||||
|
||||
$this->assertEquals($response['headers']['status-code'], 201);
|
||||
$this->assertNotEmpty($response['body']['securityPhrase']);
|
||||
$this->assertNotEmpty($response['body']['phrase']);
|
||||
$this->assertEmpty($response['body']['secret']);
|
||||
$this->assertEquals($userId, $response['body']['userId']);
|
||||
|
||||
$securityPhrase = $response['body']['securityPhrase'];
|
||||
$phrase = $response['body']['phrase'];
|
||||
|
||||
$lastEmail = $this->getLastEmail();
|
||||
$this->assertEquals('otpuser@appwrite.io', $lastEmail['to'][0]['address']);
|
||||
$this->assertEquals('OTP for ' . $this->getProject()['name'] . ' Login', $lastEmail['subject']);
|
||||
$this->assertStringContainsStringIgnoringCase('security phrase', $lastEmail['text']);
|
||||
$this->assertStringContainsStringIgnoringCase($securityPhrase, $lastEmail['text']);
|
||||
$this->assertStringContainsStringIgnoringCase($phrase, $lastEmail['text']);
|
||||
|
||||
$response = $this->client->call(Client::METHOD_POST, '/account/tokens/email', array_merge([
|
||||
'origin' => 'http://localhost',
|
||||
|
||||
@@ -2331,7 +2331,7 @@ class AccountCustomClientTest extends Scope
|
||||
$this->assertEquals(201, $response['headers']['status-code']);
|
||||
$this->assertNotEmpty($response['body']['$id']);
|
||||
$this->assertEmpty($response['body']['secret']);
|
||||
$this->assertEmpty($response['body']['securityPhrase']);
|
||||
$this->assertEmpty($response['body']['phrase']);
|
||||
$this->assertEquals(true, (new DatetimeValidator())->isValid($response['body']['expire']));
|
||||
|
||||
$userId = $response['body']['userId'];
|
||||
@@ -2399,15 +2399,15 @@ class AccountCustomClientTest extends Scope
|
||||
]), [
|
||||
'userId' => ID::unique(),
|
||||
'email' => $email,
|
||||
'securityPhrase' => true
|
||||
'phrase' => true
|
||||
]);
|
||||
|
||||
$this->assertEquals(201, $response['headers']['status-code']);
|
||||
$this->assertNotEmpty($response['body']['$id']);
|
||||
$this->assertNotEmpty($response['body']['securityPhrase']);
|
||||
$this->assertNotEmpty($response['body']['phrase']);
|
||||
|
||||
$lastEmail = $this->getLastEmail();
|
||||
$this->assertStringContainsStringIgnoringCase($response['body']['securityPhrase'], $lastEmail['text']);
|
||||
$this->assertStringContainsStringIgnoringCase($response['body']['phrase'], $lastEmail['text']);
|
||||
|
||||
$data['token'] = $token;
|
||||
$data['id'] = $userId;
|
||||
|
||||
@@ -120,6 +120,47 @@ class AccountCustomServerTest extends Scope
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* @depends testCreateAccountSession
|
||||
*/
|
||||
public function testGetAccount($data): array
|
||||
{
|
||||
$email = $data['email'] ?? '';
|
||||
$name = $data['name'] ?? '';
|
||||
$session = $data['session'] ?? '';
|
||||
|
||||
/**
|
||||
* Test for SUCCESS
|
||||
*/
|
||||
$response = $this->client->call(Client::METHOD_GET, '/account', array_merge([
|
||||
'origin' => 'http://localhost',
|
||||
'content-type' => 'application/json',
|
||||
'x-appwrite-project' => $this->getProject()['$id'],
|
||||
'x-appwrite-session' => $session,
|
||||
]));
|
||||
|
||||
$this->assertEquals(200, $response['headers']['status-code']);
|
||||
$this->assertNotEmpty($response['body']);
|
||||
$this->assertNotEmpty($response['body']['$id']);
|
||||
$this->assertEquals(true, (new DatetimeValidator())->isValid($response['body']['registration']));
|
||||
$this->assertEquals($response['body']['email'], $email);
|
||||
$this->assertEquals($response['body']['name'], $name);
|
||||
$this->assertArrayHasKey('accessedAt', $response['body']);
|
||||
$this->assertNotEmpty($response['body']['accessedAt']);
|
||||
|
||||
/**
|
||||
* Test for FAILURE
|
||||
*/
|
||||
$response = $this->client->call(Client::METHOD_GET, '/account', array_merge([
|
||||
'content-type' => 'application/json',
|
||||
'x-appwrite-project' => $this->getProject()['$id'],
|
||||
], $this->getHeaders()));
|
||||
|
||||
$this->assertEquals(404, $response['headers']['status-code']);
|
||||
|
||||
return $data;
|
||||
}
|
||||
|
||||
public function testCreateAnonymousAccount()
|
||||
{
|
||||
/**
|
||||
|
||||
@@ -207,6 +207,7 @@ trait Base
|
||||
// Providers
|
||||
public static string $CREATE_MAILGUN_PROVIDER = 'create_mailgun_provider';
|
||||
public static string $CREATE_SENDGRID_PROVIDER = 'create_sendgrid_provider';
|
||||
public static string $CREATE_SMTP_PROVIDER = 'create_smtp_provider';
|
||||
public static string $CREATE_TWILIO_PROVIDER = 'create_twilio_provider';
|
||||
public static string $CREATE_TELESIGN_PROVIDER = 'create_telesign_provider';
|
||||
public static string $CREATE_TEXTMAGIC_PROVIDER = 'create_textmagic_provider';
|
||||
@@ -218,6 +219,7 @@ trait Base
|
||||
public static string $GET_PROVIDER = 'get_provider';
|
||||
public static string $UPDATE_MAILGUN_PROVIDER = 'update_mailgun_provider';
|
||||
public static string $UPDATE_SENDGRID_PROVIDER = 'update_sendgrid_provider';
|
||||
public static string $UPDATE_SMTP_PROVIDER = 'update_smtp_provider';
|
||||
public static string $UPDATE_TWILIO_PROVIDER = 'update_twilio_provider';
|
||||
public static string $UPDATE_TELESIGN_PROVIDER = 'update_telesign_provider';
|
||||
public static string $UPDATE_TEXTMAGIC_PROVIDER = 'update_textmagic_provider';
|
||||
@@ -1809,6 +1811,16 @@ trait Base
|
||||
enabled
|
||||
}
|
||||
}';
|
||||
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) {
|
||||
_id
|
||||
name
|
||||
provider
|
||||
type
|
||||
enabled
|
||||
}
|
||||
}';
|
||||
case self::$CREATE_TWILIO_PROVIDER:
|
||||
return 'mutation createTwilioProvider($providerId: String!, $name: String!, $from: String!, $accountSid: String!, $authToken: String!) {
|
||||
messagingCreateTwilioProvider(providerId: $providerId, name: $name, from: $from, accountSid: $accountSid, authToken: $authToken) {
|
||||
@@ -1923,6 +1935,16 @@ trait Base
|
||||
enabled
|
||||
}
|
||||
}';
|
||||
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) {
|
||||
_id
|
||||
name
|
||||
provider
|
||||
type
|
||||
enabled
|
||||
}
|
||||
}';
|
||||
case self::$UPDATE_TWILIO_PROVIDER:
|
||||
return 'mutation updateTwilioProvider($providerId: String!, $name: String!, $accountSid: String!, $authToken: String!) {
|
||||
messagingUpdateTwilioProvider(providerId: $providerId, name: $name, accountSid: $accountSid, authToken: $authToken) {
|
||||
|
||||
@@ -29,6 +29,17 @@ trait MessagingBase
|
||||
'fromEmail' => 'sender-email@my-domain.com',
|
||||
'isEuRegion' => false,
|
||||
],
|
||||
'smtp' => [
|
||||
'providerId' => ID::unique(),
|
||||
'name' => 'SMTP1',
|
||||
'host' => 'smtp.appwrite.io',
|
||||
'port' => 587,
|
||||
'security' => 'tls',
|
||||
'username' => 'my-username',
|
||||
'password' => 'my-password',
|
||||
'fromName' => 'sender name',
|
||||
'fromEmail' => 'tester@appwrite.io',
|
||||
],
|
||||
'twilio' => [
|
||||
'providerId' => ID::unique(),
|
||||
'name' => 'Twilio1',
|
||||
@@ -115,6 +126,14 @@ trait MessagingBase
|
||||
'apiKey' => 'my-apikey',
|
||||
'domain' => 'my-domain',
|
||||
],
|
||||
'smtp' => [
|
||||
'name' => 'SMTP2',
|
||||
'host' => 'smtp.appwrite.io',
|
||||
'port' => 587,
|
||||
'security' => 'tls',
|
||||
'username' => 'my-username',
|
||||
'password' => 'my-password',
|
||||
],
|
||||
'twilio' => [
|
||||
'name' => 'Twilio2',
|
||||
'accountSid' => 'my-accountSid',
|
||||
@@ -224,7 +243,7 @@ trait MessagingBase
|
||||
]);
|
||||
|
||||
$this->assertEquals(200, $response['headers']['status-code']);
|
||||
$this->assertEquals(10, \count($response['body']['providers']));
|
||||
$this->assertEquals(11, \count($response['body']['providers']));
|
||||
|
||||
return $providers;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user