From 4ccee643230d890e629ad894b8a5f74004ab658f Mon Sep 17 00:00:00 2001 From: Jef Roosens Date: Fri, 20 Aug 2021 14:06:01 +0200 Subject: [PATCH 01/67] Started writing auth sql schema --- Cargo.toml | 7 +++++++ .../2021-08-20-110251_users-and-auth/down.sql | 1 + .../2021-08-20-110251_users-and-auth/up.sql | 17 +++++++++++++++++ src/rbs/main.rs | 7 ++----- 4 files changed, 27 insertions(+), 5 deletions(-) create mode 100644 migrations/2021-08-20-110251_users-and-auth/down.sql create mode 100644 migrations/2021-08-20-110251_users-and-auth/up.sql diff --git a/Cargo.toml b/Cargo.toml index 6d00ee3..85a4ffa 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -15,11 +15,18 @@ path = "src/rbs/main.rs" # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html [dependencies] +# Backend web framework rocket = "0.5.0-rc.1" +# ORM diesel = { version = "1.4.7", features = ["postgres"] } diesel_migrations = "1.4.0" +# To properly compile libpq statically openssl = "0.10.36" +# For password hashing & verification +rust-argon2 = "0.8.3" +rand = "0.8.4" +# Used to provide Rocket routes with database connections [dependencies.rocket_sync_db_pools] version = "0.1.0-rc.1" default_features = false diff --git a/migrations/2021-08-20-110251_users-and-auth/down.sql b/migrations/2021-08-20-110251_users-and-auth/down.sql new file mode 100644 index 0000000..291a97c --- /dev/null +++ b/migrations/2021-08-20-110251_users-and-auth/down.sql @@ -0,0 +1 @@ +-- This file should undo anything in `up.sql` \ No newline at end of file diff --git a/migrations/2021-08-20-110251_users-and-auth/up.sql b/migrations/2021-08-20-110251_users-and-auth/up.sql new file mode 100644 index 0000000..8852c19 --- /dev/null +++ b/migrations/2021-08-20-110251_users-and-auth/up.sql @@ -0,0 +1,17 @@ +-- Users +CREATE TABLE users ( + id uuid DEFAULT gen_random_uuid() PRIMARY KEY, + + username varchar(32) UNIQUE NOT NULL, + password text NOT NULL +); + +-- Permissions that a user can have +CREATE TABLE permissions ( + id uuid DEFAULT gen_random_uuid() PRIMARY KEY, + + user_id uuid REFERENCES users (id) NOT NULL, + name varchar NOT NULL, + + UNIQUE (user_id, name) +); diff --git a/src/rbs/main.rs b/src/rbs/main.rs index 9b67662..e0186ce 100644 --- a/src/rbs/main.rs +++ b/src/rbs/main.rs @@ -2,11 +2,8 @@ // compilation succeeds extern crate openssl; -#[macro_use] -extern crate rocket; - -#[macro_use] -extern crate diesel_migrations; +#[macro_use] extern crate rocket; +#[macro_use] extern crate diesel_migrations; use rocket::{fairing::AdHoc, Build, Rocket}; use rocket_sync_db_pools::{database, diesel}; From eefaf7acaa02ebfd02a48e0d89d0ef3d6259fbfc Mon Sep 17 00:00:00 2001 From: Jef Roosens Date: Fri, 20 Aug 2021 14:46:19 +0200 Subject: [PATCH 02/67] First draft of refresh_tokens table --- .../2021-08-20-110251_users-and-auth/up.sql | 19 ++++++++++++++++++- 1 file changed, 18 insertions(+), 1 deletion(-) diff --git a/migrations/2021-08-20-110251_users-and-auth/up.sql b/migrations/2021-08-20-110251_users-and-auth/up.sql index 8852c19..451d3b3 100644 --- a/migrations/2021-08-20-110251_users-and-auth/up.sql +++ b/migrations/2021-08-20-110251_users-and-auth/up.sql @@ -3,7 +3,10 @@ CREATE TABLE users ( id uuid DEFAULT gen_random_uuid() PRIMARY KEY, username varchar(32) UNIQUE NOT NULL, - password text NOT NULL + -- Hashed + salted representation of the username + password text NOT NULL, + -- Wether the user is currently blocked + blocked boolean DEFAULT false ); -- Permissions that a user can have @@ -15,3 +18,17 @@ CREATE TABLE permissions ( UNIQUE (user_id, name) ); + +-- TODO security reports table (e.g. when a user is blocked) + +-- Stores refresh tokens +CREATE TABLE refresh_tokens ( + -- This is more efficient than storing the text + token bytea PRIMARY KEY, + -- The user for whom the token was created + user_id uuid NOT NULL REFERENCES users(id), + -- When the token expires + expires_at timestamp NOT NULL, + -- When the token was last used (is NULL until used) + last_used_at timestamp +); From 5e861336515e44be39404238ba8b54c448cd3fb1 Mon Sep 17 00:00:00 2001 From: Jef Roosens Date: Fri, 20 Aug 2021 16:52:58 +0200 Subject: [PATCH 03/67] Started some auth stuff --- Cargo.lock | 61 +++++++++++++++++++ Cargo.toml | 12 +++- .../2021-08-20-110251_users-and-auth/down.sql | 3 +- .../2021-08-20-110251_users-and-auth/up.sql | 16 ++++- src/rb/lib.rs | 7 ++- src/rb/schema.rs | 44 +++++++++++++ src/rbs/auth.rs | 17 ++++++ src/rbs/main.rs | 2 + 8 files changed, 153 insertions(+), 9 deletions(-) create mode 100644 src/rb/schema.rs create mode 100644 src/rbs/auth.rs diff --git a/Cargo.lock b/Cargo.lock index 3d92928..c1ae49e 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2,6 +2,18 @@ # It is not intended for manual editing. version = 3 +[[package]] +name = "arrayref" +version = "0.3.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a4c527152e37cf757a3f78aae5a06fbeefdb07ccc535c980a3208ee3060dd544" + +[[package]] +name = "arrayvec" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "23b62fc65de8e4e7f52534fb52b0f3ed04746ae267519eef2a83941e8085068b" + [[package]] name = "async-stream" version = "0.3.2" @@ -66,6 +78,12 @@ version = "0.2.8" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a4521f3e3d031370679b3b140beb36dfe4801b09ac77e30c61941f97df3ef28b" +[[package]] +name = "base64" +version = "0.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "904dfeac50f3cdaba28fc6f57fdcddb75f49ed61346676a78c4ffe55877802fd" + [[package]] name = "binascii" version = "0.1.4" @@ -78,6 +96,17 @@ version = "1.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" +[[package]] +name = "blake2b_simd" +version = "0.5.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "afa748e348ad3be8263be728124b24a24f268266f6f5d58af9d75f6a40b5c587" +dependencies = [ + "arrayref", + "arrayvec", + "constant_time_eq", +] + [[package]] name = "bumpalo" version = "3.7.0" @@ -114,6 +143,12 @@ version = "0.4.8" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f92cfa0fd5690b3cf8c1ef2cabbd9b7ef22fa53cf5e1f92b05103f6d5d1cf6e7" +[[package]] +name = "constant_time_eq" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "245097e9a4535ee1e3e3931fcfcd55a796a44c643e8596ff6566d68f09b87bbc" + [[package]] name = "cookie" version = "0.15.1" @@ -125,6 +160,16 @@ dependencies = [ "version_check", ] +[[package]] +name = "crossbeam-utils" +version = "0.8.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d82cfc11ce7f2c3faef78d8a684447b40d503d9681acebed6cb728d45940c4db" +dependencies = [ + "cfg-if", + "lazy_static", +] + [[package]] name = "devise" version = "0.3.1" @@ -912,6 +957,7 @@ dependencies = [ "rocket_codegen", "rocket_http", "serde", + "serde_json", "state", "tempfile", "time", @@ -991,6 +1037,18 @@ dependencies = [ "quote", ] +[[package]] +name = "rust-argon2" +version = "0.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4b18820d944b33caa75a71378964ac46f58517c92b6ae5f762636247c09e78fb" +dependencies = [ + "base64", + "blake2b_simd", + "constant_time_eq", + "crossbeam-utils", +] + [[package]] name = "rustc_version" version = "0.2.3" @@ -1013,8 +1071,11 @@ dependencies = [ "diesel", "diesel_migrations", "openssl", + "rand", "rocket", "rocket_sync_db_pools", + "rust-argon2", + "serde", ] [[package]] diff --git a/Cargo.toml b/Cargo.toml index 85a4ffa..319fd55 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -15,8 +15,6 @@ path = "src/rbs/main.rs" # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html [dependencies] -# Backend web framework -rocket = "0.5.0-rc.1" # ORM diesel = { version = "1.4.7", features = ["postgres"] } diesel_migrations = "1.4.0" @@ -26,6 +24,16 @@ openssl = "0.10.36" rust-argon2 = "0.8.3" rand = "0.8.4" +# Backend web framework +[dependencies.rocket] +version = "0.5.0-rc.1" +features = [ "json" ] + +# Used to (de)serialize JSON +[dependencies.serde] +version = "1.0.127" +features = ["derive"] + # Used to provide Rocket routes with database connections [dependencies.rocket_sync_db_pools] version = "0.1.0-rc.1" diff --git a/migrations/2021-08-20-110251_users-and-auth/down.sql b/migrations/2021-08-20-110251_users-and-auth/down.sql index 291a97c..08e4440 100644 --- a/migrations/2021-08-20-110251_users-and-auth/down.sql +++ b/migrations/2021-08-20-110251_users-and-auth/down.sql @@ -1 +1,2 @@ --- This file should undo anything in `up.sql` \ No newline at end of file +-- This file should undo anything in `up.sql` +DROP TABLE IF EXISTS users, permissions, refresh_tokens, security_reports CASCADE; diff --git a/migrations/2021-08-20-110251_users-and-auth/up.sql b/migrations/2021-08-20-110251_users-and-auth/up.sql index 451d3b3..76de073 100644 --- a/migrations/2021-08-20-110251_users-and-auth/up.sql +++ b/migrations/2021-08-20-110251_users-and-auth/up.sql @@ -6,7 +6,7 @@ CREATE TABLE users ( -- Hashed + salted representation of the username password text NOT NULL, -- Wether the user is currently blocked - blocked boolean DEFAULT false + blocked boolean NOT NULL DEFAULT false ); -- Permissions that a user can have @@ -14,12 +14,22 @@ CREATE TABLE permissions ( id uuid DEFAULT gen_random_uuid() PRIMARY KEY, user_id uuid REFERENCES users (id) NOT NULL, - name varchar NOT NULL, + name varchar(64) NOT NULL, UNIQUE (user_id, name) ); --- TODO security reports table (e.g. when a user is blocked) +-- Security reports (e.g. when a user is blocked) +CREATE TABLE security_reports ( + id uuid DEFAULT gen_random_uuid() PRIMARY KEY, + + -- When the report was made + report_time timestamp NOT NULL DEFAULT now(), + -- What type of report it is + report_type varchar(64) NOT NULL, + -- Contents of the report + content TEXT NOT NULL +); -- Stores refresh tokens CREATE TABLE refresh_tokens ( diff --git a/src/rb/lib.rs b/src/rb/lib.rs index 4cb480e..5dcc829 100644 --- a/src/rb/lib.rs +++ b/src/rb/lib.rs @@ -1,3 +1,4 @@ -pub fn yeet() -> String { - String::from("yeet") -} +pub const PERM_CODES: [&str; 2] = [ + "modify-blog-posts", + "modify-users" +]; diff --git a/src/rb/schema.rs b/src/rb/schema.rs new file mode 100644 index 0000000..6b8f42b --- /dev/null +++ b/src/rb/schema.rs @@ -0,0 +1,44 @@ +table! { + permissions (id) { + id -> Uuid, + user_id -> Uuid, + name -> Varchar, + } +} + +table! { + refresh_tokens (token) { + token -> Bytea, + user_id -> Uuid, + expires_at -> Timestamp, + last_used_at -> Nullable, + } +} + +table! { + security_reports (id) { + id -> Uuid, + report_time -> Timestamp, + report_type -> Varchar, + content -> Text, + } +} + +table! { + users (id) { + id -> Uuid, + username -> Varchar, + password -> Text, + blocked -> Bool, + } +} + +joinable!(permissions -> users (user_id)); +joinable!(refresh_tokens -> users (user_id)); + +allow_tables_to_appear_in_same_query!( + permissions, + refresh_tokens, + security_reports, + users, +); diff --git a/src/rbs/auth.rs b/src/rbs/auth.rs new file mode 100644 index 0000000..b3c3319 --- /dev/null +++ b/src/rbs/auth.rs @@ -0,0 +1,17 @@ +use crate::RbDbConn; +use serde::Deserialize; +use rocket::serde::json::Json; + +#[derive(Deserialize)] +struct Credentials { + username: String, + password: String +} + +#[post("/login", data="")] +async fn login(conn: RbDbConn, credentials: Json) { + +} + +// /refresh +// /logout diff --git a/src/rbs/main.rs b/src/rbs/main.rs index e0186ce..9167d8e 100644 --- a/src/rbs/main.rs +++ b/src/rbs/main.rs @@ -8,6 +8,8 @@ extern crate openssl; use rocket::{fairing::AdHoc, Build, Rocket}; use rocket_sync_db_pools::{database, diesel}; +mod auth; + embed_migrations!(); #[database("postgres_rb")] From 1c524f181f31a63cbf63dacefcdf03de88b95fb8 Mon Sep 17 00:00:00 2001 From: Jef Roosens Date: Fri, 20 Aug 2021 22:25:21 +0200 Subject: [PATCH 04/67] Removed role system --- .../2021-08-20-110251_users-and-auth/down.sql | 2 +- .../2021-08-20-110251_users-and-auth/up.sql | 27 +++---------------- src/rb/lib.rs | 4 --- src/rb/schema.rs | 21 +-------------- 4 files changed, 5 insertions(+), 49 deletions(-) diff --git a/migrations/2021-08-20-110251_users-and-auth/down.sql b/migrations/2021-08-20-110251_users-and-auth/down.sql index 08e4440..dba6dd6 100644 --- a/migrations/2021-08-20-110251_users-and-auth/down.sql +++ b/migrations/2021-08-20-110251_users-and-auth/down.sql @@ -1,2 +1,2 @@ -- This file should undo anything in `up.sql` -DROP TABLE IF EXISTS users, permissions, refresh_tokens, security_reports CASCADE; +DROP TABLE IF EXISTS users, refresh_tokens CASCADE; diff --git a/migrations/2021-08-20-110251_users-and-auth/up.sql b/migrations/2021-08-20-110251_users-and-auth/up.sql index 76de073..7ffd9f1 100644 --- a/migrations/2021-08-20-110251_users-and-auth/up.sql +++ b/migrations/2021-08-20-110251_users-and-auth/up.sql @@ -1,4 +1,3 @@ --- Users CREATE TABLE users ( id uuid DEFAULT gen_random_uuid() PRIMARY KEY, @@ -6,29 +5,9 @@ CREATE TABLE users ( -- Hashed + salted representation of the username password text NOT NULL, -- Wether the user is currently blocked - blocked boolean NOT NULL DEFAULT false -); - --- Permissions that a user can have -CREATE TABLE permissions ( - id uuid DEFAULT gen_random_uuid() PRIMARY KEY, - - user_id uuid REFERENCES users (id) NOT NULL, - name varchar(64) NOT NULL, - - UNIQUE (user_id, name) -); - --- Security reports (e.g. when a user is blocked) -CREATE TABLE security_reports ( - id uuid DEFAULT gen_random_uuid() PRIMARY KEY, - - -- When the report was made - report_time timestamp NOT NULL DEFAULT now(), - -- What type of report it is - report_type varchar(64) NOT NULL, - -- Contents of the report - content TEXT NOT NULL + blocked boolean NOT NULL DEFAULT false, + -- Wether the user is an admin + admin boolean NOT NULL DEFAULT false ); -- Stores refresh tokens diff --git a/src/rb/lib.rs b/src/rb/lib.rs index 5dcc829..e69de29 100644 --- a/src/rb/lib.rs +++ b/src/rb/lib.rs @@ -1,4 +0,0 @@ -pub const PERM_CODES: [&str; 2] = [ - "modify-blog-posts", - "modify-users" -]; diff --git a/src/rb/schema.rs b/src/rb/schema.rs index 6b8f42b..8dcb725 100644 --- a/src/rb/schema.rs +++ b/src/rb/schema.rs @@ -1,11 +1,3 @@ -table! { - permissions (id) { - id -> Uuid, - user_id -> Uuid, - name -> Varchar, - } -} - table! { refresh_tokens (token) { token -> Bytea, @@ -15,30 +7,19 @@ table! { } } -table! { - security_reports (id) { - id -> Uuid, - report_time -> Timestamp, - report_type -> Varchar, - content -> Text, - } -} - table! { users (id) { id -> Uuid, username -> Varchar, password -> Text, blocked -> Bool, + admin -> Bool, } } -joinable!(permissions -> users (user_id)); joinable!(refresh_tokens -> users (user_id)); allow_tables_to_appear_in_same_query!( - permissions, refresh_tokens, - security_reports, users, ); From d90dbcdc2aa84b27bfeb1eadbb9c7037c4b89835 Mon Sep 17 00:00:00 2001 From: Jef Roosens Date: Fri, 20 Aug 2021 23:09:22 +0200 Subject: [PATCH 05/67] Some broken shit --- Cargo.lock | 8 ++++++++ Cargo.toml | 5 +++-- src/rb/auth.rs | 22 ++++++++++++++++++++++ src/rb/errors.rs | 4 ++++ src/rb/lib.rs | 7 +++++++ src/rb/models.rs | 11 +++++++++++ src/rb/schema.rs | 5 +---- src/rbs/auth.rs | 11 +++++++---- src/rbs/main.rs | 6 ++++-- 9 files changed, 67 insertions(+), 12 deletions(-) create mode 100644 src/rb/auth.rs create mode 100644 src/rb/errors.rs create mode 100644 src/rb/models.rs diff --git a/Cargo.lock b/Cargo.lock index c1ae49e..54bf4b6 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -214,6 +214,7 @@ dependencies = [ "diesel_derives", "pq-sys", "r2d2", + "uuid", ] [[package]] @@ -1076,6 +1077,7 @@ dependencies = [ "rocket_sync_db_pools", "rust-argon2", "serde", + "uuid", ] [[package]] @@ -1470,6 +1472,12 @@ version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8ccb82d61f80a663efe1f787a51b16b5a51e3314d6ac365b08639f52387b33f3" +[[package]] +name = "uuid" +version = "0.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bc5cf98d8186244414c848017f0e2676b3fcb46807f6668a97dfe67359a3c4b7" + [[package]] name = "vcpkg" version = "0.2.15" diff --git a/Cargo.toml b/Cargo.toml index 319fd55..fb5cd1f 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -16,18 +16,19 @@ path = "src/rbs/main.rs" [dependencies] # ORM -diesel = { version = "1.4.7", features = ["postgres"] } +diesel = { version = "1.4.7", features = ["postgres", "uuidv07"] } diesel_migrations = "1.4.0" # To properly compile libpq statically openssl = "0.10.36" # For password hashing & verification rust-argon2 = "0.8.3" rand = "0.8.4" +uuid = "0.8.2" # Backend web framework [dependencies.rocket] version = "0.5.0-rc.1" -features = [ "json" ] +features = ["json"] # Used to (de)serialize JSON [dependencies.serde] diff --git a/src/rb/auth.rs b/src/rb/auth.rs new file mode 100644 index 0000000..2c8dbee --- /dev/null +++ b/src/rb/auth.rs @@ -0,0 +1,22 @@ +use crate::errors::AuthError; +use crate::models::User; +use crate::schema::users::dsl as users; +use argon2::verify_encoded; +use diesel::prelude::*; +use diesel::PgConnection; + +pub fn verify_user(conn: &PgConnection, username: &str, password: &str) -> Result { + // TODO handle non-"NotFound" Diesel errors accordingely + let user = match users::users + .filter(users::username.eq(username)) + .first::(conn) + { + Err(_) => return Err(AuthError::UnknownUser), + Ok(user) => user, + }; + + match verify_encoded(user.password.as_str(), password.as_bytes()) { + Ok(true) => Ok(user), + _ => Err(AuthError::InvalidPassword), + } +} diff --git a/src/rb/errors.rs b/src/rb/errors.rs new file mode 100644 index 0000000..fc12ba9 --- /dev/null +++ b/src/rb/errors.rs @@ -0,0 +1,4 @@ +pub enum AuthError { + UnknownUser, + InvalidPassword, +} diff --git a/src/rb/lib.rs b/src/rb/lib.rs index e69de29..3d7d0f4 100644 --- a/src/rb/lib.rs +++ b/src/rb/lib.rs @@ -0,0 +1,7 @@ +#[macro_use] +extern crate diesel; + +pub mod auth; +pub mod errors; +mod models; +pub(crate) mod schema; diff --git a/src/rb/models.rs b/src/rb/models.rs new file mode 100644 index 0000000..d9455d6 --- /dev/null +++ b/src/rb/models.rs @@ -0,0 +1,11 @@ +use diesel::Queryable; +use uuid::Uuid; + +#[derive(Queryable)] +pub struct User { + id: Uuid, + username: String, + pub password: String, + blocked: bool, + admin: bool, +} diff --git a/src/rb/schema.rs b/src/rb/schema.rs index 8dcb725..e3854e3 100644 --- a/src/rb/schema.rs +++ b/src/rb/schema.rs @@ -19,7 +19,4 @@ table! { joinable!(refresh_tokens -> users (user_id)); -allow_tables_to_appear_in_same_query!( - refresh_tokens, - users, -); +allow_tables_to_appear_in_same_query!(refresh_tokens, users,); diff --git a/src/rbs/auth.rs b/src/rbs/auth.rs index b3c3319..1c9d793 100644 --- a/src/rbs/auth.rs +++ b/src/rbs/auth.rs @@ -1,16 +1,19 @@ use crate::RbDbConn; -use serde::Deserialize; +use rb::auth::verify_user; use rocket::serde::json::Json; +use serde::Deserialize; #[derive(Deserialize)] struct Credentials { username: String, - password: String + password: String, } -#[post("/login", data="")] +#[post("/login", data = "")] async fn login(conn: RbDbConn, credentials: Json) { - + let user = conn + .run(move |c| verify_user(c, &credentials.username, &credentials.password)) + .await; } // /refresh diff --git a/src/rbs/main.rs b/src/rbs/main.rs index 9167d8e..5921df7 100644 --- a/src/rbs/main.rs +++ b/src/rbs/main.rs @@ -2,8 +2,10 @@ // compilation succeeds extern crate openssl; -#[macro_use] extern crate rocket; -#[macro_use] extern crate diesel_migrations; +#[macro_use] +extern crate rocket; +#[macro_use] +extern crate diesel_migrations; use rocket::{fairing::AdHoc, Build, Rocket}; use rocket_sync_db_pools::{database, diesel}; From 6782fecc0d6da4c079193d166e5b65eaccd04b49 Mon Sep 17 00:00:00 2001 From: Jef Roosens Date: Sat, 21 Aug 2021 13:46:41 +0200 Subject: [PATCH 06/67] Started JWT token generation --- Cargo.lock | 163 +++++++++++++++++++++++++++++++++++++++++++++-- Cargo.toml | 6 +- src/rb/auth.rs | 31 +++++++++ src/rb/models.rs | 9 ++- src/rbs/auth.rs | 3 + 5 files changed, 203 insertions(+), 9 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 54bf4b6..8d50653 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -107,6 +107,15 @@ dependencies = [ "constant_time_eq", ] +[[package]] +name = "block-buffer" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4152116fd6e9dadb291ae18fc1ec3575ed6d84c29642d97890f4b4a3417297e4" +dependencies = [ + "generic-array", +] + [[package]] name = "bumpalo" version = "3.7.0" @@ -137,6 +146,19 @@ version = "1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" +[[package]] +name = "chrono" +version = "0.4.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "670ad68c9088c2a963aaa298cb369688cf3f9465ce5e2d4ca10e6e0098a1ce73" +dependencies = [ + "libc", + "num-integer", + "num-traits", + "time 0.1.44", + "winapi", +] + [[package]] name = "const_fn" version = "0.4.8" @@ -156,10 +178,19 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d5f1c7727e460397e56abc4bddc1d49e07a1ad78fc98eb2e1c8f032a58a2f80d" dependencies = [ "percent-encoding", - "time", + "time 0.2.27", "version_check", ] +[[package]] +name = "cpufeatures" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "66c99696f6c9dd7f35d486b9d04d7e6e202aa3e8c40d553f2fdf5e7e0c6a71ef" +dependencies = [ + "libc", +] + [[package]] name = "crossbeam-utils" version = "0.8.5" @@ -170,6 +201,16 @@ dependencies = [ "lazy_static", ] +[[package]] +name = "crypto-mac" +version = "0.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b1d1a86f49236c215f271d40892d5fc950490551400b02ef360692c29815c714" +dependencies = [ + "generic-array", + "subtle", +] + [[package]] name = "devise" version = "0.3.1" @@ -238,6 +279,15 @@ dependencies = [ "migrations_macros", ] +[[package]] +name = "digest" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d3dd60d1080a57a05ab032377049e0591415d2b31afd7028356dbf3cc6dcb066" +dependencies = [ + "generic-array", +] + [[package]] name = "discard" version = "1.0.4" @@ -401,6 +451,16 @@ dependencies = [ "winapi", ] +[[package]] +name = "generic-array" +version = "0.14.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "501466ecc8a30d1d3b7fc9229b122b2ce8ed6e9d9223f1138d4babb253e51817" +dependencies = [ + "typenum", + "version_check", +] + [[package]] name = "getrandom" version = "0.2.3" @@ -452,6 +512,16 @@ dependencies = [ "libc", ] +[[package]] +name = "hmac" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2a2a2320eb7ec0ebe8da8f744d7812d9fc4cb4d09344ac01898dbcb6a20ae69b" +dependencies = [ + "crypto-mac", + "digest", +] + [[package]] name = "http" version = "0.2.4" @@ -542,6 +612,21 @@ version = "0.4.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "dd25036021b0de88a0aff6b850051563c6516d0bf53f8638938edbb9de732736" +[[package]] +name = "jwt" +version = "0.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7487a802642fa9b162acacad3ed52c7e47ed4108d5fac6125cc7742dfaf622bf" +dependencies = [ + "base64", + "crypto-mac", + "digest", + "hmac", + "serde", + "serde_json", + "sha2", +] + [[package]] name = "lazy_static" version = "1.4.0" @@ -669,6 +754,25 @@ dependencies = [ "winapi", ] +[[package]] +name = "num-integer" +version = "0.1.44" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d2cc698a63b549a70bc047073d2949cce27cd1c7b0a4a862d08a8031bc2801db" +dependencies = [ + "autocfg", + "num-traits", +] + +[[package]] +name = "num-traits" +version = "0.2.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9a64b1ec5cda2586e284722486d802acf1f7dbdc623e2bfc57e65ca1cd099290" +dependencies = [ + "autocfg", +] + [[package]] name = "num_cpus" version = "1.13.0" @@ -685,6 +789,12 @@ version = "1.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "692fcb63b64b1758029e0a96ee63e049ce8c5948587f2f7208df04625e5f6b56" +[[package]] +name = "opaque-debug" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "624a8340c38c1b80fd549087862da4ba43e08858af025b236e509b6649fc13d5" + [[package]] name = "openssl" version = "0.10.36" @@ -961,7 +1071,7 @@ dependencies = [ "serde_json", "state", "tempfile", - "time", + "time 0.2.27", "tokio", "tokio-stream", "tokio-util", @@ -1009,7 +1119,7 @@ dependencies = [ "smallvec", "stable-pattern", "state", - "time", + "time 0.2.27", "tokio", "uncased", ] @@ -1069,14 +1179,18 @@ checksum = "61b3909d758bb75c79f23d4736fac9433868679d3ad2ea7a61e3c25cfda9a088" name = "rusty-bever" version = "0.1.0" dependencies = [ + "chrono", "diesel", "diesel_migrations", + "hmac", + "jwt", "openssl", "rand", "rocket", "rocket_sync_db_pools", "rust-argon2", "serde", + "sha2", "uuid", ] @@ -1159,6 +1273,19 @@ version = "0.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2579985fda508104f7587689507983eadd6a6e84dd35d6d115361f530916fa0d" +[[package]] +name = "sha2" +version = "0.9.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b362ae5752fd2137731f9fa25fd4d9058af34666ca1966fb969119cc35719f12" +dependencies = [ + "block-buffer", + "cfg-if", + "cpufeatures", + "digest", + "opaque-debug", +] + [[package]] name = "signal-hook-registry" version = "1.4.0" @@ -1272,6 +1399,12 @@ version = "0.1.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "213701ba3370744dcd1a12960caa4843b3d68b4d1c0a5d575e0d65b2ee9d16c0" +[[package]] +name = "subtle" +version = "2.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6bdef32e8150c2a081110b42772ffe7d7c9032b606bc226c8260fd97e0976601" + [[package]] name = "syn" version = "1.0.74" @@ -1297,6 +1430,17 @@ dependencies = [ "winapi", ] +[[package]] +name = "time" +version = "0.1.44" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6db9e6914ab8b1ae1c260a4ae7a49b6c5611b40328a735b21862567685e73255" +dependencies = [ + "libc", + "wasi", + "winapi", +] + [[package]] name = "time" version = "0.2.27" @@ -1441,6 +1585,12 @@ dependencies = [ "unchecked-index", ] +[[package]] +name = "typenum" +version = "1.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "879f6906492a7cd215bfa4cf595b600146ccfac0c79bcbd1f3000162af5e8b06" + [[package]] name = "ubyte" version = "0.10.1" @@ -1477,6 +1627,9 @@ name = "uuid" version = "0.8.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bc5cf98d8186244414c848017f0e2676b3fcb46807f6668a97dfe67359a3c4b7" +dependencies = [ + "serde", +] [[package]] name = "vcpkg" @@ -1502,9 +1655,9 @@ dependencies = [ [[package]] name = "wasi" -version = "0.10.2+wasi-snapshot-preview1" +version = "0.10.0+wasi-snapshot-preview1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fd6fbd9a79829dd1ad0cc20627bf1ed606756a7f77edff7b66b7064f9cb327c6" +checksum = "1a143597ca7c7793eff794def352d41792a93c481eb1042423ff7ff72ba2c31f" [[package]] name = "wasm-bindgen" diff --git a/Cargo.toml b/Cargo.toml index fb5cd1f..c669974 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -23,7 +23,11 @@ openssl = "0.10.36" # For password hashing & verification rust-argon2 = "0.8.3" rand = "0.8.4" -uuid = "0.8.2" +uuid = { version = "0.8.2", features = ["serde"] } +jwt = "0.14.0" +hmac = "*" +sha2 = "*" +chrono = "0.4.19" # Backend web framework [dependencies.rocket] diff --git a/src/rb/auth.rs b/src/rb/auth.rs index 2c8dbee..645cd08 100644 --- a/src/rb/auth.rs +++ b/src/rb/auth.rs @@ -4,6 +4,16 @@ use crate::schema::users::dsl as users; use argon2::verify_encoded; use diesel::prelude::*; use diesel::PgConnection; +use hmac::{Hmac, NewMac}; +use jwt::SignWithKey; +use sha2::Sha256; +use std::collections::HashMap; +use chrono::Utc; + +/// Expire time for the JWT tokens in seconds. +const JWT_EXP_SECONDS: i64 = 900; +/// Amount of bytes the refresh tokens should consist of +const REFRESH_TOKEN_N_BYTES: u32 = 64; pub fn verify_user(conn: &PgConnection, username: &str, password: &str) -> Result { // TODO handle non-"NotFound" Diesel errors accordingely @@ -20,3 +30,24 @@ pub fn verify_user(conn: &PgConnection, username: &str, password: &str) -> Resul _ => Err(AuthError::InvalidPassword), } } + +struct JWTResponse { + token: String, + refresh_token: String +} + +pub fn generate_jwt_token(conn: &PgConnection, user: &User) -> JWTResponse { + // TODO actually use proper secret here + // TODO don't just unwrap here + let key: Hmac = Hmac::new_from_slice(b"some-secret").unwrap(); + + // Create the claims + let mut claims = HashMap::new(); + claims.insert("id", user.id.to_string()); + claims.insert("username", user.username); + claims.insert("exp", (Utc::now().timestamp() + JWT_EXP_SECONDS).to_string()); + + // Sign the claims into a new token + // TODO don't just unwrap here + let token = claims.sign_with_key(&key).unwrap(); +} diff --git a/src/rb/models.rs b/src/rb/models.rs index d9455d6..0357366 100644 --- a/src/rb/models.rs +++ b/src/rb/models.rs @@ -1,11 +1,14 @@ use diesel::Queryable; use uuid::Uuid; +use serde::Serialize; -#[derive(Queryable)] +#[derive(Queryable, Serialize)] pub struct User { - id: Uuid, - username: String, + pub id: Uuid, + pub username: String, + #[serde(skip_serializing)] pub password: String, + #[serde(skip_serializing)] blocked: bool, admin: bool, } diff --git a/src/rbs/auth.rs b/src/rbs/auth.rs index 1c9d793..19f43a3 100644 --- a/src/rbs/auth.rs +++ b/src/rbs/auth.rs @@ -11,9 +11,12 @@ struct Credentials { #[post("/login", data = "")] async fn login(conn: RbDbConn, credentials: Json) { + let credentials = credentials.into_inner(); + let user = conn .run(move |c| verify_user(c, &credentials.username, &credentials.password)) .await; + user } // /refresh From 7a97b99bd69d6a1305174be0dd79d873e6d180f3 Mon Sep 17 00:00:00 2001 From: Jef Roosens Date: Sat, 21 Aug 2021 15:58:51 +0200 Subject: [PATCH 07/67] Added some basic error handling --- src/rb/auth.rs | 13 +++++-------- src/rb/errors.rs | 32 +++++++++++++++++++++++++++++++- src/rb/lib.rs | 2 ++ 3 files changed, 38 insertions(+), 9 deletions(-) diff --git a/src/rb/auth.rs b/src/rb/auth.rs index 645cd08..4fed292 100644 --- a/src/rb/auth.rs +++ b/src/rb/auth.rs @@ -1,4 +1,4 @@ -use crate::errors::AuthError; +use crate::errors::RBError; use crate::models::User; use crate::schema::users::dsl as users; use argon2::verify_encoded; @@ -15,19 +15,16 @@ const JWT_EXP_SECONDS: i64 = 900; /// Amount of bytes the refresh tokens should consist of const REFRESH_TOKEN_N_BYTES: u32 = 64; -pub fn verify_user(conn: &PgConnection, username: &str, password: &str) -> Result { +pub fn verify_user(conn: &PgConnection, username: &str, password: &str) -> crate::Result { // TODO handle non-"NotFound" Diesel errors accordingely - let user = match users::users + let user = users::users .filter(users::username.eq(username)) .first::(conn) - { - Err(_) => return Err(AuthError::UnknownUser), - Ok(user) => user, - }; + .map_err(|_| RBError::UnknownUser)?; match verify_encoded(user.password.as_str(), password.as_bytes()) { Ok(true) => Ok(user), - _ => Err(AuthError::InvalidPassword), + _ => Err(RBError::InvalidPassword), } } diff --git a/src/rb/errors.rs b/src/rb/errors.rs index fc12ba9..b9f387c 100644 --- a/src/rb/errors.rs +++ b/src/rb/errors.rs @@ -1,4 +1,34 @@ -pub enum AuthError { +use rocket::request::Request; +use rocket::response::{self, Response, Responder}; +use rocket::http::Status; +use std::io; + +pub enum RBError { + /// When the login requests an unknown user UnknownUser, + /// Invalid login password. InvalidPassword, + /// When a non-admin user tries to use an admin endpoint + Unauthorized, + /// When an expired JWT token is used for auth. + JWTTokenExpired } + +impl<'r> Responder<'r, 'static> for RBError { + fn respond_to(self, _: &'r Request<'_>) -> response::Result<'static> { + let (status, message): (Status, &str) = match self { + UnknownUser => (Status::NotFound, "Unknown user"), + InvalidPassword => (Status::Unauthorized, "Invalid password"), + Unauthorized => (Status::Unauthorized, "Unauthorized"), + JWTTokenExpired => (Status::Unauthorized, "Token expired"), + }; + + let res = Response::new(); + res.set_status(status); + res.set_sized_body(message.len(), io::Cursor::new(message)); + + Ok(res) + } +} + +pub type Result = std::result::Result; diff --git a/src/rb/lib.rs b/src/rb/lib.rs index 3d7d0f4..13c8330 100644 --- a/src/rb/lib.rs +++ b/src/rb/lib.rs @@ -5,3 +5,5 @@ pub mod auth; pub mod errors; mod models; pub(crate) mod schema; + +pub use errors::Result; From 9309ec77fb2c152ee2304cd479e64d3c4abe4c8a Mon Sep 17 00:00:00 2001 From: Jef Roosens Date: Sat, 21 Aug 2021 16:45:41 +0200 Subject: [PATCH 08/67] First JWT login implementation --- Cargo.lock | 1 + Cargo.toml | 1 + src/rb/auth.rs | 39 +++++++++++++++++++++++++++++---------- src/rb/errors.rs | 15 +++++++++------ src/rb/models.rs | 13 +++++++++++-- src/rbs/auth.rs | 12 ++++++++---- 6 files changed, 59 insertions(+), 22 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 8d50653..6b8acd9 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1179,6 +1179,7 @@ checksum = "61b3909d758bb75c79f23d4736fac9433868679d3ad2ea7a61e3c25cfda9a088" name = "rusty-bever" version = "0.1.0" dependencies = [ + "base64", "chrono", "diesel", "diesel_migrations", diff --git a/Cargo.toml b/Cargo.toml index c669974..e56cd9f 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -28,6 +28,7 @@ jwt = "0.14.0" hmac = "*" sha2 = "*" chrono = "0.4.19" +base64 = "0.13.0" # Backend web framework [dependencies.rocket] diff --git a/src/rb/auth.rs b/src/rb/auth.rs index 4fed292..11ed395 100644 --- a/src/rb/auth.rs +++ b/src/rb/auth.rs @@ -1,19 +1,22 @@ use crate::errors::RBError; -use crate::models::User; +use crate::models::{User, NewRefreshToken}; use crate::schema::users::dsl as users; +use crate::schema::refresh_tokens::dsl as refresh_tokens; use argon2::verify_encoded; use diesel::prelude::*; -use diesel::PgConnection; +use diesel::{PgConnection, insert_into}; use hmac::{Hmac, NewMac}; use jwt::SignWithKey; use sha2::Sha256; use std::collections::HashMap; use chrono::Utc; +use serde::Serialize; +use rand::{thread_rng, Rng}; /// Expire time for the JWT tokens in seconds. const JWT_EXP_SECONDS: i64 = 900; /// Amount of bytes the refresh tokens should consist of -const REFRESH_TOKEN_N_BYTES: u32 = 64; +const REFRESH_TOKEN_N_BYTES: usize = 64; pub fn verify_user(conn: &PgConnection, username: &str, password: &str) -> crate::Result { // TODO handle non-"NotFound" Diesel errors accordingely @@ -28,23 +31,39 @@ pub fn verify_user(conn: &PgConnection, username: &str, password: &str) -> crate } } -struct JWTResponse { +#[derive(Serialize)] +#[serde(rename_all = "camelCase")] +pub struct JWTResponse { token: String, refresh_token: String } -pub fn generate_jwt_token(conn: &PgConnection, user: &User) -> JWTResponse { +pub fn generate_jwt_token(conn: &PgConnection, user: &User) -> crate::Result { // TODO actually use proper secret here - // TODO don't just unwrap here - let key: Hmac = Hmac::new_from_slice(b"some-secret").unwrap(); + let key: Hmac = Hmac::new_from_slice(b"some-secret").map_err(|_| RBError::JWTCreationError)?; // Create the claims let mut claims = HashMap::new(); claims.insert("id", user.id.to_string()); - claims.insert("username", user.username); + claims.insert("username", user.username.clone()); + claims.insert("admin", user.admin.to_string()); claims.insert("exp", (Utc::now().timestamp() + JWT_EXP_SECONDS).to_string()); // Sign the claims into a new token - // TODO don't just unwrap here - let token = claims.sign_with_key(&key).unwrap(); + let token = claims.sign_with_key(&key).map_err(|_| RBError::JWTCreationError)?; + + // Generate a random refresh token + let mut refresh_token = [0u8; REFRESH_TOKEN_N_BYTES]; + thread_rng().fill(&mut refresh_token[..]); + + // Store refresh token in database + insert_into(refresh_tokens::refresh_tokens).values(NewRefreshToken { + token: refresh_token.to_vec(), + user_id: user.id + }).execute(conn).map_err(|_| RBError::JWTCreationError)?; + + Ok(JWTResponse { + token: token, + refresh_token: base64::encode(refresh_token) + }) } diff --git a/src/rb/errors.rs b/src/rb/errors.rs index b9f387c..adfe40b 100644 --- a/src/rb/errors.rs +++ b/src/rb/errors.rs @@ -11,19 +11,22 @@ pub enum RBError { /// When a non-admin user tries to use an admin endpoint Unauthorized, /// When an expired JWT token is used for auth. - JWTTokenExpired + JWTTokenExpired, + /// Umbrella error for when something goes wrong whilst creating a JWT token pair + JWTCreationError } impl<'r> Responder<'r, 'static> for RBError { fn respond_to(self, _: &'r Request<'_>) -> response::Result<'static> { let (status, message): (Status, &str) = match self { - UnknownUser => (Status::NotFound, "Unknown user"), - InvalidPassword => (Status::Unauthorized, "Invalid password"), - Unauthorized => (Status::Unauthorized, "Unauthorized"), - JWTTokenExpired => (Status::Unauthorized, "Token expired"), + RBError::UnknownUser => (Status::NotFound, "Unknown user"), + RBError::InvalidPassword => (Status::Unauthorized, "Invalid password"), + RBError::Unauthorized => (Status::Unauthorized, "Unauthorized"), + RBError::JWTTokenExpired => (Status::Unauthorized, "Token expired"), + RBError::JWTCreationError => (Status::InternalServerError, "Failed to create tokens."), }; - let res = Response::new(); + let mut res = Response::new(); res.set_status(status); res.set_sized_body(message.len(), io::Cursor::new(message)); diff --git a/src/rb/models.rs b/src/rb/models.rs index 0357366..1143e9b 100644 --- a/src/rb/models.rs +++ b/src/rb/models.rs @@ -1,6 +1,7 @@ -use diesel::Queryable; +use diesel::{Queryable, Insertable}; use uuid::Uuid; use serde::Serialize; +use crate::schema::refresh_tokens; #[derive(Queryable, Serialize)] pub struct User { @@ -10,5 +11,13 @@ pub struct User { pub password: String, #[serde(skip_serializing)] blocked: bool, - admin: bool, + pub admin: bool, +} + + +#[derive(Insertable)] +#[table_name = "refresh_tokens"] +pub struct NewRefreshToken { + pub token: Vec, + pub user_id: Uuid } diff --git a/src/rbs/auth.rs b/src/rbs/auth.rs index 19f43a3..9489933 100644 --- a/src/rbs/auth.rs +++ b/src/rbs/auth.rs @@ -1,5 +1,5 @@ use crate::RbDbConn; -use rb::auth::verify_user; +use rb::auth::{verify_user, JWTResponse, generate_jwt_token}; use rocket::serde::json::Json; use serde::Deserialize; @@ -9,14 +9,18 @@ struct Credentials { password: String, } +// TODO add catch for when user immediately requests new JWT token (they could totally spam this) + #[post("/login", data = "")] -async fn login(conn: RbDbConn, credentials: Json) { +async fn login(conn: RbDbConn, credentials: Json) -> rb::Result> { let credentials = credentials.into_inner(); + // Get the user, if credentials are valid let user = conn .run(move |c| verify_user(c, &credentials.username, &credentials.password)) - .await; - user + .await?; + + Ok(Json(conn.run(move |c| generate_jwt_token(c, &user)).await?)) } // /refresh From ac762a3c31929ce698abfd90bf086c2002bdb8a1 Mon Sep 17 00:00:00 2001 From: Jef Roosens Date: Sat, 21 Aug 2021 17:03:10 +0200 Subject: [PATCH 09/67] Mounted auth routes --- src/rbs/auth.rs | 4 ++++ src/rbs/main.rs | 1 + 2 files changed, 5 insertions(+) diff --git a/src/rbs/auth.rs b/src/rbs/auth.rs index 9489933..0de053a 100644 --- a/src/rbs/auth.rs +++ b/src/rbs/auth.rs @@ -3,6 +3,10 @@ use rb::auth::{verify_user, JWTResponse, generate_jwt_token}; use rocket::serde::json::Json; use serde::Deserialize; +pub(crate) fn routes() -> Vec { + routes![login] +} + #[derive(Deserialize)] struct Credentials { username: String, diff --git a/src/rbs/main.rs b/src/rbs/main.rs index 5921df7..0acc0a7 100644 --- a/src/rbs/main.rs +++ b/src/rbs/main.rs @@ -36,4 +36,5 @@ fn rocket() -> _ { "Run database migrations", run_db_migrations, )) + .mount("/auth", auth::routes()) } From 0d4d96d7614af9920e4800fd2e1de13bd72f9bcb Mon Sep 17 00:00:00 2001 From: Jef Roosens Date: Sat, 21 Aug 2021 18:05:16 +0200 Subject: [PATCH 10/67] Added very basic admin user creation --- src/rb/auth.rs | 75 +++++++++++++++++++++++++++++++++++++----------- src/rb/errors.rs | 12 ++++++-- src/rb/models.rs | 17 +++++++---- src/rbs/auth.rs | 2 +- src/rbs/main.rs | 23 +++++++++++++++ 5 files changed, 104 insertions(+), 25 deletions(-) diff --git a/src/rb/auth.rs b/src/rb/auth.rs index 11ed395..3d70159 100644 --- a/src/rb/auth.rs +++ b/src/rb/auth.rs @@ -1,17 +1,17 @@ use crate::errors::RBError; -use crate::models::{User, NewRefreshToken}; -use crate::schema::users::dsl as users; +use crate::models::{NewRefreshToken, User, NewUser}; use crate::schema::refresh_tokens::dsl as refresh_tokens; +use crate::schema::users::dsl as users; use argon2::verify_encoded; +use chrono::Utc; use diesel::prelude::*; -use diesel::{PgConnection, insert_into}; +use diesel::{insert_into, PgConnection}; use hmac::{Hmac, NewMac}; use jwt::SignWithKey; +use rand::{thread_rng, Rng}; +use serde::Serialize; use sha2::Sha256; use std::collections::HashMap; -use chrono::Utc; -use serde::Serialize; -use rand::{thread_rng, Rng}; /// Expire time for the JWT tokens in seconds. const JWT_EXP_SECONDS: i64 = 900; @@ -25,6 +25,11 @@ pub fn verify_user(conn: &PgConnection, username: &str, password: &str) -> crate .first::(conn) .map_err(|_| RBError::UnknownUser)?; + // Check if a user is blocked + if user.blocked { + return Err(RBError::BlockedUser); + } + match verify_encoded(user.password.as_str(), password.as_bytes()) { Ok(true) => Ok(user), _ => Err(RBError::InvalidPassword), @@ -35,35 +40,73 @@ pub fn verify_user(conn: &PgConnection, username: &str, password: &str) -> crate #[serde(rename_all = "camelCase")] pub struct JWTResponse { token: String, - refresh_token: String + refresh_token: String, } pub fn generate_jwt_token(conn: &PgConnection, user: &User) -> crate::Result { // TODO actually use proper secret here - let key: Hmac = Hmac::new_from_slice(b"some-secret").map_err(|_| RBError::JWTCreationError)?; + let key: Hmac = + Hmac::new_from_slice(b"some-secret").map_err(|_| RBError::JWTCreationError)?; // Create the claims let mut claims = HashMap::new(); claims.insert("id", user.id.to_string()); claims.insert("username", user.username.clone()); claims.insert("admin", user.admin.to_string()); - claims.insert("exp", (Utc::now().timestamp() + JWT_EXP_SECONDS).to_string()); + claims.insert( + "exp", + (Utc::now().timestamp() + JWT_EXP_SECONDS).to_string(), + ); // Sign the claims into a new token - let token = claims.sign_with_key(&key).map_err(|_| RBError::JWTCreationError)?; + let token = claims + .sign_with_key(&key) + .map_err(|_| RBError::JWTCreationError)?; // Generate a random refresh token let mut refresh_token = [0u8; REFRESH_TOKEN_N_BYTES]; thread_rng().fill(&mut refresh_token[..]); // Store refresh token in database - insert_into(refresh_tokens::refresh_tokens).values(NewRefreshToken { - token: refresh_token.to_vec(), - user_id: user.id - }).execute(conn).map_err(|_| RBError::JWTCreationError)?; + insert_into(refresh_tokens::refresh_tokens) + .values(NewRefreshToken { + token: refresh_token.to_vec(), + user_id: user.id, + }) + .execute(conn) + .map_err(|_| RBError::JWTCreationError)?; Ok(JWTResponse { - token: token, - refresh_token: base64::encode(refresh_token) + token, + refresh_token: base64::encode(refresh_token), }) } + +pub fn hash_password(password: &str) -> crate::Result { + // Generate a random salt + let mut salt = [0u8; 64]; + thread_rng().fill(&mut salt[..]); + + // Encode the actual password + let config = argon2::Config::default(); + argon2::hash_encoded(password.as_bytes(), &salt, &config).map_err(|_| RBError::PWSaltError) +} + +pub fn create_admin_user(conn: &PgConnection, username: &str, password: &str) -> crate::Result { + let pass_hashed = hash_password(password)?; + println!("{}", pass_hashed); + let new_user = NewUser { + username: username.to_string(), + password: pass_hashed, + admin: true, + }; + + insert_into(users::users) + .values(&new_user) + // .on_conflict((users::username, users::password, users::admin)) + // .do_update() + // .set(&new_user) + .execute(conn).map_err(|_| RBError::AdminCreationError)?; + + Ok(true) +} diff --git a/src/rb/errors.rs b/src/rb/errors.rs index adfe40b..96118e6 100644 --- a/src/rb/errors.rs +++ b/src/rb/errors.rs @@ -1,11 +1,13 @@ -use rocket::request::Request; -use rocket::response::{self, Response, Responder}; use rocket::http::Status; +use rocket::request::Request; +use rocket::response::{self, Responder, Response}; use std::io; +#[derive(Debug)] pub enum RBError { /// When the login requests an unknown user UnknownUser, + BlockedUser, /// Invalid login password. InvalidPassword, /// When a non-admin user tries to use an admin endpoint @@ -13,17 +15,21 @@ pub enum RBError { /// When an expired JWT token is used for auth. JWTTokenExpired, /// Umbrella error for when something goes wrong whilst creating a JWT token pair - JWTCreationError + JWTCreationError, + PWSaltError, + AdminCreationError, } impl<'r> Responder<'r, 'static> for RBError { fn respond_to(self, _: &'r Request<'_>) -> response::Result<'static> { let (status, message): (Status, &str) = match self { RBError::UnknownUser => (Status::NotFound, "Unknown user"), + RBError::BlockedUser => (Status::Unauthorized, "This user is blocked"), RBError::InvalidPassword => (Status::Unauthorized, "Invalid password"), RBError::Unauthorized => (Status::Unauthorized, "Unauthorized"), RBError::JWTTokenExpired => (Status::Unauthorized, "Token expired"), RBError::JWTCreationError => (Status::InternalServerError, "Failed to create tokens."), + _ => (Status::InternalServerError, "Internal server error") }; let mut res = Response::new(); diff --git a/src/rb/models.rs b/src/rb/models.rs index 1143e9b..858cab4 100644 --- a/src/rb/models.rs +++ b/src/rb/models.rs @@ -1,7 +1,7 @@ -use diesel::{Queryable, Insertable}; -use uuid::Uuid; +use crate::schema::{refresh_tokens, users}; +use diesel::{Insertable, Queryable, AsChangeset}; use serde::Serialize; -use crate::schema::refresh_tokens; +use uuid::Uuid; #[derive(Queryable, Serialize)] pub struct User { @@ -10,14 +10,21 @@ pub struct User { #[serde(skip_serializing)] pub password: String, #[serde(skip_serializing)] - blocked: bool, + pub blocked: bool, pub admin: bool, } +#[derive(Insertable, AsChangeset)] +#[table_name = "users"] +pub struct NewUser { + pub username: String, + pub password: String, + pub admin: bool, +} #[derive(Insertable)] #[table_name = "refresh_tokens"] pub struct NewRefreshToken { pub token: Vec, - pub user_id: Uuid + pub user_id: Uuid, } diff --git a/src/rbs/auth.rs b/src/rbs/auth.rs index 0de053a..39e3b88 100644 --- a/src/rbs/auth.rs +++ b/src/rbs/auth.rs @@ -1,5 +1,5 @@ use crate::RbDbConn; -use rb::auth::{verify_user, JWTResponse, generate_jwt_token}; +use rb::auth::{generate_jwt_token, verify_user, JWTResponse}; use rocket::serde::json::Json; use serde::Deserialize; diff --git a/src/rbs/main.rs b/src/rbs/main.rs index 0acc0a7..1792d9d 100644 --- a/src/rbs/main.rs +++ b/src/rbs/main.rs @@ -28,6 +28,25 @@ async fn run_db_migrations(rocket: Rocket) -> Result, Rocke .await } +async fn create_admin_user(rocket: Rocket) -> Result, Rocket> { + // In debug mode, the admin user is just a test user + let (admin_user, admin_password): (String, String); + + // if rocket.config().profile == "debug" { + admin_user = String::from("test"); + admin_password = String::from("test"); + // }else{ + // admin_user = std::env::var("ADMIN_USER").expect("no admin user provided"); + // admin_password = std::env::var("ADMIN_PASSWORD").expect("no admin password provided"); + // } + let conn = RbDbConn::get_one(&rocket) + .await + .expect("database connection"); + conn.run(move |c| rb::auth::create_admin_user(c, &admin_user, &admin_password).expect("failed to create admin user")).await; + + Ok(rocket) +} + #[launch] fn rocket() -> _ { rocket::build() @@ -36,5 +55,9 @@ fn rocket() -> _ { "Run database migrations", run_db_migrations, )) + .attach(AdHoc::try_on_ignite( + "Create admin user", + create_admin_user + )) .mount("/auth", auth::routes()) } From 13259249fd92f102789117e76b21daf4c145b4bb Mon Sep 17 00:00:00 2001 From: Jef Roosens Date: Sat, 21 Aug 2021 18:51:29 +0200 Subject: [PATCH 11/67] First successful JWT token request achieved --- Cargo.lock | 2 ++ Cargo.toml | 4 ++-- src/rb/auth.rs | 27 ++++++++++++++++++++------- src/rb/models.rs | 1 + 4 files changed, 25 insertions(+), 9 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 6b8acd9..a670689 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -155,6 +155,7 @@ dependencies = [ "libc", "num-integer", "num-traits", + "serde", "time 0.1.44", "winapi", ] @@ -252,6 +253,7 @@ checksum = "bba51ca66f57261fd17cadf8b73e4775cc307d0521d855de3f5de91a8f074e0e" dependencies = [ "bitflags", "byteorder", + "chrono", "diesel_derives", "pq-sys", "r2d2", diff --git a/Cargo.toml b/Cargo.toml index e56cd9f..676005d 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -16,7 +16,7 @@ path = "src/rbs/main.rs" [dependencies] # ORM -diesel = { version = "1.4.7", features = ["postgres", "uuidv07"] } +diesel = { version = "1.4.7", features = ["postgres", "uuidv07", "chrono"] } diesel_migrations = "1.4.0" # To properly compile libpq statically openssl = "0.10.36" @@ -27,7 +27,7 @@ uuid = { version = "0.8.2", features = ["serde"] } jwt = "0.14.0" hmac = "*" sha2 = "*" -chrono = "0.4.19" +chrono = { version = "*", features = [ "serde" ] } base64 = "0.13.0" # Backend web framework diff --git a/src/rb/auth.rs b/src/rb/auth.rs index 3d70159..094e5fe 100644 --- a/src/rb/auth.rs +++ b/src/rb/auth.rs @@ -17,6 +17,14 @@ use std::collections::HashMap; const JWT_EXP_SECONDS: i64 = 900; /// Amount of bytes the refresh tokens should consist of const REFRESH_TOKEN_N_BYTES: usize = 64; +/// Expire time for refresh tokens; here: one week +const REFRESH_TOKEN_EXP_SECONDS: i64 = 36288000; + +fn log(message: &str, o: T) -> T { + println!("{}", message); + + o +} pub fn verify_user(conn: &PgConnection, username: &str, password: &str) -> crate::Result { // TODO handle non-"NotFound" Diesel errors accordingely @@ -46,7 +54,9 @@ pub struct JWTResponse { pub fn generate_jwt_token(conn: &PgConnection, user: &User) -> crate::Result { // TODO actually use proper secret here let key: Hmac = - Hmac::new_from_slice(b"some-secret").map_err(|_| RBError::JWTCreationError)?; + Hmac::new_from_slice(b"some-secret").map_err(|_| log("Failed to create key", RBError::JWTCreationError))?; + + let current_time = Utc::now(); // Create the claims let mut claims = HashMap::new(); @@ -55,23 +65,27 @@ pub fn generate_jwt_token(conn: &PgConnection, user: &User) -> crate::Result crate::Result { pub fn create_admin_user(conn: &PgConnection, username: &str, password: &str) -> crate::Result { let pass_hashed = hash_password(password)?; - println!("{}", pass_hashed); let new_user = NewUser { username: username.to_string(), password: pass_hashed, @@ -103,9 +116,9 @@ pub fn create_admin_user(conn: &PgConnection, username: &str, password: &str) -> insert_into(users::users) .values(&new_user) - // .on_conflict((users::username, users::password, users::admin)) - // .do_update() - // .set(&new_user) + .on_conflict(users::username) + .do_update() + .set(&new_user) .execute(conn).map_err(|_| RBError::AdminCreationError)?; Ok(true) diff --git a/src/rb/models.rs b/src/rb/models.rs index 858cab4..49bd5b9 100644 --- a/src/rb/models.rs +++ b/src/rb/models.rs @@ -27,4 +27,5 @@ pub struct NewUser { pub struct NewRefreshToken { pub token: Vec, pub user_id: Uuid, + pub expires_at: chrono::NaiveDateTime } From 210eeee7b6f9d6f93b792a512815627c0e681205 Mon Sep 17 00:00:00 2001 From: Jef Roosens Date: Sat, 21 Aug 2021 20:22:19 +0200 Subject: [PATCH 12/67] Added default values to admin password --- src/rb/auth.rs | 2 +- src/rbs/main.rs | 15 ++++----------- 2 files changed, 5 insertions(+), 12 deletions(-) diff --git a/src/rb/auth.rs b/src/rb/auth.rs index 094e5fe..28bfa56 100644 --- a/src/rb/auth.rs +++ b/src/rb/auth.rs @@ -18,7 +18,7 @@ const JWT_EXP_SECONDS: i64 = 900; /// Amount of bytes the refresh tokens should consist of const REFRESH_TOKEN_N_BYTES: usize = 64; /// Expire time for refresh tokens; here: one week -const REFRESH_TOKEN_EXP_SECONDS: i64 = 36288000; +const REFRESH_TOKEN_EXP_SECONDS: i64 = 604800; fn log(message: &str, o: T) -> T { println!("{}", message); diff --git a/src/rbs/main.rs b/src/rbs/main.rs index 1792d9d..22657e5 100644 --- a/src/rbs/main.rs +++ b/src/rbs/main.rs @@ -1,5 +1,5 @@ // This needs to be explicitely included before diesel is imported to make sure -// compilation succeeds +// compilation succeeds in the release Docker image. extern crate openssl; #[macro_use] @@ -29,16 +29,9 @@ async fn run_db_migrations(rocket: Rocket) -> Result, Rocke } async fn create_admin_user(rocket: Rocket) -> Result, Rocket> { - // In debug mode, the admin user is just a test user - let (admin_user, admin_password): (String, String); + let admin_user = std::env::var("ADMIN_USER").unwrap_or(String::from("admin")); + let admin_password = std::env::var("ADMIN_PASSWORD").unwrap_or(String::from("password")); - // if rocket.config().profile == "debug" { - admin_user = String::from("test"); - admin_password = String::from("test"); - // }else{ - // admin_user = std::env::var("ADMIN_USER").expect("no admin user provided"); - // admin_password = std::env::var("ADMIN_PASSWORD").expect("no admin password provided"); - // } let conn = RbDbConn::get_one(&rocket) .await .expect("database connection"); @@ -59,5 +52,5 @@ fn rocket() -> _ { "Create admin user", create_admin_user )) - .mount("/auth", auth::routes()) + .mount("/api/auth", auth::routes()) } From 35fb38de9e7de10fc185f2e4dc464a811a97508f Mon Sep 17 00:00:00 2001 From: Jef Roosens Date: Sat, 21 Aug 2021 21:42:36 +0200 Subject: [PATCH 13/67] Added Admin & User guards --- src/rb/auth.rs | 62 +++++++++++++++++++++++++------------- src/rb/errors.rs | 8 +++-- src/rb/models.rs | 4 +-- src/rbs/auth.rs | 9 +++++- src/rbs/guards.rs | 77 +++++++++++++++++++++++++++++++++++++++++++++++ src/rbs/main.rs | 12 +++++--- 6 files changed, 141 insertions(+), 31 deletions(-) create mode 100644 src/rbs/guards.rs diff --git a/src/rb/auth.rs b/src/rb/auth.rs index 28bfa56..da60c78 100644 --- a/src/rb/auth.rs +++ b/src/rb/auth.rs @@ -1,5 +1,5 @@ use crate::errors::RBError; -use crate::models::{NewRefreshToken, User, NewUser}; +use crate::models::{NewRefreshToken, NewUser, User}; use crate::schema::refresh_tokens::dsl as refresh_tokens; use crate::schema::users::dsl as users; use argon2::verify_encoded; @@ -9,7 +9,7 @@ use diesel::{insert_into, PgConnection}; use hmac::{Hmac, NewMac}; use jwt::SignWithKey; use rand::{thread_rng, Rng}; -use serde::Serialize; +use serde::{Deserialize, Serialize}; use sha2::Sha256; use std::collections::HashMap; @@ -51,22 +51,36 @@ pub struct JWTResponse { refresh_token: String, } +#[derive(Serialize, Deserialize)] +pub struct Claims { + pub id: uuid::Uuid, + pub username: String, + pub admin: bool, + pub exp: i64, +} + pub fn generate_jwt_token(conn: &PgConnection, user: &User) -> crate::Result { - // TODO actually use proper secret here - let key: Hmac = - Hmac::new_from_slice(b"some-secret").map_err(|_| log("Failed to create key", RBError::JWTCreationError))?; + let secret = std::env::var("JWT_KEY").map_err(|_| RBError::MissingJWTKey)?; + let key: Hmac = Hmac::new_from_slice(secret.as_bytes()) + .map_err(|_| log("Failed to create key", RBError::JWTCreationError))?; let current_time = Utc::now(); // Create the claims - let mut claims = HashMap::new(); - claims.insert("id", user.id.to_string()); - claims.insert("username", user.username.clone()); - claims.insert("admin", user.admin.to_string()); - claims.insert( - "exp", - (current_time.timestamp() + JWT_EXP_SECONDS).to_string(), - ); + let claims = Claims { + id: user.id, + username: user.username.clone(), + admin: user.admin, + exp: current_time.timestamp() + JWT_EXP_SECONDS, + }; + // let mut claims = HashMap::new(); + // claims.insert("id", user.id.to_string()); + // claims.insert("username", user.username.clone()); + // claims.insert("admin", user.admin.to_string()); + // claims.insert( + // "exp", + // (current_time.timestamp() + JWT_EXP_SECONDS).to_string(), + // ); // Sign the claims into a new token let token = claims @@ -77,7 +91,8 @@ pub fn generate_jwt_token(conn: &PgConnection, user: &User) -> crate::Result crate::Result crate::Result { argon2::hash_encoded(password.as_bytes(), &salt, &config).map_err(|_| RBError::PWSaltError) } -pub fn create_admin_user(conn: &PgConnection, username: &str, password: &str) -> crate::Result { +pub fn create_admin_user( + conn: &PgConnection, + username: &str, + password: &str, +) -> crate::Result { let pass_hashed = hash_password(password)?; let new_user = NewUser { - username: username.to_string(), - password: pass_hashed, - admin: true, - }; + username: username.to_string(), + password: pass_hashed, + admin: true, + }; insert_into(users::users) .values(&new_user) .on_conflict(users::username) .do_update() .set(&new_user) - .execute(conn).map_err(|_| RBError::AdminCreationError)?; + .execute(conn) + .map_err(|_| RBError::AdminCreationError)?; Ok(true) } diff --git a/src/rb/errors.rs b/src/rb/errors.rs index 96118e6..34bfe8f 100644 --- a/src/rb/errors.rs +++ b/src/rb/errors.rs @@ -16,6 +16,8 @@ pub enum RBError { JWTTokenExpired, /// Umbrella error for when something goes wrong whilst creating a JWT token pair JWTCreationError, + JWTError, + MissingJWTKey, PWSaltError, AdminCreationError, } @@ -28,8 +30,10 @@ impl<'r> Responder<'r, 'static> for RBError { RBError::InvalidPassword => (Status::Unauthorized, "Invalid password"), RBError::Unauthorized => (Status::Unauthorized, "Unauthorized"), RBError::JWTTokenExpired => (Status::Unauthorized, "Token expired"), - RBError::JWTCreationError => (Status::InternalServerError, "Failed to create tokens."), - _ => (Status::InternalServerError, "Internal server error") + RBError::JWTCreationError | RBError::MissingJWTKey => { + (Status::InternalServerError, "Failed to create tokens.") + } + _ => (Status::InternalServerError, "Internal server error"), }; let mut res = Response::new(); diff --git a/src/rb/models.rs b/src/rb/models.rs index 49bd5b9..2df5f89 100644 --- a/src/rb/models.rs +++ b/src/rb/models.rs @@ -1,5 +1,5 @@ use crate::schema::{refresh_tokens, users}; -use diesel::{Insertable, Queryable, AsChangeset}; +use diesel::{AsChangeset, Insertable, Queryable}; use serde::Serialize; use uuid::Uuid; @@ -27,5 +27,5 @@ pub struct NewUser { pub struct NewRefreshToken { pub token: Vec, pub user_id: Uuid, - pub expires_at: chrono::NaiveDateTime + pub expires_at: chrono::NaiveDateTime, } diff --git a/src/rbs/auth.rs b/src/rbs/auth.rs index 39e3b88..65d5255 100644 --- a/src/rbs/auth.rs +++ b/src/rbs/auth.rs @@ -2,11 +2,13 @@ use crate::RbDbConn; use rb::auth::{generate_jwt_token, verify_user, JWTResponse}; use rocket::serde::json::Json; use serde::Deserialize; +use crate::guards::User; pub(crate) fn routes() -> Vec { - routes![login] + routes![login, me] } + #[derive(Deserialize)] struct Credentials { username: String, @@ -27,5 +29,10 @@ async fn login(conn: RbDbConn, credentials: Json) -> rb::Result String { + String::from("You are logged in!") +} + // /refresh // /logout diff --git a/src/rbs/guards.rs b/src/rbs/guards.rs new file mode 100644 index 0000000..95e3ef0 --- /dev/null +++ b/src/rbs/guards.rs @@ -0,0 +1,77 @@ +use rocket::{ + http::Status, + outcome::try_outcome, + request::{FromRequest, Outcome, Request} +}; +use hmac::{Hmac, NewMac}; +use jwt::VerifyWithKey; +use rb::auth::Claims; +use sha2::Sha256; + +pub struct User(Claims); + +#[rocket::async_trait] +impl<'r> FromRequest<'r> for User { + type Error = rb::errors::RBError; + + async fn from_request(req: &'r Request<'_>) -> Outcome { + // If the header isn't present, just forward to the next route + let header = match req.headers().get_one("Authorization") { + None => return Outcome::Forward(()), + Some(val) => val, + }; + + if !header.starts_with("Bearer ") { + return Outcome::Forward(()); + } + + // Extract the jwt token from the header + let jwt_token = match header.get(7..) { + Some(token) => token, + None => return Outcome::Failure((Status::Unauthorized, Self::Error::JWTError)), + }; + + // Get secret & key + let secret = match std::env::var("JWT_KEY") { + Ok(key) => key, + Err(_) => { + return Outcome::Failure((Status::InternalServerError, Self::Error::MissingJWTKey)) + } + }; + let key: Hmac = match Hmac::new_from_slice(secret.as_bytes()) { + Ok(key) => key, + Err(_) => { + return Outcome::Failure((Status::InternalServerError, Self::Error::JWTError)) + } + }; + + // Verify token using key + let claims: Claims = match jwt_token.verify_with_key(&key) { + Ok(claims) => claims, + Err(_) => return Outcome::Failure((Status::Unauthorized, Self::Error::Unauthorized)), + }; + + // Verify key hasn't yet expired + if chrono::Utc::now().timestamp() > claims.exp { + return Outcome::Failure((Status::Unauthorized, Self::Error::Unauthorized)); + } + + Outcome::Success(Self(claims)) + } +} + +pub struct Admin(Claims); + +#[rocket::async_trait] +impl<'r> FromRequest<'r> for Admin { + type Error = rb::errors::RBError; + + async fn from_request(req: &'r Request<'_>) -> Outcome { + let user = try_outcome!(req.guard::().await); + if user.0.admin { + Outcome::Success(Self(user.0)) + } else { + Outcome::Forward(()) + } + } +} diff --git a/src/rbs/main.rs b/src/rbs/main.rs index 22657e5..7e9ea26 100644 --- a/src/rbs/main.rs +++ b/src/rbs/main.rs @@ -11,6 +11,7 @@ use rocket::{fairing::AdHoc, Build, Rocket}; use rocket_sync_db_pools::{database, diesel}; mod auth; +pub(crate) mod guards; embed_migrations!(); @@ -35,7 +36,11 @@ async fn create_admin_user(rocket: Rocket) -> Result, Rocke let conn = RbDbConn::get_one(&rocket) .await .expect("database connection"); - conn.run(move |c| rb::auth::create_admin_user(c, &admin_user, &admin_password).expect("failed to create admin user")).await; + conn.run(move |c| { + rb::auth::create_admin_user(c, &admin_user, &admin_password) + .expect("failed to create admin user") + }) + .await; Ok(rocket) } @@ -48,9 +53,6 @@ fn rocket() -> _ { "Run database migrations", run_db_migrations, )) - .attach(AdHoc::try_on_ignite( - "Create admin user", - create_admin_user - )) + .attach(AdHoc::try_on_ignite("Create admin user", create_admin_user)) .mount("/api/auth", auth::routes()) } From dab90bc4a9d160a0c3f072cdae003b214dc7a174 Mon Sep 17 00:00:00 2001 From: Jef Roosens Date: Sat, 21 Aug 2021 22:21:42 +0200 Subject: [PATCH 14/67] Separated JWT header into own guard --- Rocket.toml | 2 +- src/rb/auth.rs | 32 +++++--------------------------- src/rb/lib.rs | 8 ++++++++ src/rbs/guards.rs | 19 ++++++++++++++++--- 4 files changed, 30 insertions(+), 31 deletions(-) diff --git a/Rocket.toml b/Rocket.toml index 081ed68..e931e4d 100644 --- a/Rocket.toml +++ b/Rocket.toml @@ -10,4 +10,4 @@ limits = { forms = 32768 } postgres_rb = { url = "postgres://rb:rb@localhost:5432/rb" } [release.databases] -postgres_rb = { url = "postgres://rb:rb@db:5432/rb" } +postgres_rb = { url = "postgres://rb:rb@localhost:5432/rb" } diff --git a/src/rb/auth.rs b/src/rb/auth.rs index da60c78..4951e1c 100644 --- a/src/rb/auth.rs +++ b/src/rb/auth.rs @@ -11,20 +11,6 @@ use jwt::SignWithKey; use rand::{thread_rng, Rng}; use serde::{Deserialize, Serialize}; use sha2::Sha256; -use std::collections::HashMap; - -/// Expire time for the JWT tokens in seconds. -const JWT_EXP_SECONDS: i64 = 900; -/// Amount of bytes the refresh tokens should consist of -const REFRESH_TOKEN_N_BYTES: usize = 64; -/// Expire time for refresh tokens; here: one week -const REFRESH_TOKEN_EXP_SECONDS: i64 = 604800; - -fn log(message: &str, o: T) -> T { - println!("{}", message); - - o -} pub fn verify_user(conn: &PgConnection, username: &str, password: &str) -> crate::Result { // TODO handle non-"NotFound" Diesel errors accordingely @@ -62,7 +48,7 @@ pub struct Claims { pub fn generate_jwt_token(conn: &PgConnection, user: &User) -> crate::Result { let secret = std::env::var("JWT_KEY").map_err(|_| RBError::MissingJWTKey)?; let key: Hmac = Hmac::new_from_slice(secret.as_bytes()) - .map_err(|_| log("Failed to create key", RBError::JWTCreationError))?; + .map_err(|_| RBError::JWTCreationError)?; let current_time = Utc::now(); @@ -71,28 +57,20 @@ pub fn generate_jwt_token(conn: &PgConnection, user: &User) -> crate::Result FromRequest<'r> for User { +impl<'r> FromRequest<'r> for JWT { type Error = rb::errors::RBError; async fn from_request(req: &'r Request<'_>) -> Outcome { @@ -28,9 +28,22 @@ impl<'r> FromRequest<'r> for User { // Extract the jwt token from the header let jwt_token = match header.get(7..) { Some(token) => token, - None => return Outcome::Failure((Status::Unauthorized, Self::Error::JWTError)), + None => return Outcome::Forward(()), }; + Outcome::Success(Self(jwt_token.to_string())) + } +} + +pub struct User(Claims); + +#[rocket::async_trait] +impl<'r> FromRequest<'r> for User { + type Error = rb::errors::RBError; + + async fn from_request(req: &'r Request<'_>) -> Outcome { + let jwt_token = try_outcome!(req.guard::().await).0; + // Get secret & key let secret = match std::env::var("JWT_KEY") { Ok(key) => key, From 89851a201845c4bd493d20ea4aaab627379f8515 Mon Sep 17 00:00:00 2001 From: Jef Roosens Date: Sat, 21 Aug 2021 22:41:38 +0200 Subject: [PATCH 15/67] Further split guards --- src/rb/auth.rs | 4 ++-- src/rbs/auth.rs | 12 +++++++----- src/rbs/guards.rs | 48 +++++++++++++++++++++++++++++++---------------- 3 files changed, 41 insertions(+), 23 deletions(-) diff --git a/src/rb/auth.rs b/src/rb/auth.rs index 4951e1c..18d3753 100644 --- a/src/rb/auth.rs +++ b/src/rb/auth.rs @@ -47,8 +47,8 @@ pub struct Claims { pub fn generate_jwt_token(conn: &PgConnection, user: &User) -> crate::Result { let secret = std::env::var("JWT_KEY").map_err(|_| RBError::MissingJWTKey)?; - let key: Hmac = Hmac::new_from_slice(secret.as_bytes()) - .map_err(|_| RBError::JWTCreationError)?; + let key: Hmac = + Hmac::new_from_slice(secret.as_bytes()).map_err(|_| RBError::JWTCreationError)?; let current_time = Utc::now(); diff --git a/src/rbs/auth.rs b/src/rbs/auth.rs index 65d5255..56fcb8f 100644 --- a/src/rbs/auth.rs +++ b/src/rbs/auth.rs @@ -1,23 +1,25 @@ +use crate::guards::User; use crate::RbDbConn; use rb::auth::{generate_jwt_token, verify_user, JWTResponse}; use rocket::serde::json::Json; use serde::Deserialize; -use crate::guards::User; pub(crate) fn routes() -> Vec { - routes![login, me] + routes![login, already_logged_in, me] } - #[derive(Deserialize)] struct Credentials { username: String, password: String, } -// TODO add catch for when user immediately requests new JWT token (they could totally spam this) +#[post("/login")] +async fn already_logged_in(_user: User) -> String { + String::from("You're already logged in!") +} -#[post("/login", data = "")] +#[post("/login", data = "", rank = 2)] async fn login(conn: RbDbConn, credentials: Json) -> rb::Result> { let credentials = credentials.into_inner(); diff --git a/src/rbs/guards.rs b/src/rbs/guards.rs index 96891d9..25d74a5 100644 --- a/src/rbs/guards.rs +++ b/src/rbs/guards.rs @@ -1,17 +1,18 @@ -use rocket::{ - http::Status, - outcome::try_outcome, - request::{FromRequest, Outcome, Request} -}; use hmac::{Hmac, NewMac}; use jwt::VerifyWithKey; use rb::auth::Claims; +use rocket::{ + http::Status, + outcome::try_outcome, + request::{FromRequest, Outcome, Request}, +}; use sha2::Sha256; -pub struct JWT(String); +/// Extracts a "Authorization: Bearer" string from the headers. +pub struct Bearer(String); #[rocket::async_trait] -impl<'r> FromRequest<'r> for JWT { +impl<'r> FromRequest<'r> for Bearer { type Error = rb::errors::RBError; async fn from_request(req: &'r Request<'_>) -> Outcome { @@ -26,23 +27,24 @@ impl<'r> FromRequest<'r> for JWT { } // Extract the jwt token from the header - let jwt_token = match header.get(7..) { - Some(token) => token, + let auth_string = match header.get(7..) { + Some(s) => s, None => return Outcome::Forward(()), }; - Outcome::Success(Self(jwt_token.to_string())) + Outcome::Success(Self(auth_string.to_string())) } } -pub struct User(Claims); +/// Verifies the provided JWT is valid. +pub struct JWT(Claims); #[rocket::async_trait] -impl<'r> FromRequest<'r> for User { +impl<'r> FromRequest<'r> for JWT { type Error = rb::errors::RBError; async fn from_request(req: &'r Request<'_>) -> Outcome { - let jwt_token = try_outcome!(req.guard::().await).0; + let bearer = try_outcome!(req.guard::().await).0; // Get secret & key let secret = match std::env::var("JWT_KEY") { @@ -57,22 +59,36 @@ impl<'r> FromRequest<'r> for User { return Outcome::Failure((Status::InternalServerError, Self::Error::JWTError)) } }; - // Verify token using key - let claims: Claims = match jwt_token.verify_with_key(&key) { + let claims: Claims = match bearer.verify_with_key(&key) { Ok(claims) => claims, Err(_) => return Outcome::Failure((Status::Unauthorized, Self::Error::Unauthorized)), }; + Outcome::Success(Self(claims)) + } +} + +/// Verifies the JWT has not expired. +pub struct User(Claims); + +#[rocket::async_trait] +impl<'r> FromRequest<'r> for User { + type Error = rb::errors::RBError; + + async fn from_request(req: &'r Request<'_>) -> Outcome { + let claims = try_outcome!(req.guard::().await).0; + // Verify key hasn't yet expired if chrono::Utc::now().timestamp() > claims.exp { - return Outcome::Failure((Status::Unauthorized, Self::Error::Unauthorized)); + return Outcome::Failure((Status::Forbidden, Self::Error::TokenExpired)); } Outcome::Success(Self(claims)) } } +/// Verifies the JWT belongs to an admin. pub struct Admin(Claims); #[rocket::async_trait] From cc7a668ab0d00d84148bf31a8822eaac704ec37e Mon Sep 17 00:00:00 2001 From: Jef Roosens Date: Sat, 21 Aug 2021 22:41:38 +0200 Subject: [PATCH 16/67] Further split guards --- src/rb/auth.rs | 4 ++-- src/rb/errors.rs | 1 + src/rbs/auth.rs | 20 ++++++++------------ src/rbs/guards.rs | 48 +++++++++++++++++++++++++++++++---------------- 4 files changed, 43 insertions(+), 30 deletions(-) diff --git a/src/rb/auth.rs b/src/rb/auth.rs index 4951e1c..18d3753 100644 --- a/src/rb/auth.rs +++ b/src/rb/auth.rs @@ -47,8 +47,8 @@ pub struct Claims { pub fn generate_jwt_token(conn: &PgConnection, user: &User) -> crate::Result { let secret = std::env::var("JWT_KEY").map_err(|_| RBError::MissingJWTKey)?; - let key: Hmac = Hmac::new_from_slice(secret.as_bytes()) - .map_err(|_| RBError::JWTCreationError)?; + let key: Hmac = + Hmac::new_from_slice(secret.as_bytes()).map_err(|_| RBError::JWTCreationError)?; let current_time = Utc::now(); diff --git a/src/rb/errors.rs b/src/rb/errors.rs index 34bfe8f..9d048f3 100644 --- a/src/rb/errors.rs +++ b/src/rb/errors.rs @@ -20,6 +20,7 @@ pub enum RBError { MissingJWTKey, PWSaltError, AdminCreationError, + TokenExpired, } impl<'r> Responder<'r, 'static> for RBError { diff --git a/src/rbs/auth.rs b/src/rbs/auth.rs index 65d5255..2ff7561 100644 --- a/src/rbs/auth.rs +++ b/src/rbs/auth.rs @@ -1,23 +1,25 @@ +use crate::guards::User; use crate::RbDbConn; use rb::auth::{generate_jwt_token, verify_user, JWTResponse}; use rocket::serde::json::Json; use serde::Deserialize; -use crate::guards::User; pub(crate) fn routes() -> Vec { - routes![login, me] + routes![login, already_logged_in, me] } - #[derive(Deserialize)] struct Credentials { username: String, password: String, } -// TODO add catch for when user immediately requests new JWT token (they could totally spam this) +#[post("/login")] +async fn already_logged_in(_user: User) -> String { + String::from("You're already logged in!") +} -#[post("/login", data = "")] +#[post("/login", data = "", rank = 2)] async fn login(conn: RbDbConn, credentials: Json) -> rb::Result> { let credentials = credentials.into_inner(); @@ -29,10 +31,4 @@ async fn login(conn: RbDbConn, credentials: Json) -> rb::Result String { - String::from("You are logged in!") -} - -// /refresh -// /logout +// #[post("/refresh", data=)] diff --git a/src/rbs/guards.rs b/src/rbs/guards.rs index 96891d9..25d74a5 100644 --- a/src/rbs/guards.rs +++ b/src/rbs/guards.rs @@ -1,17 +1,18 @@ -use rocket::{ - http::Status, - outcome::try_outcome, - request::{FromRequest, Outcome, Request} -}; use hmac::{Hmac, NewMac}; use jwt::VerifyWithKey; use rb::auth::Claims; +use rocket::{ + http::Status, + outcome::try_outcome, + request::{FromRequest, Outcome, Request}, +}; use sha2::Sha256; -pub struct JWT(String); +/// Extracts a "Authorization: Bearer" string from the headers. +pub struct Bearer(String); #[rocket::async_trait] -impl<'r> FromRequest<'r> for JWT { +impl<'r> FromRequest<'r> for Bearer { type Error = rb::errors::RBError; async fn from_request(req: &'r Request<'_>) -> Outcome { @@ -26,23 +27,24 @@ impl<'r> FromRequest<'r> for JWT { } // Extract the jwt token from the header - let jwt_token = match header.get(7..) { - Some(token) => token, + let auth_string = match header.get(7..) { + Some(s) => s, None => return Outcome::Forward(()), }; - Outcome::Success(Self(jwt_token.to_string())) + Outcome::Success(Self(auth_string.to_string())) } } -pub struct User(Claims); +/// Verifies the provided JWT is valid. +pub struct JWT(Claims); #[rocket::async_trait] -impl<'r> FromRequest<'r> for User { +impl<'r> FromRequest<'r> for JWT { type Error = rb::errors::RBError; async fn from_request(req: &'r Request<'_>) -> Outcome { - let jwt_token = try_outcome!(req.guard::().await).0; + let bearer = try_outcome!(req.guard::().await).0; // Get secret & key let secret = match std::env::var("JWT_KEY") { @@ -57,22 +59,36 @@ impl<'r> FromRequest<'r> for User { return Outcome::Failure((Status::InternalServerError, Self::Error::JWTError)) } }; - // Verify token using key - let claims: Claims = match jwt_token.verify_with_key(&key) { + let claims: Claims = match bearer.verify_with_key(&key) { Ok(claims) => claims, Err(_) => return Outcome::Failure((Status::Unauthorized, Self::Error::Unauthorized)), }; + Outcome::Success(Self(claims)) + } +} + +/// Verifies the JWT has not expired. +pub struct User(Claims); + +#[rocket::async_trait] +impl<'r> FromRequest<'r> for User { + type Error = rb::errors::RBError; + + async fn from_request(req: &'r Request<'_>) -> Outcome { + let claims = try_outcome!(req.guard::().await).0; + // Verify key hasn't yet expired if chrono::Utc::now().timestamp() > claims.exp { - return Outcome::Failure((Status::Unauthorized, Self::Error::Unauthorized)); + return Outcome::Failure((Status::Forbidden, Self::Error::TokenExpired)); } Outcome::Success(Self(claims)) } } +/// Verifies the JWT belongs to an admin. pub struct Admin(Claims); #[rocket::async_trait] From 7dffbb9597829dbea39baeee0815d4c39584cffd Mon Sep 17 00:00:00 2001 From: Jef Roosens Date: Sun, 22 Aug 2021 10:42:58 +0200 Subject: [PATCH 17/67] First draft of token refresh --- .../2021-08-20-110251_users-and-auth/up.sql | 2 +- src/rb/auth.rs | 35 ++++++++++++++++++- src/rb/errors.rs | 4 +++ src/rb/models.rs | 8 +++++ src/rbs/auth.rs | 19 ++++++++-- 5 files changed, 64 insertions(+), 4 deletions(-) diff --git a/migrations/2021-08-20-110251_users-and-auth/up.sql b/migrations/2021-08-20-110251_users-and-auth/up.sql index 7ffd9f1..4341778 100644 --- a/migrations/2021-08-20-110251_users-and-auth/up.sql +++ b/migrations/2021-08-20-110251_users-and-auth/up.sql @@ -15,7 +15,7 @@ CREATE TABLE refresh_tokens ( -- This is more efficient than storing the text token bytea PRIMARY KEY, -- The user for whom the token was created - user_id uuid NOT NULL REFERENCES users(id), + user_id uuid NOT NULL REFERENCES users(id) ON DELETE CASCADE, -- When the token expires expires_at timestamp NOT NULL, -- When the token was last used (is NULL until used) diff --git a/src/rb/auth.rs b/src/rb/auth.rs index 18d3753..36f5bb9 100644 --- a/src/rb/auth.rs +++ b/src/rb/auth.rs @@ -1,5 +1,5 @@ use crate::errors::RBError; -use crate::models::{NewRefreshToken, NewUser, User}; +use crate::models::{NewRefreshToken, NewUser, RefreshToken, User}; use crate::schema::refresh_tokens::dsl as refresh_tokens; use crate::schema::users::dsl as users; use argon2::verify_encoded; @@ -121,3 +121,36 @@ pub fn create_admin_user( Ok(true) } + +pub fn refresh_token(conn: &PgConnection, refresh_token: &str) -> crate::Result { + let token_bytes = base64::decode(refresh_token).map_err(|_| RBError::InvalidRefreshToken)?; + + // First, we request the token from the database to see if it's really a valid token + let token_entry = refresh_tokens::refresh_tokens + .filter(refresh_tokens::token.eq(token_bytes)) + .first::(conn) + .map_err(|_| RBError::InvalidRefreshToken)?; + + // If we see that the token has already been used before, we block the user. + if token_entry.last_used_at.is_some() { + let target = users::users.filter(users::id.eq(token_entry.user_id)); + diesel::update(target) + .set(users::blocked.eq(true)) + .execute(conn) + .map_err(|_| RBError::DBError)?; + + return Err(RBError::DuplicateRefreshToken); + } + + // We update the last_used_at value for the refresh token + let target = refresh_tokens::refresh_tokens.filter(refresh_tokens::token.eq(token_entry.token)); + diesel::update(target) + .set(refresh_tokens::last_used_at.eq(Utc::now().naive_utc())) + .execute(conn) + .map_err(|_| RBError::DBError)?; + + // Finally, we query the new user & generate a new token + let user = users::users.filter(users::id.eq(token_entry.user_id)).first::(conn).map_err(|_| RBError::DBError)?; + + generate_jwt_token(conn, &user) +} diff --git a/src/rb/errors.rs b/src/rb/errors.rs index 9d048f3..b5b01a9 100644 --- a/src/rb/errors.rs +++ b/src/rb/errors.rs @@ -21,6 +21,9 @@ pub enum RBError { PWSaltError, AdminCreationError, TokenExpired, + InvalidRefreshToken, + DuplicateRefreshToken, + DBError, } impl<'r> Responder<'r, 'static> for RBError { @@ -34,6 +37,7 @@ impl<'r> Responder<'r, 'static> for RBError { RBError::JWTCreationError | RBError::MissingJWTKey => { (Status::InternalServerError, "Failed to create tokens.") } + RBError::InvalidRefreshToken | RBError::DuplicateRefreshToken => (Status::Unauthorized, "Invalid refresh token."), _ => (Status::InternalServerError, "Internal server error"), }; diff --git a/src/rb/models.rs b/src/rb/models.rs index 2df5f89..81b34ca 100644 --- a/src/rb/models.rs +++ b/src/rb/models.rs @@ -22,6 +22,14 @@ pub struct NewUser { pub admin: bool, } +#[derive(Queryable)] +pub struct RefreshToken { + pub token: Vec, + pub user_id: Uuid, + pub expires_at: chrono::NaiveDateTime, + pub last_used_at: Option, +} + #[derive(Insertable)] #[table_name = "refresh_tokens"] pub struct NewRefreshToken { diff --git a/src/rbs/auth.rs b/src/rbs/auth.rs index 2ff7561..f9ffe55 100644 --- a/src/rbs/auth.rs +++ b/src/rbs/auth.rs @@ -5,7 +5,7 @@ use rocket::serde::json::Json; use serde::Deserialize; pub(crate) fn routes() -> Vec { - routes![login, already_logged_in, me] + routes![login, already_logged_in, refresh_token] } #[derive(Deserialize)] @@ -31,4 +31,19 @@ async fn login(conn: RbDbConn, credentials: Json) -> rb::Result, +) -> rb::Result> { + let refresh_token = refresh_token_request.into_inner().refresh_token; + + Ok(Json( + conn.run(move |c| rb::auth::refresh_token(c, &refresh_token)), + )) +} From d7333373bb161d70bc425271d8058c6a7e9b7bfe Mon Sep 17 00:00:00 2001 From: Jef Roosens Date: Sun, 22 Aug 2021 13:14:19 +0200 Subject: [PATCH 18/67] Token refresh works! --- src/rb/auth.rs | 17 +++++++++++------ src/rb/lib.rs | 2 +- src/rbs/auth.rs | 3 ++- 3 files changed, 14 insertions(+), 8 deletions(-) diff --git a/src/rb/auth.rs b/src/rb/auth.rs index 36f5bb9..6bc03c3 100644 --- a/src/rb/auth.rs +++ b/src/rb/auth.rs @@ -126,9 +126,10 @@ pub fn refresh_token(conn: &PgConnection, refresh_token: &str) -> crate::Result< let token_bytes = base64::decode(refresh_token).map_err(|_| RBError::InvalidRefreshToken)?; // First, we request the token from the database to see if it's really a valid token - let token_entry = refresh_tokens::refresh_tokens + let (token_entry, user) = refresh_tokens::refresh_tokens + .inner_join(users::users) .filter(refresh_tokens::token.eq(token_bytes)) - .first::(conn) + .first::<(RefreshToken, User)>(conn) .map_err(|_| RBError::InvalidRefreshToken)?; // If we see that the token has already been used before, we block the user. @@ -142,15 +143,19 @@ pub fn refresh_token(conn: &PgConnection, refresh_token: &str) -> crate::Result< return Err(RBError::DuplicateRefreshToken); } + // Now we check if the token has already expired + let cur_time = Utc::now().naive_utc(); + + if token_entry.expires_at < cur_time { + return Err(RBError::TokenExpired); + } + // We update the last_used_at value for the refresh token let target = refresh_tokens::refresh_tokens.filter(refresh_tokens::token.eq(token_entry.token)); diesel::update(target) - .set(refresh_tokens::last_used_at.eq(Utc::now().naive_utc())) + .set(refresh_tokens::last_used_at.eq(cur_time)) .execute(conn) .map_err(|_| RBError::DBError)?; - // Finally, we query the new user & generate a new token - let user = users::users.filter(users::id.eq(token_entry.user_id)).first::(conn).map_err(|_| RBError::DBError)?; - generate_jwt_token(conn, &user) } diff --git a/src/rb/lib.rs b/src/rb/lib.rs index e3ca322..457665d 100644 --- a/src/rb/lib.rs +++ b/src/rb/lib.rs @@ -10,7 +10,7 @@ pub use errors::Result; // Any import defaults are defined here /// Expire time for the JWT tokens in seconds. -const JWT_EXP_SECONDS: i64 = 900; +const JWT_EXP_SECONDS: i64 = 600; /// Amount of bytes the refresh tokens should consist of const REFRESH_TOKEN_N_BYTES: usize = 64; /// Expire time for refresh tokens; here: one week diff --git a/src/rbs/auth.rs b/src/rbs/auth.rs index f9ffe55..119d973 100644 --- a/src/rbs/auth.rs +++ b/src/rbs/auth.rs @@ -32,6 +32,7 @@ async fn login(conn: RbDbConn, credentials: Json) -> rb::Result Date: Sun, 22 Aug 2021 15:50:58 +0200 Subject: [PATCH 19/67] Added admin users route --- src/rb/admin.rs | 8 ++++++++ src/rb/errors.rs | 4 +++- src/rb/lib.rs | 3 ++- src/rbs/admin.rs | 13 +++++++++++++ src/rbs/auth.rs | 3 ++- src/rbs/main.rs | 2 ++ 6 files changed, 30 insertions(+), 3 deletions(-) create mode 100644 src/rb/admin.rs create mode 100644 src/rbs/admin.rs diff --git a/src/rb/admin.rs b/src/rb/admin.rs new file mode 100644 index 0000000..352f086 --- /dev/null +++ b/src/rb/admin.rs @@ -0,0 +1,8 @@ +use crate::errors::RBError; +use crate::models::User; +use diesel::prelude::*; +use crate::schema::users::dsl as users; + +pub fn get_users(conn: &PgConnection) -> crate::Result> { + users::users.load::(conn).map_err(|_| RBError::DBError) +} diff --git a/src/rb/errors.rs b/src/rb/errors.rs index b5b01a9..2e1afca 100644 --- a/src/rb/errors.rs +++ b/src/rb/errors.rs @@ -37,7 +37,9 @@ impl<'r> Responder<'r, 'static> for RBError { RBError::JWTCreationError | RBError::MissingJWTKey => { (Status::InternalServerError, "Failed to create tokens.") } - RBError::InvalidRefreshToken | RBError::DuplicateRefreshToken => (Status::Unauthorized, "Invalid refresh token."), + RBError::InvalidRefreshToken | RBError::DuplicateRefreshToken => { + (Status::Unauthorized, "Invalid refresh token.") + } _ => (Status::InternalServerError, "Internal server error"), }; diff --git a/src/rb/lib.rs b/src/rb/lib.rs index 457665d..c1b0c4a 100644 --- a/src/rb/lib.rs +++ b/src/rb/lib.rs @@ -3,7 +3,8 @@ extern crate diesel; pub mod auth; pub mod errors; -mod models; +pub mod admin; +pub mod models; pub(crate) mod schema; pub use errors::Result; diff --git a/src/rbs/admin.rs b/src/rbs/admin.rs new file mode 100644 index 0000000..9bd9d7b --- /dev/null +++ b/src/rbs/admin.rs @@ -0,0 +1,13 @@ +use crate::guards::Admin; +use crate::RbDbConn; +use rb::models::User; +use rocket::serde::json::Json; + +pub fn routes() -> Vec { + routes![get_users] +} + +#[get("/users")] +async fn get_users(admin: Admin, conn: RbDbConn) -> rb::Result>> { + Ok(Json(conn.run(|c| rb::admin::get_users(c)).await?)) +} diff --git a/src/rbs/auth.rs b/src/rbs/auth.rs index 119d973..903b52f 100644 --- a/src/rbs/auth.rs +++ b/src/rbs/auth.rs @@ -45,6 +45,7 @@ async fn refresh_token( let refresh_token = refresh_token_request.into_inner().refresh_token; Ok(Json( - conn.run(move |c| rb::auth::refresh_token(c, &refresh_token)).await? + conn.run(move |c| rb::auth::refresh_token(c, &refresh_token)) + .await?, )) } diff --git a/src/rbs/main.rs b/src/rbs/main.rs index 7e9ea26..38e03d8 100644 --- a/src/rbs/main.rs +++ b/src/rbs/main.rs @@ -11,6 +11,7 @@ use rocket::{fairing::AdHoc, Build, Rocket}; use rocket_sync_db_pools::{database, diesel}; mod auth; +mod admin; pub(crate) mod guards; embed_migrations!(); @@ -55,4 +56,5 @@ fn rocket() -> _ { )) .attach(AdHoc::try_on_ignite("Create admin user", create_admin_user)) .mount("/api/auth", auth::routes()) + .mount("/api/admin", admin::routes()) } From b13b760e2f9898d6a3062b8d52ecd9b3bf24c8c8 Mon Sep 17 00:00:00 2001 From: Jef Roosens Date: Sun, 22 Aug 2021 16:24:59 +0200 Subject: [PATCH 20/67] Completely restructured codebase --- Cargo.toml | 6 +++--- diesel.toml | 2 +- src/{rb => }/auth.rs | 5 ++++- src/db/mod.rs | 2 ++ src/db/tokens.rs | 20 ++++++++++++++++++ src/db/users.rs | 29 +++++++++++++++++++++++++++ src/{rb => }/errors.rs | 0 src/{rbs => }/guards.rs | 0 src/{rb => }/lib.rs | 3 +-- src/{rbs => }/main.rs | 7 +++---- src/rb/admin.rs | 8 -------- src/rb/models.rs | 39 ------------------------------------ src/{rbs => routes}/admin.rs | 4 ++-- src/{rbs => routes}/auth.rs | 0 src/routes/mod.rs | 2 ++ src/{rb => }/schema.rs | 0 16 files changed, 67 insertions(+), 60 deletions(-) rename src/{rb => }/auth.rs (98%) create mode 100644 src/db/mod.rs create mode 100644 src/db/tokens.rs create mode 100644 src/db/users.rs rename src/{rb => }/errors.rs (100%) rename src/{rbs => }/guards.rs (100%) rename src/{rb => }/lib.rs (93%) rename src/{rbs => }/main.rs (93%) delete mode 100644 src/rb/admin.rs delete mode 100644 src/rb/models.rs rename src/{rbs => routes}/admin.rs (75%) rename src/{rbs => routes}/auth.rs (100%) create mode 100644 src/routes/mod.rs rename src/{rb => }/schema.rs (100%) diff --git a/Cargo.toml b/Cargo.toml index 676005d..c51d32c 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -6,11 +6,11 @@ edition = "2018" [lib] name = "rb" -path = "src/rb/lib.rs" +path = "src/lib.rs" [[bin]] -name = "rbs" -path = "src/rbs/main.rs" +name = "rbd" +path = "src/main.rs" # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html diff --git a/diesel.toml b/diesel.toml index 332da6a..92267c8 100644 --- a/diesel.toml +++ b/diesel.toml @@ -2,4 +2,4 @@ # see diesel.rs/guides/configuring-diesel-cli [print_schema] -file = "src/rb/schema.rs" +file = "src/schema.rs" diff --git a/src/rb/auth.rs b/src/auth.rs similarity index 98% rename from src/rb/auth.rs rename to src/auth.rs index 6bc03c3..c010674 100644 --- a/src/rb/auth.rs +++ b/src/auth.rs @@ -1,5 +1,8 @@ use crate::errors::RBError; -use crate::models::{NewRefreshToken, NewUser, RefreshToken, User}; +use crate::db::{ + users::{User, NewUser}, + tokens::{RefreshToken, NewRefreshToken} +}; use crate::schema::refresh_tokens::dsl as refresh_tokens; use crate::schema::users::dsl as users; use argon2::verify_encoded; diff --git a/src/db/mod.rs b/src/db/mod.rs new file mode 100644 index 0000000..f65c924 --- /dev/null +++ b/src/db/mod.rs @@ -0,0 +1,2 @@ +pub mod users; +pub mod tokens; diff --git a/src/db/tokens.rs b/src/db/tokens.rs new file mode 100644 index 0000000..25011d8 --- /dev/null +++ b/src/db/tokens.rs @@ -0,0 +1,20 @@ +use uuid::Uuid; +use diesel::{Queryable, Insertable}; +use crate::schema::refresh_tokens; + + +#[derive(Queryable)] +pub struct RefreshToken { + pub token: Vec, + pub user_id: Uuid, + pub expires_at: chrono::NaiveDateTime, + pub last_used_at: Option, +} + +#[derive(Insertable)] +#[table_name = "refresh_tokens"] +pub struct NewRefreshToken { + pub token: Vec, + pub user_id: Uuid, + pub expires_at: chrono::NaiveDateTime, +} diff --git a/src/db/users.rs b/src/db/users.rs new file mode 100644 index 0000000..72fc125 --- /dev/null +++ b/src/db/users.rs @@ -0,0 +1,29 @@ +use crate::schema::users; +use diesel::{AsChangeset, Insertable, Queryable, prelude::*}; +use serde::Serialize; +use uuid::Uuid; +use crate::schema::users::dsl::*; +use crate::errors::RBError; + +#[derive(Queryable, Serialize)] +pub struct User { + pub id: Uuid, + pub username: String, + #[serde(skip_serializing)] + pub password: String, + #[serde(skip_serializing)] + pub blocked: bool, + pub admin: bool, +} + +#[derive(Insertable, AsChangeset)] +#[table_name = "users"] +pub struct NewUser { + pub username: String, + pub password: String, + pub admin: bool, +} + +pub fn all(conn: &PgConnection) -> crate::Result> { + users.load::(conn).map_err(|_| RBError::DBError) +} diff --git a/src/rb/errors.rs b/src/errors.rs similarity index 100% rename from src/rb/errors.rs rename to src/errors.rs diff --git a/src/rbs/guards.rs b/src/guards.rs similarity index 100% rename from src/rbs/guards.rs rename to src/guards.rs diff --git a/src/rb/lib.rs b/src/lib.rs similarity index 93% rename from src/rb/lib.rs rename to src/lib.rs index c1b0c4a..6d408d8 100644 --- a/src/rb/lib.rs +++ b/src/lib.rs @@ -1,10 +1,9 @@ #[macro_use] extern crate diesel; +pub mod db; pub mod auth; pub mod errors; -pub mod admin; -pub mod models; pub(crate) mod schema; pub use errors::Result; diff --git a/src/rbs/main.rs b/src/main.rs similarity index 93% rename from src/rbs/main.rs rename to src/main.rs index 38e03d8..7bb9081 100644 --- a/src/rbs/main.rs +++ b/src/main.rs @@ -10,9 +10,8 @@ extern crate diesel_migrations; use rocket::{fairing::AdHoc, Build, Rocket}; use rocket_sync_db_pools::{database, diesel}; -mod auth; -mod admin; pub(crate) mod guards; +mod routes; embed_migrations!(); @@ -55,6 +54,6 @@ fn rocket() -> _ { run_db_migrations, )) .attach(AdHoc::try_on_ignite("Create admin user", create_admin_user)) - .mount("/api/auth", auth::routes()) - .mount("/api/admin", admin::routes()) + .mount("/api/auth", routes::auth::routes()) + .mount("/api/admin", routes::admin::routes()) } diff --git a/src/rb/admin.rs b/src/rb/admin.rs deleted file mode 100644 index 352f086..0000000 --- a/src/rb/admin.rs +++ /dev/null @@ -1,8 +0,0 @@ -use crate::errors::RBError; -use crate::models::User; -use diesel::prelude::*; -use crate::schema::users::dsl as users; - -pub fn get_users(conn: &PgConnection) -> crate::Result> { - users::users.load::(conn).map_err(|_| RBError::DBError) -} diff --git a/src/rb/models.rs b/src/rb/models.rs deleted file mode 100644 index 81b34ca..0000000 --- a/src/rb/models.rs +++ /dev/null @@ -1,39 +0,0 @@ -use crate::schema::{refresh_tokens, users}; -use diesel::{AsChangeset, Insertable, Queryable}; -use serde::Serialize; -use uuid::Uuid; - -#[derive(Queryable, Serialize)] -pub struct User { - pub id: Uuid, - pub username: String, - #[serde(skip_serializing)] - pub password: String, - #[serde(skip_serializing)] - pub blocked: bool, - pub admin: bool, -} - -#[derive(Insertable, AsChangeset)] -#[table_name = "users"] -pub struct NewUser { - pub username: String, - pub password: String, - pub admin: bool, -} - -#[derive(Queryable)] -pub struct RefreshToken { - pub token: Vec, - pub user_id: Uuid, - pub expires_at: chrono::NaiveDateTime, - pub last_used_at: Option, -} - -#[derive(Insertable)] -#[table_name = "refresh_tokens"] -pub struct NewRefreshToken { - pub token: Vec, - pub user_id: Uuid, - pub expires_at: chrono::NaiveDateTime, -} diff --git a/src/rbs/admin.rs b/src/routes/admin.rs similarity index 75% rename from src/rbs/admin.rs rename to src/routes/admin.rs index 9bd9d7b..84ad6a4 100644 --- a/src/rbs/admin.rs +++ b/src/routes/admin.rs @@ -1,6 +1,6 @@ use crate::guards::Admin; use crate::RbDbConn; -use rb::models::User; +use rb::db::users::User; use rocket::serde::json::Json; pub fn routes() -> Vec { @@ -9,5 +9,5 @@ pub fn routes() -> Vec { #[get("/users")] async fn get_users(admin: Admin, conn: RbDbConn) -> rb::Result>> { - Ok(Json(conn.run(|c| rb::admin::get_users(c)).await?)) + Ok(Json(conn.run(|c| rb::db::users::all(c)).await?)) } diff --git a/src/rbs/auth.rs b/src/routes/auth.rs similarity index 100% rename from src/rbs/auth.rs rename to src/routes/auth.rs diff --git a/src/routes/mod.rs b/src/routes/mod.rs new file mode 100644 index 0000000..78b1e23 --- /dev/null +++ b/src/routes/mod.rs @@ -0,0 +1,2 @@ +pub mod auth; +pub mod admin; diff --git a/src/rb/schema.rs b/src/schema.rs similarity index 100% rename from src/rb/schema.rs rename to src/schema.rs From 16ddc9aecdbecd29b48229ad82209aa89075b6bf Mon Sep 17 00:00:00 2001 From: Jef Roosens Date: Sun, 22 Aug 2021 16:45:01 +0200 Subject: [PATCH 21/67] Configured Rustfmt --- rustfmt.toml | 69 +++++++++++++++++++++++++++++++++++++++++++++ src/auth.rs | 45 ++++++++++++++++------------- src/db/mod.rs | 2 +- src/db/tokens.rs | 10 ++++--- src/db/users.rs | 19 ++++++++----- src/errors.rs | 18 ++++++++---- src/guards.rs | 24 ++++++++++------ src/lib.rs | 2 +- src/main.rs | 9 ++++-- src/routes/admin.rs | 10 ++++--- src/routes/auth.rs | 22 +++++++++------ src/routes/mod.rs | 2 +- 12 files changed, 169 insertions(+), 63 deletions(-) create mode 100644 rustfmt.toml diff --git a/rustfmt.toml b/rustfmt.toml new file mode 100644 index 0000000..03acab0 --- /dev/null +++ b/rustfmt.toml @@ -0,0 +1,69 @@ +binop_separator = "Front" +blank_lines_lower_bound = 0 +blank_lines_upper_bound = 1 +# Trying something new +brace_style = "AlwaysNextLine" +color = "Auto" +combine_control_expr = false +comment_width = 80 +condense_wildcard_suffixes = false +control_brace_style = "AlwaysSameLine" +disable_all_formatting = false +edition = "2018" +emit_mode = "Files" +empty_item_single_line = true +enum_discrim_align_threshold = 0 +error_on_line_overflow = false +error_on_unformatted = false +fn_args_layout = "Tall" +fn_single_line = false +force_explicit_abi = true +force_multiline_blocks = false +format_code_in_doc_comments = false +format_macro_bodies = true +format_macro_matchers = false +format_strings = false +group_imports = "StdExternalCrate" +hard_tabs = false +hide_parse_errors = false +ignore = [] +imports_granularity = "Crate" +imports_indent = "Block" +imports_layout = "Mixed" +indent_style = "Block" +inline_attribute_width = 0 +license_template_path = "" +make_backup = false +match_arm_blocks = true +match_arm_leading_pipes = "Never" +match_block_trailing_comma = false +max_width = 100 +merge_derives = true +newline_style = "Auto" +normalize_comments = false +normalize_doc_attributes = false +overflow_delimited_expr = false +remove_nested_parens = true +reorder_impl_items = false +reorder_imports = true +reorder_modules = true +report_fixme = "Always" +report_todo = "Always" +required_version = "1.4.36" +skip_children = false +space_after_colon = true +space_before_colon = false +spaces_around_ranges = false +struct_field_align_threshold = 0 +struct_lit_single_line = true +tab_spaces = 4 +trailing_comma = "Vertical" +trailing_semicolon = true +type_punctuation_density = "Wide" +unstable_features = false +use_field_init_shorthand = false +use_small_heuristics = "Default" +use_try_shorthand = false +version = "One" +where_single_line = false +wrap_comments = false diff --git a/src/auth.rs b/src/auth.rs index c010674..613c210 100644 --- a/src/auth.rs +++ b/src/auth.rs @@ -1,21 +1,23 @@ -use crate::errors::RBError; -use crate::db::{ - users::{User, NewUser}, - tokens::{RefreshToken, NewRefreshToken} -}; -use crate::schema::refresh_tokens::dsl as refresh_tokens; -use crate::schema::users::dsl as users; use argon2::verify_encoded; use chrono::Utc; -use diesel::prelude::*; -use diesel::{insert_into, PgConnection}; +use diesel::{insert_into, prelude::*, PgConnection}; use hmac::{Hmac, NewMac}; use jwt::SignWithKey; use rand::{thread_rng, Rng}; use serde::{Deserialize, Serialize}; use sha2::Sha256; -pub fn verify_user(conn: &PgConnection, username: &str, password: &str) -> crate::Result { +use crate::{ + db::{ + tokens::{NewRefreshToken, RefreshToken}, + users::{NewUser, User}, + }, + errors::RBError, + schema::{refresh_tokens::dsl as refresh_tokens, users::dsl as users}, +}; + +pub fn verify_user(conn: &PgConnection, username: &str, password: &str) -> crate::Result +{ // TODO handle non-"NotFound" Diesel errors accordingely let user = users::users .filter(users::username.eq(username)) @@ -35,20 +37,23 @@ pub fn verify_user(conn: &PgConnection, username: &str, password: &str) -> crate #[derive(Serialize)] #[serde(rename_all = "camelCase")] -pub struct JWTResponse { +pub struct JWTResponse +{ token: String, refresh_token: String, } #[derive(Serialize, Deserialize)] -pub struct Claims { +pub struct Claims +{ pub id: uuid::Uuid, pub username: String, pub admin: bool, pub exp: i64, } -pub fn generate_jwt_token(conn: &PgConnection, user: &User) -> crate::Result { +pub fn generate_jwt_token(conn: &PgConnection, user: &User) -> crate::Result +{ let secret = std::env::var("JWT_KEY").map_err(|_| RBError::MissingJWTKey)?; let key: Hmac = Hmac::new_from_slice(secret.as_bytes()).map_err(|_| RBError::JWTCreationError)?; @@ -92,7 +97,8 @@ pub fn generate_jwt_token(conn: &PgConnection, user: &User) -> crate::Result crate::Result { +pub fn hash_password(password: &str) -> crate::Result +{ // Generate a random salt let mut salt = [0u8; 64]; thread_rng().fill(&mut salt[..]); @@ -102,11 +108,9 @@ pub fn hash_password(password: &str) -> crate::Result { argon2::hash_encoded(password.as_bytes(), &salt, &config).map_err(|_| RBError::PWSaltError) } -pub fn create_admin_user( - conn: &PgConnection, - username: &str, - password: &str, -) -> crate::Result { +pub fn create_admin_user(conn: &PgConnection, username: &str, password: &str) + -> crate::Result +{ let pass_hashed = hash_password(password)?; let new_user = NewUser { username: username.to_string(), @@ -125,7 +129,8 @@ pub fn create_admin_user( Ok(true) } -pub fn refresh_token(conn: &PgConnection, refresh_token: &str) -> crate::Result { +pub fn refresh_token(conn: &PgConnection, refresh_token: &str) -> crate::Result +{ let token_bytes = base64::decode(refresh_token).map_err(|_| RBError::InvalidRefreshToken)?; // First, we request the token from the database to see if it's really a valid token diff --git a/src/db/mod.rs b/src/db/mod.rs index f65c924..bf3b714 100644 --- a/src/db/mod.rs +++ b/src/db/mod.rs @@ -1,2 +1,2 @@ -pub mod users; pub mod tokens; +pub mod users; diff --git a/src/db/tokens.rs b/src/db/tokens.rs index 25011d8..cfe5904 100644 --- a/src/db/tokens.rs +++ b/src/db/tokens.rs @@ -1,10 +1,11 @@ +use diesel::{Insertable, Queryable}; use uuid::Uuid; -use diesel::{Queryable, Insertable}; + use crate::schema::refresh_tokens; - #[derive(Queryable)] -pub struct RefreshToken { +pub struct RefreshToken +{ pub token: Vec, pub user_id: Uuid, pub expires_at: chrono::NaiveDateTime, @@ -13,7 +14,8 @@ pub struct RefreshToken { #[derive(Insertable)] #[table_name = "refresh_tokens"] -pub struct NewRefreshToken { +pub struct NewRefreshToken +{ pub token: Vec, pub user_id: Uuid, pub expires_at: chrono::NaiveDateTime, diff --git a/src/db/users.rs b/src/db/users.rs index 72fc125..7b5a642 100644 --- a/src/db/users.rs +++ b/src/db/users.rs @@ -1,12 +1,15 @@ -use crate::schema::users; -use diesel::{AsChangeset, Insertable, Queryable, prelude::*}; +use diesel::{prelude::*, AsChangeset, Insertable, Queryable}; use serde::Serialize; use uuid::Uuid; -use crate::schema::users::dsl::*; -use crate::errors::RBError; + +use crate::{ + errors::RBError, + schema::{users, users::dsl::*}, +}; #[derive(Queryable, Serialize)] -pub struct User { +pub struct User +{ pub id: Uuid, pub username: String, #[serde(skip_serializing)] @@ -18,12 +21,14 @@ pub struct User { #[derive(Insertable, AsChangeset)] #[table_name = "users"] -pub struct NewUser { +pub struct NewUser +{ pub username: String, pub password: String, pub admin: bool, } -pub fn all(conn: &PgConnection) -> crate::Result> { +pub fn all(conn: &PgConnection) -> crate::Result> +{ users.load::(conn).map_err(|_| RBError::DBError) } diff --git a/src/errors.rs b/src/errors.rs index 2e1afca..bc3243f 100644 --- a/src/errors.rs +++ b/src/errors.rs @@ -1,10 +1,14 @@ -use rocket::http::Status; -use rocket::request::Request; -use rocket::response::{self, Responder, Response}; use std::io; +use rocket::{ + http::Status, + request::Request, + response::{self, Responder, Response}, +}; + #[derive(Debug)] -pub enum RBError { +pub enum RBError +{ /// When the login requests an unknown user UnknownUser, BlockedUser, @@ -26,8 +30,10 @@ pub enum RBError { DBError, } -impl<'r> Responder<'r, 'static> for RBError { - fn respond_to(self, _: &'r Request<'_>) -> response::Result<'static> { +impl<'r> Responder<'r, 'static> for RBError +{ + fn respond_to(self, _: &'r Request<'_>) -> response::Result<'static> + { let (status, message): (Status, &str) = match self { RBError::UnknownUser => (Status::NotFound, "Unknown user"), RBError::BlockedUser => (Status::Unauthorized, "This user is blocked"), diff --git a/src/guards.rs b/src/guards.rs index 25d74a5..d475eb4 100644 --- a/src/guards.rs +++ b/src/guards.rs @@ -12,10 +12,12 @@ use sha2::Sha256; pub struct Bearer(String); #[rocket::async_trait] -impl<'r> FromRequest<'r> for Bearer { +impl<'r> FromRequest<'r> for Bearer +{ type Error = rb::errors::RBError; - async fn from_request(req: &'r Request<'_>) -> Outcome { + async fn from_request(req: &'r Request<'_>) -> Outcome + { // If the header isn't present, just forward to the next route let header = match req.headers().get_one("Authorization") { None => return Outcome::Forward(()), @@ -40,10 +42,12 @@ impl<'r> FromRequest<'r> for Bearer { pub struct JWT(Claims); #[rocket::async_trait] -impl<'r> FromRequest<'r> for JWT { +impl<'r> FromRequest<'r> for JWT +{ type Error = rb::errors::RBError; - async fn from_request(req: &'r Request<'_>) -> Outcome { + async fn from_request(req: &'r Request<'_>) -> Outcome + { let bearer = try_outcome!(req.guard::().await).0; // Get secret & key @@ -73,10 +77,12 @@ impl<'r> FromRequest<'r> for JWT { pub struct User(Claims); #[rocket::async_trait] -impl<'r> FromRequest<'r> for User { +impl<'r> FromRequest<'r> for User +{ type Error = rb::errors::RBError; - async fn from_request(req: &'r Request<'_>) -> Outcome { + async fn from_request(req: &'r Request<'_>) -> Outcome + { let claims = try_outcome!(req.guard::().await).0; // Verify key hasn't yet expired @@ -92,10 +98,12 @@ impl<'r> FromRequest<'r> for User { pub struct Admin(Claims); #[rocket::async_trait] -impl<'r> FromRequest<'r> for Admin { +impl<'r> FromRequest<'r> for Admin +{ type Error = rb::errors::RBError; - async fn from_request(req: &'r Request<'_>) -> Outcome { + async fn from_request(req: &'r Request<'_>) -> Outcome + { let user = try_outcome!(req.guard::().await); if user.0.admin { Outcome::Success(Self(user.0)) diff --git a/src/lib.rs b/src/lib.rs index 6d408d8..2c7d7f1 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,8 +1,8 @@ #[macro_use] extern crate diesel; -pub mod db; pub mod auth; +pub mod db; pub mod errors; pub(crate) mod schema; diff --git a/src/main.rs b/src/main.rs index 7bb9081..2ca414d 100644 --- a/src/main.rs +++ b/src/main.rs @@ -18,7 +18,8 @@ embed_migrations!(); #[database("postgres_rb")] pub struct RbDbConn(diesel::PgConnection); -async fn run_db_migrations(rocket: Rocket) -> Result, Rocket> { +async fn run_db_migrations(rocket: Rocket) -> Result, Rocket> +{ let conn = RbDbConn::get_one(&rocket) .await .expect("database connection"); @@ -29,7 +30,8 @@ async fn run_db_migrations(rocket: Rocket) -> Result, Rocke .await } -async fn create_admin_user(rocket: Rocket) -> Result, Rocket> { +async fn create_admin_user(rocket: Rocket) -> Result, Rocket> +{ let admin_user = std::env::var("ADMIN_USER").unwrap_or(String::from("admin")); let admin_password = std::env::var("ADMIN_PASSWORD").unwrap_or(String::from("password")); @@ -46,7 +48,8 @@ async fn create_admin_user(rocket: Rocket) -> Result, Rocke } #[launch] -fn rocket() -> _ { +fn rocket() -> _ +{ rocket::build() .attach(RbDbConn::fairing()) .attach(AdHoc::try_on_ignite( diff --git a/src/routes/admin.rs b/src/routes/admin.rs index 84ad6a4..db3ecc2 100644 --- a/src/routes/admin.rs +++ b/src/routes/admin.rs @@ -1,13 +1,15 @@ -use crate::guards::Admin; -use crate::RbDbConn; use rb::db::users::User; use rocket::serde::json::Json; -pub fn routes() -> Vec { +use crate::{guards::Admin, RbDbConn}; + +pub fn routes() -> Vec +{ routes![get_users] } #[get("/users")] -async fn get_users(admin: Admin, conn: RbDbConn) -> rb::Result>> { +async fn get_users(admin: Admin, conn: RbDbConn) -> rb::Result>> +{ Ok(Json(conn.run(|c| rb::db::users::all(c)).await?)) } diff --git a/src/routes/auth.rs b/src/routes/auth.rs index 903b52f..9551ace 100644 --- a/src/routes/auth.rs +++ b/src/routes/auth.rs @@ -1,26 +1,30 @@ -use crate::guards::User; -use crate::RbDbConn; use rb::auth::{generate_jwt_token, verify_user, JWTResponse}; use rocket::serde::json::Json; use serde::Deserialize; -pub(crate) fn routes() -> Vec { +use crate::{guards::User, RbDbConn}; + +pub(crate) fn routes() -> Vec +{ routes![login, already_logged_in, refresh_token] } #[derive(Deserialize)] -struct Credentials { +struct Credentials +{ username: String, password: String, } #[post("/login")] -async fn already_logged_in(_user: User) -> String { +async fn already_logged_in(_user: User) -> String +{ String::from("You're already logged in!") } #[post("/login", data = "", rank = 2)] -async fn login(conn: RbDbConn, credentials: Json) -> rb::Result> { +async fn login(conn: RbDbConn, credentials: Json) -> rb::Result> +{ let credentials = credentials.into_inner(); // Get the user, if credentials are valid @@ -33,7 +37,8 @@ async fn login(conn: RbDbConn, credentials: Json) -> rb::Result, -) -> rb::Result> { +) -> rb::Result> +{ let refresh_token = refresh_token_request.into_inner().refresh_token; Ok(Json( diff --git a/src/routes/mod.rs b/src/routes/mod.rs index 78b1e23..52815c1 100644 --- a/src/routes/mod.rs +++ b/src/routes/mod.rs @@ -1,2 +1,2 @@ -pub mod auth; pub mod admin; +pub mod auth; From 7afdd02712be9d80f2a16ba8d02521565df9bcb4 Mon Sep 17 00:00:00 2001 From: Jef Roosens Date: Sun, 22 Aug 2021 18:57:47 +0200 Subject: [PATCH 22/67] Cleaned up Cargo.toml --- Cargo.toml | 27 ++++++++++++--------------- 1 file changed, 12 insertions(+), 15 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index c51d32c..d8ae26a 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -15,6 +15,12 @@ path = "src/main.rs" # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html [dependencies] +# Backend web framework +rocket = { version = "0.5.0-rc.1", features = [ "json" ] } +# Used to provide Rocket routes with database connections +rocket_sync_db_pools = { version = "0.1.0-rc.1", default_features = false, features = [ "diesel_postgres_pool" ] } +# Used to (de)serialize JSON +serde = { version = "1.0.127", features = [ "derive" ] } # ORM diesel = { version = "1.4.7", features = ["postgres", "uuidv07", "chrono"] } diesel_migrations = "1.4.0" @@ -24,24 +30,15 @@ openssl = "0.10.36" rust-argon2 = "0.8.3" rand = "0.8.4" uuid = { version = "0.8.2", features = ["serde"] } +# Authentification jwt = "0.14.0" hmac = "*" sha2 = "*" +# Timestamps for JWT tokens chrono = { version = "*", features = [ "serde" ] } +# Encoding of refresh tokens base64 = "0.13.0" -# Backend web framework -[dependencies.rocket] -version = "0.5.0-rc.1" -features = ["json"] - -# Used to (de)serialize JSON -[dependencies.serde] -version = "1.0.127" -features = ["derive"] - -# Used to provide Rocket routes with database connections -[dependencies.rocket_sync_db_pools] -version = "0.1.0-rc.1" -default_features = false -features = ["diesel_postgres_pool"] +[profile.release] +lto = true +panic = "abort" From b4fc6fe2c01fa65498dc23d5511ba5672f85f96d Mon Sep 17 00:00:00 2001 From: Jef Roosens Date: Sun, 22 Aug 2021 22:01:27 +0200 Subject: [PATCH 23/67] Small changes --- Cargo.toml | 1 + 1 file changed, 1 insertion(+) diff --git a/Cargo.toml b/Cargo.toml index d8ae26a..ebdf355 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -42,3 +42,4 @@ base64 = "0.13.0" [profile.release] lto = true panic = "abort" +codegen-units = 1 From 159da81b8d877ad8001df88c1963782c318b7f1c Mon Sep 17 00:00:00 2001 From: Jef Roosens Date: Sun, 22 Aug 2021 22:35:07 +0200 Subject: [PATCH 24/67] Started on user management routes --- src/auth.rs | 1 - src/db/users.rs | 24 ++++++++++++++++++++++-- src/errors.rs | 2 ++ src/routes/admin.rs | 22 ++++++++++++++++++++-- 4 files changed, 44 insertions(+), 5 deletions(-) diff --git a/src/auth.rs b/src/auth.rs index 613c210..efd991d 100644 --- a/src/auth.rs +++ b/src/auth.rs @@ -81,7 +81,6 @@ pub fn generate_jwt_token(conn: &PgConnection, user: &User) -> crate::Result crate::Result> { users.load::(conn).map_err(|_| RBError::DBError) } + +pub fn find(conn: &PgConnection, user_id: Uuid) -> Option { + users.find(user_id).first::(conn).ok() +} + +pub fn create(conn: &PgConnection, new_user: &NewUser) -> crate::Result<()> { + let count = diesel::insert_into(users).values(new_user).execute(conn).map_err(|_| RBError::DBError)?; + + if count == 0 { + return Err(RBError::DuplicateUser); + } + + Ok(()) +} + +pub fn delete(conn: &PgConnection, user_id: Uuid) -> crate::Result<()> { + diesel::delete(users.filter(id.eq(user_id))).execute(conn).map_err(|_| RBError::DBError)?; + + Ok(()) +} diff --git a/src/errors.rs b/src/errors.rs index bc3243f..7072dab 100644 --- a/src/errors.rs +++ b/src/errors.rs @@ -28,6 +28,7 @@ pub enum RBError InvalidRefreshToken, DuplicateRefreshToken, DBError, + DuplicateUser, } impl<'r> Responder<'r, 'static> for RBError @@ -46,6 +47,7 @@ impl<'r> Responder<'r, 'static> for RBError RBError::InvalidRefreshToken | RBError::DuplicateRefreshToken => { (Status::Unauthorized, "Invalid refresh token.") } + RBError::DuplicateUser => (Status::Conflict, "User already exists"), _ => (Status::InternalServerError, "Internal server error"), }; diff --git a/src/routes/admin.rs b/src/routes/admin.rs index db3ecc2..1ddce97 100644 --- a/src/routes/admin.rs +++ b/src/routes/admin.rs @@ -1,11 +1,14 @@ -use rb::db::users::User; +use rb::db::users::{User, NewUser}; +use rb::db::users as db_users; +use rb::errors::RBError; use rocket::serde::json::Json; +use uuid::Uuid; use crate::{guards::Admin, RbDbConn}; pub fn routes() -> Vec { - routes![get_users] + routes![get_users, get_user_info] } #[get("/users")] @@ -13,3 +16,18 @@ async fn get_users(admin: Admin, conn: RbDbConn) -> rb::Result>> { Ok(Json(conn.run(|c| rb::db::users::all(c)).await?)) } + +#[post("/users", data="")] +async fn create_user(admin: Admin, conn: RbDbConn, user: Json) -> rb::Result<()> { + Ok(conn.run(move |c| db_users::create(c, &user.into_inner())).await?) +} + +#[get("/users/")] +async fn get_user_info(_admin: Admin, conn: RbDbConn, user_id_str: String) -> rb::Result> { + let user_id = Uuid::parse_str(&user_id_str).map_err(|_| RBError::UnknownUser)?; + + match conn.run(move |c| db_users::find(c, user_id)).await { + Some(user) => Ok(Json(user)), + None => Err(RBError::UnknownUser), + } +} From 456c947ecddaac28610863edee53bdab1a67928f Mon Sep 17 00:00:00 2001 From: Jef Roosens Date: Mon, 23 Aug 2021 08:17:06 +0200 Subject: [PATCH 25/67] Added a single lifetime parameter --- src/db/users.rs | 20 ++++++++++++++------ src/routes/admin.rs | 24 ++++++++++++++++-------- 2 files changed, 30 insertions(+), 14 deletions(-) diff --git a/src/db/users.rs b/src/db/users.rs index b1bcfe6..b005295 100644 --- a/src/db/users.rs +++ b/src/db/users.rs @@ -1,5 +1,5 @@ use diesel::{prelude::*, AsChangeset, Insertable, Queryable}; -use serde::{Serialize, Deserialize}; +use serde::{Deserialize, Serialize}; use uuid::Uuid; use crate::{ @@ -33,12 +33,17 @@ pub fn all(conn: &PgConnection) -> crate::Result> users.load::(conn).map_err(|_| RBError::DBError) } -pub fn find(conn: &PgConnection, user_id: Uuid) -> Option { +pub fn find(conn: &PgConnection, user_id: Uuid) -> Option +{ users.find(user_id).first::(conn).ok() } -pub fn create(conn: &PgConnection, new_user: &NewUser) -> crate::Result<()> { - let count = diesel::insert_into(users).values(new_user).execute(conn).map_err(|_| RBError::DBError)?; +pub fn create(conn: &PgConnection, new_user: &NewUser) -> crate::Result<()> +{ + let count = diesel::insert_into(users) + .values(new_user) + .execute(conn) + .map_err(|_| RBError::DBError)?; if count == 0 { return Err(RBError::DuplicateUser); @@ -47,8 +52,11 @@ pub fn create(conn: &PgConnection, new_user: &NewUser) -> crate::Result<()> { Ok(()) } -pub fn delete(conn: &PgConnection, user_id: Uuid) -> crate::Result<()> { - diesel::delete(users.filter(id.eq(user_id))).execute(conn).map_err(|_| RBError::DBError)?; +pub fn delete(conn: &PgConnection, user_id: Uuid) -> crate::Result<()> +{ + diesel::delete(users.filter(id.eq(user_id))) + .execute(conn) + .map_err(|_| RBError::DBError)?; Ok(()) } diff --git a/src/routes/admin.rs b/src/routes/admin.rs index 1ddce97..b3db29b 100644 --- a/src/routes/admin.rs +++ b/src/routes/admin.rs @@ -1,6 +1,10 @@ -use rb::db::users::{User, NewUser}; -use rb::db::users as db_users; -use rb::errors::RBError; +use rb::{ + db::{ + users as db_users, + users::{NewUser, User}, + }, + errors::RBError, +}; use rocket::serde::json::Json; use uuid::Uuid; @@ -17,14 +21,18 @@ async fn get_users(admin: Admin, conn: RbDbConn) -> rb::Result>> Ok(Json(conn.run(|c| rb::db::users::all(c)).await?)) } -#[post("/users", data="")] -async fn create_user(admin: Admin, conn: RbDbConn, user: Json) -> rb::Result<()> { - Ok(conn.run(move |c| db_users::create(c, &user.into_inner())).await?) +#[post("/users", data = "")] +async fn create_user(admin: Admin, conn: RbDbConn, user: Json) -> rb::Result<()> +{ + Ok(conn + .run(move |c| db_users::create(c, &user.into_inner())) + .await?) } #[get("/users/")] -async fn get_user_info(_admin: Admin, conn: RbDbConn, user_id_str: String) -> rb::Result> { - let user_id = Uuid::parse_str(&user_id_str).map_err(|_| RBError::UnknownUser)?; +async fn get_user_info(_admin: Admin, conn: RbDbConn, user_id_str: &str) -> rb::Result> +{ + let user_id = Uuid::parse_str(user_id_str).map_err(|_| RBError::UnknownUser)?; match conn.run(move |c| db_users::find(c, user_id)).await { Some(user) => Ok(Json(user)), From a8cd8618a34017ca90a95283085b9764c09f53db Mon Sep 17 00:00:00 2001 From: Jef Roosens Date: Mon, 23 Aug 2021 12:01:04 +0200 Subject: [PATCH 26/67] Added lifetime thingy --- src/guards.rs | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/src/guards.rs b/src/guards.rs index d475eb4..2994313 100644 --- a/src/guards.rs +++ b/src/guards.rs @@ -9,10 +9,10 @@ use rocket::{ use sha2::Sha256; /// Extracts a "Authorization: Bearer" string from the headers. -pub struct Bearer(String); +pub struct Bearer<'a>(&'a str); #[rocket::async_trait] -impl<'r> FromRequest<'r> for Bearer +impl<'r> FromRequest<'r> for Bearer<'r> { type Error = rb::errors::RBError; @@ -34,7 +34,7 @@ impl<'r> FromRequest<'r> for Bearer None => return Outcome::Forward(()), }; - Outcome::Success(Self(auth_string.to_string())) + Outcome::Success(Self(auth_string)) } } @@ -104,9 +104,10 @@ impl<'r> FromRequest<'r> for Admin async fn from_request(req: &'r Request<'_>) -> Outcome { - let user = try_outcome!(req.guard::().await); - if user.0.admin { - Outcome::Success(Self(user.0)) + let user = try_outcome!(req.guard::().await).0; + + if user.admin { + Outcome::Success(Self(user)) } else { Outcome::Forward(()) } From a100ea52a0fad67515f63c5b5368ad8659b8ff40 Mon Sep 17 00:00:00 2001 From: Jef Roosens Date: Fri, 27 Aug 2021 08:50:48 +0200 Subject: [PATCH 27/67] First draft stuff --- src/db/mod.rs | 3 +++ src/errors.rs | 44 +++++++++++++++++++++++++------------------- src/routes/admin.rs | 19 ++++++++----------- 3 files changed, 36 insertions(+), 30 deletions(-) diff --git a/src/db/mod.rs b/src/db/mod.rs index bf3b714..8979d35 100644 --- a/src/db/mod.rs +++ b/src/db/mod.rs @@ -1,2 +1,5 @@ pub mod tokens; pub mod users; + +pub use users::{User, NewUser}; +pub use tokens::{RefreshToken, NewRefreshToken}; diff --git a/src/errors.rs b/src/errors.rs index 7072dab..969e573 100644 --- a/src/errors.rs +++ b/src/errors.rs @@ -7,35 +7,41 @@ use rocket::{ }; #[derive(Debug)] -pub enum RBError +pub enum RbError { - /// When the login requests an unknown user - UnknownUser, - BlockedUser, - /// Invalid login password. - InvalidPassword, - /// When a non-admin user tries to use an admin endpoint - Unauthorized, - /// When an expired JWT token is used for auth. - JWTTokenExpired, - /// Umbrella error for when something goes wrong whilst creating a JWT token pair - JWTCreationError, - JWTError, - MissingJWTKey, - PWSaltError, + AuthUnknownUser, + AuthBlockedUser, + AuthInvalidPassword, + AuthUnauthorized, + AuthTokenExpired, + AuthRefreshTokenExpired, + AuthInvalidRefreshToken, + AuthDuplicateRefreshToken, + + Custom(&'static str), + AdminCreationError, - TokenExpired, - InvalidRefreshToken, - DuplicateRefreshToken, DBError, DuplicateUser, } +impl RbError { + pub fn status(&self) -> Status { + Status::NotFound + } + + pub fn message(&self) -> &'static str { + match self { + + } + } +} + impl<'r> Responder<'r, 'static> for RBError { fn respond_to(self, _: &'r Request<'_>) -> response::Result<'static> { - let (status, message): (Status, &str) = match self { + let (status, message): (Status, &'static str) = match self { RBError::UnknownUser => (Status::NotFound, "Unknown user"), RBError::BlockedUser => (Status::Unauthorized, "This user is blocked"), RBError::InvalidPassword => (Status::Unauthorized, "Invalid password"), diff --git a/src/routes/admin.rs b/src/routes/admin.rs index b3db29b..87396ca 100644 --- a/src/routes/admin.rs +++ b/src/routes/admin.rs @@ -1,8 +1,5 @@ use rb::{ - db::{ - users as db_users, - users::{NewUser, User}, - }, + db, errors::RBError, }; use rocket::serde::json::Json; @@ -12,29 +9,29 @@ use crate::{guards::Admin, RbDbConn}; pub fn routes() -> Vec { - routes![get_users, get_user_info] + routes![get_users, get_user_info, create_user] } #[get("/users")] -async fn get_users(admin: Admin, conn: RbDbConn) -> rb::Result>> +async fn get_users(admin: Admin, conn: RbDbConn) -> rb::Result>> { - Ok(Json(conn.run(|c| rb::db::users::all(c)).await?)) + Ok(Json(conn.run(|c| db::users::all(c)).await?)) } #[post("/users", data = "")] -async fn create_user(admin: Admin, conn: RbDbConn, user: Json) -> rb::Result<()> +async fn create_user(admin: Admin, conn: RbDbConn, user: Json) -> rb::Result<()> { Ok(conn - .run(move |c| db_users::create(c, &user.into_inner())) + .run(move |c| db::users::create(c, &user.into_inner())) .await?) } #[get("/users/")] -async fn get_user_info(_admin: Admin, conn: RbDbConn, user_id_str: &str) -> rb::Result> +async fn get_user_info(_admin: Admin, conn: RbDbConn, user_id_str: &str) -> rb::Result> { let user_id = Uuid::parse_str(user_id_str).map_err(|_| RBError::UnknownUser)?; - match conn.run(move |c| db_users::find(c, user_id)).await { + match conn.run(move |c| db::users::find(c, user_id)).await { Some(user) => Ok(Json(user)), None => Err(RBError::UnknownUser), } From c912c0aa0bf17cbe2213cc482c99b2807298c071 Mon Sep 17 00:00:00 2001 From: Jef Roosens Date: Sat, 28 Aug 2021 12:41:43 +0200 Subject: [PATCH 28/67] Beginning of Makefile --- .cargo/config | 3 +++ .gitignore | 2 +- Makefile | 24 ++++++++++++++++++++++++ 3 files changed, 28 insertions(+), 1 deletion(-) create mode 100644 .cargo/config create mode 100644 Makefile diff --git a/.cargo/config b/.cargo/config new file mode 100644 index 0000000..b086373 --- /dev/null +++ b/.cargo/config @@ -0,0 +1,3 @@ +# vim: ft=toml +[build] +target-dir = "out/target" diff --git a/.gitignore b/.gitignore index 4d7b925..e79787c 100644 --- a/.gitignore +++ b/.gitignore @@ -2,7 +2,7 @@ # Generated by Cargo # will have compiled files and executables debug/ -target/ +out/ # Remove Cargo.lock from gitignore if creating an executable, leave it for libraries # More information here https://doc.rust-lang.org/cargo/guide/cargo-toml-vs-cargo-lock.html diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..b70edac --- /dev/null +++ b/Makefile @@ -0,0 +1,24 @@ +PQ_VER ?= 11.12 +SSL_VER ?= 1.1.1k + +OUT_DIR ?= out/deps + +export CC = musl-gcc -fPIC -pie -static + +# TODO check for header files (openssl-dev, libpq-dev) both for Arch & Ubuntu + + +all: openssl +.PHONY: all + + +# =====OPENSSL===== +# Download the source code +$(OUT_DIR)/openssl-$(SSL_VER)/Configure: + mkdir -p '$(OUT_DIR)' + curl -sSL "https://www.openssl.org/source/openssl-$(SSL_VER).tar.gz" | \ + tar -C "$(OUT_DIR)" -xz + +# Build OpenSSL +openssl: $(OUT_DIR)/openssl-$(SSL_VER)/Configure +.PHONY: openssl From 85cfe6290c48756bd5f9427c27b74c757ca31fce Mon Sep 17 00:00:00 2001 From: Jef Roosens Date: Sat, 28 Aug 2021 13:53:13 +0200 Subject: [PATCH 29/67] Almost working libpq --- Makefile | 55 +++++++++++++++++++++++++++++++++++++++++++++++++------ 1 file changed, 49 insertions(+), 6 deletions(-) diff --git a/Makefile b/Makefile index b70edac..2ce27a2 100644 --- a/Makefile +++ b/Makefile @@ -1,24 +1,67 @@ PQ_VER ?= 11.12 SSL_VER ?= 1.1.1k -OUT_DIR ?= out/deps +# This is such a lovely oneliner +# NOTE: $(dir PATH) outputs a trailing slash +OUT_DIR ?= $(dir $(abspath $(lastword $(MAKEFILE_LIST))))out/deps + +# Generated variables for ease of use +PREFIX := $(OUT_DIR)/prefix +OPENSSL_DIR := $(OUT_DIR)/openssl-$(SSL_VER) +PQ_DIR := $(OUT_DIR)/postgresql-$(PQ_VER) +CORES != nproc + +export CC=musl-gcc -fPIC -pie -static +export LD_LIBRARY_PATH=$(PREFIX) +export PKG_CONFIG_PATH=/usr/local/lib/pkgconfig -export CC = musl-gcc -fPIC -pie -static # TODO check for header files (openssl-dev, libpq-dev) both for Arch & Ubuntu +# Create the out dir +$(shell mkdir -p "$(PREFIX)") all: openssl .PHONY: all # =====OPENSSL===== -# Download the source code -$(OUT_DIR)/openssl-$(SSL_VER)/Configure: - mkdir -p '$(OUT_DIR)' +# Download the source code & configure the project +$(OPENSSL_DIR)/Configure: curl -sSL "https://www.openssl.org/source/openssl-$(SSL_VER).tar.gz" | \ tar -C "$(OUT_DIR)" -xz + cd "$(OPENSSL_DIR)" && \ + CC="$$CC -idirafter /usr/include" ./Configure \ + no-zlib \ + no-shared \ + --prefix="$(PREFIX)" \ + --openssldir="$(PREFIX)/ssl" \ + linux-x86_64 # Build OpenSSL -openssl: $(OUT_DIR)/openssl-$(SSL_VER)/Configure +openssl: $(OPENSSL_DIR)/Configure + C_INCLUDE_PATH="$(PREFIX)/include" $(MAKE) -C "$(OPENSSL_DIR)" depend + $(MAKE) -C "$(OPENSSL_DIR)" -j$(CORES) + $(MAKE) -C "$(OPENSSL_DIR)" install_sw .PHONY: openssl + + +# =====LIBPQ===== +# Download the source code & configure the project +$(PQ_DIR)/configure: + curl -sSL "https://ftp.postgresql.org/pub/source/v$(PQ_VER)/postgresql-$(PQ_VER).tar.gz" | \ + tar -C "$(OUT_DIR)" -xz + cd "$(PQ_DIR)" && \ + LDFLAGS="-L$(PREFIX)/lib" CFLAGS="-I$(PREFIX)/include" ./configure \ + --without-readline \ + --without-zlib \ + --with-openssl \ + --prefix="$(PREFIX)" \ + --host=x86_64-unknown-linux-musl + +libpq: $(PQ_DIR)/configure + make -C "$(PQ_DIR)/src/interfaces/libpq" -j$(CORES) all-static-lib + make -C "$(PQ_DIR)/src/interfaces/libpq" install install-lib-static + make -C "$(PQ_DIR)/src/bin/pg_config" -j $(CORES) + make -C "$(PQ_DIR)/src/bin/pg_config" install +.PHONY: libpq From 055d1f9e8da054e316ec84eb8819fe42403e4248 Mon Sep 17 00:00:00 2001 From: Jef Roosens Date: Sat, 28 Aug 2021 14:20:51 +0200 Subject: [PATCH 30/67] Added dumb-init; changed some stuff --- Makefile | 38 ++++++++++++++++++++++++++++---------- 1 file changed, 28 insertions(+), 10 deletions(-) diff --git a/Makefile b/Makefile index 2ce27a2..d7bbb41 100644 --- a/Makefile +++ b/Makefile @@ -1,16 +1,20 @@ -PQ_VER ?= 11.12 +PQ_VER ?= 11.12 SSL_VER ?= 1.1.1k +DI_VER ?= 1.2.5 -# This is such a lovely oneliner -# NOTE: $(dir PATH) outputs a trailing slash -OUT_DIR ?= $(dir $(abspath $(lastword $(MAKEFILE_LIST))))out/deps # Generated variables for ease of use -PREFIX := $(OUT_DIR)/prefix +# This is such a lovely oneliner +# NOTE: $(dir PATH) outputs a trailing slash +OUT_DIR ?= $(dir $(abspath $(lastword $(MAKEFILE_LIST))))out +PREFIX := $(OUT_DIR)/prefix OPENSSL_DIR := $(OUT_DIR)/openssl-$(SSL_VER) -PQ_DIR := $(OUT_DIR)/postgresql-$(PQ_VER) +PQ_DIR := $(OUT_DIR)/postgresql-$(PQ_VER) +DI_DIR := $(OUT_DIR)/dumb-init-$(DI_VER) + CORES != nproc + export CC=musl-gcc -fPIC -pie -static export LD_LIBRARY_PATH=$(PREFIX) export PKG_CONFIG_PATH=/usr/local/lib/pkgconfig @@ -21,8 +25,12 @@ export PKG_CONFIG_PATH=/usr/local/lib/pkgconfig # Create the out dir $(shell mkdir -p "$(PREFIX)") -all: openssl .PHONY: all +all: build + +# libpq builds openssl as a dependency +.PHONY: build +build: libpq # =====OPENSSL===== @@ -39,11 +47,11 @@ $(OPENSSL_DIR)/Configure: linux-x86_64 # Build OpenSSL +.PHONY: openssl openssl: $(OPENSSL_DIR)/Configure C_INCLUDE_PATH="$(PREFIX)/include" $(MAKE) -C "$(OPENSSL_DIR)" depend $(MAKE) -C "$(OPENSSL_DIR)" -j$(CORES) $(MAKE) -C "$(OPENSSL_DIR)" install_sw -.PHONY: openssl # =====LIBPQ===== @@ -59,9 +67,19 @@ $(PQ_DIR)/configure: --prefix="$(PREFIX)" \ --host=x86_64-unknown-linux-musl -libpq: $(PQ_DIR)/configure +.PHONY: libpq +libpq: openssl $(PQ_DIR)/configure make -C "$(PQ_DIR)/src/interfaces/libpq" -j$(CORES) all-static-lib make -C "$(PQ_DIR)/src/interfaces/libpq" install install-lib-static make -C "$(PQ_DIR)/src/bin/pg_config" -j $(CORES) make -C "$(PQ_DIR)/src/bin/pg_config" install -.PHONY: libpq + + +# =====DUMB-INIT===== +# NOTE: this is only used inside the Docker image, but it's here for completeness. +$(DI_DIR)/Makefile: + curl -sSL "https://github.com/Yelp/dumb-init/archive/refs/tags/v$(DI_VER).tar.gz" | \ + tar -C "$(OUT_DIR)" -xz + +dumb-init: $(DI_DIR)/Makefile + make -C "$(DI_DIR)" build From de8be87036871e3ea3c181df7e7d16a19b40daa3 Mon Sep 17 00:00:00 2001 From: Jef Roosens Date: Sat, 28 Aug 2021 16:37:32 +0200 Subject: [PATCH 31/67] Added some comments --- Makefile | 23 +++++++++++++++++++---- 1 file changed, 19 insertions(+), 4 deletions(-) diff --git a/Makefile b/Makefile index d7bbb41..0f1146d 100644 --- a/Makefile +++ b/Makefile @@ -1,20 +1,27 @@ +# =====CONFIGURATION===== +# Version of postgresql to compile libpq from PQ_VER ?= 11.12 +# OpenSSL version SSL_VER ?= 1.1.1k +# Dumb-init version DI_VER ?= 1.2.5 -# Generated variables for ease of use +# =====AUTO-GENERATED VARIABLES===== # This is such a lovely oneliner # NOTE: $(dir PATH) outputs a trailing slash -OUT_DIR ?= $(dir $(abspath $(lastword $(MAKEFILE_LIST))))out +OUT_DIR ?= $(dir $(abspath $(lastword $(MAKEFILE_LIST))))out + PREFIX := $(OUT_DIR)/prefix OPENSSL_DIR := $(OUT_DIR)/openssl-$(SSL_VER) PQ_DIR := $(OUT_DIR)/postgresql-$(PQ_VER) DI_DIR := $(OUT_DIR)/dumb-init-$(DI_VER) -CORES != nproc +# Used in various make calls to specify parallel recipes +CORES != nproc +# =====ENVIRONMENT VARIABLES===== export CC=musl-gcc -fPIC -pie -static export LD_LIBRARY_PATH=$(PREFIX) export PKG_CONFIG_PATH=/usr/local/lib/pkgconfig @@ -25,6 +32,8 @@ export PKG_CONFIG_PATH=/usr/local/lib/pkgconfig # Create the out dir $(shell mkdir -p "$(PREFIX)") + +# ====RECIPES==== .PHONY: all all: build @@ -32,6 +41,11 @@ all: build .PHONY: build build: libpq +.PHONY: clean +clean: + @ echo "Note: this only cleans the C dependencies, not the Cargo cache." + rm -rf "$(PQ_DIR)" "$(OPENSSL_DIR)" "$(DI_DIR)" "$(PREFIX)" + # =====OPENSSL===== # Download the source code & configure the project @@ -62,8 +76,8 @@ $(PQ_DIR)/configure: cd "$(PQ_DIR)" && \ LDFLAGS="-L$(PREFIX)/lib" CFLAGS="-I$(PREFIX)/include" ./configure \ --without-readline \ - --without-zlib \ --with-openssl \ + --without-zlib \ --prefix="$(PREFIX)" \ --host=x86_64-unknown-linux-musl @@ -81,5 +95,6 @@ $(DI_DIR)/Makefile: curl -sSL "https://github.com/Yelp/dumb-init/archive/refs/tags/v$(DI_VER).tar.gz" | \ tar -C "$(OUT_DIR)" -xz +.PHONY: dumb-init dumb-init: $(DI_DIR)/Makefile make -C "$(DI_DIR)" build From dd51d107e3ef0d412634f9b3ce9521cff28c8c52 Mon Sep 17 00:00:00 2001 From: Jef Roosens Date: Sat, 28 Aug 2021 22:06:09 +0200 Subject: [PATCH 32/67] Renamed errors; changed Responser implementation --- src/auth.rs | 36 ++++++++++--------- src/db/mod.rs | 4 +-- src/db/users.rs | 12 ++++--- src/errors.rs | 86 +++++++++++++++++++++++++++------------------ src/guards.rs | 24 ++++++++----- src/routes/admin.rs | 15 ++++---- 6 files changed, 104 insertions(+), 73 deletions(-) diff --git a/src/auth.rs b/src/auth.rs index efd991d..cb4e4b5 100644 --- a/src/auth.rs +++ b/src/auth.rs @@ -12,7 +12,7 @@ use crate::{ tokens::{NewRefreshToken, RefreshToken}, users::{NewUser, User}, }, - errors::RBError, + errors::RbError, schema::{refresh_tokens::dsl as refresh_tokens, users::dsl as users}, }; @@ -22,16 +22,16 @@ pub fn verify_user(conn: &PgConnection, username: &str, password: &str) -> crate let user = users::users .filter(users::username.eq(username)) .first::(conn) - .map_err(|_| RBError::UnknownUser)?; + .map_err(|_| RbError::AuthUnknownUser)?; // Check if a user is blocked if user.blocked { - return Err(RBError::BlockedUser); + return Err(RbError::AuthBlockedUser); } match verify_encoded(user.password.as_str(), password.as_bytes()) { Ok(true) => Ok(user), - _ => Err(RBError::InvalidPassword), + _ => Err(RbError::AuthInvalidPassword), } } @@ -54,9 +54,9 @@ pub struct Claims pub fn generate_jwt_token(conn: &PgConnection, user: &User) -> crate::Result { - let secret = std::env::var("JWT_KEY").map_err(|_| RBError::MissingJWTKey)?; - let key: Hmac = - Hmac::new_from_slice(secret.as_bytes()).map_err(|_| RBError::JWTCreationError)?; + let secret = std::env::var("JWT_KEY").map_err(|_| RbError::Custom("Missing JWT key."))?; + let key: Hmac = Hmac::new_from_slice(secret.as_bytes()) + .map_err(|_| RbError::Custom("Couldn't create Hmac key."))?; let current_time = Utc::now(); @@ -71,7 +71,7 @@ pub fn generate_jwt_token(conn: &PgConnection, user: &User) -> crate::Result crate::Result crate::Result // Encode the actual password let config = argon2::Config::default(); - argon2::hash_encoded(password.as_bytes(), &salt, &config).map_err(|_| RBError::PWSaltError) + argon2::hash_encoded(password.as_bytes(), &salt, &config) + .map_err(|_| RbError::Custom("Couldn't hash password.")) } pub fn create_admin_user(conn: &PgConnection, username: &str, password: &str) @@ -123,21 +124,22 @@ pub fn create_admin_user(conn: &PgConnection, username: &str, password: &str) .do_update() .set(&new_user) .execute(conn) - .map_err(|_| RBError::AdminCreationError)?; + .map_err(|_| RbError::Custom("Couldn't create admin."))?; Ok(true) } pub fn refresh_token(conn: &PgConnection, refresh_token: &str) -> crate::Result { - let token_bytes = base64::decode(refresh_token).map_err(|_| RBError::InvalidRefreshToken)?; + let token_bytes = + base64::decode(refresh_token).map_err(|_| RbError::AuthInvalidRefreshToken)?; // First, we request the token from the database to see if it's really a valid token let (token_entry, user) = refresh_tokens::refresh_tokens .inner_join(users::users) .filter(refresh_tokens::token.eq(token_bytes)) .first::<(RefreshToken, User)>(conn) - .map_err(|_| RBError::InvalidRefreshToken)?; + .map_err(|_| RbError::AuthInvalidRefreshToken)?; // If we see that the token has already been used before, we block the user. if token_entry.last_used_at.is_some() { @@ -145,16 +147,16 @@ pub fn refresh_token(conn: &PgConnection, refresh_token: &str) -> crate::Result< diesel::update(target) .set(users::blocked.eq(true)) .execute(conn) - .map_err(|_| RBError::DBError)?; + .map_err(|_| RbError::Custom("Couldn't block user."))?; - return Err(RBError::DuplicateRefreshToken); + return Err(RbError::AuthDuplicateRefreshToken); } // Now we check if the token has already expired let cur_time = Utc::now().naive_utc(); if token_entry.expires_at < cur_time { - return Err(RBError::TokenExpired); + return Err(RbError::AuthTokenExpired); } // We update the last_used_at value for the refresh token @@ -162,7 +164,7 @@ pub fn refresh_token(conn: &PgConnection, refresh_token: &str) -> crate::Result< diesel::update(target) .set(refresh_tokens::last_used_at.eq(cur_time)) .execute(conn) - .map_err(|_| RBError::DBError)?; + .map_err(|_| RbError::Custom("Couldn't update last used time."))?; generate_jwt_token(conn, &user) } diff --git a/src/db/mod.rs b/src/db/mod.rs index 8979d35..9c831dd 100644 --- a/src/db/mod.rs +++ b/src/db/mod.rs @@ -1,5 +1,5 @@ pub mod tokens; pub mod users; -pub use users::{User, NewUser}; -pub use tokens::{RefreshToken, NewRefreshToken}; +pub use tokens::{NewRefreshToken, RefreshToken}; +pub use users::{NewUser, User}; diff --git a/src/db/users.rs b/src/db/users.rs index b005295..79d337a 100644 --- a/src/db/users.rs +++ b/src/db/users.rs @@ -3,7 +3,7 @@ use serde::{Deserialize, Serialize}; use uuid::Uuid; use crate::{ - errors::RBError, + errors::RbError, schema::{users, users::dsl::*}, }; @@ -30,7 +30,9 @@ pub struct NewUser pub fn all(conn: &PgConnection) -> crate::Result> { - users.load::(conn).map_err(|_| RBError::DBError) + users + .load::(conn) + .map_err(|_| RbError::DbError("Couldn't get all users.")) } pub fn find(conn: &PgConnection, user_id: Uuid) -> Option @@ -43,10 +45,10 @@ pub fn create(conn: &PgConnection, new_user: &NewUser) -> crate::Result<()> let count = diesel::insert_into(users) .values(new_user) .execute(conn) - .map_err(|_| RBError::DBError)?; + .map_err(|_| RbError::DbError("Couldn't create user."))?; if count == 0 { - return Err(RBError::DuplicateUser); + return Err(RbError::UMDuplicateUser); } Ok(()) @@ -56,7 +58,7 @@ pub fn delete(conn: &PgConnection, user_id: Uuid) -> crate::Result<()> { diesel::delete(users.filter(id.eq(user_id))) .execute(conn) - .map_err(|_| RBError::DBError)?; + .map_err(|_| RbError::DbError("Couldn't delete user."))?; Ok(()) } diff --git a/src/errors.rs b/src/errors.rs index 969e573..886b6fe 100644 --- a/src/errors.rs +++ b/src/errors.rs @@ -1,9 +1,8 @@ -use std::io; - use rocket::{ http::Status, request::Request, - response::{self, Responder, Response}, + response::{self, Responder}, + serde::json::json, }; #[derive(Debug)] @@ -18,51 +17,70 @@ pub enum RbError AuthInvalidRefreshToken, AuthDuplicateRefreshToken, - Custom(&'static str), + // UM = User Management + UMDuplicateUser, + UMUnknownUser, - AdminCreationError, - DBError, - DuplicateUser, + DbError(&'static str), + Custom(&'static str), } -impl RbError { - pub fn status(&self) -> Status { - Status::NotFound +impl RbError +{ + pub fn status(&self) -> Status + { + // Every entry gets its own line for easy editing later when needed + match self { + RbError::AuthUnknownUser => Status::NotFound, + RbError::AuthBlockedUser => Status::Forbidden, + RbError::AuthInvalidPassword => Status::Unauthorized, + RbError::AuthUnauthorized => Status::Unauthorized, + RbError::AuthTokenExpired => Status::Unauthorized, + RbError::AuthRefreshTokenExpired => Status::Unauthorized, + RbError::AuthInvalidRefreshToken => Status::Unauthorized, + RbError::AuthDuplicateRefreshToken => Status::Unauthorized, + + RbError::UMDuplicateUser => Status::Conflict, + + RbError::Custom(_) => Status::InternalServerError, + _ => Status::InternalServerError, + } } - pub fn message(&self) -> &'static str { + pub fn message(&self) -> &'static str + { match self { + RbError::AuthUnknownUser => "This user doesn't exist.", + RbError::AuthBlockedUser => "This user is blocked.", + RbError::AuthInvalidPassword => "Invalid credentials.", + RbError::AuthUnauthorized => "You are not authorized to access this resource.", + RbError::AuthTokenExpired => "This token is not valid anymore.", + RbError::AuthRefreshTokenExpired => "This refresh token is not valid anymore.", + RbError::AuthInvalidRefreshToken => "This refresh token is not valid.", + RbError::AuthDuplicateRefreshToken => { + "This refresh token has already been used. The user has been blocked." + } + RbError::UMDuplicateUser => "This user already exists.", + + RbError::Custom(message) => message, + _ => "", } } } -impl<'r> Responder<'r, 'static> for RBError +impl<'r> Responder<'r, 'static> for RbError { - fn respond_to(self, _: &'r Request<'_>) -> response::Result<'static> + fn respond_to(self, req: &'r Request<'_>) -> response::Result<'static> { - let (status, message): (Status, &'static str) = match self { - RBError::UnknownUser => (Status::NotFound, "Unknown user"), - RBError::BlockedUser => (Status::Unauthorized, "This user is blocked"), - RBError::InvalidPassword => (Status::Unauthorized, "Invalid password"), - RBError::Unauthorized => (Status::Unauthorized, "Unauthorized"), - RBError::JWTTokenExpired => (Status::Unauthorized, "Token expired"), - RBError::JWTCreationError | RBError::MissingJWTKey => { - (Status::InternalServerError, "Failed to create tokens.") - } - RBError::InvalidRefreshToken | RBError::DuplicateRefreshToken => { - (Status::Unauthorized, "Invalid refresh token.") - } - RBError::DuplicateUser => (Status::Conflict, "User already exists"), - _ => (Status::InternalServerError, "Internal server error"), - }; + let status = self.status(); + let content = json!({ + "status": status.code, + "message": self.message(), + }); - let mut res = Response::new(); - res.set_status(status); - res.set_sized_body(message.len(), io::Cursor::new(message)); - - Ok(res) + content.respond_to(req) } } -pub type Result = std::result::Result; +pub type Result = std::result::Result; diff --git a/src/guards.rs b/src/guards.rs index 2994313..ae6782b 100644 --- a/src/guards.rs +++ b/src/guards.rs @@ -14,7 +14,7 @@ pub struct Bearer<'a>(&'a str); #[rocket::async_trait] impl<'r> FromRequest<'r> for Bearer<'r> { - type Error = rb::errors::RBError; + type Error = rb::errors::RbError; async fn from_request(req: &'r Request<'_>) -> Outcome { @@ -44,7 +44,7 @@ pub struct JWT(Claims); #[rocket::async_trait] impl<'r> FromRequest<'r> for JWT { - type Error = rb::errors::RBError; + type Error = rb::errors::RbError; async fn from_request(req: &'r Request<'_>) -> Outcome { @@ -54,19 +54,27 @@ impl<'r> FromRequest<'r> for JWT let secret = match std::env::var("JWT_KEY") { Ok(key) => key, Err(_) => { - return Outcome::Failure((Status::InternalServerError, Self::Error::MissingJWTKey)) + return Outcome::Failure(( + Status::InternalServerError, + Self::Error::AuthUnauthorized, + )) } }; let key: Hmac = match Hmac::new_from_slice(secret.as_bytes()) { Ok(key) => key, Err(_) => { - return Outcome::Failure((Status::InternalServerError, Self::Error::JWTError)) + return Outcome::Failure(( + Status::InternalServerError, + Self::Error::Custom("Failed to do Hmac thing."), + )) } }; // Verify token using key let claims: Claims = match bearer.verify_with_key(&key) { Ok(claims) => claims, - Err(_) => return Outcome::Failure((Status::Unauthorized, Self::Error::Unauthorized)), + Err(_) => { + return Outcome::Failure((Status::Unauthorized, Self::Error::AuthUnauthorized)) + } }; Outcome::Success(Self(claims)) @@ -79,7 +87,7 @@ pub struct User(Claims); #[rocket::async_trait] impl<'r> FromRequest<'r> for User { - type Error = rb::errors::RBError; + type Error = rb::errors::RbError; async fn from_request(req: &'r Request<'_>) -> Outcome { @@ -87,7 +95,7 @@ impl<'r> FromRequest<'r> for User // Verify key hasn't yet expired if chrono::Utc::now().timestamp() > claims.exp { - return Outcome::Failure((Status::Forbidden, Self::Error::TokenExpired)); + return Outcome::Failure((Status::Forbidden, Self::Error::AuthTokenExpired)); } Outcome::Success(Self(claims)) @@ -100,7 +108,7 @@ pub struct Admin(Claims); #[rocket::async_trait] impl<'r> FromRequest<'r> for Admin { - type Error = rb::errors::RBError; + type Error = rb::errors::RbError; async fn from_request(req: &'r Request<'_>) -> Outcome { diff --git a/src/routes/admin.rs b/src/routes/admin.rs index 87396ca..bd6f53f 100644 --- a/src/routes/admin.rs +++ b/src/routes/admin.rs @@ -1,7 +1,4 @@ -use rb::{ - db, - errors::RBError, -}; +use rb::{db, errors::RbError}; use rocket::serde::json::Json; use uuid::Uuid; @@ -27,12 +24,16 @@ async fn create_user(admin: Admin, conn: RbDbConn, user: Json) -> r } #[get("/users/")] -async fn get_user_info(_admin: Admin, conn: RbDbConn, user_id_str: &str) -> rb::Result> +async fn get_user_info( + _admin: Admin, + conn: RbDbConn, + user_id_str: &str, +) -> rb::Result> { - let user_id = Uuid::parse_str(user_id_str).map_err(|_| RBError::UnknownUser)?; + let user_id = Uuid::parse_str(user_id_str).map_err(|_| RbError::UMUnknownUser)?; match conn.run(move |c| db::users::find(c, user_id)).await { Some(user) => Ok(Json(user)), - None => Err(RBError::UnknownUser), + None => Err(RbError::UMUnknownUser), } } From 6858e9da62bdb6c22be61b0823651c8f8b80d4b5 Mon Sep 17 00:00:00 2001 From: Jef Roosens Date: Sun, 29 Aug 2021 19:04:06 +0200 Subject: [PATCH 33/67] Restructured auth --- src/{auth.rs => auth/jwt.rs} | 53 --------------------------- src/auth/mod.rs | 71 ++++++++++++++++++++++++++++++++++++ src/errors.rs | 1 + src/guards.rs | 2 +- src/routes/auth.rs | 7 +++- 5 files changed, 78 insertions(+), 56 deletions(-) rename src/{auth.rs => auth/jwt.rs} (70%) create mode 100644 src/auth/mod.rs diff --git a/src/auth.rs b/src/auth/jwt.rs similarity index 70% rename from src/auth.rs rename to src/auth/jwt.rs index cb4e4b5..5c15d57 100644 --- a/src/auth.rs +++ b/src/auth/jwt.rs @@ -1,4 +1,3 @@ -use argon2::verify_encoded; use chrono::Utc; use diesel::{insert_into, prelude::*, PgConnection}; use hmac::{Hmac, NewMac}; @@ -16,25 +15,6 @@ use crate::{ schema::{refresh_tokens::dsl as refresh_tokens, users::dsl as users}, }; -pub fn verify_user(conn: &PgConnection, username: &str, password: &str) -> crate::Result -{ - // TODO handle non-"NotFound" Diesel errors accordingely - let user = users::users - .filter(users::username.eq(username)) - .first::(conn) - .map_err(|_| RbError::AuthUnknownUser)?; - - // Check if a user is blocked - if user.blocked { - return Err(RbError::AuthBlockedUser); - } - - match verify_encoded(user.password.as_str(), password.as_bytes()) { - Ok(true) => Ok(user), - _ => Err(RbError::AuthInvalidPassword), - } -} - #[derive(Serialize)] #[serde(rename_all = "camelCase")] pub struct JWTResponse @@ -96,39 +76,6 @@ pub fn generate_jwt_token(conn: &PgConnection, user: &User) -> crate::Result crate::Result -{ - // Generate a random salt - let mut salt = [0u8; 64]; - thread_rng().fill(&mut salt[..]); - - // Encode the actual password - let config = argon2::Config::default(); - argon2::hash_encoded(password.as_bytes(), &salt, &config) - .map_err(|_| RbError::Custom("Couldn't hash password.")) -} - -pub fn create_admin_user(conn: &PgConnection, username: &str, password: &str) - -> crate::Result -{ - let pass_hashed = hash_password(password)?; - let new_user = NewUser { - username: username.to_string(), - password: pass_hashed, - admin: true, - }; - - insert_into(users::users) - .values(&new_user) - .on_conflict(users::username) - .do_update() - .set(&new_user) - .execute(conn) - .map_err(|_| RbError::Custom("Couldn't create admin."))?; - - Ok(true) -} - pub fn refresh_token(conn: &PgConnection, refresh_token: &str) -> crate::Result { let token_bytes = diff --git a/src/auth/mod.rs b/src/auth/mod.rs new file mode 100644 index 0000000..838a080 --- /dev/null +++ b/src/auth/mod.rs @@ -0,0 +1,71 @@ +use ::jwt::SignWithKey; +use argon2::verify_encoded; +use chrono::Utc; +use diesel::{insert_into, prelude::*, PgConnection}; +use hmac::{Hmac, NewMac}; +use rand::{thread_rng, Rng}; +use serde::{Deserialize, Serialize}; +use sha2::Sha256; + +use crate::{ + db::{ + tokens::{NewRefreshToken, RefreshToken}, + users::{NewUser, User}, + }, + errors::RbError, + schema::{refresh_tokens::dsl as refresh_tokens, users::dsl as users}, +}; + +pub mod jwt; + +pub fn verify_user(conn: &PgConnection, username: &str, password: &str) -> crate::Result +{ + // TODO handle non-"NotFound" Diesel errors accordingely + let user = users::users + .filter(users::username.eq(username)) + .first::(conn) + .map_err(|_| RbError::AuthUnknownUser)?; + + // Check if a user is blocked + if user.blocked { + return Err(RbError::AuthBlockedUser); + } + + match verify_encoded(user.password.as_str(), password.as_bytes()) { + Ok(true) => Ok(user), + _ => Err(RbError::AuthInvalidPassword), + } +} + +pub fn hash_password(password: &str) -> crate::Result +{ + // Generate a random salt + let mut salt = [0u8; 64]; + thread_rng().fill(&mut salt[..]); + + // Encode the actual password + let config = argon2::Config::default(); + argon2::hash_encoded(password.as_bytes(), &salt, &config) + .map_err(|_| RbError::Custom("Couldn't hash password.")) +} + +pub fn create_admin_user(conn: &PgConnection, username: &str, password: &str) + -> crate::Result +{ + let pass_hashed = hash_password(password)?; + let new_user = NewUser { + username: username.to_string(), + password: pass_hashed, + admin: true, + }; + + insert_into(users::users) + .values(&new_user) + .on_conflict(users::username) + .do_update() + .set(&new_user) + .execute(conn) + .map_err(|_| RbError::Custom("Couldn't create admin."))?; + + Ok(true) +} diff --git a/src/errors.rs b/src/errors.rs index 886b6fe..daca6bb 100644 --- a/src/errors.rs +++ b/src/errors.rs @@ -79,6 +79,7 @@ impl<'r> Responder<'r, 'static> for RbError "message": self.message(), }); + // TODO add status to response content.respond_to(req) } } diff --git a/src/guards.rs b/src/guards.rs index ae6782b..26cda21 100644 --- a/src/guards.rs +++ b/src/guards.rs @@ -1,6 +1,6 @@ use hmac::{Hmac, NewMac}; use jwt::VerifyWithKey; -use rb::auth::Claims; +use rb::auth::jwt::Claims; use rocket::{ http::Status, outcome::try_outcome, diff --git a/src/routes/auth.rs b/src/routes/auth.rs index 9551ace..955cfaa 100644 --- a/src/routes/auth.rs +++ b/src/routes/auth.rs @@ -1,4 +1,7 @@ -use rb::auth::{generate_jwt_token, verify_user, JWTResponse}; +use rb::auth::{ + jwt::{generate_jwt_token, JWTResponse}, + verify_user, +}; use rocket::serde::json::Json; use serde::Deserialize; @@ -51,7 +54,7 @@ async fn refresh_token( let refresh_token = refresh_token_request.into_inner().refresh_token; Ok(Json( - conn.run(move |c| rb::auth::refresh_token(c, &refresh_token)) + conn.run(move |c| rb::auth::jwt::refresh_token(c, &refresh_token)) .await?, )) } From 27b904b3f5b2724729b7d553b77b7e6304f715bf Mon Sep 17 00:00:00 2001 From: Jef Roosens Date: Sun, 29 Aug 2021 19:07:36 +0200 Subject: [PATCH 34/67] Pleased the linters --- src/auth/jwt.rs | 2 +- src/auth/mod.rs | 12 ++---------- src/guards.rs | 6 +++--- src/routes/admin.rs | 4 ++-- 4 files changed, 8 insertions(+), 16 deletions(-) diff --git a/src/auth/jwt.rs b/src/auth/jwt.rs index 5c15d57..8767bb3 100644 --- a/src/auth/jwt.rs +++ b/src/auth/jwt.rs @@ -9,7 +9,7 @@ use sha2::Sha256; use crate::{ db::{ tokens::{NewRefreshToken, RefreshToken}, - users::{NewUser, User}, + users::User, }, errors::RbError, schema::{refresh_tokens::dsl as refresh_tokens, users::dsl as users}, diff --git a/src/auth/mod.rs b/src/auth/mod.rs index 838a080..b315e82 100644 --- a/src/auth/mod.rs +++ b/src/auth/mod.rs @@ -1,19 +1,11 @@ -use ::jwt::SignWithKey; use argon2::verify_encoded; -use chrono::Utc; use diesel::{insert_into, prelude::*, PgConnection}; -use hmac::{Hmac, NewMac}; use rand::{thread_rng, Rng}; -use serde::{Deserialize, Serialize}; -use sha2::Sha256; use crate::{ - db::{ - tokens::{NewRefreshToken, RefreshToken}, - users::{NewUser, User}, - }, + db::users::{NewUser, User}, errors::RbError, - schema::{refresh_tokens::dsl as refresh_tokens, users::dsl as users}, + schema::users::dsl as users, }; pub mod jwt; diff --git a/src/guards.rs b/src/guards.rs index 26cda21..55df193 100644 --- a/src/guards.rs +++ b/src/guards.rs @@ -39,10 +39,10 @@ impl<'r> FromRequest<'r> for Bearer<'r> } /// Verifies the provided JWT is valid. -pub struct JWT(Claims); +pub struct Jwt(Claims); #[rocket::async_trait] -impl<'r> FromRequest<'r> for JWT +impl<'r> FromRequest<'r> for Jwt { type Error = rb::errors::RbError; @@ -91,7 +91,7 @@ impl<'r> FromRequest<'r> for User async fn from_request(req: &'r Request<'_>) -> Outcome { - let claims = try_outcome!(req.guard::().await).0; + let claims = try_outcome!(req.guard::().await).0; // Verify key hasn't yet expired if chrono::Utc::now().timestamp() > claims.exp { diff --git a/src/routes/admin.rs b/src/routes/admin.rs index bd6f53f..9da75f5 100644 --- a/src/routes/admin.rs +++ b/src/routes/admin.rs @@ -10,13 +10,13 @@ pub fn routes() -> Vec } #[get("/users")] -async fn get_users(admin: Admin, conn: RbDbConn) -> rb::Result>> +async fn get_users(_admin: Admin, conn: RbDbConn) -> rb::Result>> { Ok(Json(conn.run(|c| db::users::all(c)).await?)) } #[post("/users", data = "")] -async fn create_user(admin: Admin, conn: RbDbConn, user: Json) -> rb::Result<()> +async fn create_user(_admin: Admin, conn: RbDbConn, user: Json) -> rb::Result<()> { Ok(conn .run(move |c| db::users::create(c, &user.into_inner())) From 87d8d8ff0c06f59448a7a874beda6239b2c6ea0e Mon Sep 17 00:00:00 2001 From: Jef Roosens Date: Sun, 29 Aug 2021 20:30:33 +0200 Subject: [PATCH 35/67] Switched to binary-only project --- Cargo.toml | 4 -- src/auth/jwt.rs | 6 +-- src/auth/mod.rs | 91 +++++++++++++++++++++------------------------ src/auth/pass.rs | 61 ++++++++++++++++++++++++++++++ src/db/users.rs | 8 ++-- src/errors.rs | 2 +- src/guards.rs | 11 +++--- src/lib.rs | 17 --------- src/main.rs | 30 ++++++++++++--- src/routes/admin.rs | 19 +++++----- src/routes/auth.rs | 60 ------------------------------ src/routes/mod.rs | 1 - 12 files changed, 152 insertions(+), 158 deletions(-) create mode 100644 src/auth/pass.rs delete mode 100644 src/lib.rs delete mode 100644 src/routes/auth.rs diff --git a/Cargo.toml b/Cargo.toml index ebdf355..ff6ac33 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -4,10 +4,6 @@ version = "0.1.0" authors = ["Jef Roosens "] edition = "2018" -[lib] -name = "rb" -path = "src/lib.rs" - [[bin]] name = "rbd" path = "src/main.rs" diff --git a/src/auth/jwt.rs b/src/auth/jwt.rs index 8767bb3..4b8923f 100644 --- a/src/auth/jwt.rs +++ b/src/auth/jwt.rs @@ -11,7 +11,7 @@ use crate::{ tokens::{NewRefreshToken, RefreshToken}, users::User, }, - errors::RbError, + errors::{RbError, RbResult}, schema::{refresh_tokens::dsl as refresh_tokens, users::dsl as users}, }; @@ -32,7 +32,7 @@ pub struct Claims pub exp: i64, } -pub fn generate_jwt_token(conn: &PgConnection, user: &User) -> crate::Result +pub fn generate_jwt_token(conn: &PgConnection, user: &User) -> RbResult { let secret = std::env::var("JWT_KEY").map_err(|_| RbError::Custom("Missing JWT key."))?; let key: Hmac = Hmac::new_from_slice(secret.as_bytes()) @@ -76,7 +76,7 @@ pub fn generate_jwt_token(conn: &PgConnection, user: &User) -> crate::Result crate::Result +pub fn refresh_token(conn: &PgConnection, refresh_token: &str) -> RbResult { let token_bytes = base64::decode(refresh_token).map_err(|_| RbError::AuthInvalidRefreshToken)?; diff --git a/src/auth/mod.rs b/src/auth/mod.rs index b315e82..f239f9f 100644 --- a/src/auth/mod.rs +++ b/src/auth/mod.rs @@ -1,63 +1,58 @@ -use argon2::verify_encoded; -use diesel::{insert_into, prelude::*, PgConnection}; -use rand::{thread_rng, Rng}; +use rocket::serde::json::Json; +use serde::Deserialize; -use crate::{ - db::users::{NewUser, User}, - errors::RbError, - schema::users::dsl as users, +use self::{ + jwt::{generate_jwt_token, JWTResponse}, + pass::verify_user, }; +use crate::{guards::User, RbDbConn, RbResult}; pub mod jwt; +pub mod pass; -pub fn verify_user(conn: &PgConnection, username: &str, password: &str) -> crate::Result +#[derive(Deserialize)] +pub struct Credentials { - // TODO handle non-"NotFound" Diesel errors accordingely - let user = users::users - .filter(users::username.eq(username)) - .first::(conn) - .map_err(|_| RbError::AuthUnknownUser)?; - - // Check if a user is blocked - if user.blocked { - return Err(RbError::AuthBlockedUser); - } - - match verify_encoded(user.password.as_str(), password.as_bytes()) { - Ok(true) => Ok(user), - _ => Err(RbError::AuthInvalidPassword), - } + username: String, + password: String, } -pub fn hash_password(password: &str) -> crate::Result +#[post("/login")] +pub async fn already_logged_in(_user: User) -> String { - // Generate a random salt - let mut salt = [0u8; 64]; - thread_rng().fill(&mut salt[..]); - - // Encode the actual password - let config = argon2::Config::default(); - argon2::hash_encoded(password.as_bytes(), &salt, &config) - .map_err(|_| RbError::Custom("Couldn't hash password.")) + String::from("You're already logged in!") } -pub fn create_admin_user(conn: &PgConnection, username: &str, password: &str) - -> crate::Result +#[post("/login", data = "", rank = 2)] +pub async fn login(conn: RbDbConn, credentials: Json) -> RbResult> { - let pass_hashed = hash_password(password)?; - let new_user = NewUser { - username: username.to_string(), - password: pass_hashed, - admin: true, - }; + let credentials = credentials.into_inner(); - insert_into(users::users) - .values(&new_user) - .on_conflict(users::username) - .do_update() - .set(&new_user) - .execute(conn) - .map_err(|_| RbError::Custom("Couldn't create admin."))?; + // Get the user, if credentials are valid + let user = conn + .run(move |c| verify_user(c, &credentials.username, &credentials.password)) + .await?; - Ok(true) + Ok(Json(conn.run(move |c| generate_jwt_token(c, &user)).await?)) +} + +#[derive(Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct RefreshTokenRequest +{ + pub refresh_token: String, +} + +#[post("/refresh", data = "")] +pub async fn refresh_token( + conn: RbDbConn, + refresh_token_request: Json, +) -> RbResult> +{ + let refresh_token = refresh_token_request.into_inner().refresh_token; + + Ok(Json( + conn.run(move |c| crate::auth::jwt::refresh_token(c, &refresh_token)) + .await?, + )) } diff --git a/src/auth/pass.rs b/src/auth/pass.rs new file mode 100644 index 0000000..b67bf11 --- /dev/null +++ b/src/auth/pass.rs @@ -0,0 +1,61 @@ +use argon2::verify_encoded; +use diesel::{insert_into, prelude::*, PgConnection}; +use rand::{thread_rng, Rng}; + +use crate::{ + db::users::{NewUser, User}, + errors::RbError, + schema::users::dsl as users, + RbResult, +}; + +pub fn verify_user(conn: &PgConnection, username: &str, password: &str) -> RbResult +{ + // TODO handle non-"NotFound" Diesel errors accordingely + let user = users::users + .filter(users::username.eq(username)) + .first::(conn) + .map_err(|_| RbError::AuthUnknownUser)?; + + // Check if a user is blocked + if user.blocked { + return Err(RbError::AuthBlockedUser); + } + + match verify_encoded(user.password.as_str(), password.as_bytes()) { + Ok(true) => Ok(user), + _ => Err(RbError::AuthInvalidPassword), + } +} + +pub fn hash_password(password: &str) -> RbResult +{ + // Generate a random salt + let mut salt = [0u8; 64]; + thread_rng().fill(&mut salt[..]); + + // Encode the actual password + let config = argon2::Config::default(); + argon2::hash_encoded(password.as_bytes(), &salt, &config) + .map_err(|_| RbError::Custom("Couldn't hash password.")) +} + +pub fn create_admin_user(conn: &PgConnection, username: &str, password: &str) -> RbResult +{ + let pass_hashed = hash_password(password)?; + let new_user = NewUser { + username: username.to_string(), + password: pass_hashed, + admin: true, + }; + + insert_into(users::users) + .values(&new_user) + .on_conflict(users::username) + .do_update() + .set(&new_user) + .execute(conn) + .map_err(|_| RbError::Custom("Couldn't create admin."))?; + + Ok(true) +} diff --git a/src/db/users.rs b/src/db/users.rs index 79d337a..d6ae131 100644 --- a/src/db/users.rs +++ b/src/db/users.rs @@ -3,7 +3,7 @@ use serde::{Deserialize, Serialize}; use uuid::Uuid; use crate::{ - errors::RbError, + errors::{RbError, RbResult}, schema::{users, users::dsl::*}, }; @@ -28,7 +28,7 @@ pub struct NewUser pub admin: bool, } -pub fn all(conn: &PgConnection) -> crate::Result> +pub fn all(conn: &PgConnection) -> RbResult> { users .load::(conn) @@ -40,7 +40,7 @@ pub fn find(conn: &PgConnection, user_id: Uuid) -> Option users.find(user_id).first::(conn).ok() } -pub fn create(conn: &PgConnection, new_user: &NewUser) -> crate::Result<()> +pub fn create(conn: &PgConnection, new_user: &NewUser) -> RbResult<()> { let count = diesel::insert_into(users) .values(new_user) @@ -54,7 +54,7 @@ pub fn create(conn: &PgConnection, new_user: &NewUser) -> crate::Result<()> Ok(()) } -pub fn delete(conn: &PgConnection, user_id: Uuid) -> crate::Result<()> +pub fn delete(conn: &PgConnection, user_id: Uuid) -> RbResult<()> { diesel::delete(users.filter(id.eq(user_id))) .execute(conn) diff --git a/src/errors.rs b/src/errors.rs index daca6bb..2257fa5 100644 --- a/src/errors.rs +++ b/src/errors.rs @@ -84,4 +84,4 @@ impl<'r> Responder<'r, 'static> for RbError } } -pub type Result = std::result::Result; +pub type RbResult = std::result::Result; diff --git a/src/guards.rs b/src/guards.rs index 55df193..532ba97 100644 --- a/src/guards.rs +++ b/src/guards.rs @@ -1,6 +1,5 @@ use hmac::{Hmac, NewMac}; use jwt::VerifyWithKey; -use rb::auth::jwt::Claims; use rocket::{ http::Status, outcome::try_outcome, @@ -8,13 +7,15 @@ use rocket::{ }; use sha2::Sha256; +use crate::auth::jwt::Claims; + /// Extracts a "Authorization: Bearer" string from the headers. pub struct Bearer<'a>(&'a str); #[rocket::async_trait] impl<'r> FromRequest<'r> for Bearer<'r> { - type Error = rb::errors::RbError; + type Error = crate::errors::RbError; async fn from_request(req: &'r Request<'_>) -> Outcome { @@ -44,7 +45,7 @@ pub struct Jwt(Claims); #[rocket::async_trait] impl<'r> FromRequest<'r> for Jwt { - type Error = rb::errors::RbError; + type Error = crate::errors::RbError; async fn from_request(req: &'r Request<'_>) -> Outcome { @@ -87,7 +88,7 @@ pub struct User(Claims); #[rocket::async_trait] impl<'r> FromRequest<'r> for User { - type Error = rb::errors::RbError; + type Error = crate::errors::RbError; async fn from_request(req: &'r Request<'_>) -> Outcome { @@ -108,7 +109,7 @@ pub struct Admin(Claims); #[rocket::async_trait] impl<'r> FromRequest<'r> for Admin { - type Error = rb::errors::RbError; + type Error = crate::errors::RbError; async fn from_request(req: &'r Request<'_>) -> Outcome { diff --git a/src/lib.rs b/src/lib.rs deleted file mode 100644 index 2c7d7f1..0000000 --- a/src/lib.rs +++ /dev/null @@ -1,17 +0,0 @@ -#[macro_use] -extern crate diesel; - -pub mod auth; -pub mod db; -pub mod errors; -pub(crate) mod schema; - -pub use errors::Result; - -// Any import defaults are defined here -/// Expire time for the JWT tokens in seconds. -const JWT_EXP_SECONDS: i64 = 600; -/// Amount of bytes the refresh tokens should consist of -const REFRESH_TOKEN_N_BYTES: usize = 64; -/// Expire time for refresh tokens; here: one week -const REFRESH_TOKEN_EXP_SECONDS: i64 = 604800; diff --git a/src/main.rs b/src/main.rs index 2ca414d..fc15246 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,23 +1,38 @@ // This needs to be explicitely included before diesel is imported to make sure // compilation succeeds in the release Docker image. extern crate openssl; - #[macro_use] extern crate rocket; #[macro_use] extern crate diesel_migrations; +#[macro_use] +extern crate diesel; use rocket::{fairing::AdHoc, Build, Rocket}; -use rocket_sync_db_pools::{database, diesel}; +use rocket_sync_db_pools::database; -pub(crate) mod guards; +pub use crate::errors::{RbError, RbResult}; + +pub mod auth; +pub mod db; +pub mod errors; +pub mod guards; mod routes; +pub(crate) mod schema; -embed_migrations!(); +// Any import defaults are defined here +/// Expire time for the JWT tokens in seconds. +const JWT_EXP_SECONDS: i64 = 600; +/// Amount of bytes the refresh tokens should consist of +const REFRESH_TOKEN_N_BYTES: usize = 64; +/// Expire time for refresh tokens; here: one week +const REFRESH_TOKEN_EXP_SECONDS: i64 = 604800; #[database("postgres_rb")] pub struct RbDbConn(diesel::PgConnection); +embed_migrations!(); + async fn run_db_migrations(rocket: Rocket) -> Result, Rocket> { let conn = RbDbConn::get_one(&rocket) @@ -39,7 +54,7 @@ async fn create_admin_user(rocket: Rocket) -> Result, Rocke .await .expect("database connection"); conn.run(move |c| { - rb::auth::create_admin_user(c, &admin_user, &admin_password) + auth::pass::create_admin_user(c, &admin_user, &admin_password) .expect("failed to create admin user") }) .await; @@ -57,6 +72,9 @@ fn rocket() -> _ run_db_migrations, )) .attach(AdHoc::try_on_ignite("Create admin user", create_admin_user)) - .mount("/api/auth", routes::auth::routes()) + .mount( + "/api/auth", + routes![auth::already_logged_in, auth::login, auth::refresh_token,], + ) .mount("/api/admin", routes::admin::routes()) } diff --git a/src/routes/admin.rs b/src/routes/admin.rs index 9da75f5..741f4dd 100644 --- a/src/routes/admin.rs +++ b/src/routes/admin.rs @@ -1,8 +1,12 @@ -use rb::{db, errors::RbError}; use rocket::serde::json::Json; use uuid::Uuid; -use crate::{guards::Admin, RbDbConn}; +use crate::{ + db, + errors::{RbError, RbResult}, + guards::Admin, + RbDbConn, +}; pub fn routes() -> Vec { @@ -10,13 +14,13 @@ pub fn routes() -> Vec } #[get("/users")] -async fn get_users(_admin: Admin, conn: RbDbConn) -> rb::Result>> +async fn get_users(_admin: Admin, conn: RbDbConn) -> RbResult>> { Ok(Json(conn.run(|c| db::users::all(c)).await?)) } #[post("/users", data = "")] -async fn create_user(_admin: Admin, conn: RbDbConn, user: Json) -> rb::Result<()> +async fn create_user(_admin: Admin, conn: RbDbConn, user: Json) -> RbResult<()> { Ok(conn .run(move |c| db::users::create(c, &user.into_inner())) @@ -24,11 +28,8 @@ async fn create_user(_admin: Admin, conn: RbDbConn, user: Json) -> } #[get("/users/")] -async fn get_user_info( - _admin: Admin, - conn: RbDbConn, - user_id_str: &str, -) -> rb::Result> +async fn get_user_info(_admin: Admin, conn: RbDbConn, user_id_str: &str) + -> RbResult> { let user_id = Uuid::parse_str(user_id_str).map_err(|_| RbError::UMUnknownUser)?; diff --git a/src/routes/auth.rs b/src/routes/auth.rs deleted file mode 100644 index 955cfaa..0000000 --- a/src/routes/auth.rs +++ /dev/null @@ -1,60 +0,0 @@ -use rb::auth::{ - jwt::{generate_jwt_token, JWTResponse}, - verify_user, -}; -use rocket::serde::json::Json; -use serde::Deserialize; - -use crate::{guards::User, RbDbConn}; - -pub(crate) fn routes() -> Vec -{ - routes![login, already_logged_in, refresh_token] -} - -#[derive(Deserialize)] -struct Credentials -{ - username: String, - password: String, -} - -#[post("/login")] -async fn already_logged_in(_user: User) -> String -{ - String::from("You're already logged in!") -} - -#[post("/login", data = "", rank = 2)] -async fn login(conn: RbDbConn, credentials: Json) -> rb::Result> -{ - let credentials = credentials.into_inner(); - - // Get the user, if credentials are valid - let user = conn - .run(move |c| verify_user(c, &credentials.username, &credentials.password)) - .await?; - - Ok(Json(conn.run(move |c| generate_jwt_token(c, &user)).await?)) -} - -#[derive(Deserialize)] -#[serde(rename_all = "camelCase")] -struct RefreshTokenRequest -{ - pub refresh_token: String, -} - -#[post("/refresh", data = "")] -async fn refresh_token( - conn: RbDbConn, - refresh_token_request: Json, -) -> rb::Result> -{ - let refresh_token = refresh_token_request.into_inner().refresh_token; - - Ok(Json( - conn.run(move |c| rb::auth::jwt::refresh_token(c, &refresh_token)) - .await?, - )) -} diff --git a/src/routes/mod.rs b/src/routes/mod.rs index 52815c1..92918b0 100644 --- a/src/routes/mod.rs +++ b/src/routes/mod.rs @@ -1,2 +1 @@ pub mod admin; -pub mod auth; From 1378219fe5bd0d53367268028fe9e6085ba86a95 Mon Sep 17 00:00:00 2001 From: Jef Roosens Date: Sun, 29 Aug 2021 20:41:03 +0200 Subject: [PATCH 36/67] Moved admin functions to correct module --- src/admin.rs | 61 +++++++++++++++++++++++++++++++++++++++++++++ src/auth/mod.rs | 2 +- src/auth/pass.rs | 23 +---------------- src/main.rs | 11 ++++---- src/routes/admin.rs | 40 ----------------------------- src/routes/mod.rs | 1 - 6 files changed, 69 insertions(+), 69 deletions(-) create mode 100644 src/admin.rs delete mode 100644 src/routes/admin.rs delete mode 100644 src/routes/mod.rs diff --git a/src/admin.rs b/src/admin.rs new file mode 100644 index 0000000..2b82622 --- /dev/null +++ b/src/admin.rs @@ -0,0 +1,61 @@ +use diesel::{insert_into, prelude::*, PgConnection}; +use rocket::serde::json::Json; +use uuid::Uuid; + +use crate::{ + auth::pass::hash_password, + db, + errors::{RbError, RbResult}, + guards::Admin, + schema::users::dsl as users, + RbDbConn, +}; + +#[get("/users")] +pub async fn get_users(_admin: Admin, conn: RbDbConn) -> RbResult>> +{ + Ok(Json(conn.run(|c| db::users::all(c)).await?)) +} + +#[post("/users", data = "")] +pub async fn create_user(_admin: Admin, conn: RbDbConn, user: Json) -> RbResult<()> +{ + Ok(conn + .run(move |c| db::users::create(c, &user.into_inner())) + .await?) +} + +#[get("/users/")] +pub async fn get_user_info( + _admin: Admin, + conn: RbDbConn, + user_id_str: &str, +) -> RbResult> +{ + let user_id = Uuid::parse_str(user_id_str).map_err(|_| RbError::UMUnknownUser)?; + + match conn.run(move |c| db::users::find(c, user_id)).await { + Some(user) => Ok(Json(user)), + None => Err(RbError::UMUnknownUser), + } +} + +pub fn create_admin_user(conn: &PgConnection, username: &str, password: &str) -> RbResult +{ + let pass_hashed = hash_password(password)?; + let new_user = db::NewUser { + username: username.to_string(), + password: pass_hashed, + admin: true, + }; + + insert_into(users::users) + .values(&new_user) + .on_conflict(users::username) + .do_update() + .set(&new_user) + .execute(conn) + .map_err(|_| RbError::Custom("Couldn't create admin."))?; + + Ok(true) +} diff --git a/src/auth/mod.rs b/src/auth/mod.rs index f239f9f..b4cbf0f 100644 --- a/src/auth/mod.rs +++ b/src/auth/mod.rs @@ -5,7 +5,7 @@ use self::{ jwt::{generate_jwt_token, JWTResponse}, pass::verify_user, }; -use crate::{guards::User, RbDbConn, RbResult}; +use crate::{errors::RbResult, guards::User, RbDbConn}; pub mod jwt; pub mod pass; diff --git a/src/auth/pass.rs b/src/auth/pass.rs index b67bf11..c8d5b96 100644 --- a/src/auth/pass.rs +++ b/src/auth/pass.rs @@ -4,9 +4,8 @@ use rand::{thread_rng, Rng}; use crate::{ db::users::{NewUser, User}, - errors::RbError, + errors::{RbError, RbResult}, schema::users::dsl as users, - RbResult, }; pub fn verify_user(conn: &PgConnection, username: &str, password: &str) -> RbResult @@ -39,23 +38,3 @@ pub fn hash_password(password: &str) -> RbResult argon2::hash_encoded(password.as_bytes(), &salt, &config) .map_err(|_| RbError::Custom("Couldn't hash password.")) } - -pub fn create_admin_user(conn: &PgConnection, username: &str, password: &str) -> RbResult -{ - let pass_hashed = hash_password(password)?; - let new_user = NewUser { - username: username.to_string(), - password: pass_hashed, - admin: true, - }; - - insert_into(users::users) - .values(&new_user) - .on_conflict(users::username) - .do_update() - .set(&new_user) - .execute(conn) - .map_err(|_| RbError::Custom("Couldn't create admin."))?; - - Ok(true) -} diff --git a/src/main.rs b/src/main.rs index fc15246..400c0e8 100644 --- a/src/main.rs +++ b/src/main.rs @@ -11,13 +11,11 @@ extern crate diesel; use rocket::{fairing::AdHoc, Build, Rocket}; use rocket_sync_db_pools::database; -pub use crate::errors::{RbError, RbResult}; - +mod admin; pub mod auth; pub mod db; pub mod errors; pub mod guards; -mod routes; pub(crate) mod schema; // Any import defaults are defined here @@ -54,7 +52,7 @@ async fn create_admin_user(rocket: Rocket) -> Result, Rocke .await .expect("database connection"); conn.run(move |c| { - auth::pass::create_admin_user(c, &admin_user, &admin_password) + admin::create_admin_user(c, &admin_user, &admin_password) .expect("failed to create admin user") }) .await; @@ -76,5 +74,8 @@ fn rocket() -> _ "/api/auth", routes![auth::already_logged_in, auth::login, auth::refresh_token,], ) - .mount("/api/admin", routes::admin::routes()) + .mount( + "/api/admin", + routes![admin::get_users, admin::create_user, admin::get_user_info], + ) } diff --git a/src/routes/admin.rs b/src/routes/admin.rs deleted file mode 100644 index 741f4dd..0000000 --- a/src/routes/admin.rs +++ /dev/null @@ -1,40 +0,0 @@ -use rocket::serde::json::Json; -use uuid::Uuid; - -use crate::{ - db, - errors::{RbError, RbResult}, - guards::Admin, - RbDbConn, -}; - -pub fn routes() -> Vec -{ - routes![get_users, get_user_info, create_user] -} - -#[get("/users")] -async fn get_users(_admin: Admin, conn: RbDbConn) -> RbResult>> -{ - Ok(Json(conn.run(|c| db::users::all(c)).await?)) -} - -#[post("/users", data = "")] -async fn create_user(_admin: Admin, conn: RbDbConn, user: Json) -> RbResult<()> -{ - Ok(conn - .run(move |c| db::users::create(c, &user.into_inner())) - .await?) -} - -#[get("/users/")] -async fn get_user_info(_admin: Admin, conn: RbDbConn, user_id_str: &str) - -> RbResult> -{ - let user_id = Uuid::parse_str(user_id_str).map_err(|_| RbError::UMUnknownUser)?; - - match conn.run(move |c| db::users::find(c, user_id)).await { - Some(user) => Ok(Json(user)), - None => Err(RbError::UMUnknownUser), - } -} diff --git a/src/routes/mod.rs b/src/routes/mod.rs deleted file mode 100644 index 92918b0..0000000 --- a/src/routes/mod.rs +++ /dev/null @@ -1 +0,0 @@ -pub mod admin; From 02011e04ce387aaf83d9dc1fc60f42117b443c8e Mon Sep 17 00:00:00 2001 From: Jef Roosens Date: Sun, 29 Aug 2021 21:15:10 +0200 Subject: [PATCH 37/67] Moved some JWT db commands to db --- src/auth/jwt.rs | 25 ++++++++++--------------- src/auth/pass.rs | 4 ++-- src/db/tokens.rs | 40 ++++++++++++++++++++++++++++++++++++++-- 3 files changed, 50 insertions(+), 19 deletions(-) diff --git a/src/auth/jwt.rs b/src/auth/jwt.rs index 4b8923f..83e7a1e 100644 --- a/src/auth/jwt.rs +++ b/src/auth/jwt.rs @@ -1,5 +1,5 @@ use chrono::Utc; -use diesel::{insert_into, prelude::*, PgConnection}; +use diesel::{prelude::*, PgConnection}; use hmac::{Hmac, NewMac}; use jwt::SignWithKey; use rand::{thread_rng, Rng}; @@ -7,10 +7,8 @@ use serde::{Deserialize, Serialize}; use sha2::Sha256; use crate::{ - db::{ - tokens::{NewRefreshToken, RefreshToken}, - users::User, - }, + db, + db::{tokens::NewRefreshToken, users::User}, errors::{RbError, RbResult}, schema::{refresh_tokens::dsl as refresh_tokens, users::dsl as users}, }; @@ -61,14 +59,14 @@ pub fn generate_jwt_token(conn: &PgConnection, user: &User) -> RbResult RbResult(conn) - .map_err(|_| RbError::AuthInvalidRefreshToken)?; + let (token_entry, user) = + db::tokens::find_with_user(conn, &token_bytes).ok_or(RbError::AuthInvalidRefreshToken)?; // If we see that the token has already been used before, we block the user. if token_entry.last_used_at.is_some() { diff --git a/src/auth/pass.rs b/src/auth/pass.rs index c8d5b96..1c0f04e 100644 --- a/src/auth/pass.rs +++ b/src/auth/pass.rs @@ -1,9 +1,9 @@ use argon2::verify_encoded; -use diesel::{insert_into, prelude::*, PgConnection}; +use diesel::{prelude::*, PgConnection}; use rand::{thread_rng, Rng}; use crate::{ - db::users::{NewUser, User}, + db::users::User, errors::{RbError, RbResult}, schema::users::dsl as users, }; diff --git a/src/db/tokens.rs b/src/db/tokens.rs index cfe5904..7ca6b1d 100644 --- a/src/db/tokens.rs +++ b/src/db/tokens.rs @@ -1,7 +1,10 @@ -use diesel::{Insertable, Queryable}; +use diesel::{insert_into, prelude::*, Insertable, PgConnection, Queryable}; use uuid::Uuid; -use crate::schema::refresh_tokens; +use crate::{ + errors::{RbError, RbResult}, + schema::{refresh_tokens, refresh_tokens::dsl::*}, +}; #[derive(Queryable)] pub struct RefreshToken @@ -20,3 +23,36 @@ pub struct NewRefreshToken pub user_id: Uuid, pub expires_at: chrono::NaiveDateTime, } + +pub fn all(conn: &PgConnection) -> RbResult> +{ + refresh_tokens + .load::(conn) + .map_err(|_| RbError::DbError("Couldn't get all refresh tokens.")) +} + +pub fn create(conn: &PgConnection, new_refresh_token: &NewRefreshToken) -> RbResult<()> +{ + insert_into(refresh_tokens) + .values(new_refresh_token) + .execute(conn) + .map_err(|_| RbError::Custom("Couldn't insert refresh token."))?; + + // TODO check for conflict? + + Ok(()) +} + +pub fn find_with_user( + conn: &PgConnection, + token_val: &[u8], +) -> Option<(RefreshToken, super::users::User)> +{ + // TODO actually check for errors here + refresh_tokens + .inner_join(crate::schema::users::dsl::users) + .filter(token.eq(token_val)) + .first::<(RefreshToken, super::users::User)>(conn) + .map_err(|_| RbError::Custom("Couldn't get refresh token & user.")) + .ok() +} From 3cf7661faf11321cd36b4a887bfccb94dbdfbc88 Mon Sep 17 00:00:00 2001 From: Jef Roosens Date: Mon, 30 Aug 2021 14:27:54 +0200 Subject: [PATCH 38/67] Switched to yaml-based config --- Cargo.lock | 35 +++++++++++++++++++++++++++++++++++ Cargo.toml | 2 ++ Rb.yaml | 19 +++++++++++++++++++ Rocket.toml | 13 ------------- src/main.rs | 7 ++++++- 5 files changed, 62 insertions(+), 14 deletions(-) create mode 100644 Rb.yaml delete mode 100644 Rocket.toml diff --git a/Cargo.lock b/Cargo.lock index a670689..92643bd 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -296,6 +296,12 @@ version = "1.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "212d0f5754cb6769937f4501cc0e67f4f4483c8d2c3e1e922ee9edbe4ab4c7c0" +[[package]] +name = "dtoa" +version = "0.4.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "56899898ce76aaf4a0f24d914c97ea6ed976d42fec6ad33fcbb0a1103e07b2b0" + [[package]] name = "either" version = "1.6.1" @@ -320,6 +326,7 @@ dependencies = [ "atomic", "pear", "serde", + "serde_yaml", "toml", "uncased", "version_check", @@ -641,6 +648,12 @@ version = "0.2.99" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a7f823d141fe0a24df1e23b4af4e3c7ba9e5966ec514ea068c93024aa7deb765" +[[package]] +name = "linked-hash-map" +version = "0.5.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7fb9b38af92608140b86b693604b9ffcc5824240a484d1ecd4795bacb2fe88f3" + [[package]] name = "lock_api" version = "0.4.4" @@ -1185,6 +1198,7 @@ dependencies = [ "chrono", "diesel", "diesel_migrations", + "figment", "hmac", "jwt", "openssl", @@ -1270,6 +1284,18 @@ dependencies = [ "serde", ] +[[package]] +name = "serde_yaml" +version = "0.8.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "039ba818c784248423789eec090aab9fb566c7b94d6ebbfa1814a9fd52c8afb2" +dependencies = [ + "dtoa", + "linked-hash-map", + "serde", + "yaml-rust", +] + [[package]] name = "sha1" version = "0.6.0" @@ -1738,6 +1764,15 @@ version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" +[[package]] +name = "yaml-rust" +version = "0.4.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "56c1936c4cc7a1c9ab21a1ebb602eb942ba868cbd44a99cb7cdc5892335e1c85" +dependencies = [ + "linked-hash-map", +] + [[package]] name = "yansi" version = "0.5.0" diff --git a/Cargo.toml b/Cargo.toml index ff6ac33..5b80483 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -34,6 +34,8 @@ sha2 = "*" chrono = { version = "*", features = [ "serde" ] } # Encoding of refresh tokens base64 = "0.13.0" +# Reading in configuration files +figment = { version = "*", features = [ "yaml" ] } [profile.release] lto = true diff --git a/Rb.yaml b/Rb.yaml new file mode 100644 index 0000000..3081e7b --- /dev/null +++ b/Rb.yaml @@ -0,0 +1,19 @@ +default: + address: "0.0.0.0" + ports: 8000 + +debug: + keep_alive: 5 + read_timeout: 5 + write_timeout: 5 + log_level: "normal" + limits: + forms: 32768 + + admin_user: "admin" + admin_pass: "password" + jwt_key: "secret" + + databases: + postgres_rb: + url: "postgres://rb:rb@localhost:5432/rb" diff --git a/Rocket.toml b/Rocket.toml deleted file mode 100644 index e931e4d..0000000 --- a/Rocket.toml +++ /dev/null @@ -1,13 +0,0 @@ -[debug] -port = 8000 -keep_alive = 5 -read_timeout = 5 -write_timeout = 5 -log_level = "normal" -limits = { forms = 32768 } - -[debug.databases] -postgres_rb = { url = "postgres://rb:rb@localhost:5432/rb" } - -[release.databases] -postgres_rb = { url = "postgres://rb:rb@localhost:5432/rb" } diff --git a/src/main.rs b/src/main.rs index 400c0e8..92d46fd 100644 --- a/src/main.rs +++ b/src/main.rs @@ -10,6 +10,7 @@ extern crate diesel; use rocket::{fairing::AdHoc, Build, Rocket}; use rocket_sync_db_pools::database; +use figment::{Figment, providers::Env, providers::Yaml, providers::Format}; mod admin; pub mod auth; @@ -63,7 +64,11 @@ async fn create_admin_user(rocket: Rocket) -> Result, Rocke #[launch] fn rocket() -> _ { - rocket::build() + let figment = Figment::from(rocket::config::Config::default()) + .merge(Yaml::file("Rb.yaml").nested()) + .merge(Env::prefixed("RB_").global()); + + rocket::custom(figment) .attach(RbDbConn::fairing()) .attach(AdHoc::try_on_ignite( "Run database migrations", From 2cc4d53961455ccec5145b62e42c0b013f3aa94f Mon Sep 17 00:00:00 2001 From: Jef Roosens Date: Mon, 30 Aug 2021 15:28:01 +0200 Subject: [PATCH 39/67] Moved JWT config to config file --- Rb.yaml | 6 +++++- src/auth/jwt.rs | 24 ++++++++++++++++-------- src/auth/mod.rs | 20 +++++++++++++++----- src/main.rs | 31 ++++++++++++++++++++++--------- 4 files changed, 58 insertions(+), 23 deletions(-) diff --git a/Rb.yaml b/Rb.yaml index 3081e7b..6b40537 100644 --- a/Rb.yaml +++ b/Rb.yaml @@ -12,7 +12,11 @@ debug: admin_user: "admin" admin_pass: "password" - jwt_key: "secret" + jwt: + key: "secret" + refresh_token_size: 64 + # Just 5 seconds for debugging + refresh_token_expire: 5 databases: postgres_rb: diff --git a/src/auth/jwt.rs b/src/auth/jwt.rs index 83e7a1e..89ce734 100644 --- a/src/auth/jwt.rs +++ b/src/auth/jwt.rs @@ -11,6 +11,7 @@ use crate::{ db::{tokens::NewRefreshToken, users::User}, errors::{RbError, RbResult}, schema::{refresh_tokens::dsl as refresh_tokens, users::dsl as users}, + RbJwtConf, }; #[derive(Serialize)] @@ -30,10 +31,13 @@ pub struct Claims pub exp: i64, } -pub fn generate_jwt_token(conn: &PgConnection, user: &User) -> RbResult +pub fn generate_jwt_token( + conn: &PgConnection, + jwt: &RbJwtConf, + user: &User, +) -> RbResult { - let secret = std::env::var("JWT_KEY").map_err(|_| RbError::Custom("Missing JWT key."))?; - let key: Hmac = Hmac::new_from_slice(secret.as_bytes()) + let key: Hmac = Hmac::new_from_slice(jwt.key.as_bytes()) .map_err(|_| RbError::Custom("Couldn't create Hmac key."))?; let current_time = Utc::now(); @@ -43,7 +47,7 @@ pub fn generate_jwt_token(conn: &PgConnection, user: &User) -> RbResult RbResult RbResult RbResult +pub fn refresh_token( + conn: &PgConnection, + jwt: &RbJwtConf, + refresh_token: &str, +) -> RbResult { let token_bytes = base64::decode(refresh_token).map_err(|_| RbError::AuthInvalidRefreshToken)?; @@ -108,5 +116,5 @@ pub fn refresh_token(conn: &PgConnection, refresh_token: &str) -> RbResult String } #[post("/login", data = "", rank = 2)] -pub async fn login(conn: RbDbConn, credentials: Json) -> RbResult> +pub async fn login( + conn: RbDbConn, + conf: &State, + credentials: Json, +) -> RbResult> { let credentials = credentials.into_inner(); + let jwt = conf.jwt.clone(); // Get the user, if credentials are valid let user = conn .run(move |c| verify_user(c, &credentials.username, &credentials.password)) .await?; - Ok(Json(conn.run(move |c| generate_jwt_token(c, &user)).await?)) + Ok(Json( + conn.run(move |c| generate_jwt_token(c, &jwt, &user)) + .await?, + )) } #[derive(Deserialize)] @@ -46,13 +54,15 @@ pub struct RefreshTokenRequest #[post("/refresh", data = "")] pub async fn refresh_token( conn: RbDbConn, + conf: &State, refresh_token_request: Json, ) -> RbResult> { let refresh_token = refresh_token_request.into_inner().refresh_token; + let jwt = conf.jwt.clone(); Ok(Json( - conn.run(move |c| crate::auth::jwt::refresh_token(c, &refresh_token)) + conn.run(move |c| crate::auth::jwt::refresh_token(c, &jwt, &refresh_token)) .await?, )) } diff --git a/src/main.rs b/src/main.rs index 92d46fd..fa147e9 100644 --- a/src/main.rs +++ b/src/main.rs @@ -8,9 +8,13 @@ extern crate diesel_migrations; #[macro_use] extern crate diesel; +use figment::{ + providers::{Env, Format, Yaml}, + Figment, +}; use rocket::{fairing::AdHoc, Build, Rocket}; use rocket_sync_db_pools::database; -use figment::{Figment, providers::Env, providers::Yaml, providers::Format}; +use serde::{Deserialize, Serialize}; mod admin; pub mod auth; @@ -19,14 +23,6 @@ pub mod errors; pub mod guards; pub(crate) mod schema; -// Any import defaults are defined here -/// Expire time for the JWT tokens in seconds. -const JWT_EXP_SECONDS: i64 = 600; -/// Amount of bytes the refresh tokens should consist of -const REFRESH_TOKEN_N_BYTES: usize = 64; -/// Expire time for refresh tokens; here: one week -const REFRESH_TOKEN_EXP_SECONDS: i64 = 604800; - #[database("postgres_rb")] pub struct RbDbConn(diesel::PgConnection); @@ -61,6 +57,22 @@ async fn create_admin_user(rocket: Rocket) -> Result, Rocke Ok(rocket) } +#[derive(Debug, Deserialize, Serialize, Clone)] +pub struct RbJwtConf +{ + key: String, + refresh_token_size: usize, + refresh_token_expire: i64, +} + +#[derive(Debug, Deserialize, Serialize)] +pub struct RbConfig +{ + admin_user: String, + admin_pass: String, + jwt: RbJwtConf, +} + #[launch] fn rocket() -> _ { @@ -75,6 +87,7 @@ fn rocket() -> _ run_db_migrations, )) .attach(AdHoc::try_on_ignite("Create admin user", create_admin_user)) + .attach(AdHoc::config::()) .mount( "/api/auth", routes![auth::already_logged_in, auth::login, auth::refresh_token,], From fb2a6126fedd853635ec89195d5789efda5ec043 Mon Sep 17 00:00:00 2001 From: Jef Roosens Date: Wed, 1 Sep 2021 12:50:33 +0200 Subject: [PATCH 40/67] Moved all database code to db module --- .dockerignore | 4 ++++ .gitignore | 3 +++ src/admin.rs | 10 ++-------- src/auth/jwt.rs | 23 ++++++++--------------- src/auth/mod.rs | 2 +- src/auth/pass.rs | 12 ++++-------- src/db/tokens.rs | 14 ++++++++++++++ src/db/users.rs | 32 +++++++++++++++++++++++++++++++- 8 files changed, 67 insertions(+), 33 deletions(-) create mode 100644 .dockerignore diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 0000000..a2fdc7d --- /dev/null +++ b/.dockerignore @@ -0,0 +1,4 @@ +.vim/ +out/ +vendor/ +target/ diff --git a/.gitignore b/.gitignore index e79787c..ba2dc7a 100644 --- a/.gitignore +++ b/.gitignore @@ -16,3 +16,6 @@ out/ # Added by cargo /target + +.vim/ +vendor/ diff --git a/src/admin.rs b/src/admin.rs index 2b82622..084bd9f 100644 --- a/src/admin.rs +++ b/src/admin.rs @@ -1,4 +1,4 @@ -use diesel::{insert_into, prelude::*, PgConnection}; +use diesel::PgConnection; use rocket::serde::json::Json; use uuid::Uuid; @@ -7,7 +7,6 @@ use crate::{ db, errors::{RbError, RbResult}, guards::Admin, - schema::users::dsl as users, RbDbConn, }; @@ -49,12 +48,7 @@ pub fn create_admin_user(conn: &PgConnection, username: &str, password: &str) -> admin: true, }; - insert_into(users::users) - .values(&new_user) - .on_conflict(users::username) - .do_update() - .set(&new_user) - .execute(conn) + db::users::create_or_update(conn, &new_user) .map_err(|_| RbError::Custom("Couldn't create admin."))?; Ok(true) diff --git a/src/auth/jwt.rs b/src/auth/jwt.rs index 89ce734..0f7d37e 100644 --- a/src/auth/jwt.rs +++ b/src/auth/jwt.rs @@ -1,5 +1,5 @@ use chrono::Utc; -use diesel::{prelude::*, PgConnection}; +use diesel::PgConnection; use hmac::{Hmac, NewMac}; use jwt::SignWithKey; use rand::{thread_rng, Rng}; @@ -8,9 +8,7 @@ use sha2::Sha256; use crate::{ db, - db::{tokens::NewRefreshToken, users::User}, errors::{RbError, RbResult}, - schema::{refresh_tokens::dsl as refresh_tokens, users::dsl as users}, RbJwtConf, }; @@ -34,7 +32,7 @@ pub struct Claims pub fn generate_jwt_token( conn: &PgConnection, jwt: &RbJwtConf, - user: &User, + user: &db::User, ) -> RbResult { let key: Hmac = Hmac::new_from_slice(jwt.key.as_bytes()) @@ -65,7 +63,7 @@ pub fn generate_jwt_token( // Store refresh token in database db::tokens::create( conn, - &NewRefreshToken { + &db::NewRefreshToken { token: refresh_token.to_vec(), user_id: user.id, expires_at: refresh_expire, @@ -93,11 +91,10 @@ pub fn refresh_token( // If we see that the token has already been used before, we block the user. if token_entry.last_used_at.is_some() { - let target = users::users.filter(users::id.eq(token_entry.user_id)); - diesel::update(target) - .set(users::blocked.eq(true)) - .execute(conn) - .map_err(|_| RbError::Custom("Couldn't block user."))?; + // If we fail to block the user, the end user must know + if let Err(err) = db::users::block(conn, token_entry.user_id) { + return Err(err); + } return Err(RbError::AuthDuplicateRefreshToken); } @@ -110,11 +107,7 @@ pub fn refresh_token( } // We update the last_used_at value for the refresh token - let target = refresh_tokens::refresh_tokens.filter(refresh_tokens::token.eq(token_entry.token)); - diesel::update(target) - .set(refresh_tokens::last_used_at.eq(cur_time)) - .execute(conn) - .map_err(|_| RbError::Custom("Couldn't update last used time."))?; + db::tokens::update_last_used_at(conn, &token_entry.token, cur_time)?; generate_jwt_token(conn, jwt, &user) } diff --git a/src/auth/mod.rs b/src/auth/mod.rs index 78a4fa0..4b3207f 100644 --- a/src/auth/mod.rs +++ b/src/auth/mod.rs @@ -5,7 +5,7 @@ use self::{ jwt::{generate_jwt_token, JWTResponse}, pass::verify_user, }; -use crate::{errors::RbResult, guards::User, RbConfig, RbDbConn, RbJwtConf}; +use crate::{errors::RbResult, guards::User, RbConfig, RbDbConn}; pub mod jwt; pub mod pass; diff --git a/src/auth/pass.rs b/src/auth/pass.rs index 1c0f04e..583908d 100644 --- a/src/auth/pass.rs +++ b/src/auth/pass.rs @@ -1,20 +1,16 @@ use argon2::verify_encoded; -use diesel::{prelude::*, PgConnection}; +use diesel::PgConnection; use rand::{thread_rng, Rng}; use crate::{ - db::users::User, + db, errors::{RbError, RbResult}, - schema::users::dsl as users, }; -pub fn verify_user(conn: &PgConnection, username: &str, password: &str) -> RbResult +pub fn verify_user(conn: &PgConnection, username: &str, password: &str) -> RbResult { // TODO handle non-"NotFound" Diesel errors accordingely - let user = users::users - .filter(users::username.eq(username)) - .first::(conn) - .map_err(|_| RbError::AuthUnknownUser)?; + let user = db::users::find_by_username(conn, username).map_err(|_| RbError::AuthUnknownUser)?; // Check if a user is blocked if user.blocked { diff --git a/src/db/tokens.rs b/src/db/tokens.rs index 7ca6b1d..8940721 100644 --- a/src/db/tokens.rs +++ b/src/db/tokens.rs @@ -56,3 +56,17 @@ pub fn find_with_user( .map_err(|_| RbError::Custom("Couldn't get refresh token & user.")) .ok() } + +pub fn update_last_used_at( + conn: &PgConnection, + token_: &[u8], + last_used_at_: chrono::NaiveDateTime, +) -> RbResult<()> +{ + diesel::update(refresh_tokens.filter(token.eq(token_))) + .set(last_used_at.eq(last_used_at_)) + .execute(conn) + .map_err(|_| RbError::DbError("Couldn't update last_used_at."))?; + + Ok(()) +} diff --git a/src/db/users.rs b/src/db/users.rs index d6ae131..efc74db 100644 --- a/src/db/users.rs +++ b/src/db/users.rs @@ -14,7 +14,6 @@ pub struct User pub username: String, #[serde(skip_serializing)] pub password: String, - #[serde(skip_serializing)] pub blocked: bool, pub admin: bool, } @@ -40,6 +39,14 @@ pub fn find(conn: &PgConnection, user_id: Uuid) -> Option users.find(user_id).first::(conn).ok() } +pub fn find_by_username(conn: &PgConnection, username_: &str) -> RbResult +{ + Ok(users + .filter(username.eq(username_)) + .first::(conn) + .map_err(|_| RbError::DbError("Couldn't find users by username."))?) +} + pub fn create(conn: &PgConnection, new_user: &NewUser) -> RbResult<()> { let count = diesel::insert_into(users) @@ -54,6 +61,19 @@ pub fn create(conn: &PgConnection, new_user: &NewUser) -> RbResult<()> Ok(()) } +pub fn create_or_update(conn: &PgConnection, new_user: &NewUser) -> RbResult<()> +{ + diesel::insert_into(users) + .values(new_user) + .on_conflict(username) + .do_update() + .set(new_user) + .execute(conn) + .map_err(|_| RbError::DbError("Couldn't create or update user."))?; + + Ok(()) +} + pub fn delete(conn: &PgConnection, user_id: Uuid) -> RbResult<()> { diesel::delete(users.filter(id.eq(user_id))) @@ -62,3 +82,13 @@ pub fn delete(conn: &PgConnection, user_id: Uuid) -> RbResult<()> Ok(()) } + +pub fn block(conn: &PgConnection, user_id: Uuid) -> RbResult<()> +{ + diesel::update(users.filter(id.eq(user_id))) + .set(blocked.eq(true)) + .execute(conn) + .map_err(|_| RbError::DbError("Couldn't block user."))?; + + Ok(()) +} From f50008ff9994dfbe02216679fe2ae0e8cc63d4b8 Mon Sep 17 00:00:00 2001 From: Jef Roosens Date: Wed, 1 Sep 2021 16:18:48 +0200 Subject: [PATCH 41/67] Fixed guard still using env var for jwt key --- Rb.yaml | 2 +- rustfmt.toml | 2 +- src/guards.rs | 31 ++++++++++++++---------- tests/admin.py | 64 ++++++++++++++++++++++++++++++++++++++++++++++++++ 4 files changed, 84 insertions(+), 15 deletions(-) create mode 100644 tests/admin.py diff --git a/Rb.yaml b/Rb.yaml index 6b40537..c944758 100644 --- a/Rb.yaml +++ b/Rb.yaml @@ -16,7 +16,7 @@ debug: key: "secret" refresh_token_size: 64 # Just 5 seconds for debugging - refresh_token_expire: 5 + refresh_token_expire: 60 databases: postgres_rb: diff --git a/rustfmt.toml b/rustfmt.toml index 03acab0..5e52857 100644 --- a/rustfmt.toml +++ b/rustfmt.toml @@ -49,7 +49,7 @@ reorder_imports = true reorder_modules = true report_fixme = "Always" report_todo = "Always" -required_version = "1.4.36" +required_version = "1.4.37" skip_children = false space_after_colon = true space_before_colon = false diff --git a/src/guards.rs b/src/guards.rs index 532ba97..d515d77 100644 --- a/src/guards.rs +++ b/src/guards.rs @@ -1,13 +1,16 @@ +use std::convert::From; + use hmac::{Hmac, NewMac}; use jwt::VerifyWithKey; use rocket::{ http::Status, outcome::try_outcome, request::{FromRequest, Outcome, Request}, + State, }; use sha2::Sha256; -use crate::auth::jwt::Claims; +use crate::{auth::jwt::Claims, errors::RbError, RbConfig}; /// Extracts a "Authorization: Bearer" string from the headers. pub struct Bearer<'a>(&'a str); @@ -42,26 +45,28 @@ impl<'r> FromRequest<'r> for Bearer<'r> /// Verifies the provided JWT is valid. pub struct Jwt(Claims); +impl From<()> for RbError +{ + fn from(_: ()) -> Self + { + RbError::Custom("Couldn't get config guard.") + } +} + #[rocket::async_trait] impl<'r> FromRequest<'r> for Jwt { - type Error = crate::errors::RbError; + type Error = RbError; async fn from_request(req: &'r Request<'_>) -> Outcome { let bearer = try_outcome!(req.guard::().await).0; + let config = try_outcome!(req.guard::<&State>().await.map_failure(|_| ( + Status::InternalServerError, + RbError::Custom("Couldn't get config guard.") + ))); - // Get secret & key - let secret = match std::env::var("JWT_KEY") { - Ok(key) => key, - Err(_) => { - return Outcome::Failure(( - Status::InternalServerError, - Self::Error::AuthUnauthorized, - )) - } - }; - let key: Hmac = match Hmac::new_from_slice(secret.as_bytes()) { + let key: Hmac = match Hmac::new_from_slice(&config.jwt.key.as_bytes()) { Ok(key) => key, Err(_) => { return Outcome::Failure(( diff --git a/tests/admin.py b/tests/admin.py new file mode 100644 index 0000000..19a1c5b --- /dev/null +++ b/tests/admin.py @@ -0,0 +1,64 @@ +import requests + + +class RbClient: + def __init__(self, username, password, base_url = "http://localhost:8000/api"): + self.username = username + self.password = password + self.base_url = base_url + + self.jwt = None + self.refresh_token = None + + def _login(self): + r = requests.post(f"{self.base_url}/auth/login", json={ + "username": self.username, + "password": self.password, + }) + + if r.status_code != 200: + raise Exception("Couldn't login") + + res = r.json() + self.jwt = res["token"] + self.refresh_token = res["refreshToken"] + + def _refresh(self): + r = requests.post(f"{self.base_url}/auth/refresh", json={"refreshToken": self.refresh_token}) + + if r.status_code != 200: + raise Exception("Couldn't refresh") + + res = r.json() + self.jwt = res["token"] + self.refresh_token = res["refreshToken"] + + def _request(self, type_, url, retry=2, *args, **kwargs): + if self.jwt: + headers = kwargs.get("headers", {}) + headers["Authorization"] = f"Bearer {self.jwt}" + kwargs["headers"] = headers + print(kwargs["headers"]) + + r = requests.request(type_, url, *args, **kwargs) + + if r.status_code != 200 and retry > 0: + if self.refresh_token: + self._refresh() + + else: + self._login() + + r = self._request(type_, url, *args, **kwargs, retry=retry - 1) + + return r + + def get(self, url, *args, **kwargs): + return self._request("GET", f"{self.base_url}{url}", *args, **kwargs) + + + +if __name__ == "__main__": + client = RbClient("admin", "password") + + print(client.get("/admin/users").json()) From 505907d3a1073a3ea02ca9e50b7dc981d6d65b98 Mon Sep 17 00:00:00 2001 From: Jef Roosens Date: Wed, 1 Sep 2021 17:29:39 +0200 Subject: [PATCH 42/67] Default catcher now returns json --- src/errors.rs | 3 +++ src/guards.rs | 16 +++------------- src/main.rs | 14 +++++++++++++- 3 files changed, 19 insertions(+), 14 deletions(-) diff --git a/src/errors.rs b/src/errors.rs index 2257fa5..bb7856a 100644 --- a/src/errors.rs +++ b/src/errors.rs @@ -16,6 +16,7 @@ pub enum RbError AuthRefreshTokenExpired, AuthInvalidRefreshToken, AuthDuplicateRefreshToken, + AuthMissingHeader, // UM = User Management UMDuplicateUser, @@ -39,6 +40,7 @@ impl RbError RbError::AuthRefreshTokenExpired => Status::Unauthorized, RbError::AuthInvalidRefreshToken => Status::Unauthorized, RbError::AuthDuplicateRefreshToken => Status::Unauthorized, + RbError::AuthMissingHeader => Status::BadRequest, RbError::UMDuplicateUser => Status::Conflict, @@ -60,6 +62,7 @@ impl RbError RbError::AuthDuplicateRefreshToken => { "This refresh token has already been used. The user has been blocked." } + RbError::AuthMissingHeader => "Missing Authorization header.", RbError::UMDuplicateUser => "This user already exists.", diff --git a/src/guards.rs b/src/guards.rs index d515d77..7b40bdd 100644 --- a/src/guards.rs +++ b/src/guards.rs @@ -1,5 +1,3 @@ -use std::convert::From; - use hmac::{Hmac, NewMac}; use jwt::VerifyWithKey; use rocket::{ @@ -24,7 +22,7 @@ impl<'r> FromRequest<'r> for Bearer<'r> { // If the header isn't present, just forward to the next route let header = match req.headers().get_one("Authorization") { - None => return Outcome::Forward(()), + None => return Outcome::Failure((Status::BadRequest, Self::Error::AuthMissingHeader)), Some(val) => val, }; @@ -35,7 +33,7 @@ impl<'r> FromRequest<'r> for Bearer<'r> // Extract the jwt token from the header let auth_string = match header.get(7..) { Some(s) => s, - None => return Outcome::Forward(()), + None => return Outcome::Failure((Status::Unauthorized, Self::Error::AuthUnauthorized)), }; Outcome::Success(Self(auth_string)) @@ -45,14 +43,6 @@ impl<'r> FromRequest<'r> for Bearer<'r> /// Verifies the provided JWT is valid. pub struct Jwt(Claims); -impl From<()> for RbError -{ - fn from(_: ()) -> Self - { - RbError::Custom("Couldn't get config guard.") - } -} - #[rocket::async_trait] impl<'r> FromRequest<'r> for Jwt { @@ -123,7 +113,7 @@ impl<'r> FromRequest<'r> for Admin if user.admin { Outcome::Success(Self(user)) } else { - Outcome::Forward(()) + Outcome::Failure((Status::Unauthorized, RbError::AuthUnauthorized)) } } } diff --git a/src/main.rs b/src/main.rs index fa147e9..d4ee778 100644 --- a/src/main.rs +++ b/src/main.rs @@ -12,7 +12,12 @@ use figment::{ providers::{Env, Format, Yaml}, Figment, }; -use rocket::{fairing::AdHoc, Build, Rocket}; +use rocket::{ + fairing::AdHoc, + http::Status, + serde::json::{json, Value}, + Build, Request, Rocket, +}; use rocket_sync_db_pools::database; use serde::{Deserialize, Serialize}; @@ -26,6 +31,12 @@ pub(crate) mod schema; #[database("postgres_rb")] pub struct RbDbConn(diesel::PgConnection); +#[catch(default)] +fn default_catcher(status: Status, _: &Request) -> Value +{ + json!({"status": status.code, "message": ""}) +} + embed_migrations!(); async fn run_db_migrations(rocket: Rocket) -> Result, Rocket> @@ -88,6 +99,7 @@ fn rocket() -> _ )) .attach(AdHoc::try_on_ignite("Create admin user", create_admin_user)) .attach(AdHoc::config::()) + .register("/", catchers![default_catcher]) .mount( "/api/auth", routes![auth::already_logged_in, auth::login, auth::refresh_token,], From a6b1b0ff76ab2d9fe514e4f9dd9b66fc732e35ef Mon Sep 17 00:00:00 2001 From: Jef Roosens Date: Fri, 3 Sep 2021 16:31:06 +0200 Subject: [PATCH 43/67] Added mimalloc as allocator --- Cargo.lock | 19 +++++++++++++++++++ Cargo.toml | 3 ++- src/main.rs | 3 +++ 3 files changed, 24 insertions(+), 1 deletion(-) diff --git a/Cargo.lock b/Cargo.lock index 92643bd..b10ec77 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -648,6 +648,15 @@ version = "0.2.99" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a7f823d141fe0a24df1e23b4af4e3c7ba9e5966ec514ea068c93024aa7deb765" +[[package]] +name = "libmimalloc-sys" +version = "0.1.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1d1b8479c593dba88c2741fc50b92e13dbabbbe0bd504d979f244ccc1a5b1c01" +dependencies = [ + "cc", +] + [[package]] name = "linked-hash-map" version = "0.5.4" @@ -712,6 +721,15 @@ dependencies = [ "syn", ] +[[package]] +name = "mimalloc" +version = "0.1.26" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fb74897ce508e6c49156fd1476fc5922cbc6e75183c65e399c765a09122e5130" +dependencies = [ + "libmimalloc-sys", +] + [[package]] name = "mime" version = "0.3.16" @@ -1201,6 +1219,7 @@ dependencies = [ "figment", "hmac", "jwt", + "mimalloc", "openssl", "rand", "rocket", diff --git a/Cargo.toml b/Cargo.toml index 5b80483..c6f71fa 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -36,8 +36,9 @@ chrono = { version = "*", features = [ "serde" ] } base64 = "0.13.0" # Reading in configuration files figment = { version = "*", features = [ "yaml" ] } +mimalloc = { version = "0.1.26", default_features = false } [profile.release] -lto = true +lto = "fat" panic = "abort" codegen-units = 1 diff --git a/src/main.rs b/src/main.rs index d4ee778..2819e79 100644 --- a/src/main.rs +++ b/src/main.rs @@ -28,6 +28,9 @@ pub mod errors; pub mod guards; pub(crate) mod schema; +#[global_allocator] +static GLOBAL: mimalloc::MiMalloc = mimalloc::MiMalloc; + #[database("postgres_rb")] pub struct RbDbConn(diesel::PgConnection); From a29523786389a17feef1a59d5bc1bb0ab20c2bc6 Mon Sep 17 00:00:00 2001 From: Jef Roosens Date: Sun, 5 Sep 2021 09:59:16 +0200 Subject: [PATCH 44/67] Some random stuff tbh --- .dockerignore | 17 +++++++++++++---- Dockerfile | 11 +++++++++++ Makefile | 22 ++++++++++++++++++++-- build.sh | 15 +++++++++++++++ src/auth/jwt.rs | 5 +++++ 5 files changed, 64 insertions(+), 6 deletions(-) create mode 100644 Dockerfile create mode 100644 build.sh diff --git a/.dockerignore b/.dockerignore index a2fdc7d..04c13db 100644 --- a/.dockerignore +++ b/.dockerignore @@ -1,4 +1,13 @@ -.vim/ -out/ -vendor/ -target/ +* + +!Cargo.lock +!Cargo.toml +!Makefile +!migrations +!Rb.yaml +!rustfmt.toml +!src +!tests +!web + +web/node_modules diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..f9e3326 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,11 @@ +FROM rust:1.54 + +RUN apt update && \ + apt install -y --no-install-recommends \ + musl-dev \ + musl-tools \ + libpq-dev \ + libssl-dev && \ + rustup target add x86_64-unknown-linux-musl + +WORKDIR /usr/src/app diff --git a/Makefile b/Makefile index 0f1146d..8a4f1fe 100644 --- a/Makefile +++ b/Makefile @@ -25,6 +25,7 @@ CORES != nproc export CC=musl-gcc -fPIC -pie -static export LD_LIBRARY_PATH=$(PREFIX) export PKG_CONFIG_PATH=/usr/local/lib/pkgconfig +export PATH := /usr/local/bin:/root/.cargo/bin:$(PATH) # TODO check for header files (openssl-dev, libpq-dev) both for Arch & Ubuntu @@ -37,15 +38,32 @@ $(shell mkdir -p "$(PREFIX)") .PHONY: all all: build +.PHONY: docker +docker: + docker run \ + --rm \ + -v "$$PWD:/usr/src" \ + --workdir "/usr/src" \ + -it \ + rust:1.54 \ + bash build.sh + + # libpq builds openssl as a dependency .PHONY: build build: libpq .PHONY: clean clean: + echo "$$PATH" @ echo "Note: this only cleans the C dependencies, not the Cargo cache." rm -rf "$(PQ_DIR)" "$(OPENSSL_DIR)" "$(DI_DIR)" "$(PREFIX)" +# This is used inside the Dockerfile +.PHONY: pathfile +pathfile: + echo "$(PREFIX)/lib" >> /etc/ld-musl-x86_64.path + # =====OPENSSL===== # Download the source code & configure the project @@ -53,7 +71,7 @@ $(OPENSSL_DIR)/Configure: curl -sSL "https://www.openssl.org/source/openssl-$(SSL_VER).tar.gz" | \ tar -C "$(OUT_DIR)" -xz cd "$(OPENSSL_DIR)" && \ - CC="$$CC -idirafter /usr/include" ./Configure \ + CC="$$CC -idirafter /usr/include -idirafter /usr/include/x86_64-linux-gnu/" ./Configure \ no-zlib \ no-shared \ --prefix="$(PREFIX)" \ @@ -63,7 +81,7 @@ $(OPENSSL_DIR)/Configure: # Build OpenSSL .PHONY: openssl openssl: $(OPENSSL_DIR)/Configure - C_INCLUDE_PATH="$(PREFIX)/include" $(MAKE) -C "$(OPENSSL_DIR)" depend + env C_INCLUDE_PATH="$(PREFIX)/include" $(MAKE) -C "$(OPENSSL_DIR)" depend $(MAKE) -C "$(OPENSSL_DIR)" -j$(CORES) $(MAKE) -C "$(OPENSSL_DIR)" install_sw diff --git a/build.sh b/build.sh new file mode 100644 index 0000000..a6f57cd --- /dev/null +++ b/build.sh @@ -0,0 +1,15 @@ +#!/usr/bin/env bash + +set -e + +# Install build dependencies +apt update +apt install \ + -y --no-install-recommends \ + musl-dev \ + musl-tools \ + libssl-dev \ + libpq-dev + +make pathfile +make diff --git a/src/auth/jwt.rs b/src/auth/jwt.rs index 0f7d37e..997f68e 100644 --- a/src/auth/jwt.rs +++ b/src/auth/jwt.rs @@ -99,6 +99,11 @@ pub fn refresh_token( return Err(RbError::AuthDuplicateRefreshToken); } + // Then we check if the user is blocked + if user.blocked { + return Err(RbError::AuthBlockedUser); + } + // Now we check if the token has already expired let cur_time = Utc::now().naive_utc(); From 3d024db2e955d3ff6a090db6f09bb352df19826b Mon Sep 17 00:00:00 2001 From: Jef Roosens Date: Sun, 5 Sep 2021 11:19:18 +0200 Subject: [PATCH 45/67] This compilation is gonna kill me --- Dockerfile | 10 +++++- Makefile | 68 ++++++++++++++++++++++++++------------- Rb.yaml | 12 +++++++ build.sh | 15 ++++----- docker-compose.test.yml | 27 ++++++++++++++++ docker/Dockerfile.builder | 28 ++++++++++++++++ src/main.rs | 15 ++++----- 7 files changed, 135 insertions(+), 40 deletions(-) create mode 100644 docker-compose.test.yml create mode 100644 docker/Dockerfile.builder diff --git a/Dockerfile b/Dockerfile index f9e3326..7a7ee23 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,11 +1,19 @@ FROM rust:1.54 +ENV PREFIX="/usr/src/out/prefix" \ + CC="musl-gcc -fPIC -pie -static" \ + LD_LIBRARY_PATH="$PREFIX" \ + PKG_CONFIG_PATH="/usr/local/lib/pkgconfig" \ + PATH="/usr/local/bin:/root/.cargo/bin:$PATH" + RUN apt update && \ apt install -y --no-install-recommends \ musl-dev \ musl-tools \ libpq-dev \ libssl-dev && \ - rustup target add x86_64-unknown-linux-musl + rustup target add x86_64-unknown-linux-musl && \ + mkdir "$PREFIX" && \ + echo "$PREFIX/lib" >> /etc/ld-musl-x86_64.path WORKDIR /usr/src/app diff --git a/Makefile b/Makefile index 8a4f1fe..62cb5c4 100644 --- a/Makefile +++ b/Makefile @@ -22,9 +22,9 @@ CORES != nproc # =====ENVIRONMENT VARIABLES===== -export CC=musl-gcc -fPIC -pie -static -export LD_LIBRARY_PATH=$(PREFIX) -export PKG_CONFIG_PATH=/usr/local/lib/pkgconfig +export CC := musl-gcc -fPIC -pie -static +export LD_LIBRARY_PATH := $(PREFIX) +export PKG_CONFIG_PATH := /usr/local/lib/pkgconfig export PATH := /usr/local/bin:/root/.cargo/bin:$(PATH) @@ -34,18 +34,23 @@ export PATH := /usr/local/bin:/root/.cargo/bin:$(PATH) $(shell mkdir -p "$(PREFIX)") -# ====RECIPES==== +# =====BUILDING THE STATIC BINARY===== .PHONY: all all: build +.PHONY: builder +builder: + docker build \ + -t rusty-builder:latest - < docker/Dockerfile.builder + .PHONY: docker -docker: +docker: builder docker run \ --rm \ -v "$$PWD:/usr/src" \ --workdir "/usr/src" \ -it \ - rust:1.54 \ + rusty-builder:latest \ bash build.sh @@ -54,10 +59,9 @@ docker: build: libpq .PHONY: clean -clean: - echo "$$PATH" +clean: clean-openssl clean-libpq clean-di @ echo "Note: this only cleans the C dependencies, not the Cargo cache." - rm -rf "$(PQ_DIR)" "$(OPENSSL_DIR)" "$(DI_DIR)" "$(PREFIX)" + rm -rf "$(PREFIX)" # This is used inside the Dockerfile .PHONY: pathfile @@ -65,13 +69,13 @@ pathfile: echo "$(PREFIX)/lib" >> /etc/ld-musl-x86_64.path -# =====OPENSSL===== +## =====OPENSSL===== # Download the source code & configure the project $(OPENSSL_DIR)/Configure: curl -sSL "https://www.openssl.org/source/openssl-$(SSL_VER).tar.gz" | \ - tar -C "$(OUT_DIR)" -xz + tar -xzC "$(OUT_DIR)" cd "$(OPENSSL_DIR)" && \ - CC="$$CC -idirafter /usr/include -idirafter /usr/include/x86_64-linux-gnu/" ./Configure \ + CC="$(CC) -idirafter /usr/include -idirafter /usr/include/x86_64-linux-gnu/" ./Configure \ no-zlib \ no-shared \ --prefix="$(PREFIX)" \ @@ -81,16 +85,20 @@ $(OPENSSL_DIR)/Configure: # Build OpenSSL .PHONY: openssl openssl: $(OPENSSL_DIR)/Configure - env C_INCLUDE_PATH="$(PREFIX)/include" $(MAKE) -C "$(OPENSSL_DIR)" depend - $(MAKE) -C "$(OPENSSL_DIR)" -j$(CORES) - $(MAKE) -C "$(OPENSSL_DIR)" install_sw + cd "$(OPENSSL_DIR)" && env C_INCLUDE_PATH="$(PREFIX)/include" $(MAKE) depend 2> /dev/null + cd "$(OPENSSL_DIR)" && $(MAKE) -j$(CORES) + cd "$(OPENSSL_DIR)" && $(MAKE) install_sw + +.PHONY: clean-openssl +clean-openssl: + rm -rf "$(OPENSSL_DIR)" -# =====LIBPQ===== +## =====LIBPQ===== # Download the source code & configure the project $(PQ_DIR)/configure: curl -sSL "https://ftp.postgresql.org/pub/source/v$(PQ_VER)/postgresql-$(PQ_VER).tar.gz" | \ - tar -C "$(OUT_DIR)" -xz + tar -xzC "$(OUT_DIR)" cd "$(PQ_DIR)" && \ LDFLAGS="-L$(PREFIX)/lib" CFLAGS="-I$(PREFIX)/include" ./configure \ --without-readline \ @@ -101,10 +109,14 @@ $(PQ_DIR)/configure: .PHONY: libpq libpq: openssl $(PQ_DIR)/configure - make -C "$(PQ_DIR)/src/interfaces/libpq" -j$(CORES) all-static-lib - make -C "$(PQ_DIR)/src/interfaces/libpq" install install-lib-static - make -C "$(PQ_DIR)/src/bin/pg_config" -j $(CORES) - make -C "$(PQ_DIR)/src/bin/pg_config" install + cd "$(PQ_DIR)/src/interfaces/libpq" && $(MAKE) -j$(CORES) all-static-lib + cd "$(PQ_DIR)/src/interfaces/libpq" && $(MAKE) install install-lib-static + cd "$(PQ_DIR)/src/bin/pg_config" && $(MAKE) -j$(CORES) + cd "$(PQ_DIR)/src/bin/pg_config" && $(MAKE) install + +.PHONY: clean-libpq +clean-libpq: + rm -rf "$(PQ_DIR)" # =====DUMB-INIT===== @@ -113,6 +125,16 @@ $(DI_DIR)/Makefile: curl -sSL "https://github.com/Yelp/dumb-init/archive/refs/tags/v$(DI_VER).tar.gz" | \ tar -C "$(OUT_DIR)" -xz -.PHONY: dumb-init -dumb-init: $(DI_DIR)/Makefile +.PHONY: di +di: $(DI_DIR)/Makefile make -C "$(DI_DIR)" build + +.PHONY: clean-di +clean-di: + rm -rf "$(DI_DIR)" + + +# ====UTILITIES FOR DEVELOPMENT===== +## The tests require a database, so we run them like this +test: + docker-compose -f docker-compose.test.yml -p rb_test up diff --git a/Rb.yaml b/Rb.yaml index c944758..e3f980a 100644 --- a/Rb.yaml +++ b/Rb.yaml @@ -21,3 +21,15 @@ debug: databases: postgres_rb: url: "postgres://rb:rb@localhost:5432/rb" + +release: + admin_user: "admin" + admin_pass: "password" + jwt: + key: "secret" + refresh_token_size: 64 + refresh_token_expire: 86400 + + databases: + postgres_rb: + url: "postgres://rb:rb@db:5432/rb" diff --git a/build.sh b/build.sh index a6f57cd..1d1c93f 100644 --- a/build.sh +++ b/build.sh @@ -3,13 +3,12 @@ set -e # Install build dependencies -apt update -apt install \ - -y --no-install-recommends \ - musl-dev \ - musl-tools \ - libssl-dev \ - libpq-dev +# apt update +# apt install \ +# -y --no-install-recommends \ +# musl-dev \ +# musl-tools \ +# libssl-dev \ +# libpq-dev -make pathfile make diff --git a/docker-compose.test.yml b/docker-compose.test.yml new file mode 100644 index 0000000..84f8657 --- /dev/null +++ b/docker-compose.test.yml @@ -0,0 +1,27 @@ +version: '3' + +services: + app: + build: + context: '.' + dockerfile: 'docker/test/Dockerfile' + + image: 'rb-builder:1.54' + command: "${CMD}" + + working_dir: "/usr/src/app" + + volumes: + - '$PWD:/usr/src/app' + - 'cache:/usr/src/app/out' + + db: + image: 'postgres:13-alpine' + + environment: + - 'POSTGRES_DB=rb' + - 'POSTGRES_USER=rb' + - 'POSTGRES_PASSWORD=rb' + +volumes: + cache: diff --git a/docker/Dockerfile.builder b/docker/Dockerfile.builder new file mode 100644 index 0000000..5c87d30 --- /dev/null +++ b/docker/Dockerfile.builder @@ -0,0 +1,28 @@ +# vim: ft=dockerfile +FROM rust:1.54 + +ENV PREFIX="/usr/src/out/prefix" \ + CC="musl-gcc -fPIC -pie -static" \ + LD_LIBRARY_PATH="$PREFIX" \ + PKG_CONFIG_PATH="/usr/local/lib/pkgconfig" \ + PATH="/usr/local/bin:/root/.cargo/bin:$PATH" + +WORKDIR /usr/src/app + +RUN groupadd -g 1000 builder && \ + useradd -u 1000 -g 1000 builder && \ + mkdir -p "$PREFIX" && \ + chown -R builder:builder /usr/src/app && \ + apt update && \ + apt install -y --no-install-recommends \ + musl-dev \ + musl-tools \ + libpq-dev \ + libssl-dev && \ + rustup target add x86_64-unknown-linux-musl && \ + echo "$PREFIX/lib" >> /etc/ld-musl-x86_64.path + + +USER builder + +CMD ["cargo", "test"] diff --git a/src/main.rs b/src/main.rs index 2819e79..2c0c7c4 100644 --- a/src/main.rs +++ b/src/main.rs @@ -16,7 +16,7 @@ use rocket::{ fairing::AdHoc, http::Status, serde::json::{json, Value}, - Build, Request, Rocket, + Build, Request, Rocket, Orbit, }; use rocket_sync_db_pools::database; use serde::{Deserialize, Serialize}; @@ -54,21 +54,20 @@ async fn run_db_migrations(rocket: Rocket) -> Result, Rocke .await } -async fn create_admin_user(rocket: Rocket) -> Result, Rocket> +async fn create_admin_user<'a>(rocket: &'a Rocket) { - let admin_user = std::env::var("ADMIN_USER").unwrap_or(String::from("admin")); - let admin_password = std::env::var("ADMIN_PASSWORD").unwrap_or(String::from("password")); + let config = rocket.state::().expect("RbConfig instance"); + let admin_user = config.admin_user.clone(); + let admin_pass = config.admin_pass.clone(); let conn = RbDbConn::get_one(&rocket) .await .expect("database connection"); conn.run(move |c| { - admin::create_admin_user(c, &admin_user, &admin_password) + admin::create_admin_user(c, &admin_user, &admin_pass) .expect("failed to create admin user") }) .await; - - Ok(rocket) } #[derive(Debug, Deserialize, Serialize, Clone)] @@ -100,7 +99,7 @@ fn rocket() -> _ "Run database migrations", run_db_migrations, )) - .attach(AdHoc::try_on_ignite("Create admin user", create_admin_user)) + // .attach(AdHoc::try_on_ignite("Create admin user", create_admin_user)) .attach(AdHoc::config::()) .register("/", catchers![default_catcher]) .mount( From 211e31a00834667d7907656cf73233a40dc76ddb Mon Sep 17 00:00:00 2001 From: Jef Roosens Date: Mon, 13 Sep 2021 17:18:33 +0200 Subject: [PATCH 46/67] Wrote first draft of sections database scheme --- .../2021-09-13-143540_sections/down.sql | 7 +++ migrations/2021-09-13-143540_sections/up.sql | 56 +++++++++++++++++++ src/schema.rs | 28 +++++++++- 3 files changed, 90 insertions(+), 1 deletion(-) create mode 100644 migrations/2021-09-13-143540_sections/down.sql create mode 100644 migrations/2021-09-13-143540_sections/up.sql diff --git a/migrations/2021-09-13-143540_sections/down.sql b/migrations/2021-09-13-143540_sections/down.sql new file mode 100644 index 0000000..7af43ff --- /dev/null +++ b/migrations/2021-09-13-143540_sections/down.sql @@ -0,0 +1,7 @@ +-- This file should undo anything in `up.sql` +drop trigger insert_enforce_post_titles on posts; +drop trigger update_enforce_post_titles on posts; +drop function enforce_post_titles; + +drop table posts cascade; +drop table sections cascade; diff --git a/migrations/2021-09-13-143540_sections/up.sql b/migrations/2021-09-13-143540_sections/up.sql new file mode 100644 index 0000000..0c5ca76 --- /dev/null +++ b/migrations/2021-09-13-143540_sections/up.sql @@ -0,0 +1,56 @@ +-- Your SQL goes here +create table sections ( + id uuid DEFAULT gen_random_uuid() PRIMARY KEY, + + -- Title of the section + title varchar(255) UNIQUE NOT NULL, + -- Optional description of the section + description text, + -- Wether to show the section in the default list on the homepage + is_default boolean NOT NULL DEFAULT false, + -- Wether the posts should contain titles or not + has_titles boolean NOT NULL DEFAULT true +); + +create table posts ( + id uuid DEFAULT gen_random_uuid() PRIMARY KEY, + + section_id uuid NOT NULL REFERENCES sections(id) ON DELETE CASCADE, + -- Title of the post + -- Wether this is NULL or not is enforced using the enforce_post_titles trigger + title varchar(255), + -- Post date, defaults to today + publish_date date NOT NULL DEFAULT now(), + -- Content of the post + content text NOT NULL +); + +create function enforce_post_titles() returns trigger as $enforce_post_titles$ + begin + -- Check for a wrongfully null title + if new.title is null and exists ( + select 1 from sections where id = new.section_id and has_titles + ) then + raise exception 'Expected a post title, but got null.'; + end if; + + if new.title is not null and exists ( + select 1 from sections where id = new.section_id and not has_titles + ) then + raise exception 'Expected an empty post title, but got a value.'; + end if; + + return new; + end; +$enforce_post_titles$ language plpgsql; + +create trigger insert_enforce_post_titles + before insert on posts + for each row + execute function enforce_post_titles(); + +create trigger update_enforce_post_titles + before update of title on posts + for each row + when (old.title is distinct from new.title) + execute function enforce_post_titles(); diff --git a/src/schema.rs b/src/schema.rs index e3854e3..a38f572 100644 --- a/src/schema.rs +++ b/src/schema.rs @@ -1,3 +1,13 @@ +table! { + posts (id) { + id -> Uuid, + section_id -> Uuid, + title -> Nullable, + publish_date -> Date, + content -> Text, + } +} + table! { refresh_tokens (token) { token -> Bytea, @@ -7,6 +17,16 @@ table! { } } +table! { + sections (id) { + id -> Uuid, + title -> Varchar, + description -> Nullable, + is_default -> Bool, + has_titles -> Bool, + } +} + table! { users (id) { id -> Uuid, @@ -17,6 +37,12 @@ table! { } } +joinable!(posts -> sections (section_id)); joinable!(refresh_tokens -> users (user_id)); -allow_tables_to_appear_in_same_query!(refresh_tokens, users,); +allow_tables_to_appear_in_same_query!( + posts, + refresh_tokens, + sections, + users, +); From 8534090f0f923f48749f2c1bd263bdb5671cb613 Mon Sep 17 00:00:00 2001 From: Jef Roosens Date: Mon, 13 Sep 2021 17:35:06 +0200 Subject: [PATCH 47/67] Wrote some database boilerplate --- src/db/mod.rs | 3 +++ src/db/posts.rs | 44 ++++++++++++++++++++++++++++++++++++++++++++ src/db/sections.rs | 44 ++++++++++++++++++++++++++++++++++++++++++++ src/db/tokens.rs | 4 ++-- 4 files changed, 93 insertions(+), 2 deletions(-) create mode 100644 src/db/posts.rs create mode 100644 src/db/sections.rs diff --git a/src/db/mod.rs b/src/db/mod.rs index 9c831dd..19cb419 100644 --- a/src/db/mod.rs +++ b/src/db/mod.rs @@ -1,5 +1,8 @@ pub mod tokens; pub mod users; +pub mod sections; +pub mod posts; pub use tokens::{NewRefreshToken, RefreshToken}; pub use users::{NewUser, User}; +pub use sections::{Section, NewSection}; diff --git a/src/db/posts.rs b/src/db/posts.rs new file mode 100644 index 0000000..0493405 --- /dev/null +++ b/src/db/posts.rs @@ -0,0 +1,44 @@ +use diesel::{insert_into, prelude::*, Insertable, PgConnection, Queryable}; +use uuid::Uuid; +use chrono::NaiveDate; + +use crate::{ + errors::{RbError, RbResult}, + schema::{posts, posts::dsl::*}, +}; + +#[derive(Queryable)] +pub struct Post +{ + pub id: Uuid, + pub section_id: Uuid, + pub title: Option, + pub publish_date: NaiveDate, + pub content: String, +} + +#[derive(Insertable)] +#[table_name = "posts"] +pub struct NewPost +{ + pub section_id: Uuid, + pub title: Option, + pub publish_date: NaiveDate, +} + +pub fn all(conn: &PgConnection) -> RbResult> +{ + posts.load::(conn).map_err(|_| RbError::DbError("Couldn't get all posts.")) +} + +pub fn create(conn: &PgConnection, new_post: &NewPost) -> RbResult<()> +{ + insert_into(posts) + .values(new_post) + .execute(conn) + .map_err(|_| RbError::DbError("Couldn't insert post."))?; + + // TODO check for conflict? + + Ok(()) +} diff --git a/src/db/sections.rs b/src/db/sections.rs new file mode 100644 index 0000000..cff2736 --- /dev/null +++ b/src/db/sections.rs @@ -0,0 +1,44 @@ +use diesel::{insert_into, prelude::*, Insertable, PgConnection, Queryable}; +use uuid::Uuid; + +use crate::{ + errors::{RbError, RbResult}, + schema::{sections, sections::dsl::*}, +}; + +#[derive(Queryable)] +pub struct Section +{ + pub id: Uuid, + pub title: String, + pub description: Option, + pub is_default: bool, + pub has_titles: bool, +} + +#[derive(Insertable)] +#[table_name = "sections"] +pub struct NewSection +{ + title: String, + description: Option, + is_default: bool, + has_titles: bool, +} + +pub fn all(conn: &PgConnection) -> RbResult> +{ + sections.load::
(conn).map_err(|_| RbError::DbError("Couldn't get all sections")) +} + +pub fn create(conn: &PgConnection, new_section: &NewSection) -> RbResult<()> +{ + insert_into(sections) + .values(new_section) + .execute(conn) + .map_err(|_| RbError::DbError("Couldn't insert section."))?; + + // TODO check for conflict? + + Ok(()) +} diff --git a/src/db/tokens.rs b/src/db/tokens.rs index 8940721..97dd02d 100644 --- a/src/db/tokens.rs +++ b/src/db/tokens.rs @@ -36,7 +36,7 @@ pub fn create(conn: &PgConnection, new_refresh_token: &NewRefreshToken) -> RbRes insert_into(refresh_tokens) .values(new_refresh_token) .execute(conn) - .map_err(|_| RbError::Custom("Couldn't insert refresh token."))?; + .map_err(|_| RbError::DbError("Couldn't insert refresh token."))?; // TODO check for conflict? @@ -53,7 +53,7 @@ pub fn find_with_user( .inner_join(crate::schema::users::dsl::users) .filter(token.eq(token_val)) .first::<(RefreshToken, super::users::User)>(conn) - .map_err(|_| RbError::Custom("Couldn't get refresh token & user.")) + .map_err(|_| RbError::DbError("Couldn't get refresh token & user.")) .ok() } From 3e7612a9a8b278a21a561295464aae31f30e5202 Mon Sep 17 00:00:00 2001 From: Jef Roosens Date: Mon, 13 Sep 2021 22:15:38 +0200 Subject: [PATCH 48/67] Added create section endpoint --- Rb.yaml | 21 +++++++++++++++++++++ rustfmt.toml | 2 +- src/db/mod.rs | 6 +++--- src/db/posts.rs | 6 ++++-- src/db/sections.rs | 12 ++++++++---- src/errors.rs | 2 +- src/guards.rs | 18 ++++++++---------- src/main.rs | 2 ++ src/schema.rs | 7 +------ src/sections.rs | 16 ++++++++++++++++ tests/admin.py | 13 ++++++++++--- 11 files changed, 75 insertions(+), 30 deletions(-) create mode 100644 src/sections.rs diff --git a/Rb.yaml b/Rb.yaml index c944758..29a37f8 100644 --- a/Rb.yaml +++ b/Rb.yaml @@ -21,3 +21,24 @@ debug: databases: postgres_rb: url: "postgres://rb:rb@localhost:5432/rb" + +# This config is just used for testing, you should change it when deploying +release: + keep_alive: 5 + read_timeout: 5 + write_timeout: 5 + log_level: "normal" + limits: + forms: 32768 + + admin_user: "admin" + admin_pass: "password" + jwt: + key: "secret" + refresh_token_size: 64 + # Just 5 seconds for debugging + refresh_token_expire: 60 + + databases: + postgres_rb: + url: "postgres://rb:rb@localhost:5432/rb" diff --git a/rustfmt.toml b/rustfmt.toml index 5e52857..8e8627b 100644 --- a/rustfmt.toml +++ b/rustfmt.toml @@ -36,7 +36,7 @@ license_template_path = "" make_backup = false match_arm_blocks = true match_arm_leading_pipes = "Never" -match_block_trailing_comma = false +match_block_trailing_comma = true max_width = 100 merge_derives = true newline_style = "Auto" diff --git a/src/db/mod.rs b/src/db/mod.rs index 19cb419..d1dcdcf 100644 --- a/src/db/mod.rs +++ b/src/db/mod.rs @@ -1,8 +1,8 @@ +pub mod posts; +pub mod sections; pub mod tokens; pub mod users; -pub mod sections; -pub mod posts; +pub use sections::{NewSection, Section}; pub use tokens::{NewRefreshToken, RefreshToken}; pub use users::{NewUser, User}; -pub use sections::{Section, NewSection}; diff --git a/src/db/posts.rs b/src/db/posts.rs index 0493405..8014f3f 100644 --- a/src/db/posts.rs +++ b/src/db/posts.rs @@ -1,6 +1,6 @@ +use chrono::NaiveDate; use diesel::{insert_into, prelude::*, Insertable, PgConnection, Queryable}; use uuid::Uuid; -use chrono::NaiveDate; use crate::{ errors::{RbError, RbResult}, @@ -28,7 +28,9 @@ pub struct NewPost pub fn all(conn: &PgConnection) -> RbResult> { - posts.load::(conn).map_err(|_| RbError::DbError("Couldn't get all posts.")) + posts + .load::(conn) + .map_err(|_| RbError::DbError("Couldn't get all posts.")) } pub fn create(conn: &PgConnection, new_post: &NewPost) -> RbResult<()> diff --git a/src/db/sections.rs b/src/db/sections.rs index cff2736..19721fb 100644 --- a/src/db/sections.rs +++ b/src/db/sections.rs @@ -1,4 +1,5 @@ use diesel::{insert_into, prelude::*, Insertable, PgConnection, Queryable}; +use serde::Deserialize; use uuid::Uuid; use crate::{ @@ -16,19 +17,22 @@ pub struct Section pub has_titles: bool, } -#[derive(Insertable)] +#[derive(Deserialize, Insertable)] #[table_name = "sections"] +// #[serde(rename_all = "camelCase")] pub struct NewSection { title: String, description: Option, - is_default: bool, - has_titles: bool, + is_default: Option, + has_titles: Option, } pub fn all(conn: &PgConnection) -> RbResult> { - sections.load::
(conn).map_err(|_| RbError::DbError("Couldn't get all sections")) + sections + .load::
(conn) + .map_err(|_| RbError::DbError("Couldn't get all sections")) } pub fn create(conn: &PgConnection, new_section: &NewSection) -> RbResult<()> diff --git a/src/errors.rs b/src/errors.rs index bb7856a..1f9aff3 100644 --- a/src/errors.rs +++ b/src/errors.rs @@ -61,7 +61,7 @@ impl RbError RbError::AuthInvalidRefreshToken => "This refresh token is not valid.", RbError::AuthDuplicateRefreshToken => { "This refresh token has already been used. The user has been blocked." - } + }, RbError::AuthMissingHeader => "Missing Authorization header.", RbError::UMDuplicateUser => "This user already exists.", diff --git a/src/guards.rs b/src/guards.rs index 7b40bdd..3510163 100644 --- a/src/guards.rs +++ b/src/guards.rs @@ -10,7 +10,7 @@ use sha2::Sha256; use crate::{auth::jwt::Claims, errors::RbError, RbConfig}; -/// Extracts a "Authorization: Bearer" string from the headers. +/// Extracts an "Authorization: Bearer" string from the headers. pub struct Bearer<'a>(&'a str); #[rocket::async_trait] @@ -22,7 +22,7 @@ impl<'r> FromRequest<'r> for Bearer<'r> { // If the header isn't present, just forward to the next route let header = match req.headers().get_one("Authorization") { - None => return Outcome::Failure((Status::BadRequest, Self::Error::AuthMissingHeader)), + None => return Outcome::Forward(()), Some(val) => val, }; @@ -31,12 +31,10 @@ impl<'r> FromRequest<'r> for Bearer<'r> } // Extract the jwt token from the header - let auth_string = match header.get(7..) { - Some(s) => s, - None => return Outcome::Failure((Status::Unauthorized, Self::Error::AuthUnauthorized)), - }; - - Outcome::Success(Self(auth_string)) + match header.get(7..) { + Some(s) => Outcome::Success(Self(s)), + None => Outcome::Failure((Status::Unauthorized, Self::Error::AuthUnauthorized)), + } } } @@ -63,14 +61,14 @@ impl<'r> FromRequest<'r> for Jwt Status::InternalServerError, Self::Error::Custom("Failed to do Hmac thing."), )) - } + }, }; // Verify token using key let claims: Claims = match bearer.verify_with_key(&key) { Ok(claims) => claims, Err(_) => { return Outcome::Failure((Status::Unauthorized, Self::Error::AuthUnauthorized)) - } + }, }; Outcome::Success(Self(claims)) diff --git a/src/main.rs b/src/main.rs index 2819e79..adb2f26 100644 --- a/src/main.rs +++ b/src/main.rs @@ -27,6 +27,7 @@ pub mod db; pub mod errors; pub mod guards; pub(crate) mod schema; +pub mod sections; #[global_allocator] static GLOBAL: mimalloc::MiMalloc = mimalloc::MiMalloc; @@ -111,4 +112,5 @@ fn rocket() -> _ "/api/admin", routes![admin::get_users, admin::create_user, admin::get_user_info], ) + .mount("/api/sections", routes![sections::create_section]) } diff --git a/src/schema.rs b/src/schema.rs index a38f572..45b9813 100644 --- a/src/schema.rs +++ b/src/schema.rs @@ -40,9 +40,4 @@ table! { joinable!(posts -> sections (section_id)); joinable!(refresh_tokens -> users (user_id)); -allow_tables_to_appear_in_same_query!( - posts, - refresh_tokens, - sections, - users, -); +allow_tables_to_appear_in_same_query!(posts, refresh_tokens, sections, users,); diff --git a/src/sections.rs b/src/sections.rs new file mode 100644 index 0000000..c5e2f27 --- /dev/null +++ b/src/sections.rs @@ -0,0 +1,16 @@ +use rocket::serde::json::Json; + +use crate::{db, errors::RbResult, guards::Admin, RbDbConn}; + +/// Create a new section. +#[post("/", data = "")] +pub async fn create_section( + _admin: Admin, + conn: RbDbConn, + new_section: Json, +) -> RbResult<()> +{ + Ok(conn + .run(move |c| db::sections::create(c, &new_section.into_inner())) + .await?) +} diff --git a/tests/admin.py b/tests/admin.py index 19a1c5b..069c2dd 100644 --- a/tests/admin.py +++ b/tests/admin.py @@ -2,7 +2,7 @@ import requests class RbClient: - def __init__(self, username, password, base_url = "http://localhost:8000/api"): + def __init__(self, username = "admin", password = "password", base_url = "http://localhost:8000/api"): self.username = username self.password = password self.base_url = base_url @@ -17,6 +17,7 @@ class RbClient: }) if r.status_code != 200: + print(r.text) raise Exception("Couldn't login") res = r.json() @@ -56,9 +57,15 @@ class RbClient: def get(self, url, *args, **kwargs): return self._request("GET", f"{self.base_url}{url}", *args, **kwargs) + def post(self, url, *args, **kwargs): + return self._request("POST", f"{self.base_url}{url}", *args, **kwargs) + if __name__ == "__main__": - client = RbClient("admin", "password") + client = RbClient() - print(client.get("/admin/users").json()) + # print(client.get("/admin/users").json()) + client.post("/sections", json={ + "title": "this is a title" + }) From 6d83c1803609b76a270596f330c50956f3723bb3 Mon Sep 17 00:00:00 2001 From: Jef Roosens Date: Wed, 15 Sep 2021 11:36:40 +0200 Subject: [PATCH 49/67] Documented entire db section --- src/db/mod.rs | 3 +++ src/db/posts.rs | 16 ++++++++++++++++ src/db/sections.rs | 15 +++++++++++++++ src/db/tokens.rs | 32 +++++++++++++++++++++++++++++++ src/db/users.rs | 47 ++++++++++++++++++++++++++++++++++++++++++++++ src/sections.rs | 10 +++++++++- 6 files changed, 122 insertions(+), 1 deletion(-) diff --git a/src/db/mod.rs b/src/db/mod.rs index d1dcdcf..35e4995 100644 --- a/src/db/mod.rs +++ b/src/db/mod.rs @@ -1,3 +1,6 @@ +//! The db module contains all Diesel-related logic. This is to prevent the various Diesel imports +//! from poluting other modules' namespaces. + pub mod posts; pub mod sections; pub mod tokens; diff --git a/src/db/posts.rs b/src/db/posts.rs index 8014f3f..2486c08 100644 --- a/src/db/posts.rs +++ b/src/db/posts.rs @@ -1,3 +1,5 @@ +//! Handles all posts-related database operations. + use chrono::NaiveDate; use diesel::{insert_into, prelude::*, Insertable, PgConnection, Queryable}; use uuid::Uuid; @@ -7,6 +9,7 @@ use crate::{ schema::{posts, posts::dsl::*}, }; +/// Represents a post contained within the database. #[derive(Queryable)] pub struct Post { @@ -17,6 +20,7 @@ pub struct Post pub content: String, } +/// Represents a new post to be added to the database. #[derive(Insertable)] #[table_name = "posts"] pub struct NewPost @@ -26,6 +30,12 @@ pub struct NewPost pub publish_date: NaiveDate, } +/// Returns all posts in the database; should be used with care as this method could quickly return +/// a large amount of data. +/// +/// # Arguments +/// +/// * `conn` - a reference to a database connection pub fn all(conn: &PgConnection) -> RbResult> { posts @@ -33,6 +43,12 @@ pub fn all(conn: &PgConnection) -> RbResult> .map_err(|_| RbError::DbError("Couldn't get all posts.")) } +/// Insert a new post into the database. +/// +/// # Arguments +/// +/// * `conn` - reference to a database connection +/// * `new_post` - the new post object to insert pub fn create(conn: &PgConnection, new_post: &NewPost) -> RbResult<()> { insert_into(posts) diff --git a/src/db/sections.rs b/src/db/sections.rs index 19721fb..ca8956b 100644 --- a/src/db/sections.rs +++ b/src/db/sections.rs @@ -1,3 +1,5 @@ +//! Handles all section-related database operations. + use diesel::{insert_into, prelude::*, Insertable, PgConnection, Queryable}; use serde::Deserialize; use uuid::Uuid; @@ -7,6 +9,7 @@ use crate::{ schema::{sections, sections::dsl::*}, }; +/// Represents a section contained in the database. #[derive(Queryable)] pub struct Section { @@ -17,6 +20,7 @@ pub struct Section pub has_titles: bool, } +/// A new section to be added into the database. #[derive(Deserialize, Insertable)] #[table_name = "sections"] // #[serde(rename_all = "camelCase")] @@ -28,6 +32,11 @@ pub struct NewSection has_titles: Option, } +/// Returns all sections in the database. +/// +/// # Arguments +/// +/// * `conn` - reference to a database connection pub fn all(conn: &PgConnection) -> RbResult> { sections @@ -35,6 +44,12 @@ pub fn all(conn: &PgConnection) -> RbResult> .map_err(|_| RbError::DbError("Couldn't get all sections")) } +/// Inserts a new section into the database. +/// +/// # Arguments +/// +/// * `conn` - reference to a database connection +/// * `new_section` - the new section to be added pub fn create(conn: &PgConnection, new_section: &NewSection) -> RbResult<()> { insert_into(sections) diff --git a/src/db/tokens.rs b/src/db/tokens.rs index 97dd02d..cbb8898 100644 --- a/src/db/tokens.rs +++ b/src/db/tokens.rs @@ -1,3 +1,5 @@ +//! Handles refresh token-related database operations. + use diesel::{insert_into, prelude::*, Insertable, PgConnection, Queryable}; use uuid::Uuid; @@ -6,6 +8,7 @@ use crate::{ schema::{refresh_tokens, refresh_tokens::dsl::*}, }; +/// A refresh token as stored in the database #[derive(Queryable)] pub struct RefreshToken { @@ -15,6 +18,7 @@ pub struct RefreshToken pub last_used_at: Option, } +/// A new refresh token to be added into the database #[derive(Insertable)] #[table_name = "refresh_tokens"] pub struct NewRefreshToken @@ -24,6 +28,12 @@ pub struct NewRefreshToken pub expires_at: chrono::NaiveDateTime, } +// TODO add pagination as this could grow very quickly +/// Returns all refresh tokens contained in the database. +/// +/// # Arguments +/// +/// * `conn` - database connection to use pub fn all(conn: &PgConnection) -> RbResult> { refresh_tokens @@ -31,6 +41,12 @@ pub fn all(conn: &PgConnection) -> RbResult> .map_err(|_| RbError::DbError("Couldn't get all refresh tokens.")) } +/// Insert a new refresh token into the database. +/// +/// # Arguments +/// +/// * `conn` - database connection to use +/// * `new_refresh_token` - token to insert pub fn create(conn: &PgConnection, new_refresh_token: &NewRefreshToken) -> RbResult<()> { insert_into(refresh_tokens) @@ -43,6 +59,12 @@ pub fn create(conn: &PgConnection, new_refresh_token: &NewRefreshToken) -> RbRes Ok(()) } +/// Returns the token & user data associated with the given refresh token value. +/// +/// # Arguments +/// +/// * `conn` - database connection to use +/// * `token_val` - token value to search for pub fn find_with_user( conn: &PgConnection, token_val: &[u8], @@ -57,6 +79,16 @@ pub fn find_with_user( .ok() } +/// Updates a token's `last_used_at` column value. +/// +/// # Arguments +/// +/// * `conn` - database connection to use +/// * `token_` - value of the refresh token to update +/// * `last_used_at_` - date value to update column with +/// +/// **NOTE**: argument names use trailing underscores as to not conflict with Diesel's imported dsl +/// names. pub fn update_last_used_at( conn: &PgConnection, token_: &[u8], diff --git a/src/db/users.rs b/src/db/users.rs index efc74db..37ef9c2 100644 --- a/src/db/users.rs +++ b/src/db/users.rs @@ -1,3 +1,5 @@ +//! Handles user-related database operations. + use diesel::{prelude::*, AsChangeset, Insertable, Queryable}; use serde::{Deserialize, Serialize}; use uuid::Uuid; @@ -7,6 +9,7 @@ use crate::{ schema::{users, users::dsl::*}, }; +/// A user as stored in the database. #[derive(Queryable, Serialize)] pub struct User { @@ -18,6 +21,7 @@ pub struct User pub admin: bool, } +/// A new user to add to the database. #[derive(Insertable, AsChangeset, Deserialize)] #[table_name = "users"] pub struct NewUser @@ -27,6 +31,11 @@ pub struct NewUser pub admin: bool, } +/// Returns all users in the database. +/// +/// # Arguments +/// +/// * `conn` - database connection to use pub fn all(conn: &PgConnection) -> RbResult> { users @@ -34,11 +43,23 @@ pub fn all(conn: &PgConnection) -> RbResult> .map_err(|_| RbError::DbError("Couldn't get all users.")) } +/// Find a user with a given ID. +/// +/// # Arguments +/// +/// * `conn` - database connection to use +/// * `user_id` - ID to search for pub fn find(conn: &PgConnection, user_id: Uuid) -> Option { users.find(user_id).first::(conn).ok() } +/// Find a user with a given username. +/// +/// # Arguments +/// +/// * `conn` - database connection to use +/// * `username_` - username to search for pub fn find_by_username(conn: &PgConnection, username_: &str) -> RbResult { Ok(users @@ -47,6 +68,12 @@ pub fn find_by_username(conn: &PgConnection, username_: &str) -> RbResult .map_err(|_| RbError::DbError("Couldn't find users by username."))?) } +/// Insert a new user into the database +/// +/// # Arguments +/// +/// * `conn` - database connection to use +/// * `new_user` - user to insert pub fn create(conn: &PgConnection, new_user: &NewUser) -> RbResult<()> { let count = diesel::insert_into(users) @@ -61,6 +88,12 @@ pub fn create(conn: &PgConnection, new_user: &NewUser) -> RbResult<()> Ok(()) } +/// Either create a new user or update an existing one on conflict. +/// +/// # Arguments +/// +/// * `conn` - database connection to use +/// * `new_user` - user to insert/update pub fn create_or_update(conn: &PgConnection, new_user: &NewUser) -> RbResult<()> { diesel::insert_into(users) @@ -74,6 +107,12 @@ pub fn create_or_update(conn: &PgConnection, new_user: &NewUser) -> RbResult<()> Ok(()) } +/// Delete the user with the given ID. +/// +/// # Arguments +/// +/// `conn` - database connection to use +/// `user_id` - ID of user to delete pub fn delete(conn: &PgConnection, user_id: Uuid) -> RbResult<()> { diesel::delete(users.filter(id.eq(user_id))) @@ -83,6 +122,14 @@ pub fn delete(conn: &PgConnection, user_id: Uuid) -> RbResult<()> Ok(()) } +/// Block a user given an ID. +/// In practice, this means updating the user's entry so that the `blocked` column is set to +/// `true`. +/// +/// # Arguments +/// +/// `conn` - database connection to use +/// `user_id` - ID of user to block pub fn block(conn: &PgConnection, user_id: Uuid) -> RbResult<()> { diesel::update(users.filter(id.eq(user_id))) diff --git a/src/sections.rs b/src/sections.rs index c5e2f27..e4def24 100644 --- a/src/sections.rs +++ b/src/sections.rs @@ -1,8 +1,16 @@ +//! This module handles management of site sections (aka blogs). + use rocket::serde::json::Json; use crate::{db, errors::RbResult, guards::Admin, RbDbConn}; -/// Create a new section. +/// Route for creating a new section. +/// +/// # Arguments +/// +/// * `_admin` - guard ensuring user is admin +/// * `conn` - guard providing a connection to the database +/// * `new_section` - Json-encoded NewSection object #[post("/", data = "")] pub async fn create_section( _admin: Admin, From 548dd0d022da69667111d3c89be2fe1c3348ada0 Mon Sep 17 00:00:00 2001 From: Jef Roosens Date: Thu, 23 Sep 2021 15:30:39 +0200 Subject: [PATCH 50/67] Tried to add MirageJS but failed --- src/db/posts.rs | 4 - src/db/sections.rs | 2 +- web/package.json | 4 +- web/src/components/MirageTest.svelte | 19 + web/src/components/SvelteCounter.svelte | 2 +- web/src/layouts/index.astro | 4 + web/src/pages/index.astro | 23 + web/yarn.lock | 996 +++++++++++++++--------- 8 files changed, 681 insertions(+), 373 deletions(-) create mode 100644 web/src/components/MirageTest.svelte diff --git a/src/db/posts.rs b/src/db/posts.rs index 2486c08..163902e 100644 --- a/src/db/posts.rs +++ b/src/db/posts.rs @@ -1,5 +1,3 @@ -//! Handles all posts-related database operations. - use chrono::NaiveDate; use diesel::{insert_into, prelude::*, Insertable, PgConnection, Queryable}; use uuid::Uuid; @@ -9,7 +7,6 @@ use crate::{ schema::{posts, posts::dsl::*}, }; -/// Represents a post contained within the database. #[derive(Queryable)] pub struct Post { @@ -20,7 +17,6 @@ pub struct Post pub content: String, } -/// Represents a new post to be added to the database. #[derive(Insertable)] #[table_name = "posts"] pub struct NewPost diff --git a/src/db/sections.rs b/src/db/sections.rs index ca8956b..b429c85 100644 --- a/src/db/sections.rs +++ b/src/db/sections.rs @@ -23,7 +23,7 @@ pub struct Section /// A new section to be added into the database. #[derive(Deserialize, Insertable)] #[table_name = "sections"] -// #[serde(rename_all = "camelCase")] +#[serde(rename_all = "camelCase")] pub struct NewSection { title: String, diff --git a/web/package.json b/web/package.json index 6dfbfa6..b1070aa 100644 --- a/web/package.json +++ b/web/package.json @@ -7,7 +7,9 @@ "build": "astro build" }, "devDependencies": { + "@astrojs/renderer-svelte": "^0.1.1", "astro": "0.19.0-next.2", - "@astrojs/renderer-svelte": "^0.1.1" + "miragejs": "^0.1.41", + "typescript": "^4.4.3" } } diff --git a/web/src/components/MirageTest.svelte b/web/src/components/MirageTest.svelte new file mode 100644 index 0000000..ac3676b --- /dev/null +++ b/web/src/components/MirageTest.svelte @@ -0,0 +1,19 @@ + + + diff --git a/web/src/components/SvelteCounter.svelte b/web/src/components/SvelteCounter.svelte index f493c25..ea6cf52 100644 --- a/web/src/components/SvelteCounter.svelte +++ b/web/src/components/SvelteCounter.svelte @@ -1,4 +1,4 @@ - + + diff --git a/web2/package.json b/web2/package.json new file mode 100644 index 0000000..4a7220b --- /dev/null +++ b/web2/package.json @@ -0,0 +1,18 @@ +{ + "name": "rusty-bever", + "version": "0.0.0", + "scripts": { + "dev": "vite", + "build": "vue-tsc --noEmit && vite build", + "serve": "vite preview" + }, + "dependencies": { + "vue": "^3.2.16" + }, + "devDependencies": { + "@vitejs/plugin-vue": "^1.9.3", + "typescript": "^4.4.3", + "vite": "^2.6.4", + "vue-tsc": "^0.3.0" + } +} \ No newline at end of file diff --git a/web2/public/favicon.ico b/web2/public/favicon.ico new file mode 100644 index 0000000000000000000000000000000000000000..df36fcfb72584e00488330b560ebcf34a41c64c2 GIT binary patch literal 4286 zcmds*O-Phc6o&64GDVCEQHxsW(p4>LW*W<827=Unuo8sGpRux(DN@jWP-e29Wl%wj zY84_aq9}^Am9-cWTD5GGEo#+5Fi2wX_P*bo+xO!)p*7B;iKlbFd(U~_d(U?#hLj56 zPhFkj-|A6~Qk#@g^#D^U0XT1cu=c-vu1+SElX9NR;kzAUV(q0|dl0|%h|dI$%VICy zJnu2^L*Te9JrJMGh%-P79CL0}dq92RGU6gI{v2~|)p}sG5x0U*z<8U;Ij*hB9z?ei z@g6Xq-pDoPl=MANPiR7%172VA%r)kevtV-_5H*QJKFmd;8yA$98zCxBZYXTNZ#QFk2(TX0;Y2dt&WitL#$96|gJY=3xX zpCoi|YNzgO3R`f@IiEeSmKrPSf#h#Qd<$%Ej^RIeeYfsxhPMOG`S`Pz8q``=511zm zAm)MX5AV^5xIWPyEu7u>qYs?pn$I4nL9J!=K=SGlKLXpE<5x+2cDTXq?brj?n6sp= zphe9;_JHf40^9~}9i08r{XM$7HB!`{Ys~TK0kx<}ZQng`UPvH*11|q7&l9?@FQz;8 zx!=3<4seY*%=OlbCbcae?5^V_}*K>Uo6ZWV8mTyE^B=DKy7-sdLYkR5Z?paTgK-zyIkKjIcpyO z{+uIt&YSa_$QnN_@t~L014dyK(fOOo+W*MIxbA6Ndgr=Y!f#Tokqv}n<7-9qfHkc3 z=>a|HWqcX8fzQCT=dqVbogRq!-S>H%yA{1w#2Pn;=e>JiEj7Hl;zdt-2f+j2%DeVD zsW0Ab)ZK@0cIW%W7z}H{&~yGhn~D;aiP4=;m-HCo`BEI+Kd6 z={Xwx{TKxD#iCLfl2vQGDitKtN>z|-AdCN|$jTFDg0m3O`WLD4_s#$S literal 0 HcmV?d00001 diff --git a/web2/src/App.vue b/web2/src/App.vue new file mode 100644 index 0000000..df4e2d7 --- /dev/null +++ b/web2/src/App.vue @@ -0,0 +1,21 @@ + + + + + diff --git a/web2/src/assets/logo.png b/web2/src/assets/logo.png new file mode 100644 index 0000000000000000000000000000000000000000..f3d2503fc2a44b5053b0837ebea6e87a2d339a43 GIT binary patch literal 6849 zcmaKRcUV(fvo}bjDT-7nLI_nlK}sT_69H+`qzVWDA|yaU?}j417wLi^B1KB1SLsC& zL0ag7$U(XW5YR7p&Ux?sP$d4lvMt8C^+TcQu4F zQqv!UF!I+kw)c0jhd6+g6oCr9P?7)?!qX1ui*iL{p}sKCAGuJ{{W)0z1pLF|=>h}& zt(2Lr0Z`2ig8<5i%Zk}cO5Fm=LByqGWaS`oqChZdEFmc`0hSb#gg|Aap^{+WKOYcj zHjINK)KDG%&s?Mt4CL(T=?;~U@bU2x_mLKN!#GJuK_CzbNw5SMEJorG!}_5;?R>@1 zSl)jns3WlU7^J%=(hUtfmuUCU&C3%8B5C^f5>W2Cy8jW3#{Od{lF1}|?c61##3dzA zsPlFG;l_FzBK}8>|H_Ru_H#!_7$UH4UKo3lKOA}g1(R&|e@}GINYVzX?q=_WLZCgh z)L|eJMce`D0EIwgRaNETDsr+?vQknSGAi=7H00r`QnI%oQnFxm`G2umXso9l+8*&Q z7WqF|$p49js$mdzo^BXpH#gURy=UO;=IMrYc5?@+sR4y_?d*~0^YP7d+y0{}0)zBM zIKVM(DBvICK#~7N0a+PY6)7;u=dutmNqK3AlsrUU9U`d;msiucB_|8|2kY=(7XA;G zwDA8AR)VCA#JOkxm#6oHNS^YVuOU;8p$N)2{`;oF|rQ?B~K$%rHDxXs+_G zF5|-uqHZvSzq}L;5Kcy_P+x0${33}Ofb6+TX&=y;;PkEOpz%+_bCw_{<&~ zeLV|!bP%l1qxywfVr9Z9JI+++EO^x>ZuCK);=$VIG1`kxK8F2M8AdC$iOe3cj1fo(ce4l-9 z7*zKy3={MixvUk=enQE;ED~7tv%qh&3lR<0m??@w{ILF|e#QOyPkFYK!&Up7xWNtL zOW%1QMC<3o;G9_S1;NkPB6bqbCOjeztEc6TsBM<(q9((JKiH{01+Ud=uw9B@{;(JJ z-DxI2*{pMq`q1RQc;V8@gYAY44Z!%#W~M9pRxI(R?SJ7sy7em=Z5DbuDlr@*q|25V)($-f}9c#?D%dU^RS<(wz?{P zFFHtCab*!rl(~j@0(Nadvwg8q|4!}L^>d?0al6}Rrv9$0M#^&@zjbfJy_n!%mVHK4 z6pLRIQ^Uq~dnyy$`ay51Us6WaP%&O;@49m&{G3z7xV3dLtt1VTOMYl3UW~Rm{Eq4m zF?Zl_v;?7EFx1_+#WFUXxcK78IV)FO>42@cm@}2I%pVbZqQ}3;p;sDIm&knay03a^ zn$5}Q$G!@fTwD$e(x-~aWP0h+4NRz$KlnO_H2c< z(XX#lPuW_%H#Q+c&(nRyX1-IadKR-%$4FYC0fsCmL9ky3 zKpxyjd^JFR+vg2!=HWf}2Z?@Td`0EG`kU?{8zKrvtsm)|7>pPk9nu@2^z96aU2<#` z2QhvH5w&V;wER?mopu+nqu*n8p~(%QkwSs&*0eJwa zMXR05`OSFpfyRb!Y_+H@O%Y z0=K^y6B8Gcbl?SA)qMP3Z+=C(?8zL@=74R=EVnE?vY!1BQy2@q*RUgRx4yJ$k}MnL zs!?74QciNb-LcG*&o<9=DSL>1n}ZNd)w1z3-0Pd^4ED1{qd=9|!!N?xnXjM!EuylY z5=!H>&hSofh8V?Jofyd!h`xDI1fYAuV(sZwwN~{$a}MX^=+0TH*SFp$vyxmUv7C*W zv^3Gl0+eTFgBi3FVD;$nhcp)ka*4gSskYIqQ&+M}xP9yLAkWzBI^I%zR^l1e?bW_6 zIn{mo{dD=)9@V?s^fa55jh78rP*Ze<3`tRCN4*mpO$@7a^*2B*7N_|A(Ve2VB|)_o z$=#_=aBkhe(ifX}MLT()@5?OV+~7cXC3r!%{QJxriXo9I%*3q4KT4Xxzyd{ z9;_%=W%q!Vw$Z7F3lUnY+1HZ*lO;4;VR2+i4+D(m#01OYq|L_fbnT;KN<^dkkCwtd zF7n+O7KvAw8c`JUh6LmeIrk4`F3o|AagKSMK3))_5Cv~y2Bb2!Ibg9BO7Vkz?pAYX zoI=B}+$R22&IL`NCYUYjrdhwjnMx_v=-Qcx-jmtN>!Zqf|n1^SWrHy zK|MwJ?Z#^>)rfT5YSY{qjZ&`Fjd;^vv&gF-Yj6$9-Dy$<6zeP4s+78gS2|t%Z309b z0^fp~ue_}i`U9j!<|qF92_3oB09NqgAoehQ`)<)dSfKoJl_A6Ec#*Mx9Cpd-p#$Ez z={AM*r-bQs6*z$!*VA4|QE7bf@-4vb?Q+pPKLkY2{yKsw{&udv_2v8{Dbd zm~8VAv!G~s)`O3|Q6vFUV%8%+?ZSVUa(;fhPNg#vab@J*9XE4#D%)$UU-T5`fwjz! z6&gA^`OGu6aUk{l*h9eB?opVdrHK>Q@U>&JQ_2pR%}TyOXGq_6s56_`U(WoOaAb+K zXQr#6H}>a-GYs9^bGP2Y&hSP5gEtW+GVC4=wy0wQk=~%CSXj=GH6q z-T#s!BV`xZVxm{~jr_ezYRpqqIcXC=Oq`b{lu`Rt(IYr4B91hhVC?yg{ol4WUr3v9 zOAk2LG>CIECZ-WIs0$N}F#eoIUEtZudc7DPYIjzGqDLWk_A4#(LgacooD z2K4IWs@N`Bddm-{%oy}!k0^i6Yh)uJ1S*90>|bm3TOZxcV|ywHUb(+CeX-o1|LTZM zwU>dY3R&U)T(}5#Neh?-CWT~@{6Ke@sI)uSuzoah8COy)w)B)aslJmp`WUcjdia-0 zl2Y}&L~XfA`uYQboAJ1;J{XLhYjH){cObH3FDva+^8ioOQy%Z=xyjGLmWMrzfFoH; zEi3AG`_v+%)&lDJE;iJWJDI@-X9K5O)LD~j*PBe(wu+|%ar~C+LK1+-+lK=t# z+Xc+J7qp~5q=B~rD!x78)?1+KUIbYr^5rcl&tB-cTtj+e%{gpZZ4G~6r15+d|J(ky zjg@@UzMW0k9@S#W(1H{u;Nq(7llJbq;;4t$awM;l&(2s+$l!Ay9^Ge|34CVhr7|BG z?dAR83smef^frq9V(OH+a+ki#q&-7TkWfFM=5bsGbU(8mC;>QTCWL5ydz9s6k@?+V zcjiH`VI=59P-(-DWXZ~5DH>B^_H~;4$)KUhnmGo*G!Tq8^LjfUDO)lASN*=#AY_yS zqW9UX(VOCO&p@kHdUUgsBO0KhXxn1sprK5h8}+>IhX(nSXZKwlNsjk^M|RAaqmCZB zHBolOHYBas@&{PT=R+?d8pZu zUHfyucQ`(umXSW7o?HQ3H21M`ZJal+%*)SH1B1j6rxTlG3hx1IGJN^M7{$j(9V;MZ zRKybgVuxKo#XVM+?*yTy{W+XHaU5Jbt-UG33x{u(N-2wmw;zzPH&4DE103HV@ER86 z|FZEmQb|&1s5#`$4!Cm}&`^{(4V}OP$bk`}v6q6rm;P!H)W|2i^e{7lTk2W@jo_9q z*aw|U7#+g59Fv(5qI`#O-qPj#@_P>PC#I(GSp3DLv7x-dmYK=C7lPF8a)bxb=@)B1 zUZ`EqpXV2dR}B&r`uM}N(TS99ZT0UB%IN|0H%DcVO#T%L_chrgn#m6%x4KE*IMfjX zJ%4veCEqbXZ`H`F_+fELMC@wuy_ch%t*+Z+1I}wN#C+dRrf2X{1C8=yZ_%Pt6wL_~ zZ2NN-hXOT4P4n$QFO7yYHS-4wF1Xfr-meG9Pn;uK51?hfel`d38k{W)F*|gJLT2#T z<~>spMu4(mul-8Q3*pf=N4DcI)zzjqAgbE2eOT7~&f1W3VsdD44Ffe;3mJp-V@8UC z)|qnPc12o~$X-+U@L_lWqv-RtvB~%hLF($%Ew5w>^NR82qC_0FB z)=hP1-OEx?lLi#jnLzH}a;Nvr@JDO-zQWd}#k^an$Kwml;MrD&)sC5b`s0ZkVyPkb zt}-jOq^%_9>YZe7Y}PhW{a)c39G`kg(P4@kxjcYfgB4XOOcmezdUI7j-!gs7oAo2o zx(Ph{G+YZ`a%~kzK!HTAA5NXE-7vOFRr5oqY$rH>WI6SFvWmahFav!CfRMM3%8J&c z*p+%|-fNS_@QrFr(at!JY9jCg9F-%5{nb5Bo~z@Y9m&SHYV`49GAJjA5h~h4(G!Se zZmK{Bo7ivCfvl}@A-ptkFGcWXAzj3xfl{evi-OG(TaCn1FAHxRc{}B|x+Ua1D=I6M z!C^ZIvK6aS_c&(=OQDZfm>O`Nxsw{ta&yiYPA~@e#c%N>>#rq)k6Aru-qD4(D^v)y z*>Rs;YUbD1S8^D(ps6Jbj0K3wJw>L4m)0e(6Pee3Y?gy9i0^bZO?$*sv+xKV?WBlh zAp*;v6w!a8;A7sLB*g-^<$Z4L7|5jXxxP1}hQZ<55f9<^KJ>^mKlWSGaLcO0=$jem zWyZkRwe~u{{tU63DlCaS9$Y4CP4f?+wwa(&1ou)b>72ydrFvm`Rj-0`kBJgK@nd(*Eh!(NC{F-@=FnF&Y!q`7){YsLLHf0_B6aHc# z>WIuHTyJwIH{BJ4)2RtEauC7Yq7Cytc|S)4^*t8Va3HR zg=~sN^tp9re@w=GTx$;zOWMjcg-7X3Wk^N$n;&Kf1RgVG2}2L-(0o)54C509C&77i zrjSi{X*WV=%C17((N^6R4Ya*4#6s_L99RtQ>m(%#nQ#wrRC8Y%yxkH;d!MdY+Tw@r zjpSnK`;C-U{ATcgaxoEpP0Gf+tx);buOMlK=01D|J+ROu37qc*rD(w`#O=3*O*w9?biwNoq3WN1`&Wp8TvKj3C z3HR9ssH7a&Vr<6waJrU zdLg!ieYz%U^bmpn%;(V%%ugMk92&?_XX1K@mwnVSE6!&%P%Wdi7_h`CpScvspMx?N zQUR>oadnG17#hNc$pkTp+9lW+MBKHRZ~74XWUryd)4yd zj98$%XmIL4(9OnoeO5Fnyn&fpQ9b0h4e6EHHw*l68j;>(ya`g^S&y2{O8U>1*>4zR zq*WSI_2o$CHQ?x0!wl9bpx|Cm2+kFMR)oMud1%n2=qn5nE&t@Fgr#=Zv2?}wtEz^T z9rrj=?IH*qI5{G@Rn&}^Z{+TW}mQeb9=8b<_a`&Cm#n%n~ zU47MvCBsdXFB1+adOO)03+nczfWa#vwk#r{o{dF)QWya9v2nv43Zp3%Ps}($lA02*_g25t;|T{A5snSY?3A zrRQ~(Ygh_ebltHo1VCbJb*eOAr;4cnlXLvI>*$-#AVsGg6B1r7@;g^L zFlJ_th0vxO7;-opU@WAFe;<}?!2q?RBrFK5U{*ai@NLKZ^};Ul}beukveh?TQn;$%9=R+DX07m82gP$=}Uo_%&ngV`}Hyv8g{u z3SWzTGV|cwQuFIs7ZDOqO_fGf8Q`8MwL}eUp>q?4eqCmOTcwQuXtQckPy|4F1on8l zP*h>d+cH#XQf|+6c|S{7SF(Lg>bR~l(0uY?O{OEVlaxa5@e%T&xju=o1`=OD#qc16 zSvyH*my(dcp6~VqR;o(#@m44Lug@~_qw+HA=mS#Z^4reBy8iV?H~I;{LQWk3aKK8$bLRyt$g?- +import { ref } from 'vue' + +defineProps<{ msg: string }>() + +const count = ref(0) + + + + + diff --git a/web2/src/env.d.ts b/web2/src/env.d.ts new file mode 100644 index 0000000..d27eb5a --- /dev/null +++ b/web2/src/env.d.ts @@ -0,0 +1,8 @@ +/// + +declare module '*.vue' { + import { DefineComponent } from 'vue' + // eslint-disable-next-line @typescript-eslint/no-explicit-any, @typescript-eslint/ban-types + const component: DefineComponent<{}, {}, any> + export default component +} diff --git a/web2/src/main.ts b/web2/src/main.ts new file mode 100644 index 0000000..01433bc --- /dev/null +++ b/web2/src/main.ts @@ -0,0 +1,4 @@ +import { createApp } from 'vue' +import App from './App.vue' + +createApp(App).mount('#app') diff --git a/web2/tsconfig.json b/web2/tsconfig.json new file mode 100644 index 0000000..8617c8a --- /dev/null +++ b/web2/tsconfig.json @@ -0,0 +1,15 @@ +{ + "compilerOptions": { + "target": "esnext", + "useDefineForClassFields": true, + "module": "esnext", + "moduleResolution": "node", + "strict": true, + "jsx": "preserve", + "sourceMap": true, + "resolveJsonModule": true, + "esModuleInterop": true, + "lib": ["esnext", "dom"] + }, + "include": ["src/**/*.ts", "src/**/*.d.ts", "src/**/*.tsx", "src/**/*.vue"] +} diff --git a/web2/vite.config.ts b/web2/vite.config.ts new file mode 100644 index 0000000..315212d --- /dev/null +++ b/web2/vite.config.ts @@ -0,0 +1,7 @@ +import { defineConfig } from 'vite' +import vue from '@vitejs/plugin-vue' + +// https://vitejs.dev/config/ +export default defineConfig({ + plugins: [vue()] +}) diff --git a/web2/yarn.lock b/web2/yarn.lock new file mode 100644 index 0000000..25336fa --- /dev/null +++ b/web2/yarn.lock @@ -0,0 +1,887 @@ +# THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY. +# yarn lockfile v1 + + +"@babel/helper-validator-identifier@^7.14.9": + version "7.15.7" + resolved "https://registry.yarnpkg.com/@babel/helper-validator-identifier/-/helper-validator-identifier-7.15.7.tgz#220df993bfe904a4a6b02ab4f3385a5ebf6e2389" + integrity sha512-K4JvCtQqad9OY2+yTU8w+E82ywk/fe+ELNlt1G8z3bVGlZfn/hOcQQsUhGhW/N+tb3fxK800wLtKOE/aM0m72w== + +"@babel/parser@^7.15.0", "@babel/parser@^7.6.0", "@babel/parser@^7.9.6": + version "7.15.8" + resolved "https://registry.yarnpkg.com/@babel/parser/-/parser-7.15.8.tgz#7bacdcbe71bdc3ff936d510c15dcea7cf0b99016" + integrity sha512-BRYa3wcQnjS/nqI8Ac94pYYpJfojHVvVXJ97+IDCImX4Jc8W8Xv1+47enbruk+q1etOpsQNwnfFcNGw+gtPGxA== + +"@babel/types@^7.6.1", "@babel/types@^7.9.6": + version "7.15.6" + resolved "https://registry.yarnpkg.com/@babel/types/-/types-7.15.6.tgz#99abdc48218b2881c058dd0a7ab05b99c9be758f" + integrity sha512-BPU+7QhqNjmWyDO0/vitH/CuhpV8ZmK1wpKva8nuyNF5MJfuRNWMc+hc14+u9xT93kvykMdncrJT19h74uB1Ig== + dependencies: + "@babel/helper-validator-identifier" "^7.14.9" + to-fast-properties "^2.0.0" + +"@emmetio/abbreviation@^2.2.2": + version "2.2.2" + resolved "https://registry.yarnpkg.com/@emmetio/abbreviation/-/abbreviation-2.2.2.tgz#746762fd9e7a8c2ea604f580c62e3cfe250e6989" + integrity sha512-TtE/dBnkTCct8+LntkqVrwqQao6EnPAs1YN3cUgxOxTaBlesBCY37ROUAVZrRlG64GNnVShdl/b70RfAI3w5lw== + dependencies: + "@emmetio/scanner" "^1.0.0" + +"@emmetio/css-abbreviation@^2.1.4": + version "2.1.4" + resolved "https://registry.yarnpkg.com/@emmetio/css-abbreviation/-/css-abbreviation-2.1.4.tgz#90362e8a1122ce3b76f6c3157907d30182f53f54" + integrity sha512-qk9L60Y+uRtM5CPbB0y+QNl/1XKE09mSO+AhhSauIfr2YOx/ta3NJw2d8RtCFxgzHeRqFRr8jgyzThbu+MZ4Uw== + dependencies: + "@emmetio/scanner" "^1.0.0" + +"@emmetio/scanner@^1.0.0": + version "1.0.0" + resolved "https://registry.yarnpkg.com/@emmetio/scanner/-/scanner-1.0.0.tgz#065b2af6233fe7474d44823e3deb89724af42b5f" + integrity sha512-8HqW8EVqjnCmWXVpqAOZf+EGESdkR27odcMMMGefgKXtar00SoYNSryGv//TELI4T3QFsECo78p+0lmalk/CFA== + +"@vitejs/plugin-vue@^1.9.3": + version "1.9.3" + resolved "https://registry.yarnpkg.com/@vitejs/plugin-vue/-/plugin-vue-1.9.3.tgz#93d61893ce6c723d0209af0483ec8b91a2cd811f" + integrity sha512-yW6H/q+4Mc2PcVjSOelcsMrg/k15DnMUz8jyCFsI04emc3aLwo4AoofUfGnjHUkgirrDxSJLVqQVGhonQ3yykA== + +"@volar/code-gen@^0.27.24": + version "0.27.24" + resolved "https://registry.yarnpkg.com/@volar/code-gen/-/code-gen-0.27.24.tgz#ccdbe858951c1ee4e0c3979232d52412dc46756a" + integrity sha512-s4j/QqOZUW03PeD6LmVYI00Q1C3CfJEOePDOQwDvCTUov4lFk0iSBtFyYhjlLyQ1pdtV1+TDTErkj2aMQtc4PA== + dependencies: + "@volar/shared" "^0.27.24" + "@volar/source-map" "^0.27.24" + +"@volar/html2pug@^0.27.13": + version "0.27.13" + resolved "https://registry.yarnpkg.com/@volar/html2pug/-/html2pug-0.27.13.tgz#48dfa73ecf1ef1955a02a046d0c88845950fac85" + integrity sha512-3NYgNA5F3PDsKbbpOrVdGy2S7ZYmZIbFmbp1A/27DDzjj/uIC9Pj7HXVvbYOzi8HcOxUPt0BMrh4TVzBUaCFww== + dependencies: + domelementtype "^2.2.0" + domhandler "^4.2.0" + htmlparser2 "^6.1.0" + pug "^3.0.2" + +"@volar/shared@^0.27.24": + version "0.27.24" + resolved "https://registry.yarnpkg.com/@volar/shared/-/shared-0.27.24.tgz#a33457ec8ac0b0d367ed54c9e21913a5f8c2d6c2" + integrity sha512-Mi8a4GQaiorfb+o4EqOXDZm9E/uBJXgScFgF+NhtcMBOUKHNMKQyLI7YRGumtyJTTdaX7nSDJjGGTkv23tcOtQ== + dependencies: + upath "^2.0.1" + vscode-jsonrpc "^8.0.0-next.2" + vscode-uri "^3.0.2" + +"@volar/source-map@^0.27.24": + version "0.27.24" + resolved "https://registry.yarnpkg.com/@volar/source-map/-/source-map-0.27.24.tgz#60f2e070c169be82cbf7ffa296a30c2823c5205f" + integrity sha512-2I5a7cXqekZ66D6lHep7ttJgvVVtPEBUIe1hnpcGbnXWNA2ya6f6jKNNyTmrXQyfkh32IEuaUd4kocR+3AKMag== + dependencies: + "@volar/shared" "^0.27.24" + +"@volar/transforms@^0.27.24": + version "0.27.24" + resolved "https://registry.yarnpkg.com/@volar/transforms/-/transforms-0.27.24.tgz#68ebc53dca2e36884e247c0866ec3d24ed815784" + integrity sha512-sOHi1ZSapFlxn7yPl4MO5TXd9aWC0BVq2CgXAJ2EESb+ddh2uJbGQgLLNocX+MDh419cUuuFT2QAJpuWHhJcng== + dependencies: + "@volar/shared" "^0.27.24" + vscode-languageserver "^8.0.0-next.2" + +"@vscode/emmet-helper@^2.7.0": + version "2.7.0" + resolved "https://registry.yarnpkg.com/@vscode/emmet-helper/-/emmet-helper-2.7.0.tgz#3db485f6a650196ff8bbd38ba1b9e468ec8d22f8" + integrity sha512-LL7MoKNLUQASacQROO7hBdx5IAxsEnA0UdJFd9xXyf3sBQgz8NE3QEfo3IezE7uin8W2fkG2+EXMst3oqK6+KQ== + dependencies: + emmet "^2.3.0" + jsonc-parser "^2.3.0" + vscode-languageserver-textdocument "^1.0.1" + vscode-languageserver-types "^3.15.1" + vscode-nls "^5.0.0" + vscode-uri "^2.1.2" + +"@vue/compiler-core@3.2.19": + version "3.2.19" + resolved "https://registry.yarnpkg.com/@vue/compiler-core/-/compiler-core-3.2.19.tgz#b537dd377ce51fdb64e9b30ebfbff7cd70a64cb9" + integrity sha512-8dOPX0YOtaXol0Zf2cfLQ4NU/yHYl2H7DCKsLEZ7gdvPK6ZSEwGLJ7IdghhY2YEshEpC5RB9QKdC5I07z8Dtjg== + dependencies: + "@babel/parser" "^7.15.0" + "@vue/shared" "3.2.19" + estree-walker "^2.0.2" + source-map "^0.6.1" + +"@vue/compiler-dom@3.2.19", "@vue/compiler-dom@^3.2.19": + version "3.2.19" + resolved "https://registry.yarnpkg.com/@vue/compiler-dom/-/compiler-dom-3.2.19.tgz#0607bc90de6af55fde73b09b3c4d0bf8cb597ed8" + integrity sha512-WzQoE8rfkFjPtIioc7SSgTsnz9g2oG61DU8KHnzPrRS7fW/lji6H2uCYJfp4Z6kZE8GjnHc1Ljwl3/gxDes0cw== + dependencies: + "@vue/compiler-core" "3.2.19" + "@vue/shared" "3.2.19" + +"@vue/compiler-sfc@3.2.19": + version "3.2.19" + resolved "https://registry.yarnpkg.com/@vue/compiler-sfc/-/compiler-sfc-3.2.19.tgz#d412195a98ebd49b84602f171719294a1d9549be" + integrity sha512-pLlbgkO1UHTO02MSpa/sFOXUwIDxSMiKZ1ozE5n71CY4DM+YmI+G3gT/ZHZ46WBId7f3VTF/D8pGwMygcQbrQA== + dependencies: + "@babel/parser" "^7.15.0" + "@vue/compiler-core" "3.2.19" + "@vue/compiler-dom" "3.2.19" + "@vue/compiler-ssr" "3.2.19" + "@vue/ref-transform" "3.2.19" + "@vue/shared" "3.2.19" + estree-walker "^2.0.2" + magic-string "^0.25.7" + postcss "^8.1.10" + source-map "^0.6.1" + +"@vue/compiler-ssr@3.2.19": + version "3.2.19" + resolved "https://registry.yarnpkg.com/@vue/compiler-ssr/-/compiler-ssr-3.2.19.tgz#3e91ecf70f8f961c5f63eacd2139bcdab9a7a07c" + integrity sha512-oLon0Cn3O7WEYzzmzZavGoqXH+199LT+smdjBT3Uf3UX4HwDNuBFCmvL0TsqV9SQnIgKvBRbQ7lhbpnd4lqM3w== + dependencies: + "@vue/compiler-dom" "3.2.19" + "@vue/shared" "3.2.19" + +"@vue/reactivity@3.2.19", "@vue/reactivity@^3.2.19": + version "3.2.19" + resolved "https://registry.yarnpkg.com/@vue/reactivity/-/reactivity-3.2.19.tgz#fc6e0f0106f295226835cfed5ff5f84d927bea65" + integrity sha512-FtachoYs2SnyrWup5UikP54xDX6ZJ1s5VgHcJp4rkGoutU3Ry61jhs+nCX7J64zjX992Mh9gGUC0LqTs8q9vCA== + dependencies: + "@vue/shared" "3.2.19" + +"@vue/ref-transform@3.2.19": + version "3.2.19" + resolved "https://registry.yarnpkg.com/@vue/ref-transform/-/ref-transform-3.2.19.tgz#cf0f986486bb26838fbd09749e927bab19745600" + integrity sha512-03wwUnoIAeKti5IGGx6Vk/HEBJ+zUcm5wrUM3+PQsGf7IYnXTbeIfHHpx4HeSeWhnLAjqZjADQwW8uA4rBmVbg== + dependencies: + "@babel/parser" "^7.15.0" + "@vue/compiler-core" "3.2.19" + "@vue/shared" "3.2.19" + estree-walker "^2.0.2" + magic-string "^0.25.7" + +"@vue/runtime-core@3.2.19": + version "3.2.19" + resolved "https://registry.yarnpkg.com/@vue/runtime-core/-/runtime-core-3.2.19.tgz#807715b7f4728abb84fa4a8efdbe37d8ddb4c6d3" + integrity sha512-qArZSWKxWsgKfxk9BelZ32nY0MZ31CAW2kUUyVJyxh4cTfHaXGbjiQB5JgsvKc49ROMNffv9t3/qjasQqAH+RQ== + dependencies: + "@vue/reactivity" "3.2.19" + "@vue/shared" "3.2.19" + +"@vue/runtime-dom@3.2.19": + version "3.2.19" + resolved "https://registry.yarnpkg.com/@vue/runtime-dom/-/runtime-dom-3.2.19.tgz#7e8bf645754703e360fa132e4be9113edf2377bb" + integrity sha512-hIRboxXwafeHhbZEkZYNV0MiJXPNf4fP0X6hM2TJb0vssz8BKhD9cF92BkRgZztTQevecbhk0gu4uAPJ3dxL9A== + dependencies: + "@vue/runtime-core" "3.2.19" + "@vue/shared" "3.2.19" + csstype "^2.6.8" + +"@vue/server-renderer@3.2.19": + version "3.2.19" + resolved "https://registry.yarnpkg.com/@vue/server-renderer/-/server-renderer-3.2.19.tgz#870bcec9f7cdaee0c2187a169b6e636ab4362fb1" + integrity sha512-A9FNT7fgQJXItwdzWREntAgWKVtKYuXHBKGev/H4+ByTu8vB7gQXGcim01QxaJshdNg4dYuH2tEBZXCNCNx+/w== + dependencies: + "@vue/compiler-ssr" "3.2.19" + "@vue/shared" "3.2.19" + +"@vue/shared@3.2.19", "@vue/shared@^3.2.19": + version "3.2.19" + resolved "https://registry.yarnpkg.com/@vue/shared/-/shared-3.2.19.tgz#111ec3da18337d86274446984c49925b1b2b2dd7" + integrity sha512-Knqhx7WieLdVgwCAZgTVrDCXZ50uItuecLh9JdLC8O+a5ayaSyIQYveUK3hCRNC7ws5zalHmZwfdLMGaS8r4Ew== + +acorn@^7.1.1: + version "7.4.1" + resolved "https://registry.yarnpkg.com/acorn/-/acorn-7.4.1.tgz#feaed255973d2e77555b83dbc08851a6c63520fa" + integrity sha512-nQyp0o1/mNdbTO1PO6kHkwSrmgZ0MT/jCCpNiwbUjGoRN4dlBhqJtoQuCnEOKzgTVwg0ZWiCoQy6SxMebQVh8A== + +asap@~2.0.3: + version "2.0.6" + resolved "https://registry.yarnpkg.com/asap/-/asap-2.0.6.tgz#e50347611d7e690943208bbdafebcbc2fb866d46" + integrity sha1-5QNHYR1+aQlDIIu9r+vLwvuGbUY= + +assert-never@^1.2.1: + version "1.2.1" + resolved "https://registry.yarnpkg.com/assert-never/-/assert-never-1.2.1.tgz#11f0e363bf146205fb08193b5c7b90f4d1cf44fe" + integrity sha512-TaTivMB6pYI1kXwrFlEhLeGfOqoDNdTxjCdwRfFFkEA30Eu+k48W34nlok2EYWJfFFzqaEmichdNM7th6M5HNw== + +babel-walk@3.0.0-canary-5: + version "3.0.0-canary-5" + resolved "https://registry.yarnpkg.com/babel-walk/-/babel-walk-3.0.0-canary-5.tgz#f66ecd7298357aee44955f235a6ef54219104b11" + integrity sha512-GAwkz0AihzY5bkwIY5QDR+LvsRQgB/B+1foMPvi0FZPMl5fjD7ICiznUiBdLYMH1QYe6vqu4gWYytZOccLouFw== + dependencies: + "@babel/types" "^7.9.6" + +call-bind@^1.0.2: + version "1.0.2" + resolved "https://registry.yarnpkg.com/call-bind/-/call-bind-1.0.2.tgz#b1d4e89e688119c3c9a903ad30abb2f6a919be3c" + integrity sha512-7O+FbCihrB5WGbFYesctwmTKae6rOiIzmz1icreWJ+0aA7LJfuqhEso2T9ncpcFtzMQtzXf2QGGueWJGTYsqrA== + dependencies: + function-bind "^1.1.1" + get-intrinsic "^1.0.2" + +character-parser@^2.2.0: + version "2.2.0" + resolved "https://registry.yarnpkg.com/character-parser/-/character-parser-2.2.0.tgz#c7ce28f36d4bcd9744e5ffc2c5fcde1c73261fc0" + integrity sha1-x84o821LzZdE5f/CxfzeHHMmH8A= + dependencies: + is-regex "^1.0.3" + +constantinople@^4.0.1: + version "4.0.1" + resolved "https://registry.yarnpkg.com/constantinople/-/constantinople-4.0.1.tgz#0def113fa0e4dc8de83331a5cf79c8b325213151" + integrity sha512-vCrqcSIq4//Gx74TXXCGnHpulY1dskqLTFGDmhrGxzeXL8lF8kvXv6mpNWlJj1uD4DW23D4ljAqbY4RRaaUZIw== + dependencies: + "@babel/parser" "^7.6.0" + "@babel/types" "^7.6.1" + +csstype@^2.6.8: + version "2.6.18" + resolved "https://registry.yarnpkg.com/csstype/-/csstype-2.6.18.tgz#980a8b53085f34af313410af064f2bd241784218" + integrity sha512-RSU6Hyeg14am3Ah4VZEmeX8H7kLwEEirXe6aU2IPfKNvhXwTflK5HQRDNI0ypQXoqmm+QPyG2IaPuQE5zMwSIQ== + +doctypes@^1.1.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/doctypes/-/doctypes-1.1.0.tgz#ea80b106a87538774e8a3a4a5afe293de489e0a9" + integrity sha1-6oCxBqh1OHdOijpKWv4pPeSJ4Kk= + +dom-serializer@^1.0.1: + version "1.3.2" + resolved "https://registry.yarnpkg.com/dom-serializer/-/dom-serializer-1.3.2.tgz#6206437d32ceefaec7161803230c7a20bc1b4d91" + integrity sha512-5c54Bk5Dw4qAxNOI1pFEizPSjVsx5+bpJKmL2kPn8JhBUq2q09tTCa3mjijun2NfK78NMouDYNMBkOrPZiS+ig== + dependencies: + domelementtype "^2.0.1" + domhandler "^4.2.0" + entities "^2.0.0" + +domelementtype@^2.0.1, domelementtype@^2.2.0: + version "2.2.0" + resolved "https://registry.yarnpkg.com/domelementtype/-/domelementtype-2.2.0.tgz#9a0b6c2782ed6a1c7323d42267183df9bd8b1d57" + integrity sha512-DtBMo82pv1dFtUmHyr48beiuq792Sxohr+8Hm9zoxklYPfa6n0Z3Byjj2IV7bmr2IyqClnqEQhfgHJJ5QF0R5A== + +domhandler@^4.0.0, domhandler@^4.2.0: + version "4.2.2" + resolved "https://registry.yarnpkg.com/domhandler/-/domhandler-4.2.2.tgz#e825d721d19a86b8c201a35264e226c678ee755f" + integrity sha512-PzE9aBMsdZO8TK4BnuJwH0QT41wgMbRzuZrHUcpYncEjmQazq8QEaBWgLG7ZyC/DAZKEgglpIA6j4Qn/HmxS3w== + dependencies: + domelementtype "^2.2.0" + +domutils@^2.5.2: + version "2.8.0" + resolved "https://registry.yarnpkg.com/domutils/-/domutils-2.8.0.tgz#4437def5db6e2d1f5d6ee859bd95ca7d02048135" + integrity sha512-w96Cjofp72M5IIhpjgobBimYEfoPjx1Vx0BSX9P30WBdZW2WIKU0T1Bd0kz2eNZ9ikjKgHbEyKx8BB6H1L3h3A== + dependencies: + dom-serializer "^1.0.1" + domelementtype "^2.2.0" + domhandler "^4.2.0" + +emmet@^2.3.0: + version "2.3.4" + resolved "https://registry.yarnpkg.com/emmet/-/emmet-2.3.4.tgz#5ba0d7a5569a68c7697dfa890c772e4f3179d123" + integrity sha512-3IqSwmO+N2ZGeuhDyhV/TIOJFUbkChi53bcasSNRE7Yd+4eorbbYz4e53TpMECt38NtYkZNupQCZRlwdAYA42A== + dependencies: + "@emmetio/abbreviation" "^2.2.2" + "@emmetio/css-abbreviation" "^2.1.4" + +entities@^2.0.0: + version "2.2.0" + resolved "https://registry.yarnpkg.com/entities/-/entities-2.2.0.tgz#098dc90ebb83d8dffa089d55256b351d34c4da55" + integrity sha512-p92if5Nz619I0w+akJrLZH0MX0Pb5DX39XOwQTtXSdQQOaYH03S1uIQp4mhOZtAXrxq4ViO67YTiLBo2638o9A== + +esbuild-android-arm64@0.13.4: + version "0.13.4" + resolved "https://registry.yarnpkg.com/esbuild-android-arm64/-/esbuild-android-arm64-0.13.4.tgz#5178a20d2b7aba741a31c19609f9e67b346996b9" + integrity sha512-elDJt+jNyoHFId0/dKsuVYUPke3EcquIyUwzJCH17a3ERglN3A9aMBI5zbz+xNZ+FbaDNdpn0RaJHCFLbZX+fA== + +esbuild-darwin-64@0.13.4: + version "0.13.4" + resolved "https://registry.yarnpkg.com/esbuild-darwin-64/-/esbuild-darwin-64-0.13.4.tgz#7a3e66c8e1271b650541b25eed65c84f3564a69d" + integrity sha512-zJQGyHRAdZUXlRzbN7W+7ykmEiGC+bq3Gc4GxKYjjWTgDRSEly98ym+vRNkDjXwXYD3gGzSwvH35+MiHAtWvLA== + +esbuild-darwin-arm64@0.13.4: + version "0.13.4" + resolved "https://registry.yarnpkg.com/esbuild-darwin-arm64/-/esbuild-darwin-arm64-0.13.4.tgz#793feca6032b2a57ef291eb9b2d33768d60a49d6" + integrity sha512-r8oYvAtqSGq8HNTZCAx4TdLE7jZiGhX9ooGi5AQAey37MA6XNaP8ZNlw9OCpcgpx3ryU2WctXwIqPzkHO7a8dg== + +esbuild-freebsd-64@0.13.4: + version "0.13.4" + resolved "https://registry.yarnpkg.com/esbuild-freebsd-64/-/esbuild-freebsd-64-0.13.4.tgz#294aec3c2cf4b41fb6900212fc9c33dd8fbbb4a2" + integrity sha512-u9DRGkn09EN8+lCh6z7FKle7awi17PJRBuAKdRNgSo5ZrH/3m+mYaJK2PR2URHMpAfXiwJX341z231tSdVe3Yw== + +esbuild-freebsd-arm64@0.13.4: + version "0.13.4" + resolved "https://registry.yarnpkg.com/esbuild-freebsd-arm64/-/esbuild-freebsd-arm64-0.13.4.tgz#09fe66c751c12f9b976976b1d83f3de594cb2787" + integrity sha512-q3B2k68Uf6gfjATjcK16DqxvjqRQkHL8aPoOfj4op+lSqegdXvBacB1d8jw8PxbWJ8JHpdTLdAVUYU80kotQXA== + +esbuild-linux-32@0.13.4: + version "0.13.4" + resolved "https://registry.yarnpkg.com/esbuild-linux-32/-/esbuild-linux-32-0.13.4.tgz#a9f0793d7bcc9cef4f4ffa4398c525877fba5839" + integrity sha512-UUYJPHSiKAO8KoN3Ls/iZtgDLZvK5HarES96aolDPWZnq9FLx4dIHM/x2z4Rxv9IYqQ/DxlPoE2Co1UPBIYYeA== + +esbuild-linux-64@0.13.4: + version "0.13.4" + resolved "https://registry.yarnpkg.com/esbuild-linux-64/-/esbuild-linux-64-0.13.4.tgz#c0d0b4c9d62e3bbf8bdf2cece37403aa6d60fc2e" + integrity sha512-+RnohAKiiUW4UHLGRkNR1AnENW1gCuDWuygEtd4jxTNPIoeC7lbXGor7rtgjj9AdUzFgOEvAXyNNX01kJ8NueQ== + +esbuild-linux-arm64@0.13.4: + version "0.13.4" + resolved "https://registry.yarnpkg.com/esbuild-linux-arm64/-/esbuild-linux-arm64-0.13.4.tgz#1292d97bfa64a08d12728f8a7837bf92776c779b" + integrity sha512-+A188cAdd6QuSRxMIwRrWLjgphQA0LDAQ/ECVlrPVJwnx+1i64NjDZivoqPYLOTkSPIKntiWwMhhf0U5/RrPHQ== + +esbuild-linux-arm@0.13.4: + version "0.13.4" + resolved "https://registry.yarnpkg.com/esbuild-linux-arm/-/esbuild-linux-arm-0.13.4.tgz#186cd9b8885ac132b9953a4a0afe668168debd10" + integrity sha512-BH5gKve4jglS7UPSsfwHSX79I5agC/lm4eKoRUEyo8lwQs89frQSRp2Xup+6SFQnxt3md5EsKcd2Dbkqeb3gPA== + +esbuild-linux-mips64le@0.13.4: + version "0.13.4" + resolved "https://registry.yarnpkg.com/esbuild-linux-mips64le/-/esbuild-linux-mips64le-0.13.4.tgz#42049bf72bc586817b4a51cc9e32148d13e5e807" + integrity sha512-0xkwtPaUkG5xMTFGaQPe1AadSe5QAiQuD4Gix1O9k5Xo/U8xGIkw9UFUTvfEUeu71vFb6ZgsIacfP1NLoFjWNw== + +esbuild-linux-ppc64le@0.13.4: + version "0.13.4" + resolved "https://registry.yarnpkg.com/esbuild-linux-ppc64le/-/esbuild-linux-ppc64le-0.13.4.tgz#adf1ce2ef2302757c4383887da6ac4dd25be9d4f" + integrity sha512-E1+oJPP7A+j23GPo3CEpBhGwG1bni4B8IbTA3/3rvzjURwUMZdcN3Fhrz24rnjzdLSHmULtOE4VsbT42h1Om4Q== + +esbuild-openbsd-64@0.13.4: + version "0.13.4" + resolved "https://registry.yarnpkg.com/esbuild-openbsd-64/-/esbuild-openbsd-64-0.13.4.tgz#1c8122101898c52a20c8786935cf3eb7a19b83b4" + integrity sha512-xEkI1o5HYxDzbv9jSox0EsDxpwraG09SRiKKv0W8pH6O3bt+zPSlnoK7+I7Q69tkvONkpIq5n2o+c55uq0X7cw== + +esbuild-sunos-64@0.13.4: + version "0.13.4" + resolved "https://registry.yarnpkg.com/esbuild-sunos-64/-/esbuild-sunos-64-0.13.4.tgz#4ec95faa14a60f295fe485bebffefff408739337" + integrity sha512-bjXUMcODMnB6hQicLBBmmnBl7OMDyVpFahKvHGXJfDChIi5udiIRKCmFUFIRn+AUAKVlfrofRKdyPC7kBsbvGQ== + +esbuild-windows-32@0.13.4: + version "0.13.4" + resolved "https://registry.yarnpkg.com/esbuild-windows-32/-/esbuild-windows-32-0.13.4.tgz#3182c380487b797b04d0ec2c80c2945666869080" + integrity sha512-z4CH07pfyVY0XF98TCsGmLxKCl0kyvshKDbdpTekW9f2d+dJqn5mmoUyWhpSVJ0SfYWJg86FoD9nMbbaMVyGdg== + +esbuild-windows-64@0.13.4: + version "0.13.4" + resolved "https://registry.yarnpkg.com/esbuild-windows-64/-/esbuild-windows-64-0.13.4.tgz#b9e995f92d81f433a04f33611e603e82f9232e69" + integrity sha512-uVL11vORRPjocGLYam67rwFLd0LvkrHEs+JG+1oJN4UD9MQmNGZPa4gBHo6hDpF+kqRJ9kXgQSeDqUyRy0tj/Q== + +esbuild-windows-arm64@0.13.4: + version "0.13.4" + resolved "https://registry.yarnpkg.com/esbuild-windows-arm64/-/esbuild-windows-arm64-0.13.4.tgz#fb239532f07b764d158f4cc787178ef4c6fadb5c" + integrity sha512-vA6GLvptgftRcDcWngD5cMlL4f4LbL8JjU2UMT9yJ0MT5ra6hdZNFWnOeOoEtY4GtJ6OjZ0i+81sTqhAB0fMkg== + +esbuild@^0.13.2: + version "0.13.4" + resolved "https://registry.yarnpkg.com/esbuild/-/esbuild-0.13.4.tgz#ce2deb56c4fb360938311cbfc67f8e467bb6841b" + integrity sha512-wMA5eUwpavTBiNl+It6j8OQuKVh69l6z4DKDLzoTIqC+gChnPpcmqdA8WNHptUHRnfyML+mKEQPlW7Mybj8gHg== + optionalDependencies: + esbuild-android-arm64 "0.13.4" + esbuild-darwin-64 "0.13.4" + esbuild-darwin-arm64 "0.13.4" + esbuild-freebsd-64 "0.13.4" + esbuild-freebsd-arm64 "0.13.4" + esbuild-linux-32 "0.13.4" + esbuild-linux-64 "0.13.4" + esbuild-linux-arm "0.13.4" + esbuild-linux-arm64 "0.13.4" + esbuild-linux-mips64le "0.13.4" + esbuild-linux-ppc64le "0.13.4" + esbuild-openbsd-64 "0.13.4" + esbuild-sunos-64 "0.13.4" + esbuild-windows-32 "0.13.4" + esbuild-windows-64 "0.13.4" + esbuild-windows-arm64 "0.13.4" + +estree-walker@^2.0.2: + version "2.0.2" + resolved "https://registry.yarnpkg.com/estree-walker/-/estree-walker-2.0.2.tgz#52f010178c2a4c117a7757cfe942adb7d2da4cac" + integrity sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w== + +fsevents@~2.3.2: + version "2.3.2" + resolved "https://registry.yarnpkg.com/fsevents/-/fsevents-2.3.2.tgz#8a526f78b8fdf4623b709e0b975c52c24c02fd1a" + integrity sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA== + +function-bind@^1.1.1: + version "1.1.1" + resolved "https://registry.yarnpkg.com/function-bind/-/function-bind-1.1.1.tgz#a56899d3ea3c9bab874bb9773b7c5ede92f4895d" + integrity sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A== + +get-intrinsic@^1.0.2: + version "1.1.1" + resolved "https://registry.yarnpkg.com/get-intrinsic/-/get-intrinsic-1.1.1.tgz#15f59f376f855c446963948f0d24cd3637b4abc6" + integrity sha512-kWZrnVM42QCiEA2Ig1bG8zjoIMOgxWwYCEeNdwY6Tv/cOSeGpcoX4pXHfKUxNKVoArnrEr2e9srnAxxGIraS9Q== + dependencies: + function-bind "^1.1.1" + has "^1.0.3" + has-symbols "^1.0.1" + +has-symbols@^1.0.1, has-symbols@^1.0.2: + version "1.0.2" + resolved "https://registry.yarnpkg.com/has-symbols/-/has-symbols-1.0.2.tgz#165d3070c00309752a1236a479331e3ac56f1423" + integrity sha512-chXa79rL/UC2KlX17jo3vRGz0azaWEx5tGqZg5pO3NUyEJVB17dMruQlzCCOfUvElghKcm5194+BCRvi2Rv/Gw== + +has-tostringtag@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/has-tostringtag/-/has-tostringtag-1.0.0.tgz#7e133818a7d394734f941e73c3d3f9291e658b25" + integrity sha512-kFjcSNhnlGV1kyoGk7OXKSawH5JOb/LzUc5w9B02hOTO0dfFRjbHQKvg1d6cf3HbeUmtU9VbbV3qzZ2Teh97WQ== + dependencies: + has-symbols "^1.0.2" + +has@^1.0.3: + version "1.0.3" + resolved "https://registry.yarnpkg.com/has/-/has-1.0.3.tgz#722d7cbfc1f6aa8241f16dd814e011e1f41e8796" + integrity sha512-f2dvO0VU6Oej7RkWJGrehjbzMAjFp5/VKPp5tTpWIV4JHHZK1/BxbFRtf/siA2SWTe09caDmVtYYzWEIbBS4zw== + dependencies: + function-bind "^1.1.1" + +htmlparser2@^6.1.0: + version "6.1.0" + resolved "https://registry.yarnpkg.com/htmlparser2/-/htmlparser2-6.1.0.tgz#c4d762b6c3371a05dbe65e94ae43a9f845fb8fb7" + integrity sha512-gyyPk6rgonLFEDGoeRgQNaEUvdJ4ktTmmUh/h2t7s+M8oPpIPxgNACWa+6ESR57kXstwqPiCut0V8NRpcwgU7A== + dependencies: + domelementtype "^2.0.1" + domhandler "^4.0.0" + domutils "^2.5.2" + entities "^2.0.0" + +is-core-module@^2.2.0: + version "2.7.0" + resolved "https://registry.yarnpkg.com/is-core-module/-/is-core-module-2.7.0.tgz#3c0ef7d31b4acfc574f80c58409d568a836848e3" + integrity sha512-ByY+tjCciCr+9nLryBYcSD50EOGWt95c7tIsKTG1J2ixKKXPvF7Ej3AVd+UfDydAJom3biBGDBALaO79ktwgEQ== + dependencies: + has "^1.0.3" + +is-expression@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/is-expression/-/is-expression-4.0.0.tgz#c33155962abf21d0afd2552514d67d2ec16fd2ab" + integrity sha512-zMIXX63sxzG3XrkHkrAPvm/OVZVSCPNkwMHU8oTX7/U3AL78I0QXCEICXUM13BIa8TYGZ68PiTKfQz3yaTNr4A== + dependencies: + acorn "^7.1.1" + object-assign "^4.1.1" + +is-promise@^2.0.0: + version "2.2.2" + resolved "https://registry.yarnpkg.com/is-promise/-/is-promise-2.2.2.tgz#39ab959ccbf9a774cf079f7b40c7a26f763135f1" + integrity sha512-+lP4/6lKUBfQjZ2pdxThZvLUAafmZb8OAxFb8XXtiQmS35INgr85hdOGoEs124ez1FCnZJt6jau/T+alh58QFQ== + +is-regex@^1.0.3: + version "1.1.4" + resolved "https://registry.yarnpkg.com/is-regex/-/is-regex-1.1.4.tgz#eef5663cd59fa4c0ae339505323df6854bb15958" + integrity sha512-kvRdxDsxZjhzUX07ZnLydzS1TU/TJlTUHHY4YLL87e37oUA49DfkLqgy+VjFocowy29cKvcSiu+kIv728jTTVg== + dependencies: + call-bind "^1.0.2" + has-tostringtag "^1.0.0" + +js-stringify@^1.0.2: + version "1.0.2" + resolved "https://registry.yarnpkg.com/js-stringify/-/js-stringify-1.0.2.tgz#1736fddfd9724f28a3682adc6230ae7e4e9679db" + integrity sha1-Fzb939lyTyijaCrcYjCufk6Weds= + +jsonc-parser@^2.3.0: + version "2.3.1" + resolved "https://registry.yarnpkg.com/jsonc-parser/-/jsonc-parser-2.3.1.tgz#59549150b133f2efacca48fe9ce1ec0659af2342" + integrity sha512-H8jvkz1O50L3dMZCsLqiuB2tA7muqbSg1AtGEkN0leAqGjsUzDJir3Zwr02BhqdcITPg3ei3mZ+HjMocAknhhg== + +jsonc-parser@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/jsonc-parser/-/jsonc-parser-3.0.0.tgz#abdd785701c7e7eaca8a9ec8cf070ca51a745a22" + integrity sha512-fQzRfAbIBnR0IQvftw9FJveWiHp72Fg20giDrHz6TdfB12UH/uue0D3hm57UB5KgAVuniLMCaS8P1IMj9NR7cA== + +jstransformer@1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/jstransformer/-/jstransformer-1.0.0.tgz#ed8bf0921e2f3f1ed4d5c1a44f68709ed24722c3" + integrity sha1-7Yvwkh4vPx7U1cGkT2hwntJHIsM= + dependencies: + is-promise "^2.0.0" + promise "^7.0.1" + +lru-cache@^6.0.0: + version "6.0.0" + resolved "https://registry.yarnpkg.com/lru-cache/-/lru-cache-6.0.0.tgz#6d6fe6570ebd96aaf90fcad1dafa3b2566db3a94" + integrity sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA== + dependencies: + yallist "^4.0.0" + +magic-string@^0.25.7: + version "0.25.7" + resolved "https://registry.yarnpkg.com/magic-string/-/magic-string-0.25.7.tgz#3f497d6fd34c669c6798dcb821f2ef31f5445051" + integrity sha512-4CrMT5DOHTDk4HYDlzmwu4FVCcIYI8gauveasrdCu2IKIFOJ3f0v/8MDGJCDL9oD2ppz/Av1b0Nj345H9M+XIA== + dependencies: + sourcemap-codec "^1.4.4" + +nanoid@^3.1.28: + version "3.1.29" + resolved "https://registry.yarnpkg.com/nanoid/-/nanoid-3.1.29.tgz#214fb2d7a33e1a5bef4757b779dfaeb6a4e5aeb4" + integrity sha512-dW2pUSGZ8ZnCFIlBIA31SV8huOGCHb6OwzVCc7A69rb/a+SgPBwfmLvK5TKQ3INPbRkcI8a/Owo0XbiTNH19wg== + +object-assign@^4.1.1: + version "4.1.1" + resolved "https://registry.yarnpkg.com/object-assign/-/object-assign-4.1.1.tgz#2109adc7965887cfc05cbbd442cac8bfbb360863" + integrity sha1-IQmtx5ZYh8/AXLvUQsrIv7s2CGM= + +path-parse@^1.0.6: + version "1.0.7" + resolved "https://registry.yarnpkg.com/path-parse/-/path-parse-1.0.7.tgz#fbc114b60ca42b30d9daf5858e4bd68bbedb6735" + integrity sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw== + +picocolors@^0.2.1: + version "0.2.1" + resolved "https://registry.yarnpkg.com/picocolors/-/picocolors-0.2.1.tgz#570670f793646851d1ba135996962abad587859f" + integrity sha512-cMlDqaLEqfSaW8Z7N5Jw+lyIW869EzT73/F5lhtY9cLGoVxSXznfgfXMO0Z5K0o0Q2TkTXq+0KFsdnSe3jDViA== + +postcss@^8.1.10, postcss@^8.3.8: + version "8.3.9" + resolved "https://registry.yarnpkg.com/postcss/-/postcss-8.3.9.tgz#98754caa06c4ee9eb59cc48bd073bb6bd3437c31" + integrity sha512-f/ZFyAKh9Dnqytx5X62jgjhhzttjZS7hMsohcI7HEI5tjELX/HxCy3EFhsRxyzGvrzFF+82XPvCS8T9TFleVJw== + dependencies: + nanoid "^3.1.28" + picocolors "^0.2.1" + source-map-js "^0.6.2" + +promise@^7.0.1: + version "7.3.1" + resolved "https://registry.yarnpkg.com/promise/-/promise-7.3.1.tgz#064b72602b18f90f29192b8b1bc418ffd1ebd3bf" + integrity sha512-nolQXZ/4L+bP/UGlkfaIujX9BKxGwmQ9OT4mOt5yvy8iK1h3wqTEJCijzGANTCCl9nWjY41juyAn2K3Q1hLLTg== + dependencies: + asap "~2.0.3" + +pug-attrs@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/pug-attrs/-/pug-attrs-3.0.0.tgz#b10451e0348165e31fad1cc23ebddd9dc7347c41" + integrity sha512-azINV9dUtzPMFQktvTXciNAfAuVh/L/JCl0vtPCwvOA21uZrC08K/UnmrL+SXGEVc1FwzjW62+xw5S/uaLj6cA== + dependencies: + constantinople "^4.0.1" + js-stringify "^1.0.2" + pug-runtime "^3.0.0" + +pug-code-gen@^3.0.2: + version "3.0.2" + resolved "https://registry.yarnpkg.com/pug-code-gen/-/pug-code-gen-3.0.2.tgz#ad190f4943133bf186b60b80de483100e132e2ce" + integrity sha512-nJMhW16MbiGRiyR4miDTQMRWDgKplnHyeLvioEJYbk1RsPI3FuA3saEP8uwnTb2nTJEKBU90NFVWJBk4OU5qyg== + dependencies: + constantinople "^4.0.1" + doctypes "^1.1.0" + js-stringify "^1.0.2" + pug-attrs "^3.0.0" + pug-error "^2.0.0" + pug-runtime "^3.0.0" + void-elements "^3.1.0" + with "^7.0.0" + +pug-error@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/pug-error/-/pug-error-2.0.0.tgz#5c62173cb09c34de2a2ce04f17b8adfec74d8ca5" + integrity sha512-sjiUsi9M4RAGHktC1drQfCr5C5eriu24Lfbt4s+7SykztEOwVZtbFk1RRq0tzLxcMxMYTBR+zMQaG07J/btayQ== + +pug-filters@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/pug-filters/-/pug-filters-4.0.0.tgz#d3e49af5ba8472e9b7a66d980e707ce9d2cc9b5e" + integrity sha512-yeNFtq5Yxmfz0f9z2rMXGw/8/4i1cCFecw/Q7+D0V2DdtII5UvqE12VaZ2AY7ri6o5RNXiweGH79OCq+2RQU4A== + dependencies: + constantinople "^4.0.1" + jstransformer "1.0.0" + pug-error "^2.0.0" + pug-walk "^2.0.0" + resolve "^1.15.1" + +pug-lexer@^5.0.1: + version "5.0.1" + resolved "https://registry.yarnpkg.com/pug-lexer/-/pug-lexer-5.0.1.tgz#ae44628c5bef9b190b665683b288ca9024b8b0d5" + integrity sha512-0I6C62+keXlZPZkOJeVam9aBLVP2EnbeDw3An+k0/QlqdwH6rv8284nko14Na7c0TtqtogfWXcRoFE4O4Ff20w== + dependencies: + character-parser "^2.2.0" + is-expression "^4.0.0" + pug-error "^2.0.0" + +pug-linker@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/pug-linker/-/pug-linker-4.0.0.tgz#12cbc0594fc5a3e06b9fc59e6f93c146962a7708" + integrity sha512-gjD1yzp0yxbQqnzBAdlhbgoJL5qIFJw78juN1NpTLt/mfPJ5VgC4BvkoD3G23qKzJtIIXBbcCt6FioLSFLOHdw== + dependencies: + pug-error "^2.0.0" + pug-walk "^2.0.0" + +pug-load@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/pug-load/-/pug-load-3.0.0.tgz#9fd9cda52202b08adb11d25681fb9f34bd41b662" + integrity sha512-OCjTEnhLWZBvS4zni/WUMjH2YSUosnsmjGBB1An7CsKQarYSWQ0GCVyd4eQPMFJqZ8w9xgs01QdiZXKVjk92EQ== + dependencies: + object-assign "^4.1.1" + pug-walk "^2.0.0" + +pug-parser@^6.0.0: + version "6.0.0" + resolved "https://registry.yarnpkg.com/pug-parser/-/pug-parser-6.0.0.tgz#a8fdc035863a95b2c1dc5ebf4ecf80b4e76a1260" + integrity sha512-ukiYM/9cH6Cml+AOl5kETtM9NR3WulyVP2y4HOU45DyMim1IeP/OOiyEWRr6qk5I5klpsBnbuHpwKmTx6WURnw== + dependencies: + pug-error "^2.0.0" + token-stream "1.0.0" + +pug-runtime@^3.0.0, pug-runtime@^3.0.1: + version "3.0.1" + resolved "https://registry.yarnpkg.com/pug-runtime/-/pug-runtime-3.0.1.tgz#f636976204723f35a8c5f6fad6acda2a191b83d7" + integrity sha512-L50zbvrQ35TkpHwv0G6aLSuueDRwc/97XdY8kL3tOT0FmhgG7UypU3VztfV/LATAvmUfYi4wNxSajhSAeNN+Kg== + +pug-strip-comments@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/pug-strip-comments/-/pug-strip-comments-2.0.0.tgz#f94b07fd6b495523330f490a7f554b4ff876303e" + integrity sha512-zo8DsDpH7eTkPHCXFeAk1xZXJbyoTfdPlNR0bK7rpOMuhBYb0f5qUVCO1xlsitYd3w5FQTK7zpNVKb3rZoUrrQ== + dependencies: + pug-error "^2.0.0" + +pug-walk@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/pug-walk/-/pug-walk-2.0.0.tgz#417aabc29232bb4499b5b5069a2b2d2a24d5f5fe" + integrity sha512-yYELe9Q5q9IQhuvqsZNwA5hfPkMJ8u92bQLIMcsMxf/VADjNtEYptU+inlufAFYcWdHlwNfZOEnOOQrZrcyJCQ== + +pug@^3.0.2: + version "3.0.2" + resolved "https://registry.yarnpkg.com/pug/-/pug-3.0.2.tgz#f35c7107343454e43bc27ae0ff76c731b78ea535" + integrity sha512-bp0I/hiK1D1vChHh6EfDxtndHji55XP/ZJKwsRqrz6lRia6ZC2OZbdAymlxdVFwd1L70ebrVJw4/eZ79skrIaw== + dependencies: + pug-code-gen "^3.0.2" + pug-filters "^4.0.0" + pug-lexer "^5.0.1" + pug-linker "^4.0.0" + pug-load "^3.0.0" + pug-parser "^6.0.0" + pug-runtime "^3.0.1" + pug-strip-comments "^2.0.0" + +request-light@^0.5.4: + version "0.5.4" + resolved "https://registry.yarnpkg.com/request-light/-/request-light-0.5.4.tgz#497a98c6d8ae49536417a5e2d7f383b934f3e38c" + integrity sha512-t3566CMweOFlUk7Y1DJMu5OrtpoZEb6aSTsLQVT3wtrIEJ5NhcY9G/Oqxvjllzl4a15zXfFlcr9q40LbLVQJqw== + +resolve@^1.15.1, resolve@^1.20.0: + version "1.20.0" + resolved "https://registry.yarnpkg.com/resolve/-/resolve-1.20.0.tgz#629a013fb3f70755d6f0b7935cc1c2c5378b1975" + integrity sha512-wENBPt4ySzg4ybFQW2TT1zMQucPK95HSh/nq2CFTZVOGut2+pQvSsgtda4d26YrYcr067wjbmzOG8byDPBX63A== + dependencies: + is-core-module "^2.2.0" + path-parse "^1.0.6" + +rollup@^2.57.0: + version "2.58.0" + resolved "https://registry.yarnpkg.com/rollup/-/rollup-2.58.0.tgz#a643983365e7bf7f5b7c62a8331b983b7c4c67fb" + integrity sha512-NOXpusKnaRpbS7ZVSzcEXqxcLDOagN6iFS8p45RkoiMqPHDLwJm758UF05KlMoCRbLBTZsPOIa887gZJ1AiXvw== + optionalDependencies: + fsevents "~2.3.2" + +semver@^7.3.5: + version "7.3.5" + resolved "https://registry.yarnpkg.com/semver/-/semver-7.3.5.tgz#0b621c879348d8998e4b0e4be94b3f12e6018ef7" + integrity sha512-PoeGJYh8HK4BTO/a9Tf6ZG3veo/A7ZVsYrSA6J8ny9nb3B1VrpkuN+z9OE5wfE5p6H4LchYZsegiQgbJD94ZFQ== + dependencies: + lru-cache "^6.0.0" + +source-map-js@^0.6.2: + version "0.6.2" + resolved "https://registry.yarnpkg.com/source-map-js/-/source-map-js-0.6.2.tgz#0bb5de631b41cfbda6cfba8bd05a80efdfd2385e" + integrity sha512-/3GptzWzu0+0MBQFrDKzw/DvvMTUORvgY6k6jd/VS6iCR4RDTKWH6v6WPwQoUO8667uQEf9Oe38DxAYWY5F/Ug== + +source-map@^0.6.1: + version "0.6.1" + resolved "https://registry.yarnpkg.com/source-map/-/source-map-0.6.1.tgz#74722af32e9614e9c287a8d0bbde48b5e2f1a263" + integrity sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g== + +sourcemap-codec@^1.4.4: + version "1.4.8" + resolved "https://registry.yarnpkg.com/sourcemap-codec/-/sourcemap-codec-1.4.8.tgz#ea804bd94857402e6992d05a38ef1ae35a9ab4c4" + integrity sha512-9NykojV5Uih4lgo5So5dtw+f0JgJX30KCNI8gwhz2J9A15wD0Ml6tjHKwf6fTSa6fAdVBdZeNOs9eJ71qCk8vA== + +to-fast-properties@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/to-fast-properties/-/to-fast-properties-2.0.0.tgz#dc5e698cbd079265bc73e0377681a4e4e83f616e" + integrity sha1-3F5pjL0HkmW8c+A3doGk5Og/YW4= + +token-stream@1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/token-stream/-/token-stream-1.0.0.tgz#cc200eab2613f4166d27ff9afc7ca56d49df6eb4" + integrity sha1-zCAOqyYT9BZtJ/+a/HylbUnfbrQ= + +typescript@^4.4.3: + version "4.4.3" + resolved "https://registry.yarnpkg.com/typescript/-/typescript-4.4.3.tgz#bdc5407caa2b109efd4f82fe130656f977a29324" + integrity sha512-4xfscpisVgqqDfPaJo5vkd+Qd/ItkoagnHpufr+i2QCHBsNYp+G7UAoyFl8aPtx879u38wPV65rZ8qbGZijalA== + +upath@^2.0.1: + version "2.0.1" + resolved "https://registry.yarnpkg.com/upath/-/upath-2.0.1.tgz#50c73dea68d6f6b990f51d279ce6081665d61a8b" + integrity sha512-1uEe95xksV1O0CYKXo8vQvN1JEbtJp7lb7C5U9HMsIp6IVwntkH/oNUzyVNQSd4S1sYk2FpSSW44FqMc8qee5w== + +vite@^2.6.4: + version "2.6.4" + resolved "https://registry.yarnpkg.com/vite/-/vite-2.6.4.tgz#b932735f1ecdf69256ce6eaae0d41a7b15d77599" + integrity sha512-zNGZgjKGprdLKJ1g1taAvNt51JbGAdrAUU9hpLzgtlks+cXBxTZUsEAGEtLbF3UvlYOVAPXS8r9E9gxYAv6z+A== + dependencies: + esbuild "^0.13.2" + postcss "^8.3.8" + resolve "^1.20.0" + rollup "^2.57.0" + optionalDependencies: + fsevents "~2.3.2" + +void-elements@^3.1.0: + version "3.1.0" + resolved "https://registry.yarnpkg.com/void-elements/-/void-elements-3.1.0.tgz#614f7fbf8d801f0bb5f0661f5b2f5785750e4f09" + integrity sha1-YU9/v42AHwu18GYfWy9XhXUOTwk= + +vscode-css-languageservice@^5.1.4: + version "5.1.7" + resolved "https://registry.yarnpkg.com/vscode-css-languageservice/-/vscode-css-languageservice-5.1.7.tgz#ebbf803b03fde539bd683588cd2da6c4a61b90e2" + integrity sha512-h4oafcZaGFe2VtbNIlkZDmLEP0GQha3E5Ct2YMH4p/p9xYC8yWDNQ5CD+VF3UnSijKPSKmA+oc4cKjhJBowGKw== + dependencies: + vscode-languageserver-textdocument "^1.0.1" + vscode-languageserver-types "^3.16.0" + vscode-nls "^5.0.0" + vscode-uri "^3.0.2" + +vscode-html-languageservice@^4.0.7: + version "4.1.0" + resolved "https://registry.yarnpkg.com/vscode-html-languageservice/-/vscode-html-languageservice-4.1.0.tgz#e1662f1ff3b623b910c54d3f4a2aad8f397e53d7" + integrity sha512-QQrEKfpfbeglD8Jcai4fQDQ7vOJrN6LyiOs47Y6qAxnhve+ervw1kP2UCt9ohHe/6teNWJDYTGxLDgs5iAvitw== + dependencies: + vscode-languageserver-textdocument "^1.0.1" + vscode-languageserver-types "^3.16.0" + vscode-nls "^5.0.0" + vscode-uri "^3.0.2" + +vscode-json-languageservice@^4.1.7: + version "4.1.8" + resolved "https://registry.yarnpkg.com/vscode-json-languageservice/-/vscode-json-languageservice-4.1.8.tgz#397a39238d496e3e08a544a8b93df2cd13347d0c" + integrity sha512-0vSpg6Xd9hfV+eZAaYN63xVVMOTmJ4GgHxXnkLCh+9RsQBkWKIghzLhW2B9ebfG+LQQg8uLtsQ2aUKjTgE+QOg== + dependencies: + jsonc-parser "^3.0.0" + vscode-languageserver-textdocument "^1.0.1" + vscode-languageserver-types "^3.16.0" + vscode-nls "^5.0.0" + vscode-uri "^3.0.2" + +vscode-jsonrpc@8.0.0-next.2, vscode-jsonrpc@^8.0.0-next.2: + version "8.0.0-next.2" + resolved "https://registry.yarnpkg.com/vscode-jsonrpc/-/vscode-jsonrpc-8.0.0-next.2.tgz#285fc294be586e4768acd67e5a42efc738a5cac0" + integrity sha512-gxUyTBAjmwGkiHW/UaRScre2s4i98P8M7gnc3VB4DrVQUm3vQ0idi2cN9nbkfcjATx+uEt8C22j+MLN/8UzsJA== + +vscode-languageserver-protocol@3.17.0-next.8: + version "3.17.0-next.8" + resolved "https://registry.yarnpkg.com/vscode-languageserver-protocol/-/vscode-languageserver-protocol-3.17.0-next.8.tgz#ef2eb7423b474cccd11384239de24488e7fe818c" + integrity sha512-P89vSuJ+FA5JzFmcOoZN13Ig1yd6LsiPOig0O5m5BSGuO/rplQegCd9J0wKpaTy7trf/SYHRoypnbUBdzy14sg== + dependencies: + vscode-jsonrpc "8.0.0-next.2" + vscode-languageserver-types "3.17.0-next.3" + +vscode-languageserver-textdocument@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/vscode-languageserver-textdocument/-/vscode-languageserver-textdocument-1.0.1.tgz#178168e87efad6171b372add1dea34f53e5d330f" + integrity sha512-UIcJDjX7IFkck7cSkNNyzIz5FyvpQfY7sdzVy+wkKN/BLaD4DQ0ppXQrKePomCxTS7RrolK1I0pey0bG9eh8dA== + +vscode-languageserver-types@3.17.0-next.3: + version "3.17.0-next.3" + resolved "https://registry.yarnpkg.com/vscode-languageserver-types/-/vscode-languageserver-types-3.17.0-next.3.tgz#e1f4311e08ea3193e81126154b6a342fc1c3dba3" + integrity sha512-VQcXnhKYxUW6OiRMhG++SzmZYMJwusXknJGd+FfdOnS1yHAo734OHyR0e2eEHDlv0/oWc8RZPgx/VKSKyondVg== + +vscode-languageserver-types@^3.15.1, vscode-languageserver-types@^3.16.0: + version "3.16.0" + resolved "https://registry.yarnpkg.com/vscode-languageserver-types/-/vscode-languageserver-types-3.16.0.tgz#ecf393fc121ec6974b2da3efb3155644c514e247" + integrity sha512-k8luDIWJWyenLc5ToFQQMaSrqCHiLwyKPHKPQZ5zz21vM+vIVUSvsRpcbiECH4WR88K2XZqc4ScRcZ7nk/jbeA== + +vscode-languageserver@^8.0.0-next.2: + version "8.0.0-next.2" + resolved "https://registry.yarnpkg.com/vscode-languageserver/-/vscode-languageserver-8.0.0-next.2.tgz#3a3daf79ff10350ea9cec5c73b5302901a955117" + integrity sha512-7qCEXTeGZKkI8BGvlKh0JPXTY7BaWoiwQYKCcGaUgnMs34wt6F/yaKcxoC3XIouBBVyRxiI6Ml/JdztM3XYEaA== + dependencies: + vscode-languageserver-protocol "3.17.0-next.8" + +vscode-nls@^5.0.0: + version "5.0.0" + resolved "https://registry.yarnpkg.com/vscode-nls/-/vscode-nls-5.0.0.tgz#99f0da0bd9ea7cda44e565a74c54b1f2bc257840" + integrity sha512-u0Lw+IYlgbEJFF6/qAqG2d1jQmJl0eyAGJHoAJqr2HT4M2BNuQYSEiSE75f52pXHSJm8AlTjnLLbBFPrdz2hpA== + +vscode-pug-languageservice@^0.27.24: + version "0.27.24" + resolved "https://registry.yarnpkg.com/vscode-pug-languageservice/-/vscode-pug-languageservice-0.27.24.tgz#fa805c4d3e33dee3681e660a0767136738e68370" + integrity sha512-GSvsFB+rPhAD7cBlEKCVNNsFGIaOnp/0zyLw3WpYbXY24vJZafXu1kHvtYaaQXJRnIhqp5EI5p+EqpdI3hTBnw== + dependencies: + "@volar/code-gen" "^0.27.24" + "@volar/shared" "^0.27.24" + "@volar/source-map" "^0.27.24" + "@volar/transforms" "^0.27.24" + pug-lexer "^5.0.1" + pug-parser "^6.0.0" + vscode-languageserver "^8.0.0-next.2" + +vscode-typescript-languageservice@^0.27.25: + version "0.27.25" + resolved "https://registry.yarnpkg.com/vscode-typescript-languageservice/-/vscode-typescript-languageservice-0.27.25.tgz#acd211723b600108c25515388b75d55ce15bb056" + integrity sha512-nxpJI9MnF2rn5rKL/032Qrsq3T9DgM3slK5fwZp3suNdo90JG2zFTs3Ola8n62k7+KWu4A775obxyb4wLIW6Gw== + dependencies: + "@volar/shared" "^0.27.24" + semver "^7.3.5" + upath "^2.0.1" + vscode-languageserver "^8.0.0-next.2" + vscode-languageserver-textdocument "^1.0.1" + +vscode-uri@^2.1.2: + version "2.1.2" + resolved "https://registry.yarnpkg.com/vscode-uri/-/vscode-uri-2.1.2.tgz#c8d40de93eb57af31f3c715dd650e2ca2c096f1c" + integrity sha512-8TEXQxlldWAuIODdukIb+TR5s+9Ds40eSJrw+1iDDA9IFORPjMELarNQE3myz5XIkWWpdprmJjm1/SxMlWOC8A== + +vscode-uri@^3.0.2: + version "3.0.2" + resolved "https://registry.yarnpkg.com/vscode-uri/-/vscode-uri-3.0.2.tgz#ecfd1d066cb8ef4c3a208decdbab9a8c23d055d0" + integrity sha512-jkjy6pjU1fxUvI51P+gCsxg1u2n8LSt0W6KrCNQceaziKzff74GoWmjVG46KieVzybO1sttPQmYfrwSHey7GUA== + +vscode-vue-languageservice@^0.27.0: + version "0.27.30" + resolved "https://registry.yarnpkg.com/vscode-vue-languageservice/-/vscode-vue-languageservice-0.27.30.tgz#1f32b0203dd233582f74a457428519a6318f039e" + integrity sha512-nPnUNCMqqHfxcCPLyLWvmgbNCgos3SwvPcl/CzAnMbqcjLtNZppsdI7bKX3EEj0Jbg6SGLQ9NanIvZaMI1bsUA== + dependencies: + "@volar/code-gen" "^0.27.24" + "@volar/html2pug" "^0.27.13" + "@volar/shared" "^0.27.24" + "@volar/source-map" "^0.27.24" + "@volar/transforms" "^0.27.24" + "@vscode/emmet-helper" "^2.7.0" + "@vue/compiler-dom" "^3.2.19" + "@vue/reactivity" "^3.2.19" + "@vue/shared" "^3.2.19" + request-light "^0.5.4" + upath "^2.0.1" + vscode-css-languageservice "^5.1.4" + vscode-html-languageservice "^4.0.7" + vscode-json-languageservice "^4.1.7" + vscode-languageserver "^8.0.0-next.2" + vscode-languageserver-textdocument "^1.0.1" + vscode-pug-languageservice "^0.27.24" + vscode-typescript-languageservice "^0.27.25" + +vue-tsc@^0.3.0: + version "0.3.0" + resolved "https://registry.yarnpkg.com/vue-tsc/-/vue-tsc-0.3.0.tgz#3b3872bf4f1d2e4409b57adbd826032e253db406" + integrity sha512-zaDRZBxwRIz1XjhNP92FqugG71st6BUMnA2EwPeXrAyzbEYVRz6TezNFceYl3QYqqN8CtaxbqUhaQEDj/ntoCA== + dependencies: + vscode-vue-languageservice "^0.27.0" + +vue@^3.2.16: + version "3.2.19" + resolved "https://registry.yarnpkg.com/vue/-/vue-3.2.19.tgz#da2c80a6a0271c7097fee9e31692adfd9d569c8f" + integrity sha512-6KAMdIfAtlK+qohTIUE4urwAv4A3YRuo8uAbByApUmiB0CziGAAPs6qVugN6oHPia8YIafHB/37K0O6KZ7sGmA== + dependencies: + "@vue/compiler-dom" "3.2.19" + "@vue/compiler-sfc" "3.2.19" + "@vue/runtime-dom" "3.2.19" + "@vue/server-renderer" "3.2.19" + "@vue/shared" "3.2.19" + +with@^7.0.0: + version "7.0.2" + resolved "https://registry.yarnpkg.com/with/-/with-7.0.2.tgz#ccee3ad542d25538a7a7a80aad212b9828495bac" + integrity sha512-RNGKj82nUPg3g5ygxkQl0R937xLyho1J24ItRCBTr/m1YnZkzJy1hUiHUJrc/VlsDQzsCnInEGSg3bci0Lmd4w== + dependencies: + "@babel/parser" "^7.9.6" + "@babel/types" "^7.9.6" + assert-never "^1.2.1" + babel-walk "3.0.0-canary-5" + +yallist@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/yallist/-/yallist-4.0.0.tgz#9bb92790d9c0effec63be73519e11a35019a3a72" + integrity sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A== From 061a9d9bc68161dd38026cfddf31fd27a4b19ac4 Mon Sep 17 00:00:00 2001 From: Jef Roosens Date: Sat, 9 Oct 2021 22:01:52 +0200 Subject: [PATCH 57/67] Some more stuff --- migrations/2021-09-13-143540_sections/up.sql | 2 + web2/package.json | 4 +- web2/src/components/HelloWorld.vue | 13 + web2/src/main.ts | 5 + web2/src/server.d.ts | 1 + web2/src/server.js | 27 + web2/yarn-error.log | 1121 ++++++++++++++++++ web2/yarn.lock | 180 +++ 8 files changed, 1352 insertions(+), 1 deletion(-) create mode 100644 web2/src/server.d.ts create mode 100644 web2/src/server.js create mode 100644 web2/yarn-error.log diff --git a/migrations/2021-09-13-143540_sections/up.sql b/migrations/2021-09-13-143540_sections/up.sql index 0c5ca76..64b80cb 100644 --- a/migrations/2021-09-13-143540_sections/up.sql +++ b/migrations/2021-09-13-143540_sections/up.sql @@ -4,6 +4,8 @@ create table sections ( -- Title of the section title varchar(255) UNIQUE NOT NULL, + -- Name to use when routing (this just makes for prettier URLs) + shortname varchar(32) UNIQUE NOT NULL, -- Optional description of the section description text, -- Wether to show the section in the default list on the homepage diff --git a/web2/package.json b/web2/package.json index 4a7220b..64c0d5b 100644 --- a/web2/package.json +++ b/web2/package.json @@ -10,9 +10,11 @@ "vue": "^3.2.16" }, "devDependencies": { + "@types/node": "^16.10.3", "@vitejs/plugin-vue": "^1.9.3", + "miragejs": "^0.1.42", "typescript": "^4.4.3", "vite": "^2.6.4", "vue-tsc": "^0.3.0" } -} \ No newline at end of file +} diff --git a/web2/src/components/HelloWorld.vue b/web2/src/components/HelloWorld.vue index 2d61249..2af73ad 100644 --- a/web2/src/components/HelloWorld.vue +++ b/web2/src/components/HelloWorld.vue @@ -4,11 +4,24 @@ import { ref } from 'vue' defineProps<{ msg: string }>() const count = ref(0) +let test = ref("yeet") + +fetch("/api/users").then( + res => { + if (!res.ok) return Promise.reject() + + return res.json() + } +).then( + json => test.value = json +)