Skip to main content

actix_web_admin/
cli.rs

1use crate::auth::{AuthError, JsonUserStore, UserStore};
2use std::path::PathBuf;
3
4/// Errors that can occur in CLI operations.
5#[derive(Debug)]
6pub enum CliError {
7    Io(String),
8    Auth(AuthError),
9}
10
11impl std::fmt::Display for CliError {
12    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
13        match self {
14            CliError::Io(msg) => write!(f, "{}", msg),
15            CliError::Auth(e) => write!(f, "{}", e),
16        }
17    }
18}
19
20impl std::error::Error for CliError {}
21
22impl From<AuthError> for CliError {
23    fn from(e: AuthError) -> Self {
24        CliError::Auth(e)
25    }
26}
27
28impl From<String> for CliError {
29    fn from(e: String) -> Self {
30        CliError::Io(e)
31    }
32}
33
34/// Print help text for the admin CLI.
35pub fn print_help(bin_name: &str) {
36    println!("Actix Web Admin CLI");
37    println!();
38    println!("Usage: {} <command> [options]", bin_name);
39    println!();
40    println!("Commands:");
41    println!("  createsuperuser | create | add   Create a new superuser (interactive)");
42    println!("  deleteuser <username>             Delete a user by username");
43    println!("  listusers | list | ls             List all users");
44    println!("  help                              Show this help");
45    println!();
46    println!("Options:");
47    println!("  --file <path>    Path to users.json (default: users.json)");
48    println!();
49    println!("Examples:");
50    println!("  {} createsuperuser", bin_name);
51    println!("  {} createsuperuser --file /path/to/users.json", bin_name);
52    println!("  {} deleteuser admin", bin_name);
53    println!("  {} listusers", bin_name);
54}
55
56/// Parse `--file <path>` from args, defaulting to `users.json`.
57pub fn parse_file_arg(args: &[String]) -> PathBuf {
58    if args.len() >= 2 && args[0] == "--file" {
59        PathBuf::from(&args[1])
60    } else {
61        PathBuf::from("users.json")
62    }
63}
64
65/// Interactive superuser creation using dialoguer prompts.
66///
67/// Must be called from within a tokio runtime (e.g. `#[tokio::main]`).
68pub async fn create_superuser_interactive(args: &[String]) -> Result<(), CliError> {
69    let file_path = parse_file_arg(args);
70
71    println!("Create Superuser");
72    println!("Users will be stored in: {:?}", file_path);
73    println!();
74
75    let username: String = dialoguer::Input::new()
76        .with_prompt("Username")
77        .interact_text()
78        .map_err(|e| CliError::Io(format!("input error: {}", e)))?;
79
80    let email: String = dialoguer::Input::new()
81        .with_prompt("Email")
82        .interact_text()
83        .map_err(|e| CliError::Io(format!("input error: {}", e)))?;
84
85    let name: String = dialoguer::Input::new()
86        .with_prompt("Name")
87        .interact_text()
88        .map_err(|e| CliError::Io(format!("input error: {}", e)))?;
89
90    let password: String = dialoguer::Password::new()
91        .with_prompt("Password")
92        .with_confirmation("Confirm password", "Passwords do not match")
93        .interact()
94        .map_err(|e| CliError::Io(format!("input error: {}", e)))?;
95
96    let store = JsonUserStore::new(&file_path);
97
98    store
99        .create_user(&username, &email, &name, &password, true)
100        .await
101        .map(|user| {
102            println!();
103            println!(
104                "Superuser '{}' created successfully! (id: {})",
105                user.name, user.id
106            );
107        })
108        .map_err(CliError::from)
109}
110
111/// Delete a user by username from a JsonUserStore at the given path.
112///
113/// Must be called from within a tokio runtime (e.g. `#[tokio::main]`).
114pub async fn delete_user(args: &[String]) -> Result<(), CliError> {
115    let (file_path, username) = parse_delete_args(args)?;
116    let store = JsonUserStore::new(&file_path);
117    store
118        .delete_user(&username)
119        .await
120        .map(|_| println!("User '{}' deleted successfully.", username))
121        .map_err(CliError::from)
122}
123
124/// List all users from a JsonUserStore at the given path.
125///
126/// Must be called from within a tokio runtime (e.g. `#[tokio::main]`).
127pub async fn list_users(args: &[String]) -> Result<(), CliError> {
128    let file_path = parse_file_arg(args);
129    let store = JsonUserStore::new(&file_path);
130
131    let users = store.all_users().map_err(CliError::from)?;
132    if users.is_empty() {
133        println!("No users found.");
134        return Ok(());
135    }
136    println!("Users stored in: {:?}", file_path);
137    println!();
138    println!("{:<20} {:<30} {:<20} {}", "Username", "Email", "Name", "Superuser");
139    println!("{}", "-".repeat(80));
140    for u in &users {
141        println!(
142            "{:<20} {:<30} {:<20} {}",
143            u.username,
144            u.email,
145            u.name,
146            if u.is_superuser { "yes" } else { "no" }
147        );
148    }
149    Ok(())
150}
151
152pub(crate) fn parse_delete_args(args: &[String]) -> Result<(PathBuf, String), CliError> {
153    if args.is_empty() {
154        return Err(CliError::Io("Usage: admin-cli deleteuser <username> [--file <path>]".to_string()));
155    }
156    if args[0] == "--file" {
157        let file_path = args.get(1).ok_or_else(|| {
158            CliError::Io("--file requires a path argument".to_string())
159        })?;
160        let username = args.get(2).ok_or_else(|| {
161            CliError::Io("Usage: admin-cli deleteuser <username> --file <path>".to_string())
162        })?;
163        Ok((PathBuf::from(file_path), username.clone()))
164    } else {
165        Ok((PathBuf::from("users.json"), args[0].clone()))
166    }
167}