mod auth; mod config; mod error; mod models; mod routes; mod seed; use auth::{generate_token, AuthUser}; use config::AppConfig; use error::ApiError; use models::{AppEvent, LoginRequest, LoginResponse, User}; use rocket::http::{Cookie, CookieJar, SameSite, Status}; use rocket::response::stream::{Event, EventStream}; use rocket::serde::json::Json; use rocket::time::Duration; use rocket::tokio::sync::broadcast::{self, error::RecvError}; use rocket::{get, post, routes, State}; use sqlx::postgres::PgPoolOptions; use sqlx::PgPool; pub struct AppState { pub db: PgPool, pub jwt_secret: String, pub jwt_issuer: String, pub jwt_ttl_seconds: i64, pub cookie_name: String, pub cookie_secure: bool, pub cookie_same_site: SameSite, pub event_sender: broadcast::Sender, } #[rocket::main] async fn main() -> Result<(), rocket::Error> { dotenvy::dotenv().ok(); let config = AppConfig::from_env().expect("Kunde inte läsa konfiguration"); let pool = PgPoolOptions::new() .max_connections(config.database_max_connections) .connect(&config.database_url) .await .expect("Kunde inte ansluta till databasen"); sqlx::migrate!() .run(&pool) .await .expect("Misslyckades att köra migrationer"); seed::run(&pool, &config.admin_username, &config.admin_password) .await .expect("Misslyckades att seed:a databasen"); let (event_sender, _) = broadcast::channel(128); let state = AppState { db: pool.clone(), jwt_secret: config.jwt_secret, jwt_issuer: config.jwt_issuer, jwt_ttl_seconds: config.jwt_ttl_seconds, cookie_name: config.cookie_name, cookie_secure: config.cookie_secure, cookie_same_site: config.cookie_same_site, event_sender, }; let rocket = rocket::build() .manage(state) .mount("/", routes![healthz, login, logout, events, public_events]) .mount("/persons", routes::persons::routes()) .mount("/tournament", routes::tournaments::routes()); rocket.launch().await?; Ok(()) } #[get("/healthz")] fn healthz() -> &'static str { "ok" } #[post("/login", data = "")] async fn login( cookies: &CookieJar<'_>, state: &State, payload: Json, ) -> Result, ApiError> { let username = payload.username.trim(); let password = payload.password.trim(); if username.is_empty() || password.is_empty() { return Err(ApiError::bad_request("Användarnamn och lösenord krävs.")); } let user = sqlx::query_as::<_, User>( r#" SELECT id, username, password_hash, created_at FROM users WHERE username = $1 "#, ) .bind(username) .fetch_optional(&state.db) .await? .ok_or_else(|| ApiError::unauthorized("Felaktigt användarnamn eller lösenord."))?; let valid = bcrypt::verify(password, &user.password_hash) .map_err(|_| ApiError::unauthorized("Felaktigt användarnamn eller lösenord."))?; if !valid { return Err(ApiError::unauthorized( "Felaktigt användarnamn eller lösenord.", )); } let token = generate_token(state.inner(), user.id, &user.username)?; let mut cookie = Cookie::new(state.cookie_name.clone(), token); cookie.set_http_only(true); cookie.set_same_site(Some(state.cookie_same_site)); cookie.set_path("/".to_string()); cookie.set_max_age(Duration::seconds(state.inner().jwt_ttl_seconds)); if state.cookie_secure { cookie.set_secure(true); } cookies.add(cookie); Ok(Json(LoginResponse { username: user.username, })) } #[post("/logout")] fn logout(cookies: &CookieJar<'_>, state: &State) -> Status { let mut cookie = Cookie::new(state.cookie_name.clone(), String::new()); cookie.set_http_only(true); cookie.set_same_site(Some(state.cookie_same_site)); cookie.set_path("/".to_string()); if state.cookie_secure { cookie.set_secure(true); } cookies.remove(cookie); Status::NoContent } #[get("/events")] fn events(_user: AuthUser, state: &State) -> EventStream![Event + '_] { let mut receiver = state.event_sender.subscribe(); EventStream! { loop { match receiver.recv().await { Ok(update) => yield Event::json(&update), Err(RecvError::Closed) => break, Err(RecvError::Lagged(_)) => continue, } } } } #[get("/events/public")] fn public_events(state: &State) -> EventStream![Event + '_] { let mut receiver = state.event_sender.subscribe(); EventStream! { loop { match receiver.recv().await { Ok(event) => { match &event { AppEvent::TournamentUpserted { .. } | AppEvent::TournamentDeleted { .. } => { yield Event::json(&event); } _ => continue, } } Err(RecvError::Closed) => break, Err(RecvError::Lagged(_)) => continue, } } } }