feat: authentication system
This commit is contained in:
parent
d8c29e1ec8
commit
37153c6e36
15 changed files with 852 additions and 18 deletions
147
src/utils/cli.rs
Normal file
147
src/utils/cli.rs
Normal file
|
|
@ -0,0 +1,147 @@
|
|||
use std::{fmt::Display, sync::Arc};
|
||||
|
||||
use argon2::{password_hash::{SaltString}, Argon2, PasswordHasher};
|
||||
use inquire::{min_length, prompt_text, Confirm, Password, Select, Text};
|
||||
use password_hash::rand_core::OsRng;
|
||||
use sea_orm::{ActiveModelTrait, ActiveValue::{NotSet, Set}, ColumnTrait, DatabaseConnection, EntityTrait, ModelTrait, QueryFilter};
|
||||
|
||||
use crate::entities::{prelude::User, user::{self, ActiveModel}};
|
||||
|
||||
#[derive(Debug, Copy, Clone)]
|
||||
enum Action {
|
||||
Add,
|
||||
Delete,
|
||||
Update
|
||||
}
|
||||
|
||||
impl Action {
|
||||
const VARIANTS: &'static [Action] = &[
|
||||
Self::Add,
|
||||
Self::Delete,
|
||||
Self::Update,
|
||||
];
|
||||
}
|
||||
|
||||
impl Display for Action {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
write!(f, "{self:?}")
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Copy, Clone)]
|
||||
enum Update {
|
||||
Username,
|
||||
Password
|
||||
}
|
||||
|
||||
impl Update {
|
||||
const VARIANTS: &'static [Update] = &[
|
||||
Self::Username,
|
||||
Self::Password,
|
||||
];
|
||||
}
|
||||
|
||||
impl Display for Update {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
write!(f, "{self:?}")
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn manage_users(db: Arc<DatabaseConnection>) {
|
||||
loop {
|
||||
match Select::new("User Manager (ESC to quit)", Action::VARIANTS.to_vec()).prompt_skippable() {
|
||||
Ok(Some(action)) => {
|
||||
match action {
|
||||
Action::Add => {
|
||||
let username = Text::new("Username").with_validator(min_length!(3)).prompt().unwrap();
|
||||
if User::find().filter(user::Column::Username.eq(username.clone())).one(db.as_ref()).await.is_ok_and(|r| r.is_some()) {
|
||||
println!("Username already in use !");
|
||||
} else {
|
||||
let password = Password::new("Password")
|
||||
.with_validator(min_length!(10))
|
||||
.with_display_mode(inquire::PasswordDisplayMode::Masked)
|
||||
.prompt().unwrap();
|
||||
let new_user = user::ActiveModel {
|
||||
id: NotSet,
|
||||
username: Set(username),
|
||||
hashed_password: Set(hash_password(password))
|
||||
};
|
||||
let _ = new_user.insert(db.as_ref()).await;
|
||||
}
|
||||
},
|
||||
Action::Delete => {
|
||||
match select_user(db.clone()).await {
|
||||
Some(user) => {
|
||||
let username = user.username.clone();
|
||||
|
||||
match Confirm::new(format!("Delete {username} ?").as_ref())
|
||||
.with_default(false)
|
||||
.with_help_message("The user and all associated data will *permanently* be deleted")
|
||||
.prompt() {
|
||||
Ok(true) => {
|
||||
let _ = user.delete(db.as_ref()).await;
|
||||
println!("{username} has been permanently deleted")
|
||||
},
|
||||
Ok(false) | Err(_) => println!("Cancelled deletion of {username}"),
|
||||
}
|
||||
},
|
||||
None => println!("Could not find user")
|
||||
}
|
||||
},
|
||||
Action::Update => {
|
||||
match select_user(db.clone()).await {
|
||||
Some(user) => {
|
||||
match Select::new(format!("Editing {}", user.username).as_ref(), Update::VARIANTS.to_vec()).prompt() {
|
||||
Ok(v) => {
|
||||
let mut updated_user = ActiveModel::from(user);
|
||||
match v {
|
||||
Update::Username => {
|
||||
updated_user.username = Set(Text::new("New username")
|
||||
.with_initial_value(updated_user.username.unwrap().as_ref())
|
||||
.prompt().unwrap()
|
||||
)
|
||||
},
|
||||
Update::Password => {
|
||||
match Password::new("Password")
|
||||
.with_validator(min_length!(10))
|
||||
.with_display_mode(inquire::PasswordDisplayMode::Masked)
|
||||
.prompt() {
|
||||
Ok(new_password) => updated_user.hashed_password = Set(hash_password(new_password)),
|
||||
Err(_) => println!("Cancelled user update")
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
let _ = updated_user.save(db.as_ref()).await;
|
||||
},
|
||||
Err(_) => {}
|
||||
}
|
||||
},
|
||||
None => println!("Could not find user")
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
Ok(None) | Err(_) => {
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn hash_password(password: String) -> String {
|
||||
let salt = SaltString::generate(&mut OsRng);
|
||||
Argon2::default().hash_password(&password.clone().into_bytes(), &salt).unwrap().to_string()
|
||||
}
|
||||
|
||||
async fn select_user(db: Arc<DatabaseConnection>) -> Option<user::Model> {
|
||||
let users = User::find().all(db.as_ref()).await.unwrap();
|
||||
if users.is_empty() {
|
||||
return None;
|
||||
}
|
||||
match Select::new("Select a user", users.iter().map(|u| u.username.clone()).collect()).prompt() {
|
||||
Ok(selection) => User::find().filter(user::Column::Username.eq(selection)).one(db.as_ref()).await.unwrap(),
|
||||
Err(_) => None
|
||||
}
|
||||
}
|
||||
|
||||
Reference in a new issue