From 421f940eafc77e523d55d9fcb7f8b0fa6e300f0f Mon Sep 17 00:00:00 2001
From: vsilent <jabberroid@gmail.com>
Date: Sun, 5 Nov 2023 09:19:48 +0200
Subject: [PATCH 01/18] rename default to list_handler

---
 src/routes/rating/get.rs | 2 +-
 src/routes/stack/get.rs  | 2 +-
 src/startup.rs           | 2 +-
 3 files changed, 3 insertions(+), 3 deletions(-)

diff --git a/src/routes/rating/get.rs b/src/routes/rating/get.rs
index 011f9431..2f63c766 100644
--- a/src/routes/rating/get.rs
+++ b/src/routes/rating/get.rs
@@ -69,7 +69,7 @@ pub async fn get_handler(
 
 #[tracing::instrument(name = "Get all ratings.")]
 #[get("")]
-pub async fn default(
+pub async fn list_handler(
     path: web::Path<()>,
     pool: web::Data<PgPool>,
 ) -> Result<impl Responder> {
diff --git a/src/routes/stack/get.rs b/src/routes/stack/get.rs
index 17f91816..1497f9e1 100644
--- a/src/routes/stack/get.rs
+++ b/src/routes/stack/get.rs
@@ -24,7 +24,7 @@ pub async fn get(
 
     let (id,) = path.into_inner();
 
-    tracing::info!("User {:?} is getting stack by id {:?}", user, id);
+    tracing::info!("User {:?} gets stack by id {:?}", user.id, id);
     match sqlx::query_as!(
         models::Stack,
         r#"
diff --git a/src/startup.rs b/src/startup.rs
index e6115cfd..edd58c1b 100644
--- a/src/startup.rs
+++ b/src/startup.rs
@@ -112,7 +112,7 @@ pub async fn run(
                     .wrap(Cors::permissive())
                     .service(crate::routes::rating::add_handler)
                     .service(crate::routes::rating::get_handler)
-                    .service(crate::routes::rating::default),
+                    .service(crate::routes::rating::list_handler),
             )
             // .service(
             //     web::resource("/stack/{id}")

From 1a5016016860eebc9c0724d316c448490a582314 Mon Sep 17 00:00:00 2001
From: vsilent <jabberroid@gmail.com>
Date: Mon, 6 Nov 2023 14:02:57 +0200
Subject: [PATCH 02/18] Proposal to use unified JsonResponse implementation,
 this should reduce the number of lines in the response code

---
 custom-stack-payload-4.json                   |   1 +
 ...0230905145525_creating_stack_tables.up.sql |   5 +-
 src/forms/user.rs                             |  56 +++----
 src/helpers/json.rs                           | 156 ++++++++++++++++--
 src/routes/client/add.rs                      |  50 ++----
 src/routes/rating/add.rs                      |  44 ++---
 src/routes/rating/get.rs                      |  75 +++------
 src/routes/stack/add.rs                       |  41 +----
 src/routes/stack/get.rs                       |  48 ++----
 9 files changed, 243 insertions(+), 233 deletions(-)
 create mode 100644 custom-stack-payload-4.json

diff --git a/custom-stack-payload-4.json b/custom-stack-payload-4.json
new file mode 100644
index 00000000..4f0ce614
--- /dev/null
+++ b/custom-stack-payload-4.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":"<p class=\"content-editor__unstyled\"><strong style=\"font-weight:bold\">Portainer is a lightweight management UI which allows you to easily manage your different Docker environments (Docker hosts or Swarm clusters)</strong></p>","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":"<p class=\"content-editor__unstyled\">PostgREST description</p>","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":"mysampleproject2","custom_stack_code":"another-bot2","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/migrations/20230905145525_creating_stack_tables.up.sql b/migrations/20230905145525_creating_stack_tables.up.sql
index e908e970..c2d248c2 100644
--- a/migrations/20230905145525_creating_stack_tables.up.sql
+++ b/migrations/20230905145525_creating_stack_tables.up.sql
@@ -1,12 +1,13 @@
 -- Add up migration script here
 -- Add migration script here
 CREATE TABLE user_stack (
-    id serial,
+    id serial4 NOT NULL,
     stack_id uuid NOT NULL,
     user_id VARCHAR(50) NOT NULL,
     name TEXT NOT NULL UNIQUE,
     body JSON NOT NULL,
     created_at timestamptz NOT NULL,
-    updated_at timestamptz NOT NULL
+    updated_at timestamptz NOT NULL,
+        CONSTRAINT user_stack_pkey PRIMARY KEY (id),
 )
 
diff --git a/src/forms/user.rs b/src/forms/user.rs
index 2da0dcf8..8ff0b4ce 100644
--- a/src/forms/user.rs
+++ b/src/forms/user.rs
@@ -16,45 +16,45 @@ pub struct User {
     #[serde(rename = "_id")]
     pub id: String,
     #[serde(rename = "first_name")]
-    pub first_name: String,
+    pub first_name: Option<String>,
     #[serde(rename = "last_name")]
-    pub last_name: String,
-    pub created: String,
-    pub updated: String,
+    pub last_name: Option<String>,
+    pub created: Option<String>,
+    pub updated: Option<String>,
     pub email: String,
     #[serde(rename = "email_confirmed")]
     pub email_confirmed: bool,
     pub social: bool,
-    pub website: String,
+    pub website: Option<String>,
     pub currency: Value,
-    pub phone: String,
+    pub phone: Option<String>,
     #[serde(rename = "password_change_required")]
     pub password_change_required: Value,
-    pub photo: String,
-    pub country: String,
+    pub photo: Option<String>,
+    pub country: Option<String>,
     #[serde(rename = "billing_first_name")]
     pub billing_first_name: Value,
     #[serde(rename = "billing_last_name")]
     pub billing_last_name: Value,
     #[serde(rename = "billing_postcode")]
-    pub billing_postcode: String,
+    pub billing_postcode: Option<String>,
     #[serde(rename = "billing_address_1")]
-    pub billing_address_1: String,
+    pub billing_address_1: Option<String>,
     #[serde(rename = "billing_address_2")]
-    pub billing_address_2: String,
+    pub billing_address_2: Option<String>,
     #[serde(rename = "billing_city")]
-    pub billing_city: String,
+    pub billing_city: Option<String>,
     #[serde(rename = "billing_country_code")]
-    pub billing_country_code: String,
+    pub billing_country_code: Option<String>,
     #[serde(rename = "billing_country_area")]
-    pub billing_country_area: String,
-    pub tokens: Vec<Token>,
-    pub subscriptions: Vec<Subscription>,
-    pub plan: Plan,
+    pub billing_country_area: Option<String>,
+    pub tokens: Option<Vec<Token>>,
+    pub subscriptions: Option<Vec<Subscription>>,
+    pub plan: Option<Plan>,
     #[serde(rename = "deployments_left")]
     pub deployments_left: Value,
     #[serde(rename = "suspension_hints")]
-    pub suspension_hints: SuspensionHints,
+    pub suspension_hints: Option<SuspensionHints>,
 }
 
 #[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize)]
@@ -72,9 +72,9 @@ pub struct Subscription {
     #[serde(rename = "user_id")]
     pub user_id: i64,
     #[serde(rename = "date_created")]
-    pub date_created: String,
+    pub date_created: Option<String>,
     #[serde(rename = "date_updated")]
-    pub date_updated: String,
+    pub date_updated: Option<String>,
 }
 
 #[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize)]
@@ -92,21 +92,21 @@ pub struct Plan {
     pub billing_email: String,
     #[serde(rename = "date_of_purchase")]
     pub date_of_purchase: String,
-    pub currency: String,
-    pub price: String,
-    pub period: String,
+    pub currency: Option<String>,
+    pub price: Option<String>,
+    pub period: Option<String>,
     #[serde(rename = "date_start")]
     pub date_start: String,
     pub active: bool,
     #[serde(rename = "billing_id")]
-    pub billing_id: String,
+    pub billing_id: Option<String>,
 }
 
 #[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize)]
 #[serde(rename_all = "camelCase")]
 pub struct SupportedStacks {
-    pub monthly: i64,
-    pub annually: i64,
+    pub monthly: Option<i64>,
+    pub annually: Option<i64>,
 }
 
 #[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize)]
@@ -132,8 +132,8 @@ impl TryInto<UserModel> for UserForm {
         // )?;
         Ok(UserModel {
             id: self.user.id,
-            first_name: self.user.first_name,
-            last_name: self.user.last_name,
+            first_name: self.user.first_name.unwrap_or("Noname".to_string()),
+            last_name: self.user.last_name.unwrap_or("Noname".to_string()),
             email: self.user.email,
             email_confirmed: self.user.email_confirmed,
         })
diff --git a/src/helpers/json.rs b/src/helpers/json.rs
index c0a6545e..ad183050 100644
--- a/src/helpers/json.rs
+++ b/src/helpers/json.rs
@@ -1,41 +1,161 @@
-// use std::collections::HashMap;
-// use actix_web::{HttpRequest, HttpResponse, Responder};
+use actix_web::{HttpRequest, HttpResponse, Responder};
 use serde_derive::Serialize;
+use crate::models;
 
-#[derive(Serialize)]
-pub(crate) struct JsonResponse {
+#[derive(Serialize, Default)]
+pub(crate) struct JsonResponse<T> {
     pub(crate) status: String,
     pub(crate) message: String,
     pub(crate) code: u32,
     pub(crate) id: Option<i32>,
+    pub(crate) item: Option<T>,
+    pub(crate) list: Option<Vec<T>>
 }
 
-
+//
 // #[derive(Serialize)]
-// pub(crate) struct JsonResponse {
+// pub(crate) struct JsonErrorResponse<T> {
 //     pub(crate) status: String,
 //     pub(crate) message: String,
 //     pub(crate) code: u32,
-//     pub(crate) custom_fields: HashMap<String, Option<String>>,
+//     pub(crate) id: Option<i32>,
+//     pub(crate) item: Option<T>,
+//     pub(crate) list: Option<Vec<T>>
 // }
+
+impl<T> JsonResponse<T> {
+    pub(crate) fn new(status: String,
+                      message: String,
+                      code: u32,
+                      id: Option<i32>,
+                      item:Option<T>,
+                      list: Option<Vec<T>>) -> Self {
+        tracing::debug!("Executed..");
+        JsonResponse {
+            status,
+            message,
+            code,
+            id,
+            item,
+            list,
+        }
+    }
+
+    pub(crate) fn ok(id: i32, message: &str) -> JsonResponse<T> {
+
+        let msg = if !message.trim().is_empty() {
+            message.to_string()
+        }
+        else{
+            String::from("Success")
+        };
+
+        JsonResponse {
+            status: "OK".to_string(),
+            message: msg,
+            code: 200,
+            id: Some(id),
+            item: None,
+            list: None
+        }
+    }
+
+    pub(crate) fn not_found() -> Self {
+        JsonResponse {
+            status: "Error".to_string(),
+            code: 404,
+            message: format!("Object not found"),
+            id: None,
+            item: None,
+            list: None
+        }
+    }
+
+    pub(crate) fn internal_error(message: &str) -> Self {
+
+        let msg = if !message.trim().is_empty() {
+            message.to_string()
+        }
+        else{
+            String::from("Internal error")
+        };
+        JsonResponse {
+            status: "Error".to_string(),
+            code: 500,
+            message: msg,
+            id: None,
+            item: None,
+            list: None
+        }
+    }
+
+    pub(crate) fn not_valid(message: &str) -> Self {
+
+        let msg = if !message.trim().is_empty() {
+            message.to_string()
+        }
+        else{
+            String::from("Validation error")
+        };
+        JsonResponse {
+            status: "Error".to_string(),
+            code: 400,
+            message: msg,
+            id: None,
+            item: None,
+            list: None
+        }
+    }
+}
+
+// // Implement the Responder trait for GlobalResponse
+// impl Responder for JsonResponse<T> where T: {
+//
+//     type Body = ();
 //
-// impl JsonResponse {
-//     pub(crate) fn new(status: String, message: String, code: u32) -> Self {
-//         let custom_fields = HashMap::new();
-//         JsonResponse {
+//     fn respond_to(self, _req: &HttpRequest) -> HttpResponse {
+//         HttpResponse::Ok().json(self)
+//     }
+// }
+//
+// impl<T> JsonErrorResponse<T> {
+//     pub(crate) fn new(status: String,
+//                       message: String,
+//                       code: u32,
+//                       id: Option<i32>,
+//                       item:Option<T>,
+//                       list: Option<Vec<T>>) -> Self {
+//         JsonErrorResponse {
 //             status,
 //             message,
 //             code,
-//             custom_fields
+//             id,
+//             item,
+//             list,
 //         }
 //     }
-// }
 //
-// // Implement the Responder trait for GlobalResponse
-// impl Responder for JsonResponse {
-//     type Body = ();
+//     pub(crate) fn default() -> Self {
+//         JsonErrorResponse {
+//             status: "Internal Error".to_string(),
+//             message: "Internal Error".to_string(),
+//             code: 500,
+//             id: None,
+//             item: None,
+//             list: None,
+//         }
+//     }
 //
-//     fn respond_to(self, _req: &HttpRequest) -> HttpResponse {
-//         HttpResponse::Ok().json(self)
+//     pub(crate) fn not_found() -> Self {
+//         JsonErrorResponse {
+//             status: "Error".to_string(),
+//             code: 404,
+//             message: format!("Object not found"),
+//             id: None,
+//             item: None,
+//             list: None
+//         }
 //     }
+//
+//
 // }
diff --git a/src/routes/client/add.rs b/src/routes/client/add.rs
index 0f5435c3..ae822f86 100644
--- a/src/routes/client/add.rs
+++ b/src/routes/client/add.rs
@@ -1,20 +1,12 @@
 use crate::configuration::Settings;
-use crate::helpers::client;
+use crate::helpers::{client, JsonResponse};
 use crate::models::user::User;
 use crate::models::Client;
 use actix_web::{post, web, Responder, Result};
-use serde::Serialize;
 use sqlx::PgPool;
 use std::sync::Arc;
 use tracing::Instrument;
 
-#[derive(Serialize)]
-struct ClientAddResponse {
-    status: String,
-    message: String,
-    code: u32,
-    client: Option<Client>,
-}
 
 #[tracing::instrument(name = "Add client.")]
 #[post("")]
@@ -24,6 +16,7 @@ pub async fn add_handler(
     pool: web::Data<PgPool>,
 ) -> Result<impl Responder> {
     let query_span = tracing::info_span!("Counting the user's clients");
+
     match sqlx::query!(
         r#"
         SELECT
@@ -46,23 +39,14 @@ pub async fn add_handler(
                     client_count
                 );
 
-                return Ok(web::Json(ClientAddResponse {
-                    status: "error".to_string(),
-                    code: 400,
-                    message: "Too many clients already created".to_string(),
-                    client: None,
-                }));
+                return Ok(web::Json(JsonResponse::not_valid(
+                    "Too many clients already created"))
+                );
             }
         }
         Err(e) => {
             tracing::error!("Failed to execute query: {:?}", e);
-
-            return Ok(web::Json(ClientAddResponse {
-                status: "error".to_string(),
-                code: 500,
-                message: "Failed to insert".to_string(),
-                client: None,
-            }));
+            return Ok(web::Json(JsonResponse::internal_error("Failed to insert")));
         }
     };
 
@@ -88,22 +72,18 @@ pub async fn add_handler(
         Ok(result) => {
             tracing::info!("New client {} have been saved to database", result.id);
             client.id = result.id;
-            Ok(web::Json(ClientAddResponse {
-                status: "success".to_string(),
-                message: "".to_string(),
-                code: 200,
-                client: Some(client),
-            }))
+            Ok(web::Json(JsonResponse::new(
+                "success".to_string(),
+                "".to_string(),
+                200,
+                Some(client.id),
+                Some(client),
+                None
+            )))
         }
         Err(e) => {
             tracing::error!("Failed to execute query: {:?}", e);
-
-            return Ok(web::Json(ClientAddResponse {
-                status: "error".to_string(),
-                code: 500,
-                message: "Failed to insert".to_string(),
-                client: None,
-            }));
+            Ok(web::Json(JsonResponse::internal_error("Failed to insert")))
         }
     }
 }
diff --git a/src/routes/rating/add.rs b/src/routes/rating/add.rs
index cd7fa2b8..382b5649 100644
--- a/src/routes/rating/add.rs
+++ b/src/routes/rating/add.rs
@@ -13,6 +13,7 @@ use tracing::Instrument;
 // ACL - access to func for a user
 // ACL - access to objects for a user
 
+
 #[tracing::instrument(name = "Add rating.")]
 #[post("")]
 pub async fn add_handler(
@@ -35,12 +36,7 @@ pub async fn add_handler(
         }
         Err(e) => {
             tracing::error!("Failed to fetch product: {:?}, error: {:?}", form.obj_id, e);
-            return Ok(web::Json(JsonResponse {
-                status: "Error".to_string(),
-                code: 404,
-                message: format!("Object not found {}", form.obj_id),
-                id: None,
-            }));
+            return Ok(web::Json(JsonResponse::<models::Rating>::not_found()));
         }
     };
 
@@ -64,22 +60,19 @@ pub async fn add_handler(
                 form.category
             );
 
-            return Ok(web::Json(JsonResponse {
-                status: "Error".to_string(),
-                code: 409,
-                message: format!("Already Rated"),
-                id: Some(record.id),
-            }));
+            return Ok(web::Json(JsonResponse::<models::Rating>::new(
+                "Error".to_string(),
+                "Already rated".to_string(),
+                409,
+                Some(record.id),
+                None,
+                None
+            )));
         }
         Err(sqlx::Error::RowNotFound) => {}
         Err(e) => {
             tracing::error!("Failed to fetch rating, error: {:?}", e);
-            return Ok(web::Json(JsonResponse {
-                status: "Error".to_string(),
-                code: 500,
-                message: format!("Internal Server Error"),
-                id: None,
-            }));
+            return Ok(web::Json(JsonResponse::<models::Rating>::internal_error("")));
         }
     }
 
@@ -106,22 +99,11 @@ pub async fn add_handler(
     {
         Ok(result) => {
             tracing::info!("New rating {} have been saved to database", result.id);
-
-            Ok(web::Json(JsonResponse {
-                status: "ok".to_string(),
-                code: 200,
-                message: "Saved".to_string(),
-                id: Some(result.id),
-            }))
+            return Ok(web::Json(JsonResponse::<models::Rating>::ok(result.id, "Saved")));
         }
         Err(e) => {
             tracing::error!("Failed to execute query: {:?}", e);
-            Ok(web::Json(JsonResponse {
-                status: "error".to_string(),
-                code: 500,
-                message: "Failed to insert".to_string(),
-                id: None,
-            }))
+            return Ok(web::Json(JsonResponse::internal_error("Failed to insert")));
         }
     }
 }
diff --git a/src/routes/rating/get.rs b/src/routes/rating/get.rs
index 2f63c766..19d93998 100644
--- a/src/routes/rating/get.rs
+++ b/src/routes/rating/get.rs
@@ -1,22 +1,15 @@
 use crate::models;
 use actix_web::{get, web, Responder, Result};
-use serde_derive::Serialize;
 use sqlx::PgPool;
 use tracing::Instrument;
+use crate::helpers::JsonResponse;
+use crate::models::user::User;
 
 // 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,
-    rating: Option<models::Rating>,
-    objects: Option<Vec<models::Rating>>,
-}
 
 #[tracing::instrument(name = "Get rating.")]
 #[get("/{id}")]
@@ -24,6 +17,7 @@ pub async fn get_handler(
     path: web::Path<(i32,)>,
     pool: web::Data<PgPool>,
 ) -> Result<impl Responder> {
+    /// Get rating of any user
     let rate_id = path.0;
     let query_span = tracing::info_span!("Search for rate id={}.", rate_id);
     match sqlx::query_as!(
@@ -37,32 +31,21 @@ pub async fn get_handler(
     {
         Ok(rating) => {
             tracing::info!("rating found: {:?}", rating.id,);
-            return Ok(web::Json(JsonResponse {
-                status: "Success".to_string(),
-                code: 200,
-                message: "".to_string(),
-                rating: Some(rating),
-                objects: None
-            }));
+            return Ok(web::Json(JsonResponse::new(
+                "Success".to_string(),
+                "Rating found".to_string(),
+                200,
+                Some(rating.id),
+                Some(rating),
+                None
+            )));
         }
         Err(sqlx::Error::RowNotFound) => {
-            return Ok(web::Json(JsonResponse {
-                status: "Error".to_string(),
-                code: 404,
-                message: format!("Not Found"),
-                rating: None,
-                objects: None
-            }));
+            return Ok(web::Json(JsonResponse::not_found()));
         }
         Err(e) => {
             tracing::error!("Failed to fetch rating, error: {:?}", e);
-            return Ok(web::Json(JsonResponse {
-                status: "Error".to_string(),
-                code: 500,
-                message: format!("Internal Server Error"),
-                rating: None,
-                objects: None
-            }));
+            return Ok(web::Json(JsonResponse::internal_error("")));
         }
     }
 }
@@ -73,6 +56,7 @@ pub async fn list_handler(
     path: web::Path<()>,
     pool: web::Data<PgPool>,
 ) -> Result<impl Responder> {
+    /// Get ratings of all users
 
     let query_span = tracing::info_span!("Get all rates.");
     // let category = path.0;
@@ -86,32 +70,21 @@ pub async fn list_handler(
     {
         Ok(rating) => {
             tracing::info!("Ratings found: {:?}", rating.len());
-            return Ok(web::Json(JsonResponse {
-                status: "Success".to_string(),
-                code: 200,
-                message: "".to_string(),
-                rating: None,
-                objects: Some(rating),
-            }));
+            return Ok(web::Json(JsonResponse::new(
+                "Success".to_string(),
+                "".to_string(),
+                200,
+                None,
+                None,
+                Some(rating),
+            )));
         }
         Err(sqlx::Error::RowNotFound) => {
-            return Ok(web::Json(JsonResponse {
-                status: "Error".to_string(),
-                code: 404,
-                message: format!("Not Found"),
-                rating: None,
-                objects: None
-            }));
+            return Ok(web::Json(JsonResponse::not_found()));
         }
         Err(e) => {
             tracing::error!("Failed to fetch rating, error: {:?}", e);
-            return Ok(web::Json(JsonResponse {
-                status: "Error".to_string(),
-                code: 500,
-                message: format!("Internal Server Error"),
-                rating: None,
-                objects: None
-            }));
+            return Ok(web::Json(JsonResponse::internal_error("")));
         }
     }
 }
diff --git a/src/routes/stack/add.rs b/src/routes/stack/add.rs
index 53eb3891..d1d8b87c 100644
--- a/src/routes/stack/add.rs
+++ b/src/routes/stack/add.rs
@@ -1,4 +1,3 @@
-use std::collections::HashMap;
 use actix_web::{
     web,
     web::{Bytes, Data, Json},
@@ -17,6 +16,8 @@ use std::str;
 use tracing::Instrument;
 use uuid::Uuid;
 
+
+
 #[tracing::instrument(name = "Add stack.")]
 #[post("")]
 pub async fn add(
@@ -24,10 +25,6 @@ pub async fn add(
     user: web::ReqData<User>,
     pool: Data<PgPool>,
 ) -> Result<impl Responder> {
-    // None::<i32>.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();
@@ -35,19 +32,12 @@ pub async fn add(
     // method 2 let app_state = serde_json::from_str::<AppState>(body_str).unwrap();
     let form = match serde_json::from_str::<StackForm>(body_str) {
         Ok(f) => {
-            println!("fine");
             f
         }
         Err(err) => {
-            return Ok(Json(JsonResponse {
-                status: "Error".to_string(),
-                code: 400,
-                message: err.to_string(),
-                id: None,
-            }));
+            return Ok(Json(JsonResponse::<Stack>::not_valid("")));
         }
     };
-    // println!("app: {:?}", form);
 
     let user_id = user.id.clone();
     let request_id = Uuid::new_v4();
@@ -81,16 +71,14 @@ pub async fn add(
     let stack = Stack {
         id: 0_i32,                // internal stack id
         stack_id: Uuid::new_v4(), // public uuid of the stack
-        // user_id: Uuid::from_u128(user_id as u128),
-        user_id: user_id, //
+        user_id: user_id,         //
         name: stack_name,
         body: body,
-        // body: body_str.to_string(),
         created_at: Utc::now(),
         updated_at: Utc::now(),
     };
 
-    println!("stack object {:?}", stack);
+    tracing::debug!("stack object {:?}", stack);
     return match sqlx::query!(
         r#"
         INSERT INTO user_stack (id, stack_id, user_id, name, body, created_at, updated_at)
@@ -101,7 +89,6 @@ pub async fn add(
         stack.stack_id,
         stack.user_id,
         stack.name,
-        // sqlx::types::Json(stack.body),
         stack.body,
         stack.created_at,
         stack.updated_at
@@ -115,21 +102,11 @@ pub async fn add(
                 "req_id: {} New stack details have been saved to database",
                 request_id
             );
-            Ok(Json(JsonResponse {
-                status: "OK".to_string(),
-                code: 200,
-                message: format!("Object saved"),
-                id: Some(record.id),
-            }))
+            Ok(Json(JsonResponse::<Stack>::ok(record.id, "Object saved")))
         }
-        Err(e) => {
-            tracing::error!("req_id: {} Failed to execute query: {:?}", request_id, e);
-            Ok(Json(JsonResponse {
-                status: "Error".to_string(),
-                code: 400,
-                message: e.to_string(),
-                id: None,
-            }))
+        Err(err) => {
+            tracing::error!("req_id: {} Failed to execute query: {:?}", request_id, err);
+            Ok(Json(JsonResponse::<Stack>::not_valid("Failed to insert")))
         }
     };
 }
diff --git a/src/routes/stack/get.rs b/src/routes/stack/get.rs
index 1497f9e1..7b5fa7f5 100644
--- a/src/routes/stack/get.rs
+++ b/src/routes/stack/get.rs
@@ -1,26 +1,18 @@
 use actix_web::{web, get, Responder, Result};
-use serde_derive::Serialize;
 use sqlx::PgPool;
+use crate::helpers::JsonResponse;
 use crate::models;
 use crate::models::user::User;
 
-#[derive(Serialize)]
-struct JsonResponse {
-    status: String,
-    message: String,
-    code: u32,
-    id: Option<i32>,
-    object: Option<models::Stack>,
-    objects: Option<Vec<models::Stack>>,
-}
 
-#[tracing::instrument(name = "Get stack.")]
+#[tracing::instrument(name = "Get logged user stack.")]
 #[get("/{id}")]
 pub async fn get(
     user: web::ReqData<User>,
     path: web::Path<(i32,)>,
     pool: web::Data<PgPool>,
 ) -> Result<impl Responder> {
+    /// Get stack apps of logged user only
 
     let (id,) = path.into_inner();
 
@@ -37,36 +29,20 @@ pub async fn get(
     {
         Ok(stack) => {
             tracing::info!("stack found: {:?}", stack.id,);
-            let response = JsonResponse {
-                status: "Success".to_string(),
-                code: 200,
-                message: "".to_string(),
-                id: Some(stack.id),
-                object: Some(stack),
-                objects: None
-            };
-            return Ok(web::Json(response));
+            return Ok(web::Json(JsonResponse::<models::Stack>::new(
+                "Success".to_string(),
+                "".to_string(),
+                200,
+                Some(stack.id),
+                Some(stack),
+                None)));
         }
         Err(sqlx::Error::RowNotFound) => {
-            return Ok(web::Json(JsonResponse {
-                status: "Error".to_string(),
-                code: 404,
-                message: format!("Not Found"),
-                id: None,
-                object: None,
-                objects: None
-            }));
+            return Ok(web::Json(JsonResponse::<models::Stack>::not_found()));
         }
         Err(e) => {
             tracing::error!("Failed to fetch stack, error: {:?}", e);
-            return Ok(web::Json(JsonResponse {
-                status: "Error".to_string(),
-                code: 500,
-                message: format!("Internal Server Error"),
-                id: None,
-                object: None,
-                objects: None
-            }));
+            return Ok(web::Json(JsonResponse::<models::Stack>::internal_error("")));
         }
     }
 }

From 1dd107eddac19c8955b47d749b01edf01919b72f Mon Sep 17 00:00:00 2001
From: vsilent <jabberroid@gmail.com>
Date: Tue, 7 Nov 2023 14:34:42 +0200
Subject: [PATCH 03/18] dctypes lib added, ports converted

---
 Cargo.lock                   | 124 +++++-
 Cargo.toml                   |  12 +
 custom-stack-payload-4.json  |   2 +-
 files/docker-compose.yml     |  20 +
 src/helpers/mod.rs           |   1 +
 src/helpers/stack/builder.rs | 207 +++++++++
 src/helpers/stack/dctypes.rs | 842 +++++++++++++++++++++++++++++++++++
 src/helpers/stack/mod.rs     |   2 +
 src/models/stack.rs          |   2 +-
 src/routes/stack/add.rs      |  52 ++-
 src/routes/stack/get.rs      |   1 +
 src/startup.rs               |   1 +
 12 files changed, 1259 insertions(+), 7 deletions(-)
 create mode 100644 files/docker-compose.yml
 create mode 100644 src/helpers/stack/builder.rs
 create mode 100644 src/helpers/stack/dctypes.rs
 create mode 100644 src/helpers/stack/mod.rs

diff --git a/Cargo.lock b/Cargo.lock
index 21202d53..68ef0eb4 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -562,6 +562,41 @@ dependencies = [
  "typenum",
 ]
 
+[[package]]
+name = "darling"
+version = "0.14.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7b750cb3417fd1b327431a470f388520309479ab0bf5e323505daf0290cd3850"
+dependencies = [
+ "darling_core",
+ "darling_macro",
+]
+
+[[package]]
+name = "darling_core"
+version = "0.14.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "109c1ca6e6b7f82cc233a97004ea8ed7ca123a9af07a8230878fcfda9b158bf0"
+dependencies = [
+ "fnv",
+ "ident_case",
+ "proc-macro2",
+ "quote",
+ "strsim",
+ "syn 1.0.109",
+]
+
+[[package]]
+name = "darling_macro"
+version = "0.14.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a4aab4dbc9f7611d8b55048a3a16d2d010c2c8334e46304b40ac1cc14bf3b48e"
+dependencies = [
+ "darling_core",
+ "quote",
+ "syn 1.0.109",
+]
+
 [[package]]
 name = "deranged"
 version = "0.3.9"
@@ -571,6 +606,37 @@ dependencies = [
  "powerfmt",
 ]
 
+[[package]]
+name = "derive_builder"
+version = "0.12.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8d67778784b508018359cbc8696edb3db78160bab2c2a28ba7f56ef6932997f8"
+dependencies = [
+ "derive_builder_macro",
+]
+
+[[package]]
+name = "derive_builder_core"
+version = "0.12.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c11bdc11a0c47bc7d37d582b5285da6849c96681023680b906673c5707af7b0f"
+dependencies = [
+ "darling",
+ "proc-macro2",
+ "quote",
+ "syn 1.0.109",
+]
+
+[[package]]
+name = "derive_builder_macro"
+version = "0.12.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ebcda35c7a396850a55ffeac740804b40ffec779b98fffbb1738f4033f0ee79e"
+dependencies = [
+ "derive_builder_core",
+ "syn 1.0.109",
+]
+
 [[package]]
 name = "derive_more"
 version = "0.99.17"
@@ -645,6 +711,12 @@ dependencies = [
  "cfg-if",
 ]
 
+[[package]]
+name = "equivalent"
+version = "1.0.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5443807d6dff69373d433ab9ef5378ad8df50ca6298caf15de6e52e24aaf54d5"
+
 [[package]]
 name = "errno"
 version = "0.3.5"
@@ -803,6 +875,12 @@ version = "0.28.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "6fb8d784f27acf97159b40fc4db5ecd8aa23b9ad5ef69cdd136d3bc80665f0c0"
 
+[[package]]
+name = "glob"
+version = "0.3.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d2fabcfbdc87f4758337ca535fb41a6d701b65693ce38287d856d1674551ec9b"
+
 [[package]]
 name = "h2"
 version = "0.3.21"
@@ -815,7 +893,7 @@ dependencies = [
  "futures-sink",
  "futures-util",
  "http",
- "indexmap",
+ "indexmap 1.9.3",
  "slab",
  "tokio",
  "tokio-util",
@@ -983,6 +1061,12 @@ dependencies = [
  "cc",
 ]
 
+[[package]]
+name = "ident_case"
+version = "1.0.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b9e0384b61958566e926dc50660321d12159025e767c18e043daf26b70104c39"
+
 [[package]]
 name = "idna"
 version = "0.4.0"
@@ -1004,6 +1088,17 @@ dependencies = [
  "serde",
 ]
 
+[[package]]
+name = "indexmap"
+version = "2.1.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d530e1a18b1cb4c484e6e34556a0d948706958449fca0cab753d649f2bce3d1f"
+dependencies = [
+ "equivalent",
+ "hashbrown 0.14.1",
+ "serde",
+]
+
 [[package]]
 name = "instant"
 version = "0.1.12"
@@ -1886,7 +1981,7 @@ version = "0.16.3"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "0adc7a19d45e581abc6d169c865a0b14b84bb43a9e966d1cca4d733e70f7f35a"
 dependencies = [
- "indexmap",
+ "indexmap 1.9.3",
  "itertools 0.10.5",
  "num-traits",
  "once_cell",
@@ -1924,6 +2019,19 @@ dependencies = [
  "regex",
 ]
 
+[[package]]
+name = "serde_yaml"
+version = "0.9.25"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1a49e178e4452f45cb61d0cd8cebc1b0fafd3e41929e996cef79aa3aca91f574"
+dependencies = [
+ "indexmap 2.1.0",
+ "itoa",
+ "ryu",
+ "serde",
+ "unsafe-libyaml",
+]
+
 [[package]]
 name = "sha1"
 version = "0.10.6"
@@ -2059,7 +2167,7 @@ dependencies = [
  "hex",
  "hkdf",
  "hmac",
- "indexmap",
+ "indexmap 1.9.3",
  "itoa",
  "libc",
  "log",
@@ -2129,6 +2237,9 @@ dependencies = [
  "actix-web-httpauth",
  "chrono",
  "config",
+ "derive_builder",
+ "glob",
+ "indexmap 2.1.0",
  "rand",
  "regex",
  "reqwest",
@@ -2136,6 +2247,7 @@ dependencies = [
  "serde_derive",
  "serde_json",
  "serde_valid",
+ "serde_yaml",
  "sqlx",
  "thiserror",
  "tokio",
@@ -2545,6 +2657,12 @@ version = "0.1.1"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "39ec24b3121d976906ece63c9daad25b85969647682eee313cb5779fdd69e14e"
 
+[[package]]
+name = "unsafe-libyaml"
+version = "0.2.9"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f28467d3e1d3c6586d8f25fa243f544f5800fec42d97032474e17222c2b75cfa"
+
 [[package]]
 name = "untrusted"
 version = "0.7.1"
diff --git a/Cargo.toml b/Cargo.toml
index 47cbb107..581f4d5a 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -34,6 +34,11 @@ tracing-actix-web = "0.7.7"
 regex = "1.10.2"
 rand = "0.8.5"
 
+# dctypes
+derive_builder = "0.12.0"
+indexmap = { version = "2.0.0", features = ["serde"], optional = true }
+serde_yaml = "0.9"
+
 [dependencies.sqlx]
 version = "0.6.3"
 features = [
@@ -45,3 +50,10 @@ features = [
     "json",
     "offline"
 ]
+
+[features]
+default = ["indexmap"]
+indexmap = ["dep:indexmap"]
+
+[dev-dependencies]
+glob = "0.3"
\ No newline at end of file
diff --git a/custom-stack-payload-4.json b/custom-stack-payload-4.json
index 4f0ce614..07ee1569 100644
--- a/custom-stack-payload-4.json
+++ b/custom-stack-payload-4.json
@@ -1 +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":"<p class=\"content-editor__unstyled\"><strong style=\"font-weight:bold\">Portainer is a lightweight management UI which allows you to easily manage your different Docker environments (Docker hosts or Swarm clusters)</strong></p>","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":"<p class=\"content-editor__unstyled\">PostgREST description</p>","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":"mysampleproject2","custom_stack_code":"another-bot2","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"}}
+{"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":"<p class=\"content-editor__unstyled\"><strong style=\"font-weight:bold\">Portainer is a lightweight management UI which allows you to easily manage your different Docker environments (Docker hosts or Swarm clusters)</strong></p>","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":"<p class=\"content-editor__unstyled\">PostgREST description</p>","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":"mysampleproject2","custom_stack_code":"another-bot2","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/files/docker-compose.yml b/files/docker-compose.yml
new file mode 100644
index 00000000..c6e60d64
--- /dev/null
+++ b/files/docker-compose.yml
@@ -0,0 +1,20 @@
+version: '3.8'
+services:
+  smarty-bot:
+    image: trydirect/smarty-bot:latest
+    ports:
+    - target: 8000
+      published: 8000
+    restart: always
+  pgrst:
+    image: pgrst
+    ports:
+    - target: 9999
+      published: 9999
+    restart: always
+  portainer_ce_feature:
+    image: portainer-ce-feature
+    ports:
+    - target: 9000
+      published: 9000
+    restart: always
diff --git a/src/helpers/mod.rs b/src/helpers/mod.rs
index 3500bbd3..823b2b1c 100644
--- a/src/helpers/mod.rs
+++ b/src/helpers/mod.rs
@@ -1,5 +1,6 @@
 pub mod client;
 pub(crate) mod json;
 pub mod serialize_datetime;
+pub(crate) mod stack;
 
 pub use json::*;
diff --git a/src/helpers/stack/builder.rs b/src/helpers/stack/builder.rs
new file mode 100644
index 00000000..023ee964
--- /dev/null
+++ b/src/helpers/stack/builder.rs
@@ -0,0 +1,207 @@
+use crate::helpers::stack::dctypes::{Compose, Port, Ports, PublishedPort, Service, Services, SingleService};
+use serde_yaml;
+use crate::forms;
+use crate::forms::{StackForm, Web, Feature};
+use crate::models::stack::Stack;
+
+#[derive(Clone, Debug)]
+struct Config {
+
+}
+
+impl Default for Config {
+    fn default() -> Self {
+        Config {}
+    }
+}
+
+/// A builder for constructing docker compose.
+#[derive(Clone, Debug)]
+pub struct DcBuilder {
+    config: Config,
+    stack: Stack
+}
+
+impl TryInto<Vec<Port>> for Web {
+    type Error = String;
+    fn try_into(self) -> Result<Vec<Port>, Self::Error> {
+        convert_shared_ports(self.shared_ports.clone().unwrap())
+    }
+}
+
+impl TryInto<Vec<Port>> for &Feature {
+    type Error = String;
+    fn try_into(self) -> Result<Vec<Port>, Self::Error> {
+        convert_shared_ports(self.shared_ports.clone().unwrap())
+    }
+}
+
+impl TryInto<Vec<Port>> for &forms::stack::Service {
+    type Error = String;
+    fn try_into(self) -> Result<Vec<Port>, Self::Error> {
+        convert_shared_ports(self.shared_ports.clone().unwrap())
+    }
+}
+
+fn convert_shared_ports(ports: Vec<String>) -> Result<Vec<Port>, String> {
+    let mut _ports: Vec<Port> = vec![];
+    for p in ports {
+        let port = p.parse::<u16>().map_err(|e| e.to_string())?;
+        _ports.push(Port {
+            target: port,
+            host_ip: None,
+            published: Some(PublishedPort::Single(port)),
+            protocol: None,
+            mode: None,
+        });
+    }
+    Ok(_ports)
+}
+
+impl DcBuilder {
+
+    pub fn new() -> Self {
+        DcBuilder {
+            config: Config::default(),
+            stack: Stack {
+                id: 0,
+                stack_id: Default::default(),
+                user_id: "".to_string(),
+                name: "".to_string(),
+                body: Default::default(),
+                created_at: Default::default(),
+                updated_at: Default::default(),
+            },
+        }
+    }
+
+    // pub fn add_ports(&self, ports: &Vec<String>) -> Vec<Port> {
+    //     // @todo re-factor using TryInto or TryFrom
+    //
+    //     let mut _ports:Vec<Port> = vec![];
+    //     for p in ports {
+    //         let port = p.parse::<u16>().unwrap();
+    //         _ports.push(
+    //             Port {
+    //                 target: port,
+    //                 host_ip: None,
+    //                 published: Some(PublishedPort::Single(port)),
+    //                 protocol: None,
+    //                 mode: None,
+    //             }
+    //         );
+    //     }
+    //     _ports
+    // }
+
+    pub fn build(&self, stack:Stack) -> Option<Compose> {
+
+        tracing::debug!("Start build docker compose from {:?}", stack.body);
+        let _stack = serde_json::from_value::<StackForm>(stack.body);
+        let mut services = indexmap::IndexMap::new();
+        match _stack  {
+            Ok(apps) => {
+                println!("stack item {:?}", apps.custom.web);
+
+                for app in apps.custom.web {
+                    // println!("app name {:?}", app.name);
+                    let tag = "latest";
+                    let img= format!("{}/{}:{}",app.dockerhub_user, app.dockerhub_name, tag);
+                    let code = app.code.clone().to_owned();
+                    let mut service = Service {
+                        image: Some(img.to_string()),
+                        ..Default::default()
+                    };
+
+                    if let Some(ports) = &app.shared_ports {
+                        if !ports.is_empty() {
+                            // service.ports = Ports::Long(self.add_ports(ports));
+                            service.ports = Ports::Long(app.try_into().unwrap())
+                        }
+                    }
+
+                    service.restart = Some("always".to_owned());
+                    services.insert(
+                        code,
+                        Some(service),
+                    );
+                }
+
+                if let Some(srvs) = &apps.custom.service {
+
+                    if !srvs.is_empty() {
+
+                        for app in srvs {
+                            let code = app.code.to_owned();
+                            let tag = "latest";
+
+                            let mut service = Service {
+                                image: Some(app.dockerhub_image.as_ref().unwrap().to_owned()),
+                                ..Default::default()
+                            };
+
+                            if let Some(ports) = &app.shared_ports {
+                                if !ports.is_empty() {
+                                    // service.ports = Ports::Long(self.add_ports(ports));
+                                    service.ports = Ports::Long(app.try_into().unwrap())
+                                }
+                            }
+                            service.restart = Some("always".to_owned());
+                            services.insert(
+                                code,
+                                Some(service),
+                            );
+                        }
+                    }
+                }
+                if let Some(features) = &apps.custom.feature {
+
+                    if !features.is_empty() {
+
+                        for app in features {
+                            let code = app.code.to_owned();
+                            let mut service = Service {
+                                image: Some(app.dockerhub_image.as_ref().unwrap().to_owned()),
+                                ..Default::default()
+                            };
+
+                            if let Some(ports) = &app.shared_ports {
+                                if !ports.is_empty() {
+                                    // service.ports = Ports::Long(self.add_ports(ports));
+                                    service.ports = Ports::Long(app.try_into().unwrap())
+                                }
+                            }
+                            service.restart = Some("always".to_owned());
+                            services.insert(
+                                code,
+                                Some(service),
+                            );
+                        }
+                    }
+                }
+            }
+            Err(e) => {
+                ()
+            }
+        }
+
+        let compose_content = Compose {
+            version: Some("3.8".to_string()),
+            services: {
+                Services(services)
+            },
+            ..Default::default()
+        };
+
+        let target_file = std::path::Path::new("./files/docker-compose.yml");
+        // serialize to string
+        let serialized = match serde_yaml::to_string(&compose_content) {
+            Ok(s) => s,
+            Err(e) => panic!("Failed to serialize docker-compose file: {}", e),
+        };
+        // serialize to file
+        std::fs::write(target_file, serialized).unwrap();
+
+        Some(compose_content)
+    }
+}
diff --git a/src/helpers/stack/dctypes.rs b/src/helpers/stack/dctypes.rs
new file mode 100644
index 00000000..931deab1
--- /dev/null
+++ b/src/helpers/stack/dctypes.rs
@@ -0,0 +1,842 @@
+use derive_builder::*;
+#[cfg(feature = "indexmap")]
+use indexmap::IndexMap;
+use serde::{Deserialize, Serialize};
+use serde_yaml::Value;
+#[cfg(not(feature = "indexmap"))]
+use std::collections::HashMap;
+use std::convert::TryFrom;
+use std::fmt;
+use std::str::FromStr;
+
+#[allow(clippy::large_enum_variant)]
+#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
+#[serde(untagged)]
+pub enum ComposeFile {
+    V2Plus(Compose),
+    #[cfg(feature = "indexmap")]
+    V1(IndexMap<String, Service>),
+    #[cfg(not(feature = "indexmap"))]
+    V1(HashMap<String, Service>),
+    Single(SingleService),
+}
+
+#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Default)]
+pub struct SingleService {
+    pub service: Service,
+}
+
+#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Default)]
+pub struct Compose {
+    #[serde(skip_serializing_if = "Option::is_none")]
+    pub version: Option<String>,
+    #[serde(default, skip_serializing_if = "Services::is_empty")]
+    pub services: Services,
+    #[serde(default, skip_serializing_if = "TopLevelVolumes::is_empty")]
+    pub volumes: TopLevelVolumes,
+    #[serde(default, skip_serializing_if = "ComposeNetworks::is_empty")]
+    pub networks: ComposeNetworks,
+    #[serde(skip_serializing_if = "Option::is_none")]
+    pub service: Option<Service>,
+    #[cfg(feature = "indexmap")]
+    #[serde(flatten, skip_serializing_if = "IndexMap::is_empty")]
+    pub extensions: IndexMap<Extension, Value>,
+    #[cfg(not(feature = "indexmap"))]
+    #[serde(flatten, skip_serializing_if = "HashMap::is_empty")]
+    pub extensions: HashMap<Extension, Value>,
+}
+
+impl Compose {
+    pub fn new() -> Self {
+        Default::default()
+    }
+}
+
+#[derive(Builder, Clone, Debug, Deserialize, Serialize, PartialEq, Default)]
+#[builder(setter(into), default)]
+pub struct Service {
+    #[serde(skip_serializing_if = "Option::is_none")]
+    pub hostname: Option<String>,
+    #[serde(default, skip_serializing_if = "std::ops::Not::not")]
+    pub privileged: bool,
+    #[serde(skip_serializing_if = "Option::is_none")]
+    pub healthcheck: Option<Healthcheck>,
+    #[serde(skip_serializing_if = "Option::is_none")]
+    pub deploy: Option<Deploy>,
+    #[serde(skip_serializing_if = "Option::is_none")]
+    pub image: Option<String>,
+    #[serde(skip_serializing_if = "Option::is_none")]
+    pub container_name: Option<String>,
+    #[serde(skip_serializing_if = "Option::is_none", rename = "build")]
+    pub build_: Option<BuildStep>,
+    #[serde(skip_serializing_if = "Option::is_none")]
+    pub pid: Option<String>,
+    #[serde(default, skip_serializing_if = "Ports::is_empty")]
+    pub ports: Ports,
+    #[serde(default, skip_serializing_if = "Environment::is_empty")]
+    pub environment: Environment,
+    #[serde(skip_serializing_if = "Option::is_none")]
+    pub network_mode: Option<String>,
+    #[serde(default, skip_serializing_if = "Vec::is_empty")]
+    pub devices: Vec<String>,
+    #[serde(skip_serializing_if = "Option::is_none")]
+    pub restart: Option<String>,
+    #[serde(default, skip_serializing_if = "Labels::is_empty")]
+    pub labels: Labels,
+    #[serde(skip_serializing_if = "Option::is_none")]
+    pub tmpfs: Option<Tmpfs>,
+    #[serde(default, skip_serializing_if = "Ulimits::is_empty")]
+    pub ulimits: Ulimits,
+    #[serde(default, skip_serializing_if = "Volumes::is_empty")]
+    pub volumes: Volumes,
+    #[serde(default, skip_serializing_if = "Networks::is_empty")]
+    pub networks: Networks,
+    #[serde(default, skip_serializing_if = "Vec::is_empty")]
+    pub cap_add: Vec<String>,
+    #[serde(default, skip_serializing_if = "DependsOnOptions::is_empty")]
+    pub depends_on: DependsOnOptions,
+    #[serde(skip_serializing_if = "Option::is_none")]
+    pub command: Option<Command>,
+    #[serde(skip_serializing_if = "Option::is_none")]
+    pub entrypoint: Option<Entrypoint>,
+    #[serde(skip_serializing_if = "Option::is_none")]
+    pub env_file: Option<EnvFile>,
+    #[serde(skip_serializing_if = "Option::is_none")]
+    pub stop_grace_period: Option<String>,
+    #[serde(default, skip_serializing_if = "Vec::is_empty")]
+    pub profiles: Vec<String>,
+    #[serde(default, skip_serializing_if = "Vec::is_empty")]
+    pub links: Vec<String>,
+    #[serde(default, skip_serializing_if = "Vec::is_empty")]
+    pub dns: Vec<String>,
+    #[serde(skip_serializing_if = "Option::is_none")]
+    pub ipc: Option<String>,
+    #[serde(skip_serializing_if = "Option::is_none")]
+    pub net: Option<String>,
+    #[serde(skip_serializing_if = "Option::is_none")]
+    pub stop_signal: Option<String>,
+    #[serde(skip_serializing_if = "Option::is_none")]
+    pub user: Option<String>,
+    #[serde(skip_serializing_if = "Option::is_none")]
+    pub working_dir: Option<String>,
+    #[serde(default, skip_serializing_if = "Vec::is_empty")]
+    pub expose: Vec<String>,
+    #[serde(default, skip_serializing_if = "Vec::is_empty")]
+    pub volumes_from: Vec<String>,
+    #[cfg(feature = "indexmap")]
+    #[serde(default, skip_serializing_if = "IndexMap::is_empty")]
+    pub extends: IndexMap<String, String>,
+    #[cfg(not(feature = "indexmap"))]
+    #[serde(default, skip_serializing_if = "HashMap::is_empty")]
+    pub extends: HashMap<String, String>,
+    #[serde(skip_serializing_if = "Option::is_none")]
+    pub logging: Option<LoggingParameters>,
+    #[serde(default, skip_serializing_if = "is_zero")]
+    pub scale: i64,
+    #[serde(default, skip_serializing_if = "std::ops::Not::not")]
+    pub init: bool,
+    #[serde(default, skip_serializing_if = "std::ops::Not::not")]
+    pub stdin_open: bool,
+    #[serde(skip_serializing_if = "Option::is_none")]
+    pub shm_size: Option<String>,
+    #[cfg(feature = "indexmap")]
+    #[serde(flatten, skip_serializing_if = "IndexMap::is_empty")]
+    pub extensions: IndexMap<Extension, Value>,
+    #[cfg(not(feature = "indexmap"))]
+    #[serde(flatten, skip_serializing_if = "HashMap::is_empty")]
+    pub extensions: HashMap<Extension, Value>,
+    #[serde(default, skip_serializing_if = "Vec::is_empty")]
+    pub extra_hosts: Vec<String>,
+    #[serde(default, skip_serializing_if = "std::ops::Not::not")]
+    pub tty: bool,
+    #[serde(default, skip_serializing_if = "SysCtls::is_empty")]
+    pub sysctls: SysCtls,
+    #[serde(default, skip_serializing_if = "Vec::is_empty")]
+    pub security_opt: Vec<String>,
+}
+
+impl Service {
+    pub fn image(&self) -> &str {
+        self.image.as_deref().unwrap_or_default()
+    }
+
+    pub fn network_mode(&self) -> &str {
+        self.network_mode.as_deref().unwrap_or_default()
+    }
+}
+
+#[derive(Clone, Debug, Serialize, Deserialize, Eq, PartialEq, Hash)]
+#[serde(untagged)]
+pub enum EnvFile {
+    Simple(String),
+    List(Vec<String>),
+}
+
+#[derive(Clone, Debug, Serialize, Deserialize, Eq, PartialEq)]
+#[serde(untagged)]
+pub enum DependsOnOptions {
+    Simple(Vec<String>),
+    #[cfg(feature = "indexmap")]
+    Conditional(IndexMap<String, DependsCondition>),
+    #[cfg(not(feature = "indexmap"))]
+    Conditional(HashMap<String, DependsCondition>),
+}
+
+impl Default for DependsOnOptions {
+    fn default() -> Self {
+        Self::Simple(Vec::new())
+    }
+}
+
+impl DependsOnOptions {
+    pub fn is_empty(&self) -> bool {
+        match self {
+            Self::Simple(v) => v.is_empty(),
+            Self::Conditional(m) => m.is_empty(),
+        }
+    }
+}
+
+#[derive(Clone, Debug, Serialize, Deserialize, Eq, PartialEq, Hash)]
+pub struct DependsCondition {
+    pub condition: String,
+}
+
+#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
+pub struct LoggingParameters {
+    pub driver: String,
+    #[cfg(feature = "indexmap")]
+    #[serde(skip_serializing_if = "Option::is_none")]
+    pub options: Option<IndexMap<String, SingleValue>>,
+    #[cfg(not(feature = "indexmap"))]
+    #[serde(skip_serializing_if = "Option::is_none")]
+    pub options: Option<HashMap<String, SingleValue>>,
+}
+
+#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
+#[serde(untagged)]
+pub enum Ports {
+    Short(Vec<String>),
+    Long(Vec<Port>),
+}
+
+impl Default for Ports {
+    fn default() -> Self {
+        Self::Short(Vec::default())
+    }
+}
+
+impl Ports {
+    pub fn is_empty(&self) -> bool {
+        match self {
+            Self::Short(v) => v.is_empty(),
+            Self::Long(v) => v.is_empty(),
+        }
+    }
+}
+
+#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
+pub struct Port {
+    pub target: u16,
+    #[serde(skip_serializing_if = "Option::is_none")]
+    pub host_ip: Option<String>,
+    #[serde(skip_serializing_if = "Option::is_none")]
+    pub published: Option<PublishedPort>,
+    #[serde(skip_serializing_if = "Option::is_none")]
+    pub protocol: Option<String>,
+    #[serde(skip_serializing_if = "Option::is_none")]
+    pub mode: Option<String>,
+}
+
+#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
+#[serde(untagged)]
+pub enum PublishedPort {
+    Single(u16),
+    Range(String),
+}
+
+#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
+#[serde(untagged)]
+pub enum Environment {
+    List(Vec<String>),
+    #[cfg(feature = "indexmap")]
+    KvPair(IndexMap<String, Option<SingleValue>>),
+    #[cfg(not(feature = "indexmap"))]
+    KvPair(HashMap<String, Option<SingleValue>>),
+}
+
+impl Default for Environment {
+    fn default() -> Self {
+        Self::List(Vec::new())
+    }
+}
+
+impl Environment {
+    pub fn is_empty(&self) -> bool {
+        match self {
+            Self::List(v) => v.is_empty(),
+            Self::KvPair(m) => m.is_empty(),
+        }
+    }
+}
+
+#[derive(Clone, Debug, Serialize, Deserialize, Eq, PartialEq, Hash, Default, Ord, PartialOrd)]
+#[serde(try_from = "String")]
+pub struct Extension(String);
+
+impl FromStr for Extension {
+    type Err = ExtensionParseError;
+
+    fn from_str(s: &str) -> Result<Self, Self::Err> {
+        let owned = s.to_owned();
+        Extension::try_from(owned)
+    }
+}
+
+impl TryFrom<String> for Extension {
+    type Error = ExtensionParseError;
+
+    fn try_from(s: String) -> Result<Self, Self::Error> {
+        if s.starts_with("x-") {
+            Ok(Self(s))
+        } else {
+            Err(ExtensionParseError(s))
+        }
+    }
+}
+
+/// The result of a failed TryFrom<String> conversion for [`Extension`]
+///
+/// Contains the string that was being converted
+#[derive(Clone, Debug, Eq, PartialEq, Hash)]
+pub struct ExtensionParseError(pub String);
+
+impl fmt::Display for ExtensionParseError {
+    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
+        write!(f, "unknown attribute {:?}, extensions must start with 'x-' (see https://docs.docker.com/compose/compose-file/#extension)", self.0)
+    }
+}
+
+impl std::error::Error for ExtensionParseError {}
+
+#[cfg(feature = "indexmap")]
+#[derive(Clone, Default, Debug, Serialize, Deserialize, PartialEq)]
+pub struct Services(pub IndexMap<String, Option<Service>>);
+#[cfg(not(feature = "indexmap"))]
+#[derive(Clone, Default, Debug, Serialize, Deserialize, PartialEq)]
+pub struct Services(pub HashMap<String, Option<Service>>);
+
+impl Services {
+    pub fn is_empty(&self) -> bool {
+        self.0.is_empty()
+    }
+}
+
+#[derive(Clone, Debug, Serialize, Deserialize, Eq, PartialEq)]
+#[serde(untagged)]
+pub enum Labels {
+    List(Vec<String>),
+    #[cfg(feature = "indexmap")]
+    Map(IndexMap<String, String>),
+    #[cfg(not(feature = "indexmap"))]
+    Map(HashMap<String, String>),
+}
+
+impl Default for Labels {
+    fn default() -> Self {
+        Self::List(Vec::new())
+    }
+}
+
+impl Labels {
+    pub fn is_empty(&self) -> bool {
+        match self {
+            Self::List(v) => v.is_empty(),
+            Self::Map(m) => m.is_empty(),
+        }
+    }
+}
+
+#[derive(Clone, Debug, Serialize, Deserialize, Eq, PartialEq)]
+#[serde(untagged)]
+pub enum Tmpfs {
+    Simple(String),
+    List(Vec<String>),
+}
+
+#[cfg(feature = "indexmap")]
+#[derive(Clone, Default, Debug, Serialize, Deserialize, Eq, PartialEq)]
+pub struct Ulimits(pub IndexMap<String, Ulimit>);
+#[cfg(not(feature = "indexmap"))]
+#[derive(Clone, Default, Debug, Serialize, Deserialize, Eq, PartialEq)]
+pub struct Ulimits(pub HashMap<String, Ulimit>);
+
+impl Ulimits {
+    pub fn is_empty(&self) -> bool {
+        self.0.is_empty()
+    }
+}
+
+#[derive(Clone, Debug, Serialize, Deserialize, Eq, PartialEq)]
+#[serde(untagged)]
+pub enum Ulimit {
+    Single(i64),
+    SoftHard { soft: i64, hard: i64 },
+}
+
+#[derive(Clone, Debug, Serialize, Deserialize, Eq, PartialEq)]
+#[serde(untagged)]
+pub enum Networks {
+    Simple(Vec<String>),
+    Advanced(AdvancedNetworks),
+}
+
+impl Default for Networks {
+    fn default() -> Self {
+        Self::Simple(Vec::new())
+    }
+}
+
+impl Networks {
+    pub fn is_empty(&self) -> bool {
+        match self {
+            Self::Simple(n) => n.is_empty(),
+            Self::Advanced(n) => n.0.is_empty(),
+        }
+    }
+}
+
+#[derive(Clone, Debug, Deserialize, Serialize, Eq, PartialEq)]
+#[serde(untagged)]
+pub enum BuildStep {
+    Simple(String),
+    Advanced(AdvancedBuildStep),
+}
+
+#[derive(Builder, Clone, Debug, Deserialize, Serialize, Eq, PartialEq, Default)]
+#[serde(deny_unknown_fields)]
+#[builder(setter(into), default)]
+pub struct AdvancedBuildStep {
+    pub context: String,
+    #[serde(default, skip_serializing_if = "Option::is_none")]
+    pub dockerfile: Option<String>,
+    #[serde(default, skip_serializing_if = "Option::is_none")]
+    pub args: Option<BuildArgs>,
+    #[serde(default, skip_serializing_if = "Option::is_none")]
+    pub shm_size: Option<u64>,
+    #[serde(default, skip_serializing_if = "Option::is_none")]
+    pub target: Option<String>,
+    #[serde(default, skip_serializing_if = "Option::is_none")]
+    pub network: Option<String>,
+    #[serde(default, skip_serializing_if = "Vec::is_empty")]
+    pub cache_from: Vec<String>,
+    #[serde(default, skip_serializing_if = "Labels::is_empty")]
+    pub labels: Labels,
+}
+
+#[derive(Clone, Debug, Deserialize, Serialize, Eq, PartialEq)]
+#[serde(untagged)]
+pub enum BuildArgs {
+    Simple(String),
+    List(Vec<String>),
+    #[cfg(feature = "indexmap")]
+    KvPair(IndexMap<String, String>),
+    #[cfg(not(feature = "indexmap"))]
+    KvPair(HashMap<String, String>),
+}
+
+#[cfg(feature = "indexmap")]
+#[derive(Clone, Default, Debug, Serialize, Deserialize, Eq, PartialEq)]
+pub struct AdvancedNetworks(pub IndexMap<String, MapOrEmpty<AdvancedNetworkSettings>>);
+#[cfg(not(feature = "indexmap"))]
+#[derive(Clone, Default, Debug, Serialize, Deserialize, Eq, PartialEq)]
+pub struct AdvancedNetworks(pub HashMap<String, MapOrEmpty<AdvancedNetworkSettings>>);
+
+#[derive(Clone, Debug, Default, Serialize, Deserialize, Eq, PartialEq, Hash)]
+#[serde(deny_unknown_fields)]
+pub struct AdvancedNetworkSettings {
+    #[serde(skip_serializing_if = "Option::is_none")]
+    pub ipv4_address: Option<String>,
+    #[serde(skip_serializing_if = "Option::is_none")]
+    pub ipv6_address: Option<String>,
+    #[serde(default, skip_serializing_if = "Vec::is_empty")]
+    pub aliases: Vec<String>,
+}
+
+#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
+#[serde(untagged)]
+pub enum SysCtls {
+    List(Vec<String>),
+    #[cfg(feature = "indexmap")]
+    Map(IndexMap<String, Option<SingleValue>>),
+    #[cfg(not(feature = "indexmap"))]
+    Map(HashMap<String, Option<SingleValue>>),
+}
+
+impl Default for SysCtls {
+    fn default() -> Self {
+        Self::List(Vec::new())
+    }
+}
+
+impl SysCtls {
+    pub fn is_empty(&self) -> bool {
+        match self {
+            Self::List(v) => v.is_empty(),
+            Self::Map(m) => m.is_empty(),
+        }
+    }
+}
+
+#[cfg(feature = "indexmap")]
+#[derive(Clone, Default, Debug, Serialize, Deserialize, PartialEq)]
+pub struct TopLevelVolumes(pub IndexMap<String, MapOrEmpty<ComposeVolume>>);
+#[cfg(not(feature = "indexmap"))]
+#[derive(Clone, Default, Debug, Serialize, Deserialize, PartialEq)]
+pub struct TopLevelVolumes(pub HashMap<String, MapOrEmpty<ComposeVolume>>);
+
+impl TopLevelVolumes {
+    pub fn is_empty(&self) -> bool {
+        self.0.is_empty()
+    }
+}
+
+#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
+pub struct ComposeVolume {
+    #[serde(skip_serializing_if = "Option::is_none")]
+    pub driver: Option<String>,
+    #[cfg(feature = "indexmap")]
+    #[serde(default, skip_serializing_if = "IndexMap::is_empty")]
+    pub driver_opts: IndexMap<String, Option<SingleValue>>,
+    #[cfg(not(feature = "indexmap"))]
+    #[serde(default, skip_serializing_if = "HashMap::is_empty")]
+    pub driver_opts: HashMap<String, Option<SingleValue>>,
+    #[serde(skip_serializing_if = "Option::is_none")]
+    pub external: Option<ExternalVolume>,
+    #[serde(default, skip_serializing_if = "Labels::is_empty")]
+    pub labels: Labels,
+    #[serde(skip_serializing_if = "Option::is_none")]
+    pub name: Option<String>,
+}
+
+#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
+#[serde(untagged)]
+pub enum ExternalVolume {
+    Bool(bool),
+    Name { name: String },
+}
+
+#[cfg(feature = "indexmap")]
+#[derive(Clone, Default, Debug, Serialize, Deserialize, PartialEq)]
+pub struct ComposeNetworks(pub IndexMap<String, MapOrEmpty<NetworkSettings>>);
+
+#[cfg(not(feature = "indexmap"))]
+#[derive(Clone, Default, Debug, Serialize, Deserialize, PartialEq)]
+pub struct ComposeNetworks(pub HashMap<String, MapOrEmpty<NetworkSettings>>);
+
+impl ComposeNetworks {
+    pub fn is_empty(&self) -> bool {
+        self.0.is_empty()
+    }
+}
+
+#[derive(Clone, Debug, Serialize, Deserialize, Eq, PartialEq, Hash)]
+#[serde(untagged)]
+pub enum ComposeNetwork {
+    Detailed(ComposeNetworkSettingDetails),
+    Bool(bool),
+}
+
+#[derive(Clone, Debug, Serialize, Deserialize, Eq, PartialEq, Hash)]
+#[serde(deny_unknown_fields)]
+pub struct ComposeNetworkSettingDetails {
+    pub name: String,
+}
+
+#[derive(Clone, Debug, Serialize, Deserialize, Eq, PartialEq, Hash)]
+#[serde(deny_unknown_fields)]
+pub struct ExternalNetworkSettingBool(bool);
+
+#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Default)]
+#[serde(deny_unknown_fields)]
+pub struct NetworkSettings {
+    #[serde(default, skip_serializing_if = "std::ops::Not::not")]
+    pub attachable: bool,
+    #[serde(skip_serializing_if = "Option::is_none")]
+    pub driver: Option<String>,
+    #[cfg(feature = "indexmap")]
+    #[serde(default, skip_serializing_if = "IndexMap::is_empty")]
+    pub driver_opts: IndexMap<String, Option<SingleValue>>,
+    #[cfg(not(feature = "indexmap"))]
+    #[serde(default, skip_serializing_if = "HashMap::is_empty")]
+    pub driver_opts: HashMap<String, Option<SingleValue>>,
+    #[serde(default, skip_serializing_if = "std::ops::Not::not")]
+    pub enable_ipv6: bool,
+    #[serde(default, skip_serializing_if = "std::ops::Not::not")]
+    pub internal: bool,
+    #[serde(skip_serializing_if = "Option::is_none")]
+    pub external: Option<ComposeNetwork>,
+    #[serde(skip_serializing_if = "Option::is_none")]
+    pub ipam: Option<Ipam>,
+    #[serde(default, skip_serializing_if = "Labels::is_empty")]
+    pub labels: Labels,
+    #[serde(skip_serializing_if = "Option::is_none")]
+    pub name: Option<String>,
+}
+
+#[derive(Clone, Debug, Serialize, Deserialize, Eq, PartialEq, Hash)]
+#[serde(deny_unknown_fields)]
+pub struct Ipam {
+    #[serde(skip_serializing_if = "Option::is_none")]
+    pub driver: Option<String>,
+    #[serde(default, skip_serializing_if = "Vec::is_empty")]
+    pub config: Vec<IpamConfig>,
+}
+
+#[derive(Clone, Debug, Serialize, Deserialize, Eq, PartialEq, Hash)]
+#[serde(deny_unknown_fields)]
+pub struct IpamConfig {
+    pub subnet: String,
+    #[serde(skip_serializing_if = "Option::is_none")]
+    pub gateway: Option<String>,
+}
+
+#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Default)]
+#[serde(deny_unknown_fields)]
+pub struct Deploy {
+    #[serde(skip_serializing_if = "Option::is_none")]
+    pub mode: Option<String>,
+    #[serde(skip_serializing_if = "Option::is_none")]
+    pub replicas: Option<i64>,
+    #[serde(default, skip_serializing_if = "Vec::is_empty")]
+    pub labels: Vec<String>,
+    #[serde(skip_serializing_if = "Option::is_none")]
+    pub update_config: Option<UpdateConfig>,
+    #[serde(skip_serializing_if = "Option::is_none")]
+    pub resources: Option<Resources>,
+    #[serde(skip_serializing_if = "Option::is_none")]
+    pub restart_policy: Option<RestartPolicy>,
+    #[serde(skip_serializing_if = "Option::is_none")]
+    pub placement: Option<Placement>,
+}
+
+fn is_zero(val: &i64) -> bool {
+    *val == 0
+}
+
+#[derive(Clone, Debug, Serialize, Deserialize, Eq, PartialEq, Hash)]
+#[serde(deny_unknown_fields)]
+pub struct Healthcheck {
+    #[serde(skip_serializing_if = "Option::is_none")]
+    pub test: Option<HealthcheckTest>,
+    #[serde(skip_serializing_if = "Option::is_none")]
+    pub interval: Option<String>,
+    #[serde(skip_serializing_if = "Option::is_none")]
+    pub timeout: Option<String>,
+    #[serde(default, skip_serializing_if = "is_zero")]
+    pub retries: i64,
+    #[serde(skip_serializing_if = "Option::is_none")]
+    pub start_period: Option<String>,
+    #[serde(default, skip_serializing_if = "std::ops::Not::not")]
+    pub disable: bool,
+}
+
+#[derive(Clone, Debug, Serialize, Deserialize, Eq, PartialEq, Hash)]
+#[serde(untagged)]
+pub enum HealthcheckTest {
+    Single(String),
+    Multiple(Vec<String>),
+}
+
+#[derive(Clone, Debug, Serialize, Deserialize, Eq, PartialEq, Hash, Default)]
+#[serde(deny_unknown_fields)]
+pub struct Limits {
+    #[serde(skip_serializing_if = "Option::is_none")]
+    pub cpus: Option<String>,
+    #[serde(skip_serializing_if = "Option::is_none")]
+    pub memory: Option<String>,
+}
+
+#[derive(Clone, Debug, Serialize, Deserialize, Eq, PartialEq, Hash, Default)]
+#[serde(deny_unknown_fields)]
+pub struct Placement {
+    #[serde(skip_serializing_if = "Vec::is_empty", default)]
+    pub constraints: Vec<String>,
+    #[serde(skip_serializing_if = "Vec::is_empty", default)]
+    pub preferences: Vec<Preferences>,
+}
+
+#[derive(Clone, Debug, Serialize, Deserialize, Eq, PartialEq, Hash)]
+#[serde(deny_unknown_fields)]
+pub struct Preferences {
+    pub spread: String,
+}
+
+#[derive(Clone, Debug, Serialize, Deserialize, Eq, PartialEq, Hash, Default)]
+#[serde(deny_unknown_fields)]
+pub struct Resources {
+    pub limits: Option<Limits>,
+    pub reservations: Option<Limits>,
+}
+
+#[derive(Clone, Debug, Serialize, Deserialize, Eq, PartialEq, Hash, Default)]
+#[serde(deny_unknown_fields)]
+pub struct RestartPolicy {
+    #[serde(skip_serializing_if = "Option::is_none")]
+    pub condition: Option<String>,
+    #[serde(skip_serializing_if = "Option::is_none")]
+    pub delay: Option<String>,
+    #[serde(skip_serializing_if = "Option::is_none")]
+    pub max_attempts: Option<i64>,
+    #[serde(skip_serializing_if = "Option::is_none")]
+    pub window: Option<String>,
+}
+
+#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Default)]
+#[serde(deny_unknown_fields)]
+pub struct UpdateConfig {
+    #[serde(skip_serializing_if = "Option::is_none")]
+    pub parallelism: Option<i64>,
+    #[serde(skip_serializing_if = "Option::is_none")]
+    pub delay: Option<String>,
+    #[serde(skip_serializing_if = "Option::is_none")]
+    pub failure_action: Option<String>,
+    #[serde(skip_serializing_if = "Option::is_none")]
+    pub monitor: Option<String>,
+    #[serde(skip_serializing_if = "Option::is_none")]
+    pub max_failure_ratio: Option<f64>,
+}
+
+#[derive(Clone, Debug, Serialize, Deserialize, Eq, PartialEq, Hash)]
+#[serde(untagged)]
+pub enum Volumes {
+    Simple(Vec<String>),
+    Advanced(Vec<AdvancedVolumes>),
+}
+
+impl Default for Volumes {
+    fn default() -> Self {
+        Self::Simple(Vec::new())
+    }
+}
+
+impl Volumes {
+    pub fn is_empty(&self) -> bool {
+        match self {
+            Self::Simple(v) => v.is_empty(),
+            Self::Advanced(v) => v.is_empty(),
+        }
+    }
+}
+
+#[derive(Clone, Debug, Serialize, Deserialize, Eq, PartialEq, Hash)]
+#[serde(deny_unknown_fields)]
+pub struct AdvancedVolumes {
+    #[serde(skip_serializing_if = "Option::is_none")]
+    pub source: Option<String>,
+    pub target: String,
+    #[serde(rename = "type")]
+    pub _type: String,
+    #[serde(default, skip_serializing_if = "std::ops::Not::not")]
+    pub read_only: bool,
+    #[serde(default, skip_serializing_if = "Option::is_none")]
+    pub bind: Option<Bind>,
+    #[serde(default, skip_serializing_if = "Option::is_none")]
+    pub volume: Option<Volume>,
+    #[serde(default, skip_serializing_if = "Option::is_none")]
+    pub tmpfs: Option<TmpfsSettings>,
+}
+
+#[derive(Clone, Debug, Serialize, Deserialize, Eq, PartialEq, Hash, Default)]
+#[serde(deny_unknown_fields)]
+pub struct Bind {
+    pub propagation: String,
+}
+
+#[derive(Clone, Debug, Serialize, Deserialize, Eq, PartialEq, Hash, Default)]
+#[serde(deny_unknown_fields)]
+pub struct Volume {
+    pub nocopy: bool,
+}
+
+#[derive(Clone, Debug, Serialize, Deserialize, Eq, PartialEq, Hash, Default)]
+#[serde(deny_unknown_fields)]
+pub struct TmpfsSettings {
+    pub size: u64,
+}
+
+#[derive(Clone, Debug, Serialize, Deserialize, Eq, PartialEq, Hash)]
+#[serde(untagged)]
+pub enum Command {
+    Simple(String),
+    Args(Vec<String>),
+}
+
+#[derive(Clone, Debug, Serialize, Deserialize, Eq, PartialEq, Hash)]
+#[serde(untagged)]
+pub enum Entrypoint {
+    Simple(String),
+    List(Vec<String>),
+}
+
+#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, PartialOrd)]
+#[serde(untagged)]
+pub enum SingleValue {
+    String(String),
+    Bool(bool),
+    Unsigned(u64),
+    Signed(i64),
+    Float(f64),
+}
+
+impl fmt::Display for SingleValue {
+    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
+        match self {
+            Self::String(s) => f.write_str(s),
+            Self::Bool(b) => write!(f, "{b}"),
+            Self::Unsigned(u) => write!(f, "{u}"),
+            Self::Signed(i) => write!(f, "{i}"),
+            Self::Float(fl) => write!(f, "{fl}"),
+        }
+    }
+}
+
+#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Hash)]
+#[serde(untagged)]
+pub enum MapOrEmpty<T> {
+    Map(T),
+    Empty,
+}
+
+impl<T> Default for MapOrEmpty<T> {
+    fn default() -> Self {
+        Self::Empty
+    }
+}
+
+impl<T> From<MapOrEmpty<T>> for Option<T> {
+    fn from(value: MapOrEmpty<T>) -> Self {
+        match value {
+            MapOrEmpty::Map(t) => Some(t),
+            MapOrEmpty::Empty => None,
+        }
+    }
+}
+
+impl<T> Serialize for MapOrEmpty<T>
+    where
+        T: Serialize,
+{
+    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
+        where
+            S: serde::Serializer,
+    {
+        match self {
+            Self::Map(t) => t.serialize(serializer),
+            Self::Empty => {
+                use serde::ser::SerializeMap;
+                serializer.serialize_map(None)?.end()
+            }
+        }
+    }
+}
\ No newline at end of file
diff --git a/src/helpers/stack/mod.rs b/src/helpers/stack/mod.rs
new file mode 100644
index 00000000..7da2d2ec
--- /dev/null
+++ b/src/helpers/stack/mod.rs
@@ -0,0 +1,2 @@
+pub(crate) mod builder;
+pub(crate) mod dctypes;
\ No newline at end of file
diff --git a/src/models/stack.rs b/src/models/stack.rs
index 21432d35..9ce7370e 100644
--- a/src/models/stack.rs
+++ b/src/models/stack.rs
@@ -3,7 +3,7 @@ use serde_json::Value;
 use uuid::Uuid;
 use serde::{Serialize,Deserialize};
 
-#[derive(Debug, Serialize, Deserialize)]
+#[derive(Debug, Serialize, Deserialize, Clone)]
 pub struct Stack {
     pub id: i32,       // id - is a unique identifier for the app stack
     pub stack_id: Uuid, // external stack ID
diff --git a/src/routes/stack/add.rs b/src/routes/stack/add.rs
index d1d8b87c..aa1cfcee 100644
--- a/src/routes/stack/add.rs
+++ b/src/routes/stack/add.rs
@@ -15,8 +15,7 @@ use sqlx::PgPool;
 use std::str;
 use tracing::Instrument;
 use uuid::Uuid;
-
-
+use crate::helpers::stack::builder::DcBuilder;
 
 #[tracing::instrument(name = "Add stack.")]
 #[post("")]
@@ -110,3 +109,52 @@ pub async fn add(
         }
     };
 }
+
+#[tracing::instrument(name = "Generate docker-compose.")]
+#[post("/{id}")]
+pub async fn gen(
+    user: web::ReqData<User>,
+    path: web::Path<(i32,)>,
+    pool: Data<PgPool>,
+) -> Result<impl Responder> {
+    let id = path.0;
+    tracing::debug!("Received id: {}", id);
+
+    let stack = match sqlx::query_as!(
+        Stack,
+        r#"
+        SELECT * FROM user_stack WHERE id=$1 AND user_id=$2 LIMIT 1
+        "#,
+        id, user.id
+    )
+            .fetch_one(pool.get_ref())
+            .await
+        {
+            Ok(stack) => {
+                tracing::info!("stack found: {:?}", stack.id,);
+                Some(stack)
+            }
+            Err(sqlx::Error::RowNotFound) => {
+                tracing::error!("Row not found 404");
+                None
+            }
+            Err(e) => {
+                tracing::error!("Failed to fetch stack, error: {:?}", e);
+                None
+            }
+        };
+
+
+    match stack {
+        Some(stack) => {
+            let mut dc = DcBuilder::new();
+            let fc = dc.build(stack);
+            tracing::debug!("Docker compose file content {:?}", fc.unwrap());
+        }
+        None => {
+
+        }
+    }
+
+    Ok(Json(JsonResponse::<Stack>::ok(1, "OK")))
+}
diff --git a/src/routes/stack/get.rs b/src/routes/stack/get.rs
index 7b5fa7f5..b3ce3898 100644
--- a/src/routes/stack/get.rs
+++ b/src/routes/stack/get.rs
@@ -46,3 +46,4 @@ pub async fn get(
         }
     }
 }
+
diff --git a/src/startup.rs b/src/startup.rs
index edd58c1b..773ce2c0 100644
--- a/src/startup.rs
+++ b/src/startup.rs
@@ -128,6 +128,7 @@ pub async fn run(
                     .wrap(HttpAuthentication::bearer(bearer_guard))
                     .wrap(Cors::permissive())
                     .service(crate::routes::stack::add::add)
+                    .service(crate::routes::stack::add::gen)
                     .service(crate::routes::stack::get::get)
             )
             .app_data(db_pool.clone())

From d04ecab475ff60907c257246aa4fed29d9213029 Mon Sep 17 00:00:00 2001
From: vsilent <jabberroid@gmail.com>
Date: Tue, 7 Nov 2023 16:23:09 +0200
Subject: [PATCH 04/18] return contents of the compose file on build

---
 files/{docker-compose.yml => 1.yml}           |  0
 .../c60d329f-9159-48b7-abf3-10ccabbca213.yml  | 20 ++++++
 src/helpers/stack/builder.rs                  | 65 +++++++------------
 src/routes/stack/add.rs                       | 21 ++++--
 4 files changed, 59 insertions(+), 47 deletions(-)
 rename files/{docker-compose.yml => 1.yml} (100%)
 create mode 100644 files/c60d329f-9159-48b7-abf3-10ccabbca213.yml

diff --git a/files/docker-compose.yml b/files/1.yml
similarity index 100%
rename from files/docker-compose.yml
rename to files/1.yml
diff --git a/files/c60d329f-9159-48b7-abf3-10ccabbca213.yml b/files/c60d329f-9159-48b7-abf3-10ccabbca213.yml
new file mode 100644
index 00000000..c6e60d64
--- /dev/null
+++ b/files/c60d329f-9159-48b7-abf3-10ccabbca213.yml
@@ -0,0 +1,20 @@
+version: '3.8'
+services:
+  smarty-bot:
+    image: trydirect/smarty-bot:latest
+    ports:
+    - target: 8000
+      published: 8000
+    restart: always
+  pgrst:
+    image: pgrst
+    ports:
+    - target: 9999
+      published: 9999
+    restart: always
+  portainer_ce_feature:
+    image: portainer-ce-feature
+    ports:
+    - target: 9000
+      published: 9000
+    restart: always
diff --git a/src/helpers/stack/builder.rs b/src/helpers/stack/builder.rs
index 023ee964..1ee5fe9b 100644
--- a/src/helpers/stack/builder.rs
+++ b/src/helpers/stack/builder.rs
@@ -1,7 +1,18 @@
-use crate::helpers::stack::dctypes::{Compose, Port, Ports, PublishedPort, Service, Services, SingleService};
+use crate::helpers::stack::dctypes::{
+    Compose,
+    Port,
+    Ports,
+    PublishedPort,
+    Service,
+    Services
+};
 use serde_yaml;
 use crate::forms;
-use crate::forms::{StackForm, Web, Feature};
+use crate::forms::{
+    StackForm,
+    Web,
+    Feature
+};
 use crate::models::stack::Stack;
 
 #[derive(Clone, Debug)]
@@ -60,44 +71,17 @@ fn convert_shared_ports(ports: Vec<String>) -> Result<Vec<Port>, String> {
 
 impl DcBuilder {
 
-    pub fn new() -> Self {
+    pub fn new(stack: Stack) -> Self {
         DcBuilder {
             config: Config::default(),
-            stack: Stack {
-                id: 0,
-                stack_id: Default::default(),
-                user_id: "".to_string(),
-                name: "".to_string(),
-                body: Default::default(),
-                created_at: Default::default(),
-                updated_at: Default::default(),
-            },
+            stack: stack,
         }
     }
 
-    // pub fn add_ports(&self, ports: &Vec<String>) -> Vec<Port> {
-    //     // @todo re-factor using TryInto or TryFrom
-    //
-    //     let mut _ports:Vec<Port> = vec![];
-    //     for p in ports {
-    //         let port = p.parse::<u16>().unwrap();
-    //         _ports.push(
-    //             Port {
-    //                 target: port,
-    //                 host_ip: None,
-    //                 published: Some(PublishedPort::Single(port)),
-    //                 protocol: None,
-    //                 mode: None,
-    //             }
-    //         );
-    //     }
-    //     _ports
-    // }
-
-    pub fn build(&self, stack:Stack) -> Option<Compose> {
-
-        tracing::debug!("Start build docker compose from {:?}", stack.body);
-        let _stack = serde_json::from_value::<StackForm>(stack.body);
+    pub fn build(&self) -> Option<String> {
+
+        tracing::debug!("Start build docker compose from {:?}", &self.stack.body);
+        let _stack = serde_json::from_value::<StackForm>(self.stack.body.clone());
         let mut services = indexmap::IndexMap::new();
         match _stack  {
             Ok(apps) => {
@@ -115,7 +99,6 @@ impl DcBuilder {
 
                     if let Some(ports) = &app.shared_ports {
                         if !ports.is_empty() {
-                            // service.ports = Ports::Long(self.add_ports(ports));
                             service.ports = Ports::Long(app.try_into().unwrap())
                         }
                     }
@@ -142,7 +125,6 @@ impl DcBuilder {
 
                             if let Some(ports) = &app.shared_ports {
                                 if !ports.is_empty() {
-                                    // service.ports = Ports::Long(self.add_ports(ports));
                                     service.ports = Ports::Long(app.try_into().unwrap())
                                 }
                             }
@@ -167,7 +149,6 @@ impl DcBuilder {
 
                             if let Some(ports) = &app.shared_ports {
                                 if !ports.is_empty() {
-                                    // service.ports = Ports::Long(self.add_ports(ports));
                                     service.ports = Ports::Long(app.try_into().unwrap())
                                 }
                             }
@@ -193,15 +174,17 @@ impl DcBuilder {
             ..Default::default()
         };
 
-        let target_file = std::path::Path::new("./files/docker-compose.yml");
+        let fname= format!("./files/{}.yml", self.stack.stack_id);
+        tracing::debug!("Save docker compose to file {:?}", fname);
+        let target_file = std::path::Path::new(fname.as_str());
         // serialize to string
         let serialized = match serde_yaml::to_string(&compose_content) {
             Ok(s) => s,
             Err(e) => panic!("Failed to serialize docker-compose file: {}", e),
         };
         // serialize to file
-        std::fs::write(target_file, serialized).unwrap();
+        std::fs::write(target_file, serialized.clone()).unwrap();
 
-        Some(compose_content)
+        Some(serialized)
     }
 }
diff --git a/src/routes/stack/add.rs b/src/routes/stack/add.rs
index aa1cfcee..ef3e0fcb 100644
--- a/src/routes/stack/add.rs
+++ b/src/routes/stack/add.rs
@@ -16,6 +16,7 @@ use std::str;
 use tracing::Instrument;
 use uuid::Uuid;
 use crate::helpers::stack::builder::DcBuilder;
+use crate::helpers::stack::dctypes::Compose;
 
 #[tracing::instrument(name = "Add stack.")]
 #[post("")]
@@ -147,14 +148,22 @@ pub async fn gen(
 
     match stack {
         Some(stack) => {
-            let mut dc = DcBuilder::new();
-            let fc = dc.build(stack);
-            tracing::debug!("Docker compose file content {:?}", fc.unwrap());
+            let id = stack.id.clone();
+            let mut dc = DcBuilder::new(stack);
+            let fc = dc.build();
+            // tracing::debug!("Docker compose file content {:?}", fc.unwrap());
+            return Ok(Json(JsonResponse::new(
+                "OK".to_owned(),
+                "Success".to_owned(),
+                200,
+                Some(id),
+                Some(fc.unwrap()),
+                None
+            )));
+
         }
         None => {
-
+            return Ok(Json(JsonResponse::internal_error("Could not generate compose file")));
         }
     }
-
-    Ok(Json(JsonResponse::<Stack>::ok(1, "OK")))
 }

From 327bc563a1ab8fc2dee5451e57b7d4b7d9f396d5 Mon Sep 17 00:00:00 2001
From: vsilent <jabberroid@gmail.com>
Date: Tue, 7 Nov 2023 16:25:00 +0200
Subject: [PATCH 05/18] exclude ./files from git

---
 .gitignore                                    |  1 +
 .idea/stacker.iml                             |  2 ++
 files/1.yml                                   | 20 -------------------
 .../c60d329f-9159-48b7-abf3-10ccabbca213.yml  | 20 -------------------
 4 files changed, 3 insertions(+), 40 deletions(-)
 delete mode 100644 files/1.yml
 delete mode 100644 files/c60d329f-9159-48b7-abf3-10ccabbca213.yml

diff --git a/.gitignore b/.gitignore
index c5078494..3acd8afb 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1,2 +1,3 @@
 target
 .idea
+files
diff --git a/.idea/stacker.iml b/.idea/stacker.iml
index 227e58a7..a97e9259 100644
--- a/.idea/stacker.iml
+++ b/.idea/stacker.iml
@@ -4,6 +4,8 @@
     <content url="file://$MODULE_DIR$">
       <sourceFolder url="file://$MODULE_DIR$/src" isTestSource="false" />
       <sourceFolder url="file://$MODULE_DIR$/tests" isTestSource="true" />
+      <excludeFolder url="file://$MODULE_DIR$/.idea/dataSources" />
+      <excludeFolder url="file://$MODULE_DIR$/files" />
       <excludeFolder url="file://$MODULE_DIR$/target" />
     </content>
     <orderEntry type="inheritedJdk" />
diff --git a/files/1.yml b/files/1.yml
deleted file mode 100644
index c6e60d64..00000000
--- a/files/1.yml
+++ /dev/null
@@ -1,20 +0,0 @@
-version: '3.8'
-services:
-  smarty-bot:
-    image: trydirect/smarty-bot:latest
-    ports:
-    - target: 8000
-      published: 8000
-    restart: always
-  pgrst:
-    image: pgrst
-    ports:
-    - target: 9999
-      published: 9999
-    restart: always
-  portainer_ce_feature:
-    image: portainer-ce-feature
-    ports:
-    - target: 9000
-      published: 9000
-    restart: always
diff --git a/files/c60d329f-9159-48b7-abf3-10ccabbca213.yml b/files/c60d329f-9159-48b7-abf3-10ccabbca213.yml
deleted file mode 100644
index c6e60d64..00000000
--- a/files/c60d329f-9159-48b7-abf3-10ccabbca213.yml
+++ /dev/null
@@ -1,20 +0,0 @@
-version: '3.8'
-services:
-  smarty-bot:
-    image: trydirect/smarty-bot:latest
-    ports:
-    - target: 8000
-      published: 8000
-    restart: always
-  pgrst:
-    image: pgrst
-    ports:
-    - target: 9999
-      published: 9999
-    restart: always
-  portainer_ce_feature:
-    image: portainer-ce-feature
-    ports:
-    - target: 9000
-      published: 9000
-    restart: always

From 9726e6d97c0a2304006769ebd283582857f0c81a Mon Sep 17 00:00:00 2001
From: vsilent <jabberroid@gmail.com>
Date: Sun, 12 Nov 2023 11:24:44 +0200
Subject: [PATCH 06/18] generate docker compose, response builder, dctypes

---
 Cargo.lock                   | 508 ++++++++++++++++++++++++++++++++++-
 Cargo.toml                   |   4 +-
 configuration.yaml           |   7 +
 src/configuration.rs         |  17 ++
 src/forms/stack.rs           |  36 ++-
 src/helpers/json.rs          |  95 +++++--
 src/helpers/stack/builder.rs |   4 +-
 src/models/stack.rs          |  10 +
 src/routes/stack/add.rs      |  65 +----
 src/routes/stack/compose.rs  |  71 +++++
 src/routes/stack/deploy.rs   | 124 ++++++++-
 src/routes/stack/get.rs      |  28 +-
 src/routes/stack/mod.rs      |   2 +
 src/startup.rs               |   3 +-
 14 files changed, 873 insertions(+), 101 deletions(-)
 create mode 100644 src/routes/stack/compose.rs

diff --git a/Cargo.lock b/Cargo.lock
index 68ef0eb4..332a9ecc 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -279,6 +279,54 @@ version = "0.2.16"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "0942ffc6dcaadf03badf6e6a2d0228460359d5e34b57ccdc720b7382dfbd5ec5"
 
+[[package]]
+name = "amq-protocol"
+version = "7.1.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1d40d8b2465c7959dd40cee32ba6ac334b5de57e9fca0cc756759894a4152a5d"
+dependencies = [
+ "amq-protocol-tcp",
+ "amq-protocol-types",
+ "amq-protocol-uri",
+ "cookie-factory",
+ "nom",
+ "serde",
+]
+
+[[package]]
+name = "amq-protocol-tcp"
+version = "7.1.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9cb2100adae7da61953a2c3a01935d86caae13329fadce3333f524d6d6ce12e2"
+dependencies = [
+ "amq-protocol-uri",
+ "tcp-stream",
+ "tracing",
+]
+
+[[package]]
+name = "amq-protocol-types"
+version = "7.1.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "156ff13c8a3ced600b4e54ed826a2ae6242b6069d00dd98466827cef07d3daff"
+dependencies = [
+ "cookie-factory",
+ "nom",
+ "serde",
+ "serde_json",
+]
+
+[[package]]
+name = "amq-protocol-uri"
+version = "7.1.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "751bbd7d440576066233e740576f1b31fdc6ab86cfabfbd48c548de77eca73e4"
+dependencies = [
+ "amq-protocol-types",
+ "percent-encoding",
+ "url",
+]
+
 [[package]]
 name = "android-tzdata"
 version = "0.1.1"
@@ -294,6 +342,104 @@ dependencies = [
  "libc",
 ]
 
+[[package]]
+name = "async-channel"
+version = "1.9.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "81953c529336010edd6d8e358f886d9581267795c61b19475b71314bffa46d35"
+dependencies = [
+ "concurrent-queue",
+ "event-listener",
+ "futures-core",
+]
+
+[[package]]
+name = "async-executor"
+version = "1.6.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "4b0c4a4f319e45986f347ee47fef8bf5e81c9abc3f6f58dc2391439f30df65f0"
+dependencies = [
+ "async-lock",
+ "async-task",
+ "concurrent-queue",
+ "fastrand 2.0.1",
+ "futures-lite",
+ "slab",
+]
+
+[[package]]
+name = "async-global-executor"
+version = "2.3.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f1b6f5d7df27bd294849f8eec66ecfc63d11814df7a4f5d74168a2394467b776"
+dependencies = [
+ "async-channel",
+ "async-executor",
+ "async-io",
+ "async-lock",
+ "blocking",
+ "futures-lite",
+ "once_cell",
+]
+
+[[package]]
+name = "async-global-executor-trait"
+version = "2.1.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "33dd14c5a15affd2abcff50d84efd4009ada28a860f01c14f9d654f3e81b3f75"
+dependencies = [
+ "async-global-executor",
+ "async-trait",
+ "executor-trait",
+]
+
+[[package]]
+name = "async-io"
+version = "1.13.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0fc5b45d93ef0529756f812ca52e44c221b35341892d3dcc34132ac02f3dd2af"
+dependencies = [
+ "async-lock",
+ "autocfg",
+ "cfg-if",
+ "concurrent-queue",
+ "futures-lite",
+ "log",
+ "parking",
+ "polling",
+ "rustix 0.37.27",
+ "slab",
+ "socket2 0.4.9",
+ "waker-fn",
+]
+
+[[package]]
+name = "async-lock"
+version = "2.8.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "287272293e9d8c41773cec55e365490fe034813a2f172f502d6ddcf75b2f582b"
+dependencies = [
+ "event-listener",
+]
+
+[[package]]
+name = "async-reactor-trait"
+version = "1.1.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7a6012d170ad00de56c9ee354aef2e358359deb1ec504254e0e5a3774771de0e"
+dependencies = [
+ "async-io",
+ "async-trait",
+ "futures-core",
+ "reactor-trait",
+]
+
+[[package]]
+name = "async-task"
+version = "4.5.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b4eb2cdb97421e01129ccb49169d8279ed21e829929144f4a22a6e54ac549ca1"
+
 [[package]]
 name = "async-trait"
 version = "0.1.74"
@@ -314,6 +460,12 @@ dependencies = [
  "num-traits",
 ]
 
+[[package]]
+name = "atomic-waker"
+version = "1.1.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1505bd5d3d116872e7271a6d4e16d81d0c8570876c8de68093a09ac269d8aac0"
+
 [[package]]
 name = "autocfg"
 version = "1.1.0"
@@ -368,6 +520,31 @@ dependencies = [
  "generic-array",
 ]
 
+[[package]]
+name = "block-padding"
+version = "0.3.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a8894febbff9f758034a5b8e12d87918f56dfc64a8e1fe757d65e29041538d93"
+dependencies = [
+ "generic-array",
+]
+
+[[package]]
+name = "blocking"
+version = "1.4.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8c36a4d0d48574b3dd360b4b7d95cc651d2b6557b6402848a27d4b228a473e2a"
+dependencies = [
+ "async-channel",
+ "async-lock",
+ "async-task",
+ "fastrand 2.0.1",
+ "futures-io",
+ "futures-lite",
+ "piper",
+ "tracing",
+]
+
 [[package]]
 name = "brotli"
 version = "3.4.0"
@@ -416,6 +593,15 @@ dependencies = [
  "bytes",
 ]
 
+[[package]]
+name = "cbc"
+version = "0.1.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "26b52a9543ae338f279b96b0b9fed9c8093744685043739079ce85cd58f289a6"
+dependencies = [
+ "cipher",
+]
+
 [[package]]
 name = "cc"
 version = "1.0.83"
@@ -448,6 +634,25 @@ dependencies = [
  "windows-targets",
 ]
 
+[[package]]
+name = "cipher"
+version = "0.4.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "773f3b9af64447d2ce9850330c473515014aa235e6a783b02db81ff39e4a3dad"
+dependencies = [
+ "crypto-common",
+ "inout",
+]
+
+[[package]]
+name = "concurrent-queue"
+version = "2.3.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f057a694a54f12365049b0958a1685bb52d567f5593b355fbf685838e873d400"
+dependencies = [
+ "crossbeam-utils",
+]
+
 [[package]]
 name = "config"
 version = "0.13.3"
@@ -484,6 +689,12 @@ dependencies = [
  "version_check",
 ]
 
+[[package]]
+name = "cookie-factory"
+version = "0.3.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "396de984970346b0d9e93d1415082923c679e5ae5c3ee3dcbd104f5610af126b"
+
 [[package]]
 name = "core-foundation"
 version = "0.9.3"
@@ -650,6 +861,15 @@ dependencies = [
  "syn 1.0.109",
 ]
 
+[[package]]
+name = "des"
+version = "0.8.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ffdd80ce8ce993de27e9f063a444a4d53ce8e8db4c1f00cc03af5ad5a9867a1e"
+dependencies = [
+ "cipher",
+]
+
 [[package]]
 name = "digest"
 version = "0.10.7"
@@ -687,6 +907,12 @@ version = "0.3.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "0688c2a7f92e427f44895cd63841bff7b29f8d7a1648b9e7e07a4a365b2e1257"
 
+[[package]]
+name = "doc-comment"
+version = "0.3.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "fea41bba32d969b513997752735605054bc0dfa92b4c56bf1189f2e174be7a10"
+
 [[package]]
 name = "dotenvy"
 version = "0.15.7"
@@ -733,6 +959,24 @@ version = "2.5.3"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "0206175f82b8d6bf6652ff7d71a1e27fd2e4efde587fd368662814d6ec1d9ce0"
 
+[[package]]
+name = "executor-trait"
+version = "2.1.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1a1052dd43212a7777ec6a69b117da52f5e52f07aec47d00c1a2b33b85d06b08"
+dependencies = [
+ "async-trait",
+]
+
+[[package]]
+name = "fastrand"
+version = "1.9.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e51093e27b0797c359783294ca4f0a911c270184cb10f85783b118614a1501be"
+dependencies = [
+ "instant",
+]
+
 [[package]]
 name = "fastrand"
 version = "2.0.1"
@@ -755,6 +999,18 @@ dependencies = [
  "miniz_oxide",
 ]
 
+[[package]]
+name = "flume"
+version = "0.10.14"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1657b4441c3403d9f7b3409e47575237dac27b1b5726df654a6ecbf92f0f7577"
+dependencies = [
+ "futures-core",
+ "futures-sink",
+ "pin-project",
+ "spin 0.9.8",
+]
+
 [[package]]
 name = "fnv"
 version = "1.0.7"
@@ -812,6 +1068,27 @@ dependencies = [
  "parking_lot 0.11.2",
 ]
 
+[[package]]
+name = "futures-io"
+version = "0.3.29"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8bf34a163b5c4c52d0478a4d757da8fb65cabef42ba90515efee0f6f9fa45aaa"
+
+[[package]]
+name = "futures-lite"
+version = "1.13.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "49a9d51ce47660b1e808d3c990b4709f2f415d928835a17dfd16991515c46bce"
+dependencies = [
+ "fastrand 1.9.0",
+ "futures-core",
+ "futures-io",
+ "memchr",
+ "parking",
+ "pin-project-lite",
+ "waker-fn",
+]
+
 [[package]]
 name = "futures-sink"
 version = "0.3.28"
@@ -1099,6 +1376,16 @@ dependencies = [
  "serde",
 ]
 
+[[package]]
+name = "inout"
+version = "0.1.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a0c10553d664a4d0bcff9f4215d0aac67a639cc68ef660840afe309b807bc9f5"
+dependencies = [
+ "block-padding",
+ "generic-array",
+]
+
 [[package]]
 name = "instant"
 version = "0.1.12"
@@ -1108,6 +1395,17 @@ dependencies = [
  "cfg-if",
 ]
 
+[[package]]
+name = "io-lifetimes"
+version = "1.0.11"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "eae7b9aee968036d54dce06cebaefd919e4472e753296daccd6d344e3e2df0c2"
+dependencies = [
+ "hermit-abi",
+ "libc",
+ "windows-sys",
+]
+
 [[package]]
 name = "ipnet"
 version = "2.8.0"
@@ -1173,6 +1471,29 @@ version = "0.3.2"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "d4345964bb142484797b161f473a503a434de77149dd8c7427788c6e13379388"
 
+[[package]]
+name = "lapin"
+version = "2.3.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5f3067a1fcfbc3fc46455809c023e69b8f6602463201010f4ae5a3b572adb9dc"
+dependencies = [
+ "amq-protocol",
+ "async-global-executor-trait",
+ "async-reactor-trait",
+ "async-trait",
+ "executor-trait",
+ "flume",
+ "futures-core",
+ "futures-io",
+ "parking_lot 0.12.1",
+ "pinky-swear",
+ "reactor-trait",
+ "serde",
+ "serde_json",
+ "tracing",
+ "waker-fn",
+]
+
 [[package]]
 name = "lazy_static"
 version = "1.4.0"
@@ -1191,6 +1512,12 @@ version = "0.5.6"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "0717cef1bc8b636c6e1c1bbdefc09e6322da8a9321966e8928ef80d20f7f770f"
 
+[[package]]
+name = "linux-raw-sys"
+version = "0.3.8"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ef53942eb7bf7ff43a617b3e2c1c4a5ecf5944a7c1bc12d7ee39bbb15e5c1519"
+
 [[package]]
 name = "linux-raw-sys"
 version = "0.4.10"
@@ -1420,6 +1747,29 @@ version = "0.1.1"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "b15813163c1d831bf4a13c3610c05c0d03b39feb07f7e09fa234dac9b15aaf39"
 
+[[package]]
+name = "p12"
+version = "0.6.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d4873306de53fe82e7e484df31e1e947d61514b6ea2ed6cd7b45d63006fd9224"
+dependencies = [
+ "cbc",
+ "cipher",
+ "des",
+ "getrandom",
+ "hmac",
+ "lazy_static",
+ "rc2",
+ "sha1",
+ "yasna",
+]
+
+[[package]]
+name = "parking"
+version = "2.2.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "bb813b8af86854136c6922af0598d719255ecb2179515e6e7730d468f05c9cae"
+
 [[package]]
 name = "parking_lot"
 version = "0.11.2"
@@ -1563,12 +1913,51 @@ version = "0.1.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184"
 
+[[package]]
+name = "pinky-swear"
+version = "6.1.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d894b67aa7a4bf295db5e85349078c604edaa6fa5c8721e8eca3c7729a27f2ac"
+dependencies = [
+ "doc-comment",
+ "flume",
+ "parking_lot 0.12.1",
+ "tracing",
+]
+
+[[package]]
+name = "piper"
+version = "0.2.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "668d31b1c4eba19242f2088b2bf3316b82ca31082a8335764db4e083db7485d4"
+dependencies = [
+ "atomic-waker",
+ "fastrand 2.0.1",
+ "futures-io",
+]
+
 [[package]]
 name = "pkg-config"
 version = "0.3.27"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "26072860ba924cbfa98ea39c8c19b4dd6a4a25423dbdf219c1eca91aa0cf6964"
 
+[[package]]
+name = "polling"
+version = "2.8.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "4b2d323e8ca7996b3e23126511a523f7e62924d93ecd5ae73b333815b0eb3dce"
+dependencies = [
+ "autocfg",
+ "bitflags 1.3.2",
+ "cfg-if",
+ "concurrent-queue",
+ "libc",
+ "log",
+ "pin-project-lite",
+ "windows-sys",
+]
+
 [[package]]
 name = "powerfmt"
 version = "0.2.0"
@@ -1653,6 +2042,26 @@ dependencies = [
  "getrandom",
 ]
 
+[[package]]
+name = "rc2"
+version = "0.8.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "62c64daa8e9438b84aaae55010a93f396f8e60e3911590fcba770d04643fc1dd"
+dependencies = [
+ "cipher",
+]
+
+[[package]]
+name = "reactor-trait"
+version = "1.1.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "438a4293e4d097556730f4711998189416232f009c137389e0f961d2bc0ddc58"
+dependencies = [
+ "async-trait",
+ "futures-core",
+ "futures-io",
+]
+
 [[package]]
 name = "redox_syscall"
 version = "0.2.16"
@@ -1838,6 +2247,20 @@ dependencies = [
  "semver",
 ]
 
+[[package]]
+name = "rustix"
+version = "0.37.27"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "fea8ca367a3a01fe35e6943c400addf443c0f57670e6ec51196f71a4b8762dd2"
+dependencies = [
+ "bitflags 1.3.2",
+ "errno",
+ "io-lifetimes",
+ "libc",
+ "linux-raw-sys 0.3.8",
+ "windows-sys",
+]
+
 [[package]]
 name = "rustix"
 version = "0.38.19"
@@ -1847,7 +2270,7 @@ dependencies = [
  "bitflags 2.4.1",
  "errno",
  "libc",
- "linux-raw-sys",
+ "linux-raw-sys 0.4.10",
  "windows-sys",
 ]
 
@@ -1863,6 +2286,42 @@ dependencies = [
  "webpki",
 ]
 
+[[package]]
+name = "rustls"
+version = "0.21.8"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "446e14c5cda4f3f30fe71863c34ec70f5ac79d6087097ad0bb433e1be5edf04c"
+dependencies = [
+ "log",
+ "ring 0.17.4",
+ "rustls-webpki",
+ "sct",
+]
+
+[[package]]
+name = "rustls-connector"
+version = "0.18.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "060bcc1795b840d0e56d78f3293be5f652aa1611d249b0e63ffe19f4a8c9ae23"
+dependencies = [
+ "log",
+ "rustls 0.21.8",
+ "rustls-native-certs",
+ "rustls-webpki",
+]
+
+[[package]]
+name = "rustls-native-certs"
+version = "0.6.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a9aace74cb666635c918e9c12bc0d348266037aa8eb599b5cba565709a8dff00"
+dependencies = [
+ "openssl-probe",
+ "rustls-pemfile",
+ "schannel",
+ "security-framework",
+]
+
 [[package]]
 name = "rustls-pemfile"
 version = "1.0.3"
@@ -1872,6 +2331,16 @@ dependencies = [
  "base64 0.21.4",
 ]
 
+[[package]]
+name = "rustls-webpki"
+version = "0.101.7"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8b6275d1ee7a1cd780b64aca7726599a1dbc893b1e64144529e55c3c2f745765"
+dependencies = [
+ "ring 0.17.4",
+ "untrusted 0.9.0",
+]
+
 [[package]]
 name = "ryu"
 version = "1.0.15"
@@ -2118,6 +2587,9 @@ name = "spin"
 version = "0.9.8"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "6980e8d7511241f8acf4aebddbb1ff938df5eebe98691418c4468d0b72a96a67"
+dependencies = [
+ "lock_api",
+]
 
 [[package]]
 name = "sqlformat"
@@ -2177,7 +2649,7 @@ dependencies = [
  "paste",
  "percent-encoding",
  "rand",
- "rustls",
+ "rustls 0.20.9",
  "rustls-pemfile",
  "serde",
  "serde_json",
@@ -2238,8 +2710,10 @@ dependencies = [
  "chrono",
  "config",
  "derive_builder",
+ "futures-lite",
  "glob",
  "indexmap 2.1.0",
+ "lapin",
  "rand",
  "regex",
  "reqwest",
@@ -2325,6 +2799,18 @@ dependencies = [
  "libc",
 ]
 
+[[package]]
+name = "tcp-stream"
+version = "0.26.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "4da30af7998f51ee1aa48ab24276fe303a697b004e31ff542b192c088d5630a5"
+dependencies = [
+ "cfg-if",
+ "p12",
+ "rustls-connector",
+ "rustls-pemfile",
+]
+
 [[package]]
 name = "tempfile"
 version = "3.8.0"
@@ -2332,9 +2818,9 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "cb94d2f3cc536af71caac6b6fcebf65860b347e7ce0cc9ebe8f70d3e521054ef"
 dependencies = [
  "cfg-if",
- "fastrand",
+ "fastrand 2.0.1",
  "redox_syscall 0.3.5",
- "rustix",
+ "rustix 0.38.19",
  "windows-sys",
 ]
 
@@ -2469,7 +2955,7 @@ version = "0.23.4"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "c43ee83903113e03984cb9e5cebe6c04a5116269e900e3ddba8f068a62adda59"
 dependencies = [
- "rustls",
+ "rustls 0.20.9",
  "tokio",
  "webpki",
 ]
@@ -2714,6 +3200,12 @@ version = "0.9.4"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "49874b5167b65d7193b8aba1567f5c7d93d001cafc34600cee003eda787e483f"
 
+[[package]]
+name = "waker-fn"
+version = "1.1.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f3c4517f54858c779bbcbf228f4fca63d121bf85fbecb2dc578cdf4a39395690"
+
 [[package]]
 name = "want"
 version = "0.3.1"
@@ -2956,6 +3448,12 @@ dependencies = [
  "linked-hash-map",
 ]
 
+[[package]]
+name = "yasna"
+version = "0.5.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e17bb3549cc1321ae1296b9cdc2698e2b6cb1992adfa19a8c72e5b7a738f44cd"
+
 [[package]]
 name = "zstd"
 version = "0.12.4"
diff --git a/Cargo.toml b/Cargo.toml
index 581f4d5a..4112688c 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -38,6 +38,8 @@ rand = "0.8.5"
 derive_builder = "0.12.0"
 indexmap = { version = "2.0.0", features = ["serde"], optional = true }
 serde_yaml = "0.9"
+lapin = { version = "2.3.1", features = ["serde_json"] }
+futures-lite = "1.13.0"
 
 [dependencies.sqlx]
 version = "0.6.3"
@@ -56,4 +58,4 @@ default = ["indexmap"]
 indexmap = ["dep:indexmap"]
 
 [dev-dependencies]
-glob = "0.3"
\ No newline at end of file
+glob = "0.3"
diff --git a/configuration.yaml b/configuration.yaml
index 1de0778b..4f88a4cb 100644
--- a/configuration.yaml
+++ b/configuration.yaml
@@ -9,3 +9,10 @@ database:
   username: postgres
   password: postgres
   database_name: stacker
+
+amqp:
+  host: 51.15.74.139
+  port: 5672
+  username: guest
+  password: wua4Eeyi
+
diff --git a/src/configuration.rs b/src/configuration.rs
index a3beeaf4..90d22c92 100644
--- a/src/configuration.rs
+++ b/src/configuration.rs
@@ -7,6 +7,7 @@ pub struct Settings {
     pub app_host: String,
     pub auth_url: String,
     pub max_clients_number: i64,
+    pub amqp: AmqpSettings
 }
 
 #[derive(Debug, serde::Deserialize)]
@@ -18,6 +19,13 @@ pub struct DatabaseSettings {
     pub database_name: String,
 }
 
+#[derive(Debug, serde::Deserialize)]
+pub struct AmqpSettings {
+    pub username: String,
+    pub password: String,
+    pub host: String,
+    pub port: u16,
+}
 impl DatabaseSettings {
     // Connection string: postgresql://<username>:<password>@<host>:<port>/<database_name>
     pub fn connection_string(&self) -> String {
@@ -35,6 +43,15 @@ impl DatabaseSettings {
     }
 }
 
+impl AmqpSettings {
+    pub fn connection_string(&self) -> String {
+        format!(
+            "amqp://{}:{}@{}:{}/%2f",
+            self.username, self.password, self.host, self.port,
+        )
+    }
+}
+
 pub fn get_configuration() -> Result<Settings, config::ConfigError> {
     // Initialize our configuration reader
     let mut settings = config::Config::default();
diff --git a/src/forms/stack.rs b/src/forms/stack.rs
index 93a1be85..ce9105e8 100644
--- a/src/forms/stack.rs
+++ b/src/forms/stack.rs
@@ -3,8 +3,42 @@ use serde_json::Value;
 use serde_valid::Validate;
 
 #[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize, Validate)]
-#[serde(rename_all = "camelCase")]
+#[serde(rename_all = "snake_case")]
 pub struct StackForm {
+    #[serde(rename= "commonDomain")]
+    pub common_domain: String,
+    pub domain_list: Option<DomainList>,
+    pub region: String,
+    pub zone: Option<String>,
+    pub server: String,
+    pub os: String,
+    pub ssl: String,
+    pub vars: Option<Vec<Var>>,
+    #[serde(rename = "integrated_features")]
+    pub integrated_features: Option<Vec<Value>>,
+    #[serde(rename = "extended_features")]
+    pub extended_features: Option<Vec<Value>>,
+    pub subscriptions: Option<Vec<String>>,
+    #[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, Validate)]
+#[serde(rename_all = "snake_case")]
+pub struct StackPayload {
+    pub(crate) user_token: Option<String>,
+    pub(crate) user_email: Option<String>,
+    pub(crate) installation_id: Option<u32>,
+    #[serde(rename= "commonDomain")]
     pub common_domain: String,
     pub domain_list: Option<DomainList>,
     pub region: String,
diff --git a/src/helpers/json.rs b/src/helpers/json.rs
index ad183050..de5dfd9f 100644
--- a/src/helpers/json.rs
+++ b/src/helpers/json.rs
@@ -1,8 +1,8 @@
-use actix_web::{HttpRequest, HttpResponse, Responder};
+use actix_web::{Responder, Result};
 use serde_derive::Serialize;
-use crate::models;
+use actix_web::web;
 
-#[derive(Serialize, Default)]
+#[derive(Serialize)]
 pub(crate) struct JsonResponse<T> {
     pub(crate) status: String,
     pub(crate) message: String,
@@ -23,7 +23,67 @@ pub(crate) struct JsonResponse<T> {
 //     pub(crate) list: Option<Vec<T>>
 // }
 
-impl<T> JsonResponse<T> {
+#[derive(Serialize, Default)]
+pub struct JsonResponseBuilder<T>
+    where T: serde::Serialize + Default
+{
+    status: String,
+    message: String,
+    code: u32,
+    id: Option<i32>,
+    item: Option<T>,
+    list: Option<Vec<T>>
+}
+
+impl<T> JsonResponseBuilder<T>
+where T: serde::Serialize + Default
+{
+   fn new() -> Self {
+       Self::default()
+   }
+
+    fn set_item(mut self, item:T) -> Self {
+        self.item = Some(item);
+        self
+    }
+
+
+    fn set_list(mut self, list:Vec<T>) -> Self {
+        self.list = Some(list);
+        self
+    }
+
+    pub(crate) fn ok(self) -> Result<impl Responder>  {
+
+        Ok(web::Json(
+            JsonResponse {
+                status: self.status,
+                message: self.message,
+                code: self.code,
+                id: self.id,
+                item: self.item,
+                list: self.list,
+            }
+        ))
+    }
+}
+
+impl<T> From<T> for JsonResponseBuilder<T>
+    where T: serde::Serialize + Default {
+    fn from(value: T) -> Self {
+        JsonResponseBuilder::default().set_item(value)
+    }
+}
+
+impl<T> From<Vec<T>> for JsonResponseBuilder<T>
+where T: serde::Serialize + Default {
+    fn from(value: Vec<T>) -> Self {
+        JsonResponseBuilder::default().set_list(value)
+    }
+}
+
+impl<T> JsonResponse<T>
+{
     pub(crate) fn new(status: String,
                       message: String,
                       code: u32,
@@ -55,8 +115,7 @@ impl<T> JsonResponse<T> {
             message: msg,
             code: 200,
             id: Some(id),
-            item: None,
-            list: None
+            ..Default::default()
         }
     }
 
@@ -65,9 +124,7 @@ impl<T> JsonResponse<T> {
             status: "Error".to_string(),
             code: 404,
             message: format!("Object not found"),
-            id: None,
-            item: None,
-            list: None
+            ..Default::default()
         }
     }
 
@@ -83,9 +140,7 @@ impl<T> JsonResponse<T> {
             status: "Error".to_string(),
             code: 500,
             message: msg,
-            id: None,
-            item: None,
-            list: None
+            ..Default::default()
         }
     }
 
@@ -101,9 +156,19 @@ impl<T> JsonResponse<T> {
             status: "Error".to_string(),
             code: 400,
             message: msg,
-            id: None,
-            item: None,
-            list: None
+            ..Default::default()
+        }
+    }
+}
+
+impl<T> Default for JsonResponse<T> {
+
+    fn default() -> Self {
+        JsonResponse {
+
+            status: "200".to_string(),
+            message: "OK".to_string(),
+            ..Default::default()
         }
     }
 }
diff --git a/src/helpers/stack/builder.rs b/src/helpers/stack/builder.rs
index 1ee5fe9b..a6c9fd99 100644
--- a/src/helpers/stack/builder.rs
+++ b/src/helpers/stack/builder.rs
@@ -30,7 +30,7 @@ impl Default for Config {
 #[derive(Clone, Debug)]
 pub struct DcBuilder {
     config: Config,
-    stack: Stack
+    pub(crate) stack: Stack
 }
 
 impl TryInto<Vec<Port>> for Web {
@@ -88,10 +88,10 @@ impl DcBuilder {
                 println!("stack item {:?}", apps.custom.web);
 
                 for app in apps.custom.web {
-                    // println!("app name {:?}", app.name);
                     let tag = "latest";
                     let img= format!("{}/{}:{}",app.dockerhub_user, app.dockerhub_name, tag);
                     let code = app.code.clone().to_owned();
+
                     let mut service = Service {
                         image: Some(img.to_string()),
                         ..Default::default()
diff --git a/src/models/stack.rs b/src/models/stack.rs
index 9ce7370e..f9ce272e 100644
--- a/src/models/stack.rs
+++ b/src/models/stack.rs
@@ -14,3 +14,13 @@ pub struct Stack {
     pub created_at: DateTime<Utc>,
     pub updated_at: DateTime<Utc>,
 }
+
+impl Default for Stack {
+    fn default() -> Self {
+        Stack {
+            user_id: "".to_string(),
+            name: "".to_string(),
+                ..Default::default()
+        }
+    }
+}
\ No newline at end of file
diff --git a/src/routes/stack/add.rs b/src/routes/stack/add.rs
index ef3e0fcb..64c65cb6 100644
--- a/src/routes/stack/add.rs
+++ b/src/routes/stack/add.rs
@@ -15,8 +15,7 @@ use sqlx::PgPool;
 use std::str;
 use tracing::Instrument;
 use uuid::Uuid;
-use crate::helpers::stack::builder::DcBuilder;
-use crate::helpers::stack::dctypes::Compose;
+
 
 #[tracing::instrument(name = "Add stack.")]
 #[post("")]
@@ -28,14 +27,12 @@ pub async fn add(
 
     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::<AppState>(body_str).unwrap();
     let form = match serde_json::from_str::<StackForm>(body_str) {
         Ok(f) => {
             f
         }
-        Err(err) => {
-            return Ok(Json(JsonResponse::<Stack>::not_valid("")));
+        Err(_err) => {
+            return Ok(Json(JsonResponse::<Stack>::not_valid("Invalid data")));
         }
     };
 
@@ -111,59 +108,3 @@ pub async fn add(
     };
 }
 
-#[tracing::instrument(name = "Generate docker-compose.")]
-#[post("/{id}")]
-pub async fn gen(
-    user: web::ReqData<User>,
-    path: web::Path<(i32,)>,
-    pool: Data<PgPool>,
-) -> Result<impl Responder> {
-    let id = path.0;
-    tracing::debug!("Received id: {}", id);
-
-    let stack = match sqlx::query_as!(
-        Stack,
-        r#"
-        SELECT * FROM user_stack WHERE id=$1 AND user_id=$2 LIMIT 1
-        "#,
-        id, user.id
-    )
-            .fetch_one(pool.get_ref())
-            .await
-        {
-            Ok(stack) => {
-                tracing::info!("stack found: {:?}", stack.id,);
-                Some(stack)
-            }
-            Err(sqlx::Error::RowNotFound) => {
-                tracing::error!("Row not found 404");
-                None
-            }
-            Err(e) => {
-                tracing::error!("Failed to fetch stack, error: {:?}", e);
-                None
-            }
-        };
-
-
-    match stack {
-        Some(stack) => {
-            let id = stack.id.clone();
-            let mut dc = DcBuilder::new(stack);
-            let fc = dc.build();
-            // tracing::debug!("Docker compose file content {:?}", fc.unwrap());
-            return Ok(Json(JsonResponse::new(
-                "OK".to_owned(),
-                "Success".to_owned(),
-                200,
-                Some(id),
-                Some(fc.unwrap()),
-                None
-            )));
-
-        }
-        None => {
-            return Ok(Json(JsonResponse::internal_error("Could not generate compose file")));
-        }
-    }
-}
diff --git a/src/routes/stack/compose.rs b/src/routes/stack/compose.rs
new file mode 100644
index 00000000..dc3230d4
--- /dev/null
+++ b/src/routes/stack/compose.rs
@@ -0,0 +1,71 @@
+use actix_web::{
+    web,
+    web::{Data, Json},
+    Responder, Result,
+};
+
+use crate::helpers::JsonResponse;
+use crate::models::user::User;
+use crate::models::Stack;
+use actix_web::post;
+use sqlx::PgPool;
+use std::str;
+use tracing::Instrument;
+use crate::helpers::stack::builder::DcBuilder;
+
+#[tracing::instrument(name = "Generate docker-compose.")]
+#[post("/{id}")]
+pub async fn add(
+    user: web::ReqData<User>,
+    path: web::Path<(i32,)>,
+    pool: Data<PgPool>,
+) -> Result<impl Responder> {
+    let id = path.0;
+    tracing::debug!("Received id: {}", id);
+
+    let stack = match sqlx::query_as!(
+        Stack,
+        r#"
+        SELECT * FROM user_stack WHERE id=$1 AND user_id=$2 LIMIT 1
+        "#,
+        id, user.id
+    )
+        .fetch_one(pool.get_ref())
+        .await
+    {
+        Ok(stack) => {
+            tracing::info!("stack found: {:?}", stack.id,);
+            Some(stack)
+        }
+        Err(sqlx::Error::RowNotFound) => {
+            tracing::error!("Row not found 404");
+            None
+        }
+        Err(e) => {
+            tracing::error!("Failed to fetch stack, error: {:?}", e);
+            None
+        }
+    };
+
+
+    match stack {
+        Some(stack) => {
+            let id = stack.id.clone();
+            let mut dc = DcBuilder::new(stack);
+            let fc = dc.build();
+            // tracing::debug!("Docker compose file content {:?}", fc.unwrap());
+            return Ok(Json(JsonResponse::new(
+                "OK".to_owned(),
+                "Success".to_owned(),
+                200,
+                Some(id),
+                Some(fc.unwrap()),
+                None
+            )));
+
+        }
+        None => {
+            return Ok(Json(JsonResponse::internal_error("Could not generate compose file")));
+        }
+    }
+}
diff --git a/src/routes/stack/deploy.rs b/src/routes/stack/deploy.rs
index 67edaa81..1c871e5c 100644
--- a/src/routes/stack/deploy.rs
+++ b/src/routes/stack/deploy.rs
@@ -1,5 +1,121 @@
-use actix_web::HttpResponse;
+use std::sync::Arc;
+use actix_web::{
+    web,
+    post,
+    web::{Data, Json},
+    Responder, Result,
+};
+use crate::models::user::User;
+use crate::models::stack::Stack;
+use sqlx::PgPool;
+use lapin::{
+    options::*, publisher_confirm::Confirmation, types::FieldTable, BasicProperties, Connection,
+    ConnectionProperties
+};
+use crate::configuration::Settings;
+use crate::helpers::JsonResponse;
+use crate::helpers::stack::builder::DcBuilder;
+use futures_lite::stream::StreamExt;
+use serde::Serialize;
+use crate::forms::{StackForm, StackPayload};
 
-pub async fn deploy() -> HttpResponse {
-    unimplemented!()
-}
\ No newline at end of file
+
+#[derive(Serialize, Debug, Clone)]
+struct Payload {
+    user_token: String,
+    user_email: String,
+    installation_id: String,
+}
+
+
+#[tracing::instrument(name = "Deploy.")]
+#[post("/{id}/deploy")]
+pub async fn add(
+    user: web::ReqData<User>,
+    path: web::Path<(i32,)>,
+    pool: Data<PgPool>,
+    sets: Data<Arc<Settings>>,
+) -> Result<impl Responder> {
+    let id = path.0;
+    tracing::debug!("Received id: {}", id);
+
+    let stack = match sqlx::query_as!(
+        Stack,
+        r#"
+        SELECT * FROM user_stack WHERE id=$1 AND user_id=$2 LIMIT 1
+        "#,
+        id, user.id
+    )
+        .fetch_one(pool.get_ref())
+        .await
+    {
+        Ok(stack) => {
+            tracing::info!("Stack found: {:?}", stack.id,);
+            Some(stack)
+        }
+        Err(sqlx::Error::RowNotFound) => {
+            tracing::error!("Row not found 404");
+            None
+        }
+        Err(e) => {
+            tracing::error!("Failed to fetch stack, error: {:?}", e);
+            None
+        }
+    };
+
+    return match stack {
+        Some(stack) => {
+            let id = stack.id.clone();
+            let mut dc = DcBuilder::new(stack);
+            dc.build();
+
+            let addr = sets.amqp.connection_string();
+            let routing_key = "install.start.tfa.all.all".to_string();
+            tracing::debug!("Sending message to {:?}", routing_key);
+
+            let conn = Connection::connect(&addr, ConnectionProperties::default())
+                .await
+                .unwrap();
+
+            tracing::info!("RABBITMQ CONNECTED");
+
+            let channel = conn.create_channel().await.unwrap();
+            let mut stack_data = serde_json::from_value::<StackPayload>(
+                dc.stack.body.clone()
+            ).unwrap();
+
+            stack_data.installation_id = Some(1);
+            stack_data.user_token = Some(user.id.clone());
+            stack_data.user_email= Some(user.email.clone());
+
+            let payload = serde_json::to_string::<StackPayload>(&stack_data).unwrap();
+            let _payload = payload.as_bytes();
+
+            let confirm = channel
+                .basic_publish(
+                    "install",
+                    routing_key.as_str(),
+                    BasicPublishOptions::default(),
+                    _payload,
+                    BasicProperties::default(),
+                )
+                .await.unwrap()
+                .await.unwrap();
+
+            assert_eq!(confirm, Confirmation::NotRequested);
+            tracing::debug!("Message sent to rabbitmq");
+
+            Ok(Json(JsonResponse::<Stack>::new(
+                "OK".to_owned(),
+                "Success".to_owned(),
+                200,
+                Some(id),
+                None,
+                None
+            )))
+        }
+        None => {
+            Ok(Json(JsonResponse::internal_error("Deployment failed")))
+        }
+    }
+}
diff --git a/src/routes/stack/get.rs b/src/routes/stack/get.rs
index b3ce3898..786d905f 100644
--- a/src/routes/stack/get.rs
+++ b/src/routes/stack/get.rs
@@ -1,8 +1,9 @@
 use actix_web::{web, get, Responder, Result};
 use sqlx::PgPool;
-use crate::helpers::JsonResponse;
+use crate::helpers::{JsonResponse, JsonResponseBuilder};
 use crate::models;
 use crate::models::user::User;
+use std::convert::From;
 
 
 #[tracing::instrument(name = "Get logged user stack.")]
@@ -29,20 +30,27 @@ pub async fn get(
     {
         Ok(stack) => {
             tracing::info!("stack found: {:?}", stack.id,);
-            return Ok(web::Json(JsonResponse::<models::Stack>::new(
-                "Success".to_string(),
-                "".to_string(),
-                200,
-                Some(stack.id),
-                Some(stack),
-                None)));
+            // return Ok(web::Json(JsonResponse::<models::Stack>::new(
+            //     "Success".to_string(),
+            //     "".to_string(),
+            //     200,
+            //     Some(stack.id),
+            //     Some(stack),
+            //     None)));
+            let response_builder:JsonResponseBuilder<models::Stack> = From::from(stack);
+            // let response: JsonResponse<models::Stack> = From::<models::Stack>::from(stack).build();
+            return response_builder.ok();
         }
         Err(sqlx::Error::RowNotFound) => {
-            return Ok(web::Json(JsonResponse::<models::Stack>::not_found()));
+            // return Ok(web::Json(JsonResponse::<models::Stack>::not_found()));
+            let response_builder:JsonResponseBuilder<models::Stack> =JsonResponseBuilder::default();
+            return response_builder.ok();
         }
         Err(e) => {
             tracing::error!("Failed to fetch stack, error: {:?}", e);
-            return Ok(web::Json(JsonResponse::<models::Stack>::internal_error("")));
+            // return Ok(web::Json(JsonResponse::<models::Stack>::internal_error("")));
+            let response_builder:JsonResponseBuilder<models::Stack> = JsonResponseBuilder::default();
+            return response_builder.ok();
         }
     }
 }
diff --git a/src/routes/stack/mod.rs b/src/routes/stack/mod.rs
index f3e5bc95..27c80617 100644
--- a/src/routes/stack/mod.rs
+++ b/src/routes/stack/mod.rs
@@ -2,6 +2,8 @@ pub mod add;
 pub mod deploy;
 pub mod get;
 pub mod update;
+pub(crate) mod compose;
+
 pub use add::*;
 pub use update::*;
 pub use deploy::*;
diff --git a/src/startup.rs b/src/startup.rs
index 773ce2c0..b7fd3243 100644
--- a/src/startup.rs
+++ b/src/startup.rs
@@ -127,8 +127,9 @@ pub async fn run(
                 web::scope("/stack")
                     .wrap(HttpAuthentication::bearer(bearer_guard))
                     .wrap(Cors::permissive())
+                    .service(crate::routes::stack::deploy::add)
                     .service(crate::routes::stack::add::add)
-                    .service(crate::routes::stack::add::gen)
+                    .service(crate::routes::stack::compose::add)
                     .service(crate::routes::stack::get::get)
             )
             .app_data(db_pool.clone())

From d5be426847f7299c38ad9611b29f0b003cc231d9 Mon Sep 17 00:00:00 2001
From: vsilent <jabberroid@gmail.com>
Date: Sun, 12 Nov 2023 11:25:51 +0200
Subject: [PATCH 07/18] rabbitmq config

---
 configuration.yaml | 4 ++--
 1 file changed, 2 insertions(+), 2 deletions(-)

diff --git a/configuration.yaml b/configuration.yaml
index 4f88a4cb..9498400b 100644
--- a/configuration.yaml
+++ b/configuration.yaml
@@ -11,8 +11,8 @@ database:
   database_name: stacker
 
 amqp:
-  host: 51.15.74.139
+  host: 127.0.0.1
   port: 5672
   username: guest
-  password: wua4Eeyi
+  password:
 

From 49068d0974de880641961031160b3ca691954f1d Mon Sep 17 00:00:00 2001
From: vsilent <jabberroid@gmail.com>
Date: Mon, 13 Nov 2023 18:31:16 +0200
Subject: [PATCH 08/18] admin endpoints

---
 configuration.yaml            |   6 +-
 custom-stack-payload-4.json   |   2 +-
 custom-stack-payload-5.json   |   1 +
 docker-compose.yml            |   2 +-
 docker/dev/docker-compose.yml |   2 +-
 src/forms/stack.rs            |   7 ++-
 src/helpers/json.rs           | 110 +++++++++-------------------------
 src/routes/stack/compose.rs   |  57 ++++++++++++++++++
 src/routes/stack/deploy.rs    |   8 +--
 src/routes/stack/get.rs       |  49 ++++++++++++++-
 src/startup.rs                |   4 +-
 11 files changed, 153 insertions(+), 95 deletions(-)
 create mode 100644 custom-stack-payload-5.json

diff --git a/configuration.yaml b/configuration.yaml
index 9498400b..5a111da5 100644
--- a/configuration.yaml
+++ b/configuration.yaml
@@ -1,6 +1,5 @@
 app_host: 127.0.0.1
 app_port: 8000
-#auth_url: https://65190108818c4e98ac6000e4.mockapi.io/user/1
 auth_url: https://dev.try.direct/server/user/oauth_server/api/me
 max_clients_number: 2
 database:
@@ -11,8 +10,7 @@ database:
   database_name: stacker
 
 amqp:
-  host: 127.0.0.1
+  host: 51.15.74.139
   port: 5672
   username: guest
-  password:
-
+  password: guest
diff --git a/custom-stack-payload-4.json b/custom-stack-payload-4.json
index 07ee1569..6f4bc7c4 100644
--- a/custom-stack-payload-4.json
+++ b/custom-stack-payload-4.json
@@ -1 +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":"<p class=\"content-editor__unstyled\"><strong style=\"font-weight:bold\">Portainer is a lightweight management UI which allows you to easily manage your different Docker environments (Docker hosts or Swarm clusters)</strong></p>","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":"<p class=\"content-editor__unstyled\">PostgREST description</p>","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":"mysampleproject2","custom_stack_code":"another-bot2","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"}}
+{"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":"<p class=\"content-editor__unstyled\"><strong style=\"font-weight:bold\">Portainer is a lightweight management UI which allows you to easily manage your different Docker environments (Docker hosts or Swarm clusters)</strong></p>","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":"<p class=\"content-editor__unstyled\">PostgREST description</p>","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":"mysampleproject4","custom_stack_code":"another-bot4","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/custom-stack-payload-5.json b/custom-stack-payload-5.json
new file mode 100644
index 00000000..a8691dbf
--- /dev/null
+++ b/custom-stack-payload-5.json
@@ -0,0 +1 @@
+{"commonDomain":"test.app", "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":"<p class=\"content-editor__unstyled\"><strong style=\"font-weight:bold\">Portainer is a lightweight management UI which allows you to easily manage your different Docker environments (Docker hosts or Swarm clusters)</strong></p>","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":"<p class=\"content-editor__unstyled\">PostgREST description</p>","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":"mysampleproject5","custom_stack_code":"another-bot5","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/docker-compose.yml b/docker-compose.yml
index 2d3b934d..26311c4c 100644
--- a/docker-compose.yml
+++ b/docker-compose.yml
@@ -13,7 +13,7 @@ networks:
 services:
 
   stacker:
-    image: trydirect/stacker:0.0.3
+    image: trydirect/stacker:0.0.4
     build: .
     container_name: stacker
     restart: always
diff --git a/docker/dev/docker-compose.yml b/docker/dev/docker-compose.yml
index 1ba68f2c..bdfdd892 100644
--- a/docker/dev/docker-compose.yml
+++ b/docker/dev/docker-compose.yml
@@ -7,7 +7,7 @@ volumes:
 services:
 
   stacker:
-    image: trydirect/stacker:0.0.3
+    image: trydirect/stacker:0.0.4
     build: .
     container_name: stacker
     restart: always
diff --git a/src/forms/stack.rs b/src/forms/stack.rs
index ce9105e8..0ee3ec63 100644
--- a/src/forms/stack.rs
+++ b/src/forms/stack.rs
@@ -88,7 +88,7 @@ pub struct Custom {
     #[serde(rename = "servers_count")]
     pub servers_count: i64,
     #[serde(rename = "custom_stack_name")]
-    pub custom_stack_name: String,
+    pub custom_stack_name: Option<String>,
     #[serde(rename = "custom_stack_code")]
     pub custom_stack_code: String,
     #[serde(rename = "custom_stack_git_url")]
@@ -114,6 +114,7 @@ pub struct Web {
     pub name: String,
     pub code: String,
     pub domain: Option<String>,
+    #[serde(rename = "shared_ports")]
     pub shared_ports: Option<Vec<String>>,
     pub versions: Option<Vec<Version>>,
     pub custom: bool,
@@ -126,7 +127,9 @@ pub struct Web {
     pub dockerhub_user: String,
     #[serde(rename = "dockerhub_name")]
     pub dockerhub_name: String,
+    #[serde(rename = "url_app")]
     pub url_app: Option<String>,
+    #[serde(rename = "url_git")]
     pub url_git: Option<String>,
     #[validate(min_length=1)]
     #[validate(max_length=10)]
@@ -196,6 +199,7 @@ pub struct Feature {
     pub dockerhub_image: Option<String>,
     pub versions: Option<Vec<Version>>,
     pub domain: Option<String>,
+    #[serde(rename = "shared_ports")]
     pub shared_ports: Option<Vec<String>>,
     pub main: bool,
 }
@@ -301,6 +305,7 @@ pub struct Service {
     pub dockerhub_image: Option<String>,
     pub versions: Option<Vec<Version>>,
     pub domain: String,
+    #[serde(rename = "shared_ports")]
     pub shared_ports: Option<Vec<String>>,
     pub main: bool,
 }
diff --git a/src/helpers/json.rs b/src/helpers/json.rs
index de5dfd9f..6321a066 100644
--- a/src/helpers/json.rs
+++ b/src/helpers/json.rs
@@ -1,6 +1,7 @@
 use actix_web::{Responder, Result};
 use serde_derive::Serialize;
 use actix_web::web;
+use actix_web::web::JsonBody::Error;
 
 #[derive(Serialize)]
 pub(crate) struct JsonResponse<T> {
@@ -12,16 +13,6 @@ pub(crate) struct JsonResponse<T> {
     pub(crate) list: Option<Vec<T>>
 }
 
-//
-// #[derive(Serialize)]
-// pub(crate) struct JsonErrorResponse<T> {
-//     pub(crate) status: String,
-//     pub(crate) message: String,
-//     pub(crate) code: u32,
-//     pub(crate) id: Option<i32>,
-//     pub(crate) item: Option<T>,
-//     pub(crate) list: Option<Vec<T>>
-// }
 
 #[derive(Serialize, Default)]
 pub struct JsonResponseBuilder<T>
@@ -38,7 +29,7 @@ pub struct JsonResponseBuilder<T>
 impl<T> JsonResponseBuilder<T>
 where T: serde::Serialize + Default
 {
-   fn new() -> Self {
+   pub(crate) fn new() -> Self {
        Self::default()
    }
 
@@ -48,7 +39,7 @@ where T: serde::Serialize + Default
     }
 
 
-    fn set_list(mut self, list:Vec<T>) -> Self {
+    pub(crate) fn set_list(mut self, list:Vec<T>) -> Self {
         self.list = Some(list);
         self
     }
@@ -66,6 +57,20 @@ where T: serde::Serialize + Default
             }
         ))
     }
+
+    pub(crate) fn err(self) -> Result<impl Responder>  {
+
+        Ok(web::Json(
+            JsonResponse {
+                status: self.status,
+                message: self.message,
+                code: self.code,
+                id: self.id,
+                item: self.item,
+                list: self.list,
+            }
+        ))
+    }
 }
 
 impl<T> From<T> for JsonResponseBuilder<T>
@@ -115,7 +120,8 @@ impl<T> JsonResponse<T>
             message: msg,
             code: 200,
             id: Some(id),
-            ..Default::default()
+            item: None,
+            list: None,
         }
     }
 
@@ -123,8 +129,10 @@ impl<T> JsonResponse<T>
         JsonResponse {
             status: "Error".to_string(),
             code: 404,
+            id: None,
+            item: None,
             message: format!("Object not found"),
-            ..Default::default()
+            list: None,
         }
     }
 
@@ -139,8 +147,10 @@ impl<T> JsonResponse<T>
         JsonResponse {
             status: "Error".to_string(),
             code: 500,
+            id: None,
+            item: None,
             message: msg,
-            ..Default::default()
+            list: None,
         }
     }
 
@@ -155,72 +165,10 @@ impl<T> JsonResponse<T>
         JsonResponse {
             status: "Error".to_string(),
             code: 400,
+            id: None,
+            item: None,
             message: msg,
-            ..Default::default()
+            list: None,
         }
     }
-}
-
-impl<T> Default for JsonResponse<T> {
-
-    fn default() -> Self {
-        JsonResponse {
-
-            status: "200".to_string(),
-            message: "OK".to_string(),
-            ..Default::default()
-        }
-    }
-}
-
-// // Implement the Responder trait for GlobalResponse
-// impl Responder for JsonResponse<T> where T: {
-//
-//     type Body = ();
-//
-//     fn respond_to(self, _req: &HttpRequest) -> HttpResponse {
-//         HttpResponse::Ok().json(self)
-//     }
-// }
-//
-// impl<T> JsonErrorResponse<T> {
-//     pub(crate) fn new(status: String,
-//                       message: String,
-//                       code: u32,
-//                       id: Option<i32>,
-//                       item:Option<T>,
-//                       list: Option<Vec<T>>) -> Self {
-//         JsonErrorResponse {
-//             status,
-//             message,
-//             code,
-//             id,
-//             item,
-//             list,
-//         }
-//     }
-//
-//     pub(crate) fn default() -> Self {
-//         JsonErrorResponse {
-//             status: "Internal Error".to_string(),
-//             message: "Internal Error".to_string(),
-//             code: 500,
-//             id: None,
-//             item: None,
-//             list: None,
-//         }
-//     }
-//
-//     pub(crate) fn not_found() -> Self {
-//         JsonErrorResponse {
-//             status: "Error".to_string(),
-//             code: 404,
-//             message: format!("Object not found"),
-//             id: None,
-//             item: None,
-//             list: None
-//         }
-//     }
-//
-//
-// }
+}
\ No newline at end of file
diff --git a/src/routes/stack/compose.rs b/src/routes/stack/compose.rs
index dc3230d4..c7e7523a 100644
--- a/src/routes/stack/compose.rs
+++ b/src/routes/stack/compose.rs
@@ -11,6 +11,7 @@ use actix_web::post;
 use sqlx::PgPool;
 use std::str;
 use tracing::Instrument;
+use uuid::Uuid;
 use crate::helpers::stack::builder::DcBuilder;
 
 #[tracing::instrument(name = "Generate docker-compose.")]
@@ -47,6 +48,62 @@ pub async fn add(
         }
     };
 
+    match stack {
+        Some(stack) => {
+            let id = stack.id.clone();
+            let mut dc = DcBuilder::new(stack);
+            let fc = dc.build();
+            // tracing::debug!("Docker compose file content {:?}", fc.unwrap());
+            return Ok(Json(JsonResponse::new(
+                "OK".to_owned(),
+                "Success".to_owned(),
+                200,
+                Some(id),
+                Some(fc.unwrap()),
+                None
+            )));
+
+        }
+        None => {
+            return Ok(Json(JsonResponse::internal_error("Could not generate compose file")));
+        }
+    }
+}
+
+#[tracing::instrument(name = "Generate docker-compose.")]
+#[post("/{id}/compose")]
+pub async fn admin(
+    user: web::ReqData<User>,
+    path: web::Path<(i32,)>,
+    pool: Data<PgPool>,
+) -> Result<impl Responder> {
+    ///  Admin function for generating compose file for specified user
+    let id = path.0;
+    tracing::debug!("Received id: {}", id);
+
+    let stack = match sqlx::query_as!(
+        Stack,
+        r#"
+        SELECT * FROM user_stack WHERE id=$1 LIMIT 1
+        "#,
+        id,
+    )
+        .fetch_one(pool.get_ref())
+        .await
+    {
+        Ok(stack) => {
+            tracing::info!("stack found: {:?}", stack.id,);
+            Some(stack)
+        }
+        Err(sqlx::Error::RowNotFound) => {
+            tracing::error!("Row not found 404");
+            None
+        }
+        Err(e) => {
+            tracing::error!("Failed to fetch stack, error: {:?}", e);
+            None
+        }
+    };
 
     match stack {
         Some(stack) => {
diff --git a/src/routes/stack/deploy.rs b/src/routes/stack/deploy.rs
index 1c871e5c..27c30149 100644
--- a/src/routes/stack/deploy.rs
+++ b/src/routes/stack/deploy.rs
@@ -28,7 +28,7 @@ struct Payload {
 }
 
 
-#[tracing::instrument(name = "Deploy.")]
+#[tracing::instrument(name = "Deploy for every user. Admin endpoint")]
 #[post("/{id}/deploy")]
 pub async fn add(
     user: web::ReqData<User>,
@@ -42,9 +42,9 @@ pub async fn add(
     let stack = match sqlx::query_as!(
         Stack,
         r#"
-        SELECT * FROM user_stack WHERE id=$1 AND user_id=$2 LIMIT 1
+        SELECT * FROM user_stack WHERE id=$1 LIMIT 1
         "#,
-        id, user.id
+        id
     )
         .fetch_one(pool.get_ref())
         .await
@@ -75,7 +75,7 @@ pub async fn add(
 
             let conn = Connection::connect(&addr, ConnectionProperties::default())
                 .await
-                .unwrap();
+                .expect("Could not connect RabbitMQ");
 
             tracing::info!("RABBITMQ CONNECTED");
 
diff --git a/src/routes/stack/get.rs b/src/routes/stack/get.rs
index 786d905f..24230b97 100644
--- a/src/routes/stack/get.rs
+++ b/src/routes/stack/get.rs
@@ -4,11 +4,12 @@ use crate::helpers::{JsonResponse, JsonResponseBuilder};
 use crate::models;
 use crate::models::user::User;
 use std::convert::From;
+use tracing::Instrument;
 
 
 #[tracing::instrument(name = "Get logged user stack.")]
 #[get("/{id}")]
-pub async fn get(
+pub async fn item(
     user: web::ReqData<User>,
     path: web::Path<(i32,)>,
     pool: web::Data<PgPool>,
@@ -55,3 +56,49 @@ pub async fn get(
     }
 }
 
+#[tracing::instrument(name = "Get user's stack list.")]
+#[get("/user/{id}")]
+pub async fn list(
+    user: web::ReqData<User>,
+    path: web::Path<(String,)>,
+    pool: web::Data<PgPool>,
+) -> Result<impl Responder> {
+
+    /// This is admin endpoint, used by a m2m app, client app is confidential
+    /// it should return stacks by user id
+    /// in order to pass validation at external deployment service
+
+    let (id,) = path.into_inner();
+    tracing::info!("Logged user: {:?}", user.id);
+    tracing::info!("Get stack list for user {:?}", id);
+
+    let query_span = tracing::info_span!("Get stacks by user id.");
+
+    match sqlx::query_as!(
+        models::Stack,
+        r#"
+        SELECT * FROM user_stack WHERE user_id=$1
+        "#,
+        id
+    )
+        .fetch_all(pool.get_ref())
+        .instrument(query_span)
+        .await
+    {
+        Ok(list) => {
+            let response_builder:JsonResponseBuilder<models::Stack> = JsonResponseBuilder::new();
+            return response_builder.set_list(list).ok();
+        }
+        Err(sqlx::Error::RowNotFound) => {
+            tracing::error!("No stacks found for user: {:?}", &user.id);
+            let response_builder:JsonResponseBuilder<models::Stack> = JsonResponseBuilder::default();
+            return response_builder.ok();
+        }
+        Err(e) => {
+            tracing::error!("Failed to fetch stack, error: {:?}", e);
+            let response_builder:JsonResponseBuilder<models::Stack> = JsonResponseBuilder::default();
+            return response_builder.ok();
+        }
+    }
+}
+
diff --git a/src/startup.rs b/src/startup.rs
index b7fd3243..c295136b 100644
--- a/src/startup.rs
+++ b/src/startup.rs
@@ -130,7 +130,9 @@ pub async fn run(
                     .service(crate::routes::stack::deploy::add)
                     .service(crate::routes::stack::add::add)
                     .service(crate::routes::stack::compose::add)
-                    .service(crate::routes::stack::get::get)
+                    .service(crate::routes::stack::compose::admin)
+                    .service(crate::routes::stack::get::item)
+                    .service(crate::routes::stack::get::list)
             )
             .app_data(db_pool.clone())
             .app_data(settings.clone())

From 5b6cf49973accb398b3f35084206f17f6d8dc116 Mon Sep 17 00:00:00 2001
From: vsilent <jabberroid@gmail.com>
Date: Mon, 13 Nov 2023 19:51:24 +0200
Subject: [PATCH 09/18] new form struct, new fields

---
 src/forms/stack.rs          | 18 +++++++++---------
 src/routes/stack/add.rs     |  7 ++++---
 src/routes/stack/compose.rs |  4 ++--
 3 files changed, 15 insertions(+), 14 deletions(-)

diff --git a/src/forms/stack.rs b/src/forms/stack.rs
index 0ee3ec63..c49ae97d 100644
--- a/src/forms/stack.rs
+++ b/src/forms/stack.rs
@@ -6,8 +6,9 @@ use serde_valid::Validate;
 #[serde(rename_all = "snake_case")]
 pub struct StackForm {
     #[serde(rename= "commonDomain")]
-    pub common_domain: String,
+    pub common_domain: Option<String>,
     pub domain_list: Option<DomainList>,
+    pub stack_code: Option<String>,
     pub region: String,
     pub zone: Option<String>,
     pub server: String,
@@ -19,13 +20,13 @@ pub struct StackForm {
     #[serde(rename = "extended_features")]
     pub extended_features: Option<Vec<Value>>,
     pub subscriptions: Option<Vec<String>>,
+    pub form_app: Option<Vec<String>>,
+    pub disk_type: Option<String>,
     #[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,
@@ -52,6 +53,8 @@ pub struct StackPayload {
     #[serde(rename = "extended_features")]
     pub extended_features: Option<Vec<Value>>,
     pub subscriptions: Option<Vec<String>>,
+    pub form_app: Option<Vec<String>>,
+    pub disk_type: Option<String>,
     #[serde(rename = "save_token")]
     pub save_token: bool,
     #[serde(rename = "cloud_token")]
@@ -87,12 +90,10 @@ pub struct Custom {
     pub service: Option<Vec<Service>>,
     #[serde(rename = "servers_count")]
     pub servers_count: i64,
-    #[serde(rename = "custom_stack_name")]
-    pub custom_stack_name: Option<String>,
     #[serde(rename = "custom_stack_code")]
     pub custom_stack_code: String,
-    #[serde(rename = "custom_stack_git_url")]
-    pub custom_stack_git_url: Option<String>,
+    #[serde(rename = "project_git_url")]
+    pub project_git_url: Option<String>,
     #[serde(rename = "custom_stack_category")]
     pub custom_stack_category: Option<Vec<String>>,
     #[serde(rename = "custom_stack_short_description")]
@@ -187,8 +188,7 @@ pub struct Feature {
     pub ansible_var: Option<String>,
     #[serde(rename = "repo_dir")]
     pub repo_dir: Option<String>,
-    #[validate(min_length=1)]
-    pub cpu: String,
+    pub cpu: f64,
     #[validate(min_length=1)]
     #[serde(rename = "ram_size")]
     pub ram_size: String,
diff --git a/src/routes/stack/add.rs b/src/routes/stack/add.rs
index 64c65cb6..9239bc6d 100644
--- a/src/routes/stack/add.rs
+++ b/src/routes/stack/add.rs
@@ -32,7 +32,8 @@ pub async fn add(
             f
         }
         Err(_err) => {
-            return Ok(Json(JsonResponse::<Stack>::not_valid("Invalid data")));
+            let msg = format!("Invalid data. {:?}", _err);
+            return Ok(Json(JsonResponse::<Stack>::not_valid(msg.as_str())));
         }
     };
 
@@ -40,7 +41,7 @@ pub async fn add(
     let request_id = Uuid::new_v4();
     let request_span = tracing::info_span!(
         "Validating a new stack", %request_id,
-        commonDomain=?&form.common_domain,
+        commonDomain=?&form.custom.project_name,
         region=?&form.region,
         domainList=?&form.domain_list
     );
@@ -50,7 +51,7 @@ pub async fn add(
     tracing::info!(
         "request_id {} Adding '{}' '{}' as a new stack",
         request_id,
-        form.common_domain,
+        form.custom.project_name,
         form.region
     );
 
diff --git a/src/routes/stack/compose.rs b/src/routes/stack/compose.rs
index c7e7523a..08106647 100644
--- a/src/routes/stack/compose.rs
+++ b/src/routes/stack/compose.rs
@@ -14,7 +14,7 @@ use tracing::Instrument;
 use uuid::Uuid;
 use crate::helpers::stack::builder::DcBuilder;
 
-#[tracing::instrument(name = "Generate docker-compose.")]
+#[tracing::instrument(name = "User's generate docker-compose.")]
 #[post("/{id}")]
 pub async fn add(
     user: web::ReqData<User>,
@@ -70,7 +70,7 @@ pub async fn add(
     }
 }
 
-#[tracing::instrument(name = "Generate docker-compose.")]
+#[tracing::instrument(name = "Generate docker-compose. Admin")]
 #[post("/{id}/compose")]
 pub async fn admin(
     user: web::ReqData<User>,

From c4a1de203315cb6f063dfe8ccd6f832e7604cda6 Mon Sep 17 00:00:00 2001
From: vsilent <jabberroid@gmail.com>
Date: Tue, 14 Nov 2023 08:56:56 +0200
Subject: [PATCH 10/18] migration fix

---
 migrations/20230905145525_creating_stack_tables.up.sql | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/migrations/20230905145525_creating_stack_tables.up.sql b/migrations/20230905145525_creating_stack_tables.up.sql
index c2d248c2..52ff5313 100644
--- a/migrations/20230905145525_creating_stack_tables.up.sql
+++ b/migrations/20230905145525_creating_stack_tables.up.sql
@@ -8,6 +8,6 @@ CREATE TABLE user_stack (
     body JSON NOT NULL,
     created_at timestamptz NOT NULL,
     updated_at timestamptz NOT NULL,
-        CONSTRAINT user_stack_pkey PRIMARY KEY (id),
+        CONSTRAINT user_stack_pkey PRIMARY KEY (id)
 )
 

From 5e3ee514a56a6d0cad8c5a5ae14b52c4a67b9660 Mon Sep 17 00:00:00 2001
From: vsilent <jabberroid@gmail.com>
Date: Tue, 14 Nov 2023 10:10:10 +0200
Subject: [PATCH 11/18] indexes, cpu optional

---
 .../20230905145525_creating_stack_tables.up.sql       |  8 ++++----
 src/forms/stack.rs                                    | 11 +++++------
 2 files changed, 9 insertions(+), 10 deletions(-)

diff --git a/migrations/20230905145525_creating_stack_tables.up.sql b/migrations/20230905145525_creating_stack_tables.up.sql
index 52ff5313..b20b2cdb 100644
--- a/migrations/20230905145525_creating_stack_tables.up.sql
+++ b/migrations/20230905145525_creating_stack_tables.up.sql
@@ -1,5 +1,3 @@
--- Add up migration script here
--- Add migration script here
 CREATE TABLE user_stack (
     id serial4 NOT NULL,
     stack_id uuid NOT NULL,
@@ -8,6 +6,8 @@ CREATE TABLE user_stack (
     body JSON NOT NULL,
     created_at timestamptz NOT NULL,
     updated_at timestamptz NOT NULL,
-        CONSTRAINT user_stack_pkey PRIMARY KEY (id)
-)
+    CONSTRAINT user_stack_pkey PRIMARY KEY (id)
+);
 
+CREATE INDEX idx_stack_id ON user_stack(stack_id);
+CREATE INDEX idx_stack_user_id ON user_stack(user_id);
\ No newline at end of file
diff --git a/src/forms/stack.rs b/src/forms/stack.rs
index c49ae97d..6fe40ac0 100644
--- a/src/forms/stack.rs
+++ b/src/forms/stack.rs
@@ -143,7 +143,7 @@ pub struct Web {
     #[validate(pattern = r"^\d+G$")]
     pub ram_size: String,
     #[validate(minimum=0.1)]
-    pub cpu: f64,
+    pub cpu: Option<f64>,
 }
 
 #[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize, Validate)]
@@ -188,13 +188,13 @@ pub struct Feature {
     pub ansible_var: Option<String>,
     #[serde(rename = "repo_dir")]
     pub repo_dir: Option<String>,
-    pub cpu: f64,
+    pub cpu: Option<f64>,
     #[validate(min_length=1)]
     #[serde(rename = "ram_size")]
-    pub ram_size: String,
+    pub ram_size: Option<String>,
     #[validate(min_length=1)]
     #[serde(rename = "disk_size")]
-    pub disk_size: String,
+    pub disk_size: Option<String>,
     #[serde(rename = "dockerhub_image")]
     pub dockerhub_image: Option<String>,
     pub versions: Option<Vec<Version>>,
@@ -293,8 +293,7 @@ pub struct Service {
     pub ansible_var: Option<String>,
     #[serde(rename = "repo_dir")]
     pub repo_dir: Option<String>,
-    #[validate(min_length=1)]
-    pub cpu: String,
+    pub cpu: Option<f64>,
     #[serde(rename = "ram_size")]
     #[validate(min_length=1)]
     pub ram_size: String,

From 4dadfa3147f7c6bc7547c8e6721f914b23c69697 Mon Sep 17 00:00:00 2001
From: vsilent <jabberroid@gmail.com>
Date: Wed, 15 Nov 2023 14:17:19 +0200
Subject: [PATCH 12/18] pass stack id

---
 src/forms/stack.rs          |  1 +
 src/routes/stack/compose.rs |  4 ++--
 src/routes/stack/deploy.rs  | 10 ++--------
 3 files changed, 5 insertions(+), 10 deletions(-)

diff --git a/src/forms/stack.rs b/src/forms/stack.rs
index 6fe40ac0..4f3afd93 100644
--- a/src/forms/stack.rs
+++ b/src/forms/stack.rs
@@ -36,6 +36,7 @@ pub struct StackForm {
 #[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize, Validate)]
 #[serde(rename_all = "snake_case")]
 pub struct StackPayload {
+    pub(crate) id: i32,
     pub(crate) user_token: Option<String>,
     pub(crate) user_email: Option<String>,
     pub(crate) installation_id: Option<u32>,
diff --git a/src/routes/stack/compose.rs b/src/routes/stack/compose.rs
index 08106647..555d7dca 100644
--- a/src/routes/stack/compose.rs
+++ b/src/routes/stack/compose.rs
@@ -7,7 +7,7 @@ use actix_web::{
 use crate::helpers::JsonResponse;
 use crate::models::user::User;
 use crate::models::Stack;
-use actix_web::post;
+use actix_web::{get, post};
 use sqlx::PgPool;
 use std::str;
 use tracing::Instrument;
@@ -71,7 +71,7 @@ pub async fn add(
 }
 
 #[tracing::instrument(name = "Generate docker-compose. Admin")]
-#[post("/{id}/compose")]
+#[get("/{id}/compose")]
 pub async fn admin(
     user: web::ReqData<User>,
     path: web::Path<(i32,)>,
diff --git a/src/routes/stack/deploy.rs b/src/routes/stack/deploy.rs
index 27c30149..768da668 100644
--- a/src/routes/stack/deploy.rs
+++ b/src/routes/stack/deploy.rs
@@ -20,13 +20,6 @@ use serde::Serialize;
 use crate::forms::{StackForm, StackPayload};
 
 
-#[derive(Serialize, Debug, Clone)]
-struct Payload {
-    user_token: String,
-    user_email: String,
-    installation_id: String,
-}
-
 
 #[tracing::instrument(name = "Deploy for every user. Admin endpoint")]
 #[post("/{id}/deploy")]
@@ -84,7 +77,8 @@ pub async fn add(
                 dc.stack.body.clone()
             ).unwrap();
 
-            stack_data.installation_id = Some(1);
+            stack_data.id = id;
+            stack_data.installation_id = None;
             stack_data.user_token = Some(user.id.clone());
             stack_data.user_email= Some(user.email.clone());
 

From 942bef36130ea3cf12f36a8356a13b7746d98328 Mon Sep 17 00:00:00 2001
From: vsilent <jabberroid@gmail.com>
Date: Thu, 16 Nov 2023 15:44:10 +0200
Subject: [PATCH 13/18] rename sharedPorts to shared_ports

---
 configuration.yaml          |  2 +-
 custom-stack-payload-5.json |  2 +-
 custom-stack-payload-6.json |  1 +
 src/forms/stack.rs          | 17 +++++++++++++----
 src/routes/stack/deploy.rs  |  5 +++--
 5 files changed, 19 insertions(+), 8 deletions(-)
 create mode 100644 custom-stack-payload-6.json

diff --git a/configuration.yaml b/configuration.yaml
index 5a111da5..af99721b 100644
--- a/configuration.yaml
+++ b/configuration.yaml
@@ -10,7 +10,7 @@ database:
   database_name: stacker
 
 amqp:
-  host: 51.15.74.139
+  host: 127.0.0.1
   port: 5672
   username: guest
   password: guest
diff --git a/custom-stack-payload-5.json b/custom-stack-payload-5.json
index a8691dbf..70bda710 100644
--- a/custom-stack-payload-5.json
+++ b/custom-stack-payload-5.json
@@ -1 +1 @@
-{"commonDomain":"test.app", "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":"<p class=\"content-editor__unstyled\"><strong style=\"font-weight:bold\">Portainer is a lightweight management UI which allows you to easily manage your different Docker environments (Docker hosts or Swarm clusters)</strong></p>","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":"<p class=\"content-editor__unstyled\">PostgREST description</p>","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":"mysampleproject5","custom_stack_code":"another-bot5","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"}}
+{"commonDomain":"test.app", "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":"<p class=\"content-editor__unstyled\"><strong style=\"font-weight:bold\">Portainer is a lightweight management UI which allows you to easily manage your different Docker environments (Docker hosts or Swarm clusters)</strong></p>","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":"<p class=\"content-editor__unstyled\">PostgREST description</p>","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":"mysampleproject5","custom_stack_code":"another-bot5","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/custom-stack-payload-6.json b/custom-stack-payload-6.json
new file mode 100644
index 00000000..97e422a9
--- /dev/null
+++ b/custom-stack-payload-6.json
@@ -0,0 +1 @@
+{"commonDomain":"test.app", "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":"another-bot8","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":"<p class=\"content-editor__unstyled\"><strong style=\"font-weight:bold\">Portainer is a lightweight management UI which allows you to easily manage your different Docker environments (Docker hosts or Swarm clusters)</strong></p>","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":"<p class=\"content-editor__unstyled\">PostgREST description</p>","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":"mysampleproject6","custom_stack_code":"another-bot8","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/forms/stack.rs b/src/forms/stack.rs
index 4f3afd93..50ca124c 100644
--- a/src/forms/stack.rs
+++ b/src/forms/stack.rs
@@ -36,7 +36,7 @@ pub struct StackForm {
 #[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize, Validate)]
 #[serde(rename_all = "snake_case")]
 pub struct StackPayload {
-    pub(crate) id: i32,
+    pub(crate) id: Option<i32>,
     pub(crate) user_token: Option<String>,
     pub(crate) user_email: Option<String>,
     pub(crate) installation_id: Option<u32>,
@@ -116,7 +116,10 @@ pub struct Web {
     pub name: String,
     pub code: String,
     pub domain: Option<String>,
-    #[serde(rename = "shared_ports")]
+    //#[serde(rename = "shared_ports")]
+    // #[serde(rename= "sharedPorts")]
+    #[serde(rename(deserialize = "sharedPorts"))]
+    #[serde(rename(serialize = "shared_ports"))]
     pub shared_ports: Option<Vec<String>>,
     pub versions: Option<Vec<Version>>,
     pub custom: bool,
@@ -200,7 +203,10 @@ pub struct Feature {
     pub dockerhub_image: Option<String>,
     pub versions: Option<Vec<Version>>,
     pub domain: Option<String>,
-    #[serde(rename = "shared_ports")]
+    // #[serde(rename = "shared_ports")]
+    // #[serde(rename= "sharedPorts")]
+    #[serde(rename(deserialize = "sharedPorts"))]
+    #[serde(rename(serialize = "shared_ports"))]
     pub shared_ports: Option<Vec<String>>,
     pub main: bool,
 }
@@ -305,7 +311,10 @@ pub struct Service {
     pub dockerhub_image: Option<String>,
     pub versions: Option<Vec<Version>>,
     pub domain: String,
-    #[serde(rename = "shared_ports")]
+    // #[serde(rename = "shared_ports")]
+    // #[serde(rename= "sharedPorts")]
+    #[serde(rename(deserialize = "sharedPorts"))]
+    #[serde(rename(serialize = "shared_ports"))]
     pub shared_ports: Option<Vec<String>>,
     pub main: bool,
 }
diff --git a/src/routes/stack/deploy.rs b/src/routes/stack/deploy.rs
index 768da668..aaf71353 100644
--- a/src/routes/stack/deploy.rs
+++ b/src/routes/stack/deploy.rs
@@ -77,10 +77,11 @@ pub async fn add(
                 dc.stack.body.clone()
             ).unwrap();
 
-            stack_data.id = id;
+            stack_data.id = Some(id);
             stack_data.installation_id = None;
             stack_data.user_token = Some(user.id.clone());
-            stack_data.user_email= Some(user.email.clone());
+            stack_data.user_email = Some(user.email.clone());
+            stack_data.stack_code = stack_data.custom.custom_stack_code.clone();
 
             let payload = serde_json::to_string::<StackPayload>(&stack_data).unwrap();
             let _payload = payload.as_bytes();

From d33f33a0d08a91fb700ec1c0947abbe26cb78ec9 Mon Sep 17 00:00:00 2001
From: vsilent <jabberroid@gmail.com>
Date: Thu, 16 Nov 2023 17:00:35 +0200
Subject: [PATCH 14/18] service option fields fixes

---
 custom-stack-payload-7.json  |  1 +
 custom-stack-payload-8.json  |  1 +
 src/forms/stack.rs           | 37 ++++++++++++++++++------------------
 src/helpers/stack/builder.rs |  8 +++++++-
 4 files changed, 27 insertions(+), 20 deletions(-)
 create mode 100644 custom-stack-payload-7.json
 create mode 100644 custom-stack-payload-8.json

diff --git a/custom-stack-payload-7.json b/custom-stack-payload-7.json
new file mode 100644
index 00000000..61dedb91
--- /dev/null
+++ b/custom-stack-payload-7.json
@@ -0,0 +1 @@
+{"commonDomain":"test.app", "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":"another-bot8","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":0,"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":"<p class=\"content-editor__unstyled\"><strong style=\"font-weight:bold\">Portainer is a lightweight management UI which allows you to easily manage your different Docker environments (Docker hosts or Swarm clusters)</strong></p>","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":"<p class=\"content-editor__unstyled\">PostgREST description</p>","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":"mysampleproject6","custom_stack_code":"another-bot8","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/custom-stack-payload-8.json b/custom-stack-payload-8.json
new file mode 100644
index 00000000..49d27860
--- /dev/null
+++ b/custom-stack-payload-8.json
@@ -0,0 +1 @@
+{"commonDomain":"","domainList":{},"region":"fsn1","zone":null,"server":"cpx11","os":"ubuntu-20.04","ssl":"letsencrypt","vars":[],"integrated_features":[],"extended_features":[],"subscriptions":[],"form_app":[],"save_token":false,"disk_type":"pd-standart","cloud_token":"****","provider":"htz","stack_code":"custom-stack","selected_plan":"plan-individual-monthly","custom":{"web":[{"_etag":null,"_id":233,"_created":"2023-07-19T06:38:57.608807","_updated":"2023-08-15T11:12:14.921797","name":"FastAPI","code":"fastapi","role":null,"type":"web","default":true,"popularity":null,"descr":null,"ports":{"public":["5050","8000","8080"]},"commercial":null,"subscription":null,"autodeploy":null,"suggested":null,"dependency":null,"avoid_render":null,"price":null,"icon":{"light":{"width":2500,"height":2500,"image":"8d1ba06d-04e2-4523-879e-2846c86a10d8.svg"},"dark":{}},"category_id":null,"parent_app_id":null,"full_description":null,"description":null,"plan_type":null,"ansible_var":null,"repo_dir":null,"cpu":0.1,"ram_size":"0.2Gb","disk_size":"0.2Gb","dockerhub_image":"fastapi","form":null,"versions":[{"_etag":null,"_id":587,"_created":null,"_updated":"2023-07-20T12:51:20.321999","app_id":233,"name":"0.0.1","version":"0.0.1","update_status":"ready_for_testing","tag":"0.0.1"},{"_etag":null,"_id":590,"_created":null,"_updated":"2023-07-20T08:36:49.651219","app_id":233,"name":"0.0.1","version":"0.0.1","update_status":"ready_for_testing","tag":"latest"},{"_etag":null,"_id":589,"_created":null,"_updated":"2023-07-20T08:36:55.200575","app_id":233,"name":"0.0.1","version":"0.0.1","update_status":"ready_for_testing","tag":"unstable"},{"_etag":"591","_id":591,"_created":null,"_updated":"2023-08-15T08:17:40.226186","app_id":233,"name":"Fastapi","version":"0.100.0","update_status":"published","tag":"stable"}],"domain":"fastapi.test","sharedPorts":["8000"],"main":true,"version":{"_etag":"591","_id":591,"_created":null,"_updated":"2023-08-15T08:17:40.226186","app_id":233,"name":"Fastapi","version":"0.100.0","update_status":"published","tag":"stable"}}],"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":"<p class=\"content-editor__unstyled\"><strong style=\"font-weight:bold\">Portainer is a lightweight management UI which allows you to easily manage your different Docker environments (Docker hosts or Swarm clusters)</strong></p>","plan_type":null,"ansible_var":null,"repo_Show more
diff --git a/src/forms/stack.rs b/src/forms/stack.rs
index 50ca124c..108d1f3c 100644
--- a/src/forms/stack.rs
+++ b/src/forms/stack.rs
@@ -3,7 +3,6 @@ use serde_json::Value;
 use serde_valid::Validate;
 
 #[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize, Validate)]
-#[serde(rename_all = "snake_case")]
 pub struct StackForm {
     #[serde(rename= "commonDomain")]
     pub common_domain: Option<String>,
@@ -15,19 +14,14 @@ pub struct StackForm {
     pub os: String,
     pub ssl: String,
     pub vars: Option<Vec<Var>>,
-    #[serde(rename = "integrated_features")]
     pub integrated_features: Option<Vec<Value>>,
-    #[serde(rename = "extended_features")]
     pub extended_features: Option<Vec<Value>>,
     pub subscriptions: Option<Vec<String>>,
     pub form_app: Option<Vec<String>>,
     pub disk_type: Option<String>,
-    #[serde(rename = "save_token")]
     pub save_token: bool,
-    #[serde(rename = "cloud_token")]
     pub cloud_token: String,
     pub provider: String,
-    #[serde(rename = "selected_plan")]
     pub selected_plan: String,
     pub custom: Custom,
 }
@@ -90,7 +84,7 @@ pub struct Custom {
     pub feature: Option<Vec<Feature>>,
     pub service: Option<Vec<Service>>,
     #[serde(rename = "servers_count")]
-    pub servers_count: i64,
+    pub servers_count: u32,
     #[serde(rename = "custom_stack_code")]
     pub custom_stack_code: String,
     #[serde(rename = "project_git_url")]
@@ -122,16 +116,19 @@ pub struct Web {
     #[serde(rename(serialize = "shared_ports"))]
     pub shared_ports: Option<Vec<String>>,
     pub versions: Option<Vec<Version>>,
-    pub custom: bool,
+    pub custom: Option<bool>,
     #[serde(rename = "type")]
     pub type_field: String,
     pub main: bool,
     #[serde(rename = "_id")]
-    pub id: String,
+    pub id: u32,
     #[serde(rename = "dockerhub_user")]
-    pub dockerhub_user: String,
+    pub dockerhub_user: Option<String>,
     #[serde(rename = "dockerhub_name")]
-    pub dockerhub_name: String,
+    pub dockerhub_name: Option<String>,
+    // when no docker repo specified by user
+    // use default TD image
+    pub dockerhub_image: Option<String>,
     #[serde(rename = "url_app")]
     pub url_app: Option<String>,
     #[serde(rename = "url_git")]
@@ -145,7 +142,7 @@ pub struct Web {
     #[validate(min_length=1)]
     #[validate(max_length=10)]
     #[validate(pattern = r"^\d+G$")]
-    pub ram_size: String,
+    pub ram_size: Option<String>,
     #[validate(minimum=0.1)]
     pub cpu: Option<f64>,
 }
@@ -194,7 +191,8 @@ pub struct Feature {
     pub repo_dir: Option<String>,
     pub cpu: Option<f64>,
     #[validate(min_length=1)]
-    #[serde(rename = "ram_size")]
+    #[validate(max_length=10)]
+    #[validate(pattern = r"^\d+G$")]
     pub ram_size: Option<String>,
     #[validate(min_length=1)]
     #[serde(rename = "disk_size")]
@@ -244,13 +242,13 @@ pub struct Version {
     #[serde(rename = "_etag")]
     pub etag: Option<String>,
     #[serde(rename = "_id")]
-    pub id: i64,
+    pub id: u32,
     #[serde(rename = "_created")]
     pub created: Option<String>,
     #[serde(rename = "_updated")]
     pub updated: Option<String>,
     #[serde(rename = "app_id")]
-    pub app_id: i64,
+    pub app_id: u32,
     pub name: String,
     pub version: String,
     #[serde(rename = "update_status")]
@@ -264,7 +262,7 @@ pub struct Service {
     #[serde(rename = "_etag")]
     pub etag: Option<String>,
     #[serde(rename = "_id")]
-    pub id: i64,
+    pub id: u32,
     #[serde(rename = "_created")]
     pub created: Option<String>,
     #[serde(rename = "_updated")]
@@ -301,12 +299,13 @@ pub struct Service {
     #[serde(rename = "repo_dir")]
     pub repo_dir: Option<String>,
     pub cpu: Option<f64>,
-    #[serde(rename = "ram_size")]
     #[validate(min_length=1)]
-    pub ram_size: String,
+    #[validate(max_length=10)]
+    #[validate(pattern = r"^\d+G$")]
+    pub ram_size: Option<String>,
     #[serde(rename = "disk_size")]
     #[validate(min_length=1)]
-    pub disk_size: String,
+    pub disk_size: Option<String>,
     #[serde(rename = "dockerhub_image")]
     pub dockerhub_image: Option<String>,
     pub versions: Option<Vec<Version>>,
diff --git a/src/helpers/stack/builder.rs b/src/helpers/stack/builder.rs
index a6c9fd99..3986650d 100644
--- a/src/helpers/stack/builder.rs
+++ b/src/helpers/stack/builder.rs
@@ -89,7 +89,13 @@ impl DcBuilder {
 
                 for app in apps.custom.web {
                     let tag = "latest";
-                    let img= format!("{}/{}:{}",app.dockerhub_user, app.dockerhub_name, tag);
+                    let dim = app.dockerhub_image.clone().unwrap_or("".to_string());
+                    let img= format!("{}/{}:{}",
+                                     app.dockerhub_user.clone()
+                                         .unwrap_or("trydirect".to_string()).clone(),
+                                     app.dockerhub_name.clone().unwrap_or(dim),
+                                     tag
+                    );
                     let code = app.code.clone().to_owned();
 
                     let mut service = Service {

From 2e209bed2b8de7c52f0896743f73e190be67d885 Mon Sep 17 00:00:00 2001
From: vsilent <jabberroid@gmail.com>
Date: Fri, 17 Nov 2023 09:55:20 +0200
Subject: [PATCH 15/18] insert stack fix

---
 custom-stack-payload-9.json |  1 +
 src/forms/stack.rs          |  3 +++
 src/routes/stack/add.rs     | 28 ++++++++--------------------
 3 files changed, 12 insertions(+), 20 deletions(-)
 create mode 100644 custom-stack-payload-9.json

diff --git a/custom-stack-payload-9.json b/custom-stack-payload-9.json
new file mode 100644
index 00000000..3bf473d6
--- /dev/null
+++ b/custom-stack-payload-9.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"],"form_app":[],"save_token":true,"disk_type":"pd-standart","cloud_token":"D3dkDL4Qy1NpFezTl60V5RYfB5p0BSwLLoKVHhEqmJQ3jObSG77irP86e9YtCYVi","provider":"htz","stack_code":"custom-stack","selected_plan":"plan-individual-monthly","custom":{"web":[{"_etag":null,"_id":233,"_created":"2023-07-19T06:38:57.608807","_updated":"2023-08-15T11:12:14.921797","name":"FastAPI","code":"fastapi","role":null,"type":"web","default":true,"popularity":null,"descr":null,"ports":{"public":["5050","8000","8080"]},"commercial":null,"subscription":null,"autodeploy":null,"suggested":null,"dependency":null,"avoid_render":null,"price":null,"icon":{"light":{"width":2500,"height":2500,"image":"8d1ba06d-04e2-4523-879e-2846c86a10d8.svg"},"dark":{}},"category_id":null,"parent_app_id":null,"full_description":null,"description":null,"plan_type":null,"ansible_var":null,"repo_dir":null,"cpu":0.1,"ram_size":"0.2Gb","disk_size":"0.2Gb","dockerhub_image":"fastapi","form":null,"versions":[{"_etag":null,"_id":587,"_created":null,"_updated":"2023-07-20T12:51:20.321999","app_id":233,"name":"0.0.1","version":"0.0.1","update_status":"ready_for_testing","tag":"0.0.1"},{"_etag":null,"_id":590,"_created":null,"_updated":"2023-07-20T08:36:49.651219","app_id":233,"name":"0.0.1","version":"0.0.1","update_status":"ready_for_testing","tag":"latest"},{"_etag":null,"_id":589,"_created":null,"_updated":"2023-07-20T08:36:55.200575","app_id":233,"name":"0.0.1","version":"0.0.1","update_status":"ready_for_testing","tag":"unstable"},{"_etag":"591","_id":591,"_created":null,"_updated":"2023-08-15T08:17:40.226186","app_id":233,"name":"Fastapi","version":"0.100.0","update_status":"published","tag":"stable"}],"domain":"fastapi.test","sharedPorts":["8000"],"main":true,"version":{"_etag":"591","_id":591,"_created":null,"_updated":"2023-08-15T08:17:40.226186","app_id":233,"name":"Fastapi","version":"0.100.0","update_status":"published","tag":"stable"}},{"name":"smarty bot","code":"smarty-bot","domain":"smarty-bot.fastapi.test","sharedPorts":["8080"],"versions":[],"custom":true,"type":"web","main":false,"_id":0,"dockerhub_user":"vsilent","dockerhub_name":"smarty","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":"<p class=\"content-editor__unstyled\"><strong style=\"font-weight:bold\">Portainer is a lightweight management UI which allows you to easily manage your different Docker environments (Docker hosts or Swarm clusters)</strong></p>","plan_type":null,"ansible_var":null,"repo_dir":null,"cpu":0.6,"ram_size":"1Gb","disk_size":"1Gb","dockerhub_image":"portainer-ce-feature","form":null,"versions":[{"_etag":null,"_id":456,"_created":"2022-04-25T12:44:30.964547","_updated":"2023-09-07T07:28:51.18965","app_id":198,"name":"2.18.4","version":"2.18.4","update_status":"published","tag":"2.18.4"}],"domain":"","sharedPorts":["9000"],"main":false,"version":{"_etag":null,"_id":456,"_created":"2022-04-25T12:44:30.964547","_updated":"2023-09-07T07:28:51.18965","app_id":198,"name":"2.18.4","version":"2.18.4","update_status":"published","tag":"2.18.4"}}],"service":[{"_etag":null,"_id":246,"_created":"2023-09-15T11:41:21.353013","_updated":"2023-09-15T11:45:40.307663","name":"Mailhog","code":"mailhog_service","role":["mailhog"],"type":"service","default":null,"popularity":null,"descr":null,"ports":{"public":["8025"],"private":["1025","8025"]},"commercial":null,"subscription":null,"autodeploy":null,"suggested":null,"dependency":null,"avoid_render":null,"price":null,"icon":{"light":{"width":48,"height":48,"image":"9ed27fd5-4745-4fa1-86a6-e53557d700ff.png"},"dark":{"width":48,"height":48,"image":"0d7b38f8-d2ad-4068-a5bd-aa32c7e1958f.png"}},"category_id":null,"parent_app_id":null,"full_description":null,"description":"<p class=\"content-editor__unstyled\">MailHog is an email testing tool for developers.</p>","plan_type":null,"ansible_var":null,"repo_dir":null,"cpu":0,"ram_size":null,"disk_size":null,"dockerhub_image":null,"form":null,"versions":[{"_etag":null,"_id":625,"_created":null,"_updated":"2023-09-15T11:42:10.48131","app_id":246,"name":"latest","version":"latest","update_status":"ready_for_production","tag":"latest"}],"domain":"","sharedPorts":["8025"],"main":false,"version":{"_etag":null,"_id":625,"_created":null,"_updated":"2023-09-15T11:42:10.48131","app_id":246,"name":"latest","version":"latest","update_status":"ready_for_production","tag":"latest"}},{"_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":0,"ram_size":null,"disk_size":null,"dockerhub_image":"postgres","form":null,"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":["6372"],"main":false,"version":{"_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"}}],"servers_count":3,"project_name":"FastAPI example 3","custom_stack_code":"fastapi-example-3","project_git_url":"https://github.com/trydirect/fastapi.git"}}
diff --git a/src/forms/stack.rs b/src/forms/stack.rs
index 108d1f3c..541412c2 100644
--- a/src/forms/stack.rs
+++ b/src/forms/stack.rs
@@ -114,6 +114,7 @@ pub struct Web {
     // #[serde(rename= "sharedPorts")]
     #[serde(rename(deserialize = "sharedPorts"))]
     #[serde(rename(serialize = "shared_ports"))]
+    #[serde(alias = "shared_ports")]
     pub shared_ports: Option<Vec<String>>,
     pub versions: Option<Vec<Version>>,
     pub custom: Option<bool>,
@@ -205,6 +206,7 @@ pub struct Feature {
     // #[serde(rename= "sharedPorts")]
     #[serde(rename(deserialize = "sharedPorts"))]
     #[serde(rename(serialize = "shared_ports"))]
+    #[serde(alias = "shared_ports")]
     pub shared_ports: Option<Vec<String>>,
     pub main: bool,
 }
@@ -314,6 +316,7 @@ pub struct Service {
     // #[serde(rename= "sharedPorts")]
     #[serde(rename(deserialize = "sharedPorts"))]
     #[serde(rename(serialize = "shared_ports"))]
+    #[serde(alias = "shared_ports")]
     pub shared_ports: Option<Vec<String>>,
     pub main: bool,
 }
diff --git a/src/routes/stack/add.rs b/src/routes/stack/add.rs
index 9239bc6d..2ae44a42 100644
--- a/src/routes/stack/add.rs
+++ b/src/routes/stack/add.rs
@@ -66,30 +66,18 @@ pub async fn add(
         }
     };
 
-    let stack = Stack {
-        id: 0_i32,                // internal stack id
-        stack_id: Uuid::new_v4(), // public uuid of the stack
-        user_id: user_id,         //
-        name: stack_name,
-        body: body,
-        created_at: Utc::now(),
-        updated_at: Utc::now(),
-    };
-
-    tracing::debug!("stack object {:?}", stack);
     return match sqlx::query!(
         r#"
-        INSERT INTO user_stack (id, stack_id, user_id, name, body, created_at, updated_at)
-        VALUES ($1, $2, $3, $4, $5, $6, $7)
+        INSERT INTO user_stack (stack_id, user_id, name, body, created_at, updated_at)
+        VALUES ($1, $2, $3, $4, $5, $6)
         RETURNING id;
         "#,
-        0_i32,
-        stack.stack_id,
-        stack.user_id,
-        stack.name,
-        stack.body,
-        stack.created_at,
-        stack.updated_at
+        Uuid::new_v4(),
+        user_id,
+        stack_name,
+        body,
+        Utc::now(),
+        Utc::now(),
     )
     .fetch_one(pool.get_ref())
     .instrument(query_span)

From aa9aadaa98d860dee9c700895e3376acbb7163c5 Mon Sep 17 00:00:00 2001
From: vsilent <jabberroid@gmail.com>
Date: Mon, 20 Nov 2023 11:54:11 +0200
Subject: [PATCH 16/18] response builder

---
 custom-stack-payload-6.json  |   2 +-
 custom-stack-payload-7.json  |   2 +-
 custom-stack-payload-8.json  |   2 +-
 src/forms/stack.rs           | 236 ++++++++++++++---------------------
 src/helpers/json.rs          | 228 ++++++++++++++++++---------------
 src/helpers/stack/builder.rs |  81 ++++--------
 src/models/rating.rs         |   2 +-
 src/routes/client/add.rs     |  22 ++--
 src/routes/rating/add.rs     |  22 ++--
 src/routes/rating/get.rs     |  30 ++---
 src/routes/stack/add.rs      |  13 +-
 src/routes/stack/compose.rs  |  31 ++---
 src/routes/stack/deploy.rs   |  21 +---
 src/routes/stack/get.rs      |  30 ++---
 14 files changed, 306 insertions(+), 416 deletions(-)

diff --git a/custom-stack-payload-6.json b/custom-stack-payload-6.json
index 97e422a9..61dedb91 100644
--- a/custom-stack-payload-6.json
+++ b/custom-stack-payload-6.json
@@ -1 +1 @@
-{"commonDomain":"test.app", "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":"another-bot8","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":"<p class=\"content-editor__unstyled\"><strong style=\"font-weight:bold\">Portainer is a lightweight management UI which allows you to easily manage your different Docker environments (Docker hosts or Swarm clusters)</strong></p>","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":"<p class=\"content-editor__unstyled\">PostgREST description</p>","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":"mysampleproject6","custom_stack_code":"another-bot8","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"}}
+{"commonDomain":"test.app", "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":"another-bot8","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":0,"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":"<p class=\"content-editor__unstyled\"><strong style=\"font-weight:bold\">Portainer is a lightweight management UI which allows you to easily manage your different Docker environments (Docker hosts or Swarm clusters)</strong></p>","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":"<p class=\"content-editor__unstyled\">PostgREST description</p>","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":"mysampleproject6","custom_stack_code":"another-bot8","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/custom-stack-payload-7.json b/custom-stack-payload-7.json
index 61dedb91..1fbfbbdf 100644
--- a/custom-stack-payload-7.json
+++ b/custom-stack-payload-7.json
@@ -1 +1 @@
-{"commonDomain":"test.app", "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":"another-bot8","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":0,"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":"<p class=\"content-editor__unstyled\"><strong style=\"font-weight:bold\">Portainer is a lightweight management UI which allows you to easily manage your different Docker environments (Docker hosts or Swarm clusters)</strong></p>","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":"<p class=\"content-editor__unstyled\">PostgREST description</p>","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":"mysampleproject6","custom_stack_code":"another-bot8","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"}}
+{"commonDomain":"test.app", "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":"another-bot8","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":0,"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":"<p class=\"content-editor__unstyled\"><strong style=\"font-weight:bold\">Portainer is a lightweight management UI which allows you to easily manage your different Docker environments (Docker hosts or Swarm clusters)</strong></p>","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":"<p class=\"content-editor__unstyled\">PostgREST description</p>","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":"mysampleproject7","custom_stack_code":"another-bot9","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/custom-stack-payload-8.json b/custom-stack-payload-8.json
index 49d27860..7a55fc94 100644
--- a/custom-stack-payload-8.json
+++ b/custom-stack-payload-8.json
@@ -1 +1 @@
-{"commonDomain":"","domainList":{},"region":"fsn1","zone":null,"server":"cpx11","os":"ubuntu-20.04","ssl":"letsencrypt","vars":[],"integrated_features":[],"extended_features":[],"subscriptions":[],"form_app":[],"save_token":false,"disk_type":"pd-standart","cloud_token":"****","provider":"htz","stack_code":"custom-stack","selected_plan":"plan-individual-monthly","custom":{"web":[{"_etag":null,"_id":233,"_created":"2023-07-19T06:38:57.608807","_updated":"2023-08-15T11:12:14.921797","name":"FastAPI","code":"fastapi","role":null,"type":"web","default":true,"popularity":null,"descr":null,"ports":{"public":["5050","8000","8080"]},"commercial":null,"subscription":null,"autodeploy":null,"suggested":null,"dependency":null,"avoid_render":null,"price":null,"icon":{"light":{"width":2500,"height":2500,"image":"8d1ba06d-04e2-4523-879e-2846c86a10d8.svg"},"dark":{}},"category_id":null,"parent_app_id":null,"full_description":null,"description":null,"plan_type":null,"ansible_var":null,"repo_dir":null,"cpu":0.1,"ram_size":"0.2Gb","disk_size":"0.2Gb","dockerhub_image":"fastapi","form":null,"versions":[{"_etag":null,"_id":587,"_created":null,"_updated":"2023-07-20T12:51:20.321999","app_id":233,"name":"0.0.1","version":"0.0.1","update_status":"ready_for_testing","tag":"0.0.1"},{"_etag":null,"_id":590,"_created":null,"_updated":"2023-07-20T08:36:49.651219","app_id":233,"name":"0.0.1","version":"0.0.1","update_status":"ready_for_testing","tag":"latest"},{"_etag":null,"_id":589,"_created":null,"_updated":"2023-07-20T08:36:55.200575","app_id":233,"name":"0.0.1","version":"0.0.1","update_status":"ready_for_testing","tag":"unstable"},{"_etag":"591","_id":591,"_created":null,"_updated":"2023-08-15T08:17:40.226186","app_id":233,"name":"Fastapi","version":"0.100.0","update_status":"published","tag":"stable"}],"domain":"fastapi.test","sharedPorts":["8000"],"main":true,"version":{"_etag":"591","_id":591,"_created":null,"_updated":"2023-08-15T08:17:40.226186","app_id":233,"name":"Fastapi","version":"0.100.0","update_status":"published","tag":"stable"}}],"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":"<p class=\"content-editor__unstyled\"><strong style=\"font-weight:bold\">Portainer is a lightweight management UI which allows you to easily manage your different Docker environments (Docker hosts or Swarm clusters)</strong></p>","plan_type":null,"ansible_var":null,"repo_Show more
+{"commonDomain":"","domainList":{},"region":"fsn1","zone":null,"server":"cpx11","os":"ubuntu-20.04","ssl":"letsencrypt","vars":[],"integrated_features":[],"extended_features":[],"subscriptions":[],"form_app":[],"save_token":false,"disk_type":"pd-standart","cloud_token":"****","provider":"htz","stack_code":"custom-stack","selected_plan":"plan-individual-monthly","custom":{"web":[{"_etag":null,"_id":233,"_created":"2023-07-19T06:38:57.608807","_updated":"2023-08-15T11:12:14.921797","name":"FastAPI","code":"fastapi","role":null,"type":"web","default":true,"popularity":null,"descr":null,"ports":{"public":["5050","8000","8080"]},"commercial":null,"subscription":null,"autodeploy":null,"suggested":null,"dependency":null,"avoid_render":null,"price":null,"icon":{"light":{"width":2500,"height":2500,"image":"8d1ba06d-04e2-4523-879e-2846c86a10d8.svg"},"dark":{}},"category_id":null,"parent_app_id":null,"full_description":null,"description":null,"plan_type":null,"ansible_var":null,"repo_dir":null,"cpu":0.1,"ram_size":"0.2Gb","disk_size":"0.2Gb","dockerhub_image":"fastapi","form":null,"versions":[{"_etag":null,"_id":587,"_created":null,"_updated":"2023-07-20T12:51:20.321999","app_id":233,"name":"0.0.1","version":"0.0.1","update_status":"ready_for_testing","tag":"0.0.1"},{"_etag":null,"_id":590,"_created":null,"_updated":"2023-07-20T08:36:49.651219","app_id":233,"name":"0.0.1","version":"0.0.1","update_status":"ready_for_testing","tag":"latest"},{"_etag":null,"_id":589,"_created":null,"_updated":"2023-07-20T08:36:55.200575","app_id":233,"name":"0.0.1","version":"0.0.1","update_status":"ready_for_testing","tag":"unstable"},{"_etag":"591","_id":591,"_created":null,"_updated":"2023-08-15T08:17:40.226186","app_id":233,"name":"Fastapi","version":"0.100.0","update_status":"published","tag":"stable"}],"domain":"fastapi.test","sharedPorts":["8000"],"main":true,"version":{"_etag":"591","_id":591,"_created":null,"_updated":"2023-08-15T08:17:40.226186","app_id":233,"name":"Fastapi","version":"0.100.0","update_status":"published","tag":"stable"}}],"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":"<p class=\"content-editor__unstyled\"><strong style=\"font-weight:bold\">Portainer is a lightweight management UI which allows you to easily manage your different Docker environments (Docker hosts or Swarm clusters)</strong></p>","plan_type":null,"ansible_var":null,"repo_dir":null,"cpu":0.6,"ram_size":"1Gb","disk_size":"1Gb","dockerhub_image":"portainer-ce-feature","form":null,"versions":[{"_etag":null,"_id":456,"_created":"2022-04-25T12:44:30.964547","_updated":"2023-09-07T07:28:51.18965","app_id":198,"name":"2.18.4","version":"2.18.4","update_status":"published","tag":"2.18.4"}],"domain":"","sharedPorts":["9000"],"main":false,"version":{"_etag":null,"_id":456,"_created":"2022-04-25T12:44:30.964547","_updated":"2023-09-07T07:28:51.18965","app_id":198,"name":"2.18.4","version":"2.18.4","update_status":"published","tag":"2.18.4"}}],"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":0,"ram_size":null,"disk_size":null,"dockerhub_image":"postgres","form":null,"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":["6372"],"main":false,"version":{"_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"}}],"servers_count":3,"project_name":"FastAPI example","custom_stack_code":"fastapi-example","project_git_url":"https://github.com/trydirect/fastapi.git"}}
diff --git a/src/forms/stack.rs b/src/forms/stack.rs
index 541412c2..78a0847b 100644
--- a/src/forms/stack.rs
+++ b/src/forms/stack.rs
@@ -1,6 +1,66 @@
 use serde::{Deserialize, Serialize};
 use serde_json::Value;
 use serde_valid::Validate;
+use std::fmt;
+
+
+#[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize, Validate)]
+pub struct Role {
+    pub role: Option<Vec<String>>,
+}
+
+#[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize, Validate)]
+pub struct Requirements {
+    #[validate(minimum=0.1)]
+    pub cpu: Option<f64>,
+    #[validate(min_length=1)]
+    #[validate(max_length=10)]
+    #[validate(pattern = r"^\d+G$")]
+    #[serde(rename = "disk_size")]
+    pub disk_size: Option<String>,
+    #[serde(rename = "ram_size")]
+    #[validate(min_length=1)]
+    #[validate(max_length=10)]
+    #[validate(pattern = r"^\d+G$")]
+    pub ram_size: Option<String>,
+}
+
+#[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize, Validate)]
+pub struct Ports {
+    #[serde(rename(deserialize = "sharedPorts"))]
+    #[serde(rename(serialize = "shared_ports"))]
+    #[serde(alias = "shared_ports")]
+    pub shared_ports: Option<Vec<String>>,
+    pub ports: Option<Vec<String>>,
+}
+
+#[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize, Validate)]
+pub struct DockerImage {
+    pub dockerhub_user: Option<String>,
+    pub dockerhub_name: Option<String>,
+    pub dockerhub_image: Option<String>,
+}
+
+impl fmt::Display for DockerImage
+{
+    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
+        let tag = "latest";
+
+        let dim = self.dockerhub_image.clone()
+            .unwrap_or("".to_string());
+        write!(f, "{}/{}:{}", self.dockerhub_user.clone()
+            .unwrap_or("trydirect".to_string()).clone(),
+                self.dockerhub_name.clone().unwrap_or(dim), tag
+        )
+    }
+}
+
+impl AsRef<DockerImage> for App {
+    fn as_ref(&self) -> &DockerImage {
+        &self.docker_image
+    }
+}
+
 
 #[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize, Validate)]
 pub struct StackForm {
@@ -33,7 +93,6 @@ pub struct StackPayload {
     pub(crate) id: Option<i32>,
     pub(crate) user_token: Option<String>,
     pub(crate) user_email: Option<String>,
-    pub(crate) installation_id: Option<u32>,
     #[serde(rename= "commonDomain")]
     pub common_domain: String,
     pub domain_list: Option<DomainList>,
@@ -55,7 +114,6 @@ pub struct StackPayload {
     #[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,
@@ -103,54 +161,8 @@ pub struct Custom {
     pub project_description: Option<String>,
 }
 
-
 #[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize, Validate)]
-#[serde(rename_all = "camelCase")]
-pub struct Web {
-    pub name: String,
-    pub code: String,
-    pub domain: Option<String>,
-    //#[serde(rename = "shared_ports")]
-    // #[serde(rename= "sharedPorts")]
-    #[serde(rename(deserialize = "sharedPorts"))]
-    #[serde(rename(serialize = "shared_ports"))]
-    #[serde(alias = "shared_ports")]
-    pub shared_ports: Option<Vec<String>>,
-    pub versions: Option<Vec<Version>>,
-    pub custom: Option<bool>,
-    #[serde(rename = "type")]
-    pub type_field: String,
-    pub main: bool,
-    #[serde(rename = "_id")]
-    pub id: u32,
-    #[serde(rename = "dockerhub_user")]
-    pub dockerhub_user: Option<String>,
-    #[serde(rename = "dockerhub_name")]
-    pub dockerhub_name: Option<String>,
-    // when no docker repo specified by user
-    // use default TD image
-    pub dockerhub_image: Option<String>,
-    #[serde(rename = "url_app")]
-    pub url_app: Option<String>,
-    #[serde(rename = "url_git")]
-    pub url_git: Option<String>,
-    #[validate(min_length=1)]
-    #[validate(max_length=10)]
-    #[validate(pattern = r"^\d+G$")]
-    #[serde(rename = "disk_size")]
-    pub disk_size: String,
-    #[serde(rename = "ram_size")]
-    #[validate(min_length=1)]
-    #[validate(max_length=10)]
-    #[validate(pattern = r"^\d+G$")]
-    pub ram_size: Option<String>,
-    #[validate(minimum=0.1)]
-    pub cpu: Option<f64>,
-}
-
-#[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize, Validate)]
-#[serde(rename_all = "camelCase")]
-pub struct Feature {
+pub struct App {
     #[serde(rename = "_etag")]
     pub etag: Option<String>,
     #[serde(rename = "_id")]
@@ -161,62 +173,60 @@ pub struct Feature {
     pub updated: Option<String>,
     pub name: String,
     pub code: String,
-    pub role: Vec<String>,
     #[serde(rename = "type")]
     pub type_field: String,
+    #[serde(flatten)]
+    pub role: Role,
     pub default: Option<bool>,
-    pub popularity: Option<u32>,
-    pub descr: Option<String>,
+    #[serde(flatten)]
     pub ports: Option<Ports>,
+    pub versions: Option<Vec<Version>>,
+    #[serde(flatten)]
+    pub docker_image: DockerImage,
+    #[serde(flatten)]
+    pub requirements: Requirements,
+    pub popularity: Option<u32>,
     pub commercial: Option<bool>,
     pub subscription: Option<Value>,
     pub autodeploy: Option<bool>,
     pub suggested: Option<bool>,
     pub dependency: Option<Value>,
-    #[serde(rename = "avoid_render")]
     pub avoid_render: Option<bool>,
     pub price: Option<Price>,
     pub icon: Option<Icon>,
-    #[serde(rename = "category_id")]
+    pub domain: Option<String>,
+    pub main: bool,
     pub category_id: Option<u32>,
-    #[serde(rename = "parent_app_id")]
     pub parent_app_id: Option<u32>,
-    #[serde(rename = "full_description")]
+    pub descr: Option<String>,
     pub full_description: Option<String>,
     pub description: Option<String>,
-    #[serde(rename = "plan_type")]
     pub plan_type: Option<String>,
-    #[serde(rename = "ansible_var")]
     pub ansible_var: Option<String>,
-    #[serde(rename = "repo_dir")]
     pub repo_dir: Option<String>,
-    pub cpu: Option<f64>,
-    #[validate(min_length=1)]
-    #[validate(max_length=10)]
-    #[validate(pattern = r"^\d+G$")]
-    pub ram_size: Option<String>,
-    #[validate(min_length=1)]
-    #[serde(rename = "disk_size")]
-    pub disk_size: Option<String>,
-    #[serde(rename = "dockerhub_image")]
-    pub dockerhub_image: Option<String>,
-    pub versions: Option<Vec<Version>>,
-    pub domain: Option<String>,
-    // #[serde(rename = "shared_ports")]
-    // #[serde(rename= "sharedPorts")]
-    #[serde(rename(deserialize = "sharedPorts"))]
-    #[serde(rename(serialize = "shared_ports"))]
-    #[serde(alias = "shared_ports")]
-    pub shared_ports: Option<Vec<String>>,
-    pub main: bool,
+    pub url_app: Option<String>,
+    pub url_git: Option<String>,
 }
 
-#[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize)]
-#[serde(rename_all = "camelCase")]
-pub struct Ports {
-    pub public: Vec<String>,
+#[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize, Validate)]
+pub struct Web {
+    #[serde(flatten)]
+    pub app: App,
+    pub custom: Option<bool>,
+}
+
+#[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize, Validate)]
+pub struct Feature {
+    #[serde(flatten)]
+    pub app: App,
 }
 
+#[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize, Validate)]
+#[serde(rename_all = "camelCase")]
+pub struct Service {
+    #[serde(flatten)]
+    pub(crate) app: App,
+}
 
 #[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize)]
 #[serde(rename_all = "camelCase")]
@@ -258,66 +268,4 @@ pub struct Version {
     pub tag: String,
 }
 
-#[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize, Validate)]
-#[serde(rename_all = "camelCase")]
-pub struct Service {
-    #[serde(rename = "_etag")]
-    pub etag: Option<String>,
-    #[serde(rename = "_id")]
-    pub id: u32,
-    #[serde(rename = "_created")]
-    pub created: Option<String>,
-    #[serde(rename = "_updated")]
-    pub updated: Option<String>,
-    pub name: String,
-    pub code: String,
-    pub role: Option<Vec<Value>>,
-    #[serde(rename = "type")]
-    pub type_field: String,
-    pub default: Option<Value>,
-    pub popularity: Option<u32>,
-    pub descr: Option<String>,
-    pub ports: Option<Ports>,
-    pub commercial: Option<bool>,
-    pub subscription: Option<Value>,
-    pub autodeploy: Option<bool>,
-    pub suggested: Option<bool>,
-    pub dependency: Option<Value>,
-    #[serde(rename = "avoid_render")]
-    pub avoid_render: Option<bool>,
-    pub price: Option<Price>,
-    pub icon: Option<Icon>,
-    #[serde(rename = "category_id")]
-    pub category_id: Option<u32>,
-    #[serde(rename = "parent_app_id")]
-    pub parent_app_id: Option<u32>,
-    #[serde(rename = "full_description")]
-    pub full_description: Option<String>,
-    pub description: Option<String>,
-    #[serde(rename = "plan_type")]
-    pub plan_type: Option<String>,
-    #[serde(rename = "ansible_var")]
-    pub ansible_var: Option<String>,
-    #[serde(rename = "repo_dir")]
-    pub repo_dir: Option<String>,
-    pub cpu: Option<f64>,
-    #[validate(min_length=1)]
-    #[validate(max_length=10)]
-    #[validate(pattern = r"^\d+G$")]
-    pub ram_size: Option<String>,
-    #[serde(rename = "disk_size")]
-    #[validate(min_length=1)]
-    pub disk_size: Option<String>,
-    #[serde(rename = "dockerhub_image")]
-    pub dockerhub_image: Option<String>,
-    pub versions: Option<Vec<Version>>,
-    pub domain: String,
-    // #[serde(rename = "shared_ports")]
-    // #[serde(rename= "sharedPorts")]
-    #[serde(rename(deserialize = "sharedPorts"))]
-    #[serde(rename(serialize = "shared_ports"))]
-    #[serde(alias = "shared_ports")]
-    pub shared_ports: Option<Vec<String>>,
-    pub main: bool,
-}
 
diff --git a/src/helpers/json.rs b/src/helpers/json.rs
index 6321a066..52d40db3 100644
--- a/src/helpers/json.rs
+++ b/src/helpers/json.rs
@@ -1,13 +1,12 @@
 use actix_web::{Responder, Result};
+use actix_web::error::{ErrorBadRequest, ErrorConflict, ErrorNotFound, ErrorInternalServerError};
 use serde_derive::Serialize;
-use actix_web::web;
-use actix_web::web::JsonBody::Error;
+use actix_web::web::Json;
+use actix_web::Error;
 
 #[derive(Serialize)]
 pub(crate) struct JsonResponse<T> {
-    pub(crate) status: String,
     pub(crate) message: String,
-    pub(crate) code: u32,
     pub(crate) id: Option<i32>,
     pub(crate) item: Option<T>,
     pub(crate) list: Option<Vec<T>>
@@ -18,58 +17,96 @@ pub(crate) struct JsonResponse<T> {
 pub struct JsonResponseBuilder<T>
     where T: serde::Serialize + Default
 {
-    status: String,
-    message: String,
-    code: u32,
     id: Option<i32>,
     item: Option<T>,
     list: Option<Vec<T>>
 }
 
 impl<T> JsonResponseBuilder<T>
-where T: serde::Serialize + Default
+    where T: serde::Serialize + Default
 {
-   pub(crate) fn new() -> Self {
-       Self::default()
-   }
+    pub(crate) fn new() -> Self {
+        Self::default()
+    }
 
-    fn set_item(mut self, item:T) -> Self {
+    pub(crate) fn set_item(mut self, item:T) -> Self {
         self.item = Some(item);
         self
     }
 
+    pub(crate) fn set_id(mut self, id:i32) -> Self {
+        self.id = Some(id);
+        self
+    }
 
     pub(crate) fn set_list(mut self, list:Vec<T>) -> Self {
         self.list = Some(list);
         self
     }
 
-    pub(crate) fn ok(self) -> Result<impl Responder>  {
+    pub(crate) fn ok(self, msg: String) -> Result<Json<JsonResponse<T>>, Error>  {
 
-        Ok(web::Json(
+        Ok(Json(
             JsonResponse {
-                status: self.status,
-                message: self.message,
-                code: self.code,
+                message: msg,
                 id: self.id,
                 item: self.item,
                 list: self.list,
             }
         ))
+
     }
 
-    pub(crate) fn err(self) -> Result<impl Responder>  {
+    pub(crate) fn err(self, msg: String) -> Result<Json<JsonResponse<T>>, Error>  {
 
-        Ok(web::Json(
-            JsonResponse {
-                status: self.status,
-                message: self.message,
-                code: self.code,
-                id: self.id,
-                item: self.item,
-                list: self.list,
-            }
-        ))
+        let json_response = JsonResponse {
+            message: msg,
+            id: self.id,
+            item: self.item,
+            list: self.list
+        };
+
+        Err(ErrorBadRequest(
+            serde_json::to_string(&json_response).unwrap()))
+    }
+
+    pub(crate) fn not_found(self, msg: String) -> Result<Json<JsonResponse<T>>, Error>  {
+
+        let json_response = JsonResponse {
+            message: msg,
+            id: self.id,
+            item: self.item,
+            list: self.list
+        };
+
+        Err(ErrorNotFound(
+            serde_json::to_string(&json_response).unwrap()))
+    }
+
+    pub(crate) fn internal_error(self, msg: String) -> Result<Json<JsonResponse<T>>, Error>  {
+
+        let json_response = JsonResponse {
+            message: msg,
+            id: self.id,
+            item: self.item,
+            list: self.list
+        };
+
+        Err(ErrorInternalServerError(
+            serde_json::to_string(&json_response).unwrap()))
+    }
+
+    pub(crate) fn conflict(self, msg: String) -> Result<Json<JsonResponse<T>>, Error>  {
+
+        let json_response = JsonResponse {
+            message: msg,
+            id: self.id,
+            item: self.item,
+            list: self.list
+        };
+
+        Err(ErrorConflict(
+            serde_json::to_string(&json_response).unwrap()))
     }
 }
 
@@ -81,94 +118,87 @@ impl<T> From<T> for JsonResponseBuilder<T>
 }
 
 impl<T> From<Vec<T>> for JsonResponseBuilder<T>
-where T: serde::Serialize + Default {
+    where T: serde::Serialize + Default {
     fn from(value: Vec<T>) -> Self {
         JsonResponseBuilder::default().set_list(value)
     }
 }
 
 impl<T> JsonResponse<T>
+where T: serde::Serialize + Default
 {
-    pub(crate) fn new(status: String,
-                      message: String,
-                      code: u32,
+    pub fn build() -> JsonResponseBuilder<T>
+    {
+        JsonResponseBuilder::default()
+    }
+    pub(crate) fn new(message: String,
                       id: Option<i32>,
                       item:Option<T>,
                       list: Option<Vec<T>>) -> Self {
         tracing::debug!("Executed..");
         JsonResponse {
-            status,
             message,
-            code,
             id,
             item,
             list,
         }
     }
 
-    pub(crate) fn ok(id: i32, message: &str) -> JsonResponse<T> {
-
-        let msg = if !message.trim().is_empty() {
-            message.to_string()
-        }
-        else{
-            String::from("Success")
-        };
-
-        JsonResponse {
-            status: "OK".to_string(),
-            message: msg,
-            code: 200,
-            id: Some(id),
-            item: None,
-            list: None,
-        }
-    }
-
-    pub(crate) fn not_found() -> Self {
-        JsonResponse {
-            status: "Error".to_string(),
-            code: 404,
-            id: None,
-            item: None,
-            message: format!("Object not found"),
-            list: None,
-        }
-    }
-
-    pub(crate) fn internal_error(message: &str) -> Self {
-
-        let msg = if !message.trim().is_empty() {
-            message.to_string()
-        }
-        else{
-            String::from("Internal error")
-        };
-        JsonResponse {
-            status: "Error".to_string(),
-            code: 500,
-            id: None,
-            item: None,
-            message: msg,
-            list: None,
-        }
-    }
-
-    pub(crate) fn not_valid(message: &str) -> Self {
-
-        let msg = if !message.trim().is_empty() {
-            message.to_string()
-        }
-        else{
-            String::from("Validation error")
-        };
-        JsonResponse {
-            status: "Error".to_string(),
-            code: 400,
-            id: None,
-            item: None,
-            message: msg,
-            list: None,
-        }
-    }
+    // pub(crate) fn ok(id: i32, message: &str) -> JsonResponse<T> {
+    //
+    //     let msg = if !message.trim().is_empty() {
+    //         message.to_string()
+    //     }
+    //     else{
+    //         String::from("Success")
+    //     };
+    //
+    //     JsonResponse {
+    //         message: msg,
+    //         id: Some(id),
+    //         item: None,
+    //         list: None,
+    //     }
+    // }
+
+    // pub(crate) fn not_found() -> Self {
+    //     JsonResponse {
+    //         id: None,
+    //         item: None,
+    //         message: format!("Object not found"),
+    //         list: None,
+    //     }
+    // }
+
+    // pub(crate) fn internal_error(message: &str) -> Self {
+    //
+    //     let msg = if !message.trim().is_empty() {
+    //         message.to_string()
+    //     }
+    //     else{
+    //         String::from("Internal error")
+    //     };
+    //     JsonResponse {
+    //         id: None,
+    //         item: None,
+    //         message: msg,
+    //         list: None,
+    //     }
+    // }
+    //
+    // pub(crate) fn not_valid(message: &str) -> Self {
+    //
+    //     let msg = if !message.trim().is_empty() {
+    //         message.to_string()
+    //     }
+    //     else{
+    //         String::from("Validation error")
+    //     };
+    //     JsonResponse {
+    //         id: None,
+    //         item: None,
+    //         message: msg,
+    //         list: None,
+    //     }
+    // }
 }
\ No newline at end of file
diff --git a/src/helpers/stack/builder.rs b/src/helpers/stack/builder.rs
index 3986650d..c319d8a9 100644
--- a/src/helpers/stack/builder.rs
+++ b/src/helpers/stack/builder.rs
@@ -7,18 +7,10 @@ use crate::helpers::stack::dctypes::{
     Services
 };
 use serde_yaml;
-use crate::forms;
-use crate::forms::{
-    StackForm,
-    Web,
-    Feature
-};
+use crate::forms::{StackForm, stack};
 use crate::models::stack::Stack;
-
 #[derive(Clone, Debug)]
-struct Config {
-
-}
+struct Config {}
 
 impl Default for Config {
     fn default() -> Self {
@@ -33,26 +25,13 @@ pub struct DcBuilder {
     pub(crate) stack: Stack
 }
 
-impl TryInto<Vec<Port>> for Web {
+impl TryInto<Vec<Port>> for stack::Ports {
     type Error = String;
     fn try_into(self) -> Result<Vec<Port>, Self::Error> {
         convert_shared_ports(self.shared_ports.clone().unwrap())
     }
 }
 
-impl TryInto<Vec<Port>> for &Feature {
-    type Error = String;
-    fn try_into(self) -> Result<Vec<Port>, Self::Error> {
-        convert_shared_ports(self.shared_ports.clone().unwrap())
-    }
-}
-
-impl TryInto<Vec<Port>> for &forms::stack::Service {
-    type Error = String;
-    fn try_into(self) -> Result<Vec<Port>, Self::Error> {
-        convert_shared_ports(self.shared_ports.clone().unwrap())
-    }
-}
 
 fn convert_shared_ports(ports: Vec<String>) -> Result<Vec<Port>, String> {
     let mut _ports: Vec<Port> = vec![];
@@ -78,6 +57,7 @@ impl DcBuilder {
         }
     }
 
+
     pub fn build(&self) -> Option<String> {
 
         tracing::debug!("Start build docker compose from {:?}", &self.stack.body);
@@ -87,25 +67,16 @@ impl DcBuilder {
             Ok(apps) => {
                 println!("stack item {:?}", apps.custom.web);
 
-                for app in apps.custom.web {
-                    let tag = "latest";
-                    let dim = app.dockerhub_image.clone().unwrap_or("".to_string());
-                    let img= format!("{}/{}:{}",
-                                     app.dockerhub_user.clone()
-                                         .unwrap_or("trydirect".to_string()).clone(),
-                                     app.dockerhub_name.clone().unwrap_or(dim),
-                                     tag
-                    );
-                    let code = app.code.clone().to_owned();
-
+                for app_type in apps.custom.web {
+                    let code = app_type.app.code.clone().to_owned();
                     let mut service = Service {
-                        image: Some(img.to_string()),
+                        image: Some(app_type.app.docker_image.to_string()),
                         ..Default::default()
                     };
 
-                    if let Some(ports) = &app.shared_ports {
-                        if !ports.is_empty() {
-                            service.ports = Ports::Long(app.try_into().unwrap())
+                    if let Some(ports) = &app_type.app.ports {
+                        if !ports.shared_ports.clone()?.is_empty() {
+                            service.ports = Ports::Long(app_type.app.ports?.try_into().unwrap())
                         }
                     }
 
@@ -116,22 +87,20 @@ impl DcBuilder {
                     );
                 }
 
-                if let Some(srvs) = &apps.custom.service {
+                if let Some(srvs) = apps.custom.service {
 
                     if !srvs.is_empty() {
 
-                        for app in srvs {
-                            let code = app.code.to_owned();
-                            let tag = "latest";
-
+                        for app_type in srvs {
+                            let code = app_type.app.code.to_owned();
                             let mut service = Service {
-                                image: Some(app.dockerhub_image.as_ref().unwrap().to_owned()),
+                                image: Some(app_type.app.docker_image.to_string()),
                                 ..Default::default()
                             };
 
-                            if let Some(ports) = &app.shared_ports {
-                                if !ports.is_empty() {
-                                    service.ports = Ports::Long(app.try_into().unwrap())
+                            if let Some(ports) = &app_type.app.ports {
+                                if !ports.shared_ports.clone()?.is_empty() {
+                                    service.ports = Ports::Long(app_type.app.ports?.try_into().unwrap())
                                 }
                             }
                             service.restart = Some("always".to_owned());
@@ -142,20 +111,21 @@ impl DcBuilder {
                         }
                     }
                 }
-                if let Some(features) = &apps.custom.feature {
+                if let Some(features) = apps.custom.feature {
 
                     if !features.is_empty() {
 
-                        for app in features {
-                            let code = app.code.to_owned();
+                        for app_type in features {
+                            let code = app_type.app.code.to_owned();
                             let mut service = Service {
-                                image: Some(app.dockerhub_image.as_ref().unwrap().to_owned()),
+                                // image: Some(app.dockerhub_image.as_ref().unwrap().to_owned()),
+                                image: Some(app_type.app.docker_image.to_string()),
                                 ..Default::default()
                             };
 
-                            if let Some(ports) = &app.shared_ports {
-                                if !ports.is_empty() {
-                                    service.ports = Ports::Long(app.try_into().unwrap())
+                            if let Some(ports) = &app_type.app.ports {
+                                if !ports.shared_ports.clone()?.is_empty() {
+                                    service.ports = Ports::Long(app_type.app.ports?.try_into().unwrap())
                                 }
                             }
                             service.restart = Some("always".to_owned());
@@ -168,6 +138,7 @@ impl DcBuilder {
                 }
             }
             Err(e) => {
+                tracing::debug!("Unpack stack form {:?}", e);
                 ()
             }
         }
diff --git a/src/models/rating.rs b/src/models/rating.rs
index 9a3f00ba..dedf8fdc 100644
--- a/src/models/rating.rs
+++ b/src/models/rating.rs
@@ -16,7 +16,7 @@ pub struct Product {
     pub updated_at: DateTime<Utc>,
 }
 
-#[derive(Debug, Serialize)]
+#[derive(Debug, Serialize, Default)]
 pub struct Rating {
     pub id: i32,
     pub user_id: String,         // external user_id, 100, taken using token (middleware?)
diff --git a/src/routes/client/add.rs b/src/routes/client/add.rs
index ae822f86..8b7eded1 100644
--- a/src/routes/client/add.rs
+++ b/src/routes/client/add.rs
@@ -39,14 +39,12 @@ pub async fn add_handler(
                     client_count
                 );
 
-                return Ok(web::Json(JsonResponse::not_valid(
-                    "Too many clients already created"))
-                );
+                return JsonResponse::build().err("Too many clients created".to_owned());
             }
         }
         Err(e) => {
             tracing::error!("Failed to execute query: {:?}", e);
-            return Ok(web::Json(JsonResponse::internal_error("Failed to insert")));
+            return JsonResponse::build().internal_error("Failed to insert".to_owned());
         }
     };
 
@@ -72,18 +70,16 @@ pub async fn add_handler(
         Ok(result) => {
             tracing::info!("New client {} have been saved to database", result.id);
             client.id = result.id;
-            Ok(web::Json(JsonResponse::new(
-                "success".to_string(),
-                "".to_string(),
-                200,
-                Some(client.id),
-                Some(client),
-                None
-            )))
+
+            return JsonResponse::build()
+                .set_id(client.id)
+                .set_item(Some(client))
+                .ok("OK".to_owned());
         }
         Err(e) => {
             tracing::error!("Failed to execute query: {:?}", e);
-            Ok(web::Json(JsonResponse::internal_error("Failed to insert")))
+            let err = format!("Failed to insert. {}", e);
+            return JsonResponse::build().err(err);
         }
     }
 }
diff --git a/src/routes/rating/add.rs b/src/routes/rating/add.rs
index 382b5649..eab47d10 100644
--- a/src/routes/rating/add.rs
+++ b/src/routes/rating/add.rs
@@ -36,7 +36,8 @@ pub async fn add_handler(
         }
         Err(e) => {
             tracing::error!("Failed to fetch product: {:?}, error: {:?}", form.obj_id, e);
-            return Ok(web::Json(JsonResponse::<models::Rating>::not_found()));
+            return JsonResponse::<models::Rating>::build()
+                .err(format!("Object not found {}", form.obj_id));
         }
     };
 
@@ -59,20 +60,12 @@ pub async fn add_handler(
                 form.obj_id,
                 form.category
             );
-
-            return Ok(web::Json(JsonResponse::<models::Rating>::new(
-                "Error".to_string(),
-                "Already rated".to_string(),
-                409,
-                Some(record.id),
-                None,
-                None
-            )));
+            return JsonResponse::build().conflict("Already rated".to_owned());
         }
         Err(sqlx::Error::RowNotFound) => {}
         Err(e) => {
             tracing::error!("Failed to fetch rating, error: {:?}", e);
-            return Ok(web::Json(JsonResponse::<models::Rating>::internal_error("")));
+            return JsonResponse::build().err(format!("Internal Server Error"));
         }
     }
 
@@ -99,11 +92,14 @@ pub async fn add_handler(
     {
         Ok(result) => {
             tracing::info!("New rating {} have been saved to database", result.id);
-            return Ok(web::Json(JsonResponse::<models::Rating>::ok(result.id, "Saved")));
+
+            return JsonResponse::build()
+                .set_id(result.id)
+                .ok("Saved".to_owned());
         }
         Err(e) => {
             tracing::error!("Failed to execute query: {:?}", e);
-            return Ok(web::Json(JsonResponse::internal_error("Failed to insert")));
+            return JsonResponse::build().internal_error("Failed to insert".to_owned());
         }
     }
 }
diff --git a/src/routes/rating/get.rs b/src/routes/rating/get.rs
index 19d93998..86002f8d 100644
--- a/src/routes/rating/get.rs
+++ b/src/routes/rating/get.rs
@@ -1,9 +1,9 @@
 use crate::models;
 use actix_web::{get, web, Responder, Result};
+use serde_derive::Serialize;
 use sqlx::PgPool;
 use tracing::Instrument;
 use crate::helpers::JsonResponse;
-use crate::models::user::User;
 
 // workflow
 // add, update, list, get(user_id), ACL,
@@ -30,22 +30,15 @@ pub async fn get_handler(
     .await
     {
         Ok(rating) => {
-            tracing::info!("rating found: {:?}", rating.id,);
-            return Ok(web::Json(JsonResponse::new(
-                "Success".to_string(),
-                "Rating found".to_string(),
-                200,
-                Some(rating.id),
-                Some(rating),
-                None
-            )));
+            tracing::info!("rating found: {:?}", rating.id);
+            return JsonResponse::build().set_item(Some(rating)).ok("OK".to_string());
         }
         Err(sqlx::Error::RowNotFound) => {
-            return Ok(web::Json(JsonResponse::not_found()));
+            return JsonResponse::build().err("Not Found".to_string());
         }
         Err(e) => {
             tracing::error!("Failed to fetch rating, error: {:?}", e);
-            return Ok(web::Json(JsonResponse::internal_error("")));
+            return JsonResponse::build().err("Internal Server Error".to_string());
         }
     }
 }
@@ -70,21 +63,14 @@ pub async fn list_handler(
     {
         Ok(rating) => {
             tracing::info!("Ratings found: {:?}", rating.len());
-            return Ok(web::Json(JsonResponse::new(
-                "Success".to_string(),
-                "".to_string(),
-                200,
-                None,
-                None,
-                Some(rating),
-            )));
+            return JsonResponse::build().set_list(rating).ok("OK".to_owned());
         }
         Err(sqlx::Error::RowNotFound) => {
-            return Ok(web::Json(JsonResponse::not_found()));
+            return JsonResponse::build().not_found("Not Found".to_owned());
         }
         Err(e) => {
             tracing::error!("Failed to fetch rating, error: {:?}", e);
-            return Ok(web::Json(JsonResponse::internal_error("")));
+            return JsonResponse::build().internal_error("Internal Server Error".to_owned());
         }
     }
 }
diff --git a/src/routes/stack/add.rs b/src/routes/stack/add.rs
index 2ae44a42..841d41b2 100644
--- a/src/routes/stack/add.rs
+++ b/src/routes/stack/add.rs
@@ -1,13 +1,12 @@
 use actix_web::{
     web,
-    web::{Bytes, Data, Json},
+    web::{Bytes, Data},
     Responder, Result,
 };
 
 use crate::forms::stack::StackForm;
 use crate::helpers::JsonResponse;
 use crate::models::user::User;
-use crate::models::Stack;
 use actix_web::post;
 use chrono::Utc;
 use serde_json::Value;
@@ -33,7 +32,7 @@ pub async fn add(
         }
         Err(_err) => {
             let msg = format!("Invalid data. {:?}", _err);
-            return Ok(Json(JsonResponse::<Stack>::not_valid(msg.as_str())));
+            return JsonResponse::<StackForm>::build().err("Invalid data".to_owned());
         }
     };
 
@@ -88,11 +87,11 @@ pub async fn add(
                 "req_id: {} New stack details have been saved to database",
                 request_id
             );
-            Ok(Json(JsonResponse::<Stack>::ok(record.id, "Object saved")))
+            return JsonResponse::build().set_id(record.id).ok("OK".to_owned());
         }
-        Err(err) => {
-            tracing::error!("req_id: {} Failed to execute query: {:?}", request_id, err);
-            Ok(Json(JsonResponse::<Stack>::not_valid("Failed to insert")))
+        Err(e) => {
+            tracing::error!("req_id: {} Failed to execute query: {:?}", request_id, e);
+            return JsonResponse::build().err("Failed to fetch".to_owned());
         }
     };
 }
diff --git a/src/routes/stack/compose.rs b/src/routes/stack/compose.rs
index 555d7dca..4d19a3f5 100644
--- a/src/routes/stack/compose.rs
+++ b/src/routes/stack/compose.rs
@@ -53,19 +53,15 @@ pub async fn add(
             let id = stack.id.clone();
             let mut dc = DcBuilder::new(stack);
             let fc = dc.build();
-            // tracing::debug!("Docker compose file content {:?}", fc.unwrap());
-            return Ok(Json(JsonResponse::new(
-                "OK".to_owned(),
-                "Success".to_owned(),
-                200,
-                Some(id),
-                Some(fc.unwrap()),
-                None
-            )));
+            tracing::debug!("Docker compose file content {:?}", fc);
 
+            return JsonResponse::build()
+                .set_id(id)
+                .set_item(fc.unwrap())
+                .ok("Success".to_owned());
         }
         None => {
-            return Ok(Json(JsonResponse::internal_error("Could not generate compose file")));
+            return JsonResponse::build().err("Could not generate compose file".to_owned());
         }
     }
 }
@@ -110,19 +106,14 @@ pub async fn admin(
             let id = stack.id.clone();
             let mut dc = DcBuilder::new(stack);
             let fc = dc.build();
-            // tracing::debug!("Docker compose file content {:?}", fc.unwrap());
-            return Ok(Json(JsonResponse::new(
-                "OK".to_owned(),
-                "Success".to_owned(),
-                200,
-                Some(id),
-                Some(fc.unwrap()),
-                None
-            )));
+            // tracing::debug!("Docker compose file content {:?}", fc);
+            return JsonResponse::build()
+                .set_id(id)
+                .set_item(fc.unwrap()).ok("Success".to_owned());
 
         }
         None => {
-            return Ok(Json(JsonResponse::internal_error("Could not generate compose file")));
+            return JsonResponse::build().err("Could not generate compose file".to_owned());
         }
     }
 }
diff --git a/src/routes/stack/deploy.rs b/src/routes/stack/deploy.rs
index aaf71353..45c1b386 100644
--- a/src/routes/stack/deploy.rs
+++ b/src/routes/stack/deploy.rs
@@ -2,23 +2,21 @@ use std::sync::Arc;
 use actix_web::{
     web,
     post,
-    web::{Data, Json},
+    web::Data,
     Responder, Result,
 };
 use crate::models::user::User;
 use crate::models::stack::Stack;
 use sqlx::PgPool;
 use lapin::{
-    options::*, publisher_confirm::Confirmation, types::FieldTable, BasicProperties, Connection,
+    options::*, publisher_confirm::Confirmation, BasicProperties, Connection,
     ConnectionProperties
 };
 use crate::configuration::Settings;
 use crate::helpers::JsonResponse;
 use crate::helpers::stack::builder::DcBuilder;
 use futures_lite::stream::StreamExt;
-use serde::Serialize;
-use crate::forms::{StackForm, StackPayload};
-
+use crate::forms::StackPayload;
 
 
 #[tracing::instrument(name = "Deploy for every user. Admin endpoint")]
@@ -78,7 +76,6 @@ pub async fn add(
             ).unwrap();
 
             stack_data.id = Some(id);
-            stack_data.installation_id = None;
             stack_data.user_token = Some(user.id.clone());
             stack_data.user_email = Some(user.email.clone());
             stack_data.stack_code = stack_data.custom.custom_stack_code.clone();
@@ -99,18 +96,10 @@ pub async fn add(
 
             assert_eq!(confirm, Confirmation::NotRequested);
             tracing::debug!("Message sent to rabbitmq");
-
-            Ok(Json(JsonResponse::<Stack>::new(
-                "OK".to_owned(),
-                "Success".to_owned(),
-                200,
-                Some(id),
-                None,
-                None
-            )))
+            return JsonResponse::<Stack>::build().set_id(id).ok("Success".to_owned());
         }
         None => {
-            Ok(Json(JsonResponse::internal_error("Deployment failed")))
+            JsonResponse::build().internal_error("Deployment failed".to_owned())
         }
     }
 }
diff --git a/src/routes/stack/get.rs b/src/routes/stack/get.rs
index 24230b97..271d2dcb 100644
--- a/src/routes/stack/get.rs
+++ b/src/routes/stack/get.rs
@@ -30,28 +30,15 @@ pub async fn item(
     .await
     {
         Ok(stack) => {
-            tracing::info!("stack found: {:?}", stack.id,);
-            // return Ok(web::Json(JsonResponse::<models::Stack>::new(
-            //     "Success".to_string(),
-            //     "".to_string(),
-            //     200,
-            //     Some(stack.id),
-            //     Some(stack),
-            //     None)));
-            let response_builder:JsonResponseBuilder<models::Stack> = From::from(stack);
-            // let response: JsonResponse<models::Stack> = From::<models::Stack>::from(stack).build();
-            return response_builder.ok();
+            tracing::info!("Stack found: {:?}", stack.id,);
+            return JsonResponse::build().set_item(Some(stack)).ok("OK".to_owned());
         }
         Err(sqlx::Error::RowNotFound) => {
-            // return Ok(web::Json(JsonResponse::<models::Stack>::not_found()));
-            let response_builder:JsonResponseBuilder<models::Stack> =JsonResponseBuilder::default();
-            return response_builder.ok();
+            JsonResponse::build().not_found("Record not found".to_owned())
         }
         Err(e) => {
             tracing::error!("Failed to fetch stack, error: {:?}", e);
-            // return Ok(web::Json(JsonResponse::<models::Stack>::internal_error("")));
-            let response_builder:JsonResponseBuilder<models::Stack> = JsonResponseBuilder::default();
-            return response_builder.ok();
+            return JsonResponse::build().internal_error("Could not fetch data".to_owned());
         }
     }
 }
@@ -86,18 +73,15 @@ pub async fn list(
         .await
     {
         Ok(list) => {
-            let response_builder:JsonResponseBuilder<models::Stack> = JsonResponseBuilder::new();
-            return response_builder.set_list(list).ok();
+            return JsonResponse::build().set_list(list).ok("OK".to_string());
         }
         Err(sqlx::Error::RowNotFound) => {
             tracing::error!("No stacks found for user: {:?}", &user.id);
-            let response_builder:JsonResponseBuilder<models::Stack> = JsonResponseBuilder::default();
-            return response_builder.ok();
+            return JsonResponse::build().not_found("No stacks found for user".to_string())
         }
         Err(e) => {
             tracing::error!("Failed to fetch stack, error: {:?}", e);
-            let response_builder:JsonResponseBuilder<models::Stack> = JsonResponseBuilder::default();
-            return response_builder.ok();
+            return JsonResponse::build().internal_error("Could not fetch".to_string());
         }
     }
 }

From 0477d1d6ba8110d000ef3c0fc4b3f7a97d81b0eb Mon Sep 17 00:00:00 2001
From: vsilent <jabberroid@gmail.com>
Date: Mon, 20 Nov 2023 12:02:12 +0200
Subject: [PATCH 17/18] move json testing payloads into tests dir

---
 custom-stack-payload-2.json => tests/custom-stack-payload-2.json  | 0
 custom-stack-payload-3.json => tests/custom-stack-payload-3.json  | 0
 custom-stack-payload-4.json => tests/custom-stack-payload-4.json  | 0
 custom-stack-payload-5.json => tests/custom-stack-payload-5.json  | 0
 custom-stack-payload-6.json => tests/custom-stack-payload-6.json  | 0
 custom-stack-payload-7.json => tests/custom-stack-payload-7.json  | 0
 custom-stack-payload-8.json => tests/custom-stack-payload-8.json  | 0
 custom-stack-payload-9.json => tests/custom-stack-payload-9.json  | 0
 .../custom-stack-payload-singleapp.json                           | 0
 custom-stack-payload.json => tests/custom-stack-payload.json      | 0
 10 files changed, 0 insertions(+), 0 deletions(-)
 rename custom-stack-payload-2.json => tests/custom-stack-payload-2.json (100%)
 rename custom-stack-payload-3.json => tests/custom-stack-payload-3.json (100%)
 rename custom-stack-payload-4.json => tests/custom-stack-payload-4.json (100%)
 rename custom-stack-payload-5.json => tests/custom-stack-payload-5.json (100%)
 rename custom-stack-payload-6.json => tests/custom-stack-payload-6.json (100%)
 rename custom-stack-payload-7.json => tests/custom-stack-payload-7.json (100%)
 rename custom-stack-payload-8.json => tests/custom-stack-payload-8.json (100%)
 rename custom-stack-payload-9.json => tests/custom-stack-payload-9.json (100%)
 rename custom-stack-payload-singleapp.json => tests/custom-stack-payload-singleapp.json (100%)
 rename custom-stack-payload.json => tests/custom-stack-payload.json (100%)

diff --git a/custom-stack-payload-2.json b/tests/custom-stack-payload-2.json
similarity index 100%
rename from custom-stack-payload-2.json
rename to tests/custom-stack-payload-2.json
diff --git a/custom-stack-payload-3.json b/tests/custom-stack-payload-3.json
similarity index 100%
rename from custom-stack-payload-3.json
rename to tests/custom-stack-payload-3.json
diff --git a/custom-stack-payload-4.json b/tests/custom-stack-payload-4.json
similarity index 100%
rename from custom-stack-payload-4.json
rename to tests/custom-stack-payload-4.json
diff --git a/custom-stack-payload-5.json b/tests/custom-stack-payload-5.json
similarity index 100%
rename from custom-stack-payload-5.json
rename to tests/custom-stack-payload-5.json
diff --git a/custom-stack-payload-6.json b/tests/custom-stack-payload-6.json
similarity index 100%
rename from custom-stack-payload-6.json
rename to tests/custom-stack-payload-6.json
diff --git a/custom-stack-payload-7.json b/tests/custom-stack-payload-7.json
similarity index 100%
rename from custom-stack-payload-7.json
rename to tests/custom-stack-payload-7.json
diff --git a/custom-stack-payload-8.json b/tests/custom-stack-payload-8.json
similarity index 100%
rename from custom-stack-payload-8.json
rename to tests/custom-stack-payload-8.json
diff --git a/custom-stack-payload-9.json b/tests/custom-stack-payload-9.json
similarity index 100%
rename from custom-stack-payload-9.json
rename to tests/custom-stack-payload-9.json
diff --git a/custom-stack-payload-singleapp.json b/tests/custom-stack-payload-singleapp.json
similarity index 100%
rename from custom-stack-payload-singleapp.json
rename to tests/custom-stack-payload-singleapp.json
diff --git a/custom-stack-payload.json b/tests/custom-stack-payload.json
similarity index 100%
rename from custom-stack-payload.json
rename to tests/custom-stack-payload.json

From 96a5538bc7b4ba8dcd6fb197db7140de1e8ffe5c Mon Sep 17 00:00:00 2001
From: vsilent <jabberroid@gmail.com>
Date: Wed, 22 Nov 2023 09:38:03 +0200
Subject: [PATCH 18/18] cleanup

---
 src/startup.rs | 9 ---------
 1 file changed, 9 deletions(-)

diff --git a/src/startup.rs b/src/startup.rs
index c295136b..ca705dbd 100644
--- a/src/startup.rs
+++ b/src/startup.rs
@@ -114,15 +114,6 @@ pub async fn run(
                     .service(crate::routes::rating::get_handler)
                     .service(crate::routes::rating::list_handler),
             )
-            // .service(
-            //     web::resource("/stack/{id}")
-            //         .route(web::get()
-            //             .to(crate::routes::stack::get))
-            //         .route(web::post()
-            //             .to(crate::routes::stack::update))
-            //         .route(web::post()
-            //             .to(crate::routes::stack::add)),
-            // )
             .service(
                 web::scope("/stack")
                     .wrap(HttpAuthentication::bearer(bearer_guard))
