main
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<String>,
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<String>,
}
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<NaiveDate> {
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<RegisterCodeResponse> {
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!(
"<p>你的验证码是:<strong>{code}</strong>,{minutes} 分钟内有效。</p><p>请勿把验证码告诉任何人;如果这不是你本人的操作,请忽略这封邮件。</p>"
);
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<String>,
ip_addr: Option<String>,
request_id: String,
) -> AuthResult<UserRegisterResponse> {
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(())
}
}