feat: user profile page
This commit is contained in:
parent
33baf479b5
commit
7d47b38037
@ -37,6 +37,10 @@ pub fn init(cfg: &mut ServiceConfig) {
|
||||
cfg.service(user::post_login::post);
|
||||
cfg.service(user::get_reset::get);
|
||||
cfg.service(user::post_reset::post);
|
||||
cfg.service(user::get_profile::get);
|
||||
cfg.service(user::post_toggle::post);
|
||||
cfg.service(user::get_changepassword::get);
|
||||
cfg.service(user::post_changepassword::post);
|
||||
|
||||
cfg.service(availability::delete::delete);
|
||||
cfg.service(availability::get_new::get);
|
||||
|
45
src/endpoints/user/get_changepassword.rs
Normal file
45
src/endpoints/user/get_changepassword.rs
Normal file
@ -0,0 +1,45 @@
|
||||
use actix_web::{HttpRequest, HttpResponse, Responder};
|
||||
|
||||
#[actix_web::get("/users/changepassword")]
|
||||
pub async fn get(request: HttpRequest) -> impl Responder {
|
||||
if let Some(_) = request.headers().get("HX-Request") {
|
||||
return HttpResponse::Ok().body(r##"
|
||||
<div class="field">
|
||||
<div class="control">
|
||||
<a href="/profile" hx-boost="true" class="button is-link is-light">Schließen</a>
|
||||
</div>
|
||||
</div>
|
||||
<form class="box" hx-post="/users/changepassword" hx-target-400="#error-message" hx-on:change="document.getElementById('error-message').innerHTML = ''">
|
||||
|
||||
<div class="field">
|
||||
<label class="label" for="currentpassword">aktuelles Passwort:</label>
|
||||
<div class="control">
|
||||
<input class="input" placeholder="**********" name="currentpassword" type="password" required>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="field">
|
||||
<label class="label" for="password">neues Passwort:</label>
|
||||
<div class="control">
|
||||
<input class="input" placeholder="**********" name="password" type="password" required>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="field">
|
||||
<label class="label" for="passwordretyped">neues Passwort wiederholen:</label>
|
||||
<div class="control">
|
||||
<input class="input" placeholder="**********" name="passwordretyped" type="password" required>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div id="error-message" class="mb-3 help is-danger"></div>
|
||||
|
||||
<div class="level">
|
||||
<input class="button is-primary level-left" type="submit" value="Passwort ändern" />
|
||||
</div>
|
||||
</form>
|
||||
"##);
|
||||
}
|
||||
|
||||
HttpResponse::NotFound().finish()
|
||||
}
|
24
src/endpoints/user/get_profile.rs
Normal file
24
src/endpoints/user/get_profile.rs
Normal file
@ -0,0 +1,24 @@
|
||||
use actix_web::{web, Responder};
|
||||
use askama::Template;
|
||||
use askama_actix::TemplateToResponse;
|
||||
use sqlx::PgPool;
|
||||
|
||||
use crate::models::{Area, Function, Role, User};
|
||||
|
||||
#[derive(Template)]
|
||||
#[template(path = "user/profile.html")]
|
||||
struct ProfileTemplate {
|
||||
user: User
|
||||
}
|
||||
|
||||
#[actix_web::get("/profile")]
|
||||
pub async fn get(user: web::ReqData<User>, pool: web::Data<PgPool>) -> impl Responder {
|
||||
let area = Area::read_by_id(pool.get_ref(), user.area_id).await.unwrap();
|
||||
|
||||
let mut user = user.into_inner();
|
||||
user.area = Some(area);
|
||||
|
||||
let template = ProfileTemplate { user };
|
||||
|
||||
return template.to_response();
|
||||
}
|
@ -10,3 +10,7 @@ pub mod get_login;
|
||||
pub mod post_login;
|
||||
pub mod get_reset;
|
||||
pub mod post_reset;
|
||||
pub mod get_profile;
|
||||
pub mod post_toggle;
|
||||
pub mod get_changepassword;
|
||||
pub mod post_changepassword;
|
||||
|
@ -13,32 +13,28 @@ use crate::{
|
||||
pub struct JsonPatchDoc {
|
||||
op: String,
|
||||
path: String,
|
||||
value: Value
|
||||
value: Value,
|
||||
}
|
||||
|
||||
#[actix_web::patch("/users/edit/{id}")]
|
||||
pub async fn patch(
|
||||
user: Identity,
|
||||
user: web::ReqData<User>,
|
||||
pool: web::Data<PgPool>,
|
||||
path: web::Path<IdPath>,
|
||||
patch_docs: web::Json<Vec<JsonPatchDoc>>,
|
||||
) -> 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();
|
||||
}
|
||||
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 current_user.role == Role::AreaManager && current_user.area_id != user_in_db.area_id {
|
||||
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" {
|
||||
@ -47,17 +43,40 @@ pub async fn patch(
|
||||
|
||||
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);
|
||||
}
|
||||
},
|
||||
_ => panic!("other patch paths are not supported!"),
|
||||
}
|
||||
"/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, locked).await {
|
||||
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 {
|
||||
|
49
src/endpoints/user/post_changepassword.rs
Normal file
49
src/endpoints/user/post_changepassword.rs
Normal file
@ -0,0 +1,49 @@
|
||||
use actix_web::{web, HttpResponse, Responder};
|
||||
use serde::Deserialize;
|
||||
use sqlx::PgPool;
|
||||
|
||||
use crate::{auth::utils, models::User};
|
||||
|
||||
#[derive(Deserialize)]
|
||||
struct ChangePasswordForm {
|
||||
currentpassword: String,
|
||||
password: String,
|
||||
passwordretyped: String,
|
||||
}
|
||||
|
||||
#[actix_web::post("/users/changepassword")]
|
||||
async fn post(
|
||||
user: web::ReqData<User>,
|
||||
form: web::Form<ChangePasswordForm>,
|
||||
pool: web::Data<PgPool>,
|
||||
) -> impl Responder {
|
||||
if user.password
|
||||
== utils::hash_plain_password_with_salt(&form.currentpassword, &user.salt).unwrap()
|
||||
{
|
||||
if form.password != form.passwordretyped {
|
||||
return HttpResponse::BadRequest().body("Passwörter stimmen nicht überein!");
|
||||
}
|
||||
|
||||
let (hash, salt) = utils::generate_salt_and_hash_plain_password(&form.password).unwrap();
|
||||
|
||||
User::update(
|
||||
pool.get_ref(),
|
||||
user.id,
|
||||
None,
|
||||
None,
|
||||
Some(&hash),
|
||||
Some(&salt),
|
||||
None,
|
||||
None,
|
||||
None,
|
||||
None,
|
||||
None,
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
return HttpResponse::Ok().body(r#"<div class="block">Passwort wurde geändert.</div>"#);
|
||||
} else {
|
||||
return HttpResponse::BadRequest().body("Aktuelles Passwort ist nicht korrekt!");
|
||||
}
|
||||
}
|
@ -83,7 +83,7 @@ pub async fn post_edit(
|
||||
};
|
||||
|
||||
if changed {
|
||||
match User::update(pool.get_ref(), path.id, email, name, None, None, role, function, area, None).await {
|
||||
match User::update(pool.get_ref(), path.id, email, name, None, None, role, function, area, None, None).await {
|
||||
Ok(_) => return HttpResponse::Found().insert_header((LOCATION, "/users")).finish(),
|
||||
Err(err) => println!("{}", err)
|
||||
}
|
||||
|
@ -91,6 +91,7 @@ async fn post(form: web::Form<ResetPasswordForm>, pool: web::Data<PgPool>) -> im
|
||||
None,
|
||||
None,
|
||||
None,
|
||||
None
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
|
70
src/endpoints/user/post_toggle.rs
Normal file
70
src/endpoints/user/post_toggle.rs
Normal file
@ -0,0 +1,70 @@
|
||||
use actix_web::{web, HttpResponse, Responder};
|
||||
use serde::Deserialize;
|
||||
use sqlx::PgPool;
|
||||
|
||||
use crate::{
|
||||
endpoints::IdPath,
|
||||
models::{Role, User},
|
||||
};
|
||||
|
||||
#[derive(Deserialize)]
|
||||
struct ToggleQuery {
|
||||
field: String,
|
||||
}
|
||||
|
||||
#[actix_web::post("/users/{id}/toggle")]
|
||||
pub async fn post(
|
||||
user: web::ReqData<User>,
|
||||
pool: web::Data<PgPool>,
|
||||
path: web::Path<IdPath>,
|
||||
query: web::Query<ToggleQuery>,
|
||||
) -> impl Responder {
|
||||
if user.id != path.id && (user.role != Role::Admin || user.role != Role::AreaManager) {
|
||||
return HttpResponse::Unauthorized().finish();
|
||||
}
|
||||
|
||||
let user = if user.id != path.id {
|
||||
User::read_by_id(pool.get_ref(), path.id).await.unwrap()
|
||||
} else {
|
||||
user.into_inner()
|
||||
};
|
||||
|
||||
match query.field.as_str() {
|
||||
"locked" => User::update(
|
||||
pool.get_ref(),
|
||||
user.id,
|
||||
None,
|
||||
None,
|
||||
None,
|
||||
None,
|
||||
None,
|
||||
None,
|
||||
None,
|
||||
None,
|
||||
Some(!user.locked),
|
||||
)
|
||||
.await
|
||||
.unwrap(),
|
||||
"receiveNotifications" => {
|
||||
User::update(
|
||||
pool.get_ref(),
|
||||
user.id,
|
||||
None,
|
||||
None,
|
||||
None,
|
||||
None,
|
||||
None,
|
||||
None,
|
||||
None,
|
||||
Some(!user.receive_notifications),
|
||||
None,
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
//return HttpResponse::Ok().body("<input />");
|
||||
}
|
||||
_ => return HttpResponse::BadRequest().body("Other PATCH paths are not supported!"),
|
||||
};
|
||||
|
||||
HttpResponse::Ok().finish()
|
||||
}
|
@ -275,6 +275,7 @@ impl User {
|
||||
role: Option<Role>,
|
||||
function: Option<Function>,
|
||||
area_id: Option<i32>,
|
||||
receive_notifications: Option<bool>,
|
||||
locked: Option<bool>,
|
||||
) -> anyhow::Result<()> {
|
||||
let mut query_builder = sqlx::QueryBuilder::new("UPDATE user_ SET ");
|
||||
@ -315,6 +316,11 @@ impl User {
|
||||
separated.push_bind_unseparated(area_id);
|
||||
}
|
||||
|
||||
if let Some(receive_notifications) = receive_notifications {
|
||||
separated.push("receiveNotifications = ");
|
||||
separated.push_bind_unseparated(receive_notifications);
|
||||
}
|
||||
|
||||
if let Some(locked) = locked {
|
||||
separated.push("locked = ");
|
||||
separated.push_bind_unseparated(locked);
|
||||
|
@ -33,6 +33,16 @@
|
||||
top: -.05em;
|
||||
vertical-align: middle;
|
||||
}
|
||||
|
||||
.result {
|
||||
visibility: hidden;
|
||||
}
|
||||
|
||||
.fadeout {
|
||||
visibility: visible;
|
||||
opacity: 0;
|
||||
transition: opacity 2s ease-in;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
|
||||
|
@ -66,10 +66,10 @@
|
||||
<div class="navbar-item">
|
||||
angemeldet als {{ user.name }}
|
||||
<div class="buttons ml-3">
|
||||
<a class="button is-success">
|
||||
<a class="button is-success" hx-boost="true" href="/profile">
|
||||
<span class="icon">
|
||||
<svg class="feather">
|
||||
<use href="/static/feather-sprite.svg#check-circle" />
|
||||
<use href="/static/feather-sprite.svg#user" />
|
||||
</svg>
|
||||
</span>
|
||||
<span>Profil</span>
|
||||
|
@ -13,7 +13,8 @@
|
||||
<div class="field-body">
|
||||
<div class="field">
|
||||
<div class="control">
|
||||
<input class="input" type="text" name="email" placeholder="max.mustermann@brasiwa-leipzig.de" value="{{ email }}"/>
|
||||
<input class="input" type="text" name="email" placeholder="max.mustermann@brasiwa-leipzig.de"
|
||||
value="{{ email }}" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
89
templates/user/profile.html
Normal file
89
templates/user/profile.html
Normal file
@ -0,0 +1,89 @@
|
||||
{% extends "nav.html" %}
|
||||
|
||||
{% block content %}
|
||||
<section class="section">
|
||||
<div class="container">
|
||||
<h1 class="title">Profil</h1>
|
||||
|
||||
<div class="field">
|
||||
<label class="label">Name</label>
|
||||
<div class="control">
|
||||
<input class="input" type="text" value="{{ user.name }}" readonly>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="field">
|
||||
<label class="label">E-Mail</label>
|
||||
<div class="control">
|
||||
<input class="input" type="email" value="{{ user.name }}" readonly>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="field">
|
||||
<label class="label">Rolle</label>
|
||||
<div class="control">
|
||||
{% match user.role %}
|
||||
{% when Role::Staff %}
|
||||
<span class="tag is-info is-light">Nutzer</span>
|
||||
{% when Role::AreaManager %}
|
||||
<span class="tag is-info is-light">Bereichsleiter</span>
|
||||
{% when Role::Admin %}
|
||||
<span class="tag is-info">Admin</span>
|
||||
{% else %}
|
||||
{% endmatch %}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="field">
|
||||
<label class="label">Funktion</label>
|
||||
<div class="control">
|
||||
{% match user.function %}
|
||||
{% when Function::Posten %}
|
||||
<span class="tag is-info is-light">Posten</span>
|
||||
{% when Function::Wachhabender %}
|
||||
<span class="tag is-info">Wachhabender</span>
|
||||
{% else %}
|
||||
{% endmatch %}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="field">
|
||||
<label class="label">Bereich</label>
|
||||
<div class="control">
|
||||
<input class="input" type="text" value="{{ user.area.as_ref().unwrap().name }}" readonly>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
<div class="field">
|
||||
<div class="control">
|
||||
<label class="checkbox">
|
||||
<input hx-post="/users/{{ user.id }}/toggle?field=receiveNotifications" type="checkbox"
|
||||
hx-on::before-request="document.getElementById('success').classList.remove('fadeout')"
|
||||
hx-on::after-request="document.getElementById('success').classList.add('fadeout')"
|
||||
checked="{{ user.receive_notifications }}">
|
||||
Ich möchte E-Mail Benachrichtungen zu neuen Brasiwa-Einteilungen erhalten.
|
||||
<span id="success" class="result">
|
||||
<span class="icon mr-2">
|
||||
<svg class="feather">
|
||||
<use href="/static/feather-sprite.svg#check" />
|
||||
</svg>
|
||||
</span>
|
||||
<span>
|
||||
gespeichert
|
||||
</span>
|
||||
</span>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="field">
|
||||
<div class="control">
|
||||
<button hx-get="/users/changepassword" hx-target="closest div.field" hx-swap="outerHTML"
|
||||
class="button is-link is-light">Passwort ändern</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</section>
|
||||
{% endblock %}
|
Loading…
x
Reference in New Issue
Block a user