use std::io::Write;
use std::sync::Arc;
use crate::sql::sqlx::PgPool;
use axum::body::Body;
use axum::http::{header, Request, Response};
use tower::ServiceExt as _;
use super::error::TenancyError;
use super::operator_console::{self, SessionSecret};
use super::pools::TenantPools;
use super::resolver::ChainResolver;
use crate::tenancy::admin::TenantAdminBuilder;
#[derive(Debug, Clone)]
pub struct ServerConfig {
pub bind: String,
pub apex_domain: String,
pub operator_show_only: Vec<String>,
}
impl Default for ServerConfig {
fn default() -> Self {
Self {
bind: "0.0.0.0:8080".into(),
apex_domain: "localhost".into(),
operator_show_only: vec!["rustango_orgs".into(), "rustango_operators".into()],
}
}
}
impl ServerConfig {
#[must_use]
pub fn from_env() -> Self {
let mut cfg = Self::default();
if let Ok(v) = std::env::var("RUSTANGO_BIND") {
cfg.bind = v;
}
if let Ok(v) = std::env::var("RUSTANGO_APEX_DOMAIN") {
cfg.apex_domain = v;
}
cfg
}
}
pub async fn run<W: Write + Send>(
pools: Arc<TenantPools>,
registry_url: String,
config: ServerConfig,
writer: &mut W,
) -> Result<(), TenancyError> {
if !no_operators_warn(pools.registry(), writer).await? {
}
let session_secret = SessionSecret::from_env_or_random();
let operator_console =
operator_console::router(pools.registry().clone(), session_secret.clone());
let resolver = ChainResolver::standard(config.apex_domain.clone());
let tenant_admin = TenantAdminBuilder::new(pools.clone(), registry_url, resolver)
.with_session(session_secret)
.build();
let apex = config.apex_domain.clone();
let app = axum::Router::new().fallback_service(tower::service_fn({
let operator = operator_console.clone();
let tenants = tenant_admin.clone();
move |req: Request<Body>| {
let mut operator = operator.clone();
let mut tenants = tenants.clone();
let apex = apex.clone();
async move {
let host = req
.headers()
.get(header::HOST)
.and_then(|v| v.to_str().ok())
.map(|s| s.split(':').next().unwrap_or(s).to_owned())
.unwrap_or_default();
let response: Response<Body> = if host == apex {
operator.as_service().oneshot(req).await
} else {
tenants.as_service().oneshot(req).await
}
.map_err(|e| -> std::convert::Infallible {
panic!("axum router service is Infallible: {e}")
})?;
Ok::<_, std::convert::Infallible>(response)
}
}
}));
let listener = tokio::net::TcpListener::bind(&config.bind).await?;
let bound = listener.local_addr()?;
writeln!(writer, "==> rustango operator + tenant server")?;
writeln!(writer, " bound to {bound}")?;
writeln!(
writer,
" operator UI http://{}:{}/",
config.apex_domain,
bound.port()
)?;
writeln!(
writer,
" tenant URLs http://<slug>.{}:{}/<table>",
config.apex_domain,
bound.port()
)?;
writeln!(
writer,
" apex domain {} (override with RUSTANGO_APEX_DOMAIN)",
config.apex_domain
)?;
writeln!(writer, " Ctrl-C to stop")?;
writeln!(writer)?;
writer.flush()?;
axum::serve(listener, app)
.with_graceful_shutdown(shutdown_signal())
.await
.map_err(|e| TenancyError::Validation(format!("server error: {e}")))?;
Ok(())
}
async fn shutdown_signal() {
let _ = tokio::signal::ctrl_c().await;
tracing::info!(target: "crate::tenancy::server", "shutdown signal received");
}
async fn no_operators_warn<W: Write + Send>(
registry: &PgPool,
w: &mut W,
) -> Result<bool, TenancyError> {
use crate::core::Column as _;
use crate::sql::Fetcher;
let active: Vec<super::auth::Operator> = super::auth::Operator::objects()
.where_(super::auth::Operator::active.eq(true))
.fetch(registry)
.await?;
if active.is_empty() {
writeln!(w, "WARNING: no active operators in `rustango_operators` —")?;
writeln!(
w,
" the operator UI will reject every login until you run"
)?;
writeln!(
w,
" `cargo run -- create-operator <username> --password <p>`."
)?;
writeln!(w)?;
Ok(false)
} else {
Ok(true)
}
}