From fc3a6fe2a02df135b539dc544c263abb6ba491da Mon Sep 17 00:00:00 2001 From: Stefan Schwarz Date: Thu, 20 Aug 2020 20:39:53 +0200 Subject: [PATCH] messages --- Cargo.lock | 95 ++++++++++++++++++++++++++++++++++++++-- Cargo.toml | 2 + src/db.rs | 101 +++++++++++++++++++++++++++++++++---------- src/forms.rs | 78 +++++++++++++++++++++++++++++++++ src/main.rs | 52 +++++++++++++++++++++- src/routes.rs | 36 ++++++++++----- src/templates.rs | 14 +++++- templates/index.html | 44 +++++++++++++++---- 8 files changed, 372 insertions(+), 50 deletions(-) create mode 100644 src/forms.rs diff --git a/Cargo.lock b/Cargo.lock index 76de12e..e59f99a 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -254,6 +254,17 @@ dependencies = [ "sha2", ] +[[package]] +name = "async-sqlx-session" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bf95404b3deed3c55b22ec8dffe85e94ade9166e65c923270ddfdfe637a63b9f" +dependencies = [ + "async-session", + "async-std", + "sqlx 0.3.5", +] + [[package]] name = "async-sse" version = "4.0.0" @@ -293,6 +304,27 @@ dependencies = [ "wasm-bindgen-futures", ] +[[package]] +name = "async-stream" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "22068c0c19514942eefcfd4daf8976ef1aad84e61539f95cd200c35202f80af5" +dependencies = [ + "async-stream-impl", + "futures-core", +] + +[[package]] +name = "async-stream-impl" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "25f9db3b38af870bf7e5cc649167533b493928e50744e2c30ae350230b414670" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + [[package]] name = "async-task" version = "3.0.0" @@ -1058,10 +1090,12 @@ dependencies = [ "argh", "askama", "askama_tide", + "async-sqlx-session", "async-std", "chrono", + "http-types", "serde", - "sqlx", + "sqlx 0.4.0-beta.1", "tide", ] @@ -1735,14 +1769,50 @@ dependencies = [ "regex", ] +[[package]] +name = "sqlx" +version = "0.3.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8974cacd80085fbe49e778708d660dec6fb351604dc34c3905b26efb2803b038" +dependencies = [ + "sqlx-core 0.3.5", + "sqlx-macros 0.3.5", +] + [[package]] name = "sqlx" version = "0.4.0-beta.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8cb7b012f28c74075d6b11172ba1874f4376a255509462eaf2ef25068b31729f" dependencies = [ - "sqlx-core", - "sqlx-macros", + "sqlx-core 0.4.0-beta.1", + "sqlx-macros 0.4.0-beta.1", +] + +[[package]] +name = "sqlx-core" +version = "0.3.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "88ac5a436f941c42eac509471a730df5c3c58e1450e68cd39afedbd948206273" +dependencies = [ + "async-native-tls", + "async-std", + "async-stream", + "bitflags", + "byteorder", + "chrono", + "crossbeam-queue", + "crossbeam-utils", + "futures-channel", + "futures-core", + "futures-util", + "hex", + "libc", + "log", + "memchr", + "percent-encoding", + "sqlformat", + "url", ] [[package]] @@ -1791,6 +1861,23 @@ dependencies = [ "whoami", ] +[[package]] +name = "sqlx-macros" +version = "0.3.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "de2ae78b783af5922d811b14665a5a3755e531c3087bb805cf24cf71f15e6780" +dependencies = [ + "async-std", + "dotenv", + "futures", + "heck", + "proc-macro2", + "quote", + "sqlx-core 0.3.5", + "syn", + "url", +] + [[package]] name = "sqlx-macros" version = "0.4.0-beta.1" @@ -1804,7 +1891,7 @@ dependencies = [ "proc-macro2", "quote", "sha2", - "sqlx-core", + "sqlx-core 0.4.0-beta.1", "sqlx-rt", "syn", "url", diff --git a/Cargo.toml b/Cargo.toml index 88ccfbb..07f8152 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -9,8 +9,10 @@ anyhow = "1.0" argh = "0.1" askama_tide = "0.10" askama = { version = "0.10", features = ["with-tide"]} +async-sqlx-session = "0.2" async-std = { version = "1.6", features = ["attributes"] } chrono = "0.4" +http-types = "2.4" serde = "1.0" sqlx = { version = "0.4.0-beta.1", features = ["mysql", "chrono", "macros"] } tide = "0.13" diff --git a/src/db.rs b/src/db.rs index 98c13a3..1a2e161 100644 --- a/src/db.rs +++ b/src/db.rs @@ -1,5 +1,6 @@ -use chrono::{DateTime, Utc}; +use anyhow::{anyhow, Result}; use sqlx::database::HasArguments; +use std::convert::TryFrom; use std::net::IpAddr; type QueryAs<'q, T> = @@ -8,28 +9,32 @@ type Query<'q> = sqlx::query::Query<'q, sqlx::MySql, , pub macaddr: String, pub nickname: String, pub descr: String, pub privacy: PrivacyLevel, - pub created: DateTime, pub present: bool, } -#[derive(sqlx::Type, Debug, Clone, Copy)] -#[repr(i8)] -pub enum PrivacyLevel { - ShowUserAndDevice = 0, - ShowUser = 1, - ShowAnonymous = 2, - HideUser = 3, - DontLog = 4, -} - impl<'q> Device { - pub fn all() -> QueryAs<'q, Self> { - sqlx::query_as("SELECT * FROM mac_to_nick") + pub fn create(&'q self) -> Result> { + if let Some(_) = self.id { + return Err(anyhow!("device has already been created")); + } + Ok(sqlx::query( + " +INSERT +INTO mac_to_nick +(macaddr, nickname, descr, privacy, created) +VALUES +(?, ?, ?, ?, NOW()) +", + ) + .bind(&self.macaddr) + .bind(&self.nickname) + .bind(&self.descr) + .bind(self.privacy)) } pub fn for_user(user: &'q str) -> QueryAs<'q, Self> { @@ -54,18 +59,52 @@ ORDER BY .bind(user) } - pub fn register(mac: &'q str, user: &'q str) -> Query<'q> { - sqlx::query( + pub fn for_mac(macaddr: &'q str) -> QueryAs<'q, Self> { + dbg!(&macaddr); + sqlx::query_as( " -INSERT -INTO mac_to_nick -(macaddr, nickname, descr, privacy, created) -VALUES -(?, ?, ?, ?, NOW()) +SELECT DISTINCT + *, + FALSE present +FROM + mac_to_nick +WHERE + macaddr = ? ", ) - .bind(mac) + .bind(macaddr) } + + pub fn update(&'q self) -> Result> { + let id = match self.id { + Some(id) => id, + None => return Err(anyhow!("selected device has no id")), + }; + Ok(sqlx::query( + " +UPDATE + mac_to_nick +SET + privacy = ?, + descr = ? +WHERE + id = ? +", + ) + .bind(self.privacy as u8) + .bind(&self.descr) + .bind(id)) + } +} + +#[derive(sqlx::Type, Debug, Clone, Copy)] +#[repr(i8)] +pub enum PrivacyLevel { + ShowUserAndDevice = 0, + ShowUser = 1, + ShowAnonymous = 2, + HideUser = 3, + DontLog = 4, } impl PrivacyLevel { @@ -82,6 +121,22 @@ impl PrivacyLevel { } } +impl TryFrom for PrivacyLevel { + type Error = &'static str; + + fn try_from(i: i8) -> Result { + let level = match i { + 0 => PrivacyLevel::ShowUserAndDevice, + 1 => PrivacyLevel::ShowUser, + 2 => PrivacyLevel::ShowAnonymous, + 3 => PrivacyLevel::HideUser, + 4 => PrivacyLevel::DontLog, + _ => return Err("invalid privacy level"), + }; + Ok(level) + } +} + #[derive(sqlx::FromRow, Debug)] pub struct AliveDevice { pub macaddr: String, diff --git a/src/forms.rs b/src/forms.rs new file mode 100644 index 0000000..59f793f --- /dev/null +++ b/src/forms.rs @@ -0,0 +1,78 @@ +use crate::db; +use crate::Level; +use crate::Message; +use crate::USER; +use serde::Deserialize; +use std::convert::TryFrom; + +#[derive(Deserialize)] +pub struct RegisterForm { + macaddr: String, + descr: String, + privacy: i8, +} + +impl RegisterForm { + pub async fn handle(self, request: &crate::Request) -> Message { + let privacy = match db::PrivacyLevel::try_from(self.privacy) { + Ok(privacy) => privacy, + Err(_) => return (Level::Error, "unable to parse privacy level".to_string()), + }; + + let dbresult = db::Device { + id: None, + macaddr: self.macaddr, + nickname: USER.to_string(), + descr: self.descr, + privacy, + present: false, + }; + let dbresult = dbresult + .create() + .unwrap() + .execute(&request.state().pool) + .await; + return match dbresult { + Ok(_) => (Level::Info, "assinged device".to_string()), + Err(_) => (Level::Error, "unable to create device".to_string()), + }; + } +} + +#[derive(Deserialize)] +pub struct UpdateForm { + macaddr: String, + descr: String, + privacy: i8, +} + +impl UpdateForm { + pub async fn handle(self, request: &crate::Request) -> Message { + let mut device = match db::Device::for_mac(&self.macaddr) + .fetch_one(&request.state().pool) + .await + { + Ok(device) => device, + Err(_) => { + return ( + Level::Error, + "unable to load device from database".to_string(), + ) + } + }; + device.privacy = match db::PrivacyLevel::try_from(self.privacy) { + Ok(privacy) => privacy, + Err(_) => return (Level::Error, "unable to parse privacy level".to_string()), + }; + device.descr = self.descr; + match device + .update() + .unwrap() + .execute(&request.state().pool) + .await + { + Ok(_) => (Level::Info, "updated device".to_string()), + Err(_) => (Level::Error, "unable to update device".to_string()), + } + } +} diff --git a/src/main.rs b/src/main.rs index 25a48d6..d8d5656 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,10 +1,15 @@ use argh::FromArgs; +use serde::{Deserialize, Serialize}; use sqlx::MySqlPool; use std::io; +use tide::sessions::{MemoryStore, SessionMiddleware}; mod db; +mod forms; mod routes; mod templates; +pub const USER: &str = "foosinn"; + /// Configuration #[derive(FromArgs, Debug)] struct Config { @@ -18,6 +23,10 @@ struct Config { default = "\"mysql://administration:foosinn123@127.0.0.1/administration\".to_string()" )] dsn: String, + + /// session secret + #[argh(option, default = "\"thisisnotasecretthisisnotasecret\".into()")] + session_secret: String, } #[derive(Clone)] @@ -25,6 +34,43 @@ pub struct State { pool: MySqlPool, } +#[derive(Debug, Deserialize, Serialize)] +pub enum Level { + Info, + Warn, + Error, +} + +pub type Message = (Level, String); + +#[derive(Default, Deserialize, Serialize)] +pub struct AppSession { + messages: Vec, +} + +impl AppSession { + pub fn add_message(mut self, message: Message) -> Self { + self.messages.push(message); + self + } + + pub fn pop_messages(&mut self) -> Vec { + let mut messages: Vec = Vec::new(); + std::mem::swap(&mut messages, &mut self.messages); + messages + } + + pub fn commit(self, request: &mut Request) { + request.session_mut().insert("app", self).unwrap() + } +} + +impl From<&mut Request> for AppSession { + fn from(request: &mut Request) -> Self { + request.session().get("app").unwrap_or_default() + } +} + pub type Request = tide::Request; #[async_std::main] @@ -35,11 +81,15 @@ async fn main() -> Result<(), io::Error> { .await .map_err(|err| io::Error::new(io::ErrorKind::Other, format!("{:?}", err)))?; + let session_store = + SessionMiddleware::new(MemoryStore::new(), config.session_secret.as_bytes()); + let mut app = tide::with_state(State { pool }); + app.with(session_store); app.at("/").get(routes::index); app.at("/register").post(routes::register); + app.at("/update").post(routes::update); app.at("/healthz").get(routes::healthz); app.at("/static").serve_dir("static/")?; - app.listen(config.listen).await } diff --git a/src/routes.rs b/src/routes.rs index c437883..9dac139 100644 --- a/src/routes.rs +++ b/src/routes.rs @@ -1,14 +1,15 @@ use crate::db; +use crate::forms; use crate::templates; -use tide::prelude::*; - -const USER: &str = "foosinn"; +use crate::AppSession; +use crate::USER; +use tide::Redirect; pub async fn healthz(_request: crate::Request) -> tide::Result { Ok("ok".into()) } -pub async fn index(request: crate::Request) -> tide::Result { +pub async fn index(mut request: crate::Request) -> tide::Result { let my = db::Device::for_user(USER) .fetch_all(&request.state().pool) .await @@ -17,15 +18,28 @@ pub async fn index(request: crate::Request) -> tide::Result { .fetch_all(&request.state().pool) .await .map_err(|err| dbg!(err))?; - Ok(templates::IndexTemplate::new(my, unassinged).into()) -} -#[derive(Deserialize)] -struct RegisterForm { - macaddr: String, + let mut session = AppSession::from(&mut request); + let messages = session.pop_messages(); + session.commit(&mut request); + + Ok(templates::IndexTemplate::new(my, unassinged, messages).into()) } pub async fn register(mut request: crate::Request) -> tide::Result { - let form: RegisterForm = request.body_form().await?; - unimplemented!(); + let form: forms::RegisterForm = request.body_form().await?; + let message = form.handle(&request).await; + AppSession::from(&mut request) + .add_message(message) + .commit(&mut request); + Ok(Redirect::see_other("/").into()) +} + +pub async fn update(mut request: crate::Request) -> tide::Result { + let form: forms::UpdateForm = request.body_form().await?; + let message = form.handle(&request).await; + AppSession::from(&mut request) + .add_message(message) + .commit(&mut request); + Ok(Redirect::see_other("/").into()) } diff --git a/src/templates.rs b/src/templates.rs index eaca270..81a23ce 100644 --- a/src/templates.rs +++ b/src/templates.rs @@ -1,4 +1,5 @@ use crate::db; +use crate::Message; use askama::Template; #[derive(Template, Default)] @@ -6,10 +7,19 @@ use askama::Template; pub struct IndexTemplate { my: Vec, unassinged: Vec, + messages: Vec, } impl IndexTemplate { - pub fn new(my: Vec, unassinged: Vec) -> Self { - Self { my, unassinged } + pub fn new( + my: Vec, + unassinged: Vec, + messages: Vec, + ) -> Self { + Self { + my, + unassinged, + messages, + } } } diff --git a/templates/index.html b/templates/index.html index 482de38..c2dd093 100644 --- a/templates/index.html +++ b/templates/index.html @@ -15,16 +15,27 @@

macnickenson

+
+ {% for message in messages %} + {{ message.1 }} + {% endfor %} +

Your Devices:

{% for device in my %} +
-
{{ device.macaddr }} {{ device.present }}
+
+ {{ device.macaddr }} {{ device.present }} + +
- @@ -47,30 +58,45 @@
+
{% endfor %}

Unregistred Devices:

{% for device in unassinged %} +
-
{{ device.macaddr }}
+
+ {{ device.macaddr }} + +
{{ device.ip() }}
+ placeholder="awesome new device" + name="descr" /> +
- +
+
{% endfor %}
footer
- - -