use argon2::{Argon2, password_hash::PasswordHasher}; use db::sqlx; use model::users::UserModel; use serde::{Deserialize, Serialize}; use session::Session; use crate::{AppService, error::AppError}; #[derive(Deserialize, Serialize, Clone, Debug, utoipa::ToSchema)] pub struct RegisterParams { pub username: String, pub email: String, pub password: String, pub captcha: String, } impl AppService { #[tracing::instrument(skip(self, params, context), fields(username = %params.username))] pub async fn auth_register( &self, params: RegisterParams, context: &Session, ) -> Result { self.auth_check_captcha(context, params.captcha).await?; let password = self.auth_rsa_decode(context, params.password).await?; Self::validate_password_strength(&password)?; let username_exists = self .auth_find_user_by_username(¶ms.username) .await .is_ok(); let email_exists = self.auth_find_user_by_email(¶ms.email).await.is_ok(); if username_exists || email_exists { self.metrics .auth_register_total .with_label_values(&["already_exists"]) .inc(); return Err(AppError::AccountAlreadyExists); } let user_id = uuid::Uuid::now_v7(); let now = chrono::Utc::now(); let password_hash = Argon2::default() .hash_password(password.as_bytes()) .map_err(|e| AppError::PasswordHashError(e.to_string()))? .to_string(); let mut txn = self.db.begin().await.map_err(|_| AppError::TxnError)?; let user = sqlx::query_as::<_, UserModel>( "INSERT INTO \"user\" \ (id, username, display_name, avatar_url, website_url, allow_use, can_search, \ last_sign_in_at, created_at, updated_at) \ VALUES ($1, $2, $3, '', '', true, true, NULL, $4, $4) \ RETURNING id, username, display_name, avatar_url, website_url, allow_use, can_search, \ last_sign_in_at, created_at, updated_at", ) .bind(user_id) .bind(¶ms.username) .bind(¶ms.username) .bind(now) .fetch_one(&mut **txn.inner_mut()) .await .map_err(|e| AppError::DatabaseError(e.to_string()))?; sqlx::query( "INSERT INTO user_email (\"user\", email, created_at, active, last_use_login, updated_at) \ VALUES ($1, $2, $3, true, NULL, $3)", ) .bind(user_id) .bind(¶ms.email) .bind(now) .execute(&mut **txn.inner_mut()) .await .map_err(|e| AppError::DatabaseError(e.to_string()))?; sqlx::query( "INSERT INTO user_password (\"user\", hash, salt, is_active, reason, created_at, updated_at) \ VALUES ($1, $2, '', true, NULL, $3, $3)", ) .bind(user_id) .bind(&password_hash) .bind(now) .execute(&mut **txn.inner_mut()) .await .map_err(|e| AppError::DatabaseError(e.to_string()))?; txn.commit().await.map_err(|_| AppError::TxnError)?; context.set_user(user_id); context.remove(Self::RSA_PRIVATE_KEY); context.remove(Self::RSA_PUBLIC_KEY); tracing::info!(user_uid = %user_id, username = %user.username, "User registered successfully"); self.metrics .auth_register_total .with_label_values(&["success"]) .inc(); Ok(user) } }