diff --git a/.sqlx/query-be9867966f24f44e14f96966cf5113055f00c339dda75334e7e88d355b9bfe72.json b/.sqlx/query-be9867966f24f44e14f96966cf5113055f00c339dda75334e7e88d355b9bfe72.json new file mode 100644 index 0000000000..3bd3fbd407 --- /dev/null +++ b/.sqlx/query-be9867966f24f44e14f96966cf5113055f00c339dda75334e7e88d355b9bfe72.json @@ -0,0 +1,58 @@ +{ + "db_name": "MySQL", + "query": "\nSELECT\n username,\n display_name,\n username_is_generated as `username_is_generated: bool`,\n username_is_not_customized as `username_is_not_customized: bool`\nFROM users\nWHERE\n token = ?\n AND user_deleted_at IS NULL\n AND mod_deleted_at IS NULL\nLIMIT 1\n ", + "describe": { + "columns": [ + { + "ordinal": 0, + "name": "username", + "type_info": { + "type": "VarString", + "flags": "NOT_NULL | UNIQUE_KEY | BINARY | NO_DEFAULT_VALUE", + "char_set": 224, + "max_size": 80 + } + }, + { + "ordinal": 1, + "name": "display_name", + "type_info": { + "type": "VarString", + "flags": "NOT_NULL | BINARY | NO_DEFAULT_VALUE", + "char_set": 224, + "max_size": 80 + } + }, + { + "ordinal": 2, + "name": "username_is_generated: bool", + "type_info": { + "type": "Tiny", + "flags": "NOT_NULL", + "char_set": 63, + "max_size": 1 + } + }, + { + "ordinal": 3, + "name": "username_is_not_customized: bool", + "type_info": { + "type": "Tiny", + "flags": "NOT_NULL", + "char_set": 63, + "max_size": 1 + } + } + ], + "parameters": { + "Right": 1 + }, + "nullable": [ + false, + false, + false, + false + ] + }, + "hash": "be9867966f24f44e14f96966cf5113055f00c339dda75334e7e88d355b9bfe72" +} diff --git a/.sqlx/query-4f5100a2cfb465964e77eaacb1ae394daae94de20c56a0966a8222dc9bea63c6.json b/.sqlx/query-f4fb945b4311bd4458a68d82e417b40f8c36c08d572c07881b89cd157a8a8699.json similarity index 51% rename from .sqlx/query-4f5100a2cfb465964e77eaacb1ae394daae94de20c56a0966a8222dc9bea63c6.json rename to .sqlx/query-f4fb945b4311bd4458a68d82e417b40f8c36c08d572c07881b89cd157a8a8699.json index 6f109523e8..0a477c60b4 100644 --- a/.sqlx/query-4f5100a2cfb465964e77eaacb1ae394daae94de20c56a0966a8222dc9bea63c6.json +++ b/.sqlx/query-f4fb945b4311bd4458a68d82e417b40f8c36c08d572c07881b89cd157a8a8699.json @@ -1,6 +1,6 @@ { "db_name": "MySQL", - "query": "\nINSERT INTO wallets\nSET\n token = ?,\n wallet_namespace = ?,\n\n owner_user_token = ?,\n \n banked_credits = 0,\n monthly_credits = 0\n ", + "query": "\nINSERT INTO wallets\nSET\n token = ?,\n wallet_namespace = ?,\n\n owner_user_token = ?,\n\n banked_credits = 0,\n monthly_credits = 0\n ", "describe": { "columns": [], "parameters": { @@ -8,5 +8,5 @@ }, "nullable": [] }, - "hash": "4f5100a2cfb465964e77eaacb1ae394daae94de20c56a0966a8222dc9bea63c6" + "hash": "f4fb945b4311bd4458a68d82e417b40f8c36c08d572c07881b89cd157a8a8699" } diff --git a/crates/api_clients/artcraft/artcraft_api_defs/src/moderation/wallets/mod.rs b/crates/api_clients/artcraft/artcraft_api_defs/src/moderation/wallets/mod.rs index 80b7c4b7f9..23e8bc033e 100644 --- a/crates/api_clients/artcraft/artcraft_api_defs/src/moderation/wallets/mod.rs +++ b/crates/api_clients/artcraft/artcraft_api_defs/src/moderation/wallets/mod.rs @@ -1,3 +1,4 @@ pub mod list_user_wallets; pub mod moderator_add_banked_balance_to_wallet; +pub mod moderator_create_wallet_for_user; pub mod moderator_get_wallet; diff --git a/crates/api_clients/artcraft/artcraft_api_defs/src/moderation/wallets/moderator_create_wallet_for_user.rs b/crates/api_clients/artcraft/artcraft_api_defs/src/moderation/wallets/moderator_create_wallet_for_user.rs new file mode 100644 index 0000000000..6661ae5828 --- /dev/null +++ b/crates/api_clients/artcraft/artcraft_api_defs/src/moderation/wallets/moderator_create_wallet_for_user.rs @@ -0,0 +1,19 @@ +use enums::common::payments_namespace::PaymentsNamespace; +use serde_derive::{Deserialize, Serialize}; +use tokens::tokens::users::UserToken; +use tokens::tokens::wallets::WalletToken; +use utoipa::ToSchema; + +pub const MODERATOR_CREATE_WALLET_FOR_USER_PATH: &str = "/v1/moderation/wallet/create_for_user"; + +#[derive(Deserialize, ToSchema)] +pub struct ModeratorCreateWalletForUserRequest { + pub user_token: Option, + pub payments_namespace: Option, +} + +#[derive(Serialize, ToSchema)] +pub struct ModeratorCreateWalletForUserResponse { + pub success: bool, + pub wallet_token: WalletToken, +} diff --git a/crates/schema/database/mysql_queries/src/queries/users/user/get/get_username_by_user_token.rs b/crates/schema/database/mysql_queries/src/queries/users/user/get/get_username_by_user_token.rs new file mode 100644 index 0000000000..c513e306ec --- /dev/null +++ b/crates/schema/database/mysql_queries/src/queries/users/user/get/get_username_by_user_token.rs @@ -0,0 +1,45 @@ +use log::warn; +use sqlx::MySqlPool; + +use crate::errors::select_optional_record_error::SelectOptionalRecordError; +use tokens::tokens::users::UserToken; + +pub struct UsernameDetails { + pub username: String, + pub display_name: String, + pub username_is_generated: bool, + pub username_is_not_customized: bool, +} + +pub async fn get_username_by_user_token( + user_token: &UserToken, + pool: &MySqlPool, +) -> Result, SelectOptionalRecordError> { + let result = sqlx::query_as!( + UsernameDetails, + r#" +SELECT + username, + display_name, + username_is_generated as `username_is_generated: bool`, + username_is_not_customized as `username_is_not_customized: bool` +FROM users +WHERE + token = ? + AND user_deleted_at IS NULL + AND mod_deleted_at IS NULL +LIMIT 1 + "#, + user_token.as_str(), + ) + .fetch_optional(pool) + .await; + + match result { + Ok(maybe_record) => Ok(maybe_record), + Err(err) => { + warn!("get_username_by_user_token query error: {:?}", err); + Err(err.into()) + } + } +} diff --git a/crates/schema/database/mysql_queries/src/queries/users/user/get/mod.rs b/crates/schema/database/mysql_queries/src/queries/users/user/get/mod.rs index 9f0444e3f5..3a4483807b 100644 --- a/crates/schema/database/mysql_queries/src/queries/users/user/get/mod.rs +++ b/crates/schema/database/mysql_queries/src/queries/users/user/get/mod.rs @@ -1,4 +1,5 @@ pub mod get_user_token_by_username; +pub mod get_username_by_user_token; pub mod lookup_user_for_login_by_email; pub mod lookup_user_for_login_by_email_with_transactor; pub mod lookup_user_for_login_by_username; diff --git a/crates/schema/database/mysql_queries/src/queries/wallets/create_new_artcraft_wallet_for_owner_user.rs b/crates/schema/database/mysql_queries/src/queries/wallets/create_new_artcraft_wallet_for_owner_user.rs index dac89c0a3e..5949ea76c4 100644 --- a/crates/schema/database/mysql_queries/src/queries/wallets/create_new_artcraft_wallet_for_owner_user.rs +++ b/crates/schema/database/mysql_queries/src/queries/wallets/create_new_artcraft_wallet_for_owner_user.rs @@ -1,44 +1,12 @@ -use crate::queries::wallet_ledger_entries::internal_insert_wallet_created_ledger_entry::internal_insert_wallet_created_ledger_entry; +use crate::queries::wallets::create_new_wallet_for_owner_user::create_new_wallet_for_owner_user; use enums::common::payments_namespace::PaymentsNamespace; -use errors::AnyhowResult; -use log::error; use sqlx::MySql; use tokens::tokens::users::UserToken; use tokens::tokens::wallets::WalletToken; -const ARTCRAFT_NAMESPACE: &str = PaymentsNamespace::Artcraft.to_str(); - pub async fn create_new_artcraft_wallet_for_owner_user( user_token: &UserToken, transaction: &mut sqlx::Transaction<'_, MySql>, ) -> Result { - let token = WalletToken::generate(); - - let result = sqlx::query!( - r#" -INSERT INTO wallets -SET - token = ?, - wallet_namespace = ?, - - owner_user_token = ?, - - banked_credits = 0, - monthly_credits = 0 - "#, - token.as_str(), - ARTCRAFT_NAMESPACE, - user_token.as_str() - ) - .execute(&mut **transaction) - .await; - - if let Err(err) = result { - error!("Error while inserting wallet: {}", err); - return Err(err); - } - - internal_insert_wallet_created_ledger_entry(&token, transaction).await?; - - Ok(token) + create_new_wallet_for_owner_user(user_token, PaymentsNamespace::Artcraft, transaction).await } diff --git a/crates/schema/database/mysql_queries/src/queries/wallets/create_new_wallet_for_owner_user.rs b/crates/schema/database/mysql_queries/src/queries/wallets/create_new_wallet_for_owner_user.rs new file mode 100644 index 0000000000..a57609d880 --- /dev/null +++ b/crates/schema/database/mysql_queries/src/queries/wallets/create_new_wallet_for_owner_user.rs @@ -0,0 +1,42 @@ +use crate::queries::wallet_ledger_entries::internal_insert_wallet_created_ledger_entry::internal_insert_wallet_created_ledger_entry; +use enums::common::payments_namespace::PaymentsNamespace; +use log::error; +use sqlx::MySql; +use tokens::tokens::users::UserToken; +use tokens::tokens::wallets::WalletToken; + +pub async fn create_new_wallet_for_owner_user( + user_token: &UserToken, + namespace: PaymentsNamespace, + transaction: &mut sqlx::Transaction<'_, MySql>, +) -> Result { + let token = WalletToken::generate(); + + let result = sqlx::query!( + r#" +INSERT INTO wallets +SET + token = ?, + wallet_namespace = ?, + + owner_user_token = ?, + + banked_credits = 0, + monthly_credits = 0 + "#, + token.as_str(), + namespace.to_str(), + user_token.as_str() + ) + .execute(&mut **transaction) + .await; + + if let Err(err) = result { + error!("Error while inserting wallet: {}", err); + return Err(err); + } + + internal_insert_wallet_created_ledger_entry(&token, transaction).await?; + + Ok(token) +} diff --git a/crates/schema/database/mysql_queries/src/queries/wallets/mod.rs b/crates/schema/database/mysql_queries/src/queries/wallets/mod.rs index cd840c66f7..e3d2483bc1 100644 --- a/crates/schema/database/mysql_queries/src/queries/wallets/mod.rs +++ b/crates/schema/database/mysql_queries/src/queries/wallets/mod.rs @@ -1,6 +1,7 @@ pub (crate) mod internal_select_wallet_balance_for_update; pub mod add_durable_banked_balance_to_wallet; pub mod create_new_artcraft_wallet_for_owner_user; +pub mod create_new_wallet_for_owner_user; pub mod find_primary_wallet_for_owner; pub mod find_primary_wallet_token_for_owner; pub mod refill_monthly_credits_balance_on_wallet; diff --git a/crates/service/web/storyteller_web/src/http_server/endpoints/moderation/wallets/mod.rs b/crates/service/web/storyteller_web/src/http_server/endpoints/moderation/wallets/mod.rs index a3e0a56870..a632ac57ea 100644 --- a/crates/service/web/storyteller_web/src/http_server/endpoints/moderation/wallets/mod.rs +++ b/crates/service/web/storyteller_web/src/http_server/endpoints/moderation/wallets/mod.rs @@ -1,3 +1,4 @@ pub mod list_user_wallets_handler; pub mod moderator_add_banked_balance_to_wallet_handler; +pub mod moderator_create_wallet_for_user_handler; pub mod moderator_get_wallet_handler; diff --git a/crates/service/web/storyteller_web/src/http_server/endpoints/moderation/wallets/moderator_create_wallet_for_user_handler.rs b/crates/service/web/storyteller_web/src/http_server/endpoints/moderation/wallets/moderator_create_wallet_for_user_handler.rs new file mode 100644 index 0000000000..a4d6339a0f --- /dev/null +++ b/crates/service/web/storyteller_web/src/http_server/endpoints/moderation/wallets/moderator_create_wallet_for_user_handler.rs @@ -0,0 +1,116 @@ +use std::sync::Arc; + +use actix_web::web::Json; +use actix_web::{web, HttpRequest}; +use enums::common::payments_namespace::PaymentsNamespace; +use log::{error, warn}; +use sqlx::Acquire; + +use artcraft_api_defs::moderation::wallets::moderator_create_wallet_for_user::{ + ModeratorCreateWalletForUserRequest, + ModeratorCreateWalletForUserResponse, +}; +use mysql_queries::queries::users::user::get::get_username_by_user_token::get_username_by_user_token; +use mysql_queries::queries::wallets::create_new_wallet_for_owner_user::create_new_wallet_for_owner_user; +use mysql_queries::queries::wallets::find_primary_wallet_token_for_owner::find_primary_wallet_token_for_owner_using_connection; + +use crate::http_server::common_responses::common_web_error::CommonWebError; +use crate::http_server::web_utils::user_session::require_moderator::{require_moderator, UseDatabase}; +use crate::state::server_state::ServerState; + +/// Create a wallet for a user (moderation) +#[utoipa::path( + post, + tag = "Moderation", + path = "/v1/moderation/wallet/create_for_user", + request_body = ModeratorCreateWalletForUserRequest, + responses( + (status = 200, description = "Success", body = ModeratorCreateWalletForUserResponse), + (status = 400, description = "Bad request"), + (status = 401, description = "Unauthorized"), + (status = 500, description = "Server error"), + ), +)] +pub async fn moderator_create_wallet_for_user_handler( + http_request: HttpRequest, + request: Json, + server_state: web::Data>, +) -> Result, CommonWebError> { + + let _user_session = require_moderator(&http_request, &server_state, UseDatabase::GrabNewConnection) + .await + .map_err(|_| CommonWebError::NotAuthorized)?; + + let user_token = request.user_token.as_ref() + .ok_or_else(|| CommonWebError::BadInputWithSimpleMessage("user_token is required".to_string()))?; + + // Verify user exists + let maybe_user = get_username_by_user_token(user_token, &server_state.mysql_pool) + .await + .map_err(|err| { + warn!("moderator_create_wallet_for_user user lookup error: {:?}", err); + CommonWebError::ServerError + })?; + + if maybe_user.is_none() { + return Err(CommonWebError::BadInputWithSimpleMessage("User not found".to_string())); + } + + let namespace = request.payments_namespace.unwrap_or(PaymentsNamespace::Artcraft); + + // Check for existing wallet + let mut mysql_connection = server_state.mysql_pool + .acquire() + .await + .map_err(|err| { + error!("Error acquiring MySQL connection: {:?}", err); + CommonWebError::ServerError + })?; + + let maybe_wallet_token = find_primary_wallet_token_for_owner_using_connection( + user_token, + namespace, + &mut mysql_connection, + ).await.map_err(|err| { + error!("Error finding wallet for user {:?}: {:?}", user_token, err); + CommonWebError::ServerError + })?; + + if let Some(wallet_token) = maybe_wallet_token { + return Ok(Json(ModeratorCreateWalletForUserResponse { + success: true, + wallet_token, + })); + } + + // Create new wallet + let mut transaction = mysql_connection + .begin() + .await + .map_err(|err| { + error!("Error starting MySQL transaction: {:?}", err); + CommonWebError::ServerError + })?; + + let wallet_token = create_new_wallet_for_owner_user( + user_token, + namespace, + &mut transaction, + ).await.map_err(|err| { + error!("Error creating wallet for user {:?}: {:?}", user_token, err); + CommonWebError::ServerError + })?; + + transaction + .commit() + .await + .map_err(|err| { + error!("Error committing MySQL transaction: {:?}", err); + CommonWebError::ServerError + })?; + + Ok(Json(ModeratorCreateWalletForUserResponse { + success: true, + wallet_token, + })) +} 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 545b92eff0..315e1a00fd 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 @@ -24,6 +24,7 @@ use crate::http_server::endpoints::moderation::wallet_ledger_entries::list_walle use crate::http_server::endpoints::moderation::wallet_ledger_entries::moderator_get_wallet_ledger_entry_handler::moderator_get_wallet_ledger_entry_handler; use crate::http_server::endpoints::moderation::wallets::list_user_wallets_handler::list_user_wallets_handler; use crate::http_server::endpoints::moderation::wallets::moderator_add_banked_balance_to_wallet_handler::moderator_add_banked_balance_to_wallet_handler; +use crate::http_server::endpoints::moderation::wallets::moderator_create_wallet_for_user_handler::moderator_create_wallet_for_user_handler; use crate::http_server::endpoints::moderation::wallets::moderator_get_wallet_handler::moderator_get_wallet_handler; use crate::http_server::endpoints::moderation::jobs::get_tts_inference_queue_count::get_tts_inference_queue_count_handler; use crate::http_server::endpoints::moderation::jobs::get_w2l_inference_queue_count::get_w2l_inference_queue_count_handler; @@ -66,13 +67,19 @@ pub fn add_moderator_routes (app: App) -> App .route(web::head().to(|| HttpResponse::Ok())) ) ) - .service(web::resource("/wallet/{wallet_token}") - .route(web::get().to(moderator_get_wallet_handler)) - .route(web::head().to(|| HttpResponse::Ok())) - ) - .service(web::resource("/wallet/{wallet_token}/add_banked_balance") - .route(web::post().to(moderator_add_banked_balance_to_wallet_handler)) - .route(web::head().to(|| HttpResponse::Ok())) + .service(web::scope("/wallet") + .service(web::resource("/create_for_user") + .route(web::post().to(moderator_create_wallet_for_user_handler)) + .route(web::head().to(|| HttpResponse::Ok())) + ) + .service(web::resource("/{wallet_token}") + .route(web::get().to(moderator_get_wallet_handler)) + .route(web::head().to(|| HttpResponse::Ok())) + ) + .service(web::resource("/{wallet_token}/add_banked_balance") + .route(web::post().to(moderator_add_banked_balance_to_wallet_handler)) + .route(web::head().to(|| HttpResponse::Ok())) + ) ) .service(web::scope("/wallets") .service(web::resource("/user/{user_token}/list")