From 1c5c9caf2acd62915a21ae7b52ba44d369e2fe23 Mon Sep 17 00:00:00 2001 From: emilis Date: Mon, 12 Sep 2022 16:36:14 +0100 Subject: [PATCH] vaguely added login/logout? gunna try killing warp --- Cargo.lock | 221 ++++++++++++++++++++++++++++++- Cargo.toml | 3 + migrate/1662887862_schema.up.sql | 10 +- src/database/db.rs | 10 +- src/database/keys.rs | 39 ++++++ src/database/mod.rs | 1 + src/database/users.rs | 23 ++-- src/helpers.rs | 20 +++ src/main.rs | 6 +- src/sec/mod.rs | 26 ++++ src/servek/html.rs | 124 +++++++++++++---- src/servek/mod.rs | 17 +++ src/servek/servek.rs | 37 ++++-- src/svc/auth.rs | 92 +++++++++++++ src/svc/mod.rs | 1 + src/svc/profiles.rs | 48 +++---- static/style/login.css | 1 + static/style/main.css | 14 ++ templates/html/404.html | 5 +- templates/html/index.html | 1 + templates/html/login-error.html | 40 ++++++ templates/html/login.html | 39 ++++++ 22 files changed, 694 insertions(+), 84 deletions(-) create mode 100644 src/database/keys.rs create mode 100644 src/helpers.rs create mode 100644 src/sec/mod.rs create mode 100644 src/svc/auth.rs create mode 100644 static/style/login.css create mode 100644 templates/html/login-error.html create mode 100644 templates/html/login.html diff --git a/Cargo.lock b/Cargo.lock index c7707de..2ea31f6 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -23,6 +23,17 @@ version = "1.0.64" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b9a8f622bcf6ff3df478e9deba3e03e4e04b300f8e6a139e192c05fa3490afc7" +[[package]] +name = "argon2" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "db4ce4441f99dbd377ca8a8f57b698c44d0d6e712d8329b5040da5a64aa1ce73" +dependencies = [ + "base64ct", + "blake2", + "password-hash", +] + [[package]] name = "async-trait" version = "0.1.57" @@ -62,7 +73,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f28ebd71b3e708e895b83ec2d35c6e2ef96e34945706bf4d73826354e84f89b2" dependencies = [ "failure", - "num-bigint", + "num-bigint 0.2.6", "num-integer", "num-traits", ] @@ -73,12 +84,27 @@ version = "0.13.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "904dfeac50f3cdaba28fc6f57fdcddb75f49ed61346676a78c4ffe55877802fd" +[[package]] +name = "base64ct" +version = "1.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ea2b2456fd614d856680dcd9fcc660a51a820fa09daef2e49772b56a193c8474" + [[package]] name = "bitflags" version = "1.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" +[[package]] +name = "blake2" +version = "0.10.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b9cf849ee05b2ee5fba5e36f97ff8ec2533916700fc0758d40d92136a42f3388" +dependencies = [ + "digest 0.10.3", +] + [[package]] name = "block-buffer" version = "0.9.0" @@ -107,6 +133,12 @@ dependencies = [ "safemem", ] +[[package]] +name = "bumpalo" +version = "3.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c1ad822118d20d2c234f427000d5acc36eabe1e29a348c89b63dd60b13f28e5d" + [[package]] name = "byteorder" version = "1.4.3" @@ -212,10 +244,13 @@ name = "flabk" version = "0.0.1" dependencies = [ "anyhow", + "argon2", "base-62", "handlebars", + "jsonwebtoken", "mime_guess", "rand", + "rand_core", "rust-embed", "serde", "serde_json", @@ -496,6 +531,29 @@ version = "1.0.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6c8af84674fe1f223a982c933a0ee1086ac4d4052aa0fb8060c12c6ad838e754" +[[package]] +name = "js-sys" +version = "0.3.59" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "258451ab10b34f8af53416d1fdab72c22e805f0c92a1136d59470ec0b11138b2" +dependencies = [ + "wasm-bindgen", +] + +[[package]] +name = "jsonwebtoken" +version = "8.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1aa4b4af834c6cfd35d8763d359661b90f2e45d8f750a0849156c7f4671af09c" +dependencies = [ + "base64", + "pem", + "ring", + "serde", + "serde_json", + "simple_asn1", +] + [[package]] name = "libc" version = "0.2.132" @@ -602,6 +660,17 @@ dependencies = [ "num-traits", ] +[[package]] +name = "num-bigint" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f93ab6289c7b344a8a9f60f88d80aa20032336fe78da341afc91c8a2341fc75f" +dependencies = [ + "autocfg", + "num-integer", + "num-traits", +] + [[package]] name = "num-integer" version = "0.1.45" @@ -631,6 +700,15 @@ dependencies = [ "libc", ] +[[package]] +name = "num_threads" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2819ce041d2ee131036f4fc9d6ae7ae125a3a40e97ba64d04fe799ad9dabbb44" +dependencies = [ + "libc", +] + [[package]] name = "object" version = "0.29.0" @@ -675,6 +753,26 @@ dependencies = [ "windows-sys", ] +[[package]] +name = "password-hash" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7676374caaee8a325c9e7a2ae557f216c5563a171d6997b0ef8a65af35147700" +dependencies = [ + "base64ct", + "rand_core", + "subtle", +] + +[[package]] +name = "pem" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "03c64931a1a212348ec4f3b4362585eca7159d0d09cbdf4a7f74f02173596fd4" +dependencies = [ + "base64", +] + [[package]] name = "percent-encoding" version = "2.2.0" @@ -884,6 +982,21 @@ dependencies = [ "winapi", ] +[[package]] +name = "ring" +version = "0.16.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3053cf52e236a3ed746dfc745aa9cacf1b791d846bdaf412f60a8d7d6e17c8fc" +dependencies = [ + "cc", + "libc", + "once_cell", + "spin", + "untrusted", + "web-sys", + "winapi", +] + [[package]] name = "rust-embed" version = "6.4.0" @@ -1068,6 +1181,18 @@ dependencies = [ "libc", ] +[[package]] +name = "simple_asn1" +version = "0.6.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "adc4e5204eb1910f40f9cfa375f6f05b68c3abac4b6fd879c8ff5e7ae8a0a085" +dependencies = [ + "num-bigint 0.4.3", + "num-traits", + "thiserror", + "time", +] + [[package]] name = "siphasher" version = "0.3.10" @@ -1099,6 +1224,12 @@ dependencies = [ "winapi", ] +[[package]] +name = "spin" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6e63cff320ae2c57904679ba7cb63280a3dc4613885beafb148ee7bf9aa9042d" + [[package]] name = "stringprep" version = "0.1.2" @@ -1172,6 +1303,24 @@ dependencies = [ "syn", ] +[[package]] +name = "time" +version = "0.3.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3c3f9a28b618c3a6b9251b6908e9c99e04b9e5c02e6581ccbb67d59c34ef7f9b" +dependencies = [ + "itoa", + "libc", + "num_threads", + "time-macros", +] + +[[package]] +name = "time-macros" +version = "0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "42657b1a6f4d817cda8e7a0ace261fe0cc946cf3a80314390b22cc61ae080792" + [[package]] name = "tinyvec" version = "1.6.0" @@ -1404,6 +1553,12 @@ version = "0.2.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "957e51f3646910546462e67d5f7599b9e4fb8acdd304b087a6494730f9eebf04" +[[package]] +name = "untrusted" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a156c684c91ea7d62626509bce3cb4e1d9ed5c4d978f7b4352658f96a4c26b4a" + [[package]] name = "url" version = "2.3.1" @@ -1495,6 +1650,70 @@ version = "0.11.0+wasi-snapshot-preview1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423" +[[package]] +name = "wasm-bindgen" +version = "0.2.82" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fc7652e3f6c4706c8d9cd54832c4a4ccb9b5336e2c3bd154d5cccfbf1c1f5f7d" +dependencies = [ + "cfg-if", + "wasm-bindgen-macro", +] + +[[package]] +name = "wasm-bindgen-backend" +version = "0.2.82" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "662cd44805586bd52971b9586b1df85cdbbd9112e4ef4d8f41559c334dc6ac3f" +dependencies = [ + "bumpalo", + "log", + "once_cell", + "proc-macro2", + "quote", + "syn", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-macro" +version = "0.2.82" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b260f13d3012071dfb1512849c033b1925038373aea48ced3012c09df952c602" +dependencies = [ + "quote", + "wasm-bindgen-macro-support", +] + +[[package]] +name = "wasm-bindgen-macro-support" +version = "0.2.82" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5be8e654bdd9b79216c2929ab90721aa82faf65c48cdf08bdc4e7f51357b80da" +dependencies = [ + "proc-macro2", + "quote", + "syn", + "wasm-bindgen-backend", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-shared" +version = "0.2.82" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6598dd0bd3c7d51095ff6531a5b23e02acdc81804e30d8f07afb77b7215a140a" + +[[package]] +name = "web-sys" +version = "0.3.59" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed055ab27f941423197eb86b2035720b1a3ce40504df082cac2ecc6ed73335a1" +dependencies = [ + "js-sys", + "wasm-bindgen", +] + [[package]] name = "winapi" version = "0.3.9" diff --git a/Cargo.toml b/Cargo.toml index d844a43..11d89ff 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -7,10 +7,13 @@ edition = "2021" [dependencies] anyhow = "1.0.64" +argon2 = "0.4.1" base-62 = "0.1.1" handlebars = "4.3.3" +jsonwebtoken = "8.1.1" mime_guess = "2.0.4" rand = "0.8.5" +rand_core = { version = "0.6.3", features = ["std"] } rust-embed = "6.4.0" serde = { version = "1.0.144", features = ["derive", "std", "serde_derive"]} serde_json = "1.0.85" diff --git a/migrate/1662887862_schema.up.sql b/migrate/1662887862_schema.up.sql index db94c90..250a063 100644 --- a/migrate/1662887862_schema.up.sql +++ b/migrate/1662887862_schema.up.sql @@ -1,10 +1,18 @@ +CREATE EXTENSION IF NOT EXISTS "uuid-ossp"; + CREATE TABLE users ( id CHAR(22) NOT NULL PRIMARY KEY, username TEXT NOT NULL, host TEXT, - display_name TEXT + display_name TEXT, + password_hash TEXT NOT NULL ); CREATE UNIQUE INDEX u_username_host ON users (username, host); CREATE UNIQUE INDEX u_username_local ON users (username) WHERE host IS NULL; + --id UUID NOT NULL PRIMARY KEY DEFAULT uuid_generate_v4(), +CREATE TABLE keys ( + key TEXT NOT NULL PRIMARY KEY, + value TEXT NOT NULL +); diff --git a/src/database/db.rs b/src/database/db.rs index 069ebb0..7fc7cc9 100644 --- a/src/database/db.rs +++ b/src/database/db.rs @@ -1,8 +1,8 @@ use std::sync::Arc; -use super::users::Users; +use super::{keys::Keys, users::Users}; use tokio::sync::Mutex; -use tokio_postgres::{tls::NoTlsStream, Client, Connection, NoTls, Socket}; +use tokio_postgres::{Client, NoTls}; const DBERR_UNIQUE: &str = "23505"; @@ -30,10 +30,16 @@ impl DB { pub fn users(&self) -> Users { Users::new(self.client.clone()) } + + pub fn keys(&self) -> Keys { + Keys::new(self.client.clone()) + } } +#[derive(Debug)] pub enum DBError { Duplicate, + NotFound, Other(tokio_postgres::Error), } diff --git a/src/database/keys.rs b/src/database/keys.rs new file mode 100644 index 0000000..8a71462 --- /dev/null +++ b/src/database/keys.rs @@ -0,0 +1,39 @@ +use std::sync::Arc; + +use tokio::sync::Mutex; +use tokio_postgres::Client; + +use super::db::DBError; + +#[derive(Clone)] +pub struct Keys(Arc>); +impl Keys { + pub fn new(client: Arc>) -> Self { + Self(client) + } + + pub async fn get_key(&self, key: &str) -> Result { + Ok(self + .0 + .lock() + .await + .query("select value from keys where key = $1", &[&key]) + .await? + .first() + .ok_or_else(|| DBError::NotFound)? + .get("value")) + } + + pub async fn set_key(&self, key: &str, value: &str) -> Result<(), DBError> { + self.0 + .lock() + .await + .execute( + "insert into keys (key, value) values ($1, $2)", + &[&key, &value], + ) + .await?; + + Ok(()) + } +} diff --git a/src/database/mod.rs b/src/database/mod.rs index 295525e..615c89e 100644 --- a/src/database/mod.rs +++ b/src/database/mod.rs @@ -1,2 +1,3 @@ pub mod db; +pub mod keys; pub mod users; diff --git a/src/database/users.rs b/src/database/users.rs index e2503ba..dfc09f7 100644 --- a/src/database/users.rs +++ b/src/database/users.rs @@ -1,9 +1,10 @@ use std::sync::Arc; -use rand::Rng; use tokio::sync::Mutex; use tokio_postgres::{Client, Row}; +use crate::sec; + use super::db; #[derive(Clone)] @@ -14,25 +15,21 @@ impl Users { Self(client) } - fn new_id() -> String { - let bytes = rand::thread_rng().gen::<[u8; 16]>(); - base_62::encode(&bytes) - } - pub async fn create_user(&self, u: User) -> Result { let row = self.0.lock().await.query_one( - "insert into users (id, username, host, display_name) values ($1, $2, $3, $4) returning id", - &[&Self::new_id(), &u.username, &u.host, &u.display_name], + "insert into users (id, username, host, display_name, password_hash) values ($1, $2, $3, $4, $5) returning id", + &[&sec::new_id(), &u.username, &u.host, &u.display_name, &u.password_hash], ).await?; Ok(User { id: row.get("id"), username: u.username, host: u.host, display_name: u.display_name, + password_hash: u.password_hash, }) } - pub async fn user(&self, by: UserSelect) -> Result, anyhow::Error> { + pub async fn user(&self, by: UserSelect) -> Result { let where_param: String; let where_clause = match by { UserSelect::ID(id) => { @@ -54,7 +51,7 @@ impl Users { .await .query( format!( - "select id, username, host, display_name from users where {}", + "select id, username, host, display_name, password_hash from users where {}", where_clause ) .as_str(), @@ -63,9 +60,9 @@ impl Users { .await?; if let Some(row) = rows.first() && rows.len() == 1 { - Ok(Some(User::from(row))) + Ok(User::from(row)) } else { - Ok(None) + Err(db::DBError::NotFound) } } } @@ -75,6 +72,7 @@ pub struct User { pub username: String, pub host: Option, pub display_name: Option, + pub password_hash: String, } impl From<&Row> for User { @@ -84,6 +82,7 @@ impl From<&Row> for User { username: row.get("username"), host: row.get("host"), display_name: row.get("display_name"), + password_hash: row.get("password_hash"), } } } diff --git a/src/helpers.rs b/src/helpers.rs new file mode 100644 index 0000000..d964050 --- /dev/null +++ b/src/helpers.rs @@ -0,0 +1,20 @@ +use warp::hyper::StatusCode; + +pub(crate) fn html_with_status(body: String, status: StatusCode) -> warp::http::Response { + warp::http::Response::builder() + .header("Content-Type", "text/html; charset=utf-8") + .status(status) + .body(body) + .expect("failed marshalling html response") +} + +pub(crate) fn html(body: String) -> warp::http::Response { + html_with_status(body, StatusCode::OK) +} + +// pub(crate) fn html(body: String) -> Result, warp::http::Error> { +// warp::http::Response::builder() +// .header("Content-Type", "text/html; charset=utf-8") +// .status(StatusCode::OK) +// .body(body) +// } diff --git a/src/main.rs b/src/main.rs index a4a5012..ebe578f 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,12 +1,13 @@ mod database; mod model; +mod sec; mod servek; mod svc; use database::db::DB; use serde::{Deserialize, Serialize}; use servek::servek::Server; -use svc::profiles::Profiler; +use svc::{auth::Auth, profiles::Profiler}; #[derive(Clone, Copy, Serialize, Deserialize)] pub enum ActivityKind { @@ -43,6 +44,7 @@ async fn main() -> Result<(), anyhow::Error> { ) .await?; let profiler = Profiler::new(db.users()); - Server::new(profiler).listen_and_serve(8008).await; + let auth = Auth::new(db.keys(), db.users()).await; + Server::new(profiler, auth).listen_and_serve(8008).await; Ok(()) } diff --git a/src/sec/mod.rs b/src/sec/mod.rs new file mode 100644 index 0000000..d950d49 --- /dev/null +++ b/src/sec/mod.rs @@ -0,0 +1,26 @@ +use argon2::{ + password_hash::{rand_core::OsRng, PasswordHash, PasswordHasher, PasswordVerifier, SaltString}, + Argon2, +}; +use rand::Rng; + +pub fn hash(password: String) -> String { + let password = password.as_bytes(); + // Hash password to PHC string ($argon2id$v=19$...) + Argon2::default() + .hash_password(password, &SaltString::generate(&mut OsRng)) + .unwrap() + .to_string() +} + +pub fn compare(password: &str, password_hash: &str) -> bool { + let hash = PasswordHash::new(&password_hash).unwrap(); + Argon2::default() + .verify_password(password.as_bytes(), &hash) + .is_ok() +} + +pub fn new_id() -> String { + let bytes = rand::thread_rng().gen::<[u8; 16]>(); + base_62::encode(&bytes) +} diff --git a/src/servek/html.rs b/src/servek/html.rs index ac1b4d4..e4126d4 100644 --- a/src/servek/html.rs +++ b/src/servek/html.rs @@ -1,8 +1,19 @@ -use std::str::FromStr; +use std::{collections::HashMap, str::FromStr}; -use warp::{http::HeaderValue, hyper::Uri, path::Tail, reply::Response, Filter, Rejection, Reply}; +use warp::{ + http::HeaderValue, + hyper::Uri, + path::Tail, + reply::{Html, Response}, + Filter, Rejection, Reply, +}; -use super::servek::{Server, ServerError}; +use crate::svc::auth::AuthError; + +use super::{ + servek::{Server, ServerError}, + CreateProfileRequest, ErrorTemplate, +}; use rust_embed::RustEmbed; #[derive(RustEmbed)] @@ -14,8 +25,12 @@ impl Server { &self, ) -> impl Filter + Clone { Self::index() - .or(self.profile().await) - .or(self.create_profile().await.or(Server::static_files())) + .or(self.profile()) + .or(self.create_profile()) + .or(Server::static_files()) + .or(self.login_page()) + .or(self.login()) + .or(Self::handler_404()) } fn index() -> impl Filter + Clone { @@ -24,13 +39,63 @@ impl Server { })) } + fn handler_404() -> impl Filter + Clone { + warp::get().and(warp::path::end().map(move || { + warp::reply::html(include_str!("../../templates/html/404.html").to_owned()) + })) + } + + fn login_with_error(&self, error: String) -> Result, Rejection> { + self.hb + .render("login-error", &serde_json::json!(ErrorTemplate { error })) + .map(|html| warp::reply::html(html)) + .map_err(|e| ServerError::from(e).rejection()) + } + + fn login_page( + &self, + ) -> impl Filter + Clone { + warp::get().and(warp::path::path("login").map(move || { + warp::reply::html(include_str!("../../templates/html/login.html").to_owned()) + })) + } + + fn login(&self) -> impl Filter + Clone { + warp::post().and( + warp::body::content_length_limit(8192).and( + warp::path::path("login") + .and(Self::with_server(self.clone())) + .and(warp::body::form()) + .and_then(|srv: Server, body: HashMap| async move { + let user = body.get("username").ok_or( + ServerError::BadRequest("no username provided".to_owned()).rejection(), + )?; + let pass = body.get("password").ok_or( + ServerError::BadRequest("no password provided".to_owned()).rejection(), + )?; + let token = srv + .auth + .login(user.clone(), pass.clone()) + .await + .map(|html| warp::reply::html(html)); + if let Err(e) = &token { + if let AuthError::InvalidCredentials = e { + return srv.login_with_error("invalid credentials".to_owned()); + } + } + token.map_err(|e| ServerError::from(e).rejection()) + }), + ), + ) + } + fn with_server( srv: Server, ) -> impl Filter + Clone { warp::any().map(move || srv.clone()) } - async fn profile(&self) -> impl Filter + Clone { + fn profile(&self) -> impl Filter + Clone { warp::get().and( warp::path!("@" / String) .and(Self::with_server(self.clone())) @@ -45,28 +110,39 @@ impl Server { .map_err(|e| ServerError::from(e))?), ) .map(|html| warp::reply::html(html)) - .map_err(|e| ServerError::from(e).reject_self()) + .map_err(|e| ServerError::from(e).rejection()) }), ) } - async fn create_profile(&self) -> impl Filter + Clone { + fn create_profile(&self) -> impl Filter + Clone { warp::post().and( - warp::path!("@" / String) - .and(Self::with_server(self.clone())) - .and_then(|username: String, srv: Server| async move { - let user = srv - .profiler - .create_user(username, None) - .await - .map_err(|e| ServerError::from(e)); - match user { - Ok(u) => Ok(warp::redirect( - Uri::from_str(format!("/@/{}", u.username).as_str()).unwrap(), - )), - Err(e) => Err(e.reject_self()), - } - }), + warp::body::content_length_limit(8192).and( + warp::path!("@" / String) + .and(Self::with_server(self.clone())) + .and(warp::body::form()) + .and_then( + |username: String, srv: Server, body: CreateProfileRequest| async move { + if body.password_hash.len() == 0 { + return Err(ServerError::BadRequest( + "cannot have an empty password".to_owned(), + ) + .rejection()); + } + let user = srv + .profiler + .create_user(username, body.password_hash) + .await + .map_err(|e| ServerError::from(e)); + match user { + Ok(u) => Ok(warp::redirect( + Uri::from_str(format!("/@/{}", u.username).as_str()).unwrap(), + )), + Err(e) => Err(e.rejection()), + } + }, + ), + ), ) } @@ -75,7 +151,7 @@ impl Server { |path: Tail| async move { let asset = match StaticData::get(path.as_str()) { Some(a) => a, - None => return Err(ServerError::NotFound.reject_self()), + None => return Err(ServerError::NotFound.rejection()), }; let mime = mime_guess::from_path(path.as_str()).first_or_octet_stream(); diff --git a/src/servek/mod.rs b/src/servek/mod.rs index 5f68cc2..b183d8d 100644 --- a/src/servek/mod.rs +++ b/src/servek/mod.rs @@ -1,2 +1,19 @@ +use serde::{Deserialize, Serialize}; + mod html; pub mod servek; + +#[derive(Debug, Clone, Serialize)] +struct ErrorTemplate { + pub error: String, +} + +#[derive(Debug, Clone, Deserialize)] +pub struct CreateProfileRequest { + pub password_hash: String, +} + +#[derive(Debug, Clone, Serialize)] +pub struct Login { + pub token: String, +} diff --git a/src/servek/servek.rs b/src/servek/servek.rs index 7953e0a..d4e4d44 100644 --- a/src/servek/servek.rs +++ b/src/servek/servek.rs @@ -9,24 +9,32 @@ use warp::{ }; use crate::{ - database::users::User, model, - svc::profiles::{Profiler, UserError}, + svc::{ + auth::{Auth, AuthError}, + profiles::{Profiler, UserError}, + }, }; #[derive(Clone)] pub struct Server { pub(super) hb: Handlebars<'static>, pub(super) profiler: Profiler, + pub(super) auth: Auth, } impl Server { - pub fn new(profiler: Profiler) -> Self { + pub fn new(profiler: Profiler, auth: Auth) -> Self { let mut hb = Handlebars::new(); hb.register_template_string("profile", include_str!("../../templates/html/profile.html")) .expect("profile template"); + hb.register_template_string( + "login-error", + include_str!("../../templates/html/login-error.html"), + ) + .expect("login-error template"); - Self { hb, profiler } + Self { hb, profiler, auth } } pub async fn listen_and_serve(self, port: u16) -> ! { @@ -55,9 +63,9 @@ impl Server { code = StatusCode::NOT_FOUND; message = "not found"; } - ServerError::Duplicate => { + ServerError::BadRequest(err) => { code = StatusCode::BAD_REQUEST; - message = "duplicate entry exists"; + message = err; } } } else if let Some(err) = err.find::() { @@ -82,11 +90,11 @@ impl Server { pub(super) enum ServerError { Internal(String), NotFound, - Duplicate, + BadRequest(String), } impl ServerError { - pub(super) fn reject_self(self) -> Rejection { + pub(super) fn rejection(self) -> Rejection { warp::reject::custom(self) } } @@ -102,13 +110,24 @@ impl From for ServerError { impl From for ServerError { fn from(u: UserError) -> Self { match u { - UserError::Duplicate => Self::Duplicate, + UserError::Duplicate => Self::BadRequest("duplicate entry exists".to_owned()), UserError::NotFound => Self::NotFound, UserError::Other(o) => Self::Internal(format!("UserError: {}", o)), } } } +impl From for ServerError { + fn from(a: AuthError) -> Self { + match a { + AuthError::InvalidCredentials => { + ServerError::BadRequest("invalid credentials".to_owned()) + } + AuthError::ServerError(err) => ServerError::Internal(err), + } + } +} + impl Display for ServerError { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { write!(f, "{}", self) diff --git a/src/svc/auth.rs b/src/svc/auth.rs new file mode 100644 index 0000000..f89a2b5 --- /dev/null +++ b/src/svc/auth.rs @@ -0,0 +1,92 @@ +use std::time::SystemTime; + +use jsonwebtoken::{EncodingKey, Header}; +use serde::{Deserialize, Serialize}; + +use crate::{ + database::{ + db::DBError, + keys::Keys, + users::{self, UserSelect, Users}, + }, + sec, +}; + +#[derive(Clone)] +pub struct Auth { + secret: String, + users: Users, +} + +const KEY_JWT_SECRET: &str = "JWT_SECRET"; +const SECS_JWT_EXPIRE: u64 = 60 * 60; // 1hr + +impl Auth { + pub async fn new(db: Keys, users: Users) -> Self { + Self { + secret: match db.get_key(KEY_JWT_SECRET).await { + Ok(secret) => secret, + Err(_) => { + // Create new secret and store to db + // If that fails, crash the application + let secret = sec::new_id(); + db.set_key(KEY_JWT_SECRET, &secret).await.unwrap(); + secret + } + }, + users, + } + } + + pub async fn login(&self, username: String, password: String) -> Result { + let user = self.users.user(UserSelect::Username(username)).await?; + if !sec::compare(&password, &user.password_hash) { + return Err(AuthError::InvalidCredentials); + } + + Ok(jsonwebtoken::encode( + &Header::default(), + &Claims::from(user), + &EncodingKey::from_secret("secret".as_ref()), + )?) + } +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct Claims { + pub sub: String, + pub exp: u64, + pub iat: u64, +} + +impl From for Claims { + fn from(u: users::User) -> Self { + let now = SystemTime::now() + .duration_since(SystemTime::UNIX_EPOCH) + .unwrap() + .as_secs(); + Claims { + sub: u.id, + exp: now + SECS_JWT_EXPIRE, + iat: now, + } + } +} + +#[derive(Debug)] +pub enum AuthError { + InvalidCredentials, + ServerError(String), +} + +impl From for AuthError { + fn from(_: DBError) -> Self { + Self::InvalidCredentials + } +} + +impl From for AuthError { + fn from(e: jsonwebtoken::errors::Error) -> Self { + Self::ServerError(e.to_string()) + } +} diff --git a/src/svc/mod.rs b/src/svc/mod.rs index 0a9c08b..5805164 100644 --- a/src/svc/mod.rs +++ b/src/svc/mod.rs @@ -1 +1,2 @@ +pub mod auth; pub mod profiles; diff --git a/src/svc/profiles.rs b/src/svc/profiles.rs index 42ead63..20f9de1 100644 --- a/src/svc/profiles.rs +++ b/src/svc/profiles.rs @@ -1,9 +1,12 @@ use serde::Serialize; use warp::{reject::Reject, Rejection}; -use crate::database::{ - db, - users::{self, UserSelect, Users}, +use crate::{ + database::{ + db, + users::{self, UserSelect, Users}, + }, + sec, }; #[derive(Clone)] @@ -22,28 +25,19 @@ impl Profiler { } else { UserSelect::Username(username) }; - match self.db.user(select).await? { - Some(user) => Ok(User::from(user)), - None => Err(UserError::NotFound), - } + Ok(User::from(self.db.user(select).await?)) } - pub async fn create_user( - &self, - username: String, - display_name: Option, - ) -> Result { + pub async fn create_user(&self, username: String, password: String) -> Result { let result = self .db - .create_user( - User { - id: String::new(), - username, - display_name, - host: None, - } - .into(), - ) + .create_user(users::User { + id: String::new(), + username, + host: None, + display_name: None, + password_hash: sec::hash(password), + }) .await?; Ok(User::from(result)) @@ -69,17 +63,6 @@ impl From for User { } } -impl Into for User { - fn into(self) -> users::User { - users::User { - id: self.id, - username: self.username, - display_name: self.display_name, - host: self.host, - } - } -} - #[derive(Debug, Clone)] pub enum UserError { Duplicate, @@ -97,6 +80,7 @@ impl From for UserError { fn from(err: db::DBError) -> Self { match err { db::DBError::Duplicate => Self::Duplicate, + db::DBError::NotFound => Self::NotFound, db::DBError::Other(e) => Self::Other(e.to_string()), } } diff --git a/static/style/login.css b/static/style/login.css new file mode 100644 index 0000000..8b13789 --- /dev/null +++ b/static/style/login.css @@ -0,0 +1 @@ + diff --git a/static/style/main.css b/static/style/main.css index 606cd46..9013015 100644 --- a/static/style/main.css +++ b/static/style/main.css @@ -6,3 +6,17 @@ body { h1 { text-align: center; } + + +input { + background-color: black; + color: white; + border-color: rebeccapurple; +} + +.error { + color: rgba(255, 0, 0, 0.7); + font-weight: bold; + border: 5px solid rgba(99, 0, 0, 0.2); + background-color: rgba(95, 0, 0, 0.5); +} diff --git a/templates/html/404.html b/templates/html/404.html index d6901de..ce91864 100644 --- a/templates/html/404.html +++ b/templates/html/404.html @@ -3,10 +3,13 @@ flabk - not found + -

not found

+

404 not found

+
+

return

diff --git a/templates/html/index.html b/templates/html/index.html index 6ed51de..07473be 100644 --- a/templates/html/index.html +++ b/templates/html/index.html @@ -8,6 +8,7 @@

hi

+

login

diff --git a/templates/html/login-error.html b/templates/html/login-error.html new file mode 100644 index 0000000..aba163c --- /dev/null +++ b/templates/html/login-error.html @@ -0,0 +1,40 @@ + + + + + flabk - login error + + + + + +

login

+
+

Login form

+

error: {{error}}

+
+ + + +
+
+ + + + diff --git a/templates/html/login.html b/templates/html/login.html new file mode 100644 index 0000000..90318b5 --- /dev/null +++ b/templates/html/login.html @@ -0,0 +1,39 @@ + + + + + flabk - login + + + + + +

login

+
+

Login form

+
+ + + +
+
+ + + +