web3-proxy/web3_proxy/src/frontend/users.rs

768 lines
27 KiB
Rust
Raw Normal View History

2022-10-18 00:47:58 +03:00
//! Handle registration, logins, and managing account data.
2022-07-14 00:49:57 +03:00
2022-10-26 00:10:05 +03:00
use super::authorization::{login_is_authorized, RpcApiKey};
use super::errors::FrontendResult;
2022-09-24 08:53:45 +03:00
use crate::app::Web3ProxyApp;
2022-10-26 00:10:05 +03:00
use crate::user_queries::{
get_aggregate_rpc_stats_from_params, get_detailed_stats, get_page_from_params,
};
use crate::user_queries::{get_chain_id_from_params, get_query_start_from_params};
2022-10-31 23:05:58 +03:00
use crate::user_token::UserBearerToken;
2022-08-27 08:42:25 +03:00
use anyhow::Context;
2022-10-27 00:39:26 +03:00
use axum::headers::{Header, Origin, Referer, UserAgent};
2022-08-11 04:53:27 +03:00
use axum::{
2022-08-21 11:18:57 +03:00
extract::{Path, Query},
headers::{authorization::Bearer, Authorization},
response::IntoResponse,
Extension, Json, TypedHeader,
2022-08-11 04:53:27 +03:00
};
2022-08-04 04:10:27 +03:00
use axum_client_ip::ClientIp;
2022-08-16 22:29:00 +03:00
use axum_macros::debug_handler;
2022-10-27 03:12:42 +03:00
use entities::{revert_logs, rpc_keys, user};
2022-08-04 04:10:27 +03:00
use ethers::{prelude::Address, types::Bytes};
2022-08-19 23:18:12 +03:00
use hashbrown::HashMap;
2022-10-27 00:39:26 +03:00
use http::{HeaderValue, StatusCode};
use ipnet::IpNet;
use itertools::Itertools;
2022-09-15 20:57:24 +03:00
use redis_rate_limiter::redis::AsyncCommands;
2022-10-26 00:10:05 +03:00
use sea_orm::{
ActiveModelTrait, ColumnTrait, EntityTrait, PaginatorTrait, QueryFilter, QueryOrder,
2022-10-26 00:12:56 +03:00
TransactionTrait,
2022-10-26 00:10:05 +03:00
};
2022-10-25 22:03:11 +03:00
use serde::Deserialize;
use serde_json::json;
use siwe::{Message, VerificationOpts};
2022-08-27 05:13:36 +03:00
use std::ops::Add;
2022-10-20 11:14:38 +03:00
use std::str::FromStr;
2022-08-04 04:10:27 +03:00
use std::sync::Arc;
use time::{Duration, OffsetDateTime};
2022-10-29 01:52:47 +03:00
use tracing::{instrument, warn};
2022-08-21 11:18:57 +03:00
use ulid::Ulid;
2022-10-18 00:47:58 +03:00
/// `GET /user/login/:user_address` or `GET /user/login/:user_address/:message_eip` -- Start the "Sign In with Ethereum" (siwe) login flow.
///
/// `message_eip`s accepted:
/// - eip191_bytes
/// - eip191_hash
/// - eip4361 (default)
///
/// Coming soon: eip1271
///
/// This is the initial entrypoint for logging in. Take the response from this endpoint and give it to your user's wallet for singing. POST the response to `/user/login`.
///
/// Rate limited by IP address.
2022-10-26 00:10:05 +03:00
///
/// At first i thought about checking that user_address is in our db,
/// But theres no need to separate the registration and login flows.
/// It is a better UX to just click "login with ethereum" and have the account created if it doesn't exist.
/// We can prompt for an email and and payment after they log in.
2022-08-16 22:29:00 +03:00
#[debug_handler]
2022-10-29 01:52:47 +03:00
#[instrument(level = "trace")]
2022-10-18 00:47:58 +03:00
pub async fn user_login_get(
Extension(app): Extension<Arc<Web3ProxyApp>>,
2022-08-17 00:43:39 +03:00
ClientIp(ip): ClientIp,
// TODO: what does axum's error handling look like if the path fails to parse?
Path(mut params): Path<HashMap<String, String>>,
2022-08-17 00:43:39 +03:00
) -> FrontendResult {
2022-10-25 07:01:41 +03:00
login_is_authorized(&app, ip).await?;
2022-08-17 00:43:39 +03:00
2022-08-21 11:18:57 +03:00
// create a message and save it in redis
// TODO: how many seconds? get from config?
2022-10-20 09:54:45 +03:00
let expire_seconds: usize = 20 * 60;
2022-08-21 11:18:57 +03:00
let nonce = Ulid::new();
let issued_at = OffsetDateTime::now_utc();
let expiration_time = issued_at.add(Duration::new(expire_seconds as i64, 0));
2022-10-26 00:10:05 +03:00
// TODO: allow ENS names here?
let user_address: Address = params
.remove("user_address")
// TODO: map_err so this becomes a 500. routing must be bad
.context("impossible")?
.parse()
// TODO: map_err so this becomes a 401
.context("bad input")?;
// TODO: get most of these from the app config
let message = Message {
// TODO: should domain be llamanodes, or llamarpc, or the subdomain of llamarpc?
domain: "staging.llamanodes.com".parse().unwrap(),
address: user_address.to_fixed_bytes(),
statement: Some("🦙🦙🦙🦙🦙".to_string()),
uri: "https://staging.llamanodes.com/".parse().unwrap(),
version: siwe::Version::V1,
chain_id: 1,
expiration_time: Some(expiration_time.into()),
issued_at: issued_at.into(),
nonce: nonce.to_string(),
not_before: None,
request_id: None,
resources: vec![],
};
2022-10-19 03:56:57 +03:00
// TODO: if no redis server, store in local cache? at least give a better error. right now this seems to be a 502
2022-08-21 11:18:57 +03:00
// the address isn't enough. we need to save the actual message so we can read the nonce
// TODO: what message format is the most efficient to store in redis? probably eip191_bytes
// we add 1 to expire_seconds just to be sure redis has the key for the full expiration_time
// TODO: store a maximum number of attempted logins? anyone can request so we don't want to allow DOS attacks
2022-10-20 09:54:45 +03:00
let session_key = format!("login_nonce:{}", nonce);
app.redis_conn()
.await?
.set_ex(session_key, message.to_string(), expire_seconds + 1)
.await?;
2022-08-19 23:18:12 +03:00
// there are multiple ways to sign messages and not all wallets support them
2022-10-18 00:47:58 +03:00
// TODO: default message eip from config?
let message_eip = params
.remove("message_eip")
.unwrap_or_else(|| "eip4361".to_string());
2022-08-19 23:18:12 +03:00
let message: String = match message_eip.as_str() {
"eip191_bytes" => Bytes::from(message.eip191_bytes().unwrap()).to_string(),
2022-08-19 23:18:12 +03:00
"eip191_hash" => Bytes::from(&message.eip191_hash().unwrap()).to_string(),
2022-09-14 09:18:13 +03:00
"eip4361" => message.to_string(),
2022-09-10 03:58:33 +03:00
_ => {
2022-10-19 03:56:57 +03:00
// TODO: custom error that is handled a 401
2022-09-10 03:58:33 +03:00
return Err(anyhow::anyhow!("invalid message eip given").into());
}
2022-08-19 23:18:12 +03:00
};
Ok(message.into_response())
2022-08-16 22:29:00 +03:00
}
2022-08-04 02:17:02 +03:00
2022-10-18 00:47:58 +03:00
/// Query params for our `post_login` handler.
2022-08-21 11:18:57 +03:00
#[derive(Debug, Deserialize)]
pub struct PostLoginQuery {
2022-10-18 00:47:58 +03:00
/// While we are in alpha/beta, we require users to supply an invite code.
/// The invite code (if any) is set in the application's config.
/// This may eventually provide some sort of referral bonus.
pub invite_code: Option<String>,
2022-08-21 11:18:57 +03:00
}
/// JSON body to our `post_login` handler.
2022-10-18 00:47:58 +03:00
/// Currently only siwe logins that send an address, msg, and sig are allowed.
2022-10-19 03:56:57 +03:00
/// Email/password and other login methods are planned.
2022-10-29 01:52:47 +03:00
#[derive(Debug, Deserialize)]
2022-08-21 11:18:57 +03:00
pub struct PostLogin {
2022-10-20 11:14:38 +03:00
sig: String,
msg: String,
2022-08-21 11:18:57 +03:00
}
2022-10-18 00:47:58 +03:00
/// `POST /user/login` - Register or login by posting a signed "siwe" message.
/// It is recommended to save the returned bearer token in a cookie.
/// The bearer token can be used to authenticate other requests, such as getting the user's stats or modifying the user's profile.
#[debug_handler]
2022-10-29 01:52:47 +03:00
#[instrument(level = "trace")]
2022-10-18 00:47:58 +03:00
pub async fn user_login_post(
2022-08-21 11:18:57 +03:00
Extension(app): Extension<Arc<Web3ProxyApp>>,
ClientIp(ip): ClientIp,
2022-08-21 11:18:57 +03:00
Json(payload): Json<PostLogin>,
Query(query): Query<PostLoginQuery>,
2022-08-21 12:39:38 +03:00
) -> FrontendResult {
2022-10-25 07:01:41 +03:00
login_is_authorized(&app, ip).await?;
2022-08-04 04:10:27 +03:00
// TODO: this seems too verbose. how can we simply convert a String into a [u8; 65]
2022-10-20 11:14:38 +03:00
let their_sig_bytes = Bytes::from_str(&payload.sig).context("parsing sig")?;
if their_sig_bytes.len() != 65 {
return Err(anyhow::anyhow!("checking signature length").into());
2022-10-20 11:14:38 +03:00
}
let mut their_sig: [u8; 65] = [0; 65];
for x in 0..65 {
their_sig[x] = their_sig_bytes[x]
}
2022-10-25 22:03:11 +03:00
// we can't trust that they didn't tamper with the message in some way. like some clients return it hex encoded
// TODO: checking 0x seems fragile, but I think it will be fine. siwe message text shouldn't ever start with 0x
let their_msg: Message = if payload.msg.starts_with("0x") {
2022-10-20 11:14:38 +03:00
let their_msg_bytes = Bytes::from_str(&payload.msg).context("parsing payload message")?;
2022-10-20 09:54:45 +03:00
2022-10-20 11:25:02 +03:00
// TODO: lossy or no?
String::from_utf8_lossy(their_msg_bytes.as_ref())
.parse::<siwe::Message>()
.context("parsing hex string message")?
2022-10-20 11:14:38 +03:00
} else {
payload
.msg
.parse::<siwe::Message>()
.context("parsing string message")?
2022-10-20 11:14:38 +03:00
};
2022-10-25 22:03:11 +03:00
// the only part of the message we will trust is their nonce
// TODO: this is fragile. have a helper function/struct for redis keys
2022-10-20 09:54:45 +03:00
let login_nonce_key = format!("login_nonce:{}", &their_msg.nonce);
2022-08-21 11:18:57 +03:00
// fetch the message we gave them from our redis
2022-10-20 11:36:53 +03:00
let mut redis_conn = app.redis_conn().await?;
let our_msg: Option<String> = redis_conn.get(&login_nonce_key).await?;
2022-10-20 09:54:45 +03:00
let our_msg: String = our_msg.context("login nonce not found")?;
2022-08-17 02:03:50 +03:00
2022-10-20 09:54:45 +03:00
let our_msg: siwe::Message = our_msg.parse().context("parsing siwe message")?;
2022-08-17 02:03:50 +03:00
2022-10-25 22:03:11 +03:00
// default options are fine. the message includes timestamp and domain and nonce
2022-10-20 11:36:53 +03:00
let verify_config = VerificationOpts::default();
2022-10-20 11:14:38 +03:00
2022-10-25 22:03:11 +03:00
// Check with both verify and verify_eip191
2022-10-20 11:25:02 +03:00
if let Err(err_1) = our_msg
2022-10-20 10:10:35 +03:00
.verify(&their_sig, &verify_config)
.await
2022-10-20 11:25:02 +03:00
.context("verifying signature against our local message")
{
// verification method 1 failed. try eip191
if let Err(err_191) = our_msg
.verify_eip191(&their_sig)
.context("verifying eip191 signature against our local message")
{
return Err(anyhow::anyhow!(
2022-10-20 11:37:56 +03:00
"both the primary and eip191 verification failed: {:#?}; {:#?}",
2022-10-20 11:25:02 +03:00
err_1,
err_191
)
.into());
2022-10-20 11:25:02 +03:00
}
}
2022-08-04 04:10:27 +03:00
2022-10-20 09:17:20 +03:00
let db_conn = app.db_conn().context("Getting database connection")?;
2022-07-14 00:49:57 +03:00
2022-08-27 08:42:25 +03:00
// TODO: limit columns or load whole user?
let u = user::Entity::find()
.filter(user::Column::Address.eq(our_msg.address.as_ref()))
2022-10-20 09:17:20 +03:00
.one(&db_conn)
2022-08-27 08:42:25 +03:00
.await
.unwrap();
2022-08-04 02:17:02 +03:00
let (u, uks, status_code) = match u {
2022-08-27 08:42:25 +03:00
None => {
// user does not exist yet
// check the invite code
2022-10-25 22:03:11 +03:00
// TODO: more advanced invite codes that set different request/minute and concurrency limits
if let Some(invite_code) = &app.config.invite_code {
if query.invite_code.as_ref() != Some(invite_code) {
return Err(anyhow::anyhow!("checking invite_code").into());
}
}
2022-10-20 09:17:20 +03:00
let txn = db_conn.begin().await?;
2022-08-04 02:17:02 +03:00
2022-08-27 08:42:25 +03:00
// the only thing we need from them is an address
// everything else is optional
let u = user::ActiveModel {
2022-10-20 11:14:38 +03:00
address: sea_orm::Set(our_msg.address.into()),
2022-08-27 08:42:25 +03:00
..Default::default()
};
2022-08-17 02:03:50 +03:00
2022-08-27 08:42:25 +03:00
let u = u.insert(&txn).await?;
2022-08-17 02:03:50 +03:00
// create the user's first api key
// TODO: rename to UserApiKey? RpcApiKey?
2022-10-26 00:10:05 +03:00
let rpc_key = RpcApiKey::new();
2022-09-24 08:53:45 +03:00
// TODO: variable requests per minute depending on the invite code
2022-10-27 03:12:42 +03:00
let uk = rpc_keys::ActiveModel {
2022-08-27 08:42:25 +03:00
user_id: sea_orm::Set(u.id),
2022-10-27 03:12:42 +03:00
rpc_key: sea_orm::Set(rpc_key.into()),
2022-10-26 00:10:05 +03:00
description: sea_orm::Set(Some("first".to_string())),
2022-10-19 02:27:33 +03:00
requests_per_minute: sea_orm::Set(app.config.default_user_requests_per_minute),
2022-08-27 08:42:25 +03:00
..Default::default()
};
2022-08-21 11:18:57 +03:00
2022-08-27 08:42:25 +03:00
let uk = uk
.insert(&txn)
.await
.context("Failed saving new user key")?;
2022-08-17 02:03:50 +03:00
let uks = vec![uk];
// save the user and key to the database
2022-08-27 08:42:25 +03:00
txn.commit().await?;
2022-08-16 20:47:04 +03:00
(u, uks, StatusCode::CREATED)
2022-08-27 08:42:25 +03:00
}
Some(u) => {
// the user is already registered
2022-10-27 03:12:42 +03:00
let uks = rpc_keys::Entity::find()
.filter(rpc_keys::Column::UserId.eq(u.id))
2022-10-20 09:17:20 +03:00
.all(&db_conn)
2022-08-27 08:42:25 +03:00
.await
.context("failed loading user's key")?;
2022-08-27 08:42:25 +03:00
(u, uks, StatusCode::OK)
}
};
2022-08-27 08:42:25 +03:00
// create a bearer token for the user.
let bearer_token = Ulid::new();
2022-08-27 08:42:25 +03:00
2022-10-26 00:10:05 +03:00
// json response with everything in it
// we could return just the bearer token, but I think they will always request api keys and the user profile
2022-10-25 22:03:11 +03:00
let response_json = json!({
2022-10-27 03:12:42 +03:00
"rpc_keys": uks
.into_iter()
2022-10-25 22:03:11 +03:00
.map(|uk| (uk.id, uk))
.collect::<HashMap<_, _>>(),
"bearer_token": bearer_token,
2022-10-26 00:10:05 +03:00
"user": u,
2022-10-25 22:03:11 +03:00
});
2022-08-23 21:51:42 +03:00
let response = (status_code, Json(response_json)).into_response();
// add bearer to redis
// TODO: use a helper function/struct for this
2022-10-31 23:05:58 +03:00
let bearer_redis_key = UserBearerToken(bearer_token).to_string();
2022-09-03 22:43:19 +03:00
// expire in 4 weeks
// TODO: get expiration time from app config
redis_conn
.set_ex(bearer_redis_key, u.id.to_string(), 2_419_200)
.await?;
2022-08-23 21:51:42 +03:00
2022-10-20 11:36:53 +03:00
if let Err(err) = redis_conn.del::<_, u64>(&login_nonce_key).await {
2022-10-20 23:26:14 +03:00
warn!(
"Failed to delete login_nonce_key {}: {}",
login_nonce_key, err
);
2022-10-20 11:36:53 +03:00
}
Ok(response)
2022-07-14 00:49:57 +03:00
}
2022-10-18 00:47:58 +03:00
/// `POST /user/logout` - Forget the bearer token in the `Authentication` header.
#[debug_handler]
2022-10-29 01:52:47 +03:00
#[instrument(level = "trace")]
2022-10-18 00:47:58 +03:00
pub async fn user_logout_post(
Extension(app): Extension<Arc<Web3ProxyApp>>,
TypedHeader(Authorization(bearer)): TypedHeader<Authorization<Bearer>>,
) -> FrontendResult {
2022-09-24 07:31:06 +03:00
let mut redis_conn = app.redis_conn().await?;
// TODO: i don't like this. move this to a helper function so it is less fragile
2022-10-31 23:05:58 +03:00
let bearer_cache_key = UserBearerToken::try_from(bearer)?.to_string();
redis_conn.del(bearer_cache_key).await?;
// TODO: what should the response be? probably json something
Ok("goodbye".into_response())
}
2022-10-26 03:22:58 +03:00
/// `GET /user` -- Use a bearer token to get the user's profile.
///
/// - the email address of a user if they opted in to get contacted via email
///
/// TODO: this will change as we add better support for secondary users.
#[debug_handler]
2022-10-29 01:52:47 +03:00
#[instrument(level = "trace")]
2022-10-26 03:22:58 +03:00
pub async fn user_get(
Extension(app): Extension<Arc<Web3ProxyApp>>,
TypedHeader(Authorization(bearer_token)): TypedHeader<Authorization<Bearer>>,
) -> FrontendResult {
let (user, _semaphore) = app.bearer_is_authorized(bearer_token).await?;
Ok(Json(user).into_response())
}
/// the JSON input to the `post_user` handler.
2022-10-29 01:52:47 +03:00
#[derive(Debug, Deserialize)]
2022-10-26 03:22:58 +03:00
pub struct UserPost {
2022-07-14 00:49:57 +03:00
email: Option<String>,
2022-08-21 11:18:57 +03:00
}
2022-10-26 03:22:58 +03:00
/// `POST /user` -- modify the account connected to the bearer token in the `Authentication` header.
#[debug_handler]
2022-10-29 01:52:47 +03:00
#[instrument(level = "trace")]
2022-10-26 03:22:58 +03:00
pub async fn user_post(
Extension(app): Extension<Arc<Web3ProxyApp>>,
TypedHeader(Authorization(bearer_token)): TypedHeader<Authorization<Bearer>>,
Json(payload): Json<UserPost>,
) -> FrontendResult {
let (user, _semaphore) = app.bearer_is_authorized(bearer_token).await?;
let mut user: user::ActiveModel = user.into();
// update the email address
if let Some(x) = payload.email {
// TODO: only Set if no change
if x.is_empty() {
user.email = sea_orm::Set(None);
} else {
// TODO: do some basic validation
// TODO: don't set immediatly, send a confirmation email first
// TODO: compare first? or is sea orm smart enough to do that for us?
user.email = sea_orm::Set(Some(x));
}
}
// TODO: what else can we update here? password hash? subscription to newsletter?
let user = if user.is_changed() {
let db_conn = app.db_conn().context("Getting database connection")?;
user.save(&db_conn).await?
} else {
// no changes. no need to touch the database
user
};
let user: user::Model = user.try_into().context("Returning updated user")?;
Ok(Json(user).into_response())
}
2022-10-18 00:47:58 +03:00
/// `GET /user/balance` -- Use a bearer token to get the user's balance and spend.
///
/// - show balance in USD
/// - show deposits history (currency, amounts, transaction id)
///
2022-10-27 03:12:42 +03:00
/// TODO: one key per request? maybe /user/balance/:rpc_key?
2022-10-18 00:47:58 +03:00
/// TODO: this will change as we add better support for secondary users.
#[debug_handler]
2022-10-29 01:52:47 +03:00
#[instrument(level = "trace")]
2022-10-18 00:47:58 +03:00
pub async fn user_balance_get(
Extension(app): Extension<Arc<Web3ProxyApp>>,
2022-10-20 23:26:14 +03:00
TypedHeader(Authorization(bearer)): TypedHeader<Authorization<Bearer>>,
2022-10-18 00:47:58 +03:00
) -> FrontendResult {
2022-10-27 01:29:38 +03:00
let (user, _semaphore) = app.bearer_is_authorized(bearer).await?;
2022-10-18 00:47:58 +03:00
todo!("user_balance_get");
}
/// `POST /user/balance/:txhash` -- Manually process a confirmed txid to update a user's balance.
2022-10-18 00:47:58 +03:00
///
/// We will subscribe to events to watch for any user deposits, but sometimes events can be missed.
///
/// TODO: rate limit by user
2022-10-27 03:12:42 +03:00
/// TODO: one key per request? maybe /user/balance/:rpc_key?
2022-10-18 00:47:58 +03:00
/// TODO: this will change as we add better support for secondary users.
#[debug_handler]
2022-10-29 01:52:47 +03:00
#[instrument(level = "trace")]
2022-10-18 00:47:58 +03:00
pub async fn user_balance_post(
Extension(app): Extension<Arc<Web3ProxyApp>>,
2022-10-27 01:29:38 +03:00
TypedHeader(Authorization(bearer)): TypedHeader<Authorization<Bearer>>,
2022-10-18 00:47:58 +03:00
) -> FrontendResult {
2022-10-27 01:29:38 +03:00
let (user, _semaphore) = app.bearer_is_authorized(bearer).await?;
2022-10-18 00:47:58 +03:00
todo!("user_balance_post");
}
/// `GET /user/keys` -- Use a bearer token to get the user's api keys and their settings.
///
2022-10-27 03:12:42 +03:00
/// TODO: one key per request? maybe /user/keys/:rpc_key?
2022-10-18 00:47:58 +03:00
#[debug_handler]
2022-10-29 01:52:47 +03:00
#[instrument(level = "trace")]
2022-10-27 03:12:42 +03:00
pub async fn rpc_keys_get(
2022-10-18 00:47:58 +03:00
Extension(app): Extension<Arc<Web3ProxyApp>>,
2022-10-27 01:29:38 +03:00
TypedHeader(Authorization(bearer)): TypedHeader<Authorization<Bearer>>,
2022-10-18 00:47:58 +03:00
) -> FrontendResult {
2022-10-27 01:29:38 +03:00
let (user, _semaphore) = app.bearer_is_authorized(bearer).await?;
2022-10-25 22:03:11 +03:00
let db_conn = app.db_conn().context("getting db to fetch user's keys")?;
2022-10-27 03:12:42 +03:00
let uks = rpc_keys::Entity::find()
.filter(rpc_keys::Column::UserId.eq(user.id))
2022-10-25 22:03:11 +03:00
.all(&db_conn)
.await
.context("failed loading user's key")?;
// TODO: stricter type on this?
let response_json = json!({
2022-10-26 00:10:05 +03:00
"user_id": user.id,
"user_rpc_keys": uks
2022-10-25 22:03:11 +03:00
.into_iter()
.map(|uk| (uk.id, uk))
.collect::<HashMap::<_, _>>(),
});
Ok(Json(response_json).into_response())
2022-10-18 00:47:58 +03:00
}
2022-10-31 23:51:06 +03:00
/// `DELETE /user/keys` -- Use a bearer token to delete an existing key.
#[debug_handler]
#[instrument(level = "trace")]
pub async fn rpc_keys_delete(
Extension(app): Extension<Arc<Web3ProxyApp>>,
TypedHeader(Authorization(bearer)): TypedHeader<Authorization<Bearer>>,
) -> FrontendResult {
let (user, _semaphore) = app.bearer_is_authorized(bearer).await?;
// TODO: think about how cascading deletes and billing should work
Err(anyhow::anyhow!("work in progress").into())
}
/// the JSON input to the `rpc_keys_management` handler.
/// If `key_id` is set, it updates an existing key.
/// If `key_id` is not set, it creates a new key.
2022-10-29 01:52:47 +03:00
#[derive(Debug, Deserialize)]
2022-10-31 23:51:06 +03:00
pub struct UserKeyManagement {
key_id: Option<u64>,
2022-10-26 03:22:58 +03:00
description: Option<String>,
2022-10-27 00:39:26 +03:00
private_txs: Option<bool>,
active: Option<bool>,
// TODO: enable log_revert_trace: Option<f32>,
allowed_ips: Option<String>,
allowed_origins: Option<String>,
allowed_referers: Option<String>,
allowed_user_agents: Option<String>,
// do not allow! `requests_per_minute: Option<u64>,`
// do not allow! `max_concurrent_requests: Option<u64>,`
2022-10-26 03:22:58 +03:00
}
2022-10-31 23:51:06 +03:00
/// `POST /user/keys` or `PUT /user/keys` -- Use a bearer token to create or update an existing key.
2022-10-18 00:47:58 +03:00
#[debug_handler]
2022-10-29 01:52:47 +03:00
#[instrument(level = "trace")]
2022-10-31 23:51:06 +03:00
pub async fn rpc_keys_management(
2022-10-18 00:47:58 +03:00
Extension(app): Extension<Arc<Web3ProxyApp>>,
2022-10-27 01:29:38 +03:00
TypedHeader(Authorization(bearer)): TypedHeader<Authorization<Bearer>>,
2022-10-31 23:51:06 +03:00
Json(payload): Json<UserKeyManagement>,
2022-10-26 00:10:05 +03:00
) -> FrontendResult {
2022-10-31 23:51:06 +03:00
// TODO: is there a way we can know if this is a PUT or POST? right now we can modify or create keys with either. though that probably doesn't matter
2022-10-27 01:29:38 +03:00
let (user, _semaphore) = app.bearer_is_authorized(bearer).await?;
2022-10-26 00:10:05 +03:00
2022-10-27 00:39:26 +03:00
let db_conn = app.db_conn().context("getting db for user's keys")?;
2022-10-31 23:51:06 +03:00
let mut uk = if let Some(existing_key_id) = payload.key_id {
2022-10-26 03:22:58 +03:00
// get the key and make sure it belongs to the user
2022-10-27 03:12:42 +03:00
let uk = rpc_keys::Entity::find()
.filter(rpc_keys::Column::UserId.eq(user.id))
.filter(rpc_keys::Column::Id.eq(existing_key_id))
2022-10-27 00:39:26 +03:00
.one(&db_conn)
.await
.context("failed loading user's key")?
.context("key does not exist or is not controlled by this bearer token")?;
uk.try_into().unwrap()
2022-10-26 00:10:05 +03:00
} else {
2022-10-26 03:22:58 +03:00
// make a new key
// TODO: limit to 10 keys?
let rpc_key = RpcApiKey::new();
2022-10-26 00:10:05 +03:00
2022-10-27 03:12:42 +03:00
rpc_keys::ActiveModel {
2022-10-27 00:39:26 +03:00
user_id: sea_orm::Set(user.id),
2022-10-27 03:12:42 +03:00
rpc_key: sea_orm::Set(rpc_key.into()),
2022-10-27 00:39:26 +03:00
requests_per_minute: sea_orm::Set(app.config.default_user_requests_per_minute),
..Default::default()
}
};
// TODO: do we need null descriptions? default to empty string should be fine, right?
if let Some(description) = payload.description {
if description.is_empty() {
uk.description = sea_orm::Set(None);
} else {
uk.description = sea_orm::Set(Some(description));
}
}
if let Some(private_txs) = payload.private_txs {
uk.private_txs = sea_orm::Set(private_txs);
}
if let Some(active) = payload.active {
uk.active = sea_orm::Set(active);
}
if let Some(allowed_ips) = payload.allowed_ips {
if allowed_ips.is_empty() {
uk.allowed_ips = sea_orm::Set(None);
} else {
// split allowed ips on ',' and try to parse them all. error on invalid input
let allowed_ips = allowed_ips
.split(',')
.map(|x| x.parse::<IpNet>())
.collect::<Result<Vec<_>, _>>()?
// parse worked. convert back to Strings
.into_iter()
.map(|x| x.to_string());
// and join them back together
let allowed_ips: String =
Itertools::intersperse(allowed_ips, ", ".to_string()).collect();
uk.allowed_ips = sea_orm::Set(Some(allowed_ips));
}
}
// TODO: this should actually be bytes
if let Some(allowed_origins) = payload.allowed_origins {
if allowed_origins.is_empty() {
uk.allowed_origins = sea_orm::Set(None);
} else {
// split allowed_origins on ',' and try to parse them all. error on invalid input
let allowed_origins = allowed_origins
.split(',')
.map(HeaderValue::from_str)
.collect::<Result<Vec<_>, _>>()?
.into_iter()
.map(|x| Origin::decode(&mut [x].iter()))
.collect::<Result<Vec<_>, _>>()?
// parse worked. convert back to String and join them back together
.into_iter()
.map(|x| x.to_string());
let allowed_origins: String =
Itertools::intersperse(allowed_origins, ", ".to_string()).collect();
uk.allowed_origins = sea_orm::Set(Some(allowed_origins));
}
2022-10-26 03:22:58 +03:00
}
2022-10-27 00:39:26 +03:00
// TODO: this should actually be bytes
if let Some(allowed_referers) = payload.allowed_referers {
if allowed_referers.is_empty() {
uk.allowed_referers = sea_orm::Set(None);
} else {
// split allowed ips on ',' and try to parse them all. error on invalid input
let allowed_referers = allowed_referers
.split(',')
.map(HeaderValue::from_str)
.collect::<Result<Vec<_>, _>>()?
.into_iter()
.map(|x| Referer::decode(&mut [x].iter()))
.collect::<Result<Vec<_>, _>>()?;
// parse worked. now we can put it back together.
// but we can't go directly to String.
// so we convert to HeaderValues first
let mut header_map = vec![];
for x in allowed_referers {
x.encode(&mut header_map);
}
// convert HeaderValues to Strings
// since we got these from strings, this should always work (unless we figure out using bytes)
let allowed_referers = header_map
.into_iter()
.map(|x| x.to_str().map(|x| x.to_string()))
.collect::<Result<Vec<_>, _>>()?;
// join strings together with commas
let allowed_referers: String =
Itertools::intersperse(allowed_referers.into_iter(), ", ".to_string()).collect();
uk.allowed_referers = sea_orm::Set(Some(allowed_referers));
}
}
if let Some(allowed_user_agents) = payload.allowed_user_agents {
if allowed_user_agents.is_empty() {
uk.allowed_user_agents = sea_orm::Set(None);
} else {
// split allowed_user_agents on ',' and try to parse them all. error on invalid input
let allowed_user_agents = allowed_user_agents
.split(',')
.filter_map(|x| x.parse::<UserAgent>().ok())
// parse worked. convert back to String
.map(|x| x.to_string());
// join the strings together
let allowed_user_agents: String =
Itertools::intersperse(allowed_user_agents, ", ".to_string()).collect();
uk.allowed_user_agents = sea_orm::Set(Some(allowed_user_agents));
}
}
let uk = if uk.is_changed() {
uk.save(&db_conn).await.context("Failed saving user key")?
} else {
uk
};
2022-10-27 03:12:42 +03:00
let uk: rpc_keys::Model = uk.try_into()?;
2022-10-27 00:39:26 +03:00
Ok(Json(uk).into_response())
2022-10-18 00:47:58 +03:00
}
2022-10-20 09:17:20 +03:00
/// `GET /user/revert_logs` -- Use a bearer token to get the user's revert logs.
#[debug_handler]
2022-10-29 01:52:47 +03:00
#[instrument(level = "trace")]
2022-10-20 09:17:20 +03:00
pub async fn user_revert_logs_get(
Extension(app): Extension<Arc<Web3ProxyApp>>,
2022-10-27 01:29:38 +03:00
TypedHeader(Authorization(bearer)): TypedHeader<Authorization<Bearer>>,
2022-10-26 00:10:05 +03:00
Query(params): Query<HashMap<String, String>>,
2022-10-20 09:17:20 +03:00
) -> FrontendResult {
2022-10-27 01:29:38 +03:00
let (user, _semaphore) = app.bearer_is_authorized(bearer).await?;
2022-10-26 00:10:05 +03:00
let chain_id = get_chain_id_from_params(app.as_ref(), &params)?;
let query_start = get_query_start_from_params(&params)?;
let page = get_page_from_params(&params)?;
// TODO: page size from config
let page_size = 200;
let mut response = HashMap::new();
response.insert("page", json!(page));
response.insert("page_size", json!(page_size));
response.insert("chain_id", json!(chain_id));
response.insert("query_start", json!(query_start.timestamp() as u64));
let db_conn = app.db_conn().context("getting db for user's revert logs")?;
2022-10-27 03:12:42 +03:00
let uks = rpc_keys::Entity::find()
.filter(rpc_keys::Column::UserId.eq(user.id))
2022-10-26 00:10:05 +03:00
.all(&db_conn)
.await
.context("failed loading user's key")?;
// TODO: only select the ids
let uks: Vec<_> = uks.into_iter().map(|x| x.id).collect();
// get paginated logs
let q = revert_logs::Entity::find()
.filter(revert_logs::Column::Timestamp.gte(query_start))
2022-10-27 03:12:42 +03:00
.filter(revert_logs::Column::RpcKeyId.is_in(uks))
2022-10-26 00:10:05 +03:00
.order_by_asc(revert_logs::Column::Timestamp);
let q = if chain_id == 0 {
// don't do anything
q
} else {
// filter on chain id
q.filter(revert_logs::Column::ChainId.eq(chain_id))
};
let revert_logs = q.paginate(&db_conn, page_size).fetch_page(page).await?;
response.insert("revert_logs", json!(revert_logs));
Ok(Json(response).into_response())
2022-10-20 09:17:20 +03:00
}
2022-10-20 02:02:34 +03:00
/// `GET /user/stats/detailed` -- Use a bearer token to get the user's key stats such as bandwidth used and methods requested.
///
2022-10-20 07:44:33 +03:00
/// If no bearer is provided, detailed stats for all users will be shown.
/// View a single user with `?user_id=$x`.
/// View a single chain with `?chain_id=$x`.
2022-10-18 00:47:58 +03:00
///
2022-10-20 07:44:33 +03:00
/// Set `$x` to zero to see all.
2022-10-18 00:47:58 +03:00
///
/// TODO: this will change as we add better support for secondary users.
#[debug_handler]
2022-10-29 01:52:47 +03:00
#[instrument(level = "trace")]
2022-10-20 02:02:34 +03:00
pub async fn user_stats_detailed_get(
2022-10-18 00:47:58 +03:00
Extension(app): Extension<Arc<Web3ProxyApp>>,
2022-10-21 01:51:56 +03:00
bearer: Option<TypedHeader<Authorization<Bearer>>>,
2022-10-20 00:34:05 +03:00
Query(params): Query<HashMap<String, String>>,
2022-10-18 00:47:58 +03:00
) -> FrontendResult {
2022-10-20 09:54:45 +03:00
let x = get_detailed_stats(&app, bearer, params).await?;
2022-10-20 00:34:05 +03:00
Ok(Json(x).into_response())
2022-10-18 00:47:58 +03:00
}
2022-10-19 03:56:57 +03:00
/// `GET /user/stats/aggregate` -- Public endpoint for aggregate stats such as bandwidth used and methods requested.
#[debug_handler]
2022-10-29 01:52:47 +03:00
#[instrument(level = "trace")]
2022-10-19 03:56:57 +03:00
pub async fn user_stats_aggregate_get(
bearer: Option<TypedHeader<Authorization<Bearer>>>,
2022-10-19 03:56:57 +03:00
Extension(app): Extension<Arc<Web3ProxyApp>>,
Query(params): Query<HashMap<String, String>>,
2022-10-19 03:56:57 +03:00
) -> FrontendResult {
2022-10-20 09:17:20 +03:00
let x = get_aggregate_rpc_stats_from_params(&app, bearer, params).await?;
2022-10-19 03:56:57 +03:00
Ok(Json(x).into_response())
}