arcly-http 0.1.2

Enterprise-grade NestJS-inspired web framework on axum: zero-lock DI, declarative controllers, multi-tenant data routing, transactional outbox, ABAC, and a self-documenting OpenAPI surface
Documentation
//! OAuth 2.0 provider abstraction.
//!
//! The framework provides the trait and supporting types. Concrete providers
//! (Google, GitHub, etc.) are implemented in the application layer and
//! registered with `OAuth2Service` in the app's `ArclyPlugin::on_init`.
//!
//! ## Flow (Authorization Code + PKCE)
//!
//! 1. `GET /oauth/{provider}/authorize`
//!    — controller calls `provider.authorize_url()` which returns
//!    `(url, pkce_verifier, csrf_state)`.
//!    — controller stores `oauth_state::{state}` → `{verifier}::{provider}`
//!    in Redis with a 10-minute TTL.
//!    — returns `{ "url": "..." }` to the client.
//!
//! 2. `GET /oauth/{provider}/callback?code=&state=`
//!    — controller looks up state in Redis, extracts verifier + provider.
//!    — calls `provider.exchange_code(code, pkce_verifier)` → access token.
//!    — calls `provider.fetch_user_info(access_token)` → `OAuth2UserInfo`.
//!    — upserts local user, mints JWT + session, returns both.

use std::collections::HashMap;

use futures::future::BoxFuture;
use serde::{Deserialize, Serialize};

// ─── User info ────────────────────────────────────────────────────────────────

/// Normalised user information returned by any OAuth2 provider.
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct OAuth2UserInfo {
    /// Provider name, e.g. `"google"`, `"github"`.
    pub provider: String,
    /// The provider's unique identifier for this account.
    pub provider_id: String,
    pub email: Option<String>,
    pub name: Option<String>,
    pub avatar_url: Option<String>,
    /// Raw provider response (for debugging / storing extra fields).
    pub raw: serde_json::Value,
}

// ─── Provider trait ───────────────────────────────────────────────────────────

/// Implement this to add an OAuth2 provider to the application.
///
/// Each implementation is responsible for:
/// - Generating the authorization URL with a fresh PKCE challenge.
/// - Exchanging the authorization code for an access token (via HTTP POST).
/// - Fetching the user's profile from the provider's user-info endpoint.
pub trait OAuth2Provider: Send + Sync + 'static {
    /// Short lowercase name identifying this provider, e.g. `"google"`.
    fn name(&self) -> &'static str;

    /// Generate an authorization URL for the OAuth2 flow.
    ///
    /// Returns `(authorize_url, pkce_verifier, csrf_state)`.
    /// The caller stores `oauth_state::{csrf_state}` → `{pkce_verifier}::{provider}`
    /// in Redis (or another short-lived store) before redirecting the user.
    fn authorize_url(&self) -> (String, String, String);

    /// Exchange an authorization code for a provider access token.
    ///
    /// `pkce_verifier` is the plain verifier string stored in Redis after
    /// `authorize_url()` was called.
    fn exchange_code<'a>(
        &'a self,
        code: &'a str,
        pkce_verifier: &'a str,
    ) -> BoxFuture<'a, Result<String, String>>;

    /// Fetch the user's profile using the access token from `exchange_code`.
    fn fetch_user_info<'a>(
        &'a self,
        access_token: &'a str,
    ) -> BoxFuture<'a, Result<OAuth2UserInfo, String>>;
}

// ─── Service ──────────────────────────────────────────────────────────────────

/// Registry of OAuth2 providers keyed by `provider.name()`.
///
/// Not `#[Injectable]` — provide manually via `ctx.provide(OAuth2Service::new())`.
pub struct OAuth2Service {
    providers: HashMap<String, Box<dyn OAuth2Provider>>,
}

impl Default for OAuth2Service {
    fn default() -> Self {
        Self::new()
    }
}

impl OAuth2Service {
    pub fn new() -> Self {
        Self {
            providers: HashMap::new(),
        }
    }

    /// Register a provider. The key is `provider.name()`.
    pub fn register(&mut self, p: impl OAuth2Provider) -> &mut Self {
        self.providers.insert(p.name().to_owned(), Box::new(p));
        self
    }

    /// Look up a provider by name.
    pub fn get(&self, name: &str) -> Option<&dyn OAuth2Provider> {
        self.providers.get(name).map(|b| b.as_ref())
    }
}