2022-07-14 00:49:57 +03:00
// So the API needs to show for any given user:
// - show balance in USD
// - show deposits history (currency, amounts, transaction id)
// - show number of requests used (so we can calculate average spending over a month, burn rate for a user etc, something like "Your balance will be depleted in xx days)
// - the email address of a user if he opted in to get contacted via email
// - all the monitoring and stats but that will come from someplace else if I understand corectly?
// I wonder how we handle payment
// probably have to do manual withdrawals
2022-08-17 00:43:39 +03:00
use super ::{
errors ::{ anyhow_error_into_response , FrontendResult } ,
rate_limit ::RateLimitResult ,
} ;
2022-08-16 22:29:00 +03:00
use crate ::app ::Web3ProxyApp ;
2022-08-11 04:53:27 +03:00
use axum ::{
2022-08-17 00:43:39 +03:00
extract ::Path ,
response ::{ IntoResponse , Response } ,
2022-08-11 04:53:27 +03:00
Extension , Json ,
} ;
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-08-17 02:03:50 +03:00
use entities ::{ 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-08-17 01:52:12 +03:00
use redis_rate_limit ::redis ::AsyncCommands ;
2022-08-11 04:53:27 +03:00
use reqwest ::StatusCode ;
2022-08-04 04:10:27 +03:00
use sea_orm ::ActiveModelTrait ;
use serde ::Deserialize ;
2022-08-17 01:52:12 +03:00
use siwe ::Message ;
use std ::ops ::Add ;
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-08-17 00:43:39 +03:00
use uuid ::Uuid ;
2022-08-04 02:17:02 +03:00
2022-08-17 00:10:09 +03:00
// TODO: how do we customize axum's error response? I think we probably want an enum that implements IntoResponse instead
2022-08-16 22:29:00 +03:00
#[ debug_handler ]
2022-08-17 00:10:09 +03:00
pub async fn get_login (
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?
// TODO: allow ENS names here?
2022-08-19 23:18:12 +03:00
Path ( mut params ) : Path < HashMap < String , String > > ,
2022-08-17 00:43:39 +03:00
) -> FrontendResult {
// TODO: refactor this to use the try operator
let _ip = match app . rate_limit_by_ip ( ip ) . await {
Ok ( x ) = > match x . try_into_response ( ) . await {
Ok ( RateLimitResult ::AllowedIp ( x ) ) = > x ,
Err ( err_response ) = > return Ok ( err_response ) ,
_ = > unimplemented! ( ) ,
} ,
Err ( err ) = > return Ok ( anyhow_error_into_response ( None , None , err ) ) ,
} ;
// at first i thought about checking that user_address is in our db
// but theres no need to separate the create_user and login flows
// its 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-17 01:52:12 +03:00
// TODO: how many seconds? get from config?
let expire_seconds : usize = 300 ;
2022-08-17 02:03:50 +03:00
// create a message and save it in redis
2022-08-17 01:52:12 +03:00
let nonce = Uuid ::new_v4 ( ) ;
let issued_at = OffsetDateTime ::now_utc ( ) ;
let expiration_time = issued_at . add ( Duration ::new ( expire_seconds as i64 , 0 ) ) ;
2022-08-19 23:18:12 +03:00
// TODO: proper errors. the first unwrap should be impossible, but the second will happen with bad input
let user_address : Address = params . remove ( " user_address " ) . unwrap ( ) . parse ( ) . unwrap ( ) ;
2022-08-17 00:43:39 +03:00
2022-08-17 01:52:12 +03:00
// TODO: get most of these from the app config
let message = Message {
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 ! [ ] ,
} ;
let session_key = format! ( " pending: {} " , nonce ) ;
// TODO: if no redis server, store in local cache?
2022-08-17 00:43:39 +03:00
let redis_pool = app
. redis_pool
. as_ref ( )
. expect ( " login requires a redis server " ) ;
2022-08-17 01:52:12 +03:00
let mut redis_conn = redis_pool . get ( ) . await ? ;
2022-08-17 00:43:39 +03:00
2022-08-17 01:52:12 +03:00
// TODO: the address isn't enough. we need to save the actual message
redis_conn
. set_ex ( session_key , message . to_string ( ) , expire_seconds )
. await ? ;
2022-08-19 23:18:12 +03:00
// there are multiple ways to sign messages and not all wallets support them
let message_eip = params
. remove ( " message_eip " )
. unwrap_or_else ( | | " eip4361 " . to_string ( ) ) ;
let message : String = match message_eip . as_str ( ) {
" eip4361 " = > message . to_string ( ) ,
// https://github.com/spruceid/siwe/issues/98
" eip191_string " = > Bytes ::from ( message . eip191_string ( ) . unwrap ( ) ) . to_string ( ) ,
" eip191_hash " = > Bytes ::from ( & message . eip191_hash ( ) . unwrap ( ) ) . to_string ( ) ,
_ = > todo! ( " return a proper error " ) ,
} ;
Ok ( message . into_response ( ) )
2022-08-16 22:29:00 +03:00
}
2022-08-04 02:17:02 +03:00
2022-08-17 00:10:09 +03:00
#[ debug_handler ]
2022-07-14 00:49:57 +03:00
pub async fn create_user (
// this argument tells axum to parse the request body
// as JSON into a `CreateUser` type
Json ( payload ) : Json < CreateUser > ,
2022-08-04 04:10:27 +03:00
Extension ( app ) : Extension < Arc < Web3ProxyApp > > ,
ClientIp ( ip ) : ClientIp ,
2022-08-11 04:53:27 +03:00
) -> Response {
2022-08-16 22:29:00 +03:00
// TODO: return a Result instead
2022-08-11 04:53:27 +03:00
let _ip = match app . rate_limit_by_ip ( ip ) . await {
Ok ( x ) = > match x . try_into_response ( ) . await {
Ok ( RateLimitResult ::AllowedIp ( x ) ) = > x ,
Err ( err_response ) = > return err_response ,
_ = > unimplemented! ( ) ,
} ,
2022-08-16 22:29:00 +03:00
Err ( err ) = > return anyhow_error_into_response ( None , None , err ) ,
2022-08-11 04:53:27 +03:00
} ;
2022-08-04 04:10:27 +03:00
2022-08-10 05:37:34 +03:00
// TODO: check invite_code against the app's config or database
2022-08-04 04:10:27 +03:00
if payload . invite_code ! = " llam4n0des! " {
todo! ( " proper error message " )
}
2022-08-17 02:03:50 +03:00
let redis_pool = app
. redis_pool
. as_ref ( )
. expect ( " login requires a redis server " ) ;
let mut redis_conn = redis_pool . get ( ) . await . unwrap ( ) ;
// TODO: use getdel
// TODO: do not unwrap. make this function return a FrontendResult
let message : String = redis_conn . get ( payload . nonce . to_string ( ) ) . await . unwrap ( ) ;
let message : Message = message . parse ( ) . unwrap ( ) ;
2022-08-04 04:10:27 +03:00
// TODO: dont unwrap. proper error
let signature : [ u8 ; 65 ] = payload . signature . as_ref ( ) . try_into ( ) . unwrap ( ) ;
// TODO: calculate the expected message for the current user. include domain and a nonce. let timestamp be automatic
if let Err ( e ) = message . verify ( signature , None , None , None ) {
// message cannot be correctly authenticated
todo! ( " proper error message: {} " , e )
}
2022-08-04 02:17:02 +03:00
let user = user ::ActiveModel {
2022-08-06 03:07:12 +03:00
address : sea_orm ::Set ( payload . address . to_fixed_bytes ( ) . into ( ) ) ,
2022-08-04 04:10:27 +03:00
email : sea_orm ::Set ( payload . email ) ,
2022-08-04 02:17:02 +03:00
.. Default ::default ( )
2022-07-14 00:49:57 +03:00
} ;
2022-08-10 08:56:09 +03:00
let db = app . db_conn . as_ref ( ) . unwrap ( ) ;
2022-08-04 02:17:02 +03:00
2022-08-04 04:10:27 +03:00
// TODO: proper error message
let user = user . insert ( db ) . await . unwrap ( ) ;
2022-08-04 02:17:02 +03:00
2022-08-16 22:29:00 +03:00
// TODO: create
2022-08-17 02:03:50 +03:00
let api_key = todo! ( ) ;
/*
let rpm = app . config . something ;
// create a key for the new user
// TODO: requests_per_minute should be configurable
let uk = user_keys ::ActiveModel {
user_id : u . id ,
api_key : sea_orm ::Set ( api_key ) ,
requests_per_minute : sea_orm ::Set ( rpm ) ,
.. Default ::default ( )
} ;
// TODO: if this fails, rever adding the user, too
let uk = uk . save ( & txn ) . await . context ( " Failed saving new user key " ) ? ;
2022-08-16 20:47:04 +03:00
2022-08-11 04:53:27 +03:00
// TODO: do not expose user ids
( StatusCode ::CREATED , Json ( user ) ) . into_response ( )
2022-08-17 02:03:50 +03:00
* /
2022-07-14 00:49:57 +03:00
}
// the input to our `create_user` handler
#[ derive(Deserialize) ]
pub struct CreateUser {
2022-08-04 02:17:02 +03:00
address : Address ,
// TODO: make sure the email address is valid
2022-07-14 00:49:57 +03:00
email : Option < String > ,
signature : Bytes ,
2022-08-17 02:03:50 +03:00
nonce : Uuid ,
2022-08-04 02:17:02 +03:00
invite_code : String ,
2022-07-14 00:49:57 +03:00
}