it compiles
This commit is contained in:
parent
d9be55f83e
commit
5af834d710
12
Cargo.lock
generated
12
Cargo.lock
generated
@ -5090,7 +5090,7 @@ dependencies = [
|
||||
"http",
|
||||
"tower-layer",
|
||||
"tower-service",
|
||||
"ulid",
|
||||
"ulid 0.4.1",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@ -5235,6 +5235,15 @@ dependencies = [
|
||||
"rand 0.6.5",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "ulid"
|
||||
version = "1.0.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "13a3aaa69b04e5b66cc27309710a569ea23593612387d67daaf102e73aa974fd"
|
||||
dependencies = [
|
||||
"rand 0.8.5",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "unicode-bidi"
|
||||
version = "0.3.8"
|
||||
@ -5525,6 +5534,7 @@ dependencies = [
|
||||
"tower-request-id",
|
||||
"tracing",
|
||||
"tracing-subscriber",
|
||||
"ulid 1.0.0",
|
||||
"url",
|
||||
"uuid 1.1.2",
|
||||
]
|
||||
|
@ -62,6 +62,7 @@ tower-http = { version = "0.3.4", features = ["trace"] }
|
||||
tracing = "0.1.36"
|
||||
# TODO: tracing-subscriber has serde and serde_json features that we might want to use
|
||||
tracing-subscriber = { version = "0.3.15", features = ["env-filter", "parking_lot"] }
|
||||
ulid = "1.0.0"
|
||||
url = "2.2.2"
|
||||
uuid = "1.1.2"
|
||||
|
||||
|
@ -215,6 +215,7 @@ mod tests {
|
||||
app: AppConfig {
|
||||
chain_id: 31337,
|
||||
db_url: None,
|
||||
invite_code: None,
|
||||
redis_url: None,
|
||||
public_rate_limit_per_minute: 0,
|
||||
response_cache_max_bytes: 10_usize.pow(7),
|
||||
|
@ -44,6 +44,7 @@ pub struct AppConfig {
|
||||
// TODO: better type for chain_id? max of `u64::MAX / 2 - 36` https://github.com/ethereum/EIPs/issues/2294
|
||||
pub chain_id: u64,
|
||||
pub db_url: Option<String>,
|
||||
pub invite_code: Option<String>,
|
||||
pub redis_url: Option<String>,
|
||||
#[serde(default = "default_public_rate_limit_per_minute")]
|
||||
pub public_rate_limit_per_minute: u64,
|
||||
|
@ -1,4 +1,4 @@
|
||||
/// this should move into web3_proxy once the basics are working
|
||||
mod axum_ext;
|
||||
mod errors;
|
||||
mod http;
|
||||
mod http_proxy;
|
||||
@ -55,32 +55,25 @@ pub async fn serve(port: u16, proxy_app: Arc<Web3ProxyApp>) -> anyhow::Result<()
|
||||
)
|
||||
});
|
||||
|
||||
/*
|
||||
let some_fallible_service = tower::service_fn(|_req| async {
|
||||
// thing_that_might_fail().await?;
|
||||
Ok::<_, anyhow::Error>(Response::new(Body::empty()))
|
||||
});
|
||||
*/
|
||||
|
||||
// build our application with a route
|
||||
// order most to least common
|
||||
// build our axum Router
|
||||
let app = Router::new()
|
||||
// routes should be order most to least common
|
||||
.route("/", post(http_proxy::public_proxy_web3_rpc))
|
||||
.route("/", get(ws_proxy::public_websocket_handler))
|
||||
.route("/u/:user_key", post(http_proxy::user_proxy_web3_rpc))
|
||||
.route("/u/:user_key", get(ws_proxy::user_websocket_handler))
|
||||
.route("/health", get(http::health))
|
||||
// TODO: we probably want to remove /status in favor of the separate prometheus thread
|
||||
.route("/status", get(http::status))
|
||||
.route("/login/:user_address", get(users::get_login))
|
||||
.route("/login/:user_address/:message_eip", get(users::get_login))
|
||||
.route("/users", post(users::create_user))
|
||||
// .route(
|
||||
// "/foo",
|
||||
// HandleError::new(some_fallible_service, handle_anyhow_error),
|
||||
// )
|
||||
.route("/users", post(users::post_user))
|
||||
// layers are ordered bottom up
|
||||
// the last layer is first for requests and last for responses
|
||||
.layer(Extension(proxy_app))
|
||||
// create a unique id for each request and add it to our tracing logs
|
||||
// add the request id to our tracing logs
|
||||
.layer(request_tracing_layer)
|
||||
// create a unique id for each request
|
||||
.layer(RequestIdLayer)
|
||||
// 404 for any unknown routes
|
||||
.fallback(errors::handler_404.into_service());
|
||||
|
@ -13,7 +13,7 @@ use super::{
|
||||
};
|
||||
use crate::app::Web3ProxyApp;
|
||||
use axum::{
|
||||
extract::Path,
|
||||
extract::{Path, Query},
|
||||
response::{IntoResponse, Response},
|
||||
Extension, Json,
|
||||
};
|
||||
@ -30,7 +30,10 @@ use siwe::Message;
|
||||
use std::ops::Add;
|
||||
use std::sync::Arc;
|
||||
use time::{Duration, OffsetDateTime};
|
||||
use uuid::Uuid;
|
||||
use ulid::Ulid;
|
||||
|
||||
#[allow(unused)]
|
||||
use super::axum_ext::empty_string_as_none;
|
||||
|
||||
// TODO: how do we customize axum's error response? I think we probably want an enum that implements IntoResponse instead
|
||||
#[debug_handler]
|
||||
@ -52,15 +55,16 @@ pub async fn get_login(
|
||||
};
|
||||
|
||||
// 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
|
||||
// but theres no need to separate the registration 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
|
||||
|
||||
// create a message and save it in redis
|
||||
|
||||
// TODO: how many seconds? get from config?
|
||||
let expire_seconds: usize = 300;
|
||||
|
||||
// create a message and save it in redis
|
||||
let nonce = Uuid::new_v4();
|
||||
let nonce = Ulid::new();
|
||||
|
||||
let issued_at = OffsetDateTime::now_utc();
|
||||
|
||||
@ -88,18 +92,21 @@ pub async fn get_login(
|
||||
let session_key = format!("pending:{}", nonce);
|
||||
|
||||
// TODO: if no redis server, store in local cache?
|
||||
let redis_pool = app
|
||||
let mut redis_conn = app
|
||||
.redis_pool
|
||||
.as_ref()
|
||||
.expect("login requires a redis server");
|
||||
.expect("login requires a redis server")
|
||||
.get()
|
||||
.await?;
|
||||
|
||||
let mut redis_conn = redis_pool.get().await?;
|
||||
|
||||
// TODO: the address isn't enough. we need to save the actual message
|
||||
// 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_string
|
||||
redis_conn
|
||||
.set_ex(session_key, message.to_string(), expire_seconds)
|
||||
.await?;
|
||||
|
||||
drop(redis_conn);
|
||||
|
||||
// there are multiple ways to sign messages and not all wallets support them
|
||||
let message_eip = params
|
||||
.remove("message_eip")
|
||||
@ -110,21 +117,38 @@ pub async fn get_login(
|
||||
// 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"),
|
||||
_ => return Err(anyhow::anyhow!("invalid message eip given").into()),
|
||||
};
|
||||
|
||||
Ok(message.into_response())
|
||||
}
|
||||
|
||||
/// Query params to our `post_login` handler.
|
||||
#[derive(Debug, Deserialize)]
|
||||
pub struct PostLoginQuery {
|
||||
invite_code: Option<String>,
|
||||
}
|
||||
|
||||
/// JSON body to our `post_login` handler.
|
||||
#[derive(Deserialize)]
|
||||
pub struct PostLogin {
|
||||
address: Address,
|
||||
msg: String,
|
||||
sig: Bytes,
|
||||
version: String,
|
||||
signer: String,
|
||||
}
|
||||
|
||||
#[debug_handler]
|
||||
pub async fn create_user(
|
||||
// this argument tells axum to parse the request body
|
||||
// as JSON into a `CreateUser` type
|
||||
Json(payload): Json<CreateUser>,
|
||||
Extension(app): Extension<Arc<Web3ProxyApp>>,
|
||||
/// Post to the user endpoint to register or login.
|
||||
pub async fn post_login(
|
||||
ClientIp(ip): ClientIp,
|
||||
Extension(app): Extension<Arc<Web3ProxyApp>>,
|
||||
Json(payload): Json<PostLogin>,
|
||||
Query(query): Query<PostLoginQuery>,
|
||||
) -> Response {
|
||||
// TODO: return a Result instead
|
||||
// TODO: dry this up ip checking up
|
||||
let _ip = match app.rate_limit_by_ip(ip).await {
|
||||
Ok(x) => match x.try_into_response().await {
|
||||
Ok(RateLimitResult::AllowedIp(x)) => x,
|
||||
@ -134,11 +158,22 @@ pub async fn create_user(
|
||||
Err(err) => return anyhow_error_into_response(None, None, err),
|
||||
};
|
||||
|
||||
// TODO: check invite_code against the app's config or database
|
||||
if payload.invite_code != "llam4n0des!" {
|
||||
todo!("proper error message")
|
||||
let mut new_user = true; // TODO: check the database
|
||||
|
||||
if let Some(invite_code) = &app.config.invite_code {
|
||||
// we don't do per-user referral codes because we shouldn't collect what we don't need.
|
||||
// we don't need to build a social graph between addresses like that.
|
||||
if query.invite_code.as_ref() != Some(invite_code) {
|
||||
todo!("if address is already registered, allow login! else, error")
|
||||
}
|
||||
}
|
||||
|
||||
// we can't trust that they didn't tamper with the message in some way
|
||||
let their_msg: siwe::Message = payload.msg.parse().unwrap();
|
||||
|
||||
let their_sig: [u8; 65] = payload.sig.as_ref().try_into().unwrap();
|
||||
|
||||
// fetch the message we gave them from our redis
|
||||
let redis_pool = app
|
||||
.redis_pool
|
||||
.as_ref()
|
||||
@ -148,60 +183,76 @@ pub async fn create_user(
|
||||
|
||||
// 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 our_msg: String = redis_conn.get(&their_msg.nonce).await.unwrap();
|
||||
|
||||
let message: Message = message.parse().unwrap();
|
||||
let our_msg: siwe::Message = our_msg.parse().unwrap();
|
||||
|
||||
// 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) {
|
||||
// check the domain and a nonce. let timestamp be automatic
|
||||
if let Err(e) = their_msg.verify(their_sig, Some(&our_msg.domain), Some(&our_msg.nonce), None) {
|
||||
// message cannot be correctly authenticated
|
||||
todo!("proper error message: {}", e)
|
||||
}
|
||||
|
||||
let user = user::ActiveModel {
|
||||
address: sea_orm::Set(payload.address.to_fixed_bytes().into()),
|
||||
email: sea_orm::Set(payload.email),
|
||||
..Default::default()
|
||||
};
|
||||
if new_user {
|
||||
// the only thing we need from them is an address
|
||||
// everything else is optional
|
||||
let user = user::ActiveModel {
|
||||
address: sea_orm::Set(payload.address.to_fixed_bytes().into()),
|
||||
..Default::default()
|
||||
};
|
||||
|
||||
let db = app.db_conn.as_ref().unwrap();
|
||||
let db = app.db_conn.as_ref().unwrap();
|
||||
|
||||
// TODO: proper error message
|
||||
let user = user.insert(db).await.unwrap();
|
||||
let user = user.insert(db).await.unwrap();
|
||||
|
||||
// TODO: create
|
||||
let api_key = todo!();
|
||||
let api_key = todo!("create an api key");
|
||||
|
||||
/*
|
||||
let rpm = app.config.something;
|
||||
/*
|
||||
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()
|
||||
};
|
||||
// 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")?;
|
||||
// TODO: if this fails, rever adding the user, too
|
||||
let uk = uk.save(&txn).await.context("Failed saving new user key")?;
|
||||
|
||||
// TODO: do not expose user ids
|
||||
(StatusCode::CREATED, Json(user)).into_response()
|
||||
*/
|
||||
// TODO: set a cookie?
|
||||
|
||||
// TODO: do not expose user ids
|
||||
(StatusCode::CREATED, Json(user)).into_response()
|
||||
*/
|
||||
} else {
|
||||
todo!("load existing user from the database");
|
||||
}
|
||||
}
|
||||
|
||||
// the input to our `create_user` handler
|
||||
/// the JSON input to the `post_user` handler
|
||||
#[derive(Deserialize)]
|
||||
pub struct CreateUser {
|
||||
pub struct PostUser {
|
||||
address: Address,
|
||||
// TODO: make sure the email address is valid
|
||||
// TODO: make sure the email address is valid. probably have a "verified" column in the database
|
||||
email: Option<String>,
|
||||
signature: Bytes,
|
||||
nonce: Uuid,
|
||||
invite_code: String,
|
||||
// TODO: make them sign this JSON? cookie in session id is hard because its on a different domain
|
||||
}
|
||||
|
||||
#[debug_handler]
|
||||
/// post to the user endpoint to modify your account
|
||||
pub async fn post_user(
|
||||
Json(payload): Json<PostUser>,
|
||||
Extension(app): Extension<Arc<Web3ProxyApp>>,
|
||||
ClientIp(ip): ClientIp,
|
||||
) -> FrontendResult {
|
||||
todo!("finish post_login");
|
||||
|
||||
// let user = user::ActiveModel {
|
||||
// address: sea_orm::Set(payload.address.to_fixed_bytes().into()),
|
||||
// email: sea_orm::Set(payload.email),
|
||||
// ..Default::default()
|
||||
// };
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user