diff --git a/src/components.rs b/src/components.rs index c2e133d..c373f61 100644 --- a/src/components.rs +++ b/src/components.rs @@ -2,5 +2,5 @@ pub mod admin; pub mod agenda; pub mod home; pub mod layout; -pub mod news; +pub mod messages; pub mod settings; diff --git a/src/components/layout/navbar.rs b/src/components/layout/navbar.rs index 8b020c0..cd705a8 100644 --- a/src/components/layout/navbar.rs +++ b/src/components/layout/navbar.rs @@ -17,10 +17,10 @@ pub fn Navbar() -> Element { "Home" }, Link { - to: Route::News {}, + to: Route::MessagesPage {}, class: "btn btn-ghost flex-col flex-nowrap py-1 gap-1 font-normal h-max", icons::Newspaper {}, - div { class: "font-normal", "Nieuws" } + div { class: "font-normal", "Berichten" } }, Link { to: Route::Agenda {}, diff --git a/src/components/news.rs b/src/components/messages.rs similarity index 82% rename from src/components/news.rs rename to src/components/messages.rs index 7af4a22..9f609b5 100644 --- a/src/components/news.rs +++ b/src/components/messages.rs @@ -3,7 +3,7 @@ use dioxus::prelude::*; pub mod create; #[component] -pub fn News() -> Element { +pub fn MessagesPage() -> Element { rsx! { div { h1 { class: "text-xl font-bold text-primary", "Nieuws" } diff --git a/src/components/messages/create.rs b/src/components/messages/create.rs new file mode 100644 index 0000000..fa1c5fa --- /dev/null +++ b/src/components/messages/create.rs @@ -0,0 +1,126 @@ +use crate::util::model::message::{Message, Target, TargetKind}; +use crate::util::model::session::Session; +use std::collections::HashMap; + +use dioxus::prelude::*; + +mod content; +mod targets; + +#[derive(PartialEq)] +enum Steps { + Message, + Targets, + Verify, + Done, +} + +#[derive(Props, Clone, PartialEq, Debug)] +struct Form { + title: Signal, + body: Signal, + targets: Signal>, +} + +impl Default for Form { + fn default() -> Self { + Self { + title: use_signal(|| String::new()), + body: use_signal(|| String::new()), + targets: use_signal(|| { + HashMap::from([( + 0, + Target { + kind: TargetKind::None, + value: String::new(), + }, + )]) + }), + } + } +} + +#[component] +pub fn MessagesCreatePage() -> Element { + let mut step = use_signal(|| Steps::Message); + let form = Form::default(); + + let submit_proposal = move |_| async move { + let targets = (form.targets)(); + + if let Ok(amount) = message_proposal( + (form.title)(), + (form.body)(), + targets.into_values().collect(), + ) + .await + { + tracing::info!("{}", amount); + step.set(Steps::Verify); + } else { + tracing::info!("Error occured") + } + }; + + rsx! { + div { + class: "w-full max-w-2xl space-y-3 flex flex-col", + h1 { class: "text-xl font-bold text-primary", "Nieuw bericht" } + ul { + class: "steps pb-10 mx-auto", + li { class: "step step-primary", "Inhoud" }, + li { + class: "step", + class: if let Steps::Targets | Steps::Done | Steps::Verify = *step.read() { "step-primary" }, + "Naar" + } + li { + class: "step", + class: if let Steps::Verify | Steps::Done = *step.read() { "step-primary" }, + "Controleren" + } + li { + class: "step", + class: if let Steps::Done = *step.read() { "step-primary" }, + "Klaar" + } + } + div { + class: "flex flex-col gap-y-5", + div { + class: if let Steps::Message = *step.read() { "" } else { "hidden" }, + content::Content { + step: step, + title: form.title, + body: form.body, + } + } + div { + class: if let Steps::Targets = *step.read() { "" } else { "hidden" }, + targets::Targets { + step: step, + targets: form.targets, + onsubmit: submit_proposal, + } + } + } + } + } +} + +#[server] +async fn message_proposal( + title: String, + body: String, + targets: Vec, +) -> Result { + let user = Session::fetch_current_user().await?; + + if !user.admin { + return Err(crate::Error::NoPermissions.into()); + } + + let amount = Message::create_proposal(title, body, targets).await?; + + Ok(amount) +} diff --git a/src/components/messages/create/content.rs b/src/components/messages/create/content.rs new file mode 100644 index 0000000..5fe0f0c --- /dev/null +++ b/src/components/messages/create/content.rs @@ -0,0 +1,51 @@ +use super::Steps; +use dioxus::prelude::*; + +#[component] +pub fn Content(step: Signal, title: Signal, body: Signal) -> Element { + let submit = move |event: FormEvent| { + title.set(event.values()["title"].as_value()); + body.set(event.values()["body"].as_value()); + step.set(Steps::Targets); + }; + + rsx! { + form { + class: "flex flex-col w-full gap-y-5", + onsubmit: submit, + label { + class: "form-control w-full", + div { + class: "label", + span { class: "label-text", "Titel" } + }, + input { + r#type: "text", + required: true, + name: "title", + class: "input input-bordered w-full", + } + } + label { + class: "form-control w-full", + div { + class: "label", + span { class: "label-text", "Bericht" } + }, + textarea { + name: "body", + class: "textarea textarea-bordered w-full", + required: true, + } + }, + div { + class: "w-full flex gap-x-3 justify-end", + input { + r#type: "submit", + class: "btn btn-primary", + value: "Volgende", + } + } + } + } +} diff --git a/src/components/news/create.rs b/src/components/messages/create/targets.rs similarity index 66% rename from src/components/news/create.rs rename to src/components/messages/create/targets.rs index 7166c2e..f8197d8 100644 --- a/src/components/news/create.rs +++ b/src/components/messages/create/targets.rs @@ -1,142 +1,15 @@ -use crate::util::model::news::{Target, TargetKind}; -use std::collections::HashMap; - use dioxus::prelude::*; -#[derive(PartialEq)] -enum Steps { - Message, - Targets, - Verify, - Done, -} +use super::Steps; +use crate::util::model::message::{Message, Target, TargetKind}; +use std::collections::HashMap; -#[derive(Props, Clone, PartialEq, Debug)] -struct Form { - title: Signal, - body: Signal, +#[component] +pub fn Targets( + step: Signal, targets: Signal>, -} - -impl Default for Form { - fn default() -> Self { - Self { - title: use_signal(|| String::new()), - body: use_signal(|| String::new()), - targets: use_signal(|| { - HashMap::from([( - 0, - Target { - kind: TargetKind::None, - value: String::new(), - }, - )]) - }), - } - } -} - -#[component] -pub fn NewsCreate() -> Element { - let step = use_signal(|| Steps::Message); - let form = Form::default(); - - rsx! { - div { - class: "w-full max-w-2xl space-y-3 flex flex-col", - h1 { class: "text-xl font-bold text-primary", "Nieuw bericht" } - ul { - class: "steps pb-10 mx-auto", - li { class: "step step-primary", "Inhoud" }, - li { - class: "step", - class: if let Steps::Targets | Steps::Done | Steps::Verify = *step.read() { "step-primary" }, - "Naar" - } - li { - class: "step", - class: if let Steps::Verify | Steps::Done = *step.read() { "step-primary" }, - "Controleren" - } - li { - class: "step", - class: if let Steps::Done = *step.read() { "step-primary" }, - "Klaar" - } - } - div { - class: "flex flex-col gap-y-5", - div { - class: if let Steps::Message = *step.read() { "" } else { "hidden" }, - Message { - step: step, - title: form.title, - body: form.body, - } - } - div { - class: if let Steps::Targets = *step.read() { "" } else { "hidden" }, - TargetSelect { - step: step, - targets: form.targets, - } - } - } - } - } -} - -#[component] -fn Message(step: Signal, title: Signal, body: Signal) -> Element { - let submit = move |event: FormEvent| { - title.set(event.values()["title"].as_value()); - body.set(event.values()["body"].as_value()); - step.set(Steps::Targets); - }; - - rsx! { - form { - class: "flex flex-col w-full gap-y-5", - onsubmit: submit, - label { - class: "form-control w-full", - div { - class: "label", - span { class: "label-text", "Titel" } - }, - input { - r#type: "text", - required: true, - name: "title", - class: "input input-bordered w-full", - } - } - label { - class: "form-control w-full", - div { - class: "label", - span { class: "label-text", "Bericht" } - }, - textarea { - name: "body", - class: "textarea textarea-bordered w-full", - required: true, - } - }, - div { - class: "w-full flex gap-x-3 justify-end", - input { - r#type: "submit", - class: "btn btn-primary", - value: "Volgende", - } - } - } - } -} - -#[component] -fn TargetSelect(step: Signal, targets: Signal>) -> Element { + onsubmit: EventHandler, +) -> Element { let mut target_id = use_signal(|| 1); let target_ids = use_memo(move || { @@ -147,14 +20,10 @@ fn TargetSelect(step: Signal, targets: Signal>) -> E filtered_targets }); - let submit = move |_| { - step.set(Steps::Verify); - }; - rsx! { form { class: "w-full", - onsubmit: submit, + onsubmit: move |event| onsubmit.call(event), label { class: "form-control w-full", div { diff --git a/src/main.rs b/src/main.rs index 7b33d2f..1a3cd05 100644 --- a/src/main.rs +++ b/src/main.rs @@ -18,8 +18,8 @@ use components::agenda::Agenda; use components::home::Home; use components::layout::AdminLayout; use components::layout::Global; -use components::news::create::NewsCreate; -use components::news::News; +use components::messages::create::MessagesCreatePage; +use components::messages::MessagesPage; use components::settings::Settings; use tracing::Level; @@ -31,10 +31,10 @@ pub enum Route { Home {}, #[route("/agenda")] Agenda {}, - #[route("/news")] - News {}, - #[route("/news/create")] - NewsCreate {}, + #[route("/messages")] + MessagesPage {}, + #[route("/messages/create")] + MessagesCreatePage {}, #[route("/settings")] Settings {}, diff --git a/src/util/model.rs b/src/util/model.rs index df6c885..eab21ab 100644 --- a/src/util/model.rs +++ b/src/util/model.rs @@ -1,4 +1,4 @@ pub mod member; -pub mod news; +pub mod message; pub mod session; pub mod user; diff --git a/src/util/model/message.rs b/src/util/model/message.rs new file mode 100644 index 0000000..3e4691a --- /dev/null +++ b/src/util/model/message.rs @@ -0,0 +1,147 @@ +use serde::{Deserialize, Serialize}; + +#[cfg(feature = "server")] +use crate::util::surrealdb::{thing_to_string, DB}; +#[cfg(feature = "server")] +use surrealdb::sql::statements::{BeginStatement, CommitStatement}; + +#[derive(Debug, Serialize, Deserialize)] +pub struct PushSubscription { + endpoint: String, + keys_256dh: String, + keys_auth: String, + encodings: String, +} + +#[derive(PartialEq, Clone, Copy, Debug, Deserialize, Serialize, Eq)] +pub enum TargetKind { + None, + All, + Group, + Hourgroup, + Hour, + Member, + Account, + Day, +} + +impl TargetKind { + pub fn from_string(input: &str) -> Self { + match input { + "all" => Self::All, + "group" => Self::Group, + "hourgroup" => Self::Hourgroup, + "hour" => Self::Hour, + "member" => Self::Member, + "account" => Self::Account, + "day" => Self::Day, + _ => Self::None, + } + } +} + +#[derive(PartialEq, Debug, Deserialize, Serialize, Clone, Eq)] +pub struct Target { + pub kind: TargetKind, + pub value: String, +} + +#[derive(Debug, Deserialize, Serialize, PartialEq, Eq, Clone)] +pub struct Message { + #[cfg_attr(feature = "server", serde(deserialize_with = "thing_to_string"))] + pub id: String, + pub title: String, + pub body: String, + pub tagets: Vec, +} + +#[cfg(feature = "server")] +impl Message { + pub async fn create_proposal( + title: String, + body: String, + targets: Vec, + ) -> Result { + let member_ids = fetch_members(targets).await?; + + Ok(member_ids.len()) + } + + pub async fn insert( + title: String, + body: String, + channel: String, + member_ids: Vec, + ) -> Result<(), crate::Error> { + let mut transaction = DB + .query(BeginStatement::default()) + .query("let $message = CREATE ONLY message SET title = $title, body = $body, channel = $channel RETURN id;") + .bind(("title", title)) + .bind(("body", body)) + .bind(("channel", channel)); + + for member_id in member_ids { + transaction = transaction.query(format!("RELATE ONLY (type::thing('member', $member_id_{}))->member_to_message->(type::thing('message', $message.id));", member_id)) + .bind((format!("member_id_{}", member_id), member_id)); + } + + let res = transaction.query(CommitStatement::default()).await?; + + res.check()?; + + Ok(()) + } +} + +/// Retrieve a vec of all the members that will recieve the message +#[cfg(feature = "server")] +async fn fetch_members(targets: Vec) -> Result, surrealdb::Error> { + let mut transaction = DB.query(BeginStatement::default()); + + for target in targets { + match target.kind { + TargetKind::All => { + transaction = transaction.query("SELECT VALUE meta::id(id) FROM member;"); + + break; + } + _ => {} + }; + } + + let mut res = transaction.query(CommitStatement::default()).await?; + + res = res.check()?; + + let members: Vec = res.take(0)?; + + Ok(members) +} + +#[cfg(feature = "server")] +async fn fetch_pushsubscriptions( + targets: Vec, +) -> Result, surrealdb::Error> { + let mut transaction = DB.query(BeginStatement::default()); + + for target in targets { + match target.kind { + TargetKind::All => { + transaction = transaction + .query("RETURN array::group(SELECT VALUE pushsubscriptions FROM user);"); + + break; + } + _ => {} + }; + } + + let mut res = transaction + .query(CommitStatement::default()) + .await? + .check()?; + + let pushsubscriptions: Vec = res.take(0)?; + + Ok(pushsubscriptions) +} diff --git a/src/util/model/news.rs b/src/util/model/news.rs deleted file mode 100644 index 2960520..0000000 --- a/src/util/model/news.rs +++ /dev/null @@ -1,68 +0,0 @@ -use serde::{Deserialize, Serialize}; - -#[cfg(feature = "server")] -use crate::util::surrealdb::{thing_to_string, DB}; -#[cfg(feature = "server")] -use surrealdb::sql::{ - statements::{BeginStatement, CommitStatement}, - Thing, -}; - -#[derive(PartialEq, Clone, Copy, Debug, Deserialize, Serialize, Eq)] -pub enum TargetKind { - None, - All, - Group, - Hourgroup, - Hour, - Member, - Account, - Day, -} - -impl TargetKind { - pub fn from_string(input: &str) -> Self { - match input { - "all" => Self::All, - "group" => Self::Group, - "hourgroup" => Self::Hourgroup, - "hour" => Self::Hour, - "member" => Self::Member, - "account" => Self::Account, - "day" => Self::Day, - _ => Self::None, - } - } -} - -#[derive(PartialEq, Debug, Deserialize, Serialize, Clone, Eq)] -pub struct Target { - pub kind: TargetKind, - pub value: String, -} - -#[derive(Debug, Deserialize, Serialize, PartialEq, Eq, Clone)] -pub struct News { - #[cfg_attr(feature = "server", serde(deserialize_with = "thing_to_string"))] - pub id: String, - pub title: String, - pub body: String, - pub tagets: Vec, -} - -#[cfg(feature = "server")] -impl News { - // pub async fn new( - // title: String, - // body: String, - // targets: Vec, - // ) -> Result { - // } -} - -#[cfg(feature = "server")] -fn fetch_targets(targets: Vec) { - let mut transaction = DB.query(BeginStatement::default()); - - for target in targets {} -} diff --git a/src/util/model/user.rs b/src/util/model/user.rs index d38d762..bc96abd 100644 --- a/src/util/model/user.rs +++ b/src/util/model/user.rs @@ -8,6 +8,8 @@ use surrealdb::sql::{ Thing, }; +use super::message::PushSubscription; + #[derive(Debug, Deserialize, Serialize, PartialEq, Eq, Clone)] pub struct User { #[cfg_attr(feature = "server", serde(deserialize_with = "thing_to_string"))] @@ -174,4 +176,19 @@ impl User { Ok(()) } + + pub async fn insert_pushsubscription( + &self, + pushsubscription: PushSubscription, + ) -> Result<(), crate::Error> { + let res = DB + .query("UPDATE $user SET pushsubscriptions += $pushsubscription;") + .bind(("user", Thing::from(("user", self.id.as_str())))) + .bind(("pushsubscription", pushsubscription)) + .await?; + + res.check()?; + + Ok(()) + } } diff --git a/src/util/surrealdb.rs b/src/util/surrealdb.rs index af9aecb..dcc1c0f 100644 --- a/src/util/surrealdb.rs +++ b/src/util/surrealdb.rs @@ -47,6 +47,12 @@ async fn apply_queries() -> surrealdb::Result<()> { DEFINE FIELD password ON TABLE user TYPE string; DEFINE FIELD admin ON TABLE user TYPE bool DEFAULT false; + DEFINE FIELD pushsubscriptions ON TABLE user TYPE array + DEFAULT []; + DEFINE FIELD pushsubscriptions.*.endpoint ON TABLE user TYPE string; + DEFINE FIELD pushsubscriptions.*.keys_256dh ON TABLE user TYPE string; + DEFINE FIELD pushsubscriptions.*.keys_auth ON TABLE user TYPE string; + DEFINE FIELD pushsubscriptions.*.encodings ON TABLE user TYPE string; DEFINE INDEX userEmailIndex ON TABLE user COLUMNS email UNIQUE; ", @@ -94,5 +100,19 @@ async fn apply_queries() -> surrealdb::Result<()> { ) .await?; + // Define messages table + DB.query( + " + DEFINE TABLE OVERWRITE message SCHEMAFULL; + + DEFINE FIELD title ON TABLE message TYPE string; + DEFINE FIELD body ON TABLE message TYPE string; + DEFINE FIELD channel ON TABLE message TYPE string; + + DEFINE TABLE OVERWRITE member_to_message SCHEMAFULL TYPE RELATION FROM member TO message ENFORCED; + ", + ) + .await?; + Ok(()) }