From f54f3e0f24cd03fe1b17d612153b0ce5bbdfcddc Mon Sep 17 00:00:00 2001 From: Olivier 'reivilibre Date: Wed, 6 Dec 2023 00:08:50 +0000 Subject: [PATCH] Implement the rest of the user management CLI commands --- ...8deaed394f50e9cb38f4131dd5ce556878bae.json | 15 +++ ...7402f235f8eba9a4ed6adb7bfb6824ec86396.json | 32 +++++++ ...2d71938b68093974f335a4d89df91874fdaa2.json | 14 +++ Cargo.lock | 60 ++++++++++++ Cargo.toml | 1 + src/bin/idcoop.rs | 93 +++++++++++++++++-- src/store.rs | 36 +++++++ 7 files changed, 245 insertions(+), 6 deletions(-) create mode 100644 .sqlx/query-5531ef5a44d20da0d6286d101218deaed394f50e9cb38f4131dd5ce556878bae.json create mode 100644 .sqlx/query-bbaa41c568bee22eae314186e0d7402f235f8eba9a4ed6adb7bfb6824ec86396.json create mode 100644 .sqlx/query-dfa520877c017cd5808d02c24ef2d71938b68093974f335a4d89df91874fdaa2.json diff --git a/.sqlx/query-5531ef5a44d20da0d6286d101218deaed394f50e9cb38f4131dd5ce556878bae.json b/.sqlx/query-5531ef5a44d20da0d6286d101218deaed394f50e9cb38f4131dd5ce556878bae.json new file mode 100644 index 0000000..ac1c1b9 --- /dev/null +++ b/.sqlx/query-5531ef5a44d20da0d6286d101218deaed394f50e9cb38f4131dd5ce556878bae.json @@ -0,0 +1,15 @@ +{ + "db_name": "PostgreSQL", + "query": "UPDATE users SET locked = $1 WHERE user_id = $2", + "describe": { + "columns": [], + "parameters": { + "Left": [ + "Bool", + "Uuid" + ] + }, + "nullable": [] + }, + "hash": "5531ef5a44d20da0d6286d101218deaed394f50e9cb38f4131dd5ce556878bae" +} diff --git a/.sqlx/query-bbaa41c568bee22eae314186e0d7402f235f8eba9a4ed6adb7bfb6824ec86396.json b/.sqlx/query-bbaa41c568bee22eae314186e0d7402f235f8eba9a4ed6adb7bfb6824ec86396.json new file mode 100644 index 0000000..8ee4c1c --- /dev/null +++ b/.sqlx/query-bbaa41c568bee22eae314186e0d7402f235f8eba9a4ed6adb7bfb6824ec86396.json @@ -0,0 +1,32 @@ +{ + "db_name": "PostgreSQL", + "query": "SELECT user_name, user_id, locked FROM users ORDER BY user_name", + "describe": { + "columns": [ + { + "ordinal": 0, + "name": "user_name", + "type_info": "Text" + }, + { + "ordinal": 1, + "name": "user_id", + "type_info": "Uuid" + }, + { + "ordinal": 2, + "name": "locked", + "type_info": "Bool" + } + ], + "parameters": { + "Left": [] + }, + "nullable": [ + false, + false, + false + ] + }, + "hash": "bbaa41c568bee22eae314186e0d7402f235f8eba9a4ed6adb7bfb6824ec86396" +} diff --git a/.sqlx/query-dfa520877c017cd5808d02c24ef2d71938b68093974f335a4d89df91874fdaa2.json b/.sqlx/query-dfa520877c017cd5808d02c24ef2d71938b68093974f335a4d89df91874fdaa2.json new file mode 100644 index 0000000..79adb7f --- /dev/null +++ b/.sqlx/query-dfa520877c017cd5808d02c24ef2d71938b68093974f335a4d89df91874fdaa2.json @@ -0,0 +1,14 @@ +{ + "db_name": "PostgreSQL", + "query": "DELETE FROM users WHERE user_id = $1", + "describe": { + "columns": [], + "parameters": { + "Left": [ + "Uuid" + ] + }, + "nullable": [] + }, + "hash": "dfa520877c017cd5808d02c24ef2d71938b68093974f335a4d89df91874fdaa2" +} diff --git a/Cargo.lock b/Cargo.lock index 211a927..2e527c9 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -612,6 +612,18 @@ version = "1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "acbf1af155f9b9ef647e42cdc158db4b64a1b61f743629225fde6f3e0be2a7c7" +[[package]] +name = "comfy-table" +version = "7.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7c64043d6c7b7a4c58e39e7efccfdea7b93d885a795d0c054a69dbbf4dd52686" +dependencies = [ + "crossterm", + "strum", + "strum_macros", + "unicode-width", +] + [[package]] name = "confique" version = "0.2.4" @@ -759,6 +771,28 @@ dependencies = [ "cfg-if", ] +[[package]] +name = "crossterm" +version = "0.27.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f476fe445d41c9e991fd07515a6f463074b782242ccf4a5b7b1d1012e70824df" +dependencies = [ + "bitflags 2.4.1", + "crossterm_winapi", + "libc", + "parking_lot", + "winapi", +] + +[[package]] +name = "crossterm_winapi" +version = "0.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "acdd7c62a3665c7f6830a51635d9ac9b23ed385797f70a83bb8bafe9c572ab2b" +dependencies = [ + "winapi", +] + [[package]] name = "crunchy" version = "0.2.2" @@ -1561,6 +1595,7 @@ dependencies = [ "base64", "chrono", "clap", + "comfy-table", "confique", "eyre", "futures", @@ -3149,6 +3184,25 @@ version = "0.10.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "73473c0e59e6d5812c5dfe2a064a6444949f089e20eec9a2e5506596494e4623" +[[package]] +name = "strum" +version = "0.25.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "290d54ea6f91c969195bdbcd7442c8c2a2ba87da8bf60a7ee86a235d4bc1e125" + +[[package]] +name = "strum_macros" +version = "0.25.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "23dc1fa9ac9c169a78ba62f0b841814b7abae11bdd047b9c58f893439e309ea0" +dependencies = [ + "heck 0.4.1", + "proc-macro2", + "quote", + "rustversion", + "syn 2.0.38", +] + [[package]] name = "subtle" version = "2.5.0" @@ -3559,6 +3613,12 @@ version = "1.10.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1dd624098567895118886609431a7c3b8f516e41d30e0643f03d94592a147e36" +[[package]] +name = "unicode-width" +version = "0.1.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e51733f11c9c4f72aa0c160008246859e340b00807569a0da0e7a1079b27ba85" + [[package]] name = "unicode_categories" version = "0.1.1" diff --git a/Cargo.toml b/Cargo.toml index 4f1ecfd..82bea65 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -15,6 +15,7 @@ axum_csrf = { version = "0.7.2", features = ["layer"] } base64 = "0.21.5" chrono = "0.4.31" clap = { version = "4.4.6", features = ["derive"] } +comfy-table = "7.1.0" confique = { version = "0.2.4", features = ["toml"], default-features = false } eyre = "0.6.8" futures = "0.3.29" diff --git a/src/bin/idcoop.rs b/src/bin/idcoop.rs index 562b5f9..19a8768 100644 --- a/src/bin/idcoop.rs +++ b/src/bin/idcoop.rs @@ -3,8 +3,10 @@ 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}; +use eyre::{bail, Context, ContextCompat}; use idcoop::config::SecretConfig; use idcoop::passwords::create_password_hash; use idcoop::store::{CreateUser, IdCoopStore}; @@ -124,7 +126,11 @@ enum UserCommand { }, /// Lists all users that are registered. #[clap(alias = "ls")] - ListAll {}, + 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<()> { @@ -147,9 +153,48 @@ async fn handle_user_command(command: UserCommand, config: &Configuration) -> ey .await .context("failed to add user")?; } - UserCommand::Delete { username } => todo!(), - UserCommand::Lock { username } => todo!(), - UserCommand::Unlock { username } => todo!(), + 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 @@ -175,7 +220,43 @@ async fn handle_user_command(command: UserCommand, config: &Configuration) -> ey }) .await?; } - UserCommand::ListAll {} => todo!(), + 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/store.rs b/src/store.rs index 13db09d..2e5a67b 100644 --- a/src/store.rs +++ b/src/store.rs @@ -64,6 +64,12 @@ pub struct CreateUser { pub locked: bool, } +pub struct UserInfo { + pub user_name: String, + pub user_id: Uuid, + pub locked: bool, +} + pub struct IdCoopStoreTxn<'a, 'txn> { txn: &'a mut Transaction<'txn, Postgres>, } @@ -118,4 +124,34 @@ impl<'a, 'txn> IdCoopStoreTxn<'a, 'txn> { .context("failed to set user password in DB")?; Ok(()) } + + pub async fn set_user_locked(&mut self, user_id: Uuid, locked: bool) -> eyre::Result<()> { + sqlx::query!( + "UPDATE users SET locked = $1 WHERE user_id = $2", + locked, + user_id + ) + .execute(&mut **self.txn) + .await + .context("failed to set user (un)locked")?; + Ok(()) + } + + pub async fn delete_user(&mut self, user_id: Uuid) -> eyre::Result<()> { + sqlx::query!("DELETE FROM users WHERE user_id = $1", user_id) + .execute(&mut **self.txn) + .await + .context("failed to delete user")?; + Ok(()) + } + + pub async fn list_user_info(&mut self) -> eyre::Result> { + sqlx::query_as!( + UserInfo, + "SELECT user_name, user_id, locked FROM users ORDER BY user_name" + ) + .fetch_all(&mut **self.txn) + .await + .context("failed to list users") + } }