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
View File

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

View File

@ -50,8 +50,10 @@ siwe = "0.4.1"
sea-orm = { version = "0.9.1", features = ["macros"] }
serde = { version = "1.0.143", features = [] }
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"] }
# 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"] }
toml = "0.5.9"
tower = "0.4.13"

View File

@ -5,6 +5,7 @@ use axum::{
Json,
};
use derive_more::From;
use redis_rate_limit::{bb8::RunError, RedisError};
use serde_json::value::RawValue;
use std::error::Error;
@ -14,12 +15,32 @@ pub type FrontendResult = Result<Response, FrontendErrorResponse>;
#[derive(From)]
pub enum FrontendErrorResponse {
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 {
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()
}
}

View File

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

View File

@ -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("/health", get(http::health))
.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(
"/foo",

View File

@ -21,11 +21,14 @@ use axum_client_ip::ClientIp;
use axum_macros::debug_handler;
use entities::user;
use ethers::{prelude::Address, types::Bytes};
use redis_rate_limit::redis::{pipe, AsyncCommands};
use redis_rate_limit::redis::AsyncCommands;
use reqwest::StatusCode;
use sea_orm::ActiveModelTrait;
use serde::Deserialize;
use siwe::Message;
use std::ops::Add;
use std::sync::Arc;
use time::{Duration, OffsetDateTime};
use uuid::Uuid;
// 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(
Extension(app): Extension<Arc<Web3ProxyApp>>,
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>,
) -> FrontendResult {
// 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
// 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
.redis_pool
.as_ref()
.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?
let session_expiration_seconds = 300;
// 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?;
let reply: String = redis_conn
.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")
Ok(message.to_string().into_response())
}
#[debug_handler]