feat: add configurable verification and redemption codes
This commit is contained in:
@@ -2,6 +2,7 @@ use crate::api::envelope::Envelope;
|
||||
use crate::auth;
|
||||
use crate::error::{AppError, ErrorCode};
|
||||
use crate::services::mail;
|
||||
use crate::services::settings;
|
||||
use crate::state::AppState;
|
||||
|
||||
use argon2::{Argon2, PasswordHash, PasswordHasher, PasswordVerifier};
|
||||
@@ -79,11 +80,13 @@ async fn register(
|
||||
validate_password(&req.password)?;
|
||||
|
||||
let password_hash = hash_password(&req.password)?;
|
||||
let verification_required = settings::email_verification_required(&state).await?;
|
||||
let verified_at = (!verification_required).then(Utc::now);
|
||||
|
||||
let user = sqlx::query_as::<_, UserRow>(
|
||||
r#"
|
||||
INSERT INTO users (email, username, password_hash)
|
||||
VALUES ($1, $2, $3)
|
||||
INSERT INTO users (email, username, password_hash, email_verified_at)
|
||||
VALUES ($1, $2, $3, $4)
|
||||
RETURNING
|
||||
id,
|
||||
email,
|
||||
@@ -97,6 +100,7 @@ async fn register(
|
||||
.bind(req.email.to_lowercase())
|
||||
.bind(&req.username)
|
||||
.bind(password_hash)
|
||||
.bind(verified_at)
|
||||
.fetch_one(&state.db)
|
||||
.await
|
||||
.map_err(map_unique_violation)?;
|
||||
@@ -108,34 +112,38 @@ async fn register(
|
||||
&user.role,
|
||||
)?;
|
||||
|
||||
let verification_token = generate_token();
|
||||
let token_hash = sha256_hex(&verification_token);
|
||||
let expires_at_db = Utc::now() + Duration::hours(24);
|
||||
if verification_required {
|
||||
let verification_token = generate_token();
|
||||
let token_hash = sha256_hex(&verification_token);
|
||||
let expires_at_db = Utc::now() + Duration::hours(24);
|
||||
|
||||
sqlx::query(
|
||||
r#"
|
||||
INSERT INTO email_verifications (user_id, token_hash, expires_at)
|
||||
VALUES ($1, $2, $3)
|
||||
"#,
|
||||
)
|
||||
.bind(user.id)
|
||||
.bind(token_hash)
|
||||
.bind(expires_at_db)
|
||||
.execute(&state.db)
|
||||
.await
|
||||
.map_err(|err| AppError::new(ErrorCode::Internal, "创建邮箱验证记录失败").with_source(err))?;
|
||||
|
||||
let verification_url = format!(
|
||||
"{}/verify-email?token={}",
|
||||
state.config.public_base_url, verification_token
|
||||
);
|
||||
|
||||
mail::send_verification_email(&state, &user.email, &user.username, &verification_url)
|
||||
sqlx::query(
|
||||
r#"
|
||||
INSERT INTO email_verifications (user_id, token_hash, expires_at)
|
||||
VALUES ($1, $2, $3)
|
||||
"#,
|
||||
)
|
||||
.bind(user.id)
|
||||
.bind(token_hash)
|
||||
.bind(expires_at_db)
|
||||
.execute(&state.db)
|
||||
.await
|
||||
.map_err(|err| {
|
||||
AppError::new(ErrorCode::MailSendFailed, "验证邮件发送失败").with_source(err)
|
||||
AppError::new(ErrorCode::Internal, "创建邮箱验证记录失败").with_source(err)
|
||||
})?;
|
||||
|
||||
let verification_url = format!(
|
||||
"{}/verify-email?token={}",
|
||||
state.config.public_base_url, verification_token
|
||||
);
|
||||
|
||||
mail::send_verification_email(&state, &user.email, &user.username, &verification_url)
|
||||
.await
|
||||
.map_err(|err| {
|
||||
AppError::new(ErrorCode::MailSendFailed, "验证邮件发送失败").with_source(err)
|
||||
})?;
|
||||
}
|
||||
|
||||
let body = RegisterResponse {
|
||||
user: UserView {
|
||||
id: user.id,
|
||||
@@ -145,7 +153,11 @@ async fn register(
|
||||
email_verified: user.email_verified_at.is_some(),
|
||||
},
|
||||
token,
|
||||
message: "注册成功,验证邮件已发送至您的邮箱".to_string(),
|
||||
message: if verification_required {
|
||||
"注册成功,验证邮件已发送至您的邮箱".to_string()
|
||||
} else {
|
||||
"注册成功".to_string()
|
||||
},
|
||||
};
|
||||
|
||||
Ok(Json(Envelope {
|
||||
@@ -213,6 +225,7 @@ async fn login(
|
||||
}
|
||||
|
||||
verify_password(&req.password, &user.password_hash)?;
|
||||
let verification_required = settings::email_verification_required(&state).await?;
|
||||
|
||||
let (token, expires_at) = auth::issue_jwt(
|
||||
&state.config.jwt_secret,
|
||||
@@ -231,7 +244,7 @@ async fn login(
|
||||
email: user.email,
|
||||
username: user.username,
|
||||
role: user.role,
|
||||
email_verified: user.email_verified_at.is_some(),
|
||||
email_verified: user.email_verified_at.is_some() || !verification_required,
|
||||
},
|
||||
},
|
||||
}))
|
||||
@@ -248,6 +261,15 @@ async fn send_verification(
|
||||
) -> Result<Json<Envelope<MessageResponse>>, AppError> {
|
||||
let claims = auth::require_jwt(&state.config.jwt_secret, &headers)?;
|
||||
|
||||
if !settings::email_verification_required(&state).await? {
|
||||
return Ok(Json(Envelope {
|
||||
success: true,
|
||||
data: MessageResponse {
|
||||
message: "邮箱验证功能当前已关闭,无需验证".to_string(),
|
||||
},
|
||||
}));
|
||||
}
|
||||
|
||||
// Rate limit: 1 per minute per user
|
||||
let key = format!(
|
||||
"rate:send_verification:{}:{}",
|
||||
|
||||
Reference in New Issue
Block a user