use anyhow::Result;
use chrono::{DateTime, Utc};
use std::sync::Arc;
use tokio::sync::RwLock;
use uuid::Uuid;
use async_trait::async_trait;
use crate::{error::KirinoError, models::identity::Identity};
fn identity_id(identity: &Identity) -> Uuid {
match identity {
Identity::Basic { id, .. } => *id,
Identity::Anonymous { id, .. } => *id,
Identity::Temporary { id, .. } => *id,
Identity::Service { id, .. } => *id,
}
}
#[async_trait]
pub trait IdentityProvider: Send + Sync {
async fn create(&self, record: IdentityRecord) -> Result<()>;
async fn get(&self, id: Uuid) -> Result<Option<IdentityRecord>>;
async fn find_by_username(&self, username: &str) -> Result<Option<IdentityRecord>>;
async fn delete(&self, id: Uuid) -> Result<bool>;
async fn list(&self) -> Result<Vec<IdentityRecord>>;
}
pub struct IdentityRecord {
pub identity: Identity,
pub username: String,
pub password_hash: String,
pub is_active: bool,
pub created_at: DateTime<Utc>,
pub updated_at: DateTime<Utc>,
}
impl Clone for IdentityRecord {
fn clone(&self) -> Self {
Self {
identity: self.identity.clone(),
username: self.username.clone(),
password_hash: self.password_hash.clone(),
is_active: self.is_active,
created_at: self.created_at,
updated_at: self.updated_at,
}
}
}
impl std::fmt::Debug for IdentityRecord {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("IdentityRecord")
.field("identity", &self.identity)
.field("username", &self.username)
.field("password_hash", &"[redacted]")
.field("is_active", &self.is_active)
.field("created_at", &self.created_at)
.field("updated_at", &self.updated_at)
.finish()
}
}
pub struct InMemoryIdentityProvider {
records: Arc<RwLock<Vec<IdentityRecord>>>,
}
impl InMemoryIdentityProvider {
#[must_use]
pub fn new() -> Self {
Self {
records: Arc::new(RwLock::new(Vec::new())),
}
}
}
impl Default for InMemoryIdentityProvider {
fn default() -> Self {
Self::new()
}
}
#[async_trait]
impl IdentityProvider for InMemoryIdentityProvider {
async fn create(&self, record: IdentityRecord) -> Result<()> {
let mut records = self.records.write().await;
if records.iter().any(|r| r.username == record.username) {
return Err(KirinoError::Validation("username already exists".to_string()).into());
}
if records
.iter()
.any(|r| identity_id(&r.identity) == identity_id(&record.identity))
{
return Err(KirinoError::Validation("identity already exists".to_string()).into());
}
records.push(record);
Ok(())
}
async fn get(&self, id: Uuid) -> Result<Option<IdentityRecord>> {
let records = self.records.read().await;
Ok(records
.iter()
.find(|r| identity_id(&r.identity) == id)
.cloned())
}
async fn find_by_username(&self, username: &str) -> Result<Option<IdentityRecord>> {
let records = self.records.read().await;
Ok(records.iter().find(|r| r.username == username).cloned())
}
async fn delete(&self, id: Uuid) -> Result<bool> {
let mut records = self.records.write().await;
let before = records.len();
records.retain(|r| identity_id(&r.identity) != id);
Ok(records.len() < before)
}
async fn list(&self) -> Result<Vec<IdentityRecord>> {
let records = self.records.read().await;
Ok(records.clone())
}
}
#[cfg(test)]
mod tests {
use super::*;
fn make_record(username: &str) -> IdentityRecord {
let id = Uuid::now_v7();
let now = Utc::now();
IdentityRecord {
identity: Identity::Basic {
id,
created_at: now,
},
username: username.to_string(),
password_hash: "hashed".to_string(),
is_active: true,
created_at: now,
updated_at: now,
}
}
#[tokio::test]
async fn test_create_and_get() {
let provider = InMemoryIdentityProvider::new();
let record = make_record("alice");
let id = identity_id(&record.identity);
provider.create(record).await.unwrap();
let found = provider.get(id).await.unwrap();
assert!(found.is_some());
assert_eq!(found.unwrap().username, "alice");
}
#[tokio::test]
async fn test_get_nonexistent() {
let provider = InMemoryIdentityProvider::new();
let found = provider.get(Uuid::now_v7()).await.unwrap();
assert!(found.is_none());
}
#[tokio::test]
async fn test_find_by_username() {
let provider = InMemoryIdentityProvider::new();
provider.create(make_record("bob")).await.unwrap();
provider.create(make_record("charlie")).await.unwrap();
let found = provider.find_by_username("bob").await.unwrap();
assert!(found.is_some());
assert_eq!(found.unwrap().username, "bob");
let not_found = provider.find_by_username("dave").await.unwrap();
assert!(not_found.is_none());
}
#[tokio::test]
async fn test_delete() {
let provider = InMemoryIdentityProvider::new();
let record = make_record("eve");
let id = identity_id(&record.identity);
provider.create(record).await.unwrap();
assert!(provider.delete(id).await.unwrap());
assert!(provider.get(id).await.unwrap().is_none());
}
#[tokio::test]
async fn test_delete_nonexistent() {
let provider = InMemoryIdentityProvider::new();
assert!(!provider.delete(Uuid::now_v7()).await.unwrap());
}
#[tokio::test]
async fn test_list() {
let provider = InMemoryIdentityProvider::new();
provider.create(make_record("u1")).await.unwrap();
provider.create(make_record("u2")).await.unwrap();
provider.create(make_record("u3")).await.unwrap();
let list = provider.list().await.unwrap();
assert_eq!(list.len(), 3);
}
#[tokio::test]
async fn test_list_empty() {
let provider = InMemoryIdentityProvider::new();
let list = provider.list().await.unwrap();
assert!(list.is_empty());
}
#[tokio::test]
async fn test_create_duplicate_username_rejected() {
let provider = InMemoryIdentityProvider::new();
provider.create(make_record("alice")).await.unwrap();
let err = provider.create(make_record("alice")).await.unwrap_err();
assert!(err
.downcast_ref::<KirinoError>()
.is_some_and(|e| matches!(e, KirinoError::Validation(_))));
}
}