From b30819af3629df1a4961bd79f82f00c51ba4e79c Mon Sep 17 00:00:00 2001 From: Kyattsukuro Date: Mon, 24 Nov 2025 19:41:12 +0100 Subject: [PATCH 1/3] adds sso_identifier to /admin/users --- src/api/admin.rs | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/api/admin.rs b/src/api/admin.rs index d36da8f9..4e67ed58 100644 --- a/src/api/admin.rs +++ b/src/api/admin.rs @@ -351,7 +351,7 @@ fn logout(cookies: &CookieJar<'_>) -> Redirect { async fn get_users_json(_token: AdminToken, conn: DbConn) -> Json { let users = User::get_all(&conn).await; let mut users_json = Vec::with_capacity(users.len()); - for (u, _) in users { + for (u, sso_u) in users { let mut usr = u.to_json(&conn).await; usr["userEnabled"] = json!(u.enabled); usr["createdAt"] = json!(format_naive_datetime_local(&u.created_at, DT_FMT)); @@ -359,9 +359,10 @@ async fn get_users_json(_token: AdminToken, conn: DbConn) -> Json { Some(dt) => json!(format_naive_datetime_local(&dt, DT_FMT)), None => json!(None::), }; + usr["sso_identifier"] = json!(sso_u.map(|u| u.identifier.to_string()).unwrap_or(String::new())); + users_json.push(usr); } - Json(Value::Array(users_json)) } From 3fa90cc10d6b95b4f985f926dbfb02d0f66b8b71 Mon Sep 17 00:00:00 2001 From: Kyattsukuro Date: Thu, 27 Nov 2025 20:26:49 +0100 Subject: [PATCH 2/3] return same json object for all user queries --- src/api/admin.rs | 78 +++++++++++++++++++------------------------ src/db/models/user.rs | 11 ++++++ 2 files changed, 45 insertions(+), 44 deletions(-) diff --git a/src/api/admin.rs b/src/api/admin.rs index 4e67ed58..5c34f63f 100644 --- a/src/api/admin.rs +++ b/src/api/admin.rs @@ -23,7 +23,7 @@ use crate::{ backup_sqlite, get_sql_server_version, models::{ Attachment, Cipher, Collection, Device, Event, EventType, Group, Invitation, Membership, MembershipId, - MembershipType, OrgPolicy, Organization, OrganizationId, SsoUser, TwoFactor, User, UserId, + MembershipType, OrgPolicy, Organization, OrganizationId, SsoUser, TwoFactor, User, UserId }, DbConn, DbConnType, ACTIVE_DB_TYPE, }, @@ -296,8 +296,8 @@ struct InviteData { email: String, } -async fn get_user_or_404(user_id: &UserId, conn: &DbConn) -> ApiResult { - if let Some(user) = User::find_by_uuid(user_id, conn).await { +async fn get_user_or_404(user_id: &UserId, conn: &DbConn) -> ApiResult<(User, Option)> { + if let Some(user) = SsoUser::find_by_uuid(user_id, conn).await { Ok(user) } else { err_code!("User doesn't exist", Status::NotFound.code); @@ -347,57 +347,48 @@ fn logout(cookies: &CookieJar<'_>) -> Redirect { Redirect::to(admin_path()) } -#[get("/users")] -async fn get_users_json(_token: AdminToken, conn: DbConn) -> Json { - let users = User::get_all(&conn).await; +async fn get_users_property(users: Vec<(User, Option)>, conn: &DbConn) -> Vec { let mut users_json = Vec::with_capacity(users.len()); for (u, sso_u) in users { - let mut usr = u.to_json(&conn).await; - usr["userEnabled"] = json!(u.enabled); - usr["createdAt"] = json!(format_naive_datetime_local(&u.created_at, DT_FMT)); - usr["lastActive"] = match u.last_active(&conn).await { + let mut usr = u.to_json(conn).await; + usr["cipher_count"] = json!(Cipher::count_owned_by_user(&u.uuid, conn).await); + usr["attachment_count"] = json!(Attachment::count_by_user(&u.uuid, conn).await); + usr["attachment_size"] = json!(get_display_size(Attachment::size_by_user(&u.uuid, conn).await)); + usr["user_enabled"] = json!(u.enabled); + usr["created_at"] = json!(format_naive_datetime_local(&u.created_at, DT_FMT)); + usr["last_active"] = match u.last_active(conn).await { Some(dt) => json!(format_naive_datetime_local(&dt, DT_FMT)), - None => json!(None::), + None => json!("Never"), }; + usr["sso_identifier"] = json!(sso_u.map(|u| u.identifier.to_string()).unwrap_or(String::new())); users_json.push(usr); } + return users_json +} + +#[get("/users")] +async fn get_users_json(_token: AdminToken, conn: DbConn) -> Json { + let users = User::get_all(&conn).await; + let users_json = get_users_property(users, &conn).await; Json(Value::Array(users_json)) } + #[get("/users/overview")] async fn users_overview(_token: AdminToken, conn: DbConn) -> ApiResult> { let users = User::get_all(&conn).await; - let mut users_json = Vec::with_capacity(users.len()); - for (u, sso_u) in users { - let mut usr = u.to_json(&conn).await; - usr["cipher_count"] = json!(Cipher::count_owned_by_user(&u.uuid, &conn).await); - usr["attachment_count"] = json!(Attachment::count_by_user(&u.uuid, &conn).await); - usr["attachment_size"] = json!(get_display_size(Attachment::size_by_user(&u.uuid, &conn).await)); - usr["user_enabled"] = json!(u.enabled); - usr["created_at"] = json!(format_naive_datetime_local(&u.created_at, DT_FMT)); - usr["last_active"] = match u.last_active(&conn).await { - Some(dt) => json!(format_naive_datetime_local(&dt, DT_FMT)), - None => json!("Never"), - }; - - usr["sso_identifier"] = json!(sso_u.map(|u| u.identifier.to_string()).unwrap_or(String::new())); - - users_json.push(usr); - } - + let users_json = get_users_property(users, &conn).await; let text = AdminTemplateData::new("admin/users", json!(users_json)).render()?; Ok(Html(text)) } #[get("/users/by-mail/")] async fn get_user_by_mail_json(mail: &str, _token: AdminToken, conn: DbConn) -> JsonResult { - if let Some(u) = User::find_by_mail(mail, &conn).await { - let mut usr = u.to_json(&conn).await; - usr["userEnabled"] = json!(u.enabled); - usr["createdAt"] = json!(format_naive_datetime_local(&u.created_at, DT_FMT)); - Ok(Json(usr)) + if let Some((u, sso)) = SsoUser::find_by_mail(mail, &conn).await { + let user_json = get_users_property(vec!((u, sso)), &conn).await[0].clone(); + Ok(Json(user_json)) } else { err_code!("User doesn't exist", Status::NotFound.code); } @@ -405,16 +396,15 @@ async fn get_user_by_mail_json(mail: &str, _token: AdminToken, conn: DbConn) -> #[get("/users/")] async fn get_user_json(user_id: UserId, _token: AdminToken, conn: DbConn) -> JsonResult { - let u = get_user_or_404(&user_id, &conn).await?; - let mut usr = u.to_json(&conn).await; - usr["userEnabled"] = json!(u.enabled); - usr["createdAt"] = json!(format_naive_datetime_local(&u.created_at, DT_FMT)); - Ok(Json(usr)) + let u_sso = get_user_or_404(&user_id, &conn).await?; + let user_json = get_users_property(vec!(u_sso), &conn).await[0].clone(); + + Ok(Json(user_json)) } #[post("/users//delete", format = "application/json")] async fn delete_user(user_id: UserId, token: AdminToken, conn: DbConn) -> 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 let memberships = Membership::find_any_state_by_user(&user_id, &conn).await; @@ -459,7 +449,7 @@ async fn delete_sso_user(user_id: UserId, token: AdminToken, conn: DbConn) -> Em #[post("/users//deauth", format = "application/json")] async fn deauth_user(user_id: UserId, _token: AdminToken, conn: DbConn, nt: Notify<'_>) -> EmptyResult { - let mut user = get_user_or_404(&user_id, &conn).await?; + let (mut user, _) = get_user_or_404(&user_id, &conn).await?; nt.send_logout(&user, None, &conn).await; @@ -480,7 +470,7 @@ async fn deauth_user(user_id: UserId, _token: AdminToken, conn: DbConn, nt: Noti #[post("/users//disable", format = "application/json")] async fn disable_user(user_id: UserId, _token: AdminToken, conn: DbConn, nt: Notify<'_>) -> EmptyResult { - let mut user = get_user_or_404(&user_id, &conn).await?; + let (mut user, _) = get_user_or_404(&user_id, &conn).await?; Device::delete_all_by_user(&user.uuid, &conn).await?; user.reset_security_stamp(); user.enabled = false; @@ -494,7 +484,7 @@ async fn disable_user(user_id: UserId, _token: AdminToken, conn: DbConn, nt: Not #[post("/users//enable", format = "application/json")] async fn enable_user(user_id: UserId, _token: AdminToken, conn: DbConn) -> EmptyResult { - let mut user = get_user_or_404(&user_id, &conn).await?; + let (mut user, _) = get_user_or_404(&user_id, &conn).await?; user.enabled = true; user.save(&conn).await @@ -502,7 +492,7 @@ async fn enable_user(user_id: UserId, _token: AdminToken, conn: DbConn) -> Empty #[post("/users//remove-2fa", format = "application/json")] async fn remove_2fa(user_id: UserId, token: AdminToken, conn: DbConn) -> EmptyResult { - let mut user = get_user_or_404(&user_id, &conn).await?; + let (mut user, _) = get_user_or_404(&user_id, &conn).await?; TwoFactor::delete_all_by_user(&user.uuid, &conn).await?; two_factor::enforce_2fa_policy(&user, &ACTING_ADMIN_USER.into(), 14, &token.ip.ip, &conn).await?; user.totp_recover = None; diff --git a/src/db/models/user.rs b/src/db/models/user.rs index c7f4e1bc..c76625e4 100644 --- a/src/db/models/user.rs +++ b/src/db/models/user.rs @@ -527,6 +527,17 @@ impl SsoUser { }} } + pub async fn find_by_uuid(uuid: &UserId, conn: &DbConn) -> Option<(User, Option)> { + db_run! { conn: { + users::table + .left_join(sso_users::table) + .select(<(User, Option)>::as_select()) + .filter(users::uuid.eq(uuid)) + .first::<(User, Option)>(conn) + .ok() + }} + } + pub async fn delete(user_uuid: &UserId, conn: &DbConn) -> EmptyResult { db_run! { conn: { diesel::delete(sso_users::table.filter(sso_users::user_uuid.eq(user_uuid))) From 745f7db5f48b922f643d1ccdaceaf6216edb4b64 Mon Sep 17 00:00:00 2001 From: Kyattsukuro Date: Sat, 29 Nov 2025 19:40:36 +0100 Subject: [PATCH 3/3] pass formatting checks --- src/api/admin.rs | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) diff --git a/src/api/admin.rs b/src/api/admin.rs index 5c34f63f..51d0e73c 100644 --- a/src/api/admin.rs +++ b/src/api/admin.rs @@ -23,7 +23,7 @@ use crate::{ backup_sqlite, get_sql_server_version, models::{ Attachment, Cipher, Collection, Device, Event, EventType, Group, Invitation, Membership, MembershipId, - MembershipType, OrgPolicy, Organization, OrganizationId, SsoUser, TwoFactor, User, UserId + MembershipType, OrgPolicy, Organization, OrganizationId, SsoUser, TwoFactor, User, UserId, }, DbConn, DbConnType, ACTIVE_DB_TYPE, }, @@ -347,7 +347,7 @@ fn logout(cookies: &CookieJar<'_>) -> Redirect { Redirect::to(admin_path()) } -async fn get_users_property(users: Vec<(User, Option)>, conn: &DbConn) -> Vec { +async fn get_users_property(users: Vec<(User, Option)>, conn: &DbConn) -> Vec { let mut users_json = Vec::with_capacity(users.len()); for (u, sso_u) in users { let mut usr = u.to_json(conn).await; @@ -365,7 +365,7 @@ async fn get_users_property(users: Vec<(User, Option)>, conn: &DbConn) users_json.push(usr); } - return users_json + users_json } #[get("/users")] @@ -375,7 +375,6 @@ async fn get_users_json(_token: AdminToken, conn: DbConn) -> Json { Json(Value::Array(users_json)) } - #[get("/users/overview")] async fn users_overview(_token: AdminToken, conn: DbConn) -> ApiResult> { let users = User::get_all(&conn).await; @@ -387,7 +386,7 @@ async fn users_overview(_token: AdminToken, conn: DbConn) -> ApiResult")] async fn get_user_by_mail_json(mail: &str, _token: AdminToken, conn: DbConn) -> JsonResult { if let Some((u, sso)) = SsoUser::find_by_mail(mail, &conn).await { - let user_json = get_users_property(vec!((u, sso)), &conn).await[0].clone(); + let user_json = get_users_property(vec![(u, sso)], &conn).await[0].clone(); Ok(Json(user_json)) } else { err_code!("User doesn't exist", Status::NotFound.code); @@ -397,7 +396,7 @@ async fn get_user_by_mail_json(mail: &str, _token: AdminToken, conn: DbConn) -> #[get("/users/")] async fn get_user_json(user_id: UserId, _token: AdminToken, conn: DbConn) -> JsonResult { let u_sso = get_user_or_404(&user_id, &conn).await?; - let user_json = get_users_property(vec!(u_sso), &conn).await[0].clone(); + let user_json = get_users_property(vec![u_sso], &conn).await[0].clone(); Ok(Json(user_json)) }