From e7c01ca32aed1820710c09cc3f404734f2e6e06b Mon Sep 17 00:00:00 2001 From: Hemachandar Date: Thu, 19 Feb 2026 13:55:49 +0530 Subject: [PATCH] Move team prefs & logs API to Modules --- app/controllers/api/teams.php | 187 ------------------ .../Modules/Teams/Http/Logs/XList.php | 137 +++++++++++++ .../Modules/Teams/Http/Preferences/Get.php | 71 +++++++ .../Modules/Teams/Http/Preferences/Update.php | 82 ++++++++ .../Platform/Modules/Teams/Services/Http.php | 10 + 5 files changed, 300 insertions(+), 187 deletions(-) create mode 100644 src/Appwrite/Platform/Modules/Teams/Http/Logs/XList.php create mode 100644 src/Appwrite/Platform/Modules/Teams/Http/Preferences/Get.php create mode 100644 src/Appwrite/Platform/Modules/Teams/Http/Preferences/Update.php diff --git a/app/controllers/api/teams.php b/app/controllers/api/teams.php index 970ffaefd3..75d426f228 100644 --- a/app/controllers/api/teams.php +++ b/app/controllers/api/teams.php @@ -22,7 +22,6 @@ use Appwrite\Utopia\Response; use libphonenumber\NumberParseException; use libphonenumber\PhoneNumberUtil; use MaxMind\Db\Reader; -use Utopia\Audit\Audit; use Utopia\Auth\Proofs\Password; use Utopia\Auth\Proofs\Token; use Utopia\Auth\Store; @@ -34,7 +33,6 @@ use Utopia\Database\Exception\Authorization as AuthorizationException; use Utopia\Database\Exception\Duplicate; use Utopia\Database\Exception\Order as OrderException; use Utopia\Database\Exception\Query as QueryException; -use Utopia\Database\Exception\Structure as StructureException; use Utopia\Database\Helpers\ID; use Utopia\Database\Helpers\Permission; use Utopia\Database\Helpers\Role; @@ -44,104 +42,15 @@ use Utopia\Database\Validator\Key; use Utopia\Database\Validator\Queries; use Utopia\Database\Validator\Query\Cursor; use Utopia\Database\Validator\Query\Limit; -use Utopia\Database\Validator\Query\Offset; use Utopia\Database\Validator\UID; use Utopia\Emails\Email; use Utopia\Http\Http; use Utopia\Locale\Locale; use Utopia\System\System; use Utopia\Validator\ArrayList; -use Utopia\Validator\Assoc; use Utopia\Validator\Boolean; use Utopia\Validator\Text; -Http::get('/v1/teams/:teamId/prefs') - ->desc('Get team preferences') - ->groups(['api', 'teams']) - ->label('scope', 'teams.read') - ->label('sdk', new Method( - namespace: 'teams', - group: 'teams', - name: 'getPrefs', - description: '/docs/references/teams/get-team-prefs.md', - auth: [AuthType::ADMIN, AuthType::SESSION, AuthType::JWT], - responses: [ - new SDKResponse( - code: Response::STATUS_CODE_OK, - model: Response::MODEL_PREFERENCES, - ) - ] - )) - ->param('teamId', '', new UID(), 'Team ID.') - ->inject('response') - ->inject('dbForProject') - ->action(function (string $teamId, Response $response, Database $dbForProject) { - - $team = $dbForProject->getDocument('teams', $teamId); - - if ($team->isEmpty()) { - throw new Exception(Exception::TEAM_NOT_FOUND); - } - - $prefs = $team->getAttribute('prefs', []); - - try { - $prefs = new Document($prefs); - } catch (StructureException $e) { - throw new Exception(Exception::DOCUMENT_INVALID_STRUCTURE, $e->getMessage()); - } - - $response->dynamic($prefs, Response::MODEL_PREFERENCES); - }); - -Http::put('/v1/teams/:teamId/prefs') - ->desc('Update preferences') - ->groups(['api', 'teams']) - ->label('event', 'teams.[teamId].update.prefs') - ->label('scope', 'teams.write') - ->label('audits.event', 'team.update') - ->label('audits.resource', 'team/{response.$id}') - ->label('audits.userId', '{response.$id}') - ->label('sdk', new Method( - namespace: 'teams', - group: 'teams', - name: 'updatePrefs', - description: '/docs/references/teams/update-team-prefs.md', - auth: [AuthType::ADMIN, AuthType::SESSION, AuthType::JWT], - responses: [ - new SDKResponse( - code: Response::STATUS_CODE_OK, - model: Response::MODEL_PREFERENCES, - ) - ] - )) - ->param('teamId', '', new UID(), 'Team ID.') - ->param('prefs', '', new Assoc(), 'Prefs key-value JSON object.') - ->inject('response') - ->inject('dbForProject') - ->inject('queueForEvents') - ->action(function (string $teamId, array $prefs, Response $response, Database $dbForProject, Event $queueForEvents) { - try { - $prefs = new Document($prefs); - } catch (StructureException $e) { - throw new Exception(Exception::DOCUMENT_INVALID_STRUCTURE, $e->getMessage()); - } - - $team = $dbForProject->getDocument('teams', $teamId); - - if ($team->isEmpty()) { - throw new Exception(Exception::TEAM_NOT_FOUND); - } - - $team = $dbForProject->updateDocument('teams', $team->getId(), new Document([ - 'prefs' => $prefs->getArrayCopy() - ])); - - $queueForEvents->setParam('teamId', $team->getId()); - - $response->dynamic($prefs, Response::MODEL_PREFERENCES); - }); - Http::post('/v1/teams/:teamId/memberships') ->desc('Create team membership') ->groups(['api', 'teams', 'auth']) @@ -1109,99 +1018,3 @@ Http::delete('/v1/teams/:teamId/memberships/:membershipId') $response->noContent(); }); - -Http::get('/v1/teams/:teamId/logs') - ->desc('List team logs') - ->groups(['api', 'teams']) - ->label('scope', 'teams.read') - ->label('sdk', new Method( - namespace: 'teams', - group: 'logs', - name: 'listLogs', - description: '/docs/references/teams/get-team-logs.md', - auth: [AuthType::ADMIN], - responses: [ - new SDKResponse( - code: Response::STATUS_CODE_OK, - model: Response::MODEL_LOG_LIST, - ) - ] - )) - ->param('teamId', '', new UID(), 'Team ID.') - ->param('queries', [], new Queries([new Limit(), new Offset()]), 'Array of query strings generated using the Query class provided by the SDK. [Learn more about queries](https://appwrite.io/docs/queries). Only supported methods are limit and offset', true) - ->param('total', true, new Boolean(true), 'When set to false, the total count returned will be 0 and will not be calculated.', true) - ->inject('response') - ->inject('dbForProject') - ->inject('locale') - ->inject('geodb') - ->inject('audit') - ->action(function (string $teamId, array $queries, bool $includeTotal, Response $response, Database $dbForProject, Locale $locale, Reader $geodb, Audit $audit) { - - $team = $dbForProject->getDocument('teams', $teamId); - - if ($team->isEmpty()) { - throw new Exception(Exception::TEAM_NOT_FOUND); - } - - try { - $queries = Query::parseQueries($queries); - } catch (QueryException $e) { - throw new Exception(Exception::GENERAL_QUERY_INVALID, $e->getMessage()); - } - - $grouped = Query::groupByType($queries); - $limit = $grouped['limit'] ?? 25; - $offset = $grouped['offset'] ?? 0; - - $resource = 'team/' . $team->getId(); - $logs = $audit->getLogsByResource($resource, offset: $offset, limit: $limit); - - $output = []; - - foreach ($logs as $i => &$log) { - $log['userAgent'] = (!empty($log['userAgent'])) ? $log['userAgent'] : 'UNKNOWN'; - - $detector = new Detector($log['userAgent']); - $detector->skipBotDetection(); // OPTIONAL: If called, bot detection will completely be skipped (bots will be detected as regular devices then) - - $os = $detector->getOS(); - $client = $detector->getClient(); - $device = $detector->getDevice(); - - $output[$i] = new Document([ - 'event' => $log['event'], - 'userId' => $log['data']['userId'], - 'userEmail' => $log['data']['userEmail'] ?? null, - 'userName' => $log['data']['userName'] ?? null, - 'mode' => $log['data']['mode'] ?? null, - 'ip' => $log['ip'], - 'time' => $log['time'], - 'osCode' => $os['osCode'], - 'osName' => $os['osName'], - 'osVersion' => $os['osVersion'], - 'clientType' => $client['clientType'], - 'clientCode' => $client['clientCode'], - 'clientName' => $client['clientName'], - 'clientVersion' => $client['clientVersion'], - 'clientEngine' => $client['clientEngine'], - 'clientEngineVersion' => $client['clientEngineVersion'], - 'deviceName' => $device['deviceName'], - 'deviceBrand' => $device['deviceBrand'], - 'deviceModel' => $device['deviceModel'] - ]); - - $record = $geodb->get($log['ip']); - - if ($record) { - $output[$i]['countryCode'] = $locale->getText('countries.' . strtolower($record['country']['iso_code']), false) ? \strtolower($record['country']['iso_code']) : '--'; - $output[$i]['countryName'] = $locale->getText('countries.' . strtolower($record['country']['iso_code']), $locale->getText('locale.country.unknown')); - } else { - $output[$i]['countryCode'] = '--'; - $output[$i]['countryName'] = $locale->getText('locale.country.unknown'); - } - } - $response->dynamic(new Document([ - 'total' => $includeTotal ? $audit->countLogsByResource($resource) : 0, - 'logs' => $output, - ]), Response::MODEL_LOG_LIST); - }); diff --git a/src/Appwrite/Platform/Modules/Teams/Http/Logs/XList.php b/src/Appwrite/Platform/Modules/Teams/Http/Logs/XList.php new file mode 100644 index 0000000000..80203f43d4 --- /dev/null +++ b/src/Appwrite/Platform/Modules/Teams/Http/Logs/XList.php @@ -0,0 +1,137 @@ +setHttpMethod(Action::HTTP_REQUEST_METHOD_GET) + ->setHttpPath('/v1/teams/:teamId/logs') + ->desc('List team logs') + ->groups(['api', 'teams']) + ->label('scope', 'teams.read') + ->label('sdk', new Method( + namespace: 'teams', + group: 'logs', + name: 'listLogs', + description: '/docs/references/teams/get-team-logs.md', + auth: [AuthType::ADMIN], + responses: [ + new SDKResponse( + code: Response::STATUS_CODE_OK, + model: Response::MODEL_LOG_LIST, + ) + ] + )) + ->param('teamId', '', new UID(), 'Team ID.') + ->param('queries', [], new Queries([new Limit(), new Offset()]), 'Array of query strings generated using the Query class provided by the SDK. [Learn more about queries](https://appwrite.io/docs/queries). Only supported methods are limit and offset', true) + ->param('total', true, new Boolean(true), 'When set to false, the total count returned will be 0 and will not be calculated.', true) + ->inject('response') + ->inject('dbForProject') + ->inject('locale') + ->inject('geodb') + ->inject('audit') + ->callback($this->action(...)); + } + + public function action(string $teamId, array $queries, bool $includeTotal, Response $response, Database $dbForProject, Locale $locale, Reader $geodb, Audit $audit) + { + $team = $dbForProject->getDocument('teams', $teamId); + + if ($team->isEmpty()) { + throw new Exception(Exception::TEAM_NOT_FOUND); + } + + try { + $queries = Query::parseQueries($queries); + } catch (QueryException $e) { + throw new Exception(Exception::GENERAL_QUERY_INVALID, $e->getMessage()); + } + + $grouped = Query::groupByType($queries); + $limit = $grouped['limit'] ?? 25; + $offset = $grouped['offset'] ?? 0; + + $resource = 'team/' . $team->getId(); + $logs = $audit->getLogsByResource($resource, offset: $offset, limit: $limit); + + $output = []; + + foreach ($logs as $i => &$log) { + $log['userAgent'] = (!empty($log['userAgent'])) ? $log['userAgent'] : 'UNKNOWN'; + + $detector = new Detector($log['userAgent']); + $detector->skipBotDetection(); // OPTIONAL: If called, bot detection will completely be skipped (bots will be detected as regular devices then) + + $os = $detector->getOS(); + $client = $detector->getClient(); + $device = $detector->getDevice(); + + $output[$i] = new Document([ + 'event' => $log['event'], + 'userId' => $log['data']['userId'], + 'userEmail' => $log['data']['userEmail'] ?? null, + 'userName' => $log['data']['userName'] ?? null, + 'mode' => $log['data']['mode'] ?? null, + 'ip' => $log['ip'], + 'time' => $log['time'], + 'osCode' => $os['osCode'], + 'osName' => $os['osName'], + 'osVersion' => $os['osVersion'], + 'clientType' => $client['clientType'], + 'clientCode' => $client['clientCode'], + 'clientName' => $client['clientName'], + 'clientVersion' => $client['clientVersion'], + 'clientEngine' => $client['clientEngine'], + 'clientEngineVersion' => $client['clientEngineVersion'], + 'deviceName' => $device['deviceName'], + 'deviceBrand' => $device['deviceBrand'], + 'deviceModel' => $device['deviceModel'] + ]); + + $record = $geodb->get($log['ip']); + + if ($record) { + $output[$i]['countryCode'] = $locale->getText('countries.' . strtolower($record['country']['iso_code']), false) ? \strtolower($record['country']['iso_code']) : '--'; + $output[$i]['countryName'] = $locale->getText('countries.' . strtolower($record['country']['iso_code']), $locale->getText('locale.country.unknown')); + } else { + $output[$i]['countryCode'] = '--'; + $output[$i]['countryName'] = $locale->getText('locale.country.unknown'); + } + } + $response->dynamic(new Document([ + 'total' => $includeTotal ? $audit->countLogsByResource($resource) : 0, + 'logs' => $output, + ]), Response::MODEL_LOG_LIST); + } +} diff --git a/src/Appwrite/Platform/Modules/Teams/Http/Preferences/Get.php b/src/Appwrite/Platform/Modules/Teams/Http/Preferences/Get.php new file mode 100644 index 0000000000..762e9b84a8 --- /dev/null +++ b/src/Appwrite/Platform/Modules/Teams/Http/Preferences/Get.php @@ -0,0 +1,71 @@ +setHttpMethod(Action::HTTP_REQUEST_METHOD_GET) + ->setHttpPath('/v1/teams/:teamId/prefs') + ->desc('Get team preferences') + ->groups(['api', 'teams']) + ->label('scope', 'teams.read') + ->label('sdk', new Method( + namespace: 'teams', + group: 'teams', + name: 'getPrefs', + description: '/docs/references/teams/get-team-prefs.md', + auth: [AuthType::ADMIN, AuthType::SESSION, AuthType::JWT], + responses: [ + new SDKResponse( + code: Response::STATUS_CODE_OK, + model: Response::MODEL_PREFERENCES, + ) + ] + )) + ->param('teamId', '', new UID(), 'Team ID.') + ->inject('response') + ->inject('dbForProject') + ->callback($this->action(...)); + } + + public function action(string $teamId, Response $response, Database $dbForProject) + { + $team = $dbForProject->getDocument('teams', $teamId); + + if ($team->isEmpty()) { + throw new Exception(Exception::TEAM_NOT_FOUND); + } + + $prefs = $team->getAttribute('prefs', []); + + try { + $prefs = new Document($prefs); + } catch (StructureException $e) { + throw new Exception(Exception::DOCUMENT_INVALID_STRUCTURE, $e->getMessage()); + } + + $response->dynamic($prefs, Response::MODEL_PREFERENCES); + } +} diff --git a/src/Appwrite/Platform/Modules/Teams/Http/Preferences/Update.php b/src/Appwrite/Platform/Modules/Teams/Http/Preferences/Update.php new file mode 100644 index 0000000000..b1269dd44b --- /dev/null +++ b/src/Appwrite/Platform/Modules/Teams/Http/Preferences/Update.php @@ -0,0 +1,82 @@ +setHttpMethod(Action::HTTP_REQUEST_METHOD_PUT) + ->setHttpPath('/v1/teams/:teamId/prefs') + ->groups(['api', 'teams']) + ->label('event', 'teams.[teamId].update.prefs') + ->label('scope', 'teams.write') + ->label('audits.event', 'team.update') + ->label('audits.resource', 'team/{response.$id}') + ->label('audits.userId', '{response.$id}') + ->label('sdk', new Method( + namespace: 'teams', + group: 'teams', + name: 'updatePrefs', + description: '/docs/references/teams/update-team-prefs.md', + auth: [AuthType::ADMIN, AuthType::SESSION, AuthType::JWT], + responses: [ + new SDKResponse( + code: Response::STATUS_CODE_OK, + model: Response::MODEL_PREFERENCES, + ) + ] + )) + ->param('teamId', '', new UID(), 'Team ID.') + ->param('prefs', '', new Assoc(), 'Prefs key-value JSON object.') + ->inject('response') + ->inject('dbForProject') + ->inject('queueForEvents') + ->callback($this->action(...)); + } + + public function action(string $teamId, array $prefs, Response $response, Database $dbForProject, Event $queueForEvents) + { + try { + $prefs = new Document($prefs); + } catch (StructureException $e) { + throw new Exception(Exception::DOCUMENT_INVALID_STRUCTURE, $e->getMessage()); + } + + $team = $dbForProject->getDocument('teams', $teamId); + + if ($team->isEmpty()) { + throw new Exception(Exception::TEAM_NOT_FOUND); + } + + $team = $dbForProject->updateDocument('teams', $team->getId(), new Document([ + 'prefs' => $prefs->getArrayCopy() + ])); + + $queueForEvents->setParam('teamId', $team->getId()); + + $response->dynamic($prefs, Response::MODEL_PREFERENCES); + } +} diff --git a/src/Appwrite/Platform/Modules/Teams/Services/Http.php b/src/Appwrite/Platform/Modules/Teams/Services/Http.php index 94ba57a2a5..5b29eee43f 100644 --- a/src/Appwrite/Platform/Modules/Teams/Services/Http.php +++ b/src/Appwrite/Platform/Modules/Teams/Services/Http.php @@ -2,6 +2,9 @@ namespace Appwrite\Platform\Modules\Teams\Services; +use Appwrite\Platform\Modules\Teams\Http\Logs\XList as ListLogs; +use Appwrite\Platform\Modules\Teams\Http\Preferences\Get as GetPreferences; +use Appwrite\Platform\Modules\Teams\Http\Preferences\Update as UpdatePreferences; use Appwrite\Platform\Modules\Teams\Http\Teams\Create as CreateTeam; use Appwrite\Platform\Modules\Teams\Http\Teams\Delete as DeleteTeam; use Appwrite\Platform\Modules\Teams\Http\Teams\Get as GetTeam; @@ -21,5 +24,12 @@ class Http extends Service $this->addAction(ListTeams::getName(), new ListTeams()); $this->addAction(DeleteTeam::getName(), new DeleteTeam()); $this->addAction(UpdateTeamName::getName(), new UpdateTeamName()); + + // Preferences + $this->addAction(GetPreferences::getName(), new GetPreferences()); + $this->addAction(UpdatePreferences::getName(), new UpdatePreferences()); + + // Logs + $this->addAction(ListLogs::getName(), new ListLogs()); } }