feat: added category management

This commit is contained in:
Andrew Rioux 2025-01-31 00:04:54 -05:00
parent b381261cea
commit 66b59531c5
Signed by: andrew.rioux
GPG Key ID: 9B8BAC47C17ABB94
6 changed files with 381 additions and 29 deletions

View File

@ -39,8 +39,11 @@ CREATE TABLE beacon_template (
source_netmask int, source_netmask int,
source_gateway varchar, source_gateway varchar,
FOREIGN KEY (config_id) REFERENCES beacon_config default_category int,
FOREIGN KEY (listener_id) REFERENCES beacon_listener
FOREIGN KEY (config_id) REFERENCES beacon_config,
FOREIGN KEY (listener_id) REFERENCES beacon_listener,
FOREIGN KEY (default_category) REFERENCES beacon_category
); );
CREATE TABLE beacon_instance ( CREATE TABLE beacon_instance (

View File

@ -7,6 +7,8 @@ use leptos_router::{
}; };
use serde::{Serialize, Deserialize}; use serde::{Serialize, Deserialize};
use crate::users::User;
#[server] #[server]
pub async fn test_retrieve() -> Result<u64, ServerFnError> { pub async fn test_retrieve() -> Result<u64, ServerFnError> {
use leptos::server_fn::error::NoCustomError; use leptos::server_fn::error::NoCustomError;
@ -22,12 +24,6 @@ pub async fn test_retrieve() -> Result<u64, ServerFnError> {
Ok(since_the_epoch) Ok(since_the_epoch)
} }
#[derive(Clone, Debug, Serialize, Deserialize)]
pub struct User {
user_id: i64,
user_name: String,
}
#[server] #[server]
pub async fn me() -> Result<Option<User>, ServerFnError> { pub async fn me() -> Result<Option<User>, ServerFnError> {
let user = crate::db::user::get_auth_session().await?; let user = crate::db::user::get_auth_session().await?;
@ -103,6 +99,8 @@ pub fn App() -> impl IntoView {
leptos::logging::log!("User resource: {:?}", user.get()); leptos::logging::log!("User resource: {:?}", user.get());
}); });
crate::beacons::provide_beacon_resources();
view! { view! {
<Stylesheet id="leptos" href="/pkg/sparse-server.css"/> <Stylesheet id="leptos" href="/pkg/sparse-server.css"/>

View File

@ -15,11 +15,63 @@ pub use instances::InstancesView;
pub use listeners::ListenersView; pub use listeners::ListenersView;
pub use templates::TemplatesView; pub use templates::TemplatesView;
#[derive(Clone)]
pub struct BeaconResources {
add_listener: ServerAction<listeners::AddListener>,
remove_listener: ServerAction<listeners::RemoveListener>,
add_category: ServerAction<categories::AddCategory>,
remove_category: ServerAction<categories::RemoveCategory>,
rename_category: ServerAction<categories::RenameCategory>,
listeners: Resource<Result<Vec<listeners::PubListener>, ServerFnError>>,
categories: Resource<Result<Vec<categories::Category>, ServerFnError>>,
}
pub fn provide_beacon_resources() {
let user = expect_context::<ReadSignal<Option<crate::users::User>>>();
let add_listener = ServerAction::<listeners::AddListener>::new();
let remove_listener = ServerAction::<listeners::RemoveListener>::new();
let add_category = ServerAction::<categories::AddCategory>::new();
let remove_category = ServerAction::<categories::RemoveCategory>::new();
let rename_category = ServerAction::<categories::RenameCategory>::new();
let listeners = Resource::new(
move || (
user.get(),
add_listener.version().get(),
remove_listener.version().get(),
),
|_| async { listeners::get_listeners().await }
);
let categories = Resource::new(
move || (
user.get(),
add_category.version().get(),
remove_category.version().get(),
rename_category.version().get(),
),
|_| async { categories::get_categories().await }
);
provide_context(BeaconResources {
add_listener,
remove_listener,
add_category,
remove_category,
rename_category,
listeners,
categories
});
}
#[component] #[component]
pub fn BeaconView() -> impl IntoView { pub fn BeaconView() -> impl IntoView {
#[cfg(feature = "hydrate")]
Effect::new(move || { Effect::new(move || {
let user = expect_context::<ReadSignal<Option<crate::app::User>>>(); let user = expect_context::<ReadSignal<Option<crate::users::User>>>();
if user.get().is_none() { if user.get().is_none() {
let navigate = leptos_router::hooks::use_navigate(); let navigate = leptos_router::hooks::use_navigate();
navigate("/login?next=beacons", Default::default()); navigate("/login?next=beacons", Default::default());
@ -44,11 +96,52 @@ 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] #[component]
pub fn BeaconSidebar() -> impl IntoView { pub fn BeaconSidebar() -> impl IntoView {
let (sort_method, set_sort_method) = signal(SortMethod::Category);
let search_input = RwSignal::new("".to_string());
view! { view! {
<aside class="beacons"> <aside class="beacons">
<div class="sort-method">
</div>
<div class="search">
</div>
</aside> </aside>
} }
} }

View File

@ -1,13 +1,244 @@
use leptos::prelude::*; use leptos::{either::Either, prelude::*};
use serde::{Serialize, Deserialize};
#[cfg(feature = "ssr")]
use {
sqlx::SqlitePool,
leptos::server_fn::error::NoCustomError,
crate::db::user,
};
use super::BeaconResources;
#[derive(Clone, Serialize, Deserialize)]
pub struct Category {
category_id: i64,
category_name: String
}
#[server]
pub async fn get_categories() -> Result<Vec<Category>, 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>();
Ok(
sqlx::query_as!(
Category,
"SELECT * FROM beacon_category"
)
.fetch_all(&db)
.await?
)
}
#[server]
pub async fn add_category(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()));
}
let db = expect_context::<SqlitePool>();
sqlx::query!(
"INSERT INTO beacon_category (category_name) VALUES (?)",
name
)
.execute(&db)
.await?;
Ok(())
}
#[server]
pub async fn remove_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 = expect_context::<SqlitePool>();
sqlx::query!(
"DELETE FROM beacon_category WHERE category_id = ?",
id
)
.execute(&db)
.await?;
Ok(())
}
#[server]
pub async fn rename_category(id: i64, 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()));
}
let db = expect_context::<SqlitePool>();
sqlx::query!(
"UPDATE beacon_category SET category_name = ? WHERE category_id = ?",
name,
id
)
.execute(&db)
.await?;
Ok(())
}
#[component] #[component]
pub fn CategoriesView() -> impl IntoView { pub fn CategoriesView() -> impl IntoView {
let BeaconResources { add_category, categories, .. } = expect_context();
view! { view! {
<div> <div class="categories">
<p>"Categories"</p> <h2>"Categories"</h2>
<ul> <p>
<li>"Windows"</li> "Categories are an optional organization method that can be used to group beacons. Beacons can be assigned to multiple categories"
</ul> </p>
<ActionForm action=add_category>
<fieldset>
{move || match add_category.value().get() {
Some(Ok(_)) => Either::Right(()),
None => Either::Right(()),
Some(Err(e)) => Either::Left(view! {
<p>"Error creating category:"</p>
<p>{format!("{e:?}")}</p>
})
}}
<legend>"Add a new beacon category"</legend>
<label>"Category name"</label>
<input name="name"/>
<div></div>
<input type="submit" value="Submit" disabled=move||add_category.pending() />
</fieldset>
</ActionForm>
<Suspense fallback=|| view! { <p>"Loading..."</p> }>
{ move || match categories.get() {
Some(inner) => Either::Right(match inner {
Err(e) => Either::Left(view! {
<p>"There was an error loading categories:"</p>
<p>{format!("error: {e:?}")}</p>
}),
Ok(cs) => Either::Right(view! {
<DisplayCategories categories=cs/>
})
}),
None => Either::Left(view! {
<p>"Loading..."</p>
})
}}
</Suspense>
</div> </div>
} }
} }
#[component]
fn DisplayCategories(categories: Vec<Category>) -> impl IntoView {
let BeaconResources { remove_category, rename_category, .. } = expect_context();
let (target_rename_id, set_target_rename_id) = signal(0);
let target_rename_name = RwSignal::new("".to_owned());
let dialog_ref = NodeRef::<leptos::html::Dialog>::new();
let categories_view = categories
.iter()
.map(|category| view! {
<li>
{category.category_id}
": "
{category.category_name.clone()}
<button
on:click={
let id = category.category_id;
let name = category.category_name.clone();
let set_target_rename_id = set_target_rename_id.clone();
let target_rename_name = target_rename_name.clone();
move |_| {
set_target_rename_id(id);
target_rename_name.set(name.clone());
if let Some(dialog) = dialog_ref.get() {
let _ = dialog.show_modal();
}
}
}
>
"rename"
</button>
<button
on:click={
let id = category.category_id;
move |_| {
remove_category.dispatch(RemoveCategory { id });
}
}
>
"delete"
</button>
</li>
})
.collect_view();
Effect::watch(
move || (
rename_category.version().get(),
rename_category.value().get(),
dialog_ref.get()
),
move |(_, res,dialog_ref),_,_| {
if let (Some(Ok(())), Some(dialog)) = (res, dialog_ref) {
let _ = dialog.close();
}
},
false
);
view! {
<dialog node_ref=dialog_ref>
<ActionForm action=rename_category>
{move || match rename_category.value().get() {
Some(Ok(_)) => Either::Right(()),
None => Either::Right(()),
Some(Err(e)) => Either::Left(view! {
<p>"Failed to rename category:"</p>
<p>{format!("{e:?}")}</p>
})
}}
<fieldset>
<legend>"Rename category"</legend>
<label>"Name"</label>
<input name="name" bind:value=target_rename_name />
<input type="hidden" name="id" value=move||target_rename_id.get() />
<input type="submit" value="Submit" disabled=move||rename_category.pending()/>
</fieldset>
</ActionForm>
</dialog>
{move || match remove_category.value().get() {
Some(Ok(_)) => Either::Right(()),
None => Either::Right(()),
Some(Err(e)) => Either::Left(view! {
<p>"Failed to remove category:"</p>
<p>{format!("{e:?}")}</p>
})
}}
<ul>
{categories_view}
</ul>
}
}

View File

@ -10,6 +10,8 @@ use {
rcgen::{generate_simple_self_signed, CertifiedKey}, rcgen::{generate_simple_self_signed, CertifiedKey},
}; };
use super::BeaconResources;
#[cfg(feature = "ssr")] #[cfg(feature = "ssr")]
struct DbListener { struct DbListener {
listener_id: i64, listener_id: i64,
@ -101,6 +103,11 @@ pub async fn add_listener(public_ip: String, port: i16, domain_name: String) ->
Ok(()) Ok(())
} }
#[server]
pub async fn remove_listener(listener_id: i64) -> Result<(), ServerFnError> {
unimplemented!()
}
#[server] #[server]
pub async fn start_listener(listener_id: i64) -> Result<(), ServerFnError> { pub async fn start_listener(listener_id: i64) -> Result<(), ServerFnError> {
unimplemented!() unimplemented!()
@ -108,15 +115,14 @@ pub async fn start_listener(listener_id: i64) -> Result<(), ServerFnError> {
#[component] #[component]
pub fn ListenersView() -> impl IntoView { pub fn ListenersView() -> impl IntoView {
let add_listener = ServerAction::<AddListener>::new(); let super::BeaconResources { add_listener, listeners, .. } = expect_context::<super::BeaconResources>();
let listeners = Resource::new(
move || add_listener.version().get(),
|_| async { get_listeners().await }
);
view! { view! {
<div class="listeners"> <div class="listeners">
<h2>"Listeners"</h2>
<p>
"Listeners bind to an IP and port and have a public/private key pair that beacons can use to maintain TLS encrypted connections back to the C2 server"
</p>
<ActionForm action=add_listener> <ActionForm action=add_listener>
<fieldset> <fieldset>
{move || match add_listener.value().get() { {move || match add_listener.value().get() {
@ -147,10 +153,7 @@ pub fn ListenersView() -> impl IntoView {
<p>{format!("error: {}", e)}</p> <p>{format!("error: {}", e)}</p>
}), }),
Ok(ls) => Either::Right(view! { Ok(ls) => Either::Right(view! {
<DisplayListeners <DisplayListeners listeners=ls />
listener_resource=listeners
listeners=ls
/>
}) })
}), }),
None => Either::Left(view! { None => Either::Left(view! {
@ -163,7 +166,9 @@ pub fn ListenersView() -> impl IntoView {
} }
#[component] #[component]
fn DisplayListeners(listener_resource: Resource<Result<Vec<PubListener>, ServerFnError>>, listeners: Vec<PubListener>) -> impl IntoView { fn DisplayListeners(listeners: Vec<PubListener>) -> impl IntoView {
let BeaconResources { listeners: listener_resource, remove_listener, .. } = expect_context::<BeaconResources>();
let (error_msg, set_error_msg) = signal(None); let (error_msg, set_error_msg) = signal(None);
let start_listener_action = Action::new(move |&id: &i64| async move { let start_listener_action = Action::new(move |&id: &i64| async move {
match start_listener(id).await { match start_listener(id).await {
@ -191,6 +196,17 @@ fn DisplayListeners(listener_resource: Resource<Result<Vec<PubListener>, ServerF
{match listener.active { {match listener.active {
true => Either::Left(view! { true => Either::Left(view! {
<span>"active!"</span> <span>"active!"</span>
<button
on:click={
let id = listener.listener_id;
move |e| {
let _ = e.prevent_default();
remove_listener.dispatch(RemoveListener { listener_id: id });
}
}
>
"delete"
</button>
}), }),
false => Either::Right(view! { false => Either::Right(view! {
<button <button

View File

@ -28,6 +28,18 @@ fn format_delta(time: chrono::TimeDelta) -> String {
} }
} }
#[derive(Clone, Debug, Serialize, Deserialize, Eq)]
pub struct User {
pub user_id: i64,
pub user_name: String,
}
impl std::cmp::PartialEq for User {
fn eq(&self, other: &Self) -> bool {
self.user_id == other.user_id
}
}
#[derive(Clone, Serialize, Deserialize)] #[derive(Clone, Serialize, Deserialize)]
pub struct DbUser { pub struct DbUser {
user_id: i64, user_id: i64,
@ -250,9 +262,8 @@ async fn add_user(name: String, password: String) -> Result<(), ServerFnError> {
#[component] #[component]
pub fn UserView() -> impl IntoView { pub fn UserView() -> impl IntoView {
#[cfg(feature = "hydrate")]
Effect::new(move || { Effect::new(move || {
let user = expect_context::<ReadSignal<Option<crate::app::User>>>(); let user = expect_context::<ReadSignal<Option<User>>>();
if user.get().is_none() { if user.get().is_none() {
let navigate = leptos_router::hooks::use_navigate(); let navigate = leptos_router::hooks::use_navigate();
navigate("/login?next=users", Default::default()); navigate("/login?next=users", Default::default());