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:
parent
8aeb19b752
commit
d088af98d8
@ -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
184
src/cli.rs
Normal 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(())
|
||||
}
|
@ -5,6 +5,7 @@
|
||||
|
||||
#![deny(missing_docs)]
|
||||
|
||||
pub mod cli;
|
||||
pub mod config;
|
||||
pub mod passwords;
|
||||
pub mod store;
|
||||
|
Loading…
Reference in New Issue
Block a user