return user keys as a mapping
This commit is contained in:
parent
a67b85a327
commit
d31484467d
@ -19,14 +19,15 @@ use hashbrown::HashMap;
|
|||||||
use http::StatusCode;
|
use http::StatusCode;
|
||||||
use redis_rate_limiter::redis::AsyncCommands;
|
use redis_rate_limiter::redis::AsyncCommands;
|
||||||
use sea_orm::{ActiveModelTrait, ColumnTrait, EntityTrait, QueryFilter, TransactionTrait};
|
use sea_orm::{ActiveModelTrait, ColumnTrait, EntityTrait, QueryFilter, TransactionTrait};
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::Deserialize;
|
||||||
|
use serde_json::json;
|
||||||
use siwe::{Message, VerificationOpts};
|
use siwe::{Message, VerificationOpts};
|
||||||
use std::ops::Add;
|
use std::ops::Add;
|
||||||
use std::str::FromStr;
|
use std::str::FromStr;
|
||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
use time::{Duration, OffsetDateTime};
|
use time::{Duration, OffsetDateTime};
|
||||||
use tokio::sync::Semaphore;
|
use tokio::sync::Semaphore;
|
||||||
use tracing::{info, warn};
|
use tracing::warn;
|
||||||
use ulid::Ulid;
|
use ulid::Ulid;
|
||||||
|
|
||||||
/// `GET /user/login/:user_address` or `GET /user/login/:user_address/:message_eip` -- Start the "Sign In with Ethereum" (siwe) login flow.
|
/// `GET /user/login/:user_address` or `GET /user/login/:user_address/:message_eip` -- Start the "Sign In with Ethereum" (siwe) login flow.
|
||||||
@ -140,17 +141,6 @@ pub struct PostLogin {
|
|||||||
msg: String,
|
msg: String,
|
||||||
}
|
}
|
||||||
|
|
||||||
/// 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: HashMap<u64, UserKey>,
|
|
||||||
user_id: u64,
|
|
||||||
// TODO: what else?
|
|
||||||
}
|
|
||||||
|
|
||||||
/// `POST /user/login` - Register or login by posting a signed "siwe" message.
|
/// `POST /user/login` - Register or login by posting a signed "siwe" message.
|
||||||
/// It is recommended to save the returned bearer token in a cookie.
|
/// 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.
|
/// The bearer token can be used to authenticate other requests, such as getting the user's stats or modifying the user's profile.
|
||||||
@ -163,10 +153,6 @@ pub async fn user_login_post(
|
|||||||
) -> FrontendResult {
|
) -> FrontendResult {
|
||||||
login_is_authorized(&app, ip).await?;
|
login_is_authorized(&app, ip).await?;
|
||||||
|
|
||||||
// we can't trust that they didn't tamper with the message in some way
|
|
||||||
// TODO: it seems like some clients do things unexpectedly. these don't always parse
|
|
||||||
// let their_msg: siwe::Message = payload.msg.parse().context("parsing user's message")?;
|
|
||||||
|
|
||||||
// TODO: this seems too verbose. how can we simply convert a String into a [u8; 65]
|
// TODO: this seems too verbose. how can we simply convert a String into a [u8; 65]
|
||||||
let their_sig_bytes = Bytes::from_str(&payload.sig).context("parsing sig")?;
|
let their_sig_bytes = Bytes::from_str(&payload.sig).context("parsing sig")?;
|
||||||
if their_sig_bytes.len() != 65 {
|
if their_sig_bytes.len() != 65 {
|
||||||
@ -177,7 +163,8 @@ pub async fn user_login_post(
|
|||||||
their_sig[x] = their_sig_bytes[x]
|
their_sig[x] = their_sig_bytes[x]
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO: checking 0x seems fragile, but I think it will be fine
|
// we can't trust that they didn't tamper with the message in some way. like some clients return it hex encoded
|
||||||
|
// TODO: checking 0x seems fragile, but I think it will be fine. siwe message text shouldn't ever start with 0x
|
||||||
let their_msg: Message = if payload.msg.starts_with("0x") {
|
let their_msg: Message = if payload.msg.starts_with("0x") {
|
||||||
let their_msg_bytes = Bytes::from_str(&payload.msg).context("parsing payload message")?;
|
let their_msg_bytes = Bytes::from_str(&payload.msg).context("parsing payload message")?;
|
||||||
|
|
||||||
@ -192,6 +179,7 @@ pub async fn user_login_post(
|
|||||||
.context("parsing string message")?
|
.context("parsing string message")?
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// the only part of the message we will trust is their nonce
|
||||||
// TODO: this is fragile. have a helper function/struct for redis keys
|
// TODO: this is fragile. have a helper function/struct for redis keys
|
||||||
let login_nonce_key = format!("login_nonce:{}", &their_msg.nonce);
|
let login_nonce_key = format!("login_nonce:{}", &their_msg.nonce);
|
||||||
|
|
||||||
@ -204,15 +192,10 @@ pub async fn user_login_post(
|
|||||||
|
|
||||||
let our_msg: siwe::Message = our_msg.parse().context("parsing siwe message")?;
|
let our_msg: siwe::Message = our_msg.parse().context("parsing siwe message")?;
|
||||||
|
|
||||||
// TODO: info for now
|
// default options are fine. the message includes timestamp and domain and nonce
|
||||||
info!(?our_msg, ?their_msg);
|
|
||||||
|
|
||||||
// let timestamp be automatic
|
|
||||||
// we don't need to check domain or nonce because we store the message safely locally
|
|
||||||
let verify_config = VerificationOpts::default();
|
let verify_config = VerificationOpts::default();
|
||||||
|
|
||||||
// TODO: verify or verify_eip191?
|
// Check with both verify and verify_eip191
|
||||||
// TODO: save this when we save the message type to redis? we still need to check both
|
|
||||||
if let Err(err_1) = our_msg
|
if let Err(err_1) = our_msg
|
||||||
.verify(&their_sig, &verify_config)
|
.verify(&their_sig, &verify_config)
|
||||||
.await
|
.await
|
||||||
@ -246,6 +229,7 @@ pub async fn user_login_post(
|
|||||||
// user does not exist yet
|
// user does not exist yet
|
||||||
|
|
||||||
// check the invite code
|
// check the invite code
|
||||||
|
// TODO: more advanced invite codes that set different request/minute and concurrency limits
|
||||||
if let Some(invite_code) = &app.config.invite_code {
|
if let Some(invite_code) = &app.config.invite_code {
|
||||||
if query.invite_code.as_ref() != Some(invite_code) {
|
if query.invite_code.as_ref() != Some(invite_code) {
|
||||||
return Err(anyhow::anyhow!("checking invite_code").into());
|
return Err(anyhow::anyhow!("checking invite_code").into());
|
||||||
@ -302,14 +286,14 @@ pub async fn user_login_post(
|
|||||||
// create a bearer token for the user.
|
// create a bearer token for the user.
|
||||||
let bearer_token = Ulid::new();
|
let bearer_token = Ulid::new();
|
||||||
|
|
||||||
let response_json = PostLoginResponse {
|
let response_json = json!({
|
||||||
api_keys: uks
|
"api_keys": uks
|
||||||
.into_iter()
|
.into_iter()
|
||||||
.map(|uk| (uk.id, uk.api_key.into()))
|
.map(|uk| (uk.id, uk))
|
||||||
.collect(),
|
.collect::<HashMap<_, _>>(),
|
||||||
bearer_token,
|
"bearer_token": bearer_token,
|
||||||
user_id: u.id,
|
"user_id": u.id,
|
||||||
};
|
});
|
||||||
|
|
||||||
let response = (status_code, Json(response_json)).into_response();
|
let response = (status_code, Json(response_json)).into_response();
|
||||||
|
|
||||||
@ -443,11 +427,28 @@ pub async fn user_keys_get(
|
|||||||
Extension(app): Extension<Arc<Web3ProxyApp>>,
|
Extension(app): Extension<Arc<Web3ProxyApp>>,
|
||||||
TypedHeader(Authorization(bearer_token)): TypedHeader<Authorization<Bearer>>,
|
TypedHeader(Authorization(bearer_token)): TypedHeader<Authorization<Bearer>>,
|
||||||
) -> FrontendResult {
|
) -> FrontendResult {
|
||||||
let user = ProtectedAction::UserKeysGet
|
let user = ProtectedAction::UserKeys
|
||||||
.authorize(app.as_ref(), bearer_token)
|
.authorize(app.as_ref(), bearer_token)
|
||||||
.await?;
|
.await?;
|
||||||
|
|
||||||
todo!("user_keys_get");
|
let db_conn = app.db_conn().context("getting db to fetch user's keys")?;
|
||||||
|
|
||||||
|
let uks = user_keys::Entity::find()
|
||||||
|
.filter(user_keys::Column::UserId.eq(user.id))
|
||||||
|
.all(&db_conn)
|
||||||
|
.await
|
||||||
|
.context("failed loading user's key")?;
|
||||||
|
|
||||||
|
// TODO: stricter type on this?
|
||||||
|
let response_json = json!({
|
||||||
|
"api_keys": uks
|
||||||
|
.into_iter()
|
||||||
|
.map(|uk| (uk.id, uk))
|
||||||
|
.collect::<HashMap::<_, _>>(),
|
||||||
|
"user_id": user.id,
|
||||||
|
});
|
||||||
|
|
||||||
|
Ok(Json(response_json).into_response())
|
||||||
}
|
}
|
||||||
|
|
||||||
/// `POST /user/keys` -- Use a bearer token to create a new key or modify an existing key.
|
/// `POST /user/keys` -- Use a bearer token to create a new key or modify an existing key.
|
||||||
@ -520,7 +521,7 @@ pub async fn user_stats_aggregate_get(
|
|||||||
/// Handle authorization for a given address and bearer token.
|
/// Handle authorization for a given address and bearer token.
|
||||||
// TODO: what roles should exist?
|
// TODO: what roles should exist?
|
||||||
enum ProtectedAction {
|
enum ProtectedAction {
|
||||||
UserKeysGet,
|
UserKeys,
|
||||||
UserProfilePost(Address),
|
UserProfilePost(Address),
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -561,7 +562,7 @@ impl ProtectedAction {
|
|||||||
.context("unknown user id")?;
|
.context("unknown user id")?;
|
||||||
|
|
||||||
match self {
|
match self {
|
||||||
Self::UserKeysGet => {
|
Self::UserKeys => {
|
||||||
// no extra checks needed. bearer token gave us a user
|
// no extra checks needed. bearer token gave us a user
|
||||||
}
|
}
|
||||||
Self::UserProfilePost(primary_address) => {
|
Self::UserProfilePost(primary_address) => {
|
||||||
|
Loading…
Reference in New Issue
Block a user