Merge branch '1.5.x' of https://github.com/appwrite/appwrite into feat-rc-sdks
This commit is contained in:
commit
ce4f92a6c8
|
@ -29,7 +29,7 @@ ENV VITE_APPWRITE_GROWTH_ENDPOINT=$VITE_APPWRITE_GROWTH_ENDPOINT
|
||||||
RUN npm ci
|
RUN npm ci
|
||||||
RUN npm run build
|
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"
|
LABEL maintainer="team@appwrite.io"
|
||||||
|
|
||||||
|
|
|
@ -1507,10 +1507,10 @@ $commonCollections = [
|
||||||
]
|
]
|
||||||
],
|
],
|
||||||
|
|
||||||
'stats_v2' => [
|
'stats' => [
|
||||||
'$collection' => ID::custom(Database::METADATA),
|
'$collection' => ID::custom(Database::METADATA),
|
||||||
'$id' => ID::custom('stats_v2'),
|
'$id' => ID::custom('stats'),
|
||||||
'name' => 'stats_v2',
|
'name' => 'Stats',
|
||||||
'attributes' => [
|
'attributes' => [
|
||||||
[
|
[
|
||||||
'$id' => ID::custom('metric'),
|
'$id' => ID::custom('metric'),
|
||||||
|
@ -1903,7 +1903,29 @@ $commonCollections = [
|
||||||
'filters' => [],
|
'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,
|
'type' => Database::VAR_INTEGER,
|
||||||
'format' => '',
|
'format' => '',
|
||||||
'size' => 0,
|
'size' => 0,
|
||||||
|
|
|
@ -245,7 +245,7 @@ return [
|
||||||
Exception::USER_MORE_FACTORS_REQUIRED => [
|
Exception::USER_MORE_FACTORS_REQUIRED => [
|
||||||
'name' => Exception::USER_MORE_FACTORS_REQUIRED,
|
'name' => Exception::USER_MORE_FACTORS_REQUIRED,
|
||||||
'description' => 'More factors are required to complete the sign in process.',
|
'description' => 'More factors are required to complete the sign in process.',
|
||||||
'code' => 400,
|
'code' => 401,
|
||||||
],
|
],
|
||||||
Exception::USER_OAUTH2_BAD_REQUEST => [
|
Exception::USER_OAUTH2_BAD_REQUEST => [
|
||||||
'name' => Exception::USER_OAUTH2_BAD_REQUEST,
|
'name' => Exception::USER_OAUTH2_BAD_REQUEST,
|
||||||
|
@ -647,11 +647,6 @@ return [
|
||||||
'description' => 'Project with the requested ID already exists. Try again with a different ID or use ID.unique() to generate a unique ID.',
|
'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,
|
'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 => [
|
Exception::PROJECT_PROVIDER_DISABLED => [
|
||||||
'name' => 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.',
|
'description' => 'The chosen OAuth provider is disabled. You can enable the OAuth provider using the Appwrite console.',
|
||||||
|
@ -872,7 +867,7 @@ return [
|
||||||
],
|
],
|
||||||
Exception::MESSAGE_MISSING_TARGET => [
|
Exception::MESSAGE_MISSING_TARGET => [
|
||||||
'name' => 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,
|
'code' => 400,
|
||||||
],
|
],
|
||||||
Exception::MESSAGE_ALREADY_SENT => [
|
Exception::MESSAGE_ALREADY_SENT => [
|
||||||
|
@ -920,4 +915,11 @@ return [
|
||||||
'description' => 'Schedule with the requested ID could not be found.',
|
'description' => 'Schedule with the requested ID could not be found.',
|
||||||
'code' => 404,
|
'code' => 404,
|
||||||
],
|
],
|
||||||
|
|
||||||
|
/** Targets */
|
||||||
|
Exception::TARGET_PROVIDER_INVALID_TYPE => [
|
||||||
|
'name' => Exception::TARGET_PROVIDER_INVALID_TYPE,
|
||||||
|
'description' => 'Target has an invalid provider type.',
|
||||||
|
'code' => 400,
|
||||||
|
],
|
||||||
];
|
];
|
||||||
|
|
|
@ -163,6 +163,11 @@ App::post('/v1/account')
|
||||||
$user = Authorization::skip(fn() => $dbForProject->createDocument('users', $user));
|
$user = Authorization::skip(fn() => $dbForProject->createDocument('users', $user));
|
||||||
try {
|
try {
|
||||||
$target = Authorization::skip(fn() => $dbForProject->createDocument('targets', new Document([
|
$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(),
|
'userId' => $user->getId(),
|
||||||
'userInternalId' => $user->getInternalId(),
|
'userInternalId' => $user->getInternalId(),
|
||||||
'providerType' => MESSAGE_TYPE_EMAIL,
|
'providerType' => MESSAGE_TYPE_EMAIL,
|
||||||
|
@ -707,7 +712,7 @@ App::get('/v1/account/sessions/oauth2/:provider/redirect')
|
||||||
$userDoc = Authorization::skip(fn() => $dbForProject->createDocument('users', $user));
|
$userDoc = Authorization::skip(fn() => $dbForProject->createDocument('users', $user));
|
||||||
$dbForProject->createDocument('targets', new Document([
|
$dbForProject->createDocument('targets', new Document([
|
||||||
'$permissions' => [
|
'$permissions' => [
|
||||||
Permission::read(Role::any()),
|
Permission::read(Role::user($user->getId())),
|
||||||
Permission::update(Role::user($user->getId())),
|
Permission::update(Role::user($user->getId())),
|
||||||
Permission::delete(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));
|
Authorization::skip(fn () => $dbForProject->createDocument('users', $user));
|
||||||
try {
|
try {
|
||||||
$target = Authorization::skip(fn() => $dbForProject->createDocument('targets', new Document([
|
$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(),
|
'userId' => $user->getId(),
|
||||||
'userInternalId' => $user->getInternalId(),
|
'userInternalId' => $user->getInternalId(),
|
||||||
'providerType' => MESSAGE_TYPE_SMS,
|
'providerType' => MESSAGE_TYPE_SMS,
|
||||||
|
|
|
@ -3612,7 +3612,7 @@ App::get('/v1/databases/usage')
|
||||||
|
|
||||||
Authorization::skip(function () use ($dbForProject, $days, $metrics, &$stats) {
|
Authorization::skip(function () use ($dbForProject, $days, $metrics, &$stats) {
|
||||||
foreach ($metrics as $metric) {
|
foreach ($metrics as $metric) {
|
||||||
$result = $dbForProject->findOne('stats_v2', [
|
$result = $dbForProject->findOne('stats', [
|
||||||
Query::equal('metric', [$metric]),
|
Query::equal('metric', [$metric]),
|
||||||
Query::equal('period', ['inf'])
|
Query::equal('period', ['inf'])
|
||||||
]);
|
]);
|
||||||
|
@ -3620,7 +3620,7 @@ App::get('/v1/databases/usage')
|
||||||
$stats[$metric]['total'] = $result['value'] ?? 0;
|
$stats[$metric]['total'] = $result['value'] ?? 0;
|
||||||
$limit = $days['limit'];
|
$limit = $days['limit'];
|
||||||
$period = $days['period'];
|
$period = $days['period'];
|
||||||
$results = $dbForProject->find('stats_v2', [
|
$results = $dbForProject->find('stats', [
|
||||||
Query::equal('metric', [$metric]),
|
Query::equal('metric', [$metric]),
|
||||||
Query::equal('period', [$period]),
|
Query::equal('period', [$period]),
|
||||||
Query::limit($limit),
|
Query::limit($limit),
|
||||||
|
@ -3696,7 +3696,7 @@ App::get('/v1/databases/:databaseId/usage')
|
||||||
|
|
||||||
Authorization::skip(function () use ($dbForProject, $days, $metrics, &$stats) {
|
Authorization::skip(function () use ($dbForProject, $days, $metrics, &$stats) {
|
||||||
foreach ($metrics as $metric) {
|
foreach ($metrics as $metric) {
|
||||||
$result = $dbForProject->findOne('stats_v2', [
|
$result = $dbForProject->findOne('stats', [
|
||||||
Query::equal('metric', [$metric]),
|
Query::equal('metric', [$metric]),
|
||||||
Query::equal('period', ['inf'])
|
Query::equal('period', ['inf'])
|
||||||
]);
|
]);
|
||||||
|
@ -3704,7 +3704,7 @@ App::get('/v1/databases/:databaseId/usage')
|
||||||
$stats[$metric]['total'] = $result['value'] ?? 0;
|
$stats[$metric]['total'] = $result['value'] ?? 0;
|
||||||
$limit = $days['limit'];
|
$limit = $days['limit'];
|
||||||
$period = $days['period'];
|
$period = $days['period'];
|
||||||
$results = $dbForProject->find('stats_v2', [
|
$results = $dbForProject->find('stats', [
|
||||||
Query::equal('metric', [$metric]),
|
Query::equal('metric', [$metric]),
|
||||||
Query::equal('period', [$period]),
|
Query::equal('period', [$period]),
|
||||||
Query::limit($limit),
|
Query::limit($limit),
|
||||||
|
@ -3782,7 +3782,7 @@ App::get('/v1/databases/:databaseId/collections/:collectionId/usage')
|
||||||
|
|
||||||
Authorization::skip(function () use ($dbForProject, $days, $metrics, &$stats) {
|
Authorization::skip(function () use ($dbForProject, $days, $metrics, &$stats) {
|
||||||
foreach ($metrics as $metric) {
|
foreach ($metrics as $metric) {
|
||||||
$result = $dbForProject->findOne('stats_v2', [
|
$result = $dbForProject->findOne('stats', [
|
||||||
Query::equal('metric', [$metric]),
|
Query::equal('metric', [$metric]),
|
||||||
Query::equal('period', ['inf'])
|
Query::equal('period', ['inf'])
|
||||||
]);
|
]);
|
||||||
|
@ -3790,7 +3790,7 @@ App::get('/v1/databases/:databaseId/collections/:collectionId/usage')
|
||||||
$stats[$metric]['total'] = $result['value'] ?? 0;
|
$stats[$metric]['total'] = $result['value'] ?? 0;
|
||||||
$limit = $days['limit'];
|
$limit = $days['limit'];
|
||||||
$period = $days['period'];
|
$period = $days['period'];
|
||||||
$results = $dbForProject->find('stats_v2', [
|
$results = $dbForProject->find('stats', [
|
||||||
Query::equal('metric', [$metric]),
|
Query::equal('metric', [$metric]),
|
||||||
Query::equal('period', [$period]),
|
Query::equal('period', [$period]),
|
||||||
Query::limit($limit),
|
Query::limit($limit),
|
||||||
|
|
|
@ -492,7 +492,7 @@ App::get('/v1/functions/:functionId/usage')
|
||||||
|
|
||||||
Authorization::skip(function () use ($dbForProject, $days, $metrics, &$stats) {
|
Authorization::skip(function () use ($dbForProject, $days, $metrics, &$stats) {
|
||||||
foreach ($metrics as $metric) {
|
foreach ($metrics as $metric) {
|
||||||
$result = $dbForProject->findOne('stats_v2', [
|
$result = $dbForProject->findOne('stats', [
|
||||||
Query::equal('metric', [$metric]),
|
Query::equal('metric', [$metric]),
|
||||||
Query::equal('period', ['inf'])
|
Query::equal('period', ['inf'])
|
||||||
]);
|
]);
|
||||||
|
@ -500,7 +500,7 @@ App::get('/v1/functions/:functionId/usage')
|
||||||
$stats[$metric]['total'] = $result['value'] ?? 0;
|
$stats[$metric]['total'] = $result['value'] ?? 0;
|
||||||
$limit = $days['limit'];
|
$limit = $days['limit'];
|
||||||
$period = $days['period'];
|
$period = $days['period'];
|
||||||
$results = $dbForProject->find('stats_v2', [
|
$results = $dbForProject->find('stats', [
|
||||||
Query::equal('metric', [$metric]),
|
Query::equal('metric', [$metric]),
|
||||||
Query::equal('period', [$period]),
|
Query::equal('period', [$period]),
|
||||||
Query::limit($limit),
|
Query::limit($limit),
|
||||||
|
@ -584,7 +584,7 @@ App::get('/v1/functions/usage')
|
||||||
|
|
||||||
Authorization::skip(function () use ($dbForProject, $days, $metrics, &$stats) {
|
Authorization::skip(function () use ($dbForProject, $days, $metrics, &$stats) {
|
||||||
foreach ($metrics as $metric) {
|
foreach ($metrics as $metric) {
|
||||||
$result = $dbForProject->findOne('stats_v2', [
|
$result = $dbForProject->findOne('stats', [
|
||||||
Query::equal('metric', [$metric]),
|
Query::equal('metric', [$metric]),
|
||||||
Query::equal('period', ['inf'])
|
Query::equal('period', ['inf'])
|
||||||
]);
|
]);
|
||||||
|
@ -592,7 +592,7 @@ App::get('/v1/functions/usage')
|
||||||
$stats[$metric]['total'] = $result['value'] ?? 0;
|
$stats[$metric]['total'] = $result['value'] ?? 0;
|
||||||
$limit = $days['limit'];
|
$limit = $days['limit'];
|
||||||
$period = $days['period'];
|
$period = $days['period'];
|
||||||
$results = $dbForProject->find('stats_v2', [
|
$results = $dbForProject->find('stats', [
|
||||||
Query::equal('metric', [$metric]),
|
Query::equal('metric', [$metric]),
|
||||||
Query::equal('period', [$period]),
|
Query::equal('period', [$period]),
|
||||||
Query::limit($limit),
|
Query::limit($limit),
|
||||||
|
|
|
@ -2260,7 +2260,19 @@ App::post('/v1/messaging/topics/:topicId/subscribers')
|
||||||
|
|
||||||
try {
|
try {
|
||||||
$subscriber = $dbForProject->createDocument('subscribers', $subscriber);
|
$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) {
|
} catch (DuplicateException) {
|
||||||
throw new Exception(Exception::SUBSCRIBER_ALREADY_EXISTS);
|
throw new Exception(Exception::SUBSCRIBER_ALREADY_EXISTS);
|
||||||
}
|
}
|
||||||
|
@ -2311,7 +2323,7 @@ App::get('/v1/messaging/topics/:topicId/subscribers')
|
||||||
throw new Exception(Exception::TOPIC_NOT_FOUND);
|
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
|
* Get cursor document if there was a cursor query, we use array_filter and reset for reference $cursor to $queries
|
||||||
|
@ -2512,8 +2524,23 @@ App::delete('/v1/messaging/topics/:topicId/subscribers/:subscriberId')
|
||||||
throw new Exception(Exception::SUBSCRIBER_NOT_FOUND);
|
throw new Exception(Exception::SUBSCRIBER_NOT_FOUND);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
$target = $dbForProject->getDocument('targets', $subscriber->getAttribute('targetId'));
|
||||||
|
|
||||||
$dbForProject->deleteDocument('subscribers', $subscriberId);
|
$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
|
$queueForEvents
|
||||||
->setParam('topicId', $topic->getId())
|
->setParam('topicId', $topic->getId())
|
||||||
|
|
|
@ -73,7 +73,7 @@ App::get('/v1/project/usage')
|
||||||
|
|
||||||
Authorization::skip(function () use ($dbForProject, $firstDay, $lastDay, $period, $metrics, &$total, &$stats) {
|
Authorization::skip(function () use ($dbForProject, $firstDay, $lastDay, $period, $metrics, &$total, &$stats) {
|
||||||
foreach ($metrics['total'] as $metric) {
|
foreach ($metrics['total'] as $metric) {
|
||||||
$result = $dbForProject->findOne('stats_v2', [
|
$result = $dbForProject->findOne('stats', [
|
||||||
Query::equal('metric', [$metric]),
|
Query::equal('metric', [$metric]),
|
||||||
Query::equal('period', ['inf'])
|
Query::equal('period', ['inf'])
|
||||||
]);
|
]);
|
||||||
|
@ -81,7 +81,7 @@ App::get('/v1/project/usage')
|
||||||
}
|
}
|
||||||
|
|
||||||
foreach ($metrics['period'] as $metric) {
|
foreach ($metrics['period'] as $metric) {
|
||||||
$results = $dbForProject->find('stats_v2', [
|
$results = $dbForProject->find('stats', [
|
||||||
Query::equal('metric', [$metric]),
|
Query::equal('metric', [$metric]),
|
||||||
Query::equal('period', [$period]),
|
Query::equal('period', [$period]),
|
||||||
Query::greaterThanEqual('time', $firstDay),
|
Query::greaterThanEqual('time', $firstDay),
|
||||||
|
@ -116,7 +116,7 @@ App::get('/v1/project/usage')
|
||||||
$id = $function->getId();
|
$id = $function->getId();
|
||||||
$name = $function->getAttribute('name');
|
$name = $function->getAttribute('name');
|
||||||
$metric = str_replace('{functionInternalId}', $function->getInternalId(), METRIC_FUNCTION_ID_EXECUTIONS);
|
$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('metric', [$metric]),
|
||||||
Query::equal('period', ['inf'])
|
Query::equal('period', ['inf'])
|
||||||
]);
|
]);
|
||||||
|
@ -132,7 +132,7 @@ App::get('/v1/project/usage')
|
||||||
$id = $bucket->getId();
|
$id = $bucket->getId();
|
||||||
$name = $bucket->getAttribute('name');
|
$name = $bucket->getAttribute('name');
|
||||||
$metric = str_replace('{bucketInternalId}', $bucket->getInternalId(), METRIC_BUCKET_ID_FILES_STORAGE);
|
$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('metric', [$metric]),
|
||||||
Query::equal('period', ['inf'])
|
Query::equal('period', ['inf'])
|
||||||
]);
|
]);
|
||||||
|
|
|
@ -1539,7 +1539,7 @@ App::get('/v1/storage/usage')
|
||||||
$total = [];
|
$total = [];
|
||||||
Authorization::skip(function () use ($dbForProject, $days, $metrics, &$stats, &$total) {
|
Authorization::skip(function () use ($dbForProject, $days, $metrics, &$stats, &$total) {
|
||||||
foreach ($metrics as $metric) {
|
foreach ($metrics as $metric) {
|
||||||
$result = $dbForProject->findOne('stats_v2', [
|
$result = $dbForProject->findOne('stats', [
|
||||||
Query::equal('metric', [$metric]),
|
Query::equal('metric', [$metric]),
|
||||||
Query::equal('period', ['inf'])
|
Query::equal('period', ['inf'])
|
||||||
]);
|
]);
|
||||||
|
@ -1547,7 +1547,7 @@ App::get('/v1/storage/usage')
|
||||||
$stats[$metric]['total'] = $result['value'] ?? 0;
|
$stats[$metric]['total'] = $result['value'] ?? 0;
|
||||||
$limit = $days['limit'];
|
$limit = $days['limit'];
|
||||||
$period = $days['period'];
|
$period = $days['period'];
|
||||||
$results = $dbForProject->find('stats_v2', [
|
$results = $dbForProject->find('stats', [
|
||||||
Query::equal('metric', [$metric]),
|
Query::equal('metric', [$metric]),
|
||||||
Query::equal('period', [$period]),
|
Query::equal('period', [$period]),
|
||||||
Query::limit($limit),
|
Query::limit($limit),
|
||||||
|
@ -1624,7 +1624,7 @@ App::get('/v1/storage/:bucketId/usage')
|
||||||
|
|
||||||
Authorization::skip(function () use ($dbForProject, $days, $metrics, &$stats, &$total) {
|
Authorization::skip(function () use ($dbForProject, $days, $metrics, &$stats, &$total) {
|
||||||
foreach ($metrics as $metric) {
|
foreach ($metrics as $metric) {
|
||||||
$result = $dbForProject->findOne('stats_v2', [
|
$result = $dbForProject->findOne('stats', [
|
||||||
Query::equal('metric', [$metric]),
|
Query::equal('metric', [$metric]),
|
||||||
Query::equal('period', ['inf'])
|
Query::equal('period', ['inf'])
|
||||||
]);
|
]);
|
||||||
|
@ -1632,7 +1632,7 @@ App::get('/v1/storage/:bucketId/usage')
|
||||||
$stats[$metric]['total'] = $result['value'] ?? 0;
|
$stats[$metric]['total'] = $result['value'] ?? 0;
|
||||||
$limit = $days['limit'];
|
$limit = $days['limit'];
|
||||||
$period = $days['period'];
|
$period = $days['period'];
|
||||||
$results = $dbForProject->find('stats_v2', [
|
$results = $dbForProject->find('stats', [
|
||||||
Query::equal('metric', [$metric]),
|
Query::equal('metric', [$metric]),
|
||||||
Query::equal('period', [$period]),
|
Query::equal('period', [$period]),
|
||||||
Query::limit($limit),
|
Query::limit($limit),
|
||||||
|
|
|
@ -115,6 +115,11 @@ function createUser(string $hash, mixed $hashOptions, string $userId, ?string $e
|
||||||
if ($email) {
|
if ($email) {
|
||||||
try {
|
try {
|
||||||
$target = $dbForProject->createDocument('targets', new Document([
|
$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(),
|
'userId' => $user->getId(),
|
||||||
'userInternalId' => $user->getInternalId(),
|
'userInternalId' => $user->getInternalId(),
|
||||||
'providerType' => 'email',
|
'providerType' => 'email',
|
||||||
|
@ -132,6 +137,11 @@ function createUser(string $hash, mixed $hashOptions, string $userId, ?string $e
|
||||||
if ($phone) {
|
if ($phone) {
|
||||||
try {
|
try {
|
||||||
$target = $dbForProject->createDocument('targets', new Document([
|
$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(),
|
'userId' => $user->getId(),
|
||||||
'userInternalId' => $user->getInternalId(),
|
'userInternalId' => $user->getInternalId(),
|
||||||
'providerType' => 'sms',
|
'providerType' => 'sms',
|
||||||
|
@ -498,6 +508,11 @@ App::post('/v1/users/:userId/targets')
|
||||||
try {
|
try {
|
||||||
$target = $dbForProject->createDocument('targets', new Document([
|
$target = $dbForProject->createDocument('targets', new Document([
|
||||||
'$id' => $targetId,
|
'$id' => $targetId,
|
||||||
|
'$permissions' => [
|
||||||
|
Permission::read(Role::user($user->getId())),
|
||||||
|
Permission::update(Role::user($user->getId())),
|
||||||
|
Permission::delete(Role::user($user->getId())),
|
||||||
|
],
|
||||||
'providerId' => $providerId ?? null,
|
'providerId' => $providerId ?? null,
|
||||||
'providerInternalId' => $provider->getInternalId() ?? null,
|
'providerInternalId' => $provider->getInternalId() ?? null,
|
||||||
'providerType' => $providerType,
|
'providerType' => $providerType,
|
||||||
|
@ -1227,6 +1242,11 @@ App::patch('/v1/users/:userId/email')
|
||||||
} else {
|
} else {
|
||||||
if (\strlen($email) !== 0) {
|
if (\strlen($email) !== 0) {
|
||||||
$target = $dbForProject->createDocument('targets', new Document([
|
$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(),
|
'userId' => $user->getId(),
|
||||||
'userInternalId' => $user->getInternalId(),
|
'userInternalId' => $user->getInternalId(),
|
||||||
'providerType' => 'email',
|
'providerType' => 'email',
|
||||||
|
@ -1305,6 +1325,11 @@ App::patch('/v1/users/:userId/phone')
|
||||||
} else {
|
} else {
|
||||||
if (\strlen($number) !== 0) {
|
if (\strlen($number) !== 0) {
|
||||||
$target = $dbForProject->createDocument('targets', new Document([
|
$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(),
|
'userId' => $user->getId(),
|
||||||
'userInternalId' => $user->getInternalId(),
|
'userInternalId' => $user->getInternalId(),
|
||||||
'providerType' => 'sms',
|
'providerType' => 'sms',
|
||||||
|
@ -1976,7 +2001,7 @@ App::get('/v1/users/usage')
|
||||||
|
|
||||||
Authorization::skip(function () use ($dbForProject, $days, $metrics, &$stats) {
|
Authorization::skip(function () use ($dbForProject, $days, $metrics, &$stats) {
|
||||||
foreach ($metrics as $count => $metric) {
|
foreach ($metrics as $count => $metric) {
|
||||||
$result = $dbForProject->findOne('stats_v2', [
|
$result = $dbForProject->findOne('stats', [
|
||||||
Query::equal('metric', [$metric]),
|
Query::equal('metric', [$metric]),
|
||||||
Query::equal('period', ['inf'])
|
Query::equal('period', ['inf'])
|
||||||
]);
|
]);
|
||||||
|
@ -1984,7 +2009,7 @@ App::get('/v1/users/usage')
|
||||||
$stats[$metric]['total'] = $result['value'] ?? 0;
|
$stats[$metric]['total'] = $result['value'] ?? 0;
|
||||||
$limit = $days['limit'];
|
$limit = $days['limit'];
|
||||||
$period = $days['period'];
|
$period = $days['period'];
|
||||||
$results = $dbForProject->find('stats_v2', [
|
$results = $dbForProject->find('stats', [
|
||||||
Query::equal('metric', [$metric]),
|
Query::equal('metric', [$metric]),
|
||||||
Query::equal('period', [$period]),
|
Query::equal('period', [$period]),
|
||||||
Query::limit($limit),
|
Query::limit($limit),
|
||||||
|
|
|
@ -3,7 +3,6 @@
|
||||||
require_once __DIR__ . '/../init.php';
|
require_once __DIR__ . '/../init.php';
|
||||||
|
|
||||||
use Utopia\App;
|
use Utopia\App;
|
||||||
use Utopia\Database\Helpers\Role;
|
|
||||||
use Utopia\Locale\Locale;
|
use Utopia\Locale\Locale;
|
||||||
use Utopia\Logger\Logger;
|
use Utopia\Logger\Logger;
|
||||||
use Utopia\Logger\Log;
|
use Utopia\Logger\Log;
|
||||||
|
@ -15,7 +14,6 @@ use Appwrite\Utopia\View;
|
||||||
use Appwrite\Extend\Exception as AppwriteException;
|
use Appwrite\Extend\Exception as AppwriteException;
|
||||||
use Utopia\Config\Config;
|
use Utopia\Config\Config;
|
||||||
use Utopia\Domains\Domain;
|
use Utopia\Domains\Domain;
|
||||||
use Appwrite\Auth\Auth;
|
|
||||||
use Appwrite\Event\Certificate;
|
use Appwrite\Event\Certificate;
|
||||||
use Appwrite\Network\Validator\Origin;
|
use Appwrite\Network\Validator\Origin;
|
||||||
use Appwrite\Utopia\Response\Filters\V11 as ResponseV11;
|
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\V14 as ResponseV14;
|
||||||
use Appwrite\Utopia\Response\Filters\V15 as ResponseV15;
|
use Appwrite\Utopia\Response\Filters\V15 as ResponseV15;
|
||||||
use Appwrite\Utopia\Response\Filters\V16 as ResponseV16;
|
use Appwrite\Utopia\Response\Filters\V16 as ResponseV16;
|
||||||
|
use Appwrite\Utopia\Response\Filters\V17 as ResponseV17;
|
||||||
use Utopia\CLI\Console;
|
use Utopia\CLI\Console;
|
||||||
use Utopia\Database\Database;
|
use Utopia\Database\Database;
|
||||||
use Utopia\Database\DateTime;
|
|
||||||
use Utopia\Database\Document;
|
use Utopia\Database\Document;
|
||||||
use Utopia\Database\Query;
|
use Utopia\Database\Query;
|
||||||
use Utopia\Database\Validator\Authorization;
|
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\V14 as RequestV14;
|
||||||
use Appwrite\Utopia\Request\Filters\V15 as RequestV15;
|
use Appwrite\Utopia\Request\Filters\V15 as RequestV15;
|
||||||
use Appwrite\Utopia\Request\Filters\V16 as RequestV16;
|
use Appwrite\Utopia\Request\Filters\V16 as RequestV16;
|
||||||
|
use Appwrite\Utopia\Request\Filters\V17 as RequestV17;
|
||||||
use Utopia\Validator\Text;
|
use Utopia\Validator\Text;
|
||||||
use Utopia\Validator\WhiteList;
|
|
||||||
|
|
||||||
Config::setParam('domainVerification', false);
|
Config::setParam('domainVerification', false);
|
||||||
Config::setParam('cookieDomain', 'localhost');
|
Config::setParam('cookieDomain', 'localhost');
|
||||||
|
@ -203,15 +201,11 @@ App::init()
|
||||||
->inject('console')
|
->inject('console')
|
||||||
->inject('project')
|
->inject('project')
|
||||||
->inject('dbForConsole')
|
->inject('dbForConsole')
|
||||||
->inject('user')
|
|
||||||
->inject('locale')
|
->inject('locale')
|
||||||
->inject('localeCodes')
|
->inject('localeCodes')
|
||||||
->inject('clients')
|
->inject('clients')
|
||||||
->inject('servers')
|
|
||||||
->inject('session')
|
|
||||||
->inject('mode')
|
|
||||||
->inject('queueForCertificates')
|
->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
|
* Appwrite Router
|
||||||
*/
|
*/
|
||||||
|
@ -252,6 +246,9 @@ App::init()
|
||||||
case version_compare($requestFormat, '1.4.0', '<'):
|
case version_compare($requestFormat, '1.4.0', '<'):
|
||||||
Request::setFilter(new RequestV16());
|
Request::setFilter(new RequestV16());
|
||||||
break;
|
break;
|
||||||
|
case version_compare($requestFormat, '1.5.0', '<'):
|
||||||
|
Request::setFilter(new RequestV17());
|
||||||
|
break;
|
||||||
default:
|
default:
|
||||||
Request::setFilter(null);
|
Request::setFilter(null);
|
||||||
}
|
}
|
||||||
|
@ -319,14 +316,6 @@ App::init()
|
||||||
$locale->setDefault($localeParam);
|
$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();
|
$referrer = $request->getReferer();
|
||||||
$origin = \parse_url($request->getOrigin($referrer), PHP_URL_HOST);
|
$origin = \parse_url($request->getOrigin($referrer), PHP_URL_HOST);
|
||||||
$protocol = \parse_url($request->getOrigin($referrer), PHP_URL_SCHEME);
|
$protocol = \parse_url($request->getOrigin($referrer), PHP_URL_SCHEME);
|
||||||
|
@ -393,6 +382,9 @@ App::init()
|
||||||
case version_compare($responseFormat, '1.4.0', '<'):
|
case version_compare($responseFormat, '1.4.0', '<'):
|
||||||
Response::setFilter(new ResponseV16());
|
Response::setFilter(new ResponseV16());
|
||||||
break;
|
break;
|
||||||
|
case version_compare($responseFormat, '1.5.0', '<'):
|
||||||
|
Response::setFilter(new ResponseV17());
|
||||||
|
break;
|
||||||
default:
|
default:
|
||||||
Response::setFilter(null);
|
Response::setFilter(null);
|
||||||
}
|
}
|
||||||
|
@ -445,138 +437,6 @@ App::init()
|
||||||
) {
|
) {
|
||||||
throw new AppwriteException(AppwriteException::GENERAL_UNKNOWN_ORIGIN, $originValidator->getDescription());
|
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()
|
App::options()
|
||||||
|
|
|
@ -6,7 +6,6 @@ use Appwrite\Event\Database as EventDatabase;
|
||||||
use Appwrite\Event\Delete;
|
use Appwrite\Event\Delete;
|
||||||
use Appwrite\Event\Event;
|
use Appwrite\Event\Event;
|
||||||
use Appwrite\Event\Func;
|
use Appwrite\Event\Func;
|
||||||
use Appwrite\Event\Mail;
|
|
||||||
use Appwrite\Event\Messaging;
|
use Appwrite\Event\Messaging;
|
||||||
use Appwrite\Extend\Exception;
|
use Appwrite\Extend\Exception;
|
||||||
use Appwrite\Event\Usage;
|
use Appwrite\Event\Usage;
|
||||||
|
@ -22,7 +21,9 @@ use Utopia\Database\Database;
|
||||||
use Utopia\Database\DateTime;
|
use Utopia\Database\DateTime;
|
||||||
use Utopia\Database\Document;
|
use Utopia\Database\Document;
|
||||||
use Utopia\Database\Validator\Authorization;
|
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) {
|
$parseLabel = function (string $label, array $responsePayload, array $requestParams, Document $user) {
|
||||||
preg_match_all('/{(.*?)}/', $label, $matches);
|
preg_match_all('/{(.*?)}/', $label, $matches);
|
||||||
|
@ -135,7 +136,7 @@ $databaseListener = function (string $event, Document $document, Document $proje
|
||||||
$queueForUsage
|
$queueForUsage
|
||||||
->addMetric(METRIC_DEPLOYMENTS, $value) // per project
|
->addMetric(METRIC_DEPLOYMENTS, $value) // per project
|
||||||
->addMetric(METRIC_DEPLOYMENTS_STORAGE, $document->getAttribute('size') * $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);
|
->addMetric(str_replace(['{resourceType}', '{resourceInternalId}'], [$document->getAttribute('resourceType'), $document->getAttribute('resourceInternalId')], METRIC_FUNCTION_ID_DEPLOYMENTS_STORAGE), $document->getAttribute('size') * $value);
|
||||||
break;
|
break;
|
||||||
default:
|
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()
|
App::init()
|
||||||
->groups(['api'])
|
->groups(['api'])
|
||||||
->inject('utopia')
|
->inject('utopia')
|
||||||
|
@ -162,10 +312,6 @@ App::init()
|
||||||
|
|
||||||
$route = $utopia->getRoute();
|
$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
|
* Abuse Check
|
||||||
*/
|
*/
|
||||||
|
@ -296,7 +442,7 @@ App::init()
|
||||||
if ($fileSecurity && !$valid) {
|
if ($fileSecurity && !$valid) {
|
||||||
$file = $dbForProject->getDocument('bucket_' . $bucket->getInternalId(), $fileId);
|
$file = $dbForProject->getDocument('bucket_' . $bucket->getInternalId(), $fileId);
|
||||||
} else {
|
} else {
|
||||||
$file = Authorization::skip(fn() => $dbForProject->getDocument('bucket_' . $bucket->getInternalId(), $fileId));
|
$file = Authorization::skip(fn () => $dbForProject->getDocument('bucket_' . $bucket->getInternalId(), $fileId));
|
||||||
}
|
}
|
||||||
|
|
||||||
if ($file->isEmpty()) {
|
if ($file->isEmpty()) {
|
||||||
|
@ -497,7 +643,7 @@ App::shutdown()
|
||||||
'resource' => $resource,
|
'resource' => $resource,
|
||||||
'contentType' => $response->getContentType(),
|
'contentType' => $response->getContentType(),
|
||||||
'payload' => base64_encode($data['payload']),
|
'payload' => base64_encode($data['payload']),
|
||||||
]) ;
|
]);
|
||||||
|
|
||||||
$signature = md5($data);
|
$signature = md5($data);
|
||||||
$cacheLog = Authorization::skip(fn () => $dbForProject->getDocument('cache', $key));
|
$cacheLog = Authorization::skip(fn () => $dbForProject->getDocument('cache', $key));
|
||||||
|
|
|
@ -192,7 +192,6 @@ class Exception extends \Exception
|
||||||
|
|
||||||
/** Projects */
|
/** Projects */
|
||||||
public const PROJECT_NOT_FOUND = 'project_not_found';
|
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_DISABLED = 'project_provider_disabled';
|
||||||
public const PROJECT_PROVIDER_UNSUPPORTED = 'project_provider_unsupported';
|
public const PROJECT_PROVIDER_UNSUPPORTED = 'project_provider_unsupported';
|
||||||
public const PROJECT_ALREADY_EXISTS = 'project_already_exists';
|
public const PROJECT_ALREADY_EXISTS = 'project_already_exists';
|
||||||
|
@ -252,6 +251,7 @@ class Exception extends \Exception
|
||||||
public const PROVIDER_NOT_FOUND = 'provider_not_found';
|
public const PROVIDER_NOT_FOUND = 'provider_not_found';
|
||||||
public const PROVIDER_ALREADY_EXISTS = 'provider_already_exists';
|
public const PROVIDER_ALREADY_EXISTS = 'provider_already_exists';
|
||||||
public const PROVIDER_INCORRECT_TYPE = 'provider_incorrect_type';
|
public const PROVIDER_INCORRECT_TYPE = 'provider_incorrect_type';
|
||||||
|
|
||||||
public const PROVIDER_MISSING_CREDENTIALS = 'provider_missing_credentials';
|
public const PROVIDER_MISSING_CREDENTIALS = 'provider_missing_credentials';
|
||||||
|
|
||||||
/** Topic */
|
/** Topic */
|
||||||
|
@ -274,6 +274,9 @@ class Exception extends \Exception
|
||||||
public const MESSAGE_TARGET_NOT_PUSH = 'message_target_not_push';
|
public const MESSAGE_TARGET_NOT_PUSH = 'message_target_not_push';
|
||||||
public const MESSAGE_MISSING_SCHEDULE = 'message_missing_schedule';
|
public const MESSAGE_MISSING_SCHEDULE = 'message_missing_schedule';
|
||||||
|
|
||||||
|
/** Targets */
|
||||||
|
public const TARGET_PROVIDER_INVALID_TYPE = 'target_provider_invalid_type';
|
||||||
|
|
||||||
/** Schedules */
|
/** Schedules */
|
||||||
public const SCHEDULE_NOT_FOUND = 'schedule_not_found';
|
public const SCHEDULE_NOT_FOUND = 'schedule_not_found';
|
||||||
|
|
||||||
|
|
|
@ -78,7 +78,7 @@ abstract class Migration
|
||||||
'1.4.11' => 'V19',
|
'1.4.11' => 'V19',
|
||||||
'1.4.12' => 'V19',
|
'1.4.12' => 'V19',
|
||||||
'1.4.13' => 'V19',
|
'1.4.13' => 'V19',
|
||||||
'1.4.14' => 'V20'
|
'1.5.0' => 'V20',
|
||||||
];
|
];
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
|
@ -2,16 +2,19 @@
|
||||||
|
|
||||||
namespace Appwrite\Migration\Version;
|
namespace Appwrite\Migration\Version;
|
||||||
|
|
||||||
|
use Appwrite\Auth\Auth;
|
||||||
use Appwrite\Migration\Migration;
|
use Appwrite\Migration\Migration;
|
||||||
|
use Exception;
|
||||||
use PDOException;
|
use PDOException;
|
||||||
use Throwable;
|
use Throwable;
|
||||||
use Utopia\CLI\Console;
|
use Utopia\CLI\Console;
|
||||||
use Utopia\Database\Database;
|
use Utopia\Database\Database;
|
||||||
|
use Utopia\Database\DateTime;
|
||||||
use Utopia\Database\Document;
|
use Utopia\Database\Document;
|
||||||
use Utopia\Database\Exception;
|
|
||||||
use Utopia\Database\Exception\Authorization;
|
use Utopia\Database\Exception\Authorization;
|
||||||
use Utopia\Database\Exception\Duplicate;
|
use Utopia\Database\Exception\Duplicate;
|
||||||
use Utopia\Database\Exception\Structure;
|
use Utopia\Database\Exception\Structure;
|
||||||
|
use Utopia\Database\Helpers\ID;
|
||||||
use Utopia\Database\Query;
|
use Utopia\Database\Query;
|
||||||
|
|
||||||
class V20 extends Migration
|
class V20 extends Migration
|
||||||
|
@ -21,18 +24,14 @@ class V20 extends Migration
|
||||||
*/
|
*/
|
||||||
public function execute(): void
|
public function execute(): void
|
||||||
{
|
{
|
||||||
if ($this->project->getInternalId() == 'console') {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Disable SubQueries for Performance.
|
* 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(
|
Database::addFilter(
|
||||||
$name,
|
$name,
|
||||||
fn () => null,
|
fn() => null,
|
||||||
fn () => []
|
fn() => []
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -45,19 +44,239 @@ class V20 extends Migration
|
||||||
Console::log('Migrating Project: ' . $this->project->getAttribute('name') . ' (' . $this->project->getId() . ')');
|
Console::log('Migrating Project: ' . $this->project->getAttribute('name') . ' (' . $this->project->getId() . ')');
|
||||||
$this->projectDB->setNamespace("_{$this->project->getInternalId()}");
|
$this->projectDB->setNamespace("_{$this->project->getInternalId()}");
|
||||||
|
|
||||||
|
Console::info('Migrating Collections');
|
||||||
|
$this->migrateCollections();
|
||||||
|
|
||||||
Console::info('Migrating Functions');
|
Console::info('Migrating Functions');
|
||||||
$this->migrateFunctions();
|
$this->migrateFunctions();
|
||||||
|
|
||||||
Console::info('Migrating Databases');
|
Console::info('Migrating Databases');
|
||||||
$this->migrateDatabases();
|
$this->migrateDatabases();
|
||||||
|
|
||||||
Console::info('Migrating Collections');
|
|
||||||
$this->migrateCollections();
|
|
||||||
|
|
||||||
Console::info('Migrating Buckets');
|
Console::info('Migrating Buckets');
|
||||||
$this->migrateBuckets();
|
$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
|
* @return void
|
||||||
* @throws Authorization
|
* @throws Authorization
|
||||||
|
@ -132,6 +351,7 @@ class V20 extends Migration
|
||||||
*/
|
*/
|
||||||
protected function migrateUsageMetrics(string $from, string $to): void
|
protected function migrateUsageMetrics(string $from, string $to): void
|
||||||
{
|
{
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* inf metric
|
* inf metric
|
||||||
*/
|
*/
|
||||||
|
@ -182,11 +402,12 @@ class V20 extends Migration
|
||||||
Console::warning("Error while updating metric {$from} " . $th->getMessage());
|
Console::warning("Error while updating metric {$from} " . $th->getMessage());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Migrate functions.
|
* Migrate functions.
|
||||||
*
|
*
|
||||||
* @return void
|
* @return void
|
||||||
* @throws \Exception
|
* @throws Exception
|
||||||
*/
|
*/
|
||||||
private function migrateFunctions(): void
|
private function migrateFunctions(): void
|
||||||
{
|
{
|
||||||
|
@ -215,7 +436,7 @@ class V20 extends Migration
|
||||||
* Migrate Databases.
|
* Migrate Databases.
|
||||||
*
|
*
|
||||||
* @return void
|
* @return void
|
||||||
* @throws \Exception
|
* @throws Exception
|
||||||
*/
|
*/
|
||||||
private function migrateDatabases(): void
|
private function migrateDatabases(): void
|
||||||
{
|
{
|
||||||
|
@ -241,7 +462,7 @@ class V20 extends Migration
|
||||||
Console::log("Migrating Collections of {$collectionTable} {$collection->getId()} ({$collection->getAttribute('name')})");
|
Console::log("Migrating Collections of {$collectionTable} {$collection->getId()} ({$collection->getAttribute('name')})");
|
||||||
|
|
||||||
// Collection level
|
// Collection level
|
||||||
$collectionId = $collection->getId() ;
|
$collectionId = $collection->getId();
|
||||||
$collectionInternalId = $collection->getInternalId();
|
$collectionInternalId = $collection->getInternalId();
|
||||||
|
|
||||||
$this->migrateUsageMetrics("documents.$databaseId/$collectionId.count.total", "$databaseInternalId.$collectionInternalId.documents");
|
$this->migrateUsageMetrics("documents.$databaseId/$collectionId.count.total", "$databaseInternalId.$collectionInternalId.documents");
|
||||||
|
@ -250,53 +471,10 @@ class V20 extends Migration
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Migrate Collections.
|
* Migrating Buckets.
|
||||||
*
|
*
|
||||||
* @return void
|
* @return void
|
||||||
* @throws \Exception
|
* @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 PDOException
|
* @throws PDOException
|
||||||
*/
|
*/
|
||||||
protected function migrateBuckets(): void
|
protected function migrateBuckets(): void
|
||||||
|
@ -305,7 +483,6 @@ class V20 extends Migration
|
||||||
$this->migrateUsageMetrics('buckets.$all.count.total', 'buckets');
|
$this->migrateUsageMetrics('buckets.$all.count.total', 'buckets');
|
||||||
$this->migrateUsageMetrics('files.$all.count.total', 'files');
|
$this->migrateUsageMetrics('files.$all.count.total', 'files');
|
||||||
$this->migrateUsageMetrics('files.$all.storage.size', 'files.storage');
|
$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) {
|
foreach ($this->documentsIterator('buckets') as $bucket) {
|
||||||
$id = "bucket_{$bucket->getInternalId()}";
|
$id = "bucket_{$bucket->getInternalId()}";
|
||||||
|
@ -317,7 +494,53 @@ class V20 extends Migration
|
||||||
|
|
||||||
$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");
|
$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'];
|
$limit = $periods[$range]['limit'];
|
||||||
$period = $periods[$range]['period'];
|
$period = $periods[$range]['period'];
|
||||||
|
|
||||||
$requestDocs = $dbForProject->find('stats_v2', [
|
$requestDocs = $dbForProject->find('stats', [
|
||||||
Query::equal('metric', [$metric]),
|
Query::equal('metric', [$metric]),
|
||||||
Query::equal('period', [$period]),
|
Query::equal('period', [$period]),
|
||||||
Query::limit($limit),
|
Query::limit($limit),
|
||||||
|
|
|
@ -163,8 +163,8 @@ class CreateInfMetric extends Action
|
||||||
|
|
||||||
try {
|
try {
|
||||||
$id = \md5("_inf_{$metric}");
|
$id = \md5("_inf_{$metric}");
|
||||||
$dbForProject->deleteDocument('stats_v2', $id);
|
$dbForProject->deleteDocument('stats', $id);
|
||||||
$dbForProject->createDocument('stats_v2', new Document([
|
$dbForProject->createDocument('stats', new Document([
|
||||||
'$id' => $id,
|
'$id' => $id,
|
||||||
'metric' => $metric,
|
'metric' => $metric,
|
||||||
'period' => 'inf',
|
'period' => 'inf',
|
||||||
|
@ -186,7 +186,7 @@ class CreateInfMetric extends Action
|
||||||
protected function getFromMetric(database $dbForProject, string $metric): int|float
|
protected function getFromMetric(database $dbForProject, string $metric): int|float
|
||||||
{
|
{
|
||||||
|
|
||||||
return $dbForProject->sum('stats_v2', 'value', [
|
return $dbForProject->sum('stats', 'value', [
|
||||||
Query::equal('metric', [
|
Query::equal('metric', [
|
||||||
$metric,
|
$metric,
|
||||||
]),
|
]),
|
||||||
|
|
|
@ -3,6 +3,7 @@
|
||||||
namespace Appwrite\Platform\Workers;
|
namespace Appwrite\Platform\Workers;
|
||||||
|
|
||||||
use Appwrite\Auth\Auth;
|
use Appwrite\Auth\Auth;
|
||||||
|
use Appwrite\Extend\Exception;
|
||||||
use Executor\Executor;
|
use Executor\Executor;
|
||||||
use Throwable;
|
use Throwable;
|
||||||
use Utopia\Abuse\Abuse;
|
use Utopia\Abuse\Abuse;
|
||||||
|
@ -263,12 +264,23 @@ class Deletes extends Action
|
||||||
Query::equal('targetInternalId', [$target->getInternalId()])
|
Query::equal('targetInternalId', [$target->getInternalId()])
|
||||||
],
|
],
|
||||||
$dbForProject,
|
$dbForProject,
|
||||||
function (Document $subscriber) use ($dbForProject) {
|
function (Document $subscriber) use ($dbForProject, $target) {
|
||||||
$topicId = $subscriber->getAttribute('topicId');
|
$topicId = $subscriber->getAttribute('topicId');
|
||||||
$topicInternalId = $subscriber->getAttribute('topicInternalId');
|
$topicInternalId = $subscriber->getAttribute('topicInternalId');
|
||||||
$topic = $dbForProject->getDocument('topics', $topicId);
|
$topic = $dbForProject->getDocument('topics', $topicId);
|
||||||
if (!$topic->isEmpty() && $topic->getInternalId() === $topicInternalId) {
|
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);
|
$dbForProject = $getProjectDB($project);
|
||||||
// Delete Usage stats
|
// Delete Usage stats
|
||||||
$this->deleteByGroup('stats_v2', [
|
$this->deleteByGroup('stats', [
|
||||||
Query::lessThan('time', $hourlyUsageRetentionDatetime),
|
Query::lessThan('time', $hourlyUsageRetentionDatetime),
|
||||||
Query::equal('period', ['1h']),
|
Query::equal('period', ['1h']),
|
||||||
], $dbForProject);
|
], $dbForProject);
|
||||||
|
|
|
@ -286,7 +286,7 @@ class Hamster extends Action
|
||||||
$limit = $periodValue['limit'];
|
$limit = $periodValue['limit'];
|
||||||
$period = $periodValue['period'];
|
$period = $periodValue['period'];
|
||||||
|
|
||||||
$requestDocs = $dbForProject->find('stats_v2', [
|
$requestDocs = $dbForProject->find('stats', [
|
||||||
Query::equal('period', [$period]),
|
Query::equal('period', [$period]),
|
||||||
Query::equal('metric', [$metric]),
|
Query::equal('metric', [$metric]),
|
||||||
Query::limit($limit),
|
Query::limit($limit),
|
||||||
|
|
|
@ -249,7 +249,7 @@ class Messaging extends Action
|
||||||
}
|
}
|
||||||
|
|
||||||
// Deleting push targets when token has expired.
|
// Deleting push targets when token has expired.
|
||||||
if (($result['error'] ?? '') === 'Expired device token.') {
|
if (($result['error'] ?? '') === 'Expired device token') {
|
||||||
$target = $dbForProject->findOne('targets', [
|
$target = $dbForProject->findOne('targets', [
|
||||||
Query::equal('identifier', [$result['recipient']])
|
Query::equal('identifier', [$result['recipient']])
|
||||||
]);
|
]);
|
||||||
|
|
|
@ -69,6 +69,7 @@ class Usage extends Action
|
||||||
getProjectDB: $getProjectDB
|
getProjectDB: $getProjectDB
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
self::$stats[$projectId]['project'] = $project;
|
self::$stats[$projectId]['project'] = $project;
|
||||||
foreach ($payload['metrics'] ?? [] as $metric) {
|
foreach ($payload['metrics'] ?? [] as $metric) {
|
||||||
if (!isset(self::$stats[$projectId]['keys'][$metric['key']])) {
|
if (!isset(self::$stats[$projectId]['keys'][$metric['key']])) {
|
||||||
|
@ -105,8 +106,8 @@ class Usage extends Action
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
case $document->getCollection() === 'databases': // databases
|
case $document->getCollection() === 'databases': // databases
|
||||||
$collections = $dbForProject->getDocument('stats_v2', md5(self::INFINITY_PERIOD . str_replace('{databaseInternalId}', $document->getInternalId(), METRIC_DATABASE_ID_COLLECTIONS)));
|
$collections = $dbForProject->getDocument('stats', 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)));
|
$documents = $dbForProject->getDocument('stats', md5(self::INFINITY_PERIOD . str_replace('{databaseInternalId}', $document->getInternalId(), METRIC_DATABASE_ID_DOCUMENTS)));
|
||||||
if (!empty($collections['value'])) {
|
if (!empty($collections['value'])) {
|
||||||
$metrics[] = [
|
$metrics[] = [
|
||||||
'key' => METRIC_COLLECTIONS,
|
'key' => METRIC_COLLECTIONS,
|
||||||
|
@ -124,7 +125,7 @@ class Usage extends Action
|
||||||
case str_starts_with($document->getCollection(), 'database_') && !str_contains($document->getCollection(), 'collection'): //collections
|
case str_starts_with($document->getCollection(), 'database_') && !str_contains($document->getCollection(), 'collection'): //collections
|
||||||
$parts = explode('_', $document->getCollection());
|
$parts = explode('_', $document->getCollection());
|
||||||
$databaseInternalId = $parts[1] ?? 0;
|
$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'])) {
|
if (!empty($documents['value'])) {
|
||||||
$metrics[] = [
|
$metrics[] = [
|
||||||
|
@ -139,8 +140,8 @@ class Usage extends Action
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case $document->getCollection() === 'buckets':
|
case $document->getCollection() === 'buckets':
|
||||||
$files = $dbForProject->getDocument('stats_v2', md5(self::INFINITY_PERIOD . str_replace('{bucketInternalId}', $document->getInternalId(), METRIC_BUCKET_ID_FILES)));
|
$files = $dbForProject->getDocument('stats', 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)));
|
$storage = $dbForProject->getDocument('stats', md5(self::INFINITY_PERIOD . str_replace('{bucketInternalId}', $document->getInternalId(), METRIC_BUCKET_ID_FILES_STORAGE)));
|
||||||
|
|
||||||
if (!empty($files['value'])) {
|
if (!empty($files['value'])) {
|
||||||
$metrics[] = [
|
$metrics[] = [
|
||||||
|
@ -158,13 +159,13 @@ class Usage extends Action
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case $document->getCollection() === 'functions':
|
case $document->getCollection() === 'functions':
|
||||||
$deployments = $dbForProject->getDocument('stats_v2', md5(self::INFINITY_PERIOD . str_replace(['{resourceType}', '{resourceInternalId}'], ['functions', $document->getInternalId()], METRIC_FUNCTION_ID_DEPLOYMENTS)));
|
$deployments = $dbForProject->getDocument('stats', 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)));
|
$deploymentsStorage = $dbForProject->getDocument('stats', 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)));
|
$builds = $dbForProject->getDocument('stats', 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)));
|
$buildsStorage = $dbForProject->getDocument('stats', 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)));
|
$buildsCompute = $dbForProject->getDocument('stats', 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)));
|
$executions = $dbForProject->getDocument('stats', 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)));
|
$executionsCompute = $dbForProject->getDocument('stats', md5(self::INFINITY_PERIOD . str_replace('{functionInternalId}', $document->getInternalId(), METRIC_FUNCTION_ID_EXECUTIONS_COMPUTE)));
|
||||||
|
|
||||||
if (!empty($deployments['value'])) {
|
if (!empty($deployments['value'])) {
|
||||||
$metrics[] = [
|
$metrics[] = [
|
||||||
|
|
|
@ -67,7 +67,7 @@ class UsageHook extends Usage
|
||||||
$id = \md5("{$time}_{$period}_{$key}");
|
$id = \md5("{$time}_{$period}_{$key}");
|
||||||
|
|
||||||
try {
|
try {
|
||||||
$dbForProject->createDocument('stats_v2', new Document([
|
$dbForProject->createDocument('stats', new Document([
|
||||||
'$id' => $id,
|
'$id' => $id,
|
||||||
'period' => $period,
|
'period' => $period,
|
||||||
'time' => $time,
|
'time' => $time,
|
||||||
|
@ -78,14 +78,14 @@ class UsageHook extends Usage
|
||||||
} catch (Duplicate $th) {
|
} catch (Duplicate $th) {
|
||||||
if ($value < 0) {
|
if ($value < 0) {
|
||||||
$dbForProject->decreaseDocumentAttribute(
|
$dbForProject->decreaseDocumentAttribute(
|
||||||
'stats_v2',
|
'stats',
|
||||||
$id,
|
$id,
|
||||||
'value',
|
'value',
|
||||||
abs($value)
|
abs($value)
|
||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
$dbForProject->increaseDocumentAttribute(
|
$dbForProject->increaseDocumentAttribute(
|
||||||
'stats_v2',
|
'stats',
|
||||||
$id,
|
$id,
|
||||||
'value',
|
'value',
|
||||||
$value
|
$value
|
||||||
|
|
|
@ -7,7 +7,9 @@ class Topics extends Base
|
||||||
public const ALLOWED_ATTRIBUTES = [
|
public const ALLOWED_ATTRIBUTES = [
|
||||||
'name',
|
'name',
|
||||||
'description',
|
'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;
|
||||||
|
}
|
||||||
|
}
|
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;
|
||||||
|
}
|
||||||
|
}
|
|
@ -34,9 +34,21 @@ class Topic extends Model
|
||||||
'default' => '',
|
'default' => '',
|
||||||
'example' => 'events',
|
'example' => 'events',
|
||||||
])
|
])
|
||||||
->addRule('total', [
|
->addRule('emailTotal', [
|
||||||
'type' => self::TYPE_INTEGER,
|
'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,
|
'default' => 0,
|
||||||
'example' => 100,
|
'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) {
|
messagingCreateTopic(topicId: $topicId, name: $name) {
|
||||||
_id
|
_id
|
||||||
name
|
name
|
||||||
|
emailTotal
|
||||||
|
smsTotal
|
||||||
|
pushTotal
|
||||||
}
|
}
|
||||||
}';
|
}';
|
||||||
case self::$LIST_TOPICS:
|
case self::$LIST_TOPICS:
|
||||||
|
@ -2035,6 +2038,9 @@ trait Base
|
||||||
topics {
|
topics {
|
||||||
_id
|
_id
|
||||||
name
|
name
|
||||||
|
emailTotal
|
||||||
|
smsTotal
|
||||||
|
pushTotal
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}';
|
}';
|
||||||
|
@ -2043,6 +2049,9 @@ trait Base
|
||||||
messagingGetTopic(topicId: $topicId) {
|
messagingGetTopic(topicId: $topicId) {
|
||||||
_id
|
_id
|
||||||
name
|
name
|
||||||
|
emailTotal
|
||||||
|
smsTotal
|
||||||
|
pushTotal
|
||||||
}
|
}
|
||||||
}';
|
}';
|
||||||
case self::$UPDATE_TOPIC:
|
case self::$UPDATE_TOPIC:
|
||||||
|
@ -2050,6 +2059,9 @@ trait Base
|
||||||
messagingUpdateTopic(topicId: $topicId, name: $name) {
|
messagingUpdateTopic(topicId: $topicId, name: $name) {
|
||||||
_id
|
_id
|
||||||
name
|
name
|
||||||
|
emailTotal
|
||||||
|
smsTotal
|
||||||
|
pushTotal
|
||||||
}
|
}
|
||||||
}';
|
}';
|
||||||
case self::$DELETE_TOPIC:
|
case self::$DELETE_TOPIC:
|
||||||
|
|
|
@ -355,7 +355,9 @@ trait MessagingBase
|
||||||
'x-appwrite-key' => $this->getProject()['apiKey'],
|
'x-appwrite-key' => $this->getProject()['apiKey'],
|
||||||
], [
|
], [
|
||||||
'queries' => [
|
'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'],
|
'x-appwrite-key' => $this->getProject()['apiKey'],
|
||||||
], [
|
], [
|
||||||
'queries' => [
|
'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(200, $response['headers']['status-code']);
|
||||||
$this->assertEquals('android-app', $response['body']['name']);
|
$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(200, $topic['headers']['status-code']);
|
||||||
$this->assertEquals('android-app', $topic['body']['name']);
|
$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([
|
$response2 = $this->client->call(Client::METHOD_POST, '/messaging/topics/' . $topics['private']['$id'] . '/subscribers', \array_merge([
|
||||||
'content-type' => 'application/json',
|
'content-type' => 'application/json',
|
||||||
|
@ -695,7 +703,9 @@ trait MessagingBase
|
||||||
|
|
||||||
$this->assertEquals(200, $topic['headers']['status-code']);
|
$this->assertEquals(200, $topic['headers']['status-code']);
|
||||||
$this->assertEquals('android-app', $topic['body']['name']);
|
$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