use std::path::Path;
use crate::server::config::SecretsConfig;
use crate::server::error::AppError;
use crate::server::secret_store::{ServerSecrets, create_secret_store};
#[derive(Debug)]
pub struct ProvisionedScan {
pub has_secrets: bool,
pub has_bootstrap_seed: bool,
pub config_file_exists: bool,
}
impl ProvisionedScan {
pub fn is_provisioned(&self) -> bool {
self.has_secrets
}
}
pub async fn inspect_existing(
secrets_config: &SecretsConfig,
config_path: &Path,
) -> ProvisionedScan {
let config_file_exists = config_path.exists();
let store = match create_secret_store(secrets_config, config_path) {
Ok(s) => s,
Err(e) => {
eprintln!(" Warning: reprovision scan could not open secret store: {e}");
return ProvisionedScan {
has_secrets: false,
has_bootstrap_seed: false,
config_file_exists,
};
}
};
let has_secrets = match store.get().await {
Ok(Some(_)) => true,
Ok(None) => false,
Err(e) => {
eprintln!(" Warning: reprovision scan failed to read existing secrets: {e}");
false
}
};
let has_bootstrap_seed = matches!(store.get_bootstrap_seed().await, Ok(Some(_)));
ProvisionedScan {
has_secrets,
has_bootstrap_seed,
config_file_exists,
}
}
pub fn refuse_overwrite(config_path: &Path, scan: &ProvisionedScan) -> AppError {
eprintln!();
eprintln!(" ┌─ REFUSING TO OVERWRITE PROVISIONED INSTALL ────────────");
eprintln!(" │ Existing setup detected:");
if scan.config_file_exists {
eprintln!(" │ - config file: {}", config_path.display());
}
if scan.has_secrets {
eprintln!(" │ - secret store contains ServerSecrets");
}
if scan.has_bootstrap_seed {
eprintln!(" │ - secret store contains a pending bootstrap seed");
}
eprintln!(" │");
eprintln!(" │ Rotating credentials silently:");
eprintln!(" │ - invalidates any JWT this service has issued,");
eprintln!(" │ - breaks the active VTA session,");
eprintln!(" │ - requires every authenticated client to re-auth.");
eprintln!(" │");
eprintln!(" │ Options:");
eprintln!(" │ - re-run with --force-reprovision to rotate anyway");
eprintln!(" │ (this backs up config.toml to config.toml.bak first), or");
eprintln!(" │ - run `uninstall` first to teardown the existing deployment.");
eprintln!(" └────────────────────────────────────────────────────────");
eprintln!();
AppError::Config(format!(
"refusing to overwrite provisioned install at {}",
config_path.display()
))
}
pub fn back_up_config(config_path: &Path) -> std::io::Result<()> {
if !config_path.exists() {
return Ok(());
}
let bak = config_path.with_extension(
config_path
.extension()
.map(|e| format!("{}.bak", e.to_string_lossy()))
.unwrap_or_else(|| "bak".to_string()),
);
std::fs::copy(config_path, &bak)?;
eprintln!(
" Existing {} backed up to {} before re-provisioning.",
config_path.display(),
bak.display()
);
Ok(())
}
pub async fn run_uninstall_unchecked(
secrets_config: &SecretsConfig,
config_path: &Path,
companion_files: &[&Path],
) -> Result<UninstallReport, AppError> {
let mut report = UninstallReport::default();
let store = create_secret_store(secrets_config, config_path)?;
if let Ok(Some(_)) = store.get().await {
let blank = ServerSecrets {
signing_key: String::new(),
key_agreement_key: String::new(),
jwt_signing_key: String::new(),
vta_credential: None,
retired: Vec::new(),
};
if let Err(e) = store.set(&blank).await {
eprintln!(" Warning: could not clear ServerSecrets ({e})");
report.secrets_cleared = false;
} else {
report.secrets_cleared = true;
}
}
if let Err(e) = store.clear_bootstrap_seed().await {
eprintln!(" Warning: could not clear bootstrap seed ({e})");
} else {
report.seed_cleared = true;
}
if config_path.exists() {
match std::fs::remove_file(config_path) {
Ok(()) => report.files_removed.push(config_path.to_path_buf()),
Err(e) => eprintln!(" Warning: could not remove {}: {e}", config_path.display()),
}
}
let bak = config_path.with_extension(
config_path
.extension()
.map(|e| format!("{}.bak", e.to_string_lossy()))
.unwrap_or_else(|| "bak".to_string()),
);
if bak.exists()
&& let Ok(()) = std::fs::remove_file(&bak)
{
report.files_removed.push(bak);
}
for path in companion_files {
if path.exists()
&& let Ok(()) = std::fs::remove_file(path)
{
report.files_removed.push(path.to_path_buf());
}
}
Ok(report)
}
#[derive(Debug, Default)]
pub struct UninstallReport {
pub secrets_cleared: bool,
pub seed_cleared: bool,
pub files_removed: Vec<std::path::PathBuf>,
}
#[cfg(feature = "setup-wizard")]
pub fn prompt_uninstall_confirmation(config_path: &Path) -> Result<bool, AppError> {
use dialoguer::Input;
eprintln!();
eprintln!(" ┌─ UNINSTALL ───────────────────────────────────────────");
eprintln!(" │ This will:");
eprintln!(" │ - delete ServerSecrets from the configured backend,");
eprintln!(" │ - clear any pending bootstrap seed,");
eprintln!(
" │ - remove {} (and .bak if present).",
config_path.display()
);
eprintln!(" │");
eprintln!(" │ DIDs in the local store, ACL entries, and the data");
eprintln!(" │ directory are NOT removed — delete those manually if");
eprintln!(" │ you want a clean slate.");
eprintln!(" └────────────────────────────────────────────────────────");
eprintln!();
let typed: String = Input::new()
.with_prompt("Type DELETE to confirm")
.default(String::new())
.allow_empty(true)
.interact_text()
.map_err(|e| AppError::Config(format!("input error: {e}")))?;
Ok(typed.trim() == "DELETE")
}
#[cfg(test)]
mod tests {
use super::*;
use std::path::PathBuf;
#[test]
fn provisioned_scan_only_flags_when_secrets_present() {
let s = ProvisionedScan {
has_secrets: false,
has_bootstrap_seed: true,
config_file_exists: true,
};
assert!(!s.is_provisioned());
let s = ProvisionedScan {
has_secrets: true,
has_bootstrap_seed: false,
config_file_exists: false,
};
assert!(s.is_provisioned());
}
#[test]
fn back_up_config_skips_missing_file() {
let p = PathBuf::from("/tmp/__webvh_does_not_exist__/config.toml");
back_up_config(&p).expect("missing file is a no-op");
}
}