Files
EnvSync/cli/envsync-cli/src/commands/rollback.rs
T
Aryan Singh 473e8e01fa Add backend API for projects & snapshots
Introduce full backend API surface for projects, snapshots and user pubkeys: controllers, DTOs, JPA entities, repositories, services, and tests. Add SecurityConfig improvements (JWT decoder, audience/issuer/timestamp validation and CORS), a GlobalExceptionHandler for consistent error responses, and remove a stray test call from BackendApplication. Also update README (architecture, docs, CLI/Frontend/Backend notes), add CLAUDE.md, VSCode settings, and multiple frontend/CLI Rust changes. Small housekeeping: add .DS_Store and test adjustments.
2026-03-14 18:30:10 -05:00

97 lines
3.3 KiB
Rust

use anyhow::{anyhow, Result};
use base64::{engine::general_purpose::STANDARD as B64, Engine as _};
use dialoguer::{Confirm, Password};
use sodiumoxide::crypto::secretbox;
use std::collections::BTreeMap;
use crate::api::vault::{PushSnapshotRequest, VaultClient};
use crate::config::{Config, LocalProject};
use crate::crypto;
pub async fn run(version: u32, yes: bool, api_url: &str) -> Result<()> {
sodiumoxide::init().map_err(|_| anyhow!("Failed to initialize libsodium"))?;
let config = Config::load()?;
let auth = config.require_auth()?;
let keypair = config.require_keypair()?;
let effective_api_url = if !api_url.is_empty() { api_url } else { &config.api_url };
let mut project = LocalProject::load()?;
let vc = VaultClient::new(effective_api_url, &auth.access_token);
// Find the snapshot with the target version
let snapshots = vc.list_snapshots(&project.project_id, 1000).await?;
let target_meta = snapshots
.iter()
.find(|s| s.version == version)
.ok_or_else(|| anyhow!("Snapshot v{version} not found."))?;
println!(
"Rolling back to v{} — {} — {}",
target_meta.version,
target_meta.created_at,
target_meta.message.as_deref().unwrap_or("-")
);
if !yes {
let confirmed = Confirm::new()
.with_prompt("This will push a new snapshot with the content from that version. Continue?")
.default(false)
.interact()?;
if !confirmed {
println!("Aborted.");
return Ok(());
}
}
// Fetch full snapshot
let snap = vc.get_snapshot(&project.project_id, &target_meta.id).await?;
let passphrase = Password::new()
.with_prompt("Passphrase")
.interact()?;
let salt_bytes = B64.decode(&keypair.argon2_salt)
.map_err(|e| anyhow!("Failed to decode salt: {e}"))?;
let salt: [u8; 32] = salt_bytes
.try_into()
.map_err(|_| anyhow!("Invalid salt length"))?;
let vault_key = crypto::derive_key(&passphrase, &salt)?;
// Decrypt
let ciphertext = B64.decode(&snap.ciphertext)
.map_err(|e| anyhow!("Failed to decode ciphertext: {e}"))?;
let nonce_bytes = B64.decode(&snap.nonce)
.map_err(|e| anyhow!("Failed to decode nonce: {e}"))?;
let nonce = secretbox::Nonce::from_slice(&nonce_bytes)
.ok_or_else(|| anyhow!("Invalid nonce length"))?;
let plaintext = crypto::decrypt_vault(&ciphertext, &nonce, &vault_key)?;
let env_map: BTreeMap<String, String> = serde_json::from_slice(&plaintext)?;
// Re-encrypt with a fresh nonce and push as new snapshot (preserves history)
let (new_ct, new_nonce) = crypto::encrypt_vault(&plaintext, &vault_key)?;
let new_snap = vc
.push_snapshot(
&project.project_id,
PushSnapshotRequest {
ciphertext: B64.encode(&new_ct),
nonce: B64.encode(new_nonce.0),
message: Some(format!("Rollback to v{version}")),
},
)
.await?;
// Write .env
let env_path = std::env::current_dir()?.join(".env");
crate::commands::pull::write_env_file(&env_path, &env_map)?;
project.latest_version = new_snap.version;
project.save()?;
println!(
"Rolled back to v{version} content → pushed as v{} ({} keys)",
new_snap.version,
env_map.len()
);
Ok(())
}