Pull out user command handling into CLI module from the binary target

Signed-off-by: Olivier 'reivilibre <olivier@librepush.net>
This commit is contained in:
Olivier 'reivilibre' 2024-07-06 15:01:40 +01:00
parent 8aeb19b752
commit d088af98d8
3 changed files with 188 additions and 176 deletions

View File

@ -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(())
}

184
src/cli.rs Normal file
View File

@ -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(())
}

View File

@ -5,6 +5,7 @@
#![deny(missing_docs)]
pub mod cli;
pub mod config;
pub mod passwords;
pub mod store;