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