From d2b054205af4f5c7a1449e4faf44226a6fe00056 Mon Sep 17 00:00:00 2001 From: Michael Vlach Date: Fri, 31 Jan 2025 21:18:03 +0100 Subject: [PATCH 1/2] admin user logout --- agdb_api/rust/src/api.rs | 8 ++++ agdb_server/src/api.rs | 1 + agdb_server/src/app.rs | 4 ++ agdb_server/src/routes/admin/user.rs | 19 ++++++++ agdb_server/src/server_db.rs | 24 +++++++++- .../tests/routes/admin_user_logout_all.rs | 44 +++++++++++++++++++ agdb_server/tests/routes/mod.rs | 1 + 7 files changed, 99 insertions(+), 2 deletions(-) create mode 100644 agdb_server/tests/routes/admin_user_logout_all.rs diff --git a/agdb_api/rust/src/api.rs b/agdb_api/rust/src/api.rs index fdec6a1c..39bda5b9 100644 --- a/agdb_api/rust/src/api.rs +++ b/agdb_api/rust/src/api.rs @@ -338,6 +338,14 @@ impl AgdbApi { .0) } + pub async fn admin_user_logout_all(&self) -> AgdbApiResult { + Ok(self + .client + .post::<(), ()>(&self.url("/admin/user/logout_all"), &None, &self.token) + .await? + .0) + } + pub async fn admin_user_delete(&self, username: &str) -> AgdbApiResult { self.client .delete( diff --git a/agdb_server/src/api.rs b/agdb_server/src/api.rs index d29c742e..e8da8324 100644 --- a/agdb_server/src/api.rs +++ b/agdb_server/src/api.rs @@ -33,6 +33,7 @@ use utoipa::OpenApi; routes::admin::user::add, routes::admin::user::list, routes::admin::user::logout, + routes::admin::user::logout_all, routes::admin::user::delete, routes::admin::shutdown, routes::admin::status, diff --git a/agdb_server/src/app.rs b/agdb_server/src/app.rs index 4a9ce335..c0acb2f7 100644 --- a/agdb_server/src/app.rs +++ b/agdb_server/src/app.rs @@ -42,6 +42,10 @@ pub(crate) fn app( .route("/admin/shutdown", routing::post(routes::admin::shutdown)) .route("/admin/status", routing::get(routes::admin::status)) .route("/admin/user/list", routing::get(routes::admin::user::list)) + .route( + "/admin/user/logout_all", + routing::post(routes::admin::user::logout_all), + ) .route( "/admin/user/{username}/logout", routing::post(routes::admin::user::logout), diff --git a/agdb_server/src/routes/admin/user.rs b/agdb_server/src/routes/admin/user.rs index 392a3394..a13775aa 100644 --- a/agdb_server/src/routes/admin/user.rs +++ b/agdb_server/src/routes/admin/user.rs @@ -144,6 +144,25 @@ pub(crate) async fn logout( Ok(StatusCode::CREATED) } +#[utoipa::path(post, + path = "/api/v1/admin/user/logout_all", + operation_id = "admin_user_logout_all", + tag = "agdb", + security(("Token" = [])), + responses( + (status = 201, description = "users logged out"), + (status = 401, description = "admin only"), + ) +)] +pub(crate) async fn logout_all( + _admin: AdminId, + State(server_db): State, +) -> ServerResponse { + server_db.reset_tokens().await?; + + Ok(StatusCode::CREATED) +} + #[utoipa::path(delete, path = "/api/v1/admin/user/{username}/delete", operation_id = "admin_user_delete", diff --git a/agdb_server/src/server_db.rs b/agdb_server/src/server_db.rs index a7799033..4f3be3bb 100644 --- a/agdb_server/src/server_db.rs +++ b/agdb_server/src/server_db.rs @@ -62,6 +62,7 @@ const COMMITTED: &str = "committed"; const DB: &str = "db"; const DBS: &str = "dbs"; const EXECUTED: &str = "executed"; +const NAME: &str = "name"; const OWNER: &str = "owner"; const ROLE: &str = "role"; const TOKEN: &str = "token"; @@ -141,13 +142,13 @@ impl ServerDb { let dbs: Vec<(DbId, String, String)> = t .exec( QueryBuilder::select() - .values("name") + .values(NAME) .search() .from(DBS) .where_() .distance(CountComparison::Equal(2)) .and() - .keys("name") + .keys(NAME) .query(), )? .elements @@ -622,6 +623,25 @@ impl ServerDb { Ok(dbs) } + pub(crate) async fn reset_tokens(&self) -> ServerResult<()> { + self.0.write().await.exec_mut( + QueryBuilder::insert() + .values_uniform([(TOKEN, String::new()).into()]) + .search() + .from(USERS) + .where_() + .distance(CountComparison::Equal(2)) + .and() + .key(TOKEN) + .value(Comparison::NotEqual(String::new().into())) + .and() + .not() + .ids(ADMIN) + .query(), + )?; + Ok(()) + } + pub(crate) async fn save_db(&self, db: &Database) -> ServerResult<()> { self.0 .write() diff --git a/agdb_server/tests/routes/admin_user_logout_all.rs b/agdb_server/tests/routes/admin_user_logout_all.rs new file mode 100644 index 00000000..52170639 --- /dev/null +++ b/agdb_server/tests/routes/admin_user_logout_all.rs @@ -0,0 +1,44 @@ +use crate::next_user_name; +use crate::TestServer; +use crate::ADMIN; + +#[tokio::test] +async fn logout_all() -> anyhow::Result<()> { + let mut server = TestServer::new().await?; + let user = &next_user_name(); + let user2 = &next_user_name(); + server.api.user_login(ADMIN, ADMIN).await?; + server.api.admin_user_add(user, user).await?; + server.api.admin_user_add(user2, user2).await?; + server.api.user_login(user, user).await?; + server.api.user_login(user2, user2).await?; + server.api.user_login(ADMIN, ADMIN).await?; + server.api.admin_user_logout_all().await?; + + let list = server.api.admin_user_list().await?.1; + assert_eq!(list.iter().filter(|u| !u.admin && u.login).count(), 0); + + Ok(()) +} + +#[tokio::test] +async fn non_admin() -> anyhow::Result<()> { + let mut server = TestServer::new().await?; + let user = &next_user_name(); + server.api.user_login(ADMIN, ADMIN).await?; + server.api.admin_user_add(user, user).await?; + server.api.user_login(user, user).await?; + let status = server.api.admin_user_logout_all().await.unwrap_err().status; + assert_eq!(status, 401); + + Ok(()) +} + +#[tokio::test] +async fn no_token() -> anyhow::Result<()> { + let server = TestServer::new().await?; + let status = server.api.admin_user_logout_all().await.unwrap_err().status; + assert_eq!(status, 401); + + Ok(()) +} diff --git a/agdb_server/tests/routes/mod.rs b/agdb_server/tests/routes/mod.rs index 4988e53a..d24fb804 100644 --- a/agdb_server/tests/routes/mod.rs +++ b/agdb_server/tests/routes/mod.rs @@ -18,6 +18,7 @@ mod admin_user_add_test; mod admin_user_change_password_test; mod admin_user_delete_test; mod admin_user_list_test; +mod admin_user_logout_all; mod admin_user_logout_test; mod cluster_test; mod db_add_test; From 95548085701d18cd09de5505f0d30415947fbd5b Mon Sep 17 00:00:00 2001 From: Michael Vlach Date: Fri, 31 Jan 2025 22:47:30 +0100 Subject: [PATCH 2/2] update apis --- agdb_api/php/README.md | 2 + agdb_api/php/docs/Api/AgdbApi.md | 110 +++++ agdb_api/php/lib/Api/AgdbApi.php | 392 ++++++++++++++++++ agdb_api/rust/src/api.rs | 12 + agdb_api/typescript/src/openapi.d.ts | 52 +++ agdb_server/openapi.json | 42 ++ agdb_server/src/action.rs | 10 + agdb_server/src/action/cluster_logout.rs | 19 + agdb_server/src/api.rs | 1 + agdb_server/src/app.rs | 4 + agdb_server/src/routes/cluster.rs | 23 + agdb_server/tests/routes/cluster_test.rs | 34 ++ .../pages/en-US/docs/references/server.mdx | 2 + 13 files changed, 703 insertions(+) create mode 100644 agdb_server/src/action/cluster_logout.rs diff --git a/agdb_api/php/README.md b/agdb_api/php/README.md index f4f341e6..2c5b921c 100644 --- a/agdb_api/php/README.md +++ b/agdb_api/php/README.md @@ -101,7 +101,9 @@ Class | Method | HTTP request | Description *AgdbApi* | [**adminUserDelete**](docs/Api/AgdbApi.md#adminuserdelete) | **DELETE** /api/v1/admin/user/{username}/delete | *AgdbApi* | [**adminUserList**](docs/Api/AgdbApi.md#adminuserlist) | **GET** /api/v1/admin/user/list | *AgdbApi* | [**adminUserLogout**](docs/Api/AgdbApi.md#adminuserlogout) | **POST** /api/v1/admin/user/{username}/logout | +*AgdbApi* | [**adminUserLogoutAll**](docs/Api/AgdbApi.md#adminuserlogoutall) | **POST** /api/v1/admin/user/logout_all | *AgdbApi* | [**clusterAdminUserLogout**](docs/Api/AgdbApi.md#clusteradminuserlogout) | **POST** /api/v1/cluster/admin/user/{username}/logout | +*AgdbApi* | [**clusterAdminUserLogoutAll**](docs/Api/AgdbApi.md#clusteradminuserlogoutall) | **POST** /api/v1/cluster/admin/user/logout_all | *AgdbApi* | [**clusterStatus**](docs/Api/AgdbApi.md#clusterstatus) | **GET** /api/v1/cluster/status | *AgdbApi* | [**clusterUserLogin**](docs/Api/AgdbApi.md#clusteruserlogin) | **POST** /api/v1/cluster/user/login | *AgdbApi* | [**clusterUserLogout**](docs/Api/AgdbApi.md#clusteruserlogout) | **POST** /api/v1/cluster/user/logout | diff --git a/agdb_api/php/docs/Api/AgdbApi.md b/agdb_api/php/docs/Api/AgdbApi.md index 1beb98c4..4ac75a9e 100644 --- a/agdb_api/php/docs/Api/AgdbApi.md +++ b/agdb_api/php/docs/Api/AgdbApi.md @@ -28,7 +28,9 @@ All URIs are relative to http://localhost:3000, except if the operation defines | [**adminUserDelete()**](AgdbApi.md#adminUserDelete) | **DELETE** /api/v1/admin/user/{username}/delete | | | [**adminUserList()**](AgdbApi.md#adminUserList) | **GET** /api/v1/admin/user/list | | | [**adminUserLogout()**](AgdbApi.md#adminUserLogout) | **POST** /api/v1/admin/user/{username}/logout | | +| [**adminUserLogoutAll()**](AgdbApi.md#adminUserLogoutAll) | **POST** /api/v1/admin/user/logout_all | | | [**clusterAdminUserLogout()**](AgdbApi.md#clusterAdminUserLogout) | **POST** /api/v1/cluster/admin/user/{username}/logout | | +| [**clusterAdminUserLogoutAll()**](AgdbApi.md#clusterAdminUserLogoutAll) | **POST** /api/v1/cluster/admin/user/logout_all | | | [**clusterStatus()**](AgdbApi.md#clusterStatus) | **GET** /api/v1/cluster/status | | | [**clusterUserLogin()**](AgdbApi.md#clusterUserLogin) | **POST** /api/v1/cluster/user/login | | | [**clusterUserLogout()**](AgdbApi.md#clusterUserLogout) | **POST** /api/v1/cluster/user/logout | | @@ -1482,6 +1484,60 @@ void (empty response body) [[Back to Model list]](../../README.md#models) [[Back to README]](../../README.md) +## `adminUserLogoutAll()` + +```php +adminUserLogoutAll() +``` + + + +### Example + +```php +setAccessToken('YOUR_ACCESS_TOKEN'); + + +$apiInstance = new Agnesoft\AgdbApi\Api\AgdbApi( + // If you want use custom http client, pass your client which implements `GuzzleHttp\ClientInterface`. + // This is optional, `GuzzleHttp\Client` will be used as default. + new GuzzleHttp\Client(), + $config +); + +try { + $apiInstance->adminUserLogoutAll(); +} catch (Exception $e) { + echo 'Exception when calling AgdbApi->adminUserLogoutAll: ', $e->getMessage(), PHP_EOL; +} +``` + +### Parameters + +This endpoint does not need any parameter. + +### Return type + +void (empty response body) + +### Authorization + +[Token](../../README.md#Token) + +### HTTP request headers + +- **Content-Type**: Not defined +- **Accept**: Not defined + +[[Back to top]](#) [[Back to API list]](../../README.md#endpoints) +[[Back to Model list]](../../README.md#models) +[[Back to README]](../../README.md) + ## `clusterAdminUserLogout()` ```php @@ -1539,6 +1595,60 @@ void (empty response body) [[Back to Model list]](../../README.md#models) [[Back to README]](../../README.md) +## `clusterAdminUserLogoutAll()` + +```php +clusterAdminUserLogoutAll() +``` + + + +### Example + +```php +setAccessToken('YOUR_ACCESS_TOKEN'); + + +$apiInstance = new Agnesoft\AgdbApi\Api\AgdbApi( + // If you want use custom http client, pass your client which implements `GuzzleHttp\ClientInterface`. + // This is optional, `GuzzleHttp\Client` will be used as default. + new GuzzleHttp\Client(), + $config +); + +try { + $apiInstance->clusterAdminUserLogoutAll(); +} catch (Exception $e) { + echo 'Exception when calling AgdbApi->clusterAdminUserLogoutAll: ', $e->getMessage(), PHP_EOL; +} +``` + +### Parameters + +This endpoint does not need any parameter. + +### Return type + +void (empty response body) + +### Authorization + +[Token](../../README.md#Token) + +### HTTP request headers + +- **Content-Type**: Not defined +- **Accept**: Not defined + +[[Back to top]](#) [[Back to API list]](../../README.md#endpoints) +[[Back to Model list]](../../README.md#models) +[[Back to README]](../../README.md) + ## `clusterStatus()` ```php diff --git a/agdb_api/php/lib/Api/AgdbApi.php b/agdb_api/php/lib/Api/AgdbApi.php index bd48d137..eebbd5ce 100644 --- a/agdb_api/php/lib/Api/AgdbApi.php +++ b/agdb_api/php/lib/Api/AgdbApi.php @@ -143,9 +143,15 @@ class AgdbApi 'adminUserLogout' => [ 'application/json', ], + 'adminUserLogoutAll' => [ + 'application/json', + ], 'clusterAdminUserLogout' => [ 'application/json', ], + 'clusterAdminUserLogoutAll' => [ + 'application/json', + ], 'clusterStatus' => [ 'application/json', ], @@ -6758,6 +6764,199 @@ public function adminUserLogoutRequest($username, string $contentType = self::co } + $headers = $this->headerSelector->selectHeaders( + [], + $contentType, + $multipart + ); + + // for model (json/xml) + if (count($formParams) > 0) { + if ($multipart) { + $multipartContents = []; + foreach ($formParams as $formParamName => $formParamValue) { + $formParamValueItems = is_array($formParamValue) ? $formParamValue : [$formParamValue]; + foreach ($formParamValueItems as $formParamValueItem) { + $multipartContents[] = [ + 'name' => $formParamName, + 'contents' => $formParamValueItem + ]; + } + } + // for HTTP post (form) + $httpBody = new MultipartStream($multipartContents); + + } elseif (stripos($headers['Content-Type'], 'application/json') !== false) { + # if Content-Type contains "application/json", json_encode the form parameters + $httpBody = \GuzzleHttp\Utils::jsonEncode($formParams); + } else { + // for HTTP post (form) + $httpBody = ObjectSerializer::buildQuery($formParams); + } + } + + // this endpoint requires Bearer authentication (access token) + if (!empty($this->config->getAccessToken())) { + $headers['Authorization'] = 'Bearer ' . $this->config->getAccessToken(); + } + + $defaultHeaders = []; + if ($this->config->getUserAgent()) { + $defaultHeaders['User-Agent'] = $this->config->getUserAgent(); + } + + $headers = array_merge( + $defaultHeaders, + $headerParams, + $headers + ); + + $operationHost = $this->config->getHost(); + $query = ObjectSerializer::buildQuery($queryParams); + return new Request( + 'POST', + $operationHost . $resourcePath . ($query ? "?{$query}" : ''), + $headers, + $httpBody + ); + } + + /** + * Operation adminUserLogoutAll + * + * @param string $contentType The value for the Content-Type header. Check self::contentTypes['adminUserLogoutAll'] to see the possible values for this operation + * + * @throws \Agnesoft\AgdbApi\ApiException on non-2xx response or if the response body is not in the expected format + * @throws \InvalidArgumentException + * @return void + */ + public function adminUserLogoutAll(string $contentType = self::contentTypes['adminUserLogoutAll'][0]) + { + $this->adminUserLogoutAllWithHttpInfo($contentType); + } + + /** + * Operation adminUserLogoutAllWithHttpInfo + * + * @param string $contentType The value for the Content-Type header. Check self::contentTypes['adminUserLogoutAll'] to see the possible values for this operation + * + * @throws \Agnesoft\AgdbApi\ApiException on non-2xx response or if the response body is not in the expected format + * @throws \InvalidArgumentException + * @return array of null, HTTP status code, HTTP response headers (array of strings) + */ + public function adminUserLogoutAllWithHttpInfo(string $contentType = self::contentTypes['adminUserLogoutAll'][0]) + { + $request = $this->adminUserLogoutAllRequest($contentType); + + try { + $options = $this->createHttpClientOption(); + try { + $response = $this->client->send($request, $options); + } catch (RequestException $e) { + throw new ApiException( + "[{$e->getCode()}] {$e->getMessage()}", + (int) $e->getCode(), + $e->getResponse() ? $e->getResponse()->getHeaders() : null, + $e->getResponse() ? (string) $e->getResponse()->getBody() : null + ); + } catch (ConnectException $e) { + throw new ApiException( + "[{$e->getCode()}] {$e->getMessage()}", + (int) $e->getCode(), + null, + null + ); + } + + $statusCode = $response->getStatusCode(); + + + return [null, $statusCode, $response->getHeaders()]; + + } catch (ApiException $e) { + switch ($e->getCode()) { + } + throw $e; + } + } + + /** + * Operation adminUserLogoutAllAsync + * + * @param string $contentType The value for the Content-Type header. Check self::contentTypes['adminUserLogoutAll'] to see the possible values for this operation + * + * @throws \InvalidArgumentException + * @return \GuzzleHttp\Promise\PromiseInterface + */ + public function adminUserLogoutAllAsync(string $contentType = self::contentTypes['adminUserLogoutAll'][0]) + { + return $this->adminUserLogoutAllAsyncWithHttpInfo($contentType) + ->then( + function ($response) { + return $response[0]; + } + ); + } + + /** + * Operation adminUserLogoutAllAsyncWithHttpInfo + * + * @param string $contentType The value for the Content-Type header. Check self::contentTypes['adminUserLogoutAll'] to see the possible values for this operation + * + * @throws \InvalidArgumentException + * @return \GuzzleHttp\Promise\PromiseInterface + */ + public function adminUserLogoutAllAsyncWithHttpInfo(string $contentType = self::contentTypes['adminUserLogoutAll'][0]) + { + $returnType = ''; + $request = $this->adminUserLogoutAllRequest($contentType); + + return $this->client + ->sendAsync($request, $this->createHttpClientOption()) + ->then( + function ($response) use ($returnType) { + return [null, $response->getStatusCode(), $response->getHeaders()]; + }, + function ($exception) { + $response = $exception->getResponse(); + $statusCode = $response->getStatusCode(); + throw new ApiException( + sprintf( + '[%d] Error connecting to the API (%s)', + $statusCode, + $exception->getRequest()->getUri() + ), + $statusCode, + $response->getHeaders(), + (string) $response->getBody() + ); + } + ); + } + + /** + * Create request for operation 'adminUserLogoutAll' + * + * @param string $contentType The value for the Content-Type header. Check self::contentTypes['adminUserLogoutAll'] to see the possible values for this operation + * + * @throws \InvalidArgumentException + * @return \GuzzleHttp\Psr7\Request + */ + public function adminUserLogoutAllRequest(string $contentType = self::contentTypes['adminUserLogoutAll'][0]) + { + + + $resourcePath = '/api/v1/admin/user/logout_all'; + $formParams = []; + $queryParams = []; + $headerParams = []; + $httpBody = ''; + $multipart = false; + + + + + $headers = $this->headerSelector->selectHeaders( [], $contentType, @@ -6971,6 +7170,199 @@ public function clusterAdminUserLogoutRequest($username, string $contentType = s } + $headers = $this->headerSelector->selectHeaders( + [], + $contentType, + $multipart + ); + + // for model (json/xml) + if (count($formParams) > 0) { + if ($multipart) { + $multipartContents = []; + foreach ($formParams as $formParamName => $formParamValue) { + $formParamValueItems = is_array($formParamValue) ? $formParamValue : [$formParamValue]; + foreach ($formParamValueItems as $formParamValueItem) { + $multipartContents[] = [ + 'name' => $formParamName, + 'contents' => $formParamValueItem + ]; + } + } + // for HTTP post (form) + $httpBody = new MultipartStream($multipartContents); + + } elseif (stripos($headers['Content-Type'], 'application/json') !== false) { + # if Content-Type contains "application/json", json_encode the form parameters + $httpBody = \GuzzleHttp\Utils::jsonEncode($formParams); + } else { + // for HTTP post (form) + $httpBody = ObjectSerializer::buildQuery($formParams); + } + } + + // this endpoint requires Bearer authentication (access token) + if (!empty($this->config->getAccessToken())) { + $headers['Authorization'] = 'Bearer ' . $this->config->getAccessToken(); + } + + $defaultHeaders = []; + if ($this->config->getUserAgent()) { + $defaultHeaders['User-Agent'] = $this->config->getUserAgent(); + } + + $headers = array_merge( + $defaultHeaders, + $headerParams, + $headers + ); + + $operationHost = $this->config->getHost(); + $query = ObjectSerializer::buildQuery($queryParams); + return new Request( + 'POST', + $operationHost . $resourcePath . ($query ? "?{$query}" : ''), + $headers, + $httpBody + ); + } + + /** + * Operation clusterAdminUserLogoutAll + * + * @param string $contentType The value for the Content-Type header. Check self::contentTypes['clusterAdminUserLogoutAll'] to see the possible values for this operation + * + * @throws \Agnesoft\AgdbApi\ApiException on non-2xx response or if the response body is not in the expected format + * @throws \InvalidArgumentException + * @return void + */ + public function clusterAdminUserLogoutAll(string $contentType = self::contentTypes['clusterAdminUserLogoutAll'][0]) + { + $this->clusterAdminUserLogoutAllWithHttpInfo($contentType); + } + + /** + * Operation clusterAdminUserLogoutAllWithHttpInfo + * + * @param string $contentType The value for the Content-Type header. Check self::contentTypes['clusterAdminUserLogoutAll'] to see the possible values for this operation + * + * @throws \Agnesoft\AgdbApi\ApiException on non-2xx response or if the response body is not in the expected format + * @throws \InvalidArgumentException + * @return array of null, HTTP status code, HTTP response headers (array of strings) + */ + public function clusterAdminUserLogoutAllWithHttpInfo(string $contentType = self::contentTypes['clusterAdminUserLogoutAll'][0]) + { + $request = $this->clusterAdminUserLogoutAllRequest($contentType); + + try { + $options = $this->createHttpClientOption(); + try { + $response = $this->client->send($request, $options); + } catch (RequestException $e) { + throw new ApiException( + "[{$e->getCode()}] {$e->getMessage()}", + (int) $e->getCode(), + $e->getResponse() ? $e->getResponse()->getHeaders() : null, + $e->getResponse() ? (string) $e->getResponse()->getBody() : null + ); + } catch (ConnectException $e) { + throw new ApiException( + "[{$e->getCode()}] {$e->getMessage()}", + (int) $e->getCode(), + null, + null + ); + } + + $statusCode = $response->getStatusCode(); + + + return [null, $statusCode, $response->getHeaders()]; + + } catch (ApiException $e) { + switch ($e->getCode()) { + } + throw $e; + } + } + + /** + * Operation clusterAdminUserLogoutAllAsync + * + * @param string $contentType The value for the Content-Type header. Check self::contentTypes['clusterAdminUserLogoutAll'] to see the possible values for this operation + * + * @throws \InvalidArgumentException + * @return \GuzzleHttp\Promise\PromiseInterface + */ + public function clusterAdminUserLogoutAllAsync(string $contentType = self::contentTypes['clusterAdminUserLogoutAll'][0]) + { + return $this->clusterAdminUserLogoutAllAsyncWithHttpInfo($contentType) + ->then( + function ($response) { + return $response[0]; + } + ); + } + + /** + * Operation clusterAdminUserLogoutAllAsyncWithHttpInfo + * + * @param string $contentType The value for the Content-Type header. Check self::contentTypes['clusterAdminUserLogoutAll'] to see the possible values for this operation + * + * @throws \InvalidArgumentException + * @return \GuzzleHttp\Promise\PromiseInterface + */ + public function clusterAdminUserLogoutAllAsyncWithHttpInfo(string $contentType = self::contentTypes['clusterAdminUserLogoutAll'][0]) + { + $returnType = ''; + $request = $this->clusterAdminUserLogoutAllRequest($contentType); + + return $this->client + ->sendAsync($request, $this->createHttpClientOption()) + ->then( + function ($response) use ($returnType) { + return [null, $response->getStatusCode(), $response->getHeaders()]; + }, + function ($exception) { + $response = $exception->getResponse(); + $statusCode = $response->getStatusCode(); + throw new ApiException( + sprintf( + '[%d] Error connecting to the API (%s)', + $statusCode, + $exception->getRequest()->getUri() + ), + $statusCode, + $response->getHeaders(), + (string) $response->getBody() + ); + } + ); + } + + /** + * Create request for operation 'clusterAdminUserLogoutAll' + * + * @param string $contentType The value for the Content-Type header. Check self::contentTypes['clusterAdminUserLogoutAll'] to see the possible values for this operation + * + * @throws \InvalidArgumentException + * @return \GuzzleHttp\Psr7\Request + */ + public function clusterAdminUserLogoutAllRequest(string $contentType = self::contentTypes['clusterAdminUserLogoutAll'][0]) + { + + + $resourcePath = '/api/v1/cluster/admin/user/logout_all'; + $formParams = []; + $queryParams = []; + $headerParams = []; + $httpBody = ''; + $multipart = false; + + + + + $headers = $this->headerSelector->selectHeaders( [], $contentType, diff --git a/agdb_api/rust/src/api.rs b/agdb_api/rust/src/api.rs index 39bda5b9..8df2eaf0 100644 --- a/agdb_api/rust/src/api.rs +++ b/agdb_api/rust/src/api.rs @@ -379,6 +379,18 @@ impl AgdbApi { .0) } + pub async fn cluster_admin_user_logout_all(&self) -> AgdbApiResult { + Ok(self + .client + .post::<(), ()>( + &self.url("/cluster/admin/user/logout_all"), + &None, + &self.token, + ) + .await? + .0) + } + pub async fn cluster_user_login( &mut self, username: &str, diff --git a/agdb_api/typescript/src/openapi.d.ts b/agdb_api/typescript/src/openapi.d.ts index 9b13f0c0..d6894741 100644 --- a/agdb_api/typescript/src/openapi.d.ts +++ b/agdb_api/typescript/src/openapi.d.ts @@ -1850,6 +1850,14 @@ declare namespace Paths { } } } + namespace AdminUserLogoutAll { + namespace Responses { + export interface $201 { + } + export interface $401 { + } + } + } namespace ClusterAdminUserLogout { namespace Parameters { export type Username = string; @@ -1866,6 +1874,14 @@ declare namespace Paths { } } } + namespace ClusterAdminUserLogoutAll { + namespace Responses { + export interface $201 { + } + export interface $401 { + } + } + } namespace ClusterStatus { namespace Responses { export type $200 = Components.Schemas.ClusterStatus[]; @@ -2445,6 +2461,14 @@ export interface OperationMethods { data?: any, config?: AxiosRequestConfig ): OperationResponse + /** + * admin_user_logout_all + */ + 'admin_user_logout_all'( + parameters?: Parameters | null, + data?: any, + config?: AxiosRequestConfig + ): OperationResponse /** * admin_user_add */ @@ -2477,6 +2501,14 @@ export interface OperationMethods { data?: any, config?: AxiosRequestConfig ): OperationResponse + /** + * cluster_admin_user_logout_all + */ + 'cluster_admin_user_logout_all'( + parameters?: Parameters | null, + data?: any, + config?: AxiosRequestConfig + ): OperationResponse /** * cluster_admin_user_logout */ @@ -2888,6 +2920,16 @@ export interface PathsDictionary { config?: AxiosRequestConfig ): OperationResponse } + ['/api/v1/admin/user/logout_all']: { + /** + * admin_user_logout_all + */ + 'post'( + parameters?: Parameters | null, + data?: any, + config?: AxiosRequestConfig + ): OperationResponse + } ['/api/v1/admin/user/{username}/add']: { /** * admin_user_add @@ -2928,6 +2970,16 @@ export interface PathsDictionary { config?: AxiosRequestConfig ): OperationResponse } + ['/api/v1/cluster/admin/user/logout_all']: { + /** + * cluster_admin_user_logout_all + */ + 'post'( + parameters?: Parameters | null, + data?: any, + config?: AxiosRequestConfig + ): OperationResponse + } ['/api/v1/cluster/admin/user/{username}/logout']: { /** * cluster_admin_user_logout diff --git a/agdb_server/openapi.json b/agdb_server/openapi.json index 8f0d99be..13449d6f 100644 --- a/agdb_server/openapi.json +++ b/agdb_server/openapi.json @@ -1002,6 +1002,27 @@ ] } }, + "/api/v1/admin/user/logout_all": { + "post": { + "tags": [ + "agdb" + ], + "operationId": "admin_user_logout_all", + "responses": { + "201": { + "description": "users logged out" + }, + "401": { + "description": "admin only" + } + }, + "security": [ + { + "Token": [] + } + ] + } + }, "/api/v1/admin/user/{username}/add": { "post": { "tags": [ @@ -1178,6 +1199,27 @@ ] } }, + "/api/v1/cluster/admin/user/logout_all": { + "post": { + "tags": [ + "agdb" + ], + "operationId": "cluster_admin_user_logout_all", + "responses": { + "201": { + "description": "users logged out" + }, + "401": { + "description": "admin only" + } + }, + "security": [ + { + "Token": [] + } + ] + } + }, "/api/v1/cluster/admin/user/{username}/logout": { "post": { "tags": [ diff --git a/agdb_server/src/action.rs b/agdb_server/src/action.rs index 15356d53..f1c1bf3d 100644 --- a/agdb_server/src/action.rs +++ b/agdb_server/src/action.rs @@ -1,5 +1,6 @@ pub(crate) mod change_password; pub(crate) mod cluster_login; +pub(crate) mod cluster_logout; pub(crate) mod db_add; pub(crate) mod db_backup; pub(crate) mod db_clear; @@ -18,6 +19,7 @@ pub(crate) mod user_delete; use crate::action::change_password::ChangePassword; use crate::action::cluster_login::ClusterLogin; +use crate::action::cluster_logout::ClusterLogout; use crate::action::db_add::DbAdd; use crate::action::db_backup::DbBackup; use crate::action::db_clear::DbClear; @@ -45,6 +47,7 @@ use serde::Serialize; pub(crate) enum ClusterAction { UserAdd(UserAdd), ClusterLogin(ClusterLogin), + ClusterLogout(ClusterLogout), ChangePassword(ChangePassword), UserDelete(UserDelete), DbAdd(DbAdd), @@ -80,6 +83,7 @@ impl ClusterAction { match self { ClusterAction::UserAdd(action) => action.exec(db, db_pool).await, ClusterAction::ClusterLogin(action) => action.exec(db, db_pool).await, + ClusterAction::ClusterLogout(action) => action.exec(db, db_pool).await, ClusterAction::ChangePassword(action) => action.exec(db, db_pool).await, ClusterAction::UserDelete(action) => action.exec(db, db_pool).await, ClusterAction::DbAdd(action) => action.exec(db, db_pool).await, @@ -111,6 +115,12 @@ impl From for ClusterAction { } } +impl From for ClusterAction { + fn from(value: ClusterLogout) -> Self { + ClusterAction::ClusterLogout(value) + } +} + impl From for ClusterAction { fn from(value: ChangePassword) -> Self { ClusterAction::ChangePassword(value) diff --git a/agdb_server/src/action/cluster_logout.rs b/agdb_server/src/action/cluster_logout.rs new file mode 100644 index 00000000..d0c842b8 --- /dev/null +++ b/agdb_server/src/action/cluster_logout.rs @@ -0,0 +1,19 @@ +use super::DbPool; +use super::ServerDb; +use crate::action::Action; +use crate::action::ClusterActionResult; +use crate::server_error::ServerResult; +use agdb::AgdbDeSerialize; +use serde::Deserialize; +use serde::Serialize; + +#[derive(Clone, Serialize, Deserialize, AgdbDeSerialize)] +pub(crate) struct ClusterLogout {} + +impl Action for ClusterLogout { + async fn exec(self, db: ServerDb, _db_pool: DbPool) -> ServerResult { + db.reset_tokens().await?; + + Ok(ClusterActionResult::None) + } +} diff --git a/agdb_server/src/api.rs b/agdb_server/src/api.rs index e8da8324..7b173fd0 100644 --- a/agdb_server/src/api.rs +++ b/agdb_server/src/api.rs @@ -62,6 +62,7 @@ use utoipa::OpenApi; routes::cluster::logout, routes::cluster::status, routes::cluster::admin_logout, + routes::cluster::admin_logout_all, ), components(schemas( routes::db::DbTypeParam, diff --git a/agdb_server/src/app.rs b/agdb_server/src/app.rs index c0acb2f7..6f6fb0c1 100644 --- a/agdb_server/src/app.rs +++ b/agdb_server/src/app.rs @@ -181,6 +181,10 @@ pub(crate) fn app( "/cluster/admin/user/{username}/logout", routing::post(routes::cluster::admin_logout), ) + .route( + "/cluster/admin/user/logout_all", + routing::post(routes::cluster::admin_logout_all), + ) .route("/cluster/status", routing::get(routes::cluster::status)) .route("/user/login", routing::post(routes::user::login)) .route("/user/logout", routing::post(routes::user::logout)) diff --git a/agdb_server/src/routes/cluster.rs b/agdb_server/src/routes/cluster.rs index e50a490f..2e2c4766 100644 --- a/agdb_server/src/routes/cluster.rs +++ b/agdb_server/src/routes/cluster.rs @@ -1,4 +1,5 @@ use crate::action::cluster_login::ClusterLogin; +use crate::action::cluster_logout::ClusterLogout; use crate::action::ClusterAction; use crate::cluster; use crate::cluster::Cluster; @@ -64,6 +65,28 @@ pub(crate) async fn admin_logout( )) } +#[utoipa::path(post, + path = "/api/v1/cluster/admin/user/logout_all", + operation_id = "cluster_admin_user_logout_all", + tag = "agdb", + security(("Token" = [])), + responses( + (status = 201, description = "users logged out"), + (status = 401, description = "admin only"), + ) +)] +pub(crate) async fn admin_logout_all( + _admin: AdminId, + State(cluster): State, +) -> ServerResponse { + let (commit_index, _result) = cluster.exec(ClusterLogout {}).await?; + + Ok(( + StatusCode::CREATED, + [("commit-index", commit_index.to_string())], + )) +} + #[utoipa::path(post, path = "/api/v1/cluster/user/login", operation_id = "cluster_user_login", diff --git a/agdb_server/tests/routes/cluster_test.rs b/agdb_server/tests/routes/cluster_test.rs index d3a700a7..a0601c3c 100644 --- a/agdb_server/tests/routes/cluster_test.rs +++ b/agdb_server/tests/routes/cluster_test.rs @@ -396,6 +396,40 @@ async fn admin_cluster_logout() -> anyhow::Result<()> { Ok(()) } +#[tokio::test] +async fn admin_cluster_logout_all() -> anyhow::Result<()> { + let servers = create_cluster(3, false).await?; + let mut client = AgdbApi::new( + ReqwestClient::with_client(reqwest_client()), + &servers[1].address, + ); + + let user = &next_user_name(); + let user2 = &next_user_name(); + + client.cluster_user_login(ADMIN, ADMIN).await?; + client.admin_user_add(user, user).await?; + client.admin_user_add(user2, user2).await?; + client.cluster_user_login(user, user).await?; + client.cluster_user_login(user2, user2).await?; + + client.user_login(ADMIN, ADMIN).await?; + client.cluster_admin_user_logout_all().await?; + + let list = client.admin_user_list().await?.1; + assert_eq!(list.iter().filter(|u| !u.admin && u.login).count(), 0); + + let mut client = AgdbApi::new( + ReqwestClient::with_client(reqwest_client()), + &servers[0].address, + ); + client.user_login(ADMIN, ADMIN).await?; + let list = client.admin_user_list().await?.1; + assert_eq!(list.iter().filter(|u| !u.admin && u.login).count(), 0); + + Ok(()) +} + #[tokio::test] async fn admin_user_delete() -> anyhow::Result<()> { let mut cluster = TestCluster::new().await?; diff --git a/agdb_web/pages/en-US/docs/references/server.mdx b/agdb_web/pages/en-US/docs/references/server.mdx index 7195b521..f971f18d 100644 --- a/agdb_web/pages/en-US/docs/references/server.mdx +++ b/agdb_web/pages/en-US/docs/references/server.mdx @@ -186,9 +186,11 @@ Each `agdb_server` has exactly one admin account (`admin` by default) that acts | /api/v1/admin/user/\{username\}/add | adds new user to the server | | /api/v1/admin/user/\{username\}/change_password | changes password of a user | | /api/v1/admin/user/\{username\}/logout | force logout of any user | +| /api/v1/admin/user/logout_all | force logout of all users except admins | | /api/v1/admin/user/\{username\}/delete | deletes user and all their data (databases) from the server | | /api/v1/admin/user/list | lists the all users on the server | | /api/v1/cluster/admin/user/\{username\}/logout | force logout of any user from all nodes in the cluster | +| /api/v1/cluster/admin/user/logout_all | force logout of all user from all nodes in the cluster except admins | ## Shutdown