diff --git a/app/controllers/api/teams.php b/app/controllers/api/teams.php index 3b3039a0fd..40e1bd4c60 100644 --- a/app/controllers/api/teams.php +++ b/app/controllers/api/teams.php @@ -37,10 +37,12 @@ App::post('/v1/teams') ->inject('response') ->inject('user') ->inject('projectDB') - ->action(function ($name, $roles, $response, $user, $projectDB) { + ->inject('events') + ->action(function ($name, $roles, $response, $user, $projectDB, $events) { /** @var Appwrite\Utopia\Response $response */ /** @var Appwrite\Database\Document $user */ /** @var Appwrite\Database\Database $projectDB */ + /** @var Appwrite\Event\Event $events */ Authorization::disable(); @@ -90,6 +92,10 @@ App::post('/v1/teams') } } + if (!empty($user->getId())) { + $events->setParam('userId', $user->getId()); + } + $response ->setStatusCode(Response::STATUS_CODE_CREATED) ->dynamic($team, Response::MODEL_TEAM) diff --git a/app/controllers/shared/api.php b/app/controllers/shared/api.php index 5a1eeda8de..57ffe867de 100644 --- a/app/controllers/shared/api.php +++ b/app/controllers/shared/api.php @@ -201,6 +201,7 @@ App::shutdown(function ($utopia, $request, $response, $project, $events, $audits if ($project->getId() !== 'console') { $realtime ->setEvent($events->getParam('event')) + ->setUserId($events->getParam('userId')) ->setProject($project->getId()) ->setPayload($response->getPayload()) ->trigger(); diff --git a/src/Appwrite/Event/Realtime.php b/src/Appwrite/Event/Realtime.php index fb722be54d..28eb2f7c34 100644 --- a/src/Appwrite/Event/Realtime.php +++ b/src/Appwrite/Event/Realtime.php @@ -17,6 +17,11 @@ class Realtime */ protected $event = ''; + /** + * @var string + */ + protected $userId = ''; + /** * @var array */ @@ -27,6 +32,11 @@ class Realtime */ protected $permissions = []; + /** + * @var false + */ + protected $permissionsChanged = false; + /** * @var Document */ @@ -57,6 +67,16 @@ class Realtime return $this; } + /** + * @param string $userId + * return $this + */ + public function setUserId(string $userId): self + { + $this->userId = $userId; + return $this; + } + /** * @return string */ @@ -120,6 +140,25 @@ class Realtime $this->channels[] = 'account.' . $this->payload->getId(); $this->permissions = ['user:' . $this->payload->getId()]; + break; + case strpos($this->event, 'teams.memberships') === 0: + $this->channels[] = 'memberships'; + $this->channels[] = 'memberships.' . $this->payload->getId(); + $this->permissions = ['team:' . $this->payload->getAttribute('teamId')]; + + break; + case strpos($this->event, 'teams.create') === 0: + $this->permissionsChanged = true; + $this->channels[] = 'teams'; + $this->channels[] = 'teams.' . $this->payload->getId(); + $this->permissions = ['user:' . $this->userId]; + + break; + case strpos($this->event, 'teams.') === 0: + $this->channels[] = 'teams'; + $this->channels[] = 'teams.' . $this->payload->getId(); + $this->permissions = ['team:' . $this->payload->getId()]; + break; case strpos($this->event, 'database.collections.') === 0: $this->channels[] = 'collections'; @@ -166,6 +205,8 @@ class Realtime $redis->publish('realtime', json_encode([ 'project' => $this->project, 'permissions' => $this->permissions, + 'permissionsChanged' => $this->permissionsChanged, + 'userId' => $this->userId, 'data' => [ 'event' => $this->event, 'channels' => $this->channels, diff --git a/src/Appwrite/Realtime/Parser.php b/src/Appwrite/Realtime/Parser.php index f99e7bfbe9..8a5fd1bfcf 100644 --- a/src/Appwrite/Realtime/Parser.php +++ b/src/Appwrite/Realtime/Parser.php @@ -163,6 +163,7 @@ class Parser $connections[$connection] = [ 'projectId' => $projectId, 'roles' => $roles, + 'channels' => $channels ]; } diff --git a/src/Appwrite/Realtime/Server.php b/src/Appwrite/Realtime/Server.php index 0eed3f24b4..e9e79913ec 100644 --- a/src/Appwrite/Realtime/Server.php +++ b/src/Appwrite/Realtime/Server.php @@ -2,6 +2,9 @@ namespace Appwrite\Realtime; +use Appwrite\Database\Database; +use Appwrite\Database\Adapter\MySQL as MySQLAdapter; +use Appwrite\Database\Adapter\Redis as RedisAdapter; use Appwrite\Event\Event; use Appwrite\Network\Validator\Origin; use Appwrite\Utopia\Response; @@ -17,6 +20,7 @@ use Utopia\Abuse\Abuse; use Utopia\Abuse\Adapters\TimeLimit; use Utopia\App; use Utopia\CLI\Console; +use Utopia\Config\Config; use Utopia\Exception as UtopiaException; use Utopia\Registry\Registry; use Utopia\Swoole\Request as SwooleRequest; @@ -176,7 +180,7 @@ class Server return $db; }); - $this->register->set('cache', function () use (&$redis) { // Register cache connection + $this->register->set('cache', function () use (&$redis) { return $redis; }); @@ -318,20 +322,12 @@ class Server */ public function onRedisPublish(string $payload, SwooleServer &$server, int $workerId) { - /** - * Supported Resources: - * - Collection - * - Document - * - File - * - Account - * - Session - * - Team? (not implemented yet) - * - Membership? (not implemented yet) - * - Function - * - Execution - */ $event = json_decode($payload, true); + if ($event['permissionsChanged'] && $event['userId']) { + $this->addPermission($event); + } + $receivers = Parser::identifyReceivers($event, $this->subscriptions); // Temporarily print debug logs by default for Alpha testing. @@ -390,4 +386,43 @@ class Server } } } + + private function addPermission(array $event) + { + $project = $event['project']; + $userId = $event['userId']; + + if (array_key_exists($project, $this->subscriptions) && array_key_exists('user:'.$userId, $this->subscriptions[$project])) { + $connection = array_key_first(reset($this->subscriptions[$project]['user:'.$userId])); + } else { + return; + } + + /** + * This is redundant soon and will be gone with merging the usage branch. + */ + $db = $this->register->get('dbPool')->get(); + $redis = $this->register->get('redisPool')->get(); + + $this->register->set('db', function () use (&$db) { + return $db; + }); + + $this->register->set('cache', function () use (&$redis) { + return $redis; + }); + + $projectDB = new Database(); + $projectDB->setAdapter(new RedisAdapter(new MySQLAdapter($this->register), $this->register)); + $projectDB->setNamespace('app_'.$project); + $projectDB->setMocks(Config::getParam('collections', [])); + + $user = $projectDB->getDocument($userId); + + Parser::setUser($user); + + $roles = Parser::getRoles(); + + Parser::subscribe($project, $connection, $roles, $this->subscriptions, $this->connections, $this->connections[$connection]['channels']); + } } diff --git a/tests/e2e/Services/Realtime/RealtimeBase.php b/tests/e2e/Services/Realtime/RealtimeBase.php index 164bb025c8..305345c11f 100644 --- a/tests/e2e/Services/Realtime/RealtimeBase.php +++ b/tests/e2e/Services/Realtime/RealtimeBase.php @@ -686,4 +686,69 @@ trait RealtimeBase $client->close(); } + + public function testChannelTeams() + { + $user = $this->getUser(); + $session = $user['session'] ?? ''; + $projectId = $this->getProject()['$id']; + + $client = $this->getWebsocket(['teams'], [ + 'origin' => 'http://localhost', + 'cookie' => 'a_session_'.$projectId.'=' . $session + ]); + + $response = json_decode($client->receive(), true); + + $this->assertCount(1, $response); + $this->assertArrayHasKey('teams', $response); + + /** + * Test Team Create + */ + $team = $this->client->call(Client::METHOD_POST, '/teams', array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $projectId, + ], $this->getHeaders()), [ + 'name' => 'Arsenal' + ]); + + $teamId = $team['body']['$id'] ?? ''; + + $this->assertEquals(201, $team['headers']['status-code']); + $this->assertNotEmpty($team['body']['$id']); + + $response = json_decode($client->receive(), true); + + $this->assertArrayHasKey('timestamp', $response); + $this->assertCount(2, $response['channels']); + $this->assertContains('teams', $response['channels']); + $this->assertContains('teams.' . $teamId, $response['channels']); + $this->assertEquals('teams.create', $response['event']); + $this->assertNotEmpty($response['payload']); + + /** + * Test Team Update + */ + $team = $this->client->call(Client::METHOD_PUT, '/teams/'.$teamId, array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $projectId, + ], $this->getHeaders()), [ + 'name' => 'Manchester' + ]); + + $this->assertEquals($team['headers']['status-code'], 200); + $this->assertNotEmpty($team['body']['$id']); + + $response = json_decode($client->receive(), true); + + $this->assertArrayHasKey('timestamp', $response); + $this->assertCount(2, $response['channels']); + $this->assertContains('teams', $response['channels']); + $this->assertContains('teams.' . $teamId, $response['channels']); + $this->assertEquals('teams.update', $response['event']); + $this->assertNotEmpty($response['payload']); + + $client->close(); + } }