feat: event management and websocket for updates
This commit is contained in:
50
sparse-server/.sqlx/query-04472775affd2b1694c05f5ab8125528bb5448e0378a7b9cf3ed58eaa2101d1e.json
generated
Normal file
50
sparse-server/.sqlx/query-04472775affd2b1694c05f5ab8125528bb5448e0378a7b9cf3ed58eaa2101d1e.json
generated
Normal file
@@ -0,0 +1,50 @@
|
||||
{
|
||||
"db_name": "SQLite",
|
||||
"query": "SELECT c.mode, c.regular_interval, c.random_min_time, c.random_max_time, c.cron_schedule, c.cron_mode\n FROM beacon_template t\n INNER JOIN beacon_config c ON c.config_id = t.config_id\n WHERE t.template_id = ?",
|
||||
"describe": {
|
||||
"columns": [
|
||||
{
|
||||
"name": "mode",
|
||||
"ordinal": 0,
|
||||
"type_info": "Text"
|
||||
},
|
||||
{
|
||||
"name": "regular_interval",
|
||||
"ordinal": 1,
|
||||
"type_info": "Integer"
|
||||
},
|
||||
{
|
||||
"name": "random_min_time",
|
||||
"ordinal": 2,
|
||||
"type_info": "Integer"
|
||||
},
|
||||
{
|
||||
"name": "random_max_time",
|
||||
"ordinal": 3,
|
||||
"type_info": "Integer"
|
||||
},
|
||||
{
|
||||
"name": "cron_schedule",
|
||||
"ordinal": 4,
|
||||
"type_info": "Text"
|
||||
},
|
||||
{
|
||||
"name": "cron_mode",
|
||||
"ordinal": 5,
|
||||
"type_info": "Text"
|
||||
}
|
||||
],
|
||||
"parameters": {
|
||||
"Right": 1
|
||||
},
|
||||
"nullable": [
|
||||
true,
|
||||
true,
|
||||
true,
|
||||
true,
|
||||
true,
|
||||
true
|
||||
]
|
||||
},
|
||||
"hash": "04472775affd2b1694c05f5ab8125528bb5448e0378a7b9cf3ed58eaa2101d1e"
|
||||
}
|
||||
12
sparse-server/.sqlx/query-6e5e4ddd5c8b60c2180ca4cadf1449dc8b8b37a8887cd5b01f1ca9d0a411b897.json
generated
Normal file
12
sparse-server/.sqlx/query-6e5e4ddd5c8b60c2180ca4cadf1449dc8b8b37a8887cd5b01f1ca9d0a411b897.json
generated
Normal file
@@ -0,0 +1,12 @@
|
||||
{
|
||||
"db_name": "SQLite",
|
||||
"query": "INSERT INTO beacon_checkin (beacon_id, checkin_date) VALUES (?, ?)",
|
||||
"describe": {
|
||||
"columns": [],
|
||||
"parameters": {
|
||||
"Right": 2
|
||||
},
|
||||
"nullable": []
|
||||
},
|
||||
"hash": "6e5e4ddd5c8b60c2180ca4cadf1449dc8b8b37a8887cd5b01f1ca9d0a411b897"
|
||||
}
|
||||
68
sparse-server/.sqlx/query-75edc2bc9adda52aa7e9cd68f980db95744690cb3fc1b9cccfb3ab6f63d0ab25.json
generated
Normal file
68
sparse-server/.sqlx/query-75edc2bc9adda52aa7e9cd68f980db95744690cb3fc1b9cccfb3ab6f63d0ab25.json
generated
Normal file
@@ -0,0 +1,68 @@
|
||||
{
|
||||
"db_name": "SQLite",
|
||||
"query": "SELECT beacon_id, template_id, peer_ip, nickname, cwd, operating_system, beacon_userent, hostname, config_id FROM beacon_instance",
|
||||
"describe": {
|
||||
"columns": [
|
||||
{
|
||||
"name": "beacon_id",
|
||||
"ordinal": 0,
|
||||
"type_info": "Text"
|
||||
},
|
||||
{
|
||||
"name": "template_id",
|
||||
"ordinal": 1,
|
||||
"type_info": "Integer"
|
||||
},
|
||||
{
|
||||
"name": "peer_ip",
|
||||
"ordinal": 2,
|
||||
"type_info": "Text"
|
||||
},
|
||||
{
|
||||
"name": "nickname",
|
||||
"ordinal": 3,
|
||||
"type_info": "Text"
|
||||
},
|
||||
{
|
||||
"name": "cwd",
|
||||
"ordinal": 4,
|
||||
"type_info": "Text"
|
||||
},
|
||||
{
|
||||
"name": "operating_system",
|
||||
"ordinal": 5,
|
||||
"type_info": "Text"
|
||||
},
|
||||
{
|
||||
"name": "beacon_userent",
|
||||
"ordinal": 6,
|
||||
"type_info": "Text"
|
||||
},
|
||||
{
|
||||
"name": "hostname",
|
||||
"ordinal": 7,
|
||||
"type_info": "Text"
|
||||
},
|
||||
{
|
||||
"name": "config_id",
|
||||
"ordinal": 8,
|
||||
"type_info": "Integer"
|
||||
}
|
||||
],
|
||||
"parameters": {
|
||||
"Right": 0
|
||||
},
|
||||
"nullable": [
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
true
|
||||
]
|
||||
},
|
||||
"hash": "75edc2bc9adda52aa7e9cd68f980db95744690cb3fc1b9cccfb3ab6f63d0ab25"
|
||||
}
|
||||
50
sparse-server/.sqlx/query-bb07fb691f373dea848c0368d4c36e4c2b079d2d1efc0006d43d6833023101a3.json
generated
Normal file
50
sparse-server/.sqlx/query-bb07fb691f373dea848c0368d4c36e4c2b079d2d1efc0006d43d6833023101a3.json
generated
Normal file
@@ -0,0 +1,50 @@
|
||||
{
|
||||
"db_name": "SQLite",
|
||||
"query": "SELECT c.mode as mode, c.regular_interval as regular_interval, c.random_min_time as random_min_time,\n c.random_max_time as random_max_time, c.cron_schedule as cron_schedule, c.cron_mode as cron_mode\n FROM beacon_instance i\n INNER JOIN beacon_config c ON c.config_id = i.config_id\n WHERE i.beacon_id = ?\n UNION\n SELECT c.mode as mode, c.regular_interval as regular_interval, c.random_min_time as random_min_time,\n c.random_max_time as random_max_time, c.cron_schedule as cron_schedule, c.cron_mode as cron_mode\n FROM beacon_instance i\n INNER JOIN beacon_template t ON i.template_id = t.template_id\n INNER JOIN beacon_config c ON t.config_id = c.config_id\n WHERE i.beacon_id = ?",
|
||||
"describe": {
|
||||
"columns": [
|
||||
{
|
||||
"name": "mode",
|
||||
"ordinal": 0,
|
||||
"type_info": "Text"
|
||||
},
|
||||
{
|
||||
"name": "regular_interval",
|
||||
"ordinal": 1,
|
||||
"type_info": "Integer"
|
||||
},
|
||||
{
|
||||
"name": "random_min_time",
|
||||
"ordinal": 2,
|
||||
"type_info": "Integer"
|
||||
},
|
||||
{
|
||||
"name": "random_max_time",
|
||||
"ordinal": 3,
|
||||
"type_info": "Integer"
|
||||
},
|
||||
{
|
||||
"name": "cron_schedule",
|
||||
"ordinal": 4,
|
||||
"type_info": "Text"
|
||||
},
|
||||
{
|
||||
"name": "cron_mode",
|
||||
"ordinal": 5,
|
||||
"type_info": "Text"
|
||||
}
|
||||
],
|
||||
"parameters": {
|
||||
"Right": 2
|
||||
},
|
||||
"nullable": [
|
||||
true,
|
||||
true,
|
||||
true,
|
||||
true,
|
||||
true,
|
||||
true
|
||||
]
|
||||
},
|
||||
"hash": "bb07fb691f373dea848c0368d4c36e4c2b079d2d1efc0006d43d6833023101a3"
|
||||
}
|
||||
26
sparse-server/.sqlx/query-e7dddc194dcb297f672a9270f801ea23192e9fa53559b9fcfcffddeb3f571d15.json
generated
Normal file
26
sparse-server/.sqlx/query-e7dddc194dcb297f672a9270f801ea23192e9fa53559b9fcfcffddeb3f571d15.json
generated
Normal file
@@ -0,0 +1,26 @@
|
||||
{
|
||||
"db_name": "SQLite",
|
||||
"query": "SELECT beacon_id, category_id FROM beacon_category_assignment",
|
||||
"describe": {
|
||||
"columns": [
|
||||
{
|
||||
"name": "beacon_id",
|
||||
"ordinal": 0,
|
||||
"type_info": "Text"
|
||||
},
|
||||
{
|
||||
"name": "category_id",
|
||||
"ordinal": 1,
|
||||
"type_info": "Integer"
|
||||
}
|
||||
],
|
||||
"parameters": {
|
||||
"Right": 0
|
||||
},
|
||||
"nullable": [
|
||||
false,
|
||||
false
|
||||
]
|
||||
},
|
||||
"hash": "e7dddc194dcb297f672a9270f801ea23192e9fa53559b9fcfcffddeb3f571d15"
|
||||
}
|
||||
@@ -1,36 +1,26 @@
|
||||
{
|
||||
"db_name": "SQLite",
|
||||
"query": "SELECT * FROM beacon_listener WHERE listener_id = ?",
|
||||
"query": "SELECT port, domain_name, certificate, privkey FROM beacon_listener WHERE listener_id = ?",
|
||||
"describe": {
|
||||
"columns": [
|
||||
{
|
||||
"name": "listener_id",
|
||||
"name": "port",
|
||||
"ordinal": 0,
|
||||
"type_info": "Integer"
|
||||
},
|
||||
{
|
||||
"name": "port",
|
||||
"ordinal": 1,
|
||||
"type_info": "Integer"
|
||||
},
|
||||
{
|
||||
"name": "public_ip",
|
||||
"ordinal": 2,
|
||||
"type_info": "Text"
|
||||
},
|
||||
{
|
||||
"name": "domain_name",
|
||||
"ordinal": 3,
|
||||
"ordinal": 1,
|
||||
"type_info": "Text"
|
||||
},
|
||||
{
|
||||
"name": "certificate",
|
||||
"ordinal": 4,
|
||||
"ordinal": 2,
|
||||
"type_info": "Blob"
|
||||
},
|
||||
{
|
||||
"name": "privkey",
|
||||
"ordinal": 5,
|
||||
"ordinal": 3,
|
||||
"type_info": "Blob"
|
||||
}
|
||||
],
|
||||
@@ -38,13 +28,11 @@
|
||||
"Right": 1
|
||||
},
|
||||
"nullable": [
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
false
|
||||
]
|
||||
},
|
||||
"hash": "e7dc753795b8976b14b5c4baec20d16eff715a4d2ffe93c6723bad368483fb69"
|
||||
"hash": "f130d1b3b891a4f5d57b69bcf111230b85d7fb636517e825a42c2a59fc8b8311"
|
||||
}
|
||||
12
sparse-server/.sqlx/query-fa45ebf8fb26791336d5ab3701a3fc6fdf17d3eaddb0e5cff099e0396fe4dddd.json
generated
Normal file
12
sparse-server/.sqlx/query-fa45ebf8fb26791336d5ab3701a3fc6fdf17d3eaddb0e5cff099e0396fe4dddd.json
generated
Normal file
@@ -0,0 +1,12 @@
|
||||
{
|
||||
"db_name": "SQLite",
|
||||
"query": "INSERT INTO beacon_instance\n (beacon_id, template_id, peer_ip, nickname, cwd, operating_system, beacon_userent, hostname)\n VALUES\n (?, ?, ?, \"\", ?, ?, ?, ?)",
|
||||
"describe": {
|
||||
"columns": [],
|
||||
"parameters": {
|
||||
"Right": 7
|
||||
},
|
||||
"nullable": []
|
||||
},
|
||||
"hash": "fa45ebf8fb26791336d5ab3701a3fc6fdf17d3eaddb0e5cff099e0396fe4dddd"
|
||||
}
|
||||
@@ -30,7 +30,7 @@ futures-util = { version = "0.3", optional = true }
|
||||
tracing = { version = "0.1", optional = true }
|
||||
web-sys = { version = "0.3", features = ["WebSocket"] }
|
||||
leptos-use = { version = "0.15", default-features = false, features = ["use_websocket", "use_interval"] }
|
||||
codee = "0.2"
|
||||
codee = { version = "0.2", features = ["json_serde"] }
|
||||
sqlx = { version = "0.8", default-features = false, features = ["chrono", "macros", "migrate", "runtime-tokio", "sqlite", "sqlx-sqlite"], optional = true }
|
||||
chrono = { version = "0.4", features = ["serde"] }
|
||||
rpassword = { version = "7.3", optional = true }
|
||||
@@ -46,6 +46,8 @@ rand = { version = "0.9", optional = true }
|
||||
|
||||
sparse-actions = { path = "../sparse-actions", optional = true }
|
||||
sparse-handler = { path = "../sparse-handler", optional = true }
|
||||
serde_json = "1.0.139"
|
||||
send_wrapper = "0.6.0"
|
||||
|
||||
[features]
|
||||
embed-beacons = []
|
||||
|
||||
@@ -0,0 +1,19 @@
|
||||
DROP TABLE beacon_config_assignment;
|
||||
DROP TABLE beacon_instance;
|
||||
|
||||
CREATE TABLE beacon_instance (
|
||||
beacon_id varchar PRIMARY KEY NOT NULL,
|
||||
template_id int NOT NULL,
|
||||
peer_ip varchar NOT NULL,
|
||||
nickname varchar NOT NULL,
|
||||
|
||||
cwd varchar NOT NULL,
|
||||
operating_system varchar NOT NULL,
|
||||
beacon_userent varchar NOT NULL,
|
||||
hostname varchar NOT NULL,
|
||||
|
||||
config_id int,
|
||||
|
||||
FOREIGN KEY (template_id) REFERENCES beacon_template,
|
||||
FOREIGN KEY (config_id) REFERENCES beacon_config
|
||||
);
|
||||
11
sparse-server/migrations/20250222071958_new_assignments.sql
Normal file
11
sparse-server/migrations/20250222071958_new_assignments.sql
Normal file
@@ -0,0 +1,11 @@
|
||||
DROP TABLE beacon_category_assignment;
|
||||
|
||||
CREATE TABLE beacon_category_assignment (
|
||||
category_id int NOT NULL,
|
||||
beacon_id varchar NOT NULL,
|
||||
|
||||
PRIMARY KEY (category_id, beacon_id),
|
||||
|
||||
FOREIGN KEY (category_id) REFERENCES beacon_category,
|
||||
FOREIGN KEY (beacon_id) REFERENCES beacon_instance
|
||||
);
|
||||
@@ -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>
|
||||
}/>
|
||||
|
||||
@@ -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>
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
|
||||
@@ -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,
|
||||
};
|
||||
|
||||
|
||||
@@ -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(())
|
||||
}
|
||||
|
||||
311
sparse-server/src/beacons/sidebar.rs
Normal file
311
sparse-server/src/beacons/sidebar.rs
Normal 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>
|
||||
}
|
||||
}
|
||||
@@ -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}");
|
||||
}
|
||||
},
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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())
|
||||
|
||||
1
sparse-server/src/socket.rs
Normal file
1
sparse-server/src/socket.rs
Normal file
@@ -0,0 +1 @@
|
||||
|
||||
@@ -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)),
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -4,6 +4,7 @@
|
||||
@use 'beacons/templates';
|
||||
@use 'beacons/instances';
|
||||
@use 'beacons/commands';
|
||||
@use 'beacons/sidebar';
|
||||
|
||||
main.beacons {
|
||||
display: grid;
|
||||
|
||||
6
sparse-server/style/beacons/_sidebar.scss
Normal file
6
sparse-server/style/beacons/_sidebar.scss
Normal file
@@ -0,0 +1,6 @@
|
||||
aside.beacons {
|
||||
grid-area: beacons;
|
||||
background-color: #11111c;
|
||||
overflow-y: auto;
|
||||
overflow-x: hidden;
|
||||
}
|
||||
Reference in New Issue
Block a user