diff --git a/.sqlx/query-5d45897a5d4c326c8d17f105cae81db7f5878c197b55e8ea61c225e739d11234.json b/.sqlx/query-5d45897a5d4c326c8d17f105cae81db7f5878c197b55e8ea61c225e739d11234.json new file mode 100644 index 0000000000..f3258c6b64 --- /dev/null +++ b/.sqlx/query-5d45897a5d4c326c8d17f105cae81db7f5878c197b55e8ea61c225e739d11234.json @@ -0,0 +1,146 @@ +{ + "db_name": "MySQL", + "query": "\nSELECT\n uir.id,\n uir.impersonator_user_token as `impersonator_user_token: UserToken`,\n impersonator.username as impersonator_username,\n impersonator.display_name as impersonator_display_name,\n uir.impersonated_user_token as `impersonated_user_token: UserToken`,\n impersonated.username as impersonated_username,\n impersonated.display_name as impersonated_display_name,\n uir.is_redeemed as `is_redeemed: bool`,\n (uir.expires_at < NOW()) as `is_expired: bool`,\n uir.expires_at,\n uir.created_at,\n uir.updated_at\nFROM user_impersonation_requests uir\nJOIN users impersonator ON impersonator.token = uir.impersonator_user_token\nJOIN users impersonated ON impersonated.token = uir.impersonated_user_token\nWHERE uir.id < ?\nORDER BY uir.id DESC\nLIMIT ?\n ", + "describe": { + "columns": [ + { + "ordinal": 0, + "name": "id", + "type_info": { + "type": "LongLong", + "flags": "NOT_NULL | UNSIGNED | AUTO_INCREMENT | BINARY", + "char_set": 63, + "max_size": 20 + } + }, + { + "ordinal": 1, + "name": "impersonator_user_token: UserToken", + "type_info": { + "type": "VarString", + "flags": "NOT_NULL | MULTIPLE_KEY | BINARY | NO_DEFAULT_VALUE", + "char_set": 224, + "max_size": 128 + } + }, + { + "ordinal": 2, + "name": "impersonator_username", + "type_info": { + "type": "VarString", + "flags": "NOT_NULL | UNIQUE_KEY | BINARY | NO_DEFAULT_VALUE", + "char_set": 224, + "max_size": 80 + } + }, + { + "ordinal": 3, + "name": "impersonator_display_name", + "type_info": { + "type": "VarString", + "flags": "NOT_NULL | BINARY | NO_DEFAULT_VALUE", + "char_set": 224, + "max_size": 80 + } + }, + { + "ordinal": 4, + "name": "impersonated_user_token: UserToken", + "type_info": { + "type": "VarString", + "flags": "NOT_NULL | MULTIPLE_KEY | BINARY | NO_DEFAULT_VALUE", + "char_set": 224, + "max_size": 128 + } + }, + { + "ordinal": 5, + "name": "impersonated_username", + "type_info": { + "type": "VarString", + "flags": "NOT_NULL | UNIQUE_KEY | BINARY | NO_DEFAULT_VALUE", + "char_set": 224, + "max_size": 80 + } + }, + { + "ordinal": 6, + "name": "impersonated_display_name", + "type_info": { + "type": "VarString", + "flags": "NOT_NULL | BINARY | NO_DEFAULT_VALUE", + "char_set": 224, + "max_size": 80 + } + }, + { + "ordinal": 7, + "name": "is_redeemed: bool", + "type_info": { + "type": "Tiny", + "flags": "NOT_NULL | BINARY", + "char_set": 63, + "max_size": 1 + } + }, + { + "ordinal": 8, + "name": "is_expired: bool", + "type_info": { + "type": "LongLong", + "flags": "NOT_NULL | BINARY", + "char_set": 63, + "max_size": 1 + } + }, + { + "ordinal": 9, + "name": "expires_at", + "type_info": { + "type": "Timestamp", + "flags": "NOT_NULL | BINARY | TIMESTAMP | NO_DEFAULT_VALUE", + "char_set": 63, + "max_size": 19 + } + }, + { + "ordinal": 10, + "name": "created_at", + "type_info": { + "type": "Timestamp", + "flags": "NOT_NULL | BINARY | TIMESTAMP", + "char_set": 63, + "max_size": 19 + } + }, + { + "ordinal": 11, + "name": "updated_at", + "type_info": { + "type": "Timestamp", + "flags": "NOT_NULL | BINARY | TIMESTAMP", + "char_set": 63, + "max_size": 19 + } + } + ], + "parameters": { + "Right": 2 + }, + "nullable": [ + false, + false, + false, + false, + false, + false, + false, + false, + false, + false, + false, + false + ] + }, + "hash": "5d45897a5d4c326c8d17f105cae81db7f5878c197b55e8ea61c225e739d11234" +} \ No newline at end of file diff --git a/.sqlx/query-e72b3dfdd9df4ee10a696faa4dfc3fb8addc9d2b88017f503c339fb616bef6f1.json b/.sqlx/query-e72b3dfdd9df4ee10a696faa4dfc3fb8addc9d2b88017f503c339fb616bef6f1.json new file mode 100644 index 0000000000..2d417e7dd8 --- /dev/null +++ b/.sqlx/query-e72b3dfdd9df4ee10a696faa4dfc3fb8addc9d2b88017f503c339fb616bef6f1.json @@ -0,0 +1,146 @@ +{ + "db_name": "MySQL", + "query": "\nSELECT\n uir.id,\n uir.impersonator_user_token as `impersonator_user_token: UserToken`,\n impersonator.username as impersonator_username,\n impersonator.display_name as impersonator_display_name,\n uir.impersonated_user_token as `impersonated_user_token: UserToken`,\n impersonated.username as impersonated_username,\n impersonated.display_name as impersonated_display_name,\n uir.is_redeemed as `is_redeemed: bool`,\n (uir.expires_at < NOW()) as `is_expired: bool`,\n uir.expires_at,\n uir.created_at,\n uir.updated_at\nFROM user_impersonation_requests uir\nJOIN users impersonator ON impersonator.token = uir.impersonator_user_token\nJOIN users impersonated ON impersonated.token = uir.impersonated_user_token\nORDER BY uir.id DESC\nLIMIT ?\n ", + "describe": { + "columns": [ + { + "ordinal": 0, + "name": "id", + "type_info": { + "type": "LongLong", + "flags": "NOT_NULL | UNSIGNED | AUTO_INCREMENT | BINARY", + "char_set": 63, + "max_size": 20 + } + }, + { + "ordinal": 1, + "name": "impersonator_user_token: UserToken", + "type_info": { + "type": "VarString", + "flags": "NOT_NULL | MULTIPLE_KEY | BINARY | NO_DEFAULT_VALUE", + "char_set": 224, + "max_size": 128 + } + }, + { + "ordinal": 2, + "name": "impersonator_username", + "type_info": { + "type": "VarString", + "flags": "NOT_NULL | UNIQUE_KEY | BINARY | NO_DEFAULT_VALUE", + "char_set": 224, + "max_size": 80 + } + }, + { + "ordinal": 3, + "name": "impersonator_display_name", + "type_info": { + "type": "VarString", + "flags": "NOT_NULL | BINARY | NO_DEFAULT_VALUE", + "char_set": 224, + "max_size": 80 + } + }, + { + "ordinal": 4, + "name": "impersonated_user_token: UserToken", + "type_info": { + "type": "VarString", + "flags": "NOT_NULL | MULTIPLE_KEY | BINARY | NO_DEFAULT_VALUE", + "char_set": 224, + "max_size": 128 + } + }, + { + "ordinal": 5, + "name": "impersonated_username", + "type_info": { + "type": "VarString", + "flags": "NOT_NULL | UNIQUE_KEY | BINARY | NO_DEFAULT_VALUE", + "char_set": 224, + "max_size": 80 + } + }, + { + "ordinal": 6, + "name": "impersonated_display_name", + "type_info": { + "type": "VarString", + "flags": "NOT_NULL | BINARY | NO_DEFAULT_VALUE", + "char_set": 224, + "max_size": 80 + } + }, + { + "ordinal": 7, + "name": "is_redeemed: bool", + "type_info": { + "type": "Tiny", + "flags": "NOT_NULL | BINARY", + "char_set": 63, + "max_size": 1 + } + }, + { + "ordinal": 8, + "name": "is_expired: bool", + "type_info": { + "type": "LongLong", + "flags": "NOT_NULL | BINARY", + "char_set": 63, + "max_size": 1 + } + }, + { + "ordinal": 9, + "name": "expires_at", + "type_info": { + "type": "Timestamp", + "flags": "NOT_NULL | BINARY | TIMESTAMP | NO_DEFAULT_VALUE", + "char_set": 63, + "max_size": 19 + } + }, + { + "ordinal": 10, + "name": "created_at", + "type_info": { + "type": "Timestamp", + "flags": "NOT_NULL | BINARY | TIMESTAMP", + "char_set": 63, + "max_size": 19 + } + }, + { + "ordinal": 11, + "name": "updated_at", + "type_info": { + "type": "Timestamp", + "flags": "NOT_NULL | BINARY | TIMESTAMP", + "char_set": 63, + "max_size": 19 + } + } + ], + "parameters": { + "Right": 1 + }, + "nullable": [ + false, + false, + false, + false, + false, + false, + false, + false, + false, + false, + false, + false + ] + }, + "hash": "e72b3dfdd9df4ee10a696faa4dfc3fb8addc9d2b88017f503c339fb616bef6f1" +} \ No newline at end of file diff --git a/crates/schema/database/mysql_queries/src/queries/user_impersonation_requests/list_user_impersonation_requests.rs b/crates/schema/database/mysql_queries/src/queries/user_impersonation_requests/list_user_impersonation_requests.rs new file mode 100644 index 0000000000..2a5d4cab62 --- /dev/null +++ b/crates/schema/database/mysql_queries/src/queries/user_impersonation_requests/list_user_impersonation_requests.rs @@ -0,0 +1,95 @@ +use chrono::{DateTime, Utc}; +use sqlx::MySqlPool; + +use tokens::tokens::users::UserToken; + +pub struct UserImpersonationRequestListItem { + pub id: u64, + pub impersonator_user_token: UserToken, + pub impersonator_username: String, + pub impersonator_display_name: String, + pub impersonated_user_token: UserToken, + pub impersonated_username: String, + pub impersonated_display_name: String, + pub is_redeemed: bool, + pub is_expired: bool, + pub expires_at: DateTime, + pub created_at: DateTime, + pub updated_at: DateTime, +} + +pub struct ListUserImpersonationRequestsArgs<'a> { + pub maybe_cursor_id: Option, + pub limit: u32, + pub mysql_pool: &'a MySqlPool, +} + +pub async fn list_user_impersonation_requests( + args: ListUserImpersonationRequestsArgs<'_>, +) -> Result, sqlx::Error> { + + let limit = args.limit as i64; + + let items = match args.maybe_cursor_id { + Some(cursor_id) => { + sqlx::query_as!( + UserImpersonationRequestListItem, + r#" +SELECT + uir.id, + uir.impersonator_user_token as `impersonator_user_token: UserToken`, + impersonator.username as impersonator_username, + impersonator.display_name as impersonator_display_name, + uir.impersonated_user_token as `impersonated_user_token: UserToken`, + impersonated.username as impersonated_username, + impersonated.display_name as impersonated_display_name, + uir.is_redeemed as `is_redeemed: bool`, + (uir.expires_at < NOW()) as `is_expired: bool`, + uir.expires_at, + uir.created_at, + uir.updated_at +FROM user_impersonation_requests uir +JOIN users impersonator ON impersonator.token = uir.impersonator_user_token +JOIN users impersonated ON impersonated.token = uir.impersonated_user_token +WHERE uir.id < ? +ORDER BY uir.id DESC +LIMIT ? + "#, + cursor_id as u64, + limit, + ) + .fetch_all(args.mysql_pool) + .await? + } + None => { + sqlx::query_as!( + UserImpersonationRequestListItem, + r#" +SELECT + uir.id, + uir.impersonator_user_token as `impersonator_user_token: UserToken`, + impersonator.username as impersonator_username, + impersonator.display_name as impersonator_display_name, + uir.impersonated_user_token as `impersonated_user_token: UserToken`, + impersonated.username as impersonated_username, + impersonated.display_name as impersonated_display_name, + uir.is_redeemed as `is_redeemed: bool`, + (uir.expires_at < NOW()) as `is_expired: bool`, + uir.expires_at, + uir.created_at, + uir.updated_at +FROM user_impersonation_requests uir +JOIN users impersonator ON impersonator.token = uir.impersonator_user_token +JOIN users impersonated ON impersonated.token = uir.impersonated_user_token +ORDER BY uir.id DESC +LIMIT ? + "#, + limit, + ) + .fetch_all(args.mysql_pool) + .await? + } + }; + + Ok(items) +} diff --git a/crates/schema/database/mysql_queries/src/queries/user_impersonation_requests/mod.rs b/crates/schema/database/mysql_queries/src/queries/user_impersonation_requests/mod.rs index e1a7bc1475..f3ee799b8c 100644 --- a/crates/schema/database/mysql_queries/src/queries/user_impersonation_requests/mod.rs +++ b/crates/schema/database/mysql_queries/src/queries/user_impersonation_requests/mod.rs @@ -1,4 +1,5 @@ pub mod insert_user_impersonation_request; +pub mod list_user_impersonation_requests; pub mod list_user_impersonation_requests_for_user; pub mod lookup_user_impersonation_request; pub mod mark_impersonation_token_as_redeemed; diff --git a/crates/service/web/storyteller_web/src/docs/api_doc.rs b/crates/service/web/storyteller_web/src/docs/api_doc.rs index d1c6f2e752..444a6d5304 100644 --- a/crates/service/web/storyteller_web/src/docs/api_doc.rs +++ b/crates/service/web/storyteller_web/src/docs/api_doc.rs @@ -87,6 +87,7 @@ use crate::http_server::endpoints::media_files::upsert_upload::write_scene_file: use crate::http_server::endpoints::model_download::enqueue_gptsovits_model_download_handler::*; use crate::http_server::endpoints::moderation::user_feature_flags::edit_user_feature_flags_handler::*; use crate::http_server::endpoints::moderation::user_sessions::moderator_list_user_session_impersonation_requests_for_user_handler::*; +use crate::http_server::endpoints::moderation::user_sessions::moderator_list_user_session_impersonation_requests_handler::*; use crate::http_server::endpoints::moderation::user_sessions::moderator_user_session_impersonation_request_handler::*; use crate::http_server::endpoints::prompts::get_prompt_handler::*; use artcraft_api_defs::common::responses::job_details::JobDetailsLipsyncRequest; @@ -510,6 +511,7 @@ use crate::http_server::endpoints::media_files::list::list_batch_generated_redux crate::http_server::endpoints::moderation::wallets::moderator_create_wallet_for_user_handler::moderator_create_wallet_for_user_handler, crate::http_server::endpoints::moderation::wallets::moderator_get_wallet_handler::moderator_get_wallet_handler, crate::http_server::endpoints::moderation::user_sessions::moderator_list_user_session_impersonation_requests_for_user_handler::moderator_list_user_session_impersonation_requests_for_user_handler, + crate::http_server::endpoints::moderation::user_sessions::moderator_list_user_session_impersonation_requests_handler::moderator_list_user_session_impersonation_requests_handler, crate::http_server::endpoints::moderation::user_sessions::moderator_user_session_impersonation_request_handler::moderator_user_session_impersonation_request_handler, // Credits crate::http_server::endpoints::credits::get_session_credits_handler::get_session_credits_handler, @@ -596,6 +598,7 @@ use crate::http_server::endpoints::media_files::list::list_batch_generated_redux WeightsDefaultCoverInfo, // Endpoint API types + AllImpersonationRequestResponse, AppStateError, AppStateLegacyPermissionFlags, AppStatePermissions, @@ -889,6 +892,8 @@ use crate::http_server::endpoints::media_files::list::list_batch_generated_redux ListFeaturedWeightsError, ListFeaturedWeightsQueryParams, ListFeaturedWeightsSuccessResponse, + ListAllImpersonationRequestsQueryParams, + ListAllImpersonationRequestsSuccessResponse, ListImpersonationRequestsPathInfo, ListImpersonationRequestsQueryParams, ListUserImpersonationRequestsSuccessResponse, diff --git a/crates/service/web/storyteller_web/src/http_server/endpoints/moderation/user_sessions/mod.rs b/crates/service/web/storyteller_web/src/http_server/endpoints/moderation/user_sessions/mod.rs index 9fa718c173..09a65ac982 100644 --- a/crates/service/web/storyteller_web/src/http_server/endpoints/moderation/user_sessions/mod.rs +++ b/crates/service/web/storyteller_web/src/http_server/endpoints/moderation/user_sessions/mod.rs @@ -1,2 +1,3 @@ pub mod moderator_list_user_session_impersonation_requests_for_user_handler; +pub mod moderator_list_user_session_impersonation_requests_handler; pub mod moderator_user_session_impersonation_request_handler; diff --git a/crates/service/web/storyteller_web/src/http_server/endpoints/moderation/user_sessions/moderator_list_user_session_impersonation_requests_for_user_handler.rs b/crates/service/web/storyteller_web/src/http_server/endpoints/moderation/user_sessions/moderator_list_user_session_impersonation_requests_for_user_handler.rs index 03316bf24f..0250ed7149 100644 --- a/crates/service/web/storyteller_web/src/http_server/endpoints/moderation/user_sessions/moderator_list_user_session_impersonation_requests_for_user_handler.rs +++ b/crates/service/web/storyteller_web/src/http_server/endpoints/moderation/user_sessions/moderator_list_user_session_impersonation_requests_for_user_handler.rs @@ -64,7 +64,7 @@ pub struct UserImpersonationRequestResponse { #[utoipa::path( get, tag = "Moderation", - path = "/v1/moderation/user_sessions/impersonation_requests/{user_token}", + path = "/v1/moderation/user_sessions/impersonation_requests/user/{user_token}", params( ListImpersonationRequestsQueryParams, ), diff --git a/crates/service/web/storyteller_web/src/http_server/endpoints/moderation/user_sessions/moderator_list_user_session_impersonation_requests_handler.rs b/crates/service/web/storyteller_web/src/http_server/endpoints/moderation/user_sessions/moderator_list_user_session_impersonation_requests_handler.rs new file mode 100644 index 0000000000..687dd9fdf7 --- /dev/null +++ b/crates/service/web/storyteller_web/src/http_server/endpoints/moderation/user_sessions/moderator_list_user_session_impersonation_requests_handler.rs @@ -0,0 +1,145 @@ +use std::sync::Arc; + +use actix_web::web::{Json, Query}; +use actix_web::{web, HttpRequest}; +use chrono::{DateTime, Utc}; +use log::warn; +use utoipa::{IntoParams, ToSchema}; + +use mysql_queries::queries::user_impersonation_requests::list_user_impersonation_requests::{ + list_user_impersonation_requests, ListUserImpersonationRequestsArgs, +}; +use tokens::tokens::users::UserToken; + +use crate::http_server::common_responses::advanced_common_web_error::AdvancedCommonWebError; +use crate::http_server::web_utils::user_session::require_moderator::{ + require_moderator, UseDatabase, +}; +use crate::state::server_state::ServerState; + +const CURSOR_NAME: &str = "modallimp"; +const DEFAULT_LIMIT: u32 = 50; +const MAX_LIMIT: u32 = 1000; + +// --- Request --- + +#[derive(Deserialize, ToSchema, IntoParams)] +pub struct ListAllImpersonationRequestsQueryParams { + pub cursor: Option, + pub limit: Option, +} + +// --- Response --- + +#[derive(Serialize, ToSchema)] +pub struct ListAllImpersonationRequestsSuccessResponse { + pub success: bool, + pub impersonation_requests: Vec, + pub maybe_cursor: Option, +} + +#[derive(Serialize, ToSchema)] +pub struct AllImpersonationRequestResponse { + pub impersonator_user_token: UserToken, + pub impersonator_username: String, + pub impersonator_display_name: String, + pub impersonated_user_token: UserToken, + pub impersonated_username: String, + pub impersonated_display_name: String, + pub is_redeemed: bool, + pub is_expired: bool, + pub expires_at: DateTime, + pub created_at: DateTime, + pub updated_at: DateTime, +} + +// --- Handler --- + +/// List all impersonation requests (not scoped to a user). Moderators only. +#[utoipa::path( + get, + tag = "Moderation", + path = "/v1/moderation/user_sessions/impersonation_requests/list", + params( + ListAllImpersonationRequestsQueryParams, + ), + responses( + (status = 200, description = "Success", body = ListAllImpersonationRequestsSuccessResponse), + (status = 401, description = "Unauthorized"), + (status = 500, description = "Server error"), + ), +)] +pub async fn moderator_list_user_session_impersonation_requests_handler( + http_request: HttpRequest, + query: Query, + server_state: web::Data>, +) -> Result, AdvancedCommonWebError> { + + let _user_session = require_moderator( + &http_request, + &server_state, + UseDatabase::GrabNewConnection, + ).await.map_err(|err| { + warn!("Moderator check failed: {:?}", err); + AdvancedCommonWebError::NotAuthorized + })?; + + let limit = query.limit + .unwrap_or(DEFAULT_LIMIT) + .min(MAX_LIMIT); + + let maybe_cursor_id = match &query.cursor { + None => None, + Some(cursor_str) => { + let decoded = server_state.opaque_cursors + .decode_cursor_expecting_name(CURSOR_NAME, cursor_str) + .map_err(|err| { + warn!("Failed to decode cursor: {:?}", err); + AdvancedCommonWebError::BadInputWithSimpleMessage( + "Invalid cursor".to_string()) + })?; + decoded.last_id + } + }; + + let records = list_user_impersonation_requests( + ListUserImpersonationRequestsArgs { + maybe_cursor_id, + limit, + mysql_pool: &server_state.mysql_pool, + }, + ).await.map_err(|err| { + warn!("Failed to list impersonation requests: {:?}", err); + AdvancedCommonWebError::from_error(err) + })?; + + let maybe_cursor = records.last().map(|last| { + server_state.opaque_cursors + .encode_last_id_cursor(CURSOR_NAME, last.id) + }).transpose().map_err(|err| { + warn!("Failed to encode cursor: {:?}", err); + AdvancedCommonWebError::server_error_with_message("Failed to encode cursor") + })?; + + let impersonation_requests = records.into_iter().map(|r| { + AllImpersonationRequestResponse { + impersonator_user_token: r.impersonator_user_token, + impersonator_username: r.impersonator_username, + impersonator_display_name: r.impersonator_display_name, + impersonated_user_token: r.impersonated_user_token, + impersonated_username: r.impersonated_username, + impersonated_display_name: r.impersonated_display_name, + is_redeemed: r.is_redeemed, + is_expired: r.is_expired, + expires_at: r.expires_at, + created_at: r.created_at, + updated_at: r.updated_at, + } + }).collect(); + + Ok(Json(ListAllImpersonationRequestsSuccessResponse { + success: true, + impersonation_requests, + maybe_cursor, + })) +} diff --git a/crates/service/web/storyteller_web/src/http_server/routes/application_routes/moderation_routes.rs b/crates/service/web/storyteller_web/src/http_server/routes/application_routes/moderation_routes.rs index f657df59af..99a411ed91 100644 --- a/crates/service/web/storyteller_web/src/http_server/routes/application_routes/moderation_routes.rs +++ b/crates/service/web/storyteller_web/src/http_server/routes/application_routes/moderation_routes.rs @@ -37,6 +37,7 @@ use crate::http_server::endpoints::moderation::user::moderator_user_lookup_handl use crate::http_server::endpoints::moderation::alerts::moderation_send_alert_handler::moderation_send_alert_handler; use crate::http_server::endpoints::moderation::user_feature_flags::edit_user_feature_flags_handler::edit_user_feature_flags_handler; use crate::http_server::endpoints::moderation::user_sessions::moderator_list_user_session_impersonation_requests_for_user_handler::moderator_list_user_session_impersonation_requests_for_user_handler; +use crate::http_server::endpoints::moderation::user_sessions::moderator_list_user_session_impersonation_requests_handler::moderator_list_user_session_impersonation_requests_handler; use crate::http_server::endpoints::moderation::user_sessions::moderator_user_session_impersonation_request_handler::moderator_user_session_impersonation_request_handler; pub fn add_moderator_routes (app: App) -> App @@ -110,7 +111,11 @@ pub fn add_moderator_routes (app: App) -> App .route(web::post().to(moderator_user_session_impersonation_request_handler)) .route(web::head().to(|| HttpResponse::Ok())) ) - .service(web::resource("/impersonation_requests/{user_token}") + .service(web::resource("/impersonation_requests/list") + .route(web::get().to(moderator_list_user_session_impersonation_requests_handler)) + .route(web::head().to(|| HttpResponse::Ok())) + ) + .service(web::resource("/impersonation_requests/user/{user_token}") .route(web::get().to(moderator_list_user_session_impersonation_requests_for_user_handler)) .route(web::head().to(|| HttpResponse::Ok())) )