naru-config 0.7.0

A security-first configuration manager with encryption and audit logging
Documentation
use crate::cli::interactive;
use crate::core::audit;
use crate::core::constants::{NARU_DIR, SCHEMA_FILE};
use crate::core::models::{FieldDefinition, SchemaFile};
use crate::core::persistence;
use crate::core::schema;
use crate::core::security;
use anyhow::{anyhow, Result};

pub struct SchemaAddCommand {
    pub key: Option<String>,
    pub r#type: String,
    pub description: Option<String>,
    pub secret: bool,
}

pub struct SchemaRemoveCommand {
    pub key: Option<String>,
}

pub struct SchemaEditCommand {
    pub key: Option<String>,
}

pub fn execute_add(cmd: SchemaAddCommand) -> Result<()> {
    let field_def = if let Some(key) = cmd.key {
        security::validate_config_key(&key).map_err(|e| anyhow!("Invalid config key: {}", e))?;

        FieldDefinition {
            key: key.clone(),
            r#type: cmd.r#type,
            description: cmd.description,
            validation: None,
            is_secret: cmd.secret,
        }
    } else {
        interactive::prompt_add_field()?
    };

    let key_name = field_def.key.clone();
    schema::add_field(field_def)?;

    let log_path = format!("{}/audit.log", NARU_DIR);
    if let Err(e) = audit::log_action(
        "SCHEMA_ADD",
        "schema",
        Some(&key_name),
        None,
        None,
        &log_path,
    ) {
        eprintln!("Warning: Failed to log audit entry: {}", e);
    }

    Ok(())
}

pub fn execute_remove(cmd: SchemaRemoveCommand) -> Result<()> {
    let key = if let Some(key) = cmd.key {
        key
    } else {
        let schema: SchemaFile =
            persistence::atomic_read_json(SCHEMA_FILE, |s: &SchemaFile| s.clone()).unwrap_or_else(
                |_| SchemaFile {
                    version: "1.0".to_string(),
                    fields: vec![],
                },
            );
        interactive::prompt_select_field(&schema)?
    };

    schema::remove_field(&key)?;

    let log_path = format!("{}/audit.log", NARU_DIR);
    if let Err(e) = audit::log_action("SCHEMA_REMOVE", "schema", Some(&key), None, None, &log_path)
    {
        eprintln!("Warning: Failed to log audit entry: {}", e);
    }

    Ok(())
}

pub fn execute_view() -> Result<()> {
    let schema: SchemaFile = persistence::atomic_read_json(SCHEMA_FILE, |s: &SchemaFile| s.clone())
        .unwrap_or_else(|_| {
            eprintln!("Warning: Could not load schema file, using default schema");
            SchemaFile {
                version: "1.0".to_string(),
                fields: vec![],
            }
        });

    println!("Schema version: {}", schema.version);
    if schema.fields.is_empty() {
        println!("No fields defined in schema.");
    } else {
        println!("Fields:");
        for field in &schema.fields {
            println!(
                "  - {}: {} ({})",
                field.key,
                field.r#type,
                field.description.as_deref().unwrap_or("no description")
            );
        }
    }
    Ok(())
}

pub fn execute_edit(cmd: SchemaEditCommand) -> Result<()> {
    let schema: SchemaFile = persistence::atomic_read_json(SCHEMA_FILE, |s: &SchemaFile| s.clone())
        .map_err(|e| anyhow!("Failed to load schema: {}", e))?;

    let key = if let Some(key) = cmd.key {
        key
    } else {
        interactive::prompt_select_field(&schema)?
    };

    security::validate_config_key(&key).map_err(|e| anyhow!("Invalid field key: {}", e))?;

    let existing_field = schema
        .fields
        .iter()
        .find(|f| f.key == key)
        .ok_or_else(|| anyhow!("Field '{}' not found in schema", key))?;

    let updated_field = interactive::prompt_edit_field(existing_field)?;
    let key_name = updated_field.key.clone();
    schema::update_field(&key, updated_field)?;

    let log_path = format!("{}/audit.log", NARU_DIR);
    if let Err(e) = audit::log_action(
        "SCHEMA_EDIT",
        "schema",
        Some(&key_name),
        None,
        None,
        &log_path,
    ) {
        eprintln!("Warning: Failed to log audit entry: {}", e);
    }

    Ok(())
}