Adding admin commands

This commit is contained in:
Daniel Flanagan 2024-07-14 15:55:05 -05:00
parent 9e1fcb2923
commit f1b1533268
2 changed files with 121 additions and 21 deletions

View file

@ -1,15 +1,32 @@
use color_eyre::eyre::anyhow;
use rand::{distributions::Alphanumeric, thread_rng, Rng};
use sled::IVec;
use uuid::Uuid;
use crate::{
db::{self, Data},
user::{self, User},
};
use super::prelude::*;
#[derive(Args)]
pub struct Admin {
/// Register a user account
#[command(subcommand)]
command: Commands,
command: AdminCommands,
}
#[derive(Subcommand)]
enum Commands {
CreateAccount(CreateAccount),
pub enum AdminCommands {
#[command(subcommand)]
Accounts(AccountsCommands),
}
#[derive(Subcommand)]
enum AccountsCommands {
Create(Create),
Delete(Delete),
List(List),
}
#[derive(Error, Debug)]
@ -24,40 +41,112 @@ pub enum RunError {
impl Admin {
pub async fn run(&self) -> Result<(), RunError> {
match &self.command {
Commands::CreateAccount(args) => Ok(args.run().await?),
AdminCommands::Accounts(accounts) => match accounts {
AccountsCommands::Create(args) => Ok(args.run().await?),
AccountsCommands::Delete(args) => Ok(args.run().await?),
AccountsCommands::List(args) => Ok(args.run().await?),
},
}
}
}
#[derive(Args)]
pub struct List {}
impl List {
pub async fn run(&self) -> Result<(), CreateAccountError> {
let db = Data::try_new()?;
for entry in db.all::<IVec, User>(User::tree())? {
if let Ok((_, user)) = entry {
println!("ID: {}, Username: {}", user.id, user.username);
} else {
println!("Invalid user entry when iterating database: {:?}", entry)
}
}
Ok(())
}
}
#[derive(Args)]
pub struct Delete {
id: Uuid,
}
impl Delete {
pub async fn run(&self) -> Result<(), color_eyre::Report> {
let db = Data::try_new()?;
let result = db.delete::<Uuid, User>(User::tree(), self.id)?;
if let Some(user) = result {
println!(
"Deleted user with username: {} (ID: {}",
user.username, self.id
);
} else {
return Err(anyhow!("user not found"));
}
Ok(())
}
}
/// Create a user account
#[derive(Args)]
pub struct CreateAccount {
pub struct Create {
/// Whether or not the user is a site super admin
#[arg(short, long, default_value = "false")]
#[arg(long, default_value = "false")]
pub superadmin: bool,
/// The email address of the user account
#[arg(short = 'e', long)]
pub email_address: String,
/// The email address of the account
// #[arg(short = 'e', long)]
// pub email_address: String,
/// The user's initial password - if none is set, a random one will be used and output
#[arg(short, long)]
/// The username of the new account
#[arg(short = 'u', long)]
pub username: String,
/// The account's initial password - if none is set, a random one will be used and output
#[arg(short = 'p', long)]
pub initial_password: Option<String>,
}
#[derive(Error, Debug)]
pub enum CreateAccountError {}
pub enum CreateAccountError {
#[error("password hash error: {0}")]
PasswordHash(#[from] argon2::password_hash::Error),
impl CreateAccount {
#[error("data error: {0}")]
Data(#[from] db::Error),
#[error("user error: {0}")]
User(#[from] user::Error),
#[error("bincode error: {0}")]
Bincode(#[from] bincode::Error),
}
impl Create {
pub async fn run(&self) -> Result<(), CreateAccountError> {
// self.email_address
let password = self.initial_password.unwrap_or_else(|| {
rand::thread_rng()
.sample_iter(&Alphanumeric)
.take(7)
.map(char::from)
.collect()
});
let password: String = if let Some(password) = self.initial_password.as_ref() {
password.clone()
} else {
let generated_password: String = (0..16)
.map(|_| thread_rng().sample(Alphanumeric) as char)
.collect();
println!("Generated password: {generated_password}");
generated_password
};
let user = User::try_new(&self.username, &password)?;
let db = Data::try_new()?;
let existing_user: Option<User> = db.get(User::tree(), &self.username)?;
// TODO: fail2ban?
if existing_user.is_some() {
// timing/enumeration attacks or something
return Err(user::Error::UsernameExists(Box::new(self.username.clone())).into());
}
db.insert(User::tree(), &user.username, bincode::serialize(&user)?)?;
println!("Created user with username: {}", self.username);
Ok(())
}

View file

@ -23,6 +23,17 @@ impl Data {
})
}
pub fn delete<K, V>(&self, tree_name: &str, key: K) -> Result<Option<V>, Error>
where
K: AsRef<[u8]>,
V: DeserializeOwned,
{
match self.db.open_tree(tree_name)?.remove(key.as_ref())? {
Some(v) => Ok(Some(bincode::deserialize::<V>(&v)?)),
None => Ok(None),
}
}
pub fn get<K, V>(&self, tree_name: &str, key: K) -> Result<Option<V>, Error>
where
K: AsRef<[u8]>,