Files
Oxicloud/src/domain/repositories/user_repository.rs
T
Edouard Vanbelle 5fab0532dc feat(user): add given_name/family_name auth.users
reflect OIDC
  schema migration, User entity additions, and three defense-in-depth gaps closed, with all 280 unit tests and 13 Hurl files green
  infrastructure (lettre + EmailSender port).

  - Migration migrations/20260612000003_users_username_email_login.sql — adds nullable given_name/family_name columns to auth.users.
  - User entity (src/domain/entities/user.rs) — has_login_credential() placeholder-check encapsulation, set_username revalidating setter, given/family-name fields + getters/setters, validate_username widened 32→254 and now accepts email shape.
  from_data_full extended with two new params; all 7 callsites in user_pg_repository.rs updated.
  - Schema-side legacy guards (src/application/services/auth_application_service.rs) — bumped the duplicated 32-char check in setup_create_admin and admin_create_user to 254 to match.
  - Gap #1 (subject_group_service.rs) — add_member now rejects external candidates with an audit-logged AccessDenied. Service gained an Arc<UserPgRepository> field, wired through DI. New integration test test_external_user_cannot_be_added_as_member.
  - Gap #2 (user_repository.rs + auth_ports.rs + user_pg_repository.rs) — list_users/search_users gained an include_external: bool param defaulting effectively to false everywhere internal-user-facing. auth_application_service exposes a new
  list_users_including_external for the admin surface.
  - Gap #3 (pg_acl_engine.rs) — expand_user now SELECTs is_external and skips INTERNAL_GROUP_ID for externals; defaults to is_external=true on missing user to fail closed.
2026-06-03 00:31:59 +02:00

141 lines
4.5 KiB
Rust

use crate::common::errors::DomainError;
use crate::domain::entities::user::{User, UserRole};
use uuid::Uuid;
#[derive(Debug, thiserror::Error)]
pub enum UserRepositoryError {
#[error("User not found: {0}")]
NotFound(String),
#[error("User already exists: {0}")]
AlreadyExists(String),
#[error("Database error: {0}")]
DatabaseError(String),
#[error("Validation error: {0}")]
ValidationError(String),
#[error("Timeout error: {0}")]
Timeout(String),
#[error("Operation not allowed: {0}")]
OperationNotAllowed(String),
}
pub type UserRepositoryResult<T> = Result<T, UserRepositoryError>;
// Conversion from UserRepositoryError to DomainError
impl From<UserRepositoryError> for DomainError {
fn from(err: UserRepositoryError) -> Self {
match err {
UserRepositoryError::NotFound(msg) => DomainError::not_found("User", msg),
UserRepositoryError::AlreadyExists(msg) => DomainError::already_exists("User", msg),
UserRepositoryError::DatabaseError(msg) => DomainError::internal_error("Database", msg),
UserRepositoryError::ValidationError(msg) => DomainError::validation_error(msg),
UserRepositoryError::Timeout(msg) => DomainError::timeout("Database", msg),
UserRepositoryError::OperationNotAllowed(msg) => {
DomainError::access_denied("User", msg)
}
}
}
}
pub trait UserRepository: Send + Sync + 'static {
/// Creates a new user
async fn create_user(&self, user: User) -> UserRepositoryResult<User>;
/// Gets a user by ID
async fn get_user_by_id(&self, id: Uuid) -> UserRepositoryResult<User>;
/// Gets a user by username
async fn get_user_by_username(&self, username: &str) -> UserRepositoryResult<User>;
/// Gets a user by email
async fn get_user_by_email(&self, email: &str) -> UserRepositoryResult<User>;
/// Updates an existing user
async fn update_user(&self, user: User) -> UserRepositoryResult<User>;
/// Updates only a user's storage usage
async fn update_storage_usage(
&self,
user_id: Uuid,
usage_bytes: i64,
) -> UserRepositoryResult<()>;
/// Updates the last login date
async fn update_last_login(&self, user_id: Uuid) -> UserRepositoryResult<()>;
/// Lists users with pagination.
///
/// `include_external` controls whether external (grant-only) users
/// appear in the result. Default callers should pass `false` so
/// external users stay invisible to internal-user surfaces (system
/// address book autocomplete, sharee search, etc.). Only the admin
/// management UI should request `true`.
async fn list_users(
&self,
limit: i64,
offset: i64,
include_external: bool,
) -> UserRepositoryResult<Vec<User>>;
/// Searches users by username or email (SQL ILIKE) with a limit.
/// See [`list_users`] for the meaning of `include_external`.
async fn search_users(
&self,
query: &str,
limit: i64,
include_external: bool,
) -> UserRepositoryResult<Vec<User>>;
/// Activates or deactivates a user
async fn set_user_active_status(&self, user_id: Uuid, active: bool)
-> UserRepositoryResult<()>;
/// Changes a user's password
async fn change_password(&self, user_id: Uuid, password_hash: &str)
-> UserRepositoryResult<()>;
/// Changes a user's role
async fn change_role(&self, user_id: Uuid, role: UserRole) -> UserRepositoryResult<()>;
/// Lists users by role (admin or user)
async fn list_users_by_role(&self, role: &str) -> UserRepositoryResult<Vec<User>>;
/// Deletes a user
async fn delete_user(&self, user_id: Uuid) -> UserRepositoryResult<()>;
/// Finds a user by OIDC provider + subject pair
async fn get_user_by_oidc_subject(
&self,
provider: &str,
subject: &str,
) -> UserRepositoryResult<User>;
/// Updates a user's storage quota
async fn update_storage_quota(
&self,
user_id: Uuid,
quota_bytes: i64,
) -> UserRepositoryResult<()>;
/// Counts the total number of users
async fn count_users(&self) -> UserRepositoryResult<i64>;
/// Gets aggregated storage statistics
async fn get_storage_stats(&self) -> UserRepositoryResult<StorageStats>;
}
/// Aggregated storage statistics
#[derive(Debug, Clone)]
pub struct StorageStats {
pub total_users: i64,
pub active_users: i64,
pub total_quota_bytes: i64,
pub total_used_bytes: i64,
pub users_over_80_percent: i64,
pub users_over_quota: i64,
}