From fb95a05599bcf1aa4a429ea3eaad1dd375b0bae3 Mon Sep 17 00:00:00 2001 From: shimon Date: Thu, 27 Nov 2025 12:13:07 +0200 Subject: [PATCH] teams update --- app/controllers/api/teams.php | 80 ++++---- composer.lock | 136 ++++++------- .../Utopia/Database/Documents/User.php | 182 ++++++++++++++++++ 3 files changed, 292 insertions(+), 106 deletions(-) create mode 100644 src/Appwrite/Utopia/Database/Documents/User.php diff --git a/app/controllers/api/teams.php b/app/controllers/api/teams.php index d992672938..72120c6df0 100644 --- a/app/controllers/api/teams.php +++ b/app/controllers/api/teams.php @@ -1,6 +1,5 @@ inject('queueForEvents') ->action(function (string $teamId, string $name, array $roles, Response $response, Document $user, Database $dbForProject, Authorization $authorization, Event $queueForEvents) { - $isPrivilegedUser = Auth::isPrivilegedUser($authorization->getRoles()); - $isAppUser = Auth::isAppUser($authorization->getRoles()); + $isPrivilegedUser = User::isPrivilegedUser($authorization->getRoles()); + $isAppUser = User::isAppUser($authorization->getRoles()); $teamId = $teamId == 'unique()' ? ID::unique() : $teamId; @@ -177,6 +180,7 @@ App::get('/v1/teams') ->inject('dbForProject') ->action(function (array $queries, string $search, bool $includeTotal, Response $response, Database $dbForProject) { + try { $queries = Query::parseQueries($queries); } catch (QueryException $e) { @@ -475,10 +479,9 @@ App::post('/v1/teams/:teamId/memberships') ->param('phone', '', new Phone(), 'Phone number. Format this number with a leading \'+\' and a country code, e.g., +16175551212.', true) ->param('roles', [], function (Document $project) { if ($project->getId() === 'console') { - ; $roles = array_keys(Config::getParam('roles', [])); - array_filter($roles, function ($role) { - return !in_array($role, [Auth::USER_ROLE_APPS, Auth::USER_ROLE_GUESTS, Auth::USER_ROLE_USERS]); + $roles = array_filter($roles, function ($role) { + return !in_array($role, [User::ROLE_APPS, User::ROLE_GUESTS, User::ROLE_USERS]); }); return new ArrayList(new WhiteList($roles), APP_LIMIT_ARRAY_PARAMS_SIZE); } @@ -498,9 +501,11 @@ App::post('/v1/teams/:teamId/memberships') ->inject('timelimit') ->inject('queueForStatsUsage') ->inject('plan') - ->action(function (string $teamId, string $email, string $userId, string $phone, array $roles, string $url, string $name, Response $response, Document $project, Document $user, Database $dbForProject, Authorization $authorization, Locale $locale, Mail $queueForMails, Messaging $queueForMessaging, Event $queueForEvents, callable $timelimit, StatsUsage $queueForStatsUsage, array $plan) { - $isAppUser = Auth::isAppUser($authorization->getRoles()); - $isPrivilegedUser = Auth::isPrivilegedUser($authorization->getRoles()); + ->inject('proofForPassword') + ->inject('proofForToken') + ->action(function (string $teamId, string $email, string $userId, string $phone, array $roles, string $url, string $name, Response $response, Document $project, Document $user, Database $dbForProject, Authorization $authorization, Locale $locale, Mail $queueForMails, Messaging $queueForMessaging, Event $queueForEvents, callable $timelimit, StatsUsage $queueForStatsUsage, array $plan, Password $proofForPassword, Token $proofForToken) { + $isAppUser = User::isApp(Authorization::getRoles()); + $isPrivilegedUser = User::isPrivileged(Authorization::getRoles()); $url = htmlentities($url); if (empty($url)) { @@ -570,6 +575,8 @@ App::post('/v1/teams/:teamId/memberships') } try { + $userId = ID::unique(); + $hash = $proofForPassword->hash($proofForPassword->generate()); $emailCanonical = new Email($email); } catch (Throwable) { $emailCanonical = null; @@ -590,9 +597,9 @@ App::post('/v1/teams/:teamId/memberships') 'emailVerification' => false, 'status' => true, // TODO: Set password empty? - 'password' => Auth::passwordHash(Auth::passwordGenerator(), Auth::DEFAULT_ALGO, Auth::DEFAULT_ALGO_OPTIONS), - 'hash' => Auth::DEFAULT_ALGO, - 'hashOptions' => Auth::DEFAULT_ALGO_OPTIONS, + 'password' => $hash, + 'hash' => $proofForPassword->getHash()->getName(), + 'hashOptions' => $proofForPassword->getHash()->getOptions(), /** * Set the password update time to 0 for users created using * team invite and OAuth to allow password updates without an @@ -632,7 +639,7 @@ App::post('/v1/teams/:teamId/memberships') Query::equal('teamInternalId', [$team->getSequence()]), ]); - $secret = Auth::tokenGenerator(); + $secret = $proofForToken->generate(); if ($membership->isEmpty()) { $membershipId = ID::unique(); $membership = new Document([ @@ -652,7 +659,7 @@ App::post('/v1/teams/:teamId/memberships') 'invited' => DateTime::now(), 'joined' => ($isPrivilegedUser || $isAppUser) ? DateTime::now() : null, 'confirm' => ($isPrivilegedUser || $isAppUser), - 'secret' => Auth::hash($secret), + 'secret' => $proofForToken->hash($secret), 'search' => implode(' ', [$membershipId, $invitee->getId()]) ]); @@ -663,9 +670,8 @@ App::post('/v1/teams/:teamId/memberships') if ($isPrivilegedUser || $isAppUser) { $authorization->skip(fn () => $dbForProject->increaseDocumentAttribute('teams', $team->getId(), 'total', 1)); } - } elseif ($membership->getAttribute('confirm') === false) { - $membership->setAttribute('secret', Auth::hash($secret)); + $membership->setAttribute('secret', $proofForToken->hash($secret)); $membership->setAttribute('invited', DateTime::now()); if ($isPrivilegedUser || $isAppUser) { @@ -768,7 +774,6 @@ App::post('/v1/teams/:teamId/memberships') ->setName($invitee->getAttribute('name', '')) ->setVariables($emailVariables) ->trigger(); - } elseif (!empty($phone)) { if (empty(System::getEnv('_APP_SMS_PROVIDER'))) { throw new Exception(Exception::GENERAL_PHONE_DISABLED, 'Phone provider not configured'); @@ -934,7 +939,7 @@ App::get('/v1/teams/:teamId/memberships') $roles = $authorization->getRoles(); $isPrivilegedUser = Auth::isPrivilegedUser($roles); - $isAppUser = Auth::isAppUser($roles); + $isAppUser = User::isAppUser($roles); $membershipsPrivacy = array_map(function ($privacy) use ($isPrivilegedUser, $isAppUser) { return $privacy || $isPrivilegedUser || $isAppUser; @@ -1026,7 +1031,7 @@ App::get('/v1/teams/:teamId/memberships/:membershipId') $roles = $authorization->getRoles(); $isPrivilegedUser = Auth::isPrivilegedUser($roles); - $isAppUser = Auth::isAppUser($roles); + $isAppUser = User::isAppUser($roles); $membershipsPrivacy = array_map(function ($privacy) use ($isPrivilegedUser, $isAppUser) { return $privacy || $isPrivilegedUser || $isAppUser; @@ -1091,8 +1096,8 @@ App::patch('/v1/teams/:teamId/memberships/:membershipId') ->param('roles', [], function (Document $project) { if ($project->getId() === 'console') { $roles = array_keys(Config::getParam('roles', [])); - array_filter($roles, function ($role) { - return !in_array($role, [Auth::USER_ROLE_APPS, Auth::USER_ROLE_GUESTS, Auth::USER_ROLE_USERS]); + $roles = array_filter($roles, function ($role) { + return !in_array($role, [User::ROLE_APPS, User::ROLE_GUESTS, User::ROLE_USERS]); }); return new ArrayList(new WhiteList($roles), APP_LIMIT_ARRAY_PARAMS_SIZE); } @@ -1122,8 +1127,8 @@ App::patch('/v1/teams/:teamId/memberships/:membershipId') throw new Exception(Exception::USER_NOT_FOUND); } - $isPrivilegedUser = Auth::isPrivilegedUser($authorization->getRoles()); - $isAppUser = Auth::isAppUser($authorization->getRoles()); + $isPrivilegedUser = User::isPrivilegedUser($authorization->getRoles()); + $isAppUser = User::isAppUser($authorization->getRoles()); $isOwner = $authorization->hasRole('team:' . $team->getId() . '/owner'); if ($project->getId() === 'console') { @@ -1209,7 +1214,9 @@ App::patch('/v1/teams/:teamId/memberships/:membershipId/status') ->inject('project') ->inject('geodb') ->inject('queueForEvents') - ->action(function (string $teamId, string $membershipId, string $userId, string $secret, Request $request, Response $response, Document $user, Database $dbForProject, Authorization $authorization, Document $project, Reader $geodb, Event $queueForEvents) { + ->inject('store') + ->inject('proofForToken') + ->action(function (string $teamId, string $membershipId, string $userId, string $secret, Request $request, Response $response, Document $user, Database $dbForProject, Authorization $authorization, $project, Reader $geodb, Event $queueForEvents, Store $store, Token $proofForToken) { $protocol = $request->getProtocol(); $membership = $dbForProject->getDocument('memberships', $membershipId); @@ -1228,7 +1235,7 @@ App::patch('/v1/teams/:teamId/memberships/:membershipId/status') throw new Exception(Exception::TEAM_MEMBERSHIP_MISMATCH); } - if (Auth::hash($secret) !== $membership->getAttribute('secret')) { + if (!$proofForToken->verify($secret, $membership->getAttribute('secret'))) { throw new Exception(Exception::TEAM_INVALID_SECRET); } @@ -1262,9 +1269,9 @@ App::patch('/v1/teams/:teamId/memberships/:membershipId/status') $detector = new Detector($request->getUserAgent('UNKNOWN')); $record = $geodb->get($request->getIP()); - $authDuration = $project->getAttribute('auths', [])['duration'] ?? Auth::TOKEN_EXPIRATION_LOGIN_LONG; + $authDuration = $project->getAttribute('auths', [])['duration'] ?? TOKEN_EXPIRATION_LOGIN_LONG; $expire = DateTime::addSeconds(new \DateTime(), $authDuration); - $secret = Auth::tokenGenerator(); + $secret = $proofForToken->generate(); $session = new Document(array_merge([ '$id' => ID::unique(), '$permissions' => [ @@ -1274,9 +1281,9 @@ App::patch('/v1/teams/:teamId/memberships/:membershipId/status') ], 'userId' => $user->getId(), 'userInternalId' => $user->getSequence(), - 'provider' => Auth::SESSION_PROVIDER_EMAIL, + 'provider' => SESSION_PROVIDER_EMAIL, 'providerUid' => $user->getAttribute('email'), - 'secret' => Auth::hash($secret), // One way hash encryption to protect DB leak + 'secret' => $proofForToken->hash($secret), // One way hash encryption to protect DB leak 'userAgent' => $request->getUserAgent('UNKNOWN'), 'ip' => $request->getIP(), 'factors' => ['email'], @@ -1288,14 +1295,19 @@ App::patch('/v1/teams/:teamId/memberships/:membershipId/status') $authorization->addRole(Role::user($userId)->toString()); + $encoded = $store + ->setProperty('id', $user->getId()) + ->setProperty('secret', $secret) + ->encode(); + if (!Config::getParam('domainVerification')) { - $response->addHeader('X-Fallback-Cookies', \json_encode([Auth::$cookieName => Auth::encodeSession($user->getId(), $secret)])); + $response->addHeader('X-Fallback-Cookies', \json_encode([$store->getKey() => $encoded])); } $response ->addCookie( - name: Auth::$cookieName . '_legacy', - value: Auth::encodeSession($user->getId(), $secret), + name: $store->getKey() . '_legacy', + value: $encoded, expire: (new \DateTime($expire))->getTimestamp(), path: '/', domain: Config::getParam('cookieDomain'), @@ -1303,8 +1315,8 @@ App::patch('/v1/teams/:teamId/memberships/:membershipId/status') httponly: true ) ->addCookie( - name: Auth::$cookieName, - value: Auth::encodeSession($user->getId(), $secret), + name: $store->getKey(), + value: $encoded, expire: (new \DateTime($expire))->getTimestamp(), path: '/', domain: Config::getParam('cookieDomain'), diff --git a/composer.lock b/composer.lock index 2eac78f02e..045e53ea85 100644 --- a/composer.lock +++ b/composer.lock @@ -7874,47 +7874,39 @@ }, { "name": "symfony/console", - "version": "v7.3.6", + "version": "v8.0.0", "source": { "type": "git", "url": "https://github.com/symfony/console.git", - "reference": "c28ad91448f86c5f6d9d2c70f0cf68bf135f252a" + "reference": "307d3cf852f5ead3618ac60ecbedbdd512c348b1" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/console/zipball/c28ad91448f86c5f6d9d2c70f0cf68bf135f252a", - "reference": "c28ad91448f86c5f6d9d2c70f0cf68bf135f252a", + "url": "https://api.github.com/repos/symfony/console/zipball/307d3cf852f5ead3618ac60ecbedbdd512c348b1", + "reference": "307d3cf852f5ead3618ac60ecbedbdd512c348b1", "shasum": "" }, "require": { - "php": ">=8.2", - "symfony/deprecation-contracts": "^2.5|^3", - "symfony/polyfill-mbstring": "~1.0", + "php": ">=8.4", + "symfony/polyfill-mbstring": "^1.0", "symfony/service-contracts": "^2.5|^3", - "symfony/string": "^7.2" - }, - "conflict": { - "symfony/dependency-injection": "<6.4", - "symfony/dotenv": "<6.4", - "symfony/event-dispatcher": "<6.4", - "symfony/lock": "<6.4", - "symfony/process": "<6.4" + "symfony/string": "^7.4|^8.0" }, "provide": { "psr/log-implementation": "1.0|2.0|3.0" }, "require-dev": { "psr/log": "^1|^2|^3", - "symfony/config": "^6.4|^7.0", - "symfony/dependency-injection": "^6.4|^7.0", - "symfony/event-dispatcher": "^6.4|^7.0", - "symfony/http-foundation": "^6.4|^7.0", - "symfony/http-kernel": "^6.4|^7.0", - "symfony/lock": "^6.4|^7.0", - "symfony/messenger": "^6.4|^7.0", - "symfony/process": "^6.4|^7.0", - "symfony/stopwatch": "^6.4|^7.0", - "symfony/var-dumper": "^6.4|^7.0" + "symfony/config": "^7.4|^8.0", + "symfony/dependency-injection": "^7.4|^8.0", + "symfony/event-dispatcher": "^7.4|^8.0", + "symfony/http-foundation": "^7.4|^8.0", + "symfony/http-kernel": "^7.4|^8.0", + "symfony/lock": "^7.4|^8.0", + "symfony/messenger": "^7.4|^8.0", + "symfony/process": "^7.4|^8.0", + "symfony/stopwatch": "^7.4|^8.0", + "symfony/var-dumper": "^7.4|^8.0" }, "type": "library", "autoload": { @@ -7948,7 +7940,7 @@ "terminal" ], "support": { - "source": "https://github.com/symfony/console/tree/v7.3.6" + "source": "https://github.com/symfony/console/tree/v8.0.0" }, "funding": [ { @@ -7968,29 +7960,29 @@ "type": "tidelift" } ], - "time": "2025-11-04T01:21:42+00:00" + "time": "2025-11-21T13:19:49+00:00" }, { "name": "symfony/filesystem", - "version": "v7.3.6", + "version": "v8.0.0", "source": { "type": "git", "url": "https://github.com/symfony/filesystem.git", - "reference": "e9bcfd7837928ab656276fe00464092cc9e1826a" + "reference": "7fc96ae83372620eaba3826874f46e26295768ca" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/filesystem/zipball/e9bcfd7837928ab656276fe00464092cc9e1826a", - "reference": "e9bcfd7837928ab656276fe00464092cc9e1826a", + "url": "https://api.github.com/repos/symfony/filesystem/zipball/7fc96ae83372620eaba3826874f46e26295768ca", + "reference": "7fc96ae83372620eaba3826874f46e26295768ca", "shasum": "" }, "require": { - "php": ">=8.2", + "php": ">=8.4", "symfony/polyfill-ctype": "~1.8", "symfony/polyfill-mbstring": "~1.8" }, "require-dev": { - "symfony/process": "^6.4|^7.0" + "symfony/process": "^7.4|^8.0" }, "type": "library", "autoload": { @@ -8018,7 +8010,7 @@ "description": "Provides basic utilities for the filesystem", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/filesystem/tree/v7.3.6" + "source": "https://github.com/symfony/filesystem/tree/v8.0.0" }, "funding": [ { @@ -8038,27 +8030,27 @@ "type": "tidelift" } ], - "time": "2025-11-05T09:52:27+00:00" + "time": "2025-11-05T14:36:47+00:00" }, { "name": "symfony/finder", - "version": "v7.3.5", + "version": "v8.0.0", "source": { "type": "git", "url": "https://github.com/symfony/finder.git", - "reference": "9f696d2f1e340484b4683f7853b273abff94421f" + "reference": "7598dd5770580fa3517ec83e8da0c9b9e01f4291" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/finder/zipball/9f696d2f1e340484b4683f7853b273abff94421f", - "reference": "9f696d2f1e340484b4683f7853b273abff94421f", + "url": "https://api.github.com/repos/symfony/finder/zipball/7598dd5770580fa3517ec83e8da0c9b9e01f4291", + "reference": "7598dd5770580fa3517ec83e8da0c9b9e01f4291", "shasum": "" }, "require": { - "php": ">=8.2" + "php": ">=8.4" }, "require-dev": { - "symfony/filesystem": "^6.4|^7.0" + "symfony/filesystem": "^7.4|^8.0" }, "type": "library", "autoload": { @@ -8086,7 +8078,7 @@ "description": "Finds files and directories via an intuitive fluent interface", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/finder/tree/v7.3.5" + "source": "https://github.com/symfony/finder/tree/v8.0.0" }, "funding": [ { @@ -8106,24 +8098,24 @@ "type": "tidelift" } ], - "time": "2025-10-15T18:45:57+00:00" + "time": "2025-11-05T14:36:47+00:00" }, { "name": "symfony/options-resolver", - "version": "v7.3.3", + "version": "v8.0.0", "source": { "type": "git", "url": "https://github.com/symfony/options-resolver.git", - "reference": "0ff2f5c3df08a395232bbc3c2eb7e84912df911d" + "reference": "d2b592535ffa6600c265a3893a7f7fd2bad82dd7" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/options-resolver/zipball/0ff2f5c3df08a395232bbc3c2eb7e84912df911d", - "reference": "0ff2f5c3df08a395232bbc3c2eb7e84912df911d", + "url": "https://api.github.com/repos/symfony/options-resolver/zipball/d2b592535ffa6600c265a3893a7f7fd2bad82dd7", + "reference": "d2b592535ffa6600c265a3893a7f7fd2bad82dd7", "shasum": "" }, "require": { - "php": ">=8.2", + "php": ">=8.4", "symfony/deprecation-contracts": "^2.5|^3" }, "type": "library", @@ -8157,7 +8149,7 @@ "options" ], "support": { - "source": "https://github.com/symfony/options-resolver/tree/v7.3.3" + "source": "https://github.com/symfony/options-resolver/tree/v8.0.0" }, "funding": [ { @@ -8177,7 +8169,7 @@ "type": "tidelift" } ], - "time": "2025-08-05T10:16:07+00:00" + "time": "2025-11-12T15:55:31+00:00" }, { "name": "symfony/polyfill-ctype", @@ -8511,20 +8503,20 @@ }, { "name": "symfony/process", - "version": "v7.3.4", + "version": "v8.0.0", "source": { "type": "git", "url": "https://github.com/symfony/process.git", - "reference": "f24f8f316367b30810810d4eb30c543d7003ff3b" + "reference": "a0a750500c4ce900d69ba4e9faf16f82c10ee149" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/process/zipball/f24f8f316367b30810810d4eb30c543d7003ff3b", - "reference": "f24f8f316367b30810810d4eb30c543d7003ff3b", + "url": "https://api.github.com/repos/symfony/process/zipball/a0a750500c4ce900d69ba4e9faf16f82c10ee149", + "reference": "a0a750500c4ce900d69ba4e9faf16f82c10ee149", "shasum": "" }, "require": { - "php": ">=8.2" + "php": ">=8.4" }, "type": "library", "autoload": { @@ -8552,7 +8544,7 @@ "description": "Executes commands in sub-processes", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/process/tree/v7.3.4" + "source": "https://github.com/symfony/process/tree/v8.0.0" }, "funding": [ { @@ -8572,38 +8564,38 @@ "type": "tidelift" } ], - "time": "2025-09-11T10:12:26+00:00" + "time": "2025-10-16T16:25:44+00:00" }, { "name": "symfony/string", - "version": "v7.3.4", + "version": "v8.0.0", "source": { "type": "git", "url": "https://github.com/symfony/string.git", - "reference": "f96476035142921000338bad71e5247fbc138872" + "reference": "f929eccf09531078c243df72398560e32fa4cf4f" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/string/zipball/f96476035142921000338bad71e5247fbc138872", - "reference": "f96476035142921000338bad71e5247fbc138872", + "url": "https://api.github.com/repos/symfony/string/zipball/f929eccf09531078c243df72398560e32fa4cf4f", + "reference": "f929eccf09531078c243df72398560e32fa4cf4f", "shasum": "" }, "require": { - "php": ">=8.2", - "symfony/polyfill-ctype": "~1.8", - "symfony/polyfill-intl-grapheme": "~1.0", - "symfony/polyfill-intl-normalizer": "~1.0", - "symfony/polyfill-mbstring": "~1.0" + "php": ">=8.4", + "symfony/polyfill-ctype": "^1.8", + "symfony/polyfill-intl-grapheme": "^1.33", + "symfony/polyfill-intl-normalizer": "^1.0", + "symfony/polyfill-mbstring": "^1.0" }, "conflict": { "symfony/translation-contracts": "<2.5" }, "require-dev": { - "symfony/emoji": "^7.1", - "symfony/http-client": "^6.4|^7.0", - "symfony/intl": "^6.4|^7.0", + "symfony/emoji": "^7.4|^8.0", + "symfony/http-client": "^7.4|^8.0", + "symfony/intl": "^7.4|^8.0", "symfony/translation-contracts": "^2.5|^3.0", - "symfony/var-exporter": "^6.4|^7.0" + "symfony/var-exporter": "^7.4|^8.0" }, "type": "library", "autoload": { @@ -8642,7 +8634,7 @@ "utf8" ], "support": { - "source": "https://github.com/symfony/string/tree/v7.3.4" + "source": "https://github.com/symfony/string/tree/v8.0.0" }, "funding": [ { @@ -8662,7 +8654,7 @@ "type": "tidelift" } ], - "time": "2025-09-11T14:36:48+00:00" + "time": "2025-09-11T14:37:55+00:00" }, { "name": "textalk/websocket", diff --git a/src/Appwrite/Utopia/Database/Documents/User.php b/src/Appwrite/Utopia/Database/Documents/User.php new file mode 100644 index 0000000000..a85b0a897c --- /dev/null +++ b/src/Appwrite/Utopia/Database/Documents/User.php @@ -0,0 +1,182 @@ +getAttribute('email'); + } + + public function getPhone(): ?string + { + return $this->getAttribute('phone'); + } + + /** + * Returns all roles for a user. + * + * @return array + */ + public function getRoles(): array + { + $roles = []; + + if (!$this->isPrivileged(Authorization::getRoles()) && !$this->isApp(Authorization::getRoles())) { + if ($this->getId()) { + $roles[] = Role::user($this->getId())->toString(); + $roles[] = Role::users()->toString(); + + $emailVerified = $this->getAttribute('emailVerification', false); + $phoneVerified = $this->getAttribute('phoneVerification', false); + + if ($emailVerified || $phoneVerified) { + $roles[] = Role::user($this->getId(), Roles::DIMENSION_VERIFIED)->toString(); + $roles[] = Role::users(Roles::DIMENSION_VERIFIED)->toString(); + } else { + $roles[] = Role::user($this->getId(), Roles::DIMENSION_UNVERIFIED)->toString(); + $roles[] = Role::users(Roles::DIMENSION_UNVERIFIED)->toString(); + } + } else { + return [Role::guests()->toString()]; + } + } + + foreach ($this->getAttribute('memberships', []) as $node) { + if (!isset($node['confirm']) || !$node['confirm']) { + continue; + } + + if (isset($node['$id']) && isset($node['teamId'])) { + $roles[] = Role::team($node['teamId'])->toString(); + $roles[] = Role::member($node['$id'])->toString(); + + if (isset($node['roles'])) { + foreach ($node['roles'] as $nodeRole) { // Set all team roles + $roles[] = Role::team($node['teamId'], $nodeRole)->toString(); + } + } + } + } + + foreach ($this->getAttribute('labels', []) as $label) { + $roles[] = 'label:' . $label; + } + + return $roles; + } + + /** + * Check if user is anonymous. + * + * @param Document $this + * @return bool + */ + public function isAnonymous(): bool + { + return is_null($this->getEmail()) + && is_null($this->getPhone()); + } + + /** + * Is Privileged User? + * + * @param array $roles + * + * @return bool + */ + public static function isPrivileged(array $roles): bool + { + if ( + in_array(self::ROLE_OWNER, $roles) || + in_array(self::ROLE_DEVELOPER, $roles) || + in_array(self::ROLE_ADMIN, $roles) + ) { + return true; + } + + return false; + } + + /** + * Is App User? + * + * @param array $roles + * + * @return bool + */ + public static function isApp(array $roles): bool + { + if (in_array(self::ROLE_APPS, $roles)) { + return true; + } + + return false; + } + + public function tokenVerify(int $type = null, string $secret, Proof $proofForToken): false|Document + { + $tokens = $this->getAttribute('tokens', []); + foreach ($tokens as $token) { + if ( + $token->isSet('secret') && + $token->isSet('expire') && + $token->isSet('type') && + ($type === null || $token->getAttribute('type') === $type) && + $proofForToken->verify($secret, $token->getAttribute('secret')) && + DateTime::formatTz($token->getAttribute('expire')) >= DateTime::formatTz(DateTime::now()) + ) { + return $token; + } + } + + return false; + } + + /** + * Verify session and check that its not expired. + * + * @param array $sessions + * @param string $secret + * + * @return bool|string + */ + public function sessionVerify(string $secret, Token $proofForToken) + { + $sessions = $this->getAttribute('sessions', []); + + foreach ($sessions as $session) { + if ( + $session->isSet('secret') && + $session->isSet('provider') && + $session->isSet('expire') && + $proofForToken->verify($secret, $session->getAttribute('secret')) && + DateTime::formatTz(DateTime::format(new \DateTime($session->getAttribute('expire')))) >= DateTime::formatTz(DateTime::now()) + ) { + return $session->getId(); + } + } + + return false; + + return false; + } +}