diff --git a/src/bin/idcoop.rs b/src/bin/idcoop.rs index cbdfc4a..582902c 100644 --- a/src/bin/idcoop.rs +++ b/src/bin/idcoop.rs @@ -1,15 +1,12 @@ -use std::io::stdin; use std::sync::Arc; use std::{net::SocketAddr, path::PathBuf}; use clap::Parser; -use comfy_table::presets::UTF8_FULL; -use comfy_table::{Attribute, Cell, Color, ContentArrangement, Row, Table}; use confique::{Config, Partial}; -use eyre::{bail, Context, ContextCompat}; +use eyre::{bail, Context}; +use idcoop::cli::{handle_user_command, UserCommand}; use idcoop::config::{SecretConfig, SeparateSecretConfiguration}; -use idcoop::passwords::create_password_hash; -use idcoop::store::{CreateUser, IdCoopStore}; +use idcoop::store::IdCoopStore; use idcoop::{config::Configuration, web}; use tracing_subscriber::fmt::format::FmtSpan; use tracing_subscriber::layer::SubscriberExt; @@ -113,173 +110,3 @@ async fn main() -> eyre::Result<()> { Ok(()) } - -/// Commands for user management. -#[derive(Clone, Parser)] -enum UserCommand { - /// Add a user. - #[clap(alias = "new", alias = "create")] - Add { - /// The login name of the user. - // TODO this should be a richer newtype with validation - username: String, - - #[clap(long = "locked")] - locked: bool, - }, - /// Deletes a user. - /// Consider whether this is what you really want: in most cases locking a user is more appropriate. - #[clap(alias = "remove", alias = "rm", alias = "del")] - Delete { - /// The login name of the user. - username: String, - }, - /// Locks a user, preventing them from logging in. - Lock { - /// The login name of the user. - username: String, - }, - /// Unlocks a user, letting them log in once more. - Unlock { - /// The login name of the user. - username: String, - }, - /// Changes a user's password. - #[clap(alias = "chpass", alias = "passwd")] - ChangePassword { - /// The login name of the user. - username: String, - }, - /// Lists all users that are registered. - #[clap(alias = "ls")] - ListAll { - /// Only show a list of usernames, without table formatting characters and one per line. May be useful in scripts. - #[clap(long = "usernames")] - usernames: bool, - }, -} - -async fn handle_user_command(command: UserCommand, config: &Configuration) -> eyre::Result<()> { - let store = IdCoopStore::connect(&config.postgres.connect) - .await - .context("Failed to connect to Postgres")?; - match command { - UserCommand::Add { username, locked } => { - store - .txn(|mut txn| { - Box::pin(async move { - txn.create_user(CreateUser { - user_login_name: username, - password_hash: None, - locked, - }) - .await - }) - }) - .await - .context("failed to add user")?; - } - UserCommand::Delete { username } => { - store - .txn(|mut txn| { - Box::pin(async move { - let user_id = txn - .lookup_user_by_name(username) - .await? - .context("No user by that name")? - .user_id; - txn.delete_user(user_id).await - }) - }) - .await?; - } - UserCommand::Lock { username } => { - store - .txn(|mut txn| { - Box::pin(async move { - let user_id = txn - .lookup_user_by_name(username) - .await? - .context("No user by that name")? - .user_id; - txn.set_user_locked(user_id, true).await - }) - }) - .await?; - } - UserCommand::Unlock { username } => { - store - .txn(|mut txn| { - Box::pin(async move { - let user_id = txn - .lookup_user_by_name(username) - .await? - .context("No user by that name")? - .user_id; - txn.set_user_locked(user_id, false).await - }) - }) - .await?; - } - UserCommand::ChangePassword { username } => { - let Some(user) = store.txn(|mut txn| { Box::pin(async move { - txn.lookup_user_by_name(username).await - })}).await? else { - bail!("No user by that name."); - }; - println!("Change password for {} ({}):", user.user_name, user.user_id); - let mut buf_line = String::new(); - stdin() - .read_line(&mut buf_line) - .context("failed to read password")?; - let raw_password = buf_line.trim(); - let hash = create_password_hash(raw_password, &config.password_hashing) - .context("unable to hash password!")?; - store - .txn(|mut txn| { - Box::pin( - async move { txn.change_user_password(user.user_id, Some(hash)).await }, - ) - }) - .await?; - } - UserCommand::ListAll { usernames } => { - let user_infos = store - .txn(|mut txn| Box::pin(async move { txn.list_user_info().await })) - .await?; - - if usernames { - for user_info in user_infos { - println!("{}", user_info.user_name); - } - } else { - let mut table = Table::new(); - table - .load_preset(UTF8_FULL) - .set_content_arrangement(ContentArrangement::Dynamic) - .set_width(80) - .set_header(vec![ - Cell::new("Name").add_attribute(Attribute::Bold), - Cell::new("UUID").add_attribute(Attribute::Bold), - Cell::new("Locked").add_attribute(Attribute::Bold), - ]); - - for user_info in user_infos { - let mut row = Row::new(); - row.add_cell(Cell::new(user_info.user_name)); - row.add_cell(Cell::new(user_info.user_id).fg(Color::Grey)); - let (lock_str, lock_colour) = if user_info.locked { - ("yes", Color::Red) - } else { - ("no", Color::White) - }; - row.add_cell(Cell::new(lock_str).fg(lock_colour)); - table.add_row(row); - } - - println!("{}", table); - } - } - } - Ok(()) -} diff --git a/src/cli.rs b/src/cli.rs new file mode 100644 index 0000000..35e1880 --- /dev/null +++ b/src/cli.rs @@ -0,0 +1,184 @@ +//! idCoop Command Line Interface + +use std::io::stdin; + +use crate::config::Configuration; +use crate::passwords::create_password_hash; +use crate::store::{CreateUser, IdCoopStore}; +use clap::Parser; +use comfy_table::presets::UTF8_FULL; +use comfy_table::{Attribute, Cell, Color, ContentArrangement, Row, Table}; +use eyre::{bail, Context, ContextCompat}; + +/// Commands for user management. +#[derive(Clone, Parser)] +pub enum UserCommand { + /// Add a user. + #[clap(alias = "new", alias = "create")] + Add { + /// The login name of the user. + // TODO this should be a richer newtype with validation + username: String, + + /// Set this flag if the user should be locked. + #[clap(long = "locked")] + locked: bool, + }, + /// Deletes a user. + /// Consider whether this is what you really want: in most cases locking a user is more appropriate. + #[clap(alias = "remove", alias = "rm", alias = "del")] + Delete { + /// The login name of the user. + username: String, + }, + /// Locks a user, preventing them from logging in. + Lock { + /// The login name of the user. + username: String, + }, + /// Unlocks a user, letting them log in once more. + Unlock { + /// The login name of the user. + username: String, + }, + /// Changes a user's password. + #[clap(alias = "chpass", alias = "passwd")] + ChangePassword { + /// The login name of the user. + username: String, + }, + /// Lists all users that are registered. + #[clap(alias = "ls")] + ListAll { + /// Only show a list of usernames, without table formatting characters and one per line. May be useful in scripts. + #[clap(long = "usernames")] + usernames: bool, + }, +} + +/// Handles a user command from the command-line interface. +pub async fn handle_user_command(command: UserCommand, config: &Configuration) -> eyre::Result<()> { + let store = IdCoopStore::connect(&config.postgres.connect) + .await + .context("Failed to connect to Postgres")?; + match command { + UserCommand::Add { username, locked } => { + store + .txn(|mut txn| { + Box::pin(async move { + txn.create_user(CreateUser { + user_login_name: username, + password_hash: None, + locked, + }) + .await + }) + }) + .await + .context("failed to add user")?; + } + UserCommand::Delete { username } => { + store + .txn(|mut txn| { + Box::pin(async move { + let user_id = txn + .lookup_user_by_name(username) + .await? + .context("No user by that name")? + .user_id; + txn.delete_user(user_id).await + }) + }) + .await?; + } + UserCommand::Lock { username } => { + store + .txn(|mut txn| { + Box::pin(async move { + let user_id = txn + .lookup_user_by_name(username) + .await? + .context("No user by that name")? + .user_id; + txn.set_user_locked(user_id, true).await + }) + }) + .await?; + } + UserCommand::Unlock { username } => { + store + .txn(|mut txn| { + Box::pin(async move { + let user_id = txn + .lookup_user_by_name(username) + .await? + .context("No user by that name")? + .user_id; + txn.set_user_locked(user_id, false).await + }) + }) + .await?; + } + UserCommand::ChangePassword { username } => { + let Some(user) = store + .txn(|mut txn| Box::pin(async move { txn.lookup_user_by_name(username).await })) + .await? + else { + bail!("No user by that name."); + }; + println!("Change password for {} ({}):", user.user_name, user.user_id); + let mut buf_line = String::new(); + stdin() + .read_line(&mut buf_line) + .context("failed to read password")?; + let raw_password = buf_line.trim(); + let hash = create_password_hash(raw_password, &config.password_hashing) + .context("unable to hash password!")?; + store + .txn(|mut txn| { + Box::pin( + async move { txn.change_user_password(user.user_id, Some(hash)).await }, + ) + }) + .await?; + } + UserCommand::ListAll { usernames } => { + let user_infos = store + .txn(|mut txn| Box::pin(async move { txn.list_user_info().await })) + .await?; + + if usernames { + for user_info in user_infos { + println!("{}", user_info.user_name); + } + } else { + let mut table = Table::new(); + table + .load_preset(UTF8_FULL) + .set_content_arrangement(ContentArrangement::Dynamic) + .set_width(80) + .set_header(vec![ + Cell::new("Name").add_attribute(Attribute::Bold), + Cell::new("UUID").add_attribute(Attribute::Bold), + Cell::new("Locked").add_attribute(Attribute::Bold), + ]); + + for user_info in user_infos { + let mut row = Row::new(); + row.add_cell(Cell::new(user_info.user_name)); + row.add_cell(Cell::new(user_info.user_id).fg(Color::Grey)); + let (lock_str, lock_colour) = if user_info.locked { + ("yes", Color::Red) + } else { + ("no", Color::White) + }; + row.add_cell(Cell::new(lock_str).fg(lock_colour)); + table.add_row(row); + } + + println!("{}", table); + } + } + } + Ok(()) +} diff --git a/src/lib.rs b/src/lib.rs index 6720ec7..e979a14 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -5,6 +5,7 @@ #![deny(missing_docs)] +pub mod cli; pub mod config; pub mod passwords; pub mod store;