feat: add admin initialization and database migration tasks
This commit is contained in:
@@ -1 +1,2 @@
|
||||
pub mod constants;
|
||||
pub mod database;
|
||||
|
||||
1
apps/api/src/helpers/constants.rs
Normal file
1
apps/api/src/helpers/constants.rs
Normal file
@@ -0,0 +1 @@
|
||||
pub const ADMIN_INIT_SECRET_KEY: &str = "admin_init_secret";
|
||||
@@ -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<dyn std::error::Error>> {
|
||||
// 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<dyn std::error::Error>> {
|
||||
// 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(())
|
||||
}
|
||||
|
||||
11
apps/api/src/tasks/startup/db_migrate.rs
Normal file
11
apps/api/src/tasks/startup/db_migrate.rs
Normal file
@@ -0,0 +1,11 @@
|
||||
use migration::migrate_database;
|
||||
use tracing::{debug, info};
|
||||
|
||||
pub async fn run_database_migrations(db_url: &str) -> Result<(), Box<dyn std::error::Error>> {
|
||||
// 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(())
|
||||
}
|
||||
116
apps/api/src/tasks/startup/init_admin.rs
Normal file
116
apps/api/src/tasks/startup/init_admin.rs
Normal file
@@ -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<DatabaseConnection>,
|
||||
) -> Result<(), Box<dyn std::error::Error>> {
|
||||
// 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<DatabaseConnection>,
|
||||
) -> Result<String, Box<dyn std::error::Error>> {
|
||||
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<DatabaseConnection>,
|
||||
) -> Result<(), Box<dyn std::error::Error>> {
|
||||
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(())
|
||||
}
|
||||
Reference in New Issue
Block a user