Implementing a User Store
The UserStore trait is responsible for user persistence.
AuthBox delegates all user-related database operations to this trait, allowing you to integrate any storage backend while keeping the authentication layer storage-agnostic.
Responsibilities
A user store implementation must support:
- Finding users by ID
- Finding users by email
- Creating users
- Updating users
- Deleting users
- Checking email verification status
Trait Overview
#![allow(unused)]
fn main() {
#[async_trait]
pub trait UserStore {
type Error;
type User: AuthUser;
type RegisterDto: RegisterUserInput;
async fn find_by_id(
&self,
user_id: &str,
) -> Option<Self::User>;
async fn find_by_email(
&self,
email: &str,
) -> Option<Self::User>;
async fn create_user(
&self,
input: Self::RegisterDto,
password_hash: String,
) -> Result<Self::User, Self::Error>;
async fn update_user(
&self,
user: Self::User,
) -> Result<Self::User, Self::Error>;
async fn delete_user(
&self,
user_id: &str,
) -> Result<(), Self::Error>;
async fn check_email_verified(
&self,
user_id: &str,
) -> Result<bool, Self::Error>;
}
}
Associated Types
User
The user model used by AuthBox.
This type must implement the AuthUser trait.
#![allow(unused)]
fn main() {
type User: AuthUser;
}
RegisterDto
The input type used when registering a new user.
This type must implement the RegisterUserInput trait.
#![allow(unused)]
fn main() {
type RegisterDto: RegisterUserInput;
}
Error
The error type returned by your storage backend.
#![allow(unused)]
fn main() {
type Error;
}
This can be:
sqlx::Errormongodb::error::Errordiesel::result::Error- A custom application error type
- Any other backend-specific error
Supported Backends
AuthBox does not require a specific database.
You can use any storage backend, including:
- PostgreSQL
- MySQL
- SQLite
- MongoDB
- Redis
- In-memory stores
- REST APIs
- gRPC services
- Custom storage layers
Required Methods
find_by_id
Finds a user by their unique identifier.
#![allow(unused)]
fn main() {
async fn find_by_id(
&self,
user_id: &str,
) -> Option<Self::User>;
}
Returns:
Some(user)if foundNoneif the user does not exist
find_by_email
Finds a user by email address.
#![allow(unused)]
fn main() {
async fn find_by_email(
&self,
email: &str,
) -> Option<Self::User>;
}
Returns:
Some(user)if foundNoneif the user does not exist
create_user
Creates a new user record.
#![allow(unused)]
fn main() {
async fn create_user(
&self,
input: Self::RegisterDto,
password_hash: String,
) -> Result<Self::User, Self::Error>;
}
Parameters:
input— Registration datapassword_hash— Already-hashed password generated by AuthBox
Returns the newly created user.
update_user
Updates an existing user.
#![allow(unused)]
fn main() {
async fn update_user(
&self,
user: Self::User,
) -> Result<Self::User, Self::Error>;
}
Returns the updated user.
delete_user
Deletes a user from storage.
#![allow(unused)]
fn main() {
async fn delete_user(
&self,
user_id: &str,
) -> Result<(), Self::Error>;
}
Returns:
#![allow(unused)]
fn main() {
Ok(())
}
when the operation succeeds.
check_email_verified
Checks whether a user has verified their email address.
#![allow(unused)]
fn main() {
async fn check_email_verified(
&self,
user_id: &str,
) -> Result<bool, Self::Error>;
}
Returns:
Ok(true)if verifiedOk(false)if not verifiedErr(_)if the backend operation fails
Example: In-Memory User Store
The following example demonstrates a simple in-memory implementation using a HashMap.
#![allow(unused)]
fn main() {
use async_trait::async_trait;
use std::collections::HashMap;
use std::sync::{Arc, Mutex};
#[derive(Clone)]
pub struct UserStoreImpl {
pub users: Arc<Mutex<HashMap<String, User>>>,
}
impl UserStoreImpl {
pub fn new() -> Self {
Self {
users: Arc::new(Mutex::new(HashMap::new())),
}
}
}
#[async_trait]
impl UserStore for UserStoreImpl {
type Error = String;
type User = User;
// Custom registration DTO
type RegisterDto = RegisterDto;
async fn find_by_id(
&self,
user_id: &str,
) -> Option<User> {
let users = self.users.lock().unwrap();
users.get(user_id).cloned()
}
async fn find_by_email(
&self,
email: &str,
) -> Option<User> {
let users = self.users.lock().unwrap();
users
.values()
.find(|u| u.email == email)
.cloned()
}
async fn create_user(
&self,
input: RegisterDto,
password_hash: String,
) -> Result<User, Self::Error> {
let mut users = self.users.lock().unwrap();
let user = User {
id: input.email.clone(),
email: input.email,
password_hash,
is_email_verified: false,
username: input.username,
phone: input.phone,
country: input.country,
city: input.city,
age: input.age,
};
users.insert(user.id.clone(), user.clone());
Ok(user)
}
async fn update_user(
&self,
user: User,
) -> Result<User, Self::Error> {
let mut users = self.users.lock().unwrap();
users.insert(user.id.clone(), user.clone());
Ok(user)
}
async fn delete_user(
&self,
user_id: &str,
) -> Result<(), Self::Error> {
let mut users = self.users.lock().unwrap();
users.remove(user_id);
Ok(())
}
async fn check_email_verified(
&self,
user_id: &str,
) -> Result<bool, Self::Error> {
let users = self.users.lock().unwrap();
Ok(
users
.get(user_id)
.map(|u| u.is_email_verified)
.unwrap_or(false)
)
}
}
}
Notes
- AuthBox never directly interacts with your database.
- All user persistence is handled through the
UserStoretrait. - You are free to use any ORM, query builder, or database driver.
- The store implementation should be thread-safe if used in a concurrent environment.
- Password hashing is performed by AuthBox before
create_useris called. update_usershould persist any modifications made to a user record.is_email_verifiedis used internally to determine whether a user has completed email verification.
By implementing UserStore, AuthBox can operate on top of virtually any persistence layer without requiring database-specific integrations.