seed.rs 241 lignes · 7534 octets
//! Seed data binary for RustWarden
//!
//! This binary loads test data (Happy Days characters) into the database.
//! It is intended for development and testing only.
//!
//! Usage:
//!   cargo run --bin seed
//!   cargo run --bin seed -- --force  # Force execution even if data exists
//!
//! Safety:
//!   - Refuses to run in production environment
//!   - Checks for existing data before creating
//!   - Idempotent (safe to run multiple times)

use chrono::Utc;
use dotenvy::dotenv;
use rustwarden_core::config::database::create_connection;
use rustwarden_core::error::AppError;
use rustwarden_core::services::password_service::PasswordService;
use rustwarden_core::services::role_service::RoleService;
use rustwarden_core::services::user_service::UserService;
use sea_orm::{ActiveModelTrait, ColumnTrait, EntityTrait, PaginatorTrait, QueryFilter, Set};
use std::env;
use tracing::{error, info, warn};

/// Happy Days characters to seed
const SEED_USERS: &[(&str, &str, &str)] = &[
    ("richie", "richie@rustwarden.local", "user"),
    ("fonzie", "fonzie@rustwarden.local", "user"),
    ("potsie", "potsie@rustwarden.local", "user"),
    ("ralph", "ralph@rustwarden.local", "user"),
    ("joanie", "joanie@rustwarden.local", "admin"),
];

const HAPPY_DAYS_PASSWORD: &str = "happydays123";

fn validate_email_format(email: &str) -> Result<(), String> {
    if email.is_empty() {
        return Err("Email cannot be empty".to_string());
    }

    let parts: Vec<&str> = email.split('@').collect();
    if parts.len() != 2 {
        return Err(format!(
            "Email must contain exactly one @ symbol: {}",
            email
        ));
    }

    let local = parts[0];
    let domain = parts[1];

    if local.is_empty() {
        return Err(format!("Email local part cannot be empty: {}", email));
    }

    if domain.is_empty() {
        return Err(format!("Email domain cannot be empty: {}", email));
    }

    if !domain.contains('.') {
        return Err(format!(
            "Email domain must contain at least one dot: {}",
            email
        ));
    }

    Ok(())
}

#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
    dotenv().ok();

    tracing_subscriber::fmt()
        .with_env_filter(tracing_subscriber::EnvFilter::from_default_env())
        .init();

    info!("Starting seed data loading...");

    let environment = env::var("ENVIRONMENT").unwrap_or_else(|_| "development".to_string());
    if environment == "production" {
        error!("Seed data cannot be loaded in production environment");
        error!("Set ENVIRONMENT=development to allow seed data loading");
        std::process::exit(1);
    }

    let force = env::args().any(|arg| arg == "--force");

    let db = create_connection().await?;
    info!("Database connection established");

    info!("Initializing default roles...");
    match RoleService::initialize_default_roles(&db).await {
        Ok((admin_role, user_role, _app_manager_role)) => {
            info!(
                "Default roles initialized: admin={}, user={}",
                admin_role.id, user_role.id
            );
        }
        Err(e) => {
            error!("Failed to initialize default roles: {}", e);
            return Err(format!("Failed to initialize default roles: {}", e).into());
        }
    }

    use rustwarden_core::models::user;
    let user_count = user::Entity::find()
        .count(&db)
        .await
        .map_err(|e| AppError::Internal(format!("Database error: {}", e)))?;

    if user_count > 10 && !force {
        warn!("Database already contains {} users", user_count);
        warn!("Use --force to load seed data anyway");
        warn!("Seed data loading skipped");
        return Ok(());
    }

    if user_count > 10 && force {
        warn!("Force mode: loading seed data despite existing users");
    }

    let mut created = 0;
    let mut skipped = 0;

    for (username, email, role_name) in SEED_USERS {
        if let Err(e) = validate_email_format(email) {
            error!("{} - Skipping user creation", e);
            skipped += 1;
            continue;
        }

        let existing_user = user::Entity::find()
            .filter(user::Column::Username.eq(*username))
            .one(&db)
            .await
            .map_err(AppError::Database)?;

        if existing_user.is_some() {
            info!("User '{}' already exists, skipping", username);
            skipped += 1;
            continue;
        }

        let password_hash = PasswordService::hash_password(HAPPY_DAYS_PASSWORD).await?;

        let user = UserService::create_user(
            &db,
            username.to_string(),
            email.to_string(),
            password_hash,
            false,
        )
        .await?;

        // Mark seed user as email verified
        let mut user_model: rustwarden_core::models::user::ActiveModel = user.clone().into();
        user_model.email_verified = Set(true);
        user_model.email_verified_at = Set(Some(Utc::now()));
        user_model.update(&db).await?;

        info!(
            "Created user: {} ({}) [email pre-verified]",
            username, email
        );

        let role = RoleService::get_role_by_name(&db, role_name).await?;
        if let Some(role) = role {
            UserService::assign_role_to_user(&db, user.id, role.id).await?;
            info!("   Assigned role: {}", role_name);
        } else {
            warn!("Role '{}' not found, user created without role", role_name);
        }

        created += 1;
    }

    println!();
    println!("Seed data loading complete!");
    println!();
    println!("Summary:");
    println!("   Created: {} users", created);
    println!("   Skipped: {} users", skipped);
    println!();

    if created > 0 {
        println!("Happy Days users created (email pre-verified):");
        for (username, _, _) in SEED_USERS {
            println!("   - {} | Password: {}", username, HAPPY_DAYS_PASSWORD);
        }
        println!();
    }

    println!("Login with any Happy Days user:");
    println!("   Username: richie, fonzie, potsie, ralph, joanie");
    println!("   Password: {}", HAPPY_DAYS_PASSWORD);
    println!();

    Ok(())
}

#[cfg(test)]
mod tests {
    use super::validate_email_format;

    #[test]
    fn test_validate_email_format_valid() {
        assert!(validate_email_format("user@example.com").is_ok());
        assert!(validate_email_format("richie@rustwarden.local").is_ok());
        assert!(validate_email_format("test.user@domain.co.uk").is_ok());
        assert!(validate_email_format("a@b.c").is_ok());
    }

    #[test]
    fn test_validate_email_format_invalid() {
        assert!(validate_email_format("").is_err());
        assert!(validate_email_format("no-at-symbol").is_err());
        assert!(validate_email_format("@domain.com").is_err());
        assert!(validate_email_format("user@").is_err());
        assert!(validate_email_format("user@domain").is_err());
        assert!(validate_email_format("user@@domain.com").is_err());
    }

    #[test]
    fn test_seed_users_email_domain() {
        use super::SEED_USERS;
        for (_, email, _) in SEED_USERS {
            assert!(
                email.ends_with("@rustwarden.local"),
                "Email {} does not use @rustwarden.local domain",
                email
            );
            assert!(
                validate_email_format(email).is_ok(),
                "Email {} has invalid format",
                email
            );
        }
    }
}