use crate::AppService; use crate::error::AppError; use argon2::password_hash::{Salt, SaltString}; use argon2::{Argon2, PasswordHasher}; use models::users::{user, user_activity_log, user_email, user_password}; use models::workspaces::{WorkspaceRole, workspace, workspace_membership}; use sea_orm::*; use serde::{Deserialize, Serialize}; use session::Session; use uuid::Uuid; #[derive(Deserialize, Serialize, Clone, Debug, utoipa::ToSchema)] pub struct RegisterParams { pub username: String, pub email: String, pub password: String, pub captcha: String, } impl AppService { 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?; if self .utils_find_user_by_username(params.username.clone()) .await .is_ok() { tracing::warn!(username = %params.username, "Registration failed: username already exists"); return Err(AppError::UserNameExists); } if self .utils_find_user_by_email(params.email.clone()) .await .is_ok() { tracing::warn!(email = %params.email, "Registration failed: email already exists"); return Err(AppError::EmailExists); } let user_uid = Uuid::now_v7(); let now = chrono::Utc::now(); let txn = self.db.begin().await.map_err(|_| AppError::TxnError)?; let user_model = user::ActiveModel { uid: Set(user_uid), username: Set(params.username.clone()), display_name: Set(Some(params.username.clone())), avatar_url: Set(None), website_url: Set(None), organization: Set(None), last_sign_in_at: Set(None), created_at: Set(now), updated_at: Set(now), }; let user = user_model.insert(&txn).await.map_err(|e| { tracing::error!(error = ?e, "Failed to insert user"); AppError::UserNotFound })?; let user_email_model = user_email::ActiveModel { user: Set(user_uid), email: Set(params.email), created_at: Set(now), }; user_email_model.insert(&txn).await.map_err(|e| { tracing::error!(error = ?e, "Failed to insert user email"); AppError::UserNotFound })?; let salt = SaltString::generate(&mut rsa::rand_core::OsRng::default()); let password_hash = Argon2::default() .hash_password(password.as_bytes(), Salt::from_b64(&*salt.to_string())?) .map_err(|e| { tracing::error!(error = ?e, "Failed to hash password"); AppError::UserNotFound })? .to_string(); let user_password_model = user_password::ActiveModel { user: Set(user_uid), password_hash: Set(password_hash), password_salt: Set(Some(salt.to_string())), is_active: Set(true), created_at: Set(now), updated_at: Set(now), }; user_password_model.insert(&txn).await.map_err(|e| { tracing::error!(error = ?e, "Failed to insert user password"); AppError::UserNotFound })?; let _ = user_activity_log::ActiveModel { user_uid: Set(Option::from(user_uid)), action: Set("register".to_string()), ip_address: Set(context.ip_address()), user_agent: Set(context.user_agent()), details: Set(serde_json::json!({ "username": user.username.clone(), "method": "password" }) .into()), created_at: Set(now), ..Default::default() } .insert(&txn) .await; // Auto-create personal workspace for the new user let personal_slug = format!("~{}", params.username); let ws = workspace::ActiveModel { id: Set(Uuid::now_v7()), slug: Set(personal_slug), name: Set(format!("{} 的工作空间", params.username)), description: Set(None), avatar_url: Set(None), plan: Set("free".to_string()), billing_email: Set(None), stripe_customer_id: Set(None), stripe_subscription_id: Set(None), plan_expires_at: Set(None), deleted_at: Set(None), created_at: Set(now), updated_at: Set(now), }; let ws = ws.insert(&txn).await.map_err(|e| { tracing::error!(error = ?e, "Failed to insert personal workspace"); AppError::UserNotFound })?; let _ = workspace_membership::ActiveModel { id: Default::default(), workspace_id: Set(ws.id), user_id: Set(user_uid), role: Set(WorkspaceRole::Owner.to_string()), status: Set("active".to_string()), invited_by: Set(None), joined_at: Set(now), invite_token: Set(None), invite_expires_at: Set(None), } .insert(&txn) .await; txn.commit().await.map_err(|_| AppError::TxnError)?; context.set_user(user_uid); context.set_current_workspace_id(ws.id); context.remove(Self::RSA_PRIVATE_KEY); context.remove(Self::RSA_PUBLIC_KEY); tracing::info!(user_uid = %user_uid, username = %user.username, "User registered successfully"); Ok(user) } }