From 9d2b6ee10ec5359cc91769d430485c8c869ba1a8 Mon Sep 17 00:00:00 2001 From: Akshay Date: Thu, 24 Dec 2020 10:51:40 +0530 Subject: monorepo --- backend/src/bin/server.rs | 94 +++++++++++++++++++++++ backend/src/handlers/cart_items.rs | 107 +++++++++++++++++++++++++++ backend/src/handlers/mod.rs | 5 ++ backend/src/handlers/product.rs | 138 ++++++++++++++++++++++++++++++++++ backend/src/handlers/rating.rs | 91 +++++++++++++++++++++++ backend/src/handlers/smoke.rs | 15 ++++ backend/src/handlers/users.rs | 148 +++++++++++++++++++++++++++++++++++++ backend/src/lib.rs | 10 +++ backend/src/models.rs | 97 ++++++++++++++++++++++++ backend/src/schema.rs | 61 +++++++++++++++ 10 files changed, 766 insertions(+) create mode 100644 backend/src/bin/server.rs create mode 100644 backend/src/handlers/cart_items.rs create mode 100644 backend/src/handlers/mod.rs create mode 100644 backend/src/handlers/product.rs create mode 100644 backend/src/handlers/rating.rs create mode 100644 backend/src/handlers/smoke.rs create mode 100644 backend/src/handlers/users.rs create mode 100644 backend/src/lib.rs create mode 100644 backend/src/models.rs create mode 100644 backend/src/schema.rs (limited to 'backend/src') diff --git a/backend/src/bin/server.rs b/backend/src/bin/server.rs new file mode 100644 index 0000000..7c67e4f --- /dev/null +++ b/backend/src/bin/server.rs @@ -0,0 +1,94 @@ +use actix_cors::Cors; +use actix_identity::{CookieIdentityPolicy, IdentityService}; +use actix_web::middleware; +use actix_web::{web, App, HttpServer}; +use diesel::r2d2::{ConnectionManager, Pool}; +use diesel::MysqlConnection; +use furby::handlers::smoke::manual_hello; +use furby::handlers::{cart_items, product, rating, users}; +use rand::Rng; + +#[actix_web::main] +async fn main() -> std::io::Result<()> { + pretty_env_logger::init(); + + let db_url = env!("DATABASE_URL"); + let manager = ConnectionManager::::new(db_url); + let pool = Pool::builder() + .build(manager) + .expect("Failed to create pool."); + + let private_key = rand::thread_rng().gen::<[u8; 32]>(); + HttpServer::new(move || { + App::new() + .wrap(IdentityService::new( + CookieIdentityPolicy::new(&private_key) + .name("user-login") + .domain("127.0.0.1") + .path("/") + .same_site(actix_web::cookie::SameSite::None) + .http_only(true) + .secure(false), + )) + .wrap( + Cors::default() + .allowed_origin("http://127.0.0.1:8000") + .allowed_origin("http://localhost:8000") + .allow_any_method() + .allow_any_header(), + ) + .wrap( + middleware::DefaultHeaders::new() + .header("Access-Control-Allow-Credentials", "true") + .header("Access-Control-Expose-Headers", "set-cookie"), + ) + .wrap(middleware::Logger::default()) + .data(pool.clone()) + .service( + web::scope("/user") + .route("/existing", web::post().to(users::name_exists)) + .route("/login", web::post().to(users::login)) + .route("/{uname}", web::get().to(users::user_details)) + .route("/new", web::post().to(users::new_user)) + .route( + "/change_password", + web::post().to(users::change_password), + ), + ) + .service( + web::scope("/product") + .route("/catalog", web::get().to(product::get_all_products)) + .route("/new", web::post().to(product::new_product)) + .route("/{id}", web::get().to(product::product_details)) + .route( + "/reviews/{id}", + web::get().to(product::get_product_reviews), + ) + .route( + "/update_product/{id}", + web::post().to(product::update_product), + ), + ) + .service( + web::scope("/cart") + .route( + "/items", + web::get().to(cart_items::get_user_cart_items), + ) + .route("/add", web::post().to(cart_items::add_to_cart)) + .route( + "/remove", + web::post().to(cart_items::remove_from_cart), + ), + ) + .service( + web::scope("/rating") + .route("/add", web::post().to(rating::add_rating)) + .route("/remove", web::post().to(rating::remove_rating)), + ) + .route("/hey", web::get().to(manual_hello)) + }) + .bind("127.0.0.1:7878")? + .run() + .await +} diff --git a/backend/src/handlers/cart_items.rs b/backend/src/handlers/cart_items.rs new file mode 100644 index 0000000..25baaeb --- /dev/null +++ b/backend/src/handlers/cart_items.rs @@ -0,0 +1,107 @@ +use crate::models::{AddCartItem, CartItem, Customer, Product}; +use crate::schema::product::dsl as prod; +use crate::schema::{cart_items::dsl::*, customer::dsl::*}; +use crate::TPool; + +use actix_identity::Identity; +use actix_web::{web, HttpResponse, Responder}; +use diesel::prelude::*; +use log::{error, info}; +use serde::Deserialize; + +pub async fn add_to_cart( + cookie: Identity, + item_id: String, + pool: web::Data, +) -> impl Responder { + let item_details = item_id.parse::().unwrap_or(-1); + info!("Add to cart hit: {:?}", item_details); + info!("[cart] Current user: {:?}", cookie.identity()); + let conn = pool.get().unwrap(); + if let Some(uname) = cookie.identity() { + let selected_user = customer + .filter(username.eq(&uname)) + .limit(1) + .first::(&conn) + .expect("Couldn't connect to DB"); + let new_cart_item = AddCartItem { + cart_id: selected_user.id, + product_id: item_details, + }; + info!( + "cart id: {:?}, product id {:?}", + selected_user.id, item_details + ); + diesel::insert_into(cart_items) + .values((cart_id.eq(selected_user.id), product_id.eq(item_details))) + .execute(&conn) + .expect("Coundn't connect to DB"); + HttpResponse::Ok().body("Inserted successfully!") + } else { + error!("Unauthorized add to cart action!"); + return HttpResponse::Unauthorized() + .body("Need to be logged in to add to cart!"); + } +} + +pub async fn remove_from_cart( + cookie: Identity, + item_id: String, + pool: web::Data, +) -> impl Responder { + info!("Remove from cart hit: {:?}", item_id); + let item_details = item_id.parse::().unwrap_or(-1); + let conn = pool.get().unwrap(); + if let Some(uname) = cookie.identity() { + let selected_user = customer + .filter(username.eq(&uname)) + .limit(1) + .first::(&conn) + .expect("Couldn't connect to DB"); + + diesel::delete( + cart_items + .filter(cart_id.eq(selected_user.id)) + .filter(product_id.eq(item_details)), + ) + .execute(&conn) + .expect("Coundn't connect to DB"); + HttpResponse::Ok().body("Removed successfully!") + } else { + error!("Unauthorized add to cart action!"); + return HttpResponse::Unauthorized() + .body("Need to be logged in to add to cart!"); + } +} + +pub async fn get_user_cart_items( + cookie: Identity, + pool: web::Data, +) -> impl Responder { + let conn = pool.get().unwrap(); + if let Some(uname) = cookie.identity() { + let selected_user = customer + .filter(username.eq(&uname)) + .limit(1) + .first::(&conn) + .expect("Couldn't connect to DB"); + let user_cart_items = cart_items + .filter(cart_id.eq(selected_user.id)) + .load::(&conn) + .expect("Couldn't connect to DB"); + let cart_products = user_cart_items + .into_iter() + .map(|item| { + prod::product + .filter(prod::id.eq(item.product_id)) + .limit(1) + .first::(&conn) + .expect("Couldn't connect to db") + }) + .collect::>(); + return HttpResponse::Ok().json(&cart_products); + } else { + return HttpResponse::Unauthorized() + .body("Need to be logged in to add to cart!"); + } +} diff --git a/backend/src/handlers/mod.rs b/backend/src/handlers/mod.rs new file mode 100644 index 0000000..9416857 --- /dev/null +++ b/backend/src/handlers/mod.rs @@ -0,0 +1,5 @@ +pub mod cart_items; +pub mod product; +pub mod rating; +pub mod smoke; +pub mod users; diff --git a/backend/src/handlers/product.rs b/backend/src/handlers/product.rs new file mode 100644 index 0000000..41a47a0 --- /dev/null +++ b/backend/src/handlers/product.rs @@ -0,0 +1,138 @@ +use crate::models::{Customer, NewProduct, Product, Rating, UpdateProduct}; +use crate::schema::customer::dsl as cust; +use crate::schema::product::dsl::*; +use crate::schema::rating::dsl as rating; +use crate::TPool; + +use actix_web::{web, HttpResponse, Responder}; +use chrono::naive::NaiveDate; +use diesel::prelude::*; +use log::{error, info}; +use serde::{Deserialize, Serialize}; + +pub async fn new_product( + pool: web::Data, + item: web::Json, +) -> impl Responder { + info!("New product hit: {:?}", item.name); + let conn = pool.get().unwrap(); + diesel::insert_into(product) + .values(item.into_inner()) + .execute(&conn) + .expect("Coundn't connect to DB"); + HttpResponse::Ok().body("Inserted successfully!") +} + +pub async fn product_details( + pool: web::Data, + product_id: web::Path, +) -> impl Responder { + let conn = pool.get().unwrap(); + let product_id = product_id.into_inner(); + info!("Fetching product details for {}", product_id); + let selected_product = product + .filter(id.eq(&product_id)) + .limit(1) + .first::(&conn); + match selected_product { + Ok(m) => { + info!("Found product: {}", product_id); + HttpResponse::Ok().json(m) + } + Err(_) => { + error!("Product not found: {}", product_id); + HttpResponse::NotFound().finish() + } + } +} + +pub async fn update_product( + pool: web::Data, + product_id: web::Path, + product_details: web::Json, +) -> impl Responder { + let conn = pool.get().unwrap(); + let product_id = product_id.into_inner(); + let product_details = product_details.into_inner(); + info!("Updating product: {:?}", product_id); + match diesel::update(product.filter(id.eq(product_id))) + .set(( + name.eq(product_details.name), + kind.eq(product_details.kind), + price.eq(product_details.price), + description.eq(product_details.description), + )) + .execute(&conn) + { + Ok(_) => { + return HttpResponse::Ok().body("Changed product successfully") + } + _ => { + return HttpResponse::InternalServerError() + .body("Unable to update record") + } + } +} + +pub async fn get_all_products(pool: web::Data) -> impl Responder { + let conn = pool.get().unwrap(); + info!("Generating and returning catalog ..."); + match product.load::(&conn) { + Ok(products) => return HttpResponse::Ok().json(&products), + Err(_) => { + return HttpResponse::InternalServerError() + .body("Unable to fetch product catalog") + } + } +} + +#[derive(Serialize, Deserialize, Debug)] +struct ProductRating { + pub comment_text: Option, + pub comment_date: NaiveDate, + pub product_name: String, + pub customer_name: String, + pub stars: Option, +} + +pub async fn get_product_reviews( + pool: web::Data, + product_id: web::Path, +) -> impl Responder { + let conn = pool.get().unwrap(); + info!("Fetching product reviews for {}", product_id); + let pid = product_id.into_inner(); + let rating_entries = rating::rating + .filter(rating::product_id.eq(pid)) + .load::(&conn) + .expect("Couldn't connect to DB"); + let json_ratings = rating_entries + .into_iter() + .map(move |p| { + let selected_product = product + .filter(id.eq(&p.product_id.unwrap())) + .limit(1) + .first::(&conn) + .unwrap() + .name + .clone(); + + let selected_customer = cust::customer + .filter(cust::id.eq(&p.customer_id.unwrap())) + .limit(1) + .first::(&conn) + .unwrap() + .username + .clone(); + + ProductRating { + comment_text: p.comment_text, + comment_date: p.comment_date.unwrap(), + product_name: selected_product, + customer_name: selected_customer, + stars: p.stars, + } + }) + .collect::>(); + return HttpResponse::Ok().json(&json_ratings); +} diff --git a/backend/src/handlers/rating.rs b/backend/src/handlers/rating.rs new file mode 100644 index 0000000..dfbeb3e --- /dev/null +++ b/backend/src/handlers/rating.rs @@ -0,0 +1,91 @@ +use crate::models::{AddRating, Customer, Rating}; +use crate::schema::rating::dsl as rating; +use crate::schema::{customer::dsl::*, product::dsl::*}; +use crate::TPool; + +use actix_identity::Identity; +use actix_web::{web, HttpResponse, Responder}; +use diesel::prelude::*; +use log::{error, info}; +use serde::Deserialize; + +#[derive(Deserialize, Debug)] +pub struct AddRatingJson { + pub comment_text: Option, + pub stars: Option, + pub product_id: i32, +} + +pub async fn add_rating( + cookie: Identity, + rating_details: web::Json, + pool: web::Data, +) -> impl Responder { + info!("Add rating hit: {:?}", rating_details.product_id); + info!("{:?}", cookie.identity()); + let conn = pool.get().unwrap(); + if let Some(uname) = cookie.identity() { + let selected_user = customer + .filter(username.eq(&uname)) + .limit(1) + .first::(&conn) + .expect("Couldn't connect to DB"); + let rating_details = rating_details.into_inner(); + let new_rating = AddRating { + comment_text: rating_details.comment_text, + stars: rating_details.stars, + product_id: rating_details.product_id, + customer_id: selected_user.id, + }; + diesel::insert_into(rating::rating) + .values(new_rating) + .execute(&conn) + .expect("Coundn't connect to DB"); + HttpResponse::Ok().body("Inserted rating successfully!") + } else { + error!("Unauthorized add rating action!"); + return HttpResponse::Unauthorized() + .body("Need to be logged in to add rating!"); + } +} + +#[derive(Deserialize, Debug)] +pub struct RemoveRating { + rating_id: i32, +} + +pub async fn remove_rating( + cookie: Identity, + rating_details: web::Json, + pool: web::Data, +) -> impl Responder { + info!("Remove rating hit: {:?}", rating_details.rating_id); + let conn = pool.get().unwrap(); + if let Some(uname) = cookie.identity() { + let selected_user = customer + .filter(username.eq(&uname)) + .limit(1) + .first::(&conn) + .expect("Couldn't connect to DB"); + + diesel::delete( + rating::rating + .filter(rating::customer_id.eq(selected_user.id)) + .filter(rating::id.eq(rating_details.rating_id)), + ) + .execute(&conn) + .expect("Coundn't connect to DB"); + HttpResponse::Ok().body("Removed successfully!") + } else { + error!("Unauthorized add to cart action!"); + return HttpResponse::Unauthorized() + .body("Need to be logged in to add to cart!"); + } +} + +// pub async fn get_product_reviews( +// product: web::Json, +// pool: web::Data, +// ) -> impl Responder { +// unimplemented!() +// } diff --git a/backend/src/handlers/smoke.rs b/backend/src/handlers/smoke.rs new file mode 100644 index 0000000..d0a1038 --- /dev/null +++ b/backend/src/handlers/smoke.rs @@ -0,0 +1,15 @@ +use actix_web::{get, post, HttpResponse, Responder}; + +#[get("/")] +async fn hello() -> impl Responder { + HttpResponse::Ok().body("Hello world!") +} + +#[post("/echo")] +async fn echo(req_body: String) -> impl Responder { + HttpResponse::Ok().body(req_body) +} + +pub async fn manual_hello() -> impl Responder { + HttpResponse::Ok().body("Hey there!") +} diff --git a/backend/src/handlers/users.rs b/backend/src/handlers/users.rs new file mode 100644 index 0000000..24fb591 --- /dev/null +++ b/backend/src/handlers/users.rs @@ -0,0 +1,148 @@ +use crate::models::{Customer, NewCustomer}; +use crate::schema::customer::dsl::*; +use crate::TPool; + +use actix_identity::Identity; +use actix_web::{web, HttpResponse, Responder}; +use bcrypt::{hash, verify, DEFAULT_COST}; +use diesel::prelude::*; +use log::{error, info}; +use serde::Deserialize; + +pub async fn new_user( + pool: web::Data, + item: web::Json, +) -> impl Responder { + info!("Creating ... {:?}", item.username); + let conn = pool.get().unwrap(); + let hashed_item = NewCustomer { + password: hash(&item.password, DEFAULT_COST).unwrap(), + ..(item.into_inner()) + }; + diesel::insert_into(customer) + .values(hashed_item) + .execute(&conn) + .expect("Coundn't connect to DB"); + HttpResponse::Ok().body("Inserted successfully!") +} + +pub async fn name_exists( + pool: web::Data, + item: String, +) -> impl Responder { + let conn = pool.get().unwrap(); + info!("target: {:?}", item); + if (customer + .filter(username.eq(&item)) + .limit(1) + .load::(&conn) + .expect("Coundn't connect to DB")) + .len() + > 0 + { + HttpResponse::Ok().body("true") + } else { + HttpResponse::Ok().body("false") + } +} + +#[derive(Deserialize)] +pub struct Login { + username: String, + password: String, +} + +pub async fn login( + pool: web::Data, + cookie: Identity, + login_details: web::Json, +) -> impl Responder { + info!("Login hit"); + if let Some(uname) = cookie.identity() { + info!("Found existing cookie: {:?}", cookie.identity()); + return HttpResponse::Ok().finish(); + } + let conn = pool.get().unwrap(); + let entered_pass = &login_details.password; + let selected_user = customer + .filter(username.eq(&login_details.username)) + .limit(1) + .first::(&conn) + .expect("Couldn't connect to DB"); + let hashed_pass = selected_user.password; + if verify(entered_pass, &hashed_pass).unwrap() { + cookie.remember(login_details.username.clone()); + info!( + "Successful login: {} {}", + selected_user.username, selected_user.email_id + ); + HttpResponse::Ok().finish() + } else { + HttpResponse::Unauthorized().finish() + } +} + +pub async fn logout(cookie: Identity) -> impl Responder { + cookie.forget(); + HttpResponse::Found().header("location", "/").finish() +} + +pub async fn user_details( + uname: web::Path, + pool: web::Data, +) -> impl Responder { + let conn = pool.get().unwrap(); + let uname = uname.into_inner(); + info!("Fetching info for: \"{}\"", uname); + let selected_user = customer + .filter(username.eq(&uname)) + .limit(1) + .first::(&conn); + match selected_user { + Ok(m) => { + info!("Found user: {}", uname); + HttpResponse::Ok().json(m) + } + Err(_) => { + error!("User not found: {}", uname); + HttpResponse::NotFound().finish() + } + } +} + +#[derive(Deserialize, Debug)] +pub struct ChangePassword { + old_password: String, + new_password: String, +} + +pub async fn change_password( + cookie: Identity, + password_details: web::Json, + pool: web::Data, +) -> impl Responder { + info!("Change password request: {:?}", password_details); + let conn = pool.get().unwrap(); + if let Some(uname) = cookie.identity() { + let entered_pass = &password_details.old_password; + let new_password = &password_details.new_password; + let selected_user = customer + .filter(username.eq(&uname)) + .limit(1) + .first::(&conn) + .expect("Couldn't connect to DB"); + let hashed_pass = selected_user.password; + if verify(entered_pass, &hashed_pass).unwrap() { + let hashed_new_password = + hash(&new_password, DEFAULT_COST).unwrap(); + diesel::update(customer.filter(id.eq(selected_user.id))) + .set(password.eq(hashed_new_password)) + .execute(&conn) + .unwrap(); + return HttpResponse::Ok().body("Changed password successfully"); + } else { + return HttpResponse::Ok().body("Invalid password"); + } + } + return HttpResponse::Unauthorized().body("Login first"); +} diff --git a/backend/src/lib.rs b/backend/src/lib.rs new file mode 100644 index 0000000..d956a3f --- /dev/null +++ b/backend/src/lib.rs @@ -0,0 +1,10 @@ +#[macro_use] +extern crate diesel; + +pub mod handlers; +pub mod models; +pub mod schema; + +use diesel::r2d2::{self, ConnectionManager}; +use diesel::MysqlConnection; +pub type TPool = r2d2::Pool>; diff --git a/backend/src/models.rs b/backend/src/models.rs new file mode 100644 index 0000000..a104209 --- /dev/null +++ b/backend/src/models.rs @@ -0,0 +1,97 @@ +use super::schema::{cart_items, customer, product, rating, transaction}; + +use chrono::naive::{NaiveDate, NaiveDateTime}; +use diesel::{Insertable, Queryable}; +use serde::{Deserialize, Serialize}; + +/* Member */ +#[derive(Queryable, Serialize)] +pub struct Customer { + pub id: i32, + pub username: String, + pub password: String, + pub phone_number: String, + pub email_id: String, + pub address: Option, +} + +#[derive(Insertable, Deserialize)] +#[table_name = "customer"] +pub struct NewCustomer { + pub username: String, + pub password: String, + pub phone_number: String, + pub email_id: String, + + #[serde(skip_serializing_if = "Option::is_none")] + pub address: Option, +} + +/* Product */ +#[derive(Queryable, Serialize)] +pub struct Product { + pub id: i32, + pub name: String, + pub kind: Option, + pub price: f32, + pub description: Option, +} + +#[derive(Insertable, Deserialize)] +#[table_name = "product"] +pub struct NewProduct { + pub name: String, + + #[serde(skip_serializing_if = "Option::is_none")] + pub kind: Option, + pub price: f32, + + #[serde(skip_serializing_if = "Option::is_none")] + pub description: Option, +} + +#[derive(Deserialize)] +pub struct UpdateProduct { + pub name: String, + pub kind: Option, + pub price: f32, + pub description: Option, +} + +/* Cart Items */ +#[derive(Queryable, Serialize)] +pub struct CartItem { + pub cart_id: i32, + pub product_id: i32, +} + +#[derive(Insertable, Deserialize)] +#[table_name = "cart_items"] +pub struct AddCartItem { + pub cart_id: i32, + pub product_id: i32, +} + +/* Rating */ +#[derive(Queryable, Serialize)] +pub struct Rating { + pub id: i32, + pub comment_text: Option, + pub comment_date: Option, + pub product_id: Option, + pub customer_id: Option, + pub stars: Option, +} + +#[derive(Insertable, Deserialize)] +#[table_name = "rating"] +pub struct AddRating { + #[serde(skip_serializing_if = "Option::is_none")] + pub comment_text: Option, + + #[serde(skip_serializing_if = "Option::is_none")] + pub stars: Option, + + pub product_id: i32, + pub customer_id: i32, +} diff --git a/backend/src/schema.rs b/backend/src/schema.rs new file mode 100644 index 0000000..f08221a --- /dev/null +++ b/backend/src/schema.rs @@ -0,0 +1,61 @@ +table! { + cart_items (cart_id, product_id) { + cart_id -> Integer, + product_id -> Integer, + } +} + +table! { + customer (id) { + id -> Integer, + username -> Varchar, + password -> Varchar, + phone_number -> Varchar, + email_id -> Varchar, + address -> Nullable, + } +} + +table! { + product (id) { + id -> Integer, + name -> Varchar, + kind -> Nullable, + price -> Float, + description -> Nullable, + } +} + +table! { + rating (id) { + id -> Integer, + comment_text -> Nullable, + comment_date -> Nullable, + product_id -> Nullable, + customer_id -> Nullable, + stars -> Nullable, + } +} + +table! { + transaction (id) { + id -> Integer, + payment_type -> Varchar, + amount -> Float, + customer_id -> Nullable, + } +} + +joinable!(cart_items -> customer (cart_id)); +joinable!(cart_items -> product (product_id)); +joinable!(rating -> customer (customer_id)); +joinable!(rating -> product (product_id)); +joinable!(transaction -> customer (customer_id)); + +allow_tables_to_appear_in_same_query!( + cart_items, + customer, + product, + rating, + transaction, +); -- cgit v1.2.3