Skip to main content

ironflow_cli/commands/
user.rs

1//! User subcommands: list, create, delete, set-role.
2
3use std::slice;
4
5use anyhow::{Context, Result};
6use clap::{ArgGroup, Args, Subcommand};
7use ironflow_sdk::IronflowClient;
8use ironflow_sdk::types::{CreateUserRequest, UpdateRoleRequest};
9use uuid::Uuid;
10
11use crate::confirm::{confirm, resolve_secret_value};
12use crate::output;
13
14/// Arguments for the `user` command group.
15#[derive(Debug, Args)]
16pub struct UserArgs {
17    /// User subcommand.
18    #[command(subcommand)]
19    pub command: UserCommands,
20}
21
22/// Available user subcommands.
23#[derive(Debug, Subcommand)]
24pub enum UserCommands {
25    /// List users.
26    List,
27    /// Create a user.
28    Create {
29        /// Display username.
30        username: String,
31        /// Email address.
32        #[arg(long)]
33        email: String,
34        /// Plaintext password (min 8 characters). Read from stdin when
35        /// omitted, which keeps it out of the shell history.
36        #[arg(long)]
37        password: Option<String>,
38        /// Grant admin rights to the new user.
39        #[arg(long)]
40        admin: bool,
41    },
42    /// Delete a user.
43    Delete {
44        /// User UUID.
45        id: Uuid,
46        /// Skip the interactive confirmation.
47        #[arg(long)]
48        yes: bool,
49    },
50    /// Promote a user to admin or demote them to member.
51    #[command(group(ArgGroup::new("role").required(true).args(["admin", "member"])))]
52    SetRole {
53        /// User UUID.
54        id: Uuid,
55        /// Grant admin rights.
56        #[arg(long)]
57        admin: bool,
58        /// Revoke admin rights.
59        #[arg(long)]
60        member: bool,
61    },
62}
63
64/// Execute a user subcommand.
65///
66/// # Errors
67///
68/// Returns an error on API failure, on an empty password, or when a
69/// destructive command is not confirmed.
70pub async fn execute(client: &IronflowClient, args: &UserArgs, json_mode: bool) -> Result<()> {
71    match &args.command {
72        UserCommands::List => {
73            let response = client.list_users().await?;
74            output::print_output(json_mode, &response, || output::users_table(&response.data))?;
75        }
76        UserCommands::Create {
77            username,
78            email,
79            password,
80            admin,
81        } => {
82            let password = resolve_secret_value(password.as_deref(), "password")?;
83            let request: CreateUserRequest = CreateUserRequest::builder()
84                .username(username.clone())
85                .email(email.clone())
86                .password(password)
87                .is_admin(*admin)
88                .try_into()
89                .context("failed to build CreateUserRequest")?;
90
91            let response = client.create_user(&request).await?;
92            output::print_output(json_mode, &response, || {
93                output::users_table(slice::from_ref(&response.data))
94            })?;
95        }
96        UserCommands::Delete { id, yes } => {
97            confirm(&format!("Delete user '{id}'?"), *yes)?;
98            client.delete_user(*id).await?;
99            output::report_deletion(json_mode, "user", id.to_string())?;
100        }
101        // `--admin` and `--member` are an exclusive, required clap group, so
102        // `admin` alone carries the whole decision.
103        UserCommands::SetRole { id, admin, .. } => {
104            let request: UpdateRoleRequest = UpdateRoleRequest::builder()
105                .is_admin(*admin)
106                .try_into()
107                .context("failed to build UpdateRoleRequest")?;
108
109            let response = client.update_role(*id, &request).await?;
110            output::print_output(json_mode, &response, || {
111                output::users_table(slice::from_ref(&response.data))
112            })?;
113        }
114    }
115    Ok(())
116}