chore: migrate authifier into codebase (#658)
Co-authored-by: izzy <me@insrt.uk> Signed-off-by: Zomatree <me@zomatree.live> Signed-off-by: izzy <me@insrt.uk>
This commit is contained in:
347
crates/delta/src/routes/account/create_account.rs
Normal file
347
crates/delta/src/routes/account/create_account.rs
Normal file
@@ -0,0 +1,347 @@
|
||||
//! 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);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user