Merge pull request #683 from EdouardVanbelle/refactor/userdto
This commit is contained in:
@@ -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,
|
||||
|
||||
@@ -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,49 +25,26 @@ 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.
|
||||
/// 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`)
|
||||
/// or from a cross-table lookup (`auth.sessions.last_seen_at` for
|
||||
/// `is_online`). Companion to `User` on the list projection: the
|
||||
/// repo computes both, the application layer packs them into
|
||||
/// [`FullUserDto`](crate::application::dtos::user_dto::FullUserDto).
|
||||
///
|
||||
/// 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).
|
||||
/// Not "admin-only" — every field ends up on `FullUserDto`, which
|
||||
/// 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 design; this type
|
||||
/// replaced the earlier `UserListEntry` narrow projection as of P6.
|
||||
#[derive(Debug, Clone, Copy)]
|
||||
pub struct UserDerivedFlags {
|
||||
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,
|
||||
pub is_online: bool,
|
||||
}
|
||||
|
||||
// Conversion from UserRepositoryError to DomainError
|
||||
@@ -94,6 +70,20 @@ pub trait UserRepository: Send + Sync + 'static {
|
||||
/// Gets a user by ID
|
||||
async fn get_user_by_id(&self, id: Uuid) -> UserRepositoryResult<User>;
|
||||
|
||||
/// Fetch the full `User` entity + the [`UserDerivedFlags`] in a
|
||||
/// single query. Used by `/api/auth/me` and future admin single-user
|
||||
/// views — anywhere the caller needs both the row itself AND the
|
||||
/// derived booleans (`has_password`, OPAQUE flags, `is_online`) to
|
||||
/// build a [`FullUserDto`](crate::application::dtos::user_dto::FullUserDto)
|
||||
/// or [`SelfUserDto`](crate::application::dtos::user_dto::SelfUserDto).
|
||||
/// Single query is cheaper than `get_user_by_id` + separate lookups
|
||||
/// for OPAQUE state + `is_online`; the EXISTS subquery is cheap
|
||||
/// thanks to the partial index `idx_sessions_last_seen_at`.
|
||||
async fn get_user_with_derived_flags(
|
||||
&self,
|
||||
id: Uuid,
|
||||
) -> UserRepositoryResult<(User, UserDerivedFlags)>;
|
||||
|
||||
/// Batch-loads a set of users by id, preserving no particular order
|
||||
/// and silently skipping ids that don't match any row. Caller is
|
||||
/// responsible for de-duplicating the input vec. Returns an empty
|
||||
@@ -152,15 +142,22 @@ 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.
|
||||
async fn list_user_summaries(
|
||||
/// 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
|
||||
/// `Vec<FullUserDto>` for `/api/admin/users` without paying two
|
||||
/// round-trips per row (once for User, once for derived flags).
|
||||
///
|
||||
/// Same `include_external` semantics as [`Self::list_users`]:
|
||||
/// admin management UI passes `true`; every other caller passes
|
||||
/// `false` so external / grant-only users stay off internal-user
|
||||
/// surfaces.
|
||||
async fn list_users_with_derived_flags(
|
||||
&self,
|
||||
limit: i64,
|
||||
offset: i64,
|
||||
include_external: bool,
|
||||
) -> UserRepositoryResult<Vec<UserListEntry>>;
|
||||
) -> UserRepositoryResult<Vec<(User, UserDerivedFlags)>>;
|
||||
|
||||
/// Searches users by username or email (SQL ILIKE) with a limit.
|
||||
/// See [`list_users`] for the meaning of `include_external`.
|
||||
|
||||
Reference in New Issue
Block a user