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..1ae28c2fba --- /dev/null +++ b/crates/schema/database/mysql_queries/src/queries/users/user/get/get_username_by_user_token.rs @@ -0,0 +1,46 @@ +use anyhow::anyhow; +use log::warn; +use sqlx::MySqlPool; + +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, anyhow::Error> { + 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_one(pool) + .await; + + match result { + Ok(record) => Ok(Some(record)), + Err(sqlx::Error::RowNotFound) => Ok(None), + Err(err) => { + warn!("get_username_by_user_token query error: {:?}", err); + Err(anyhow!("query error")) + } + } +} 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_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..1275aafeda 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,6 +67,10 @@ pub fn add_moderator_routes (app: App) -> App .route(web::head().to(|| HttpResponse::Ok())) ) ) + .service(web::resource("/wallet/create_for_user") + .route(web::post().to(moderator_create_wallet_for_user_handler)) + .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()))