Skip to content
use actix_web::{App, HttpResponse, HttpServer, Responder, get, web};
use db::Database;
use session::SessionStore;

mod assets;
mod auth;
mod config;
mod db;
mod middleware;
mod session;
mod view;

#[get("/up")]
async fn up() -> impl Responder {
    HttpResponse::Ok()
}

fn init_logging() {
    let mut builder = env_logger::Builder::from_env(
        env_logger::Env::default().default_filter_or("info,libsql=warn,turso=warn,tracing=warn"),
    );
    builder.filter_module("tracing", log::LevelFilter::Warn);
    let env_logger = builder.build();
    let _ = log::set_boxed_logger(Box::new(env_logger));
    log::set_max_level(log::LevelFilter::Debug);
}

fn spawn_database_init(db: Database, session_store: SessionStore) {
    actix_web::rt::spawn(async move {
        loop {
            match db.init().await {
                Ok(()) => {
                    log::info!("Database initialized successfully");
                    if let Err(e) = session_store.load().await {
                        log::warn!("Failed to load persisted sessions: {e}");
                    }
                    return;
                }
                Err(e) => {
                    log::warn!("Database init failed (will retry): {e}");
                    actix_web::rt::time::sleep(std::time::Duration::from_secs(1)).await;
                }
            }
        }
    });
}

#[actix_web::main]
async fn main() -> std::io::Result<()> {
    init_logging();
    let config = config::from_env();
    config.maybe_reset_database();

    let db = Database::new(&config.db_path);
    let session_store = SessionStore::new(db.clone());
    spawn_database_init(db.clone(), session_store.clone());

    let port = config.port;
    let cache_control = config.cache_control.clone();
    let config_data = web::Data::new(config);
    let db_data = web::Data::new(db);
    let session_data = web::Data::new(session_store);

    HttpServer::new(move || {
        App::new()
            .wrap(middleware::CacheControl::new(cache_control.clone()))
            .app_data(config_data.clone())
            .app_data(db_data.clone())
            .app_data(session_data.clone())
            .service(up)
            .service(assets::assets)
            .service(view::index)
            .service(view::verify_pin)
            .service(
                web::resource("/api/notify").route(web::post().to(view::post_notification_json)),
            )
            .service(
                web::resource("/api/notifications")
                    .route(web::post().to(view::post_notification_json)),
            )
            .service(
                web::resource("/notifications").route(web::post().to(view::post_notification_json)),
            )
            .service(view::notify_action)
            .service(view::notify::notify)
            .service(view::debug::debug)
            .service(view::install_page)
            .service(
                web::resource("/install/omarchy.sh")
                    .route(web::get().to(view::omarchy_installer_script)),
            )
            .service(
                web::resource("/install/omarchy")
                    .route(web::get().to(view::omarchy_installer_script)),
            )
            .service(
                web::resource("/api/install/omarchy.sh")
                    .route(web::get().to(view::omarchy_installer_script)),
            )
            .service(
                web::resource("/install/omarchy/download")
                    .route(web::get().to(view::omarchy_installer_script)),
            )
            .service(
                web::resource("/install/omarchy.tar.gz")
                    .route(web::get().to(view::omarchy_installer_script)),
            )
            .service(
                web::resource("/api/install/omarchy.tar.gz")
                    .route(web::get().to(view::omarchy_installer_script)),
            )
            .service(
                web::resource("/install/omarchy/plugin/{filename:.*}")
                    .route(web::get().to(view::omarchy_plugin_file)),
            )
            .service(
                web::resource("/install/omarchy/config")
                    .route(web::get().to(view::omarchy_plugin_config)),
            )
            .service(
                web::resource("/install/mac.sh").route(web::get().to(view::mac_installer_script)),
            )
            .service(
                web::resource("/install/linux.sh")
                    .route(web::get().to(view::linux_installer_script)),
            )
            .service(web::resource("/install/mac").route(web::get().to(view::mac_installer_script)))
            .service(
                web::resource("/install/macos.sh").route(web::get().to(view::mac_installer_script)),
            )
            .service(
                web::resource("/api/install/mac.sh")
                    .route(web::get().to(view::mac_installer_script)),
            )
            .service(
                web::resource("/download/{filename:.*}").route(web::get().to(view::download_file)),
            )
            .service(
                web::resource("/install/download/{filename:.*}")
                    .route(web::get().to(view::download_file)),
            )
            .service(
                web::resource("/api/download/{filename:.*}")
                    .route(web::get().to(view::download_file)),
            )
            .service(web::resource("/config/export").route(web::get().to(view::export_config)))
            .service(web::resource("/export").route(web::get().to(view::export_config)))
            .service(web::resource("/api/config/export").route(web::get().to(view::export_config)))
            .service(
                web::resource("/notifications/scan")
                    .route(web::post().to(view::scan_notifications))
                    .route(web::get().to(view::scan_notifications)),
            )
            .service(
                web::resource("/api/notifications/scan")
                    .route(web::post().to(view::scan_notifications))
                    .route(web::get().to(view::scan_notifications)),
            )
            .service(
                web::resource("/notifications/sent")
                    .route(web::post().to(view::mark_notifications_sent)),
            )
            .service(
                web::resource("/api/notifications/sent")
                    .route(web::post().to(view::mark_notifications_sent)),
            )
            .service(
                web::resource("/notifications/completed")
                    .route(web::post().to(view::mark_notifications_sent)),
            )
            .service(
                web::resource("/api/notifications/completed")
                    .route(web::post().to(view::mark_notifications_sent)),
            )
            .service(
                web::resource("/notifications/{id}/sent")
                    .route(web::post().to(view::mark_single_notification_sent)),
            )
            .service(
                web::resource("/api/notifications/{id}/sent")
                    .route(web::post().to(view::mark_single_notification_sent)),
            )
            .service(
                web::resource("/notifications/{id}/completed")
                    .route(web::post().to(view::mark_single_notification_sent)),
            )
            .service(
                web::resource("/api/notifications/{id}/completed")
                    .route(web::post().to(view::mark_single_notification_sent)),
            )
            .service(view::api_notify_action)
    })
    .bind(("0.0.0.0", port))?
    .run()
    .await
}

#[cfg(test)]
mod tests {
    use super::*;
    use actix_web::http::header::HeaderValue;
    use actix_web::test as aw_test;

    #[actix_web::test]
    async fn test_app_skips_browser_cache_when_cache_control_is_no_cache() {
        let config = config::Config::new(
            8080,
            HeaderValue::from_static("no-cache"),
            "notify.db".to_string(),
            false,
            "123456".to_string(),
        );
        let cache_control = config.cache_control.clone();
        let config_data = web::Data::new(config);
        let session_data = web::Data::new(SessionStore::in_memory());

        let app = aw_test::init_service(
            App::new()
                .wrap(middleware::CacheControl::new(cache_control))
                .app_data(config_data)
                .app_data(session_data)
                .service(up)
                .service(assets::assets)
                .service(view::index)
                .service(view::notify::notify),
        )
        .await;

        for path in ["/up", "/", "/notify", "/assets/app.css", "/assets/h.js"] {
            let req = aw_test::TestRequest::get().uri(path).to_request();
            let resp = aw_test::call_service(&app, req).await;
            assert!(resp.status().is_success(), "{path} should succeed");
            assert_eq!(
                resp.headers()
                    .get("cache-control")
                    .and_then(|v| v.to_str().ok()),
                Some("no-cache"),
                "{path} should skip browser cache with no-cache"
            );
        }
    }

    #[actix_web::test]
    async fn test_app_retains_default_cache_when_cache_control_not_set() {
        let config = config::Config::new(
            8080,
            None,
            "notify.db".to_string(),
            false,
            "123456".to_string(),
        );
        let cache_control = config.cache_control.clone();
        let config_data = web::Data::new(config);
        let session_data = web::Data::new(SessionStore::in_memory());

        let app = aw_test::init_service(
            App::new()
                .wrap(middleware::CacheControl::new(cache_control))
                .app_data(config_data)
                .app_data(session_data)
                .service(up)
                .service(assets::assets)
                .service(view::index)
                .service(view::notify::notify),
        )
        .await;

        let req = aw_test::TestRequest::get()
            .uri("/assets/app.css")
            .to_request();
        let resp = aw_test::call_service(&app, req).await;
        assert!(resp.status().is_success());
        assert_eq!(
            resp.headers()
                .get("cache-control")
                .and_then(|v| v.to_str().ok()),
            Some(config::DEFAULT_CACHE_CONTROL),
            "/assets/app.css should use DEFAULT_CACHE_CONTROL when unset"
        );

        let req = aw_test::TestRequest::get().uri("/").to_request();
        let resp = aw_test::call_service(&app, req).await;
        assert!(resp.status().is_success());
        assert_eq!(
            resp.headers().get("cache-control"),
            None,
            "/ should not have cache-control forced when unset"
        );
    }
}