fix(users): /api/admin/users always returns a FullUserDto[]
This commit is contained in:
@@ -108,14 +108,15 @@ pub struct AdminResetPasswordDto {
|
||||
pub new_password: String,
|
||||
}
|
||||
|
||||
/// Query parameters for listing users
|
||||
/// Query parameters for listing users. `/api/admin/users` used to
|
||||
/// bifurcate on `?summary=` (flat `PublicUserDto` vs nested
|
||||
/// `FullUserDto`); that split was retired — the endpoint now always
|
||||
/// returns `FullUserDto`. Unknown query params are ignored, so
|
||||
/// existing callers still passing `?summary=true` keep working.
|
||||
#[derive(Debug, Serialize, Deserialize)]
|
||||
pub struct ListUsersQueryDto {
|
||||
pub limit: Option<i64>,
|
||||
pub offset: Option<i64>,
|
||||
/// Return only the fields rendered by the paginated management table.
|
||||
/// Defaults to `false` so existing API clients keep the full user shape.
|
||||
pub summary: Option<bool>,
|
||||
}
|
||||
|
||||
/// Query parameters for the admin sessions listing.
|
||||
|
||||
@@ -3247,7 +3247,7 @@ impl AuthApplicationService {
|
||||
/// out so that internal-user surfaces — system address book, OCS
|
||||
/// sharee search, etc. — never expose external identities. Admin
|
||||
/// surfaces that need the full list should call
|
||||
/// [`list_users_including_external_with_perms`] instead.
|
||||
/// [`list_user_summaries_including_external_with_perms`] instead.
|
||||
pub async fn list_users(
|
||||
&self,
|
||||
limit: i64,
|
||||
@@ -3260,23 +3260,6 @@ impl AuthApplicationService {
|
||||
.collect())
|
||||
}
|
||||
|
||||
/// Admin-only: lists users including external (grant-only) recipients.
|
||||
/// Used by the admin user-management UI.
|
||||
pub async fn list_users_including_external_with_perms<A: AuthorizationEngine>(
|
||||
&self,
|
||||
authorization: &A,
|
||||
caller_id: Uuid,
|
||||
limit: i64,
|
||||
offset: i64,
|
||||
) -> Result<Vec<PublicUserDto>, DomainError> {
|
||||
self.require_admin_caller(authorization, caller_id).await?;
|
||||
let users = self.user_storage.list_users(limit, offset, true).await?;
|
||||
Ok(users
|
||||
.into_iter()
|
||||
.map(|u| PublicUserDto::new(u, false))
|
||||
.collect())
|
||||
}
|
||||
|
||||
/// Admin-only user listing. Returns `Vec<FullUserDto>` — same
|
||||
/// `FullUserDto` shape [`SelfUserDto`] embeds, so the FE reads
|
||||
/// admin table rows and `/me` responses through identical field
|
||||
@@ -3284,7 +3267,10 @@ impl AuthApplicationService {
|
||||
/// (`user.is_online`) so the admin table renders the vignette +
|
||||
/// green dot without per-row follow-up fetches to
|
||||
/// `/api/users/{id}` (the N+1 that motivated the widening — see
|
||||
/// `docs/plan/userdto-refactor.md` § N+1).
|
||||
/// `docs/plan/userdto-refactor.md` § N+1). This is the sole
|
||||
/// admin-visible listing path; the former flat
|
||||
/// `list_users_including_external_with_perms` variant was
|
||||
/// retired when `?summary` was dropped.
|
||||
pub async fn list_user_summaries_including_external_with_perms<A: AuthorizationEngine>(
|
||||
&self,
|
||||
authorization: &A,
|
||||
@@ -3362,8 +3348,8 @@ impl AuthApplicationService {
|
||||
// `interfaces/api/routes.rs::admin_router`) — but every admin
|
||||
// method here still calls `require_admin_caller` as a
|
||||
// defense-in-depth check, matching the pattern
|
||||
// `list_users_including_external_with_perms` established. If a
|
||||
// handler is ever wired outside the /admin subtree, the AuthZ
|
||||
// `list_user_summaries_including_external_with_perms` established.
|
||||
// If a handler is ever wired outside the /admin subtree, the AuthZ
|
||||
// still holds.
|
||||
|
||||
/// List sessions for the admin panel. `user_id_filter = Some(uuid)`
|
||||
|
||||
@@ -39,25 +39,14 @@ use crate::interfaces::middleware::auth::AuthUser;
|
||||
use std::sync::Arc;
|
||||
use uuid::Uuid;
|
||||
|
||||
#[derive(serde::Serialize)]
|
||||
#[serde(untagged)]
|
||||
enum AdminUsersPayload {
|
||||
/// Fat-`PublicUserDto` per row. Emitted when `?summary=false` — legacy
|
||||
/// path retained until the FE drops the `summary=false` query
|
||||
/// (rare; the SPA uses `summary=true` for the paginated table).
|
||||
Full(Vec<PublicUserDto>),
|
||||
/// `FullUserDto` per row — same shape one row of the /me
|
||||
/// response's embedded `full` carries. Emitted when
|
||||
/// `?summary=true`. The FE seeds `resolveUser` cache from
|
||||
/// `row.user` here (kills the per-row `/api/users/{id}` fetch).
|
||||
/// The old `AdminUserSummaryDto` returned here has been replaced
|
||||
/// by `FullUserDto`; see `docs/plan/userdto-refactor.md`.
|
||||
Summary(Vec<FullUserDto>),
|
||||
}
|
||||
|
||||
/// Response envelope for `GET /api/admin/users`. `users` is always
|
||||
/// `Vec<FullUserDto>` — same shape one row of `/me`'s embedded
|
||||
/// `full` block carries; the FE seeds `resolveUser` cache from
|
||||
/// `row.user` (kills the per-row `/api/users/{id}` fetch). See
|
||||
/// `docs/plan/userdto-refactor.md`.
|
||||
#[derive(serde::Serialize)]
|
||||
struct AdminUsersPageResponse {
|
||||
users: AdminUsersPayload,
|
||||
users: Vec<FullUserDto>,
|
||||
total: i64,
|
||||
limit: i64,
|
||||
offset: i64,
|
||||
@@ -1094,13 +1083,20 @@ pub async fn get_dashboard_stats(
|
||||
// ============================================================================
|
||||
|
||||
/// GET /api/admin/users?limit=50&offset=0 — list all users
|
||||
///
|
||||
/// Always returns `Vec<FullUserDto>` — the shape one row of the
|
||||
/// `/me` response's embedded `full` block carries. The former
|
||||
/// `?summary` toggle (flat `PublicUserDto` vs nested `FullUserDto`)
|
||||
/// has been retired: admin listing is low-volume and the FE always
|
||||
/// asked for the nested shape anyway, so the two-shape split served
|
||||
/// no caller and only invited jq-path bugs. See
|
||||
/// `docs/plan/userdto-refactor.md`.
|
||||
#[utoipa::path(
|
||||
get,
|
||||
path = "/api/admin/users",
|
||||
params(
|
||||
("limit" = Option<i64>, Query, description = "Max users to return (default 100, max 500)"),
|
||||
("offset" = Option<i64>, Query, description = "Pagination offset"),
|
||||
("summary" = Option<bool>, Query, description = "Return the compact management-table projection")
|
||||
("offset" = Option<i64>, Query, description = "Pagination offset")
|
||||
),
|
||||
responses(
|
||||
(status = 200, description = "List of users"),
|
||||
@@ -1128,31 +1124,16 @@ pub async fn list_users(
|
||||
// internal-only variant is used by system address book / sharee
|
||||
// search, where surfacing externals would leak identities. See
|
||||
// `auth_application_service::list_users` doc for the split.
|
||||
let users = if query.summary.unwrap_or(false) {
|
||||
AdminUsersPayload::Summary(
|
||||
auth.auth_application_service
|
||||
.list_user_summaries_including_external_with_perms(
|
||||
state.authorization.as_ref(),
|
||||
auth_user.id,
|
||||
limit,
|
||||
offset,
|
||||
)
|
||||
.await
|
||||
.map_err(AppError::from)?,
|
||||
let users = auth
|
||||
.auth_application_service
|
||||
.list_user_summaries_including_external_with_perms(
|
||||
state.authorization.as_ref(),
|
||||
auth_user.id,
|
||||
limit,
|
||||
offset,
|
||||
)
|
||||
} else {
|
||||
AdminUsersPayload::Full(
|
||||
auth.auth_application_service
|
||||
.list_users_including_external_with_perms(
|
||||
state.authorization.as_ref(),
|
||||
auth_user.id,
|
||||
limit,
|
||||
offset,
|
||||
)
|
||||
.await
|
||||
.map_err(AppError::from)?,
|
||||
)
|
||||
};
|
||||
.await
|
||||
.map_err(AppError::from)?;
|
||||
|
||||
let total = auth
|
||||
.auth_application_service
|
||||
|
||||
Reference in New Issue
Block a user