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:
@@ -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,
|
||||
}))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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" }))
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user