more docs

This commit is contained in:
Bryan Stitt 2022-10-17 21:47:58 +00:00
parent d6662afbe8
commit 848af3d8b3
13 changed files with 237 additions and 104 deletions

4
Cargo.lock generated
View File

@ -1029,9 +1029,9 @@ checksum = "5827cebf4670468b8772dd191856768aedcb1b0278a04f989f7766351917b9dc"
[[package]]
name = "counter"
version = "0.5.6"
version = "0.5.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "48388d8711a360319610960332b6a6f9fc2b5a63bba9fd10f1b7aa50677d956f"
checksum = "2d458e66999348f56fd3ffcfbb7f7951542075ca8359687c703de6500c1ddccd"
dependencies = [
"num-traits",
]

50
TODO.md
View File

@ -181,19 +181,22 @@ These are roughly in order of completition
- [x] change stats to using the database
- [x] emit user stat on retry
- [ ] include if archive query or not in the stats
- [ ] ability to generate a key from a web endpoint
- this is already partially done, but we need to double check it works. preferrably with tests
- [ ] ability to domain lock or ip lock said key
- [-] ability to domain lock or ip lock said key
- the code to check the database and use these entries already exists, but users don't have a way to set them
- [ ] view stats about key
- [ ] display requests per second per api key (only with authentication!)
- [ ] display concurrent requests per api key (only with authentication!)
- [ ] display distribution of methods per api key (eth_call, eth_getLogs, etc.) (only with authentication!)
- [ ] display logged reverts on an endpoint that requires authentication
- [ ] sign in
- i think this is done, but double check
- [ ] sign out
- i think this is done, but double check
- [-] new endpoints for users (not totally sure about the exact paths, but these features are all needed):
- [ ] ability to generate a key from a web endpoint
- [x] sign in
- [x] sign out
- [-] GET profile endpoint
- [-] POST profile endpoint
- [-] GET stats endpoint
- [ ] display requests per second per api key (only with authentication!)
- [ ] display concurrent requests per api key (only with authentication!)
- [ ] display distribution of methods per api key (eth_call, eth_getLogs, etc.) (only with authentication!)
- [ ] POST key endpoint
- allow setting things such as private relay, revert logging, ip/origin/etc checks
- [ ] GET logged reverts on an endpoint that requires authentication.
- [ ] endpoint for creating/modifying api keys and their advanced security features
- [ ] WARN http_request:request: web3_proxy::block_number: could not get block from params err=unexpected params length id=01GF4HTRKM4JV6NX52XSF9AYMW method=POST authorized_request=User(Some(SqlxMySqlPoolConnection), AuthorizedKey { ip: 10.11.12.15, origin: None, user_key_id: 4, log_revert_chance: 0.0000 })
- ERROR http_request:request:try_send_all_upstream_servers: web3_proxy::rpcs::request: bad response! err=JsonRpcClientError(JsonRpcError(JsonRpcError { code: -32000, message: "INTERNAL_ERROR: existing tx with same hash", data: None })) method=eth_sendRawTransaction rpc=local_erigon_alpha_archive id=01GF4HV03Y4ZNKQV8DW5NDQ5CG method=POST authorized_request=User(Some(SqlxMySqlPoolConnection), AuthorizedKey { ip: 10.11.12.15, origin: None, user_key_id: 4, log_revert_chance: 0.0000 }) self=Web3Connections { conns: {"local_erigon_alpha_archive_ws": Web3Connection { name: "local_erigon_alpha_archive_ws", blocks: "all", .. }, "local_geth_ws": Web3Connection { name: "local_geth_ws", blocks: 64, .. }, "local_erigon_alpha_archive": Web3Connection { name: "local_erigon_alpha_archive", blocks: "all", .. }}, .. } authorized_request=Some(User(Some(SqlxMySqlPoolConnection), AuthorizedKey { ip: 10.11.12.15, origin: None, user_key_id: 4, log_revert_chance: 0.0000 })) request=JsonRpcRequest { id: RawValue(39), method: "eth_sendRawTransaction", .. } request_metadata=Some(RequestMetadata { datetime: 2022-10-11T22:14:57.406829095Z, period_seconds: 60, request_bytes: 633, backend_requests: 0, no_servers: 0, error_response: false, response_bytes: 0, response_millis: 0 }) block_needed=None
@ -231,6 +234,8 @@ These are roughly in order of completition
These are not yet ordered.
- [ ] GET balance endpoint
- [ ] POST balance endpoint
- [ ] eth_1 | 2022-10-11T22:14:57.408114Z ERROR http_request:request:try_send_all_upstream_servers: web3_proxy::rpcs::request: bad response! err=JsonRpcClientError(JsonRpcError(JsonRpcError { code: -32000, message: "INTERNAL_ERROR: existing tx with same hash", data: None })) method=eth_sendRawTransaction rpc=local_erigon_alpha_archive id=01GF4HV03Y4ZNKQV8DW5NDQ5CG method=POST authorized_request=User(Some(SqlxMySqlPoolConnection), AuthorizedKey { ip: 10.11.12.15, origin: None, user_key_id: 4, log_revert_chance: 0.0000 }) self=Web3Connections { conns: {"local_erigon_alpha_archive_ws": Web3Connection { name: "local_erigon_alpha_archive_ws", blocks: "all", .. }, "local_geth_ws": Web3Connection { name: "local_geth_ws", blocks: 64, .. }, "local_erigon_alpha_archive": Web3Connection { name: "local_erigon_alpha_archive", blocks: "all", .. }}, .. } authorized_request=Some(User(Some(SqlxMySqlPoolConnection), AuthorizedKey { ip: 10.11.12.15, origin: None, user_key_id: 4, log_revert_chance: 0.0000 })) request=JsonRpcRequest { id: RawValue(39), method: "eth_sendRawTransaction", .. } request_metadata=Some(RequestMetadata { datetime: 2022-10-11T22:14:57.406829095Z, period_seconds: 60, request_bytes: 633, backend_requests: 0, no_servers: 0, error_response: false, response_bytes: 0, response_millis: 0 }) block_needed=None
- eth_sendRawTransaction should accept "INTERNAL_ERROR: existing tx with same hash" as a successful response. we just want to be sure that the server has our tx and in this case, it does.
- [ ] EIP1271 for siwe
@ -277,29 +282,6 @@ These are not yet ordered.
- [ ] if we subscribe to a server that is syncing, it gives us null block_data_limit. when it catches up, we don't ever send queries to it. we need to recheck block_data_limit
- [ ] add a "failover" tier that is only used if balanced_rpcs has "no servers synced"
- use this tier (and private tier) to check timestamp on latest block. if we are behind that by more than a few seconds, something is wrong
new endpoints for users (not totally sure about the exact paths, but these features are all needed):
- [x] GET /u/:api_key
- proxies to web3 websocket
- [x] POST /u/:api_key
- proxies to web3
- [ ] GET /user/login/$address
- returns a JSON string for the user to sign
- [ ] POST /user/login/$address
- returns a JSON string including the api key
- sets session cookie
- [ ] GET /user/$address
- checks for api key in session cookie or header
- returns a JSON string including user stats
- balance in USD
- deposits history (currency, amounts, transaction id)
- 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 success/retry/fail counts and latencies (but that may better come from somewhere else)
- [ ] POST /user/$address
- opt-in link email address
- checks for api key in session cookie or header
- allows modifying user settings
- [ ] sometimes when fetching a txid through the proxy it fails, but fetching from the backends works fine
- check flashprofits logs for examples
- [ ] relevant erigon changelogs: add pendingTransactionWithBody subscription method (#5675)

View File

@ -27,7 +27,7 @@ axum-client-ip = "0.2.0"
axum-macros = "0.2.3"
# TODO: import chrono from sea-orm so we always have the same version
chrono = "0.4.22"
counter = "0.5.6"
counter = "0.5.7"
derive_more = "0.99.17"
dotenv = "0.15.0"
ethers = { version = "0.17.0", features = ["rustls", "ws"] }

View File

@ -178,10 +178,12 @@ impl Web3ProxyApp {
Pin<Box<dyn Future<Output = anyhow::Result<()>>>>,
)> {
// safety checks on the config
assert!(
top_config.app.redirect_user_url.contains("{{user_id}}"),
"redirect user url must contain \"{{user_id}}\""
);
if let Some(redirect) = &top_config.app.redirect_user_url {
assert!(
redirect.contains("{{user_id}}"),
"redirect_user_url user url must contain \"{{user_id}}\""
);
}
// setup metrics
let app_metrics = Default::default();

View File

@ -78,7 +78,7 @@ fn run(
let prometheus_handle = tokio::spawn(metrics_frontend::serve(app, app_prometheus_port));
// if everything is working, these should both run forever
// TODO: join these instead and use shutdown handler properly
// TODO: join these instead and use shutdown handler properly. probably use tokio's ctrl+c helper
tokio::select! {
x = app_handle => {
match x {
@ -220,8 +220,8 @@ mod tests {
min_synced_rpcs: 1,
frontend_rate_limit_per_minute: 1_000_000,
response_cache_max_bytes: 10_usize.pow(7),
redirect_public_url: "example.com/".to_string(),
redirect_user_url: "example.com/{{user_id}}".to_string(),
redirect_public_url: Some("example.com/".to_string()),
redirect_user_url: Some("example.com/{{user_id}}".to_string()),
..Default::default()
},
balanced_rpcs: HashMap::from([

View File

@ -91,9 +91,9 @@ pub struct AppConfig {
#[serde(default = "default_response_cache_max_bytes")]
pub response_cache_max_bytes: usize,
/// the stats page url for an anonymous user.
pub redirect_public_url: String,
/// the stats page url for a logged in user. it must contain "{user_id}"
pub redirect_user_url: String,
pub redirect_public_url: Option<String>,
/// the stats page url for a logged in user. if set, must contain "{user_id}"
pub redirect_user_url: Option<String>,
}
/// This might cause a thundering herd!

View File

@ -1,3 +1,5 @@
//! Utilities for authorization of logged in and anonymous users.
use super::errors::FrontendErrorResponse;
use crate::app::{UserKeyData, Web3ProxyApp};
use crate::jsonrpc::JsonRpcRequest;
@ -21,6 +23,7 @@ use ulid::Ulid;
use uuid::Uuid;
/// This lets us use UUID and ULID while we transition to only ULIDs
/// TODO: include the key's description.
#[derive(Copy, Clone, Debug, Eq, PartialEq, serde::Serialize)]
pub enum UserKey {
Ulid(Ulid),

View File

@ -1,3 +1,5 @@
//! Utlities for logging errors for admins and displaying errors to users.
use crate::{app::UserKeyData, jsonrpc::JsonRpcForwardedResponse};
use axum::{
http::StatusCode,

View File

@ -1,9 +1,12 @@
//! `frontend` contains HTTP and websocket endpoints for use by users and admins.
pub mod authorization;
mod errors;
mod rpc_proxy_http;
mod rpc_proxy_ws;
mod status;
mod users;
// TODO: these are only public so docs are generated. What's a better way to do this?
pub mod rpc_proxy_http;
pub mod rpc_proxy_ws;
pub mod status;
pub mod users;
use crate::app::Web3ProxyApp;
use axum::{
@ -23,7 +26,7 @@ use tower_http::trace::TraceLayer;
use tower_request_id::{RequestId, RequestIdLayer};
use tracing::{error_span, info};
/// http and websocket frontend for customers
/// Start the frontend server.
pub async fn serve(port: u16, proxy_app: Arc<Web3ProxyApp>) -> anyhow::Result<()> {
// create a tracing span for each request with a random request id and the method
// GET: websocket or static pages
@ -48,7 +51,7 @@ pub async fn serve(port: u16, proxy_app: Arc<Web3ProxyApp>) -> anyhow::Result<()
// build our axum Router
let app = Router::new()
// routes should be order most to least common
// routes should be ordered most to least common
.route("/rpc", post(rpc_proxy_http::proxy_web3_rpc))
.route("/rpc", get(rpc_proxy_ws::websocket_handler))
.route(
@ -60,17 +63,21 @@ pub async fn serve(port: u16, proxy_app: Arc<Web3ProxyApp>) -> anyhow::Result<()
get(rpc_proxy_ws::websocket_handler_with_key),
)
.route("/health", get(status::health))
.route("/user/login/:user_address", get(users::user_login_get))
.route(
"/user/login/:user_address/:message_eip",
get(users::user_login_get),
)
.route("/user/login", post(users::user_login_post))
.route("/user/balance", get(users::user_balance_get))
.route("/user/balance/:txid", post(users::user_balance_post))
.route("/user/profile", get(users::user_profile_get))
.route("/user/profile", post(users::user_profile_post))
.route("/user/stats", get(users::user_stats_get))
.route("/user/logout", post(users::user_logout_post))
.route("/status", get(status::status))
// TODO: make this optional or remove it since it is available on another port
.route("/prometheus", get(status::prometheus))
.route("/user/login/:user_address", get(users::get_login))
.route(
"/user/login/:user_address/:message_eip",
get(users::get_login),
)
.route("/user/login", post(users::post_login))
.route("/user", post(users::post_user))
.route("/user/logout", get(users::get_logout))
// layers are ordered bottom up
// the last layer is first for requests and last for responses
// Mark the `Authorization` request header as sensitive so it doesn't show in logs
@ -112,8 +119,9 @@ pub async fn serve(port: u16, proxy_app: Arc<Web3ProxyApp>) -> anyhow::Result<()
}
/// Tokio signal handler that will wait for a user to press CTRL+C.
/// We use this in our hyper `Server` method `with_graceful_shutdown`.
/// Used in our hyper `Server` method `with_graceful_shutdown`.
async fn signal_shutdown() {
// TODO: take a shutdown_receiver and select on ctrl_c and it
info!("ctrl-c to quit");
tokio::signal::ctrl_c()
.await

View File

@ -1,3 +1,5 @@
//! Take a user's HTTP JSON-RPC requests and either respond from local data or proxy the request to a backend rpc server.
use super::authorization::{bearer_is_authorized, ip_is_authorized, key_is_authorized};
use super::errors::FrontendResult;
use crate::{app::Web3ProxyApp, jsonrpc::JsonRpcRequestEnum};
@ -10,6 +12,9 @@ use axum_client_ip::ClientIp;
use std::sync::Arc;
use tracing::{error_span, Instrument};
/// POST /rpc -- Public entrypoint for HTTP JSON-RPC requests. Web3 wallets use this.
/// Defaults to rate limiting by IP address, but can also read the Authorization header for a bearer token.
/// If possible, please use a WebSocket instead.
pub async fn proxy_web3_rpc(
Extension(app): Extension<Arc<Web3ProxyApp>>,
bearer: Option<TypedHeader<Authorization<Bearer>>>,
@ -40,6 +45,7 @@ pub async fn proxy_web3_rpc(
let authorized_request = Arc::new(authorized_request);
// TODO: spawn earlier?
let f = tokio::spawn(async move {
app.proxy_web3_rpc(authorized_request, payload)
.instrument(request_span)
@ -51,6 +57,10 @@ pub async fn proxy_web3_rpc(
Ok(Json(&response).into_response())
}
/// Authenticated entrypoint for HTTP JSON-RPC requests. Web3 wallets use this.
/// Rate limit and billing based on the api key in the url.
/// Can optionally authorized based on origin, referer, or user agent.
/// If possible, please use a WebSocket instead.
pub async fn proxy_web3_rpc_with_key(
Extension(app): Extension<Arc<Web3ProxyApp>>,
ClientIp(ip): ClientIp,
@ -64,7 +74,6 @@ pub async fn proxy_web3_rpc_with_key(
let request_span = error_span!("request", %ip, ?referer, ?user_agent);
// TODO: this should probably return the user_key_id instead? or maybe both?
let (authorized_request, _semaphore) = key_is_authorized(
&app,
user_key,
@ -80,6 +89,8 @@ pub async fn proxy_web3_rpc_with_key(
let authorized_request = Arc::new(authorized_request);
// the request can take a while, so we spawn so that we can start serving another request
// TODO: spawn even earlier?
let f = tokio::spawn(async move {
app.proxy_web3_rpc(authorized_request, payload)
.instrument(request_span)

View File

@ -1,3 +1,7 @@
//! Take a user's WebSocket JSON-RPC requests and either respond from local data or proxy the request to a backend rpc server.
//!
//! WebSockets are the preferred method of receiving requests, but not all clients have good support.
use super::authorization::{
bearer_is_authorized, ip_is_authorized, key_is_authorized, AuthorizedRequest,
};
@ -28,6 +32,8 @@ use crate::{
jsonrpc::{JsonRpcForwardedResponse, JsonRpcForwardedResponseEnum, JsonRpcRequest},
};
/// Public entrypoint for WebSocket JSON-RPC requests.
/// Defaults to rate limiting by IP address, but can also read the Authorization header for a bearer token.
#[debug_handler]
pub async fn websocket_handler(
bearer: Option<TypedHeader<Authorization<Bearer>>>,
@ -66,12 +72,23 @@ pub async fn websocket_handler(
})
.into_response()),
None => {
// this is not a websocket. redirect to a friendly page
Ok(Redirect::to(&app.config.redirect_public_url).into_response())
if let Some(redirect) = &app.config.redirect_public_url {
// this is not a websocket. redirect to a friendly page
Ok(Redirect::to(redirect).into_response())
} else {
// TODO: do not use an anyhow error. send the user a 400
Err(
anyhow::anyhow!("redirect_public_url not set. only websockets work here")
.into(),
)
}
}
}
}
/// Authenticated entrypoint for WebSocket JSON-RPC requests. Web3 wallets use this.
/// Rate limit and billing based on the api key in the url.
/// Can optionally authorized based on origin, referer, or user agent.
#[debug_handler]
pub async fn websocket_handler_with_key(
Extension(app): Extension<Arc<Web3ProxyApp>>,
@ -107,20 +124,25 @@ pub async fn websocket_handler_with_key(
proxy_web3_socket(app, authorized_request, socket).instrument(request_span)
})),
None => {
// TODO: store this on the app and use register_template?
let reg = Handlebars::new();
if let Some(redirect) = &app.config.redirect_user_url {
// TODO: store this on the app and use register_template?
let reg = Handlebars::new();
// TODO: show the user's address, not their id (remember to update the checks for {{user_id}}} in app.rs)
// TODO: query to get the user's address. expose that instead of user_id
let user_url = reg
.render_template(
&app.config.redirect_user_url,
&json!({ "authorized_request": authorized_request }),
)
.expect("templating should always work");
// TODO: show the user's address, not their id (remember to update the checks for {{user_id}}} in app.rs)
// TODO: query to get the user's address. expose that instead of user_id
let user_url = reg
.render_template(
redirect,
&json!({ "authorized_request": authorized_request }),
)
.expect("templating should always work");
// this is not a websocket. redirect to a page for this user
Ok(Redirect::to(&user_url).into_response())
// this is not a websocket. redirect to a page for this user
Ok(Redirect::to(&user_url).into_response())
} else {
// TODO: do not use an anyhow error. send the user a 400
Err(anyhow::anyhow!("redirect_user_url not set. only websockets work here").into())
}
}
}
}

View File

@ -1,10 +1,15 @@
//! Used by admins for health checks and inspecting global statistics.
//!
//! For ease of development, users can currently access these endponts.
//! They will eventually move to another port.
use crate::app::Web3ProxyApp;
use axum::{http::StatusCode, response::IntoResponse, Extension, Json};
use moka::future::ConcurrentCacheExt;
use serde_json::json;
use std::sync::Arc;
/// Health check page for load balancers to use
/// Health check page for load balancers to use.
pub async fn health(Extension(app): Extension<Arc<Web3ProxyApp>>) -> impl IntoResponse {
// TODO: also check that the head block is not too old
if app.balanced_rpcs.synced() {
@ -14,13 +19,15 @@ pub async fn health(Extension(app): Extension<Arc<Web3ProxyApp>>) -> impl IntoRe
}
}
/// Prometheus metrics
/// Prometheus metrics.
///
/// TODO: when done debugging, remove this and only allow access on a different port
pub async fn prometheus(Extension(app): Extension<Arc<Web3ProxyApp>>) -> impl IntoResponse {
app.prometheus_metrics()
}
/// Very basic status page
/// Very basic status page.
///
/// TODO: replace this with proper stats and monitoring
pub async fn status(Extension(app): Extension<Arc<Web3ProxyApp>>) -> impl IntoResponse {
app.pending_transactions.sync();

View File

@ -1,11 +1,4 @@
// 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
//! Handle registration, logins, and managing account data.
use super::authorization::{login_is_authorized, UserKey};
use super::errors::FrontendResult;
@ -32,9 +25,20 @@ use std::sync::Arc;
use time::{Duration, OffsetDateTime};
use ulid::Ulid;
// TODO: how do we customize axum's error response? I think we probably want an enum that implements IntoResponse instead
/// `GET /user/login/:user_address` or `GET /user/login/:user_address/:message_eip` -- Start the "Sign In with Ethereum" (siwe) login flow.
///
/// `message_eip`s accepted:
/// - eip191_bytes
/// - eip191_hash
/// - eip4361 (default)
///
/// Coming soon: eip1271
///
/// This is the initial entrypoint for logging in. Take the response from this endpoint and give it to your user's wallet for singing. POST the response to `/user/login`.
///
/// Rate limited by IP address.
#[debug_handler]
pub async fn get_login(
pub async fn user_login_get(
Extension(app): Extension<Arc<Web3ProxyApp>>,
ClientIp(ip): ClientIp,
// TODO: what does axum's error handling look like if the path fails to parse?
@ -69,6 +73,7 @@ pub async fn get_login(
// TODO: get most of these from the app config
let message = Message {
// TODO: should domain be llamanodes, or llamarpc?
domain: "staging.llamanodes.com".parse().unwrap(),
address: user_address.to_fixed_bytes(),
statement: Some("🦙🦙🦙🦙🦙".to_string()),
@ -95,6 +100,7 @@ pub async fn get_login(
.await?;
// there are multiple ways to sign messages and not all wallets support them
// TODO: default message eip from config?
let message_eip = params
.remove("message_eip")
.unwrap_or_else(|| "eip4361".to_string());
@ -112,35 +118,41 @@ pub async fn get_login(
Ok(message.into_response())
}
/// Query params to our `post_login` handler.
/// Query params for our `post_login` handler.
#[derive(Debug, Deserialize)]
pub struct PostLoginQuery {
invite_code: Option<String>,
/// While we are in alpha/beta, we require users to supply an invite code.
/// The invite code (if any) is set in the application's config.
/// This may eventually provide some sort of referral bonus.
pub invite_code: Option<String>,
}
/// JSON body to our `post_login` handler.
/// TODO: this should be an enum with the different login methods having different structs
/// Currently only siwe logins that send an address, msg, and sig are allowed.
#[derive(Deserialize)]
pub struct PostLogin {
address: Address,
msg: String,
sig: Bytes,
pub address: Address,
pub msg: String,
pub sig: Bytes,
// TODO: do we care about these? we should probably check the version is something we expect
// version: String,
// signer: String,
}
/// TODO: what information should we return?
/// Successful logins receive a bearer_token and all of the user's api keys.
#[derive(Serialize)]
pub struct PostLoginResponse {
/// Used for authenticating additonal requests.
bearer_token: Ulid,
/// Used for authenticating with the RPC endpoints.
api_keys: Vec<UserKey>,
}
/// Post to the user endpoint to register or login.
/// It is recommended to save the returned bearer this in a cookie and send bac
/// `POST /user/login` - Register or login by posting a signed "siwe" message.
/// It is recommended to save the returned bearer token in a cookie.
/// The bearer token can be used to authenticate other requests, such as getting the user's stats or modifying the user's profile.
#[debug_handler]
pub async fn post_login(
pub async fn user_login_post(
Extension(app): Extension<Arc<Web3ProxyApp>>,
ClientIp(ip): ClientIp,
Json(payload): Json<PostLogin>,
@ -266,9 +278,9 @@ pub async fn post_login(
Ok(response)
}
/// Log out the user connected to the given Authentication header.
/// `POST /user/logout` - Forget the bearer token in the `Authentication` header.
#[debug_handler]
pub async fn get_logout(
pub async fn user_logout_post(
Extension(app): Extension<Arc<Web3ProxyApp>>,
TypedHeader(Authorization(bearer)): TypedHeader<Authorization<Bearer>>,
) -> FrontendResult {
@ -293,9 +305,9 @@ pub struct PostUser {
// TODO: make them sign this JSON? cookie in session id is hard because its on a different domain
}
/// `POST /user/profile` -- modify the account connected to the bearer token in the `Authentication` header.
#[debug_handler]
/// post to the user endpoint to modify your existing account
pub async fn post_user(
pub async fn user_profile_post(
TypedHeader(Authorization(bearer_token)): TypedHeader<Authorization<Bearer>>,
ClientIp(ip): ClientIp,
Extension(app): Extension<Arc<Web3ProxyApp>>,
@ -330,17 +342,101 @@ pub async fn post_user(
todo!("finish post_user");
}
/// `GET /user/balance` -- Use a bearer token to get the user's balance and spend.
///
/// - show balance in USD
/// - show deposits history (currency, amounts, transaction id)
///
/// TODO: one key per request? maybe /user/balance/:api_key?
/// TODO: this will change as we add better support for secondary users.
#[debug_handler]
pub async fn user_balance_get(
TypedHeader(Authorization(bearer_token)): TypedHeader<Authorization<Bearer>>,
Extension(app): Extension<Arc<Web3ProxyApp>>,
) -> FrontendResult {
todo!("user_balance_get");
}
/// `POST /user/balance` -- Manually process a confirmed txid to update a user's balance.
///
/// We will subscribe to events to watch for any user deposits, but sometimes events can be missed.
///
/// TODO: rate limit by user
/// TODO: one key per request? maybe /user/balance/:api_key?
/// TODO: this will change as we add better support for secondary users.
#[debug_handler]
pub async fn user_balance_post(
TypedHeader(Authorization(bearer_token)): TypedHeader<Authorization<Bearer>>,
Extension(app): Extension<Arc<Web3ProxyApp>>,
) -> FrontendResult {
todo!("user_balance_post");
}
/// `GET /user/keys` -- Use a bearer token to get the user's api keys and their settings.
///
/// TODO: one key per request? maybe /user/keys/:api_key?
#[debug_handler]
pub async fn user_keys_get(
TypedHeader(Authorization(bearer_token)): TypedHeader<Authorization<Bearer>>,
Extension(app): Extension<Arc<Web3ProxyApp>>,
) -> FrontendResult {
todo!("user_keys_get");
}
/// `POST /user/keys` -- Use a bearer token to create a new key or modify an existing key.
///
/// TODO: read json from the request body
/// TODO: one key per request? maybe /user/keys/:api_key?
#[debug_handler]
pub async fn user_keys_post(
TypedHeader(Authorization(bearer_token)): TypedHeader<Authorization<Bearer>>,
Extension(app): Extension<Arc<Web3ProxyApp>>,
) -> FrontendResult {
todo!("user_keys_post");
}
/// `GET /user/profile` -- Use a bearer token to get the user's profile.
///
/// - the email address of a user if they opted in to get contacted via email
///
/// TODO: this will change as we add better support for secondary users.
#[debug_handler]
pub async fn user_profile_get(
TypedHeader(Authorization(bearer_token)): TypedHeader<Authorization<Bearer>>,
Extension(app): Extension<Arc<Web3ProxyApp>>,
) -> FrontendResult {
todo!("get_user_profile");
}
/// `GET /user/stats` -- Use a bearer token to get the user's key stats such as bandwidth used and methods requested.
///
/// - 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)
///
/// TODO: one key per request? maybe /user/stats/:api_key?
/// TODO: this will change as we add better support for secondary users.
#[debug_handler]
pub async fn user_stats_get(
TypedHeader(Authorization(bearer_token)): TypedHeader<Authorization<Bearer>>,
Extension(app): Extension<Arc<Web3ProxyApp>>,
) -> FrontendResult {
todo!("get_user_stats");
}
/// `GET /user/profile` -- Use a bearer token to get the user's profile such as their optional email address.
/// Handle authorization for a given address and bearer token.
// TODO: what roles should exist?
enum ProtectedAction {
PostUser,
}
impl ProtectedAction {
/// Verify that the given bearer token and address are allowed to take the specified action.
async fn verify(
self,
app: &Web3ProxyApp,
// TODO: i don't think we want Bearer here. we want user_key and a helper for bearer -> user_key
bearer: Bearer,
// TODO: what about secondary addresses? maybe an enum for primary or secondary?
primary_address: &Address,
) -> anyhow::Result<user::Model> {
// get the attached address from redis for the given auth_token.