mirror of
https://github.com/rustmailer/bichon.git
synced 2026-08-03 07:48:34 +02:00
feat(admin): add interactive data migration tool
This commit is contained in:
+17
-279
@@ -16,19 +16,13 @@
|
||||
// You should have received a copy of the GNU Affero General Public License
|
||||
// along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
use std::{
|
||||
fs,
|
||||
path::{Path, PathBuf},
|
||||
};
|
||||
use console::style;
|
||||
use dialoguer::{theme::ColorfulTheme, Select};
|
||||
|
||||
use bichon_core::{
|
||||
admin::meta::{find_admin, init_meta_database, update_admin_password},
|
||||
error::BichonError,
|
||||
utils::encrypt::internal_decrypt_string,
|
||||
};
|
||||
use console::{style, Emoji};
|
||||
use dialoguer::Confirm;
|
||||
use dialoguer::{theme::ColorfulTheme, Input, Password, Select};
|
||||
use crate::{migrate::handle_migration, reset::handle_reset_password};
|
||||
|
||||
pub mod migrate;
|
||||
pub mod reset;
|
||||
|
||||
#[tokio::main]
|
||||
async fn main() {
|
||||
@@ -38,7 +32,12 @@ async fn main() {
|
||||
style("BICHON ADMINISTRATIVE TOOL").bold().bright().cyan()
|
||||
);
|
||||
|
||||
let main_options = vec!["Reset Admin Password", "Exit"];
|
||||
let main_options = vec![
|
||||
"Reset Admin Password",
|
||||
"Migrate Legacy v0.x Storage to v1.0",
|
||||
"Exit",
|
||||
];
|
||||
|
||||
let selection = Select::with_theme(&theme)
|
||||
.with_prompt("Select an operation")
|
||||
.default(0)
|
||||
@@ -46,272 +45,11 @@ async fn main() {
|
||||
.interact()
|
||||
.unwrap();
|
||||
|
||||
if selection == 1 {
|
||||
println!("{}", style("Exiting...").dim());
|
||||
return;
|
||||
}
|
||||
|
||||
let root_dir_str: String = Input::with_theme(&theme)
|
||||
.with_prompt("Enter the absolute path for 'bichon_root_dir'")
|
||||
.validate_with(|input: &String| -> Result<(), &str> {
|
||||
let path = Path::new(input);
|
||||
if !path.is_absolute() {
|
||||
return Err("Path must be absolute.");
|
||||
}
|
||||
if !path.exists() {
|
||||
return Err("Directory does not exist.");
|
||||
}
|
||||
let has_metadata = path.join("meta.db").exists();
|
||||
if !has_metadata {
|
||||
return Err("Invalid directory: 'meta.db' not found.");
|
||||
}
|
||||
Ok(())
|
||||
})
|
||||
.interact_text()
|
||||
.unwrap();
|
||||
|
||||
let root_path = PathBuf::from(&root_dir_str);
|
||||
|
||||
let database = match init_meta_database(&root_path.join("meta.db")) {
|
||||
Ok(database) => database,
|
||||
Err(e) => match e {
|
||||
BichonError::Generic {
|
||||
message,
|
||||
location,
|
||||
code,
|
||||
} => {
|
||||
if message.contains("RedbDatabaseError(DatabaseAlreadyOpen") {
|
||||
println!("\n{}", style("ERROR: Database is locked.").red().bold());
|
||||
println!(
|
||||
"{}",
|
||||
style("The Bichon service is likely still running.").yellow()
|
||||
);
|
||||
println!(
|
||||
"Since the database cannot be shared between multiple instances, \n\
|
||||
you must {} the Bichon service before proceeding.",
|
||||
style("STOP").underlined().bold()
|
||||
);
|
||||
std::process::exit(1);
|
||||
} else {
|
||||
eprintln!(
|
||||
"\n{} (Code: {:#?})\nLocation: {}\nMessage: {}",
|
||||
style("A database error occurred:").red().bold(),
|
||||
code,
|
||||
location,
|
||||
message
|
||||
);
|
||||
std::process::exit(1);
|
||||
}
|
||||
}
|
||||
},
|
||||
};
|
||||
|
||||
let admin = find_admin(&database);
|
||||
|
||||
match admin {
|
||||
Ok(Some(user)) => {
|
||||
println!("\n{}", style("Admin user found:").green().bold());
|
||||
println!("----------------------------------------");
|
||||
println!("{:<12} : {}", "Username", style(&user.username).cyan());
|
||||
println!("{:<12} : {}", "Email", style(&user.email).cyan());
|
||||
}
|
||||
Ok(None) => {
|
||||
println!(
|
||||
"\n{}",
|
||||
style("ERROR: No admin user found in the database.")
|
||||
.red()
|
||||
.bold()
|
||||
);
|
||||
println!("Please ensure the system has been initialized correctly.");
|
||||
std::process::exit(1);
|
||||
}
|
||||
Err(e) => {
|
||||
eprintln!(
|
||||
"\n{} Failed to query admin user.",
|
||||
style("ERROR:").red().bold()
|
||||
);
|
||||
eprintln!("Details: {:?}", e);
|
||||
std::process::exit(1);
|
||||
}
|
||||
}
|
||||
|
||||
let encryption_key = loop {
|
||||
let auth_methods = vec![
|
||||
"Enter encryption password manually",
|
||||
"Read from password file",
|
||||
];
|
||||
let method = Select::with_theme(&theme)
|
||||
.with_prompt("How would you like to provide the database encryption key?")
|
||||
.items(&auth_methods)
|
||||
.interact()
|
||||
.unwrap();
|
||||
|
||||
let raw_key = if method == 0 {
|
||||
Password::with_theme(&theme)
|
||||
.with_prompt("Enter Encryption Password")
|
||||
.interact()
|
||||
.unwrap()
|
||||
} else {
|
||||
let file_path: String = Input::with_theme(&theme)
|
||||
.with_prompt("Enter path to encryption password file")
|
||||
.interact_text()
|
||||
.unwrap();
|
||||
|
||||
match fs::read_to_string(&file_path) {
|
||||
Ok(content) => content.trim().to_string(),
|
||||
Err(e) => {
|
||||
println!("{}: {}", style("Failed to read file").red(), e);
|
||||
continue;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
if raw_key.is_empty() {
|
||||
println!("{}", style("Key cannot be empty.").red());
|
||||
continue;
|
||||
}
|
||||
|
||||
let prompt_message = format!(
|
||||
"Encryption key loaded: [ {} ]\n\n \
|
||||
{}: This key must match the database encryption key used by the server.\n \
|
||||
It corresponds to these settings in your service:\n \
|
||||
- Arguments: {} or {}\n \
|
||||
- Envs: {} or {}\n\n \
|
||||
Do you want to continue?",
|
||||
style(&raw_key).cyan().bold(),
|
||||
style("IMPORTANT").yellow().bold(),
|
||||
style("--bichon_encrypt_password").italic(),
|
||||
style("--bichon_encrypt_password_file").italic(),
|
||||
style("BICHON_ENCRYPT_PASSWORD").green(),
|
||||
style("BICHON_ENCRYPT_PASSWORD_FILE").green()
|
||||
);
|
||||
|
||||
if Confirm::with_theme(&theme)
|
||||
.with_prompt(prompt_message)
|
||||
.default(true)
|
||||
.interact()
|
||||
.unwrap()
|
||||
{
|
||||
break raw_key;
|
||||
}
|
||||
};
|
||||
|
||||
let admin = find_admin(&database);
|
||||
|
||||
match admin {
|
||||
Ok(Some(user)) => {
|
||||
println!("----------------------------------------");
|
||||
println!("{:<12} : {}", "Username", style(&user.username).cyan());
|
||||
println!("{:<12} : {}", "Email", style(&user.email).cyan());
|
||||
|
||||
let pwd_display = match &user.password {
|
||||
Some(p) => {
|
||||
let password = match internal_decrypt_string(&encryption_key, p) {
|
||||
Ok(p) => p,
|
||||
Err(e) => {
|
||||
println!("\n{}", style("ERROR: Decryption Failed").red().bold());
|
||||
println!(
|
||||
"{}",
|
||||
style("The provided encryption key is incorrect or invalid for this database.").yellow()
|
||||
);
|
||||
println!(
|
||||
"{} Please verify your {} or the {} you provided.",
|
||||
style("➔").cyan(),
|
||||
style("encryption password").bold(),
|
||||
style("key file").bold()
|
||||
);
|
||||
eprintln!("\nTechnical details: {:?}", e);
|
||||
std::process::exit(1);
|
||||
}
|
||||
};
|
||||
style(password).yellow().to_string()
|
||||
}
|
||||
None => style("None (No password set)").dim().italic().to_string(),
|
||||
};
|
||||
|
||||
println!("{:<12} : {}", "Password", pwd_display);
|
||||
println!("----------------------------------------");
|
||||
|
||||
if !dialoguer::Confirm::with_theme(&theme)
|
||||
.with_prompt(format!(
|
||||
"Do you want to reset the password for '{}'?",
|
||||
user.username
|
||||
))
|
||||
.interact()
|
||||
.unwrap()
|
||||
{
|
||||
println!("Operation cancelled.");
|
||||
return;
|
||||
}
|
||||
}
|
||||
Ok(None) => {
|
||||
println!(
|
||||
"\n{}",
|
||||
style("ERROR: No admin user found in the database.")
|
||||
.red()
|
||||
.bold()
|
||||
);
|
||||
println!("Please ensure the system has been initialized correctly.");
|
||||
std::process::exit(1);
|
||||
}
|
||||
Err(e) => {
|
||||
eprintln!(
|
||||
"\n{} Failed to query admin user.",
|
||||
style("ERROR:").red().bold()
|
||||
);
|
||||
eprintln!("Details: {:?}", e);
|
||||
std::process::exit(1);
|
||||
}
|
||||
}
|
||||
|
||||
println!(
|
||||
"\n{}",
|
||||
style("TARGET: Reset password for user 'admin'")
|
||||
.yellow()
|
||||
.bold()
|
||||
);
|
||||
|
||||
let new_login_password = Password::with_theme(&theme)
|
||||
.with_prompt("Enter new Admin Login Password")
|
||||
.with_confirmation("Repeat password to confirm", "Passwords do not match!")
|
||||
.interact()
|
||||
.unwrap();
|
||||
|
||||
if !Confirm::with_theme(&theme)
|
||||
.with_prompt("Proceed with database update?")
|
||||
.interact()
|
||||
.unwrap()
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
println!("\n{} {}", style("⌛").yellow(), "Updating database...");
|
||||
|
||||
match update_admin_password(&database, new_login_password, &encryption_key) {
|
||||
Ok(_) => {
|
||||
println!(
|
||||
"\n{} {}",
|
||||
Emoji("✨", "*"),
|
||||
style("Success! Admin password has been updated.")
|
||||
.green()
|
||||
.bold()
|
||||
);
|
||||
println!(
|
||||
"{}",
|
||||
style("You can now log in with the new password.").dim()
|
||||
);
|
||||
}
|
||||
Err(e) => {
|
||||
println!(
|
||||
"\n{}",
|
||||
style("ERROR: Failed to update database").red().bold()
|
||||
);
|
||||
eprintln!(
|
||||
"{} Could not save the new password to the database.",
|
||||
style("➔").cyan()
|
||||
);
|
||||
eprintln!("\nDetails: {:?}", e);
|
||||
std::process::exit(1);
|
||||
match selection {
|
||||
0 => handle_reset_password(&theme),
|
||||
1 => handle_migration(&theme),
|
||||
_ => {
|
||||
println!("{}", style("Exiting...").dim());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,284 @@
|
||||
use std::path::{Path, PathBuf};
|
||||
|
||||
use bichon_core::migrate::{
|
||||
do_migrate, is_tantivy_index_dir,
|
||||
store::{LegacyDirs, NewDirs},
|
||||
};
|
||||
use console::style;
|
||||
use dialoguer::{theme::ColorfulTheme, Confirm, Input};
|
||||
use indicatif::{ProgressBar, ProgressStyle};
|
||||
|
||||
pub fn handle_migration(theme: &ColorfulTheme) {
|
||||
println!(
|
||||
"\n{}",
|
||||
style("MIGRATION: Bichon v0.x Storage Architecture → v1.0")
|
||||
.bold()
|
||||
.yellow()
|
||||
);
|
||||
|
||||
println!(
|
||||
"{}",
|
||||
style(
|
||||
"This tool migrates data from the legacy v0.x Tantivy-based storage \
|
||||
architecture (used in versions 0.0.1 through 0.3.7) to the new v1.0 \
|
||||
separated index and Fjall-backed storage format."
|
||||
)
|
||||
.dim()
|
||||
);
|
||||
|
||||
println!(
|
||||
"{}",
|
||||
style(
|
||||
"Legacy v0.x architecture:\n\
|
||||
• envelope metadata stored in Tantivy\n\
|
||||
• message data stored in Tantivy\n\n\
|
||||
New v1.0 architecture:\n\
|
||||
• mail indexes stored in Tantivy\n\
|
||||
• attachment indexes stored in Tantivy\n\
|
||||
• raw message data stored in Fjall\n\
|
||||
• attachment blobs stored in Fjall"
|
||||
)
|
||||
.dim()
|
||||
);
|
||||
|
||||
println!(
|
||||
"\n{} {}",
|
||||
style("IMPORTANT:").yellow().bold(),
|
||||
style(
|
||||
"The paths below must exactly match what your old bichon server was configured with."
|
||||
)
|
||||
.yellow()
|
||||
);
|
||||
|
||||
// --- bichon-root-dir ---
|
||||
let root_dir_str: String = Input::with_theme(theme)
|
||||
.with_prompt("Enter --bichon-root-dir (same value used by the old server)")
|
||||
.validate_with(|input: &String| -> Result<(), &str> {
|
||||
let path = Path::new(input);
|
||||
if !path.is_absolute() {
|
||||
return Err("Path must be absolute.");
|
||||
}
|
||||
if !path.exists() {
|
||||
return Err("Directory does not exist.");
|
||||
}
|
||||
Ok(())
|
||||
})
|
||||
.interact_text()
|
||||
.unwrap();
|
||||
|
||||
let root_path = PathBuf::from(&root_dir_str);
|
||||
|
||||
// --- bichon-index-dir ---
|
||||
let default_index = root_path.join("envelope");
|
||||
let default_new_index = root_path.join("bichon-indices");
|
||||
let index_dir_str: String = Input::with_theme(theme)
|
||||
.with_prompt(format!(
|
||||
"Enter --bichon-index-dir (leave blank to use default: {})",
|
||||
style(default_index.display()).cyan()
|
||||
))
|
||||
.allow_empty(true)
|
||||
.validate_with(|input: &String| -> Result<(), &str> {
|
||||
if input.is_empty() {
|
||||
return Ok(());
|
||||
}
|
||||
let path = Path::new(input);
|
||||
if !path.is_absolute() {
|
||||
return Err("Path must be absolute.");
|
||||
}
|
||||
|
||||
if !path.exists() {
|
||||
return Err("Directory does not exist.");
|
||||
}
|
||||
Ok(())
|
||||
})
|
||||
.interact_text()
|
||||
.unwrap();
|
||||
|
||||
let index_path = if index_dir_str.is_empty() {
|
||||
default_index
|
||||
} else {
|
||||
PathBuf::from(&index_dir_str)
|
||||
};
|
||||
|
||||
let new_index_path = if index_dir_str.is_empty() {
|
||||
default_new_index
|
||||
} else {
|
||||
PathBuf::from(&index_dir_str).join("bichon-indices")
|
||||
};
|
||||
|
||||
// --- bichon-data-dir ---
|
||||
let default_data = root_path.join("eml");
|
||||
let default_new_data = root_path.join("bichon-storage");
|
||||
let data_dir_str: String = Input::with_theme(theme)
|
||||
.with_prompt(format!(
|
||||
"Enter --bichon-data-dir (leave blank to use default: {})",
|
||||
style(default_data.display()).cyan()
|
||||
))
|
||||
.allow_empty(true)
|
||||
.validate_with(|input: &String| -> Result<(), &str> {
|
||||
if input.is_empty() {
|
||||
return Ok(());
|
||||
}
|
||||
let path = Path::new(input);
|
||||
if !path.is_absolute() {
|
||||
return Err("Path must be absolute.");
|
||||
}
|
||||
if !path.exists() {
|
||||
return Err("Directory does not exist.");
|
||||
}
|
||||
Ok(())
|
||||
})
|
||||
.interact_text()
|
||||
.unwrap();
|
||||
|
||||
let data_path = if data_dir_str.is_empty() {
|
||||
default_data
|
||||
} else {
|
||||
PathBuf::from(&data_dir_str)
|
||||
};
|
||||
|
||||
let new_data_path = if data_dir_str.is_empty() {
|
||||
default_new_data
|
||||
} else {
|
||||
PathBuf::from(&data_dir_str).join("bichon-storage")
|
||||
};
|
||||
|
||||
println!("\n{}", style("Paths to be migrated:").bold());
|
||||
println!("----------------------------------------");
|
||||
println!(
|
||||
"{:<20} : {}",
|
||||
"bichon-root-dir",
|
||||
style(root_path.display()).cyan()
|
||||
);
|
||||
println!(
|
||||
"{:<20} : {}",
|
||||
"bichon-index-dir",
|
||||
style(index_path.display()).cyan()
|
||||
);
|
||||
println!(
|
||||
"{:<20} : {}",
|
||||
"bichon-data-dir",
|
||||
style(data_path.display()).cyan()
|
||||
);
|
||||
println!("----------------------------------------");
|
||||
|
||||
println!(
|
||||
"\n{} Checking legacy v0.x storage layout...",
|
||||
style("⌛").yellow()
|
||||
);
|
||||
|
||||
match is_legacy_data_layout_with_paths(&index_path, &data_path) {
|
||||
Ok(true) => {
|
||||
println!(
|
||||
"{} {}",
|
||||
style("✔").green(),
|
||||
style("Legacy v0.x Tantivy-based storage detected. Migration to v1.0 is required.")
|
||||
.yellow()
|
||||
);
|
||||
}
|
||||
Ok(false) => {
|
||||
println!(
|
||||
"{} {}",
|
||||
style("✔").green(),
|
||||
style("No legacy v0.x storage layout was detected at the specified paths.").green()
|
||||
);
|
||||
|
||||
println!(
|
||||
"{}",
|
||||
style(
|
||||
"The selected directories may already be using the v1.0 storage architecture."
|
||||
)
|
||||
.dim()
|
||||
);
|
||||
|
||||
return;
|
||||
}
|
||||
Err(e) => {
|
||||
eprintln!(
|
||||
"{} Failed to verify legacy storage layout: {:?}",
|
||||
style("ERROR:").red().bold(),
|
||||
e
|
||||
);
|
||||
|
||||
std::process::exit(1);
|
||||
}
|
||||
}
|
||||
|
||||
println!(
|
||||
"\n{} {}",
|
||||
style("⚠").yellow(),
|
||||
style(
|
||||
"This migration is non-destructive. Existing v0.x storage files will remain unchanged."
|
||||
)
|
||||
.yellow()
|
||||
);
|
||||
|
||||
if !Confirm::with_theme(theme)
|
||||
.with_prompt("Ready to migrate?")
|
||||
.default(true)
|
||||
.interact()
|
||||
.unwrap()
|
||||
{
|
||||
println!("{}", style("Migration cancelled.").dim());
|
||||
return;
|
||||
}
|
||||
|
||||
println!("\n{} Migrating...", style("⌛").yellow());
|
||||
let pb = ProgressBar::new(0);
|
||||
pb.set_style(ProgressStyle::default_bar()
|
||||
.template("{spinner:.green} [{elapsed_precise}] [{bar:40.cyan/blue}] {pos}/{len} ({eta}) {msg}")
|
||||
.unwrap()
|
||||
.progress_chars("#>-"));
|
||||
let legacy = LegacyDirs::new(index_path, data_path);
|
||||
let new_dirs = NewDirs::new(new_index_path, new_data_path);
|
||||
if let Err(e) = do_migrate(legacy, new_dirs, |msg| {
|
||||
if let Some(data) = msg.strip_prefix("PROGRESS:") {
|
||||
let parts: Vec<&str> = data.split(':').collect();
|
||||
if parts.len() == 2 {
|
||||
let migrated = parts[0].parse::<u64>().unwrap_or(0);
|
||||
let skipped = parts[1].parse::<u64>().unwrap_or(0);
|
||||
|
||||
pb.set_position(migrated + skipped);
|
||||
pb.set_message(format!(
|
||||
"Migrated: {}, {} {}",
|
||||
style(migrated).green(),
|
||||
style(skipped).red(),
|
||||
style("skipped").dim()
|
||||
));
|
||||
}
|
||||
} else if let Some(total) = msg.strip_prefix("TOTAL:") {
|
||||
pb.set_length(total.parse().unwrap_or(0));
|
||||
} else if msg.starts_with("WARN:") {
|
||||
pb.println(format!("{} {}", style("⚠").yellow(), &msg[5..]));
|
||||
} else if let Some(done_data) = msg.strip_prefix("DONE:") {
|
||||
let parts: Vec<&str> = done_data.split(':').collect();
|
||||
pb.finish_with_message(format!(
|
||||
"Migration finished. Total: {}, Skipped: {}",
|
||||
parts.get(0).unwrap_or(&"0"),
|
||||
parts.get(1).unwrap_or(&"0")
|
||||
));
|
||||
}
|
||||
}) {
|
||||
eprintln!(
|
||||
"\n{} Migration failed:\n{:?}",
|
||||
style("✘").red().bold(),
|
||||
style(e).red()
|
||||
);
|
||||
return;
|
||||
}
|
||||
println!(
|
||||
"{} {}",
|
||||
style("✔").green(),
|
||||
style("Migration completed successfully!").bold()
|
||||
);
|
||||
}
|
||||
|
||||
pub fn is_legacy_data_layout_with_paths(
|
||||
envelope_dir: &PathBuf,
|
||||
eml_dir: &PathBuf,
|
||||
) -> std::io::Result<bool> {
|
||||
let envelope_result = is_tantivy_index_dir(envelope_dir)?;
|
||||
let eml_result = is_tantivy_index_dir(eml_dir)?;
|
||||
|
||||
Ok(envelope_result || eml_result)
|
||||
}
|
||||
@@ -0,0 +1,274 @@
|
||||
use std::path::{Path, PathBuf};
|
||||
|
||||
use bichon_core::{
|
||||
admin::meta::{find_admin, init_meta_database, update_admin_password},
|
||||
error::BichonError,
|
||||
utils::encrypt::internal_decrypt_string,
|
||||
};
|
||||
use console::{style, Emoji};
|
||||
use dialoguer::{theme::ColorfulTheme, Confirm, Input, Password, Select};
|
||||
|
||||
pub fn handle_reset_password(theme: &ColorfulTheme) {
|
||||
let root_dir_str: String = Input::with_theme(theme)
|
||||
.with_prompt("Enter the absolute path for 'bichon_root_dir'")
|
||||
.validate_with(|input: &String| -> Result<(), &str> {
|
||||
let path = Path::new(input);
|
||||
if !path.is_absolute() {
|
||||
return Err("Path must be absolute.");
|
||||
}
|
||||
if !path.exists() {
|
||||
return Err("Directory does not exist.");
|
||||
}
|
||||
let has_metadata = path.join("meta.db").exists();
|
||||
if !has_metadata {
|
||||
return Err("Invalid directory: 'meta.db' not found.");
|
||||
}
|
||||
Ok(())
|
||||
})
|
||||
.interact_text()
|
||||
.unwrap();
|
||||
|
||||
let root_path = PathBuf::from(&root_dir_str);
|
||||
let database = match init_meta_database(&root_path.join("meta.db")) {
|
||||
Ok(database) => database,
|
||||
Err(e) => match e {
|
||||
BichonError::Generic {
|
||||
message,
|
||||
location,
|
||||
code,
|
||||
} => {
|
||||
if message.contains("RedbDatabaseError(DatabaseAlreadyOpen") {
|
||||
println!("\n{}", style("ERROR: Database is locked.").red().bold());
|
||||
println!(
|
||||
"{}",
|
||||
style("The Bichon service is likely still running.").yellow()
|
||||
);
|
||||
println!(
|
||||
"Since the database cannot be shared between multiple instances, \n\
|
||||
you must {} the Bichon service before proceeding.",
|
||||
style("STOP").underlined().bold()
|
||||
);
|
||||
std::process::exit(1);
|
||||
} else {
|
||||
eprintln!(
|
||||
"\n{} (Code: {:#?})\nLocation: {}\nMessage: {}",
|
||||
style("A database error occurred:").red().bold(),
|
||||
code,
|
||||
location,
|
||||
message
|
||||
);
|
||||
std::process::exit(1);
|
||||
}
|
||||
}
|
||||
},
|
||||
};
|
||||
|
||||
let admin = find_admin(&database);
|
||||
|
||||
match admin {
|
||||
Ok(Some(user)) => {
|
||||
println!("\n{}", style("Admin user found:").green().bold());
|
||||
println!("----------------------------------------");
|
||||
println!("{:<12} : {}", "Username", style(&user.username).cyan());
|
||||
println!("{:<12} : {}", "Email", style(&user.email).cyan());
|
||||
}
|
||||
Ok(None) => {
|
||||
println!(
|
||||
"\n{}",
|
||||
style("ERROR: No admin user found in the database.")
|
||||
.red()
|
||||
.bold()
|
||||
);
|
||||
println!("Please ensure the system has been initialized correctly.");
|
||||
std::process::exit(1);
|
||||
}
|
||||
Err(e) => {
|
||||
eprintln!(
|
||||
"\n{} Failed to query admin user.",
|
||||
style("ERROR:").red().bold()
|
||||
);
|
||||
eprintln!("Details: {:?}", e);
|
||||
std::process::exit(1);
|
||||
}
|
||||
}
|
||||
|
||||
let encryption_key = loop {
|
||||
let auth_methods = vec![
|
||||
"Enter encryption password manually",
|
||||
"Read from password file",
|
||||
];
|
||||
let method = Select::with_theme(theme)
|
||||
.with_prompt("How would you like to provide the database encryption key?")
|
||||
.items(&auth_methods)
|
||||
.interact()
|
||||
.unwrap();
|
||||
|
||||
let raw_key = if method == 0 {
|
||||
Password::with_theme(theme)
|
||||
.with_prompt("Enter Encryption Password")
|
||||
.interact()
|
||||
.unwrap()
|
||||
} else {
|
||||
let file_path: String = Input::with_theme(theme)
|
||||
.with_prompt("Enter path to encryption password file")
|
||||
.interact_text()
|
||||
.unwrap();
|
||||
|
||||
match std::fs::read_to_string(&file_path) {
|
||||
Ok(content) => content.trim().to_string(),
|
||||
Err(e) => {
|
||||
println!("{}: {}", style("Failed to read file").red(), e);
|
||||
continue;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
if raw_key.is_empty() {
|
||||
println!("{}", style("Key cannot be empty.").red());
|
||||
continue;
|
||||
}
|
||||
|
||||
let prompt_message = format!(
|
||||
"Encryption key loaded: [ {} ]\n\n \
|
||||
{}: This key must match the database encryption key used by the server.\n \
|
||||
It corresponds to these settings in your service:\n \
|
||||
- Arguments: {} or {}\n \
|
||||
- Envs: {} or {}\n\n \
|
||||
Do you want to continue?",
|
||||
style(&raw_key).cyan().bold(),
|
||||
style("IMPORTANT").yellow().bold(),
|
||||
style("--bichon_encrypt_password").italic(),
|
||||
style("--bichon_encrypt_password_file").italic(),
|
||||
style("BICHON_ENCRYPT_PASSWORD").green(),
|
||||
style("BICHON_ENCRYPT_PASSWORD_FILE").green()
|
||||
);
|
||||
|
||||
if Confirm::with_theme(theme)
|
||||
.with_prompt(prompt_message)
|
||||
.default(true)
|
||||
.interact()
|
||||
.unwrap()
|
||||
{
|
||||
break raw_key;
|
||||
}
|
||||
};
|
||||
|
||||
let admin = find_admin(&database);
|
||||
|
||||
match admin {
|
||||
Ok(Some(user)) => {
|
||||
println!("----------------------------------------");
|
||||
println!("{:<12} : {}", "Username", style(&user.username).cyan());
|
||||
println!("{:<12} : {}", "Email", style(&user.email).cyan());
|
||||
|
||||
let pwd_display = match &user.password {
|
||||
Some(p) => {
|
||||
let password = match internal_decrypt_string(&encryption_key, p) {
|
||||
Ok(p) => p,
|
||||
Err(e) => {
|
||||
println!("\n{}", style("ERROR: Decryption Failed").red().bold());
|
||||
println!(
|
||||
"{}",
|
||||
style("The provided encryption key is incorrect or invalid for this database.").yellow()
|
||||
);
|
||||
println!(
|
||||
"{} Please verify your {} or the {} you provided.",
|
||||
style("➔").cyan(),
|
||||
style("encryption password").bold(),
|
||||
style("key file").bold()
|
||||
);
|
||||
eprintln!("\nTechnical details: {:?}", e);
|
||||
std::process::exit(1);
|
||||
}
|
||||
};
|
||||
style(password).yellow().to_string()
|
||||
}
|
||||
None => style("None (No password set)").dim().italic().to_string(),
|
||||
};
|
||||
|
||||
println!("{:<12} : {}", "Password", pwd_display);
|
||||
println!("----------------------------------------");
|
||||
|
||||
if !dialoguer::Confirm::with_theme(theme)
|
||||
.with_prompt(format!(
|
||||
"Do you want to reset the password for '{}'?",
|
||||
user.username
|
||||
))
|
||||
.interact()
|
||||
.unwrap()
|
||||
{
|
||||
println!("Operation cancelled.");
|
||||
return;
|
||||
}
|
||||
}
|
||||
Ok(None) => {
|
||||
println!(
|
||||
"\n{}",
|
||||
style("ERROR: No admin user found in the database.")
|
||||
.red()
|
||||
.bold()
|
||||
);
|
||||
println!("Please ensure the system has been initialized correctly.");
|
||||
std::process::exit(1);
|
||||
}
|
||||
Err(e) => {
|
||||
eprintln!(
|
||||
"\n{} Failed to query admin user.",
|
||||
style("ERROR:").red().bold()
|
||||
);
|
||||
eprintln!("Details: {:?}", e);
|
||||
std::process::exit(1);
|
||||
}
|
||||
}
|
||||
|
||||
println!(
|
||||
"\n{}",
|
||||
style("TARGET: Reset password for user 'admin'")
|
||||
.yellow()
|
||||
.bold()
|
||||
);
|
||||
|
||||
let new_login_password = Password::with_theme(theme)
|
||||
.with_prompt("Enter new Admin Login Password")
|
||||
.with_confirmation("Repeat password to confirm", "Passwords do not match!")
|
||||
.interact()
|
||||
.unwrap();
|
||||
|
||||
if !Confirm::with_theme(theme)
|
||||
.with_prompt("Proceed with database update?")
|
||||
.interact()
|
||||
.unwrap()
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
println!("\n{} {}", style("⌛").yellow(), "Updating database...");
|
||||
|
||||
match update_admin_password(&database, new_login_password, &encryption_key) {
|
||||
Ok(_) => {
|
||||
println!(
|
||||
"\n{} {}",
|
||||
Emoji("✨", "*"),
|
||||
style("Success! Admin password has been updated.")
|
||||
.green()
|
||||
.bold()
|
||||
);
|
||||
println!(
|
||||
"{}",
|
||||
style("You can now log in with the new password.").dim()
|
||||
);
|
||||
}
|
||||
Err(e) => {
|
||||
println!(
|
||||
"\n{}",
|
||||
style("ERROR: Failed to update database").red().bold()
|
||||
);
|
||||
eprintln!(
|
||||
"{} Could not save the new password to the database.",
|
||||
style("➔").cyan()
|
||||
);
|
||||
eprintln!("\nDetails: {:?}", e);
|
||||
std::process::exit(1);
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user