athena_rs 1.1.0

Database gateway API
Documentation
//! Supabase Management API endpoints.
//!
//! This module provides endpoints for interacting with the Supabase Management API,
//! including SSL enforcement configuration.
//!
//! ## SSL Enforcement Endpoint
//!
//! **POST** `/api/v2/supabase/ssl_enforcement`
//!
//! Toggle SSL enforcement for a Supabase project's database connection.
//!
//! ### Request Body
//!
//! ```json
//! {
//!   "enabled": true,
//!   "access_token": "optional-override",
//!   "project_ref": "optional-override"
//! }
//! ```
//!
//! - `enabled` (required): Boolean indicating whether to enable or disable SSL enforcement.
//! - `access_token` (optional): Supabase access token. Falls back to `SUPABASE_ACCESS_TOKEN` env var.
//! - `project_ref` (optional): Supabase project reference. Falls back to `PROJECT_REF` env var.
//!
//! ### Response (Success)
//!
//! ```json
//! {
//!   "status": "success",
//!   "message": "SSL enforcement enabled",
//!   "data": {
//!     "currentConfig": { "database": true },
//!     "appliedSuccessfully": true
//!   }
//! }
//! ```
//!
//! ### Response (Error)
//!
//! ```json
//! {
//!   "status": "error",
//!   "message": "Failed to update SSL enforcement",
//!   "error": "HTTP 401: Unauthorized"
//! }
//! ```
//!
//! ### CLI Alternative
//!
//! This endpoint mirrors the functionality of the `scripts/ssl_enforcement.py` CLI tool.
//! Use the CLI for local development or scripting:
//!
//! ```bash
//! python scripts/ssl_enforcement.py enable --project PROJECT_REF --token TOKEN
//! ```

use actix_web::{HttpResponse, post, web};
use reqwest::{Error, Response, StatusCode};
use serde::{Deserialize, Serialize};
use std::env;
use tracing::error;
use web::{Data, Json};

use crate::AppState;
use crate::api::response::{api_success, bad_request, internal_error};

/// Supabase Management API base URL.
/// Floris; honestly this should be deprecated into a configurable / overriddable config
const SUPABASE_API_BASE: &str = "https://api.supabase.com/v1";

/// Request body for the SSL enforcement endpoint.
#[derive(Debug, Deserialize)]
pub struct SslEnforcementRequest {
    /// Whether to enable (true) or disable (false) SSL enforcement.
    pub enabled: bool,
    /// Optional Supabase access token override (falls back to SUPABASE_ACCESS_TOKEN env var).
    pub access_token: Option<String>,
    /// Optional project reference override (falls back to PROJECT_REF env var).
    pub project_ref: Option<String>,
}

/// Supabase SSL enforcement API response structure.
#[derive(Debug, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct SslEnforcementResponse {
    /// The current SSL configuration after the request.
    pub current_config: SslConfig,
    /// Whether the configuration was applied successfully.
    pub applied_successfully: bool,
}

/// SSL configuration for database connections.
#[derive(Debug, Serialize, Deserialize)]
pub struct SslConfig {
    /// Whether SSL is enforced for database connections.
    pub database: bool,
}

/// Payload sent to the Supabase Management API.
#[derive(Debug, Serialize)]
#[serde(rename_all = "camelCase")]
struct SslEnforcementPayload {
    requested_config: SslConfig,
}

/// Resolve credentials from request body or environment variables.
fn resolve_credentials(req: &SslEnforcementRequest) -> Result<(String, String), &'static str> {
    let access_token: String = req
        .access_token
        .clone()
        .or_else(|| env::var("SUPABASE_ACCESS_TOKEN").ok())
        .filter(|s| !s.is_empty())
        .ok_or("Missing Supabase access token. Provide via request body or SUPABASE_ACCESS_TOKEN env var.")?;

    let project_ref: String = req
        .project_ref
        .clone()
        .or_else(|| env::var("PROJECT_REF").ok())
        .filter(|s| !s.is_empty())
        .ok_or("Missing project reference. Provide via request body or PROJECT_REF env var.")?;

    Ok((access_token, project_ref))
}

#[post("/api/v2/supabase/ssl_enforcement")]
/// Toggle SSL enforcement for a Supabase project.
///
/// This endpoint calls the Supabase Management API to enable or disable SSL
/// enforcement on the project's database connections.
pub async fn ssl_enforcement(
    body: Json<SslEnforcementRequest>,
    app_state: Data<AppState>,
) -> HttpResponse {
    // Resolve credentials
    let (access_token, project_ref) = match resolve_credentials(&body) {
        Ok(creds) => creds,
        Err(err) => return bad_request("Missing credentials", err),
    };

    let url: String = format!(
        "{}/projects/{}/ssl-enforcement",
        SUPABASE_API_BASE, project_ref
    );

    let payload: SslEnforcementPayload = SslEnforcementPayload {
        requested_config: SslConfig {
            database: body.enabled,
        },
    };

    // Make the API request using the shared HTTP client
    let response: Result<Response, Error> = app_state
        .client
        .put(&url)
        .bearer_auth(&access_token)
        .json(&payload)
        .send()
        .await;

    match response {
        Ok(resp) => {
            let status: StatusCode = resp.status();

            if status.is_success() {
                match resp.json::<SslEnforcementResponse>().await {
                    Ok(data) => {
                        let message: &str = if body.enabled {
                            "SSL enforcement enabled"
                        } else {
                            "SSL enforcement disabled"
                        };

                        api_success(message, data)
                    }
                    Err(e) => {
                        error!(error = %e, "Failed to parse Supabase API response");
                        internal_error(
                            "Failed to parse API response",
                            format!("Invalid response format: {}", e),
                        )
                    }
                }
            } else {
                let error_text: String = resp
                    .text()
                    .await
                    .unwrap_or_else(|_| "Unknown error".to_string());
                error!(
                    status = %status,
                    error = %error_text,
                    "Supabase API returned error"
                );
                internal_error(
                    "Failed to update SSL enforcement",
                    format!("HTTP {}: {}", status, error_text),
                )
            }
        }
        Err(e) => {
            error!(error = %e, "Failed to connect to Supabase API");
            internal_error(
                "Failed to connect to Supabase API",
                format!("Connection error: {}", e),
            )
        }
    }
}