Co-authored-by: izzy <me@insrt.uk> Signed-off-by: Zomatree <me@zomatree.live> Signed-off-by: izzy <me@insrt.uk>
348 lines
9.5 KiB
Rust
348 lines
9.5 KiB
Rust
//! 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 = "<data>")]
|
|
pub async fn create_account(
|
|
db: &State<Database>,
|
|
data: Json<v0::DataCreateAccount>,
|
|
mut shield: ShieldValidationInput,
|
|
) -> Result<EmptyResponse> {
|
|
let data = data.into_inner();
|
|
|
|
// Random jitter from 0-1000ms
|
|
sleep(Duration::from_millis((rand::random::<f32>() * 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::<Error>().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::<Error>().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::<Error>().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::<Error>().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::<Error>().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);
|
|
}
|
|
}
|