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",
|
"http",
|
||||||
"tower-layer",
|
"tower-layer",
|
||||||
"tower-service",
|
"tower-service",
|
||||||
"ulid",
|
"ulid 0.4.1",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@ -5235,6 +5235,15 @@ dependencies = [
|
|||||||
"rand 0.6.5",
|
"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]]
|
[[package]]
|
||||||
name = "unicode-bidi"
|
name = "unicode-bidi"
|
||||||
version = "0.3.8"
|
version = "0.3.8"
|
||||||
@ -5525,6 +5534,7 @@ dependencies = [
|
|||||||
"tower-request-id",
|
"tower-request-id",
|
||||||
"tracing",
|
"tracing",
|
||||||
"tracing-subscriber",
|
"tracing-subscriber",
|
||||||
|
"ulid 1.0.0",
|
||||||
"url",
|
"url",
|
||||||
"uuid 1.1.2",
|
"uuid 1.1.2",
|
||||||
]
|
]
|
||||||
|
@ -62,6 +62,7 @@ tower-http = { version = "0.3.4", features = ["trace"] }
|
|||||||
tracing = "0.1.36"
|
tracing = "0.1.36"
|
||||||
# TODO: tracing-subscriber has serde and serde_json features that we might want to use
|
# 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"] }
|
tracing-subscriber = { version = "0.3.15", features = ["env-filter", "parking_lot"] }
|
||||||
|
ulid = "1.0.0"
|
||||||
url = "2.2.2"
|
url = "2.2.2"
|
||||||
uuid = "1.1.2"
|
uuid = "1.1.2"
|
||||||
|
|
||||||
|
@ -215,6 +215,7 @@ mod tests {
|
|||||||
app: AppConfig {
|
app: AppConfig {
|
||||||
chain_id: 31337,
|
chain_id: 31337,
|
||||||
db_url: None,
|
db_url: None,
|
||||||
|
invite_code: None,
|
||||||
redis_url: None,
|
redis_url: None,
|
||||||
public_rate_limit_per_minute: 0,
|
public_rate_limit_per_minute: 0,
|
||||||
response_cache_max_bytes: 10_usize.pow(7),
|
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
|
// TODO: better type for chain_id? max of `u64::MAX / 2 - 36` https://github.com/ethereum/EIPs/issues/2294
|
||||||
pub chain_id: u64,
|
pub chain_id: u64,
|
||||||
pub db_url: Option<String>,
|
pub db_url: Option<String>,
|
||||||
|
pub invite_code: Option<String>,
|
||||||
pub redis_url: Option<String>,
|
pub redis_url: Option<String>,
|
||||||
#[serde(default = "default_public_rate_limit_per_minute")]
|
#[serde(default = "default_public_rate_limit_per_minute")]
|
||||||
pub public_rate_limit_per_minute: u64,
|
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 errors;
|
||||||
mod http;
|
mod http;
|
||||||
mod http_proxy;
|
mod http_proxy;
|
||||||
@ -55,32 +55,25 @@ pub async fn serve(port: u16, proxy_app: Arc<Web3ProxyApp>) -> anyhow::Result<()
|
|||||||
)
|
)
|
||||||
});
|
});
|
||||||
|
|
||||||
/*
|
// build our axum Router
|
||||||
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
|
|
||||||
let app = Router::new()
|
let app = Router::new()
|
||||||
|
// routes should be order most to least common
|
||||||
.route("/", post(http_proxy::public_proxy_web3_rpc))
|
.route("/", post(http_proxy::public_proxy_web3_rpc))
|
||||||
.route("/", get(ws_proxy::public_websocket_handler))
|
.route("/", get(ws_proxy::public_websocket_handler))
|
||||||
.route("/u/:user_key", post(http_proxy::user_proxy_web3_rpc))
|
.route("/u/:user_key", post(http_proxy::user_proxy_web3_rpc))
|
||||||
.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))
|
||||||
|
// TODO: we probably want to remove /status in favor of the separate prometheus thread
|
||||||
.route("/status", get(http::status))
|
.route("/status", get(http::status))
|
||||||
.route("/login/:user_address", get(users::get_login))
|
.route("/login/:user_address", get(users::get_login))
|
||||||
.route("/login/:user_address/:message_eip", get(users::get_login))
|
.route("/login/:user_address/:message_eip", get(users::get_login))
|
||||||
.route("/users", post(users::create_user))
|
.route("/users", post(users::post_user))
|
||||||
// .route(
|
// layers are ordered bottom up
|
||||||
// "/foo",
|
// the last layer is first for requests and last for responses
|
||||||
// HandleError::new(some_fallible_service, handle_anyhow_error),
|
|
||||||
// )
|
|
||||||
.layer(Extension(proxy_app))
|
.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)
|
.layer(request_tracing_layer)
|
||||||
|
// create a unique id for each request
|
||||||
.layer(RequestIdLayer)
|
.layer(RequestIdLayer)
|
||||||
// 404 for any unknown routes
|
// 404 for any unknown routes
|
||||||
.fallback(errors::handler_404.into_service());
|
.fallback(errors::handler_404.into_service());
|
||||||
|
@ -13,7 +13,7 @@ use super::{
|
|||||||
};
|
};
|
||||||
use crate::app::Web3ProxyApp;
|
use crate::app::Web3ProxyApp;
|
||||||
use axum::{
|
use axum::{
|
||||||
extract::Path,
|
extract::{Path, Query},
|
||||||
response::{IntoResponse, Response},
|
response::{IntoResponse, Response},
|
||||||
Extension, Json,
|
Extension, Json,
|
||||||
};
|
};
|
||||||
@ -30,7 +30,10 @@ use siwe::Message;
|
|||||||
use std::ops::Add;
|
use std::ops::Add;
|
||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
use time::{Duration, OffsetDateTime};
|
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
|
// TODO: how do we customize axum's error response? I think we probably want an enum that implements IntoResponse instead
|
||||||
#[debug_handler]
|
#[debug_handler]
|
||||||
@ -52,15 +55,16 @@ pub async fn get_login(
|
|||||||
};
|
};
|
||||||
|
|
||||||
// at first i thought about checking that user_address is in our db
|
// 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
|
// 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
|
||||||
|
|
||||||
|
// create a message and save it in redis
|
||||||
|
|
||||||
// TODO: how many seconds? get from config?
|
// TODO: how many seconds? get from config?
|
||||||
let expire_seconds: usize = 300;
|
let expire_seconds: usize = 300;
|
||||||
|
|
||||||
// create a message and save it in redis
|
let nonce = Ulid::new();
|
||||||
let nonce = Uuid::new_v4();
|
|
||||||
|
|
||||||
let issued_at = OffsetDateTime::now_utc();
|
let issued_at = OffsetDateTime::now_utc();
|
||||||
|
|
||||||
@ -88,18 +92,21 @@ pub async fn get_login(
|
|||||||
let session_key = format!("pending:{}", nonce);
|
let session_key = format!("pending:{}", nonce);
|
||||||
|
|
||||||
// TODO: if no redis server, store in local cache?
|
// TODO: if no redis server, store in local cache?
|
||||||
let redis_pool = app
|
let mut redis_conn = app
|
||||||
.redis_pool
|
.redis_pool
|
||||||
.as_ref()
|
.as_ref()
|
||||||
.expect("login requires a redis server");
|
.expect("login requires a redis server")
|
||||||
|
.get()
|
||||||
|
.await?;
|
||||||
|
|
||||||
let mut redis_conn = redis_pool.get().await?;
|
// 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
|
||||||
// TODO: the address isn't enough. we need to save the actual message
|
|
||||||
redis_conn
|
redis_conn
|
||||||
.set_ex(session_key, message.to_string(), expire_seconds)
|
.set_ex(session_key, message.to_string(), expire_seconds)
|
||||||
.await?;
|
.await?;
|
||||||
|
|
||||||
|
drop(redis_conn);
|
||||||
|
|
||||||
// there are multiple ways to sign messages and not all wallets support them
|
// there are multiple ways to sign messages and not all wallets support them
|
||||||
let message_eip = params
|
let message_eip = params
|
||||||
.remove("message_eip")
|
.remove("message_eip")
|
||||||
@ -110,21 +117,38 @@ pub async fn get_login(
|
|||||||
// https://github.com/spruceid/siwe/issues/98
|
// https://github.com/spruceid/siwe/issues/98
|
||||||
"eip191_string" => Bytes::from(message.eip191_string().unwrap()).to_string(),
|
"eip191_string" => Bytes::from(message.eip191_string().unwrap()).to_string(),
|
||||||
"eip191_hash" => Bytes::from(&message.eip191_hash().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())
|
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]
|
#[debug_handler]
|
||||||
pub async fn create_user(
|
/// Post to the user endpoint to register or login.
|
||||||
// this argument tells axum to parse the request body
|
pub async fn post_login(
|
||||||
// as JSON into a `CreateUser` type
|
|
||||||
Json(payload): Json<CreateUser>,
|
|
||||||
Extension(app): Extension<Arc<Web3ProxyApp>>,
|
|
||||||
ClientIp(ip): ClientIp,
|
ClientIp(ip): ClientIp,
|
||||||
|
Extension(app): Extension<Arc<Web3ProxyApp>>,
|
||||||
|
Json(payload): Json<PostLogin>,
|
||||||
|
Query(query): Query<PostLoginQuery>,
|
||||||
) -> Response {
|
) -> Response {
|
||||||
// TODO: return a Result instead
|
// TODO: return a Result instead
|
||||||
|
// TODO: dry this up ip checking up
|
||||||
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,
|
||||||
@ -134,11 +158,22 @@ pub async fn create_user(
|
|||||||
Err(err) => return anyhow_error_into_response(None, None, err),
|
Err(err) => return anyhow_error_into_response(None, None, err),
|
||||||
};
|
};
|
||||||
|
|
||||||
// TODO: check invite_code against the app's config or database
|
let mut new_user = true; // TODO: check the database
|
||||||
if payload.invite_code != "llam4n0des!" {
|
|
||||||
todo!("proper error message")
|
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
|
let redis_pool = app
|
||||||
.redis_pool
|
.redis_pool
|
||||||
.as_ref()
|
.as_ref()
|
||||||
@ -148,32 +183,29 @@ pub async fn create_user(
|
|||||||
|
|
||||||
// TODO: use getdel
|
// TODO: use getdel
|
||||||
// TODO: do not unwrap. make this function return a FrontendResult
|
// 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
|
// check the domain and a nonce. let timestamp be automatic
|
||||||
let signature: [u8; 65] = payload.signature.as_ref().try_into().unwrap();
|
if let Err(e) = their_msg.verify(their_sig, Some(&our_msg.domain), Some(&our_msg.nonce), None) {
|
||||||
|
|
||||||
// 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
|
// message cannot be correctly authenticated
|
||||||
todo!("proper error message: {}", e)
|
todo!("proper error message: {}", e)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if new_user {
|
||||||
|
// the only thing we need from them is an address
|
||||||
|
// everything else is optional
|
||||||
let user = user::ActiveModel {
|
let user = user::ActiveModel {
|
||||||
address: sea_orm::Set(payload.address.to_fixed_bytes().into()),
|
address: sea_orm::Set(payload.address.to_fixed_bytes().into()),
|
||||||
email: sea_orm::Set(payload.email),
|
|
||||||
..Default::default()
|
..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!("create an api key");
|
||||||
let api_key = todo!();
|
|
||||||
|
|
||||||
/*
|
/*
|
||||||
let rpm = app.config.something;
|
let rpm = app.config.something;
|
||||||
@ -190,18 +222,37 @@ pub async fn create_user(
|
|||||||
// TODO: if this fails, rever adding the user, too
|
// TODO: if this fails, rever adding the user, too
|
||||||
let uk = uk.save(&txn).await.context("Failed saving new user key")?;
|
let uk = uk.save(&txn).await.context("Failed saving new user key")?;
|
||||||
|
|
||||||
|
// TODO: set a cookie?
|
||||||
|
|
||||||
// TODO: do not expose user ids
|
// TODO: do not expose user ids
|
||||||
(StatusCode::CREATED, Json(user)).into_response()
|
(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)]
|
#[derive(Deserialize)]
|
||||||
pub struct CreateUser {
|
pub struct PostUser {
|
||||||
address: Address,
|
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>,
|
email: Option<String>,
|
||||||
signature: Bytes,
|
// TODO: make them sign this JSON? cookie in session id is hard because its on a different domain
|
||||||
nonce: Uuid,
|
}
|
||||||
invite_code: String,
|
|
||||||
|
#[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