oxinbox-backend 0.1.0

HTTP/WS API server for the oxinbox GTD productivity system
Documentation
use axum::Router;
use tower_http::cors::CorsLayer;
use tower_http::trace::TraceLayer;
use tracing_subscriber::EnvFilter;

use oxinbox_backend::{ai, auth, database, db, push, routes};

#[tokio::main]
async fn main() {
    if std::env::var("TRACING_FORMAT").as_deref() == Ok("json") {
        tracing_subscriber::fmt()
            .json()
            .with_env_filter(EnvFilter::try_from_default_env().unwrap_or_else(|_| "info".into()))
            .init();
    } else {
        tracing_subscriber::fmt()
            .pretty()
            .with_env_filter(EnvFilter::try_from_default_env().unwrap_or_else(|_| "info".into()))
            .with_target(true)
            .with_file(true)
            .with_line_number(true)
            .init();
    }

    dotenvy::dotenv().ok();
    tracing::info!("oxinbox backend starting");

    let pool = if let Ok(database_url) = std::env::var("DATABASE_URL") {
        let pool = db::create_pool(&database_url)
            .await
            .expect("failed to connect to database");
        db::run_migrations(&pool)
            .await
            .expect("failed to run migrations");
        tracing::info!("database connected and migrated");
        Some(pool)
    } else {
        tracing::warn!("DATABASE_URL not set, running without database");
        None
    };

    let db_repo = pool
        .as_ref()
        .map(|p| database::ParadeDbRepository::arc_new(p.clone()));

    let ai_provider = match ai::create_provider() {
        Ok(provider) => {
            tracing::info!("AI provider ready");
            Some(provider)
        }
        Err(e) => {
            tracing::warn!("AI provider not configured: {e}");
            None
        }
    };

    let push_service = push::PushService::new();
    let auth_state = auth::AuthState::new(ai_provider, db_repo, push_service);
    tracing::info!(
        "WebAuthn configured: rp_id={rp_id}",
        rp_id = std::env::var("RP_ID").unwrap_or_else(|_| "localhost".into())
    );

    push::start_background_worker(auth_state.clone());

    let app = Router::new()
        .merge(routes::api_routes(&auth_state))
        .layer(TraceLayer::new_for_http())
        .layer(CorsLayer::permissive())
        .with_state(auth_state);

    let port = std::env::var("PORT").unwrap_or_else(|_| "3300".into());
    let addr = format!("0.0.0.0:{port}");
    let listener = tokio::net::TcpListener::bind(&addr)
        .await
        .expect("failed to bind");

    tracing::info!("listening on {}", listener.local_addr().unwrap());

    axum::serve(listener, app).await.expect("server error");
}