refactor(User): move UserDto to PublicUserDto

This commit is contained in:
Edouard Vanbelle
2026-08-21 16:09:10 +02:00
parent d17b3b6bd3
commit a11ae679cf
13 changed files with 136 additions and 606 deletions
+1 -1
View File
@@ -207,7 +207,7 @@ pub struct User {
/// Owned decomposition of a [`User`] (mirrors `FileParts` / `FolderParts` /
/// `ContactParts`). Lets a consumer MOVE the heap fields out instead of cloning
/// them through the borrowing accessors — notably `image` (a data URI up to
/// 512 KiB) and `ui_preferences` (a JSON tree). See `UserDto::from`
/// 512 KiB) and `ui_preferences` (a JSON tree). See `PublicUserDto::from`
/// (benches/ROUND20.md §A2).
pub struct UserParts {
pub id: Uuid,
+2 -87
View File
@@ -1,6 +1,5 @@
use crate::common::errors::DomainError;
use crate::domain::entities::user::{User, UserRole};
use chrono::{DateTime, Utc};
use uuid::Uuid;
#[derive(Debug, thiserror::Error)]
@@ -26,72 +25,6 @@ pub enum UserRepositoryError {
pub type UserRepositoryResult<T> = Result<T, UserRepositoryError>;
/// Narrow projection for user-directory tables that do not need secrets,
/// profile pictures, or the cross-device UI-preferences document.
///
/// The full [`User`] row intentionally carries all of those fields for account
/// detail and the system address book. Reusing it for the paginated admin
/// table made PostgreSQL detoast and transfer an avatar of up to 512 KiB per
/// row, only for the handler to serialize it back to the browser where the
/// table never reads it. Keeping the projection explicit prevents a future
/// full-row field from silently returning to that hot path.
#[derive(Debug, Clone)]
pub struct UserListEntry {
pub id: Uuid,
pub username: Option<String>,
pub email: String,
pub role: UserRole,
pub storage_quota_bytes: i64,
pub storage_used_bytes: i64,
pub last_login_at: Option<DateTime<Utc>>,
pub active: bool,
pub federation_kind: Option<String>,
pub federation_issuer: Option<String>,
pub is_external: bool,
/// TRUE when `auth.users.password_hash IS NOT NULL` — user has a
/// server-verifiable password on file (legacy or admin-set).
/// Distinct from `opaque_registered` (which is the zero-knowledge
/// envelope): a fully-migrated user carries BOTH — password for
/// the fallback / operator flows, envelope for the actual login.
/// A user with `has_password = false AND !opaque_registered AND
/// federation_issuer IS NULL` is passwordless — the only path in is
/// via magic-link (or, for externals, whatever grant they hold).
pub has_password: bool,
/// TRUE when `auth.users.opaque_envelope IS NOT NULL` — the user
/// has completed OPAQUE registration (typically via the Phase 2
/// silent-migration hook after a successful legacy login). Surfaced
/// on the admin user table so operators can see rollout progress
/// per-user. Admin-only exposure — see `AdminUserSummaryDto`.
pub opaque_registered: bool,
/// TRUE when `auth.users.opaque_migrated_at IS NOT NULL` — the
/// user has completed at least one successful OPAQUE login. Distinct
/// from `opaque_registered` because a user can have an envelope on
/// file without having actually logged in via OPAQUE yet (e.g.
/// admin cleared the envelope, silent-migration hasn't re-run).
pub opaque_migrated: bool,
/// Optional avatar payload (base64, up to 512 KiB per row). Included
/// on the admin list projection so the SPA can seed its per-user
/// `resolveUser` cache from the list row and skip the follow-up
/// `/api/users/{id}` fetch UserVignette would otherwise trigger.
/// The narrow-projection concern that motivated omitting this
/// column originally is retired by that cache-seeding path — the
/// bytes now do useful work per page load instead of being
/// discarded. Deferred: moving avatar storage out of the row
/// entirely (planned refactor); this shape is transitional.
pub image: Option<String>,
/// Presence signal — TRUE when the server observed a request on
/// any of this user's non-revoked sessions within the last
/// [`ONLINE_WINDOW`](crate::application::dtos::session_dto::ONLINE_WINDOW)
/// (5 min). Populated via an `EXISTS(...)` subquery on
/// `auth.sessions` in the list projection — the partial index
/// `idx_sessions_last_seen_at WHERE revoked = FALSE` covers the
/// scan, so per-row cost is ~μs. Surfaces to the FE via
/// `UserDto::is_online` so both `/api/users/{id}` and the admin
/// listing carry it, and the admin table renders a green/grey
/// presence dot next to each vignette.
pub is_online: bool,
}
/// DB-computed booleans about a user that aren't fields on the
/// [`User`](crate::domain::entities::user::User) entity itself —
/// either derived from column presence (`password_hash IS NOT NULL`)
@@ -104,10 +37,8 @@ pub struct UserListEntry {
/// both admin AND self read. The name reflects "derived from the DB
/// row, not intrinsic to the User entity".
///
/// See `docs/plan/userdto-refactor.md` for the phasing that
/// introduces this type; it will replace [`UserListEntry`] once the
/// list repo is switched from narrow projection to
/// `Vec<(User, UserDerivedFlags)>` (P6 of the refactor).
/// See `docs/plan/userdto-refactor.md` for the design; this type
/// replaced the earlier `UserListEntry` narrow projection as of P6.
#[derive(Debug, Clone, Copy)]
pub struct UserDerivedFlags {
pub has_password: bool,
@@ -211,22 +142,6 @@ pub trait UserRepository: Send + Sync + 'static {
include_external: bool,
) -> UserRepositoryResult<Vec<User>>;
/// Lists the columns needed by compact user-management tables. Unlike
/// [`Self::list_users`], this never fetches password hashes, OIDC subjects,
/// avatars, names, locale state, or UI preferences.
///
/// **Deprecated** — [`Self::list_users_with_derived_flags`] supersedes
/// this: it returns the full `User` entity + [`UserDerivedFlags`] so
/// the application layer can build [`FullUserDto`](crate::application::dtos::user_dto::FullUserDto)
/// directly. Kept only until P6 of `docs/plan/userdto-refactor.md`
/// removes `UserListEntry` + the last remaining caller.
async fn list_user_summaries(
&self,
limit: i64,
offset: i64,
include_external: bool,
) -> UserRepositoryResult<Vec<UserListEntry>>;
/// Paginated admin user listing — full `User` entity + the derived
/// booleans (`has_password`, OPAQUE flags, `is_online`) in one wide
/// SELECT. Called by the admin service to build