From ea35f5475ff16d2af9fa77f8ba314a7a997a7f29 Mon Sep 17 00:00:00 2001 From: Max Hohlfeld Date: Sun, 8 Sep 2024 19:47:05 +0200 Subject: [PATCH] feat: registration of users --- migrations/20230609121618_initial.sql | 20 +++++++------- src/endpoints/mod.rs | 2 ++ src/endpoints/user/delete.rs | 40 ++++++++++++++------------- src/endpoints/user/get_register.rs | 14 +++++----- src/endpoints/user/post_new.rs | 7 +++-- src/endpoints/user/post_register.rs | 3 +- src/middleware/redirect_to_login.rs | 1 + src/models/area.rs | 4 +-- src/models/registration.rs | 4 +-- src/models/user.rs | 4 +-- src/utils/application_error.rs | 24 +++++++++++++++- src/utils/email.rs | 19 +++++++------ templates/user/change_password.html | 2 +- 13 files changed, 88 insertions(+), 56 deletions(-) diff --git a/migrations/20230609121618_initial.sql b/migrations/20230609121618_initial.sql index 6b0a6b5e..977a9785 100644 --- a/migrations/20230609121618_initial.sql +++ b/migrations/20230609121618_initial.sql @@ -13,7 +13,7 @@ CREATE TABLE location ( id SERIAL PRIMARY KEY, name TEXT NOT NULL, - areaId INTEGER NOT NULL REFERENCES area (id) + areaId INTEGER NOT NULL REFERENCES area (id) ON DELETE CASCADE ); CREATE TABLE user_ @@ -25,7 +25,7 @@ CREATE TABLE user_ salt TEXT , role role NOT NULL, function function NOT NULL, - areaId INTEGER NOT NULL REFERENCES area (id), + areaId INTEGER NOT NULL REFERENCES area (id) ON DELETE CASCADE, locked BOOLEAN NOT NULL DEFAULT false, lastLogin TIMESTAMP WITH TIME ZONE, receiveNotifications BOOLEAN NOT NULL DEFAULT true @@ -34,7 +34,7 @@ CREATE TABLE user_ CREATE TABLE availabillity ( id SERIAL PRIMARY KEY, - userId INTEGER NOT NULL REFERENCES user_ (id), + userId INTEGER NOT NULL REFERENCES user_ (id) ON DELETE CASCADE, date DATE NOT NULL, startTime TIME, endTime TIME, @@ -48,7 +48,7 @@ CREATE TABLE event startTime TIME NOT NULL, endTime TIME NOT NULL, name TEXT NOT NULL, - locationId INTEGER NOT NULL REFERENCES location (id), + locationId INTEGER NOT NULL REFERENCES location (id) ON DELETE CASCADE, voluntaryWachhabender BOOLEAN NOT NULL, amountOfPosten SMALLINT NOT NULL CHECK (amountOfPosten >= 0), clothing TEXT NOT NULL, @@ -57,8 +57,8 @@ CREATE TABLE event CREATE TABLE assignment ( - eventId INTEGER REFERENCES event (id), - availabillityId INTEGER REFERENCES availabillity (id), + eventId INTEGER REFERENCES event (id) ON DELETE CASCADE, + availabillityId INTEGER REFERENCES availabillity (id) ON DELETE CASCADE, function function NOT NULL, startTime TIME NOT NULL, endTime TIME NOT NULL, @@ -74,8 +74,8 @@ CREATE TABLE vehicle CREATE TABLE vehicleassignement ( - eventId INTEGER REFERENCES event (id), - vehicleId INTEGER REFERENCES vehicle (id), + eventId INTEGER REFERENCES event (id) ON DELETE CASCADE, + vehicleId INTEGER REFERENCES vehicle (id) ON DELETE CASCADE, PRIMARY KEY (eventId, vehicleId) ); @@ -93,7 +93,7 @@ CREATE UNLOGGED TABLE passwordReset ( id SERIAL PRIMARY KEY, token TEXT UNIQUE NOT NULL, - userId INTEGER NOT NULL REFERENCES user_ (id), + userId INTEGER NOT NULL REFERENCES user_ (id) ON DELETE CASCADE, expires TIMESTAMP NOT NULL ); @@ -103,7 +103,7 @@ CREATE UNLOGGED TABLE registration ( id SERIAL PRIMARY KEY, token TEXT UNIQUE NOT NULL, - userId INTEGER NOT NULL REFERENCES user_ (id), + userId INTEGER NOT NULL REFERENCES user_ (id) ON DELETE CASCADE, expires TIMESTAMP NOT NULL ); diff --git a/src/endpoints/mod.rs b/src/endpoints/mod.rs index 00834464..d4f8223e 100644 --- a/src/endpoints/mod.rs +++ b/src/endpoints/mod.rs @@ -41,6 +41,8 @@ pub fn init(cfg: &mut ServiceConfig) { cfg.service(user::post_toggle::post); cfg.service(user::get_changepassword::get); cfg.service(user::post_changepassword::post); + cfg.service(user::get_register::get); + cfg.service(user::post_register::post); cfg.service(availability::delete::delete); cfg.service(availability::get_new::get); diff --git a/src/endpoints/user/delete.rs b/src/endpoints/user/delete.rs index 36705260..e1a2bbc8 100644 --- a/src/endpoints/user/delete.rs +++ b/src/endpoints/user/delete.rs @@ -1,30 +1,32 @@ -use actix_identity::Identity; use actix_web::{web, HttpResponse, Responder}; use sqlx::PgPool; -use crate::{endpoints::IdPath, models::{Role, User}}; +use crate::{ + endpoints::IdPath, + models::{Role, User}, utils::ApplicationError, +}; #[actix_web::delete("/users/{id}")] -pub async fn delete(user: Identity, pool: web::Data, path: web::Path) -> impl Responder { - let current_user = User::read_by_id(pool.get_ref(), user.id().unwrap().parse().unwrap()) - .await - .unwrap(); - - if current_user.role != Role::AreaManager && current_user.role != Role::Admin { - return HttpResponse::Unauthorized().finish(); +pub async fn delete( + user: web::ReqData, + pool: web::Data, + path: web::Path, +) -> Result { + if user.role != Role::AreaManager && user.role != Role::Admin { + return Err(ApplicationError::Unauthorized); } - if let Ok(user_in_db) = User::read_by_id(pool.get_ref(), path.id).await { - if current_user.role == Role::AreaManager && current_user.area_id != user_in_db.area_id { - return HttpResponse::Unauthorized().finish(); - } + let user_in_db = User::read_by_id(pool.get_ref(), path.id).await?; - if user_in_db.locked { - if let Ok(_) = User::delete(pool.get_ref(), user_in_db.id).await { - return HttpResponse::Ok().finish(); - } - } + if user.role == Role::AreaManager && user.area_id != user_in_db.area_id { + return Err(ApplicationError::Unauthorized); } - HttpResponse::BadRequest().finish() + if user_in_db.locked { + User::delete(pool.get_ref(), user_in_db.id).await?; + + return Ok(HttpResponse::Ok().finish()); + } + + Ok(HttpResponse::BadRequest().finish()) } diff --git a/src/endpoints/user/get_register.rs b/src/endpoints/user/get_register.rs index 097d064c..773922b5 100644 --- a/src/endpoints/user/get_register.rs +++ b/src/endpoints/user/get_register.rs @@ -4,7 +4,7 @@ use askama_actix::TemplateToResponse; use serde::Deserialize; use sqlx::PgPool; -use crate::models::Registration; +use crate::{models::Registration, utils::ApplicationError}; use super::ResetPasswordTemplate; @@ -18,14 +18,14 @@ pub async fn get( user: Option, pool: web::Data, query: web::Query, -) -> impl Responder { +) -> Result { if user.is_some() { - return HttpResponse::Found() + return Ok(HttpResponse::Found() .insert_header((LOCATION, "/")) - .finish(); + .finish()); } - if let Ok(_) = Registration::does_token_exist(pool.get_ref(), &query.token).await { + if let Some(_) = Registration::does_token_exist(pool.get_ref(), &query.token).await? { let template = ResetPasswordTemplate { token: &query.token, title: "Brass - Registrierung", @@ -35,8 +35,8 @@ pub async fn get( submit_button_label: "Registrieren", }; - return template.to_response(); + return Ok(template.to_response()); } - HttpResponse::NotFound().finish() + Ok(HttpResponse::NotFound().finish()) } diff --git a/src/endpoints/user/post_new.rs b/src/endpoints/user/post_new.rs index d433b54a..80a2a786 100644 --- a/src/endpoints/user/post_new.rs +++ b/src/endpoints/user/post_new.rs @@ -22,7 +22,7 @@ pub async fn post_new( user: web::ReqData, pool: web::Data, form: web::Form, - mailer: web::ReqData, + mailer: web::Data, ) -> Result { if user.role != Role::AreaManager && user.role != Role::Admin { return Err(ApplicationError::Unauthorized); @@ -48,9 +48,10 @@ pub async fn post_new( .await?; let registration = Registration::insert_new_for_user(pool.get_ref(), id).await?; - let message = email::build_registration_message(&user, ®istration.token); + let new_user = User::read_by_id(pool.get_ref(), id).await?; + let message = email::build_registration_message(&new_user, ®istration.token)?; - mailer.send(&message).unwrap(); + mailer.send(&message)?; Ok(HttpResponse::Found() .insert_header((LOCATION, "/users")) diff --git a/src/endpoints/user/post_register.rs b/src/endpoints/user/post_register.rs index d6995900..f639cd77 100644 --- a/src/endpoints/user/post_register.rs +++ b/src/endpoints/user/post_register.rs @@ -23,7 +23,8 @@ async fn post( pool: web::Data, ) -> Result { let is_dry = form.dry.unwrap_or(false); - let token = Registration::does_token_exist(pool.get_ref(), &form.token).await?; + // TODO: flip unauthorized with not found or unwrap result in a other way + let token = Registration::does_token_exist(pool.get_ref(), &form.token).await?.ok_or(ApplicationError::Unauthorized)?; if form.password.chars().count() > 256 { if is_dry { diff --git a/src/middleware/redirect_to_login.rs b/src/middleware/redirect_to_login.rs index 2d3cdd69..7723ebce 100644 --- a/src/middleware/redirect_to_login.rs +++ b/src/middleware/redirect_to_login.rs @@ -52,6 +52,7 @@ where && request.path() != "/login" && request.path() != "/imprint" && !request.path().starts_with("/reset-password") + && !request.path().starts_with("/register") && !request.path().starts_with("/static") { let (request, _pl) = request.into_parts(); diff --git a/src/models/area.rs b/src/models/area.rs index b4bb06d3..873249ae 100644 --- a/src/models/area.rs +++ b/src/models/area.rs @@ -1,6 +1,6 @@ use sqlx::{query, query_as, PgPool}; -#[derive(Clone)] +#[derive(Clone, Debug)] pub struct Area { pub id: i32, pub name: String @@ -9,7 +9,7 @@ pub struct Area { impl Area { pub async fn create(pool: &PgPool, name: &str) -> anyhow::Result { let result = query!("INSERT INTO area (name) VALUES ($1) RETURNING id;", name).fetch_one(pool).await?; - + Ok(result.id) } diff --git a/src/models/registration.rs b/src/models/registration.rs index f5371e30..3192697d 100644 --- a/src/models/registration.rs +++ b/src/models/registration.rs @@ -30,13 +30,13 @@ impl Registration { Ok(inserted) } - pub async fn does_token_exist(pool: &PgPool, token: &str) -> Result { + pub async fn does_token_exist(pool: &PgPool, token: &str) -> Result> { query_as!( Registration, "SELECT * FROM registration WHERE token = $1 AND expires > NOW();", token ) - .fetch_one(pool) + .fetch_optional(pool) .await } diff --git a/src/models/user.rs b/src/models/user.rs index febd0511..cf35b285 100644 --- a/src/models/user.rs +++ b/src/models/user.rs @@ -3,7 +3,7 @@ use sqlx::PgPool; use super::{Area, Function, Result, Role}; -#[derive(Clone)] +#[derive(Clone, Debug)] pub struct User { pub id: i32, pub name: String, @@ -366,7 +366,7 @@ impl User { Ok(()) } - pub async fn delete(pool: &PgPool, id: i32) -> anyhow::Result<()> { + pub async fn delete(pool: &PgPool, id: i32) -> Result<()> { sqlx::query!("DELETE FROM user_ WHERE id = $1;", id) .execute(pool) .await?; diff --git a/src/utils/application_error.rs b/src/utils/application_error.rs index f47454c0..47d47ca6 100644 --- a/src/utils/application_error.rs +++ b/src/utils/application_error.rs @@ -9,6 +9,14 @@ pub enum ApplicationError { Unauthorized, #[error("database error")] Database(#[from] sqlx::Error), + #[error("environment variable not present or not unicode")] + EnvVariable(#[from] std::env::VarError), + #[error("email address not conform")] + EmailAdress(#[from] lettre::address::AddressError), + #[error("email content not right")] + Email(#[from] lettre::error::Error), + #[error("email transport not working")] + EmailTransport(#[from] lettre::transport::smtp::Error), } impl actix_web::error::ResponseError for ApplicationError { @@ -17,10 +25,24 @@ impl actix_web::error::ResponseError for ApplicationError { ApplicationError::UnsupportedEnumValue { .. } => StatusCode::BAD_REQUEST, ApplicationError::Unauthorized { .. } => StatusCode::UNAUTHORIZED, ApplicationError::Database(_) => StatusCode::INTERNAL_SERVER_ERROR, + ApplicationError::EnvVariable(_) => StatusCode::INTERNAL_SERVER_ERROR, + ApplicationError::EmailAdress(_) => StatusCode::INTERNAL_SERVER_ERROR, + ApplicationError::Email(_) => StatusCode::INTERNAL_SERVER_ERROR, + ApplicationError::EmailTransport(_) => StatusCode::INTERNAL_SERVER_ERROR, } } fn error_response(&self) -> HttpResponse { - HttpResponse::build(self.status_code()).body(self.to_string()) + let mut response = HttpResponse::build(self.status_code()); + + match self { + ApplicationError::UnsupportedEnumValue { .. } => response.body(self.to_string()), + ApplicationError::Unauthorized { .. } => response.body(self.to_string()), + ApplicationError::Database(e) => response.body(format!("{self} - {e}")), + ApplicationError::EnvVariable(_) => response.body(self.to_string()), + ApplicationError::EmailAdress(e) => response.body(format!("{self} - {e}")), + ApplicationError::Email(e) => response.body(format!("{self} - {e}")), + ApplicationError::EmailTransport(e) => response.body(format!("{self} - {e}")), + } } } diff --git a/src/utils/email.rs b/src/utils/email.rs index dd86c053..9a45b8fc 100644 --- a/src/utils/email.rs +++ b/src/utils/email.rs @@ -1,13 +1,15 @@ use std::env; use lettre::{ - message::{header::ContentType, MultiPart, SinglePart}, + message::{header::ContentType, Mailbox, MultiPart, SinglePart}, transport::smtp::{authentication::Credentials, extension::ClientId}, Message, SmtpTransport, }; use crate::models::User; +use super::ApplicationError; + pub fn get_mailer() -> anyhow::Result { let server = &env::var("SMTP_SERVER")?; let port = &env::var("SMTP_PORT")?.parse()?; @@ -79,14 +81,15 @@ Viele Grüße"##, user.name)) return message; } -pub fn build_registration_message(user: &User, token: &str) -> Message { - let hostname = env::var("HOSTNAME").unwrap(); +pub fn build_registration_message(user: &User, token: &str) -> Result { + let hostname = env::var("HOSTNAME")?; let register_url = format!("https://{hostname}/register?token={token}"); + println!("{user:?}"); let message = Message::builder() - .from("noreply ".parse().unwrap()) - .reply_to("noreply ".parse().unwrap()) - .to(format!("{} <{}>", user.name, user.email).parse().unwrap()) + .from("noreply ".parse()?) + .reply_to("noreply ".parse()?) + .to(Mailbox::new(Some(user.name.clone()), user.email.parse()?)) .subject("Brass: Registrierung deines Accounts") .multipart( MultiPart::alternative() @@ -116,7 +119,7 @@ Viele Grüße"##, user.name))

Viele Grüße

"##, user.name)) )) - .unwrap(); + ?; - return message; + Ok(message) } diff --git a/templates/user/change_password.html b/templates/user/change_password.html index da5f6e08..844df4c3 100644 --- a/templates/user/change_password.html +++ b/templates/user/change_password.html @@ -13,7 +13,7 @@
-