use std::sync::Arc;
use axum::Router;
use axum::extract::{Request, State};
use axum::http::HeaderMap;
use axum::middleware::Next;
use axum::response::{IntoResponse, Response};
use axum::routing::any;
use chrono::Utc;
use crate::relational::Catalog;
use crate::sql::engine::{Database, Session};
use crate::sql::{ExecResult, RelationalStorage, SqlError, SqlValue};
use crate::supabase::error::SupaError;
use crate::supabase::jwt::Claims;
use crate::supabase::project::{ServiceConfig, SupabaseCompatProject};
pub struct AppState<S: RelationalStorage> {
pub db: Arc<Database<S>>,
pub project: Arc<SupabaseCompatProject>,
pub config: Arc<ServiceConfig>,
pub schema_ready: Arc<tokio::sync::OnceCell<()>>,
pub storage_ready: Arc<tokio::sync::OnceCell<()>>,
pub realtime: Arc<crate::supabase::realtime::RealtimeShared>,
}
impl<S: RelationalStorage> Clone for AppState<S> {
fn clone(&self) -> Self {
Self {
db: self.db.clone(),
project: self.project.clone(),
config: self.config.clone(),
schema_ready: self.schema_ready.clone(),
storage_ready: self.storage_ready.clone(),
realtime: self.realtime.clone(),
}
}
}
impl<S: RelationalStorage> AppState<S> {
pub fn new(
db: Arc<Database<S>>,
project: SupabaseCompatProject,
config: ServiceConfig,
) -> Self {
Self {
db,
project: Arc::new(project),
config: Arc::new(config),
schema_ready: Arc::new(tokio::sync::OnceCell::new()),
storage_ready: Arc::new(tokio::sync::OnceCell::new()),
realtime: Arc::new(crate::supabase::realtime::RealtimeShared::new()),
}
}
}
#[derive(Clone, Debug)]
pub struct AuthContext {
pub role: String,
pub api_key_role: String,
pub claims: Option<Claims>,
pub request_id: String,
}
impl AuthContext {
pub fn is_service_role(&self) -> bool {
self.role == "service_role"
}
pub fn user_id(&self) -> Option<&str> {
self.claims
.as_ref()
.and_then(|c| c.sub.as_deref())
.filter(|s| !s.is_empty())
}
pub fn claims_json(&self) -> String {
self.claims
.as_ref()
.and_then(|c| serde_json::to_string(c).ok())
.unwrap_or_else(|| serde_json::json!({ "role": self.role }).to_string())
}
}
#[derive(Clone, Debug)]
pub struct RequestId(pub String);
pub fn build_router<S: RelationalStorage + 'static>(state: AppState<S>) -> Router {
let apikey_layer = axum::middleware::from_fn_with_state(state.clone(), require_apikey::<S>);
let protected = Router::new()
.nest("/rest/v1", crate::supabase::rest::router::<S>())
.nest("/auth/v1", crate::supabase::auth::router::<S>())
.nest("/graphql/v1", crate::supabase::graphql::router::<S>())
.nest("/pg-meta", crate::supabase::pg_meta::router::<S>())
.nest("/platform/pg-meta", crate::supabase::pg_meta::router::<S>())
.layer(apikey_layer.clone());
let storage = Router::new()
.merge(crate::supabase::storage::protected_router::<S>().layer(apikey_layer.clone()))
.merge(crate::supabase::storage::public_router::<S>())
.fallback(crate::supabase::storage::unsupported_route);
Router::new()
.merge(protected)
.nest("/storage/v1", storage)
.nest("/realtime/v1", crate::supabase::realtime::router::<S>())
.merge(stub_router::<S>())
.route("/health", any(health))
.layer(axum::middleware::from_fn(request_id))
.with_state(state)
}
fn stub_router<S: RelationalStorage + 'static>() -> Router<AppState<S>> {
Router::new().route("/functions/v1/{*rest}", any(|| not_impl("FUNCTIONS")))
}
async fn not_impl(service: &'static str) -> Response {
SupaError::NotImplemented(service).into_response()
}
async fn health() -> Response {
axum::Json(serde_json::json!({"status": "ok", "service": "guardian-supabase"})).into_response()
}
pub async fn request_id(mut req: Request, next: Next) -> Response {
let rid = header_str(req.headers(), "x-request-id")
.map(str::to_string)
.unwrap_or_else(|| uuid::Uuid::new_v4().to_string());
req.extensions_mut().insert(RequestId(rid.clone()));
let mut resp = next.run(req).await;
if let Ok(value) = rid.parse() {
resp.headers_mut().insert("x-request-id", value);
}
resp
}
pub async fn require_apikey<S: RelationalStorage + 'static>(
State(state): State<AppState<S>>,
mut req: Request,
next: Next,
) -> Response {
let now = Utc::now().timestamp();
let headers = req.headers();
let apikey = header_str(headers, "apikey")
.map(str::to_string)
.or_else(|| bearer_token(headers).map(str::to_string));
let Some(apikey) = apikey else {
return SupaError::MissingApiKey.into_response();
};
let api_claims = match state.project.keys.verify_api_key(&apikey, now) {
Ok(c) => c,
Err(_) => return SupaError::InvalidApiKey.into_response(),
};
let api_key_role = api_claims.pg_role().to_string();
let claims = match bearer_token(headers) {
Some(tok) => match state.project.keys.verify_api_key(tok, now) {
Ok(c) => Some(c),
Err(e) => return SupaError::InvalidJwt(e).into_response(),
},
None => None,
};
let role = claims
.as_ref()
.map(|c| c.pg_role().to_string())
.unwrap_or_else(|| api_key_role.clone());
let request_id = req
.extensions()
.get::<RequestId>()
.map(|r| r.0.clone())
.or_else(|| header_str(req.headers(), "x-request-id").map(str::to_string))
.unwrap_or_else(|| uuid::Uuid::new_v4().to_string());
req.extensions_mut().insert(AuthContext {
role,
api_key_role,
claims,
request_id,
});
next.run(req).await
}
pub(crate) fn header_str<'a>(headers: &'a HeaderMap, name: &str) -> Option<&'a str> {
headers.get(name).and_then(|v| v.to_str().ok())
}
fn bearer_token(headers: &HeaderMap) -> Option<&str> {
header_str(headers, "authorization").and_then(|v| {
v.strip_prefix("Bearer ")
.or_else(|| {
v.strip_prefix("bearer ")
.or_else(|| v.strip_prefix("BEARER "))
})
.map(str::trim)
})
}
pub(crate) async fn load_catalog<S: RelationalStorage>(
db: &Database<S>,
) -> Result<Option<Catalog>, SupaError> {
match db.storage().load_catalog().await {
Ok(Some(json)) => serde_json::from_value(json)
.map(Some)
.map_err(|e| SupaError::Internal(format!("corrupt catalog: {e}"))),
Ok(None) => Ok(None),
Err(e) => Err(SupaError::Internal(format!("catalog load failed: {e}"))),
}
}
pub(crate) async fn run_sql<S: RelationalStorage + 'static>(
db: &Arc<Database<S>>,
role: &str,
sql: &str,
params: Vec<SqlValue>,
) -> Result<ExecResult, SqlError> {
let mut session = Session::new(db.clone(), role.to_string());
let prepared = session.prepare(sql)?;
session.execute_one(&prepared.statement, ¶ms).await
}
pub(crate) async fn run_sql_as<S: RelationalStorage + 'static>(
db: &Arc<Database<S>>,
auth: &AuthContext,
sql: &str,
params: Vec<SqlValue>,
) -> Result<ExecResult, SqlError> {
let mut session = Session::new(db.clone(), auth.role.clone());
session.set_var("request.jwt.claims", &auth.claims_json());
let prepared = session.prepare(sql)?;
session.execute_one(&prepared.statement, ¶ms).await
}
pub(crate) async fn run_batch<S: RelationalStorage + 'static>(
db: &Arc<Database<S>>,
role: &str,
sql: &str,
) -> Result<Vec<ExecResult>, SqlError> {
let mut session = Session::new(db.clone(), role.to_string());
session.execute(sql).await
}