Skip to content

Commit

Permalink
Merge branch 'main' of github.com:ambrosus/airdao-gov-user-verifier i…
Browse files Browse the repository at this point in the history
…nto update-rust-toolchain
  • Loading branch information
Kirill-K-1 committed Jun 9, 2024
2 parents 790a671 + 509f7b0 commit 2d95f35
Show file tree
Hide file tree
Showing 3 changed files with 191 additions and 1 deletion.
38 changes: 38 additions & 0 deletions gov-portal-db/src/server.rs
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,9 @@ pub struct AppState {
pub quiz: Quiz,
}

/// Maximum number of wallets are allowed at once to request with `/users` endpoint to fetch users profiles
const USERS_MAX_WALLETS_REQ_LIMIT: usize = 50;

impl AppState {
pub async fn new(
config: AppConfig,
Expand All @@ -52,6 +55,14 @@ pub enum TokenQuery {
NoMessage {},
}

/// JSON-serialized request passed as POST-data to `/users` endpoint
#[derive(Debug, Deserialize)]
pub struct UsersRequest {
wallets: Vec<Address>,
#[serde(flatten)]
pub session: SessionToken,
}

#[derive(Debug, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct SignedQuizResponse {
Expand Down Expand Up @@ -147,6 +158,7 @@ pub async fn start(config: AppConfig, users_manager: Arc<UsersManager>) -> Resul
let app = Router::new()
.route("/token", post(token_route))
.route("/user", post(user_route))
.route("/users", post(users_route))
.route("/update-user", post(update_user_route))
.route("/check-email", post(check_email_route))
.route("/verify-email", post(verify_email_route))
Expand Down Expand Up @@ -205,6 +217,32 @@ async fn user_route(
res.map(Json)
}

/// Route handler to read multiple User's profiles from MongoDB
async fn users_route(
State(state): State<AppState>,
Json(req): Json<UsersRequest>,
) -> Result<Json<Vec<UserProfile>>, String> {
tracing::debug!(
"[/users] Request (session: {session:?}, wallets: {wallets})",
session = req.session,
wallets = req.wallets.len()
);

let res = match state.session_manager.verify_token(&req.session) {
Ok(_) => state
.users_manager
.get_users_by_wallets(&req.wallets[..USERS_MAX_WALLETS_REQ_LIMIT])
.await
.map_err(|e| format!("Unable to acquire users profiles. Error: {e}")),

Err(e) => Err(format!("Users request failure. Error: {e}")),
};

tracing::debug!("[/users] Response {res:?}");

res.map(Json)
}

/// Route handler to request quiz questions
async fn quiz_route(
State(state): State<AppState>,
Expand Down
58 changes: 57 additions & 1 deletion gov-portal-db/src/users_manager/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -157,7 +157,7 @@ impl UsersManager {
}
}

/// Searches for a user profile within MongoDB by provided EVM-like address [`Address`] and returns [`User`]
/// Searches for a user profile within MongoDB by provided EVM-like address [`Address`] and returns [`UserProfile`]
pub async fn get_user_by_wallet(&self, wallet: Address) -> Result<UserProfile, error::Error> {
let filter = doc! {
"wallet": bson::to_bson(&wallet)?,
Expand Down Expand Up @@ -190,6 +190,62 @@ impl UsersManager {
})
}

/// Searches for multiple user profiles within MongoDB by provided EVM-like address [`Address`] list and returns [`Vec<UserProfile>`]
pub async fn get_users_by_wallets(
&self,
wallets: &[Address],
) -> Result<Vec<UserProfile>, error::Error> {
if wallets.is_empty() {
return Ok(vec![]);
}

let filter = doc! {
"wallet": {
"$in": bson::to_bson(wallets)?
},
};

let find_options = FindOptions::builder()
.max_time(self.mongo_client.req_timeout)
.build();

let res = tokio::time::timeout(self.mongo_client.req_timeout, async {
let mut stream = self.mongo_client.find(filter, find_options).await?;
let mut profiles = Vec::with_capacity(wallets.len());
loop {
if profiles.len() == wallets.len() {
break;
}

if let Ok(Some(doc)) = stream.try_next().await {
let profile =
bson::from_document::<RawUserProfile>(doc).map_err(error::Error::from)?;
profiles.push(profile);
} else {
break;
}
}
Ok(profiles)
})
.await?;

tracing::debug!("Get users by wallets ({wallets:?}) result: {res:?}");

res.and_then(|raw_profiles| {
raw_profiles
.into_iter()
.map(|raw_profile| {
UserProfile::new(
raw_profile,
Utc::now() + self.config.lifetime,
self.config.secret.as_bytes(),
)
.map_err(error::Error::from)
})
.collect::<Result<_, _>>()
})
}

/// Updates user profile stored in MongoDB by updated [`User`] struct. Input [`User`] struct is verified for correctness.
pub async fn update_user(&self, user: UserInfo) -> Result<(), error::Error> {
self.verify_user(&user)?;
Expand Down
96 changes: 96 additions & 0 deletions gov-portal-db/tests/test_db.rs
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,12 @@ async fn test_register_user() -> Result<(), anyhow::Error> {
)
.await?;

users_manager
.mongo_client
.collection
.delete_many(bson::doc! {}, None)
.await?;

let addr_1 = Address::from_low_u64_le(0);
let addr_2 = Address::from_low_u64_le(1);

Expand Down Expand Up @@ -77,6 +83,90 @@ async fn test_register_user() -> Result<(), anyhow::Error> {
Ok(())
}

#[tokio::test]
async fn test_users_endpoint() -> Result<(), anyhow::Error> {
let mongo_config = mongo_client::MongoConfig {
url: Some("mongodb://localhost:27017".to_owned()),
db: "AirDAOGovPortal_IntegrationTest".to_owned(),
collection: "Users".to_owned(),
request_timeout: 10,
};

let users_manager = std::sync::Arc::new(
UsersManager::new(
&mongo_config,
UsersManagerConfig {
secret: "IntegrationTestRegistrationSecretForJWT".to_owned(),
lifetime: std::time::Duration::from_secs(600),
user_profile_attributes: UserProfileAttributes::default(),
email_verification: EmailVerificationConfig {
mailer_base_url: "http://mailer".try_into().unwrap(),
send_timeout: std::time::Duration::from_secs(10),
template_url: "https://registration?token={{VERIFICATION_TOKEN}}".to_string(),
from: EmailFrom {
email: "gwg@airdao.io".try_into().unwrap(),
name: "AirDAO Gov Portal".to_string(),
},
subject: "Complete Your Governor Email Verification".to_string(),
},
},
)
.await?,
);

users_manager
.mongo_client
.collection
.delete_many(bson::doc! {}, None)
.await?;

futures_util::future::join_all((0u64..=8).map(|i| {
let users_manager = users_manager.clone();

async move {
let wallet = Address::from_low_u64_le(i);
users_manager
.register_user(&UserInfo {
wallet,
email: Some(format!("test{i}@test.com").try_into().unwrap()),
..default_user_info()
})
.await
}
}))
.await;

assert_eq!(
users_manager
.get_users_by_wallets(&[
Address::from_low_u64_le(10),
Address::from_low_u64_le(0),
Address::from_low_u64_le(1),
Address::from_low_u64_le(7),
Address::from_low_u64_le(8),
Address::from_low_u64_le(2),
Address::from_low_u64_le(9),
])
.await
.and_then(|profiles| profiles
.into_iter()
.map(|profile| Ok(profile.info.email))
.collect::<Result<Vec<_>, _>>())
.unwrap(),
vec![
Some("test0@test.com".parse().unwrap()),
Some("test1@test.com".parse().unwrap()),
Some("test2@test.com".parse().unwrap()),
Some("test7@test.com".parse().unwrap()),
Some("test8@test.com".parse().unwrap())
]
);

users_manager.mongo_client.collection.drop(None).await?;

Ok(())
}

#[tokio::test]
async fn test_complete_profile() -> Result<(), anyhow::Error> {
let quiz_config = serde_json::from_str::<QuizConfig>(
Expand Down Expand Up @@ -152,6 +242,12 @@ async fn test_complete_profile() -> Result<(), anyhow::Error> {
)
.await?;

users_manager
.mongo_client
.collection
.delete_many(bson::doc! {}, None)
.await?;

let quiz_result = quiz.verify_answers(vec![
serde_json::from_str::<QuizAnswer>(
r#"{"question": "Question 1", "variant": "some valid answer 3"}"#,
Expand Down

0 comments on commit 2d95f35

Please sign in to comment.