Compare commits
3 Commits
a6acbde093
...
4e14e383b4
Author | SHA1 | Date | |
---|---|---|---|
4e14e383b4 | |||
c9ca23cd82 | |||
52888b3af7 |
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",
|
||||||
|
@ -21,4 +21,5 @@ uuid = { version = "0.8.2", features = ["v4"] }
|
|||||||
mime_guess = "2.0.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>,
|
||||||
}
|
}
|
||||||
@ -119,4 +126,18 @@ impl Handler<GetAllUsersRequest> for UsersActor {
|
|||||||
fn handle(&mut self, _msg: GetAllUsersRequest, _ctx: &mut Self::Context) -> Self::Result {
|
fn handle(&mut self, _msg: GetAllUsersRequest, _ctx: &mut Self::Context) -> Self::Result {
|
||||||
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
|
||||||
|
}
|
||||||
|
}))
|
||||||
|
}
|
||||||
}
|
}
|
@ -37,4 +37,7 @@ pub const LOGIN_ROUTE: &str = "/login";
|
|||||||
/// Bruteforce protection
|
/// Bruteforce protection
|
||||||
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())
|
||||||
|
});
|
||||||
|
|
||||||
|
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()
|
||||||
|
}
|
@ -1,7 +1,7 @@
|
|||||||
{% extends "base_settings_page.html" %}
|
{% extends "base_settings_page.html" %}
|
||||||
{% block content %}
|
{% block content %}
|
||||||
|
|
||||||
<form method="post" target="/admin/users">
|
<form method="post" action="/admin/users" id="edit_user_form">
|
||||||
<!-- User ID -->
|
<!-- User ID -->
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<label class="form-label mt-4" for="userID">User ID</label>
|
<label class="form-label mt-4" for="userID">User ID</label>
|
||||||
@ -11,7 +11,7 @@
|
|||||||
|
|
||||||
<!-- User name -->
|
<!-- User name -->
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<label class="form-label mt-4" for="username">User name</label>
|
<label class="form-label mt-4" for="username">Username</label>
|
||||||
<input class="form-control" id="username" type="text"
|
<input class="form-control" id="username" type="text"
|
||||||
name="username" value="{{ u.username }}" required/>
|
name="username" value="{{ u.username }}" required/>
|
||||||
<div class="valid-feedback">This username is valid</div>
|
<div class="valid-feedback">This username is valid</div>
|
||||||
@ -73,23 +73,24 @@
|
|||||||
<legend class="mt-4">Granted clients</legend>
|
<legend class="mt-4">Granted clients</legend>
|
||||||
<div class="form-check">
|
<div class="form-check">
|
||||||
<label class="form-check-label">
|
<label class="form-check-label">
|
||||||
<input type="radio" class="form-check-input" name="granted_clients"
|
<input type="radio" class="form-check-input" name="grant_type"
|
||||||
value="all_clients" {% if u.authorized_clients== None %} checked="" {% endif %}>
|
value="all_clients" {% if u.authorized_clients== None %} checked="" {% endif %}>
|
||||||
Grant all clients
|
Grant all clients
|
||||||
</label>
|
</label>
|
||||||
</div>
|
</div>
|
||||||
<div class="form-check">
|
<div class="form-check">
|
||||||
<label class="form-check-label">
|
<label class="form-check-label">
|
||||||
<input type="radio" class="form-check-input" name="granted_clients"
|
<input type="radio" class="form-check-input" name="grant_type"
|
||||||
value="custom_clients" {% if u.authorized_clients !=None %} checked="checked" {% endif %}>
|
value="custom_clients" {% if u.authorized_clients !=None %} checked="checked" {% endif %}>
|
||||||
Manually specify allowed clients
|
Manually specify allowed clients
|
||||||
</label>
|
</label>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div id="clients_target">
|
<div id="clients_target">
|
||||||
|
<input type="hidden" name="granted_clients" value=""/>
|
||||||
{% for c in clients %}
|
{% for c in clients %}
|
||||||
<div class="form-check">
|
<div class="form-check">
|
||||||
<input class="form-check-input" type="checkbox" class="authorize_client" data-id="{{ c.id.0 }}"
|
<input class="form-check-input authorize_client_checkbox" type="checkbox" data-id="{{ c.id.0 }}"
|
||||||
{% if u.can_access_app(c.id) %} checked="" {% endif %}>
|
{% if u.can_access_app(c.id) %} checked="" {% endif %}>
|
||||||
<label class="form-check-label" for="admin">
|
<label class="form-check-label" for="admin">
|
||||||
{{ c.name }}
|
{{ c.name }}
|
||||||
@ -98,6 +99,8 @@
|
|||||||
{% endfor %}
|
{% endfor %}
|
||||||
</div>
|
</div>
|
||||||
</fieldset>
|
</fieldset>
|
||||||
|
|
||||||
|
<input type="submit" class="btn btn-primary mt-4" value="{{ page_title }}">
|
||||||
</form>
|
</form>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
@ -137,16 +140,30 @@
|
|||||||
// Clients granted
|
// Clients granted
|
||||||
function refreshDisplayAuthorizedClients() {
|
function refreshDisplayAuthorizedClients() {
|
||||||
const clientsSelectorEl = document.getElementById("clients_target");
|
const clientsSelectorEl = document.getElementById("clients_target");
|
||||||
const radioBtn = document.querySelector("input[name=granted_clients][value=custom_clients]");
|
const radioBtn = document.querySelector("input[name=grant_type][value=custom_clients]");
|
||||||
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)
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|
||||||
|
// Handle submitted form
|
||||||
|
const form = document.getElementById("edit_user_form");
|
||||||
|
form.addEventListener("submit", (ev) => {
|
||||||
|
ev.preventDefault();
|
||||||
|
|
||||||
|
const authorized_clients = [...document.querySelectorAll(".authorize_client_checkbox")]
|
||||||
|
.filter(e => e.checked)
|
||||||
|
.map(e => e.getAttribute("data-id")).join(",")
|
||||||
|
|
||||||
|
document.querySelector("input[name=granted_clients]").value = authorized_clients;
|
||||||
|
|
||||||
|
form.submit();
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
</script>
|
</script>
|
||||||
{% endblock content %}
|
|
||||||
|
{% endblock content %}
|
Loading…
x
Reference in New Issue
Block a user