arcly-http-identity 0.8.0

CIAM building-block engine for arcly-http: identity store traits, password hashing, MFA (TOTP), passwordless & account lifecycle, risk signals, and an OIDC provider — storage-agnostic, zero-lock, all I/O behind traits.
Documentation
//! Single-use authorization-code storage.

use std::collections::HashMap;
use std::sync::RwLock;

use async_trait::async_trait;

use crate::error::Result;

/// A pending authorization code, bound to the client, user, redirect, scope, and
/// (for PKCE) the code challenge it was issued with.
#[derive(Debug, Clone)]
pub struct AuthCode {
    pub code: String,
    pub client_id: String,
    pub user_id: String,
    pub redirect_uri: String,
    pub scope: String,
    pub nonce: Option<String>,
    pub code_challenge: Option<String>,
    pub expires_at: u64,
}

/// Storage for authorization codes. Codes are single-use: [`take`](Self::take)
/// must atomically fetch-and-delete. Back with Redis (per-key TTL) in prod.
#[async_trait]
pub trait AuthCodeStore: Send + Sync + 'static {
    async fn put(&self, code: AuthCode) -> Result<()>;
    async fn take(&self, code: &str) -> Result<Option<AuthCode>>;
}

/// In-memory [`AuthCodeStore`] — tests / dev only.
#[derive(Default)]
pub struct MemoryAuthCodeStore {
    inner: RwLock<HashMap<String, AuthCode>>,
}

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

#[async_trait]
impl AuthCodeStore for MemoryAuthCodeStore {
    async fn put(&self, code: AuthCode) -> Result<()> {
        self.inner.write().unwrap().insert(code.code.clone(), code);
        Ok(())
    }
    async fn take(&self, code: &str) -> Result<Option<AuthCode>> {
        Ok(self.inner.write().unwrap().remove(code))
    }
}