kirino 0.3.0

A customizable, zero-trust authentication framework.
Documentation
use anyhow::{anyhow, Result};
use std::collections::HashMap;
use uuid::Uuid;

use async_trait::async_trait;

use crate::service::login::{UserDatabase, UserRecord};

#[derive(Clone, Default)]
pub struct InMemoryUserDatabase {
    users: std::sync::Arc<tokio::sync::RwLock<HashMap<String, UserRecord>>>,
}

impl InMemoryUserDatabase {
    pub fn new() -> Self {
        Self::default()
    }
}

#[async_trait]
impl UserDatabase for InMemoryUserDatabase {
    async fn create_user(&self, user: &UserRecord) -> Result<()> {
        let mut users = self.users.write().await;
        users.insert(user.username.clone(), user.clone());
        Ok(())
    }

    async fn find_by_username(&self, username: &str) -> Result<Option<UserRecord>> {
        let users = self.users.read().await;
        Ok(users.get(username).cloned())
    }

    async fn find_by_id(&self, id: &Uuid) -> Result<Option<UserRecord>> {
        let users = self.users.read().await;
        Ok(users.values().find(|u| &u.id == id).cloned())
    }

    async fn update_password(&self, id: &Uuid, new_hash: &str) -> Result<()> {
        let mut users = self.users.write().await;
        let user = users
            .values_mut()
            .find(|u| &u.id == id)
            .ok_or_else(|| anyhow!("user not found"))?;
        user.password_hash = new_hash.to_string();
        user.updated_at = chrono::Utc::now();
        Ok(())
    }

    async fn delete_user(&self, id: &Uuid) -> Result<bool> {
        let mut users = self.users.write().await;
        let username = users
            .values()
            .find(|u| &u.id == id)
            .map(|u| u.username.clone());
        match username {
            Some(name) => Ok(users.remove(&name).is_some()),
            None => Ok(false),
        }
    }

    async fn list_users(&self) -> Result<Vec<UserRecord>> {
        let users = self.users.read().await;
        Ok(users.values().cloned().collect())
    }

    async fn count_users(&self) -> Result<u64> {
        let users = self.users.read().await;
        Ok(users.len() as u64)
    }
}

#[cfg(test)]
mod tests {
    use super::*;
    use crate::models::identity::Identity;

    fn make_user(username: &str) -> UserRecord {
        UserRecord {
            id: Uuid::now_v7(),
            username: username.to_string(),
            password_hash: "hash".to_string(),
            display_name: None,
            is_active: true,
            identity: Identity::Basic { id: Uuid::now_v7() },
            created_at: chrono::Utc::now(),
            updated_at: chrono::Utc::now(),
        }
    }

    #[tokio::test]
    async fn test_crud() {
        let db = InMemoryUserDatabase::new();
        let user = make_user("alice");
        db.create_user(&user).await.unwrap();

        let found = db.find_by_username("alice").await.unwrap().unwrap();
        assert_eq!(found.username, "alice");

        let listed = db.list_users().await.unwrap();
        assert_eq!(listed.len(), 1);

        let count = db.count_users().await.unwrap();
        assert_eq!(count, 1);

        assert!(db.delete_user(&user.id).await.unwrap());
        assert!(db.find_by_username("alice").await.unwrap().is_none());
    }
}