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}