Skip to main content

axum_gate/accounts/
account_insert.rs

1use super::{Account, AccountRepository};
2use crate::accounts::{AccountOperation, AccountsError};
3#[cfg(feature = "audit-logging")]
4use crate::audit;
5use crate::authz::AccessHierarchy;
6use crate::errors::{Error, Result};
7use crate::hashing::argon2::Argon2Hasher;
8use crate::permissions::Permissions;
9use crate::secrets::{Secret, SecretRepository};
10
11use std::sync::Arc;
12
13use tracing::debug;
14
15/// Service for creating new user accounts with their associated authentication secrets.
16///
17/// This service provides an ergonomic builder pattern for creating accounts with roles,
18/// groups, and permissions, then storing both the account data and authentication secrets
19/// in their respective repositories.
20///
21/// # Basic Usage
22///
23/// ```rust
24/// use axum_gate::accounts::AccountInsertService;
25/// use axum_gate::prelude::{Role, Group};
26/// use axum_gate::repositories::memory::{MemoryAccountRepository, MemorySecretRepository};
27/// use std::sync::Arc;
28///
29/// # tokio_test::block_on(async {
30/// let account_repo = Arc::new(MemoryAccountRepository::<Role, Group>::default());
31/// let secret_repo = Arc::new(MemorySecretRepository::new_with_argon2_hasher().unwrap());
32///
33/// let account = AccountInsertService::insert("user@example.com", "secure_password")
34///     .with_roles(vec![Role::User])
35///     .with_groups(vec![Group::new("staff")])
36///     .into_repositories(account_repo, secret_repo)
37///     .await
38///     .unwrap()
39///     .unwrap();
40///
41/// println!("Created account: {}", account.user_id);
42/// # });
43/// ```
44pub struct AccountInsertService<R, G>
45where
46    R: AccessHierarchy + Eq,
47    G: Eq,
48{
49    user_id: String,
50    secret: String,
51    roles: Vec<R>,
52    groups: Vec<G>,
53    permissions: Permissions,
54}
55
56impl<R, G> AccountInsertService<R, G>
57where
58    R: AccessHierarchy + Eq,
59    G: Eq + Clone,
60{
61    /// Creates a new account insertion builder with the specified credentials.
62    ///
63    /// This is the starting point for creating a new account. The user ID should be
64    /// unique within your application (typically an email or username), and the secret
65    /// will be hashed before storage using Argon2.
66    ///
67    /// # Arguments
68    /// * `user_id` - Unique identifier for the user (e.g., email or username)
69    /// * `secret` - Plain text password that will be securely hashed
70    ///
71    /// # Example
72    /// ```rust
73    /// use axum_gate::accounts::AccountInsertService;
74    /// use axum_gate::prelude::{Role, Group};
75    ///
76    /// let builder = AccountInsertService::<Role, Group>::insert("admin@example.com", "strong_password");
77    /// // Continue with .with_roles(), .with_groups(), etc.
78    /// ```
79    pub fn insert(user_id: &str, secret: &str) -> Self {
80        Self {
81            user_id: user_id.to_string(),
82            secret: secret.to_string(),
83            roles: vec![],
84            groups: vec![],
85            permissions: Permissions::new(),
86        }
87    }
88
89    /// Adds roles to the account being created.
90    ///
91    /// Roles determine what actions the user can perform. Use the pre-defined
92    /// `Role` enum or create your own custom role type.
93    ///
94    /// # Example
95    /// ```rust
96    /// use axum_gate::accounts::AccountInsertService;
97    /// use axum_gate::prelude::{Role, Group};
98    ///
99    /// let builder = AccountInsertService::<Role, Group>::insert("user@example.com", "password")
100    ///     .with_roles(vec![Role::User, Role::Reporter]);
101    /// ```
102    pub fn with_roles(self, roles: Vec<R>) -> Self {
103        Self { roles, ..self }
104    }
105
106    /// Adds groups to the account being created.
107    ///
108    /// Groups provide organizational structure for users, such as department
109    /// or team membership. They offer another dimension of access control.
110    ///
111    /// # Example
112    /// ```rust
113    /// use axum_gate::accounts::AccountInsertService;
114    /// use axum_gate::prelude::{Role, Group};
115    ///
116    /// let builder = AccountInsertService::<Role, Group>::insert("user@example.com", "password")
117    ///     .with_groups(vec![Group::new("engineering"), Group::new("backend-team")]);
118    /// ```
119    pub fn with_groups(self, groups: Vec<G>) -> Self {
120        Self { groups, ..self }
121    }
122
123    /// Adds custom permissions to the account being created.
124    ///
125    /// This method allows you to set specific permissions using the zero-synchronization
126    /// permission system. Permissions are stored as a compressed bitmap for efficiency.
127    ///
128    /// # Arguments
129    /// * `permissions` - A Permissions set containing the permission names
130    ///
131    /// # Example
132    /// ```rust
133    /// use axum_gate::accounts::AccountInsertService;
134    /// use axum_gate::permissions::Permissions;
135    /// use axum_gate::prelude::{Role, Group};
136    ///
137    /// let permissions: Permissions = [
138    ///     "read:api",
139    ///     "write:api",
140    ///     "manage:users"
141    /// ].into_iter().collect();
142    ///
143    /// let builder = AccountInsertService::<Role, Group>::insert("admin@example.com", "password")
144    ///     .with_permissions(permissions);
145    /// ```
146    pub fn with_permissions(self, permissions: Permissions) -> Self {
147        Self {
148            permissions,
149            ..self
150        }
151    }
152
153    /// Creates the account and secret, storing them in the provided repositories.
154    ///
155    /// This method consumes the builder and performs the actual account creation:
156    /// 1. Creates an `Account` with the specified details
157    /// 2. Stores the account in the account repository
158    /// 3. Hashes the password using Argon2
159    /// 4. Creates and stores the secret in the secret repository
160    ///
161    /// Both operations must succeed for the account to be considered created.
162    ///
163    /// # Arguments
164    /// * `account_repository` - Repository for storing account data
165    /// * `secret_repository` - Repository for storing password hashes
166    ///
167    /// # Returns
168    /// * `Ok(Some(Account))` - Account successfully created
169    /// * `Ok(None)` - Account creation failed (repository returned None)
170    /// * `Err(...)` - Error during creation process
171    ///
172    /// # Example
173    /// ```rust
174    /// use axum_gate::accounts::AccountInsertService;
175    /// use axum_gate::prelude::{Role, Group};
176    /// use axum_gate::repositories::memory::{MemoryAccountRepository, MemorySecretRepository};
177    /// use std::sync::Arc;
178    ///
179    /// # tokio_test::block_on(async {
180    /// let account_repo = Arc::new(MemoryAccountRepository::<Role, Group>::default());
181    /// let secret_repo = Arc::new(MemorySecretRepository::new_with_argon2_hasher().unwrap());
182    ///
183    /// let result = AccountInsertService::insert("user@example.com", "password")
184    ///     .with_roles(vec![Role::User])
185    ///     .into_repositories(account_repo, secret_repo)
186    ///     .await?;
187    ///
188    /// match result {
189    ///     Some(account) => println!("Created account: {}", account.user_id),
190    ///     None => println!("Account creation failed"),
191    /// }
192    /// # Ok::<(), Box<dyn std::error::Error>>(())
193    /// # });
194    /// ```
195    pub async fn into_repositories<AccRepo, SecRepo>(
196        self,
197        account_repository: Arc<AccRepo>,
198        secret_repository: Arc<SecRepo>,
199    ) -> Result<Option<Account<R, G>>>
200    where
201        AccRepo: AccountRepository<R, G>,
202        SecRepo: SecretRepository,
203    {
204        let account = Account::new(&self.user_id, &self.roles, &self.groups)
205            .with_permissions(self.permissions);
206        debug!("Created account.");
207        let Some(account) = account_repository.store_account(account).await? else {
208            #[cfg(feature = "audit-logging")]
209            {
210                audit::account_insert_failure(&self.user_id, "account_repo_none");
211            }
212            return Err(Error::Accounts(AccountsError::operation(
213                AccountOperation::Create,
214                "Account repository returned None on insertion",
215                Some(self.user_id.clone()),
216            )));
217        };
218        #[cfg(feature = "audit-logging")]
219        {
220            // Account persisted successfully in repository
221            audit::account_created(&self.user_id, &account.account_id);
222        }
223        debug!("Stored account in account repository.");
224        let id = &account.account_id;
225        let secret = Secret::new(id, &self.secret, Argon2Hasher::new_recommended()?)?;
226        if !secret_repository.store_secret(secret).await? {
227            #[cfg(feature = "audit-logging")]
228            {
229                audit::account_insert_failure(&self.user_id, "secret_store_false");
230            }
231            Err(Error::Accounts(AccountsError::operation(
232                AccountOperation::Create,
233                "Storing secret in repository returned false",
234                Some(account.account_id.to_string()),
235            )))
236        } else {
237            debug!("Stored secret in secret repository.");
238            Ok(Some(account))
239        }
240    }
241}