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:
Zomatree
2026-06-21 00:50:06 +01:00
committed by GitHub
parent a7af24b38d
commit d27917b824
145 changed files with 108392 additions and 1189 deletions

View 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);
}
}