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::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, pub email: String, pub password: String, pub pow_answer: String, pub date_of_birth: Option, pub accept_agreements: Vec, } #[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 { pub async fn register( &self, session: &Session, cmd: RegisterCmd, ) -> Result { 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 html = crate::templates::mail_verify_code( &artifacts.code, minutes, "Verify your account", link.as_deref(), ); let text = format!("Verify your account with code: {}", artifacts.code); if let Err(error) = self .send_email(&email, "Verify your account", &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, }) } }