half the login page and better error handling

This commit is contained in:
Bryan Stitt 2022-08-16 22:52:12 +00:00
parent 0ccda2f40b
commit 115657e97c
6 changed files with 71 additions and 21 deletions

1
Cargo.lock generated

@ -5525,6 +5525,7 @@ dependencies = [
"serde", "serde",
"serde_json", "serde_json",
"siwe", "siwe",
"time 0.3.11",
"tokio", "tokio",
"tokio-stream", "tokio-stream",
"toml", "toml",

@ -50,8 +50,10 @@ siwe = "0.4.1"
sea-orm = { version = "0.9.1", features = ["macros"] } sea-orm = { version = "0.9.1", features = ["macros"] }
serde = { version = "1.0.143", features = [] } serde = { version = "1.0.143", features = [] }
serde_json = { version = "1.0.83", default-features = false, features = ["alloc", "raw_value"] } serde_json = { version = "1.0.83", default-features = false, features = ["alloc", "raw_value"] }
# TODO: make sure this time version matches siwe. PR to put this in their prelude
time = "0.3.11"
tokio = { version = "1.20.1", features = ["full", "tracing"] } tokio = { version = "1.20.1", features = ["full", "tracing"] }
# TODO: make sure this uuid version matches what is in sea orm. PR on sea orm to put builder into prelude # TODO: make sure this uuid version matches sea-orm. PR to put this in their prelude
tokio-stream = { version = "0.1.9", features = ["sync"] } tokio-stream = { version = "0.1.9", features = ["sync"] }
toml = "0.5.9" toml = "0.5.9"
tower = "0.4.13" tower = "0.4.13"

@ -5,6 +5,7 @@ use axum::{
Json, Json,
}; };
use derive_more::From; use derive_more::From;
use redis_rate_limit::{bb8::RunError, RedisError};
use serde_json::value::RawValue; use serde_json::value::RawValue;
use std::error::Error; use std::error::Error;
@ -14,12 +15,32 @@ pub type FrontendResult = Result<Response, FrontendErrorResponse>;
#[derive(From)] #[derive(From)]
pub enum FrontendErrorResponse { pub enum FrontendErrorResponse {
Anyhow(anyhow::Error), Anyhow(anyhow::Error),
BoxError(Box<dyn Error>), Box(Box<dyn Error>),
// TODO: should we box these instead?
Redis(RedisError),
RedisRunError(RunError<RedisError>),
} }
impl IntoResponse for FrontendErrorResponse { impl IntoResponse for FrontendErrorResponse {
fn into_response(self) -> Response { fn into_response(self) -> Response {
todo!("into_response based on the error type") let null_id = RawValue::from_string("null".to_string()).unwrap();
// TODO: think more about this. this match should probably give us http and jsonrpc codes
let err = match self {
Self::Anyhow(err) => err,
Self::Box(err) => anyhow::anyhow!("Boxed error: {:?}", err),
Self::Redis(err) => err.into(),
Self::RedisRunError(err) => err.into(),
};
let err = JsonRpcForwardedResponse::from_anyhow_error(err, null_id);
let code = StatusCode::INTERNAL_SERVER_ERROR;
// TODO: logs here are too verbose. emit a stat instead? or maybe only log internal errors?
// warn!("Responding with error: {:?}", err);
(code, Json(err)).into_response()
} }
} }

@ -15,6 +15,7 @@ pub async fn public_proxy_web3_rpc(
Extension(app): Extension<Arc<Web3ProxyApp>>, Extension(app): Extension<Arc<Web3ProxyApp>>,
ClientIp(ip): ClientIp, ClientIp(ip): ClientIp,
) -> Response { ) -> Response {
// TODO: dry this up a lot
let _ip = match app.rate_limit_by_ip(ip).await { let _ip = match app.rate_limit_by_ip(ip).await {
Ok(x) => match x.try_into_response().await { Ok(x) => match x.try_into_response().await {
Ok(RateLimitResult::AllowedIp(x)) => x, Ok(RateLimitResult::AllowedIp(x)) => x,

@ -68,7 +68,7 @@ pub async fn serve(port: u16, proxy_app: Arc<Web3ProxyApp>) -> anyhow::Result<()
.route("/u/:user_key", get(ws_proxy::user_websocket_handler)) .route("/u/:user_key", get(ws_proxy::user_websocket_handler))
.route("/health", get(http::health)) .route("/health", get(http::health))
.route("/status", get(http::status)) .route("/status", get(http::status))
.route("/login", get(users::get_login)) .route("/login/:user_address", get(users::get_login))
.route("/users", post(users::create_user)) .route("/users", post(users::create_user))
.route( .route(
"/foo", "/foo",

@ -21,11 +21,14 @@ use axum_client_ip::ClientIp;
use axum_macros::debug_handler; use axum_macros::debug_handler;
use entities::user; use entities::user;
use ethers::{prelude::Address, types::Bytes}; use ethers::{prelude::Address, types::Bytes};
use redis_rate_limit::redis::{pipe, AsyncCommands}; use redis_rate_limit::redis::AsyncCommands;
use reqwest::StatusCode; use reqwest::StatusCode;
use sea_orm::ActiveModelTrait; use sea_orm::ActiveModelTrait;
use serde::Deserialize; use serde::Deserialize;
use siwe::Message;
use std::ops::Add;
use std::sync::Arc; use std::sync::Arc;
use time::{Duration, OffsetDateTime};
use uuid::Uuid; use uuid::Uuid;
// TODO: how do we customize axum's error response? I think we probably want an enum that implements IntoResponse instead // TODO: how do we customize axum's error response? I think we probably want an enum that implements IntoResponse instead
@ -33,7 +36,8 @@ use uuid::Uuid;
pub async fn get_login( pub async fn get_login(
Extension(app): Extension<Arc<Web3ProxyApp>>, Extension(app): Extension<Arc<Web3ProxyApp>>,
ClientIp(ip): ClientIp, ClientIp(ip): ClientIp,
// TODO: what does axum's error handling look like? // TODO: what does axum's error handling look like if the path fails to parse?
// TODO: allow ENS names here?
Path(user_address): Path<Address>, Path(user_address): Path<Address>,
) -> FrontendResult { ) -> FrontendResult {
// TODO: refactor this to use the try operator // TODO: refactor this to use the try operator
@ -51,29 +55,50 @@ pub async fn get_login(
// its a better UX to just click "login with ethereum" and have the account created if it doesn't exist // 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 // we can prompt for an email and and payment after they log in
let session_id = uuid::Uuid::new_v4(); // TODO: how many seconds? get from config?
let expire_seconds: usize = 300;
// TODO: if no redis, store in local cache? // create a session id and save it in redis
let nonce = Uuid::new_v4();
let issued_at = OffsetDateTime::now_utc();
let expiration_time = issued_at.add(Duration::new(expire_seconds as i64, 0));
// TODO: get request_id out of the trace? do we need that when we have a none?
// 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?
let redis_pool = app let redis_pool = app
.redis_pool .redis_pool
.as_ref() .as_ref()
.expect("login requires a redis server"); .expect("login requires a redis server");
let mut redis_conn = redis_pool.get().await.unwrap(); let mut redis_conn = redis_pool.get().await?;
// TODO: how many seconds? get from config? // TODO: the address isn't enough. we need to save the actual message
let session_expiration_seconds = 300; redis_conn
.set_ex(session_key, message.to_string(), expire_seconds)
.await?;
let reply: String = redis_conn Ok(message.to_string().into_response())
.set_ex(
session_id.to_string(),
user_address.to_string(),
session_expiration_seconds,
)
.await
.unwrap();
todo!("how should this work? probably keep stuff in redis")
} }
#[debug_handler] #[debug_handler]