systemprompt-cli 0.2.1

Unified CLI for systemprompt.io AI governance: agent orchestration, MCP governance, analytics, profiles, cloud deploy, and self-hosted operations.
Documentation
use anyhow::{Result, anyhow};
use clap::Args;
use systemprompt_runtime::AppContext;
use systemprompt_users::{BanDuration, BanIpParams, BannedIpRepository};

use crate::CliConfig;
use crate::commands::admin::users::types::BanAddOutput;
use crate::shared::CommandResult;

const CLI_BAN_SOURCE: &str = "cli";

#[derive(Debug, Args)]
pub struct AddArgs {
    pub ip: String,

    #[arg(long)]
    pub reason: String,

    #[arg(long)]
    pub duration: Option<String>,

    #[arg(long)]
    pub permanent: bool,
}

pub async fn execute(args: AddArgs, _config: &CliConfig) -> Result<CommandResult<BanAddOutput>> {
    let ctx = AppContext::new().await?;
    let ban_repository = BannedIpRepository::new(ctx.db_pool())?;

    let duration = if args.permanent {
        BanDuration::Permanent
    } else {
        parse_duration(args.duration.as_deref())?
    };

    let params = BanIpParams::new(&args.ip, &args.reason, duration, CLI_BAN_SOURCE);

    ban_repository.ban_ip(params).await?;

    let expires_at = duration.to_expiry();

    let output = BanAddOutput {
        ip_address: args.ip.clone(),
        reason: args.reason.clone(),
        expires_at,
        is_permanent: args.permanent,
        message: format!("IP address '{}' has been banned", args.ip),
    };

    Ok(CommandResult::text(output).with_title("IP Banned"))
}

fn parse_duration(duration_str: Option<&str>) -> Result<BanDuration> {
    let Some(s) = duration_str else {
        return Ok(BanDuration::Days(7));
    };

    let s = s.trim().to_lowercase();

    if s.ends_with('h') {
        let hours: i64 = s
            .trim_end_matches('h')
            .parse()
            .map_err(|_| anyhow!("Invalid hours format"))?;
        Ok(BanDuration::Hours(hours))
    } else if s.ends_with('d') {
        let days: i64 = s
            .trim_end_matches('d')
            .parse()
            .map_err(|_| anyhow!("Invalid days format"))?;
        Ok(BanDuration::Days(days))
    } else {
        Err(anyhow!(
            "Invalid duration format. Use format like '1h', '7d', '30d'"
        ))
    }
}