refactor: endpoint for user locking

refs #21
This commit is contained in:
Max Hohlfeld 2025-04-25 00:55:22 +02:00
parent 5446fcb128
commit 08646ba971
7 changed files with 176 additions and 111 deletions

View File

@ -43,7 +43,6 @@ pub fn init(cfg: &mut ServiceConfig) {
cfg.service(user::get_reset::get); cfg.service(user::get_reset::get);
cfg.service(user::post_reset::post); cfg.service(user::post_reset::post);
cfg.service(user::get_profile::get); cfg.service(user::get_profile::get);
cfg.service(user::post_toggle::post);
cfg.service(user::get_changepassword::get); cfg.service(user::get_changepassword::get);
cfg.service(user::post_changepassword::post); cfg.service(user::post_changepassword::post);
cfg.service(user::get_register::get); cfg.service(user::get_register::get);
@ -51,6 +50,8 @@ pub fn init(cfg: &mut ServiceConfig) {
cfg.service(user::post_resend_registration::post); cfg.service(user::post_resend_registration::post);
cfg.service(user::put_receive_notifications::put_subscribe); cfg.service(user::put_receive_notifications::put_subscribe);
cfg.service(user::put_receive_notifications::put_unsubscribe); cfg.service(user::put_receive_notifications::put_unsubscribe);
cfg.service(user::put_lock::put_lock_user);
cfg.service(user::put_lock::put_unlock_user);
cfg.service(availability::delete::delete); cfg.service(availability::delete::delete);
cfg.service(availability::get_new::get); cfg.service(availability::get_new::get);

View File

@ -22,7 +22,8 @@ pub mod post_new;
pub mod post_register; pub mod post_register;
pub mod post_resend_registration; pub mod post_resend_registration;
pub mod post_reset; pub mod post_reset;
pub mod post_toggle; pub mod put_receive_notifications;
pub mod put_lock;
#[derive(Template)] #[derive(Template)]
#[template(path = "user/new_or_edit.html")] #[template(path = "user/new_or_edit.html")]

View File

@ -1,74 +0,0 @@
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 {
// Todo: rewrite
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()
.unwrap()
} else {
user.into_inner()
};
match query.field.as_str() {
"locked" => {
User::update_locked(pool.get_ref(), user.id, !user.locked)
.await
.unwrap();
if !user.locked {
return HttpResponse::Ok().body(format!(
r##"<svg class="icon">
<use href="/static/feather-sprite.svg#unlock" />
</svg>
<span>Entsperren</span>
<div id="user-{id}-locked" hx-swap-oob="true">ja</div>
<button id="user-{id}-delete" hx-swap-oob="true" class="button is-danger is-light" hx-delete="/users/{id}" hx-target="closest tr" hx-swap="delete" hx-trigger="confirmed">
<svg class="icon">
<use href="/static/feather-sprite.svg#x-circle" />
</svg>
<span>Löschen</span>
</button>"##,
id = user.id));
} else {
return HttpResponse::Ok().body(format!(
r##"<svg class="icon">
<use href="/static/feather-sprite.svg#lock" />
</svg>
<span>Sperren</span>
<div id="user-{id}-locked" hx-swap-oob="true">nein</div>
<button id="user-{id}-delete" hx-swap-oob="true" class="button is-danger is-light" disabled hx-delete="/users/{id}" hx-target="closest tr" hx-swap="delete" hx-trigger="confirmed">
<svg class="icon">
<use href="/static/feather-sprite.svg#x-circle" />
</svg>
<span>Löschen</span>
</button>"##,
id = user.id));
}
}
_ => return HttpResponse::BadRequest().body("Other PATCH paths are not supported!"),
};
}

View File

@ -0,0 +1,128 @@
use actix_web::{web, HttpResponse, Responder};
use askama::Template;
use sqlx::PgPool;
use crate::{
endpoints::IdPath,
filters,
models::{Role, User},
utils::ApplicationError,
};
#[derive(Template, Debug)]
#[template(path = "user/overview_locked_td.html")]
struct OverviewPartialLockedTemplate {
u: User,
is_oob: bool,
}
#[derive(Template, Debug)]
#[template(path = "user/overview_buttons_td.html")]
struct OverviewPartialButtonsTemplate {
u: User,
}
#[actix_web::put("/users/{id}/lock")]
pub async fn put_lock_user(
user: web::ReqData<User>,
pool: web::Data<PgPool>,
path: web::Path<IdPath>,
) -> Result<impl Responder, ApplicationError> {
handle_lock_state_for_user(user, pool, path, true).await
}
#[actix_web::put("/users/{id}/unlock")]
pub async fn put_unlock_user(
user: web::ReqData<User>,
pool: web::Data<PgPool>,
path: web::Path<IdPath>,
) -> Result<impl Responder, ApplicationError> {
handle_lock_state_for_user(user, pool, path, false).await
}
async fn handle_lock_state_for_user(
user: web::ReqData<User>,
pool: web::Data<PgPool>,
path: web::Path<IdPath>,
lock_state: bool,
) -> Result<impl Responder, ApplicationError> {
if user.role != Role::AreaManager && user.role != Role::Admin {
return Err(ApplicationError::Unauthorized);
}
if user.id == path.id {
return Ok(HttpResponse::BadRequest().finish());
}
let Some(mut user_in_db) = User::read_by_id(pool.get_ref(), path.id).await? else {
return Ok(HttpResponse::NotFound().finish());
};
if user.role == Role::AreaManager
&& (user.area_id != user_in_db.area_id || user_in_db.role == Role::Admin)
{
return Err(ApplicationError::Unauthorized);
}
if user_in_db.locked != lock_state {
User::update_locked(pool.get_ref(), user_in_db.id, lock_state).await?;
user_in_db.locked = lock_state;
}
let buttons = OverviewPartialButtonsTemplate {
u: user_in_db.clone(),
};
let locked_oob = OverviewPartialLockedTemplate {
u: user_in_db,
is_oob: true,
};
let mut body = buttons.render()?;
body.push_str(&locked_oob.render()?);
Ok(HttpResponse::Ok().body(body))
}
// TODO: Tests schreiben
// #[cfg(test)]
// mod tests {
// use crate::utils::test_helper::{
// assert_snapshot, read_body, test_put, DbTestContext, RequestConfig, StatusCode,
// };
// use brass_macros::db_test;
//
// #[db_test]
// async fn user_can_toggle_subscription_for_himself(context: &DbTestContext) {
// let app = context.app().await;
//
// let unsubscribe_config = RequestConfig::new("/users/1/unsubscribeNotifications");
// let unsubscribe_response =
// test_put::<_, _, String>(&context.db_pool, &app, &unsubscribe_config, None).await;
//
// assert_eq!(StatusCode::OK, unsubscribe_response.status());
//
// let unsubscribe_body = read_body(unsubscribe_response).await;
// assert_snapshot!(unsubscribe_body);
//
// let subscribe_config = RequestConfig::new("/users/1/subscribeNotifications");
// let subscribe_response =
// test_put::<_, _, String>(&context.db_pool, &app, &subscribe_config, None).await;
//
// assert_eq!(StatusCode::OK, subscribe_response.status());
//
// let subscribe_body = read_body(subscribe_response).await;
// assert_snapshot!(subscribe_body);
// }
//
// #[db_test]
// async fn user_cant_toggle_subscription_for_someone_else(context: &DbTestContext) {
// let app = context.app().await;
//
// let unsubscribe_config = RequestConfig::new("/users/3/unsubscribeNotifications");
// let unsubscribe_response =
// test_put::<_, _, String>(&context.db_pool, &app, &unsubscribe_config, None).await;
//
// assert_eq!(StatusCode::UNAUTHORIZED, unsubscribe_response.status());
// }
// }

View File

@ -72,44 +72,15 @@
{% endif %} {% endif %}
</td> </td>
<td> <td>
<div id="user-{{ u.id }}-locked"> {% include "overview_locked_td.html" %}
{% if u.locked %}
ja
{% else %}
nein
{% endif %}
</div>
</td> </td>
<td> <td>
{% if user.id != u.id %} {% let current_user_is_area_manager_and_user_is_not_admin = user.role == Role::AreaManager && u.role !=
Role::Admin %}
{% if user.id != u.id && (user.role == Role::Admin || current_user_is_area_manager_and_user_is_not_admin)
%}
<div class="buttons is-right"> <div class="buttons is-right">
<button class="button is-link is-light" hx-post="/users/{{ u.id }}/toggle?field=locked"> {% include "overview_buttons_td.html" %}
<svg class="icon">
<use href="/static/feather-sprite.svg#{% if u.locked %}unlock{% else %}lock{% endif %}" />
</svg>
<span>{% if u.locked %}Entsperren{% else %}Sperren{% endif %}</span>
</button>
<a class="button is-primary is-light" hx-boost="true" href="/users/edit/{{ u.id }}">
<svg class="icon">
<use href="/static/feather-sprite.svg#edit" />
</svg>
<span>Bearbeiten</span>
</a>
<button id="user-{{ u.id }}-delete" class="button is-danger is-light" {% if !u.locked %}disabled{% endif
%} hx-delete="/users/{{ u.id }}" hx-target="closest tr" hx-swap="delete" hx-trigger="confirmed">
<svg class="icon">
<use href="/static/feather-sprite.svg#x-circle" />
</svg>
<span>Löschen</span>
</button>
{% if u.password.is_none() && u.salt.is_none() && u.last_login.is_none() %}
<button class="button is-warning is-light" hx-post="/users/{{ u.id }}/resend-registration">
<svg class="icon">
<use href="/static/feather-sprite.svg#send" />
</svg>
<span>Registrierungsmail erneut senden</span>
</button>
{% endif %}
</div> </div>
{% endif %} {% endif %}
</td> </td>

View File

@ -0,0 +1,31 @@
<button class="button is-link is-light" hx-put="/users/{{ u.id }}/{%- if u.locked -%}un{%- endif -%}lock"
hx-target="closest div" hx-swap="innerHTML">
<svg class="icon">
<use href="/static/feather-sprite.svg#{%- if u.locked -%}un{%- endif -%}lock" />
</svg>
<span>{%- if u.locked -%}Entsperren{%- else -%}Sperren{%- endif -%}</span>
</button>
<a class="button is-primary is-light" hx-boost="true" href="/users/edit/{{ u.id }}">
<svg class="icon">
<use href="/static/feather-sprite.svg#edit" />
</svg>
<span>Bearbeiten</span>
</a>
<button id="user-{{ u.id }}-delete" class="button is-danger is-light" {{ !u.locked|cond_show("disabled") }}
hx-delete="/users/{{ u.id }}" hx-target="closest tr" hx-swap="delete" hx-trigger="confirmed">
<svg class="icon">
<use href="/static/feather-sprite.svg#x-circle" />
</svg>
<span>Löschen</span>
</button>
{% if u.password.is_none() && u.salt.is_none() && u.last_login.is_none() %}
<button class="button is-warning is-light" hx-post="/users/{{ u.id }}/resend-registration">
<svg class="icon">
<use href="/static/feather-sprite.svg#send" />
</svg>
<span>Registrierungsmail erneut senden</span>
</button>
{% endif %}

View File

@ -0,0 +1,7 @@
<div id="user-{{ u.id }}-locked" {% if is_oob -%}hx-swap-oob="true" {%- endif -%}>
{%- if u.locked -%}
ja
{%- else -%}
nein
{%- endif -%}
</div>