vaguely added login/logout? gunna try killing warp

This commit is contained in:
emilis 2022-09-12 16:36:14 +01:00
parent 1d9802530d
commit 1c5c9caf2a
22 changed files with 694 additions and 84 deletions

221
Cargo.lock generated
View File

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

View File

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

View File

@ -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
);

View File

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

39
src/database/keys.rs Normal file
View File

@ -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<Mutex<Client>>);
impl Keys {
pub fn new(client: Arc<Mutex<Client>>) -> Self {
Self(client)
}
pub async fn get_key(&self, key: &str) -> Result<String, DBError> {
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(())
}
}

View File

@ -1,2 +1,3 @@
pub mod db;
pub mod keys;
pub mod users;

View File

@ -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<User, db::DBError> {
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<Option<User>, anyhow::Error> {
pub async fn user(&self, by: UserSelect) -> Result<User, db::DBError> {
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<String>,
pub display_name: Option<String>,
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"),
}
}
}

20
src/helpers.rs Normal file
View File

@ -0,0 +1,20 @@
use warp::hyper::StatusCode;
pub(crate) fn html_with_status(body: String, status: StatusCode) -> warp::http::Response<String> {
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<String> {
html_with_status(body, StatusCode::OK)
}
// pub(crate) fn html(body: String) -> Result<warp::http::Response<String>, warp::http::Error> {
// warp::http::Response::builder()
// .header("Content-Type", "text/html; charset=utf-8")
// .status(StatusCode::OK)
// .body(body)
// }

View File

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

26
src/sec/mod.rs Normal file
View File

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

View File

@ -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<Extract = impl warp::Reply, Error = warp::Rejection> + 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<Extract = impl warp::Reply, Error = warp::Rejection> + Clone {
@ -24,13 +39,63 @@ impl Server {
}))
}
fn handler_404() -> impl Filter<Extract = impl warp::Reply, Error = warp::Rejection> + 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<Html<String>, 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<Extract = impl warp::Reply, Error = warp::Rejection> + 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<Extract = impl warp::Reply, Error = warp::Rejection> + 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<String, String>| 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<Extract = (Server,), Error = std::convert::Infallible> + Clone {
warp::any().map(move || srv.clone())
}
async fn profile(&self) -> impl Filter<Extract = impl Reply, Error = Rejection> + Clone {
fn profile(&self) -> impl Filter<Extract = impl Reply, Error = Rejection> + 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<Extract = impl Reply, Error = Rejection> + Clone {
fn create_profile(&self) -> impl Filter<Extract = impl Reply, Error = Rejection> + 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();

View File

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

View File

@ -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::<MethodNotAllowed>() {
@ -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<RenderError> for ServerError {
impl From<UserError> 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<AuthError> 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)

92
src/svc/auth.rs Normal file
View File

@ -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<String, AuthError> {
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<users::User> 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<DBError> for AuthError {
fn from(_: DBError) -> Self {
Self::InvalidCredentials
}
}
impl From<jsonwebtoken::errors::Error> for AuthError {
fn from(e: jsonwebtoken::errors::Error) -> Self {
Self::ServerError(e.to_string())
}
}

View File

@ -1 +1,2 @@
pub mod auth;
pub mod profiles;

View File

@ -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<String>,
) -> Result<User, UserError> {
pub async fn create_user(&self, username: String, password: String) -> Result<User, UserError> {
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<users::User> for User {
}
}
impl Into<users::User> 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<db::DBError> 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()),
}
}

1
static/style/login.css Normal file
View File

@ -0,0 +1 @@

View File

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

View File

@ -3,10 +3,13 @@
<head>
<title>flabk - not found</title>
<link rel="stylesheet" href="/static/style/main.css">
</head>
<body>
<h1>not found</h1>
<h1>404 not found</h1>
<br />
<h1><a href="/">return</a></h1>
</body>
</html>

View File

@ -8,6 +8,7 @@
<body>
<h1>hi</h1>
<h1><a href="/login">login</a></h1>
</body>
</html>

View File

@ -0,0 +1,40 @@
<!DOCTYPE html>
<html>
<head>
<title>flabk - login error</title>
<link rel="stylesheet" href="/static/style/main.css">
<style>
#login-main {
width: 30%;
border: 5px solid rebeccapurple;
height: 30vw;
padding: 10px;
margin: 0 auto;
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
background-color: rgba(13, 6, 19, 0.4);
font-size: large;
font-weight: bold;
}
</style>
</head>
<body>
<h1>login</h1>
<div id="login-main">
<p>Login form</p>
<h2 class="error">error: {{error}}</h2>
<form id="login" method="post" action="/login">
<input type="text" name="username">
<input type="password" name="password">
<input type="submit" value="Submit" hidden>
</form>
</div>
</body>
</html>

39
templates/html/login.html Normal file
View File

@ -0,0 +1,39 @@
<!DOCTYPE html>
<html>
<head>
<title>flabk - login</title>
<link rel="stylesheet" href="/static/style/main.css">
<style>
#login-main {
width: 30%;
border: 5px solid rebeccapurple;
height: 30vw;
padding: 10px;
margin: 0 auto;
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
background-color: rgba(13, 6, 19, 0.4);
font-size: large;
font-weight: bold;
}
</style>
</head>
<body>
<h1>login</h1>
<div id="login-main">
<p>Login form</p>
<form id="login" method="post" action="/login">
<input type="text" name="username">
<input type="password" name="password">
<input type="submit" value="Submit" hidden>
</form>
</div>
</body>
</html>