feat: added most beacon DS and listener CRUD

This commit is contained in:
Andrew Rioux
2025-01-30 20:59:14 -05:00
parent 0d6b2b4c16
commit b381261cea
26 changed files with 599 additions and 26 deletions

View File

@@ -1,7 +1,7 @@
use leptos::prelude::*;
use leptos_meta::{provide_meta_context, MetaTags, Stylesheet, Title};
use leptos_router::{
components::{A, Route, Router, Routes},
components::{A, ParentRoute, Route, Router, Routes},
hooks::use_query_map,
path
};
@@ -30,12 +30,8 @@ pub struct User {
#[server]
pub async fn me() -> Result<Option<User>, ServerFnError> {
tracing::info!("I'm being checked!");
let user = crate::db::user::get_auth_session().await?;
tracing::debug!("User returned: {:?}", user);
Ok(user.map(|user| User {
user_id: user.user_id,
user_name: user.user_name
@@ -149,12 +145,22 @@ pub fn App() -> impl IntoView {
</Suspense>
</nav>
<aside class="beacons">
</aside>
<crate::beacons::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!("") view=|| view! {
<p>"Select a menu item on the left to get started"</p>
}/>
</ParentRoute>
<Route path=path!("") view=HomePage/>
</Routes>
</Router>

View File

@@ -0,0 +1,10 @@
use std::{
collections::HashMap,
sync::{Arc, RwLock},
};
use tokio::task::JoinHandle;
pub type BeaconListenerHandle = JoinHandle<()>;
pub type BeaconListenerMap = Arc<RwLock<HashMap<i64, BeaconListenerHandle>>>;

View File

@@ -0,0 +1,54 @@
use leptos::prelude::*;
use leptos_router::{components::A, nested_router::Outlet};
mod categories;
mod commands;
mod configs;
mod instances;
mod listeners;
mod templates;
pub use categories::CategoriesView;
pub use commands::CommandsView;
pub use configs::ConfigsView;
pub use instances::InstancesView;
pub use listeners::ListenersView;
pub use templates::TemplatesView;
#[component]
pub fn BeaconView() -> impl IntoView {
#[cfg(feature = "hydrate")]
Effect::new(move || {
let user = expect_context::<ReadSignal<Option<crate::app::User>>>();
if user.get().is_none() {
let navigate = leptos_router::hooks::use_navigate();
navigate("/login?next=beacons", Default::default());
}
});
view! {
<main class="beacons">
<aside class="beacon-menu">
<ul>
<li><A href="/beacons/listeners">"Listeners"</A></li>
<li><A href="/beacons/configs">"Configs"</A></li>
<li><A href="/beacons/categories">"Categories"</A></li>
<li><A href="/beacons/templates">"Templates"</A></li>
<li><A href="/beacons/instances">"Instances"</A></li>
<li><A href="/beacons/commands">"Commands"</A></li>
</ul>
</aside>
<Outlet />
</main>
}
}
#[component]
pub fn BeaconSidebar() -> impl IntoView {
view! {
<aside class="beacons">
</aside>
}
}

View File

@@ -0,0 +1,13 @@
use leptos::prelude::*;
#[component]
pub fn CategoriesView() -> impl IntoView {
view! {
<div>
<p>"Categories"</p>
<ul>
<li>"Windows"</li>
</ul>
</div>
}
}

View File

@@ -0,0 +1,10 @@
use leptos::prelude::*;
#[component]
pub fn CommandsView() -> impl IntoView {
view! {
<div>
</div>
}
}

View File

@@ -0,0 +1,10 @@
use leptos::prelude::*;
#[component]
pub fn ConfigsView() -> impl IntoView {
view! {
<div>
</div>
}
}

View File

@@ -0,0 +1,10 @@
use leptos::prelude::*;
#[component]
pub fn InstancesView() -> impl IntoView {
view! {
<div>
</div>
}
}

View File

@@ -0,0 +1,226 @@
use std::net::Ipv4Addr;
use leptos::{either::Either, prelude::*};
use serde::{Serialize, Deserialize};
#[cfg(feature = "ssr")]
use {
sqlx::SqlitePool,
leptos::server_fn::error::NoCustomError,
crate::{db::user, beacon_handler::BeaconListenerMap},
rcgen::{generate_simple_self_signed, CertifiedKey},
};
#[cfg(feature = "ssr")]
struct DbListener {
listener_id: i64,
port: i64,
public_ip: String,
domain_name: String
}
#[derive(Clone, Serialize, Deserialize)]
pub struct PubListener {
listener_id: i64,
port: i64,
public_ip: String,
domain_name: String,
active: bool
}
#[server]
pub async fn get_listeners() -> Result<Vec<PubListener>, 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 = expect_context::<SqlitePool>();
let beacon_handles = expect_context::<BeaconListenerMap>();
let listeners = sqlx::query_as!(
DbListener,
"SELECT listener_id, port, public_ip, domain_name FROM beacon_listener"
)
.fetch_all(&db)
.await?;
let Ok(beacon_handles_handle) = beacon_handles.read() else {
return Err(ServerFnError::<NoCustomError>::ServerError("".to_string()));
};
Ok(listeners
.into_iter()
.map(|b| PubListener {
listener_id: b.listener_id,
port: b.port,
public_ip: b.public_ip,
domain_name: b.domain_name,
active: beacon_handles_handle
.get(&b.listener_id)
.map(|h| !h.is_finished())
.unwrap_or(false)
})
.collect())
}
#[server]
pub async fn add_listener(public_ip: String, port: i16, domain_name: String) -> 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()));
}
if public_ip.parse::<Ipv4Addr>().is_err() {
return Err(ServerFnError::<NoCustomError>::ServerError("Unable to parse public IP address".to_owned()));
}
let subject_alt_names = vec![public_ip.to_string(), domain_name.clone()];
let CertifiedKey { cert, key_pair } = tokio::task::spawn_blocking(|| {
generate_simple_self_signed(subject_alt_names)
}).await??;
let db = expect_context::<SqlitePool>();
let public_ip = public_ip.to_string();
let cert = cert.pem().to_string();
let key_pair = key_pair.serialize_pem().to_string();
sqlx::query!(
"INSERT INTO beacon_listener (port, public_ip, domain_name, certificate, privkey) VALUES (?, ?, ?, ?, ?)",
port,
public_ip,
domain_name,
cert,
key_pair
)
.execute(&db)
.await?;
Ok(())
}
#[server]
pub async fn start_listener(listener_id: i64) -> Result<(), ServerFnError> {
unimplemented!()
}
#[component]
pub fn ListenersView() -> impl IntoView {
let add_listener = ServerAction::<AddListener>::new();
let listeners = Resource::new(
move || add_listener.version().get(),
|_| async { get_listeners().await }
);
view! {
<div class="listeners">
<ActionForm action=add_listener>
<fieldset>
{move || match add_listener.value().get() {
Some(Ok(_)) => Either::Right(()),
None => Either::Right(()),
Some(Err(e)) => Either::Left(view! {
<p>"Error creating listener:"</p>
<p>{format!("{e:?}")}</p>
})
}}
<legend>"Add a new listener"</legend>
<label>"Public IP address"</label>
<input name="public_ip"/>
<label>"Port"</label>
<input name="port" type="number"/>
<label>"Domain name (for HTTPS)"</label>
<input name="domain_name"/>
<div></div>
<input type="submit" value="Submit"/>
</fieldset>
</ActionForm>
<Suspense fallback=|| view! { <p>"Loading..."</p> }>
{ move || match listeners.get() {
Some(inner) => Either::Right(match inner {
Err(e) => Either::Left(view! {
<p>"There was an error loading listeners:"</p>
<p>{format!("error: {}", e)}</p>
}),
Ok(ls) => Either::Right(view! {
<DisplayListeners
listener_resource=listeners
listeners=ls
/>
})
}),
None => Either::Left(view! {
<p>"Loading..."</p>
})
}}
</Suspense>
</div>
}
}
#[component]
fn DisplayListeners(listener_resource: Resource<Result<Vec<PubListener>, ServerFnError>>, listeners: Vec<PubListener>) -> impl IntoView {
let (error_msg, set_error_msg) = signal(None);
let start_listener_action = Action::new(move |&id: &i64| async move {
match start_listener(id).await {
Ok(()) => {
listener_resource.refetch();
}
Err(e) => {
set_error_msg(Some(e.to_string()));
}
}
});
let listeners_view = listeners
.iter()
.map(|listener| view! {
<li>
{listener.listener_id}
": "
{listener.domain_name.clone()}
" ("
{listener.public_ip.clone()}
":"
{listener.port}
")"
{match listener.active {
true => Either::Left(view! {
<span>"active!"</span>
}),
false => Either::Right(view! {
<button
on:click={
let id = listener.listener_id;
move |e| {
let _ = e.prevent_default();
start_listener_action.dispatch(id);
}
}
>
"activate"
</button>
})
}}
</li>
})
.collect_view();
view! {
{move || error_msg
.get()
.map(|err| view! {
<p>
"Error starting listener: "
{err}
</p>
})}
<ul>
{listeners_view}
</ul>
}
}

View File

@@ -0,0 +1,10 @@
use leptos::prelude::*;
#[component]
pub fn TemplatesView() -> impl IntoView {
view! {
<div>
</div>
}
}

View File

@@ -1,10 +1,10 @@
pub mod app;
pub mod users;
pub mod error;
#[cfg(feature = "ssr")]
pub mod beacon_handler;
pub mod beacons;
pub mod db;
pub mod error;
pub mod users;
#[cfg(feature = "hydrate")]
#[wasm_bindgen::prelude::wasm_bindgen]

View File

@@ -1,5 +1,5 @@
#[cfg(feature = "ssr")]
pub(crate) mod beacons {
pub(crate) mod beacon_binaries {
#[allow(dead_code)]
pub const LINUX_BEACON: &'static [u8] = include_bytes!(std::env!("SPARSE_BEACON_LINUX"));
#[allow(dead_code)]
@@ -15,16 +15,15 @@ pub(crate) mod beacons {
#[cfg(feature = "ssr")]
mod cli;
#[cfg(feature = "ssr")]
mod webserver;
#[cfg(feature = "ssr")]
mod beacons;
#[cfg(feature = "ssr")]
pub mod users;
pub mod error;
pub mod db;
pub mod beacon_handler;
#[cfg(feature = "ssr")]
#[tokio::main]
@@ -56,7 +55,7 @@ async fn main() -> anyhow::Result<std::process::ExitCode> {
let db_exists = std::fs::metadata(&db_location);
let run_init = if let Err(e) = db_exists {
if let Err(e) = db_exists {
if !options.init_ok {
tracing::error!("Database doesn't exist, and initialization not allowed!");
tracing::error!("{:?}", e);
@@ -64,10 +63,7 @@ async fn main() -> anyhow::Result<std::process::ExitCode> {
}
tracing::info!("Database doesn't exist, readying initialization");
true
} else {
false
};
}
let pool = SqlitePool::connect_with(
SqliteConnectOptions::from_str(&format!("sqlite://{}", db_location.to_string_lossy()))?

View File

@@ -255,7 +255,7 @@ pub fn UserView() -> impl IntoView {
let user = expect_context::<ReadSignal<Option<crate::app::User>>>();
if user.get().is_none() {
let navigate = leptos_router::hooks::use_navigate();
navigate("/login?next=/users", Default::default());
navigate("/login?next=users", Default::default());
}
});

View File

@@ -12,6 +12,7 @@ pub async fn serve_web(management_address: SocketAddrV4, _bind_address: SocketAd
let conf = get_configuration(None).unwrap();
let leptos_options = conf.leptos_options;
let routes = generate_route_list(App);
let beacon_listeners = crate::beacon_handler::BeaconListenerMap::default();
let compression_layer = tower_http::compression::CompressionLayer::new()
.gzip(true)
@@ -23,7 +24,10 @@ pub async fn serve_web(management_address: SocketAddrV4, _bind_address: SocketAd
.leptos_routes_with_context(
&leptos_options,
routes,
move || provide_context(db.clone()),
move || {
provide_context(std::sync::Arc::clone(&beacon_listeners));
provide_context(db.clone())
},
{
let leptos_options = leptos_options.clone();
move || shell(leptos_options.clone())