Merge pull request #148 from yenicelik/david/65-stats-premium-and-subuser-constraints
stats subuser + premium access logic
This commit is contained in:
commit
f3ad6b9c17
@ -44,6 +44,7 @@ impl From<Web3ProxyError> for Web3ProxyResult<()> {
|
||||
pub enum Web3ProxyError {
|
||||
Abi(ethers::abi::Error),
|
||||
AccessDenied,
|
||||
AccessDeniedNoSubuser,
|
||||
#[error(ignore)]
|
||||
Anyhow(anyhow::Error),
|
||||
Arc(Arc<Self>),
|
||||
@ -188,6 +189,17 @@ impl Web3ProxyError {
|
||||
},
|
||||
)
|
||||
}
|
||||
Self::AccessDeniedNoSubuser => {
|
||||
trace!("access denied not a subuser");
|
||||
(
|
||||
StatusCode::FORBIDDEN,
|
||||
JsonRpcErrorData {
|
||||
message: "FORBIDDEN: NOT A SUBUSER".into(),
|
||||
code: StatusCode::FORBIDDEN.as_u16().into(),
|
||||
data: None,
|
||||
},
|
||||
)
|
||||
}
|
||||
Self::Anyhow(err) => {
|
||||
warn!("anyhow. err={:?}", err);
|
||||
(
|
||||
|
@ -15,12 +15,13 @@ use axum::{
|
||||
Json, TypedHeader,
|
||||
};
|
||||
use entities::sea_orm_active_enums::Role;
|
||||
use entities::{rpc_key, secondary_user};
|
||||
use entities::{balance, rpc_key, secondary_user};
|
||||
use fstrings::{f, format_args_f};
|
||||
use hashbrown::HashMap;
|
||||
use influxdb2::api::query::FluxRecord;
|
||||
use influxdb2::models::Query;
|
||||
use migration::sea_orm::{ColumnTrait, EntityTrait, QueryFilter};
|
||||
use rust_decimal::Decimal;
|
||||
use serde_json::json;
|
||||
use tracing::{debug, error, trace, warn};
|
||||
use ulid::Ulid;
|
||||
@ -28,10 +29,10 @@ use ulid::Ulid;
|
||||
pub async fn query_user_stats<'a>(
|
||||
app: &'a Web3ProxyApp,
|
||||
bearer: Option<TypedHeader<Authorization<Bearer>>>,
|
||||
params: &'a HashMap<String, String>,
|
||||
mut params: &'a HashMap<String, String>,
|
||||
stat_response_type: StatType,
|
||||
) -> Web3ProxyResponse {
|
||||
let (user_id, _semaphore) = match bearer {
|
||||
let (caller_user_id, _semaphore) = match bearer {
|
||||
Some(TypedHeader(Authorization(bearer))) => {
|
||||
let (user, semaphore) = app.bearer_is_authorized(bearer).await?;
|
||||
(user.id, Some(semaphore))
|
||||
@ -40,14 +41,75 @@ pub async fn query_user_stats<'a>(
|
||||
};
|
||||
|
||||
// Return an error if the bearer is **not** set, but the StatType is Detailed
|
||||
if stat_response_type == StatType::Detailed && user_id == 0 {
|
||||
if stat_response_type == StatType::Detailed && caller_user_id == 0 {
|
||||
return Err(Web3ProxyError::BadRequest(
|
||||
"Detailed Stats Response requires you to authorize with a bearer token".into(),
|
||||
));
|
||||
}
|
||||
|
||||
// Read the (optional) user-id from the request, this is the logic for subusers
|
||||
let user_id: u64 = params
|
||||
.get("user_id")
|
||||
.and_then(|x| x.parse::<u64>().ok())
|
||||
.unwrap_or_else(|| caller_user_id);
|
||||
|
||||
let db_replica = app.db_replica()?;
|
||||
|
||||
// In any case, we don't allow stats if the target user does not have a balance
|
||||
// No subuser, we can check the balance directly
|
||||
match balance::Entity::find()
|
||||
.filter(balance::Column::UserId.eq(user_id))
|
||||
.one(db_replica.as_ref())
|
||||
.await?
|
||||
{
|
||||
// TODO: We should add the threshold that determines if a user is premium into app.config or so
|
||||
Some(user_balance) => {
|
||||
if user_balance.total_spent_outside_free_tier - user_balance.total_deposits
|
||||
< Decimal::from(0)
|
||||
{
|
||||
trace!("User has 0 balance");
|
||||
return Err(Web3ProxyError::PaymentRequired);
|
||||
}
|
||||
// Otherwise make the user pass
|
||||
}
|
||||
None => {
|
||||
trace!("User does not have a balance record, implying that he has no balance. Users must have a balance to access their stats dashboards");
|
||||
return Err(Web3ProxyError::PaymentRequired);
|
||||
}
|
||||
}
|
||||
|
||||
// (Possible) subuser relation
|
||||
// Check if the caller is a proper subuser (there is a subuser record)
|
||||
// Check if the subuser has more than collaborator status
|
||||
if user_id != caller_user_id {
|
||||
// Find all rpc-keys related to the caller user
|
||||
let user_rpc_keys: Vec<u64> = rpc_key::Entity::find()
|
||||
.filter(rpc_key::Column::UserId.eq(user_id))
|
||||
.all(db_replica.as_ref())
|
||||
.await?
|
||||
.into_iter()
|
||||
.map(|x| x.id)
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
match secondary_user::Entity::find()
|
||||
.filter(secondary_user::Column::UserId.eq(caller_user_id))
|
||||
.filter(secondary_user::Column::RpcSecretKeyId.is_in(user_rpc_keys))
|
||||
.one(db_replica.as_ref())
|
||||
.await?
|
||||
{
|
||||
Some(secondary_user_record) => {
|
||||
if secondary_user_record.role == Role::Collaborator {
|
||||
trace!("Subuser is only a collaborator, collaborators cannot see stats");
|
||||
return Err(Web3ProxyError::AccessDenied);
|
||||
}
|
||||
}
|
||||
None => {
|
||||
// Then we must do an access denied
|
||||
return Err(Web3ProxyError::AccessDeniedNoSubuser);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// TODO: have a getter for this. do we need a connection pool on it?
|
||||
let influxdb_client = app
|
||||
.influxdb_client
|
||||
|
Loading…
Reference in New Issue
Block a user