axum_gate/accounts/
account_repository.rs

1use crate::errors::Result;
2use crate::{accounts::Account, authz::AccessHierarchy};
3
4use std::future::Future;
5
6/// Repository abstraction for persisting and retrieving [`Account`] entities.
7///
8/// This trait is implemented by storage backends (e.g. in‑memory, SurrealDB, SeaORM).
9/// It deliberately uses an `Option<Account<..>>` in results for operations where
10/// absence is a normal outcome (delete / update / query) so callers can
11/// distinguish “not found” from actual errors (`Result::Err`).
12///
13/// # Semantics
14///
15/// | Method                | Success (`Ok`) Return Value                        | Typical `None` Meaning                    | Error (`Err`) Meaning                            |
16/// |-----------------------|----------------------------------------------------|-------------------------------------------|--------------------------------------------------|
17/// | `store_account`       | `Some(Account)` if stored                          | `None` only if backend chooses (rare)     | Persistence / connectivity / constraint failure  |
18/// | `delete_account`      | `Some(Account)` = deleted & returned               | `None` = no account with that user id     | Backend / IO failure                             |
19/// | `update_account`      | `Some(Account)` = updated                          | `None` = no existing account to update    | Backend / IO / optimistic concurrency failure    |
20/// | `query_account_by_user_id` | `Some(Account)` = found                      | `None` = not found                        | Backend / IO failure                             |
21///
22/// Backends SHOULD:
23/// - Treat `user_id` as a logical unique key
24/// - Enforce uniqueness at storage level where possible
25/// - Return **identical timing characteristics** for “found” vs “not found” where feasible
26///   (helps upstream login logic resist user enumeration timing attacks)
27///
28/// # Concurrency & Consistency
29///
30/// This trait does not prescribe isolation semantics. Implementations should document:
31/// - Whether updates are last‑write‑wins
32/// - Whether optimistic locking / versioning is applied
33///
34/// # Example (generic usage)
35/// ```rust
36///
37/// use axum_gate::accounts::Account;
38/// use axum_gate::prelude::{Role, Group};
39/// use axum_gate::accounts::AccountRepository;
40/// use axum_gate::repositories::memory::MemoryAccountRepository;
41///
42/// # #[tokio::test]
43/// async fn load_or_create(
44///     repo: &MemoryAccountRepository<Role, Group>,
45///     template: Account<Role, Group>
46/// ) -> axum_gate::errors::Result<Account<Role, Group>> {
47///     if let Some(existing) = repo.query_account_by_user_id(&template.user_id).await? {
48///         Ok(existing)
49///     } else {
50///         Ok(repo.store_account(template).await?.expect("store returned None"))
51///     }
52/// }
53/// ```
54///
55/// # Error Handling
56///
57/// Return `Err` only for exceptional backend failures (connectivity, serialization,
58/// constraint violation, etc.). Use `Ok(None)` for “not found” / “no-op” outcomes.
59///
60/// # Extensibility
61///
62/// If you add methods (e.g. pagination, search), prefer separate traits to avoid forcing
63/// all backends to implement optional features.
64pub trait AccountRepository<R, G>
65where
66    R: AccessHierarchy + Eq,
67    G: Eq + Clone,
68{
69    /// Persist a new account.
70    ///
71    /// Implementations SHOULD enforce uniqueness of `user_id`. Returning `Ok(Some(account))`
72    /// indicates success. Returning `Ok(None)` is discouraged unless there is a documented
73    /// race / conditional insert semantics the backend wishes to expose.
74    fn store_account(
75        &self,
76        account: Account<R, G>,
77    ) -> impl Future<Output = Result<Option<Account<R, G>>>>;
78
79    /// Delete an account identified by its `user_id`.
80    ///
81    /// Returns:
82    /// - `Ok(Some(account))` if the account existed and was removed
83    /// - `Ok(None)` if no account matched `user_id`
84    /// - `Err(e)` on backend error
85    fn delete_account(&self, user_id: &str) -> impl Future<Output = Result<Option<Account<R, G>>>>;
86
87    /// Update an existing account.
88    ///
89    /// Implementations may perform either full replacement or partial persistence depending
90    /// on backend capabilities (document if non‑standard). Returns:
91    /// - `Ok(Some(updated_account))` on success
92    /// - `Ok(None)` if the account does not exist
93    /// - `Err(e)` on failure
94    fn update_account(
95        &self,
96        account: Account<R, G>,
97    ) -> impl Future<Output = Result<Option<Account<R, G>>>>;
98
99    /// Fetch an account by its logical user identifier.
100    ///
101    /// This must **not** leak timing differences exploitable for enumeration if used
102    /// together with authentication flows relying on indistinguishable “not found”.
103    ///
104    /// Returns:
105    /// - `Ok(Some(account))` if found
106    /// - `Ok(None)` if not found
107    /// - `Err(e)` on backend failure
108    fn query_account_by_user_id(
109        &self,
110        user_id: &str,
111    ) -> impl Future<Output = Result<Option<Account<R, G>>>>;
112}