//! M7.1 smoke service. //! //! A standalone Axum binary whose only job is to host the //! [`compliance_core::auth`] middleware + [`compliance_core::tenant_ctx`] //! extractor on three endpoints, so `scripts/smoke.sh` can prove the //! tenant-gating contract end-to-end before any auth-path PR merges. //! //! Endpoints: //! * `GET /api/v1/health` — public, never authenticated. //! * `GET /api/v1/echo` — protected read; returns the [`TenantContext`]. //! * `POST /api/v1/echo` — protected write; exercises the `Frozen → 402` //! gate on the same handler. //! //! Configuration (env): //! * `KEYCLOAK_URL` — e.g. `http://localhost:8080`. Required. //! * `KEYCLOAK_REALM` — e.g. `certifai`. Required. //! * `SMOKE_PORT` — defaults to `3010`. use std::sync::Arc; use axum::{middleware, routing::get, Extension, Json, Router}; use compliance_core::{ auth::{require_jwt_auth, require_tenant_status, JwksState}, tenant_ctx::TenantCtx, }; use serde::Serialize; use tokio::sync::RwLock; #[derive(Serialize)] struct EchoResponse { method: &'static str, tenant_id: String, tenant_slug: String, plan: String, status: String, products: Vec, org_roles: Vec, user_id: String, user_name: Option, } async fn health() -> Json { Json(serde_json::json!({ "ok": true })) } async fn echo_read(TenantCtx(ctx): TenantCtx) -> Json { Json(echo(ctx, "GET")) } async fn echo_write(TenantCtx(ctx): TenantCtx) -> Json { Json(echo(ctx, "POST")) } fn echo(ctx: compliance_core::TenantContext, method: &'static str) -> EchoResponse { EchoResponse { method, tenant_id: ctx.tenant_id, tenant_slug: ctx.tenant_slug, plan: ctx.plan, status: ctx.status.to_string(), products: ctx.products, org_roles: ctx.org_roles.iter().map(|r| format!("{r:?}")).collect(), user_id: ctx.user_id, user_name: ctx.user_name, } } #[tokio::main] async fn main() -> Result<(), Box> { tracing_subscriber::fmt() .with_env_filter( tracing_subscriber::EnvFilter::try_from_default_env() .unwrap_or_else(|_| tracing_subscriber::EnvFilter::new("info")), ) .init(); let kc_url = std::env::var("KEYCLOAK_URL") .map_err(|_| "KEYCLOAK_URL is required (e.g. http://localhost:8080)")?; let kc_realm = std::env::var("KEYCLOAK_REALM") .map_err(|_| "KEYCLOAK_REALM is required (e.g. certifai)")?; let port: u16 = std::env::var("SMOKE_PORT") .ok() .and_then(|s| s.parse().ok()) .unwrap_or(3010); let jwks_url = format!("{kc_url}/realms/{kc_realm}/protocol/openid-connect/certs"); let jwks_state = JwksState { jwks: Arc::new(RwLock::new(None)), jwks_url: jwks_url.clone(), }; // Layers execute outermost-first. The Extension must be registered // before `require_jwt_auth` so the middleware can read JwksState; the // status gate must run after JWT so `TenantContext` is in extensions. let app = Router::new() .route("/api/v1/health", get(health)) .route("/api/v1/echo", get(echo_read).post(echo_write)) .layer(middleware::from_fn(require_tenant_status)) .layer(middleware::from_fn(require_jwt_auth)) .layer(Extension(jwks_state)); let addr = format!("0.0.0.0:{port}"); let listener = tokio::net::TcpListener::bind(&addr).await?; tracing::info!( port, jwks = %jwks_url, "compliance-smoke listening — try `scripts/smoke.sh`" ); axum::serve(listener, app).await?; Ok(()) }