2025-03-20 09:22:31 +01:00
|
|
|
use crate::common::errors::DomainError;
|
2026-02-14 01:29:34 +01:00
|
|
|
use crate::domain::entities::user::{User, UserRole};
|
2026-03-07 14:59:32 +01:00
|
|
|
use uuid::Uuid;
|
2025-03-20 09:22:31 +01:00
|
|
|
|
|
|
|
|
#[derive(Debug, thiserror::Error)]
|
|
|
|
|
pub enum UserRepositoryError {
|
2026-02-12 09:41:25 +01:00
|
|
|
#[error("User not found: {0}")]
|
2025-03-20 09:22:31 +01:00
|
|
|
NotFound(String),
|
2026-02-14 01:29:34 +01:00
|
|
|
|
2026-02-12 09:41:25 +01:00
|
|
|
#[error("User already exists: {0}")]
|
2025-03-20 09:22:31 +01:00
|
|
|
AlreadyExists(String),
|
2026-02-14 01:29:34 +01:00
|
|
|
|
2026-02-12 09:41:25 +01:00
|
|
|
#[error("Database error: {0}")]
|
2025-03-20 09:22:31 +01:00
|
|
|
DatabaseError(String),
|
2026-02-14 01:29:34 +01:00
|
|
|
|
2026-02-12 09:41:25 +01:00
|
|
|
#[error("Validation error: {0}")]
|
2025-03-20 09:22:31 +01:00
|
|
|
ValidationError(String),
|
2026-02-14 01:29:34 +01:00
|
|
|
|
2026-02-12 09:41:25 +01:00
|
|
|
#[error("Timeout error: {0}")]
|
2025-03-20 09:22:31 +01:00
|
|
|
Timeout(String),
|
2026-02-14 01:29:34 +01:00
|
|
|
|
2026-02-12 09:41:25 +01:00
|
|
|
#[error("Operation not allowed: {0}")]
|
2025-03-20 09:22:31 +01:00
|
|
|
OperationNotAllowed(String),
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
pub type UserRepositoryResult<T> = Result<T, UserRepositoryError>;
|
|
|
|
|
|
2026-02-12 09:41:25 +01:00
|
|
|
// Conversion from UserRepositoryError to DomainError
|
2025-03-20 09:22:31 +01:00
|
|
|
impl From<UserRepositoryError> for DomainError {
|
|
|
|
|
fn from(err: UserRepositoryError) -> Self {
|
|
|
|
|
match err {
|
2026-02-14 01:29:34 +01:00
|
|
|
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),
|
2025-03-20 09:22:31 +01:00
|
|
|
UserRepositoryError::OperationNotAllowed(msg) => {
|
|
|
|
|
DomainError::access_denied("User", msg)
|
2026-02-14 01:29:34 +01:00
|
|
|
}
|
2025-03-20 09:22:31 +01:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
pub trait UserRepository: Send + Sync + 'static {
|
2026-02-12 09:41:25 +01:00
|
|
|
/// Creates a new user
|
2025-03-20 09:22:31 +01:00
|
|
|
async fn create_user(&self, user: User) -> UserRepositoryResult<User>;
|
2026-02-14 01:29:34 +01:00
|
|
|
|
2026-02-12 09:41:25 +01:00
|
|
|
/// Gets a user by ID
|
2026-03-07 14:59:32 +01:00
|
|
|
async fn get_user_by_id(&self, id: Uuid) -> UserRepositoryResult<User>;
|
2026-02-14 01:29:34 +01:00
|
|
|
|
2026-06-05 09:46:51 +02:00
|
|
|
/// 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
|
|
|
|
|
/// vec when given an empty input. Used by group-recipient expansion
|
|
|
|
|
/// in `RecipientNotificationService` to avoid N+1 queries.
|
|
|
|
|
async fn get_users_by_ids(&self, ids: Vec<Uuid>) -> UserRepositoryResult<Vec<User>>;
|
|
|
|
|
|
2026-02-12 09:41:25 +01:00
|
|
|
/// Gets a user by username
|
2025-03-20 09:22:31 +01:00
|
|
|
async fn get_user_by_username(&self, username: &str) -> UserRepositoryResult<User>;
|
2026-02-14 01:29:34 +01:00
|
|
|
|
2026-02-12 09:41:25 +01:00
|
|
|
/// Gets a user by email
|
2025-03-20 09:22:31 +01:00
|
|
|
async fn get_user_by_email(&self, email: &str) -> UserRepositoryResult<User>;
|
2026-02-14 01:29:34 +01:00
|
|
|
|
2026-02-12 09:41:25 +01:00
|
|
|
/// Updates an existing user
|
2025-03-20 09:22:31 +01:00
|
|
|
async fn update_user(&self, user: User) -> UserRepositoryResult<User>;
|
2026-02-14 01:29:34 +01:00
|
|
|
|
2026-02-12 09:41:25 +01:00
|
|
|
/// Updates only a user's storage usage
|
2026-02-14 01:29:34 +01:00
|
|
|
async fn update_storage_usage(
|
|
|
|
|
&self,
|
2026-03-07 14:59:32 +01:00
|
|
|
user_id: Uuid,
|
2026-02-14 01:29:34 +01:00
|
|
|
usage_bytes: i64,
|
|
|
|
|
) -> UserRepositoryResult<()>;
|
|
|
|
|
|
2026-02-12 09:41:25 +01:00
|
|
|
/// Updates the last login date
|
2026-03-07 14:59:32 +01:00
|
|
|
async fn update_last_login(&self, user_id: Uuid) -> UserRepositoryResult<()>;
|
2026-02-14 01:29:34 +01:00
|
|
|
|
2026-06-01 20:37:36 +02:00
|
|
|
/// 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>>;
|
2026-02-14 01:29:34 +01:00
|
|
|
|
2026-03-04 14:02:15 +01:00
|
|
|
/// Searches users by username or email (SQL ILIKE) with a limit.
|
2026-06-01 20:37:36 +02:00
|
|
|
/// See [`list_users`] for the meaning of `include_external`.
|
|
|
|
|
async fn search_users(
|
|
|
|
|
&self,
|
|
|
|
|
query: &str,
|
|
|
|
|
limit: i64,
|
|
|
|
|
include_external: bool,
|
|
|
|
|
) -> UserRepositoryResult<Vec<User>>;
|
2026-03-04 14:02:15 +01:00
|
|
|
|
2026-02-12 09:41:25 +01:00
|
|
|
/// Activates or deactivates a user
|
2026-03-07 14:59:32 +01:00
|
|
|
async fn set_user_active_status(&self, user_id: Uuid, active: bool)
|
2026-02-14 01:29:34 +01:00
|
|
|
-> UserRepositoryResult<()>;
|
|
|
|
|
|
2026-02-12 09:41:25 +01:00
|
|
|
/// Changes a user's password
|
2026-03-07 14:59:32 +01:00
|
|
|
async fn change_password(&self, user_id: Uuid, password_hash: &str)
|
2026-02-14 01:29:34 +01:00
|
|
|
-> UserRepositoryResult<()>;
|
|
|
|
|
|
2026-02-12 09:41:25 +01:00
|
|
|
/// Changes a user's role
|
2026-03-07 14:59:32 +01:00
|
|
|
async fn change_role(&self, user_id: Uuid, role: UserRole) -> UserRepositoryResult<()>;
|
2026-02-14 01:29:34 +01:00
|
|
|
|
2026-02-12 09:41:25 +01:00
|
|
|
/// Lists users by role (admin or user)
|
2025-04-12 18:58:21 +02:00
|
|
|
async fn list_users_by_role(&self, role: &str) -> UserRepositoryResult<Vec<User>>;
|
2026-02-14 01:29:34 +01:00
|
|
|
|
2026-07-21 10:25:36 +00:00
|
|
|
/// Counts users with a given role via a scalar `COUNT(*)` — no row
|
|
|
|
|
/// hydration (benches/ROUND29.md §G).
|
|
|
|
|
async fn count_users_by_role(&self, role: &str) -> UserRepositoryResult<i64>;
|
|
|
|
|
|
2026-02-12 09:41:25 +01:00
|
|
|
/// Deletes a user
|
2026-03-07 14:59:32 +01:00
|
|
|
async fn delete_user(&self, user_id: Uuid) -> UserRepositoryResult<()>;
|
2026-02-10 20:32:32 +01:00
|
|
|
|
|
|
|
|
/// Finds a user by OIDC provider + subject pair
|
2026-02-14 01:29:34 +01:00
|
|
|
async fn get_user_by_oidc_subject(
|
|
|
|
|
&self,
|
|
|
|
|
provider: &str,
|
|
|
|
|
subject: &str,
|
|
|
|
|
) -> UserRepositoryResult<User>;
|
2026-02-11 01:08:00 +01:00
|
|
|
|
2026-02-12 09:41:25 +01:00
|
|
|
/// Updates a user's storage quota
|
2026-02-14 01:29:34 +01:00
|
|
|
async fn update_storage_quota(
|
|
|
|
|
&self,
|
2026-03-07 14:59:32 +01:00
|
|
|
user_id: Uuid,
|
2026-02-14 01:29:34 +01:00
|
|
|
quota_bytes: i64,
|
|
|
|
|
) -> UserRepositoryResult<()>;
|
2026-02-11 01:08:00 +01:00
|
|
|
|
2026-02-12 09:41:25 +01:00
|
|
|
/// Counts the total number of users
|
2026-02-11 01:08:00 +01:00
|
|
|
async fn count_users(&self) -> UserRepositoryResult<i64>;
|
|
|
|
|
|
2026-02-12 09:41:25 +01:00
|
|
|
/// Gets aggregated storage statistics
|
2026-02-11 01:08:00 +01:00
|
|
|
async fn get_storage_stats(&self) -> UserRepositoryResult<StorageStats>;
|
|
|
|
|
}
|
|
|
|
|
|
2026-02-12 09:41:25 +01:00
|
|
|
/// Aggregated storage statistics
|
2026-02-11 01:08:00 +01:00
|
|
|
#[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,
|
2026-02-14 01:29:34 +01:00
|
|
|
}
|