2 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
64 changed files with 4300 additions and 528 deletions

88
.drone.yml Normal file
View File

@@ -0,0 +1,88 @@
kind: pipeline
name: default
type: docker
steps:
- name: load_secret
image: debian:buster-slim
volumes:
- name: ssh
path: /root/.ssh/
environment:
SSH_KEY:
from_secret: gitea_id_ed25519
commands:
- mkdir -p $HOME/.ssh/
- echo "$SSH_KEY" | base64 -d > $HOME/.ssh/id_ed25519
- name: build
image: kasperhermansen/cuddle:latest
pull: always
volumes:
- name: ssh
path: /root/.ssh/
- name: dockersock
path: /var/run
commands:
- apk add bash git
- git remote set-url origin $DRONE_GIT_SSH_URL
- cuddle_cli x setup_ssh
- cuddle_cli x start_deployment
- cuddle_cli x render_templates
- cuddle_cli x render_como_templates
- cuddle_cli x build_release
- cuddle_cli x push_release
- cuddle_cli x deploy_release
environment:
DOCKER_BUILDKIT: 1
DOCKER_USERNAME:
from_secret: docker_username
DOCKER_PASSWORD:
from_secret: docker_password
SSH_KEY:
from_secret: gitea_id_ed25519
- name: push_tags
image: kasperhermansen/drone-semantic-release:latest
pull: always
volumes:
- name: ssh
path: /root/.ssh/
- name: dockersock
path: /var/run
commands:
- semantic-release --no-ci
environment:
DOCKER_BUILDKIT: 1
SSH_KEY:
from_secret: gitea_id_ed25519
depends_on:
- build
- name: send telegram notification
image: appleboy/drone-telegram
settings:
token:
from_secret: telegram_token
to: 2129601481
format: markdown
depends_on:
- build
- push_tags
when:
status: [failure, success]
services:
- name: docker
image: docker:dind
privileged: true
volumes:
- name: dockersock
path: /var/run
volumes:
- name: ssh
temp: {}
- name: dockersock
temp: {}

11
.env Normal file
View File

@@ -0,0 +1,11 @@
POSTGRES_DB=como
POSTGRES_USER=como
POSTGRES_PASSWORD=somenotverysecurepassword
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

3
.gitignore vendored
View File

@@ -1,2 +1,3 @@
target/ /target
.cuddle/ .cuddle/
node_modules/

8
.releaserc.yml Normal file
View File

@@ -0,0 +1,8 @@
branches:
- "main"
plugins:
- "@semantic-release/commit-analyzer"
- "@semantic-release/release-notes-generator"
- "@semantic-release/changelog"
- "@semantic-release/git"

3312
Cargo.lock generated

File diff suppressed because it is too large Load Diff

View File

@@ -1,3 +1,9 @@
[workspace] [workspace]
members = [ "crates/*" ] members = [
resolver = "2" "como_bin",
"como_core",
"como_domain",
"como_infrastructure",
"como_gql",
"como_api",
]

View File

@@ -1,76 +1 @@
# Omnia # Cibus Backend
`Omnia` is a tool to provide a common platform for everything. It is a tool
build to support note-taking, handling personal relationships, project
development, todos, research, up-keep of external work, and much more.
This may seem like a large list of tasks, and it is. However, Omnia is designed
to be opinionated, and provide a minimalistic approach to each of the above.
Omnia is not a general purpose tool, text editing tool, it is designed to fit
into your existing toolstack, with its opinionated project structure, and
workflow.
Alternatives to this tool:
- Notion: with second brain templates etc.
- Obsidian: with zen garden templates etc.
Omnia is a commandline tool, which functions using a terminal ui, or pure cli
commands. Called `interactive` for the former, and `prompt` for the latter. It
is designed to work with your favorite EDITOR, to keep the scope of this project
reasonable, `Omnia` doesn't try to bundle with an editor. Instead it will
respect your `OMNIA_EDITOR`, or `EDITOR` environment variables, and launch the
files using those.
## How to use `Omnia`
To launch the fully interactive view, simply type `omnia` in your shell of
choice. This will boot up the TUI, press `?` for help and it will show a brief,
menu as well as the most common keybinds.
Following each command will be shown separately, these will be available in the
UI as well, just follow along in the menu, or use the command key `:` to open
the command palette.
### Commands
In Omnia everything is designed to use Markdown files, even the templates are
markdown files, though with some special syntax to make prompting easier. This
also means that you can open your local ~/omnia directory using your favorite
editor, as everything is just markdown files.
Projects are the cornerstone of how Omnia functions. Every navigation item is a
project, be they todo lists, research items, external sites etc. Projects can be
nested, and projects can contain pages. External apps can be configured as a
project, and will need a certain interface to be functional.
This means that when you type a command:
`omnia --help`, each subcommand will be a project, some keywords are reserved:
(todo, inbox, project, etc.), each project decides which commands are available,
and this is fully customizeable, through our plugin system. Though note that we
ship the default view with a set of preconfigured plugins:
- todo
- inbox
- projects
- areas
Typing each of the commands above will open the fully interactive tui:
`omnia todo`. Todo has a set of commands available to it: `omnia todo create` as
an example. This will open the prompt view, which will interactive ask the user
to fill out a form. These questions will also be available using commandline
flags as well.
### Views
Some of the projects are built as views, this may be a list of recent
notifications on github, apis of interest, metrics and whatnot.
## Remote first
Omnia will sync remote first in nearly all cases. The only exception is in
progress forms and whatnot. This is to keep complexity down, as well as making
sure Omnia is as easily crossplatform as possible. You should be able to use
`Omnia` from all your terminal capable devices. This sprung out of my own need
for having my notes available everywhere, without having conflicts.

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

39
como_bin/Cargo.toml Normal file
View File

@@ -0,0 +1,39 @@
[package]
name = "como_bin"
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" }
como_api = { path = "../como_api" }
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"] }

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

41
como_bin/src/main.rs Normal file
View File

@@ -0,0 +1,41 @@
use std::sync::Arc;
mod error;
use clap::Parser;
use anyhow::Context;
use como_api::router::Api;
use como_infrastructure::{
configs::AppConfig, database::ConnectionPoolManager, register::ServiceRegister,
};
use tracing_subscriber::{layer::SubscriberExt, util::SubscriberInitExt};
#[tokio::main]
async fn main() -> anyhow::Result<()> {
tracing::info!("Loading dotenv");
dotenv::dotenv()?;
let config = Arc::new(AppConfig::parse());
tracing_subscriber::registry()
.with(tracing_subscriber::EnvFilter::new(&config.rust_log))
.with(tracing_subscriber::fmt::layer())
.init();
let pool = ConnectionPoolManager::new_pool(&config.database_url, true).await?;
let service_register = ServiceRegister::new(pool, config.clone());
Api::run_api(
config.api_port,
&config.cors_origin,
service_register.clone(),
)
.await
.context("could not initialize API")?;
Ok(())
}

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,8 @@
-- Add migration script here
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)

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

@@ -0,0 +1,83 @@
use argon2::{password_hash::SaltString, Argon2, PasswordHash, PasswordHasher, PasswordVerifier};
use axum::async_trait;
use como_core::users::UserService;
use rand_core::OsRng;
use crate::database::ConnectionPool;
pub struct DefaultUserService {
pool: ConnectionPool,
}
impl DefaultUserService {
pub fn new(pool: ConnectionPool) -> Self {
Self { pool }
}
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 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<Option<String>> {
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),
}
}
}

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

@@ -1 +0,0 @@
/target

View File

@@ -1,15 +0,0 @@
[package]
name = "omnia"
version = "0.1.0"
edition = "2021"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[dependencies]
clap = { version = "4.4.11", features = ["string"] }
color-eyre = "0.6.2"
eyre = "0.6.10"
tokio = { version = "1.35.0", features = ["full"] }
tracing = { version = "0.1.40", features = ["log"] }
tracing-subscriber = { version = "0.3.18", features = ["tracing"] }
uuid = { version = "1.6.1", features = ["v4"] }

View File

@@ -1,94 +0,0 @@
use cli::CliCommand;
use inbox::services::Inbox;
#[tokio::main]
async fn main() -> eyre::Result<()> {
tracing_subscriber::fmt().init();
if let Err(e) = color_eyre::install() {
tracing::error!("failed to install color_eyre: {}", e);
}
let cmd = clap::Command::new("omnia");
let inbox = Inbox::new();
let cmd = cmd.subcommand(inbox.get_command());
let matches = cmd.get_matches();
match matches.subcommand() {
Some((name, args)) => {
tracing::debug!("executing: {}", name);
if inbox.matches_command(name) {
inbox.execute_command(args).await?;
}
}
None => {
tracing::info!("executing raw command")
}
}
Ok(())
}
pub mod cli {
use clap::{ArgMatches, Command};
pub trait CliCommand {
fn get_name(&self) -> String;
fn get_command(&self) -> Command {
Command::new(self.get_name())
}
fn matches_command(&self, name: &str) -> bool {
name == self.get_name()
}
async fn execute_command(&self, args: &ArgMatches) -> eyre::Result<()>;
}
}
pub mod inbox {
pub mod cli {}
pub mod domain {
#[derive(Debug, Clone)]
pub struct InboxItem {
pub title: String,
pub description: Option<String>,
pub project: Option<String>,
}
}
pub mod services {
use crate::cli::CliCommand;
use super::domain::*;
pub struct Inbox {}
impl Inbox {
pub fn new() -> Self {
Self {}
}
pub async fn add_item(&self, inbox_item: InboxItem) -> eyre::Result<uuid::Uuid> {
Ok(uuid::Uuid::new_v4())
}
}
impl CliCommand for Inbox {
fn get_name(&self) -> String {
"inbox".to_string()
}
async fn execute_command(&self, args: &clap::ArgMatches) -> eyre::Result<()> {
self.add_item(InboxItem {
title: "something".into(),
description: Some("description".into()),
project: Some("project".into()),
})
.await?;
Ok(())
}
}
}
}

19
cuddle.yaml Normal file
View File

@@ -0,0 +1,19 @@
# yaml-language-server: $schema=https://git.front.kjuulh.io/kjuulh/cuddle/raw/branch/main/schemas/base.json
base: "git@git.front.kjuulh.io:kjuulh/cuddle-rust-plan.git"
vars:
service: "como-backend"
deployments: "git@git.front.kjuulh.io:como/deployments.git"
scripts:
render_como_templates:
type: shell
local_up:
type: shell
local_down:
type: shell
run_como:
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

7
scripts/local_up.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 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

@@ -0,0 +1,10 @@
#!/bin/bash
set -e
deploymentrepo="$TMP/deployments"
CUDDLE_FETCH_POLICY=never cuddle_cli render_template \
--template-file "$TMP/.env.example.tmpl" \
--dest "$deploymentrepo/$SERVICE/env.example"

5
scripts/run_como.sh Executable file
View File

@@ -0,0 +1,5 @@
#!/bin/bash
set -e
(cd como_bin; cargo watch -x run)

View File

@@ -0,0 +1,4 @@
POSTGRES_DB=como
POSTGRES_USER=como
POSTGRES_PASSWORD=somenotverysecurepassword
DATABASE_URL="postgres://como:somenotverysecurepassword@localhost:5432/como"

View File

@@ -0,0 +1,7 @@
target/
.git/
.cuddle/
scripts/
cuddle.yaml
local.sh
README.md

View File

@@ -0,0 +1,17 @@
version: '3.7'
services:
db:
build:
context: .
dockerfile: local_up.Dockerfile
restart: always
environment:
- POSTGRES_PASSWORD=somenotverysecurepassword
ports:
- 5432:5432
volumes:
- 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/