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::sync::Arc;
|
||||||
use std::{net::SocketAddr, path::PathBuf};
|
use std::{net::SocketAddr, path::PathBuf};
|
||||||
|
|
||||||
use clap::Parser;
|
use clap::Parser;
|
||||||
use comfy_table::presets::UTF8_FULL;
|
|
||||||
use comfy_table::{Attribute, Cell, Color, ContentArrangement, Row, Table};
|
|
||||||
use confique::{Config, Partial};
|
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::config::{SecretConfig, SeparateSecretConfiguration};
|
||||||
use idcoop::passwords::create_password_hash;
|
use idcoop::store::IdCoopStore;
|
||||||
use idcoop::store::{CreateUser, IdCoopStore};
|
|
||||||
use idcoop::{config::Configuration, web};
|
use idcoop::{config::Configuration, web};
|
||||||
use tracing_subscriber::fmt::format::FmtSpan;
|
use tracing_subscriber::fmt::format::FmtSpan;
|
||||||
use tracing_subscriber::layer::SubscriberExt;
|
use tracing_subscriber::layer::SubscriberExt;
|
||||||
@ -113,173 +110,3 @@ async fn main() -> eyre::Result<()> {
|
|||||||
|
|
||||||
Ok(())
|
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)]
|
#![deny(missing_docs)]
|
||||||
|
|
||||||
|
pub mod cli;
|
||||||
pub mod config;
|
pub mod config;
|
||||||
pub mod passwords;
|
pub mod passwords;
|
||||||
pub mod store;
|
pub mod store;
|
||||||
|
Loading…
Reference in New Issue
Block a user