1
0
Fork 0
mirror of synced 2024-07-01 20:50:49 +12:00

Merge branch '1.5.x' into feat-zoho-oauth

This commit is contained in:
Utkarsh Ahuja 2024-01-03 11:24:24 +05:30 committed by GitHub
commit a74e5cc2c6
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
64 changed files with 9318 additions and 285 deletions

7
.env
View file

@ -87,7 +87,7 @@ _APP_LOGGING_PROVIDER=
_APP_LOGGING_CONFIG=
_APP_GRAPHQL_MAX_BATCH_SIZE=10
_APP_GRAPHQL_MAX_COMPLEXITY=250
_APP_GRAPHQL_MAX_DEPTH=3
_APP_GRAPHQL_MAX_DEPTH=4
_APP_DOCKER_HUB_USERNAME=
_APP_DOCKER_HUB_PASSWORD=
_APP_VCS_GITHUB_APP_NAME=
@ -98,4 +98,7 @@ _APP_VCS_GITHUB_CLIENT_SECRET=
_APP_VCS_GITHUB_WEBHOOK_SECRET=
_APP_MIGRATIONS_FIREBASE_CLIENT_ID=
_APP_MIGRATIONS_FIREBASE_CLIENT_SECRET=
_APP_ASSISTANT_OPENAI_API_KEY=
_APP_ASSISTANT_OPENAI_API_KEY=
_APP_MESSAGE_SMS_TEST_DSN=
_APP_MESSAGE_EMAIL_TEST_DSN=
_APP_MESSAGE_PUSH_TEST_DSN=

View file

@ -99,6 +99,7 @@ jobs:
Users,
Webhooks,
VCS,
Messaging,
]
steps:

1
.gitignore vendored
View file

@ -13,3 +13,4 @@ debug/
app/sdks
dev/yasd_init.php
.phpunit.result.cache
Makefile

View file

@ -484,7 +484,7 @@
## Features
- Added Phone Authentication by @TorstenDittmann in https://github.com/appwrite/appwrite/pull/3357
- Added Twilio Support
- Added TextMagic Support
- Added Textmagic Support
- Added Telesign Support
- Added Endpoint to create Phone Session (`POST:/v1/account/sessions/phone`)
- Added Endpoint to confirm Phone Session (`PUT:/v1/account/sessions/phone`)

View file

@ -5,7 +5,7 @@ use Utopia\Config\Config;
use Utopia\Database\Database;
use Utopia\Database\Helpers\ID;
$providers = Config::getParam('providers', []);
$providers = Config::getParam('oAuthProviders', []);
$auth = Config::getParam('auth', []);
/**
@ -221,6 +221,17 @@ $commonCollections = [
'array' => false,
'filters' => ['subQueryMemberships'],
],
[
'$id' => ID::custom('targets'),
'type' => Database::VAR_STRING,
'format' => '',
'size' => 16384,
'signed' => true,
'required' => false,
'default' => null,
'array' => false,
'filters' => ['subQueryTargets'],
],
[
'$id' => ID::custom('search'),
'type' => Database::VAR_STRING,
@ -1366,7 +1377,608 @@ $commonCollections = [
],
],
],
];
'providers' => [
'$collection' => ID::custom(DATABASE::METADATA),
'$id' => ID::custom('providers'),
'name' => 'Providers',
'attributes' => [
[
'$id' => ID::custom('name'),
'type' => Database::VAR_STRING,
'format' => '',
'size' => 128,
'signed' => true,
'required' => true,
'default' => null,
'array' => false,
'filters' => [],
],
[
'$id' => ID::custom('provider'),
'type' => Database::VAR_STRING,
'format' => '',
'size' => Database::LENGTH_KEY,
'signed' => true,
'required' => true,
'default' => null,
'array' => false,
'filters' => [],
],
[
'$id' => ID::custom('type'),
'type' => Database::VAR_STRING,
'format' => '',
'size' => 128,
'signed' => true,
'required' => true,
'default' => null,
'array' => false,
'filters' => [],
],
[
'$id' => ID::custom('enabled'),
'type' => Database::VAR_BOOLEAN,
'signed' => true,
'size' => 0,
'format' => '',
'filters' => [],
'required' => true,
'default' => true,
'array' => false,
],
[
'$id' => ID::custom('credentials'),
'type' => Database::VAR_STRING,
'format' => '',
'size' => 16384,
'signed' => true,
'required' => true,
'default' => null,
'array' => false,
'filters' => ['json', 'encrypt'],
],
[
'$id' => ID::custom('options'),
'type' => Database::VAR_STRING,
'format' => '',
'size' => 16384,
'signed' => true,
'required' => false,
'default' => [],
'array' => false,
'filters' => ['json'],
],
[
'$id' => ID::custom('search'),
'type' => Database::VAR_STRING,
'format' => '',
'size' => 65535,
'signed' => true,
'required' => false,
'default' => '',
'array' => false,
'filters' => ['providerSearch'],
],
],
'indexes' => [
[
'$id' => ID::custom('_key_provider'),
'type' => Database::INDEX_KEY,
'attributes' => ['provider'],
'lengths' => [],
'orders' => [Database::ORDER_ASC],
],
[
'$id' => ID::custom('_key_name'),
'type' => Database::INDEX_FULLTEXT,
'attributes' => ['name'],
'lengths' => [],
'orders' => [Database::ORDER_ASC],
],
[
'$id' => ID::custom('_key_type'),
'type' => Database::INDEX_KEY,
'attributes' => ['type'],
'lengths' => [],
'orders' => [Database::ORDER_ASC],
],
[
'$id' => ID::custom('_key_enabled_type'),
'type' => Database::INDEX_KEY,
'attributes' => ['enabled','type'],
'lengths' => [],
'orders' => [Database::ORDER_ASC],
],
[
'$id' => ID::custom('_key_search'),
'type' => Database::INDEX_FULLTEXT,
'attributes' => ['search'],
'lengths' => [],
'orders' => [],
]
],
],
'messages' => [
'$collection' => ID::custom(DATABASE::METADATA),
'$id' => ID::custom('messages'),
'name' => 'Messages',
'attributes' => [
[
'$id' => ID::custom('providerType'),
'type' => Database::VAR_STRING,
'format' => '',
'size' => Database::LENGTH_KEY,
'signed' => true,
'required' => true,
'default' => null,
'array' => false,
'filters' => [],
],
[
'$id' => ID::custom('description'),
'type' => Database::VAR_STRING,
'format' => '',
'size' => 256,
'signed' => true,
'required' => false,
'default' => '',
'array' => false,
'filters' => [],
],
[
'$id' => ID::custom('status'),
'type' => Database::VAR_STRING,
'format' => '',
'size' => Database::LENGTH_KEY,
'signed' => true,
'required' => true,
'default' => 'processing',
'array' => false,
'filters' => [],
],
[
'$id' => ID::custom('data'),
'type' => Database::VAR_STRING,
'format' => '',
'size' => 65535,
'signed' => true,
'required' => true,
'default' => null,
'array' => false,
'filters' => ['json'],
],
[
'$id' => ID::custom('topics'),
'type' => Database::VAR_STRING,
'format' => '',
'size' => 21845,
'signed' => true,
'required' => false,
'default' => [],
'array' => true,
'filters' => [],
],
[
'$id' => ID::custom('users'),
'type' => Database::VAR_STRING,
'format' => '',
'size' => 21845,
'signed' => true,
'required' => false,
'default' => [],
'array' => true,
'filters' => [],
],
[
'$id' => ID::custom('targets'),
'type' => Database::VAR_STRING,
'format' => '',
'size' => 21845,
'signed' => true,
'required' => false,
'default' => [],
'array' => true,
'filters' => [],
],
[
'$id' => ID::custom('scheduledAt'),
'type' => Database::VAR_DATETIME,
'format' => '',
'size' => 0,
'signed' => false,
'required' => false,
'default' => null,
'array' => false,
'filters' => ['datetime'],
],
[
'$id' => ID::custom('deliveredAt'),
'type' => Database::VAR_DATETIME,
'format' => '',
'size' => 0,
'signed' => false,
'required' => false,
'default' => null,
'array' => false,
'filters' => ['datetime'],
],
[
'$id' => ID::custom('deliveryErrors'),
'type' => Database::VAR_STRING,
'format' => '',
'size' => 65535,
'signed' => true,
'required' => false,
'default' => null,
'array' => true,
'filters' => [],
],
[
'$id' => ID::custom('deliveredTotal'),
'type' => Database::VAR_INTEGER,
'format' => '',
'size' => 0,
'signed' => true,
'required' => false,
'default' => 0,
'array' => false,
'filters' => [],
],
[
'$id' => ID::custom('search'),
'type' => Database::VAR_STRING,
'format' => '',
'size' => 16384,
'signed' => true,
'required' => false,
'default' => '',
'array' => false,
'filters' => ['messageSearch'],
],
],
'indexes' => [
[
'$id' => ID::custom('_key_search'),
'type' => Database::INDEX_FULLTEXT,
'attributes' => ['search'],
'lengths' => [],
'orders' => [],
],
],
],
'topics' => [
'$collection' => ID::custom(DATABASE::METADATA),
'$id' => ID::custom('topics'),
'name' => 'Topics',
'attributes' => [
[
'$id' => ID::custom('name'),
'type' => Database::VAR_STRING,
'format' => '',
'size' => 128,
'signed' => true,
'required' => true,
'default' => null,
'array' => false,
'filters' => [],
],
[
'$id' => ID::custom('description'),
'type' => Database::VAR_STRING,
'format' => '',
'size' => 2048,
'signed' => true,
'required' => false,
'default' => null,
'array' => false,
'filters' => [],
],
[
'$id' => ID::custom('total'),
'type' => Database::VAR_INTEGER,
'format' => '',
'size' => 0,
'signed' => true,
'required' => false,
'default' => 0,
'array' => false,
'filters' => [],
],
[
'$id' => ID::custom('targets'),
'type' => Database::VAR_STRING,
'format' => '',
'size' => 16384,
'signed' => true,
'required' => false,
'default' => null,
'array' => false,
'filters' => ['subQueryTopicTargets'],
],
[
'$id' => ID::custom('search'),
'type' => Database::VAR_STRING,
'format' => '',
'size' => 16384,
'signed' => true,
'required' => false,
'default' => '',
'array' => false,
'filters' => ['topicSearch'],
],
],
'indexes' => [
[
'$id' => ID::custom('_key_name'),
'type' => Database::INDEX_FULLTEXT,
'attributes' => ['name'],
'lengths' => [],
'orders' => [],
],
[
'$id' => ID::custom('_key_search'),
'type' => Database::INDEX_FULLTEXT,
'attributes' => ['search'],
'lengths' => [],
'orders' => [Database::ORDER_ASC],
]
],
],
'subscribers' => [
'$collection' => ID::custom(DATABASE::METADATA),
'$id' => ID::custom('subscribers'),
'name' => 'Subscribers',
'attributes' => [
[
'$id' => ID::custom('targetId'),
'type' => Database::VAR_STRING,
'format' => '',
'size' => Database::LENGTH_KEY,
'signed' => true,
'required' => true,
'default' => null,
'array' => false,
'filters' => [],
],
[
'$id' => ID::custom('targetInternalId'),
'type' => Database::VAR_STRING,
'format' => '',
'size' => Database::LENGTH_KEY,
'signed' => true,
'required' => true,
'default' => null,
'array' => false,
'filters' => [],
],
[
'$id' => ID::custom('userId'),
'type' => Database::VAR_STRING,
'format' => '',
'size' => Database::LENGTH_KEY,
'signed' => true,
'required' => true,
'default' => null,
'array' => false,
'filters' => [],
],
[
'$id' => ID::custom('userInternalId'),
'type' => Database::VAR_STRING,
'format' => '',
'size' => Database::LENGTH_KEY,
'signed' => true,
'required' => true,
'default' => null,
'array' => false,
'filters' => [],
],
[
'$id' => ID::custom('topicId'),
'type' => Database::VAR_STRING,
'format' => '',
'size' => Database::LENGTH_KEY,
'signed' => true,
'required' => true,
'default' => null,
'array' => false,
'filters' => [],
],
[
'$id' => ID::custom('topicInternalId'),
'type' => Database::VAR_STRING,
'format' => '',
'size' => Database::LENGTH_KEY,
'signed' => true,
'required' => true,
'default' => null,
'array' => false,
'filters' => [],
],
[
'$id' => ID::custom('providerType'),
'type' => Database::VAR_STRING,
'format' => '',
'size' => 128,
'signed' => true,
'required' => true,
'default' => null,
'array' => false,
'filters' => [],
],
],
'indexes' => [
[
'$id' => ID::custom('_key_targetId'),
'type' => Database::INDEX_KEY,
'attributes' => ['targetId'],
'lengths' => [],
'orders' => [],
],
[
'$id' => ID::custom('_key_targetInternalId'),
'type' => Database::INDEX_KEY,
'attributes' => ['targetInternalId'],
'lengths' => [],
'orders' => [],
],
[
'$id' => ID::custom('_key_userId'),
'type' => Database::INDEX_KEY,
'attributes' => ['userId'],
'lengths' => [],
'orders' => [],
],
[
'$id' => ID::custom('_key_userInternalId'),
'type' => Database::INDEX_KEY,
'attributes' => ['userInternalId'],
'lengths' => [],
'orders' => [],
],
[
'$id' => ID::custom('_key_topicId'),
'type' => Database::INDEX_KEY,
'attributes' => ['topicId'],
'lengths' => [],
'orders' => [],
],
[
'$id' => ID::custom('_key_topicInternalId'),
'type' => Database::INDEX_KEY,
'attributes' => ['topicInternalId'],
'lengths' => [],
'orders' => [],
]
],
],
'targets' => [
'$collection' => ID::custom(DATABASE::METADATA),
'$id' => ID::custom('targets'),
'name' => 'Targets',
'attributes' => [
[
'$id' => ID::custom('userId'),
'type' => Database::VAR_STRING,
'format' => '',
'size' => Database::LENGTH_KEY,
'signed' => true,
'required' => true,
'default' => null,
'array' => false,
'filters' => [],
],
[
'$id' => ID::custom('userInternalId'),
'type' => Database::VAR_STRING,
'format' => '',
'size' => Database::LENGTH_KEY,
'signed' => true,
'required' => true,
'default' => null,
'array' => false,
'filters' => [],
],
[
'$id' => ID::custom('providerType'),
'type' => Database::VAR_STRING,
'format' => '',
'size' => Database::LENGTH_KEY,
'signed' => true,
'required' => true,
'default' => null,
'array' => false,
'filters' => [],
],
[
'$id' => ID::custom('providerId'),
'type' => Database::VAR_STRING,
'format' => '',
'size' => Database::LENGTH_KEY,
'signed' => true,
'required' => false,
'default' => null,
'array' => false,
'filters' => [],
],
[
'$id' => ID::custom('providerInternalId'),
'type' => Database::VAR_STRING,
'format' => '',
'size' => Database::LENGTH_KEY,
'signed' => true,
'required' => false,
'default' => null,
'array' => false,
'filters' => [],
],
[
'$id' => ID::custom('identifier'),
'type' => Database::VAR_STRING,
'format' => '',
'size' => Database::LENGTH_KEY,
'signed' => true,
'required' => true,
'default' => null,
'array' => false,
'filters' => [],
],
[
'$id' => ID::custom('name'),
'type' => Database::VAR_STRING,
'format' => '',
'size' => Database::LENGTH_KEY,
'signed' => true,
'required' => false,
'default' => null,
'array' => false,
'filters' => [],
],
],
'indexes' => [
[
'$id' => ID::custom('_key_userId'),
'type' => Database::INDEX_KEY,
'attributes' => ['userId'],
'lengths' => [],
'orders' => [Database::ORDER_ASC],
],
[
'$id' => ID::custom('_key_userInternalId'),
'type' => Database::INDEX_KEY,
'attributes' => ['userInternalId'],
'lengths' => [],
'orders' => [Database::ORDER_ASC],
],
[
'$id' => ID::custom('_key_providerId'),
'type' => Database::INDEX_KEY,
'attributes' => ['providerId'],
'lengths' => [],
'orders' => [],
],
[
'$id' => ID::custom('_key_providerInternalId'),
'type' => Database::INDEX_KEY,
'attributes' => ['providerInternalId'],
'lengths' => [],
'orders' => [],
],
[
'$id' => ID::custom('_key_identifier'),
'type' => Database::INDEX_UNIQUE,
'attributes' => ['identifier'],
'lengths' => [],
'orders' => [],
],
],
],
];
$projectCollections = array_merge([
'databases' => [
@ -3412,7 +4024,7 @@ $consoleCollections = array_merge([
'filters' => ['json'],
],
[
'$id' => ID::custom('authProviders'),
'$id' => ID::custom('oAuthProviders'),
'type' => Database::VAR_STRING,
'format' => '',
'size' => 16384,

View file

@ -103,6 +103,16 @@ return [
'description' => 'This method was not fully implemented yet. If you believe this is a mistake, please upgrade your Appwrite server version.',
'code' => 405,
],
Exception::GENERAL_INVALID_EMAIL => [
'name' => Exception::GENERAL_INVALID_EMAIL,
'description' => 'Value must be a valid email address.',
'code' => 400,
],
Exception::GENERAL_INVALID_PHONE => [
'name' => Exception::GENERAL_INVALID_PHONE,
'description' => 'Value must be a valid phone number. Format this number with a leading \'+\' and a country code, e.g., +16175551212.',
'code' => 400,
],
/** User Errors */
Exception::USER_COUNT_EXCEEDED => [
@ -251,6 +261,16 @@ return [
'description' => 'User phone is already verified',
'code' => 409
],
Exception::USER_TARGET_NOT_FOUND => [
'name' => Exception::USER_TARGET_NOT_FOUND,
'description' => 'The target could not be found.',
'code' => 404,
],
Exception::USER_TARGET_ALREADY_EXISTS => [
'name' => Exception::USER_TARGET_ALREADY_EXISTS,
'description' => 'A target with the same ID already exists.',
'code' => 409,
],
/** Teams */
Exception::TEAM_NOT_FOUND => [
@ -773,4 +793,83 @@ return [
'code' => 503,
'publish' => false
],
/** Providers */
Exception::PROVIDER_NOT_FOUND => [
'name' => Exception::PROVIDER_NOT_FOUND,
'description' => 'Provider with the requested ID could not be found.',
'code' => 404,
],
Exception::PROVIDER_ALREADY_EXISTS => [
'name' => Exception::PROVIDER_ALREADY_EXISTS,
'description' => 'Provider with the requested ID already exists.',
'code' => 409,
],
Exception::PROVIDER_INCORRECT_TYPE => [
'name' => Exception::PROVIDER_INCORRECT_TYPE,
'description' => 'Provider with the requested ID is of incorrect type: ',
'code' => 400,
],
/** Topics */
Exception::TOPIC_NOT_FOUND => [
'name' => Exception::TOPIC_NOT_FOUND,
'description' => 'Topic with the request ID could not be found.',
'code' => 404,
],
Exception::TOPIC_ALREADY_EXISTS => [
'name' => Exception::TOPIC_ALREADY_EXISTS,
'description' => 'Topic with the request ID already exists.',
'code' => 409,
],
/** Subscribers */
Exception::SUBSCRIBER_NOT_FOUND => [
'name' => Exception::SUBSCRIBER_NOT_FOUND,
'description' => 'Subscriber with the request ID could not be found.',
'code' => 404,
],
Exception::SUBSCRIBER_ALREADY_EXISTS => [
'name' => Exception::SUBSCRIBER_ALREADY_EXISTS,
'description' => 'Subscriber with the request ID already exists.',
'code' => 409,
],
/** Messages */
Exception::MESSAGE_NOT_FOUND => [
'name' => Exception::MESSAGE_NOT_FOUND,
'description' => 'Message with the requested ID could not be found.',
'code' => 404,
],
Exception::MESSAGE_MISSING_TARGET => [
'name' => Exception::MESSAGE_MISSING_TARGET,
'description' => 'Message with the requested ID is missing a target (Topics or Users or Targets).',
'code' => 400,
],
Exception::MESSAGE_ALREADY_SENT => [
'name' => Exception::MESSAGE_ALREADY_SENT,
'description' => 'Message with the requested ID has already been sent.',
'code' => 400,
],
Exception::MESSAGE_ALREADY_SCHEDULED => [
'name' => Exception::MESSAGE_ALREADY_SCHEDULED,
'description' => 'Message with the requested ID has already been scheduled for delivery.',
'code' => 400,
],
Exception::MESSAGE_TARGET_NOT_EMAIL => [
'name' => Exception::MESSAGE_TARGET_NOT_EMAIL,
'description' => 'Message with the target ID is not an email target:',
'code' => 400,
],
Exception::MESSAGE_TARGET_NOT_SMS => [
'name' => Exception::MESSAGE_TARGET_NOT_SMS,
'description' => 'Message with the target ID is not an SMS target:',
'code' => 400,
],
Exception::MESSAGE_TARGET_NOT_PUSH => [
'name' => Exception::MESSAGE_TARGET_NOT_PUSH,
'description' => 'Message with the target ID is not a push target:',
'code' => 400,
],
];

View file

@ -44,6 +44,20 @@ return [
'$description' => 'This event triggers when a verification token for a user is validated.'
],
],
'targets' => [
'$model' => Response::MODEL_TARGET,
'$resource' => true,
'$description' => 'This event triggers on any user\'s target event.',
'create' => [
'$description' => 'This event triggers when a user\'s target is created.',
],
'update' => [
'$description' => 'This event triggers when a user\'s target is updated.',
],
'delete' => [
'$description' => 'This event triggers when a user\'s target is deleted.',
],
],
'create' => [
'$description' => 'This event triggers when a user is created.'
],
@ -237,6 +251,56 @@ return [
'$description' => 'This event triggers when a function is updated.',
]
],
'messages' => [
'$model' => Response::MODEL_MESSAGE,
'$resource' => true,
'$description' => 'This event triggers on any messaging event.',
'create' => [
'$description' => 'This event triggers when a message is created.',
],
'update' => [
'$description' => 'This event triggers when a message is updated.',
],
],
'topics' => [
'$model' => Response::MODEL_TOPIC,
'$resource' => true,
'$description' => 'This event triggers on any topic event.',
'create' => [
'$description' => 'This event triggers when a topic is created.',
],
'update' => [
'$description' => 'This event triggers when a topic is updated.',
],
'delete' => [
'$description' => 'This event triggers when a topic is deleted.'
],
'subscribers' => [
'$model' => Response::MODEL_SUBSCRIBER,
'$resource' => true,
'$description' => 'This event triggers on any subscriber event.',
'create' => [
'$description' => 'This event triggers when a subscriber is created.',
],
'delete' => [
'$description' => 'This event triggers when a subscriber is deleted.'
],
],
],
'providers' => [
'$model' => Response::MODEL_PROVIDER,
'$resource' => true,
'$description' => 'This event triggers on any provider event.',
'create' => [
'$description' => 'This event triggers when a provider is created.',
],
'update' => [
'$description' => 'This event triggers when a provider is updated.',
],
'delete' => [
'$description' => 'This event triggers when a provider is deleted.'
],
],
'rules' => [
'$model' => Response::MODEL_PROXY_RULE,
'$resource' => true,

View file

@ -21,6 +21,10 @@ $member = [
'avatars.read',
'execution.read',
'execution.write',
'targets.read',
'targets.write',
'subscribers.write',
'subscribers.read',
'assistant.read',
];
@ -60,6 +64,16 @@ $admins = [
'migrations.write',
'vcs.read',
'vcs.write',
'targets.read',
'targets.write',
'providers.write',
'providers.read',
'messages.write',
'messages.read',
'topics.write',
'topics.read',
'subscribers.write',
'subscribers.read'
];
return [

View file

@ -76,6 +76,36 @@ return [ // List of publicly visible scopes
'health.read' => [
'description' => 'Access to read your project\'s health status',
],
'providers.read' => [
'description' => 'Access to read your project\'s providers',
],
'providers.write' => [
'description' => 'Access to create, update, and delete your project\'s providers',
],
'messages.read' => [
'description' => 'Access to read your project\'s messages',
],
'messages.write' => [
'description' => 'Access to create, update, and delete your project\'s messages',
],
'topics.read' => [
'description' => 'Access to read your project\'s topics',
],
'topics.write' => [
'description' => 'Access to create, update, and delete your project\'s topics',
],
'subscribers.read' => [
'description' => 'Access to read your project\'s subscribers',
],
'subscribers.write' => [
'description' => 'Access to create, update, and delete your project\'s subscribers',
],
'targets.read' => [
'description' => 'Access to read your project\'s targets',
],
'targets.write' => [
'description' => 'Access to create, update, and delete your project\'s targets',
],
'rules.read' => [
'description' => 'Access to read your project\'s proxy rules',
],

View file

@ -251,4 +251,17 @@ return [
'optional' => false,
'icon' => '/images/services/migrations.png',
],
'messaging' => [
'key' => 'messaging',
'name' => 'Messaging',
'subtitle' => 'The Messaging service allows you to send messages to any provider type (SMTP, push notification, SMS, etc.).',
'description' => '/docs/services/messaging.md',
'controller' => 'api/messaging.php',
'sdk' => true,
'docs' => true,
'docsUrl' => 'https://appwrite.io/docs/server/messaging',
'tests' => true,
'optional' => true,
'icon' => '/images/services/messaging.png',
]
];

View file

@ -440,7 +440,7 @@ return [
'variables' => [
[
'name' => '_APP_SMS_PROVIDER',
'description' => "Provider used for delivering SMS for Phone authentication. Use the following format: 'sms://[USER]:[SECRET]@[PROVIDER]'.\n\nEnsure `[USER]` and `[SECRET]` are URL encoded if they contain any non-alphanumeric characters.\n\nAvailable providers are twilio, text-magic, telesign, msg91, and vonage.",
'description' => "Provider used for delivering SMS for Phone authentication. Use the following format: 'sms://[USER]:[SECRET]@[PROVIDER]'.\n\nEnsure `[USER]` and `[SECRET]` are URL encoded if they contain any non-alphanumeric characters.\n\nAvailable providers are twilio, Textmagic, telesign, msg91, and vonage.",
'introduction' => '0.15.0',
'default' => '',
'required' => false,

View file

@ -8,7 +8,6 @@ use Appwrite\Auth\Validator\Phone;
use Appwrite\Detector\Detector;
use Appwrite\Event\Event;
use Appwrite\Event\Mail;
use Appwrite\Event\Phone as EventPhone;
use Appwrite\Extend\Exception;
use Appwrite\Network\Validator\Email;
use Utopia\Validator\Host;
@ -45,6 +44,7 @@ use Utopia\Validator\WhiteList;
use Appwrite\Auth\Validator\PasswordHistory;
use Appwrite\Auth\Validator\PasswordDictionary;
use Appwrite\Auth\Validator\PersonalData;
use Appwrite\Event\Messaging;
$oauthDefaultSuccess = '/auth/oauth2/success';
$oauthDefaultFailure = '/auth/oauth2/failure';
@ -69,7 +69,7 @@ App::post('/v1/account')
->label('abuse-limit', 10)
->param('userId', '', new CustomId(), 'Unique 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('password', '', fn ($project, $passwordsDictionary) => new PasswordDictionary($passwordsDictionary, $project->getAttribute('auths', [])['passwordDictionary'] ?? false), 'New user password. Must be at least 8 chars.', false, ['project', 'passwordsDictionary'])
->param('password', '', fn ($project, $passwordsDictionary) => new PasswordDictionary($passwordsDictionary, $project->getAttribute('auths', [])['passwordDictionary'] ?? false), 'New user password. Must be between 8 and 256 chars.', false, ['project', 'passwordsDictionary'])
->param('name', '', new Text(128), 'User name. Max length: 128 chars.', true)
->inject('request')
->inject('response')
@ -148,7 +148,22 @@ App::post('/v1/account')
'accessedAt' => DateTime::now(),
]);
$user->removeAttribute('$internalId');
Authorization::skip(fn() => $dbForProject->createDocument('users', $user));
$user = Authorization::skip(fn() => $dbForProject->createDocument('users', $user));
try {
$target = Authorization::skip(fn() => $dbForProject->createDocument('targets', new Document([
'userId' => $user->getId(),
'userInternalId' => $user->getInternalId(),
'providerType' => MESSAGE_TYPE_EMAIL,
'identifier' => $email,
])));
$user->setAttribute('targets', [...$user->getAttribute('targets', []), $target]);
} catch (Duplicate) {
$existingTarget = $dbForProject->findOne('targets', [
Query::equal('identifier', [$email]),
]);
$user->setAttribute('targets', [...$user->getAttribute('targets', []), $existingTarget]);
}
$dbForProject->deleteCachedDocument('users', $user->getId());
} catch (Duplicate) {
throw new Exception(Exception::USER_ALREADY_EXISTS);
}
@ -299,7 +314,7 @@ App::get('/v1/account/sessions/oauth2/:provider')
->label('sdk.methodType', 'webAuth')
->label('abuse-limit', 50)
->label('abuse-key', 'ip:{ip}')
->param('provider', '', new WhiteList(\array_keys(Config::getParam('providers')), true), 'OAuth2 Provider. Currently, supported providers are: ' . \implode(', ', \array_keys(\array_filter(Config::getParam('providers'), fn($node) => (!$node['mock'])))) . '.')
->param('provider', '', new WhiteList(\array_keys(Config::getParam('oAuthProviders')), true), 'OAuth2 Provider. Currently, supported providers are: ' . \implode(', ', \array_keys(\array_filter(Config::getParam('oAuthProviders'), fn($node) => (!$node['mock'])))) . '.')
->param('success', '', fn($clients) => new Host($clients), 'URL to redirect back to your app after a successful login attempt. Only URLs from hostnames in your project\'s 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('failure', '', fn($clients) => new Host($clients), 'URL to redirect back to your app after a failed login attempt. Only URLs from hostnames in your project\'s 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('scopes', [], new ArrayList(new Text(APP_LIMIT_ARRAY_ELEMENT_SIZE), APP_LIMIT_ARRAY_PARAMS_SIZE), 'A list of custom OAuth2 scopes. Check each provider internal docs for a list of supported scopes. Maximum of ' . APP_LIMIT_ARRAY_PARAMS_SIZE . ' scopes are allowed, each ' . APP_LIMIT_ARRAY_ELEMENT_SIZE . ' characters long.', true)
@ -311,14 +326,14 @@ App::get('/v1/account/sessions/oauth2/:provider')
$protocol = $request->getProtocol();
$callback = $protocol . '://' . $request->getHostname() . '/v1/account/sessions/oauth2/callback/' . $provider . '/' . $project->getId();
$providerEnabled = $project->getAttribute('authProviders', [])[$provider . 'Enabled'] ?? false;
$providerEnabled = $project->getAttribute('oAuthProviders', [])[$provider . 'Enabled'] ?? false;
if (!$providerEnabled) {
throw new Exception(Exception::PROJECT_PROVIDER_DISABLED, 'This provider is disabled. Please enable the provider from your ' . APP_NAME . ' console to continue.');
}
$appId = $project->getAttribute('authProviders', [])[$provider . 'Appid'] ?? '';
$appSecret = $project->getAttribute('authProviders', [])[$provider . 'Secret'] ?? '{}';
$appId = $project->getAttribute('oAuthProviders', [])[$provider . 'Appid'] ?? '';
$appSecret = $project->getAttribute('oAuthProviders', [])[$provider . 'Secret'] ?? '{}';
if (!empty($appSecret) && isset($appSecret['version'])) {
$key = App::getEnv('_APP_OPENSSL_KEY_V' . $appSecret['version']);
@ -358,7 +373,7 @@ App::get('/v1/account/sessions/oauth2/callback/:provider/:projectId')
->label('scope', 'public')
->label('docs', false)
->param('projectId', '', new Text(1024), 'Project ID.')
->param('provider', '', new WhiteList(\array_keys(Config::getParam('providers')), true), 'OAuth2 provider.')
->param('provider', '', new WhiteList(\array_keys(Config::getParam('oAuthProviders')), true), 'OAuth2 provider.')
->param('code', '', new Text(2048, 0), 'OAuth2 code. This is a temporary code that the will be later exchanged for an access token.', true)
->param('state', '', new Text(2048), 'Login state params.', true)
->param('error', '', new Text(2048, 0), 'Error code returned from the OAuth2 provider.', true)
@ -391,7 +406,7 @@ App::post('/v1/account/sessions/oauth2/callback/:provider/:projectId')
->label('origin', '*')
->label('docs', false)
->param('projectId', '', new Text(1024), 'Project ID.')
->param('provider', '', new WhiteList(\array_keys(Config::getParam('providers')), true), 'OAuth2 provider.')
->param('provider', '', new WhiteList(\array_keys(Config::getParam('oAuthProviders')), true), 'OAuth2 provider.')
->param('code', '', new Text(2048, 0), 'OAuth2 code. This is a temporary code that the will be later exchanged for an access token.', true)
->param('state', '', new Text(2048), 'Login state params.', true)
->param('error', '', new Text(2048, 0), 'Error code returned from the OAuth2 provider.', true)
@ -430,7 +445,7 @@ App::get('/v1/account/sessions/oauth2/:provider/redirect')
->label('docs', false)
->label('usage.metric', 'sessions.{scope}.requests.create')
->label('usage.params', ['provider:{request.provider}'])
->param('provider', '', new WhiteList(\array_keys(Config::getParam('providers')), true), 'OAuth2 provider.')
->param('provider', '', new WhiteList(\array_keys(Config::getParam('oAuthProviders')), true), 'OAuth2 provider.')
->param('code', '', new Text(2048, 0), 'OAuth2 code. This is a temporary code that the will be later exchanged for an access token.', true)
->param('state', '', new Text(2048), 'OAuth2 state params.', true)
->param('error', '', new Text(2048, 0), 'Error code returned from the OAuth2 provider.', true)
@ -448,9 +463,9 @@ App::get('/v1/account/sessions/oauth2/:provider/redirect')
$callback = $protocol . '://' . $request->getHostname() . '/v1/account/sessions/oauth2/callback/' . $provider . '/' . $project->getId();
$defaultState = ['success' => $project->getAttribute('url', ''), 'failure' => ''];
$validateURL = new URL();
$appId = $project->getAttribute('authProviders', [])[$provider . 'Appid'] ?? '';
$appSecret = $project->getAttribute('authProviders', [])[$provider . 'Secret'] ?? '{}';
$providerEnabled = $project->getAttribute('authProviders', [])[$provider . 'Enabled'] ?? false;
$appId = $project->getAttribute('oAuthProviders', [])[$provider . 'Appid'] ?? '';
$appSecret = $project->getAttribute('oAuthProviders', [])[$provider . 'Secret'] ?? '{}';
$providerEnabled = $project->getAttribute('oAuthProviders', [])[$provider . 'Enabled'] ?? false;
$className = 'Appwrite\\Auth\\OAuth2\\' . \ucfirst($provider);
@ -458,7 +473,7 @@ App::get('/v1/account/sessions/oauth2/:provider/redirect')
throw new Exception(Exception::PROJECT_PROVIDER_UNSUPPORTED);
}
$providers = Config::getParam('providers');
$providers = Config::getParam('oAuthProviders');
$providerName = $providers[$provider]['name'] ?? '';
/** @var Appwrite\Auth\OAuth2 $oauth2 */
@ -664,7 +679,18 @@ App::get('/v1/account/sessions/oauth2/:provider/redirect')
'accessedAt' => DateTime::now(),
]);
$user->removeAttribute('$internalId');
Authorization::skip(fn() => $dbForProject->createDocument('users', $user));
$userDoc = Authorization::skip(fn() => $dbForProject->createDocument('users', $user));
$dbForProject->createDocument('targets', new Document([
'$permissions' => [
Permission::read(Role::any()),
Permission::update(Role::user($user->getId())),
Permission::delete(Role::user($user->getId())),
],
'userId' => $userDoc->getId(),
'userInternalId' => $userDoc->getInternalId(),
'providerType' => MESSAGE_TYPE_EMAIL,
'identifier' => $email,
]));
} catch (Duplicate) {
$failureRedirect(Exception::USER_ALREADY_EXISTS);
}
@ -1243,8 +1269,7 @@ App::post('/v1/account/sessions/phone')
->inject('queueForEvents')
->inject('queueForMessaging')
->inject('locale')
->action(function (string $userId, string $phone, Request $request, Response $response, Document $user, Document $project, Database $dbForProject, Event $queueForEvents, EventPhone $queueForMessaging, Locale $locale) {
->action(function (string $userId, string $phone, Request $request, Response $response, Document $user, Document $project, Database $dbForProject, Event $queueForEvents, Messaging $queueForMessaging, Locale $locale) {
if (empty(App::getEnv('_APP_SMS_PROVIDER'))) {
throw new Exception(Exception::GENERAL_PHONE_DISABLED, 'Phone provider not configured');
}
@ -1294,6 +1319,21 @@ App::post('/v1/account/sessions/phone')
$user->removeAttribute('$internalId');
Authorization::skip(fn () => $dbForProject->createDocument('users', $user));
try {
$target = Authorization::skip(fn() => $dbForProject->createDocument('targets', new Document([
'userId' => $user->getId(),
'userInternalId' => $user->getInternalId(),
'providerType' => MESSAGE_TYPE_SMS,
'identifier' => $phone,
])));
$user->setAttribute('targets', [...$user->getAttribute('targets', []), $target]);
} catch (Duplicate) {
$existingTarget = $dbForProject->findOne('targets', [
Query::equal('identifier', [$phone]),
]);
$user->setAttribute('targets', [...$user->getAttribute('targets', []), $existingTarget]);
}
$dbForProject->deleteCachedDocument('users', $user->getId());
}
$secret = Auth::codeGenerator();
@ -1331,9 +1371,19 @@ App::post('/v1/account/sessions/phone')
$message = $message->setParam('{{token}}', $secret);
$message = $message->render();
$messageDoc = new Document([
'$id' => $token->getId(),
'data' => [
'content' => $message,
],
]);
$queueForMessaging
->setRecipient($phone)
->setMessage($message)
->setMessage($messageDoc)
->setRecipients([$phone])
->setProviderType(MESSAGE_TYPE_SMS)
->setProject($project)
->trigger();
$queueForEvents->setPayload(
@ -1652,6 +1702,81 @@ App::post('/v1/account/jwt')
])]), Response::MODEL_JWT);
});
App::post('/v1/account/targets/push')
->desc('Create Account\'s push target')
->groups(['api', 'account'])
->label('error', __DIR__ . '/../../views/general/error.phtml')
->label('audits.event', 'target.create')
->label('audits.resource', 'target/response.$id')
->label('event', 'users.[userId].targets.[targetId].create')
->label('sdk.auth', [APP_AUTH_TYPE_SESSION])
->label('sdk.namespace', 'account')
->label('sdk.method', 'createPushTarget')
->label('sdk.response.code', Response::STATUS_CODE_CREATED)
->label('sdk.response.type', Response::CONTENT_TYPE_JSON)
->label('sdk.response.model', Response::MODEL_TARGET)
->label('docs', false)
->param('targetId', '', new CustomId(), 'Target 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('providerId', '', new UID(), 'Provider ID. Message will be sent to this target from the specified provider ID. If no provider ID is set the first setup provider will be used.')
->param('identifier', '', new Text(Database::LENGTH_KEY), 'The target identifier (token, email, phone etc.)')
->inject('queueForEvents')
->inject('user')
->inject('request')
->inject('response')
->inject('dbForProject')
->action(function (string $targetId, string $providerId, string $identifier, Event $queueForEvents, Document $user, Request $request, Response $response, Database $dbForProject) {
$targetId = $targetId == 'unique()' ? ID::unique() : $targetId;
$provider = Authorization::skip(fn () => $dbForProject->getDocument('providers', $providerId));
if ($provider->isEmpty()) {
throw new Exception(Exception::PROVIDER_NOT_FOUND);
}
if ($user->isEmpty()) {
throw new Exception(Exception::USER_NOT_FOUND);
}
$target = Authorization::skip(fn () => $dbForProject->getDocument('targets', $targetId));
if (!$target->isEmpty()) {
throw new Exception(Exception::USER_TARGET_ALREADY_EXISTS);
}
$detector = new Detector($request->getUserAgent());
$detector->skipBotDetection(); // OPTIONAL: If called, bot detection will completely be skipped (bots will be detected as regular devices then)
$device = $detector->getDevice();
try {
$target = $dbForProject->createDocument('targets', new Document([
'$id' => $targetId,
'$permissions' => [
Permission::read(Role::user($user->getId())),
Permission::update(Role::user($user->getId())),
],
'providerId' => $providerId ?? null,
'providerInternalId' => $provider->getInternalId() ?? null,
'providerType' => MESSAGE_TYPE_PUSH,
'userId' => $user->getId(),
'userInternalId' => $user->getInternalId(),
'identifier' => $identifier,
'name' => "{$device['deviceBrand']} {$device['deviceModel']}"
]));
} catch (Duplicate) {
throw new Exception(Exception::USER_TARGET_ALREADY_EXISTS);
}
$dbForProject->deleteCachedDocument('users', $user->getId());
$queueForEvents
->setParam('userId', $user->getId())
->setParam('targetId', $target->getId());
$response
->setStatusCode(Response::STATUS_CODE_CREATED)
->dynamic($target, Response::MODEL_TARGET);
});
App::get('/v1/account')
->desc('Get account')
->groups(['api', 'account'])
@ -1977,6 +2102,7 @@ App::patch('/v1/account/email')
throw new Exception(Exception::USER_INVALID_CREDENTIALS);
}
$oldEmail = $user->getAttribute('email');
$email = \strtolower($email);
// Makes sure this email is not already used in another identity
@ -2001,8 +2127,25 @@ App::patch('/v1/account/email')
->setAttribute('passwordUpdate', DateTime::now());
}
$target = Authorization::skip(fn () => $dbForProject->findOne('targets', [
Query::equal('identifier', [$email]),
]));
if ($target instanceof Document && !$target->isEmpty()) {
throw new Exception(Exception::USER_TARGET_ALREADY_EXISTS);
}
try {
$user = $dbForProject->withRequestTimestamp($requestTimestamp, fn () => $dbForProject->updateDocument('users', $user->getId(), $user));
/**
* @var Document $oldTarget
*/
$oldTarget = $user->find('identifier', $oldEmail, 'targets');
if ($oldTarget instanceof Document && !$oldTarget->isEmpty()) {
Authorization::skip(fn () => $dbForProject->updateDocument('targets', $oldTarget->getId(), $oldTarget->setAttribute('identifier', $email)));
}
$dbForProject->deleteCachedDocument('users', $user->getId());
} catch (Duplicate) {
throw new Exception(Exception::USER_EMAIL_ALREADY_EXISTS);
}
@ -2047,6 +2190,16 @@ App::patch('/v1/account/phone')
throw new Exception(Exception::USER_INVALID_CREDENTIALS);
}
$target = Authorization::skip(fn () => $dbForProject->findOne('targets', [
Query::equal('identifier', [$phone]),
]));
if ($target instanceof Document && !$target->isEmpty()) {
throw new Exception(Exception::USER_TARGET_ALREADY_EXISTS);
}
$oldPhone = $user->getAttribute('phone');
$user
->setAttribute('phone', $phone)
->setAttribute('phoneVerification', false) // After this user needs to confirm phone number again
@ -2062,6 +2215,15 @@ App::patch('/v1/account/phone')
try {
$user = $dbForProject->withRequestTimestamp($requestTimestamp, fn () => $dbForProject->updateDocument('users', $user->getId(), $user));
/**
* @var Document $oldTarget
*/
$oldTarget = $user->find('identifier', $oldPhone, 'targets');
if ($oldTarget instanceof Document && !$oldTarget->isEmpty()) {
Authorization::skip(fn () => $dbForProject->updateDocument('targets', $oldTarget->getId(), $oldTarget->setAttribute('identifier', $phone)));
}
$dbForProject->deleteCachedDocument('users', $user->getId());
} catch (Duplicate $th) {
throw new Exception(Exception::USER_PHONE_ALREADY_EXISTS);
}
@ -2272,8 +2434,8 @@ App::patch('/v1/account/sessions/:sessionId')
$provider = $session->getAttribute('provider');
$refreshToken = $session->getAttribute('providerRefreshToken');
$appId = $project->getAttribute('authProviders', [])[$provider . 'Appid'] ?? '';
$appSecret = $project->getAttribute('authProviders', [])[$provider . 'Secret'] ?? '{}';
$appId = $project->getAttribute('oAuthProviders', [])[$provider . 'Appid'] ?? '';
$appSecret = $project->getAttribute('oAuthProviders', [])[$provider . 'Secret'] ?? '{}';
$className = 'Appwrite\\Auth\\OAuth2\\' . \ucfirst($provider);
@ -2570,8 +2732,8 @@ App::put('/v1/account/recovery')
->label('abuse-key', 'url:{url},userId:{param-userId}')
->param('userId', '', new UID(), 'User ID.')
->param('secret', '', new Text(256), 'Valid reset token.')
->param('password', '', new Password(), 'New user password. Must be at least 8 chars.')
->param('passwordAgain', '', new Password(), 'Repeat new user password. Must be at least 8 chars.')
->param('password', '', fn ($project, $passwordsDictionary) => new PasswordDictionary($passwordsDictionary, $project->getAttribute('auths', [])['passwordDictionary'] ?? false), 'New user password. Must be between 8 and 256 chars.', false, ['project', 'passwordsDictionary'])
->param('passwordAgain', '', fn ($project, $passwordsDictionary) => new PasswordDictionary($passwordsDictionary, $project->getAttribute('auths', [])['passwordDictionary'] ?? false), 'Repeat new user password. Must be between 8 and 256 chars.', false, ['project', 'passwordsDictionary'])
->inject('response')
->inject('user')
->inject('dbForProject')
@ -2885,10 +3047,9 @@ App::post('/v1/account/verification/phone')
->inject('queueForMessaging')
->inject('project')
->inject('locale')
->action(function (Request $request, Response $response, Document $user, Database $dbForProject, Event $queueForEvents, EventPhone $queueForMessaging, Document $project, Locale $locale) {
->action(function (Request $request, Response $response, Document $user, Database $dbForProject, Event $queueForEvents, Messaging $queueForMessaging, Document $project, Locale $locale) {
if (empty(App::getEnv('_APP_SMS_PROVIDER'))) {
throw new Exception(Exception::GENERAL_PHONE_DISABLED);
throw new Exception(Exception::GENERAL_PHONE_DISABLED, 'Phone provider not configured');
}
if (empty($user->getAttribute('phone'))) {
@ -2937,11 +3098,19 @@ App::post('/v1/account/verification/phone')
$message = $message->setParam('{{token}}', $secret);
$message = $message->render();
$messageDoc = new Document([
'$id' => $verification->getId(),
'data' => [
'content' => $message,
],
]);
$queueForMessaging
->setRecipient($user->getAttribute('phone'))
->setMessage($message)
->trigger()
;
->setMessage($messageDoc)
->setRecipients([$user->getAttribute('phone')])
->setProviderType(MESSAGE_TYPE_SMS)
->setProject($project)
->trigger();
$queueForEvents
->setParam('userId', $user->getId())
@ -3018,3 +3187,61 @@ App::put('/v1/account/verification/phone')
$response->dynamic($verificationDocument, Response::MODEL_TOKEN);
});
App::put('/v1/account/targets/:targetId/push')
->desc('Update Account\'s push target')
->groups(['api', 'account'])
->label('error', __DIR__ . '/../../views/general/error.phtml')
->label('audits.event', 'target.update')
->label('audits.resource', 'target/response.$id')
->label('event', 'users.[userId].targets.[targetId].update')
->label('sdk.auth', [APP_AUTH_TYPE_SESSION])
->label('sdk.namespace', 'account')
->label('sdk.method', 'updatePushTarget')
->label('sdk.response.code', Response::STATUS_CODE_OK)
->label('sdk.response.type', Response::CONTENT_TYPE_JSON)
->label('sdk.response.model', Response::MODEL_TARGET)
->label('docs', false)
->param('targetId', '', new UID(), 'Target ID.')
->param('identifier', '', new Text(Database::LENGTH_KEY), 'The target identifier (token, email, phone etc.)')
->inject('queueForEvents')
->inject('user')
->inject('request')
->inject('response')
->inject('dbForProject')
->action(function (string $targetId, string $identifier, Event $queueForEvents, Document $user, Request $request, Response $response, Database $dbForProject) {
if ($user->isEmpty()) {
throw new Exception(Exception::USER_NOT_FOUND);
}
$target = Authorization::skip(fn () => $dbForProject->getDocument('targets', $targetId));
if ($target->isEmpty()) {
throw new Exception(Exception::USER_TARGET_NOT_FOUND);
}
if ($user->getId() !== $target->getAttribute('userId')) {
throw new Exception(Exception::USER_TARGET_NOT_FOUND);
}
if ($identifier) {
$target->setAttribute('identifier', $identifier);
}
$detector = new Detector($request->getUserAgent());
$detector->skipBotDetection(); // OPTIONAL: If called, bot detection will completely be skipped (bots will be detected as regular devices then)
$device = $detector->getDevice();
$target->setAttribute('name', "{$device['deviceBrand']} {$device['deviceModel']}");
$target = $dbForProject->updateDocument('targets', $target->getId(), $target);
$dbForProject->deleteCachedDocument('users', $user->getId());
$queueForEvents
->setParam('userId', $user->getId())
->setParam('targetId', $target->getId());
$response
->dynamic($target, Response::MODEL_TARGET);
});

View file

@ -84,8 +84,8 @@ $getUserGitHub = function (string $userId, Document $project, Database $dbForPro
$accessTokenExpiry = $gitHubSession->getAttribute('providerAccessTokenExpiry');
$refreshToken = $gitHubSession->getAttribute('providerRefreshToken');
$appId = $project->getAttribute('authProviders', [])[$provider . 'Appid'] ?? '';
$appSecret = $project->getAttribute('authProviders', [])[$provider . 'Secret'] ?? '{}';
$appId = $project->getAttribute('oAuthProviders', [])[$provider . 'Appid'] ?? '';
$appSecret = $project->getAttribute('oAuthProviders', [])[$provider . 'Secret'] ?? '{}';
$className = 'Appwrite\\Auth\\OAuth2\\' . \ucfirst($provider);

View file

@ -1561,7 +1561,7 @@ App::post('/v1/databases/:databaseId/collections/:collectionId/attributes/dateti
->param('collectionId', '', new UID(), 'Collection ID. You can create a new collection using the Database service [server integration](https://appwrite.io/docs/server/databases#databasesCreateCollection).')
->param('key', '', new Key(), 'Attribute Key.')
->param('required', null, new Boolean(), 'Is attribute required?')
->param('default', null, new DatetimeValidator(), 'Default value for the attribute in ISO 8601 format. Cannot be set when attribute is required.', true)
->param('default', null, new DatetimeValidator(), 'Default value for the attribute in [ISO 8601](https://www.iso.org/iso-8601-date-and-time-format.html) format. Cannot be set when attribute is required.', true)
->param('array', false, new Boolean(), 'Is attribute an array?', true)
->inject('response')
->inject('dbForProject')

File diff suppressed because it is too large Load diff

View file

@ -159,7 +159,7 @@ App::post('/v1/projects')
'legalTaxId' => ID::custom($legalTaxId),
'services' => new stdClass(),
'platforms' => null,
'authProviders' => [],
'oAuthProviders' => [],
'webhooks' => null,
'keys' => null,
'auths' => $auths,
@ -598,7 +598,7 @@ App::patch('/v1/projects/:projectId/oauth2')
->label('sdk.response.type', Response::CONTENT_TYPE_JSON)
->label('sdk.response.model', Response::MODEL_PROJECT)
->param('projectId', '', new UID(), 'Project unique ID.')
->param('provider', '', new WhiteList(\array_keys(Config::getParam('providers')), true), 'Provider Name')
->param('provider', '', new WhiteList(\array_keys(Config::getParam('oAuthProviders')), true), 'Provider Name')
->param('appId', null, new Text(256), 'Provider app ID. Max length: 256 chars.', true)
->param('secret', null, new text(512), 'Provider secret key. Max length: 512 chars.', true)
->param('enabled', null, new Boolean(), 'Provider status. Set to \'false\' to disable new session creation.', true)
@ -612,7 +612,7 @@ App::patch('/v1/projects/:projectId/oauth2')
throw new Exception(Exception::PROJECT_NOT_FOUND);
}
$providers = $project->getAttribute('authProviders', []);
$providers = $project->getAttribute('oAuthProviders', []);
if ($appId !== null) {
$providers[$provider . 'Appid'] = $appId;
@ -626,7 +626,7 @@ App::patch('/v1/projects/:projectId/oauth2')
$providers[$provider . 'Enabled'] = $enabled;
}
$project = $dbForConsole->updateDocument('projects', $project->getId(), $project->setAttribute('authProviders', $providers));
$project = $dbForConsole->updateDocument('projects', $project->getId(), $project->setAttribute('oAuthProviders', $providers));
$response->dynamic($project, Response::MODEL_PROJECT);
});
@ -1151,7 +1151,7 @@ App::post('/v1/projects/:projectId/keys')
->param('projectId', '', new UID(), 'Project unique ID.')
->param('name', null, new Text(128), 'Key name. Max length: 128 chars.')
->param('scopes', null, new ArrayList(new WhiteList(array_keys(Config::getParam('scopes')), true), APP_LIMIT_ARRAY_PARAMS_SIZE), 'Key scopes list. Maximum of ' . APP_LIMIT_ARRAY_PARAMS_SIZE . ' scopes are allowed.')
->param('expire', null, new DatetimeValidator(), 'Expiration time in ISO 8601 format. Use null for unlimited expiration.', true)
->param('expire', null, new DatetimeValidator(), 'Expiration time in [ISO 8601](https://www.iso.org/iso-8601-date-and-time-format.html) format. Use null for unlimited expiration.', true)
->inject('response')
->inject('dbForConsole')
->action(function (string $projectId, string $name, array $scopes, ?string $expire, Response $response, Database $dbForConsole) {
@ -1268,7 +1268,7 @@ App::put('/v1/projects/:projectId/keys/:keyId')
->param('keyId', '', new UID(), 'Key unique ID.')
->param('name', null, new Text(128), 'Key name. Max length: 128 chars.')
->param('scopes', null, new ArrayList(new WhiteList(array_keys(Config::getParam('scopes')), true), APP_LIMIT_ARRAY_PARAMS_SIZE), 'Key scopes list. Maximum of ' . APP_LIMIT_ARRAY_PARAMS_SIZE . ' events are allowed.')
->param('expire', null, new DatetimeValidator(), 'Expiration time in ISO 8601 format. Use null for unlimited expiration.', true)
->param('expire', null, new DatetimeValidator(), 'Expiration time in [ISO 8601](https://www.iso.org/iso-8601-date-and-time-format.html) format. Use null for unlimited expiration.', true)
->inject('response')
->inject('dbForConsole')
->action(function (string $projectId, string $keyId, string $name, array $scopes, ?string $expire, Response $response, Database $dbForConsole) {

View file

@ -6,7 +6,7 @@ use Appwrite\Detector\Detector;
use Appwrite\Event\Delete;
use Appwrite\Event\Event;
use Appwrite\Event\Mail;
use Appwrite\Event\Phone as EventPhone;
use Appwrite\Event\Messaging;
use Appwrite\Extend\Exception;
use Appwrite\Network\Validator\Email;
use Utopia\Validator\Host;
@ -388,7 +388,7 @@ App::post('/v1/teams/:teamId/memberships')
->inject('queueForMails')
->inject('queueForMessaging')
->inject('queueForEvents')
->action(function (string $teamId, string $email, string $userId, string $phone, array $roles, string $url, string $name, Response $response, Document $project, Document $user, Database $dbForProject, Locale $locale, Mail $queueForMails, EventPhone $queueForMessaging, Event $queueForEvents) {
->action(function (string $teamId, string $email, string $userId, string $phone, array $roles, string $url, string $name, Response $response, Document $project, Document $user, Database $dbForProject, Locale $locale, Mail $queueForMails, Messaging $queueForMessaging, Event $queueForEvents) {
$isAPIKey = Auth::isAppUser(Authorization::getRoles());
$isPrivilegedUser = Auth::isPrivilegedUser(Authorization::getRoles());
@ -628,6 +628,10 @@ App::post('/v1/teams/:teamId/memberships')
->trigger()
;
} elseif (!empty($phone)) {
if (empty(App::getEnv('_APP_SMS_PROVIDER'))) {
throw new Exception(Exception::GENERAL_PHONE_DISABLED, 'Phone provider not configured');
}
$message = Template::fromFile(__DIR__ . '/../../config/locale/templates/sms-base.tpl');
$customTemplate = $project->getAttribute('templates', [])['sms.invitation-' . $locale->default] ?? [];
@ -638,9 +642,18 @@ App::post('/v1/teams/:teamId/memberships')
$message = $message->setParam('{{token}}', $url);
$message = $message->render();
$messageDoc = new Document([
'$id' => ID::unique(),
'data' => [
'content' => $message,
],
]);
$queueForMessaging
->setRecipient($phone)
->setMessage($message)
->setMessage($messageDoc)
->setRecipients([$phone])
->setProviderType('SMS')
->setProject($project)
->trigger();
}
}

View file

@ -9,6 +9,7 @@ use Appwrite\Event\Event;
use Appwrite\Network\Validator\Email;
use Appwrite\Utopia\Database\Validator\CustomId;
use Appwrite\Utopia\Database\Validator\Queries\Identities;
use Appwrite\Utopia\Database\Validator\Queries\Targets;
use Utopia\Database\Validator\Queries;
use Appwrite\Utopia\Database\Validator\Queries\Users;
use Utopia\Database\Validator\Query\Limit;
@ -98,6 +99,42 @@ function createUser(string $hash, mixed $hashOptions, string $userId, ?string $e
'memberships' => null,
'search' => implode(' ', [$userId, $email, $phone, $name]),
]));
if ($email) {
try {
$target = $dbForProject->createDocument('targets', new Document([
'userId' => $user->getId(),
'userInternalId' => $user->getInternalId(),
'providerType' => 'email',
'identifier' => $email,
]));
$user->setAttribute('targets', [...$user->getAttribute('targets', []), $target]);
} catch (Duplicate) {
$existingTarget = $dbForProject->findOne('targets', [
Query::equal('identifier', [$email]),
]);
$user->setAttribute('targets', [...$user->getAttribute('targets', []), $existingTarget]);
}
}
if ($phone) {
try {
$target = $dbForProject->createDocument('targets', new Document([
'userId' => $user->getId(),
'userInternalId' => $user->getInternalId(),
'providerType' => 'sms',
'identifier' => $phone,
]));
$user->setAttribute('targets', [...$user->getAttribute('targets', []), $target]);
} catch (Duplicate) {
$existingTarget = $dbForProject->findOne('targets', [
Query::equal('identifier', [$phone]),
]);
$user->setAttribute('targets', [...$user->getAttribute('targets', []), $existingTarget]);
}
}
$dbForProject->deleteCachedDocument('users', $user->getId());
} catch (Duplicate $th) {
throw new Exception(Exception::USER_ALREADY_EXISTS);
}
@ -379,6 +416,98 @@ App::post('/v1/users/scrypt-modified')
->dynamic($user, Response::MODEL_USER);
});
App::post('/v1/users/:userId/targets')
->desc('Create User Target')
->groups(['api', 'users'])
->label('audits.event', 'target.create')
->label('audits.resource', 'target/response.$id')
->label('event', 'users.[userId].targets.[targetId].create')
->label('scope', 'targets.write')
->label('sdk.auth', [APP_AUTH_TYPE_KEY, APP_AUTH_TYPE_ADMIN])
->label('sdk.namespace', 'users')
->label('sdk.method', 'createTarget')
->label('sdk.description', '/docs/references/users/create-target.md')
->label('sdk.response.code', Response::STATUS_CODE_CREATED)
->label('sdk.response.type', Response::CONTENT_TYPE_JSON)
->label('sdk.response.model', Response::MODEL_TARGET)
->param('targetId', '', new CustomId(), 'Target 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('userId', '', new UID(), 'User ID.')
->param('providerType', '', new WhiteList([MESSAGE_TYPE_EMAIL, MESSAGE_TYPE_SMS, MESSAGE_TYPE_PUSH]), 'The target provider type. Can be one of the following: `email`, `sms` or `push`.')
->param('identifier', '', new Text(Database::LENGTH_KEY), 'The target identifier (token, email, phone etc.)')
->param('providerId', '', new UID(), 'Provider ID. Message will be sent to this target from the specified provider ID. If no provider ID is set the first setup provider will be used.', true)
->param('name', '', new Text(128), 'Target name. Max length: 128 chars. For example: My Awesome App Galaxy S23.', true)
->inject('queueForEvents')
->inject('response')
->inject('dbForProject')
->action(function (string $targetId, string $userId, string $providerType, string $identifier, string $providerId, string $name, Event $queueForEvents, Response $response, Database $dbForProject) {
$targetId = $targetId == 'unique()' ? ID::unique() : $targetId;
$provider = new Document();
if ($providerType === MESSAGE_TYPE_PUSH) {
$provider = $dbForProject->getDocument('providers', $providerId);
if ($provider->isEmpty()) {
throw new Exception(Exception::PROVIDER_NOT_FOUND);
}
}
switch ($providerType) {
case 'email':
$validator = new Email();
if (!$validator->isValid($identifier)) {
throw new Exception(Exception::GENERAL_INVALID_EMAIL);
}
break;
case MESSAGE_TYPE_SMS:
$validator = new Phone();
if (!$validator->isValid($identifier)) {
throw new Exception(Exception::GENERAL_INVALID_PHONE);
}
break;
case MESSAGE_TYPE_PUSH:
break;
default:
throw new Exception(Exception::PROVIDER_INCORRECT_TYPE);
}
$user = $dbForProject->getDocument('users', $userId);
if ($user->isEmpty()) {
throw new Exception(Exception::USER_NOT_FOUND);
}
$target = $dbForProject->getDocument('targets', $targetId);
if (!$target->isEmpty()) {
throw new Exception(Exception::USER_TARGET_ALREADY_EXISTS);
}
try {
$target = $dbForProject->createDocument('targets', new Document([
'$id' => $targetId,
'providerId' => $providerId ?? null,
'providerInternalId' => $provider->getInternalId() ?? null,
'providerType' => $providerType,
'userId' => $userId,
'userInternalId' => $user->getInternalId(),
'identifier' => $identifier,
'name' => ($name !== '') ? $name : null,
]));
} catch (Duplicate) {
throw new Exception(Exception::USER_TARGET_ALREADY_EXISTS);
}
$dbForProject->deleteCachedDocument('users', $user->getId());
$queueForEvents
->setParam('userId', $user->getId())
->setParam('targetId', $target->getId());
$response
->setStatusCode(Response::STATUS_CODE_CREATED)
->dynamic($target, Response::MODEL_TARGET);
});
App::get('/v1/users')
->desc('List users')
->groups(['api', 'users'])
@ -482,6 +611,38 @@ App::get('/v1/users/:userId/prefs')
$response->dynamic(new Document($prefs), Response::MODEL_PREFERENCES);
});
App::get('/v1/users/:userId/targets/:targetId')
->desc('Get User Target')
->groups(['api', 'users'])
->label('scope', 'targets.read')
->label('sdk.auth', [APP_AUTH_TYPE_KEY, APP_AUTH_TYPE_ADMIN])
->label('sdk.namespace', 'users')
->label('sdk.method', 'getTarget')
->label('sdk.description', '/docs/references/users/get-user-target.md')
->label('sdk.response.code', Response::STATUS_CODE_OK)
->label('sdk.response.type', Response::CONTENT_TYPE_JSON)
->label('sdk.response.model', Response::MODEL_TARGET)
->param('userId', '', new UID(), 'User ID.')
->param('targetId', '', new UID(), 'Target ID.')
->inject('response')
->inject('dbForProject')
->action(function (string $userId, string $targetId, Response $response, Database $dbForProject) {
$user = $dbForProject->getDocument('users', $userId);
if ($user->isEmpty()) {
throw new Exception(Exception::USER_NOT_FOUND);
}
$target = $user->find('$id', $targetId, 'targets');
if (empty($target)) {
throw new Exception(Exception::USER_TARGET_NOT_FOUND);
}
$response->dynamic($target, Response::MODEL_TARGET);
});
App::get('/v1/users/:userId/sessions')
->desc('List user sessions')
->groups(['api', 'users'])
@ -646,6 +807,53 @@ App::get('/v1/users/:userId/logs')
]), Response::MODEL_LOG_LIST);
});
App::get('/v1/users/:userId/targets')
->desc('List User Targets')
->groups(['api', 'users'])
->label('scope', 'targets.read')
->label('sdk.auth', [APP_AUTH_TYPE_KEY, APP_AUTH_TYPE_ADMIN])
->label('sdk.namespace', 'users')
->label('sdk.method', 'listTargets')
->label('sdk.description', '/docs/references/users/list-user-targets.md')
->label('sdk.response.code', Response::STATUS_CODE_OK)
->label('sdk.response.type', Response::CONTENT_TYPE_JSON)
->label('sdk.response.model', Response::MODEL_TARGET_LIST)
->param('userId', '', new UID(), 'User ID.')
->param('queries', [], new Targets(), 'Array of query strings generated using the Query class provided by the SDK. [Learn more about queries](https://appwrite.io/docs/queries). Maximum of ' . APP_LIMIT_ARRAY_PARAMS_SIZE . ' queries are allowed, each ' . APP_LIMIT_ARRAY_ELEMENT_SIZE . ' characters long. You may filter on the following attributes: ' . implode(', ', Users::ALLOWED_ATTRIBUTES), true)
->inject('response')
->inject('dbForProject')
->action(function (string $userId, array $queries, Response $response, Database $dbForProject) {
$user = $dbForProject->getDocument('users', $userId);
if ($user->isEmpty()) {
throw new Exception(Exception::USER_NOT_FOUND);
}
$queries = Query::parseQueries($queries);
$queries[] = Query::equal('userId', [$userId]);
// Get cursor document if there was a cursor query
$cursor = Query::getByType($queries, [Query::TYPE_CURSORAFTER, Query::TYPE_CURSORBEFORE]);
$cursor = reset($cursor);
if ($cursor) {
$targetId = $cursor->getValue();
$cursorDocument = $dbForProject->getDocument('targets', $targetId);
if ($cursorDocument->isEmpty()) {
throw new Exception(Exception::GENERAL_CURSOR_NOT_FOUND, "Target '{$targetId}' for the 'cursor' value not found.");
}
$cursor->setValue($cursorDocument);
}
$response->dynamic(new Document([
'targets' => $dbForProject->find('targets', $queries),
'total' => $dbForProject->count('targets', $queries, APP_LIMIT_COUNT),
]), Response::MODEL_TARGET_LIST);
});
App::get('/v1/users/identities')
->desc('List Identities')
->groups(['api', 'users'])
@ -949,6 +1157,16 @@ App::patch('/v1/users/:userId/email')
throw new Exception(Exception::USER_EMAIL_ALREADY_EXISTS);
}
$target = $dbForProject->findOne('targets', [
Query::equal('identifier', [$email]),
]);
if ($target instanceof Document && !$target->isEmpty()) {
throw new Exception(Exception::USER_TARGET_ALREADY_EXISTS);
}
$oldEmail = $user->getAttribute('email');
$user
->setAttribute('email', $email)
->setAttribute('emailVerification', false)
@ -957,6 +1175,15 @@ App::patch('/v1/users/:userId/email')
try {
$user = $dbForProject->updateDocument('users', $user->getId(), $user);
/**
* @var Document $oldTarget
*/
$oldTarget = $user->find('identifier', $oldEmail, 'targets');
if ($oldTarget instanceof Document && !$oldTarget->isEmpty()) {
$dbForProject->updateDocument('targets', $oldTarget->getId(), $oldTarget->setAttribute('identifier', $email));
}
$dbForProject->deleteCachedDocument('users', $user->getId());
} catch (Duplicate $th) {
throw new Exception(Exception::USER_EMAIL_ALREADY_EXISTS);
}
@ -994,13 +1221,32 @@ App::patch('/v1/users/:userId/phone')
throw new Exception(Exception::USER_NOT_FOUND);
}
$oldPhone = $user->getAttribute('phone');
$user
->setAttribute('phone', $number)
->setAttribute('phoneVerification', false)
;
$target = $dbForProject->findOne('targets', [
Query::equal('identifier', [$number]),
]);
if ($target instanceof Document && !$target->isEmpty()) {
throw new Exception(Exception::USER_TARGET_ALREADY_EXISTS);
}
try {
$user = $dbForProject->updateDocument('users', $user->getId(), $user);
/**
* @var Document $oldTarget
*/
$oldTarget = $user->find('identifier', $oldPhone, 'targets');
if ($oldTarget instanceof Document && !$oldTarget->isEmpty()) {
$dbForProject->updateDocument('targets', $oldTarget->getId(), $oldTarget->setAttribute('identifier', $number));
}
$dbForProject->deleteCachedDocument('users', $user->getId());
} catch (Duplicate $th) {
throw new Exception(Exception::USER_PHONE_ALREADY_EXISTS);
}
@ -1080,6 +1326,100 @@ App::patch('/v1/users/:userId/prefs')
$response->dynamic(new Document($prefs), Response::MODEL_PREFERENCES);
});
App::patch('/v1/users/:userId/targets/:targetId')
->desc('Update User target')
->groups(['api', 'users'])
->label('audits.event', 'target.update')
->label('audits.resource', 'target/{response.$id}')
->label('event', 'users.[userId].targets.[targetId].update')
->label('scope', 'targets.write')
->label('sdk.auth', [APP_AUTH_TYPE_KEY, APP_AUTH_TYPE_ADMIN])
->label('sdk.namespace', 'users')
->label('sdk.method', 'updateTarget')
->label('sdk.description', '/docs/references/users/update-target.md')
->label('sdk.response.code', Response::STATUS_CODE_OK)
->label('sdk.response.type', Response::CONTENT_TYPE_JSON)
->label('sdk.response.model', Response::MODEL_TARGET)
->param('userId', '', new UID(), 'User ID.')
->param('targetId', '', new UID(), 'Target ID.')
->param('identifier', '', new Text(Database::LENGTH_KEY), 'The target identifier (token, email, phone etc.)', true)
->param('providerId', '', new UID(), 'Provider ID. Message will be sent to this target from the specified provider ID. If no provider ID is set the first setup provider will be used.', true)
->param('name', '', new Text(128), 'Target name. Max length: 128 chars. For example: My Awesome App Galaxy S23.', true)
->inject('queueForEvents')
->inject('response')
->inject('dbForProject')
->action(function (string $userId, string $targetId, string $identifier, string $providerId, string $name, Event $queueForEvents, Response $response, Database $dbForProject) {
$user = $dbForProject->getDocument('users', $userId);
if ($user->isEmpty()) {
throw new Exception(Exception::USER_NOT_FOUND);
}
$target = $dbForProject->getDocument('targets', $targetId);
if ($target->isEmpty()) {
throw new Exception(Exception::USER_TARGET_NOT_FOUND);
}
if ($user->getId() !== $target->getAttribute('userId')) {
throw new Exception(Exception::USER_TARGET_NOT_FOUND);
}
if ($identifier) {
$providerType = $target->getAttribute('providerType');
switch ($providerType) {
case 'email':
$validator = new Email();
if (!$validator->isValid($identifier)) {
throw new Exception(Exception::GENERAL_INVALID_EMAIL);
}
break;
case MESSAGE_TYPE_SMS:
$validator = new Phone();
if (!$validator->isValid($identifier)) {
throw new Exception(Exception::GENERAL_INVALID_PHONE);
}
break;
case MESSAGE_TYPE_PUSH:
break;
default:
throw new Exception(Exception::PROVIDER_INCORRECT_TYPE);
}
$target->setAttribute('identifier', $identifier);
}
if ($providerId) {
$provider = $dbForProject->getDocument('providers', $providerId);
if ($provider->isEmpty()) {
throw new Exception(Exception::PROVIDER_NOT_FOUND);
}
if ($provider->getAttribute('type') !== $target->getAttribute('providerType')) {
throw new Exception(Exception::PROVIDER_INCORRECT_TYPE);
}
$target->setAttribute('providerId', $provider->getId());
$target->setAttribute('providerInternalId', $provider->getInternalId());
}
if ($name) {
$target->setAttribute('name', $name);
}
$target = $dbForProject->updateDocument('targets', $target->getId(), $target);
$dbForProject->deleteCachedDocument('users', $user->getId());
$queueForEvents
->setParam('userId', $user->getId())
->setParam('targetId', $target->getId());
$response
->dynamic($target, Response::MODEL_TARGET);
});
App::delete('/v1/users/:userId/sessions/:sessionId')
->desc('Delete user session')
->groups(['api', 'users'])
@ -1210,6 +1550,53 @@ App::delete('/v1/users/:userId')
$response->noContent();
});
App::delete('/v1/users/:userId/targets/:targetId')
->desc('Delete user target')
->groups(['api', 'users'])
->label('audits.event', 'target.delete')
->label('audits.resource', 'target/{request.$targetId}')
->label('event', 'users.[userId].targets.[targetId].delete')
->label('scope', 'targets.write')
->label('sdk.auth', [APP_AUTH_TYPE_KEY, APP_AUTH_TYPE_ADMIN])
->label('sdk.namespace', 'users')
->label('sdk.method', 'deleteTarget')
->label('sdk.description', '/docs/references/users/delete-target.md')
->label('sdk.response.code', Response::STATUS_CODE_NOCONTENT)
->label('sdk.response.type', Response::CONTENT_TYPE_JSON)
->label('sdk.response.model', Response::MODEL_NONE)
->param('userId', '', new UID(), 'User ID.')
->param('targetId', '', new UID(), 'Target ID.')
->inject('queueForEvents')
->inject('response')
->inject('dbForProject')
->action(function (string $userId, string $targetId, Event $queueForEvents, Response $response, Database $dbForProject) {
$user = $dbForProject->getDocument('users', $userId);
if ($user->isEmpty()) {
throw new Exception(Exception::USER_NOT_FOUND);
}
$target = $dbForProject->getDocument('targets', $targetId);
if ($target->isEmpty()) {
throw new Exception(Exception::USER_TARGET_NOT_FOUND);
}
if ($user->getId() !== $target->getAttribute('userId')) {
throw new Exception(Exception::USER_TARGET_NOT_FOUND);
}
$dbForProject->deleteDocument('targets', $target->getId());
$dbForProject->deleteCachedDocument('users', $user->getId());
$queueForEvents
->setParam('userId', $user->getId())
->setParam('targetId', $target->getId());
$response->noContent();
});
App::delete('/v1/users/identities/:identityId')
->desc('Delete Identity')
->groups(['api', 'users'])
@ -1251,7 +1638,7 @@ App::get('/v1/users/usage')
->label('sdk.response.type', Response::CONTENT_TYPE_JSON)
->label('sdk.response.model', Response::MODEL_USAGE_USERS)
->param('range', '30d', new WhiteList(['24h', '7d', '30d', '90d'], true), 'Date range.', true)
->param('provider', '', new WhiteList(\array_merge(['email', 'anonymous'], \array_map(fn ($value) => "oauth-" . $value, \array_keys(Config::getParam('providers', [])))), true), 'Provider Name.', true)
->param('provider', '', new WhiteList(\array_merge(['email', 'anonymous'], \array_map(fn ($value) => "oauth-" . $value, \array_keys(Config::getParam('oAuthProviders', [])))), true), 'Provider Name.', true)
->inject('response')
->inject('dbForProject')
->inject('register')

View file

@ -25,7 +25,7 @@ use Appwrite\Event\Audit;
use Appwrite\Event\Database as EventDatabase;
use Appwrite\Event\Event;
use Appwrite\Event\Mail;
use Appwrite\Event\Phone;
use Appwrite\Event\Messaging;
use Appwrite\Event\Delete;
use Appwrite\GraphQL\Schema;
use Appwrite\Network\Validator\Email;
@ -47,13 +47,7 @@ use Utopia\Database\Validator\Datetime as DatetimeValidator;
use Utopia\Database\Validator\Structure;
use Utopia\Locale\Locale;
use Utopia\DSN\DSN;
use Utopia\Messaging\Adapters\SMS\Mock;
use Appwrite\GraphQL\Promises\Adapter\Swoole;
use Utopia\Messaging\Adapters\SMS\Msg91;
use Utopia\Messaging\Adapters\SMS\Telesign;
use Utopia\Messaging\Adapters\SMS\TextMagic;
use Utopia\Messaging\Adapters\SMS\Twilio;
use Utopia\Messaging\Adapters\SMS\Vonage;
use Utopia\Registry\Registry;
use Utopia\Storage\Device;
use Utopia\Storage\Device\Backblaze;
@ -83,6 +77,7 @@ use Utopia\Validator\Range;
use Utopia\Validator\IP;
use Utopia\Validator\URL;
use Utopia\Validator\WhiteList;
use Utopia\CLI\Console;
const APP_NAME = 'Appwrite';
const APP_DOMAIN = 'appwrite.io';
@ -103,6 +98,7 @@ const APP_LIMIT_COMPRESSION = 20000000; //20MB
const APP_LIMIT_ARRAY_PARAMS_SIZE = 100; // Default maximum of how many elements can there be in API parameter that expects array value
const APP_LIMIT_ARRAY_ELEMENT_SIZE = 4096; // Default maximum length of element in array parameter represented by maximum URL length.
const APP_LIMIT_SUBQUERY = 1000;
const APP_LIMIT_SUBSCRIBERS_SUBQUERY = 1000000;
const APP_LIMIT_WRITE_RATE_DEFAULT = 60; // Default maximum write rate per rate period
const APP_LIMIT_WRITE_RATE_PERIOD_DEFAULT = 60; // Default maximum write rate period in seconds
const APP_LIMIT_LIST_DEFAULT = 25; // Default maximum number of items to return in list API calls
@ -173,6 +169,7 @@ const DELETE_TYPE_SESSIONS = 'sessions';
const DELETE_TYPE_CACHE_BY_TIMESTAMP = 'cacheByTimeStamp';
const DELETE_TYPE_CACHE_BY_RESOURCE = 'cacheByResource';
const DELETE_TYPE_SCHEDULES = 'schedules';
const DELETE_TYPE_TOPIC = 'topic';
// Mail Types
const MAIL_TYPE_VERIFICATION = 'verification';
const MAIL_TYPE_MAGIC_SESSION = 'magicSession';
@ -189,6 +186,10 @@ const MAX_OUTPUT_CHUNK_SIZE = 2 * 1024 * 1024; // 2MB
// Function headers
const FUNCTION_ALLOWLIST_HEADERS_REQUEST = ['content-type', 'agent', 'content-length', 'host'];
const FUNCTION_ALLOWLIST_HEADERS_RESPONSE = ['content-type', 'content-length'];
// Message types
const MESSAGE_TYPE_EMAIL = 'email';
const MESSAGE_TYPE_SMS = 'sms';
const MESSAGE_TYPE_PUSH = 'push';
// Usage metrics
const METRIC_TEAMS = 'teams';
const METRIC_USERS = 'users';
@ -233,7 +234,7 @@ App::setMode(App::getEnv('_APP_ENV', App::MODE_TYPE_PRODUCTION));
Config::load('events', __DIR__ . '/config/events.php');
Config::load('auth', __DIR__ . '/config/auth.php');
Config::load('errors', __DIR__ . '/config/errors.php');
Config::load('providers', __DIR__ . '/config/providers.php');
Config::load('oAuthProviders', __DIR__ . '/config/oAuthProviders.php');
Config::load('platforms', __DIR__ . '/config/platforms.php');
Config::load('collections', __DIR__ . '/config/collections.php');
Config::load('runtimes', __DIR__ . '/config/runtimes.php');
@ -522,6 +523,107 @@ Database::addFilter(
}
);
Database::addFilter(
'subQueryTargets',
function (mixed $value) {
return null;
},
function (mixed $value, Document $document, Database $database) {
return Authorization::skip(fn() => $database
->find('targets', [
Query::equal('userInternalId', [$document->getInternalId()]),
Query::limit(APP_LIMIT_SUBQUERY)
]));
}
);
Database::addFilter(
'subQueryTopicTargets',
function (mixed $value) {
return null;
},
function (mixed $value, Document $document, Database $database) {
$targetIds = Authorization::skip(fn () => \array_map(
fn ($document) => $document->getAttribute('targetId'),
$database
->find('subscribers', [
Query::equal('topicInternalId', [$document->getInternalId()]),
Query::limit(APP_LIMIT_SUBSCRIBERS_SUBQUERY)
])
));
if (\count($targetIds) > 0) {
return $database->find('targets', [Query::equal('$id', $targetIds)]);
}
return [];
}
);
Database::addFilter(
'providerSearch',
function (mixed $value, Document $provider) {
$searchValues = [
$provider->getId(),
$provider->getAttribute('name', ''),
$provider->getAttribute('provider', ''),
$provider->getAttribute('type', '')
];
$search = \implode(' ', \array_filter($searchValues));
return $search;
},
function (mixed $value) {
return $value;
}
);
Database::addFilter(
'topicSearch',
function (mixed $value, Document $topic) {
$searchValues = [
$topic->getId(),
$topic->getAttribute('name', ''),
$topic->getAttribute('description', ''),
];
$search = \implode(' ', \array_filter($searchValues));
return $search;
},
function (mixed $value) {
return $value;
}
);
Database::addFilter(
'messageSearch',
function (mixed $value, Document $message) {
$searchValues = [
$message->getId(),
$message->getAttribute('description', ''),
$message->getAttribute('status', ''),
];
$data = \json_decode($message->getAttribute('data', []), true);
$providerType = $message->getAttribute('providerType', '');
if ($providerType === MESSAGE_TYPE_EMAIL) {
$searchValues = \array_merge($searchValues, [$data['subject'], MESSAGE_TYPE_EMAIL]);
} elseif ($providerType === MESSAGE_TYPE_SMS) {
$searchValues = \array_merge($searchValues, [$data['content'], MESSAGE_TYPE_SMS]);
} else {
$searchValues = \array_merge($searchValues, [$data['title'], MESSAGE_TYPE_PUSH]);
}
$search = \implode(' ', \array_filter($searchValues));
return $search;
},
function (mixed $value) {
return $value;
}
);
/**
* DB Formats
*/
@ -902,7 +1004,7 @@ App::setResource('queue', function (Group $pools) {
return $pools->get('queue')->pop()->getResource();
}, ['pools']);
App::setResource('queueForMessaging', function (Connection $queue) {
return new Phone($queue);
return new Messaging($queue);
}, ['queue']);
App::setResource('queueForMails', function (Connection $queue) {
return new Mail($queue);
@ -1118,7 +1220,7 @@ App::setResource('console', function () {
],
'authWhitelistEmails' => (!empty(App::getEnv('_APP_CONSOLE_WHITELIST_EMAILS', null))) ? \explode(',', App::getEnv('_APP_CONSOLE_WHITELIST_EMAILS', null)) : [],
'authWhitelistIPs' => (!empty(App::getEnv('_APP_CONSOLE_WHITELIST_IPS', null))) ? \explode(',', App::getEnv('_APP_CONSOLE_WHITELIST_IPS', null)) : [],
'authProviders' => [
'oAuthProviders' => [
'githubEnabled' => true,
'githubSecret' => App::getEnv('_APP_CONSOLE_GITHUB_SECRET', ''),
'githubAppid' => App::getEnv('_APP_CONSOLE_GITHUB_APP_ID', '')
@ -1342,21 +1444,6 @@ App::setResource('passwordsDictionary', function ($register) {
return $register->get('passwordsDictionary');
}, ['register']);
App::setResource('sms', function () {
$dsn = new DSN(App::getEnv('_APP_SMS_PROVIDER'));
$user = $dsn->getUser();
$secret = $dsn->getPassword();
return match ($dsn->getHost()) {
'mock' => new Mock($user, $secret), // used for tests
'twilio' => new Twilio($user, $secret),
'text-magic' => new TextMagic($user, $secret),
'telesign' => new Telesign($user, $secret),
'msg91' => new Msg91($user, $secret),
'vonage' => new Vonage($user, $secret),
default => null
};
});
App::setResource('servers', function () {
$platforms = Config::getParam('platforms');

View file

@ -521,14 +521,20 @@ services:
environment:
- _APP_ENV
- _APP_WORKER_PER_CORE
- _APP_OPENSSL_KEY_V1
- _APP_REDIS_HOST
- _APP_REDIS_PORT
- _APP_REDIS_USER
- _APP_REDIS_PASS
- _APP_SMS_PROVIDER
- _APP_SMS_FROM
- _APP_DB_HOST
- _APP_DB_PORT
- _APP_DB_SCHEMA
- _APP_DB_USER
- _APP_DB_PASS
- _APP_LOGGING_PROVIDER
- _APP_LOGGING_CONFIG
- _APP_SMS_FROM
- _APP_SMS_PROVIDER
appwrite-worker-migrations:
image: <?php echo $organization; ?>/<?php echo $image; ?>:<?php echo $version."\n"; ?>

View file

@ -13,7 +13,6 @@ use Appwrite\Event\Hamster;
use Appwrite\Event\Mail;
use Appwrite\Event\Messaging;
use Appwrite\Event\Migration;
use Appwrite\Event\Phone;
use Appwrite\Platform\Appwrite;
use Appwrite\Usage\Stats;
use Swoole\Runtime;
@ -130,7 +129,7 @@ Server::setResource('queueForDatabase', function (Connection $queue) {
return new EventDatabase($queue);
}, ['queue']);
Server::setResource('queueForMessaging', function (Connection $queue) {
return new Phone($queue);
return new Messaging($queue);
}, ['queue']);
Server::setResource('queueForMails', function (Connection $queue) {
return new Mail($queue);

View file

@ -188,7 +188,9 @@ services:
- _APP_MIGRATIONS_FIREBASE_CLIENT_ID
- _APP_MIGRATIONS_FIREBASE_CLIENT_SECRET
- _APP_ASSISTANT_OPENAI_API_KEY
- _APP_MESSAGE_SMS_TEST_DSN
- _APP_MESSAGE_EMAIL_TEST_DSN
- _APP_MESSAGE_PUSH_TEST_DSN
appwrite-realtime:
entrypoint: realtime
<<: *x-logging
@ -568,14 +570,20 @@ services:
environment:
- _APP_ENV
- _APP_WORKER_PER_CORE
- _APP_OPENSSL_KEY_V1
- _APP_REDIS_HOST
- _APP_REDIS_PORT
- _APP_REDIS_USER
- _APP_REDIS_PASS
- _APP_SMS_PROVIDER
- _APP_SMS_FROM
- _APP_DB_HOST
- _APP_DB_PORT
- _APP_DB_SCHEMA
- _APP_DB_USER
- _APP_DB_PASS
- _APP_LOGGING_PROVIDER
- _APP_LOGGING_CONFIG
- _APP_SMS_FROM
- _APP_SMS_PROVIDER
appwrite-worker-migrations:
entrypoint: worker-migrations

View file

@ -37,7 +37,7 @@ Finally, you will need to create a `feat-XXX-YYY-oauth` branch based on the `mas
The first step in adding a new OAuth2 provider is to add it to the list of providers located at:
```
app/config/providers.php
app/config/oAuthProviders.php
```
Make sure to fill in all data needed and that your provider array key name:
@ -45,7 +45,7 @@ Make sure to fill in all data needed and that your provider array key name:
- is in [`camelCase`](https://en.wikipedia.org/wiki/Camel_case) format for sentence, but lowercase for names. `github` must be all lowercased, but `paypalSandbox` should have uppercase S
- has no spaces or special characters
> Please make sure to keep the list of providers in `providers.php` in the alphabetical order A-Z.
> Please make sure to keep the list of providers in `oAuthProviders.php` in the alphabetical order A-Z.
### 2.2 Add Provider Logo
@ -199,7 +199,7 @@ If you need any help with the contribution, feel free to head over to [our Disco
If your OAuth provider requires special configuration apart from `clientId` and `clientSecret` you can create a custom form. Currently this is being realized through putting all custom fields as JSON into the `clientSecret` field to keep the project API stable. You can implement your custom form following these steps:
1. Add your custom form in `app/views/console/users/oauth/[PROVIDER].phtml`. Below is a template you can use. Add the filename to `app/config/providers.php`.
1. Add your custom form in `app/views/console/users/oauth/[PROVIDER].phtml`. Below is a template you can use. Add the filename to `app/config/oAuthProviders.php`.
```php
<?php

View file

@ -32,6 +32,7 @@
<directory>./tests/e2e/Services/Projects</directory>
<directory>./tests/e2e/Services/Storage</directory>
<directory>./tests/e2e/Services/Webhooks</directory>
<directory>./tests/e2e/Services/Messaging</directory>
<file>./tests/e2e/Services/Functions/FunctionsBase.php</file>
<file>./tests/e2e/Services/Functions/FunctionsCustomServerTest.php</file>
<file>./tests/e2e/Services/Functions/FunctionsCustomClientTest.php</file>

View file

@ -20,7 +20,7 @@ class Password extends Validator
*/
public function getDescription(): string
{
return 'Password must be at least 8 characters';
return 'Password must be between 8 and 256 characters long.';
}
/**
@ -40,6 +40,10 @@ class Password extends Validator
return false;
}
if (\strlen($value) > 256) {
return false;
}
return true;
}

View file

@ -27,7 +27,7 @@ class PasswordDictionary extends Password
*/
public function getDescription(): string
{
return 'Password must be at least 8 characters and should not be one of the commonly used password.';
return 'Password must be between 8 and 265 characters long, and should not be one of the commonly used password.';
}
/**

View file

@ -0,0 +1,173 @@
<?php
namespace Appwrite\Event;
use Utopia\Database\Document;
use Utopia\Queue\Connection;
use Utopia\Queue\Client;
class Messaging extends Event
{
protected ?string $messageId = null;
protected ?Document $message = null;
protected ?array $recipients = null;
protected ?string $scheduledAt = null;
protected ?string $providerType = null;
public function __construct(protected Connection $connection)
{
parent::__construct($connection);
$this
->setQueue(Event::MESSAGING_QUEUE_NAME)
->setClass(Event::MESSAGING_CLASS_NAME);
}
/**
* Sets recipient for the messaging event.
*
* @param string[] $recipients
* @return self
*/
public function setRecipients(array $recipients): self
{
$this->recipients = $recipients;
return $this;
}
/**
* Returns set recipient for messaging event.
*
* @return string[]
*/
public function getRecipient(): array
{
return $this->recipients;
}
/**
* Sets message document for the messaging event.
*
* @param Document $message
* @return self
*/
public function setMessage(Document $message): self
{
$this->message = $message;
return $this;
}
/**
* Returns message document for the messaging event.
*
* @return string
*/
public function getMessage(): Document
{
return $this->message;
}
/**
* Sets message ID for the messaging event.
*
* @param string $message
* @return self
*/
public function setMessageId(string $messageId): self
{
$this->messageId = $messageId;
return $this;
}
/**
* Returns set message ID for the messaging event.
*
* @return string
*/
public function getMessageId(): string
{
return $this->messageId;
}
/**
* Sets provider type for the messaging event.
*
* @param string $providerType
* @return self
*/
public function setProviderType(string $providerType): self
{
$this->providerType = $providerType;
return $this;
}
/**
* Returns set provider type for the messaging event.
*
* @return string
*/
public function getProviderType(): string
{
return $this->providerType;
}
/**
* Sets Scheduled delivery time for the messaging event.
*
* @param string $scheduledAt
* @return self
*/
public function setScheduledAt(string $scheduledAt): self
{
$this->scheduledAt = $scheduledAt;
return $this;
}
/**
* Returns set Delivery Time for the messaging event.
*
* @return string
*/
public function getScheduledAt(): string
{
return $this->scheduledAt;
}
/**
* Set project for this event.
*
* @param Document $project
* @return self
*/
public function setProject(Document $project): self
{
$this->project = $project;
return $this;
}
/**
* Executes the event and sends it to the messaging worker.
* @return string|bool
* @throws \InvalidArgumentException
*/
public function trigger(): string | bool
{
$client = new Client($this->queue, $this->connection);
return $client->enqueue([
'project' => $this->project,
'user' => $this->user,
'messageId' => $this->messageId,
'message' => $this->message,
'recipients' => $this->recipients,
'providerType' => $this->providerType,
]);
}
}

View file

@ -1,87 +0,0 @@
<?php
namespace Appwrite\Event;
use Utopia\Queue\Client;
use Utopia\Queue\Connection;
class Phone extends Event
{
protected string $recipient = '';
protected string $message = '';
public function __construct(protected Connection $connection)
{
parent::__construct($connection);
$this
->setQueue(Event::MESSAGING_QUEUE_NAME)
->setClass(Event::MESSAGING_CLASS_NAME);
}
/**
* Sets recipient for the messaging event.
*
* @param string $recipient
* @return self
*/
public function setRecipient(string $recipient): self
{
$this->recipient = $recipient;
return $this;
}
/**
* Returns set recipient for this messaging event.
*
* @return string
*/
public function getRecipient(): string
{
return $this->recipient;
}
/**
* Sets url for the messaging event.
*
* @param string $message
* @return self
*/
public function setMessage(string $message): self
{
$this->message = $message;
return $this;
}
/**
* Returns set url for the messaging event.
*
* @return string
*/
public function getMessage(): string
{
return $this->message;
}
/**
* Executes the event and sends it to the messaging worker.
*
* @return string|bool
* @throws \InvalidArgumentException
*/
public function trigger(): string|bool
{
$client = new Client($this->queue, $this->connection);
return $client->enqueue([
'project' => $this->project,
'user' => $this->user,
'payload' => $this->payload,
'recipient' => $this->recipient,
'message' => $this->message,
'events' => Event::generateEvents($this->getEvent(), $this->getParams())
]);
}
}

View file

@ -55,6 +55,8 @@ class Exception extends \Exception
public const GENERAL_CODES_DISABLED = 'general_codes_disabled';
public const GENERAL_USAGE_DISABLED = 'general_usage_disabled';
public const GENERAL_NOT_IMPLEMENTED = 'general_not_implemented';
public const GENERAL_INVALID_EMAIL = 'general_invalid_email';
public const GENERAL_INVALID_PHONE = 'general_invalid_phone';
/** Users */
public const USER_COUNT_EXCEEDED = 'user_count_exceeded';
@ -86,6 +88,8 @@ class Exception extends \Exception
public const USER_OAUTH2_PROVIDER_ERROR = 'user_oauth2_provider_error';
public const USER_EMAIL_ALREADY_VERIFIED = 'user_email_alread_verified';
public const USER_PHONE_ALREADY_VERIFIED = 'user_phone_already_verified';
public const USER_TARGET_NOT_FOUND = 'user_target_not_found';
public const USER_TARGET_ALREADY_EXISTS = 'user_target_already_exists';
/** Teams */
public const TEAM_NOT_FOUND = 'team_not_found';
@ -236,6 +240,30 @@ class Exception extends \Exception
/** Health */
public const QUEUE_SIZE_EXCEEDED = 'queue_size_exceeded';
/** Provider */
public const PROVIDER_NOT_FOUND = 'provider_not_found';
public const PROVIDER_ALREADY_EXISTS = 'provider_already_exists';
public const PROVIDER_INCORRECT_TYPE = 'provider_incorrect_type';
public const PROVIDER_INTERNAL_UPDATE_DISABLED = 'provider_internal_update_disabled';
/** Topic */
public const TOPIC_NOT_FOUND = 'topic_not_found';
public const TOPIC_ALREADY_EXISTS = 'topic_already_exists';
/** Subscriber */
public const SUBSCRIBER_NOT_FOUND = 'subscriber_not_found';
public const SUBSCRIBER_ALREADY_EXISTS = 'subscriber_already_exists';
/** Message */
public const MESSAGE_NOT_FOUND = 'message_not_found';
public const MESSAGE_MISSING_TARGET = 'message_missing_target';
public const MESSAGE_ALREADY_SENT = 'message_already_sent';
public const MESSAGE_ALREADY_SCHEDULED = 'message_already_scheduled';
public const MESSAGE_TARGET_NOT_EMAIL = 'message_target_not_email';
public const MESSAGE_TARGET_NOT_SMS = 'message_target_not_sms';
public const MESSAGE_TARGET_NOT_PUSH = 'message_target_not_push';
protected string $type = '';
protected array $errors = [];
protected bool $publish = true;

View file

@ -34,7 +34,7 @@ class V15 extends Migration
['email', 'anonymous'],
\array_map(
fn ($value) => "oauth-" . $value,
\array_keys(Config::getParam('providers', []))
\array_keys(Config::getParam('oAuthProviders', []))
)
);

View file

@ -124,23 +124,23 @@ class V16 extends Migration
/**
* Enable OAuth providers with data
*/
$authProviders = $document->getAttribute('authProviders', []);
$oAuthProviders = $document->getAttribute('oAuthProviders', []);
foreach (Config::getParam('providers') as $provider => $value) {
foreach (Config::getParam('oAuthProviders') as $provider => $value) {
if (!$value['enabled']) {
continue;
}
if (($authProviders[$provider . 'Appid'] ?? false) && ($authProviders[$provider . 'Secret'] ?? false)) {
if (array_key_exists($provider . 'Enabled', $authProviders)) {
if (($oAuthProviders[$provider . 'Appid'] ?? false) && ($oAuthProviders[$provider . 'Secret'] ?? false)) {
if (array_key_exists($provider . 'Enabled', $oAuthProviders)) {
continue;
}
$authProviders[$provider . 'Enabled'] = true;
$oAuthProviders[$provider . 'Enabled'] = true;
}
}
$document->setAttribute('authProviders', $authProviders);
$document->setAttribute('oAuthProviders', $oAuthProviders);
break;
}

View file

@ -2,6 +2,7 @@
namespace Appwrite\Platform\Workers;
use Appwrite\Auth\Auth;
use Executor\Executor;
use Throwable;
use Utopia\Abuse\Abuse;
@ -149,6 +150,9 @@ class Deletes extends Action
case DELETE_TYPE_SCHEDULES:
$this->deleteSchedules($dbForConsole, $getProjectDB, $datetime);
break;
case DELETE_TYPE_TOPIC:
$this->deleteTopic($project, $getProjectDB, $document);
break;
default:
Console::error('No delete operation for type: ' . $type);
break;
@ -193,6 +197,25 @@ class Deletes extends Action
);
}
/**
* @param Document $project
* @param callable $getProjectDB
* @param Document $topic
* @throws Exception
*/
protected function deleteTopic(Document $project, callable $getProjectDB, Document $topic)
{
if ($topic->isEmpty()) {
Console::error('Failed to delete subscribers. Topic not found');
return;
}
$dbForProject = $getProjectDB($project);
$this->deleteByGroup('subscribers', [
Query::equal('topicInternalId', [$topic->getInternalId()])
], $dbForProject);
}
/**
* @param Document $project
* @param callable $getProjectDB
@ -533,6 +556,11 @@ class Deletes extends Action
$this->deleteByGroup('identities', [
Query::equal('userInternalId', [$userInternalId])
], $dbForProject);
// Delete targets
$this->deleteByGroup('targets', [
Query::equal('userInternalId', [$userInternalId])
], $dbForProject);
}
/**

View file

@ -2,30 +2,41 @@
namespace Appwrite\Platform\Workers;
use Exception;
use Appwrite\Extend\Exception;
use Utopia\App;
use Utopia\CLI\Console;
use Utopia\Database\Helpers\ID;
use Utopia\DSN\DSN;
use Utopia\Messaging\Messages\SMS;
use Utopia\Platform\Action;
use Utopia\Queue\Message;
use Utopia\Database\Database;
use Utopia\Database\DateTime;
use Utopia\Database\Document;
use Utopia\Database\Query;
use Utopia\Messaging\Adapters\SMS as SMSAdapter;
use Utopia\Messaging\Adapters\SMS\Mock;
use Utopia\Messaging\Adapters\SMS\Msg91;
use Utopia\Messaging\Adapters\SMS\Telesign;
use Utopia\Messaging\Adapters\SMS\TextMagic;
use Utopia\Messaging\Adapters\SMS\Textmagic;
use Utopia\Messaging\Adapters\SMS\Twilio;
use Utopia\Messaging\Adapters\SMS\Vonage;
use Utopia\Platform\Action;
use Utopia\Queue\Message;
use Utopia\Messaging\Adapters\Push as PushAdapter;
use Utopia\Messaging\Adapters\Push\APNS;
use Utopia\Messaging\Adapters\Push\FCM;
use Utopia\Messaging\Adapters\Email as EmailAdapter;
use Utopia\Messaging\Adapters\Email\Mailgun;
use Utopia\Messaging\Adapters\Email\SendGrid;
use Utopia\Messaging\Messages\Email;
use Utopia\Messaging\Messages\Push;
use Utopia\Messaging\Messages\SMS;
use function Swoole\Coroutine\batch;
class Messaging extends Action
{
private ?DSN $dsn = null;
private string $user = '';
private string $secret = '';
private string $provider = '';
public static function getName(): string
{
return 'messaging';
return "messaging";
}
/**
@ -33,25 +44,20 @@ class Messaging extends Action
*/
public function __construct()
{
$this->provider = App::getEnv('_APP_SMS_PROVIDER', '');
if (!empty($this->provider)) {
$this->dsn = new DSN($this->provider);
$this->user = $this->dsn->getUser();
$this->secret = $this->dsn->getPassword();
}
$this
->desc('Messaging worker')
->inject('message')
->callback(fn($message) => $this->action($message));
->inject('dbForProject')
->callback(fn(Message $message, Database $dbForProject) => $this->action($message, $dbForProject));
}
/**
* @param Message $message
* @param Database $dbForProject
* @return void
* @throws Exception
*/
public function action(Message $message): void
public function action(Message $message, Database $dbForProject): void
{
$payload = $message->getPayload() ?? [];
@ -60,48 +66,318 @@ class Messaging extends Action
return;
}
if (empty($payload['recipient'])) {
Console::error('Recipient arg not found');
if (!\is_null($payload['message']) && !\is_null($payload['recipients'])) {
if ($payload['providerType'] === MESSAGE_TYPE_SMS) {
$this->processInternalSMSMessage(new Document($payload['message']), $payload['recipients']);
}
} else {
$message = $dbForProject->getDocument('messages', $payload['messageId']);
$this->processMessage($dbForProject, $message);
}
}
private function processMessage(Database $dbForProject, Document $message): void
{
$topicsId = $message->getAttribute('topics', []);
$targetsId = $message->getAttribute('targets', []);
$usersId = $message->getAttribute('users', []);
/**
* @var Document[] $recipients
*/
$recipients = [];
if (\count($topicsId) > 0) {
$topics = $dbForProject->find('topics', [Query::equal('$id', $topicsId)]);
foreach ($topics as $topic) {
$targets = \array_filter($topic->getAttribute('targets'), fn (Document $target) => $target->getAttribute('providerType') === $message->getAttribute('providerType'));
$recipients = \array_merge($recipients, $targets);
}
}
if (\count($usersId) > 0) {
$users = $dbForProject->find('users', [Query::equal('$id', $usersId)]);
foreach ($users as $user) {
$targets = \array_filter($user->getAttribute('targets'), fn (Document $target) => $target->getAttribute('providerType') === $message->getAttribute('providerType'));
$recipients = \array_merge($recipients, $targets);
}
}
if (\count($targetsId) > 0) {
$targets = $dbForProject->find('targets', [Query::equal('$id', $targetsId)]);
$recipients = \array_merge($recipients, $targets);
}
$primaryProvider = $dbForProject->findOne('providers', [
Query::equal('enabled', [true]),
Query::equal('type', [$recipients[0]->getAttribute('providerType')]),
]);
/**
* @var array<string, array<string>> $identifiersByProviderId
*/
$identifiersByProviderId = [];
/**
* @var Document[] $providers
*/
$providers = [];
foreach ($recipients as $recipient) {
$providerId = $recipient->getAttribute('providerId');
if (!$providerId && $primaryProvider instanceof Document && !$primaryProvider->isEmpty()) {
$providerId = $primaryProvider->getId();
}
if ($providerId) {
if (!isset($identifiersByProviderId[$providerId])) {
$identifiersByProviderId[$providerId] = [];
}
$identifiersByProviderId[$providerId][] = $recipient->getAttribute('identifier');
}
}
/**
* @var array[] $results
*/
$results = batch(\array_map(function ($providerId) use ($identifiersByProviderId, $providers, $primaryProvider, $message, $dbForProject) {
return function () use ($providerId, $identifiersByProviderId, $providers, $primaryProvider, $message, $dbForProject) {
$provider = new Document();
if ($primaryProvider->getId() === $providerId) {
$provider = $primaryProvider;
} else {
$provider = $dbForProject->getDocument('providers', $providerId, [Query::equal('enabled', [true])]);
if ($provider->isEmpty()) {
$provider = $primaryProvider;
}
}
$providers[] = $provider;
$identifiers = $identifiersByProviderId[$providerId];
$adapter = match ($provider->getAttribute('type')) {
MESSAGE_TYPE_SMS => $this->sms($provider),
MESSAGE_TYPE_PUSH => $this->push($provider),
MESSAGE_TYPE_EMAIL => $this->email($provider),
default => throw new Exception(Exception::PROVIDER_INCORRECT_TYPE)
};
$maxBatchSize = $adapter->getMaxMessagesPerRequest();
$batches = \array_chunk($identifiers, $maxBatchSize);
$batchIndex = 0;
$results = batch(\array_map(function ($batch) use ($message, $provider, $adapter, $batchIndex) {
return function () use ($batch, $message, $provider, $adapter, $batchIndex) {
$deliveredTotal = 0;
$deliveryErrors = [];
$messageData = clone $message;
$messageData->setAttribute('to', $batch);
$data = match ($provider->getAttribute('type')) {
MESSAGE_TYPE_SMS => $this->buildSMSMessage($messageData, $provider),
MESSAGE_TYPE_PUSH => $this->buildPushMessage($messageData),
MESSAGE_TYPE_EMAIL => $this->buildEmailMessage($messageData, $provider),
default => throw new Exception(Exception::PROVIDER_INCORRECT_TYPE)
};
try {
$adapter->send($data);
$deliveredTotal += \count($batch);
} catch (\Exception $e) {
$deliveryErrors[] = 'Failed sending to targets ' . $batchIndex + 1 . '-' . \count($batch) . ' with error: ' . $e->getMessage();
} finally {
$batchIndex++;
return [
'deliveredTotal' => $deliveredTotal,
'deliveryErrors' => $deliveryErrors,
];
}
};
}, $batches));
return $results;
};
}, \array_keys($identifiersByProviderId)));
$results = array_merge(...$results);
$deliveredTotal = 0;
$deliveryErrors = [];
foreach ($results as $result) {
$deliveredTotal += $result['deliveredTotal'];
$deliveryErrors = \array_merge($deliveryErrors, $result['deliveryErrors']);
}
$message->setAttribute('deliveryErrors', $deliveryErrors);
if (\count($message->getAttribute('deliveryErrors')) > 0) {
$message->setAttribute('status', 'failed');
} else {
$message->setAttribute('status', 'sent');
}
$message->removeAttribute('to');
foreach ($providers as $provider) {
$message->setAttribute('search', "{$message->getAttribute('search')} {$provider->getAttribute('name')} {$provider->getAttribute('provider')} {$provider->getAttribute('type')}");
}
$message->setAttribute('deliveredTotal', $deliveredTotal);
$message->setAttribute('deliveredAt', DateTime::now());
$dbForProject->updateDocument('messages', $message->getId(), $message);
}
private function processInternalSMSMessage(Document $message, array $recipients): void
{
if (empty(App::getEnv('_APP_SMS_PROVIDER')) || empty(App::getEnv('_APP_SMS_FROM'))) {
Console::info('Skipped SMS processing. No Phone configuration has been set.');
return;
}
if (empty($payload['message'])) {
Console::error('Message arg not found');
return;
}
$sms = match ($this->dsn->getHost()) {
'mock' => new Mock($this->user, $this->secret), // used for tests
'twilio' => new Twilio($this->user, $this->secret),
'text-magic' => new TextMagic($this->user, $this->secret),
'telesign' => new Telesign($this->user, $this->secret),
'msg91' => new Msg91($this->user, $this->secret),
'vonage' => new Vonage($this->user, $this->secret),
default => null
};
if (empty(App::getEnv('_APP_SMS_PROVIDER'))) {
Console::error('Skipped sms processing. No Phone provider has been set.');
return;
}
$smsDSN = new DSN(App::getEnv('_APP_SMS_PROVIDER'));
$host = $smsDSN->getHost();
$password = $smsDSN->getPassword();
$user = $smsDSN->getUser();
$from = App::getEnv('_APP_SMS_FROM');
if (empty($from)) {
Console::error('Skipped sms processing. No phone number has been set.');
return;
}
$provider = new Document([
'$id' => ID::unique(),
'provider' => $host,
'type' => MESSAGE_TYPE_SMS,
'name' => 'Internal SMS',
'enabled' => true,
'credentials' => match ($host) {
'twilio' => [
'accountSid' => $user,
'authToken' => $password
],
'textmagic' => [
'username' => $user,
'apiKey' => $password
],
'telesign' => [
'username' => $user,
'password' => $password
],
'msg91' => [
'senderId' => $user,
'authKey' => $password
],
'vonage' => [
'apiKey' => $user,
'apiSecret' => $password
],
default => null
},
'options' => [
'from' => $from
]
]);
$message = new SMS(
to: [$payload['recipient']],
content: $payload['message'],
from: $from,
);
$adapter = $this->sms($provider);
try {
$sms->send($message);
} catch (\Exception $error) {
throw new Exception('Error sending message: ' . $error->getMessage(), 500);
}
$maxBatchSize = $adapter->getMaxMessagesPerRequest();
$batches = \array_chunk($recipients, $maxBatchSize);
$batchIndex = 0;
batch(\array_map(function ($batch) use ($message, $provider, $adapter, $batchIndex) {
return function () use ($batch, $message, $provider, $adapter, $batchIndex) {
$message->setAttribute('to', $batch);
$data = $this->buildSMSMessage($message, $provider);
try {
$adapter->send($data);
} catch (\Exception $e) {
Console::error('Failed sending to targets ' . $batchIndex + 1 . '-' . \count($batch) . ' with error: ' . $e->getMessage());
}
};
}, $batches));
}
public function shutdown(): void
{
}
private function sms(Document $provider): ?SMSAdapter
{
$credentials = $provider->getAttribute('credentials');
return match ($provider->getAttribute('provider')) {
'mock' => new Mock('username', 'password'),
'twilio' => new Twilio($credentials['accountSid'], $credentials['authToken']),
'textmagic' => new Textmagic($credentials['username'], $credentials['apiKey']),
'telesign' => new Telesign($credentials['username'], $credentials['password']),
'msg91' => new Msg91($credentials['senderId'], $credentials['authKey']),
'vonage' => new Vonage($credentials['apiKey'], $credentials['apiSecret']),
default => null
};
}
private function push(Document $provider): ?PushAdapter
{
$credentials = $provider->getAttribute('credentials');
return match ($provider->getAttribute('provider')) {
'apns' => new APNS(
$credentials['authKey'],
$credentials['authKeyId'],
$credentials['teamId'],
$credentials['bundleId'],
$credentials['endpoint']
),
'fcm' => new FCM($credentials['serverKey']),
default => null
};
}
private function email(Document $provider): ?EmailAdapter
{
$credentials = $provider->getAttribute('credentials');
return match ($provider->getAttribute('provider')) {
'mailgun' => new Mailgun($credentials['apiKey'], $credentials['domain'], $credentials['isEuRegion']),
'sendgrid' => new SendGrid($credentials['apiKey']),
default => null
};
}
private function buildEmailMessage(Document $message, Document $provider): Email
{
$from = $provider['options']['from'];
$to = $message['to'];
$subject = $message['data']['subject'];
$content = $message['data']['content'];
$html = $message['data']['html'];
return new Email($to, $subject, $content, $from, null, $html);
}
private function buildSMSMessage(Document $message, Document $provider): SMS
{
$to = $message['to'];
$content = $message['data']['content'];
$from = $provider['options']['from'];
return new SMS($to, $content, $from);
}
private function buildPushMessage(Document $message): Push
{
$to = $message['to'];
$title = $message['data']['title'];
$body = $message['data']['body'];
$data = $message['data']['data'];
$action = $message['data']['action'];
$sound = $message['data']['sound'];
$icon = $message['data']['icon'];
$color = $message['data']['color'];
$tag = $message['data']['tag'];
$badge = $message['data']['badge'];
return new Push($to, $title, $body, $data, $action, $sound, $icon, $color, $tag, $badge);
}
}

View file

@ -0,0 +1,28 @@
<?php
namespace Appwrite\Utopia\Database\Validator\Queries;
class Messages extends Base
{
public const ALLOWED_ATTRIBUTES = [
'topics',
'users',
'targets',
'providerId',
'deliveredAt',
'deliveredTo',
'deliveryErrors',
'status',
'description',
'data'
];
/**
* Expression constructor
*
*/
public function __construct()
{
parent::__construct('messages', self::ALLOWED_ATTRIBUTES);
}
}

View file

@ -0,0 +1,22 @@
<?php
namespace Appwrite\Utopia\Database\Validator\Queries;
class Providers extends Base
{
public const ALLOWED_ATTRIBUTES = [
'name',
'provider',
'type',
'enabled',
];
/**
* Expression constructor
*
*/
public function __construct()
{
parent::__construct('providers', self::ALLOWED_ATTRIBUTES);
}
}

View file

@ -0,0 +1,22 @@
<?php
namespace Appwrite\Utopia\Database\Validator\Queries;
class Subscribers extends Base
{
public const ALLOWED_ATTRIBUTES = [
'targetId',
'topicId',
'userId',
'providerType'
];
/**
* Expression constructor
*
*/
public function __construct()
{
parent::__construct('subscribers', self::ALLOWED_ATTRIBUTES);
}
}

View file

@ -0,0 +1,21 @@
<?php
namespace Appwrite\Utopia\Database\Validator\Queries;
class Targets extends Base
{
public const ALLOWED_ATTRIBUTES = [
'userId',
'providerId',
'identifier',
];
/**
* Expression constructor
*
*/
public function __construct()
{
parent::__construct('targets', self::ALLOWED_ATTRIBUTES);
}
}

View file

@ -0,0 +1,20 @@
<?php
namespace Appwrite\Utopia\Database\Validator\Queries;
class Topics extends Base
{
public const ALLOWED_ATTRIBUTES = [
'name',
'description',
];
/**
* Expression constructor
*
*/
public function __construct()
{
parent::__construct('topics', self::ALLOWED_ATTRIBUTES);
}
}

View file

@ -31,6 +31,7 @@ use Appwrite\Utopia\Response\Model\AttributeIP;
use Appwrite\Utopia\Response\Model\AttributeURL;
use Appwrite\Utopia\Response\Model\AttributeDatetime;
use Appwrite\Utopia\Response\Model\AttributeRelationship;
use Appwrite\Utopia\Response\Model\AuthProvider;
use Appwrite\Utopia\Response\Model\BaseList;
use Appwrite\Utopia\Response\Model\Branch;
use Appwrite\Utopia\Response\Model\Collection;
@ -80,8 +81,12 @@ use Appwrite\Utopia\Response\Model\HealthVersion;
use Appwrite\Utopia\Response\Model\Installation;
use Appwrite\Utopia\Response\Model\LocaleCode;
use Appwrite\Utopia\Response\Model\Provider;
use Appwrite\Utopia\Response\Model\Message;
use Appwrite\Utopia\Response\Model\Subscriber;
use Appwrite\Utopia\Response\Model\Topic;
use Appwrite\Utopia\Response\Model\ProviderRepository;
use Appwrite\Utopia\Response\Model\Runtime;
use Appwrite\Utopia\Response\Model\Target;
use Appwrite\Utopia\Response\Model\TemplateSMS;
use Appwrite\Utopia\Response\Model\UsageBuckets;
use Appwrite\Utopia\Response\Model\UsageCollection;
@ -191,6 +196,18 @@ class Response extends SwooleResponse
public const MODEL_PHONE = 'phone';
public const MODEL_PHONE_LIST = 'phoneList';
// Messaging
public const MODEL_PROVIDER = 'provider';
public const MODEL_PROVIDER_LIST = 'providerList';
public const MODEL_MESSAGE = 'message';
public const MODEL_MESSAGE_LIST = 'messageList';
public const MODEL_TOPIC = 'topic';
public const MODEL_TOPIC_LIST = 'topicList';
public const MODEL_SUBSCRIBER = 'subscriber';
public const MODEL_SUBSCRIBER_LIST = 'subscriberList';
public const MODEL_TARGET = 'target';
public const MODEL_TARGET_LIST = 'targetList';
// Teams
public const MODEL_TEAM = 'team';
public const MODEL_TEAM_LIST = 'teamList';
@ -238,8 +255,8 @@ class Response extends SwooleResponse
public const MODEL_WEBHOOK_LIST = 'webhookList';
public const MODEL_KEY = 'key';
public const MODEL_KEY_LIST = 'keyList';
public const MODEL_PROVIDER = 'provider';
public const MODEL_PROVIDER_LIST = 'providerList';
public const MODEL_AUTH_PROVIDER = 'authProvider';
public const MODEL_AUTH_PROVIDER_LIST = 'authProviderList';
public const MODEL_PLATFORM = 'platform';
public const MODEL_PLATFORM_LIST = 'platformList';
public const MODEL_VARIABLE = 'variable';
@ -316,7 +333,7 @@ class Response extends SwooleResponse
->setModel(new BaseList('Projects List', self::MODEL_PROJECT_LIST, 'projects', self::MODEL_PROJECT, true, false))
->setModel(new BaseList('Webhooks List', self::MODEL_WEBHOOK_LIST, 'webhooks', self::MODEL_WEBHOOK, true, false))
->setModel(new BaseList('API Keys List', self::MODEL_KEY_LIST, 'keys', self::MODEL_KEY, true, false))
->setModel(new BaseList('Providers List', self::MODEL_PROVIDER_LIST, 'platforms', self::MODEL_PROVIDER, true, false))
->setModel(new BaseList('Auth Providers List', self::MODEL_AUTH_PROVIDER_LIST, 'platforms', self::MODEL_AUTH_PROVIDER, true, false))
->setModel(new BaseList('Platforms List', self::MODEL_PLATFORM_LIST, 'platforms', self::MODEL_PLATFORM, true, false))
->setModel(new BaseList('Countries List', self::MODEL_COUNTRY_LIST, 'countries', self::MODEL_COUNTRY))
->setModel(new BaseList('Continents List', self::MODEL_CONTINENT_LIST, 'continents', self::MODEL_CONTINENT))
@ -328,6 +345,11 @@ class Response extends SwooleResponse
->setModel(new BaseList('Status List', self::MODEL_HEALTH_STATUS_LIST, 'statuses', self::MODEL_HEALTH_STATUS))
->setModel(new BaseList('Rule List', self::MODEL_PROXY_RULE_LIST, 'rules', self::MODEL_PROXY_RULE))
->setModel(new BaseList('Locale codes list', self::MODEL_LOCALE_CODE_LIST, 'localeCodes', self::MODEL_LOCALE_CODE))
->setModel(new BaseList('Provider list', self::MODEL_PROVIDER_LIST, 'providers', self::MODEL_PROVIDER))
->setModel(new BaseList('Message list', self::MODEL_MESSAGE_LIST, 'messages', self::MODEL_MESSAGE))
->setModel(new BaseList('Topic list', self::MODEL_TOPIC_LIST, 'topics', self::MODEL_TOPIC))
->setModel(new BaseList('Subscriber list', self::MODEL_SUBSCRIBER_LIST, 'subscribers', self::MODEL_SUBSCRIBER))
->setModel(new BaseList('Target list', self::MODEL_TARGET_LIST, 'targets', self::MODEL_TARGET))
->setModel(new BaseList('Migrations List', self::MODEL_MIGRATION_LIST, 'migrations', self::MODEL_MIGRATION))
->setModel(new BaseList('Migrations Firebase Projects List', self::MODEL_MIGRATION_FIREBASE_PROJECT_LIST, 'projects', self::MODEL_MIGRATION_FIREBASE_PROJECT))
// Entities
@ -380,7 +402,7 @@ class Response extends SwooleResponse
->setModel(new Project())
->setModel(new Webhook())
->setModel(new Key())
->setModel(new Provider())
->setModel(new AuthProvider())
->setModel(new Platform())
->setModel(new Variable())
->setModel(new Country())
@ -408,6 +430,11 @@ class Response extends SwooleResponse
->setModel(new TemplateSMS())
->setModel(new TemplateEmail())
->setModel(new ConsoleVariables())
->setModel(new Provider())
->setModel(new Message())
->setModel(new Topic())
->setModel(new Subscriber())
->setModel(new Target())
->setModel(new Migration())
->setModel(new MigrationReport())
->setModel(new MigrationFirebaseProject())

View file

@ -60,8 +60,8 @@ class V13 extends Filter
protected function parseProject($content)
{
$content['providers'] = $content['authProviders'];
unset($content['authProviders']);
$content['providers'] = $content['oAuthProviders'];
unset($content['oAuthProviders']);
return $content;
}

View file

@ -88,9 +88,9 @@ class V16 extends Filter
protected function parseProject(array $content)
{
foreach ($content['providers'] ?? [] as $i => $provider) {
$content['providers'][$i]['name'] = \ucfirst($provider['key']);
unset($content['providers'][$i]['key']);
foreach ($content['oAuthProviders'] ?? [] as $i => $provider) {
$content['oAuthProviders'][$i]['name'] = \ucfirst($provider['key']);
unset($content['oAuthProviders'][$i]['key']);
}
$content['domains'] = [];

View file

@ -0,0 +1,69 @@
<?php
namespace Appwrite\Utopia\Response\Model;
use Appwrite\Utopia\Response;
use Appwrite\Utopia\Response\Model;
class AuthProvider extends Model
{
/**
* @var bool
*/
protected bool $public = false;
public function __construct()
{
$this
->addRule('key', [
'type' => self::TYPE_STRING,
'description' => 'Auth Provider.',
'default' => '',
'example' => 'github',
])
->addRule('name', [
'type' => self::TYPE_STRING,
'description' => 'Auth Provider name.',
'default' => '',
'example' => 'GitHub',
])
->addRule('appId', [
'type' => self::TYPE_STRING,
'description' => 'OAuth 2.0 application ID.',
'default' => '',
'example' => '259125845563242502',
])
->addRule('secret', [
'type' => self::TYPE_STRING,
'description' => 'OAuth 2.0 application secret. Might be JSON string if provider requires extra configuration.',
'default' => '',
'example' => 'Bpw_g9c2TGXxfgLshDbSaL8tsCcqgczQ',
])
->addRule('enabled', [
'type' => self::TYPE_BOOLEAN,
'description' => 'Auth Provider is active and can be used to create session.',
'example' => '',
])
;
}
/**
* Get Name
*
* @return string
*/
public function getName(): string
{
return 'AuthProvider';
}
/**
* Get Type
*
* @return string
*/
public function getType(): string
{
return Response::MODEL_AUTH_PROVIDER;
}
}

View file

@ -0,0 +1,131 @@
<?php
namespace Appwrite\Utopia\Response\Model;
use Appwrite\Utopia\Response;
use Appwrite\Utopia\Response\Model;
use Utopia\Database\DateTime;
use Utopia\Database\Document as DatabaseDocument;
class Message extends Model
{
public function __construct()
{
$this
->addRule('$id', [
'type' => self::TYPE_STRING,
'description' => 'Message ID.',
'default' => '',
'example' => '5e5ea5c16897e',
])
->addRule('$createdAt', [
'type' => self::TYPE_DATETIME,
'description' => 'Message creation time in ISO 8601 format.',
'default' => '',
'example' => self::TYPE_DATETIME_EXAMPLE,
])
->addRule('$updatedAt', [
'type' => self::TYPE_DATETIME,
'description' => 'Message update date in ISO 8601 format.',
'default' => '',
'example' => self::TYPE_DATETIME_EXAMPLE,
])
->addRule('providerType', [
'type' => self::TYPE_STRING,
'description' => 'Message provider type.',
'default' => '',
'example' => MESSAGE_TYPE_EMAIL,
])
->addRule('topics', [
'type' => self::TYPE_STRING,
'description' => 'Topic IDs set as recipients.',
'default' => '',
'array' => true,
'example' => ['5e5ea5c16897e'],
])
->addRule('users', [
'type' => self::TYPE_STRING,
'description' => 'User IDs set as recipients.',
'default' => '',
'array' => true,
'example' => ['5e5ea5c16897e'],
])
->addRule('targets', [
'type' => self::TYPE_STRING,
'description' => 'Target IDs set as recipients.',
'default' => '',
'array' => true,
'example' => ['5e5ea5c16897e'],
])
->addRule('scheduledAt', [
'type' => self::TYPE_DATETIME,
'description' => 'The scheduled time for message.',
'required' => false,
'default' => DateTime::now(),
'example' => self::TYPE_DATETIME_EXAMPLE,
])
->addRule('deliveredAt', [
'type' => self::TYPE_DATETIME,
'description' => 'The time when the message was delivered.',
'required' => false,
'default' => '',
'example' => self::TYPE_DATETIME_EXAMPLE,
])
->addRule('deliveryErrors', [
'type' => self::TYPE_STRING,
'description' => 'Delivery errors if any.',
'required' => false,
'default' => '',
'array' => true,
'example' => ['Failed to send message to target 5e5ea5c16897e: Credentials not valid.'],
])
->addRule('deliveredTotal', [
'type' => self::TYPE_INTEGER,
'description' => 'Number of recipients the message was delivered to.',
'default' => 0,
'example' => 1,
])
->addRule('data', [
'type' => self::TYPE_JSON,
'description' => 'Data of the message.',
'default' => [],
'example' => [
'subject' => 'Welcome to Appwrite',
'content' => 'Hi there, welcome to Appwrite family.',
],
])
->addRule('status', [
'type' => self::TYPE_STRING,
'description' => 'Status of delivery.',
'default' => 'processing',
'example' => 'Message status can be one of the following: processing, sent, cancelled, failed.',
])
->addRule('description', [
'type' => self::TYPE_STRING,
'description' => 'Message description.',
'required' => false,
'default' => '',
'example' => 'Welcome Email.',
]);
}
/**
* Get Name
*
* @return string
*/
public function getName(): string
{
return 'Message';
}
/**
* Get Type
*
* @return string
*/
public function getType(): string
{
return Response::MODEL_MESSAGE;
}
}

View file

@ -138,9 +138,9 @@ class Project extends Model
'default' => false,
'example' => true,
])
->addRule('providers', [
'type' => Response::MODEL_PROVIDER,
'description' => 'List of Providers.',
->addRule('oAuthProviders', [
'type' => Response::MODEL_AUTH_PROVIDER,
'description' => 'List of Auth Providers.',
'default' => [],
'example' => [new \stdClass()],
'array' => true,
@ -328,9 +328,9 @@ class Project extends Model
$document->setAttribute('auth' . ucfirst($key), $value);
}
// Providers
$providers = Config::getParam('providers', []);
$providerValues = $document->getAttribute('authProviders', []);
// OAuth Providers
$providers = Config::getParam('oAuthProviders', []);
$providerValues = $document->getAttribute('oAuthProviders', []);
$projectProviders = [];
foreach ($providers as $key => $provider) {
@ -348,7 +348,7 @@ class Project extends Model
]);
}
$document->setAttribute("providers", $projectProviders);
$document->setAttribute('oAuthProviders', $projectProviders);
return $document;
}

View file

@ -7,44 +7,68 @@ use Appwrite\Utopia\Response\Model;
class Provider extends Model
{
/**
* @var bool
*/
protected bool $public = false;
public function __construct()
{
$this
->addRule('key', [
->addRule('$id', [
'type' => self::TYPE_STRING,
'description' => 'Provider.',
'description' => 'Provider ID.',
'default' => '',
'example' => 'github',
'example' => '5e5ea5c16897e',
])
->addRule('$createdAt', [
'type' => self::TYPE_DATETIME,
'description' => 'Provider creation time in ISO 8601 format.',
'default' => '',
'example' => self::TYPE_DATETIME_EXAMPLE,
])
->addRule('$updatedAt', [
'type' => self::TYPE_DATETIME,
'description' => 'Provider update date in ISO 8601 format.',
'default' => '',
'example' => self::TYPE_DATETIME_EXAMPLE,
])
->addRule('name', [
'type' => self::TYPE_STRING,
'description' => 'Provider name.',
'description' => 'The name for the provider instance.',
'default' => '',
'example' => 'GitHub',
'example' => 'Mailgun',
])
->addRule('appId', [
->addRule('provider', [
'type' => self::TYPE_STRING,
'description' => 'OAuth 2.0 application ID.',
'description' => 'The name of the provider service.',
'default' => '',
'example' => '259125845563242502',
])
->addRule('secret', [
'type' => self::TYPE_STRING,
'description' => 'OAuth 2.0 application secret. Might be JSON string if provider requires extra configuration.',
'default' => '',
'example' => 'Bpw_g9c2TGXxfgLshDbSaL8tsCcqgczQ',
'example' => 'mailgun',
])
->addRule('enabled', [
'type' => self::TYPE_BOOLEAN,
'description' => 'Provider is active and can be used to create session.',
'example' => '',
'description' => 'Is provider enabled?',
'default' => true,
'example' => true,
])
;
->addRule('type', [
'type' => self::TYPE_STRING,
'description' => 'Type of provider.',
'default' => '',
'example' => MESSAGE_TYPE_SMS,
])
->addRule('credentials', [
'type' => self::TYPE_JSON,
'description' => 'Provider credentials.',
'default' => [],
'example' => [
'key' => '123456789'
],
])
->addRule('options', [
'type' => self::TYPE_JSON,
'description' => 'Provider options.',
'default' => [],
'required' => false,
'example' => [
'from' => 'sender-email@mydomain'
],
]);
}
/**

View file

@ -0,0 +1,97 @@
<?php
namespace Appwrite\Utopia\Response\Model;
use Appwrite\Utopia\Response;
use Appwrite\Utopia\Response\Model;
class Subscriber extends Model
{
public function __construct()
{
$this
->addRule('$id', [
'type' => self::TYPE_STRING,
'description' => 'Subscriber ID.',
'default' => '',
'example' => '259125845563242502',
])
->addRule('$createdAt', [
'type' => self::TYPE_DATETIME,
'description' => 'Subscriber creation time in ISO 8601 format.',
'default' => '',
'example' => self::TYPE_DATETIME_EXAMPLE,
])
->addRule('$updatedAt', [
'type' => self::TYPE_DATETIME,
'description' => 'Subscriber update date in ISO 8601 format.',
'default' => '',
'example' => self::TYPE_DATETIME_EXAMPLE,
])
->addRule('targetId', [
'type' => self::TYPE_STRING,
'description' => 'Target ID.',
'default' => '',
'example' => '259125845563242502',
])
->addRule('target', [
'type' => Response::MODEL_TARGET,
'description' => 'Target.',
'default' => [],
'example' => [
'$id' => '259125845563242502',
'$createdAt' => self::TYPE_DATETIME_EXAMPLE,
'$updatedAt' => self::TYPE_DATETIME_EXAMPLE,
'providerType' => 'email',
'providerId' => '259125845563242502',
'name' => 'ageon-app-email',
'identifier' => 'random-mail@email.org',
'userId' => '5e5ea5c16897e',
],
])
->addRule('userId', [
'type' => self::TYPE_STRING,
'description' => 'Topic ID.',
'default' => '',
'example' => '5e5ea5c16897e',
])
->addRule('userName', [
'type' => self::TYPE_STRING,
'description' => 'User Name.',
'default' => '',
'example' => 'Aegon Targaryen',
])
->addRule('topicId', [
'type' => self::TYPE_STRING,
'description' => 'Topic ID.',
'default' => '',
'example' => '259125845563242502',
])
->addRule('providerType', [
'type' => self::TYPE_STRING,
'description' => 'The target provider type. Can be one of the following: `email`, `sms` or `push`.',
'default' => '',
'example' => MESSAGE_TYPE_EMAIL,
]);
}
/**
* Get Name
*
* @return string
*/
public function getName(): string
{
return 'Subscriber';
}
/**
* Get Type
*
* @return string
*/
public function getType(): string
{
return Response::MODEL_SUBSCRIBER;
}
}

View file

@ -0,0 +1,83 @@
<?php
namespace Appwrite\Utopia\Response\Model;
use Appwrite\Utopia\Response;
use Appwrite\Utopia\Response\Model;
class Target extends Model
{
public function __construct()
{
$this
->addRule('$id', [
'type' => self::TYPE_STRING,
'description' => 'Target ID.',
'default' => '',
'example' => '259125845563242502',
])
->addRule('$createdAt', [
'type' => self::TYPE_DATETIME,
'description' => 'Target creation time in ISO 8601 format.',
'default' => '',
'example' => self::TYPE_DATETIME_EXAMPLE,
])
->addRule('$updatedAt', [
'type' => self::TYPE_DATETIME,
'description' => 'Target update date in ISO 8601 format.',
'default' => '',
'example' => self::TYPE_DATETIME_EXAMPLE,
])
->addRule('name', [
'type' => self::TYPE_STRING,
'description' => 'Target Name.',
'default' => '',
'example' => 'Aegon apple token',
])
->addRule('userId', [
'type' => self::TYPE_STRING,
'description' => 'User ID.',
'default' => '',
'example' => '259125845563242502',
])
->addRule('providerId', [
'type' => self::TYPE_STRING,
'description' => 'Provider ID.',
'required' => false,
'default' => '',
'example' => '259125845563242502',
])
->addRule('providerType', [
'type' => self::TYPE_STRING,
'description' => 'The target provider type. Can be one of the following: `email`, `sms` or `push`.',
'default' => '',
'example' => MESSAGE_TYPE_EMAIL,
])
->addRule('identifier', [
'type' => self::TYPE_STRING,
'description' => 'The target identifier.',
'default' => '',
'example' => 'token',
]);
}
/**
* Get Name
*
* @return string
*/
public function getName(): string
{
return 'Target';
}
/**
* Get Type
*
* @return string
*/
public function getType(): string
{
return Response::MODEL_TARGET;
}
}

View file

@ -0,0 +1,71 @@
<?php
namespace Appwrite\Utopia\Response\Model;
use Appwrite\Utopia\Response;
use Appwrite\Utopia\Response\Model;
class Topic extends Model
{
public function __construct()
{
$this
->addRule('$id', [
'type' => self::TYPE_STRING,
'description' => 'Topic ID.',
'default' => '',
'example' => '259125845563242502',
])
->addRule('$createdAt', [
'type' => self::TYPE_DATETIME,
'description' => 'Topic creation time in ISO 8601 format.',
'default' => '',
'example' => self::TYPE_DATETIME_EXAMPLE,
])
->addRule('$updatedAt', [
'type' => self::TYPE_DATETIME,
'description' => 'Topic update date in ISO 8601 format.',
'default' => '',
'example' => self::TYPE_DATETIME_EXAMPLE,
])
->addRule('name', [
'type' => self::TYPE_STRING,
'description' => 'The name of the topic.',
'default' => '',
'example' => 'events',
])
->addRule('total', [
'type' => self::TYPE_INTEGER,
'description' => 'Total count of subscribers subscribed to topic.',
'default' => 0,
'example' => 100,
])
->addRule('description', [
'type' => self::TYPE_STRING,
'description' => 'Description of the topic.',
'default' => '',
'required' => false,
'example' => 'All events related messages will be sent to this topic.',
]);
}
/**
* Get Name
*
* @return string
*/
public function getName(): string
{
return 'Topic';
}
/**
* Get Type
*
* @return string
*/
public function getType(): string
{
return Response::MODEL_TOPIC;
}
}

View file

@ -120,6 +120,13 @@ class User extends Model
'default' => new \stdClass(),
'example' => ['theme' => 'pink', 'timezone' => 'UTC'],
])
->addRule('targets', [
'type' => Response::MODEL_TARGET,
'description' => 'A user-owned message receiver. A single user may have multiple e.g. emails, phones, and a browser. Each target is registered with a single provider.',
'default' => [],
'array' => true,
'example' => [],
])
->addRule('accessedAt', [
'type' => self::TYPE_DATETIME,
'description' => 'Most recent access date in ISO 8601 format. This attribute is only updated again after ' . APP_USER_ACCCESS / 60 / 60 . ' hours.',

View file

@ -82,7 +82,17 @@ trait ProjectCustom
'avatars.read',
'health.read',
'rules.read',
'rules.write'
'rules.write',
'targets.read',
'targets.write',
'providers.read',
'providers.write',
'messages.read',
'messages.write',
'topics.write',
'topics.read',
'subscribers.write',
'subscribers.read',
],
]);

View file

@ -94,6 +94,36 @@ trait AccountBase
$this->assertEquals($response['headers']['status-code'], 400);
$shortPassword = 'short';
$response = $this->client->call(Client::METHOD_POST, '/account', array_merge([
'origin' => 'http://localhost',
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
]), [
'userId' => ID::unique(),
'email' => 'shortpass@appwrite.io',
'password' => $shortPassword
]);
$this->assertEquals($response['headers']['status-code'], 400);
$longPassword = '';
for ($i = 0; $i < 257; $i++) { // 256 is the limit
$longPassword .= 'p';
}
$response = $this->client->call(Client::METHOD_POST, '/account', array_merge([
'origin' => 'http://localhost',
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
]), [
'userId' => ID::unique(),
'email' => 'longpass@appwrite.io',
'password' => $longPassword,
]);
$this->assertEquals($response['headers']['status-code'], 400);
return [
'id' => $id,
'email' => $email,

View file

@ -2,16 +2,16 @@
namespace Tests\E2E\Services\Account;
use Appwrite\Extend\Exception;
use Appwrite\SMS\Adapter\Mock;
use Appwrite\Tests\Retry;
use Tests\E2E\Client;
use Tests\E2E\Scopes\Scope;
use Tests\E2E\Scopes\ProjectCustom;
use Tests\E2E\Scopes\SideClient;
use Utopia\App;
use Utopia\Database\DateTime;
use Utopia\Database\Helpers\ID;
use Utopia\Database\Validator\Datetime as DatetimeValidator;
use Utopia\DSN\DSN;
use function sleep;
@ -764,6 +764,7 @@ class AccountCustomClientTest extends Scope
$this->assertEquals(true, (new DatetimeValidator())->isValid($response['body']['expire']));
$userId = $response['body']['userId'];
$messageId = $response['body']['$id'];
/**
* Test for FAILURE

View file

@ -6,7 +6,9 @@ use Tests\E2E\Client;
use Tests\E2E\Scopes\ProjectCustom;
use Tests\E2E\Scopes\Scope;
use Tests\E2E\Scopes\SideClient;
use Utopia\App;
use Utopia\Database\Helpers\ID;
use Utopia\DSN\DSN;
class AccountTest extends Scope
{
@ -123,6 +125,7 @@ class AccountTest extends Scope
public function testCreatePhoneVerification(): array
{
$projectId = $this->getProject()['$id'];
$query = $this->getQuery(self::$CREATE_PHONE_VERIFICATION);
$graphQLPayload = [
'query' => $query,

View file

@ -109,6 +109,11 @@ trait Base
public static string $DELETE_USER_SESSIONS = 'delete_user_sessions';
public static string $DELETE_USER_SESSION = 'delete_user_session';
public static string $DELETE_USER = 'delete_user';
public static string $CREATE_USER_TARGET = 'create_user_target';
public static string $LIST_USER_TARGETS = 'list_user_targets';
public static string $GET_USER_TARGET = 'get_user_target';
public static string $UPDATE_USER_TARGET = 'update_user_target';
public static string $DELETE_USER_TARGET = 'delete_user_target';
// Teams
public static string $GET_TEAM = 'get_team';
@ -199,6 +204,53 @@ trait Base
public static string $GET_QRCODE = 'get_qrcode';
public static string $GET_USER_INITIALS = 'get_user_initials';
// Providers
public static string $CREATE_MAILGUN_PROVIDER = 'create_mailgun_provider';
public static string $CREATE_SENDGRID_PROVIDER = 'create_sendgrid_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';
public static string $CREATE_MSG91_PROVIDER = 'create_msg91_provider';
public static string $CREATE_VONAGE_PROVIDER = 'create_vonage_provider';
public static string $CREATE_FCM_PROVIDER = 'create_fcm_provider';
public static string $CREATE_APNS_PROVIDER = 'create_apns_provider';
public static string $LIST_PROVIDERS = 'list_providers';
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_TWILIO_PROVIDER = 'update_twilio_provider';
public static string $UPDATE_TELESIGN_PROVIDER = 'update_telesign_provider';
public static string $UPDATE_TEXTMAGIC_PROVIDER = 'update_textmagic_provider';
public static string $UPDATE_MSG91_PROVIDER = 'update_msg91_provider';
public static string $UPDATE_VONAGE_PROVIDER = 'update_vonage_provider';
public static string $UPDATE_FCM_PROVIDER = 'update_fcm_provider';
public static string $UPDATE_APNS_PROVIDER = 'update_apns_provider';
public static string $DELETE_PROVIDER = 'delete_provider';
// Topics
public static string $CREATE_TOPIC = 'create_topic';
public static string $LIST_TOPICS = 'list_topics';
public static string $GET_TOPIC = 'get_topic';
public static string $UPDATE_TOPIC = 'update_topic';
public static string $DELETE_TOPIC = 'delete_topic';
// Subscriptions
public static string $CREATE_SUBSCRIBER = 'create_subscriber';
public static string $LIST_SUBSCRIBERS = 'list_subscribers';
public static string $GET_SUBSCRIBER = 'get_subscriber';
public static string $DELETE_SUBSCRIBER = 'delete_subscriber';
// Messages
public static string $CREATE_EMAIL = 'create_email';
public static string $CREATE_SMS = 'create_sms';
public static string $CREATE_PUSH_NOTIFICATION = 'create_push_notification';
public static string $LIST_MESSAGES = 'list_messages';
public static string $GET_MESSAGE = 'get_message';
public static string $UPDATE_EMAIL = 'update_email';
public static string $UPDATE_SMS = 'update_sms';
public static string $UPDATE_PUSH_NOTIFICATION = 'update_push_notification';
// Complex queries
public static string $COMPLEX_QUERY = 'complex_query';
@ -881,6 +933,55 @@ trait Base
status
}
}';
case self::$CREATE_USER_TARGET:
return 'mutation createUserTarget($userId: String!, $targetId: String!, $providerType: String!, $identifier: String! $providerId: String){
usersCreateTarget(userId: $userId, targetId: $targetId, providerType: $providerType, identifier: $identifier, providerId: $providerId) {
_id
userId
providerType
providerId
identifier
}
}';
case self::$LIST_USER_TARGETS:
return 'query listUserTargets($userId: String!) {
usersListTargets(userId: $userId) {
total
targets {
_id
userId
providerType
providerId
identifier
}
}
}';
case self::$GET_USER_TARGET:
return 'query getUserTarget($userId: String!, $targetId: String!) {
usersGetTarget(userId: $userId, targetId: $targetId) {
_id
userId
providerType
providerId
identifier
}
}';
case self::$UPDATE_USER_TARGET:
return 'mutation updateUserTarget($userId: String!, $targetId: String!, $providerId: String, $identifier: String){
usersUpdateTarget(userId: $userId, targetId: $targetId, providerId: $providerId, identifier: $identifier) {
_id
userId
providerType
providerId
identifier
}
}';
case self::$DELETE_USER_TARGET:
return 'mutation deleteUserTarget($userId: String!, $targetId: String!){
usersDeleteTarget(userId: $userId, targetId: $targetId) {
status
}
}';
case self::$GET_LOCALE:
return 'query getLocale {
localeGet {
@ -1688,6 +1789,439 @@ trait Base
status
}
}';
case self::$CREATE_MAILGUN_PROVIDER:
return 'mutation createMailgunProvider($providerId: String!, $name: String!, $domain: String!, $apiKey: String!, $from: String!, $isEuRegion: Boolean!) {
messagingCreateMailgunProvider(providerId: $providerId, name: $name, domain: $domain, apiKey: $apiKey, from: $from, isEuRegion: $isEuRegion) {
_id
name
provider
type
enabled
}
}';
case self::$CREATE_SENDGRID_PROVIDER:
return 'mutation createSendgridProvider($providerId: String!, $name: String!, $from: String!, $apiKey: String!) {
messagingCreateSendgridProvider(providerId: $providerId, name: $name, from: $from, apiKey: $apiKey) {
_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) {
_id
name
provider
type
enabled
}
}';
case self::$CREATE_TELESIGN_PROVIDER:
return 'mutation createTelesignProvider($providerId: String!, $name: String!, $from: String!, $username: String!, $password: String!) {
messagingCreateTelesignProvider(providerId: $providerId, name: $name, from: $from, username: $username, password: $password) {
_id
name
provider
type
enabled
}
}';
case self::$CREATE_TEXTMAGIC_PROVIDER:
return 'mutation createTextmagicProvider($providerId: String!, $name: String!, $from: String!, $username: String!, $apiKey: String!) {
messagingCreateTextmagicProvider(providerId: $providerId, name: $name, from: $from, username: $username, apiKey: $apiKey) {
_id
name
provider
type
enabled
}
}';
case self::$CREATE_MSG91_PROVIDER:
return 'mutation createMsg91Provider($providerId: String!, $name: String!, $from: String!, $senderId: String!, $authKey: String!, $enabled: Boolean) {
messagingCreateMsg91Provider(providerId: $providerId, name: $name, from: $from, senderId: $senderId, authKey: $authKey, enabled: $enabled) {
_id
name
provider
type
enabled
}
}';
case self::$CREATE_VONAGE_PROVIDER:
return 'mutation createVonageProvider($providerId: String!, $name: String!, $from: String!, $apiKey: String!, $apiSecret: String!) {
messagingCreateVonageProvider(providerId: $providerId, name: $name, from: $from, apiKey: $apiKey, apiSecret: $apiSecret) {
_id
name
provider
type
enabled
}
}';
case self::$CREATE_FCM_PROVIDER:
return 'mutation createFcmProvider($providerId: String!, $name: String!, $serverKey: String!) {
messagingCreateFcmProvider(providerId: $providerId, name: $name, serverKey: $serverKey) {
_id
name
provider
type
enabled
}
}';
case self::$CREATE_APNS_PROVIDER:
return 'mutation createApnsProvider($providerId: String!, $name: String!, $authKey: String!, $authKeyId: String!, $teamId: String!, $bundleId: String!, $endpoint: String!) {
messagingCreateApnsProvider(providerId: $providerId, name: $name, authKey: $authKey, authKeyId: $authKeyId, teamId: $teamId, bundleId: $bundleId, endpoint: $endpoint) {
_id
name
provider
type
enabled
}
}';
case self::$LIST_PROVIDERS:
return 'query listProviders {
messagingListProviders {
total
providers {
_id
name
provider
type
enabled
}
}
}';
case self::$GET_PROVIDER:
return 'query getProvider($providerId: String!) {
messagingGetProvider(providerId: $providerId) {
_id
name
provider
type
enabled
}
}';
case self::$UPDATE_MAILGUN_PROVIDER:
return 'mutation updateMailgunProvider($providerId: String!, $name: String!, $domain: String!, $apiKey: String!, $isEuRegion: Boolean, $enabled: Boolean) {
messagingUpdateMailgunProvider(providerId: $providerId, name: $name, domain: $domain, apiKey: $apiKey, isEuRegion: $isEuRegion, enabled: $enabled) {
_id
name
provider
type
enabled
}
}';
case self::$UPDATE_SENDGRID_PROVIDER:
return 'mutation messagingUpdateSendgridProvider($providerId: String!, $name: String!, $apiKey: String!) {
messagingUpdateSendgridProvider(providerId: $providerId, name: $name, apiKey: $apiKey) {
_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) {
_id
name
provider
type
enabled
}
}';
case self::$UPDATE_TELESIGN_PROVIDER:
return 'mutation updateTelesignProvider($providerId: String!, $name: String!, $username: String!, $password: String!) {
messagingUpdateTelesignProvider(providerId: $providerId, name: $name, username: $username, password: $password) {
_id
name
provider
type
enabled
}
}';
case self::$UPDATE_TEXTMAGIC_PROVIDER:
return 'mutation updateTextmagicProvider($providerId: String!, $name: String!, $username: String!, $apiKey: String!) {
messagingUpdateTextmagicProvider(providerId: $providerId, name: $name, username: $username, apiKey: $apiKey) {
_id
name
provider
type
enabled
}
}';
case self::$UPDATE_MSG91_PROVIDER:
return 'mutation updateMsg91Provider($providerId: String!, $name: String!, $senderId: String!, $authKey: String!) {
messagingUpdateMsg91Provider(providerId: $providerId, name: $name, senderId: $senderId, authKey: $authKey) {
_id
name
provider
type
enabled
}
}';
case self::$UPDATE_VONAGE_PROVIDER:
return 'mutation updateVonageProvider($providerId: String!, $name: String!, $apiKey: String!, $apiSecret: String!) {
messagingUpdateVonageProvider(providerId: $providerId, name: $name, apiKey: $apiKey, apiSecret: $apiSecret) {
_id
name
provider
type
enabled
}
}';
case self::$UPDATE_FCM_PROVIDER:
return 'mutation updateFcmProvider($providerId: String!, $name: String!, $serverKey: String!) {
messagingUpdateFcmProvider(providerId: $providerId, name: $name, serverKey: $serverKey) {
_id
name
provider
type
enabled
}
}';
case self::$UPDATE_APNS_PROVIDER:
return 'mutation updateApnsProvider($providerId: String!, $name: String!, $authKey: String!, $authKeyId: String!, $teamId: String!, $bundleId: String!, $endpoint: String!) {
messagingUpdateApnsProvider(providerId: $providerId, name: $name, authKey: $authKey, authKeyId: $authKeyId, teamId: $teamId, bundleId: $bundleId, endpoint: $endpoint) {
_id
name
provider
type
enabled
}
}';
case self::$DELETE_PROVIDER:
return 'mutation deleteProvider($providerId: String!) {
messagingDeleteProvider(providerId: $providerId) {
status
}
}';
case self::$CREATE_TOPIC:
return 'mutation createTopic($topicId: String!, $name: String!, $description: String!) {
messagingCreateTopic(topicId: $topicId, name: $name, description: $description) {
_id
name
description
}
}';
case self::$LIST_TOPICS:
return 'query listTopics {
messagingListTopics {
total
topics {
_id
name
description
}
}
}';
case self::$GET_TOPIC:
return 'query getTopic($topicId: String!) {
messagingGetTopic(topicId: $topicId) {
_id
name
description
}
}';
case self::$UPDATE_TOPIC:
return 'mutation updateTopic($topicId: String!, $name: String!, $description: String!) {
messagingUpdateTopic(topicId: $topicId, name: $name, description: $description) {
_id
name
description
}
}';
case self::$DELETE_TOPIC:
return 'mutation deleteTopic($topicId: String!) {
messagingDeleteTopic(topicId: $topicId) {
status
}
}';
case self::$CREATE_SUBSCRIBER:
return 'mutation createSubscriber($subscriberId: String!, $targetId: String!, $topicId: String!) {
messagingCreateSubscriber(subscriberId: $subscriberId, targetId: $targetId, topicId: $topicId) {
_id
targetId
topicId
userName
target {
_id
userId
name
providerType
identifier
}
}
}';
case self::$LIST_SUBSCRIBERS:
return 'query listSubscribers($topicId: String!) {
messagingListSubscribers(topicId: $topicId) {
total
subscribers {
_id
targetId
topicId
userName
target {
_id
userId
name
providerType
identifier
}
}
}
}';
case self::$GET_SUBSCRIBER:
return 'query getSubscriber($topicId: String!, $subscriberId: String!) {
messagingGetSubscriber(topicId: $topicId, subscriberId: $subscriberId) {
_id
targetId
topicId
userName
target {
_id
userId
name
providerType
identifier
}
}
}';
case self::$DELETE_SUBSCRIBER:
return 'mutation deleteSubscriber($topicId: String!, $subscriberId: String!) {
messagingDeleteSubscriber(topicId: $topicId, subscriberId: $subscriberId) {
status
}
}';
case self::$CREATE_EMAIL:
return 'mutation createEmail($messageId: String!, $topics: [String!], $users: [String!], $targets: [String!], $subject: String!, $content: String!, $status: String, $description: String, $html: Boolean, $scheduledAt: String) {
messagingCreateEmail(messageId: $messageId, topics: $topics, users: $users, targets: $targets, subject: $subject, content: $content, status: $status, description: $description, html: $html, scheduledAt: $scheduledAt) {
_id
topics
users
targets
scheduledAt
deliveredAt
deliveryErrors
deliveredTotal
status
description
}
}';
case self::$CREATE_SMS:
return 'mutation createSMS($messageId: String!, $topics: [String!], $users: [String!], $targets: [String!], $content: String!, $status: String, $description: String, $scheduledAt: String) {
messagingCreateSMS(messageId: $messageId, topics: $topics, users: $users, targets: $targets, content: $content, status: $status, description: $description, scheduledAt: $scheduledAt) {
_id
topics
users
targets
scheduledAt
deliveredAt
deliveryErrors
deliveredTotal
status
description
}
}';
case self::$CREATE_PUSH_NOTIFICATION:
return 'mutation createPushNotification($messageId: String!, $topics: [String!], $users: [String!], $targets: [String!], $title: String!, $body: String!, $data: Json, $action: String, $icon: String, $sound: String, $color: String, $tag: String, $badge: String, $status: String, $description: String, $scheduledAt: String) {
messagingCreatePushNotification(messageId: $messageId, topics: $topics, users: $users, targets: $targets, title: $title, body: $body, data: $data, action: $action, icon: $icon, sound: $sound, color: $color, tag: $tag, badge: $badge, status: $status, description: $description, scheduledAt: $scheduledAt) {
_id
topics
users
targets
scheduledAt
deliveredAt
deliveryErrors
deliveredTotal
status
description
}
}';
case self::$LIST_MESSAGES:
return 'query listMessages {
messagingListMessages {
total
messages {
_id
providerType
topics
users
targets
scheduledAt
deliveredAt
deliveryErrors
deliveredTotal
status
description
}
}
}';
case self::$GET_MESSAGE:
return 'query getMessage($messageId: String!) {
messagingGetMessage(messageId: $messageId) {
_id
providerType
topics
users
targets
scheduledAt
deliveredAt
deliveryErrors
deliveredTotal
status
description
}
}';
case self::$UPDATE_EMAIL:
return 'mutation updateEmail($messageId: String!, $topics: [String!], $users: [String!], $targets: [String!], $subject: String, $content: String, $status: String, $description: String, $html: Boolean, $scheduledAt: String) {
messagingUpdateEmail(messageId: $messageId, topics: $topics, users: $users, targets: $targets, subject: $subject, content: $content, status: $status, description: $description, html: $html, scheduledAt: $scheduledAt) {
_id
topics
users
targets
scheduledAt
deliveredAt
deliveryErrors
deliveredTotal
status
description
}
}';
case self::$UPDATE_SMS:
return 'mutation updateSMS($messageId: String!, $topics: [String!], $users: [String!], $targets: [String!], $content: String, $status: String, $description: String, $scheduledAt: String) {
messagingUpdateSMS(messageId: $messageId, topics: $topics, users: $users, targets: $targets, content: $content, status: $status, description: $description, scheduledAt: $scheduledAt) {
_id
topics
users
targets
scheduledAt
deliveredAt
deliveryErrors
deliveredTotal
status
description
}
}';
case self::$UPDATE_PUSH_NOTIFICATION:
return 'mutation updatePushNotification($messageId: String!, $topics: [String!], $users: [String!], $targets: [String!], $title: String, $body: String, $data: Json, $action: String, $icon: String, $sound: String, $color: String, $tag: String, $badge: String, $status: String, $description: String, $scheduledAt: String) {
messagingUpdatePushNotification(messageId: $messageId, topics: $topics, users: $users, targets: $targets, title: $title, body: $body, data: $data, action: $action, icon: $icon, sound: $sound, color: $color, tag: $tag, badge: $badge, status: $status, description: $description, scheduledAt: $scheduledAt) {
_id
topics
users
targets
scheduledAt
deliveredAt
deliveryErrors
deliveredTotal
status
description
}
}';
case self::$COMPLEX_QUERY:
return 'mutation complex($databaseId: String!, $databaseName: String!, $collectionId: String!, $collectionName: String!, $documentSecurity: Boolean!, $collectionPermissions: [String!]!) {
databasesCreate(databaseId: $databaseId, name: $databaseName) {
@ -1940,7 +2474,7 @@ trait Base
protected string $stdout = '';
protected string $stderr = '';
protected function packageCode($folder)
protected function packageCode($folder): void
{
Console::execute('cd ' . realpath(__DIR__ . "/../../../resources/functions") . "/$folder && tar --exclude code.tar.gz -czf code.tar.gz .", '', $this->stdout, $this->stderr);
}

File diff suppressed because it is too large Load diff

View file

@ -45,6 +45,56 @@ class UsersTest extends Scope
return $user;
}
/**
* @depends testCreateUser
*/
public function testCreateUserTarget(array $user)
{
$projectId = $this->getProject()['$id'];
$query = $this->getQuery(self::$CREATE_MAILGUN_PROVIDER);
$graphQLPayload = [
'query' => $query,
'variables' => [
'providerId' => ID::unique(),
'name' => 'Mailgun1',
'apiKey' => 'api-key',
'domain' => 'domain',
'from' => 'from@domain.com',
'isEuRegion' => false,
],
];
$provider = $this->client->call(Client::METHOD_POST, '/graphql', \array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $projectId,
], $this->getHeaders()), $graphQLPayload);
$providerId = $provider['body']['data']['messagingCreateMailgunProvider']['_id'];
$this->assertEquals(200, $provider['headers']['status-code']);
$query = $this->getQuery(self::$CREATE_USER_TARGET);
$graphQLPayload = [
'query' => $query,
'variables' => [
'targetId' => ID::unique(),
'userId' => $user['_id'],
'providerType' => 'email',
'providerId' => $providerId,
'identifier' => 'random-email@mail.org',
]
];
$target = $this->client->call(Client::METHOD_POST, '/graphql', \array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $projectId,
], $this->getHeaders()), $graphQLPayload);
$this->assertEquals(200, $target['headers']['status-code']);
$this->assertEquals('random-email@mail.org', $target['body']['data']['usersCreateTarget']['identifier']);
return $target['body']['data']['usersCreateTarget'];
}
public function testGetUsers()
{
$projectId = $this->getProject()['$id'];
@ -176,6 +226,54 @@ class UsersTest extends Scope
$this->assertIsArray($user['body']['data']['usersListLogs']);
}
/**
* @depends testCreateUserTarget
*/
public function testListUserTargets(array $target)
{
$projectId = $this->getProject()['$id'];
$query = $this->getQuery(self::$LIST_USER_TARGETS);
$graphQLPayload = [
'query' => $query,
'variables' => [
'userId' => $target['userId'],
]
];
$targets = $this->client->call(Client::METHOD_POST, '/graphql', \array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $projectId,
], $this->getHeaders()), $graphQLPayload);
$this->assertEquals(200, $targets['headers']['status-code']);
$this->assertIsArray($targets['body']['data']['usersListTargets']);
$this->assertCount(2, $targets['body']['data']['usersListTargets']['targets']);
}
/**
* @depends testCreateUserTarget
*/
public function testGetUserTarget(array $target)
{
$projectId = $this->getProject()['$id'];
$query = $this->getQuery(self::$GET_USER_TARGET);
$graphQLPayload = [
'query' => $query,
'variables' => [
'userId' => $target['userId'],
'targetId' => $target['_id'],
]
];
$target = $this->client->call(Client::METHOD_POST, '/graphql', \array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $projectId,
], $this->getHeaders()), $graphQLPayload);
$this->assertEquals(200, $target['headers']['status-code']);
$this->assertEquals('random-email@mail.org', $target['body']['data']['usersGetTarget']['identifier']);
}
public function testUpdateUserStatus()
{
$projectId = $this->getProject()['$id'];
@ -360,6 +458,31 @@ class UsersTest extends Scope
$this->assertEquals('{"key":"value"}', $user['body']['data']['usersUpdatePrefs']['data']);
}
/**
* @depends testCreateUserTarget
*/
public function testUpdateUserTarget(array $target)
{
$projectId = $this->getProject()['$id'];
$query = $this->getQuery(self::$UPDATE_USER_TARGET);
$graphQLPayload = [
'query' => $query,
'variables' => [
'userId' => $target['userId'],
'targetId' => $target['_id'],
'identifier' => 'random-email1@mail.org',
],
];
$target = $this->client->call(Client::METHOD_POST, '/graphql', \array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $projectId,
], $this->getHeaders()), $graphQLPayload);
$this->assertEquals(200, $target['headers']['status-code']);
$this->assertEquals('random-email1@mail.org', $target['body']['data']['usersUpdateTarget']['identifier']);
}
public function testDeleteUserSessions()
{
$projectId = $this->getProject()['$id'];
@ -407,6 +530,29 @@ class UsersTest extends Scope
$this->getUser();
}
/**
* @depends testCreateUserTarget
*/
public function testDeleteUserTarget(array $target)
{
$projectId = $this->getProject()['$id'];
$query = $this->getQuery(self::$DELETE_USER_TARGET);
$graphQLPayload = [
'query' => $query,
'variables' => [
'userId' => $target['userId'],
'targetId' => $target['_id'],
]
];
$target = $this->client->call(Client::METHOD_POST, '/graphql', \array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $projectId,
], $this->getHeaders()), $graphQLPayload);
$this->assertEquals(204, $target['headers']['status-code']);
}
public function testDeleteUser()
{
$projectId = $this->getProject()['$id'];

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,413 @@
<?php
namespace Tests\E2E\Services\Messaging;
use Tests\E2E\Client;
use Tests\E2E\Scopes\ProjectCustom;
use Tests\E2E\Scopes\Scope;
use Tests\E2E\Scopes\SideConsole;
use Utopia\Database\Helpers\ID;
class MessagingConsoleClientTest extends Scope
{
use MessagingBase;
use ProjectCustom;
use SideConsole;
/**
* @depends testListProviders
*/
public function testGetProviderLogs(array $providers): void
{
/**
* Test for SUCCESS
*/
$logs = $this->client->call(Client::METHOD_GET, '/messaging/providers/' . $providers[0]['$id'] . '/logs', \array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
], $this->getHeaders()));
$this->assertEquals($logs['headers']['status-code'], 200);
$this->assertIsArray($logs['body']['logs']);
$this->assertIsNumeric($logs['body']['total']);
$provider = $this->client->call(Client::METHOD_POST, '/messaging/providers/sendgrid/', \array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
], $this->getHeaders()), [
'providerId' => ID::unique(),
'name' => 'Sengrid1',
'apiKey' => 'my-apikey',
'from' => 'sender-email@my-domain.com',
]);
$this->assertEquals(201, $provider['headers']['status-code']);
$response = $this->client->call(Client::METHOD_PATCH, '/messaging/providers/sendgrid/' . $provider['body']['$id'], \array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
], $this->getHeaders()), [
'sendgrid' => [
'name' => 'Sendgrid2',
]]);
$this->assertEquals(200, $response['headers']['status-code']);
$logs = $this->client->call(Client::METHOD_GET, '/messaging/providers/' . $provider['body']['$id'] . '/logs', \array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
], $this->getHeaders()));
$this->assertEquals($logs['headers']['status-code'], 200);
$this->assertIsArray($logs['body']['logs']);
$this->assertIsNumeric($logs['body']['total']);
$this->assertCount(2, $logs['body']['logs']);
$logs = $this->client->call(Client::METHOD_GET, '/messaging/providers/' . $provider['body']['$id'] . '/logs', \array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
], $this->getHeaders()), [
'queries' => ['limit(1)'],
]);
$this->assertEquals($logs['headers']['status-code'], 200);
$this->assertIsArray($logs['body']['logs']);
$this->assertLessThanOrEqual(1, count($logs['body']['logs']));
$this->assertIsNumeric($logs['body']['total']);
$logs = $this->client->call(Client::METHOD_GET, '/messaging/providers/' . $provider['body']['$id'] . '/logs', \array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
], $this->getHeaders()), [
'queries' => ['offset(1)'],
]);
$this->assertEquals($logs['headers']['status-code'], 200);
$this->assertIsArray($logs['body']['logs']);
$this->assertIsNumeric($logs['body']['total']);
$logs = $this->client->call(Client::METHOD_GET, '/messaging/providers/' . $provider['body']['$id'] . '/logs', \array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
], $this->getHeaders()), [
'queries' => ['limit(1)', 'offset(1)'],
]);
$this->assertEquals($logs['headers']['status-code'], 200);
$this->assertIsArray($logs['body']['logs']);
$this->assertLessThanOrEqual(1, count($logs['body']['logs']));
$this->assertIsNumeric($logs['body']['total']);
/**
* Test for FAILURE
*/
$response = $this->client->call(Client::METHOD_GET, '/messaging/providers/' . $provider['body']['$id'] . '/logs', \array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
], $this->getHeaders()), [
'queries' => ['limit(-1)']
]);
$this->assertEquals($response['headers']['status-code'], 400);
$response = $this->client->call(Client::METHOD_GET, '/messaging/providers/' . $provider['body']['$id'] . '/logs', \array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
], $this->getHeaders()), [
'queries' => ['offset(-1)']
]);
$this->assertEquals($response['headers']['status-code'], 400);
$response = $this->client->call(Client::METHOD_GET, '/messaging/providers/' . $provider['body']['$id'] . '/logs', \array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
], $this->getHeaders()), [
'queries' => ['equal("$id", "asdf")']
]);
$this->assertEquals($response['headers']['status-code'], 400);
$response = $this->client->call(Client::METHOD_GET, '/messaging/providers/' . $provider['body']['$id'] . '/logs', \array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
], $this->getHeaders()), [
'queries' => ['orderAsc("$id")']
]);
$this->assertEquals($response['headers']['status-code'], 400);
$response = $this->client->call(Client::METHOD_GET, '/messaging/providers/' . $provider['body']['$id'] . '/logs', \array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
], $this->getHeaders()), [
'queries' => ['cursorAsc("$id")']
]);
$this->assertEquals($response['headers']['status-code'], 400);
}
/**
* @depends testListTopic
*/
public function testGetTopicLogs(string $topicId): void
{
/**
* Test for SUCCESS
*/
$logs = $this->client->call(Client::METHOD_GET, '/messaging/topics/' . $topicId . '/logs', \array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
], $this->getHeaders()));
$this->assertEquals($logs['headers']['status-code'], 200);
$this->assertIsArray($logs['body']['logs']);
$this->assertIsNumeric($logs['body']['total']);
$topic = $this->client->call(Client::METHOD_POST, '/messaging/topics', \array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
], $this->getHeaders()), [
'topicId' => ID::unique(),
'name' => 'my-app',
'description' => 'web app'
]);
$this->assertEquals(201, $topic['headers']['status-code']);
$response = $this->client->call(Client::METHOD_PATCH, '/messaging/topics/' . $topic['body']['$id'], \array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
], $this->getHeaders()), [
'description' => 'updated-description'
]);
$this->assertEquals(200, $response['headers']['status-code']);
$logs = $this->client->call(Client::METHOD_GET, '/messaging/topics/' . $topic['body']['$id'] . '/logs', \array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
], $this->getHeaders()));
$this->assertEquals($logs['headers']['status-code'], 200);
$this->assertIsArray($logs['body']['logs']);
$this->assertCount(2, $logs['body']['logs']);
$this->assertIsNumeric($logs['body']['total']);
$logs = $this->client->call(Client::METHOD_GET, '/messaging/topics/' . $topic['body']['$id'] . '/logs', \array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
], $this->getHeaders()), [
'queries' => ['limit(1)'],
]);
$this->assertEquals($logs['headers']['status-code'], 200);
$this->assertIsArray($logs['body']['logs']);
$this->assertLessThanOrEqual(1, count($logs['body']['logs']));
$this->assertIsNumeric($logs['body']['total']);
$logs = $this->client->call(Client::METHOD_GET, '/messaging/topics/' . $topic['body']['$id'] . '/logs', \array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
], $this->getHeaders()), [
'queries' => ['offset(1)'],
]);
$this->assertEquals($logs['headers']['status-code'], 200);
$this->assertIsArray($logs['body']['logs']);
$this->assertIsNumeric($logs['body']['total']);
$logs = $this->client->call(Client::METHOD_GET, '/messaging/topics/' . $topic['body']['$id'] . '/logs', \array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
], $this->getHeaders()), [
'queries' => ['limit(1)', 'offset(1)'],
]);
$this->assertEquals($logs['headers']['status-code'], 200);
$this->assertIsArray($logs['body']['logs']);
$this->assertLessThanOrEqual(1, count($logs['body']['logs']));
$this->assertIsNumeric($logs['body']['total']);
/**
* Test for FAILURE
*/
$response = $this->client->call(Client::METHOD_GET, '/messaging/topics/' . $topic['body']['$id'] . '/logs', \array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
], $this->getHeaders()), [
'queries' => ['limit(-1)']
]);
$this->assertEquals($response['headers']['status-code'], 400);
$response = $this->client->call(Client::METHOD_GET, '/messaging/topics/' . $topic['body']['$id'] . '/logs', \array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
], $this->getHeaders()), [
'queries' => ['offset(-1)']
]);
$this->assertEquals($response['headers']['status-code'], 400);
$response = $this->client->call(Client::METHOD_GET, '/messaging/topics/' . $topic['body']['$id'] . '/logs', \array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
], $this->getHeaders()), [
'queries' => ['equal("$id", "asdf")']
]);
$this->assertEquals($response['headers']['status-code'], 400);
$response = $this->client->call(Client::METHOD_GET, '/messaging/topics/' . $topic['body']['$id'] . '/logs', \array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
], $this->getHeaders()), [
'queries' => ['orderAsc("$id")']
]);
$this->assertEquals($response['headers']['status-code'], 400);
$response = $this->client->call(Client::METHOD_GET, '/messaging/topics/' . $topic['body']['$id'] . '/logs', \array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
], $this->getHeaders()), [
'queries' => ['cursorAsc("$id")']
]);
$this->assertEquals($response['headers']['status-code'], 400);
}
/**
* @depends testSendEmail
*/
public function testGetMessageLogs(array $email): void
{
/**
* Test for SUCCESS
*/
$logs = $this->client->call(Client::METHOD_GET, '/messaging/messages/' . $email['body']['$id'] . '/logs', [
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
'x-appwrite-key' => $this->getProject()['apiKey'],
]);
$this->assertEquals($logs['headers']['status-code'], 200);
$this->assertIsArray($logs['body']['logs']);
$this->assertIsNumeric($logs['body']['total']);
$email = $this->client->call(Client::METHOD_POST, '/messaging/messages/email', \array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
], $this->getHeaders()), [
'messageId' => ID::unique(),
'status' => 'draft',
'topics' => [ID::unique()],
'subject' => 'Khali beats Undertaker',
'content' => 'https://www.youtube.com/watch?v=dQw4w9WgXcQ',
]);
$this->assertEquals(201, $email['headers']['status-code']);
$response = $this->client->call(Client::METHOD_PATCH, '/messaging/messages/email/' . $email['body']['$id'], \array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
], $this->getHeaders()), [
'subject' => 'Khali beats John Cena!',
]);
$this->assertEquals(200, $response['headers']['status-code']);
$logs = $this->client->call(Client::METHOD_GET, '/messaging/messages/' . $email['body']['$id'] . '/logs', \array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
], $this->getHeaders()));
$this->assertEquals($logs['headers']['status-code'], 200);
$this->assertIsArray($logs['body']['logs']);
$this->assertIsNumeric($logs['body']['total']);
$this->assertCount(2, $logs['body']['logs']);
$logs = $this->client->call(Client::METHOD_GET, '/messaging/messages/' . $email['body']['$id'] . '/logs', \array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
], $this->getHeaders()), [
'queries' => ['limit(1)'],
]);
$this->assertEquals($logs['headers']['status-code'], 200);
$this->assertIsArray($logs['body']['logs']);
$this->assertLessThanOrEqual(1, count($logs['body']['logs']));
$this->assertIsNumeric($logs['body']['total']);
$logs = $this->client->call(Client::METHOD_GET, '/messaging/messages/' . $email['body']['$id'] . '/logs', \array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
], $this->getHeaders()), [
'queries' => ['offset(1)'],
]);
$this->assertEquals($logs['headers']['status-code'], 200);
$this->assertIsArray($logs['body']['logs']);
$this->assertIsNumeric($logs['body']['total']);
$logs = $this->client->call(Client::METHOD_GET, '/messaging/messages/' . $email['body']['$id'] . '/logs', \array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
], $this->getHeaders()), [
'queries' => ['limit(1)', 'offset(1)'],
]);
$this->assertEquals($logs['headers']['status-code'], 200);
$this->assertIsArray($logs['body']['logs']);
$this->assertLessThanOrEqual(1, count($logs['body']['logs']));
$this->assertIsNumeric($logs['body']['total']);
/**
* Test for FAILURE
*/
$response = $this->client->call(Client::METHOD_GET, '/messaging/messages/' . $email['body']['$id'] . '/logs', \array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
], $this->getHeaders()), [
'queries' => ['limit(-1)']
]);
$this->assertEquals($response['headers']['status-code'], 400);
$response = $this->client->call(Client::METHOD_GET, '/messaging/messages/' . $email['body']['$id'] . '/logs', \array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
], $this->getHeaders()), [
'queries' => ['offset(-1)']
]);
$this->assertEquals($response['headers']['status-code'], 400);
$response = $this->client->call(Client::METHOD_GET, '/messaging/messages/' . $email['body']['$id'] . '/logs', \array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
], $this->getHeaders()), [
'queries' => ['equal("$id", "asdf")']
]);
$this->assertEquals($response['headers']['status-code'], 400);
$response = $this->client->call(Client::METHOD_GET, '/messaging/messages/' . $email['body']['$id'] . '/logs', \array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
], $this->getHeaders()), [
'queries' => ['orderAsc("$id")']
]);
$this->assertEquals($response['headers']['status-code'], 400);
$response = $this->client->call(Client::METHOD_GET, '/messaging/messages/' . $email['body']['$id'] . '/logs', \array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
], $this->getHeaders()), [
'queries' => ['cursorAsc("$id")']
]);
$this->assertEquals($response['headers']['status-code'], 400);
}
}

View file

@ -0,0 +1,14 @@
<?php
namespace Tests\E2E\Services\Messaging;
use Tests\E2E\Scopes\ProjectCustom;
use Tests\E2E\Scopes\Scope;
use Tests\E2E\Scopes\SideClient;
class MessagingCustomClientTest extends Scope
{
use MessagingBase;
use ProjectCustom;
use SideClient;
}

View file

@ -0,0 +1,14 @@
<?php
namespace Tests\E2E\Services\Messaging;
use Tests\E2E\Scopes\ProjectCustom;
use Tests\E2E\Scopes\Scope;
use Tests\E2E\Scopes\SideServer;
class MessagingCustomServerTest extends Scope
{
use MessagingBase;
use ProjectCustom;
use SideServer;
}

View file

@ -794,7 +794,7 @@ class ProjectsConsoleClientTest extends Scope
public function testUpdateProjectOAuth($data): array
{
$id = $data['projectId'] ?? '';
$providers = require('app/config/providers.php');
$providers = require('app/config/oAuthProviders.php');
/**
* Test for SUCCESS
@ -825,7 +825,7 @@ class ProjectsConsoleClientTest extends Scope
foreach ($providers as $key => $provider) {
$asserted = false;
foreach ($response['body']['providers'] as $responseProvider) {
foreach ($response['body']['oAuthProviders'] as $responseProvider) {
if ($responseProvider['key'] === $key) {
$this->assertEquals('AppId-' . ucfirst($key), $responseProvider['appId']);
$this->assertEquals('Secret-' . ucfirst($key), $responseProvider['secret']);
@ -867,7 +867,7 @@ class ProjectsConsoleClientTest extends Scope
$i = 0;
foreach ($providers as $key => $provider) {
$asserted = false;
foreach ($response['body']['providers'] as $responseProvider) {
foreach ($response['body']['oAuthProviders'] as $responseProvider) {
if ($responseProvider['key'] === $key) {
// On first provider, test enabled=false
$this->assertEquals($i !== 0, $responseProvider['enabled']);

View file

@ -23,6 +23,7 @@ trait UsersBase
'password' => 'password',
'name' => 'Cristiano Ronaldo',
], false);
$this->assertEquals($user['headers']['status-code'], 201);
// Test empty prefs is object not array
$bodyString = $user['body'];
@ -1223,6 +1224,99 @@ trait UsersBase
$this->assertEquals($response['headers']['status-code'], 400);
}
/**
* @depends testGetUser
*/
public function testCreateUserTarget(array $data): array
{
$provider = $this->client->call(Client::METHOD_POST, '/messaging/providers/sendgrid', \array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
], $this->getHeaders()), [
'providerId' => ID::unique(),
'name' => 'Sengrid1',
'apiKey' => 'my-apikey',
'from' => 'from@domain.com',
]);
$this->assertEquals(201, $provider['headers']['status-code']);
$response = $this->client->call(Client::METHOD_POST, '/users/' . $data['userId'] . '/targets', array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
], $this->getHeaders()), [
'targetId' => ID::unique(),
'providerId' => $provider['body']['$id'],
'providerType' => 'email',
'identifier' => 'random-email@mail.org',
]);
$this->assertEquals(201, $response['headers']['status-code']);
$this->assertEquals($provider['body']['$id'], $response['body']['providerId']);
$this->assertEquals('random-email@mail.org', $response['body']['identifier']);
return $response['body'];
}
/**
* @depends testCreateUserTarget
*/
public function testUpdateUserTarget(array $data): array
{
$response = $this->client->call(Client::METHOD_PATCH, '/users/' . $data['userId'] . '/targets/' . $data['$id'], array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
], $this->getHeaders()), [
'identifier' => 'random-email1@mail.org',
]);
$this->assertEquals(200, $response['headers']['status-code']);
$this->assertEquals('random-email1@mail.org', $response['body']['identifier']);
return $response['body'];
}
/**
* @depends testUpdateUserTarget
*/
public function testListUserTarget(array $data)
{
$response = $this->client->call(Client::METHOD_GET, '/users/' . $data['userId'] . '/targets', array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
], $this->getHeaders()));
$this->assertEquals(200, $response['headers']['status-code']);
$this->assertEquals(2, \count($response['body']['targets']));
}
/**
* @depends testUpdateUserTarget
*/
public function testGetUserTarget(array $data)
{
$response = $this->client->call(Client::METHOD_GET, '/users/' . $data['userId'] . '/targets/' . $data['$id'], array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
], $this->getHeaders()));
$this->assertEquals(200, $response['headers']['status-code']);
$this->assertEquals($data['$id'], $response['body']['$id']);
}
/**
* @depends testUpdateUserTarget
*/
public function testDeleteUserTarget(array $data)
{
$response = $this->client->call(Client::METHOD_DELETE, '/users/' . $data['userId'] . '/targets/' . $data['$id'], array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
], $this->getHeaders()));
$this->assertEquals(204, $response['headers']['status-code']);
$response = $this->client->call(Client::METHOD_GET, '/users/' . $data['userId'] . '/targets', array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
], $this->getHeaders()));
$this->assertEquals(200, $response['headers']['status-code']);
$this->assertEquals(1, $response['body']['total']);
}
/**
* @depends testGetUser
*/

View file

@ -24,5 +24,16 @@ class PasswordDictionaryTest extends TestCase
$this->assertEquals($this->object->isValid('123456'), false);
$this->assertEquals($this->object->isValid('password'), false);
$this->assertEquals($this->object->isValid('myPasswordIsRight'), true);
$pass = ''; // 256 chars
for ($i = 0; $i < 256; $i++) {
$pass .= 'p';
}
$this->assertEquals($this->object->isValid($pass), true);
$pass .= 'p'; // 257 chars
$this->assertEquals($this->object->isValid($pass), false);
}
}

View file

@ -154,9 +154,9 @@ class V16Test extends TestCase
public function projectProvider(): array
{
return [
'providers' => [
'oAuthProviders' => [
[
'providers' => [
'oAuthProviders' => [
[
'key' => 'github',
'name' => 'GitHub',
@ -167,7 +167,7 @@ class V16Test extends TestCase
],
],
[
'providers' => [
'oAuthProviders' => [
[
'name' => 'Github',
'appId' => 'client_id',