Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

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::Error
  • mongodb::error::Error
  • diesel::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 found
  • None if 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 found
  • None if 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 data
  • password_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 verified
  • Ok(false) if not verified
  • Err(_) 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 UserStore trait.
  • 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_user is called.
  • update_user should persist any modifications made to a user record.
  • is_email_verified is 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.