This commit is contained in:
Stefan Schwarz 2020-08-20 20:39:53 +02:00
parent 288f193754
commit fc3a6fe2a0
8 changed files with 372 additions and 50 deletions

95
Cargo.lock generated
View file

@ -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",

View file

@ -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"

101
src/db.rs
View file

@ -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, <sqlx::MySql as HasArgument
#[derive(sqlx::FromRow, Debug)]
pub struct Device {
pub id: i32,
pub id: Option<i32>,
pub macaddr: String,
pub nickname: String,
pub descr: String,
pub privacy: PrivacyLevel,
pub created: DateTime<Utc>,
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<Query<'q>> {
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<Query<'q>> {
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<i8> for PrivacyLevel {
type Error = &'static str;
fn try_from(i: i8) -> Result<Self, Self::Error> {
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,

78
src/forms.rs Normal file
View file

@ -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()),
}
}
}

View file

@ -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<Message>,
}
impl AppSession {
pub fn add_message(mut self, message: Message) -> Self {
self.messages.push(message);
self
}
pub fn pop_messages(&mut self) -> Vec<Message> {
let mut messages: Vec<Message> = 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<State>;
#[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
}

View file

@ -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())
}

View file

@ -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<db::Device>,
unassinged: Vec<db::AliveDevice>,
messages: Vec<Message>,
}
impl IndexTemplate {
pub fn new(my: Vec<db::Device>, unassinged: Vec<db::AliveDevice>) -> Self {
Self { my, unassinged }
pub fn new(
my: Vec<db::Device>,
unassinged: Vec<db::AliveDevice>,
messages: Vec<Message>,
) -> Self {
Self {
my,
unassinged,
messages,
}
}
}

View file

@ -15,16 +15,27 @@
<h1 class="text-xl underline text-center p-2">
macnickenson
</h1>
<div>
{% for message in messages %}
{{ message.1 }}
{% endfor %}
</div>
<div class="p-2">
<h2 class="underline">Your Devices:</h2>
{% for device in my %}
<form action="/update" method="POST">
<div class="grid grid-cols-3 grid-gap-1">
<div class="font-mono">{{ device.macaddr }} {{ device.present }}</div>
<div class="font-mono">
{{ device.macaddr }} {{ device.present }}
<input type="hidden" name="macaddr" value="{{ device.macaddr }}" />
</div>
<div>
<input class="focus:shadow-outline border border-gray-300 px-2"
value="{{ device.descr }}"/>
<select class="focus:shadow-outline border border-gray-300 px-2">
name="descr"
value="{{ device.descr }}" />
<select class="focus:shadow-outline border border-gray-300 px-2"
name="privacy">
<option value="0"
{{ device.privacy.selected(crate::db::PrivacyLevel::ShowUserAndDevice) }}
>Show User and Device</option>
@ -47,30 +58,45 @@
<button class="rounded bg-gray-300 font-bold px-2 bg-red-500">Delete</button>
</div>
</div>
</form>
{% endfor %}
</div>
<div class="p-2">
<h2 class="underline">Unregistred Devices:</h2>
{% for device in unassinged %}
<form action="/register" method="POST">
<div class="grid grid-cols-4 grid-gap-1 p-2">
<div class="font-mono">{{ device.macaddr }}</div>
<div class="font-mono">
{{ device.macaddr }}
<input type="hidden" name="macaddr" value="{{ device.macaddr }}" />
</div>
<div class="font-mono">{{ device.ip() }}</div>
<div>
<input class="focus:shadow-outline border border-gray-300 px-2"
placeholder="awesome new device"/>
placeholder="awesome new device"
name="descr" />
<select class="focus:shadow-outline border border-gray-300 px-2"
name="privacy">
<option value="0">Show User and Device</option>
<option value="1">Show User</option>
<option value="2" selected>Show Anonymous</option>
<option value="3">Hide User</option>
<option value="4">Dont Log</option>
</select>
</div>
<div>
<button class="rounded bg-gray-300 font-bold px-2 bg-blue-300">Register</button>
<button class="rounded bg-gray-300 font-bold px-2 bg-blue-300"
type="submit">
Register
</button>
</div>
</div>
</form>
{% endfor %}
</div>
<div class="shadow-lg p-2">
footer
</div>
</div>
</body>
</html>