axum_gate/accounts/mod.rs
1//! Account management and user data structures.
2//!
3//! This module provides the core [`Account`] type and services for managing user accounts,
4//! including creation, deletion, and repository abstractions for data persistence.
5//!
6//! # Quick Start
7//!
8//! ```rust
9//! use axum_gate::accounts::{Account, AccountInsertService};
10//! use axum_gate::prelude::{Role, Group};
11//! use axum_gate::permissions::Permissions;
12//! use axum_gate::repositories::memory::{MemoryAccountRepository, MemorySecretRepository};
13//! use std::sync::Arc;
14//!
15//! # tokio_test::block_on(async {
16//! // Create repositories
17//! let account_repo = Arc::new(MemoryAccountRepository::<Role, Group>::default());
18//! let secret_repo = Arc::new(MemorySecretRepository::new_with_argon2_hasher().unwrap());
19//!
20//! // Create a new account
21//! let account = AccountInsertService::insert("user@example.com", "password")
22//! .with_roles(vec![Role::User, Role::Reporter])
23//! .with_groups(vec![Group::new("engineering"), Group::new("backend-team")])
24//! .with_permissions(Permissions::from_iter(["read:api", "write:docs"]))
25//! .into_repositories(account_repo, secret_repo)
26//! .await;
27//! # });
28//! ```
29
30mod account_delete;
31mod account_insert;
32mod account_repository;
33
34use crate::authz::AccessHierarchy;
35#[cfg(feature = "storage-seaorm")]
36use crate::comma_separated_value::CommaSeparatedValue;
37use crate::permissions::{PermissionId, Permissions};
38pub use account_delete::AccountDeleteService;
39pub use account_insert::AccountInsertService;
40pub use account_repository::AccountRepository;
41pub mod errors;
42pub use errors::{AccountOperation, AccountsError};
43
44use serde::{Deserialize, Serialize};
45use uuid::Uuid;
46
47/// An account contains authorization information about a user.
48///
49/// Accounts store user identification, roles, groups, and permissions. They are the
50/// core entity for authorization decisions in axum-gate.
51///
52/// # Creating Accounts
53///
54/// ```rust
55/// use axum_gate::accounts::Account;
56/// use axum_gate::prelude::{Role, Group};
57/// use axum_gate::permissions::Permissions;
58///
59/// // Create a basic account
60/// let account = Account::new("user123", &[Role::User], &[Group::new("staff")]);
61///
62/// // Create account with permissions
63/// let permissions: Permissions = ["read:profile", "write:profile"].into_iter().collect();
64/// let account = Account::<Role, Group>::new("admin@example.com", &[Role::Admin], &[])
65/// .with_permissions(permissions);
66/// ```
67///
68/// # Working with Permissions
69///
70/// ```rust
71/// # use axum_gate::accounts::Account;
72/// # use axum_gate::prelude::{Role, Group};
73/// # use axum_gate::permissions::PermissionId;
74/// # let mut account = Account::<Role, Group>::new("user", &[], &[]);
75/// // Grant permissions
76/// account.grant_permission("read:api");
77/// account.grant_permission(PermissionId::from("write:api"));
78///
79/// // Check permissions directly
80/// if account.permissions.has("read:api") {
81/// println!("User can read API");
82/// }
83///
84/// // Revoke permissions
85/// account.revoke_permission("write:api");
86/// ```
87#[derive(Serialize, Deserialize, Clone)]
88pub struct Account<R, G>
89where
90 R: AccessHierarchy + Eq,
91 G: Eq + Clone,
92{
93 /// The unique identifier of the account generated during registration.
94 ///
95 /// This UUID links the account to its corresponding authentication secret
96 /// in the secret repository. The separation of account data from secrets
97 /// enhances security by allowing different storage backends and access controls.
98 pub account_id: Uuid,
99 /// The user identifier for this account (e.g., email, username).
100 ///
101 /// This should be unique within your application and is typically what users
102 /// provide during login. It's used to look up accounts in the repository.
103 pub user_id: String,
104 /// Roles assigned to this account.
105 ///
106 /// Roles determine what actions a user can perform. If your roles implement
107 /// `AccessHierarchy`, supervisor roles automatically inherit subordinate permissions.
108 pub roles: Vec<R>,
109 /// Groups this account belongs to.
110 ///
111 /// Groups provide another dimension of access control, allowing you to grant
112 /// permissions based on team membership, department, or other organizational units.
113 pub groups: Vec<G>,
114 /// Custom permissions granted to this account.
115 ///
116 /// Uses a compressed bitmap for efficient storage and fast permission checks.
117 /// Permissions are automatically available when referenced by name using
118 /// deterministic hashing - no coordination between nodes required.
119 pub permissions: Permissions,
120}
121
122impl<R, G> Account<R, G>
123where
124 R: AccessHierarchy + Eq + Clone,
125 G: Eq + Clone,
126{
127 /// Creates a new account with the specified user ID, roles, and groups.
128 ///
129 /// A random UUID is automatically generated for the account ID. The account
130 /// starts with no permissions - use `with_permissions()` or `grant_permission()`
131 /// to add them.
132 ///
133 /// # Arguments
134 /// * `user_id` - Unique identifier for the user (e.g., email or username)
135 /// * `roles` - Roles to assign to this account
136 /// * `groups` - Groups this account should belong to
137 ///
138 /// # Example
139 /// ```rust
140 /// use axum_gate::accounts::Account;
141 /// use axum_gate::prelude::{Role, Group};
142 ///
143 /// let account = Account::new(
144 /// "user@example.com",
145 /// &[Role::User, Role::Reporter],
146 /// &[Group::new("engineering"), Group::new("backend-team")]
147 /// );
148 /// ```
149 pub fn new(user_id: &str, roles: &[R], groups: &[G]) -> Self {
150 let roles = roles.to_vec();
151 let groups = groups.to_vec();
152 Self {
153 account_id: Uuid::now_v7(),
154 user_id: user_id.to_owned(),
155 groups,
156 roles,
157 permissions: Permissions::new(),
158 }
159 }
160
161 /// Creates a new account with the specified account ID.
162 ///
163 /// This constructor is primarily used internally when loading accounts from
164 /// repositories. Most applications should use `new()` which generates a random ID.
165 ///
166 /// # Arguments
167 /// * `account_id` - The UUID to use for this account
168 /// * `user_id` - Unique identifier for the user
169 /// * `roles` - Roles to assign to this account
170 /// * `groups` - Groups this account should belong to
171 #[cfg(feature = "storage-seaorm")]
172 pub(crate) fn new_with_account_id(
173 account_id: &Uuid,
174 user_id: &str,
175 roles: &[R],
176 groups: &[G],
177 ) -> Self {
178 let roles = roles.to_vec();
179 let groups = groups.to_vec();
180 Self {
181 account_id: account_id.to_owned(),
182 user_id: user_id.to_owned(),
183 groups,
184 roles,
185 permissions: Permissions::new(),
186 }
187 }
188
189 /// Consumes this account and returns it with the specified permissions.
190 ///
191 /// This is useful when building accounts with specific permission sets.
192 ///
193 /// # Example
194 /// ```rust
195 /// use axum_gate::accounts::Account;
196 /// use axum_gate::prelude::{Role, Group};
197 /// use axum_gate::permissions::Permissions;
198 ///
199 /// // Create permissions
200 /// let permissions: Permissions = ["read:profile", "write:profile"].into_iter().collect();
201 /// let account = Account::<Role, Group>::new("user@example.com", &[Role::User], &[])
202 /// .with_permissions(permissions);
203 /// ```
204 pub fn with_permissions(self, permissions: Permissions) -> Self {
205 Self {
206 permissions,
207 ..self
208 }
209 }
210
211 /// Grants a permission to this account.
212 ///
213 /// # Example
214 /// ```rust
215 /// use axum_gate::accounts::Account;
216 /// use axum_gate::prelude::{Role, Group};
217 /// use axum_gate::permissions::PermissionId;
218 ///
219 /// let mut account = Account::<Role, Group>::new("user", &[], &[]);
220 /// account.grant_permission("read:profile");
221 /// account.grant_permission(PermissionId::from("write:profile"));
222 /// ```
223 pub fn grant_permission<P>(&mut self, permission: P)
224 where
225 P: Into<PermissionId>,
226 {
227 self.permissions.grant(permission);
228 }
229
230 /// Revokes a permission from this account.
231 ///
232 /// # Example
233 /// ```rust
234 /// use axum_gate::accounts::Account;
235 /// use axum_gate::prelude::{Role, Group};
236 /// use axum_gate::permissions::PermissionId;
237 ///
238 /// let mut account = Account::<Role, Group>::new("user", &[], &[]);
239 /// account.grant_permission("write:profile");
240 /// account.revoke_permission(PermissionId::from("write:profile"));
241 /// ```
242 pub fn revoke_permission<P>(&mut self, permission: P)
243 where
244 P: Into<PermissionId>,
245 {
246 self.permissions.revoke(permission);
247 }
248
249 /// Returns true if this account has the given role.
250 ///
251 /// # Example
252 ///
253 /// ```rust
254 /// use axum_gate::accounts::Account;
255 /// use axum_gate::prelude::{Role, Group};
256 ///
257 /// let account = Account::<Role, Group>::new(
258 /// "user@example.com",
259 /// &[Role::User],
260 /// &[Group::new("engineering")]
261 /// );
262 ///
263 /// assert!(account.has_role(&Role::User));
264 /// assert!(!account.has_role(&Role::Admin));
265 /// ```
266 pub fn has_role(&self, role: &R) -> bool {
267 self.roles.contains(role)
268 }
269
270 /// Returns true if this account is a member of the given group.
271 ///
272 /// # Example
273 ///
274 /// ```rust
275 /// use axum_gate::accounts::Account;
276 /// use axum_gate::prelude::{Role, Group};
277 ///
278 /// let account = Account::<Role, Group>::new(
279 /// "user@example.com",
280 /// &[Role::User],
281 /// &[Group::new("engineering")]
282 /// );
283 ///
284 /// assert!(account.is_member_of(&Group::new("engineering")));
285 /// assert!(!account.is_member_of(&Group::new("marketing")));
286 /// ```
287 pub fn is_member_of(&self, group: &G) -> bool {
288 self.groups.contains(group)
289 }
290
291 /// Returns true if this account has the specified permission.
292 ///
293 /// Accepts any type that converts into `PermissionId` (e.g., `&str`, `PermissionId`).
294 ///
295 /// # Example
296 ///
297 /// ```rust
298 /// use axum_gate::accounts::Account;
299 /// use axum_gate::prelude::{Role, Group};
300 /// use axum_gate::permissions::PermissionId;
301 ///
302 /// let mut account = Account::<Role, Group>::new("user@example.com", &[], &[]);
303 /// account.grant_permission("read:api");
304 /// account.grant_permission(PermissionId::from("write:docs"));
305 ///
306 /// assert!(account.has_permission("read:api"));
307 /// assert!(account.has_permission(PermissionId::from("write:docs")));
308 /// assert!(!account.has_permission("admin:system"));
309 /// ```
310 pub fn has_permission<P>(&self, permission: P) -> bool
311 where
312 P: Into<PermissionId>,
313 {
314 self.permissions.has(permission)
315 }
316}
317
318#[cfg(feature = "storage-seaorm")]
319impl<R, G> TryFrom<crate::repositories::sea_orm::models::account::Model> for Account<R, G>
320where
321 R: AccessHierarchy + Eq + std::fmt::Display + Clone,
322 Vec<R>: CommaSeparatedValue,
323 G: Eq + Clone,
324 Vec<G>: CommaSeparatedValue,
325{
326 type Error = String;
327
328 fn try_from(
329 value: crate::repositories::sea_orm::models::account::Model,
330 ) -> Result<Self, Self::Error> {
331 Ok(Self::new_with_account_id(
332 &value.account_id,
333 &value.user_id,
334 &Vec::<R>::from_csv(&value.roles)?,
335 &Vec::<G>::from_csv(&value.groups)?,
336 ))
337 }
338}