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_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 (
|
||||||
|
|||||||
@ -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"/>
|
||||||
|
|
||||||
|
|||||||
@ -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>
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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>
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@ -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
|
||||||
|
|||||||
@ -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());
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user