diff --git a/entities/src/admin_increase_balance_receipt.rs b/entities/src/admin_increase_balance_receipt.rs new file mode 100644 index 00000000..d3d3cf27 --- /dev/null +++ b/entities/src/admin_increase_balance_receipt.rs @@ -0,0 +1,49 @@ +//! `SeaORM` Entity. Generated by sea-orm-codegen 0.10.6 + +use sea_orm::entity::prelude::*; + +#[derive(Clone, Debug, PartialEq, DeriveEntityModel, Eq)] +#[sea_orm(table_name = "admin_increase_balance_receipt")] +pub struct Model { + #[sea_orm(primary_key)] + pub id: i32, + #[sea_orm(column_type = "Decimal(Some((20, 10)))")] + pub amount: Decimal, + pub admin_id: u64, + pub deposit_to_user_id: u64, + pub note: String, +} + +#[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)] +pub enum Relation { + #[sea_orm( + belongs_to = "super::admin::Entity", + from = "Column::AdminId", + to = "super::admin::Column::Id", + on_update = "NoAction", + on_delete = "NoAction" + )] + Admin, + #[sea_orm( + belongs_to = "super::user::Entity", + from = "Column::DepositToUserId", + to = "super::user::Column::Id", + on_update = "NoAction", + on_delete = "NoAction" + )] + User, +} + +impl Related for Entity { + fn to() -> RelationDef { + Relation::Admin.def() + } +} + +impl Related for Entity { + fn to() -> RelationDef { + Relation::User.def() + } +} + +impl ActiveModelBehavior for ActiveModel {} diff --git a/entities/src/mod.rs b/entities/src/mod.rs index 91a8a460..00510fae 100644 --- a/entities/src/mod.rs +++ b/entities/src/mod.rs @@ -3,6 +3,7 @@ pub mod prelude; pub mod admin; +pub mod admin_increase_balance_receipt; pub mod admin_trail; pub mod balance; pub mod increase_on_chain_balance_receipt; diff --git a/entities/src/prelude.rs b/entities/src/prelude.rs index 9d5f4cc0..20b764f3 100644 --- a/entities/src/prelude.rs +++ b/entities/src/prelude.rs @@ -1,6 +1,7 @@ //! `SeaORM` Entity. Generated by sea-orm-codegen 0.10.7 pub use super::admin::Entity as Admin; +pub use super::admin_increase_balance_receipt::Entity as AdminIncreaseBalanceReceipt; pub use super::admin_trail::Entity as AdminTrail; pub use super::balance::Entity as Balance; pub use super::increase_on_chain_balance_receipt::Entity as IncreaseOnChainBalanceReceipt; diff --git a/migration/src/lib.rs b/migration/src/lib.rs index ae9adaf7..91c45391 100644 --- a/migration/src/lib.rs +++ b/migration/src/lib.rs @@ -27,6 +27,7 @@ mod m20230412_171916_modify_secondary_user_add_primary_user; mod m20230422_172555_premium_downgrade_logic; mod m20230511_161214_remove_columns_statsv2_origin_and_method; mod m20230512_220213_allow_null_rpc_key_id_in_stats_v2; +mod m20230514_114803_admin_add_credits; pub struct Migrator; @@ -51,16 +52,17 @@ impl MigratorTrait for Migrator { Box::new(m20230125_204810_stats_v2::Migration), Box::new(m20230130_124740_read_only_login_logic::Migration), Box::new(m20230130_165144_prepare_admin_imitation_pre_login::Migration), - Box::new(m20230215_152254_admin_trail::Migration), - Box::new(m20230307_002623_migrate_rpc_accounting_to_rpc_accounting_v2::Migration), Box::new(m20230205_130035_create_balance::Migration), Box::new(m20230205_133755_create_referrals::Migration), Box::new(m20230214_134254_increase_balance_transactions::Migration), + Box::new(m20230215_152254_admin_trail::Migration), Box::new(m20230221_230953_track_spend::Migration), + Box::new(m20230307_002623_migrate_rpc_accounting_to_rpc_accounting_v2::Migration), Box::new(m20230412_171916_modify_secondary_user_add_primary_user::Migration), Box::new(m20230422_172555_premium_downgrade_logic::Migration), Box::new(m20230511_161214_remove_columns_statsv2_origin_and_method::Migration), Box::new(m20230512_220213_allow_null_rpc_key_id_in_stats_v2::Migration), + Box::new(m20230514_114803_admin_add_credits::Migration), ] } } diff --git a/migration/src/m20230514_114803_admin_add_credits.rs b/migration/src/m20230514_114803_admin_add_credits.rs new file mode 100644 index 00000000..0344d5d6 --- /dev/null +++ b/migration/src/m20230514_114803_admin_add_credits.rs @@ -0,0 +1,97 @@ +use sea_orm_migration::prelude::*; + +#[derive(DeriveMigrationName)] +pub struct Migration; + +#[async_trait::async_trait] +impl MigrationTrait for Migration { + async fn up(&self, manager: &SchemaManager) -> Result<(), DbErr> { + manager + .create_table( + Table::create() + .table(AdminIncreaseBalanceReceipt::Table) + .if_not_exists() + .col( + ColumnDef::new(AdminIncreaseBalanceReceipt::Id) + .integer() + .not_null() + .auto_increment() + .primary_key(), + ) + .col( + ColumnDef::new(AdminIncreaseBalanceReceipt::Amount) + .decimal_len(20, 10) + .not_null(), + ) + .col( + ColumnDef::new(AdminIncreaseBalanceReceipt::AdminId) + .big_unsigned() + .not_null(), + ) + .foreign_key( + ForeignKey::create() + .name("fk-admin_id") + .from( + AdminIncreaseBalanceReceipt::Table, + AdminIncreaseBalanceReceipt::AdminId, + ) + .to(Admin::Table, Admin::Id), + ) + .col( + ColumnDef::new(AdminIncreaseBalanceReceipt::DepositToUserId) + .big_unsigned() + .not_null(), + ) + .foreign_key( + ForeignKey::create() + .name("fk-admin_deposits_to_user_id") + .from( + AdminIncreaseBalanceReceipt::Table, + AdminIncreaseBalanceReceipt::DepositToUserId, + ) + .to(User::Table, User::Id), + ) + .col( + ColumnDef::new(AdminIncreaseBalanceReceipt::Note) + .string() + .not_null(), + ) + .to_owned(), + ) + .await + } + + async fn down(&self, manager: &SchemaManager) -> Result<(), DbErr> { + manager + .drop_table( + Table::drop() + .table(AdminIncreaseBalanceReceipt::Table) + .to_owned(), + ) + .await + } +} + +#[derive(Iden)] +enum Admin { + Table, + Id, +} + +/// Learn more at https://docs.rs/sea-query#iden +#[derive(Iden)] +enum User { + Table, + Id, +} + +/// Learn more at https://docs.rs/sea-query#iden +#[derive(Iden)] +enum AdminIncreaseBalanceReceipt { + Table, + Id, + Amount, + AdminId, + DepositToUserId, + Note, +} diff --git a/scripts/manual-tests/45-admin-add-balance.sh b/scripts/manual-tests/45-admin-add-balance.sh new file mode 100644 index 00000000..61b194f8 --- /dev/null +++ b/scripts/manual-tests/45-admin-add-balance.sh @@ -0,0 +1,44 @@ + +# Create / Login user1 +curl -X GET "http://127.0.0.1:8544/user/login/0xeb3e928a2e54be013ef8241d4c9eaf4dfae94d5a" +curl -X POST http://127.0.0.1:8544/user/login \ + -H 'Content-Type: application/json' \ + -d '{ + "address": "0xeb3e928a2e54be013ef8241d4c9eaf4dfae94d5a", + "msg": "0x6c6c616d616e6f6465732e636f6d2077616e747320796f7520746f207369676e20696e207769746820796f757220457468657265756d206163636f756e743a0a3078654233453932384132453534424530313345463832343164344339456146344466414539344435610a0af09fa699f09fa699f09fa699f09fa699f09fa6990a0a5552493a2068747470733a2f2f6c6c616d616e6f6465732e636f6d2f0a56657273696f6e3a20310a436861696e2049443a20310a4e6f6e63653a203031483044573642334a48355a4b384a4e3947504d594e4d4b370a4973737565642041743a20323032332d30352d31345431393a33353a35352e3736323632395a0a45787069726174696f6e2054696d653a20323032332d30352d31345431393a35353a35352e3736323632395a", + "sig": "f88b42d638246f8e51637c753052cab3a13b2a138faf3107c921ce2f0027d6506b9adcd3a7b72af830cdf50d20e6e9cb3f9f456dd1be47f6543990ea050909791c", + "version": "3", + "signer": "MEW" + }' + +# 01H0DW6VFCP365B9TXVQVVMHHY +# 01H0DVZNDJWQ7YG8RBHXQHJ301 + +# Make user1 an admin +cargo run change_admin_status 0xeB3E928A2E54BE013EF8241d4C9EaF4DfAE94D5a true + +# Create/Login user2 +curl -X GET "http://127.0.0.1:8544/user/login/0x762390ae7a3c4D987062a398C1eA8767029AB08E" + +curl -X POST http://127.0.0.1:8544/user/login \ + -H 'Content-Type: application/json' \ + -d '{ + "address": "0x762390ae7a3c4d987062a398c1ea8767029ab08e", + "msg": "0x6c6c616d616e6f6465732e636f6d2077616e747320796f7520746f207369676e20696e207769746820796f757220457468657265756d206163636f756e743a0a3078373632333930616537613363344439383730363261333938433165413837363730323941423038450a0af09fa699f09fa699f09fa699f09fa699f09fa6990a0a5552493a2068747470733a2f2f6c6c616d616e6f6465732e636f6d2f0a56657273696f6e3a20310a436861696e2049443a20310a4e6f6e63653a20303148304457384233304e534447594e484d33514d4a31434e530a4973737565642041743a20323032332d30352d31345431393a33373a30312e3238303338355a0a45787069726174696f6e2054696d653a20323032332d30352d31345431393a35373a30312e3238303338355a", + "sig": "c545235557b7952a789dffa2af153af5cf663dcc05449bcc4b651b04cda57de05bcef55c0f5cbf6aa2432369582eb6a40927d14ad0a2d15f48fa45f32fbf273f1c", + "version": "3", + "signer": "MEW" + }' + +# 01H0DWPXRQA7XX2VFSNR02CG1N +# 01H0DWPXQQ951Y3R90QMF6MYGE + +curl \ +-H "Authorization: Bearer 01H0DWPXRQA7XX2VFSNR02CG1N" \ +-X GET "127.0.0.1:8544/user/balance" + + +# Admin add balance +curl \ +-H "Authorization: Bearer 01H0DW6VFCP365B9TXVQVVMHHY" \ +-X GET "127.0.0.1:8544/admin/increase_balance?user_address=0x762390ae7a3c4D987062a398C1eA8767029AB08E&amount=100.0" diff --git a/web3_proxy/src/frontend/admin.rs b/web3_proxy/src/frontend/admin.rs index c3ddf453..e2527b93 100644 --- a/web3_proxy/src/frontend/admin.rs +++ b/web3_proxy/src/frontend/admin.rs @@ -5,8 +5,11 @@ use super::errors::Web3ProxyResponse; use crate::admin_queries::query_admin_modify_usertier; use crate::app::Web3ProxyApp; use crate::frontend::errors::{Web3ProxyError, Web3ProxyErrorContext}; +use crate::http_params::get_user_id_from_params; use crate::user_token::UserBearerToken; use crate::PostLogin; +use anyhow::Context; +use axum::body::HttpBody; use axum::{ extract::{Path, Query}, headers::{authorization::Bearer, Authorization}, @@ -16,15 +19,20 @@ use axum::{ use axum_client_ip::InsecureClientIp; use axum_macros::debug_handler; use chrono::{TimeZone, Utc}; -use entities::{admin_trail, login, pending_login, rpc_key, user}; +use entities::{ + admin, admin_increase_balance_receipt, admin_trail, balance, login, pending_login, rpc_key, + user, user_tier, +}; use ethers::{prelude::Address, types::Bytes}; use hashbrown::HashMap; use http::StatusCode; use log::{debug, info, warn}; -use migration::sea_orm::prelude::Uuid; +use migration::sea_orm::prelude::{Decimal, Uuid}; use migration::sea_orm::{ - self, ActiveModelTrait, ColumnTrait, EntityTrait, IntoActiveModel, QueryFilter, + self, ActiveModelTrait, ActiveValue, ColumnTrait, EntityTrait, IntoActiveModel, QueryFilter, + TransactionTrait, Update, }; +use migration::{ConnectionTrait, Expr, OnConflict}; use serde_json::json; use siwe::{Message, VerificationOpts}; use std::ops::Add; @@ -33,6 +41,138 @@ use std::sync::Arc; use time::{Duration, OffsetDateTime}; use ulid::Ulid; +/// `GET /admin/increase_balance` -- As an admin, modify a user's user-tier +/// +/// - user_address that is to credited balance +/// - user_role_tier that is supposed to be adapted +#[debug_handler] +pub async fn admin_increase_balance( + Extension(app): Extension>, + TypedHeader(Authorization(bearer)): TypedHeader>, + Query(params): Query>, +) -> Web3ProxyResponse { + let (caller, _) = app.bearer_is_authorized(bearer).await?; + let caller_id = caller.id; + + // Establish connections + let db_conn = app + .db_conn() + .context("query_admin_modify_user needs a db")?; + + // Check if the caller is an admin (if not, return early) + let admin_entry: admin::Model = admin::Entity::find() + .filter(admin::Column::UserId.eq(caller_id)) + .one(&db_conn) + .await? + .ok_or(Web3ProxyError::AccessDenied)?; + + // Get the user from params + let user_address: Address = params + .get("user_address") + .ok_or_else(|| { + Web3ProxyError::BadRequest("Unable to find user_address key in request".to_string()) + })? + .parse::
() + .map_err(|_| { + Web3ProxyError::BadRequest("Unable to parse user_address as an Address".to_string()) + })?; + let user_address_bytes: Vec = user_address.clone().to_fixed_bytes().into(); + let note: String = params + .get("note") + .ok_or_else(|| { + Web3ProxyError::BadRequest("Unable to find 'note' key in request".to_string()) + })? + .parse::() + .map_err(|_| { + Web3ProxyError::BadRequest("Unable to parse 'note' as a String".to_string()) + })?; + // Get the amount from params + // Decimal::from_str + let amount: Decimal = params + .get("amount") + .ok_or_else(|| { + Web3ProxyError::BadRequest("Unable to get the amount key from the request".to_string()) + }) + .map(|x| Decimal::from_str(x))? + .or_else(|err| { + Err(Web3ProxyError::BadRequest(format!( + "Unable to parse amount from the request {:?}", + err + ))) + })?; + + let user_entry: user::Model = user::Entity::find() + .filter(user::Column::Address.eq(user_address_bytes.clone())) + .one(&db_conn) + .await? + .ok_or(Web3ProxyError::BadRequest( + "No user with this id found".to_string(), + ))?; + + let increase_balance_receipt = admin_increase_balance_receipt::ActiveModel { + amount: sea_orm::Set(amount), + admin_id: sea_orm::Set(admin_entry.id), + deposit_to_user_id: sea_orm::Set(user_entry.id), + note: sea_orm::Set(note), + ..Default::default() + }; + increase_balance_receipt.save(&db_conn).await?; + + let mut out = HashMap::new(); + out.insert( + "user", + serde_json::Value::String(format!("{:?}", user_address)), + ); + out.insert("amount", serde_json::Value::String(amount.to_string())); + + // Get the balance row + let balance_entry: balance::Model = balance::Entity::find() + .filter(balance::Column::UserId.eq(user_entry.id)) + .one(&db_conn) + .await? + .context("User does not have a balance row")?; + + // Finally make the user premium if balance is above 10$ + let premium_user_tier = user_tier::Entity::find() + .filter(user_tier::Column::Title.eq("Premium")) + .one(&db_conn) + .await? + .context("Premium tier was not found!")?; + + let balance_entry = balance_entry.into_active_model(); + balance::Entity::insert(balance_entry) + .on_conflict( + OnConflict::new() + .values([ + // ( + // balance::Column::Id, + // Expr::col(balance::Column::Id).add(self.frontend_requests), + // ), + ( + balance::Column::AvailableBalance, + Expr::col(balance::Column::AvailableBalance).add(amount), + ), + // ( + // balance::Column::Used, + // Expr::col(balance::Column::UsedBalance).add(self.backend_retries), + // ), + // ( + // balance::Column::UserId, + // Expr::col(balance::Column::UserId).add(self.no_servers), + // ), + ]) + .to_owned(), + ) + .exec(&db_conn) + .await?; + // TODO: Downgrade otherwise, right now not functioning properly + + // Then read and save in one transaction + let response = (StatusCode::OK, Json(out)).into_response(); + + Ok(response) +} + /// `GET /admin/modify_role` -- As an admin, modify a user's user-tier /// /// - user_address that is to be modified diff --git a/web3_proxy/src/frontend/mod.rs b/web3_proxy/src/frontend/mod.rs index e1496960..107360c4 100644 --- a/web3_proxy/src/frontend/mod.rs +++ b/web3_proxy/src/frontend/mod.rs @@ -199,6 +199,10 @@ pub async fn serve( "/user/logout", post(users::authentication::user_logout_post), ) + .route( + "/admin/increase_balance", + get(admin::admin_increase_balance), + ) .route("/admin/modify_role", get(admin::admin_change_user_roles)) .route( "/admin/imitate-login/:admin_address/:user_address",