rok-cli 0.3.2

Developer CLI for rok-based Axum applications
//! `rok key:generate` and `rok key:rotate` — JWT secret management.

use rand::RngCore;

fn generate_hex_key() -> String {
    let mut bytes = [0u8; 64];
    rand::thread_rng().fill_bytes(&mut bytes);
    bytes.iter().map(|b| format!("{b:02x}")).collect()
}

pub fn run() {
    let secret = generate_hex_key();
    println!("JWT_SECRET={secret}");
    println!();
    println!("Add this to your .env file.");
}

/// `rok key:rotate` — generate a new key and optionally write it to .env.
pub fn rotate(key_name: &str, write: bool) -> anyhow::Result<()> {
    use console::style;

    let new_key = generate_hex_key();

    // Read existing .env to show old value (masked)
    let env_path = std::path::Path::new(".env");
    let old_masked = if env_path.exists() {
        let content = std::fs::read_to_string(env_path)?;
        content
            .lines()
            .find(|l| l.starts_with(&format!("{key_name}=")))
            .map(|line| {
                let val = line.split_once('=').map(|x| x.1).unwrap_or("");
                if val.len() > 8 {
                    format!("{}{}", &val[..4], &val[val.len() - 4..])
                } else {
                    "****".to_string()
                }
            })
            .unwrap_or_else(|| "(not set)".to_string())
    } else {
        "(no .env file)".to_string()
    };

    println!("{} Rotating {key_name}", style("key:rotate").green().bold());
    println!("  Old: {old_masked}");
    println!("  New: {}{}", &new_key[..8], &new_key[new_key.len() - 8..]);
    println!();

    println!(
        "  {} Existing JWTs and encrypted values signed with the old key will be invalidated.",
        style("").yellow()
    );

    if write {
        let new_line = format!("{key_name}={new_key}");
        if env_path.exists() {
            let content = std::fs::read_to_string(env_path)?;
            let updated = if content
                .lines()
                .any(|l| l.starts_with(&format!("{key_name}=")))
            {
                content
                    .lines()
                    .map(|l| {
                        if l.starts_with(&format!("{key_name}=")) {
                            new_line.clone()
                        } else {
                            l.to_string()
                        }
                    })
                    .collect::<Vec<_>>()
                    .join("\n")
                    + "\n"
            } else {
                format!("{content}\n{new_line}\n")
            };
            std::fs::write(env_path, updated)?;
            println!("  {} Updated .env with new {key_name}", style("").green());
        } else {
            std::fs::write(env_path, format!("{new_line}\n"))?;
            println!("  {} Created .env with {key_name}", style("").green());
        }
    } else {
        println!();
        println!("  Run with --write to update .env in place:");
        println!("    rok key:rotate --write");
        println!();
        println!("  Or manually update your .env:");
        println!("    {key_name}={new_key}");
    }

    Ok(())
}