feat: factory reset, backup restore, auto-identity creation

- system.factory-reset RPC: wipes user data, preserves images/node_key
- Factory Reset button in Settings with confirmation modal
- backup.restore-identity RPC: decrypts and restores DID key
- Restore from Backup panel in OnboardingIntro first screen
- Auto-create default identity with Nostr key on boot if none exist

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
Dorian
2026-03-15 05:18:12 +00:00
parent b447100637
commit c545b79b65
9 changed files with 346 additions and 13 deletions

View File

@@ -291,4 +291,35 @@ impl RpcHandler {
"size_bytes": size,
}))
}
/// Restore identity from an encrypted DID backup JSON.
/// Params: { backup: { version, blob, ... }, passphrase }
pub(super) async fn handle_backup_restore_identity(
&self,
params: &serde_json::Value,
) -> Result<serde_json::Value> {
let backup = params
.get("backup")
.ok_or_else(|| anyhow::anyhow!("Missing 'backup' parameter"))?;
let passphrase = params
.get("passphrase")
.and_then(|v| v.as_str())
.ok_or_else(|| anyhow::anyhow!("Missing 'passphrase' parameter"))?;
let identity_dir = self.config.data_dir.join("identity");
let (did, pubkey) = crate::backup::restore_encrypted_backup(
&identity_dir,
backup,
passphrase,
)
.await
.context("Identity restore failed")?;
info!(did = %did, "Identity restored from backup");
Ok(serde_json::json!({
"did": did,
"pubkey": pubkey,
}))
}
}

View File

@@ -109,7 +109,10 @@ const UNAUTHENTICATED_METHODS: &[&str] = &[
"auth.login.totp",
"auth.login.backup",
"auth.isOnboardingComplete",
"auth.isSetup",
"health",
// Onboarding restore (before user account exists)
"backup.restore-identity",
// Inter-node RPC: called by federated peers over Tor, no session cookies
"federation.peer-joined",
"federation.peer-address-changed",
@@ -602,6 +605,7 @@ impl RpcHandler {
"system.detect-usb-devices" => self.handle_system_detect_usb_devices().await,
"system.disk-status" => self.handle_system_disk_status().await,
"system.disk-cleanup" => self.handle_system_disk_cleanup().await,
"system.factory-reset" => self.handle_system_factory_reset(params).await,
// Opt-in anonymous analytics
"analytics.get-status" => self.handle_analytics_get_status().await,
@@ -646,6 +650,10 @@ impl RpcHandler {
let p = params.unwrap_or(serde_json::json!({}));
self.handle_backup_restore(&p).await
}
"backup.restore-identity" => {
let p = params.unwrap_or(serde_json::json!({}));
self.handle_backup_restore_identity(&p).await
}
"backup.delete" => {
let p = params.unwrap_or(serde_json::json!({}));
self.handle_backup_delete(&p).await

View File

@@ -590,3 +590,78 @@ async fn read_temperatures() -> Result<Vec<serde_json::Value>> {
Ok(temps)
}
impl RpcHandler {
/// system.factory-reset — Wipe all user data and restart.
/// Preserves container images and node_key (hardware identity).
pub(super) async fn handle_system_factory_reset(
&self,
params: Option<serde_json::Value>,
) -> Result<serde_json::Value> {
// Safety check: require { confirm: true }
let confirmed = params
.as_ref()
.and_then(|p| p.get("confirm"))
.and_then(|v| v.as_bool())
.unwrap_or(false);
if !confirmed {
anyhow::bail!("Factory reset requires {{ \"confirm\": true }}");
}
tracing::warn!("Factory reset initiated — wiping user data");
let data_dir = &self.config.data_dir;
// Stop all running containers
if let Ok(client) = archipelago_container::PodmanClient::detect().await {
if let Ok(containers) = client.list_containers().await {
for c in &containers {
let _ = client.stop_container(&c.names).await;
}
}
}
// Delete user data (preserving node_key and container images)
let files_to_remove = [
"user.json",
"onboarding.json",
"peers.json",
"server-name",
];
for f in &files_to_remove {
let path = data_dir.join(f);
if path.exists() {
let _ = tokio::fs::remove_file(&path).await;
}
}
let dirs_to_remove = [
"identities",
"credentials",
"did-cache",
"dwn",
];
for d in &dirs_to_remove {
let path = data_dir.join(d);
if path.exists() {
let _ = tokio::fs::remove_dir_all(&path).await;
}
}
// Clear all sessions
self.session_store.invalidate_all_except("").await;
tracing::warn!("Factory reset complete — restarting service");
// Restart the service via systemd
tokio::spawn(async {
tokio::time::sleep(std::time::Duration::from_secs(1)).await;
let _ = std::process::Command::new("sudo")
.args(["systemctl", "restart", "archipelago"])
.spawn();
});
Ok(serde_json::json!({ "status": "resetting" }))
}
}