diff --git a/Cargo.lock b/Cargo.lock index 843201a..e33c215 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -68,6 +68,17 @@ version = "1.0.60" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c794e162a5eff65c72ef524dfe393eb923c354e350bb78b9c7383df13f3bc142" +[[package]] +name = "argon2" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "db4ce4441f99dbd377ca8a8f57b698c44d0d6e712d8329b5040da5a64aa1ce73" +dependencies = [ + "base64ct", + "blake2", + "password-hash", +] + [[package]] name = "ascii_utils" version = "0.9.3" @@ -256,12 +267,27 @@ version = "0.13.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "904dfeac50f3cdaba28fc6f57fdcddb75f49ed61346676a78c4ffe55877802fd" +[[package]] +name = "base64ct" +version = "1.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ea2b2456fd614d856680dcd9fcc660a51a820fa09daef2e49772b56a193c8474" + [[package]] name = "bitflags" version = "1.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" +[[package]] +name = "blake2" +version = "0.10.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b9cf849ee05b2ee5fba5e36f97ff8ec2533916700fc0758d40d92136a42f3388" +dependencies = [ + "digest", +] + [[package]] name = "block-buffer" version = "0.10.2" @@ -330,9 +356,12 @@ name = "como_bin" version = "0.1.0" dependencies = [ "anyhow", + "argon2", "async-graphql", "axum", + "cookie", "dotenv", + "rand_core", "sqlx", "tokio", "tower-http", @@ -341,6 +370,16 @@ dependencies = [ "uuid", ] +[[package]] +name = "cookie" +version = "0.16.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "344adc371239ef32293cb1c4fe519592fcf21206c79c02854320afcdf3ab4917" +dependencies = [ + "time", + "version_check", +] + [[package]] name = "cpufeatures" version = "0.2.2" @@ -490,6 +529,9 @@ name = "either" version = "1.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3f107b87b6afc2a64fd13cac55fe06d6c8859f12d4b14cbcdd2c67d0976781be" +dependencies = [ + "serde", +] [[package]] name = "encoding_rs" @@ -982,6 +1024,15 @@ dependencies = [ "libc", ] +[[package]] +name = "num_threads" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2819ce041d2ee131036f4fc9d6ae7ae125a3a40e97ba64d04fe799ad9dabbb44" +dependencies = [ + "libc", +] + [[package]] name = "once_cell" version = "1.13.0" @@ -1036,6 +1087,17 @@ dependencies = [ "windows-sys", ] +[[package]] +name = "password-hash" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7676374caaee8a325c9e7a2ae557f216c5563a171d6997b0ef8a65af35147700" +dependencies = [ + "base64ct", + "rand_core", + "subtle", +] + [[package]] name = "paste" version = "1.0.8" @@ -1458,6 +1520,7 @@ dependencies = [ "thiserror", "tokio-stream", "url", + "uuid", "webpki-roots", "whoami", ] @@ -1471,9 +1534,12 @@ dependencies = [ "dotenvy", "either", "heck", + "hex", "once_cell", "proc-macro2", "quote", + "serde", + "serde_json", "sha2", "sqlx-core", "sqlx-rt", @@ -1580,6 +1646,24 @@ dependencies = [ "once_cell", ] +[[package]] +name = "time" +version = "0.3.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3c3f9a28b618c3a6b9251b6908e9c99e04b9e5c02e6581ccbb67d59c34ef7f9b" +dependencies = [ + "itoa", + "libc", + "num_threads", + "time-macros", +] + +[[package]] +name = "time-macros" +version = "0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "42657b1a6f4d817cda8e7a0ace261fe0cc946cf3a80314390b22cc61ae080792" + [[package]] name = "tinyvec" version = "1.6.0" diff --git a/como_bin/Cargo.toml b/como_bin/Cargo.toml index c0888dc..6cbb2d4 100644 --- a/como_bin/Cargo.toml +++ b/como_bin/Cargo.toml @@ -8,11 +8,20 @@ edition = "2021" [dependencies] async-graphql = "4.0.6" axum = "0.5.13" -tokio = {version="1.20.1", features=["full"]} -uuid = {version="1.1.2", features=["v4", "fast-rng"]} -sqlx = { version = "0.6", features = [ "runtime-tokio-rustls", "postgres", "migrate"] } +tokio = { version = "1.20.1", features = ["full"] } +uuid = { version = "1.1.2", features = ["v4", "fast-rng"] } +sqlx = { version = "0.6", features = [ + "runtime-tokio-rustls", + "postgres", + "migrate", + "uuid", + "offline", +] } anyhow = "1.0.60" dotenv = "0.15.0" tracing = "0.1.36" tracing-subscriber = { version = "0.3.15", features = ["env-filter"] } tower-http = { version = "0.3.4", features = ["full"] } +argon2 = "0.4" +rand_core = { version = "0.6", features = ["std"] } +cookie = "0.16" diff --git a/como_bin/db/migrations/20220808220223_initial_migration.sql b/como_bin/db/migrations/20220808220223_initial_migration.sql index a51b447..73fa09f 100644 --- a/como_bin/db/migrations/20220808220223_initial_migration.sql +++ b/como_bin/db/migrations/20220808220223_initial_migration.sql @@ -1,4 +1,8 @@ -- Add migration script here -CREATE TABLE IF NOT EXISTS events ( - id BIGSERIAL PRIMARY KEY +CREATE TABLE IF NOT EXISTS users ( + id UUID PRIMARY KEY DEFAULT gen_random_uuid(), + username varchar not null, + password_hash varchar not null ); + +CREATE unique index users_username_idx on users(username) diff --git a/como_bin/db/migrations/20220808233152_something.sql b/como_bin/db/migrations/20220808233152_something.sql deleted file mode 100644 index 8ddc1d3..0000000 --- a/como_bin/db/migrations/20220808233152_something.sql +++ /dev/null @@ -1 +0,0 @@ --- Add migration script here diff --git a/como_bin/sqlx-data.json b/como_bin/sqlx-data.json new file mode 100644 index 0000000..e4e4275 --- /dev/null +++ b/como_bin/sqlx-data.json @@ -0,0 +1,56 @@ +{ + "db": "PostgreSQL", + "3b4484c5ccfd4dcb887c4e978fe6e45d4c9ecc2a73909be207dced79ddf17d87": { + "describe": { + "columns": [ + { + "name": "id", + "ordinal": 0, + "type_info": "Uuid" + } + ], + "nullable": [ + false + ], + "parameters": { + "Left": [ + "Varchar", + "Varchar" + ] + } + }, + "query": "\n INSERT INTO users (username, password_hash) \n VALUES ( $1, $2 ) \n RETURNING id\n " + }, + "d3f222cf6c3d9816705426fdbed3b13cb575bb432eb1f33676c0b414e67aecaf": { + "describe": { + "columns": [ + { + "name": "id", + "ordinal": 0, + "type_info": "Uuid" + }, + { + "name": "username", + "ordinal": 1, + "type_info": "Varchar" + }, + { + "name": "password_hash", + "ordinal": 2, + "type_info": "Varchar" + } + ], + "nullable": [ + false, + false, + false + ], + "parameters": { + "Left": [ + "Text" + ] + } + }, + "query": "\n SELECT * from users\n where username=$1\n " + } +} \ No newline at end of file diff --git a/como_bin/src/gqlx/mod.rs b/como_bin/src/gqlx/mod.rs new file mode 100644 index 0000000..995a558 --- /dev/null +++ b/como_bin/src/gqlx/mod.rs @@ -0,0 +1,2 @@ +pub mod users; + diff --git a/como_bin/src/gqlx/users.rs b/como_bin/src/gqlx/users.rs new file mode 100644 index 0000000..e69de29 diff --git a/como_bin/src/graphql.rs b/como_bin/src/graphql.rs index 1bfe684..3eab7c4 100644 --- a/como_bin/src/graphql.rs +++ b/como_bin/src/graphql.rs @@ -1,13 +1,49 @@ -use async_graphql::{Context, EmptyMutation, EmptySubscription, Object, Schema, SimpleObject}; +use async_graphql::{Context, EmptySubscription, Object, Schema, SimpleObject}; use uuid::Uuid; -pub type CibusSchema = Schema; +use crate::services::users_service::UserService; + +pub type CibusSchema = Schema; + +pub struct MutationRoot; + +#[Object] +impl MutationRoot { + async fn login( + &self, + ctx: &Context<'_>, + username: String, + password: String, + ) -> anyhow::Result { + let user_service = ctx.data_unchecked::(); + + let valid = user_service.validate_user(username, password).await?; + + Ok(match valid { + Some(..) => true, + None => false, + }) + } + + async fn register( + &self, + ctx: &Context<'_>, + username: String, + password: String, + ) -> anyhow::Result { + let user_service = ctx.data_unchecked::(); + + let user_id = user_service.add_user(username, password).await?; + + Ok(user_id.into()) + } +} pub struct QueryRoot; #[Object] impl QueryRoot { - async fn get_upcoming(&self, ctx: &Context<'_>) -> Vec { + async fn get_upcoming(&self, _ctx: &Context<'_>) -> Vec { vec![Event::new( None, "Some-name".into(), diff --git a/como_bin/src/main.rs b/como_bin/src/main.rs index de977cb..1a59b46 100644 --- a/como_bin/src/main.rs +++ b/como_bin/src/main.rs @@ -1,6 +1,8 @@ use std::env::{self, current_dir}; +mod gqlx; mod graphql; +mod services; use axum::{ extract::Extension, @@ -12,14 +14,15 @@ use axum::{ use async_graphql::{ http::{playground_source, GraphQLPlaygroundConfig}, - EmptyMutation, EmptySubscription, Request, Response, Schema, + EmptySubscription, Request, Response, Schema, }; use graphql::CibusSchema; +use services::users_service; use sqlx::PgPool; use tower_http::{cors::CorsLayer, trace::TraceLayer}; use tracing_subscriber::{layer::SubscriberExt, util::SubscriberInitExt}; -use crate::graphql::QueryRoot; +use crate::graphql::{MutationRoot, QueryRoot}; async fn graphql_handler(schema: Extension, req: Json) -> Json { schema.execute(req.0).await.into() @@ -39,7 +42,8 @@ async fn main() -> anyhow::Result<()> { tracing_subscriber::registry() .with(tracing_subscriber::EnvFilter::new( std::env::var("RUST_LOG").unwrap_or_else(|_| { - "como_bin=debug,tower_http=debug,axum_extra=debug,hyper=info,mio=info".into() + "como_bin=debug,tower_http=debug,axum_extra=debug,hyper=info,mio=info,sqlx=info,async_graphql=debug" + .into() }), )) .with(tracing_subscriber::fmt::layer()) @@ -58,7 +62,9 @@ async fn main() -> anyhow::Result<()> { // Schema println!("Building schema"); - let schema = Schema::build(QueryRoot, EmptyMutation, EmptySubscription).finish(); + let schema = Schema::build(QueryRoot, MutationRoot, EmptySubscription) + .data(users_service::UserService::new(pool)) + .finish(); // CORS let cors = vec!["http://localhost:3000".parse().unwrap()]; diff --git a/como_bin/src/services/cookie_service.rs b/como_bin/src/services/cookie_service.rs new file mode 100644 index 0000000..c0d64c6 --- /dev/null +++ b/como_bin/src/services/cookie_service.rs @@ -0,0 +1 @@ +pub struct CookieService {} diff --git a/como_bin/src/services/mod.rs b/como_bin/src/services/mod.rs new file mode 100644 index 0000000..793e34e --- /dev/null +++ b/como_bin/src/services/mod.rs @@ -0,0 +1,2 @@ +pub mod cookie_service; +pub mod users_service; diff --git a/como_bin/src/services/users_service.rs b/como_bin/src/services/users_service.rs new file mode 100644 index 0000000..059d8e3 --- /dev/null +++ b/como_bin/src/services/users_service.rs @@ -0,0 +1,80 @@ +use std::sync::Arc; + +use anyhow::anyhow; +use argon2::{password_hash::SaltString, Argon2, PasswordHash, PasswordHasher, PasswordVerifier}; +use rand_core::OsRng; +use sqlx::{Pool, Postgres}; + +pub struct UserService { + pgx: Pool, +} + +impl UserService { + pub fn new(pgx: Pool) -> Self { + Self { pgx } + } + + pub async fn add_user(&self, username: String, password: String) -> anyhow::Result { + let hashed_password = self.hash_password(password)?; + + let rec = sqlx::query!( + r#" + INSERT INTO users (username, password_hash) + VALUES ( $1, $2 ) + RETURNING id + "#, + username, + hashed_password + ) + .fetch_one(&self.pgx) + .await?; + + Ok(rec.id.to_string()) + } + + pub async fn validate_user( + &self, + username: String, + password: String, + ) -> anyhow::Result> { + let rec = sqlx::query!( + r#" + SELECT * from users + where username=$1 + "#, + username, + ) + .fetch_optional(&self.pgx) + .await?; + + match rec { + Some(user) => match self.validate_password(password, user.password_hash)? { + true => Ok(Some(())), + false => Ok(None), + }, + None => Ok(None), + } + } + + fn hash_password(&self, password: String) -> anyhow::Result { + let salt = SaltString::generate(&mut OsRng); + let argon2 = Argon2::default(); + + let password_hash = argon2 + .hash_password(password.as_bytes(), &salt) + .map_err(|e| anyhow!(e))? + .to_string(); + + Ok(password_hash) + } + + fn validate_password(&self, password: String, hashed_password: String) -> anyhow::Result { + let argon2 = Argon2::default(); + + let parsed_hash = PasswordHash::new(&hashed_password).map_err(|e| anyhow!(e))?; + match argon2.verify_password(password.as_bytes(), &parsed_hash) { + Ok(..) => Ok(true), + Err(..) => Ok(false), + } + } +} diff --git a/scripts/local_up.sh b/scripts/local_up.sh index ee0e79c..f1038a0 100755 --- a/scripts/local_up.sh +++ b/scripts/local_up.sh @@ -2,6 +2,6 @@ set -e -cuddle_cli render_templates --template-file $TMP/docker-compose.local_up.yml.tmpl --dest $TMP/docker-compose.local_up.yml +cuddle_cli render_template --template-file $TMP/docker-compose.local_up.yml.tmpl --dest $TMP/docker-compose.local_up.yml docker compose -f $TMP/docker-compose.local_up.yml up -d --remove-orphans