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