181 lines
5.1 KiB
Rust
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,
|
|
}
|
|
}
|
|
}
|
|
}
|