allowthem-saas 0.0.9

SaaS multi-tenancy layer for allowthem
Documentation
use axum::extract::State;
use axum::response::Json;

use crate::api_keys::ApiKey;
use crate::control_db::TenantUsage;

use super::{AdminKey, ManageError, ManageState};
use axum::extract::Extension;

pub async fn get_usage(
    State(state): State<ManageState>,
    Extension(api_key): Extension<ApiKey>,
    _admin: AdminKey,
) -> Result<Json<Vec<TenantUsage>>, ManageError> {
    let tenant_id = api_key.tenant_id;
    let usage = state
        .control_db
        .usage_for_tenant(&tenant_id)
        .await
        .map_err(|e| ManageError::Internal(e.to_string()))?;
    Ok(Json(usage))
}

pub fn usage_routes() -> axum::Router<ManageState> {
    use axum::routing::get;
    axum::Router::new().route("/", get(get_usage))
}

#[cfg(test)]
mod tests {
    use std::path::PathBuf;
    use std::sync::Arc;

    use axum::Router;
    use axum::body::{Body, to_bytes};
    use axum::http::{Request, StatusCode};
    use chrono::Utc;
    use tower::ServiceExt;
    use uuid::Uuid;

    use super::*;
    use crate::api_keys::{ApiKey, ApiKeyId, ApiKeyScope};
    use crate::cache::HandleCache;
    use crate::control_db::ControlDb;
    use crate::manage::ManageState;
    use crate::tenants::{TenantBuilderConfig, TenantId};

    async fn make_state() -> ManageState {
        use std::str::FromStr;
        let opts = sqlx::sqlite::SqliteConnectOptions::from_str("sqlite::memory:")
            .unwrap()
            .pragma("foreign_keys", "ON");
        let pool = sqlx::SqlitePool::connect_with(opts).await.unwrap();
        let ctrl = Arc::new(ControlDb::new(pool).await.unwrap());
        ManageState::new(
            ctrl,
            HandleCache::new(1),
            PathBuf::from("/tmp"),
            Arc::new(TenantBuilderConfig {
                mfa_key: [0u8; 32],
                signing_key: [0u8; 32],
                csrf_key: [0u8; 32],
                base_domain: "test.example.com".into(),
                is_production: false,
                email_sender: None,
                event_sink: None,
                event_sink_factory: None,
                mau_sink: None,
                email_sender_factory: None,
            }),
            1000,
            Arc::new(crate::dns::MockDnsResolver::new()),
        )
    }

    fn make_admin_key() -> ApiKey {
        ApiKey {
            id: ApiKeyId::from_uuid(Uuid::nil()),
            tenant_id: TenantId::from(Uuid::nil()),
            name: "test".into(),
            scope: vec![ApiKeyScope::Admin],
            created_at: Utc::now(),
            expires_at: None,
            last_used_at: None,
        }
    }

    async fn test_router(state: ManageState) -> Router {
        Router::<ManageState>::new()
            .merge(usage_routes())
            .layer(axum::Extension(make_admin_key()))
            .with_state(state)
    }

    #[tokio::test]
    async fn usage_returns_empty_for_new_tenant() {
        let state = make_state().await;
        let app = test_router(state).await;
        let req = Request::get("/").body(Body::empty()).unwrap();
        let resp = app.oneshot(req).await.unwrap();
        assert_eq!(resp.status(), StatusCode::OK);
        let body = to_bytes(resp.into_body(), 4096).await.unwrap();
        let json: serde_json::Value = serde_json::from_slice(&body).unwrap();
        assert_eq!(json.as_array().unwrap().len(), 0);
    }
}