//! Create a new account //! POST /account/create use std::time::Duration; use async_std::task::sleep; use revolt_config::config; use revolt_database::{ util::{ captcha::check_captcha, email::validate_email, password::assert_safe, shield::{validate_shield, ShieldValidationInput}, }, Account, Database, }; use revolt_models::v0; use revolt_result::{create_error, Result}; use rocket::serde::json::Json; use rocket::State; use rocket_empty::EmptyResponse; /// # Create Account /// /// Create a new account. #[openapi(tag = "Account")] #[post("/create", data = "")] pub async fn create_account( db: &State, data: Json, mut shield: ShieldValidationInput, ) -> Result { let data = data.into_inner(); // Random jitter from 0-1000ms sleep(Duration::from_millis((rand::random::() * 1000.) as u64)).await; // Check Captcha token check_captcha(data.captcha.as_deref()).await?; // Validate the request shield.email = Some(data.email.to_string()); validate_shield(shield).await?; // Make sure email is valid and not blocked validate_email(&data.email)?; // Ensure password is safe to use assert_safe(&data.password).await?; // If required, fetch valid invite let invite = if config().await.api.registration.invite_only { if let Some(invite) = data.invite { Some(db.fetch_account_invite(&invite).await?) } else { return Err(create_error!(MissingInvite)); } } else { None }; // Create account let account = Account::new(db, data.email, data.password, true).await?; // Use up the invite if let Some(mut invite) = invite { invite.claimed_by = Some(account.id); invite.used = true; db.save_account_invite(&invite).await?; } Ok(EmptyResponse) } #[cfg(test)] mod tests { use crate::{rocket, util::test::TestHarness}; use revolt_config::overwrite_config; use revolt_database::{events::client::EventV1, AccountInvite}; use revolt_result::{Error, ErrorType}; use rocket::http::{ContentType, Status}; #[rocket::async_test] async fn success() { let mut harness = TestHarness::new().await; let res = harness .client .post("/auth/account/create") .header(ContentType::JSON) .body( json!({ "email": "success@validemail.com", "password": "valid password" }) .to_string(), ) .dispatch() .await; assert_eq!(res.status(), Status::NoContent); drop(res); harness .wait_for_event("global", |e| matches!(e, EventV1::CreateAccount { .. })) .await; } #[rocket::async_test] async fn fail_invalid_email() { let harness = TestHarness::new().await; let res = harness .client .post("/auth/account/create") .header(ContentType::JSON) .body( json!({ "email": "invalid", "password": "valid password" }) .to_string(), ) .dispatch() .await; assert_eq!(res.status(), Status::BadRequest); assert!(matches!( res.into_json::().await.unwrap().error_type, ErrorType::IncorrectData { .. }, )); } #[rocket::async_test] async fn fail_invalid_password() { let harness = TestHarness::new().await; let res = harness .client .post("/auth/account/create") .header(ContentType::JSON) .body( json!({ "email": "fail_invalid_password@validemail.com", "password": "password" }) .to_string(), ) .dispatch() .await; assert_eq!(res.status(), Status::BadRequest); assert!(matches!( res.into_json::().await.unwrap().error_type, ErrorType::CompromisedPassword, )); } #[rocket::async_test] async fn fail_invalid_invite() { overwrite_config(|config| config.api.registration.invite_only = true).await; let harness = TestHarness::new().await; let res = harness .client .post("/auth/account/create") .header(ContentType::JSON) .body( json!({ "email": "fail_invalid_invite@validemail.com", "password": "valid password", "invite": "invalid" }) .to_string(), ) .dispatch() .await; assert_eq!(res.status(), Status::BadRequest); assert!(matches!( res.into_json::().await.unwrap().error_type, ErrorType::InvalidInvite, )); } #[rocket::async_test] async fn success_valid_invite() { overwrite_config(|config| config.api.registration.invite_only = true).await; let harness = TestHarness::new().await; let invite = AccountInvite { id: "invite".to_string(), used: false, claimed_by: None, }; invite.save(&harness.db).await.unwrap(); let res = harness .client .post("/auth/account/create") .header(ContentType::JSON) .body( json!({ "email": "success_valid_invite@validemail.com", "password": "valid password", "invite": "invite" }) .to_string(), ) .dispatch() .await; assert_eq!(res.status(), Status::NoContent); let invite = harness .db .fetch_account_invite("invite") .await .expect("`Invite`"); assert!(invite.used); } #[rocket::async_test] async fn fail_missing_captcha() { overwrite_config(|config| { config.api.security.captcha.hcaptcha_key = "0x0000000000000000000000000000000000000000".to_string() }) .await; let harness = TestHarness::new().await; let res = harness .client .post("/auth/account/create") .header(ContentType::JSON) .body( json!({ "email": "fail_missing_captcha@validemail.com", "password": "valid password", }) .to_string(), ) .dispatch() .await; assert_eq!(res.status(), Status::BadRequest); assert!(matches!( res.into_json::().await.unwrap().error_type, ErrorType::CaptchaFailed, )); } #[rocket::async_test] async fn fail_captcha_invalid() { overwrite_config(|config| { config.api.security.captcha.hcaptcha_key = "0x0000000000000000000000000000000000000000".to_string() }) .await; let harness = TestHarness::new().await; let res = harness .client .post("/auth/account/create") .header(ContentType::JSON) .body( json!({ "email": "fail_captcha_invalid@validemail.com", "password": "valid password", "captcha": "00000000-aaaa-bbbb-cccc-000000000000" }) .to_string(), ) .dispatch() .await; assert_eq!(res.status(), Status::BadRequest); assert!(matches!( res.into_json::().await.unwrap().error_type, ErrorType::CaptchaFailed, )); } #[rocket::async_test] async fn success_captcha_valid() { overwrite_config(|config| { config.api.security.captcha.hcaptcha_key = "0x0000000000000000000000000000000000000000".to_string() }) .await; let harness = TestHarness::new().await; let res = harness .client .post("/auth/account/create") .header(ContentType::JSON) .body( json!({ "email": "success_captcha_valid@validemail.com", "password": "valid password", "captcha": "20000000-aaaa-bbbb-cccc-000000000002" }) .to_string(), ) .dispatch() .await; assert_eq!(res.status(), Status::NoContent); } #[rocket::async_test] async fn success_smtp_sent() { let harness = TestHarness::new().await; let res = harness .client .post("/auth/account/create") .header(ContentType::JSON) .body( json!({ "email": "success_smtp_sent@smtp.test", "password": "valid password", }) .to_string(), ) .dispatch() .await; assert_eq!(res.status(), Status::NoContent); let (_, code) = harness.assert_email("success_smtp_sent@smtp.test").await; let res = harness .client .post(format!("/auth/account/verify/{code}")) .dispatch() .await; assert_eq!(res.status(), Status::Ok); } }