Can change user password
This commit is contained in:
parent
f21e40d804
commit
83e6871997
@ -1,7 +1,7 @@
|
|||||||
use actix::{Actor, Context, Handler, Message, MessageResult};
|
use actix::{Actor, Context, Handler, Message, MessageResult};
|
||||||
|
|
||||||
use crate::data::entity_manager::EntityManager;
|
use crate::data::entity_manager::EntityManager;
|
||||||
use crate::data::user::{User, UserID, verify_password};
|
use crate::data::user::{User, UserID};
|
||||||
|
|
||||||
#[derive(Debug)]
|
#[derive(Debug)]
|
||||||
pub enum LoginResult {
|
pub enum LoginResult {
|
||||||
@ -57,7 +57,7 @@ impl Handler<LoginRequest> for UsersActor {
|
|||||||
match self.manager.find_by_username_or_email(&msg.login) {
|
match self.manager.find_by_username_or_email(&msg.login) {
|
||||||
None => MessageResult(LoginResult::AccountNotFound),
|
None => MessageResult(LoginResult::AccountNotFound),
|
||||||
Some(user) => {
|
Some(user) => {
|
||||||
if !verify_password(msg.password, &user.password) {
|
if !user.verify_password(&msg.password) {
|
||||||
return MessageResult(LoginResult::InvalidPassword);
|
return MessageResult(LoginResult::InvalidPassword);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -3,9 +3,11 @@ use actix_identity::Identity;
|
|||||||
use actix_web::{HttpResponse, Responder, web};
|
use actix_web::{HttpResponse, Responder, web};
|
||||||
use askama::Template;
|
use askama::Template;
|
||||||
|
|
||||||
use crate::actors::users_actor;
|
use crate::actors::{bruteforce_actor, users_actor};
|
||||||
|
use crate::actors::bruteforce_actor::BruteForceActor;
|
||||||
use crate::actors::users_actor::UsersActor;
|
use crate::actors::users_actor::UsersActor;
|
||||||
use crate::constants::APP_NAME;
|
use crate::constants::{APP_NAME, MAX_FAILED_LOGIN_ATTEMPTS, MIN_PASS_LEN};
|
||||||
|
use crate::data::remote_ip::RemoteIP;
|
||||||
use crate::data::session_identity::SessionIdentity;
|
use crate::data::session_identity::SessionIdentity;
|
||||||
use crate::data::user::User;
|
use crate::data::user::User;
|
||||||
|
|
||||||
@ -21,11 +23,12 @@ struct BaseSettingsPage {
|
|||||||
}
|
}
|
||||||
|
|
||||||
impl BaseSettingsPage {
|
impl BaseSettingsPage {
|
||||||
async fn get(user: &User) -> BaseSettingsPage {
|
async fn get(page_title: &'static str, user: &User,
|
||||||
|
danger_message: Option<String>, success_message: Option<String>) -> BaseSettingsPage {
|
||||||
Self {
|
Self {
|
||||||
danger_message: None,
|
danger_message,
|
||||||
success_message: None,
|
success_message,
|
||||||
page_title: "Account details",
|
page_title,
|
||||||
app_name: APP_NAME,
|
app_name: APP_NAME,
|
||||||
is_admin: user.admin,
|
is_admin: user.admin,
|
||||||
user_name: user.username.to_string(),
|
user_name: user.username.to_string(),
|
||||||
@ -44,19 +47,87 @@ struct AccountDetailsPage {
|
|||||||
email: String,
|
email: String,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Template)]
|
||||||
|
#[template(path = "settings/change_password.html")]
|
||||||
|
struct ChangePasswordPage {
|
||||||
|
_parent: BaseSettingsPage,
|
||||||
|
min_pwd_len: usize,
|
||||||
|
}
|
||||||
|
|
||||||
/// Account details page
|
/// Account details page
|
||||||
pub async fn account_settings_details_route(id: Identity, user_actor: web::Data<Addr<UsersActor>>) -> impl Responder {
|
pub async fn account_settings_details_route(id: Identity, users: web::Data<Addr<UsersActor>>) -> impl Responder {
|
||||||
let user: User = user_actor.send(
|
let user: User = users.send(
|
||||||
users_actor::GetUserRequest(SessionIdentity(&id).user_id())
|
users_actor::GetUserRequest(SessionIdentity(&id).user_id())
|
||||||
).await.unwrap().0.unwrap();
|
).await.unwrap().0.unwrap();
|
||||||
|
|
||||||
HttpResponse::Ok()
|
HttpResponse::Ok()
|
||||||
.body(AccountDetailsPage {
|
.body(AccountDetailsPage {
|
||||||
_parent: BaseSettingsPage::get(&user).await,
|
_parent: BaseSettingsPage::get("Account details", &user, None, None).await,
|
||||||
user_id: user.uid,
|
user_id: user.uid,
|
||||||
first_name: user.first_name,
|
first_name: user.first_name,
|
||||||
last_name: user.last_last,
|
last_name: user.last_last,
|
||||||
username: user.username,
|
username: user.username,
|
||||||
email: user.email,
|
email: user.email,
|
||||||
}.render().unwrap())
|
}.render().unwrap())
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(serde::Deserialize)]
|
||||||
|
pub struct PassChangeRequest {
|
||||||
|
pub old_pass: String,
|
||||||
|
pub new_pass: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Change password route
|
||||||
|
pub async fn change_password_route(id: Identity,
|
||||||
|
users: web::Data<Addr<UsersActor>>,
|
||||||
|
req: Option<web::Form<PassChangeRequest>>,
|
||||||
|
bruteforce: web::Data<Addr<BruteForceActor>>,
|
||||||
|
remote_ip: RemoteIP) -> impl Responder {
|
||||||
|
let mut danger = None;
|
||||||
|
let mut success = None;
|
||||||
|
|
||||||
|
let user: User = users.send(
|
||||||
|
users_actor::GetUserRequest(SessionIdentity(&id).user_id())
|
||||||
|
).await.unwrap().0.unwrap();
|
||||||
|
|
||||||
|
let failed_attempts = bruteforce.send(bruteforce_actor::CountFailedAttempt { ip: remote_ip.into() }).await.unwrap();
|
||||||
|
if failed_attempts > MAX_FAILED_LOGIN_ATTEMPTS {
|
||||||
|
danger = Some("Too many invalid password attempts. Please try to change your password later.".to_string());
|
||||||
|
} else if let Some(req) = req {
|
||||||
|
|
||||||
|
// Invalid password
|
||||||
|
if !user.verify_password(&req.old_pass) {
|
||||||
|
danger = Some("Old password is invalid!".to_string());
|
||||||
|
bruteforce.send(bruteforce_actor::RecordFailedAttempt { ip: remote_ip.into() }).await.unwrap();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Password too short
|
||||||
|
else if req.new_pass.len() < MIN_PASS_LEN {
|
||||||
|
danger = Some("New password is too short!".to_string());
|
||||||
|
}
|
||||||
|
|
||||||
|
// Change password
|
||||||
|
else {
|
||||||
|
let res = users.send(
|
||||||
|
users_actor::ChangePasswordRequest {
|
||||||
|
user_id: user.uid.clone(),
|
||||||
|
new_password: req.new_pass.to_string(),
|
||||||
|
temporary: false,
|
||||||
|
}
|
||||||
|
).await.unwrap().0;
|
||||||
|
|
||||||
|
if !res {
|
||||||
|
danger = Some("An error occurred while trying to change your password!".to_string());
|
||||||
|
} else {
|
||||||
|
success = Some("Your password was successfully changed!".to_string());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
HttpResponse::Ok()
|
||||||
|
.body(ChangePasswordPage {
|
||||||
|
_parent: BaseSettingsPage::get("Change password", &user, danger, success).await,
|
||||||
|
min_pwd_len: MIN_PASS_LEN,
|
||||||
|
}.render().unwrap())
|
||||||
}
|
}
|
@ -3,3 +3,4 @@ pub mod entity_manager;
|
|||||||
pub mod service;
|
pub mod service;
|
||||||
pub mod session_identity;
|
pub mod session_identity;
|
||||||
pub mod user;
|
pub mod user;
|
||||||
|
pub mod remote_ip;
|
28
src/data/remote_ip.rs
Normal file
28
src/data/remote_ip.rs
Normal file
@ -0,0 +1,28 @@
|
|||||||
|
use std::net::IpAddr;
|
||||||
|
|
||||||
|
use actix_web::{Error, FromRequest, HttpRequest, web};
|
||||||
|
use actix_web::dev::Payload;
|
||||||
|
use futures_util::future::{Ready, ready};
|
||||||
|
|
||||||
|
use crate::data::app_config::AppConfig;
|
||||||
|
use crate::utils::network_utils::get_remote_ip;
|
||||||
|
|
||||||
|
#[derive(Copy, Clone, Debug, Eq, PartialEq)]
|
||||||
|
pub struct RemoteIP(pub IpAddr);
|
||||||
|
|
||||||
|
impl Into<IpAddr> for RemoteIP {
|
||||||
|
fn into(self) -> IpAddr {
|
||||||
|
self.0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl FromRequest for RemoteIP {
|
||||||
|
type Error = Error;
|
||||||
|
type Future = Ready<Result<RemoteIP, Error>>;
|
||||||
|
|
||||||
|
#[inline]
|
||||||
|
fn from_request(req: &HttpRequest, _: &mut Payload) -> Self::Future {
|
||||||
|
let config: &web::Data<AppConfig> = req.app_data().expect("AppData undefined!");
|
||||||
|
ready(Ok(RemoteIP(get_remote_ip(req, config.proxy_ip.as_deref()))))
|
||||||
|
}
|
||||||
|
}
|
@ -21,6 +21,12 @@ pub struct User {
|
|||||||
pub authorized_services: Option<Vec<ServiceID>>,
|
pub authorized_services: Option<Vec<ServiceID>>,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl User {
|
||||||
|
pub fn verify_password<P: AsRef<[u8]>>(&self, pass: P) -> bool {
|
||||||
|
verify_password(pass, &self.password)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
impl PartialEq for User {
|
impl PartialEq for User {
|
||||||
fn eq(&self, other: &Self) -> bool {
|
fn eq(&self, other: &Self) -> bool {
|
||||||
self.uid.eq(&other.uid)
|
self.uid.eq(&other.uid)
|
||||||
@ -81,8 +87,8 @@ impl EntityManager<User> {
|
|||||||
|
|
||||||
/// Update user information
|
/// Update user information
|
||||||
fn update_user<F>(&mut self, id: &UserID, update: F) -> bool
|
fn update_user<F>(&mut self, id: &UserID, update: F) -> bool
|
||||||
where
|
where
|
||||||
F: FnOnce(User) -> User,
|
F: FnOnce(User) -> User,
|
||||||
{
|
{
|
||||||
let user = match self.find_by_user_id(id) {
|
let user = match self.find_by_user_id(id) {
|
||||||
None => return false,
|
None => return false,
|
||||||
|
@ -106,6 +106,8 @@ async fn main() -> std::io::Result<()> {
|
|||||||
|
|
||||||
// Settings routes
|
// Settings routes
|
||||||
.route("/settings", web::get().to(settings_controller::account_settings_details_route))
|
.route("/settings", web::get().to(settings_controller::account_settings_details_route))
|
||||||
|
.route("/settings/change_password", web::get().to(settings_controller::change_password_route))
|
||||||
|
.route("/settings/change_password", web::post().to(settings_controller::change_password_route))
|
||||||
})
|
})
|
||||||
.bind(listen_address)?
|
.bind(listen_address)?
|
||||||
.run()
|
.run()
|
||||||
|
@ -1,69 +1,81 @@
|
|||||||
<!DOCTYPE html>
|
<!DOCTYPE html>
|
||||||
<html lang="en">
|
<html lang="en">
|
||||||
<head>
|
<head>
|
||||||
<meta charset="UTF-8">
|
<meta charset="UTF-8">
|
||||||
<title>{{ page_title }} - {{ app_name }}</title>
|
<title>{{ page_title }} - {{ app_name }}</title>
|
||||||
|
|
||||||
<!-- Bootstrap core CSS -->
|
<!-- Bootstrap core CSS -->
|
||||||
<link href="/assets/css/bootstrap.css" rel="stylesheet" crossorigin="anonymous"/>
|
<link href="/assets/css/bootstrap.css" rel="stylesheet" crossorigin="anonymous"/>
|
||||||
|
|
||||||
<link rel="stylesheet" href="/assets/css/base_settings_page.css">
|
<link rel="stylesheet" href="/assets/css/base_settings_page.css">
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<div class="d-flex flex-column flex-shrink-0 p-3 bg-light" style="width: 280px;">
|
<div class="d-flex flex-column flex-shrink-0 p-3 bg-light" style="width: 280px;">
|
||||||
<a href="/" class="d-flex align-items-center mb-3 mb-md-0 me-md-auto link-dark text-decoration-none">
|
<a href="/" class="d-flex align-items-center mb-3 mb-md-0 me-md-auto link-dark text-decoration-none">
|
||||||
<span class="fs-4">{{ app_name }}</span>
|
<span class="fs-4">{{ app_name }}</span>
|
||||||
</a>
|
</a>
|
||||||
<hr>
|
<hr>
|
||||||
<ul class="nav nav-pills flex-column mb-auto">
|
<ul class="nav nav-pills flex-column mb-auto">
|
||||||
<li class="nav-item">
|
<li class="nav-item">
|
||||||
<a href="/settings" class="nav-link active" aria-current="page">
|
<a href="/settings" class="nav-link link-dark">
|
||||||
Account details
|
Account details
|
||||||
</a>
|
</a>
|
||||||
</li>
|
</li>
|
||||||
<li>
|
<li>
|
||||||
<a href="/settings/change_password" class="nav-link link-dark">
|
<a href="/settings/change_password" class="nav-link link-dark">
|
||||||
Change password
|
Change password
|
||||||
</a>
|
</a>
|
||||||
</li>
|
</li>
|
||||||
|
|
||||||
<hr />
|
<hr/>
|
||||||
{% if is_admin %}
|
{% if is_admin %}
|
||||||
<li>
|
<li>
|
||||||
<a href="/admin/apps" class="nav-link link-dark">
|
<a href="/admin/apps" class="nav-link link-dark">
|
||||||
Applications
|
Applications
|
||||||
</a>
|
</a>
|
||||||
</li>
|
</li>
|
||||||
<li>
|
<li>
|
||||||
<a href="/admin/users" class="nav-link link-dark">
|
<a href="/admin/users" class="nav-link link-dark">
|
||||||
Users
|
Users
|
||||||
</a>
|
</a>
|
||||||
</li>
|
</li>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</ul>
|
</ul>
|
||||||
<hr>
|
<hr>
|
||||||
<div class="dropdown">
|
<div class="dropdown">
|
||||||
<a href="#" class="d-flex align-items-center link-dark text-decoration-none dropdown-toggle" id="dropdownUser" data-bs-toggle="dropdown" aria-expanded="false">
|
<a href="#" class="d-flex align-items-center link-dark text-decoration-none dropdown-toggle" id="dropdownUser"
|
||||||
<img src="/assets/img/account.png" alt="" width="32" height="32" class="rounded-circle me-2">
|
data-bs-toggle="dropdown" aria-expanded="false">
|
||||||
<strong>{{ user_name }}</strong>
|
<img src="/assets/img/account.png" alt="" width="32" height="32" class="rounded-circle me-2">
|
||||||
</a>
|
<strong>{{ user_name }}</strong>
|
||||||
<ul class="dropdown-menu text-small shadow" aria-labelledby="dropdownUser">
|
</a>
|
||||||
<li><a class="dropdown-item" href="/logout">Sign out</a></li>
|
<ul class="dropdown-menu text-small shadow" aria-labelledby="dropdownUser">
|
||||||
</ul>
|
<li><a class="dropdown-item" href="/logout">Sign out</a></li>
|
||||||
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="page_body" style="flex: 1">
|
<div class="page_body" style="flex: 1">
|
||||||
{% if let Some(msg) = danger_message %}<div class="alert alert-danger">{{ msg }}</div>{% endif %}
|
{% if let Some(msg) = danger_message %}
|
||||||
{% if let Some(msg) = success_message %}<div class="alert alert-success">{{ msg }}</div>{% endif %}
|
<div class="alert alert-danger">{{ msg }}</div>
|
||||||
|
{% endif %}
|
||||||
|
{% if let Some(msg) = success_message %}
|
||||||
|
<div class="alert alert-success">{{ msg }}</div>
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
<h2 class="bd-title mt-0" style="margin-bottom: 40px;">{{ page_title }}</h2>
|
<h2 class="bd-title mt-0" style="margin-bottom: 40px;">{{ page_title }}</h2>
|
||||||
|
|
||||||
{% block content %}
|
{% block content %}
|
||||||
TO_REPLACE
|
TO_REPLACE
|
||||||
{% endblock content %}
|
{% endblock content %}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<script src="/assets/js/bootstrap.bundle.min.js"></script>
|
<script src="/assets/js/bootstrap.bundle.min.js"></script>
|
||||||
|
<script>
|
||||||
|
document.querySelectorAll(".nav-link").forEach(el => {
|
||||||
|
if(el.href === location.href) el.classList.add("active");
|
||||||
|
else el.classList.remove("active")
|
||||||
|
})
|
||||||
|
|
||||||
|
</script>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
57
templates/settings/change_password.html
Normal file
57
templates/settings/change_password.html
Normal file
@ -0,0 +1,57 @@
|
|||||||
|
{% extends "base_settings_page.html" %}
|
||||||
|
{% block content %}
|
||||||
|
<form id="change_password_form" action="/settings/change_password" method="post">
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="currPassword" class="form-label mt-4">Current password</label>
|
||||||
|
<input type="password" name="old_pass" class="form-control"
|
||||||
|
id="currPassword" placeholder="Your current password" required />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div> </div>
|
||||||
|
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="newPassword" class="form-label mt-4">New password</label>
|
||||||
|
<input type="password" name="new_pass" class="form-control" id="newPassword"
|
||||||
|
placeholder="New password" minlength="{{ min_pwd_len }}" />
|
||||||
|
<div class="invalid-feedback" id="errNewPass"></div>
|
||||||
|
<small class="form-text text-muted">Please choose a password of at least {{ min_pwd_len }} characters.</small>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="confirmNewPassword" class="form-label mt-4">Confirm new password</label>
|
||||||
|
<input type="password" class="form-control" id="confirmNewPassword" placeholder="Confirm new password">
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div> </div>
|
||||||
|
<div> </div>
|
||||||
|
<div> </div>
|
||||||
|
|
||||||
|
<button type="submit" class="btn btn-primary">Change password</button>
|
||||||
|
</form>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
const form = document.getElementById("change_password_form");
|
||||||
|
const errPass1 = document.getElementById("errNewPass");
|
||||||
|
form.addEventListener("submit", (e) => {
|
||||||
|
e.preventDefault();
|
||||||
|
|
||||||
|
const pass1 = document.getElementById("newPassword");
|
||||||
|
const pass2 = document.getElementById("confirmNewPassword");
|
||||||
|
|
||||||
|
errPass1.innerHTML = "";
|
||||||
|
pass1.classList.remove("is-invalid");
|
||||||
|
|
||||||
|
if (pass1.value.length < {{ min_pwd_len }}) {
|
||||||
|
errPass1.innerHTML = "Your password must have at least {{ min_pwd_len }} characters!";
|
||||||
|
pass1.classList.add("is-invalid");
|
||||||
|
}
|
||||||
|
else if (pass1.value !== pass2.value) {
|
||||||
|
errPass1.innerHTML = "The password and its confirmation are not the same !";
|
||||||
|
pass1.classList.add("is-invalid");
|
||||||
|
}
|
||||||
|
else
|
||||||
|
form.submit();
|
||||||
|
})
|
||||||
|
</script>
|
||||||
|
|
||||||
|
{% endblock content %}
|
Loading…
Reference in New Issue
Block a user