feat: event management and websocket for updates

This commit is contained in:
Andrew Rioux
2025-02-22 16:04:15 -05:00
parent 005048f1ce
commit faaa4d2d1a
48 changed files with 1409 additions and 204 deletions

View File

@@ -56,6 +56,7 @@ pub fn App() -> impl IntoView {
let login = ServerAction::<Login>::new();
#[cfg_attr(not(feature = "hydrate"), allow(unused_variables))]
let (user_res, set_user_res) = signal(None::<User>);
let user = Resource::new(move || login.version().get(), |_| async { me().await });
@@ -77,11 +78,11 @@ pub fn App() -> impl IntoView {
<Router>
<nav>
<h1>"Sparse control"</h1>
<h1>"Sparse Control"</h1>
<A href="/">"Home"</A>
{move || match user_res.get() {
Some(_) => Either::Left(view! {
<A href="/beacons">"Beacon management"</A>
<A href="/beacons">"Beacon Management"</A>
<A href="/users">"Users"</A>
<a
href="#"
@@ -101,18 +102,18 @@ pub fn App() -> impl IntoView {
}}
</nav>
<crate::beacons::BeaconSidebar />
<crate::beacons::sidebar::BeaconSidebar />
<Routes fallback=|| "Page not found.".into_view()>
<Route path=path!("users") view=crate::users::UserView />
<Route path=path!("login") view=move || view! { <LoginPage login/> }/>
<ParentRoute path=path!("beacons") view=crate::beacons::BeaconView>
<Route path=path!("categories") view=crate::beacons::CategoriesView/>
<Route path=path!("commands") view=crate::beacons::CommandsView/>
<Route path=path!("configs") view=crate::beacons::ConfigsView/>
<Route path=path!("templates") view=crate::beacons::TemplatesView/>
<Route path=path!("instances") view=crate::beacons::InstancesView/>
<Route path=path!("listeners") view=crate::beacons::ListenersView/>
<Route path=path!("categories") view=crate::beacons::categories::CategoriesView/>
<Route path=path!("commands") view=crate::beacons::commands::CommandsView/>
<Route path=path!("configs") view=crate::beacons::configs::ConfigsView/>
<Route path=path!("templates") view=crate::beacons::templates::TemplatesView/>
<Route path=path!("instances") view=crate::beacons::instances::InstancesView/>
<Route path=path!("listeners") view=crate::beacons::listeners::ListenersView/>
<Route path=path!("") view=|| view! {
<p>"Select a menu item on the left to get started"</p>
}/>

View File

@@ -1,25 +1,13 @@
use leptos::prelude::*;
use leptos_router::{components::A, nested_router::Outlet};
mod categories;
mod commands;
mod configs;
mod instances;
mod listeners;
mod templates;
#[allow(dead_code)]
pub use categories::CategoriesView;
#[allow(dead_code)]
pub use commands::CommandsView;
#[allow(dead_code)]
pub use configs::ConfigsView;
#[allow(dead_code)]
pub use instances::InstancesView;
#[allow(dead_code)]
pub use listeners::ListenersView;
#[allow(dead_code)]
pub use templates::TemplatesView;
pub mod categories;
pub mod commands;
pub mod configs;
pub mod instances;
pub mod listeners;
pub mod templates;
pub mod sidebar;
#[derive(Clone)]
pub struct BeaconResources {
@@ -39,6 +27,9 @@ pub struct BeaconResources {
templates: Resource<Result<Vec<templates::BeaconTemplate>, ServerFnError>>,
}
// For some reason, this function "isn't used"
// See app.rs:72
#[allow(dead_code)]
pub fn provide_beacon_resources() {
let user = expect_context::<ReadSignal<Option<crate::users::User>>>();
@@ -146,53 +137,3 @@ pub fn BeaconView() -> impl IntoView {
}
}
enum SortMethod {
Listener,
Config,
Category,
Template,
}
impl std::str::FromStr for SortMethod {
type Err = ();
fn from_str(s: &str) -> Result<Self, Self::Err> {
match s {
"Listener" => Ok(Self::Listener),
"Config" => Ok(Self::Config),
"Category" => Ok(Self::Category),
"Template" => Ok(Self::Template),
&_ => Err(()),
}
}
}
impl std::string::ToString for SortMethod {
fn to_string(&self) -> String {
use SortMethod as SM;
match self {
SM::Listener => "Listener",
SM::Config => "Config",
SM::Category => "Category",
SM::Template => "Template",
}
.to_string()
}
}
#[component]
pub fn BeaconSidebar() -> impl IntoView {
let (sort_method, set_sort_method) = signal(SortMethod::Category);
let search_input = RwSignal::new("".to_string());
view! {
<aside class="beacons">
<div class="sort-method">
</div>
<div class="search">
</div>
</aside>
}
}

View File

@@ -1,7 +1,7 @@
use leptos::{either::Either, prelude::*};
use serde::{Deserialize, Serialize};
#[cfg(feature = "ssr")]
use {crate::db::user, leptos::server_fn::error::NoCustomError, sqlx::SqlitePool};
use {crate::db::user, leptos::server_fn::error::NoCustomError};
use super::BeaconResources;

View File

@@ -4,7 +4,7 @@ use serde::{Deserialize, Serialize};
use {
crate::db::user,
leptos::server_fn::error::NoCustomError,
sqlx::{sqlite::SqliteRow, FromRow, Row, SqlitePool},
sqlx::{sqlite::SqliteRow, FromRow, Row},
std::str::FromStr,
};

View File

@@ -7,9 +7,7 @@ use serde::{Deserialize, Serialize};
use {
crate::db::user,
leptos::server_fn::error::NoCustomError,
rcgen::{generate_simple_self_signed, CertifiedKey},
sparse_handler::BeaconListenerMap,
sqlx::SqlitePool,
sparse_handler::BeaconListenerMap
};
use super::BeaconResources;
@@ -185,7 +183,12 @@ pub async fn start_listener(listener_id: i64) -> Result<(), ServerFnError> {
));
}
sparse_handler::start_listener(expect_context(), listener_id, expect_context()).await?;
sparse_handler::start_listener(
expect_context(),
listener_id,
expect_context(),
expect_context()
).await?;
Ok(())
}

View File

@@ -0,0 +1,311 @@
use std::sync::Arc;
use leptos::prelude::*;
#[cfg(feature = "hydrate")]
use leptos_use::{use_websocket, UseWebSocketReturn};
use serde::{Deserialize, Serialize};
use crate::beacons::BeaconResources;
#[cfg(feature = "hydrate")]
use super::templates::BeaconTemplate;
#[derive(Clone)]
enum SortMethod {
Listener,
Config,
Category,
Template,
}
impl std::str::FromStr for SortMethod {
type Err = ();
fn from_str(s: &str) -> Result<Self, Self::Err> {
match s {
"Listener" => Ok(Self::Listener),
"Config" => Ok(Self::Config),
"Category" => Ok(Self::Category),
"Template" => Ok(Self::Template),
&_ => Err(()),
}
}
}
impl std::string::ToString for SortMethod {
fn to_string(&self) -> String {
use SortMethod as SM;
match self {
SM::Listener => "Listener",
SM::Config => "Config",
SM::Category => "Category",
SM::Template => "Template",
}
.to_string()
}
}
#[derive(Clone, Debug, Serialize, Deserialize)]
pub struct CurrentBeaconInstance {
pub beacon_id: String,
pub ip: String,
pub nickname: String,
pub cwd: String,
pub operating_system: String,
pub userent: String,
pub hostname: String,
pub last_checkin: chrono::DateTime<chrono::Utc>,
pub config_id: Option<i64>,
pub template_id: i64,
pub category_ids: Vec<i64>,
}
// Safety: the only time this comes up is on the client, which is
// not a multi-threaded environment
unsafe impl Send for CurrentBeaconInstance {}
unsafe impl Send for SidebarEvents {}
#[derive(Clone, Serialize, Deserialize)]
pub enum SidebarEvents {
BeaconList(Vec<CurrentBeaconInstance>),
NewBeacon(CurrentBeaconInstance),
Checkin(String),
}
#[component]
pub fn BeaconSidebar() -> impl IntoView {
let BeaconResources {
listeners,
templates,
configs,
categories,
..
} = expect_context::<BeaconResources>();
let current_beacons = RwSignal::new(None::<Vec<CurrentBeaconInstance>>);
#[cfg(feature = "hydrate")]
let (web_socket, rebuild_websocket) = signal(use_websocket::<
(),
SidebarEvents,
codee::string::JsonSerdeCodec,
>("/api/subscribe/listener"));
#[cfg(feature = "hydrate")]
Effect::new(move |_| {
web_socket.with(move |uwsr| {
uwsr.message.with(move |message| {
let Some(m) = message else {
return;
};
match m {
SidebarEvents::BeaconList(bs) => {
let mut bs = bs.to_vec();
bs.sort_by_key(|b| b.last_checkin);
current_beacons.set(Some(bs));
}
SidebarEvents::NewBeacon(b) => {
current_beacons.update(|bso| {
if let Some(ref mut bs) = bso {
bs.push(b.clone())
}
});
}
SidebarEvents::Checkin(bid) => current_beacons.update(|bs| {
let Some(ref mut bs) = bs else {
return;
};
if let Some(ref mut b) = bs.iter_mut().find(|b| b.beacon_id == *bid) {
b.last_checkin = chrono::Utc::now();
}
}),
}
});
});
});
#[cfg(feature = "hydrate")]
Effect::new(move |_| {
let user = expect_context::<ReadSignal<Option<crate::users::User>>>();
user.with(move |_| {
rebuild_websocket(use_websocket::<
(),
SidebarEvents,
codee::string::JsonSerdeCodec,
>("/api/subscribe/listener"));
});
});
let (sort_method, set_sort_method) = signal(None::<SortMethod>);
let search_input = RwSignal::new("".to_string());
struct BeaconPartition {
title: Option<String>,
beacons: Arc<dyn Fn() -> Vec<CurrentBeaconInstance> + Send + Sync>,
}
// Safety: BeaconPartition is only ever constructed on the client side,
// where there is only one thread
unsafe impl Send for BeaconPartition {}
#[cfg(not(feature = "ssr"))]
let partitions = move || -> Vec<BeaconPartition> {
leptos::logging::log!(
"There are {:?} beacons",
current_beacons.read().as_ref().map(Vec::len)
);
let sm = sort_method.read();
//let Some(Ok(ref listeners)) = *listeners.read() else {
// return vec![];
//};
//let Some(Ok(ref templates)) = *templates.read() else {
// return vec![];
//};
//let Some(Ok(ref categories)) = *categories.read() else {
// return vec![];
//};
match *sm {
Some(SortMethod::Config) => {
let Some(Ok(ref configs)) = *configs.read() else {
return vec![];
};
configs
.iter()
.map(|config| {
let config = config.clone();
BeaconPartition {
title: Some(config.config_name.clone()),
beacons: Arc::new(move || {
let Some(Ok(ref templates)) = *templates.read() else {
return vec![];
};
current_beacons
.get()
.unwrap_or(vec![])
.iter()
.filter(|b| {
b.config_id.or(templates
.iter()
.find(|t| t.template_id == b.template_id)
.map(|t| t.config_id))
== Some(config.config_id)
})
.map(Clone::clone)
.collect()
}),
}
})
.collect::<Vec<_>>()
}
Some(SortMethod::Listener) => {
let Some(Ok(ref listeners)) = *listeners.read() else {
return vec![];
};
listeners
.iter()
.map(|listener| {
let listener = listener.clone();
BeaconPartition {
title: Some(listener.domain_name.clone()),
beacons: Arc::new(move || {
let Some(Ok(ref templates)) = *templates.read() else {
return vec![];
};
current_beacons
.get()
.unwrap_or(vec![])
.iter()
.filter(|b| {
templates
.iter()
.find(|t| t.template_id == b.template_id)
.map(|t| t.listener_id == listener.listener_id)
.unwrap_or_default()
})
.map(Clone::clone)
.collect()
}),
}
})
.collect()
}
_ => vec![BeaconPartition {
title: None,
beacons: Arc::new(move || current_beacons.get().unwrap_or(vec![]).clone()),
}],
}
};
// Safety: because this constructs nothing, it maintains the Safety
// invariants above
#[cfg(feature = "ssr")]
let partitions = || {
vec![BeaconPartition {
title: None,
beacons: Arc::new(move || vec![]),
}]
};
view! {
<aside class="beacons">
<div class="sort-method">
<p>"Sort beacons by:"</p>
<select
name="beacon-sort"
on:change:target=move |ev| {
set_sort_method(ev.target().value().parse().ok());
}
prop:value=move || sort_method
.get()
.as_ref()
.map(SortMethod::to_string)
.unwrap_or("".to_string())
>
<option value="">"---"</option>
<option value="Listener">"Listener"</option>
<option value="Config">"Config"</option>
<option value="Category">"Category"</option>
<option value="Template">"Template"</option>
</select>
</div>
<div class="search">
<input bind:value=search_input name="beacon-search" placeholder="Search..." />
</div>
<Suspense fallback=|| view! { "Loading..." }>
<div class="beacon-list">
{move || partitions()
.iter()
.map(|partition| view! {
<div class="beacon-partition">
{partition.title.as_ref().map(|title| view! {
<div class="partition-title">
{title.clone()}
</div>
})}
<For
each={
let beacons = Arc::clone(&partition.beacons);
move || (beacons)()
}
key=|b| b.beacon_id.clone()
let:beacon
>
<div>{beacon.beacon_id.clone()}</div>
</For>
</div>
})
.collect_view()}
</div>
</Suspense>
</aside>
}
}

View File

@@ -4,7 +4,7 @@ use serde::{Deserialize, Serialize};
use {
crate::db::user,
leptos::server_fn::error::NoCustomError,
sqlx::{sqlite::SqliteRow, FromRow, Row, SqlitePool},
sqlx::{sqlite::SqliteRow, FromRow, Row},
std::net::Ipv4Addr,
};
@@ -35,18 +35,18 @@ impl FromRow<'_, SqliteRow> for BeaconSourceMode {
#[cfg_attr(feature = "ssr", derive(FromRow))]
#[derive(Clone, Serialize, Deserialize)]
pub struct BeaconTemplate {
template_id: i64,
template_name: String,
operating_system: String,
pub template_id: i64,
pub template_name: String,
pub operating_system: String,
source_ip: String,
source_mac: Option<String>,
pub source_ip: String,
pub source_mac: Option<String>,
#[cfg_attr(feature = "ssr", sqlx(flatten))]
source_mode: BeaconSourceMode,
pub source_mode: BeaconSourceMode,
config_id: i64,
listener_id: i64,
default_category: Option<i64>,
pub config_id: i64,
pub listener_id: i64,
pub default_category: Option<i64>,
}
cfg_if::cfg_if! {
@@ -113,12 +113,12 @@ pub async fn add_template(
.fetch_one(&db)
.await?;
use rcgen::{Certificate, CertificateParams, KeyPair};
use rcgen::{CertificateParams, KeyPair};
let keypair = KeyPair::from_der_and_sign_algo(
match &rustls_pki_types::PrivateKeyDer::try_from(&*listener.privkey) {
Ok(pk) => pk,
Err(e) => {
Err(_) => {
srverr!("Could not parse private key: {e}");
}
},

View File

@@ -1,3 +1,4 @@
use axum_extra::extract::cookie::CookieJar;
use leptos::{prelude::*, server_fn::error::NoCustomError};
use leptos_axum::{extract, ResponseOptions};
use pbkdf2::{
@@ -196,15 +197,10 @@ pub async fn destroy_auth_session() -> Result<(), ServerFnError> {
Ok(())
}
pub async fn get_auth_session() -> Result<Option<User>, ServerFnError> {
use axum_extra::extract::cookie::CookieJar;
println!("In get auth session");
let owner = leptos::prelude::Owner::current().unwrap();
let db = crate::db::get_db()?;
let jar = extract::<CookieJar>().await?;
pub async fn get_auth_session_inner(
db: SqlitePool,
jar: CookieJar
) -> Result<Option<User>, crate::error::Error> {
let Some(cookie) = jar.get(SESSION_ID_KEY) else {
return Ok(None);
};
@@ -252,3 +248,12 @@ pub async fn get_auth_session() -> Result<Option<User>, ServerFnError> {
Ok(user)
}
pub async fn get_auth_session() -> Result<Option<User>, ServerFnError> {
let db = crate::db::get_db()?;
let jar = extract::<CookieJar>().await?;
get_auth_session_inner(db, jar)
.await
.map_err(Into::into)
}

View File

@@ -10,7 +10,10 @@ pub enum Error {
Pbkdf2(pbkdf2::password_hash::errors::Error),
#[cfg(feature = "ssr")]
Io(std::io::Error),
#[cfg(feature = "ssr")]
Axum(axum::Error),
AddrParse(std::net::AddrParseError),
Json(serde_json::Error),
}
impl std::fmt::Display for Error {
@@ -41,6 +44,13 @@ impl std::fmt::Display for Error {
Error::AddrParse(err) => {
write!(f, "ip address parse error: {err:?}")
}
Error::Json(err) => {
write!(f, "json encode/decode error: {err:?}")
}
#[cfg(feature = "ssr")]
Error::Axum(err) => {
write!(f, "axum error: {err:?}")
}
}
}
}
@@ -55,6 +65,9 @@ impl std::error::Error for Error {
#[cfg(feature = "ssr")]
Error::Io(err) => Some(err),
Error::AddrParse(err) => Some(err),
Error::Json(err) => Some(err),
#[cfg(feature = "ssr")]
Error::Axum(err) => Some(err),
_ => None,
}
}
@@ -112,3 +125,16 @@ impl From<std::net::AddrParseError> for Error {
Self::AddrParse(err)
}
}
impl From<serde_json::Error> for Error {
fn from(err: serde_json::Error) -> Self {
Self::Json(err)
}
}
#[cfg(feature = "ssr")]
impl From<axum::Error> for Error {
fn from(err: axum::Error) -> Self {
Self::Axum(err)
}
}

View File

@@ -21,7 +21,7 @@ async fn main() -> anyhow::Result<std::process::ExitCode> {
tracing_subscriber::registry()
.with(
tracing_subscriber::EnvFilter::try_from_default_env().unwrap_or_else(|_| {
format!("{}=debug,sparse_handler=debug,tower_http=trace", env!("CARGO_CRATE_NAME")).into()
format!("{}=debug,sparse_handler=debug", env!("CARGO_CRATE_NAME")).into()
}),
)
.with(tracing_subscriber::fmt::layer())

View File

@@ -0,0 +1 @@

View File

@@ -2,7 +2,7 @@ use chrono::{offset::Utc, DateTime};
use leptos::prelude::*;
use serde::{Deserialize, Serialize};
#[cfg(feature = "ssr")]
use {crate::db::user, leptos::server_fn::error::NoCustomError, sqlx::SqlitePool};
use {crate::db::user, leptos::server_fn::error::NoCustomError};
pub fn format_delta(time: chrono::TimeDelta) -> String {
let seconds = time.num_seconds();
@@ -98,9 +98,9 @@ async fn reset_password(user_id: i64, password: String) -> Result<(), ServerFnEr
pub fn RenderUser(refresh_user_list: Action<(), ()>, user: PubUser) -> impl IntoView {
use leptos_use::{use_interval, UseIntervalReturn};
#[cfg_attr(feature = "ssr", allow(unused_variables))]
#[cfg(feature = "hydrate")]
let UseIntervalReturn { counter, .. } = use_interval(1000);
#[cfg_attr(feature = "ssr", allow(unused_variables))]
#[cfg_attr(not(feature = "hydrate"), allow(unused_variables))]
let (time_ago, set_time_ago) = signal(
user.last_active
.map(|active| format_delta(Utc::now() - active)),

View File

@@ -1,11 +1,12 @@
use std::{net::SocketAddrV4, process::ExitCode};
use axum::{
extract::{FromRef, Path, Query, State},
extract::{ws, FromRef, Path, Query, State},
response::IntoResponse,
routing::{get, post},
Router,
};
use axum_extra::extract::cookie::CookieJar;
use leptos::prelude::*;
use leptos_axum::{generate_route_list, LeptosRoutes};
use serde::Deserialize;
@@ -94,10 +95,12 @@ pub async fn get_beacon(btype: &str) -> Result<Vec<u8>, crate::error::Error> {
}
}
#[derive(FromRef, Clone, Debug)]
#[derive(FromRef, Clone)]
pub struct AppState {
db: SqlitePool,
leptos_options: leptos::config::LeptosOptions,
beacon_listeners: sparse_handler::BeaconListenerMap,
beacon_event_broadcast: tokio::sync::broadcast::Sender<sparse_handler::BeaconEvent>,
}
async fn get_parameters_bytes(
@@ -237,7 +240,10 @@ pub async fn download_beacon(
State(db): State<AppState>,
Query(beacon_params): Query<BeaconDownloadParams>,
) -> Result<impl IntoResponse, crate::error::Error> {
let (parameters_bytes, operating_system) = get_parameters_bytes(template_id, db.db).await?;
use rand::{rngs::OsRng, TryRngCore};
use sparse_actions::payload_types::{Parameters_t, XOR_KEY};
let (mut parameters_bytes, operating_system) = get_parameters_bytes(template_id, db.db).await?;
let binary = if beacon_params.use_svc.unwrap_or_default() {
tracing::debug!("Downloading windows service");
@@ -252,6 +258,22 @@ pub async fn download_beacon(
let installer_bytes = get_beacon(&binary).await?;
let parameters: &mut Parameters_t =
unsafe { std::mem::transmute(parameters_bytes.as_mut_ptr()) };
let mut identifier = [0u8; 32];
OsRng
.try_fill_bytes(&mut identifier)
.expect("Could not generate beacon identifier");
let hex_ident = hex::encode(&identifier)
.as_bytes()
.iter()
.map(|b| b ^ (XOR_KEY as u8))
.collect::<Vec<_>>();
parameters
.beacon_identifier
.copy_from_slice(&hex_ident);
use axum::http::header;
Ok((
@@ -301,6 +323,118 @@ pub async fn download_beacon_installer(
))
}
pub async fn subscribe_to_listener_events(
State(state): State<AppState>,
cookie_jar: CookieJar,
ws: ws::WebSocketUpgrade
) -> axum::response::Response {
let user = match crate::db::user::get_auth_session_inner(
state.db.clone(),
cookie_jar
).await {
Ok(u) => u,
Err(e) => {
tracing::warn!("Could not load user session: {e:?}");
return axum::http::StatusCode::INTERNAL_SERVER_ERROR.into_response();
}
};
if user.is_none() {
return axum::http::StatusCode::UNAUTHORIZED.into_response();
}
ws
.on_upgrade(move |socket: ws::WebSocket| async move {
if let Err(e) = handle_listener_events(socket, state).await {
tracing::warn!("Encountered error when handling event subscriber: {e}");
};
})
.into_response()
}
async fn handle_listener_events(
mut socket: ws::WebSocket,
state: AppState,
) -> Result<(), crate::error::Error> {
use sqlx::{sqlite::SqliteRow, Row};
use crate::beacons::sidebar::{CurrentBeaconInstance, SidebarEvents};
{
let beacons = sqlx::query!(
"SELECT beacon_id, template_id, peer_ip, nickname, cwd, operating_system, beacon_userent, hostname, config_id FROM beacon_instance"
)
.fetch_all(&state.db)
.await?;
struct CheckinResult {
beacon_id: String,
checkin_date: chrono::DateTime<chrono::Utc>
}
impl sqlx::FromRow<'_, SqliteRow> for CheckinResult {
fn from_row(row: &SqliteRow) -> sqlx::Result<Self> {
Ok(CheckinResult {
beacon_id: row.get("beacon_id"),
checkin_date: row.get("checkin_date")
})
}
}
let last_checkin: Vec<CheckinResult> = sqlx::query_as(
"SELECT beacon_id, MAX(checkin_date) as checkin_date FROM beacon_checkin
GROUP BY beacon_id"
)
.fetch_all(&state.db)
.await?;
let category_ids = sqlx::query!(
"SELECT beacon_id, category_id FROM beacon_category_assignment"
)
.fetch_all(&state.db)
.await?;
let beacons = SidebarEvents::BeaconList(
beacons
.into_iter()
.map(|b| CurrentBeaconInstance {
beacon_id: b.beacon_id.clone(),
template_id: b.template_id,
ip: b.peer_ip,
nickname: b.nickname,
cwd: b.cwd,
operating_system: b.operating_system,
userent: b.beacon_userent,
hostname: b.hostname,
config_id: b.config_id,
last_checkin: last_checkin
.iter()
.find(|ch| ch.beacon_id == b.beacon_id)
.clone()
.map(|ch| ch.checkin_date)
.unwrap_or_else(|| chrono::Utc::now()),
category_ids: category_ids
.iter()
.filter(|cat| cat.beacon_id == b.beacon_id)
.map(|cat| cat.category_id)
.collect()
})
.collect::<Vec<_>>()
);
let json = serde_json::to_string(&beacons)?;
socket.send(ws::Message::Text(json.into())).await?;
}
let mut event_receiver = state.beacon_event_broadcast.subscribe();
loop {
let event = event_receiver.recv().await;
}
}
pub async fn serve_web(
management_address: SocketAddrV4,
db: SqlitePool,
@@ -308,6 +442,7 @@ pub async fn serve_web(
let conf = get_configuration(None).unwrap();
let leptos_options = conf.leptos_options;
let routes = generate_route_list(App);
let beacon_event_broadcast = tokio::sync::broadcast::Sender::<sparse_handler::BeaconEvent>::new(128);
let beacon_listeners = sparse_handler::BeaconListenerMap::default();
let compression_layer = tower_http::compression::CompressionLayer::new()
@@ -316,11 +451,17 @@ pub async fn serve_web(
.br(true)
.zstd(true);
sparse_handler::start_all_listeners(beacon_listeners.clone(), db.clone()).await?;
sparse_handler::start_all_listeners(
beacon_listeners.clone(),
db.clone(),
beacon_event_broadcast.clone()
).await?;
let state = AppState {
leptos_options: leptos_options.clone(),
db: db.clone(),
beacon_listeners: beacon_listeners.clone(),
beacon_event_broadcast: beacon_event_broadcast.clone()
};
let app = Router::new()
@@ -329,6 +470,10 @@ pub async fn serve_web(
get(download_beacon_installer),
)
.route("/binaries/beacon/:template_id", get(download_beacon))
.route(
"/api/subscribe/listener",
axum::routing::any(subscribe_to_listener_events)
)
.route("/api/*fn_name", post(leptos_axum::handle_server_fns))
.leptos_routes_with_context(
&state,
@@ -336,6 +481,7 @@ pub async fn serve_web(
move || {
provide_context(beacon_listeners.clone());
provide_context(db.clone());
provide_context(beacon_event_broadcast.clone());
},
{
let leptos_options = leptos_options.clone();