From e81fb88736dcdbaba3f3f9fd0abd9643e218486a Mon Sep 17 00:00:00 2001 From: Eldad Fux Date: Fri, 30 Oct 2020 21:53:27 +0200 Subject: [PATCH 1/4] First commit --- app/controllers/api/account.php | 391 +- app/controllers/api/database.php | 252 +- app/controllers/api/functions.php | 246 +- app/controllers/api/locale.php | 79 +- app/controllers/api/projects.php | 134 +- app/controllers/api/storage.php | 55 +- app/controllers/api/teams.php | 105 +- app/controllers/api/users.php | 268 +- app/controllers/general.php | 17 +- app/views/console/account/index.phtml | 22 +- app/views/console/functions/function.phtml | 134 +- app/views/console/functions/index.phtml | 3 +- app/views/console/home/index.phtml | 120 +- app/views/console/keys/index.phtml | 60 +- app/views/console/settings/index.phtml | 6 +- app/views/console/tasks/index.phtml | 6 +- app/views/console/users/user.phtml | 26 +- app/views/console/webhooks/index.phtml | 60 +- app/views/home/auth/oauth2.phtml | 2 +- gulpfile.js | 1 + public/scripts/dependencies/chart.js | 34616 +++++++--------- public/scripts/routes.js | 8 + public/scripts/views/forms/chart.js | 149 +- public/scripts/views/forms/select-all.js | 51 + public/styles/comps/alerts.less | 76 +- public/styles/comps/modal.less | 4 +- public/styles/forms.less | 22 + public/styles/functions.less | 4 + public/styles/scopes/console.less | 28 +- tests/e2e/Scopes/Scope.php | 11 + tests/e2e/Services/Account/AccountBase.php | 131 +- .../Functions/FunctionsCustomClientTest.php | 6 +- .../Functions/FunctionsCustomServerTest.php | 48 +- tests/e2e/Services/Locale/LocaleBase.php | 94 +- .../Projects/ProjectsConsoleClientTest.php | 14 +- tests/e2e/Services/Storage/StorageBase.php | 7 - tests/e2e/Services/Users/UsersBase.php | 8 +- tests/e2e/Services/Workers/WebhooksTest.php | 152 + 38 files changed, 17554 insertions(+), 19862 deletions(-) create mode 100644 public/scripts/views/forms/select-all.js create mode 100644 tests/e2e/Services/Workers/WebhooksTest.php diff --git a/app/controllers/api/account.php b/app/controllers/api/account.php index 229b70efd..e7f12088d 100644 --- a/app/controllers/api/account.php +++ b/app/controllers/api/account.php @@ -28,23 +28,10 @@ use Utopia\Validator\ArrayList; $oauthDefaultSuccess = App::getEnv('_APP_HOME').'/auth/oauth2/success'; $oauthDefaultFailure = App::getEnv('_APP_HOME').'/auth/oauth2/failure'; -$oauth2Keys = []; - -App::init(function() use (&$oauth2Keys) { - foreach (Config::getParam('providers') as $key => $provider) { - if (!$provider['enabled']) { - continue; - } - - $oauth2Keys[] = 'oauth2'.\ucfirst($key); - $oauth2Keys[] = 'oauth2'.\ucfirst($key).'AccessToken'; - } -}, [], 'account'); - App::post('/v1/account') ->desc('Create Account') ->groups(['api', 'account']) - ->label('webhook', 'account.create') + ->label('event', 'account.create') ->label('scope', 'public') ->label('sdk.platform', [APP_PLATFORM_CLIENT]) ->label('sdk.namespace', 'account') @@ -54,12 +41,11 @@ App::post('/v1/account') ->param('email', '', new Email(), 'User email.') ->param('password', '', new Password(), 'User password. Must be between 6 to 32 chars.') ->param('name', '', new Text(128), 'User name. Max length: 128 chars.', true) - ->action(function ($email, $password, $name, $request, $response, $project, $projectDB, $webhooks, $audits) use ($oauth2Keys) { + ->action(function ($email, $password, $name, $request, $response, $project, $projectDB, $audits) { /** @var Utopia\Swoole\Request $request */ /** @var Appwrite\Utopia\Response $response */ /** @var Appwrite\Database\Document $project */ /** @var Appwrite\Database\Database $projectDB */ - /** @var Appwrite\Event\Event $webhooks */ /** @var Appwrite\Event\Event $audits */ if ('console' === $project->getId()) { @@ -120,36 +106,24 @@ App::post('/v1/account') throw new Exception('Failed saving user to DB', 500); } - $webhooks - ->setParam('payload', [ - 'name' => $name, - 'email' => $email, - ]) - ; - $audits ->setParam('userId', $user->getId()) ->setParam('event', 'account.create') ->setParam('resource', 'users/'.$user->getId()) ; - $response - ->setStatusCode(Response::STATUS_CODE_CREATED) - ->json(\array_merge($user->getArrayCopy(\array_merge( - [ - '$id', - 'email', - 'registration', - 'name', - ], - $oauth2Keys - )), ['roles' => Authorization::getRoles()])); - }, ['request', 'response', 'project', 'projectDB', 'webhooks', 'audits']); + $user + ->setAttribute('roles', Authorization::getRoles()) + ; + + $response->setStatusCode(Response::STATUS_CODE_CREATED); + $response->dynamic($user, Response::MODEL_USER); + }, ['request', 'response', 'project', 'projectDB', 'audits']); App::post('/v1/account/sessions') ->desc('Create Account Session') ->groups(['api', 'account']) - ->label('webhook', 'account.sessions.create') + ->label('event', 'account.sessions.create') ->label('scope', 'public') ->label('sdk.platform', [APP_PLATFORM_CLIENT]) ->label('sdk.namespace', 'account') @@ -159,11 +133,12 @@ App::post('/v1/account/sessions') ->label('abuse-key', 'url:{url},email:{param-email}') ->param('email', '', new Email(), 'User email.') ->param('password', '', new Password(), 'User password. Must be between 6 to 32 chars.') - ->action(function ($email, $password, $request, $response, $projectDB, $webhooks, $audits) { + ->action(function ($email, $password, $request, $response, $projectDB, $locale, $geodb, $audits) { /** @var Utopia\Swoole\Request $request */ /** @var Appwrite\Utopia\Response $response */ /** @var Appwrite\Database\Database $projectDB */ - /** @var Appwrite\Event\Event $webhooks */ + /** @var Utopia\Locale\Locale $locale */ + /** @var MaxMind\Db\Reader $geodb */ /** @var Appwrite\Event\Event $audits */ $protocol = $request->getProtocol(); @@ -185,6 +160,23 @@ App::post('/v1/account/sessions') throw new Exception('Invalid credentials', 401); // Wrong password or username } + $dd = new DeviceDetector($request->getUserAgent('UNKNOWN')); + + $dd->parse(); + + $os = $dd->getOs(); + $osCode = (isset($os['short_name'])) ? $os['short_name'] : ''; + $osName = (isset($os['name'])) ? $os['name'] : ''; + $osVersion = (isset($os['version'])) ? $os['version'] : ''; + + $client = $dd->getClient(); + $clientType = (isset($client['type'])) ? $client['type'] : ''; + $clientCode = (isset($client['short_name'])) ? $client['short_name'] : ''; + $clientName = (isset($client['name'])) ? $client['name'] : ''; + $clientVersion = (isset($client['version'])) ? $client['version'] : ''; + $clientEngine = (isset($client['engine'])) ? $client['engine'] : ''; + $clientEngineVersion = (isset($client['engine_version'])) ? $client['engine_version'] : ''; + $expiry = \time() + Auth::TOKEN_EXPIRATION_LOGIN_LONG; $secret = Auth::tokenGenerator(); $session = new Document([ @@ -195,8 +187,33 @@ App::post('/v1/account/sessions') 'expire' => $expiry, 'userAgent' => $request->getUserAgent('UNKNOWN'), 'ip' => $request->getIP(), + + 'osCode' => $osCode, + 'osName' => $osName, + 'osVersion' => $osVersion, + 'clientType' => $clientType, + 'clientCode' => $clientCode, + 'clientName' => $clientName, + 'clientVersion' => $clientVersion, + 'clientEngine' => $clientEngine, + 'clientEngineVersion' => $clientEngineVersion, + 'deviceName' => $dd->getDeviceName(), + 'deviceBrand' => $dd->getBrandName(), + 'deviceModel' => $dd->getModel(), ]); + $record = $geodb->get($request->getIP()); + + if($record) { + $session + ->setAttribute('countryCode', \strtolower($record['country']['iso_code'])) + ; + } else { + $session + ->setAttribute('countryCode', '--') + ; + } + Authorization::setRole('user:'.$profile->getId()); $session = $projectDB->createDocument($session->getArrayCopy()); @@ -212,14 +229,7 @@ App::post('/v1/account/sessions') if (false === $profile) { throw new Exception('Failed saving user to DB', 500); } - - $webhooks - ->setParam('payload', [ - 'name' => $profile->getAttribute('name', ''), - 'email' => $profile->getAttribute('email', ''), - ]) - ; - + $audits ->setParam('userId', $profile->getId()) ->setParam('event', 'account.sessions.create') @@ -237,10 +247,14 @@ App::post('/v1/account/sessions') ->addCookie(Auth::$cookieName, Auth::encodeSession($profile->getId(), $secret), $expiry, '/', Config::getParam('cookieDomain'), ('https' == $protocol), true, Config::getParam('cookieSamesite')) ->setStatusCode(Response::STATUS_CODE_CREATED) ; + + $session + ->setAttribute('current', true) + ->setAttribute('countryName', (isset($countries[$session->getAttribute('countryCode')])) ? $countries[$session->getAttribute('countryCode')] : $locale->getText('locale.country.unknown')) + ; $response->dynamic($session, Response::MODEL_SESSION); - ; - }, ['request', 'response', 'projectDB', 'webhooks', 'audits']); + }, ['request', 'response', 'projectDB', 'locale', 'geodb', 'audits']); App::get('/v1/account/sessions/oauth2/:provider') ->desc('Create Account Session with OAuth2') @@ -348,7 +362,7 @@ App::get('/v1/account/sessions/oauth2/:provider/redirect') ->desc('OAuth2 Redirect') ->groups(['api', 'account']) ->label('error', __DIR__.'/../../views/general/error.phtml') - ->label('webhook', 'account.sessions.create') + ->label('event', 'account.sessions.create') ->label('scope', 'public') ->label('abuse-limit', 50) ->label('abuse-key', 'ip:{ip}') @@ -356,12 +370,13 @@ App::get('/v1/account/sessions/oauth2/:provider/redirect') ->param('provider', '', new WhiteList(\array_keys(Config::getParam('providers')), true), 'OAuth2 provider.') ->param('code', '', new Text(1024), 'OAuth2 code.') ->param('state', '', new Text(2048), 'OAuth2 state params.', true) - ->action(function ($provider, $code, $state, $request, $response, $project, $user, $projectDB, $audits) use ($oauthDefaultSuccess) { + ->action(function ($provider, $code, $state, $request, $response, $project, $user, $projectDB, $geodb, $audits) use ($oauthDefaultSuccess) { /** @var Utopia\Swoole\Request $request */ /** @var Appwrite\Utopia\Response $response */ /** @var Appwrite\Database\Document $project */ /** @var Appwrite\Database\Document $user */ /** @var Appwrite\Database\Database $projectDB */ + /** @var MaxMind\Db\Reader $geodb */ /** @var Appwrite\Event\Event $audits */ $protocol = $request->getProtocol(); @@ -482,6 +497,24 @@ App::get('/v1/account/sessions/oauth2/:provider/redirect') // Create session token, verify user account and update OAuth2 ID and Access Token + + $dd = new DeviceDetector($request->getUserAgent('UNKNOWN')); + + $dd->parse(); + + $os = $dd->getOs(); + $osCode = (isset($os['short_name'])) ? $os['short_name'] : ''; + $osName = (isset($os['name'])) ? $os['name'] : ''; + $osVersion = (isset($os['version'])) ? $os['version'] : ''; + + $client = $dd->getClient(); + $clientType = (isset($client['type'])) ? $client['type'] : ''; + $clientCode = (isset($client['short_name'])) ? $client['short_name'] : ''; + $clientName = (isset($client['name'])) ? $client['name'] : ''; + $clientVersion = (isset($client['version'])) ? $client['version'] : ''; + $clientEngine = (isset($client['engine'])) ? $client['engine'] : ''; + $clientEngineVersion = (isset($client['engine_version'])) ? $client['engine_version'] : ''; + $secret = Auth::tokenGenerator(); $expiry = \time() + Auth::TOKEN_EXPIRATION_LOGIN_LONG; $session = new Document([ @@ -492,8 +525,33 @@ App::get('/v1/account/sessions/oauth2/:provider/redirect') 'expire' => $expiry, 'userAgent' => $request->getUserAgent('UNKNOWN'), 'ip' => $request->getIP(), + + 'osCode' => $osCode, + 'osName' => $osName, + 'osVersion' => $osVersion, + 'clientType' => $clientType, + 'clientCode' => $clientCode, + 'clientName' => $clientName, + 'clientVersion' => $clientVersion, + 'clientEngine' => $clientEngine, + 'clientEngineVersion' => $clientEngineVersion, + 'deviceName' => $dd->getDeviceName(), + 'deviceBrand' => $dd->getBrandName(), + 'deviceModel' => $dd->getModel(), ]); + $record = $geodb->get($request->getIP()); + + if($record) { + $session + ->setAttribute('countryCode', \strtolower($record['country']['iso_code'])) + ; + } else { + $session + ->setAttribute('countryCode', '--') + ; + } + $user ->setAttribute('oauth2'.\ucfirst($provider), $oauth2ID) ->setAttribute('oauth2'.\ucfirst($provider).'AccessToken', $accessToken) @@ -523,7 +581,7 @@ App::get('/v1/account/sessions/oauth2/:provider/redirect') } // Add keys for non-web platforms - TODO - add verification phase to aviod session sniffing - if (parse_url($state['success'], PHP_URL_PATH) === $oauthDefaultSuccess) { + if (parse_url($state['success'], PHP_URL_PATH) === parse_url($oauthDefaultSuccess, PHP_URL_PATH)) { $state['success'] = URLParser::parse($state['success']); $query = URLParser::parseQuery($state['success']['query']); $query['project'] = $project->getId(); @@ -541,7 +599,7 @@ App::get('/v1/account/sessions/oauth2/:provider/redirect') ->addCookie(Auth::$cookieName, Auth::encodeSession($user->getId(), $secret), $expiry, '/', Config::getParam('cookieDomain'), ('https' == $protocol), true, Config::getParam('cookieSamesite')) ->redirect($state['success']) ; - }, ['request', 'response', 'project', 'user', 'projectDB', 'audits']); + }, ['request', 'response', 'project', 'user', 'projectDB', 'geodb', 'audits']); App::get('/v1/account') ->desc('Get Account') @@ -552,20 +610,15 @@ App::get('/v1/account') ->label('sdk.method', 'get') ->label('sdk.description', '/docs/references/account/get.md') ->label('sdk.response', ['200' => 'user']) - ->action(function ($response, $user) use ($oauth2Keys) { + ->action(function ($response, $user) { /** @var Appwrite\Utopia\Response $response */ /** @var Appwrite\Database\Document $user */ - $response->json(\array_merge($user->getArrayCopy(\array_merge( - [ - '$id', - 'email', - 'emailVerification', - 'registration', - 'name', - ], - $oauth2Keys - )), ['roles' => Authorization::getRoles()])); + $user + ->setAttribute('roles', Authorization::getRoles()) + ; + + $response->dynamic($user, Response::MODEL_USER); }, ['response', 'user']); App::get('/v1/account/prefs') @@ -580,14 +633,7 @@ App::get('/v1/account/prefs') /** @var Appwrite\Utopia\Response $response */ /** @var Appwrite\Database\Document $user */ - $prefs = $user->getAttribute('prefs', '{}'); - - try { - $prefs = \json_decode($prefs, true); - $prefs = ($prefs) ? $prefs : []; - } catch (\Exception $error) { - throw new Exception('Failed to parse prefs', 500); - } + $prefs = $user->getAttribute('prefs', new \stdClass); $response->json($prefs); }, ['response', 'user']); @@ -600,64 +646,34 @@ App::get('/v1/account/sessions') ->label('sdk.namespace', 'account') ->label('sdk.method', 'getSessions') ->label('sdk.description', '/docs/references/account/get-sessions.md') - ->action(function ($response, $user, $locale, $geodb) { + ->action(function ($response, $user, $locale) { /** @var Appwrite\Utopia\Response $response */ /** @var Appwrite\Database\Document $user */ /** @var Utopia\Locale\Locale $locale */ - /** @var MaxMind\Db\Reader $geodb */ $tokens = $user->getAttribute('tokens', []); $sessions = []; - $current = Auth::tokenVerify($tokens, Auth::TOKEN_TYPE_LOGIN, Auth::$secret); - $index = 0; $countries = $locale->getText('countries'); + $current = Auth::tokenVerify($tokens, Auth::TOKEN_TYPE_LOGIN, Auth::$secret); foreach ($tokens as $token) { /* @var $token Document */ if (Auth::TOKEN_TYPE_LOGIN != $token->getAttribute('type')) { continue; } - $userAgent = (!empty($token->getAttribute('userAgent'))) ? $token->getAttribute('userAgent') : 'UNKNOWN'; + $token->setAttribute('countryName', (isset($countries[$token->getAttribute('contryCode')])) + ? $countries[$token->getAttribute('contryCode')] + : $locale->getText('locale.country.unknown')); + $token->setAttribute('current', ($current == $token->getId()) ? true : false); - $dd = new DeviceDetector($userAgent); - - // OPTIONAL: If called, bot detection will completely be skipped (bots will be detected as regular devices then) - // $dd->skipBotDetection(); - - $dd->parse(); - - $sessions[$index] = [ - '$id' => $token->getId(), - 'OS' => $dd->getOs(), - 'client' => $dd->getClient(), - 'device' => $dd->getDevice(), - 'brand' => $dd->getBrand(), - 'model' => $dd->getModel(), - 'ip' => $token->getAttribute('ip', ''), - 'geo' => [], - 'current' => ($current == $token->getId()) ? true : false, - ]; - - try { - $record = $geodb->get($token->getAttribute('ip', '')); - - if ($record) { - $sessions[$index]['geo']['isoCode'] = \strtolower($record['country']['iso_code']); - $sessions[$index]['geo']['country'] = (isset($countries[$record['country']['iso_code']])) ? $countries[$record['country']['iso_code']] : $locale->getText('locale.country.unknown'); - } else { - $sessions[$index]['geo']['isoCode'] = '--'; - $sessions[$index]['geo']['country'] = $locale->getText('locale.country.unknown'); - } - } catch (\Exception $e) { - $sessions[$index]['geo']['isoCode'] = '--'; - $sessions[$index]['geo']['country'] = $locale->getText('locale.country.unknown'); - } - - ++$index; + $sessions[] = $token; } - $response->json($sessions); - }, ['response', 'user', 'locale', 'geodb']); + $response->dynamic(new Document([ + 'sum' => count($sessions), + 'sessions' => $sessions + ]), Response::MODEL_SESSION_LIST); + }, ['response', 'user', 'locale']); App::get('/v1/account/logs') ->desc('Get Account Logs') @@ -709,48 +725,64 @@ App::get('/v1/account/logs') $dd->parse(); - $output[$i] = [ + $os = $dd->getOs(); + $osCode = (isset($os['short_name'])) ? $os['short_name'] : ''; + $osName = (isset($os['name'])) ? $os['name'] : ''; + $osVersion = (isset($os['version'])) ? $os['version'] : ''; + + $client = $dd->getClient(); + $clientType = (isset($client['type'])) ? $client['type'] : ''; + $clientCode = (isset($client['short_name'])) ? $client['short_name'] : ''; + $clientName = (isset($client['name'])) ? $client['name'] : ''; + $clientVersion = (isset($client['version'])) ? $client['version'] : ''; + $clientEngine = (isset($client['engine'])) ? $client['engine'] : ''; + $clientEngineVersion = (isset($client['engine_version'])) ? $client['engine_version'] : ''; + + $output[$i] = new Document([ 'event' => $log['event'], 'ip' => $log['ip'], 'time' => \strtotime($log['time']), - 'OS' => $dd->getOs(), - 'client' => $dd->getClient(), - 'device' => $dd->getDevice(), - 'brand' => $dd->getBrand(), - 'model' => $dd->getModel(), - 'geo' => [], - ]; - try { - $record = $geodb->get($log['ip']); + 'osCode' => $osCode, + 'osName' => $osName, + 'osVersion' => $osVersion, + 'clientType' => $clientType, + 'clientCode' => $clientCode, + 'clientName' => $clientName, + 'clientVersion' => $clientVersion, + 'clientEngine' => $clientEngine, + 'clientEngineVersion' => $clientEngineVersion, + 'deviceName' => $dd->getDeviceName(), + 'deviceBrand' => $dd->getBrandName(), + 'deviceModel' => $dd->getModel(), + ]); - if ($record) { - $output[$i]['geo']['isoCode'] = \strtolower($record['country']['iso_code']); - $output[$i]['geo']['country'] = (isset($countries[$record['country']['iso_code']])) ? $countries[$record['country']['iso_code']] : $locale->getText('locale.country.unknown'); - } else { - $output[$i]['geo']['isoCode'] = '--'; - $output[$i]['geo']['country'] = $locale->getText('locale.country.unknown'); - } - } catch (\Exception $e) { - $output[$i]['geo']['isoCode'] = '--'; - $output[$i]['geo']['country'] = $locale->getText('locale.country.unknown'); + $record = $geodb->get($log['ip']); + + if ($record) { + $output[$i]['countryCode'] = (isset($countries[$record['country']['iso_code']])) ? \strtolower($record['country']['iso_code']) : '--'; + $output[$i]['countryName'] = (isset($countries[$record['country']['iso_code']])) ? $countries[$record['country']['iso_code']] : $locale->getText('locale.country.unknown'); + } else { + $output[$i]['countryCode'] = '--'; + $output[$i]['countryName'] = $locale->getText('locale.country.unknown'); } + } - $response->json($output); + $response->dynamic(new Document(['logs' => $output]), Response::MODEL_LOG_LIST); }, ['response', 'register', 'project', 'user', 'locale', 'geodb']); App::patch('/v1/account/name') ->desc('Update Account Name') ->groups(['api', 'account']) - ->label('webhook', 'account.update.name') + ->label('event', 'account.update.name') ->label('scope', 'account') ->label('sdk.platform', [APP_PLATFORM_CLIENT]) ->label('sdk.namespace', 'account') ->label('sdk.method', 'updateName') ->label('sdk.description', '/docs/references/account/update-name.md') ->param('name', '', new Text(128), 'User name. Max length: 128 chars.') - ->action(function ($name, $response, $user, $projectDB, $audits) use ($oauth2Keys) { + ->action(function ($name, $response, $user, $projectDB, $audits) { /** @var Appwrite\Utopia\Response $response */ /** @var Appwrite\Database\Document $user */ /** @var Appwrite\Database\Database $projectDB */ @@ -764,27 +796,21 @@ App::patch('/v1/account/name') throw new Exception('Failed saving user to DB', 500); } + $user->setAttribute('roles', Authorization::getRoles()); + $audits ->setParam('userId', $user->getId()) ->setParam('event', 'account.update.name') ->setParam('resource', 'users/'.$user->getId()) ; - $response->json(\array_merge($user->getArrayCopy(\array_merge( - [ - '$id', - 'email', - 'registration', - 'name', - ], - $oauth2Keys - )), ['roles' => Authorization::getRoles()])); + $response->dynamic($user, Response::MODEL_USER); }, ['response', 'user', 'projectDB', 'audits']); App::patch('/v1/account/password') ->desc('Update Account Password') ->groups(['api', 'account']) - ->label('webhook', 'account.update.password') + ->label('event', 'account.update.password') ->label('scope', 'account') ->label('sdk.platform', [APP_PLATFORM_CLIENT]) ->label('sdk.namespace', 'account') @@ -792,7 +818,7 @@ App::patch('/v1/account/password') ->label('sdk.description', '/docs/references/account/update-password.md') ->param('password', '', new Password(), 'New user password. Must be between 6 to 32 chars.') ->param('oldPassword', '', new Password(), 'Old user password. Must be between 6 to 32 chars.') - ->action(function ($password, $oldPassword, $response, $user, $projectDB, $audits) use ($oauth2Keys) { + ->action(function ($password, $oldPassword, $response, $user, $projectDB, $audits) { /** @var Appwrite\Utopia\Response $response */ /** @var Appwrite\Database\Document $user */ /** @var Appwrite\Database\Database $projectDB */ @@ -810,27 +836,21 @@ App::patch('/v1/account/password') throw new Exception('Failed saving user to DB', 500); } + $user->setAttribute('roles', Authorization::getRoles()); + $audits ->setParam('userId', $user->getId()) ->setParam('event', 'account.update.password') ->setParam('resource', 'users/'.$user->getId()) ; - $response->json(\array_merge($user->getArrayCopy(\array_merge( - [ - '$id', - 'email', - 'registration', - 'name', - ], - $oauth2Keys - )), ['roles' => Authorization::getRoles()])); + $response->dynamic($user, Response::MODEL_USER); }, ['response', 'user', 'projectDB', 'audits']); App::patch('/v1/account/email') ->desc('Update Account Email') ->groups(['api', 'account']) - ->label('webhook', 'account.update.email') + ->label('event', 'account.update.email') ->label('scope', 'account') ->label('sdk.platform', [APP_PLATFORM_CLIENT]) ->label('sdk.namespace', 'account') @@ -838,7 +858,7 @@ App::patch('/v1/account/email') ->label('sdk.description', '/docs/references/account/update-email.md') ->param('email', '', new Email(), 'User email.') ->param('password', '', new Password(), 'User password. Must be between 6 to 32 chars.') - ->action(function ($email, $password, $response, $user, $projectDB, $audits) use ($oauth2Keys) { + ->action(function ($email, $password, $response, $user, $projectDB, $audits) { /** @var Appwrite\Utopia\Response $response */ /** @var Appwrite\Database\Document $user */ /** @var Appwrite\Database\Database $projectDB */ @@ -871,44 +891,35 @@ App::patch('/v1/account/email') throw new Exception('Failed saving user to DB', 500); } + $user->setAttribute('roles', Authorization::getRoles()); + $audits ->setParam('userId', $user->getId()) ->setParam('event', 'account.update.email') ->setParam('resource', 'users/'.$user->getId()) ; - $response->json(\array_merge($user->getArrayCopy(\array_merge( - [ - '$id', - 'email', - 'registration', - 'name', - ], - $oauth2Keys - )), ['roles' => Authorization::getRoles()])); + $response->dynamic($user, Response::MODEL_USER); }, ['response', 'user', 'projectDB', 'audits']); App::patch('/v1/account/prefs') ->desc('Update Account Preferences') ->groups(['api', 'account']) - ->label('webhook', 'account.update.prefs') + ->label('event', 'account.update.prefs') ->label('scope', 'account') ->label('sdk.platform', [APP_PLATFORM_CLIENT]) ->label('sdk.namespace', 'account') ->label('sdk.method', 'updatePrefs') - ->param('prefs', '', new Assoc(), 'Prefs key-value JSON object.') ->label('sdk.description', '/docs/references/account/update-prefs.md') + ->param('prefs', [], new Assoc(), 'Prefs key-value JSON object.') ->action(function ($prefs, $response, $user, $projectDB, $audits) { /** @var Appwrite\Utopia\Response $response */ /** @var Appwrite\Database\Document $user */ /** @var Appwrite\Database\Database $projectDB */ /** @var Appwrite\Event\Event $audits */ - - $old = \json_decode($user->getAttribute('prefs', '{}'), true); - $old = ($old) ? $old : []; - + $user = $projectDB->updateDocument(\array_merge($user->getArrayCopy(), [ - 'prefs' => \json_encode(\array_merge($old, $prefs)), + 'prefs' => $prefs, ])); if (false === $user) { @@ -920,14 +931,7 @@ App::patch('/v1/account/prefs') ->setParam('resource', 'users/'.$user->getId()) ; - $prefs = $user->getAttribute('prefs', '{}'); - - try { - $prefs = \json_decode($prefs, true); - $prefs = ($prefs) ? $prefs : []; - } catch (\Exception $error) { - throw new Exception('Failed to parse prefs', 500); - } + $prefs = $user->getAttribute('prefs', new \stdClass); $response->json($prefs); }, ['response', 'user', 'projectDB', 'audits']); @@ -935,7 +939,7 @@ App::patch('/v1/account/prefs') App::delete('/v1/account') ->desc('Delete Account') ->groups(['api', 'account']) - ->label('webhook', 'account.delete') + ->label('event', 'account.delete') ->label('scope', 'account') ->label('sdk.platform', [APP_PLATFORM_CLIENT]) ->label('sdk.namespace', 'account') @@ -974,10 +978,7 @@ App::delete('/v1/account') ; $webhooks - ->setParam('payload', [ - 'name' => $user->getAttribute('name', ''), - 'email' => $user->getAttribute('email', ''), - ]) + ->setParam('payload', $response->output($user, Response::MODEL_USER)) ; if (!Config::getParam('domainVerification')) { @@ -997,7 +998,7 @@ App::delete('/v1/account/sessions/:sessionId') ->desc('Delete Account Session') ->groups(['api', 'account']) ->label('scope', 'account') - ->label('webhook', 'account.sessions.delete') + ->label('event', 'account.sessions.delete') ->label('sdk.platform', [APP_PLATFORM_CLIENT]) ->label('sdk.namespace', 'account') ->label('sdk.method', 'deleteSession') @@ -1032,10 +1033,7 @@ App::delete('/v1/account/sessions/:sessionId') ; $webhooks - ->setParam('payload', [ - 'name' => $user->getAttribute('name', ''), - 'email' => $user->getAttribute('email', ''), - ]) + ->setParam('payload', $response->output($user, Response::MODEL_USER)) ; if (!Config::getParam('domainVerification')) { @@ -1062,7 +1060,7 @@ App::delete('/v1/account/sessions') ->desc('Delete All Account Sessions') ->groups(['api', 'account']) ->label('scope', 'account') - ->label('webhook', 'account.sessions.delete') + ->label('event', 'account.sessions.delete') ->label('sdk.platform', [APP_PLATFORM_CLIENT]) ->label('sdk.namespace', 'account') ->label('sdk.method', 'deleteSessions') @@ -1089,12 +1087,9 @@ App::delete('/v1/account/sessions') ->setParam('event', 'account.sessions.delete') ->setParam('resource', '/user/'.$user->getId()) ; - + $webhooks - ->setParam('payload', [ - 'name' => $user->getAttribute('name', ''), - 'email' => $user->getAttribute('email', ''), - ]) + ->setParam('payload', $response->output($user, Response::MODEL_USER)) ; if (!Config::getParam('domainVerification')) { diff --git a/app/controllers/api/database.php b/app/controllers/api/database.php index b535bb489..7f32abb77 100644 --- a/app/controllers/api/database.php +++ b/app/controllers/api/database.php @@ -2,15 +2,11 @@ use Utopia\App; use Utopia\Exception; -use Utopia\Response; use Utopia\Validator\Range; use Utopia\Validator\WhiteList; use Utopia\Validator\Text; use Utopia\Validator\ArrayList; use Utopia\Validator\JSON; -// use Utopia\Locale\Locale; -// use Utopia\Audit\Audit; -// use Utopia\Audit\Adapters\MySQL as AuditAdapter; use Appwrite\Database\Database; use Appwrite\Database\Document; use Appwrite\Database\Validator\UID; @@ -20,11 +16,12 @@ use Appwrite\Database\Validator\Collection; use Appwrite\Database\Validator\Authorization; use Appwrite\Database\Exception\Authorization as AuthorizationException; use Appwrite\Database\Exception\Structure as StructureException; +use Appwrite\Utopia\Response; App::post('/v1/database/collections') ->desc('Create Collection') ->groups(['api', 'database']) - ->label('webhook', 'database.collections.create') + ->label('event', 'database.collections.create') ->label('scope', 'collections.write') ->label('sdk.namespace', 'database') ->label('sdk.platform', [APP_PLATFORM_SERVER]) @@ -34,10 +31,9 @@ App::post('/v1/database/collections') ->param('read', [], new ArrayList(new Text(64)), '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.') ->param('write', [], new ArrayList(new Text(64)), '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.') ->param('rules', [], function ($projectDB) { return new ArrayList(new Collection($projectDB, [Database::SYSTEM_COLLECTION_RULES], ['$collection' => Database::SYSTEM_COLLECTION_RULES, '$permissions' => ['read' => [], 'write' => []]])); }, 'Array of [rule objects](/docs/rules). Each rule define a collection field name, data type and validation.', false, ['projectDB']) - ->action(function ($name, $read, $write, $rules, $response, $projectDB, $webhooks, $audits) { + ->action(function ($name, $read, $write, $rules, $response, $projectDB, $audits) { /** @var Appwrite\Utopia\Response $response */ /** @var Appwrite\Database\Database $projectDB */ - /** @var Appwrite\Event\Event $webhooks */ /** @var Appwrite\Event\Event $audits */ $parsedRules = []; @@ -77,26 +73,15 @@ App::post('/v1/database/collections') throw new Exception('Failed saving collection to DB', 500); } - $data = $data->getArrayCopy(); - - $webhooks - ->setParam('payload', $data) - ; - $audits ->setParam('event', 'database.collections.create') - ->setParam('resource', 'database/collection/'.$data['$id']) - ->setParam('data', $data) + ->setParam('resource', 'database/collection/'.$data->getId()) + ->setParam('data', $data->getArrayCopy()) ; - /* - * View - */ - $response - ->setStatusCode(Response::STATUS_CODE_CREATED) - ->json($data) - ; - }, ['response', 'projectDB', 'webhooks', 'audits']); + $response->setStatusCode(Response::STATUS_CODE_CREATED); + $response->dynamic($data, Response::MODEL_COLLECTION); + }, ['response', 'projectDB', 'audits']); App::get('/v1/database/collections') ->desc('List Collections') @@ -126,7 +111,10 @@ App::get('/v1/database/collections') ], ]); - $response->json(['sum' => $projectDB->getSum(), 'collections' => $results]); + $response->dynamic(new Document([ + 'sum' => $projectDB->getSum(), + 'collections' => $results + ]), Response::MODEL_COLLECTION_LIST); }, ['response', 'projectDB']); App::get('/v1/database/collections/:collectionId') @@ -148,79 +136,14 @@ App::get('/v1/database/collections/:collectionId') throw new Exception('Collection not found', 404); } - $response->json($collection->getArrayCopy()); + $response->dynamic($collection, Response::MODEL_COLLECTION); }, ['response', 'projectDB']); -// App::get('/v1/database/collections/:collectionId/logs') -// ->desc('Get Collection Logs') -// ->groups(['api', 'database']) -// ->label('scope', 'collections.read') -// ->label('sdk.platform', [APP_PLATFORM_SERVER]) -// ->label('sdk.namespace', 'database') -// ->label('sdk.method', 'getCollectionLogs') -// ->label('sdk.description', '/docs/references/database/get-collection-logs.md') -// ->param('collectionId', '', new UID(), 'Collection unique ID.') -// ->action( -// function ($collectionId) use ($response, $register, $projectDB, $project) { -// $collection = $projectDB->getDocument($collectionId, false); - -// if (empty($collection->getId()) || Database::SYSTEM_COLLECTION_COLLECTIONS != $collection->getCollection()) { -// throw new Exception('Collection not found', 404); -// } - -// $adapter = new AuditAdapter($register->get('db')); -// $adapter->setNamespace('app_'.$project->getId()); - -// $audit = new Audit($adapter); - -// $countries = Locale::getText('countries'); - -// $logs = $audit->getLogsByResource('database/collection/'.$collection->getId()); - -// $reader = new Reader(__DIR__.'/../../db/DBIP/dbip-country-lite-2020-01.mmdb'); -// $output = []; - -// foreach ($logs as $i => &$log) { -// $log['userAgent'] = (!empty($log['userAgent'])) ? $log['userAgent'] : 'UNKNOWN'; - -// $dd = new DeviceDetector($log['userAgent']); - -// $dd->skipBotDetection(); // OPTIONAL: If called, bot detection will completely be skipped (bots will be detected as regular devices then) - -// $dd->parse(); - -// $output[$i] = [ -// 'event' => $log['event'], -// 'ip' => $log['ip'], -// 'time' => strtotime($log['time']), -// 'OS' => $dd->getOs(), -// 'client' => $dd->getClient(), -// 'device' => $dd->getDevice(), -// 'brand' => $dd->getBrand(), -// 'model' => $dd->getModel(), -// 'geo' => [], -// ]; - -// try { -// $record = $reader->country($log['ip']); -// $output[$i]['geo']['isoCode'] = strtolower($record->country->isoCode); -// $output[$i]['geo']['country'] = $record->country->name; -// $output[$i]['geo']['country'] = (isset($countries[$record->country->isoCode])) ? $countries[$record->country->isoCode] : Locale::getText('locale.country.unknown'); -// } catch (\Exception $e) { -// $output[$i]['geo']['isoCode'] = '--'; -// $output[$i]['geo']['country'] = Locale::getText('locale.country.unknown'); -// } -// } - -// $response->json($output); -// } -// ); - App::put('/v1/database/collections/:collectionId') ->desc('Update Collection') ->groups(['api', 'database']) ->label('scope', 'collections.write') - ->label('webhook', 'database.collections.update') + ->label('event', 'database.collections.update') ->label('sdk.namespace', 'database') ->label('sdk.platform', [APP_PLATFORM_SERVER]) ->label('sdk.method', 'updateCollection') @@ -230,10 +153,9 @@ App::put('/v1/database/collections/:collectionId') ->param('read', [], new ArrayList(new Text(64)), '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.') ->param('write', [], new ArrayList(new Text(64)), '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.') ->param('rules', [], function ($projectDB) { return new ArrayList(new Collection($projectDB, [Database::SYSTEM_COLLECTION_RULES], ['$collection' => Database::SYSTEM_COLLECTION_RULES, '$permissions' => ['read' => [], 'write' => []]])); }, 'Array of [rule objects](/docs/rules). Each rule define a collection field name, data type and validation.', true, ['projectDB']) - ->action(function ($collectionId, $name, $read, $write, $rules, $response, $projectDB, $webhooks, $audits) { + ->action(function ($collectionId, $name, $read, $write, $rules, $response, $projectDB, $audits) { /** @var Appwrite\Utopia\Response $response */ /** @var Appwrite\Database\Database $projectDB */ - /** @var Appwrite\Event\Event $webhooks */ /** @var Appwrite\Event\Event $audits */ $collection = $projectDB->getDocument($collectionId, false); @@ -277,26 +199,20 @@ App::put('/v1/database/collections/:collectionId') throw new Exception('Failed saving collection to DB', 500); } - $data = $collection->getArrayCopy(); - - $webhooks - ->setParam('payload', $data) - ; - $audits ->setParam('event', 'database.collections.update') - ->setParam('resource', 'database/collections/'.$data['$id']) - ->setParam('data', $data) + ->setParam('resource', 'database/collections/'.$collection->getId()) + ->setParam('data', $collection->getArrayCopy()) ; - $response->json($collection->getArrayCopy()); - }, ['response', 'projectDB', 'webhooks', 'audits']); + $response->dynamic($collection, Response::MODEL_COLLECTION); + }, ['response', 'projectDB', 'audits']); App::delete('/v1/database/collections/:collectionId') ->desc('Delete Collection') ->groups(['api', 'database']) ->label('scope', 'collections.write') - ->label('webhook', 'database.collections.delete') + ->label('event', 'database.collections.delete') ->label('sdk.namespace', 'database') ->label('sdk.platform', [APP_PLATFORM_SERVER]) ->label('sdk.method', 'deleteCollection') @@ -318,16 +234,14 @@ App::delete('/v1/database/collections/:collectionId') throw new Exception('Failed to remove collection from DB', 500); } - $data = $collection->getArrayCopy(); - $webhooks - ->setParam('payload', $data) + ->setParam('payload', $response->output($collection, Response::MODEL_COLLECTION)) ; $audits ->setParam('event', 'database.collections.delete') - ->setParam('resource', 'database/collections/'.$data['$id']) - ->setParam('data', $data) + ->setParam('resource', 'database/collections/'.$collection->getId()) + ->setParam('data', $collection->getArrayCopy()) ; $response->noContent(); @@ -336,7 +250,7 @@ App::delete('/v1/database/collections/:collectionId') App::post('/v1/database/collections/:collectionId/documents') ->desc('Create Document') ->groups(['api', 'database']) - ->label('webhook', 'database.documents.create') + ->label('event', 'database.documents.create') ->label('scope', 'documents.write') ->label('sdk.namespace', 'database') ->label('sdk.platform', [APP_PLATFORM_CLIENT, APP_PLATFORM_SERVER]) @@ -349,10 +263,9 @@ App::post('/v1/database/collections/:collectionId/documents') ->param('parentDocument', '', new UID(), 'Parent document unique ID. Use when you want your new document to be a child of a parent document.', true) ->param('parentProperty', '', new Key(), 'Parent document property name. Use when you want your new document to be a child of a parent document.', true) ->param('parentPropertyType', Document::SET_TYPE_ASSIGN, new WhiteList([Document::SET_TYPE_ASSIGN, Document::SET_TYPE_APPEND, Document::SET_TYPE_PREPEND], true), 'Parent document property connection type. You can set this value to **assign**, **append** or **prepend**, default value is assign. Use when you want your new document to be a child of a parent document.', true) - ->action(function ($collectionId, $data, $read, $write, $parentDocument, $parentProperty, $parentPropertyType, $response, $projectDB, $webhooks, $audits) { + ->action(function ($collectionId, $data, $read, $write, $parentDocument, $parentProperty, $parentPropertyType, $response, $projectDB, $audits) { /** @var Appwrite\Utopia\Response $response */ /** @var Appwrite\Database\Database $projectDB */ - /** @var Appwrite\Event\Event $webhooks */ /** @var Appwrite\Event\Event $audits */ $data = (\is_string($data)) ? \json_decode($data, true) : $data; // Cast to JSON array @@ -387,11 +300,11 @@ App::post('/v1/database/collections/:collectionId/documents') } /* - * 1. Check child has valid structure, - * 2. Check user have write permission for parent document - * 3. Assign parent data (including child) to $data - * 4. Validate the combined result has valid structure (inside $projectDB->createDocument method) - */ + * 1. Check child has valid structure, + * 2. Check user have write permission for parent document + * 3. Assign parent data (including child) to $data + * 4. Validate the combined result has valid structure (inside $projectDB->createDocument method) + */ $new = new Document($data); @@ -436,26 +349,18 @@ App::post('/v1/database/collections/:collectionId/documents') throw new Exception('Failed saving document to DB'.$exception->getMessage(), 500); } - $data = $data->getArrayCopy(); - - $webhooks - ->setParam('payload', $data) - ; - $audits ->setParam('event', 'database.documents.create') ->setParam('resource', 'database/document/'.$data['$id']) - ->setParam('data', $data) + ->setParam('data', $data->getArrayCopy()) ; - /* - * View - */ $response ->setStatusCode(Response::STATUS_CODE_CREATED) - ->json($data) ; - }, ['response', 'projectDB', 'webhooks', 'audits']); + + $response->dynamic($data, Response::MODEL_ANY); + }, ['response', 'projectDB', 'audits']); App::get('/v1/database/collections/:collectionId/documents') ->desc('List Documents') @@ -495,27 +400,24 @@ App::get('/v1/database/collections/:collectionId/documents') ]), ]); - if (App::isDevelopment()) { - $collection - ->setAttribute('debug', $projectDB->getDebug()) - ->setAttribute('limit', $limit) - ->setAttribute('offset', $offset) - ->setAttribute('orderField', $orderField) - ->setAttribute('orderType', $orderType) - ->setAttribute('orderCast', $orderCast) - ->setAttribute('filters', $filters) - ; - } + // if (App::isDevelopment()) { + // $collection + // ->setAttribute('debug', $projectDB->getDebug()) + // ->setAttribute('limit', $limit) + // ->setAttribute('offset', $offset) + // ->setAttribute('orderField', $orderField) + // ->setAttribute('orderType', $orderType) + // ->setAttribute('orderCast', $orderCast) + // ->setAttribute('filters', $filters) + // ; + // } $collection ->setAttribute('sum', $projectDB->getSum()) ->setAttribute('documents', $list) ; - /* - * View - */ - $response->json($collection->getArrayCopy(/*['$id', '$collection', 'name', 'documents']*/[], ['rules'])); + $response->dynamic($collection, Response::MODEL_DOCUMENT_LIST); }, ['response', 'projectDB']); App::get('/v1/database/collections/:collectionId/documents/:documentId') @@ -540,36 +442,13 @@ App::get('/v1/database/collections/:collectionId/documents/:documentId') throw new Exception('No document found', 404); } - $output = $document->getArrayCopy(); - - $paths = \explode('/', $request->getParam('q', '')); - $paths = \array_slice($paths, 7, \count($paths)); - - if (\count($paths) > 0) { - if (\count($paths) % 2 == 1) { - $output = $document->getAttribute(\implode('.', $paths)); - } else { - $id = (int) \array_pop($paths); - $output = $document->search('$id', $id, $document->getAttribute(\implode('.', $paths))); - } - - $output = ($output instanceof Document) ? $output->getArrayCopy() : $output; - - if (!\is_array($output)) { - throw new Exception('No document found', 404); - } - } - - /* - * View - */ - $response->json($output); + $response->dynamic($document, Response::MODEL_ANY); }, ['request', 'response', 'projectDB']); App::patch('/v1/database/collections/:collectionId/documents/:documentId') ->desc('Update Document') ->groups(['api', 'database']) - ->label('webhook', 'database.documents.update') + ->label('event', 'database.documents.update') ->label('scope', 'documents.write') ->label('sdk.namespace', 'database') ->label('sdk.platform', [APP_PLATFORM_CLIENT, APP_PLATFORM_SERVER]) @@ -580,10 +459,9 @@ App::patch('/v1/database/collections/:collectionId/documents/:documentId') ->param('data', [], new JSON(), 'Document data as JSON object.') ->param('read', [], new ArrayList(new Text(64)), '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.') ->param('write', [], new ArrayList(new Text(64)), '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.') - ->action(function ($collectionId, $documentId, $data, $read, $write, $response, $projectDB, $webhooks, $audits) { + ->action(function ($collectionId, $documentId, $data, $read, $write, $response, $projectDB, $audits) { /** @var Appwrite\Utopia\Response $response */ /** @var Appwrite\Database\Database $projectDB */ - /** @var Appwrite\Event\Event $webhooks */ /** @var Appwrite\Event\Event $audits */ $collection = $projectDB->getDocument($collectionId, false); @@ -592,7 +470,7 @@ App::patch('/v1/database/collections/:collectionId/documents/:documentId') $data = (\is_string($data)) ? \json_decode($data, true) : $data; // Cast to JSON array if (!\is_array($data)) { - throw new Exception('Data param should be a valid JSON', 400); + throw new Exception('Data param should be a valid JSON object', 400); } if (\is_null($collection->getId()) || Database::SYSTEM_COLLECTION_COLLECTIONS != $collection->getCollection()) { @@ -621,6 +499,7 @@ App::patch('/v1/database/collections/:collectionId/documents/:documentId') if (empty($data)) { throw new Exception('Missing payload', 400); } + try { $data = $projectDB->updateDocument($data); } catch (AuthorizationException $exception) { @@ -631,29 +510,20 @@ App::patch('/v1/database/collections/:collectionId/documents/:documentId') throw new Exception('Failed saving document to DB', 500); } - $data = $data->getArrayCopy(); - - $webhooks - ->setParam('payload', $data) - ; - $audits ->setParam('event', 'database.documents.update') - ->setParam('resource', 'database/document/'.$data['$id']) - ->setParam('data', $data) + ->setParam('resource', 'database/document/'.$data->getId()) + ->setParam('data', $data->getArrayCopy()) ; - /* - * View - */ - $response->json($data); - }, ['response', 'projectDB', 'webhooks', 'audits']); + $response->dynamic($data, Response::MODEL_ANY); + }, ['response', 'projectDB', 'audits']); App::delete('/v1/database/collections/:collectionId/documents/:documentId') ->desc('Delete Document') ->groups(['api', 'database']) ->label('scope', 'documents.write') - ->label('webhook', 'database.documents.delete') + ->label('event', 'database.documents.delete') ->label('sdk.namespace', 'database') ->label('sdk.platform', [APP_PLATFORM_CLIENT, APP_PLATFORM_SERVER]) ->label('sdk.method', 'deleteDocument') @@ -687,16 +557,14 @@ App::delete('/v1/database/collections/:collectionId/documents/:documentId') throw new Exception('Failed to remove document from DB', 500); } - $data = $document->getArrayCopy(); - $webhooks - ->setParam('payload', $data) + ->setParam('payload', $response->output($document, Response::MODEL_ANY)) ; - + $audits ->setParam('event', 'database.documents.delete') - ->setParam('resource', 'database/document/'.$data['$id']) - ->setParam('data', $data) // Audit document in case of malicious or disastrous action + ->setParam('resource', 'database/document/'.$document->getId()) + ->setParam('data', $document->getArrayCopy()) // Audit document in case of malicious or disastrous action ; $response->noContent(); diff --git a/app/controllers/api/functions.php b/app/controllers/api/functions.php index 41ea32348..a46e8f32a 100644 --- a/app/controllers/api/functions.php +++ b/app/controllers/api/functions.php @@ -1,15 +1,16 @@ time(), 'dateUpdated' => time(), - 'status' => 'paused', + 'status' => 'disabled', 'name' => $name, 'env' => $env, 'tag' => '', 'vars' => $vars, 'events' => $events, 'schedule' => $schedule, - 'previous' => null, - 'next' => null, + 'schedulePrevious' => null, + 'scheduleNext' => null, 'timeout' => $timeout, ]); @@ -59,10 +60,8 @@ App::post('/v1/functions') throw new Exception('Failed saving function to DB', 500); } - $response - ->setStatusCode(Response::STATUS_CODE_CREATED) - ->json($function->getArrayCopy()) - ; + $response->setStatusCode(Response::STATUS_CODE_CREATED); + $response->dynamic($function, Response::MODEL_FUNCTION); }, ['response', 'projectDB']); App::get('/v1/functions') @@ -90,7 +89,10 @@ App::get('/v1/functions') ], ]); - $response->json(['sum' => $projectDB->getSum(), 'functions' => $results]); + $response->dynamic(new Document([ + 'sum' => $projectDB->getSum(), + 'functions' => $results + ]), Response::MODEL_FUNCTION_LIST); }, ['response', 'projectDB']); App::get('/v1/functions/:functionId') @@ -106,12 +108,125 @@ App::get('/v1/functions/:functionId') $function = $projectDB->getDocument($functionId); if (empty($function->getId()) || Database::SYSTEM_COLLECTION_FUNCTIONS != $function->getCollection()) { - throw new Exception('function not found', 404); + throw new Exception('Function not found', 404); } - $response->json($function->getArrayCopy()); + $response->dynamic($function, Response::MODEL_FUNCTION); }, ['response', 'projectDB']); +App::get('/v1/functions/:functionId/usage') + ->desc('Get Function Usage') + ->groups(['api', 'functions']) + ->label('scope', 'functions.read') + ->label('sdk.platform', [APP_PLATFORM_CONSOLE]) + ->label('sdk.namespace', 'functions') + ->label('sdk.method', 'getUsage') + ->param('functionId', '', new UID(), 'Function unique ID.') + ->param('range', '30d', new WhiteList(['24h', '7d', '30d', '90d']), 'Date range.', true) + ->action(function ($functionId, $range, $response, $project, $projectDB, $register) { + /** @var Appwrite\Utopia\Response $response */ + /** @var Appwrite\Database\Document $project */ + /** @var Appwrite\Database\Database $consoleDB */ + /** @var Appwrite\Database\Database $projectDB */ + /** @var Utopia\Registry\Registry $register */ + + $function = $projectDB->getDocument($functionId); + + if (empty($function->getId()) || Database::SYSTEM_COLLECTION_FUNCTIONS != $function->getCollection()) { + throw new Exception('Function not found', 404); + } + + $period = [ + '24h' => [ + 'start' => DateTime::createFromFormat('U', \strtotime('-24 hours')), + 'end' => DateTime::createFromFormat('U', \strtotime('+1 hour')), + 'group' => '30m', + ], + '7d' => [ + 'start' => DateTime::createFromFormat('U', \strtotime('-7 days')), + 'end' => DateTime::createFromFormat('U', \strtotime('now')), + 'group' => '1d', + ], + '30d' => [ + 'start' => DateTime::createFromFormat('U', \strtotime('-30 days')), + 'end' => DateTime::createFromFormat('U', \strtotime('now')), + 'group' => '1d', + ], + '90d' => [ + 'start' => DateTime::createFromFormat('U', \strtotime('-90 days')), + 'end' => DateTime::createFromFormat('U', \strtotime('now')), + 'group' => '1d', + ], + ]; + + $client = $register->get('influxdb'); + + $executions = []; + $failures = []; + $compute = []; + + if ($client) { + $start = $period[$range]['start']->format(DateTime::RFC3339); + $end = $period[$range]['end']->format(DateTime::RFC3339); + $database = $client->selectDB('telegraf'); + + // Executions + $result = $database->query('SELECT sum(value) AS "value" FROM "appwrite_usage_executions_all" WHERE time > \''.$start.'\' AND time < \''.$end.'\' AND "metric_type"=\'counter\' AND "project"=\''.$project->getId().'\' AND "functionId"=\''.$function->getId().'\' GROUP BY time('.$period[$range]['group'].') FILL(null)'); + $points = $result->getPoints(); + + foreach ($points as $point) { + $executions[] = [ + 'value' => (!empty($point['value'])) ? $point['value'] : 0, + 'date' => \strtotime($point['time']), + ]; + } + + // Failures + $result = $database->query('SELECT sum(value) AS "value" FROM "appwrite_usage_executions_all" WHERE time > \''.$start.'\' AND time < \''.$end.'\' AND "metric_type"=\'counter\' AND "project"=\''.$project->getId().'\' AND "functionId"=\''.$function->getId().'\' AND "functionStatus"=\'failed\' GROUP BY time('.$period[$range]['group'].') FILL(null)'); + $points = $result->getPoints(); + + foreach ($points as $point) { + $failures[] = [ + 'value' => (!empty($point['value'])) ? $point['value'] : 0, + 'date' => \strtotime($point['time']), + ]; + } + + // Compute + $result = $database->query('SELECT sum(value) AS "value" FROM "appwrite_usage_executions_time" WHERE time > \''.$start.'\' AND time < \''.$end.'\' AND "metric_type"=\'counter\' AND "project"=\''.$project->getId().'\' AND "functionId"=\''.$function->getId().'\' GROUP BY time('.$period[$range]['group'].') FILL(null)'); + $points = $result->getPoints(); + + foreach ($points as $point) { + $compute[] = [ + 'value' => round((!empty($point['value'])) ? $point['value'] / 1000 : 0, 2), // minutes + 'date' => \strtotime($point['time']), + ]; + } + } + + $response->json([ + 'range' => $range, + 'executions' => [ + 'data' => $executions, + 'total' => \array_sum(\array_map(function ($item) { + return $item['value']; + }, $executions)), + ], + 'failures' => [ + 'data' => $failures, + 'total' => \array_sum(\array_map(function ($item) { + return $item['value']; + }, $failures)), + ], + 'compute' => [ + 'data' => $compute, + 'total' => \array_sum(\array_map(function ($item) { + return $item['value']; + }, $compute)), + ], + ]); + }, ['response', 'project', 'projectDB', 'register']); + App::put('/v1/functions/:functionId') ->groups(['api', 'functions']) ->desc('Update Function') @@ -142,16 +257,29 @@ App::put('/v1/functions/:functionId') 'vars' => $vars, 'events' => $events, 'schedule' => $schedule, - 'previous' => null, - 'next' => $next, + 'schedulePrevious' => null, + 'scheduleNext' => $next, 'timeout' => $timeout, ])); + if ($next) { + ResqueScheduler::enqueueAt($next, 'v1-functions', 'FunctionsV1', [ + + ]); + + // ->setParam('projectId', $project->getId()) + // ->setParam('event', $route->getLabel('event', '')) + // ->setParam('payload', []) + // ->setParam('functionId', null) + // ->setParam('executionId', null) + // ->setParam('trigger', 'event') + } + if (false === $function) { throw new Exception('Failed saving function to DB', 500); } - $response->json($function->getArrayCopy()); + $response->dynamic($function, Response::MODEL_FUNCTION); }, ['response', 'projectDB']); App::patch('/v1/functions/:functionId/tag') @@ -182,14 +310,14 @@ App::patch('/v1/functions/:functionId/tag') $function = $projectDB->updateDocument(array_merge($function->getArrayCopy(), [ 'tag' => $tag->getId(), - 'next' => $next, + 'scheduleNext' => $next, ])); if (false === $function) { throw new Exception('Failed saving function to DB', 500); } - $response->json($function->getArrayCopy()); + $response->dynamic($function, Response::MODEL_FUNCTION); }, ['response', 'projectDB']); App::delete('/v1/functions/:functionId') @@ -201,7 +329,11 @@ App::delete('/v1/functions/:functionId') ->label('sdk.method', 'delete') ->label('sdk.description', '/docs/references/functions/delete-function.md') ->param('functionId', '', new UID(), 'Function unique ID.') - ->action(function ($functionId, $response, $projectDB) { + ->action(function ($functionId, $response, $projectDB, $deletes) { + /** @var Appwrite\Utopia\Response $response */ + /** @var Appwrite\Database\Database $projectDB */ + /** @var Appwrite\Event\Event $deletes */ + $function = $projectDB->getDocument($functionId); if (empty($function->getId()) || Database::SYSTEM_COLLECTION_FUNCTIONS != $function->getCollection()) { @@ -212,8 +344,12 @@ App::delete('/v1/functions/:functionId') throw new Exception('Failed to remove function from DB', 500); } + $deletes + ->setParam('document', $function->getArrayCopy()) + ; + $response->noContent(); - }, ['response', 'projectDB']); + }, ['response', 'projectDB', 'deletes']); App::post('/v1/functions/:functionId/tags') ->groups(['api', 'functions']) @@ -276,11 +412,11 @@ App::post('/v1/functions/:functionId/tags') 'read' => [], 'write' => [], ], - 'dateCreated' => time(), 'functionId' => $function->getId(), + 'dateCreated' => time(), 'command' => $command, - 'codePath' => $path, - 'codeSize' => $size, + 'path' => $path, + 'size' => $size, ]); if (false === $tag) { @@ -288,13 +424,11 @@ App::post('/v1/functions/:functionId/tags') } $usage - ->setParam('storage', $tag->getAttribute('codeSize', 0)) + ->setParam('storage', $tag->getAttribute('size', 0)) ; - $response - ->setStatusCode(Response::STATUS_CODE_CREATED) - ->json($tag->getArrayCopy()) - ; + $response->setStatusCode(Response::STATUS_CODE_CREATED); + $response->dynamic($tag, Response::MODEL_TAG); }, ['request', 'response', 'projectDB', 'usage']); App::get('/v1/functions/:functionId/tags') @@ -330,7 +464,10 @@ App::get('/v1/functions/:functionId/tags') ], ]); - $response->json(['sum' => $projectDB->getSum(), 'tags' => $results]); + $response->dynamic(new Document([ + 'sum' => $projectDB->getSum(), + 'tags' => $results + ]), Response::MODEL_TAG_LIST); }, ['response', 'projectDB']); App::get('/v1/functions/:functionId/tags/:tagId') @@ -360,7 +497,7 @@ App::get('/v1/functions/:functionId/tags/:tagId') throw new Exception('Tag not found', 404); } - $response->json($tag->getArrayCopy()); + $response->dynamic($tag, Response::MODEL_TAG); }, ['response', 'projectDB']); App::delete('/v1/functions/:functionId/tags/:tagId') @@ -392,14 +529,24 @@ App::delete('/v1/functions/:functionId/tags/:tagId') $device = Storage::getDevice('functions'); - if ($device->delete($tag->getAttribute('codePath', ''))) { + if ($device->delete($tag->getAttribute('path', ''))) { if (!$projectDB->deleteDocument($tag->getId())) { throw new Exception('Failed to remove tag from DB', 500); } } + if($function->getAttribute('tag') === $tag->getId()) { // Reset function tag + $function = $projectDB->updateDocument(array_merge($function->getArrayCopy(), [ + 'tag' => '', + ])); + + if (false === $function) { + throw new Exception('Failed saving function to DB', 500); + } + } + $usage - ->setParam('storage', $tag->getAttribute('codeSize', 0) * -1) + ->setParam('storage', $tag->getAttribute('size', 0) * -1) ; $response->noContent(); @@ -414,8 +561,8 @@ App::post('/v1/functions/:functionId/executions') ->label('sdk.method', 'createExecution') ->label('sdk.description', '/docs/references/functions/create-execution.md') ->param('functionId', '', new UID(), 'Function unique ID.') - ->param('async', 1, new Range(0, 1), 'Execute code asynchronously. Pass 1 for true, 0 for false. Default value is 1.', true) - ->action(function ($functionId, $async, $response, $project, $projectDB) { + // ->param('async', 1, new Range(0, 1), 'Execute code asynchronously. Pass 1 for true, 0 for false. Default value is 1.', true) + ->action(function ($functionId, /*$async,*/ $response, $project, $projectDB) { /** @var Appwrite\Utopia\Response $response */ /** @var Appwrite\Database\Document $project */ /** @var Appwrite\Database\Database $projectDB */ @@ -444,6 +591,7 @@ App::post('/v1/functions/:functionId/executions') ], 'dateCreated' => time(), 'functionId' => $function->getId(), + 'trigger' => 'http', // http / schedule / event 'status' => 'waiting', // waiting / processing / completed / failed 'exitCode' => 0, 'stdout' => '', @@ -454,22 +602,17 @@ App::post('/v1/functions/:functionId/executions') if (false === $execution) { throw new Exception('Failed saving execution to DB', 500); } - - if ((bool)$async) { - // Issue a TLS certificate when domain is verified - Resque::enqueue('v1-functions', 'FunctionsV1', [ - 'projectId' => $project->getId(), - 'functionId' => $function->getId(), - 'executionId' => $execution->getId(), - 'functionTag' => $tag->getId(), - 'functionTrigger' => 'API', - ]); - } + + // Issue a TLS certificate when domain is verified + Resque::enqueue('v1-functions', 'FunctionsV1', [ + 'projectId' => $project->getId(), + 'functionId' => $function->getId(), + 'executionId' => $execution->getId(), + 'trigger' => 'http', + ]); - $response - ->setStatusCode(Response::STATUS_CODE_CREATED) - ->json($execution->getArrayCopy()) - ; + $response->setStatusCode(Response::STATUS_CODE_CREATED); + $response->dynamic($execution, Response::MODEL_EXECUTION); }, ['response', 'project', 'projectDB']); App::get('/v1/functions/:functionId/executions') @@ -505,7 +648,10 @@ App::get('/v1/functions/:functionId/executions') ], ]); - $response->json(['sum' => $projectDB->getSum(), 'executions' => $results]); + $response->dynamic(new Document([ + 'sum' => $projectDB->getSum(), + 'executions' => $results + ]), Response::MODEL_EXECUTION_LIST); }, ['response', 'projectDB']); App::get('/v1/functions/:functionId/executions/:executionId') @@ -535,5 +681,5 @@ App::get('/v1/functions/:functionId/executions/:executionId') throw new Exception('Execution not found', 404); } - $response->json($execution->getArrayCopy()); - }, ['response', 'projectDB']); \ No newline at end of file + $response->dynamic($execution, Response::MODEL_EXECUTION); + }, ['response', 'projectDB']); diff --git a/app/controllers/api/locale.php b/app/controllers/api/locale.php index 7be2a5ce4..f65aeb88f 100644 --- a/app/controllers/api/locale.php +++ b/app/controllers/api/locale.php @@ -1,5 +1,7 @@ getText('countries'); $continents = $locale->getText('continents'); - + $output['ip'] = $ip; $currency = null; @@ -57,7 +59,8 @@ App::get('/v1/locale') $response ->addHeader('Cache-Control', 'public, max-age='.$time) ->addHeader('Expires', \date('D, d M Y H:i:s', \time() + $time).' GMT') // 45 days cache - ->json($output); + ; + $response->dynamic(new Document($output), Response::MODEL_LOCALE); }, ['request', 'response', 'locale', 'geodb']); App::get('/v1/locale/countries') @@ -73,10 +76,18 @@ App::get('/v1/locale/countries') /** @var Utopia\Locale\Locale $locale */ $list = $locale->getText('countries'); /* @var $list array */ + $output = []; - \asort($list); + \asort($list); // sort by abc per locale - $response->json($list); + foreach ($list as $key => $value) { + $output[] = new Document([ + 'name' => $value, + 'code' => $key, + ]); + } + + $response->dynamic(new Document(['countries' => $output, 'sum' => \count($output)]), Response::MODEL_COUNTRY_LIST); }, ['response', 'locale']); App::get('/v1/locale/countries/eu') @@ -91,19 +102,22 @@ App::get('/v1/locale/countries/eu') /** @var Appwrite\Utopia\Response $response */ /** @var Utopia\Locale\Locale $locale */ - $countries = $locale->getText('countries'); /* @var $countries array */ + $list = $locale->getText('countries'); /* @var $countries array */ $eu = Config::getParam('locale-eu'); - $list = []; - - foreach ($eu as $code) { - if (\array_key_exists($code, $countries)) { - $list[$code] = $countries[$code]; - } - } + $output = []; \asort($list); - $response->json($list); + foreach ($eu as $code) { + if (\array_key_exists($code, $list)) { + $output[] = new Document([ + 'name' => $list[$code], + 'code' => $code, + ]); + } + } + + $response->dynamic(new Document(['countries' => $output, 'sum' => \count($output)]), Response::MODEL_COUNTRY_LIST); }, ['response', 'locale']); App::get('/v1/locale/countries/phones') @@ -119,18 +133,22 @@ App::get('/v1/locale/countries/phones') /** @var Utopia\Locale\Locale $locale */ $list = Config::getParam('locale-phones'); /* @var $list array */ - $countries = $locale->getText('countries'); /* @var $countries array */ + $output = []; + + \asort($list); foreach ($list as $code => $name) { if (\array_key_exists($code, $countries)) { - $list[$code] = '+'.$list[$code]; + $output[] = new Document([ + 'code' => '+'.$list[$code], + 'countryCode' => $code, + 'countryName' => $countries[$code], + ]); } } - \asort($list); - - $response->json($list); + $response->dynamic(new Document(['phones' => $output, 'sum' => \count($output)]), Response::MODEL_PHONE_LIST); }, ['response', 'locale']); App::get('/v1/locale/continents') @@ -148,8 +166,15 @@ App::get('/v1/locale/continents') $list = $locale->getText('continents'); /* @var $list array */ \asort($list); + + foreach ($list as $key => $value) { + $output[] = new Document([ + 'name' => $value, + 'code' => $key, + ]); + } - $response->json($list); + $response->dynamic(new Document(['continents' => $output, 'sum' => \count($output)]), Response::MODEL_CONTINENT_LIST); }, ['response', 'locale']); App::get('/v1/locale/currencies') @@ -163,9 +188,13 @@ App::get('/v1/locale/currencies') ->action(function ($response) { /** @var Appwrite\Utopia\Response $response */ - $currencies = Config::getParam('locale-currencies'); + $list = Config::getParam('locale-currencies'); - $response->json($currencies); + $list = array_map(function($node) { + return new Document($node); + }, $list); + + $response->dynamic(new Document(['currencies' => $list, 'sum' => \count($list)]), Response::MODEL_CURRENCY_LIST); }, ['response']); @@ -180,7 +209,11 @@ App::get('/v1/locale/languages') ->action(function ($response) { /** @var Appwrite\Utopia\Response $response */ - $languages = Config::getParam('locale-languages'); + $list = Config::getParam('locale-languages'); - $response->json($languages); + $list = array_map(function($node) { + return new Document($node); + }, $list); + + $response->dynamic(new Document(['languages' => $list, 'sum' => \count($list)]), Response::MODEL_LANGUAGE_LIST); }, ['response']); \ No newline at end of file diff --git a/app/controllers/api/projects.php b/app/controllers/api/projects.php index 5037624b2..c8b6aa19a 100644 --- a/app/controllers/api/projects.php +++ b/app/controllers/api/projects.php @@ -2,7 +2,6 @@ use Utopia\App; use Utopia\Exception; -use Utopia\Response; use Utopia\Validator\ArrayList; use Utopia\Validator\Boolean; use Utopia\Validator\Text; @@ -18,6 +17,7 @@ use Appwrite\Database\Document; use Appwrite\Database\Validator\UID; use Appwrite\Network\Validator\CNAME; use Appwrite\Network\Validator\Domain as DomainValidator; +use Appwrite\Utopia\Response; use Cron\CronExpression; App::post('/v1/projects') @@ -70,6 +70,7 @@ App::post('/v1/projects') 'webhooks' => [], 'keys' => [], 'tasks' => [], + 'domains' => [], ] ); @@ -79,10 +80,8 @@ App::post('/v1/projects') $consoleDB->createNamespace($project->getId()); - $response - ->setStatusCode(Response::STATUS_CODE_CREATED) - ->json($project->getArrayCopy()) - ; + $response->setStatusCode(Response::STATUS_CODE_CREATED); + $response->dynamic($project, Response::MODEL_PROJECT); }, ['response', 'consoleDB', 'projectDB']); App::get('/v1/projects') @@ -111,7 +110,10 @@ App::get('/v1/projects') ], ]); - $response->json(['sum' => $consoleDB->getSum(), 'projects' => $results]); + $response->dynamic(new Document([ + 'sum' => $consoleDB->getSum(), + 'projects' => $results + ]), Response::MODEL_PROJECT_LIST); }, ['response', 'consoleDB']); App::get('/v1/projects/:projectId') @@ -131,7 +133,7 @@ App::get('/v1/projects/:projectId') throw new Exception('Project not found', 404); } - $response->json($project->getArrayCopy()); + $response->dynamic($project, Response::MODEL_PROJECT); }, ['response', 'consoleDB']); App::get('/v1/projects/:projectId/usage') @@ -141,7 +143,7 @@ App::get('/v1/projects/:projectId/usage') ->label('sdk.namespace', 'projects') ->label('sdk.method', 'getUsage') ->param('projectId', '', new UID(), 'Project unique ID.') - ->param('range', 'last30', new WhiteList(['daily', 'monthly', 'last30', 'last90'], true), 'Date range.', true) + ->param('range', '30d', new WhiteList(['24h', '7d', '30d', '90d'], true), 'Date range.', true) ->action(function ($projectId, $range, $response, $consoleDB, $projectDB, $register) { /** @var Appwrite\Utopia\Response $response */ /** @var Appwrite\Database\Database $consoleDB */ @@ -155,31 +157,26 @@ App::get('/v1/projects/:projectId/usage') } $period = [ - 'daily' => [ - 'start' => DateTime::createFromFormat('U', \strtotime('today')), - 'end' => DateTime::createFromFormat('U', \strtotime('tomorrow')), - 'group' => '1m', + '24h' => [ + 'start' => DateTime::createFromFormat('U', \strtotime('-24 hours')), + 'end' => DateTime::createFromFormat('U', \strtotime('+1 hour')), + 'group' => '30m', ], - 'monthly' => [ - 'start' => DateTime::createFromFormat('U', \strtotime('midnight first day of this month')), - 'end' => DateTime::createFromFormat('U', \strtotime('midnight last day of this month')), + '7d' => [ + 'start' => DateTime::createFromFormat('U', \strtotime('-7 days')), + 'end' => DateTime::createFromFormat('U', \strtotime('now')), 'group' => '1d', ], - 'last30' => [ + '30d' => [ 'start' => DateTime::createFromFormat('U', \strtotime('-30 days')), - 'end' => DateTime::createFromFormat('U', \strtotime('tomorrow')), + 'end' => DateTime::createFromFormat('U', \strtotime('now')), 'group' => '1d', ], - 'last90' => [ + '90d' => [ 'start' => DateTime::createFromFormat('U', \strtotime('-90 days')), - 'end' => DateTime::createFromFormat('U', \strtotime('today')), + 'end' => DateTime::createFromFormat('U', \strtotime('now')), 'group' => '1d', ], - // 'yearly' => [ - // 'start' => DateTime::createFromFormat('U', strtotime('midnight first day of january')), - // 'end' => DateTime::createFromFormat('U', strtotime('midnight last day of december')), - // 'group' => '4w', - // ], ]; $client = $register->get('influxdb'); @@ -257,6 +254,7 @@ App::get('/v1/projects/:projectId/usage') $tasksTotal = \count($project->getAttribute('tasks', [])); $response->json([ + 'range' => $range, 'requests' => [ 'data' => $requests, 'total' => \array_sum(\array_map(function ($item) { @@ -298,7 +296,7 @@ App::get('/v1/projects/:projectId/usage') ) + $projectDB->getCount( [ - 'attribute' => 'codeSize', + 'attribute' => 'size', 'filters' => [ '$collection='.Database::SYSTEM_COLLECTION_TAGS, ], @@ -352,7 +350,7 @@ App::patch('/v1/projects/:projectId') throw new Exception('Failed saving project to DB', 500); } - $response->json($project->getArrayCopy()); + $response->dynamic($project, Response::MODEL_PROJECT); }, ['response', 'consoleDB']); App::patch('/v1/projects/:projectId/oauth2') @@ -384,7 +382,7 @@ App::patch('/v1/projects/:projectId/oauth2') throw new Exception('Failed saving project to DB', 500); } - $response->json($project->getArrayCopy()); + $response->dynamic($project, Response::MODEL_PROJECT); }, ['response', 'consoleDB']); App::delete('/v1/projects/:projectId') @@ -487,10 +485,8 @@ App::post('/v1/projects/:projectId/webhooks') throw new Exception('Failed saving project to DB', 500); } - $response - ->setStatusCode(Response::STATUS_CODE_CREATED) - ->json($webhook->getArrayCopy()) - ; + $response->setStatusCode(Response::STATUS_CODE_CREATED); + $response->dynamic($webhook, Response::MODEL_WEBHOOK); }, ['response', 'consoleDB']); App::get('/v1/projects/:projectId/webhooks') @@ -512,7 +508,10 @@ App::get('/v1/projects/:projectId/webhooks') $webhooks = $project->getAttribute('webhooks', []); - $response->json($webhooks); + $response->dynamic(new Document([ + 'sum' => count($webhooks), + 'webhooks' => $webhooks + ]), Response::MODEL_WEBHOOK_LIST); }, ['response', 'consoleDB']); App::get('/v1/projects/:projectId/webhooks/:webhookId') @@ -539,7 +538,7 @@ App::get('/v1/projects/:projectId/webhooks/:webhookId') throw new Exception('Webhook not found', 404); } - $response->json($webhook->getArrayCopy()); + $response->dynamic($webhook, Response::MODEL_WEBHOOK); }, ['response', 'consoleDB']); App::put('/v1/projects/:projectId/webhooks/:webhookId') @@ -587,7 +586,7 @@ App::put('/v1/projects/:projectId/webhooks/:webhookId') throw new Exception('Failed saving webhook to DB', 500); } - $response->json($webhook->getArrayCopy()); + $response->dynamic($webhook, Response::MODEL_WEBHOOK); }, ['response', 'consoleDB']); App::delete('/v1/projects/:projectId/webhooks/:webhookId') @@ -665,10 +664,8 @@ App::post('/v1/projects/:projectId/keys') throw new Exception('Failed saving project to DB', 500); } - $response - ->setStatusCode(Response::STATUS_CODE_CREATED) - ->json($key->getArrayCopy()) - ; + $response->setStatusCode(Response::STATUS_CODE_CREATED); + $response->dynamic($key, Response::MODEL_KEY); }, ['response', 'consoleDB']); App::get('/v1/projects/:projectId/keys') @@ -688,7 +685,12 @@ App::get('/v1/projects/:projectId/keys') throw new Exception('Project not found', 404); } - $response->json($project->getAttribute('keys', [])); //FIXME make sure array objects return correctly + $keys = $project->getAttribute('keys', []); + + $response->dynamic(new Document([ + 'sum' => count($keys), + 'keys' => $keys + ]), Response::MODEL_KEY_LIST); }, ['response', 'consoleDB']); App::get('/v1/projects/:projectId/keys/:keyId') @@ -712,7 +714,7 @@ App::get('/v1/projects/:projectId/keys/:keyId') throw new Exception('Key not found', 404); } - $response->json($key->getArrayCopy()); + $response->dynamic($key, Response::MODEL_KEY); }, ['response', 'consoleDB']); App::put('/v1/projects/:projectId/keys/:keyId') @@ -750,7 +752,7 @@ App::put('/v1/projects/:projectId/keys/:keyId') throw new Exception('Failed saving key to DB', 500); } - $response->json($key->getArrayCopy()); + $response->dynamic($key, Response::MODEL_KEY); }, ['response', 'consoleDB']); App::delete('/v1/projects/:projectId/keys/:keyId') @@ -855,10 +857,8 @@ App::post('/v1/projects/:projectId/tasks') ResqueScheduler::enqueueAt($next, 'v1-tasks', 'TasksV1', $task->getArrayCopy()); } - $response - ->setStatusCode(Response::STATUS_CODE_CREATED) - ->json($task->getArrayCopy()) - ; + $response->setStatusCode(Response::STATUS_CODE_CREATED); + $response->dynamic($task, Response::MODEL_TASK); }, ['response', 'consoleDB']); App::get('/v1/projects/:projectId/tasks') @@ -880,7 +880,11 @@ App::get('/v1/projects/:projectId/tasks') $tasks = $project->getAttribute('tasks', []); - $response->json($tasks); + $response->dynamic(new Document([ + 'sum' => count($tasks), + 'tasks' => $tasks + ]), Response::MODEL_TASK_LIST); + }, ['response', 'consoleDB']); App::get('/v1/projects/:projectId/tasks/:taskId') @@ -907,7 +911,7 @@ App::get('/v1/projects/:projectId/tasks/:taskId') throw new Exception('Task not found', 404); } - $response->json($task->getArrayCopy()); + $response->dynamic($task, Response::MODEL_TASK); }, ['response', 'consoleDB']); App::put('/v1/projects/:projectId/tasks/:taskId') @@ -970,7 +974,7 @@ App::put('/v1/projects/:projectId/tasks/:taskId') ResqueScheduler::enqueueAt($next, 'v1-tasks', 'TasksV1', $task->getArrayCopy()); } - $response->json($task->getArrayCopy()); + $response->dynamic($task, Response::MODEL_TASK); }, ['response', 'consoleDB']); App::delete('/v1/projects/:projectId/tasks/:taskId') @@ -1055,10 +1059,8 @@ App::post('/v1/projects/:projectId/platforms') throw new Exception('Failed saving project to DB', 500); } - $response - ->setStatusCode(Response::STATUS_CODE_CREATED) - ->json($platform->getArrayCopy()) - ; + $response->setStatusCode(Response::STATUS_CODE_CREATED); + $response->dynamic($platform, Response::MODEL_PLATFORM); }, ['response', 'consoleDB']); App::get('/v1/projects/:projectId/platforms') @@ -1080,7 +1082,10 @@ App::get('/v1/projects/:projectId/platforms') $platforms = $project->getAttribute('platforms', []); - $response->json($platforms); + $response->dynamic(new Document([ + 'sum' => count($platforms), + 'platforms' => $platforms + ]), Response::MODEL_PLATFORM_LIST); }, ['response', 'consoleDB']); App::get('/v1/projects/:projectId/platforms/:platformId') @@ -1107,7 +1112,7 @@ App::get('/v1/projects/:projectId/platforms/:platformId') throw new Exception('Platform not found', 404); } - $response->json($platform->getArrayCopy()); + $response->dynamic($platform, Response::MODEL_PLATFORM); }, ['response', 'consoleDB']); App::put('/v1/projects/:projectId/platforms/:platformId') @@ -1150,7 +1155,7 @@ App::put('/v1/projects/:projectId/platforms/:platformId') throw new Exception('Failed saving platform to DB', 500); } - $response->json($platform->getArrayCopy()); + $response->dynamic($platform, Response::MODEL_PLATFORM); }, ['response', 'consoleDB']); App::delete('/v1/projects/:projectId/platforms/:platformId') @@ -1244,10 +1249,8 @@ App::post('/v1/projects/:projectId/domains') throw new Exception('Failed saving project to DB', 500); } - $response - ->setStatusCode(Response::STATUS_CODE_CREATED) - ->json($domain->getArrayCopy()) - ; + $response->setStatusCode(Response::STATUS_CODE_CREATED); + $response->dynamic($domain, Response::MODEL_DOMAIN); }, ['response', 'consoleDB']); App::get('/v1/projects/:projectId/domains') @@ -1268,8 +1271,11 @@ App::get('/v1/projects/:projectId/domains') } $domains = $project->getAttribute('domains', []); - - $response->json($domains); + + $response->dynamic(new Document([ + 'sum' => count($domains), + 'domains' => $domains + ]), Response::MODEL_DOMAIN_LIST); }, ['response', 'consoleDB']); App::get('/v1/projects/:projectId/domains/:domainId') @@ -1296,7 +1302,7 @@ App::get('/v1/projects/:projectId/domains/:domainId') throw new Exception('Domain not found', 404); } - $response->json($domain->getArrayCopy()); + $response->dynamic($domain, Response::MODEL_DOMAIN); }, ['response', 'consoleDB']); App::patch('/v1/projects/:projectId/domains/:domainId/verification') @@ -1330,7 +1336,7 @@ App::patch('/v1/projects/:projectId/domains/:domainId/verification') } if ($domain->getAttribute('verification') === true) { - return $response->json($domain->getArrayCopy()); + return $response->dynamic($domain, Response::MODEL_DOMAIN); } // Verify Domain with DNS records @@ -1354,7 +1360,7 @@ App::patch('/v1/projects/:projectId/domains/:domainId/verification') 'domain' => $domain->getAttribute('domain'), ]); - $response->json($domain->getArrayCopy()); + $response->dynamic($domain, Response::MODEL_DOMAIN); }, ['response', 'consoleDB']); App::delete('/v1/projects/:projectId/domains/:domainId') diff --git a/app/controllers/api/storage.php b/app/controllers/api/storage.php index 2add12c61..f9fe7c661 100644 --- a/app/controllers/api/storage.php +++ b/app/controllers/api/storage.php @@ -2,7 +2,6 @@ use Utopia\App; use Utopia\Exception; -use Utopia\Response; use Utopia\Validator\ArrayList; use Utopia\Validator\WhiteList; use Utopia\Validator\Range; @@ -12,6 +11,7 @@ use Utopia\Cache\Cache; use Utopia\Cache\Adapter\Filesystem; use Appwrite\ClamAV\Network; use Appwrite\Database\Database; +use Appwrite\Database\Document; use Appwrite\Database\Validator\UID; use Appwrite\Storage\Storage; use Appwrite\Storage\Validator\File; @@ -20,13 +20,14 @@ use Appwrite\Storage\Validator\Upload; use Appwrite\Storage\Compression\Algorithms\GZIP; use Appwrite\Resize\Resize; use Appwrite\OpenSSL\OpenSSL; +use Appwrite\Utopia\Response; use Utopia\Config\Config; App::post('/v1/storage/files') ->desc('Create File') ->groups(['api', 'storage']) ->label('scope', 'files.write') - ->label('webhook', 'storage.files.create') + ->label('event', 'storage.files.create') ->label('sdk.platform', [APP_PLATFORM_CLIENT, APP_PLATFORM_SERVER]) ->label('sdk.namespace', 'storage') ->label('sdk.method', 'createFile') @@ -36,12 +37,11 @@ App::post('/v1/storage/files') ->param('file', [], new File(), 'Binary file.', false) ->param('read', [], new ArrayList(new Text(64)), '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.') ->param('write', [], new ArrayList(new Text(64)), '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.') - ->action(function ($file, $read, $write, $request, $response, $user, $projectDB, $webhooks, $audits, $usage) { + ->action(function ($file, $read, $write, $request, $response, $user, $projectDB, $audits, $usage) { /** @var Utopia\Swoole\Request $request */ /** @var Appwrite\Utopia\Response $response */ /** @var Appwrite\Database\Document $user */ /** @var Appwrite\Database\Database $projectDB */ - /** @var Appwrite\Event\Event $webhooks */ /** @var Appwrite\Event\Event $audits */ /** @var Appwrite\Event\Event $usage */ @@ -140,10 +140,6 @@ App::post('/v1/storage/files') throw new Exception('Failed saving file to DB', 500); } - $webhooks - ->setParam('payload', $file->getArrayCopy()) - ; - $audits ->setParam('event', 'storage.files.create') ->setParam('resource', 'storage/files/'.$file->getId()) @@ -153,11 +149,9 @@ App::post('/v1/storage/files') ->setParam('storage', $sizeActual) ; - $response - ->setStatusCode(Response::STATUS_CODE_CREATED) - ->json($file->getArrayCopy()) - ; - }, ['request', 'response', 'user', 'projectDB', 'webhooks', 'audits', 'usage']); + $response->setStatusCode(Response::STATUS_CODE_CREATED); + $response->dynamic($file, Response::MODEL_FILE); + }, ['request', 'response', 'user', 'projectDB', 'audits', 'usage']); App::get('/v1/storage/files') ->desc('List Files') @@ -187,11 +181,10 @@ App::get('/v1/storage/files') ], ]); - $results = \array_map(function ($value) { /* @var $value \Database\Document */ - return $value->getArrayCopy(['$id', '$permissions', 'name', 'dateCreated', 'signature', 'mimeType', 'sizeOriginal']); - }, $results); - - $response->json(['sum' => $projectDB->getSum(), 'files' => $results]); + $response->dynamic(new Document([ + 'sum' => $projectDB->getSum(), + 'files' => $results + ]), Response::MODEL_FILE_LIST); }, ['response', 'projectDB']); App::get('/v1/storage/files/:fileId') @@ -213,7 +206,7 @@ App::get('/v1/storage/files/:fileId') throw new Exception('File not found', 404); } - $response->json($file->getArrayCopy(['$id', '$permissions', 'name', 'dateCreated', 'signature', 'mimeType', 'sizeOriginal'])); + $response->dynamic($file, Response::MODEL_FILE); }, ['response', 'projectDB']); App::get('/v1/storage/files/:fileId/preview') @@ -474,7 +467,7 @@ App::put('/v1/storage/files/:fileId') ->desc('Update File') ->groups(['api', 'storage']) ->label('scope', 'files.write') - ->label('webhook', 'storage.files.update') + ->label('event', 'storage.files.update') ->label('sdk.platform', [APP_PLATFORM_CLIENT, APP_PLATFORM_SERVER]) ->label('sdk.namespace', 'storage') ->label('sdk.method', 'updateFile') @@ -482,10 +475,9 @@ App::put('/v1/storage/files/:fileId') ->param('fileId', '', new UID(), 'File unique ID.') ->param('read', [], new ArrayList(new Text(64)), '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.') ->param('write', [], new ArrayList(new Text(64)), '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.') - ->action(function ($fileId, $read, $write, $response, $projectDB, $webhooks, $audits) { + ->action(function ($fileId, $read, $write, $response, $projectDB, $audits) { /** @var Appwrite\Utopia\Response $response */ /** @var Appwrite\Database\Database $projectDB */ - /** @var Appwrite\Event\Event $webhooks */ /** @var Appwrite\Event\Event $audits */ $file = $projectDB->getDocument($fileId); @@ -506,23 +498,19 @@ App::put('/v1/storage/files/:fileId') throw new Exception('Failed saving file to DB', 500); } - $webhooks - ->setParam('payload', $file->getArrayCopy()) - ; - $audits ->setParam('event', 'storage.files.update') ->setParam('resource', 'storage/files/'.$file->getId()) ; - $response->json($file->getArrayCopy()); + $response->dynamic($file, Response::MODEL_FILE); }, ['response', 'projectDB', 'webhooks', 'audits']); App::delete('/v1/storage/files/:fileId') ->desc('Delete File') ->groups(['api', 'storage']) ->label('scope', 'files.write') - ->label('webhook', 'storage.files.delete') + ->label('event', 'storage.files.delete') ->label('sdk.platform', [APP_PLATFORM_CLIENT, APP_PLATFORM_SERVER]) ->label('sdk.namespace', 'storage') ->label('sdk.method', 'deleteFile') @@ -548,11 +536,7 @@ App::delete('/v1/storage/files/:fileId') throw new Exception('Failed to remove file from DB', 500); } } - - $webhooks - ->setParam('payload', $file->getArrayCopy()) - ; - + $audits ->setParam('event', 'storage.files.delete') ->setParam('resource', 'storage/files/'.$file->getId()) @@ -562,6 +546,10 @@ App::delete('/v1/storage/files/:fileId') ->setParam('storage', $file->getAttribute('size', 0) * -1) ; + $webhooks + ->setParam('payload', $response->output($file, Response::MODEL_FILE)) + ; + $response->noContent(); }, ['response', 'projectDB', 'webhooks', 'audits', 'usage']); @@ -613,6 +601,5 @@ App::delete('/v1/storage/files/:fileId') // //var_dump($antiVirus->version()); // //var_dump($antiVirus->fileScan('/storage/uploads/app-1/5/9/f/e/59fecaed49645.pdf')); -// //$response->json($antiVirus->continueScan($device->getRoot())); // } // ); diff --git a/app/controllers/api/teams.php b/app/controllers/api/teams.php index 28f3aff49..fe17a42e8 100644 --- a/app/controllers/api/teams.php +++ b/app/controllers/api/teams.php @@ -18,6 +18,7 @@ use Appwrite\Database\Exception\Duplicate; use Appwrite\Database\Validator\Key; use Appwrite\Template\Template; use Appwrite\Utopia\Response; +use DeviceDetector\DeviceDetector; App::post('/v1/teams') ->desc('Create Team') @@ -80,10 +81,8 @@ App::post('/v1/teams') } } - $response - ->setStatusCode(Response::STATUS_CODE_CREATED) - ->json($team->getArrayCopy()) - ; + $response->setStatusCode(Response::STATUS_CODE_CREATED); + $response->dynamic($team, Response::MODEL_TEAM); }, ['response', 'user', 'projectDB', 'mode']); App::get('/v1/teams') @@ -114,7 +113,10 @@ App::get('/v1/teams') ], ]); - $response->json(['sum' => $projectDB->getSum(), 'teams' => $results]); + $response->dynamic(new Document([ + 'sum' => $projectDB->getSum(), + 'teams' => $results + ]), Response::MODEL_TEAM_LIST); }, ['response', 'projectDB']); App::get('/v1/teams/:teamId') @@ -136,7 +138,7 @@ App::get('/v1/teams/:teamId') throw new Exception('Team not found', 404); } - $response->json($team->getArrayCopy([])); + $response->dynamic($team, Response::MODEL_TEAM); }, ['response', 'projectDB']); App::put('/v1/teams/:teamId') @@ -166,8 +168,8 @@ App::put('/v1/teams/:teamId') if (false === $team) { throw new Exception('Failed saving team to DB', 500); } - - $response->json($team->getArrayCopy()); + + $response->dynamic($team, Response::MODEL_TEAM); }, ['response', 'projectDB']); App::delete('/v1/teams/:teamId') @@ -391,21 +393,12 @@ App::post('/v1/teams/:teamId/memberships') ->setParam('resource', 'teams/'.$teamId) ; - $response - ->setStatusCode(Response::STATUS_CODE_CREATED) // TODO change response of this endpoint - ->json(\array_merge($membership->getArrayCopy([ - '$id', - 'userId', - 'teamId', - 'roles', - 'invited', - 'joined', - 'confirm', - ]), [ - 'email' => $email, - 'name' => $name, - ])) - ; + $response->setStatusCode(Response::STATUS_CODE_CREATED); + + $response->dynamic(new Document(\array_merge($membership->getArrayCopy(), [ + 'email' => $email, + 'name' => $name, + ])), Response::MODEL_MEMBERSHIP); }, ['response', 'project', 'user', 'projectDB', 'locale', 'audits', 'mails', 'mode']); App::get('/v1/teams/:teamId/memberships') @@ -453,18 +446,10 @@ App::get('/v1/teams/:teamId/memberships') $temp = $projectDB->getDocument($membership->getAttribute('userId', null))->getArrayCopy(['email', 'name']); - $users[] = \array_merge($temp, $membership->getArrayCopy([ - '$id', - 'userId', - 'teamId', - 'roles', - 'invited', - 'joined', - 'confirm', - ])); + $users[] = new Document(\array_merge($temp, $membership->getArrayCopy())); } - $response->json(['sum' => $projectDB->getSum(), 'memberships' => $users]); + $response->dynamic(new Document(['sum' => $projectDB->getSum(), 'memberships' => $users]), Response::MODEL_MEMBERSHIP_LIST); }, ['response', 'projectDB']); App::patch('/v1/teams/:teamId/memberships/:inviteId/status') @@ -479,11 +464,12 @@ App::patch('/v1/teams/:teamId/memberships/:inviteId/status') ->param('inviteId', '', new UID(), 'Invite unique ID.') ->param('userId', '', new UID(), 'User unique ID.') ->param('secret', '', new Text(256), 'Secret key.') - ->action(function ($teamId, $inviteId, $userId, $secret, $request, $response, $user, $projectDB, $audits) { + ->action(function ($teamId, $inviteId, $userId, $secret, $request, $response, $user, $projectDB, $geodb, $audits) { /** @var Utopia\Swoole\Request $request */ /** @var Appwrite\Utopia\Response $response */ /** @var Appwrite\Database\Document $user */ /** @var Appwrite\Database\Database $projectDB */ + /** @var MaxMind\Db\Reader $geodb */ /** @var Appwrite\Event\Event $audits */ $protocol = $request->getProtocol(); @@ -540,10 +526,28 @@ App::patch('/v1/teams/:teamId/memberships/:inviteId/status') ; // Log user in + + $dd = new DeviceDetector($request->getUserAgent('UNKNOWN')); + + $dd->parse(); + + $os = $dd->getOs(); + $osCode = (isset($os['short_name'])) ? $os['short_name'] : ''; + $osName = (isset($os['name'])) ? $os['name'] : ''; + $osVersion = (isset($os['version'])) ? $os['version'] : ''; + + $client = $dd->getClient(); + $clientType = (isset($client['type'])) ? $client['type'] : ''; + $clientCode = (isset($client['short_name'])) ? $client['short_name'] : ''; + $clientName = (isset($client['name'])) ? $client['name'] : ''; + $clientVersion = (isset($client['version'])) ? $client['version'] : ''; + $clientEngine = (isset($client['engine'])) ? $client['engine'] : ''; + $clientEngineVersion = (isset($client['engine_version'])) ? $client['engine_version'] : ''; + $expiry = \time() + Auth::TOKEN_EXPIRATION_LOGIN_LONG; $secret = Auth::tokenGenerator(); - $user->setAttribute('tokens', new Document([ + $session = new Document([ '$collection' => Database::SYSTEM_COLLECTION_TOKENS, '$permissions' => ['read' => ['user:'.$user->getId()], 'write' => ['user:'.$user->getId()]], 'type' => Auth::TOKEN_TYPE_LOGIN, @@ -551,7 +555,34 @@ App::patch('/v1/teams/:teamId/memberships/:inviteId/status') 'expire' => $expiry, 'userAgent' => $request->getUserAgent('UNKNOWN'), 'ip' => $request->getIP(), - ]), Document::SET_TYPE_APPEND); + + 'osCode' => $osCode, + 'osName' => $osName, + 'osVersion' => $osVersion, + 'clientType' => $clientType, + 'clientCode' => $clientCode, + 'clientName' => $clientName, + 'clientVersion' => $clientVersion, + 'clientEngine' => $clientEngine, + 'clientEngineVersion' => $clientEngineVersion, + 'deviceName' => $dd->getDeviceName(), + 'deviceBrand' => $dd->getBrandName(), + 'deviceModel' => $dd->getModel(), + ]); + + $record = $geodb->get($request->getIP()); + + if($record) { + $session + ->setAttribute('countryCode', \strtolower($record['country']['iso_code'])) + ; + } else { + $session + ->setAttribute('countryCode', '--') + ; + } + + $user->setAttribute('tokens', $session, Document::SET_TYPE_APPEND); Authorization::setRole('user:'.$userId); @@ -594,7 +625,7 @@ App::patch('/v1/teams/:teamId/memberships/:inviteId/status') 'email' => $user->getAttribute('email'), 'name' => $user->getAttribute('name'), ])), Response::MODEL_MEMBERSHIP); - }, ['request', 'response', 'user', 'projectDB', 'audits']); + }, ['request', 'response', 'user', 'projectDB', 'geodb', 'audits']); App::delete('/v1/teams/:teamId/memberships/:inviteId') ->desc('Delete Team Membership') diff --git a/app/controllers/api/users.php b/app/controllers/api/users.php index d83efeaca..cf8405304 100644 --- a/app/controllers/api/users.php +++ b/app/controllers/api/users.php @@ -9,10 +9,10 @@ use Utopia\Validator\Text; use Utopia\Validator\Range; use Utopia\Audit\Audit; use Utopia\Audit\Adapters\MySQL as AuditAdapter; -use Utopia\Config\Config; use Appwrite\Auth\Auth; use Appwrite\Auth\Validator\Password; use Appwrite\Database\Database; +use Appwrite\Database\Document; use Appwrite\Database\Exception\Duplicate; use Appwrite\Database\Validator\UID; use Appwrite\Utopia\Response; @@ -21,6 +21,7 @@ use DeviceDetector\DeviceDetector; App::post('/v1/users') ->desc('Create User') ->groups(['api', 'users']) + ->label('event', 'users.create') ->label('scope', 'users.write') ->label('sdk.platform', [APP_PLATFORM_SERVER]) ->label('sdk.namespace', 'users') @@ -65,27 +66,8 @@ App::post('/v1/users') throw new Exception('Account already exists', 409); } - $oauth2Keys = []; - - foreach (Config::getParam('providers') as $key => $provider) { - if (!$provider['enabled']) { - continue; - } - - $oauth2Keys[] = 'oauth2'.\ucfirst($key); - $oauth2Keys[] = 'oauth2'.\ucfirst($key).'AccessToken'; - } - - $response - ->setStatusCode(Response::STATUS_CODE_CREATED) - ->json(\array_merge($user->getArrayCopy(\array_merge([ - '$id', - 'status', - 'email', - 'registration', - 'emailVerification', - 'name', - ], $oauth2Keys)), ['roles' => []])); + $response->setStatusCode(Response::STATUS_CODE_CREATED); + $response->dynamic($user, Response::MODEL_USER); }, ['response', 'projectDB']); App::get('/v1/users') @@ -116,32 +98,10 @@ App::get('/v1/users') ], ]); - $oauth2Keys = []; - - foreach (Config::getParam('providers') as $key => $provider) { - if (!$provider['enabled']) { - continue; - } - - $oauth2Keys[] = 'oauth2'.\ucfirst($key); - $oauth2Keys[] = 'oauth2'.\ucfirst($key).'AccessToken'; - } - - $results = \array_map(function ($value) use ($oauth2Keys) { /* @var $value \Database\Document */ - return $value->getArrayCopy(\array_merge( - [ - '$id', - 'status', - 'email', - 'registration', - 'emailVerification', - 'name', - ], - $oauth2Keys - )); - }, $results); - - $response->json(['sum' => $projectDB->getSum(), 'users' => $results]); + $response->dynamic(new Document([ + 'sum' => $projectDB->getSum(), + 'users' => $results + ]), Response::MODEL_USER_LIST); }, ['response', 'projectDB']); App::get('/v1/users/:userId') @@ -163,28 +123,7 @@ App::get('/v1/users/:userId') throw new Exception('User not found', 404); } - $oauth2Keys = []; - - foreach (Config::getParam('providers') as $key => $provider) { - if (!$provider['enabled']) { - continue; - } - - $oauth2Keys[] = 'oauth2'.\ucfirst($key); - $oauth2Keys[] = 'oauth2'.\ucfirst($key).'AccessToken'; - } - - $response->json(\array_merge($user->getArrayCopy(\array_merge( - [ - '$id', - 'status', - 'email', - 'registration', - 'emailVerification', - 'name', - ], - $oauth2Keys - )), ['roles' => []])); + $response->dynamic($user, Response::MODEL_USER); }, ['response', 'projectDB']); App::get('/v1/users/:userId/prefs') @@ -208,13 +147,6 @@ App::get('/v1/users/:userId/prefs') $prefs = $user->getAttribute('prefs', ''); - try { - $prefs = \json_decode($prefs, true); - $prefs = ($prefs) ? $prefs : []; - } catch (\Exception $error) { - throw new Exception('Failed to parse prefs', 500); - } - $response->json($prefs); }, ['response', 'projectDB']); @@ -227,11 +159,10 @@ App::get('/v1/users/:userId/sessions') ->label('sdk.method', 'getSessions') ->label('sdk.description', '/docs/references/users/get-user-sessions.md') ->param('userId', '', new UID(), 'User unique ID.') - ->action(function ($userId, $response, $projectDB, $locale, $geodb) { + ->action(function ($userId, $response, $projectDB, $locale) { /** @var Appwrite\Utopia\Response $response */ /** @var Appwrite\Database\Database $projectDB */ /** @var Utopia\Locale\Locale $locale */ - /** @var MaxMind\Db\Reader $geodb */ $user = $projectDB->getDocument($userId); @@ -241,7 +172,6 @@ App::get('/v1/users/:userId/sessions') $tokens = $user->getAttribute('tokens', []); $sessions = []; - $index = 0; $countries = $locale->getText('countries'); foreach ($tokens as $token) { /* @var $token Document */ @@ -249,46 +179,19 @@ App::get('/v1/users/:userId/sessions') continue; } - $userAgent = (!empty($token->getAttribute('userAgent'))) ? $token->getAttribute('userAgent') : 'UNKNOWN'; + $token->setAttribute('countryName', (isset($countries[$token->getAttribute('contryCode')])) + ? $countries[$token->getAttribute('contryCode')] + : $locale->getText('locale.country.unknown')); + $token->setAttribute('current', false); - $dd = new DeviceDetector($userAgent); - - // OPTIONAL: If called, bot detection will completely be skipped (bots will be detected as regular devices then) - // $dd->skipBotDetection(); - - $dd->parse(); - - $sessions[$index] = [ - '$id' => $token->getId(), - 'OS' => $dd->getOs(), - 'client' => $dd->getClient(), - 'device' => $dd->getDevice(), - 'brand' => $dd->getBrand(), - 'model' => $dd->getModel(), - 'ip' => $token->getAttribute('ip', ''), - 'geo' => [], - ]; - - try { - $record = $geodb->get($token->getAttribute('ip', '')); - - if ($record) { - $sessions[$index]['geo']['isoCode'] = \strtolower($record['country']['iso_code']); - $sessions[$index]['geo']['country'] = (isset($countries[$record['country']['iso_code']])) ? $countries[$record['country']['iso_code']] : $locale->getText('locale.country.unknown'); - } else { - $sessions[$index]['geo']['isoCode'] = '--'; - $sessions[$index]['geo']['country'] = $locale->getText('locale.country.unknown'); - } - } catch (\Exception $e) { - $sessions[$index]['geo']['isoCode'] = '--'; - $sessions[$index]['geo']['country'] = $locale->getText('locale.country.unknown'); - } - - ++$index; + $sessions[] = $token; } - $response->json($sessions); - }, ['response', 'projectDB', 'locale', 'geodb']); + $response->dynamic(new Document([ + 'sum' => count($sessions), + 'sessions' => $sessions + ]), Response::MODEL_SESSION_LIST); + }, ['response', 'projectDB', 'locale']); App::get('/v1/users/:userId/logs') ->desc('Get User Logs') @@ -349,40 +252,56 @@ App::get('/v1/users/:userId/logs') $dd->parse(); - $output[$i] = [ + $os = $dd->getOs(); + $osCode = (isset($os['short_name'])) ? $os['short_name'] : ''; + $osName = (isset($os['name'])) ? $os['name'] : ''; + $osVersion = (isset($os['version'])) ? $os['version'] : ''; + + $client = $dd->getClient(); + $clientType = (isset($client['type'])) ? $client['type'] : ''; + $clientCode = (isset($client['short_name'])) ? $client['short_name'] : ''; + $clientName = (isset($client['name'])) ? $client['name'] : ''; + $clientVersion = (isset($client['version'])) ? $client['version'] : ''; + $clientEngine = (isset($client['engine'])) ? $client['engine'] : ''; + $clientEngineVersion = (isset($client['engine_version'])) ? $client['engine_version'] : ''; + + $output[$i] = new Document([ 'event' => $log['event'], 'ip' => $log['ip'], 'time' => \strtotime($log['time']), - 'OS' => $dd->getOs(), - 'client' => $dd->getClient(), - 'device' => $dd->getDevice(), - 'brand' => $dd->getBrand(), - 'model' => $dd->getModel(), - 'geo' => [], - ]; - try { - $record = $geodb->get($log['ip']); + 'osCode' => $osCode, + 'osName' => $osName, + 'osVersion' => $osVersion, + 'clientType' => $clientType, + 'clientCode' => $clientCode, + 'clientName' => $clientName, + 'clientVersion' => $clientVersion, + 'clientEngine' => $clientEngine, + 'clientEngineVersion' => $clientEngineVersion, + 'deviceName' => $dd->getDeviceName(), + 'deviceBrand' => $dd->getBrandName(), + 'deviceModel' => $dd->getModel(), + ]); - if ($record) { - $output[$i]['geo']['isoCode'] = \strtolower($record['country']['iso_code']); - $output[$i]['geo']['country'] = (isset($countries[$record['country']['iso_code']])) ? $countries[$record['country']['iso_code']] : $locale->getText('locale.country.unknown'); - } else { - $output[$i]['geo']['isoCode'] = '--'; - $output[$i]['geo']['country'] = $locale->getText('locale.country.unknown'); - } - } catch (\Exception $e) { - $output[$i]['geo']['isoCode'] = '--'; - $output[$i]['geo']['country'] = $locale->getText('locale.country.unknown'); + $record = $geodb->get($log['ip']); + + if ($record) { + $output[$i]['countryCode'] = (isset($countries[$record['country']['iso_code']])) ? \strtolower($record['country']['iso_code']) : '--'; + $output[$i]['countryName'] = (isset($countries[$record['country']['iso_code']])) ? $countries[$record['country']['iso_code']] : $locale->getText('locale.country.unknown'); + } else { + $output[$i]['countryCode'] = '--'; + $output[$i]['countryName'] = $locale->getText('locale.country.unknown'); } } - $response->json($output); + $response->dynamic(new Document(['logs' => $output]), Response::MODEL_LOG_LIST); }, ['response', 'register', 'project', 'projectDB', 'locale', 'geodb']); App::patch('/v1/users/:userId/status') ->desc('Update User Status') ->groups(['api', 'users']) + ->label('event', 'users.update.status') ->label('scope', 'users.write') ->label('sdk.platform', [APP_PLATFORM_SERVER]) ->label('sdk.namespace', 'users') @@ -407,27 +326,8 @@ App::patch('/v1/users/:userId/status') if (false === $user) { throw new Exception('Failed saving user to DB', 500); } - - $oauth2Keys = []; - foreach (Config::getParam('providers') as $key => $provider) { - if (!$provider['enabled']) { - continue; - } - - $oauth2Keys[] = 'oauth2'.\ucfirst($key); - $oauth2Keys[] = 'oauth2'.\ucfirst($key).'AccessToken'; - } - - $response - ->json(\array_merge($user->getArrayCopy(\array_merge([ - '$id', - 'status', - 'email', - 'registration', - 'emailVerification', - 'name', - ], $oauth2Keys)), ['roles' => []])); + $response->dynamic($user, Response::MODEL_USER); }, ['response', 'projectDB']); App::patch('/v1/users/:userId/prefs') @@ -450,32 +350,21 @@ App::patch('/v1/users/:userId/prefs') throw new Exception('User not found', 404); } - $old = \json_decode($user->getAttribute('prefs', '{}'), true); - $old = ($old) ? $old : []; - $user = $projectDB->updateDocument(\array_merge($user->getArrayCopy(), [ - 'prefs' => \json_encode(\array_merge($old, $prefs)), + 'prefs' => $prefs, ])); if (false === $user) { throw new Exception('Failed saving user to DB', 500); } - $prefs = $user->getAttribute('prefs', ''); - - try { - $prefs = \json_decode($prefs, true); - $prefs = ($prefs) ? $prefs : []; - } catch (\Exception $error) { - throw new Exception('Failed to parse prefs', 500); - } - $response->json($prefs); }, ['response', 'projectDB']); App::delete('/v1/users/:userId/sessions/:sessionId') ->desc('Delete User Session') ->groups(['api', 'users']) + ->label('event', 'users.sessions.delete') ->label('scope', 'users.write') ->label('sdk.platform', [APP_PLATFORM_SERVER]) ->label('sdk.namespace', 'users') @@ -484,9 +373,10 @@ App::delete('/v1/users/:userId/sessions/:sessionId') ->label('abuse-limit', 100) ->param('userId', '', new UID(), 'User unique ID.') ->param('sessionId', null, new UID(), 'User unique session ID.') - ->action(function ($userId, $sessionId, $response, $projectDB) { + ->action(function ($userId, $sessionId, $response, $projectDB, $webhooks) { /** @var Appwrite\Utopia\Response $response */ /** @var Appwrite\Database\Database $projectDB */ + /** @var Appwrite\Event\Event $webhooks */ $user = $projectDB->getDocument($userId); @@ -501,15 +391,20 @@ App::delete('/v1/users/:userId/sessions/:sessionId') if (!$projectDB->deleteDocument($token->getId())) { throw new Exception('Failed to remove token from DB', 500); } + + $webhooks + ->setParam('payload', $response->output($user, Response::MODEL_USER)) + ; } } - $response->json(array('result' => 'success')); - }, ['response', 'projectDB']); + $response->noContent(); + }, ['response', 'projectDB', 'webhooks']); App::delete('/v1/users/:userId/sessions') ->desc('Delete User Sessions') ->groups(['api', 'users']) + ->label('event', 'users.sessions.delete') ->label('scope', 'users.write') ->label('sdk.platform', [APP_PLATFORM_SERVER]) ->label('sdk.namespace', 'users') @@ -517,9 +412,10 @@ App::delete('/v1/users/:userId/sessions') ->label('sdk.description', '/docs/references/users/delete-user-sessions.md') ->label('abuse-limit', 100) ->param('userId', '', new UID(), 'User unique ID.') - ->action(function ($userId, $response, $projectDB) { + ->action(function ($userId, $response, $projectDB, $webhooks) { /** @var Appwrite\Utopia\Response $response */ /** @var Appwrite\Database\Database $projectDB */ + /** @var Appwrite\Event\Event $webhooks */ $user = $projectDB->getDocument($userId); @@ -535,12 +431,17 @@ App::delete('/v1/users/:userId/sessions') } } - $response->json(array('result' => 'success')); - }, ['response', 'projectDB']); + $webhooks + ->setParam('payload', $response->output($user, Response::MODEL_USER)) + ; + + $response->noContent(); + }, ['response', 'projectDB', 'webhooks']); App::delete('/v1/users/:userId') ->desc('Delete User') ->groups(['api', 'users']) + ->label('event', 'users.delete') ->label('scope', 'users.write') ->label('sdk.platform', [APP_PLATFORM_SERVER]) ->label('sdk.namespace', 'users') @@ -548,9 +449,10 @@ App::delete('/v1/users/:userId') ->label('sdk.description', '/docs/references/users/delete-user.md') ->label('abuse-limit', 100) ->param('userId', '', function () {return new UID();}, 'User unique ID.') - ->action(function ($userId, $response, $projectDB, $deletes) { + ->action(function ($userId, $response, $projectDB, $webhooks, $deletes) { /** @var Appwrite\Utopia\Response $response */ /** @var Appwrite\Database\Database $projectDB */ + /** @var Appwrite\Event\Event $webhooks */ /** @var Appwrite\Event\Event $deletes */ $user = $projectDB->getDocument($userId); @@ -578,7 +480,13 @@ App::delete('/v1/users/:userId') throw new Exception('Failed saving reserved id to DB', 500); } - $deletes->setParam('document', $user); + $deletes + ->setParam('document', $user) + ; + + $webhooks + ->setParam('payload', $response->output($user, Response::MODEL_USER)) + ; $response->noContent(); - }, ['response', 'projectDB', 'deletes']); + }, ['response', 'projectDB', 'webhooks', 'deletes']); diff --git a/app/controllers/general.php b/app/controllers/general.php index 94926a936..c92181620 100644 --- a/app/controllers/general.php +++ b/app/controllers/general.php @@ -22,7 +22,7 @@ Config::setParam('domainVerification', false); Config::setParam('cookieDomain', 'localhost'); Config::setParam('cookieSamesite', Response::COOKIE_SAMESITE_NONE); -App::init(function ($utopia, $request, $response, $console, $project, $user, $locale, $webhooks, $audits, $usage, $clients) { +App::init(function ($utopia, $request, $response, $console, $project, $user, $locale, $webhooks, $audits, $usage, $deletes, $clients) { /** @var Utopia\Swoole\Request $request */ /** @var Appwrite\Utopia\Response $response */ /** @var Appwrite\Database\Document $console */ @@ -32,6 +32,7 @@ App::init(function ($utopia, $request, $response, $console, $project, $user, $lo /** @var Appwrite\Event\Event $webhooks */ /** @var Appwrite\Event\Event $audits */ /** @var Appwrite\Event\Event $usage */ + /** @var Appwrite\Event\Event $deletes */ /** @var bool $mode */ /** @var array $clients */ @@ -187,7 +188,7 @@ App::init(function ($utopia, $request, $response, $console, $project, $user, $lo if ($user->getId()) { Authorization::setRole('user:'.$user->getId()); } - + Authorization::setRole('role:'.$role); \array_map(function ($node) { @@ -223,7 +224,7 @@ App::init(function ($utopia, $request, $response, $console, $project, $user, $lo */ $webhooks ->setParam('projectId', $project->getId()) - ->setParam('event', $route->getLabel('webhook', '')) + ->setParam('event', $route->getLabel('event', '')) ->setParam('payload', []) ; @@ -246,7 +247,11 @@ App::init(function ($utopia, $request, $response, $console, $project, $user, $lo ->setParam('networkResponseSize', 0) ->setParam('storage', 0) ; -}, ['utopia', 'request', 'response', 'console', 'project', 'user', 'locale', 'webhooks', 'audits', 'usage', 'clients']); + + $deletes + ->setParam('projectId', $project->getId()) + ; +}, ['utopia', 'request', 'response', 'console', 'project', 'user', 'locale', 'webhooks', 'audits', 'usage', 'deletes', 'clients']); App::shutdown(function ($utopia, $request, $response, $project, $webhooks, $audits, $usage, $deletes, $mode) { /** @var Utopia\App $utopia */ @@ -260,6 +265,10 @@ App::shutdown(function ($utopia, $request, $response, $project, $webhooks, $audi /** @var bool $mode */ if (!empty($webhooks->getParam('event'))) { + if(empty($webhooks->getParam('payload'))) { + $webhooks->setParam('payload', $response->getPayload()); + } + $webhooks->trigger(); } diff --git a/app/views/console/account/index.phtml b/app/views/console/account/index.phtml index 1606b41bd..84a18521d 100644 --- a/app/views/console/account/index.phtml +++ b/app/views/console/account/index.phtml @@ -192,7 +192,7 @@ data-name="sessions" data-event="load,account.deleteRemoteSession"> - diff --git a/app/views/console/home/index.phtml b/app/views/console/home/index.phtml index afc44428f..4243e2767 100644 --- a/app/views/console/home/index.phtml +++ b/app/views/console/home/index.phtml @@ -13,69 +13,105 @@ $graph = $this->getParam('graph', false);     -