mod auth_guard; mod business_metrics; mod db; mod irc; mod lua; mod manager; mod routes; mod state; use std::sync::Arc; use axum::{Router, routing::{get, post}}; use tower_http::services::ServeDir; use openid::DiscoveredClient; use state::AppState; use tower_sessions::{MemoryStore, SessionManagerLayer, cookie::SameSite}; use irc_now_common::auth::OidcConfig; use irc_now_common::db::DbConfig; use sqlx::PgPool; async fn health() -> &'static str { "ok" } async fn cleanup_old_logs(db: PgPool) { let mut interval = tokio::time::interval(std::time::Duration::from_secs(3600)); loop { interval.tick().await; match db::cleanup_old_logs(&db, 7).await { Ok(n) => { if n > 0 { tracing::info!("cleaned up {n} old bot log entries"); } } Err(e) => tracing::error!("log cleanup failed: {e}"), } } } #[tokio::main] async fn main() { tracing_subscriber::fmt() .with_env_filter(tracing_subscriber::EnvFilter::from_default_env()) .init(); let db = DbConfig::from_env("BOT").connect().await.unwrap(); let oidc = OidcConfig::from_env(); let issuer_url = url::Url::parse(&oidc.issuer_url).expect("invalid OIDC issuer URL"); tracing::info!("discovering OIDC provider at {issuer_url}"); let oidc_client = DiscoveredClient::discover( oidc.client_id.clone(), oidc.client_secret.clone(), Some(oidc.redirect_url.clone()), issuer_url, ) .await .expect("OIDC discovery failed"); tracing::info!("OIDC provider discovered"); let bot_manager = Arc::new(manager::BotManager::new(db.clone())); let state = AppState { db: db.clone(), oidc, oidc_client: Arc::new(oidc_client), bot_manager: bot_manager.clone(), }; irc_now_common::metrics::setup_metrics_recorder(); let metrics_db = db.clone(); tokio::spawn(cleanup_old_logs(db)); tokio::spawn(business_metrics::record_bot_metrics(metrics_db)); { let mgr = bot_manager.clone(); tokio::spawn(async move { mgr.start_all_enabled().await; }); } let session_store = MemoryStore::default(); let session_layer = SessionManagerLayer::new(session_store).with_same_site(SameSite::Lax); let app = Router::new() .route("/health", get(health)) .route("/metrics", get(irc_now_common::metrics::metrics_handler)) .route("/", get(|| async { axum::response::Redirect::temporary("/my") })) .route("/my", get(routes::bot::list)) .route("/create", get(routes::bot::create_form).post(routes::bot::create)) .route("/{id}", get(routes::bot::detail)) .route("/{id}/update", post(routes::bot::update)) .route("/{id}/delete", post(routes::bot::delete)) .route("/{id}/start", post(routes::bot::start)) .route("/{id}/stop", post(routes::bot::stop)) .route("/{id}/scripts/create", get(routes::script::create_form).post(routes::script::create)) .route("/{id}/scripts/{sid}", get(routes::script::edit)) .route("/{id}/scripts/{sid}/update", post(routes::script::update)) .route("/{id}/scripts/{sid}/delete", post(routes::script::delete)) .route("/{id}/logs", get(routes::logs::history)) .route("/{id}/logs/ws", get(routes::logs::ws_upgrade)) .route("/{id}/kv", get(routes::kv::list)) .route("/{id}/kv/set", post(routes::kv::set)) .route("/{id}/kv/{key}/delete", post(routes::kv::delete)) .route("/auth/login", get(routes::auth::login)) .route("/auth/callback", get(routes::auth::callback)) .route("/auth/logout", get(routes::auth::logout)) .nest_service("/static", ServeDir::new("static")) .layer(session_layer) .layer(axum::middleware::from_fn(irc_now_common::metrics::track_metrics)) .with_state(state); let listener = tokio::net::TcpListener::bind("0.0.0.0:8080").await.unwrap(); tracing::info!("listening on {}", listener.local_addr().unwrap()); axum::serve(listener, app).await.unwrap(); }