Can create user accounts
This commit is contained in:
		
							
								
								
									
										1
									
								
								Cargo.lock
									
									
									
										generated
									
									
									
								
							
							
						
						
									
										1
									
								
								Cargo.lock
									
									
									
										generated
									
									
									
								
							@@ -393,6 +393,7 @@ dependencies = [
 | 
				
			|||||||
 "include_dir",
 | 
					 "include_dir",
 | 
				
			||||||
 "log",
 | 
					 "log",
 | 
				
			||||||
 "mime_guess",
 | 
					 "mime_guess",
 | 
				
			||||||
 | 
					 "rand",
 | 
				
			||||||
 "serde",
 | 
					 "serde",
 | 
				
			||||||
 "serde_json",
 | 
					 "serde_json",
 | 
				
			||||||
 "serde_yaml",
 | 
					 "serde_yaml",
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -22,3 +22,4 @@ mime_guess = "2.0.4"
 | 
				
			|||||||
askama = "0.11.1"
 | 
					askama = "0.11.1"
 | 
				
			||||||
futures-util = "0.3.21"
 | 
					futures-util = "0.3.21"
 | 
				
			||||||
urlencoding = "2.1.0"
 | 
					urlencoding = "2.1.0"
 | 
				
			||||||
 | 
					rand = "0.8.5"
 | 
				
			||||||
@@ -18,9 +18,6 @@ pub struct LoginRequest {
 | 
				
			|||||||
    pub password: String,
 | 
					    pub password: String,
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
#[derive(Debug)]
 | 
					 | 
				
			||||||
pub struct ChangePasswordResult(pub bool);
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
#[derive(Message)]
 | 
					#[derive(Message)]
 | 
				
			||||||
#[rtype(GetUserResult)]
 | 
					#[rtype(GetUserResult)]
 | 
				
			||||||
pub struct GetUserRequest(pub UserID);
 | 
					pub struct GetUserRequest(pub UserID);
 | 
				
			||||||
@@ -50,6 +47,16 @@ pub struct ChangePasswordRequest {
 | 
				
			|||||||
    pub temporary: bool,
 | 
					    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 {
 | 
					pub struct UsersActor {
 | 
				
			||||||
    manager: EntityManager<User>,
 | 
					    manager: EntityManager<User>,
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
@@ -120,3 +127,17 @@ impl Handler<GetAllUsersRequest> for UsersActor {
 | 
				
			|||||||
        MessageResult(GetAllUsersResult(self.manager.cloned()))
 | 
					        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
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					        }))
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
@@ -38,3 +38,6 @@ pub const LOGIN_ROUTE: &str = "/login";
 | 
				
			|||||||
pub const KEEP_FAILED_LOGIN_ATTEMPTS_FOR: u64 = 3600;
 | 
					pub const KEEP_FAILED_LOGIN_ATTEMPTS_FOR: u64 = 3600;
 | 
				
			||||||
pub const MAX_FAILED_LOGIN_ATTEMPTS: usize = 15;
 | 
					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;
 | 
				
			||||||
@@ -6,10 +6,12 @@ use askama::Template;
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
use crate::actors::users_actor;
 | 
					use crate::actors::users_actor;
 | 
				
			||||||
use crate::actors::users_actor::UsersActor;
 | 
					use crate::actors::users_actor::UsersActor;
 | 
				
			||||||
 | 
					use crate::constants::TEMPORARY_PASSWORDS_LEN;
 | 
				
			||||||
use crate::controllers::settings_controller::BaseSettingsPage;
 | 
					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::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)]
 | 
					#[derive(Template)]
 | 
				
			||||||
#[template(path = "settings/clients_list.html")]
 | 
					#[template(path = "settings/clients_list.html")]
 | 
				
			||||||
@@ -46,15 +48,85 @@ pub async fn clients_route(user: CurrentUser, clients: web::Data<ClientManager>)
 | 
				
			|||||||
    }.render().unwrap())
 | 
					    }.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;
 | 
					    let users = users.send(users_actor::GetAllUsersRequest).await.unwrap().0;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    HttpResponse::Ok().body(UsersListTemplate {
 | 
					    HttpResponse::Ok().body(UsersListTemplate {
 | 
				
			||||||
        _parent: BaseSettingsPage::get(
 | 
					        _parent: BaseSettingsPage::get(
 | 
				
			||||||
            "Users list",
 | 
					            "Users list",
 | 
				
			||||||
            &user,
 | 
					            &user,
 | 
				
			||||||
            None,
 | 
					            danger,
 | 
				
			||||||
            None,
 | 
					            success,
 | 
				
			||||||
        ),
 | 
					        ),
 | 
				
			||||||
        users,
 | 
					        users,
 | 
				
			||||||
    }.render().unwrap())
 | 
					    }.render().unwrap())
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -91,4 +91,21 @@ impl<E> EntityManager<E>
 | 
				
			|||||||
    pub fn cloned(&self) -> Vec<E> {
 | 
					    pub fn cloned(&self) -> Vec<E> {
 | 
				
			||||||
        self.list.clone()
 | 
					        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()
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -22,6 +22,10 @@ pub struct User {
 | 
				
			|||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
impl 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 {
 | 
					    pub fn can_access_app(&self, id: &ClientID) -> bool {
 | 
				
			||||||
        match &self.authorized_clients {
 | 
					        match &self.authorized_clients {
 | 
				
			||||||
            None => true,
 | 
					            None => true,
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -116,6 +116,7 @@ async fn main() -> std::io::Result<()> {
 | 
				
			|||||||
                .to(|| async { HttpResponse::Found().append_header(("Location", "/settings")).finish() }))
 | 
					                .to(|| async { HttpResponse::Found().append_header(("Location", "/settings")).finish() }))
 | 
				
			||||||
            .route("/admin/clients", web::get().to(admin_controller::clients_route))
 | 
					            .route("/admin/clients", web::get().to(admin_controller::clients_route))
 | 
				
			||||||
            .route("/admin/users", web::get().to(admin_controller::users_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))
 | 
					            .route("/admin/create_user", web::get().to(admin_controller::create_user))
 | 
				
			||||||
 | 
					
 | 
				
			||||||
            // Admin API
 | 
					            // Admin API
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -1,3 +1,4 @@
 | 
				
			|||||||
pub mod err;
 | 
					pub mod err;
 | 
				
			||||||
pub mod time;
 | 
					pub mod time;
 | 
				
			||||||
pub mod network_utils;
 | 
					pub mod network_utils;
 | 
				
			||||||
 | 
					pub mod string_utils;
 | 
				
			||||||
							
								
								
									
										11
									
								
								src/utils/string_utils.rs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										11
									
								
								src/utils/string_utils.rs
									
									
									
									
									
										Normal 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()
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
@@ -144,7 +144,7 @@
 | 
				
			|||||||
        clientsSelectorEl.style.display = radioBtn.checked ? "block" : "none";
 | 
					        clientsSelectorEl.style.display = radioBtn.checked ? "block" : "none";
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
    refreshDisplayAuthorizedClients();
 | 
					    refreshDisplayAuthorizedClients();
 | 
				
			||||||
    document.querySelectorAll("input[name=granted_clients]").forEach(el=> {
 | 
					    document.querySelectorAll("input[name=grant_type]").forEach(el=> {
 | 
				
			||||||
        el.addEventListener("change", refreshDisplayAuthorizedClients)
 | 
					        el.addEventListener("change", refreshDisplayAuthorizedClients)
 | 
				
			||||||
    })
 | 
					    })
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 
 | 
				
			|||||||
		Reference in New Issue
	
	Block a user