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