feat: added basic beacon instance management UI
This commit is contained in:
parent
7778e9b454
commit
a57a95a98a
@ -11,11 +11,12 @@ pub mod error;
|
|||||||
|
|
||||||
mod router;
|
mod router;
|
||||||
|
|
||||||
#[derive(Clone)]
|
#[derive(Clone, Debug)]
|
||||||
#[non_exhaustive]
|
#[non_exhaustive]
|
||||||
pub enum BeaconEvent {
|
pub enum BeaconEvent {
|
||||||
NewBeacon(String),
|
NewBeacon(String),
|
||||||
Checkin(String),
|
Checkin(String),
|
||||||
|
BeaconUpdate(String),
|
||||||
BeaconCommandFinished(String, i64)
|
BeaconCommandFinished(String, i64)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -175,7 +175,10 @@ pub async fn issue_command(
|
|||||||
}
|
}
|
||||||
|
|
||||||
#[component]
|
#[component]
|
||||||
pub fn CommandForm(categories: Vec<Category>, beacon_id: Option<String>) -> impl IntoView {
|
pub fn CommandForm(
|
||||||
|
categories: Vec<Category>,
|
||||||
|
beacon_info: Option<(String, String, sparse_actions::version::Version)>
|
||||||
|
) -> impl IntoView {
|
||||||
let command_action = Action::new(move |data: &SendWrapper<FormData>| {
|
let command_action = Action::new(move |data: &SendWrapper<FormData>| {
|
||||||
let data = data.clone();
|
let data = data.clone();
|
||||||
async move {
|
async move {
|
||||||
@ -186,7 +189,7 @@ pub fn CommandForm(categories: Vec<Category>, beacon_id: Option<String>) -> impl
|
|||||||
let (current_cmd, set_current_cmd) = signal("Exec".to_owned());
|
let (current_cmd, set_current_cmd) = signal("Exec".to_owned());
|
||||||
|
|
||||||
view! {
|
view! {
|
||||||
{(categories.is_empty() && beacon_id.is_none())
|
{(categories.is_empty() && beacon_info.is_none())
|
||||||
.then(|| view! {
|
.then(|| view! {
|
||||||
<span class="error">"Missing categories! Cannot assign a command to a category if there are no categories"</span>
|
<span class="error">"Missing categories! Cannot assign a command to a category if there are no categories"</span>
|
||||||
})}
|
})}
|
||||||
@ -211,7 +214,7 @@ pub fn CommandForm(categories: Vec<Category>, beacon_id: Option<String>) -> impl
|
|||||||
}>
|
}>
|
||||||
<fieldset>
|
<fieldset>
|
||||||
<legend>"Issue new command"</legend>
|
<legend>"Issue new command"</legend>
|
||||||
{if let Some(bid) = beacon_id.clone() {
|
{if let Some((bid, _, _)) = beacon_info.clone() {
|
||||||
Either::Left(view! {
|
Either::Left(view! {
|
||||||
<input name="target_beacon_id" type="hidden" value=bid />
|
<input name="target_beacon_id" type="hidden" value=bid />
|
||||||
})
|
})
|
||||||
@ -242,6 +245,13 @@ pub fn CommandForm(categories: Vec<Category>, beacon_id: Option<String>) -> impl
|
|||||||
>
|
>
|
||||||
{sparse_actions::actions::ACTION_BUILDERS
|
{sparse_actions::actions::ACTION_BUILDERS
|
||||||
.iter()
|
.iter()
|
||||||
|
.filter(|b| beacon_info
|
||||||
|
.as_ref()
|
||||||
|
.map(|(_, os, vers)| {
|
||||||
|
*vers >= b.required_version() &&
|
||||||
|
b.required_os().map(|osv| osv == os).unwrap_or(true)
|
||||||
|
})
|
||||||
|
.unwrap_or(true))
|
||||||
.map(|b| view! {
|
.map(|b| view! {
|
||||||
<option value=b.name()>{b.name()}</option>
|
<option value=b.name()>{b.name()}</option>
|
||||||
})
|
})
|
||||||
@ -306,7 +316,7 @@ pub fn CommandsView() -> impl IntoView {
|
|||||||
};
|
};
|
||||||
|
|
||||||
Either::Right(view! {
|
Either::Right(view! {
|
||||||
<CommandForm categories beacon_id=None />
|
<CommandForm categories beacon_info=None />
|
||||||
})
|
})
|
||||||
})}
|
})}
|
||||||
</Suspense>
|
</Suspense>
|
||||||
|
|||||||
@ -1,10 +1,346 @@
|
|||||||
use leptos::prelude::*;
|
use leptos::{either::Either, prelude::*};
|
||||||
|
|
||||||
|
use leptos_router::params::Params;
|
||||||
|
use serde::{Serialize, Deserialize};
|
||||||
|
|
||||||
|
#[cfg(feature = "ssr")]
|
||||||
|
use {crate::db::user, leptos::server_fn::error::NoCustomError};
|
||||||
|
|
||||||
|
use sparse_actions::version::Version;
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||||
|
pub enum BeaconViewEvent {
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||||
|
pub enum BeaconClientMessage {
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||||
|
pub struct BeaconInstance {
|
||||||
|
beacon_id: String,
|
||||||
|
template_id: i64,
|
||||||
|
peer_ip: String,
|
||||||
|
nickname: String,
|
||||||
|
|
||||||
|
cwd: String,
|
||||||
|
operating_system: String,
|
||||||
|
beacon_userent: String,
|
||||||
|
hostname: String,
|
||||||
|
|
||||||
|
version: Version,
|
||||||
|
|
||||||
|
config_id: Option<i64>,
|
||||||
|
|
||||||
|
category_ids: Vec<i64>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[server(prefix = "/api/instance", endpoint = "get")]
|
||||||
|
pub async fn get_instance_data(beacon_id: String) -> Result<BeaconInstance, ServerFnError> {
|
||||||
|
let user = user::get_auth_session().await?;
|
||||||
|
|
||||||
|
if user.is_none() {
|
||||||
|
return Err(ServerFnError::<NoCustomError>::ServerError(
|
||||||
|
"You are not signed in!".to_owned(),
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
let db = crate::db::get_db()?;
|
||||||
|
|
||||||
|
let instance = sqlx::query!(
|
||||||
|
r#"SELECT beacon_id, template_id, peer_ip, nickname, cwd, operating_system,
|
||||||
|
beacon_userent, hostname, version as "version: Version", config_id FROM beacon_instance
|
||||||
|
WHERE beacon_id = ?"#,
|
||||||
|
beacon_id
|
||||||
|
)
|
||||||
|
.fetch_one(&db)
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
let category_ids = sqlx::query!(
|
||||||
|
"SELECT category_id FROM beacon_category_assignment WHERE beacon_id = ?",
|
||||||
|
beacon_id
|
||||||
|
)
|
||||||
|
.fetch_all(&db)
|
||||||
|
.await?
|
||||||
|
.iter()
|
||||||
|
.map(|r| r.category_id)
|
||||||
|
.collect();
|
||||||
|
|
||||||
|
Ok(BeaconInstance {
|
||||||
|
beacon_id: instance.beacon_id,
|
||||||
|
template_id: instance.template_id,
|
||||||
|
peer_ip: instance.peer_ip,
|
||||||
|
nickname: instance.nickname,
|
||||||
|
|
||||||
|
cwd: instance.cwd,
|
||||||
|
operating_system: instance.operating_system,
|
||||||
|
beacon_userent: instance.beacon_userent,
|
||||||
|
hostname: instance.hostname,
|
||||||
|
|
||||||
|
version: instance.version,
|
||||||
|
|
||||||
|
config_id: instance.config_id,
|
||||||
|
|
||||||
|
category_ids,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
#[server(prefix = "/api/instance", endpoint = "update")]
|
||||||
|
pub async fn update_instance_prefs(
|
||||||
|
beacon_id: String,
|
||||||
|
nickname: String,
|
||||||
|
config_id: i64,
|
||||||
|
new_category_id: i64,
|
||||||
|
) -> Result<(), ServerFnError> {
|
||||||
|
let config_id = (config_id > 0).then(|| config_id);
|
||||||
|
let new_category_id = (new_category_id > 0).then(|| new_category_id);
|
||||||
|
|
||||||
|
let user = user::get_auth_session().await?;
|
||||||
|
|
||||||
|
if user.is_none() {
|
||||||
|
return Err(ServerFnError::<NoCustomError>::ServerError(
|
||||||
|
"You are not signed in!".to_owned(),
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
let db = crate::db::get_db()?;
|
||||||
|
|
||||||
|
sqlx::query!(
|
||||||
|
"UPDATE beacon_instance SET nickname = ?, config_id = ?
|
||||||
|
WHERE beacon_id = ?",
|
||||||
|
nickname,
|
||||||
|
config_id,
|
||||||
|
beacon_id
|
||||||
|
)
|
||||||
|
.execute(&db)
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
if let Some(new_category_id) = new_category_id {
|
||||||
|
sqlx::query!(
|
||||||
|
"INSERT INTO beacon_category_assignment (beacon_id, category_id) VALUES (?, ?)",
|
||||||
|
beacon_id,
|
||||||
|
new_category_id
|
||||||
|
)
|
||||||
|
.execute(&db)
|
||||||
|
.await?;
|
||||||
|
}
|
||||||
|
|
||||||
|
let update_notifier = expect_context::<tokio::sync::broadcast::Sender::<sparse_handler::BeaconEvent>>();
|
||||||
|
update_notifier.send(sparse_handler::BeaconEvent::BeaconUpdate(beacon_id))?;
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
#[server(prefix = "/api/instance", endpoint = "remove_category")]
|
||||||
|
pub async fn remove_category(
|
||||||
|
beacon_id: String,
|
||||||
|
category_id: i64
|
||||||
|
) -> Result<(), ServerFnError> {
|
||||||
|
let user = user::get_auth_session().await?;
|
||||||
|
|
||||||
|
if user.is_none() {
|
||||||
|
return Err(ServerFnError::<NoCustomError>::ServerError(
|
||||||
|
"You are not signed in!".to_owned(),
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
let db = crate::db::get_db()?;
|
||||||
|
|
||||||
|
sqlx::query!(
|
||||||
|
"DELETE FROM beacon_category_assignment
|
||||||
|
WHERE beacon_id = ? AND category_id = ?",
|
||||||
|
beacon_id,
|
||||||
|
category_id
|
||||||
|
)
|
||||||
|
.execute(&db)
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
let update_notifier = expect_context::<tokio::sync::broadcast::Sender::<sparse_handler::BeaconEvent>>();
|
||||||
|
update_notifier.send(sparse_handler::BeaconEvent::BeaconUpdate(beacon_id))?;
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Params, PartialEq)]
|
||||||
|
struct InstanceParams {
|
||||||
|
id: String
|
||||||
|
}
|
||||||
|
|
||||||
#[component]
|
#[component]
|
||||||
pub fn InstancesView() -> impl IntoView {
|
pub fn InstancesView() -> impl IntoView {
|
||||||
view! {
|
let instance_id = leptos_router::hooks::use_params::<InstanceParams>();
|
||||||
<div>
|
|
||||||
|
|
||||||
|
let update_instance = ServerAction::<UpdateInstancePrefs>::new();
|
||||||
|
let remove_category = ServerAction::<RemoveCategory>::new();
|
||||||
|
|
||||||
|
let instance = Resource::new(
|
||||||
|
move || (
|
||||||
|
update_instance.version().get(),
|
||||||
|
remove_category.version().get(),
|
||||||
|
instance_id.get().expect("could not extract ID from URL").id
|
||||||
|
),
|
||||||
|
|(_, _, id)| get_instance_data(id)
|
||||||
|
);
|
||||||
|
|
||||||
|
let super::BeaconResources {
|
||||||
|
configs,
|
||||||
|
templates,
|
||||||
|
categories,
|
||||||
|
..
|
||||||
|
} = expect_context();
|
||||||
|
|
||||||
|
view! {
|
||||||
|
<div class="instance">
|
||||||
|
<Suspense fallback=|| view! {"Loading..."}>
|
||||||
|
{move || Suspend::new(async move {
|
||||||
|
let configs = match configs.await {
|
||||||
|
Ok(cs) => cs,
|
||||||
|
Err(e) => return Either::Left(view! {
|
||||||
|
<p>{"There was an error loading configs:".to_string()}</p>
|
||||||
|
<p>{format!("error: {}", e)}</p>
|
||||||
|
})
|
||||||
|
};
|
||||||
|
|
||||||
|
let templates = match templates.await {
|
||||||
|
Ok(cs) => cs,
|
||||||
|
Err(e) => return Either::Left(view! {
|
||||||
|
<p>{"There was an error loading configs:".to_string()}</p>
|
||||||
|
<p>{format!("error: {}", e)}</p>
|
||||||
|
})
|
||||||
|
};
|
||||||
|
|
||||||
|
let categories = match categories.await {
|
||||||
|
Ok(cs) => cs,
|
||||||
|
Err(e) => return Either::Left(view! {
|
||||||
|
<p>{"There was an error loading configs:".to_string()}</p>
|
||||||
|
<p>{format!("error: {}", e)}</p>
|
||||||
|
})
|
||||||
|
};
|
||||||
|
|
||||||
|
let instance = match instance.await {
|
||||||
|
Ok(inst) => inst,
|
||||||
|
Err(e) => return Either::Left(view! {
|
||||||
|
<p>{"There was an error loading configs:".to_string()}</p>
|
||||||
|
<p>{format!("error: {}", e)}</p>
|
||||||
|
})
|
||||||
|
};
|
||||||
|
|
||||||
|
let beacon_info = Some((
|
||||||
|
instance.beacon_id.clone(),
|
||||||
|
instance.operating_system.clone(),
|
||||||
|
instance.version
|
||||||
|
));
|
||||||
|
|
||||||
|
Either::Right(view! {
|
||||||
|
<ManageInstance
|
||||||
|
configs
|
||||||
|
instance
|
||||||
|
templates
|
||||||
|
categories=categories.clone()
|
||||||
|
update_instance
|
||||||
|
remove_category
|
||||||
|
/>
|
||||||
|
|
||||||
|
<super::commands::CommandForm
|
||||||
|
categories
|
||||||
|
beacon_info
|
||||||
|
/>
|
||||||
|
})
|
||||||
|
})}
|
||||||
|
</Suspense>
|
||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[component]
|
||||||
|
fn ManageInstance(
|
||||||
|
configs: Vec<super::configs::BeaconConfig>,
|
||||||
|
templates: Vec<super::templates::BeaconTemplate>,
|
||||||
|
categories: Vec<super::categories::Category>,
|
||||||
|
instance: BeaconInstance,
|
||||||
|
update_instance: ServerAction<UpdateInstancePrefs>,
|
||||||
|
remove_category: ServerAction<RemoveCategory>
|
||||||
|
) -> impl IntoView {
|
||||||
|
|
||||||
|
view! {
|
||||||
|
<h3>"Beacon instance"</h3>
|
||||||
|
<div><span>"Beacon ID: "</span> {instance.beacon_id.clone()}</div>
|
||||||
|
<div><span>"Peer IP: "</span> {instance.peer_ip.clone()}</div>
|
||||||
|
<div><span>"Current working directory: "</span> {instance.cwd.clone()}</div>
|
||||||
|
<div><span>"Operating system: "</span> {instance.operating_system.clone()}</div>
|
||||||
|
<div><span>"User: "</span> {instance.beacon_userent.clone()}</div>
|
||||||
|
<div><span>"Hostname: "</span> {instance.hostname.clone()}</div>
|
||||||
|
<div><span>"Version: "</span> {instance.version.to_string()}</div>
|
||||||
|
{templates.iter().find(|t| t.template_id == instance.template_id).map(|t| view! {
|
||||||
|
<div><span>"Template: "</span> {t.template_name.clone()}</div>
|
||||||
|
})}
|
||||||
|
<div>"Categories:"</div>
|
||||||
|
<ul>
|
||||||
|
{categories
|
||||||
|
.iter()
|
||||||
|
.filter(|cat| instance.category_ids.contains(&cat.category_id))
|
||||||
|
.map(|cat| view! {
|
||||||
|
<li>
|
||||||
|
{cat.category_name.clone()}
|
||||||
|
|
||||||
|
<button
|
||||||
|
on:click={
|
||||||
|
let category_id = cat.category_id;
|
||||||
|
let beacon_id = instance.beacon_id.clone();
|
||||||
|
move |_| {
|
||||||
|
remove_category.dispatch(RemoveCategory {
|
||||||
|
beacon_id: beacon_id.clone(),
|
||||||
|
category_id
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
class="warning"
|
||||||
|
>
|
||||||
|
"Remove"
|
||||||
|
</button>
|
||||||
|
</li>
|
||||||
|
})
|
||||||
|
.collect_view()}
|
||||||
|
</ul>
|
||||||
|
<ActionForm action=update_instance>
|
||||||
|
<fieldset>
|
||||||
|
<input type="hidden" name="beacon_id" value=instance.beacon_id.clone()/>
|
||||||
|
<legend>"Beacon configuration preferences"</legend>
|
||||||
|
<label>"Nickname"</label>
|
||||||
|
<input name="nickname" value=instance.nickname.clone()/>
|
||||||
|
<label>"Override configuration"</label>
|
||||||
|
<select name="config_id">
|
||||||
|
<option value="-1">"---"</option>
|
||||||
|
{configs
|
||||||
|
.iter()
|
||||||
|
.map(|conf| view! {
|
||||||
|
<option
|
||||||
|
value=conf.config_id
|
||||||
|
selected=Some(conf.config_id) == instance.config_id
|
||||||
|
>
|
||||||
|
{conf.config_name.clone()}
|
||||||
|
</option>
|
||||||
|
})
|
||||||
|
.collect_view()}
|
||||||
|
</select>
|
||||||
|
<label>"Add new category"</label>
|
||||||
|
<select name="new_category_id">
|
||||||
|
<option value="-1">"---"</option>
|
||||||
|
{categories
|
||||||
|
.iter()
|
||||||
|
.filter(|cat| !instance.category_ids.contains(&cat.category_id))
|
||||||
|
.map(|cat| view! {
|
||||||
|
<option value=cat.category_id>
|
||||||
|
{cat.category_name.clone()}
|
||||||
|
</option>
|
||||||
|
})
|
||||||
|
.collect_view()}
|
||||||
|
</select>
|
||||||
|
<div></div>
|
||||||
|
<input type="submit" value="Submit"/>
|
||||||
|
</fieldset>
|
||||||
|
</ActionForm>
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@ -104,13 +104,13 @@ pub fn BeaconSidebar() -> impl IntoView {
|
|||||||
match m {
|
match m {
|
||||||
SidebarEvents::BeaconList(bs) => {
|
SidebarEvents::BeaconList(bs) => {
|
||||||
let mut bs = bs.to_vec();
|
let mut bs = bs.to_vec();
|
||||||
bs.sort_by_key(|b| b.last_checkin);
|
bs.sort_by_key(|b| std::cmp::Reverse(b.last_checkin));
|
||||||
current_beacons.set(Some(bs));
|
current_beacons.set(Some(bs));
|
||||||
}
|
}
|
||||||
SidebarEvents::NewBeacon(b) => {
|
SidebarEvents::NewBeacon(b) => {
|
||||||
current_beacons.update(|bso| {
|
current_beacons.update(|bso| {
|
||||||
if let Some(ref mut bs) = bso {
|
if let Some(ref mut bs) = bso {
|
||||||
bs.push(b.clone())
|
bs.insert(0, b.clone())
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@ -126,9 +126,15 @@ pub fn BeaconSidebar() -> impl IntoView {
|
|||||||
let Some(ref mut bs) = bs else {
|
let Some(ref mut bs) = bs else {
|
||||||
return;
|
return;
|
||||||
};
|
};
|
||||||
if let Some(ref mut b) = bs.iter_mut().find(|b| b.beacon_id == *bid) {
|
let Some(bpos) = bs.iter().position(|b| b.beacon_id == *bid) else {
|
||||||
b.last_checkin = chrono::Utc::now();
|
return;
|
||||||
|
};
|
||||||
|
bs[bpos].last_checkin = chrono::Utc::now();
|
||||||
|
let first = bs[bpos].clone();
|
||||||
|
for i in (1..=bpos).rev() {
|
||||||
|
bs[i] = bs[i - 1].clone();
|
||||||
}
|
}
|
||||||
|
bs[0] = first;
|
||||||
}),
|
}),
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
@ -558,7 +564,7 @@ pub fn BeaconSidebar() -> impl IntoView {
|
|||||||
each={
|
each={
|
||||||
move || (partition.beacons)()
|
move || (partition.beacons)()
|
||||||
}
|
}
|
||||||
key=|b| (b.beacon_id.clone(), b.last_checkin)
|
key=|b| (b.beacon_id.clone(), b.last_checkin, b.nickname.clone(), b.category_ids.len())
|
||||||
let:beacon
|
let:beacon
|
||||||
>
|
>
|
||||||
<div class="beacon-instance">
|
<div class="beacon-instance">
|
||||||
@ -574,6 +580,7 @@ pub fn BeaconSidebar() -> impl IntoView {
|
|||||||
Either::Right(view! {
|
Either::Right(view! {
|
||||||
<A href=format!("/beacons/beacon/{}", &beacon.beacon_id)>
|
<A href=format!("/beacons/beacon/{}", &beacon.beacon_id)>
|
||||||
{nick}
|
{nick}
|
||||||
|
" "
|
||||||
<span>"(" {beacon.beacon_id.clone()} ")"</span>
|
<span>"(" {beacon.beacon_id.clone()} ")"</span>
|
||||||
</A>
|
</A>
|
||||||
})
|
})
|
||||||
|
|||||||
@ -361,6 +361,20 @@ async fn handle_listener_events(
|
|||||||
|
|
||||||
use crate::beacons::sidebar::{CurrentBeaconInstance, SidebarEvents};
|
use crate::beacons::sidebar::{CurrentBeaconInstance, SidebarEvents};
|
||||||
|
|
||||||
|
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 beacons = sqlx::query!(
|
let beacons = sqlx::query!(
|
||||||
r#"SELECT beacon_id, template_id, peer_ip, nickname, cwd, operating_system,
|
r#"SELECT beacon_id, template_id, peer_ip, nickname, cwd, operating_system,
|
||||||
@ -369,20 +383,6 @@ async fn handle_listener_events(
|
|||||||
.fetch_all(&state.db)
|
.fetch_all(&state.db)
|
||||||
.await?;
|
.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(
|
let last_checkin: Vec<CheckinResult> = sqlx::query_as(
|
||||||
"SELECT beacon_id, MAX(checkin_date) as checkin_date FROM beacon_checkin
|
"SELECT beacon_id, MAX(checkin_date) as checkin_date FROM beacon_checkin
|
||||||
GROUP BY beacon_id"
|
GROUP BY beacon_id"
|
||||||
@ -476,6 +476,49 @@ async fn handle_listener_events(
|
|||||||
let json = serde_json::to_string(&SidebarEvents::NewBeacon(beacon))?;
|
let json = serde_json::to_string(&SidebarEvents::NewBeacon(beacon))?;
|
||||||
socket.send(ws::Message::Text(json)).await?;
|
socket.send(ws::Message::Text(json)).await?;
|
||||||
}
|
}
|
||||||
|
Ok(BeaconEvent::BeaconUpdate(bid)) => {
|
||||||
|
let beacon = sqlx::query!(
|
||||||
|
r#"SELECT template_id, peer_ip, nickname, cwd, operating_system, beacon_userent, hostname, config_id, version as "version: Version" FROM beacon_instance
|
||||||
|
WHERE beacon_id = ?"#,
|
||||||
|
bid
|
||||||
|
)
|
||||||
|
.fetch_one(&state.db)
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
let category_ids = sqlx::query!(
|
||||||
|
"SELECT category_id FROM beacon_category_assignment
|
||||||
|
WHERE beacon_id = ?",
|
||||||
|
bid
|
||||||
|
)
|
||||||
|
.fetch_all(&state.db)
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
let check_in: CheckinResult = sqlx::query_as(
|
||||||
|
r#"SELECT beacon_id, MAX(checkin_date) as checkin_date FROM beacon_checkin
|
||||||
|
WHERE beacon_id = ?"#,
|
||||||
|
)
|
||||||
|
.bind(&bid)
|
||||||
|
.fetch_one(&state.db)
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
let beacon = CurrentBeaconInstance {
|
||||||
|
beacon_id: bid,
|
||||||
|
template_id: beacon.template_id,
|
||||||
|
ip: beacon.peer_ip,
|
||||||
|
nickname: beacon.nickname,
|
||||||
|
cwd: beacon.cwd,
|
||||||
|
operating_system: beacon.operating_system,
|
||||||
|
userent: beacon.beacon_userent,
|
||||||
|
hostname: beacon.hostname,
|
||||||
|
config_id: beacon.config_id,
|
||||||
|
version: beacon.version,
|
||||||
|
last_checkin: check_in.checkin_date,
|
||||||
|
category_ids: category_ids.iter().map(|r| r.category_id).collect()
|
||||||
|
};
|
||||||
|
|
||||||
|
let json = serde_json::to_string(&SidebarEvents::BeaconUpdate(beacon.beacon_id.clone(), beacon))?;
|
||||||
|
socket.send(ws::Message::Text(json)).await?;
|
||||||
|
}
|
||||||
Ok(_) => {
|
Ok(_) => {
|
||||||
// this event isn't meant for public announcement
|
// this event isn't meant for public announcement
|
||||||
}
|
}
|
||||||
@ -486,6 +529,43 @@ async fn handle_listener_events(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub async fn attach_to_beacon(
|
||||||
|
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_beacon_socket(socket, state).await {
|
||||||
|
tracing::warn!("Encountered error when handling beacon subscriber: {e}");
|
||||||
|
};
|
||||||
|
})
|
||||||
|
.into_response()
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn handle_beacon_socket(
|
||||||
|
mut socket: ws::WebSocket,
|
||||||
|
state: AppState,
|
||||||
|
) -> Result<(), crate::error::Error> {
|
||||||
|
unimplemented!()
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
pub async fn serve_web(
|
pub async fn serve_web(
|
||||||
management_address: SocketAddrV4,
|
management_address: SocketAddrV4,
|
||||||
file_store: PathBuf,
|
file_store: PathBuf,
|
||||||
|
|||||||
@ -0,0 +1,15 @@
|
|||||||
|
div.instance {
|
||||||
|
padding: 10px;
|
||||||
|
overflow-y: scroll;
|
||||||
|
|
||||||
|
fieldset {
|
||||||
|
margin-top: 20px;
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: 400px 200px;
|
||||||
|
grid-row-gap: 10px;
|
||||||
|
|
||||||
|
input, label {
|
||||||
|
margin: 10px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -68,6 +68,7 @@ aside.beacons {
|
|||||||
|
|
||||||
.beacon-instance-id a {
|
.beacon-instance-id a {
|
||||||
text-decoration: none;
|
text-decoration: none;
|
||||||
|
color: white;
|
||||||
|
|
||||||
&:hover {
|
&:hover {
|
||||||
text-decoration: underline;
|
text-decoration: underline;
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user