1
0
Fork 0
mirror of synced 2024-09-29 17:01:37 +13:00

Merge remote-tracking branch 'origin/1.5.x' into feat-push-images

# Conflicts:
#	src/Appwrite/Extend/Exception.php
This commit is contained in:
Jake Barnby 2024-02-19 20:44:11 +13:00
commit 1bb75fdd63
No known key found for this signature in database
GPG key ID: C437A8CC85B96E9C
43 changed files with 1483 additions and 364 deletions

2
.gitmodules vendored
View file

@ -1,4 +1,4 @@
[submodule "app/console"]
path = app/console
url = https://github.com/appwrite/console
branch = 1.5.x
branch = chore-update-sdk

View file

@ -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"

View file

@ -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'),
@ -1903,7 +1903,29 @@ $commonCollections = [
'filters' => [],
],
[
'$id' => ID::custom('total'),
'$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,

View file

@ -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,
@ -652,11 +652,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.',
@ -877,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 => [
@ -925,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,
],
];

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

@ -1 +1 @@
Subproject commit 1d942975d16397a252a58ab730fb57819d679213
Subproject commit 44edd461c6036cb462047c1424b80f0903cdc15e

View file

@ -163,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,
@ -707,7 +712,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())),
],
@ -1699,6 +1704,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,
@ -3425,10 +3435,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')
@ -3441,10 +3451,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')
@ -3455,24 +3465,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'));
@ -3481,7 +3491,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
@ -3500,10 +3510,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')
@ -3514,13 +3524,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')
@ -3528,9 +3538,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
};
@ -3540,9 +3550,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);
@ -3552,14 +3562,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')
@ -3574,16 +3584,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
};
@ -3610,40 +3620,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,
@ -3656,7 +3664,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');
@ -3720,7 +3728,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')

View file

@ -3612,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'])
]);
@ -3620,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),
@ -3696,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'])
]);
@ -3704,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),
@ -3782,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'])
]);
@ -3790,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),

View file

@ -492,7 +492,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'])
]);
@ -500,7 +500,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),
@ -584,7 +584,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'])
]);
@ -592,7 +592,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),

View file

@ -2262,7 +2262,19 @@ App::post('/v1/messaging/topics/:topicId/subscribers')
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);
}
@ -2313,7 +2325,7 @@ 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, we use array_filter and reset for reference $cursor to $queries
@ -2514,8 +2526,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())

View file

@ -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'])
]);

View file

@ -1539,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'])
]);
@ -1547,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),
@ -1624,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'])
]);
@ -1632,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),

View file

@ -115,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',
@ -132,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',
@ -498,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,
@ -1227,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',
@ -1305,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',
@ -1523,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')
@ -1551,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')
@ -1571,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
};
@ -1976,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'])
]);
@ -1984,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),

View file

@ -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()

View file

@ -6,7 +6,6 @@ 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 +21,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 +136,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 +144,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')
@ -162,10 +312,6 @@ App::init()
$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
*/
@ -296,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()) {
@ -497,7 +643,7 @@ App::shutdown()
'resource' => $resource,
'contentType' => $response->getContentType(),
'payload' => base64_encode($data['payload']),
]) ;
]);
$signature = md5($data);
$cacheLog = Authorization::skip(fn () => $dbForProject->getDocument('cache', $key));
@ -505,10 +651,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);

View file

@ -112,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';

View file

@ -193,7 +193,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';
@ -275,6 +274,9 @@ class Exception extends \Exception
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';

View file

@ -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',
];
/**

View file

@ -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;
}
}

View file

@ -269,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),

View file

@ -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,
]),

View file

@ -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;
@ -263,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
);
}
}
);
@ -457,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);

View file

@ -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),

View file

@ -249,7 +249,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']])
]);

View file

@ -69,6 +69,7 @@ class Usage extends Action
getProjectDB: $getProjectDB
);
}
self::$stats[$projectId]['project'] = $project;
foreach ($payload['metrics'] ?? [] as $metric) {
if (!isset(self::$stats[$projectId]['keys'][$metric['key']])) {
@ -105,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,
@ -124,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[] = [
@ -139,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[] = [
@ -158,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[] = [

View file

@ -67,7 +67,7 @@ class UsageHook extends Usage
$id = \md5("{$time}_{$period}_{$key}");
try {
$dbForProject->createDocument('stats_v2', new Document([
$dbForProject->createDocument('stats', new Document([
'$id' => $id,
'period' => $period,
'time' => $time,
@ -78,14 +78,14 @@ class UsageHook extends Usage
} catch (Duplicate $th) {
if ($value < 0) {
$dbForProject->decreaseDocumentAttribute(
'stats_v2',
'stats',
$id,
'value',
abs($value)
);
} else {
$dbForProject->increaseDocumentAttribute(
'stats_v2',
'stats',
$id,
'value',
$value

View file

@ -7,7 +7,9 @@ class Topics extends Base
public const ALLOWED_ATTRIBUTES = [
'name',
'description',
'total'
'emailTotal',
'smsTotal',
'pushTotal',
];
/**

View file

@ -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;
}
}

View file

@ -76,13 +76,13 @@ 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;
@ -170,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';
@ -443,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())

View file

@ -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;
}
}

View file

@ -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;
}
}

View file

@ -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;
}
}

View file

@ -34,9 +34,21 @@ 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,
])

View file

@ -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']);
}
}

View file

@ -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:

View file

@ -355,7 +355,9 @@ 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(),
],
]);
@ -368,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(),
],
]);
@ -390,7 +394,9 @@ 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']);
}
/**
@ -446,7 +452,9 @@ 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',
@ -695,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']);
}
/**

View file

@ -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);
}
}

View file

@ -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);
}
}