diff --git a/Cargo.lock b/Cargo.lock index 6c5834b6..73e0b28a 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2803,6 +2803,16 @@ dependencies = [ "serde", ] +[[package]] +name = "serde_path_to_error" +version = "0.1.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4beec8bce849d58d06238cb50db2e1c417cfeafa4c63f692b15c82b7c80f8335" +dependencies = [ + "itoa", + "serde", +] + [[package]] name = "serde_qs" version = "0.8.5" @@ -3107,6 +3117,7 @@ dependencies = [ "serde", "serde_derive", "serde_json", + "serde_path_to_error", "serde_valid", "serde_yaml", "sha2", diff --git a/Cargo.toml b/Cargo.toml index b23bf3b2..54ee40f6 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -52,6 +52,7 @@ serde_yaml = "0.9" lapin = { version = "2.3.1", features = ["serde_json"] } futures-lite = "1.13.0" clap = { version = "4.4.8", features = ["derive"] } +serde_path_to_error = "0.1.14" [dependencies.sqlx] version = "0.6.3" diff --git a/migrations/20230903063840_creating_rating_tables.down.sql b/migrations/20230903063840_creating_rating_tables.down.sql index e12e4ab3..b32b52b5 100644 --- a/migrations/20230903063840_creating_rating_tables.down.sql +++ b/migrations/20230903063840_creating_rating_tables.down.sql @@ -6,3 +6,5 @@ DROP INDEX idx_obj_id_rating_id; DROP table rating; DROP table product; + +DROP TYPE rate_category; diff --git a/migrations/20230903063840_creating_rating_tables.up.sql b/migrations/20230903063840_creating_rating_tables.up.sql index 579bef6f..28422706 100644 --- a/migrations/20230903063840_creating_rating_tables.up.sql +++ b/migrations/20230903063840_creating_rating_tables.up.sql @@ -1,5 +1,17 @@ -- Add up migration script here +CREATE TYPE rate_category AS ENUM ( + 'application', + 'cloud', + 'stack', + 'deploymentSpeed', + 'documentation', + 'design', + 'techSupport', + 'price', + 'memoryUsage' +); + CREATE TABLE product ( id integer NOT NULL, PRIMARY KEY(id), obj_id integer NOT NULL, @@ -12,7 +24,7 @@ CREATE TABLE rating ( id serial, user_id VARCHAR(50) NOT NULL, obj_id integer NOT NULL, - category VARCHAR(255) NOT NULL, + category rate_category NOT NULL, comment TEXT DEFAULT NULL, hidden BOOLEAN DEFAULT FALSE, rate INTEGER, diff --git a/src/console/commands/appclient/new.rs b/src/console/commands/appclient/new.rs index 448ac788..ada01acb 100644 --- a/src/console/commands/appclient/new.rs +++ b/src/console/commands/appclient/new.rs @@ -26,7 +26,7 @@ impl crate::console::commands::CallableTrait for NewCommand { //todo get user from trydirect let user = crate::models::user::User { - id: "first_name".to_string(), + id: format!("{}", self.user_id), first_name: "first_name".to_string(), last_name: "last_name".to_string(), email: "email".to_string(), diff --git a/src/db/client.rs b/src/db/client.rs new file mode 100644 index 00000000..b8307a72 --- /dev/null +++ b/src/db/client.rs @@ -0,0 +1,105 @@ +use sqlx::PgPool; +use crate::models; +use tracing::Instrument; + +pub async fn update(pool: &PgPool, client: models::Client) -> Result { + let query_span = tracing::info_span!("Updating client into the database"); + sqlx::query!( + r#" + UPDATE client + SET + secret=$1, + updated_at=NOW() at time zone 'utc' + WHERE id = $2 + "#, + client.secret, + client.id + ) + .execute(pool) + .instrument(query_span) + .await + .map(|_|{ + tracing::info!("Client {} have been saved to database", client.id); + client + }) + .map_err(|err| { + tracing::error!("Failed to execute query: {:?}", err); + "".to_string() + }) +} + +pub async fn fetch(pool: &PgPool, id: i32) -> Result, String> { + let query_span = tracing::info_span!("Fetching the client by ID"); + sqlx::query_as!( + models::Client, + r#" + SELECT + id, + user_id, + secret + FROM client c + WHERE c.id = $1 + LIMIT 1 + "#, + id, + ) + .fetch_one(pool) + .instrument(query_span) + .await + .map(|client| Some(client)) + .or_else(|e| { + match e { + sqlx::Error::RowNotFound => Ok(None), + s => { + tracing::error!("Failed to execute fetch query: {:?}", s); + Err("".to_string()) + } + } + }) +} + +pub async fn count_by_user(pool: &PgPool , user_id: &String) -> Result { + let query_span = tracing::info_span!("Counting the user's clients"); + + sqlx::query!( + r#" + SELECT + count(*) as client_count + FROM client c + WHERE c.user_id = $1 + "#, + user_id.clone(), + ) + .fetch_one(pool) + .instrument(query_span) + .await + .map(|result| {result.client_count.unwrap()}) + .map_err(|err| { + tracing::error!("Failed to execute query: {:?}", err); + "Internal Server Error".to_string() + }) +} + +pub async fn insert(pool: &PgPool, mut client: models::Client) -> Result { + let query_span = tracing::info_span!("Saving new client into the database"); + sqlx::query!( + r#" + INSERT INTO client (user_id, secret, created_at, updated_at) + VALUES ($1, $2, NOW() at time zone 'utc', NOW() at time zone 'utc') + RETURNING id + "#, + client.user_id.clone(), + client.secret, + ) + .fetch_one(pool) + .instrument(query_span) + .await + .map(move |result| { + client.id = result.id; + client + }) + .map_err(|e| { + tracing::error!("Failed to execute query: {:?}", e); + "Failed to insert".to_string() + }) +} diff --git a/src/db/mod.rs b/src/db/mod.rs new file mode 100644 index 00000000..a4fe3ae7 --- /dev/null +++ b/src/db/mod.rs @@ -0,0 +1,4 @@ +pub mod client; +pub mod product; +pub mod rating; +pub mod stack; diff --git a/src/db/product.rs b/src/db/product.rs new file mode 100644 index 00000000..e9c591ae --- /dev/null +++ b/src/db/product.rs @@ -0,0 +1,30 @@ +use sqlx::PgPool; +use crate::models; +use tracing::Instrument; + +pub async fn fetch_by_obj(pg_pool: &PgPool, obj_id: i32) -> Result, String> { + let query_span = tracing::info_span!("Check product existence by id."); + sqlx::query_as!( + models::Product, + r#"SELECT + * + FROM product + WHERE obj_id = $1 + LIMIT 1 + "#, + obj_id + ) + .fetch_one(pg_pool) + .instrument(query_span) + .await + .map(|product| Some(product)) + .or_else(|e| { + match e { + sqlx::Error::RowNotFound => Ok(None), + s => { + tracing::error!("Failed to execute fetch query: {:?}", s); + Err("".to_string()) + } + } + }) +} diff --git a/src/db/rating.rs b/src/db/rating.rs new file mode 100644 index 00000000..599c49cc --- /dev/null +++ b/src/db/rating.rs @@ -0,0 +1,133 @@ +use sqlx::PgPool; +use crate::models; +use tracing::Instrument; + +pub async fn fetch_all(pool: &PgPool) -> Result, String> { + let query_span = tracing::info_span!("Fetch all ratings."); + sqlx::query_as!( + models::Rating, + r#"SELECT + id, + user_id, + obj_id, + category as "category: _", + comment, + hidden, + rate, + created_at, + updated_at + FROM rating"# + ) + .fetch_all(pool) + .instrument(query_span) + .await + .map_err(|e| { + tracing::error!("Failed to execute fetch query: {:?}", e); + "".to_string() + }) +} + +pub async fn fetch(pool: &PgPool, id: i32) -> Result, String> { + let query_span = tracing::info_span!("Fetch rating by id"); + sqlx::query_as!( + models::Rating, + r#"SELECT + id, + user_id, + obj_id, + category as "category: _", + comment, + hidden, + rate, + created_at, + updated_at + FROM rating + WHERE id=$1 + LIMIT 1"#, + id + ) + .fetch_one(pool) + .instrument(query_span) + .await + .map(|rating| Some(rating)) + .or_else(|e| { + match e { + sqlx::Error::RowNotFound => Ok(None), + s => { + tracing::error!("Failed to execute fetch query: {:?}", s); + Err("".to_string()) + } + } + }) +} + +pub async fn fetch_by_obj_and_user_and_category( + pool: &PgPool, + obj_id: i32, + user_id: String, + category: models::RateCategory, +) -> Result, String> { + let query_span = tracing::info_span!("Fetch rating by obj, user and category."); + sqlx::query_as!( + models::Rating, + r#"SELECT + id, + user_id, + obj_id, + category as "category: _", + comment, + hidden, + rate, + created_at, + updated_at + FROM rating + WHERE user_id=$1 + AND obj_id=$2 + AND category=$3 + LIMIT 1"#, + user_id, + obj_id, + category as _ + ) + .fetch_one(pool) + .instrument(query_span) + .await + .map(|rating| Some(rating)) + .or_else(|e| { + match e { + sqlx::Error::RowNotFound => Ok(None), + s => { + tracing::error!("Failed to execute fetch query: {:?}", s); + Err("".to_string()) + } + } + }) +} + +pub async fn insert(pool: &PgPool, mut rating: models::Rating) -> Result { + let query_span = tracing::info_span!("Saving new rating details into the database"); + sqlx::query!( + r#" + INSERT INTO rating (user_id, obj_id, category, comment, hidden, rate, created_at, updated_at) + VALUES ($1, $2, $3, $4, $5, $6, NOW() at time zone 'utc', NOW() at time zone 'utc') + RETURNING id + "#, + rating.user_id, + rating.obj_id, + rating.category as _, + rating.comment, + rating.hidden, + rating.rate + ) + .fetch_one(pool) + .instrument(query_span) + .await + .map(move |result| { + rating.id = result.id; + rating + }) + .map_err(|e| { + tracing::error!("Failed to execute query: {:?}", e); + "Failed to insert".to_string() + }) +} diff --git a/src/db/stack.rs b/src/db/stack.rs new file mode 100644 index 00000000..92725cdb --- /dev/null +++ b/src/db/stack.rs @@ -0,0 +1,103 @@ +use crate::models; +use sqlx::PgPool; +use tracing::Instrument; + +pub async fn fetch(pool: &PgPool, id: i32) -> Result, String> { + tracing::info!("Fecth stack {}", id); + sqlx::query_as!( + models::Stack, + r#" + SELECT + * + FROM user_stack + WHERE id=$1 + LIMIT 1 + "#, + id + ) + .fetch_one(pool) + .await + .map(|stack| Some(stack)) + .or_else(|err| match err { + sqlx::Error::RowNotFound => Ok(None), + e => { + tracing::error!("Failed to fetch stack, error: {:?}", e); + Err("Could not fetch data".to_string()) + } + }) +} + +pub async fn fetch_by_user(pool: &PgPool, user_id: &str) -> Result, String> { + let query_span = tracing::info_span!("Fetch stacks by user id."); + sqlx::query_as!( + models::Stack, + r#" + SELECT + * + FROM user_stack + WHERE user_id=$1 + "#, + user_id + ) + .fetch_all(pool) + .instrument(query_span) + .await + .map_err(|err| { + tracing::error!("Failed to fetch stack, error: {:?}", err); + "".to_string() + }) +} + +pub async fn fetch_one_by_name(pool: &PgPool, name: &str) -> Result, String> { + let query_span = tracing::info_span!("Fetch one stack by name."); + sqlx::query_as!( + models::Stack, + r#" + SELECT + * + FROM user_stack + WHERE name=$1 + LIMIT 1 + "#, + name + ) + .fetch_one(pool) + .instrument(query_span) + .await + .map(|stack| Some(stack)) + .or_else(|err| match err { + sqlx::Error::RowNotFound => Ok(None), + err => { + tracing::error!("Failed to fetch one stack by name, error: {:?}", err); + Err("".to_string()) + } + }) +} + +pub async fn insert(pool: &PgPool, mut stack: models::Stack) -> Result { + let query_span = tracing::info_span!("Saving new stack into the database"); + sqlx::query!( + r#" + INSERT INTO user_stack (stack_id, user_id, name, body, created_at, updated_at) + VALUES ($1, $2, $3, $4, $5, $6) + RETURNING id; + "#, + stack.stack_id, + stack.user_id, + stack.name, + stack.body, + stack.created_at, + stack.updated_at, + ) + .fetch_one(pool) + .instrument(query_span) + .await + .map(move |result| { + stack.id = result.id; + stack + }) + .map_err(|e| { + tracing::error!("Failed to execute query: {:?}", e); + "Failed to insert".to_string() + }) +} diff --git a/src/forms/rating.rs b/src/forms/rating.rs index 76efca44..d7a4bd7e 100644 --- a/src/forms/rating.rs +++ b/src/forms/rating.rs @@ -12,3 +12,16 @@ pub struct Rating { #[validate(maximum = 10)] pub rate: i32, // } + +impl Into for Rating { + fn into(self) -> models::Rating { + let mut rating = models::Rating::default(); + rating.obj_id = self.obj_id; + rating.category = self.category.into(); //todo change the type of category field to the RateCategory + rating.hidden = Some(false); + rating.rate = Some(self.rate); + rating.comment = self.comment; + + rating + } +} diff --git a/src/forms/stack.rs b/src/forms/stack.rs index b67a8566..cc322330 100644 --- a/src/forms/stack.rs +++ b/src/forms/stack.rs @@ -1,10 +1,9 @@ -use std::collections::HashMap; use serde::{Deserialize, Serialize}; use serde_json::Value; use serde_valid::Validate; +use std::collections::HashMap; use std::fmt; - #[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize, Validate)] pub struct Role { pub role: Option>, @@ -12,18 +11,18 @@ pub struct Role { #[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize, Validate)] pub struct Requirements { - #[validate(min_length=1)] - #[validate(max_length=10)] + #[validate(min_length = 1)] + #[validate(max_length = 10)] #[validate(pattern = r"^\d+\.?[0-9]+$")] pub cpu: Option, - #[validate(min_length=1)] - #[validate(max_length=10)] + #[validate(min_length = 1)] + #[validate(max_length = 10)] #[validate(pattern = r"^\d+G$")] #[serde(rename = "disk_size")] pub disk_size: Option, #[serde(rename = "ram_size")] - #[validate(min_length=1)] - #[validate(max_length=10)] + #[validate(min_length = 1)] + #[validate(max_length = 10)] #[validate(pattern = r"^\d+G$")] pub ram_size: Option, } @@ -31,7 +30,7 @@ pub struct Requirements { #[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize)] pub struct Port { pub host_port: Option, - pub container_port: Option + pub container_port: Option, } // #[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize)] @@ -45,27 +44,31 @@ pub struct Port { #[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize, Validate)] pub struct DockerImage { - #[validate(min_length=3)] - #[validate(max_length=50)] + #[validate(min_length = 3)] + #[validate(max_length = 50)] pub dockerhub_user: Option, - #[validate(min_length=3)] - #[validate(max_length=50)] + #[validate(min_length = 3)] + #[validate(max_length = 50)] pub dockerhub_name: Option, - #[validate(min_length=3)] - #[validate(max_length=100)] + #[validate(min_length = 3)] + #[validate(max_length = 100)] pub dockerhub_image: Option, } -impl fmt::Display for DockerImage -{ +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 + 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 ) } } @@ -76,61 +79,59 @@ impl AsRef for App { } } - #[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize, Validate)] pub struct StackForm { // #[validate(min_length=2)] // #[validate(max_length=255)] - #[serde(rename= "commonDomain")] + #[serde(rename = "commonDomain")] pub common_domain: Option, pub domain_list: Option, - #[validate(min_length=2)] - #[validate(max_length=255)] + #[validate(min_length = 2)] + #[validate(max_length = 255)] pub stack_code: Option, - #[validate(min_length=2)] - #[validate(max_length=50)] + #[validate(min_length = 2)] + #[validate(max_length = 50)] pub region: String, - #[validate(min_length=2)] - #[validate(max_length=50)] + #[validate(min_length = 2)] + #[validate(max_length = 50)] pub zone: Option, - #[validate(min_length=2)] - #[validate(max_length=50)] + #[validate(min_length = 2)] + #[validate(max_length = 50)] pub server: String, - #[validate(min_length=2)] - #[validate(max_length=50)] + #[validate(min_length = 2)] + #[validate(max_length = 50)] pub os: String, - #[validate(min_length=3)] - #[validate(max_length=50)] + #[validate(min_length = 3)] + #[validate(max_length = 50)] pub ssl: String, pub vars: Option>, pub integrated_features: Option>, pub extended_features: Option>, pub subscriptions: Option>, pub form_app: Option>, - #[validate(min_length=3)] - #[validate(max_length=50)] + #[validate(min_length = 3)] + #[validate(max_length = 50)] pub disk_type: Option, pub save_token: bool, - #[validate(min_length=10)] - #[validate(max_length=255)] + #[validate(min_length = 10)] + #[validate(max_length = 255)] pub cloud_token: String, - #[validate(min_length=2)] - #[validate(max_length=50)] + #[validate(min_length = 2)] + #[validate(max_length = 50)] pub provider: String, - #[validate(min_length=3)] - #[validate(max_length=50)] + #[validate(min_length = 3)] + #[validate(max_length = 50)] 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) id: Option, pub(crate) user_token: Option, pub(crate) user_email: Option, - #[serde(rename= "commonDomain")] + #[serde(rename = "commonDomain")] pub common_domain: String, pub domain_list: Option, pub region: String, @@ -159,17 +160,15 @@ pub struct StackPayload { #[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize)] #[serde(rename_all = "camelCase")] -pub struct DomainList { -} +pub struct DomainList {} #[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize)] #[serde(rename_all = "camelCase")] -pub struct Var { -} +pub struct Var {} #[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize)] pub struct Price { - pub value: f64 + pub value: f64, } #[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize, Validate)] pub struct Custom { @@ -179,17 +178,17 @@ pub struct Custom { #[validate(minimum = 0)] #[validate(maximum = 10)] pub servers_count: u32, - #[validate(min_length=3)] - #[validate(max_length=50)] + #[validate(min_length = 3)] + #[validate(max_length = 50)] pub custom_stack_code: String, - #[validate(min_length=3)] - #[validate(max_length=255)] + #[validate(min_length = 3)] + #[validate(max_length = 255)] pub project_git_url: Option, pub custom_stack_category: Option>, pub custom_stack_short_description: Option, pub custom_stack_description: Option, - #[validate(min_length=3)] - #[validate(max_length=255)] + #[validate(min_length = 3)] + #[validate(max_length = 255)] pub project_name: String, pub project_overview: Option, pub project_description: Option, @@ -199,15 +198,14 @@ pub struct Custom { #[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize, Validate)] pub struct Network { - name: String + name: String, } - #[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize, Validate)] pub struct App { #[serde(rename = "_etag")] - #[validate(min_length=3)] - #[validate(max_length=255)] + #[validate(min_length = 3)] + #[validate(max_length = 255)] pub etag: Option, #[serde(rename = "_id")] pub id: u32, @@ -215,14 +213,14 @@ pub struct App { pub created: Option, #[serde(rename = "_updated")] pub updated: Option, - #[validate(min_length=3)] - #[validate(max_length=50)] + #[validate(min_length = 3)] + #[validate(max_length = 50)] pub name: String, - #[validate(min_length=3)] - #[validate(max_length=50)] + #[validate(min_length = 3)] + #[validate(max_length = 50)] pub code: String, - #[validate(min_length=3)] - #[validate(max_length=50)] + #[validate(min_length = 3)] + #[validate(max_length = 50)] #[serde(rename = "type")] pub type_field: String, #[serde(flatten)] @@ -233,7 +231,7 @@ pub struct App { pub docker_image: DockerImage, #[serde(flatten)] pub requirements: Requirements, - #[validate(minimum=1)] + #[validate(minimum = 1)] pub popularity: Option, pub commercial: Option, pub subscription: Option, @@ -271,28 +269,28 @@ pub struct App { #[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize)] pub struct Environment { - pub(crate) environment: Option>> + pub(crate) environment: Option>>, } #[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize)] pub struct Volume { pub(crate) host_path: Option, - pub(crate) container_path: Option + pub(crate) container_path: Option, } #[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize)] pub struct Volumes { - volumes: Vec + volumes: Vec, } // pub(crate) type Networks = Option>; #[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize)] pub struct ServiceNetworks { - pub network: Option> + pub network: Option>, } #[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize)] pub struct ComposeNetworks { - pub networks: Option> + pub networks: Option>, } #[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize)] @@ -337,8 +335,7 @@ pub struct IconLight { } #[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize)] -pub struct IconDark { -} +pub struct IconDark {} #[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize, Validate)] pub struct Version { @@ -352,14 +349,12 @@ pub struct Version { pub updated: Option, pub app_id: Option, pub name: String, - #[validate(min_length=3)] - #[validate(max_length=20)] + #[validate(min_length = 3)] + #[validate(max_length = 20)] pub version: String, #[serde(rename = "update_status")] pub update_status: Option, - #[validate(min_length=3)] - #[validate(max_length=20)] + #[validate(min_length = 3)] + #[validate(max_length = 20)] pub tag: String, } - - diff --git a/src/helpers/json.rs b/src/helpers/json.rs index 8990788d..952d7680 100644 --- a/src/helpers/json.rs +++ b/src/helpers/json.rs @@ -1,7 +1,6 @@ -use actix_web::error::{ErrorBadRequest, ErrorConflict, ErrorInternalServerError, ErrorNotFound}; +use actix_web::error::{ErrorBadRequest, ErrorConflict, ErrorInternalServerError, ErrorNotFound, ErrorUnauthorized}; use actix_web::web::Json; use actix_web::Error; -use actix_web::Result; use serde_derive::Serialize; #[derive(Serialize)] @@ -64,30 +63,37 @@ where serde_json::to_string(&json_response).unwrap() } - pub(crate) fn ok>(self, msg: I) -> Result>, Error> { - Ok(Json(self.set_msg(msg).to_json_response())) + pub(crate) fn ok>(self, msg: I) -> Json> { + Json(self.set_msg(msg).to_json_response()) } pub(crate) fn bad_request>( self, msg: I, - ) -> Result>, Error> { - Err(ErrorBadRequest(self.set_msg(msg).to_string())) + ) -> Error { + ErrorBadRequest(self.set_msg(msg).to_string()) } - pub(crate) fn not_found>(self, msg: I) -> Result>, Error> { - Err(ErrorNotFound(self.set_msg(msg).to_string())) + pub(crate) fn not_found>(self, msg: I) -> Error { + ErrorNotFound(self.set_msg(msg).to_string()) } pub(crate) fn internal_server_error>( self, msg: I, - ) -> Result>, Error> { - Err(ErrorInternalServerError(self.set_msg(msg).to_string())) + ) -> Error { + ErrorInternalServerError(self.set_msg(msg).to_string()) } - pub(crate) fn conflict>(self, msg: I) -> Result>, Error> { - Err(ErrorConflict(self.set_msg(msg).to_string())) + pub(crate) fn unauthorized>( + self, + msg: I, + ) -> Error { + ErrorUnauthorized(self.set_msg(msg).to_string()) + } + + pub(crate) fn conflict>(self, msg: I) -> Error { + ErrorConflict(self.set_msg(msg).to_string()) } } diff --git a/src/lib.rs b/src/lib.rs index 37e4fae9..3c6af7cc 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,5 +1,6 @@ pub mod configuration; pub mod console; +pub mod db; pub mod forms; pub mod helpers; mod middleware; diff --git a/src/middleware/client.rs b/src/middleware/client.rs index 27aa97b7..60bce270 100644 --- a/src/middleware/client.rs +++ b/src/middleware/client.rs @@ -1,7 +1,6 @@ use crate::helpers::JsonResponse; use crate::models::Client; use actix_http::header::CONTENT_LENGTH; -use actix_web::error::{ErrorForbidden, ErrorInternalServerError, ErrorNotFound, PayloadError}; use actix_web::web::BytesMut; use actix_web::HttpMessage; use futures::future::{FutureExt, LocalBoxFuture}; diff --git a/src/middleware/trydirect.rs b/src/middleware/trydirect.rs index bbe5e4bf..e40f89b1 100644 --- a/src/middleware/trydirect.rs +++ b/src/middleware/trydirect.rs @@ -1,5 +1,5 @@ use crate::{models, configuration::Settings, forms::user::UserForm, helpers::JsonResponse}; -use actix_web::{web, dev::ServiceRequest, Error, HttpMessage, error::{ErrorInternalServerError, ErrorUnauthorized}}; +use actix_web::{web, dev::ServiceRequest, Error, HttpMessage}; use actix_web_httpauth::extractors::bearer::BearerAuth; use futures::future::{FutureExt}; use reqwest::header::{ACCEPT, CONTENT_TYPE}; @@ -9,14 +9,15 @@ use std::sync::Arc; pub async fn bearer_guard( req: ServiceRequest, credentials: BearerAuth) -> Result { let settings = req.app_data::>().unwrap(); let token = credentials.token(); - let user = fetch_user(settings.auth_url.as_str(), token).await; - if let Err(err) = user { - return Err((ErrorUnauthorized(JsonResponse::::build().set_msg(err).to_string()), req)); - } + let user = match fetch_user(settings.auth_url.as_str(), token).await { + Ok(user) => user, + Err(err) => { + return Err((JsonResponse::::build().unauthorized(err), req)); + } + }; - let user = user.unwrap(); if req.extensions_mut().insert(Arc::new(user)).is_some() { - return Err((ErrorUnauthorized(JsonResponse::::build().set_msg("user already logged").to_string()), req)); + return Err((JsonResponse::::build().unauthorized("user already logged"), req)); } Ok(req) diff --git a/src/models/mod.rs b/src/models/mod.rs index b6951144..a17ea327 100644 --- a/src/models/mod.rs +++ b/src/models/mod.rs @@ -1,4 +1,7 @@ mod client; +mod product; +mod ratecategory; +mod rules; pub mod rating; pub mod stack; pub mod user; @@ -7,3 +10,6 @@ pub use client::*; pub use rating::*; pub use stack::*; pub use user::*; +pub use product::*; +pub use ratecategory::*; +pub use rules::*; diff --git a/src/models/product.rs b/src/models/product.rs new file mode 100644 index 00000000..992818db --- /dev/null +++ b/src/models/product.rs @@ -0,0 +1,16 @@ +use chrono::{DateTime, Utc}; + +pub struct Product { + // Product - is an external object that we want to store in the database, + // that can be a stack or an app in the stack. feature, service, web app etc. + // id - is a unique identifier for the product + // user_id - is a unique identifier for the user + // rating - is a rating of the product + // product type stack & app, + // id is generated based on the product type and external obj_id + pub id: i32, //primary key, for better data management + pub obj_id: i32, // external product ID db, no autoincrement, example: 100 + pub obj_type: String, // stack | app, unique index + pub created_at: DateTime, + pub updated_at: DateTime, +} diff --git a/src/models/ratecategory.rs b/src/models/ratecategory.rs new file mode 100644 index 00000000..8228ae0e --- /dev/null +++ b/src/models/ratecategory.rs @@ -0,0 +1,27 @@ +use serde::{Deserialize, Serialize}; + +#[derive(sqlx::Type, Serialize, Deserialize, Debug, Clone, Copy)] +#[sqlx(rename_all = "lowercase", type_name = "rate_category")] +pub enum RateCategory { + Application, // app, feature, extension + Cloud, // is user satisfied working with this cloud + Stack, // app stack + DeploymentSpeed, + Documentation, + Design, + TechSupport, + Price, + MemoryUsage, +} + +impl Into for RateCategory { + fn into(self) -> String { + format!("{:?}", self) + } +} + +impl Default for RateCategory { + fn default() -> Self { + RateCategory::Application + } +} diff --git a/src/models/rating.rs b/src/models/rating.rs index c9ba7054..e1522dea 100644 --- a/src/models/rating.rs +++ b/src/models/rating.rs @@ -1,58 +1,16 @@ use chrono::{DateTime, Utc}; use serde::{Deserialize, Serialize}; - -pub struct Product { - // Product - is an external object that we want to store in the database, - // that can be a stack or an app in the stack. feature, service, web app etc. - // id - is a unique identifier for the product - // user_id - is a unique identifier for the user - // rating - is a rating of the product - // product type stack & app, - // id is generated based on the product type and external obj_id - pub id: i32, //primary key, for better data management - pub obj_id: i32, // external product ID db, no autoincrement, example: 100 - pub obj_type: String, // stack | app, unique index - pub created_at: DateTime, - pub updated_at: DateTime, -} +use crate::models; #[derive(Debug, Serialize, Default)] pub struct Rating { pub id: i32, pub user_id: String, // external user_id, 100, taken using token (middleware?) pub obj_id: i32, // id of the external object - pub category: String, // rating of product | rating of service etc + pub category: models::RateCategory, // rating of product | rating of service etc pub comment: Option, // always linked to a product pub hidden: Option, // rating can be hidden for non-adequate user behaviour pub rate: Option, pub created_at: DateTime, pub updated_at: DateTime, } - - - -#[derive(sqlx::Type, Serialize, Deserialize, Debug, Clone, Copy)] -#[sqlx(rename_all = "lowercase", type_name = "varchar")] -pub enum RateCategory { - Application, // app, feature, extension - Cloud, // is user satisfied working with this cloud - Stack, // app stack - DeploymentSpeed, - Documentation, - Design, - TechSupport, - Price, - MemoryUsage, -} - -impl Into for RateCategory { - fn into(self) -> String { - format!("{:?}", self) - } -} - -pub struct Rules { - //-> Product.id - // example: allow to add only a single comment - comments_per_user: i32, // default = 1 -} diff --git a/src/models/rules.rs b/src/models/rules.rs new file mode 100644 index 00000000..58afbd97 --- /dev/null +++ b/src/models/rules.rs @@ -0,0 +1,5 @@ +pub struct Rules { + //-> Product.id + // example: allow to add only a single comment + comments_per_user: i32, // default = 1 +} diff --git a/src/models/stack.rs b/src/models/stack.rs index f9ce272e..a808f130 100644 --- a/src/models/stack.rs +++ b/src/models/stack.rs @@ -1,13 +1,13 @@ use chrono::{DateTime, Utc}; +use serde::{Deserialize, Serialize}; use serde_json::Value; use uuid::Uuid; -use serde::{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 - pub user_id: String, // external unique identifier for the user + pub id: i32, // id - is a unique identifier for the app stack + pub stack_id: Uuid, // external stack ID + pub user_id: String, // external unique identifier for the user pub name: String, // pub body: sqlx::types::Json, pub body: Value, //json type @@ -15,12 +15,26 @@ pub struct Stack { pub updated_at: DateTime, } +impl Stack { + pub fn new(user_id: String, name: String, body: Value) -> Self { + Self { + id: 0, + stack_id: Uuid::new_v4(), + user_id: user_id, + name: name, + body: body, + created_at: Utc::now(), + updated_at: Utc::now(), + } + } +} + impl Default for Stack { fn default() -> Self { Stack { user_id: "".to_string(), name: "".to_string(), - ..Default::default() + ..Default::default() } } -} \ No newline at end of file +} diff --git a/src/routes/client/add.rs b/src/routes/client/add.rs index 8883f25e..b3d90d94 100644 --- a/src/routes/client/add.rs +++ b/src/routes/client/add.rs @@ -2,6 +2,7 @@ use crate::configuration::Settings; use crate::helpers::client; use crate::helpers::JsonResponse; use crate::models; +use crate::db; use actix_web::{post, web, Responder, Result}; use sqlx::PgPool; use tracing::Instrument; @@ -14,10 +15,10 @@ pub async fn add_handler( settings: web::Data, pool: web::Data, ) -> Result { - match add_handler_inner(&user.id, settings, pool).await { - Ok(client) => JsonResponse::build().set_item(client).ok("Ok"), - Err(msg) => JsonResponse::build().bad_request(msg), - } + add_handler_inner(&user.id, settings, pool) + .await + .map(|client| JsonResponse::build().set_item(client).ok("Ok")) + .map_err(|err| JsonResponse::::build().bad_request(err)) } pub async fn add_handler_inner( @@ -25,58 +26,13 @@ pub async fn add_handler_inner( settings: web::Data, pool: web::Data, ) -> Result { - let client_count = db_count_clients_by_user(pool.get_ref(), user_id).await?; + let client_count = db::client::count_by_user(pool.get_ref(), user_id).await?; if client_count >= settings.max_clients_number { return Err("Too many clients created".to_string()); } let client = create_client(pool.get_ref(), user_id).await?; - db_insert_client(pool.get_ref(), client).await -} - -async fn db_count_clients_by_user(pool: &PgPool , user_id: &String) -> Result { - let query_span = tracing::info_span!("Counting the user's clients"); - - sqlx::query!( - r#" - SELECT - count(*) as client_count - FROM client c - WHERE c.user_id = $1 - "#, - user_id.clone(), - ) - .fetch_one(pool) - .instrument(query_span) - .await - .map(|result| {result.client_count.unwrap()}) - .map_err(|err| { - tracing::error!("Failed to execute query: {:?}", err); - "Internal Server Error".to_string() - }) -} - -async fn db_insert_client(pool: &PgPool, mut client: models::Client) -> Result { - let query_span = tracing::info_span!("Saving new client into the database"); - sqlx::query!( - r#" - INSERT INTO client (user_id, secret, created_at, updated_at) - VALUES ($1, $2, NOW() at time zone 'utc', NOW() at time zone 'utc') - RETURNING id - "#, - client.user_id.clone(), - client.secret, - ) - .fetch_one(pool) - .instrument(query_span) - .await - .map(move |result| { - client.id = result.id; - client - }).map_err(|e| { - tracing::error!("Failed to execute query: {:?}", e); - "Failed to insert".to_string() - }) + db::client::insert(pool.get_ref(), client).await } async fn create_client(pool: &PgPool, user_id: &String) -> Result { diff --git a/src/routes/client/disable.rs b/src/routes/client/disable.rs index a513bd53..057a183f 100644 --- a/src/routes/client/disable.rs +++ b/src/routes/client/disable.rs @@ -1,6 +1,7 @@ use crate::configuration::Settings; use crate::helpers::JsonResponse; use crate::models; +use crate::db; use actix_web::{put, web, Responder, Result}; use sqlx::PgPool; use tracing::Instrument; @@ -14,72 +15,19 @@ pub async fn disable_handler( pool: web::Data, path: web::Path<(i32,)>, ) -> Result { - match async { let client_id = path.0; - let mut client = db_fetch_client_by_id(pool.get_ref(), client_id).await?; + let mut client = db::client::fetch(pool.get_ref(), client_id) + .await + .map_err(|msg| JsonResponse::::build().internal_server_error(msg))? + .ok_or_else(|| JsonResponse::::build().not_found("not found"))?; + if client.secret.is_none() { - return Err("client is not active".to_string()); + return Err(JsonResponse::::build().bad_request("client is not active")); } client.secret = None; - db_update_client(pool.get_ref(), client).await - }.await { - Ok(client) => { - JsonResponse::build().set_item(client).ok("success") - } - Err(msg) => { - JsonResponse::::build().bad_request(msg) - } - } -} - -async fn db_fetch_client_by_id(pool: &PgPool, id: i32) -> Result { - let query_span = tracing::info_span!("Fetching the client by ID"); - sqlx::query_as!( - models::Client, - r#" - SELECT - id, user_id, secret - FROM client c - WHERE c.id = $1 - "#, - id, - ) - .fetch_one(pool) - .instrument(query_span) - .await - .map_err(|e| { - match e { - sqlx::Error::RowNotFound => "client not found".to_string(), - s => { - tracing::error!("Failed to execute fetch query: {:?}", s); - "".to_string() - } - } - }) -} - -async fn db_update_client(pool: &PgPool, client: models::Client) -> Result { - let query_span = tracing::info_span!("Updating client into the database"); - sqlx::query!( - r#" - UPDATE client SET - secret=$1, - updated_at=NOW() at time zone 'utc' - WHERE id = $2 - "#, - client.secret, - client.id - ) - .execute(pool) - .instrument(query_span) - .await - .map(|_|{ - tracing::info!("Client {} have been saved to database", client.id); - client - }) - .map_err(|err| { - tracing::error!("Failed to execute query: {:?}", err); - "".to_string() - }) + db::client::update(pool.get_ref(), client) + .await + .map(|client| JsonResponse::build().set_item(client).ok("success")) + .map_err(|msg| JsonResponse::::build().bad_request(msg)) } diff --git a/src/routes/client/enable.rs b/src/routes/client/enable.rs index 6f10bebe..5669accc 100644 --- a/src/routes/client/enable.rs +++ b/src/routes/client/enable.rs @@ -2,9 +2,10 @@ use crate::configuration::Settings; use crate::helpers::client; use crate::helpers::JsonResponse; use crate::models; +use crate::db; use actix_web::{put, web, Responder, Result}; -use sqlx::PgPool; use tracing::Instrument; +use sqlx::PgPool; use std::sync::Arc; #[tracing::instrument(name = "Enable client.")] @@ -15,72 +16,24 @@ pub async fn enable_handler( pool: web::Data, path: web::Path<(i32,)>, ) -> Result { - match async { - let client_id = path.0; - let mut client = db_fetch_client_by_id(pool.get_ref(), client_id).await?; - if client.secret.is_some() { - return Err("client is already active".to_string()); - } + let client_id = path.0; + let mut client = db::client::fetch(pool.get_ref(), client_id) + .await + .map_err(|msg| JsonResponse::::build().internal_server_error(msg))? + .ok_or_else(|| JsonResponse::::build().not_found("not found"))? + ; - client.secret = Some(client::generate_secret(pool.get_ref(), 255).await?); - db_update_client(pool.get_ref(), client).await - }.await { - Ok(client) => { - JsonResponse::build().set_item(client).ok("success") - } - Err(msg) => { - JsonResponse::::build().bad_request(msg) - } + if client.secret.is_some() { + return Err(JsonResponse::::build().bad_request("client is already active")); } -} -async fn db_fetch_client_by_id(pool: &PgPool, id: i32) -> Result { - let query_span = tracing::info_span!("Fetching the client by ID"); - sqlx::query_as!( - models::Client, - r#" - SELECT - id, user_id, secret - FROM client c - WHERE c.id = $1 - "#, - id, - ) - .fetch_one(pool) - .instrument(query_span) - .await - .map_err(|e| { - match e { - sqlx::Error::RowNotFound => "client not found".to_string(), - s => { - tracing::error!("Failed to execute fetch query: {:?}", s); - "".to_string() - } - } - }) -} + client.secret = client::generate_secret(pool.get_ref(), 255) + .await + .map(|secret| Some(secret)) + .map_err(|err| JsonResponse::::build().bad_request(err))?; -async fn db_update_client(pool: &PgPool, client: models::Client) -> Result { - let query_span = tracing::info_span!("Updating client into the database"); - sqlx::query!( - r#" - UPDATE client SET - secret=$1, - updated_at=NOW() at time zone 'utc' - WHERE id = $2 - "#, - client.secret, - client.id - ) - .execute(pool) - .instrument(query_span) - .await - .map(|_|{ - tracing::info!("Client {} have been saved to database", client.id); - client - }) - .map_err(|err| { - tracing::error!("Failed to execute query: {:?}", err); - "".to_string() - }) + db::client::update(pool.get_ref(), client) + .await + .map(|client| JsonResponse::build().set_item(client).ok("success")) + .map_err(|err| JsonResponse::::build().bad_request(err)) } diff --git a/src/routes/client/update.rs b/src/routes/client/update.rs index 7aa28ebc..b1003162 100644 --- a/src/routes/client/update.rs +++ b/src/routes/client/update.rs @@ -1,74 +1,41 @@ use crate::helpers::client; -use crate::models::user::User; -use crate::models::Client; +use crate::models; +use crate::db; use crate::{configuration::Settings, helpers::JsonResponse}; -use actix_web::{error::ErrorBadRequest, put, web, Responder, Result}; +use actix_web::{put, web, Responder, Result}; use sqlx::PgPool; use std::sync::Arc; use tracing::Instrument; +use futures::TryFutureExt; #[tracing::instrument(name = "Update client.")] #[put("/{id}")] pub async fn update_handler( - user: web::ReqData>, + user: web::ReqData>, settings: web::Data, pool: web::Data, path: web::Path<(i32,)>, ) -> Result { let client_id = path.0; - let query_span = tracing::info_span!("Fetching the client by ID"); - let mut client: Client = match sqlx::query_as!( - Client, - r#" - SELECT - id, user_id, secret - FROM client c - WHERE c.id = $1 - "#, - client_id, - ) - .fetch_one(pool.get_ref()) - .instrument(query_span) - .await - { - Ok(client) if client.secret.is_some() => Ok(client), - Ok(_client) => Err("client is not active"), - Err(sqlx::Error::RowNotFound) => Err("the client is not found"), - Err(e) => { - tracing::error!("Failed to execute fetch query: {:?}", e); + let mut client = db::client::fetch(pool.get_ref(), client_id) + .await + .map_err(|msg| JsonResponse::::build().internal_server_error(msg))? + .ok_or_else(|| JsonResponse::::build().not_found("not found"))?; - Err("") - } + if client.secret.is_none() { + return Err(JsonResponse::::build().bad_request("client is not active")); } - .map_err(|s| ErrorBadRequest(JsonResponse::::build().set_msg(s).to_string()))?; client.secret = client::generate_secret(pool.get_ref(), 255) .await .map(|s| Some(s)) - .map_err(|s| ErrorBadRequest(JsonResponse::::build().set_msg(s).to_string()))?; + .map_err(|msg| JsonResponse::::build().bad_request(msg))?; - let query_span = tracing::info_span!("Updating client into the database"); - match sqlx::query!( - r#" - UPDATE client SET - secret=$1, - updated_at=NOW() at time zone 'utc' - WHERE id = $2 - "#, - client.secret, - client.id - ) - .execute(pool.get_ref()) - .instrument(query_span) - .await - { - Ok(_) => { - tracing::info!("Client {} have been saved to database", client.id); - JsonResponse::build().set_item(client).ok("success") - } - Err(e) => { - tracing::error!("Failed to execute query: {:?}", e); - JsonResponse::build().internal_server_error("") - } - } + db::client::update(pool.get_ref(), client) + .await + .map(|client| JsonResponse::::build().set_item(client).ok("success")) + .map_err(|err| { + tracing::error!("Failed to execute query: {:?}", err); + JsonResponse::::build().internal_server_error("") + }) } diff --git a/src/routes/rating/add.rs b/src/routes/rating/add.rs index 922cee0d..48f03309 100644 --- a/src/routes/rating/add.rs +++ b/src/routes/rating/add.rs @@ -1,12 +1,12 @@ use crate::forms; use crate::helpers::JsonResponse; use crate::models; -use crate::models::user::User; -use crate::models::RateCategory; +use crate::db; use actix_web::{post, web, Responder, Result}; use sqlx::PgPool; use tracing::Instrument; use std::sync::Arc; +use futures::TryFutureExt; // workflow // add, update, list, get(user_id), ACL, @@ -16,86 +16,28 @@ use std::sync::Arc; #[tracing::instrument(name = "Add rating.")] #[post("")] pub async fn add_handler( - user: web::ReqData>, + user: web::ReqData>, form: web::Json, - pool: web::Data, + pg_pool: web::Data, ) -> Result { - let query_span = tracing::info_span!("Check product existence by id."); - match sqlx::query_as!( - models::Product, - r"SELECT * FROM product WHERE obj_id = $1", - form.obj_id - ) - .fetch_one(pool.get_ref()) - .instrument(query_span) - .await - { - Ok(product) => { - tracing::info!("Found product: {:?}", product.obj_id); - } - Err(e) => { - tracing::error!("Failed to fetch product: {:?}, error: {:?}", form.obj_id, e); - return JsonResponse::::build().bad_request("Object not found"); - } - }; + let _product = db::product::fetch_by_obj(pg_pool.get_ref(), form.obj_id) + .await + .map_err(|msg| JsonResponse::::build().internal_server_error(msg))? + .ok_or_else(|| JsonResponse::::build().not_found("not found"))? + ; - let query_span = tracing::info_span!("Search for existing vote."); - match sqlx::query!( - r"SELECT id FROM rating where user_id=$1 AND obj_id=$2 AND category=$3 LIMIT 1", - user.id, - form.obj_id, - form.category as RateCategory - ) - .fetch_one(pool.get_ref()) - .instrument(query_span) - .await - { - Ok(record) => { - tracing::info!( - "rating exists: {:?}, user: {}, product: {}, category: {:?}", - record.id, - user.id, - form.obj_id, - form.category - ); - return JsonResponse::build().conflict("Already rated"); - } - Err(sqlx::Error::RowNotFound) => {} - Err(e) => { - tracing::error!("Failed to fetch rating, error: {:?}", e); - return JsonResponse::build().internal_server_error("Internal Server Error"); - } + let rating = db::rating::fetch_by_obj_and_user_and_category(pg_pool.get_ref(), form.obj_id, user.id.clone(), form.category) + .await + .map_err(|err| JsonResponse::::build().internal_server_error(err))?; + if rating.is_some() { + return Err(JsonResponse::::build().bad_request("already rated")); } - let query_span = tracing::info_span!("Saving new rating details into the database"); - // Insert rating - match sqlx::query!( - r#" - INSERT INTO rating (user_id, obj_id, category, comment, hidden,rate, - created_at, - updated_at) - VALUES ($1, $2, $3, $4, $5, $6, NOW() at time zone 'utc', NOW() at time zone 'utc') - RETURNING id - "#, - user.id, - form.obj_id, - form.category as models::RateCategory, - form.comment, - false, - form.rate - ) - .fetch_one(pool.get_ref()) - .instrument(query_span) - .await - { - Ok(result) => { - tracing::info!("New rating {} have been saved to database", result.id); + let mut rating: models::Rating = form.into_inner().into(); + rating.user_id = user.id.clone(); - JsonResponse::build().set_id(result.id).ok("Saved") - } - Err(e) => { - tracing::error!("Failed to execute query: {:?}", e); - JsonResponse::build().internal_server_error("Failed to insert") - } - } + db::rating::insert(pg_pool.get_ref(), rating) + .await + .map(|rating| JsonResponse::build().set_item(rating).ok("success")) + .map_err(|err| JsonResponse::::build().internal_server_error("Failed to insert")) } diff --git a/src/routes/rating/get.rs b/src/routes/rating/get.rs index e781d9a8..4bc0c1ff 100644 --- a/src/routes/rating/get.rs +++ b/src/routes/rating/get.rs @@ -1,5 +1,6 @@ use crate::helpers::JsonResponse; use crate::models; +use crate::db; use actix_web::{get, web, Responder, Result}; use sqlx::PgPool; use tracing::Instrument; @@ -15,53 +16,20 @@ pub async fn get_handler( path: web::Path<(i32,)>, pool: web::Data, ) -> Result { - /// 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!( - models::Rating, - r"SELECT * FROM rating WHERE id=$1 LIMIT 1", - rate_id - ) - .fetch_one(pool.get_ref()) - .instrument(query_span) - .await - { - Ok(rating) => { - tracing::info!("rating found: {:?}", rating.id); - return JsonResponse::build().set_item(Some(rating)).ok("OK"); - } - Err(sqlx::Error::RowNotFound) => { - return JsonResponse::build().not_found(""); - } - Err(e) => { - tracing::error!("Failed to fetch rating, error: {:?}", e); - return JsonResponse::build().internal_server_error(""); - } - } + let rating = db::rating::fetch(pool.get_ref(), rate_id) + .await + .map_err(|_err| JsonResponse::::build().internal_server_error(""))? + .ok_or_else(|| JsonResponse::::build().not_found("not found"))?; + + Ok(JsonResponse::build().set_item(rating).ok("OK")) } #[tracing::instrument(name = "Get all ratings.")] #[get("")] pub async fn list_handler(path: web::Path<()>, pool: web::Data) -> Result { - /// Get ratings of all users - let query_span = tracing::info_span!("Get all rates."); - // let category = path.0; - match sqlx::query_as!(models::Rating, r"SELECT * FROM rating") - .fetch_all(pool.get_ref()) - .instrument(query_span) + db::rating::fetch_all(pool.get_ref()) .await - { - Ok(rating) => { - tracing::info!("Ratings found: {:?}", rating.len()); - return JsonResponse::build().set_list(rating).ok("OK"); - } - Err(sqlx::Error::RowNotFound) => { - return JsonResponse::build().not_found(""); - } - Err(e) => { - tracing::error!("Failed to fetch rating, error: {:?}", e); - return JsonResponse::build().internal_server_error(""); - } - } + .map(|ratings| JsonResponse::build().set_list(ratings).ok("OK")) + .map_err(|err| JsonResponse::::build().internal_server_error("")) } diff --git a/src/routes/stack/add.rs b/src/routes/stack/add.rs index 7a12bfe8..ef38ac5f 100644 --- a/src/routes/stack/add.rs +++ b/src/routes/stack/add.rs @@ -1,128 +1,76 @@ +use crate::db; use crate::forms::stack::StackForm; use crate::helpers::JsonResponse; use crate::models; -use crate::models::user::User; -use actix_web::post; +use actix_web::Error; use actix_web::{ - web, + post, web, web::{Bytes, Data}, Responder, Result, }; -use chrono::Utc; use serde_json::Value; +use serde_valid::Validate; use sqlx::PgPool; use std::str; -use serde_valid::Validate; -use tracing::Instrument; -use uuid::Uuid; use std::sync::Arc; #[tracing::instrument(name = "Add stack.")] #[post("")] pub async fn add( body: Bytes, - user: web::ReqData>, + user: web::ReqData>, pool: Data, ) -> Result { - // @todo ACL - let body_bytes = actix_web::body::to_bytes(body).await.unwrap(); - let body_str = str::from_utf8(&body_bytes).unwrap(); - let form = match serde_json::from_str::(body_str) { - Ok(f) => f, - Err(_err) => { - let msg = format!("Invalid data. {:?}", _err); - return JsonResponse::::build().bad_request(msg); - } - }; - + let form = body_into_form(body).await?; let stack_name = form.custom.custom_stack_code.clone(); - tracing::debug!("form before convert {:?}", form); - - let query_span = tracing::info_span!("Check project/stack existence by custom_stack_code."); - match sqlx::query_as!( - models::Stack, - r"SELECT * FROM user_stack WHERE name = $1", - stack_name - ) - .fetch_one(pool.get_ref()) - .instrument(query_span) - .await - { - Ok(record) => { - tracing::info!("record exists: id: {}, name: {}", record.id, record.name); - return JsonResponse::build().conflict("Stack with that name already exists" - .to_owned()); - } - Err(sqlx::Error::RowNotFound) => {} - Err(e) => { - tracing::error!("Failed to fetch stack info, error: {:?}", e); - return JsonResponse::build().bad_request(format!("Internal Server Error")); - } - }; - let user_id = user.id.clone(); - let request_id = Uuid::new_v4(); - let request_span = tracing::info_span!( - "Validating a new stack", %request_id, - commonDomain=?&form.custom.project_name, - region=?&form.region, - domainList=?&form.domain_list - ); - // using `enter` is an async function - let _request_span_guard = request_span.enter(); // ->exit + check_if_stack_exists(pool.get_ref(), &stack_name).await?; - tracing::info!( - "request_id {} Adding '{}' '{}' as a new stack", - request_id, - form.custom.project_name, - form.region - ); + let body: Value = serde_json::to_value::(form) + .or(serde_json::to_value::(StackForm::default())) + .unwrap(); - let query_span = tracing::info_span!("Saving new stack details into the database"); + let stack = models::Stack::new(user.id.clone(), stack_name, body); + db::stack::insert(pool.get_ref(), stack) + .await + .map(|stack| JsonResponse::build().set_item(stack).ok("Ok")) + .map_err(|_| { + JsonResponse::::build().internal_server_error("Internal Server Error") + }) +} - if !form.validate().is_ok() { - let errors = form.validate().unwrap_err(); - let err_msg = format!("Invalid data received {:?}", &errors.to_string()); - tracing::debug!(err_msg); - return JsonResponse::build().bad_request(errors.to_string());// tmp solution - } +async fn check_if_stack_exists(pool: &PgPool, stack_name: &String) -> Result<(), Error> { + db::stack::fetch_one_by_name(pool, stack_name) + .await + .map_err(|_| { + JsonResponse::::build().internal_server_error("Internal Server Error") + }) + .and_then(|stack| match stack { + Some(_) => Err(JsonResponse::::build() + .conflict("Stack with that name already exists")), + None => Ok(()), + }) +} - let body: Value = match serde_json::to_value::(form) { - Ok(body) => body, - Err(err) => { - tracing::error!("Request_id {} error unwrap body {:?}", request_id, err); - serde_json::to_value::(StackForm::default()).unwrap() - } - }; +async fn body_into_form(body: Bytes) -> Result { + let body_bytes = actix_web::body::to_bytes(body).await.unwrap(); + let body_str = str::from_utf8(&body_bytes) + .map_err(|err| JsonResponse::::build().internal_server_error(err.to_string()))?; + let deserializer = &mut serde_json::Deserializer::from_str(body_str); + serde_path_to_error::deserialize(deserializer) + .map_err(|err| { + let msg = format!("{}:{:?}", err.path().to_string(), err); + JsonResponse::::build().bad_request(msg) + }) + .and_then(|form: StackForm| { + if !form.validate().is_ok() { + let errors = form.validate().unwrap_err(); + let err_msg = format!("Invalid data received {:?}", &errors.to_string()); + tracing::debug!(err_msg); + return Err(JsonResponse::::build().bad_request(errors.to_string())); + } - match sqlx::query!( - r#" - INSERT INTO user_stack (stack_id, user_id, name, body, created_at, updated_at) - VALUES ($1, $2, $3, $4, $5, $6) - RETURNING id; - "#, - Uuid::new_v4(), - user_id, - stack_name, - body, - Utc::now(), - Utc::now(), - ) - .fetch_one(pool.get_ref()) - .instrument(query_span) - .await - { - Ok(record) => { - tracing::info!( - "req_id: {} New stack details have been saved to database", - request_id - ); - return JsonResponse::build().set_id(record.id).ok("OK"); - } - Err(e) => { - tracing::error!("req_id: {} Failed to execute query: {:?}", request_id, e); - return JsonResponse::build().internal_server_error("Internal Server Error"); - } - }; + Ok(form) + }) } diff --git a/src/routes/stack/compose.rs b/src/routes/stack/compose.rs index 819a8865..28290902 100644 --- a/src/routes/stack/compose.rs +++ b/src/routes/stack/compose.rs @@ -1,129 +1,61 @@ -use actix_web::{ - web, - web::{Data, Json}, - Responder, Result, -}; - +use crate::db; use crate::helpers::stack::builder::DcBuilder; use crate::helpers::JsonResponse; -use crate::models::user::User; -use crate::models::Stack; -use actix_web::{get, post}; +use crate::models; +use actix_web::{get, web, web::Data, Responder, Result}; use sqlx::PgPool; -use std::str; -use tracing::Instrument; use std::sync::Arc; #[tracing::instrument(name = "User's generate docker-compose.")] #[get("/{id}")] pub async fn add( - user: web::ReqData>, + user: web::ReqData>, path: web::Path<(i32,)>, pool: Data, ) -> Result { 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 dc = DcBuilder::new(stack); - let fc = dc.build(); - tracing::debug!("Docker compose file content {:?}", fc); - - return JsonResponse::build() - .set_id(id) - .set_item(fc.unwrap()) - .ok("Success"); - } - None => { - return JsonResponse::build().bad_request("Could not generate compose file"); - } - } + let stack = db::stack::fetch(pool.get_ref(), id) + .await + .map_err(|err| JsonResponse::::build().internal_server_error(err)) + .and_then(|stack| match stack { + Some(stack) if stack.user_id != user.id => { + Err(JsonResponse::::build().not_found("not found")) + } + Some(stack) => Ok(stack), + None => Err(JsonResponse::::build().not_found("not found")), + })?; + + let id = stack.id.clone(); + let fc = DcBuilder::new(stack).build().ok_or_else(|| { + tracing::error!("Error. Compose builder returned an empty string"); + JsonResponse::::build().internal_server_error("troubles at building") + })?; + + Ok(JsonResponse::build().set_id(id).set_item(fc).ok("Success")) } #[tracing::instrument(name = "Generate docker-compose. Admin")] #[get("/{id}/compose")] pub async fn admin( - user: web::ReqData>, + user: web::ReqData>, path: web::Path<(i32,)>, pool: Data, ) -> Result { /// 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!("Record found: {:?}", stack.id); - Some(stack) - } - Err(sqlx::Error::RowNotFound) => { - tracing::error!("Record not found"); - None - } - Err(e) => { - tracing::error!("Failed to fetch stack, error: {:?}", e); - None - } - }; - - match stack { - Some(stack) => { - let id = stack.id.clone(); - let dc = DcBuilder::new(stack); - let fc = match dc.build() { - Some(fc) => { - fc - } - None => { - tracing::error!("Error. Compose builder returned an empty string"); - "".to_string() - } - - }; - // tracing::debug!("Docker compose file content {:?}", fc); - return JsonResponse::build() - .set_id(id) - .set_item(fc).ok("Success"); - - } - None => { - return JsonResponse::build().bad_request("Could not generate compose file"); - } - } + let stack = db::stack::fetch(pool.get_ref(), id) + .await + .map_err(|err| JsonResponse::::build().internal_server_error(err)) + .and_then(|stack| match stack { + Some(stack) => Ok(stack), + None => Err(JsonResponse::::build().not_found("not found")), + })?; + + let id = stack.id.clone(); + let fc = DcBuilder::new(stack).build().ok_or_else(|| { + tracing::error!("Error. Compose builder returned an empty string"); + JsonResponse::::build().internal_server_error("troubles at building") + })?; + + Ok(JsonResponse::build().set_id(id).set_item(fc).ok("Success")) } diff --git a/src/routes/stack/deploy.rs b/src/routes/stack/deploy.rs index d1224c59..1d52caa7 100644 --- a/src/routes/stack/deploy.rs +++ b/src/routes/stack/deploy.rs @@ -91,10 +91,10 @@ pub async fn add( assert_eq!(confirm, Confirmation::NotRequested); tracing::debug!("Message sent to rabbitmq"); - return JsonResponse::::build().set_id(id).ok("Success"); + return Ok(JsonResponse::::build().set_id(id).ok("Success")); } None => { - JsonResponse::build().internal_server_error("Deployment failed") + Err(JsonResponse::::build().internal_server_error("Deployment failed")) } } } diff --git a/src/routes/stack/get.rs b/src/routes/stack/get.rs index dc8c09f7..ac21f342 100644 --- a/src/routes/stack/get.rs +++ b/src/routes/stack/get.rs @@ -1,83 +1,50 @@ -use crate::helpers::{JsonResponse, JsonResponseBuilder}; +use crate::db; +use crate::helpers::JsonResponse; use crate::models; -use crate::models::user::User; use actix_web::{get, web, Responder, Result}; use sqlx::PgPool; use std::convert::From; -use tracing::Instrument; use std::sync::Arc; +use tracing::Instrument; #[tracing::instrument(name = "Get logged user stack.")] #[get("/{id}")] pub async fn item( - user: web::ReqData>, + user: web::ReqData>, path: web::Path<(i32,)>, pool: web::Data, ) -> Result { /// Get stack apps of logged user only let (id,) = path.into_inner(); - tracing::info!("User {:?} gets stack by id {:?}", user.id, id); - match sqlx::query_as!( - models::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,); - return JsonResponse::build().set_item(Some(stack)).ok("OK"); - } - Err(sqlx::Error::RowNotFound) => JsonResponse::build().not_found("Record not found"), - Err(e) => { - tracing::error!("Failed to fetch stack, error: {:?}", e); - return JsonResponse::build().internal_server_error("Could not fetch data"); - } - } + let stack = db::stack::fetch(pool.get_ref(), id) + .await + .map_err(|err| JsonResponse::::build().internal_server_error(err)) + .and_then(|stack| match stack { + Some(stack) if stack.user_id != user.id => { + Err(JsonResponse::::build().not_found("not found")) + } + Some(stack) => Ok(stack), + None => Err(JsonResponse::::build().not_found("not found")), + })?; + + Ok(JsonResponse::build().set_item(Some(stack)).ok("OK")) } #[tracing::instrument(name = "Get user's stack list.")] #[get("/user/{id}")] pub async fn list( - user: web::ReqData>, + user: web::ReqData>, path: web::Path<(String,)>, pool: web::Data, ) -> Result { /// 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."); + let user_id = path.into_inner().0; - 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) => { - return JsonResponse::build().set_list(list).ok("OK"); - } - Err(sqlx::Error::RowNotFound) => { - tracing::error!("No stacks found for user: {:?}", &user.id); - return JsonResponse::build().not_found("No stacks found for user"); - } - Err(e) => { - tracing::error!("Failed to fetch stack, error: {:?}", e); - return JsonResponse::build().internal_server_error("Could not fetch".to_string()); - } - } + db::stack::fetch_by_user(pool.get_ref(), &user_id) + .await + .map_err(|err| JsonResponse::::build().internal_server_error(err)) + .map(|stacks| JsonResponse::build().set_list(stacks).ok("OK")) } diff --git a/src/routes/stack/update.rs b/src/routes/stack/update.rs index d51c95ff..a418b532 100644 --- a/src/routes/stack/update.rs +++ b/src/routes/stack/update.rs @@ -43,8 +43,8 @@ pub async fn update( } Err(e) => { tracing::error!("Failed to fetch record: {:?}, error: {:?}", id, e); - return JsonResponse::::build() - .not_found(format!("Object not found {}", id)); + return Err(JsonResponse::::build() + .not_found(format!("Object not found {}", id))); } }; @@ -72,7 +72,7 @@ pub async fn update( let errors = form.validate().unwrap_err(); let err_msg = format!("Invalid data received {:?}", &errors.to_string()); tracing::debug!(err_msg); - return JsonResponse::build().bad_request(errors.to_string()); + return Err(JsonResponse::::build().bad_request(errors.to_string())); } let body: Value = match serde_json::to_value::(form.into_inner()) { @@ -106,11 +106,11 @@ pub async fn update( "req_id: {} stack details have been saved to database", request_id ); - return JsonResponse::build().set_id(id).ok("OK"); + return Ok(JsonResponse::::build().set_id(id).ok("OK")); } Err(e) => { tracing::error!("req_id: {} Failed to execute query: {:?}", request_id, e); - return JsonResponse::build().bad_request("Internal Server Error"); + return Err(JsonResponse::::build().bad_request("Internal Server Error")); } }; } diff --git a/src/routes/test/deploy.rs b/src/routes/test/deploy.rs index 51cc2014..4f36a3ae 100644 --- a/src/routes/test/deploy.rs +++ b/src/routes/test/deploy.rs @@ -13,5 +13,5 @@ struct DeployResponse { #[tracing::instrument(name = "Test deploy.")] #[post("/deploy")] pub async fn handler(client: web::ReqData>) -> Result { - JsonResponse::build().set_item(client.into_inner()).ok("success") + Ok(JsonResponse::build().set_item(client.into_inner()).ok("success")) }