├── .gitignore ├── src ├── routes │ ├── backend │ │ ├── customer │ │ │ ├── mod.rs │ │ │ └── address.rs │ │ ├── items │ │ │ ├── mod.rs │ │ │ └── list_items.rs │ │ ├── reviews │ │ │ ├── mod.rs │ │ │ ├── list.rs │ │ │ └── new_review.rs │ │ ├── logout.rs │ │ ├── mod.rs │ │ ├── register.rs │ │ └── login.rs │ ├── admin_backend │ │ ├── mod.rs │ │ └── create_products.rs │ ├── mod.rs │ └── frontend │ │ ├── contact.rs │ │ ├── shop.rs │ │ ├── handlers.rs │ │ └── mod.rs ├── utils │ ├── mod.rs │ ├── app_state.rs │ ├── hash.rs │ ├── app_error.rs │ ├── custom_middleware.rs │ ├── custom_frontend_middleware.rs │ └── jwt.rs ├── database │ ├── mod.rs │ ├── prelude.rs │ ├── product_review.rs │ ├── address.rs │ ├── categories.rs │ ├── customer.rs │ ├── colors.rs │ ├── sizes.rs │ └── product.rs ├── lib.rs └── main.rs ├── env_example ├── README.md ├── todo.md ├── Cargo.toml ├── templates ├── components │ ├── thankyou.html │ ├── products.html │ ├── footer.html │ ├── topbar.html │ └── navbar.html ├── base.html ├── contact.html ├── cart.html ├── checkout.html ├── shop.html ├── detail.html └── index.html ├── database_setup └── init.sql └── docs └── README.md /.gitignore: -------------------------------------------------------------------------------- 1 | **/target 2 | **/dist 3 | **/.env 4 | -------------------------------------------------------------------------------- /src/routes/backend/customer/mod.rs: -------------------------------------------------------------------------------- 1 | pub mod address; 2 | -------------------------------------------------------------------------------- /src/routes/backend/items/mod.rs: -------------------------------------------------------------------------------- 1 | pub mod list_items; 2 | -------------------------------------------------------------------------------- /src/routes/backend/reviews/mod.rs: -------------------------------------------------------------------------------- 1 | pub mod new_review; 2 | pub mod list; 3 | -------------------------------------------------------------------------------- /env_example: -------------------------------------------------------------------------------- 1 | PORT="3000" 2 | BASE_ADDRESS="0.0.0.0" 3 | DATABASE_URL=postgresql://postgres:password@localhost:5432/e_commerce 4 | JWT_SECRET="NEW_SECRET" 5 | -------------------------------------------------------------------------------- /src/utils/mod.rs: -------------------------------------------------------------------------------- 1 | pub mod app_state; 2 | pub mod app_error; 3 | pub mod jwt; 4 | pub mod hash; 5 | pub mod custom_middleware; 6 | pub mod custom_frontend_middleware; 7 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ## E-Commerce Website 2 | 3 | > **DISCLAIMER:** My Initial idea was to use `yew` for the frontend but shit happened so I just used `Askama` to render the `HTML` templates. 4 | -------------------------------------------------------------------------------- /todo.md: -------------------------------------------------------------------------------- 1 | ## TODO: 2 | - [ ] finish up review listing API 3 | - [ ] The API should be able to list one review 4 | - [ ] The api should be able to list all items. 5 | 6 | - [ ] Start creating the customer address part. 7 | -------------------------------------------------------------------------------- /src/database/mod.rs: -------------------------------------------------------------------------------- 1 | //! `SeaORM` Entity. Generated by sea-orm-codegen 0.12.15 2 | 3 | pub mod prelude; 4 | 5 | pub mod address; 6 | pub mod categories; 7 | pub mod colors; 8 | pub mod customer; 9 | pub mod product; 10 | pub mod product_review; 11 | pub mod sizes; 12 | -------------------------------------------------------------------------------- /src/routes/admin_backend/mod.rs: -------------------------------------------------------------------------------- 1 | pub mod create_products; 2 | 3 | use axum::{routing::post, Router}; 4 | 5 | use crate::utils::app_state::AppState; 6 | 7 | use self::create_products::create_products; 8 | 9 | pub fn admin_routes(app_state: AppState) -> Router{ 10 | Router::new() 11 | .route("/create_products", post(create_products)) 12 | .with_state(app_state) 13 | 14 | } 15 | -------------------------------------------------------------------------------- /src/database/prelude.rs: -------------------------------------------------------------------------------- 1 | //! `SeaORM` Entity. Generated by sea-orm-codegen 0.12.15 2 | 3 | pub use super::address::Entity as Address; 4 | pub use super::categories::Entity as Categories; 5 | pub use super::colors::Entity as Colors; 6 | pub use super::customer::Entity as Customer; 7 | pub use super::product::Entity as Product; 8 | pub use super::product_review::Entity as ProductReview; 9 | pub use super::sizes::Entity as Sizes; 10 | -------------------------------------------------------------------------------- /src/utils/app_state.rs: -------------------------------------------------------------------------------- 1 | use axum::extract::FromRef; 2 | use sea_orm::DatabaseConnection; 3 | 4 | 5 | #[derive(Clone, FromRef)] 6 | pub struct AppState{ 7 | pub database: DatabaseConnection, 8 | pub base_url: Wrapper, 9 | pub jwt_secret: TokenWrapper 10 | } 11 | 12 | #[derive(Clone)] 13 | pub struct Wrapper{ 14 | pub url: String, 15 | pub port: String 16 | } 17 | 18 | #[derive(Clone)] 19 | pub struct TokenWrapper(pub String); 20 | -------------------------------------------------------------------------------- /src/database/product_review.rs: -------------------------------------------------------------------------------- 1 | //! `SeaORM` Entity. Generated by sea-orm-codegen 0.12.15 2 | 3 | use sea_orm::entity::prelude::*; 4 | 5 | #[derive(Clone, Debug, PartialEq, DeriveEntityModel, Eq)] 6 | #[sea_orm(table_name = "product_review")] 7 | pub struct Model { 8 | #[sea_orm(primary_key)] 9 | pub id: i32, 10 | pub customer_id: i32, 11 | pub product_id: i32, 12 | pub rating: i32, 13 | pub review_text: String, 14 | pub review_date: DateTime, 15 | } 16 | 17 | #[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)] 18 | pub enum Relation {} 19 | 20 | impl ActiveModelBehavior for ActiveModel {} 21 | -------------------------------------------------------------------------------- /src/database/address.rs: -------------------------------------------------------------------------------- 1 | //! `SeaORM` Entity. Generated by sea-orm-codegen 0.12.15 2 | 3 | use sea_orm::entity::prelude::*; 4 | 5 | #[derive(Clone, Debug, PartialEq, DeriveEntityModel, Eq)] 6 | #[sea_orm(table_name = "address")] 7 | pub struct Model { 8 | #[sea_orm(primary_key)] 9 | pub id: i32, 10 | pub country: String, 11 | pub state: String, 12 | pub town: String, 13 | pub zip: String, 14 | pub address_line_1: String, 15 | pub address_line_2: Option, 16 | } 17 | 18 | #[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)] 19 | pub enum Relation {} 20 | 21 | impl ActiveModelBehavior for ActiveModel {} 22 | -------------------------------------------------------------------------------- /src/database/categories.rs: -------------------------------------------------------------------------------- 1 | //! `SeaORM` Entity. Generated by sea-orm-codegen 0.12.15 2 | 3 | use sea_orm::entity::prelude::*; 4 | 5 | #[derive(Clone, Debug, PartialEq, DeriveEntityModel, Eq)] 6 | #[sea_orm(table_name = "categories")] 7 | pub struct Model { 8 | #[sea_orm(primary_key)] 9 | pub id: i32, 10 | #[sea_orm(unique)] 11 | pub name: String, 12 | } 13 | 14 | #[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)] 15 | pub enum Relation { 16 | #[sea_orm(has_many = "super::product::Entity")] 17 | Product, 18 | } 19 | 20 | impl Related for Entity { 21 | fn to() -> RelationDef { 22 | Relation::Product.def() 23 | } 24 | } 25 | 26 | impl ActiveModelBehavior for ActiveModel {} 27 | -------------------------------------------------------------------------------- /src/routes/mod.rs: -------------------------------------------------------------------------------- 1 | use axum::Router; 2 | use tower_http::services::ServeDir; 3 | 4 | use crate::utils::app_state::AppState; 5 | 6 | use self::{admin_backend::admin_routes, backend::backend_routes, frontend::frontend_routes}; 7 | 8 | pub mod backend; 9 | pub mod frontend; 10 | pub mod admin_backend; 11 | 12 | pub fn create_route(app_state: AppState) -> Router { 13 | 14 | let asset_path = std::env::current_dir().unwrap(); 15 | Router::new() 16 | .nest("/api/v1", backend_routes(app_state.clone())) 17 | .nest("/api/admin/v1", admin_routes(app_state.clone())) 18 | .nest("/", frontend_routes(app_state.clone())) 19 | .nest_service("/assets", ServeDir::new(format!("{}/assets", asset_path.to_str().unwrap()))) 20 | } 21 | -------------------------------------------------------------------------------- /src/routes/backend/logout.rs: -------------------------------------------------------------------------------- 1 | use axum::{extract::State, http::StatusCode, Extension}; 2 | use sea_orm::{ActiveModelTrait, DatabaseConnection, IntoActiveModel, Set}; 3 | 4 | use crate::{database::customer, utils::app_error::AppError}; 5 | 6 | 7 | pub async fn logout( 8 | State(database): State, 9 | Extension(user): Extension 10 | )-> Result{ 11 | 12 | let mut user = user.into_active_model(); 13 | user.token = Set(None); 14 | user.save(&database) 15 | .await 16 | .map_err(|error|{ 17 | eprintln!("Error login out: {}", error); 18 | AppError::new(StatusCode::INTERNAL_SERVER_ERROR, "Error login out, please try again later") 19 | })?; 20 | Ok(StatusCode::OK) 21 | } 22 | -------------------------------------------------------------------------------- /src/utils/hash.rs: -------------------------------------------------------------------------------- 1 | use axum::http::StatusCode; 2 | use bcrypt::{hash, verify}; 3 | 4 | use super::app_error::AppError; 5 | 6 | pub fn create_hash(password: String) -> Result{ 7 | hash(password, 13) 8 | .map_err(|error| { 9 | eprint!("error while hashing password: {}", error); 10 | AppError::new(StatusCode::INTERNAL_SERVER_ERROR, "Error occured while hashing password") 11 | }) 12 | } 13 | 14 | pub fn verifiy_pass(password: String, hash: &str) -> Result { 15 | 16 | verify(password, hash) 17 | .map_err(|error|{ 18 | eprintln!("Error verifying your password: {}", error); 19 | AppError::new(StatusCode::INTERNAL_SERVER_ERROR, "Something went wrong while verifying your password") 20 | }) 21 | } 22 | 23 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "ecommerce" 3 | version = "0.1.0" 4 | edition = "2021" 5 | 6 | [dependencies] 7 | askama = { version = "0.12.1", features = ["serde", "serde-json"] } 8 | axum = { version = "0.7.5", features = ["macros"] } 9 | axum-extra = { version = "0.9.3", features = ["typed-header"] } 10 | bcrypt = "0.15.1" 11 | chrono = { version = "0.4.38", features = ["serde"] } 12 | dotenvy = "0.15.7" 13 | dotenvy_macro = "0.15.7" 14 | hyper = "1.3.1" 15 | jsonwebtoken = "9.3.0" 16 | reqwest = { version = "0.12.5", features = ["json"] } 17 | sea-orm = { version = "0.12.15", features = ["sqlx-postgres", "runtime-tokio-rustls"] } 18 | serde = { version = "1.0.198", features = ["derive"] } 19 | tokio = { version = "1.37.0", features = ["macros", "rt-multi-thread", "net"] } 20 | tower-http = { version = "0.5.2", features = ["fs"] } 21 | -------------------------------------------------------------------------------- /src/utils/app_error.rs: -------------------------------------------------------------------------------- 1 | use axum::{http::StatusCode, response::IntoResponse, Json}; 2 | use serde::{Deserialize, Serialize}; 3 | 4 | #[derive(Debug)] 5 | pub struct AppError{ 6 | code: StatusCode, 7 | message: String 8 | } 9 | 10 | impl AppError { 11 | 12 | pub fn new( code: StatusCode, message: impl Into) -> Self { 13 | 14 | Self{ 15 | code, 16 | message: message.into() 17 | } 18 | } 19 | } 20 | 21 | impl IntoResponse for AppError { 22 | fn into_response(self) -> axum::response::Response { 23 | ( 24 | self.code, 25 | Json(ErrorResponse{ error: self.message.clone()}) 26 | ).into_response() 27 | } 28 | 29 | } 30 | 31 | #[derive(Serialize, Deserialize)] 32 | struct ErrorResponse{ 33 | error: String 34 | } 35 | -------------------------------------------------------------------------------- /src/database/customer.rs: -------------------------------------------------------------------------------- 1 | //! `SeaORM` Entity. Generated by sea-orm-codegen 0.12.15 2 | 3 | use sea_orm::entity::prelude::*; 4 | 5 | #[derive(Clone, Debug, PartialEq, DeriveEntityModel, Eq)] 6 | #[sea_orm(table_name = "customer")] 7 | pub struct Model { 8 | #[sea_orm(primary_key)] 9 | pub id: i32, 10 | #[sea_orm(unique)] 11 | pub username: String, 12 | pub first_name: String, 13 | pub last_name: String, 14 | pub email: String, 15 | pub telephone: String, 16 | #[sea_orm(unique)] 17 | pub default_address_id: Option, 18 | #[sea_orm(unique)] 19 | pub salt: String, 20 | pub password_hash: String, 21 | #[sea_orm(column_type = "Text", nullable)] 22 | pub token: Option, 23 | } 24 | 25 | #[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)] 26 | pub enum Relation {} 27 | 28 | impl ActiveModelBehavior for ActiveModel {} 29 | -------------------------------------------------------------------------------- /src/database/colors.rs: -------------------------------------------------------------------------------- 1 | //! `SeaORM` Entity. Generated by sea-orm-codegen 0.12.15 2 | 3 | use sea_orm::entity::prelude::*; 4 | 5 | #[derive(Clone, Debug, PartialEq, DeriveEntityModel, Eq)] 6 | #[sea_orm(table_name = "colors")] 7 | pub struct Model { 8 | #[sea_orm(primary_key)] 9 | pub id: i32, 10 | pub product_id: i32, 11 | pub color: String, 12 | pub quantity: i32, 13 | } 14 | 15 | #[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)] 16 | pub enum Relation { 17 | #[sea_orm( 18 | belongs_to = "super::product::Entity", 19 | from = "Column::ProductId", 20 | to = "super::product::Column::Id", 21 | on_update = "NoAction", 22 | on_delete = "Cascade" 23 | )] 24 | Product, 25 | } 26 | 27 | impl Related for Entity { 28 | fn to() -> RelationDef { 29 | Relation::Product.def() 30 | } 31 | } 32 | 33 | impl ActiveModelBehavior for ActiveModel {} 34 | -------------------------------------------------------------------------------- /src/database/sizes.rs: -------------------------------------------------------------------------------- 1 | //! `SeaORM` Entity. Generated by sea-orm-codegen 0.12.15 2 | 3 | use sea_orm::entity::prelude::*; 4 | 5 | #[derive(Clone, Debug, PartialEq, DeriveEntityModel, Eq)] 6 | #[sea_orm(table_name = "sizes")] 7 | pub struct Model { 8 | #[sea_orm(primary_key)] 9 | pub id: i32, 10 | pub product_id: i32, 11 | pub size: String, 12 | pub quantity: i32, 13 | } 14 | 15 | #[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)] 16 | pub enum Relation { 17 | #[sea_orm( 18 | belongs_to = "super::product::Entity", 19 | from = "Column::ProductId", 20 | to = "super::product::Column::Id", 21 | on_update = "NoAction", 22 | on_delete = "Cascade" 23 | )] 24 | Product, 25 | } 26 | 27 | impl Related for Entity { 28 | fn to() -> RelationDef { 29 | Relation::Product.def() 30 | } 31 | } 32 | 33 | impl ActiveModelBehavior for ActiveModel {} 34 | -------------------------------------------------------------------------------- /src/lib.rs: -------------------------------------------------------------------------------- 1 | pub mod utils; 2 | pub mod database; 3 | mod routes; 4 | 5 | use axum::http::StatusCode; 6 | use routes::create_route; 7 | use utils::{app_error::AppError, app_state::AppState}; 8 | 9 | pub async fn launch(app_state: AppState)-> Result<(), AppError>{ 10 | let apps_state = app_state.clone(); 11 | let app = create_route(app_state); 12 | 13 | let address = format!("{}:{}", apps_state.base_url.url, apps_state.base_url.port); 14 | let listenter = tokio::net::TcpListener::bind(&address) 15 | .await 16 | .map_err(|error|{ 17 | eprintln!("There was an issue with the bind address {}, {}", address, error); 18 | AppError::new(StatusCode::INTERNAL_SERVER_ERROR, "Could not connect with the address and port") 19 | })?; 20 | 21 | axum::serve(listenter, app) 22 | .await 23 | .map_err(|error|{ 24 | eprintln!("Could not start the server: {}", error); 25 | AppError::new(StatusCode::INTERNAL_SERVER_ERROR, "Could not start the server") 26 | }) 27 | 28 | } 29 | 30 | -------------------------------------------------------------------------------- /templates/components/thankyou.html: -------------------------------------------------------------------------------- 1 | 2 | {% extends "base.html" %} 3 | {% block content %} 4 | 5 | {% include "components/topbar.html" %} 6 | 7 | 8 | 9 | 10 | {% include "components/navbar.html" %} 11 | 12 | 13 | 14 | 15 |
16 |
17 |
18 | 22 |
23 |
24 |
25 | 26 | 27 |
28 |

Thankyou!

29 |

{{ message }}

30 |
31 | 32 | 33 | 34 | 35 | {% include "components/footer.html" %} 36 | 37 | 38 | {% endblock %} 39 | -------------------------------------------------------------------------------- /src/main.rs: -------------------------------------------------------------------------------- 1 | use axum::http::StatusCode; 2 | use ecommerce::{launch, utils::{app_error::AppError, app_state::{AppState, TokenWrapper, Wrapper}}}; 3 | use dotenvy_macro::dotenv; 4 | use sea_orm::Database; 5 | 6 | 7 | #[tokio::main] 8 | async fn main() -> Result<(), AppError> { 9 | 10 | dotenvy::dotenv().ok(); 11 | let port = dotenv!("PORT").to_string(); 12 | let base_url = dotenv!("BASE_ADDRESS").to_string(); 13 | let database_url = dotenv!("DATABASE_URL"); 14 | let jwt_secret = dotenv!("JWT_SECRET").to_string(); 15 | let database = Database::connect(database_url) 16 | .await 17 | .map_err(|error|{ 18 | eprintln!("Error could not connect to the database: {}", error); 19 | AppError::new(StatusCode::INTERNAL_SERVER_ERROR, "Could not connect to the database") 20 | })?; 21 | let app_state = AppState{ 22 | database, 23 | base_url: Wrapper { url: base_url, port }, 24 | jwt_secret: TokenWrapper(jwt_secret) 25 | 26 | }; 27 | launch(app_state).await?; 28 | Ok(()) 29 | } 30 | -------------------------------------------------------------------------------- /src/routes/frontend/contact.rs: -------------------------------------------------------------------------------- 1 | use axum::{extract::rejection::FormRejection, response::{Html, IntoResponse}, Form}; 2 | use serde::{Deserialize, Serialize}; 3 | 4 | use crate::routes::frontend::{handlers::{ContactTemplate, ThankyouTemplate}, HtmlTemplate}; 5 | 6 | #[derive(Serialize, Deserialize, Debug)] 7 | pub struct ContactDetails{ 8 | name: String, 9 | email: String, 10 | subject: String, 11 | message: String 12 | } 13 | 14 | pub async fn contact_us(form_res: Result, FormRejection> ) -> impl IntoResponse { 15 | dbg!(&form_res); 16 | match form_res { 17 | Ok(Form(details)) => { 18 | 19 | dbg!(&details); 20 | let message = format!("Thankyou {}, Your form has been submitted successfull", details.name); 21 | let temp = ThankyouTemplate{ 22 | message 23 | }; 24 | HtmlTemplate(temp) 25 | 26 | } 27 | Err(e) => { 28 | eprintln!("something went wrong: {}:", e); 29 | let message = format!("Something went wrong"); 30 | let temp = ThankyouTemplate{ 31 | message 32 | }; 33 | HtmlTemplate(temp) 34 | } 35 | 36 | } 37 | 38 | 39 | 40 | } 41 | -------------------------------------------------------------------------------- /src/routes/frontend/shop.rs: -------------------------------------------------------------------------------- 1 | use askama::Template; 2 | use axum::{extract::{Request, State}, http::HeaderValue, response::IntoResponse}; 3 | use hyper::{header::AUTHORIZATION, HeaderMap}; 4 | use reqwest::Client; 5 | use sea_orm::prelude::Decimal; 6 | use serde::{Deserialize, Serialize}; 7 | 8 | use crate::utils::{app_state::AppState, custom_frontend_middleware::frontend_guard}; 9 | 10 | use super::HtmlTemplate; 11 | 12 | #[derive(Template)] 13 | #[template(path="shop.html")] 14 | pub struct ShopTemplate{ 15 | token: String, 16 | } 17 | 18 | #[derive(Serialize, Deserialize, Debug)] 19 | pub struct Items{ 20 | product_name: String, 21 | description: String, 22 | image_name: String, 23 | price: Decimal, 24 | quantity: String, 25 | date_added: String, 26 | } 27 | 28 | pub async fn shop(State(client): State, mut req: Request) -> impl IntoResponse{ 29 | let url = "http://localhost:3000/api/v1/list"; 30 | let jwt_token = "eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJleHAiOjE3MjA1NDMzODcsImlhdCI6MTcyMDU0Mjc4N30.969xF0E3uLS-VFYgGD34lI0vTQo9dfzs2cyYKwSFfRc"; 31 | 32 | let tkn = format!("Bearer {}", jwt_token).parse().unwrap(); 33 | req.headers_mut().insert("Authorization", tkn); 34 | 35 | let temp = ShopTemplate{ 36 | token: jwt_token.to_string() 37 | }; 38 | HtmlTemplate(temp) 39 | 40 | } 41 | -------------------------------------------------------------------------------- /src/routes/backend/mod.rs: -------------------------------------------------------------------------------- 1 | mod register; 2 | mod login; 3 | mod logout; 4 | mod items; 5 | mod reviews; 6 | mod customer; 7 | use axum::{middleware, routing::{get, post}, Router}; 8 | use serde::{Deserialize, Serialize}; 9 | 10 | use crate::utils::{app_state::AppState, custom_middleware::guard_routes}; 11 | use self::{customer::address::create_address, items::list_items::{list_all_items, list_one_item}, login::login, logout::logout, register::register, reviews::{list::{list_all_reviews, list_reviews}, new_review::create_review}}; 12 | 13 | pub fn backend_routes(app_state: AppState) -> Router{ 14 | Router::new() 15 | .route("/address", post(create_address)) 16 | .route("/review", post(create_review)) 17 | .route("/review/:id", get(list_reviews)) 18 | .route("/review", get(list_all_reviews)) 19 | .route("/logout", post(logout)) 20 | .route("/list/:id", get(list_one_item)) 21 | .route("/list", get(list_all_items)) 22 | .route_layer(middleware::from_fn_with_state(app_state.clone(), guard_routes)) 23 | .route("/register", post(register)) 24 | .route("/login", post(login)) 25 | .with_state(app_state) 26 | 27 | } 28 | 29 | 30 | #[derive(Serialize, Deserialize)] 31 | pub struct RespondUser{ 32 | user_id: i32, 33 | username: String, 34 | telephone: String, 35 | email: String, 36 | pub token: Option 37 | } 38 | -------------------------------------------------------------------------------- /src/routes/frontend/handlers.rs: -------------------------------------------------------------------------------- 1 | use askama::Template; 2 | use axum::{response::IntoResponse, Form}; 3 | use serde::{Deserialize, Serialize}; 4 | 5 | use super::{contact::ContactDetails, HtmlTemplate}; 6 | 7 | #[derive(Template)] 8 | #[template(path="index.html")] 9 | pub struct HomeTemplate{} 10 | 11 | pub async fn home() -> impl IntoResponse{ 12 | let temp = HomeTemplate{}; 13 | HtmlTemplate(temp) 14 | 15 | } 16 | 17 | #[derive(Template)] 18 | #[template(path="contact.html")] 19 | pub struct ContactTemplate{} 20 | 21 | 22 | pub async fn contact() -> impl IntoResponse{ 23 | let temp = ContactTemplate{}; 24 | HtmlTemplate(temp) 25 | 26 | } 27 | 28 | #[derive(Template)] 29 | #[template(path="detail.html")] 30 | pub struct DetailTemplate{} 31 | 32 | pub async fn detail() -> impl IntoResponse{ 33 | let temp = DetailTemplate{}; 34 | HtmlTemplate(temp) 35 | 36 | } 37 | 38 | #[derive(Template)] 39 | #[template(path="checkout.html")] 40 | pub struct CheckoutTemplate{} 41 | 42 | pub async fn checkout() -> impl IntoResponse{ 43 | let temp = CheckoutTemplate{}; 44 | HtmlTemplate(temp) 45 | 46 | } 47 | 48 | 49 | 50 | #[derive(Template)] 51 | #[template(path="cart.html")] 52 | pub struct CartTemplate{} 53 | 54 | pub async fn cart() -> impl IntoResponse{ 55 | let temp = CartTemplate{}; 56 | HtmlTemplate(temp) 57 | 58 | } 59 | 60 | #[derive(Template)] 61 | #[template(path="components/thankyou.html")] 62 | pub struct ThankyouTemplate{ 63 | pub message: String 64 | } 65 | 66 | -------------------------------------------------------------------------------- /src/utils/custom_middleware.rs: -------------------------------------------------------------------------------- 1 | use axum::{extract::{Request, State}, http::StatusCode, middleware::Next, response::Response}; 2 | use axum_extra::headers::{authorization::Bearer, Authorization, HeaderMapExt}; 3 | use sea_orm::{ColumnTrait, DatabaseConnection, EntityTrait, QueryFilter}; 4 | 5 | use super::{app_error::AppError, app_state::TokenWrapper}; 6 | use crate::{database::customer::{self, Entity as Users}, utils::jwt::validate_jwt}; 7 | 8 | pub async fn guard_routes( 9 | State(database): State, 10 | State(jwt_secret): State, 11 | mut request: Request, 12 | next: Next 13 | )-> Result { 14 | 15 | let token = request.headers().typed_get::>() 16 | .ok_or_else(||AppError::new(StatusCode::BAD_REQUEST, "Not authenticated Please login"))? 17 | .token() 18 | .to_owned(); 19 | 20 | let user = Users::find() 21 | .filter(customer::Column::Token.eq(&token)) 22 | .one(&database) 23 | .await 24 | .map_err(|error|{ 25 | eprintln!("Error finding user: {}", error); 26 | AppError::new(StatusCode::INTERNAL_SERVER_ERROR, "Something went wrong") 27 | })?; 28 | validate_jwt(&jwt_secret.0, &token)?; 29 | 30 | if let Some(user) = user{ 31 | request.extensions_mut().insert(user); 32 | 33 | }else { 34 | return Err( AppError::new(StatusCode::UNAUTHORIZED, "You are not authorized")); 35 | } 36 | Ok(next.run(request).await) 37 | } 38 | 39 | -------------------------------------------------------------------------------- /src/utils/custom_frontend_middleware.rs: -------------------------------------------------------------------------------- 1 | 2 | use axum::{body::Body, extract::{Request, State}, middleware::Next, response::Response}; 3 | use axum_extra::headers::{authorization::{self, Bearer}, Authorization, HeaderMapExt}; 4 | use reqwest::{StatusCode}; 5 | use sea_orm::{ColumnTrait, DatabaseConnection, EntityTrait, QueryFilter}; 6 | 7 | use super::{app_error::AppError, app_state::TokenWrapper, jwt::validate_jwt}; 8 | use crate::database::customer::{self, Entity as Users}; 9 | 10 | pub async fn frontend_guard( 11 | State(database): State, 12 | 13 | State(jwt_secret): State, 14 | mut req: Request, 15 | next: Next, 16 | ) -> Result { 17 | 18 | let token = req.headers().typed_get::>() 19 | .ok_or(AppError::new(StatusCode::UNAUTHORIZED, "No token found"))? 20 | .token() 21 | .to_owned(); 22 | 23 | let user = Users::find() 24 | .filter(customer::Column::Token.eq(&token)) 25 | .one(&database) 26 | .await 27 | .map_err(|error|{ 28 | eprintln!("Error finding user: {}", error); 29 | AppError::new(StatusCode::INTERNAL_SERVER_ERROR, "Something went wrong") 30 | })?; 31 | validate_jwt(&jwt_secret.0, &token)?; 32 | 33 | if let Some(user) = user{ 34 | req.extensions_mut().insert(user); 35 | 36 | }else { 37 | return Err( AppError::new(StatusCode::UNAUTHORIZED, "You are not authorized")); 38 | } 39 | 40 | 41 | Ok(next.run(req).await) 42 | 43 | 44 | } 45 | -------------------------------------------------------------------------------- /src/database/product.rs: -------------------------------------------------------------------------------- 1 | //! `SeaORM` Entity. Generated by sea-orm-codegen 0.12.15 2 | 3 | use sea_orm::entity::prelude::*; 4 | 5 | #[derive(Clone, Debug, PartialEq, DeriveEntityModel, Eq)] 6 | #[sea_orm(table_name = "product")] 7 | pub struct Model { 8 | #[sea_orm(primary_key)] 9 | pub id: i32, 10 | pub product_name: String, 11 | #[sea_orm(column_type = "Decimal(Some((10, 2)))")] 12 | pub price: Decimal, 13 | pub star: Option, 14 | pub image_name: String, 15 | pub date_added: DateTime, 16 | pub category_id: i32, 17 | #[sea_orm(column_type = "Text")] 18 | pub description: String, 19 | pub quantity: i32, 20 | } 21 | 22 | #[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)] 23 | pub enum Relation { 24 | #[sea_orm( 25 | belongs_to = "super::categories::Entity", 26 | from = "Column::CategoryId", 27 | to = "super::categories::Column::Id", 28 | on_update = "NoAction", 29 | on_delete = "NoAction" 30 | )] 31 | Categories, 32 | #[sea_orm(has_many = "super::colors::Entity")] 33 | Colors, 34 | #[sea_orm(has_many = "super::sizes::Entity")] 35 | Sizes, 36 | } 37 | 38 | impl Related for Entity { 39 | fn to() -> RelationDef { 40 | Relation::Categories.def() 41 | } 42 | } 43 | 44 | impl Related for Entity { 45 | fn to() -> RelationDef { 46 | Relation::Colors.def() 47 | } 48 | } 49 | 50 | impl Related for Entity { 51 | fn to() -> RelationDef { 52 | Relation::Sizes.def() 53 | } 54 | } 55 | 56 | impl ActiveModelBehavior for ActiveModel {} 57 | -------------------------------------------------------------------------------- /src/utils/jwt.rs: -------------------------------------------------------------------------------- 1 | use axum::http::StatusCode; 2 | use chrono::{Duration, Utc}; 3 | use jsonwebtoken::{decode, encode, Algorithm, DecodingKey, EncodingKey, Header, Validation}; 4 | use serde::{Deserialize, Serialize}; 5 | 6 | use super::app_error::AppError; 7 | 8 | #[derive(Debug, Serialize, Deserialize)] 9 | struct Claims{ 10 | exp: usize, 11 | iat: usize 12 | } 13 | pub fn create_jwt(jwt_secret: &str) -> Result { 14 | 15 | let now = Utc::now(); 16 | let iat = now.timestamp() as usize; 17 | let exp_time = Duration::try_minutes(10).expect("Enter valid number"); 18 | let exp = (now + exp_time).timestamp() as usize; 19 | 20 | let claims = Claims{ 21 | iat, 22 | exp 23 | 24 | }; 25 | let key = &EncodingKey::from_secret(jwt_secret.as_bytes()); 26 | encode(&Header::default(), &claims, key) 27 | .map_err(|error|{ 28 | eprintln!("Could not create jwt {}", error); 29 | AppError::new(StatusCode::INTERNAL_SERVER_ERROR, "Error occured when creating jwt token, Please try again later") 30 | }) 31 | 32 | } 33 | pub fn validate_jwt(jwt_secret: &str, token: &str) -> Result{ 34 | let key = DecodingKey::from_secret(jwt_secret.as_bytes()); 35 | let validation = &Validation::new(Algorithm::HS256); 36 | 37 | decode::(token, &key, &validation) 38 | .map_err(|error| 39 | match error.kind() { 40 | jsonwebtoken::errors::ErrorKind::ExpiredSignature => AppError::new(StatusCode::UNAUTHORIZED, "Your session has expired"), 41 | _=> { 42 | eprintln!("Error validating token: {}", error); 43 | AppError::new(StatusCode::INTERNAL_SERVER_ERROR, "Something went wrong please try again later") 44 | } 45 | 46 | })?; 47 | 48 | Ok(true) 49 | } 50 | -------------------------------------------------------------------------------- /templates/base.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Collo shop 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | {% block content %} 31 | 32 | {% endblock %} 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | -------------------------------------------------------------------------------- /src/routes/backend/customer/address.rs: -------------------------------------------------------------------------------- 1 | use axum::http::StatusCode; 2 | use axum::{extract::State, Json}; 3 | use sea_orm::{ActiveModelTrait, DatabaseConnection, Set, TryIntoModel}; 4 | use serde::{Deserialize, Serialize}; 5 | 6 | use crate::utils::app_error::AppError; 7 | use crate::database::address as Address; 8 | 9 | #[derive(Serialize, Deserialize)] 10 | pub struct RequestAddress{ 11 | country: String, 12 | state: String, 13 | town: String, 14 | zip: String, 15 | address_line_1: String, 16 | address_line_2: Option 17 | } 18 | 19 | #[derive(Serialize, Deserialize)] 20 | pub struct RespondAddress{ 21 | id: i32, 22 | country: String, 23 | state: String, 24 | town: String, 25 | zip: String, 26 | address_line_1: String, 27 | address_line_2: Option 28 | 29 | } 30 | 31 | pub async fn create_address( 32 | State(database): State, 33 | Json(request_address): Json 34 | ) -> Result, AppError> { 35 | 36 | let address = Address::ActiveModel{ 37 | country: Set(request_address.country), 38 | state: Set(request_address.state), 39 | town: Set(request_address.town), 40 | zip: Set(request_address.zip), 41 | address_line_1: Set(request_address.address_line_1), 42 | address_line_2: Set(request_address.address_line_2), 43 | ..Default::default() 44 | }.save(&database) 45 | .await 46 | .map_err(|error|{ 47 | eprintln!("[*] Error adding address to the database: {}", error); 48 | AppError::new(StatusCode::INTERNAL_SERVER_ERROR, "Something went wrong") 49 | })?; 50 | 51 | let address_new_model = address.try_into_model() 52 | .map_err(|error|{ 53 | eprintln!("[*] Error converting into model {}", error); 54 | AppError::new(StatusCode::INTERNAL_SERVER_ERROR, "Something went wrong") 55 | })?; 56 | 57 | Ok(Json(RespondAddress { id: address_new_model.id, country: address_new_model.country, state: address_new_model.state, town: address_new_model.town, zip: address_new_model.zip, address_line_1: address_new_model.address_line_1, address_line_2: address_new_model.address_line_2 })) 58 | } 59 | -------------------------------------------------------------------------------- /templates/components/products.html: -------------------------------------------------------------------------------- 1 | {% for product in products %} 2 |
3 |
4 |
5 | 6 |
7 | 8 | 9 | 10 | 11 |
12 |
13 |
14 | {{ product.product_name }} 15 |
16 |
KSH {{ product.price }}
17 |
18 |
19 | 20 | 21 | 22 | 23 | 24 | ({{ product.quantity}}) 25 |
26 |
27 |
28 |
29 | {% endfor %} 30 | 31 | -------------------------------------------------------------------------------- /database_setup/init.sql: -------------------------------------------------------------------------------- 1 | CREATE TABLE categories ( 2 | id SERIAL PRIMARY KEY, 3 | name VARCHAR(255) NOT NULL UNIQUE 4 | ); 5 | 6 | CREATE TABLE product ( 7 | id SERIAL PRIMARY KEY, 8 | product_name VARCHAR(255) NOT NULL UNIQUE, 9 | price DECIMAL(10, 2) NOT NULL, 10 | star INTEGER CHECK (star >= 0 AND star <= 5), 11 | image_name VARCHAR(255) NOT NULL, 12 | date_added TIMESTAMP DEFAULT CURRENT_TIMESTAMP NOT NULL, 13 | category_id INTEGER NOT NULL, 14 | description TEXT NOT NULL, 15 | quantity INTEGER NOT NULL CHECK (quantity >= 0), 16 | FOREIGN KEY (category_id) REFERENCES categories(id) 17 | ); 18 | 19 | CREATE TABLE colors ( 20 | id SERIAL PRIMARY KEY, 21 | product_id INTEGER NOT NULL, 22 | color VARCHAR(50) NOT NULL, 23 | quantity INTEGER NOT NULL CHECK (quantity >= 0), 24 | FOREIGN KEY (product_id) REFERENCES product(id) ON DELETE CASCADE 25 | ); 26 | 27 | CREATE TABLE sizes ( 28 | id SERIAL PRIMARY KEY, 29 | product_id INTEGER NOT NULL, 30 | size VARCHAR(50) NOT NULL, 31 | quantity INTEGER NOT NULL CHECK (quantity >= 0), 32 | FOREIGN KEY (product_id) REFERENCES product(id) ON DELETE CASCADE 33 | ); 34 | 35 | create table customer ( 36 | id int generated always as identity primary key, 37 | username varchar(50) not null unique, 38 | first_name varchar(50) not null, 39 | last_name varchar(50) not null, 40 | email varchar(320) not null, 41 | telephone varchar(320) not null, 42 | default_address_id int unique, 43 | salt varchar(64) unique not null, 44 | password_hash varchar(64) not null, 45 | token text default null 46 | ); 47 | 48 | create table address ( 49 | id int generated always as identity primary key, 50 | country varchar(50) not null, 51 | state varchar(50) not null, 52 | town varchar(50) not null, 53 | zip varchar(20) not null, 54 | address_line_1 varchar(100) not null, 55 | address_line_2 varchar(100) 56 | ); 57 | 58 | 59 | create table product_review ( 60 | id int generated always as identity primary key, 61 | customer_id int not null, 62 | product_id int not null, 63 | rating int not null, 64 | review_text varchar(1000) not null, 65 | review_date timestamp without time zone default current_timestamp not null 66 | ); 67 | -------------------------------------------------------------------------------- /src/routes/backend/reviews/list.rs: -------------------------------------------------------------------------------- 1 | use axum::{extract::{Path, State}, http::StatusCode, Json}; 2 | use chrono::NaiveDateTime; 3 | use sea_orm::{DatabaseConnection, EntityTrait}; 4 | use serde::{Deserialize, Serialize}; 5 | 6 | use crate::utils::app_error::AppError; 7 | use crate::database::product_review::Entity as Review; 8 | 9 | #[derive(Serialize, Deserialize)] 10 | pub struct PostReviews{ 11 | review_id: i32, 12 | product_id: i32, 13 | customer_id: i32, 14 | rating: i32, 15 | review_message: String, 16 | review_date: NaiveDateTime 17 | 18 | 19 | } 20 | pub async fn list_reviews( 21 | State(database): State, 22 | Path(id): Path 23 | ) -> Result, AppError>{ 24 | let review = Review::find_by_id(id) 25 | .one(&database) 26 | .await 27 | .map_err(|error|{ 28 | eprintln!("[*] Error finding the review {}", error); 29 | AppError::new(StatusCode::INTERNAL_SERVER_ERROR, "Review not found") 30 | })?; 31 | if let Some(review) = review{ 32 | return Ok(Json(PostReviews { review_id: review.id, product_id: review.product_id, customer_id: review.customer_id, rating: review.rating, review_message: review.review_text, review_date: review.review_date })); 33 | }else { 34 | Err(AppError::new(StatusCode::NOT_FOUND, "Review not found")) 35 | } 36 | 37 | } 38 | 39 | pub async fn list_all_reviews( 40 | State(database): State 41 | ) -> Result>, AppError>{ 42 | 43 | let reviews = Review::find() 44 | .all(&database) 45 | .await 46 | .map_err(|error|{ 47 | eprintln!("[*] Error finding the review, {}", error); 48 | AppError::new(StatusCode::INTERNAL_SERVER_ERROR, "Something went wrong") 49 | })?; 50 | 51 | let post: Vec = reviews.iter() 52 | .map(|rev| PostReviews{ 53 | review_id: rev.id, 54 | product_id: rev.product_id, 55 | customer_id: rev.customer_id, 56 | rating: rev.rating, 57 | review_message: rev.clone().review_text, 58 | review_date: rev.review_date 59 | }).collect(); 60 | 61 | Ok(Json(post)) 62 | } 63 | -------------------------------------------------------------------------------- /src/routes/backend/items/list_items.rs: -------------------------------------------------------------------------------- 1 | use axum::{extract::{Path, State}, http::StatusCode, Json}; 2 | use sea_orm::{prelude::Decimal, DatabaseConnection, EntityTrait }; 3 | use serde::Serialize; 4 | 5 | use crate::{database::product::Entity as Product, utils::app_error::AppError}; 6 | 7 | #[derive(Serialize)] 8 | pub struct Items{ 9 | product_name: String, 10 | description: String, 11 | image_name: String, 12 | price: Decimal, 13 | quantity: i32, 14 | date_added: String, 15 | 16 | } 17 | pub async fn list_one_item( 18 | State(database): State, 19 | Path(id): Path 20 | ) -> Result, AppError> { 21 | 22 | let item = Product::find_by_id(id) 23 | .one(&database) 24 | .await 25 | .map_err(|error|{ 26 | eprintln!("Error: Getting tasks from the database, {}", error); 27 | AppError::new(StatusCode::INTERNAL_SERVER_ERROR, "There was something that went wrong") 28 | })?; 29 | if let Some(item) = item{ 30 | return Ok(Json(Items { 31 | product_name: item.product_name, 32 | description: item.description, 33 | image_name: item.image_name, 34 | price: item.price, 35 | quantity: item.quantity, 36 | date_added: item.date_added.to_string() 37 | })); 38 | }else { 39 | 40 | Err(AppError::new(StatusCode::NOT_FOUND, "product not found")) 41 | } 42 | 43 | } 44 | 45 | 46 | pub async fn list_all_items( 47 | State(database): State 48 | ) -> Result>, AppError>{ 49 | 50 | let items: Vec = Product::find() 51 | .all(&database) 52 | .await 53 | .map_err(|error|{ 54 | eprintln!("Error getting all tasks, {}", error); 55 | AppError::new(StatusCode::INTERNAL_SERVER_ERROR, "Something went wrong") 56 | })? 57 | .into_iter() 58 | .map(|db_item| Items{ 59 | product_name: db_item.product_name, 60 | description: db_item.description, 61 | image_name: db_item.image_name, 62 | price: db_item.price, 63 | quantity: db_item.quantity, 64 | date_added: db_item.date_added.to_string() 65 | }) 66 | .collect(); 67 | 68 | Ok(Json(items)) 69 | } 70 | -------------------------------------------------------------------------------- /docs/README.md: -------------------------------------------------------------------------------- 1 | # E-Commerce backend 2 | 3 | ## Features 4 | 5 | - [ ] Create 6 | - [x] User account 7 | - [x] Being able to create new user account 8 | - [x] The user being able to login to their account 9 | - [x] The user would be logged out after a certain period of time 10 | - [x] The user should be able to logout 11 | - [ ] Admin account 12 | - [ ] Account Setup 13 | - [ ] Create new admin account 14 | - [ ] Admin should be able to login to their account 15 | - [ ] Admin would be logged out after a certain period of time 16 | - [ ] Admin should be able to logout 17 | - [ ] Products 18 | - [ ] Be able to create new products 19 | 20 | - [ ] Read 21 | - [x] Get a list of all products with: 22 | - [x] All corresponding price 23 | - [x] All corresponding number of items in stock 24 | - [x] Get one product 25 | - [x] With the corresponding price, description of the product and number of items in stock 26 | - [ ] Get a list of all users accounts (only admin) 27 | - [ ] with Username, First Name, Last Name and Telephone Number 28 | - [ ] Get individual user account (admin only) 29 | - [ ] with Username, First Name, Last Name and Telephone Number 30 | 31 | 32 | 33 | - [ ] Update 34 | 35 | 36 | - [ ] Delete 37 | 38 | ### APIs 39 | 40 | Users endpoints: 41 | - `POST /api/v1/user/create_account` 42 | - `POST /api/v1/user/login` 43 | - `POST /api/v1/user/logout` 44 | 45 | Admin endpoints: 46 | - `POST /api/v1/admin/create_account` 47 | - `POST /api/v1/admin/login` 48 | - `POST /api/v1/admin/logout` 49 | - `POST /api/v1/admin/new_product` - create product 50 | - `GET /api/v1/users` - list all users 51 | - `GET /api/v1/users/:id` - list one user 52 | 53 | Other endpoints: 54 | - `GET /api/v1/products` - List all product 55 | - `GET /api/v1/products/:id` - List one product 56 | 57 | 58 | ## Tech 59 | 60 | - Axum 0.7.5 61 | - Features: 62 | - macros 63 | 64 | - Tokio v1.37.0 65 | - Features: 66 | - macros 67 | - rt-multi-thread 68 | - net 69 | - dotenvy v0.15.7 70 | - dotenvy_macro v0.15.7 71 | - serde v1.0.198 72 | - Features: 73 | - derive 74 | ## Setup 75 | 76 | 77 | ## Database 78 | 79 | 80 | ## Testing 81 | 82 | > Using curl 83 | -------------------------------------------------------------------------------- /src/routes/backend/register.rs: -------------------------------------------------------------------------------- 1 | use axum::extract::State; 2 | use axum::http::StatusCode; 3 | use axum::Json; 4 | use sea_orm::{ActiveModelTrait, DatabaseConnection, Set, TryIntoModel}; 5 | use serde::{Deserialize, Serialize}; 6 | 7 | use crate::utils::app_error::AppError; 8 | use crate::database::customer as User; 9 | use crate::utils::hash::create_hash; 10 | 11 | use super::RespondUser; 12 | 13 | #[derive(Serialize, Deserialize)] 14 | pub struct RequestUser{ 15 | username: String, 16 | first_name: String, 17 | last_name: String, 18 | email: String, 19 | telephone: String, 20 | default_address_id: Option, 21 | password: String, 22 | salt: String, 23 | password_hash: String, 24 | } 25 | 26 | 27 | pub async fn register( 28 | State(database): State, 29 | Json(request_user): Json 30 | ) -> Result, AppError>{ 31 | 32 | let new_user = User::ActiveModel{ 33 | username: Set(request_user.username), 34 | first_name: Set(request_user.first_name), 35 | last_name: Set(request_user.last_name), 36 | email: Set(request_user.email), 37 | telephone: Set(request_user.telephone), 38 | default_address_id: Set(request_user.default_address_id), 39 | password_hash: Set(create_hash(request_user.password)?), 40 | salt: Set(request_user.salt), 41 | ..Default::default() 42 | }.save(&database) 43 | .await 44 | .map_err(|error|{ 45 | let error_mess = error.to_string(); 46 | if error_mess.contains("duplicate key value violates unique constraint"){ 47 | return AppError::new(StatusCode::BAD_REQUEST, "Another user having those details"); 48 | } 49 | eprintln!("Error could not create new user: {} ", error); 50 | AppError::new(StatusCode::INTERNAL_SERVER_ERROR, "Could not create the account, Please try again later") 51 | })?; 52 | let user = new_user.try_into_model() 53 | .map_err(|error|{ 54 | eprintln!("Error, could not convert users into model: {}", error); 55 | AppError::new(StatusCode::INTERNAL_SERVER_ERROR, "Something went wrong") 56 | })?; 57 | 58 | Ok(Json(RespondUser { user_id: user.id, username: user.username, telephone: user.telephone, email: user.email, token: user.token })) 59 | 60 | } 61 | 62 | -------------------------------------------------------------------------------- /src/routes/backend/login.rs: -------------------------------------------------------------------------------- 1 | use axum::http::StatusCode; 2 | use axum::{extract::State, Json}; 3 | use sea_orm::{ActiveModelTrait, ColumnTrait, DatabaseConnection, EntityTrait, IntoActiveModel, QueryFilter, Set, TryIntoModel}; 4 | use serde::{Deserialize, Serialize}; 5 | 6 | use crate::utils::app_error::AppError; 7 | use crate::database::customer::{self, Entity as User}; 8 | use crate::utils::app_state::TokenWrapper; 9 | use crate::utils::hash::verifiy_pass; 10 | use crate::utils::jwt::create_jwt; 11 | 12 | use super::RespondUser; 13 | 14 | #[derive(Serialize, Deserialize)] 15 | pub struct RequestLoginUser{ 16 | pub username: String, 17 | pub password: String 18 | } 19 | pub async fn login( 20 | State(database): State, 21 | State(jwt_secret): State, 22 | Json(requet_user): Json 23 | )-> Result, AppError> { 24 | 25 | let user = User::find() 26 | .filter(customer::Column::Username.eq(requet_user.username)) 27 | .one(&database) 28 | .await 29 | .map_err(|error|{ 30 | eprintln!("Error finding the user {}", error); 31 | AppError::new(StatusCode::INTERNAL_SERVER_ERROR, "Error login in, Please try again later") 32 | })?; 33 | if let Some(user) = user{ 34 | // verify password 35 | if !verifiy_pass(requet_user.password, &user.password_hash)?{ 36 | return Err(AppError::new(StatusCode::UNAUTHORIZED, "Bad username OR password")); 37 | } 38 | 39 | // generate jwt 40 | let token = create_jwt(&jwt_secret.0)?; 41 | 42 | let mut user = user.into_active_model(); 43 | user.token = Set(Some(token)); 44 | let saved_user = user.save(&database) 45 | .await 46 | .map_err(|error|{ 47 | eprintln!("Error, could not save the token: {}", error); 48 | AppError::new(StatusCode::INTERNAL_SERVER_ERROR, "Something went wrong") 49 | })?; 50 | let user = saved_user.try_into_model() 51 | .map_err(|error|{ 52 | eprintln!("Error, Could not convert into model: {}", error); 53 | AppError::new(StatusCode::INTERNAL_SERVER_ERROR, "Something went wrong") 54 | })?; 55 | Ok(Json(RespondUser{email: user.email, telephone: user.telephone, user_id: user.id, username:user.username, token: user.token})) 56 | 57 | }else { 58 | return Err(AppError::new(StatusCode::NOT_FOUND, "Bad username OR password")); 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /src/routes/frontend/mod.rs: -------------------------------------------------------------------------------- 1 | pub mod handlers; 2 | pub mod contact; 3 | pub mod shop; 4 | 5 | use askama::Template; 6 | use axum::{extract::{Request, State}, middleware::{self, Next}, response::{Html, IntoResponse, Response}, routing::{get, post}, Router}; 7 | use hyper::StatusCode; 8 | use reqwest::Client; 9 | 10 | use crate::utils::{app_error::AppError, app_state::AppState, custom_frontend_middleware::{self, frontend_guard}, custom_middleware::guard_routes}; 11 | 12 | use self::{contact::contact_us, handlers::{cart, checkout, contact, detail, home}, shop::shop}; 13 | 14 | use super::backend::RespondUser; 15 | 16 | 17 | pub fn frontend_routes(app_state: AppState) -> Router{ 18 | let client = Client::new(); 19 | 20 | Router::new() 21 | .route("/contact", get(contact)) 22 | .route("/contact", post(contact_us)) 23 | .route("/detail", get(detail)) 24 | .route("/checkout", get(checkout)) 25 | .route("/shop", get(shop)) 26 | .route("/cart", get(cart)) 27 | .route_layer(middleware::from_fn_with_state(app_state.clone(), frontend_guard)) 28 | .route("/", get(home)) 29 | .layer( 30 | middleware::from_fn(inject_token) 31 | ) 32 | //.route_layer(middleware::from_fn_with_state(app_state.clone(), frontend_guard)) 33 | .with_state(client) 34 | .with_state(app_state) 35 | 36 | } 37 | 38 | 39 | pub struct HtmlTemplate(T); 40 | 41 | /// Allows us to convert Askama HTML templates into valid HTML for axum to serve in the response. 42 | impl IntoResponse for HtmlTemplate 43 | where 44 | T: Template, 45 | { 46 | fn into_response(self) -> Response { 47 | // Attempt to render the template with askama 48 | match self.0.render() { 49 | // If we're able to successfully parse and aggregate the template, serve it 50 | Ok(html) => Html(html).into_response(), 51 | // If we're not, return an error or some bit of fallback HTML 52 | Err(err) => ( 53 | StatusCode::INTERNAL_SERVER_ERROR, 54 | format!("Failed to render template. Error: {}", err), 55 | ) 56 | .into_response(), 57 | } 58 | } 59 | } 60 | 61 | 62 | async fn inject_token( 63 | mut req: Request, 64 | next: Next 65 | ) -> Result{ 66 | 67 | let token = "eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJleHAiOjE3MjA1MjUwNDAsImlhdCI6MTcyMDUyNDQ0MH0.P-k6mtl0XERRBLE7-WX6dxH2AFAIdXLNnoz47F-vlTU".to_string(); 68 | let tkn = format!("Bearer {}", token).parse().unwrap(); 69 | req.headers_mut().insert("Authorization", tkn); 70 | 71 | Ok(next.run(req).await) 72 | } 73 | 74 | -------------------------------------------------------------------------------- /src/routes/backend/reviews/new_review.rs: -------------------------------------------------------------------------------- 1 | use axum::http::StatusCode; 2 | use axum::{extract::State, Json}; 3 | use chrono::{NaiveDateTime, Utc}; 4 | use sea_orm::{ActiveModelTrait, ColumnTrait, EntityTrait, QueryFilter, Set, TryIntoModel}; 5 | use sea_orm::DatabaseConnection; 6 | use serde::{Deserialize, Serialize}; 7 | 8 | use crate::utils::app_error::AppError; 9 | use crate::database::{customer, product, product_review as Product_review}; 10 | use crate::database::customer::Entity as User; 11 | use crate::database::product::Entity as Product; 12 | 13 | #[derive(Serialize, Deserialize)] 14 | pub struct GetReview{ 15 | username: String, 16 | message: String, 17 | rating: i32, 18 | product_id: i32 19 | 20 | } 21 | #[derive(Serialize, Deserialize)] 22 | pub struct RespondReview{ 23 | review_id: i32, 24 | customer_id: i32, 25 | product_id: i32, 26 | rating: i32, 27 | rev_message: String, 28 | review_date: NaiveDateTime 29 | } 30 | 31 | pub async fn create_review( 32 | State(database): State, 33 | Json(review): Json 34 | ) -> Result, AppError>{ 35 | 36 | let user = User::find() 37 | .filter(customer::Column::Username.eq(review.username)) 38 | .one(&database) 39 | .await 40 | .map_err(|error|{ 41 | eprintln!("error finding user: {}", error); 42 | AppError::new(StatusCode::NOT_FOUND, "User not found") 43 | })?; 44 | let product = Product::find() 45 | .filter(product::Column::Id.eq(review.product_id)) 46 | .one(&database) 47 | .await 48 | .map_err(|error|{ 49 | eprintln!("error finding the product: {}", error); 50 | AppError::new(StatusCode::NOT_FOUND, "Product NOT_FOUND") 51 | })?; 52 | let now = Utc::now(); 53 | 54 | let rev_date = now.naive_utc(); 55 | 56 | let product = Product_review::ActiveModel{ 57 | review_text: Set(review.message), 58 | rating: Set(review.rating), 59 | customer_id: if let Some(user) = user{ 60 | Set(user.id) 61 | }else{return Err(AppError::new(StatusCode::NOT_FOUND, "User not found"));}, 62 | product_id: if let Some(product) = product{ 63 | Set(product.id) 64 | }else {return Err(AppError::new(StatusCode::NOT_FOUND, "product not found"));}, 65 | review_date: Set(rev_date), 66 | ..Default::default() 67 | }.save(&database) 68 | .await 69 | .map_err(|error|{ 70 | eprintln!("Error saving review: {}", error); 71 | AppError::new(StatusCode::INTERNAL_SERVER_ERROR, "something went wrong") 72 | })?; 73 | 74 | let final_product = product.try_into_model() 75 | .map_err(|error| { 76 | eprintln!("Error converting into model: {}", error); 77 | AppError::new(StatusCode::INTERNAL_SERVER_ERROR, "Something went wrong") 78 | })?; 79 | 80 | Ok(Json(RespondReview { 81 | review_id: final_product.id, 82 | customer_id: final_product.customer_id, 83 | product_id: final_product.product_id, 84 | rating: final_product.rating, 85 | rev_message: final_product.review_text, 86 | review_date: final_product.review_date 87 | })) 88 | } 89 | -------------------------------------------------------------------------------- /templates/components/footer.html: -------------------------------------------------------------------------------- 1 |
2 |
3 |
4 |
Get In Touch
5 |

We love hearing from our customers! Whether you have a question about our products, need assistance with an order, or just want to share your shopping experience, we’re here to help.

6 |

Eldoret, Kenya

7 |

yrncollo@gmail.com

8 |

+254 7863 3575

9 |
10 |
11 |
12 |
13 |
Quick Shop
14 |
15 | Home 16 | Our Shop 17 | Shop Detail 18 | Shopping Cart 19 | Checkout 20 | Contact Us 21 |
22 |
23 |
24 |
Newsletter
25 |

Subscribe to our newsletter for exclusive deals, new arrivals, and insider updates!

26 |
27 |
28 | 29 |
30 | 31 |
32 |
33 |
34 |
Follow Us
35 |
36 | 37 | 38 | 39 | 40 |
41 |
42 |
43 |
44 |
45 |
46 |
47 |

48 | © yrncollo.me. All Rights Reserved. Designed 49 | by 50 | YrnCollo 51 |

52 |
53 |
54 | 55 |
56 |
57 |
58 | 59 | -------------------------------------------------------------------------------- /templates/contact.html: -------------------------------------------------------------------------------- 1 | {% extends "base.html" %} 2 | {% block content %} 3 | 4 | {% include "components/topbar.html" %} 5 | 6 | 7 | 8 | 9 | {% include "components/navbar.html" %} 10 | 11 | 12 | 13 | 14 |
15 |
16 |
17 | 21 |
22 |
23 |
24 | 25 | 26 | 27 | 28 |
29 |

Contact Us

30 |
31 |
32 |
33 |
34 |
35 |
36 | 38 |

39 |
40 |
41 | 43 |

44 |
45 |
46 | 48 |

49 |
50 |
51 | 54 |

55 |
56 |
57 | 59 |
60 |
61 |
62 |
63 |
64 |
65 | 68 |
69 |
70 |

Eldoret, Kenya

71 |

yrncollo.com

72 |

+254-7563-3575

73 |
74 |
75 |
76 |
77 | 78 | 79 | 80 | 81 | {% include "components/footer.html" %} 82 | 83 | 84 | {% endblock %} 85 | -------------------------------------------------------------------------------- /templates/components/topbar.html: -------------------------------------------------------------------------------- 1 |
2 |
3 |
4 |
5 | About 6 | Contact 7 | Help 8 | FAQs 9 |
10 |
11 |
12 |
13 |
14 | 15 | 19 |
20 |
21 | 22 | 28 |
29 |
30 | 31 | 36 |
37 |
38 | 48 |
49 |
50 |
51 | 57 |
58 |
59 |
60 | 61 |
62 | 63 | 64 | 65 |
66 |
67 |
68 |
69 |
70 |

Customer Service

71 |
+254 7863 3575
72 |
73 |
74 |
75 | 76 | -------------------------------------------------------------------------------- /templates/components/navbar.html: -------------------------------------------------------------------------------- 1 |
2 |
3 |
4 | 5 |
Categories
6 | 7 |
8 | 29 |
30 |
31 | 65 |
66 |
67 |
68 | -------------------------------------------------------------------------------- /src/routes/admin_backend/create_products.rs: -------------------------------------------------------------------------------- 1 | use axum::{extract::State, Json}; 2 | use chrono::Utc; 3 | use hyper::StatusCode; 4 | use sea_orm::prelude::Decimal; 5 | use sea_orm::{ActiveModelTrait, ActiveValue, ColumnTrait, DbErr, EntityTrait, QueryFilter, TryIntoModel}; 6 | use sea_orm::{prelude::DateTimeUtc, DatabaseConnection}; 7 | use serde::{Deserialize, Serialize}; 8 | use sea_orm::Set; 9 | 10 | use crate::utils::app_error::AppError; 11 | use crate::database::product as ProductDB; 12 | use crate::database::categories::{self as CategoryDB, Model}; 13 | use crate::database::colors as ColorDB; 14 | 15 | #[derive(Serialize, Deserialize, Clone)] 16 | pub struct GetProducts{ 17 | product_name: String, 18 | price: Decimal, 19 | image_name: String, 20 | total_quantity: i32, 21 | category: String, 22 | description: String, 23 | color: String, 24 | quantity_per_color: i32 25 | } 26 | 27 | 28 | #[derive(Serialize, Deserialize)] 29 | pub struct ProductsResponse{ 30 | product_name: String, 31 | price: String, 32 | image_name: String, 33 | date_added: String, 34 | total_quantity: String, 35 | category: String, 36 | color: String, 37 | color_quantity: String, 38 | size: String, 39 | size_quantity: String, 40 | } 41 | 42 | pub async fn create_products( State(database): State, 43 | Json(raw_products): Json 44 | ) -> Result, AppError>{ 45 | 46 | let find_category = CategoryDB::Entity::find() 47 | .filter(CategoryDB::Column::Name.eq(&raw_products.category)) 48 | .one(&database) 49 | .await 50 | .map_err(|error|{ 51 | eprintln!("error finding category: {}", error); 52 | AppError::new(StatusCode::NOT_FOUND, "category not found") 53 | })?; 54 | 55 | 56 | if find_category.is_none(){ 57 | 58 | let category = CategoryDB::ActiveModel{ 59 | name: Set(raw_products.category.clone()), 60 | ..Default::default() 61 | }.save(&database) 62 | .await 63 | .map_err(|error|{ 64 | let error_mess = error.to_string(); 65 | 66 | if error_mess.contains("duplicate key value violates unique constraint"){ 67 | eprintln!("category already added"); 68 | return AppError::new(StatusCode::INTERNAL_SERVER_ERROR, "kjsafdlk"); 69 | } 70 | 71 | eprintln!("Error saving category {}", error); 72 | return AppError::new(StatusCode::INTERNAL_SERVER_ERROR, "something went wrong"); 73 | })?; 74 | 75 | let category_db_id = category.try_into_model() 76 | .map_err(|error|{ 77 | eprintln!("Error converting category into model {}", error); 78 | AppError::new(StatusCode::INTERNAL_SERVER_ERROR, "something went wrong") 79 | })?; 80 | 81 | finish_building(database, category_db_id, raw_products).await 82 | 83 | }else{ 84 | 85 | let category_db_id = find_category.unwrap().try_into_model() 86 | .map_err(|error|{ 87 | eprintln!("Error converting category into model {}", error); 88 | AppError::new(StatusCode::INTERNAL_SERVER_ERROR, "something went wrong") 89 | })?; 90 | 91 | finish_building(database, category_db_id, raw_products).await 92 | } 93 | } 94 | 95 | 96 | async fn finish_building(database: DatabaseConnection, 97 | category_db_id:Model, raw_products: GetProducts) -> Result, AppError>{ 98 | 99 | 100 | let now = Utc::now(); 101 | let date_product_added = now.naive_utc(); 102 | 103 | let product = ProductDB::ActiveModel{ 104 | category_id: Set(category_db_id.id), 105 | product_name: Set(raw_products.product_name), 106 | description: Set(raw_products.description), 107 | image_name: Set(raw_products.image_name), 108 | price: Set(raw_products.price), 109 | quantity: Set(raw_products.total_quantity), 110 | date_added: Set(date_product_added), 111 | ..Default::default() 112 | }.save(&database) 113 | .await 114 | .map_err(|error|{ 115 | let error_mess = error.to_string(); 116 | 117 | if error_mess.contains("duplicate key value violates unique constraint"){ 118 | eprintln!("Product already exist"); 119 | return AppError::new(StatusCode::INTERNAL_SERVER_ERROR, "Product already exist"); 120 | } 121 | eprintln!("Error saving the product {}", error); 122 | AppError::new(StatusCode::INTERNAL_SERVER_ERROR, "something went wrong") 123 | })?; 124 | 125 | let final_product = product.try_into_model() 126 | .map_err(|error|{ 127 | eprintln!("Error converting product into model{}", error); 128 | AppError::new(StatusCode::INTERNAL_SERVER_ERROR, "something went wrong") 129 | })?; 130 | 131 | let color = ColorDB::ActiveModel{ 132 | color: Set(raw_products.color), 133 | quantity: Set(raw_products.quantity_per_color), 134 | product_id: Set(final_product.id), 135 | ..Default::default() 136 | }.save(&database) 137 | .await 138 | .map_err(|error|{ 139 | eprintln!("Error saving color: {}", error); 140 | AppError::new(StatusCode::INTERNAL_SERVER_ERROR, "something went wrong") 141 | })?; 142 | let color = color.try_into_model() 143 | .map_err(|error|{ 144 | eprintln!("Error converting product into model{}", error); 145 | AppError::new(StatusCode::INTERNAL_SERVER_ERROR, "something went wrong") 146 | })?; 147 | 148 | 149 | Ok(Json(ProductsResponse { 150 | product_name: final_product.product_name, 151 | price: final_product.price.to_string(), 152 | image_name: final_product.image_name, 153 | date_added: final_product.date_added.to_string(), 154 | total_quantity: final_product.quantity.to_string(), 155 | category: category_db_id.name, 156 | color: color.color, 157 | color_quantity: color.quantity.to_string(), 158 | size: "to be checked".to_string(), 159 | size_quantity: "to be checked".to_string() 160 | } 161 | )) 162 | 163 | 164 | } 165 | -------------------------------------------------------------------------------- /templates/cart.html: -------------------------------------------------------------------------------- 1 | {% extends "base.html" %} 2 | {% block content %} 3 | 4 | {% include "components/topbar.html" %} 5 | 6 | 7 | 8 | 9 | {% include "components/navbar.html" %} 10 | 11 | 12 | 13 | 14 |
15 |
16 |
17 | 22 |
23 |
24 |
25 | 26 | 27 | 28 | 29 |
30 |
31 |
32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 82 | 83 | 84 | 85 | 86 | 87 | 88 | 103 | 104 | 105 | 106 | 107 | 108 | 109 | 124 | 125 | 126 | 127 | 128 | 129 | 130 | 145 | 146 | 147 | 148 | 149 |
ProductsPriceQuantityTotalRemove
Product Name$150 47 |
48 |
49 | 52 |
53 | 54 |
55 | 58 |
59 |
60 |
$150
Product Name$150 68 |
69 |
70 | 73 |
74 | 75 |
76 | 79 |
80 |
81 |
$150
Product Name$150 89 |
90 |
91 | 94 |
95 | 96 |
97 | 100 |
101 |
102 |
$150
Product Name$150 110 |
111 |
112 | 115 |
116 | 117 |
118 | 121 |
122 |
123 |
$150
Product Name$150 131 |
132 |
133 | 136 |
137 | 138 |
139 | 142 |
143 |
144 |
$150
150 |
151 |
152 |
153 |
154 | 155 |
156 | 157 |
158 |
159 |
160 |
Cart Summary
161 |
162 |
163 |
164 |
Subtotal
165 |
$150
166 |
167 |
168 |
Shipping
169 |
$10
170 |
171 |
172 |
173 |
174 |
Total
175 |
$160
176 |
177 | 178 |
179 |
180 |
181 |
182 |
183 | 184 | 185 | 186 | 187 | {% include "components/footer.html" %} 188 | 189 | 190 | {% endblock %} 191 | -------------------------------------------------------------------------------- /templates/checkout.html: -------------------------------------------------------------------------------- 1 | {% extends "base.html" %} 2 | {% block content %} 3 | 4 | {% include "components/topbar.html" %} 5 | 6 | 7 | 8 | 9 | {% include "components/navbar.html" %} 10 | 11 | 12 | 13 | 14 |
15 |
16 |
17 | 22 |
23 |
24 |
25 | 26 | 27 | 28 | 29 |
30 |
31 |
32 |
Billing Address
33 |
34 |
35 |
36 | 37 | 38 |
39 |
40 | 41 | 42 |
43 |
44 | 45 | 46 |
47 |
48 | 49 | 50 |
51 |
52 | 53 | 54 |
55 |
56 | 57 | 58 |
59 |
60 | 61 | 67 |
68 |
69 | 70 | 71 |
72 |
73 | 74 | 75 |
76 |
77 | 78 | 79 |
80 |
81 |
82 | 83 | 84 |
85 |
86 |
87 |
88 | 89 | 90 |
91 |
92 |
93 |
94 |
95 |
Shipping Address
96 |
97 |
98 |
99 | 100 | 101 |
102 |
103 | 104 | 105 |
106 |
107 | 108 | 109 |
110 |
111 | 112 | 113 |
114 |
115 | 116 | 117 |
118 |
119 | 120 | 121 |
122 |
123 | 124 | 130 |
131 |
132 | 133 | 134 |
135 |
136 | 137 | 138 |
139 |
140 | 141 | 142 |
143 |
144 |
145 |
146 |
147 |
148 |
Order Total
149 |
150 |
151 |
Products
152 |
153 |

Product Name 1

154 |

$150

155 |
156 |
157 |

Product Name 2

158 |

$150

159 |
160 |
161 |

Product Name 3

162 |

$150

163 |
164 |
165 |
166 |
167 |
Subtotal
168 |
$150
169 |
170 |
171 |
Shipping
172 |
$10
173 |
174 |
175 |
176 |
177 |
Total
178 |
$160
179 |
180 |
181 |
182 |
183 |
Payment
184 |
185 |
186 |
187 | 188 | 189 |
190 |
191 |
192 |
193 | 194 | 195 |
196 |
197 |
198 |
199 | 200 | 201 |
202 |
203 | 204 |
205 |
206 |
207 |
208 |
209 | 210 | 211 | 212 | 213 | {% include "components/footer.html" %} 214 | 215 | 216 | {% endblock %} 217 | 218 | -------------------------------------------------------------------------------- /templates/shop.html: -------------------------------------------------------------------------------- 1 | {% extends "base.html" %} 2 | {% block content %} 3 | 4 | {% include "components/topbar.html" %} 5 | 6 | 7 | 8 | 9 | {% include "components/navbar.html" %} 10 | 11 | 12 | 13 | 14 |
15 |
16 |
17 | 22 |
23 |
24 |
25 | 26 | 27 | 28 | 29 |
30 |
31 | 32 |
33 | 34 |
Filter by price
35 |
36 |
37 |
38 | 39 | 40 | 1000 41 |
42 |
43 | 44 | 45 | 150 46 |
47 |
48 | 49 | 50 | 295 51 |
52 |
53 | 54 | 55 | 246 56 |
57 |
58 | 59 | 60 | 145 61 |
62 |
63 | 64 | 65 | 168 66 |
67 |
68 |
69 | 70 | 71 | 72 |
Filter by color
73 |
74 |
75 |
76 | 77 | 78 | 1000 79 |
80 |
81 | 82 | 83 | 150 84 |
85 |
86 | 87 | 88 | 295 89 |
90 |
91 | 92 | 93 | 246 94 |
95 |
96 | 97 | 98 | 145 99 |
100 |
101 | 102 | 103 | 168 104 |
105 |
106 |
107 | 108 | 109 | 110 |
Filter by size
111 |
112 |
113 |
114 | 115 | 116 | 1000 117 |
118 |
119 | 120 | 121 | 150 122 |
123 |
124 | 125 | 126 | 295 127 |
128 |
129 | 130 | 131 | 246 132 |
133 |
134 | 135 | 136 | 145 137 |
138 |
139 | 140 | 141 | 168 142 |
143 |
144 |
145 | 146 |
147 | 148 | 149 | 150 | 151 |
152 |
153 |
154 |
155 |
156 | 157 | 158 |
159 |
160 |
161 | 162 | 167 |
168 |
169 | 170 | 175 |
176 |
177 |
178 |
179 | 180 | {% include "components/products.html" %} 181 |
182 | 191 |
192 |
193 |
194 | 195 |
196 |
197 | 198 | 199 | 200 | 201 | {% include "components/footer.html" %} 202 | 203 | 204 | {% endblock %} 205 | 206 | -------------------------------------------------------------------------------- /templates/detail.html: -------------------------------------------------------------------------------- 1 | {% extends "base.html" %} 2 | {% block content %} 3 | 4 | {% include "components/topbar.html" %} 5 | 6 | 7 | 8 | 9 | {% include "components/navbar.html" %} 10 | 11 | 12 | 13 | 14 |
15 |
16 |
17 | 22 |
23 |
24 |
25 | 26 | 27 | 28 | 29 |
30 |
31 |
32 | 54 |
55 | 56 |
57 |
58 |

Product Name Goes Here

59 |
60 |
61 | 62 | 63 | 64 | 65 | 66 |
67 | (99 Reviews) 68 |
69 |

$150.00

70 |

Volup erat ipsum diam elitr rebum et dolor. Est nonumy elitr erat diam stet sit 71 | clita ea. Sanc ipsum et, labore clita lorem magna duo dolor no sea 72 | Nonumy

73 |
74 | Sizes: 75 |
76 |
77 | 78 | 79 |
80 |
81 | 82 | 83 |
84 |
85 | 86 | 87 |
88 |
89 | 90 | 91 |
92 |
93 | 94 | 95 |
96 |
97 |
98 |
99 | Colors: 100 |
101 |
102 | 103 | 104 |
105 |
106 | 107 | 108 |
109 |
110 | 111 | 112 |
113 |
114 | 115 | 116 |
117 |
118 | 119 | 120 |
121 |
122 |
123 |
124 |
125 |
126 | 129 |
130 | 131 |
132 | 135 |
136 |
137 | 139 |
140 |
141 | Share on: 142 | 156 |
157 |
158 |
159 |
160 |
161 |
162 |
163 | 168 |
169 |
170 |

Product Description

171 |

Eos no lorem eirmod diam diam, eos elitr et gubergren diam sea. Consetetur vero aliquyam invidunt duo dolores et duo sit. Vero diam ea vero et dolore rebum, dolor rebum eirmod consetetur invidunt sed sed et, lorem duo et eos elitr, sadipscing kasd ipsum rebum diam. Dolore diam stet rebum sed tempor kasd eirmod. Takimata kasd ipsum accusam sadipscing, eos dolores sit no ut diam consetetur duo justo est, sit sanctus diam tempor aliquyam eirmod nonumy rebum dolor accusam, ipsum kasd eos consetetur at sit rebum, diam kasd invidunt tempor lorem, ipsum lorem elitr sanctus eirmod takimata dolor ea invidunt.

172 |

Dolore magna est eirmod sanctus dolor, amet diam et eirmod et ipsum. Amet dolore tempor consetetur sed lorem dolor sit lorem tempor. Gubergren amet amet labore sadipscing clita clita diam clita. Sea amet et sed ipsum lorem elitr et, amet et labore voluptua sit rebum. Ea erat sed et diam takimata sed justo. Magna takimata justo et amet magna et.

173 |
174 |
175 |

Additional Information

176 |

Eos no lorem eirmod diam diam, eos elitr et gubergren diam sea. Consetetur vero aliquyam invidunt duo dolores et duo sit. Vero diam ea vero et dolore rebum, dolor rebum eirmod consetetur invidunt sed sed et, lorem duo et eos elitr, sadipscing kasd ipsum rebum diam. Dolore diam stet rebum sed tempor kasd eirmod. Takimata kasd ipsum accusam sadipscing, eos dolores sit no ut diam consetetur duo justo est, sit sanctus diam tempor aliquyam eirmod nonumy rebum dolor accusam, ipsum kasd eos consetetur at sit rebum, diam kasd invidunt tempor lorem, ipsum lorem elitr sanctus eirmod takimata dolor ea invidunt.

177 |
178 |
179 |
    180 |
  • 181 | Sit erat duo lorem duo ea consetetur, et eirmod takimata. 182 |
  • 183 |
  • 184 | Amet kasd gubergren sit sanctus et lorem eos sadipscing at. 185 |
  • 186 |
  • 187 | Duo amet accusam eirmod nonumy stet et et stet eirmod. 188 |
  • 189 |
  • 190 | Takimata ea clita labore amet ipsum erat justo voluptua. Nonumy. 191 |
  • 192 |
193 |
194 |
195 |
    196 |
  • 197 | Sit erat duo lorem duo ea consetetur, et eirmod takimata. 198 |
  • 199 |
  • 200 | Amet kasd gubergren sit sanctus et lorem eos sadipscing at. 201 |
  • 202 |
  • 203 | Duo amet accusam eirmod nonumy stet et et stet eirmod. 204 |
  • 205 |
  • 206 | Takimata ea clita labore amet ipsum erat justo voluptua. Nonumy. 207 |
  • 208 |
209 |
210 |
211 |
212 |
213 |
214 |
215 |

1 review for "Product Name"

216 |
217 | Image 218 |
219 |
John Doe - 01 Jan 2045
220 |
221 | 222 | 223 | 224 | 225 | 226 |
227 |

Diam amet duo labore stet elitr ea clita ipsum, tempor labore accusam ipsum et no at. Kasd diam tempor rebum magna dolores sed sed eirmod ipsum.

228 |
229 |
230 |
231 |
232 |

Leave a review

233 | Your email address will not be published. Required fields are marked * 234 |
235 |

Your Rating * :

236 |
237 | 238 | 239 | 240 | 241 | 242 |
243 |
244 |
245 |
246 | 247 | 248 |
249 |
250 | 251 | 252 |
253 |
254 | 255 | 256 |
257 |
258 | 259 |
260 |
261 |
262 |
263 |
264 |
265 |
266 |
267 |
268 |
269 | 270 | 271 | 272 | 273 |
274 |

You May Also Like

275 |
276 |
277 | 404 |
405 |
406 |
407 | 408 | 409 | 410 | 411 | {% include "components/footer.html" %} 412 | 413 | 414 | {% endblock %} 415 | -------------------------------------------------------------------------------- /templates/index.html: -------------------------------------------------------------------------------- 1 | {% extends "base.html" %} 2 | {% block content %} 3 | 4 | {% include "components/topbar.html" %} 5 | 6 | 7 | 8 | 9 | {% include "components/navbar.html" %} 10 | 11 | 12 | 13 | 14 |
15 |
16 |
17 | 56 |
57 |
58 |
59 | 60 |
61 |
Save 20%
62 |

Special Offer

63 | Shop Now 64 |
65 |
66 |
67 | 68 |
69 |
Save 20%
70 |

Special Offer

71 | Shop Now 72 |
73 |
74 |
75 |
76 |
77 | 78 | 79 | 80 | 81 |
82 |
83 |
84 |
85 |

86 |
Quality Product
87 |
88 |
89 |
90 |
91 |

92 |
Free Shipping
93 |
94 |
95 |
96 |
97 |

98 |
14-Day Return
99 |
100 |
101 |
102 |
103 |

104 |
24/7 Support
105 |
106 |
107 |
108 |
109 | 110 | 111 | 112 | 113 | 274 | 275 | 276 | 277 | 278 |
279 |

Featured Products

280 |
281 |
282 |
283 |
284 | 285 |
286 | 287 | 288 | 289 | 290 |
291 |
292 |
293 | Product Name Goes Here 294 |
295 |
$123.00
$123.00
296 |
297 |
298 | 299 | 300 | 301 | 302 | 303 | (99) 304 |
305 |
306 |
307 |
308 |
309 |
310 |
311 | 312 |
313 | 314 | 315 | 316 | 317 |
318 |
319 |
320 | Product Name Goes Here 321 |
322 |
$123.00
$123.00
323 |
324 |
325 | 326 | 327 | 328 | 329 | 330 | (99) 331 |
332 |
333 |
334 |
335 |
336 |
337 |
338 | 339 |
340 | 341 | 342 | 343 | 344 |
345 |
346 |
347 | Product Name Goes Here 348 |
349 |
$123.00
$123.00
350 |
351 |
352 | 353 | 354 | 355 | 356 | 357 | (99) 358 |
359 |
360 |
361 |
362 |
363 |
364 |
365 | 366 |
367 | 368 | 369 | 370 | 371 |
372 |
373 |
374 | Product Name Goes Here 375 |
376 |
$123.00
$123.00
377 |
378 |
379 | 380 | 381 | 382 | 383 | 384 | (99) 385 |
386 |
387 |
388 |
389 |
390 |
391 |
392 | 393 |
394 | 395 | 396 | 397 | 398 |
399 |
400 |
401 | Product Name Goes Here 402 |
403 |
$123.00
$123.00
404 |
405 |
406 | 407 | 408 | 409 | 410 | 411 | (99) 412 |
413 |
414 |
415 |
416 |
417 |
418 |
419 | 420 |
421 | 422 | 423 | 424 | 425 |
426 |
427 |
428 | Product Name Goes Here 429 |
430 |
$123.00
$123.00
431 |
432 |
433 | 434 | 435 | 436 | 437 | 438 | (99) 439 |
440 |
441 |
442 |
443 |
444 |
445 |
446 | 447 |
448 | 449 | 450 | 451 | 452 |
453 |
454 |
455 | Product Name Goes Here 456 |
457 |
$123.00
$123.00
458 |
459 |
460 | 461 | 462 | 463 | 464 | 465 | (99) 466 |
467 |
468 |
469 |
470 |
471 |
472 |
473 | 474 |
475 | 476 | 477 | 478 | 479 |
480 |
481 |
482 | Product Name Goes Here 483 |
484 |
$123.00
$123.00
485 |
486 |
487 | 488 | 489 | 490 | 491 | 492 | (99) 493 |
494 |
495 |
496 |
497 |
498 |
499 | 500 | 501 | 502 | 503 |
504 |
505 |
506 |
507 | 508 |
509 |
Save 20%
510 |

Special Offer

511 | Shop Now 512 |
513 |
514 |
515 |
516 |
517 | 518 |
519 |
Save 20%
520 |

Special Offer

521 | Shop Now 522 |
523 |
524 |
525 |
526 |
527 | 528 | 529 | 530 | 531 |
532 |

Recent Products

533 |
534 |
535 |
536 |
537 | 538 |
539 | 540 | 541 | 542 | 543 |
544 |
545 |
546 | Product Name Goes Here 547 |
548 |
$123.00
$123.00
549 |
550 |
551 | 552 | 553 | 554 | 555 | 556 | (99) 557 |
558 |
559 |
560 |
561 |
562 |
563 |
564 | 565 |
566 | 567 | 568 | 569 | 570 |
571 |
572 |
573 | Product Name Goes Here 574 |
575 |
$123.00
$123.00
576 |
577 |
578 | 579 | 580 | 581 | 582 | 583 | (99) 584 |
585 |
586 |
587 |
588 |
589 |
590 |
591 | 592 |
593 | 594 | 595 | 596 | 597 |
598 |
599 |
600 | Product Name Goes Here 601 |
602 |
$123.00
$123.00
603 |
604 |
605 | 606 | 607 | 608 | 609 | 610 | (99) 611 |
612 |
613 |
614 |
615 |
616 |
617 |
618 | 619 |
620 | 621 | 622 | 623 | 624 |
625 |
626 |
627 | Product Name Goes Here 628 |
629 |
$123.00
$123.00
630 |
631 |
632 | 633 | 634 | 635 | 636 | 637 | (99) 638 |
639 |
640 |
641 |
642 |
643 |
644 |
645 | 646 |
647 | 648 | 649 | 650 | 651 |
652 |
653 |
654 | Product Name Goes Here 655 |
656 |
$123.00
$123.00
657 |
658 |
659 | 660 | 661 | 662 | 663 | 664 | (99) 665 |
666 |
667 |
668 |
669 |
670 |
671 |
672 | 673 |
674 | 675 | 676 | 677 | 678 |
679 |
680 |
681 | Product Name Goes Here 682 |
683 |
$123.00
$123.00
684 |
685 |
686 | 687 | 688 | 689 | 690 | 691 | (99) 692 |
693 |
694 |
695 |
696 |
697 |
698 |
699 | 700 |
701 | 702 | 703 | 704 | 705 |
706 |
707 |
708 | Product Name Goes Here 709 |
710 |
$123.00
$123.00
711 |
712 |
713 | 714 | 715 | 716 | 717 | 718 | (99) 719 |
720 |
721 |
722 |
723 |
724 |
725 |
726 | 727 |
728 | 729 | 730 | 731 | 732 |
733 |
734 |
735 | Product Name Goes Here 736 |
737 |
$123.00
$123.00
738 |
739 |
740 | 741 | 742 | 743 | 744 | 745 | (99) 746 |
747 |
748 |
749 |
750 |
751 |
752 | 753 | 754 | 755 | 756 |
757 |
758 |
759 | 785 |
786 |
787 |
788 | 789 | 790 | 791 | 792 | {% include "components/footer.html" %} 793 | 794 | 795 | 796 | {% endblock %} 797 | --------------------------------------------------------------------------------