vbytes-lan-attendence/api/src/main.rs

181 lines
5.1 KiB
Rust

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<AppEvent>,
}
#[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 = "<payload>")]
async fn login(
cookies: &CookieJar<'_>,
state: &State<AppState>,
payload: Json<LoginRequest>,
) -> Result<Json<LoginResponse>, 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<AppState>) -> 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<AppState>) -> 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<AppState>) -> 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,
}
}
}
}