feat: authentication system

This commit is contained in:
Ninjdai 2025-08-03 01:50:18 +02:00
parent d8c29e1ec8
commit 37153c6e36
15 changed files with 852 additions and 18 deletions

147
src/utils/cli.rs Normal file
View 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
}
}