Skip to content
use actix_identity::IdentityMiddleware;
use actix_session::{SessionMiddleware, config::PersistentSession};
use actix_web::{App, HttpResponse, HttpServer, Responder, cookie::time::Duration, get, web};
use db::Database;

mod assets;
mod auth;
mod config;
mod db;
#[cfg(test)]
mod integration_tests;
mod middleware;
mod models;
mod view;

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

#[get("/version")]
async fn version() -> impl Responder {
    HttpResponse::Ok().json(serde_json::json!({
        "version": env!("CARGO_PKG_VERSION"),
    }))
}

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);
}

async fn initialize_database(db: &Database) {
    loop {
        match db.init().await {
            Ok(()) => {
                log::info!("Database initialized successfully");
                return;
            }
            Err(e) => {
                log::warn!("Database init failed (will retry): {e}");
                actix_web::rt::time::sleep(std::time::Duration::from_secs(1)).await;
            }
        }
    }
}

/// Registers the public landing page, private task list, and authentication routes.
pub(crate) fn configure_routes(cfg: &mut web::ServiceConfig) {
    cfg.service(up)
        .service(version)
        .service(assets::assets)
        // Auth
        .service(view::auth::login_page)
        .service(view::auth::login_submit)
        .service(view::auth::signup_page)
        .service(view::auth::signup_submit)
        .service(view::auth::invite_page)
        .service(view::auth::create_invite)
        .service(view::auth::logout)
        // Tickets and home dashboard
        .service(view::tickets::home)
        .service(view::tickets::index)
        .service(view::tickets::new_ticket_page)
        .service(view::tickets::create)
        .service(view::tickets::close)
        .service(view::tickets::reopen);
}

#[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);
    initialize_database(&db).await;

    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.clone());

    let server = HttpServer::new(move || {
        App::new()
            .wrap(middleware::CacheControl::new(cache_control.clone()))
            .wrap(IdentityMiddleware::default())
            .wrap(
                SessionMiddleware::builder(
                    db::session_store::SqliteSessionStore::new(db.clone()),
                    config_data.session_key.clone(),
                )
                .cookie_name(auth::SESSION_COOKIE_NAME.to_string())
                .session_lifecycle(PersistentSession::default().session_ttl(Duration::days(30)))
                .build(),
            )
            .app_data(config_data.clone())
            .app_data(db_data.clone())
            .configure(configure_routes)
    })
    .bind(("0.0.0.0", port))?;

    log::info!("Listening on http://localhost:{port}");
    server.run().await
}

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

    #[actix_web::test]
    async fn test_health_and_asset_routes() {
        let config = config::Config::new(8080, None, "pear.db".to_string(), false);
        let app = aw_test::init_service(
            App::new()
                .app_data(web::Data::new(config))
                .app_data(web::Data::new(Database::new("pear.db")))
                .service(up)
                .service(version)
                .service(assets::assets),
        )
        .await;

        for path in ["/up", "/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");
        }

        let req = aw_test::TestRequest::get().uri("/version").to_request();
        let resp = aw_test::call_service(&app, req).await;
        assert!(resp.status().is_success());
        let body = aw_test::read_body(resp).await;
        assert_eq!(
            body,
            format!(r#"{{"version":"{}"}}"#, env!("CARGO_PKG_VERSION"))
        );
    }
}