Adding admin commands
This commit is contained in:
parent
9e1fcb2923
commit
f1b1533268
2 changed files with 121 additions and 21 deletions
131
src/cli/admin.rs
131
src/cli/admin.rs
|
@ -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(())
|
||||
}
|
||||
|
|
11
src/db.rs
11
src/db.rs
|
@ -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]>,
|
||||
|
|
Loading…
Reference in a new issue