diff --git a/apps/api/src/helpers.rs b/apps/api/src/helpers.rs index 8fd0a6b..6fbb533 100644 --- a/apps/api/src/helpers.rs +++ b/apps/api/src/helpers.rs @@ -1 +1,2 @@ +pub mod constants; pub mod database; diff --git a/apps/api/src/helpers/constants.rs b/apps/api/src/helpers/constants.rs new file mode 100644 index 0000000..a36bf1c --- /dev/null +++ b/apps/api/src/helpers/constants.rs @@ -0,0 +1 @@ +pub const ADMIN_INIT_SECRET_KEY: &str = "admin_init_secret"; diff --git a/apps/api/src/tasks/startup.rs b/apps/api/src/tasks/startup.rs index 17d150b..ccf7b75 100644 --- a/apps/api/src/tasks/startup.rs +++ b/apps/api/src/tasks/startup.rs @@ -1,25 +1,34 @@ -use migration::migrate_database; -use tracing::{debug, info}; +mod db_migrate; +mod init_admin; + +use std::sync::Arc; + +use sea_orm::ConnectOptions; +use tracing::info; use crate::configs::ProgramSettings; +use database::get_connection; pub async fn run_startup_tasks(config: &ProgramSettings) -> Result<(), Box> { // Here you can add any startup tasks you want to run when the application starts. info!("Running startup tasks..."); + + let db_options = |options: &mut ConnectOptions| { + options.max_connections(config.database.max_connections); + }; + + let db_connection = Arc::new( + get_connection(&config.database.url, Some(db_options)) + .await + .map_err(|err| format!("Failed to establish database connection: {}", err))?, + ); + if config.database.migrate_on_startup { - run_database_migrations(&config.database.url).await?; + db_migrate::run_database_migrations(&config.database.url).await?; } else { info!("Database migration on startup is disabled. Skipping migration."); } + init_admin::init_admin(config, db_connection.clone()).await?; Ok(()) } - -async fn run_database_migrations(db_url: &str) -> Result<(), Box> { - // Logic to run database migrations - info!("Running database migrations..."); - debug!("Database URL: {}", db_url); - migrate_database(db_url).await.map_err(Box::new)?; - info!("Database migrations completed."); - Ok(()) -} diff --git a/apps/api/src/tasks/startup/db_migrate.rs b/apps/api/src/tasks/startup/db_migrate.rs new file mode 100644 index 0000000..41ee5aa --- /dev/null +++ b/apps/api/src/tasks/startup/db_migrate.rs @@ -0,0 +1,11 @@ +use migration::migrate_database; +use tracing::{debug, info}; + +pub async fn run_database_migrations(db_url: &str) -> Result<(), Box> { + // Logic to run database migrations + info!("Running database migrations..."); + debug!("Database URL: {}", db_url); + migrate_database(db_url).await.map_err(Box::new)?; + info!("Database migrations completed."); + Ok(()) +} diff --git a/apps/api/src/tasks/startup/init_admin.rs b/apps/api/src/tasks/startup/init_admin.rs new file mode 100644 index 0000000..e204da6 --- /dev/null +++ b/apps/api/src/tasks/startup/init_admin.rs @@ -0,0 +1,116 @@ +use std::sync::Arc; + +use argon2::password_hash::{SaltString, rand_core::OsRng}; +use database::generated::entities::user; +use sea_orm::{ColumnTrait, DatabaseConnection, EntityTrait, QueryFilter, TransactionTrait}; +use tracing::{debug, info, warn}; + +use crate::configs::ProgramSettings; +use crate::helpers::constants::ADMIN_INIT_SECRET_KEY; +use crate::services::{ + auth::{ + authentication::strategies::password::PasswordStrategy, + user::{NewUser, UserService, UserServiceImpl}, + }, + settings::{SettingsService, SettingsStore}, +}; + +pub async fn init_admin( + config: &ProgramSettings, + db: Arc, +) -> Result<(), Box> { + // if admin user already exists, skip + let admin_exists = user::Entity::find() + .filter(user::Column::IsAdmin.eq(true)) + .filter(user::Column::IsActive.eq(true)) + .one(db.as_ref()) + .await + .map_err(|err| format!("Failed to query for existing admin user: {}", err))? + .is_some(); + + if admin_exists { + debug!("Admin user already exists. Skipping admin initialization."); + return Ok(()); + } + + // if config contains admin init settings, run admin init + if let (Some(username), Some(password)) = ( + &config.auth.default_admin_username, + &config.auth.default_admin_password, + ) { + let r = _init_admin(username, password, db.clone()).await; + if let Err(e) = r { + warn!("Failed to initialize admin user: {}", e); + info!("Defaulting to manual creation from dashboard."); + } else { + return Ok(()); + } + } + // else generate a random secret to be used when initializing admin from dashboard + let secret = generate_admin_init_secret(db.clone()).await?; + info!( + "Admin initialization secret generated. Use this secret to initialize the admin user from the dashboard: {}. This secret will only be shown once and is only valid until the admin user is created or the application is restarted.", + secret + ); + Ok(()) +} + +async fn generate_admin_init_secret( + db: Arc, +) -> Result> { + let secret = SaltString::generate(&mut OsRng).as_str().to_owned(); + + // Store the secret in a settings table + let setting = SettingsService::new(db.clone()); + setting + .set_setting(ADMIN_INIT_SECRET_KEY, secret.clone()) + .await + .map_err(|err| format!("Failed to store admin init secret: {}", err))?; + + Ok(secret) +} + +async fn _init_admin( + username: &str, + password: &str, + db: Arc, +) -> Result<(), Box> { + info!("Initializing admin user..."); + // Check if an admin user already exists + let admin_exists = user::Entity::find() + .filter(user::Column::IsAdmin.eq(true)) + .one(db.as_ref()) + .await? + .is_some(); + + if admin_exists { + debug!("Admin user already exists. Skipping initialization."); + return Ok(()); + } + info!("No admin user found. Creating default admin user..."); + + let user_service = UserServiceImpl::new(db.clone()); + let password_strategy = PasswordStrategy::new(db.clone()); + + let user = NewUser { + username: username.to_string(), + is_admin: true, + }; + + let mut tx = db.begin().await?; + // create user + let user = user_service.create_user(user, Some(&mut tx)).await?; + // create temporary password + password_strategy + .create_identity(user.id, password, Some(&mut tx)) + .await?; + // + tx.commit().await?; + + info!( + "Default admin user created successfully, username: {}", + username + ); + + Ok(()) +}