use crate::server::middleware::AuthRateLimiter;
use crate::server::routes::ApiResponse;
use crate::server::state::AppState;
use crate::utils::auth::crypto::password::hash_password;
use crate::utils::data::validation::DataValidator;
use actix_web::{HttpRequest, HttpResponse, Result as ActixResult, web};
use std::sync::Arc;
use std::sync::atomic::{AtomicU64, Ordering};
use tracing::{error, info, warn};
use super::models::{RegisterRequest, RegisterResponse};
static REGISTER_RATE_LIMITER: std::sync::OnceLock<Arc<AuthRateLimiter>> =
std::sync::OnceLock::new();
fn get_register_rate_limiter() -> Arc<AuthRateLimiter> {
REGISTER_RATE_LIMITER
.get_or_init(|| Arc::new(AuthRateLimiter::new(10, 3600, 3600)))
.clone()
}
fn extract_client_ip(req: &HttpRequest) -> String {
req.connection_info()
.peer_addr()
.unwrap_or("unknown")
.to_string()
}
static REQUEST_COUNTER: AtomicU64 = AtomicU64::new(0);
const DUPLICATE_CREDENTIALS_ERROR: &str =
"Registration failed. An account with these credentials may already exist.";
pub async fn register(
req: HttpRequest,
state: web::Data<AppState>,
request: web::Json<RegisterRequest>,
) -> ActixResult<HttpResponse> {
let client_ip = extract_client_ip(&req);
let limiter = get_register_rate_limiter();
let count = REQUEST_COUNTER.fetch_add(1, Ordering::Relaxed);
if count.is_multiple_of(100) {
limiter.cleanup_old_entries();
}
if let Err(retry_after) = limiter.check_allowed(&client_ip) {
warn!(
"Registration rate limit exceeded for IP {}: retry after {}s",
client_ip, retry_after
);
return Ok(HttpResponse::TooManyRequests()
.insert_header(("Retry-After", retry_after.to_string()))
.json(ApiResponse::<()>::error(
"Too many registration attempts. Please try again later.".to_string(),
)));
}
limiter.record_failure(&client_ip);
info!("User registration attempt: {}", request.username);
if let Err(e) = DataValidator::validate_username(&request.username) {
return Ok(
HttpResponse::BadRequest().json(ApiResponse::<()>::error_for_type(e.to_string()))
);
}
if let Err(e) = crate::utils::config::helpers::ConfigValidator::validate_email(&request.email) {
return Ok(
HttpResponse::BadRequest().json(ApiResponse::<()>::error_for_type(e.to_string()))
);
}
if let Err(e) = DataValidator::validate_password(&request.password) {
return Ok(
HttpResponse::BadRequest().json(ApiResponse::<()>::error_for_type(e.to_string()))
);
}
match state
.storage
.database
.find_user_by_username(&request.username)
.await
{
Ok(Some(_)) => {
return Ok(HttpResponse::BadRequest().json(ApiResponse::<()>::error(
DUPLICATE_CREDENTIALS_ERROR.to_string(),
)));
}
Ok(None) => {} Err(e) => {
error!("Failed to check existing user: {}", e);
return Ok(HttpResponse::InternalServerError()
.json(ApiResponse::<()>::error("Database error".to_string())));
}
}
match state
.storage
.database
.find_user_by_email(&request.email)
.await
{
Ok(Some(_)) => {
return Ok(HttpResponse::BadRequest().json(ApiResponse::<()>::error(
DUPLICATE_CREDENTIALS_ERROR.to_string(),
)));
}
Ok(None) => {} Err(e) => {
error!("Failed to check existing email: {}", e);
return Ok(HttpResponse::InternalServerError()
.json(ApiResponse::<()>::error("Database error".to_string())));
}
}
let password_hash = match hash_password(&request.password) {
Ok(hash) => hash,
Err(e) => {
error!("Failed to hash password: {}", e);
return Ok(
HttpResponse::InternalServerError().json(ApiResponse::<()>::error(
"Password hashing failed".to_string(),
)),
);
}
};
let user = crate::core::models::user::types::User::new(
request.username.clone(),
request.email.clone(),
password_hash,
);
match state.storage.database.create_user(&user).await {
Ok(created_user) => {
info!("User registered successfully: {}", created_user.username);
let response = RegisterResponse {
user_id: created_user.id(),
username: created_user.username,
email: created_user.email,
message: "Registration successful. Please verify your email.".to_string(),
};
Ok(HttpResponse::Created().json(ApiResponse::success(response)))
}
Err(e) => {
error!("Failed to create user: {}", e);
Ok(HttpResponse::InternalServerError()
.json(ApiResponse::<()>::error("User creation failed".to_string())))
}
}
}