diff --git a/Cargo.lock b/Cargo.lock index fc75567..fc67f30 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1390,6 +1390,26 @@ dependencies = [ "sha2", ] +[[package]] +name = "pin-project" +version = "1.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fda4ed1c6c173e3fc7a83629421152e01d7b1f9b7f65fb301e490e8cfc656422" +dependencies = [ + "pin-project-internal", +] + +[[package]] +name = "pin-project-internal" +version = "1.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4359fd9c9171ec6e8c62926d6faaf553a8dc3f64e1507e76da7911b4f6a04405" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.31", +] + [[package]] name = "pin-project-lite" version = "0.2.9" @@ -2006,6 +2026,7 @@ dependencies = [ "thiserror", "tokio", "tracing", + "tracing-actix-web", "tracing-bunyan-formatter", "tracing-log", "tracing-subscriber", @@ -2256,6 +2277,18 @@ dependencies = [ "tracing-core", ] +[[package]] +name = "tracing-actix-web" +version = "0.7.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "94982c2ad939d5d0bfd71c2f9b7ed273c72348485c72bb87bb4db6bd69df10cb" +dependencies = [ + "actix-web", + "pin-project", + "tracing", + "uuid", +] + [[package]] name = "tracing-attributes" version = "0.1.26" diff --git a/Cargo.toml b/Cargo.toml index d1b3e87..bcab850 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -30,6 +30,7 @@ serde_json = { version = "1.0.105", features = [] } serde_derive = "1.0.188" actix-web-httpauth = "0.8.1" actix-cors = "0.6.4" +tracing-actix-web = "0.7.7" [dependencies.sqlx] version = "0.6.3" diff --git a/configuration.yaml b/configuration.yaml index dbd16f2..b3918f1 100644 --- a/configuration.yaml +++ b/configuration.yaml @@ -1,7 +1,8 @@ application_port: 8000 +auth_url: https://65190108818c4e98ac6000e4.mockapi.io/user/1 database: host: localhost port: 5432 username: postgres password: "postgres" - database_name: stacker \ No newline at end of file + database_name: stacker diff --git a/src/configuration.rs b/src/configuration.rs index cce5199..9787660 100644 --- a/src/configuration.rs +++ b/src/configuration.rs @@ -4,6 +4,7 @@ use serde; pub struct Settings { pub database: DatabaseSettings, pub application_port: u16, + pub auth_url: String, } #[derive(Debug, serde::Deserialize)] diff --git a/src/helpers/mod.rs b/src/helpers/mod.rs new file mode 100644 index 0000000..7932cbb --- /dev/null +++ b/src/helpers/mod.rs @@ -0,0 +1 @@ +pub mod serialize_datetime; diff --git a/src/helpers/serialize_datetime.rs b/src/helpers/serialize_datetime.rs new file mode 100644 index 0000000..5c0f3c1 --- /dev/null +++ b/src/helpers/serialize_datetime.rs @@ -0,0 +1,21 @@ +use chrono::{DateTime, TimeZone, Utc}; +use serde::{Deserialize, Deserializer, Serializer}; + +const FORMAT: &'static str = "%Y-%m-%d %H:%M:%S"; + +pub fn serialize(date: &DateTime, serializer: S) -> Result +where + S: Serializer, +{ + let s = format!("{}", date.format(FORMAT)); + serializer.serialize_str(&s) +} + +pub fn deserialize<'de, D>(deserializer: D) -> Result, D::Error> +where + D: Deserializer<'de>, +{ + let s = String::deserialize(deserializer)?; + Utc.datetime_from_str(&s, FORMAT) + .map_err(serde::de::Error::custom) +} diff --git a/src/lib.rs b/src/lib.rs index 9d3cc9b..56bd1f0 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,5 +1,6 @@ pub mod configuration; pub mod forms; +pub mod helpers; mod middleware; pub mod models; pub mod routes; diff --git a/src/main.rs b/src/main.rs index 0b761d7..50788f2 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,4 +1,3 @@ -use sqlx::PgPool; use stacker::configuration::get_configuration; use stacker::startup::run; use stacker::telemetry::{get_subscriber, init_subscriber}; @@ -9,16 +8,6 @@ async fn main() -> std::io::Result<()> { init_subscriber(subscriber); let configuration = get_configuration().expect("Failed to read configuration."); - let connection_pool = PgPool::connect(&configuration.database.connection_string()) - .await - .expect("Failed to connect to database."); - let address = format!("127.0.0.1:{}", configuration.application_port); - tracing::info!("Start server at {:?}", &address); - let listener = std::net::TcpListener::bind(address).expect(&format!( - "failed to bind to {}", - configuration.application_port - )); - run(listener, connection_pool)?.await + run(configuration).await?.await } - diff --git a/src/models/rating.rs b/src/models/rating.rs index b1242dd..6e914f0 100644 --- a/src/models/rating.rs +++ b/src/models/rating.rs @@ -1,6 +1,5 @@ use chrono::{DateTime, Utc}; use serde::{Deserialize, Serialize}; -use uuid::Uuid; pub struct Product { // Product - is an external object that we want to store in the database, @@ -17,15 +16,18 @@ pub struct Product { pub updated_at: DateTime, } +#[derive(Debug, Serialize)] pub struct Rating { pub id: i32, - pub user_id: Uuid, // external user_id, 100, taken using token (middleware?) + pub user_id: i32, // external user_id, 100, taken using token (middleware?) pub product_id: i32, //primary key, for better data management pub category: String, // rating of product | rating of service etc - pub comment: String, // always linked to a product - pub hidden: bool, // rating can be hidden for non-adequate user behaviour - pub rate: u32, + pub comment: Option, // always linked to a product + pub hidden: Option, // rating can be hidden for non-adequate user behaviour + pub rate: Option, + #[serde(with = "crate::helpers::serialize_datetime")] pub created_at: DateTime, + #[serde(with = "crate::helpers::serialize_datetime")] pub updated_at: DateTime, } diff --git a/src/routes/health_checks.rs b/src/routes/health_checks.rs index 1881c80..89630f4 100644 --- a/src/routes/health_checks.rs +++ b/src/routes/health_checks.rs @@ -1,6 +1,6 @@ -use actix_web::{HttpRequest, HttpResponse}; +use actix_web::{get, HttpRequest, HttpResponse}; -pub async fn health_check(req: HttpRequest) -> HttpResponse { +#[get("")] +pub async fn health_check(_req: HttpRequest) -> HttpResponse { HttpResponse::Ok().finish() } - diff --git a/src/routes/rating.rs b/src/routes/rating/add.rs similarity index 73% rename from src/routes/rating.rs rename to src/routes/rating/add.rs index bc3995c..7801658 100644 --- a/src/routes/rating.rs +++ b/src/routes/rating/add.rs @@ -2,11 +2,11 @@ use crate::forms; use crate::models; use crate::models::user::User; use crate::models::RateCategory; -use actix_web::{web, HttpResponse, Responder, Result}; +use actix_web::post; +use actix_web::{web, Responder, Result}; use serde_derive::Serialize; use sqlx::PgPool; use tracing::Instrument; -use uuid::Uuid; // workflow // add, update, list, get(user_id), ACL, @@ -21,13 +21,13 @@ struct JsonResponse { id: Option, } -pub async fn rating( +#[tracing::instrument(name = "Add rating.")] +#[post("")] +pub async fn add_handler( user: web::ReqData, form: web::Json, pool: web::Data, ) -> Result { - //TODO. check if there already exists a rating for this product committed by this user - let request_id = Uuid::new_v4(); let query_span = tracing::info_span!("Check product existence by id."); match sqlx::query_as!( models::Product, @@ -39,16 +39,10 @@ pub async fn rating( .await { Ok(product) => { - tracing::info!("req_id: {} Found product: {:?}", request_id, product.obj_id); + tracing::info!("Found product: {:?}", product.obj_id); } Err(e) => { - tracing::error!( - "req_id: {} Failed to fetch product: {:?}, error: {:?}", - request_id, - form.obj_id, - e - ); - // return HttpResponse::InternalServerError().finish(); + tracing::error!("Failed to fetch product: {:?}, error: {:?}", form.obj_id, e); return Ok(web::Json(JsonResponse { status: "Error".to_string(), code: 404, @@ -71,8 +65,7 @@ pub async fn rating( { Ok(record) => { tracing::info!( - "req_id: {} rating exists: {:?}, user: {}, product: {}, category: {:?}", - request_id, + "rating exists: {:?}, user: {}, product: {}, category: {:?}", record.id, user.id, form.obj_id, @@ -86,13 +79,19 @@ pub async fn rating( id: Some(record.id), })); } - Err(err) => { - // @todo, match the sqlx response + 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, + })); } } let query_span = tracing::info_span!("Saving new rating details into the database"); - // Get product by id // Insert rating match sqlx::query!( r#" @@ -114,12 +113,7 @@ pub async fn rating( .await { Ok(result) => { - println!("Query returned {:?}", result); - tracing::info!( - "req_id: {} New rating {} have been saved to database", - request_id, - result.id - ); + tracing::info!("New rating {} have been saved to database", result.id); Ok(web::Json(JsonResponse { status: "ok".to_string(), @@ -129,7 +123,7 @@ pub async fn rating( })) } Err(e) => { - tracing::error!("req_id: {} Failed to execute query: {:?}", request_id, e); + tracing::error!("Failed to execute query: {:?}", e); Ok(web::Json(JsonResponse { status: "error".to_string(), code: 500, diff --git a/src/routes/rating/get.rs b/src/routes/rating/get.rs new file mode 100644 index 0000000..505e66d --- /dev/null +++ b/src/routes/rating/get.rs @@ -0,0 +1,65 @@ +use crate::models; +use actix_web::get; +use actix_web::{web, Responder, Result}; +use serde_derive::Serialize; +use sqlx::PgPool; +use tracing::Instrument; + +// 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, +} + +#[tracing::instrument(name = "Get rating.")] +#[get("/{id}")] +pub async fn get_handler( + path: web::Path<(i32,)>, + pool: web::Data, +) -> Result { + 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 Ok(web::Json(JsonResponse { + status: "Success".to_string(), + code: 200, + message: "".to_string(), + rating: Some(rating), + })); + } + Err(sqlx::Error::RowNotFound) => { + return Ok(web::Json(JsonResponse { + status: "Error".to_string(), + code: 404, + message: format!("Not Found"), + rating: None, + })); + } + 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, + })); + } + } +} diff --git a/src/routes/rating/mod.rs b/src/routes/rating/mod.rs new file mode 100644 index 0000000..0b54a65 --- /dev/null +++ b/src/routes/rating/mod.rs @@ -0,0 +1,4 @@ +mod add; +mod get; +pub use add::*; +pub use get::*; diff --git a/src/startup.rs b/src/startup.rs index 615fcab..a00c02d 100644 --- a/src/startup.rs +++ b/src/startup.rs @@ -1,6 +1,7 @@ +use crate::configuration::Settings; use actix_cors::Cors; use actix_web::dev::{Server, ServiceRequest}; -use actix_web::middleware::Logger; +use actix_web::error::{ErrorInternalServerError, ErrorUnauthorized}; use actix_web::HttpMessage; use actix_web::{ // http::header::HeaderName, @@ -12,65 +13,81 @@ use actix_web::{ use actix_web_httpauth::{extractors::bearer::BearerAuth, middleware::HttpAuthentication}; use reqwest::header::{ACCEPT, CONTENT_TYPE}; use sqlx::PgPool; -use std::net::TcpListener; +use std::sync::Arc; +use tracing_actix_web::TracingLogger; use crate::models::user::User; +#[tracing::instrument(name = "Bearer guard.")] async fn bearer_guard( req: ServiceRequest, credentials: BearerAuth, ) -> Result { - eprintln!("{credentials:?}"); - //todo check that credentials.token is a real. get in sync with auth server - //todo get user from auth server + let settings = req.app_data::>().unwrap(); let client = reqwest::Client::new(); let resp = client - .get("https://65190108818c4e98ac6000e4.mockapi.io/user/1") //todo add the right url + .get(&settings.auth_url) .bearer_auth(credentials.token()) .header(CONTENT_TYPE, "application/json") .header(ACCEPT, "application/json") .send() - .await - .unwrap() //todo process the response rightly. At moment it's some of something - ; - eprintln!("{resp:?}"); + .await; + + let resp = match resp { + Ok(resp) if resp.status().is_success() => resp, + Ok(resp) => { + tracing::error!("Authentication service returned no success {:?}", resp); + return Err((ErrorUnauthorized(""), req)); + } + Err(err) => { + tracing::error!("error from reqwest {:?}", err); + return Err((ErrorInternalServerError(""), req)); + } + }; - let user: User = match resp.status() { - reqwest::StatusCode::OK => match resp.json().await { - Ok(user) => user, - Err(err) => panic!("can't parse the user from json {err:?}"), //todo - }, - other => { - //todo process the other status code accordingly - panic!("unexpected status code {other}"); + let user: User = match resp.json().await { + Ok(user) => { + tracing::info!("unpacked user {user:?}"); + user + } + Err(err) => { + tracing::error!("can't parse the response body {:?}", err); + return Err((ErrorUnauthorized(""), req)); } }; - //let user = User { id: 1 }; - tracing::info!("authentication middleware. {user:?}"); let existent_user = req.extensions_mut().insert(user); if existent_user.is_some() { - tracing::error!("authentication middleware. already logged {existent_user:?}"); - //return Err(("".into(), req)); + tracing::error!("already logged {existent_user:?}"); + return Err((ErrorInternalServerError(""), req)); } + Ok(req) } -pub fn run(listener: TcpListener, db_pool: PgPool) -> Result { +pub async fn run(settings: Settings) -> Result { + let settings = Arc::new(settings); + let db_pool = PgPool::connect(&settings.database.connection_string()) + .await + .expect("Failed to connect to database."); let db_pool = web::Data::new(db_pool); + + let address = format!("127.0.0.1:{}", settings.application_port); + tracing::info!("Start server at {:?}", &address); + let listener = std::net::TcpListener::bind(address) + .expect(&format!("failed to bind to {}", settings.application_port)); + let server = HttpServer::new(move || { App::new() - .wrap(Logger::default()) - .wrap(HttpAuthentication::bearer(bearer_guard)) - .wrap(Cors::permissive()) - .service( - web::resource("/health_check").route(web::get().to(crate::routes::health_check)), - ) + .wrap(TracingLogger::default()) + .service(web::scope("/health_check").service(crate::routes::health_check)) .service( - web::resource("/rating") - .route(web::get().to(crate::routes::rating)) - .route(web::post().to(crate::routes::rating)), + web::scope("/rating") + .wrap(HttpAuthentication::bearer(bearer_guard)) + .wrap(Cors::permissive()) + .service(crate::routes::add_handler) + .service(crate::routes::get_handler), ) // .service( // web::resource("/stack/{id}") @@ -86,6 +103,7 @@ pub fn run(listener: TcpListener, db_pool: PgPool) -> Result