From f72fc7751c33001bbff8c843ee67e150ad21384d Mon Sep 17 00:00:00 2001 From: vsilent Date: Mon, 28 Aug 2023 10:38:56 +0300 Subject: [PATCH 01/29] project structure changes for stack endpoint --- src/lib.rs | 3 +- src/models/mod.rs | 3 +- src/models/stack.rs | 13 ++++++++ src/routes/mod.rs | 12 +++----- src/routes/{add_stack.rs => stack/add.rs} | 15 +--------- src/routes/{ => stack}/deploy.rs | 0 src/routes/{get_stack.rs => stack/get.rs} | 2 +- src/routes/stack/mod.rs | 8 +++++ src/routes/stack/update.rs | 4 +++ src/services/mod.rs | 1 + src/services/stack.rs | 0 src/startup.rs | 36 +++++++++++++---------- 12 files changed, 56 insertions(+), 41 deletions(-) create mode 100644 src/models/stack.rs rename src/routes/{add_stack.rs => stack/add.rs} (76%) rename src/routes/{ => stack}/deploy.rs (100%) rename src/routes/{get_stack.rs => stack/get.rs} (96%) create mode 100644 src/routes/stack/mod.rs create mode 100644 src/routes/stack/update.rs create mode 100644 src/services/mod.rs create mode 100644 src/services/stack.rs diff --git a/src/lib.rs b/src/lib.rs index 54cbb91..bae3244 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -3,4 +3,5 @@ pub mod startup; pub mod configuration; pub mod telemetry; mod middleware; -mod models; \ No newline at end of file +mod models; +mod services; \ No newline at end of file diff --git a/src/models/mod.rs b/src/models/mod.rs index a4ece87..68367a8 100644 --- a/src/models/mod.rs +++ b/src/models/mod.rs @@ -1,2 +1,3 @@ mod rating; -mod user; \ No newline at end of file +mod user; +mod stack; \ No newline at end of file diff --git a/src/models/stack.rs b/src/models/stack.rs new file mode 100644 index 0000000..25e198d --- /dev/null +++ b/src/models/stack.rs @@ -0,0 +1,13 @@ +use uuid::Uuid; +use chrono::{DateTime, Utc}; + +pub struct Stack { + pub id: Uuid, // id - is a unique identifier for the app stack + pub stack_id: Uuid, // external stack ID + pub user_id: Uuid, // external unique identifier for the user + pub body: String, + pub created_at: DateTime, + pub updated_at: DateTime, +} + + diff --git a/src/routes/mod.rs b/src/routes/mod.rs index ad99270..0fc97bd 100644 --- a/src/routes/mod.rs +++ b/src/routes/mod.rs @@ -1,10 +1,6 @@ -mod get_stack; mod health_checks; -// mod add_stack; -// mod deploy; -mod rating; - -// pub use get_stack::*; pub use health_checks::*; -// pub use add_stack::*; -// crate::routes:: +mod rating; +pub use rating::*; +pub(crate) mod stack; +pub use stack::*; diff --git a/src/routes/add_stack.rs b/src/routes/stack/add.rs similarity index 76% rename from src/routes/add_stack.rs rename to src/routes/stack/add.rs index b86e648..609a0c0 100644 --- a/src/routes/add_stack.rs +++ b/src/routes/stack/add.rs @@ -2,19 +2,6 @@ use actix_web::{web, HttpResponse}; use sqlx::PgPool; use tracing::Instrument; use uuid::Uuid; -use chrono::{DateTime, Utc}; - - -pub struct Stack { - // that can be a stack or an app in the stack. feature, service, web app etc. - // id - is a unique identifier for the product - // user_id - is a unique identifier for the user - pub id: Uuid, - pub user_id: Uuid, - pub body: String, - pub created_at: DateTime, - pub updated_at: DateTime, -} #[derive(serde::Deserialize)] @@ -23,7 +10,7 @@ pub struct FormData { stack_json: String, } -pub async fn validate(form: web::Form, pool: web::Data) -> HttpResponse { +pub async fn add(form: web::Form, pool: web::Data) -> HttpResponse { let request_id = Uuid::new_v4(); let request_span = tracing::info_span!( "Validating a new stack", %request_id, diff --git a/src/routes/deploy.rs b/src/routes/stack/deploy.rs similarity index 100% rename from src/routes/deploy.rs rename to src/routes/stack/deploy.rs diff --git a/src/routes/get_stack.rs b/src/routes/stack/get.rs similarity index 96% rename from src/routes/get_stack.rs rename to src/routes/stack/get.rs index eff518b..d3fdba7 100644 --- a/src/routes/get_stack.rs +++ b/src/routes/stack/get.rs @@ -3,7 +3,7 @@ use actix_web::{web, HttpResponse}; use sqlx::PgPool; // use uuid::Uuid; -pub async fn get_stack( +pub async fn get( id: web::Path, pool: web::Data, ) -> HttpResponse { diff --git a/src/routes/stack/mod.rs b/src/routes/stack/mod.rs new file mode 100644 index 0000000..ae5008a --- /dev/null +++ b/src/routes/stack/mod.rs @@ -0,0 +1,8 @@ +mod add; +mod deploy; +mod get; +mod update; +pub use add::*; +pub use update::*; +pub use deploy::*; +pub use get::*; diff --git a/src/routes/stack/update.rs b/src/routes/stack/update.rs new file mode 100644 index 0000000..5a4fa0c --- /dev/null +++ b/src/routes/stack/update.rs @@ -0,0 +1,4 @@ +use actix_web::HttpResponse; +pub async fn update() -> HttpResponse { + unimplemented!() +} diff --git a/src/services/mod.rs b/src/services/mod.rs new file mode 100644 index 0000000..4d551f8 --- /dev/null +++ b/src/services/mod.rs @@ -0,0 +1 @@ +mod stack; \ No newline at end of file diff --git a/src/services/stack.rs b/src/services/stack.rs new file mode 100644 index 0000000..e69de29 diff --git a/src/startup.rs b/src/startup.rs index 74a0e46..f93261b 100644 --- a/src/startup.rs +++ b/src/startup.rs @@ -1,7 +1,7 @@ use actix_web::dev::Server; use actix_web::middleware::Logger; use actix_web::{ - http::header::HeaderName, + // http::header::HeaderName, web::{self, Form}, App, HttpServer, }; @@ -23,21 +23,25 @@ pub fn run(listener: TcpListener, db_pool: PgPool) -> Result Date: Fri, 1 Sep 2023 15:35:11 +0300 Subject: [PATCH 02/29] id field type to i32 --- Cargo.lock | 1 + Cargo.toml | 1 + migrations/20230604060546_create_user_stack_table.sql | 10 ++++++---- src/models/stack.rs | 1 + src/routes/stack/add.rs | 11 +++++++---- src/routes/stack/get.rs | 4 ++-- 6 files changed, 18 insertions(+), 10 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 33d7898..b0d6055 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1898,6 +1898,7 @@ dependencies = [ "reqwest", "serde", "sqlx", + "thiserror", "tokio", "tracing", "tracing-bunyan-formatter", diff --git a/Cargo.toml b/Cargo.toml index b55616f..061583c 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -24,6 +24,7 @@ tracing-bunyan-formatter = "0.3.8" tracing-log = "0.1.3" tracing-subscriber = { version = "0.3.17", features = ["registry", "env-filter"] } uuid = { version = "1.3.4", features = ["v4"] } +thiserror = "1.0" [dependencies.sqlx] version = "0.6.3" diff --git a/migrations/20230604060546_create_user_stack_table.sql b/migrations/20230604060546_create_user_stack_table.sql index 0039965..51e7530 100644 --- a/migrations/20230604060546_create_user_stack_table.sql +++ b/migrations/20230604060546_create_user_stack_table.sql @@ -1,9 +1,11 @@ -- Add migration script here CREATE TABLE user_stack ( - id uuid NOT NULL, PRIMARY KEY(id), - user_id TEXT NOT NULL, + id integer NOT NULL, PRIMARY KEY(id), + stack_id integer NOT NULL, + user_id integer NOT NULL, name TEXT NOT NULL, body JSON NOT NULL, - created_at timestamptz NOT NULL + created_at timestamptz NOT NULL, updated_at timestamptz NOT NULL -) \ No newline at end of file +) + diff --git a/src/models/stack.rs b/src/models/stack.rs index 25e198d..40255dd 100644 --- a/src/models/stack.rs +++ b/src/models/stack.rs @@ -5,6 +5,7 @@ pub struct Stack { pub id: Uuid, // id - is a unique identifier for the app stack pub stack_id: Uuid, // external stack ID pub user_id: Uuid, // external unique identifier for the user + pub name: String, pub body: String, pub created_at: DateTime, pub updated_at: DateTime, diff --git a/src/routes/stack/add.rs b/src/routes/stack/add.rs index 609a0c0..7a1bc74 100644 --- a/src/routes/stack/add.rs +++ b/src/routes/stack/add.rs @@ -2,11 +2,14 @@ use actix_web::{web, HttpResponse}; use sqlx::PgPool; use tracing::Instrument; use uuid::Uuid; +use chrono::Utc; #[derive(serde::Deserialize)] pub struct FormData { - id: String, + id: i32, + user_id: i32, + stack_name: String, stack_json: String, } @@ -15,7 +18,7 @@ pub async fn add(form: web::Form, pool: web::Data) -> HttpResp let request_span = tracing::info_span!( "Validating a new stack", %request_id, user_id=?form.user_id, - stack_json=?form.stack + stack_json=?form.stack_json ); // using `enter` is an async function @@ -37,9 +40,9 @@ pub async fn add(form: web::Form, pool: web::Data) -> HttpResp INSERT INTO user_stack (id, user_id, name, created_at, updated_at) VALUES ($1, $2, $3, $4, $5) "#, - Uuid::new_v4(), + 0_i32, form.user_id, - stack_name, + form.stack_name, Utc::now(), Utc::now() ) diff --git a/src/routes/stack/get.rs b/src/routes/stack/get.rs index d3fdba7..734f22b 100644 --- a/src/routes/stack/get.rs +++ b/src/routes/stack/get.rs @@ -15,13 +15,13 @@ pub async fn get( SELECT id FROM user_stack WHERE id=$1 "#, - id + id.parse::().unwrap() ) .fetch_one(pool.get_ref()) .await { Ok(_) => { - tracing::info!("Stack found by id {}", email); + tracing::info!("Stack found by id {}", id); HttpResponse::Ok().finish() } Err(e) => { From eea24ba7f43dd54e26ce6ccc4428cd048a3b31cf Mon Sep 17 00:00:00 2001 From: vsilent Date: Sun, 3 Sep 2023 08:48:20 +0300 Subject: [PATCH 03/29] test payload fields --- src/routes/stack/add.rs | 19 ++++++++++--------- 1 file changed, 10 insertions(+), 9 deletions(-) diff --git a/src/routes/stack/add.rs b/src/routes/stack/add.rs index 7a1bc74..fb653ed 100644 --- a/src/routes/stack/add.rs +++ b/src/routes/stack/add.rs @@ -7,18 +7,19 @@ use chrono::Utc; #[derive(serde::Deserialize)] pub struct FormData { - id: i32, - user_id: i32, - stack_name: String, - stack_json: String, + commonDomain: String, + region: String, + domainList: String, + user_id: i32 } pub async fn add(form: web::Form, pool: web::Data) -> HttpResponse { let request_id = Uuid::new_v4(); let request_span = tracing::info_span!( "Validating a new stack", %request_id, - user_id=?form.user_id, - stack_json=?form.stack_json + commonDomain=?form.commonDomain, + region=?form.region, + domainList=?form.domainList ); // using `enter` is an async function @@ -27,8 +28,8 @@ pub async fn add(form: web::Form, pool: web::Data) -> HttpResp tracing::info!( "request_id {} Adding '{}' '{}' as a new stack", request_id, - form.user_id, - form.stack_json + form.commonDomain, + form.region ); let query_span = tracing::info_span!( @@ -42,7 +43,7 @@ pub async fn add(form: web::Form, pool: web::Data) -> HttpResp "#, 0_i32, form.user_id, - form.stack_name, + form.commonDomain, Utc::now(), Utc::now() ) From 09fdb06c623b336f3accf93ec6d30cd327d1ccd8 Mon Sep 17 00:00:00 2001 From: vsilent Date: Sun, 3 Sep 2023 11:07:24 +0300 Subject: [PATCH 04/29] validate add rating request, added server_valid --- Cargo.lock | 76 ++++++++++++++++++++++++++++++++++++++++++++ Cargo.toml | 1 + src/models/mod.rs | 2 +- src/models/rating.rs | 15 +++++++++ src/routes/rating.rs | 43 ++++++++++++++++++++++--- 5 files changed, 131 insertions(+), 6 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 33d7898..17d2a6a 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -932,6 +932,7 @@ checksum = "bd070e393353796e801d209ad339e89596eb4c8d430d18ede6a1cced8fafbd99" dependencies = [ "autocfg", "hashbrown 0.12.3", + "serde", ] [[package]] @@ -1382,6 +1383,30 @@ version = "0.2.17" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5b40af805b3121feab8a3c29f04d8ad262fa8e0561883e7653e024ae4479e6de" +[[package]] +name = "proc-macro-error" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "da25490ff9892aab3fcf7c36f08cfb902dd3e71ca0f9f9517bea02a73a5ce38c" +dependencies = [ + "proc-macro-error-attr", + "proc-macro2", + "quote", + "syn 1.0.109", + "version_check", +] + +[[package]] +name = "proc-macro-error-attr" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a1be40180e52ecc98ad80b184934baf3d0d29f979574e439af5a55274b35f869" +dependencies = [ + "proc-macro2", + "quote", + "version_check", +] + [[package]] name = "proc-macro2" version = "1.0.56" @@ -1711,6 +1736,50 @@ dependencies = [ "serde", ] +[[package]] +name = "serde_valid" +version = "0.16.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0adc7a19d45e581abc6d169c865a0b14b84bb43a9e966d1cca4d733e70f7f35a" +dependencies = [ + "indexmap", + "itertools", + "num-traits", + "once_cell", + "paste", + "regex", + "serde", + "serde_json", + "serde_valid_derive", + "serde_valid_literal", + "thiserror", + "unicode-segmentation", +] + +[[package]] +name = "serde_valid_derive" +version = "0.16.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "071237362e267e2a76ffe4434094e089dcd8b5e9d8423ada499e5550dcb0181d" +dependencies = [ + "paste", + "proc-macro-error", + "proc-macro2", + "quote", + "strsim", + "syn 1.0.109", +] + +[[package]] +name = "serde_valid_literal" +version = "0.16.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f57df292b1d64449f90794fc7a67efca0b21acca91493e64a46418a29bbe36b4" +dependencies = [ + "paste", + "regex", +] + [[package]] name = "sha1" version = "0.10.5" @@ -1897,6 +1966,7 @@ dependencies = [ "config", "reqwest", "serde", + "serde_valid", "sqlx", "tokio", "tracing", @@ -1916,6 +1986,12 @@ dependencies = [ "unicode-normalization", ] +[[package]] +name = "strsim" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "73473c0e59e6d5812c5dfe2a064a6444949f089e20eec9a2e5506596494e4623" + [[package]] name = "subtle" version = "2.4.1" diff --git a/Cargo.toml b/Cargo.toml index b55616f..87d2d31 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -18,6 +18,7 @@ chrono = { version = "0.4.26", features = ["time"] } config = "0.13.3" reqwest = { version = "0.11.17", features = ["json"] } serde = { version = "1.0.162", features = ["derive"] } +serde_valid = "0.16.3" tokio = { version = "1.28.1", features = ["full"] } tracing = { version = "0.1.37", features = ["log"] } tracing-bunyan-formatter = "0.3.8" diff --git a/src/models/mod.rs b/src/models/mod.rs index a4ece87..d84007d 100644 --- a/src/models/mod.rs +++ b/src/models/mod.rs @@ -1,2 +1,2 @@ -mod rating; +pub mod rating; mod user; \ No newline at end of file diff --git a/src/models/rating.rs b/src/models/rating.rs index 7b30c97..290287a 100644 --- a/src/models/rating.rs +++ b/src/models/rating.rs @@ -1,5 +1,6 @@ use uuid::Uuid; use chrono::{DateTime, Utc}; +use serde::{Deserialize, Serialize}; pub struct Product { // Product - is an external object that we want to store in the database, @@ -29,6 +30,20 @@ pub struct Rating { pub updated_at: DateTime, } + +#[derive(Serialize, Deserialize, Debug)] +pub enum RateCategory { + Application, // app, feature, extension + Cloud, // is user satisfied working with this cloud + Stack, // app stack + DeploymentSpeed, + Documentation, + Design, + TechSupport, + Price, + MemoryUsage +} + pub struct Rules { //-> Product.id // example: allow to add only a single comment diff --git a/src/routes/rating.rs b/src/routes/rating.rs index 8e2b2ab..ddc1439 100644 --- a/src/routes/rating.rs +++ b/src/routes/rating.rs @@ -1,21 +1,54 @@ use actix_web::{web, HttpResponse}; use serde::{Deserialize, Serialize}; - +use crate::models::rating::RateCategory; +use serde_valid::Validate; +use sqlx::PgPool; // workflow // add, update, list, get(user_id), ACL, // ACL - access to func for a user // ACL - access to objects for a user -#[derive(Serialize, Deserialize, Debug)] +#[derive(Serialize, Deserialize, Debug, Validate)] pub struct RatingForm { pub obj_id: u32, // product external id - pub category: String, // rating of product | rating of service etc - pub comment: String, // always linked to a product + pub category: RateCategory, // rating of product | rating of service etc + #[validate(max_length = 1000)] + pub comment: Option, // always linked to a product + #[validate(minimum = 0)] + #[validate(maximum = 10)] pub rate: u32, // } -pub async fn rating(form: web::Json) -> HttpResponse { +pub async fn rating(form: web::Json, pool: web::Data) -> HttpResponse { + let user_id = 1; // Let's assume we have a user id already taken from auth + + // Get product by id + // Insert rating + + // match sqlx::query!( + // r#" + // INSERT INTO rating () + // VALUES ($1, $2, $3, $4) + // "#, + // args + // ) + // .execute(pool.get_ref()) + // .instrument(query_span) + // .await + // { + // Ok(_) => { + // tracing::info!( + // "req_id: {} New subscriber details have been saved to database", + // request_id + // ); + // HttpResponse::Ok().finish() + // } + // Err(e) => { + // tracing::error!("req_id: {} Failed to execute query: {:?}", request_id, e); + // HttpResponse::InternalServerError().finish() + // } + // } println!("{:?}", form); HttpResponse::Ok().finish() } From 6d4fdf78c7e3ccda9f0a30064916bd19c6de0b06 Mon Sep 17 00:00:00 2001 From: vsilent Date: Tue, 5 Sep 2023 17:40:06 +0300 Subject: [PATCH 05/29] validate text field --- src/routes/rating.rs | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/routes/rating.rs b/src/routes/rating.rs index ddc1439..bdd0038 100644 --- a/src/routes/rating.rs +++ b/src/routes/rating.rs @@ -11,13 +11,13 @@ use sqlx::PgPool; #[derive(Serialize, Deserialize, Debug, Validate)] pub struct RatingForm { - pub obj_id: u32, // product external id - pub category: RateCategory, // rating of product | rating of service etc + pub obj_id: u32, // product external id + pub category: RateCategory, // rating of product | rating of service etc #[validate(max_length = 1000)] - pub comment: Option, // always linked to a product + pub comment: Option, // always linked to a product #[validate(minimum = 0)] #[validate(maximum = 10)] - pub rate: u32, // + pub rate: u32, // } pub async fn rating(form: web::Json, pool: web::Data) -> HttpResponse { From 880c9e611f30a2eb532692368e1c2fadd8d83511 Mon Sep 17 00:00:00 2001 From: vsilent Date: Tue, 5 Sep 2023 17:44:26 +0300 Subject: [PATCH 06/29] health_check, cleanup --- tests/health_check.rs | 56 ------------------------------------------- 1 file changed, 56 deletions(-) diff --git a/tests/health_check.rs b/tests/health_check.rs index fac89b7..f2a8323 100644 --- a/tests/health_check.rs +++ b/tests/health_check.rs @@ -87,59 +87,3 @@ async fn spawn_app() -> TestApp { db_pool: connection_pool, } } - -#[tokio::test] -async fn subscribe_returns_a_200_for_valid_form_data() { - // Arrange - let app = spawn_app().await; - let client = reqwest::Client::new(); - - let body = "name=le%20guin&email=ursula_le_guin%40gmail.com"; // %20 - space, %40 - @ - let response = client - .post(&format!("{}/subscriptions", &app.address)) - .header("Content-Type", "application/x-www-form-urlencoded") - .body(body) - .send() - .await - .expect("Failed to execute request."); - - assert_eq!(200, response.status().as_u16()); - - let saved = sqlx::query!("SELECT email, name FROM subscriptions",) - .fetch_one(&app.db_pool) - .await - .expect("Failed to fetch saved subscription."); - - assert_eq!(saved.email, "ursula_le_guin@gmail.com"); - assert_eq!(saved.name, "le guin"); -} - -#[tokio::test] -async fn subscribe_returns_a_400_when_data_is_missing() { - // Arrange - let app = spawn_app().await; - let client = reqwest::Client::new(); - - let test_cases = vec![ - ("name=le%20guin", "missing the email"), - ("email=ursula_le_guin%40gmail.com", "missing the name"), - ("", "missing both name and email"), - ]; - - for (invalid_body, error_message) in test_cases { - let response = client - .post(&format!("{}/subscriptions", &app.address)) - .header("Content-Type", "application/x-www-form-urlencoded") - .body(invalid_body) - .send() - .await - .expect("Failed to execute request."); - - assert_eq!( - 400, - response.status().as_u16(), - "The API did not fail with 400 Bad Request when the payload was {}.", - error_message - ); - } -} From e94766aca3b0653bde990011262587c644beabd7 Mon Sep 17 00:00:00 2001 From: vsilent Date: Tue, 5 Sep 2023 17:58:01 +0300 Subject: [PATCH 07/29] import structure and files related to rating from dev branch --- Cargo.lock | 76 +++++++++++++++++++ Cargo.toml | 1 + README.md | 24 +++++- ...0903063840_creating_rating_tables.down.sql | 8 ++ ...230903063840_creating_rating_tables.up.sql | 28 +++++++ ...30905145525_creating_stack_tables.down.sql | 3 + ...230905145525_creating_stack_tables.up.sql} | 1 + src/models/mod.rs | 6 +- src/models/rating.rs | 17 ++++- src/routes/mod.rs | 3 +- src/routes/rating.rs | 50 +++++++++++- src/startup.rs | 15 ++-- 12 files changed, 213 insertions(+), 19 deletions(-) create mode 100644 migrations/20230903063840_creating_rating_tables.down.sql create mode 100644 migrations/20230903063840_creating_rating_tables.up.sql create mode 100644 migrations/20230905145525_creating_stack_tables.down.sql rename migrations/{20230604060546_create_user_stack_table.sql => 20230905145525_creating_stack_tables.up.sql} (89%) diff --git a/Cargo.lock b/Cargo.lock index b0d6055..e12cd47 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -932,6 +932,7 @@ checksum = "bd070e393353796e801d209ad339e89596eb4c8d430d18ede6a1cced8fafbd99" dependencies = [ "autocfg", "hashbrown 0.12.3", + "serde", ] [[package]] @@ -1382,6 +1383,30 @@ version = "0.2.17" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5b40af805b3121feab8a3c29f04d8ad262fa8e0561883e7653e024ae4479e6de" +[[package]] +name = "proc-macro-error" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "da25490ff9892aab3fcf7c36f08cfb902dd3e71ca0f9f9517bea02a73a5ce38c" +dependencies = [ + "proc-macro-error-attr", + "proc-macro2", + "quote", + "syn 1.0.109", + "version_check", +] + +[[package]] +name = "proc-macro-error-attr" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a1be40180e52ecc98ad80b184934baf3d0d29f979574e439af5a55274b35f869" +dependencies = [ + "proc-macro2", + "quote", + "version_check", +] + [[package]] name = "proc-macro2" version = "1.0.56" @@ -1711,6 +1736,50 @@ dependencies = [ "serde", ] +[[package]] +name = "serde_valid" +version = "0.16.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0adc7a19d45e581abc6d169c865a0b14b84bb43a9e966d1cca4d733e70f7f35a" +dependencies = [ + "indexmap", + "itertools", + "num-traits", + "once_cell", + "paste", + "regex", + "serde", + "serde_json", + "serde_valid_derive", + "serde_valid_literal", + "thiserror", + "unicode-segmentation", +] + +[[package]] +name = "serde_valid_derive" +version = "0.16.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "071237362e267e2a76ffe4434094e089dcd8b5e9d8423ada499e5550dcb0181d" +dependencies = [ + "paste", + "proc-macro-error", + "proc-macro2", + "quote", + "strsim", + "syn 1.0.109", +] + +[[package]] +name = "serde_valid_literal" +version = "0.16.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f57df292b1d64449f90794fc7a67efca0b21acca91493e64a46418a29bbe36b4" +dependencies = [ + "paste", + "regex", +] + [[package]] name = "sha1" version = "0.10.5" @@ -1897,6 +1966,7 @@ dependencies = [ "config", "reqwest", "serde", + "serde_valid", "sqlx", "thiserror", "tokio", @@ -1917,6 +1987,12 @@ dependencies = [ "unicode-normalization", ] +[[package]] +name = "strsim" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "73473c0e59e6d5812c5dfe2a064a6444949f089e20eec9a2e5506596494e4623" + [[package]] name = "subtle" version = "2.4.1" diff --git a/Cargo.toml b/Cargo.toml index 061583c..d48e7c5 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -25,6 +25,7 @@ tracing-log = "0.1.3" tracing-subscriber = { version = "0.3.17", features = ["registry", "env-filter"] } uuid = { version = "1.3.4", features = ["v4"] } thiserror = "1.0" +serde_valid = "0.16.3" [dependencies.sqlx] version = "0.6.3" diff --git a/README.md b/README.md index 14de5f5..0a33450 100644 --- a/README.md +++ b/README.md @@ -1 +1,23 @@ -# stacker \ No newline at end of file +# stacker + + +Run db migration +``` +sqlx migrate run + +``` + +Down migration + +``` +sqlx migrate revert +``` + + +Add rating + +``` + + curl -vX POST 'http://localhost:8000/rating' -d '{"obj_id": 111, "category": "application", "comment":"some comment", "rate": 10}' --header 'Content-Type: application/json' + +``` \ No newline at end of file diff --git a/migrations/20230903063840_creating_rating_tables.down.sql b/migrations/20230903063840_creating_rating_tables.down.sql new file mode 100644 index 0000000..403a002 --- /dev/null +++ b/migrations/20230903063840_creating_rating_tables.down.sql @@ -0,0 +1,8 @@ +-- Add down migration script here + +DROP INDEX idx_category; +DROP INDEX idx_user_id; +DROP INDEX idx_product_id_rating_id; + +DROP table rating; +DROP table product; diff --git a/migrations/20230903063840_creating_rating_tables.up.sql b/migrations/20230903063840_creating_rating_tables.up.sql new file mode 100644 index 0000000..c693139 --- /dev/null +++ b/migrations/20230903063840_creating_rating_tables.up.sql @@ -0,0 +1,28 @@ +-- Add up migration script here + +CREATE TABLE product ( + id integer NOT NULL, PRIMARY KEY(id), + obj_id integer NOT NULL, + obj_type TEXT NOT NULL, + created_at timestamptz NOT NULL, + updated_at timestamptz NOT NULL +); + +CREATE TABLE rating ( + id integer NOT NULL, PRIMARY KEY(id), + user_id uuid NOT NULL, + product_id integer NOT NULL, + category VARCHAR(255) NOT NULL, + comment TEXT DEFAULT NULL, + hidden BOOLEAN DEFAULT FALSE, + rate INTEGER, + created_at timestamptz NOT NULL, + updated_at timestamptz NOT NULL, + CONSTRAINT fk_product + FOREIGN KEY(product_id) + REFERENCES product(id) +); + +CREATE INDEX idx_category ON rating(category); +CREATE INDEX idx_user_id ON rating(user_id); +CREATE INDEX idx_product_id_rating_id ON rating(product_id, rate); \ No newline at end of file diff --git a/migrations/20230905145525_creating_stack_tables.down.sql b/migrations/20230905145525_creating_stack_tables.down.sql new file mode 100644 index 0000000..203a95a --- /dev/null +++ b/migrations/20230905145525_creating_stack_tables.down.sql @@ -0,0 +1,3 @@ +-- Add down migration script here + +DROP TABLE user_stack; diff --git a/migrations/20230604060546_create_user_stack_table.sql b/migrations/20230905145525_creating_stack_tables.up.sql similarity index 89% rename from migrations/20230604060546_create_user_stack_table.sql rename to migrations/20230905145525_creating_stack_tables.up.sql index 51e7530..eab6cf2 100644 --- a/migrations/20230604060546_create_user_stack_table.sql +++ b/migrations/20230905145525_creating_stack_tables.up.sql @@ -1,3 +1,4 @@ +-- Add up migration script here -- Add migration script here CREATE TABLE user_stack ( id integer NOT NULL, PRIMARY KEY(id), diff --git a/src/models/mod.rs b/src/models/mod.rs index 68367a8..7595e12 100644 --- a/src/models/mod.rs +++ b/src/models/mod.rs @@ -1,3 +1,3 @@ -mod rating; -mod user; -mod stack; \ No newline at end of file +pub mod rating; +pub mod user; +pub mod stack; \ No newline at end of file diff --git a/src/models/rating.rs b/src/models/rating.rs index 9606a77..290287a 100644 --- a/src/models/rating.rs +++ b/src/models/rating.rs @@ -1,5 +1,6 @@ use uuid::Uuid; use chrono::{DateTime, Utc}; +use serde::{Deserialize, Serialize}; pub struct Product { // Product - is an external object that we want to store in the database, @@ -21,7 +22,7 @@ pub struct Product { pub struct Rating { pub id: i32, pub user_id: Uuid, // external user_id, 100, taken using token (middleware?) - pub category: String, + pub category: String, // rating of product | rating of service etc pub comment: String, // always linked to a product pub hidden: bool, // rating can be hidden for non-adequate user behaviour pub rate: u32, @@ -29,6 +30,20 @@ pub struct Rating { pub updated_at: DateTime, } + +#[derive(Serialize, Deserialize, Debug)] +pub enum RateCategory { + Application, // app, feature, extension + Cloud, // is user satisfied working with this cloud + Stack, // app stack + DeploymentSpeed, + Documentation, + Design, + TechSupport, + Price, + MemoryUsage +} + pub struct Rules { //-> Product.id // example: allow to add only a single comment diff --git a/src/routes/mod.rs b/src/routes/mod.rs index 0fc97bd..46433c7 100644 --- a/src/routes/mod.rs +++ b/src/routes/mod.rs @@ -1,6 +1,7 @@ mod health_checks; -pub use health_checks::*; mod rating; + +pub use health_checks::*; pub use rating::*; pub(crate) mod stack; pub use stack::*; diff --git a/src/routes/rating.rs b/src/routes/rating.rs index 871f255..bdd0038 100644 --- a/src/routes/rating.rs +++ b/src/routes/rating.rs @@ -1,10 +1,54 @@ -use actix_web::HttpResponse; +use actix_web::{web, HttpResponse}; +use serde::{Deserialize, Serialize}; +use crate::models::rating::RateCategory; +use serde_valid::Validate; +use sqlx::PgPool; // workflow // add, update, list, get(user_id), ACL, // ACL - access to func for a user // ACL - access to objects for a user -pub async fn rating() -> HttpResponse { - unimplemented!() +#[derive(Serialize, Deserialize, Debug, Validate)] +pub struct RatingForm { + pub obj_id: u32, // product external id + pub category: RateCategory, // rating of product | rating of service etc + #[validate(max_length = 1000)] + pub comment: Option, // always linked to a product + #[validate(minimum = 0)] + #[validate(maximum = 10)] + pub rate: u32, // +} + +pub async fn rating(form: web::Json, pool: web::Data) -> HttpResponse { + let user_id = 1; // Let's assume we have a user id already taken from auth + + // Get product by id + // Insert rating + + // match sqlx::query!( + // r#" + // INSERT INTO rating () + // VALUES ($1, $2, $3, $4) + // "#, + // args + // ) + // .execute(pool.get_ref()) + // .instrument(query_span) + // .await + // { + // Ok(_) => { + // tracing::info!( + // "req_id: {} New subscriber details have been saved to database", + // request_id + // ); + // HttpResponse::Ok().finish() + // } + // Err(e) => { + // tracing::error!("req_id: {} Failed to execute query: {:?}", request_id, e); + // HttpResponse::InternalServerError().finish() + // } + // } + println!("{:?}", form); + HttpResponse::Ok().finish() } diff --git a/src/startup.rs b/src/startup.rs index f93261b..1803d6b 100644 --- a/src/startup.rs +++ b/src/startup.rs @@ -20,22 +20,17 @@ pub fn run(listener: TcpListener, db_pool: PgPool) -> Result Date: Sun, 10 Sep 2023 09:09:00 +0300 Subject: [PATCH 08/29] add.rs --- .env | 2 +- Cargo.lock | 44 +++---- Cargo.toml | 2 + configuration.yaml | 2 +- custom-stack-payload.json | 1 + scripts/init_db.sh | 2 +- src/models/stack.rs | 245 ++++++++++++++++++++++++++++++++++++++ src/routes/stack/add.rs | 13 +- src/routes/stack/mod.rs | 8 +- src/startup.rs | 17 ++- 10 files changed, 293 insertions(+), 43 deletions(-) create mode 100644 custom-stack-payload.json diff --git a/.env b/.env index 8538b56..f6e3b68 100644 --- a/.env +++ b/.env @@ -1 +1 @@ -DATABASE_URL=postgres://postgres:postgres@localhost:5432/newsletter \ No newline at end of file +DATABASE_URL=postgres://postgres:postgres@localhost:5432/stacker \ No newline at end of file diff --git a/Cargo.lock b/Cargo.lock index e12cd47..908b64a 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -259,7 +259,7 @@ checksum = "b9ccdd8f2a161be9bd5c023df56f1b2a0bd1d83872ae53b71a84a12c9bf6e842" dependencies = [ "proc-macro2", "quote", - "syn 2.0.15", + "syn 2.0.31", ] [[package]] @@ -1212,7 +1212,7 @@ checksum = "a948666b637a0f465e8564c73e89d4dde00d72d4d473cc972f390fc3dcee7d9c" dependencies = [ "proc-macro2", "quote", - "syn 2.0.15", + "syn 2.0.31", ] [[package]] @@ -1345,7 +1345,7 @@ dependencies = [ "pest_meta", "proc-macro2", "quote", - "syn 2.0.15", + "syn 2.0.31", ] [[package]] @@ -1409,18 +1409,18 @@ dependencies = [ [[package]] name = "proc-macro2" -version = "1.0.56" +version = "1.0.66" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2b63bdb0cd06f1f4dedf69b254734f9b45af66e4a031e42a7480257d9898b435" +checksum = "18fb31db3f9bddb2ea821cde30a9f70117e3f119938b5ee630b7403aa6e2ead9" dependencies = [ "unicode-ident", ] [[package]] name = "quote" -version = "1.0.26" +version = "1.0.33" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4424af4bf778aae2051a77b60283332f386554255d722233d09fbfc7e30da2fc" +checksum = "5267fca4496028628a95160fc423a33e8b2e6af8a5302579e322e4b520293cae" dependencies = [ "proc-macro2", ] @@ -1695,29 +1695,29 @@ checksum = "bebd363326d05ec3e2f532ab7660680f3b02130d780c299bca73469d521bc0ed" [[package]] name = "serde" -version = "1.0.162" +version = "1.0.188" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "71b2f6e1ab5c2b98c05f0f35b236b22e8df7ead6ffbf51d7808da7f8817e7ab6" +checksum = "cf9e0fcba69a370eed61bcf2b728575f726b50b55cba78064753d708ddc7549e" dependencies = [ "serde_derive", ] [[package]] name = "serde_derive" -version = "1.0.162" +version = "1.0.188" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a2a0814352fd64b58489904a44ea8d90cb1a91dcb6b4f5ebabc32c8318e93cb6" +checksum = "4eca7ac642d82aa35b60049a6eccb4be6be75e599bd2e9adb5f875a737654af2" dependencies = [ "proc-macro2", "quote", - "syn 2.0.15", + "syn 2.0.31", ] [[package]] name = "serde_json" -version = "1.0.96" +version = "1.0.105" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "057d394a50403bcac12672b2b18fb387ab6d289d957dab67dd201875391e52f1" +checksum = "693151e1ac27563d6dbcec9dee9fbd5da8539b20fa14ad3752b2e6d363ace360" dependencies = [ "itoa", "ryu", @@ -1966,6 +1966,8 @@ dependencies = [ "config", "reqwest", "serde", + "serde_derive", + "serde_json", "serde_valid", "sqlx", "thiserror", @@ -2012,9 +2014,9 @@ dependencies = [ [[package]] name = "syn" -version = "2.0.15" +version = "2.0.31" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a34fcf3e8b60f57e6a14301a2e916d323af98b0ea63c599441eec8558660c822" +checksum = "718fa2415bcb8d8bd775917a1bf12a7931b6dfa890753378538118181e0cb398" dependencies = [ "proc-macro2", "quote", @@ -2051,7 +2053,7 @@ checksum = "f9456a42c5b0d803c8cd86e73dd7cc9edd429499f37a3550d286d5e86720569f" dependencies = [ "proc-macro2", "quote", - "syn 2.0.15", + "syn 2.0.31", ] [[package]] @@ -2144,7 +2146,7 @@ checksum = "630bdcf245f78637c13ec01ffae6187cca34625e8c63150d424b59e55af2675e" dependencies = [ "proc-macro2", "quote", - "syn 2.0.15", + "syn 2.0.31", ] [[package]] @@ -2229,7 +2231,7 @@ checksum = "5f4f31f56159e98206da9efd823404b79b6ef3143b4a7ab76e67b1751b25a4ab" dependencies = [ "proc-macro2", "quote", - "syn 2.0.15", + "syn 2.0.31", ] [[package]] @@ -2427,7 +2429,7 @@ dependencies = [ "once_cell", "proc-macro2", "quote", - "syn 2.0.15", + "syn 2.0.31", "wasm-bindgen-shared", ] @@ -2461,7 +2463,7 @@ checksum = "4783ce29f09b9d93134d41297aded3a712b7b979e9c6f28c32cb88c973a94869" dependencies = [ "proc-macro2", "quote", - "syn 2.0.15", + "syn 2.0.31", "wasm-bindgen-backend", "wasm-bindgen-shared", ] diff --git a/Cargo.toml b/Cargo.toml index d48e7c5..049cbfd 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -26,6 +26,8 @@ tracing-subscriber = { version = "0.3.17", features = ["registry", "env-filter"] uuid = { version = "1.3.4", features = ["v4"] } thiserror = "1.0" serde_valid = "0.16.3" +serde_json = "1.0.105" +serde_derive = "1.0.188" [dependencies.sqlx] version = "0.6.3" diff --git a/configuration.yaml b/configuration.yaml index 70fe16f..dbd16f2 100644 --- a/configuration.yaml +++ b/configuration.yaml @@ -4,4 +4,4 @@ database: port: 5432 username: postgres password: "postgres" - database_name: newsletter \ No newline at end of file + database_name: stacker \ No newline at end of file diff --git a/custom-stack-payload.json b/custom-stack-payload.json new file mode 100644 index 0000000..57dbd93 --- /dev/null +++ b/custom-stack-payload.json @@ -0,0 +1 @@ +{"commonDomain":"","domainList":{},"region":"fsn1","zone":null,"server":"cx21","os":"ubuntu-20.04","ssl":"letsencrypt","vars":[],"integrated_features":[],"extended_features":[],"subscriptions":["stack_migration","stack_health_monitoring","stack_security_monitoring"],"save_token":true,"cloud_token":"r6LAjqrynVt7pUwctVkzBlJmKjLOCxJIWjZFMLTkPYCCB4rsgphhEVhiL4DuO757","provider":"htz","stack_code":"custom-stack","selected_plan":"plan-individual-monthly","custom":{"web":[{"name":"smarty database","code":"smarty-database","domain":"smarty-db.example.com","sharedPorts":["6532"],"versions":[],"custom":true,"type":"feature","main":true,"_id":"lm0gdh732y2qrojfl","dockerhub_user":"trydirect","dockerhub_name":"smarty-db","ram_size":"1Gb","cpu":1,"disk_size":"1Gb"}],"feature":[{"_etag":null,"_id":235,"_created":"2023-08-11T07:07:12.123355","_updated":"2023-08-15T13:07:30.597485","name":"Nginx Proxy Manager","code":"nginx_proxy_manager","role":["nginx_proxy_manager"],"type":"feature","default":null,"popularity":null,"descr":null,"ports":{"public":["80","81","443"]},"commercial":null,"subscription":null,"autodeploy":null,"suggested":null,"dependency":null,"avoid_render":null,"price":null,"icon":{"light":{"width":192,"height":192,"image":"205128e6-0303-4b62-b946-9810b61f3d04.png"},"dark":{}},"category_id":2,"parent_app_id":null,"full_description":null,"description":"

Nginx Proxy Manager is a user-friendly software application designed to effortlessly route traffic to your websites, whether they're hosted at home or elsewhere. It comes equipped with free SSL capabilities, eliminating the need for extensive Nginx or Letsencrypt knowledge. This tool proves especially handy for simplifying SSL generation and seamlessly proxying your docker containers.

","plan_type":null,"ansible_var":null,"repo_dir":null,"cpu":"1","ram_size":"1Gb","disk_size":"0.3Gb","dockerhub_image":"nginx-proxy-manager","versions":[{"_etag":"599","_id":599,"_created":"2023-08-11T10:23:33","_updated":"2023-08-11T10:23:34.420583","app_id":235,"name":"Nginx proxy manager","version":"2.10.4","update_status":"ready_for_testing","tag":"unstable"},{"_etag":"601","_id":601,"_created":null,"_updated":"2023-08-15T08:11:19.703882","app_id":235,"name":"Nginx proxy manager","version":"2.10.4","update_status":"published","tag":"stable"},{"_etag":null,"_id":600,"_created":null,"_updated":"2023-08-11T07:08:43.944998","app_id":235,"name":"Nginx proxy manager","version":"2.10.4","update_status":"ready_for_testing","tag":"latest"}],"domain":"","sharedPorts":["443"],"main":true}],"service":[{"_etag":null,"_id":24,"_created":"2020-06-19T13:07:24.228389","_updated":"2023-08-08T10:34:13.4985","name":"PostgreSQL","code":"postgres","role":[],"type":"service","default":null,"popularity":null,"descr":null,"ports":null,"commercial":null,"subscription":null,"autodeploy":null,"suggested":null,"dependency":null,"avoid_render":null,"price":null,"icon":{"light":{"width":576,"height":594,"image":"fd23f54c-e250-4228-8d56-7e5d93ffb925.svg"},"dark":{}},"category_id":null,"parent_app_id":null,"full_description":null,"description":null,"plan_type":null,"ansible_var":null,"repo_dir":null,"cpu":null,"ram_size":null,"disk_size":null,"dockerhub_image":"postgres","versions":[{"_etag":null,"_id":458,"_created":"2022-10-20T07:57:05.88997","_updated":"2023-04-05T07:24:39.637749","app_id":24,"name":"15","version":"15","update_status":"published","tag":"15"},{"_etag":null,"_id":288,"_created":"2022-10-20T07:56:16.160116","_updated":"2023-03-17T13:46:51.433539","app_id":24,"name":"10.22","version":"10.22","update_status":"published","tag":"10.22"},{"_etag":null,"_id":303,"_created":"2022-10-20T07:57:24.710286","_updated":"2023-03-17T13:46:51.433539","app_id":24,"name":"13.8","version":"13.8","update_status":"published","tag":"13.8"},{"_etag":null,"_id":266,"_created":"2022-10-20T07:56:32.360852","_updated":"2023-04-05T06:49:31.782132","app_id":24,"name":"11","version":"11","update_status":"published","tag":"11"},{"_etag":null,"_id":267,"_created":"2022-10-20T07:57:35.552085","_updated":"2023-03-17T13:46:51.433539","app_id":24,"name":"12.12","version":"12.12","update_status":"published","tag":"12.12"},{"_etag":null,"_id":38,"_created":"2020-06-19T13:07:24.258724","_updated":"2022-10-20T07:58:06.882602","app_id":24,"name":"14.5","version":"14.5","update_status":"published","tag":"14.5"},{"_etag":null,"_id":564,"_created":null,"_updated":"2023-05-24T12:55:57.894215","app_id":24,"name":"0.0.5","version":"0.0.5","update_status":"ready_for_testing","tag":"0.0.5"},{"_etag":null,"_id":596,"_created":null,"_updated":"2023-08-09T11:00:33.004267","app_id":24,"name":"Postgres","version":"15.1","update_status":"published","tag":"15.1"}],"domain":"","sharedPorts":["5432"],"main":true}],"servers_count":3,"custom_stack_name":"SMBO","custom_stack_code":"sample-stack","custom_stack_git_url":"https://github.com/vsilent/smbo.git","custom_stack_category":["New","Marketing Automation"],"custom_stack_short_description":"Should be what is my project about shortly","custom_stack_description":"what is my project about more detailed","project_name":"sample stack","project_overview":"my short description, stack to marketplace, keep my token","project_description":"my full description, stack to marketplace, keep my token"}} diff --git a/scripts/init_db.sh b/scripts/init_db.sh index 9b13934..8d84403 100755 --- a/scripts/init_db.sh +++ b/scripts/init_db.sh @@ -14,7 +14,7 @@ fi DB_USER=${POSTGRES_USER:=postgres} DB_PASSWORD=${POSTGRES_PASSWORD:=postgres} -DB_NAME=${POSTGRES_DB:=newsletter} +DB_NAME=${POSTGRES_DB:=stacker} DB_PORT=${POSTGRES_PORT:=5432} docker run \ diff --git a/src/models/stack.rs b/src/models/stack.rs index 40255dd..8cb79b1 100644 --- a/src/models/stack.rs +++ b/src/models/stack.rs @@ -1,5 +1,8 @@ use uuid::Uuid; use chrono::{DateTime, Utc}; +use serde_derive::Deserialize; +use serde_derive::Serialize; +use serde_json::Value; pub struct Stack { pub id: Uuid, // id - is a unique identifier for the app stack @@ -12,3 +15,245 @@ pub struct Stack { } +#[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct FormData { + pub common_domain: String, + pub domain_list: DomainList, + pub region: String, + pub zone: Value, + pub server: String, + pub os: String, + pub ssl: String, + pub vars: Vec, + #[serde(rename = "integrated_features")] + pub integrated_features: Vec, + #[serde(rename = "extended_features")] + pub extended_features: Vec, + pub subscriptions: Vec, + #[serde(rename = "save_token")] + pub save_token: bool, + #[serde(rename = "cloud_token")] + pub cloud_token: String, + pub provider: String, + #[serde(rename = "stack_code")] + pub stack_code: String, + #[serde(rename = "selected_plan")] + pub selected_plan: String, + pub custom: Custom, +} + +#[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct DomainList { +} + +#[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct Custom { + pub web: Vec, + pub feature: Vec, + pub service: Vec, + #[serde(rename = "servers_count")] + pub servers_count: i64, + #[serde(rename = "custom_stack_name")] + pub custom_stack_name: String, + #[serde(rename = "custom_stack_code")] + pub custom_stack_code: String, + #[serde(rename = "custom_stack_git_url")] + pub custom_stack_git_url: String, + #[serde(rename = "custom_stack_category")] + pub custom_stack_category: Vec, + #[serde(rename = "custom_stack_short_description")] + pub custom_stack_short_description: String, + #[serde(rename = "custom_stack_description")] + pub custom_stack_description: String, + #[serde(rename = "project_name")] + pub project_name: String, + #[serde(rename = "project_overview")] + pub project_overview: String, + #[serde(rename = "project_description")] + pub project_description: String, +} + +#[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct Web { + pub name: String, + pub code: String, + pub domain: String, + pub shared_ports: Vec, + pub versions: Vec, + pub custom: bool, + #[serde(rename = "type")] + pub type_field: String, + pub main: bool, + #[serde(rename = "_id")] + pub id: String, + #[serde(rename = "dockerhub_user")] + pub dockerhub_user: String, + #[serde(rename = "dockerhub_name")] + pub dockerhub_name: String, + #[serde(rename = "ram_size")] + pub ram_size: String, + pub cpu: i64, + #[serde(rename = "disk_size")] + pub disk_size: String, +} + +#[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct Feature { + #[serde(rename = "_etag")] + pub etag: Value, + #[serde(rename = "_id")] + pub id: i64, + #[serde(rename = "_created")] + pub created: String, + #[serde(rename = "_updated")] + pub updated: String, + pub name: String, + pub code: String, + pub role: Vec, + #[serde(rename = "type")] + pub type_field: String, + pub default: Value, + pub popularity: Value, + pub descr: Value, + pub ports: Ports, + pub commercial: Value, + pub subscription: Value, + pub autodeploy: Value, + pub suggested: Value, + pub dependency: Value, + #[serde(rename = "avoid_render")] + pub avoid_render: Value, + pub price: Value, + pub icon: Icon, + #[serde(rename = "category_id")] + pub category_id: i64, + #[serde(rename = "parent_app_id")] + pub parent_app_id: Value, + #[serde(rename = "full_description")] + pub full_description: Value, + pub description: String, + #[serde(rename = "plan_type")] + pub plan_type: Value, + #[serde(rename = "ansible_var")] + pub ansible_var: Value, + #[serde(rename = "repo_dir")] + pub repo_dir: Value, + pub cpu: String, + #[serde(rename = "ram_size")] + pub ram_size: String, + #[serde(rename = "disk_size")] + pub disk_size: String, + #[serde(rename = "dockerhub_image")] + pub dockerhub_image: String, + pub versions: Vec, + pub domain: String, + pub shared_ports: Vec, + pub main: bool, +} + +#[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct Ports { + pub public: Vec, +} + +#[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct Icon { + pub light: IconLight, + pub dark: IconDark, +} + +#[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct IconLight { + pub width: i64, + pub height: i64, + pub image: String, +} + +#[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct IconDark { +} + +#[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct Version { + #[serde(rename = "_etag")] + pub etag: Option, + #[serde(rename = "_id")] + pub id: i64, + #[serde(rename = "_created")] + pub created: Option, + #[serde(rename = "_updated")] + pub updated: String, + #[serde(rename = "app_id")] + pub app_id: i64, + pub name: String, + pub version: String, + #[serde(rename = "update_status")] + pub update_status: String, + pub tag: String, +} + +#[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct Service { + #[serde(rename = "_etag")] + pub etag: Value, + #[serde(rename = "_id")] + pub id: i64, + #[serde(rename = "_created")] + pub created: String, + #[serde(rename = "_updated")] + pub updated: String, + pub name: String, + pub code: String, + pub role: Vec, + #[serde(rename = "type")] + pub type_field: String, + pub default: Value, + pub popularity: Value, + pub descr: Value, + pub ports: Value, + pub commercial: Value, + pub subscription: Value, + pub autodeploy: Value, + pub suggested: Value, + pub dependency: Value, + #[serde(rename = "avoid_render")] + pub avoid_render: Value, + pub price: Value, + pub icon: Icon, + #[serde(rename = "category_id")] + pub category_id: Value, + #[serde(rename = "parent_app_id")] + pub parent_app_id: Value, + #[serde(rename = "full_description")] + pub full_description: Value, + pub description: Value, + #[serde(rename = "plan_type")] + pub plan_type: Value, + #[serde(rename = "ansible_var")] + pub ansible_var: Value, + #[serde(rename = "repo_dir")] + pub repo_dir: Value, + pub cpu: Value, + #[serde(rename = "ram_size")] + pub ram_size: Value, + #[serde(rename = "disk_size")] + pub disk_size: Value, + #[serde(rename = "dockerhub_image")] + pub dockerhub_image: String, + pub versions: Vec, + pub domain: String, + pub shared_ports: Vec, + pub main: bool, +} + diff --git a/src/routes/stack/add.rs b/src/routes/stack/add.rs index fb653ed..76b768f 100644 --- a/src/routes/stack/add.rs +++ b/src/routes/stack/add.rs @@ -4,16 +4,10 @@ use tracing::Instrument; use uuid::Uuid; use chrono::Utc; - -#[derive(serde::Deserialize)] -pub struct FormData { - commonDomain: String, - region: String, - domainList: String, - user_id: i32 -} - pub async fn add(form: web::Form, pool: web::Data) -> HttpResponse { + tracing::debug!("we are here"); + tracing::info!("we are here"); + let request_id = Uuid::new_v4(); let request_span = tracing::info_span!( "Validating a new stack", %request_id, @@ -63,4 +57,5 @@ pub async fn add(form: web::Form, pool: web::Data) -> HttpResp HttpResponse::InternalServerError().finish() } } + } diff --git a/src/routes/stack/mod.rs b/src/routes/stack/mod.rs index ae5008a..f3e5bc9 100644 --- a/src/routes/stack/mod.rs +++ b/src/routes/stack/mod.rs @@ -1,7 +1,7 @@ -mod add; -mod deploy; -mod get; -mod update; +pub mod add; +pub mod deploy; +pub mod get; +pub mod update; pub use add::*; pub use update::*; pub use deploy::*; diff --git a/src/startup.rs b/src/startup.rs index 1803d6b..5be6133 100644 --- a/src/startup.rs +++ b/src/startup.rs @@ -23,14 +23,19 @@ pub fn run(listener: TcpListener, db_pool: PgPool) -> Result Date: Mon, 11 Sep 2023 22:19:55 +0300 Subject: [PATCH 09/29] still broken --- ...230903063840_creating_rating_tables.up.sql | 2 +- src/lib.rs | 2 +- src/models/rating.rs | 3 + src/routes/rating.rs | 70 ++++++++++++------- src/startup.rs | 9 +++ src/telemetry.rs | 21 +++--- 6 files changed, 67 insertions(+), 40 deletions(-) diff --git a/migrations/20230903063840_creating_rating_tables.up.sql b/migrations/20230903063840_creating_rating_tables.up.sql index c693139..5a0e53e 100644 --- a/migrations/20230903063840_creating_rating_tables.up.sql +++ b/migrations/20230903063840_creating_rating_tables.up.sql @@ -9,7 +9,7 @@ CREATE TABLE product ( ); CREATE TABLE rating ( - id integer NOT NULL, PRIMARY KEY(id), + id serial PRIMARY KEY(id), user_id uuid NOT NULL, product_id integer NOT NULL, category VARCHAR(255) NOT NULL, diff --git a/src/lib.rs b/src/lib.rs index bae3244..45d6aae 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -4,4 +4,4 @@ pub mod configuration; pub mod telemetry; mod middleware; mod models; -mod services; \ No newline at end of file +mod services; diff --git a/src/models/rating.rs b/src/models/rating.rs index 290287a..2be8574 100644 --- a/src/models/rating.rs +++ b/src/models/rating.rs @@ -32,6 +32,9 @@ pub struct Rating { #[derive(Serialize, Deserialize, Debug)] +#[derive(sqlx::Type)] +#[sqlx(type_name = "category")] +#[sqlx(rename_all = "lowercase")] pub enum RateCategory { Application, // app, feature, extension Cloud, // is user satisfied working with this cloud diff --git a/src/routes/rating.rs b/src/routes/rating.rs index bdd0038..0eda51f 100644 --- a/src/routes/rating.rs +++ b/src/routes/rating.rs @@ -3,6 +3,10 @@ use serde::{Deserialize, Serialize}; use crate::models::rating::RateCategory; use serde_valid::Validate; use sqlx::PgPool; +use tracing::instrument; +use uuid::Uuid; +use crate::startup::AppState; +use tracing::Instrument; // workflow // add, update, list, get(user_id), ACL, @@ -11,44 +15,56 @@ use sqlx::PgPool; #[derive(Serialize, Deserialize, Debug, Validate)] pub struct RatingForm { - pub obj_id: u32, // product external id + pub obj_id: i32, // product external id pub category: RateCategory, // rating of product | rating of service etc #[validate(max_length = 1000)] pub comment: Option, // always linked to a product #[validate(minimum = 0)] #[validate(maximum = 10)] - pub rate: u32, // + pub rate: i32, // } -pub async fn rating(form: web::Json, pool: web::Data) -> HttpResponse { - let user_id = 1; // Let's assume we have a user id already taken from auth +pub async fn rating(app_state: web::Data, form: web::Json, pool: +web::Data) -> HttpResponse { + let request_id = Uuid::new_v4(); + let user_id = app_state.user_id; // uuid Let's assume we have a user id already taken from auth + + let query_span = tracing::info_span!( + "Saving new rating details in the database" + ); // Get product by id // Insert rating - // match sqlx::query!( - // r#" - // INSERT INTO rating () - // VALUES ($1, $2, $3, $4) - // "#, - // args - // ) - // .execute(pool.get_ref()) - // .instrument(query_span) - // .await - // { - // Ok(_) => { - // tracing::info!( - // "req_id: {} New subscriber details have been saved to database", - // request_id - // ); - // HttpResponse::Ok().finish() - // } - // Err(e) => { - // tracing::error!("req_id: {} Failed to execute query: {:?}", request_id, e); - // HttpResponse::InternalServerError().finish() - // } - // } + match sqlx::query!( + r#" + INSERT INTO rating (user_id, product_id, category, comment, hidden,rate,created_at, + updated_at) + VALUES ($1, $2, $3, $4, $5, $6, NOW() at time zone 'utc', NOW() at time zone 'utc') + "#, + user_id, + form.obj_id, + form.category, + form.comment, + false, + form.rate + ) + .execute(pool.get_ref()) + .instrument(query_span) + .await + { + Ok(_) => { + tracing::info!( + "req_id: {} New subscriber details have been saved to database", + request_id + ); + HttpResponse::Ok().finish() + } + Err(e) => { + tracing::error!("req_id: {} Failed to execute query: {:?}", request_id, e); + HttpResponse::InternalServerError().finish() + } + } println!("{:?}", form); HttpResponse::Ok().finish() } diff --git a/src/startup.rs b/src/startup.rs index 747922b..db1ecd4 100644 --- a/src/startup.rs +++ b/src/startup.rs @@ -7,6 +7,12 @@ use actix_web::{ }; use sqlx::PgPool; use std::net::TcpListener; +use uuid::Uuid; + +pub struct AppState { + pub user_id: Uuid // @todo User must be move lates to actix session and obtained from auth +} + pub fn run(listener: TcpListener, db_pool: PgPool) -> Result { let db_pool = web::Data::new(db_pool); @@ -24,6 +30,9 @@ pub fn run(listener: TcpListener, db_pool: PgPool) -> Result impl Subscriber + Send + Sync { - + env_filter: String, // Subscriber is a trait for our spans, Send - trait for thread safety to send to another thread, Sync - trait for thread safety share between trheads +) -> impl Subscriber + Send + Sync { // when tracing_subscriber is used, env_logger is not needed // env_logger::Builder::from_env(Env::default().default_filter_or("info")).init(); - let env_filter = EnvFilter::try_from_default_env().unwrap_or_else(|_| EnvFilter::new(env_filter)); + let env_filter = + EnvFilter::try_from_default_env().unwrap_or_else(|_| EnvFilter::new(env_filter)); let formatting_layer = BunyanFormattingLayer::new( - name, + name, // Output the formatted spans to stdout. - std::io::stdout + std::io::stdout, ); // the with method is provided by the SubscriberExt trait for Subscriber exposed by tracing_subscriber Registry::default() @@ -27,10 +26,10 @@ pub fn get_subscriber( } pub fn init_subscriber(subscriber: impl Subscriber + Send + Sync) { - // set_global_default + // set_global_default //redirect all log's events to the tracing subscriber LogTracer::init().expect("Failed to set logger."); // Result set_global_default(subscriber).expect("Failed to set subscriber."); -} \ No newline at end of file +} From 86c810d95cf8f0b6c3f03079e5f996bf75b09258 Mon Sep 17 00:00:00 2001 From: vsilent Date: Wed, 13 Sep 2023 12:02:44 +0300 Subject: [PATCH 10/29] category enum to db --- README.md | 13 +++++++++++++ src/models/rating.rs | 4 ++-- src/routes/rating.rs | 3 ++- 3 files changed, 17 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index 0a33450..d5055e7 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,19 @@ # stacker +Stacker - is a web application that helps users to create custom IT solutions based on open +source apps and user's custom applications. Users can build their own stack of applications, and +deploy the final result to their favorite clouds using TryDirect API. + +Stacker includes: +1. Security module. User Authorization +2. Application Management +3. Cloud Provider Key Management +4. docker-compose generator +5. TryDirect API Client +6. Rating module + + Run db migration ``` sqlx migrate run diff --git a/src/models/rating.rs b/src/models/rating.rs index 2be8574..7d27da5 100644 --- a/src/models/rating.rs +++ b/src/models/rating.rs @@ -31,10 +31,10 @@ pub struct Rating { } -#[derive(Serialize, Deserialize, Debug)] #[derive(sqlx::Type)] -#[sqlx(type_name = "category")] #[sqlx(rename_all = "lowercase")] +#[sqlx(type_name = "category")] +#[derive(Serialize, Deserialize, Debug)] pub enum RateCategory { Application, // app, feature, extension Cloud, // is user satisfied working with this cloud diff --git a/src/routes/rating.rs b/src/routes/rating.rs index 0eda51f..7749a37 100644 --- a/src/routes/rating.rs +++ b/src/routes/rating.rs @@ -38,7 +38,8 @@ web::Data) -> HttpResponse { match sqlx::query!( r#" - INSERT INTO rating (user_id, product_id, category, comment, hidden,rate,created_at, + INSERT INTO rating (user_id, product_id, category, comment, hidden,rate, + created_at, updated_at) VALUES ($1, $2, $3, $4, $5, $6, NOW() at time zone 'utc', NOW() at time zone 'utc') "#, From cabba793b97bffebc6a7a6ec570c15923719d428 Mon Sep 17 00:00:00 2001 From: vsilent Date: Wed, 13 Sep 2023 12:48:28 +0300 Subject: [PATCH 11/29] fixes from dev, use AppState for the first tests --- src/models/rating.rs | 3 ++ src/routes/rating.rs | 31 ++++++++++++++----- src/routes/stack/add.rs | 68 ++++++++++++++++++++++------------------- src/startup.rs | 9 ++++++ tests/health_check.rs | 56 --------------------------------- 5 files changed, 73 insertions(+), 94 deletions(-) diff --git a/src/models/rating.rs b/src/models/rating.rs index 290287a..7d27da5 100644 --- a/src/models/rating.rs +++ b/src/models/rating.rs @@ -31,6 +31,9 @@ pub struct Rating { } +#[derive(sqlx::Type)] +#[sqlx(rename_all = "lowercase")] +#[sqlx(type_name = "category")] #[derive(Serialize, Deserialize, Debug)] pub enum RateCategory { Application, // app, feature, extension diff --git a/src/routes/rating.rs b/src/routes/rating.rs index bdd0038..f81fc4c 100644 --- a/src/routes/rating.rs +++ b/src/routes/rating.rs @@ -3,6 +3,10 @@ use serde::{Deserialize, Serialize}; use crate::models::rating::RateCategory; use serde_valid::Validate; use sqlx::PgPool; +use tracing::instrument; +use uuid::Uuid; +use crate::startup::AppState; +use tracing::Instrument; // workflow // add, update, list, get(user_id), ACL, @@ -11,27 +15,40 @@ use sqlx::PgPool; #[derive(Serialize, Deserialize, Debug, Validate)] pub struct RatingForm { - pub obj_id: u32, // product external id + pub obj_id: i32, // product external id pub category: RateCategory, // rating of product | rating of service etc #[validate(max_length = 1000)] pub comment: Option, // always linked to a product #[validate(minimum = 0)] #[validate(maximum = 10)] - pub rate: u32, // + pub rate: i32, // } -pub async fn rating(form: web::Json, pool: web::Data) -> HttpResponse { - let user_id = 1; // Let's assume we have a user id already taken from auth +pub async fn rating(app_state: web::Data, form: web::Json, pool: +web::Data) -> HttpResponse { + let request_id = Uuid::new_v4(); + let user_id = app_state.user_id; // uuid Let's assume we have a user id already taken from auth + + let query_span = tracing::info_span!( + "Saving new rating details in the database" + ); // Get product by id // Insert rating // match sqlx::query!( // r#" - // INSERT INTO rating () - // VALUES ($1, $2, $3, $4) + // INSERT INTO rating (user_id, product_id, category, comment, hidden,rate, + // created_at, + // updated_at) + // VALUES ($1, $2, $3, $4, $5, $6, NOW() at time zone 'utc', NOW() at time zone 'utc') // "#, - // args + // user_id, + // form.obj_id, + // form.category, + // form.comment, + // false, + // form.rate // ) // .execute(pool.get_ref()) // .instrument(query_span) diff --git a/src/routes/stack/add.rs b/src/routes/stack/add.rs index 76b768f..916aa29 100644 --- a/src/routes/stack/add.rs +++ b/src/routes/stack/add.rs @@ -3,17 +3,22 @@ use sqlx::PgPool; use tracing::Instrument; use uuid::Uuid; use chrono::Utc; +use crate::models::stack::FormData; +use crate::startup::AppState; -pub async fn add(form: web::Form, pool: web::Data) -> HttpResponse { + +pub async fn add(app_state: web::Data, form: web::Form, pool: +web::Data) -> HttpResponse { tracing::debug!("we are here"); tracing::info!("we are here"); + let user_id = app_state.user_id; let request_id = Uuid::new_v4(); let request_span = tracing::info_span!( "Validating a new stack", %request_id, - commonDomain=?form.commonDomain, + commonDomain=?form.common_domain, region=?form.region, - domainList=?form.domainList + domainList=?form.domain_list ); // using `enter` is an async function @@ -22,7 +27,7 @@ pub async fn add(form: web::Form, pool: web::Data) -> HttpResp tracing::info!( "request_id {} Adding '{}' '{}' as a new stack", request_id, - form.commonDomain, + form.common_domain, form.region ); @@ -30,32 +35,33 @@ pub async fn add(form: web::Form, pool: web::Data) -> HttpResp "Saving new stack details into the database" ); - match sqlx::query!( - r#" - INSERT INTO user_stack (id, user_id, name, created_at, updated_at) - VALUES ($1, $2, $3, $4, $5) - "#, - 0_i32, - form.user_id, - form.commonDomain, - Utc::now(), - Utc::now() - ) - .execute(pool.get_ref()) - .instrument(query_span) - .await - { - Ok(_) => { - tracing::info!( - "req_id: {} New stack details have been saved to database", - request_id - ); - HttpResponse::Ok().finish() - } - Err(e) => { - tracing::error!("req_id: {} Failed to execute query: {:?}", request_id, e); - HttpResponse::InternalServerError().finish() - } - } + // match sqlx::query!( + // r#" + // INSERT INTO user_stack (id, user_id, name, created_at, updated_at) + // VALUES ($1, $2, $3, $4, $5) + // "#, + // 0_i32, + // user_id, + // form.common_domain, + // Utc::now(), + // Utc::now() + // ) + // .execute(pool.get_ref()) + // .instrument(query_span) + // .await + // { + // Ok(_) => { + // tracing::info!( + // "req_id: {} New stack details have been saved to database", + // request_id + // ); + // HttpResponse::Ok().finish() + // } + // Err(e) => { + // tracing::error!("req_id: {} Failed to execute query: {:?}", request_id, e); + // HttpResponse::InternalServerError().finish() + // } + // } + HttpResponse::Ok().finish() } diff --git a/src/startup.rs b/src/startup.rs index 5be6133..be47109 100644 --- a/src/startup.rs +++ b/src/startup.rs @@ -7,6 +7,12 @@ use actix_web::{ }; use sqlx::PgPool; use std::net::TcpListener; +use uuid::Uuid; + +pub struct AppState { + pub user_id: i32 // @todo User must be move later to actix session and obtained from auth +} + pub fn run(listener: TcpListener, db_pool: PgPool) -> Result { let db_pool = web::Data::new(db_pool); @@ -43,6 +49,9 @@ pub fn run(listener: TcpListener, db_pool: PgPool) -> Result TestApp { db_pool: connection_pool, } } - -#[tokio::test] -async fn subscribe_returns_a_200_for_valid_form_data() { - // Arrange - let app = spawn_app().await; - let client = reqwest::Client::new(); - - let body = "name=le%20guin&email=ursula_le_guin%40gmail.com"; // %20 - space, %40 - @ - let response = client - .post(&format!("{}/subscriptions", &app.address)) - .header("Content-Type", "application/x-www-form-urlencoded") - .body(body) - .send() - .await - .expect("Failed to execute request."); - - assert_eq!(200, response.status().as_u16()); - - let saved = sqlx::query!("SELECT email, name FROM subscriptions",) - .fetch_one(&app.db_pool) - .await - .expect("Failed to fetch saved subscription."); - - assert_eq!(saved.email, "ursula_le_guin@gmail.com"); - assert_eq!(saved.name, "le guin"); -} - -#[tokio::test] -async fn subscribe_returns_a_400_when_data_is_missing() { - // Arrange - let app = spawn_app().await; - let client = reqwest::Client::new(); - - let test_cases = vec![ - ("name=le%20guin", "missing the email"), - ("email=ursula_le_guin%40gmail.com", "missing the name"), - ("", "missing both name and email"), - ]; - - for (invalid_body, error_message) in test_cases { - let response = client - .post(&format!("{}/subscriptions", &app.address)) - .header("Content-Type", "application/x-www-form-urlencoded") - .body(invalid_body) - .send() - .await - .expect("Failed to execute request."); - - assert_eq!( - 400, - response.status().as_u16(), - "The API did not fail with 400 Bad Request when the payload was {}.", - error_message - ); - } -} From 2211a6f41fa0d798d32c581faaf0a32b27246688 Mon Sep 17 00:00:00 2001 From: vsilent Date: Sat, 16 Sep 2023 09:59:53 +0300 Subject: [PATCH 12/29] Json extractor instead Form --- custom-stack-payload-2.json | 1 + src/routes/stack/add.rs | 113 +++++++++++++++++++----------------- src/startup.rs | 2 + 3 files changed, 63 insertions(+), 53 deletions(-) create mode 100644 custom-stack-payload-2.json diff --git a/custom-stack-payload-2.json b/custom-stack-payload-2.json new file mode 100644 index 0000000..e64ec97 --- /dev/null +++ b/custom-stack-payload-2.json @@ -0,0 +1 @@ +{"commonDomain":"","domainList":{},"region":"fsn1","zone":null,"server":"cx21","os":"ubuntu-20.04","ssl":"letsencrypt","vars":[],"integrated_features":[],"extended_features":[],"subscriptions":["stack_migration"],"save_token":false,"cloud_token":"r6LAjqrynVt7pUwctVkzBlJmKjLOCxJIWjZFMLTkPYCCB4rsgphhEVhiL4DuO757","provider":"htz","stack_code":"custom-stack","selected_plan":"plan-individual-monthly","custom":{"web":[{"name":"Smarty Bot","code":"smarty-bot","domain":"smartybot.xyz","sharedPorts":["8000"],"versions":[],"custom":true,"type":"web","main":true,"_id":"lltkpq6p347kystct","dockerhub_user":"trydirect","dockerhub_name":"smarty-bot","url_app":"smartybot.xyz","url_git":"https://github.com/vsilent/smarty.git","disk_size":"1Gb","ram_size":"1Gb","cpu":1}],"feature":[{"_etag":null,"_id":198,"_created":"2022-04-27T14:10:27.280327","_updated":"2023-08-03T08:24:18.958721","name":"Portainer CE Feature","code":"portainer_ce_feature","role":["portainer-ce-feature"],"type":"feature","default":null,"popularity":null,"descr":null,"ports":{"public":["9000","8000"]},"commercial":null,"subscription":null,"autodeploy":null,"suggested":null,"dependency":null,"avoid_render":null,"price":null,"icon":{"light":{"width":1138,"height":1138,"image":"08589075-44e6-430e-98a5-f9dcf711e054.svg"},"dark":{}},"category_id":2,"parent_app_id":null,"full_description":null,"description":"

Portainer is a lightweight management UI which allows you to easily manage your different Docker environments (Docker hosts or Swarm clusters)

","plan_type":null,"ansible_var":null,"repo_dir":null,"cpu":"0.6","ram_size":"1Gb","disk_size":"1Gb","dockerhub_image":"portainer-ce-feature","versions":[{"_etag":null,"_id":456,"_created":"2022-04-25T12:44:30.964547","_updated":"2023-03-17T13:46:51.433539","app_id":198,"name":"latest","version":"latest","update_status":"published","tag":"latest"}],"domain":"","sharedPorts":["9000"],"main":true,"version":{"_etag":null,"_id":456,"_created":"2022-04-25T12:44:30.964547","_updated":"2023-03-17T13:46:51.433539","app_id":198,"name":"latest","version":"latest","update_status":"published","tag":"latest"}}],"service":[{"_etag":null,"_id":230,"_created":"2023-05-24T12:51:52.108972","_updated":"2023-08-04T12:18:34.670194","name":"pgrst","code":"pgrst","role":null,"type":"service","default":null,"popularity":null,"descr":null,"ports":null,"commercial":null,"subscription":null,"autodeploy":null,"suggested":null,"dependency":null,"avoid_render":null,"price":null,"icon":null,"category_id":null,"parent_app_id":null,"full_description":null,"description":"

PostgREST description

","plan_type":null,"ansible_var":null,"repo_dir":null,"cpu":"1","ram_size":"1Gb","disk_size":"1Gb","dockerhub_image":"pgrst","versions":[{"_etag":"566","_id":566,"_created":"2023-08-15T12:10:44","_updated":"2023-08-15T12:10:44.905249","app_id":230,"name":"PostgreSQL","version":"15_4","update_status":"ready_for_testing","tag":"unstable"},{"_etag":null,"_id":563,"_created":null,"_updated":"2023-05-24T12:52:15.351522","app_id":230,"name":"0.0.5","version":"0.0.5","update_status":"ready_for_testing","tag":"0.0.5"}],"domain":"","sharedPorts":["9999"],"main":true,"version":{"_etag":"566","_id":566,"_created":"2023-08-15T12:10:44","_updated":"2023-08-15T12:10:44.905249","app_id":230,"name":"PostgreSQL","version":"15_4","update_status":"ready_for_testing","tag":"unstable"}}],"servers_count":3,"custom_stack_name":"mysampleproject","custom_stack_code":"smarty-bot","custom_stack_category":["New"],"custom_stack_short_description":"sample short description","custom_stack_description":"stack description","custom_stack_publish":false,"project_name":"Smarty Bot","project_git_url":"https://github.com/vsilent/smarty.git","project_overview":"my product 1","project_description":"my product 1"}} diff --git a/src/routes/stack/add.rs b/src/routes/stack/add.rs index 916aa29..f3e5acc 100644 --- a/src/routes/stack/add.rs +++ b/src/routes/stack/add.rs @@ -1,4 +1,4 @@ -use actix_web::{web, HttpResponse}; +use actix_web::{web::{Data, Bytes, Json}, HttpResponse, HttpRequest, FromRequest}; use sqlx::PgPool; use tracing::Instrument; use uuid::Uuid; @@ -7,61 +7,68 @@ use crate::models::stack::FormData; use crate::startup::AppState; -pub async fn add(app_state: web::Data, form: web::Form, pool: -web::Data) -> HttpResponse { - tracing::debug!("we are here"); +pub async fn add(req: HttpRequest, app_state: Data, pool: +Data, body: Bytes) -> HttpResponse { + let content_type = req.headers().get("content-type"); + println!("=================== Request Content-Type: {:?}", content_type); + println!("request: {:?}", body); + // println!("app: {:?}", body); tracing::info!("we are here"); + match Json::::extract(&req).await { + Ok(form) => println!("Hello from {:?}!", form), + Err(err) => println!("error={:?}", err), + }; - let user_id = app_state.user_id; - let request_id = Uuid::new_v4(); - let request_span = tracing::info_span!( - "Validating a new stack", %request_id, - commonDomain=?form.common_domain, - region=?form.region, - domainList=?form.domain_list - ); - - // using `enter` is an async function - let _request_span_guard = request_span.enter(); // ->exit - - tracing::info!( - "request_id {} Adding '{}' '{}' as a new stack", - request_id, - form.common_domain, - form.region - ); - - let query_span = tracing::info_span!( - "Saving new stack details into the database" - ); - - // match sqlx::query!( - // r#" - // INSERT INTO user_stack (id, user_id, name, created_at, updated_at) - // VALUES ($1, $2, $3, $4, $5) - // "#, - // 0_i32, - // user_id, + // let user_id = app_state.user_id; + // let request_id = Uuid::new_v4(); + // let request_span = tracing::info_span!( + // "Validating a new stack", %request_id, + // commonDomain=?form.common_domain, + // region=?form.region, + // domainList=?form.domain_list + // ); + // + // // using `enter` is an async function + // let _request_span_guard = request_span.enter(); // ->exit + // + // tracing::info!( + // "request_id {} Adding '{}' '{}' as a new stack", + // request_id, // form.common_domain, - // Utc::now(), - // Utc::now() - // ) - // .execute(pool.get_ref()) - // .instrument(query_span) - // .await - // { - // Ok(_) => { - // tracing::info!( - // "req_id: {} New stack details have been saved to database", - // request_id - // ); - // HttpResponse::Ok().finish() - // } - // Err(e) => { - // tracing::error!("req_id: {} Failed to execute query: {:?}", request_id, e); - // HttpResponse::InternalServerError().finish() - // } - // } + // form.region + // ); + // + // let query_span = tracing::info_span!( + // "Saving new stack details into the database" + // ); + // + // // match sqlx::query!( + // // r#" + // // INSERT INTO user_stack (id, user_id, name, created_at, updated_at) + // // VALUES ($1, $2, $3, $4, $5) + // // "#, + // // 0_i32, + // // user_id, + // // form.common_domain, + // // Utc::now(), + // // Utc::now() + // // ) + // // .execute(pool.get_ref()) + // // .instrument(query_span) + // // .await + // // { + // // Ok(_) => { + // // tracing::info!( + // // "req_id: {} New stack details have been saved to database", + // // request_id + // // ); + // // HttpResponse::Ok().finish() + // // } + // // Err(e) => { + // // tracing::error!("req_id: {} Failed to execute query: {:?}", request_id, e); + // // HttpResponse::InternalServerError().finish() + // // } + // // } HttpResponse::Ok().finish() } diff --git a/src/startup.rs b/src/startup.rs index be47109..4134a37 100644 --- a/src/startup.rs +++ b/src/startup.rs @@ -7,8 +7,10 @@ use actix_web::{ }; use sqlx::PgPool; use std::net::TcpListener; +use serde_derive::{Deserialize, Serialize}; use uuid::Uuid; +#[derive(Serialize, Deserialize, Debug)] pub struct AppState { pub user_id: i32 // @todo User must be move later to actix session and obtained from auth } From 41f8de0db52e8bf12b6bdfce0d0ce3ad58772621 Mon Sep 17 00:00:00 2001 From: vsilent Date: Sun, 17 Sep 2023 09:56:39 +0300 Subject: [PATCH 13/29] AppState requst handling --- src/routes/stack/add.rs | 41 ++++++++++++++++++++++++++++------------- 1 file changed, 28 insertions(+), 13 deletions(-) diff --git a/src/routes/stack/add.rs b/src/routes/stack/add.rs index f3e5acc..def23ae 100644 --- a/src/routes/stack/add.rs +++ b/src/routes/stack/add.rs @@ -1,23 +1,36 @@ -use actix_web::{web::{Data, Bytes, Json}, HttpResponse, HttpRequest, FromRequest}; +use std::io::Read; +use actix_web::{web::{Data, Bytes, Json}, HttpResponse, HttpRequest, Responder, Result}; +use actix_web::error::{Error, JsonPayloadError, PayloadError}; use sqlx::PgPool; use tracing::Instrument; use uuid::Uuid; use chrono::Utc; use crate::models::stack::FormData; use crate::startup::AppState; +use std::str; -pub async fn add(req: HttpRequest, app_state: Data, pool: -Data, body: Bytes) -> HttpResponse { - let content_type = req.headers().get("content-type"); - println!("=================== Request Content-Type: {:?}", content_type); - println!("request: {:?}", body); - // println!("app: {:?}", body); - tracing::info!("we are here"); - match Json::::extract(&req).await { - Ok(form) => println!("Hello from {:?}!", form), - Err(err) => println!("error={:?}", err), - }; +// pub async fn add(req: HttpRequest, app_state: Data, pool: +pub async fn add(body: Bytes) -> Result { + // None::.expect("my error"); + // return Err(JsonPayloadError::Payload(PayloadError::Overflow).into()); + // let content_type = req.headers().get("content-type"); + // println!("=================== Request Content-Type: {:?}", content_type); + + let body_bytes = actix_web::body::to_bytes(body).await.unwrap(); + let body_str = str::from_utf8(&body_bytes).unwrap(); + // method 1 + // let app_state: AppState = serde_json::from_str(body_str).unwrap(); + // method 2 + let app_state = serde_json::from_str::(body_str).unwrap(); + println!("request: {:?}", app_state); + // // println!("app: {:?}", body); + // println!("user_id: {:?}", data.user_id); + // tracing::info!("we are here"); + // match Json::::extract(&req).await { + // Ok(form) => println!("Hello from {:?}!", form), + // Err(err) => println!("error={:?}", err), + // }; // let user_id = app_state.user_id; // let request_id = Uuid::new_v4(); @@ -70,5 +83,7 @@ Data, body: Bytes) -> HttpResponse { // // } // // } - HttpResponse::Ok().finish() + // HttpResponse::Ok().finish() + Ok(Json(app_state)) + // Ok(HttpResponse::Ok().finish()) } From 218ba56d2cd0893eae6cdf6254f9e2954e768eb4 Mon Sep 17 00:00:00 2001 From: vsilent Date: Sun, 17 Sep 2023 10:34:12 +0300 Subject: [PATCH 14/29] Add rating implemented, database creds changed --- .env | 2 +- configuration.yaml | 2 +- scripts/init_db.sh | 2 +- src/models/rating.rs | 11 ++++++++--- src/routes/rating.rs | 6 +++--- 5 files changed, 14 insertions(+), 9 deletions(-) diff --git a/.env b/.env index 8538b56..f6e3b68 100644 --- a/.env +++ b/.env @@ -1 +1 @@ -DATABASE_URL=postgres://postgres:postgres@localhost:5432/newsletter \ No newline at end of file +DATABASE_URL=postgres://postgres:postgres@localhost:5432/stacker \ No newline at end of file diff --git a/configuration.yaml b/configuration.yaml index 70fe16f..dbd16f2 100644 --- a/configuration.yaml +++ b/configuration.yaml @@ -4,4 +4,4 @@ database: port: 5432 username: postgres password: "postgres" - database_name: newsletter \ No newline at end of file + database_name: stacker \ No newline at end of file diff --git a/scripts/init_db.sh b/scripts/init_db.sh index 9b13934..8d84403 100755 --- a/scripts/init_db.sh +++ b/scripts/init_db.sh @@ -14,7 +14,7 @@ fi DB_USER=${POSTGRES_USER:=postgres} DB_PASSWORD=${POSTGRES_PASSWORD:=postgres} -DB_NAME=${POSTGRES_DB:=newsletter} +DB_NAME=${POSTGRES_DB:=stacker} DB_PORT=${POSTGRES_PORT:=5432} docker run \ diff --git a/src/models/rating.rs b/src/models/rating.rs index 7d27da5..d9a7a31 100644 --- a/src/models/rating.rs +++ b/src/models/rating.rs @@ -32,9 +32,8 @@ pub struct Rating { #[derive(sqlx::Type)] -#[sqlx(rename_all = "lowercase")] -#[sqlx(type_name = "category")] -#[derive(Serialize, Deserialize, Debug)] +#[sqlx(rename_all = "lowercase", type_name = "category")] +#[derive(Serialize, Deserialize, Debug, Clone, Copy)] pub enum RateCategory { Application, // app, feature, extension Cloud, // is user satisfied working with this cloud @@ -47,6 +46,12 @@ pub enum RateCategory { MemoryUsage } +impl Into for RateCategory { + fn into(self) -> String { + format!("{:?}", self) + } +} + pub struct Rules { //-> Product.id // example: allow to add only a single comment diff --git a/src/routes/rating.rs b/src/routes/rating.rs index 7749a37..1b1ed13 100644 --- a/src/routes/rating.rs +++ b/src/routes/rating.rs @@ -35,7 +35,7 @@ web::Data) -> HttpResponse { ); // Get product by id // Insert rating - + let category = Into::::into(form.category.clone()); match sqlx::query!( r#" INSERT INTO rating (user_id, product_id, category, comment, hidden,rate, @@ -45,7 +45,7 @@ web::Data) -> HttpResponse { "#, user_id, form.obj_id, - form.category, + category.as_str(), form.comment, false, form.rate @@ -65,7 +65,7 @@ web::Data) -> HttpResponse { tracing::error!("req_id: {} Failed to execute query: {:?}", request_id, e); HttpResponse::InternalServerError().finish() } - } + }; println!("{:?}", form); HttpResponse::Ok().finish() } From 2039789307fba0c94d73b663e33057a7917be83e Mon Sep 17 00:00:00 2001 From: vsilent Date: Sun, 17 Sep 2023 18:23:23 +0300 Subject: [PATCH 15/29] github actions, telegram notifier and docker image build --- .github/workflows/docker.yml | 155 +++++++++++++++++++++++++++++++++ .github/workflows/notifier.yml | 19 ++++ custom-stack-payload.json | 3 + 3 files changed, 177 insertions(+) create mode 100644 .github/workflows/docker.yml create mode 100644 .github/workflows/notifier.yml diff --git a/.github/workflows/docker.yml b/.github/workflows/docker.yml new file mode 100644 index 0000000..bf3ee4c --- /dev/null +++ b/.github/workflows/docker.yml @@ -0,0 +1,155 @@ +name: Docker CICD + +on: + push: + branches: + - master + - testing + pull_request: + branches: + - master + +jobs: + cicd-linux-docker: + name: Cargo and npm build + runs-on: ubuntu-latest + steps: + - name: Checkout sources + uses: actions/checkout@v2 + + - name: Install stable toolchain + uses: actions-rs/toolchain@v1 + with: + toolchain: stable + profile: minimal + override: true + components: rustfmt, clippy + + - name: Cache cargo registry + uses: actions/cache@v3.0.7 + with: + path: ~/.cargo/registry + key: docker-registry-${{ hashFiles('**/Cargo.lock') }} + restore-keys: | + docker-registry- + docker- + + - name: Cache cargo index + uses: actions/cache@v3.0.7 + with: + path: ~/.cargo/git + key: docker-index-${{ hashFiles('**/Cargo.lock') }} + restore-keys: | + docker-index- + docker- + + - name: Generate Secret Key + run: | + head -c16 /dev/urandom > src/secret.key + + - name: Cache cargo build + uses: actions/cache@v3.0.7 + with: + path: target + key: docker-build-${{ hashFiles('**/Cargo.lock') }} + restore-keys: | + docker-build- + docker- + + - name: Cargo check + uses: actions-rs/cargo@v1 + with: + command: check + + - name: Cargo test + if: ${{ always() }} + uses: actions-rs/cargo@v1 + with: + command: test + + - name: Rustfmt + uses: actions-rs/toolchain@v1 + with: + toolchain: stable + profile: minimal + override: true + components: rustfmt + command: fmt + args: --all -- --check + + - name: Rustfmt + uses: actions-rs/toolchain@v1 + with: + toolchain: stable + profile: minimal + override: true + components: clippy + command: clippy + args: -- -D warnings + + - name: Run cargo build + uses: actions-rs/cargo@v1 + with: + command: build + args: --release + + - name: npm install, build, and test + working-directory: ./web + run: | + npm install + npm run build + # npm test + + - name: Archive production artifacts + uses: actions/upload-artifact@v2 + with: + name: dist-without-markdown + path: | + web/dist + !web/dist/**/*.md + + - name: Display structure of downloaded files + run: ls -R web/dist + + - name: Copy app files and zip + run: | + mkdir -p app/stacker/dist + cp target/release/stacker app/stacker + cp -a web/dist/. app/stacker + cp docker/prod/Dockerfile app/Dockerfile + cd app + touch .env + tar -czvf ../app.tar.gz . + cd .. + + - name: Upload app archive for Docker job + uses: actions/upload-artifact@v2.2.2 + with: + name: artifact-linux-docker + path: app.tar.gz + + cicd-docker: + name: CICD Docker + runs-on: ubuntu-latest + needs: cicd-linux-docker + steps: + - name: Download app archive + uses: actions/download-artifact@v2 + with: + name: artifact-linux-docker + + - name: Extract app archive + run: tar -zxvf app.tar.gz + + - name: Display structure of downloaded files + run: ls -R + + - name: Docker build and publish + uses: docker/build-push-action@v1 + with: + username: ${{ secrets.DOCKER_USERNAME }} + password: ${{ secrets.DOCKER_PASSWORD }} + repository: trydirect/stacker + add_git_labels: true + tag_with_ref: true + #no-cache: true \ No newline at end of file diff --git a/.github/workflows/notifier.yml b/.github/workflows/notifier.yml new file mode 100644 index 0000000..ba3ed81 --- /dev/null +++ b/.github/workflows/notifier.yml @@ -0,0 +1,19 @@ +name: Notifier +on: + push: + branches: [ main ] + pull_request: + branches: [ main ] + +jobs: + + notifyTelegram: + runs-on: ubuntu-latest + steps: + - name: send custom message + uses: appleboy/telegram-action@master + with: + to: ${{ secrets.TELEGRAM_TO }} + token: ${{ secrets.TELEGRAM_TOKEN }} + message: | + "Issue ${{ github.event.action }}: \n${{ github.event.issue.html_url }}" \ No newline at end of file diff --git a/custom-stack-payload.json b/custom-stack-payload.json index 57dbd93..a9ca754 100644 --- a/custom-stack-payload.json +++ b/custom-stack-payload.json @@ -1 +1,4 @@ {"commonDomain":"","domainList":{},"region":"fsn1","zone":null,"server":"cx21","os":"ubuntu-20.04","ssl":"letsencrypt","vars":[],"integrated_features":[],"extended_features":[],"subscriptions":["stack_migration","stack_health_monitoring","stack_security_monitoring"],"save_token":true,"cloud_token":"r6LAjqrynVt7pUwctVkzBlJmKjLOCxJIWjZFMLTkPYCCB4rsgphhEVhiL4DuO757","provider":"htz","stack_code":"custom-stack","selected_plan":"plan-individual-monthly","custom":{"web":[{"name":"smarty database","code":"smarty-database","domain":"smarty-db.example.com","sharedPorts":["6532"],"versions":[],"custom":true,"type":"feature","main":true,"_id":"lm0gdh732y2qrojfl","dockerhub_user":"trydirect","dockerhub_name":"smarty-db","ram_size":"1Gb","cpu":1,"disk_size":"1Gb"}],"feature":[{"_etag":null,"_id":235,"_created":"2023-08-11T07:07:12.123355","_updated":"2023-08-15T13:07:30.597485","name":"Nginx Proxy Manager","code":"nginx_proxy_manager","role":["nginx_proxy_manager"],"type":"feature","default":null,"popularity":null,"descr":null,"ports":{"public":["80","81","443"]},"commercial":null,"subscription":null,"autodeploy":null,"suggested":null,"dependency":null,"avoid_render":null,"price":null,"icon":{"light":{"width":192,"height":192,"image":"205128e6-0303-4b62-b946-9810b61f3d04.png"},"dark":{}},"category_id":2,"parent_app_id":null,"full_description":null,"description":"

Nginx Proxy Manager is a user-friendly software application designed to effortlessly route traffic to your websites, whether they're hosted at home or elsewhere. It comes equipped with free SSL capabilities, eliminating the need for extensive Nginx or Letsencrypt knowledge. This tool proves especially handy for simplifying SSL generation and seamlessly proxying your docker containers.

","plan_type":null,"ansible_var":null,"repo_dir":null,"cpu":"1","ram_size":"1Gb","disk_size":"0.3Gb","dockerhub_image":"nginx-proxy-manager","versions":[{"_etag":"599","_id":599,"_created":"2023-08-11T10:23:33","_updated":"2023-08-11T10:23:34.420583","app_id":235,"name":"Nginx proxy manager","version":"2.10.4","update_status":"ready_for_testing","tag":"unstable"},{"_etag":"601","_id":601,"_created":null,"_updated":"2023-08-15T08:11:19.703882","app_id":235,"name":"Nginx proxy manager","version":"2.10.4","update_status":"published","tag":"stable"},{"_etag":null,"_id":600,"_created":null,"_updated":"2023-08-11T07:08:43.944998","app_id":235,"name":"Nginx proxy manager","version":"2.10.4","update_status":"ready_for_testing","tag":"latest"}],"domain":"","sharedPorts":["443"],"main":true}],"service":[{"_etag":null,"_id":24,"_created":"2020-06-19T13:07:24.228389","_updated":"2023-08-08T10:34:13.4985","name":"PostgreSQL","code":"postgres","role":[],"type":"service","default":null,"popularity":null,"descr":null,"ports":null,"commercial":null,"subscription":null,"autodeploy":null,"suggested":null,"dependency":null,"avoid_render":null,"price":null,"icon":{"light":{"width":576,"height":594,"image":"fd23f54c-e250-4228-8d56-7e5d93ffb925.svg"},"dark":{}},"category_id":null,"parent_app_id":null,"full_description":null,"description":null,"plan_type":null,"ansible_var":null,"repo_dir":null,"cpu":null,"ram_size":null,"disk_size":null,"dockerhub_image":"postgres","versions":[{"_etag":null,"_id":458,"_created":"2022-10-20T07:57:05.88997","_updated":"2023-04-05T07:24:39.637749","app_id":24,"name":"15","version":"15","update_status":"published","tag":"15"},{"_etag":null,"_id":288,"_created":"2022-10-20T07:56:16.160116","_updated":"2023-03-17T13:46:51.433539","app_id":24,"name":"10.22","version":"10.22","update_status":"published","tag":"10.22"},{"_etag":null,"_id":303,"_created":"2022-10-20T07:57:24.710286","_updated":"2023-03-17T13:46:51.433539","app_id":24,"name":"13.8","version":"13.8","update_status":"published","tag":"13.8"},{"_etag":null,"_id":266,"_created":"2022-10-20T07:56:32.360852","_updated":"2023-04-05T06:49:31.782132","app_id":24,"name":"11","version":"11","update_status":"published","tag":"11"},{"_etag":null,"_id":267,"_created":"2022-10-20T07:57:35.552085","_updated":"2023-03-17T13:46:51.433539","app_id":24,"name":"12.12","version":"12.12","update_status":"published","tag":"12.12"},{"_etag":null,"_id":38,"_created":"2020-06-19T13:07:24.258724","_updated":"2022-10-20T07:58:06.882602","app_id":24,"name":"14.5","version":"14.5","update_status":"published","tag":"14.5"},{"_etag":null,"_id":564,"_created":null,"_updated":"2023-05-24T12:55:57.894215","app_id":24,"name":"0.0.5","version":"0.0.5","update_status":"ready_for_testing","tag":"0.0.5"},{"_etag":null,"_id":596,"_created":null,"_updated":"2023-08-09T11:00:33.004267","app_id":24,"name":"Postgres","version":"15.1","update_status":"published","tag":"15.1"}],"domain":"","sharedPorts":["5432"],"main":true}],"servers_count":3,"custom_stack_name":"SMBO","custom_stack_code":"sample-stack","custom_stack_git_url":"https://github.com/vsilent/smbo.git","custom_stack_category":["New","Marketing Automation"],"custom_stack_short_description":"Should be what is my project about shortly","custom_stack_description":"what is my project about more detailed","project_name":"sample stack","project_overview":"my short description, stack to marketplace, keep my token","project_description":"my full description, stack to marketplace, keep my token"}} + + + From 725dca8b84100ab641fc592410f5dabdc0fdef59 Mon Sep 17 00:00:00 2001 From: vsilent Date: Sun, 17 Sep 2023 18:41:18 +0300 Subject: [PATCH 16/29] add rating fix from dev --- ...230903063840_creating_rating_tables.up.sql | 2 +- src/models/rating.rs | 11 +++- src/routes/rating.rs | 62 +++++++++---------- 3 files changed, 40 insertions(+), 35 deletions(-) diff --git a/migrations/20230903063840_creating_rating_tables.up.sql b/migrations/20230903063840_creating_rating_tables.up.sql index c693139..5a0e53e 100644 --- a/migrations/20230903063840_creating_rating_tables.up.sql +++ b/migrations/20230903063840_creating_rating_tables.up.sql @@ -9,7 +9,7 @@ CREATE TABLE product ( ); CREATE TABLE rating ( - id integer NOT NULL, PRIMARY KEY(id), + id serial PRIMARY KEY(id), user_id uuid NOT NULL, product_id integer NOT NULL, category VARCHAR(255) NOT NULL, diff --git a/src/models/rating.rs b/src/models/rating.rs index 7d27da5..d9a7a31 100644 --- a/src/models/rating.rs +++ b/src/models/rating.rs @@ -32,9 +32,8 @@ pub struct Rating { #[derive(sqlx::Type)] -#[sqlx(rename_all = "lowercase")] -#[sqlx(type_name = "category")] -#[derive(Serialize, Deserialize, Debug)] +#[sqlx(rename_all = "lowercase", type_name = "category")] +#[derive(Serialize, Deserialize, Debug, Clone, Copy)] pub enum RateCategory { Application, // app, feature, extension Cloud, // is user satisfied working with this cloud @@ -47,6 +46,12 @@ pub enum RateCategory { MemoryUsage } +impl Into for RateCategory { + fn into(self) -> String { + format!("{:?}", self) + } +} + pub struct Rules { //-> Product.id // example: allow to add only a single comment diff --git a/src/routes/rating.rs b/src/routes/rating.rs index f81fc4c..1b1ed13 100644 --- a/src/routes/rating.rs +++ b/src/routes/rating.rs @@ -35,37 +35,37 @@ web::Data) -> HttpResponse { ); // Get product by id // Insert rating - - // match sqlx::query!( - // r#" - // INSERT INTO rating (user_id, product_id, category, comment, hidden,rate, - // created_at, - // updated_at) - // VALUES ($1, $2, $3, $4, $5, $6, NOW() at time zone 'utc', NOW() at time zone 'utc') - // "#, - // user_id, - // form.obj_id, - // form.category, - // form.comment, - // false, - // form.rate - // ) - // .execute(pool.get_ref()) - // .instrument(query_span) - // .await - // { - // Ok(_) => { - // tracing::info!( - // "req_id: {} New subscriber details have been saved to database", - // request_id - // ); - // HttpResponse::Ok().finish() - // } - // Err(e) => { - // tracing::error!("req_id: {} Failed to execute query: {:?}", request_id, e); - // HttpResponse::InternalServerError().finish() - // } - // } + let category = Into::::into(form.category.clone()); + match sqlx::query!( + r#" + INSERT INTO rating (user_id, product_id, category, comment, hidden,rate, + created_at, + updated_at) + VALUES ($1, $2, $3, $4, $5, $6, NOW() at time zone 'utc', NOW() at time zone 'utc') + "#, + user_id, + form.obj_id, + category.as_str(), + form.comment, + false, + form.rate + ) + .execute(pool.get_ref()) + .instrument(query_span) + .await + { + Ok(_) => { + tracing::info!( + "req_id: {} New subscriber details have been saved to database", + request_id + ); + HttpResponse::Ok().finish() + } + Err(e) => { + tracing::error!("req_id: {} Failed to execute query: {:?}", request_id, e); + HttpResponse::InternalServerError().finish() + } + }; println!("{:?}", form); HttpResponse::Ok().finish() } From 7a13f3658f63518c2bc5adbb93bd889476675191 Mon Sep 17 00:00:00 2001 From: Vasili Pascal Date: Sun, 17 Sep 2023 19:02:48 +0300 Subject: [PATCH 17/29] Update README.md --- README.md | 25 ++++++++++++++----------- 1 file changed, 14 insertions(+), 11 deletions(-) diff --git a/README.md b/README.md index d5055e7..f6bb4df 100644 --- a/README.md +++ b/README.md @@ -1,20 +1,23 @@ -# stacker +# Stacker -Stacker - is a web application that helps users to create custom IT solutions based on open -source apps and user's custom applications. Users can build their own stack of applications, and +Stacker - is an application that helps users to create custom IT solutions based on dockerized open +source apps and user's custom applications docker containers. Users can build their own stack of applications, and deploy the final result to their favorite clouds using TryDirect API. -Stacker includes: -1. Security module. User Authorization -2. Application Management -3. Cloud Provider Key Management -4. docker-compose generator -5. TryDirect API Client -6. Rating module +Application will consist of: +1. Web UI (Application Stack builder) +2. Back-end RESTful API, includes: + a. Security module. User Authorization + b. Application Management + c. Cloud Provider Key Management + d. docker-compose generator + e. TryDirect API Client + f. Rating module Run db migration + ``` sqlx migrate run @@ -33,4 +36,4 @@ Add rating curl -vX POST 'http://localhost:8000/rating' -d '{"obj_id": 111, "category": "application", "comment":"some comment", "rate": 10}' --header 'Content-Type: application/json' -``` \ No newline at end of file +``` From 50c489297443e853891ff6bcef3cbc4dfb441b13 Mon Sep 17 00:00:00 2001 From: vsilent Date: Sun, 17 Sep 2023 19:27:23 +0300 Subject: [PATCH 18/29] product test fixture --- README.md | 15 +++++++++++---- .../20230917162549_creating_test_product.down.sql | 1 + .../20230917162549_creating_test_product.up.sql | 1 + 3 files changed, 13 insertions(+), 4 deletions(-) create mode 100644 migrations/20230917162549_creating_test_product.down.sql create mode 100644 migrations/20230917162549_creating_test_product.up.sql diff --git a/README.md b/README.md index f6bb4df..812e4d7 100644 --- a/README.md +++ b/README.md @@ -16,24 +16,31 @@ Application will consist of: f. Rating module -Run db migration +#### Run db migration ``` sqlx migrate run ``` -Down migration + +#### Down migration ``` sqlx migrate revert ``` -Add rating +#### Rate Product + +``` + + curl -vX POST 'http://localhost:8000/rating' -d '{"obj_id": 1, "category": "application", "comment":"some comment", "rate": 10}' --header 'Content-Type: application/json' ``` - curl -vX POST 'http://localhost:8000/rating' -d '{"obj_id": 111, "category": "application", "comment":"some comment", "rate": 10}' --header 'Content-Type: application/json' +#### Deploy ``` +curl -X POST -H "Content-Type: application/json" -d @custom-stack-payload-2.json http://127.0.0.1:8000/stack +``` \ No newline at end of file diff --git a/migrations/20230917162549_creating_test_product.down.sql b/migrations/20230917162549_creating_test_product.down.sql new file mode 100644 index 0000000..f9f6339 --- /dev/null +++ b/migrations/20230917162549_creating_test_product.down.sql @@ -0,0 +1 @@ +delete from product where id=1; diff --git a/migrations/20230917162549_creating_test_product.up.sql b/migrations/20230917162549_creating_test_product.up.sql new file mode 100644 index 0000000..7a1d8d6 --- /dev/null +++ b/migrations/20230917162549_creating_test_product.up.sql @@ -0,0 +1 @@ +INSERT INTO public.product (id, obj_id, obj_type, created_at, updated_at) VALUES(1, 1, 'Application', '2023-09-17 10:30:02.579', '2023-09-17 10:30:02.579'); \ No newline at end of file From 2dd86eba92e474004e16e3be833496a211a2efd0 Mon Sep 17 00:00:00 2001 From: Petru Date: Mon, 18 Sep 2023 17:09:56 +0300 Subject: [PATCH 19/29] fixed bug primary key --- migrations/20230903063840_creating_rating_tables.up.sql | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/migrations/20230903063840_creating_rating_tables.up.sql b/migrations/20230903063840_creating_rating_tables.up.sql index 5a0e53e..62f4bab 100644 --- a/migrations/20230903063840_creating_rating_tables.up.sql +++ b/migrations/20230903063840_creating_rating_tables.up.sql @@ -9,7 +9,7 @@ CREATE TABLE product ( ); CREATE TABLE rating ( - id serial PRIMARY KEY(id), + id serial, user_id uuid NOT NULL, product_id integer NOT NULL, category VARCHAR(255) NOT NULL, @@ -18,11 +18,10 @@ CREATE TABLE rating ( rate INTEGER, created_at timestamptz NOT NULL, updated_at timestamptz NOT NULL, - CONSTRAINT fk_product - FOREIGN KEY(product_id) - REFERENCES product(id) + CONSTRAINT fk_product FOREIGN KEY(product_id) REFERENCES product(id), + CONSTRAINT rating_pk PRIMARY KEY (id) ); CREATE INDEX idx_category ON rating(category); CREATE INDEX idx_user_id ON rating(user_id); -CREATE INDEX idx_product_id_rating_id ON rating(product_id, rate); \ No newline at end of file +CREATE INDEX idx_product_id_rating_id ON rating(product_id, rate); From af252847f6c7e13c1abee4233eac55674a5a62d0 Mon Sep 17 00:00:00 2001 From: Petru Date: Tue, 19 Sep 2023 16:34:14 +0300 Subject: [PATCH 20/29] fixed rating migration --- migrations/20230903063840_creating_rating_tables.up.sql | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/migrations/20230903063840_creating_rating_tables.up.sql b/migrations/20230903063840_creating_rating_tables.up.sql index 62f4bab..4aaeb3f 100644 --- a/migrations/20230903063840_creating_rating_tables.up.sql +++ b/migrations/20230903063840_creating_rating_tables.up.sql @@ -10,7 +10,7 @@ CREATE TABLE product ( CREATE TABLE rating ( id serial, - user_id uuid NOT NULL, + user_id integer NOT NULL, product_id integer NOT NULL, category VARCHAR(255) NOT NULL, comment TEXT DEFAULT NULL, From 46d6ec37144850572152203f18111c320821dde5 Mon Sep 17 00:00:00 2001 From: Petru Date: Wed, 20 Sep 2023 17:22:35 +0300 Subject: [PATCH 21/29] issue_enum - enum fixed --- src/models/rating.rs | 33 +++++++++++++++------------------ src/routes/rating.rs | 36 ++++++++++++++++++------------------ 2 files changed, 33 insertions(+), 36 deletions(-) diff --git a/src/models/rating.rs b/src/models/rating.rs index d9a7a31..0851674 100644 --- a/src/models/rating.rs +++ b/src/models/rating.rs @@ -1,6 +1,6 @@ -use uuid::Uuid; use chrono::{DateTime, Utc}; use serde::{Deserialize, Serialize}; +use uuid::Uuid; pub struct Product { // Product - is an external object that we want to store in the database, @@ -10,10 +10,10 @@ pub struct Product { // rating - is a rating of the product // product type stack & app, // id is generated based on the product type and external obj_id - pub id: i32, //primary key, for better data management - pub obj_id: u32, // external product ID db, no autoincrement, example: 100 - pub obj_type: String, // stack | app, unique index - pub rating: Rating, // 0-10 + pub id: i32, //primary key, for better data management + pub obj_id: u32, // external product ID db, no autoincrement, example: 100 + pub obj_type: String, // stack | app, unique index + pub rating: Rating, // 0-10 // pub rules: Rules, pub created_at: DateTime, pub updated_at: DateTime, @@ -21,29 +21,27 @@ pub struct Product { pub struct Rating { pub id: i32, - pub user_id: Uuid, // external user_id, 100, taken using token (middleware?) - pub category: String, // rating of product | rating of service etc - pub comment: String, // always linked to a product - pub hidden: bool, // rating can be hidden for non-adequate user behaviour + pub user_id: Uuid, // external user_id, 100, taken using token (middleware?) + pub category: String, // rating of product | rating of service etc + pub comment: String, // always linked to a product + pub hidden: bool, // rating can be hidden for non-adequate user behaviour pub rate: u32, pub created_at: DateTime, pub updated_at: DateTime, } - -#[derive(sqlx::Type)] -#[sqlx(rename_all = "lowercase", type_name = "category")] -#[derive(Serialize, Deserialize, Debug, Clone, Copy)] +#[derive(sqlx::Type, Serialize, Deserialize, Debug, Clone, Copy)] +#[sqlx(rename_all = "lowercase", type_name = "varchar")] pub enum RateCategory { - Application, // app, feature, extension - Cloud, // is user satisfied working with this cloud - Stack, // app stack + Application, // app, feature, extension + Cloud, // is user satisfied working with this cloud + Stack, // app stack DeploymentSpeed, Documentation, Design, TechSupport, Price, - MemoryUsage + MemoryUsage, } impl Into for RateCategory { @@ -57,4 +55,3 @@ pub struct Rules { // example: allow to add only a single comment comments_per_user: i32, // default = 1 } - diff --git a/src/routes/rating.rs b/src/routes/rating.rs index 1b1ed13..3534689 100644 --- a/src/routes/rating.rs +++ b/src/routes/rating.rs @@ -1,12 +1,12 @@ +use crate::models::rating::RateCategory; +use crate::startup::AppState; use actix_web::{web, HttpResponse}; use serde::{Deserialize, Serialize}; -use crate::models::rating::RateCategory; use serde_valid::Validate; use sqlx::PgPool; use tracing::instrument; -use uuid::Uuid; -use crate::startup::AppState; use tracing::Instrument; +use uuid::Uuid; // workflow // add, update, list, get(user_id), ACL, @@ -15,27 +15,29 @@ use tracing::Instrument; #[derive(Serialize, Deserialize, Debug, Validate)] pub struct RatingForm { - pub obj_id: i32, // product external id - pub category: RateCategory, // rating of product | rating of service etc + pub obj_id: i32, // product external id + pub category: RateCategory, // rating of product | rating of service etc #[validate(max_length = 1000)] - pub comment: Option, // always linked to a product + pub comment: Option, // always linked to a product #[validate(minimum = 0)] #[validate(maximum = 10)] - pub rate: i32, // + pub rate: i32, // } -pub async fn rating(app_state: web::Data, form: web::Json, pool: -web::Data) -> HttpResponse { +pub async fn rating( + app_state: web::Data, + form: web::Json, + pool: web::Data, +) -> HttpResponse { + //TODO. check if there already exists a rating for this product committed by this user + //TODO. check if this obj_id exists let request_id = Uuid::new_v4(); let user_id = app_state.user_id; // uuid Let's assume we have a user id already taken from auth - - let query_span = tracing::info_span!( - "Saving new rating details in the database" - ); + let query_span = tracing::info_span!("Saving new rating details in the database"); // Get product by id // Insert rating - let category = Into::::into(form.category.clone()); + //let category = Into::::into(form.category.clone()); match sqlx::query!( r#" INSERT INTO rating (user_id, product_id, category, comment, hidden,rate, @@ -45,7 +47,7 @@ web::Data) -> HttpResponse { "#, user_id, form.obj_id, - category.as_str(), + form.category as RateCategory, form.comment, false, form.rate @@ -65,7 +67,5 @@ web::Data) -> HttpResponse { tracing::error!("req_id: {} Failed to execute query: {:?}", request_id, e); HttpResponse::InternalServerError().finish() } - }; - println!("{:?}", form); - HttpResponse::Ok().finish() + } } From ece5118a81fb961bb38a5a6139329f0e132c3557 Mon Sep 17 00:00:00 2001 From: Petru Date: Thu, 21 Sep 2023 18:07:51 +0300 Subject: [PATCH 22/29] issue_enum: fetch product when adding a rating --- src/models/rating.rs | 4 ++-- src/routes/rating.rs | 25 +++++++++++++++++++++++-- 2 files changed, 25 insertions(+), 4 deletions(-) diff --git a/src/models/rating.rs b/src/models/rating.rs index 0851674..dfeba35 100644 --- a/src/models/rating.rs +++ b/src/models/rating.rs @@ -11,9 +11,9 @@ pub struct Product { // product type stack & app, // id is generated based on the product type and external obj_id pub id: i32, //primary key, for better data management - pub obj_id: u32, // external product ID db, no autoincrement, example: 100 + pub obj_id: i32, // external product ID db, no autoincrement, example: 100 pub obj_type: String, // stack | app, unique index - pub rating: Rating, // 0-10 + //pub rating: Rating, // 0-10 TODO sqlx + select + foreign keys // pub rules: Rules, pub created_at: DateTime, pub updated_at: DateTime, diff --git a/src/routes/rating.rs b/src/routes/rating.rs index 3534689..3110f2f 100644 --- a/src/routes/rating.rs +++ b/src/routes/rating.rs @@ -1,3 +1,4 @@ +use crate::models::rating::Product; use crate::models::rating::RateCategory; use crate::startup::AppState; use actix_web::{web, HttpResponse}; @@ -30,10 +31,30 @@ pub async fn rating( pool: web::Data, ) -> HttpResponse { //TODO. check if there already exists a rating for this product committed by this user - //TODO. check if this obj_id exists let request_id = Uuid::new_v4(); - let user_id = app_state.user_id; // uuid Let's assume we have a user id already taken from auth + match sqlx::query_as!( + Product, + r"SELECT * FROM product WHERE obj_id = $1", + form.obj_id + ) + .fetch_one(pool.get_ref()) + .await + { + Ok(product) => { + tracing::info!("req_id: {} Found product: {:?}", request_id, product.obj_id); + } + Err(e) => { + tracing::error!( + "req_id: {} Failed to fetch product: {:?}, error: {:?}", + request_id, + form.obj_id, + e + ); + return HttpResponse::InternalServerError().finish(); + } + }; + let user_id = app_state.user_id; // uuid Let's assume we have a user id already taken from auth let query_span = tracing::info_span!("Saving new rating details in the database"); // Get product by id // Insert rating From afd860b210580c7f1ddc835250fb8663132c60fe Mon Sep 17 00:00:00 2001 From: Petru Date: Fri, 22 Sep 2023 17:09:40 +0300 Subject: [PATCH 23/29] issue_enum - removed rating column --- src/models/rating.rs | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/models/rating.rs b/src/models/rating.rs index dfeba35..b1242dd 100644 --- a/src/models/rating.rs +++ b/src/models/rating.rs @@ -13,8 +13,6 @@ pub struct Product { pub id: i32, //primary key, for better data management pub obj_id: i32, // external product ID db, no autoincrement, example: 100 pub obj_type: String, // stack | app, unique index - //pub rating: Rating, // 0-10 TODO sqlx + select + foreign keys - // pub rules: Rules, pub created_at: DateTime, pub updated_at: DateTime, } @@ -22,6 +20,7 @@ pub struct Product { pub struct Rating { pub id: i32, pub user_id: Uuid, // external user_id, 100, taken using token (middleware?) + pub product_id: i32, //primary key, for better data management pub category: String, // rating of product | rating of service etc pub comment: String, // always linked to a product pub hidden: bool, // rating can be hidden for non-adequate user behaviour From f32b19078a43cf6d370750622ac52fb9a32a0598 Mon Sep 17 00:00:00 2001 From: Petru Date: Fri, 22 Sep 2023 17:32:16 +0300 Subject: [PATCH 24/29] issuue_enum - separate namespace for forms --- src/forms/mod.rs | 3 +++ src/forms/rating.rs | 14 ++++++++++++++ src/lib.rs | 9 +++++---- src/models/mod.rs | 4 +++- src/routes/rating.rs | 23 +++++------------------ 5 files changed, 30 insertions(+), 23 deletions(-) create mode 100644 src/forms/mod.rs create mode 100644 src/forms/rating.rs diff --git a/src/forms/mod.rs b/src/forms/mod.rs new file mode 100644 index 0000000..7a10a3e --- /dev/null +++ b/src/forms/mod.rs @@ -0,0 +1,3 @@ +mod rating; + +pub use rating::*; diff --git a/src/forms/rating.rs b/src/forms/rating.rs new file mode 100644 index 0000000..76efca4 --- /dev/null +++ b/src/forms/rating.rs @@ -0,0 +1,14 @@ +use crate::models; +use serde::{Deserialize, Serialize}; +use serde_valid::Validate; + +#[derive(Serialize, Deserialize, Debug, Validate)] +pub struct Rating { + pub obj_id: i32, // product external id + pub category: models::RateCategory, // rating of product | rating of service etc + #[validate(max_length = 1000)] + pub comment: Option, // always linked to a product + #[validate(minimum = 0)] + #[validate(maximum = 10)] + pub rate: i32, // +} diff --git a/src/lib.rs b/src/lib.rs index 911ec72..9d3cc9b 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,7 +1,8 @@ -pub mod routes; -pub mod startup; pub mod configuration; -pub mod telemetry; +pub mod forms; mod middleware; -mod models; +pub mod models; +pub mod routes; pub mod services; +pub mod startup; +pub mod telemetry; diff --git a/src/models/mod.rs b/src/models/mod.rs index 7595e12..c3cbfed 100644 --- a/src/models/mod.rs +++ b/src/models/mod.rs @@ -1,3 +1,5 @@ pub mod rating; +pub mod stack; pub mod user; -pub mod stack; \ No newline at end of file + +pub use rating::*; diff --git a/src/routes/rating.rs b/src/routes/rating.rs index 3110f2f..7aea2f5 100644 --- a/src/routes/rating.rs +++ b/src/routes/rating.rs @@ -1,9 +1,7 @@ -use crate::models::rating::Product; -use crate::models::rating::RateCategory; +use crate::forms; +use crate::models; use crate::startup::AppState; use actix_web::{web, HttpResponse}; -use serde::{Deserialize, Serialize}; -use serde_valid::Validate; use sqlx::PgPool; use tracing::instrument; use tracing::Instrument; @@ -14,26 +12,15 @@ use uuid::Uuid; // ACL - access to func for a user // ACL - access to objects for a user -#[derive(Serialize, Deserialize, Debug, Validate)] -pub struct RatingForm { - pub obj_id: i32, // product external id - pub category: RateCategory, // rating of product | rating of service etc - #[validate(max_length = 1000)] - pub comment: Option, // always linked to a product - #[validate(minimum = 0)] - #[validate(maximum = 10)] - pub rate: i32, // -} - pub async fn rating( app_state: web::Data, - form: web::Json, + form: web::Json, pool: web::Data, ) -> HttpResponse { //TODO. check if there already exists a rating for this product committed by this user let request_id = Uuid::new_v4(); match sqlx::query_as!( - Product, + models::Product, r"SELECT * FROM product WHERE obj_id = $1", form.obj_id ) @@ -68,7 +55,7 @@ pub async fn rating( "#, user_id, form.obj_id, - form.category as RateCategory, + form.category as models::RateCategory, form.comment, false, form.rate From a8b3814715bea57a979cf90a88377b0ddcfefc01 Mon Sep 17 00:00:00 2001 From: Petru Date: Sun, 24 Sep 2023 08:44:12 +0300 Subject: [PATCH 25/29] issue_enum --- src/routes/rating.rs | 1 + 1 file changed, 1 insertion(+) diff --git a/src/routes/rating.rs b/src/routes/rating.rs index 7aea2f5..33a65a5 100644 --- a/src/routes/rating.rs +++ b/src/routes/rating.rs @@ -65,6 +65,7 @@ pub async fn rating( .await { Ok(_) => { + //TODO return json containing the id of the new rating tracing::info!( "req_id: {} New subscriber details have been saved to database", request_id From d8c32cb9b579c2be666fd7d027af425013ad945a Mon Sep 17 00:00:00 2001 From: vsilent Date: Sun, 24 Sep 2023 11:14:00 +0300 Subject: [PATCH 26/29] Rating: check vote existence, query spans, json response --- Cargo.lock | 1 - Cargo.toml | 2 +- README.md | 25 +++++++----- src/routes/rating.rs | 84 ++++++++++++++++++++++++++++++++++------- src/routes/stack/add.rs | 13 ++++--- src/startup.rs | 5 ++- 6 files changed, 98 insertions(+), 32 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index ed3da6b..908b64a 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1386,7 +1386,6 @@ checksum = "5b40af805b3121feab8a3c29f04d8ad262fa8e0561883e7653e024ae4479e6de" [[package]] name = "proc-macro-error" version = "1.0.4" - source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "da25490ff9892aab3fcf7c36f08cfb902dd3e71ca0f9f9517bea02a73a5ce38c" dependencies = [ diff --git a/Cargo.toml b/Cargo.toml index 049cbfd..342ca3d 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -26,7 +26,7 @@ tracing-subscriber = { version = "0.3.17", features = ["registry", "env-filter"] uuid = { version = "1.3.4", features = ["v4"] } thiserror = "1.0" serde_valid = "0.16.3" -serde_json = "1.0.105" +serde_json = { version = "1.0.105", features = [] } serde_derive = "1.0.188" [dependencies.sqlx] diff --git a/README.md b/README.md index 812e4d7..a708c47 100644 --- a/README.md +++ b/README.md @@ -5,16 +5,20 @@ Stacker - is an application that helps users to create custom IT solutions based source apps and user's custom applications docker containers. Users can build their own stack of applications, and deploy the final result to their favorite clouds using TryDirect API. -Application will consist of: -1. Web UI (Application Stack builder) -2. Back-end RESTful API, includes: - a. Security module. User Authorization - b. Application Management - c. Cloud Provider Key Management - d. docker-compose generator - e. TryDirect API Client - f. Rating module - +Application development will include: +- Web UI (Application Stack builder) +- Command line interface +- Back-end RESTful API, includes: + - [ ] Security module. + - [ ] User Authorization + - [ ] Application Management + - [ ] Application Key Management + - [ ] Cloud Provider Key Management + - [ ] docker-compose.yml generator + - [ ] TryDirect API Client + - [ ] Rating module + +## How to start #### Run db migration @@ -31,6 +35,7 @@ sqlx migrate revert ``` +## CURL examples #### Rate Product ``` diff --git a/src/routes/rating.rs b/src/routes/rating.rs index 33a65a5..e930624 100644 --- a/src/routes/rating.rs +++ b/src/routes/rating.rs @@ -1,30 +1,41 @@ use crate::forms; use crate::models; use crate::startup::AppState; -use actix_web::{web, HttpResponse}; +use actix_web::{web, HttpResponse, Responder, Result}; +use serde_derive::Serialize; use sqlx::PgPool; -use tracing::instrument; use tracing::Instrument; use uuid::Uuid; +use crate::models::RateCategory; // workflow // add, update, list, get(user_id), ACL, // ACL - access to func for a user // ACL - access to objects for a user +#[derive(Serialize)] +struct JsonResponse { + status: String, + message: String, + code: u32, + id: Option +} + pub async fn rating( app_state: web::Data, form: web::Json, pool: web::Data, -) -> HttpResponse { +) -> Result { //TODO. check if there already exists a rating for this product committed by this user let request_id = Uuid::new_v4(); + let query_span = tracing::info_span!("Check product existence by id."); match sqlx::query_as!( models::Product, r"SELECT * FROM product WHERE obj_id = $1", form.obj_id ) .fetch_one(pool.get_ref()) + .instrument(query_span) .await { Ok(product) => { @@ -37,21 +48,55 @@ pub async fn rating( form.obj_id, e ); - return HttpResponse::InternalServerError().finish(); + // return HttpResponse::InternalServerError().finish(); + return Ok(web::Json(JsonResponse { + status : "Error".to_string(), + code: 404, + message: format!("Object not found {}", form.obj_id), + id: None + })); } }; - let user_id = app_state.user_id; // uuid Let's assume we have a user id already taken from auth - let query_span = tracing::info_span!("Saving new rating details in the database"); + let user_id = app_state.user_id; // uuid Let's assume user_id already taken from auth + + let query_span = tracing::info_span!("Search for existing vote."); + match sqlx::query!( + r"SELECT id FROM rating where user_id=$1 AND product_id=$2 AND category=$3 LIMIT 1", + user_id, + form.obj_id, + form.category as RateCategory + ) + .fetch_one(pool.get_ref()) + .instrument(query_span) + .await + { + Ok(record) => { + tracing::info!("req_id: {} rating exists: {:?}, user: {}, product: {}, category: {:?}", + request_id, record.id, user_id, form.obj_id, form.category); + + return Ok(web::Json(JsonResponse{ + status: "Error".to_string(), + code: 409, + message: format!("Already Rated"), + id: Some(record.id) + })); + } + Err(err) => { + // @todo, match the sqlx response + } + } + + let query_span = tracing::info_span!("Saving new rating details into the database"); // Get product by id // Insert rating - //let category = Into::::into(form.category.clone()); match sqlx::query!( r#" INSERT INTO rating (user_id, product_id, category, comment, hidden,rate, created_at, updated_at) VALUES ($1, $2, $3, $4, $5, $6, NOW() at time zone 'utc', NOW() at time zone 'utc') + RETURNING id "#, user_id, form.obj_id, @@ -60,21 +105,34 @@ pub async fn rating( false, form.rate ) - .execute(pool.get_ref()) + .fetch_one(pool.get_ref()) .instrument(query_span) .await { - Ok(_) => { + Ok(result) => { + println!("Query returned {:?}", result); //TODO return json containing the id of the new rating tracing::info!( - "req_id: {} New subscriber details have been saved to database", - request_id + "req_id: {} New rating {} have been saved to database", + request_id, + result.id ); - HttpResponse::Ok().finish() + + Ok(web::Json(JsonResponse { + status : "ok".to_string(), + code: 200, + message: "Saved".to_string(), + id: Some(result.id) + })) } Err(e) => { tracing::error!("req_id: {} Failed to execute query: {:?}", request_id, e); - HttpResponse::InternalServerError().finish() + Ok(web::Json(JsonResponse{ + status: "error".to_string(), + code: 500, + message: "Failed to insert".to_string(), + id: None + })) } } } diff --git a/src/routes/stack/add.rs b/src/routes/stack/add.rs index def23ae..67379a5 100644 --- a/src/routes/stack/add.rs +++ b/src/routes/stack/add.rs @@ -5,9 +5,10 @@ use sqlx::PgPool; use tracing::Instrument; use uuid::Uuid; use chrono::Utc; -use crate::models::stack::FormData; +use crate::models::stack::{FormData}; use crate::startup::AppState; use std::str; +use actix_web::web::Form; // pub async fn add(req: HttpRequest, app_state: Data, pool: @@ -22,9 +23,11 @@ pub async fn add(body: Bytes) -> Result { // method 1 // let app_state: AppState = serde_json::from_str(body_str).unwrap(); // method 2 - let app_state = serde_json::from_str::(body_str).unwrap(); - println!("request: {:?}", app_state); - // // println!("app: {:?}", body); + // let app_state = serde_json::from_str::(body_str).unwrap(); + // println!("request: {:?}", app_state); + + let stack = serde_json::from_str::(body_str).unwrap(); + println!("app: {:?}", stack); // println!("user_id: {:?}", data.user_id); // tracing::info!("we are here"); // match Json::::extract(&req).await { @@ -84,6 +87,6 @@ pub async fn add(body: Bytes) -> Result { // // } // HttpResponse::Ok().finish() - Ok(Json(app_state)) + Ok(Json(stack)) // Ok(HttpResponse::Ok().finish()) } diff --git a/src/startup.rs b/src/startup.rs index fc739ad..a9ad2fd 100644 --- a/src/startup.rs +++ b/src/startup.rs @@ -7,8 +7,9 @@ use actix_web::{ }; use sqlx::PgPool; use std::net::TcpListener; -use serde_derive::{Deserialize, Serialize}; -use uuid::Uuid; +use serde::{Deserialize, Serialize}; +// use serde_derive::{Deserialize, Serialize}; +// use uuid::Uuid; #[derive(Serialize, Deserialize, Debug)] pub struct AppState { From 882f76a15c442bca8efc668a1537ec5228795d89 Mon Sep 17 00:00:00 2001 From: Petru Date: Fri, 29 Sep 2023 16:56:54 +0300 Subject: [PATCH 27/29] issue-3 bearer_guard --- Cargo.lock | 33 +++++++++++++++++++++++++++++++++ Cargo.toml | 2 ++ src/startup.rs | 49 +++++++++++++++++++++++++++---------------------- 3 files changed, 62 insertions(+), 22 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 908b64a..fc75567 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -19,6 +19,21 @@ dependencies = [ "tokio-util", ] +[[package]] +name = "actix-cors" +version = "0.6.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b340e9cfa5b08690aae90fb61beb44e9b06f44fe3d0f93781aaa58cfba86245e" +dependencies = [ + "actix-utils", + "actix-web", + "derive_more", + "futures-util", + "log", + "once_cell", + "smallvec", +] + [[package]] name = "actix-http" version = "3.3.1" @@ -183,6 +198,21 @@ dependencies = [ "syn 1.0.109", ] +[[package]] +name = "actix-web-httpauth" +version = "0.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1d613edf08a42ccc6864c941d30fe14e1b676a77d16f1dbadc1174d065a0a775" +dependencies = [ + "actix-utils", + "actix-web", + "base64 0.21.0", + "futures-core", + "futures-util", + "log", + "pin-project-lite", +] + [[package]] name = "adler" version = "1.0.2" @@ -693,6 +723,7 @@ dependencies = [ "futures-task", "pin-project-lite", "pin-utils", + "slab", ] [[package]] @@ -1961,7 +1992,9 @@ dependencies = [ name = "stacker" version = "0.1.0" dependencies = [ + "actix-cors", "actix-web", + "actix-web-httpauth", "chrono", "config", "reqwest", diff --git a/Cargo.toml b/Cargo.toml index 342ca3d..d1b3e87 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -28,6 +28,8 @@ thiserror = "1.0" serde_valid = "0.16.3" serde_json = { version = "1.0.105", features = [] } serde_derive = "1.0.188" +actix-web-httpauth = "0.8.1" +actix-cors = "0.6.4" [dependencies.sqlx] version = "0.6.3" diff --git a/src/startup.rs b/src/startup.rs index a9ad2fd..da9b592 100644 --- a/src/startup.rs +++ b/src/startup.rs @@ -1,31 +1,44 @@ -use actix_web::dev::Server; +use actix_cors::Cors; +use actix_web::dev::{Server, ServiceRequest}; use actix_web::middleware::Logger; use actix_web::{ // http::header::HeaderName, web::{self, Form}, - App, HttpServer, + App, + Error, + HttpServer, }; +use actix_web_httpauth::{extractors::bearer::BearerAuth, middleware::HttpAuthentication}; +use serde::{Deserialize, Serialize}; use sqlx::PgPool; use std::net::TcpListener; -use serde::{Deserialize, Serialize}; -// use serde_derive::{Deserialize, Serialize}; -// use uuid::Uuid; #[derive(Serialize, Deserialize, Debug)] pub struct AppState { - pub user_id: i32 // @todo User must be move later to actix session and obtained from auth + pub user_id: i32, // @todo User must be move later to actix session and obtained from auth } +async fn bearer_guard( + req: ServiceRequest, + credentials: BearerAuth, +) -> Result { + eprintln!("{credentials:?}"); + //todo check that credentials.token is a real. get in sync with auth server + //todo get user from auth server + //todo save the server in the request state + //todo get the user in the rating route + Ok(req) +} pub fn run(listener: TcpListener, db_pool: PgPool) -> Result { let db_pool = web::Data::new(db_pool); let server = HttpServer::new(move || { App::new() .wrap(Logger::default()) + .wrap(HttpAuthentication::bearer(bearer_guard)) + .wrap(Cors::permissive()) .service( - web::resource("/health_check") - .route(web::get() - .to(crate::routes::health_check)), + web::resource("/health_check").route(web::get().to(crate::routes::health_check)), ) .service( web::resource("/rating") @@ -41,23 +54,15 @@ pub fn run(listener: TcpListener, db_pool: PgPool) -> Result Date: Sat, 30 Sep 2023 21:04:51 +0300 Subject: [PATCH 28/29] issue-3. request state. user.id --- src/models/stack.rs | 10 +++---- src/models/user.rs | 5 ++-- src/routes/rating.rs | 59 ++++++++++++++++++++++------------------- src/routes/stack/add.rs | 19 ++++++------- src/startup.rs | 17 +++++++----- 5 files changed, 57 insertions(+), 53 deletions(-) diff --git a/src/models/stack.rs b/src/models/stack.rs index 8cb79b1..ad8ebf8 100644 --- a/src/models/stack.rs +++ b/src/models/stack.rs @@ -1,8 +1,8 @@ -use uuid::Uuid; use chrono::{DateTime, Utc}; use serde_derive::Deserialize; use serde_derive::Serialize; use serde_json::Value; +use uuid::Uuid; pub struct Stack { pub id: Uuid, // id - is a unique identifier for the app stack @@ -14,7 +14,6 @@ pub struct Stack { pub updated_at: DateTime, } - #[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize)] #[serde(rename_all = "camelCase")] pub struct FormData { @@ -45,8 +44,7 @@ pub struct FormData { #[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize)] #[serde(rename_all = "camelCase")] -pub struct DomainList { -} +pub struct DomainList {} #[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize)] #[serde(rename_all = "camelCase")] @@ -179,8 +177,7 @@ pub struct IconLight { #[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize)] #[serde(rename_all = "camelCase")] -pub struct IconDark { -} +pub struct IconDark {} #[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize)] #[serde(rename_all = "camelCase")] @@ -256,4 +253,3 @@ pub struct Service { pub shared_ports: Vec, pub main: bool, } - diff --git a/src/models/user.rs b/src/models/user.rs index 9e86a34..0c57db4 100644 --- a/src/models/user.rs +++ b/src/models/user.rs @@ -1,3 +1,4 @@ +#[derive(Debug, Copy, Clone)] pub struct User { - -} \ No newline at end of file + pub id: i32, +} diff --git a/src/routes/rating.rs b/src/routes/rating.rs index e930624..bc3995c 100644 --- a/src/routes/rating.rs +++ b/src/routes/rating.rs @@ -1,12 +1,12 @@ use crate::forms; use crate::models; -use crate::startup::AppState; +use crate::models::user::User; +use crate::models::RateCategory; use actix_web::{web, HttpResponse, Responder, Result}; use serde_derive::Serialize; use sqlx::PgPool; use tracing::Instrument; use uuid::Uuid; -use crate::models::RateCategory; // workflow // add, update, list, get(user_id), ACL, @@ -18,11 +18,11 @@ struct JsonResponse { status: String, message: String, code: u32, - id: Option + id: Option, } pub async fn rating( - app_state: web::Data, + user: web::ReqData, form: web::Json, pool: web::Data, ) -> Result { @@ -50,40 +50,44 @@ pub async fn rating( ); // return HttpResponse::InternalServerError().finish(); return Ok(web::Json(JsonResponse { - status : "Error".to_string(), + status: "Error".to_string(), code: 404, message: format!("Object not found {}", form.obj_id), - id: None + id: None, })); } }; - let user_id = app_state.user_id; // uuid Let's assume user_id already taken from auth - let query_span = tracing::info_span!("Search for existing vote."); match sqlx::query!( r"SELECT id FROM rating where user_id=$1 AND product_id=$2 AND category=$3 LIMIT 1", - user_id, + user.id, form.obj_id, form.category as RateCategory ) - .fetch_one(pool.get_ref()) - .instrument(query_span) - .await + .fetch_one(pool.get_ref()) + .instrument(query_span) + .await { - Ok(record) => { - tracing::info!("req_id: {} rating exists: {:?}, user: {}, product: {}, category: {:?}", - request_id, record.id, user_id, form.obj_id, form.category); + Ok(record) => { + tracing::info!( + "req_id: {} rating exists: {:?}, user: {}, product: {}, category: {:?}", + request_id, + record.id, + user.id, + form.obj_id, + form.category + ); - return Ok(web::Json(JsonResponse{ + return Ok(web::Json(JsonResponse { status: "Error".to_string(), code: 409, message: format!("Already Rated"), - id: Some(record.id) + id: Some(record.id), })); } Err(err) => { - // @todo, match the sqlx response + // @todo, match the sqlx response } } @@ -98,7 +102,7 @@ pub async fn rating( VALUES ($1, $2, $3, $4, $5, $6, NOW() at time zone 'utc', NOW() at time zone 'utc') RETURNING id "#, - user_id, + user.id, form.obj_id, form.category as models::RateCategory, form.comment, @@ -111,7 +115,6 @@ pub async fn rating( { Ok(result) => { println!("Query returned {:?}", result); - //TODO return json containing the id of the new rating tracing::info!( "req_id: {} New rating {} have been saved to database", request_id, @@ -119,20 +122,20 @@ pub async fn rating( ); Ok(web::Json(JsonResponse { - status : "ok".to_string(), + status: "ok".to_string(), code: 200, message: "Saved".to_string(), - id: Some(result.id) + id: Some(result.id), })) } Err(e) => { tracing::error!("req_id: {} Failed to execute query: {:?}", request_id, e); - Ok(web::Json(JsonResponse{ - status: "error".to_string(), - code: 500, - message: "Failed to insert".to_string(), - id: None - })) + Ok(web::Json(JsonResponse { + status: "error".to_string(), + code: 500, + message: "Failed to insert".to_string(), + id: None, + })) } } } diff --git a/src/routes/stack/add.rs b/src/routes/stack/add.rs index 67379a5..4ea14aa 100644 --- a/src/routes/stack/add.rs +++ b/src/routes/stack/add.rs @@ -1,18 +1,19 @@ -use std::io::Read; -use actix_web::{web::{Data, Bytes, Json}, HttpResponse, HttpRequest, Responder, Result}; +use crate::models::stack::FormData; use actix_web::error::{Error, JsonPayloadError, PayloadError}; +use actix_web::web::Form; +use actix_web::{ + web::{Bytes, Data, Json}, + HttpRequest, HttpResponse, Responder, Result, +}; +use chrono::Utc; use sqlx::PgPool; +use std::io::Read; +use std::str; use tracing::Instrument; use uuid::Uuid; -use chrono::Utc; -use crate::models::stack::{FormData}; -use crate::startup::AppState; -use std::str; -use actix_web::web::Form; - // pub async fn add(req: HttpRequest, app_state: Data, pool: -pub async fn add(body: Bytes) -> Result { +pub async fn add(body: Bytes) -> Result { // None::.expect("my error"); // return Err(JsonPayloadError::Payload(PayloadError::Overflow).into()); // let content_type = req.headers().get("content-type"); diff --git a/src/startup.rs b/src/startup.rs index da9b592..5999bae 100644 --- a/src/startup.rs +++ b/src/startup.rs @@ -1,22 +1,19 @@ use actix_cors::Cors; use actix_web::dev::{Server, ServiceRequest}; use actix_web::middleware::Logger; +use actix_web::HttpMessage; use actix_web::{ // http::header::HeaderName, - web::{self, Form}, + web::{self}, App, Error, HttpServer, }; use actix_web_httpauth::{extractors::bearer::BearerAuth, middleware::HttpAuthentication}; -use serde::{Deserialize, Serialize}; use sqlx::PgPool; use std::net::TcpListener; -#[derive(Serialize, Deserialize, Debug)] -pub struct AppState { - pub user_id: i32, // @todo User must be move later to actix session and obtained from auth -} +use crate::models::user::User; async fn bearer_guard( req: ServiceRequest, @@ -27,6 +24,13 @@ async fn bearer_guard( //todo get user from auth server //todo save the server in the request state //todo get the user in the rating route + let user = User { id: 1 }; + tracing::info!("authentication middleware. {user:?}"); + let existent_user = req.extensions_mut().insert(user); + if existent_user.is_some() { + tracing::error!("authentication middleware. already logged {existent_user:?}"); + //return Err(("".into(), req)); + } Ok(req) } @@ -59,7 +63,6 @@ pub fn run(listener: TcpListener, db_pool: PgPool) -> Result Date: Sun, 1 Oct 2023 08:37:30 +0300 Subject: [PATCH 29/29] issue-3 response from user service --- src/models/user.rs | 4 +++- src/startup.rs | 29 ++++++++++++++++++++++++++--- 2 files changed, 29 insertions(+), 4 deletions(-) diff --git a/src/models/user.rs b/src/models/user.rs index 0c57db4..f345e40 100644 --- a/src/models/user.rs +++ b/src/models/user.rs @@ -1,4 +1,6 @@ -#[derive(Debug, Copy, Clone)] +use serde::Deserialize; + +#[derive(Debug, Copy, Clone, Deserialize)] pub struct User { pub id: i32, } diff --git a/src/startup.rs b/src/startup.rs index 5999bae..615fcab 100644 --- a/src/startup.rs +++ b/src/startup.rs @@ -10,6 +10,7 @@ use actix_web::{ HttpServer, }; use actix_web_httpauth::{extractors::bearer::BearerAuth, middleware::HttpAuthentication}; +use reqwest::header::{ACCEPT, CONTENT_TYPE}; use sqlx::PgPool; use std::net::TcpListener; @@ -22,9 +23,31 @@ async fn bearer_guard( eprintln!("{credentials:?}"); //todo check that credentials.token is a real. get in sync with auth server //todo get user from auth server - //todo save the server in the request state - //todo get the user in the rating route - let user = User { id: 1 }; + + let client = reqwest::Client::new(); + let resp = client + .get("https://65190108818c4e98ac6000e4.mockapi.io/user/1") //todo add the right url + .bearer_auth(credentials.token()) + .header(CONTENT_TYPE, "application/json") + .header(ACCEPT, "application/json") + .send() + .await + .unwrap() //todo process the response rightly. At moment it's some of something + ; + eprintln!("{resp:?}"); + + let user: User = match resp.status() { + reqwest::StatusCode::OK => match resp.json().await { + Ok(user) => user, + Err(err) => panic!("can't parse the user from json {err:?}"), //todo + }, + other => { + //todo process the other status code accordingly + panic!("unexpected status code {other}"); + } + }; + + //let user = User { id: 1 }; tracing::info!("authentication middleware. {user:?}"); let existent_user = req.extensions_mut().insert(user); if existent_user.is_some() {