13 Commits

Author SHA1 Message Date
c58c888725 refactor 2022-10-16 14:06:07 +02:00
78b9732ca1 Fix shortcommings 2022-10-12 17:38:40 +02:00
1d4cda7c48 with memory db 2022-10-04 22:39:57 +02:00
c7f8dc6198 with graphql 2022-10-04 12:06:00 +02:00
5e9001b998 Removed unused imports 2022-10-04 11:07:14 +02:00
ae74f66c3a Added apis 2022-10-04 11:06:48 +02:00
6234cf18e8 Add initial services 2022-10-03 23:00:31 +02:00
b01b33f7d1 add core models 2022-10-03 22:20:01 +02:00
8c51b68523 remove bff 2022-10-03 22:08:25 +02:00
d88abefa9a fixed main 2022-10-02 20:53:48 +02:00
02b40a8491 with bff again 2022-10-02 20:52:29 +02:00
b20d3c418c Add bff 2022-10-02 20:51:06 +02:00
71bdea4001 fixed stuff 2022-10-02 14:16:13 +02:00
61 changed files with 2356 additions and 352 deletions

7
.env
View File

@@ -1,4 +1,11 @@
POSTGRES_DB=como POSTGRES_DB=como
POSTGRES_USER=como POSTGRES_USER=como
POSTGRES_PASSWORD=somenotverysecurepassword POSTGRES_PASSWORD=somenotverysecurepassword
DATABASE_URL="postgres://como:somenotverysecurepassword@localhost:5432/como" DATABASE_URL="postgres://como:somenotverysecurepassword@localhost:5432/como"
RUST_LOG=como_api=info,como_bin=info,como_core=info,como_domain=info,como_gql=info,como_infrastructure=info,sqlx=debug,tower_http=debug
TOKEN_SECRET=something
API_PORT=3001
CORS_ORIGIN=http://localhost:3000
RUN_MIGRATIONS=true
SEED=true

1349
Cargo.lock generated

File diff suppressed because it is too large Load Diff

View File

@@ -1,4 +1,9 @@
[workspace] [workspace]
members = [ members = [
"como_bin" "como_bin",
"como_core",
"como_domain",
"como_infrastructure",
"como_gql",
"como_api",
] ]

View File

@@ -1,3 +1 @@
# Cibus Backend # Cibus Backend
Some text

39
como_api/Cargo.toml Normal file
View File

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

View File

@@ -0,0 +1,21 @@
use async_graphql::{EmptySubscription, Schema};
use axum::{routing::get, Extension, Router};
use como_gql::{
graphql::{MutationRoot, QueryRoot},
graphql_handler, graphql_playground,
};
use como_infrastructure::register::ServiceRegister;
pub struct GraphQLController;
impl GraphQLController {
pub fn new_router(service_register: ServiceRegister) -> Router {
let schema = Schema::build(QueryRoot, MutationRoot, EmptySubscription)
.data(service_register)
.finish();
Router::new()
.route("/", get(graphql_playground).post(graphql_handler))
.layer(Extension(schema))
}
}

View File

@@ -0,0 +1 @@
pub mod graphql;

2
como_api/src/lib.rs Normal file
View File

@@ -0,0 +1,2 @@
mod controllers;
pub mod router;

44
como_api/src/router.rs Normal file
View File

@@ -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 run_api(
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::<HeaderValue>()
.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(())
}
}

View File

@@ -6,8 +6,19 @@ edition = "2021"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[dependencies] [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 = "4.0.6"
async-graphql-axum = "*"
axum = "0.5.13" 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"] } tokio = { version = "1.20.1", features = ["full"] }
uuid = { version = "1.1.2", features = ["v4", "fast-rng"] } uuid = { version = "1.1.2", features = ["v4", "fast-rng"] }
sqlx = { version = "0.6", features = [ sqlx = { version = "0.6", features = [
@@ -24,4 +35,5 @@ tracing-subscriber = { version = "0.3.15", features = ["env-filter"] }
tower-http = { version = "0.3.4", features = ["full"] } tower-http = { version = "0.3.4", features = ["full"] }
argon2 = "0.4" argon2 = "0.4"
rand_core = { version = "0.6", features = ["std"] } rand_core = { version = "0.6", features = ["std"] }
cookie = "0.16" cookie = { version = "0.16", features = ["secure", "percent-encode"] }
clap = { version = "3", features = ["derive", "env"] }

View File

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

View File

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

22
como_bin/src/error.rs Normal file
View File

@@ -0,0 +1,22 @@
use axum::{http::StatusCode, response::IntoResponse, Json};
use serde_json::json;
#[allow(dead_code)]
#[derive(Debug)]
pub enum AppError {
WrongCredentials,
InternalServerError,
}
impl IntoResponse for AppError {
fn into_response(self) -> axum::response::Response {
let (status, err_msg) = match self {
Self::WrongCredentials => (StatusCode::BAD_REQUEST, "invalid credentials"),
Self::InternalServerError => (
StatusCode::INTERNAL_SERVER_ERROR,
"something went wrong with your request",
),
};
(status, Json(json!({ "error": err_msg }))).into_response()
}
}

View File

@@ -1,2 +0,0 @@
pub mod users;

View File

@@ -1,103 +0,0 @@
use async_graphql::{Context, EmptySubscription, Object, Schema, SimpleObject};
use uuid::Uuid;
use crate::services::users_service::UserService;
pub type CibusSchema = Schema<QueryRoot, MutationRoot, EmptySubscription>;
pub struct MutationRoot;
#[Object]
impl MutationRoot {
async fn login(
&self,
ctx: &Context<'_>,
username: String,
password: String,
) -> anyhow::Result<bool> {
let user_service = ctx.data_unchecked::<UserService>();
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<String> {
let user_service = ctx.data_unchecked::<UserService>();
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<Event> {
vec![Event::new(
None,
"Some-name".into(),
None,
None,
EventDate::new(2022, 08, 08, 23, 51),
)]
}
}
#[derive(SimpleObject)]
pub struct Event {
pub id: String,
pub name: String,
pub description: Option<Vec<String>>,
pub location: Option<String>,
pub date: EventDate,
}
impl Event {
pub fn new(
id: Option<String>,
name: String,
description: Option<Vec<String>>,
location: Option<String>,
date: EventDate,
) -> Self {
Self {
id: id.unwrap_or_else(|| Uuid::new_v4().to_string()),
name,
description,
location,
date,
}
}
}
#[derive(SimpleObject)]
pub struct EventDate {
pub year: u32,
pub month: u32,
pub day: u32,
pub hour: u32,
pub minute: u32,
}
impl EventDate {
pub fn new(year: u32, month: u32, day: u32, hour: u32, minute: u32) -> Self {
Self {
year,
month,
day,
hour,
minute,
}
}
}

View File

@@ -1,92 +1,41 @@
use std::env::{self, current_dir}; use std::sync::Arc;
mod gqlx; mod error;
mod graphql;
mod services;
use axum::{ use clap::Parser;
extract::Extension,
http::Method, use anyhow::Context;
response::{Html, IntoResponse},
routing::get, use como_api::router::Api;
Json, Router, use como_infrastructure::{
configs::AppConfig, database::ConnectionPoolManager, register::ServiceRegister,
}; };
use async_graphql::{
http::{playground_source, GraphQLPlaygroundConfig},
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 tracing_subscriber::{layer::SubscriberExt, util::SubscriberInitExt};
use crate::graphql::{MutationRoot, QueryRoot};
async fn graphql_handler(schema: Extension<CibusSchema>, req: Json<Request>) -> Json<Response> {
schema.execute(req.0).await.into()
}
async fn graphql_playground() -> impl IntoResponse {
Html(playground_source(GraphQLPlaygroundConfig::new("/")))
}
#[tokio::main] #[tokio::main]
async fn main() -> anyhow::Result<()> { async fn main() -> anyhow::Result<()> {
// Environment
tracing::info!("Loading dotenv"); tracing::info!("Loading dotenv");
dotenv::dotenv()?; dotenv::dotenv()?;
// Logging let config = Arc::new(AppConfig::parse());
tracing_subscriber::registry() tracing_subscriber::registry()
.with(tracing_subscriber::EnvFilter::new( .with(tracing_subscriber::EnvFilter::new(&config.rust_log))
std::env::var("RUST_LOG").unwrap_or_else(|_| {
"como_bin=debug,tower_http=debug,axum_extra=debug,hyper=info,mio=info,sqlx=info,async_graphql=debug"
.into()
}),
))
.with(tracing_subscriber::fmt::layer()) .with(tracing_subscriber::fmt::layer())
.init(); .init();
// Database let pool = ConnectionPoolManager::new_pool(&config.database_url, true).await?;
tracing::info!("Creating pool");
let db_url = env::var("DATABASE_URL")?;
let pool = PgPool::connect(&db_url).await?;
// Database Migrate let service_register = ServiceRegister::new(pool, config.clone());
tracing::info!("Migrating db");
sqlx::migrate!("db/migrations").run(&pool).await?;
tracing::info!("current path: {}", current_dir()?.to_string_lossy()); Api::run_api(
config.api_port,
// Schema &config.cors_origin,
println!("Building schema"); service_register.clone(),
let schema = Schema::build(QueryRoot, MutationRoot, EmptySubscription) )
.data(users_service::UserService::new(pool)) .await
.finish(); .context("could not initialize API")?;
// CORS
let cors = vec!["http://localhost:3000".parse().unwrap()];
// Webserver
tracing::info!("Building router");
let app = Router::new()
.route("/", get(graphql_playground).post(graphql_handler))
.layer(Extension(schema))
.layer(TraceLayer::new_for_http())
.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())
.await
.unwrap();
Ok(()) Ok(())
} }

View File

@@ -1 +0,0 @@
pub struct CookieService {}

View File

@@ -1,2 +0,0 @@
pub mod cookie_service;
pub mod users_service;

31
como_core/Cargo.toml Normal file
View File

@@ -0,0 +1,31 @@
[package]
name = "como_core"
version = "0.1.0"
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"
# utilty crates
serde = { version = "1.0.136", features = ["derive"] }
sqlx = { version = "0.5", features = [
"runtime-tokio-rustls",
"postgres",
"time",
] }
serde_json = "1.0.81"
dotenv = "0.15.0"
tracing = "0.1"
tracing-subscriber = "0.3"
anyhow = "1"
validator = { version = "0.15", features = ["derive"] }
async-trait = "0.1"
thiserror = "1"
rust-argon2 = "1.0"
clap = { version = "3", features = ["derive", "env"] }
mockall = "0.11.1"
time = "0.2"

View File

@@ -0,0 +1,18 @@
use std::sync::Arc;
use async_trait::async_trait;
use como_domain::item::{
queries::{GetItemQuery, GetItemsQuery},
requests::CreateItemDto,
responses::CreatedItemDto,
ItemDto,
};
pub type DynItemService = Arc<dyn ItemService + Send + Sync>;
#[async_trait]
pub trait ItemService {
async fn add_item(&self, item: CreateItemDto) -> anyhow::Result<CreatedItemDto>;
async fn get_item(&self, query: GetItemQuery) -> anyhow::Result<ItemDto>;
async fn get_items(&self, query: GetItemsQuery) -> anyhow::Result<Vec<ItemDto>>;
}

3
como_core/src/lib.rs Normal file
View File

@@ -0,0 +1,3 @@
pub mod items;
pub mod projects;
pub mod users;

View File

@@ -0,0 +1,15 @@
use std::sync::Arc;
use async_trait::async_trait;
use como_domain::projects::{
queries::{GetProjectQuery, GetProjectsQuery},
ProjectDto,
};
pub type DynProjectService = Arc<dyn ProjectService + Send + Sync>;
#[async_trait]
pub trait ProjectService {
async fn get_project(&self, query: GetProjectQuery) -> anyhow::Result<ProjectDto>;
async fn get_projects(&self, query: GetProjectsQuery) -> anyhow::Result<Vec<ProjectDto>>;
}

View File

@@ -0,0 +1,15 @@
use std::sync::Arc;
use async_trait::async_trait;
pub type DynUserService = Arc<dyn UserService + Send + Sync>;
#[async_trait]
pub trait UserService {
async fn add_user(&self, username: String, password: String) -> anyhow::Result<String>;
async fn validate_user(
&self,
username: String,
password: String,
) -> anyhow::Result<Option<String>>;
}

13
como_domain/Cargo.toml Normal file
View File

@@ -0,0 +1,13 @@
[package]
name = "como_domain"
version = "0.1.0"
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"
uuid = { version = "1.1.2", features = ["v4", "fast-rng", "serde"] }

View File

@@ -0,0 +1,23 @@
pub mod queries;
pub mod requests;
pub mod responses;
use async_graphql::{Enum, InputObject};
use serde::{Deserialize, Serialize};
use uuid::Uuid;
#[derive(Debug, Deserialize, Serialize, Clone, PartialEq, Eq, Enum, Copy)]
pub enum ItemState {
Created,
Done,
Archived,
Deleted,
}
#[derive(Debug, Serialize, Deserialize, Clone, PartialEq, Eq, InputObject)]
pub struct ItemDto {
pub id: Uuid,
pub title: String,
pub description: Option<String>,
pub state: ItemState,
}

View File

@@ -0,0 +1,13 @@
use async_graphql::InputObject;
use serde::{Deserialize, Serialize};
use uuid::Uuid;
#[derive(Debug, Serialize, Deserialize, Clone, PartialEq, Eq, InputObject)]
pub struct GetItemQuery {
pub item_id: Uuid,
}
#[derive(Debug, Serialize, Deserialize, Clone, PartialEq, Eq, InputObject)]
pub struct GetItemsQuery {
pub user_id: Uuid,
}

View File

@@ -0,0 +1,7 @@
use async_graphql::InputObject;
use serde::{Deserialize, Serialize};
#[derive(Debug, Serialize, Deserialize, Clone, PartialEq, Eq, InputObject)]
pub struct CreateItemDto {
pub name: String,
}

View File

@@ -0,0 +1,3 @@
use super::ItemDto;
pub type CreatedItemDto = ItemDto;

3
como_domain/src/lib.rs Normal file
View File

@@ -0,0 +1,3 @@
pub mod item;
pub mod projects;
pub mod users;

View File

@@ -0,0 +1,13 @@
pub mod queries;
pub mod requests;
pub mod responses;
use async_graphql::{InputObject, SimpleObject};
use serde::{Deserialize, Serialize};
use uuid::Uuid;
#[derive(Debug, Serialize, Deserialize, Clone, PartialEq, Eq, InputObject, SimpleObject)]
pub struct ProjectDto {
pub id: Uuid,
pub name: String,
}

View File

@@ -0,0 +1,14 @@
use async_graphql::InputObject;
use serde::{Deserialize, Serialize};
use uuid::Uuid;
#[derive(Debug, Serialize, Deserialize, Clone, PartialEq, Eq, InputObject)]
pub struct GetProjectQuery {
pub project_id: Option<Uuid>,
pub item_id: Option<Uuid>,
}
#[derive(Debug, Serialize, Deserialize, Clone, PartialEq, Eq, InputObject)]
pub struct GetProjectsQuery {
pub user_id: Uuid,
}

View File

@@ -0,0 +1,6 @@
use serde::{Deserialize, Serialize};
#[derive(Serialize, Deserialize)]
pub struct CreateProjectDto {
pub name: String,
}

View File

@@ -0,0 +1,3 @@
use super::ProjectDto;
pub type CreatedProjectDto = ProjectDto;

View File

@@ -0,0 +1,12 @@
pub mod requests;
pub mod responses;
use serde::{Deserialize, Serialize};
use uuid::Uuid;
#[derive(Serialize, Deserialize, Debug)]
pub struct UserDto {
pub id: Uuid,
pub username: String,
pub email: String,
}

View File

@@ -0,0 +1,8 @@
use serde::{Deserialize, Serialize};
#[derive(Serialize, Deserialize)]
pub struct CreateUserDto {
pub username: String,
pub email: String,
pub password: String,
}

View File

@@ -0,0 +1,3 @@
use super::UserDto;
pub type UserCreatedDto = UserDto;

36
como_gql/Cargo.toml Normal file
View File

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

125
como_gql/src/graphql.rs Normal file
View File

@@ -0,0 +1,125 @@
use async_graphql::{Context, EmptySubscription, Object, Schema};
use como_domain::{
item::{
queries::{GetItemQuery, GetItemsQuery},
requests::CreateItemDto,
},
projects::{
queries::{GetProjectQuery, GetProjectsQuery},
ProjectDto,
},
};
use como_infrastructure::register::ServiceRegister;
use crate::items::{CreatedItem, Item};
pub type ComoSchema = Schema<QueryRoot, MutationRoot, EmptySubscription>;
pub struct MutationRoot;
#[Object]
impl MutationRoot {
async fn login(
&self,
ctx: &Context<'_>,
username: String,
password: String,
) -> anyhow::Result<bool> {
let service_register = ctx.data_unchecked::<ServiceRegister>();
let valid = service_register
.user_service
.validate_user(username, password)
.await?;
let returnvalid = match valid {
Some(..) => true,
None => false,
};
Ok(returnvalid)
}
async fn register(
&self,
ctx: &Context<'_>,
username: String,
password: String,
) -> anyhow::Result<String> {
let service_register = ctx.data_unchecked::<ServiceRegister>();
let user_id = service_register
.user_service
.add_user(username, password)
.await?;
Ok(user_id)
}
async fn create_item(
&self,
ctx: &Context<'_>,
item: CreateItemDto,
) -> anyhow::Result<CreatedItem> {
let services_register = ctx.data_unchecked::<ServiceRegister>();
let created_item = services_register.item_service.add_item(item).await?;
Ok(CreatedItem {
id: created_item.id,
})
}
}
pub struct QueryRoot;
#[Object]
impl QueryRoot {
// Items
async fn get_item(&self, ctx: &Context<'_>, query: GetItemQuery) -> anyhow::Result<Item> {
let item = ctx
.data_unchecked::<ServiceRegister>()
.item_service
.get_item(query)
.await?;
Ok(Item::from(item))
}
async fn get_items(
&self,
ctx: &Context<'_>,
query: GetItemsQuery,
) -> anyhow::Result<Vec<Item>> {
let items = ctx
.data_unchecked::<ServiceRegister>()
.item_service
.get_items(query)
.await?;
Ok(items.iter().map(|i| Item::from(i.clone())).collect())
}
// Projects
async fn get_project(
&self,
ctx: &Context<'_>,
query: GetProjectQuery,
) -> anyhow::Result<ProjectDto> {
ctx.data_unchecked::<ServiceRegister>()
.project_service
.get_project(query)
.await
}
async fn get_projects(
&self,
ctx: &Context<'_>,
query: GetProjectsQuery,
) -> anyhow::Result<Vec<ProjectDto>> {
ctx.data_unchecked::<ServiceRegister>()
.project_service
.get_projects(query)
.await
}
}

76
como_gql/src/items.rs Normal file
View File

@@ -0,0 +1,76 @@
use async_graphql::{Context, Object};
use como_domain::{
item::{queries::GetItemQuery, ItemDto, ItemState},
projects::queries::GetProjectQuery,
};
use como_infrastructure::register::ServiceRegister;
use uuid::Uuid;
use crate::projects::Project;
pub struct CreatedItem {
pub id: Uuid,
}
#[Object]
impl CreatedItem {
pub async fn item(&self, ctx: &Context<'_>) -> anyhow::Result<Item> {
let item = ctx
.data_unchecked::<ServiceRegister>()
.item_service
.get_item(GetItemQuery { item_id: self.id })
.await?;
Ok(item.into())
}
}
pub struct Item {
pub id: Uuid,
pub title: String,
pub description: Option<String>,
pub state: ItemState,
}
#[Object]
impl Item {
pub async fn id(&self, _ctx: &Context<'_>) -> anyhow::Result<Uuid> {
Ok(self.id)
}
pub async fn title(&self, _ctx: &Context<'_>) -> anyhow::Result<String> {
Ok(self.title.clone())
}
pub async fn description(&self, _ctx: &Context<'_>) -> anyhow::Result<Option<String>> {
Ok(self.description.clone())
}
pub async fn state(&self, _ctx: &Context<'_>) -> anyhow::Result<ItemState> {
Ok(self.state)
}
pub async fn project(&self, ctx: &Context<'_>) -> anyhow::Result<Project> {
let project = ctx
.data_unchecked::<ServiceRegister>()
.project_service
.get_project(GetProjectQuery {
item_id: Some(self.id),
project_id: None,
})
.await?;
Ok(project.into())
}
}
impl From<ItemDto> for Item {
fn from(dto: ItemDto) -> Self {
Self {
id: dto.id,
title: dto.title,
description: dto.description,
state: dto.state,
}
}
}

26
como_gql/src/lib.rs Normal file
View File

@@ -0,0 +1,26 @@
use async_graphql_axum::{GraphQLRequest, GraphQLResponse};
use axum::{
extract::Extension,
http::StatusCode,
response::{Html, IntoResponse},
};
use async_graphql::http::{playground_source, GraphQLPlaygroundConfig};
use graphql::ComoSchema;
pub mod graphql;
mod items;
mod projects;
pub async fn graphql_handler(
schema: Extension<ComoSchema>,
req: GraphQLRequest,
) -> Result<GraphQLResponse, StatusCode> {
let req = req.into_inner();
Ok(schema.execute(req).await.into())
}
pub async fn graphql_playground() -> impl IntoResponse {
Html(playground_source(GraphQLPlaygroundConfig::new("/graphql")))
}

18
como_gql/src/projects.rs Normal file
View File

@@ -0,0 +1,18 @@
use async_graphql::SimpleObject;
use como_domain::projects::ProjectDto;
use uuid::Uuid;
#[derive(SimpleObject)]
pub struct Project {
pub id: Uuid,
pub name: String,
}
impl From<ProjectDto> for Project {
fn from(dto: ProjectDto) -> Self {
Self {
id: dto.id,
name: dto.name,
}
}
}

View File

@@ -0,0 +1,36 @@
[package]
name = "como_infrastructure"
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" }
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"] }
clap = { version = "3", features = ["derive", "env"] }

View File

@@ -0,0 +1,17 @@
#[derive(clap::Parser)]
pub struct AppConfig {
#[clap(long, env)]
pub database_url: String,
#[clap(long, env)]
pub rust_log: String,
#[clap(long, env)]
pub token_secret: String,
#[clap(long, env)]
pub api_port: u32,
#[clap(long, env)]
pub run_migrations: bool,
#[clap(long, env)]
pub seed: bool,
#[clap(long, env)]
pub cors_origin: String,
}

View File

@@ -0,0 +1,33 @@
use anyhow::Context;
use sqlx::{postgres::PgPoolOptions, Pool, Postgres};
use tracing::log::info;
pub type ConnectionPool = Pool<Postgres>;
pub struct ConnectionPoolManager;
impl ConnectionPoolManager {
pub async fn new_pool(
connection_string: &str,
run_migrations: bool,
) -> anyhow::Result<ConnectionPool> {
info!("initializing the database connection pool");
let pool = PgPoolOptions::new()
.max_connections(5)
.connect(connection_string)
.await
.context("error while initializing the database connection pool")?;
if run_migrations {
info!("migrations enabled");
info!("migrating database");
sqlx::migrate!()
.run(&pool)
.await
.context("error while running database migrations")?;
}
Ok(pool)
}
}

View File

@@ -0,0 +1,5 @@
pub mod configs;
pub mod database;
pub mod register;
pub mod repositories;
pub mod services;

View File

@@ -0,0 +1,38 @@
use std::sync::Arc;
use como_core::{items::DynItemService, projects::DynProjectService, users::DynUserService};
use tracing::log::info;
use crate::{
configs::AppConfig,
database::ConnectionPool,
services::{
item_service::MemoryItemService, project_service::MemoryProjectService,
user_service::DefaultUserService,
},
};
#[derive(Clone)]
pub struct ServiceRegister {
pub item_service: DynItemService,
pub project_service: DynProjectService,
pub user_service: DynUserService,
}
impl ServiceRegister {
pub fn new(pool: ConnectionPool, _config: Arc<AppConfig>) -> Self {
info!("creating services");
let item_service = Arc::new(MemoryItemService::new()) as DynItemService;
let project_service = Arc::new(MemoryProjectService::new()) as DynProjectService;
let user_service = Arc::new(DefaultUserService::new(pool)) as DynUserService;
info!("services created succesfully");
Self {
item_service,
user_service,
project_service,
}
}
}

View File

@@ -0,0 +1,97 @@
use std::{
collections::HashMap,
sync::{Arc, Mutex},
};
use anyhow::Context;
use axum::async_trait;
use como_core::items::ItemService;
use como_domain::item::{
queries::{GetItemQuery, GetItemsQuery},
requests::CreateItemDto,
responses::CreatedItemDto,
ItemDto,
};
use uuid::Uuid;
pub struct DefaultItemService {}
impl DefaultItemService {
pub fn new() -> Self {
Self {}
}
}
impl Default for DefaultItemService {
fn default() -> Self {
Self::new()
}
}
#[async_trait]
impl ItemService for DefaultItemService {
async fn add_item(&self, _item: CreateItemDto) -> anyhow::Result<CreatedItemDto> {
todo!()
}
async fn get_item(&self, _query: GetItemQuery) -> anyhow::Result<ItemDto> {
todo!()
}
async fn get_items(&self, _query: GetItemsQuery) -> anyhow::Result<Vec<ItemDto>> {
todo!()
}
}
pub struct MemoryItemService {
item_store: Arc<Mutex<HashMap<String, ItemDto>>>,
}
impl MemoryItemService {
pub fn new() -> Self {
Self {
item_store: Arc::new(Mutex::new(HashMap::new())),
}
}
}
impl Default for MemoryItemService {
fn default() -> Self {
Self::new()
}
}
#[async_trait]
impl ItemService for MemoryItemService {
async fn add_item(&self, create_item: CreateItemDto) -> anyhow::Result<CreatedItemDto> {
if let Ok(mut item_store) = self.item_store.lock() {
let item = ItemDto {
id: Uuid::new_v4(),
title: create_item.name,
description: None,
state: como_domain::item::ItemState::Created,
};
item_store.insert(item.id.to_string(), item.clone());
return Ok(item);
} else {
Err(anyhow::anyhow!("could not unlock item_store"))
}
}
async fn get_item(&self, query: GetItemQuery) -> anyhow::Result<ItemDto> {
if let Ok(item_store) = self.item_store.lock() {
let item = item_store
.get(&query.item_id.to_string())
.context("could not find item")?;
return Ok(item.clone());
} else {
Err(anyhow::anyhow!("could not unlock item_store"))
}
}
async fn get_items(&self, _query: GetItemsQuery) -> anyhow::Result<Vec<ItemDto>> {
todo!()
}
}

View File

@@ -0,0 +1,3 @@
pub mod item_service;
pub mod project_service;
pub mod user_service;

View File

@@ -0,0 +1,70 @@
use std::{collections::HashMap, sync::Arc};
use anyhow::Context;
use axum::async_trait;
use como_core::projects::ProjectService;
use como_domain::projects::{
queries::{GetProjectQuery, GetProjectsQuery},
ProjectDto,
};
use tokio::sync::Mutex;
pub struct DefaultProjectService {}
impl DefaultProjectService {
pub fn new() -> Self {
Self {}
}
}
impl Default for DefaultProjectService {
fn default() -> Self {
Self::new()
}
}
#[async_trait]
impl ProjectService for DefaultProjectService {
async fn get_project(&self, _query: GetProjectQuery) -> anyhow::Result<ProjectDto> {
todo!()
}
async fn get_projects(&self, _query: GetProjectsQuery) -> anyhow::Result<Vec<ProjectDto>> {
todo!()
}
}
pub struct MemoryProjectService {
project_store: Arc<Mutex<HashMap<String, ProjectDto>>>,
}
impl MemoryProjectService {
pub fn new() -> Self {
Self {
project_store: Arc::new(Mutex::new(HashMap::new())),
}
}
}
impl Default for MemoryProjectService {
fn default() -> Self {
Self::new()
}
}
#[async_trait]
impl ProjectService for MemoryProjectService {
async fn get_project(&self, query: GetProjectQuery) -> anyhow::Result<ProjectDto> {
let ps = self.project_store.lock().await;
if let Some(item_id) = query.item_id {
Ok(ps
.get(&item_id.to_string())
.context("could not find project")?
.clone())
} else {
Err(anyhow::anyhow!("could not find project"))
}
}
async fn get_projects(&self, _query: GetProjectsQuery) -> anyhow::Result<Vec<ProjectDto>> {
todo!()
}
}

View File

@@ -1,20 +1,45 @@
use std::sync::Arc;
use anyhow::anyhow;
use argon2::{password_hash::SaltString, Argon2, PasswordHash, PasswordHasher, PasswordVerifier}; use argon2::{password_hash::SaltString, Argon2, PasswordHash, PasswordHasher, PasswordVerifier};
use axum::async_trait;
use como_core::users::UserService;
use rand_core::OsRng; use rand_core::OsRng;
use sqlx::{Pool, Postgres};
pub struct UserService { use crate::database::ConnectionPool;
pgx: Pool<Postgres>,
pub struct DefaultUserService {
pool: ConnectionPool,
} }
impl UserService { impl DefaultUserService {
pub fn new(pgx: Pool<Postgres>) -> Self { pub fn new(pool: ConnectionPool) -> Self {
Self { pgx } Self { pool }
} }
pub async fn add_user(&self, username: String, password: String) -> anyhow::Result<String> { fn hash_password(&self, password: String) -> anyhow::Result<String> {
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<bool> {
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),
}
}
}
#[async_trait]
impl UserService for DefaultUserService {
async fn add_user(&self, username: String, password: String) -> anyhow::Result<String> {
let hashed_password = self.hash_password(password)?; let hashed_password = self.hash_password(password)?;
let rec = sqlx::query!( let rec = sqlx::query!(
@@ -26,17 +51,17 @@ impl UserService {
username, username,
hashed_password hashed_password
) )
.fetch_one(&self.pgx) .fetch_one(&self.pool)
.await?; .await?;
Ok(rec.id.to_string()) Ok(rec.id.to_string())
} }
pub async fn validate_user( async fn validate_user(
&self, &self,
username: String, username: String,
password: String, password: String,
) -> anyhow::Result<Option<()>> { ) -> anyhow::Result<Option<String>> {
let rec = sqlx::query!( let rec = sqlx::query!(
r#" r#"
SELECT * from users SELECT * from users
@@ -44,37 +69,15 @@ impl UserService {
"#, "#,
username, username,
) )
.fetch_optional(&self.pgx) .fetch_optional(&self.pool)
.await?; .await?;
match rec { match rec {
Some(user) => match self.validate_password(password, user.password_hash)? { Some(user) => match self.validate_password(password, user.password_hash)? {
true => Ok(Some(())), true => Ok(Some(user.id.to_string())),
false => Ok(None), false => Ok(None),
}, },
None => Ok(None), None => Ok(None),
} }
} }
fn hash_password(&self, password: String) -> anyhow::Result<String> {
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<bool> {
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),
}
}
} }

View File

@@ -0,0 +1,33 @@
{
"query": "\n SELECT * from users\n where username=$1\n ",
"describe": {
"columns": [
{
"ordinal": 0,
"name": "id",
"type_info": "Uuid"
},
{
"ordinal": 1,
"name": "username",
"type_info": "Varchar"
},
{
"ordinal": 2,
"name": "password_hash",
"type_info": "Varchar"
}
],
"parameters": {
"Left": [
"Text"
]
},
"nullable": [
false,
false,
false
]
},
"hash": "d3f222cf6c3d9816705426fdbed3b13cb575bb432eb1f33676c0b414e67aecaf"
}

View File

@@ -11,5 +11,9 @@ scripts:
type: shell type: shell
local_up: local_up:
type: shell type: shell
local_down:
type: shell
run_como: run_como:
type: shell type: shell
migrate_como:
type: shell

7
scripts/local_down.sh Executable file
View File

@@ -0,0 +1,7 @@
#!/bin/bash
set -e
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 down -v

View File

@@ -4,4 +4,4 @@ set -e
cuddle_cli render_template --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 docker compose -f $TMP/docker-compose.local_up.yml up -d --remove-orphans --build

6
scripts/migrate_como.sh Executable file
View File

@@ -0,0 +1,6 @@
#!/bin/bash
export $(cat .env | xargs)
cargo sqlx migrate run --source como_infrastructure/migrations --database-url=$DATABASE_URL

View File

@@ -2,4 +2,4 @@
set -e set -e
cargo run como_bin/ (cd como_bin; cargo watch -x run)

View File

@@ -2,14 +2,16 @@ version: '3.7'
services: services:
db: db:
image: postgres:13.5 build:
context: .
dockerfile: local_up.Dockerfile
restart: always restart: always
environment: environment:
- POSTGRES_DB=como
- POSTGRES_USER=como
- POSTGRES_PASSWORD=somenotverysecurepassword - POSTGRES_PASSWORD=somenotverysecurepassword
- DATABASE_URL="postgres://como:somenotverysecurepassword@localhost:5432/como"
ports: ports:
- 5432:5432 - 5432:5432
volumes: volumes:
- ./data/postgres:/var/lib/postgresql/data - pgdata:/var/lib/postgresql/data
volumes:
pgdata:

View File

@@ -0,0 +1,8 @@
#!/bin/bash
set -e
psql -v ON_ERROR_STOP=1 --username "$POSTGRES_USER" --dbname "$POSTGRES_DB" <<-EOSQL
CREATE USER como WITH PASSWORD 'somenotverysecurepassword';
CREATE DATABASE como;
GRANT ALL PRIVILEGES ON DATABASE como TO como;
EOSQL

View File

@@ -0,0 +1,3 @@
FROM postgres:14-alpine
COPY *.sh /docker-entrypoint-initdb.d/