mirror of
https://github.com/Djeeberjr/fw-anwesenheit.git
synced 2026-04-30 18:49:09 +00:00
144 lines
3.7 KiB
Rust
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
|
|
}
|