Skip to main content

postrust_proxy/saas/handlers/
mod.rs

1//! API handlers for SaaS domain management.
2
3pub mod auth;
4pub mod domains;
5pub mod routes;
6pub mod upstreams;
7pub mod wellknown;
8
9use crate::admin::api::ApiResponse;
10use crate::error::{ProxyError, ProxyResult};
11use crate::saas::api_keys::ApiKeyService;
12use crate::saas::auth::SaasAuthLayer;
13use crate::saas::manager::DomainManager;
14use crate::saas::verification::DomainVerificationService;
15use crate::tls::CertificateStore;
16use axum::{
17    middleware,
18    routing::{delete, get, post, put},
19    Router,
20};
21use sqlx::PgPool;
22use std::sync::Arc;
23
24/// Shared state for SaaS API handlers.
25#[derive(Clone)]
26pub struct SaasState {
27    /// Domains, their routes and their upstreams.
28    pub domain_manager: Arc<DomainManager>,
29    /// Creating, listing and revoking API keys.
30    pub api_key_service: Arc<ApiKeyService>,
31    /// The middleware that turns a credential into an [`AuthContext`].
32    ///
33    /// [`AuthContext`]: crate::saas::AuthContext
34    pub auth_layer: Arc<SaasAuthLayer>,
35    /// The pool, for handlers whose concern is not domain management.
36    ///
37    /// `wellknown` serves ACME challenges out of `proxy_acme_challenges`, which
38    /// is a TLS concern that has no business being a method on
39    /// `DomainManager`. The original code carried a comment saying it needed
40    /// this and did not have it, and answered with a placeholder instead.
41    pub pool: PgPool,
42
43    /// Where uploaded and issued certificates are kept.
44    pub cert_store: Arc<CertificateStore>,
45}
46
47impl SaasState {
48    /// Create new SaaS state.
49    /// Build the state.
50    ///
51    /// `cert_dir` is where certificates are cached on disk. A parameter rather
52    /// than a default, because a proxy that silently writes private keys to a
53    /// directory nobody chose is worse than one that makes you say where.
54    pub async fn new(
55        pool: PgPool,
56        jwt_secret: Option<String>,
57        cert_dir: impl AsRef<std::path::Path>,
58    ) -> ProxyResult<Self> {
59        let verification_service = Arc::new(DomainVerificationService::new());
60        let domain_manager = Arc::new(DomainManager::new(pool.clone(), verification_service));
61        let api_key_service = Arc::new(ApiKeyService::new(pool.clone()));
62        let pool_for_handlers = pool.clone();
63        let auth_layer = Arc::new(SaasAuthLayer::new(pool, jwt_secret));
64
65        let cert_store =
66            Arc::new(CertificateStore::new(pool_for_handlers.clone(), cert_dir).await?);
67
68        Ok(Self {
69            domain_manager,
70            api_key_service,
71            auth_layer,
72            pool: pool_for_handlers,
73            cert_store,
74        })
75    }
76}
77
78/// Create the SaaS API router.
79pub fn saas_router(state: SaasState) -> Router {
80    let auth_layer = state.auth_layer.clone();
81
82    // Public routes (no auth required)
83    let public_routes = Router::new().route(
84        "/.well-known/postrust-verification/{token}",
85        get(wellknown::handle_verification_challenge),
86    );
87
88    // Public because the CA fetching it has no credentials and cannot be given
89    // any. It only ever returns a value we put there ourselves. Gated with the
90    // issuer that writes those values.
91    #[cfg(feature = "acme")]
92    let public_routes = public_routes.route(
93        "/.well-known/acme-challenge/{token}",
94        get(wellknown::handle_acme_challenge),
95    );
96
97    // Protected routes (require authentication)
98    let protected_routes = Router::new();
99
100    // Queue certificate issuance -- and requeue it after a failure, which is
101    // the same operation. Gated because the worker that acts on it only exists
102    // with the `acme` feature: an endpoint that sets a status nothing drains
103    // would be worse than no endpoint.
104    #[cfg(feature = "acme")]
105    let protected_routes =
106        protected_routes.route("/domains/{id}/ssl/provision", post(domains::provision_ssl));
107
108    let protected_routes = protected_routes
109        // Auth / API Keys
110        .route("/auth/api-keys", post(auth::create_api_key))
111        .route("/auth/api-keys", get(auth::list_api_keys))
112        .route("/auth/api-keys/{id}", delete(auth::revoke_api_key))
113        // Tenant
114        .route("/tenant/me", get(auth::get_current_tenant))
115        .route("/tenant/usage", get(auth::get_tenant_usage))
116        // Domains
117        .route("/domains", get(domains::list_domains))
118        .route("/domains", post(domains::create_domain))
119        .route("/domains/{id}", get(domains::get_domain))
120        .route("/domains/{id}", delete(domains::delete_domain))
121        .route("/domains/{id}/verify", post(domains::verify_domain))
122        .route("/domains/{id}/enable", post(domains::enable_domain))
123        .route("/domains/{id}", put(domains::update_domain))
124        .route("/domains/{id}/disable", post(domains::disable_domain))
125        .route(
126            "/domains/{id}/ssl/upload",
127            post(domains::upload_certificate),
128        )
129        // Domain Routes
130        .route("/domains/{domain_id}/routes", get(routes::list_routes))
131        .route("/domains/{domain_id}/routes", post(routes::create_route))
132        .route("/domains/{domain_id}/routes/{id}", get(routes::get_route))
133        .route(
134            "/domains/{domain_id}/routes/{id}",
135            put(routes::update_route),
136        )
137        .route(
138            "/domains/{domain_id}/routes/{id}",
139            delete(routes::delete_route),
140        )
141        // Upstreams
142        .route("/upstreams", get(upstreams::list_upstreams))
143        .route("/upstreams", post(upstreams::create_upstream))
144        .route("/upstreams/{id}", get(upstreams::get_upstream))
145        .route("/upstreams/{id}", put(upstreams::update_upstream))
146        .route("/upstreams/{id}", delete(upstreams::delete_upstream))
147        .route("/upstreams/{id}/backends", post(upstreams::add_backend))
148        .route(
149            "/upstreams/{id}/backends/{backend_id}",
150            delete(upstreams::remove_backend),
151        )
152        .layer(middleware::from_fn_with_state(
153            auth_layer,
154            crate::saas::auth::auth_middleware,
155        ))
156        .with_state(state.clone());
157
158    // Combine public and protected routes
159    Router::new()
160        .merge(public_routes.with_state(state.clone()))
161        .nest("/api/v1", protected_routes)
162}
163
164/// Helper to convert ProxyError to API response.
165pub fn error_response(error: ProxyError) -> (axum::http::StatusCode, axum::Json<ApiResponse<()>>) {
166    let status = error.status_code();
167    let response = ApiResponse::<()>::error(error.to_string());
168    (status, axum::Json(response))
169}