use std::path::Path; use std::sync::Arc; use serde::Deserialize; use tracing::{info, warn}; use crate::ports::{PasswordHasher, UserRepository}; use crate::sqlite::SqliteDatabase; #[derive(Debug, Deserialize)] pub struct SeedConfig { #[serde(default)] pub user: Option, } #[derive(Debug, Deserialize)] pub struct SeedUser { pub email: String, pub display_name: String, pub password: String, } /// Reads the seed config file and creates the configured default user if the /// database has no users yet. The file is optional; if it does not exist (or /// cannot be parsed) seeding is skipped. pub async fn seed_if_needed( db: &SqliteDatabase, users: &Arc, hasher: &Arc, path: &Path, ) { let Ok(contents) = std::fs::read_to_string(path) else { return; }; let config: SeedConfig = match serde_json::from_str(&contents) { Ok(config) => config, Err(error) => { warn!(path = %path.display(), %error, "could not parse seed config; skipping"); return; } }; let Some(user) = config.user else { return; }; if user.email.trim().is_empty() { warn!("seed user requires a non-empty email; skipping"); return; } let users_for_check = users.clone(); let has_users = match db .run(move |txn| Box::pin(async move { users_for_check.has_users(txn).await })) .await { Ok(has_users) => has_users, Err(error) => { warn!(%error, "could not check for existing users; skipping seed"); return; } }; if has_users { info!("database already has users; skipping seed"); return; } let password_hash = match hasher.hash(&user.password) { Ok(hash) => hash, Err(error) => { warn!(%error, "could not hash seed password; skipping"); return; } }; let users_for_create = users.clone(); match db .run(move |txn| { Box::pin(async move { users_for_create .create_user( txn, user.email.trim().to_lowercase(), user.display_name.trim().to_owned(), password_hash, ) .await }) }) .await { Ok(user) => info!(id = user.id, email = %user.email, "seeded default user"), Err(error) => warn!(%error, "could not seed default user"), } }