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 } ;
2022-08-21 12:44:53 +03:00
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-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 } ,
2022-09-24 00:46:27 +03:00
headers ::{ authorization ::Bearer , Authorization } ,
2022-08-21 12:44:53 +03:00
response ::IntoResponse ,
2022-09-24 00:46:27 +03:00
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-26 00:10:05 +03:00
use entities ::{ revert_logs , user , user_keys } ;
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 ;
2022-10-07 05:15:53 +03:00
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 ;
2022-08-17 01:52:12 +03:00
use time ::{ Duration , OffsetDateTime } ;
2022-10-25 22:03:11 +03:00
use tracing ::warn ;
2022-08-21 11:18:57 +03:00
use ulid ::Ulid ;
2022-10-27 00:39:26 +03:00
use uuid ::Uuid ;
2022-08-21 11:18:57 +03:00
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-18 00:47:58 +03:00
pub async fn user_login_get (
2022-08-17 00:10:09 +03:00
Extension ( app ) : Extension < Arc < Web3ProxyApp > > ,
2022-08-17 00:43:39 +03:00
ClientIp ( ip ) : ClientIp ,
2022-08-17 01:52:12 +03:00
// TODO: what does axum's error handling look like if the path fails to parse?
2022-10-19 21:50:16 +03:00
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
2022-08-17 01:52:12 +03:00
// TODO: how many seconds? get from config?
2022-10-20 09:54:45 +03:00
let expire_seconds : usize = 20 * 60 ;
2022-08-17 01:52:12 +03:00
2022-08-21 11:18:57 +03:00
let nonce = Ulid ::new ( ) ;
2022-08-17 01:52:12 +03:00
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?
2022-10-19 21:50:16 +03:00
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 " ) ? ;
2022-08-17 01:52:12 +03:00
// TODO: get most of these from the app config
let message = Message {
2022-10-19 21:38:00 +03:00
// TODO: should domain be llamanodes, or llamarpc, or the subdomain of llamarpc?
2022-08-17 01:52:12 +03:00
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
2022-09-02 08:40:56 +03:00
// TODO: what message format is the most efficient to store in redis? probably eip191_bytes
2022-08-23 21:48:27 +03:00
// we add 1 to expire_seconds just to be sure redis has the key for the full expiration_time
2022-10-19 21:38:00 +03:00
// 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 ) ;
2022-08-23 21:48:27 +03:00
app . redis_conn ( )
. await ?
. set_ex ( session_key , message . to_string ( ) , expire_seconds + 1 )
2022-08-17 01:52:12 +03:00
. 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?
2022-10-19 21:50:16 +03:00
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 ( ) {
2022-09-02 08:40:56 +03:00
" 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-08-21 11:18:57 +03:00
#[ derive(Deserialize) ]
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.
2022-09-24 05:47:44 +03:00
#[ debug_handler ]
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 > > ,
2022-09-24 00:46:27 +03:00
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
2022-10-25 20:21:11 +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 {
2022-10-25 20:21:11 +03:00
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
2022-10-25 20:21:11 +03:00
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?
2022-10-25 20:21:11 +03:00
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 {
2022-10-25 20:21:11 +03:00
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
2022-10-25 20:21:11 +03:00
// 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
2022-10-25 20:21:11 +03:00
)
. 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
2022-10-25 20:21:11 +03:00
let ( u , uks , status_code ) = match u {
2022-08-27 08:42:25 +03:00
None = > {
2022-10-25 20:21:11 +03:00
// 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
2022-10-25 20:21:11 +03:00
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
2022-10-25 20:21:11 +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
2022-10-25 20:21:11 +03:00
// TODO: variable requests per minute depending on the invite code
2022-08-27 08:42:25 +03:00
let uk = user_keys ::ActiveModel {
user_id : sea_orm ::Set ( u . id ) ,
2022-10-26 00:10:05 +03:00
api_key : sea_orm ::Set ( rpc_key . into ( ) ) ,
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
2022-09-24 05:47:44 +03:00
let uks = vec! [ uk ] ;
2022-10-25 20:21:11 +03:00
// 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
2022-10-25 20:21:11 +03:00
( u , uks , StatusCode ::CREATED )
2022-08-27 08:42:25 +03:00
}
Some ( u ) = > {
// the user is already registered
2022-09-24 05:47:44 +03:00
let uks = user_keys ::Entity ::find ( )
2022-08-27 08:42:25 +03:00
. filter ( user_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
2022-09-24 05:47:44 +03:00
. context ( " failed loading user's key " ) ? ;
2022-08-27 08:42:25 +03:00
2022-10-25 20:21:11 +03:00
( u , uks , StatusCode ::OK )
}
} ;
2022-08-27 08:42:25 +03:00
2022-10-25 20:21:11 +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! ( {
" api_keys " : uks
2022-10-25 20:21:11 +03:00
. 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
2022-10-25 20:21:11 +03:00
let response = ( status_code , Json ( response_json ) ) . into_response ( ) ;
2022-09-24 05:47:44 +03:00
// add bearer to redis
2022-10-25 20:21:11 +03:00
// TODO: use a helper function/struct for this
2022-09-24 05:47:44 +03:00
let bearer_redis_key = format! ( " bearer: {} " , bearer_token ) ;
2022-09-03 22:43:19 +03:00
2022-09-24 05:47:44 +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
}
2022-08-23 21:56:19 +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.
2022-09-24 00:46:27 +03:00
#[ debug_handler ]
2022-10-18 00:47:58 +03:00
pub async fn user_logout_post (
2022-09-24 00:46:27 +03:00
Extension ( app ) : Extension < Arc < Web3ProxyApp > > ,
2022-09-24 05:47:44 +03:00
TypedHeader ( Authorization ( bearer ) ) : TypedHeader < Authorization < Bearer > > ,
2022-09-24 00:46:27 +03:00
) -> FrontendResult {
2022-09-24 07:31:06 +03:00
let mut redis_conn = app . redis_conn ( ) . await ? ;
2022-09-24 05:47:44 +03:00
// TODO: i don't like this. move this to a helper function so it is less fragile
let bearer_cache_key = format! ( " bearer: {} " , bearer . token ( ) ) ;
2022-09-24 00:46:27 +03:00
2022-09-24 05:47:44 +03:00
redis_conn . del ( bearer_cache_key ) . await ? ;
2022-09-24 00:46:27 +03:00
// 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 ]
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 ( ) )
}
2022-10-25 21:26:58 +03:00
/// the JSON input to the `post_user` handler.
2022-07-14 00:49:57 +03:00
#[ derive(Deserialize) ]
2022-10-26 03:22:58 +03:00
pub struct UserPost {
2022-08-21 11:18:57 +03:00
// TODO: make sure the email address is valid. probably have a "verified" column in the database
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 ]
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)
///
/// TODO: one key per request? maybe /user/balance/:api_key?
/// TODO: this will change as we add better support for secondary users.
#[ debug_handler ]
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 {
todo! ( " user_balance_get " ) ;
}
2022-10-25 21:26:58 +03:00
/// `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
/// TODO: one key per request? maybe /user/balance/:api_key?
/// TODO: this will change as we add better support for secondary users.
#[ debug_handler ]
pub async fn user_balance_post (
Extension ( app ) : Extension < Arc < Web3ProxyApp > > ,
2022-10-21 01:51:56 +03:00
TypedHeader ( Authorization ( bearer_token ) ) : TypedHeader < Authorization < Bearer > > ,
2022-10-18 00:47:58 +03:00
) -> FrontendResult {
todo! ( " user_balance_post " ) ;
}
/// `GET /user/keys` -- Use a bearer token to get the user's api keys and their settings.
///
/// TODO: one key per request? maybe /user/keys/:api_key?
#[ debug_handler ]
pub async fn user_keys_get (
Extension ( app ) : Extension < Arc < Web3ProxyApp > > ,
2022-10-21 01:51:56 +03:00
TypedHeader ( Authorization ( bearer_token ) ) : TypedHeader < Authorization < Bearer > > ,
2022-10-18 00:47:58 +03:00
) -> FrontendResult {
2022-10-26 00:10:05 +03:00
let ( user , _semaphore ) = app . bearer_is_authorized ( bearer_token ) . await ? ;
2022-10-25 21:26:58 +03:00
2022-10-25 22:03:11 +03:00
let db_conn = app . db_conn ( ) . context ( " getting db to fetch user's keys " ) ? ;
let uks = user_keys ::Entity ::find ( )
. filter ( user_keys ::Column ::UserId . eq ( user . id ) )
. 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-26 03:22:58 +03:00
/// the JSON input to the `user_keys_post` handler.
#[ derive(Deserialize) ]
pub struct UserKeysPost {
// TODO: make sure the email address is valid. probably have a "verified" column in the database
existing_key_id : Option < u64 > ,
existing_key : Option < RpcApiKey > ,
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-18 00:47:58 +03:00
/// `POST /user/keys` -- Use a bearer token to create a new key or modify an existing key.
///
/// TODO: read json from the request body
/// TODO: one key per request? maybe /user/keys/:api_key?
#[ debug_handler ]
pub async fn user_keys_post (
Extension ( app ) : Extension < Arc < Web3ProxyApp > > ,
2022-10-21 01:51:56 +03:00
TypedHeader ( Authorization ( bearer_token ) ) : TypedHeader < Authorization < Bearer > > ,
2022-10-26 03:22:58 +03:00
Json ( payload ) : Json < UserKeysPost > ,
2022-10-26 00:10:05 +03:00
) -> FrontendResult {
let ( user , _semaphore ) = app . bearer_is_authorized ( bearer_token ) . await ? ;
2022-10-27 00:39:26 +03:00
let db_conn = app . db_conn ( ) . context ( " getting db for user's keys " ) ? ;
let mut uk = if let Some ( existing_key_id ) = payload . existing_key_id {
2022-10-26 03:22:58 +03:00
// get the key and make sure it belongs to the user
2022-10-27 00:39:26 +03:00
let uk = user_keys ::Entity ::find ( )
. filter ( user_keys ::Column ::UserId . eq ( user . id ) )
. filter ( user_keys ::Column ::Id . eq ( existing_key_id ) )
. 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 03:22:58 +03:00
} else if let Some ( existing_key ) = payload . existing_key {
// get the key and make sure it belongs to the user
2022-10-27 00:39:26 +03:00
let uk = user_keys ::Entity ::find ( )
. filter ( user_keys ::Column ::UserId . eq ( user . id ) )
. filter ( user_keys ::Column ::ApiKey . eq ( Uuid ::from ( existing_key ) ) )
. 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 00:39:26 +03:00
user_keys ::ActiveModel {
user_id : sea_orm ::Set ( user . id ) ,
api_key : sea_orm ::Set ( rpc_key . into ( ) ) ,
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
} ;
let uk : user_keys ::Model = uk . try_into ( ) ? ;
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 ]
pub async fn user_revert_logs_get (
Extension ( app ) : Extension < Arc < Web3ProxyApp > > ,
2022-10-21 01:51:56 +03:00
TypedHeader ( Authorization ( bearer_token ) ) : 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-26 00:10:05 +03:00
let ( user , _semaphore ) = app . bearer_is_authorized ( bearer_token ) . await ? ;
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 " ) ? ;
let uks = user_keys ::Entity ::find ( )
. filter ( user_keys ::Column ::UserId . eq ( user . id ) )
. 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 ) )
. filter ( revert_logs ::Column ::UserKeyId . is_in ( uks ) )
. 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-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 ]
pub async fn user_stats_aggregate_get (
2022-10-20 01:20:34 +03:00
bearer : Option < TypedHeader < Authorization < Bearer > > > ,
2022-10-19 03:56:57 +03:00
Extension ( app ) : Extension < Arc < Web3ProxyApp > > ,
2022-10-19 21:38:00 +03:00
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 ( ) )
}