use std::env;
use std::sync::Arc;
use std::time::Duration;
use m4a_product_example::{router, ProductApp, SqliteStore};
use m4a_product_kit::dbkey;
use m4a_product_kit::door::LoginDoor;
use m4a_product_kit::nick_rules::NickRules;
use m4a_product_kit::tiers::TierTable;
use m4a_product_kit::{send_reconcile, EdgeLink, EventPublisher, SqliteOutbox, UserService};
fn req(name: &str) -> Result<String, String> {
env::var(name).ok().filter(|v| !v.is_empty()).ok_or_else(|| format!("{name} is required"))
}
fn opt(name: &str) -> Option<String> {
env::var(name).ok().filter(|v| !v.is_empty())
}
async fn run() -> Result<(), String> {
let bind = opt("M4A_PRODUCT_BIND").unwrap_or_else(|| "127.0.0.1:8080".into());
let secret = req("M4A_ASSERTION_SECRET")?;
if secret.len() < 16 {
return Err("M4A_ASSERTION_SECRET must be at least 16 characters".into());
}
let db_path = req("M4A_PRODUCT_DB")?;
let store = Arc::new(SqliteStore::open(&dbkey::config_from_env(&db_path, "M4A_PRODUCT_DB_KEY_HEX")?).map_err(|e| format!("M4A_PRODUCT_DB (wrong key or not an encrypted database?): {e}"))?);
let link = EdgeLink::new(&req("M4A_PRODUCT_EDGE_URL")?, secret.into_bytes(), opt("M4A_ASSERTION_HEADER")).with_link_token(opt("M4A_LINK_TOKEN"));
let outbox_db = match opt("M4A_PRODUCT_OUTBOX_DB") {
None => store.db(),
Some(path) => {
let var = if opt("M4A_PRODUCT_OUTBOX_DB_KEY_HEX").is_some() { "M4A_PRODUCT_OUTBOX_DB_KEY_HEX" } else { "M4A_PRODUCT_DB_KEY_HEX" };
tesserax_store::Db::open(&dbkey::config_from_env(&path, var)?).map_err(|e| format!("event outbox: {e}"))?
}
};
let outbox = Arc::new(SqliteOutbox::new(outbox_db).map_err(|e| format!("event outbox: {e}"))?);
let events = EventPublisher::spawn_with(link.clone(), opt("M4A_EVENT_SIG_HEADER"), Duration::from_secs(1), outbox);
let mut doors: Vec<Arc<dyn LoginDoor>> = Vec::new();
#[cfg(feature = "matrix-address-door")]
if matches!(env::var("M4A_PRODUCT_LOGIN_MATRIX").as_deref(), Ok("1") | Ok("true")) {
use m4a_product_kit::door::{HttpUserinfo, MatrixAddressDoor};
let names: Vec<String> = opt("M4A_PRODUCT_LOCAL_NAMES").unwrap_or_default().split(',').map(|s| s.trim().to_string()).filter(|s| !s.is_empty()).collect();
if names.is_empty() {
return Err("M4A_PRODUCT_LOGIN_MATRIX needs M4A_PRODUCT_LOCAL_NAMES (own server names, refused as subjects)".into());
}
let ui = HttpUserinfo::new().with_overrides(&opt("M4A_PRODUCT_PEER_OVERRIDE").unwrap_or_default());
doors.push(Arc::new(MatrixAddressDoor::new(Arc::new(ui), names)));
}
let app = Arc::new(ProductApp {
users: UserService::new(store, NickRules::from_env()?, TierTable::from_env()?),
link,
events,
admin_token: opt("M4A_PRODUCT_ADMIN_TOKEN").unwrap_or_default(),
doors,
anon_read: opt("M4A_PRODUCT_ANON_READ").is_some_and(|v| v.eq_ignore_ascii_case("on")),
tokens: Default::default(),
challenges: m4a_product_kit::ChallengeBook::new(opt("M4A_PRODUCT_AUDIENCE").unwrap_or_else(|| "m4a-product".into())),
access_ttl_ms: opt("M4A_PRODUCT_ACCESS_TTL_SECS").and_then(|v| v.parse::<i64>().ok()).unwrap_or(3600) * 1000,
});
if !opt("M4A_PRODUCT_RECONCILE").is_some_and(|v| v.eq_ignore_ascii_case("off")) {
let (app, link, hdr) = (Arc::clone(&app), app.link.clone(), opt("M4A_EVENT_SIG_HEADER"));
tokio::spawn(async move {
let snaps = {
let app = Arc::clone(&app);
tokio::task::spawn_blocking(move || app.users.reconcile_snapshots(2000)).await.unwrap_or_else(|_| Err(m4a_product_kit::service::ServiceError::Internal("task failed".into())))
};
match snaps {
Ok(snaps) => {
for snap in snaps {
if let Err(e) = send_reconcile(&link, hdr.clone(), &snap, Duration::from_secs(1), 12).await {
tracing::error!(error = %e, "startup reconcile failed");
return;
}
}
tracing::info!("startup reconcile delivered");
}
Err(e) => tracing::error!(error = ?e, "startup reconcile: cannot read credentials"),
}
});
}
let listener = tokio::net::TcpListener::bind(&bind).await.map_err(|e| format!("bind {bind}: {e}"))?;
eprintln!("m4a-product-example listening on {bind}");
axum::serve(listener, router(app)).with_graceful_shutdown(async { let _ = tokio::signal::ctrl_c().await; }).await.map_err(|e| e.to_string())
}
#[tokio::main]
async fn main() {
if let Err(e) = run().await {
eprintln!("m4a-product-example: {e}");
std::process::exit(2);
}
}