feat(WIP): registration of users
This commit is contained in:
parent
ae2cff0c3a
commit
2374f78a07
1
Cargo.lock
generated
1
Cargo.lock
generated
@ -791,6 +791,7 @@ dependencies = [
|
|||||||
"serde_json",
|
"serde_json",
|
||||||
"sqlx",
|
"sqlx",
|
||||||
"static-files",
|
"static-files",
|
||||||
|
"thiserror",
|
||||||
"zxcvbn",
|
"zxcvbn",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
@ -29,6 +29,7 @@ quick-xml = { version = "0.31.0", features = ["serde", "serialize"] }
|
|||||||
actix-web-static-files = "4.0"
|
actix-web-static-files = "4.0"
|
||||||
static-files = "0.2.1"
|
static-files = "0.2.1"
|
||||||
zxcvbn = "3.1.0"
|
zxcvbn = "3.1.0"
|
||||||
|
thiserror = "1.0.63"
|
||||||
|
|
||||||
[build-dependencies]
|
[build-dependencies]
|
||||||
built = "0.7.4"
|
built = "0.7.4"
|
||||||
|
@ -21,8 +21,8 @@ CREATE TABLE user_
|
|||||||
id SERIAL PRIMARY KEY,
|
id SERIAL PRIMARY KEY,
|
||||||
name TEXT NOT NULL,
|
name TEXT NOT NULL,
|
||||||
email TEXT NOT NULL,
|
email TEXT NOT NULL,
|
||||||
password TEXT NOT NULL,
|
password TEXT ,
|
||||||
salt TEXT NOT NULL,
|
salt TEXT ,
|
||||||
role role NOT NULL,
|
role role NOT NULL,
|
||||||
function function NOT NULL,
|
function function NOT NULL,
|
||||||
areaId INTEGER NOT NULL REFERENCES area (id),
|
areaId INTEGER NOT NULL REFERENCES area (id),
|
||||||
|
@ -31,7 +31,6 @@ pub fn init(cfg: &mut ServiceConfig) {
|
|||||||
cfg.service(user::post_new::post_new);
|
cfg.service(user::post_new::post_new);
|
||||||
cfg.service(user::get_edit::get_edit);
|
cfg.service(user::get_edit::get_edit);
|
||||||
cfg.service(user::post_edit::post_edit);
|
cfg.service(user::post_edit::post_edit);
|
||||||
cfg.service(user::patch::patch);
|
|
||||||
cfg.service(user::delete::delete);
|
cfg.service(user::delete::delete);
|
||||||
cfg.service(user::get_logout::get);
|
cfg.service(user::get_logout::get);
|
||||||
cfg.service(user::get_login::get);
|
cfg.service(user::get_login::get);
|
||||||
|
42
src/endpoints/user/get_register.rs
Normal file
42
src/endpoints/user/get_register.rs
Normal file
@ -0,0 +1,42 @@
|
|||||||
|
use actix_identity::Identity;
|
||||||
|
use actix_web::{get, http::header::LOCATION, web, HttpResponse, Responder};
|
||||||
|
use askama_actix::TemplateToResponse;
|
||||||
|
use serde::Deserialize;
|
||||||
|
use sqlx::PgPool;
|
||||||
|
|
||||||
|
use crate::models::Registration;
|
||||||
|
|
||||||
|
use super::ResetPasswordTemplate;
|
||||||
|
|
||||||
|
#[derive(Deserialize)]
|
||||||
|
struct TokenQuery {
|
||||||
|
token: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[get("/register")]
|
||||||
|
pub async fn get(
|
||||||
|
user: Option<Identity>,
|
||||||
|
pool: web::Data<PgPool>,
|
||||||
|
query: web::Query<TokenQuery>,
|
||||||
|
) -> impl Responder {
|
||||||
|
if user.is_some() {
|
||||||
|
return HttpResponse::Found()
|
||||||
|
.insert_header((LOCATION, "/"))
|
||||||
|
.finish();
|
||||||
|
}
|
||||||
|
|
||||||
|
if let Ok(_) = Registration::does_token_exist(pool.get_ref(), &query.token).await {
|
||||||
|
let template = ResetPasswordTemplate {
|
||||||
|
token: &query.token,
|
||||||
|
title: "Brass - Registrierung",
|
||||||
|
endpoint: "/register",
|
||||||
|
new_password_label: "Passwort:",
|
||||||
|
retype_label: "Passwort wiederholen:",
|
||||||
|
submit_button_label: "Registrieren",
|
||||||
|
};
|
||||||
|
|
||||||
|
return template.to_response();
|
||||||
|
}
|
||||||
|
|
||||||
|
HttpResponse::NotFound().finish()
|
||||||
|
}
|
@ -3,33 +3,41 @@ use actix_web::{get, http::header::LOCATION, web, HttpResponse, Responder};
|
|||||||
use askama::Template;
|
use askama::Template;
|
||||||
use askama_actix::TemplateToResponse;
|
use askama_actix::TemplateToResponse;
|
||||||
use serde::Deserialize;
|
use serde::Deserialize;
|
||||||
|
use sqlx::PgPool;
|
||||||
|
|
||||||
|
use crate::models::PasswordReset;
|
||||||
|
|
||||||
|
use super::ResetPasswordTemplate;
|
||||||
|
|
||||||
#[derive(Template)]
|
#[derive(Template)]
|
||||||
#[template(path = "user/forgot_password.html")]
|
#[template(path = "user/forgot_password.html")]
|
||||||
struct ForgotPasswordTemplate {}
|
struct ForgotPasswordTemplate {}
|
||||||
|
|
||||||
#[derive(Template)]
|
|
||||||
#[template(path = "user/reset_password.html")]
|
|
||||||
struct ResetPasswordTemplate {
|
|
||||||
token: String
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Deserialize)]
|
#[derive(Deserialize)]
|
||||||
struct TokenQuery {
|
struct TokenQuery {
|
||||||
token: Option<String>
|
token: Option<String>,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[get("/reset-password")]
|
#[get("/reset-password")]
|
||||||
pub async fn get(user: Option<Identity>, query: web::Query<TokenQuery>) -> impl Responder {
|
pub async fn get(
|
||||||
|
user: Option<Identity>,
|
||||||
|
pool: web::Data<PgPool>,
|
||||||
|
query: web::Query<TokenQuery>,
|
||||||
|
) -> impl Responder {
|
||||||
if let Some(_) = user {
|
if let Some(_) = user {
|
||||||
return HttpResponse::Found()
|
return HttpResponse::Found()
|
||||||
.insert_header((LOCATION, "/"))
|
.insert_header((LOCATION, "/"))
|
||||||
.finish();
|
.finish();
|
||||||
} else if let Some(token) = &query.token {
|
} else if let Some(token) = &query.token {
|
||||||
let token_exists = true;
|
if let Ok(_) = PasswordReset::does_token_exist(pool.get_ref(), token).await {
|
||||||
|
let template = ResetPasswordTemplate {
|
||||||
if token_exists {
|
token,
|
||||||
let template = ResetPasswordTemplate { token: token.to_string() };
|
title: "Brass - Passwort zurücksetzen",
|
||||||
|
endpoint: "/reset-password",
|
||||||
|
new_password_label: "/reset-password",
|
||||||
|
retype_label: "neues Passwort wiederholen:",
|
||||||
|
submit_button_label: "Passwort zurücksetzen",
|
||||||
|
};
|
||||||
|
|
||||||
return template.to_response();
|
return template.to_response();
|
||||||
}
|
}
|
||||||
|
@ -11,13 +11,14 @@ pub mod get_new;
|
|||||||
pub mod get_overview;
|
pub mod get_overview;
|
||||||
pub mod get_profile;
|
pub mod get_profile;
|
||||||
pub mod get_reset;
|
pub mod get_reset;
|
||||||
pub mod patch;
|
|
||||||
pub mod post_changepassword;
|
pub mod post_changepassword;
|
||||||
pub mod post_edit;
|
pub mod post_edit;
|
||||||
pub mod post_login;
|
pub mod post_login;
|
||||||
pub mod post_new;
|
pub mod post_new;
|
||||||
pub mod post_reset;
|
pub mod post_reset;
|
||||||
pub mod post_toggle;
|
pub mod post_toggle;
|
||||||
|
pub mod get_register;
|
||||||
|
pub mod post_register;
|
||||||
|
|
||||||
#[derive(Template)]
|
#[derive(Template)]
|
||||||
#[template(path = "user/new_or_edit.html")]
|
#[template(path = "user/new_or_edit.html")]
|
||||||
@ -31,3 +32,14 @@ pub struct NewOrEditUserTemplate {
|
|||||||
function: Option<u8>,
|
function: Option<u8>,
|
||||||
area_id: Option<i32>,
|
area_id: Option<i32>,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Template)]
|
||||||
|
#[template(path = "user/change_password.html")]
|
||||||
|
struct ResetPasswordTemplate<'a> {
|
||||||
|
token: &'a str,
|
||||||
|
title: &'a str,
|
||||||
|
endpoint: &'a str,
|
||||||
|
new_password_label: &'a str,
|
||||||
|
retype_label: &'a str,
|
||||||
|
submit_button_label: &'a str
|
||||||
|
}
|
||||||
|
@ -1,88 +0,0 @@
|
|||||||
use actix_web::{web, HttpResponse, Responder};
|
|
||||||
use serde::Deserialize;
|
|
||||||
use serde_json::value::Value;
|
|
||||||
use sqlx::PgPool;
|
|
||||||
|
|
||||||
use crate::{
|
|
||||||
endpoints::IdPath,
|
|
||||||
models::{Role, User},
|
|
||||||
};
|
|
||||||
|
|
||||||
#[derive(Deserialize)]
|
|
||||||
pub struct JsonPatchDoc {
|
|
||||||
op: String,
|
|
||||||
path: String,
|
|
||||||
value: Value,
|
|
||||||
}
|
|
||||||
|
|
||||||
// TODO: deprecated route
|
|
||||||
#[actix_web::patch("/users/edit/{id}")]
|
|
||||||
pub async fn patch(
|
|
||||||
user: web::ReqData<User>,
|
|
||||||
pool: web::Data<PgPool>,
|
|
||||||
path: web::Path<IdPath>,
|
|
||||||
patch_docs: web::Json<Vec<JsonPatchDoc>>,
|
|
||||||
) -> impl Responder {
|
|
||||||
|
|
||||||
let is_superuser = user.role != Role::AreaManager && user.role != Role::Admin;
|
|
||||||
|
|
||||||
if let Ok(user_in_db) = User::read_by_id(pool.get_ref(), path.id).await {
|
|
||||||
if user.role == Role::AreaManager && user.area_id != user_in_db.area_id {
|
|
||||||
return HttpResponse::Unauthorized().finish();
|
|
||||||
}
|
|
||||||
|
|
||||||
let mut changed = false;
|
|
||||||
|
|
||||||
let mut locked: Option<bool> = None;
|
|
||||||
let mut receive_notifications: Option<bool> = None;
|
|
||||||
|
|
||||||
for doc in patch_docs.iter() {
|
|
||||||
if doc.op.as_str() != "replace" {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
match doc.path.as_str() {
|
|
||||||
"/locked" => {
|
|
||||||
if !is_superuser {
|
|
||||||
return HttpResponse::Unauthorized().finish();
|
|
||||||
}
|
|
||||||
changed = true;
|
|
||||||
if let Value::Bool(b) = doc.value {
|
|
||||||
locked = Some(b);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
"/receiveNotifications" => {
|
|
||||||
changed = true;
|
|
||||||
if let Value::Bool(b) = doc.value {
|
|
||||||
receive_notifications = Some(b)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
_ => return HttpResponse::BadRequest().body("Other PATCH paths are not supported!")
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
if changed {
|
|
||||||
if let Ok(_) = User::update(
|
|
||||||
pool.get_ref(),
|
|
||||||
path.id,
|
|
||||||
None,
|
|
||||||
None,
|
|
||||||
None,
|
|
||||||
None,
|
|
||||||
None,
|
|
||||||
None,
|
|
||||||
None,
|
|
||||||
receive_notifications,
|
|
||||||
locked,
|
|
||||||
)
|
|
||||||
.await
|
|
||||||
{
|
|
||||||
return HttpResponse::Ok().body("");
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
return HttpResponse::Ok().body("");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
HttpResponse::BadRequest().body("Fehler bei User PATCH")
|
|
||||||
}
|
|
@ -17,9 +17,13 @@ async fn post(
|
|||||||
form: web::Form<ChangePasswordForm>,
|
form: web::Form<ChangePasswordForm>,
|
||||||
pool: web::Data<PgPool>,
|
pool: web::Data<PgPool>,
|
||||||
) -> impl Responder {
|
) -> impl Responder {
|
||||||
if user.password
|
if user.password.as_ref().is_some_and(|p|
|
||||||
== utils::hash_plain_password_with_salt(&form.currentpassword, &user.salt).unwrap()
|
p == &utils::hash_plain_password_with_salt(
|
||||||
{
|
&form.currentpassword,
|
||||||
|
user.salt.as_ref().unwrap(),
|
||||||
|
)
|
||||||
|
.unwrap()
|
||||||
|
) {
|
||||||
if form.password != form.passwordretyped {
|
if form.password != form.passwordretyped {
|
||||||
return HttpResponse::BadRequest().body("Passwörter stimmen nicht überein!");
|
return HttpResponse::BadRequest().body("Passwörter stimmen nicht überein!");
|
||||||
}
|
}
|
||||||
|
@ -18,8 +18,10 @@ async fn post(
|
|||||||
pool: web::Data<PgPool>,
|
pool: web::Data<PgPool>,
|
||||||
) -> impl Responder {
|
) -> impl Responder {
|
||||||
if let Ok(user) = User::read_for_login(pool.get_ref(), &form.email).await {
|
if let Ok(user) = User::read_for_login(pool.get_ref(), &form.email).await {
|
||||||
let hash = hash_plain_password_with_salt(&form.password, &user.salt).unwrap();
|
let salt = user.salt.unwrap();
|
||||||
if hash == user.password {
|
|
||||||
|
let hash = hash_plain_password_with_salt(&form.password, &salt).unwrap();
|
||||||
|
if hash == user.password.unwrap() {
|
||||||
Identity::login(&request.extensions(), user.id.to_string()).unwrap();
|
Identity::login(&request.extensions(), user.id.to_string()).unwrap();
|
||||||
|
|
||||||
User::update_login_timestamp(pool.get_ref(), user.id)
|
User::update_login_timestamp(pool.get_ref(), user.id)
|
||||||
|
@ -1,46 +1,58 @@
|
|||||||
use actix_identity::Identity;
|
|
||||||
use actix_web::{http::header::LOCATION, web, HttpResponse, Responder};
|
use actix_web::{http::header::LOCATION, web, HttpResponse, Responder};
|
||||||
|
use lettre::{SmtpTransport, Transport};
|
||||||
use serde::Deserialize;
|
use serde::Deserialize;
|
||||||
use sqlx::PgPool;
|
use sqlx::PgPool;
|
||||||
|
|
||||||
use crate::{auth::utils::generate_salt_and_hash_plain_password, models::{Function, Role, User}};
|
use crate::{
|
||||||
|
models::{Function, Registration, Role, User},
|
||||||
|
utils::{email, ApplicationError},
|
||||||
|
};
|
||||||
|
|
||||||
#[derive(Deserialize)]
|
#[derive(Deserialize)]
|
||||||
pub struct NewUserForm {
|
pub struct NewUserForm {
|
||||||
email: String,
|
email: String,
|
||||||
name: String,
|
name: String,
|
||||||
password: String,
|
|
||||||
role: u8,
|
role: u8,
|
||||||
function: u8,
|
function: u8,
|
||||||
area: Option<i32>
|
area: Option<i32>,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[actix_web::post("/users/new")]
|
#[actix_web::post("/users/new")]
|
||||||
pub async fn post_new(user: Identity, pool: web::Data<PgPool>, form: web::Form<NewUserForm>) -> impl Responder {
|
pub async fn post_new(
|
||||||
let current_user = User::read_by_id(pool.get_ref(), user.id().unwrap().parse().unwrap()).await.unwrap();
|
user: web::ReqData<User>,
|
||||||
|
pool: web::Data<PgPool>,
|
||||||
if current_user.role != Role::AreaManager && current_user.role != Role::Admin {
|
form: web::Form<NewUserForm>,
|
||||||
return HttpResponse::Unauthorized().finish();
|
mailer: web::ReqData<SmtpTransport>,
|
||||||
|
) -> Result<impl Responder, ApplicationError> {
|
||||||
|
if user.role != Role::AreaManager && user.role != Role::Admin {
|
||||||
|
return Err(ApplicationError::Unauthorized);
|
||||||
}
|
}
|
||||||
|
|
||||||
let mut area_id = current_user.area_id;
|
let mut area_id = user.area_id;
|
||||||
|
|
||||||
if current_user.role == Role::Admin {
|
if user.role == Role::Admin && form.area.is_some() {
|
||||||
if let Some(id) = form.area {
|
area_id = form.area.unwrap();
|
||||||
area_id = id;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if let Ok((hash, salt)) = generate_salt_and_hash_plain_password(&form.password) {
|
let role = Role::try_from(form.role)?;
|
||||||
if let Ok(role) = Role::try_from(form.role) {
|
let function = Function::try_from(form.function)?;
|
||||||
if let Ok(function) = Function::try_from(form.function) {
|
|
||||||
match User::create(pool.get_ref(), &form.name, &form.email, &hash, &salt, role, function, area_id).await {
|
|
||||||
Ok(_) => return HttpResponse::Found().insert_header((LOCATION, "/users")).finish(),
|
|
||||||
Err(err) => println!("{}", err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return HttpResponse::BadRequest().body("Fehler beim Erstellen des Nutzers");
|
let id = User::create(
|
||||||
|
pool.get_ref(),
|
||||||
|
&form.name,
|
||||||
|
&form.email,
|
||||||
|
role,
|
||||||
|
function,
|
||||||
|
area_id,
|
||||||
|
)
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
let registration = Registration::insert_new_for_user(pool.get_ref(), id).await?;
|
||||||
|
let message = email::build_registration_message(&user, ®istration.token);
|
||||||
|
|
||||||
|
mailer.send(&message).unwrap();
|
||||||
|
|
||||||
|
Ok(HttpResponse::Found()
|
||||||
|
.insert_header((LOCATION, "/users"))
|
||||||
|
.finish())
|
||||||
}
|
}
|
||||||
|
85
src/endpoints/user/post_register.rs
Normal file
85
src/endpoints/user/post_register.rs
Normal file
@ -0,0 +1,85 @@
|
|||||||
|
use actix_web::{post, web, HttpResponse, Responder};
|
||||||
|
use serde::Deserialize;
|
||||||
|
use sqlx::PgPool;
|
||||||
|
use zxcvbn::{zxcvbn, Score};
|
||||||
|
|
||||||
|
use crate::{
|
||||||
|
auth,
|
||||||
|
models::{Registration, User},
|
||||||
|
utils::{password_help, ApplicationError},
|
||||||
|
};
|
||||||
|
|
||||||
|
#[derive(Deserialize)]
|
||||||
|
struct RegisterForm {
|
||||||
|
token: String,
|
||||||
|
password: String,
|
||||||
|
passwordretyped: String,
|
||||||
|
dry: Option<bool>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[post("/register")]
|
||||||
|
async fn post(
|
||||||
|
form: web::Form<RegisterForm>,
|
||||||
|
pool: web::Data<PgPool>,
|
||||||
|
) -> Result<impl Responder, ApplicationError> {
|
||||||
|
let is_dry = form.dry.unwrap_or(false);
|
||||||
|
let token = Registration::does_token_exist(pool.get_ref(), &form.token).await?;
|
||||||
|
|
||||||
|
if form.password.chars().count() > 256 {
|
||||||
|
if is_dry {
|
||||||
|
return Ok(HttpResponse::BadRequest().body("<div id=\"password-strength\" class=\"mb-3 help content is-danger\">Password darf nicht länger als 256 Zeichen sein.</div>"));
|
||||||
|
} else {
|
||||||
|
return Ok(HttpResponse::NoContent().finish());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let user = User::read_by_id(pool.get_ref(), token.userid).await?;
|
||||||
|
let mut split_names: Vec<&str> = user.name.as_str().split_whitespace().collect();
|
||||||
|
let mut user_inputs = vec![user.email.as_str()];
|
||||||
|
user_inputs.append(&mut split_names);
|
||||||
|
let entropy = zxcvbn(&form.password, &user_inputs);
|
||||||
|
|
||||||
|
if entropy.score() < Score::Three {
|
||||||
|
if is_dry {
|
||||||
|
let message = password_help::generate_for_entropy(&entropy);
|
||||||
|
|
||||||
|
return Ok(HttpResponse::BadRequest().body(message));
|
||||||
|
} else {
|
||||||
|
return Ok(HttpResponse::NoContent().finish());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if is_dry {
|
||||||
|
if entropy.score() == Score::Three {
|
||||||
|
return Ok(HttpResponse::Ok()
|
||||||
|
.body("<div id=\"password-strength\" class=\"mb-3 help content is-success\">Sicheres Passwort.</div>"));
|
||||||
|
} else {
|
||||||
|
return Ok(HttpResponse::Ok()
|
||||||
|
.body("<div id=\"password-strength\" class=\"mb-3 help content is-success\">Sehr sicheres Passwort.</div>"));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if form.password != form.passwordretyped {
|
||||||
|
return Ok(HttpResponse::BadRequest().body("Passwörter stimmen nicht überein!"));
|
||||||
|
}
|
||||||
|
let (hash, salt) = auth::utils::generate_salt_and_hash_plain_password(&form.password).unwrap();
|
||||||
|
|
||||||
|
User::update(
|
||||||
|
pool.get_ref(),
|
||||||
|
token.userid,
|
||||||
|
None,
|
||||||
|
None,
|
||||||
|
Some(&hash),
|
||||||
|
Some(&salt),
|
||||||
|
None,
|
||||||
|
None,
|
||||||
|
None,
|
||||||
|
None,
|
||||||
|
None,
|
||||||
|
)
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
Registration::delete(pool.get_ref(), &token.token).await?;
|
||||||
|
|
||||||
|
Ok(HttpResponse::Ok().body(r#"<div class="block">Registrierung abgeschlossen.</div><a class="block button is-primary" hx-boost="true" href="/login">Zum Login</a>"#))
|
||||||
|
}
|
@ -1,10 +1,5 @@
|
|||||||
use std::env;
|
|
||||||
|
|
||||||
use actix_web::{web, HttpResponse, Responder};
|
use actix_web::{web, HttpResponse, Responder};
|
||||||
use lettre::{
|
use lettre::{SmtpTransport, Transport};
|
||||||
message::{header::ContentType, MultiPart, SinglePart},
|
|
||||||
Message, SmtpTransport, Transport,
|
|
||||||
};
|
|
||||||
use serde::Deserialize;
|
use serde::Deserialize;
|
||||||
use sqlx::PgPool;
|
use sqlx::PgPool;
|
||||||
use zxcvbn::{zxcvbn, Score};
|
use zxcvbn::{zxcvbn, Score};
|
||||||
@ -12,7 +7,7 @@ use zxcvbn::{zxcvbn, Score};
|
|||||||
use crate::{
|
use crate::{
|
||||||
auth::{self},
|
auth::{self},
|
||||||
models::{PasswordReset, User},
|
models::{PasswordReset, User},
|
||||||
utils::password_help,
|
utils::{email, password_help},
|
||||||
};
|
};
|
||||||
|
|
||||||
#[derive(Deserialize, Debug)]
|
#[derive(Deserialize, Debug)]
|
||||||
@ -40,43 +35,7 @@ async fn post(
|
|||||||
.await
|
.await
|
||||||
.unwrap();
|
.unwrap();
|
||||||
|
|
||||||
let hostname = env::var("HOSTNAME").unwrap();
|
let message = email::build_forgot_password_message(&user, &reset.token);
|
||||||
let reset_url = format!("https://{}/reset-password?token={}", hostname, reset.token);
|
|
||||||
|
|
||||||
let message = Message::builder()
|
|
||||||
.from("noreply <noreply@brasiwa-leipzig.de>".parse().unwrap())
|
|
||||||
.reply_to("noreply <noreply@brasiwa-leipzig.de>".parse().unwrap())
|
|
||||||
.to(format!("{} <{}>", user.name, user.email).parse().unwrap())
|
|
||||||
.subject("Brass: Zurücksetzen des Passworts angefordert")
|
|
||||||
.multipart(
|
|
||||||
MultiPart::alternative()
|
|
||||||
.singlepart(
|
|
||||||
SinglePart::builder()
|
|
||||||
.header(ContentType::TEXT_PLAIN)
|
|
||||||
.body(format!(r##"Hallo {},
|
|
||||||
|
|
||||||
du hast angefordert, dein Passwort zurückzusetzen. Kopiere dafür folgenden Link in deinen Browser:
|
|
||||||
|
|
||||||
{}
|
|
||||||
|
|
||||||
Bitte beachte, dass der Link nur 24 Stunden gültig ist. Solltest du nichts angefordert haben, dann musst du nichts weiter tun.
|
|
||||||
|
|
||||||
Viele Grüße"##, user.name, reset_url))
|
|
||||||
)
|
|
||||||
.singlepart(
|
|
||||||
SinglePart::builder()
|
|
||||||
.header(ContentType::TEXT_HTML)
|
|
||||||
.body(format!(r##"<p>Hallo {},</p>
|
|
||||||
|
|
||||||
<p>du hast angefordert, dein Passwort zurückzusetzen. Klicke dafür <a href="{}" target="_blank">hier</a> oder kopiere folgenden Link in deinen Browser:</p>
|
|
||||||
|
|
||||||
<p>{}</p>
|
|
||||||
|
|
||||||
<p>Bitte beachte, dass der Link <b>nur 24 Stunden gültig</b> ist. Solltest du nichts angefordert haben, dann musst du nichts weiter tun.</p>
|
|
||||||
|
|
||||||
<p>Viele Grüße</p>"##, user.name, reset_url, reset_url))
|
|
||||||
))
|
|
||||||
.unwrap();
|
|
||||||
|
|
||||||
mailer.send(&message).unwrap();
|
mailer.send(&message).unwrap();
|
||||||
}
|
}
|
||||||
|
@ -2,6 +2,8 @@ use std::fmt::Display;
|
|||||||
|
|
||||||
use serde::Serialize;
|
use serde::Serialize;
|
||||||
|
|
||||||
|
use crate::utils::ApplicationError;
|
||||||
|
|
||||||
#[derive(sqlx::Type, Debug, Clone, Copy, PartialEq, Eq, Serialize)]
|
#[derive(sqlx::Type, Debug, Clone, Copy, PartialEq, Eq, Serialize)]
|
||||||
#[sqlx(type_name = "function", rename_all = "lowercase")]
|
#[sqlx(type_name = "function", rename_all = "lowercase")]
|
||||||
pub enum Function {
|
pub enum Function {
|
||||||
@ -13,19 +15,22 @@ impl Display for Function {
|
|||||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||||
match self {
|
match self {
|
||||||
Function::Posten => write!(f, "Posten"),
|
Function::Posten => write!(f, "Posten"),
|
||||||
Function::Wachhabender => write!(f, "Wachhabender")
|
Function::Wachhabender => write!(f, "Wachhabender"),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl TryFrom<u8> for Function {
|
impl TryFrom<u8> for Function {
|
||||||
type Error = ();
|
type Error = ApplicationError;
|
||||||
|
|
||||||
fn try_from(value: u8) -> Result<Self, Self::Error> {
|
fn try_from(value: u8) -> Result<Self, Self::Error> {
|
||||||
match value {
|
match value {
|
||||||
1 => Ok(Function::Posten),
|
1 => Ok(Function::Posten),
|
||||||
10 => Ok(Function::Wachhabender),
|
10 => Ok(Function::Wachhabender),
|
||||||
_ => Err(()),
|
_ => Err(ApplicationError::UnsupportedEnumValue {
|
||||||
|
value: value.to_string(),
|
||||||
|
enum_name: String::from("Function"),
|
||||||
|
}),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -8,6 +8,7 @@ mod role;
|
|||||||
mod user;
|
mod user;
|
||||||
mod vehicle;
|
mod vehicle;
|
||||||
mod password_reset;
|
mod password_reset;
|
||||||
|
mod registration;
|
||||||
|
|
||||||
pub use area::Area;
|
pub use area::Area;
|
||||||
pub use availabillity::Availabillity;
|
pub use availabillity::Availabillity;
|
||||||
@ -18,3 +19,6 @@ pub use role::Role;
|
|||||||
pub use user::User;
|
pub use user::User;
|
||||||
pub use assignement::Assignment;
|
pub use assignement::Assignment;
|
||||||
pub use password_reset::PasswordReset;
|
pub use password_reset::PasswordReset;
|
||||||
|
pub use registration::Registration;
|
||||||
|
|
||||||
|
type Result<T> = std::result::Result<T, sqlx::Error>;
|
||||||
|
@ -1,8 +1,9 @@
|
|||||||
use anyhow::Result;
|
use anyhow::Result;
|
||||||
use chrono::{NaiveDateTime, TimeDelta, Utc};
|
use chrono::{NaiveDateTime, TimeDelta};
|
||||||
use rand::{distributions::Alphanumeric, rngs::OsRng, Rng as _};
|
|
||||||
use sqlx::{query_as, PgPool};
|
use sqlx::{query_as, PgPool};
|
||||||
|
|
||||||
|
use crate::utils::token_generation::generate_token_and_expiration;
|
||||||
|
|
||||||
#[derive(Debug)]
|
#[derive(Debug)]
|
||||||
pub struct PasswordReset {
|
pub struct PasswordReset {
|
||||||
pub id: i32,
|
pub id: i32,
|
||||||
@ -13,14 +14,7 @@ pub struct PasswordReset {
|
|||||||
|
|
||||||
impl PasswordReset {
|
impl PasswordReset {
|
||||||
pub async fn insert_new_for_user(pool: &PgPool, user_id: i32) -> Result<PasswordReset>{
|
pub async fn insert_new_for_user(pool: &PgPool, user_id: i32) -> Result<PasswordReset>{
|
||||||
let value = std::iter::repeat(())
|
let (token, expires) = generate_token_and_expiration(64, TimeDelta::hours(24));
|
||||||
.map(|()| OsRng.sample(Alphanumeric))
|
|
||||||
.take(64)
|
|
||||||
.collect::<Vec<_>>();
|
|
||||||
|
|
||||||
let token = String::from_utf8(value).unwrap().try_into().unwrap();
|
|
||||||
|
|
||||||
let expires = Utc::now().naive_utc() + TimeDelta::hours(24);
|
|
||||||
|
|
||||||
let inserted = query_as!(
|
let inserted = query_as!(
|
||||||
PasswordReset,
|
PasswordReset,
|
||||||
|
50
src/models/registration.rs
Normal file
50
src/models/registration.rs
Normal file
@ -0,0 +1,50 @@
|
|||||||
|
use chrono::{NaiveDateTime, TimeDelta};
|
||||||
|
use sqlx::{query_as, PgPool};
|
||||||
|
|
||||||
|
use crate::utils::token_generation::generate_token_and_expiration;
|
||||||
|
|
||||||
|
#[derive(Debug)]
|
||||||
|
pub struct Registration {
|
||||||
|
pub id: i32,
|
||||||
|
pub token: String,
|
||||||
|
pub userid: i32,
|
||||||
|
pub expires: NaiveDateTime,
|
||||||
|
}
|
||||||
|
|
||||||
|
use super::Result;
|
||||||
|
|
||||||
|
impl Registration {
|
||||||
|
pub async fn insert_new_for_user(pool: &PgPool, user_id: i32) -> Result<Registration> {
|
||||||
|
let (token, expires) = generate_token_and_expiration(64, TimeDelta::hours(24));
|
||||||
|
|
||||||
|
let inserted = query_as!(
|
||||||
|
Registration,
|
||||||
|
"INSERT INTO registration (token, userId, expires) VALUES ($1, $2, $3) RETURNING *;",
|
||||||
|
token,
|
||||||
|
user_id,
|
||||||
|
expires
|
||||||
|
)
|
||||||
|
.fetch_one(pool)
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
Ok(inserted)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn does_token_exist(pool: &PgPool, token: &str) -> Result<Registration> {
|
||||||
|
query_as!(
|
||||||
|
Registration,
|
||||||
|
"SELECT * FROM registration WHERE token = $1 AND expires > NOW();",
|
||||||
|
token
|
||||||
|
)
|
||||||
|
.fetch_one(pool)
|
||||||
|
.await
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn delete(pool: &PgPool, token: &str) -> Result<()> {
|
||||||
|
sqlx::query!("DELETE FROM registration WHERE token = $1;", token)
|
||||||
|
.execute(pool)
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
@ -1,3 +1,5 @@
|
|||||||
|
use crate::utils::ApplicationError;
|
||||||
|
|
||||||
#[derive(sqlx::Type, Debug, Clone, Copy, PartialEq)]
|
#[derive(sqlx::Type, Debug, Clone, Copy, PartialEq)]
|
||||||
#[sqlx(type_name = "role", rename_all = "lowercase")]
|
#[sqlx(type_name = "role", rename_all = "lowercase")]
|
||||||
pub enum Role {
|
pub enum Role {
|
||||||
@ -7,14 +9,17 @@ pub enum Role {
|
|||||||
}
|
}
|
||||||
|
|
||||||
impl TryFrom<u8> for Role {
|
impl TryFrom<u8> for Role {
|
||||||
type Error = ();
|
type Error = ApplicationError;
|
||||||
|
|
||||||
fn try_from(value: u8) -> Result<Self, Self::Error> {
|
fn try_from(value: u8) -> Result<Self, Self::Error> {
|
||||||
match value {
|
match value {
|
||||||
1 => Ok(Role::Staff),
|
1 => Ok(Role::Staff),
|
||||||
10 => Ok(Role::AreaManager),
|
10 => Ok(Role::AreaManager),
|
||||||
100 => Ok(Role::Admin),
|
100 => Ok(Role::Admin),
|
||||||
_ => Err(()),
|
_ => Err(ApplicationError::UnsupportedEnumValue {
|
||||||
|
value: value.to_string(),
|
||||||
|
enum_name: String::from("Role"),
|
||||||
|
}),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,15 +1,15 @@
|
|||||||
use chrono::{DateTime, Utc};
|
use chrono::{DateTime, Utc};
|
||||||
use sqlx::PgPool;
|
use sqlx::PgPool;
|
||||||
|
|
||||||
use super::{Area, Function, Role};
|
use super::{Area, Function, Result, Role};
|
||||||
|
|
||||||
#[derive(Clone)]
|
#[derive(Clone)]
|
||||||
pub struct User {
|
pub struct User {
|
||||||
pub id: i32,
|
pub id: i32,
|
||||||
pub name: String,
|
pub name: String,
|
||||||
pub email: String,
|
pub email: String,
|
||||||
pub password: String,
|
pub password: Option<String>,
|
||||||
pub salt: String,
|
pub salt: Option<String>,
|
||||||
pub role: Role,
|
pub role: Role,
|
||||||
pub function: Function,
|
pub function: Function,
|
||||||
pub area_id: i32,
|
pub area_id: i32,
|
||||||
@ -21,6 +21,31 @@ pub struct User {
|
|||||||
|
|
||||||
impl User {
|
impl User {
|
||||||
pub async fn create(
|
pub async fn create(
|
||||||
|
pool: &PgPool,
|
||||||
|
name: &str,
|
||||||
|
email: &str,
|
||||||
|
role: Role,
|
||||||
|
function: Function,
|
||||||
|
area_id: i32,
|
||||||
|
) -> Result<i32> {
|
||||||
|
sqlx::query!(
|
||||||
|
r#"
|
||||||
|
INSERT INTO user_ (name, email, role, function, areaId)
|
||||||
|
VALUES ($1, $2, $3, $4, $5)
|
||||||
|
RETURNING id;
|
||||||
|
"#,
|
||||||
|
name,
|
||||||
|
email,
|
||||||
|
role as Role,
|
||||||
|
function as Function,
|
||||||
|
area_id
|
||||||
|
)
|
||||||
|
.fetch_one(pool)
|
||||||
|
.await
|
||||||
|
.and_then(|r| Ok(r.id))
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn create_with_password(
|
||||||
pool: &PgPool,
|
pool: &PgPool,
|
||||||
name: &str,
|
name: &str,
|
||||||
email: &str,
|
email: &str,
|
||||||
@ -29,8 +54,8 @@ impl User {
|
|||||||
role: Role,
|
role: Role,
|
||||||
function: Function,
|
function: Function,
|
||||||
area_id: i32,
|
area_id: i32,
|
||||||
) -> anyhow::Result<i32> {
|
) -> Result<i32> {
|
||||||
let created = sqlx::query!(
|
let b = sqlx::query!(
|
||||||
r#"
|
r#"
|
||||||
INSERT INTO user_ (name, email, password, salt, role, function, areaId)
|
INSERT INTO user_ (name, email, password, salt, role, function, areaId)
|
||||||
VALUES ($1, $2, $3, $4, $5, $6, $7)
|
VALUES ($1, $2, $3, $4, $5, $6, $7)
|
||||||
@ -45,15 +70,13 @@ impl User {
|
|||||||
area_id
|
area_id
|
||||||
)
|
)
|
||||||
.fetch_one(pool)
|
.fetch_one(pool)
|
||||||
.await;
|
.await
|
||||||
|
.and_then(|r| Ok(r.id));
|
||||||
|
|
||||||
match created {
|
b
|
||||||
Ok(result) => Ok(result.id),
|
|
||||||
Err(err) => Err(err.into()),
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn read_by_id(pool: &PgPool, id: i32) -> anyhow::Result<User> {
|
pub async fn read_by_id(pool: &PgPool, id: i32) -> Result<User> {
|
||||||
let record = sqlx::query!(
|
let record = sqlx::query!(
|
||||||
r#"
|
r#"
|
||||||
SELECT id,
|
SELECT id,
|
||||||
@ -108,7 +131,7 @@ impl User {
|
|||||||
lastLogin,
|
lastLogin,
|
||||||
receiveNotifications
|
receiveNotifications
|
||||||
FROM user_
|
FROM user_
|
||||||
WHERE email = $1 AND locked = FALSE;
|
WHERE email = $1 AND locked = FALSE AND password IS NOT NULL AND salt IS NOT NULL;
|
||||||
"#,
|
"#,
|
||||||
email,
|
email,
|
||||||
)
|
)
|
||||||
@ -277,7 +300,7 @@ impl User {
|
|||||||
area_id: Option<i32>,
|
area_id: Option<i32>,
|
||||||
receive_notifications: Option<bool>,
|
receive_notifications: Option<bool>,
|
||||||
locked: Option<bool>,
|
locked: Option<bool>,
|
||||||
) -> anyhow::Result<()> {
|
) -> Result<()> {
|
||||||
let mut query_builder = sqlx::QueryBuilder::new("UPDATE user_ SET ");
|
let mut query_builder = sqlx::QueryBuilder::new("UPDATE user_ SET ");
|
||||||
let mut separated = query_builder.separated(", ");
|
let mut separated = query_builder.separated(", ");
|
||||||
|
|
||||||
|
26
src/utils/application_error.rs
Normal file
26
src/utils/application_error.rs
Normal file
@ -0,0 +1,26 @@
|
|||||||
|
use actix_web::{http::StatusCode, HttpResponse};
|
||||||
|
use thiserror::Error;
|
||||||
|
|
||||||
|
#[derive(Debug, Error)]
|
||||||
|
pub enum ApplicationError {
|
||||||
|
#[error("unsupported value '{value}' for enum '{enum_name}'")]
|
||||||
|
UnsupportedEnumValue { value: String, enum_name: String },
|
||||||
|
#[error("unauthorized")]
|
||||||
|
Unauthorized,
|
||||||
|
#[error("database error")]
|
||||||
|
Database(#[from] sqlx::Error),
|
||||||
|
}
|
||||||
|
|
||||||
|
impl actix_web::error::ResponseError for ApplicationError {
|
||||||
|
fn status_code(&self) -> StatusCode {
|
||||||
|
match *self {
|
||||||
|
ApplicationError::UnsupportedEnumValue { .. } => StatusCode::BAD_REQUEST,
|
||||||
|
ApplicationError::Unauthorized { .. } => StatusCode::UNAUTHORIZED,
|
||||||
|
ApplicationError::Database(_) => StatusCode::INTERNAL_SERVER_ERROR,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn error_response(&self) -> HttpResponse {
|
||||||
|
HttpResponse::build(self.status_code()).body(self.to_string())
|
||||||
|
}
|
||||||
|
}
|
@ -1,10 +1,13 @@
|
|||||||
use std::env;
|
use std::env;
|
||||||
|
|
||||||
use lettre::{
|
use lettre::{
|
||||||
|
message::{header::ContentType, MultiPart, SinglePart},
|
||||||
transport::smtp::{authentication::Credentials, extension::ClientId},
|
transport::smtp::{authentication::Credentials, extension::ClientId},
|
||||||
SmtpTransport,
|
Message, SmtpTransport,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
use crate::models::User;
|
||||||
|
|
||||||
pub fn get_mailer() -> anyhow::Result<SmtpTransport> {
|
pub fn get_mailer() -> anyhow::Result<SmtpTransport> {
|
||||||
let server = &env::var("SMTP_SERVER")?;
|
let server = &env::var("SMTP_SERVER")?;
|
||||||
let port = &env::var("SMTP_PORT")?.parse()?;
|
let port = &env::var("SMTP_PORT")?.parse()?;
|
||||||
@ -33,3 +36,87 @@ pub fn get_mailer() -> anyhow::Result<SmtpTransport> {
|
|||||||
|
|
||||||
Ok(mailer)
|
Ok(mailer)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn build_forgot_password_message(user: &User, token: &str) -> Message {
|
||||||
|
let hostname = env::var("HOSTNAME").unwrap();
|
||||||
|
let reset_url = format!("https://{hostname}/reset-password?token={token}");
|
||||||
|
|
||||||
|
let message = Message::builder()
|
||||||
|
.from("noreply <noreply@brasiwa-leipzig.de>".parse().unwrap())
|
||||||
|
.reply_to("noreply <noreply@brasiwa-leipzig.de>".parse().unwrap())
|
||||||
|
.to(format!("{} <{}>", user.name, user.email).parse().unwrap())
|
||||||
|
.subject("Brass: Zurücksetzen des Passworts angefordert")
|
||||||
|
.multipart(
|
||||||
|
MultiPart::alternative()
|
||||||
|
.singlepart(
|
||||||
|
SinglePart::builder()
|
||||||
|
.header(ContentType::TEXT_PLAIN)
|
||||||
|
.body(format!(r##"Hallo {},
|
||||||
|
|
||||||
|
du hast angefordert, dein Passwort zurückzusetzen. Kopiere dafür folgenden Link in deinen Browser:
|
||||||
|
|
||||||
|
{reset_url}
|
||||||
|
|
||||||
|
Bitte beachte, dass der Link nur 24 Stunden gültig ist. Solltest du nichts angefordert haben, dann musst du nichts weiter tun.
|
||||||
|
|
||||||
|
Viele Grüße"##, user.name))
|
||||||
|
)
|
||||||
|
.singlepart(
|
||||||
|
SinglePart::builder()
|
||||||
|
.header(ContentType::TEXT_HTML)
|
||||||
|
.body(format!(r##"<p>Hallo {},</p>
|
||||||
|
|
||||||
|
<p>du hast angefordert, dein Passwort zurückzusetzen. Klicke dafür <a href="{reset_url}" target="_blank">hier</a> oder kopiere folgenden Link in deinen Browser:</p>
|
||||||
|
|
||||||
|
<p>{reset_url}</p>
|
||||||
|
|
||||||
|
<p>Bitte beachte, dass der Link <b>nur 24 Stunden gültig</b> ist. Solltest du nichts angefordert haben, dann musst du nichts weiter tun.</p>
|
||||||
|
|
||||||
|
<p>Viele Grüße</p>"##, user.name))
|
||||||
|
))
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
return message;
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn build_registration_message(user: &User, token: &str) -> Message {
|
||||||
|
let hostname = env::var("HOSTNAME").unwrap();
|
||||||
|
let register_url = format!("https://{hostname}/register?token={token}");
|
||||||
|
|
||||||
|
let message = Message::builder()
|
||||||
|
.from("noreply <noreply@brasiwa-leipzig.de>".parse().unwrap())
|
||||||
|
.reply_to("noreply <noreply@brasiwa-leipzig.de>".parse().unwrap())
|
||||||
|
.to(format!("{} <{}>", user.name, user.email).parse().unwrap())
|
||||||
|
.subject("Brass: Registrierung deines Accounts")
|
||||||
|
.multipart(
|
||||||
|
MultiPart::alternative()
|
||||||
|
.singlepart(
|
||||||
|
SinglePart::builder()
|
||||||
|
.header(ContentType::TEXT_PLAIN)
|
||||||
|
.body(format!(r##"Hallo {},
|
||||||
|
|
||||||
|
dein Account für https:://{hostname} wurde erstellt. Du musst nur noch ein Passwort festlegen. Kopiere dafür folgenden Link in deinen Browser:
|
||||||
|
|
||||||
|
{register_url}
|
||||||
|
|
||||||
|
Bitte beachte, dass der Link nur 24 Stunden gültig ist.
|
||||||
|
|
||||||
|
Viele Grüße"##, user.name))
|
||||||
|
)
|
||||||
|
.singlepart(
|
||||||
|
SinglePart::builder()
|
||||||
|
.header(ContentType::TEXT_HTML)
|
||||||
|
.body(format!(r##"<p>Hallo {},</p>
|
||||||
|
|
||||||
|
<p>dein Account für <a href="https:://{hostname}" target="_blank">https://{hostname}</a> wurde erstellt. Du musst nur noch ein Passwort festlegen. Klicke dafür <a href="{register_url}" target="_blank">hier</a> oder kopiere folgenden Link in deinen Browser:</p>
|
||||||
|
|
||||||
|
<p>{register_url}</p>
|
||||||
|
|
||||||
|
<p>Bitte beachte, dass der Link <b>nur 24 Stunden gültig</b> ist.</p>
|
||||||
|
|
||||||
|
<p>Viele Grüße</p>"##, user.name))
|
||||||
|
))
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
return message;
|
||||||
|
}
|
||||||
|
@ -67,7 +67,7 @@ pub async fn handle_command(command: Option<Command>, pool: &Pool<Postgres>) ->
|
|||||||
|
|
||||||
let (hash, salt) = generate_salt_and_hash_plain_password(&password)?;
|
let (hash, salt) = generate_salt_and_hash_plain_password(&password)?;
|
||||||
|
|
||||||
User::create(
|
User::create_with_password(
|
||||||
&pool,
|
&pool,
|
||||||
&name,
|
&name,
|
||||||
&email,
|
&email,
|
||||||
|
@ -1,3 +1,7 @@
|
|||||||
pub mod email;
|
pub mod email;
|
||||||
pub mod manage_commands;
|
pub mod manage_commands;
|
||||||
pub mod password_help;
|
pub mod password_help;
|
||||||
|
pub mod token_generation;
|
||||||
|
mod application_error;
|
||||||
|
|
||||||
|
pub use application_error::ApplicationError;
|
||||||
|
15
src/utils/token_generation.rs
Normal file
15
src/utils/token_generation.rs
Normal file
@ -0,0 +1,15 @@
|
|||||||
|
use chrono::{NaiveDateTime, TimeDelta, Utc};
|
||||||
|
use rand::{distributions::Alphanumeric, rngs::OsRng, Rng};
|
||||||
|
|
||||||
|
pub fn generate_token_and_expiration(token_length_bytes: usize, validity: TimeDelta) -> (String, NaiveDateTime) {
|
||||||
|
let value = std::iter::repeat(())
|
||||||
|
.map(|()| OsRng.sample(Alphanumeric))
|
||||||
|
.take(token_length_bytes)
|
||||||
|
.collect::<Vec<_>>();
|
||||||
|
|
||||||
|
let token = String::from_utf8(value).unwrap().try_into().unwrap();
|
||||||
|
|
||||||
|
let expires = Utc::now().naive_utc() + validity;
|
||||||
|
|
||||||
|
return (token, expires);
|
||||||
|
}
|
@ -3,15 +3,15 @@
|
|||||||
{% block body %}
|
{% block body %}
|
||||||
<section class="section">
|
<section class="section">
|
||||||
<div class="container">
|
<div class="container">
|
||||||
<h1 class="title">Brass - Passwort zurücksetzen</h1>
|
<h1 class="title">{{ title }}</h1>
|
||||||
<form class="box" hx-post="/reset-password" hx-params="not dry" hx-target-400="#error-message-retype"
|
<form class="box" hx-post="{{ endpoint }}" hx-params="not dry" hx-target-400="#error-message-retype"
|
||||||
hx-on:input="document.getElementById('error-message-retype').innerHTML = ''">
|
hx-on:input="document.getElementById('error-message-retype').innerHTML = ''">
|
||||||
<input type="hidden" name="token" value="{{ token }}" />
|
<input type="hidden" name="token" value="{{ token }}" />
|
||||||
|
|
||||||
<input type="hidden" name="dry" value="true" />
|
<input type="hidden" name="dry" value="true" />
|
||||||
|
|
||||||
<div class="field">
|
<div class="field">
|
||||||
<label class="label" for="password">neues Passwort:</label>
|
<label class="label" for="password">{{ new_password_label }}</label>
|
||||||
<div class="control">
|
<div class="control">
|
||||||
<input class="input" hx-post="/reset-password?dry=true" hx-params="*" hx-trigger="keyup changed delay:500ms"
|
<input class="input" hx-post="/reset-password?dry=true" hx-params="*" hx-trigger="keyup changed delay:500ms"
|
||||||
hx-target="#password-strength" hx-target-400="#password-strength" placeholder="**********" name="password"
|
hx-target="#password-strength" hx-target-400="#password-strength" placeholder="**********" name="password"
|
||||||
@ -22,7 +22,7 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="field">
|
<div class="field">
|
||||||
<label class="label" for="passwordretyped">neues Passwort wiederholen:</label>
|
<label class="label" for="passwordretyped">{{ retype_label }}</label>
|
||||||
<div class="control">
|
<div class="control">
|
||||||
<input class="input" placeholder="**********" name="passwordretyped" type="password" maxlength=256 required>
|
<input class="input" placeholder="**********" name="passwordretyped" type="password" maxlength=256 required>
|
||||||
</div>
|
</div>
|
||||||
@ -30,7 +30,7 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="level">
|
<div class="level">
|
||||||
<input class="button is-primary level-left" type="submit" value="Passwort zurücksetzen" />
|
<input class="button is-primary level-left" type="submit" value="{{ submit_button_label }}" />
|
||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
</div>
|
</div>
|
Loading…
x
Reference in New Issue
Block a user