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::*;
|
use super::prelude::*;
|
||||||
|
|
||||||
#[derive(Args)]
|
#[derive(Args)]
|
||||||
pub struct Admin {
|
pub struct Admin {
|
||||||
/// Register a user account
|
|
||||||
#[command(subcommand)]
|
#[command(subcommand)]
|
||||||
command: Commands,
|
command: AdminCommands,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Subcommand)]
|
#[derive(Subcommand)]
|
||||||
enum Commands {
|
pub enum AdminCommands {
|
||||||
CreateAccount(CreateAccount),
|
#[command(subcommand)]
|
||||||
|
Accounts(AccountsCommands),
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Subcommand)]
|
||||||
|
enum AccountsCommands {
|
||||||
|
Create(Create),
|
||||||
|
Delete(Delete),
|
||||||
|
List(List),
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Error, Debug)]
|
#[derive(Error, Debug)]
|
||||||
|
@ -24,40 +41,112 @@ pub enum RunError {
|
||||||
impl Admin {
|
impl Admin {
|
||||||
pub async fn run(&self) -> Result<(), RunError> {
|
pub async fn run(&self) -> Result<(), RunError> {
|
||||||
match &self.command {
|
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
|
/// Create a user account
|
||||||
#[derive(Args)]
|
#[derive(Args)]
|
||||||
pub struct CreateAccount {
|
pub struct Create {
|
||||||
/// Whether or not the user is a site super admin
|
/// Whether or not the user is a site super admin
|
||||||
#[arg(short, long, default_value = "false")]
|
#[arg(long, default_value = "false")]
|
||||||
pub superadmin: bool,
|
pub superadmin: bool,
|
||||||
|
|
||||||
/// The email address of the user account
|
/// The email address of the account
|
||||||
#[arg(short = 'e', long)]
|
// #[arg(short = 'e', long)]
|
||||||
pub email_address: String,
|
// pub email_address: String,
|
||||||
|
|
||||||
/// The user's initial password - if none is set, a random one will be used and output
|
/// The username of the new account
|
||||||
#[arg(short, long)]
|
#[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>,
|
pub initial_password: Option<String>,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Error, Debug)]
|
#[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> {
|
pub async fn run(&self) -> Result<(), CreateAccountError> {
|
||||||
// self.email_address
|
// self.email_address
|
||||||
let password = self.initial_password.unwrap_or_else(|| {
|
let password: String = if let Some(password) = self.initial_password.as_ref() {
|
||||||
rand::thread_rng()
|
password.clone()
|
||||||
.sample_iter(&Alphanumeric)
|
} else {
|
||||||
.take(7)
|
let generated_password: String = (0..16)
|
||||||
.map(char::from)
|
.map(|_| thread_rng().sample(Alphanumeric) as char)
|
||||||
.collect()
|
.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(())
|
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>
|
pub fn get<K, V>(&self, tree_name: &str, key: K) -> Result<Option<V>, Error>
|
||||||
where
|
where
|
||||||
K: AsRef<[u8]>,
|
K: AsRef<[u8]>,
|
||||||
|
|
Loading…
Reference in a new issue