80f9e01
use models::users::user::ProfileVisibility;
use models::users::user_email_verification_codes::EmailVerificationPurpose;
use models::users::{user, user_emails, user_profiles};
use sea_orm::{ActiveModelTrait, ColumnTrait, DbErr, EntityTrait, QueryFilter, Set};
use serde::{Deserialize, Serialize};
use session_layer::Session;
use time::{Date, OffsetDateTime};
use uuid::Uuid;
use super::AuthService;
use super::email::build_verify_link;
use crate::errors::AuthError;
use crate::share::crypto::{hash_password, validate_password};
use crate::share::validate::{derive_username, validate_age, validate_email};
#[derive(Debug, Clone, Serialize, Deserialize, utoipa::ToSchema)]
pub struct RegisterCmd {
pub username: Option<String>,
pub email: String,
pub password: String,
pub pow_answer: String,
pub date_of_birth: Option<Date>,
pub accept_agreements: Vec<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize, utoipa::ToSchema)]
pub struct RegisterResp {
pub user_id: Uuid,
pub pending: bool,
pub purpose: EmailVerificationPurpose,
pub temp_id: Uuid,
pub expires_in_secs: u64,
}
impl AuthService {
/// ccept_language is the raw request header; the recipient has no
/// account preference yet (the row is created here), so the mail follows
/// the browser and only falls back to [email].default_locale.
#[tracing::instrument(name = "auth.register", skip_all)]
pub async fn register(
&self,
session: &Session,
cmd: RegisterCmd,
accept_language: Option<&str>,
) -> Result<RegisterResp, AuthError> {
self.verify_pow(session, &cmd.pow_answer)?;
let policy = &self.config().auth.password_policy;
validate_password(&cmd.password, policy)?;
let email = validate_email(&cmd.email)?;
let username = derive_username(&email, cmd.username.as_deref())?;
let lower_name = username.to_lowercase();
let lower_email = email.to_lowercase();
validate_age(cmd.date_of_birth, self.config().auth.min_age)?;
let db = self.db();
let user_exists = user::Entity::find()
.filter(user::Column::LowerName.eq(&lower_name))
.one(db.write())
.await?;
let email_exists = user_emails::Entity::find()
.filter(user_emails::Column::LowerEmail.eq(&lower_email))
.one(db.write())
.await?;
if user_exists.is_some() || email_exists.is_some() {
return Err(AuthError::Conflict("registration failed".to_string()));
}
let password_hash = hash_password(&cmd.password)?;
let uid = Uuid::now_v7();
let now = OffsetDateTime::now_utc();
let email_for_tx = email.clone();
let lower_email_for_tx = lower_email.clone();
let username_for_tx = username.clone();
sea_orm::TransactionTrait::transaction(db.primary(), |txn| {
Box::pin(async move {
let user_row = user::ActiveModel {
uid: Set(uid),
username: Set(username_for_tx.clone()),
lower_name: Set(lower_name.clone()),
display_name: Set(username_for_tx.clone()),
avatar_url: Set(String::new()),
is_active: Set(false),
is_restricted: Set(false),
prohibit_login: Set(false),
visibility: Set(ProfileVisibility::Public),
bot: Set(false),
system: Set(false),
last_login_at: Set(None),
last_login_ip_hash: Set(None),
version: Set(1),
passwd_hash: Set(Some(password_hash)),
password_last_changed_at: Set(Some(now)),
passwd_version: Set(Some(1)),
totp_encrypted_secret: Set(None),
totp_enabled: Set(false),
totp_last_used_at: Set(None),
totp_last_counter: Set(None),
timezone: Set(None),
locale: Set(None),
language: Set(None),
created_at: Set(now),
updated_at: Set(now),
};
user_row.insert(txn).await?;
let profile_row = user_profiles::ActiveModel {
user_id: Set(uid),
real_name: Set(None),
bio: Set(None),
pronouns: Set(None),
website: Set(None),
location: Set(None),
company: Set(None),
pronouns_privacy: Set(None),
date_of_birth: Set(cmd.date_of_birth),
banner_url: Set(None),
banner_hash: Set(None),
banner_color: Set(None),
accent_color: Set(None),
timezone_privacy_flags: Set(None),
custom_status_emoji: Set(None),
custom_status_text: Set(None),
custom_status_expires_at: Set(None),
show_email: Set(false),
show_location: Set(false),
show_real_name: Set(false),
show_activity: Set(false),
privacy_agreed_at: Set(None),
created_at: Set(now),
updated_at: Set(now),
};
profile_row.insert(txn).await?;
let email_row = user_emails::ActiveModel {
id: Set(Uuid::now_v7()),
user_id: Set(uid),
email: Set(email_for_tx),
lower_email: Set(lower_email_for_tx),
is_activated: Set(false),
is_primary: Set(true),
created_at: Set(now),
updated_at: Set(now),
activated_at: Set(None),
deleted_at: Set(None),
};
email_row.insert(txn).await?;
Ok::<(), DbErr>(())
})
})
.await?;
let artifacts = self
.issue_code(
uid,
&lower_email,
models::users::user_email_verification_codes::EmailVerificationPurpose::Signup,
)
.await?;
let minutes = self.config().auth.verify_code_ttl_secs / 60;
let link = build_verify_link(
&self.config().web.web_base_url,
&lower_email,
&artifacts.temp_id.to_string(),
&artifacts.code,
models::users::user_email_verification_codes::EmailVerificationPurpose::Signup,
);
let locale = self.mail_locale(None, accept_language).await;
let purpose = i18n::catalog::verify_subject(locale, i18n::VerifyPurpose::Signup);
let html = crate::templates::mail_verify_code(
&artifacts.code,
minutes,
purpose,
link.as_deref(),
locale,
);
let text = i18n::catalog::verify_text(locale, &artifacts.code);
if let Err(error) = self
.send_email(&email, purpose, &text, html.0.as_str())
.await
{
tracing::warn!(%error, "failed to send verification email");
}
for key in &cmd.accept_agreements {
if let Some(doc) = self
.config()
.auth
.agreements
.iter()
.find(|doc| &doc.key == key)
{
let _ = self.accept_agreement(uid, &doc.key, &doc.version).await;
}
}
Ok(RegisterResp {
user_id: uid,
pending: true,
purpose: EmailVerificationPurpose::Signup,
temp_id: artifacts.temp_id,
expires_in_secs: artifacts.expires_in_secs,
})
}
}