feat: registration of users

This commit is contained in:
Max Hohlfeld 2024-09-08 19:47:05 +02:00
parent 2374f78a07
commit ea35f5475f
13 changed files with 88 additions and 56 deletions

View File

@ -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
);

View File

@ -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);

View File

@ -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<PgPool>, path: web::Path<IdPath>) -> 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<User>,
pool: web::Data<PgPool>,
path: web::Path<IdPath>,
) -> Result<impl Responder, ApplicationError> {
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())
}

View File

@ -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<Identity>,
pool: web::Data<PgPool>,
query: web::Query<TokenQuery>,
) -> impl Responder {
) -> Result<impl Responder, ApplicationError> {
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())
}

View File

@ -22,7 +22,7 @@ pub async fn post_new(
user: web::ReqData<User>,
pool: web::Data<PgPool>,
form: web::Form<NewUserForm>,
mailer: web::ReqData<SmtpTransport>,
mailer: web::Data<SmtpTransport>,
) -> Result<impl Responder, ApplicationError> {
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, &registration.token);
let new_user = User::read_by_id(pool.get_ref(), id).await?;
let message = email::build_registration_message(&new_user, &registration.token)?;
mailer.send(&message).unwrap();
mailer.send(&message)?;
Ok(HttpResponse::Found()
.insert_header((LOCATION, "/users"))

View File

@ -23,7 +23,8 @@ async fn post(
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?;
// 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 {

View File

@ -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();

View File

@ -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<i32> {
let result = query!("INSERT INTO area (name) VALUES ($1) RETURNING id;", name).fetch_one(pool).await?;
Ok(result.id)
}

View File

@ -30,13 +30,13 @@ impl Registration {
Ok(inserted)
}
pub async fn does_token_exist(pool: &PgPool, token: &str) -> Result<Registration> {
pub async fn does_token_exist(pool: &PgPool, token: &str) -> Result<Option<Registration>> {
query_as!(
Registration,
"SELECT * FROM registration WHERE token = $1 AND expires > NOW();",
token
)
.fetch_one(pool)
.fetch_optional(pool)
.await
}

View File

@ -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?;

View File

@ -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}")),
}
}
}

View File

@ -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<SmtpTransport> {
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<Message, ApplicationError> {
let hostname = env::var("HOSTNAME")?;
let register_url = format!("https://{hostname}/register?token={token}");
println!("{user:?}");
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())
.from("noreply <noreply@brasiwa-leipzig.de>".parse()?)
.reply_to("noreply <noreply@brasiwa-leipzig.de>".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))
<p>Viele Grüße</p>"##, user.name))
))
.unwrap();
?;
return message;
Ok(message)
}

View File

@ -13,7 +13,7 @@
<div class="field">
<label class="label" for="password">{{ new_password_label }}</label>
<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="{{ endpoint }}?dry=true" hx-params="*" hx-trigger="keyup changed delay:500ms"
hx-target="#password-strength" hx-target-400="#password-strength" placeholder="**********" name="password"
type="password" required hx-swap="outerHTML" maxlength=256
hx-on:input="document.getElementById('password-strength').innerHTML = ''">