use crate::auth::password::{PasswordHasher, PasswordPolicy};
use crate::auth::storage::UserCreator;
use crate::error::{Result, TidewayError};
use super::types::RegisterRequest;
pub struct RegistrationFlow<C: UserCreator> {
user_creator: C,
password_hasher: PasswordHasher,
password_policy: PasswordPolicy,
}
impl<C: UserCreator> RegistrationFlow<C> {
pub fn new(user_creator: C) -> Self {
Self {
user_creator,
password_hasher: PasswordHasher::default(),
password_policy: PasswordPolicy::modern(),
}
}
pub fn with_policy(mut self, policy: PasswordPolicy) -> Self {
self.password_policy = policy;
self
}
pub fn with_hasher(mut self, hasher: PasswordHasher) -> Self {
self.password_hasher = hasher;
self
}
#[cfg(feature = "auth")]
pub async fn register(&self, req: RegisterRequest) -> Result<C::User> {
let email = req.email.trim().to_lowercase();
if !is_valid_email(&email) {
tracing::info!(
target: "auth.register.failed",
email = %email,
reason = "invalid_email_format",
"Registration failed: invalid email format"
);
return Err(TidewayError::BadRequest("Invalid email format".into()));
}
if let Err(e) = self.password_policy.check(&req.password) {
tracing::info!(
target: "auth.register.failed",
email = %email,
reason = "weak_password",
"Registration failed: password policy violation"
);
return Err(e);
}
if self.user_creator.email_exists(&email).await? {
tracing::info!(
target: "auth.register.failed",
email = %email,
reason = "email_exists",
"Registration failed: email already registered"
);
return Err(TidewayError::BadRequest("Registration failed".into()));
}
let hash = self.password_hasher.hash(&req.password)?;
let user = self
.user_creator
.create_user(&email, &hash, req.name.as_deref())
.await?;
let user_id = self.user_creator.user_id(&user);
tracing::info!(
target: "auth.register.success",
user_id = %user_id,
email = %email,
"User registered successfully"
);
if let Err(e) = self.user_creator.send_verification_email(&user).await {
tracing::warn!(
target: "auth.register.verification_email_failed",
user_id = %user_id,
email = %email,
error = %e,
"Failed to send verification email"
);
}
Ok(user)
}
#[cfg(not(feature = "auth"))]
pub async fn register(&self, _req: RegisterRequest) -> Result<C::User> {
Err(TidewayError::Internal("auth feature not enabled".into()))
}
}
fn is_valid_email(email: &str) -> bool {
let parts: Vec<&str> = email.split('@').collect();
if parts.len() != 2 {
return false;
}
let local = parts[0];
let domain = parts[1];
!local.is_empty()
&& !domain.is_empty()
&& domain.contains('.')
&& !domain.starts_with('.')
&& !domain.ends_with('.')
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_valid_emails() {
assert!(is_valid_email("user@example.com"));
assert!(is_valid_email("user.name@example.com"));
assert!(is_valid_email("user+tag@example.co.uk"));
}
#[test]
fn test_invalid_emails() {
assert!(!is_valid_email("userexample.com"));
assert!(!is_valid_email("user@"));
assert!(!is_valid_email("@example.com"));
assert!(!is_valid_email("user@.com"));
assert!(!is_valid_email("user@example."));
assert!(!is_valid_email("user@@example.com"));
}
}