Can create user accounts

This commit is contained in:
Pierre HUBERT 2022-04-07 18:59:48 +02:00
parent 52888b3af7
commit c9ca23cd82
11 changed files with 144 additions and 12 deletions

1
Cargo.lock generated
View File

@ -393,6 +393,7 @@ dependencies = [
"include_dir",
"log",
"mime_guess",
"rand",
"serde",
"serde_json",
"serde_yaml",

View File

@ -21,4 +21,5 @@ uuid = { version = "0.8.2", features = ["v4"] }
mime_guess = "2.0.4"
askama = "0.11.1"
futures-util = "0.3.21"
urlencoding = "2.1.0"
urlencoding = "2.1.0"
rand = "0.8.5"

View File

@ -18,9 +18,6 @@ pub struct LoginRequest {
pub password: String,
}
#[derive(Debug)]
pub struct ChangePasswordResult(pub bool);
#[derive(Message)]
#[rtype(GetUserResult)]
pub struct GetUserRequest(pub UserID);
@ -50,6 +47,16 @@ pub struct ChangePasswordRequest {
pub temporary: bool,
}
#[derive(Debug)]
pub struct ChangePasswordResult(pub bool);
#[derive(Debug)]
pub struct UpdateUserResult(pub bool);
#[derive(Message)]
#[rtype(UpdateUserResult)]
pub struct UpdateUserRequest(pub User);
pub struct UsersActor {
manager: EntityManager<User>,
}
@ -119,4 +126,18 @@ impl Handler<GetAllUsersRequest> for UsersActor {
fn handle(&mut self, _msg: GetAllUsersRequest, _ctx: &mut Self::Context) -> Self::Result {
MessageResult(GetAllUsersResult(self.manager.cloned()))
}
}
impl Handler<UpdateUserRequest> for UsersActor {
type Result = MessageResult<UpdateUserRequest>;
fn handle(&mut self, msg: UpdateUserRequest, _ctx: &mut Self::Context) -> Self::Result {
MessageResult(UpdateUserResult(match self.manager.update_or_replace(msg.0) {
Ok(_) => true,
Err(e) => {
log::error!("Failed to update user information! {:?}", e);
false
}
}))
}
}

View File

@ -37,4 +37,7 @@ pub const LOGIN_ROUTE: &str = "/login";
/// Bruteforce protection
pub const KEEP_FAILED_LOGIN_ATTEMPTS_FOR: u64 = 3600;
pub const MAX_FAILED_LOGIN_ATTEMPTS: usize = 15;
pub const FAIL_LOGIN_ATTEMPT_CLEANUP_INTERVAL: Duration = Duration::from_secs(60);
pub const FAIL_LOGIN_ATTEMPT_CLEANUP_INTERVAL: Duration = Duration::from_secs(60);
/// Temporary password length
pub const TEMPORARY_PASSWORDS_LEN: usize = 20;

View File

@ -6,10 +6,12 @@ use askama::Template;
use crate::actors::users_actor;
use crate::actors::users_actor::UsersActor;
use crate::constants::TEMPORARY_PASSWORDS_LEN;
use crate::controllers::settings_controller::BaseSettingsPage;
use crate::data::client::{Client, ClientManager};
use crate::data::client::{Client, ClientID, ClientManager};
use crate::data::current_user::CurrentUser;
use crate::data::user::User;
use crate::data::user::{hash_password, User, UserID};
use crate::utils::string_utils::rand_str;
#[derive(Template)]
#[template(path = "settings/clients_list.html")]
@ -46,15 +48,85 @@ pub async fn clients_route(user: CurrentUser, clients: web::Data<ClientManager>)
}.render().unwrap())
}
pub async fn users_route(user: CurrentUser, users: web::Data<Addr<UsersActor>>) -> impl Responder {
#[derive(serde::Deserialize, Debug)]
pub struct UpdateUserQuery {
uid: UserID,
username: String,
first_name: String,
last_name: String,
email: String,
gen_new_password: Option<String>,
enabled: Option<String>,
admin: Option<String>,
grant_type: String,
granted_clients: String,
}
pub async fn users_route(user: CurrentUser, users: web::Data<Addr<UsersActor>>, update_query: Option<web::Form<UpdateUserQuery>>) -> impl Responder {
let mut danger = None;
let mut success = None;
if let Some(update) = update_query {
let current_user: Option<User> = users.send(users_actor::FindUserByUsername(update.username.to_string()))
.await.unwrap().0;
let is_creating = current_user.is_none();
let mut user = current_user.unwrap_or_default();
user.uid = update.0.uid;
user.username = update.0.username;
user.first_name = update.0.first_name;
user.last_name = update.0.last_name;
user.email = update.0.email;
user.enabled = update.0.enabled.is_some();
user.admin = update.0.admin.is_some();
user.authorized_clients = match update.0.grant_type.as_str() {
"all_clients" => None,
"custom_clients" => Some(update.0.granted_clients.split(',')
.map(|c| ClientID(c.to_string()))
.collect::<Vec<_>>()),
_ => Some(Vec::new())
};
let new_password = match update.0.gen_new_password.is_some() {
false => None,
true => {
let temp_pass = rand_str(TEMPORARY_PASSWORDS_LEN);
user.password = hash_password(&temp_pass)
.expect("Failed to hash password");
user.need_reset_password = true;
Some(temp_pass)
}
};
let res = users.send(users_actor::UpdateUserRequest(user.clone())).await.unwrap().0;
if !res {
danger = Some(match is_creating {
true => "Failed to create user!",
false => "Failed to update user!"
}.to_string())
} else {
success = Some(match is_creating {
true => format!("User {} was successfully updated!", user.full_name()),
false => format!("Failed to update {}'s account!", user.full_name())
}.to_string());
if let Some(pass) = new_password {
danger = Some(format!("{}'s temporary time password is {}", user.full_name(), pass));
}
}
}
let users = users.send(users_actor::GetAllUsersRequest).await.unwrap().0;
HttpResponse::Ok().body(UsersListTemplate {
_parent: BaseSettingsPage::get(
"Users list",
&user,
None,
None,
danger,
success,
),
users,
}.render().unwrap())

View File

@ -91,4 +91,21 @@ impl<E> EntityManager<E>
pub fn cloned(&self) -> Vec<E> {
self.list.clone()
}
pub fn update_or_replace(&mut self, entry: E) -> Res {
let mut found = false;
for i in &mut self.list {
if i == &entry {
*i = entry.clone();
found = true;
break;
}
}
if !found {
self.list.push(entry);
}
self.save()
}
}

View File

@ -22,6 +22,10 @@ pub struct User {
}
impl User {
pub fn full_name(&self) -> String {
format!("{} {}", self.first_name, self.last_name)
}
pub fn can_access_app(&self, id: &ClientID) -> bool {
match &self.authorized_clients {
None => true,

View File

@ -116,6 +116,7 @@ async fn main() -> std::io::Result<()> {
.to(|| async { HttpResponse::Found().append_header(("Location", "/settings")).finish() }))
.route("/admin/clients", web::get().to(admin_controller::clients_route))
.route("/admin/users", web::get().to(admin_controller::users_route))
.route("/admin/users", web::post().to(admin_controller::users_route))
.route("/admin/create_user", web::get().to(admin_controller::create_user))
// Admin API

View File

@ -1,3 +1,4 @@
pub mod err;
pub mod time;
pub mod network_utils;
pub mod network_utils;
pub mod string_utils;

11
src/utils/string_utils.rs Normal file
View File

@ -0,0 +1,11 @@
use rand::distributions::Alphanumeric;
use rand::Rng;
/// Generate a random string of a given size
pub fn rand_str(len: usize) -> String {
rand::thread_rng()
.sample_iter(&Alphanumeric)
.map(char::from)
.take(len)
.collect()
}

View File

@ -144,7 +144,7 @@
clientsSelectorEl.style.display = radioBtn.checked ? "block" : "none";
}
refreshDisplayAuthorizedClients();
document.querySelectorAll("input[name=granted_clients]").forEach(el=> {
document.querySelectorAll("input[name=grant_type]").forEach(el=> {
el.addEventListener("change", refreshDisplayAuthorizedClients)
})