From ae74f66c3a7fc4b3d1da70bea611dd36942b72aa Mon Sep 17 00:00:00 2001 From: kjuulh Date: Tue, 4 Oct 2022 11:06:48 +0200 Subject: [PATCH] Added apis --- Cargo.lock | 64 +++++++- Cargo.toml | 9 +- como_api/Cargo.toml | 39 +++++ como_api/src/controllers/graphql.rs | 11 ++ como_api/src/controllers/mod.rs | 1 + como_api/src/lib.rs | 2 + como_api/src/router.rs | 44 ++++++ como_bin/Cargo.toml | 3 + como_bin/build.rs | 6 - como_bin/sqlx-data.json | 56 ------- como_bin/src/gqlx/mod.rs | 2 - como_bin/src/gqlx/users.rs | 0 como_bin/src/main.rs | 149 ++---------------- como_bin/src/services/mod.rs | 1 - como_bin/src/services/users_service.rs | 78 --------- como_core/Cargo.toml | 1 + como_core/src/items/mod.rs | 5 +- como_core/src/users/mod.rs | 9 +- como_domain/Cargo.toml | 1 + como_domain/src/item/mod.rs | 5 +- como_domain/src/item/requests.rs | 3 +- como_gql/Cargo.toml | 36 +++++ {como_bin => como_gql}/src/graphql.rs | 32 ++-- como_gql/src/lib.rs | 42 +++++ como_infrastructure/src/register.rs | 4 +- .../src/services/item_service.rs | 9 +- .../src/services/user_service.rs | 80 +++++++++- ...fe3ad16b72230967de01f640b7e4729b49fce.json | 0 28 files changed, 392 insertions(+), 300 deletions(-) create mode 100644 como_api/Cargo.toml create mode 100644 como_api/src/controllers/graphql.rs create mode 100644 como_api/src/controllers/mod.rs create mode 100644 como_api/src/lib.rs create mode 100644 como_api/src/router.rs delete mode 100644 como_bin/build.rs delete mode 100644 como_bin/sqlx-data.json delete mode 100644 como_bin/src/gqlx/mod.rs delete mode 100644 como_bin/src/gqlx/users.rs delete mode 100644 como_bin/src/services/mod.rs delete mode 100644 como_bin/src/services/users_service.rs create mode 100644 como_gql/Cargo.toml rename {como_bin => como_gql}/src/graphql.rs (54%) create mode 100644 como_gql/src/lib.rs rename {como_bin => como_infrastructure}/target/sqlx/query-4e07408562bedb8b60ce05c1decfe3ad16b72230967de01f640b7e4729b49fce.json (100%) diff --git a/Cargo.lock b/Cargo.lock index 0d43380..427de5d 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -189,6 +189,7 @@ dependencies = [ "static_assertions", "tempfile", "thiserror", + "uuid", ] [[package]] @@ -640,7 +641,7 @@ dependencies = [ ] [[package]] -name = "como_bin" +name = "como_api" version = "0.1.0" dependencies = [ "anyhow", @@ -652,6 +653,38 @@ dependencies = [ "axum-sessions", "como_core", "como_domain", + "como_gql", + "como_infrastructure", + "cookie", + "dotenv", + "rand_core", + "serde", + "serde_json", + "sqlx 0.6.1", + "tokio", + "tower", + "tower-http", + "tracing", + "tracing-subscriber", + "uuid", +] + +[[package]] +name = "como_bin" +version = "0.1.0" +dependencies = [ + "anyhow", + "argon2", + "async-graphql", + "async-graphql-axum", + "axum", + "axum-extra", + "axum-sessions", + "clap", + "como_api", + "como_core", + "como_domain", + "como_gql", "como_infrastructure", "cookie", "dotenv", @@ -674,6 +707,7 @@ dependencies = [ "async-trait", "axum", "clap", + "como_domain", "dotenv", "mockall", "rust-argon2", @@ -693,11 +727,39 @@ name = "como_domain" version = "0.1.0" dependencies = [ "anyhow", + "async-graphql", "serde", "serde_json", "uuid", ] +[[package]] +name = "como_gql" +version = "0.1.0" +dependencies = [ + "anyhow", + "argon2", + "async-graphql", + "async-graphql-axum", + "axum", + "axum-extra", + "axum-sessions", + "como_core", + "como_domain", + "como_infrastructure", + "cookie", + "dotenv", + "rand_core", + "serde", + "serde_json", + "sqlx 0.6.1", + "tokio", + "tower-http", + "tracing", + "tracing-subscriber", + "uuid", +] + [[package]] name = "como_infrastructure" version = "0.1.0" diff --git a/Cargo.toml b/Cargo.toml index 64a846e..79f2391 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,2 +1,9 @@ [workspace] -members = ["como_bin", "como_core", "como_domain", "como_infrastructure"] +members = [ + "como_bin", + "como_core", + "como_domain", + "como_infrastructure", + "como_gql", + "como_api", +] diff --git a/como_api/Cargo.toml b/como_api/Cargo.toml new file mode 100644 index 0000000..78fde40 --- /dev/null +++ b/como_api/Cargo.toml @@ -0,0 +1,39 @@ +[package] +name = "como_api" +version = "0.1.0" +edition = "2021" + +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html + +[dependencies] +como_gql = { path = "../como_gql" } +como_core = { path = "../como_core" } +como_domain = { path = "../como_domain" } +como_infrastructure = { path = "../como_infrastructure" } + +async-graphql = "4.0.6" +async-graphql-axum = "*" +axum = "0.5.13" +axum-extra = { version = "*", features = ["cookie", "cookie-private"] } +axum-sessions = { version = "*" } +serde = { version = "1.0", features = ["derive"] } +serde_json = "1.0.68" + +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"] } +argon2 = "0.4" +rand_core = { version = "0.6", features = ["std"] } +cookie = { version = "0.16", features = ["secure", "percent-encode"] } +tower = { version = "0.4", features = ["timeout"] } +tower-http = { version = "0.3", features = ["trace", "cors"] } diff --git a/como_api/src/controllers/graphql.rs b/como_api/src/controllers/graphql.rs new file mode 100644 index 0000000..4893f67 --- /dev/null +++ b/como_api/src/controllers/graphql.rs @@ -0,0 +1,11 @@ +use axum::{routing::get, Router}; +use como_gql::{graphql_handler, graphql_playground}; +use como_infrastructure::register::ServiceRegister; + +pub struct GraphQLController; + +impl GraphQLController { + pub fn new_router(_service_register: ServiceRegister) -> Router { + Router::new().route("/", get(graphql_playground).post(graphql_handler)) + } +} diff --git a/como_api/src/controllers/mod.rs b/como_api/src/controllers/mod.rs new file mode 100644 index 0000000..50b9335 --- /dev/null +++ b/como_api/src/controllers/mod.rs @@ -0,0 +1 @@ +pub mod graphql; diff --git a/como_api/src/lib.rs b/como_api/src/lib.rs new file mode 100644 index 0000000..3037b9c --- /dev/null +++ b/como_api/src/lib.rs @@ -0,0 +1,2 @@ +mod controllers; +pub mod router; diff --git a/como_api/src/router.rs b/como_api/src/router.rs new file mode 100644 index 0000000..89d4574 --- /dev/null +++ b/como_api/src/router.rs @@ -0,0 +1,44 @@ +use anyhow::Context; +use axum::{ + http::{HeaderValue, Method}, + Router, +}; +use como_infrastructure::register::ServiceRegister; +use tower::ServiceBuilder; +use tower_http::{cors::CorsLayer, trace::TraceLayer}; + +use crate::controllers::graphql::GraphQLController; + +pub struct Api; + +impl Api { + pub async fn new( + port: u32, + cors_origin: &str, + service_register: ServiceRegister, + ) -> anyhow::Result<()> { + let router = Router::new() + .nest( + "/graphql", + GraphQLController::new_router(service_register.clone()), + ) + .layer(ServiceBuilder::new().layer(TraceLayer::new_for_http())) + .layer( + CorsLayer::new() + .allow_origin( + cors_origin + .parse::() + .context("could not parse cors origin as header")?, + ) + .allow_headers([axum::http::header::CONTENT_TYPE]) + .allow_methods([Method::GET, Method::POST, Method::OPTIONS]), + ); + + axum::Server::bind(&format!("0.0.0.0:{}", port).parse().unwrap()) + .serve(router.into_make_service()) + .await + .context("error while starting API")?; + + Ok(()) + } +} diff --git a/como_bin/Cargo.toml b/como_bin/Cargo.toml index a3b4516..148e1b8 100644 --- a/como_bin/Cargo.toml +++ b/como_bin/Cargo.toml @@ -6,9 +6,11 @@ edition = "2021" # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html [dependencies] +como_gql = { path = "../como_gql" } como_core = { path = "../como_core" } como_domain = { path = "../como_domain" } como_infrastructure = { path = "../como_infrastructure" } +como_api = { path = "../como_api" } async-graphql = "4.0.6" async-graphql-axum = "*" @@ -34,3 +36,4 @@ tower-http = { version = "0.3.4", features = ["full"] } argon2 = "0.4" rand_core = { version = "0.6", features = ["std"] } cookie = { version = "0.16", features = ["secure", "percent-encode"] } +clap = { version = "3", features = ["derive", "env"] } diff --git a/como_bin/build.rs b/como_bin/build.rs deleted file mode 100644 index 630211f..0000000 --- a/como_bin/build.rs +++ /dev/null @@ -1,6 +0,0 @@ -// generated by `sqlx migrate build-script` -fn main() { - // trigger recompilation when a new migration is added - println!("cargo:rerun-if-changed=migrations"); -} - diff --git a/como_bin/sqlx-data.json b/como_bin/sqlx-data.json deleted file mode 100644 index e4e4275..0000000 --- a/como_bin/sqlx-data.json +++ /dev/null @@ -1,56 +0,0 @@ -{ - "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 deleted file mode 100644 index 995a558..0000000 --- a/como_bin/src/gqlx/mod.rs +++ /dev/null @@ -1,2 +0,0 @@ -pub mod users; - diff --git a/como_bin/src/gqlx/users.rs b/como_bin/src/gqlx/users.rs deleted file mode 100644 index e69de29..0000000 diff --git a/como_bin/src/main.rs b/como_bin/src/main.rs index 71a0f40..d1d7f91 100644 --- a/como_bin/src/main.rs +++ b/como_bin/src/main.rs @@ -1,11 +1,9 @@ -use std::env::{self, current_dir}; +use std::{ + env::{self, current_dir}, + sync::Arc, +}; mod error; -mod gqlx; -mod graphql; -mod services; - -use async_graphql_axum::{GraphQLRequest, GraphQLResponse}; use axum::{ extract::Extension, http::{Method, StatusCode}, @@ -14,57 +12,31 @@ use axum::{ Json, Router, }; use axum_extra::extract::cookie::Key; +use clap::Parser; -use async_graphql::{ - http::{playground_source, GraphQLPlaygroundConfig}, - EmptySubscription, Schema, -}; +use anyhow::Context; use axum_sessions::{ async_session::MemoryStore, extractors::{ReadableSession, WritableSession}, SessionLayer, }; -use error::AppError; -use graphql::CibusSchema; +use como_api::router::Api; +use como_infrastructure::{ + configs::AppConfig, database::ConnectionPoolManager, register::ServiceRegister, +}; use serde::{Deserialize, Serialize}; use serde_json::{json, Value}; -use services::users_service; use sqlx::PgPool; use tower_http::{cors::CorsLayer, trace::TraceLayer}; use tracing_subscriber::{layer::SubscriberExt, util::SubscriberInitExt}; -use crate::graphql::{MutationRoot, QueryRoot}; - -async fn graphql_handler( - schema: Extension, - session: ReadableSession, - req: GraphQLRequest, -) -> Result { - let req = req.into_inner(); - - //if let Some(user_id) = session.get::("userId") { - // req = req.data(user_id); - return Ok(schema.execute(req).await.into()); - //} else if let Some(on) = &req.operation_name { - // if on == "IntrospectionQuery" { - // return Ok(schema.execute(req).await.into()); - // } - //} - - //Err(StatusCode::FORBIDDEN) -} - -async fn graphql_playground() -> impl IntoResponse { - Html(playground_source(GraphQLPlaygroundConfig::new("/"))) -} - #[tokio::main] async fn main() -> anyhow::Result<()> { - // Environment tracing::info!("Loading dotenv"); dotenv::dotenv()?; - // Logging + let config = Arc::new(AppConfig::parse()); + tracing_subscriber::registry() .with(tracing_subscriber::EnvFilter::new( std::env::var("RUST_LOG").unwrap_or_else(|_| { @@ -75,102 +47,13 @@ async fn main() -> anyhow::Result<()> { .with(tracing_subscriber::fmt::layer()) .init(); - // Database - tracing::info!("Creating pool"); - let db_url = env::var("DATABASE_URL")?; - let pool = PgPool::connect(&db_url).await?; + let pool = ConnectionPoolManager::new_pool(&config.database_url, true).await?; - // Database Migrate - tracing::info!("Migrating db"); - sqlx::migrate!("db/migrations").run(&pool).await?; + let service_register = ServiceRegister::new(pool, config.clone()); - tracing::info!("current path: {}", current_dir()?.to_string_lossy()); - - // Schema - println!("Building schema"); - let schema = Schema::build(QueryRoot, MutationRoot, EmptySubscription) - .data(users_service::UserService::new(pool.clone())) - .finish(); - - // CORS - let cors = vec!["http://localhost:3000".parse().unwrap()]; - - // Key - let key = Key::generate(); - - let store = MemoryStore::new(); - let session_layer = SessionLayer::new(store, key.master()); - - // Webserver - tracing::info!("Building router"); - let app = Router::new() - .route("/", get(graphql_playground).post(graphql_handler)) - .route("/auth/login", post(login)) - .route("/auth/register", post(register)) - .layer(TraceLayer::new_for_http()) - .layer(Extension(schema)) - .layer(Extension(key)) - .layer(Extension(pool)) - .layer(session_layer) - .layer( - CorsLayer::new() - .allow_origin(cors) - .allow_headers([axum::http::header::CONTENT_TYPE]) - .allow_methods([Method::GET, Method::POST, Method::OPTIONS]), - ); - - tracing::info!("Starting webserver"); - axum::Server::bind(&"0.0.0.0:3001".parse().unwrap()) - .serve(app.into_make_service()) + Api::new(3001, &config.cors_origin, service_register.clone()) .await - .unwrap(); + .context("could not initialize API")?; Ok(()) } - -#[derive(Serialize, Deserialize)] -pub struct Credentials { - pub username: String, - pub password: String, -} - -async fn login( - Json(credentials): Json, - Extension(pool): Extension, - mut session: WritableSession, -) -> Result, error::AppError> { - let us = users_service::UserService::new(pool); - match us - .validate_user(credentials.username, credentials.password) - .await - .map_err(|e| { - tracing::error!("could not validate user: {}", e); - - AppError::InternalServerError - })? { - Some(user_id) => { - if let Err(e) = session.insert("userId", user_id.clone()) { - tracing::error!("could not insert session: {}", e); - return Err(AppError::InternalServerError); - } - - Ok(Json(json!({ "userId": user_id }))) - } - None => Err(AppError::WrongCredentials), - } -} -async fn register( - Json(credentials): Json, - Extension(pool): Extension, -) -> Result, error::AppError> { - let us = users_service::UserService::new(pool) - .add_user(credentials.username, credentials.password) - .await - .map_err(|e| { - tracing::error!("could not add user: {}", e); - - AppError::InternalServerError - })?; - - Ok(Json(json!({ "userId": us }))) -} diff --git a/como_bin/src/services/mod.rs b/como_bin/src/services/mod.rs deleted file mode 100644 index 433996f..0000000 --- a/como_bin/src/services/mod.rs +++ /dev/null @@ -1 +0,0 @@ -pub mod users_service; diff --git a/como_bin/src/services/users_service.rs b/como_bin/src/services/users_service.rs deleted file mode 100644 index f0bf1e7..0000000 --- a/como_bin/src/services/users_service.rs +++ /dev/null @@ -1,78 +0,0 @@ -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(user.id.to_string())), - 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/como_core/Cargo.toml b/como_core/Cargo.toml index dd72add..63d202a 100644 --- a/como_core/Cargo.toml +++ b/como_core/Cargo.toml @@ -6,6 +6,7 @@ edition = "2021" # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html [dependencies] +como_domain = { path = "../como_domain" } tokio = { version = "1", features = ["full"] } axum = "0.5.1" diff --git a/como_core/src/items/mod.rs b/como_core/src/items/mod.rs index f274129..7ceebc2 100644 --- a/como_core/src/items/mod.rs +++ b/como_core/src/items/mod.rs @@ -1,8 +1,11 @@ use std::sync::Arc; use async_trait::async_trait; +use como_domain::item::{requests::CreateItemDto, responses::CreatedItemDto}; pub type DynItemService = Arc; #[async_trait] -pub trait ItemService {} +pub trait ItemService { + async fn add_item(&self, item: CreateItemDto) -> anyhow::Result; +} diff --git a/como_core/src/users/mod.rs b/como_core/src/users/mod.rs index a9ca003..f2b10de 100644 --- a/como_core/src/users/mod.rs +++ b/como_core/src/users/mod.rs @@ -5,4 +5,11 @@ use async_trait::async_trait; pub type DynUserService = Arc; #[async_trait] -pub trait UserService {} +pub trait UserService { + async fn add_user(&self, username: String, password: String) -> anyhow::Result; + async fn validate_user( + &self, + username: String, + password: String, + ) -> anyhow::Result>; +} diff --git a/como_domain/Cargo.toml b/como_domain/Cargo.toml index f6b02b6..ce8b82c 100644 --- a/como_domain/Cargo.toml +++ b/como_domain/Cargo.toml @@ -6,6 +6,7 @@ edition = "2021" # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html [dependencies] +async-graphql = { version = "4.0.6", features = ["uuid"] } anyhow = "1.0.60" serde = { version = "1.0", features = ["derive"] } serde_json = "1.0.68" diff --git a/como_domain/src/item/mod.rs b/como_domain/src/item/mod.rs index 99f565a..46edd86 100644 --- a/como_domain/src/item/mod.rs +++ b/como_domain/src/item/mod.rs @@ -1,10 +1,11 @@ pub mod requests; pub mod responses; +use async_graphql::{Enum, InputObject, SimpleObject}; use serde::{Deserialize, Serialize}; use uuid::Uuid; -#[derive(Debug, Deserialize, Serialize)] +#[derive(Debug, Deserialize, Serialize, Clone, PartialEq, Eq, Enum, Copy)] pub enum ItemState { Created, Done, @@ -12,7 +13,7 @@ pub enum ItemState { Deleted, } -#[derive(Debug, Serialize, Deserialize)] +#[derive(Debug, Serialize, Deserialize, Clone, PartialEq, Eq, InputObject, SimpleObject)] pub struct ItemDto { pub id: Uuid, pub title: String, diff --git a/como_domain/src/item/requests.rs b/como_domain/src/item/requests.rs index 1a5cf78..a11bd02 100644 --- a/como_domain/src/item/requests.rs +++ b/como_domain/src/item/requests.rs @@ -1,6 +1,7 @@ +use async_graphql::InputObject; use serde::{Deserialize, Serialize}; -#[derive(Debug, Serialize, Deserialize)] +#[derive(Debug, Serialize, Deserialize, Clone, PartialEq, Eq, InputObject)] pub struct CreateItemDto { pub name: String, } diff --git a/como_gql/Cargo.toml b/como_gql/Cargo.toml new file mode 100644 index 0000000..e4f8f16 --- /dev/null +++ b/como_gql/Cargo.toml @@ -0,0 +1,36 @@ +[package] +name = "como_gql" +version = "0.1.0" +edition = "2021" + +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html + +[dependencies] +como_core = { path = "../como_core" } +como_domain = { path = "../como_domain" } +como_infrastructure = { path = "../como_infrastructure" } + +async-graphql = "4.0.6" +async-graphql-axum = "*" +axum = "0.5.13" +axum-extra = { version = "*", features = ["cookie", "cookie-private"] } +axum-sessions = { version = "*" } +serde = { version = "1.0", features = ["derive"] } +serde_json = "1.0.68" +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 = { version = "0.16", features = ["secure", "percent-encode"] } diff --git a/como_bin/src/graphql.rs b/como_gql/src/graphql.rs similarity index 54% rename from como_bin/src/graphql.rs rename to como_gql/src/graphql.rs index ceda14b..679ac26 100644 --- a/como_bin/src/graphql.rs +++ b/como_gql/src/graphql.rs @@ -1,8 +1,8 @@ -use async_graphql::{Context, EmptySubscription, Object, Schema}; +use async_graphql::{Context, EmptySubscription, Object, Schema, SimpleObject}; -use como_domain::item::{requests::CreateItemDto, responses::CreatedItemDto}; - -use crate::services::users_service::UserService; +use como_domain::item::{requests::CreateItemDto, responses::CreatedItemDto, ItemState}; +use como_infrastructure::register::ServiceRegister; +use uuid::Uuid; pub type CibusSchema = Schema; @@ -16,9 +16,12 @@ impl MutationRoot { username: String, password: String, ) -> anyhow::Result { - let user_service = ctx.data_unchecked::(); + let service_register = ctx.data_unchecked::(); - let valid = user_service.validate_user(username, password).await?; + let valid = service_register + .user_service + .validate_user(username, password) + .await?; let returnvalid = match valid { Some(..) => true, None => false, @@ -33,9 +36,12 @@ impl MutationRoot { username: String, password: String, ) -> anyhow::Result { - let user_service = ctx.data_unchecked::(); + let service_register = ctx.data_unchecked::(); - let user_id = user_service.add_user(username, password).await?; + let user_id = service_register + .user_service + .add_user(username, password) + .await?; Ok(user_id) } @@ -45,7 +51,11 @@ impl MutationRoot { ctx: &Context<'_>, item: CreateItemDto, ) -> anyhow::Result { - let services_register = ctx.data_unchecked::() + let services_register = ctx.data_unchecked::(); + + let created_item = services_register.item_service.add_item(item).await?; + + Ok(created_item) } } @@ -53,5 +63,7 @@ pub struct QueryRoot; #[Object] impl QueryRoot { - async fn get_upcoming(&self, _ctx: &Context<'_>) -> Vec {} + async fn hello(&self, ctx: &Context<'_>) -> String { + "hello".into() + } } diff --git a/como_gql/src/lib.rs b/como_gql/src/lib.rs new file mode 100644 index 0000000..c5d5638 --- /dev/null +++ b/como_gql/src/lib.rs @@ -0,0 +1,42 @@ +use async_graphql_axum::{GraphQLRequest, GraphQLResponse}; +use axum::{ + extract::Extension, + http::{Method, StatusCode}, + response::{Html, IntoResponse}, + routing::{get, post}, + Json, Router, +}; +use axum_extra::extract::cookie::Key; + +use async_graphql::{ + http::{playground_source, GraphQLPlaygroundConfig}, + EmptySubscription, Schema, +}; +use axum_sessions::{ + async_session::MemoryStore, + extractors::{ReadableSession, WritableSession}, + SessionLayer, +}; +use graphql::CibusSchema; +use serde::{Deserialize, Serialize}; +use serde_json::{json, Value}; +use sqlx::PgPool; +use tower_http::{cors::CorsLayer, trace::TraceLayer}; +use tracing_subscriber::{layer::SubscriberExt, util::SubscriberInitExt}; + +use crate::graphql::{MutationRoot, QueryRoot}; +pub mod graphql; + +pub async fn graphql_handler( + schema: Extension, + session: ReadableSession, + req: GraphQLRequest, +) -> Result { + let req = req.into_inner(); + + Ok(schema.execute(req).await.into()) +} + +pub async fn graphql_playground() -> impl IntoResponse { + Html(playground_source(GraphQLPlaygroundConfig::new("/"))) +} diff --git a/como_infrastructure/src/register.rs b/como_infrastructure/src/register.rs index 88ec1e8..ceac362 100644 --- a/como_infrastructure/src/register.rs +++ b/como_infrastructure/src/register.rs @@ -20,12 +20,12 @@ pub struct ServiceRegister { } impl ServiceRegister { - pub fn new(_pool: ConnectionPool, _config: AppConfig) -> Self { + pub fn new(pool: ConnectionPool, _config: Arc) -> Self { info!("creating services"); let item_service = Arc::new(DefaultItemService::new()) as DynItemService; let project_service = Arc::new(DefaultProjectService::new()) as DynProjectService; - let user_service = Arc::new(DefaultUserService::new()) as DynUserService; + let user_service = Arc::new(DefaultUserService::new(pool.clone())) as DynUserService; info!("services created succesfully"); diff --git a/como_infrastructure/src/services/item_service.rs b/como_infrastructure/src/services/item_service.rs index e29094a..ea6b32a 100644 --- a/como_infrastructure/src/services/item_service.rs +++ b/como_infrastructure/src/services/item_service.rs @@ -1,4 +1,6 @@ +use axum::async_trait; use como_core::items::ItemService; +use como_domain::item::{requests::CreateItemDto, responses::CreatedItemDto}; pub struct DefaultItemService {} @@ -8,4 +10,9 @@ impl DefaultItemService { } } -impl ItemService for DefaultItemService {} +#[async_trait] +impl ItemService for DefaultItemService { + async fn add_item(&self, _item: CreateItemDto) -> anyhow::Result { + todo!() + } +} diff --git a/como_infrastructure/src/services/user_service.rs b/como_infrastructure/src/services/user_service.rs index 5869b7f..33d3471 100644 --- a/como_infrastructure/src/services/user_service.rs +++ b/como_infrastructure/src/services/user_service.rs @@ -1,11 +1,83 @@ +use argon2::{password_hash::SaltString, Argon2, PasswordHash, PasswordHasher, PasswordVerifier}; +use axum::async_trait; use como_core::users::UserService; +use rand_core::OsRng; -pub struct DefaultUserService {} +use crate::database::ConnectionPool; + +pub struct DefaultUserService { + pool: ConnectionPool, +} impl DefaultUserService { - pub fn new() -> Self { - Self {} + pub fn new(pool: ConnectionPool) -> Self { + Self { pool } + } + + 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::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::anyhow!(e))?; + match argon2.verify_password(password.as_bytes(), &parsed_hash) { + Ok(..) => Ok(true), + Err(..) => Ok(false), + } } } -impl UserService for DefaultUserService {} +#[async_trait] +impl UserService for DefaultUserService { + 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.pool) + .await?; + + Ok(rec.id.to_string()) + } + + 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.pool) + .await?; + + match rec { + Some(user) => match self.validate_password(password, user.password_hash)? { + true => Ok(Some(user.id.to_string())), + false => Ok(None), + }, + None => Ok(None), + } + } +} diff --git a/como_bin/target/sqlx/query-4e07408562bedb8b60ce05c1decfe3ad16b72230967de01f640b7e4729b49fce.json b/como_infrastructure/target/sqlx/query-4e07408562bedb8b60ce05c1decfe3ad16b72230967de01f640b7e4729b49fce.json similarity index 100% rename from como_bin/target/sqlx/query-4e07408562bedb8b60ce05c1decfe3ad16b72230967de01f640b7e4729b49fce.json rename to como_infrastructure/target/sqlx/query-4e07408562bedb8b60ce05c1decfe3ad16b72230967de01f640b7e4729b49fce.json