Merge remote-tracking branch 'origin/1.5.x' into feat-push-images
# Conflicts: # src/Appwrite/Extend/Exception.php
This commit is contained in:
commit
1bb75fdd63
43 changed files with 1483 additions and 364 deletions
2
.gitmodules
vendored
2
.gitmodules
vendored
|
@ -1,4 +1,4 @@
|
|||
[submodule "app/console"]
|
||||
path = app/console
|
||||
url = https://github.com/appwrite/console
|
||||
branch = 1.5.x
|
||||
branch = chore-update-sdk
|
||||
|
|
|
@ -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"
|
||||
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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
|
|
@ -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')
|
||||
|
|
|
@ -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),
|
||||
|
|
|
@ -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),
|
||||
|
|
|
@ -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())
|
||||
|
|
|
@ -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'])
|
||||
]);
|
||||
|
|
|
@ -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),
|
||||
|
|
|
@ -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),
|
||||
|
|
|
@ -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()
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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';
|
||||
|
|
|
@ -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';
|
||||
|
||||
|
|
|
@ -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',
|
||||
];
|
||||
|
||||
/**
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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),
|
||||
|
|
|
@ -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,
|
||||
]),
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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),
|
||||
|
|
|
@ -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']])
|
||||
]);
|
||||
|
|
|
@ -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[] = [
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -7,7 +7,9 @@ class Topics extends Base
|
|||
public const ALLOWED_ATTRIBUTES = [
|
||||
'name',
|
||||
'description',
|
||||
'total'
|
||||
'emailTotal',
|
||||
'smsTotal',
|
||||
'pushTotal',
|
||||
];
|
||||
|
||||
/**
|
||||
|
|
341
src/Appwrite/Utopia/Request/Filters/V17.php
Normal file
341
src/Appwrite/Utopia/Request/Filters/V17.php
Normal 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;
|
||||
}
|
||||
}
|
|
@ -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())
|
||||
|
|
48
src/Appwrite/Utopia/Response/Filters/V17.php
Normal file
48
src/Appwrite/Utopia/Response/Filters/V17.php
Normal 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;
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
}
|
|
@ -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,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,
|
||||
])
|
||||
|
|
158
tests/e2e/General/HooksTest.php
Normal file
158
tests/e2e/General/HooksTest.php
Normal 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']);
|
||||
}
|
||||
}
|
|
@ -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:
|
||||
|
|
|
@ -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']);
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
89
tests/unit/Utopia/Request/Filters/V17Test.php
Normal file
89
tests/unit/Utopia/Request/Filters/V17Test.php
Normal 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);
|
||||
}
|
||||
}
|
119
tests/unit/Utopia/Response/Filters/V17Test.php
Normal file
119
tests/unit/Utopia/Response/Filters/V17Test.php
Normal 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);
|
||||
}
|
||||
}
|
Loading…
Reference in a new issue