teams update

This commit is contained in:
shimon
2025-11-27 12:13:07 +02:00
parent b76f01b144
commit fb95a05599
3 changed files with 292 additions and 106 deletions
+46 -34
View File
@@ -1,6 +1,5 @@
<?php
use Appwrite\Auth\Auth;
use Appwrite\Auth\MFA\Type\TOTP;
use Appwrite\Auth\Validator\Phone;
use Appwrite\Detector\Detector;
@@ -18,6 +17,7 @@ use Appwrite\SDK\ContentType;
use Appwrite\SDK\Method;
use Appwrite\SDK\Response as SDKResponse;
use Appwrite\Template\Template;
use Appwrite\Utopia\Database\Documents\User;
use Appwrite\Utopia\Database\Validator\CustomId;
use Appwrite\Utopia\Database\Validator\Queries\Memberships;
use Appwrite\Utopia\Database\Validator\Queries\Teams;
@@ -28,6 +28,9 @@ use MaxMind\Db\Reader;
use Utopia\Abuse\Abuse;
use Utopia\App;
use Utopia\Audit\Audit;
use Utopia\Auth\Proofs\Password;
use Utopia\Auth\Proofs\Token;
use Utopia\Auth\Store;
use Utopia\Config\Config;
use Utopia\Database\Database;
use Utopia\Database\DateTime;
@@ -88,8 +91,8 @@ App::post('/v1/teams')
->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'),
Generated
+64 -72
View File
@@ -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",
@@ -0,0 +1,182 @@
<?php
namespace Appwrite\Utopia\Database\Documents;
use Utopia\Auth\Proof;
use Utopia\Auth\Proofs\Token;
use Utopia\Database\DateTime;
use Utopia\Database\Document;
use Utopia\Database\Helpers\Role;
use Utopia\Database\Validator\Authorization;
use Utopia\Database\Validator\Roles;
class User extends Document
{
public const ROLE_ANY = 'any';
public const ROLE_GUESTS = 'guests';
public const ROLE_USERS = 'users';
public const ROLE_ADMIN = 'admin';
public const ROLE_DEVELOPER = 'developer';
public const ROLE_OWNER = 'owner';
public const ROLE_APPS = 'apps';
public const ROLE_SYSTEM = 'system';
public function getEmail(): ?string
{
return $this->getAttribute('email');
}
public function getPhone(): ?string
{
return $this->getAttribute('phone');
}
/**
* Returns all roles for a user.
*
* @return array<string>
*/
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<string> $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<string> $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<Document> $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;
}
}