mirror of
https://github.com/appwrite/appwrite.git
synced 2026-05-26 13:51:13 +00:00
Merge remote-tracking branch 'origin/1.5.x' into refactor-disallow-new-session-with-existing
This commit is contained in:
@@ -59,6 +59,7 @@ _APP_SMTP_USERNAME=
|
||||
_APP_SMTP_PASSWORD=
|
||||
_APP_SMS_PROVIDER=sms://username:password@mock
|
||||
_APP_SMS_FROM=+123456789
|
||||
_APP_SMS_PROJECTS_DENY_LIST=
|
||||
_APP_STORAGE_LIMIT=30000000
|
||||
_APP_STORAGE_PREVIEW_LIMIT=20000000
|
||||
_APP_FUNCTIONS_SIZE_LIMIT=30000000
|
||||
@@ -73,6 +74,7 @@ _APP_EXECUTOR_SECRET=your-secret-key
|
||||
_APP_EXECUTOR_HOST=http://proxy/v1
|
||||
_APP_FUNCTIONS_RUNTIMES=php-8.0,node-18.0,python-3.9,ruby-3.1
|
||||
_APP_MAINTENANCE_INTERVAL=86400
|
||||
_APP_MAINTENANCE_DELAY=
|
||||
_APP_MAINTENANCE_RETENTION_CACHE=2592000
|
||||
_APP_MAINTENANCE_RETENTION_EXECUTION=1209600
|
||||
_APP_MAINTENANCE_RETENTION_ABUSE=86400
|
||||
@@ -100,4 +102,5 @@ _APP_ASSISTANT_OPENAI_API_KEY=
|
||||
_APP_MESSAGE_SMS_TEST_DSN=
|
||||
_APP_MESSAGE_EMAIL_TEST_DSN=
|
||||
_APP_MESSAGE_PUSH_TEST_DSN=
|
||||
_APP_WEBHOOK_MAX_FAILED_ATTEMPTS=10
|
||||
_APP_WEBHOOK_MAX_FAILED_ATTEMPTS=10
|
||||
_APP_PROJECT_REGIONS=default
|
||||
+1
-1
@@ -1,4 +1,4 @@
|
||||
[submodule "app/console"]
|
||||
path = app/console
|
||||
url = https://github.com/appwrite/console
|
||||
branch = 1.5.x
|
||||
branch = chore-update-sdk
|
||||
|
||||
+6
-2
@@ -12,7 +12,7 @@ RUN composer install --ignore-platform-reqs --optimize-autoloader \
|
||||
--no-plugins --no-scripts --prefer-dist \
|
||||
`if [ "$TESTING" != "true" ]; then echo "--no-dev"; fi`
|
||||
|
||||
FROM --platform=$BUILDPLATFORM node:16.14.2-alpine3.15 as node
|
||||
FROM --platform=$BUILDPLATFORM node:20.11.0-alpine3.19 as node
|
||||
|
||||
COPY app/console /usr/local/src/console
|
||||
|
||||
@@ -29,7 +29,7 @@ ENV VITE_APPWRITE_GROWTH_ENDPOINT=$VITE_APPWRITE_GROWTH_ENDPOINT
|
||||
RUN npm ci
|
||||
RUN npm run build
|
||||
|
||||
FROM appwrite/base:0.7.2 as final
|
||||
FROM appwrite/base:0.8.0 as final
|
||||
|
||||
LABEL maintainer="team@appwrite.io"
|
||||
|
||||
@@ -89,6 +89,10 @@ RUN chmod +x /usr/local/bin/doctor && \
|
||||
chmod +x /usr/local/bin/test && \
|
||||
chmod +x /usr/local/bin/upgrade && \
|
||||
chmod +x /usr/local/bin/vars && \
|
||||
chmod +x /usr/local/bin/queue-retry && \
|
||||
chmod +x /usr/local/bin/queue-count-failed && \
|
||||
chmod +x /usr/local/bin/queue-count-processing && \
|
||||
chmod +x /usr/local/bin/queue-count-success && \
|
||||
chmod +x /usr/local/bin/worker-audits && \
|
||||
chmod +x /usr/local/bin/worker-builds && \
|
||||
chmod +x /usr/local/bin/worker-certificates && \
|
||||
|
||||
Binary file not shown.
Binary file not shown.
+1
-1
@@ -77,7 +77,7 @@ CLI::setResource('dbForConsole', function ($pools, $cache) {
|
||||
}
|
||||
|
||||
$ready = true;
|
||||
} catch (\Exception $err) {
|
||||
} catch (\Throwable $err) {
|
||||
Console::warning($err->getMessage());
|
||||
$pools->get('console')->reclaim();
|
||||
sleep($sleep);
|
||||
|
||||
@@ -1507,10 +1507,10 @@ $commonCollections = [
|
||||
]
|
||||
],
|
||||
|
||||
'stats_v2' => [
|
||||
'stats' => [
|
||||
'$collection' => ID::custom(Database::METADATA),
|
||||
'$id' => ID::custom('stats_v2'),
|
||||
'name' => 'stats_v2',
|
||||
'$id' => ID::custom('stats'),
|
||||
'name' => 'Stats',
|
||||
'attributes' => [
|
||||
[
|
||||
'$id' => ID::custom('metric'),
|
||||
@@ -1892,7 +1892,40 @@ $commonCollections = [
|
||||
'filters' => [],
|
||||
],
|
||||
[
|
||||
'$id' => ID::custom('total'),
|
||||
'$id' => ID::custom('subscribe'),
|
||||
'type' => Database::VAR_STRING,
|
||||
'format' => '',
|
||||
'size' => 128,
|
||||
'signed' => true,
|
||||
'required' => false,
|
||||
'default' => null,
|
||||
'array' => true,
|
||||
'filters' => [],
|
||||
],
|
||||
[
|
||||
'$id' => ID::custom('emailTotal'),
|
||||
'type' => Database::VAR_INTEGER,
|
||||
'format' => '',
|
||||
'size' => 0,
|
||||
'signed' => true,
|
||||
'required' => false,
|
||||
'default' => 0,
|
||||
'array' => false,
|
||||
'filters' => [],
|
||||
],
|
||||
[
|
||||
'$id' => ID::custom('smsTotal'),
|
||||
'type' => Database::VAR_INTEGER,
|
||||
'format' => '',
|
||||
'size' => 0,
|
||||
'signed' => true,
|
||||
'required' => false,
|
||||
'default' => 0,
|
||||
'array' => false,
|
||||
'filters' => [],
|
||||
],
|
||||
[
|
||||
'$id' => ID::custom('pushTotal'),
|
||||
'type' => Database::VAR_INTEGER,
|
||||
'format' => '',
|
||||
'size' => 0,
|
||||
|
||||
+39
-9
@@ -245,7 +245,7 @@ return [
|
||||
Exception::USER_MORE_FACTORS_REQUIRED => [
|
||||
'name' => Exception::USER_MORE_FACTORS_REQUIRED,
|
||||
'description' => 'More factors are required to complete the sign in process.',
|
||||
'code' => 400,
|
||||
'code' => 401,
|
||||
],
|
||||
Exception::USER_OAUTH2_BAD_REQUEST => [
|
||||
'name' => Exception::USER_OAUTH2_BAD_REQUEST,
|
||||
@@ -647,11 +647,6 @@ return [
|
||||
'description' => 'Project with the requested ID already exists. Try again with a different ID or use ID.unique() to generate a unique ID.',
|
||||
'code' => 409,
|
||||
],
|
||||
Exception::PROJECT_UNKNOWN => [
|
||||
'name' => Exception::PROJECT_UNKNOWN,
|
||||
'description' => 'The project ID is either missing or not valid. Please check the value of the X-Appwrite-Project header to ensure the correct project ID is being used.',
|
||||
'code' => 400,
|
||||
],
|
||||
Exception::PROJECT_PROVIDER_DISABLED => [
|
||||
'name' => Exception::PROJECT_PROVIDER_DISABLED,
|
||||
'description' => 'The chosen OAuth provider is disabled. You can enable the OAuth provider using the Appwrite console.',
|
||||
@@ -711,6 +706,7 @@ return [
|
||||
'name' => Exception::RULE_VERIFICATION_FAILED,
|
||||
'description' => 'Domain verification failed. Please check if your DNS records are correct and try again.',
|
||||
'code' => 401,
|
||||
'publish' => true
|
||||
],
|
||||
Exception::PROJECT_SMTP_CONFIG_INVALID => [
|
||||
'name' => Exception::PROJECT_SMTP_CONFIG_INVALID,
|
||||
@@ -722,6 +718,11 @@ return [
|
||||
'description' => 'You can\'t delete default template. If you are trying to reset your template changes, you can ignore this error as it\'s already been reset.',
|
||||
'code' => 401,
|
||||
],
|
||||
Exception::PROJECT_REGION_UNSUPPORTED => [
|
||||
'name' => Exception::PROJECT_REGION_UNSUPPORTED,
|
||||
'description' => 'The requested region is either inactive or unsupported. Please check the value of the _APP_REGIONS environment variable.',
|
||||
'code' => 400,
|
||||
],
|
||||
Exception::WEBHOOK_NOT_FOUND => [
|
||||
'name' => Exception::WEBHOOK_NOT_FOUND,
|
||||
'description' => 'Webhook with the requested ID could not be found.',
|
||||
@@ -798,13 +799,25 @@ return [
|
||||
],
|
||||
|
||||
/** Health */
|
||||
Exception::QUEUE_SIZE_EXCEEDED => [
|
||||
'name' => Exception::QUEUE_SIZE_EXCEEDED,
|
||||
Exception::HEALTH_QUEUE_SIZE_EXCEEDED => [
|
||||
'name' => Exception::HEALTH_QUEUE_SIZE_EXCEEDED,
|
||||
'description' => 'Queue size threshold hit.',
|
||||
'code' => 503,
|
||||
'publish' => false
|
||||
],
|
||||
|
||||
Exception::HEALTH_CERTIFICATE_EXPIRED => [
|
||||
'name' => Exception::HEALTH_CERTIFICATE_EXPIRED,
|
||||
'description' => 'The SSL certificate for the specified domain has expired and is no longer valid.',
|
||||
'code' => 404,
|
||||
],
|
||||
|
||||
Exception::HEALTH_INVALID_HOST => [
|
||||
'name' => Exception::HEALTH_INVALID_HOST,
|
||||
'description' => 'Failed to establish a connection to the specified domain. Please verify the domain name and ensure that the server is running and accessible.',
|
||||
'code' => 404,
|
||||
],
|
||||
|
||||
/** Providers */
|
||||
Exception::PROVIDER_NOT_FOUND => [
|
||||
'name' => Exception::PROVIDER_NOT_FOUND,
|
||||
@@ -859,7 +872,7 @@ return [
|
||||
],
|
||||
Exception::MESSAGE_MISSING_TARGET => [
|
||||
'name' => Exception::MESSAGE_MISSING_TARGET,
|
||||
'description' => 'Message with the requested ID is missing a target (Topics or Users or Targets).',
|
||||
'description' => 'Message with the requested ID has no recipients (topics or users or targets).',
|
||||
'code' => 400,
|
||||
],
|
||||
Exception::MESSAGE_ALREADY_SENT => [
|
||||
@@ -867,6 +880,16 @@ return [
|
||||
'description' => 'Message with the requested ID has already been sent.',
|
||||
'code' => 400,
|
||||
],
|
||||
Exception::MESSAGE_ALREADY_PROCESSING => [
|
||||
'name' => Exception::MESSAGE_ALREADY_PROCESSING,
|
||||
'description' => 'Message with the requested ID is already being processed.',
|
||||
'code' => 400,
|
||||
],
|
||||
Exception::MESSAGE_ALREADY_FAILED => [
|
||||
'name' => Exception::MESSAGE_ALREADY_FAILED,
|
||||
'description' => 'Message with the requested ID has already failed.',
|
||||
'code' => 400,
|
||||
],
|
||||
Exception::MESSAGE_ALREADY_SCHEDULED => [
|
||||
'name' => Exception::MESSAGE_ALREADY_SCHEDULED,
|
||||
'description' => 'Message with the requested ID has already been scheduled for delivery.',
|
||||
@@ -897,4 +920,11 @@ return [
|
||||
'description' => 'Schedule with the requested ID could not be found.',
|
||||
'code' => 404,
|
||||
],
|
||||
|
||||
/** Targets */
|
||||
Exception::TARGET_PROVIDER_INVALID_TYPE => [
|
||||
'name' => Exception::TARGET_PROVIDER_INVALID_TYPE,
|
||||
'description' => 'Target has an invalid provider type.',
|
||||
'code' => 400,
|
||||
],
|
||||
];
|
||||
|
||||
+68
-3
@@ -2,8 +2,73 @@
|
||||
|
||||
return [
|
||||
'default' => [
|
||||
'name' => 'Default',
|
||||
'default' => true,
|
||||
'$id' => 'default',
|
||||
'name' => 'Frankfurt',
|
||||
'disabled' => false,
|
||||
]
|
||||
'flag' => 'de',
|
||||
'default' => true,
|
||||
],
|
||||
'fra' => [
|
||||
'$id' => 'fra',
|
||||
'name' => 'Frankfurt',
|
||||
'disabled' => false,
|
||||
'flag' => 'de',
|
||||
'default' => true,
|
||||
],
|
||||
'nyc' => [
|
||||
'$id' => 'nyc',
|
||||
'name' => 'New York',
|
||||
'disabled' => true,
|
||||
'flag' => 'us',
|
||||
'default' => true,
|
||||
],
|
||||
'sfo' => [
|
||||
'$id' => 'sfo',
|
||||
'name' => 'San Francisco',
|
||||
'disabled' => true,
|
||||
'flag' => 'us',
|
||||
'default' => true,
|
||||
],
|
||||
'blr' => [
|
||||
'$id' => 'blr',
|
||||
'name' => 'Banglore',
|
||||
'disabled' => true,
|
||||
'flag' => 'in',
|
||||
'default' => true,
|
||||
],
|
||||
'lon' => [
|
||||
'$id' => 'lon',
|
||||
'name' => 'London',
|
||||
'disabled' => true,
|
||||
'flag' => 'gb',
|
||||
'default' => true,
|
||||
],
|
||||
'ams' => [
|
||||
'$id' => 'ams',
|
||||
'name' => 'Amsterdam',
|
||||
'disabled' => true,
|
||||
'flag' => 'nl',
|
||||
'default' => true,
|
||||
],
|
||||
'sgp' => [
|
||||
'$id' => 'sgp',
|
||||
'name' => 'Singapore',
|
||||
'disabled' => true,
|
||||
'flag' => 'sg',
|
||||
'default' => true,
|
||||
],
|
||||
'tor' => [
|
||||
'$id' => 'tor',
|
||||
'name' => 'Toronto',
|
||||
'disabled' => true,
|
||||
'flag' => 'ca',
|
||||
'default' => true,
|
||||
],
|
||||
'syd' => [
|
||||
'$id' => 'syd',
|
||||
'name' => 'Sydney',
|
||||
'disabled' => true,
|
||||
'flag' => 'au',
|
||||
'default' => true,
|
||||
],
|
||||
];
|
||||
|
||||
@@ -1,16 +1,9 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* List of Appwrite Cloud Functions supported runtimes
|
||||
* List of Appwrite Functions supported runtimes
|
||||
*/
|
||||
|
||||
use Utopia\App;
|
||||
use Appwrite\Runtimes\Runtimes;
|
||||
|
||||
$runtimes = new Runtimes('v3');
|
||||
|
||||
$allowList = empty(App::getEnv('_APP_FUNCTIONS_RUNTIMES')) ? [] : \explode(',', App::getEnv('_APP_FUNCTIONS_RUNTIMES'));
|
||||
|
||||
$runtimes = $runtimes->getAll(true, $allowList);
|
||||
|
||||
return $runtimes;
|
||||
return (new Runtimes('v3'))->getAll();
|
||||
|
||||
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@@ -948,6 +948,15 @@ return [
|
||||
'question' => '',
|
||||
'filter' => ''
|
||||
],
|
||||
[
|
||||
'name' => '_APP_MAINTENANCE_DELAY',
|
||||
'description' => 'Delay value containing the number of seconds that the Appwrite maintenance process should wait before executing system cleanups and optimizations. The default value is 0 seconds.',
|
||||
'introduction' => '1.5.0',
|
||||
'default' => '0',
|
||||
'required' => false,
|
||||
'question' => '',
|
||||
'filter' => ''
|
||||
],
|
||||
[
|
||||
'name' => '_APP_MAINTENANCE_RETENTION_CACHE',
|
||||
'description' => 'The maximum duration (in seconds) upto which to retain cached files. The default value is 2592000 seconds (30 days).',
|
||||
|
||||
+1
-1
Submodule app/console updated: 0a007a3b1b...44edd461c6
@@ -14,6 +14,7 @@ use Appwrite\Event\Mail;
|
||||
use Appwrite\Auth\Phrase;
|
||||
use Appwrite\Extend\Exception;
|
||||
use Appwrite\Network\Validator\Email;
|
||||
use Utopia\Database\Exception\Query as QueryException;
|
||||
use Utopia\Validator\Host;
|
||||
use Utopia\Validator\URL;
|
||||
use Utopia\Validator\Boolean;
|
||||
@@ -162,6 +163,11 @@ App::post('/v1/account')
|
||||
$user = Authorization::skip(fn() => $dbForProject->createDocument('users', $user));
|
||||
try {
|
||||
$target = Authorization::skip(fn() => $dbForProject->createDocument('targets', new Document([
|
||||
'$permissions' => [
|
||||
Permission::read(Role::user($user->getId())),
|
||||
Permission::update(Role::user($user->getId())),
|
||||
Permission::delete(Role::user($user->getId())),
|
||||
],
|
||||
'userId' => $user->getId(),
|
||||
'userInternalId' => $user->getInternalId(),
|
||||
'providerType' => MESSAGE_TYPE_EMAIL,
|
||||
@@ -511,7 +517,7 @@ App::get('/v1/account/sessions/oauth2/:provider/redirect')
|
||||
if (!empty($state)) {
|
||||
try {
|
||||
$state = \array_merge($defaultState, $oauth2->parseState($state));
|
||||
} catch (\Exception $exception) {
|
||||
} catch (\Throwable $exception) {
|
||||
throw new Exception(Exception::GENERAL_SERVER_ERROR, 'Failed to parse login state params as passed from OAuth2 provider');
|
||||
}
|
||||
} else {
|
||||
@@ -712,7 +718,7 @@ App::get('/v1/account/sessions/oauth2/:provider/redirect')
|
||||
$userDoc = Authorization::skip(fn() => $dbForProject->createDocument('users', $user));
|
||||
$dbForProject->createDocument('targets', new Document([
|
||||
'$permissions' => [
|
||||
Permission::read(Role::any()),
|
||||
Permission::read(Role::user($user->getId())),
|
||||
Permission::update(Role::user($user->getId())),
|
||||
Permission::delete(Role::user($user->getId())),
|
||||
],
|
||||
@@ -913,11 +919,17 @@ App::get('/v1/account/identities')
|
||||
->inject('dbForProject')
|
||||
->action(function (array $queries, Response $response, Document $user, Database $dbForProject) {
|
||||
|
||||
$queries = Query::parseQueries($queries);
|
||||
try {
|
||||
$queries = Query::parseQueries($queries);
|
||||
} catch (QueryException $e) {
|
||||
throw new Exception(Exception::GENERAL_QUERY_INVALID, $e->getMessage());
|
||||
}
|
||||
|
||||
$queries[] = Query::equal('userInternalId', [$user->getInternalId()]);
|
||||
|
||||
// Get cursor document if there was a cursor query
|
||||
/**
|
||||
* Get cursor document if there was a cursor query, we use array_filter and reset for reference $cursor to $queries
|
||||
*/
|
||||
$cursor = \array_filter($queries, function ($query) {
|
||||
return \in_array($query->getMethod(), [Query::TYPE_CURSOR_AFTER, Query::TYPE_CURSOR_BEFORE]);
|
||||
});
|
||||
@@ -984,7 +996,7 @@ App::delete('/v1/account/identities/:identityId')
|
||||
App::post('/v1/account/tokens/magic-url')
|
||||
->alias('/v1/account/sessions/magic-url')
|
||||
->desc('Create magic URL token')
|
||||
->groups(['api', 'account'])
|
||||
->groups(['api', 'account', 'auth'])
|
||||
->label('scope', 'sessions.write')
|
||||
->label('auth.type', 'magic-url')
|
||||
->label('audits.event', 'session.create')
|
||||
@@ -992,14 +1004,14 @@ App::post('/v1/account/tokens/magic-url')
|
||||
->label('audits.userId', '{response.userId}')
|
||||
->label('sdk.auth', [])
|
||||
->label('sdk.namespace', 'account')
|
||||
->label('sdk.method', ['createMagicURLToken', 'createMagicURLSession'])
|
||||
->label('sdk.method', 'createMagicURLToken')
|
||||
->label('sdk.description', '/docs/references/account/create-token-magic-url.md')
|
||||
->label('sdk.response.code', Response::STATUS_CODE_CREATED)
|
||||
->label('sdk.response.type', Response::CONTENT_TYPE_JSON)
|
||||
->label('sdk.response.model', Response::MODEL_TOKEN)
|
||||
->label('abuse-limit', 10)
|
||||
->label('abuse-key', 'url:{url},email:{param-email}')
|
||||
->param('userId', '', new CustomId(), 'User ID. Choose a custom ID or generate a random ID with `ID.unique()`. Valid chars are a-z, A-Z, 0-9, period, hyphen, and underscore. Can\'t start with a special char. Max length is 36 chars.')
|
||||
->label('abuse-limit', 60)
|
||||
->label('abuse-key', ['url:{url},email:{param-email}', 'url:{url},ip:{ip}'])
|
||||
->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('url', '', fn($clients) => new Host($clients), 'URL to redirect the user back to your app from the magic URL login. Only URLs from hostnames in your project platform list are allowed. This requirement helps to prevent an [open redirect](https://cheatsheetseries.owasp.org/cheatsheets/Unvalidated_Redirects_and_Forwards_Cheat_Sheet.html) attack against your project API.', true, ['clients'])
|
||||
->param('phrase', false, new Boolean(), 'Toggle for security phrase. If enabled, email will be send with a randomly generated phrase and the phrase will also be included in the response. Confirming phrases match increases the security of your authentication flow.', true)
|
||||
@@ -1634,13 +1646,13 @@ App::post('/v1/account/tokens/phone')
|
||||
->label('audits.userId', '{response.userId}')
|
||||
->label('sdk.auth', [])
|
||||
->label('sdk.namespace', 'account')
|
||||
->label('sdk.method', ['createPhoneToken', 'createPhoneSession'])
|
||||
->label('sdk.method', 'createPhoneToken')
|
||||
->label('sdk.description', '/docs/references/account/create-token-phone.md')
|
||||
->label('sdk.response.code', Response::STATUS_CODE_CREATED)
|
||||
->label('sdk.response.type', Response::CONTENT_TYPE_JSON)
|
||||
->label('sdk.response.model', Response::MODEL_TOKEN)
|
||||
->label('abuse-limit', 10)
|
||||
->label('abuse-key', 'url:{url},phone:{param-phone}')
|
||||
->label('abuse-key', ['url:{url},phone:{param-phone}', 'url:{url},ip:{ip}'])
|
||||
->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('phone', '', new Phone(), 'Phone number. Format this number with a leading \'+\' and a country code, e.g., +16175551212.')
|
||||
->inject('request')
|
||||
@@ -1703,6 +1715,11 @@ App::post('/v1/account/tokens/phone')
|
||||
Authorization::skip(fn () => $dbForProject->createDocument('users', $user));
|
||||
try {
|
||||
$target = Authorization::skip(fn() => $dbForProject->createDocument('targets', new Document([
|
||||
'$permissions' => [
|
||||
Permission::read(Role::user($user->getId())),
|
||||
Permission::update(Role::user($user->getId())),
|
||||
Permission::delete(Role::user($user->getId())),
|
||||
],
|
||||
'userId' => $user->getId(),
|
||||
'userInternalId' => $user->getInternalId(),
|
||||
'providerType' => MESSAGE_TYPE_SMS,
|
||||
@@ -1767,10 +1784,10 @@ App::post('/v1/account/tokens/phone')
|
||||
]);
|
||||
|
||||
$queueForMessaging
|
||||
->setType(MESSAGE_SEND_TYPE_INTERNAL)
|
||||
->setMessage($messageDoc)
|
||||
->setRecipients([$phone])
|
||||
->setProviderType(MESSAGE_TYPE_SMS)
|
||||
->trigger();
|
||||
->setProviderType(MESSAGE_TYPE_SMS);
|
||||
|
||||
$queueForEvents->setPayload(
|
||||
$response->output(
|
||||
@@ -2076,7 +2093,12 @@ App::get('/v1/account/logs')
|
||||
->inject('dbForProject')
|
||||
->action(function (array $queries, Response $response, Document $user, Locale $locale, Reader $geodb, Database $dbForProject) {
|
||||
|
||||
$queries = Query::parseQueries($queries);
|
||||
try {
|
||||
$queries = Query::parseQueries($queries);
|
||||
} catch (QueryException $e) {
|
||||
throw new Exception(Exception::GENERAL_QUERY_INVALID, $e->getMessage());
|
||||
}
|
||||
|
||||
$grouped = Query::groupByType($queries);
|
||||
$limit = $grouped['limit'] ?? APP_LIMIT_COUNT;
|
||||
$offset = $grouped['offset'] ?? 0;
|
||||
@@ -2746,7 +2768,7 @@ App::post('/v1/account/recovery')
|
||||
->label('sdk.response.type', Response::CONTENT_TYPE_JSON)
|
||||
->label('sdk.response.model', Response::MODEL_TOKEN)
|
||||
->label('abuse-limit', 10)
|
||||
->label('abuse-key', ['url:{url},email:{param-email}', 'ip:{ip}'])
|
||||
->label('abuse-key', ['url:{url},email:{param-email}', 'url:{url},ip:{ip}'])
|
||||
->param('email', '', new Email(), 'User email.')
|
||||
->param('url', '', fn ($clients) => new Host($clients), 'URL to redirect the user back to your app from the recovery email. Only URLs from hostnames in your project platform list are allowed. This requirement helps to prevent an [open redirect](https://cheatsheetseries.owasp.org/cheatsheets/Unvalidated_Redirects_and_Forwards_Cheat_Sheet.html) attack against your project API.', false, ['clients'])
|
||||
->inject('request')
|
||||
@@ -3214,8 +3236,9 @@ App::put('/v1/account/verification')
|
||||
|
||||
App::post('/v1/account/verification/phone')
|
||||
->desc('Create phone verification')
|
||||
->groups(['api', 'account'])
|
||||
->groups(['api', 'account', 'auth'])
|
||||
->label('scope', 'accounts.write')
|
||||
->label('auth.type', 'phone')
|
||||
->label('event', 'users.[userId].verification.[tokenId].create')
|
||||
->label('audits.event', 'verification.create')
|
||||
->label('audits.resource', 'user/{response.userId}')
|
||||
@@ -3227,7 +3250,7 @@ App::post('/v1/account/verification/phone')
|
||||
->label('sdk.response.type', Response::CONTENT_TYPE_JSON)
|
||||
->label('sdk.response.model', Response::MODEL_TOKEN)
|
||||
->label('abuse-limit', 10)
|
||||
->label('abuse-key', 'userId:{userId}')
|
||||
->label('abuse-key', ['url:{url},userId:{userId}', 'url:{url},ip:{ip}'])
|
||||
->inject('request')
|
||||
->inject('response')
|
||||
->inject('user')
|
||||
@@ -3301,10 +3324,10 @@ App::post('/v1/account/verification/phone')
|
||||
]);
|
||||
|
||||
$queueForMessaging
|
||||
->setType(MESSAGE_SEND_TYPE_INTERNAL)
|
||||
->setMessage($messageDoc)
|
||||
->setRecipients([$user->getAttribute('phone')])
|
||||
->setProviderType(MESSAGE_TYPE_SMS)
|
||||
->trigger();
|
||||
->setProviderType(MESSAGE_TYPE_SMS);
|
||||
|
||||
$queueForEvents
|
||||
->setParam('userId', $user->getId())
|
||||
@@ -3422,10 +3445,10 @@ App::get('/v1/account/mfa/factors')
|
||||
->label('sdk.auth', [APP_AUTH_TYPE_SESSION, APP_AUTH_TYPE_JWT])
|
||||
->label('sdk.namespace', 'account')
|
||||
->label('sdk.method', 'listFactors')
|
||||
->label('sdk.description', '/docs/references/account/get.md')
|
||||
->label('sdk.description', '/docs/references/account/list-factors.md')
|
||||
->label('sdk.response.code', Response::STATUS_CODE_OK)
|
||||
->label('sdk.response.type', Response::CONTENT_TYPE_JSON)
|
||||
->label('sdk.response.model', Response::MODEL_MFA_PROVIDERS)
|
||||
->label('sdk.response.model', Response::MODEL_MFA_FACTORS)
|
||||
->label('sdk.offline.model', '/account')
|
||||
->label('sdk.offline.key', 'current')
|
||||
->inject('response')
|
||||
@@ -3438,10 +3461,10 @@ App::get('/v1/account/mfa/factors')
|
||||
'phone' => $user->getAttribute('phone', false) && $user->getAttribute('phoneVerification', false)
|
||||
]);
|
||||
|
||||
$response->dynamic($providers, Response::MODEL_MFA_PROVIDERS);
|
||||
$response->dynamic($providers, Response::MODEL_MFA_FACTORS);
|
||||
});
|
||||
|
||||
App::post('/v1/account/mfa/:factor')
|
||||
App::post('/v1/account/mfa/:type')
|
||||
->desc('Add Authenticator')
|
||||
->groups(['api', 'account'])
|
||||
->label('event', 'users.[userId].update.mfa')
|
||||
@@ -3452,24 +3475,24 @@ App::post('/v1/account/mfa/:factor')
|
||||
->label('sdk.auth', [APP_AUTH_TYPE_SESSION, APP_AUTH_TYPE_JWT])
|
||||
->label('sdk.namespace', 'account')
|
||||
->label('sdk.method', 'addAuthenticator')
|
||||
->label('sdk.description', '/docs/references/account/update-mfa.md')
|
||||
->label('sdk.description', '/docs/references/account/add-authenticator.md')
|
||||
->label('sdk.response.code', Response::STATUS_CODE_OK)
|
||||
->label('sdk.response.type', Response::CONTENT_TYPE_JSON)
|
||||
->label('sdk.response.model', Response::MODEL_MFA_PROVIDER)
|
||||
->label('sdk.response.model', Response::MODEL_MFA_TYPE)
|
||||
->label('sdk.offline.model', '/account')
|
||||
->label('sdk.offline.key', 'current')
|
||||
->param('factor', null, new WhiteList(['totp']), 'Factor.')
|
||||
->param('type', null, new WhiteList(['totp']), 'Type of authenticator.')
|
||||
->inject('requestTimestamp')
|
||||
->inject('response')
|
||||
->inject('project')
|
||||
->inject('user')
|
||||
->inject('dbForProject')
|
||||
->inject('queueForEvents')
|
||||
->action(function (string $factor, ?\DateTime $requestTimestamp, Response $response, Document $project, Document $user, Database $dbForProject, Event $queueForEvents) {
|
||||
->action(function (string $type, ?\DateTime $requestTimestamp, Response $response, Document $project, Document $user, Database $dbForProject, Event $queueForEvents) {
|
||||
|
||||
$otp = match ($factor) {
|
||||
$otp = match ($type) {
|
||||
'totp' => new TOTP(),
|
||||
default => throw new Exception(Exception::GENERAL_UNKNOWN, 'Unknown provider.')
|
||||
default => throw new Exception(Exception::GENERAL_UNKNOWN, 'Unknown type.')
|
||||
};
|
||||
|
||||
$otp->setLabel($user->getAttribute('email'));
|
||||
@@ -3478,7 +3501,7 @@ App::post('/v1/account/mfa/:factor')
|
||||
$backups = Provider::generateBackupCodes();
|
||||
|
||||
if ($user->getAttribute('totp') && $user->getAttribute('totpVerification')) {
|
||||
throw new Exception(Exception::GENERAL_UNKNOWN, 'TOTP already exists.');
|
||||
throw new Exception(Exception::GENERAL_UNKNOWN, 'TOTP already exists on this account.');
|
||||
}
|
||||
|
||||
$user
|
||||
@@ -3497,10 +3520,10 @@ App::post('/v1/account/mfa/:factor')
|
||||
|
||||
$queueForEvents->setParam('userId', $user->getId());
|
||||
|
||||
$response->dynamic($model, Response::MODEL_MFA_PROVIDER);
|
||||
$response->dynamic($model, Response::MODEL_MFA_TYPE);
|
||||
});
|
||||
|
||||
App::put('/v1/account/mfa/:factor')
|
||||
App::put('/v1/account/mfa/:type')
|
||||
->desc('Verify Authenticator')
|
||||
->groups(['api', 'account'])
|
||||
->label('event', 'users.[userId].update.mfa')
|
||||
@@ -3511,13 +3534,13 @@ App::put('/v1/account/mfa/:factor')
|
||||
->label('sdk.auth', [APP_AUTH_TYPE_SESSION, APP_AUTH_TYPE_JWT])
|
||||
->label('sdk.namespace', 'account')
|
||||
->label('sdk.method', 'verifyAuthenticator')
|
||||
->label('sdk.description', '/docs/references/account/update-mfa.md')
|
||||
->label('sdk.description', '/docs/references/account/verify-authenticator.md')
|
||||
->label('sdk.response.code', Response::STATUS_CODE_OK)
|
||||
->label('sdk.response.type', Response::CONTENT_TYPE_JSON)
|
||||
->label('sdk.response.model', Response::MODEL_USER)
|
||||
->label('sdk.offline.model', '/account')
|
||||
->label('sdk.offline.key', 'current')
|
||||
->param('factor', null, new WhiteList(['totp']), 'Factor.')
|
||||
->param('type', null, new WhiteList(['totp']), 'Type of authenticator.')
|
||||
->param('otp', '', new Text(256), 'Valid verification token.')
|
||||
->inject('requestTimestamp')
|
||||
->inject('response')
|
||||
@@ -3525,9 +3548,9 @@ App::put('/v1/account/mfa/:factor')
|
||||
->inject('project')
|
||||
->inject('dbForProject')
|
||||
->inject('queueForEvents')
|
||||
->action(function (string $factor, string $otp, ?\DateTime $requestTimestamp, Response $response, Document $user, Document $project, Database $dbForProject, Event $queueForEvents) {
|
||||
->action(function (string $type, string $otp, ?\DateTime $requestTimestamp, Response $response, Document $user, Document $project, Database $dbForProject, Event $queueForEvents) {
|
||||
|
||||
$success = match ($factor) {
|
||||
$success = match ($type) {
|
||||
'totp' => Challenge\TOTP::verify($user, $otp),
|
||||
default => false
|
||||
};
|
||||
@@ -3537,9 +3560,9 @@ App::put('/v1/account/mfa/:factor')
|
||||
}
|
||||
|
||||
if (!$user->getAttribute('totp')) {
|
||||
throw new Exception(Exception::GENERAL_UNKNOWN, 'TOTP not added.');
|
||||
throw new Exception(Exception::GENERAL_UNKNOWN, 'Authenticator needs to be added first.');
|
||||
} elseif ($user->getAttribute('totpVerification')) {
|
||||
throw new Exception(Exception::GENERAL_UNKNOWN, 'TOTP already verified.');
|
||||
throw new Exception(Exception::GENERAL_UNKNOWN, 'Authenticator already verified on this account.');
|
||||
}
|
||||
|
||||
$user->setAttribute('totpVerification', true);
|
||||
@@ -3549,14 +3572,14 @@ App::put('/v1/account/mfa/:factor')
|
||||
$authDuration = $project->getAttribute('auths', [])['duration'] ?? Auth::TOKEN_EXPIRATION_LOGIN_LONG;
|
||||
$sessionId = Auth::sessionVerify($user->getAttribute('sessions', []), Auth::$secret, $authDuration);
|
||||
$session = $dbForProject->getDocument('sessions', $sessionId);
|
||||
$dbForProject->updateDocument('sessions', $sessionId, $session->setAttribute('factors', $provider, Document::SET_TYPE_APPEND));
|
||||
$dbForProject->updateDocument('sessions', $sessionId, $session->setAttribute('factors', $type, Document::SET_TYPE_APPEND));
|
||||
|
||||
$queueForEvents->setParam('userId', $user->getId());
|
||||
|
||||
$response->dynamic($user, Response::MODEL_ACCOUNT);
|
||||
});
|
||||
|
||||
App::delete('/v1/account/mfa/:provider')
|
||||
App::delete('/v1/account/mfa/:type')
|
||||
->desc('Delete Authenticator')
|
||||
->groups(['api', 'account'])
|
||||
->label('event', 'users.[userId].delete.mfa')
|
||||
@@ -3571,16 +3594,16 @@ App::delete('/v1/account/mfa/:provider')
|
||||
->label('sdk.response.code', Response::STATUS_CODE_OK)
|
||||
->label('sdk.response.type', Response::CONTENT_TYPE_JSON)
|
||||
->label('sdk.response.model', Response::MODEL_USER)
|
||||
->param('provider', null, new WhiteList(['totp']), 'Provider.')
|
||||
->param('type', null, new WhiteList(['totp']), 'Type of authenticator.')
|
||||
->param('otp', '', new Text(256), 'Valid verification token.')
|
||||
->inject('requestTimestamp')
|
||||
->inject('response')
|
||||
->inject('user')
|
||||
->inject('dbForProject')
|
||||
->inject('queueForEvents')
|
||||
->action(function (string $provider, string $otp, ?\DateTime $requestTimestamp, Response $response, Document $user, Database $dbForProject, Event $queueForEvents) {
|
||||
->action(function (string $type, string $otp, ?\DateTime $requestTimestamp, Response $response, Document $user, Database $dbForProject, Event $queueForEvents) {
|
||||
|
||||
$success = match ($provider) {
|
||||
$success = match ($type) {
|
||||
'totp' => Challenge\TOTP::verify($user, $otp),
|
||||
default => false
|
||||
};
|
||||
@@ -3607,40 +3630,38 @@ App::delete('/v1/account/mfa/:provider')
|
||||
});
|
||||
|
||||
App::post('/v1/account/mfa/challenge')
|
||||
->desc('Create MFA Challenge')
|
||||
->desc('Create 2FA Challenge')
|
||||
->groups(['api', 'account', 'mfa'])
|
||||
->label('scope', 'accounts.write')
|
||||
->label('event', 'users.[userId].challenges.[challengeId].create')
|
||||
->label('auth.type', 'createChallenge')
|
||||
->label('audits.event', 'challenge.create')
|
||||
->label('audits.resource', 'user/{response.userId}')
|
||||
->label('audits.userId', '{response.userId}')
|
||||
->label('sdk.auth', [])
|
||||
->label('sdk.namespace', 'account')
|
||||
->label('sdk.method', 'createChallenge')
|
||||
->label('sdk.description', '/docs/references/account/create-challenge.md')
|
||||
->label('sdk.method', 'create2FAChallenge')
|
||||
->label('sdk.description', '/docs/references/account/create-2fa-challenge.md')
|
||||
->label('sdk.response.code', Response::STATUS_CODE_CREATED)
|
||||
->label('sdk.response.type', Response::CONTENT_TYPE_JSON)
|
||||
->label('sdk.response.model', Response::MODEL_MFA_CHALLENGE)
|
||||
->label('abuse-limit', 10)
|
||||
->label('abuse-key', 'url:{url},token:{param-token}')
|
||||
->param('provider', '', new WhiteList(['totp', 'phone', 'email']), 'provider.')
|
||||
->param('factor', '', new WhiteList(['totp', 'phone', 'email']), 'Factor used for verification.')
|
||||
->inject('response')
|
||||
->inject('dbForProject')
|
||||
->inject('user')
|
||||
->inject('project')
|
||||
->inject('queueForEvents')
|
||||
->inject('queueForMessaging')
|
||||
->inject('queueForMails')
|
||||
->inject('locale')
|
||||
->action(function (string $provider, Response $response, Database $dbForProject, Document $user, Document $project, Event $queueForEvents, Messaging $queueForMessaging, Mail $queueForMails, Locale $locale) {
|
||||
->action(function (string $factor, Response $response, Database $dbForProject, Document $user, Event $queueForEvents, Messaging $queueForMessaging, Mail $queueForMails, Locale $locale) {
|
||||
|
||||
$expire = DateTime::addSeconds(new \DateTime(), Auth::TOKEN_EXPIRATION_CONFIRM);
|
||||
$code = Auth::codeGenerator();
|
||||
$challenge = new Document([
|
||||
'userId' => $user->getId(),
|
||||
'userInternalId' => $user->getInternalId(),
|
||||
'provider' => $provider,
|
||||
'provider' => $factor,
|
||||
'token' => Auth::tokenGenerator(),
|
||||
'code' => $code,
|
||||
'expire' => $expire,
|
||||
@@ -3653,7 +3674,7 @@ App::post('/v1/account/mfa/challenge')
|
||||
|
||||
$challenge = $dbForProject->createDocument('challenges', $challenge);
|
||||
|
||||
switch ($provider) {
|
||||
switch ($factor) {
|
||||
case 'phone':
|
||||
if (empty(App::getEnv('_APP_SMS_PROVIDER'))) {
|
||||
throw new Exception(Exception::GENERAL_PHONE_DISABLED, 'Phone provider not configured');
|
||||
@@ -3666,14 +3687,14 @@ App::post('/v1/account/mfa/challenge')
|
||||
}
|
||||
|
||||
$queueForMessaging
|
||||
->setType(MESSAGE_SEND_TYPE_INTERNAL)
|
||||
->setMessage(new Document([
|
||||
'$id' => $challenge->getId(),
|
||||
'data' => [
|
||||
'content' => $code,
|
||||
],
|
||||
]))
|
||||
->setRecipients([$user->getAttribute('phone')])
|
||||
->trigger();
|
||||
->setRecipients([$user->getAttribute('phone')]);
|
||||
break;
|
||||
case 'email':
|
||||
if (empty(App::getEnv('_APP_SMTP_HOST'))) {
|
||||
@@ -3717,7 +3738,7 @@ App::put('/v1/account/mfa/challenge')
|
||||
->label('sdk.response.model', Response::MODEL_SESSION)
|
||||
->label('abuse-limit', 10)
|
||||
->label('abuse-key', 'userId:{param-userId}')
|
||||
->param('challengeId', '', new Text(256), 'Valid verification token.')
|
||||
->param('challengeId', '', new Text(256), 'ID of the challenge.')
|
||||
->param('otp', '', new Text(256), 'Valid verification token.')
|
||||
->inject('project')
|
||||
->inject('response')
|
||||
|
||||
@@ -291,7 +291,7 @@ App::get('/v1/avatars/image')
|
||||
|
||||
try {
|
||||
$image = new Image($fetch);
|
||||
} catch (\Exception $exception) {
|
||||
} catch (\Throwable $exception) {
|
||||
throw new Exception(Exception::GENERAL_SERVER_ERROR, 'Unable to parse image');
|
||||
}
|
||||
|
||||
|
||||
@@ -19,7 +19,6 @@ use Utopia\App;
|
||||
use Utopia\Audit\Audit;
|
||||
use Utopia\Config\Config;
|
||||
use Utopia\Database\Database;
|
||||
use Utopia\Database\DateTime;
|
||||
use Utopia\Database\Document;
|
||||
use Utopia\Database\Exception\Authorization as AuthorizationException;
|
||||
use Utopia\Database\Exception\Conflict as ConflictException;
|
||||
@@ -151,7 +150,7 @@ function createAttribute(string $databaseId, string $collectionId, Document $att
|
||||
throw new Exception(Exception::ATTRIBUTE_ALREADY_EXISTS);
|
||||
} catch (LimitException) {
|
||||
throw new Exception(Exception::ATTRIBUTE_LIMIT_EXCEEDED, 'Attribute limit exceeded');
|
||||
} catch (\Exception $e) {
|
||||
} catch (\Throwable $e) {
|
||||
$dbForProject->purgeCachedDocument('database_' . $db->getInternalId(), $collectionId);
|
||||
$dbForProject->purgeCachedCollection('database_' . $db->getInternalId() . '_collection_' . $collection->getInternalId());
|
||||
throw $e;
|
||||
@@ -195,7 +194,7 @@ function createAttribute(string $databaseId, string $collectionId, Document $att
|
||||
} catch (LimitException) {
|
||||
$dbForProject->deleteDocument('attributes', $attribute->getId());
|
||||
throw new Exception(Exception::ATTRIBUTE_LIMIT_EXCEEDED, 'Attribute limit exceeded');
|
||||
} catch (\Exception $e) {
|
||||
} catch (\Throwable $e) {
|
||||
$dbForProject->purgeCachedDocument('database_' . $db->getInternalId(), $relatedCollection->getId());
|
||||
$dbForProject->purgeCachedCollection('database_' . $db->getInternalId() . '_collection_' . $relatedCollection->getInternalId());
|
||||
throw $e;
|
||||
@@ -487,13 +486,19 @@ App::get('/v1/databases')
|
||||
->inject('dbForProject')
|
||||
->action(function (array $queries, string $search, Response $response, Database $dbForProject) {
|
||||
|
||||
$queries = Query::parseQueries($queries);
|
||||
try {
|
||||
$queries = Query::parseQueries($queries);
|
||||
} catch (QueryException $e) {
|
||||
throw new Exception(Exception::GENERAL_QUERY_INVALID, $e->getMessage());
|
||||
}
|
||||
|
||||
if (!empty($search)) {
|
||||
$queries[] = Query::search('search', $search);
|
||||
}
|
||||
|
||||
// Get cursor document if there was a cursor query
|
||||
/**
|
||||
* Get cursor document if there was a cursor query, we use array_filter and reset for reference $cursor to $queries
|
||||
*/
|
||||
$cursor = \array_filter($queries, function ($query) {
|
||||
return \in_array($query->getMethod(), [Query::TYPE_CURSOR_AFTER, Query::TYPE_CURSOR_BEFORE]);
|
||||
});
|
||||
@@ -567,7 +572,12 @@ App::get('/v1/databases/:databaseId/logs')
|
||||
throw new Exception(Exception::DATABASE_NOT_FOUND);
|
||||
}
|
||||
|
||||
$queries = Query::parseQueries($queries);
|
||||
try {
|
||||
$queries = Query::parseQueries($queries);
|
||||
} catch (QueryException $e) {
|
||||
throw new Exception(Exception::GENERAL_QUERY_INVALID, $e->getMessage());
|
||||
}
|
||||
|
||||
$grouped = Query::groupByType($queries);
|
||||
$limit = $grouped['limit'] ?? APP_LIMIT_COUNT;
|
||||
$offset = $grouped['offset'] ?? 0;
|
||||
@@ -809,13 +819,19 @@ App::get('/v1/databases/:databaseId/collections')
|
||||
throw new Exception(Exception::DATABASE_NOT_FOUND);
|
||||
}
|
||||
|
||||
$queries = Query::parseQueries($queries);
|
||||
try {
|
||||
$queries = Query::parseQueries($queries);
|
||||
} catch (QueryException $e) {
|
||||
throw new Exception(Exception::GENERAL_QUERY_INVALID, $e->getMessage());
|
||||
}
|
||||
|
||||
if (!empty($search)) {
|
||||
$queries[] = Query::search('search', $search);
|
||||
}
|
||||
|
||||
// Get cursor document if there was a cursor query
|
||||
/**
|
||||
* Get cursor document if there was a cursor query, we use array_filter and reset for reference $cursor to $queries
|
||||
*/
|
||||
$cursor = \array_filter($queries, function ($query) {
|
||||
return \in_array($query->getMethod(), [Query::TYPE_CURSOR_AFTER, Query::TYPE_CURSOR_BEFORE]);
|
||||
});
|
||||
@@ -908,7 +924,12 @@ App::get('/v1/databases/:databaseId/collections/:collectionId/logs')
|
||||
throw new Exception(Exception::COLLECTION_NOT_FOUND);
|
||||
}
|
||||
|
||||
$queries = Query::parseQueries($queries);
|
||||
try {
|
||||
$queries = Query::parseQueries($queries);
|
||||
} catch (QueryException $e) {
|
||||
throw new Exception(Exception::GENERAL_QUERY_INVALID, $e->getMessage());
|
||||
}
|
||||
|
||||
$grouped = Query::groupByType($queries);
|
||||
$limit = $grouped['limit'] ?? APP_LIMIT_COUNT;
|
||||
$offset = $grouped['offset'] ?? 0;
|
||||
@@ -1649,7 +1670,7 @@ App::get('/v1/databases/:databaseId/collections/:collectionId/attributes')
|
||||
->inject('response')
|
||||
->inject('dbForProject')
|
||||
->action(function (string $databaseId, string $collectionId, array $queries, Response $response, Database $dbForProject) {
|
||||
|
||||
/** @var Document $database */
|
||||
$database = Authorization::skip(fn() => $dbForProject->getDocument('databases', $databaseId));
|
||||
|
||||
if ($database->isEmpty()) {
|
||||
@@ -1662,26 +1683,31 @@ App::get('/v1/databases/:databaseId/collections/:collectionId/attributes')
|
||||
throw new Exception(Exception::COLLECTION_NOT_FOUND);
|
||||
}
|
||||
|
||||
$queries = Query::parseQueries($queries);
|
||||
try {
|
||||
$queries = Query::parseQueries($queries);
|
||||
} catch (QueryException $e) {
|
||||
throw new Exception(Exception::GENERAL_QUERY_INVALID, $e->getMessage());
|
||||
}
|
||||
|
||||
\array_push(
|
||||
$queries,
|
||||
Query::equal('collectionId', [$collectionId]),
|
||||
Query::equal('databaseId', [$databaseId])
|
||||
Query::equal('collectionInternalId', [$collection->getInternalId()]),
|
||||
Query::equal('databaseInternalId', [$database->getInternalId()])
|
||||
);
|
||||
|
||||
// Get cursor document if there was a cursor query
|
||||
/**
|
||||
* Get cursor document if there was a cursor query, we use array_filter and reset for reference $cursor to $queries
|
||||
*/
|
||||
$cursor = \array_filter($queries, function ($query) {
|
||||
return \in_array($query->getMethod(), [Query::TYPE_CURSOR_AFTER, Query::TYPE_CURSOR_BEFORE]);
|
||||
});
|
||||
|
||||
$cursor = \reset($cursor);
|
||||
|
||||
if ($cursor) {
|
||||
$attributeId = $cursor->getValue();
|
||||
$cursorDocument = Authorization::skip(fn() => $dbForProject->find('attributes', [
|
||||
Query::equal('collectionId', [$collectionId]),
|
||||
Query::equal('databaseId', [$databaseId]),
|
||||
Query::equal('collectionInternalId', [$collection->getInternalId()]),
|
||||
Query::equal('databaseInternalId', [$database->getInternalId()]),
|
||||
Query::equal('key', [$attributeId]),
|
||||
Query::limit(1),
|
||||
]));
|
||||
@@ -2411,6 +2437,7 @@ App::post('/v1/databases/:databaseId/collections/:collectionId/indexes')
|
||||
$attributeStatus = $oldAttributes[$attributeIndex]['status'];
|
||||
$attributeType = $oldAttributes[$attributeIndex]['type'];
|
||||
$attributeSize = $oldAttributes[$attributeIndex]['size'];
|
||||
$attributeArray = $oldAttributes[$attributeIndex]['array'] ?? false;
|
||||
|
||||
if ($attributeType === Database::VAR_RELATIONSHIP) {
|
||||
throw new Exception(Exception::ATTRIBUTE_TYPE_INVALID, 'Cannot create an index for a relationship attribute: ' . $oldAttributes[$attributeIndex]['key']);
|
||||
@@ -2421,8 +2448,16 @@ App::post('/v1/databases/:databaseId/collections/:collectionId/indexes')
|
||||
throw new Exception(Exception::ATTRIBUTE_NOT_AVAILABLE, 'Attribute not available: ' . $oldAttributes[$attributeIndex]['key']);
|
||||
}
|
||||
|
||||
// set attribute size as index length only for strings
|
||||
$lengths[$i] = ($attributeType === Database::VAR_STRING) ? $attributeSize : null;
|
||||
$lengths[$i] = null;
|
||||
|
||||
if ($attributeType === Database::VAR_STRING) {
|
||||
$lengths[$i] = $attributeSize; // set attribute size as index length only for strings
|
||||
}
|
||||
|
||||
if ($attributeArray === true) {
|
||||
$lengths[$i] = Database::ARRAY_INDEX_LENGTH;
|
||||
$orders[$i] = null;
|
||||
}
|
||||
}
|
||||
|
||||
$index = new Document([
|
||||
@@ -2491,7 +2526,7 @@ App::get('/v1/databases/:databaseId/collections/:collectionId/indexes')
|
||||
->inject('response')
|
||||
->inject('dbForProject')
|
||||
->action(function (string $databaseId, string $collectionId, array $queries, Response $response, Database $dbForProject) {
|
||||
|
||||
/** @var Document $database */
|
||||
$database = Authorization::skip(fn() => $dbForProject->getDocument('databases', $databaseId));
|
||||
|
||||
if ($database->isEmpty()) {
|
||||
@@ -2504,10 +2539,17 @@ App::get('/v1/databases/:databaseId/collections/:collectionId/indexes')
|
||||
throw new Exception(Exception::COLLECTION_NOT_FOUND);
|
||||
}
|
||||
|
||||
$queries = Query::parseQueries($queries);
|
||||
try {
|
||||
$queries = Query::parseQueries($queries);
|
||||
} catch (QueryException $e) {
|
||||
throw new Exception(Exception::GENERAL_QUERY_INVALID, $e->getMessage());
|
||||
}
|
||||
|
||||
\array_push($queries, Query::equal('collectionId', [$collectionId]), Query::equal('databaseId', [$databaseId]));
|
||||
|
||||
// Get cursor document if there was a cursor query
|
||||
/**
|
||||
* Get cursor document if there was a cursor query, we use array_filter and reset for reference $cursor to $queries
|
||||
*/
|
||||
$cursor = \array_filter($queries, function ($query) {
|
||||
return \in_array($query->getMethod(), [Query::TYPE_CURSOR_AFTER, Query::TYPE_CURSOR_BEFORE]);
|
||||
});
|
||||
@@ -2516,8 +2558,8 @@ App::get('/v1/databases/:databaseId/collections/:collectionId/indexes')
|
||||
if ($cursor) {
|
||||
$indexId = $cursor->getValue();
|
||||
$cursorDocument = Authorization::skip(fn() => $dbForProject->find('indexes', [
|
||||
Query::equal('collectionId', [$collectionId]),
|
||||
Query::equal('databaseId', [$databaseId]),
|
||||
Query::equal('collectionInternalId', [$collection->getInternalId()]),
|
||||
Query::equal('databaseInternalId', [$database->getInternalId()]),
|
||||
Query::equal('key', [$indexId]),
|
||||
Query::limit(1)
|
||||
]));
|
||||
@@ -2912,9 +2954,15 @@ App::get('/v1/databases/:databaseId/collections/:collectionId/documents')
|
||||
throw new Exception(Exception::COLLECTION_NOT_FOUND);
|
||||
}
|
||||
|
||||
$queries = Query::parseQueries($queries);
|
||||
try {
|
||||
$queries = Query::parseQueries($queries);
|
||||
} catch (QueryException $e) {
|
||||
throw new Exception(Exception::GENERAL_QUERY_INVALID, $e->getMessage());
|
||||
}
|
||||
|
||||
// Get cursor document if there was a cursor query
|
||||
/**
|
||||
* Get cursor document if there was a cursor query, we use array_filter and reset for reference $cursor to $queries
|
||||
*/
|
||||
$cursor = \array_filter($queries, function ($query) {
|
||||
return \in_array($query->getMethod(), [Query::TYPE_CURSOR_AFTER, Query::TYPE_CURSOR_BEFORE]);
|
||||
});
|
||||
@@ -2933,14 +2981,13 @@ App::get('/v1/databases/:databaseId/collections/:collectionId/documents')
|
||||
$cursor->setValue($cursorDocument);
|
||||
}
|
||||
|
||||
|
||||
try {
|
||||
$documents = $dbForProject->find('database_' . $database->getInternalId() . '_collection_' . $collection->getInternalId(), $queries);
|
||||
$total = $dbForProject->count('database_' . $database->getInternalId() . '_collection_' . $collection->getInternalId(), $queries, APP_LIMIT_COUNT);
|
||||
} catch (AuthorizationException) {
|
||||
throw new Exception(Exception::USER_UNAUTHORIZED);
|
||||
} catch (QueryException $e) {
|
||||
throw new Exception(Exception::GENERAL_ARGUMENT_INVALID, $e->getMessage());
|
||||
throw new Exception(Exception::GENERAL_QUERY_INVALID, $e->getMessage());
|
||||
}
|
||||
|
||||
// Add $collectionId and $databaseId for all documents
|
||||
@@ -3038,14 +3085,13 @@ App::get('/v1/databases/:databaseId/collections/:collectionId/documents/:documen
|
||||
throw new Exception(Exception::COLLECTION_NOT_FOUND);
|
||||
}
|
||||
|
||||
$queries = Query::parseQueries($queries);
|
||||
|
||||
try {
|
||||
$queries = Query::parseQueries($queries);
|
||||
$document = $dbForProject->getDocument('database_' . $database->getInternalId() . '_collection_' . $collection->getInternalId(), $documentId, $queries);
|
||||
} catch (AuthorizationException) {
|
||||
throw new Exception(Exception::USER_UNAUTHORIZED);
|
||||
} catch (QueryException $e) {
|
||||
throw new Exception(Exception::GENERAL_ARGUMENT_INVALID, $e->getMessage());
|
||||
throw new Exception(Exception::GENERAL_QUERY_INVALID, $e->getMessage());
|
||||
}
|
||||
|
||||
if ($document->isEmpty()) {
|
||||
@@ -3134,7 +3180,12 @@ App::get('/v1/databases/:databaseId/collections/:collectionId/documents/:documen
|
||||
throw new Exception(Exception::DOCUMENT_NOT_FOUND);
|
||||
}
|
||||
|
||||
$queries = Query::parseQueries($queries);
|
||||
try {
|
||||
$queries = Query::parseQueries($queries);
|
||||
} catch (QueryException $e) {
|
||||
throw new Exception(Exception::GENERAL_QUERY_INVALID, $e->getMessage());
|
||||
}
|
||||
|
||||
$grouped = Query::groupByType($queries);
|
||||
$limit = $grouped['limit'] ?? APP_LIMIT_COUNT;
|
||||
$offset = $grouped['offset'] ?? 0;
|
||||
@@ -3561,7 +3612,7 @@ App::get('/v1/databases/usage')
|
||||
|
||||
Authorization::skip(function () use ($dbForProject, $days, $metrics, &$stats) {
|
||||
foreach ($metrics as $metric) {
|
||||
$result = $dbForProject->findOne('stats_v2', [
|
||||
$result = $dbForProject->findOne('stats', [
|
||||
Query::equal('metric', [$metric]),
|
||||
Query::equal('period', ['inf'])
|
||||
]);
|
||||
@@ -3569,7 +3620,7 @@ App::get('/v1/databases/usage')
|
||||
$stats[$metric]['total'] = $result['value'] ?? 0;
|
||||
$limit = $days['limit'];
|
||||
$period = $days['period'];
|
||||
$results = $dbForProject->find('stats_v2', [
|
||||
$results = $dbForProject->find('stats', [
|
||||
Query::equal('metric', [$metric]),
|
||||
Query::equal('period', [$period]),
|
||||
Query::limit($limit),
|
||||
@@ -3645,7 +3696,7 @@ App::get('/v1/databases/:databaseId/usage')
|
||||
|
||||
Authorization::skip(function () use ($dbForProject, $days, $metrics, &$stats) {
|
||||
foreach ($metrics as $metric) {
|
||||
$result = $dbForProject->findOne('stats_v2', [
|
||||
$result = $dbForProject->findOne('stats', [
|
||||
Query::equal('metric', [$metric]),
|
||||
Query::equal('period', ['inf'])
|
||||
]);
|
||||
@@ -3653,7 +3704,7 @@ App::get('/v1/databases/:databaseId/usage')
|
||||
$stats[$metric]['total'] = $result['value'] ?? 0;
|
||||
$limit = $days['limit'];
|
||||
$period = $days['period'];
|
||||
$results = $dbForProject->find('stats_v2', [
|
||||
$results = $dbForProject->find('stats', [
|
||||
Query::equal('metric', [$metric]),
|
||||
Query::equal('period', [$period]),
|
||||
Query::limit($limit),
|
||||
@@ -3731,7 +3782,7 @@ App::get('/v1/databases/:databaseId/collections/:collectionId/usage')
|
||||
|
||||
Authorization::skip(function () use ($dbForProject, $days, $metrics, &$stats) {
|
||||
foreach ($metrics as $metric) {
|
||||
$result = $dbForProject->findOne('stats_v2', [
|
||||
$result = $dbForProject->findOne('stats', [
|
||||
Query::equal('metric', [$metric]),
|
||||
Query::equal('period', ['inf'])
|
||||
]);
|
||||
@@ -3739,7 +3790,7 @@ App::get('/v1/databases/:databaseId/collections/:collectionId/usage')
|
||||
$stats[$metric]['total'] = $result['value'] ?? 0;
|
||||
$limit = $days['limit'];
|
||||
$period = $days['period'];
|
||||
$results = $dbForProject->find('stats_v2', [
|
||||
$results = $dbForProject->find('stats', [
|
||||
Query::equal('metric', [$metric]),
|
||||
Query::equal('period', [$period]),
|
||||
Query::limit($limit),
|
||||
|
||||
@@ -12,6 +12,7 @@ use Appwrite\Utopia\Response\Model\Rule;
|
||||
use Appwrite\Extend\Exception;
|
||||
use Appwrite\Utopia\Database\Validator\CustomId;
|
||||
use Appwrite\Messaging\Adapter\Realtime;
|
||||
use Utopia\Database\Exception\Query as QueryException;
|
||||
use Utopia\Validator\Assoc;
|
||||
use Utopia\Database\Helpers\ID;
|
||||
use Utopia\Database\Helpers\Permission;
|
||||
@@ -120,9 +121,7 @@ $redeployVcs = function (Request $request, Document $function, Document $project
|
||||
->setType(BUILD_TYPE_DEPLOYMENT)
|
||||
->setResource($function)
|
||||
->setDeployment($deployment)
|
||||
->setTemplate($template)
|
||||
->setProject($project)
|
||||
->trigger();
|
||||
->setTemplate($template);
|
||||
};
|
||||
|
||||
App::post('/v1/functions')
|
||||
@@ -171,6 +170,12 @@ App::post('/v1/functions')
|
||||
->action(function (string $functionId, string $name, string $runtime, array $execute, array $events, string $schedule, int $timeout, bool $enabled, bool $logging, string $entrypoint, string $commands, string $installationId, string $providerRepositoryId, string $providerBranch, bool $providerSilentMode, string $providerRootDirectory, string $templateRepository, string $templateOwner, string $templateRootDirectory, string $templateBranch, Request $request, Response $response, Database $dbForProject, Document $project, Document $user, Event $queueForEvents, Build $queueForBuilds, Database $dbForConsole, GitHub $github) use ($redeployVcs) {
|
||||
$functionId = ($functionId == 'unique()') ? ID::unique() : $functionId;
|
||||
|
||||
$allowList = \array_filter(\explode(',', App::getEnv('_APP_FUNCTIONS_RUNTIMES', '')));
|
||||
|
||||
if (!empty($allowList) && !\in_array($runtime, $allowList)) {
|
||||
throw new Exception(Exception::FUNCTION_RUNTIME_UNSUPPORTED, 'Runtime "' . $runtime . '" is not supported');
|
||||
}
|
||||
|
||||
// build from template
|
||||
$template = new Document([]);
|
||||
if (
|
||||
@@ -366,13 +371,19 @@ App::get('/v1/functions')
|
||||
->inject('dbForProject')
|
||||
->action(function (array $queries, string $search, Response $response, Database $dbForProject) {
|
||||
|
||||
$queries = Query::parseQueries($queries);
|
||||
try {
|
||||
$queries = Query::parseQueries($queries);
|
||||
} catch (QueryException $e) {
|
||||
throw new Exception(Exception::GENERAL_QUERY_INVALID, $e->getMessage());
|
||||
}
|
||||
|
||||
if (!empty($search)) {
|
||||
$queries[] = Query::search('search', $search);
|
||||
}
|
||||
|
||||
// Get cursor document if there was a cursor query
|
||||
/**
|
||||
* Get cursor document if there was a cursor query, we use array_filter and reset for reference $cursor to $queries
|
||||
*/
|
||||
$cursor = \array_filter($queries, function ($query) {
|
||||
return \in_array($query->getMethod(), [Query::TYPE_CURSOR_AFTER, Query::TYPE_CURSOR_BEFORE]);
|
||||
});
|
||||
@@ -485,7 +496,7 @@ App::get('/v1/functions/:functionId/usage')
|
||||
|
||||
Authorization::skip(function () use ($dbForProject, $days, $metrics, &$stats) {
|
||||
foreach ($metrics as $metric) {
|
||||
$result = $dbForProject->findOne('stats_v2', [
|
||||
$result = $dbForProject->findOne('stats', [
|
||||
Query::equal('metric', [$metric]),
|
||||
Query::equal('period', ['inf'])
|
||||
]);
|
||||
@@ -493,7 +504,7 @@ App::get('/v1/functions/:functionId/usage')
|
||||
$stats[$metric]['total'] = $result['value'] ?? 0;
|
||||
$limit = $days['limit'];
|
||||
$period = $days['period'];
|
||||
$results = $dbForProject->find('stats_v2', [
|
||||
$results = $dbForProject->find('stats', [
|
||||
Query::equal('metric', [$metric]),
|
||||
Query::equal('period', [$period]),
|
||||
Query::limit($limit),
|
||||
@@ -577,7 +588,7 @@ App::get('/v1/functions/usage')
|
||||
|
||||
Authorization::skip(function () use ($dbForProject, $days, $metrics, &$stats) {
|
||||
foreach ($metrics as $metric) {
|
||||
$result = $dbForProject->findOne('stats_v2', [
|
||||
$result = $dbForProject->findOne('stats', [
|
||||
Query::equal('metric', [$metric]),
|
||||
Query::equal('period', ['inf'])
|
||||
]);
|
||||
@@ -585,7 +596,7 @@ App::get('/v1/functions/usage')
|
||||
$stats[$metric]['total'] = $result['value'] ?? 0;
|
||||
$limit = $days['limit'];
|
||||
$period = $days['period'];
|
||||
$results = $dbForProject->find('stats_v2', [
|
||||
$results = $dbForProject->find('stats', [
|
||||
Query::equal('metric', [$metric]),
|
||||
Query::equal('period', [$period]),
|
||||
Query::limit($limit),
|
||||
@@ -1189,9 +1200,7 @@ App::post('/v1/functions/:functionId/deployments')
|
||||
$queueForBuilds
|
||||
->setType(BUILD_TYPE_DEPLOYMENT)
|
||||
->setResource($function)
|
||||
->setDeployment($deployment)
|
||||
->setProject($project)
|
||||
->trigger();
|
||||
->setDeployment($deployment);
|
||||
} else {
|
||||
if ($deployment->isEmpty()) {
|
||||
$deployment = $dbForProject->createDocument('deployments', new Document([
|
||||
@@ -1256,7 +1265,11 @@ App::get('/v1/functions/:functionId/deployments')
|
||||
throw new Exception(Exception::FUNCTION_NOT_FOUND);
|
||||
}
|
||||
|
||||
$queries = Query::parseQueries($queries);
|
||||
try {
|
||||
$queries = Query::parseQueries($queries);
|
||||
} catch (QueryException $e) {
|
||||
throw new Exception(Exception::GENERAL_QUERY_INVALID, $e->getMessage());
|
||||
}
|
||||
|
||||
if (!empty($search)) {
|
||||
$queries[] = Query::search('search', $search);
|
||||
@@ -1266,7 +1279,9 @@ App::get('/v1/functions/:functionId/deployments')
|
||||
$queries[] = Query::equal('resourceId', [$function->getId()]);
|
||||
$queries[] = Query::equal('resourceType', ['functions']);
|
||||
|
||||
// Get cursor document if there was a cursor query
|
||||
/**
|
||||
* Get cursor document if there was a cursor query, we use array_filter and reset for reference $cursor to $queries
|
||||
*/
|
||||
$cursor = \array_filter($queries, function ($query) {
|
||||
return \in_array($query->getMethod(), [Query::TYPE_CURSOR_AFTER, Query::TYPE_CURSOR_BEFORE]);
|
||||
});
|
||||
@@ -1465,9 +1480,7 @@ App::post('/v1/functions/:functionId/deployments/:deploymentId/builds/:buildId')
|
||||
$queueForBuilds
|
||||
->setType(BUILD_TYPE_DEPLOYMENT)
|
||||
->setResource($function)
|
||||
->setDeployment($deployment)
|
||||
->setProject($project)
|
||||
->trigger();
|
||||
->setDeployment($deployment);
|
||||
|
||||
$queueForEvents
|
||||
->setParam('functionId', $function->getId())
|
||||
@@ -1794,7 +1807,11 @@ App::get('/v1/functions/:functionId/executions')
|
||||
throw new Exception(Exception::FUNCTION_NOT_FOUND);
|
||||
}
|
||||
|
||||
$queries = Query::parseQueries($queries);
|
||||
try {
|
||||
$queries = Query::parseQueries($queries);
|
||||
} catch (QueryException $e) {
|
||||
throw new Exception(Exception::GENERAL_QUERY_INVALID, $e->getMessage());
|
||||
}
|
||||
|
||||
if (!empty($search)) {
|
||||
$queries[] = Query::search('search', $search);
|
||||
@@ -1803,7 +1820,9 @@ App::get('/v1/functions/:functionId/executions')
|
||||
// Set internal queries
|
||||
$queries[] = Query::equal('functionId', [$function->getId()]);
|
||||
|
||||
// Get cursor document if there was a cursor query
|
||||
/**
|
||||
* Get cursor document if there was a cursor query, we use array_filter and reset for reference $cursor to $queries
|
||||
*/
|
||||
$cursor = \array_filter($queries, function ($query) {
|
||||
return \in_array($query->getMethod(), [Query::TYPE_CURSOR_AFTER, Query::TYPE_CURSOR_BEFORE]);
|
||||
});
|
||||
|
||||
+106
-11
@@ -7,6 +7,7 @@ use Appwrite\Utopia\Response;
|
||||
use Utopia\App;
|
||||
use Utopia\Config\Config;
|
||||
use Utopia\Database\Document;
|
||||
use Utopia\Domains\Validator\PublicDomain;
|
||||
use Utopia\Pools\Group;
|
||||
use Utopia\Queue\Client;
|
||||
use Utopia\Queue\Connection;
|
||||
@@ -14,8 +15,11 @@ use Utopia\Registry\Registry;
|
||||
use Utopia\Storage\Device;
|
||||
use Utopia\Storage\Device\Local;
|
||||
use Utopia\Storage\Storage;
|
||||
use Utopia\Validator\Domain;
|
||||
use Utopia\Validator\Integer;
|
||||
use Utopia\Validator\Multiple;
|
||||
use Utopia\Validator\Text;
|
||||
use Utopia\Validator\WhiteList;
|
||||
|
||||
App::get('/v1/health')
|
||||
->desc('Get HTTP')
|
||||
@@ -355,7 +359,7 @@ App::get('/v1/health/queue/webhooks')
|
||||
$size = $client->getQueueSize();
|
||||
|
||||
if ($size >= $threshold) {
|
||||
throw new Exception(Exception::QUEUE_SIZE_EXCEEDED, "Queue size threshold hit. Current size is {$size} and threshold is {$threshold}.");
|
||||
throw new Exception(Exception::HEALTH_QUEUE_SIZE_EXCEEDED, "Queue size threshold hit. Current size is {$size} and threshold is {$threshold}.");
|
||||
}
|
||||
|
||||
$response->dynamic(new Document([ 'size' => $size ]), Response::MODEL_HEALTH_QUEUE);
|
||||
@@ -382,12 +386,62 @@ App::get('/v1/health/queue/logs')
|
||||
$size = $client->getQueueSize();
|
||||
|
||||
if ($size >= $threshold) {
|
||||
throw new Exception(Exception::QUEUE_SIZE_EXCEEDED, "Queue size threshold hit. Current size is {$size} and threshold is {$threshold}.");
|
||||
throw new Exception(Exception::HEALTH_QUEUE_SIZE_EXCEEDED, "Queue size threshold hit. Current size is {$size} and threshold is {$threshold}.");
|
||||
}
|
||||
|
||||
$response->dynamic(new Document([ 'size' => $size ]), Response::MODEL_HEALTH_QUEUE);
|
||||
}, ['response']);
|
||||
|
||||
App::get('/v1/health/certificate')
|
||||
->desc('Get the SSL certificate for a domain')
|
||||
->groups(['api', 'health'])
|
||||
->label('scope', 'health.read')
|
||||
->label('sdk.auth', [APP_AUTH_TYPE_KEY])
|
||||
->label('sdk.namespace', 'health')
|
||||
->label('sdk.method', 'getCertificate')
|
||||
->label('sdk.description', '/docs/references/health/get-certificate.md')
|
||||
->label('sdk.response.code', Response::STATUS_CODE_OK)
|
||||
->label('sdk.response.type', Response::CONTENT_TYPE_JSON)
|
||||
->label('sdk.response.model', Response::MODEL_HEALTH_CERTIFICATE)
|
||||
->param('domain', null, new Multiple([new Domain(), new PublicDomain()]), Multiple::TYPE_STRING, 'Domain name')
|
||||
->inject('response')
|
||||
->action(function (string $domain, Response $response) {
|
||||
if (filter_var($domain, FILTER_VALIDATE_URL)) {
|
||||
$domain = parse_url($domain, PHP_URL_HOST);
|
||||
}
|
||||
|
||||
$sslContext = stream_context_create([
|
||||
"ssl" => [
|
||||
"capture_peer_cert" => true
|
||||
]
|
||||
]);
|
||||
$sslSocket = stream_socket_client("ssl://" . $domain . ":443", $errno, $errstr, 30, STREAM_CLIENT_CONNECT, $sslContext);
|
||||
if (!$sslSocket) {
|
||||
throw new Exception(Exception::HEALTH_INVALID_HOST);
|
||||
}
|
||||
|
||||
$streamContextParams = stream_context_get_params($sslSocket);
|
||||
$peerCertificate = $streamContextParams['options']['ssl']['peer_certificate'];
|
||||
$certificatePayload = openssl_x509_parse($peerCertificate);
|
||||
|
||||
|
||||
$sslExpiration = $certificatePayload['validTo_time_t'];
|
||||
$status = $sslExpiration < time() ? 'fail' : 'pass';
|
||||
|
||||
if ($status === 'fail') {
|
||||
throw new Exception(Exception::HEALTH_CERTIFICATE_EXPIRED);
|
||||
}
|
||||
|
||||
$response->dynamic(new Document([
|
||||
'name' => $certificatePayload['name'],
|
||||
'subjectSN' => $certificatePayload['subject']['CN'],
|
||||
'issuerOrganisation' => $certificatePayload['issuer']['O'],
|
||||
'validFrom' => $certificatePayload['validFrom_time_t'],
|
||||
'validTo' => $certificatePayload['validTo_time_t'],
|
||||
'signatureTypeSN' => $certificatePayload['signatureTypeSN'],
|
||||
]), Response::MODEL_HEALTH_CERTIFICATE);
|
||||
}, ['response']);
|
||||
|
||||
App::get('/v1/health/queue/certificates')
|
||||
->desc('Get certificates queue')
|
||||
->groups(['api', 'health'])
|
||||
@@ -409,7 +463,7 @@ App::get('/v1/health/queue/certificates')
|
||||
$size = $client->getQueueSize();
|
||||
|
||||
if ($size >= $threshold) {
|
||||
throw new Exception(Exception::QUEUE_SIZE_EXCEEDED, "Queue size threshold hit. Current size is {$size} and threshold is {$threshold}.");
|
||||
throw new Exception(Exception::HEALTH_QUEUE_SIZE_EXCEEDED, "Queue size threshold hit. Current size is {$size} and threshold is {$threshold}.");
|
||||
}
|
||||
|
||||
$response->dynamic(new Document([ 'size' => $size ]), Response::MODEL_HEALTH_QUEUE);
|
||||
@@ -436,7 +490,7 @@ App::get('/v1/health/queue/builds')
|
||||
$size = $client->getQueueSize();
|
||||
|
||||
if ($size >= $threshold) {
|
||||
throw new Exception(Exception::QUEUE_SIZE_EXCEEDED, "Queue size threshold hit. Current size is {$size} and threshold is {$threshold}.");
|
||||
throw new Exception(Exception::HEALTH_QUEUE_SIZE_EXCEEDED, "Queue size threshold hit. Current size is {$size} and threshold is {$threshold}.");
|
||||
}
|
||||
|
||||
$response->dynamic(new Document([ 'size' => $size ]), Response::MODEL_HEALTH_QUEUE);
|
||||
@@ -464,7 +518,7 @@ App::get('/v1/health/queue/databases')
|
||||
$size = $client->getQueueSize();
|
||||
|
||||
if ($size >= $threshold) {
|
||||
throw new Exception(Exception::QUEUE_SIZE_EXCEEDED, "Queue size threshold hit. Current size is {$size} and threshold is {$threshold}.");
|
||||
throw new Exception(Exception::HEALTH_QUEUE_SIZE_EXCEEDED, "Queue size threshold hit. Current size is {$size} and threshold is {$threshold}.");
|
||||
}
|
||||
|
||||
$response->dynamic(new Document([ 'size' => $size ]), Response::MODEL_HEALTH_QUEUE);
|
||||
@@ -491,7 +545,7 @@ App::get('/v1/health/queue/deletes')
|
||||
$size = $client->getQueueSize();
|
||||
|
||||
if ($size >= $threshold) {
|
||||
throw new Exception(Exception::QUEUE_SIZE_EXCEEDED, "Queue size threshold hit. Current size is {$size} and threshold is {$threshold}.");
|
||||
throw new Exception(Exception::HEALTH_QUEUE_SIZE_EXCEEDED, "Queue size threshold hit. Current size is {$size} and threshold is {$threshold}.");
|
||||
}
|
||||
|
||||
$response->dynamic(new Document([ 'size' => $size ]), Response::MODEL_HEALTH_QUEUE);
|
||||
@@ -518,7 +572,7 @@ App::get('/v1/health/queue/mails')
|
||||
$size = $client->getQueueSize();
|
||||
|
||||
if ($size >= $threshold) {
|
||||
throw new Exception(Exception::QUEUE_SIZE_EXCEEDED, "Queue size threshold hit. Current size is {$size} and threshold is {$threshold}.");
|
||||
throw new Exception(Exception::HEALTH_QUEUE_SIZE_EXCEEDED, "Queue size threshold hit. Current size is {$size} and threshold is {$threshold}.");
|
||||
}
|
||||
|
||||
$response->dynamic(new Document([ 'size' => $size ]), Response::MODEL_HEALTH_QUEUE);
|
||||
@@ -545,7 +599,7 @@ App::get('/v1/health/queue/messaging')
|
||||
$size = $client->getQueueSize();
|
||||
|
||||
if ($size >= $threshold) {
|
||||
throw new Exception(Exception::QUEUE_SIZE_EXCEEDED, "Queue size threshold hit. Current size is {$size} and threshold is {$threshold}.");
|
||||
throw new Exception(Exception::HEALTH_QUEUE_SIZE_EXCEEDED, "Queue size threshold hit. Current size is {$size} and threshold is {$threshold}.");
|
||||
}
|
||||
|
||||
$response->dynamic(new Document([ 'size' => $size ]), Response::MODEL_HEALTH_QUEUE);
|
||||
@@ -572,7 +626,7 @@ App::get('/v1/health/queue/migrations')
|
||||
$size = $client->getQueueSize();
|
||||
|
||||
if ($size >= $threshold) {
|
||||
throw new Exception(Exception::QUEUE_SIZE_EXCEEDED, "Queue size threshold hit. Current size is {$size} and threshold is {$threshold}.");
|
||||
throw new Exception(Exception::HEALTH_QUEUE_SIZE_EXCEEDED, "Queue size threshold hit. Current size is {$size} and threshold is {$threshold}.");
|
||||
}
|
||||
|
||||
$response->dynamic(new Document([ 'size' => $size ]), Response::MODEL_HEALTH_QUEUE);
|
||||
@@ -599,7 +653,7 @@ App::get('/v1/health/queue/functions')
|
||||
$size = $client->getQueueSize();
|
||||
|
||||
if ($size >= $threshold) {
|
||||
throw new Exception(Exception::QUEUE_SIZE_EXCEEDED, "Queue size threshold hit. Current size is {$size} and threshold is {$threshold}.");
|
||||
throw new Exception(Exception::HEALTH_QUEUE_SIZE_EXCEEDED, "Queue size threshold hit. Current size is {$size} and threshold is {$threshold}.");
|
||||
}
|
||||
|
||||
$response->dynamic(new Document([ 'size' => $size ]), Response::MODEL_HEALTH_QUEUE);
|
||||
@@ -679,7 +733,7 @@ App::get('/v1/health/anti-virus')
|
||||
try {
|
||||
$output['version'] = @$antivirus->version();
|
||||
$output['status'] = (@$antivirus->ping()) ? 'pass' : 'fail';
|
||||
} catch (\Exception $e) {
|
||||
} catch (\Throwable $e) {
|
||||
throw new Exception(Exception::GENERAL_SERVER_ERROR, 'Antivirus is not available');
|
||||
}
|
||||
}
|
||||
@@ -687,6 +741,47 @@ App::get('/v1/health/anti-virus')
|
||||
$response->dynamic(new Document($output), Response::MODEL_HEALTH_ANTIVIRUS);
|
||||
});
|
||||
|
||||
App::get('/v1/health/queue/failed/:name')
|
||||
->desc('Get number of failed queue jobs')
|
||||
->groups(['api', 'health'])
|
||||
->label('scope', 'health.read')
|
||||
->label('sdk.auth', [APP_AUTH_TYPE_KEY])
|
||||
->label('sdk.namespace', 'health')
|
||||
->label('sdk.method', 'getFailedJobs')
|
||||
->param('name', '', new WhiteList([
|
||||
Event::DATABASE_QUEUE_NAME,
|
||||
Event::DELETE_QUEUE_NAME,
|
||||
Event::AUDITS_QUEUE_NAME,
|
||||
Event::MAILS_QUEUE_NAME,
|
||||
Event::FUNCTIONS_QUEUE_NAME,
|
||||
Event::USAGE_QUEUE_NAME,
|
||||
Event::WEBHOOK_CLASS_NAME,
|
||||
Event::CERTIFICATES_QUEUE_NAME,
|
||||
Event::BUILDS_QUEUE_NAME,
|
||||
Event::MESSAGING_QUEUE_NAME,
|
||||
Event::MIGRATIONS_QUEUE_NAME,
|
||||
Event::HAMSTER_CLASS_NAME
|
||||
]), 'The name of the queue')
|
||||
->param('threshold', 5000, new Integer(true), 'Queue size threshold. When hit (equal or higher), endpoint returns server error. Default value is 5000.', true)
|
||||
->label('sdk.description', '/docs/references/health/get-failed-queue-jobs.md')
|
||||
->label('sdk.response.code', Response::STATUS_CODE_OK)
|
||||
->label('sdk.response.type', Response::CONTENT_TYPE_JSON)
|
||||
->label('sdk.response.model', Response::MODEL_HEALTH_QUEUE)
|
||||
->inject('response')
|
||||
->inject('queue')
|
||||
->action(function (string $name, int|string $threshold, Response $response, Connection $queue) {
|
||||
$threshold = \intval($threshold);
|
||||
|
||||
$client = new Client($name, $queue);
|
||||
$failed = $client->countFailedJobs();
|
||||
|
||||
if ($failed >= $threshold) {
|
||||
throw new Exception(Exception::HEALTH_QUEUE_SIZE_EXCEEDED, "Queue failed jobs threshold hit. Current size is {$failed} and threshold is {$threshold}.");
|
||||
}
|
||||
|
||||
$response->dynamic(new Document([ 'size' => $failed ]), Response::MODEL_HEALTH_QUEUE);
|
||||
});
|
||||
|
||||
App::get('/v1/health/stats') // Currently only used internally
|
||||
->desc('Get system stats')
|
||||
->groups(['api', 'health'])
|
||||
|
||||
@@ -22,6 +22,7 @@ use Utopia\Audit\Audit;
|
||||
use Utopia\Database\Database;
|
||||
use Utopia\Database\Document;
|
||||
use Utopia\Database\Exception\Duplicate as DuplicateException;
|
||||
use Utopia\Database\Exception\Query as QueryException;
|
||||
use Utopia\Database\Helpers\ID;
|
||||
use Utopia\Database\Query;
|
||||
use Utopia\Database\Validator\Authorization;
|
||||
@@ -29,6 +30,7 @@ use Utopia\Database\Validator\Datetime as DatetimeValidator;
|
||||
use Utopia\Database\Validator\Queries;
|
||||
use Utopia\Database\Validator\Query\Limit;
|
||||
use Utopia\Database\Validator\Query\Offset;
|
||||
use Utopia\Database\Validator\Roles;
|
||||
use Utopia\Database\Validator\UID;
|
||||
use Utopia\Locale\Locale;
|
||||
use Utopia\Validator\ArrayList;
|
||||
@@ -387,13 +389,13 @@ App::post('/v1/messaging/providers/telesign')
|
||||
->param('providerId', '', new CustomId(), 'Provider ID. Choose a custom ID or generate a random ID with `ID.unique()`. Valid chars are a-z, A-Z, 0-9, period, hyphen, and underscore. Can\'t start with a special char. Max length is 36 chars.')
|
||||
->param('name', '', new Text(128), 'Provider name.')
|
||||
->param('from', '', new Phone(), 'Sender Phone number. Format this number with a leading \'+\' and a country code, e.g., +16175551212.', true)
|
||||
->param('username', '', new Text(0), 'Telesign username.', true)
|
||||
->param('password', '', new Text(0), 'Telesign password.', true)
|
||||
->param('customerId', '', new Text(0), 'Telesign customer ID.', true)
|
||||
->param('apiKey', '', new Text(0), 'Telesign API key.', true)
|
||||
->param('enabled', null, new Boolean(), 'Set as enabled.', true)
|
||||
->inject('queueForEvents')
|
||||
->inject('dbForProject')
|
||||
->inject('response')
|
||||
->action(function (string $providerId, string $name, string $from, string $username, string $password, ?bool $enabled, Event $queueForEvents, Database $dbForProject, Response $response) {
|
||||
->action(function (string $providerId, string $name, string $from, string $customerId, string $apiKey, ?bool $enabled, Event $queueForEvents, Database $dbForProject, Response $response) {
|
||||
$providerId = $providerId == 'unique()' ? ID::unique() : $providerId;
|
||||
|
||||
$options = [];
|
||||
@@ -404,18 +406,18 @@ App::post('/v1/messaging/providers/telesign')
|
||||
|
||||
$credentials = [];
|
||||
|
||||
if (!empty($username)) {
|
||||
$credentials['username'] = $username;
|
||||
if (!empty($customerId)) {
|
||||
$credentials['customerId'] = $customerId;
|
||||
}
|
||||
|
||||
if (!empty($password)) {
|
||||
$credentials['password'] = $password;
|
||||
if (!empty($apiKey)) {
|
||||
$credentials['apiKey'] = $apiKey;
|
||||
}
|
||||
|
||||
if (
|
||||
$enabled === true
|
||||
&& \array_key_exists('username', $credentials)
|
||||
&& \array_key_exists('password', $credentials)
|
||||
&& \array_key_exists('customerId', $credentials)
|
||||
&& \array_key_exists('apiKey', $credentials)
|
||||
&& \array_key_exists('from', $options)
|
||||
) {
|
||||
$enabled = true;
|
||||
@@ -837,14 +839,22 @@ App::get('/v1/messaging/providers')
|
||||
->inject('dbForProject')
|
||||
->inject('response')
|
||||
->action(function (array $queries, string $search, Database $dbForProject, Response $response) {
|
||||
$queries = Query::parseQueries($queries);
|
||||
try {
|
||||
$queries = Query::parseQueries($queries);
|
||||
} catch (QueryException $e) {
|
||||
throw new Exception(Exception::GENERAL_QUERY_INVALID, $e->getMessage());
|
||||
}
|
||||
|
||||
if (!empty($search)) {
|
||||
$queries[] = Query::search('search', $search);
|
||||
}
|
||||
|
||||
// Get cursor document if there was a cursor query
|
||||
$cursor = Query::getByType($queries, [Query::TYPE_CURSOR_AFTER, Query::TYPE_CURSOR_BEFORE]);
|
||||
/**
|
||||
* Get cursor document if there was a cursor query, we use array_filter and reset for reference $cursor to $queries
|
||||
*/
|
||||
$cursor = \array_filter($queries, function ($query) {
|
||||
return \in_array($query->getMethod(), [Query::TYPE_CURSOR_AFTER, Query::TYPE_CURSOR_BEFORE]);
|
||||
});
|
||||
$cursor = reset($cursor);
|
||||
|
||||
if ($cursor) {
|
||||
@@ -888,7 +898,12 @@ App::get('/v1/messaging/providers/:providerId/logs')
|
||||
throw new Exception(Exception::PROVIDER_NOT_FOUND);
|
||||
}
|
||||
|
||||
$queries = Query::parseQueries($queries);
|
||||
try {
|
||||
$queries = Query::parseQueries($queries);
|
||||
} catch (QueryException $e) {
|
||||
throw new Exception(Exception::GENERAL_QUERY_INVALID, $e->getMessage());
|
||||
}
|
||||
|
||||
$grouped = Query::groupByType($queries);
|
||||
$limit = $grouped['limit'] ?? APP_LIMIT_COUNT;
|
||||
$offset = $grouped['offset'] ?? 0;
|
||||
@@ -1190,6 +1205,7 @@ App::patch('/v1/messaging/providers/smtp/:providerId')
|
||||
->param('password', '', new Text(0), 'Authentication password.', true)
|
||||
->param('encryption', '', new WhiteList(['none', 'ssl', 'tls']), 'Encryption type. Can be \'ssl\' or \'tls\'', true)
|
||||
->param('autoTLS', null, new Boolean(), 'Enable SMTP AutoTLS feature.', true)
|
||||
->param('mailer', '', new Text(0), 'The value to use for the X-Mailer header.', true)
|
||||
->param('fromName', '', new Text(128), 'Sender Name.', true)
|
||||
->param('fromEmail', '', new Email(), 'Sender email address.', true)
|
||||
->param('replyToName', '', new Text(128), 'Name set in the Reply To field for the mail. Default value is Sender Name.', true)
|
||||
@@ -1198,16 +1214,14 @@ App::patch('/v1/messaging/providers/smtp/:providerId')
|
||||
->inject('queueForEvents')
|
||||
->inject('dbForProject')
|
||||
->inject('response')
|
||||
->action(function (string $providerId, string $name, string $host, ?int $port, string $username, string $password, string $encryption, ?bool $autoTLS, string $fromName, string $fromEmail, string $replyToName, string $replyToEmail, ?bool $enabled, Event $queueForEvents, Database $dbForProject, Response $response) {
|
||||
->action(function (string $providerId, string $name, string $host, ?int $port, string $username, string $password, string $encryption, ?bool $autoTLS, string $mailer, string $fromName, string $fromEmail, string $replyToName, string $replyToEmail, ?bool $enabled, Event $queueForEvents, Database $dbForProject, Response $response) {
|
||||
$provider = $dbForProject->getDocument('providers', $providerId);
|
||||
|
||||
if ($provider->isEmpty()) {
|
||||
throw new Exception(Exception::PROVIDER_NOT_FOUND);
|
||||
}
|
||||
|
||||
$providerAttr = $provider->getAttribute('provider');
|
||||
|
||||
if ($providerAttr !== 'smtp') {
|
||||
if ($provider->getAttribute('provider') !== 'smtp') {
|
||||
throw new Exception(Exception::PROVIDER_INCORRECT_TYPE);
|
||||
}
|
||||
|
||||
@@ -1217,6 +1231,18 @@ App::patch('/v1/messaging/providers/smtp/:providerId')
|
||||
|
||||
$options = $provider->getAttribute('options');
|
||||
|
||||
if (!empty($encryption)) {
|
||||
$options['encryption'] = $encryption === 'none' ? '' : $encryption;
|
||||
}
|
||||
|
||||
if (!\is_null($autoTLS)) {
|
||||
$options['autoTLS'] = $autoTLS;
|
||||
}
|
||||
|
||||
if (!empty($mailer)) {
|
||||
$options['mailer'] = $mailer;
|
||||
}
|
||||
|
||||
if (!empty($fromName)) {
|
||||
$options['fromName'] = $fromName;
|
||||
}
|
||||
@@ -1253,14 +1279,6 @@ App::patch('/v1/messaging/providers/smtp/:providerId')
|
||||
$credentials['password'] = $password;
|
||||
}
|
||||
|
||||
if (!empty($encryption)) {
|
||||
$credentials['encryption'] = $encryption === 'none' ? '' : $encryption;
|
||||
}
|
||||
|
||||
if (!\is_null($autoTLS)) {
|
||||
$credentials['autoTLS'] = $autoTLS;
|
||||
}
|
||||
|
||||
$provider->setAttribute('credentials', $credentials);
|
||||
|
||||
if (!\is_null($enabled)) {
|
||||
@@ -1386,13 +1404,13 @@ App::patch('/v1/messaging/providers/telesign/:providerId')
|
||||
->param('providerId', '', new UID(), 'Provider ID.')
|
||||
->param('name', '', new Text(128), 'Provider name.', true)
|
||||
->param('enabled', null, new Boolean(), 'Set as enabled.', true)
|
||||
->param('username', '', new Text(0), 'Telesign username.', true)
|
||||
->param('password', '', new Text(0), 'Telesign password.', true)
|
||||
->param('customerId', '', new Text(0), 'Telesign customer ID.', true)
|
||||
->param('apiKey', '', new Text(0), 'Telesign API key.', true)
|
||||
->param('from', '', new Text(256), 'Sender number.', true)
|
||||
->inject('queueForEvents')
|
||||
->inject('dbForProject')
|
||||
->inject('response')
|
||||
->action(function (string $providerId, string $name, ?bool $enabled, string $username, string $password, string $from, Event $queueForEvents, Database $dbForProject, Response $response) {
|
||||
->action(function (string $providerId, string $name, ?bool $enabled, string $customerId, string $apiKey, string $from, Event $queueForEvents, Database $dbForProject, Response $response) {
|
||||
$provider = $dbForProject->getDocument('providers', $providerId);
|
||||
|
||||
if ($provider->isEmpty()) {
|
||||
@@ -1416,12 +1434,12 @@ App::patch('/v1/messaging/providers/telesign/:providerId')
|
||||
|
||||
$credentials = $provider->getAttribute('credentials');
|
||||
|
||||
if (!empty($username)) {
|
||||
$credentials['username'] = $username;
|
||||
if (!empty($customerId)) {
|
||||
$credentials['customerId'] = $customerId;
|
||||
}
|
||||
|
||||
if (!empty($password)) {
|
||||
$credentials['password'] = $password;
|
||||
if (!empty($apiKey)) {
|
||||
$credentials['apiKey'] = $apiKey;
|
||||
}
|
||||
|
||||
$provider->setAttribute('credentials', $credentials);
|
||||
@@ -1429,8 +1447,8 @@ App::patch('/v1/messaging/providers/telesign/:providerId')
|
||||
if (!\is_null($enabled)) {
|
||||
if ($enabled) {
|
||||
if (
|
||||
\array_key_exists('username', $credentials) &&
|
||||
\array_key_exists('password', $credentials) &&
|
||||
\array_key_exists('customerId', $credentials) &&
|
||||
\array_key_exists('apiKey', $credentials) &&
|
||||
\array_key_exists('from', $provider->getAttribute('options'))
|
||||
) {
|
||||
$provider->setAttribute('enabled', true);
|
||||
@@ -1903,15 +1921,17 @@ App::post('/v1/messaging/topics')
|
||||
->label('sdk.response.model', Response::MODEL_TOPIC)
|
||||
->param('topicId', '', new CustomId(), 'Topic ID. Choose a custom Topic ID or a new Topic ID.')
|
||||
->param('name', '', new Text(128), 'Topic Name.')
|
||||
->param('subscribe', [Role::users()], new Roles(APP_LIMIT_ARRAY_PARAMS_SIZE), 'An array of role strings with subscribe permission. By default all users are granted with any subscribe permission. [learn more about roles](https://appwrite.io/docs/permissions#permission-roles). Maximum of ' . APP_LIMIT_ARRAY_PARAMS_SIZE . ' roles are allowed, each 64 characters long.', true)
|
||||
->inject('queueForEvents')
|
||||
->inject('dbForProject')
|
||||
->inject('response')
|
||||
->action(function (string $topicId, string $name, Event $queueForEvents, Database $dbForProject, Response $response) {
|
||||
->action(function (string $topicId, string $name, array $subscribe, Event $queueForEvents, Database $dbForProject, Response $response) {
|
||||
$topicId = $topicId == 'unique()' ? ID::unique() : $topicId;
|
||||
|
||||
$topic = new Document([
|
||||
'$id' => $topicId,
|
||||
'name' => $name,
|
||||
'subscribe' => $subscribe,
|
||||
]);
|
||||
|
||||
try {
|
||||
@@ -1944,14 +1964,22 @@ App::get('/v1/messaging/topics')
|
||||
->inject('dbForProject')
|
||||
->inject('response')
|
||||
->action(function (array $queries, string $search, Database $dbForProject, Response $response) {
|
||||
$queries = Query::parseQueries($queries);
|
||||
try {
|
||||
$queries = Query::parseQueries($queries);
|
||||
} catch (QueryException $e) {
|
||||
throw new Exception(Exception::GENERAL_QUERY_INVALID, $e->getMessage());
|
||||
}
|
||||
|
||||
if (!empty($search)) {
|
||||
$queries[] = Query::search('search', $search);
|
||||
}
|
||||
|
||||
// Get cursor document if there was a cursor query
|
||||
$cursor = Query::getByType($queries, [Query::TYPE_CURSOR_AFTER, Query::TYPE_CURSOR_BEFORE]);
|
||||
/**
|
||||
* Get cursor document if there was a cursor query, we use array_filter and reset for reference $cursor to $queries
|
||||
*/
|
||||
$cursor = \array_filter($queries, function ($query) {
|
||||
return \in_array($query->getMethod(), [Query::TYPE_CURSOR_AFTER, Query::TYPE_CURSOR_BEFORE]);
|
||||
});
|
||||
$cursor = reset($cursor);
|
||||
|
||||
if ($cursor) {
|
||||
@@ -1995,7 +2023,12 @@ App::get('/v1/messaging/topics/:topicId/logs')
|
||||
throw new Exception(Exception::TOPIC_NOT_FOUND);
|
||||
}
|
||||
|
||||
$queries = Query::parseQueries($queries);
|
||||
try {
|
||||
$queries = Query::parseQueries($queries);
|
||||
} catch (QueryException $e) {
|
||||
throw new Exception(Exception::GENERAL_QUERY_INVALID, $e->getMessage());
|
||||
}
|
||||
|
||||
$grouped = Query::groupByType($queries);
|
||||
$limit = $grouped['limit'] ?? APP_LIMIT_COUNT;
|
||||
$offset = $grouped['offset'] ?? 0;
|
||||
@@ -2190,6 +2223,12 @@ App::post('/v1/messaging/topics/:topicId/subscribers')
|
||||
throw new Exception(Exception::TOPIC_NOT_FOUND);
|
||||
}
|
||||
|
||||
$validator = new Authorization('subscribe');
|
||||
|
||||
if (!$validator->isValid($topic->getAttribute('subscribe'))) {
|
||||
throw new Exception(Exception::USER_UNAUTHORIZED, $validator->getDescription());
|
||||
}
|
||||
|
||||
$target = Authorization::skip(fn () => $dbForProject->getDocument('targets', $targetId));
|
||||
|
||||
if ($target->isEmpty()) {
|
||||
@@ -2198,32 +2237,42 @@ App::post('/v1/messaging/topics/:topicId/subscribers')
|
||||
|
||||
$user = Authorization::skip(fn () => $dbForProject->getDocument('users', $target->getAttribute('userId')));
|
||||
|
||||
$userId = $user->getId();
|
||||
|
||||
$subscriber = new Document([
|
||||
'$id' => $subscriberId,
|
||||
'$permissions' => [
|
||||
Permission::read(Role::user($userId)),
|
||||
Permission::delete(Role::user($userId)),
|
||||
Permission::read(Role::user($user->getId())),
|
||||
Permission::delete(Role::user($user->getId())),
|
||||
],
|
||||
'topicId' => $topicId,
|
||||
'topicInternalId' => $topic->getInternalId(),
|
||||
'targetId' => $targetId,
|
||||
'targetInternalId' => $target->getInternalId(),
|
||||
'userId' => $userId,
|
||||
'userId' => $user->getId(),
|
||||
'userInternalId' => $user->getInternalId(),
|
||||
'providerType' => $target->getAttribute('providerType'),
|
||||
'search' => implode(' ', [
|
||||
$subscriberId,
|
||||
$targetId,
|
||||
$userId,
|
||||
$user->getId(),
|
||||
$target->getAttribute('providerType'),
|
||||
]),
|
||||
]);
|
||||
|
||||
try {
|
||||
$subscriber = $dbForProject->createDocument('subscribers', $subscriber);
|
||||
Authorization::skip(fn () => $dbForProject->increaseDocumentAttribute('topics', $topicId, 'total', 1));
|
||||
|
||||
$totalAttribute = match ($target->getAttribute('providerType')) {
|
||||
MESSAGE_TYPE_EMAIL => 'emailTotal',
|
||||
MESSAGE_TYPE_SMS => 'smsTotal',
|
||||
MESSAGE_TYPE_PUSH => 'pushTotal',
|
||||
default => throw new Exception(Exception::TARGET_PROVIDER_INVALID_TYPE),
|
||||
};
|
||||
|
||||
Authorization::skip(fn () => $dbForProject->increaseDocumentAttribute(
|
||||
'topics',
|
||||
$topicId,
|
||||
$totalAttribute,
|
||||
));
|
||||
} catch (DuplicateException) {
|
||||
throw new Exception(Exception::SUBSCRIBER_ALREADY_EXISTS);
|
||||
}
|
||||
@@ -2258,7 +2307,11 @@ App::get('/v1/messaging/topics/:topicId/subscribers')
|
||||
->inject('dbForProject')
|
||||
->inject('response')
|
||||
->action(function (string $topicId, array $queries, string $search, Database $dbForProject, Response $response) {
|
||||
$queries = Query::parseQueries($queries);
|
||||
try {
|
||||
$queries = Query::parseQueries($queries);
|
||||
} catch (QueryException $e) {
|
||||
throw new Exception(Exception::GENERAL_QUERY_INVALID, $e->getMessage());
|
||||
}
|
||||
|
||||
if (!empty($search)) {
|
||||
$queries[] = Query::search('search', $search);
|
||||
@@ -2270,10 +2323,14 @@ App::get('/v1/messaging/topics/:topicId/subscribers')
|
||||
throw new Exception(Exception::TOPIC_NOT_FOUND);
|
||||
}
|
||||
|
||||
\array_push($queries, Query::equal('topicInternalId', [$topic->getInternalId()]));
|
||||
$queries[] = Query::equal('topicInternalId', [$topic->getInternalId()]);
|
||||
|
||||
// Get cursor document if there was a cursor query
|
||||
$cursor = Query::getByType($queries, [Query::TYPE_CURSOR_AFTER, Query::TYPE_CURSOR_BEFORE]);
|
||||
/**
|
||||
* Get cursor document if there was a cursor query, we use array_filter and reset for reference $cursor to $queries
|
||||
*/
|
||||
$cursor = \array_filter($queries, function ($query) {
|
||||
return \in_array($query->getMethod(), [Query::TYPE_CURSOR_AFTER, Query::TYPE_CURSOR_BEFORE]);
|
||||
});
|
||||
$cursor = reset($cursor);
|
||||
|
||||
if ($cursor) {
|
||||
@@ -2331,7 +2388,12 @@ App::get('/v1/messaging/subscribers/:subscriberId/logs')
|
||||
throw new Exception(Exception::SUBSCRIBER_NOT_FOUND);
|
||||
}
|
||||
|
||||
$queries = Query::parseQueries($queries);
|
||||
try {
|
||||
$queries = Query::parseQueries($queries);
|
||||
} catch (QueryException $e) {
|
||||
throw new Exception(Exception::GENERAL_QUERY_INVALID, $e->getMessage());
|
||||
}
|
||||
|
||||
$grouped = Query::groupByType($queries);
|
||||
$limit = $grouped['limit'] ?? APP_LIMIT_COUNT;
|
||||
$offset = $grouped['offset'] ?? 0;
|
||||
@@ -2462,8 +2524,23 @@ App::delete('/v1/messaging/topics/:topicId/subscribers/:subscriberId')
|
||||
throw new Exception(Exception::SUBSCRIBER_NOT_FOUND);
|
||||
}
|
||||
|
||||
$target = $dbForProject->getDocument('targets', $subscriber->getAttribute('targetId'));
|
||||
|
||||
$dbForProject->deleteDocument('subscribers', $subscriberId);
|
||||
Authorization::skip(fn () => $dbForProject->decreaseDocumentAttribute('topics', $topicId, 'total', 1));
|
||||
|
||||
$totalAttribute = match ($target->getAttribute('providerType')) {
|
||||
MESSAGE_TYPE_EMAIL => 'emailTotal',
|
||||
MESSAGE_TYPE_SMS => 'smsTotal',
|
||||
MESSAGE_TYPE_PUSH => 'pushTotal',
|
||||
default => throw new Exception(Exception::TARGET_PROVIDER_INVALID_TYPE),
|
||||
};
|
||||
|
||||
Authorization::skip(fn () => $dbForProject->decreaseDocumentAttribute(
|
||||
'topics',
|
||||
$topicId,
|
||||
$totalAttribute,
|
||||
min: 0
|
||||
));
|
||||
|
||||
$queueForEvents
|
||||
->setParam('topicId', $topic->getId())
|
||||
@@ -2558,8 +2635,8 @@ App::post('/v1/messaging/messages/email')
|
||||
switch ($status) {
|
||||
case MessageStatus::PROCESSING:
|
||||
$queueForMessaging
|
||||
->setMessageId($message->getId())
|
||||
->trigger();
|
||||
->setType(MESSAGE_SEND_TYPE_EXTERNAL)
|
||||
->setMessageId($message->getId());
|
||||
break;
|
||||
case MessageStatus::SCHEDULED:
|
||||
$schedule = $dbForConsole->createDocument('schedules', new Document([
|
||||
@@ -2667,8 +2744,8 @@ App::post('/v1/messaging/messages/sms')
|
||||
switch ($status) {
|
||||
case MessageStatus::PROCESSING:
|
||||
$queueForMessaging
|
||||
->setMessageId($message->getId())
|
||||
->trigger();
|
||||
->setType(MESSAGE_SEND_TYPE_EXTERNAL)
|
||||
->setMessageId($message->getId());
|
||||
break;
|
||||
case MessageStatus::SCHEDULED:
|
||||
$schedule = $dbForConsole->createDocument('schedules', new Document([
|
||||
@@ -2793,8 +2870,8 @@ App::post('/v1/messaging/messages/push')
|
||||
switch ($status) {
|
||||
case MessageStatus::PROCESSING:
|
||||
$queueForMessaging
|
||||
->setMessageId($message->getId())
|
||||
->trigger();
|
||||
->setType(MESSAGE_SEND_TYPE_EXTERNAL)
|
||||
->setMessageId($message->getId());
|
||||
break;
|
||||
case MessageStatus::SCHEDULED:
|
||||
$schedule = $dbForConsole->createDocument('schedules', new Document([
|
||||
@@ -2845,14 +2922,22 @@ App::get('/v1/messaging/messages')
|
||||
->inject('dbForProject')
|
||||
->inject('response')
|
||||
->action(function (array $queries, string $search, Database $dbForProject, Response $response) {
|
||||
$queries = Query::parseQueries($queries);
|
||||
try {
|
||||
$queries = Query::parseQueries($queries);
|
||||
} catch (QueryException $e) {
|
||||
throw new Exception(Exception::GENERAL_QUERY_INVALID, $e->getMessage());
|
||||
}
|
||||
|
||||
if (!empty($search)) {
|
||||
$queries[] = Query::search('search', $search);
|
||||
}
|
||||
|
||||
// Get cursor document if there was a cursor query
|
||||
$cursor = Query::getByType($queries, [Query::TYPE_CURSOR_AFTER, Query::TYPE_CURSOR_BEFORE]);
|
||||
/**
|
||||
* Get cursor document if there was a cursor query, we use array_filter and reset for reference $cursor to $queries
|
||||
*/
|
||||
$cursor = \array_filter($queries, function ($query) {
|
||||
return \in_array($query->getMethod(), [Query::TYPE_CURSOR_AFTER, Query::TYPE_CURSOR_BEFORE]);
|
||||
});
|
||||
$cursor = reset($cursor);
|
||||
|
||||
if ($cursor) {
|
||||
@@ -2896,7 +2981,12 @@ App::get('/v1/messaging/messages/:messageId/logs')
|
||||
throw new Exception(Exception::MESSAGE_NOT_FOUND);
|
||||
}
|
||||
|
||||
$queries = Query::parseQueries($queries);
|
||||
try {
|
||||
$queries = Query::parseQueries($queries);
|
||||
} catch (QueryException $e) {
|
||||
throw new Exception(Exception::GENERAL_QUERY_INVALID, $e->getMessage());
|
||||
}
|
||||
|
||||
$grouped = Query::groupByType($queries);
|
||||
$limit = $grouped['limit'] ?? APP_LIMIT_COUNT;
|
||||
$offset = $grouped['offset'] ?? 0;
|
||||
@@ -2971,9 +3061,7 @@ App::get('/v1/messaging/messages/:messageId/targets')
|
||||
->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(', ', Targets::ALLOWED_ATTRIBUTES), true)
|
||||
->inject('response')
|
||||
->inject('dbForProject')
|
||||
->inject('locale')
|
||||
->inject('geodb')
|
||||
->action(function (string $messageId, array $queries, Response $response, Database $dbForProject, Locale $locale, Reader $geodb) {
|
||||
->action(function (string $messageId, array $queries, Response $response, Database $dbForProject) {
|
||||
$message = $dbForProject->getDocument('messages', $messageId);
|
||||
|
||||
if ($message->isEmpty()) {
|
||||
@@ -2990,12 +3078,20 @@ App::get('/v1/messaging/messages/:messageId/targets')
|
||||
return;
|
||||
}
|
||||
|
||||
$queries = Query::parseQueries($queries);
|
||||
try {
|
||||
$queries = Query::parseQueries($queries);
|
||||
} catch (QueryException $e) {
|
||||
throw new Exception(Exception::GENERAL_QUERY_INVALID, $e->getMessage());
|
||||
}
|
||||
|
||||
$queries[] = Query::equal('$id', $targetIDs);
|
||||
|
||||
// Get cursor document if there was a cursor query
|
||||
$cursor = Query::getByType($queries, [Query::TYPE_CURSOR_AFTER, Query::TYPE_CURSOR_BEFORE]);
|
||||
/**
|
||||
* Get cursor document if there was a cursor query, we use array_filter and reset for reference $cursor to $queries
|
||||
*/
|
||||
$cursor = \array_filter($queries, function ($query) {
|
||||
return \in_array($query->getMethod(), [Query::TYPE_CURSOR_AFTER, Query::TYPE_CURSOR_BEFORE]);
|
||||
});
|
||||
$cursor = reset($cursor);
|
||||
|
||||
if ($cursor) {
|
||||
@@ -3077,8 +3173,13 @@ App::patch('/v1/messaging/messages/email/:messageId')
|
||||
throw new Exception(Exception::MESSAGE_NOT_FOUND);
|
||||
}
|
||||
|
||||
if ($message->getAttribute('status') === MessageStatus::SENT) {
|
||||
throw new Exception(Exception::MESSAGE_ALREADY_SENT);
|
||||
switch ($message->getAttribute('status')) {
|
||||
case MessageStatus::PROCESSING:
|
||||
throw new Exception(Exception::MESSAGE_ALREADY_PROCESSING);
|
||||
case MessageStatus::SENT:
|
||||
throw new Exception(Exception::MESSAGE_ALREADY_SENT);
|
||||
case MessageStatus::FAILED:
|
||||
throw new Exception(Exception::MESSAGE_ALREADY_FAILED);
|
||||
}
|
||||
|
||||
if (!\is_null($message->getAttribute('scheduledAt')) && $message->getAttribute('scheduledAt') < new \DateTime()) {
|
||||
@@ -3136,7 +3237,7 @@ App::patch('/v1/messaging/messages/email/:messageId')
|
||||
'resourceUpdatedAt' => DateTime::now(),
|
||||
'projectId' => $project->getId(),
|
||||
'schedule' => $scheduledAt,
|
||||
'active' => $status === 'processing',
|
||||
'active' => $status === MessageStatus::SCHEDULED,
|
||||
]));
|
||||
|
||||
$message->setAttribute('scheduleId', $schedule->getId());
|
||||
@@ -3150,7 +3251,7 @@ App::patch('/v1/messaging/messages/email/:messageId')
|
||||
$schedule
|
||||
->setAttribute('resourceUpdatedAt', DateTime::now())
|
||||
->setAttribute('schedule', $scheduledAt)
|
||||
->setAttribute('active', $status === 'processing');
|
||||
->setAttribute('active', $status === MessageStatus::SCHEDULED);
|
||||
|
||||
$dbForConsole->updateDocument('schedules', $schedule->getId(), $schedule);
|
||||
}
|
||||
@@ -3162,8 +3263,8 @@ App::patch('/v1/messaging/messages/email/:messageId')
|
||||
|
||||
if ($status === MessageStatus::PROCESSING) {
|
||||
$queueForMessaging
|
||||
->setMessageId($message->getId())
|
||||
->trigger();
|
||||
->setType(MESSAGE_SEND_TYPE_EXTERNAL)
|
||||
->setMessageId($message->getId());
|
||||
}
|
||||
|
||||
$queueForEvents
|
||||
@@ -3207,8 +3308,13 @@ App::patch('/v1/messaging/messages/sms/:messageId')
|
||||
throw new Exception(Exception::MESSAGE_NOT_FOUND);
|
||||
}
|
||||
|
||||
if ($message->getAttribute('status') === 'sent') {
|
||||
throw new Exception(Exception::MESSAGE_ALREADY_SENT);
|
||||
switch ($message->getAttribute('status')) {
|
||||
case MessageStatus::PROCESSING:
|
||||
throw new Exception(Exception::MESSAGE_ALREADY_PROCESSING);
|
||||
case MessageStatus::SENT:
|
||||
throw new Exception(Exception::MESSAGE_ALREADY_SENT);
|
||||
case MessageStatus::FAILED:
|
||||
throw new Exception(Exception::MESSAGE_ALREADY_FAILED);
|
||||
}
|
||||
|
||||
if (!is_null($message->getAttribute('scheduledAt')) && $message->getAttribute('scheduledAt') < new \DateTime()) {
|
||||
@@ -3250,7 +3356,7 @@ App::patch('/v1/messaging/messages/sms/:messageId')
|
||||
'resourceUpdatedAt' => DateTime::now(),
|
||||
'projectId' => $project->getId(),
|
||||
'schedule' => $scheduledAt,
|
||||
'active' => $status === 'processing',
|
||||
'active' => $status === MessageStatus::SCHEDULED,
|
||||
]));
|
||||
|
||||
$message->setAttribute('scheduleId', $schedule->getId());
|
||||
@@ -3264,7 +3370,7 @@ App::patch('/v1/messaging/messages/sms/:messageId')
|
||||
$schedule
|
||||
->setAttribute('resourceUpdatedAt', DateTime::now())
|
||||
->setAttribute('schedule', $scheduledAt)
|
||||
->setAttribute('active', $status === 'processing');
|
||||
->setAttribute('active', $status === MessageStatus::SCHEDULED);
|
||||
|
||||
$dbForConsole->updateDocument('schedules', $schedule->getId(), $schedule);
|
||||
}
|
||||
@@ -3274,10 +3380,10 @@ App::patch('/v1/messaging/messages/sms/:messageId')
|
||||
|
||||
$message = $dbForProject->updateDocument('messages', $message->getId(), $message);
|
||||
|
||||
if ($status === 'processing' && \is_null($message->getAttribute('scheduledAt'))) {
|
||||
if ($status === MessageStatus::PROCESSING) {
|
||||
$queueForMessaging
|
||||
->setMessageId($message->getId())
|
||||
->trigger();
|
||||
->setType(MESSAGE_SEND_TYPE_EXTERNAL)
|
||||
->setMessageId($message->getId());
|
||||
}
|
||||
|
||||
$queueForEvents
|
||||
@@ -3329,8 +3435,13 @@ App::patch('/v1/messaging/messages/push/:messageId')
|
||||
throw new Exception(Exception::MESSAGE_NOT_FOUND);
|
||||
}
|
||||
|
||||
if ($message->getAttribute('status') === 'sent') {
|
||||
throw new Exception(Exception::MESSAGE_ALREADY_SENT);
|
||||
switch ($message->getAttribute('status')) {
|
||||
case MessageStatus::PROCESSING:
|
||||
throw new Exception(Exception::MESSAGE_ALREADY_PROCESSING);
|
||||
case MessageStatus::SENT:
|
||||
throw new Exception(Exception::MESSAGE_ALREADY_SENT);
|
||||
case MessageStatus::FAILED:
|
||||
throw new Exception(Exception::MESSAGE_ALREADY_FAILED);
|
||||
}
|
||||
|
||||
if (!is_null($message->getAttribute('scheduledAt')) && $message->getAttribute('scheduledAt') < new \DateTime()) {
|
||||
@@ -3404,7 +3515,7 @@ App::patch('/v1/messaging/messages/push/:messageId')
|
||||
'resourceUpdatedAt' => DateTime::now(),
|
||||
'projectId' => $project->getId(),
|
||||
'schedule' => $scheduledAt,
|
||||
'active' => $status === 'processing',
|
||||
'active' => $status === MessageStatus::SCHEDULED,
|
||||
]));
|
||||
|
||||
$message->setAttribute('scheduleId', $schedule->getId());
|
||||
@@ -3418,7 +3529,7 @@ App::patch('/v1/messaging/messages/push/:messageId')
|
||||
$schedule
|
||||
->setAttribute('resourceUpdatedAt', DateTime::now())
|
||||
->setAttribute('schedule', $scheduledAt)
|
||||
->setAttribute('active', $status === 'processing');
|
||||
->setAttribute('active', $status === MessageStatus::SCHEDULED);
|
||||
|
||||
$dbForConsole->updateDocument('schedules', $schedule->getId(), $schedule);
|
||||
}
|
||||
@@ -3428,10 +3539,10 @@ App::patch('/v1/messaging/messages/push/:messageId')
|
||||
|
||||
$message = $dbForProject->updateDocument('messages', $message->getId(), $message);
|
||||
|
||||
if ($status === 'processing' && \is_null($message->getAttribute('scheduledAt'))) {
|
||||
if ($status === MessageStatus::PROCESSING) {
|
||||
$queueForMessaging
|
||||
->setMessageId($message->getId())
|
||||
->trigger();
|
||||
->setType(MESSAGE_SEND_TYPE_EXTERNAL)
|
||||
->setMessageId($message->getId());
|
||||
}
|
||||
|
||||
$queueForEvents
|
||||
|
||||
@@ -14,6 +14,7 @@ use Utopia\App;
|
||||
use Utopia\Database\Database;
|
||||
use Utopia\Database\DateTime;
|
||||
use Utopia\Database\Document;
|
||||
use Utopia\Database\Exception\Query as QueryException;
|
||||
use Utopia\Database\Helpers\ID;
|
||||
use Utopia\Database\Query;
|
||||
use Utopia\Database\Validator\UID;
|
||||
@@ -384,13 +385,19 @@ App::get('/v1/migrations')
|
||||
->inject('response')
|
||||
->inject('dbForProject')
|
||||
->action(function (array $queries, string $search, Response $response, Database $dbForProject) {
|
||||
$queries = Query::parseQueries($queries);
|
||||
try {
|
||||
$queries = Query::parseQueries($queries);
|
||||
} catch (QueryException $e) {
|
||||
throw new Exception(Exception::GENERAL_QUERY_INVALID, $e->getMessage());
|
||||
}
|
||||
|
||||
if (!empty($search)) {
|
||||
$queries[] = Query::search('search', $search);
|
||||
}
|
||||
|
||||
// Get cursor document if there was a cursor query
|
||||
/**
|
||||
* Get cursor document if there was a cursor query, we use array_filter and reset for reference $cursor to $queries
|
||||
*/
|
||||
$cursor = \array_filter($queries, function ($query) {
|
||||
return \in_array($query->getMethod(), [Query::TYPE_CURSOR_AFTER, Query::TYPE_CURSOR_BEFORE]);
|
||||
});
|
||||
@@ -610,7 +617,7 @@ App::get('/v1/migrations/firebase/report/oauth')
|
||||
|
||||
try {
|
||||
$report = $firebase->report($resources);
|
||||
} catch (\Exception $e) {
|
||||
} catch (\Throwable $e) {
|
||||
throw new Exception(Exception::GENERAL_SERVER_ERROR, 'Source Error: ' . $e->getMessage());
|
||||
}
|
||||
|
||||
@@ -822,7 +829,7 @@ App::get('/v1/migrations/firebase/projects')
|
||||
if ($isExpired) {
|
||||
try {
|
||||
$firebase->refreshTokens($refreshToken);
|
||||
} catch (\Exception $e) {
|
||||
} catch (\Throwable $e) {
|
||||
throw new Exception(Exception::USER_IDENTITY_NOT_FOUND);
|
||||
}
|
||||
|
||||
@@ -852,7 +859,7 @@ App::get('/v1/migrations/firebase/projects')
|
||||
'projectId' => $project['projectId'],
|
||||
];
|
||||
}
|
||||
} catch (\Exception $e) {
|
||||
} catch (\Throwable $e) {
|
||||
throw new Exception(Exception::USER_IDENTITY_NOT_FOUND);
|
||||
}
|
||||
|
||||
|
||||
@@ -73,7 +73,7 @@ App::get('/v1/project/usage')
|
||||
|
||||
Authorization::skip(function () use ($dbForProject, $firstDay, $lastDay, $period, $metrics, &$total, &$stats) {
|
||||
foreach ($metrics['total'] as $metric) {
|
||||
$result = $dbForProject->findOne('stats_v2', [
|
||||
$result = $dbForProject->findOne('stats', [
|
||||
Query::equal('metric', [$metric]),
|
||||
Query::equal('period', ['inf'])
|
||||
]);
|
||||
@@ -81,7 +81,7 @@ App::get('/v1/project/usage')
|
||||
}
|
||||
|
||||
foreach ($metrics['period'] as $metric) {
|
||||
$results = $dbForProject->find('stats_v2', [
|
||||
$results = $dbForProject->find('stats', [
|
||||
Query::equal('metric', [$metric]),
|
||||
Query::equal('period', [$period]),
|
||||
Query::greaterThanEqual('time', $firstDay),
|
||||
@@ -116,7 +116,7 @@ App::get('/v1/project/usage')
|
||||
$id = $function->getId();
|
||||
$name = $function->getAttribute('name');
|
||||
$metric = str_replace('{functionInternalId}', $function->getInternalId(), METRIC_FUNCTION_ID_EXECUTIONS);
|
||||
$value = $dbForProject->findOne('stats_v2', [
|
||||
$value = $dbForProject->findOne('stats', [
|
||||
Query::equal('metric', [$metric]),
|
||||
Query::equal('period', ['inf'])
|
||||
]);
|
||||
@@ -132,7 +132,7 @@ App::get('/v1/project/usage')
|
||||
$id = $bucket->getId();
|
||||
$name = $bucket->getAttribute('name');
|
||||
$metric = str_replace('{bucketInternalId}', $bucket->getInternalId(), METRIC_BUCKET_ID_FILES_STORAGE);
|
||||
$value = $dbForProject->findOne('stats_v2', [
|
||||
$value = $dbForProject->findOne('stats', [
|
||||
Query::equal('metric', [$metric]),
|
||||
Query::equal('period', ['inf'])
|
||||
]);
|
||||
|
||||
@@ -18,20 +18,18 @@ use Utopia\Audit\Audit;
|
||||
use Utopia\Cache\Cache;
|
||||
use Utopia\Config\Config;
|
||||
use Utopia\Database\Database;
|
||||
use Utopia\Database\DateTime;
|
||||
use Utopia\Database\Document;
|
||||
use Utopia\Database\Exception\Duplicate;
|
||||
use Utopia\Database\Exception\Query as QueryException;
|
||||
use Utopia\Database\Helpers\ID;
|
||||
use Utopia\Database\Helpers\Permission;
|
||||
use Utopia\Database\Helpers\Role;
|
||||
use Utopia\Database\Query;
|
||||
use Utopia\Database\Validator\Authorization;
|
||||
use Utopia\Database\Validator\Datetime as DatetimeValidator;
|
||||
use Utopia\Database\Validator\UID;
|
||||
use Utopia\Domains\Validator\PublicDomain;
|
||||
use Utopia\Locale\Locale;
|
||||
use Utopia\Pools\Group;
|
||||
use Utopia\Registry\Registry;
|
||||
use Utopia\Validator\ArrayList;
|
||||
use Utopia\Validator\Boolean;
|
||||
use Utopia\Validator\Hostname;
|
||||
@@ -80,13 +78,18 @@ App::post('/v1/projects')
|
||||
->inject('pools')
|
||||
->action(function (string $projectId, string $name, string $teamId, string $region, string $description, string $logo, string $url, string $legalName, string $legalCountry, string $legalState, string $legalCity, string $legalAddress, string $legalTaxId, Response $response, Database $dbForConsole, Cache $cache, Group $pools) {
|
||||
|
||||
|
||||
$team = $dbForConsole->getDocument('teams', $teamId);
|
||||
|
||||
if ($team->isEmpty()) {
|
||||
throw new Exception(Exception::TEAM_NOT_FOUND);
|
||||
}
|
||||
|
||||
$allowList = \array_filter(\explode(',', App::getEnv('_APP_PROJECT_REGIONS', '')));
|
||||
|
||||
if (!empty($allowList) && !\in_array($region, $allowList)) {
|
||||
throw new Exception(Exception::PROJECT_REGION_UNSUPPORTED, 'Region "' . $region . '" is not supported');
|
||||
}
|
||||
|
||||
$auth = Config::getParam('auth', []);
|
||||
$auths = ['limit' => 0, 'maxSessions' => APP_LIMIT_USER_SESSIONS_DEFAULT, 'passwordHistory' => 0, 'passwordDictionary' => false, 'duration' => Auth::TOKEN_EXPIRATION_LOGIN_LONG, 'personalDataCheck' => false];
|
||||
foreach ($auth as $index => $method) {
|
||||
@@ -241,13 +244,19 @@ App::get('/v1/projects')
|
||||
->inject('dbForConsole')
|
||||
->action(function (array $queries, string $search, Response $response, Database $dbForConsole) {
|
||||
|
||||
$queries = Query::parseQueries($queries);
|
||||
try {
|
||||
$queries = Query::parseQueries($queries);
|
||||
} catch (QueryException $e) {
|
||||
throw new Exception(Exception::GENERAL_QUERY_INVALID, $e->getMessage());
|
||||
}
|
||||
|
||||
if (!empty($search)) {
|
||||
$queries[] = Query::search('search', $search);
|
||||
}
|
||||
|
||||
// Get cursor document if there was a cursor query
|
||||
/**
|
||||
* Get cursor document if there was a cursor query, we use array_filter and reset for reference $cursor to $queries
|
||||
*/
|
||||
$cursor = \array_filter($queries, function ($query) {
|
||||
return \in_array($query->getMethod(), [Query::TYPE_CURSOR_AFTER, Query::TYPE_CURSOR_BEFORE]);
|
||||
});
|
||||
|
||||
@@ -10,10 +10,12 @@ use Appwrite\Utopia\Response;
|
||||
use Utopia\App;
|
||||
use Utopia\Database\Database;
|
||||
use Utopia\Database\Document;
|
||||
use Utopia\Database\Exception\Query as QueryException;
|
||||
use Utopia\Database\Helpers\ID;
|
||||
use Utopia\Database\Query;
|
||||
use Utopia\Database\Validator\UID;
|
||||
use Utopia\Domains\Domain;
|
||||
use Utopia\Logger\Log;
|
||||
use Utopia\Validator\Domain as ValidatorDomain;
|
||||
use Utopia\Validator\Text;
|
||||
use Utopia\Validator\WhiteList;
|
||||
@@ -89,7 +91,7 @@ App::post('/v1/proxy/rules')
|
||||
|
||||
try {
|
||||
$domain = new Domain($domain);
|
||||
} catch (\Exception) {
|
||||
} catch (\Throwable) {
|
||||
throw new Exception(Exception::GENERAL_ARGUMENT_INVALID, 'Domain may not start with http:// or https://.');
|
||||
}
|
||||
|
||||
@@ -155,7 +157,11 @@ App::get('/v1/proxy/rules')
|
||||
->inject('project')
|
||||
->inject('dbForConsole')
|
||||
->action(function (array $queries, string $search, Response $response, Document $project, Database $dbForConsole) {
|
||||
$queries = Query::parseQueries($queries);
|
||||
try {
|
||||
$queries = Query::parseQueries($queries);
|
||||
} catch (QueryException $e) {
|
||||
throw new Exception(Exception::GENERAL_QUERY_INVALID, $e->getMessage());
|
||||
}
|
||||
|
||||
if (!empty($search)) {
|
||||
$queries[] = Query::search('search', $search);
|
||||
@@ -163,8 +169,12 @@ App::get('/v1/proxy/rules')
|
||||
|
||||
$queries[] = Query::equal('projectInternalId', [$project->getInternalId()]);
|
||||
|
||||
// Get cursor document if there was a cursor query
|
||||
$cursor = Query::getByType($queries, [Query::TYPE_CURSOR_AFTER, Query::TYPE_CURSOR_BEFORE]);
|
||||
/**
|
||||
* Get cursor document if there was a cursor query, we use array_filter and reset for reference $cursor to $queries
|
||||
*/
|
||||
$cursor = \array_filter($queries, function ($query) {
|
||||
return \in_array($query->getMethod(), [Query::TYPE_CURSOR_AFTER, Query::TYPE_CURSOR_BEFORE]);
|
||||
});
|
||||
$cursor = reset($cursor);
|
||||
if ($cursor) {
|
||||
/** @var Query $cursor */
|
||||
@@ -278,7 +288,8 @@ App::patch('/v1/proxy/rules/:ruleId/verification')
|
||||
->inject('queueForEvents')
|
||||
->inject('project')
|
||||
->inject('dbForConsole')
|
||||
->action(function (string $ruleId, Response $response, Certificate $queueForCertificates, Event $queueForEvents, Document $project, Database $dbForConsole) {
|
||||
->inject('log')
|
||||
->action(function (string $ruleId, Response $response, Certificate $queueForCertificates, Event $queueForEvents, Document $project, Database $dbForConsole, Log $log) {
|
||||
$rule = $dbForConsole->getDocument('rules', $ruleId);
|
||||
|
||||
if ($rule->isEmpty() || $rule->getAttribute('projectInternalId') !== $project->getInternalId()) {
|
||||
@@ -298,7 +309,14 @@ App::patch('/v1/proxy/rules/:ruleId/verification')
|
||||
$validator = new CNAME($target->get()); // Verify Domain with DNS records
|
||||
$domain = new Domain($rule->getAttribute('domain', ''));
|
||||
|
||||
$validationStart = \microtime(true);
|
||||
if (!$validator->isValid($domain->get())) {
|
||||
$log->addExtra('dnsTiming', \strval(\microtime(true) - $validationStart));
|
||||
$log->addTag('dnsDomain', $domain->get());
|
||||
|
||||
$error = $validator->getLogs();
|
||||
$log->addExtra('dnsResponse', \is_array($error) ? \json_encode($error) : \strval($error));
|
||||
|
||||
throw new Exception(Exception::RULE_VERIFICATION_FAILED);
|
||||
}
|
||||
|
||||
|
||||
@@ -12,10 +12,10 @@ use Utopia\App;
|
||||
use Utopia\Config\Config;
|
||||
use Utopia\Database\Database;
|
||||
use Utopia\Database\Document;
|
||||
use Utopia\Database\DateTime;
|
||||
use Utopia\Database\Exception\Duplicate;
|
||||
use Utopia\Database\Exception\Authorization as AuthorizationException;
|
||||
use Utopia\Database\Exception\Duplicate as DuplicateException;
|
||||
use Utopia\Database\Exception\Query as QueryException;
|
||||
use Utopia\Database\Exception\Structure as StructureException;
|
||||
use Utopia\Database\Helpers\ID;
|
||||
use Utopia\Database\Helpers\Permission;
|
||||
@@ -42,7 +42,6 @@ use Utopia\Validator\HexColor;
|
||||
use Utopia\Validator\Range;
|
||||
use Utopia\Validator\Text;
|
||||
use Utopia\Validator\WhiteList;
|
||||
use Utopia\DSN\DSN;
|
||||
use Utopia\Swoole\Request;
|
||||
use Utopia\Storage\Compression\Compression;
|
||||
|
||||
@@ -161,13 +160,19 @@ App::get('/v1/storage/buckets')
|
||||
->inject('dbForProject')
|
||||
->action(function (array $queries, string $search, Response $response, Database $dbForProject) {
|
||||
|
||||
$queries = Query::parseQueries($queries);
|
||||
try {
|
||||
$queries = Query::parseQueries($queries);
|
||||
} catch (QueryException $e) {
|
||||
throw new Exception(Exception::GENERAL_QUERY_INVALID, $e->getMessage());
|
||||
}
|
||||
|
||||
if (!empty($search)) {
|
||||
$queries[] = Query::search('search', $search);
|
||||
}
|
||||
|
||||
// Get cursor document if there was a cursor query
|
||||
/**
|
||||
* Get cursor document if there was a cursor query, we use array_filter and reset for reference $cursor to $queries
|
||||
*/
|
||||
$cursor = \array_filter($queries, function ($query) {
|
||||
return \in_array($query->getMethod(), [Query::TYPE_CURSOR_AFTER, Query::TYPE_CURSOR_BEFORE]);
|
||||
});
|
||||
@@ -737,13 +742,19 @@ App::get('/v1/storage/buckets/:bucketId/files')
|
||||
throw new Exception(Exception::USER_UNAUTHORIZED);
|
||||
}
|
||||
|
||||
$queries = Query::parseQueries($queries);
|
||||
try {
|
||||
$queries = Query::parseQueries($queries);
|
||||
} catch (QueryException $e) {
|
||||
throw new Exception(Exception::GENERAL_QUERY_INVALID, $e->getMessage());
|
||||
}
|
||||
|
||||
if (!empty($search)) {
|
||||
$queries[] = Query::search('search', $search);
|
||||
}
|
||||
|
||||
// Get cursor document if there was a cursor query
|
||||
/**
|
||||
* Get cursor document if there was a cursor query, we use array_filter and reset for reference $cursor to $queries
|
||||
*/
|
||||
$cursor = \array_filter($queries, function ($query) {
|
||||
return \in_array($query->getMethod(), [Query::TYPE_CURSOR_AFTER, Query::TYPE_CURSOR_BEFORE]);
|
||||
});
|
||||
@@ -1528,7 +1539,7 @@ App::get('/v1/storage/usage')
|
||||
$total = [];
|
||||
Authorization::skip(function () use ($dbForProject, $days, $metrics, &$stats, &$total) {
|
||||
foreach ($metrics as $metric) {
|
||||
$result = $dbForProject->findOne('stats_v2', [
|
||||
$result = $dbForProject->findOne('stats', [
|
||||
Query::equal('metric', [$metric]),
|
||||
Query::equal('period', ['inf'])
|
||||
]);
|
||||
@@ -1536,7 +1547,7 @@ App::get('/v1/storage/usage')
|
||||
$stats[$metric]['total'] = $result['value'] ?? 0;
|
||||
$limit = $days['limit'];
|
||||
$period = $days['period'];
|
||||
$results = $dbForProject->find('stats_v2', [
|
||||
$results = $dbForProject->find('stats', [
|
||||
Query::equal('metric', [$metric]),
|
||||
Query::equal('period', [$period]),
|
||||
Query::limit($limit),
|
||||
@@ -1613,7 +1624,7 @@ App::get('/v1/storage/:bucketId/usage')
|
||||
|
||||
Authorization::skip(function () use ($dbForProject, $days, $metrics, &$stats, &$total) {
|
||||
foreach ($metrics as $metric) {
|
||||
$result = $dbForProject->findOne('stats_v2', [
|
||||
$result = $dbForProject->findOne('stats', [
|
||||
Query::equal('metric', [$metric]),
|
||||
Query::equal('period', ['inf'])
|
||||
]);
|
||||
@@ -1621,7 +1632,7 @@ App::get('/v1/storage/:bucketId/usage')
|
||||
$stats[$metric]['total'] = $result['value'] ?? 0;
|
||||
$limit = $days['limit'];
|
||||
$period = $days['period'];
|
||||
$results = $dbForProject->find('stats_v2', [
|
||||
$results = $dbForProject->find('stats', [
|
||||
Query::equal('metric', [$metric]),
|
||||
Query::equal('period', [$period]),
|
||||
Query::limit($limit),
|
||||
|
||||
@@ -9,6 +9,7 @@ use Appwrite\Event\Mail;
|
||||
use Appwrite\Event\Messaging;
|
||||
use Appwrite\Extend\Exception;
|
||||
use Appwrite\Network\Validator\Email;
|
||||
use Utopia\Database\Exception\Query as QueryException;
|
||||
use Utopia\Validator\Host;
|
||||
use Appwrite\Template\Template;
|
||||
use Appwrite\Utopia\Database\Validator\CustomId;
|
||||
@@ -146,13 +147,19 @@ App::get('/v1/teams')
|
||||
->inject('dbForProject')
|
||||
->action(function (array $queries, string $search, Response $response, Database $dbForProject) {
|
||||
|
||||
$queries = Query::parseQueries($queries);
|
||||
try {
|
||||
$queries = Query::parseQueries($queries);
|
||||
} catch (QueryException $e) {
|
||||
throw new Exception(Exception::GENERAL_QUERY_INVALID, $e->getMessage());
|
||||
}
|
||||
|
||||
if (!empty($search)) {
|
||||
$queries[] = Query::search('search', $search);
|
||||
}
|
||||
|
||||
// Get cursor document if there was a cursor query
|
||||
/**
|
||||
* Get cursor document if there was a cursor query, we use array_filter and reset for reference $cursor to $queries
|
||||
*/
|
||||
$cursor = \array_filter($queries, function ($query) {
|
||||
return \in_array($query->getMethod(), [Query::TYPE_CURSOR_AFTER, Query::TYPE_CURSOR_BEFORE]);
|
||||
});
|
||||
@@ -534,8 +541,8 @@ App::post('/v1/teams/:teamId/memberships')
|
||||
} catch (Duplicate $th) {
|
||||
throw new Exception(Exception::TEAM_INVITE_ALREADY_EXISTS);
|
||||
}
|
||||
$team->setAttribute('total', $team->getAttribute('total', 0) + 1);
|
||||
$team = Authorization::skip(fn() => $dbForProject->updateDocument('teams', $team->getId(), $team));
|
||||
|
||||
Authorization::skip(fn() => $dbForProject->increaseDocumentAttribute('teams', $team->getId(), 'total', 1));
|
||||
|
||||
$dbForProject->purgeCachedDocument('users', $invitee->getId());
|
||||
} else {
|
||||
@@ -651,10 +658,10 @@ App::post('/v1/teams/:teamId/memberships')
|
||||
]);
|
||||
|
||||
$queueForMessaging
|
||||
->setType(MESSAGE_SEND_TYPE_INTERNAL)
|
||||
->setMessage($messageDoc)
|
||||
->setRecipients([$phone])
|
||||
->setProviderType('SMS')
|
||||
->trigger();
|
||||
->setProviderType('SMS');
|
||||
}
|
||||
}
|
||||
|
||||
@@ -699,7 +706,11 @@ App::get('/v1/teams/:teamId/memberships')
|
||||
throw new Exception(Exception::TEAM_NOT_FOUND);
|
||||
}
|
||||
|
||||
$queries = Query::parseQueries($queries);
|
||||
try {
|
||||
$queries = Query::parseQueries($queries);
|
||||
} catch (QueryException $e) {
|
||||
throw new Exception(Exception::GENERAL_QUERY_INVALID, $e->getMessage());
|
||||
}
|
||||
|
||||
if (!empty($search)) {
|
||||
$queries[] = Query::search('search', $search);
|
||||
@@ -708,7 +719,9 @@ App::get('/v1/teams/:teamId/memberships')
|
||||
// Set internal queries
|
||||
$queries[] = Query::equal('teamId', [$teamId]);
|
||||
|
||||
// Get cursor document if there was a cursor query
|
||||
/**
|
||||
* Get cursor document if there was a cursor query, we use array_filter and reset for reference $cursor to $queries
|
||||
*/
|
||||
$cursor = \array_filter($queries, function ($query) {
|
||||
return \in_array($query->getMethod(), [Query::TYPE_CURSOR_AFTER, Query::TYPE_CURSOR_BEFORE]);
|
||||
});
|
||||
@@ -1055,7 +1068,7 @@ App::delete('/v1/teams/:teamId/memberships/:membershipId')
|
||||
$dbForProject->deleteDocument('memberships', $membership->getId());
|
||||
} catch (AuthorizationException $exception) {
|
||||
throw new Exception(Exception::USER_UNAUTHORIZED);
|
||||
} catch (\Exception $exception) {
|
||||
} catch (\Throwable $exception) {
|
||||
throw new Exception(Exception::GENERAL_SERVER_ERROR, 'Failed to remove membership from DB');
|
||||
}
|
||||
|
||||
@@ -1100,7 +1113,12 @@ App::get('/v1/teams/:teamId/logs')
|
||||
throw new Exception(Exception::TEAM_NOT_FOUND);
|
||||
}
|
||||
|
||||
$queries = Query::parseQueries($queries);
|
||||
try {
|
||||
$queries = Query::parseQueries($queries);
|
||||
} catch (QueryException $e) {
|
||||
throw new Exception(Exception::GENERAL_QUERY_INVALID, $e->getMessage());
|
||||
}
|
||||
|
||||
$grouped = Query::groupByType($queries);
|
||||
$limit = $grouped['limit'] ?? APP_LIMIT_COUNT;
|
||||
$offset = $grouped['offset'] ?? 0;
|
||||
|
||||
@@ -11,6 +11,7 @@ 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\Exception\Query as QueryException;
|
||||
use Utopia\Database\Validator\Queries;
|
||||
use Appwrite\Utopia\Database\Validator\Queries\Users;
|
||||
use Utopia\Database\Validator\Query\Limit;
|
||||
@@ -114,6 +115,11 @@ function createUser(string $hash, mixed $hashOptions, string $userId, ?string $e
|
||||
if ($email) {
|
||||
try {
|
||||
$target = $dbForProject->createDocument('targets', new Document([
|
||||
'$permissions' => [
|
||||
Permission::read(Role::user($user->getId())),
|
||||
Permission::update(Role::user($user->getId())),
|
||||
Permission::delete(Role::user($user->getId())),
|
||||
],
|
||||
'userId' => $user->getId(),
|
||||
'userInternalId' => $user->getInternalId(),
|
||||
'providerType' => 'email',
|
||||
@@ -131,6 +137,11 @@ function createUser(string $hash, mixed $hashOptions, string $userId, ?string $e
|
||||
if ($phone) {
|
||||
try {
|
||||
$target = $dbForProject->createDocument('targets', new Document([
|
||||
'$permissions' => [
|
||||
Permission::read(Role::user($user->getId())),
|
||||
Permission::update(Role::user($user->getId())),
|
||||
Permission::delete(Role::user($user->getId())),
|
||||
],
|
||||
'userId' => $user->getId(),
|
||||
'userInternalId' => $user->getInternalId(),
|
||||
'providerType' => 'sms',
|
||||
@@ -497,6 +508,11 @@ App::post('/v1/users/:userId/targets')
|
||||
try {
|
||||
$target = $dbForProject->createDocument('targets', new Document([
|
||||
'$id' => $targetId,
|
||||
'$permissions' => [
|
||||
Permission::read(Role::user($user->getId())),
|
||||
Permission::update(Role::user($user->getId())),
|
||||
Permission::delete(Role::user($user->getId())),
|
||||
],
|
||||
'providerId' => $providerId ?? null,
|
||||
'providerInternalId' => $provider->getInternalId() ?? null,
|
||||
'providerType' => $providerType,
|
||||
@@ -536,13 +552,19 @@ App::get('/v1/users')
|
||||
->inject('dbForProject')
|
||||
->action(function (array $queries, string $search, Response $response, Database $dbForProject) {
|
||||
|
||||
$queries = Query::parseQueries($queries);
|
||||
try {
|
||||
$queries = Query::parseQueries($queries);
|
||||
} catch (QueryException $e) {
|
||||
throw new Exception(Exception::GENERAL_QUERY_INVALID, $e->getMessage());
|
||||
}
|
||||
|
||||
if (!empty($search)) {
|
||||
$queries[] = Query::search('search', $search);
|
||||
}
|
||||
|
||||
// Get cursor document if there was a cursor query
|
||||
/**
|
||||
* Get cursor document if there was a cursor query, we use array_filter and reset for reference $cursor to $queries
|
||||
*/
|
||||
$cursor = \array_filter($queries, function ($query) {
|
||||
return \in_array($query->getMethod(), [Query::TYPE_CURSOR_AFTER, Query::TYPE_CURSOR_BEFORE]);
|
||||
});
|
||||
@@ -756,7 +778,12 @@ App::get('/v1/users/:userId/logs')
|
||||
throw new Exception(Exception::USER_NOT_FOUND);
|
||||
}
|
||||
|
||||
$queries = Query::parseQueries($queries);
|
||||
try {
|
||||
$queries = Query::parseQueries($queries);
|
||||
} catch (QueryException $e) {
|
||||
throw new Exception(Exception::GENERAL_QUERY_INVALID, $e->getMessage());
|
||||
}
|
||||
|
||||
$grouped = Query::groupByType($queries);
|
||||
$limit = $grouped['limit'] ?? APP_LIMIT_COUNT;
|
||||
$offset = $grouped['offset'] ?? 0;
|
||||
@@ -834,13 +861,21 @@ App::get('/v1/users/:userId/targets')
|
||||
throw new Exception(Exception::USER_NOT_FOUND);
|
||||
}
|
||||
|
||||
$queries = Query::parseQueries($queries);
|
||||
try {
|
||||
$queries = Query::parseQueries($queries);
|
||||
} catch (QueryException $e) {
|
||||
throw new Exception(Exception::GENERAL_QUERY_INVALID, $e->getMessage());
|
||||
}
|
||||
|
||||
$queries[] = Query::equal('userId', [$userId]);
|
||||
|
||||
// Get cursor document if there was a cursor query
|
||||
$cursor = Query::getByType($queries, [Query::TYPE_CURSOR_AFTER, Query::TYPE_CURSOR_BEFORE]);
|
||||
$cursor = reset($cursor);
|
||||
/**
|
||||
* Get cursor document if there was a cursor query, we use array_filter and reset for reference $cursor to $queries
|
||||
*/
|
||||
$cursor = \array_filter($queries, function ($query) {
|
||||
return \in_array($query->getMethod(), [Query::TYPE_CURSOR_AFTER, Query::TYPE_CURSOR_BEFORE]);
|
||||
});
|
||||
$cursor = reset($cursor);
|
||||
|
||||
if ($cursor) {
|
||||
$targetId = $cursor->getValue();
|
||||
@@ -876,13 +911,19 @@ App::get('/v1/users/identities')
|
||||
->inject('dbForProject')
|
||||
->action(function (array $queries, string $search, Response $response, Database $dbForProject) {
|
||||
|
||||
$queries = Query::parseQueries($queries);
|
||||
try {
|
||||
$queries = Query::parseQueries($queries);
|
||||
} catch (QueryException $e) {
|
||||
throw new Exception(Exception::GENERAL_QUERY_INVALID, $e->getMessage());
|
||||
}
|
||||
|
||||
if (!empty($search)) {
|
||||
$queries[] = Query::search('search', $search);
|
||||
}
|
||||
|
||||
// Get cursor document if there was a cursor query
|
||||
/**
|
||||
* Get cursor document if there was a cursor query, we use array_filter and reset for reference $cursor to $queries
|
||||
*/
|
||||
$cursor = \array_filter($queries, function ($query) {
|
||||
return \in_array($query->getMethod(), [Query::TYPE_CURSOR_AFTER, Query::TYPE_CURSOR_BEFORE]);
|
||||
});
|
||||
@@ -1201,6 +1242,11 @@ App::patch('/v1/users/:userId/email')
|
||||
} else {
|
||||
if (\strlen($email) !== 0) {
|
||||
$target = $dbForProject->createDocument('targets', new Document([
|
||||
'$permissions' => [
|
||||
Permission::read(Role::user($user->getId())),
|
||||
Permission::update(Role::user($user->getId())),
|
||||
Permission::delete(Role::user($user->getId())),
|
||||
],
|
||||
'userId' => $user->getId(),
|
||||
'userInternalId' => $user->getInternalId(),
|
||||
'providerType' => 'email',
|
||||
@@ -1279,6 +1325,11 @@ App::patch('/v1/users/:userId/phone')
|
||||
} else {
|
||||
if (\strlen($number) !== 0) {
|
||||
$target = $dbForProject->createDocument('targets', new Document([
|
||||
'$permissions' => [
|
||||
Permission::read(Role::user($user->getId())),
|
||||
Permission::update(Role::user($user->getId())),
|
||||
Permission::delete(Role::user($user->getId())),
|
||||
],
|
||||
'userId' => $user->getId(),
|
||||
'userInternalId' => $user->getInternalId(),
|
||||
'providerType' => 'sms',
|
||||
@@ -1497,18 +1548,18 @@ App::patch('/v1/users/:userId/mfa')
|
||||
$response->dynamic($user, Response::MODEL_USER);
|
||||
});
|
||||
|
||||
App::get('/v1/users/:userId/providers')
|
||||
->desc('List Providers')
|
||||
App::get('/v1/users/:userId/mfa/factors')
|
||||
->desc('List Factors')
|
||||
->groups(['api', 'users'])
|
||||
->label('scope', 'users.read')
|
||||
->label('usage.metric', 'users.{scope}.requests.read')
|
||||
->label('sdk.auth', [APP_AUTH_TYPE_KEY])
|
||||
->label('sdk.namespace', 'users')
|
||||
->label('sdk.method', 'listProviders')
|
||||
->label('sdk.description', '/docs/references/users/list-providers.md')
|
||||
->label('sdk.method', 'listFactors')
|
||||
->label('sdk.description', '/docs/references/users/list-factors.md')
|
||||
->label('sdk.response.code', Response::STATUS_CODE_OK)
|
||||
->label('sdk.response.type', Response::CONTENT_TYPE_JSON)
|
||||
->label('sdk.response.model', Response::MODEL_MFA_PROVIDERS)
|
||||
->label('sdk.response.model', Response::MODEL_MFA_FACTORS)
|
||||
->param('userId', '', new UID(), 'User ID.')
|
||||
->inject('response')
|
||||
->inject('dbForProject')
|
||||
@@ -1525,10 +1576,10 @@ App::get('/v1/users/:userId/providers')
|
||||
'phone' => $user->getAttribute('phone', false) && $user->getAttribute('phoneVerification', false)
|
||||
]);
|
||||
|
||||
$response->dynamic($providers, Response::MODEL_MFA_PROVIDERS);
|
||||
$response->dynamic($providers, Response::MODEL_MFA_FACTORS);
|
||||
});
|
||||
|
||||
App::delete('/v1/users/:userId/mfa/:provider')
|
||||
App::delete('/v1/users/:userId/mfa/:type')
|
||||
->desc('Delete Authenticator')
|
||||
->groups(['api', 'users'])
|
||||
->label('event', 'users.[userId].delete.mfa')
|
||||
@@ -1545,20 +1596,20 @@ App::delete('/v1/users/:userId/mfa/:provider')
|
||||
->label('sdk.response.type', Response::CONTENT_TYPE_JSON)
|
||||
->label('sdk.response.model', Response::MODEL_USER)
|
||||
->param('userId', '', new UID(), 'User ID.')
|
||||
->param('provider', null, new WhiteList(['totp']), 'Provider.')
|
||||
->param('type', null, new WhiteList(['totp']), 'Type of authenticator.')
|
||||
->param('otp', '', new Text(256), 'Valid verification token.')
|
||||
->inject('requestTimestamp')
|
||||
->inject('response')
|
||||
->inject('dbForProject')
|
||||
->inject('queueForEvents')
|
||||
->action(function (string $userId, string $provider, string $otp, ?\DateTime $requestTimestamp, Response $response, Database $dbForProject, Event $queueForEvents) {
|
||||
->action(function (string $userId, string $type, string $otp, ?\DateTime $requestTimestamp, Response $response, Database $dbForProject, Event $queueForEvents) {
|
||||
$user = $dbForProject->getDocument('users', $userId);
|
||||
|
||||
if ($user->isEmpty()) {
|
||||
throw new Exception(Exception::USER_NOT_FOUND);
|
||||
}
|
||||
|
||||
$success = match ($provider) {
|
||||
$success = match ($type) {
|
||||
'totp' => Challenge\TOTP::verify($user, $otp),
|
||||
default => false
|
||||
};
|
||||
@@ -1950,7 +2001,7 @@ App::get('/v1/users/usage')
|
||||
|
||||
Authorization::skip(function () use ($dbForProject, $days, $metrics, &$stats) {
|
||||
foreach ($metrics as $count => $metric) {
|
||||
$result = $dbForProject->findOne('stats_v2', [
|
||||
$result = $dbForProject->findOne('stats', [
|
||||
Query::equal('metric', [$metric]),
|
||||
Query::equal('period', ['inf'])
|
||||
]);
|
||||
@@ -1958,7 +2009,7 @@ App::get('/v1/users/usage')
|
||||
$stats[$metric]['total'] = $result['value'] ?? 0;
|
||||
$limit = $days['limit'];
|
||||
$period = $days['period'];
|
||||
$results = $dbForProject->find('stats_v2', [
|
||||
$results = $dbForProject->find('stats', [
|
||||
Query::equal('metric', [$metric]),
|
||||
Query::equal('period', [$period]),
|
||||
Query::limit($limit),
|
||||
|
||||
@@ -4,6 +4,7 @@ use Appwrite\Auth\OAuth2\Github as OAuth2Github;
|
||||
use Utopia\App;
|
||||
use Appwrite\Event\Build;
|
||||
use Appwrite\Event\Delete;
|
||||
use Utopia\Database\Exception\Query as QueryException;
|
||||
use Utopia\Validator\Host;
|
||||
use Utopia\Database\Database;
|
||||
use Utopia\Database\Document;
|
||||
@@ -237,9 +238,7 @@ $createGitDeployments = function (GitHub $github, string $providerInstallationId
|
||||
$queueForBuilds
|
||||
->setType(BUILD_TYPE_DEPLOYMENT)
|
||||
->setResource($function)
|
||||
->setDeployment($deployment)
|
||||
->setProject($project)
|
||||
->trigger();
|
||||
->setDeployment($deployment);
|
||||
|
||||
//TODO: Add event?
|
||||
}
|
||||
@@ -969,7 +968,11 @@ App::get('/v1/vcs/installations')
|
||||
->inject('dbForProject')
|
||||
->inject('dbForConsole')
|
||||
->action(function (array $queries, string $search, Response $response, Document $project, Database $dbForProject, Database $dbForConsole) {
|
||||
$queries = Query::parseQueries($queries);
|
||||
try {
|
||||
$queries = Query::parseQueries($queries);
|
||||
} catch (QueryException $e) {
|
||||
throw new Exception(Exception::GENERAL_QUERY_INVALID, $e->getMessage());
|
||||
}
|
||||
|
||||
$queries[] = Query::equal('projectInternalId', [$project->getInternalId()]);
|
||||
|
||||
@@ -977,8 +980,12 @@ App::get('/v1/vcs/installations')
|
||||
$queries[] = Query::search('search', $search);
|
||||
}
|
||||
|
||||
// Get cursor document if there was a cursor query
|
||||
$cursor = Query::getByType($queries, [Query::TYPE_CURSOR_AFTER, Query::TYPE_CURSOR_BEFORE]);
|
||||
/**
|
||||
* Get cursor document if there was a cursor query, we use array_filter and reset for reference $cursor to $queries
|
||||
*/
|
||||
$cursor = \array_filter($queries, function ($query) {
|
||||
return \in_array($query->getMethod(), [Query::TYPE_CURSOR_AFTER, Query::TYPE_CURSOR_BEFORE]);
|
||||
});
|
||||
$cursor = reset($cursor);
|
||||
if ($cursor) {
|
||||
/** @var Query $cursor */
|
||||
|
||||
+53
-201
@@ -3,7 +3,6 @@
|
||||
require_once __DIR__ . '/../init.php';
|
||||
|
||||
use Utopia\App;
|
||||
use Utopia\Database\Helpers\Role;
|
||||
use Utopia\Locale\Locale;
|
||||
use Utopia\Logger\Logger;
|
||||
use Utopia\Logger\Log;
|
||||
@@ -15,7 +14,6 @@ use Appwrite\Utopia\View;
|
||||
use Appwrite\Extend\Exception as AppwriteException;
|
||||
use Utopia\Config\Config;
|
||||
use Utopia\Domains\Domain;
|
||||
use Appwrite\Auth\Auth;
|
||||
use Appwrite\Event\Certificate;
|
||||
use Appwrite\Network\Validator\Origin;
|
||||
use Appwrite\Utopia\Response\Filters\V11 as ResponseV11;
|
||||
@@ -24,9 +22,9 @@ use Appwrite\Utopia\Response\Filters\V13 as ResponseV13;
|
||||
use Appwrite\Utopia\Response\Filters\V14 as ResponseV14;
|
||||
use Appwrite\Utopia\Response\Filters\V15 as ResponseV15;
|
||||
use Appwrite\Utopia\Response\Filters\V16 as ResponseV16;
|
||||
use Appwrite\Utopia\Response\Filters\V17 as ResponseV17;
|
||||
use Utopia\CLI\Console;
|
||||
use Utopia\Database\Database;
|
||||
use Utopia\Database\DateTime;
|
||||
use Utopia\Database\Document;
|
||||
use Utopia\Database\Query;
|
||||
use Utopia\Database\Validator\Authorization;
|
||||
@@ -36,8 +34,8 @@ use Appwrite\Utopia\Request\Filters\V13 as RequestV13;
|
||||
use Appwrite\Utopia\Request\Filters\V14 as RequestV14;
|
||||
use Appwrite\Utopia\Request\Filters\V15 as RequestV15;
|
||||
use Appwrite\Utopia\Request\Filters\V16 as RequestV16;
|
||||
use Appwrite\Utopia\Request\Filters\V17 as RequestV17;
|
||||
use Utopia\Validator\Text;
|
||||
use Utopia\Validator\WhiteList;
|
||||
|
||||
Config::setParam('domainVerification', false);
|
||||
Config::setParam('cookieDomain', 'localhost');
|
||||
@@ -203,15 +201,11 @@ App::init()
|
||||
->inject('console')
|
||||
->inject('project')
|
||||
->inject('dbForConsole')
|
||||
->inject('user')
|
||||
->inject('locale')
|
||||
->inject('localeCodes')
|
||||
->inject('clients')
|
||||
->inject('servers')
|
||||
->inject('session')
|
||||
->inject('mode')
|
||||
->inject('queueForCertificates')
|
||||
->action(function (App $utopia, SwooleRequest $swooleRequest, Request $request, Response $response, Document $console, Document $project, Database $dbForConsole, Document $user, Locale $locale, array $localeCodes, array $clients, array $servers, ?Document $session, string $mode, Certificate $queueForCertificates) {
|
||||
->action(function (App $utopia, SwooleRequest $swooleRequest, Request $request, Response $response, Document $console, Document $project, Database $dbForConsole, Locale $locale, array $localeCodes, array $clients, Certificate $queueForCertificates) {
|
||||
/*
|
||||
* Appwrite Router
|
||||
*/
|
||||
@@ -252,6 +246,9 @@ App::init()
|
||||
case version_compare($requestFormat, '1.4.0', '<'):
|
||||
Request::setFilter(new RequestV16());
|
||||
break;
|
||||
case version_compare($requestFormat, '1.5.0', '<'):
|
||||
Request::setFilter(new RequestV17());
|
||||
break;
|
||||
default:
|
||||
Request::setFilter(null);
|
||||
}
|
||||
@@ -319,14 +316,6 @@ App::init()
|
||||
$locale->setDefault($localeParam);
|
||||
}
|
||||
|
||||
if ($project->isEmpty()) {
|
||||
throw new AppwriteException(AppwriteException::PROJECT_NOT_FOUND);
|
||||
}
|
||||
|
||||
if (!empty($route->getLabel('sdk.auth', [])) && $project->isEmpty() && ($route->getLabel('scope', '') !== 'public')) {
|
||||
throw new AppwriteException(AppwriteException::PROJECT_UNKNOWN);
|
||||
}
|
||||
|
||||
$referrer = $request->getReferer();
|
||||
$origin = \parse_url($request->getOrigin($referrer), PHP_URL_HOST);
|
||||
$protocol = \parse_url($request->getOrigin($referrer), PHP_URL_SCHEME);
|
||||
@@ -393,6 +382,9 @@ App::init()
|
||||
case version_compare($responseFormat, '1.4.0', '<'):
|
||||
Response::setFilter(new ResponseV16());
|
||||
break;
|
||||
case version_compare($responseFormat, '1.5.0', '<'):
|
||||
Response::setFilter(new ResponseV17());
|
||||
break;
|
||||
default:
|
||||
Response::setFilter(null);
|
||||
}
|
||||
@@ -445,138 +437,6 @@ App::init()
|
||||
) {
|
||||
throw new AppwriteException(AppwriteException::GENERAL_UNKNOWN_ORIGIN, $originValidator->getDescription());
|
||||
}
|
||||
|
||||
/*
|
||||
* ACL Check
|
||||
*/
|
||||
$role = ($user->isEmpty())
|
||||
? Role::guests()->toString()
|
||||
: Role::users()->toString();
|
||||
|
||||
// Add user roles
|
||||
$memberships = $user->find('teamId', $project->getAttribute('teamId'), 'memberships');
|
||||
|
||||
if ($memberships) {
|
||||
foreach ($memberships->getAttribute('roles', []) as $memberRole) {
|
||||
switch ($memberRole) {
|
||||
case 'owner':
|
||||
$role = Auth::USER_ROLE_OWNER;
|
||||
break;
|
||||
case 'admin':
|
||||
$role = Auth::USER_ROLE_ADMIN;
|
||||
break;
|
||||
case 'developer':
|
||||
$role = Auth::USER_ROLE_DEVELOPER;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
$roles = Config::getParam('roles', []);
|
||||
$scope = $route->getLabel('scope', 'none'); // Allowed scope for chosen route
|
||||
$scopes = $roles[$role]['scopes']; // Allowed scopes for user role
|
||||
|
||||
$authKey = $request->getHeader('x-appwrite-key', '');
|
||||
|
||||
if (!empty($authKey)) { // API Key authentication
|
||||
// Check if given key match project API keys
|
||||
$key = $project->find('secret', $authKey, 'keys');
|
||||
|
||||
/*
|
||||
* Try app auth when we have project key and no user
|
||||
* Mock user to app and grant API key scopes in addition to default app scopes
|
||||
*/
|
||||
if ($key && $user->isEmpty()) {
|
||||
$user = new Document([
|
||||
'$id' => '',
|
||||
'status' => true,
|
||||
'email' => 'app.' . $project->getId() . '@service.' . $request->getHostname(),
|
||||
'password' => '',
|
||||
'name' => $project->getAttribute('name', 'Untitled'),
|
||||
]);
|
||||
|
||||
$role = Auth::USER_ROLE_APPS;
|
||||
$scopes = \array_merge($roles[$role]['scopes'], $key->getAttribute('scopes', []));
|
||||
|
||||
$expire = $key->getAttribute('expire');
|
||||
if (!empty($expire) && $expire < DateTime::formatTz(DateTime::now())) {
|
||||
throw new AppwriteException(AppwriteException::PROJECT_KEY_EXPIRED);
|
||||
}
|
||||
|
||||
Authorization::setRole(Auth::USER_ROLE_APPS);
|
||||
Authorization::setDefaultStatus(false); // Cancel security segmentation for API keys.
|
||||
|
||||
$accessedAt = $key->getAttribute('accessedAt', '');
|
||||
if (DateTime::formatTz(DateTime::addSeconds(new \DateTime(), -APP_KEY_ACCCESS)) > $accessedAt) {
|
||||
$key->setAttribute('accessedAt', DateTime::now());
|
||||
$dbForConsole->updateDocument('keys', $key->getId(), $key);
|
||||
$dbForConsole->purgeCachedDocument('projects', $project->getId());
|
||||
}
|
||||
|
||||
$sdkValidator = new WhiteList($servers, true);
|
||||
$sdk = $request->getHeader('x-sdk-name', 'UNKNOWN');
|
||||
if ($sdkValidator->isValid($sdk)) {
|
||||
$sdks = $key->getAttribute('sdks', []);
|
||||
if (!in_array($sdk, $sdks)) {
|
||||
array_push($sdks, $sdk);
|
||||
$key->setAttribute('sdks', $sdks);
|
||||
|
||||
/** Update access time as well */
|
||||
$key->setAttribute('accessedAt', Datetime::now());
|
||||
$dbForConsole->updateDocument('keys', $key->getId(), $key);
|
||||
$dbForConsole->purgeCachedDocument('projects', $project->getId());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Authorization::setRole($role);
|
||||
|
||||
foreach (Auth::getRoles($user) as $authRole) {
|
||||
Authorization::setRole($authRole);
|
||||
}
|
||||
|
||||
$service = $route->getLabel('sdk.namespace', '');
|
||||
if (!empty($service)) {
|
||||
if (
|
||||
array_key_exists($service, $project->getAttribute('services', []))
|
||||
&& !$project->getAttribute('services', [])[$service]
|
||||
&& !(Auth::isPrivilegedUser(Authorization::getRoles()) || Auth::isAppUser(Authorization::getRoles()))
|
||||
) {
|
||||
throw new AppwriteException(AppwriteException::GENERAL_SERVICE_DISABLED);
|
||||
}
|
||||
}
|
||||
|
||||
if (!\in_array($scope, $scopes)) {
|
||||
if ($project->isEmpty()) { // Check if permission is denied because project is missing
|
||||
throw new AppwriteException(AppwriteException::PROJECT_NOT_FOUND);
|
||||
}
|
||||
|
||||
throw new AppwriteException(AppwriteException::GENERAL_UNAUTHORIZED_SCOPE, $user->getAttribute('email', 'User') . ' (role: ' . \strtolower($roles[$role]['label']) . ') missing scope (' . $scope . ')');
|
||||
}
|
||||
|
||||
if (false === $user->getAttribute('status')) { // Account is blocked
|
||||
throw new AppwriteException(AppwriteException::USER_BLOCKED);
|
||||
}
|
||||
|
||||
if ($user->getAttribute('reset')) {
|
||||
throw new AppwriteException(AppwriteException::USER_PASSWORD_RESET_REQUIRED);
|
||||
}
|
||||
|
||||
if ($mode !== APP_MODE_ADMIN) {
|
||||
$mfaEnabled = $user->getAttribute('mfa', false);
|
||||
$hasVerifiedAuthenticator = $user->getAttribute('totpVerification', false);
|
||||
$hasVerifiedEmail = $user->getAttribute('emailVerification', false);
|
||||
$hasVerifiedPhone = $user->getAttribute('phoneVerification', false);
|
||||
$hasMoreFactors = $hasVerifiedEmail || $hasVerifiedPhone || $hasVerifiedAuthenticator;
|
||||
$minimumFactors = ($mfaEnabled && $hasMoreFactors) ? 2 : 1;
|
||||
|
||||
if (!in_array('mfa', $route->getGroups())) {
|
||||
if ($session && \count($session->getAttribute('factors')) < $minimumFactors) {
|
||||
throw new AppwriteException(AppwriteException::USER_MORE_FACTORS_REQUIRED);
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
App::options()
|
||||
@@ -617,66 +477,58 @@ App::error()
|
||||
->inject('response')
|
||||
->inject('project')
|
||||
->inject('logger')
|
||||
->inject('loggerBreadcrumbs')
|
||||
->action(function (Throwable $error, App $utopia, Request $request, Response $response, Document $project, ?Logger $logger, array $loggerBreadcrumbs) {
|
||||
|
||||
->inject('log')
|
||||
->action(function (Throwable $error, App $utopia, Request $request, Response $response, Document $project, ?Logger $logger, Log $log) {
|
||||
$version = App::getEnv('_APP_VERSION', 'UNKNOWN');
|
||||
$route = $utopia->getRoute();
|
||||
$publish = true;
|
||||
|
||||
if ($error instanceof AppwriteException) {
|
||||
$publish = $error->isPublishable();
|
||||
} else {
|
||||
$publish = $error->getCode() === 0 || $error->getCode() >= 500;
|
||||
}
|
||||
|
||||
if ($logger && $publish) {
|
||||
if ($error->getCode() >= 500 || $error->getCode() === 0) {
|
||||
try {
|
||||
/** @var Utopia\Database\Document $user */
|
||||
$user = $utopia->getResource('user');
|
||||
} catch (\Throwable $th) {
|
||||
// All good, user is optional information for logger
|
||||
}
|
||||
|
||||
$log = new Utopia\Logger\Log();
|
||||
|
||||
if (isset($user) && !$user->isEmpty()) {
|
||||
$log->setUser(new User($user->getId()));
|
||||
}
|
||||
|
||||
$log->setNamespace("http");
|
||||
$log->setServer(\gethostname());
|
||||
$log->setVersion($version);
|
||||
$log->setType(Log::TYPE_ERROR);
|
||||
$log->setMessage($error->getMessage());
|
||||
|
||||
$log->addTag('database', $project->getAttribute('database', 'console'));
|
||||
$log->addTag('method', $route->getMethod());
|
||||
$log->addTag('url', $route->getPath());
|
||||
$log->addTag('verboseType', get_class($error));
|
||||
$log->addTag('code', $error->getCode());
|
||||
$log->addTag('projectId', $project->getId());
|
||||
$log->addTag('hostname', $request->getHostname());
|
||||
$log->addTag('locale', (string)$request->getParam('locale', $request->getHeader('x-appwrite-locale', '')));
|
||||
|
||||
$log->addExtra('file', $error->getFile());
|
||||
$log->addExtra('line', $error->getLine());
|
||||
$log->addExtra('trace', $error->getTraceAsString());
|
||||
$log->addExtra('detailedTrace', $error->getTrace());
|
||||
$log->addExtra('roles', Authorization::getRoles());
|
||||
|
||||
$action = $route->getLabel("sdk.namespace", "UNKNOWN_NAMESPACE") . '.' . $route->getLabel("sdk.method", "UNKNOWN_METHOD");
|
||||
$log->setAction($action);
|
||||
|
||||
$isProduction = App::getEnv('_APP_ENV', 'development') === 'production';
|
||||
$log->setEnvironment($isProduction ? Log::ENVIRONMENT_PRODUCTION : Log::ENVIRONMENT_STAGING);
|
||||
|
||||
foreach ($loggerBreadcrumbs as $loggerBreadcrumb) {
|
||||
$log->addBreadcrumb($loggerBreadcrumb);
|
||||
}
|
||||
|
||||
$responseCode = $logger->addLog($log);
|
||||
Console::info('Log pushed with status code: ' . $responseCode);
|
||||
if ($logger && ($publish || $error->getCode() === 0)) {
|
||||
try {
|
||||
/** @var Utopia\Database\Document $user */
|
||||
$user = $utopia->getResource('user');
|
||||
} catch (\Throwable $th) {
|
||||
// All good, user is optional information for logger
|
||||
}
|
||||
|
||||
if (isset($user) && !$user->isEmpty()) {
|
||||
$log->setUser(new User($user->getId()));
|
||||
}
|
||||
|
||||
$log->setNamespace("http");
|
||||
$log->setServer(\gethostname());
|
||||
$log->setVersion($version);
|
||||
$log->setType(Log::TYPE_ERROR);
|
||||
$log->setMessage($error->getMessage());
|
||||
|
||||
$log->addTag('database', $project->getAttribute('database', 'console'));
|
||||
$log->addTag('method', $route->getMethod());
|
||||
$log->addTag('url', $route->getPath());
|
||||
$log->addTag('verboseType', get_class($error));
|
||||
$log->addTag('code', $error->getCode());
|
||||
$log->addTag('projectId', $project->getId());
|
||||
$log->addTag('hostname', $request->getHostname());
|
||||
$log->addTag('locale', (string)$request->getParam('locale', $request->getHeader('x-appwrite-locale', '')));
|
||||
|
||||
$log->addExtra('file', $error->getFile());
|
||||
$log->addExtra('line', $error->getLine());
|
||||
$log->addExtra('trace', $error->getTraceAsString());
|
||||
$log->addExtra('detailedTrace', $error->getTrace());
|
||||
$log->addExtra('roles', Authorization::getRoles());
|
||||
|
||||
$action = $route->getLabel("sdk.namespace", "UNKNOWN_NAMESPACE") . '.' . $route->getLabel("sdk.method", "UNKNOWN_METHOD");
|
||||
$log->setAction($action);
|
||||
|
||||
$isProduction = App::getEnv('_APP_ENV', 'development') === 'production';
|
||||
$log->setEnvironment($isProduction ? Log::ENVIRONMENT_PRODUCTION : Log::ENVIRONMENT_STAGING);
|
||||
|
||||
$responseCode = $logger->addLog($log);
|
||||
Console::info('Log pushed with status code: ' . $responseCode);
|
||||
}
|
||||
|
||||
$code = $error->getCode();
|
||||
|
||||
+176
-95
@@ -2,11 +2,11 @@
|
||||
|
||||
use Appwrite\Auth\Auth;
|
||||
use Appwrite\Event\Audit;
|
||||
use Appwrite\Event\Build;
|
||||
use Appwrite\Event\Database as EventDatabase;
|
||||
use Appwrite\Event\Delete;
|
||||
use Appwrite\Event\Event;
|
||||
use Appwrite\Event\Func;
|
||||
use Appwrite\Event\Mail;
|
||||
use Appwrite\Event\Messaging;
|
||||
use Appwrite\Extend\Exception;
|
||||
use Appwrite\Event\Usage;
|
||||
@@ -22,7 +22,9 @@ use Utopia\Database\Database;
|
||||
use Utopia\Database\DateTime;
|
||||
use Utopia\Database\Document;
|
||||
use Utopia\Database\Validator\Authorization;
|
||||
use MaxMind\Db\Reader;
|
||||
use Utopia\Config\Config;
|
||||
use Utopia\Database\Helpers\Role;
|
||||
use Utopia\Validator\WhiteList;
|
||||
|
||||
$parseLabel = function (string $label, array $responsePayload, array $requestParams, Document $user) {
|
||||
preg_match_all('/{(.*?)}/', $label, $matches);
|
||||
@@ -135,7 +137,7 @@ $databaseListener = function (string $event, Document $document, Document $proje
|
||||
$queueForUsage
|
||||
->addMetric(METRIC_DEPLOYMENTS, $value) // per project
|
||||
->addMetric(METRIC_DEPLOYMENTS_STORAGE, $document->getAttribute('size') * $value) // per project
|
||||
->addMetric(str_replace(['{resourceType}', '{resourceInternalId}'], [$document->getAttribute('resourceType'), $document->getAttribute('resourceInternalId')], METRIC_FUNCTION_ID_DEPLOYMENTS), $value)// per function
|
||||
->addMetric(str_replace(['{resourceType}', '{resourceInternalId}'], [$document->getAttribute('resourceType'), $document->getAttribute('resourceInternalId')], METRIC_FUNCTION_ID_DEPLOYMENTS), $value) // per function
|
||||
->addMetric(str_replace(['{resourceType}', '{resourceInternalId}'], [$document->getAttribute('resourceType'), $document->getAttribute('resourceInternalId')], METRIC_FUNCTION_ID_DEPLOYMENTS_STORAGE), $document->getAttribute('size') * $value);
|
||||
break;
|
||||
default:
|
||||
@@ -143,6 +145,155 @@ $databaseListener = function (string $event, Document $document, Document $proje
|
||||
}
|
||||
};
|
||||
|
||||
App::init()
|
||||
->groups(['api'])
|
||||
->inject('utopia')
|
||||
->inject('request')
|
||||
->inject('dbForConsole')
|
||||
->inject('project')
|
||||
->inject('user')
|
||||
->inject('session')
|
||||
->inject('servers')
|
||||
->inject('mode')
|
||||
->action(function (App $utopia, Request $request, Database $dbForConsole, Document $project, Document $user, ?Document $session, array $servers, string $mode) {
|
||||
$route = $utopia->getRoute();
|
||||
|
||||
if ($project->isEmpty()) {
|
||||
throw new Exception(Exception::PROJECT_NOT_FOUND);
|
||||
}
|
||||
|
||||
/**
|
||||
* ACL Check
|
||||
*/
|
||||
$role = ($user->isEmpty())
|
||||
? Role::guests()->toString()
|
||||
: Role::users()->toString();
|
||||
|
||||
// Add user roles
|
||||
$memberships = $user->find('teamId', $project->getAttribute('teamId'), 'memberships');
|
||||
|
||||
if ($memberships) {
|
||||
foreach ($memberships->getAttribute('roles', []) as $memberRole) {
|
||||
switch ($memberRole) {
|
||||
case 'owner':
|
||||
$role = Auth::USER_ROLE_OWNER;
|
||||
break;
|
||||
case 'admin':
|
||||
$role = Auth::USER_ROLE_ADMIN;
|
||||
break;
|
||||
case 'developer':
|
||||
$role = Auth::USER_ROLE_DEVELOPER;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
$roles = Config::getParam('roles', []);
|
||||
$scope = $route->getLabel('scope', 'none'); // Allowed scope for chosen route
|
||||
$scopes = $roles[$role]['scopes']; // Allowed scopes for user role
|
||||
|
||||
$authKey = $request->getHeader('x-appwrite-key', '');
|
||||
|
||||
if (!empty($authKey)) { // API Key authentication
|
||||
// Check if given key match project API keys
|
||||
$key = $project->find('secret', $authKey, 'keys');
|
||||
|
||||
/*
|
||||
* Try app auth when we have project key and no user
|
||||
* Mock user to app and grant API key scopes in addition to default app scopes
|
||||
*/
|
||||
if ($key && $user->isEmpty()) {
|
||||
$user = new Document([
|
||||
'$id' => '',
|
||||
'status' => true,
|
||||
'email' => 'app.' . $project->getId() . '@service.' . $request->getHostname(),
|
||||
'password' => '',
|
||||
'name' => $project->getAttribute('name', 'Untitled'),
|
||||
]);
|
||||
|
||||
$role = Auth::USER_ROLE_APPS;
|
||||
$scopes = \array_merge($roles[$role]['scopes'], $key->getAttribute('scopes', []));
|
||||
|
||||
$expire = $key->getAttribute('expire');
|
||||
if (!empty($expire) && $expire < DateTime::formatTz(DateTime::now())) {
|
||||
throw new Exception(Exception::PROJECT_KEY_EXPIRED);
|
||||
}
|
||||
|
||||
Authorization::setRole(Auth::USER_ROLE_APPS);
|
||||
Authorization::setDefaultStatus(false); // Cancel security segmentation for API keys.
|
||||
|
||||
$accessedAt = $key->getAttribute('accessedAt', '');
|
||||
if (DateTime::formatTz(DateTime::addSeconds(new \DateTime(), -APP_KEY_ACCCESS)) > $accessedAt) {
|
||||
$key->setAttribute('accessedAt', DateTime::now());
|
||||
$dbForConsole->updateDocument('keys', $key->getId(), $key);
|
||||
$dbForConsole->purgeCachedDocument('projects', $project->getId());
|
||||
}
|
||||
|
||||
$sdkValidator = new WhiteList($servers, true);
|
||||
$sdk = $request->getHeader('x-sdk-name', 'UNKNOWN');
|
||||
if ($sdkValidator->isValid($sdk)) {
|
||||
$sdks = $key->getAttribute('sdks', []);
|
||||
if (!in_array($sdk, $sdks)) {
|
||||
array_push($sdks, $sdk);
|
||||
$key->setAttribute('sdks', $sdks);
|
||||
|
||||
/** Update access time as well */
|
||||
$key->setAttribute('accessedAt', Datetime::now());
|
||||
$dbForConsole->updateDocument('keys', $key->getId(), $key);
|
||||
$dbForConsole->purgeCachedDocument('projects', $project->getId());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Authorization::setRole($role);
|
||||
|
||||
foreach (Auth::getRoles($user) as $authRole) {
|
||||
Authorization::setRole($authRole);
|
||||
}
|
||||
|
||||
$service = $route->getLabel('sdk.namespace', '');
|
||||
if (!empty($service)) {
|
||||
if (
|
||||
array_key_exists($service, $project->getAttribute('services', []))
|
||||
&& !$project->getAttribute('services', [])[$service]
|
||||
&& !(Auth::isPrivilegedUser(Authorization::getRoles()) || Auth::isAppUser(Authorization::getRoles()))
|
||||
) {
|
||||
throw new Exception(Exception::GENERAL_SERVICE_DISABLED);
|
||||
}
|
||||
}
|
||||
if (!\in_array($scope, $scopes)) {
|
||||
if ($project->isEmpty()) { // Check if permission is denied because project is missing
|
||||
throw new Exception(Exception::PROJECT_NOT_FOUND);
|
||||
}
|
||||
|
||||
throw new Exception(Exception::GENERAL_UNAUTHORIZED_SCOPE, $user->getAttribute('email', 'User') . ' (role: ' . \strtolower($roles[$role]['label']) . ') missing scope (' . $scope . ')');
|
||||
}
|
||||
|
||||
if (false === $user->getAttribute('status')) { // Account is blocked
|
||||
throw new Exception(Exception::USER_BLOCKED);
|
||||
}
|
||||
|
||||
if ($user->getAttribute('reset')) {
|
||||
throw new Exception(Exception::USER_PASSWORD_RESET_REQUIRED);
|
||||
}
|
||||
|
||||
if ($mode !== APP_MODE_ADMIN) {
|
||||
$mfaEnabled = $user->getAttribute('mfa', false);
|
||||
$hasVerifiedAuthenticator = $user->getAttribute('totpVerification', false);
|
||||
$hasVerifiedEmail = $user->getAttribute('emailVerification', false);
|
||||
$hasVerifiedPhone = $user->getAttribute('phoneVerification', false);
|
||||
$hasMoreFactors = $hasVerifiedEmail || $hasVerifiedPhone || $hasVerifiedAuthenticator;
|
||||
$minimumFactors = ($mfaEnabled && $hasMoreFactors) ? 2 : 1;
|
||||
|
||||
if (!in_array('mfa', $route->getGroups())) {
|
||||
if ($session && \count($session->getAttribute('factors')) < $minimumFactors) {
|
||||
throw new Exception(Exception::USER_MORE_FACTORS_REQUIRED);
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
App::init()
|
||||
->groups(['api'])
|
||||
->inject('utopia')
|
||||
@@ -155,17 +306,14 @@ App::init()
|
||||
->inject('queueForAudits')
|
||||
->inject('queueForDeletes')
|
||||
->inject('queueForDatabase')
|
||||
->inject('queueForBuilds')
|
||||
->inject('queueForUsage')
|
||||
->inject('dbForProject')
|
||||
->inject('mode')
|
||||
->action(function (App $utopia, Request $request, Response $response, Document $project, Document $user, Event $queueForEvents, Messaging $queueForMessaging, Audit $queueForAudits, Delete $queueForDeletes, EventDatabase $queueForDatabase, Usage $queueForUsage, Database $dbForProject, string $mode) use ($databaseListener) {
|
||||
->action(function (App $utopia, Request $request, Response $response, Document $project, Document $user, Event $queueForEvents, Messaging $queueForMessaging, Audit $queueForAudits, Delete $queueForDeletes, EventDatabase $queueForDatabase, Build $queueForBuilds, Usage $queueForUsage, Database $dbForProject, string $mode) use ($databaseListener) {
|
||||
|
||||
$route = $utopia->getRoute();
|
||||
|
||||
if ($project->isEmpty() && $route->getLabel('abuse-limit', 0) > 0) { // Abuse limit requires an active project scope
|
||||
throw new Exception(Exception::PROJECT_UNKNOWN);
|
||||
}
|
||||
|
||||
/*
|
||||
* Abuse Check
|
||||
*/
|
||||
@@ -179,6 +327,7 @@ App::init()
|
||||
$end = $request->getContentRangeEnd();
|
||||
$timeLimit = new TimeLimit($abuseKey, $route->getLabel('abuse-limit', 0), $route->getLabel('abuse-time', 3600), $dbForProject);
|
||||
$timeLimit
|
||||
->setParam('{projectId}', $project->getId())
|
||||
->setParam('{userId}', $user->getId())
|
||||
->setParam('{userAgent}', $request->getUserAgent(''))
|
||||
->setParam('{ip}', $request->getIP())
|
||||
@@ -235,9 +384,6 @@ App::init()
|
||||
->setProject($project)
|
||||
->setUser($user);
|
||||
|
||||
$queueForMessaging
|
||||
->setProject($project);
|
||||
|
||||
$queueForAudits
|
||||
->setMode($mode)
|
||||
->setUserAgent($request->getUserAgent(''))
|
||||
@@ -246,9 +392,10 @@ App::init()
|
||||
->setProject($project)
|
||||
->setUser($user);
|
||||
|
||||
|
||||
$queueForDeletes->setProject($project);
|
||||
$queueForDatabase->setProject($project);
|
||||
$queueForBuilds->setProject($project);
|
||||
$queueForMessaging->setProject($project);
|
||||
|
||||
$dbForProject
|
||||
->on(Database::EVENT_DOCUMENT_CREATE, 'calculate-usage', fn ($event, $document) => $databaseListener($event, $document, $project, $queueForUsage, $dbForProject))
|
||||
@@ -295,7 +442,7 @@ App::init()
|
||||
if ($fileSecurity && !$valid) {
|
||||
$file = $dbForProject->getDocument('bucket_' . $bucket->getInternalId(), $fileId);
|
||||
} else {
|
||||
$file = Authorization::skip(fn() => $dbForProject->getDocument('bucket_' . $bucket->getInternalId(), $fileId));
|
||||
$file = Authorization::skip(fn () => $dbForProject->getDocument('bucket_' . $bucket->getInternalId(), $fileId));
|
||||
}
|
||||
|
||||
if ($file->isEmpty()) {
|
||||
@@ -315,82 +462,6 @@ App::init()
|
||||
}
|
||||
});
|
||||
|
||||
App::init()
|
||||
->groups(['auth'])
|
||||
->inject('utopia')
|
||||
->inject('request')
|
||||
->inject('project')
|
||||
->inject('geodb')
|
||||
->action(function (App $utopia, Request $request, Document $project, Reader $geodb) {
|
||||
$denylist = App::getEnv('_APP_CONSOLE_COUNTRIES_DENYLIST', '');
|
||||
if (!empty($denylist) && $project->getId() === 'console') {
|
||||
$countries = explode(',', $denylist);
|
||||
$record = $geodb->get($request->getIP()) ?? [];
|
||||
$country = $record['country']['iso_code'] ?? '';
|
||||
if (in_array($country, $countries)) {
|
||||
throw new Exception(Exception::GENERAL_REGION_ACCESS_DENIED);
|
||||
}
|
||||
}
|
||||
|
||||
$route = $utopia->getRoute();
|
||||
|
||||
$isPrivilegedUser = Auth::isPrivilegedUser(Authorization::getRoles());
|
||||
$isAppUser = Auth::isAppUser(Authorization::getRoles());
|
||||
|
||||
if ($isAppUser || $isPrivilegedUser) { // Skip limits for app and console devs
|
||||
return;
|
||||
}
|
||||
|
||||
$auths = $project->getAttribute('auths', []);
|
||||
switch ($route->getLabel('auth.type', '')) {
|
||||
case 'emailPassword':
|
||||
if (($auths['emailPassword'] ?? true) === false) {
|
||||
throw new Exception(Exception::USER_AUTH_METHOD_UNSUPPORTED, 'Email / Password authentication is disabled for this project');
|
||||
}
|
||||
break;
|
||||
|
||||
case 'magic-url':
|
||||
if ($project->getAttribute('usersAuthMagicURL', true) === false) {
|
||||
throw new Exception(Exception::USER_AUTH_METHOD_UNSUPPORTED, 'Magic URL authentication is disabled for this project');
|
||||
}
|
||||
break;
|
||||
|
||||
case 'anonymous':
|
||||
if (($auths['anonymous'] ?? true) === false) {
|
||||
throw new Exception(Exception::USER_AUTH_METHOD_UNSUPPORTED, 'Anonymous authentication is disabled for this project');
|
||||
}
|
||||
break;
|
||||
|
||||
case 'invites':
|
||||
if (($auths['invites'] ?? true) === false) {
|
||||
throw new Exception(Exception::USER_AUTH_METHOD_UNSUPPORTED, 'Invites authentication is disabled for this project');
|
||||
}
|
||||
break;
|
||||
|
||||
case 'jwt':
|
||||
if (($auths['JWT'] ?? true) === false) {
|
||||
throw new Exception(Exception::USER_AUTH_METHOD_UNSUPPORTED, 'JWT authentication is disabled for this project');
|
||||
}
|
||||
break;
|
||||
|
||||
case 'phone':
|
||||
if (($auths['phone'] ?? true) === false) {
|
||||
throw new Exception(Exception::USER_AUTH_METHOD_UNSUPPORTED, 'Phone authentication is disabled for this project');
|
||||
}
|
||||
break;
|
||||
|
||||
case 'email-otp':
|
||||
if (($auths['emailOTP'] ?? true) === false) {
|
||||
throw new Exception(Exception::USER_AUTH_METHOD_UNSUPPORTED, 'Email OTP authentication is disabled for this project');
|
||||
}
|
||||
break;
|
||||
|
||||
default:
|
||||
throw new Exception(Exception::USER_AUTH_METHOD_UNSUPPORTED, 'Unsupported authentication route');
|
||||
break;
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* Limit user session
|
||||
*
|
||||
@@ -442,11 +513,13 @@ App::shutdown()
|
||||
->inject('queueForUsage')
|
||||
->inject('queueForDeletes')
|
||||
->inject('queueForDatabase')
|
||||
->inject('queueForBuilds')
|
||||
->inject('queueForMessaging')
|
||||
->inject('dbForProject')
|
||||
->inject('queueForFunctions')
|
||||
->inject('mode')
|
||||
->inject('dbForConsole')
|
||||
->action(function (App $utopia, Request $request, Response $response, Document $project, Document $user, Event $queueForEvents, Audit $queueForAudits, Usage $queueForUsage, Delete $queueForDeletes, EventDatabase $queueForDatabase, Database $dbForProject, Func $queueForFunctions, string $mode, Database $dbForConsole) use ($parseLabel) {
|
||||
->action(function (App $utopia, Request $request, Response $response, Document $project, Document $user, Event $queueForEvents, Audit $queueForAudits, Usage $queueForUsage, Delete $queueForDeletes, EventDatabase $queueForDatabase, Build $queueForBuilds, Messaging $queueForMessaging, Database $dbForProject, Func $queueForFunctions, string $mode, Database $dbForConsole) use ($parseLabel) {
|
||||
|
||||
$responsePayload = $response->getPayload();
|
||||
|
||||
@@ -547,6 +620,14 @@ App::shutdown()
|
||||
$queueForDatabase->trigger();
|
||||
}
|
||||
|
||||
if (!empty($queueForBuilds->getType())) {
|
||||
$queueForBuilds->trigger();
|
||||
}
|
||||
|
||||
if (!empty($queueForMessaging->getType())) {
|
||||
$queueForMessaging->trigger();
|
||||
}
|
||||
|
||||
/**
|
||||
* Cache label
|
||||
*/
|
||||
@@ -572,7 +653,7 @@ App::shutdown()
|
||||
'resource' => $resource,
|
||||
'contentType' => $response->getContentType(),
|
||||
'payload' => base64_encode($data['payload']),
|
||||
]) ;
|
||||
]);
|
||||
|
||||
$signature = md5($data);
|
||||
$cacheLog = Authorization::skip(fn () => $dbForProject->getDocument('cache', $key));
|
||||
@@ -580,10 +661,10 @@ App::shutdown()
|
||||
$now = DateTime::now();
|
||||
if ($cacheLog->isEmpty()) {
|
||||
Authorization::skip(fn () => $dbForProject->createDocument('cache', new Document([
|
||||
'$id' => $key,
|
||||
'resource' => $resource,
|
||||
'accessedAt' => $now,
|
||||
'signature' => $signature,
|
||||
'$id' => $key,
|
||||
'resource' => $resource,
|
||||
'accessedAt' => $now,
|
||||
'signature' => $signature,
|
||||
])));
|
||||
} elseif (DateTime::formatTz(DateTime::addSeconds(new \DateTime(), -APP_CACHE_UPDATE)) > $accessedAt) {
|
||||
$cacheLog->setAttribute('accessedAt', $now);
|
||||
|
||||
@@ -43,7 +43,7 @@ App::init()
|
||||
break;
|
||||
|
||||
case 'magic-url':
|
||||
if ($project->getAttribute('usersAuthMagicURL', true) === false) {
|
||||
if (($auths['usersAuthMagicURL'] ?? true) === false) {
|
||||
throw new Exception(Exception::USER_AUTH_METHOD_UNSUPPORTED, 'Magic URL authentication is disabled for this project');
|
||||
}
|
||||
break;
|
||||
@@ -54,6 +54,12 @@ App::init()
|
||||
}
|
||||
break;
|
||||
|
||||
case 'phone':
|
||||
if (($auths['phone'] ?? true) === false) {
|
||||
throw new Exception(Exception::USER_AUTH_METHOD_UNSUPPORTED, 'Phone authentication is disabled for this project');
|
||||
}
|
||||
break;
|
||||
|
||||
case 'invites':
|
||||
if (($auths['invites'] ?? true) === false) {
|
||||
throw new Exception(Exception::USER_AUTH_METHOD_UNSUPPORTED, 'Invites authentication is disabled for this project');
|
||||
@@ -66,12 +72,6 @@ App::init()
|
||||
}
|
||||
break;
|
||||
|
||||
case 'phone':
|
||||
if (($auths['phone'] ?? true) === false) {
|
||||
throw new Exception(Exception::USER_AUTH_METHOD_UNSUPPORTED, 'Phone authentication is disabled for this project');
|
||||
}
|
||||
break;
|
||||
|
||||
case 'email-otp':
|
||||
if (($auths['emailOTP'] ?? true) === false) {
|
||||
throw new Exception(Exception::USER_AUTH_METHOD_UNSUPPORTED, 'Email OTP authentication is disabled for this project');
|
||||
@@ -80,6 +80,5 @@ App::init()
|
||||
|
||||
default:
|
||||
throw new Exception(Exception::USER_AUTH_METHOD_UNSUPPORTED, 'Unsupported authentication route');
|
||||
break;
|
||||
}
|
||||
});
|
||||
|
||||
+3
-8
@@ -77,7 +77,7 @@ $http->on('start', function (Server $http) use ($payloadSize, $register) {
|
||||
$dbForConsole = $app->getResource('dbForConsole');
|
||||
/** @var Utopia\Database\Database $dbForConsole */
|
||||
break; // leave the do-while if successful
|
||||
} catch (\Exception $e) {
|
||||
} catch (\Throwable $e) {
|
||||
Console::warning("Database not ready. Retrying connection ({$attempts})...");
|
||||
if ($attempts >= $max) {
|
||||
throw new \Exception('Failed to connect to database: ' . $e->getMessage());
|
||||
@@ -91,7 +91,7 @@ $http->on('start', function (Server $http) use ($payloadSize, $register) {
|
||||
try {
|
||||
Console::success('[Setup] - Creating database: appwrite...');
|
||||
$dbForConsole->create();
|
||||
} catch (\Exception $e) {
|
||||
} catch (\Throwable $e) {
|
||||
Console::success('[Setup] - Skip: metadata table already exists');
|
||||
}
|
||||
|
||||
@@ -264,10 +264,9 @@ $http->on('request', function (SwooleRequest $swooleRequest, SwooleResponse $swo
|
||||
// All good, user is optional information for logger
|
||||
}
|
||||
|
||||
$loggerBreadcrumbs = $app->getResource("loggerBreadcrumbs");
|
||||
$route = $app->getRoute();
|
||||
|
||||
$log = new Utopia\Logger\Log();
|
||||
$log = $app->getResource("log");
|
||||
|
||||
if (isset($user) && !$user->isEmpty()) {
|
||||
$log->setUser(new User($user->getId()));
|
||||
@@ -299,10 +298,6 @@ $http->on('request', function (SwooleRequest $swooleRequest, SwooleResponse $swo
|
||||
$isProduction = App::getEnv('_APP_ENV', 'development') === 'production';
|
||||
$log->setEnvironment($isProduction ? Log::ENVIRONMENT_PRODUCTION : Log::ENVIRONMENT_STAGING);
|
||||
|
||||
foreach ($loggerBreadcrumbs as $loggerBreadcrumb) {
|
||||
$log->addBreadcrumb($loggerBreadcrumb);
|
||||
}
|
||||
|
||||
$responseCode = $logger->addLog($log);
|
||||
Console::info('Log pushed with status code: ' . $responseCode);
|
||||
}
|
||||
|
||||
+16
-8
@@ -35,6 +35,7 @@ use Appwrite\OpenSSL\OpenSSL;
|
||||
use Appwrite\URL\URL as AppwriteURL;
|
||||
use Utopia\App;
|
||||
use Utopia\Database\Adapter\SQL;
|
||||
use Utopia\Database\Exception\Query as QueryException;
|
||||
use Utopia\Logger\Logger;
|
||||
use Utopia\Cache\Adapter\Redis as RedisCache;
|
||||
use Utopia\Cache\Cache;
|
||||
@@ -71,6 +72,7 @@ use Appwrite\Hooks\Hooks;
|
||||
use MaxMind\Db\Reader;
|
||||
use PHPMailer\PHPMailer\PHPMailer;
|
||||
use Swoole\Database\PDOProxy;
|
||||
use Utopia\Logger\Log;
|
||||
use Utopia\Queue;
|
||||
use Utopia\Queue\Connection;
|
||||
use Utopia\Storage\Storage;
|
||||
@@ -110,8 +112,8 @@ const APP_LIMIT_LIST_DEFAULT = 25; // Default maximum number of items to return
|
||||
const APP_KEY_ACCCESS = 24 * 60 * 60; // 24 hours
|
||||
const APP_USER_ACCCESS = 24 * 60 * 60; // 24 hours
|
||||
const APP_CACHE_UPDATE = 24 * 60 * 60; // 24 hours
|
||||
const APP_CACHE_BUSTER = 329;
|
||||
const APP_VERSION_STABLE = '1.4.13';
|
||||
const APP_CACHE_BUSTER = 330;
|
||||
const APP_VERSION_STABLE = '1.5.0';
|
||||
const APP_DATABASE_ATTRIBUTE_EMAIL = 'email';
|
||||
const APP_DATABASE_ATTRIBUTE_ENUM = 'enum';
|
||||
const APP_DATABASE_ATTRIBUTE_IP = 'ip';
|
||||
@@ -140,9 +142,11 @@ const APP_SOCIAL_DEV = 'https://dev.to/appwrite';
|
||||
const APP_SOCIAL_STACKSHARE = 'https://stackshare.io/appwrite';
|
||||
const APP_SOCIAL_YOUTUBE = 'https://www.youtube.com/c/appwrite?sub_confirmation=1';
|
||||
const APP_HOSTNAME_INTERNAL = 'appwrite';
|
||||
|
||||
// Database Reconnect
|
||||
const DATABASE_RECONNECT_SLEEP = 2;
|
||||
const DATABASE_RECONNECT_MAX_ATTEMPTS = 10;
|
||||
|
||||
// Database Worker Types
|
||||
const DATABASE_TYPE_CREATE_ATTRIBUTE = 'createAttribute';
|
||||
const DATABASE_TYPE_CREATE_INDEX = 'createIndex';
|
||||
@@ -150,9 +154,11 @@ const DATABASE_TYPE_DELETE_ATTRIBUTE = 'deleteAttribute';
|
||||
const DATABASE_TYPE_DELETE_INDEX = 'deleteIndex';
|
||||
const DATABASE_TYPE_DELETE_COLLECTION = 'deleteCollection';
|
||||
const DATABASE_TYPE_DELETE_DATABASE = 'deleteDatabase';
|
||||
|
||||
// Build Worker Types
|
||||
const BUILD_TYPE_DEPLOYMENT = 'deployment';
|
||||
const BUILD_TYPE_RETRY = 'retry';
|
||||
|
||||
// Deletion Types
|
||||
const DELETE_TYPE_DATABASES = 'databases';
|
||||
const DELETE_TYPE_DOCUMENT = 'document';
|
||||
@@ -178,6 +184,10 @@ const DELETE_TYPE_TOPIC = 'topic';
|
||||
const DELETE_TYPE_TARGET = 'target';
|
||||
const DELETE_TYPE_EXPIRED_TARGETS = 'invalid_targets';
|
||||
const DELETE_TYPE_SESSION_TARGETS = 'session_targets';
|
||||
|
||||
// Message types
|
||||
const MESSAGE_SEND_TYPE_INTERNAL = 'internal';
|
||||
const MESSAGE_SEND_TYPE_EXTERNAL = 'external';
|
||||
// Mail Types
|
||||
const MAIL_TYPE_VERIFICATION = 'verification';
|
||||
const MAIL_TYPE_MAGIC_SESSION = 'magicSession';
|
||||
@@ -201,6 +211,7 @@ const MESSAGE_TYPE_PUSH = 'push';
|
||||
// Usage metrics
|
||||
const METRIC_TEAMS = 'teams';
|
||||
const METRIC_USERS = 'users';
|
||||
const METRIC_MESSAGES = 'messages';
|
||||
const METRIC_SESSIONS = 'sessions';
|
||||
const METRIC_DATABASES = 'databases';
|
||||
const METRIC_COLLECTIONS = 'collections';
|
||||
@@ -937,7 +948,7 @@ $register->set('smtp', function () {
|
||||
return $mail;
|
||||
});
|
||||
$register->set('geodb', function () {
|
||||
return new Reader(__DIR__ . '/assets/dbip/dbip-country-lite-2023-01.mmdb');
|
||||
return new Reader(__DIR__ . '/assets/dbip/dbip-country-lite-2024-02.mmdb');
|
||||
});
|
||||
$register->set('passwordsDictionary', function () {
|
||||
$content = \file_get_contents(__DIR__ . '/assets/security/10k-common-passwords');
|
||||
@@ -986,6 +997,7 @@ foreach ($locales as $locale) {
|
||||
]);
|
||||
|
||||
// Runtime Execution
|
||||
App::setResource('log', fn() => new Log());
|
||||
App::setResource('logger', function ($register) {
|
||||
return $register->get('logger');
|
||||
}, ['register']);
|
||||
@@ -994,10 +1006,6 @@ App::setResource('hooks', function ($register) {
|
||||
return $register->get('hooks');
|
||||
}, ['register']);
|
||||
|
||||
App::setResource('loggerBreadcrumbs', function () {
|
||||
return [];
|
||||
});
|
||||
|
||||
App::setResource('register', fn() => $register);
|
||||
App::setResource('locale', fn() => new Locale(App::getEnv('_APP_LOCALE', 'en')));
|
||||
|
||||
@@ -1410,7 +1418,7 @@ function getDevice($root): Device
|
||||
$accessSecret = $dsn->getPassword() ?? '';
|
||||
$bucket = $dsn->getPath() ?? '';
|
||||
$region = $dsn->getParam('region');
|
||||
} catch (\Exception $e) {
|
||||
} catch (\Throwable $e) {
|
||||
Console::warning($e->getMessage() . 'Invalid DSN. Defaulting to Local device.');
|
||||
}
|
||||
|
||||
|
||||
@@ -143,6 +143,7 @@ services:
|
||||
- _APP_LOGGING_PROVIDER
|
||||
- _APP_LOGGING_CONFIG
|
||||
- _APP_MAINTENANCE_INTERVAL
|
||||
- _APP_MAINTENANCE_DELAY
|
||||
- _APP_MAINTENANCE_RETENTION_EXECUTION
|
||||
- _APP_MAINTENANCE_RETENTION_CACHE
|
||||
- _APP_MAINTENANCE_RETENTION_ABUSE
|
||||
|
||||
+51
-44
@@ -45,8 +45,7 @@ Server::setResource('dbForConsole', function (Cache $cache, Registry $register)
|
||||
$database = $pools
|
||||
->get('console')
|
||||
->pop()
|
||||
->getResource()
|
||||
;
|
||||
->getResource();
|
||||
|
||||
$adapter = new Database($database, $cache);
|
||||
$adapter->setNamespace('_console');
|
||||
@@ -54,26 +53,6 @@ Server::setResource('dbForConsole', function (Cache $cache, Registry $register)
|
||||
return $adapter;
|
||||
}, ['cache', 'register']);
|
||||
|
||||
Server::setResource('dbForProject', function (Cache $cache, Registry $register, Message $message, Database $dbForConsole) {
|
||||
$payload = $message->getPayload() ?? [];
|
||||
$project = new Document($payload['project'] ?? []);
|
||||
|
||||
if ($project->isEmpty() || $project->getId() === 'console') {
|
||||
return $dbForConsole;
|
||||
}
|
||||
|
||||
$pools = $register->get('pools');
|
||||
$database = $pools
|
||||
->get($project->getAttribute('database'))
|
||||
->pop()
|
||||
->getResource()
|
||||
;
|
||||
|
||||
$adapter = new Database($database, $cache);
|
||||
$adapter->setNamespace('_' . $project->getInternalId());
|
||||
return $adapter;
|
||||
}, ['cache', 'register', 'message', 'dbForConsole']);
|
||||
|
||||
Server::setResource('project', function (Message $message, Database $dbForConsole) {
|
||||
$payload = $message->getPayload() ?? [];
|
||||
$project = new Document($payload['project'] ?? []);
|
||||
@@ -81,10 +60,26 @@ Server::setResource('project', function (Message $message, Database $dbForConsol
|
||||
if ($project->getId() === 'console') {
|
||||
return $project;
|
||||
}
|
||||
|
||||
return $dbForConsole->getDocument('projects', $project->getId());
|
||||
;
|
||||
}, ['message', 'dbForConsole']);
|
||||
|
||||
Server::setResource('dbForProject', function (Cache $cache, Registry $register, Message $message, Document $project, Database $dbForConsole) {
|
||||
if ($project->isEmpty() || $project->getId() === 'console') {
|
||||
return $dbForConsole;
|
||||
}
|
||||
|
||||
$pools = $register->get('pools');
|
||||
$database = $pools
|
||||
->get($project->getAttribute('database'))
|
||||
->pop()
|
||||
->getResource();
|
||||
|
||||
$adapter = new Database($database, $cache);
|
||||
$adapter->setNamespace('_' . $project->getInternalId());
|
||||
return $adapter;
|
||||
}, ['cache', 'register', 'message', 'project', 'dbForConsole']);
|
||||
|
||||
Server::setResource('getProjectDB', function (Group $pools, Database $dbForConsole, $cache) {
|
||||
$databases = []; // TODO: @Meldiron This should probably be responsibility of utopia-php/pools
|
||||
|
||||
@@ -143,72 +138,84 @@ Server::setResource('cache', function (Registry $register) {
|
||||
|
||||
return new Cache(new Sharding($adapters));
|
||||
}, ['register']);
|
||||
|
||||
Server::setResource('log', fn() => new Log());
|
||||
|
||||
Server::setResource('queueForUsage', function (Connection $queue) {
|
||||
return new Usage($queue);
|
||||
}, ['queue']);
|
||||
|
||||
Server::setResource('queue', function (Group $pools) {
|
||||
return $pools->get('queue')->pop()->getResource();
|
||||
}, ['pools']);
|
||||
|
||||
Server::setResource('queueForDatabase', function (Connection $queue) {
|
||||
return new EventDatabase($queue);
|
||||
}, ['queue']);
|
||||
|
||||
Server::setResource('queueForMessaging', function (Connection $queue) {
|
||||
return new Messaging($queue);
|
||||
}, ['queue']);
|
||||
|
||||
Server::setResource('queueForMails', function (Connection $queue) {
|
||||
return new Mail($queue);
|
||||
}, ['queue']);
|
||||
|
||||
Server::setResource('queueForBuilds', function (Connection $queue) {
|
||||
return new Build($queue);
|
||||
}, ['queue']);
|
||||
|
||||
Server::setResource('queueForDeletes', function (Connection $queue) {
|
||||
return new Delete($queue);
|
||||
}, ['queue']);
|
||||
|
||||
Server::setResource('queueForEvents', function (Connection $queue) {
|
||||
return new Event($queue);
|
||||
}, ['queue']);
|
||||
|
||||
Server::setResource('queueForAudits', function (Connection $queue) {
|
||||
return new Audit($queue);
|
||||
}, ['queue']);
|
||||
|
||||
Server::setResource('queueForFunctions', function (Connection $queue) {
|
||||
return new Func($queue);
|
||||
}, ['queue']);
|
||||
|
||||
Server::setResource('queueForCertificates', function (Connection $queue) {
|
||||
return new Certificate($queue);
|
||||
}, ['queue']);
|
||||
|
||||
Server::setResource('queueForMigrations', function (Connection $queue) {
|
||||
return new Migration($queue);
|
||||
}, ['queue']);
|
||||
|
||||
Server::setResource('queueForHamster', function (Connection $queue) {
|
||||
return new Hamster($queue);
|
||||
}, ['queue']);
|
||||
|
||||
Server::setResource('logger', function (Registry $register) {
|
||||
return $register->get('logger');
|
||||
}, ['register']);
|
||||
|
||||
Server::setResource('pools', function (Registry $register) {
|
||||
return $register->get('pools');
|
||||
}, ['register']);
|
||||
Server::setResource('getFunctionsDevice', function () {
|
||||
return function (string $projectId) {
|
||||
return getDevice(APP_STORAGE_FUNCTIONS . '/app-' . $projectId);
|
||||
};
|
||||
});
|
||||
Server::setResource('getFilesDevice', function () {
|
||||
return function (string $projectId) {
|
||||
return getDevice(APP_STORAGE_UPLOADS . '/app-' . $projectId);
|
||||
};
|
||||
});
|
||||
Server::setResource('getBuildsDevice', function () {
|
||||
return function (string $projectId) {
|
||||
return getDevice(APP_STORAGE_BUILDS . '/app-' . $projectId);
|
||||
};
|
||||
});
|
||||
Server::setResource('getCacheDevice', function () {
|
||||
return function (string $projectId) {
|
||||
return getDevice(APP_STORAGE_CACHE . '/app-' . $projectId);
|
||||
};
|
||||
});
|
||||
|
||||
Server::setResource('functionsDevice', function (Document $project) {
|
||||
return getDevice(APP_STORAGE_FUNCTIONS . '/app-' . $project->getId());
|
||||
}, ['project']);
|
||||
|
||||
Server::setResource('filesDevice', function (Document $project) {
|
||||
return getDevice(APP_STORAGE_UPLOADS . '/app-' . $project->getId());
|
||||
}, ['project']);
|
||||
|
||||
Server::setResource('buildsDevice', function (Document $project) {
|
||||
return getDevice(APP_STORAGE_BUILDS . '/app-' . $project->getId());
|
||||
}, ['project']);
|
||||
|
||||
Server::setResource('cacheDevice', function (Document $project) {
|
||||
return getDevice(APP_STORAGE_CACHE . '/app-' . $project->getId());
|
||||
}, ['project']);
|
||||
|
||||
$pools = $register->get('pools');
|
||||
$platform = new Appwrite();
|
||||
@@ -246,7 +253,7 @@ try {
|
||||
'workerName' => strtolower($workerName) ?? null,
|
||||
'queueName' => $queueName
|
||||
]);
|
||||
} catch (\Exception $e) {
|
||||
} catch (\Throwable $e) {
|
||||
Console::error($e->getMessage() . ', File: ' . $e->getFile() . ', Line: ' . $e->getLine());
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,3 @@
|
||||
#!/bin/sh
|
||||
|
||||
php /usr/src/code/app/cli.php queue-count --type=failed $@
|
||||
@@ -0,0 +1,3 @@
|
||||
#!/bin/sh
|
||||
|
||||
php /usr/src/code/app/cli.php queue-count --type=processing $@
|
||||
@@ -0,0 +1,3 @@
|
||||
#!/bin/sh
|
||||
|
||||
php /usr/src/code/app/cli.php queue-count --type=success $@
|
||||
@@ -0,0 +1,3 @@
|
||||
#!/bin/sh
|
||||
|
||||
php /usr/src/code/app/cli.php queue-retry $@
|
||||
+2
-8
@@ -62,7 +62,7 @@
|
||||
"utopia-php/platform": "0.5.*",
|
||||
"utopia-php/pools": "0.4.*",
|
||||
"utopia-php/preloader": "0.2.*",
|
||||
"utopia-php/queue": "0.6.*",
|
||||
"utopia-php/queue": "0.7.*",
|
||||
"utopia-php/registry": "0.5.*",
|
||||
"utopia-php/storage": "0.18.*",
|
||||
"utopia-php/swoole": "0.8.*",
|
||||
@@ -75,14 +75,8 @@
|
||||
"adhocore/jwt": "1.1.2",
|
||||
"spomky-labs/otphp": "^10.0",
|
||||
"webonyx/graphql-php": "14.11.*",
|
||||
"league/csv": "^9.14"
|
||||
"league/csv": "9.14.*"
|
||||
},
|
||||
"repositories": [
|
||||
{
|
||||
"url": "https://github.com/appwrite/runtimes.git",
|
||||
"type": "git"
|
||||
}
|
||||
],
|
||||
"require-dev": {
|
||||
"ext-fileinfo": "*",
|
||||
"appwrite/sdk-generator": "0.36.*",
|
||||
|
||||
Generated
+54
-52
@@ -4,7 +4,7 @@
|
||||
"Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies",
|
||||
"This file is @generated automatically"
|
||||
],
|
||||
"content-hash": "e6e0d6874f4d718d96396d71a66864da",
|
||||
"content-hash": "609062319cc652e2760367f39604ac77",
|
||||
"packages": [
|
||||
{
|
||||
"name": "adhocore/jwt",
|
||||
@@ -162,6 +162,12 @@
|
||||
"url": "https://github.com/appwrite/runtimes.git",
|
||||
"reference": "214a37c2c66e0f2bc9c30fdfde66955d9fd084a1"
|
||||
},
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/appwrite/runtimes/zipball/214a37c2c66e0f2bc9c30fdfde66955d9fd084a1",
|
||||
"reference": "214a37c2c66e0f2bc9c30fdfde66955d9fd084a1",
|
||||
"shasum": ""
|
||||
},
|
||||
"require": {
|
||||
"php": ">=8.0",
|
||||
"utopia-php/system": "0.7.*"
|
||||
@@ -176,6 +182,7 @@
|
||||
"Appwrite\\Runtimes\\": "src/Runtimes"
|
||||
}
|
||||
},
|
||||
"notification-url": "https://packagist.org/downloads/",
|
||||
"license": [
|
||||
"BSD-3-Clause"
|
||||
],
|
||||
@@ -195,6 +202,10 @@
|
||||
"php",
|
||||
"runtimes"
|
||||
],
|
||||
"support": {
|
||||
"issues": "https://github.com/appwrite/runtimes/issues",
|
||||
"source": "https://github.com/appwrite/runtimes/tree/0.13.2"
|
||||
},
|
||||
"time": "2023-11-22T15:36:00+00:00"
|
||||
},
|
||||
{
|
||||
@@ -1029,16 +1040,16 @@
|
||||
},
|
||||
{
|
||||
"name": "symfony/polyfill-php80",
|
||||
"version": "v1.28.0",
|
||||
"version": "v1.29.0",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/symfony/polyfill-php80.git",
|
||||
"reference": "6caa57379c4aec19c0a12a38b59b26487dcfe4b5"
|
||||
"reference": "87b68208d5c1188808dd7839ee1e6c8ec3b02f1b"
|
||||
},
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/symfony/polyfill-php80/zipball/6caa57379c4aec19c0a12a38b59b26487dcfe4b5",
|
||||
"reference": "6caa57379c4aec19c0a12a38b59b26487dcfe4b5",
|
||||
"url": "https://api.github.com/repos/symfony/polyfill-php80/zipball/87b68208d5c1188808dd7839ee1e6c8ec3b02f1b",
|
||||
"reference": "87b68208d5c1188808dd7839ee1e6c8ec3b02f1b",
|
||||
"shasum": ""
|
||||
},
|
||||
"require": {
|
||||
@@ -1046,9 +1057,6 @@
|
||||
},
|
||||
"type": "library",
|
||||
"extra": {
|
||||
"branch-alias": {
|
||||
"dev-main": "1.28-dev"
|
||||
},
|
||||
"thanks": {
|
||||
"name": "symfony/polyfill",
|
||||
"url": "https://github.com/symfony/polyfill"
|
||||
@@ -1092,7 +1100,7 @@
|
||||
"shim"
|
||||
],
|
||||
"support": {
|
||||
"source": "https://github.com/symfony/polyfill-php80/tree/v1.28.0"
|
||||
"source": "https://github.com/symfony/polyfill-php80/tree/v1.29.0"
|
||||
},
|
||||
"funding": [
|
||||
{
|
||||
@@ -1108,7 +1116,7 @@
|
||||
"type": "tidelift"
|
||||
}
|
||||
],
|
||||
"time": "2023-01-26T09:26:14+00:00"
|
||||
"time": "2024-01-29T20:11:03+00:00"
|
||||
},
|
||||
{
|
||||
"name": "thecodingmachine/safe",
|
||||
@@ -1751,16 +1759,16 @@
|
||||
},
|
||||
{
|
||||
"name": "utopia-php/image",
|
||||
"version": "0.6.0",
|
||||
"version": "0.6.1",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/utopia-php/image.git",
|
||||
"reference": "88f7209172bdabd81e76ac981c95fac117dc6e08"
|
||||
"reference": "2d74c27e69e65a93cf94a16586598a04fe435bf0"
|
||||
},
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/utopia-php/image/zipball/88f7209172bdabd81e76ac981c95fac117dc6e08",
|
||||
"reference": "88f7209172bdabd81e76ac981c95fac117dc6e08",
|
||||
"url": "https://api.github.com/repos/utopia-php/image/zipball/2d74c27e69e65a93cf94a16586598a04fe435bf0",
|
||||
"reference": "2d74c27e69e65a93cf94a16586598a04fe435bf0",
|
||||
"shasum": ""
|
||||
},
|
||||
"require": {
|
||||
@@ -1793,9 +1801,9 @@
|
||||
],
|
||||
"support": {
|
||||
"issues": "https://github.com/utopia-php/image/issues",
|
||||
"source": "https://github.com/utopia-php/image/tree/0.6.0"
|
||||
"source": "https://github.com/utopia-php/image/tree/0.6.1"
|
||||
},
|
||||
"time": "2024-01-24T06:59:44+00:00"
|
||||
"time": "2024-02-05T13:31:44+00:00"
|
||||
},
|
||||
{
|
||||
"name": "utopia-php/locale",
|
||||
@@ -1903,16 +1911,16 @@
|
||||
},
|
||||
{
|
||||
"name": "utopia-php/messaging",
|
||||
"version": "0.9.0",
|
||||
"version": "0.9.1",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/utopia-php/messaging.git",
|
||||
"reference": "df54ba51570e886724590edeb03dbd455bb0464d"
|
||||
"reference": "7beec07684e9e1dfcf4ab5b1ba731fa396dccbdf"
|
||||
},
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/utopia-php/messaging/zipball/df54ba51570e886724590edeb03dbd455bb0464d",
|
||||
"reference": "df54ba51570e886724590edeb03dbd455bb0464d",
|
||||
"url": "https://api.github.com/repos/utopia-php/messaging/zipball/7beec07684e9e1dfcf4ab5b1ba731fa396dccbdf",
|
||||
"reference": "7beec07684e9e1dfcf4ab5b1ba731fa396dccbdf",
|
||||
"shasum": ""
|
||||
},
|
||||
"require": {
|
||||
@@ -1947,9 +1955,9 @@
|
||||
],
|
||||
"support": {
|
||||
"issues": "https://github.com/utopia-php/messaging/issues",
|
||||
"source": "https://github.com/utopia-php/messaging/tree/0.9.0"
|
||||
"source": "https://github.com/utopia-php/messaging/tree/0.9.1"
|
||||
},
|
||||
"time": "2024-01-31T11:51:27+00:00"
|
||||
"time": "2024-02-15T03:44:44+00:00"
|
||||
},
|
||||
{
|
||||
"name": "utopia-php/migration",
|
||||
@@ -2264,16 +2272,16 @@
|
||||
},
|
||||
{
|
||||
"name": "utopia-php/queue",
|
||||
"version": "0.6.0",
|
||||
"version": "0.7.0",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/utopia-php/queue.git",
|
||||
"reference": "0120bd21904cb2bee34e4571b1737589ffff0eb1"
|
||||
"reference": "917565256eb94bcab7246f7a746b1a486813761b"
|
||||
},
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/utopia-php/queue/zipball/0120bd21904cb2bee34e4571b1737589ffff0eb1",
|
||||
"reference": "0120bd21904cb2bee34e4571b1737589ffff0eb1",
|
||||
"url": "https://api.github.com/repos/utopia-php/queue/zipball/917565256eb94bcab7246f7a746b1a486813761b",
|
||||
"reference": "917565256eb94bcab7246f7a746b1a486813761b",
|
||||
"shasum": ""
|
||||
},
|
||||
"require": {
|
||||
@@ -2319,9 +2327,9 @@
|
||||
],
|
||||
"support": {
|
||||
"issues": "https://github.com/utopia-php/queue/issues",
|
||||
"source": "https://github.com/utopia-php/queue/tree/0.6.0"
|
||||
"source": "https://github.com/utopia-php/queue/tree/0.7.0"
|
||||
},
|
||||
"time": "2023-10-16T16:59:45+00:00"
|
||||
"time": "2024-01-17T19:00:43+00:00"
|
||||
},
|
||||
{
|
||||
"name": "utopia-php/registry",
|
||||
@@ -2771,16 +2779,16 @@
|
||||
"packages-dev": [
|
||||
{
|
||||
"name": "appwrite/sdk-generator",
|
||||
"version": "0.36.2",
|
||||
"version": "0.36.3",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/appwrite/sdk-generator.git",
|
||||
"reference": "0aa67479d75f0e0cb7b60454031534d7f0abaece"
|
||||
"reference": "8d308f7f492545da3e51ea5b91c0778392c40b93"
|
||||
},
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/appwrite/sdk-generator/zipball/0aa67479d75f0e0cb7b60454031534d7f0abaece",
|
||||
"reference": "0aa67479d75f0e0cb7b60454031534d7f0abaece",
|
||||
"url": "https://api.github.com/repos/appwrite/sdk-generator/zipball/8d308f7f492545da3e51ea5b91c0778392c40b93",
|
||||
"reference": "8d308f7f492545da3e51ea5b91c0778392c40b93",
|
||||
"shasum": ""
|
||||
},
|
||||
"require": {
|
||||
@@ -2816,9 +2824,9 @@
|
||||
"description": "Appwrite PHP library for generating API SDKs for multiple programming languages and platforms",
|
||||
"support": {
|
||||
"issues": "https://github.com/appwrite/sdk-generator/issues",
|
||||
"source": "https://github.com/appwrite/sdk-generator/tree/0.36.2"
|
||||
"source": "https://github.com/appwrite/sdk-generator/tree/0.36.3"
|
||||
},
|
||||
"time": "2024-01-19T01:04:35+00:00"
|
||||
"time": "2024-02-14T06:33:38+00:00"
|
||||
},
|
||||
{
|
||||
"name": "doctrine/deprecations",
|
||||
@@ -5123,16 +5131,16 @@
|
||||
},
|
||||
{
|
||||
"name": "symfony/polyfill-ctype",
|
||||
"version": "v1.28.0",
|
||||
"version": "v1.29.0",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/symfony/polyfill-ctype.git",
|
||||
"reference": "ea208ce43cbb04af6867b4fdddb1bdbf84cc28cb"
|
||||
"reference": "ef4d7e442ca910c4764bce785146269b30cb5fc4"
|
||||
},
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/symfony/polyfill-ctype/zipball/ea208ce43cbb04af6867b4fdddb1bdbf84cc28cb",
|
||||
"reference": "ea208ce43cbb04af6867b4fdddb1bdbf84cc28cb",
|
||||
"url": "https://api.github.com/repos/symfony/polyfill-ctype/zipball/ef4d7e442ca910c4764bce785146269b30cb5fc4",
|
||||
"reference": "ef4d7e442ca910c4764bce785146269b30cb5fc4",
|
||||
"shasum": ""
|
||||
},
|
||||
"require": {
|
||||
@@ -5146,9 +5154,6 @@
|
||||
},
|
||||
"type": "library",
|
||||
"extra": {
|
||||
"branch-alias": {
|
||||
"dev-main": "1.28-dev"
|
||||
},
|
||||
"thanks": {
|
||||
"name": "symfony/polyfill",
|
||||
"url": "https://github.com/symfony/polyfill"
|
||||
@@ -5185,7 +5190,7 @@
|
||||
"portable"
|
||||
],
|
||||
"support": {
|
||||
"source": "https://github.com/symfony/polyfill-ctype/tree/v1.28.0"
|
||||
"source": "https://github.com/symfony/polyfill-ctype/tree/v1.29.0"
|
||||
},
|
||||
"funding": [
|
||||
{
|
||||
@@ -5201,20 +5206,20 @@
|
||||
"type": "tidelift"
|
||||
}
|
||||
],
|
||||
"time": "2023-01-26T09:26:14+00:00"
|
||||
"time": "2024-01-29T20:11:03+00:00"
|
||||
},
|
||||
{
|
||||
"name": "symfony/polyfill-mbstring",
|
||||
"version": "v1.28.0",
|
||||
"version": "v1.29.0",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/symfony/polyfill-mbstring.git",
|
||||
"reference": "42292d99c55abe617799667f454222c54c60e229"
|
||||
"reference": "9773676c8a1bb1f8d4340a62efe641cf76eda7ec"
|
||||
},
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/symfony/polyfill-mbstring/zipball/42292d99c55abe617799667f454222c54c60e229",
|
||||
"reference": "42292d99c55abe617799667f454222c54c60e229",
|
||||
"url": "https://api.github.com/repos/symfony/polyfill-mbstring/zipball/9773676c8a1bb1f8d4340a62efe641cf76eda7ec",
|
||||
"reference": "9773676c8a1bb1f8d4340a62efe641cf76eda7ec",
|
||||
"shasum": ""
|
||||
},
|
||||
"require": {
|
||||
@@ -5228,9 +5233,6 @@
|
||||
},
|
||||
"type": "library",
|
||||
"extra": {
|
||||
"branch-alias": {
|
||||
"dev-main": "1.28-dev"
|
||||
},
|
||||
"thanks": {
|
||||
"name": "symfony/polyfill",
|
||||
"url": "https://github.com/symfony/polyfill"
|
||||
@@ -5268,7 +5270,7 @@
|
||||
"shim"
|
||||
],
|
||||
"support": {
|
||||
"source": "https://github.com/symfony/polyfill-mbstring/tree/v1.28.0"
|
||||
"source": "https://github.com/symfony/polyfill-mbstring/tree/v1.29.0"
|
||||
},
|
||||
"funding": [
|
||||
{
|
||||
@@ -5284,7 +5286,7 @@
|
||||
"type": "tidelift"
|
||||
}
|
||||
],
|
||||
"time": "2023-07-28T09:04:16+00:00"
|
||||
"time": "2024-01-29T20:11:03+00:00"
|
||||
},
|
||||
{
|
||||
"name": "textalk/websocket",
|
||||
|
||||
@@ -582,6 +582,7 @@ services:
|
||||
- _APP_REDIS_PORT
|
||||
- _APP_REDIS_USER
|
||||
- _APP_REDIS_PASS
|
||||
- _APP_SMS_PROJECTS_DENY_LIST
|
||||
- _APP_DB_HOST
|
||||
- _APP_DB_PORT
|
||||
- _APP_DB_SCHEMA
|
||||
@@ -662,6 +663,7 @@ services:
|
||||
- _APP_MAINTENANCE_RETENTION_AUDIT
|
||||
- _APP_MAINTENANCE_RETENTION_USAGE_HOURLY
|
||||
- _APP_MAINTENANCE_RETENTION_SCHEDULES
|
||||
- _APP_MAINTENANCE_DELAY
|
||||
|
||||
appwrite-worker-usage:
|
||||
entrypoint: worker-usage
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
Get the SSL certificate for a domain
|
||||
@@ -0,0 +1 @@
|
||||
Returns the amount of failed jobs in a given queue.
|
||||
@@ -279,7 +279,7 @@ class Firebase extends OAuth2
|
||||
$role = \json_decode($role, true);
|
||||
|
||||
return $role;
|
||||
} catch (\Exception $e) {
|
||||
} catch (\Throwable $e) {
|
||||
if ($e->getCode() !== 404) {
|
||||
throw $e;
|
||||
}
|
||||
|
||||
@@ -8,13 +8,13 @@ use Utopia\Queue\Client;
|
||||
|
||||
class Messaging extends Event
|
||||
{
|
||||
protected string $type = '';
|
||||
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);
|
||||
@@ -24,6 +24,29 @@ class Messaging extends Event
|
||||
->setClass(Event::MESSAGING_CLASS_NAME);
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets type for the build event.
|
||||
*
|
||||
* @param string $type Can be `MESSAGE_TYPE_INTERNAL` or `MESSAGE_TYPE_EXTERNAL`.
|
||||
* @return self
|
||||
*/
|
||||
public function setType(string $type): self
|
||||
{
|
||||
$this->type = $type;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns set type for the function event.
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function getType(): string
|
||||
{
|
||||
return $this->type;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets recipient for the messaging event.
|
||||
*
|
||||
@@ -162,6 +185,7 @@ class Messaging extends Event
|
||||
$client = new Client($this->queue, $this->connection);
|
||||
|
||||
return $client->enqueue([
|
||||
'type' => $this->type,
|
||||
'project' => $this->project,
|
||||
'user' => $this->user,
|
||||
'messageId' => $this->messageId,
|
||||
|
||||
@@ -192,7 +192,6 @@ class Exception extends \Exception
|
||||
|
||||
/** Projects */
|
||||
public const PROJECT_NOT_FOUND = 'project_not_found';
|
||||
public const PROJECT_UNKNOWN = 'project_unknown';
|
||||
public const PROJECT_PROVIDER_DISABLED = 'project_provider_disabled';
|
||||
public const PROJECT_PROVIDER_UNSUPPORTED = 'project_provider_unsupported';
|
||||
public const PROJECT_ALREADY_EXISTS = 'project_already_exists';
|
||||
@@ -205,6 +204,8 @@ class Exception extends \Exception
|
||||
|
||||
public const PROJECT_TEMPLATE_DEFAULT_DELETION = 'project_template_default_deletion';
|
||||
|
||||
public const PROJECT_REGION_UNSUPPORTED = 'project_region_unsupported';
|
||||
|
||||
/** Webhooks */
|
||||
public const WEBHOOK_NOT_FOUND = 'webhook_not_found';
|
||||
|
||||
@@ -244,12 +245,15 @@ class Exception extends \Exception
|
||||
public const REALTIME_POLICY_VIOLATION = 'realtime_policy_violation';
|
||||
|
||||
/** Health */
|
||||
public const QUEUE_SIZE_EXCEEDED = 'queue_size_exceeded';
|
||||
public const HEALTH_QUEUE_SIZE_EXCEEDED = 'health_queue_size_exceeded';
|
||||
public const HEALTH_CERTIFICATE_EXPIRED = 'health_certificate_expired';
|
||||
public const HEALTH_INVALID_HOST = 'health_invalid_host';
|
||||
|
||||
/** 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_MISSING_CREDENTIALS = 'provider_missing_credentials';
|
||||
|
||||
/** Topic */
|
||||
@@ -264,33 +268,33 @@ class Exception extends \Exception
|
||||
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_PROCESSING = 'message_already_processing';
|
||||
public const MESSAGE_ALREADY_FAILED = 'message_already_failed';
|
||||
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';
|
||||
public const MESSAGE_MISSING_SCHEDULE = 'message_missing_schedule';
|
||||
|
||||
/** Targets */
|
||||
public const TARGET_PROVIDER_INVALID_TYPE = 'target_provider_invalid_type';
|
||||
|
||||
/** Schedules */
|
||||
public const SCHEDULE_NOT_FOUND = 'schedule_not_found';
|
||||
|
||||
|
||||
protected string $type = '';
|
||||
protected array $errors = [];
|
||||
protected bool $publish = true;
|
||||
protected bool $publish;
|
||||
|
||||
public function __construct(string $type = Exception::GENERAL_UNKNOWN, string $message = null, int $code = null, \Throwable $previous = null)
|
||||
{
|
||||
$this->errors = Config::getParam('errors');
|
||||
$this->type = $type;
|
||||
$this->code = $code ?? $this->errors[$type]['code'];
|
||||
$this->message = $message ?? $this->errors[$type]['description'];
|
||||
|
||||
if (isset($this->errors[$type])) {
|
||||
$this->code = $this->errors[$type]['code'];
|
||||
$this->message = $this->errors[$type]['description'];
|
||||
$this->publish = $this->errors[$type]['publish'] ?? true;
|
||||
}
|
||||
|
||||
$this->message = $message ?? $this->message;
|
||||
$this->code = $code ?? $this->code;
|
||||
$this->publish = $this->errors[$type]['publish'] ?? ($this->code >= 500);
|
||||
|
||||
parent::__construct($this->message, $this->code, $previous);
|
||||
}
|
||||
|
||||
@@ -78,7 +78,7 @@ abstract class Migration
|
||||
'1.4.11' => 'V19',
|
||||
'1.4.12' => 'V19',
|
||||
'1.4.13' => 'V19',
|
||||
'1.4.14' => 'V20'
|
||||
'1.5.0' => 'V20',
|
||||
];
|
||||
|
||||
/**
|
||||
@@ -401,7 +401,7 @@ abstract class Migration
|
||||
|
||||
try {
|
||||
$stmt->execute();
|
||||
} catch (\Exception $e) {
|
||||
} catch (\Throwable $e) {
|
||||
Console::warning($e->getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,16 +2,19 @@
|
||||
|
||||
namespace Appwrite\Migration\Version;
|
||||
|
||||
use Appwrite\Auth\Auth;
|
||||
use Appwrite\Migration\Migration;
|
||||
use Exception;
|
||||
use PDOException;
|
||||
use Throwable;
|
||||
use Utopia\CLI\Console;
|
||||
use Utopia\Database\Database;
|
||||
use Utopia\Database\DateTime;
|
||||
use Utopia\Database\Document;
|
||||
use Utopia\Database\Exception;
|
||||
use Utopia\Database\Exception\Authorization;
|
||||
use Utopia\Database\Exception\Duplicate;
|
||||
use Utopia\Database\Exception\Structure;
|
||||
use Utopia\Database\Helpers\ID;
|
||||
use Utopia\Database\Query;
|
||||
|
||||
class V20 extends Migration
|
||||
@@ -21,18 +24,14 @@ class V20 extends Migration
|
||||
*/
|
||||
public function execute(): void
|
||||
{
|
||||
if ($this->project->getInternalId() == 'console') {
|
||||
return;
|
||||
}
|
||||
|
||||
/**
|
||||
* Disable SubQueries for Performance.
|
||||
*/
|
||||
foreach (['subQueryIndexes', 'subQueryPlatforms', 'subQueryDomains', 'subQueryKeys', 'subQueryWebhooks', 'subQuerySessions', 'subQueryTokens', 'subQueryMemberships', 'subQueryVariables'] as $name) {
|
||||
foreach (['subQueryIndexes', 'subQueryPlatforms', 'subQueryDomains', 'subQueryKeys', 'subQueryWebhooks', 'subQuerySessions', 'subQueryTokens', 'subQueryMemberships', 'subQueryVariables', 'subQueryChallenges', 'subQueryProjectVariables', 'subQueryTargets', 'subQueryTopicTargets'] as $name) {
|
||||
Database::addFilter(
|
||||
$name,
|
||||
fn () => null,
|
||||
fn () => []
|
||||
fn() => null,
|
||||
fn() => []
|
||||
);
|
||||
}
|
||||
|
||||
@@ -45,19 +44,239 @@ class V20 extends Migration
|
||||
Console::log('Migrating Project: ' . $this->project->getAttribute('name') . ' (' . $this->project->getId() . ')');
|
||||
$this->projectDB->setNamespace("_{$this->project->getInternalId()}");
|
||||
|
||||
Console::info('Migrating Collections');
|
||||
$this->migrateCollections();
|
||||
|
||||
Console::info('Migrating Functions');
|
||||
$this->migrateFunctions();
|
||||
|
||||
Console::info('Migrating Databases');
|
||||
$this->migrateDatabases();
|
||||
|
||||
Console::info('Migrating Collections');
|
||||
$this->migrateCollections();
|
||||
|
||||
Console::info('Migrating Buckets');
|
||||
$this->migrateBuckets();
|
||||
|
||||
Console::info('Migrating Documents');
|
||||
$this->forEachDocument([$this, 'fixDocument']);
|
||||
}
|
||||
|
||||
/**
|
||||
* Migrate Collections.
|
||||
*
|
||||
* @return void
|
||||
* @throws Exception|Throwable
|
||||
*/
|
||||
private function migrateCollections(): void
|
||||
{
|
||||
$internalProjectId = $this->project->getInternalId();
|
||||
$collectionType = match ($internalProjectId) {
|
||||
'console' => 'console',
|
||||
default => 'projects',
|
||||
};
|
||||
|
||||
// Support database array type migration (user collections)
|
||||
foreach (
|
||||
$this->documentsIterator('attributes', [
|
||||
Query::equal('array', [true]),
|
||||
]) as $attribute
|
||||
) {
|
||||
$foundIndex = false;
|
||||
foreach (
|
||||
$this->documentsIterator('indexes', [
|
||||
Query::equal('databaseInternalId', [$attribute['databaseInternalId']]),
|
||||
Query::equal('collectionInternalId', [$attribute['collectionInternalId']]),
|
||||
]) as $index
|
||||
) {
|
||||
if (in_array($attribute['key'], $index['attributes'])) {
|
||||
$this->projectDB->deleteIndex($index['collectionId'], $index['$id']);
|
||||
$foundIndex = true;
|
||||
}
|
||||
}
|
||||
if ($foundIndex === true) {
|
||||
$this->projectDB->updateAttribute($attribute['collectionInternalId'], $attribute['key'], $attribute['type']);
|
||||
}
|
||||
}
|
||||
|
||||
$collections = $this->collections[$collectionType];
|
||||
foreach ($collections as $collection) {
|
||||
$id = $collection['$id'];
|
||||
|
||||
Console::log("Migrating Collection \"{$id}\"");
|
||||
|
||||
$this->projectDB->setNamespace("_$internalProjectId");
|
||||
|
||||
// Support database array type migration
|
||||
$foundIndex = false;
|
||||
foreach ($collection['attributes'] ?? [] as $attribute) {
|
||||
if ($attribute['array'] === true) {
|
||||
foreach ($collection['indexes'] ?? [] as $index) {
|
||||
if (in_array($attribute['$id'], $index['attributes'])) {
|
||||
$this->projectDB->deleteIndex($id, $index['$id']);
|
||||
$foundIndex = true;
|
||||
}
|
||||
}
|
||||
if ($foundIndex === true) {
|
||||
$this->projectDB->updateAttribute($id, $attribute['$id'], $attribute['type']);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
switch ($id) {
|
||||
case '_metadata':
|
||||
$this->createCollection('providers');
|
||||
$this->createCollection('messages');
|
||||
$this->createCollection('topics');
|
||||
$this->createCollection('subscribers');
|
||||
$this->createCollection('targets');
|
||||
$this->createCollection('challenges');
|
||||
|
||||
break;
|
||||
case 'cache':
|
||||
// Create resourceType attribute
|
||||
try {
|
||||
$this->createAttributeFromCollection($this->projectDB, $id, 'resourceType');
|
||||
$this->projectDB->purgeCachedCollection($id);
|
||||
} catch (Throwable $th) {
|
||||
Console::warning("'resourceType' from {$id}: {$th->getMessage()}");
|
||||
}
|
||||
|
||||
// Create mimeType attribute
|
||||
try {
|
||||
$this->createAttributeFromCollection($this->projectDB, $id, 'mimeType');
|
||||
$this->projectDB->purgeCachedCollection($id);
|
||||
} catch (Throwable $th) {
|
||||
Console::warning("'mimeType' from {$id}: {$th->getMessage()}");
|
||||
}
|
||||
|
||||
break;
|
||||
case 'stats':
|
||||
try {
|
||||
/**
|
||||
* Delete 'type' attribute
|
||||
*/
|
||||
$this->projectDB->deleteAttribute($id, 'type');
|
||||
/**
|
||||
* Alter `signed` internal type on `value` attr
|
||||
*/
|
||||
$this->projectDB->updateAttribute(collection: $id, id: 'value', signed: true);
|
||||
$this->projectDB->purgeCachedCollection($id);
|
||||
} catch (Throwable $th) {
|
||||
Console::warning("'type' from {$id}: {$th->getMessage()}");
|
||||
}
|
||||
|
||||
// update stats index
|
||||
$index = '_key_metric_period_time';
|
||||
|
||||
try {
|
||||
$this->projectDB->deleteIndex($id, $index);
|
||||
} catch (\Throwable $th) {
|
||||
Console::warning("'$index' from {$id}: {$th->getMessage()}");
|
||||
}
|
||||
|
||||
try {
|
||||
$this->createIndexFromCollection($this->projectDB, $id, $index);
|
||||
} catch (\Throwable $th) {
|
||||
Console::warning("'$index' from {$id}: {$th->getMessage()}");
|
||||
}
|
||||
|
||||
break;
|
||||
case 'sessions':
|
||||
// Create expire attribute
|
||||
try {
|
||||
$this->createAttributeFromCollection($this->projectDB, $id, 'expire');
|
||||
$this->projectDB->purgeCachedCollection($id);
|
||||
} catch (Throwable $th) {
|
||||
Console::warning("'expire' from {$id}: {$th->getMessage()}");
|
||||
}
|
||||
|
||||
// Create factors attribute
|
||||
try {
|
||||
$this->createAttributeFromCollection($this->projectDB, $id, 'factors');
|
||||
$this->projectDB->purgeCachedCollection($id);
|
||||
} catch (Throwable $th) {
|
||||
Console::warning("'factors' from {$id}: {$th->getMessage()}");
|
||||
}
|
||||
|
||||
break;
|
||||
case 'users':
|
||||
// Create targets attribute
|
||||
try {
|
||||
$this->createAttributeFromCollection($this->projectDB, $id, 'targets');
|
||||
$this->projectDB->purgeCachedCollection($id);
|
||||
} catch (Throwable $th) {
|
||||
Console::warning("'targets' from {$id}: {$th->getMessage()}");
|
||||
}
|
||||
|
||||
// Create mfa attribute
|
||||
try {
|
||||
$this->createAttributeFromCollection($this->projectDB, $id, 'mfa');
|
||||
$this->projectDB->purgeCachedCollection($id);
|
||||
} catch (Throwable $th) {
|
||||
Console::warning("'mfa' from {$id}: {$th->getMessage()}");
|
||||
}
|
||||
|
||||
// Create totp attribute
|
||||
try {
|
||||
$this->createAttributeFromCollection($this->projectDB, $id, 'totp');
|
||||
$this->projectDB->purgeCachedCollection($id);
|
||||
} catch (Throwable $th) {
|
||||
Console::warning("'totp' from {$id}: {$th->getMessage()}");
|
||||
}
|
||||
|
||||
// Create totpVerification attribute
|
||||
try {
|
||||
$this->createAttributeFromCollection($this->projectDB, $id, 'totpVerification');
|
||||
$this->projectDB->purgeCachedCollection($id);
|
||||
} catch (Throwable $th) {
|
||||
Console::warning("'totpVerification' from {$id}: {$th->getMessage()}");
|
||||
}
|
||||
|
||||
// Create totpSecret attribute
|
||||
try {
|
||||
$this->createAttributeFromCollection($this->projectDB, $id, 'totpSecret');
|
||||
$this->projectDB->purgeCachedCollection($id);
|
||||
} catch (Throwable $th) {
|
||||
Console::warning("'totpSecret' from {$id}: {$th->getMessage()}");
|
||||
}
|
||||
|
||||
// Create totpBackup attribute
|
||||
try {
|
||||
$this->createAttributeFromCollection($this->projectDB, $id, 'totpBackup');
|
||||
$this->projectDB->purgeCachedCollection($id);
|
||||
} catch (Throwable $th) {
|
||||
Console::warning("'totpBackup' from {$id}: {$th->getMessage()}");
|
||||
}
|
||||
|
||||
break;
|
||||
case 'projects':
|
||||
// Rename providers authProviders to oAuthProviders
|
||||
try {
|
||||
$this->projectDB->renameAttribute($id, 'authProviders', 'oAuthProviders');
|
||||
$this->projectDB->purgeCachedCollection($id);
|
||||
} catch (Throwable $th) {
|
||||
Console::warning("'oAuthProviders' from {$id}: {$th->getMessage()}");
|
||||
}
|
||||
break;
|
||||
case 'webhooks':
|
||||
try {
|
||||
$this->createAttributeFromCollection($this->projectDB, $id, 'enabled');
|
||||
$this->createAttributeFromCollection($this->projectDB, $id, 'logs');
|
||||
$this->createAttributeFromCollection($this->projectDB, $id, 'attempts');
|
||||
$this->projectDB->purgeCachedCollection($id);
|
||||
} catch (Throwable $th) {
|
||||
Console::warning("'webhooks' from {$id}: {$th->getMessage()}");
|
||||
}
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
|
||||
usleep(50000);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
/**
|
||||
* @return void
|
||||
* @throws Authorization
|
||||
@@ -89,7 +308,7 @@ class V20 extends Migration
|
||||
Query::equal('period', ['1d']),
|
||||
]);
|
||||
|
||||
$sessionsDeleted = $query['value'] ?? 0;
|
||||
$sessionsDeleted = $query['value'] ?? 0;
|
||||
$value = $sessionsCreated - $sessionsDeleted;
|
||||
$this->createInfMetric('sessions', $value);
|
||||
}
|
||||
@@ -115,8 +334,8 @@ class V20 extends Migration
|
||||
'$id' => $id,
|
||||
'metric' => $metric,
|
||||
'period' => 'inf',
|
||||
'value' => $value,
|
||||
'time' => null,
|
||||
'value' => $value,
|
||||
'time' => null,
|
||||
'region' => 'default',
|
||||
]));
|
||||
} catch (Duplicate $th) {
|
||||
@@ -132,6 +351,7 @@ class V20 extends Migration
|
||||
*/
|
||||
protected function migrateUsageMetrics(string $from, string $to): void
|
||||
{
|
||||
|
||||
/**
|
||||
* inf metric
|
||||
*/
|
||||
@@ -159,7 +379,7 @@ class V20 extends Migration
|
||||
while ($sum === $limit) {
|
||||
$paginationQueries = [Query::limit($limit)];
|
||||
if ($latestDocument !== null) {
|
||||
$paginationQueries[] = Query::cursorAfter($latestDocument);
|
||||
$paginationQueries[] = Query::cursorAfter($latestDocument);
|
||||
}
|
||||
$stats = $this->projectDB->find('stats', \array_merge($paginationQueries, [
|
||||
Query::equal('metric', [$from]),
|
||||
@@ -182,11 +402,12 @@ class V20 extends Migration
|
||||
Console::warning("Error while updating metric {$from} " . $th->getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Migrate functions.
|
||||
*
|
||||
* @return void
|
||||
* @throws \Exception
|
||||
* @throws Exception
|
||||
*/
|
||||
private function migrateFunctions(): void
|
||||
{
|
||||
@@ -215,7 +436,7 @@ class V20 extends Migration
|
||||
* Migrate Databases.
|
||||
*
|
||||
* @return void
|
||||
* @throws \Exception
|
||||
* @throws Exception
|
||||
*/
|
||||
private function migrateDatabases(): void
|
||||
{
|
||||
@@ -241,7 +462,7 @@ class V20 extends Migration
|
||||
Console::log("Migrating Collections of {$collectionTable} {$collection->getId()} ({$collection->getAttribute('name')})");
|
||||
|
||||
// Collection level
|
||||
$collectionId = $collection->getId() ;
|
||||
$collectionId = $collection->getId();
|
||||
$collectionInternalId = $collection->getInternalId();
|
||||
|
||||
$this->migrateUsageMetrics("documents.$databaseId/$collectionId.count.total", "$databaseInternalId.$collectionInternalId.documents");
|
||||
@@ -250,53 +471,10 @@ class V20 extends Migration
|
||||
}
|
||||
|
||||
/**
|
||||
* Migrate Collections.
|
||||
* Migrating Buckets.
|
||||
*
|
||||
* @return void
|
||||
* @throws \Exception
|
||||
*/
|
||||
private function migrateCollections(): void
|
||||
{
|
||||
$internalProjectId = $this->project->getInternalId();
|
||||
$collectionType = match ($internalProjectId) {
|
||||
'console' => 'console',
|
||||
default => 'projects',
|
||||
};
|
||||
|
||||
$collections = $this->collections[$collectionType];
|
||||
|
||||
foreach ($collections as $collection) {
|
||||
$id = $collection['$id'];
|
||||
|
||||
Console::log("Migrating Collection \"{$id}\"");
|
||||
|
||||
$this->projectDB->setNamespace("_$internalProjectId");
|
||||
|
||||
switch ($id) {
|
||||
case 'stats':
|
||||
try {
|
||||
/**
|
||||
* Delete 'type' attribute
|
||||
*/
|
||||
$this->projectDB->deleteAttribute($id, 'type');
|
||||
/**
|
||||
* Alter `signed` internal type on `value` attr
|
||||
*/
|
||||
$this->projectDB->updateAttribute($id, 'value', null, null, null, null, true);
|
||||
$this->projectDB->deleteCachedCollection($id);
|
||||
} catch (Throwable $th) {
|
||||
Console::warning("'type' from {$id}: {$th->getMessage()}");
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Migrating all Bucket tables.
|
||||
*
|
||||
* @return void
|
||||
* @throws \Exception
|
||||
* @throws Exception
|
||||
* @throws PDOException
|
||||
*/
|
||||
protected function migrateBuckets(): void
|
||||
@@ -305,7 +483,6 @@ class V20 extends Migration
|
||||
$this->migrateUsageMetrics('buckets.$all.count.total', 'buckets');
|
||||
$this->migrateUsageMetrics('files.$all.count.total', 'files');
|
||||
$this->migrateUsageMetrics('files.$all.storage.size', 'files.storage');
|
||||
// There is also project.$all.storage.size which is the same as files.$all.storage.size
|
||||
|
||||
foreach ($this->documentsIterator('buckets') as $bucket) {
|
||||
$id = "bucket_{$bucket->getInternalId()}";
|
||||
@@ -315,9 +492,55 @@ class V20 extends Migration
|
||||
$bucketId = $bucket->getId();
|
||||
$bucketInternalId = $bucket->getInternalId();
|
||||
|
||||
$this->migrateUsageMetrics("files.$bucketId.count.total", "$bucketInternalId.files");
|
||||
$this->migrateUsageMetrics("files.$bucketId.count.total", "$bucketInternalId.files");
|
||||
$this->migrateUsageMetrics("files.$bucketId.storage.size", "$bucketInternalId.files.storage");
|
||||
// some stats come with $ prefix in front of the id -> files.$650c3fda307b7fec4934.storage.size;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Fix run on each document
|
||||
*
|
||||
* @param Document $document
|
||||
* @return Document
|
||||
*/
|
||||
protected function fixDocument(Document $document): Document
|
||||
{
|
||||
switch ($document->getCollection()) {
|
||||
case 'projects':
|
||||
/**
|
||||
* Bump version number.
|
||||
*/
|
||||
$document->setAttribute('version', '1.5.0');
|
||||
break;
|
||||
case 'users':
|
||||
if ($document->getAttribute('email', '') !== '') {
|
||||
$target = new Document([
|
||||
'$id' => ID::unique(),
|
||||
'userId' => $document->getId(),
|
||||
'userInternalId' => $document->getInternalId(),
|
||||
'providerType' => MESSAGE_TYPE_EMAIL,
|
||||
'identifier' => $document->getAttribute('email'),
|
||||
]);
|
||||
$this->projectDB->createDocument('targets', $target);
|
||||
}
|
||||
|
||||
if ($document->getAttribute('phone', '') !== '') {
|
||||
$target = new Document([
|
||||
'$id' => ID::unique(),
|
||||
'userId' => $document->getId(),
|
||||
'userInternalId' => $document->getInternalId(),
|
||||
'providerType' => MESSAGE_TYPE_SMS,
|
||||
'identifier' => $document->getAttribute('phone'),
|
||||
]);
|
||||
$this->projectDB->createDocument('targets', $target);
|
||||
}
|
||||
break;
|
||||
case 'sessions':
|
||||
$duration = $this->project->getAttribute('auths', [])['duration'] ?? Auth::TOKEN_EXPIRATION_LOGIN_LONG;
|
||||
$expire = DateTime::addSeconds(new \DateTime(), $duration);
|
||||
$document->setAttribute('expire', $expire);
|
||||
break;
|
||||
}
|
||||
return $document;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6,6 +6,11 @@ use Utopia\Validator;
|
||||
|
||||
class CNAME extends Validator
|
||||
{
|
||||
/**
|
||||
* @var mixed
|
||||
*/
|
||||
protected mixed $logs;
|
||||
|
||||
/**
|
||||
* @var string
|
||||
*/
|
||||
@@ -27,6 +32,14 @@ class CNAME extends Validator
|
||||
return 'Invalid CNAME record';
|
||||
}
|
||||
|
||||
/**
|
||||
* @return mixed
|
||||
*/
|
||||
public function getLogs(): mixed
|
||||
{
|
||||
return $this->logs;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if CNAME record target value matches selected target
|
||||
*
|
||||
@@ -42,6 +55,7 @@ class CNAME extends Validator
|
||||
|
||||
try {
|
||||
$records = \dns_get_record($domain, DNS_CNAME);
|
||||
$this->logs = $records;
|
||||
} catch (\Throwable $th) {
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
namespace Appwrite\Platform\Services;
|
||||
|
||||
use Appwrite\Platform\Tasks\CalcTierStats;
|
||||
use Appwrite\Platform\Tasks\CreateInfMetric;
|
||||
use Appwrite\Platform\Tasks\DeleteOrphanedProjects;
|
||||
use Appwrite\Platform\Tasks\DevGenerateTranslations;
|
||||
use Appwrite\Platform\Tasks\Doctor;
|
||||
@@ -12,6 +13,8 @@ use Appwrite\Platform\Tasks\Install;
|
||||
use Appwrite\Platform\Tasks\Maintenance;
|
||||
use Appwrite\Platform\Tasks\Migrate;
|
||||
use Appwrite\Platform\Tasks\PatchRecreateRepositoriesDocuments;
|
||||
use Appwrite\Platform\Tasks\QueueCount;
|
||||
use Appwrite\Platform\Tasks\QueueRetry;
|
||||
use Appwrite\Platform\Tasks\SDKs;
|
||||
use Appwrite\Platform\Tasks\SSL;
|
||||
use Appwrite\Platform\Tasks\ScheduleFunctions;
|
||||
@@ -21,7 +24,6 @@ use Appwrite\Platform\Tasks\Upgrade;
|
||||
use Appwrite\Platform\Tasks\Vars;
|
||||
use Appwrite\Platform\Tasks\Version;
|
||||
use Appwrite\Platform\Tasks\VolumeSync;
|
||||
use Appwrite\Platform\Tasks\CreateInfMetric;
|
||||
use Utopia\Platform\Service;
|
||||
|
||||
class Tasks extends Service
|
||||
@@ -30,19 +32,8 @@ class Tasks extends Service
|
||||
{
|
||||
$this->type = self::TYPE_CLI;
|
||||
$this
|
||||
->addAction(Version::getName(), new Version())
|
||||
->addAction(Vars::getName(), new Vars())
|
||||
->addAction(SSL::getName(), new SSL())
|
||||
->addAction(Hamster::getName(), new Hamster())
|
||||
->addAction(Doctor::getName(), new Doctor())
|
||||
->addAction(Install::getName(), new Install())
|
||||
->addAction(Upgrade::getName(), new Upgrade())
|
||||
->addAction(Maintenance::getName(), new Maintenance())
|
||||
->addAction(Migrate::getName(), new Migrate())
|
||||
->addAction(SDKs::getName(), new SDKs())
|
||||
->addAction(VolumeSync::getName(), new VolumeSync())
|
||||
->addAction(Specs::getName(), new Specs())
|
||||
->addAction(CalcTierStats::getName(), new CalcTierStats())
|
||||
->addAction(CreateInfMetric::getName(), new CreateInfMetric())
|
||||
->addAction(DeleteOrphanedProjects::getName(), new DeleteOrphanedProjects())
|
||||
->addAction(DevGenerateTranslations::getName(), new DevGenerateTranslations())
|
||||
->addAction(Doctor::getName(), new Doctor())
|
||||
@@ -51,7 +42,10 @@ class Tasks extends Service
|
||||
->addAction(Install::getName(), new Install())
|
||||
->addAction(Maintenance::getName(), new Maintenance())
|
||||
->addAction(Migrate::getName(), new Migrate())
|
||||
->addAction(Migrate::getName(), new Migrate())
|
||||
->addAction(PatchRecreateRepositoriesDocuments::getName(), new PatchRecreateRepositoriesDocuments())
|
||||
->addAction(QueueCount::getName(), new QueueCount())
|
||||
->addAction(QueueRetry::getName(), new QueueRetry())
|
||||
->addAction(SDKs::getName(), new SDKs())
|
||||
->addAction(SSL::getName(), new SSL())
|
||||
->addAction(ScheduleFunctions::getName(), new ScheduleFunctions())
|
||||
@@ -61,7 +55,6 @@ class Tasks extends Service
|
||||
->addAction(Vars::getName(), new Vars())
|
||||
->addAction(Version::getName(), new Version())
|
||||
->addAction(VolumeSync::getName(), new VolumeSync())
|
||||
->addAction(CreateInfMetric::getName(), new CreateInfMetric())
|
||||
;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,7 +2,6 @@
|
||||
|
||||
namespace Appwrite\Platform\Tasks;
|
||||
|
||||
use Exception;
|
||||
use League\Csv\CannotInsertRecord;
|
||||
use Utopia\App;
|
||||
use Utopia\Database\Document;
|
||||
@@ -200,7 +199,7 @@ class CalcTierStats extends Action
|
||||
$mail->Body = "Please find the daily cloud report atttached";
|
||||
$mail->send();
|
||||
Console::success('Email has been sent!');
|
||||
} catch (Exception $e) {
|
||||
} catch (\Throwable $e) {
|
||||
Console::error("Message could not be sent. Mailer Error: {$mail->ErrorInfo}");
|
||||
}
|
||||
}
|
||||
@@ -270,7 +269,7 @@ class CalcTierStats extends Action
|
||||
$limit = $periods[$range]['limit'];
|
||||
$period = $periods[$range]['period'];
|
||||
|
||||
$requestDocs = $dbForProject->find('stats_v2', [
|
||||
$requestDocs = $dbForProject->find('stats', [
|
||||
Query::equal('metric', [$metric]),
|
||||
Query::equal('period', [$period]),
|
||||
Query::limit($limit),
|
||||
|
||||
@@ -163,8 +163,8 @@ class CreateInfMetric extends Action
|
||||
|
||||
try {
|
||||
$id = \md5("_inf_{$metric}");
|
||||
$dbForProject->deleteDocument('stats_v2', $id);
|
||||
$dbForProject->createDocument('stats_v2', new Document([
|
||||
$dbForProject->deleteDocument('stats', $id);
|
||||
$dbForProject->createDocument('stats', new Document([
|
||||
'$id' => $id,
|
||||
'metric' => $metric,
|
||||
'period' => 'inf',
|
||||
@@ -186,7 +186,7 @@ class CreateInfMetric extends Action
|
||||
protected function getFromMetric(database $dbForProject, string $metric): int|float
|
||||
{
|
||||
|
||||
return $dbForProject->sum('stats_v2', 'value', [
|
||||
return $dbForProject->sum('stats', 'value', [
|
||||
Query::equal('metric', [
|
||||
$metric,
|
||||
]),
|
||||
|
||||
@@ -2,7 +2,6 @@
|
||||
|
||||
namespace Appwrite\Platform\Tasks;
|
||||
|
||||
use Exception;
|
||||
use League\Csv\CannotInsertRecord;
|
||||
use Utopia\App;
|
||||
use Utopia\Platform\Action;
|
||||
@@ -180,7 +179,7 @@ class GetMigrationStats extends Action
|
||||
$mail->Body = "Please find the migration report atttached";
|
||||
$mail->send();
|
||||
Console::success('Email has been sent!');
|
||||
} catch (Exception $e) {
|
||||
} catch (\Throwable $e) {
|
||||
Console::error("Message could not be sent. Mailer Error: {$mail->ErrorInfo}");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,7 +3,6 @@
|
||||
namespace Appwrite\Platform\Tasks;
|
||||
|
||||
use Appwrite\Event\Hamster as EventHamster;
|
||||
use Exception;
|
||||
use Utopia\App;
|
||||
use Utopia\Platform\Action;
|
||||
use Utopia\CLI\Console;
|
||||
@@ -120,7 +119,7 @@ class Hamster extends Action
|
||||
->setType(EventHamster::TYPE_ORGANISATION)
|
||||
->setOrganization($organization)
|
||||
->trigger();
|
||||
} catch (Exception $e) {
|
||||
} catch (\Throwable $e) {
|
||||
Console::error($e->getMessage());
|
||||
}
|
||||
});
|
||||
@@ -135,7 +134,7 @@ class Hamster extends Action
|
||||
->setType(EventHamster::TYPE_PROJECT)
|
||||
->setProject($project)
|
||||
->trigger();
|
||||
} catch (Exception $e) {
|
||||
} catch (\Throwable $e) {
|
||||
Console::error($e->getMessage());
|
||||
}
|
||||
});
|
||||
@@ -150,7 +149,7 @@ class Hamster extends Action
|
||||
->setType(EventHamster::TYPE_USER)
|
||||
->setUser($user)
|
||||
->trigger();
|
||||
} catch (Exception $e) {
|
||||
} catch (\Throwable $e) {
|
||||
Console::error($e->getMessage());
|
||||
}
|
||||
});
|
||||
|
||||
@@ -36,6 +36,7 @@ class Maintenance extends Action
|
||||
|
||||
// # of days in seconds (1 day = 86400s)
|
||||
$interval = (int) App::getEnv('_APP_MAINTENANCE_INTERVAL', '86400');
|
||||
$delay = (int) App::getEnv('_APP_MAINTENANCE_DELAY', '0');
|
||||
$usageStatsRetentionHourly = (int) App::getEnv('_APP_MAINTENANCE_RETENTION_USAGE_HOURLY', '8640000'); //100 days
|
||||
$cacheRetention = (int) App::getEnv('_APP_MAINTENANCE_RETENTION_CACHE', '2592000'); // 30 days
|
||||
$schedulesDeletionRetention = (int) App::getEnv('_APP_MAINTENANCE_RETENTION_SCHEDULES', '86400'); // 1 Day
|
||||
@@ -60,7 +61,7 @@ class Maintenance extends Action
|
||||
$this->notifyDeleteCache($cacheRetention, $queueForDeletes);
|
||||
$this->notifyDeleteSchedules($schedulesDeletionRetention, $queueForDeletes);
|
||||
$this->notifyDeleteTargets($queueForDeletes);
|
||||
}, $interval);
|
||||
}, $interval, $delay);
|
||||
}
|
||||
|
||||
protected function foreachProject(Database $dbForConsole, callable $callback): void
|
||||
|
||||
@@ -0,0 +1,70 @@
|
||||
<?php
|
||||
|
||||
namespace Appwrite\Platform\Tasks;
|
||||
|
||||
use Appwrite\Event\Event;
|
||||
use Utopia\CLI\Console;
|
||||
use Utopia\Platform\Action;
|
||||
use Utopia\Queue\Client;
|
||||
use Utopia\Queue\Connection;
|
||||
use Utopia\Validator\WhiteList;
|
||||
|
||||
class QueueCount extends Action
|
||||
{
|
||||
public static function getName(): string
|
||||
{
|
||||
return 'queue-count';
|
||||
}
|
||||
|
||||
|
||||
public function __construct()
|
||||
{
|
||||
$this
|
||||
->desc('Return the number of from a specific queue identified by the name parameter with a specific type')
|
||||
->param('name', '', new WhiteList([
|
||||
Event::DATABASE_QUEUE_NAME,
|
||||
Event::DELETE_QUEUE_NAME,
|
||||
Event::AUDITS_QUEUE_NAME,
|
||||
Event::MAILS_QUEUE_NAME,
|
||||
Event::FUNCTIONS_QUEUE_NAME,
|
||||
Event::USAGE_QUEUE_NAME,
|
||||
Event::WEBHOOK_QUEUE_NAME,
|
||||
Event::CERTIFICATES_QUEUE_NAME,
|
||||
Event::BUILDS_QUEUE_NAME,
|
||||
Event::MESSAGING_QUEUE_NAME,
|
||||
Event::MIGRATIONS_QUEUE_NAME,
|
||||
Event::HAMSTER_QUEUE_NAME
|
||||
]), 'Queue name')
|
||||
->param('type', '', new WhiteList([
|
||||
'success',
|
||||
'failed',
|
||||
'processing',
|
||||
]), 'Queue type')
|
||||
->inject('queue')
|
||||
->callback(fn ($name, $type, $queue) => $this->action($name, $type, $queue));
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $name The name of the queue to count the jobs from
|
||||
* @param string $type The type of jobs to count
|
||||
* @param Connection $queue
|
||||
*/
|
||||
public function action(string $name, string $type, Connection $queue): void
|
||||
{
|
||||
if (!$name) {
|
||||
Console::error('Missing required parameter $name');
|
||||
return;
|
||||
}
|
||||
|
||||
$queueClient = new Client($name, $queue);
|
||||
|
||||
$count = match ($type) {
|
||||
'success' => $queueClient->countSuccessfulJobs(),
|
||||
'failed' => $queueClient->countFailedJobs(),
|
||||
'processing' => $queueClient->countProcessingJobs(),
|
||||
default => 0
|
||||
};
|
||||
|
||||
Console::log("Queue: '{$name}' has {$count} {$type} jobs.");
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,64 @@
|
||||
<?php
|
||||
|
||||
namespace Appwrite\Platform\Tasks;
|
||||
|
||||
use Appwrite\Event\Event;
|
||||
use Utopia\CLI\Console;
|
||||
use Utopia\Platform\Action;
|
||||
use Utopia\Queue\Client;
|
||||
use Utopia\Queue\Connection;
|
||||
use Utopia\Validator\WhiteList;
|
||||
|
||||
class QueueRetry extends Action
|
||||
{
|
||||
public static function getName(): string
|
||||
{
|
||||
return 'queue-retry';
|
||||
}
|
||||
|
||||
|
||||
public function __construct()
|
||||
{
|
||||
$this
|
||||
->desc('Retry failed jobs from a specific queue identified by the name parameter')
|
||||
->param('name', '', new WhiteList([
|
||||
Event::DATABASE_QUEUE_NAME,
|
||||
Event::DELETE_QUEUE_NAME,
|
||||
Event::AUDITS_QUEUE_NAME,
|
||||
Event::MAILS_QUEUE_NAME,
|
||||
Event::FUNCTIONS_QUEUE_NAME,
|
||||
Event::USAGE_QUEUE_NAME,
|
||||
Event::WEBHOOK_CLASS_NAME,
|
||||
Event::CERTIFICATES_QUEUE_NAME,
|
||||
Event::BUILDS_QUEUE_NAME,
|
||||
Event::MESSAGING_QUEUE_NAME,
|
||||
Event::MIGRATIONS_QUEUE_NAME,
|
||||
Event::HAMSTER_CLASS_NAME
|
||||
]), 'Queue name')
|
||||
->inject('queue')
|
||||
->callback(fn ($name, $queue) => $this->action($name, $queue));
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $name The name of the queue to retry jobs from
|
||||
* @param Connection $queue
|
||||
*/
|
||||
public function action(string $name, Connection $queue): void
|
||||
{
|
||||
if (!$name) {
|
||||
Console::error('Missing required parameter $name');
|
||||
return;
|
||||
}
|
||||
|
||||
$queueClient = new Client($name, $queue);
|
||||
|
||||
if ($queueClient->countFailedJobs() === 0) {
|
||||
Console::error('No failed jobs found.');
|
||||
return;
|
||||
}
|
||||
|
||||
Console::log('Retrying failed jobs...');
|
||||
|
||||
$queueClient->retry();
|
||||
}
|
||||
}
|
||||
@@ -18,8 +18,6 @@ use Appwrite\SDK\Language\Python;
|
||||
use Appwrite\SDK\Language\REST;
|
||||
use Appwrite\SDK\Language\Ruby;
|
||||
use Appwrite\SDK\Language\Swift;
|
||||
use Exception;
|
||||
use Throwable;
|
||||
use Appwrite\SDK\Language\Apple;
|
||||
use Appwrite\SDK\Language\Web;
|
||||
use Appwrite\SDK\SDK;
|
||||
@@ -242,9 +240,7 @@ THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
|
||||
|
||||
try {
|
||||
$sdk->generate($result);
|
||||
} catch (Exception $exception) {
|
||||
Console::error($exception->getMessage());
|
||||
} catch (Throwable $exception) {
|
||||
} catch (\Throwable $exception) {
|
||||
Console::error($exception->getMessage());
|
||||
}
|
||||
|
||||
|
||||
@@ -7,6 +7,7 @@ use Appwrite\Event\Certificate;
|
||||
use Utopia\App;
|
||||
use Utopia\CLI\Console;
|
||||
use Utopia\Database\Document;
|
||||
use Utopia\Validator\Boolean;
|
||||
use Utopia\Validator\Hostname;
|
||||
|
||||
class SSL extends Action
|
||||
@@ -21,19 +22,22 @@ class SSL extends Action
|
||||
$this
|
||||
->desc('Validate server certificates')
|
||||
->param('domain', App::getEnv('_APP_DOMAIN', ''), new Hostname(), 'Domain to generate certificate for. If empty, main domain will be used.', true)
|
||||
->param('skip-check', true, new Boolean(true), 'If DNS and renew check should be skipped. Defaults to true, and when true, all jobs will result in certificate generation attempt.', true)
|
||||
->inject('queueForCertificates')
|
||||
->callback(fn (string $domain, Certificate $queueForCertificates) => $this->action($domain, $queueForCertificates));
|
||||
->callback(fn (string $domain, bool|string $skipCheck, Certificate $queueForCertificates) => $this->action($domain, $skipCheck, $queueForCertificates));
|
||||
}
|
||||
|
||||
public function action(string $domain, Certificate $queueForCertificates): void
|
||||
public function action(string $domain, bool|string $skipCheck, Certificate $queueForCertificates): void
|
||||
{
|
||||
$skipCheck = \strval($skipCheck) === 'true';
|
||||
|
||||
Console::success('Scheduling a job to issue a TLS certificate for domain: ' . $domain);
|
||||
|
||||
$queueForCertificates
|
||||
->setDomain(new Document([
|
||||
'domain' => $domain
|
||||
]))
|
||||
->setSkipRenewCheck(true)
|
||||
->setSkipRenewCheck($skipCheck)
|
||||
->trigger();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -50,6 +50,7 @@ class ScheduleMessages extends ScheduleBase
|
||||
$queueForMessaging = new Messaging($connection);
|
||||
|
||||
$queueForMessaging
|
||||
->setType(MESSAGE_SEND_TYPE_EXTERNAL)
|
||||
->setMessageId($schedule['resourceId'])
|
||||
->setProject($schedule['project'])
|
||||
->trigger();
|
||||
|
||||
@@ -8,7 +8,6 @@ use Appwrite\Event\Usage;
|
||||
use Appwrite\Messaging\Adapter\Realtime;
|
||||
use Appwrite\Utopia\Response\Model\Deployment;
|
||||
use Appwrite\Vcs\Comment;
|
||||
use Exception;
|
||||
use Swoole\Coroutine as Co;
|
||||
use Executor\Executor;
|
||||
use Utopia\App;
|
||||
@@ -26,6 +25,7 @@ use Utopia\Database\Helpers\ID;
|
||||
use Utopia\Logger\Log;
|
||||
use Utopia\Platform\Action;
|
||||
use Utopia\Queue\Message;
|
||||
use Utopia\Storage\Device;
|
||||
use Utopia\Storage\Device\Local;
|
||||
use Utopia\VCS\Adapter\Git\GitHub;
|
||||
|
||||
@@ -50,9 +50,9 @@ class Builds extends Action
|
||||
->inject('queueForUsage')
|
||||
->inject('cache')
|
||||
->inject('dbForProject')
|
||||
->inject('getFunctionsDevice')
|
||||
->inject('functionsDevice')
|
||||
->inject('log')
|
||||
->callback(fn($message, Database $dbForConsole, Event $queueForEvents, Func $queueForFunctions, Usage $usage, Cache $cache, Database $dbForProject, callable $getFunctionsDevice, Log $log) => $this->action($message, $dbForConsole, $queueForEvents, $queueForFunctions, $usage, $cache, $dbForProject, $getFunctionsDevice, $log));
|
||||
->callback(fn($message, Database $dbForConsole, Event $queueForEvents, Func $queueForFunctions, Usage $usage, Cache $cache, Database $dbForProject, Device $functionsDevice, Log $log) => $this->action($message, $dbForConsole, $queueForEvents, $queueForFunctions, $usage, $cache, $dbForProject, $functionsDevice, $log));
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -63,12 +63,12 @@ class Builds extends Action
|
||||
* @param Usage $queueForUsage
|
||||
* @param Cache $cache
|
||||
* @param Database $dbForProject
|
||||
* @param callable $getFunctionsDevice
|
||||
* @param Device $functionsDevice
|
||||
* @param Log $log
|
||||
* @return void
|
||||
* @throws \Utopia\Database\Exception
|
||||
*/
|
||||
public function action(Message $message, Database $dbForConsole, Event $queueForEvents, Func $queueForFunctions, Usage $queueForUsage, Cache $cache, Database $dbForProject, callable $getFunctionsDevice, Log $log): void
|
||||
public function action(Message $message, Database $dbForConsole, Event $queueForEvents, Func $queueForFunctions, Usage $queueForUsage, Cache $cache, Database $dbForProject, Device $functionsDevice, Log $log): void
|
||||
{
|
||||
$payload = $message->getPayload() ?? [];
|
||||
|
||||
@@ -90,7 +90,7 @@ class Builds extends Action
|
||||
case BUILD_TYPE_RETRY:
|
||||
Console::info('Creating build for deployment: ' . $deployment->getId());
|
||||
$github = new GitHub($cache);
|
||||
$this->buildDeployment($getFunctionsDevice, $queueForFunctions, $queueForEvents, $queueForUsage, $dbForConsole, $dbForProject, $github, $project, $resource, $deployment, $template, $log);
|
||||
$this->buildDeployment($functionsDevice, $queueForFunctions, $queueForEvents, $queueForUsage, $dbForConsole, $dbForProject, $github, $project, $resource, $deployment, $template, $log);
|
||||
break;
|
||||
|
||||
default:
|
||||
@@ -99,7 +99,7 @@ class Builds extends Action
|
||||
}
|
||||
|
||||
/**
|
||||
* @param callable $getFunctionsDevice
|
||||
* @param Device $functionsDevice
|
||||
* @param Func $queueForFunctions
|
||||
* @param Event $queueForEvents
|
||||
* @param Usage $queueForUsage
|
||||
@@ -115,7 +115,7 @@ class Builds extends Action
|
||||
* @throws \Utopia\Database\Exception
|
||||
* @throws Exception
|
||||
*/
|
||||
protected function buildDeployment(callable $getFunctionsDevice, Func $queueForFunctions, Event $queueForEvents, Usage $queueForUsage, Database $dbForConsole, Database $dbForProject, GitHub $github, Document $project, Document $function, Document $deployment, Document $template, Log $log): void
|
||||
protected function buildDeployment(Device $functionsDevice, Func $queueForFunctions, Event $queueForEvents, Usage $queueForUsage, Database $dbForConsole, Database $dbForProject, GitHub $github, Document $project, Document $function, Document $deployment, Document $template, Log $log): void
|
||||
{
|
||||
$executor = new Executor(App::getEnv('_APP_EXECUTOR_HOST'));
|
||||
|
||||
@@ -157,7 +157,6 @@ class Builds extends Action
|
||||
$durationStart = \microtime(true);
|
||||
$buildId = $deployment->getAttribute('buildId', '');
|
||||
$isNewBuild = empty($buildId);
|
||||
$deviceFunctions = $getFunctionsDevice($project->getId());
|
||||
|
||||
if ($isNewBuild) {
|
||||
$buildId = ID::unique();
|
||||
@@ -171,7 +170,7 @@ class Builds extends Action
|
||||
'path' => '',
|
||||
'runtime' => $function->getAttribute('runtime'),
|
||||
'source' => $deployment->getAttribute('path', ''),
|
||||
'sourceType' => strtolower($deviceFunctions->getType()),
|
||||
'sourceType' => strtolower($functionsDevice->getType()),
|
||||
'logs' => '',
|
||||
'endTime' => null,
|
||||
'duration' => 0,
|
||||
@@ -189,7 +188,7 @@ class Builds extends Action
|
||||
$installationId = $deployment->getAttribute('installationId', '');
|
||||
$providerRepositoryId = $deployment->getAttribute('providerRepositoryId', '');
|
||||
$providerCommitHash = $deployment->getAttribute('providerCommitHash', '');
|
||||
$isVcsEnabled = $providerRepositoryId ? true : false;
|
||||
$isVcsEnabled = !empty($providerRepositoryId);
|
||||
$owner = '';
|
||||
$repositoryName = '';
|
||||
|
||||
@@ -312,10 +311,8 @@ class Builds extends Action
|
||||
|
||||
Console::execute('tar --exclude code.tar.gz -czf ' . $tmpPathFile . ' -C /tmp/builds/' . \escapeshellcmd($buildId) . '/code' . (empty($rootDirectory) ? '' : '/' . $rootDirectory) . ' .', '', $stdout, $stderr);
|
||||
|
||||
$deviceFunctions = $getFunctionsDevice($project->getId());
|
||||
|
||||
$path = $deviceFunctions->getPath($deployment->getId() . '.' . \pathinfo('code.tar.gz', PATHINFO_EXTENSION));
|
||||
$result = $localDevice->transfer($tmpPathFile, $path, $deviceFunctions);
|
||||
$path = $functionsDevice->getPath($deployment->getId() . '.' . \pathinfo('code.tar.gz', PATHINFO_EXTENSION));
|
||||
$result = $localDevice->transfer($tmpPathFile, $path, $functionsDevice);
|
||||
|
||||
if (!$result) {
|
||||
throw new \Exception("Unable to move file");
|
||||
@@ -420,7 +417,7 @@ class Builds extends Action
|
||||
variables: $vars,
|
||||
command: $command
|
||||
);
|
||||
} catch (Exception $error) {
|
||||
} catch (\Throwable $error) {
|
||||
$err = $error;
|
||||
}
|
||||
}),
|
||||
@@ -459,7 +456,7 @@ class Builds extends Action
|
||||
}
|
||||
}
|
||||
);
|
||||
} catch (Exception $error) {
|
||||
} catch (\Throwable $error) {
|
||||
if (empty($err)) {
|
||||
$err = $error;
|
||||
}
|
||||
@@ -617,7 +614,7 @@ class Builds extends Action
|
||||
'$id' => $commentId
|
||||
]));
|
||||
break;
|
||||
} catch (Exception $err) {
|
||||
} catch (\Throwable $err) {
|
||||
if ($retries >= 9) {
|
||||
throw $err;
|
||||
}
|
||||
|
||||
@@ -75,7 +75,7 @@ class Certificates extends Action
|
||||
|
||||
$log->addTag('domain', $domain->get());
|
||||
|
||||
$this->execute($domain, $dbForConsole, $queueForMails, $queueForEvents, $queueForFunctions, $skipRenewCheck);
|
||||
$this->execute($domain, $dbForConsole, $queueForMails, $queueForEvents, $queueForFunctions, $log, $skipRenewCheck);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -89,7 +89,7 @@ class Certificates extends Action
|
||||
* @throws Throwable
|
||||
* @throws \Utopia\Database\Exception
|
||||
*/
|
||||
private function execute(Domain $domain, Database $dbForConsole, Mail $queueForMails, Event $queueForEvents, Func $queueForFunctions, bool $skipRenewCheck = false): void
|
||||
private function execute(Domain $domain, Database $dbForConsole, Mail $queueForMails, Event $queueForEvents, Func $queueForFunctions, Log $log, bool $skipRenewCheck = false): void
|
||||
{
|
||||
/**
|
||||
* 1. Read arguments and validate domain
|
||||
@@ -143,11 +143,11 @@ class Certificates extends Action
|
||||
if (!$skipRenewCheck) {
|
||||
$mainDomain = $this->getMainDomain();
|
||||
$isMainDomain = !isset($mainDomain) || $domain->get() === $mainDomain;
|
||||
$this->validateDomain($domain, $isMainDomain);
|
||||
$this->validateDomain($domain, $isMainDomain, $log);
|
||||
}
|
||||
|
||||
// If certificate exists already, double-check expiry date. Skip if job is forced
|
||||
if (!$skipRenewCheck && !$this->isRenewRequired($domain->get())) {
|
||||
if (!$skipRenewCheck && !$this->isRenewRequired($domain->get(), $log)) {
|
||||
throw new Exception('Renew isn\'t required.');
|
||||
}
|
||||
|
||||
@@ -185,6 +185,8 @@ class Certificates extends Action
|
||||
|
||||
// Send email to security email
|
||||
$this->notifyError($domain->get(), $e->getMessage(), $attempts, $queueForMails);
|
||||
|
||||
throw $e;
|
||||
} finally {
|
||||
// All actions result in new updatedAt date
|
||||
$certificate->setAttribute('updated', DateTime::now());
|
||||
@@ -252,7 +254,7 @@ class Certificates extends Action
|
||||
* @return void
|
||||
* @throws Exception
|
||||
*/
|
||||
private function validateDomain(Domain $domain, bool $isMainDomain): void
|
||||
private function validateDomain(Domain $domain, bool $isMainDomain, Log $log): void
|
||||
{
|
||||
if (empty($domain->get())) {
|
||||
throw new Exception('Missing certificate domain.');
|
||||
@@ -272,8 +274,15 @@ class Certificates extends Action
|
||||
}
|
||||
|
||||
// Verify domain with DNS records
|
||||
$validationStart = \microtime(true);
|
||||
$validator = new CNAME($target->get());
|
||||
if (!$validator->isValid($domain->get())) {
|
||||
$log->addExtra('dnsTiming', \strval(\microtime(true) - $validationStart));
|
||||
$log->addTag('dnsDomain', $domain->get());
|
||||
|
||||
$error = $validator->getLogs();
|
||||
$log->addExtra('dnsResponse', \is_array($error) ? \json_encode($error) : \strval($error));
|
||||
|
||||
throw new Exception('Failed to verify domain DNS records.');
|
||||
}
|
||||
} else {
|
||||
@@ -289,7 +298,7 @@ class Certificates extends Action
|
||||
* @return bool True, if certificate needs to be renewed
|
||||
* @throws Exception
|
||||
*/
|
||||
private function isRenewRequired(string $domain): bool
|
||||
private function isRenewRequired(string $domain, Log $log): bool
|
||||
{
|
||||
$certPath = APP_STORAGE_CERTIFICATES . '/' . $domain . '/cert.pem';
|
||||
if (\file_exists($certPath)) {
|
||||
@@ -299,12 +308,15 @@ class Certificates extends Action
|
||||
$validTo = $certData['validTo_time_t'] ?? 0;
|
||||
|
||||
if (empty($validTo)) {
|
||||
$log->addTag('certificateDomain', $domain);
|
||||
throw new Exception('Unable to read certificate file (cert.pem).');
|
||||
}
|
||||
|
||||
// LetsEncrypt allows renewal 30 days before expiry
|
||||
$expiryInAdvance = (60 * 60 * 24 * 30);
|
||||
if ($validTo - $expiryInAdvance > \time()) {
|
||||
$log->addTag('certificateDomain', $domain);
|
||||
$log->addExtra('certificateData', \is_array($certData) ? \json_encode($certData) : \strval($certData));
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -165,7 +165,7 @@ class Databases extends Action
|
||||
}
|
||||
|
||||
$dbForProject->updateDocument('attributes', $attribute->getId(), $attribute->setAttribute('status', 'available'));
|
||||
} catch (\Exception $e) {
|
||||
} catch (\Throwable $e) {
|
||||
// TODO: Send non DatabaseExceptions to Sentry
|
||||
Console::error($e->getMessage());
|
||||
|
||||
@@ -269,7 +269,7 @@ class Databases extends Action
|
||||
if (!$relatedAttribute->isEmpty()) {
|
||||
$dbForProject->deleteDocument('attributes', $relatedAttribute->getId());
|
||||
}
|
||||
} catch (\Exception $e) {
|
||||
} catch (\Throwable $e) {
|
||||
// TODO: Send non DatabaseExceptions to Sentry
|
||||
Console::error($e->getMessage());
|
||||
|
||||
@@ -397,7 +397,7 @@ class Databases extends Action
|
||||
throw new DatabaseException('Failed to create Index');
|
||||
}
|
||||
$dbForProject->updateDocument('indexes', $index->getId(), $index->setAttribute('status', 'available'));
|
||||
} catch (\Exception $e) {
|
||||
} catch (\Throwable $e) {
|
||||
// TODO: Send non DatabaseExceptions to Sentry
|
||||
Console::error($e->getMessage());
|
||||
|
||||
@@ -455,7 +455,7 @@ class Databases extends Action
|
||||
}
|
||||
$dbForProject->deleteDocument('indexes', $index->getId());
|
||||
$index->setAttribute('status', 'deleted');
|
||||
} catch (\Exception $e) {
|
||||
} catch (\Throwable $e) {
|
||||
// TODO: Send non DatabaseExceptions to Sentry
|
||||
Console::error($e->getMessage());
|
||||
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
namespace Appwrite\Platform\Workers;
|
||||
|
||||
use Appwrite\Auth\Auth;
|
||||
use Appwrite\Extend\Exception;
|
||||
use Executor\Executor;
|
||||
use Throwable;
|
||||
use Utopia\Abuse\Abuse;
|
||||
@@ -11,7 +12,6 @@ use Utopia\Audit\Audit;
|
||||
use Utopia\Cache\Adapter\Filesystem;
|
||||
use Utopia\Cache\Cache;
|
||||
use Utopia\Database\Database;
|
||||
use Exception;
|
||||
use Utopia\App;
|
||||
use Utopia\CLI\Console;
|
||||
use Utopia\Database\DateTime;
|
||||
@@ -44,22 +44,22 @@ class Deletes extends Action
|
||||
->inject('message')
|
||||
->inject('dbForConsole')
|
||||
->inject('getProjectDB')
|
||||
->inject('getFilesDevice')
|
||||
->inject('getFunctionsDevice')
|
||||
->inject('getBuildsDevice')
|
||||
->inject('getCacheDevice')
|
||||
->inject('filesDevice')
|
||||
->inject('functionsDevice')
|
||||
->inject('buildsDevice')
|
||||
->inject('cacheDevice')
|
||||
->inject('abuseRetention')
|
||||
->inject('executionRetention')
|
||||
->inject('auditRetention')
|
||||
->inject('log')
|
||||
->callback(fn ($message, $dbForConsole, callable $getProjectDB, callable $getFilesDevice, callable $getFunctionsDevice, callable $getBuildsDevice, callable $getCacheDevice, string $abuseRetention, string $executionRetention, string $auditRetention, Log $log) => $this->action($message, $dbForConsole, $getProjectDB, $getFilesDevice, $getFunctionsDevice, $getBuildsDevice, $getCacheDevice, $abuseRetention, $executionRetention, $auditRetention, $log));
|
||||
->callback(fn ($message, $dbForConsole, callable $getProjectDB, Device $filesDevice, Device $functionsDevice, Device $buildsDevice, Device $cacheDevice, string $abuseRetention, string $executionRetention, string $auditRetention, Log $log) => $this->action($message, $dbForConsole, $getProjectDB, $filesDevice, $functionsDevice, $buildsDevice, $cacheDevice, $abuseRetention, $executionRetention, $auditRetention, $log));
|
||||
}
|
||||
|
||||
/**
|
||||
* @throws Exception
|
||||
* @throws Throwable
|
||||
*/
|
||||
public function action(Message $message, Database $dbForConsole, callable $getProjectDB, callable $getFilesDevice, callable $getFunctionsDevice, callable $getBuildsDevice, callable $getCacheDevice, string $abuseRetention, string $executionRetention, string $auditRetention, Log $log): void
|
||||
public function action(Message $message, Database $dbForConsole, callable $getProjectDB, Device $filesDevice, Device $functionsDevice, Device $buildsDevice, Device $cacheDevice, string $abuseRetention, string $executionRetention, string $auditRetention, Log $log): void
|
||||
{
|
||||
$payload = $message->getPayload() ?? [];
|
||||
|
||||
@@ -87,13 +87,13 @@ class Deletes extends Action
|
||||
$this->deleteCollection($getProjectDB, $document, $project);
|
||||
break;
|
||||
case DELETE_TYPE_PROJECTS:
|
||||
$this->deleteProject($dbForConsole, $getProjectDB, $getFilesDevice, $getFunctionsDevice, $getBuildsDevice, $getCacheDevice, $document);
|
||||
$this->deleteProject($dbForConsole, $getProjectDB, $filesDevice, $functionsDevice, $buildsDevice, $cacheDevice, $document);
|
||||
break;
|
||||
case DELETE_TYPE_FUNCTIONS:
|
||||
$this->deleteFunction($dbForConsole, $getProjectDB, $getFunctionsDevice, $getBuildsDevice, $document, $project);
|
||||
$this->deleteFunction($dbForConsole, $getProjectDB, $functionsDevice, $buildsDevice, $document, $project);
|
||||
break;
|
||||
case DELETE_TYPE_DEPLOYMENTS:
|
||||
$this->deleteDeployment($getProjectDB, $getFunctionsDevice, $getBuildsDevice, $document, $project);
|
||||
$this->deleteDeployment($getProjectDB, $functionsDevice, $buildsDevice, $document, $project);
|
||||
break;
|
||||
case DELETE_TYPE_USERS:
|
||||
$this->deleteUser($getProjectDB, $document, $project);
|
||||
@@ -101,11 +101,11 @@ class Deletes extends Action
|
||||
case DELETE_TYPE_TEAMS:
|
||||
$this->deleteMemberships($getProjectDB, $document, $project);
|
||||
if ($project->getId() === 'console') {
|
||||
$this->deleteProjectsByTeam($dbForConsole, $getProjectDB, $getFilesDevice, $getFunctionsDevice, $getBuildsDevice, $getCacheDevice, $document);
|
||||
$this->deleteProjectsByTeam($dbForConsole, $getProjectDB, $filesDevice, $functionsDevice, $buildsDevice, $cacheDevice, $document);
|
||||
}
|
||||
break;
|
||||
case DELETE_TYPE_BUCKETS:
|
||||
$this->deleteBucket($getProjectDB, $getFilesDevice, $document, $project);
|
||||
$this->deleteBucket($getProjectDB, $filesDevice, $document, $project);
|
||||
break;
|
||||
case DELETE_TYPE_INSTALLATIONS:
|
||||
$this->deleteInstallation($dbForConsole, $getProjectDB, $document, $project);
|
||||
@@ -264,12 +264,23 @@ class Deletes extends Action
|
||||
Query::equal('targetInternalId', [$target->getInternalId()])
|
||||
],
|
||||
$dbForProject,
|
||||
function (Document $subscriber) use ($dbForProject) {
|
||||
function (Document $subscriber) use ($dbForProject, $target) {
|
||||
$topicId = $subscriber->getAttribute('topicId');
|
||||
$topicInternalId = $subscriber->getAttribute('topicInternalId');
|
||||
$topic = $dbForProject->getDocument('topics', $topicId);
|
||||
if (!$topic->isEmpty() && $topic->getInternalId() === $topicInternalId) {
|
||||
$dbForProject->decreaseDocumentAttribute('topics', $topicId, 'total', min: 0);
|
||||
$totalAttribute = match ($target->getAttribute('providerType')) {
|
||||
MESSAGE_TYPE_EMAIL => 'emailTotal',
|
||||
MESSAGE_TYPE_SMS => 'smsTotal',
|
||||
MESSAGE_TYPE_PUSH => 'pushTotal',
|
||||
default => throw new Exception('Invalid target provider type'),
|
||||
};
|
||||
$dbForProject->decreaseDocumentAttribute(
|
||||
'topics',
|
||||
$topicId,
|
||||
$totalAttribute,
|
||||
min: 0
|
||||
);
|
||||
}
|
||||
}
|
||||
);
|
||||
@@ -458,7 +469,7 @@ class Deletes extends Action
|
||||
{
|
||||
$dbForProject = $getProjectDB($project);
|
||||
// Delete Usage stats
|
||||
$this->deleteByGroup('stats_v2', [
|
||||
$this->deleteByGroup('stats', [
|
||||
Query::lessThan('time', $hourlyUsageRetentionDatetime),
|
||||
Query::equal('period', ['1h']),
|
||||
], $dbForProject);
|
||||
@@ -500,14 +511,14 @@ class Deletes extends Action
|
||||
* @throws Restricted
|
||||
* @throws Structure
|
||||
*/
|
||||
private function deleteProjectsByTeam(Database $dbForConsole, callable $getProjectDB, callable $getFilesDevice, callable $getFunctionsDevice, callable $getBuildsDevice, callable $getCacheDevice, Document $document): void
|
||||
private function deleteProjectsByTeam(Database $dbForConsole, callable $getProjectDB, Device $filesDevice, Device $functionsDevice, Device $buildsDevice, Device $cacheDevice, Document $document): void
|
||||
{
|
||||
|
||||
$projects = $dbForConsole->find('projects', [
|
||||
Query::equal('teamInternalId', [$document->getInternalId()])
|
||||
]);
|
||||
foreach ($projects as $project) {
|
||||
$this->deleteProject($dbForConsole, $getProjectDB, $getFilesDevice, $getFunctionsDevice, $getBuildsDevice, $getCacheDevice, $project);
|
||||
$this->deleteProject($dbForConsole, $getProjectDB, $filesDevice, $functionsDevice, $buildsDevice, $cacheDevice, $project);
|
||||
$dbForConsole->deleteDocument('projects', $project->getId());
|
||||
}
|
||||
}
|
||||
@@ -515,17 +526,17 @@ class Deletes extends Action
|
||||
/**
|
||||
* @param Database $dbForConsole
|
||||
* @param callable $getProjectDB
|
||||
* @param callable $getFilesDevice
|
||||
* @param callable $getFunctionsDevice
|
||||
* @param callable $getBuildsDevice
|
||||
* @param callable $getCacheDevice
|
||||
* @param Device $filesDevice
|
||||
* @param Device $functionsDevice
|
||||
* @param Device $buildsDevice
|
||||
* @param Device $cacheDevice
|
||||
* @param Document $document
|
||||
* @return void
|
||||
* @throws Exception
|
||||
* @throws Authorization
|
||||
* @throws \Utopia\Database\Exception
|
||||
*/
|
||||
private function deleteProject(Database $dbForConsole, callable $getProjectDB, callable $getFilesDevice, callable $getFunctionsDevice, callable $getBuildsDevice, callable $getCacheDevice, Document $document): void
|
||||
private function deleteProject(Database $dbForConsole, callable $getProjectDB, Device $filesDevice, Device $functionsDevice, Device $buildsDevice, Device $cacheDevice, Document $document): void
|
||||
{
|
||||
$projectId = $document->getId();
|
||||
$projectInternalId = $document->getInternalId();
|
||||
@@ -585,21 +596,16 @@ class Deletes extends Action
|
||||
// Delete metadata tables
|
||||
try {
|
||||
$dbForProject->deleteCollection('_metadata');
|
||||
} catch (Exception) {
|
||||
} catch (\Throwable) {
|
||||
// Ignore: deleteCollection tries to delete a metadata entry after the collection is deleted,
|
||||
// which will throw an exception here because the metadata collection is already deleted.
|
||||
}
|
||||
|
||||
// Delete all storage directories
|
||||
$uploads = $getFilesDevice($projectId);
|
||||
$functions = $getFunctionsDevice($projectId);
|
||||
$builds = $getBuildsDevice($projectId);
|
||||
$cache = $getCacheDevice($projectId);
|
||||
|
||||
$uploads->delete($uploads->getRoot(), true);
|
||||
$functions->delete($functions->getRoot(), true);
|
||||
$builds->delete($builds->getRoot(), true);
|
||||
$cache->delete($cache->getRoot(), true);
|
||||
$filesDevice->delete($filesDevice->getRoot(), true);
|
||||
$functionsDevice->delete($functionsDevice->getRoot(), true);
|
||||
$buildsDevice->delete($buildsDevice->getRoot(), true);
|
||||
$cacheDevice->delete($cacheDevice->getRoot(), true);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -630,12 +636,7 @@ class Deletes extends Action
|
||||
$teamId = $document->getAttribute('teamId');
|
||||
$team = $dbForProject->getDocument('teams', $teamId);
|
||||
if (!$team->isEmpty()) {
|
||||
$team = $dbForProject->updateDocument(
|
||||
'teams',
|
||||
$teamId,
|
||||
// Ensure that total >= 0
|
||||
$team->setAttribute('total', \max($team->getAttribute('total', 0) - 1, 0))
|
||||
);
|
||||
$dbForProject->decreaseDocumentAttribute('teams', $teamId, 'total', 1, 0);
|
||||
}
|
||||
}
|
||||
});
|
||||
@@ -766,14 +767,14 @@ class Deletes extends Action
|
||||
|
||||
/**
|
||||
* @param callable $getProjectDB
|
||||
* @param callable $getFunctionsDevice
|
||||
* @param callable $getBuildsDevice
|
||||
* @param Device $functionsDevice
|
||||
* @param Device $buildsDevice
|
||||
* @param Document $document function document
|
||||
* @param Document $project
|
||||
* @return void
|
||||
* @throws Exception
|
||||
*/
|
||||
private function deleteFunction(Database $dbForConsole, callable $getProjectDB, callable $getFunctionsDevice, callable $getBuildsDevice, Document $document, Document $project): void
|
||||
private function deleteFunction(Database $dbForConsole, callable $getProjectDB, Device $functionsDevice, Device $buildsDevice, Document $document, Document $project): void
|
||||
{
|
||||
$projectId = $project->getId();
|
||||
$dbForProject = $getProjectDB($project);
|
||||
@@ -805,25 +806,25 @@ class Deletes extends Action
|
||||
* Delete Deployments
|
||||
*/
|
||||
Console::info("Deleting deployments for function " . $functionId);
|
||||
$functionsStorage = $getFunctionsDevice($projectId);
|
||||
|
||||
$deploymentInternalIds = [];
|
||||
$this->deleteByGroup('deployments', [
|
||||
Query::equal('resourceInternalId', [$functionInternalId])
|
||||
], $dbForProject, function (Document $document) use ($functionsStorage, &$deploymentInternalIds) {
|
||||
], $dbForProject, function (Document $document) use ($functionsDevice, &$deploymentInternalIds) {
|
||||
$deploymentInternalIds[] = $document->getInternalId();
|
||||
$this->deleteDeploymentFiles($functionsStorage, $document);
|
||||
$this->deleteDeploymentFiles($functionsDevice, $document);
|
||||
});
|
||||
|
||||
/**
|
||||
* Delete builds
|
||||
*/
|
||||
Console::info("Deleting builds for function " . $functionId);
|
||||
$buildsStorage = $getBuildsDevice($projectId);
|
||||
|
||||
foreach ($deploymentInternalIds as $deploymentInternalId) {
|
||||
$this->deleteByGroup('builds', [
|
||||
Query::equal('deploymentInternalId', [$deploymentInternalId])
|
||||
], $dbForProject, function (Document $document) use ($buildsStorage) {
|
||||
$this->deleteBuildFiles($buildsStorage, $document);
|
||||
], $dbForProject, function (Document $document) use ($buildsDevice) {
|
||||
$this->deleteBuildFiles($buildsDevice, $document);
|
||||
});
|
||||
}
|
||||
|
||||
@@ -923,14 +924,14 @@ class Deletes extends Action
|
||||
|
||||
/**
|
||||
* @param callable $getProjectDB
|
||||
* @param callable $getFunctionsDevice
|
||||
* @param callable $getBuildsDevice
|
||||
* @param Device $functionsDevice
|
||||
* @param Device $buildsDevice
|
||||
* @param Document $document
|
||||
* @param Document $project
|
||||
* @return void
|
||||
* @throws Exception
|
||||
*/
|
||||
private function deleteDeployment(callable $getProjectDB, callable $getFunctionsDevice, callable $getBuildsDevice, Document $document, Document $project): void
|
||||
private function deleteDeployment(callable $getProjectDB, Device $functionsDevice, Device $buildsDevice, Document $document, Document $project): void
|
||||
{
|
||||
$projectId = $project->getId();
|
||||
$dbForProject = $getProjectDB($project);
|
||||
@@ -940,18 +941,17 @@ class Deletes extends Action
|
||||
/**
|
||||
* Delete deployment files
|
||||
*/
|
||||
$functionsStorage = $getFunctionsDevice($projectId);
|
||||
$this->deleteDeploymentFiles($functionsStorage, $document);
|
||||
$this->deleteDeploymentFiles($functionsDevice, $document);
|
||||
|
||||
/**
|
||||
* Delete builds
|
||||
*/
|
||||
Console::info("Deleting builds for deployment " . $deploymentId);
|
||||
$buildsStorage = $getBuildsDevice($projectId);
|
||||
|
||||
$this->deleteByGroup('builds', [
|
||||
Query::equal('deploymentInternalId', [$deploymentInternalId])
|
||||
], $dbForProject, function (Document $document) use ($buildsStorage) {
|
||||
$this->deleteBuildFiles($buildsStorage, $document);
|
||||
], $dbForProject, function (Document $document) use ($buildsDevice) {
|
||||
$this->deleteBuildFiles($buildsDevice, $document);
|
||||
});
|
||||
|
||||
/**
|
||||
@@ -1095,21 +1095,18 @@ class Deletes extends Action
|
||||
|
||||
/**
|
||||
* @param callable $getProjectDB
|
||||
* @param callable $getFilesDevice
|
||||
* @param Device $filesDevice
|
||||
* @param Document $document
|
||||
* @param Document $project
|
||||
* @return void
|
||||
*/
|
||||
private function deleteBucket(callable $getProjectDB, callable $getFilesDevice, Document $document, Document $project): void
|
||||
private function deleteBucket(callable $getProjectDB, Device $filesDevice, Document $document, Document $project): void
|
||||
{
|
||||
$projectId = $project->getId();
|
||||
$dbForProject = $getProjectDB($project);
|
||||
|
||||
$dbForProject->deleteCollection('bucket_' . $document->getInternalId());
|
||||
|
||||
$device = $getFilesDevice($projectId);
|
||||
|
||||
$device->deletePath($document->getId());
|
||||
$filesDevice->deletePath($document->getId());
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -286,7 +286,7 @@ class Hamster extends Action
|
||||
$limit = $periodValue['limit'];
|
||||
$period = $periodValue['period'];
|
||||
|
||||
$requestDocs = $dbForProject->find('stats_v2', [
|
||||
$requestDocs = $dbForProject->find('stats', [
|
||||
Query::equal('period', [$period]),
|
||||
Query::equal('metric', [$metric]),
|
||||
Query::limit($limit),
|
||||
@@ -340,7 +340,7 @@ class Hamster extends Action
|
||||
if (!$res) {
|
||||
Console::error('Failed to create event for project: ' . $project->getId());
|
||||
}
|
||||
} catch (\Exception $e) {
|
||||
} catch (\Throwable $e) {
|
||||
Console::error('Failed to send stats for project: ' . $project->getId());
|
||||
Console::error($e->getMessage());
|
||||
} finally {
|
||||
@@ -410,7 +410,7 @@ class Hamster extends Action
|
||||
if (!$res) {
|
||||
throw new \Exception('Failed to create event for organization : ' . $organization->getId());
|
||||
}
|
||||
} catch (\Exception $e) {
|
||||
} catch (\Throwable $e) {
|
||||
Console::error($e->getMessage());
|
||||
}
|
||||
}
|
||||
@@ -464,7 +464,7 @@ class Hamster extends Action
|
||||
if (!$res) {
|
||||
throw new \Exception('Failed to create user profile for user: ' . $user->getId());
|
||||
}
|
||||
} catch (\Exception $e) {
|
||||
} catch (\Throwable $e) {
|
||||
Console::error($e->getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -130,7 +130,7 @@ class Mails extends Action
|
||||
|
||||
try {
|
||||
$mail->send();
|
||||
} catch (\Exception $error) {
|
||||
} catch (\Throwable $error) {
|
||||
throw new Exception('Error sending mail: ' . $error->getMessage(), 500);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,23 +2,22 @@
|
||||
|
||||
namespace Appwrite\Platform\Workers;
|
||||
|
||||
use Appwrite\Event\Usage;
|
||||
use Appwrite\Extend\Exception;
|
||||
use Appwrite\Messaging\Status as MessageStatus;
|
||||
use Utopia\App;
|
||||
use Utopia\CLI\Console;
|
||||
use Utopia\Database\Helpers\ID;
|
||||
use Utopia\DSN\DSN;
|
||||
use Utopia\Logger\Log;
|
||||
use Utopia\Platform\Action;
|
||||
use Utopia\Queue\Message;
|
||||
use Utopia\Database\Database;
|
||||
use Utopia\Database\DateTime;
|
||||
use Utopia\Database\Document;
|
||||
use Utopia\Database\Helpers\ID;
|
||||
use Utopia\Database\Query;
|
||||
use Utopia\Logger\Log;
|
||||
use Utopia\Messaging\Adapter\Email as EmailAdapter;
|
||||
use Utopia\Messaging\Adapter\Email\Mailgun;
|
||||
use Utopia\Messaging\Adapter\Email\Sendgrid;
|
||||
use Utopia\Messaging\Adapter\Email\SMTP;
|
||||
use Utopia\Messaging\Adapter\Email\Sendgrid;
|
||||
use Utopia\Messaging\Adapter\Push as PushAdapter;
|
||||
use Utopia\Messaging\Adapter\Push\APNS;
|
||||
use Utopia\Messaging\Adapter\Push\FCM;
|
||||
@@ -32,7 +31,8 @@ use Utopia\Messaging\Adapter\SMS\Vonage;
|
||||
use Utopia\Messaging\Messages\Email;
|
||||
use Utopia\Messaging\Messages\Push;
|
||||
use Utopia\Messaging\Messages\SMS;
|
||||
use Utopia\Messaging\Response;
|
||||
use Utopia\Platform\Action;
|
||||
use Utopia\Queue\Message;
|
||||
|
||||
use function Swoole\Coroutine\batch;
|
||||
|
||||
@@ -44,7 +44,7 @@ class Messaging extends Action
|
||||
}
|
||||
|
||||
/**
|
||||
* @throws Exception
|
||||
* @throws \Exception
|
||||
*/
|
||||
public function __construct()
|
||||
{
|
||||
@@ -53,39 +53,47 @@ class Messaging extends Action
|
||||
->inject('message')
|
||||
->inject('log')
|
||||
->inject('dbForProject')
|
||||
->callback(fn(Message $message, Log $log, Database $dbForProject) => $this->action($message, $log, $dbForProject));
|
||||
->inject('queueForUsage')
|
||||
->callback(fn(Message $message, Log $log, Database $dbForProject, Usage $queueForUsage) => $this->action($message, $log, $dbForProject, $queueForUsage));
|
||||
}
|
||||
|
||||
/**
|
||||
* @param Message $message
|
||||
* @param Log $log
|
||||
* @param Database $dbForProject
|
||||
* @param Usage $queueForUsage
|
||||
* @return void
|
||||
* @throws Exception
|
||||
* @throws \Exception
|
||||
*/
|
||||
public function action(Message $message, Log $log, Database $dbForProject): void
|
||||
public function action(Message $message, Log $log, Database $dbForProject, Usage $queueForUsage): void
|
||||
{
|
||||
$payload = $message->getPayload() ?? [];
|
||||
|
||||
if (empty($payload)) {
|
||||
throw new \Exception('Payload not found.');
|
||||
throw new Exception('Missing payload');
|
||||
}
|
||||
|
||||
if (
|
||||
!\is_null($payload['message'])
|
||||
&& !\is_null($payload['recipients'])
|
||||
&& $payload['providerType'] === MESSAGE_TYPE_SMS
|
||||
) {
|
||||
// Message was triggered internally
|
||||
$this->processInternalSMSMessage($log, new Document($payload['message']), $payload['recipients']);
|
||||
} else {
|
||||
$message = $dbForProject->getDocument('messages', $payload['messageId']);
|
||||
$type = $payload['type'] ?? '';
|
||||
$project = new Document($payload['project'] ?? []);
|
||||
|
||||
$this->processMessage($dbForProject, $message);
|
||||
switch ($type) {
|
||||
case MESSAGE_SEND_TYPE_INTERNAL:
|
||||
$message = new Document($payload['message'] ?? []);
|
||||
$recipients = $payload['recipients'] ?? [];
|
||||
|
||||
$this->sendInternalSMSMessage($message, $project, $recipients, $queueForUsage, $log);
|
||||
break;
|
||||
case MESSAGE_SEND_TYPE_EXTERNAL:
|
||||
$message = $dbForProject->getDocument('messages', $payload['messageId']);
|
||||
|
||||
$this->sendExternalMessage($dbForProject, $message);
|
||||
break;
|
||||
default:
|
||||
throw new Exception('Unknown message type: ' . $type);
|
||||
}
|
||||
}
|
||||
|
||||
private function processMessage(Database $dbForProject, Document $message): void
|
||||
private function sendExternalMessage(Database $dbForProject, Document $message): void
|
||||
{
|
||||
$topicIds = $message->getAttribute('topics', []);
|
||||
$targetIds = $message->getAttribute('targets', []);
|
||||
@@ -207,9 +215,9 @@ class Messaging extends Action
|
||||
$identifiers = $identifiers[$providerId];
|
||||
|
||||
$adapter = match ($provider->getAttribute('type')) {
|
||||
MESSAGE_TYPE_SMS => $this->sms($provider),
|
||||
MESSAGE_TYPE_PUSH => $this->push($provider),
|
||||
MESSAGE_TYPE_EMAIL => $this->email($provider),
|
||||
MESSAGE_TYPE_SMS => $this->getSmsAdapter($provider),
|
||||
MESSAGE_TYPE_PUSH => $this->getPushAdapter($provider),
|
||||
MESSAGE_TYPE_EMAIL => $this->getEmailAdapter($provider),
|
||||
default => throw new Exception(Exception::PROVIDER_INCORRECT_TYPE)
|
||||
};
|
||||
|
||||
@@ -225,7 +233,7 @@ class Messaging extends Action
|
||||
$messageData->setAttribute('to', $batch);
|
||||
|
||||
$data = match ($provider->getAttribute('type')) {
|
||||
MESSAGE_TYPE_SMS => $this->buildSMSMessage($messageData, $provider),
|
||||
MESSAGE_TYPE_SMS => $this->buildSmsMessage($messageData, $provider),
|
||||
MESSAGE_TYPE_PUSH => $this->buildPushMessage($messageData),
|
||||
MESSAGE_TYPE_EMAIL => $this->buildEmailMessage($dbForProject, $messageData, $provider),
|
||||
default => throw new Exception(Exception::PROVIDER_INCORRECT_TYPE)
|
||||
@@ -240,7 +248,7 @@ class Messaging extends Action
|
||||
}
|
||||
|
||||
// Deleting push targets when token has expired.
|
||||
if (($result['error'] ?? '') === 'Expired device token.') {
|
||||
if (($result['error'] ?? '') === 'Expired device token') {
|
||||
$target = $dbForProject->findOne('targets', [
|
||||
Query::equal('identifier', [$result['recipient']])
|
||||
]);
|
||||
@@ -254,8 +262,8 @@ class Messaging extends Action
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (\Exception $e) {
|
||||
$deliveryErrors[] = 'Failed sending to targets ' . $batchIndex + 1 . '-' . \count($batch) . ' with error: ' . $e->getMessage();
|
||||
} catch (\Throwable $e) {
|
||||
$deliveryErrors[] = 'Failed sending to targets ' . $batchIndex + 1 . ' of ' . \count($batch) . ' with error: ' . $e->getMessage();
|
||||
} finally {
|
||||
$batchIndex++;
|
||||
|
||||
@@ -279,6 +287,10 @@ class Messaging extends Action
|
||||
$deliveryErrors = \array_merge($deliveryErrors, $result['deliveryErrors']);
|
||||
}
|
||||
|
||||
if (empty($deliveryErrors) && $deliveredTotal === 0) {
|
||||
$deliveryErrors[] = 'Unknown error';
|
||||
}
|
||||
|
||||
$message->setAttribute('deliveryErrors', $deliveryErrors);
|
||||
|
||||
if (\count($message->getAttribute('deliveryErrors')) > 0) {
|
||||
@@ -299,12 +311,26 @@ class Messaging extends Action
|
||||
$dbForProject->updateDocument('messages', $message->getId(), $message);
|
||||
}
|
||||
|
||||
private function processInternalSMSMessage(Log $log, Document $message, array $recipients): void
|
||||
private function sendInternalSMSMessage(Document $message, Document $project, array $recipients, Usage $queueForUsage, Log $log): void
|
||||
{
|
||||
if (empty(App::getEnv('_APP_SMS_PROVIDER')) || empty(App::getEnv('_APP_SMS_FROM'))) {
|
||||
throw new \Exception('Skipped SMS processing. Missing "_APP_SMS_PROVIDER" or "_APP_SMS_FROM" environment variables.');
|
||||
}
|
||||
|
||||
if ($project->isEmpty()) {
|
||||
throw new Exception('Project not set in payload');
|
||||
}
|
||||
|
||||
Console::log('Project: ' . $project->getId());
|
||||
|
||||
$denyList = App::getEnv('_APP_SMS_PROJECTS_DENY_LIST', '');
|
||||
$denyList = explode(',', $denyList);
|
||||
|
||||
if (\in_array($project->getId(), $denyList)) {
|
||||
Console::error('Project is in the deny list. Skipping...');
|
||||
return;
|
||||
}
|
||||
|
||||
$smsDSN = new DSN(App::getEnv('_APP_SMS_PROVIDER'));
|
||||
$host = $smsDSN->getHost();
|
||||
$password = $smsDSN->getPassword();
|
||||
@@ -330,8 +356,8 @@ class Messaging extends Action
|
||||
'apiKey' => $password
|
||||
],
|
||||
'telesign' => [
|
||||
'username' => $user,
|
||||
'password' => $password
|
||||
'customerId' => $user,
|
||||
'apiKey' => $password
|
||||
],
|
||||
'msg91' => [
|
||||
'senderId' => $user,
|
||||
@@ -348,48 +374,51 @@ class Messaging extends Action
|
||||
]
|
||||
]);
|
||||
|
||||
$adapter = $this->sms($provider);
|
||||
$adapter = $this->getSmsAdapter($provider);
|
||||
|
||||
$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) {
|
||||
batch(\array_map(function ($batch) use ($message, $provider, $adapter, $batchIndex, $project, $queueForUsage) {
|
||||
return function () use ($batch, $message, $provider, $adapter, $batchIndex, $project, $queueForUsage) {
|
||||
$message->setAttribute('to', $batch);
|
||||
|
||||
$data = $this->buildSMSMessage($message, $provider);
|
||||
$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()); // TODO: Find a way to log into Sentry
|
||||
|
||||
$queueForUsage
|
||||
->setProject($project)
|
||||
->addMetric(METRIC_MESSAGES, 1)
|
||||
->trigger();
|
||||
} catch (\Throwable $e) {
|
||||
throw new Exception('Failed sending to targets ' . $batchIndex + 1 . '-' . \count($batch) . ' with error: ' . $e->getMessage(), 500);
|
||||
}
|
||||
};
|
||||
}, $batches));
|
||||
}
|
||||
|
||||
public function shutdown(): void
|
||||
{
|
||||
}
|
||||
|
||||
private function sms(Document $provider): ?SMSAdapter
|
||||
private function getSmsAdapter(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']),
|
||||
'telesign' => new Telesign($credentials['customerId'], $credentials['apiKey']),
|
||||
'msg91' => new Msg91($credentials['senderId'], $credentials['authKey'], $credentials['templateId']),
|
||||
'vonage' => new Vonage($credentials['apiKey'], $credentials['apiSecret']),
|
||||
default => null
|
||||
};
|
||||
}
|
||||
|
||||
private function push(Document $provider): ?PushAdapter
|
||||
private function getPushAdapter(Document $provider): ?PushAdapter
|
||||
{
|
||||
$credentials = $provider->getAttribute('credentials');
|
||||
|
||||
return match ($provider->getAttribute('provider')) {
|
||||
'mock' => new Mock('username', 'password'),
|
||||
'apns' => new APNS(
|
||||
@@ -403,10 +432,11 @@ class Messaging extends Action
|
||||
};
|
||||
}
|
||||
|
||||
private function email(Document $provider): ?EmailAdapter
|
||||
private function getEmailAdapter(Document $provider): ?EmailAdapter
|
||||
{
|
||||
$credentials = $provider->getAttribute('credentials', []);
|
||||
$options = $provider->getAttribute('options', []);
|
||||
|
||||
return match ($provider->getAttribute('provider')) {
|
||||
'mock' => new Mock('username', 'password'),
|
||||
'smtp' => new SMTP(
|
||||
@@ -468,7 +498,7 @@ class Messaging extends Action
|
||||
return new Email($to, $subject, $content, $fromName, $fromEmail, $replyToName, $replyToEmail, $cc, $bcc, null, $html);
|
||||
}
|
||||
|
||||
private function buildSMSMessage(Document $message, Document $provider): SMS
|
||||
private function buildSmsMessage(Document $message, Document $provider): SMS
|
||||
{
|
||||
$to = $message['to'];
|
||||
$content = $message['data']['content'];
|
||||
|
||||
@@ -20,7 +20,6 @@ class Usage extends Action
|
||||
];
|
||||
|
||||
protected const INFINITY_PERIOD = '_inf_';
|
||||
protected const DEBUG_PROJECT_ID = 85293;
|
||||
public static function getName(): string
|
||||
{
|
||||
return 'usage';
|
||||
@@ -70,16 +69,6 @@ class Usage extends Action
|
||||
getProjectDB: $getProjectDB
|
||||
);
|
||||
}
|
||||
if ($project->getInternalId() == self::DEBUG_PROJECT_ID) {
|
||||
var_dump([
|
||||
'type' => 'payload',
|
||||
'project' => $project->getInternalId(),
|
||||
'database' => $project['database'] ?? '',
|
||||
$payload['metrics']
|
||||
]);
|
||||
|
||||
var_dump('==========================');
|
||||
}
|
||||
|
||||
self::$stats[$projectId]['project'] = $project;
|
||||
foreach ($payload['metrics'] ?? [] as $metric) {
|
||||
@@ -91,7 +80,6 @@ class Usage extends Action
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* On Documents that tied by relations like functions>deployments>build || documents>collection>database || buckets>files.
|
||||
* When we remove a parent document we need to deduct his children aggregation from the project scope.
|
||||
@@ -118,8 +106,8 @@ class Usage extends Action
|
||||
}
|
||||
break;
|
||||
case $document->getCollection() === 'databases': // databases
|
||||
$collections = $dbForProject->getDocument('stats_v2', md5(self::INFINITY_PERIOD . str_replace('{databaseInternalId}', $document->getInternalId(), METRIC_DATABASE_ID_COLLECTIONS)));
|
||||
$documents = $dbForProject->getDocument('stats_v2', md5(self::INFINITY_PERIOD . str_replace('{databaseInternalId}', $document->getInternalId(), METRIC_DATABASE_ID_DOCUMENTS)));
|
||||
$collections = $dbForProject->getDocument('stats', md5(self::INFINITY_PERIOD . str_replace('{databaseInternalId}', $document->getInternalId(), METRIC_DATABASE_ID_COLLECTIONS)));
|
||||
$documents = $dbForProject->getDocument('stats', md5(self::INFINITY_PERIOD . str_replace('{databaseInternalId}', $document->getInternalId(), METRIC_DATABASE_ID_DOCUMENTS)));
|
||||
if (!empty($collections['value'])) {
|
||||
$metrics[] = [
|
||||
'key' => METRIC_COLLECTIONS,
|
||||
@@ -137,7 +125,7 @@ class Usage extends Action
|
||||
case str_starts_with($document->getCollection(), 'database_') && !str_contains($document->getCollection(), 'collection'): //collections
|
||||
$parts = explode('_', $document->getCollection());
|
||||
$databaseInternalId = $parts[1] ?? 0;
|
||||
$documents = $dbForProject->getDocument('stats_v2', md5(self::INFINITY_PERIOD . str_replace(['{databaseInternalId}', '{collectionInternalId}'], [$databaseInternalId, $document->getInternalId()], METRIC_DATABASE_ID_COLLECTION_ID_DOCUMENTS)));
|
||||
$documents = $dbForProject->getDocument('stats', md5(self::INFINITY_PERIOD . str_replace(['{databaseInternalId}', '{collectionInternalId}'], [$databaseInternalId, $document->getInternalId()], METRIC_DATABASE_ID_COLLECTION_ID_DOCUMENTS)));
|
||||
|
||||
if (!empty($documents['value'])) {
|
||||
$metrics[] = [
|
||||
@@ -152,8 +140,8 @@ class Usage extends Action
|
||||
break;
|
||||
|
||||
case $document->getCollection() === 'buckets':
|
||||
$files = $dbForProject->getDocument('stats_v2', md5(self::INFINITY_PERIOD . str_replace('{bucketInternalId}', $document->getInternalId(), METRIC_BUCKET_ID_FILES)));
|
||||
$storage = $dbForProject->getDocument('stats_v2', md5(self::INFINITY_PERIOD . str_replace('{bucketInternalId}', $document->getInternalId(), METRIC_BUCKET_ID_FILES_STORAGE)));
|
||||
$files = $dbForProject->getDocument('stats', md5(self::INFINITY_PERIOD . str_replace('{bucketInternalId}', $document->getInternalId(), METRIC_BUCKET_ID_FILES)));
|
||||
$storage = $dbForProject->getDocument('stats', md5(self::INFINITY_PERIOD . str_replace('{bucketInternalId}', $document->getInternalId(), METRIC_BUCKET_ID_FILES_STORAGE)));
|
||||
|
||||
if (!empty($files['value'])) {
|
||||
$metrics[] = [
|
||||
@@ -171,13 +159,13 @@ class Usage extends Action
|
||||
break;
|
||||
|
||||
case $document->getCollection() === 'functions':
|
||||
$deployments = $dbForProject->getDocument('stats_v2', md5(self::INFINITY_PERIOD . str_replace(['{resourceType}', '{resourceInternalId}'], ['functions', $document->getInternalId()], METRIC_FUNCTION_ID_DEPLOYMENTS)));
|
||||
$deploymentsStorage = $dbForProject->getDocument('stats_v2', md5(self::INFINITY_PERIOD . str_replace(['{resourceType}', '{resourceInternalId}'], ['functions', $document->getInternalId()], METRIC_FUNCTION_ID_DEPLOYMENTS_STORAGE)));
|
||||
$builds = $dbForProject->getDocument('stats_v2', md5(self::INFINITY_PERIOD . str_replace('{functionInternalId}', $document->getInternalId(), METRIC_FUNCTION_ID_BUILDS)));
|
||||
$buildsStorage = $dbForProject->getDocument('stats_v2', md5(self::INFINITY_PERIOD . str_replace('{functionInternalId}', $document->getInternalId(), METRIC_FUNCTION_ID_BUILDS_STORAGE)));
|
||||
$buildsCompute = $dbForProject->getDocument('stats_v2', md5(self::INFINITY_PERIOD . str_replace('{functionInternalId}', $document->getInternalId(), METRIC_FUNCTION_ID_BUILDS_COMPUTE)));
|
||||
$executions = $dbForProject->getDocument('stats_v2', md5(self::INFINITY_PERIOD . str_replace('{functionInternalId}', $document->getInternalId(), METRIC_FUNCTION_ID_EXECUTIONS)));
|
||||
$executionsCompute = $dbForProject->getDocument('stats_v2', md5(self::INFINITY_PERIOD . str_replace('{functionInternalId}', $document->getInternalId(), METRIC_FUNCTION_ID_EXECUTIONS_COMPUTE)));
|
||||
$deployments = $dbForProject->getDocument('stats', md5(self::INFINITY_PERIOD . str_replace(['{resourceType}', '{resourceInternalId}'], ['functions', $document->getInternalId()], METRIC_FUNCTION_ID_DEPLOYMENTS)));
|
||||
$deploymentsStorage = $dbForProject->getDocument('stats', md5(self::INFINITY_PERIOD . str_replace(['{resourceType}', '{resourceInternalId}'], ['functions', $document->getInternalId()], METRIC_FUNCTION_ID_DEPLOYMENTS_STORAGE)));
|
||||
$builds = $dbForProject->getDocument('stats', md5(self::INFINITY_PERIOD . str_replace('{functionInternalId}', $document->getInternalId(), METRIC_FUNCTION_ID_BUILDS)));
|
||||
$buildsStorage = $dbForProject->getDocument('stats', md5(self::INFINITY_PERIOD . str_replace('{functionInternalId}', $document->getInternalId(), METRIC_FUNCTION_ID_BUILDS_STORAGE)));
|
||||
$buildsCompute = $dbForProject->getDocument('stats', md5(self::INFINITY_PERIOD . str_replace('{functionInternalId}', $document->getInternalId(), METRIC_FUNCTION_ID_BUILDS_COMPUTE)));
|
||||
$executions = $dbForProject->getDocument('stats', md5(self::INFINITY_PERIOD . str_replace('{functionInternalId}', $document->getInternalId(), METRIC_FUNCTION_ID_EXECUTIONS)));
|
||||
$executionsCompute = $dbForProject->getDocument('stats', md5(self::INFINITY_PERIOD . str_replace('{functionInternalId}', $document->getInternalId(), METRIC_FUNCTION_ID_EXECUTIONS_COMPUTE)));
|
||||
|
||||
if (!empty($deployments['value'])) {
|
||||
$metrics[] = [
|
||||
@@ -231,7 +219,7 @@ class Usage extends Action
|
||||
default:
|
||||
break;
|
||||
}
|
||||
} catch (\Exception $e) {
|
||||
} catch (\Throwable $e) {
|
||||
console::error("[reducer] " . " {DateTime::now()} " . " {$project->getInternalId()} " . " {$e->getMessage()}");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -57,14 +57,6 @@ class UsageHook extends Usage
|
||||
|
||||
try {
|
||||
$dbForProject = $getProjectDB($data['project']);
|
||||
if ($projectInternalId == 85293) {
|
||||
var_dump([
|
||||
'project' => $projectInternalId,
|
||||
'database' => $database,
|
||||
'time' => DateTime::now(),
|
||||
'data' => $data['keys']
|
||||
]);
|
||||
}
|
||||
foreach ($data['keys'] ?? [] as $key => $value) {
|
||||
if ($value == 0) {
|
||||
continue;
|
||||
@@ -75,16 +67,7 @@ class UsageHook extends Usage
|
||||
$id = \md5("{$time}_{$period}_{$key}");
|
||||
|
||||
try {
|
||||
if ($projectInternalId == self::DEBUG_PROJECT_ID) {
|
||||
var_dump([
|
||||
'type' => 'create',
|
||||
'period' => $period,
|
||||
'metric' => $key,
|
||||
'id' => $id,
|
||||
'value' => $value
|
||||
]);
|
||||
}
|
||||
$dbForProject->createDocument('stats_v2', new Document([
|
||||
$dbForProject->createDocument('stats', new Document([
|
||||
'$id' => $id,
|
||||
'period' => $period,
|
||||
'time' => $time,
|
||||
@@ -94,33 +77,15 @@ class UsageHook extends Usage
|
||||
]));
|
||||
} catch (Duplicate $th) {
|
||||
if ($value < 0) {
|
||||
if ($projectInternalId == self::DEBUG_PROJECT_ID) {
|
||||
var_dump([
|
||||
'type' => 'decrease',
|
||||
'period' => $period,
|
||||
'metric' => $key,
|
||||
'id' => $id,
|
||||
'value' => $value
|
||||
]);
|
||||
}
|
||||
$dbForProject->decreaseDocumentAttribute(
|
||||
'stats_v2',
|
||||
'stats',
|
||||
$id,
|
||||
'value',
|
||||
abs($value)
|
||||
);
|
||||
} else {
|
||||
if ($projectInternalId == self::DEBUG_PROJECT_ID) {
|
||||
var_dump([
|
||||
'type' => 'increase',
|
||||
'period' => $period,
|
||||
'metric' => $key,
|
||||
'id' => $id,
|
||||
'value' => $value
|
||||
]);
|
||||
}
|
||||
$dbForProject->increaseDocumentAttribute(
|
||||
'stats_v2',
|
||||
'stats',
|
||||
$id,
|
||||
'value',
|
||||
$value
|
||||
@@ -129,7 +94,7 @@ class UsageHook extends Usage
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (\Exception $e) {
|
||||
} catch (\Throwable $e) {
|
||||
console::error(DateTime::now() . ' ' . $projectInternalId . ' ' . $e->getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -240,17 +240,24 @@ abstract class Format
|
||||
break;
|
||||
case 'projects':
|
||||
switch ($method) {
|
||||
case 'getSmsTemplate':
|
||||
case 'getEmailTemplate':
|
||||
case 'updateSmsTemplate':
|
||||
case 'updateEmailTemplate':
|
||||
case 'deleteSmsTemplate':
|
||||
case 'deleteEmailTemplate':
|
||||
switch ($param) {
|
||||
case 'type':
|
||||
return 'TemplateType';
|
||||
return 'EmailTemplateType';
|
||||
case 'locale':
|
||||
return 'TemplateLocale';
|
||||
return 'EmailTemplateLocale';
|
||||
}
|
||||
break;
|
||||
case 'getSmsTemplate':
|
||||
case 'updateSmsTemplate':
|
||||
case 'deleteSmsTemplate':
|
||||
switch ($param) {
|
||||
case 'type':
|
||||
return 'SMSTemplateType';
|
||||
case 'locale':
|
||||
return 'SMSTemplateLocale';
|
||||
}
|
||||
break;
|
||||
case 'createPlatform':
|
||||
|
||||
@@ -7,7 +7,9 @@ class Topics extends Base
|
||||
public const ALLOWED_ATTRIBUTES = [
|
||||
'name',
|
||||
'description',
|
||||
'total'
|
||||
'emailTotal',
|
||||
'smsTotal',
|
||||
'pushTotal',
|
||||
];
|
||||
|
||||
/**
|
||||
|
||||
@@ -12,7 +12,8 @@ class Users extends Base
|
||||
'passwordUpdate',
|
||||
'registration',
|
||||
'emailVerification',
|
||||
'phoneVerification'
|
||||
'phoneVerification',
|
||||
'labels',
|
||||
];
|
||||
|
||||
/**
|
||||
|
||||
@@ -0,0 +1,341 @@
|
||||
<?php
|
||||
|
||||
namespace Appwrite\Utopia\Request\Filters;
|
||||
|
||||
use Appwrite\Utopia\Request\Filter;
|
||||
use Utopia\Database\Query;
|
||||
|
||||
class V17 extends Filter
|
||||
{
|
||||
protected const CHAR_SINGLE_QUOTE = '\'';
|
||||
protected const CHAR_DOUBLE_QUOTE = '"';
|
||||
protected const CHAR_COMMA = ',';
|
||||
protected const CHAR_SPACE = ' ';
|
||||
protected const CHAR_BRACKET_START = '[';
|
||||
protected const CHAR_BRACKET_END = ']';
|
||||
protected const CHAR_PARENTHESES_START = '(';
|
||||
protected const CHAR_PARENTHESES_END = ')';
|
||||
protected const CHAR_BACKSLASH = '\\';
|
||||
|
||||
// Convert 1.4 params to 1.5
|
||||
public function parse(array $content, string $model): array
|
||||
{
|
||||
switch ($model) {
|
||||
case 'account.updateRecovery':
|
||||
unset($content['passwordAgain']);
|
||||
break;
|
||||
// Queries
|
||||
case 'account.listIdentities':
|
||||
case 'account.listLogs':
|
||||
case 'databases.list':
|
||||
case 'databases.listLogs':
|
||||
case 'databases.listCollections':
|
||||
case 'databases.listCollectionLogs':
|
||||
case 'databases.listAttributes':
|
||||
case 'databases.listIndexes':
|
||||
case 'databases.listDocuments':
|
||||
case 'databases.getDocument':
|
||||
case 'databases.listDocumentLogs':
|
||||
case 'functions.list':
|
||||
case 'functions.listDeployments':
|
||||
case 'functions.listExecutions':
|
||||
case 'migrations.list':
|
||||
case 'projects.list':
|
||||
case 'proxy.listRules':
|
||||
case 'storage.listBuckets':
|
||||
case 'storage.listFiles':
|
||||
case 'teams.list':
|
||||
case 'teams.listMemberships':
|
||||
case 'teams.listLogs':
|
||||
case 'users.list':
|
||||
case 'users.listLogs':
|
||||
case 'users.listIdentities':
|
||||
case 'vcs.listInstallations':
|
||||
$content = $this->convertOldQueries($content);
|
||||
break;
|
||||
}
|
||||
return $content;
|
||||
}
|
||||
|
||||
private function convertOldQueries(array $content): array
|
||||
{
|
||||
$parsed = [];
|
||||
foreach ($content['queries'] as $query) {
|
||||
try {
|
||||
$query = $this->parseQuery($query);
|
||||
$parsed[] = json_encode(array_filter($query->toArray()));
|
||||
} catch (\Throwable $th) {
|
||||
throw new \Exception("Invalid query: {$query}", previous: $th);
|
||||
}
|
||||
}
|
||||
|
||||
$content['queries'] = $parsed;
|
||||
|
||||
return $content;
|
||||
}
|
||||
|
||||
// 1.4 query parser
|
||||
public function parseQuery(string $filter): Query
|
||||
{
|
||||
// Init empty vars we fill later
|
||||
$method = '';
|
||||
$params = [];
|
||||
|
||||
// Separate method from filter
|
||||
$paramsStart = mb_strpos($filter, '(');
|
||||
|
||||
if ($paramsStart === false) {
|
||||
throw new \Exception('Invalid query');
|
||||
}
|
||||
|
||||
$method = mb_substr($filter, 0, $paramsStart);
|
||||
|
||||
// Separate params from filter
|
||||
$paramsEnd = \strlen($filter) - 1; // -1 to ignore )
|
||||
$parametersStart = $paramsStart + 1; // +1 to ignore (
|
||||
|
||||
// Check for deprecated query syntax
|
||||
if (\str_contains($method, '.')) {
|
||||
throw new \Exception('Invalid query method');
|
||||
}
|
||||
|
||||
$currentParam = ""; // We build param here before pushing when it's ended
|
||||
$currentArrayParam = []; // We build array param here before pushing when it's ended
|
||||
|
||||
$stack = []; // State for stack of parentheses
|
||||
$stackCount = 0; // Length of stack array. Kept as variable to improve performance
|
||||
$stringStackState = null; // State for string support
|
||||
|
||||
|
||||
// Loop thorough all characters
|
||||
for ($i = $parametersStart; $i < $paramsEnd; $i++) {
|
||||
$char = $filter[$i];
|
||||
|
||||
$isStringStack = $stringStackState !== null;
|
||||
$isArrayStack = !$isStringStack && $stackCount > 0;
|
||||
|
||||
if ($char === static::CHAR_BACKSLASH) {
|
||||
if (!(static::isSpecialChar($filter[$i + 1]))) {
|
||||
static::appendSymbol($isStringStack, $filter[$i], $i, $filter, $currentParam);
|
||||
}
|
||||
|
||||
static::appendSymbol($isStringStack, $filter[$i + 1], $i, $filter, $currentParam);
|
||||
$i++;
|
||||
|
||||
continue;
|
||||
}
|
||||
|
||||
// String support + escaping support
|
||||
if (
|
||||
(self::isQuote($char)) && // Must be string indicator
|
||||
($filter[$i - 1] !== static::CHAR_BACKSLASH || $filter[$i - 2] === static::CHAR_BACKSLASH) // Must not be escaped;
|
||||
) {
|
||||
if ($isStringStack) {
|
||||
// Dont mix-up string symbols. Only allow the same as on start
|
||||
if ($char === $stringStackState) {
|
||||
// End of string
|
||||
$stringStackState = null;
|
||||
}
|
||||
|
||||
// Either way, add symbol to builder
|
||||
static::appendSymbol($isStringStack, $char, $i, $filter, $currentParam);
|
||||
} else {
|
||||
// Start of string
|
||||
$stringStackState = $char;
|
||||
static::appendSymbol($isStringStack, $char, $i, $filter, $currentParam);
|
||||
}
|
||||
|
||||
continue;
|
||||
}
|
||||
|
||||
// Array support
|
||||
if (!($isStringStack)) {
|
||||
if ($char === static::CHAR_BRACKET_START) {
|
||||
// Start of array
|
||||
$stack[] = $char;
|
||||
$stackCount++;
|
||||
continue;
|
||||
} elseif ($char === static::CHAR_BRACKET_END) {
|
||||
// End of array
|
||||
\array_pop($stack);
|
||||
$stackCount--;
|
||||
|
||||
if (strlen($currentParam)) {
|
||||
$currentArrayParam[] = $currentParam;
|
||||
}
|
||||
|
||||
$params[] = $currentArrayParam;
|
||||
$currentArrayParam = [];
|
||||
$currentParam = "";
|
||||
|
||||
continue;
|
||||
} elseif ($char === static::CHAR_COMMA) { // Params separation support
|
||||
// If in array stack, dont merge yet, just mark it in array param builder
|
||||
if ($isArrayStack) {
|
||||
$currentArrayParam[] = $currentParam;
|
||||
$currentParam = "";
|
||||
} else {
|
||||
// Append from parap builder. Either value, or array
|
||||
if (empty($currentArrayParam)) {
|
||||
if (strlen($currentParam)) {
|
||||
$params[] = $currentParam;
|
||||
}
|
||||
|
||||
$currentParam = "";
|
||||
}
|
||||
}
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
// Value, not relevant to syntax
|
||||
static::appendSymbol($isStringStack, $char, $i, $filter, $currentParam);
|
||||
}
|
||||
|
||||
if (strlen($currentParam)) {
|
||||
$params[] = $currentParam;
|
||||
$currentParam = "";
|
||||
}
|
||||
|
||||
$parsedParams = [];
|
||||
|
||||
foreach ($params as $param) {
|
||||
// If array, parse each child separatelly
|
||||
if (\is_array($param)) {
|
||||
foreach ($param as $element) {
|
||||
$arr[] = self::parseValue($element);
|
||||
}
|
||||
|
||||
$parsedParams[] = $arr ?? [];
|
||||
} else {
|
||||
$parsedParams[] = self::parseValue($param);
|
||||
}
|
||||
}
|
||||
|
||||
switch ($method) {
|
||||
case Query::TYPE_EQUAL:
|
||||
case Query::TYPE_NOT_EQUAL:
|
||||
case Query::TYPE_LESSER:
|
||||
case Query::TYPE_LESSER_EQUAL:
|
||||
case Query::TYPE_GREATER:
|
||||
case Query::TYPE_GREATER_EQUAL:
|
||||
case Query::TYPE_CONTAINS:
|
||||
case Query::TYPE_SEARCH:
|
||||
case Query::TYPE_IS_NULL:
|
||||
case Query::TYPE_IS_NOT_NULL:
|
||||
case Query::TYPE_STARTS_WITH:
|
||||
case Query::TYPE_ENDS_WITH:
|
||||
$attribute = $parsedParams[0] ?? '';
|
||||
if (count($parsedParams) < 2) {
|
||||
return new Query($method, $attribute);
|
||||
}
|
||||
return new Query($method, $attribute, \is_array($parsedParams[1]) ? $parsedParams[1] : [$parsedParams[1]]);
|
||||
|
||||
case Query::TYPE_BETWEEN:
|
||||
return new Query($method, $parsedParams[0], [$parsedParams[1], $parsedParams[2]]);
|
||||
case Query::TYPE_SELECT:
|
||||
return new Query($method, values: $parsedParams[0]);
|
||||
case Query::TYPE_ORDER_ASC:
|
||||
case Query::TYPE_ORDER_DESC:
|
||||
return new Query($method, $parsedParams[0] ?? '');
|
||||
|
||||
case Query::TYPE_LIMIT:
|
||||
case Query::TYPE_OFFSET:
|
||||
case Query::TYPE_CURSOR_AFTER:
|
||||
case Query::TYPE_CURSOR_BEFORE:
|
||||
if (count($parsedParams) > 0) {
|
||||
return new Query($method, values: [$parsedParams[0]]);
|
||||
}
|
||||
return new Query($method);
|
||||
|
||||
default:
|
||||
return new Query($method);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Parses value.
|
||||
*
|
||||
* @param string $value
|
||||
* @return mixed
|
||||
*/
|
||||
private function parseValue(string $value): mixed
|
||||
{
|
||||
$value = \trim($value);
|
||||
|
||||
if ($value === 'false') { // Boolean value
|
||||
return false;
|
||||
} elseif ($value === 'true') {
|
||||
return true;
|
||||
} elseif ($value === 'null') { // Null value
|
||||
return null;
|
||||
} elseif (\is_numeric($value)) { // Numeric value
|
||||
// Cast to number
|
||||
return $value + 0;
|
||||
} elseif (\str_starts_with($value, static::CHAR_DOUBLE_QUOTE) || \str_starts_with($value, static::CHAR_SINGLE_QUOTE)) { // String param
|
||||
$value = \substr($value, 1, -1); // Remove '' or ""
|
||||
return $value;
|
||||
}
|
||||
|
||||
// Unknown format
|
||||
return $value;
|
||||
}
|
||||
|
||||
/**
|
||||
* Utility method to only append symbol if relevant.
|
||||
*
|
||||
* @param bool $isStringStack
|
||||
* @param string $char
|
||||
* @param int $index
|
||||
* @param string $filter
|
||||
* @param string $currentParam
|
||||
* @return void
|
||||
*/
|
||||
private function appendSymbol(bool $isStringStack, string $char, int $index, string $filter, string &$currentParam): void
|
||||
{
|
||||
// Ignore spaces and commas outside of string
|
||||
$canBeIgnored = false;
|
||||
|
||||
if ($char === static::CHAR_SPACE) {
|
||||
$canBeIgnored = true;
|
||||
} elseif ($char === static::CHAR_COMMA) {
|
||||
$canBeIgnored = true;
|
||||
}
|
||||
|
||||
if ($canBeIgnored) {
|
||||
if ($isStringStack) {
|
||||
$currentParam .= $char;
|
||||
}
|
||||
} else {
|
||||
$currentParam .= $char;
|
||||
}
|
||||
}
|
||||
|
||||
private function isQuote(string $char): bool
|
||||
{
|
||||
if ($char === self::CHAR_SINGLE_QUOTE) {
|
||||
return true;
|
||||
} elseif ($char === self::CHAR_DOUBLE_QUOTE) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
private function isSpecialChar(string $char): bool
|
||||
{
|
||||
if ($char === static::CHAR_COMMA) {
|
||||
return true;
|
||||
} elseif ($char === static::CHAR_BRACKET_END) {
|
||||
return true;
|
||||
} elseif ($char === static::CHAR_BRACKET_START) {
|
||||
return true;
|
||||
} elseif ($char === static::CHAR_DOUBLE_QUOTE) {
|
||||
return true;
|
||||
} elseif ($char === static::CHAR_SINGLE_QUOTE) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
}
|
||||
@@ -70,18 +70,19 @@ use Appwrite\Utopia\Response\Model\Token;
|
||||
use Appwrite\Utopia\Response\Model\Webhook;
|
||||
use Appwrite\Utopia\Response\Model\Preferences;
|
||||
use Appwrite\Utopia\Response\Model\HealthAntivirus;
|
||||
use Appwrite\Utopia\Response\Model\HealthCertificate;
|
||||
use Appwrite\Utopia\Response\Model\HealthQueue;
|
||||
use Appwrite\Utopia\Response\Model\HealthStatus;
|
||||
use Appwrite\Utopia\Response\Model\HealthTime;
|
||||
use Appwrite\Utopia\Response\Model\HealthVersion;
|
||||
use Appwrite\Utopia\Response\Model\MFAChallenge;
|
||||
use Appwrite\Utopia\Response\Model\MFAProvider;
|
||||
use Appwrite\Utopia\Response\Model\MFAProviders;
|
||||
use Appwrite\Utopia\Response\Model\Installation;
|
||||
use Appwrite\Utopia\Response\Model\LocaleCode;
|
||||
use Appwrite\Utopia\Response\Model\MetricBreakdown;
|
||||
use Appwrite\Utopia\Response\Model\Provider;
|
||||
use Appwrite\Utopia\Response\Model\Message;
|
||||
use Appwrite\Utopia\Response\Model\MFAFactors;
|
||||
use Appwrite\Utopia\Response\Model\MFAType;
|
||||
use Appwrite\Utopia\Response\Model\Subscriber;
|
||||
use Appwrite\Utopia\Response\Model\Topic;
|
||||
use Appwrite\Utopia\Response\Model\ProviderRepository;
|
||||
@@ -169,8 +170,8 @@ class Response extends SwooleResponse
|
||||
public const MODEL_PREFERENCES = 'preferences';
|
||||
|
||||
// MFA
|
||||
public const MODEL_MFA_PROVIDER = 'mfaProvider';
|
||||
public const MODEL_MFA_PROVIDERS = 'mfaProviders';
|
||||
public const MODEL_MFA_TYPE = 'mfaType';
|
||||
public const MODEL_MFA_FACTORS = 'mfaFactors';
|
||||
public const MODEL_MFA_OTP = 'mfaTotp';
|
||||
public const MODEL_MFA_CHALLENGE = 'mfaChallenge';
|
||||
|
||||
@@ -279,6 +280,7 @@ class Response extends SwooleResponse
|
||||
public const MODEL_HEALTH_QUEUE = 'healthQueue';
|
||||
public const MODEL_HEALTH_TIME = 'healthTime';
|
||||
public const MODEL_HEALTH_ANTIVIRUS = 'healthAntivirus';
|
||||
public const MODEL_HEALTH_CERTIFICATE = 'healthCertificate';
|
||||
public const MODEL_HEALTH_STATUS_LIST = 'healthStatusList';
|
||||
|
||||
// Console
|
||||
@@ -421,6 +423,7 @@ class Response extends SwooleResponse
|
||||
->setModel(new HealthAntivirus())
|
||||
->setModel(new HealthQueue())
|
||||
->setModel(new HealthStatus())
|
||||
->setModel(new HealthCertificate())
|
||||
->setModel(new HealthTime())
|
||||
->setModel(new HealthVersion())
|
||||
->setModel(new Metric())
|
||||
@@ -440,8 +443,8 @@ class Response extends SwooleResponse
|
||||
->setModel(new TemplateEmail())
|
||||
->setModel(new ConsoleVariables())
|
||||
->setModel(new MFAChallenge())
|
||||
->setModel(new MFAProvider())
|
||||
->setModel(new MFAProviders())
|
||||
->setModel(new MFAType())
|
||||
->setModel(new MFAFactors())
|
||||
->setModel(new Provider())
|
||||
->setModel(new Message())
|
||||
->setModel(new Topic())
|
||||
|
||||
@@ -0,0 +1,48 @@
|
||||
<?php
|
||||
|
||||
namespace Appwrite\Utopia\Response\Filters;
|
||||
|
||||
use Appwrite\Utopia\Response;
|
||||
use Appwrite\Utopia\Response\Filter;
|
||||
|
||||
class V17 extends Filter
|
||||
{
|
||||
// Convert 1.5 Data format to 1.4 format
|
||||
public function parse(array $content, string $model): array
|
||||
{
|
||||
$parsedResponse = $content;
|
||||
|
||||
switch ($model) {
|
||||
case Response::MODEL_PROJECT:
|
||||
$parsedResponse = $this->parseProject($parsedResponse);
|
||||
break;
|
||||
case Response::MODEL_USER:
|
||||
$parsedResponse = $this->parseUser($parsedResponse);
|
||||
break;
|
||||
case Response::MODEL_TOKEN:
|
||||
$parsedResponse = $this->parseToken($parsedResponse);
|
||||
break;
|
||||
}
|
||||
|
||||
return $parsedResponse;
|
||||
}
|
||||
|
||||
protected function parseUser(array $content)
|
||||
{
|
||||
unset($content['targets']);
|
||||
return $content;
|
||||
}
|
||||
|
||||
protected function parseProject(array $content)
|
||||
{
|
||||
$content['providers'] = $content['oAuthProviders'];
|
||||
unset($content['oAuthProviders']);
|
||||
return $content;
|
||||
}
|
||||
|
||||
protected function parseToken(array $content)
|
||||
{
|
||||
unset($content['phrase']);
|
||||
return $content;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,71 @@
|
||||
<?php
|
||||
|
||||
namespace Appwrite\Utopia\Response\Model;
|
||||
|
||||
use Appwrite\Utopia\Response;
|
||||
use Appwrite\Utopia\Response\Model;
|
||||
|
||||
class HealthCertificate extends Model
|
||||
{
|
||||
public function __construct()
|
||||
{
|
||||
$this
|
||||
->addRule('name', [
|
||||
'type' => self::TYPE_STRING,
|
||||
'description' => 'Certificate name',
|
||||
'default' => '',
|
||||
'example' => '/CN=www.google.com',
|
||||
])
|
||||
->addRule('subjectSN', [
|
||||
'type' => self::TYPE_STRING,
|
||||
'description' => 'Subject SN',
|
||||
'default' => 'www.google.com',
|
||||
'example' => '',
|
||||
])
|
||||
->addRule('issuerOrganisation', [
|
||||
'type' => self::TYPE_STRING,
|
||||
'description' => 'Issuer organisation',
|
||||
'default' => 'Google Trust Services LLC',
|
||||
'example' => '',
|
||||
])
|
||||
->addRule('validFrom', [
|
||||
'type' => self::TYPE_STRING,
|
||||
'description' => 'Valid from',
|
||||
'default' => '',
|
||||
'example' => '1704200998',
|
||||
])
|
||||
->addRule('validTo', [
|
||||
'type' => self::TYPE_STRING,
|
||||
'description' => 'Valid to',
|
||||
'default' => '',
|
||||
'example' => '1711458597',
|
||||
])
|
||||
->addRule('signatureTypeSN', [
|
||||
'type' => self::TYPE_STRING,
|
||||
'description' => 'Signature type SN',
|
||||
'default' => '',
|
||||
'example' => 'RSA-SHA256',
|
||||
])
|
||||
;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get Name
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function getName(): string
|
||||
{
|
||||
return 'Health Certificate';
|
||||
}
|
||||
|
||||
/**
|
||||
* Get Type
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function getType(): string
|
||||
{
|
||||
return Response::MODEL_HEALTH_CERTIFICATE;
|
||||
}
|
||||
}
|
||||
+3
-3
@@ -5,7 +5,7 @@ namespace Appwrite\Utopia\Response\Model;
|
||||
use Appwrite\Utopia\Response;
|
||||
use Appwrite\Utopia\Response\Model;
|
||||
|
||||
class MFAProviders extends Model
|
||||
class MFAFactors extends Model
|
||||
{
|
||||
public function __construct()
|
||||
{
|
||||
@@ -38,7 +38,7 @@ class MFAProviders extends Model
|
||||
*/
|
||||
public function getName(): string
|
||||
{
|
||||
return 'MFAProviders';
|
||||
return 'MFAFactors';
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -48,6 +48,6 @@ class MFAProviders extends Model
|
||||
*/
|
||||
public function getType(): string
|
||||
{
|
||||
return Response::MODEL_MFA_PROVIDERS;
|
||||
return Response::MODEL_MFA_FACTORS;
|
||||
}
|
||||
}
|
||||
+3
-3
@@ -5,7 +5,7 @@ namespace Appwrite\Utopia\Response\Model;
|
||||
use Appwrite\Utopia\Response;
|
||||
use Appwrite\Utopia\Response\Model;
|
||||
|
||||
class MFAProvider extends Model
|
||||
class MFAType extends Model
|
||||
{
|
||||
public function __construct()
|
||||
{
|
||||
@@ -39,7 +39,7 @@ class MFAProvider extends Model
|
||||
*/
|
||||
public function getName(): string
|
||||
{
|
||||
return 'MFAProvider';
|
||||
return 'MFAType';
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -49,6 +49,6 @@ class MFAProvider extends Model
|
||||
*/
|
||||
public function getType(): string
|
||||
{
|
||||
return Response::MODEL_MFA_PROVIDER;
|
||||
return Response::MODEL_MFA_TYPE;
|
||||
}
|
||||
}
|
||||
@@ -34,11 +34,30 @@ class Topic extends Model
|
||||
'default' => '',
|
||||
'example' => 'events',
|
||||
])
|
||||
->addRule('total', [
|
||||
->addRule('emailTotal', [
|
||||
'type' => self::TYPE_INTEGER,
|
||||
'description' => 'Total count of subscribers subscribed to topic.',
|
||||
'description' => 'Total count of email subscribers subscribed to the topic.',
|
||||
'default' => 0,
|
||||
'example' => 100,
|
||||
])
|
||||
->addRule('smsTotal', [
|
||||
'type' => self::TYPE_INTEGER,
|
||||
'description' => 'Total count of SMS subscribers subscribed to the topic.',
|
||||
'default' => 0,
|
||||
'example' => 100,
|
||||
])
|
||||
->addRule('pushTotal', [
|
||||
'type' => self::TYPE_INTEGER,
|
||||
'description' => 'Total count of push subscribers subscribed to the topic.',
|
||||
'default' => 0,
|
||||
'example' => 100,
|
||||
])
|
||||
->addRule('subscribe', [
|
||||
'type' => self::TYPE_STRING,
|
||||
'description' => 'Subscribe permissions.',
|
||||
'default' => ['users'],
|
||||
'example' => 'users',
|
||||
'array' => true,
|
||||
]);
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,158 @@
|
||||
<?php
|
||||
|
||||
namespace Tests\E2E\General;
|
||||
|
||||
use Appwrite\Extend\Exception;
|
||||
use Appwrite\ID;
|
||||
use Tests\E2E\Client;
|
||||
use Tests\E2E\Scopes\ProjectConsole;
|
||||
use Tests\E2E\Scopes\Scope;
|
||||
use Tests\E2E\Scopes\SideClient;
|
||||
|
||||
class HooksTest extends Scope
|
||||
{
|
||||
use ProjectConsole;
|
||||
use SideClient;
|
||||
|
||||
public function setUp(): void
|
||||
{
|
||||
parent::setUp();
|
||||
$this->client->setEndpoint('http://localhost');
|
||||
}
|
||||
|
||||
public function testProjectHooks()
|
||||
{
|
||||
/**
|
||||
* Test for api controllers
|
||||
*/
|
||||
$response = $this->client->call(Client::METHOD_GET, '/v1/locale', \array_merge([
|
||||
'origin' => 'http://localhost',
|
||||
'content-type' => 'application/json',
|
||||
]), [
|
||||
'project' => 'console'
|
||||
]);
|
||||
|
||||
$this->assertEquals(200, $response['headers']['status-code']);
|
||||
|
||||
$response = $this->client->call(Client::METHOD_GET, '/v1/locale', \array_merge([
|
||||
'origin' => 'http://localhost',
|
||||
'content-type' => 'application/json',
|
||||
]), [
|
||||
'project' => '$this_project_doesnt_exist'
|
||||
]);
|
||||
|
||||
$this->assertEquals(404, $response['headers']['status-code']);
|
||||
|
||||
/**
|
||||
* Test for web controllers
|
||||
*/
|
||||
$response = $this->client->call(Client::METHOD_GET, headers: [
|
||||
'origin' => 'http://localhost',
|
||||
'content-type' => 'application/json',
|
||||
], params: [
|
||||
'project' => 'console'
|
||||
]);
|
||||
|
||||
$this->assertEquals(200, $response['headers']['status-code']);
|
||||
|
||||
$response = $this->client->call(Client::METHOD_GET, headers: [
|
||||
'origin' => 'http://localhost',
|
||||
'content-type' => 'application/json',
|
||||
], params: [
|
||||
'project' => '$this_project_doesnt_exist'
|
||||
]);
|
||||
|
||||
$this->assertEquals(200, $response['headers']['status-code']);
|
||||
}
|
||||
|
||||
public function testUserHooks()
|
||||
{
|
||||
/**
|
||||
* Setup blocked user
|
||||
*/
|
||||
$email = uniqid() . 'user@localhost.test';
|
||||
$password = 'password';
|
||||
|
||||
$response = $this->client->call(Client::METHOD_POST, '/v1/account', [
|
||||
'origin' => 'http://localhost',
|
||||
'content-type' => 'application/json',
|
||||
'x-appwrite-project' => $this->getProject()['$id'],
|
||||
], [
|
||||
'userId' => ID::unique(),
|
||||
'email' => $email,
|
||||
'password' => $password,
|
||||
]);
|
||||
|
||||
$id = $response['body']['$id'];
|
||||
|
||||
$this->assertEquals(201, $response['headers']['status-code']);
|
||||
|
||||
$response = $this->client->call(Client::METHOD_POST, '/v1/account/sessions/email', [
|
||||
'origin' => 'http://localhost',
|
||||
'content-type' => 'application/json',
|
||||
'x-appwrite-project' => $this->getProject()['$id'],
|
||||
], [
|
||||
'email' => $email,
|
||||
'password' => $password,
|
||||
]);
|
||||
|
||||
$this->assertEquals(201, $response['headers']['status-code']);
|
||||
|
||||
$session = $response['cookies']['a_session_' . $this->getProject()['$id']];
|
||||
$cookie = 'a_session_' . $this->getProject()['$id'] . '=' . $session;
|
||||
|
||||
$response = $this->client->call(Client::METHOD_GET, '/v1/account', [
|
||||
'origin' => 'http://localhost',
|
||||
'content-type' => 'application/json',
|
||||
'x-appwrite-project' => $this->getProject()['$id'],
|
||||
'cookie' => $cookie,
|
||||
]);
|
||||
|
||||
$this->assertEquals(200, $response['headers']['status-code']);
|
||||
|
||||
$response = $this->client->call(Client::METHOD_PATCH, '/v1/account/status', [
|
||||
'origin' => 'http://localhost',
|
||||
'content-type' => 'application/json',
|
||||
'x-appwrite-project' => $this->getProject()['$id'],
|
||||
'cookie' => $cookie,
|
||||
], [
|
||||
'status' => false,
|
||||
]);
|
||||
|
||||
$this->assertEquals(200, $response['headers']['status-code']);
|
||||
|
||||
$response = $this->client->call(Client::METHOD_GET, '/v1/account', [
|
||||
'origin' => 'http://localhost',
|
||||
'content-type' => 'application/json',
|
||||
'x-appwrite-project' => $this->getProject()['$id'],
|
||||
'cookie' => $cookie,
|
||||
]);
|
||||
|
||||
$this->assertEquals(401, $response['headers']['status-code']);
|
||||
|
||||
/**
|
||||
* Test for api controllers
|
||||
*/
|
||||
$response = $this->client->call(Client::METHOD_GET, '/v1/locale', [
|
||||
'origin' => 'http://localhost',
|
||||
'content-type' => 'application/json',
|
||||
'x-appwrite-project' => $this->getProject()['$id'],
|
||||
'cookie' => $cookie,
|
||||
]);
|
||||
|
||||
$this->assertEquals(401, $response['headers']['status-code']);
|
||||
$this->assertEquals(Exception::USER_BLOCKED, $response['body']['type']);
|
||||
|
||||
/**
|
||||
* Test for web controllers
|
||||
*/
|
||||
$response = $this->client->call(Client::METHOD_GET, headers: [
|
||||
'origin' => 'http://localhost',
|
||||
'content-type' => 'application/json',
|
||||
'x-appwrite-project' => $this->getProject()['$id'],
|
||||
'cookie' => $cookie,
|
||||
]);
|
||||
|
||||
$this->assertEquals(200, $response['headers']['status-code']);
|
||||
}
|
||||
}
|
||||
@@ -1918,7 +1918,7 @@ class AccountCustomClientTest extends Scope
|
||||
$this->assertEquals($response['body']['users'][0]['email'], $email);
|
||||
}
|
||||
|
||||
|
||||
#[Retry(count: 2)]
|
||||
public function testCreatePhone(): array
|
||||
{
|
||||
$number = '+123456789';
|
||||
@@ -1941,22 +1941,8 @@ class AccountCustomClientTest extends Scope
|
||||
$this->assertEquals(true, (new DatetimeValidator())->isValid($response['body']['expire']));
|
||||
|
||||
$userId = $response['body']['userId'];
|
||||
$messageId = $response['body']['$id'];
|
||||
|
||||
/**
|
||||
* Test for FAILURE
|
||||
*/
|
||||
$response = $this->client->call(Client::METHOD_POST, '/account/tokens/phone', array_merge([
|
||||
'origin' => 'http://localhost',
|
||||
'content-type' => 'application/json',
|
||||
'x-appwrite-project' => $this->getProject()['$id'],
|
||||
]), [
|
||||
'userId' => ID::unique()
|
||||
]);
|
||||
|
||||
$this->assertEquals(400, $response['headers']['status-code']);
|
||||
|
||||
\sleep(5);
|
||||
\sleep(7);
|
||||
|
||||
$smsRequest = $this->getLastRequest();
|
||||
|
||||
@@ -1972,6 +1958,19 @@ class AccountCustomClientTest extends Scope
|
||||
$data['id'] = $userId;
|
||||
$data['number'] = $number;
|
||||
|
||||
/**
|
||||
* Test for FAILURE
|
||||
*/
|
||||
$response = $this->client->call(Client::METHOD_POST, '/account/tokens/phone', array_merge([
|
||||
'origin' => 'http://localhost',
|
||||
'content-type' => 'application/json',
|
||||
'x-appwrite-project' => $this->getProject()['$id'],
|
||||
]), [
|
||||
'userId' => ID::unique()
|
||||
]);
|
||||
|
||||
$this->assertEquals(400, $response['headers']['status-code']);
|
||||
|
||||
return $data;
|
||||
}
|
||||
|
||||
@@ -2227,7 +2226,7 @@ class AccountCustomClientTest extends Scope
|
||||
/**
|
||||
* @depends testUpdatePhone
|
||||
*/
|
||||
#[Retry(count: 1)]
|
||||
#[Retry(count: 2)]
|
||||
public function testPhoneVerification(array $data): array
|
||||
{
|
||||
$session = $data['session'] ?? '';
|
||||
@@ -2248,7 +2247,7 @@ class AccountCustomClientTest extends Scope
|
||||
$this->assertEmpty($response['body']['secret']);
|
||||
$this->assertEquals(true, (new DatetimeValidator())->isValid($response['body']['expire']));
|
||||
|
||||
\sleep(5);
|
||||
\sleep(10);
|
||||
|
||||
$smsRequest = $this->getLastRequest();
|
||||
|
||||
|
||||
@@ -1268,7 +1268,7 @@ trait DatabasesBase
|
||||
$this->assertCount(1, $releaseYearIndex['body']['attributes']);
|
||||
$this->assertEquals('releaseYear', $releaseYearIndex['body']['attributes'][0]);
|
||||
|
||||
$releaseWithDate = $this->client->call(Client::METHOD_POST, '/databases/' . $databaseId . '/collections/' . $data['moviesId'] . '/indexes', array_merge([
|
||||
$releaseWithDate1 = $this->client->call(Client::METHOD_POST, '/databases/' . $databaseId . '/collections/' . $data['moviesId'] . '/indexes', array_merge([
|
||||
'content-type' => 'application/json',
|
||||
'x-appwrite-project' => $this->getProject()['$id'],
|
||||
'x-appwrite-key' => $this->getProject()['apiKey']
|
||||
@@ -1278,33 +1278,15 @@ trait DatabasesBase
|
||||
'attributes' => ['releaseYear', '$createdAt', '$updatedAt'],
|
||||
]);
|
||||
|
||||
$this->assertEquals(202, $releaseWithDate['headers']['status-code']);
|
||||
$this->assertEquals('releaseYearDated', $releaseWithDate['body']['key']);
|
||||
$this->assertEquals('key', $releaseWithDate['body']['type']);
|
||||
$this->assertCount(3, $releaseWithDate['body']['attributes']);
|
||||
$this->assertEquals('releaseYear', $releaseWithDate['body']['attributes'][0]);
|
||||
$this->assertEquals('$createdAt', $releaseWithDate['body']['attributes'][1]);
|
||||
$this->assertEquals('$updatedAt', $releaseWithDate['body']['attributes'][2]);
|
||||
$this->assertEquals(202, $releaseWithDate1['headers']['status-code']);
|
||||
$this->assertEquals('releaseYearDated', $releaseWithDate1['body']['key']);
|
||||
$this->assertEquals('key', $releaseWithDate1['body']['type']);
|
||||
$this->assertCount(3, $releaseWithDate1['body']['attributes']);
|
||||
$this->assertEquals('releaseYear', $releaseWithDate1['body']['attributes'][0]);
|
||||
$this->assertEquals('$createdAt', $releaseWithDate1['body']['attributes'][1]);
|
||||
$this->assertEquals('$updatedAt', $releaseWithDate1['body']['attributes'][2]);
|
||||
|
||||
// wait for database worker to create index
|
||||
sleep(2);
|
||||
|
||||
$movies = $this->client->call(Client::METHOD_GET, '/databases/' . $databaseId . '/collections/' . $data['moviesId'], array_merge([
|
||||
'content-type' => 'application/json',
|
||||
'x-appwrite-project' => $this->getProject()['$id'],
|
||||
'x-appwrite-key' => $this->getProject()['apiKey']
|
||||
]), []);
|
||||
|
||||
$this->assertIsArray($movies['body']['indexes']);
|
||||
$this->assertCount(3, $movies['body']['indexes']);
|
||||
$this->assertEquals($titleIndex['body']['key'], $movies['body']['indexes'][0]['key']);
|
||||
$this->assertEquals($releaseYearIndex['body']['key'], $movies['body']['indexes'][1]['key']);
|
||||
$this->assertEquals($releaseWithDate['body']['key'], $movies['body']['indexes'][2]['key']);
|
||||
$this->assertEquals('available', $movies['body']['indexes'][0]['status']);
|
||||
$this->assertEquals('available', $movies['body']['indexes'][1]['status']);
|
||||
$this->assertEquals('available', $movies['body']['indexes'][2]['status']);
|
||||
|
||||
$releaseWithDate = $this->client->call(Client::METHOD_POST, '/databases/' . $databaseId . '/collections/' . $data['moviesId'] . '/indexes', array_merge([
|
||||
$releaseWithDate2 = $this->client->call(Client::METHOD_POST, '/databases/' . $databaseId . '/collections/' . $data['moviesId'] . '/indexes', array_merge([
|
||||
'content-type' => 'application/json',
|
||||
'x-appwrite-project' => $this->getProject()['$id'],
|
||||
'x-appwrite-key' => $this->getProject()['apiKey']
|
||||
@@ -1314,11 +1296,11 @@ trait DatabasesBase
|
||||
'attributes' => ['birthDay'],
|
||||
]);
|
||||
|
||||
$this->assertEquals(202, $releaseWithDate['headers']['status-code']);
|
||||
$this->assertEquals('birthDay', $releaseWithDate['body']['key']);
|
||||
$this->assertEquals('key', $releaseWithDate['body']['type']);
|
||||
$this->assertCount(1, $releaseWithDate['body']['attributes']);
|
||||
$this->assertEquals('birthDay', $releaseWithDate['body']['attributes'][0]);
|
||||
$this->assertEquals(202, $releaseWithDate2['headers']['status-code']);
|
||||
$this->assertEquals('birthDay', $releaseWithDate2['body']['key']);
|
||||
$this->assertEquals('key', $releaseWithDate2['body']['type']);
|
||||
$this->assertCount(1, $releaseWithDate2['body']['attributes']);
|
||||
$this->assertEquals('birthDay', $releaseWithDate2['body']['attributes'][0]);
|
||||
|
||||
// Test for failure
|
||||
$fulltextReleaseYear = $this->client->call(Client::METHOD_POST, '/databases/' . $databaseId . '/collections/' . $data['moviesId'] . '/indexes', array_merge([
|
||||
@@ -1406,9 +1388,12 @@ trait DatabasesBase
|
||||
'key' => 'index-ip-actors',
|
||||
'type' => 'key',
|
||||
'attributes' => ['releaseYear', 'actors'], // 2 levels
|
||||
'orders' => ['DESC', 'DESC'],
|
||||
]);
|
||||
|
||||
$this->assertEquals(202, $twoLevelsArray['headers']['status-code']);
|
||||
$this->assertEquals('DESC', $twoLevelsArray['body']['orders'][0]);
|
||||
$this->assertEquals(null, $twoLevelsArray['body']['orders'][1]); // Overwrite by API (array)
|
||||
|
||||
$unknown = $this->client->call(Client::METHOD_POST, '/databases/' . $databaseId . '/collections/' . $data['moviesId'] . '/indexes', array_merge([
|
||||
'content-type' => 'application/json',
|
||||
@@ -1423,6 +1408,50 @@ trait DatabasesBase
|
||||
$this->assertEquals(400, $unknown['headers']['status-code']);
|
||||
$this->assertEquals('Unknown attribute: Unknown', $unknown['body']['message']);
|
||||
|
||||
$index1 = $this->client->call(Client::METHOD_POST, '/databases/' . $databaseId . '/collections/' . $data['moviesId'] . '/indexes', array_merge([
|
||||
'content-type' => 'application/json',
|
||||
'x-appwrite-project' => $this->getProject()['$id'],
|
||||
'x-appwrite-key' => $this->getProject()['apiKey'],
|
||||
]), [
|
||||
'key' => 'integers-order',
|
||||
'type' => 'key',
|
||||
'attributes' => ['integers'], // array attribute
|
||||
'orders' => ['DESC'], // Check order is removed in API
|
||||
]);
|
||||
$this->assertEquals(202, $index1['headers']['status-code']);
|
||||
|
||||
$index2 = $this->client->call(Client::METHOD_POST, '/databases/' . $databaseId . '/collections/' . $data['moviesId'] . '/indexes', array_merge([
|
||||
'content-type' => 'application/json',
|
||||
'x-appwrite-project' => $this->getProject()['$id'],
|
||||
'x-appwrite-key' => $this->getProject()['apiKey'],
|
||||
]), [
|
||||
'key' => 'integers-size',
|
||||
'type' => 'key',
|
||||
'attributes' => ['integers'], // array attribute
|
||||
]);
|
||||
$this->assertEquals(202, $index2['headers']['status-code']);
|
||||
|
||||
/**
|
||||
* Create Indexes by worker
|
||||
*/
|
||||
sleep(2);
|
||||
|
||||
$movies = $this->client->call(Client::METHOD_GET, '/databases/' . $databaseId . '/collections/' . $data['moviesId'], array_merge([
|
||||
'content-type' => 'application/json',
|
||||
'x-appwrite-project' => $this->getProject()['$id'],
|
||||
'x-appwrite-key' => $this->getProject()['apiKey']
|
||||
]), []);
|
||||
|
||||
$this->assertIsArray($movies['body']['indexes']);
|
||||
$this->assertCount(8, $movies['body']['indexes']);
|
||||
$this->assertEquals($titleIndex['body']['key'], $movies['body']['indexes'][0]['key']);
|
||||
$this->assertEquals($releaseYearIndex['body']['key'], $movies['body']['indexes'][1]['key']);
|
||||
$this->assertEquals($releaseWithDate1['body']['key'], $movies['body']['indexes'][2]['key']);
|
||||
$this->assertEquals($releaseWithDate2['body']['key'], $movies['body']['indexes'][3]['key']);
|
||||
foreach ($movies['body']['indexes'] as $index) {
|
||||
$this->assertEquals('available', $index['status']);
|
||||
}
|
||||
|
||||
return $data;
|
||||
}
|
||||
|
||||
@@ -2034,6 +2063,18 @@ trait DatabasesBase
|
||||
$this->assertEquals(2017, $documents['body']['documents'][1]['releaseYear']);
|
||||
$this->assertCount(2, $documents['body']['documents']);
|
||||
|
||||
$documents = $this->client->call(Client::METHOD_GET, '/databases/' . $databaseId . '/collections/' . $data['moviesId'] . '/documents', array_merge([
|
||||
'content-type' => 'application/json',
|
||||
'x-appwrite-project' => $this->getProject()['$id'],
|
||||
], $this->getHeaders()), [
|
||||
'queries' => [
|
||||
'{"method":"contains","attribute":"title","values":[bad]}'
|
||||
],
|
||||
]);
|
||||
|
||||
$this->assertEquals(400, $documents['headers']['status-code']);
|
||||
$this->assertEquals('Invalid query: Syntax error', $documents['body']['message']);
|
||||
|
||||
$documents = $this->client->call(Client::METHOD_GET, '/databases/' . $databaseId . '/collections/' . $data['moviesId'] . '/documents', array_merge([
|
||||
'content-type' => 'application/json',
|
||||
'x-appwrite-project' => $this->getProject()['$id'],
|
||||
|
||||
@@ -89,9 +89,9 @@ class AccountTest extends Scope
|
||||
|
||||
$this->assertArrayNotHasKey('errors', $session['body']);
|
||||
$this->assertIsArray($session['body']['data']);
|
||||
$this->assertIsArray($session['body']['data']['accountCreateMagicURLSession']);
|
||||
$this->assertIsArray($session['body']['data']['accountCreateMagicURLToken']);
|
||||
|
||||
return $session['body']['data']['accountCreateMagicURLSession'];
|
||||
return $session['body']['data']['accountCreateMagicURLToken'];
|
||||
}
|
||||
|
||||
public function testCreateEmailVerification(): array
|
||||
|
||||
@@ -1208,7 +1208,7 @@ trait Base
|
||||
}';
|
||||
case self::$CREATE_MAGIC_URL:
|
||||
return 'mutation createMagicURL($userId: String!, $email: String!){
|
||||
accountCreateMagicURLSession(userId: $userId, email: $email) {
|
||||
accountCreateMagicURLToken(userId: $userId, email: $email) {
|
||||
userId
|
||||
expire
|
||||
}
|
||||
@@ -1832,8 +1832,8 @@ trait Base
|
||||
}
|
||||
}';
|
||||
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) {
|
||||
return 'mutation createTelesignProvider($providerId: String!, $name: String!, $from: String!, $customerId: String!, $apiKey: String!) {
|
||||
messagingCreateTelesignProvider(providerId: $providerId, name: $name, from: $from, customerId: $customerId, apiKey: $apiKey) {
|
||||
_id
|
||||
name
|
||||
provider
|
||||
@@ -1956,8 +1956,8 @@ trait Base
|
||||
}
|
||||
}';
|
||||
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) {
|
||||
return 'mutation updateTelesignProvider($providerId: String!, $name: String!, $customerId: String!, $apiKey: String!) {
|
||||
messagingUpdateTelesignProvider(providerId: $providerId, name: $name, customerId: $customerId, apiKey: $apiKey) {
|
||||
_id
|
||||
name
|
||||
provider
|
||||
@@ -2026,6 +2026,9 @@ trait Base
|
||||
messagingCreateTopic(topicId: $topicId, name: $name) {
|
||||
_id
|
||||
name
|
||||
emailTotal
|
||||
smsTotal
|
||||
pushTotal
|
||||
}
|
||||
}';
|
||||
case self::$LIST_TOPICS:
|
||||
@@ -2035,6 +2038,9 @@ trait Base
|
||||
topics {
|
||||
_id
|
||||
name
|
||||
emailTotal
|
||||
smsTotal
|
||||
pushTotal
|
||||
}
|
||||
}
|
||||
}';
|
||||
@@ -2043,6 +2049,9 @@ trait Base
|
||||
messagingGetTopic(topicId: $topicId) {
|
||||
_id
|
||||
name
|
||||
emailTotal
|
||||
smsTotal
|
||||
pushTotal
|
||||
}
|
||||
}';
|
||||
case self::$UPDATE_TOPIC:
|
||||
@@ -2050,6 +2059,9 @@ trait Base
|
||||
messagingUpdateTopic(topicId: $topicId, name: $name) {
|
||||
_id
|
||||
name
|
||||
emailTotal
|
||||
smsTotal
|
||||
pushTotal
|
||||
}
|
||||
}';
|
||||
case self::$DELETE_TOPIC:
|
||||
|
||||
@@ -45,8 +45,8 @@ class MessagingTest extends Scope
|
||||
'Telesign' => [
|
||||
'providerId' => ID::unique(),
|
||||
'name' => 'Telesign1',
|
||||
'username' => 'my-username',
|
||||
'password' => 'my-password',
|
||||
'customerId' => 'my-username',
|
||||
'apiKey' => 'my-password',
|
||||
'from' => '+123456789',
|
||||
],
|
||||
'Textmagic' => [
|
||||
@@ -104,7 +104,7 @@ class MessagingTest extends Scope
|
||||
'x-appwrite-key' => $this->getProject()['apiKey'],
|
||||
]), $graphQLPayload);
|
||||
|
||||
\array_push($providers, $response['body']['data']['messagingCreate' . $key . 'Provider']);
|
||||
$providers[] = $response['body']['data']['messagingCreate' . $key . 'Provider'];
|
||||
$this->assertEquals(200, $response['headers']['status-code']);
|
||||
$this->assertEquals($providersParams[$key]['name'], $response['body']['data']['messagingCreate' . $key . 'Provider']['name']);
|
||||
}
|
||||
@@ -138,8 +138,8 @@ class MessagingTest extends Scope
|
||||
'Telesign' => [
|
||||
'providerId' => $providers[3]['_id'],
|
||||
'name' => 'Telesign2',
|
||||
'username' => 'my-username',
|
||||
'password' => 'my-password',
|
||||
'customerId' => 'my-username',
|
||||
'apiKey' => 'my-password',
|
||||
],
|
||||
'Textmagic' => [
|
||||
'providerId' => $providers[4]['_id'],
|
||||
|
||||
@@ -424,4 +424,74 @@ class HealthCustomServerTest extends Scope
|
||||
|
||||
return [];
|
||||
}
|
||||
|
||||
public function testCertificateValidity(): array
|
||||
{
|
||||
/**
|
||||
* Test for SUCCESS
|
||||
*/
|
||||
$response = $this->client->call(Client::METHOD_GET, '/health/certificate?domain=www.google.com', array_merge([
|
||||
'content-type' => 'application/json',
|
||||
'x-appwrite-project' => $this->getProject()['$id'],
|
||||
], $this->getHeaders()), []);
|
||||
|
||||
$this->assertEquals(200, $response['headers']['status-code']);
|
||||
$this->assertEquals('/CN=www.google.com', $response['body']['name']);
|
||||
$this->assertEquals('www.google.com', $response['body']['subjectSN']);
|
||||
$this->assertEquals('Google Trust Services LLC', $response['body']['issuerOrganisation']);
|
||||
$this->assertIsInt($response['body']['validFrom']);
|
||||
$this->assertIsInt($response['body']['validTo']);
|
||||
|
||||
$response = $this->client->call(Client::METHOD_GET, '/health/certificate?domain=appwrite.io', array_merge([
|
||||
'content-type' => 'application/json',
|
||||
'x-appwrite-project' => $this->getProject()['$id'],
|
||||
], $this->getHeaders()), []);
|
||||
|
||||
$this->assertEquals(200, $response['headers']['status-code']);
|
||||
$this->assertEquals('/CN=appwrite.io', $response['body']['name']);
|
||||
$this->assertEquals('appwrite.io', $response['body']['subjectSN']);
|
||||
$this->assertEquals("Let's Encrypt", $response['body']['issuerOrganisation']);
|
||||
$this->assertIsInt($response['body']['validFrom']);
|
||||
$this->assertIsInt($response['body']['validTo']);
|
||||
|
||||
$response = $this->client->call(Client::METHOD_GET, '/health/certificate?domain=https://google.com', array_merge([
|
||||
'content-type' => 'application/json',
|
||||
'x-appwrite-project' => $this->getProject()['$id'],
|
||||
], $this->getHeaders()), []);
|
||||
|
||||
$this->assertEquals(200, $response['headers']['status-code']);
|
||||
|
||||
/**
|
||||
* Test for FAILURE
|
||||
*/
|
||||
$response = $this->client->call(Client::METHOD_GET, '/health/certificate?domain=localhost', array_merge([
|
||||
'content-type' => 'application/json',
|
||||
'x-appwrite-project' => $this->getProject()['$id'],
|
||||
], $this->getHeaders()), []);
|
||||
|
||||
$this->assertEquals(400, $response['headers']['status-code']);
|
||||
|
||||
$response = $this->client->call(Client::METHOD_GET, '/health/certificate?domain=doesnotexist.com', array_merge([
|
||||
'content-type' => 'application/json',
|
||||
'x-appwrite-project' => $this->getProject()['$id'],
|
||||
], $this->getHeaders()), []);
|
||||
|
||||
$this->assertEquals(404, $response['headers']['status-code']);
|
||||
|
||||
$response = $this->client->call(Client::METHOD_GET, '/health/certificate?domain=www.google.com/usr/src/local', array_merge([
|
||||
'content-type' => 'application/json',
|
||||
'x-appwrite-project' => $this->getProject()['$id'],
|
||||
], $this->getHeaders()), []);
|
||||
|
||||
$this->assertEquals(400, $response['headers']['status-code']);
|
||||
|
||||
$response = $this->client->call(Client::METHOD_GET, '/health/certificate?domain=', array_merge([
|
||||
'content-type' => 'application/json',
|
||||
'x-appwrite-project' => $this->getProject()['$id'],
|
||||
], $this->getHeaders()), []);
|
||||
|
||||
$this->assertEquals(400, $response['headers']['status-code']);
|
||||
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,7 +5,9 @@ namespace Tests\E2E\Services\Messaging;
|
||||
use Appwrite\Messaging\Status as MessageStatus;
|
||||
use Tests\E2E\Client;
|
||||
use Utopia\App;
|
||||
use Utopia\Database\Document;
|
||||
use Utopia\Database\Helpers\ID;
|
||||
use Utopia\Database\Helpers\Role;
|
||||
use Utopia\Database\Query;
|
||||
use Utopia\DSN\DSN;
|
||||
|
||||
@@ -50,8 +52,8 @@ trait MessagingBase
|
||||
'telesign' => [
|
||||
'providerId' => ID::unique(),
|
||||
'name' => 'Telesign1',
|
||||
'username' => 'my-username',
|
||||
'password' => 'my-password',
|
||||
'customerId' => 'my-username',
|
||||
'apiKey' => 'my-password',
|
||||
'from' => '+123456789',
|
||||
],
|
||||
'textmagic' => [
|
||||
@@ -141,8 +143,8 @@ trait MessagingBase
|
||||
],
|
||||
'telesign' => [
|
||||
'name' => 'Telesign2',
|
||||
'username' => 'my-username',
|
||||
'password' => 'my-password',
|
||||
'customerId' => 'my-username',
|
||||
'apiKey' => 'my-password',
|
||||
],
|
||||
'textmagic' => [
|
||||
'name' => 'Textmagic2',
|
||||
@@ -176,14 +178,26 @@ trait MessagingBase
|
||||
'bundleId' => 'my-bundleid',
|
||||
],
|
||||
];
|
||||
foreach (\array_keys($providersParams) as $index => $key) {
|
||||
$response = $this->client->call(Client::METHOD_PATCH, '/messaging/providers/' . $key . '/' . $providers[$index]['$id'], [
|
||||
|
||||
foreach (\array_keys($providersParams) as $index => $name) {
|
||||
$response = $this->client->call(Client::METHOD_PATCH, '/messaging/providers/' . $name . '/' . $providers[$index]['$id'], [
|
||||
'content-type' => 'application/json',
|
||||
'x-appwrite-project' => $this->getProject()['$id'],
|
||||
'x-appwrite-key' => $this->getProject()['apiKey'],
|
||||
], $providersParams[$key]);
|
||||
], $providersParams[$name]);
|
||||
|
||||
$this->assertEquals(200, $response['headers']['status-code']);
|
||||
$this->assertEquals($providersParams[$key]['name'], $response['body']['name']);
|
||||
$this->assertEquals($providersParams[$name]['name'], $response['body']['name']);
|
||||
|
||||
if ($name === 'smtp') {
|
||||
$this->assertArrayHasKey('encryption', $response['body']['options']);
|
||||
$this->assertArrayHasKey('autoTLS', $response['body']['options']);
|
||||
$this->assertArrayHasKey('mailer', $response['body']['options']);
|
||||
$this->assertArrayNotHasKey('encryption', $response['body']['credentials']);
|
||||
$this->assertArrayNotHasKey('autoTLS', $response['body']['credentials']);
|
||||
$this->assertArrayNotHasKey('mailer', $response['body']['credentials']);
|
||||
}
|
||||
|
||||
$providers[$index] = $response['body'];
|
||||
}
|
||||
|
||||
@@ -198,10 +212,13 @@ trait MessagingBase
|
||||
'isEuRegion' => true,
|
||||
'enabled' => false,
|
||||
]);
|
||||
|
||||
$this->assertEquals(200, $response['headers']['status-code']);
|
||||
$this->assertEquals('Mailgun2', $response['body']['name']);
|
||||
$this->assertEquals(false, $response['body']['enabled']);
|
||||
|
||||
$providers[1] = $response['body'];
|
||||
|
||||
return $providers;
|
||||
}
|
||||
|
||||
@@ -279,7 +296,7 @@ trait MessagingBase
|
||||
|
||||
public function testCreateTopic(): array
|
||||
{
|
||||
$response = $this->client->call(Client::METHOD_POST, '/messaging/topics', [
|
||||
$response1 = $this->client->call(Client::METHOD_POST, '/messaging/topics', [
|
||||
'content-type' => 'application/json',
|
||||
'x-appwrite-project' => $this->getProject()['$id'],
|
||||
'x-appwrite-key' => $this->getProject()['apiKey'],
|
||||
@@ -287,18 +304,34 @@ trait MessagingBase
|
||||
'topicId' => ID::unique(),
|
||||
'name' => 'my-app',
|
||||
]);
|
||||
$this->assertEquals(201, $response['headers']['status-code']);
|
||||
$this->assertEquals('my-app', $response['body']['name']);
|
||||
$this->assertEquals(201, $response1['headers']['status-code']);
|
||||
$this->assertEquals('my-app', $response1['body']['name']);
|
||||
|
||||
return $response['body'];
|
||||
$response2 = $this->client->call(Client::METHOD_POST, '/messaging/topics', [
|
||||
'content-type' => 'application/json',
|
||||
'x-appwrite-project' => $this->getProject()['$id'],
|
||||
'x-appwrite-key' => $this->getProject()['apiKey'],
|
||||
], [
|
||||
'topicId' => ID::unique(),
|
||||
'name' => 'my-app2',
|
||||
'subscribe' => [Role::user('invalid')->toString()],
|
||||
]);
|
||||
$this->assertEquals(201, $response2['headers']['status-code']);
|
||||
$this->assertEquals('my-app2', $response2['body']['name']);
|
||||
$this->assertEquals(1, \count($response2['body']['subscribe']));
|
||||
|
||||
return [
|
||||
'public' => $response1['body'],
|
||||
'private' => $response2['body'],
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* @depends testCreateTopic
|
||||
*/
|
||||
public function testUpdateTopic(array $topic): string
|
||||
public function testUpdateTopic(array $topics): string
|
||||
{
|
||||
$response = $this->client->call(Client::METHOD_PATCH, '/messaging/topics/' . $topic['$id'], [
|
||||
$response = $this->client->call(Client::METHOD_PATCH, '/messaging/topics/' . $topics['public']['$id'], [
|
||||
'content-type' => 'application/json',
|
||||
'x-appwrite-project' => $this->getProject()['$id'],
|
||||
'x-appwrite-key' => $this->getProject()['apiKey'],
|
||||
@@ -307,6 +340,7 @@ trait MessagingBase
|
||||
]);
|
||||
$this->assertEquals(200, $response['headers']['status-code']);
|
||||
$this->assertEquals('android-app', $response['body']['name']);
|
||||
|
||||
return $response['body']['$id'];
|
||||
}
|
||||
|
||||
@@ -321,12 +355,14 @@ trait MessagingBase
|
||||
'x-appwrite-key' => $this->getProject()['apiKey'],
|
||||
], [
|
||||
'queries' => [
|
||||
Query::equal('total', [0])->toString(),
|
||||
Query::equal('emailTotal', [0])->toString(),
|
||||
Query::equal('smsTotal', [0])->toString(),
|
||||
Query::equal('pushTotal', [0])->toString(),
|
||||
],
|
||||
]);
|
||||
|
||||
$this->assertEquals(200, $response['headers']['status-code']);
|
||||
$this->assertEquals(1, \count($response['body']['topics']));
|
||||
$this->assertEquals(2, \count($response['body']['topics']));
|
||||
|
||||
$response = $this->client->call(Client::METHOD_GET, '/messaging/topics', [
|
||||
'content-type' => 'application/json',
|
||||
@@ -334,7 +370,9 @@ trait MessagingBase
|
||||
'x-appwrite-key' => $this->getProject()['apiKey'],
|
||||
], [
|
||||
'queries' => [
|
||||
Query::greaterThan('total', 0)->toString(),
|
||||
Query::greaterThan('emailTotal', 0)->toString(),
|
||||
Query::greaterThan('smsTotal', 0)->toString(),
|
||||
Query::greaterThan('pushTotal', 0)->toString(),
|
||||
],
|
||||
]);
|
||||
|
||||
@@ -356,13 +394,15 @@ trait MessagingBase
|
||||
]);
|
||||
$this->assertEquals(200, $response['headers']['status-code']);
|
||||
$this->assertEquals('android-app', $response['body']['name']);
|
||||
$this->assertEquals(0, $response['body']['total']);
|
||||
$this->assertEquals(0, $response['body']['emailTotal']);
|
||||
$this->assertEquals(0, $response['body']['smsTotal']);
|
||||
$this->assertEquals(0, $response['body']['pushTotal']);
|
||||
}
|
||||
|
||||
/**
|
||||
* @depends testCreateTopic
|
||||
*/
|
||||
public function testCreateSubscriber(array $topic)
|
||||
public function testCreateSubscriber(array $topics)
|
||||
{
|
||||
$userId = $this->getUser()['$id'];
|
||||
|
||||
@@ -392,7 +432,7 @@ trait MessagingBase
|
||||
|
||||
$this->assertEquals(201, $target['headers']['status-code']);
|
||||
|
||||
$response = $this->client->call(Client::METHOD_POST, '/messaging/topics/' . $topic['$id'] . '/subscribers', \array_merge([
|
||||
$response = $this->client->call(Client::METHOD_POST, '/messaging/topics/' . $topics['public']['$id'] . '/subscribers', \array_merge([
|
||||
'content-type' => 'application/json',
|
||||
'x-appwrite-project' => $this->getProject()['$id'],
|
||||
], $this->getHeaders()), [
|
||||
@@ -404,7 +444,7 @@ trait MessagingBase
|
||||
$this->assertEquals($target['body']['userId'], $response['body']['target']['userId']);
|
||||
$this->assertEquals($target['body']['providerType'], $response['body']['target']['providerType']);
|
||||
|
||||
$topic = $this->client->call(Client::METHOD_GET, '/messaging/topics/' . $topic['$id'], [
|
||||
$topic = $this->client->call(Client::METHOD_GET, '/messaging/topics/' . $topics['public']['$id'], [
|
||||
'content-type' => 'application/json',
|
||||
'x-appwrite-project' => $this->getProject()['$id'],
|
||||
'x-appwrite-key' => $this->getProject()['apiKey'],
|
||||
@@ -412,7 +452,23 @@ trait MessagingBase
|
||||
|
||||
$this->assertEquals(200, $topic['headers']['status-code']);
|
||||
$this->assertEquals('android-app', $topic['body']['name']);
|
||||
$this->assertEquals(1, $topic['body']['total']);
|
||||
$this->assertEquals(1, $topic['body']['emailTotal']);
|
||||
$this->assertEquals(0, $topic['body']['smsTotal']);
|
||||
$this->assertEquals(0, $topic['body']['pushTotal']);
|
||||
|
||||
$response2 = $this->client->call(Client::METHOD_POST, '/messaging/topics/' . $topics['private']['$id'] . '/subscribers', \array_merge([
|
||||
'content-type' => 'application/json',
|
||||
'x-appwrite-project' => $this->getProject()['$id'],
|
||||
], $this->getHeaders()), [
|
||||
'subscriberId' => ID::unique(),
|
||||
'targetId' => $target['body']['$id'],
|
||||
]);
|
||||
|
||||
if ($this->getSide() === 'client') {
|
||||
$this->assertEquals(401, $response2['headers']['status-code']);
|
||||
} else {
|
||||
$this->assertEquals(201, $response2['headers']['status-code']);
|
||||
}
|
||||
|
||||
return [
|
||||
'topicId' => $topic['body']['$id'],
|
||||
@@ -647,7 +703,9 @@ trait MessagingBase
|
||||
|
||||
$this->assertEquals(200, $topic['headers']['status-code']);
|
||||
$this->assertEquals('android-app', $topic['body']['name']);
|
||||
$this->assertEquals(0, $topic['body']['total']);
|
||||
$this->assertEquals(0, $topic['body']['emailTotal']);
|
||||
$this->assertEquals(0, $topic['body']['smsTotal']);
|
||||
$this->assertEquals(0, $topic['body']['pushTotal']);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -682,12 +740,27 @@ trait MessagingBase
|
||||
'x-appwrite-key' => $this->getProject()['apiKey'],
|
||||
]);
|
||||
|
||||
$this->assertEquals(200, $response['headers']['status-code']);
|
||||
|
||||
$targetList = $response['body'];
|
||||
$this->assertEquals(1, $targetList['total']);
|
||||
$this->assertEquals(1, count($targetList['targets']));
|
||||
$this->assertEquals(2, $targetList['total']);
|
||||
$this->assertEquals(2, count($targetList['targets']));
|
||||
$this->assertEquals($message['targets'][0], $targetList['targets'][0]['$id']);
|
||||
$this->assertEquals($message['targets'][1], $targetList['targets'][1]['$id']);
|
||||
|
||||
/**
|
||||
* Cursor Test
|
||||
*/
|
||||
$response = $this->client->call(Client::METHOD_GET, '/messaging/messages/' . $message['$id'] . '/targets', [
|
||||
'content-type' => 'application/json',
|
||||
'x-appwrite-project' => $this->getProject()['$id'],
|
||||
'x-appwrite-key' => $this->getProject()['apiKey'],
|
||||
], [
|
||||
'queries' => [
|
||||
Query::cursorAfter(new Document(['$id' => $targetList['targets'][0]['$id']]))->toString(),
|
||||
]
|
||||
]);
|
||||
$this->assertEquals(2, $response['body']['total']);
|
||||
$this->assertEquals(1, count($response['body']['targets']));
|
||||
$this->assertEquals($targetList['targets'][1]['$id'], $response['body']['targets'][0]['$id']);
|
||||
|
||||
// Test for empty targets
|
||||
$response = $this->client->call(Client::METHOD_POST, '/messaging/messages/email', [
|
||||
@@ -719,7 +792,7 @@ trait MessagingBase
|
||||
|
||||
public function testCreateDraftEmail()
|
||||
{
|
||||
// Create User
|
||||
// Create User 1
|
||||
$response = $this->client->call(Client::METHOD_POST, '/users', [
|
||||
'content-type' => 'application/json',
|
||||
'x-appwrite-project' => $this->getProject()['$id'],
|
||||
@@ -728,15 +801,33 @@ trait MessagingBase
|
||||
'userId' => ID::unique(),
|
||||
'email' => uniqid() . "@example.com",
|
||||
'password' => 'password',
|
||||
'name' => 'Messaging User',
|
||||
'name' => 'Messaging User 1',
|
||||
]);
|
||||
|
||||
$this->assertEquals(201, $response['headers']['status-code'], "Error creating user: " . var_export($response['body'], true));
|
||||
|
||||
$user = $response['body'];
|
||||
$user1 = $response['body'];
|
||||
|
||||
$this->assertEquals(1, \count($user['targets']));
|
||||
$targetId = $user['targets'][0]['$id'];
|
||||
$this->assertEquals(1, \count($user1['targets']));
|
||||
$targetId1 = $user1['targets'][0]['$id'];
|
||||
|
||||
// Create User 2
|
||||
$response = $this->client->call(Client::METHOD_POST, '/users', [
|
||||
'content-type' => 'application/json',
|
||||
'x-appwrite-project' => $this->getProject()['$id'],
|
||||
'x-appwrite-key' => $this->getProject()['apiKey'],
|
||||
], [
|
||||
'userId' => ID::unique(),
|
||||
'email' => uniqid() . "@example.com",
|
||||
'password' => 'password',
|
||||
'name' => 'Messaging User 2',
|
||||
]);
|
||||
|
||||
$this->assertEquals(201, $response['headers']['status-code'], "Error creating user: " . var_export($response['body'], true));
|
||||
$user2 = $response['body'];
|
||||
|
||||
$this->assertEquals(1, \count($user2['targets']));
|
||||
$targetId2 = $user2['targets'][0]['$id'];
|
||||
|
||||
// Create Email
|
||||
$response = $this->client->call(Client::METHOD_POST, '/messaging/messages/email', [
|
||||
@@ -745,13 +836,12 @@ trait MessagingBase
|
||||
'x-appwrite-key' => $this->getProject()['apiKey'],
|
||||
], [
|
||||
'messageId' => ID::unique(),
|
||||
'targets' => [$targetId],
|
||||
'targets' => [$targetId1, $targetId2],
|
||||
'subject' => 'New blog post',
|
||||
'content' => 'Check out the new blog post at http://localhost',
|
||||
]);
|
||||
|
||||
$this->assertEquals(201, $response['headers']['status-code']);
|
||||
|
||||
$message = $response['body'];
|
||||
$this->assertEquals(MessageStatus::DRAFT, $message['status']);
|
||||
|
||||
|
||||
@@ -0,0 +1,89 @@
|
||||
<?php
|
||||
|
||||
namespace Tests\Unit\Utopia\Request\Filters;
|
||||
|
||||
use Appwrite\Utopia\Request\Filter;
|
||||
use Appwrite\Utopia\Request\Filters\V17;
|
||||
use PHPUnit\Framework\TestCase;
|
||||
|
||||
class V17Test extends TestCase
|
||||
{
|
||||
/**
|
||||
* @var Filter
|
||||
*/
|
||||
protected $filter;
|
||||
|
||||
public function setUp(): void
|
||||
{
|
||||
$this->filter = new V17();
|
||||
}
|
||||
|
||||
public function tearDown(): void
|
||||
{
|
||||
}
|
||||
|
||||
public function createUpdateRecoveryProvider()
|
||||
{
|
||||
return [
|
||||
'remove passwordAgain' => [
|
||||
[
|
||||
'userId' => 'test',
|
||||
'secret' => 'test',
|
||||
'password' => '123456',
|
||||
'passwordAgain' => '123456'
|
||||
],
|
||||
[
|
||||
'userId' => 'test',
|
||||
'secret' => 'test',
|
||||
'password' => '123456',
|
||||
]
|
||||
]
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* @dataProvider createUpdateRecoveryProvider
|
||||
*/
|
||||
public function testUpdateRecovery(array $content, array $expected): void
|
||||
{
|
||||
$model = 'account.updateRecovery';
|
||||
|
||||
$result = $this->filter->parse($content, $model);
|
||||
|
||||
$this->assertEquals($expected, $result);
|
||||
}
|
||||
|
||||
public function createQueryProvider()
|
||||
{
|
||||
return [
|
||||
'convert queries' => [
|
||||
[
|
||||
'queries' => [
|
||||
'cursorAfter("exampleId")',
|
||||
'search("name", ["example"])',
|
||||
'isNotNull("name")'
|
||||
]
|
||||
],
|
||||
[
|
||||
'queries' => [
|
||||
'{"method":"cursorAfter","values":["exampleId"]}',
|
||||
'{"method":"search","attribute":"name","values":["example"]}',
|
||||
'{"method":"isNotNull","attribute":"name"}'
|
||||
]
|
||||
],
|
||||
]
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* @dataProvider createQueryProvider
|
||||
*/
|
||||
public function testQuery(array $content, array $expected): void
|
||||
{
|
||||
$model = 'databases.getDocument';
|
||||
|
||||
$result = $this->filter->parse($content, $model);
|
||||
|
||||
$this->assertEquals($expected, $result);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,119 @@
|
||||
<?php
|
||||
|
||||
namespace Tests\Unit\Utopia\Response\Filters;
|
||||
|
||||
use Appwrite\Utopia\Response\Filters\V17;
|
||||
use Appwrite\Utopia\Response\Model;
|
||||
use Appwrite\Utopia\Response;
|
||||
use Cron\CronExpression;
|
||||
use PHPUnit\Framework\TestCase;
|
||||
use Utopia\Database\DateTime;
|
||||
|
||||
class V17Test extends TestCase
|
||||
{
|
||||
/**
|
||||
* @var Filter
|
||||
*/
|
||||
protected $filter = null;
|
||||
|
||||
public function setUp(): void
|
||||
{
|
||||
$this->filter = new V17();
|
||||
}
|
||||
|
||||
public function tearDown(): void
|
||||
{
|
||||
}
|
||||
|
||||
public function projectProvider(): array
|
||||
{
|
||||
return [
|
||||
'rename providers' => [
|
||||
[
|
||||
'oAuthProviders' => [
|
||||
[
|
||||
'key' => 'github',
|
||||
'name' => 'GitHub',
|
||||
'appId' => 'client_id',
|
||||
'secret' => 'client_secret',
|
||||
'enabled' => true,
|
||||
],
|
||||
],
|
||||
],
|
||||
[
|
||||
'providers' => [
|
||||
[
|
||||
'key' => 'github',
|
||||
'name' => 'GitHub',
|
||||
'appId' => 'client_id',
|
||||
'secret' => 'client_secret',
|
||||
'enabled' => true,
|
||||
],
|
||||
],
|
||||
],
|
||||
],
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* @dataProvider projectProvider
|
||||
*/
|
||||
public function testProject(array $content, array $expected): void
|
||||
{
|
||||
$model = Response::MODEL_PROJECT;
|
||||
|
||||
$result = $this->filter->parse($content, $model);
|
||||
|
||||
$this->assertEquals($expected, $result);
|
||||
}
|
||||
|
||||
public function userProvider(): array
|
||||
{
|
||||
return [
|
||||
'remove targets' => [
|
||||
[
|
||||
'targets' => 'test',
|
||||
],
|
||||
[
|
||||
],
|
||||
],
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* @dataProvider userProvider
|
||||
*/
|
||||
public function testUser(array $content, array $expected): void
|
||||
{
|
||||
$model = Response::MODEL_USER;
|
||||
|
||||
$result = $this->filter->parse($content, $model);
|
||||
|
||||
$this->assertEquals($expected, $result);
|
||||
}
|
||||
|
||||
public function tokenProvider(): array
|
||||
{
|
||||
return [
|
||||
'remove securityPhrase' => [
|
||||
[
|
||||
'phrase' => 'Lorum Ipsum',
|
||||
],
|
||||
[
|
||||
],
|
||||
],
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* @dataProvider tokenProvider
|
||||
*/
|
||||
public function testToken(array $content, array $expected): void
|
||||
{
|
||||
$model = Response::MODEL_TOKEN;
|
||||
|
||||
$result = $this->filter->parse($content, $model);
|
||||
|
||||
$this->assertEquals($expected, $result);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user