feat: added category management
This commit is contained in:
parent
b381261cea
commit
66b59531c5
@ -39,8 +39,11 @@ CREATE TABLE beacon_template (
|
||||
source_netmask int,
|
||||
source_gateway varchar,
|
||||
|
||||
FOREIGN KEY (config_id) REFERENCES beacon_config
|
||||
FOREIGN KEY (listener_id) REFERENCES beacon_listener
|
||||
default_category int,
|
||||
|
||||
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 (
|
||||
|
||||
@ -7,6 +7,8 @@ use leptos_router::{
|
||||
};
|
||||
use serde::{Serialize, Deserialize};
|
||||
|
||||
use crate::users::User;
|
||||
|
||||
#[server]
|
||||
pub async fn test_retrieve() -> Result<u64, ServerFnError> {
|
||||
use leptos::server_fn::error::NoCustomError;
|
||||
@ -22,12 +24,6 @@ pub async fn test_retrieve() -> Result<u64, ServerFnError> {
|
||||
Ok(since_the_epoch)
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Serialize, Deserialize)]
|
||||
pub struct User {
|
||||
user_id: i64,
|
||||
user_name: String,
|
||||
}
|
||||
|
||||
#[server]
|
||||
pub async fn me() -> Result<Option<User>, ServerFnError> {
|
||||
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());
|
||||
});
|
||||
|
||||
crate::beacons::provide_beacon_resources();
|
||||
|
||||
view! {
|
||||
<Stylesheet id="leptos" href="/pkg/sparse-server.css"/>
|
||||
|
||||
|
||||
@ -15,11 +15,63 @@ pub use instances::InstancesView;
|
||||
pub use listeners::ListenersView;
|
||||
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]
|
||||
pub fn BeaconView() -> impl IntoView {
|
||||
#[cfg(feature = "hydrate")]
|
||||
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() {
|
||||
let navigate = leptos_router::hooks::use_navigate();
|
||||
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]
|
||||
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,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]
|
||||
pub fn CategoriesView() -> impl IntoView {
|
||||
let BeaconResources { add_category, categories, .. } = expect_context();
|
||||
|
||||
|
||||
view! {
|
||||
<div>
|
||||
<p>"Categories"</p>
|
||||
<ul>
|
||||
<li>"Windows"</li>
|
||||
</ul>
|
||||
<div class="categories">
|
||||
<h2>"Categories"</h2>
|
||||
<p>
|
||||
"Categories are an optional organization method that can be used to group beacons. Beacons can be assigned to multiple categories"
|
||||
</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>
|
||||
}
|
||||
}
|
||||
|
||||
#[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>
|
||||
}
|
||||
}
|
||||
|
||||
@ -10,6 +10,8 @@ use {
|
||||
rcgen::{generate_simple_self_signed, CertifiedKey},
|
||||
};
|
||||
|
||||
use super::BeaconResources;
|
||||
|
||||
#[cfg(feature = "ssr")]
|
||||
struct DbListener {
|
||||
listener_id: i64,
|
||||
@ -101,6 +103,11 @@ pub async fn add_listener(public_ip: String, port: i16, domain_name: String) ->
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[server]
|
||||
pub async fn remove_listener(listener_id: i64) -> Result<(), ServerFnError> {
|
||||
unimplemented!()
|
||||
}
|
||||
|
||||
#[server]
|
||||
pub async fn start_listener(listener_id: i64) -> Result<(), ServerFnError> {
|
||||
unimplemented!()
|
||||
@ -108,15 +115,14 @@ pub async fn start_listener(listener_id: i64) -> Result<(), ServerFnError> {
|
||||
|
||||
#[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 }
|
||||
);
|
||||
let super::BeaconResources { add_listener, listeners, .. } = expect_context::<super::BeaconResources>();
|
||||
|
||||
view! {
|
||||
<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>
|
||||
<fieldset>
|
||||
{move || match add_listener.value().get() {
|
||||
@ -147,10 +153,7 @@ pub fn ListenersView() -> impl IntoView {
|
||||
<p>{format!("error: {}", e)}</p>
|
||||
}),
|
||||
Ok(ls) => Either::Right(view! {
|
||||
<DisplayListeners
|
||||
listener_resource=listeners
|
||||
listeners=ls
|
||||
/>
|
||||
<DisplayListeners listeners=ls />
|
||||
})
|
||||
}),
|
||||
None => Either::Left(view! {
|
||||
@ -163,7 +166,9 @@ pub fn ListenersView() -> impl IntoView {
|
||||
}
|
||||
|
||||
#[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 start_listener_action = Action::new(move |&id: &i64| async move {
|
||||
match start_listener(id).await {
|
||||
@ -191,6 +196,17 @@ fn DisplayListeners(listener_resource: Resource<Result<Vec<PubListener>, ServerF
|
||||
{match listener.active {
|
||||
true => Either::Left(view! {
|
||||
<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! {
|
||||
<button
|
||||
|
||||
@ -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)]
|
||||
pub struct DbUser {
|
||||
user_id: i64,
|
||||
@ -250,9 +262,8 @@ async fn add_user(name: String, password: String) -> Result<(), ServerFnError> {
|
||||
|
||||
#[component]
|
||||
pub fn UserView() -> impl IntoView {
|
||||
#[cfg(feature = "hydrate")]
|
||||
Effect::new(move || {
|
||||
let user = expect_context::<ReadSignal<Option<crate::app::User>>>();
|
||||
let user = expect_context::<ReadSignal<Option<User>>>();
|
||||
if user.get().is_none() {
|
||||
let navigate = leptos_router::hooks::use_navigate();
|
||||
navigate("/login?next=users", Default::default());
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user