Send logout notification when deleting a user (#7839)

This commit is contained in:
Cassian433 authored and GitHub committed 2026-10-29 23:16:14 +01:00
1 parent f1c7eccd4a
commit e056359a42
5 files changed
+51 -15

No files matched your search

+11 -3
View File
@@ -17,7 +17,7 @@ use crate::{
api::{ api::{
ApiResult, EmptyResult, JsonResult, Notify, ApiResult, EmptyResult, JsonResult, Notify,
core::{log_event, two_factor}, core::{log_event, two_factor},
unregister_push_device, push_logout_to_relay, unregister_push_device,
}, },
auth::{ClientIp, Secure, decode_admin, encode_jwt, generate_admin_claims}, auth::{ClientIp, Secure, decode_admin, encode_jwt, generate_admin_claims},
config::ConfigBuilder, config::ConfigBuilder,
@@ -419,13 +419,21 @@ async fn get_user_json(user_id: UserId, _token: AdminToken, conn: DbConn) -> Jso
} }
#[post("/users/<user_id>/delete", format = "application/json")] #[post("/users/<user_id>/delete", format = "application/json")]
async fn delete_user(user_id: UserId, token: AdminToken, conn: DbConn) -> EmptyResult { async fn delete_user(user_id: UserId, token: AdminToken, conn: DbConn, nt: Notify<'_>) -> EmptyResult {
let user = get_user_or_404(&user_id, &conn).await?; let user = get_user_or_404(&user_id, &conn).await?;
// Get the membership records before deleting the actual user // Get the membership records and push devices before deleting the actual user
let memberships = Membership::find_any_state_by_user(&user_id, &conn).await; let memberships = Membership::find_any_state_by_user(&user_id, &conn).await;
let has_push_device = CONFIG.push_enabled() && Device::check_user_has_push_device(&user_id, &conn).await;
let res = user.delete(&conn).await; let res = user.delete(&conn).await;
if res.is_ok() {
nt.send_logout(&user, None, &conn).await;
if has_push_device {
push_logout_to_relay(&user, None);
}
}
for membership in memberships { for membership in memberships {
log_event( log_event(
EventType::OrganizationUserDeleted, EventType::OrganizationUserDeleted,
+29 -7
View File
@@ -13,7 +13,7 @@ use crate::{
api::{ api::{
AnonymousNotify, ApiResult, EmptyResult, JsonResult, Notify, PasswordOrOtpData, UpdateType, AnonymousNotify, ApiResult, EmptyResult, JsonResult, Notify, PasswordOrOtpData, UpdateType,
core::{accept_org_invite, log_user_event, two_factor::email}, core::{accept_org_invite, log_user_event, two_factor::email},
master_password_policy, register_push_device, unregister_push_device, master_password_policy, push_logout_to_relay, register_push_device, unregister_push_device,
}, },
auth::{ClientHeaders, ClientIp, Headers, decode_delete, decode_invite, decode_verify_email}, auth::{ClientHeaders, ClientIp, Headers, decode_delete, decode_invite, decode_verify_email},
crypto, crypto,
@@ -1301,7 +1301,7 @@ struct DeleteRecoverTokenData {
} }
#[post("/accounts/delete-recover-token", data = "<data>")] #[post("/accounts/delete-recover-token", data = "<data>")]
async fn post_delete_recover_token(data: Json<DeleteRecoverTokenData>, conn: DbConn) -> EmptyResult { async fn post_delete_recover_token(data: Json<DeleteRecoverTokenData>, conn: DbConn, nt: Notify<'_>) -> EmptyResult {
let data: DeleteRecoverTokenData = data.into_inner(); let data: DeleteRecoverTokenData = data.into_inner();
let Ok(claims) = decode_delete(&data.token) else { let Ok(claims) = decode_delete(&data.token) else {
@@ -1315,22 +1315,44 @@ async fn post_delete_recover_token(data: Json<DeleteRecoverTokenData>, conn: DbC
if claims.sub != *user.uuid { if claims.sub != *user.uuid {
err!("Invalid claim"); err!("Invalid claim");
} }
user.delete(&conn).await
// The devices are deleted together with the user, so check for push devices beforehand
let has_push_device = CONFIG.push_enabled() && Device::check_user_has_push_device(&user.uuid, &conn).await;
user.delete(&conn).await?;
nt.send_logout(&user, None, &conn).await;
if has_push_device {
push_logout_to_relay(&user, None);
}
Ok(())
} }
#[post("/accounts/delete", data = "<data>")] #[post("/accounts/delete", data = "<data>")]
async fn post_delete_account(data: Json<PasswordOrOtpData>, headers: Headers, conn: DbConn) -> EmptyResult { async fn post_delete_account(
delete_account(data, headers, conn).await data: Json<PasswordOrOtpData>,
headers: Headers,
conn: DbConn,
nt: Notify<'_>,
) -> EmptyResult {
delete_account(data, headers, conn, nt).await
} }
#[delete("/accounts", data = "<data>")] #[delete("/accounts", data = "<data>")]
async fn delete_account(data: Json<PasswordOrOtpData>, headers: Headers, conn: DbConn) -> EmptyResult { async fn delete_account(data: Json<PasswordOrOtpData>, headers: Headers, conn: DbConn, nt: Notify<'_>) -> EmptyResult {
let data: PasswordOrOtpData = data.into_inner(); let data: PasswordOrOtpData = data.into_inner();
let user = headers.user; let user = headers.user;
data.validate(&user, true, &conn).await?; data.validate(&user, true, &conn).await?;
user.delete(&conn).await // The devices are deleted together with the user, so check for push devices beforehand
let has_push_device = CONFIG.push_enabled() && Device::check_user_has_push_device(&user.uuid, &conn).await;
user.delete(&conn).await?;
nt.send_logout(&user, None, &conn).await;
if has_push_device {
push_logout_to_relay(&user, None);
}
Ok(())
} }
#[expect(clippy::needless_pass_by_value, reason = "Not beneficial for Headers")] #[expect(clippy::needless_pass_by_value, reason = "Not beneficial for Headers")]
+2 -2
View File
@@ -25,8 +25,8 @@ pub use crate::api::{
notifications::routes as notifications_routes, notifications::routes as notifications_routes,
notifications::{AnonymousNotify, Notify, UpdateType, WS_ANONYMOUS_SUBSCRIPTIONS, WS_USERS}, notifications::{AnonymousNotify, Notify, UpdateType, WS_ANONYMOUS_SUBSCRIPTIONS, WS_USERS},
push::{ push::{
push_cipher_update, push_folder_update, push_logout, push_send_update, push_user_update, register_push_device, push_cipher_update, push_folder_update, push_logout, push_logout_to_relay, push_send_update, push_user_update,
unregister_push_device, register_push_device, unregister_push_device,
}, },
web::catchers as web_catchers, web::catchers as web_catchers,
web::routes as web_routes, web::routes as web_routes,
+7 -1
View File
@@ -194,6 +194,13 @@ pub async fn push_cipher_update(ut: UpdateType, cipher: &Cipher, device: &Device
pub async fn push_logout(user: &User, acting_device: Option<&Device>, conn: &DbConn) { pub async fn push_logout(user: &User, acting_device: Option<&Device>, conn: &DbConn) {
if Device::check_user_has_push_device(&user.uuid, conn).await { if Device::check_user_has_push_device(&user.uuid, conn).await {
push_logout_to_relay(user, acting_device);
}
}
// Sends the logout without checking for push devices first.
// Needed after deleting a user, when the devices are already gone from the database.
pub fn push_logout_to_relay(user: &User, acting_device: Option<&Device>) {
tokio::task::spawn(send_to_push_relay(json!({ tokio::task::spawn(send_to_push_relay(json!({
"userId": user.uuid, "userId": user.uuid,
"organizationId": (), "organizationId": (),
@@ -207,7 +214,6 @@ pub async fn push_logout(user: &User, acting_device: Option<&Device>, conn: &DbC
"clientType": null, "clientType": null,
"installationId": null "installationId": null
}))); })));
}
} }
pub async fn push_user_update(ut: UpdateType, user: &User, push_uuid: Option<&PushId>, conn: &DbConn) { pub async fn push_user_update(ut: UpdateType, user: &User, push_uuid: Option<&PushId>, conn: &DbConn) {
+2 -2
View File
@@ -362,7 +362,7 @@ impl User {
} }
} }
pub async fn delete(self, conn: &DbConn) -> EmptyResult { pub async fn delete(&self, conn: &DbConn) -> EmptyResult {
for member in Membership::find_confirmed_by_user(&self.uuid, conn).await { for member in Membership::find_confirmed_by_user(&self.uuid, conn).await {
if member.atype == MembershipType::Owner if member.atype == MembershipType::Owner
&& Membership::count_confirmed_by_org_and_type(&member.org_uuid, MembershipType::Owner, conn).await <= 1 && Membership::count_confirmed_by_org_and_type(&member.org_uuid, MembershipType::Owner, conn).await <= 1
@@ -385,7 +385,7 @@ impl User {
Invitation::take(&self.email, conn).await; // Delete invitation if any Invitation::take(&self.email, conn).await; // Delete invitation if any
conn.run(move |conn| { conn.run(move |conn| {
diesel::delete(users::table.filter(users::uuid.eq(self.uuid))).execute(conn).map_res("Error deleting user") diesel::delete(users::table.filter(users::uuid.eq(&self.uuid))).execute(conn).map_res("Error deleting user")
}) })
.await .await
} }