//! HTTP handlers for user account management.
//!
//! These endpoints allow users to manage their account settings:
//! - Change password
//! - Change email
//! - Delete account
use axum::{
extract::State,
http::{HeaderMap, StatusCode},
response::IntoResponse,
Json,
};
use serde::{Deserialize, Serialize};
use utoipa::ToSchema;
use crate::server::auth::UserOnly;
use crate::server::messages::ApiError;
use crate::server::state::SharedState;
// =============================================================================
// Request/Response Types
// =============================================================================
/// Request to change password.
#[derive(Debug, Deserialize, ToSchema)]
#[serde(rename_all = "camelCase")]
pub struct ChangePasswordRequest {
/// The user's current password for verification
pub current_password: String,
/// The new password to set
pub new_password: String,
}
/// Response after changing password.
#[derive(Debug, Serialize, ToSchema)]
#[serde(rename_all = "camelCase")]
pub struct ChangePasswordResponse {
pub success: bool,
pub message: String,
}
/// Request to change email.
#[derive(Debug, Deserialize, ToSchema)]
#[serde(rename_all = "camelCase")]
pub struct ChangeEmailRequest {
/// The user's password for verification
pub password: String,
/// The new email address to set
pub new_email: String,
}
/// Response after changing email.
#[derive(Debug, Serialize, ToSchema)]
#[serde(rename_all = "camelCase")]
pub struct ChangeEmailResponse {
pub success: bool,
pub message: String,
/// Whether a verification email was sent to the new address
pub verification_sent: bool,
}
/// Request to delete account.
#[derive(Debug, Deserialize, ToSchema)]
#[serde(rename_all = "camelCase")]
pub struct DeleteAccountRequest {
/// The user's password for verification
pub password: String,
/// Confirmation text - must match the user's email
pub confirmation: String,
}
/// Response after deleting account.
#[derive(Debug, Serialize, ToSchema)]
#[serde(rename_all = "camelCase")]
pub struct DeleteAccountResponse {
pub success: bool,
pub message: String,
}
// =============================================================================
// Password Validation
// =============================================================================
/// Password strength validation result.
#[derive(Debug)]
pub struct PasswordValidation {
pub is_valid: bool,
pub errors: Vec<String>,
}
/// Validate password strength.
/// Requirements:
/// - At least 6 characters (matches login form)
fn validate_password_strength(password: &str) -> PasswordValidation {
let mut errors = Vec::new();
if password.len() < 6 {
errors.push("Password must be at least 6 characters long".to_string());
}
PasswordValidation {
is_valid: errors.is_empty(),
errors,
}
}
/// Validate email format.
fn validate_email(email: &str) -> bool {
// Basic email validation - must contain @ and at least one . after @
let parts: Vec<&str> = email.split('@').collect();
if parts.len() != 2 {
return false;
}
let local = parts[0];
let domain = parts[1];
// Local part must not be empty
if local.is_empty() {
return false;
}
// Domain must have at least one dot and not start/end with dot
domain.contains('.') && !domain.starts_with('.') && !domain.ends_with('.')
}
// =============================================================================
// Supabase Admin Client
// =============================================================================
/// Supabase Admin API client for user management operations.
/// Uses the service role key for admin-level operations.
pub struct SupabaseAdminClient {
base_url: String,
secret_api_key: String,
client: reqwest::Client,
}
impl SupabaseAdminClient {
/// Create a new Supabase admin client from environment variables.
pub fn from_env() -> Option<Self> {
let base_url = std::env::var("SUPABASE_URL").ok()?;
let secret_api_key = std::env::var("SUPABASE_SECRET_API_KEY").ok()?;
Some(Self {
base_url,
secret_api_key,
client: reqwest::Client::new(),
})
}
/// Verify a user's password by attempting to sign in.
pub async fn verify_password(&self, email: &str, password: &str) -> Result<bool, String> {
let url = format!("{}/auth/v1/token?grant_type=password", self.base_url);
let response = self
.client
.post(&url)
.header("apikey", &self.secret_api_key)
.header("Content-Type", "application/json")
.json(&serde_json::json!({
"email": email,
"password": password
}))
.send()
.await
.map_err(|e| format!("Failed to verify password: {}", e))?;
Ok(response.status().is_success())
}
/// Update a user's password.
pub async fn update_password(
&self,
user_id: &str,
new_password: &str,
) -> Result<(), String> {
let url = format!("{}/auth/v1/admin/users/{}", self.base_url, user_id);
let response = self
.client
.put(&url)
.header("apikey", &self.secret_api_key)
.header("Authorization", format!("Bearer {}", self.secret_api_key))
.header("Content-Type", "application/json")
.json(&serde_json::json!({
"password": new_password
}))
.send()
.await
.map_err(|e| format!("Failed to update password: {}", e))?;
if response.status().is_success() {
Ok(())
} else {
let error_text = response.text().await.unwrap_or_default();
Err(format!("Failed to update password: {}", error_text))
}
}
/// Update a user's email.
pub async fn update_email(
&self,
user_id: &str,
new_email: &str,
) -> Result<(), String> {
let url = format!("{}/auth/v1/admin/users/{}", self.base_url, user_id);
let response = self
.client
.put(&url)
.header("apikey", &self.secret_api_key)
.header("Authorization", format!("Bearer {}", self.secret_api_key))
.header("Content-Type", "application/json")
.json(&serde_json::json!({
"email": new_email,
"email_confirm": true
}))
.send()
.await
.map_err(|e| format!("Failed to update email: {}", e))?;
if response.status().is_success() {
Ok(())
} else {
let error_text = response.text().await.unwrap_or_default();
Err(format!("Failed to update email: {}", error_text))
}
}
/// Delete a user from Supabase Auth.
pub async fn delete_user(&self, user_id: &str) -> Result<(), String> {
let url = format!("{}/auth/v1/admin/users/{}", self.base_url, user_id);
let response = self
.client
.delete(&url)
.header("apikey", &self.secret_api_key)
.header("Authorization", format!("Bearer {}", self.secret_api_key))
.send()
.await
.map_err(|e| format!("Failed to delete user: {}", e))?;
if response.status().is_success() {
Ok(())
} else {
let error_text = response.text().await.unwrap_or_default();
Err(format!("Failed to delete user: {}", error_text))
}
}
/// Get user info including email.
pub async fn get_user(&self, user_id: &str) -> Result<Option<String>, String> {
let url = format!("{}/auth/v1/admin/users/{}", self.base_url, user_id);
let response = self
.client
.get(&url)
.header("apikey", &self.secret_api_key)
.header("Authorization", format!("Bearer {}", self.secret_api_key))
.send()
.await
.map_err(|e| format!("Failed to get user: {}", e))?;
if response.status().is_success() {
let json: serde_json::Value = response
.json()
.await
.map_err(|e| format!("Failed to parse user data: {}", e))?;
Ok(json.get("email").and_then(|e| e.as_str()).map(String::from))
} else if response.status() == reqwest::StatusCode::NOT_FOUND {
Ok(None)
} else {
let error_text = response.text().await.unwrap_or_default();
Err(format!("Failed to get user: {}", error_text))
}
}
}
// =============================================================================
// Supabase User Client (uses user's JWT, no admin required)
// =============================================================================
/// Supabase User API client for self-service operations.
/// Uses the user's JWT token - no admin/service role key required.
pub struct SupabaseUserClient {
base_url: String,
anon_key: String,
jwt_token: String,
client: reqwest::Client,
}
impl SupabaseUserClient {
/// Create a new Supabase user client from environment and JWT token.
pub fn new(jwt_token: String) -> Option<Self> {
let base_url = std::env::var("SUPABASE_URL").ok()?;
let anon_key = std::env::var("SUPABASE_ANON_KEY").ok()?;
Some(Self {
base_url,
anon_key,
jwt_token,
client: reqwest::Client::new(),
})
}
/// Update the user's password using their own JWT.
pub async fn update_password(&self, new_password: &str) -> Result<(), String> {
let url = format!("{}/auth/v1/user", self.base_url);
let response = self
.client
.put(&url)
.header("apikey", &self.anon_key)
.header("Authorization", format!("Bearer {}", self.jwt_token))
.header("Content-Type", "application/json")
.json(&serde_json::json!({
"password": new_password
}))
.send()
.await
.map_err(|e| format!("Failed to update password: {}", e))?;
if response.status().is_success() {
Ok(())
} else {
let error_text = response.text().await.unwrap_or_default();
Err(format!("Failed to update password: {}", error_text))
}
}
/// Update the user's email using their own JWT.
pub async fn update_email(&self, new_email: &str) -> Result<(), String> {
let url = format!("{}/auth/v1/user", self.base_url);
let response = self
.client
.put(&url)
.header("apikey", &self.anon_key)
.header("Authorization", format!("Bearer {}", self.jwt_token))
.header("Content-Type", "application/json")
.json(&serde_json::json!({
"email": new_email
}))
.send()
.await
.map_err(|e| format!("Failed to update email: {}", e))?;
if response.status().is_success() {
Ok(())
} else {
let error_text = response.text().await.unwrap_or_default();
Err(format!("Failed to update email: {}", error_text))
}
}
/// Verify current password by attempting to sign in.
pub async fn verify_password(&self, email: &str, password: &str) -> Result<bool, String> {
let url = format!("{}/auth/v1/token?grant_type=password", self.base_url);
let response = self
.client
.post(&url)
.header("apikey", &self.anon_key)
.header("Content-Type", "application/json")
.json(&serde_json::json!({
"email": email,
"password": password
}))
.send()
.await
.map_err(|e| format!("Failed to verify password: {}", e))?;
Ok(response.status().is_success())
}
}
// =============================================================================
// Handlers
// =============================================================================
/// Change the authenticated user's password.
///
/// Requires verification of the current password before allowing the change.
/// The new password must meet strength requirements.
#[utoipa::path(
put,
path = "/api/v1/users/me/password",
request_body = ChangePasswordRequest,
responses(
(status = 200, description = "Password changed successfully", body = ChangePasswordResponse),
(status = 400, description = "Invalid request (weak password, wrong current password)", body = ApiError),
(status = 401, description = "Not authenticated", body = ApiError),
(status = 403, description = "Forbidden (tool keys not allowed)", body = ApiError),
(status = 503, description = "Supabase not configured", body = ApiError),
(status = 500, description = "Internal server error", body = ApiError),
),
security(
("bearer_auth" = [])
),
tag = "Users"
)]
pub async fn change_password_handler(
State(_state): State<SharedState>,
headers: HeaderMap,
UserOnly(user): UserOnly,
Json(req): Json<ChangePasswordRequest>,
) -> impl IntoResponse {
// Validate new password strength
let validation = validate_password_strength(&req.new_password);
if !validation.is_valid {
return (
StatusCode::BAD_REQUEST,
Json(ApiError::new(
"WEAK_PASSWORD",
&validation.errors.join("; "),
)),
)
.into_response();
}
// Get user's email (required for password verification)
let email = match &user.email {
Some(email) => email.clone(),
None => {
return (
StatusCode::BAD_REQUEST,
Json(ApiError::new("EMAIL_REQUIRED", "User email not available")),
)
.into_response();
}
};
// Extract JWT from Authorization header for user-level API calls
let jwt_token = headers
.get("Authorization")
.and_then(|v| v.to_str().ok())
.and_then(|s| s.strip_prefix("Bearer "))
.map(|s| s.to_string());
// Try user client first (uses JWT, no admin required), fall back to admin client
if let Some(token) = jwt_token {
if let Some(user_client) = SupabaseUserClient::new(token) {
// Verify current password
match user_client.verify_password(&email, &req.current_password).await {
Ok(true) => {}
Ok(false) => {
return (
StatusCode::BAD_REQUEST,
Json(ApiError::new("INVALID_PASSWORD", "Current password is incorrect")),
)
.into_response();
}
Err(e) => {
tracing::error!("Failed to verify password: {}", e);
return (
StatusCode::INTERNAL_SERVER_ERROR,
Json(ApiError::new("INTERNAL_ERROR", "Failed to verify password")),
)
.into_response();
}
}
// Update password using user's JWT
return match user_client.update_password(&req.new_password).await {
Ok(()) => {
tracing::info!("Password changed for user {}", user.user_id);
Json(ChangePasswordResponse {
success: true,
message: "Password changed successfully".to_string(),
})
.into_response()
}
Err(e) => {
tracing::error!("Failed to update password: {}", e);
(
StatusCode::INTERNAL_SERVER_ERROR,
Json(ApiError::new("INTERNAL_ERROR", "Failed to update password")),
)
.into_response()
}
};
}
}
// Fall back to admin client if user client not available
let admin_client = match SupabaseAdminClient::from_env() {
Some(client) => client,
None => {
return (
StatusCode::SERVICE_UNAVAILABLE,
Json(ApiError::new(
"SUPABASE_NOT_CONFIGURED",
"Supabase not configured. Ensure SUPABASE_URL and SUPABASE_ANON_KEY are set.",
)),
)
.into_response();
}
};
// Verify current password
match admin_client.verify_password(&email, &req.current_password).await {
Ok(true) => {}
Ok(false) => {
return (
StatusCode::BAD_REQUEST,
Json(ApiError::new("INVALID_PASSWORD", "Current password is incorrect")),
)
.into_response();
}
Err(e) => {
tracing::error!("Failed to verify password: {}", e);
return (
StatusCode::INTERNAL_SERVER_ERROR,
Json(ApiError::new("INTERNAL_ERROR", "Failed to verify password")),
)
.into_response();
}
}
// Update password in Supabase
match admin_client
.update_password(&user.user_id.to_string(), &req.new_password)
.await
{
Ok(()) => {
tracing::info!("Password changed for user {}", user.user_id);
Json(ChangePasswordResponse {
success: true,
message: "Password changed successfully".to_string(),
})
.into_response()
}
Err(e) => {
tracing::error!("Failed to update password: {}", e);
(
StatusCode::INTERNAL_SERVER_ERROR,
Json(ApiError::new("INTERNAL_ERROR", "Failed to update password")),
)
.into_response()
}
}
}
/// Change the authenticated user's email address.
///
/// Requires password verification before allowing the change.
/// The new email will be updated directly (Supabase handles verification if configured).
#[utoipa::path(
put,
path = "/api/v1/users/me/email",
request_body = ChangeEmailRequest,
responses(
(status = 200, description = "Email changed successfully", body = ChangeEmailResponse),
(status = 400, description = "Invalid request (invalid email, wrong password)", body = ApiError),
(status = 401, description = "Not authenticated", body = ApiError),
(status = 403, description = "Forbidden (tool keys not allowed)", body = ApiError),
(status = 503, description = "Supabase not configured", body = ApiError),
(status = 500, description = "Internal server error", body = ApiError),
),
security(
("bearer_auth" = [])
),
tag = "Users"
)]
pub async fn change_email_handler(
State(state): State<SharedState>,
headers: HeaderMap,
UserOnly(user): UserOnly,
Json(req): Json<ChangeEmailRequest>,
) -> impl IntoResponse {
let Some(ref pool) = state.db_pool else {
return (
StatusCode::SERVICE_UNAVAILABLE,
Json(ApiError::new("DB_UNAVAILABLE", "Database not configured")),
)
.into_response();
};
// Validate new email format
if !validate_email(&req.new_email) {
return (
StatusCode::BAD_REQUEST,
Json(ApiError::new("INVALID_EMAIL", "Invalid email format")),
)
.into_response();
}
// Get user's current email (required for password verification)
let current_email = match &user.email {
Some(email) => email.clone(),
None => {
return (
StatusCode::BAD_REQUEST,
Json(ApiError::new("EMAIL_REQUIRED", "User email not available")),
)
.into_response();
}
};
// Extract JWT from Authorization header for user-level API calls
let jwt_token = headers
.get("Authorization")
.and_then(|v| v.to_str().ok())
.and_then(|s| s.strip_prefix("Bearer "))
.map(|s| s.to_string());
// Try user client first (uses JWT, no admin required), fall back to admin client
if let Some(token) = jwt_token {
if let Some(user_client) = SupabaseUserClient::new(token) {
// Verify password
match user_client.verify_password(¤t_email, &req.password).await {
Ok(true) => {}
Ok(false) => {
return (
StatusCode::BAD_REQUEST,
Json(ApiError::new("INVALID_PASSWORD", "Password is incorrect")),
)
.into_response();
}
Err(e) => {
tracing::error!("Failed to verify password: {}", e);
return (
StatusCode::INTERNAL_SERVER_ERROR,
Json(ApiError::new("INTERNAL_ERROR", "Failed to verify password")),
)
.into_response();
}
}
// Update email using user's JWT
if let Err(e) = user_client.update_email(&req.new_email).await {
tracing::error!("Failed to update email in Supabase: {}", e);
return (
StatusCode::INTERNAL_SERVER_ERROR,
Json(ApiError::new("INTERNAL_ERROR", "Failed to update email")),
)
.into_response();
}
// Update email in our database
if let Err(e) = sqlx::query("UPDATE users SET email = $1, updated_at = NOW() WHERE id = $2")
.bind(&req.new_email)
.bind(user.user_id)
.execute(pool)
.await
{
tracing::error!("Failed to update email in database: {}", e);
}
tracing::info!(
"Email changed for user {} from {} to {}",
user.user_id,
current_email,
req.new_email
);
return Json(ChangeEmailResponse {
success: true,
message: "Email changed successfully".to_string(),
verification_sent: false,
})
.into_response();
}
}
// Fall back to admin client if user client not available
let admin_client = match SupabaseAdminClient::from_env() {
Some(client) => client,
None => {
return (
StatusCode::SERVICE_UNAVAILABLE,
Json(ApiError::new(
"SUPABASE_NOT_CONFIGURED",
"Supabase not configured. Ensure SUPABASE_URL and SUPABASE_ANON_KEY are set.",
)),
)
.into_response();
}
};
// Verify password
match admin_client.verify_password(¤t_email, &req.password).await {
Ok(true) => {}
Ok(false) => {
return (
StatusCode::BAD_REQUEST,
Json(ApiError::new("INVALID_PASSWORD", "Password is incorrect")),
)
.into_response();
}
Err(e) => {
tracing::error!("Failed to verify password: {}", e);
return (
StatusCode::INTERNAL_SERVER_ERROR,
Json(ApiError::new("INTERNAL_ERROR", "Failed to verify password")),
)
.into_response();
}
}
// Update email in Supabase
if let Err(e) = admin_client
.update_email(&user.user_id.to_string(), &req.new_email)
.await
{
tracing::error!("Failed to update email in Supabase: {}", e);
return (
StatusCode::INTERNAL_SERVER_ERROR,
Json(ApiError::new("INTERNAL_ERROR", "Failed to update email")),
)
.into_response();
}
// Update email in our database
if let Err(e) = sqlx::query("UPDATE users SET email = $1, updated_at = NOW() WHERE id = $2")
.bind(&req.new_email)
.bind(user.user_id)
.execute(pool)
.await
{
tracing::error!("Failed to update email in database: {}", e);
}
tracing::info!(
"Email changed for user {} from {} to {}",
user.user_id,
current_email,
req.new_email
);
Json(ChangeEmailResponse {
success: true,
message: "Email changed successfully".to_string(),
verification_sent: false,
})
.into_response()
}
/// Delete the authenticated user's account.
///
/// This permanently deletes:
/// - The user's Supabase Auth account
/// - The user's record in our database
/// - All associated data (API keys, files, tasks, etc. via CASCADE)
///
/// Requires password verification and confirmation text matching the user's email.
#[utoipa::path(
delete,
path = "/api/v1/users/me",
request_body = DeleteAccountRequest,
responses(
(status = 200, description = "Account deleted successfully", body = DeleteAccountResponse),
(status = 400, description = "Invalid request (wrong password, wrong confirmation)", body = ApiError),
(status = 401, description = "Not authenticated", body = ApiError),
(status = 403, description = "Forbidden (tool keys not allowed)", body = ApiError),
(status = 503, description = "Supabase not configured", body = ApiError),
(status = 500, description = "Internal server error", body = ApiError),
),
security(
("bearer_auth" = [])
),
tag = "Users"
)]
pub async fn delete_account_handler(
State(state): State<SharedState>,
UserOnly(user): UserOnly,
Json(req): Json<DeleteAccountRequest>,
) -> impl IntoResponse {
let Some(ref pool) = state.db_pool else {
return (
StatusCode::SERVICE_UNAVAILABLE,
Json(ApiError::new("DB_UNAVAILABLE", "Database not configured")),
)
.into_response();
};
// Get Supabase admin client - required for full account deletion
let admin_client = match SupabaseAdminClient::from_env() {
Some(client) => client,
None => {
return (
StatusCode::SERVICE_UNAVAILABLE,
Json(ApiError::new(
"SUPABASE_ADMIN_NOT_CONFIGURED",
"Account deletion requires SUPABASE_SECRET_API_KEY to be configured",
)),
)
.into_response();
}
};
// Verify confirmation is "DELETE MY ACCOUNT"
const REQUIRED_CONFIRMATION: &str = "DELETE MY ACCOUNT";
if req.confirmation != REQUIRED_CONFIRMATION {
return (
StatusCode::BAD_REQUEST,
Json(ApiError::new(
"INVALID_CONFIRMATION",
format!("Confirmation text must be exactly: {}", REQUIRED_CONFIRMATION),
)),
)
.into_response();
}
// Get user's email (required for password verification)
let email = match &user.email {
Some(e) => e.clone(),
None => {
return (
StatusCode::BAD_REQUEST,
Json(ApiError::new("EMAIL_REQUIRED", "User email not available")),
)
.into_response();
}
};
// Verify password
match admin_client.verify_password(&email, &req.password).await {
Ok(true) => {}
Ok(false) => {
return (
StatusCode::BAD_REQUEST,
Json(ApiError::new("INVALID_PASSWORD", "Password is incorrect")),
)
.into_response();
}
Err(e) => {
tracing::error!("Failed to verify password: {}", e);
return (
StatusCode::INTERNAL_SERVER_ERROR,
Json(ApiError::new("INTERNAL_ERROR", "Failed to verify password")),
)
.into_response();
}
}
// Delete from our database first (this will cascade to related records)
// Get the owner_id before deleting
let owner_id = user.owner_id;
// Delete API keys for this user (explicit deletion for audit purposes)
if let Err(e) = sqlx::query("UPDATE api_keys SET revoked_at = NOW() WHERE user_id = $1 AND revoked_at IS NULL")
.bind(user.user_id)
.execute(pool)
.await
{
tracing::warn!("Failed to revoke API keys during account deletion: {}", e);
}
// Delete user record
if let Err(e) = sqlx::query("DELETE FROM users WHERE id = $1")
.bind(user.user_id)
.execute(pool)
.await
{
tracing::error!("Failed to delete user from database: {}", e);
return (
StatusCode::INTERNAL_SERVER_ERROR,
Json(ApiError::new("INTERNAL_ERROR", "Failed to delete account")),
)
.into_response();
}
// Delete files owned by this user
if let Err(e) = sqlx::query("DELETE FROM files WHERE owner_id = $1")
.bind(owner_id)
.execute(pool)
.await
{
tracing::warn!("Failed to delete user files: {}", e);
}
// Delete tasks owned by this user
if let Err(e) = sqlx::query("DELETE FROM tasks WHERE owner_id = $1")
.bind(owner_id)
.execute(pool)
.await
{
tracing::warn!("Failed to delete user tasks: {}", e);
}
// Delete mesh chat conversations owned by this user
if let Err(e) = sqlx::query("DELETE FROM mesh_chat_conversations WHERE owner_id = $1")
.bind(owner_id)
.execute(pool)
.await
{
tracing::warn!("Failed to delete mesh chat conversations: {}", e);
}
// Delete daemons owned by this user
if let Err(e) = sqlx::query("DELETE FROM daemons WHERE owner_id = $1")
.bind(owner_id)
.execute(pool)
.await
{
tracing::warn!("Failed to delete user daemons: {}", e);
}
// Delete owner record
if let Err(e) = sqlx::query("DELETE FROM owners WHERE id = $1")
.bind(owner_id)
.execute(pool)
.await
{
tracing::warn!("Failed to delete owner record: {}", e);
}
// Delete from Supabase Auth
if let Err(e) = admin_client.delete_user(&user.user_id.to_string()).await {
tracing::error!("Failed to delete user from Supabase Auth: {}", e);
return (
StatusCode::INTERNAL_SERVER_ERROR,
Json(ApiError::new(
"SUPABASE_DELETE_FAILED",
"Failed to delete user from authentication system",
)),
)
.into_response();
}
tracing::info!("Account deleted for user {} ({})", user.user_id, email);
Json(DeleteAccountResponse {
success: true,
message: "Account deleted successfully".to_string(),
})
.into_response()
}
// =============================================================================
// Tests
// =============================================================================
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_password_validation_success() {
// Minimum 6 characters
let result = validate_password_strength("abcdef");
assert!(result.is_valid);
assert!(result.errors.is_empty());
let result = validate_password_strength("Password123");
assert!(result.is_valid);
assert!(result.errors.is_empty());
}
#[test]
fn test_password_validation_too_short() {
let result = validate_password_strength("12345");
assert!(!result.is_valid);
assert!(result.errors.iter().any(|e| e.contains("6 characters")));
}
#[test]
fn test_email_validation_valid() {
assert!(validate_email("user@example.com"));
assert!(validate_email("user.name@example.co.uk"));
assert!(validate_email("user+tag@example.org"));
}
#[test]
fn test_email_validation_invalid() {
assert!(!validate_email("userexample.com"));
assert!(!validate_email("user@"));
assert!(!validate_email("@example.com"));
assert!(!validate_email("user@.com"));
assert!(!validate_email("user@example."));
}
}