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.
This commit is contained in:
1 parent
3d72c53262
commit
473e8e01fa
57 files changed
+3888
-281
No files matched your search
@@ -0,0 +1,96 @@
|
||||
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(())
|
||||
}
|
||||
Reference in new issue
Block a user