use crate::AuthService; use crate::errors::{AuthError, AuthResult}; use crate::events::{Labels, label}; use crate::handlers::challenge::PowProofRequest; use crate::models::users::user_events::UserEventType; use crate::models::users::users; use crate::session::Session; use crate::share::codes::{consume_email_code, store_email_code}; use crate::share::password::{hash as hash_password, validate_new_password}; use crate::share::validate::{validate_display_name, validate_email, validate_username}; use crate::share::{crypto, rate}; use chrono::Utc; use rand::RngExt; use sea_orm::entity::Set; use sea_orm::sea_query::{Expr, ExprTrait, Func}; use sea_orm::{ActiveModelTrait, ColumnTrait, Condition, EntityTrait, QueryFilter, SqlErr}; use serde::{Deserialize, Serialize}; use utoipa::ToSchema; #[derive(Clone, Debug, Deserialize, Serialize, ToSchema)] pub struct RegisterCodeRequest { pub email: String, pub pow_payload: PowProofRequest, } #[derive(Clone, Debug, Deserialize, Serialize, ToSchema)] pub struct RegisterCodeResponse { pub expires_in_secs: u64, } #[derive(Clone, Debug, Deserialize, Serialize, ToSchema)] pub struct UserRegisterRequest { pub username: String, pub displayname: Option, pub birth_date: String, pub email: String, pub password_hash: String, pub email_code: String, pub pow_payload: PowProofRequest, } #[derive(Clone, Debug, Deserialize, Serialize, ToSchema)] pub struct UserRegisterResponse { pub user_id: i64, pub username: String, pub displayname: Option, } pub mod age { use chrono::{Datelike, NaiveDate}; use crate::errors::{AuthError, AuthResult}; pub const EARLIEST_BIRTH_YEAR: i32 = 1900; pub fn parse_birth_date(raw: &str, min_age: u8) -> AuthResult { let parsed = NaiveDate::parse_from_str(raw, "%Y-%m-%d").map_err(|_| AuthError::InvalidBirthDate)?; if parsed.format("%Y-%m-%d").to_string() != raw { return Err(AuthError::InvalidBirthDate); } if parsed.year() < EARLIEST_BIRTH_YEAR { return Err(AuthError::InvalidBirthDate); } crate::share::validate::validate_age(Some(parsed), min_age) } } const REGISTER_CODE_KEY_PREFIX: &str = "auth:code:register:"; const REGISTER_RATE_KEY_PREFIX: &str = "auth:rate:register:"; const USERNAME_MAX_LEN: usize = 32; const PASSWORD_DIGEST_MAX_LEN: usize = 255; const ID_INSERT_ATTEMPTS: u32 = 8; impl AuthService { #[tracing::instrument(skip_all, fields(request_id = %request_id, client_ip = ?session.client_ip().ok().flatten()))] pub async fn send_register_code( &self, session: Session, request: RegisterCodeRequest, request_id: String, ) -> AuthResult { self.challenge_verify_pow( session, &request.pow_payload, request.email.as_bytes(), request_id.clone(), ) .await?; let email = validate_email(&request.email)?; // 限流必须排在「邮箱是否已注册」之前:否则每次调用都能免费探测一个地址是否注册。 rate::rate_limit_once( self.cache(), &format!("{REGISTER_RATE_KEY_PREFIX}{email}"), self.auth().email_code_rate_limit_secs, Some(request_id.as_str()), ) .await?; self.ensure_email_available(&email).await?; let code = crypto::random_numeric_code(self.auth().verify_code_len); store_email_code( self.cache(), &format!("{REGISTER_CODE_KEY_PREFIX}{email}"), &code, self.auth().verify_code_ttl_secs, Some(request_id.as_str()), ) .await?; let minutes = self.auth().verify_code_ttl_secs / 60; let subject = "验证你的账号"; let text = format!( "你的验证码是:{code},{minutes} 分钟内有效。请勿把验证码告诉任何人;如果这不是你本人的操作,请忽略这封邮件。" ); let html = format!( "

你的验证码是:{code},{minutes} 分钟内有效。

请勿把验证码告诉任何人;如果这不是你本人的操作,请忽略这封邮件。

" ); self.publish_mail(&email, subject, &text, &html, Some(request_id.as_str())) .await?; let response = RegisterCodeResponse { expires_in_secs: self.auth().verify_code_ttl_secs, }; self.emit_labeled( label::AUTH_REGISTER_CREATE, Labels::new().with(label::REQUEST_ID, &request_id), &response, Some(request_id.as_str()), ) .await; Ok(response) } #[tracing::instrument(skip_all, fields(request_id = %request_id, client_ip = ?session.client_ip().ok().flatten()))] pub async fn register( &self, session: Session, request: UserRegisterRequest, user_agent: Option, ip_addr: Option, request_id: String, ) -> AuthResult { self.challenge_verify_pow( session.clone(), &request.pow_payload, request.username.as_bytes(), request_id.clone(), ) .await?; let username = validate_username(&request.username)?; if username.chars().count() > USERNAME_MAX_LEN { return Err(AuthError::InvalidUsername); } let displayname = match request.displayname.as_deref() { Some(name) if !name.trim().is_empty() => Some(validate_display_name(name)?), _ => None, }; let birth_date = age::parse_birth_date(request.birth_date.trim(), self.auth().min_age)?; let email = validate_email(&request.email)?; if request.password_hash.is_empty() || request.password_hash.len() > PASSWORD_DIGEST_MAX_LEN { return Err(AuthError::InvalidPassword); } validate_new_password(&request.password_hash, &self.auth().password_policy)?; consume_email_code( self.cache(), &format!("{REGISTER_CODE_KEY_PREFIX}{email}"), &request.email_code, self.auth().max_verify_attempts as i64, Some(request_id.as_str()), ) .await?; let password_hash = hash_password(request.password_hash.clone(), Some(request_id.as_str())).await?; let lowername = username.to_lowercase(); let now = Utc::now(); let mut attempt = 0u32; loop { attempt += 1; self.ensure_username_available(&username, &lowername) .await?; self.ensure_email_available(&email).await?; let inserted = users::ActiveModel { id: Set({ let mut rng = rand::rng(); let digits = rng.random_range(5u32..=10); let upper = 10i64.pow(digits); rng.random_range((upper / 10)..upper) }), username: Set(username.clone()), lowername: Set(lowername.clone()), displayname: Set(displayname.clone()), birth_date: Set(Some(birth_date)), email: Set(email.clone()), email_verified_at: Set(Some(now)), password_hash: Set(password_hash.clone()), password_updated_at: Set(Some(now)), created_at: Set(now), ..Default::default() } .insert(self.db().write()) .await; let user = match inserted { Ok(user) => user, Err(error) => { let unique = matches!(error.sql_err(), Some(SqlErr::UniqueConstraintViolation(_))); if !unique { return Err(AuthError::Database(crate::db::DbError::from(error))); } if attempt >= ID_INSERT_ATTEMPTS { // 重试耗尽仍撞唯一约束:说明预检查看到的名字/邮箱已被别人占住。 return Err(AuthError::UsernameTaken); } continue; } }; session.set_user_id(user.id)?; session.set_security_version(user.security_version)?; session.renew(); self.bind_auth_session(&session, &user, false, Some(request_id.as_str())) .await?; crate::share::audit::entry( self.db(), user.id, Some(user.id), UserEventType::Register, Some(serde_json::json!({ "username": user.username.clone(), "email": user.email.clone(), })), Some(&request_id), ip_addr, user_agent, ) .await?; let response = UserRegisterResponse { user_id: user.id, username: user.username, displayname: user.displayname, }; self.emit_labeled( label::AUTH_REGISTER_CREATE, Labels::new() .with(label::USER_ID, response.user_id) .with(label::REQUEST_ID, &request_id), &response, Some(request_id.as_str()), ) .await; return Ok(response); } } async fn ensure_username_available(&self, username: &str, lowername: &str) -> AuthResult<()> { let taken = users::Entity::find() .filter( Condition::any() .add(users::Column::Username.eq(username.to_owned())) .add(users::Column::Lowername.eq(lowername.to_owned())), ) .one(self.db().write()) .await?; if taken.is_some() { return Err(AuthError::UsernameTaken); } Ok(()) } async fn ensure_email_available(&self, email: &str) -> AuthResult<()> { let taken = users::Entity::find() .filter(Expr::expr(Func::lower(Expr::col(users::Column::Email))).eq(email.to_owned())) .one(self.db().write()) .await?; if taken.is_some() { return Err(AuthError::EmailTaken); } Ok(()) } }