1
0
Fork 0
mirror of synced 2024-06-26 18:20:43 +12:00

Merge branch 'refactor-permissions-inc-console-fix' of https://github.com/appwrite/appwrite into feat-improve-keys

This commit is contained in:
Christy Jacob 2022-08-19 09:32:22 +00:00
commit 2a750eaa73
106 changed files with 3356 additions and 2590 deletions

3
.gitignore vendored
View file

@ -9,4 +9,5 @@
.php_cs.cache
debug/
app/sdks
dev/yasd_init.php
dev/yasd_init.php
.phpunit.result.cache

View file

@ -142,7 +142,7 @@ Learn more at our [Technology Stack](#technology-stack) section.
##### Security
- [Appwrite Auth and ACL](https://github.com/appwrite/appwrite/blob/0.7.x/docs/specs/authentication.drawio.svg)
- [Appwrite Auth and ACL](https://github.com/appwrite/appwrite/blob/master/docs/specs/authentication.drawio.svg)
- [OAuth](https://en.wikipedia.org/wiki/OAuth)
- [Encryption](https://medium.com/searchencrypt/what-is-encryption-how-does-it-work-e8f20e340537#:~:text=Encryption%20is%20a%20process%20that,%2C%20or%20decrypt%2C%20the%20information.)
- [Hashing](https://searchsqlserver.techtarget.com/definition/hashing#:~:text=Hashing%20is%20the%20transformation%20of,it%20using%20the%20original%20value.)

File diff suppressed because it is too large Load diff

View file

@ -52,8 +52,8 @@ $admins = [
];
return [
Auth::USER_ROLE_GUEST => [
'label' => 'Guest',
Auth::USER_ROLE_GUESTS => [
'label' => 'Guests',
'scopes' => [
'public',
'home',
@ -64,8 +64,8 @@ return [
'avatars.read',
],
],
Auth::USER_ROLE_MEMBER => [
'label' => 'Member',
Auth::USER_ROLE_USERS => [
'label' => 'Users',
'scopes' => \array_merge($member, []),
],
Auth::USER_ROLE_ADMIN => [
@ -80,8 +80,8 @@ return [
'label' => 'Owner',
'scopes' => \array_merge($member, $admins, []),
],
Auth::USER_ROLE_APP => [
'label' => 'Application',
Auth::USER_ROLE_APPS => [
'label' => 'Applications',
'scopes' => ['health.read'],
],
];

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View file

@ -29,7 +29,10 @@ use Utopia\Database\Database;
use Utopia\Database\Document;
use Utopia\Database\DateTime;
use Utopia\Database\Exception\Duplicate;
use Utopia\Database\ID;
use Utopia\Database\Permission;
use Utopia\Database\Query;
use Utopia\Database\Role;
use Utopia\Database\Validator\Authorization;
use Utopia\Database\Validator\UID;
use Utopia\Locale\Locale;
@ -95,11 +98,14 @@ App::post('/v1/account')
}
try {
$userId = $userId == 'unique()' ? $dbForProject->getId() : $userId;
$userId = $userId == 'unique()' ? ID::unique() : $userId;
$user = Authorization::skip(fn() => $dbForProject->createDocument('users', new Document([
'$id' => $userId,
'$read' => ['role:all'],
'$write' => ['user:' . $userId],
'$permissions' => [
Permission::read(Role::any()),
Permission::update(Role::user($userId)),
Permission::delete(Role::user($userId)),
],
'email' => $email,
'emailVerification' => false,
'status' => true,
@ -120,9 +126,9 @@ App::post('/v1/account')
throw new Exception(Exception::USER_ALREADY_EXISTS);
}
Authorization::unsetRole('role:' . Auth::USER_ROLE_GUEST);
Authorization::setRole('user:' . $user->getId());
Authorization::setRole('role:' . Auth::USER_ROLE_MEMBER);
Authorization::unsetRole(Role::guests()->toString());
Authorization::setRole(Role::user($user->getId())->toString());
Authorization::setRole(Role::users()->toString());
$usage->setParam('users.create', 1);
$events->setParam('userId', $user->getId());
@ -181,7 +187,7 @@ App::post('/v1/account/sessions/email')
$secret = Auth::tokenGenerator();
$session = new Document(array_merge(
[
'$id' => $dbForProject->getId(),
'$id' => ID::unique(),
'userId' => $profile->getId(),
'userInternalId' => $profile->getInternalId(),
'provider' => Auth::SESSION_PROVIDER_EMAIL,
@ -197,7 +203,7 @@ App::post('/v1/account/sessions/email')
$detector->getDevice()
));
Authorization::setRole('user:' . $profile->getId());
Authorization::setRole(Role::user($profile->getId())->toString());
// Re-hash if not using recommended algo
if ($profile->getAttribute('hash') !== Auth::DEFAULT_ALGO) {
@ -208,12 +214,15 @@ App::post('/v1/account/sessions/email')
$dbForProject->updateDocument('users', $profile->getId(), $profile);
}
$session = $dbForProject->createDocument('sessions', $session
->setAttribute('$read', ['user:' . $profile->getId()])
->setAttribute('$write', ['user:' . $profile->getId()]));
$dbForProject->deleteCachedDocument('users', $profile->getId());
$session = $dbForProject->createDocument('sessions', $session->setAttribute('$permissions', [
Permission::read(Role::user($profile->getId())),
Permission::update(Role::user($profile->getId())),
Permission::delete(Role::user($profile->getId())),
]));
if (!Config::getParam('domainVerification')) {
$response
->addHeader('X-Fallback-Cookies', \json_encode([Auth::$cookieName => Auth::encodeSession($profile->getId(), $secret)]))
@ -481,11 +490,14 @@ App::get('/v1/account/sessions/oauth2/:provider/redirect')
}
try {
$userId = $dbForProject->getId();
$userId = ID::unique();
$user = Authorization::skip(fn() => $dbForProject->createDocument('users', new Document([
'$id' => $userId,
'$read' => ['role:all'],
'$write' => ['user:' . $userId],
'$permissions' => [
Permission::read(Role::any()),
Permission::update(Role::user($userId)),
Permission::delete(Role::user($userId)),
],
'email' => $email,
'emailVerification' => true,
'status' => true, // Email should already be authenticated by OAuth2 provider
@ -519,7 +531,7 @@ App::get('/v1/account/sessions/oauth2/:provider/redirect')
$expire = DateTime::addSeconds(new \DateTime(), Auth::TOKEN_EXPIRATION_LOGIN_LONG);
$session = new Document(array_merge([
'$id' => $dbForProject->getId(),
'$id' => ID::unique(),
'userId' => $user->getId(),
'userInternalId' => $user->getInternalId(),
'provider' => $provider,
@ -547,13 +559,15 @@ App::get('/v1/account/sessions/oauth2/:provider/redirect')
->setAttribute('status', true)
;
Authorization::setRole('user:' . $user->getId());
Authorization::setRole(Role::user($user->getId())->toString());
$dbForProject->updateDocument('users', $user->getId(), $user);
$session = $dbForProject->createDocument('sessions', $session
->setAttribute('$read', ['user:' . $user->getId()])
->setAttribute('$write', ['user:' . $user->getId()]));
$session = $dbForProject->createDocument('sessions', $session->setAttribute('$permissions', [
Permission::read(Role::user($user->getId())),
Permission::update(Role::user($user->getId())),
Permission::delete(Role::user($user->getId())),
]));
$dbForProject->deleteCachedDocument('users', $user->getId());
@ -643,12 +657,15 @@ App::post('/v1/account/sessions/magic-url')
}
}
$userId = $userId == 'unique()' ? $dbForProject->getId() : $userId;
$userId = $userId == 'unique()' ? ID::unique() : $userId;
$user = Authorization::skip(fn () => $dbForProject->createDocument('users', new Document([
'$id' => $userId,
'$read' => ['role:all'],
'$write' => ['user:' . $userId],
'$permissions' => [
Permission::read(Role::any()),
Permission::update(Role::user($userId)),
Permission::delete(Role::user($userId)),
],
'email' => $email,
'emailVerification' => false,
'status' => true,
@ -670,7 +687,7 @@ App::post('/v1/account/sessions/magic-url')
$expire = DateTime::addSeconds(new \DateTime(), Auth::TOKEN_EXPIRATION_CONFIRM);
$token = new Document([
'$id' => $dbForProject->getId(),
'$id' => ID::unique(),
'userId' => $user->getId(),
'userInternalId' => $user->getInternalId(),
'type' => Auth::TOKEN_TYPE_MAGIC_URL,
@ -680,11 +697,14 @@ App::post('/v1/account/sessions/magic-url')
'ip' => $request->getIP(),
]);
Authorization::setRole('user:' . $user->getId());
Authorization::setRole(Role::user($user->getId())->toString());
$token = $dbForProject->createDocument('tokens', $token
->setAttribute('$read', ['user:' . $user->getId()])
->setAttribute('$write', ['user:' . $user->getId()]));
->setAttribute('$permissions', [
Permission::read(Role::user($user->getId())),
Permission::update(Role::user($user->getId())),
Permission::delete(Role::user($user->getId())),
]));
$dbForProject->deleteCachedDocument('users', $user->getId());
@ -767,7 +787,7 @@ App::put('/v1/account/sessions/magic-url')
$session = new Document(array_merge(
[
'$id' => $dbForProject->getId(),
'$id' => ID::unique(),
'userId' => $user->getId(),
'userInternalId' => $user->getInternalId(),
'provider' => Auth::SESSION_PROVIDER_MAGIC_URL,
@ -782,11 +802,14 @@ App::put('/v1/account/sessions/magic-url')
$detector->getDevice()
));
Authorization::setRole('user:' . $user->getId());
Authorization::setRole(Role::user($user->getId())->toString());
$session = $dbForProject->createDocument('sessions', $session
->setAttribute('$read', ['user:' . $user->getId()])
->setAttribute('$write', ['user:' . $user->getId()]));
->setAttribute('$permissions', [
Permission::read(Role::user($user->getId())),
Permission::update(Role::user($user->getId())),
Permission::delete(Role::user($user->getId())),
]));
$dbForProject->deleteCachedDocument('users', $user->getId());
@ -881,12 +904,15 @@ App::post('/v1/account/sessions/phone')
}
}
$userId = $userId == 'unique()' ? $dbForProject->getId() : $userId;
$userId = $userId == 'unique()' ? ID::unique() : $userId;
$user = Authorization::skip(fn () => $dbForProject->createDocument('users', new Document([
'$id' => $userId,
'$read' => ['role:all'],
'$write' => ['user:' . $userId],
'$permissions' => [
Permission::read(Role::any()),
Permission::update(Role::user($userId)),
Permission::delete(Role::user($userId)),
],
'email' => null,
'phone' => $phone,
'emailVerification' => false,
@ -908,7 +934,7 @@ App::post('/v1/account/sessions/phone')
$expire = DateTime::addSeconds(new \DateTime(), Auth::TOKEN_EXPIRATION_PHONE);
$token = new Document([
'$id' => $dbForProject->getId(),
'$id' => ID::unique(),
'userId' => $user->getId(),
'userInternalId' => $user->getInternalId(),
'type' => Auth::TOKEN_TYPE_PHONE,
@ -918,11 +944,14 @@ App::post('/v1/account/sessions/phone')
'ip' => $request->getIP(),
]);
Authorization::setRole('user:' . $user->getId());
Authorization::setRole(Role::user($user->getId())->toString());
$token = $dbForProject->createDocument('tokens', $token
->setAttribute('$read', ['user:' . $user->getId()])
->setAttribute('$write', ['user:' . $user->getId()]));
->setAttribute('$permissions', [
Permission::read(Role::user($user->getId())),
Permission::update(Role::user($user->getId())),
Permission::delete(Role::user($user->getId())),
]));
$dbForProject->deleteCachedDocument('users', $user->getId());
@ -990,7 +1019,7 @@ App::put('/v1/account/sessions/phone')
$session = new Document(array_merge(
[
'$id' => $dbForProject->getId(),
'$id' => ID::unique(),
'userId' => $user->getId(),
'userInternalId' => $user->getInternalId(),
'provider' => Auth::SESSION_PROVIDER_PHONE,
@ -1005,11 +1034,14 @@ App::put('/v1/account/sessions/phone')
$detector->getDevice()
));
Authorization::setRole('user:' . $user->getId());
Authorization::setRole(Role::user($user->getId())->toString());
$session = $dbForProject->createDocument('sessions', $session
->setAttribute('$read', ['user:' . $user->getId()])
->setAttribute('$write', ['user:' . $user->getId()]));
->setAttribute('$permissions', [
Permission::read(Role::user($user->getId())),
Permission::update(Role::user($user->getId())),
Permission::delete(Role::user($user->getId())),
]));
$dbForProject->deleteCachedDocument('users', $user->getId());
@ -1103,11 +1135,14 @@ App::post('/v1/account/sessions/anonymous')
}
}
$userId = $dbForProject->getId();
$userId = ID::unique();
$user = Authorization::skip(fn() => $dbForProject->createDocument('users', new Document([
'$id' => $userId,
'$read' => ['role:all'],
'$write' => ['user:' . $userId],
'$permissions' => [
Permission::read(Role::any()),
Permission::update(Role::user($userId)),
Permission::delete(Role::user($userId)),
],
'email' => null,
'emailVerification' => false,
'status' => true,
@ -1134,7 +1169,7 @@ App::post('/v1/account/sessions/anonymous')
$session = new Document(array_merge(
[
'$id' => $dbForProject->getId(),
'$id' => ID::unique(),
'userId' => $user->getId(),
'userInternalId' => $user->getInternalId(),
'provider' => Auth::SESSION_PROVIDER_ANONYMOUS,
@ -1149,11 +1184,13 @@ App::post('/v1/account/sessions/anonymous')
$detector->getDevice()
));
Authorization::setRole('user:' . $user->getId());
Authorization::setRole(Role::user($user->getId())->toString());
$session = $dbForProject->createDocument('sessions', $session
->setAttribute('$read', ['user:' . $user->getId()])
->setAttribute('$write', ['user:' . $user->getId()]));
$session = $dbForProject->createDocument('sessions', $session-> setAttribute('$permissions', [
Permission::read(Role::user($user->getId())),
Permission::update(Role::user($user->getId())),
Permission::delete(Role::user($user->getId())),
]));
$dbForProject->deleteCachedDocument('users', $user->getId());
@ -1468,7 +1505,7 @@ App::patch('/v1/account/password')
->action(function (string $password, string $oldPassword, Response $response, Document $user, Database $dbForProject, Stats $usage, Event $events) {
// Check old password only if its an existing user.
if ($user->getAttribute('passwordUpdate') !== 0 && !Auth::passwordVerify($oldPassword, $user->getAttribute('password'), $user->getAttribute('hash'), $user->getAttribute('hashOptions'))) { // Double check user password
if ($user->getAttribute('passwordUpdate') !== null && !Auth::passwordVerify($oldPassword, $user->getAttribute('password'), $user->getAttribute('hash'), $user->getAttribute('hashOptions'))) { // Double check user password
throw new Exception(Exception::USER_INVALID_CREDENTIALS);
}
@ -1930,7 +1967,7 @@ App::post('/v1/account/recovery')
$secret = Auth::tokenGenerator();
$recovery = new Document([
'$id' => $dbForProject->getId(),
'$id' => ID::unique(),
'userId' => $profile->getId(),
'userInternalId' => $profile->getInternalId(),
'type' => Auth::TOKEN_TYPE_RECOVERY,
@ -1940,11 +1977,14 @@ App::post('/v1/account/recovery')
'ip' => $request->getIP(),
]);
Authorization::setRole('user:' . $profile->getId());
Authorization::setRole(Role::user($profile->getId())->toString());
$recovery = $dbForProject->createDocument('tokens', $recovery
->setAttribute('$read', ['user:' . $profile->getId()])
->setAttribute('$write', ['user:' . $profile->getId()]));
->setAttribute('$permissions', [
Permission::read(Role::user($profile->getId())),
Permission::update(Role::user($profile->getId())),
Permission::delete(Role::user($profile->getId())),
]));
$dbForProject->deleteCachedDocument('users', $profile->getId());
@ -2022,7 +2062,7 @@ App::put('/v1/account/recovery')
throw new Exception(Exception::USER_INVALID_TOKEN);
}
Authorization::setRole('user:' . $profile->getId());
Authorization::setRole(Role::user($profile->getId())->toString());
$profile = $dbForProject->updateDocument('users', $profile->getId(), $profile
->setAttribute('password', Auth::passwordHash($password, Auth::DEFAULT_ALGO, Auth::DEFAULT_ALGO_OPTIONS))
@ -2088,7 +2128,7 @@ App::post('/v1/account/verification')
$expire = DateTime::addSeconds(new \DateTime(), Auth::TOKEN_EXPIRATION_CONFIRM);
$verification = new Document([
'$id' => $dbForProject->getId(),
'$id' => ID::unique(),
'userId' => $user->getId(),
'userInternalId' => $user->getInternalId(),
'type' => Auth::TOKEN_TYPE_VERIFICATION,
@ -2098,11 +2138,14 @@ App::post('/v1/account/verification')
'ip' => $request->getIP(),
]);
Authorization::setRole('user:' . $user->getId());
Authorization::setRole(Role::user($user->getId())->toString());
$verification = $dbForProject->createDocument('tokens', $verification
->setAttribute('$read', ['user:' . $user->getId()])
->setAttribute('$write', ['user:' . $user->getId()]));
->setAttribute('$permissions', [
Permission::read(Role::user($user->getId())),
Permission::update(Role::user($user->getId())),
Permission::delete(Role::user($user->getId())),
]));
$dbForProject->deleteCachedDocument('users', $user->getId());
@ -2174,7 +2217,7 @@ App::put('/v1/account/verification')
throw new Exception(Exception::USER_INVALID_TOKEN);
}
Authorization::setRole('user:' . $profile->getId());
Authorization::setRole(Role::user($profile->getId())->toString());
$profile = $dbForProject->updateDocument('users', $profile->getId(), $profile->setAttribute('emailVerification', true));
@ -2237,7 +2280,7 @@ App::post('/v1/account/verification/phone')
$expire = DateTime::addSeconds(new \DateTime(), Auth::TOKEN_EXPIRATION_CONFIRM);
$verification = new Document([
'$id' => $dbForProject->getId(),
'$id' => ID::unique(),
'userId' => $user->getId(),
'userInternalId' => $user->getInternalId(),
'type' => Auth::TOKEN_TYPE_PHONE,
@ -2247,11 +2290,14 @@ App::post('/v1/account/verification/phone')
'ip' => $request->getIP(),
]);
Authorization::setRole('user:' . $user->getId());
Authorization::setRole(Role::user($user->getId())->toString());
$verification = $dbForProject->createDocument('tokens', $verification
->setAttribute('$read', ['user:' . $user->getId()])
->setAttribute('$write', ['user:' . $user->getId()]));
->setAttribute('$permissions', [
Permission::read(Role::user($user->getId())),
Permission::update(Role::user($user->getId())),
Permission::delete(Role::user($user->getId())),
]));
$dbForProject->deleteCachedDocument('users', $user->getId());
@ -2315,7 +2361,7 @@ App::put('/v1/account/verification/phone')
throw new Exception(Exception::USER_INVALID_TOKEN);
}
Authorization::setRole('user:' . $profile->getId());
Authorization::setRole(Role::user($profile->getId())->toString());
$profile = $dbForProject->updateDocument('users', $profile->getId(), $profile->setAttribute('phoneVerification', true));

View file

@ -1,10 +1,14 @@
<?php
use Appwrite\Permissions\PermissionsProcessor;
use Utopia\App;
use Appwrite\Event\Delete;
use Appwrite\Extend\Exception;
use Utopia\Audit\Audit;
use Utopia\Database\Permission;
use Utopia\Database\Role;
use Utopia\Database\Validator\DatetimeValidator;
use Utopia\Database\ID;
use Utopia\Validator\Boolean;
use Utopia\Validator\FloatValidator;
use Utopia\Validator\Integer;
@ -94,7 +98,7 @@ function createAttribute(string $databaseId, string $collectionId, Document $att
try {
$attribute = new Document([
'$id' => $db->getInternalId() . '_' . $collection->getInternalId() . '_' . $key,
'$id' => ID::custom($db->getInternalId() . '_' . $collection->getInternalId() . '_' . $key),
'key' => $key,
'databaseInternalId' => $db->getInternalId(),
'databaseId' => $db->getId(),
@ -168,7 +172,7 @@ App::post('/v1/databases')
->inject('events')
->action(function (string $databaseId, string $name, Response $response, Database $dbForProject, Stats $usage, Event $events) {
$databaseId = $databaseId == 'unique()' ? $dbForProject->getId() : $databaseId;
$databaseId = $databaseId == 'unique()' ? ID::unique() : $databaseId;
try {
$dbForProject->createDocument('databases', new Document([
@ -340,7 +344,7 @@ App::get('/v1/databases/:databaseId/logs')
$output[$i] = new Document([
'event' => $log['event'],
'userId' => $log['userId'],
'userId' => ID::custom($log['userId']),
'userEmail' => $log['data']['userEmail'] ?? null,
'userName' => $log['data']['userName'] ?? null,
'mode' => $log['data']['mode'] ?? null,
@ -485,14 +489,13 @@ App::post('/v1/databases/:databaseId/collections')
->param('databaseId', '', new UID(), 'Database ID.')
->param('collectionId', '', new CustomId(), 'Unique Id. Choose your own unique ID or pass the string "unique()" to auto generate it. Valid chars are a-z, A-Z, 0-9, period, hyphen, and underscore. Can\'t start with a special char. Max length is 36 chars.')
->param('name', '', new Text(128), 'Collection name. Max length: 128 chars.')
->param('permission', null, new WhiteList(['document', 'collection']), 'Specifies the permissions model used in this collection, which accepts either \'collection\' or \'document\'. For \'collection\' level permission, the permissions specified in read and write params are applied to all documents in the collection. For \'document\' level permissions, read and write permissions are specified in each document. [learn more about permissions](https://appwrite.io/docs/permissions) and get a full list of available permissions.')
->param('read', null, new Permissions(APP_LIMIT_ARRAY_PARAMS_SIZE), 'An array of strings with read permissions. By default no user is granted with any read permissions. [learn more about permissions](https://appwrite.io/docs/permissions) and get a full list of available permissions.')
->param('write', null, new Permissions(APP_LIMIT_ARRAY_PARAMS_SIZE), 'An array of strings with write permissions. By default no user is granted with any write permissions. [learn more about permissions](https://appwrite.io/docs/permissions) and get a full list of available permissions.')
->param('permissions', null, new Permissions(APP_LIMIT_ARRAY_PARAMS_SIZE), 'An array of strings with permissions. By default no user is granted with any read permissions. [learn more about permissions](https://appwrite.io/docs/permissions) and get a full list of available permissions.')
->param('documentSecurity', false, new Boolean(true), 'Specifies the permissions model used in this collection, which accepts either \'collection\' or \'document\'. For \'collection\' level permission, the permissions specified in read and write params are applied to all documents in the collection. For \'document\' level permissions, read and write permissions are specified in each document. [learn more about permissions](https://appwrite.io/docs/permissions) and get a full list of available permissions.')
->inject('response')
->inject('dbForProject')
->inject('usage')
->inject('events')
->action(function (string $databaseId, string $collectionId, string $name, ?string $permission, ?array $read, ?array $write, Response $response, Database $dbForProject, Stats $usage, Event $events) {
->action(function (string $databaseId, string $collectionId, string $name, ?array $permissions, bool $documentSecurity, Response $response, Database $dbForProject, Stats $usage, Event $events) {
$database = Authorization::skip(fn () => $dbForProject->getDocument('databases', $databaseId));
@ -500,16 +503,21 @@ App::post('/v1/databases/:databaseId/collections')
throw new Exception(Exception::DATABASE_NOT_FOUND);
}
$collectionId = $collectionId == 'unique()' ? $dbForProject->getId() : $collectionId;
$collectionId = $collectionId == 'unique()' ? ID::unique() : $collectionId;
/**
* Map aggregate permissions into the multiple permissions they represent,
* accounting for the resource type given that some types not allowed specific permissions.
*/
$permissions = PermissionsProcessor::aggregate($permissions, 'collection');
try {
$dbForProject->createDocument('database_' . $database->getInternalId(), new Document([
'$id' => $collectionId,
'databaseInternalId' => $database->getInternalId(),
'databaseId' => $databaseId,
'$read' => $read ?? [], // Collection permissions for collection documents (based on permission model)
'$write' => $write ?? [], // Collection permissions for collection documents (based on permission model)
'permission' => $permission, // Permissions model type (document vs collection)
'$permissions' => $permissions ?? [],
'documentSecurity' => $documentSecurity,
'enabled' => true,
'name' => $name,
'search' => implode(' ', [$collectionId, $name]),
@ -583,7 +591,9 @@ App::get('/v1/databases/:databaseId/collections')
throw new Exception(Exception::GENERAL_CURSOR_NOT_FOUND, "Collection '{$cursor}' for the 'cursor' value not found.");
}
$queries[] = $cursorDirection === Database::CURSOR_AFTER ? Query::cursorAfter($cursorDocument) : Query::cursorBefore($cursorDocument);
$queries[] = $cursorDirection === Database::CURSOR_AFTER
? Query::cursorAfter($cursorDocument)
: Query::cursorBefore($cursorDocument);
}
$usage
@ -620,6 +630,7 @@ App::get('/v1/databases/:databaseId/collections/:collectionId')
if ($database->isEmpty()) {
throw new Exception(Exception::DATABASE_NOT_FOUND);
}
$collection = $dbForProject->getDocument('database_' . $database->getInternalId(), $collectionId);
if ($collection->isEmpty()) {
@ -740,40 +751,44 @@ App::put('/v1/databases/:databaseId/collections/:collectionId')
->param('databaseId', '', new UID(), 'Database ID.')
->param('collectionId', '', new UID(), 'Collection ID.')
->param('name', null, new Text(128), 'Collection name. Max length: 128 chars.')
->param('permission', null, new WhiteList(['document', 'collection']), 'Permissions type model to use for reading documents in this collection. You can use collection-level permission set once on the collection using the `read` and `write` params, or you can set document-level permission where each document read and write params will decide who has access to read and write to each document individually. [learn more about permissions](https://appwrite.io/docs/permissions) and get a full list of available permissions.')
->param('read', null, new Permissions(APP_LIMIT_ARRAY_PARAMS_SIZE), 'An array of strings with read permissions. By default inherits the existing read permissions. [learn more about permissions](https://appwrite.io/docs/permissions) and get a full list of available permissions.', true)
->param('write', null, new Permissions(APP_LIMIT_ARRAY_PARAMS_SIZE), 'An array of strings with write permissions. By default inherits the existing write permissions. [learn more about permissions](https://appwrite.io/docs/permissions) and get a full list of available permissions.', true)
->param('permissions', null, new Permissions(APP_LIMIT_ARRAY_PARAMS_SIZE), 'An array of strings with permissions. By default no user is granted with any permissions. [learn more about permissions](/docs/permissions) and get a full list of available permissions.', true)
->param('documentSecurity', false, new Boolean(true), 'Whether to enable document-level permission where each document\'s permissions parameter will decide who has access to each file individually. [learn more about permissions](/docs/permissions) and get a full list of available permissions.')
->param('enabled', true, new Boolean(), 'Is collection enabled?', true)
->inject('response')
->inject('dbForProject')
->inject('usage')
->inject('events')
->action(function (string $databaseId, string $collectionId, string $name, string $permission, ?array $read, ?array $write, bool $enabled, Response $response, Database $dbForProject, Stats $usage, Event $events) {
->action(function (string $databaseId, string $collectionId, string $name, ?array $permissions, bool $documentSecurity, bool $enabled, Response $response, Database $dbForProject, Stats $usage, Event $events) {
$database = Authorization::skip(fn () => $dbForProject->getDocument('databases', $databaseId));
if ($database->isEmpty()) {
throw new Exception(Exception::DATABASE_NOT_FOUND);
}
$collection = $dbForProject->getDocument('database_' . $database->getInternalId(), $collectionId);
if ($collection->isEmpty()) {
throw new Exception(Exception::COLLECTION_NOT_FOUND);
}
$read ??= $collection->getRead() ?? []; // By default inherit read permissions
$write ??= $collection->getWrite() ?? []; // By default inherit write permissions
$permissions ??= $collection->getPermissions() ?? [];
/**
* Map aggregate permissions into the multiple permissions they represent.
*/
$permissions = PermissionsProcessor::aggregate($permissions, 'collection');
$enabled ??= $collection->getAttribute('enabled', true);
try {
$collection = $dbForProject->updateDocument('database_' . $database->getInternalId(), $collectionId, $collection
->setAttribute('$write', $write)
->setAttribute('$read', $read)
->setAttribute('name', $name)
->setAttribute('permission', $permission)
->setAttribute('$permissions', $permissions)
->setAttribute('documentSecurity', $documentSecurity)
->setAttribute('enabled', $enabled)
->setAttribute('search', implode(' ', [$collectionId, $name])));
} catch (AuthorizationException $exception) {
} catch (AuthorizationException) {
throw new Exception(Exception::USER_UNAUTHORIZED);
} catch (StructureException $exception) {
throw new Exception(Exception::DOCUMENT_INVALID_STRUCTURE, 'Bad structure. ' . $exception->getMessage());
@ -1619,7 +1634,7 @@ App::post('/v1/databases/:databaseId/collections/:collectionId/indexes')
try {
$index = $dbForProject->createDocument('indexes', new Document([
'$id' => $db->getInternalId() . '_' . $collection->getInternalId() . '_' . $key,
'$id' => ID::custom($db->getInternalId() . '_' . $collection->getInternalId() . '_' . $key),
'key' => $key,
'status' => 'processing', // processing, available, failed, deleting, stuck
'databaseInternalId' => $db->getInternalId(),
@ -1841,21 +1856,15 @@ App::post('/v1/databases/:databaseId/collections/:collectionId/documents')
->param('documentId', '', new CustomId(), 'Document ID. Choose your own unique ID or pass the string "unique()" to auto generate it. Valid chars are a-z, A-Z, 0-9, period, hyphen, and underscore. Can\'t start with a special char. Max length is 36 chars.')
->param('collectionId', null, new UID(), 'Collection ID. You can create a new collection using the Database service [server integration](https://appwrite.io/docs/server/database#createCollection). Make sure to define attributes before creating documents.')
->param('data', [], new JSON(), 'Document data as JSON object.')
->param('read', null, new Permissions(APP_LIMIT_ARRAY_PARAMS_SIZE), 'An array of strings with read permissions. By default only the current user is granted with read permissions. [learn more about permissions](https://appwrite.io/docs/permissions) and get a full list of available permissions.', true)
->param('write', null, new Permissions(APP_LIMIT_ARRAY_PARAMS_SIZE), 'An array of strings with write permissions. By default only the current user is granted with write permissions. [learn more about permissions](https://appwrite.io/docs/permissions) and get a full list of available permissions.', true)
->param('permissions', null, new Permissions(APP_LIMIT_ARRAY_PARAMS_SIZE, [Database::PERMISSION_READ, Database::PERMISSION_UPDATE, Database::PERMISSION_DELETE]), 'An array of strings with permissions. By default no user is granted with any permissions. [learn more about permissions](/docs/permissions) and get a full list of available permissions.', true)
->inject('response')
->inject('dbForProject')
->inject('user')
->inject('usage')
->inject('events')
->inject('mode')
->action(function (string $databaseId, string $documentId, string $collectionId, string|array $data, ?array $read, ?array $write, Response $response, Database $dbForProject, Document $user, Stats $usage, Event $events, string $mode) {
->action(function (string $databaseId, string $documentId, string $collectionId, string|array $data, ?array $permissions, Response $response, Database $dbForProject, Document $user, Stats $usage, Event $events, string $mode) {
$database = Authorization::skip(fn () => $dbForProject->getDocument('databases', $databaseId));
if ($database->isEmpty()) {
throw new Exception(Exception::DATABASE_NOT_FOUND);
}
$data = (\is_string($data)) ? \json_decode($data, true) : $data; // Cast to JSON array
if (empty($data)) {
@ -1866,11 +1875,12 @@ App::post('/v1/databases/:databaseId/collections/:collectionId/documents')
throw new Exception(Exception::DOCUMENT_INVALID_STRUCTURE, '$id is not allowed for creating new documents, try update instead');
}
/**
* Skip Authorization to get the collection. Needed in case of empty permissions for document level permissions.
*
* @var Document $collection
*/
$database = Authorization::skip(fn () => $dbForProject->getDocument('databases', $databaseId));
if ($database->isEmpty()) {
throw new Exception(Exception::DATABASE_NOT_FOUND);
}
$collection = Authorization::skip(fn() => $dbForProject->getDocument('database_' . $database->getInternalId(), $collectionId));
if ($collection->isEmpty() || !$collection->getAttribute('enabled')) {
@ -1879,42 +1889,72 @@ App::post('/v1/databases/:databaseId/collections/:collectionId/documents')
}
}
// Check collection permissions when enforced
if ($collection->getAttribute('permission') === 'collection') {
$validator = new Authorization('write');
if (!$validator->isValid($collection->getWrite())) {
throw new Exception(Exception::USER_UNAUTHORIZED);
$validator = new Authorization('create');
if (!$validator->isValid($collection->getCreate())) {
throw new Exception(Exception::USER_UNAUTHORIZED);
}
/**
* Map aggregate permissions into the multiple permissions they represent,
* accounting for the resource type given that some types not allowed specific permissions.
*/
$permissions = PermissionsProcessor::aggregate($permissions, 'document');
/**
* Add permissions for current the user for any missing types
* from the allowed permissions for this resource type.
*/
$allowedPermissions = \array_filter(
Database::PERMISSIONS,
fn ($permission) => $permission !== Database::PERMISSION_CREATE
);
if (\is_null($permissions)) {
$permissions = [];
if (!empty($user->getId())) {
foreach ($allowedPermissions as $permission) {
$permissions[] = (new Permission($permission, 'user', $user->getId()))->toString();
}
}
} else {
foreach ($allowedPermissions as $permission) {
/**
* If an allowed permission was not passed in the request,
* and there is a current user, add it for the current user.
*/
if (empty(\preg_grep("#^{$permission}\(.+\)$#", $permissions)) && !empty($user->getId())) {
$permissions[] = (new Permission($permission, 'user', $user->getId()))->toString();
}
}
}
// Users can only manage their own roles, API keys and Admin users can manage any
// Users can only manage their own roles, API keys and Admin users can manage any
$roles = Authorization::getRoles();
if (!Auth::isAppUser($roles) && !Auth::isPrivilegedUser($roles)) {
foreach (Database::PERMISSIONS as $type) {
foreach ($permissions as $permission) {
$permission = Permission::parse($permission);
if ($permission->getPermission() != $type) {
continue;
}
$role = (new Role(
$permission->getRole(),
$permission->getIdentifier(),
$permission->getDimension()
))->toString();
if (!Authorization::isRole($role)) {
throw new Exception(Exception::USER_UNAUTHORIZED, 'Permissions must be one of: (' . \implode(', ', Authorization::getRoles()) . ')');
}
}
}
}
$data['$collection'] = $collection->getId(); // Adding this param to make API easier for developers
$data['$id'] = $documentId == 'unique()' ? $dbForProject->getId() : $documentId;
$data['$read'] = (is_null($read) && !$user->isEmpty()) ? ['user:' . $user->getId()] : $read ?? []; // By default set read permissions for user
$data['$write'] = (is_null($write) && !$user->isEmpty()) ? ['user:' . $user->getId()] : $write ?? []; // By default set write permissions for user
// Users can only add their roles to documents, API keys and Admin users can add any
$roles = Authorization::getRoles();
if (!Auth::isAppUser($roles) && !Auth::isPrivilegedUser($roles)) {
foreach ($data['$read'] as $read) {
if (!Authorization::isRole($read)) {
throw new Exception(Exception::USER_UNAUTHORIZED, 'Read permissions must be one of: (' . \implode(', ', $roles) . ')');
}
}
foreach ($data['$write'] as $write) {
if (!Authorization::isRole($write)) {
throw new Exception(Exception::USER_UNAUTHORIZED, 'Write permissions must be one of: (' . \implode(', ', $roles) . ')');
}
}
}
$data['$id'] = $documentId == 'unique()' ? ID::unique() : $documentId;
$data['$permissions'] = $permissions;
try {
if ($collection->getAttribute('permission') === 'collection') {
/** @var Document $document */
$document = Authorization::skip(fn() => $dbForProject->createDocument('database_' . $database->getInternalId() . '_collection_' . $collection->getInternalId(), new Document($data)));
} else {
$document = $dbForProject->createDocument('database_' . $database->getInternalId() . '_collection_' . $collection->getInternalId(), new Document($data));
}
$document = $dbForProject->createDocument('database_' . $database->getInternalId() . '_collection_' . $collection->getInternalId(), new Document($data));
$document->setAttribute('$collection', $collectionId);
} catch (StructureException $exception) {
throw new Exception(Exception::DOCUMENT_INVALID_STRUCTURE, $exception->getMessage());
@ -1972,11 +2012,7 @@ App::get('/v1/databases/:databaseId/collections/:collectionId/documents')
if ($database->isEmpty()) {
throw new Exception(Exception::DATABASE_NOT_FOUND);
}
/**
* Skip Authorization to get the collection. Needed in case of empty permissions for document level permissions.
*
* @var Utopia\Database\Document $collection
*/
$collection = Authorization::skip(fn() => $dbForProject->getDocument('database_' . $database->getInternalId(), $collectionId));
if ($collection->isEmpty() || !$collection->getAttribute('enabled')) {
@ -1985,12 +2021,11 @@ App::get('/v1/databases/:databaseId/collections/:collectionId/documents')
}
}
// Check collection permissions when enforced
if ($collection->getAttribute('permission') === 'collection') {
$validator = new Authorization('read');
if (!$validator->isValid($collection->getRead())) {
throw new Exception(Exception::USER_UNAUTHORIZED);
}
$documentSecurity = $collection->getAttribute('documentSecurity', false);
$validator = new Authorization('read');
$valid = $validator->isValid($collection->getRead());
if (!$valid && !$documentSecurity) {
throw new Exception(Exception::USER_UNAUTHORIZED);
}
$filterQueries = \array_map(function ($query) {
@ -2011,10 +2046,11 @@ App::get('/v1/databases/:databaseId/collections/:collectionId/documents')
}
if (!empty($cursor)) {
$cursorDocument = $collection->getAttribute('permission') === 'collection'
? Authorization::skip(fn () => $dbForProject->getDocument('database_' . $database->getInternalId() . '_collection_' . $collection->getInternalId(), $cursor))
: $dbForProject->getDocument('database_' . $database->getInternalId() . '_collection_' . $collection->getInternalId(), $cursor);
if ($documentSecurity) {
$cursorDocument = $dbForProject->getDocument('database_' . $database->getInternalId() . '_collection_' . $collection->getInternalId(), $cursor);
} else {
$cursorDocument = Authorization::skip(fn() => $dbForProject->getDocument('database_' . $database->getInternalId() . '_collection_' . $collection->getInternalId(), $cursor));
}
if ($cursorDocument->isEmpty()) {
throw new Exception(Exception::GENERAL_CURSOR_NOT_FOUND, "Document '{$cursor}' for the 'cursor' value not found.");
}
@ -2032,13 +2068,12 @@ App::get('/v1/databases/:databaseId/collections/:collectionId/documents')
}
}
if ($collection->getAttribute('permission') === 'collection') {
/** @var Document[] $documents */
$documents = Authorization::skip(fn () => $dbForProject->find('database_' . $database->getInternalId() . '_collection_' . $collection->getInternalId(), $allQueries));
$total = Authorization::skip(fn () => $dbForProject->count('database_' . $database->getInternalId() . '_collection_' . $collection->getInternalId(), $filterQueries, APP_LIMIT_COUNT));
} else {
if ($documentSecurity) {
$documents = $dbForProject->find('database_' . $database->getInternalId() . '_collection_' . $collection->getInternalId(), $allQueries);
$total = $dbForProject->count('database_' . $database->getInternalId() . '_collection_' . $collection->getInternalId(), $filterQueries, APP_LIMIT_COUNT);
} else {
$documents = Authorization::skip(fn () => $dbForProject->find('database_' . $database->getInternalId() . '_collection_' . $collection->getInternalId(), $allQueries));
$total = Authorization::skip(fn () => $dbForProject->count('database_' . $database->getInternalId() . '_collection_' . $collection->getInternalId(), $filterQueries, APP_LIMIT_COUNT));
}
/**
@ -2084,9 +2119,7 @@ App::get('/v1/databases/:databaseId/collections/:collectionId/documents/:documen
if ($database->isEmpty()) {
throw new Exception(Exception::DATABASE_NOT_FOUND);
}
/**
* Skip Authorization to get the collection. Needed in case of empty permissions for document level permissions.
*/
$collection = Authorization::skip(fn() => $dbForProject->getDocument('database_' . $database->getInternalId(), $collectionId));
if ($collection->isEmpty() || !$collection->getAttribute('enabled')) {
@ -2095,19 +2128,24 @@ App::get('/v1/databases/:databaseId/collections/:collectionId/documents/:documen
}
}
// Check collection permissions when enforced
if ($collection->getAttribute('permission') === 'collection') {
$validator = new Authorization('read');
if (!$validator->isValid($collection->getRead())) {
throw new Exception(Exception::USER_UNAUTHORIZED);
}
$documentSecurity = $collection->getAttribute('documentSecurity', false);
$validator = new Authorization('read');
$valid = $validator->isValid($collection->getRead());
if (!$valid && !$documentSecurity) {
throw new Exception(Exception::USER_UNAUTHORIZED);
}
if ($collection->getAttribute('permission') === 'collection') {
/** @var Document $document */
$document = Authorization::skip(fn() => $dbForProject->getDocument('database_' . $database->getInternalId() . '_collection_' . $collection->getInternalId(), $documentId));
} else {
$document = $dbForProject->getDocument('database_' . $database->getInternalId() . '_collection_' . $collection->getInternalId(), $documentId);
$document = $dbForProject->getDocument('database_' . $database->getInternalId() . '_collection_' . $collection->getInternalId(), $documentId);
if ($document->isEmpty()) {
throw new Exception(Exception::DOCUMENT_NOT_FOUND);
}
if ($documentSecurity) {
$valid |= $validator->isValid($document->getRead());
if (!$valid) {
throw new Exception(Exception::USER_UNAUTHORIZED);
}
}
/**
@ -2115,10 +2153,6 @@ App::get('/v1/databases/:databaseId/collections/:collectionId/documents/:documen
*/
$document->setAttribute('$collection', $collectionId);
if ($document->isEmpty()) {
throw new Exception(Exception::DOCUMENT_NOT_FOUND);
}
$usage
->setParam('databases.documents.read', 1)
->setParam('databaseId', $databaseId)
@ -2240,23 +2274,26 @@ App::patch('/v1/databases/:databaseId/collections/:collectionId/documents/:docum
->param('collectionId', null, new UID(), 'Collection ID.')
->param('documentId', null, new UID(), 'Document ID.')
->param('data', [], new JSON(), 'Document data as JSON object. Include only attribute and value pairs to be updated.', true)
->param('read', null, new Permissions(APP_LIMIT_ARRAY_PARAMS_SIZE), 'An array of strings with read permissions. By default inherits the existing read permissions. [learn more about permissions](https://appwrite.io/docs/permissions) and get a full list of available permissions.', true)
->param('write', null, new Permissions(APP_LIMIT_ARRAY_PARAMS_SIZE), 'An array of strings with write permissions. By default inherits the existing write permissions. [learn more about permissions](https://appwrite.io/docs/permissions) and get a full list of available permissions.', true)
->param('permissions', null, new Permissions(APP_LIMIT_ARRAY_PARAMS_SIZE), 'An array of strings with permissions. By default no user is granted with any permissions. [learn more about permissions](/docs/permissions) and get a full list of available permissions.', true)
->inject('response')
->inject('dbForProject')
->inject('usage')
->inject('events')
->inject('mode')
->action(function (string $databaseId, string $collectionId, string $documentId, string|array $data, ?array $read, ?array $write, Response $response, Database $dbForProject, Stats $usage, Event $events, string $mode) {
->action(function (string $databaseId, string $collectionId, string $documentId, string|array $data, ?array $permissions, Response $response, Database $dbForProject, Stats $usage, Event $events, string $mode) {
$data = (\is_string($data)) ? \json_decode($data, true) : $data; // Cast to JSON array
if (empty($data) && \is_null($permissions)) {
throw new Exception(Exception::DOCUMENT_MISSING_PAYLOAD);
}
$database = Authorization::skip(fn () => $dbForProject->getDocument('databases', $databaseId));
if ($database->isEmpty()) {
throw new Exception(Exception::DATABASE_NOT_FOUND);
}
/**
* Skip Authorization to get the collection. Needed in case of empty permissions for document level permissions.
*/
$collection = Authorization::skip(fn() => $dbForProject->getDocument('database_' . $database->getInternalId(), $collectionId));
if ($collection->isEmpty() || !$collection->getAttribute('enabled')) {
@ -2265,75 +2302,73 @@ App::patch('/v1/databases/:databaseId/collections/:collectionId/documents/:docum
}
}
// Check collection permissions when enforced
if ($collection->getAttribute('permission') === 'collection') {
$validator = new Authorization('write');
if (!$validator->isValid($collection->getWrite())) {
throw new Exception(Exception::USER_UNAUTHORIZED);
}
$document = Authorization::skip(fn() => $dbForProject->getDocument('database_' . $database->getInternalId() . '_collection_' . $collection->getInternalId(), $documentId));
} else {
$document = $dbForProject->getDocument('database_' . $database->getInternalId() . '_collection_' . $collection->getInternalId(), $documentId);
$documentSecurity = $collection->getAttribute('documentSecurity', false);
$validator = new Authorization('update');
$valid = $validator->isValid($collection->getUpdate());
if (!$valid && !$documentSecurity) {
throw new Exception(Exception::USER_UNAUTHORIZED);
}
$document = $dbForProject->getDocument('database_' . $database->getInternalId() . '_collection_' . $collection->getInternalId(), $documentId);
if ($document->isEmpty()) {
throw new Exception(Exception::DOCUMENT_NOT_FOUND);
}
$data = (\is_string($data)) ? \json_decode($data, true) : $data; // Cast to JSON array
if (empty($data) && empty($read) && empty($write)) {
throw new Exception(Exception::DOCUMENT_MISSING_PAYLOAD, 'Missing payload or read/write permissions');
if ($documentSecurity) {
$valid |= $validator->isValid($document->getUpdate());
if (!$valid) {
throw new Exception(Exception::USER_UNAUTHORIZED);
}
}
if (!\is_array($data)) {
throw new Exception(Exception::DOCUMENT_INVALID_STRUCTURE, 'Data param should be a valid JSON object');
// Users can only manage their own roles, API keys and Admin users can manage any
$roles = Authorization::getRoles();
if (!Auth::isAppUser($roles) && !Auth::isPrivilegedUser($roles) && !\is_null($permissions)) {
foreach (Database::PERMISSIONS as $type) {
foreach ($permissions as $permission) {
$permission = Permission::parse($permission);
if ($permission->getPermission() != $type) {
continue;
}
$role = (new Role(
$permission->getRole(),
$permission->getIdentifier(),
$permission->getDimension()
))->toString();
if (!Authorization::isRole($role)) {
throw new Exception(Exception::USER_UNAUTHORIZED, 'Permissions must be one of: (' . \implode(', ', Authorization::getRoles()) . ')');
}
}
}
}
/**
* Map aggregate permissions into the multiple permissions they represent,
* accounting for the resource type given that some types not allowed specific permissions.
*/
$permissions = PermissionsProcessor::aggregate($permissions, 'document');
if (\is_null($permissions)) {
$permissions = $document->getPermissions() ?? [];
}
$data = \array_merge($document->getArrayCopy(), $data);
$data['$collection'] = $collection->getId(); // Make sure user don't switch collectionID
$data['$createdAt'] = $document->getCreatedAt(); // Make sure user don't switch createdAt
$data['$id'] = $document->getId(); // Make sure user don't switch document unique ID
$data['$read'] = (is_null($read)) ? ($document->getRead() ?? []) : $read; // By default inherit read permissions
$data['$write'] = (is_null($write)) ? ($document->getWrite() ?? []) : $write; // By default inherit write permissions
// Users can only add their roles to documents, API keys and Admin users can add any
$roles = Authorization::getRoles();
if (!Auth::isAppUser($roles) && !Auth::isPrivilegedUser($roles)) {
if (!is_null($read)) {
foreach ($data['$read'] as $read) {
if (!Authorization::isRole($read)) {
throw new Exception(Exception::USER_UNAUTHORIZED, 'Read permissions must be one of: (' . \implode(', ', $roles) . ')');
}
}
}
if (!is_null($write)) {
foreach ($data['$write'] as $write) {
if (!Authorization::isRole($write)) {
throw new Exception(Exception::USER_UNAUTHORIZED, 'Write permissions must be one of: (' . \implode(', ', $roles) . ')');
}
}
}
}
$data['$collection'] = $collection->getId(); // Make sure user doesn't switch collectionID
$data['$createdAt'] = $document->getCreatedAt(); // Make sure user doesn't switch createdAt
$data['$id'] = $document->getId(); // Make sure user doesn't switch document unique ID
$data['$permissions'] = $permissions;
try {
if ($collection->getAttribute('permission') === 'collection') {
/** @var Document $document */
$document = Authorization::skip(fn() => $dbForProject->updateDocument('database_' . $database->getInternalId() . '_collection_' . $collection->getInternalId(), $document->getId(), new Document($data)));
} else {
$document = $dbForProject->updateDocument('database_' . $database->getInternalId() . '_collection_' . $collection->getInternalId(), $document->getId(), new Document($data));
}
$document = $dbForProject->updateDocument('database_' . $database->getInternalId() . '_collection_' . $collection->getInternalId(), $document->getId(), new Document($data));
/**
* Reset $collection attribute to remove prefix.
*/
$document->setAttribute('$collection', $collectionId);
} catch (AuthorizationException $exception) {
} catch (AuthorizationException) {
throw new Exception(Exception::USER_UNAUTHORIZED);
} catch (DuplicateException $exception) {
} catch (DuplicateException) {
throw new Exception(Exception::DOCUMENT_ALREADY_EXISTS);
} catch (StructureException $exception) {
throw new Exception(Exception::DOCUMENT_INVALID_STRUCTURE, $exception->getMessage());
@ -2385,9 +2420,7 @@ App::delete('/v1/databases/:databaseId/collections/:collectionId/documents/:docu
if ($database->isEmpty()) {
throw new Exception(Exception::DATABASE_NOT_FOUND);
}
/**
* Skip Authorization to get the collection. Needed in case of empty permissions for document level permissions.
*/
$collection = Authorization::skip(fn() => $dbForProject->getDocument('database_' . $database->getInternalId(), $collectionId));
if ($collection->isEmpty() || !$collection->getAttribute('enabled')) {
@ -2396,31 +2429,28 @@ App::delete('/v1/databases/:databaseId/collections/:collectionId/documents/:docu
}
}
// Check collection permissions when enforced
if ($collection->getAttribute('permission') === 'collection') {
$validator = new Authorization('write');
if (!$validator->isValid($collection->getWrite())) {
throw new Exception(Exception::USER_UNAUTHORIZED);
}
$documentSecurity = $collection->getAttribute('documentSecurity', false);
$validator = new Authorization('delete');
$valid = $validator->isValid($collection->getDelete());
if (!$valid && !$documentSecurity) {
throw new Exception(Exception::USER_UNAUTHORIZED);
}
if ($collection->getAttribute('permission') === 'collection') {
/** @var Document $document */
$document = Authorization::skip(fn() => $dbForProject->getDocument('database_' . $database->getInternalId() . '_collection_' . $collection->getInternalId(), $documentId));
} else {
$document = $dbForProject->getDocument('database_' . $database->getInternalId() . '_collection_' . $collection->getInternalId(), $documentId);
}
$document = $dbForProject->getDocument('database_' . $database->getInternalId() . '_collection_' . $collection->getInternalId(), $documentId);
if ($document->isEmpty()) {
throw new Exception(Exception::DOCUMENT_NOT_FOUND);
}
if ($collection->getAttribute('permission') === 'collection') {
Authorization::skip(fn() => $dbForProject->deleteDocument('database_' . $database->getInternalId() . '_collection_' . $collection->getInternalId(), $documentId));
} else {
$dbForProject->deleteDocument('database_' . $database->getInternalId() . '_collection_' . $collection->getInternalId(), $documentId);
if ($documentSecurity) {
$valid |= $validator->isValid($document->getDelete());
if (!$valid) {
throw new Exception(Exception::USER_UNAUTHORIZED);
}
}
$dbForProject->deleteDocument('database_' . $database->getInternalId() . '_collection_' . $collection->getInternalId(), $documentId);
$dbForProject->deleteCachedDocument('database_' . $database->getInternalId() . '_collection_' . $collection->getInternalId(), $documentId);
/**

View file

@ -9,6 +9,9 @@ use Appwrite\Event\Func;
use Appwrite\Event\Validator\Event as ValidatorEvent;
use Appwrite\Extend\Exception;
use Appwrite\Utopia\Database\Validator\CustomId;
use Utopia\Database\ID;
use Utopia\Database\Permission;
use Utopia\Database\Role;
use Utopia\Database\Validator\UID;
use Appwrite\Stats\Stats;
use Utopia\Storage\Device;
@ -34,7 +37,7 @@ use Utopia\Config\Config;
use Cron\CronExpression;
use Executor\Executor;
use Utopia\CLI\Console;
use Utopia\Database\Validator\Permissions;
use Utopia\Database\Validator\Roles;
use Utopia\Validator\Boolean;
include_once __DIR__ . '/../shared/api.php';
@ -54,7 +57,7 @@ App::post('/v1/functions')
->label('sdk.response.model', Response::MODEL_FUNCTION)
->param('functionId', '', new CustomId(), 'Function ID. Choose your own unique ID or pass the string "unique()" to auto generate it. Valid chars are a-z, A-Z, 0-9, period, hyphen, and underscore. Can\'t start with a special char. Max length is 36 chars.')
->param('name', '', new Text(128), 'Function name. Max length: 128 chars.')
->param('execute', [], new Permissions(APP_LIMIT_ARRAY_PARAMS_SIZE), 'An array of strings with execution permissions. By default no user is granted with any execute permissions. [learn more about permissions](https://appwrite.io/docs/permissions) and get a full list of available permissions. Maximum of ' . APP_LIMIT_ARRAY_PARAMS_SIZE . ' scopes are allowed, each 64 characters long.')
->param('execute', [], new Roles(APP_LIMIT_ARRAY_PARAMS_SIZE), 'An array of strings with execution roles. By default no user is granted with any execute permissions. [learn more about permissions](https://appwrite.io/docs/permissions) and get a full list of available permissions. Maximum of ' . APP_LIMIT_ARRAY_PARAMS_SIZE . ' scopes are allowed, each 64 characters long.')
->param('runtime', '', new WhiteList(array_keys(Config::getParam('runtimes')), true), 'Execution runtime.')
->param('vars', [], new Assoc(), 'Key-value JSON object that will be passed to the function as environment variables.', true)
->param('events', [], new ArrayList(new ValidatorEvent(), APP_LIMIT_ARRAY_PARAMS_SIZE), 'Events list. Maximum of ' . APP_LIMIT_ARRAY_PARAMS_SIZE . ' events are allowed.', true)
@ -65,7 +68,7 @@ App::post('/v1/functions')
->inject('events')
->action(function (string $functionId, string $name, array $execute, string $runtime, array $vars, array $events, string $schedule, int $timeout, Response $response, Database $dbForProject, Event $eventsInstance) {
$functionId = ($functionId == 'unique()') ? $dbForProject->getId() : $functionId;
$functionId = ($functionId == 'unique()') ? ID::unique() : $functionId;
$function = $dbForProject->createDocument('functions', new Document([
'$id' => $functionId,
'execute' => $execute,
@ -301,7 +304,7 @@ App::put('/v1/functions/:functionId')
->label('sdk.response.model', Response::MODEL_FUNCTION)
->param('functionId', '', new UID(), 'Function ID.')
->param('name', '', new Text(128), 'Function name. Max length: 128 chars.')
->param('execute', [], new Permissions(APP_LIMIT_ARRAY_PARAMS_SIZE), 'An array of strings with execution permissions. By default no user is granted with any execute permissions. [learn more about permissions](https://appwrite.io/docs/permissions) and get a full list of available permissions. Maximum of ' . APP_LIMIT_ARRAY_PARAMS_SIZE . ' scopes are allowed, each 64 characters long.')
->param('execute', [], new Roles(APP_LIMIT_ARRAY_PARAMS_SIZE), 'An array of strings with execution roles. By default no user is granted with any execute permissions. [learn more about permissions](https://appwrite.io/docs/permissions) and get a full list of available permissions. Maximum of ' . APP_LIMIT_ARRAY_PARAMS_SIZE . ' scopes are allowed, each 64 characters long.')
->param('vars', [], new Assoc(), 'Key-value JSON object that will be passed to the function as environment variables.', true)
->param('events', [], new ArrayList(new ValidatorEvent(), APP_LIMIT_ARRAY_PARAMS_SIZE), 'Events list. Maximum of ' . APP_LIMIT_ARRAY_PARAMS_SIZE . ' events are allowed.', true)
->param('schedule', '', new Cron(), 'Schedule CRON syntax.', true)
@ -508,7 +511,7 @@ App::post('/v1/functions/:functionId/deployments')
}
$contentRange = $request->getHeader('content-range');
$deploymentId = $dbForProject->getId();
$deploymentId = ID::unique();
$chunk = 1;
$chunks = 1;
@ -582,8 +585,11 @@ App::post('/v1/functions/:functionId/deployments')
if ($deployment->isEmpty()) {
$deployment = $dbForProject->createDocument('deployments', new Document([
'$id' => $deploymentId,
'$read' => ['role:all'],
'$write' => ['role:all'],
'$permissions' => [
Permission::read(Role::any()),
Permission::update(Role::any()),
Permission::delete(Role::any()),
],
'resourceId' => $function->getId(),
'resourceType' => 'functions',
'entrypoint' => $entrypoint,
@ -611,8 +617,11 @@ App::post('/v1/functions/:functionId/deployments')
if ($deployment->isEmpty()) {
$deployment = $dbForProject->createDocument('deployments', new Document([
'$id' => $deploymentId,
'$read' => ['role:all'],
'$write' => ['role:all'],
'$permissions' => [
Permission::read(Role::any()),
Permission::update(Role::any()),
Permission::delete(Role::any()),
],
'resourceId' => $function->getId(),
'resourceType' => 'functions',
'entrypoint' => $entrypoint,
@ -869,13 +878,12 @@ App::post('/v1/functions/:functionId/executions')
throw new Exception(Exception::USER_UNAUTHORIZED, $validator->getDescription());
}
$executionId = $dbForProject->getId();
$executionId = ID::unique();
/** @var Document $execution */
$execution = Authorization::skip(fn () => $dbForProject->createDocument('executions', new Document([
'$id' => $executionId,
'$read' => (!$user->isEmpty()) ? ['user:' . $user->getId()] : [],
'$write' => [],
'$permissions' => !$user->isEmpty() ? [Permission::read(Role::user($user->getId()))] : [],
'functionId' => $function->getId(),
'deploymentId' => $deployment->getId(),
'trigger' => 'http', // http / schedule / event

View file

@ -17,8 +17,11 @@ use Utopia\Audit\Audit;
use Utopia\Config\Config;
use Utopia\Database\Database;
use Utopia\Database\Document;
use Utopia\Database\ID;
use Utopia\Database\DateTime;
use Utopia\Database\Permission;
use Utopia\Database\Query;
use Utopia\Database\Role;
use Utopia\Database\Validator\Authorization;
use Utopia\Database\Validator\DatetimeValidator;
use Utopia\Database\Validator\UID;
@ -80,7 +83,7 @@ App::post('/v1/projects')
$auths[$method['key'] ?? ''] = true;
}
$projectId = ($projectId == 'unique()') ? $dbForConsole->getId() : $projectId;
$projectId = ($projectId == 'unique()') ? ID::unique() : $projectId;
if ($projectId === 'console') {
throw new Exception(Exception::PROJECT_RESERVED_PROJECT, "'console' is a reserved project.");
@ -88,8 +91,13 @@ App::post('/v1/projects')
$project = $dbForConsole->createDocument('projects', new Document([
'$id' => $projectId,
'$read' => ['team:' . $teamId],
'$write' => ['team:' . $teamId . '/owner', 'team:' . $teamId . '/developer'],
'$permissions' => [
Permission::read(Role::team(ID::custom($teamId))),
Permission::update(Role::team(ID::custom($teamId), 'owner')),
Permission::update(Role::team(ID::custom($teamId), 'developer')),
Permission::delete(Role::team(ID::custom($teamId), 'owner')),
Permission::delete(Role::team(ID::custom($teamId), 'developer')),
],
'name' => $name,
'teamInternalId' => $team->getInternalId(),
'teamId' => $team->getId(),
@ -102,7 +110,7 @@ App::post('/v1/projects')
'legalState' => $legalState,
'legalCity' => $legalCity,
'legalAddress' => $legalAddress,
'legalTaxId' => $legalTaxId,
'legalTaxId' => ID::custom($legalTaxId),
'services' => new stdClass(),
'platforms' => null,
'authProviders' => [],
@ -598,9 +606,12 @@ App::post('/v1/projects/:projectId/webhooks')
$security = (bool) filter_var($security, FILTER_VALIDATE_BOOLEAN);
$webhook = new Document([
'$id' => $dbForConsole->getId(),
'$read' => ['role:all'],
'$write' => ['role:all'],
'$id' => ID::unique(),
'$permissions' => [
Permission::read(Role::any()),
Permission::update(Role::any()),
Permission::delete(Role::any()),
],
'projectInternalId' => $project->getInternalId(),
'projectId' => $project->getId(),
'name' => $name,
@ -843,9 +854,12 @@ App::post('/v1/projects/:projectId/keys')
}
$key = new Document([
'$id' => $dbForConsole->getId(),
'$read' => ['role:all'],
'$write' => ['role:all'],
'$id' => ID::unique(),
'$permissions' => [
Permission::read(Role::any()),
Permission::update(Role::any()),
Permission::delete(Role::any()),
],
'projectInternalId' => $project->getInternalId(),
'projectId' => $project->getId(),
'name' => $name,
@ -1042,9 +1056,12 @@ App::post('/v1/projects/:projectId/platforms')
}
$platform = new Document([
'$id' => $dbForConsole->getId(),
'$read' => ['role:all'],
'$write' => ['role:all'],
'$id' => ID::unique(),
'$permissions' => [
Permission::read(Role::any()),
Permission::update(Role::any()),
Permission::delete(Role::any()),
],
'projectInternalId' => $project->getInternalId(),
'projectId' => $project->getId(),
'type' => $type,
@ -1255,9 +1272,12 @@ App::post('/v1/projects/:projectId/domains')
$domain = new Domain($domain);
$domain = new Document([
'$id' => $dbForConsole->getId(),
'$read' => ['role:all'],
'$write' => ['role:all'],
'$id' => ID::unique(),
'$permissions' => [
Permission::read(Role::any()),
Permission::update(Role::any()),
Permission::delete(Role::any()),
],
'projectInternalId' => $project->getInternalId(),
'projectId' => $project->getId(),
'updated' => DateTime::now(),

View file

@ -4,6 +4,7 @@ use Appwrite\Auth\Auth;
use Appwrite\ClamAV\Network;
use Appwrite\Event\Delete;
use Appwrite\Event\Event;
use Appwrite\Permissions\PermissionsProcessor;
use Appwrite\Utopia\Database\Validator\CustomId;
use Appwrite\OpenSSL\OpenSSL;
use Appwrite\Stats\Stats;
@ -16,7 +17,10 @@ use Utopia\Database\DateTime;
use Utopia\Database\Exception\Duplicate;
use Utopia\Database\Exception\Duplicate as DuplicateException;
use Utopia\Database\Exception\Structure as StructureException;
use Utopia\Database\ID;
use Utopia\Database\Permission;
use Utopia\Database\Query;
use Utopia\Database\Role;
use Utopia\Database\Validator\Authorization;
use Utopia\Database\Validator\Permissions;
use Utopia\Database\Validator\UID;
@ -54,9 +58,8 @@ App::post('/v1/storage/buckets')
->label('sdk.response.model', Response::MODEL_BUCKET)
->param('bucketId', '', new CustomId(), 'Unique Id. Choose your own unique ID or pass the string `unique()` to auto generate it. Valid chars are a-z, A-Z, 0-9, period, hyphen, and underscore. Can\'t start with a special char. Max length is 36 chars.')
->param('name', '', new Text(128), 'Bucket name')
->param('permission', null, new WhiteList(['file', 'bucket']), 'Permissions type model to use for reading files in this bucket. You can use bucket-level permission set once on the bucket using the `read` and `write` params, or you can set file-level permission where each file read and write params will decide who has access to read and write to each file individually. [learn more about permissions](/docs/permissions) and get a full list of available permissions.')
->param('read', null, new Permissions(APP_LIMIT_ARRAY_PARAMS_SIZE), 'An array of strings with read permissions. By default no user is granted with any read permissions. [learn more about permissions](/docs/permissions) and get a full list of available permissions.', true)
->param('write', null, new Permissions(APP_LIMIT_ARRAY_PARAMS_SIZE), 'An array of strings with write permissions. By default no user is granted with any write permissions. [learn more about permissions](/docs/permissions) and get a full list of available permissions.', true)
->param('permissions', null, new Permissions(APP_LIMIT_ARRAY_PARAMS_SIZE), 'An array of strings with permissions. By default no user is granted with any permissions. [learn more about permissions](/docs/permissions) and get a full list of available permissions.', true)
->param('fileSecurity', false, new Boolean(true), 'Whether to enable file-level permission where each files permissions parameter will decide who has access to each file individually. [learn more about permissions](/docs/permissions) and get a full list of available permissions.')
->param('enabled', true, new Boolean(true), 'Is bucket enabled?', true)
->param('maximumFileSize', (int) App::getEnv('_APP_STORAGE_LIMIT', 0), new Range(1, (int) App::getEnv('_APP_STORAGE_LIMIT', 0)), 'Maximum file size allowed in bytes. Maximum allowed value is ' . Storage::human(App::getEnv('_APP_STORAGE_LIMIT', 0), 0) . '. For self-hosted setups you can change the max limit by changing the `_APP_STORAGE_LIMIT` environment variable. [Learn more about storage environment variables](docs/environment-variables#storage)', true)
->param('allowedFileExtensions', [], new ArrayList(new Text(64), APP_LIMIT_ARRAY_PARAMS_SIZE), 'Allowed file extensions. Maximum of ' . APP_LIMIT_ARRAY_PARAMS_SIZE . ' extensions are allowed, each 64 characters long.', true)
@ -66,9 +69,16 @@ App::post('/v1/storage/buckets')
->inject('dbForProject')
->inject('usage')
->inject('events')
->action(function (string $bucketId, string $name, string $permission, ?array $read, ?array $write, bool $enabled, int $maximumFileSize, array $allowedFileExtensions, bool $encryption, bool $antivirus, Response $response, Database $dbForProject, Stats $usage, Event $events) {
->action(function (string $bucketId, string $name, ?array $permissions, string $fileSecurity, bool $enabled, int $maximumFileSize, array $allowedFileExtensions, bool $encryption, bool $antivirus, Response $response, Database $dbForProject, Stats $usage, Event $events) {
$bucketId = $bucketId === 'unique()' ? ID::unique() : $bucketId;
/**
* Map aggregate permissions into the multiple permissions they represent,
* accounting for the resource type given that some types not allowed specific permissions.
*/
$permissions = PermissionsProcessor::aggregate($permissions, 'bucket');
$bucketId = $bucketId === 'unique()' ? $dbForProject->getId() : $bucketId;
try {
$files = Config::getParam('collections', [])['files'] ?? [];
if (empty($files)) {
@ -102,25 +112,24 @@ App::post('/v1/storage/buckets')
]);
}
$bucket = $dbForProject->createDocument('buckets', new Document([
$dbForProject->createDocument('buckets', new Document([
'$id' => $bucketId,
'$collection' => 'buckets',
'$permissions' => $permissions,
'name' => $name,
'permission' => $permission,
'maximumFileSize' => $maximumFileSize,
'allowedFileExtensions' => $allowedFileExtensions,
'fileSecurity' => (bool) filter_var($fileSecurity, FILTER_VALIDATE_BOOLEAN),
'enabled' => (bool) filter_var($enabled, FILTER_VALIDATE_BOOLEAN),
'encryption' => (bool) filter_var($encryption, FILTER_VALIDATE_BOOLEAN),
'antivirus' => (bool) filter_var($antivirus, FILTER_VALIDATE_BOOLEAN),
'$read' => $read ?? [],
'$write' => $write ?? [],
'search' => implode(' ', [$bucketId, $name]),
]));
$bucket = $dbForProject->getDocument('buckets', $bucketId);
$dbForProject->createCollection('bucket_' . $bucket->getInternalId(), $attributes, $indexes);
} catch (Duplicate $th) {
} catch (Duplicate) {
throw new Exception(Exception::STORAGE_BUCKET_ALREADY_EXISTS);
}
@ -227,9 +236,8 @@ App::put('/v1/storage/buckets/:bucketId')
->label('sdk.response.model', Response::MODEL_BUCKET)
->param('bucketId', '', new UID(), 'Bucket unique ID.')
->param('name', null, new Text(128), 'Bucket name', false)
->param('permission', null, new WhiteList(['file', 'bucket']), 'Permissions type model to use for reading files in this bucket. You can use bucket-level permission set once on the bucket using the `read` and `write` params, or you can set file-level permission where each file read and write params will decide who has access to read and write to each file individually. [learn more about permissions](/docs/permissions) and get a full list of available permissions.')
->param('read', null, new Permissions(APP_LIMIT_ARRAY_PARAMS_SIZE), 'An array of strings with read permissions. By default inherits the existing read permissions. [learn more about permissions](/docs/permissions) and get a full list of available permissions.', true)
->param('write', null, new Permissions(APP_LIMIT_ARRAY_PARAMS_SIZE), 'An array of strings with write permissions. By default inherits the existing write permissions. [learn more about permissions](/docs/permissions) and get a full list of available permissions.', true)
->param('permissions', null, new Permissions(APP_LIMIT_ARRAY_PARAMS_SIZE), 'An array of strings with permissions. By default no user is granted with any permissions. [learn more about permissions](/docs/permissions) and get a full list of available permissions.', true)
->param('fileSecurity', false, new Boolean(true), 'Whether to enable file-level permission where each files permissions parameter will decide who has access to each file individually. [learn more about permissions](/docs/permissions) and get a full list of available permissions.')
->param('enabled', true, new Boolean(true), 'Is bucket enabled?', true)
->param('maximumFileSize', null, new Range(1, (int) App::getEnv('_APP_STORAGE_LIMIT', 0)), 'Maximum file size allowed in bytes. Maximum allowed value is ' . Storage::human((int)App::getEnv('_APP_STORAGE_LIMIT', 0), 0) . '. For self hosted version you can change the limit by changing _APP_STORAGE_LIMIT environment variable. [Learn more about storage environment variables](docs/environment-variables#storage)', true)
->param('allowedFileExtensions', [], new ArrayList(new Text(64), APP_LIMIT_ARRAY_PARAMS_SIZE), 'Allowed file extensions. Maximum of ' . APP_LIMIT_ARRAY_PARAMS_SIZE . ' extensions are allowed, each 64 characters long.', true)
@ -239,30 +247,34 @@ App::put('/v1/storage/buckets/:bucketId')
->inject('dbForProject')
->inject('usage')
->inject('events')
->action(function (string $bucketId, string $name, string $permission, ?array $read, ?array $write, bool $enabled, ?int $maximumFileSize, array $allowedFileExtensions, bool $encryption, bool $antivirus, Response $response, Database $dbForProject, Stats $usage, Event $events) {
->action(function (string $bucketId, string $name, ?array $permissions, string $fileSecurity, bool $enabled, ?int $maximumFileSize, array $allowedFileExtensions, bool $encryption, bool $antivirus, Response $response, Database $dbForProject, Stats $usage, Event $events) {
$bucket = $dbForProject->getDocument('buckets', $bucketId);
if ($bucket->isEmpty()) {
throw new Exception(Exception::STORAGE_BUCKET_NOT_FOUND);
}
$read ??= $bucket->getAttribute('$read', []); // By default inherit read permissions
$write ??= $bucket->getAttribute('$write', []); // By default inherit write permissions
$permissions ??= $bucket->getPermissions();
$maximumFileSize ??= $bucket->getAttribute('maximumFileSize', (int) App::getEnv('_APP_STORAGE_LIMIT', 0));
$allowedFileExtensions ??= $bucket->getAttribute('allowedFileExtensions', []);
$enabled ??= $bucket->getAttribute('enabled', true);
$encryption ??= $bucket->getAttribute('encryption', true);
$antivirus ??= $bucket->getAttribute('antivirus', true);
/**
* Map aggregate permissions into the multiple permissions they represent,
* accounting for the resource type given that some types not allowed specific permissions.
*/
$permissions = PermissionsProcessor::aggregate($permissions, 'bucket');
$bucket = $dbForProject->updateDocument('buckets', $bucket->getId(), $bucket
->setAttribute('name', $name)
->setAttribute('$read', $read)
->setAttribute('$write', $write)
->setAttribute('$permissions', $permissions)
->setAttribute('maximumFileSize', $maximumFileSize)
->setAttribute('allowedFileExtensions', $allowedFileExtensions)
->setAttribute('fileSecurity', (bool) filter_var($fileSecurity, FILTER_VALIDATE_BOOLEAN))
->setAttribute('enabled', (bool) filter_var($enabled, FILTER_VALIDATE_BOOLEAN))
->setAttribute('encryption', (bool) filter_var($encryption, FILTER_VALIDATE_BOOLEAN))
->setAttribute('permission', $permission)
->setAttribute('antivirus', (bool) filter_var($antivirus, FILTER_VALIDATE_BOOLEAN)));
$events
@ -336,8 +348,7 @@ App::post('/v1/storage/buckets/:bucketId/files')
->param('bucketId', null, new UID(), 'Storage bucket unique ID. You can create a new storage bucket using the Storage service [server integration](/docs/server/storage#createBucket).')
->param('fileId', '', new CustomId(), 'File ID. Choose your own unique ID or pass the string "unique()" to auto generate it. Valid chars are a-z, A-Z, 0-9, period, hyphen, and underscore. Can\'t start with a special char. Max length is 36 chars.')
->param('file', [], new File(), 'Binary file.', false)
->param('read', null, new Permissions(APP_LIMIT_ARRAY_PARAMS_SIZE), 'An array of strings with read permissions. By default only the current user is granted with read permissions. [learn more about permissions](https://appwrite.io/docs/permissions) and get a full list of available permissions.', true)
->param('write', null, new Permissions(APP_LIMIT_ARRAY_PARAMS_SIZE), 'An array of strings with write permissions. By default only the current user is granted with write permissions. [learn more about permissions](https://appwrite.io/docs/permissions) and get a full list of available permissions.', true)
->param('permissions', null, new Permissions(APP_LIMIT_ARRAY_PARAMS_SIZE, [Database::PERMISSION_READ, Database::PERMISSION_UPDATE, Database::PERMISSION_DELETE]), 'An array of strings with permissions. By default no user is granted with any permissions. [learn more about permissions](/docs/permissions) and get a full list of available permissions.', true)
->inject('request')
->inject('response')
->inject('dbForProject')
@ -348,41 +359,69 @@ App::post('/v1/storage/buckets/:bucketId/files')
->inject('deviceFiles')
->inject('deviceLocal')
->inject('deletes')
->action(function (string $bucketId, string $fileId, mixed $file, ?array $read, ?array $write, Request $request, Response $response, Database $dbForProject, Document $user, Stats $usage, Event $events, string $mode, Device $deviceFiles, Device $deviceLocal, Delete $deletes) {
->action(function (string $bucketId, string $fileId, mixed $file, ?array $permissions, Request $request, Response $response, Database $dbForProject, Document $user, Stats $usage, Event $events, string $mode, Device $deviceFiles, Device $deviceLocal, Delete $deletes) {
$bucket = Authorization::skip(fn () => $dbForProject->getDocument('buckets', $bucketId));
if (
$bucket->isEmpty()
|| (!$bucket->getAttribute('enabled') && $mode !== APP_MODE_ADMIN)
) {
if ($bucket->isEmpty() || (!$bucket->getAttribute('enabled') && $mode !== APP_MODE_ADMIN)) {
throw new Exception(Exception::STORAGE_BUCKET_NOT_FOUND);
}
// Check bucket permissions when enforced
$permissionBucket = $bucket->getAttribute('permission') === 'bucket';
if ($permissionBucket) {
$validator = new Authorization('write');
if (!$validator->isValid($bucket->getWrite())) {
throw new Exception(Exception::USER_UNAUTHORIZED);
$validator = new Authorization('create');
if (!$validator->isValid($bucket->getCreate())) {
throw new Exception(Exception::USER_UNAUTHORIZED);
}
/**
* Map aggregate permissions into the multiple permissions they represent,
* accounting for the resource type given that some types not allowed specific permissions.
*/
$permissions = PermissionsProcessor::aggregate($permissions, 'file');
/**
* Add permissions for current the user for any missing types
* from the allowed permissions for this resource type.
*/
$allowedPermissions = \array_filter(
Database::PERMISSIONS,
fn ($permission) => $permission !== Database::PERMISSION_CREATE
);
if (\is_null($permissions)) {
$permissions = [];
if (!empty($user->getId())) {
foreach ($allowedPermissions as $permission) {
$permissions[] = (new Permission($permission, 'user', $user->getId()))->toString();
}
}
} else {
foreach ($allowedPermissions as $permission) {
/**
* If an allowed permission was not passed in the request,
* and there is a current user, add it for the current user.
*/
if (empty(\preg_grep("#^{$permission}\(.+\)$#", $permissions)) && !empty($user->getId())) {
$permissions[] = (new Permission($permission, 'user', $user->getId()))->toString();
}
}
}
$read = (is_null($read) && !$user->isEmpty()) ? ['user:' . $user->getId()] : $read ?? []; // By default set read permissions for user
$write = (is_null($write) && !$user->isEmpty()) ? ['user:' . $user->getId()] : $write ?? [];
// Users can only add their roles to files, API keys and Admin users can add any
// Users can only manage their own roles, API keys and Admin users can manage any
$roles = Authorization::getRoles();
if (!Auth::isAppUser($roles) && !Auth::isPrivilegedUser($roles)) {
foreach ($read as $role) {
if (!Authorization::isRole($role)) {
throw new Exception(Exception::USER_UNAUTHORIZED, 'Read permissions must be one of: (' . \implode(', ', $roles) . ')', 400);
}
}
foreach ($write as $role) {
if (!Authorization::isRole($role)) {
throw new Exception(Exception::USER_UNAUTHORIZED, 'Write permissions must be one of: (' . \implode(', ', $roles) . ')', 400);
foreach (Database::PERMISSIONS as $type) {
foreach ($permissions as $permission) {
$permission = Permission::parse($permission);
if ($permission->getPermission() != $type) {
continue;
}
$role = (new Role(
$permission->getRole(),
$permission->getIdentifier(),
$permission->getDimension()
))->toString();
if (!Authorization::isRole($role)) {
throw new Exception(Exception::USER_UNAUTHORIZED, 'Permissions must be one of: (' . \implode(', ', Authorization::getRoles()) . ')');
}
}
}
}
@ -411,7 +450,7 @@ App::post('/v1/storage/buckets/:bucketId/files')
$fileSize = (\is_array($file['size']) && isset($file['size'][0])) ? $file['size'][0] : $file['size'];
$contentRange = $request->getHeader('content-range');
$fileId = $fileId === 'unique()' ? $dbForProject->getId() : $fileId;
$fileId = $fileId === 'unique()' ? ID::unique() : $fileId;
$chunk = 1;
$chunks = 1;
@ -460,13 +499,7 @@ App::post('/v1/storage/buckets/:bucketId/files')
$path = $deviceFiles->getPath($fileId . '.' . \pathinfo($fileName, PATHINFO_EXTENSION));
$path = str_ireplace($deviceFiles->getRoot(), $deviceFiles->getRoot() . DIRECTORY_SEPARATOR . $bucket->getId(), $path); // Add bucket id to path after root
if ($permissionBucket) {
$file = Authorization::skip(function () use ($dbForProject, $bucket, $fileId) {
return $dbForProject->getDocument('bucket_' . $bucket->getInternalId(), $fileId);
});
} else {
$file = $dbForProject->getDocument('bucket_' . $bucket->getInternalId(), $fileId);
}
$file = $dbForProject->getDocument('bucket_' . $bucket->getInternalId(), $fileId);
$metadata = ['content_type' => $deviceLocal->getFileMimeType($fileTmpName)];
if (!$file->isEmpty()) {
@ -482,8 +515,6 @@ App::post('/v1/storage/buckets/:bucketId/files')
throw new Exception(Exception::GENERAL_SERVER_ERROR, 'Failed uploading file');
}
$read = (is_null($read) && !$user->isEmpty()) ? ['user:' . $user->getId()] : $read ?? [];
$write = (is_null($write) && !$user->isEmpty()) ? ['user:' . $user->getId()] : $write ?? [];
if ($chunksUploaded === $chunks) {
if (App::getEnv('_APP_STORAGE_ANTIVIRUS') === 'enabled' && $bucket->getAttribute('antivirus', true) && $fileSize <= APP_LIMIT_ANTIVIRUS && App::getEnv('_APP_STORAGE_DEVICE', Storage::DEVICE_LOCAL) === Storage::DEVICE_LOCAL) {
$antivirus = new Network(
@ -537,8 +568,7 @@ App::post('/v1/storage/buckets/:bucketId/files')
if ($file->isEmpty()) {
$doc = new Document([
'$id' => $fileId,
'$read' => $read,
'$write' => $write,
'$permissions' => $permissions,
'bucketId' => $bucket->getId(),
'name' => $fileName,
'path' => $path,
@ -557,15 +587,11 @@ App::post('/v1/storage/buckets/:bucketId/files')
'search' => implode(' ', [$fileId, $fileName]),
'metadata' => $metadata,
]);
if ($permissionBucket) {
$file = Authorization::skip(fn () => $dbForProject->createDocument('bucket_' . $bucket->getInternalId(), $doc));
} else {
$file = $dbForProject->createDocument('bucket_' . $bucket->getInternalId(), $doc);
}
$file = $dbForProject->createDocument('bucket_' . $bucket->getInternalId(), $doc);
} else {
$file = $file
->setAttribute('$read', $read)
->setAttribute('$write', $write)
->setAttribute('$permissions', $permissions)
->setAttribute('signature', $fileHash)
->setAttribute('mimeType', $mimeType)
->setAttribute('sizeActual', $sizeActual)
@ -577,15 +603,11 @@ App::post('/v1/storage/buckets/:bucketId/files')
->setAttribute('metadata', $metadata)
->setAttribute('chunksUploaded', $chunksUploaded);
if ($permissionBucket) {
$file = Authorization::skip(fn () => $dbForProject->updateDocument('bucket_' . $bucket->getInternalId(), $fileId, $file));
} else {
$file = $dbForProject->updateDocument('bucket_' . $bucket->getInternalId(), $fileId, $file);
}
$file = $dbForProject->updateDocument('bucket_' . $bucket->getInternalId(), $fileId, $file);
}
} catch (StructureException $exception) {
throw new Exception(Exception::DOCUMENT_INVALID_STRUCTURE, $exception->getMessage());
} catch (DuplicateException $exception) {
} catch (DuplicateException) {
throw new Exception(Exception::DOCUMENT_ALREADY_EXISTS);
}
@ -598,9 +620,8 @@ App::post('/v1/storage/buckets/:bucketId/files')
try {
if ($file->isEmpty()) {
$doc = new Document([
'$id' => $fileId,
'$read' => $read,
'$write' => $write,
'$id' => ID::custom($fileId),
'$permissions' => $permissions,
'bucketId' => $bucket->getId(),
'name' => $fileName,
'path' => $path,
@ -615,25 +636,18 @@ App::post('/v1/storage/buckets/:bucketId/files')
'search' => implode(' ', [$fileId, $fileName]),
'metadata' => $metadata,
]);
if ($permissionBucket) {
$file = Authorization::skip(fn () => $dbForProject->createDocument('bucket_' . $bucket->getInternalId(), $doc));
} else {
$file = $dbForProject->createDocument('bucket_' . $bucket->getInternalId(), $doc);
}
$file = $dbForProject->createDocument('bucket_' . $bucket->getInternalId(), $doc);
} else {
$file = $file
->setAttribute('chunksUploaded', $chunksUploaded)
->setAttribute('metadata', $metadata);
if ($permissionBucket) {
$file = Authorization::skip(fn () => $dbForProject->updateDocument('bucket_' . $bucket->getInternalId(), $fileId, $file));
} else {
$file = $dbForProject->updateDocument('bucket_' . $bucket->getInternalId(), $fileId, $file);
}
$file = $dbForProject->updateDocument('bucket_' . $bucket->getInternalId(), $fileId, $file);
}
} catch (StructureException $exception) {
throw new Exception(Exception::DOCUMENT_INVALID_STRUCTURE, $exception->getMessage());
} catch (DuplicateException $exception) {
} catch (DuplicateException) {
throw new Exception(Exception::DOCUMENT_ALREADY_EXISTS);
}
}
@ -682,19 +696,13 @@ App::get('/v1/storage/buckets/:bucketId/files')
$bucket = Authorization::skip(fn () => $dbForProject->getDocument('buckets', $bucketId));
if (
$bucket->isEmpty()
|| (!$bucket->getAttribute('enabled') && $mode !== APP_MODE_ADMIN)
) {
if ($bucket->isEmpty() || (!$bucket->getAttribute('enabled') && $mode !== APP_MODE_ADMIN)) {
throw new Exception(Exception::STORAGE_BUCKET_NOT_FOUND);
}
// Check bucket permissions when enforced
if ($bucket->getAttribute('permission') === 'bucket') {
$validator = new Authorization('read');
if (!$validator->isValid($bucket->getRead())) {
throw new Exception(Exception::USER_UNAUTHORIZED);
}
$validator = new Authorization('read');
if (!$validator->isValid($bucket->getRead())) {
throw new Exception(Exception::USER_UNAUTHORIZED);
}
$filterQueries = [];
@ -708,11 +716,7 @@ App::get('/v1/storage/buckets/:bucketId/files')
$queries[] = Query::offset($offset);
$queries[] = $orderType === Database::ORDER_ASC ? Query::orderAsc('') : Query::orderDesc('');
if (!empty($cursor)) {
if ($bucket->getAttribute('permission') === 'bucket') {
$cursorDocument = Authorization::skip(fn () => $dbForProject->getDocument('bucket_' . $bucket->getInternalId(), $cursor));
} else {
$cursorDocument = $dbForProject->getDocument('bucket_' . $bucket->getInternalId(), $cursor);
}
$cursorDocument = $dbForProject->getDocument('bucket_' . $bucket->getInternalId(), $cursor);
if ($cursorDocument->isEmpty()) {
throw new Exception(Exception::GENERAL_CURSOR_NOT_FOUND, "File '{$cursor}' for the 'cursor' value not found.");
@ -721,12 +725,12 @@ App::get('/v1/storage/buckets/:bucketId/files')
$queries[] = $cursorDirection === Database::CURSOR_AFTER ? Query::cursorAfter($cursorDocument) : Query::cursorBefore($cursorDocument);
}
if ($bucket->getAttribute('permission') === 'bucket') {
$files = Authorization::skip(fn () => $dbForProject->find('bucket_' . $bucket->getInternalId(), \array_merge($filterQueries, $queries)));
$total = Authorization::skip(fn () => $dbForProject->count('bucket_' . $bucket->getInternalId(), $filterQueries, APP_LIMIT_COUNT));
} else {
if ($bucket->getAttribute('fileSecurity', false)) {
$files = $dbForProject->find('bucket_' . $bucket->getInternalId(), \array_merge($filterQueries, $queries));
$total = $dbForProject->count('bucket_' . $bucket->getInternalId(), $filterQueries, APP_LIMIT_COUNT);
} else {
$files = Authorization::skip(fn () => $dbForProject->find('bucket_' . $bucket->getInternalId(), \array_merge($filterQueries, $queries)));
$total = Authorization::skip(fn () => $dbForProject->count('bucket_' . $bucket->getInternalId(), $filterQueries, APP_LIMIT_COUNT));
}
$usage
@ -762,31 +766,30 @@ App::get('/v1/storage/buckets/:bucketId/files/:fileId')
$bucket = Authorization::skip(fn () => $dbForProject->getDocument('buckets', $bucketId));
if (
$bucket->isEmpty()
|| (!$bucket->getAttribute('enabled') && $mode !== APP_MODE_ADMIN)
) {
if ($bucket->isEmpty() || (!$bucket->getAttribute('enabled') && $mode !== APP_MODE_ADMIN)) {
throw new Exception(Exception::STORAGE_BUCKET_NOT_FOUND);
}
// Check bucket permissions when enforced
if ($bucket->getAttribute('permission') === 'bucket') {
$validator = new Authorization('read');
if (!$validator->isValid($bucket->getRead())) {
throw new Exception(Exception::USER_UNAUTHORIZED);
}
$fileSecurity = $bucket->getAttribute('fileSecurity', false);
$validator = new Authorization('read');
$valid = $validator->isValid($bucket->getRead());
if (!$valid && !$fileSecurity) {
throw new Exception(Exception::USER_UNAUTHORIZED);
}
if ($bucket->getAttribute('permission') === 'bucket') {
$file = Authorization::skip(fn () => $dbForProject->getDocument('bucket_' . $bucket->getInternalId(), $fileId));
} else {
$file = $dbForProject->getDocument('bucket_' . $bucket->getInternalId(), $fileId);
}
$file = $dbForProject->getDocument('bucket_' . $bucket->getInternalId(), $fileId);
if ($file->isEmpty() || $file->getAttribute('bucketId') !== $bucketId) {
throw new Exception(Exception::STORAGE_FILE_NOT_FOUND);
}
if ($fileSecurity) {
$valid = $validator->isValid($file->getRead());
if (!$valid) {
throw new Exception(Exception::USER_UNAUTHORIZED);
}
}
$usage
->setParam('storage.files.read', 1)
->setParam('bucketId', $bucketId)
@ -838,19 +841,15 @@ App::get('/v1/storage/buckets/:bucketId/files/:fileId/preview')
$bucket = Authorization::skip(fn () => $dbForProject->getDocument('buckets', $bucketId));
if (
$bucket->isEmpty()
|| (!$bucket->getAttribute('enabled') && $mode !== APP_MODE_ADMIN)
) {
if ($bucket->isEmpty() || (!$bucket->getAttribute('enabled') && $mode !== APP_MODE_ADMIN)) {
throw new Exception(Exception::STORAGE_BUCKET_NOT_FOUND);
}
// Check bucket permissions when enforced
if ($bucket->getAttribute('permission') === 'bucket') {
$validator = new Authorization('read');
if (!$validator->isValid($bucket->getRead())) {
throw new Exception(Exception::STORAGE_BUCKET_NOT_FOUND, 'Unauthorized permissions');
}
$fileSecurity = $bucket->getAttribute('fileSecurity', false);
$validator = new Authorization('read');
$valid = $validator->isValid($bucket->getRead());
if (!$valid && !$fileSecurity) {
throw new Exception(Exception::USER_UNAUTHORIZED);
}
if ((\strpos($request->getAccept(), 'image/webp') === false) && ('webp' === $output)) { // Fallback webp to jpeg when no browser support
@ -861,17 +860,22 @@ App::get('/v1/storage/buckets/:bucketId/files/:fileId/preview')
$outputs = Config::getParam('storage-outputs');
$fileLogos = Config::getParam('storage-logos');
if ($bucket->getAttribute('permission') === 'bucket') {
// skip authorization
$file = Authorization::skip(fn () => $dbForProject->getDocument('bucket_' . $bucket->getInternalId(), $fileId));
} else {
$file = $dbForProject->getDocument('bucket_' . $bucket->getInternalId(), $fileId);
}
$date = \date('D, d M Y H:i:s', \time() + (60 * 60 * 24 * 45)) . ' GMT'; // 45 days cache
$key = \md5($fileId . $width . $height . $gravity . $quality . $borderWidth . $borderColor . $borderRadius . $opacity . $rotation . $background . $output);
$file = Authorization::skip(fn() => $dbForProject->getDocument('bucket_' . $bucket->getInternalId(), $fileId));
if ($file->isEmpty() || $file->getAttribute('bucketId') !== $bucketId) {
throw new Exception(Exception::STORAGE_FILE_NOT_FOUND);
}
if ($fileSecurity) {
$valid |= $validator->isValid($file->getRead());
if (!$valid) {
throw new Exception(Exception::USER_UNAUTHORIZED);
}
}
$path = $file->getAttribute('path');
$type = \strtolower(\pathinfo($path, PATHINFO_EXTENSION));
$algorithm = $file->getAttribute('algorithm');
@ -988,31 +992,30 @@ App::get('/v1/storage/buckets/:bucketId/files/:fileId/download')
$bucket = Authorization::skip(fn () => $dbForProject->getDocument('buckets', $bucketId));
if (
$bucket->isEmpty()
|| (!$bucket->getAttribute('enabled') && $mode !== APP_MODE_ADMIN)
) {
if ($bucket->isEmpty() || (!$bucket->getAttribute('enabled') && $mode !== APP_MODE_ADMIN)) {
throw new Exception(Exception::STORAGE_BUCKET_NOT_FOUND);
}
// Check bucket permissions when enforced
if ($bucket->getAttribute('permission') === 'bucket') {
$validator = new Authorization('read');
if (!$validator->isValid($bucket->getRead())) {
throw new Exception(Exception::USER_UNAUTHORIZED);
}
$fileSecurity = $bucket->getAttribute('fileSecurity', false);
$validator = new Authorization('read');
$valid = $validator->isValid($bucket->getRead());
if (!$valid && !$fileSecurity) {
throw new Exception(Exception::USER_UNAUTHORIZED);
}
if ($bucket->getAttribute('permission') === 'bucket') {
$file = Authorization::skip(fn () => $dbForProject->getDocument('bucket_' . $bucket->getInternalId(), $fileId));
} else {
$file = $dbForProject->getDocument('bucket_' . $bucket->getInternalId(), $fileId);
}
$file = $dbForProject->getDocument('bucket_' . $bucket->getInternalId(), $fileId);
if ($file->isEmpty() || $file->getAttribute('bucketId') !== $bucketId) {
throw new Exception(Exception::STORAGE_FILE_NOT_FOUND);
}
if ($fileSecurity) {
$valid |= $validator->isValid($file->getRead());
if (!$valid) {
throw new Exception(Exception::USER_UNAUTHORIZED);
}
}
$path = $file->getAttribute('path', '');
if (!$deviceFiles->exists($path)) {
@ -1127,41 +1130,38 @@ App::get('/v1/storage/buckets/:bucketId/files/:fileId/view')
$bucket = Authorization::skip(fn () => $dbForProject->getDocument('buckets', $bucketId));
if (
$bucket->isEmpty()
|| (!$bucket->getAttribute('enabled') && $mode !== APP_MODE_ADMIN)
) {
if ($bucket->isEmpty() || (!$bucket->getAttribute('enabled') && $mode !== APP_MODE_ADMIN)) {
throw new Exception(Exception::STORAGE_BUCKET_NOT_FOUND);
}
// Check bucket permissions when enforced
if ($bucket->getAttribute('permission') === 'bucket') {
$validator = new Authorization('read');
if (!$validator->isValid($bucket->getRead())) {
throw new Exception(Exception::USER_UNAUTHORIZED);
}
$fileSecurity = $bucket->getAttribute('fileSecurity', false);
$validator = new Authorization('read');
$valid = $validator->isValid($bucket->getRead());
if (!$valid && !$fileSecurity) {
throw new Exception(Exception::USER_UNAUTHORIZED);
}
if ($bucket->getAttribute('permission') === 'bucket') {
$file = Authorization::skip(fn () => $dbForProject->getDocument('bucket_' . $bucket->getInternalId(), $fileId));
} else {
$file = $dbForProject->getDocument('bucket_' . $bucket->getInternalId(), $fileId);
}
$mimes = Config::getParam('storage-mimes');
$file = $dbForProject->getDocument('bucket_' . $bucket->getInternalId(), $fileId);
if ($file->isEmpty() || $file->getAttribute('bucketId') !== $bucketId) {
throw new Exception(Exception::STORAGE_FILE_NOT_FOUND);
}
if ($fileSecurity) {
$valid |= !$validator->isValid($file->getRead());
if (!$valid) {
throw new Exception(Exception::USER_UNAUTHORIZED);
}
}
$mimes = Config::getParam('storage-mimes');
$path = $file->getAttribute('path', '');
if (!$deviceFiles->exists($path)) {
throw new Exception(Exception::STORAGE_FILE_NOT_FOUND, 'File not found in ' . $path);
}
$compressor = new GZIP();
$contentType = 'text/plain';
if (\in_array($file->getAttribute('mimeType'), $mimes)) {
@ -1271,71 +1271,73 @@ App::put('/v1/storage/buckets/:bucketId/files/:fileId')
->label('sdk.response.model', Response::MODEL_FILE)
->param('bucketId', null, new UID(), 'Storage bucket unique ID. You can create a new storage bucket using the Storage service [server integration](/docs/server/storage#createBucket).')
->param('fileId', '', new UID(), 'File unique ID.')
->param('read', null, new Permissions(APP_LIMIT_ARRAY_PARAMS_SIZE), 'An array of strings with read permissions. By default no user is granted with any read permissions. [learn more about permissions](https://appwrite.io/docs/permissions) and get a full list of available permissions.', true)
->param('write', null, new Permissions(APP_LIMIT_ARRAY_PARAMS_SIZE), 'An array of strings with write permissions. By default no user is granted with any write permissions. [learn more about permissions](https://appwrite.io/docs/permissions) and get a full list of available permissions.', true)
->param('permissions', null, new Permissions(APP_LIMIT_ARRAY_PARAMS_SIZE), 'An array of strings with permissions. By default no user is granted with any permissions. [learn more about permissions](/docs/permissions) and get a full list of available permissions.', true)
->inject('response')
->inject('dbForProject')
->inject('user')
->inject('usage')
->inject('mode')
->inject('events')
->action(function (string $bucketId, string $fileId, ?array $read, ?array $write, Response $response, Database $dbForProject, Document $user, Stats $usage, string $mode, Event $events) {
->action(function (string $bucketId, string $fileId, ?array $permissions, Response $response, Database $dbForProject, Document $user, Stats $usage, string $mode, Event $events) {
$bucket = Authorization::skip(fn () => $dbForProject->getDocument('buckets', $bucketId));
$read = (is_null($read) && !$user->isEmpty()) ? ['user:' . $user->getId()] : $read ?? []; // By default set read permissions for user
$write = (is_null($write) && !$user->isEmpty()) ? ['user:' . $user->getId()] : $write ?? [];
// Users can only add their roles to files, API keys and Admin users can add any
$roles = Authorization::getRoles();
if (!Auth::isAppUser($roles) && !Auth::isPrivilegedUser($roles)) {
foreach ($read as $role) {
if (!Authorization::isRole($role)) {
throw new Exception(Exception::USER_UNAUTHORIZED, 'Read permissions must be one of: (' . \implode(', ', $roles) . ')', 400);
}
}
foreach ($write as $role) {
if (!Authorization::isRole($role)) {
throw new Exception(Exception::USER_UNAUTHORIZED, 'Write permissions must be one of: (' . \implode(', ', $roles) . ')', 400);
}
}
}
if (
$bucket->isEmpty()
|| (!$bucket->getAttribute('enabled') && $mode !== APP_MODE_ADMIN)
) {
if ($bucket->isEmpty() || (!$bucket->getAttribute('enabled') && $mode !== APP_MODE_ADMIN)) {
throw new Exception(Exception::STORAGE_BUCKET_NOT_FOUND);
}
// Check bucket permissions when enforced
if ($bucket->getAttribute('permission') === 'bucket') {
$validator = new Authorization('write');
if (!$validator->isValid($bucket->getWrite())) {
throw new Exception(Exception::USER_UNAUTHORIZED);
}
$fileSecurity = $bucket->getAttributes('fileSecurity', false);
$validator = new Authorization('update');
$valid = $validator->isValid($bucket->getUpdate());
if (!$valid && !$fileSecurity) {
throw new Exception(Exception::USER_UNAUTHORIZED);
}
if ($bucket->getAttribute('permission') === 'bucket') {
$file = Authorization::skip(fn () => $dbForProject->getDocument('bucket_' . $bucket->getInternalId(), $fileId));
} else {
$file = $dbForProject->getDocument('bucket_' . $bucket->getInternalId(), $fileId);
}
$file = $dbForProject->getDocument('bucket_' . $bucket->getInternalId(), $fileId);
if ($file->isEmpty() || $file->getAttribute('bucketId') !== $bucketId) {
throw new Exception(Exception::STORAGE_FILE_NOT_FOUND);
}
$file
->setAttribute('$read', $read)
->setAttribute('$write', $write)
;
if ($bucket->getAttribute('permission') === 'bucket') {
$file = Authorization::skip(fn () => $dbForProject->updateDocument('bucket_' . $bucket->getInternalId(), $fileId, $file));
} else {
$file = $dbForProject->updateDocument('bucket_' . $bucket->getInternalId(), $fileId, $file);
if ($fileSecurity) {
$valid |= $validator->isValid($file->getUpdate());
if (!$valid) {
throw new Exception(Exception::USER_UNAUTHORIZED);
}
}
// Users can only manage their own roles, API keys and Admin users can manage any
// Users can only manage their own roles, API keys and Admin users can manage any
$roles = Authorization::getRoles();
if (!Auth::isAppUser($roles) && !Auth::isPrivilegedUser($roles)) {
foreach (Database::PERMISSIONS as $type) {
foreach ($permissions as $permission) {
$permission = Permission::parse($permission);
if ($permission->getPermission() != $type) {
continue;
}
$role = (new Role(
$permission->getRole(),
$permission->getIdentifier(),
$permission->getDimension()
))->toString();
if (!Authorization::isRole($role)) {
throw new Exception(Exception::USER_UNAUTHORIZED, 'Permissions must be one of: (' . \implode(', ', Authorization::getRoles()) . ')');
}
}
}
}
/**
* Map aggregate permissions into the multiple permissions they represent,
* accounting for the resource type given that some types not allowed specific permissions.
*/
$permissions = PermissionsProcessor::aggregate($permissions, 'file');
$file->setAttribute('$permissions', $permissions);
$file = $dbForProject->updateDocument('bucket_' . $bucket->getInternalId(), $fileId, $file);
$events
->setParam('bucketId', $bucket->getId())
->setParam('fileId', $file->getId())
@ -1375,31 +1377,30 @@ App::delete('/v1/storage/buckets/:bucketId/files/:fileId')
->action(function (string $bucketId, string $fileId, Response $response, Database $dbForProject, Event $events, Stats $usage, string $mode, Device $deviceFiles, Delete $deletes) {
$bucket = Authorization::skip(fn () => $dbForProject->getDocument('buckets', $bucketId));
if (
$bucket->isEmpty()
|| (!$bucket->getAttribute('enabled') && $mode !== APP_MODE_ADMIN)
) {
if ($bucket->isEmpty() || (!$bucket->getAttribute('enabled') && $mode !== APP_MODE_ADMIN)) {
throw new Exception(Exception::STORAGE_BUCKET_NOT_FOUND);
}
// Check bucket permissions when enforced
if ($bucket->getAttribute('permission') === 'bucket') {
$validator = new Authorization('write');
if (!$validator->isValid($bucket->getWrite())) {
throw new Exception(Exception::USER_UNAUTHORIZED);
}
$fileSecurity = $bucket->getAttributes('fileSecurity', false);
$validator = new Authorization('delete');
$valid = $validator->isValid($bucket->getDelete());
if (!$valid && !$fileSecurity) {
throw new Exception(Exception::USER_UNAUTHORIZED);
}
if ($bucket->getAttribute('permission') === 'bucket') {
$file = Authorization::skip(fn () => $dbForProject->getDocument('bucket_' . $bucket->getInternalId(), $fileId));
} else {
$file = $dbForProject->getDocument('bucket_' . $bucket->getInternalId(), $fileId);
}
$file = $dbForProject->getDocument('bucket_' . $bucket->getInternalId(), $fileId);
if ($file->isEmpty() || $file->getAttribute('bucketId') !== $bucketId) {
throw new Exception(Exception::STORAGE_FILE_NOT_FOUND);
}
if ($fileSecurity) {
$valid |= $validator->isValid($file->getDelete());
if (!$valid) {
throw new Exception(Exception::USER_UNAUTHORIZED);
}
}
$deviceDeleted = false;
if ($file->getAttribute('chunksTotal') !== $file->getAttribute('chunksUploaded')) {
$deviceDeleted = $deviceFiles->abort(
@ -1416,11 +1417,8 @@ App::delete('/v1/storage/buckets/:bucketId/files/:fileId')
->setResource('file/' . $fileId)
;
if ($bucket->getAttribute('permission') === 'bucket') {
$deleted = Authorization::skip(fn () => $dbForProject->deleteDocument('bucket_' . $bucket->getInternalId(), $fileId));
} else {
$deleted = $dbForProject->deleteDocument('bucket_' . $bucket->getInternalId(), $fileId);
}
$deleted = $dbForProject->deleteDocument('bucket_' . $bucket->getInternalId(), $fileId);
if (!$deleted) {
throw new Exception(Exception::GENERAL_SERVER_ERROR, 'Failed to remove file from DB');
}

View file

@ -21,8 +21,11 @@ use Utopia\Database\Database;
use Utopia\Database\Document;
use Utopia\Database\Exception\Authorization as AuthorizationException;
use Utopia\Database\Exception\Duplicate;
use Utopia\Database\ID;
use Utopia\Database\Permission;
use Utopia\Database\Query;
use Utopia\Database\DateTime;
use Utopia\Database\Role;
use Utopia\Database\Validator\Authorization;
use Utopia\Database\Validator\Key;
use Utopia\Database\Validator\UID;
@ -57,22 +60,31 @@ App::post('/v1/teams')
$isPrivilegedUser = Auth::isPrivilegedUser(Authorization::getRoles());
$isAppUser = Auth::isAppUser(Authorization::getRoles());
$teamId = $teamId == 'unique()' ? $dbForProject->getId() : $teamId;
$teamId = $teamId == 'unique()' ? ID::unique() : $teamId;
$team = Authorization::skip(fn() => $dbForProject->createDocument('teams', new Document([
'$id' => $teamId ,
'$read' => ['team:' . $teamId],
'$write' => ['team:' . $teamId . '/owner'],
'$id' => $teamId,
'$permissions' => [
Permission::read(Role::team($teamId)),
Permission::update(Role::team($teamId), 'owner'),
Permission::delete(Role::team($teamId), 'owner'),
],
'name' => $name,
'total' => ($isPrivilegedUser || $isAppUser) ? 0 : 1,
'search' => implode(' ', [$teamId, $name]),
])));
if (!$isPrivilegedUser && !$isAppUser) { // Don't add user on server mode
$membershipId = $dbForProject->getId();
$membershipId = ID::unique();
$membership = new Document([
'$id' => $membershipId,
'$read' => ['user:' . $user->getId(), 'team:' . $team->getId()],
'$write' => ['user:' . $user->getId(), 'team:' . $team->getId() . '/owner'],
'$permissions' => [
Permission::read(Role::user($user->getId())),
Permission::read(Role::team($team->getId())),
Permission::update(Role::user($user->getId())),
Permission::update(Role::team($team->getId(), 'owner')),
Permission::delete(Role::user($user->getId())),
Permission::delete(Role::team($team->getId(), 'owner')),
],
'userId' => $user->getId(),
'userInternalId' => $user->getInternalId(),
'teamId' => $team->getId(),
@ -321,11 +333,15 @@ App::post('/v1/teams/:teamId/memberships')
}
try {
$userId = $dbForProject->getId();
$userId = ID::unique();
$invitee = Authorization::skip(fn() => $dbForProject->createDocument('users', new Document([
'$id' => $userId,
'$read' => ['user:' . $userId, 'role:all'],
'$write' => ['user:' . $userId],
'$permissions' => [
Permission::read(Role::any()),
Permission::read(Role::user($userId)),
Permission::update(Role::user($userId)),
Permission::delete(Role::user($userId)),
],
'email' => $email,
'emailVerification' => false,
'status' => true,
@ -360,11 +376,16 @@ App::post('/v1/teams/:teamId/memberships')
$secret = Auth::tokenGenerator();
$membershipId = $dbForProject->getId();
$membershipId = ID::unique();
$membership = new Document([
'$id' => $membershipId,
'$read' => ['role:all'],
'$write' => ['user:' . $invitee->getId(), 'team:' . $team->getId() . '/owner'],
'$permissions' => [
Permission::read(Role::any()),
Permission::update(Role::user($invitee->getId())),
Permission::update(Role::team($team->getId(), 'owner')),
Permission::delete(Role::user($invitee->getId())),
Permission::delete(Role::team($team->getId(), 'owner')),
],
'userId' => $invitee->getId(),
'userInternalId' => $invitee->getInternalId(),
'teamId' => $team->getId(),
@ -688,14 +709,14 @@ App::patch('/v1/teams/:teamId/memberships/:membershipId/status')
// Log user in
Authorization::setRole('user:' . $user->getId());
Authorization::setRole(Role::user($user->getId())->toString());
$detector = new Detector($request->getUserAgent('UNKNOWN'));
$record = $geodb->get($request->getIP());
$expire = DateTime::addSeconds(new \DateTime(), Auth::TOKEN_EXPIRATION_LOGIN_LONG);
$secret = Auth::tokenGenerator();
$session = new Document(array_merge([
'$id' => $dbForProject->getId(),
'$id' => ID::unique(),
'userId' => $user->getId(),
'userInternalId' => $user->getInternalId(),
'provider' => Auth::SESSION_PROVIDER_EMAIL,
@ -708,12 +729,15 @@ App::patch('/v1/teams/:teamId/memberships/:membershipId/status')
], $detector->getOS(), $detector->getClient(), $detector->getDevice()));
$session = $dbForProject->createDocument('sessions', $session
->setAttribute('$read', ['user:' . $user->getId()])
->setAttribute('$write', ['user:' . $user->getId()]));
->setAttribute('$permissions', [
Permission::read(Role::user($user->getId())),
Permission::update(Role::user($user->getId())),
Permission::delete(Role::user($user->getId())),
]));
$dbForProject->deleteCachedDocument('users', $user->getId());
Authorization::setRole('user:' . $userId);
Authorization::setRole(Role::user($userId)->toString());
$membership = $dbForProject->updateDocument('memberships', $membership->getId(), $membership);
@ -787,6 +811,14 @@ App::delete('/v1/teams/:teamId/memberships/:membershipId')
throw new Exception(Exception::TEAM_NOT_FOUND);
}
/**
* Force document security
*/
$validator = new Authorization('delete');
if (!$validator->isValid($membership->getDelete())) {
throw new Exception(Exception::USER_UNAUTHORIZED);
}
try {
$dbForProject->deleteDocument('memberships', $membership->getId());
} catch (AuthorizationException $exception) {

View file

@ -13,6 +13,9 @@ use Appwrite\Utopia\Response;
use Utopia\App;
use Utopia\Audit\Audit;
use Utopia\Config\Config;
use Utopia\Database\ID;
use Utopia\Database\Permission;
use Utopia\Database\Role;
use Utopia\Locale\Locale;
use Appwrite\Extend\Exception;
use Utopia\Database\Document;
@ -40,12 +43,17 @@ function createUser(string $hash, mixed $hashOptions, string $userId, ?string $e
}
try {
$userId = $userId == 'unique()' ? $dbForProject->getId() : $userId;
$userId = $userId == 'unique()'
? ID::unique()
: ID::custom($userId);
$user = $dbForProject->createDocument('users', new Document([
'$id' => $userId,
'$read' => ['role:all'],
'$write' => ['user:' . $userId],
'$permissions' => [
Permission::read(Role::any()),
Permission::update(Role::user($userId)),
Permission::delete(Role::user($userId)),
],
'email' => $email,
'emailVerification' => false,
'phone' => $phone,

View file

@ -3,6 +3,7 @@
require_once __DIR__ . '/../init.php';
use Utopia\App;
use Utopia\Database\Role;
use Utopia\Locale\Locale;
use Utopia\Logger\Logger;
use Utopia\Logger\Log;
@ -248,7 +249,9 @@ App::init()
/*
* ACL Check
*/
$role = ($user->isEmpty()) ? Auth::USER_ROLE_GUEST : Auth::USER_ROLE_MEMBER;
$role = ($user->isEmpty())
? Role::guests()->toString()
: Role::users()->toString();
// Add user roles
$memberships = $user->find('teamId', $project->getAttribute('teamId', null), 'memberships');
@ -292,16 +295,15 @@ App::init()
'name' => $project->getAttribute('name', 'Untitled'),
]);
$role = Auth::USER_ROLE_APP;
$role = Auth::USER_ROLE_APPS;
$scopes = \array_merge($roles[$role]['scopes'], $key->getAttribute('scopes', []));
$expire = $key->getAttribute('expire');
if (!empty($expire) && $expire < DateTime::now()) {
if (!empty($expire) && $expire < DateTime::formatTz(DateTime::now())) {
throw new AppwriteException(AppwriteException:: PROJECT_KEY_EXPIRED);
}
Authorization::setRole('role:' . Auth::USER_ROLE_APP);
Authorization::setRole(Auth::USER_ROLE_APPS);
Authorization::setDefaultStatus(false); // Cancel security segmentation for API keys.
if (time() > $key->getAttribute('accessedAt', 0) + APP_KEY_ACCCESS) {
@ -327,7 +329,7 @@ App::init()
}
}
Authorization::setRole('role:' . $role);
Authorization::setRole($role);
foreach (Auth::getRoles($user) as $authRole) {
Authorization::setRole($authRole);

View file

@ -283,7 +283,7 @@ App::post('/v1/mock/tests/general/upload')
if ($end !== $size) {
$response->json([
'$id' => 'newfileid',
'$id' => ID::custom('newfileid'),
'chunksTotal' => $file['size'] / $chunkSize,
'chunksUploaded' => $start / $chunkSize
]);

View file

@ -5,6 +5,7 @@ use Appwrite\Utopia\Response;
use Appwrite\Utopia\View;
use Utopia\App;
use Utopia\Config\Config;
use Utopia\Database\Database;
use Utopia\Domains\Domain;
use Utopia\Database\Validator\UID;
use Utopia\Storage\Storage;
@ -289,9 +290,22 @@ App::get('/console/databases/collection')
])
;
$permissions = new View(__DIR__ . '/../../views/console/comps/permissions-matrix.phtml');
$permissions
->setParam('method', 'databases.getCollection')
->setParam('events', 'databases.updateCollection')
->setParam('data', 'project-collection')
->setParam('params', [
'collection-id' => '{{router.params.id}}',
'database-id' => '{{router.params.databaseId}}'
]);
$page = new View(__DIR__ . '/../../views/console/databases/collection.phtml');
$page->setParam('logs', $logs);
$page
->setParam('permissions', $permissions)
->setParam('logs', $logs)
;
$layout
->setParam('title', APP_NAME . ' - Database Collection')
@ -326,12 +340,28 @@ App::get('/console/databases/document')
])
;
$permissions = new View(__DIR__ . '/../../views/console/comps/permissions-matrix.phtml');
$permissions
->setParam('method', 'databases.getDocument')
->setParam('data', 'project-document')
->setParam('permissions', \array_filter(
Database::PERMISSIONS,
fn ($perm) => $perm != Database::PERMISSION_CREATE
))
->setParam('params', [
'collection-id' => '{{router.params.collection}}',
'database-id' => '{{router.params.databaseId}}',
'document-id' => '{{router.params.id}}',
]);
$page = new View(__DIR__ . '/../../views/console/databases/document.phtml');
$page
->setParam('new', false)
->setParam('database', $databaseId)
->setParam('collection', $collection)
->setParam('permissions', $permissions)
->setParam('logs', $logs)
;
@ -349,12 +379,27 @@ App::get('/console/databases/document/new')
->inject('layout')
->action(function (string $databaseId, string $collection, View $layout) {
$permissions = new View(__DIR__ . '/../../views/console/comps/permissions-matrix.phtml');
$permissions
->setParam('data', 'project-document')
->setParam('permissions', \array_filter(
Database::PERMISSIONS,
fn ($perm) => $perm != Database::PERMISSION_CREATE
))
->setParam('params', [
'collection-id' => '{{router.params.collection}}',
'database-id' => '{{router.params.databaseId}}',
'document-id' => '{{router.params.id}}',
]);
$page = new View(__DIR__ . '/../../views/console/databases/document.phtml');
$page
->setParam('new', true)
->setParam('database', $databaseId)
->setParam('collection', $collection)
->setParam('permissions', $permissions)
->setParam('logs', new View())
;
@ -392,11 +437,49 @@ App::get('/console/storage/bucket')
->inject('layout')
->action(function (string $id, Response $response, View $layout) {
$bucketPermissions = new View(__DIR__ . '/../../views/console/comps/permissions-matrix.phtml');
$bucketPermissions
->setParam('method', 'databases.getBucket')
->setParam('events', 'load,databases.updateBucket')
->setParam('data', 'project-bucket')
->setParam('form', 'bucketPermissions')
->setParam('params', [
'bucket-id' => '{{router.params.id}}',
]);
$fileCreatePermissions = new View(__DIR__ . '/../../views/console/comps/permissions-matrix.phtml');
$fileCreatePermissions
->setParam('data', 'project-document')
->setParam('form', 'fileCreatePermissions')
->setParam('permissions', \array_filter(
Database::PERMISSIONS,
fn ($perm) => $perm != Database::PERMISSION_CREATE
))
->setParam('params', [
'bucket-id' => '{{router.params.id}}',
]);
$fileUpdatePermissions = new View(__DIR__ . '/../../views/console/comps/permissions-matrix.phtml');
$fileUpdatePermissions
->setParam('method', 'storage.getFile')
->setParam('data', 'project-document')
->setParam('form', 'fileUpdatePermissions')
->setParam('permissions', \array_filter(
Database::PERMISSIONS,
fn ($perm) => $perm != Database::PERMISSION_CREATE
))
->setParam('params', [
'bucket-id' => '{{router.params.id}}',
]);
$page = new View(__DIR__ . '/../../views/console/storage/bucket.phtml');
$page
->setParam('home', App::getEnv('_APP_HOME', 0))
->setParam('fileLimit', App::getEnv('_APP_STORAGE_LIMIT', 0))
->setParam('fileLimitHuman', Storage::human(App::getEnv('_APP_STORAGE_LIMIT', 0)))
->setParam('bucketPermissions', $bucketPermissions)
->setParam('fileCreatePermissions', $fileCreatePermissions)
->setParam('fileUpdatePermissions', $fileUpdatePermissions)
;
$layout

View file

@ -10,6 +10,9 @@ use Swoole\Http\Response as SwooleResponse;
use Utopia\App;
use Utopia\CLI\Console;
use Utopia\Config\Config;
use Utopia\Database\ID;
use Utopia\Database\Permission;
use Utopia\Database\Role;
use Utopia\Database\Validator\Authorization;
use Utopia\Audit\Audit;
use Utopia\Abuse\Adapters\TimeLimit;
@ -132,7 +135,7 @@ $http->on('start', function (Server $http) use ($payloadSize, $register) {
foreach ($collection['attributes'] as $attribute) {
$attributes[] = new Document([
'$id' => $attribute['$id'],
'$id' => ID::custom($attribute['$id']),
'type' => $attribute['type'],
'size' => $attribute['size'],
'required' => $attribute['required'],
@ -146,7 +149,7 @@ $http->on('start', function (Server $http) use ($payloadSize, $register) {
foreach ($collection['indexes'] as $index) {
$indexes[] = new Document([
'$id' => $index['$id'],
'$id' => ID::custom($index['$id']),
'type' => $index['type'],
'attributes' => $index['attributes'],
'lengths' => $index['lengths'],
@ -160,17 +163,20 @@ $http->on('start', function (Server $http) use ($payloadSize, $register) {
if ($dbForConsole->getDocument('buckets', 'default')->isEmpty()) {
Console::success('[Setup] - Creating default bucket...');
$dbForConsole->createDocument('buckets', new Document([
'$id' => 'default',
'$collection' => 'buckets',
'$id' => ID::custom('default'),
'$collection' => ID::custom('buckets'),
'name' => 'Default',
'permission' => 'file',
'maximumFileSize' => (int) App::getEnv('_APP_STORAGE_LIMIT', 0), // 10MB
'allowedFileExtensions' => [],
'enabled' => true,
'encryption' => true,
'antivirus' => true,
'$read' => ['role:all'],
'$write' => ['role:all'],
'fileSecurity' => true,
'$permissions' => [
Permission::read(Role::any()),
Permission::update(Role::any()),
Permission::delete(Role::any()),
],
'search' => 'buckets Default',
]));
@ -187,7 +193,7 @@ $http->on('start', function (Server $http) use ($payloadSize, $register) {
foreach ($files['attributes'] as $attribute) {
$attributes[] = new Document([
'$id' => $attribute['$id'],
'$id' => ID::custom($attribute['$id']),
'type' => $attribute['type'],
'size' => $attribute['size'],
'required' => $attribute['required'],
@ -201,7 +207,7 @@ $http->on('start', function (Server $http) use ($payloadSize, $register) {
foreach ($files['indexes'] as $index) {
$indexes[] = new Document([
'$id' => $index['$id'],
'$id' => ID::custom($index['$id']),
'type' => $index['type'],
'attributes' => $index['attributes'],
'lengths' => $index['lengths'],
@ -252,7 +258,7 @@ $http->on('request', function (SwooleRequest $swooleRequest, SwooleResponse $swo
try {
Authorization::cleanRoles();
Authorization::setRole('role:all');
Authorization::setRole(Role::any()->toString());
$app->run($request, $response);
} catch (\Throwable $th) {

View file

@ -43,6 +43,7 @@ use Appwrite\OpenSSL\OpenSSL;
use Appwrite\Stats\Stats;
use Appwrite\Utopia\View;
use Utopia\App;
use Utopia\Database\ID;
use Utopia\Logger\Logger;
use Utopia\Config\Config;
use Utopia\Locale\Locale;
@ -729,7 +730,7 @@ App::setResource('usage', function ($register) {
App::setResource('clients', function ($request, $console, $project) {
$console->setAttribute('platforms', [ // Always allow current host
'$collection' => 'platforms',
'$collection' => ID::custom('platforms'),
'name' => 'Current Host',
'type' => 'web',
'hostname' => $request->getHostname(),
@ -805,7 +806,7 @@ App::setResource('user', function ($mode, $project, $console, $request, $respons
if (APP_MODE_ADMIN !== $mode) {
if ($project->isEmpty()) {
$user = new Document(['$id' => '', '$collection' => 'users']);
$user = new Document(['$id' => ID::custom(''), '$collection' => 'users']);
} else {
$user = $dbForProject->getDocument('users', Auth::$unique);
}
@ -817,14 +818,14 @@ App::setResource('user', function ($mode, $project, $console, $request, $respons
$user->isEmpty() // Check a document has been found in the DB
|| !Auth::sessionVerify($user->getAttribute('sessions', []), Auth::$secret)
) { // Validate user has valid login token
$user = new Document(['$id' => '', '$collection' => 'users']);
$user = new Document(['$id' => ID::custom(''), '$collection' => 'users']);
}
if (APP_MODE_ADMIN === $mode) {
if ($user->find('teamId', $project->getAttribute('teamId'), 'memberships')) {
Authorization::setDefaultStatus(false); // Cancel security segmentation for admin users.
} else {
$user = new Document(['$id' => '', '$collection' => 'users']);
$user = new Document(['$id' => ID::custom(''), '$collection' => 'users']);
}
}
@ -847,7 +848,7 @@ App::setResource('user', function ($mode, $project, $console, $request, $respons
}
if (empty($user->find('$id', $jwtSessionId, 'sessions'))) { // Match JWT to active token
$user = new Document(['$id' => '', '$collection' => 'users']);
$user = new Document(['$id' => ID::custom(''), '$collection' => 'users']);
}
}
@ -872,10 +873,10 @@ App::setResource('project', function ($dbForConsole, $request, $console) {
App::setResource('console', function () {
return new Document([
'$id' => 'console',
'$internalId' => 'console',
'$id' => ID::custom('console'),
'$internalId' => ID::custom('console'),
'name' => 'Appwrite',
'$collection' => 'projects',
'$collection' => ID::custom('projects'),
'description' => 'Appwrite core engine',
'logo' => '',
'teamId' => -1,
@ -883,7 +884,7 @@ App::setResource('console', function () {
'keys' => [],
'platforms' => [
[
'$collection' => 'platforms',
'$collection' => ID::custom('platforms'),
'name' => 'Localhost',
'type' => 'web',
'hostname' => 'localhost',

View file

@ -13,6 +13,8 @@ use Utopia\Abuse\Abuse;
use Utopia\Abuse\Adapters\TimeLimit;
use Utopia\App;
use Utopia\CLI\Console;
use Utopia\Database\ID;
use Utopia\Database\Role;
use Utopia\Logger\Log;
use Utopia\Database\Database;
use Utopia\Database\DateTime;
@ -134,7 +136,7 @@ function getDatabase(Registry &$register, string $namespace)
$server->onStart(function () use ($stats, $register, $containerId, &$statsDocument, $logError) {
sleep(5); // wait for the initial database schema to be ready
Console::success('Server started succefully');
Console::success('Server started successfully');
/**
* Create document for this worker to share stats across Containers.
@ -146,10 +148,9 @@ $server->onStart(function () use ($stats, $register, $containerId, &$statsDocume
try {
$attempts++;
$document = new Document([
'$id' => $database->getId(),
'$collection' => 'realtime',
'$read' => [],
'$write' => [],
'$id' => ID::unique(),
'$collection' => ID::custom('realtime'),
'$permissions' => [],
'container' => $containerId,
'timestamp' => DateTime::now(),
'value' => '{}'
@ -203,7 +204,7 @@ $server->onWorkerStart(function (int $workerId) use ($server, $register, $stats,
/**
* Sending current connections to project channels on the console project every 5 seconds.
*/
if ($realtime->hasSubscriber('console', 'role:member', 'project')) {
if ($realtime->hasSubscriber('console', Role::users()->toString(), 'project')) {
[$database, $returnDatabase] = getDatabase($register, '_console');
$payload = [];
@ -254,12 +255,12 @@ $server->onWorkerStart(function (int $workerId) use ($server, $register, $stats,
/**
* Sending test message for SDK E2E tests every 5 seconds.
*/
if ($realtime->hasSubscriber('console', 'role:guest', 'tests')) {
if ($realtime->hasSubscriber('console', Role::guests()->toString(), 'tests')) {
$payload = ['response' => 'WS:/v1/realtime:passed'];
$event = [
'project' => 'console',
'roles' => ['role:guest'],
'roles' => [Role::guests()->toString()],
'data' => [
'events' => ['test.event'],
'channels' => ['tests'],

View file

@ -0,0 +1,113 @@
<?php
use Utopia\Database\Database;
// Data
$method = $this->getParam('method', '');
$params = $this->getParam('params', []);
$events = $this->getParam('events', '');
$permissions = $this->getParam('permissions', Database::PERMISSIONS);
// Names
$data = $this->getParam('data', '');
$form = $this->getParam('form', 'form');
$escapedPermissions = \array_map(function ($perm) {
// Alpine won't bind to a parameter named delete :/
if ($perm == 'delete') {
return 'xdelete';
}
return $perm;
}, $permissions);
?>
<div
<?php if ($method): ?>
data-method="<?php echo $method; ?>"
<?php endif; ?>
<?php foreach ($params as $key => $value): ?>
data-param-<?php echo $key; ?>="<?php echo $value; ?>"
<?php endforeach; ?>
data-scope="sdk"
data-event="load<?php if (!empty($events)) echo ',' . $events; ?>"
data-name="<?php echo $data; ?>"
class="permissions-matrix margin-bottom-large"
x-data="permissionsMatrix"
@reset.window="permissions = rawPermissions = []">
<input
type="hidden"
name="permissions"
data-cast-from="csv"
data-cast-to="array"
data-ls-bind="{{<?php echo $data ?>.$permissions}}"
:value="rawPermissions"/>
<table data-ls-attrs="x-init=load({{<?php echo $data ?>.$permissions}})">
<thead>
<tr>
<th>Role</th>
<?php foreach ($permissions as $permission): ?>
<th><?php echo \ucfirst($permission); ?></th>
<?php endforeach; ?>
<th></th>
</tr>
</thead>
<tbody>
<template x-for="(permission, index) in permissions">
<tr>
<td>
<p x-text="permission.role"></p>
</td>
<?php foreach ($escapedPermissions as $permission): ?>
<td>
<input
type="checkbox"
name="<?php echo $permission ?>"
x-model="permission.<?php echo $permission; ?>"
@click="updatePermission(index)"/>
</td>
<?php endforeach; ?>
<td>
<span class="action" @click="removePermission(index)">
<i class="icon-trash"></i>
</span>
</td>
</tr>
</template>
<tr x-data="permissionsRow"
@addrow.window="addPermission('<?php echo $form; ?>',role,{<?php echo \implode(',', $escapedPermissions) ?>})">
<td>
<datalist id="types">
<option value="user:">
<option value="team:">
<option value="users">
<option value="guests">
<option value="any">
</datalist>
<input
required
id="<?php echo $form; ?>"
name="<?php echo $form; ?>"
form="<?php echo $form ?>"
list="types"
type="text"
x-model="role" />
</td>
<?php foreach ($escapedPermissions as $permission): ?>
<td>
<input type="checkbox" name="<?php echo $permission ?>" x-model="<?php echo $permission; ?>"/>
</td>
<?php endforeach; ?>
<td></td>
</tr>
</tbody>
<tfoot>
<tr>
<td colspan="<?php \count($permissions) + 2 ?>">
<button type="button" class="btn btn-primary margin-top-small" @click="$dispatch('addrow')">Add</button>
</td>
</tr>
</tfoot>
</table>
</div>

View file

@ -1,6 +1,7 @@
<?php
$logs = $this->getParam('logs', null);
$permissions = $this->getParam('permissions', null);
?>
<div
@ -489,8 +490,8 @@ $logs = $this->getParam('logs', null);
<div class="box margin-bottom-small">
<div class="margin-start-negative-small margin-end-negative-small margin-top-negative-small margin-bottom-negative-small">
<div class="chart background-image-no border-no margin-bottom-no">
<input
type="hidden"
<input
type="hidden"
data-ls-bind="{{usage}}"
data-forms-chart="Created=documentsCreate,Read=documentsRead,Updated=documentsUpdate,Deleted=documentsDelete"
data-show-y-axis="true"
@ -513,6 +514,8 @@ $logs = $this->getParam('logs', null);
<div class="row responsive margin-top-negative">
<div class="col span-8 margin-bottom">
<form id="<?php echo $permissions->getParam('form', 'permissions') ?>"></form>
<form
data-analytics
data-analytics-activity
@ -531,8 +534,6 @@ $logs = $this->getParam('logs', null);
data-failure-param-alert-text="Failed to update collection"
data-failure-param-alert-classname="error">
<label>&nbsp;</label>
<div class="box">
<label for="collection-name">Name</label>
<input name="name" id="collection-name" type="text" autocomplete="off" data-ls-bind="{{project-collection.name}}" data-forms-text-direction required placeholder="Collection Name" maxlength="128" />
@ -541,36 +542,25 @@ $logs = $this->getParam('logs', null);
<input name="enabled" type="hidden" data-forms-switch data-cast-to="boolean" data-ls-bind="{{project-collection.enabled}}" /> &nbsp; Enabled <span class="tooltip" data-tooltip="Mark whether collection is enabled"><i class="icon-info-circled"></i></span>
</div>
<hr class="margin-top-small" />
<label class="margin-bottom-small">Permissions</label>
<p class="text-fade text-size-small">Choose the permissions model for this collection.</p>
<p class="text-fade text-size-small">Configure the permissions for this collection.</p>
<hr class="margin-top-small" />
<div class="row">
<div class="col span-1"><input name="permission" value="collection" type="radio" class="margin-top-tiny" data-ls-bind="{{project-collection.permission}}" /></div>
<div class="col span-11">
<b>Collection Level</b>
<p class="text-fade margin-top-tiny">With Collection Level permissions, you assign permissions only once in the collection.</p>
<p class="text-fade margin-top-tiny">In this permission level, permissions assigned to collection takes the precedence and documents permissions are ignored.</p>
<div data-ls-if="{{project-collection.permission}} === 'collection'">
<label for="collection-read">Read Access <span class="text-size-small">(<a data-ls-attrs="href={{env.HOME}}/docs/permissions" target="_blank" rel="noopener">Learn more</a>)</span></label>
<input type="hidden" id="collection-read" name="read" data-forms-tags data-cast-to="json" data-ls-bind="{{project-collection.$read}}" placeholder="User ID, Team ID or Role" />
<div class="text-fade text-size-xs margin-top-negative-small margin-bottom">Add 'role:all' for wildcard access</div>
<?php echo $permissions->render(); ?>
<label for="collection-write">Write Access <span class="text-size-small">(<a data-ls-attrs="href={{env.HOME}}/docs/permissions" target="_blank" rel="noopener">Learn more</a>)</label>
<input type="hidden" id="collection-write" name="write" data-forms-tags data-cast-to="json" data-ls-bind="{{project-collection.$write}}" placeholder="User ID, Team ID or Role" />
<div class="text-fade text-size-xs margin-top-negative-small margin-bottom">Add 'role:all' for wildcard access</div>
</div>
</div>
</div>
<hr class="margin-top-no" />
<label class="margin-bottom-small">Document Security</label>
<div class="row">
<div class="col span-1"><input name="permission" value="document" type="radio" class="margin-top-no" data-ls-bind="{{project-collection.permission}}" /></div>
<div class="col span-1"><input name="documentSecurity" value="false" type="checkbox" class="margin-top-no" data-ls-bind="{{project-collection.documentSecurity}}" /></div>
<div class="col span-11">
<b>Document Level</b>
<p class="text-fade margin-top-tiny">With Document Level permissions, you have granular access control over every document. Users will only be able to access documents for which they have explicit permissions.</p>
<p class="text-fade margin-top-tiny">In this permission level, document permissions take precedence and collection permissions are ignored.</p>
<b>Enabled</b>
<p class="text-fade margin-top-tiny">With Document Security enabled, users will be able to access documents for which they have been granted <b>either</b> Document or Collection permissions.</p>
</div>
</div>
@ -592,9 +582,9 @@ $logs = $this->getParam('logs', null);
</div>
<ul class="margin-bottom-large text-fade text-size-small">
<li class="margin-bottom-small"><i class="icon-angle-circled-right margin-start-tiny margin-end-tiny"></i>
<button data-ls-ui-trigger="open-json"
class="link text-size-small"
<li class="margin-bottom-small"><i class="icon-angle-circled-right margin-start-tiny margin-end-tiny"></i>
<button data-ls-ui-trigger="open-json"
class="link text-size-small"
data-analytics
data-analytics-event="click"
data-analytics-category="console"

View file

@ -131,9 +131,8 @@
<label for="collection-name">Name</label>
<input type="text" class="full-width" id="collection-name" name="name" required autocomplete="off" maxlength="128" />
<input type="hidden" id="collection-permission" name="permission" required value="collection" />
<input type="hidden" id="collection-read" name="read" required data-cast-to="json" value="<?php echo htmlentities(json_encode([])); ?>" />
<input type="hidden" id="collection-write" name="write" required data-cast-to="json" value="<?php echo htmlentities(json_encode([])); ?>" />
<input type="hidden" id="collection-permissions" name="permissions" required data-cast-to="json" value="<?php echo htmlentities(json_encode([])); ?>" />
<input type="hidden" id="collection-documentSecurity" name="documentSecurity" required data-cast-to="boolean" value="false" />
<hr />

View file

@ -2,6 +2,7 @@
$new = $this->getParam('new', false);
$logs = $this->getParam('logs', null);
$permissions = $this->getParam('permissions', null);
?>
<div
@ -52,6 +53,8 @@ $logs = $this->getParam('logs', null);
<div class="row responsive">
<div class="col span-8 margin-bottom">
<form id="<?php echo $permissions->getParam('form', 'permissions') ?>"></form>
<form
data-analytics
data-analytics-activity
@ -333,19 +336,13 @@ $logs = $this->getParam('logs', null);
</ul>
</fieldset>
<div class="toggle margin-bottom" data-ls-ui-open data-button-aria="Open Permissions">
<div class="toggle margin-bottom" data-ls-if="{{project-collection.documentSecurity}}" data-ls-ui-open data-button-aria="Open Permissions">
<i class="icon-plus pull-end margin-top-tiny"></i>
<i class="icon-minus pull-end margin-top-tiny"></i>
<h3 class="margin-bottom-large">Permissions</h3>
<label for="collection-read">Read Access <span class="text-size-small">(<a data-ls-attrs="href={{env.HOME}}/docs/permissions" target="_blank" rel="noopener">Learn more</a>)</span></label>
<input type="hidden" id="collection-read" name="read" data-forms-tags data-cast-to="json" data-ls-bind="{{project-document.$read}}" placeholder="User ID, Team ID or Role" />
<div class="text-fade text-size-xs margin-top-negative-small margin-bottom">Add 'role:all' for wildcard access</div>
<label for="collection-write">Write Access <span class="text-size-small">(<a data-ls-attrs="href={{env.HOME}}/docs/permissions" target="_blank" rel="noopener">Learn more</a>)</label>
<input type="hidden" id="collection-write" name="write" data-forms-tags data-cast-to="json" data-ls-bind="{{project-document.$write}}" placeholder="User ID, Team ID or Role" />
<div class="text-fade text-size-xs margin-top-negative-small margin-bottom">Add 'role:all' for wildcard access</div>
<?php echo $permissions->render() ?>
</div>
<button data-ls-if="({{project-document.$id}})">Update</button>

View file

@ -552,7 +552,7 @@ sort($patterns);
<label for="execute">Execute Access <span class="tooltip small" data-tooltip="Choose who can execute this function using the client API."><i class="icon-info-circled"></i></span> <span class="text-size-small">(<a data-ls-attrs="href={{env.HOME}}/docs/permissions" target="_blank" rel="noopener">Learn more</a>)</span></label>
<input type="hidden" id="execute" name="execute" data-forms-tags data-cast-to="json" data-ls-bind="{{project-function.execute}}" placeholder="User ID, Team ID or Role" />
<div class="text-fade text-size-xs margin-top-negative-small margin-bottom">Add 'role:all' for wildcard access</div>
<div class="text-fade text-size-xs margin-top-negative-small margin-bottom">Add 'any' for wildcard access</div>
<label for="timeout">Timeout (seconds) <span class="tooltip small" data-tooltip="Limit the execution time of your function."><i class="icon-info-circled"></i></span></label>
<input name="timeout" id="function-timeout" type="number" autocomplete="off" data-ls-bind="{{project-function.timeout}}" min="1" max="<?php echo $this->escape($timeout); ?>" data-cast-to="integer" />

View file

@ -57,7 +57,7 @@ $smtpEnabled = $this->getParam('smtpEnabled', false);
<label for="logo">Project Logo</label>
<div class="text-align-center clear">
<input type="hidden" name="logo" data-ls-bind="{{console-project.logo}}" data-read="<?php echo $this->escape(json_encode(['role:all'])); ?>" data-write="<?php echo $this->escape(json_encode(['team:{{console-project.teamId}}'])); ?>" data-accept="image/*" data-forms-upload="" data-label-button="Upload" data-preview-alt="Project Logo" data-scope="console" data-default="">
<input type="hidden" name="logo" data-ls-bind="{{console-project.logo}}" data-read="<?php echo $this->escape(json_encode(['any'])); ?>" data-write="<?php echo $this->escape(json_encode(['team:{{console-project.teamId}}'])); ?>" data-accept="image/*" data-forms-upload="" data-label-button="Upload" data-preview-alt="Project Logo" data-scope="console" data-default="">
</div>
<hr />

View file

@ -2,6 +2,9 @@
$home = $this->getParam('home', '');
$fileLimit = $this->getParam('fileLimit', 0);
$fileLimitHuman = $this->getParam('fileLimitHuman', 0);
$bucketPermissions = $this->getParam('bucketPermissions', null);
$fileCreatePermissions = $this->getParam('fileCreatePermissions', null);
$fileUpdatePermissions = $this->getParam('fileUpdatePermissions', null);
?>
<div
@ -34,6 +37,11 @@ $fileLimitHuman = $this->getParam('fileLimitHuman', 0);
</div>
<div class="zone xl">
<!-- Required for permission input validation -->
<form id="<?php echo $bucketPermissions->getParam('form') ?>"></form>
<form id="<?php echo $fileCreatePermissions->getParam('form') ?>"></form>
<form id="<?php echo $fileUpdatePermissions->getParam('form') ?>"></form>
<ul class="phases clear" data-ui-phases data-selected="{{router.params.tab}}">
<li data-state="/console/storage/bucket?id={{router.params.id}}&project={{router.params.project}}">
<h2 class="margin-bottom">Files</h2>
@ -131,13 +139,14 @@ $fileLimitHuman = $this->getParam('fileLimitHuman', 0);
</div>
<input type="hidden" data-ls-attrs="id=file-bucketId-{{file.$id}}" name="bucketId" data-ls-bind="{{file.bucketId}}">
<label for="file-read">Read Access (<a data-ls-attrs="href={{env.HOME}}/docs/permissions" target="_blank" rel="noopener">Learn more</a>)</label>
<input type="hidden" data-ls-attrs="id=file-read-{{file.$id}}" name="read" data-forms-tags data-cast-to="json" data-ls-bind="{{file.$read}}" placeholder="User ID, Team ID or Role" />
<div class="text-fade text-size-xs margin-top-negative-small margin-bottom">Add 'role:all' for wildcard access</div>
<div class="toggle margin-bottom" data-ls-if="{{project-bucket.fileSecurity}}" data-ls-ui-open data-button-aria="Open Permissions">
<i class="icon-plus pull-end margin-top-tiny"></i>
<i class="icon-minus pull-end margin-top-tiny"></i>
<label for="file-write">Write Access (<a data-ls-attrs="href={{env.HOME}}/docs/permissions" target="_blank" rel="noopener">Learn more</a>)</label>
<input type="hidden" data-ls-attrs="id=file-write-{{file.$id}}" name="write" data-forms-tags data-cast-to="json" data-ls-bind="{{file.$write}}" placeholder="User ID, Team ID or Role" />
<div class="text-fade text-size-xs margin-top-negative-small margin-bottom">Add 'role:all' for wildcard access</div>
<h3 class="margin-bottom-large">Permissions</h3>
<?php echo $fileUpdatePermissions->render(); ?>
</div>
</form>
<form class="strip"
@ -270,8 +279,7 @@ $fileLimitHuman = $this->getParam('fileLimitHuman', 0);
data-analytics-category="console"
data-analytics-label="Create Storage File"
x-data
@submit.prevent="$store.uploader.uploadFile($event.target)"
>
@submit.prevent="$store.uploader.uploadFile($event.target)">
<input type="hidden" name="bucketId" id="files-bucketId" data-ls-bind="{{router.params.id}}">
<label for="fileId">File ID</label>
@ -285,18 +293,19 @@ $fileLimitHuman = $this->getParam('fileLimitHuman', 0);
name="fileId"
id="fileId" />
<label for="file-read">File</label>
<label for="file">File</label>
<input type="file" name="file" id="file-file" size="1" required>
<div class="text-fade text-size-xs margin-top-negative-small margin-bottom">(Max file size allowed: <?php echo $fileLimitHuman; ?>)</div>
<label for="file-read">Read Access (<a data-ls-attrs="href={{env.HOME}}/docs/permissions" target="_blank" rel="noopener">Learn more</a>)</label>
<input type="hidden" id="file-read" name="read" data-forms-tags data-cast-to="json" value="<?php echo htmlentities(json_encode(['role:all'])); ?>" placeholder="User ID, Team ID or Role" />
<div class="text-fade text-size-xs margin-top-negative-small margin-bottom">Add 'role:all' for wildcard access</div>
<div class="toggle margin-bottom" data-ls-if="{{project-bucket.fileSecurity}}" data-ls-ui-open data-button-aria="Open Permissions">
<i class="icon-plus pull-end margin-top-tiny"></i>
<i class="icon-minus pull-end margin-top-tiny"></i>
<label for="file-write">Write Access (<a data-ls-attrs="href={{env.HOME}}/docs/permissions" target="_blank" rel="noopener">Learn more</a>)</label>
<input type="hidden" id="file-write" name="write" data-forms-tags data-cast-to="json" value="" placeholder="User ID, Team ID or Role" />
<div class="text-fade text-size-xs margin-top-negative-small margin-bottom">Add 'role:all' for wildcard access</div>
<h3 class="margin-bottom-large">Permissions</h3>
<?php echo $fileCreatePermissions->render() ?>
</div>
<footer>
<button type="submit">Create</button> &nbsp; <button data-ui-modal-close="" type="button" class="reverse">Cancel</button>
@ -381,6 +390,7 @@ $fileLimitHuman = $this->getParam('fileLimitHuman', 0);
<div class="row responsive margin-top-negative">
<div class="col span-8 margin-bottom">
<form
data-analytics
data-analytics-activity
@ -426,35 +436,21 @@ $fileLimitHuman = $this->getParam('fileLimitHuman', 0);
<label class="margin-bottom-small">Permissions</label>
<p class="text-fade text-size-small">Choose the permissions model for this bucket.</p>
<p class="text-fade text-size-small">Configure the permissions for this bucket.</p>
<hr class="margin-top-small" />
<div class="row">
<div class="col span-1"><input name="permission" value="bucket" type="radio" class="margin-top-tiny" data-ls-bind="{{project-bucket.permission}}" /></div>
<div class="col span-11">
<b>Bucket Level</b>
<p class="text-fade margin-top-tiny">With Bucket Level permissions, you assign permissions only once in the bucket.</p>
<p class="text-fade margin-top-tiny">In this permission level permissions assigned to bucket takes the precedence and file permissions are ignored</p>
<div data-ls-if="{{project-bucket.permission}} == 'bucket'">
<?php echo $bucketPermissions->render(); ?>
<label for="bucket-read">Read Access <span class="text-size-small">(<a data-ls-attrs="href={{env.HOME}}/docs/permissions" target="_blank" rel="noopener">Learn more</a>)</span></label>
<input type="hidden" id="bucket-read" name="read" data-forms-tags data-cast-to="json" data-ls-bind="{{project-bucket.$read}}" placeholder="User ID, Team ID or Role" />
<div class="text-fade text-size-xs margin-top-negative-small margin-bottom">Add 'role:all' for wildcard access</div>
<hr class="margin-top-no" />
<label for="bucket-write">Write Access <span class="text-size-small">(<a data-ls-attrs="href={{env.HOME}}/docs/permissions" target="_blank" rel="noopener">Learn more</a>)</label>
<input type="hidden" id="bucket-write" name="write" data-forms-tags data-cast-to="json" data-ls-bind="{{project-bucket.$write}}" placeholder="User ID, Team ID or Role" />
<div class="text-fade text-size-xs margin-top-negative-small margin-bottom">Add 'role:all' for wildcard access</div>
</div>
</div>
</div>
<label class="margin-bottom-small">File Security</label>
<div class="row">
<div class="col span-1"><input name="permission" value="file" type="radio" class="margin-top-no" data-ls-bind="{{project-bucket.permission}}" /></div>
<div class="col span-1"><input name="fileSecurity" value="false" type="checkbox" class="margin-top-no" data-ls-bind="{{project-bucket.fileSecurity}}" /></div>
<div class="col span-11">
<b>File Level</b>
<p class="text-fade margin-top-tiny">With File Level permissions, you have granular access control over every file. Users will only be able to access files for which they have explicit permissions.</p>
<p class="text-fade margin-top-tiny">In this permission level file permissions take precedence and bucket permissions are ignored.</p>
<b>Enabled</b>
<p class="text-fade margin-top-tiny">With File Security enabled, users will be able to access files for which they have been granted <b>either</b> File or Bucket permissions.</p>
</div>
</div>

View file

@ -108,9 +108,8 @@
<label for="bucket-name">Name</label>
<input type="text" class="full-width" id="bucket-name" name="name" required autocomplete="off" maxlength="128" />
<input type="hidden" id="bucket-permission" name="permission" required value="bucket" />
<input type="hidden" id="bucket-read" name="read" required data-cast-to="json" value="<?php echo htmlentities(json_encode([])); ?>" />
<input type="hidden" id="bucket-write" name="write" required data-cast-to="json" value="<?php echo htmlentities(json_encode([])); ?>" />
<input type="hidden" id="bucket-permissions" name="permissions" required data-cast-to="json" value="<?php echo htmlentities(json_encode([])); ?>" />
<input type="hidden" id="bucket-fileSecurity" name="fileSecurity" required value="false" data-cast-to="boolean" />
<hr />

View file

@ -329,8 +329,8 @@ $smtpEnabled = $this->getParam('smtpEnabled', false);
</li>
<li data-state="/console/users/providers?project={{router.params.project}}">
<p data-ls-if="{{console-project.authLimit}} == 0" class="text-fade text-size-small margin-bottom pull-end">Unlimited Users <span class="link" data-ls-ui-trigger="project-update-auth-users-limit">Set Limit</a></p>
<p data-ls-if="{{console-project.authLimit}} != 0" class="text-fade text-size-small margin-bottom pull-end"><span data-ls-bind="{{console-project.authLimit|statsTotal}}"></span> Users allowed <span class="link" data-ls-ui-trigger="project-update-auth-users-limit">Change Limit</a></p>
<p data-ls-if="{{console-project.authLimit}} == 0" class="text-fade text-size-small margin-bottom pull-end">Unlimited Users <span class="link" data-ls-ui-trigger="project-update-auth-users-limit">Set Limit</span></p>
<p data-ls-if="{{console-project.authLimit}} != 0" class="text-fade text-size-small margin-bottom pull-end"><span data-ls-bind="{{console-project.authLimit|statsTotal}}"></span> Users allowed <span class="link" data-ls-ui-trigger="project-update-auth-users-limit">Change Limit</span></p>
<h2>Settings</h2>

View file

@ -10,6 +10,7 @@ use Utopia\Database\Database;
use Utopia\Database\DateTime;
use Utopia\App;
use Utopia\CLI\Console;
use Utopia\Database\ID;
use Utopia\Storage\Storage;
use Utopia\Database\Document;
use Utopia\Config\Config;
@ -78,11 +79,10 @@ class BuildsV1 extends Worker
$buildId = $deployment->getAttribute('buildId', '');
$startTime = DateTime::now();
if (empty($buildId)) {
$buildId = $dbForProject->getId();
$buildId = ID::unique();
$build = $dbForProject->createDocument('builds', new Document([
'$id' => $buildId,
'$read' => [],
'$write' => [],
'$permissions' => [],
'startTime' => $startTime,
'deploymentId' => $deployment->getId(),
'status' => 'processing',

View file

@ -14,7 +14,10 @@ use Utopia\Config\Config;
use Utopia\Database\Database;
use Utopia\Database\DateTime;
use Utopia\Database\Document;
use Utopia\Database\ID;
use Utopia\Database\Permission;
use Utopia\Database\Query;
use Utopia\Database\Role;
require_once __DIR__ . '/../init.php';
@ -234,11 +237,10 @@ class FunctionsV1 extends Worker
/** Create execution or update execution status */
$execution = $dbForProject->getDocument('executions', $executionId ?? '');
if ($execution->isEmpty()) {
$executionId = $dbForProject->getId();
$executionId = ID::unique();
$execution = $dbForProject->createDocument('executions', new Document([
'$id' => $executionId,
'$read' => $user->isEmpty() ? [] : ['user:' . $user->getId()],
'$write' => [],
'$permissions' => $user->isEmpty() ? [] : [Permission::read(Role::user($user->getId()))],
'functionId' => $functionId,
'deploymentId' => $deploymentId,
'trigger' => $trigger,

View file

@ -45,13 +45,13 @@
"appwrite/php-runtimes": "0.11.*",
"utopia-php/framework": "0.21.*",
"utopia-php/logger": "0.3.*",
"utopia-php/abuse": "0.9.*",
"utopia-php/abuse": "0.10.*",
"utopia-php/analytics": "0.2.*",
"utopia-php/audit": "0.10.*",
"utopia-php/audit": "0.11.*",
"utopia-php/cache": "0.6.*",
"utopia-php/cli": "0.13.*",
"utopia-php/config": "0.2.*",
"utopia-php/database": "0.20.*",
"utopia-php/database": "0.22.*",
"utopia-php/locale": "0.4.*",
"utopia-php/registry": "0.5.*",
"utopia-php/preloader": "0.2.*",
@ -74,22 +74,11 @@
{
"url": "https://github.com/appwrite/runtimes.git",
"type": "git"
},
{
"url": "https://github.com/utopia-php/database.git",
"type": "git"
},
{
"url": "https://github.com/utopia-php/abuse.git",
"type": "git"
},
{
"url": "https://github.com/utopia-php/audit.git",
"type": "git"
}
],
"require-dev": {
"appwrite/sdk-generator": "dev-feat-new-headers",
"ext-fileinfo": "*",
"phpunit/phpunit": "9.5.20",
"squizlabs/php_codesniffer": "^3.6",
"swoole/ide-helper": "4.8.9",
@ -103,4 +92,4 @@
"php": "8.0"
}
}
}
}

78
composer.lock generated
View file

@ -1733,17 +1733,23 @@
},
{
"name": "utopia-php/abuse",
"version": "0.9.0",
"version": "0.10.0",
"source": {
"type": "git",
"url": "https://github.com/utopia-php/abuse.git",
"reference": "34156bb5292d704bb8bc8141bb5151126ed4830a"
"reference": "b5beadce6581291e4385b0cc86f1be2a79bb2ef0"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/utopia-php/abuse/zipball/b5beadce6581291e4385b0cc86f1be2a79bb2ef0",
"reference": "b5beadce6581291e4385b0cc86f1be2a79bb2ef0",
"shasum": ""
},
"require": {
"ext-curl": "*",
"ext-pdo": "*",
"php": ">=8.0",
"utopia-php/database": "0.20.0"
"utopia-php/database": "0.22.0"
},
"require-dev": {
"phpunit/phpunit": "^9.4",
@ -1755,6 +1761,7 @@
"Utopia\\Abuse\\": "src/Abuse"
}
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
@ -1766,13 +1773,17 @@
],
"description": "A simple abuse library to manage application usage limits",
"keywords": [
"abuse",
"Abuse",
"framework",
"php",
"upf",
"utopia"
],
"time": "2022-08-15T07:35:56+00:00"
"support": {
"issues": "https://github.com/utopia-php/abuse/issues",
"source": "https://github.com/utopia-php/abuse/tree/0.10.0"
},
"time": "2022-08-17T14:31:54+00:00"
},
{
"name": "utopia-php/analytics",
@ -1831,16 +1842,22 @@
},
{
"name": "utopia-php/audit",
"version": "0.10.0",
"version": "0.11.0",
"source": {
"type": "git",
"url": "https://github.com/utopia-php/audit.git",
"reference": "458da3e60ea222bf9791f4891591d7f2ee16e4bb"
"reference": "a06f784f8e8b69bcae4f1a5bca58d41bda76c250"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/utopia-php/audit/zipball/a06f784f8e8b69bcae4f1a5bca58d41bda76c250",
"reference": "a06f784f8e8b69bcae4f1a5bca58d41bda76c250",
"shasum": ""
},
"require": {
"ext-pdo": "*",
"php": ">=8.0",
"utopia-php/database": "0.20.0"
"utopia-php/database": "0.22.0"
},
"require-dev": {
"phpunit/phpunit": "^9.3",
@ -1852,6 +1869,7 @@
"Utopia\\Audit\\": "src/Audit"
}
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
@ -1863,13 +1881,17 @@
],
"description": "A simple audit library to manage application users logs",
"keywords": [
"audit",
"Audit",
"framework",
"php",
"upf",
"utopia"
],
"time": "2022-08-14T19:59:21+00:00"
"support": {
"issues": "https://github.com/utopia-php/audit/issues",
"source": "https://github.com/utopia-php/audit/tree/0.11.0"
},
"time": "2022-08-17T15:08:58+00:00"
},
{
"name": "utopia-php/cache",
@ -2030,11 +2052,17 @@
},
{
"name": "utopia-php/database",
"version": "0.20.0",
"version": "0.22.0",
"source": {
"type": "git",
"url": "https://github.com/utopia-php/database.git",
"reference": "cd89b41564223cddf7d87a41bbaf736a0c89f327"
"reference": "22c45ae83612e907203b7571cd8e3115ae3ae4c5"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/utopia-php/database/zipball/22c45ae83612e907203b7571cd8e3115ae3ae4c5",
"reference": "22c45ae83612e907203b7571cd8e3115ae3ae4c5",
"shasum": ""
},
"require": {
"ext-mongodb": "*",
@ -2058,11 +2086,7 @@
"Utopia\\Database\\": "src/Database"
}
},
"autoload-dev": {
"psr-4": {
"Utopia\\Tests\\": "tests/Database"
}
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
@ -2084,7 +2108,11 @@
"upf",
"utopia"
],
"time": "2022-08-14T15:22:34+00:00"
"support": {
"issues": "https://github.com/utopia-php/database/issues",
"source": "https://github.com/utopia-php/database/tree/0.22.0"
},
"time": "2022-08-17T12:55:37+00:00"
},
{
"name": "utopia-php/domains",
@ -2825,6 +2853,7 @@
"brianium/paratest": "^6.4",
"phpunit/phpunit": "^9.5.21"
},
"default-branch": true,
"type": "library",
"autoload": {
"psr-4": {
@ -5319,7 +5348,14 @@
"time": "2022-08-12T06:47:24+00:00"
}
],
"aliases": [],
"aliases": [
{
"package": "appwrite/sdk-generator",
"version": "9999999-dev",
"alias": "0.19.5",
"alias_normalized": "0.19.5.0"
}
],
"minimum-stability": "stable",
"stability-flags": {
"appwrite/sdk-generator": 20
@ -5341,7 +5377,9 @@
"ext-zlib": "*",
"ext-sockets": "*"
},
"platform-dev": [],
"platform-dev": {
"ext-fileinfo": "*"
},
"platform-overrides": {
"php": "8.0"
},

View file

@ -342,7 +342,7 @@
<div style="display: inline-block; font-size: 12px; font-family: Helvetica; color: rgb(0, 0, 0); line-height: 1.2; pointer-events: all; white-space: normal; overflow-wrap: normal;">
Guset
<br/>
role:guest
guests
</div>
</div>
</div>
@ -363,7 +363,7 @@
<div style="display: inline-block; font-size: 12px; font-family: Helvetica; color: rgb(0, 0, 0); line-height: 1.2; pointer-events: all; white-space: normal; overflow-wrap: normal;">
Member
<br/>
role:member
users
</div>
</div>
</div>

Before

Width:  |  Height:  |  Size: 64 KiB

After

Width:  |  Height:  |  Size: 64 KiB

View file

@ -35,6 +35,7 @@ const configApp = {
'public/scripts/app.js',
'public/scripts/upload-modal.js',
'public/scripts/events.js',
'public/scripts/permissions-matrix.js',
'public/scripts/views/service.js',

View file

@ -13,7 +13,7 @@
</extensions>
<testsuites>
<testsuite name="unit">
<directory>./tests/unit</directory>
<directory>./tests/unit/</directory>
</testsuite>
<testsuite name="e2e">
<file>./tests/e2e/Client.php</file>

View file

@ -220,29 +220,26 @@ if(typeof cursor!=='undefined'){payload['cursor']=cursor;}
if(typeof cursorDirection!=='undefined'){payload['cursorDirection']=cursorDirection;}
if(typeof orderType!=='undefined'){payload['orderType']=orderType;}
const uri=new URL(this.client.config.endpoint+path);return yield this.client.call('get',uri,{'content-type':'application/json',},payload);});}
createCollection(databaseId,collectionId,name,permission,read,write){return __awaiter(this,void 0,void 0,function*(){if(typeof databaseId==='undefined'){throw new AppwriteException('Missing required parameter: "databaseId"');}
createCollection(databaseId,collectionId,name,permissions,documentSecurity){return __awaiter(this,void 0,void 0,function*(){if(typeof databaseId==='undefined'){throw new AppwriteException('Missing required parameter: "databaseId"');}
if(typeof collectionId==='undefined'){throw new AppwriteException('Missing required parameter: "collectionId"');}
if(typeof name==='undefined'){throw new AppwriteException('Missing required parameter: "name"');}
if(typeof permission==='undefined'){throw new AppwriteException('Missing required parameter: "permission"');}
if(typeof read==='undefined'){throw new AppwriteException('Missing required parameter: "read"');}
if(typeof write==='undefined'){throw new AppwriteException('Missing required parameter: "write"');}
if(typeof permissions==='undefined'){throw new AppwriteException('Missing required parameter: "permissions"');}
if(typeof documentSecurity==='undefined'){throw new AppwriteException('Missing required parameter: "documentSecurity"');}
let path='/databases/{databaseId}/collections'.replace('{databaseId}',databaseId);let payload={};if(typeof collectionId!=='undefined'){payload['collectionId']=collectionId;}
if(typeof name!=='undefined'){payload['name']=name;}
if(typeof permission!=='undefined'){payload['permission']=permission;}
if(typeof read!=='undefined'){payload['read']=read;}
if(typeof write!=='undefined'){payload['write']=write;}
if(typeof permissions!=='undefined'){payload['permissions']=permissions;}
if(typeof documentSecurity!=='undefined'){payload['documentSecurity']=documentSecurity;}
const uri=new URL(this.client.config.endpoint+path);return yield this.client.call('post',uri,{'content-type':'application/json',},payload);});}
getCollection(databaseId,collectionId){return __awaiter(this,void 0,void 0,function*(){if(typeof databaseId==='undefined'){throw new AppwriteException('Missing required parameter: "databaseId"');}
if(typeof collectionId==='undefined'){throw new AppwriteException('Missing required parameter: "collectionId"');}
let path='/databases/{databaseId}/collections/{collectionId}'.replace('{databaseId}',databaseId).replace('{collectionId}',collectionId);let payload={};const uri=new URL(this.client.config.endpoint+path);return yield this.client.call('get',uri,{'content-type':'application/json',},payload);});}
updateCollection(databaseId,collectionId,name,permission,read,write,enabled){return __awaiter(this,void 0,void 0,function*(){if(typeof databaseId==='undefined'){throw new AppwriteException('Missing required parameter: "databaseId"');}
updateCollection(databaseId,collectionId,name,documentSecurity,permissions,enabled){return __awaiter(this,void 0,void 0,function*(){if(typeof databaseId==='undefined'){throw new AppwriteException('Missing required parameter: "databaseId"');}
if(typeof collectionId==='undefined'){throw new AppwriteException('Missing required parameter: "collectionId"');}
if(typeof name==='undefined'){throw new AppwriteException('Missing required parameter: "name"');}
if(typeof permission==='undefined'){throw new AppwriteException('Missing required parameter: "permission"');}
if(typeof documentSecurity==='undefined'){throw new AppwriteException('Missing required parameter: "documentSecurity"');}
let path='/databases/{databaseId}/collections/{collectionId}'.replace('{databaseId}',databaseId).replace('{collectionId}',collectionId);let payload={};if(typeof name!=='undefined'){payload['name']=name;}
if(typeof permission!=='undefined'){payload['permission']=permission;}
if(typeof read!=='undefined'){payload['read']=read;}
if(typeof write!=='undefined'){payload['write']=write;}
if(typeof permissions!=='undefined'){payload['permissions']=permissions;}
if(typeof documentSecurity!=='undefined'){payload['documentSecurity']=documentSecurity;}
if(typeof enabled!=='undefined'){payload['enabled']=enabled;}
const uri=new URL(this.client.config.endpoint+path);return yield this.client.call('put',uri,{'content-type':'application/json',},payload);});}
deleteCollection(databaseId,collectionId){return __awaiter(this,void 0,void 0,function*(){if(typeof databaseId==='undefined'){throw new AppwriteException('Missing required parameter: "databaseId"');}
@ -260,6 +257,15 @@ if(typeof required!=='undefined'){payload['required']=required;}
if(typeof xdefault!=='undefined'){payload['default']=xdefault;}
if(typeof array!=='undefined'){payload['array']=array;}
const uri=new URL(this.client.config.endpoint+path);return yield this.client.call('post',uri,{'content-type':'application/json',},payload);});}
createDatetimeAttribute(databaseId,collectionId,key,required,xdefault,array){return __awaiter(this,void 0,void 0,function*(){if(typeof databaseId==='undefined'){throw new AppwriteException('Missing required parameter: "databaseId"');}
if(typeof collectionId==='undefined'){throw new AppwriteException('Missing required parameter: "collectionId"');}
if(typeof key==='undefined'){throw new AppwriteException('Missing required parameter: "key"');}
if(typeof required==='undefined'){throw new AppwriteException('Missing required parameter: "required"');}
let path='/databases/{databaseId}/collections/{collectionId}/attributes/datetime'.replace('{databaseId}',databaseId).replace('{collectionId}',collectionId);let payload={};if(typeof key!=='undefined'){payload['key']=key;}
if(typeof required!=='undefined'){payload['required']=required;}
if(typeof xdefault!=='undefined'){payload['default']=xdefault;}
if(typeof array!=='undefined'){payload['array']=array;}
const uri=new URL(this.client.config.endpoint+path);return yield this.client.call('post',uri,{'content-type':'application/json',},payload);});}
createEmailAttribute(databaseId,collectionId,key,required,xdefault,array){return __awaiter(this,void 0,void 0,function*(){if(typeof databaseId==='undefined'){throw new AppwriteException('Missing required parameter: "databaseId"');}
if(typeof collectionId==='undefined'){throw new AppwriteException('Missing required parameter: "collectionId"');}
if(typeof key==='undefined'){throw new AppwriteException('Missing required parameter: "key"');}
@ -349,25 +355,23 @@ if(typeof cursorDirection!=='undefined'){payload['cursorDirection']=cursorDirect
if(typeof orderAttributes!=='undefined'){payload['orderAttributes']=orderAttributes;}
if(typeof orderTypes!=='undefined'){payload['orderTypes']=orderTypes;}
const uri=new URL(this.client.config.endpoint+path);return yield this.client.call('get',uri,{'content-type':'application/json',},payload);});}
createDocument(databaseId,collectionId,documentId,data,read,write){return __awaiter(this,void 0,void 0,function*(){if(typeof databaseId==='undefined'){throw new AppwriteException('Missing required parameter: "databaseId"');}
createDocument(databaseId,collectionId,documentId,data,permissions){return __awaiter(this,void 0,void 0,function*(){if(typeof databaseId==='undefined'){throw new AppwriteException('Missing required parameter: "databaseId"');}
if(typeof collectionId==='undefined'){throw new AppwriteException('Missing required parameter: "collectionId"');}
if(typeof documentId==='undefined'){throw new AppwriteException('Missing required parameter: "documentId"');}
if(typeof data==='undefined'){throw new AppwriteException('Missing required parameter: "data"');}
let path='/databases/{databaseId}/collections/{collectionId}/documents'.replace('{databaseId}',databaseId).replace('{collectionId}',collectionId);let payload={};if(typeof documentId!=='undefined'){payload['documentId']=documentId;}
if(typeof data!=='undefined'){payload['data']=data;}
if(typeof read!=='undefined'){payload['read']=read;}
if(typeof write!=='undefined'){payload['write']=write;}
if(typeof permissions!=='undefined'){payload['permissions']=permissions;}
const uri=new URL(this.client.config.endpoint+path);return yield this.client.call('post',uri,{'content-type':'application/json',},payload);});}
getDocument(databaseId,collectionId,documentId){return __awaiter(this,void 0,void 0,function*(){if(typeof databaseId==='undefined'){throw new AppwriteException('Missing required parameter: "databaseId"');}
if(typeof collectionId==='undefined'){throw new AppwriteException('Missing required parameter: "collectionId"');}
if(typeof documentId==='undefined'){throw new AppwriteException('Missing required parameter: "documentId"');}
let path='/databases/{databaseId}/collections/{collectionId}/documents/{documentId}'.replace('{databaseId}',databaseId).replace('{collectionId}',collectionId).replace('{documentId}',documentId);let payload={};const uri=new URL(this.client.config.endpoint+path);return yield this.client.call('get',uri,{'content-type':'application/json',},payload);});}
updateDocument(databaseId,collectionId,documentId,data,read,write){return __awaiter(this,void 0,void 0,function*(){if(typeof databaseId==='undefined'){throw new AppwriteException('Missing required parameter: "databaseId"');}
updateDocument(databaseId,collectionId,documentId,data,permissions){return __awaiter(this,void 0,void 0,function*(){if(typeof databaseId==='undefined'){throw new AppwriteException('Missing required parameter: "databaseId"');}
if(typeof collectionId==='undefined'){throw new AppwriteException('Missing required parameter: "collectionId"');}
if(typeof documentId==='undefined'){throw new AppwriteException('Missing required parameter: "documentId"');}
let path='/databases/{databaseId}/collections/{collectionId}/documents/{documentId}'.replace('{databaseId}',databaseId).replace('{collectionId}',collectionId).replace('{documentId}',documentId);let payload={};if(typeof data!=='undefined'){payload['data']=data;}
if(typeof read!=='undefined'){payload['read']=read;}
if(typeof write!=='undefined'){payload['write']=write;}
if(typeof permissions!=='undefined'){payload['permissions']=permissions;}
const uri=new URL(this.client.config.endpoint+path);return yield this.client.call('patch',uri,{'content-type':'application/json',},payload);});}
deleteDocument(databaseId,collectionId,documentId){return __awaiter(this,void 0,void 0,function*(){if(typeof databaseId==='undefined'){throw new AppwriteException('Missing required parameter: "databaseId"');}
if(typeof collectionId==='undefined'){throw new AppwriteException('Missing required parameter: "collectionId"');}
@ -696,14 +700,13 @@ if(typeof cursor!=='undefined'){payload['cursor']=cursor;}
if(typeof cursorDirection!=='undefined'){payload['cursorDirection']=cursorDirection;}
if(typeof orderType!=='undefined'){payload['orderType']=orderType;}
const uri=new URL(this.client.config.endpoint+path);return yield this.client.call('get',uri,{'content-type':'application/json',},payload);});}
createBucket(bucketId,name,permission,read,write,enabled,maximumFileSize,allowedFileExtensions,encryption,antivirus){return __awaiter(this,void 0,void 0,function*(){if(typeof bucketId==='undefined'){throw new AppwriteException('Missing required parameter: "bucketId"');}
createBucket(bucketId,name,fileSecurity,permissions,enabled,maximumFileSize,allowedFileExtensions,encryption,antivirus){return __awaiter(this,void 0,void 0,function*(){if(typeof bucketId==='undefined'){throw new AppwriteException('Missing required parameter: "bucketId"');}
if(typeof name==='undefined'){throw new AppwriteException('Missing required parameter: "name"');}
if(typeof permission==='undefined'){throw new AppwriteException('Missing required parameter: "permission"');}
if(typeof fileSecurity==='undefined'){throw new AppwriteException('Missing required parameter: "fileSecurity"');}
let path='/storage/buckets';let payload={};if(typeof bucketId!=='undefined'){payload['bucketId']=bucketId;}
if(typeof name!=='undefined'){payload['name']=name;}
if(typeof permission!=='undefined'){payload['permission']=permission;}
if(typeof read!=='undefined'){payload['read']=read;}
if(typeof write!=='undefined'){payload['write']=write;}
if(typeof permissions!=='undefined'){payload['permissions']=permissions;}
if(typeof fileSecurity!=='undefined'){payload['fileSecurity']=fileSecurity;}
if(typeof enabled!=='undefined'){payload['enabled']=enabled;}
if(typeof maximumFileSize!=='undefined'){payload['maximumFileSize']=maximumFileSize;}
if(typeof allowedFileExtensions!=='undefined'){payload['allowedFileExtensions']=allowedFileExtensions;}
@ -712,13 +715,12 @@ if(typeof antivirus!=='undefined'){payload['antivirus']=antivirus;}
const uri=new URL(this.client.config.endpoint+path);return yield this.client.call('post',uri,{'content-type':'application/json',},payload);});}
getBucket(bucketId){return __awaiter(this,void 0,void 0,function*(){if(typeof bucketId==='undefined'){throw new AppwriteException('Missing required parameter: "bucketId"');}
let path='/storage/buckets/{bucketId}'.replace('{bucketId}',bucketId);let payload={};const uri=new URL(this.client.config.endpoint+path);return yield this.client.call('get',uri,{'content-type':'application/json',},payload);});}
updateBucket(bucketId,name,permission,read,write,enabled,maximumFileSize,allowedFileExtensions,encryption,antivirus){return __awaiter(this,void 0,void 0,function*(){if(typeof bucketId==='undefined'){throw new AppwriteException('Missing required parameter: "bucketId"');}
updateBucket(bucketId,name,fileSecurity,permissions,enabled,maximumFileSize,allowedFileExtensions,encryption,antivirus){return __awaiter(this,void 0,void 0,function*(){if(typeof bucketId==='undefined'){throw new AppwriteException('Missing required parameter: "bucketId"');}
if(typeof name==='undefined'){throw new AppwriteException('Missing required parameter: "name"');}
if(typeof permission==='undefined'){throw new AppwriteException('Missing required parameter: "permission"');}
if(typeof fileSecurity==='undefined'){throw new AppwriteException('Missing required parameter: "fileSecurity"');}
let path='/storage/buckets/{bucketId}'.replace('{bucketId}',bucketId);let payload={};if(typeof name!=='undefined'){payload['name']=name;}
if(typeof permission!=='undefined'){payload['permission']=permission;}
if(typeof read!=='undefined'){payload['read']=read;}
if(typeof write!=='undefined'){payload['write']=write;}
if(typeof permissions!=='undefined'){payload['permissions']=permissions;}
if(typeof fileSecurity!=='undefined'){payload['fileSecurity']=fileSecurity;}
if(typeof enabled!=='undefined'){payload['enabled']=enabled;}
if(typeof maximumFileSize!=='undefined'){payload['maximumFileSize']=maximumFileSize;}
if(typeof allowedFileExtensions!=='undefined'){payload['allowedFileExtensions']=allowedFileExtensions;}
@ -735,13 +737,12 @@ if(typeof cursor!=='undefined'){payload['cursor']=cursor;}
if(typeof cursorDirection!=='undefined'){payload['cursorDirection']=cursorDirection;}
if(typeof orderType!=='undefined'){payload['orderType']=orderType;}
const uri=new URL(this.client.config.endpoint+path);return yield this.client.call('get',uri,{'content-type':'application/json',},payload);});}
createFile(bucketId,fileId,file,read,write,onProgress=(progress)=>{}){return __awaiter(this,void 0,void 0,function*(){if(typeof bucketId==='undefined'){throw new AppwriteException('Missing required parameter: "bucketId"');}
createFile(bucketId,fileId,file,permissions,onProgress=(progress)=>{}){return __awaiter(this,void 0,void 0,function*(){if(typeof bucketId==='undefined'){throw new AppwriteException('Missing required parameter: "bucketId"');}
if(typeof fileId==='undefined'){throw new AppwriteException('Missing required parameter: "fileId"');}
if(typeof file==='undefined'){throw new AppwriteException('Missing required parameter: "file"');}
let path='/storage/buckets/{bucketId}/files'.replace('{bucketId}',bucketId);let payload={};if(typeof fileId!=='undefined'){payload['fileId']=fileId;}
if(typeof file!=='undefined'){payload['file']=file;}
if(typeof read!=='undefined'){payload['read']=read;}
if(typeof write!=='undefined'){payload['write']=write;}
if(typeof permissions!=='undefined'){payload['permissions']=permissions;}
const uri=new URL(this.client.config.endpoint+path);if(!(file instanceof File)){throw new AppwriteException('Parameter "file" has to be a File.');}
const size=file.size;if(size<=Service.CHUNK_SIZE){return yield this.client.call('post',uri,{'content-type':'multipart/form-data',},payload);}
let id=undefined;let response=undefined;const headers={'content-type':'multipart/form-data',};let counter=0;const totalCounters=Math.ceil(size/Service.CHUNK_SIZE);if(fileId!='unique()'){try{response=yield this.client.call('GET',new URL(this.client.config.endpoint+path+'/'+fileId),headers);counter=response.chunksUploaded;}
@ -753,10 +754,9 @@ return response;});}
getFile(bucketId,fileId){return __awaiter(this,void 0,void 0,function*(){if(typeof bucketId==='undefined'){throw new AppwriteException('Missing required parameter: "bucketId"');}
if(typeof fileId==='undefined'){throw new AppwriteException('Missing required parameter: "fileId"');}
let path='/storage/buckets/{bucketId}/files/{fileId}'.replace('{bucketId}',bucketId).replace('{fileId}',fileId);let payload={};const uri=new URL(this.client.config.endpoint+path);return yield this.client.call('get',uri,{'content-type':'application/json',},payload);});}
updateFile(bucketId,fileId,read,write){return __awaiter(this,void 0,void 0,function*(){if(typeof bucketId==='undefined'){throw new AppwriteException('Missing required parameter: "bucketId"');}
updateFile(bucketId,fileId,permissions){return __awaiter(this,void 0,void 0,function*(){if(typeof bucketId==='undefined'){throw new AppwriteException('Missing required parameter: "bucketId"');}
if(typeof fileId==='undefined'){throw new AppwriteException('Missing required parameter: "fileId"');}
let path='/storage/buckets/{bucketId}/files/{fileId}'.replace('{bucketId}',bucketId).replace('{fileId}',fileId);let payload={};if(typeof read!=='undefined'){payload['read']=read;}
if(typeof write!=='undefined'){payload['write']=write;}
let path='/storage/buckets/{bucketId}/files/{fileId}'.replace('{bucketId}',bucketId).replace('{fileId}',fileId);let payload={};if(typeof permissions!=='undefined'){payload['permissions']=permissions;}
const uri=new URL(this.client.config.endpoint+path);return yield this.client.call('put',uri,{'content-type':'application/json',},payload);});}
deleteFile(bucketId,fileId){return __awaiter(this,void 0,void 0,function*(){if(typeof bucketId==='undefined'){throw new AppwriteException('Missing required parameter: "bucketId"');}
if(typeof fileId==='undefined'){throw new AppwriteException('Missing required parameter: "fileId"');}
@ -3938,115 +3938,15 @@ params=formData;break;}
return new Promise(function(resolve,reject){let request=new XMLHttpRequest(),key;request.withCredentials=true;request.open(method,path,true);for(key in headers){if(headers.hasOwnProperty(key)){request.setRequestHeader(key,headers[key]);}}
request.onload=function(){if(4===request.readyState&&399>=request.status){let data=request.response;let contentType=this.getResponseHeader('content-type');contentType=contentType.substring(0,contentType.indexOf(';'));switch(contentType){case'application/json':data=JSON.parse(data);break;}
resolve(data);}else{reject(new Error(request.statusText));}};if(progress){request.addEventListener('progress',progress);request.upload.addEventListener('progress',progress,false);}
request.onerror=function(){reject(new Error("Network Error"));};request.send(params);})};return{'get':function(path,headers={},params={}){return call('GET',path+((params.length>0)?'?'+buildQuery(params):''),headers,{});},'post':function(path,headers={},params={},progress=null){return call('POST',path,headers,params,progress);},'put':function(path,headers={},params={},progress=null){return call('PUT',headers,params,progress);},'patch':function(path,headers={},params={},progress=null){return call('PATCH',path,headers,params,progress);},'delete':function(path,headers={},params={},progress=null){return call('DELETE',path,headers,params,progress);},'addGlobalParam':addGlobalParam,'addGlobalHeader':addGlobalHeader}}(window.document);let analytics={create:function(id,source,activity,url){return http.post('/analytics',{'content-type':'application/json'},{id:id,source:source,activity:activity,url:url,version:env.VERSION,setup:env.SETUP});},};return{analytics:analytics,};},true);})(window);(function(window){"use strict";window.ls.container.set('console',function(window){var client=new Appwrite.Client();var endpoint=window.location.origin+'/v1';client.setEndpoint(endpoint).setProject('console').setLocale(APP_ENV.LOCALE);return{client:client,account:new Appwrite.Account(client),avatars:new Appwrite.Avatars(client),databases:new Appwrite.Databases(client),functions:new Appwrite.Functions(client),health:new Appwrite.Health(client),locale:new Appwrite.Locale(client),projects:new Appwrite.Projects(client),storage:new Appwrite.Storage(client),teams:new Appwrite.Teams(client),users:new Appwrite.Users(client)}},true);})(window);(function(window){"use strict";window.ls.container.set('date',function(){function format(format,timestamp){var jsdate,f
var txtWords=['Sun','Mon','Tues','Wednes','Thurs','Fri','Satur','January','February','March','April','May','June','July','August','September','October','November','December']
var formatChr=/\\?(.?)/gi
var formatChrCb=function(t,s){return f[t]?f[t]():s}
var _pad=function(n,c){n=String(n)
while(n.length<c){n='0'+n}
return n}
f={d:function(){return _pad(f.j(),2)},D:function(){return f.l().slice(0,3)},j:function(){return jsdate.getDate()},l:function(){return txtWords[f.w()]+'day'},N:function(){return f.w()||7},S:function(){var j=f.j()
var i=j%10
if(i<=3&&parseInt((j%100)/10,10)===1){i=0}
return['st','nd','rd'][i-1]||'th'},w:function(){return jsdate.getDay()},z:function(){var a=new Date(f.Y(),f.n()-1,f.j())
var b=new Date(f.Y(),0,1)
return Math.round((a-b)/864e5)},W:function(){var a=new Date(f.Y(),f.n()-1,f.j()-f.N()+3)
var b=new Date(a.getFullYear(),0,4)
return _pad(1+Math.round((a-b)/864e5/7),2)},F:function(){return txtWords[6+f.n()]},m:function(){return _pad(f.n(),2)},M:function(){return f.F().slice(0,3)},n:function(){return jsdate.getMonth()+1},t:function(){return(new Date(f.Y(),f.n(),0)).getDate()},L:function(){var j=f.Y()
return j%4===0&j%100!==0|j%400===0},o:function(){var n=f.n()
var W=f.W()
var Y=f.Y()
return Y+(n===12&&W<9?1:n===1&&W>9?-1:0)},Y:function(){return jsdate.getFullYear()},y:function(){return f.Y().toString().slice(-2)},a:function(){return jsdate.getHours()>11?'pm':'am'},A:function(){return f.a().toUpperCase()},B:function(){var H=jsdate.getUTCHours()*36e2
var i=jsdate.getUTCMinutes()*60
var s=jsdate.getUTCSeconds()
return _pad(Math.floor((H+i+s+36e2)/86.4)%1e3,3)},g:function(){return f.G()%12||12},G:function(){return jsdate.getHours()},h:function(){return _pad(f.g(),2)},H:function(){return _pad(f.G(),2)},i:function(){return _pad(jsdate.getMinutes(),2)},s:function(){return _pad(jsdate.getSeconds(),2)},u:function(){return _pad(jsdate.getMilliseconds()*1000,6)},e:function(){var msg='Not supported (see source code of date() for timezone on how to add support)'
throw new Error(msg)},I:function(){var a=new Date(f.Y(),0)
var c=Date.UTC(f.Y(),0)
var b=new Date(f.Y(),6)
var d=Date.UTC(f.Y(),6)
return((a-c)!==(b-d))?1:0},O:function(){var tzo=jsdate.getTimezoneOffset()
var a=Math.abs(tzo)
return(tzo>0?'-':'+')+_pad(Math.floor(a/60)*100+a%60,4)},P:function(){var O=f.O()
return(O.substr(0,3)+':'+O.substr(3,2))},T:function(){return'UTC'},Z:function(){return-jsdate.getTimezoneOffset()*60},c:function(){return'Y-m-d\\TH:i:sP'.replace(formatChr,formatChrCb)},r:function(){return'D, d M Y H:i:s O'.replace(formatChr,formatChrCb)},U:function(){return jsdate/1000|0}}
var _date=function(format,timestamp){jsdate=(timestamp===undefined?new Date():(timestamp instanceof Date)?new Date(timestamp):new Date(timestamp*1000))
return format.replace(formatChr,formatChrCb)}
return _date(format,timestamp)}
function strtotime(text,now){var parsed
var match
var today
var year
var date
var days
var ranges
var len
var times
var regex
var i
var fail=false
if(!text){return fail}
text=text.replace(/^\s+|\s+$/g,'').replace(/\s{2,}/g,' ').replace(/[\t\r\n]/g,'').toLowerCase()
var pattern=new RegExp(['^(\\d{1,4})','([\\-\\.\\/:])','(\\d{1,2})','([\\-\\.\\/:])','(\\d{1,4})','(?:\\s(\\d{1,2}):(\\d{2})?:?(\\d{2})?)?','(?:\\s([A-Z]+)?)?$'].join(''))
match=text.match(pattern)
if(match&&match[2]===match[4]){if(match[1]>1901){switch(match[2]){case'-':if(match[3]>12||match[5]>31){return fail}
return new Date(match[1],parseInt(match[3],10)-1,match[5],match[6]||0,match[7]||0,match[8]||0,match[9]||0)/1000
case'.':return fail
case'/':if(match[3]>12||match[5]>31){return fail}
return new Date(match[1],parseInt(match[3],10)-1,match[5],match[6]||0,match[7]||0,match[8]||0,match[9]||0)/1000}}else if(match[5]>1901){switch(match[2]){case'-':if(match[3]>12||match[1]>31){return fail}
return new Date(match[5],parseInt(match[3],10)-1,match[1],match[6]||0,match[7]||0,match[8]||0,match[9]||0)/1000
case'.':if(match[3]>12||match[1]>31){return fail}
return new Date(match[5],parseInt(match[3],10)-1,match[1],match[6]||0,match[7]||0,match[8]||0,match[9]||0)/1000
case'/':if(match[1]>12||match[3]>31){return fail}
return new Date(match[5],parseInt(match[1],10)-1,match[3],match[6]||0,match[7]||0,match[8]||0,match[9]||0)/1000}}else{switch(match[2]){case'-':if(match[3]>12||match[5]>31||(match[1]<70&&match[1]>38)){return fail}
year=match[1]>=0&&match[1]<=38?+match[1]+2000:match[1]
return new Date(year,parseInt(match[3],10)-1,match[5],match[6]||0,match[7]||0,match[8]||0,match[9]||0)/1000
case'.':if(match[5]>=70){if(match[3]>12||match[1]>31){return fail}
return new Date(match[5],parseInt(match[3],10)-1,match[1],match[6]||0,match[7]||0,match[8]||0,match[9]||0)/1000}
if(match[5]<60&&!match[6]){if(match[1]>23||match[3]>59){return fail}
today=new Date()
return new Date(today.getFullYear(),today.getMonth(),today.getDate(),match[1]||0,match[3]||0,match[5]||0,match[9]||0)/1000}
return fail
case'/':if(match[1]>12||match[3]>31||(match[5]<70&&match[5]>38)){return fail}
year=match[5]>=0&&match[5]<=38?+match[5]+2000:match[5]
return new Date(year,parseInt(match[1],10)-1,match[3],match[6]||0,match[7]||0,match[8]||0,match[9]||0)/1000
case':':if(match[1]>23||match[3]>59||match[5]>59){return fail}
today=new Date()
return new Date(today.getFullYear(),today.getMonth(),today.getDate(),match[1]||0,match[3]||0,match[5]||0)/1000}}}
if(text==='now'){return now===null||isNaN(now)?new Date().getTime()/1000|0:now|0}
if(!isNaN(parsed=Date.parse(text))){return parsed/1000|0}
pattern=new RegExp(['^([0-9]{4}-[0-9]{2}-[0-9]{2})','[ t]','([0-9]{2}:[0-9]{2}:[0-9]{2}(\\.[0-9]+)?)','([\\+-][0-9]{2}(:[0-9]{2})?|z)'].join(''))
match=text.match(pattern)
if(match){if(match[4]==='z'){match[4]='Z'}else if(match[4].match(/^([+-][0-9]{2})$/)){match[4]=match[4]+':00'}
if(!isNaN(parsed=Date.parse(match[1]+'T'+match[2]+match[4]))){return parsed/1000|0}}
date=now?new Date(now*1000):new Date()
days={'sun':0,'mon':1,'tue':2,'wed':3,'thu':4,'fri':5,'sat':6}
ranges={'yea':'FullYear','mon':'Month','day':'Date','hou':'Hours','min':'Minutes','sec':'Seconds'}
function lastNext(type,range,modifier){var diff
var day=days[range]
if(typeof day!=='undefined'){diff=day-date.getDay()
if(diff===0){diff=7*modifier}else if(diff>0&&type==='last'){diff-=7}else if(diff<0&&type==='next'){diff+=7}
date.setDate(date.getDate()+diff)}}
function process(val){var splt=val.split(' ')
var type=splt[0]
var range=splt[1].substring(0,3)
var typeIsNumber=/\d+/.test(type)
var ago=splt[2]==='ago'
var num=(type==='last'?-1:1)*(ago?-1:1)
if(typeIsNumber){num*=parseInt(type,10)}
if(ranges.hasOwnProperty(range)&&!splt[1].match(/^mon(day|\.)?$/i)){return date['set'+ranges[range]](date['get'+ranges[range]]()+num)}
if(range==='wee'){return date.setDate(date.getDate()+(num*7))}
if(type==='next'||type==='last'){lastNext(type,range,num)}else if(!typeIsNumber){return false}
return true}
times='(years?|months?|weeks?|days?|hours?|minutes?|min|seconds?|sec'+'|sunday|sun\\.?|monday|mon\\.?|tuesday|tue\\.?|wednesday|wed\\.?'+'|thursday|thu\\.?|friday|fri\\.?|saturday|sat\\.?)'
regex='([+-]?\\d+\\s'+times+'|'+'(last|next)\\s'+times+')(\\sago)?'
match=text.match(new RegExp(regex,'gi'))
if(!match){return fail}
for(i=0,len=match.length;i<len;i++){if(!process(match[i])){return fail}}
return(date.getTime()/1000)}
return{format:format,strtotime:strtotime}}(),true);})(window);(function(window){"use strict";window.ls.container.set('env',function(){return APP_ENV;},true);})(window);(function(window){"use strict";window.ls.container.set('form',function(){function cast(value,to){if(value&&Array.isArray(value)&&to!=='array'){value=value.map(element=>cast(element,to));return value;}
request.onerror=function(){reject(new Error("Network Error"));};request.send(params);})};return{'get':function(path,headers={},params={}){return call('GET',path+((params.length>0)?'?'+buildQuery(params):''),headers,{});},'post':function(path,headers={},params={},progress=null){return call('POST',path,headers,params,progress);},'put':function(path,headers={},params={},progress=null){return call('PUT',headers,params,progress);},'patch':function(path,headers={},params={},progress=null){return call('PATCH',path,headers,params,progress);},'delete':function(path,headers={},params={},progress=null){return call('DELETE',path,headers,params,progress);},'addGlobalParam':addGlobalParam,'addGlobalHeader':addGlobalHeader}}(window.document);let analytics={create:function(id,source,activity,url){return http.post('/analytics',{'content-type':'application/json'},{id:id,source:source,activity:activity,url:url,version:env.VERSION,setup:env.SETUP});},};return{analytics:analytics,};},true);})(window);(function(window){"use strict";window.ls.container.set('console',function(window){var client=new Appwrite.Client();var endpoint=window.location.origin+'/v1';client.setEndpoint(endpoint).setProject('console').setLocale(APP_ENV.LOCALE);return{client:client,account:new Appwrite.Account(client),avatars:new Appwrite.Avatars(client),databases:new Appwrite.Databases(client),functions:new Appwrite.Functions(client),health:new Appwrite.Health(client),locale:new Appwrite.Locale(client),projects:new Appwrite.Projects(client),storage:new Appwrite.Storage(client),teams:new Appwrite.Teams(client),users:new Appwrite.Users(client)}},true);})(window);(function(window){"use strict";window.ls.container.set('date',function(){function format(format,datetime){if(!datetime){return null;}
return new Intl.DateTimeFormat('en-US',{timeZone:'UTC',hourCycle:'h24',...format}).format(new Date(datetime));}
return{format:format,}}(),true);})(window);(function(window){"use strict";window.ls.container.set('env',function(){return APP_ENV;},true);})(window);(function(window){"use strict";window.ls.container.set('form',function(){function cast(value,from,to,){if(value&&Array.isArray(value)&&to!=='array'){value=value.map(element=>cast(element,from,to));return value;}
switch(to){case'int':case'integer':value=parseInt(value);break;case'numeric':value=Number(value);break;case'float':value=parseFloat(value);break;case'string':value=value.toString();if(value.length===0){value=null;}
break;case'json':value=(value)?JSON.parse(value):[];break;case'array':value=(value&&value.constructor&&value.constructor===Array)?value:[value];break;case'array-empty':value=[];break;case'bool':case'boolean':value=(value==='false')?false:value;value=!!value;break;}
break;case'json':value=(value)?JSON.parse(value):[];break;case'array':if(value&&value.constructor&&value.constructor===Array){break;}
if(from==='csv'){if(value.length===0){value=[];}else{value=value.split(',');}}else{value=[value];}
break;case'array-empty':value=[];break;case'bool':case'boolean':value=(value==='false')?false:value;value=!!value;break;}
return value;}
function toJson(element,json){json=json||{};let name=element.getAttribute('name');let type=element.getAttribute('type');let castTo=element.getAttribute('data-cast-to');let ref=json;if(name&&'FORM'!==element.tagName){if(name.startsWith('[')){let splitName=name.split('.');if(splitName.length>1&&splitName[0].endsWith(']')){name=splitName[splitName.length-1];}}
function toJson(element,json){json=json||{};let name=element.getAttribute('name');let type=element.getAttribute('type');let castTo=element.getAttribute('data-cast-to');let castFrom=element.getAttribute('data-cast-from');let ref=json;if(name&&'FORM'!==element.tagName){if(name.startsWith('[')){let splitName=name.split('.');if(splitName.length>1&&splitName[0].endsWith(']')){name=splitName[splitName.length-1];}}
if('FIELDSET'===element.tagName){if(castTo==='object'){if(json[name]===undefined){json[name]={};}
ref=json[name];}
else{if(!Array.isArray(json[name])){json[name]=[];}
@ -4059,7 +3959,7 @@ else if('file'===type){json[name]=element.files[0];}
else if(undefined!==element.value){if((json[name]!==undefined)&&(!Array.isArray(json[name]))){json[name]=[json[name]];}
if(Array.isArray(json[name])){json[name].push(element.value);}
else{json[name]=element.value;}}
json[name]=cast(json[name],castTo);}}
json[name]=cast(json[name],castFrom,castTo);}}
for(let i=0;i<element.children.length;i++){if(Array.isArray(ref)){ref.push({});toJson(element.children[i],ref[ref.length]);}
else{toJson(element.children[i],ref);}}
return json;}
@ -4072,7 +3972,7 @@ return false;};return{isRTL:isRTL,};},true);})(window);(function(window){"use st
let size=element.dataset["size"]||80;let name=$value.name||$value||"";name=(typeof name!=='string')?'--':name;return def="/v1/avatars/initials?project=console"+"&name="+
encodeURIComponent(name)+"&width="+
size+"&height="+
size;}).add("selectedCollection",function($value,router){return $value===router.params.collectionId?"selected":"";}).add("selectedDocument",function($value,router){return $value===router.params.documentId?"selected":"";}).add("localeString",function($value){$value=parseInt($value);return!Number.isNaN($value)?$value.toLocaleString():"";}).add("date",function($value,date){return $value?date.format("Y-m-d",$value):"";}).add("dateTime",function($value,date){return $value?date.format("Y-m-d H:i",$value):"";}).add("dateText",function($value,date){return $value?date.format("d M Y",$value):"";}).add("timeSince",function($value){$value=$value*1000;let seconds=Math.floor((Date.now()-$value)/1000);let unit="second";let direction="ago";if(seconds<0){seconds=-seconds;direction="from now";}
size;}).add("selectedCollection",function($value,router){return $value===router.params.collectionId?"selected":"";}).add("selectedDocument",function($value,router){return $value===router.params.documentId?"selected":"";}).add("localeString",function($value){$value=parseInt($value);return!Number.isNaN($value)?$value.toLocaleString():"";}).add("dateTime",function($value,date){return $value?date.format({year:'numeric',month:'2-digit',day:'2-digit',hour:'2-digit',minute:'2-digit'},$value):"";}).add("date",function($value,date){return $value?date.format({year:'numeric',month:'short',day:'2-digit',},$value):"";}).add("timeSince",function($value){$value=new Date($value).getTime();let now=new Date();now.setMinutes(now.getMinutes()+now.getTimezoneOffset());let timestamp=new Date(now.toISOString()).getTime();let seconds=Math.floor((timestamp-$value)/1000);let unit="second";let direction="ago";if(seconds<0){seconds=-seconds;direction="from now";}
let value=seconds;if(seconds>=31536000){value=Math.floor(seconds/31536000);unit="year";}
else if(seconds>=86400){value=Math.floor(seconds/86400);unit="day";}
else if(seconds>=3600){value=Math.floor(seconds/3600);unit="hour";}
@ -4110,11 +4010,10 @@ if(forcePlaces!==false){rounded=Number(rounded).toFixed(forcePlaces);}
return rounded+abbr;}
window.ls.container.get("view").add({selector:"data-acl",controller:function(element,document,router,alerts){document.body.classList.remove("console");document.body.classList.remove("home");document.body.classList.add(router.getCurrent().view.scope);if(!router.getCurrent().view.project){document.body.classList.add("hide-nav");document.body.classList.remove("show-nav");}else{document.body.classList.add("show-nav");document.body.classList.remove("hide-nav");}
if("/console"===router.getCurrent().path){document.body.classList.add("index");}else{document.body.classList.remove("index");}}}).add({selector:"data-prism",controller:function(window,document,element,alerts){Prism.highlightElement(element);let copy=document.createElement("i");copy.className="icon-docs copy";copy.title="Copy to Clipboard";copy.textContent="Click Here to Copy";copy.addEventListener("click",function(){window.getSelection().removeAllRanges();let range=document.createRange();range.selectNode(element);window.getSelection().addRange(range);try{document.execCommand("copy");alerts.add({text:"Copied to clipboard",class:""},3000);}catch(err){alerts.add({text:"Failed to copy text ",class:"error"},3000);}
window.getSelection().removeAllRanges();});element.parentNode.parentNode.appendChild(copy);}});(function(window){document.addEventListener('alpine:init',()=>{Alpine.store('uploader',{_files:[],files(){return(this._files??[]).filter((file)=>!file.cancelled);},isOpen:true,init(){window.addEventListener('beforeunload',(event)=>{if(this.hasOngoingUploads()){let confirmationMessage="There are incomplete uploads, are you sure you want to leave?";event.returnValue=confirmationMessage;return confirmationMessage;}});},cancelAll(){if(this.hasOngoingUploads()?confirm("Are you sure? This will cancel and remove any ongoing uploads?"):true){this._files.forEach(file=>{if(file.completed||file.failed){this.removeFile(file.id);}else{this.updateFile(file.id,{cancelled:true});}});}},hasOngoingUploads(){let ongoing=false;this._files.some((file)=>{if(!file.completed&&!file.failed){ongoing=true;return;}});return ongoing;},toggle(){this.isOpen=!this.isOpen;},addFile(file){this._files.push(file);},updateFile(id,file){this._files=this._files.map((oldFile)=>id==oldFile.id?{...oldFile,...file}:oldFile);},removeFile(id){const file=this.getFile(id)??{};if(file.completed||file.failed){this._files=this._files.filter((file)=>file.id!==id);}else{if(confirm("Are you sure you want to cancel the upload?")){this.updateFile(id,{cancelled:true});}}},getFile(id){return this._files.find((file)=>file.id===id);},async uploadFile(target){const formData=new FormData(target);const sdk=window.ls.container.get('sdk');const bucketId=formData.get('bucketId');const file=formData.get('file');const fileId=formData.get('fileId');let id=fileId==='unique()'?performance.now():fileId;let read=formData.get('read');if(!file||!fileId){return;}
if(read){read=JSON.parse(read);}
let write=formData.get('write');if(write){write=JSON.parse(write);}
window.getSelection().removeAllRanges();});element.parentNode.parentNode.appendChild(copy);}});(function(window){document.addEventListener('alpine:init',()=>{Alpine.store('uploader',{_files:[],files(){return(this._files??[]).filter((file)=>!file.cancelled);},isOpen:true,init(){window.addEventListener('beforeunload',(event)=>{if(this.hasOngoingUploads()){let confirmationMessage="There are incomplete uploads, are you sure you want to leave?";event.returnValue=confirmationMessage;return confirmationMessage;}});},cancelAll(){if(this.hasOngoingUploads()?confirm("Are you sure? This will cancel and remove any ongoing uploads?"):true){this._files.forEach(file=>{if(file.completed||file.failed){this.removeFile(file.id);}else{this.updateFile(file.id,{cancelled:true});}});}},hasOngoingUploads(){let ongoing=false;this._files.some((file)=>{if(!file.completed&&!file.failed){ongoing=true;return;}});return ongoing;},toggle(){this.isOpen=!this.isOpen;},addFile(file){this._files.push(file);},updateFile(id,file){this._files=this._files.map((oldFile)=>id==oldFile.id?{...oldFile,...file}:oldFile);},removeFile(id){const file=this.getFile(id)??{};if(file.completed||file.failed){this._files=this._files.filter((file)=>file.id!==id);}else{if(confirm("Are you sure you want to cancel the upload?")){this.updateFile(id,{cancelled:true});}}},getFile(id){return this._files.find((file)=>file.id===id);},async uploadFile(target){const formData=new FormData(target);const sdk=window.ls.container.get('sdk');const bucketId=formData.get('bucketId');const file=formData.get('file');const fileId=formData.get('fileId');let id=fileId==='unique()'?performance.now():fileId;if(!file||!fileId){return;}
let permissions=formData.get('permissions');if(permissions){permissions=permissions.split(',');}
if(this.getFile(id)){this.updateFile(id,{name:file.name,completed:false,failed:false,cancelled:false,error:"",});}else{this.addFile({id:id,name:file.name,progress:0,completed:false,failed:false,cancelled:false,error:"",});}
target.reset();try{const response=await sdk.storage.createFile(bucketId,fileId,file,read,write,(progress)=>{this.updateFile(id,{id:progress.$id,progress:Math.round(progress.progress),error:"",});id=progress.$id;const file=this.getFile(id)??{};if(file.cancelled===true){throw'USER_CANCELLED';}});const existingFile=this.getFile(id)??{};if(existingFile.cancelled){this.updateFile(id,{id:response.$id,name:response.name,failed:false,});id=response.$id;throw'USER_CANCELLED'}else{this.updateFile(id,{id:response.$id,name:response.name,progress:100,completed:true,failed:false,});id=response.$id;}
target.reset();try{const response=await sdk.storage.createFile(bucketId,fileId,file,permissions,(progress)=>{this.updateFile(id,{id:progress.$id,progress:Math.round(progress.progress),error:"",});id=progress.$id;const file=this.getFile(id)??{};if(file.cancelled===true){throw'USER_CANCELLED';}});const existingFile=this.getFile(id)??{};if(existingFile.cancelled){this.updateFile(id,{id:response.$id,name:response.name,failed:false,});id=response.$id;throw'USER_CANCELLED'}else{this.updateFile(id,{id:response.$id,name:response.name,progress:100,completed:true,failed:false,});id=response.$id;}
document.dispatchEvent(new CustomEvent('storage.createFile'));}catch(error){if(error==='USER_CANCELLED'){await sdk.storage.deleteFile(bucketId,id);this.updateFile(id,{cancelled:false,failed:true,});this.removeFile(id);}else{this.updateFile(id,{id:id,failed:true,error:error.message??error});}
document.dispatchEvent(new CustomEvent('storage.createFile'));}}});});})(window);(function(window){document.addEventListener('alpine:init',()=>{Alpine.data('events',()=>({events:new Set(),selected:null,action:null,type:null,subType:null,subSubType:null,resource:null,resourceName:'',subResource:null,subResourceName:'',subSubResource:null,subSubResourceName:'',hasResource:false,hasSubResource:false,hasSubSubResource:false,attribute:null,hasAttribute:false,attributes:[],load(events){this.events=new Set(events);},reset(){this.hasResource=this.hasSubResource=this.hasSubSubResource=this.hasAttribute=false;this.type=this.subType=this.subResource=this.resource=this.attribute=this.selected=this.action=null;},setEvent(){this.hasResource=this.hasSubResource=this.hasSubSubResource=this.hasAttribute=this.action=false;if(!this.selected){this.reset();return;}
let[type,action]=this.selected.split('.');switch(type){case'users':if(action==='update'){this.hasAttribute=true;this.attributes=['email','name','password','status','prefs']}
@ -4124,7 +4023,14 @@ this.action=action;},showModal(modal){document.documentElement.classList.add("mo
if(this.hasSubSubResource){event+=`.${this.subSubType}.${this.subSubResource ? this.subSubResource : '*'}`;}
if(this.action){event+=`.${this.action}`;}
if(this.attribute){event+=`.${this.attribute}`;}
this.events.add(event);this.reset();},removeEvent(value){this.events.delete(value);}}));});})(window);(function(window){"use strict";window.ls.view.add({selector:"data-service",controller:function(element,view,container,form,alerts,expression,window){let action=element.dataset["service"];let service=element.dataset["name"]||null;let event=expression.parse(element.dataset["event"]);let confirm=element.dataset["confirm"]||"";let loading=element.dataset["loading"]||"";let loaderId=null;let scope=element.dataset["scope"]||"sdk";let success=element.dataset["success"]||"";let failure=element.dataset["failure"]||"";let running=false;let callbacks={hide:function(){return function(){return element.style.opacity='0';};},reset:function(){return function(){if("FORM"===element.tagName){return element.reset();}
this.events.add(event);this.reset();},removeEvent(value){this.events.delete(value);}}));});})(window);(function(window){document.addEventListener('alpine:init',()=>{Alpine.data('permissionsMatrix',()=>({permissions:[],rawPermissions:[],load(permissions){if(permissions===undefined){return;}
this.rawPermissions=permissions;permissions.map(p=>{let{type,role}=this.parsePermission(p);type=this.parseInputPermission(type);let index=-1;let existing=this.permissions.find((p,idx)=>{if(p.role===role){index=idx;return true;}})
if(existing===undefined){this.permissions.push({role,[type]:true,});}
if(index!==-1){existing[type]=true;this.permissions[index]=existing;}});},addPermission(formId,role,permissions){if(!document.getElementById(formId).reportValidity()){return;}
Object.entries(permissions).forEach(entry=>{let[type,enabled]=entry;type=this.parseOutputPermission(type);if(enabled){this.rawPermissions.push(this.buildPermission(type,role));}});this.permissions.push({role,...permissions,});this.reset();},updatePermission(index){setTimeout(()=>{const permission=this.permissions[index];Object.keys(permission).forEach(key=>{if(key==='role'){return;}
const parsedKey=this.parseOutputPermission(key);const permissionString=this.buildPermission(parsedKey,permission.role);if(permission[key]){if(!this.rawPermissions.includes(permissionString)){this.rawPermissions.push(permissionString);}}else{this.rawPermissions=this.rawPermissions.filter(p=>{return!p.includes(permissionString);});}});});},removePermission(index){let row=this.permissions.splice(index,1);if(row.length===1){this.rawPermissions=this.rawPermissions.filter(p=>!p.includes(row[0].role));}},parsePermission(permission){let parts=permission.split('(');let type=parts[0];let role=parts[1].replace(')','').replace(' ','').replaceAll('"','');return{type,role};},buildPermission(type,role){return`${type}("${role}")`},parseInputPermission(key){if(key==='delete'){return'xdelete';}
return key;},parseOutputPermission(key){if(key==='xdelete'){return'delete';}
return key;}}));Alpine.data('permissionsRow',()=>({role:'',read:false,create:false,update:false,xdelete:false,reset(){this.role='';this.read=this.create=this.update=this.xdelete=false;}}));});})(window);(function(window){"use strict";window.ls.view.add({selector:"data-service",controller:function(element,view,container,form,alerts,expression,window){let action=element.dataset["service"];let service=element.dataset["name"]||null;let event=expression.parse(element.dataset["event"]);let confirm=element.dataset["confirm"]||"";let loading=element.dataset["loading"]||"";let loaderId=null;let scope=element.dataset["scope"]||"sdk";let success=element.dataset["success"]||"";let failure=element.dataset["failure"]||"";let running=false;let callbacks={hide:function(){return function(){return element.style.opacity='0';};},reset:function(){return function(){if("FORM"===element.tagName){return element.reset();}
throw new Error("This callback is only valid for forms");};},alert:function(text,classname){return function(alerts){alerts.add({text:text,class:classname||"success"},6000);};},redirect:function(url){return function(router){if(url==="/console"){window.location=url;return;}
router.change(url||"/");};},reload:function(){return function(router){router.reload();};},state:function(keys){let updateQueryString=function(key,value,url){var re=new RegExp("([?&])"+key+"=.*?(&|#|$)(.*)","gi"),hash;if(re.test(url)){if(typeof value!=="undefined"&&value!==null){return url.replace(re,"$1"+key+"="+value+"$2$3");}else{hash=url.split("#");url=hash[0].replace(re,"$1$3").replace(/(&|\?)$/,"");if(typeof hash[1]!=="undefined"&&hash[1]!==null){url+="#"+hash[1];}
return url;}}else{if(typeof value!=="undefined"&&value!==null){var separator=url.indexOf("?")!==-1?"&":"?";hash=url.split("#");url=hash[0]+separator+key+"="+value;if(typeof hash[1]!=="undefined"&&hash[1]!==null){url+="#"+hash[1];}
@ -4170,9 +4076,9 @@ button.addEventListener("click",function(){var clone=document.createElement(elem
clone.innerHTML=template;clone.className=element.className;var input=clone.querySelector("input, select, textarea");view.render(clone);if(debug){console.log('Debug: clone: ',clone);console.log('Debug: target: ',target);}
if(target){target.appendChild(clone);}else{button.parentNode.insertBefore(clone,button);}
if(input){input.focus();}
Array.prototype.slice.call(clone.querySelectorAll("[data-remove]")).map(function(obj){obj.addEventListener("click",function(){clone.parentNode.removeChild(clone);obj.scrollIntoView({behavior:"smooth"});});});Array.prototype.slice.call(clone.querySelectorAll("[data-up]")).map(function(obj){obj.addEventListener("click",function(){if(clone.previousElementSibling){clone.parentNode.insertBefore(clone,clone.previousElementSibling);obj.scrollIntoView({behavior:"smooth"});}});});Array.prototype.slice.call(clone.querySelectorAll("[data-down]")).map(function(obj){obj.addEventListener("click",function(){if(clone.nextElementSibling){clone.parentNode.insertBefore(clone.nextElementSibling,clone);obj.scrollIntoView({behavior:"smooth"});}});});});element.parentNode.insertBefore(button,element.nextSibling);element.parentNode.removeChild(element);button.form.addEventListener('reset',function(event){target.innerHTML='';if(first){button.click();}});if(first){button.click();}}});})(window);(function(window){"use strict";window.ls.container.get("view").add({selector:"data-forms-add",repeat:false,controller:function(element,view,container,document){for(var i=0;i<element.children.length;i++){let button=document.createElement("button");let template=element.children[i].cloneNode(true);let as=element.getAttribute('data-ls-as');let counter=0;button.type="button";button.innerText="Add";button.classList.add("reverse");button.classList.add("margin-end-small");button.addEventListener('click',function(){container.addNamespace(as,'new-'+counter++);console.log(container.namespaces,container.get(as),as);container.set(as,null,true,true);let child=template.cloneNode(true);view.render(child);element.appendChild(child);element.style.visibility='visible';let inputs=child.querySelectorAll('input,textarea');for(let index=0;index<inputs.length;++index){if(inputs[index].type!=='hidden'){inputs[index].focus();break;}}});element.after(button);}}});})(window);(function(window){"use strict";window.ls.container.get("view").add({selector:"data-forms-chart",controller:function(element,container,date,document){let wrapper=document.createElement("div");let child=document.createElement("canvas");let sources=element.getAttribute('data-forms-chart');let width=element.getAttribute('data-width')||500;let height=element.getAttribute('data-height')||175;let showXAxis=element.getAttribute('data-show-x-axis')||false;let showYAxis=element.getAttribute('data-show-y-axis')||false;let colors=(element.getAttribute('data-colors')||'blue,green,orange,red').split(',');let themes={'blue':'#29b5d9','green':'#4eb55b','orange':'#fba233','red':'#dc3232','create':'#00b680','read':'#009cde','update':'#696fd7','delete':'#da5d95',};let range={'24h':'H:i','7d':'d F Y','30d':'d F Y','90d':'d F Y'}
Array.prototype.slice.call(clone.querySelectorAll("[data-remove]")).map(function(obj){obj.addEventListener("click",function(){clone.parentNode.removeChild(clone);obj.scrollIntoView({behavior:"smooth"});});});Array.prototype.slice.call(clone.querySelectorAll("[data-up]")).map(function(obj){obj.addEventListener("click",function(){if(clone.previousElementSibling){clone.parentNode.insertBefore(clone,clone.previousElementSibling);obj.scrollIntoView({behavior:"smooth"});}});});Array.prototype.slice.call(clone.querySelectorAll("[data-down]")).map(function(obj){obj.addEventListener("click",function(){if(clone.nextElementSibling){clone.parentNode.insertBefore(clone.nextElementSibling,clone);obj.scrollIntoView({behavior:"smooth"});}});});});element.parentNode.insertBefore(button,element.nextSibling);element.parentNode.removeChild(element);button.form.addEventListener('reset',function(event){target.innerHTML='';if(first){button.click();}});if(first){button.click();}}});})(window);(function(window){"use strict";window.ls.container.get("view").add({selector:"data-forms-add",repeat:false,controller:function(element,view,container,document){for(var i=0;i<element.children.length;i++){let button=document.createElement("button");let template=element.children[i].cloneNode(true);let as=element.getAttribute('data-ls-as');let counter=0;button.type="button";button.innerText="Add";button.classList.add("reverse");button.classList.add("margin-end-small");button.addEventListener('click',function(){container.addNamespace(as,'new-'+counter++);console.log(container.namespaces,container.get(as),as);container.set(as,null,true,true);let child=template.cloneNode(true);view.render(child);element.appendChild(child);element.style.visibility='visible';let inputs=child.querySelectorAll('input,textarea');for(let index=0;index<inputs.length;++index){if(inputs[index].type!=='hidden'){inputs[index].focus();break;}}});element.after(button);}}});})(window);(function(window){"use strict";window.ls.container.get("view").add({selector:"data-forms-chart",controller:function(element,container,date,document){let wrapper=document.createElement("div");let child=document.createElement("canvas");let sources=element.getAttribute('data-forms-chart');let width=element.getAttribute('data-width')||500;let height=element.getAttribute('data-height')||175;let showXAxis=element.getAttribute('data-show-x-axis')||false;let showYAxis=element.getAttribute('data-show-y-axis')||false;let colors=(element.getAttribute('data-colors')||'blue,green,orange,red').split(',');let themes={'blue':'#29b5d9','green':'#4eb55b','orange':'#fba233','red':'#dc3232','create':'#00b680','read':'#009cde','update':'#696fd7','delete':'#da5d95',};let range={'24h':{hour:'2-digit',minute:'2-digit'},'7d':{year:'numeric',month:'short',day:'2-digit',},'30d':{year:'numeric',month:'short',day:'2-digit',},'90d':{year:'numeric',month:'short',day:'2-digit',}}
let ticksCount=5;element.parentNode.insertBefore(wrapper,element.nextSibling);wrapper.classList.add('content');child.width=width;child.height=height;sources=sources.split(',');wrapper.appendChild(child);let chart=null;let check=function(){let config={type:"line",data:{labels:[],datasets:[]},options:{animation:{duration:0},responsive:true,hover:{mode:"nearest",intersect:false},scales:{x:{display:showXAxis},y:{display:showYAxis,min:0,ticks:{count:ticksCount,fontColor:"#8f8f8f"},}},plugins:{title:{display:false,text:"Stats"},legend:{display:false},tooltip:{mode:"index",intersect:false,caretPadding:0},}}};let highest=0;for(let i=0;i<sources.length;i++){let label=sources[i].substring(0,sources[i].indexOf('='));let path=sources[i].substring(sources[i].indexOf('=')+1);let usage=container.get('usage');let data=usage[path];let value=JSON.parse(element.value);config.data.labels[i]=label;config.data.datasets[i]={};config.data.datasets[i].label=label;config.data.datasets[i].borderColor=themes[colors[i]];config.data.datasets[i].backgroundColor=themes[colors[i]]+'36';config.data.datasets[i].borderWidth=2;config.data.datasets[i].data=[0,0,0,0,0,0,0];config.data.datasets[i].fill=true;if(!data){return;}
let dateFormat=(value.range&&range[value.range])?range[value.range]:'d F Y';for(let x=0;x<data.length;x++){if(data[x].value>highest){highest=data[x].value;}
let dateFormat=(value.range&&range[value.range])?range[value.range]:{year:'numeric',month:'short',day:'2-digit',};for(let x=0;x<data.length;x++){if(data[x].value>highest){highest=data[x].value;}
config.data.datasets[i].data[x]=data[x].value;config.data.labels[x]=date.format(dateFormat,data[x].date);}}
if(highest==0){config.options.scales.y.ticks.stepSize=1;config.options.scales.y.max=ticksCount;}else{highest=Math.ceil(highest/ticksCount)*ticksCount;config.options.scales.y.ticks.stepSize=highest/ticksCount;config.options.scales.y.max=highest;}
if(chart){chart.destroy();}
@ -4245,7 +4151,8 @@ var file=document.createElement("li");var image=document.createElement("img");im
result.bucketId+"/files/"+
result.fileId+"/preview?width="+
previewWidth+"&height="+
previewHeight+"&project="+project+"&mode=admin";image.alt=previewAlt;file.className="file avatar";file.tabIndex=0;file.appendChild(image);preview.appendChild(file);var remove=(function(result){return function(event){render(result.$id);element.value='';};})(result);file.addEventListener("click",remove);file.addEventListener("keypress",remove);element.value=JSON.stringify(result);};input.addEventListener("change",function(){var message=alerts.add({text:labelLoading,class:""},0);var files=input.files;var read=JSON.parse(expression.parse(element.dataset["read"]||"[]"));var write=JSON.parse(expression.parse(element.dataset["write"]||"[]"));sdk.storage.createFile('default','unique()',files[0],read,write).then(function(response){onComplete(message);render({bucketId:response.bucketId,fileId:response.$id});},function(error){alerts.add({text:"An error occurred!",class:""},3000);onComplete(message);});input.disabled=true;});element.addEventListener("change",function(){if(!element.value){return;}
previewHeight+"&project="+project+"&mode=admin";image.alt=previewAlt;file.className="file avatar";file.tabIndex=0;file.appendChild(image);preview.appendChild(file);var remove=(function(result){return function(event){render(result.$id);element.value='';};})(result);file.addEventListener("click",remove);file.addEventListener("keypress",remove);element.value=JSON.stringify(result);};input.addEventListener("change",function(){var message=alerts.add({text:labelLoading,class:""},0);var files=input.files;var permissions=JSON.parse(expression.parse(element.dataset["permissions"]||"[]"))
sdk.storage.createFile('default','unique()',files[0],permissions).then(function(response){onComplete(message);render({bucketId:response.bucketId,fileId:response.$id});},function(error){alerts.add({text:"An error occurred!",class:""},3000);onComplete(message);});input.disabled=true;});element.addEventListener("change",function(){if(!element.value){return;}
render(element.value);wrapper.scrollIntoView();});upload.addEventListener("keypress",function(){input.click();});element.parentNode.insertBefore(wrapper,element);wrapper.appendChild(preview);wrapper.appendChild(progress);wrapper.appendChild(upload);upload.appendChild(input);render(output);if(searchButton){let searchOpen=document.createElement("button");searchOpen.type='button';searchOpen.innerHTML='<i class="icon icon-search"></i> Search';searchOpen.classList.add('reverse');let path=container.scope(searchButton);searchOpen.addEventListener('click',function(){search.selected=element.value;search.path=path;document.dispatchEvent(new CustomEvent("open-file-search",{bubbles:false,cancelable:true}));});wrapper.appendChild(searchOpen);}}});})(window);(function(window){window.ls.container.get("view").add({selector:"data-cookies",controller:function(element,alerts,cookie,env){if(!cookie.get("cookie-alert")){let text=element.dataset["cookies"]||"";alerts.add({text:text,class:"cookie-alert",link:env.HOME+"/policy/cookies",label:'Learn More',callback:function(){cookie.set("cookie-alert","true",365*10);}},0);}}});})(window);(function(window){"use strict";window.ls.view.add({selector:'data-general-copy',repeat:false,controller:function(document,element,alerts){let button=document.createElement("i");button.type="button";button.title="Copy to Clipboard";button.className=element.getAttribute("data-class")||"icon-docs note copy";button.style.cursor="pointer";element.parentNode.insertBefore(button,element.nextSibling);let copy=function(event){window.getSelection().removeAllRanges();let range=document.createRange();range.selectNode(element);window.getSelection().addRange(range);try{document.execCommand("copy");alerts.add({text:"Copied to clipboard",class:""},3000);}catch(err){alerts.add({text:"Failed to copy text ",class:"error"},3000);}
window.getSelection().removeAllRanges();};button.addEventListener("click",copy);}});})(window);(function(window){window.ls.container.get("view").add({selector:"data-page-title",repeat:true,controller:function(element,document,expression){document.title=expression.parse(element.getAttribute("data-page-title"))||document.title;}});})(window);(function(window){"use strict";window.ls.view.add({selector:'data-general-scroll-to',repeat:false,controller:function(element,window){let button=window.document.createElement('button');button.className='scroll-to icon-up-dir';button.alt='Back To Top';button.title='Back To Top';button.addEventListener('click',function(){element.scrollIntoView(true,{behavior:'smooth'});button.blur();},false);element.appendChild(button);}});})(window);(function(window){"use strict";window.ls.view.add({selector:'data-general-scroll-direction',repeat:false,controller:function(element,window){let position=0;let check=function(){let direction=window.document.documentElement.scrollTop;if(direction>position){element.classList.remove('scroll-to-top')
element.classList.add('scroll-to-bottom')}

View file

@ -220,29 +220,26 @@ if(typeof cursor!=='undefined'){payload['cursor']=cursor;}
if(typeof cursorDirection!=='undefined'){payload['cursorDirection']=cursorDirection;}
if(typeof orderType!=='undefined'){payload['orderType']=orderType;}
const uri=new URL(this.client.config.endpoint+path);return yield this.client.call('get',uri,{'content-type':'application/json',},payload);});}
createCollection(databaseId,collectionId,name,permission,read,write){return __awaiter(this,void 0,void 0,function*(){if(typeof databaseId==='undefined'){throw new AppwriteException('Missing required parameter: "databaseId"');}
createCollection(databaseId,collectionId,name,permissions,documentSecurity){return __awaiter(this,void 0,void 0,function*(){if(typeof databaseId==='undefined'){throw new AppwriteException('Missing required parameter: "databaseId"');}
if(typeof collectionId==='undefined'){throw new AppwriteException('Missing required parameter: "collectionId"');}
if(typeof name==='undefined'){throw new AppwriteException('Missing required parameter: "name"');}
if(typeof permission==='undefined'){throw new AppwriteException('Missing required parameter: "permission"');}
if(typeof read==='undefined'){throw new AppwriteException('Missing required parameter: "read"');}
if(typeof write==='undefined'){throw new AppwriteException('Missing required parameter: "write"');}
if(typeof permissions==='undefined'){throw new AppwriteException('Missing required parameter: "permissions"');}
if(typeof documentSecurity==='undefined'){throw new AppwriteException('Missing required parameter: "documentSecurity"');}
let path='/databases/{databaseId}/collections'.replace('{databaseId}',databaseId);let payload={};if(typeof collectionId!=='undefined'){payload['collectionId']=collectionId;}
if(typeof name!=='undefined'){payload['name']=name;}
if(typeof permission!=='undefined'){payload['permission']=permission;}
if(typeof read!=='undefined'){payload['read']=read;}
if(typeof write!=='undefined'){payload['write']=write;}
if(typeof permissions!=='undefined'){payload['permissions']=permissions;}
if(typeof documentSecurity!=='undefined'){payload['documentSecurity']=documentSecurity;}
const uri=new URL(this.client.config.endpoint+path);return yield this.client.call('post',uri,{'content-type':'application/json',},payload);});}
getCollection(databaseId,collectionId){return __awaiter(this,void 0,void 0,function*(){if(typeof databaseId==='undefined'){throw new AppwriteException('Missing required parameter: "databaseId"');}
if(typeof collectionId==='undefined'){throw new AppwriteException('Missing required parameter: "collectionId"');}
let path='/databases/{databaseId}/collections/{collectionId}'.replace('{databaseId}',databaseId).replace('{collectionId}',collectionId);let payload={};const uri=new URL(this.client.config.endpoint+path);return yield this.client.call('get',uri,{'content-type':'application/json',},payload);});}
updateCollection(databaseId,collectionId,name,permission,read,write,enabled){return __awaiter(this,void 0,void 0,function*(){if(typeof databaseId==='undefined'){throw new AppwriteException('Missing required parameter: "databaseId"');}
updateCollection(databaseId,collectionId,name,documentSecurity,permissions,enabled){return __awaiter(this,void 0,void 0,function*(){if(typeof databaseId==='undefined'){throw new AppwriteException('Missing required parameter: "databaseId"');}
if(typeof collectionId==='undefined'){throw new AppwriteException('Missing required parameter: "collectionId"');}
if(typeof name==='undefined'){throw new AppwriteException('Missing required parameter: "name"');}
if(typeof permission==='undefined'){throw new AppwriteException('Missing required parameter: "permission"');}
if(typeof documentSecurity==='undefined'){throw new AppwriteException('Missing required parameter: "documentSecurity"');}
let path='/databases/{databaseId}/collections/{collectionId}'.replace('{databaseId}',databaseId).replace('{collectionId}',collectionId);let payload={};if(typeof name!=='undefined'){payload['name']=name;}
if(typeof permission!=='undefined'){payload['permission']=permission;}
if(typeof read!=='undefined'){payload['read']=read;}
if(typeof write!=='undefined'){payload['write']=write;}
if(typeof permissions!=='undefined'){payload['permissions']=permissions;}
if(typeof documentSecurity!=='undefined'){payload['documentSecurity']=documentSecurity;}
if(typeof enabled!=='undefined'){payload['enabled']=enabled;}
const uri=new URL(this.client.config.endpoint+path);return yield this.client.call('put',uri,{'content-type':'application/json',},payload);});}
deleteCollection(databaseId,collectionId){return __awaiter(this,void 0,void 0,function*(){if(typeof databaseId==='undefined'){throw new AppwriteException('Missing required parameter: "databaseId"');}
@ -260,6 +257,15 @@ if(typeof required!=='undefined'){payload['required']=required;}
if(typeof xdefault!=='undefined'){payload['default']=xdefault;}
if(typeof array!=='undefined'){payload['array']=array;}
const uri=new URL(this.client.config.endpoint+path);return yield this.client.call('post',uri,{'content-type':'application/json',},payload);});}
createDatetimeAttribute(databaseId,collectionId,key,required,xdefault,array){return __awaiter(this,void 0,void 0,function*(){if(typeof databaseId==='undefined'){throw new AppwriteException('Missing required parameter: "databaseId"');}
if(typeof collectionId==='undefined'){throw new AppwriteException('Missing required parameter: "collectionId"');}
if(typeof key==='undefined'){throw new AppwriteException('Missing required parameter: "key"');}
if(typeof required==='undefined'){throw new AppwriteException('Missing required parameter: "required"');}
let path='/databases/{databaseId}/collections/{collectionId}/attributes/datetime'.replace('{databaseId}',databaseId).replace('{collectionId}',collectionId);let payload={};if(typeof key!=='undefined'){payload['key']=key;}
if(typeof required!=='undefined'){payload['required']=required;}
if(typeof xdefault!=='undefined'){payload['default']=xdefault;}
if(typeof array!=='undefined'){payload['array']=array;}
const uri=new URL(this.client.config.endpoint+path);return yield this.client.call('post',uri,{'content-type':'application/json',},payload);});}
createEmailAttribute(databaseId,collectionId,key,required,xdefault,array){return __awaiter(this,void 0,void 0,function*(){if(typeof databaseId==='undefined'){throw new AppwriteException('Missing required parameter: "databaseId"');}
if(typeof collectionId==='undefined'){throw new AppwriteException('Missing required parameter: "collectionId"');}
if(typeof key==='undefined'){throw new AppwriteException('Missing required parameter: "key"');}
@ -349,25 +355,23 @@ if(typeof cursorDirection!=='undefined'){payload['cursorDirection']=cursorDirect
if(typeof orderAttributes!=='undefined'){payload['orderAttributes']=orderAttributes;}
if(typeof orderTypes!=='undefined'){payload['orderTypes']=orderTypes;}
const uri=new URL(this.client.config.endpoint+path);return yield this.client.call('get',uri,{'content-type':'application/json',},payload);});}
createDocument(databaseId,collectionId,documentId,data,read,write){return __awaiter(this,void 0,void 0,function*(){if(typeof databaseId==='undefined'){throw new AppwriteException('Missing required parameter: "databaseId"');}
createDocument(databaseId,collectionId,documentId,data,permissions){return __awaiter(this,void 0,void 0,function*(){if(typeof databaseId==='undefined'){throw new AppwriteException('Missing required parameter: "databaseId"');}
if(typeof collectionId==='undefined'){throw new AppwriteException('Missing required parameter: "collectionId"');}
if(typeof documentId==='undefined'){throw new AppwriteException('Missing required parameter: "documentId"');}
if(typeof data==='undefined'){throw new AppwriteException('Missing required parameter: "data"');}
let path='/databases/{databaseId}/collections/{collectionId}/documents'.replace('{databaseId}',databaseId).replace('{collectionId}',collectionId);let payload={};if(typeof documentId!=='undefined'){payload['documentId']=documentId;}
if(typeof data!=='undefined'){payload['data']=data;}
if(typeof read!=='undefined'){payload['read']=read;}
if(typeof write!=='undefined'){payload['write']=write;}
if(typeof permissions!=='undefined'){payload['permissions']=permissions;}
const uri=new URL(this.client.config.endpoint+path);return yield this.client.call('post',uri,{'content-type':'application/json',},payload);});}
getDocument(databaseId,collectionId,documentId){return __awaiter(this,void 0,void 0,function*(){if(typeof databaseId==='undefined'){throw new AppwriteException('Missing required parameter: "databaseId"');}
if(typeof collectionId==='undefined'){throw new AppwriteException('Missing required parameter: "collectionId"');}
if(typeof documentId==='undefined'){throw new AppwriteException('Missing required parameter: "documentId"');}
let path='/databases/{databaseId}/collections/{collectionId}/documents/{documentId}'.replace('{databaseId}',databaseId).replace('{collectionId}',collectionId).replace('{documentId}',documentId);let payload={};const uri=new URL(this.client.config.endpoint+path);return yield this.client.call('get',uri,{'content-type':'application/json',},payload);});}
updateDocument(databaseId,collectionId,documentId,data,read,write){return __awaiter(this,void 0,void 0,function*(){if(typeof databaseId==='undefined'){throw new AppwriteException('Missing required parameter: "databaseId"');}
updateDocument(databaseId,collectionId,documentId,data,permissions){return __awaiter(this,void 0,void 0,function*(){if(typeof databaseId==='undefined'){throw new AppwriteException('Missing required parameter: "databaseId"');}
if(typeof collectionId==='undefined'){throw new AppwriteException('Missing required parameter: "collectionId"');}
if(typeof documentId==='undefined'){throw new AppwriteException('Missing required parameter: "documentId"');}
let path='/databases/{databaseId}/collections/{collectionId}/documents/{documentId}'.replace('{databaseId}',databaseId).replace('{collectionId}',collectionId).replace('{documentId}',documentId);let payload={};if(typeof data!=='undefined'){payload['data']=data;}
if(typeof read!=='undefined'){payload['read']=read;}
if(typeof write!=='undefined'){payload['write']=write;}
if(typeof permissions!=='undefined'){payload['permissions']=permissions;}
const uri=new URL(this.client.config.endpoint+path);return yield this.client.call('patch',uri,{'content-type':'application/json',},payload);});}
deleteDocument(databaseId,collectionId,documentId){return __awaiter(this,void 0,void 0,function*(){if(typeof databaseId==='undefined'){throw new AppwriteException('Missing required parameter: "databaseId"');}
if(typeof collectionId==='undefined'){throw new AppwriteException('Missing required parameter: "collectionId"');}
@ -696,14 +700,13 @@ if(typeof cursor!=='undefined'){payload['cursor']=cursor;}
if(typeof cursorDirection!=='undefined'){payload['cursorDirection']=cursorDirection;}
if(typeof orderType!=='undefined'){payload['orderType']=orderType;}
const uri=new URL(this.client.config.endpoint+path);return yield this.client.call('get',uri,{'content-type':'application/json',},payload);});}
createBucket(bucketId,name,permission,read,write,enabled,maximumFileSize,allowedFileExtensions,encryption,antivirus){return __awaiter(this,void 0,void 0,function*(){if(typeof bucketId==='undefined'){throw new AppwriteException('Missing required parameter: "bucketId"');}
createBucket(bucketId,name,fileSecurity,permissions,enabled,maximumFileSize,allowedFileExtensions,encryption,antivirus){return __awaiter(this,void 0,void 0,function*(){if(typeof bucketId==='undefined'){throw new AppwriteException('Missing required parameter: "bucketId"');}
if(typeof name==='undefined'){throw new AppwriteException('Missing required parameter: "name"');}
if(typeof permission==='undefined'){throw new AppwriteException('Missing required parameter: "permission"');}
if(typeof fileSecurity==='undefined'){throw new AppwriteException('Missing required parameter: "fileSecurity"');}
let path='/storage/buckets';let payload={};if(typeof bucketId!=='undefined'){payload['bucketId']=bucketId;}
if(typeof name!=='undefined'){payload['name']=name;}
if(typeof permission!=='undefined'){payload['permission']=permission;}
if(typeof read!=='undefined'){payload['read']=read;}
if(typeof write!=='undefined'){payload['write']=write;}
if(typeof permissions!=='undefined'){payload['permissions']=permissions;}
if(typeof fileSecurity!=='undefined'){payload['fileSecurity']=fileSecurity;}
if(typeof enabled!=='undefined'){payload['enabled']=enabled;}
if(typeof maximumFileSize!=='undefined'){payload['maximumFileSize']=maximumFileSize;}
if(typeof allowedFileExtensions!=='undefined'){payload['allowedFileExtensions']=allowedFileExtensions;}
@ -712,13 +715,12 @@ if(typeof antivirus!=='undefined'){payload['antivirus']=antivirus;}
const uri=new URL(this.client.config.endpoint+path);return yield this.client.call('post',uri,{'content-type':'application/json',},payload);});}
getBucket(bucketId){return __awaiter(this,void 0,void 0,function*(){if(typeof bucketId==='undefined'){throw new AppwriteException('Missing required parameter: "bucketId"');}
let path='/storage/buckets/{bucketId}'.replace('{bucketId}',bucketId);let payload={};const uri=new URL(this.client.config.endpoint+path);return yield this.client.call('get',uri,{'content-type':'application/json',},payload);});}
updateBucket(bucketId,name,permission,read,write,enabled,maximumFileSize,allowedFileExtensions,encryption,antivirus){return __awaiter(this,void 0,void 0,function*(){if(typeof bucketId==='undefined'){throw new AppwriteException('Missing required parameter: "bucketId"');}
updateBucket(bucketId,name,fileSecurity,permissions,enabled,maximumFileSize,allowedFileExtensions,encryption,antivirus){return __awaiter(this,void 0,void 0,function*(){if(typeof bucketId==='undefined'){throw new AppwriteException('Missing required parameter: "bucketId"');}
if(typeof name==='undefined'){throw new AppwriteException('Missing required parameter: "name"');}
if(typeof permission==='undefined'){throw new AppwriteException('Missing required parameter: "permission"');}
if(typeof fileSecurity==='undefined'){throw new AppwriteException('Missing required parameter: "fileSecurity"');}
let path='/storage/buckets/{bucketId}'.replace('{bucketId}',bucketId);let payload={};if(typeof name!=='undefined'){payload['name']=name;}
if(typeof permission!=='undefined'){payload['permission']=permission;}
if(typeof read!=='undefined'){payload['read']=read;}
if(typeof write!=='undefined'){payload['write']=write;}
if(typeof permissions!=='undefined'){payload['permissions']=permissions;}
if(typeof fileSecurity!=='undefined'){payload['fileSecurity']=fileSecurity;}
if(typeof enabled!=='undefined'){payload['enabled']=enabled;}
if(typeof maximumFileSize!=='undefined'){payload['maximumFileSize']=maximumFileSize;}
if(typeof allowedFileExtensions!=='undefined'){payload['allowedFileExtensions']=allowedFileExtensions;}
@ -735,13 +737,12 @@ if(typeof cursor!=='undefined'){payload['cursor']=cursor;}
if(typeof cursorDirection!=='undefined'){payload['cursorDirection']=cursorDirection;}
if(typeof orderType!=='undefined'){payload['orderType']=orderType;}
const uri=new URL(this.client.config.endpoint+path);return yield this.client.call('get',uri,{'content-type':'application/json',},payload);});}
createFile(bucketId,fileId,file,read,write,onProgress=(progress)=>{}){return __awaiter(this,void 0,void 0,function*(){if(typeof bucketId==='undefined'){throw new AppwriteException('Missing required parameter: "bucketId"');}
createFile(bucketId,fileId,file,permissions,onProgress=(progress)=>{}){return __awaiter(this,void 0,void 0,function*(){if(typeof bucketId==='undefined'){throw new AppwriteException('Missing required parameter: "bucketId"');}
if(typeof fileId==='undefined'){throw new AppwriteException('Missing required parameter: "fileId"');}
if(typeof file==='undefined'){throw new AppwriteException('Missing required parameter: "file"');}
let path='/storage/buckets/{bucketId}/files'.replace('{bucketId}',bucketId);let payload={};if(typeof fileId!=='undefined'){payload['fileId']=fileId;}
if(typeof file!=='undefined'){payload['file']=file;}
if(typeof read!=='undefined'){payload['read']=read;}
if(typeof write!=='undefined'){payload['write']=write;}
if(typeof permissions!=='undefined'){payload['permissions']=permissions;}
const uri=new URL(this.client.config.endpoint+path);if(!(file instanceof File)){throw new AppwriteException('Parameter "file" has to be a File.');}
const size=file.size;if(size<=Service.CHUNK_SIZE){return yield this.client.call('post',uri,{'content-type':'multipart/form-data',},payload);}
let id=undefined;let response=undefined;const headers={'content-type':'multipart/form-data',};let counter=0;const totalCounters=Math.ceil(size/Service.CHUNK_SIZE);if(fileId!='unique()'){try{response=yield this.client.call('GET',new URL(this.client.config.endpoint+path+'/'+fileId),headers);counter=response.chunksUploaded;}
@ -753,10 +754,9 @@ return response;});}
getFile(bucketId,fileId){return __awaiter(this,void 0,void 0,function*(){if(typeof bucketId==='undefined'){throw new AppwriteException('Missing required parameter: "bucketId"');}
if(typeof fileId==='undefined'){throw new AppwriteException('Missing required parameter: "fileId"');}
let path='/storage/buckets/{bucketId}/files/{fileId}'.replace('{bucketId}',bucketId).replace('{fileId}',fileId);let payload={};const uri=new URL(this.client.config.endpoint+path);return yield this.client.call('get',uri,{'content-type':'application/json',},payload);});}
updateFile(bucketId,fileId,read,write){return __awaiter(this,void 0,void 0,function*(){if(typeof bucketId==='undefined'){throw new AppwriteException('Missing required parameter: "bucketId"');}
updateFile(bucketId,fileId,permissions){return __awaiter(this,void 0,void 0,function*(){if(typeof bucketId==='undefined'){throw new AppwriteException('Missing required parameter: "bucketId"');}
if(typeof fileId==='undefined'){throw new AppwriteException('Missing required parameter: "fileId"');}
let path='/storage/buckets/{bucketId}/files/{fileId}'.replace('{bucketId}',bucketId).replace('{fileId}',fileId);let payload={};if(typeof read!=='undefined'){payload['read']=read;}
if(typeof write!=='undefined'){payload['write']=write;}
let path='/storage/buckets/{bucketId}/files/{fileId}'.replace('{bucketId}',bucketId).replace('{fileId}',fileId);let payload={};if(typeof permissions!=='undefined'){payload['permissions']=permissions;}
const uri=new URL(this.client.config.endpoint+path);return yield this.client.call('put',uri,{'content-type':'application/json',},payload);});}
deleteFile(bucketId,fileId){return __awaiter(this,void 0,void 0,function*(){if(typeof bucketId==='undefined'){throw new AppwriteException('Missing required parameter: "bucketId"');}
if(typeof fileId==='undefined'){throw new AppwriteException('Missing required parameter: "fileId"');}

View file

@ -521,115 +521,15 @@ params=formData;break;}
return new Promise(function(resolve,reject){let request=new XMLHttpRequest(),key;request.withCredentials=true;request.open(method,path,true);for(key in headers){if(headers.hasOwnProperty(key)){request.setRequestHeader(key,headers[key]);}}
request.onload=function(){if(4===request.readyState&&399>=request.status){let data=request.response;let contentType=this.getResponseHeader('content-type');contentType=contentType.substring(0,contentType.indexOf(';'));switch(contentType){case'application/json':data=JSON.parse(data);break;}
resolve(data);}else{reject(new Error(request.statusText));}};if(progress){request.addEventListener('progress',progress);request.upload.addEventListener('progress',progress,false);}
request.onerror=function(){reject(new Error("Network Error"));};request.send(params);})};return{'get':function(path,headers={},params={}){return call('GET',path+((params.length>0)?'?'+buildQuery(params):''),headers,{});},'post':function(path,headers={},params={},progress=null){return call('POST',path,headers,params,progress);},'put':function(path,headers={},params={},progress=null){return call('PUT',headers,params,progress);},'patch':function(path,headers={},params={},progress=null){return call('PATCH',path,headers,params,progress);},'delete':function(path,headers={},params={},progress=null){return call('DELETE',path,headers,params,progress);},'addGlobalParam':addGlobalParam,'addGlobalHeader':addGlobalHeader}}(window.document);let analytics={create:function(id,source,activity,url){return http.post('/analytics',{'content-type':'application/json'},{id:id,source:source,activity:activity,url:url,version:env.VERSION,setup:env.SETUP});},};return{analytics:analytics,};},true);})(window);(function(window){"use strict";window.ls.container.set('console',function(window){var client=new Appwrite.Client();var endpoint=window.location.origin+'/v1';client.setEndpoint(endpoint).setProject('console').setLocale(APP_ENV.LOCALE);return{client:client,account:new Appwrite.Account(client),avatars:new Appwrite.Avatars(client),databases:new Appwrite.Databases(client),functions:new Appwrite.Functions(client),health:new Appwrite.Health(client),locale:new Appwrite.Locale(client),projects:new Appwrite.Projects(client),storage:new Appwrite.Storage(client),teams:new Appwrite.Teams(client),users:new Appwrite.Users(client)}},true);})(window);(function(window){"use strict";window.ls.container.set('date',function(){function format(format,timestamp){var jsdate,f
var txtWords=['Sun','Mon','Tues','Wednes','Thurs','Fri','Satur','January','February','March','April','May','June','July','August','September','October','November','December']
var formatChr=/\\?(.?)/gi
var formatChrCb=function(t,s){return f[t]?f[t]():s}
var _pad=function(n,c){n=String(n)
while(n.length<c){n='0'+n}
return n}
f={d:function(){return _pad(f.j(),2)},D:function(){return f.l().slice(0,3)},j:function(){return jsdate.getDate()},l:function(){return txtWords[f.w()]+'day'},N:function(){return f.w()||7},S:function(){var j=f.j()
var i=j%10
if(i<=3&&parseInt((j%100)/10,10)===1){i=0}
return['st','nd','rd'][i-1]||'th'},w:function(){return jsdate.getDay()},z:function(){var a=new Date(f.Y(),f.n()-1,f.j())
var b=new Date(f.Y(),0,1)
return Math.round((a-b)/864e5)},W:function(){var a=new Date(f.Y(),f.n()-1,f.j()-f.N()+3)
var b=new Date(a.getFullYear(),0,4)
return _pad(1+Math.round((a-b)/864e5/7),2)},F:function(){return txtWords[6+f.n()]},m:function(){return _pad(f.n(),2)},M:function(){return f.F().slice(0,3)},n:function(){return jsdate.getMonth()+1},t:function(){return(new Date(f.Y(),f.n(),0)).getDate()},L:function(){var j=f.Y()
return j%4===0&j%100!==0|j%400===0},o:function(){var n=f.n()
var W=f.W()
var Y=f.Y()
return Y+(n===12&&W<9?1:n===1&&W>9?-1:0)},Y:function(){return jsdate.getFullYear()},y:function(){return f.Y().toString().slice(-2)},a:function(){return jsdate.getHours()>11?'pm':'am'},A:function(){return f.a().toUpperCase()},B:function(){var H=jsdate.getUTCHours()*36e2
var i=jsdate.getUTCMinutes()*60
var s=jsdate.getUTCSeconds()
return _pad(Math.floor((H+i+s+36e2)/86.4)%1e3,3)},g:function(){return f.G()%12||12},G:function(){return jsdate.getHours()},h:function(){return _pad(f.g(),2)},H:function(){return _pad(f.G(),2)},i:function(){return _pad(jsdate.getMinutes(),2)},s:function(){return _pad(jsdate.getSeconds(),2)},u:function(){return _pad(jsdate.getMilliseconds()*1000,6)},e:function(){var msg='Not supported (see source code of date() for timezone on how to add support)'
throw new Error(msg)},I:function(){var a=new Date(f.Y(),0)
var c=Date.UTC(f.Y(),0)
var b=new Date(f.Y(),6)
var d=Date.UTC(f.Y(),6)
return((a-c)!==(b-d))?1:0},O:function(){var tzo=jsdate.getTimezoneOffset()
var a=Math.abs(tzo)
return(tzo>0?'-':'+')+_pad(Math.floor(a/60)*100+a%60,4)},P:function(){var O=f.O()
return(O.substr(0,3)+':'+O.substr(3,2))},T:function(){return'UTC'},Z:function(){return-jsdate.getTimezoneOffset()*60},c:function(){return'Y-m-d\\TH:i:sP'.replace(formatChr,formatChrCb)},r:function(){return'D, d M Y H:i:s O'.replace(formatChr,formatChrCb)},U:function(){return jsdate/1000|0}}
var _date=function(format,timestamp){jsdate=(timestamp===undefined?new Date():(timestamp instanceof Date)?new Date(timestamp):new Date(timestamp*1000))
return format.replace(formatChr,formatChrCb)}
return _date(format,timestamp)}
function strtotime(text,now){var parsed
var match
var today
var year
var date
var days
var ranges
var len
var times
var regex
var i
var fail=false
if(!text){return fail}
text=text.replace(/^\s+|\s+$/g,'').replace(/\s{2,}/g,' ').replace(/[\t\r\n]/g,'').toLowerCase()
var pattern=new RegExp(['^(\\d{1,4})','([\\-\\.\\/:])','(\\d{1,2})','([\\-\\.\\/:])','(\\d{1,4})','(?:\\s(\\d{1,2}):(\\d{2})?:?(\\d{2})?)?','(?:\\s([A-Z]+)?)?$'].join(''))
match=text.match(pattern)
if(match&&match[2]===match[4]){if(match[1]>1901){switch(match[2]){case'-':if(match[3]>12||match[5]>31){return fail}
return new Date(match[1],parseInt(match[3],10)-1,match[5],match[6]||0,match[7]||0,match[8]||0,match[9]||0)/1000
case'.':return fail
case'/':if(match[3]>12||match[5]>31){return fail}
return new Date(match[1],parseInt(match[3],10)-1,match[5],match[6]||0,match[7]||0,match[8]||0,match[9]||0)/1000}}else if(match[5]>1901){switch(match[2]){case'-':if(match[3]>12||match[1]>31){return fail}
return new Date(match[5],parseInt(match[3],10)-1,match[1],match[6]||0,match[7]||0,match[8]||0,match[9]||0)/1000
case'.':if(match[3]>12||match[1]>31){return fail}
return new Date(match[5],parseInt(match[3],10)-1,match[1],match[6]||0,match[7]||0,match[8]||0,match[9]||0)/1000
case'/':if(match[1]>12||match[3]>31){return fail}
return new Date(match[5],parseInt(match[1],10)-1,match[3],match[6]||0,match[7]||0,match[8]||0,match[9]||0)/1000}}else{switch(match[2]){case'-':if(match[3]>12||match[5]>31||(match[1]<70&&match[1]>38)){return fail}
year=match[1]>=0&&match[1]<=38?+match[1]+2000:match[1]
return new Date(year,parseInt(match[3],10)-1,match[5],match[6]||0,match[7]||0,match[8]||0,match[9]||0)/1000
case'.':if(match[5]>=70){if(match[3]>12||match[1]>31){return fail}
return new Date(match[5],parseInt(match[3],10)-1,match[1],match[6]||0,match[7]||0,match[8]||0,match[9]||0)/1000}
if(match[5]<60&&!match[6]){if(match[1]>23||match[3]>59){return fail}
today=new Date()
return new Date(today.getFullYear(),today.getMonth(),today.getDate(),match[1]||0,match[3]||0,match[5]||0,match[9]||0)/1000}
return fail
case'/':if(match[1]>12||match[3]>31||(match[5]<70&&match[5]>38)){return fail}
year=match[5]>=0&&match[5]<=38?+match[5]+2000:match[5]
return new Date(year,parseInt(match[1],10)-1,match[3],match[6]||0,match[7]||0,match[8]||0,match[9]||0)/1000
case':':if(match[1]>23||match[3]>59||match[5]>59){return fail}
today=new Date()
return new Date(today.getFullYear(),today.getMonth(),today.getDate(),match[1]||0,match[3]||0,match[5]||0)/1000}}}
if(text==='now'){return now===null||isNaN(now)?new Date().getTime()/1000|0:now|0}
if(!isNaN(parsed=Date.parse(text))){return parsed/1000|0}
pattern=new RegExp(['^([0-9]{4}-[0-9]{2}-[0-9]{2})','[ t]','([0-9]{2}:[0-9]{2}:[0-9]{2}(\\.[0-9]+)?)','([\\+-][0-9]{2}(:[0-9]{2})?|z)'].join(''))
match=text.match(pattern)
if(match){if(match[4]==='z'){match[4]='Z'}else if(match[4].match(/^([+-][0-9]{2})$/)){match[4]=match[4]+':00'}
if(!isNaN(parsed=Date.parse(match[1]+'T'+match[2]+match[4]))){return parsed/1000|0}}
date=now?new Date(now*1000):new Date()
days={'sun':0,'mon':1,'tue':2,'wed':3,'thu':4,'fri':5,'sat':6}
ranges={'yea':'FullYear','mon':'Month','day':'Date','hou':'Hours','min':'Minutes','sec':'Seconds'}
function lastNext(type,range,modifier){var diff
var day=days[range]
if(typeof day!=='undefined'){diff=day-date.getDay()
if(diff===0){diff=7*modifier}else if(diff>0&&type==='last'){diff-=7}else if(diff<0&&type==='next'){diff+=7}
date.setDate(date.getDate()+diff)}}
function process(val){var splt=val.split(' ')
var type=splt[0]
var range=splt[1].substring(0,3)
var typeIsNumber=/\d+/.test(type)
var ago=splt[2]==='ago'
var num=(type==='last'?-1:1)*(ago?-1:1)
if(typeIsNumber){num*=parseInt(type,10)}
if(ranges.hasOwnProperty(range)&&!splt[1].match(/^mon(day|\.)?$/i)){return date['set'+ranges[range]](date['get'+ranges[range]]()+num)}
if(range==='wee'){return date.setDate(date.getDate()+(num*7))}
if(type==='next'||type==='last'){lastNext(type,range,num)}else if(!typeIsNumber){return false}
return true}
times='(years?|months?|weeks?|days?|hours?|minutes?|min|seconds?|sec'+'|sunday|sun\\.?|monday|mon\\.?|tuesday|tue\\.?|wednesday|wed\\.?'+'|thursday|thu\\.?|friday|fri\\.?|saturday|sat\\.?)'
regex='([+-]?\\d+\\s'+times+'|'+'(last|next)\\s'+times+')(\\sago)?'
match=text.match(new RegExp(regex,'gi'))
if(!match){return fail}
for(i=0,len=match.length;i<len;i++){if(!process(match[i])){return fail}}
return(date.getTime()/1000)}
return{format:format,strtotime:strtotime}}(),true);})(window);(function(window){"use strict";window.ls.container.set('env',function(){return APP_ENV;},true);})(window);(function(window){"use strict";window.ls.container.set('form',function(){function cast(value,to){if(value&&Array.isArray(value)&&to!=='array'){value=value.map(element=>cast(element,to));return value;}
request.onerror=function(){reject(new Error("Network Error"));};request.send(params);})};return{'get':function(path,headers={},params={}){return call('GET',path+((params.length>0)?'?'+buildQuery(params):''),headers,{});},'post':function(path,headers={},params={},progress=null){return call('POST',path,headers,params,progress);},'put':function(path,headers={},params={},progress=null){return call('PUT',headers,params,progress);},'patch':function(path,headers={},params={},progress=null){return call('PATCH',path,headers,params,progress);},'delete':function(path,headers={},params={},progress=null){return call('DELETE',path,headers,params,progress);},'addGlobalParam':addGlobalParam,'addGlobalHeader':addGlobalHeader}}(window.document);let analytics={create:function(id,source,activity,url){return http.post('/analytics',{'content-type':'application/json'},{id:id,source:source,activity:activity,url:url,version:env.VERSION,setup:env.SETUP});},};return{analytics:analytics,};},true);})(window);(function(window){"use strict";window.ls.container.set('console',function(window){var client=new Appwrite.Client();var endpoint=window.location.origin+'/v1';client.setEndpoint(endpoint).setProject('console').setLocale(APP_ENV.LOCALE);return{client:client,account:new Appwrite.Account(client),avatars:new Appwrite.Avatars(client),databases:new Appwrite.Databases(client),functions:new Appwrite.Functions(client),health:new Appwrite.Health(client),locale:new Appwrite.Locale(client),projects:new Appwrite.Projects(client),storage:new Appwrite.Storage(client),teams:new Appwrite.Teams(client),users:new Appwrite.Users(client)}},true);})(window);(function(window){"use strict";window.ls.container.set('date',function(){function format(format,datetime){if(!datetime){return null;}
return new Intl.DateTimeFormat('en-US',{timeZone:'UTC',hourCycle:'h24',...format}).format(new Date(datetime));}
return{format:format,}}(),true);})(window);(function(window){"use strict";window.ls.container.set('env',function(){return APP_ENV;},true);})(window);(function(window){"use strict";window.ls.container.set('form',function(){function cast(value,from,to,){if(value&&Array.isArray(value)&&to!=='array'){value=value.map(element=>cast(element,from,to));return value;}
switch(to){case'int':case'integer':value=parseInt(value);break;case'numeric':value=Number(value);break;case'float':value=parseFloat(value);break;case'string':value=value.toString();if(value.length===0){value=null;}
break;case'json':value=(value)?JSON.parse(value):[];break;case'array':value=(value&&value.constructor&&value.constructor===Array)?value:[value];break;case'array-empty':value=[];break;case'bool':case'boolean':value=(value==='false')?false:value;value=!!value;break;}
break;case'json':value=(value)?JSON.parse(value):[];break;case'array':if(value&&value.constructor&&value.constructor===Array){break;}
if(from==='csv'){if(value.length===0){value=[];}else{value=value.split(',');}}else{value=[value];}
break;case'array-empty':value=[];break;case'bool':case'boolean':value=(value==='false')?false:value;value=!!value;break;}
return value;}
function toJson(element,json){json=json||{};let name=element.getAttribute('name');let type=element.getAttribute('type');let castTo=element.getAttribute('data-cast-to');let ref=json;if(name&&'FORM'!==element.tagName){if(name.startsWith('[')){let splitName=name.split('.');if(splitName.length>1&&splitName[0].endsWith(']')){name=splitName[splitName.length-1];}}
function toJson(element,json){json=json||{};let name=element.getAttribute('name');let type=element.getAttribute('type');let castTo=element.getAttribute('data-cast-to');let castFrom=element.getAttribute('data-cast-from');let ref=json;if(name&&'FORM'!==element.tagName){if(name.startsWith('[')){let splitName=name.split('.');if(splitName.length>1&&splitName[0].endsWith(']')){name=splitName[splitName.length-1];}}
if('FIELDSET'===element.tagName){if(castTo==='object'){if(json[name]===undefined){json[name]={};}
ref=json[name];}
else{if(!Array.isArray(json[name])){json[name]=[];}
@ -642,7 +542,7 @@ else if('file'===type){json[name]=element.files[0];}
else if(undefined!==element.value){if((json[name]!==undefined)&&(!Array.isArray(json[name]))){json[name]=[json[name]];}
if(Array.isArray(json[name])){json[name].push(element.value);}
else{json[name]=element.value;}}
json[name]=cast(json[name],castTo);}}
json[name]=cast(json[name],castFrom,castTo);}}
for(let i=0;i<element.children.length;i++){if(Array.isArray(ref)){ref.push({});toJson(element.children[i],ref[ref.length]);}
else{toJson(element.children[i],ref);}}
return json;}
@ -655,7 +555,7 @@ return false;};return{isRTL:isRTL,};},true);})(window);(function(window){"use st
let size=element.dataset["size"]||80;let name=$value.name||$value||"";name=(typeof name!=='string')?'--':name;return def="/v1/avatars/initials?project=console"+"&name="+
encodeURIComponent(name)+"&width="+
size+"&height="+
size;}).add("selectedCollection",function($value,router){return $value===router.params.collectionId?"selected":"";}).add("selectedDocument",function($value,router){return $value===router.params.documentId?"selected":"";}).add("localeString",function($value){$value=parseInt($value);return!Number.isNaN($value)?$value.toLocaleString():"";}).add("date",function($value,date){return $value?date.format("Y-m-d",$value):"";}).add("dateTime",function($value,date){return $value?date.format("Y-m-d H:i",$value):"";}).add("dateText",function($value,date){return $value?date.format("d M Y",$value):"";}).add("timeSince",function($value){$value=$value*1000;let seconds=Math.floor((Date.now()-$value)/1000);let unit="second";let direction="ago";if(seconds<0){seconds=-seconds;direction="from now";}
size;}).add("selectedCollection",function($value,router){return $value===router.params.collectionId?"selected":"";}).add("selectedDocument",function($value,router){return $value===router.params.documentId?"selected":"";}).add("localeString",function($value){$value=parseInt($value);return!Number.isNaN($value)?$value.toLocaleString():"";}).add("dateTime",function($value,date){return $value?date.format({year:'numeric',month:'2-digit',day:'2-digit',hour:'2-digit',minute:'2-digit'},$value):"";}).add("date",function($value,date){return $value?date.format({year:'numeric',month:'short',day:'2-digit',},$value):"";}).add("timeSince",function($value){$value=new Date($value).getTime();let now=new Date();now.setMinutes(now.getMinutes()+now.getTimezoneOffset());let timestamp=new Date(now.toISOString()).getTime();let seconds=Math.floor((timestamp-$value)/1000);let unit="second";let direction="ago";if(seconds<0){seconds=-seconds;direction="from now";}
let value=seconds;if(seconds>=31536000){value=Math.floor(seconds/31536000);unit="year";}
else if(seconds>=86400){value=Math.floor(seconds/86400);unit="day";}
else if(seconds>=3600){value=Math.floor(seconds/3600);unit="hour";}
@ -693,11 +593,10 @@ if(forcePlaces!==false){rounded=Number(rounded).toFixed(forcePlaces);}
return rounded+abbr;}
window.ls.container.get("view").add({selector:"data-acl",controller:function(element,document,router,alerts){document.body.classList.remove("console");document.body.classList.remove("home");document.body.classList.add(router.getCurrent().view.scope);if(!router.getCurrent().view.project){document.body.classList.add("hide-nav");document.body.classList.remove("show-nav");}else{document.body.classList.add("show-nav");document.body.classList.remove("hide-nav");}
if("/console"===router.getCurrent().path){document.body.classList.add("index");}else{document.body.classList.remove("index");}}}).add({selector:"data-prism",controller:function(window,document,element,alerts){Prism.highlightElement(element);let copy=document.createElement("i");copy.className="icon-docs copy";copy.title="Copy to Clipboard";copy.textContent="Click Here to Copy";copy.addEventListener("click",function(){window.getSelection().removeAllRanges();let range=document.createRange();range.selectNode(element);window.getSelection().addRange(range);try{document.execCommand("copy");alerts.add({text:"Copied to clipboard",class:""},3000);}catch(err){alerts.add({text:"Failed to copy text ",class:"error"},3000);}
window.getSelection().removeAllRanges();});element.parentNode.parentNode.appendChild(copy);}});(function(window){document.addEventListener('alpine:init',()=>{Alpine.store('uploader',{_files:[],files(){return(this._files??[]).filter((file)=>!file.cancelled);},isOpen:true,init(){window.addEventListener('beforeunload',(event)=>{if(this.hasOngoingUploads()){let confirmationMessage="There are incomplete uploads, are you sure you want to leave?";event.returnValue=confirmationMessage;return confirmationMessage;}});},cancelAll(){if(this.hasOngoingUploads()?confirm("Are you sure? This will cancel and remove any ongoing uploads?"):true){this._files.forEach(file=>{if(file.completed||file.failed){this.removeFile(file.id);}else{this.updateFile(file.id,{cancelled:true});}});}},hasOngoingUploads(){let ongoing=false;this._files.some((file)=>{if(!file.completed&&!file.failed){ongoing=true;return;}});return ongoing;},toggle(){this.isOpen=!this.isOpen;},addFile(file){this._files.push(file);},updateFile(id,file){this._files=this._files.map((oldFile)=>id==oldFile.id?{...oldFile,...file}:oldFile);},removeFile(id){const file=this.getFile(id)??{};if(file.completed||file.failed){this._files=this._files.filter((file)=>file.id!==id);}else{if(confirm("Are you sure you want to cancel the upload?")){this.updateFile(id,{cancelled:true});}}},getFile(id){return this._files.find((file)=>file.id===id);},async uploadFile(target){const formData=new FormData(target);const sdk=window.ls.container.get('sdk');const bucketId=formData.get('bucketId');const file=formData.get('file');const fileId=formData.get('fileId');let id=fileId==='unique()'?performance.now():fileId;let read=formData.get('read');if(!file||!fileId){return;}
if(read){read=JSON.parse(read);}
let write=formData.get('write');if(write){write=JSON.parse(write);}
window.getSelection().removeAllRanges();});element.parentNode.parentNode.appendChild(copy);}});(function(window){document.addEventListener('alpine:init',()=>{Alpine.store('uploader',{_files:[],files(){return(this._files??[]).filter((file)=>!file.cancelled);},isOpen:true,init(){window.addEventListener('beforeunload',(event)=>{if(this.hasOngoingUploads()){let confirmationMessage="There are incomplete uploads, are you sure you want to leave?";event.returnValue=confirmationMessage;return confirmationMessage;}});},cancelAll(){if(this.hasOngoingUploads()?confirm("Are you sure? This will cancel and remove any ongoing uploads?"):true){this._files.forEach(file=>{if(file.completed||file.failed){this.removeFile(file.id);}else{this.updateFile(file.id,{cancelled:true});}});}},hasOngoingUploads(){let ongoing=false;this._files.some((file)=>{if(!file.completed&&!file.failed){ongoing=true;return;}});return ongoing;},toggle(){this.isOpen=!this.isOpen;},addFile(file){this._files.push(file);},updateFile(id,file){this._files=this._files.map((oldFile)=>id==oldFile.id?{...oldFile,...file}:oldFile);},removeFile(id){const file=this.getFile(id)??{};if(file.completed||file.failed){this._files=this._files.filter((file)=>file.id!==id);}else{if(confirm("Are you sure you want to cancel the upload?")){this.updateFile(id,{cancelled:true});}}},getFile(id){return this._files.find((file)=>file.id===id);},async uploadFile(target){const formData=new FormData(target);const sdk=window.ls.container.get('sdk');const bucketId=formData.get('bucketId');const file=formData.get('file');const fileId=formData.get('fileId');let id=fileId==='unique()'?performance.now():fileId;if(!file||!fileId){return;}
let permissions=formData.get('permissions');if(permissions){permissions=permissions.split(',');}
if(this.getFile(id)){this.updateFile(id,{name:file.name,completed:false,failed:false,cancelled:false,error:"",});}else{this.addFile({id:id,name:file.name,progress:0,completed:false,failed:false,cancelled:false,error:"",});}
target.reset();try{const response=await sdk.storage.createFile(bucketId,fileId,file,read,write,(progress)=>{this.updateFile(id,{id:progress.$id,progress:Math.round(progress.progress),error:"",});id=progress.$id;const file=this.getFile(id)??{};if(file.cancelled===true){throw'USER_CANCELLED';}});const existingFile=this.getFile(id)??{};if(existingFile.cancelled){this.updateFile(id,{id:response.$id,name:response.name,failed:false,});id=response.$id;throw'USER_CANCELLED'}else{this.updateFile(id,{id:response.$id,name:response.name,progress:100,completed:true,failed:false,});id=response.$id;}
target.reset();try{const response=await sdk.storage.createFile(bucketId,fileId,file,permissions,(progress)=>{this.updateFile(id,{id:progress.$id,progress:Math.round(progress.progress),error:"",});id=progress.$id;const file=this.getFile(id)??{};if(file.cancelled===true){throw'USER_CANCELLED';}});const existingFile=this.getFile(id)??{};if(existingFile.cancelled){this.updateFile(id,{id:response.$id,name:response.name,failed:false,});id=response.$id;throw'USER_CANCELLED'}else{this.updateFile(id,{id:response.$id,name:response.name,progress:100,completed:true,failed:false,});id=response.$id;}
document.dispatchEvent(new CustomEvent('storage.createFile'));}catch(error){if(error==='USER_CANCELLED'){await sdk.storage.deleteFile(bucketId,id);this.updateFile(id,{cancelled:false,failed:true,});this.removeFile(id);}else{this.updateFile(id,{id:id,failed:true,error:error.message??error});}
document.dispatchEvent(new CustomEvent('storage.createFile'));}}});});})(window);(function(window){document.addEventListener('alpine:init',()=>{Alpine.data('events',()=>({events:new Set(),selected:null,action:null,type:null,subType:null,subSubType:null,resource:null,resourceName:'',subResource:null,subResourceName:'',subSubResource:null,subSubResourceName:'',hasResource:false,hasSubResource:false,hasSubSubResource:false,attribute:null,hasAttribute:false,attributes:[],load(events){this.events=new Set(events);},reset(){this.hasResource=this.hasSubResource=this.hasSubSubResource=this.hasAttribute=false;this.type=this.subType=this.subResource=this.resource=this.attribute=this.selected=this.action=null;},setEvent(){this.hasResource=this.hasSubResource=this.hasSubSubResource=this.hasAttribute=this.action=false;if(!this.selected){this.reset();return;}
let[type,action]=this.selected.split('.');switch(type){case'users':if(action==='update'){this.hasAttribute=true;this.attributes=['email','name','password','status','prefs']}
@ -707,7 +606,14 @@ this.action=action;},showModal(modal){document.documentElement.classList.add("mo
if(this.hasSubSubResource){event+=`.${this.subSubType}.${this.subSubResource ? this.subSubResource : '*'}`;}
if(this.action){event+=`.${this.action}`;}
if(this.attribute){event+=`.${this.attribute}`;}
this.events.add(event);this.reset();},removeEvent(value){this.events.delete(value);}}));});})(window);(function(window){"use strict";window.ls.view.add({selector:"data-service",controller:function(element,view,container,form,alerts,expression,window){let action=element.dataset["service"];let service=element.dataset["name"]||null;let event=expression.parse(element.dataset["event"]);let confirm=element.dataset["confirm"]||"";let loading=element.dataset["loading"]||"";let loaderId=null;let scope=element.dataset["scope"]||"sdk";let success=element.dataset["success"]||"";let failure=element.dataset["failure"]||"";let running=false;let callbacks={hide:function(){return function(){return element.style.opacity='0';};},reset:function(){return function(){if("FORM"===element.tagName){return element.reset();}
this.events.add(event);this.reset();},removeEvent(value){this.events.delete(value);}}));});})(window);(function(window){document.addEventListener('alpine:init',()=>{Alpine.data('permissionsMatrix',()=>({permissions:[],rawPermissions:[],load(permissions){if(permissions===undefined){return;}
this.rawPermissions=permissions;permissions.map(p=>{let{type,role}=this.parsePermission(p);type=this.parseInputPermission(type);let index=-1;let existing=this.permissions.find((p,idx)=>{if(p.role===role){index=idx;return true;}})
if(existing===undefined){this.permissions.push({role,[type]:true,});}
if(index!==-1){existing[type]=true;this.permissions[index]=existing;}});},addPermission(formId,role,permissions){if(!document.getElementById(formId).reportValidity()){return;}
Object.entries(permissions).forEach(entry=>{let[type,enabled]=entry;type=this.parseOutputPermission(type);if(enabled){this.rawPermissions.push(this.buildPermission(type,role));}});this.permissions.push({role,...permissions,});this.reset();},updatePermission(index){setTimeout(()=>{const permission=this.permissions[index];Object.keys(permission).forEach(key=>{if(key==='role'){return;}
const parsedKey=this.parseOutputPermission(key);const permissionString=this.buildPermission(parsedKey,permission.role);if(permission[key]){if(!this.rawPermissions.includes(permissionString)){this.rawPermissions.push(permissionString);}}else{this.rawPermissions=this.rawPermissions.filter(p=>{return!p.includes(permissionString);});}});});},removePermission(index){let row=this.permissions.splice(index,1);if(row.length===1){this.rawPermissions=this.rawPermissions.filter(p=>!p.includes(row[0].role));}},parsePermission(permission){let parts=permission.split('(');let type=parts[0];let role=parts[1].replace(')','').replace(' ','').replaceAll('"','');return{type,role};},buildPermission(type,role){return`${type}("${role}")`},parseInputPermission(key){if(key==='delete'){return'xdelete';}
return key;},parseOutputPermission(key){if(key==='xdelete'){return'delete';}
return key;}}));Alpine.data('permissionsRow',()=>({role:'',read:false,create:false,update:false,xdelete:false,reset(){this.role='';this.read=this.create=this.update=this.xdelete=false;}}));});})(window);(function(window){"use strict";window.ls.view.add({selector:"data-service",controller:function(element,view,container,form,alerts,expression,window){let action=element.dataset["service"];let service=element.dataset["name"]||null;let event=expression.parse(element.dataset["event"]);let confirm=element.dataset["confirm"]||"";let loading=element.dataset["loading"]||"";let loaderId=null;let scope=element.dataset["scope"]||"sdk";let success=element.dataset["success"]||"";let failure=element.dataset["failure"]||"";let running=false;let callbacks={hide:function(){return function(){return element.style.opacity='0';};},reset:function(){return function(){if("FORM"===element.tagName){return element.reset();}
throw new Error("This callback is only valid for forms");};},alert:function(text,classname){return function(alerts){alerts.add({text:text,class:classname||"success"},6000);};},redirect:function(url){return function(router){if(url==="/console"){window.location=url;return;}
router.change(url||"/");};},reload:function(){return function(router){router.reload();};},state:function(keys){let updateQueryString=function(key,value,url){var re=new RegExp("([?&])"+key+"=.*?(&|#|$)(.*)","gi"),hash;if(re.test(url)){if(typeof value!=="undefined"&&value!==null){return url.replace(re,"$1"+key+"="+value+"$2$3");}else{hash=url.split("#");url=hash[0].replace(re,"$1$3").replace(/(&|\?)$/,"");if(typeof hash[1]!=="undefined"&&hash[1]!==null){url+="#"+hash[1];}
return url;}}else{if(typeof value!=="undefined"&&value!==null){var separator=url.indexOf("?")!==-1?"&":"?";hash=url.split("#");url=hash[0]+separator+key+"="+value;if(typeof hash[1]!=="undefined"&&hash[1]!==null){url+="#"+hash[1];}
@ -753,9 +659,9 @@ button.addEventListener("click",function(){var clone=document.createElement(elem
clone.innerHTML=template;clone.className=element.className;var input=clone.querySelector("input, select, textarea");view.render(clone);if(debug){console.log('Debug: clone: ',clone);console.log('Debug: target: ',target);}
if(target){target.appendChild(clone);}else{button.parentNode.insertBefore(clone,button);}
if(input){input.focus();}
Array.prototype.slice.call(clone.querySelectorAll("[data-remove]")).map(function(obj){obj.addEventListener("click",function(){clone.parentNode.removeChild(clone);obj.scrollIntoView({behavior:"smooth"});});});Array.prototype.slice.call(clone.querySelectorAll("[data-up]")).map(function(obj){obj.addEventListener("click",function(){if(clone.previousElementSibling){clone.parentNode.insertBefore(clone,clone.previousElementSibling);obj.scrollIntoView({behavior:"smooth"});}});});Array.prototype.slice.call(clone.querySelectorAll("[data-down]")).map(function(obj){obj.addEventListener("click",function(){if(clone.nextElementSibling){clone.parentNode.insertBefore(clone.nextElementSibling,clone);obj.scrollIntoView({behavior:"smooth"});}});});});element.parentNode.insertBefore(button,element.nextSibling);element.parentNode.removeChild(element);button.form.addEventListener('reset',function(event){target.innerHTML='';if(first){button.click();}});if(first){button.click();}}});})(window);(function(window){"use strict";window.ls.container.get("view").add({selector:"data-forms-add",repeat:false,controller:function(element,view,container,document){for(var i=0;i<element.children.length;i++){let button=document.createElement("button");let template=element.children[i].cloneNode(true);let as=element.getAttribute('data-ls-as');let counter=0;button.type="button";button.innerText="Add";button.classList.add("reverse");button.classList.add("margin-end-small");button.addEventListener('click',function(){container.addNamespace(as,'new-'+counter++);console.log(container.namespaces,container.get(as),as);container.set(as,null,true,true);let child=template.cloneNode(true);view.render(child);element.appendChild(child);element.style.visibility='visible';let inputs=child.querySelectorAll('input,textarea');for(let index=0;index<inputs.length;++index){if(inputs[index].type!=='hidden'){inputs[index].focus();break;}}});element.after(button);}}});})(window);(function(window){"use strict";window.ls.container.get("view").add({selector:"data-forms-chart",controller:function(element,container,date,document){let wrapper=document.createElement("div");let child=document.createElement("canvas");let sources=element.getAttribute('data-forms-chart');let width=element.getAttribute('data-width')||500;let height=element.getAttribute('data-height')||175;let showXAxis=element.getAttribute('data-show-x-axis')||false;let showYAxis=element.getAttribute('data-show-y-axis')||false;let colors=(element.getAttribute('data-colors')||'blue,green,orange,red').split(',');let themes={'blue':'#29b5d9','green':'#4eb55b','orange':'#fba233','red':'#dc3232','create':'#00b680','read':'#009cde','update':'#696fd7','delete':'#da5d95',};let range={'24h':'H:i','7d':'d F Y','30d':'d F Y','90d':'d F Y'}
Array.prototype.slice.call(clone.querySelectorAll("[data-remove]")).map(function(obj){obj.addEventListener("click",function(){clone.parentNode.removeChild(clone);obj.scrollIntoView({behavior:"smooth"});});});Array.prototype.slice.call(clone.querySelectorAll("[data-up]")).map(function(obj){obj.addEventListener("click",function(){if(clone.previousElementSibling){clone.parentNode.insertBefore(clone,clone.previousElementSibling);obj.scrollIntoView({behavior:"smooth"});}});});Array.prototype.slice.call(clone.querySelectorAll("[data-down]")).map(function(obj){obj.addEventListener("click",function(){if(clone.nextElementSibling){clone.parentNode.insertBefore(clone.nextElementSibling,clone);obj.scrollIntoView({behavior:"smooth"});}});});});element.parentNode.insertBefore(button,element.nextSibling);element.parentNode.removeChild(element);button.form.addEventListener('reset',function(event){target.innerHTML='';if(first){button.click();}});if(first){button.click();}}});})(window);(function(window){"use strict";window.ls.container.get("view").add({selector:"data-forms-add",repeat:false,controller:function(element,view,container,document){for(var i=0;i<element.children.length;i++){let button=document.createElement("button");let template=element.children[i].cloneNode(true);let as=element.getAttribute('data-ls-as');let counter=0;button.type="button";button.innerText="Add";button.classList.add("reverse");button.classList.add("margin-end-small");button.addEventListener('click',function(){container.addNamespace(as,'new-'+counter++);console.log(container.namespaces,container.get(as),as);container.set(as,null,true,true);let child=template.cloneNode(true);view.render(child);element.appendChild(child);element.style.visibility='visible';let inputs=child.querySelectorAll('input,textarea');for(let index=0;index<inputs.length;++index){if(inputs[index].type!=='hidden'){inputs[index].focus();break;}}});element.after(button);}}});})(window);(function(window){"use strict";window.ls.container.get("view").add({selector:"data-forms-chart",controller:function(element,container,date,document){let wrapper=document.createElement("div");let child=document.createElement("canvas");let sources=element.getAttribute('data-forms-chart');let width=element.getAttribute('data-width')||500;let height=element.getAttribute('data-height')||175;let showXAxis=element.getAttribute('data-show-x-axis')||false;let showYAxis=element.getAttribute('data-show-y-axis')||false;let colors=(element.getAttribute('data-colors')||'blue,green,orange,red').split(',');let themes={'blue':'#29b5d9','green':'#4eb55b','orange':'#fba233','red':'#dc3232','create':'#00b680','read':'#009cde','update':'#696fd7','delete':'#da5d95',};let range={'24h':{hour:'2-digit',minute:'2-digit'},'7d':{year:'numeric',month:'short',day:'2-digit',},'30d':{year:'numeric',month:'short',day:'2-digit',},'90d':{year:'numeric',month:'short',day:'2-digit',}}
let ticksCount=5;element.parentNode.insertBefore(wrapper,element.nextSibling);wrapper.classList.add('content');child.width=width;child.height=height;sources=sources.split(',');wrapper.appendChild(child);let chart=null;let check=function(){let config={type:"line",data:{labels:[],datasets:[]},options:{animation:{duration:0},responsive:true,hover:{mode:"nearest",intersect:false},scales:{x:{display:showXAxis},y:{display:showYAxis,min:0,ticks:{count:ticksCount,fontColor:"#8f8f8f"},}},plugins:{title:{display:false,text:"Stats"},legend:{display:false},tooltip:{mode:"index",intersect:false,caretPadding:0},}}};let highest=0;for(let i=0;i<sources.length;i++){let label=sources[i].substring(0,sources[i].indexOf('='));let path=sources[i].substring(sources[i].indexOf('=')+1);let usage=container.get('usage');let data=usage[path];let value=JSON.parse(element.value);config.data.labels[i]=label;config.data.datasets[i]={};config.data.datasets[i].label=label;config.data.datasets[i].borderColor=themes[colors[i]];config.data.datasets[i].backgroundColor=themes[colors[i]]+'36';config.data.datasets[i].borderWidth=2;config.data.datasets[i].data=[0,0,0,0,0,0,0];config.data.datasets[i].fill=true;if(!data){return;}
let dateFormat=(value.range&&range[value.range])?range[value.range]:'d F Y';for(let x=0;x<data.length;x++){if(data[x].value>highest){highest=data[x].value;}
let dateFormat=(value.range&&range[value.range])?range[value.range]:{year:'numeric',month:'short',day:'2-digit',};for(let x=0;x<data.length;x++){if(data[x].value>highest){highest=data[x].value;}
config.data.datasets[i].data[x]=data[x].value;config.data.labels[x]=date.format(dateFormat,data[x].date);}}
if(highest==0){config.options.scales.y.ticks.stepSize=1;config.options.scales.y.max=ticksCount;}else{highest=Math.ceil(highest/ticksCount)*ticksCount;config.options.scales.y.ticks.stepSize=highest/ticksCount;config.options.scales.y.max=highest;}
if(chart){chart.destroy();}
@ -828,7 +734,8 @@ var file=document.createElement("li");var image=document.createElement("img");im
result.bucketId+"/files/"+
result.fileId+"/preview?width="+
previewWidth+"&height="+
previewHeight+"&project="+project+"&mode=admin";image.alt=previewAlt;file.className="file avatar";file.tabIndex=0;file.appendChild(image);preview.appendChild(file);var remove=(function(result){return function(event){render(result.$id);element.value='';};})(result);file.addEventListener("click",remove);file.addEventListener("keypress",remove);element.value=JSON.stringify(result);};input.addEventListener("change",function(){var message=alerts.add({text:labelLoading,class:""},0);var files=input.files;var read=JSON.parse(expression.parse(element.dataset["read"]||"[]"));var write=JSON.parse(expression.parse(element.dataset["write"]||"[]"));sdk.storage.createFile('default','unique()',files[0],read,write).then(function(response){onComplete(message);render({bucketId:response.bucketId,fileId:response.$id});},function(error){alerts.add({text:"An error occurred!",class:""},3000);onComplete(message);});input.disabled=true;});element.addEventListener("change",function(){if(!element.value){return;}
previewHeight+"&project="+project+"&mode=admin";image.alt=previewAlt;file.className="file avatar";file.tabIndex=0;file.appendChild(image);preview.appendChild(file);var remove=(function(result){return function(event){render(result.$id);element.value='';};})(result);file.addEventListener("click",remove);file.addEventListener("keypress",remove);element.value=JSON.stringify(result);};input.addEventListener("change",function(){var message=alerts.add({text:labelLoading,class:""},0);var files=input.files;var permissions=JSON.parse(expression.parse(element.dataset["permissions"]||"[]"))
sdk.storage.createFile('default','unique()',files[0],permissions).then(function(response){onComplete(message);render({bucketId:response.bucketId,fileId:response.$id});},function(error){alerts.add({text:"An error occurred!",class:""},3000);onComplete(message);});input.disabled=true;});element.addEventListener("change",function(){if(!element.value){return;}
render(element.value);wrapper.scrollIntoView();});upload.addEventListener("keypress",function(){input.click();});element.parentNode.insertBefore(wrapper,element);wrapper.appendChild(preview);wrapper.appendChild(progress);wrapper.appendChild(upload);upload.appendChild(input);render(output);if(searchButton){let searchOpen=document.createElement("button");searchOpen.type='button';searchOpen.innerHTML='<i class="icon icon-search"></i> Search';searchOpen.classList.add('reverse');let path=container.scope(searchButton);searchOpen.addEventListener('click',function(){search.selected=element.value;search.path=path;document.dispatchEvent(new CustomEvent("open-file-search",{bubbles:false,cancelable:true}));});wrapper.appendChild(searchOpen);}}});})(window);(function(window){window.ls.container.get("view").add({selector:"data-cookies",controller:function(element,alerts,cookie,env){if(!cookie.get("cookie-alert")){let text=element.dataset["cookies"]||"";alerts.add({text:text,class:"cookie-alert",link:env.HOME+"/policy/cookies",label:'Learn More',callback:function(){cookie.set("cookie-alert","true",365*10);}},0);}}});})(window);(function(window){"use strict";window.ls.view.add({selector:'data-general-copy',repeat:false,controller:function(document,element,alerts){let button=document.createElement("i");button.type="button";button.title="Copy to Clipboard";button.className=element.getAttribute("data-class")||"icon-docs note copy";button.style.cursor="pointer";element.parentNode.insertBefore(button,element.nextSibling);let copy=function(event){window.getSelection().removeAllRanges();let range=document.createRange();range.selectNode(element);window.getSelection().addRange(range);try{document.execCommand("copy");alerts.add({text:"Copied to clipboard",class:""},3000);}catch(err){alerts.add({text:"Failed to copy text ",class:"error"},3000);}
window.getSelection().removeAllRanges();};button.addEventListener("click",copy);}});})(window);(function(window){window.ls.container.get("view").add({selector:"data-page-title",repeat:true,controller:function(element,document,expression){document.title=expression.parse(element.getAttribute("data-page-title"))||document.title;}});})(window);(function(window){"use strict";window.ls.view.add({selector:'data-general-scroll-to',repeat:false,controller:function(element,window){let button=window.document.createElement('button');button.className='scroll-to icon-up-dir';button.alt='Back To Top';button.title='Back To Top';button.addEventListener('click',function(){element.scrollIntoView(true,{behavior:'smooth'});button.blur();},false);element.appendChild(button);}});})(window);(function(window){"use strict";window.ls.view.add({selector:'data-general-scroll-direction',repeat:false,controller:function(element,window){let position=0;let check=function(){let direction=window.document.documentElement.scrollTop;if(direction>position){element.classList.remove('scroll-to-top')
element.classList.add('scroll-to-bottom')}

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View file

@ -1,29 +1,29 @@
(function (exports, isomorphicFormData, crossFetch) {
'use strict';
/******************************************************************************
Copyright (c) Microsoft Corporation.
Permission to use, copy, modify, and/or distribute this software for any
purpose with or without fee is hereby granted.
THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH
REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY
AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT,
INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM
LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR
OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
PERFORMANCE OF THIS SOFTWARE.
***************************************************************************** */
function __awaiter(thisArg, _arguments, P, generator) {
function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
return new (P || (P = Promise))(function (resolve, reject) {
function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
step((generator = generator.apply(thisArg, _arguments || [])).next());
});
/******************************************************************************
Copyright (c) Microsoft Corporation.
Permission to use, copy, modify, and/or distribute this software for any
purpose with or without fee is hereby granted.
THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH
REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY
AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT,
INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM
LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR
OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
PERFORMANCE OF THIS SOFTWARE.
***************************************************************************** */
function __awaiter(thisArg, _arguments, P, generator) {
function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
return new (P || (P = Promise))(function (resolve, reject) {
function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
step((generator = generator.apply(thisArg, _arguments || [])).next());
});
}
class Service {
@ -1867,13 +1867,12 @@
* @param {string} databaseId
* @param {string} collectionId
* @param {string} name
* @param {string} permission
* @param {string[]} read
* @param {string[]} write
* @param {string[]} permissions
* @param {boolean} documentSecurity
* @throws {AppwriteException}
* @returns {Promise}
*/
createCollection(databaseId, collectionId, name, permission, read, write) {
createCollection(databaseId, collectionId, name, permissions, documentSecurity) {
return __awaiter(this, void 0, void 0, function* () {
if (typeof databaseId === 'undefined') {
throw new AppwriteException('Missing required parameter: "databaseId"');
@ -1884,14 +1883,11 @@
if (typeof name === 'undefined') {
throw new AppwriteException('Missing required parameter: "name"');
}
if (typeof permission === 'undefined') {
throw new AppwriteException('Missing required parameter: "permission"');
if (typeof permissions === 'undefined') {
throw new AppwriteException('Missing required parameter: "permissions"');
}
if (typeof read === 'undefined') {
throw new AppwriteException('Missing required parameter: "read"');
}
if (typeof write === 'undefined') {
throw new AppwriteException('Missing required parameter: "write"');
if (typeof documentSecurity === 'undefined') {
throw new AppwriteException('Missing required parameter: "documentSecurity"');
}
let path = '/databases/{databaseId}/collections'.replace('{databaseId}', databaseId);
let payload = {};
@ -1901,14 +1897,11 @@
if (typeof name !== 'undefined') {
payload['name'] = name;
}
if (typeof permission !== 'undefined') {
payload['permission'] = permission;
if (typeof permissions !== 'undefined') {
payload['permissions'] = permissions;
}
if (typeof read !== 'undefined') {
payload['read'] = read;
}
if (typeof write !== 'undefined') {
payload['write'] = write;
if (typeof documentSecurity !== 'undefined') {
payload['documentSecurity'] = documentSecurity;
}
const uri = new URL(this.client.config.endpoint + path);
return yield this.client.call('post', uri, {
@ -1951,14 +1944,13 @@
* @param {string} databaseId
* @param {string} collectionId
* @param {string} name
* @param {string} permission
* @param {string[]} read
* @param {string[]} write
* @param {boolean} documentSecurity
* @param {string[]} permissions
* @param {boolean} enabled
* @throws {AppwriteException}
* @returns {Promise}
*/
updateCollection(databaseId, collectionId, name, permission, read, write, enabled) {
updateCollection(databaseId, collectionId, name, documentSecurity, permissions, enabled) {
return __awaiter(this, void 0, void 0, function* () {
if (typeof databaseId === 'undefined') {
throw new AppwriteException('Missing required parameter: "databaseId"');
@ -1969,22 +1961,19 @@
if (typeof name === 'undefined') {
throw new AppwriteException('Missing required parameter: "name"');
}
if (typeof permission === 'undefined') {
throw new AppwriteException('Missing required parameter: "permission"');
if (typeof documentSecurity === 'undefined') {
throw new AppwriteException('Missing required parameter: "documentSecurity"');
}
let path = '/databases/{databaseId}/collections/{collectionId}'.replace('{databaseId}', databaseId).replace('{collectionId}', collectionId);
let payload = {};
if (typeof name !== 'undefined') {
payload['name'] = name;
}
if (typeof permission !== 'undefined') {
payload['permission'] = permission;
if (typeof permissions !== 'undefined') {
payload['permissions'] = permissions;
}
if (typeof read !== 'undefined') {
payload['read'] = read;
}
if (typeof write !== 'undefined') {
payload['write'] = write;
if (typeof documentSecurity !== 'undefined') {
payload['documentSecurity'] = documentSecurity;
}
if (typeof enabled !== 'undefined') {
payload['enabled'] = enabled;
@ -2096,6 +2085,53 @@
}, payload);
});
}
/**
* Create DateTime Attribute
*
*
* @param {string} databaseId
* @param {string} collectionId
* @param {string} key
* @param {boolean} required
* @param {string} xdefault
* @param {boolean} array
* @throws {AppwriteException}
* @returns {Promise}
*/
createDatetimeAttribute(databaseId, collectionId, key, required, xdefault, array) {
return __awaiter(this, void 0, void 0, function* () {
if (typeof databaseId === 'undefined') {
throw new AppwriteException('Missing required parameter: "databaseId"');
}
if (typeof collectionId === 'undefined') {
throw new AppwriteException('Missing required parameter: "collectionId"');
}
if (typeof key === 'undefined') {
throw new AppwriteException('Missing required parameter: "key"');
}
if (typeof required === 'undefined') {
throw new AppwriteException('Missing required parameter: "required"');
}
let path = '/databases/{databaseId}/collections/{collectionId}/attributes/datetime'.replace('{databaseId}', databaseId).replace('{collectionId}', collectionId);
let payload = {};
if (typeof key !== 'undefined') {
payload['key'] = key;
}
if (typeof required !== 'undefined') {
payload['required'] = required;
}
if (typeof xdefault !== 'undefined') {
payload['default'] = xdefault;
}
if (typeof array !== 'undefined') {
payload['array'] = array;
}
const uri = new URL(this.client.config.endpoint + path);
return yield this.client.call('post', uri, {
'content-type': 'application/json',
}, payload);
});
}
/**
* Create Email Attribute
*
@ -2596,12 +2632,11 @@
* @param {string} collectionId
* @param {string} documentId
* @param {Omit<Document, keyof Models.Document>} data
* @param {string[]} read
* @param {string[]} write
* @param {string[]} permissions
* @throws {AppwriteException}
* @returns {Promise}
*/
createDocument(databaseId, collectionId, documentId, data, read, write) {
createDocument(databaseId, collectionId, documentId, data, permissions) {
return __awaiter(this, void 0, void 0, function* () {
if (typeof databaseId === 'undefined') {
throw new AppwriteException('Missing required parameter: "databaseId"');
@ -2623,11 +2658,8 @@
if (typeof data !== 'undefined') {
payload['data'] = data;
}
if (typeof read !== 'undefined') {
payload['read'] = read;
}
if (typeof write !== 'undefined') {
payload['write'] = write;
if (typeof permissions !== 'undefined') {
payload['permissions'] = permissions;
}
const uri = new URL(this.client.config.endpoint + path);
return yield this.client.call('post', uri, {
@ -2676,12 +2708,11 @@
* @param {string} collectionId
* @param {string} documentId
* @param {Partial<Omit<Document, keyof Models.Document>>} data
* @param {string[]} read
* @param {string[]} write
* @param {string[]} permissions
* @throws {AppwriteException}
* @returns {Promise}
*/
updateDocument(databaseId, collectionId, documentId, data, read, write) {
updateDocument(databaseId, collectionId, documentId, data, permissions) {
return __awaiter(this, void 0, void 0, function* () {
if (typeof databaseId === 'undefined') {
throw new AppwriteException('Missing required parameter: "databaseId"');
@ -2697,11 +2728,8 @@
if (typeof data !== 'undefined') {
payload['data'] = data;
}
if (typeof read !== 'undefined') {
payload['read'] = read;
}
if (typeof write !== 'undefined') {
payload['write'] = write;
if (typeof permissions !== 'undefined') {
payload['permissions'] = permissions;
}
const uri = new URL(this.client.config.endpoint + path);
return yield this.client.call('patch', uri, {
@ -4415,7 +4443,7 @@
* @param {string} projectId
* @param {string} name
* @param {string[]} scopes
* @param {number} expire
* @param {string} expire
* @throws {AppwriteException}
* @returns {Promise}
*/
@ -4480,7 +4508,7 @@
* @param {string} keyId
* @param {string} name
* @param {string[]} scopes
* @param {number} expire
* @param {string} expire
* @throws {AppwriteException}
* @returns {Promise}
*/
@ -5068,9 +5096,8 @@
*
* @param {string} bucketId
* @param {string} name
* @param {string} permission
* @param {string[]} read
* @param {string[]} write
* @param {boolean} fileSecurity
* @param {string[]} permissions
* @param {boolean} enabled
* @param {number} maximumFileSize
* @param {string[]} allowedFileExtensions
@ -5079,7 +5106,7 @@
* @throws {AppwriteException}
* @returns {Promise}
*/
createBucket(bucketId, name, permission, read, write, enabled, maximumFileSize, allowedFileExtensions, encryption, antivirus) {
createBucket(bucketId, name, fileSecurity, permissions, enabled, maximumFileSize, allowedFileExtensions, encryption, antivirus) {
return __awaiter(this, void 0, void 0, function* () {
if (typeof bucketId === 'undefined') {
throw new AppwriteException('Missing required parameter: "bucketId"');
@ -5087,8 +5114,8 @@
if (typeof name === 'undefined') {
throw new AppwriteException('Missing required parameter: "name"');
}
if (typeof permission === 'undefined') {
throw new AppwriteException('Missing required parameter: "permission"');
if (typeof fileSecurity === 'undefined') {
throw new AppwriteException('Missing required parameter: "fileSecurity"');
}
let path = '/storage/buckets';
let payload = {};
@ -5098,14 +5125,11 @@
if (typeof name !== 'undefined') {
payload['name'] = name;
}
if (typeof permission !== 'undefined') {
payload['permission'] = permission;
if (typeof permissions !== 'undefined') {
payload['permissions'] = permissions;
}
if (typeof read !== 'undefined') {
payload['read'] = read;
}
if (typeof write !== 'undefined') {
payload['write'] = write;
if (typeof fileSecurity !== 'undefined') {
payload['fileSecurity'] = fileSecurity;
}
if (typeof enabled !== 'undefined') {
payload['enabled'] = enabled;
@ -5158,9 +5182,8 @@
*
* @param {string} bucketId
* @param {string} name
* @param {string} permission
* @param {string[]} read
* @param {string[]} write
* @param {boolean} fileSecurity
* @param {string[]} permissions
* @param {boolean} enabled
* @param {number} maximumFileSize
* @param {string[]} allowedFileExtensions
@ -5169,7 +5192,7 @@
* @throws {AppwriteException}
* @returns {Promise}
*/
updateBucket(bucketId, name, permission, read, write, enabled, maximumFileSize, allowedFileExtensions, encryption, antivirus) {
updateBucket(bucketId, name, fileSecurity, permissions, enabled, maximumFileSize, allowedFileExtensions, encryption, antivirus) {
return __awaiter(this, void 0, void 0, function* () {
if (typeof bucketId === 'undefined') {
throw new AppwriteException('Missing required parameter: "bucketId"');
@ -5177,22 +5200,19 @@
if (typeof name === 'undefined') {
throw new AppwriteException('Missing required parameter: "name"');
}
if (typeof permission === 'undefined') {
throw new AppwriteException('Missing required parameter: "permission"');
if (typeof fileSecurity === 'undefined') {
throw new AppwriteException('Missing required parameter: "fileSecurity"');
}
let path = '/storage/buckets/{bucketId}'.replace('{bucketId}', bucketId);
let payload = {};
if (typeof name !== 'undefined') {
payload['name'] = name;
}
if (typeof permission !== 'undefined') {
payload['permission'] = permission;
if (typeof permissions !== 'undefined') {
payload['permissions'] = permissions;
}
if (typeof read !== 'undefined') {
payload['read'] = read;
}
if (typeof write !== 'undefined') {
payload['write'] = write;
if (typeof fileSecurity !== 'undefined') {
payload['fileSecurity'] = fileSecurity;
}
if (typeof enabled !== 'undefined') {
payload['enabled'] = enabled;
@ -5310,12 +5330,11 @@
* @param {string} bucketId
* @param {string} fileId
* @param {File} file
* @param {string[]} read
* @param {string[]} write
* @param {string[]} permissions
* @throws {AppwriteException}
* @returns {Promise}
*/
createFile(bucketId, fileId, file, read, write, onProgress = (progress) => { }) {
createFile(bucketId, fileId, file, permissions, onProgress = (progress) => { }) {
return __awaiter(this, void 0, void 0, function* () {
if (typeof bucketId === 'undefined') {
throw new AppwriteException('Missing required parameter: "bucketId"');
@ -5334,11 +5353,8 @@
if (typeof file !== 'undefined') {
payload['file'] = file;
}
if (typeof read !== 'undefined') {
payload['read'] = read;
}
if (typeof write !== 'undefined') {
payload['write'] = write;
if (typeof permissions !== 'undefined') {
payload['permissions'] = permissions;
}
const uri = new URL(this.client.config.endpoint + path);
if (!(file instanceof File)) {
@ -5426,12 +5442,11 @@
*
* @param {string} bucketId
* @param {string} fileId
* @param {string[]} read
* @param {string[]} write
* @param {string[]} permissions
* @throws {AppwriteException}
* @returns {Promise}
*/
updateFile(bucketId, fileId, read, write) {
updateFile(bucketId, fileId, permissions) {
return __awaiter(this, void 0, void 0, function* () {
if (typeof bucketId === 'undefined') {
throw new AppwriteException('Missing required parameter: "bucketId"');
@ -5441,11 +5456,8 @@
}
let path = '/storage/buckets/{bucketId}/files/{fileId}'.replace('{bucketId}', bucketId).replace('{fileId}', fileId);
let payload = {};
if (typeof read !== 'undefined') {
payload['read'] = read;
}
if (typeof write !== 'undefined') {
payload['write'] = write;
if (typeof permissions !== 'undefined') {
payload['permissions'] = permissions;
}
const uri = new URL(this.client.config.endpoint + path);
return yield this.client.call('put', uri, {

View file

@ -0,0 +1,122 @@
(function (window) {
document.addEventListener('alpine:init', () => {
Alpine.data('permissionsMatrix', () => ({
permissions: [],
rawPermissions: [],
load(permissions) {
if (permissions === undefined) {
return;
}
this.rawPermissions = permissions;
permissions.map(p => {
let {type, role} = this.parsePermission(p);
type = this.parseInputPermission(type);
let index = -1;
let existing = this.permissions.find((p, idx) => {
if (p.role === role) {
index = idx;
return true;
}
})
if (existing === undefined) {
this.permissions.push({
role,
[type]: true,
});
}
if (index !== -1) {
existing[type] = true;
this.permissions[index] = existing;
}
});
},
addPermission(formId, role, permissions) {
if (!document.getElementById(formId).reportValidity()) {
return;
}
Object.entries(permissions).forEach(entry => {
let [type, enabled] = entry;
type = this.parseOutputPermission(type);
if (enabled) {
this.rawPermissions.push(this.buildPermission(type, role));
}
});
this.permissions.push({
role,
...permissions,
});
this.reset();
},
updatePermission(index) {
// Because the x-model does not update before the click event,
// we setTimeout to give Alpine enough time to update the model.
setTimeout(() => {
const permission = this.permissions[index];
Object.keys(permission).forEach(key => {
if (key === 'role') {
return;
}
const parsedKey = this.parseOutputPermission(key);
const permissionString = this.buildPermission(parsedKey, permission.role);
if (permission[key]) {
if (!this.rawPermissions.includes(permissionString)) {
this.rawPermissions.push(permissionString);
}
} else {
this.rawPermissions = this.rawPermissions.filter(p => {
return !p.includes(permissionString);
});
}
});
});
},
removePermission(index) {
let row = this.permissions.splice(index, 1);
if (row.length === 1) {
this.rawPermissions = this.rawPermissions.filter(p => !p.includes(row[0].role));
}
},
parsePermission(permission) {
let parts = permission.split('(');
let type = parts[0];
let role = parts[1]
.replace(')', '')
.replace(' ', '')
.replaceAll('"', '');
return {type, role};
},
buildPermission(type, role) {
return `${type}("${role}")`
},
parseInputPermission(key) {
// Can't bind to a property named delete
if (key === 'delete') {
return 'xdelete';
}
return key;
},
parseOutputPermission(key) {
// Can't bind to a property named delete
if (key === 'xdelete') {
return 'delete';
}
return key;
}
}));
Alpine.data('permissionsRow', () => ({
role: '',
read: false,
create: false,
update: false,
xdelete: false,
reset() {
this.role = '';
this.read = this.create = this.update = this.xdelete = false;
}
}));
});
})(window);

View file

@ -3,9 +3,9 @@
window.ls.container.set('form', function () {
function cast(value, to) {
function cast(value, from, to,) {
if (value && Array.isArray(value) && to !== 'array') {
value = value.map(element => cast(element, to));
value = value.map(element => cast(element, from, to));
return value;
}
switch (to) {
@ -29,7 +29,18 @@
value = (value) ? JSON.parse(value) : [];
break;
case 'array':
value = (value && value.constructor && value.constructor === Array) ? value : [value];
if (value && value.constructor && value.constructor === Array) {
break;
}
if (from === 'csv') {
if (value.length === 0) {
value = [];
} else {
value = value.split(',');
}
} else {
value = [value];
}
break;
case 'array-empty':
value = [];
@ -49,6 +60,7 @@
let name = element.getAttribute('name');
let type = element.getAttribute('type');
let castTo = element.getAttribute('data-cast-to');
let castFrom = element.getAttribute('data-cast-from');
let ref = json;
if (name && 'FORM' !== element.tagName) {
@ -121,7 +133,7 @@
}
}
json[name] = cast(json[name], castTo); // Apply casting
json[name] = cast(json[name], castFrom, castTo); // Apply casting
}
}

View file

@ -65,16 +65,12 @@
const file = formData.get('file');
const fileId = formData.get('fileId');
let id = fileId === 'unique()' ? performance.now() : fileId;
let read = formData.get('read');
if(!file || !fileId) {
return;
}
if(read) {
read = JSON.parse(read);
}
let write = formData.get('write');
if(write) {
write = JSON.parse(write);
let permissions = formData.get('permissions');
if(permissions) {
permissions = permissions.split(',');
}
if(this.getFile(id)) {
@ -103,8 +99,7 @@
bucketId,
fileId,
file,
read,
write,
permissions,
(progress) => {
this.updateFile(id, {
id: progress.$id,

View file

@ -109,14 +109,11 @@
input.addEventListener("change", function() {
var message = alerts.add({ text: labelLoading, class: "" }, 0);
var files = input.files;
var read = JSON.parse(
expression.parse(element.dataset["read"] || "[]")
);
var write = JSON.parse(
expression.parse(element.dataset["write"] || "[]")
);
var permissions = JSON.parse(
expression.parse(element.dataset["permissions"] || "[]")
)
sdk.storage.createFile('default', 'unique()', files[0], read, write).then(
sdk.storage.createFile('default', 'unique()', files[0], permissions).then(
function(response) {
onComplete(message);

View file

@ -0,0 +1,24 @@
.permissions-matrix {
th:first-child, td:first-child {
width: 100px;
}
th:not(:first-child):not(:last-child), td:not(:first-child):not(:last-child) {
width: 50px;
text-align: center;
}
th:last-child, td:last-child {
width: 20px;
}
td {
vertical-align: middle;
}
input, p {
margin-bottom: 0;
}
p {
margin-left: 15px;
}
i {
cursor: pointer;
}
}

View file

@ -38,6 +38,7 @@ img[src=""] {
@import "comps/preview-box";
@import "comps/upload-box";
@import "comps/pill";
@import "comps/permissions-matrix";
html {
padding: 0;

View file

@ -11,6 +11,7 @@ use Appwrite\Auth\Hash\Scryptmodified;
use Appwrite\Auth\Hash\Sha;
use Utopia\Database\Document;
use Utopia\Database\DateTime;
use Utopia\Database\Role;
use Utopia\Database\Validator\Authorization;
class Auth
@ -32,13 +33,13 @@ class Auth
/**
* User Roles.
*/
public const USER_ROLE_ALL = 'all';
public const USER_ROLE_GUEST = 'guest';
public const USER_ROLE_MEMBER = 'member';
public const USER_ROLE_ANY = 'any';
public const USER_ROLE_GUESTS = 'guests';
public const USER_ROLE_USERS = 'users';
public const USER_ROLE_ADMIN = 'admin';
public const USER_ROLE_DEVELOPER = 'developer';
public const USER_ROLE_OWNER = 'owner';
public const USER_ROLE_APP = 'app';
public const USER_ROLE_APPS = 'apps';
public const USER_ROLE_SYSTEM = 'system';
/**
@ -316,7 +317,7 @@ class Auth
$token->isSet('expire') &&
$token->getAttribute('type') == $type &&
$token->getAttribute('secret') === self::hash($secret) &&
$token->getAttribute('expire') >= DateTime::now()
DateTime::formatTz($token->getAttribute('expire')) >= DateTime::formatTz(DateTime::now())
) {
return (string)$token->getId();
}
@ -335,7 +336,7 @@ class Auth
$token->isSet('expire') &&
$token->getAttribute('type') == Auth::TOKEN_TYPE_PHONE &&
$token->getAttribute('secret') === $secret &&
$token->getAttribute('expire') >= DateTime::now()
DateTime::formatTz($token->getAttribute('expire')) >= DateTime::formatTz(DateTime::now())
) {
return (string) $token->getId();
}
@ -361,7 +362,7 @@ class Auth
$session->isSet('expire') &&
$session->isSet('provider') &&
$session->getAttribute('secret') === self::hash($secret) &&
$session->getAttribute('expire') >= DateTime::now()
DateTime::formatTz($session->getAttribute('expire')) >= DateTime::formatTz(DateTime::now())
) {
return $session->getId();
}
@ -380,9 +381,9 @@ class Auth
public static function isPrivilegedUser(array $roles): bool
{
if (
in_array('role:' . self::USER_ROLE_OWNER, $roles) ||
in_array('role:' . self::USER_ROLE_DEVELOPER, $roles) ||
in_array('role:' . self::USER_ROLE_ADMIN, $roles)
in_array(self::USER_ROLE_OWNER, $roles) ||
in_array(self::USER_ROLE_DEVELOPER, $roles) ||
in_array(self::USER_ROLE_ADMIN, $roles)
) {
return true;
}
@ -399,7 +400,7 @@ class Auth
*/
public static function isAppUser(array $roles): bool
{
if (in_array('role:' . self::USER_ROLE_APP, $roles)) {
if (in_array(self::USER_ROLE_APPS, $roles)) {
return true;
}
@ -418,19 +419,19 @@ class Auth
if (!self::isPrivilegedUser(Authorization::getRoles()) && !self::isAppUser(Authorization::getRoles())) {
if ($user->getId()) {
$roles[] = 'user:' . $user->getId();
$roles[] = 'role:' . Auth::USER_ROLE_MEMBER;
$roles[] = Role::user($user->getId())->toString();
$roles[] = Role::users()->toString();
} else {
return ['role:' . Auth::USER_ROLE_GUEST];
return [Role::guests()->toString()];
}
}
foreach ($user->getAttribute('memberships', []) as $node) {
if (isset($node['teamId']) && isset($node['roles'])) {
$roles[] = 'team:' . $node['teamId'];
$roles[] = Role::team($node['teamId'])->toString();
foreach ($node['roles'] as $nodeRole) { // Set all team roles
$roles[] = 'team:' . $node['teamId'] . '/' . $nodeRole;
$roles[] = Role::team($node['teamId'], $nodeRole)->toString();
}
}
}

View file

@ -6,6 +6,8 @@ use Utopia\Database\DateTime;
use Utopia\Database\Document;
use Appwrite\Messaging\Adapter;
use Utopia\App;
use Utopia\Database\ID;
use Utopia\Database\Role;
class Realtime extends Adapter
{
@ -187,7 +189,7 @@ class Realtime extends Adapter
*/
if (
\array_key_exists($channel, $this->subscriptions[$event['project']][$role])
&& (\in_array($role, $event['roles']) || \in_array('role:all', $event['roles']))
&& (\in_array($role, $event['roles']) || \in_array(Role::any()->toString(), $event['roles']))
) {
/**
* Saving all connections that are allowed to receive this event.
@ -256,27 +258,25 @@ class Realtime extends Adapter
case 'users':
$channels[] = 'account';
$channels[] = 'account.' . $parts[1];
$roles = ['user:' . $parts[1]];
$roles = [Role::user(ID::custom($parts[1]))->toString()];
break;
case 'teams':
if ($parts[2] === 'memberships') {
$permissionsChanged = $parts[4] ?? false;
$channels[] = 'memberships';
$channels[] = 'memberships.' . $parts[3];
$roles = ['team:' . $parts[1]];
} else {
$permissionsChanged = $parts[2] === 'create';
$channels[] = 'teams';
$channels[] = 'teams.' . $parts[1];
$roles = ['team:' . $parts[1]];
}
$roles = [Role::team(ID::custom($parts[1]))->toString()];
break;
case 'databases':
if (in_array($parts[4] ?? [], ['attributes', 'indexes'])) {
$channels[] = 'console';
$projectId = 'console';
$roles = ['team:' . $project->getAttribute('teamId')];
$roles = [Role::team($project->getAttribute('teamId'))->toString()];
} elseif (($parts[4] ?? '') === 'documents') {
if ($database->isEmpty()) {
throw new \Exception('Database needs to be passed to Realtime for Document events in the Database.');
@ -289,18 +289,23 @@ class Realtime extends Adapter
$channels[] = 'databases.' . $database->getId() . '.collections.' . $payload->getCollection() . '.documents';
$channels[] = 'databases.' . $database->getId() . '.collections.' . $payload->getCollection() . '.documents.' . $payload->getId();
$roles = ($collection->getAttribute('permission') === 'collection') ? $collection->getRead() : $payload->getRead();
$roles = $collection->getAttribute('documentSecurity', false)
? \array_merge($collection->getRead(), $payload->getRead())
: $collection->getRead();
}
break;
case 'buckets':
if ($parts[2] === 'files') {
if ($bucket->isEmpty()) {
throw new \Exception('Bucket needs to be pased to Realtime for File events in the Storage.');
throw new \Exception('Bucket needs to be passed to Realtime for File events in the Storage.');
}
$channels[] = 'files';
$channels[] = 'buckets.' . $payload->getAttribute('bucketId') . '.files';
$channels[] = 'buckets.' . $payload->getAttribute('bucketId') . '.files.' . $payload->getId();
$roles = ($bucket->getAttribute('permission') === 'bucket') ? $bucket->getRead() : $payload->getRead();
$roles = $bucket->getAttribute('fileSecurity', false)
? \array_merge($bucket->getRead(), $payload->getRead())
: $bucket->getRead();
}
break;
@ -316,7 +321,8 @@ class Realtime extends Adapter
}
} elseif ($parts[2] === 'deployments') {
$channels[] = 'console';
$roles = ['team:' . $project->getAttribute('teamId')];
$roles = [Role::team($project->getAttribute('teamId'))->toString()];
}
break;

View file

@ -10,6 +10,7 @@ use Utopia\CLI\Console;
use Utopia\Config\Config;
use Exception;
use Utopia\App;
use Utopia\Database\ID;
use Utopia\Database\Validator\Authorization;
abstract class Migration
@ -63,15 +64,15 @@ abstract class Migration
Authorization::setDefaultStatus(false);
$this->collections = array_merge([
'_metadata' => [
'$id' => '_metadata',
'$id' => ID::custom('_metadata'),
'$collection' => Database::METADATA
],
'audit' => [
'$id' => 'audit',
'$id' => ID::custom('audit'),
'$collection' => Database::METADATA
],
'abuse' => [
'$id' => 'abuse',
'$id' => ID::custom('abuse'),
'$collection' => Database::METADATA
]
], Config::getParam('collections', []));

View file

@ -158,8 +158,8 @@ class V12 extends Migration
if (!$this->projectDB->findOne('buckets', [Query::equal('$id', ['default'])])) {
$this->projectDB->createDocument('buckets', new Document([
'$id' => 'default',
'$collection' => 'buckets',
'$id' => ID::custom('default'),
'$collection' => ID::custom('buckets'),
'dateCreated' => \time(),
'dateUpdated' => \time(),
'name' => 'Default',

View file

@ -67,7 +67,7 @@ class V14 extends Migration
try {
$this->projectDB->createDocument('databases', new Document([
'$id' => 'default',
'$id' => ID::custom('default'),
'name' => 'Default',
'search' => 'default Default'
]));

View file

@ -0,0 +1,62 @@
<?php
namespace Appwrite\Permissions;
use Utopia\Database\Database;
use Utopia\Database\Permission;
class PermissionsProcessor
{
public static function aggregate(?array $permissions, string $resource): ?array
{
if (\is_null($permissions)) {
return null;
}
$aggregates = self::getAggregates($resource);
foreach ($permissions as $i => $permission) {
$permission = Permission::parse($permission);
foreach ($aggregates as $type => $subTypes) {
if ($permission->getPermission() != $type) {
continue;
}
foreach ($subTypes as $subType) {
$permissions[] = (new Permission(
$subType,
$permission->getRole(),
$permission->getIdentifier(),
$permission->getDimension()
))->toString();
}
unset($permissions[$i]);
}
}
return $permissions;
}
private static function getAggregates($resource): array
{
$aggregates = [];
switch ($resource) {
case 'document':
case 'file':
$aggregates['write'] = [
Database::PERMISSION_UPDATE,
Database::PERMISSION_DELETE
];
break;
case 'collection':
case 'bucket':
$aggregates['write'] = [
Database::PERMISSION_CREATE,
Database::PERMISSION_UPDATE,
Database::PERMISSION_DELETE
];
break;
}
return $aggregates;
}
}

View file

@ -5,6 +5,8 @@ namespace Appwrite\Specification\Format;
use Appwrite\Specification\Format;
use Appwrite\Template\Template;
use Appwrite\Utopia\Response\Model;
use Utopia\Database\Permission;
use Utopia\Database\Role;
use Utopia\Validator;
class Swagger2 extends Format
@ -334,7 +336,15 @@ class Swagger2 extends Format
$node['items'] = [
'type' => 'string',
];
$node['x-example'] = '["role:all"]';
$node['x-example'] = '["' . Permission::read(Role::any()) . '"]';
break;
case 'Utopia\Database\Validator\Roles':
$node['type'] = $validator->getType();
$node['collectionFormat'] = 'multi';
$node['items'] = [
'type' => 'string',
];
$node['x-example'] = '["' . Role::any()->toString() . '"]';
break;
case 'Appwrite\Auth\Validator\Password':
$node['type'] = $validator->getType();

View file

@ -278,7 +278,7 @@ class V11 extends Filter
$content['rules'] = \array_map(function ($attribute) use ($content) {
return [
'$id' => $attribute['key'],
'$collection' => $content['$id'],
'$collection' => ID::custom($content['$id']),
'type' => $attribute['type'],
'key' => $attribute['key'],
'label' => $attribute['key'],

View file

@ -12,7 +12,7 @@ abstract class Model
public const TYPE_BOOLEAN = 'boolean';
public const TYPE_JSON = 'json';
public const TYPE_DATETIME = 'datetime';
public const TYPE_DATETIME_EXAMPLE = '2020-10-15T06:38:00.000Z';
public const TYPE_DATETIME_EXAMPLE = '2020-10-15T06:38:00.000+00:00';
/**
* @var bool

View file

@ -28,25 +28,18 @@ class Bucket extends Model
'default' => '',
'example' => self::TYPE_DATETIME_EXAMPLE,
])
->addRule('$read', [
->addRule('$permissions', [
'type' => self::TYPE_STRING,
'description' => 'File read permissions.',
'description' => 'File permissions.',
'default' => [],
'example' => ['role:all'],
'example' => ['read("any")'],
'array' => true,
])
->addRule('$write', [
->addRule('fileSecurity', [
'type' => self::TYPE_STRING,
'description' => 'File write permissions.',
'default' => [],
'example' => ['user:608f9da25e7e1'],
'array' => true,
])
->addRule('permission', [
'type' => self::TYPE_STRING,
'description' => 'Bucket permission model. Possible values: `bucket` or `file`',
'description' => 'Whether file-level security is enabled.',
'default' => '',
'example' => 'file',
'example' => true,
])
->addRule('name', [
'type' => self::TYPE_STRING,

View file

@ -28,18 +28,11 @@ class Collection extends Model
'default' => '',
'example' => self::TYPE_DATETIME_EXAMPLE,
])
->addRule('$read', [
->addRule('$permissions', [
'type' => self::TYPE_STRING,
'description' => 'Collection read permissions.',
'description' => 'Collection permissions.',
'default' => '',
'example' => 'role:all',
'array' => true
])
->addRule('$write', [
'type' => self::TYPE_STRING,
'description' => 'Collection write permissions.',
'default' => '',
'example' => 'user:608f9da25e7e1',
'example' => ['read("any")'],
'array' => true
])
->addRule('databaseId', [
@ -60,11 +53,11 @@ class Collection extends Model
'default' => true,
'example' => false,
])
->addRule('permission', [
'type' => self::TYPE_STRING,
'description' => 'Collection permission model. Possible values: `document` or `collection`',
->addRule('documentSecurity', [
'type' => self::TYPE_BOOLEAN,
'description' => 'Whether document-level permissions are enabled.',
'default' => '',
'example' => 'document',
'example' => true,
])
->addRule('attributes', [
'type' => [

View file

@ -54,18 +54,11 @@ class Document extends Any
'default' => '',
'example' => self::TYPE_DATETIME_EXAMPLE,
])
->addRule('$read', [
->addRule('$permissions', [
'type' => self::TYPE_STRING,
'description' => 'Document read permissions.',
'description' => 'Document permissions.',
'default' => '',
'example' => 'role:all',
'array' => true,
])
->addRule('$write', [
'type' => self::TYPE_STRING,
'description' => 'Document write permissions.',
'default' => '',
'example' => 'user:608f9da25e7e1',
'example' => ['read("any")'],
'array' => true,
])
;

View file

@ -4,6 +4,7 @@ namespace Appwrite\Utopia\Response\Model;
use Appwrite\Utopia\Response;
use Appwrite\Utopia\Response\Model;
use Utopia\Database\Role;
class Execution extends Model
{
@ -28,11 +29,11 @@ class Execution extends Model
'default' => '',
'example' => self::TYPE_DATETIME_EXAMPLE,
])
->addRule('$read', [
->addRule('$permissions', [
'type' => self::TYPE_STRING,
'description' => 'Execution read permissions.',
'description' => 'Execution roles.',
'default' => '',
'example' => 'role:all',
'example' => [Role::any()->toString()],
'array' => true,
])
->addRule('functionId', [

View file

@ -34,18 +34,11 @@ class File extends Model
'default' => '',
'example' => self::TYPE_DATETIME_EXAMPLE,
])
->addRule('$read', [
->addRule('$permissions', [
'type' => self::TYPE_STRING,
'description' => 'File read permissions.',
'description' => 'File permissions.',
'default' => [],
'example' => 'role:all',
'array' => true,
])
->addRule('$write', [
'type' => self::TYPE_STRING,
'description' => 'File write permissions.',
'default' => [],
'example' => 'user:608f9da25e7e1',
'example' => ['read("any")'],
'array' => true,
])
->addRule('name', [

View file

@ -34,7 +34,7 @@ class Func extends Model
'type' => self::TYPE_STRING,
'description' => 'Execution permissions.',
'default' => [],
'example' => 'role:member',
'example' => 'users',
'array' => true,
])
->addRule('name', [

View file

@ -2,12 +2,14 @@
namespace Tests\E2E\Scopes;
use Utopia\Database\ID;
trait ProjectConsole
{
public function getProject(): array
{
return [
'$id' => 'console',
'$id' => ID::custom('console'),
'name' => 'Appwrite',
'apiKey' => '',
];

View file

@ -3,6 +3,7 @@
namespace Tests\E2E\Scopes;
use Tests\E2E\Client;
use Utopia\Database\ID;
trait ProjectCustom
{
@ -26,7 +27,7 @@ trait ProjectCustom
'cookie' => 'a_session_console=' . $this->getRoot()['session'],
'x-appwrite-project' => 'console',
], [
'teamId' => 'unique()',
'teamId' => ID::unique(),
'name' => 'Demo Project Team',
]);
$this->assertEquals(201, $team['headers']['status-code']);
@ -39,7 +40,7 @@ trait ProjectCustom
'cookie' => 'a_session_console=' . $this->getRoot()['session'],
'x-appwrite-project' => 'console',
], [
'projectId' => 'unique()',
'projectId' => ID::unique(),
'name' => 'Demo Project',
'teamId' => $team['body']['$id'],
'description' => 'Demo Project Description',

View file

@ -4,6 +4,7 @@ namespace Tests\E2E\Scopes;
use Tests\E2E\Client;
use PHPUnit\Framework\TestCase;
use Utopia\Database\ID;
abstract class Scope extends TestCase
{
@ -87,7 +88,7 @@ abstract class Scope extends TestCase
'content-type' => 'application/json',
'x-appwrite-project' => 'console',
], [
'userId' => 'unique()',
'userId' => ID::unique(),
'email' => $email,
'password' => $password,
'name' => $name,
@ -107,7 +108,7 @@ abstract class Scope extends TestCase
$session = $this->client->parseCookie((string)$session['headers']['set-cookie'])['a_session_console'];
self::$root = [
'$id' => $root['body']['$id'],
'$id' => ID::custom($root['body']['$id']),
'name' => $root['body']['name'],
'email' => $root['body']['email'],
'session' => $session,
@ -139,7 +140,7 @@ abstract class Scope extends TestCase
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
], [
'userId' => 'unique()',
'userId' => ID::unique(),
'email' => $email,
'password' => $password,
'name' => $name,
@ -159,7 +160,7 @@ abstract class Scope extends TestCase
$session = $this->client->parseCookie((string)$session['headers']['set-cookie'])['a_session_' . $this->getProject()['$id']];
self::$user[$this->getProject()['$id']] = [
'$id' => $user['body']['$id'],
'$id' => ID::custom($user['body']['$id']),
'name' => $user['body']['name'],
'email' => $user['body']['email'],
'session' => $session,

View file

@ -3,6 +3,7 @@
namespace Tests\E2E\Services\Account;
use Tests\E2E\Client;
use Utopia\Database\ID;
use Utopia\Database\DateTime;
trait AccountBase
@ -21,7 +22,7 @@ trait AccountBase
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
]), [
'userId' => 'unique()',
'userId' => ID::unique(),
'email' => $email,
'password' => $password,
'name' => $name,
@ -44,7 +45,7 @@ trait AccountBase
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
]), [
'userId' => 'unique()',
'userId' => ID::unique(),
'email' => $email,
'password' => $password,
'name' => $name,
@ -57,7 +58,7 @@ trait AccountBase
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
]), [
'userId' => 'unique()',
'userId' => ID::unique(),
'email' => '',
'password' => '',
]);
@ -69,7 +70,7 @@ trait AccountBase
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
]), [
'userId' => 'unique()',
'userId' => ID::unique(),
'email' => $email,
'password' => '',
]);
@ -81,7 +82,7 @@ trait AccountBase
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
]), [
'userId' => 'unique()',
'userId' => ID::unique(),
'email' => '',
'password' => $password,
]);
@ -664,7 +665,7 @@ trait AccountBase
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
]), [
'userId' => 'unique()',
'userId' => ID::unique(),
'email' => $data['email'],
'password' => $data['password'],
'name' => $data['name'],
@ -824,7 +825,7 @@ trait AccountBase
$this->assertEquals('Account Verification', $lastEmail['subject']);
$verification = substr($lastEmail['text'], strpos($lastEmail['text'], '&secret=', 0) + 8, 256);
$expireTime = strpos($lastEmail['text'], 'expire=' . urlencode($response['body']['expire']), 0);
$expireTime = strpos($lastEmail['text'], 'expire=' . urlencode(DateTime::format(new \DateTime($response['body']['expire']))), 0);
$this->assertNotFalse($expireTime);
$secretTest = strpos($lastEmail['text'], 'secret=' . $response['body']['secret'], 0);
@ -898,7 +899,7 @@ trait AccountBase
'x-appwrite-project' => $this->getProject()['$id'],
'cookie' => 'a_session_' . $this->getProject()['$id'] . '=' . $session,
]), [
'userId' => 'ewewe',
'userId' => ID::custom('ewewe'),
'secret' => $verification,
]);
@ -1127,7 +1128,7 @@ trait AccountBase
$recovery = substr($lastEmail['text'], strpos($lastEmail['text'], '&secret=', 0) + 8, 256);
$expireTime = strpos($lastEmail['text'], 'expire=' . urlencode($response['body']['expire']), 0);
$expireTime = strpos($lastEmail['text'], 'expire=' . urlencode(DateTime::format(new \DateTime($response['body']['expire']))), 0);
$this->assertNotFalse($expireTime);
@ -1213,7 +1214,7 @@ trait AccountBase
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
]), [
'userId' => 'ewewe',
'userId' => ID::custom('ewewe'),
'secret' => $recovery,
'password' => $newPassowrd,
'passwordAgain' => $newPassowrd,
@ -1262,7 +1263,7 @@ trait AccountBase
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
]), [
'userId' => 'unique()',
'userId' => ID::unique(),
'email' => $email,
// 'url' => 'http://localhost/magiclogin',
]);
@ -1280,7 +1281,7 @@ trait AccountBase
$token = substr($lastEmail['text'], strpos($lastEmail['text'], '&secret=', 0) + 8, 256);
$expireTime = strpos($lastEmail['text'], 'expire=' . urlencode($response['body']['expire']), 0);
$expireTime = strpos($lastEmail['text'], 'expire=' . urlencode(DateTime::format(new \DateTime($response['body']['expire']))), 0);
$this->assertNotFalse($expireTime);
@ -1300,7 +1301,7 @@ trait AccountBase
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
]), [
'userId' => 'unique()',
'userId' => ID::unique(),
'email' => $email,
'url' => 'localhost/magiclogin',
]);
@ -1312,7 +1313,7 @@ trait AccountBase
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
]), [
'userId' => 'unique()',
'userId' => ID::unique(),
'email' => $email,
'url' => 'http://remotehost/magiclogin',
]);
@ -1388,7 +1389,7 @@ trait AccountBase
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
]), [
'userId' => 'ewewe',
'userId' => ID::custom('ewewe'),
'secret' => $token,
]);

View file

@ -8,6 +8,7 @@ use Tests\E2E\Scopes\Scope;
use Tests\E2E\Scopes\ProjectCustom;
use Tests\E2E\Scopes\SideClient;
use Utopia\Database\DateTime;
use Utopia\Database\ID;
use function sleep;
@ -71,7 +72,7 @@ class AccountCustomClientTest extends Scope
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
]), [
'userId' => 'unique()',
'userId' => ID::unique(),
'email' => $email,
'password' => $password,
'name' => $name,
@ -152,7 +153,7 @@ class AccountCustomClientTest extends Scope
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
]), [
'userId' => 'unique()',
'userId' => ID::unique(),
'email' => $email,
'password' => $password,
'name' => $name,
@ -231,7 +232,7 @@ class AccountCustomClientTest extends Scope
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
]), [
'userId' => 'unique()',
'userId' => ID::unique(),
'email' => $email,
'password' => $password,
'name' => $name,
@ -411,7 +412,7 @@ class AccountCustomClientTest extends Scope
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
]), [
'userId' => 'unique()',
'userId' => ID::unique(),
'email' => $email,
'password' => $password
]);
@ -690,7 +691,7 @@ class AccountCustomClientTest extends Scope
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
]), [
'userId' => 'unique()',
'userId' => ID::unique(),
'phone' => $number,
]);
@ -709,7 +710,7 @@ class AccountCustomClientTest extends Scope
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
]), [
'userId' => 'unique()'
'userId' => ID::unique()
]);
$this->assertEquals(400, $response['headers']['status-code']);
@ -738,7 +739,7 @@ class AccountCustomClientTest extends Scope
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
]), [
'userId' => 'ewewe',
'userId' => ID::custom('ewewe'),
'secret' => $token,
]);
@ -964,7 +965,7 @@ class AccountCustomClientTest extends Scope
'x-appwrite-project' => $this->getProject()['$id'],
'cookie' => 'a_session_' . $this->getProject()['$id'] . '=' . $session,
]), [
'userId' => 'ewewe',
'userId' => ID::custom('ewewe'),
'secret' => Mock::$digits,
]);

View file

@ -6,6 +6,7 @@ use Tests\E2E\Client;
use Tests\E2E\Scopes\ProjectCustom;
use Tests\E2E\Scopes\Scope;
use Tests\E2E\Scopes\SideServer;
use Utopia\Database\ID;
class AccountCustomServerTest extends Scope
{
@ -26,7 +27,7 @@ class AccountCustomServerTest extends Scope
'x-appwrite-project' => $this->getProject()['$id'],
'x-appwrite-key' => $this->getProject()['apiKey'],
], [
'userId' => 'unique()',
'userId' => ID::unique(),
'email' => $email,
'password' => $password,
'name' => $name,

File diff suppressed because it is too large Load diff

View file

@ -6,6 +6,9 @@ use Tests\E2E\Scopes\Scope;
use Tests\E2E\Scopes\ProjectCustom;
use Tests\E2E\Client;
use Tests\E2E\Scopes\SideConsole;
use Utopia\Database\ID;
use Utopia\Database\Permission;
use Utopia\Database\Role;
class DatabasesConsoleClientTest extends Scope
{
@ -19,7 +22,7 @@ class DatabasesConsoleClientTest extends Scope
'x-appwrite-project' => $this->getProject()['$id'],
'x-appwrite-key' => $this->getProject()['apiKey']
]), [
'databaseId' => 'unique()',
'databaseId' => ID::unique(),
'name' => 'invalidDocumentDatabase',
]);
$this->assertEquals(201, $database['headers']['status-code']);
@ -33,14 +36,18 @@ class DatabasesConsoleClientTest extends Scope
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
], $this->getHeaders()), [
'collectionId' => 'unique()',
'collectionId' => ID::unique(),
'name' => 'Movies',
'read' => ['role:all'],
'write' => ['role:all'],
'permission' => 'document',
'permissions' => [
Permission::read(Role::any()),
Permission::create(Role::any()),
Permission::update(Role::any()),
Permission::delete(Role::any()),
],
'documentSecurity' => true,
]);
$this->assertEquals($movies['headers']['status-code'], 201);
$this->assertEquals(201, $movies['headers']['status-code']);
$this->assertEquals($movies['body']['name'], 'Movies');
return ['moviesId' => $movies['body']['$id'], 'databaseId' => $databaseId];
@ -63,7 +70,7 @@ class DatabasesConsoleClientTest extends Scope
// 'range' => '32h'
// ]);
// $this->assertEquals($response['headers']['status-code'], 400);
// $this->assertEquals(400, $response['headers']['status-code']);
// /**
// * Test for SUCCESS
@ -76,7 +83,7 @@ class DatabasesConsoleClientTest extends Scope
// 'range' => '24h'
// ]);
// $this->assertEquals($response['headers']['status-code'], 200);
// $this->assertEquals(200, $response['headers']['status-code']);
// $this->assertEquals(count($response['body']), 11);
// $this->assertEquals($response['body']['range'], '24h');
// $this->assertIsArray($response['body']['documentsCount']);
@ -109,7 +116,7 @@ class DatabasesConsoleClientTest extends Scope
'range' => '32h'
]);
$this->assertEquals($response['headers']['status-code'], 400);
$this->assertEquals(400, $response['headers']['status-code']);
$response = $this->client->call(Client::METHOD_GET, '/databases/' . $databaseId . '/collections/randomCollectionId/usage', array_merge([
'content-type' => 'application/json',
@ -118,7 +125,7 @@ class DatabasesConsoleClientTest extends Scope
'range' => '24h'
]);
$this->assertEquals($response['headers']['status-code'], 404);
$this->assertEquals(404, $response['headers']['status-code']);
/**
* Test for SUCCESS
@ -130,7 +137,7 @@ class DatabasesConsoleClientTest extends Scope
'range' => '24h'
]);
$this->assertEquals($response['headers']['status-code'], 200);
$this->assertEquals(200, $response['headers']['status-code']);
$this->assertEquals(count($response['body']), 6);
$this->assertEquals($response['body']['range'], '24h');
$this->assertIsArray($response['body']['documentsCount']);
@ -154,7 +161,7 @@ class DatabasesConsoleClientTest extends Scope
'x-appwrite-project' => $this->getProject()['$id'],
], $this->getHeaders()));
$this->assertEquals($logs['headers']['status-code'], 200);
$this->assertEquals(200, $logs['headers']['status-code']);
$this->assertIsArray($logs['body']['logs']);
$this->assertIsNumeric($logs['body']['total']);
@ -165,7 +172,7 @@ class DatabasesConsoleClientTest extends Scope
'limit' => 1
]);
$this->assertEquals($logs['headers']['status-code'], 200);
$this->assertEquals(200, $logs['headers']['status-code']);
$this->assertIsArray($logs['body']['logs']);
$this->assertLessThanOrEqual(1, count($logs['body']['logs']));
$this->assertIsNumeric($logs['body']['total']);
@ -177,7 +184,7 @@ class DatabasesConsoleClientTest extends Scope
'offset' => 1
]);
$this->assertEquals($logs['headers']['status-code'], 200);
$this->assertEquals(200, $logs['headers']['status-code']);
$this->assertIsArray($logs['body']['logs']);
$this->assertIsNumeric($logs['body']['total']);
@ -189,7 +196,7 @@ class DatabasesConsoleClientTest extends Scope
'limit' => 1
]);
$this->assertEquals($logs['headers']['status-code'], 200);
$this->assertEquals(200, $logs['headers']['status-code']);
$this->assertIsArray($logs['body']['logs']);
$this->assertLessThanOrEqual(1, count($logs['body']['logs']));
$this->assertIsNumeric($logs['body']['total']);

View file

@ -6,6 +6,9 @@ use Tests\E2E\Client;
use Tests\E2E\Scopes\Scope;
use Tests\E2E\Scopes\ProjectCustom;
use Tests\E2E\Scopes\SideClient;
use Utopia\Database\ID;
use Utopia\Database\Permission;
use Utopia\Database\Role;
class DatabasesCustomClientTest extends Scope
{
@ -32,7 +35,7 @@ class DatabasesCustomClientTest extends Scope
'x-appwrite-project' => $this->getProject()['$id'],
'x-appwrite-key' => $this->getProject()['apiKey']
]), [
'databaseId' => 'permissionCheckDatabase',
'databaseId' => ID::custom('permissionCheckDatabase'),
'name' => 'Test Database',
]);
$this->assertEquals(201, $database['headers']['status-code']);
@ -45,11 +48,10 @@ class DatabasesCustomClientTest extends Scope
'x-appwrite-project' => $this->getProject()['$id'],
'x-appwrite-key' => $this->getProject()['apiKey']
]), [
'collectionId' => 'permissionCheck',
'collectionId' => ID::custom('permissionCheck'),
'name' => 'permissionCheck',
'read' => [],
'write' => [],
'permission' => 'document'
'permissions' => [],
'documentSecurity' => true,
]);
$this->assertEquals(201, $response['headers']['status-code']);
@ -74,13 +76,18 @@ class DatabasesCustomClientTest extends Scope
'x-appwrite-project' => $this->getProject()['$id'],
'x-appwrite-key' => $this->getProject()['apiKey']
]), [
'documentId' => 'permissionCheckDocument',
'documentId' => ID::custom('permissionCheckDocument'),
'data' => [
'name' => 'AppwriteBeginner',
],
'read' => ['user:' . $userId, 'user:user2'],
'write' => ['user:' . $userId],
'permissions' => [
Permission::read(Role::user(ID::custom('user2'))),
Permission::read(Role::user($userId)),
Permission::update(Role::user($userId)),
Permission::delete(Role::user($userId)),
],
]);
$this->assertEquals(201, $response['headers']['status-code']);
// Update document
@ -93,6 +100,7 @@ class DatabasesCustomClientTest extends Scope
'name' => 'AppwriteExpert',
]
]);
$this->assertEquals(200, $response['headers']['status-code']);
// Get name of the document, should be the new one

View file

@ -7,6 +7,9 @@ use Tests\E2E\Scopes\Scope;
use Tests\E2E\Scopes\SideServer;
use Tests\E2E\Client;
use Utopia\Database\Database;
use Utopia\Database\ID;
use Utopia\Database\Permission;
use Utopia\Database\Role;
class DatabasesCustomServerTest extends Scope
{
@ -21,7 +24,7 @@ class DatabasesCustomServerTest extends Scope
'x-appwrite-project' => $this->getProject()['$id'],
'x-appwrite-key' => $this->getProject()['apiKey']
]), [
'databaseId' => 'first',
'databaseId' => ID::custom('first'),
'name' => 'Test 1',
]);
$this->assertEquals(201, $test1['headers']['status-code']);
@ -32,7 +35,7 @@ class DatabasesCustomServerTest extends Scope
'x-appwrite-project' => $this->getProject()['$id'],
'x-appwrite-key' => $this->getProject()['apiKey']
]), [
'databaseId' => 'second',
'databaseId' => ID::custom('second'),
'name' => 'Test 2',
]);
$this->assertEquals(201, $test2['headers']['status-code']);
@ -172,7 +175,7 @@ class DatabasesCustomServerTest extends Scope
'x-appwrite-key' => $this->getProject()['apiKey']
]), [
'name' => 'Test 1',
'databaseId' => 'first',
'databaseId' => ID::custom('first'),
]);
$this->assertEquals(409, $response['headers']['status-code']);
@ -233,7 +236,7 @@ class DatabasesCustomServerTest extends Scope
'x-appwrite-project' => $this->getProject()['$id'],
'x-appwrite-key' => $this->getProject()['apiKey']
]), [
'databaseId' => 'unique()',
'databaseId' => ID::unique(),
'name' => 'invalidDocumentDatabase',
]);
$this->assertEquals(201, $database['headers']['status-code']);
@ -249,10 +252,14 @@ class DatabasesCustomServerTest extends Scope
'x-appwrite-key' => $this->getProject()['apiKey']
]), [
'name' => 'Test 1',
'collectionId' => 'first',
'read' => ['role:all'],
'write' => ['role:all'],
'permission' => 'document'
'collectionId' => ID::custom('first'),
'permissions' => [
Permission::read(Role::any()),
Permission::create(Role::any()),
Permission::update(Role::any()),
Permission::delete(Role::any()),
],
'documentSecurity' => true,
]);
$test2 = $this->client->call(Client::METHOD_POST, '/databases/' . $databaseId . '/collections', array_merge([
@ -261,10 +268,14 @@ class DatabasesCustomServerTest extends Scope
'x-appwrite-key' => $this->getProject()['apiKey']
]), [
'name' => 'Test 2',
'collectionId' => 'second',
'read' => ['role:all'],
'write' => ['role:all'],
'permission' => 'document'
'collectionId' => ID::custom('second'),
'permissions' => [
Permission::read(Role::any()),
Permission::create(Role::any()),
Permission::update(Role::any()),
Permission::delete(Role::any()),
],
'documentSecurity' => true,
]);
$collections = $this->client->call(Client::METHOD_GET, '/databases/' . $databaseId . '/collections', array_merge([
@ -392,7 +403,7 @@ class DatabasesCustomServerTest extends Scope
'cursor' => 'unknown',
]);
$this->assertEquals($response['headers']['status-code'], 400);
$this->assertEquals(400, $response['headers']['status-code']);
// This collection already exists
$response = $this->client->call(Client::METHOD_POST, '/databases/' . $databaseId . '/collections', array_merge([
@ -401,13 +412,17 @@ class DatabasesCustomServerTest extends Scope
'x-appwrite-key' => $this->getProject()['apiKey']
]), [
'name' => 'Test 1',
'collectionId' => 'first',
'read' => ['role:all'],
'write' => ['role:all'],
'permission' => 'document'
'collectionId' => ID::custom('first'),
'permissions' => [
Permission::read(Role::any()),
Permission::create(Role::any()),
Permission::update(Role::any()),
Permission::delete(Role::any()),
],
'documentSecurity' => true,
]);
$this->assertEquals($response['headers']['status-code'], 409);
$this->assertEquals(409, $response['headers']['status-code']);
}
public function testDeleteAttribute(): array
@ -417,7 +432,7 @@ class DatabasesCustomServerTest extends Scope
'x-appwrite-project' => $this->getProject()['$id'],
'x-appwrite-key' => $this->getProject()['apiKey']
]), [
'databaseId' => 'unique()',
'databaseId' => ID::unique(),
'name' => 'invalidDocumentDatabase',
]);
$this->assertEquals(201, $database['headers']['status-code']);
@ -434,14 +449,18 @@ class DatabasesCustomServerTest extends Scope
'x-appwrite-project' => $this->getProject()['$id'],
'x-appwrite-key' => $this->getProject()['apiKey']
]), [
'collectionId' => 'unique()',
'collectionId' => ID::unique(),
'name' => 'Actors',
'read' => ['role:all'],
'write' => ['role:all'],
'permission' => 'document'
'permissions' => [
Permission::read(Role::any()),
Permission::create(Role::any()),
Permission::update(Role::any()),
Permission::delete(Role::any()),
],
'documentSecurity' => true,
]);
$this->assertEquals($actors['headers']['status-code'], 201);
$this->assertEquals(201, $actors['headers']['status-code']);
$this->assertEquals($actors['body']['name'], 'Actors');
$firstName = $this->client->call(Client::METHOD_POST, '/databases/' . $databaseId . '/collections/' . $actors['body']['$id'] . '/attributes/string', array_merge([
@ -483,14 +502,17 @@ class DatabasesCustomServerTest extends Scope
'x-appwrite-project' => $this->getProject()['$id'],
'x-appwrite-key' => $this->getProject()['apiKey']
]), [
'documentId' => 'unique()',
'documentId' => ID::unique(),
'data' => [
'firstName' => 'lorem',
'lastName' => 'ipsum',
'unneeded' => 'dolor'
],
'read' => ['role:all'],
'write' => ['role:all'],
'permissions' => [
Permission::read(Role::any()),
Permission::update(Role::any()),
Permission::delete(Role::any()),
],
]);
$index = $this->client->call(Client::METHOD_POST, '/databases/' . $databaseId . '/collections/' . $actors['body']['$id'] . '/indexes', array_merge([
@ -516,7 +538,7 @@ class DatabasesCustomServerTest extends Scope
$unneededId = $unneeded['body']['key'];
$this->assertEquals($collection['headers']['status-code'], 200);
$this->assertEquals(200, $collection['headers']['status-code']);
$this->assertIsArray($collection['body']['attributes']);
$this->assertCount(3, $collection['body']['attributes']);
$this->assertEquals($collection['body']['attributes'][0]['key'], $firstName['body']['key']);
@ -532,7 +554,7 @@ class DatabasesCustomServerTest extends Scope
'x-appwrite-key' => $this->getProject()['apiKey']
]));
$this->assertEquals($attribute['headers']['status-code'], 204);
$this->assertEquals(204, $attribute['headers']['status-code']);
sleep(2);
@ -551,7 +573,7 @@ class DatabasesCustomServerTest extends Scope
'x-appwrite-key' => $this->getProject()['apiKey']
]), []);
$this->assertEquals($collection['headers']['status-code'], 200);
$this->assertEquals(200, $collection['headers']['status-code']);
$this->assertIsArray($collection['body']['attributes']);
$this->assertCount(2, $collection['body']['attributes']);
$this->assertEquals($collection['body']['attributes'][0]['key'], $firstName['body']['key']);
@ -576,7 +598,7 @@ class DatabasesCustomServerTest extends Scope
'x-appwrite-key' => $this->getProject()['apiKey']
]));
$this->assertEquals($index['headers']['status-code'], 204);
$this->assertEquals(204, $index['headers']['status-code']);
// Wait for database worker to finish deleting index
sleep(2);
@ -660,7 +682,7 @@ class DatabasesCustomServerTest extends Scope
'x-appwrite-key' => $this->getProject()['apiKey']
]));
$this->assertEquals($deleted['headers']['status-code'], 204);
$this->assertEquals(204, $deleted['headers']['status-code']);
// wait for database worker to complete
sleep(2);
@ -686,7 +708,7 @@ class DatabasesCustomServerTest extends Scope
'x-appwrite-key' => $this->getProject()['apiKey']
]));
$this->assertEquals($deleted['headers']['status-code'], 204);
$this->assertEquals(204, $deleted['headers']['status-code']);
return $data;
}
@ -698,7 +720,7 @@ class DatabasesCustomServerTest extends Scope
'x-appwrite-project' => $this->getProject()['$id'],
'x-appwrite-key' => $this->getProject()['apiKey']
]), [
'databaseId' => 'unique()',
'databaseId' => ID::unique(),
'name' => 'invalidDocumentDatabase',
]);
$this->assertEquals(201, $database['headers']['status-code']);
@ -710,11 +732,15 @@ class DatabasesCustomServerTest extends Scope
'x-appwrite-project' => $this->getProject()['$id'],
'x-appwrite-key' => $this->getProject()['apiKey']
]), [
'collectionId' => 'unique()',
'collectionId' => ID::unique(),
'name' => 'TestCleanupDuplicateIndexOnDeleteAttribute',
'read' => ['role:all'],
'write' => ['role:all'],
'permission' => 'document',
'permissions' => [
Permission::read(Role::any()),
Permission::create(Role::any()),
Permission::update(Role::any()),
Permission::delete(Role::any()),
],
'documentSecurity' => true,
]);
$this->assertEquals(201, $collection['headers']['status-code']);
@ -784,7 +810,7 @@ class DatabasesCustomServerTest extends Scope
'x-appwrite-key' => $this->getProject()['apiKey']
]));
$this->assertEquals($deleted['headers']['status-code'], 204);
$this->assertEquals(204, $deleted['headers']['status-code']);
// wait for database worker to complete
sleep(2);
@ -810,7 +836,7 @@ class DatabasesCustomServerTest extends Scope
'x-appwrite-key' => $this->getProject()['apiKey']
]));
$this->assertEquals($deleted['headers']['status-code'], 204);
$this->assertEquals(204, $deleted['headers']['status-code']);
}
/**
@ -826,41 +852,43 @@ class DatabasesCustomServerTest extends Scope
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
], $this->getHeaders()), [
'documentId' => 'unique()',
'documentId' => ID::unique(),
'data' => [
'firstName' => 'Tom',
'lastName' => 'Holland',
],
'read' => ['user:' . $this->getUser()['$id']],
'write' => ['user:' . $this->getUser()['$id']],
'permissions' => [
Permission::read(Role::user($this->getUser()['$id'])),
Permission::update(Role::user($this->getUser()['$id'])),
Permission::delete(Role::user($this->getUser()['$id'])),
],
]);
$document2 = $this->client->call(Client::METHOD_POST, '/databases/' . $databaseId . '/collections/' . $collectionId . '/documents', array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
], $this->getHeaders()), [
'documentId' => 'unique()',
'documentId' => ID::unique(),
'data' => [
'firstName' => 'Samuel',
'lastName' => 'Jackson',
],
'read' => ['user:' . $this->getUser()['$id']],
'write' => ['user:' . $this->getUser()['$id']],
'permissions' => [
Permission::read(Role::user($this->getUser()['$id'])),
Permission::update(Role::user($this->getUser()['$id'])),
Permission::delete(Role::user($this->getUser()['$id'])),
],
]);
$this->assertEquals($document1['headers']['status-code'], 201);
$this->assertIsArray($document1['body']['$read']);
$this->assertIsArray($document1['body']['$write']);
$this->assertCount(1, $document1['body']['$read']);
$this->assertCount(1, $document1['body']['$write']);
$this->assertEquals(201, $document1['headers']['status-code']);
$this->assertIsArray($document1['body']['$permissions']);
$this->assertCount(3, $document1['body']['$permissions']);
$this->assertEquals($document1['body']['firstName'], 'Tom');
$this->assertEquals($document1['body']['lastName'], 'Holland');
$this->assertEquals($document2['headers']['status-code'], 201);
$this->assertIsArray($document2['body']['$read']);
$this->assertIsArray($document2['body']['$write']);
$this->assertCount(1, $document2['body']['$read']);
$this->assertCount(1, $document2['body']['$write']);
$this->assertEquals(201, $document2['headers']['status-code']);
$this->assertIsArray($document2['body']['$permissions']);
$this->assertCount(3, $document2['body']['$permissions']);
$this->assertEquals($document2['body']['firstName'], 'Samuel');
$this->assertEquals($document2['body']['lastName'], 'Jackson');
@ -871,7 +899,7 @@ class DatabasesCustomServerTest extends Scope
'x-appwrite-key' => $this->getProject()['apiKey']
], $this->getHeaders()));
$this->assertEquals($response['headers']['status-code'], 204);
$this->assertEquals(204, $response['headers']['status-code']);
$this->assertEquals($response['body'], "");
// Try to get the collection and check if it has been deleted
@ -880,7 +908,7 @@ class DatabasesCustomServerTest extends Scope
'x-appwrite-project' => $this->getProject()['$id']
], $this->getHeaders()));
$this->assertEquals($response['headers']['status-code'], 404);
$this->assertEquals(404, $response['headers']['status-code']);
}
// Adds several minutes to test to replicate coverage in Utopia\Database unit tests
@ -899,11 +927,11 @@ class DatabasesCustomServerTest extends Scope
// 'x-appwrite-project' => $this->getProject()['$id'],
// 'x-appwrite-key' => $this->getProject()['apiKey']
// ]), [
// 'collectionId' => 'unique()',
// 'collectionId' => ID::unique(),
// 'name' => 'attributeCountLimit',
// 'read' => ['role:all'],
// 'write' => ['role:all'],
// 'permission' => 'document',
// 'read' => ['any'],
// 'write' => ['any'],
// 'documentSecurity' => true,
// ]);
// $collectionId = $collection['body']['$id'];
@ -944,7 +972,7 @@ class DatabasesCustomServerTest extends Scope
'x-appwrite-project' => $this->getProject()['$id'],
'x-appwrite-key' => $this->getProject()['apiKey']
]), [
'databaseId' => 'unique()',
'databaseId' => ID::unique(),
'name' => 'invalidDocumentDatabase',
]);
$this->assertEquals(201, $database['headers']['status-code']);
@ -956,14 +984,18 @@ class DatabasesCustomServerTest extends Scope
'x-appwrite-project' => $this->getProject()['$id'],
'x-appwrite-key' => $this->getProject()['apiKey']
]), [
'collectionId' => 'attributeRowWidthLimit',
'collectionId' => ID::custom('attributeRowWidthLimit'),
'name' => 'attributeRowWidthLimit',
'read' => ['role:all'],
'write' => ['role:all'],
'permission' => 'document',
'permissions' => [
Permission::read(Role::any()),
Permission::create(Role::any()),
Permission::update(Role::any()),
Permission::delete(Role::any()),
],
'documentSecurity' => true,
]);
$this->assertEquals($collection['headers']['status-code'], 201);
$this->assertEquals(201, $collection['headers']['status-code']);
$this->assertEquals($collection['body']['name'], 'attributeRowWidthLimit');
$collectionId = $collection['body']['$id'];
@ -980,7 +1012,7 @@ class DatabasesCustomServerTest extends Scope
'required' => true,
]);
$this->assertEquals($attribute['headers']['status-code'], 202);
$this->assertEquals(202, $attribute['headers']['status-code']);
}
sleep(5);
@ -1006,7 +1038,7 @@ class DatabasesCustomServerTest extends Scope
'x-appwrite-project' => $this->getProject()['$id'],
'x-appwrite-key' => $this->getProject()['apiKey']
]), [
'databaseId' => 'unique()',
'databaseId' => ID::unique(),
'name' => 'invalidDocumentDatabase',
]);
$this->assertEquals(201, $database['headers']['status-code']);
@ -1018,14 +1050,18 @@ class DatabasesCustomServerTest extends Scope
'x-appwrite-project' => $this->getProject()['$id'],
'x-appwrite-key' => $this->getProject()['apiKey']
]), [
'collectionId' => 'testLimitException',
'collectionId' => ID::custom('testLimitException'),
'name' => 'testLimitException',
'read' => ['role:all'],
'write' => ['role:all'],
'permission' => 'document',
'permissions' => [
Permission::read(Role::any()),
Permission::create(Role::any()),
Permission::update(Role::any()),
Permission::delete(Role::any()),
],
'documentSecurity' => true,
]);
$this->assertEquals($collection['headers']['status-code'], 201);
$this->assertEquals(201, $collection['headers']['status-code']);
$this->assertEquals($collection['body']['name'], 'testLimitException');
$collectionId = $collection['body']['$id'];
@ -1043,7 +1079,7 @@ class DatabasesCustomServerTest extends Scope
'required' => true,
]);
$this->assertEquals($attribute['headers']['status-code'], 202);
$this->assertEquals(202, $attribute['headers']['status-code']);
}
sleep(20);
@ -1054,7 +1090,7 @@ class DatabasesCustomServerTest extends Scope
'x-appwrite-key' => $this->getProject()['apiKey']
]));
$this->assertEquals($collection['headers']['status-code'], 200);
$this->assertEquals(200, $collection['headers']['status-code']);
$this->assertEquals($collection['body']['name'], 'testLimitException');
$this->assertIsArray($collection['body']['attributes']);
$this->assertIsArray($collection['body']['indexes']);
@ -1092,7 +1128,7 @@ class DatabasesCustomServerTest extends Scope
'x-appwrite-key' => $this->getProject()['apiKey']
]));
$this->assertEquals($collection['headers']['status-code'], 200);
$this->assertEquals(200, $collection['headers']['status-code']);
$this->assertEquals($collection['body']['name'], 'testLimitException');
$this->assertIsArray($collection['body']['attributes']);
$this->assertIsArray($collection['body']['indexes']);

View file

@ -6,6 +6,9 @@ use Tests\E2E\Client;
use Tests\E2E\Scopes\Scope;
use Tests\E2E\Scopes\ProjectCustom;
use Tests\E2E\Scopes\SideClient;
use Utopia\Database\ID;
use Utopia\Database\Permission;
use Utopia\Database\Role;
class DatabasesPermissionsGuestTest extends Scope
{
@ -20,7 +23,7 @@ class DatabasesPermissionsGuestTest extends Scope
'x-appwrite-project' => $this->getProject()['$id'],
'x-appwrite-key' => $this->getProject()['apiKey']
]), [
'databaseId' => 'unique()',
'databaseId' => ID::unique(),
'name' => 'InvalidDocumentDatabase',
]);
$this->assertEquals(201, $database['headers']['status-code']);
@ -28,11 +31,15 @@ class DatabasesPermissionsGuestTest extends Scope
$databaseId = $database['body']['$id'];
$movies = $this->client->call(Client::METHOD_POST, '/databases/' . $databaseId . '/collections', $this->getServerHeader(), [
'collectionId' => 'unique()',
'collectionId' => ID::unique(),
'name' => 'Movies',
'read' => ['role:all'],
'write' => ['role:all'],
'permission' => 'document',
'permissions' => [
Permission::read(Role::any()),
Permission::create(Role::any()),
Permission::update(Role::any()),
Permission::delete(Role::any()),
],
'documentSecurity' => true,
]);
$collection = ['id' => $movies['body']['$id']];
@ -49,36 +56,36 @@ class DatabasesPermissionsGuestTest extends Scope
}
/**
* [string[] $read, string[] $write]
* [string[] $permissions]
*/
public function readDocumentsProvider()
{
return [
[['role:all'], []],
[['role:member'], []],
[[] ,['role:all']],
[['role:all'], ['role:all']],
[['role:member'], ['role:member']],
[['role:all'], ['role:member']],
[[Permission::read(Role::any())]],
[[Permission::read(Role::users())]],
[[Permission::update(Role::any()), Permission::delete(Role::any())]],
[[Permission::read(Role::any()), Permission::update(Role::any()), Permission::delete(Role::any())]],
[[Permission::read(Role::users()), Permission::update(Role::users()), Permission::delete(Role::users())]],
[[Permission::read(Role::any()), Permission::update(Role::users()), Permission::delete(Role::users())]],
];
}
/**
* @dataProvider readDocumentsProvider
*/
public function testReadDocuments($read, $write)
public function testReadDocuments($permissions)
{
$data = $this->createCollection();
$collectionId = $data['collectionId'];
$databaseId = $data['databaseId'];
$response = $this->client->call(Client::METHOD_POST, '/databases/' . $databaseId . '/collections/' . $collectionId . '/documents', $this->getServerHeader(), [
'documentId' => 'unique()',
'documentId' => ID::unique(),
'data' => [
'title' => 'Lorem',
],
'read' => $read,
'write' => $write,
'permissions' => $permissions,
]);
$this->assertEquals(201, $response['headers']['status-code']);
$documents = $this->client->call(Client::METHOD_GET, '/databases/' . $databaseId . '/collections/' . $collectionId . '/documents', [
@ -87,7 +94,13 @@ class DatabasesPermissionsGuestTest extends Scope
]);
foreach ($documents['body']['documents'] as $document) {
$this->assertContains('role:all', $document['$read']);
foreach ($document['$permissions'] as $permission) {
$permission = Permission::parse($permission);
if ($permission->getPermission() != 'read') {
continue;
}
$this->assertEquals($permission->getRole(), Role::any()->toString());
}
}
}
}

View file

@ -6,6 +6,9 @@ use Tests\E2E\Client;
use Tests\E2E\Scopes\Scope;
use Tests\E2E\Scopes\ProjectCustom;
use Tests\E2E\Scopes\SideClient;
use Utopia\Database\ID;
use Utopia\Database\Permission;
use Utopia\Database\Role;
class DatabasesPermissionsMemberTest extends Scope
{
@ -23,38 +26,36 @@ class DatabasesPermissionsMemberTest extends Scope
];
}
/**
* [string[] $read, string[] $write]
*/
public function readDocumentsProvider()
public function permissionsProvider(): array
{
return [
[['role:all'], []],
[['role:member'], []],
[['user:random'], []],
[['user:lorem'] ,['user:lorem']],
[['user:dolor'] ,['user:dolor']],
[['user:dolor', 'user:lorem'] ,['user:dolor']],
[[], ['role:all']],
[['role:all'], ['role:all']],
[['role:member'], ['role:member']],
[['role:all'], ['role:member']],
[[Permission::read(Role::any())]],
[[Permission::read(Role::users())]],
[[Permission::read(Role::user(ID::custom('random')))]],
[[Permission::read(Role::user(ID::custom('lorem'))), Permission::update(Role::user('lorem')), Permission::delete(Role::user('lorem'))]],
[[Permission::read(Role::user(ID::custom('dolor'))), Permission::update(Role::user('dolor')), Permission::delete(Role::user('dolor'))]],
[[Permission::read(Role::user(ID::custom('dolor'))), Permission::read(Role::user('lorem')), Permission::update(Role::user('dolor')), Permission::delete(Role::user('dolor'))]],
[[Permission::update(Role::any()), Permission::delete(Role::any())]],
[[Permission::read(Role::any()), Permission::update(Role::any()), Permission::delete(Role::any())]],
[[Permission::read(Role::users()), Permission::update(Role::users()), Permission::delete(Role::users())]],
[[Permission::read(Role::any()), Permission::update(Role::users()), Permission::delete(Role::users())]],
];
}
/**
* Setup database
*
* Data providers lose object state
* so explicitly pass [$users, $collections] to each iteration
* Data providers lose object state so explicitly pass [$users, $collections] to each iteration
*
* @return array
* @throws \Exception
*/
public function testSetupDatabase(): array
{
$this->createUsers();
$db = $this->client->call(Client::METHOD_POST, '/databases', $this->getServerHeader(), [
'databaseId' => 'unique()',
'databaseId' => ID::unique(),
'name' => 'Test Database',
]);
$this->assertEquals(201, $db['headers']['status-code']);
@ -62,11 +63,15 @@ class DatabasesPermissionsMemberTest extends Scope
$databaseId = $db['body']['$id'];
$public = $this->client->call(Client::METHOD_POST, '/databases/' . $databaseId . '/collections', $this->getServerHeader(), [
'collectionId' => 'unique()',
'collectionId' => ID::unique(),
'name' => 'Movies',
'read' => ['role:all'],
'write' => ['role:all'],
'permission' => 'document',
'permissions' => [
Permission::read(Role::any()),
Permission::create(Role::any()),
Permission::update(Role::any()),
Permission::delete(Role::any()),
],
'documentSecurity' => true,
]);
$this->assertEquals(201, $public['headers']['status-code']);
@ -80,11 +85,15 @@ class DatabasesPermissionsMemberTest extends Scope
$this->assertEquals(202, $response['headers']['status-code']);
$private = $this->client->call(Client::METHOD_POST, '/databases/' . $databaseId . '/collections', $this->getServerHeader(), [
'collectionId' => 'unique()',
'collectionId' => ID::unique(),
'name' => 'Private Movies',
'read' => ['role:member'],
'write' => ['role:member'],
'permission' => 'document',
'permissions' => [
Permission::read(Role::users()),
Permission::create(Role::users()),
Permission::update(Role::users()),
Permission::delete(Role::users()),
],
'documentSecurity' => true,
]);
$this->assertEquals(201, $private['headers']['status-code']);
@ -108,37 +117,35 @@ class DatabasesPermissionsMemberTest extends Scope
/**
* Data provider params are passed before test dependencies
* @dataProvider readDocumentsProvider
* @dataProvider permissionsProvider
* @depends testSetupDatabase
*/
public function testReadDocuments($read, $write, $data)
public function testReadDocuments($permissions, $data)
{
$users = $data['users'];
$collections = $data['collections'];
$databaseId = $data['databaseId'];
$response = $this->client->call(Client::METHOD_POST, '/databases/' . $databaseId . '/collections/' . $collections['public'] . '/documents', $this->getServerHeader(), [
'documentId' => 'unique()',
'documentId' => ID::unique(),
'data' => [
'title' => 'Lorem',
],
'read' => $read,
'write' => $write,
'permissions' => $permissions
]);
$this->assertEquals(201, $response['headers']['status-code']);
$response = $this->client->call(Client::METHOD_POST, '/databases/' . $databaseId . '/collections/' . $collections['private'] . '/documents', $this->getServerHeader(), [
'documentId' => 'unique()',
'documentId' => ID::unique(),
'data' => [
'title' => 'Lorem',
],
'read' => $read,
'write' => $write,
'permissions' => $permissions
]);
$this->assertEquals(201, $response['headers']['status-code']);
/**
* Check role:all collection
* Check "any" collection
*/
$documents = $this->client->call(Client::METHOD_GET, '/databases/' . $databaseId . '/collections/' . $collections['public'] . '/documents', [
'origin' => 'http://localhost',
@ -148,9 +155,23 @@ class DatabasesPermissionsMemberTest extends Scope
]);
foreach ($documents['body']['documents'] as $document) {
$hasPermissions = \array_reduce(['role:all', 'role:member', 'user:' . $users['user1']['$id']], function ($carry, $item) use ($document) {
return $carry ? true : \in_array($item, $document['$read']);
$hasPermissions = \array_reduce([
Role::any()->toString(),
Role::users()->toString(),
Role::user($users['user1']['$id'])->toString(),
], function (bool $carry, string $role) use ($document) {
if ($carry) {
return true;
}
foreach ($document['$permissions'] as $permission) {
$permission = Permission::parse($permission);
if ($permission->getPermission() == 'read' && $permission->getRole() == $role) {
return true;
}
}
return false;
}, false);
$this->assertTrue($hasPermissions);
}
@ -165,9 +186,23 @@ class DatabasesPermissionsMemberTest extends Scope
]);
foreach ($documents['body']['documents'] as $document) {
$hasPermissions = \array_reduce(['role:all', 'role:member', 'user:' . $users['user1']['$id']], function ($carry, $item) use ($document) {
return $carry ? true : \in_array($item, $document['$read']);
$hasPermissions = \array_reduce([
Role::any()->toString(),
Role::users()->toString(),
Role::user($users['user1']['$id'])->toString(),
], function (bool $carry, string $role) use ($document) {
if ($carry) {
return true;
}
foreach ($document['$permissions'] as $permission) {
$permission = Permission::parse($permission);
if ($permission->getPermission() == 'read' && $permission->getRole() == $role) {
return true;
}
}
return false;
}, false);
$this->assertTrue($hasPermissions);
}
}

View file

@ -6,6 +6,9 @@ use Tests\E2E\Client;
use Tests\E2E\Scopes\Scope;
use Tests\E2E\Scopes\ProjectCustom;
use Tests\E2E\Scopes\SideClient;
use Utopia\Database\ID;
use Utopia\Database\Permission;
use Utopia\Database\Role;
class DatabasesPermissionsTeamTest extends Scope
{
@ -42,11 +45,14 @@ class DatabasesPermissionsTeamTest extends Scope
$this->assertEquals(201, $db['headers']['status-code']);
$collection1 = $this->client->call(Client::METHOD_POST, '/databases/' . $this->databaseId . '/collections', $this->getServerHeader(), [
'collectionId' => 'collection1',
'collectionId' => ID::custom('collection1'),
'name' => 'Collection 1',
'read' => ['team:' . $teams['team1']['$id']],
'write' => ['team:' . $teams['team1']['$id'] . '/admin'],
'permission' => 'collection',
'permissions' => [
Permission::read(Role::team($teams['team1']['$id'])),
Permission::create(Role::team($teams['team1']['$id'], 'admin')),
Permission::update(Role::team($teams['team1']['$id'], 'admin')),
Permission::delete(Role::team($teams['team1']['$id'], 'admin')),
],
]);
$this->collections['collection1'] = $collection1['body']['$id'];
@ -58,11 +64,14 @@ class DatabasesPermissionsTeamTest extends Scope
]);
$collection2 = $this->client->call(Client::METHOD_POST, '/databases/' . $this->databaseId . '/collections', $this->getServerHeader(), [
'collectionId' => 'collection2',
'collectionId' => ID::custom('collection2'),
'name' => 'Collection 2',
'read' => ['team:' . $teams['team2']['$id']],
'write' => ['team:' . $teams['team2']['$id'] . '/owner'],
'permission' => 'collection',
'permissions' => [
Permission::read(Role::team($teams['team2']['$id'])),
Permission::create(Role::team($teams['team2']['$id'], 'owner')),
Permission::update(Role::team($teams['team2']['$id'], 'owner')),
Permission::delete(Role::team($teams['team2']['$id'], 'owner')),
]
]);
$this->collections['collection2'] = $collection2['body']['$id'];
@ -132,7 +141,7 @@ class DatabasesPermissionsTeamTest extends Scope
$this->createCollections($this->teams);
$response = $this->client->call(Client::METHOD_POST, '/databases/' . $this->databaseId . '/collections/' . $this->collections['collection1'] . '/documents', $this->getServerHeader(), [
'documentId' => 'unique()',
'documentId' => ID::unique(),
'data' => [
'title' => 'Lorem',
],
@ -140,7 +149,7 @@ class DatabasesPermissionsTeamTest extends Scope
$this->assertEquals(201, $response['headers']['status-code']);
$response = $this->client->call(Client::METHOD_POST, '/databases/' . $this->databaseId . '/collections/' . $this->collections['collection2'] . '/documents', $this->getServerHeader(), [
'documentId' => 'unique()',
'documentId' => ID::unique(),
'data' => [
'title' => 'Ipsum',
],
@ -183,7 +192,7 @@ class DatabasesPermissionsTeamTest extends Scope
'x-appwrite-project' => $this->getProject()['$id'],
'cookie' => 'a_session_' . $this->getProject()['$id'] . '=' . $users[$user]['session'],
], [
'documentId' => 'unique()',
'documentId' => ID::unique(),
'data' => [
'title' => 'Ipsum',
],

View file

@ -6,6 +6,7 @@ use Tests\E2E\Scopes\Scope;
use Tests\E2E\Scopes\ProjectCustom;
use Tests\E2E\Client;
use Tests\E2E\Scopes\SideConsole;
use Utopia\Database\ID;
class FunctionsConsoleClientTest extends Scope
{
@ -18,9 +19,9 @@ class FunctionsConsoleClientTest extends Scope
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
], $this->getHeaders()), [
'functionId' => 'unique()',
'functionId' => ID::unique(),
'name' => 'Test',
'execute' => ['user:' . $this->getUser()['$id']],
'execute' => ["user:{$this->getUser()['$id']}"],
'runtime' => 'php-8.0',
'vars' => [
'funcKey1' => 'funcValue1',
@ -41,7 +42,7 @@ class FunctionsConsoleClientTest extends Scope
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
], $this->getHeaders()), [
'functionId' => 'unique()',
'functionId' => ID::unique(),
'name' => 'Test Failure',
'execute' => ['some-random-string'],
'runtime' => 'php-8.0'

View file

@ -9,6 +9,8 @@ use Tests\E2E\Scopes\Scope;
use Tests\E2E\Scopes\SideClient;
use Utopia\CLI\Console;
use Utopia\Database\Database;
use Utopia\Database\ID;
use Utopia\Database\Role;
class FunctionsCustomClientTest extends Scope
{
@ -25,7 +27,7 @@ class FunctionsCustomClientTest extends Scope
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
], $this->getHeaders()), [
'functionId' => 'unique()',
'functionId' => ID::unique(),
'name' => 'Test',
'vars' => [
'funcKey1' => 'funcValue1',
@ -55,9 +57,9 @@ class FunctionsCustomClientTest extends Scope
'x-appwrite-project' => $this->getProject()['$id'],
'x-appwrite-key' => $this->getProject()['apiKey'],
], [
'functionId' => 'unique()',
'functionId' => ID::unique(),
'name' => 'Test',
'execute' => ['user:' . $this->getUser()['$id']],
'execute' => ["user:{$this->getUser()['$id']}"],
'runtime' => 'php-8.0',
'vars' => [
'funcKey1' => 'funcValue1',
@ -145,9 +147,9 @@ class FunctionsCustomClientTest extends Scope
'x-appwrite-project' => $projectId,
'x-appwrite-key' => $apikey,
], [
'functionId' => 'unique()',
'functionId' => ID::unique(),
'name' => 'Test',
'execute' => ['role:all'],
'execute' => [Role::any()->toString()],
'runtime' => 'php-8.0',
'vars' => [
'funcKey1' => 'funcValue1',
@ -236,7 +238,7 @@ class FunctionsCustomClientTest extends Scope
'x-appwrite-project' => $this->getProject()['$id'],
'x-appwrite-key' => $this->getProject()['apiKey'],
], [
'functionId' => 'unique()',
'functionId' => ID::unique(),
'name' => 'Test',
'execute' => [],
'runtime' => 'php-8.0',
@ -330,9 +332,9 @@ class FunctionsCustomClientTest extends Scope
'x-appwrite-project' => $projectId,
'x-appwrite-key' => $apikey,
], [
'functionId' => 'unique()',
'functionId' => ID::unique(),
'name' => 'Test',
'execute' => ['role:all'],
'execute' => [Role::any()->toString()],
'runtime' => 'php-8.0',
'vars' => [
'funcKey1' => 'funcValue1',

View file

@ -10,6 +10,7 @@ use Tests\E2E\Scopes\SideServer;
use Utopia\CLI\Console;
use Utopia\Database\Database;
use Utopia\Database\DateTime;
use Utopia\Database\ID;
class FunctionsCustomServerTest extends Scope
{
@ -26,7 +27,7 @@ class FunctionsCustomServerTest extends Scope
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
], $this->getHeaders()), [
'functionId' => 'unique()',
'functionId' => ID::unique(),
'name' => 'Test',
'runtime' => 'php-8.0',
'vars' => [
@ -124,7 +125,7 @@ class FunctionsCustomServerTest extends Scope
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
], $this->getHeaders()), [
'functionId' => 'unique()',
'functionId' => ID::unique(),
'name' => 'Test 2',
'runtime' => 'php-8.0',
'vars' => [
@ -712,7 +713,7 @@ class FunctionsCustomServerTest extends Scope
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
], $this->getHeaders()), [
'functionId' => 'unique()',
'functionId' => ID::unique(),
'name' => 'Test ' . $name,
'runtime' => $name,
'vars' => [],
@ -797,7 +798,7 @@ class FunctionsCustomServerTest extends Scope
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
], $this->getHeaders()), [
'functionId' => 'unique()',
'functionId' => ID::unique(),
'name' => 'Test ' . $name,
'runtime' => $name,
'vars' => [],
@ -907,7 +908,7 @@ class FunctionsCustomServerTest extends Scope
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
], $this->getHeaders()), [
'functionId' => 'unique()',
'functionId' => ID::unique(),
'name' => 'Test ' . $name,
'runtime' => $name,
'vars' => [
@ -1012,7 +1013,7 @@ class FunctionsCustomServerTest extends Scope
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
], $this->getHeaders()), [
'functionId' => 'unique()',
'functionId' => ID::unique(),
'name' => 'Test ' . $name,
'runtime' => $name,
'vars' => [
@ -1117,7 +1118,7 @@ class FunctionsCustomServerTest extends Scope
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
], $this->getHeaders()), [
'functionId' => 'unique()',
'functionId' => ID::unique(),
'name' => 'Test ' . $name,
'runtime' => $name,
'vars' => [
@ -1222,7 +1223,7 @@ class FunctionsCustomServerTest extends Scope
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
], $this->getHeaders()), [
'functionId' => 'unique()',
'functionId' => ID::unique(),
'name' => 'Test ' . $name,
'runtime' => $name,
'vars' => [
@ -1327,7 +1328,7 @@ class FunctionsCustomServerTest extends Scope
// 'content-type' => 'application/json',
// 'x-appwrite-project' => $this->getProject()['$id'],
// ], $this->getHeaders()), [
// 'functionId' => 'unique()',
// 'functionId' => ID::unique(),
// 'name' => 'Test '.$name,
// 'runtime' => $name,
// 'vars' => [

View file

@ -10,6 +10,7 @@ use Tests\E2E\Services\Projects\ProjectsBase;
use Tests\E2E\Client;
use Utopia\Database\Database;
use Utopia\Database\DateTime;
use Utopia\Database\ID;
class ProjectsConsoleClientTest extends Scope
{
@ -26,7 +27,7 @@ class ProjectsConsoleClientTest extends Scope
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
], $this->getHeaders()), [
'teamId' => 'unique()',
'teamId' => ID::unique(),
'name' => 'Project Test',
]);
@ -38,7 +39,7 @@ class ProjectsConsoleClientTest extends Scope
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
], $this->getHeaders()), [
'projectId' => 'unique()',
'projectId' => ID::unique(),
'name' => 'Project Test',
'teamId' => $team['body']['$id'],
]);
@ -60,7 +61,7 @@ class ProjectsConsoleClientTest extends Scope
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
], $this->getHeaders()), [
'projectId' => 'unique()',
'projectId' => ID::unique(),
'name' => '',
'teamId' => $team['body']['$id'],
]);
@ -71,7 +72,7 @@ class ProjectsConsoleClientTest extends Scope
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
], $this->getHeaders()), [
'projectId' => 'unique()',
'projectId' => ID::unique(),
'name' => 'Project Test',
]);
@ -137,7 +138,7 @@ class ProjectsConsoleClientTest extends Scope
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
], $this->getHeaders()), [
'teamId' => 'unique()',
'teamId' => ID::unique(),
'name' => 'Project Test 2',
]);
@ -149,7 +150,7 @@ class ProjectsConsoleClientTest extends Scope
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
], $this->getHeaders()), [
'projectId' => 'unique()',
'projectId' => ID::unique(),
'name' => 'Project Test 2',
'teamId' => $team['body']['$id'],
]);
@ -316,7 +317,7 @@ class ProjectsConsoleClientTest extends Scope
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
], $this->getHeaders()), [
'projectId' => 'unique()',
'projectId' => ID::unique(),
'name' => 'Project Test 2',
]);
@ -337,7 +338,7 @@ class ProjectsConsoleClientTest extends Scope
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
], $this->getHeaders()), [
'projectId' => 'unique()',
'projectId' => ID::unique(),
'name' => '',
]);
@ -395,7 +396,7 @@ class ProjectsConsoleClientTest extends Scope
'x-appwrite-project' => $this->getProject()['$id'],
], $this->getHeaders()), [
'provider' => 'unknown',
'appId' => 'AppId',
'appId' => ID::custom('AppId'),
'secret' => 'Secret',
]);
@ -421,7 +422,7 @@ class ProjectsConsoleClientTest extends Scope
'content-type' => 'application/json',
'x-appwrite-project' => $id,
]), [
'userId' => 'unique()',
'userId' => ID::unique(),
'email' => $originalEmail,
'password' => $originalPassword,
'name' => $originalName,
@ -474,7 +475,7 @@ class ProjectsConsoleClientTest extends Scope
'content-type' => 'application/json',
'x-appwrite-project' => $id,
]), [
'userId' => 'unique()',
'userId' => ID::unique(),
'email' => $email,
'password' => $password,
'name' => $name,
@ -487,7 +488,7 @@ class ProjectsConsoleClientTest extends Scope
'x-appwrite-project' => $id,
'cookie' => 'a_session_' . $id . '=' . $session,
]), [
'teamId' => 'unique()',
'teamId' => ID::unique(),
'name' => 'Arsenal'
]);
@ -581,7 +582,7 @@ class ProjectsConsoleClientTest extends Scope
'content-type' => 'application/json',
'x-appwrite-project' => $id,
]), [
'userId' => 'unique()',
'userId' => ID::unique(),
'email' => $email,
'password' => $password,
'name' => $name,
@ -607,7 +608,7 @@ class ProjectsConsoleClientTest extends Scope
'content-type' => 'application/json',
'x-appwrite-project' => $id,
]), [
'userId' => 'unique()',
'userId' => ID::unique(),
'email' => $email,
'password' => $password,
'name' => $name,
@ -625,7 +626,7 @@ class ProjectsConsoleClientTest extends Scope
'x-appwrite-project' => $this->getProject()['$id'],
'cookie' => 'a_session_console=' . $this->getRoot()['session'],
]), [
'teamId' => 'unique()',
'teamId' => ID::unique(),
'name' => 'Project Test',
]);
$this->assertEquals(201, $team['headers']['status-code']);
@ -636,7 +637,7 @@ class ProjectsConsoleClientTest extends Scope
'x-appwrite-project' => $this->getProject()['$id'],
'cookie' => 'a_session_console=' . $this->getRoot()['session'],
]), [
'projectId' => 'unique()',
'projectId' => ID::unique(),
'name' => 'Project Test',
'teamId' => $team['body']['$id'],
]);
@ -769,7 +770,7 @@ class ProjectsConsoleClientTest extends Scope
'content-type' => 'application/json',
'x-appwrite-project' => $id,
]), [
'teamId' => 'unique()',
'teamId' => ID::unique(),
'name' => 'Arsenal'
]);
@ -859,7 +860,7 @@ class ProjectsConsoleClientTest extends Scope
'x-appwrite-project' => $id,
'x-appwrite-key' => $keySecret,
]), [
'teamId' => 'unique()',
'teamId' => ID::unique(),
'name' => 'Arsenal'
]);

View file

@ -6,6 +6,9 @@ use Tests\E2E\Client;
use Tests\E2E\Scopes\Scope;
use Tests\E2E\Scopes\ProjectCustom;
use Tests\E2E\Scopes\SideConsole;
use Utopia\Database\ID;
use Utopia\Database\Permission;
use Utopia\Database\Role;
class RealtimeConsoleClientTest extends Scope
{
@ -145,7 +148,7 @@ class RealtimeConsoleClientTest extends Scope
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
], $this->getHeaders()), [
'databaseId' => 'unique()',
'databaseId' => ID::unique(),
'name' => 'Actors DB',
]);
@ -157,11 +160,14 @@ class RealtimeConsoleClientTest extends Scope
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
], $this->getHeaders()), [
'collectionId' => 'unique()',
'collectionId' => ID::unique(),
'name' => 'Actors',
'read' => ['role:all'],
'write' => ['role:all'],
'permission' => 'collection'
'permissions' => [
Permission::read(Role::any()),
Permission::create(Role::any()),
Permission::update(Role::any()),
Permission::delete(Role::any()),
],
]);
$actorsId = $actors['body']['$id'];

View file

@ -8,6 +8,9 @@ use Tests\E2E\Scopes\Scope;
use Tests\E2E\Scopes\ProjectCustom;
use Tests\E2E\Scopes\SideClient;
use Utopia\CLI\Console;
use Utopia\Database\ID;
use Utopia\Database\Permission;
use Utopia\Database\Role;
use WebSocket\ConnectionException;
class RealtimeCustomClientTest extends Scope
@ -628,7 +631,7 @@ class RealtimeCustomClientTest extends Scope
'x-appwrite-project' => $this->getProject()['$id'],
'x-appwrite-key' => $this->getProject()['apiKey']
]), [
'databaseId' => 'unique()',
'databaseId' => ID::unique(),
'name' => 'Actors DB',
]);
@ -642,11 +645,15 @@ class RealtimeCustomClientTest extends Scope
'x-appwrite-project' => $this->getProject()['$id'],
'x-appwrite-key' => $this->getProject()['apiKey']
]), [
'collectionId' => 'unique()',
'collectionId' => ID::unique(),
'name' => 'Actors',
'read' => [],
'write' => [],
'permission' => 'document'
'permissions' => [
Permission::read(Role::users()),
Permission::create(Role::users()),
Permission::update(Role::users()),
Permission::delete(Role::users()),
],
'documentSecurity' => true,
]);
$actorsId = $actors['body']['$id'];
@ -676,12 +683,15 @@ class RealtimeCustomClientTest extends Scope
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
], $this->getHeaders()), [
'documentId' => 'unique()',
'documentId' => ID::unique(),
'data' => [
'name' => 'Chris Evans'
],
'read' => ['role:all'],
'write' => ['role:all'],
'permissions' => [
Permission::read(Role::any()),
Permission::update(Role::any()),
Permission::delete(Role::any()),
],
]);
$response = json_decode($client->receive(), true);
@ -719,12 +729,15 @@ class RealtimeCustomClientTest extends Scope
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
], $this->getHeaders()), [
'documentId' => 'unique()',
'documentId' => ID::unique(),
'data' => [
'name' => 'Chris Evans 2'
],
'read' => ['role:all'],
'write' => ['role:all'],
'permissions' => [
Permission::read(Role::any()),
Permission::update(Role::any()),
Permission::delete(Role::any()),
],
]);
$response = json_decode($client->receive(), true);
@ -761,12 +774,15 @@ class RealtimeCustomClientTest extends Scope
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
], $this->getHeaders()), [
'documentId' => 'unique()',
'documentId' => ID::unique(),
'data' => [
'name' => 'Bradley Cooper'
],
'read' => ['role:all'],
'write' => ['role:all'],
'permissions' => [
Permission::read(Role::any()),
Permission::update(Role::any()),
Permission::delete(Role::any()),
],
]);
$client->receive();
@ -838,7 +854,7 @@ class RealtimeCustomClientTest extends Scope
'x-appwrite-project' => $this->getProject()['$id'],
'x-appwrite-key' => $this->getProject()['apiKey']
]), [
'databaseId' => 'unique()',
'databaseId' => ID::unique(),
'name' => 'Actors DB',
]);
@ -852,11 +868,14 @@ class RealtimeCustomClientTest extends Scope
'x-appwrite-project' => $this->getProject()['$id'],
'x-appwrite-key' => $this->getProject()['apiKey']
]), [
'collectionId' => 'unique()',
'collectionId' => ID::unique(),
'name' => 'Actors',
'read' => ['role:all'],
'write' => ['role:all'],
'permission' => 'collection'
'permissions' => [
Permission::read(Role::any()),
Permission::create(Role::any()),
Permission::update(Role::any()),
Permission::delete(Role::any()),
]
]);
$actorsId = $actors['body']['$id'];
@ -886,12 +905,11 @@ class RealtimeCustomClientTest extends Scope
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
], $this->getHeaders()), [
'documentId' => 'unique()',
'documentId' => ID::unique(),
'data' => [
'name' => 'Chris Evans'
],
'read' => [],
'write' => [],
'permissions' => [],
]);
$documentId = $document['body']['$id'];
@ -932,8 +950,7 @@ class RealtimeCustomClientTest extends Scope
'data' => [
'name' => 'Chris Evans 2'
],
'read' => [],
'write' => [],
'permissions' => [],
]);
$response = json_decode($client->receive(), true);
@ -970,12 +987,11 @@ class RealtimeCustomClientTest extends Scope
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
], $this->getHeaders()), [
'documentId' => 'unique()',
'documentId' => ID::unique(),
'data' => [
'name' => 'Bradley Cooper'
],
'read' => [],
'write' => [],
'permissions' => [],
]);
$documentId = $document['body']['$id'];
@ -1045,11 +1061,14 @@ class RealtimeCustomClientTest extends Scope
'x-appwrite-project' => $this->getProject()['$id'],
'x-appwrite-key' => $this->getProject()['apiKey']
]), [
'bucketId' => 'unique()',
'bucketId' => ID::unique(),
'name' => 'Bucket 1',
'read' => ['role:all'],
'write' => ['role:all'],
'permission' => 'bucket'
'permissions' => [
Permission::read(Role::any()),
Permission::create(Role::any()),
Permission::update(Role::any()),
Permission::delete(Role::any()),
]
]);
$bucketId = $bucket1['body']['$id'];
@ -1061,10 +1080,13 @@ class RealtimeCustomClientTest extends Scope
'content-type' => 'multipart/form-data',
'x-appwrite-project' => $this->getProject()['$id'],
], $this->getHeaders()), [
'fileId' => 'unique()',
'fileId' => ID::unique(),
'file' => new CURLFile(realpath(__DIR__ . '/../../../resources/logo.png'), 'image/png', 'logo.png'),
'read' => ['role:all'],
'write' => ['role:all'],
'permissions' => [
Permission::read(Role::any()),
Permission::update(Role::any()),
Permission::delete(Role::any()),
],
]);
$fileId = $file['body']['$id'];
@ -1101,8 +1123,12 @@ class RealtimeCustomClientTest extends Scope
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
], $this->getHeaders()), [
'read' => ['role:all'],
'write' => ['role:all'],
'permissions' => [
Permission::read(Role::any()),
Permission::create(Role::any()),
Permission::update(Role::any()),
Permission::delete(Role::any()),
],
]);
$response = json_decode($client->receive(), true);
@ -1192,9 +1218,9 @@ class RealtimeCustomClientTest extends Scope
'x-appwrite-project' => $this->getProject()['$id'],
'x-appwrite-key' => $this->getProject()['apiKey']
], [
'functionId' => 'unique()',
'functionId' => ID::unique(),
'name' => 'Test',
'execute' => ['role:member'],
'execute' => ['users'],
'runtime' => 'php-8.0',
'timeout' => 10,
]);
@ -1335,7 +1361,7 @@ class RealtimeCustomClientTest extends Scope
'content-type' => 'application/json',
'x-appwrite-project' => $projectId,
], $this->getHeaders()), [
'teamId' => 'unique()',
'teamId' => ID::unique(),
'name' => 'Arsenal'
]);

View file

@ -5,6 +5,9 @@ namespace Tests\E2E\Services\Storage;
use CURLFile;
use Tests\E2E\Client;
use Utopia\Database\DateTime;
use Utopia\Database\ID;
use Utopia\Database\Permission;
use Utopia\Database\Role;
trait StorageBase
{
@ -18,13 +21,17 @@ trait StorageBase
'x-appwrite-project' => $this->getProject()['$id'],
'x-appwrite-key' => $this->getProject()['apiKey'],
], [
'bucketId' => 'unique()',
'bucketId' => ID::unique(),
'name' => 'Test Bucket',
'permission' => 'file',
'fileSecurity' => true,
'maximumFileSize' => 2000000, //2MB
'allowedFileExtensions' => ["jpg", "png"],
'read' => ['role:all'],
'write' => ['role:all'],
'permissions' => [
Permission::read(Role::any()),
Permission::create(Role::any()),
Permission::update(Role::any()),
Permission::delete(Role::any()),
],
]);
$this->assertEquals(201, $bucket['headers']['status-code']);
$this->assertNotEmpty($bucket['body']['$id']);
@ -35,10 +42,13 @@ trait StorageBase
'content-type' => 'multipart/form-data',
'x-appwrite-project' => $this->getProject()['$id'],
], $this->getHeaders()), [
'fileId' => 'unique()',
'fileId' => ID::unique(),
'file' => new CURLFile(realpath(__DIR__ . '/../../../resources/logo.png'), 'image/png', 'logo.png'),
'read' => ['role:all'],
'write' => ['role:all'],
'permissions' => [
Permission::read(Role::any()),
Permission::update(Role::any()),
Permission::delete(Role::any()),
],
]);
$this->assertEquals(201, $file['headers']['status-code']);
$this->assertNotEmpty($file['body']['$id']);
@ -58,11 +68,15 @@ trait StorageBase
'x-appwrite-project' => $this->getProject()['$id'],
'x-appwrite-key' => $this->getProject()['apiKey'],
], [
'bucketId' => 'unique()',
'bucketId' => ID::unique(),
'name' => 'Test Bucket 2',
'permission' => 'file',
'read' => ['role:all'],
'write' => ['role:all'],
'fileSecurity' => true,
'permissions' => [
Permission::read(Role::any()),
Permission::create(Role::any()),
Permission::update(Role::any()),
Permission::delete(Role::any()),
],
]);
$this->assertEquals(201, $bucket2['headers']['status-code']);
$this->assertNotEmpty($bucket2['body']['$id']);
@ -93,8 +107,11 @@ trait StorageBase
$largeFile = $this->client->call(Client::METHOD_POST, '/storage/buckets/' . $bucket2['body']['$id'] . '/files', array_merge($headers, $this->getHeaders()), [
'fileId' => $fileId,
'file' => $curlFile,
'read' => ['role:all'],
'write' => ['role:all'],
'permissions' => [
Permission::read(Role::any()),
Permission::update(Role::any()),
Permission::delete(Role::any()),
],
]);
$counter++;
$id = $largeFile['body']['$id'];
@ -131,8 +148,11 @@ trait StorageBase
$res = $this->client->call(Client::METHOD_POST, '/storage/buckets/' . $bucket2['body']['$id'] . '/files', $this->getHeaders(), [
'fileId' => $fileId,
'file' => $curlFile,
'read' => ['role:all'],
'write' => ['role:all'],
'permissions' => [
Permission::read(Role::any()),
Permission::update(Role::any()),
Permission::delete(Role::any()),
],
]);
@fclose($handle);
@ -147,10 +167,13 @@ trait StorageBase
'content-type' => 'multipart/form-data',
'x-appwrite-project' => $this->getProject()['$id'],
], $this->getHeaders()), [
'fileId' => 'unique()',
'fileId' => ID::unique(),
'file' => new CURLFile(realpath(__DIR__ . '/../../../resources/logo.png'), 'image/png', 'logo.png'),
'read' => ['role:all'],
'write' => ['role:all'],
'permissions' => [
Permission::read(Role::any()),
Permission::update(Role::any()),
Permission::delete(Role::any()),
],
]);
$this->assertEquals(404, $res['headers']['status-code']);
@ -162,10 +185,13 @@ trait StorageBase
'content-type' => 'multipart/form-data',
'x-appwrite-project' => $this->getProject()['$id'],
], $this->getHeaders()), [
'fileId' => 'unique()',
'fileId' => ID::unique(),
'file' => new CURLFile(realpath(__DIR__ . '/../../../resources/disk-b/kitten-1.png'), 'image/png', 'kitten-1.png'),
'read' => ['role:all'],
'write' => ['role:all'],
'permissions' => [
Permission::read(Role::any()),
Permission::update(Role::any()),
Permission::delete(Role::any()),
],
]);
$this->assertEquals(400, $res['headers']['status-code']);
@ -179,17 +205,18 @@ trait StorageBase
'content-type' => 'multipart/form-data',
'x-appwrite-project' => $this->getProject()['$id'],
], $this->getHeaders()), [
'fileId' => 'unique()',
'fileId' => ID::unique(),
'file' => new CURLFile(realpath(__DIR__ . '/../../../resources/disk-a/kitten-3.gif'), 'image/gif', 'kitten-3.gif'),
'read' => ['role:all'],
'write' => ['role:all'],
'permissions' => [
Permission::read(Role::any()),
Permission::update(Role::any()),
Permission::delete(Role::any()),
],
]);
$this->assertEquals(400, $res['headers']['status-code']);
$this->assertEquals('File extension not allowed', $res['body']['message']);
return ['bucketId' => $bucketId, 'fileId' => $file['body']['$id'], 'largeFileId' => $largeFile['body']['$id'], 'largeBucketId' => $bucket2['body']['$id']];
/**
* Test for FAILURE create bucket with too high limit (bigger then _APP_STORAGE_LIMIT)
*/
@ -198,15 +225,22 @@ trait StorageBase
'x-appwrite-project' => $this->getProject()['$id'],
'x-appwrite-key' => $this->getProject()['apiKey'],
], [
'bucketId' => 'unique()',
'bucketId' => ID::unique(),
'name' => 'Test Bucket 2',
'permission' => 'file',
'fileSecurity' => true,
'maximumFileSize' => 200000000, //200MB
'allowedFileExtensions' => ["jpg", "png"],
'read' => ['role:all'],
'write' => ['role:all'],
'permissions' => [
Permission::read(Role::any()),
Permission::create(Role::any()),
Permission::update(Role::any()),
Permission::delete(Role::any()),
],
]);
$this->assertEquals(400, $failedBucket['headers']['status-code']);
return ['bucketId' => $bucketId, 'fileId' => $file['body']['$id'], 'largeFileId' => $largeFile['body']['$id'], 'largeBucketId' => $bucket2['body']['$id']];
}
/**
@ -258,16 +292,8 @@ trait StorageBase
$this->assertEquals('logo.png', $file1['body']['name']);
$this->assertEquals('image/png', $file1['body']['mimeType']);
$this->assertEquals(47218, $file1['body']['sizeOriginal']);
//$this->assertEquals(54944, $file1['body']['sizeActual']);
//$this->assertEquals('gzip', $file1['body']['algorithm']);
//$this->assertEquals('1', $file1['body']['fileOpenSSLVersion']);
//$this->assertEquals('aes-128-gcm', $file1['body']['fileOpenSSLCipher']);
//$this->assertNotEmpty($file1['body']['fileOpenSSLTag']);
//$this->assertNotEmpty($file1['body']['fileOpenSSLIV']);
$this->assertIsArray($file1['body']['$read']);
$this->assertIsArray($file1['body']['$write']);
$this->assertCount(1, $file1['body']['$read']);
$this->assertCount(1, $file1['body']['$write']);
$this->assertIsArray($file1['body']['$permissions']);
$this->assertCount(3, $file1['body']['$permissions']);
$file2 = $this->client->call(Client::METHOD_GET, '/storage/buckets/' . $bucketId . '/files/' . $data['fileId'] . '/preview', array_merge([
'content-type' => 'application/json',
@ -454,10 +480,13 @@ trait StorageBase
'content-type' => 'multipart/form-data',
'x-appwrite-project' => $this->getProject()['$id'],
], $this->getHeaders()), [
'fileId' => 'testcache',
'fileId' => ID::custom('testcache'),
'file' => new CURLFile(realpath(__DIR__ . '/../../../resources/logo.png'), 'image/png', 'logo.png'),
'read' => ['role:all'],
'write' => ['role:all'],
'permissions' => [
Permission::read(Role::any()),
Permission::update(Role::any()),
Permission::delete(Role::any()),
],
]);
$this->assertEquals(201, $file['headers']['status-code']);
$this->assertNotEmpty($file['body']['$id']);
@ -497,10 +526,13 @@ trait StorageBase
'content-type' => 'multipart/form-data',
'x-appwrite-project' => $this->getProject()['$id'],
], $this->getHeaders()), [
'fileId' => 'testcache',
'fileId' => ID::custom('testcache'),
'file' => new CURLFile(realpath(__DIR__ . '/../../../resources/disk-b/kitten-2.png'), 'image/png', 'logo.png'),
'read' => ['role:all'],
'write' => ['role:all'],
'permissions' => [
Permission::read(Role::any()),
Permission::update(Role::any()),
Permission::delete(Role::any()),
],
]);
$this->assertEquals(201, $file['headers']['status-code']);
$this->assertNotEmpty($file['body']['$id']);
@ -542,8 +574,11 @@ trait StorageBase
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
], $this->getHeaders()), [
'read' => ['user:' . $this->getUser()['$id']],
'write' => ['user:' . $this->getUser()['$id']],
'permissions' => [
Permission::read(Role::user($this->getUser()['$id'])),
Permission::update(Role::user($this->getUser()['$id'])),
Permission::delete(Role::user($this->getUser()['$id'])),
]
]);
$this->assertEquals(200, $file['headers']['status-code']);
@ -558,10 +593,8 @@ trait StorageBase
//$this->assertEquals('aes-128-gcm', $file['body']['fileOpenSSLCipher']);
//$this->assertNotEmpty($file['body']['fileOpenSSLTag']);
//$this->assertNotEmpty($file['body']['fileOpenSSLIV']);
$this->assertIsArray($file['body']['$read']);
$this->assertIsArray($file['body']['$write']);
$this->assertCount(1, $file['body']['$read']);
$this->assertCount(1, $file['body']['$write']);
$this->assertIsArray($file['body']['$permissions']);
$this->assertCount(3, $file['body']['$permissions']);
/**
* Test for FAILURE unknown Bucket
@ -571,8 +604,11 @@ trait StorageBase
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
], $this->getHeaders()), [
'read' => ['user:' . $this->getUser()['$id']],
'write' => ['user:' . $this->getUser()['$id']],
'permissions' => [
Permission::read(Role::user($this->getUser()['$id'])),
Permission::update(Role::user($this->getUser()['$id'])),
Permission::delete(Role::user($this->getUser()['$id'])),
]
]);
$this->assertEquals(404, $file['headers']['status-code']);

View file

@ -6,6 +6,7 @@ use Tests\E2E\Client;
use Tests\E2E\Scopes\Scope;
use Tests\E2E\Scopes\ProjectCustom;
use Tests\E2E\Scopes\SideConsole;
use Utopia\Database\ID;
class StorageConsoleClientTest extends Scope
{
@ -51,7 +52,7 @@ class StorageConsoleClientTest extends Scope
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
], $this->getHeaders()), [
'bucketId' => 'unique()',
'bucketId' => ID::unique(),
'name' => 'Test Bucket',
'permission' => 'file'
]);

View file

@ -11,6 +11,9 @@ use Tests\E2E\Scopes\Scope;
use Tests\E2E\Scopes\ProjectCustom;
use Tests\E2E\Scopes\SideClient;
use Utopia\Database\DateTime;
use Utopia\Database\ID;
use Utopia\Database\Permission;
use Utopia\Database\Role;
class StorageCustomClientTest extends Scope
{
@ -28,11 +31,14 @@ class StorageCustomClientTest extends Scope
'x-appwrite-project' => $this->getProject()['$id'],
'x-appwrite-key' => $this->getProject()['apiKey'],
], [
'bucketId' => 'unique()',
'bucketId' => ID::unique(),
'name' => 'Test Bucket',
'permission' => 'bucket',
'read' => ['role:all'],
'write' => ['role:member'],
'permissions' => [
Permission::read(Role::any()),
Permission::create(Role::users()),
Permission::update(Role::users()),
Permission::delete(Role::users()),
],
]);
$bucketId = $bucket['body']['$id'];
@ -43,7 +49,7 @@ class StorageCustomClientTest extends Scope
'content-type' => 'multipart/form-data',
'x-appwrite-project' => $this->getProject()['$id'],
], $this->getHeaders()), [
'fileId' => 'unique()',
'fileId' => ID::unique(),
'file' => new CURLFile(realpath(__DIR__ . '/../../../resources/logo.png'), 'image/png', 'permissions.png'),
]);
@ -90,7 +96,7 @@ class StorageCustomClientTest extends Scope
'content-type' => 'multipart/form-data',
'x-appwrite-project' => $this->getProject()['$id'],
], [
'fileId' => 'unique()',
'fileId' => ID::unique(),
'file' => new CURLFile(realpath(__DIR__ . '/../../../resources/logo.png'), 'image/png', 'permissions.png'),
]);
@ -118,11 +124,15 @@ class StorageCustomClientTest extends Scope
'x-appwrite-project' => $this->getProject()['$id'],
'x-appwrite-key' => $this->getProject()['apiKey'],
], [
'bucketId' => 'unique()',
'bucketId' => ID::unique(),
'name' => 'Test Bucket',
'permission' => 'file',
'read' => ['role:all'],
'write' => ['role:all'],
'fileSecurity' => true,
'permissions' => [
Permission::read(Role::any()),
Permission::create(Role::any()),
Permission::update(Role::any()),
Permission::delete(Role::any()),
],
]);
$this->assertEquals(201, $bucket['headers']['status-code']);
$this->assertNotEmpty($bucket['body']['$id']);
@ -131,14 +141,15 @@ class StorageCustomClientTest extends Scope
'content-type' => 'multipart/form-data',
'x-appwrite-project' => $this->getProject()['$id'],
], $this->getHeaders()), [
'fileId' => 'unique()',
'fileId' => ID::unique(),
'file' => new CURLFile(realpath(__DIR__ . '/../../../resources/logo.png'), 'image/png', 'permissions.png'),
]);
$this->assertEquals($file['headers']['status-code'], 201);
$this->assertNotEmpty($file['body']['$id']);
$this->assertContains('user:' . $this->getUser()['$id'], $file['body']['$read']);
$this->assertContains('user:' . $this->getUser()['$id'], $file['body']['$write']);
$this->assertContains(Permission::read(Role::user($this->getUser()['$id'])), $file['body']['$permissions']);
$this->assertContains(Permission::update(Role::user($this->getUser()['$id'])), $file['body']['$permissions']);
$this->assertContains(Permission::delete(Role::user($this->getUser()['$id'])), $file['body']['$permissions']);
$this->assertEquals(true, DateTime::isValid($file['body']['$createdAt']));
$this->assertEquals('permissions.png', $file['body']['name']);
$this->assertEquals('image/png', $file['body']['mimeType']);
@ -159,49 +170,57 @@ class StorageCustomClientTest extends Scope
'content-type' => 'multipart/form-data',
'x-appwrite-project' => $this->getProject()['$id'],
], $this->getHeaders()), [
'fileId' => 'unique()',
'fileId' => ID::unique(),
'file' => new CURLFile(realpath(__DIR__ . '/../../../resources/logo.png'), 'image/png', 'permissions.png'),
'folderId' => 'xyz',
'read' => ['user:notme']
'folderId' => ID::custom('xyz'),
'permissions' => [
Permission::read(Role::user(ID::custom('notme'))),
],
]);
$this->assertEquals(400, $file['headers']['status-code']);
$this->assertStringStartsWith('Read permissions must be one of:', $file['body']['message']);
$this->assertStringContainsString('role:all', $file['body']['message']);
$this->assertStringContainsString('role:member', $file['body']['message']);
$this->assertEquals(401, $file['headers']['status-code']);
$this->assertStringStartsWith('Permissions must be one of:', $file['body']['message']);
$this->assertStringContainsString('any', $file['body']['message']);
$this->assertStringContainsString('users', $file['body']['message']);
$this->assertStringContainsString('user:' . $this->getUser()['$id'], $file['body']['message']);
$file = $this->client->call(Client::METHOD_POST, '/storage/buckets/' . $data['bucketId'] . '/files', array_merge([
'content-type' => 'multipart/form-data',
'x-appwrite-project' => $this->getProject()['$id'],
], $this->getHeaders()), [
'fileId' => 'unique()',
'fileId' => ID::unique(),
'file' => new CURLFile(realpath(__DIR__ . '/../../../resources/logo.png'), 'image/png', 'permissions.png'),
'folderId' => 'xyz',
'write' => ['user:notme']
'folderId' => ID::custom('xyz'),
'permissions' => [
Permission::update(Role::user(ID::custom('notme'))),
Permission::delete(Role::user(ID::custom('notme'))),
]
]);
$this->assertEquals($file['headers']['status-code'], 400);
$this->assertStringStartsWith('Write permissions must be one of:', $file['body']['message']);
$this->assertStringContainsString('role:all', $file['body']['message']);
$this->assertStringContainsString('role:member', $file['body']['message']);
$this->assertEquals(401, $file['headers']['status-code']);
$this->assertStringStartsWith('Permissions must be one of:', $file['body']['message']);
$this->assertStringContainsString('any', $file['body']['message']);
$this->assertStringContainsString('users', $file['body']['message']);
$this->assertStringContainsString('user:' . $this->getUser()['$id'], $file['body']['message']);
$file = $this->client->call(Client::METHOD_POST, '/storage/buckets/' . $data['bucketId'] . '/files', array_merge([
'content-type' => 'multipart/form-data',
'x-appwrite-project' => $this->getProject()['$id'],
], $this->getHeaders()), [
'fileId' => 'unique()',
'fileId' => ID::unique(),
'file' => new CURLFile(realpath(__DIR__ . '/../../../resources/logo.png'), 'image/png', 'permissions.png'),
'folderId' => 'xyz',
'read' => ['user:notme'],
'write' => ['user:notme']
'folderId' => ID::custom('xyz'),
'permissions' => [
Permission::read(Role::user(ID::custom('notme'))),
Permission::update(Role::user(ID::custom('notme'))),
Permission::delete(Role::user(ID::custom('notme'))),
],
]);
$this->assertEquals($file['headers']['status-code'], 400);
$this->assertStringStartsWith('Read permissions must be one of:', $file['body']['message']);
$this->assertStringContainsString('role:all', $file['body']['message']);
$this->assertStringContainsString('role:member', $file['body']['message']);
$this->assertEquals(401, $file['headers']['status-code']);
$this->assertStringStartsWith('Permissions must be one of:', $file['body']['message']);
$this->assertStringContainsString('any', $file['body']['message']);
$this->assertStringContainsString('users', $file['body']['message']);
$this->assertStringContainsString('user:' . $this->getUser()['$id'], $file['body']['message']);
}
@ -217,40 +236,49 @@ class StorageCustomClientTest extends Scope
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
], $this->getHeaders()), [
'read' => ['user:notme']
'permissions' => [
Permission::read(Role::user(ID::custom('notme'))),
],
]);
$this->assertEquals($file['headers']['status-code'], 400);
$this->assertStringStartsWith('Read permissions must be one of:', $file['body']['message']);
$this->assertStringContainsString('role:all', $file['body']['message']);
$this->assertStringContainsString('role:member', $file['body']['message']);
$this->assertEquals(401, $file['headers']['status-code']);
$this->assertStringStartsWith('Permissions must be one of:', $file['body']['message']);
$this->assertStringContainsString('any', $file['body']['message']);
$this->assertStringContainsString('users', $file['body']['message']);
$this->assertStringContainsString('user:' . $this->getUser()['$id'], $file['body']['message']);
$file = $this->client->call(Client::METHOD_PUT, '/storage/buckets/' . $data['bucketId'] . '/files/' . $data['fileId'], array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
], $this->getHeaders()), [
'write' => ['user:notme']
'permissions' => [
Permission::update(Role::user(ID::custom('notme'))),
Permission::delete(Role::user(ID::custom('notme'))),
]
]);
$this->assertEquals($file['headers']['status-code'], 400);
$this->assertStringStartsWith('Write permissions must be one of:', $file['body']['message']);
$this->assertStringContainsString('role:all', $file['body']['message']);
$this->assertStringContainsString('role:member', $file['body']['message']);
$this->assertEquals(401, $file['headers']['status-code']);
$this->assertStringStartsWith('Permissions must be one of:', $file['body']['message']);
$this->assertStringContainsString('any', $file['body']['message']);
$this->assertStringContainsString('users', $file['body']['message']);
$this->assertStringContainsString('user:' . $this->getUser()['$id'], $file['body']['message']);
$file = $this->client->call(Client::METHOD_PUT, '/storage/buckets/' . $data['bucketId'] . '/files/' . $data['fileId'], array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
], $this->getHeaders()), [
'read' => ['user:notme'],
'write' => ['user:notme']
'permissions' => [
Permission::read(Role::user(ID::custom('notme'))),
Permission::create(Role::user(ID::custom('notme'))),
Permission::update(Role::user(ID::custom('notme'))),
Permission::delete(Role::user(ID::custom('notme'))),
],
]);
$this->assertEquals($file['headers']['status-code'], 400);
$this->assertStringStartsWith('Read permissions must be one of:', $file['body']['message']);
$this->assertStringContainsString('role:all', $file['body']['message']);
$this->assertStringContainsString('role:member', $file['body']['message']);
$this->assertEquals(401, $file['headers']['status-code']);
$this->assertStringStartsWith('Permissions must be one of:', $file['body']['message']);
$this->assertStringContainsString('any', $file['body']['message']);
$this->assertStringContainsString('users', $file['body']['message']);
$this->assertStringContainsString('user:' . $this->getUser()['$id'], $file['body']['message']);
}
}

View file

@ -7,6 +7,7 @@ use Tests\E2E\Scopes\ProjectCustom;
use Tests\E2E\Scopes\Scope;
use Tests\E2E\Scopes\SideServer;
use Utopia\Database\DateTime;
use Utopia\Database\ID;
class StorageCustomServerTest extends Scope
{
@ -23,15 +24,14 @@ class StorageCustomServerTest extends Scope
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
], $this->getHeaders()), [
'bucketId' => 'unique()',
'bucketId' => ID::unique(),
'name' => 'Test Bucket',
'permission' => 'file',
'fileSecurity' => true,
]);
$this->assertEquals(201, $bucket['headers']['status-code']);
$this->assertNotEmpty($bucket['body']['$id']);
$this->assertEquals(true, DateTime::isValid($bucket['body']['$createdAt']));
$this->assertIsArray($bucket['body']['$read']);
$this->assertIsArray($bucket['body']['$write']);
$this->assertIsArray($bucket['body']['$permissions']);
$this->assertIsArray($bucket['body']['allowedFileExtensions']);
$this->assertEquals('Test Bucket', $bucket['body']['name']);
$this->assertEquals(true, $bucket['body']['enabled']);
@ -46,9 +46,9 @@ class StorageCustomServerTest extends Scope
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
], $this->getHeaders()), [
'bucketId' => 'bucket1',
'bucketId' => ID::custom('bucket1'),
'name' => 'Test Bucket',
'permission' => 'file',
'fileSecurity' => true,
]);
$this->assertEquals(201, $bucket['headers']['status-code']);
$this->assertEquals('bucket1', $bucket['body']['$id']);
@ -60,9 +60,9 @@ class StorageCustomServerTest extends Scope
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
], $this->getHeaders()), [
'bucketId' => 'unique()',
'bucketId' => ID::unique(),
'name' => '',
'permission' => 'file',
'fileSecurity' => true,
]);
$this->assertEquals(400, $bucket['headers']['status-code']);
@ -181,16 +181,15 @@ class StorageCustomServerTest extends Scope
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
], $this->getHeaders()), [
'bucketId' => 'unique()',
'bucketId' => ID::unique(),
'name' => 'Test Bucket Updated',
'enabled' => false,
'permission' => 'file',
'fileSecurity' => true,
]);
$this->assertEquals(200, $bucket['headers']['status-code']);
$this->assertNotEmpty($bucket['body']['$id']);
$this->assertEquals(true, DateTime::isValid($bucket['body']['$createdAt']));
$this->assertIsArray($bucket['body']['$read']);
$this->assertIsArray($bucket['body']['$write']);
$this->assertIsArray($bucket['body']['$permissions']);
$this->assertIsArray($bucket['body']['allowedFileExtensions']);
$this->assertEquals('Test Bucket Updated', $bucket['body']['name']);
$this->assertEquals(false, $bucket['body']['enabled']);

View file

@ -5,6 +5,7 @@ namespace Tests\E2E\Services\Teams;
use Tests\E2E\Client;
use Utopia\Database\Database;
use Utopia\Database\DateTime;
use Utopia\Database\ID;
trait TeamsBase
{
@ -17,7 +18,7 @@ trait TeamsBase
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
], $this->getHeaders()), [
'teamId' => 'unique()',
'teamId' => ID::unique(),
'name' => 'Arsenal'
]);
@ -31,7 +32,7 @@ trait TeamsBase
$teamUid = $response1['body']['$id'];
$teamName = $response1['body']['name'];
$teamId = \uniqid();
$teamId = ID::unique();
$response2 = $this->client->call(Client::METHOD_POST, '/teams', array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
@ -52,7 +53,7 @@ trait TeamsBase
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
], $this->getHeaders()), [
'teamId' => 'unique()',
'teamId' => ID::unique(),
'name' => 'Newcastle'
]);
@ -251,7 +252,7 @@ trait TeamsBase
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
], $this->getHeaders()), [
'teamId' => 'unique()',
'teamId' => ID::unique(),
'name' => 'Demo'
]);
@ -266,7 +267,7 @@ trait TeamsBase
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
], $this->getHeaders()), [
'teamId' => 'unique()',
'teamId' => ID::unique(),
'name' => 'Demo New'
]);
@ -300,7 +301,7 @@ trait TeamsBase
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
], $this->getHeaders()), [
'teamId' => 'unique()',
'teamId' => ID::unique(),
'name' => 'Demo'
]);

View file

@ -4,6 +4,7 @@ namespace Tests\E2E\Services\Teams;
use Tests\E2E\Client;
use Utopia\Database\DateTime;
use Utopia\Database\ID;
trait TeamsBaseClient
{
@ -341,7 +342,7 @@ trait TeamsBaseClient
'x-appwrite-project' => $this->getProject()['$id'],
]), [
'secret' => $secret,
'userId' => 'sdasd',
'userId' => ID::custom('sdasd'),
]);
$this->assertEquals(401, $response['headers']['status-code']);
@ -352,7 +353,7 @@ trait TeamsBaseClient
'x-appwrite-project' => $this->getProject()['$id'],
]), [
'secret' => $secret,
'userId' => '',
'userId' => ID::custom(''),
]);
$this->assertEquals(400, $response['headers']['status-code']);

View file

@ -6,6 +6,7 @@ use Tests\E2E\Client;
use Tests\E2E\Scopes\Scope;
use Tests\E2E\Scopes\ProjectConsole;
use Tests\E2E\Scopes\SideClient;
use Utopia\Database\ID;
class TeamsConsoleClientTest extends Scope
{
@ -24,7 +25,7 @@ class TeamsConsoleClientTest extends Scope
'x-appwrite-project' => 'console'
], $this->getHeaders()), [
'name' => 'Latest version Team',
'teamId' => 'unique()'
'teamId' => ID::unique()
]);
$this->assertEquals(201, $response['headers']['status-code']);

View file

@ -4,6 +4,7 @@ namespace Tests\E2E\Services\Users;
use Tests\E2E\Client;
use Utopia\Database\Database;
use Utopia\Database\ID;
trait UsersBase
{
@ -16,7 +17,7 @@ trait UsersBase
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
], $this->getHeaders()), [
'userId' => 'unique()',
'userId' => ID::unique(),
'email' => 'cristiano.ronaldo@manchester-united.co.uk',
'password' => 'password',
'name' => 'Cristiano Ronaldo',
@ -42,7 +43,7 @@ trait UsersBase
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
], $this->getHeaders()), [
'userId' => 'user1',
'userId' => ID::custom('user1'),
'email' => 'lionel.messi@psg.fr',
'password' => 'password',
'name' => 'Lionel Messi',

View file

@ -5,6 +5,9 @@ namespace Tests\E2E\Services\Webhooks;
use CURLFile;
use Tests\E2E\Client;
use Utopia\Database\DateTime;
use Utopia\Database\ID;
use Utopia\Database\Permission;
use Utopia\Database\Role;
trait WebhooksBase
{
@ -26,7 +29,7 @@ trait WebhooksBase
'x-appwrite-project' => $this->getProject()['$id'],
'x-appwrite-key' => $this->getProject()['apiKey']
]), [
'databaseId' => 'unique()',
'databaseId' => ID::unique(),
'name' => 'Actors DB',
]);
@ -40,11 +43,15 @@ trait WebhooksBase
'x-appwrite-project' => $this->getProject()['$id'],
'x-appwrite-key' => $this->getProject()['apiKey']
]), [
'collectionId' => 'unique()',
'collectionId' => ID::unique(),
'name' => 'Actors',
'read' => ['role:all'],
'write' => ['role:all'],
'permission' => 'document',
'permissions' => [
Permission::read(Role::any()),
Permission::create(Role::any()),
Permission::update(Role::any()),
Permission::delete(Role::any()),
],
'documentSecurity' => true,
]);
$actorsId = $actors['body']['$id'];
@ -68,10 +75,8 @@ trait WebhooksBase
$this->assertEquals(empty($webhook['headers']['X-Appwrite-Webhook-User-Id'] ?? ''), true);
$this->assertNotEmpty($webhook['data']['$id']);
$this->assertEquals($webhook['data']['name'], 'Actors');
$this->assertIsArray($webhook['data']['$read']);
$this->assertIsArray($webhook['data']['$write']);
$this->assertCount(1, $webhook['data']['$read']);
$this->assertCount(1, $webhook['data']['$write']);
$this->assertIsArray($webhook['data']['$permissions']);
$this->assertCount(4, $webhook['data']['$permissions']);
return array_merge(['actorsId' => $actorsId, 'databaseId' => $databaseId]);
}
@ -188,13 +193,16 @@ trait WebhooksBase
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
], $this->getHeaders()), [
'documentId' => 'unique()',
'documentId' => ID::unique(),
'data' => [
'firstName' => 'Chris',
'lastName' => 'Evans',
],
'read' => ['role:all'],
'write' => ['role:all'],
'permissions' => [
Permission::read(Role::any()),
Permission::update(Role::any()),
Permission::delete(Role::any()),
],
]);
$documentId = $document['body']['$id'];
@ -225,10 +233,8 @@ trait WebhooksBase
$this->assertNotEmpty($webhook['data']['$id']);
$this->assertEquals($webhook['data']['firstName'], 'Chris');
$this->assertEquals($webhook['data']['lastName'], 'Evans');
$this->assertIsArray($webhook['data']['$read']);
$this->assertIsArray($webhook['data']['$write']);
$this->assertCount(1, $webhook['data']['$read']);
$this->assertCount(1, $webhook['data']['$write']);
$this->assertIsArray($webhook['data']['$permissions']);
$this->assertCount(3, $webhook['data']['$permissions']);
$data['documentId'] = $document['body']['$id'];
@ -254,8 +260,11 @@ trait WebhooksBase
'firstName' => 'Chris1',
'lastName' => 'Evans2',
],
'read' => ['role:all'],
'write' => ['role:all'],
'permissions' => [
Permission::read(Role::any()),
Permission::update(Role::any()),
Permission::delete(Role::any()),
],
]);
$documentId = $document['body']['$id'];
@ -286,10 +295,8 @@ trait WebhooksBase
$this->assertNotEmpty($webhook['data']['$id']);
$this->assertEquals($webhook['data']['firstName'], 'Chris1');
$this->assertEquals($webhook['data']['lastName'], 'Evans2');
$this->assertIsArray($webhook['data']['$read']);
$this->assertIsArray($webhook['data']['$write']);
$this->assertCount(1, $webhook['data']['$read']);
$this->assertCount(1, $webhook['data']['$write']);
$this->assertIsArray($webhook['data']['$permissions']);
$this->assertCount(3, $webhook['data']['$permissions']);
return $data;
}
@ -309,14 +316,17 @@ trait WebhooksBase
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
], $this->getHeaders()), [
'documentId' => 'unique()',
'documentId' => ID::unique(),
'data' => [
'firstName' => 'Bradly',
'lastName' => 'Cooper',
],
'read' => ['role:all'],
'write' => ['role:all'],
'permissions' => [
Permission::read(Role::any()),
Permission::update(Role::any()),
Permission::delete(Role::any()),
],
]);
$documentId = $document['body']['$id'];
@ -354,10 +364,8 @@ trait WebhooksBase
$this->assertNotEmpty($webhook['data']['$id']);
$this->assertEquals($webhook['data']['firstName'], 'Bradly');
$this->assertEquals($webhook['data']['lastName'], 'Cooper');
$this->assertIsArray($webhook['data']['$read']);
$this->assertIsArray($webhook['data']['$write']);
$this->assertCount(1, $webhook['data']['$read']);
$this->assertCount(1, $webhook['data']['$write']);
$this->assertIsArray($webhook['data']['$permissions']);
$this->assertCount(3, $webhook['data']['$permissions']);
return $data;
}
@ -373,11 +381,14 @@ trait WebhooksBase
'x-appwrite-project' => $this->getProject()['$id'],
'x-appwrite-key' => $this->getProject()['apiKey']
]), [
'bucketId' => 'unique()',
'bucketId' => ID::unique(),
'name' => 'Test Bucket',
'permission' => 'bucket',
'read' => ['role:all'],
'write' => ['role:all']
'permissions' => [
Permission::read(Role::any()),
Permission::create(Role::any()),
Permission::update(Role::any()),
Permission::delete(Role::any()),
],
]);
$bucketId = $bucket['body']['$id'];
@ -402,8 +413,7 @@ trait WebhooksBase
$this->assertNotEmpty($webhook['data']['$id']);
$this->assertEquals('Test Bucket', $webhook['data']['name']);
$this->assertEquals(true, $webhook['data']['enabled']);
$this->assertIsArray($webhook['data']['$read']);
$this->assertIsArray($webhook['data']['$write']);
$this->assertIsArray($webhook['data']['$permissions']);
return array_merge(['bucketId' => $bucketId]);
}
@ -424,7 +434,7 @@ trait WebhooksBase
'x-appwrite-key' => $this->getProject()['apiKey']
]), [
'name' => 'Test Bucket Updated',
'permission' => 'file',
'fileSecurity' => true,
'enabled' => false,
]);
@ -448,8 +458,7 @@ trait WebhooksBase
$this->assertNotEmpty($webhook['data']['$id']);
$this->assertEquals('Test Bucket Updated', $webhook['data']['name']);
$this->assertEquals(false, $webhook['data']['enabled']);
$this->assertIsArray($webhook['data']['$read']);
$this->assertIsArray($webhook['data']['$write']);
$this->assertIsArray($webhook['data']['$permissions']);
return array_merge(['bucketId' => $bucket['body']['$id']]);
}
@ -468,7 +477,7 @@ trait WebhooksBase
'x-appwrite-key' => $this->getProject()['apiKey']
]), [
'name' => 'Test Bucket Updated',
'permission' => 'file',
'fileSecurity' => true,
'enabled' => true,
]);
@ -480,11 +489,14 @@ trait WebhooksBase
'content-type' => 'multipart/form-data',
'x-appwrite-project' => $this->getProject()['$id'],
], $this->getHeaders()), [
'fileId' => 'unique()',
'fileId' => ID::unique(),
'file' => new CURLFile(realpath(__DIR__ . '/../../../resources/logo.png'), 'image/png', 'logo.png'),
'read' => ['role:all'],
'write' => ['role:all'],
'folderId' => 'xyz',
'permissions' => [
Permission::read(Role::any()),
Permission::update(Role::any()),
Permission::delete(Role::any()),
],
'folderId' => ID::custom('xyz'),
]);
$fileId = $file['body']['$id'];
@ -513,8 +525,7 @@ trait WebhooksBase
$this->assertEquals($webhook['headers']['X-Appwrite-Webhook-Project-Id'] ?? '', $this->getProject()['$id']);
$this->assertEquals(empty($webhook['headers']['X-Appwrite-Webhook-User-Id'] ?? ''), ('server' === $this->getSide()));
$this->assertNotEmpty($webhook['data']['$id']);
$this->assertIsArray($webhook['data']['$read']);
$this->assertIsArray($webhook['data']['$write']);
$this->assertIsArray($webhook['data']['$permissions']);
$this->assertEquals($webhook['data']['name'], 'logo.png');
$this->assertEquals(true, DateTime::isValid($webhook['data']['$createdAt']));
$this->assertNotEmpty($webhook['data']['signature']);
@ -541,8 +552,12 @@ trait WebhooksBase
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
], $this->getHeaders()), [
'read' => ['role:all'],
'write' => ['role:all'],
'permissions' => [
Permission::read(Role::any()),
Permission::create(Role::any()),
Permission::update(Role::any()),
Permission::delete(Role::any()),
],
]);
$this->assertEquals($file['headers']['status-code'], 200);
@ -569,8 +584,7 @@ trait WebhooksBase
$this->assertEquals($webhook['headers']['X-Appwrite-Webhook-Project-Id'] ?? '', $this->getProject()['$id']);
$this->assertEquals(empty($webhook['headers']['X-Appwrite-Webhook-User-Id'] ?? ''), ('server' === $this->getSide()));
$this->assertNotEmpty($webhook['data']['$id']);
$this->assertIsArray($webhook['data']['$read']);
$this->assertIsArray($webhook['data']['$write']);
$this->assertIsArray($webhook['data']['$permissions']);
$this->assertEquals($webhook['data']['name'], 'logo.png');
$this->assertEquals(true, DateTime::isValid($webhook['data']['$createdAt']));
$this->assertNotEmpty($webhook['data']['signature']);
@ -620,8 +634,7 @@ trait WebhooksBase
$this->assertEquals($webhook['headers']['X-Appwrite-Webhook-Project-Id'] ?? '', $this->getProject()['$id']);
$this->assertEquals(empty($webhook['headers']['X-Appwrite-Webhook-User-Id'] ?? ''), ('server' === $this->getSide()));
$this->assertNotEmpty($webhook['data']['$id']);
$this->assertIsArray($webhook['data']['$read']);
$this->assertIsArray($webhook['data']['$write']);
$this->assertIsArray($webhook['data']['$permissions']);
$this->assertEquals($webhook['data']['name'], 'logo.png');
$this->assertEquals(true, DateTime::isValid($webhook['data']['$createdAt']));
$this->assertNotEmpty($webhook['data']['signature']);
@ -666,8 +679,7 @@ trait WebhooksBase
$this->assertNotEmpty($webhook['data']['$id']);
$this->assertEquals('Test Bucket Updated', $webhook['data']['name']);
$this->assertEquals(true, $webhook['data']['enabled']);
$this->assertIsArray($webhook['data']['$read']);
$this->assertIsArray($webhook['data']['$write']);
$this->assertIsArray($webhook['data']['$permissions']);
}
public function testCreateTeam(): array
@ -679,7 +691,7 @@ trait WebhooksBase
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
], $this->getHeaders()), [
'teamId' => 'unique()',
'teamId' => ID::unique(),
'name' => 'Arsenal'
]);
@ -768,7 +780,7 @@ trait WebhooksBase
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
], $this->getHeaders()), [
'teamId' => 'unique()',
'teamId' => ID::unique(),
'name' => 'Chelsea'
]);

View file

@ -7,6 +7,7 @@ use Tests\E2E\Scopes\Scope;
use Tests\E2E\Scopes\ProjectCustom;
use Tests\E2E\Scopes\SideClient;
use Utopia\Database\DateTime;
use Utopia\Database\ID;
class WebhooksCustomClientTest extends Scope
{
@ -28,7 +29,7 @@ class WebhooksCustomClientTest extends Scope
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
]), [
'userId' => 'unique()',
'userId' => ID::unique(),
'email' => $email,
'password' => $password,
'name' => $name,
@ -86,7 +87,7 @@ class WebhooksCustomClientTest extends Scope
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
]), [
'userId' => 'unique()',
'userId' => ID::unique(),
'email' => $email,
'password' => $password,
'name' => $name,

View file

@ -9,6 +9,9 @@ use Tests\E2E\Scopes\Scope;
use Tests\E2E\Scopes\SideServer;
use Utopia\CLI\Console;
use Utopia\Database\DateTime;
use Utopia\Database\ID;
use Utopia\Database\Permission;
use Utopia\Database\Role;
class WebhooksCustomServerTest extends Scope
{
@ -33,7 +36,7 @@ class WebhooksCustomServerTest extends Scope
'x-appwrite-key' => $this->getProject()['apiKey']
]), [
'name' => 'Actors1',
'permission' => 'document',
'documentSecurity' => true,
]);
$this->assertEquals($actors['headers']['status-code'], 200);
@ -55,10 +58,8 @@ class WebhooksCustomServerTest extends Scope
$this->assertEquals(empty($webhook['headers']['X-Appwrite-Webhook-User-Id'] ?? ''), true);
$this->assertNotEmpty($webhook['data']['$id']);
$this->assertEquals($webhook['data']['name'], 'Actors1');
$this->assertIsArray($webhook['data']['$read']);
$this->assertIsArray($webhook['data']['$write']);
$this->assertCount(1, $webhook['data']['$read']);
$this->assertCount(1, $webhook['data']['$write']);
$this->assertIsArray($webhook['data']['$permissions']);
$this->assertCount(4, $webhook['data']['$permissions']);
return array_merge(['actorsId' => $actors['body']['$id']]);
}
@ -144,7 +145,7 @@ class WebhooksCustomServerTest extends Scope
'x-appwrite-project' => $this->getProject()['$id'],
'x-appwrite-key' => $this->getProject()['apiKey']
], $this->getHeaders()), [
'databaseId' => 'unique()',
'databaseId' => ID::unique(),
'name' => 'Actors DB',
]);
@ -158,11 +159,15 @@ class WebhooksCustomServerTest extends Scope
'x-appwrite-project' => $this->getProject()['$id'],
'x-appwrite-key' => $this->getProject()['apiKey']
]), [
'collectionId' => 'unique()',
'collectionId' => ID::unique(),
'name' => 'Demo',
'read' => ['role:all'],
'write' => ['role:all'],
'permission' => 'document'
'permissions' => [
Permission::read(Role::any()),
Permission::create(Role::any()),
Permission::update(Role::any()),
Permission::delete(Role::any()),
],
'documentSecurity' => true,
]);
$id = $actors['body']['$id'];
@ -194,10 +199,8 @@ class WebhooksCustomServerTest extends Scope
$this->assertEquals(empty($webhook['headers']['X-Appwrite-Webhook-User-Id'] ?? ''), true);
$this->assertNotEmpty($webhook['data']['$id']);
$this->assertEquals($webhook['data']['name'], 'Demo');
$this->assertIsArray($webhook['data']['$read']);
$this->assertIsArray($webhook['data']['$write']);
$this->assertCount(1, $webhook['data']['$read']);
$this->assertCount(1, $webhook['data']['$write']);
$this->assertIsArray($webhook['data']['$permissions']);
$this->assertCount(4, $webhook['data']['$permissions']);
return [];
}
@ -215,7 +218,7 @@ class WebhooksCustomServerTest extends Scope
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
], $this->getHeaders()), [
'userId' => 'unique()',
'userId' => ID::unique(),
'email' => $email,
'password' => $password,
'name' => $name,
@ -393,9 +396,9 @@ class WebhooksCustomServerTest extends Scope
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
], $this->getHeaders()), [
'functionId' => 'unique()',
'functionId' => ID::unique(),
'name' => 'Test',
'execute' => ['role:all'],
'execute' => [Role::any()->toString()],
'runtime' => 'php-8.0',
'timeout' => 10,
]);
@ -444,7 +447,7 @@ class WebhooksCustomServerTest extends Scope
], $this->getHeaders()), [
'name' => 'Test',
'runtime' => 'php-8.0',
'execute' => ['role:all'],
'execute' => [Role::any()->toString()],
'vars' => [
'key1' => 'value1',
]

View file

@ -9,9 +9,9 @@ class TestHook implements AfterTestHook
public function executeAfterTest(string $test, float $time): void
{
printf(
"%s ended in %s seconds\n",
"%s ended in %s milliseconds\n",
$test,
$time
$time * 1000
);
}
}

View file

@ -5,6 +5,8 @@ namespace Tests\Unit\Auth;
use Appwrite\Auth\Auth;
use Utopia\Database\DateTime;
use Utopia\Database\Document;
use Utopia\Database\ID;
use Utopia\Database\Role;
use Utopia\Database\Validator\Authorization;
use PHPUnit\Framework\TestCase;
use Utopia\Database\Database;
@ -17,7 +19,7 @@ class AuthTest extends TestCase
public function tearDown(): void
{
Authorization::cleanRoles();
Authorization::setRole('role:all');
Authorization::setRole(Role::any()->toString());
}
public function testCookieName(): void
@ -205,15 +207,15 @@ class AuthTest extends TestCase
$hash = Auth::hash($secret);
$tokens1 = [
new Document([
'$id' => 'token1',
'expire' => DateTime::addSeconds(new \DateTime(), 60 * 60 * 24),
'$id' => ID::custom('token1'),
'expire' => DateTime::formatTz(DateTime::addSeconds(new \DateTime(), 60 * 60 * 24)),
'secret' => $hash,
'provider' => Auth::SESSION_PROVIDER_EMAIL,
'providerUid' => 'test@example.com',
]),
new Document([
'$id' => 'token2',
'expire' => DateTime::addSeconds(new \DateTime(), -60 * 60 * 24),
'$id' => ID::custom('token2'),
'expire' => DateTime::formatTz(DateTime::addSeconds(new \DateTime(), -60 * 60 * 24)),
'secret' => 'secret2',
'provider' => Auth::SESSION_PROVIDER_EMAIL,
'providerUid' => 'test@example.com',
@ -222,15 +224,15 @@ class AuthTest extends TestCase
$tokens2 = [
new Document([ // Correct secret and type time, wrong expire time
'$id' => 'token1',
'expire' => DateTime::addSeconds(new \DateTime(), -60 * 60 * 24),
'$id' => ID::custom('token1'),
'expire' => DateTime::formatTz(DateTime::addSeconds(new \DateTime(), -60 * 60 * 24)),
'secret' => $hash,
'provider' => Auth::SESSION_PROVIDER_EMAIL,
'providerUid' => 'test@example.com',
]),
new Document([
'$id' => 'token2',
'expire' => DateTime::addSeconds(new \DateTime(), -60 * 60 * 24),
'$id' => ID::custom('token2'),
'expire' => DateTime::formatTz(DateTime::addSeconds(new \DateTime(), -60 * 60 * 24)),
'secret' => 'secret2',
'provider' => Auth::SESSION_PROVIDER_EMAIL,
'providerUid' => 'test@example.com',
@ -249,45 +251,45 @@ class AuthTest extends TestCase
$hash = Auth::hash($secret);
$tokens1 = [
new Document([
'$id' => 'token1',
'$id' => ID::custom('token1'),
'type' => Auth::TOKEN_TYPE_RECOVERY,
'expire' => DateTime::addSeconds(new \DateTime(), 60 * 60 * 24),
'expire' => DateTime::formatTz(DateTime::addSeconds(new \DateTime(), 60 * 60 * 24)),
'secret' => $hash,
]),
new Document([
'$id' => 'token2',
'$id' => ID::custom('token2'),
'type' => Auth::TOKEN_TYPE_RECOVERY,
'expire' => DateTime::addSeconds(new \DateTime(), -60 * 60 * 24),
'expire' => DateTime::formatTz(DateTime::addSeconds(new \DateTime(), -60 * 60 * 24)),
'secret' => 'secret2',
]),
];
$tokens2 = [
new Document([ // Correct secret and type time, wrong expire time
'$id' => 'token1',
'$id' => ID::custom('token1'),
'type' => Auth::TOKEN_TYPE_RECOVERY,
'expire' => DateTime::addSeconds(new \DateTime(), -60 * 60 * 24),
'expire' => DateTime::formatTz(DateTime::addSeconds(new \DateTime(), -60 * 60 * 24)),
'secret' => $hash,
]),
new Document([
'$id' => 'token2',
'$id' => ID::custom('token2'),
'type' => Auth::TOKEN_TYPE_RECOVERY,
'expire' => DateTime::addSeconds(new \DateTime(), -60 * 60 * 24),
'expire' => DateTime::formatTz(DateTime::addSeconds(new \DateTime(), -60 * 60 * 24)),
'secret' => 'secret2',
]),
];
$tokens3 = [ // Correct secret and expire time, wrong type
new Document([
'$id' => 'token1',
'$id' => ID::custom('token1'),
'type' => Auth::TOKEN_TYPE_INVITE,
'expire' => DateTime::addSeconds(new \DateTime(), 60 * 60 * 24),
'expire' => DateTime::formatTz(DateTime::addSeconds(new \DateTime(), 60 * 60 * 24)),
'secret' => $hash,
]),
new Document([
'$id' => 'token2',
'$id' => ID::custom('token2'),
'type' => Auth::TOKEN_TYPE_RECOVERY,
'expire' => DateTime::addSeconds(new \DateTime(), -60 * 60 * 24),
'expire' => DateTime::formatTz(DateTime::addSeconds(new \DateTime(), -60 * 60 * 24)),
'secret' => 'secret2',
]),
];
@ -303,35 +305,35 @@ class AuthTest extends TestCase
public function testIsPrivilegedUser(): void
{
$this->assertEquals(false, Auth::isPrivilegedUser([]));
$this->assertEquals(false, Auth::isPrivilegedUser(['role:' . Auth::USER_ROLE_GUEST]));
$this->assertEquals(false, Auth::isPrivilegedUser(['role:' . Auth::USER_ROLE_MEMBER]));
$this->assertEquals(true, Auth::isPrivilegedUser(['role:' . Auth::USER_ROLE_ADMIN]));
$this->assertEquals(true, Auth::isPrivilegedUser(['role:' . Auth::USER_ROLE_DEVELOPER]));
$this->assertEquals(true, Auth::isPrivilegedUser(['role:' . Auth::USER_ROLE_OWNER]));
$this->assertEquals(false, Auth::isPrivilegedUser(['role:' . Auth::USER_ROLE_APP]));
$this->assertEquals(false, Auth::isPrivilegedUser(['role:' . Auth::USER_ROLE_SYSTEM]));
$this->assertEquals(false, Auth::isPrivilegedUser([Role::guests()->toString()]));
$this->assertEquals(false, Auth::isPrivilegedUser([Role::users()->toString()]));
$this->assertEquals(true, Auth::isPrivilegedUser([Auth::USER_ROLE_ADMIN]));
$this->assertEquals(true, Auth::isPrivilegedUser([Auth::USER_ROLE_DEVELOPER]));
$this->assertEquals(true, Auth::isPrivilegedUser([Auth::USER_ROLE_OWNER]));
$this->assertEquals(false, Auth::isPrivilegedUser([Auth::USER_ROLE_APPS]));
$this->assertEquals(false, Auth::isPrivilegedUser([Auth::USER_ROLE_SYSTEM]));
$this->assertEquals(false, Auth::isPrivilegedUser(['role:' . Auth::USER_ROLE_APP, 'role:' . Auth::USER_ROLE_APP]));
$this->assertEquals(false, Auth::isPrivilegedUser(['role:' . Auth::USER_ROLE_APP, 'role:' . Auth::USER_ROLE_GUEST]));
$this->assertEquals(true, Auth::isPrivilegedUser(['role:' . Auth::USER_ROLE_OWNER, 'role:' . Auth::USER_ROLE_GUEST]));
$this->assertEquals(true, Auth::isPrivilegedUser(['role:' . Auth::USER_ROLE_OWNER, 'role:' . Auth::USER_ROLE_ADMIN, 'role:' . Auth::USER_ROLE_DEVELOPER]));
$this->assertEquals(false, Auth::isPrivilegedUser([Auth::USER_ROLE_APPS, Auth::USER_ROLE_APPS]));
$this->assertEquals(false, Auth::isPrivilegedUser([Auth::USER_ROLE_APPS, Role::guests()->toString()]));
$this->assertEquals(true, Auth::isPrivilegedUser([Auth::USER_ROLE_OWNER, Role::guests()->toString()]));
$this->assertEquals(true, Auth::isPrivilegedUser([Auth::USER_ROLE_OWNER, Auth::USER_ROLE_ADMIN, Auth::USER_ROLE_DEVELOPER]));
}
public function testIsAppUser(): void
{
$this->assertEquals(false, Auth::isAppUser([]));
$this->assertEquals(false, Auth::isAppUser(['role:' . Auth::USER_ROLE_GUEST]));
$this->assertEquals(false, Auth::isAppUser(['role:' . Auth::USER_ROLE_MEMBER]));
$this->assertEquals(false, Auth::isAppUser(['role:' . Auth::USER_ROLE_ADMIN]));
$this->assertEquals(false, Auth::isAppUser(['role:' . Auth::USER_ROLE_DEVELOPER]));
$this->assertEquals(false, Auth::isAppUser(['role:' . Auth::USER_ROLE_OWNER]));
$this->assertEquals(true, Auth::isAppUser(['role:' . Auth::USER_ROLE_APP]));
$this->assertEquals(false, Auth::isAppUser(['role:' . Auth::USER_ROLE_SYSTEM]));
$this->assertEquals(false, Auth::isAppUser([Role::guests()->toString()]));
$this->assertEquals(false, Auth::isAppUser([Role::users()->toString()]));
$this->assertEquals(false, Auth::isAppUser([Auth::USER_ROLE_ADMIN]));
$this->assertEquals(false, Auth::isAppUser([Auth::USER_ROLE_DEVELOPER]));
$this->assertEquals(false, Auth::isAppUser([Auth::USER_ROLE_OWNER]));
$this->assertEquals(true, Auth::isAppUser([Auth::USER_ROLE_APPS]));
$this->assertEquals(false, Auth::isAppUser([Auth::USER_ROLE_SYSTEM]));
$this->assertEquals(true, Auth::isAppUser(['role:' . Auth::USER_ROLE_APP, 'role:' . Auth::USER_ROLE_APP]));
$this->assertEquals(true, Auth::isAppUser(['role:' . Auth::USER_ROLE_APP, 'role:' . Auth::USER_ROLE_GUEST]));
$this->assertEquals(false, Auth::isAppUser(['role:' . Auth::USER_ROLE_OWNER, 'role:' . Auth::USER_ROLE_GUEST]));
$this->assertEquals(false, Auth::isAppUser(['role:' . Auth::USER_ROLE_OWNER, 'role:' . Auth::USER_ROLE_ADMIN, 'role:' . Auth::USER_ROLE_DEVELOPER]));
$this->assertEquals(true, Auth::isAppUser([Auth::USER_ROLE_APPS, Auth::USER_ROLE_APPS]));
$this->assertEquals(true, Auth::isAppUser([Auth::USER_ROLE_APPS, Role::guests()->toString()]));
$this->assertEquals(false, Auth::isAppUser([Auth::USER_ROLE_OWNER, Role::guests()->toString()]));
$this->assertEquals(false, Auth::isAppUser([Auth::USER_ROLE_OWNER, Auth::USER_ROLE_ADMIN, Auth::USER_ROLE_DEVELOPER]));
}
public function testGuestRoles(): void
@ -342,23 +344,23 @@ class AuthTest extends TestCase
$roles = Auth::getRoles($user);
$this->assertCount(1, $roles);
$this->assertContains('role:guest', $roles);
$this->assertContains(Role::guests()->toString(), $roles);
}
public function testUserRoles(): void
{
$user = new Document([
'$id' => '123',
'$id' => ID::custom('123'),
'memberships' => [
[
'teamId' => 'abc',
'teamId' => ID::custom('abc'),
'roles' => [
'administrator',
'moderator'
]
],
[
'teamId' => 'def',
'teamId' => ID::custom('def'),
'roles' => [
'guest'
]
@ -369,7 +371,7 @@ class AuthTest extends TestCase
$roles = Auth::getRoles($user);
$this->assertCount(7, $roles);
$this->assertContains('role:member', $roles);
$this->assertContains('users', $roles);
$this->assertContains('user:123', $roles);
$this->assertContains('team:abc', $roles);
$this->assertContains('team:abc/administrator', $roles);
@ -380,19 +382,19 @@ class AuthTest extends TestCase
public function testPrivilegedUserRoles(): void
{
Authorization::setRole('role:' . Auth::USER_ROLE_OWNER);
Authorization::setRole(Auth::USER_ROLE_OWNER);
$user = new Document([
'$id' => '123',
'$id' => ID::custom('123'),
'memberships' => [
[
'teamId' => 'abc',
'teamId' => ID::custom('abc'),
'roles' => [
'administrator',
'moderator'
]
],
[
'teamId' => 'def',
'teamId' => ID::custom('def'),
'roles' => [
'guest'
]
@ -403,7 +405,7 @@ class AuthTest extends TestCase
$roles = Auth::getRoles($user);
$this->assertCount(5, $roles);
$this->assertNotContains('role:member', $roles);
$this->assertNotContains('users', $roles);
$this->assertNotContains('user:123', $roles);
$this->assertContains('team:abc', $roles);
$this->assertContains('team:abc/administrator', $roles);
@ -414,19 +416,19 @@ class AuthTest extends TestCase
public function testAppUserRoles(): void
{
Authorization::setRole('role:' . Auth::USER_ROLE_APP);
Authorization::setRole(Auth::USER_ROLE_APPS);
$user = new Document([
'$id' => '123',
'$id' => ID::custom('123'),
'memberships' => [
[
'teamId' => 'abc',
'teamId' => ID::custom('abc'),
'roles' => [
'administrator',
'moderator'
]
],
[
'teamId' => 'def',
'teamId' => ID::custom('def'),
'roles' => [
'guest'
]
@ -437,7 +439,7 @@ class AuthTest extends TestCase
$roles = Auth::getRoles($user);
$this->assertCount(5, $roles);
$this->assertNotContains('role:member', $roles);
$this->assertNotContains('users', $roles);
$this->assertNotContains('user:123', $roles);
$this->assertContains('team:abc', $roles);
$this->assertContains('team:abc/administrator', $roles);

View file

@ -6,6 +6,8 @@ use Appwrite\Auth\Auth;
use Utopia\Database\Document;
use Appwrite\Messaging\Adapter\Realtime;
use PHPUnit\Framework\TestCase;
use Utopia\Database\ID;
use Utopia\Database\Role;
class MessagingChannelsTest extends TestCase
{
@ -49,12 +51,14 @@ class MessagingChannelsTest extends TestCase
for ($i = 0; $i < $this->connectionsPerChannel; $i++) {
foreach ($this->allChannels as $index => $channel) {
$user = new Document([
'$id' => 'user' . $this->connectionsCount,
'$id' => ID::custom('user' . $this->connectionsCount),
'memberships' => [
[
'teamId' => 'team' . $i,
'teamId' => ID::custom('team' . $i),
'roles' => [
empty($index % 2) ? 'admin' : 'member'
empty($index % 2)
? Auth::USER_ROLE_ADMIN
: Role::users()->toString(),
]
]
]
@ -118,8 +122,8 @@ class MessagingChannelsTest extends TestCase
* - XXX users
* - XXX teams
* - XXX team roles (2 roles per team)
* - 1 role:guest
* - 1 role:member
* - 1 guests
* - 1 users
*/
$this->assertCount(($this->connectionsAuthenticated + (3 * $this->connectionsPerChannel) + 2), $this->realtime->subscriptions['1']);
@ -146,14 +150,14 @@ class MessagingChannelsTest extends TestCase
}
/**
* Tests Wildcard (role:all) Permissions on every channel.
* Tests Wildcard ("any") Permissions on every channel.
*/
public function testWildcardPermission(): void
{
foreach ($this->allChannels as $index => $channel) {
$event = [
'project' => '1',
'roles' => ['role:all'],
'roles' => [Role::any()->toString()],
'data' => [
'channels' => [
0 => $channel,
@ -179,7 +183,10 @@ class MessagingChannelsTest extends TestCase
public function testRolePermissions(): void
{
$roles = ['role:guest', 'role:member'];
$roles = [
Role::guests()->toString(),
Role::users()->toString()
];
foreach ($this->allChannels as $index => $channel) {
foreach ($roles as $role) {
$permissions = [$role];
@ -216,7 +223,7 @@ class MessagingChannelsTest extends TestCase
foreach ($this->allChannels as $index => $channel) {
$permissions = [];
for ($i = 0; $i < $this->connectionsPerChannel; $i++) {
$permissions[] = 'user:user' . (!empty($i) ? $i : '') . $index;
$permissions[] = Role::user(ID::custom('user' . (!empty($i) ? $i : '') . $index))->toString();
}
$event = [
'project' => '1',
@ -250,7 +257,7 @@ class MessagingChannelsTest extends TestCase
$permissions = [];
for ($i = 0; $i < $this->connectionsPerChannel; $i++) {
$permissions[] = 'team:team' . $i;
$permissions[] = Role::team(ID::custom('team' . $i))->toString();
}
$event = [
'project' => '1',
@ -276,7 +283,14 @@ class MessagingChannelsTest extends TestCase
$this->assertStringEndsWith($index, $receiver);
}
$permissions = ['team:team' . $index . '/' . (empty($index % 2) ? 'admin' : 'member')];
$permissions = [
Role::team(
ID::custom('team' . $index),
(empty($index % 2)
? Auth::USER_ROLE_ADMIN
: Role::users()->toString())
)->toString()
];
$event = [
'project' => '1',

View file

@ -4,6 +4,8 @@ namespace Tests\Unit\Messaging;
use Appwrite\Messaging\Adapter\Realtime;
use PHPUnit\Framework\TestCase;
use Utopia\Database\ID;
use Utopia\Database\Role;
class MessagingGuestTest extends TestCase
{
@ -14,13 +16,13 @@ class MessagingGuestTest extends TestCase
$realtime->subscribe(
'1',
1,
['role:guest'],
[Role::guests()->toString()],
['files' => 0, 'documents' => 0, 'documents.789' => 0, 'account.123' => 0]
);
$event = [
'project' => '1',
'roles' => ['role:all'],
'roles' => [Role::any()->toString()],
'data' => [
'channels' => [
0 => 'documents',
@ -34,68 +36,68 @@ class MessagingGuestTest extends TestCase
$this->assertCount(1, $receivers);
$this->assertEquals(1, $receivers[0]);
$event['roles'] = ['role:guest'];
$event['roles'] = [Role::guests()->toString()];
$receivers = $realtime->getSubscribers($event);
$this->assertCount(1, $receivers);
$this->assertEquals(1, $receivers[0]);
$event['roles'] = ['role:member'];
$event['roles'] = [Role::users()->toString()];
$receivers = $realtime->getSubscribers($event);
$this->assertEmpty($receivers);
$event['roles'] = ['user:123'];
$event['roles'] = [Role::user(ID::custom('123'))->toString()];
$receivers = $realtime->getSubscribers($event);
$this->assertEmpty($receivers);
$event['roles'] = ['team:abc'];
$event['roles'] = [Role::team(ID::custom('abc'))->toString()];
$receivers = $realtime->getSubscribers($event);
$this->assertEmpty($receivers);
$event['roles'] = ['team:abc/administrator'];
$event['roles'] = [Role::team(ID::custom('abc'), 'administrator')->toString()];
$receivers = $realtime->getSubscribers($event);
$this->assertEmpty($receivers);
$event['roles'] = ['team:abc/god'];
$event['roles'] = [Role::team(ID::custom('abc'), 'god')->toString()];
$receivers = $realtime->getSubscribers($event);
$this->assertEmpty($receivers);
$event['roles'] = ['team:def'];
$event['roles'] = [Role::team(ID::custom('def'))->toString()];
$receivers = $realtime->getSubscribers($event);
$this->assertEmpty($receivers);
$event['roles'] = ['team:def/guest'];
$event['roles'] = [Role::team(ID::custom('def'), 'guest')->toString()];
$receivers = $realtime->getSubscribers($event);
$this->assertEmpty($receivers);
$event['roles'] = ['user:456'];
$event['roles'] = [Role::user(ID::custom('456'))->toString()];
$receivers = $realtime->getSubscribers($event);
$this->assertEmpty($receivers);
$event['roles'] = ['team:def/member'];
$event['roles'] = [Role::team(ID::custom('def'), 'member')->toString()];
$receivers = $realtime->getSubscribers($event);
$this->assertEmpty($receivers);
$event['roles'] = ['role:all'];
$event['roles'] = [Role::any()->toString()];
$event['data']['channels'] = ['documents.123'];
$receivers = $realtime->getSubscribers($event);

Some files were not shown because too many files have changed in this diff Show more