153 lines
4 KiB
Rust
153 lines
4 KiB
Rust
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 {
|
|
#[command(subcommand)]
|
|
command: AdminCommands,
|
|
}
|
|
|
|
#[derive(Subcommand)]
|
|
pub enum AdminCommands {
|
|
#[command(subcommand)]
|
|
Accounts(AccountsCommands),
|
|
}
|
|
|
|
#[derive(Subcommand)]
|
|
enum AccountsCommands {
|
|
Create(Create),
|
|
Delete(Delete),
|
|
List(List),
|
|
}
|
|
|
|
#[derive(Error, Debug)]
|
|
pub enum RunError {
|
|
#[error("{0}")]
|
|
Eyre(#[from] color_eyre::Report),
|
|
|
|
#[error("create account error: {0}")]
|
|
CreateAccount(#[from] CreateAccountError),
|
|
}
|
|
|
|
impl Admin {
|
|
pub async fn run(&self) -> Result<(), RunError> {
|
|
match &self.command {
|
|
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 Create {
|
|
/// Whether or not the user is a site super admin
|
|
#[arg(long, default_value = "false")]
|
|
pub superadmin: bool,
|
|
|
|
/// The email address of the account
|
|
// #[arg(short = 'e', long)]
|
|
// pub email_address: String,
|
|
|
|
/// 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 {
|
|
#[error("password hash error: {0}")]
|
|
PasswordHash(#[from] argon2::password_hash::Error),
|
|
|
|
#[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: 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(())
|
|
}
|
|
}
|