Files
fw-anwesenheit/src/webserver.rs

144 lines
3.7 KiB
Rust

use log::{error, info, warn};
use rocket::http::Status;
use rocket::response::stream::{Event, EventStream};
use rocket::serde::json::Json;
use rocket::{Config, Shutdown, State, post};
use rocket::{get, http::ContentType, response::content::RawHtml, routes};
use rust_embed::Embed;
use serde::Deserialize;
use std::borrow::Cow;
use std::env;
use std::ffi::OsStr;
use std::sync::Arc;
use tokio::select;
use tokio::sync::Mutex;
use tokio::sync::broadcast::Sender;
use crate::activity_fairing::ActivityNotifier;
use crate::id_mapping::{IDMapping, Name};
use crate::id_store::IDStore;
use crate::tally_id::TallyID;
#[derive(Embed)]
#[folder = "web/dist"]
struct Asset;
#[derive(Deserialize)]
struct NewMapping {
id: String,
name: Name,
}
pub async fn start_webserver(
store: Arc<Mutex<IDStore>>,
sse_broadcaster: Sender<String>,
fairing: ActivityNotifier,
) -> Result<(), rocket::Error> {
let port = match env::var("HTTP_PORT") {
Ok(port) => port.parse().unwrap_or_else(|_| {
warn!("Failed to parse HTTP_PORT. Using default 80");
80
}),
Err(_) => 80,
};
let config = Config {
address: "0.0.0.0".parse().unwrap(), // Listen on all interfaces
port,
..Config::default()
};
rocket::custom(config)
.attach(fairing)
.mount(
"/",
routes![
static_files,
index,
export_csv,
id_event,
get_mapping,
add_mapping
],
)
.manage(store)
.manage(sse_broadcaster)
.launch()
.await?;
Ok(())
}
#[get("/")]
fn index() -> Option<RawHtml<Cow<'static, [u8]>>> {
let asset = Asset::get("index.html")?;
Some(RawHtml(asset.data))
}
#[get("/<file..>")]
fn static_files(file: std::path::PathBuf) -> Option<(ContentType, Vec<u8>)> {
let filename = file.display().to_string();
let asset = Asset::get(&filename)?;
let content_type = file
.extension()
.and_then(OsStr::to_str)
.and_then(ContentType::from_extension)
.unwrap_or(ContentType::Bytes);
Some((content_type, asset.data.into_owned()))
}
#[get("/api/idevent")]
fn id_event(sse_broadcaster: &State<Sender<String>>, shutdown: Shutdown) -> EventStream![] {
let mut rx = sse_broadcaster.subscribe();
EventStream! {
loop {
select! {
msg = rx.recv() => {
if let Ok(id) = msg {
yield Event::data(id);
}
}
_ = &mut shutdown.clone() => {
// Shutdown signal received, exit the loop
break;
}
}
}
}
}
#[get("/api/csv")]
async fn export_csv(manager: &State<Arc<Mutex<IDStore>>>) -> Result<String, Status> {
info!("Exporting CSV");
match manager.lock().await.export_csv() {
Ok(csv) => Ok(csv),
Err(e) => {
error!("Failed to generate csv: {e}");
Err(Status::InternalServerError)
}
}
}
#[get("/api/mapping")]
async fn get_mapping(store: &State<Arc<Mutex<IDStore>>>) -> Json<IDMapping> {
Json(store.lock().await.mapping.clone())
}
#[post("/api/mapping", format = "json", data = "<new_mapping>")]
async fn add_mapping(store: &State<Arc<Mutex<IDStore>>>, new_mapping: Json<NewMapping>) -> Status {
if new_mapping.id.is_empty()
|| new_mapping.name.first.is_empty()
|| new_mapping.name.last.is_empty()
{
return Status::BadRequest;
}
store
.lock()
.await
.mapping
.add_mapping(TallyID(new_mapping.id.clone()), new_mapping.name.clone());
Status::Created
}