├── .mvn └── wrapper │ ├── maven-wrapper.jar │ └── maven-wrapper.properties ├── src ├── main │ ├── java │ │ └── com │ │ │ └── youtube │ │ │ └── tutorial │ │ │ └── ecommercebackend │ │ │ ├── exception │ │ │ ├── EmailFailureException.java │ │ │ ├── EmailNotFoundException.java │ │ │ ├── UserAlreadyExistsException.java │ │ │ └── UserNotVerifiedException.java │ │ │ ├── model │ │ │ ├── dao │ │ │ │ ├── ProductDAO.java │ │ │ │ ├── AddressDAO.java │ │ │ │ ├── WebOrderDAO.java │ │ │ │ ├── LocalUserDAO.java │ │ │ │ └── VerificationTokenDAO.java │ │ │ ├── Inventory.java │ │ │ ├── WebOrderQuantities.java │ │ │ ├── VerificationToken.java │ │ │ ├── WebOrder.java │ │ │ ├── Address.java │ │ │ ├── Product.java │ │ │ └── LocalUser.java │ │ │ ├── EcommerceBackendApplication.java │ │ │ ├── api │ │ │ ├── model │ │ │ │ ├── LoginBody.java │ │ │ │ ├── LoginResponse.java │ │ │ │ ├── PasswordResetBody.java │ │ │ │ ├── DataChange.java │ │ │ │ └── RegistrationBody.java │ │ │ ├── controller │ │ │ │ ├── product │ │ │ │ │ └── ProductController.java │ │ │ │ ├── order │ │ │ │ │ └── OrderController.java │ │ │ │ ├── user │ │ │ │ │ └── UserController.java │ │ │ │ └── auth │ │ │ │ │ └── AuthenticationController.java │ │ │ └── security │ │ │ │ ├── WebSecurityConfig.java │ │ │ │ ├── JWTRequestFilter.java │ │ │ │ └── WebsocketConfiguration.java │ │ │ └── service │ │ │ ├── ProductService.java │ │ │ ├── OrderService.java │ │ │ ├── EncryptionService.java │ │ │ ├── EmailService.java │ │ │ ├── JWTService.java │ │ │ └── UserService.java │ └── resources │ │ └── application.properties └── test │ ├── resources │ ├── config │ │ └── application.properties │ └── data.sql │ └── java │ └── com │ └── youtube │ └── tutorial │ └── ecommercebackend │ ├── EcommerceBackendApplicationTests.java │ ├── api │ ├── controller │ │ ├── product │ │ │ └── ProductControllerTest.java │ │ ├── order │ │ │ └── OrderControllerTest.java │ │ └── auth │ │ │ └── AuthenticationControllerTest.java │ └── security │ │ ├── JUnitUserDetailsService.java │ │ └── JWTRequestFilterTest.java │ └── service │ ├── EncryptionServiceTest.java │ ├── JWTServiceTest.java │ └── UserServiceTest.java ├── .gitignore ├── tools └── websocket-tool │ ├── app.js │ └── index.html ├── pom.xml ├── insertDummyDevData.sql ├── mvnw.cmd └── mvnw /.mvn/wrapper/maven-wrapper.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/backslashprogramming/Spring-Boot-E-Commerce-Tutorial/HEAD/.mvn/wrapper/maven-wrapper.jar -------------------------------------------------------------------------------- /.mvn/wrapper/maven-wrapper.properties: -------------------------------------------------------------------------------- 1 | distributionUrl=https://repo.maven.apache.org/maven2/org/apache/maven/apache-maven/3.8.6/apache-maven-3.8.6-bin.zip 2 | wrapperUrl=https://repo.maven.apache.org/maven2/org/apache/maven/wrapper/maven-wrapper/3.1.0/maven-wrapper-3.1.0.jar 3 | -------------------------------------------------------------------------------- /src/main/java/com/youtube/tutorial/ecommercebackend/exception/EmailFailureException.java: -------------------------------------------------------------------------------- 1 | package com.youtube.tutorial.ecommercebackend.exception; 2 | 3 | /** 4 | * Exception to highlight that we were unable to send an email. 5 | */ 6 | public class EmailFailureException extends Exception { 7 | } 8 | -------------------------------------------------------------------------------- /src/main/java/com/youtube/tutorial/ecommercebackend/exception/EmailNotFoundException.java: -------------------------------------------------------------------------------- 1 | package com.youtube.tutorial.ecommercebackend.exception; 2 | 3 | /** 4 | * Exception thrown when an email address given could not be found. 5 | */ 6 | public class EmailNotFoundException extends Exception { 7 | } 8 | -------------------------------------------------------------------------------- /src/main/java/com/youtube/tutorial/ecommercebackend/exception/UserAlreadyExistsException.java: -------------------------------------------------------------------------------- 1 | package com.youtube.tutorial.ecommercebackend.exception; 2 | 3 | /** 4 | * Exception thrown at user registration if an existing user already exists 5 | * with the given information. 6 | */ 7 | public class UserAlreadyExistsException extends Exception { 8 | } 9 | -------------------------------------------------------------------------------- /src/test/resources/config/application.properties: -------------------------------------------------------------------------------- 1 | # This file is to override properties in the main application.properties without replacing the file. 2 | 3 | spring.datasource.url=jdbc:h2:mem:test 4 | spring.jpa.properties.hibernate.dialect=org.hibernate.dialect.H2Dialect 5 | spring.jpa.defer-datasource-initialization=true 6 | 7 | spring.mail.port=3025 8 | spring.mail.username=springboot 9 | spring.mail.password=secret -------------------------------------------------------------------------------- /src/main/java/com/youtube/tutorial/ecommercebackend/model/dao/ProductDAO.java: -------------------------------------------------------------------------------- 1 | package com.youtube.tutorial.ecommercebackend.model.dao; 2 | 3 | import com.youtube.tutorial.ecommercebackend.model.Product; 4 | import org.springframework.data.repository.ListCrudRepository; 5 | 6 | /** 7 | * Data Access Object for accessing Product data. 8 | */ 9 | public interface ProductDAO extends ListCrudRepository { 10 | } 11 | -------------------------------------------------------------------------------- /src/main/java/com/youtube/tutorial/ecommercebackend/EcommerceBackendApplication.java: -------------------------------------------------------------------------------- 1 | package com.youtube.tutorial.ecommercebackend; 2 | 3 | import org.springframework.boot.SpringApplication; 4 | import org.springframework.boot.autoconfigure.SpringBootApplication; 5 | 6 | @SpringBootApplication 7 | public class EcommerceBackendApplication { 8 | 9 | public static void main(String[] args) { 10 | SpringApplication.run(EcommerceBackendApplication.class, args); 11 | } 12 | 13 | } 14 | -------------------------------------------------------------------------------- /src/test/java/com/youtube/tutorial/ecommercebackend/EcommerceBackendApplicationTests.java: -------------------------------------------------------------------------------- 1 | package com.youtube.tutorial.ecommercebackend; 2 | 3 | import org.junit.jupiter.api.Test; 4 | import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc; 5 | import org.springframework.boot.test.context.SpringBootTest; 6 | 7 | @SpringBootTest 8 | @AutoConfigureMockMvc 9 | class EcommerceBackendApplicationTests { 10 | 11 | @Test 12 | void contextLoads() { 13 | } 14 | 15 | } 16 | -------------------------------------------------------------------------------- /src/main/java/com/youtube/tutorial/ecommercebackend/model/dao/AddressDAO.java: -------------------------------------------------------------------------------- 1 | package com.youtube.tutorial.ecommercebackend.model.dao; 2 | 3 | import com.youtube.tutorial.ecommercebackend.model.Address; 4 | import org.springframework.data.repository.ListCrudRepository; 5 | 6 | import java.util.List; 7 | 8 | /** 9 | * Data Access Object for the Address data. 10 | */ 11 | public interface AddressDAO extends ListCrudRepository { 12 | 13 | List
findByUser_Id(Long id); 14 | 15 | } 16 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | HELP.md 2 | target/ 3 | !.mvn/wrapper/maven-wrapper.jar 4 | !**/src/main/**/target/ 5 | !**/src/test/**/target/ 6 | 7 | ### STS ### 8 | .apt_generated 9 | .classpath 10 | .factorypath 11 | .project 12 | .settings 13 | .springBeans 14 | .sts4-cache 15 | 16 | ### IntelliJ IDEA ### 17 | .idea 18 | *.iws 19 | *.iml 20 | *.ipr 21 | 22 | ### NetBeans ### 23 | /nbproject/private/ 24 | /nbbuild/ 25 | /dist/ 26 | /nbdist/ 27 | /.nb-gradle/ 28 | build/ 29 | !**/src/main/**/build/ 30 | !**/src/test/**/build/ 31 | 32 | ### VS Code ### 33 | .vscode/ 34 | -------------------------------------------------------------------------------- /src/main/java/com/youtube/tutorial/ecommercebackend/model/dao/WebOrderDAO.java: -------------------------------------------------------------------------------- 1 | package com.youtube.tutorial.ecommercebackend.model.dao; 2 | 3 | import com.youtube.tutorial.ecommercebackend.model.LocalUser; 4 | import com.youtube.tutorial.ecommercebackend.model.WebOrder; 5 | import org.springframework.data.repository.ListCrudRepository; 6 | 7 | import java.util.List; 8 | 9 | /** 10 | * Data Access Object to access WebOrder data. 11 | */ 12 | public interface WebOrderDAO extends ListCrudRepository { 13 | 14 | List findByUser(LocalUser user); 15 | 16 | } 17 | -------------------------------------------------------------------------------- /src/main/java/com/youtube/tutorial/ecommercebackend/model/dao/LocalUserDAO.java: -------------------------------------------------------------------------------- 1 | package com.youtube.tutorial.ecommercebackend.model.dao; 2 | 3 | import com.youtube.tutorial.ecommercebackend.model.LocalUser; 4 | import org.springframework.data.repository.ListCrudRepository; 5 | 6 | import java.util.Optional; 7 | 8 | /** 9 | * Data Access Object for the LocalUser data. 10 | */ 11 | public interface LocalUserDAO extends ListCrudRepository { 12 | 13 | Optional findByUsernameIgnoreCase(String username); 14 | 15 | Optional findByEmailIgnoreCase(String email); 16 | 17 | } 18 | -------------------------------------------------------------------------------- /src/main/java/com/youtube/tutorial/ecommercebackend/model/dao/VerificationTokenDAO.java: -------------------------------------------------------------------------------- 1 | package com.youtube.tutorial.ecommercebackend.model.dao; 2 | 3 | import com.youtube.tutorial.ecommercebackend.model.LocalUser; 4 | import com.youtube.tutorial.ecommercebackend.model.VerificationToken; 5 | import org.springframework.data.repository.ListCrudRepository; 6 | 7 | import java.util.List; 8 | import java.util.Optional; 9 | 10 | /** 11 | * Data Access Object for the VerificationToken data. 12 | */ 13 | public interface VerificationTokenDAO extends ListCrudRepository { 14 | 15 | Optional findByToken(String token); 16 | 17 | void deleteByUser(LocalUser user); 18 | 19 | List findByUser_IdOrderByIdDesc(Long id); 20 | 21 | } 22 | -------------------------------------------------------------------------------- /src/main/java/com/youtube/tutorial/ecommercebackend/exception/UserNotVerifiedException.java: -------------------------------------------------------------------------------- 1 | package com.youtube.tutorial.ecommercebackend.exception; 2 | 3 | /** 4 | * Exception to highlight a user does not have a verified email address. 5 | */ 6 | public class UserNotVerifiedException extends Exception { 7 | 8 | /** Did we send a new email? */ 9 | private boolean newEmailSent; 10 | 11 | /** 12 | * Constructor. 13 | * @param newEmailSent Was a new email sent? 14 | */ 15 | public UserNotVerifiedException(boolean newEmailSent) { 16 | this.newEmailSent = newEmailSent; 17 | } 18 | 19 | /** 20 | * Was a new email sent? 21 | * @return True if it was, false otherwise. 22 | */ 23 | public boolean isNewEmailSent() { 24 | return newEmailSent; 25 | } 26 | 27 | } 28 | -------------------------------------------------------------------------------- /src/main/java/com/youtube/tutorial/ecommercebackend/api/model/LoginBody.java: -------------------------------------------------------------------------------- 1 | package com.youtube.tutorial.ecommercebackend.api.model; 2 | 3 | import jakarta.validation.constraints.NotBlank; 4 | import jakarta.validation.constraints.NotNull; 5 | 6 | /** 7 | * The body for the login requests. 8 | */ 9 | public class LoginBody { 10 | 11 | /** The username to log in with. */ 12 | @NotNull 13 | @NotBlank 14 | private String username; 15 | /** The password to log in with. */ 16 | @NotNull 17 | @NotBlank 18 | private String password; 19 | 20 | public String getUsername() { 21 | return username; 22 | } 23 | 24 | public void setUsername(String username) { 25 | this.username = username; 26 | } 27 | 28 | public String getPassword() { 29 | return password; 30 | } 31 | 32 | public void setPassword(String password) { 33 | this.password = password; 34 | } 35 | 36 | } 37 | -------------------------------------------------------------------------------- /src/main/java/com/youtube/tutorial/ecommercebackend/service/ProductService.java: -------------------------------------------------------------------------------- 1 | package com.youtube.tutorial.ecommercebackend.service; 2 | 3 | import com.youtube.tutorial.ecommercebackend.model.Product; 4 | import com.youtube.tutorial.ecommercebackend.model.dao.ProductDAO; 5 | import org.springframework.stereotype.Service; 6 | 7 | import java.util.List; 8 | 9 | /** 10 | * Service for handling product actions. 11 | */ 12 | @Service 13 | public class ProductService { 14 | 15 | /** The Product DAO. */ 16 | private ProductDAO productDAO; 17 | 18 | /** 19 | * Constructor for spring injection. 20 | * @param productDAO 21 | */ 22 | public ProductService(ProductDAO productDAO) { 23 | this.productDAO = productDAO; 24 | } 25 | 26 | /** 27 | * Gets the all products available. 28 | * @return The list of products. 29 | */ 30 | public List getProducts() { 31 | return productDAO.findAll(); 32 | } 33 | 34 | } 35 | -------------------------------------------------------------------------------- /src/main/resources/application.properties: -------------------------------------------------------------------------------- 1 | # Trick to let IntelliJ view endpoints through actuator. 2 | management.endpoints.jmx.exposure.include=* 3 | 4 | # Database connection & configuration 5 | spring.datasource.username=sa 6 | spring.datasource.password=Password1 7 | spring.datasource.url=jdbc:sqlserver://localhost;databaseName=ecommerce;encrypt=true;trustServerCertificate=true 8 | 9 | spring.jpa.hibernate.ddl-auto=update 10 | 11 | # Encryption configuration 12 | encryption.salt.rounds=10 13 | 14 | # JWT configuration 15 | jwt.algorithm.key=SuperSecureSecretKey 16 | jwt.issuer=eCommerce 17 | jwt.expiryInSeconds=604800 18 | 19 | # Email configuration 20 | email.from=no-reply@ecommerce.com 21 | 22 | # General configuration 23 | app.frontend.url=http://ecommerce.com 24 | 25 | # SMTP configuration 26 | spring.mail.host=localhost 27 | spring.mail.port=25 28 | #spring.mail.username= 29 | #spring.mail.password= 30 | #spring.properties.mail.smtp.auth=true 31 | #spring.properties.mail.smtp.starttls.enable=true -------------------------------------------------------------------------------- /src/main/java/com/youtube/tutorial/ecommercebackend/api/model/LoginResponse.java: -------------------------------------------------------------------------------- 1 | package com.youtube.tutorial.ecommercebackend.api.model; 2 | 3 | /** 4 | * The response object sent from login request. 5 | */ 6 | public class LoginResponse { 7 | 8 | /** The JWT token to be used for authentication. */ 9 | private String jwt; 10 | /** Was the login process successful? */ 11 | private boolean success; 12 | /** The reason for failure on login. */ 13 | private String failureReason; 14 | 15 | public String getJwt() { 16 | return jwt; 17 | } 18 | 19 | public void setJwt(String jwt) { 20 | this.jwt = jwt; 21 | } 22 | 23 | public boolean isSuccess() { 24 | return success; 25 | } 26 | 27 | public void setSuccess(boolean success) { 28 | this.success = success; 29 | } 30 | 31 | public String getFailureReason() { 32 | return failureReason; 33 | } 34 | 35 | public void setFailureReason(String failureReason) { 36 | this.failureReason = failureReason; 37 | } 38 | 39 | } 40 | -------------------------------------------------------------------------------- /src/main/java/com/youtube/tutorial/ecommercebackend/service/OrderService.java: -------------------------------------------------------------------------------- 1 | package com.youtube.tutorial.ecommercebackend.service; 2 | 3 | import com.youtube.tutorial.ecommercebackend.model.LocalUser; 4 | import com.youtube.tutorial.ecommercebackend.model.WebOrder; 5 | import com.youtube.tutorial.ecommercebackend.model.dao.WebOrderDAO; 6 | import org.springframework.stereotype.Service; 7 | 8 | import java.util.List; 9 | 10 | /** 11 | * Service for handling order actions. 12 | */ 13 | @Service 14 | public class OrderService { 15 | 16 | /** The Web Order DAO. */ 17 | private WebOrderDAO webOrderDAO; 18 | 19 | /** 20 | * Constructor for spring injection. 21 | * @param webOrderDAO 22 | */ 23 | public OrderService(WebOrderDAO webOrderDAO) { 24 | this.webOrderDAO = webOrderDAO; 25 | } 26 | 27 | /** 28 | * Gets the list of orders for a given user. 29 | * @param user The user to search for. 30 | * @return The list of orders. 31 | */ 32 | public List getOrders(LocalUser user) { 33 | return webOrderDAO.findByUser(user); 34 | } 35 | 36 | } 37 | -------------------------------------------------------------------------------- /src/main/java/com/youtube/tutorial/ecommercebackend/api/model/PasswordResetBody.java: -------------------------------------------------------------------------------- 1 | package com.youtube.tutorial.ecommercebackend.api.model; 2 | 3 | import jakarta.validation.constraints.NotBlank; 4 | import jakarta.validation.constraints.NotNull; 5 | import jakarta.validation.constraints.Pattern; 6 | import jakarta.validation.constraints.Size; 7 | 8 | /** 9 | * Request body to reset a password using a password reset token. 10 | */ 11 | public class PasswordResetBody { 12 | 13 | /** The token to authenticate the request. */ 14 | @NotBlank 15 | @NotNull 16 | private String token; 17 | /** The password to set to the account. */ 18 | @NotNull 19 | @NotBlank 20 | @Pattern(regexp = "^(?=.*[A-Za-z])(?=.*\\d)[A-Za-z\\d]{6,}$") 21 | @Size(min=6, max=32) 22 | private String password; 23 | 24 | public String getToken() { 25 | return token; 26 | } 27 | 28 | public void setToken(String token) { 29 | this.token = token; 30 | } 31 | 32 | public String getPassword() { 33 | return password; 34 | } 35 | 36 | public void setPassword(String password) { 37 | this.password = password; 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /src/main/java/com/youtube/tutorial/ecommercebackend/api/model/DataChange.java: -------------------------------------------------------------------------------- 1 | package com.youtube.tutorial.ecommercebackend.api.model; 2 | 3 | /** 4 | * Data model to outline data changes for websockets. 5 | * @param The data type being changed. 6 | */ 7 | public class DataChange { 8 | 9 | /** The ChangeType. */ 10 | private ChangeType changeType; 11 | /** The data being changed. */ 12 | private T data; 13 | 14 | /** 15 | * Default constructor. 16 | */ 17 | public DataChange() { 18 | } 19 | 20 | /** 21 | * Creates an instance. 22 | * @param changeType The ChangeType. 23 | * @param data The data changed. 24 | */ 25 | public DataChange(ChangeType changeType, T data) { 26 | this.changeType = changeType; 27 | this.data = data; 28 | } 29 | 30 | public ChangeType getChangeType() { 31 | return changeType; 32 | } 33 | 34 | public T getData() { 35 | return data; 36 | } 37 | 38 | public void setData(T data) { 39 | this.data = data; 40 | } 41 | 42 | /** 43 | * Enum to specify what kind of change is taking place. 44 | */ 45 | public enum ChangeType { 46 | INSERT, 47 | UPDATE, 48 | DELETE 49 | } 50 | 51 | } 52 | -------------------------------------------------------------------------------- /src/test/java/com/youtube/tutorial/ecommercebackend/api/controller/product/ProductControllerTest.java: -------------------------------------------------------------------------------- 1 | package com.youtube.tutorial.ecommercebackend.api.controller.product; 2 | 3 | import org.junit.jupiter.api.Test; 4 | import org.springframework.beans.factory.annotation.Autowired; 5 | import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc; 6 | import org.springframework.boot.test.context.SpringBootTest; 7 | import org.springframework.http.HttpStatus; 8 | import org.springframework.test.web.servlet.MockMvc; 9 | 10 | import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; 11 | import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; 12 | 13 | /** 14 | * Class for testing the ProductController 15 | */ 16 | @SpringBootTest 17 | @AutoConfigureMockMvc 18 | public class ProductControllerTest { 19 | 20 | /** Mocked MVC. */ 21 | @Autowired 22 | private MockMvc mvc; 23 | 24 | /** 25 | * Tests getting the product list. 26 | * @throws Exception 27 | */ 28 | @Test 29 | public void testProductList() throws Exception { 30 | mvc.perform(get("/product")).andExpect(status().is(HttpStatus.OK.value())); 31 | } 32 | 33 | } 34 | -------------------------------------------------------------------------------- /src/main/java/com/youtube/tutorial/ecommercebackend/api/controller/product/ProductController.java: -------------------------------------------------------------------------------- 1 | package com.youtube.tutorial.ecommercebackend.api.controller.product; 2 | 3 | import com.youtube.tutorial.ecommercebackend.model.Product; 4 | import com.youtube.tutorial.ecommercebackend.service.ProductService; 5 | import org.springframework.web.bind.annotation.GetMapping; 6 | import org.springframework.web.bind.annotation.RequestMapping; 7 | import org.springframework.web.bind.annotation.RestController; 8 | 9 | import java.util.List; 10 | 11 | /** 12 | * Controller to handle the creation, updating & viewing of products. 13 | */ 14 | @RestController 15 | @RequestMapping("/product") 16 | public class ProductController { 17 | 18 | /** The Product Service. */ 19 | private ProductService productService; 20 | 21 | /** 22 | * Constructor for spring injection. 23 | * @param productService 24 | */ 25 | public ProductController(ProductService productService) { 26 | this.productService = productService; 27 | } 28 | 29 | /** 30 | * Gets the list of products available. 31 | * @return The list of products. 32 | */ 33 | @GetMapping 34 | public List getProducts() { 35 | return productService.getProducts(); 36 | } 37 | 38 | } 39 | -------------------------------------------------------------------------------- /src/test/java/com/youtube/tutorial/ecommercebackend/service/EncryptionServiceTest.java: -------------------------------------------------------------------------------- 1 | package com.youtube.tutorial.ecommercebackend.service; 2 | 3 | import org.junit.jupiter.api.Assertions; 4 | import org.junit.jupiter.api.Test; 5 | import org.springframework.beans.factory.annotation.Autowired; 6 | import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc; 7 | import org.springframework.boot.test.context.SpringBootTest; 8 | 9 | /** 10 | * Class to test the Encryption Service. 11 | */ 12 | @SpringBootTest 13 | @AutoConfigureMockMvc 14 | public class EncryptionServiceTest { 15 | 16 | /** The EncryptionService to test. */ 17 | @Autowired 18 | private EncryptionService encryptionService; 19 | 20 | /** 21 | * Tests when a password is encrypted it is still valid when verifying. 22 | */ 23 | @Test 24 | public void testPasswordEncryption() { 25 | String password = "PasswordIsASecret!123"; 26 | String hash = encryptionService.encryptPassword(password); 27 | Assertions.assertTrue(encryptionService.verifyPassword(password, hash), "Hashed password should match original."); 28 | Assertions.assertFalse(encryptionService.verifyPassword(password + "Sike!", hash), "Altered password should not be valid."); 29 | } 30 | 31 | } 32 | -------------------------------------------------------------------------------- /src/test/java/com/youtube/tutorial/ecommercebackend/api/security/JUnitUserDetailsService.java: -------------------------------------------------------------------------------- 1 | package com.youtube.tutorial.ecommercebackend.api.security; 2 | 3 | import com.youtube.tutorial.ecommercebackend.model.LocalUser; 4 | import com.youtube.tutorial.ecommercebackend.model.dao.LocalUserDAO; 5 | import org.springframework.beans.factory.annotation.Autowired; 6 | import org.springframework.context.annotation.Primary; 7 | import org.springframework.security.core.userdetails.UserDetails; 8 | import org.springframework.security.core.userdetails.UserDetailsService; 9 | import org.springframework.security.core.userdetails.UsernameNotFoundException; 10 | import org.springframework.stereotype.Service; 11 | 12 | import java.util.Optional; 13 | 14 | /** 15 | * Service to provide spring with Authentication Principals for unit testing. 16 | */ 17 | @Service 18 | @Primary 19 | public class JUnitUserDetailsService implements UserDetailsService { 20 | 21 | /** The Local User DAO. */ 22 | @Autowired 23 | private LocalUserDAO localUserDAO; 24 | 25 | /** 26 | * {@inheritDoc} 27 | */ 28 | public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException { 29 | Optional opUser = localUserDAO.findByUsernameIgnoreCase(username); 30 | if (opUser.isPresent()) 31 | return opUser.get(); 32 | return null; 33 | } 34 | 35 | } 36 | -------------------------------------------------------------------------------- /src/main/java/com/youtube/tutorial/ecommercebackend/api/controller/order/OrderController.java: -------------------------------------------------------------------------------- 1 | package com.youtube.tutorial.ecommercebackend.api.controller.order; 2 | 3 | import com.youtube.tutorial.ecommercebackend.model.LocalUser; 4 | import com.youtube.tutorial.ecommercebackend.model.WebOrder; 5 | import com.youtube.tutorial.ecommercebackend.service.OrderService; 6 | import org.springframework.security.core.annotation.AuthenticationPrincipal; 7 | import org.springframework.web.bind.annotation.GetMapping; 8 | import org.springframework.web.bind.annotation.RequestMapping; 9 | import org.springframework.web.bind.annotation.RestController; 10 | 11 | import java.util.List; 12 | 13 | /** 14 | * Controller to handle requests to create, update and view orders. 15 | */ 16 | @RestController 17 | @RequestMapping("/order") 18 | public class OrderController { 19 | 20 | /** The Order Service. */ 21 | private OrderService orderService; 22 | 23 | /** 24 | * Constructor for spring injection. 25 | * @param orderService 26 | */ 27 | public OrderController(OrderService orderService) { 28 | this.orderService = orderService; 29 | } 30 | 31 | /** 32 | * Endpoint to get all orders for a specific user. 33 | * @param user The user provided by spring security context. 34 | * @return The list of orders the user had made. 35 | */ 36 | @GetMapping 37 | public List getOrders(@AuthenticationPrincipal LocalUser user) { 38 | return orderService.getOrders(user); 39 | } 40 | 41 | } 42 | -------------------------------------------------------------------------------- /src/main/java/com/youtube/tutorial/ecommercebackend/service/EncryptionService.java: -------------------------------------------------------------------------------- 1 | package com.youtube.tutorial.ecommercebackend.service; 2 | 3 | import jakarta.annotation.PostConstruct; 4 | import org.springframework.beans.factory.annotation.Value; 5 | import org.springframework.security.crypto.bcrypt.BCrypt; 6 | import org.springframework.stereotype.Service; 7 | 8 | /** 9 | * Service for handling encryption of passwords. 10 | */ 11 | @Service 12 | public class EncryptionService { 13 | 14 | /** How many salt rounds should the encryption run. */ 15 | @Value("${encryption.salt.rounds}") 16 | private int saltRounds; 17 | /** The salt built after construction. */ 18 | private String salt; 19 | 20 | /** 21 | * Post construction method. 22 | */ 23 | @PostConstruct 24 | public void postConstruct() { 25 | salt = BCrypt.gensalt(saltRounds); 26 | } 27 | 28 | /** 29 | * Encrypts the given password. 30 | * @param password The plain text password. 31 | * @return The encrypted password. 32 | */ 33 | public String encryptPassword(String password) { 34 | return BCrypt.hashpw(password, salt); 35 | } 36 | 37 | /** 38 | * Verifies that a password is correct. 39 | * @param password The plain text password. 40 | * @param hash The encrypted password. 41 | * @return True if the password is correct, false otherwise. 42 | */ 43 | public boolean verifyPassword(String password, String hash) { 44 | return BCrypt.checkpw(password, hash); 45 | } 46 | 47 | } 48 | -------------------------------------------------------------------------------- /src/main/java/com/youtube/tutorial/ecommercebackend/api/security/WebSecurityConfig.java: -------------------------------------------------------------------------------- 1 | package com.youtube.tutorial.ecommercebackend.api.security; 2 | 3 | import org.springframework.context.annotation.Bean; 4 | import org.springframework.context.annotation.Configuration; 5 | import org.springframework.security.config.annotation.web.builders.HttpSecurity; 6 | import org.springframework.security.web.SecurityFilterChain; 7 | import org.springframework.security.web.access.intercept.AuthorizationFilter; 8 | 9 | /** 10 | * Configuration of the security on endpoints. 11 | */ 12 | @Configuration 13 | public class WebSecurityConfig { 14 | 15 | private JWTRequestFilter jwtRequestFilter; 16 | 17 | public WebSecurityConfig(JWTRequestFilter jwtRequestFilter) { 18 | this.jwtRequestFilter = jwtRequestFilter; 19 | } 20 | 21 | /** 22 | * Filter chain to configure security. 23 | * @param http The security object. 24 | * @return The chain built. 25 | * @throws Exception Thrown on error configuring. 26 | */ 27 | @Bean 28 | public SecurityFilterChain filterChain(HttpSecurity http) throws Exception { 29 | http.csrf().disable().cors().disable(); 30 | // We need to make sure our authentication filter is run before the http request filter is run. 31 | http.addFilterBefore(jwtRequestFilter, AuthorizationFilter.class); 32 | http.authorizeHttpRequests() 33 | // Specific exclusions or rules. 34 | .requestMatchers("/product", "/auth/register", "/auth/login", 35 | "/auth/verify", "/auth/forgot", "/auth/reset", "/error", 36 | "/websocket", "/websocket/**").permitAll() 37 | // Everything else should be authenticated. 38 | .anyRequest().authenticated(); 39 | return http.build(); 40 | } 41 | 42 | } 43 | -------------------------------------------------------------------------------- /src/main/java/com/youtube/tutorial/ecommercebackend/api/model/RegistrationBody.java: -------------------------------------------------------------------------------- 1 | package com.youtube.tutorial.ecommercebackend.api.model; 2 | 3 | import jakarta.validation.constraints.Email; 4 | import jakarta.validation.constraints.NotBlank; 5 | import jakarta.validation.constraints.NotNull; 6 | import jakarta.validation.constraints.Pattern; 7 | import jakarta.validation.constraints.Size; 8 | 9 | /** 10 | * The information required to register a user. 11 | */ 12 | public class RegistrationBody { 13 | 14 | /** The username. */ 15 | @NotNull 16 | @NotBlank 17 | @Size(min=3, max=255) 18 | private String username; 19 | /** The email. */ 20 | @NotNull 21 | @NotBlank 22 | @Email 23 | private String email; 24 | /** The password. */ 25 | @NotNull 26 | @NotBlank 27 | @Pattern(regexp = "^(?=.*[A-Za-z])(?=.*\\d)[A-Za-z\\d]{6,}$") 28 | @Size(min=6, max=32) 29 | private String password; 30 | /** The first name. */ 31 | @NotNull 32 | @NotBlank 33 | private String firstName; 34 | /** The last name. */ 35 | @NotNull 36 | @NotBlank 37 | private String lastName; 38 | 39 | public String getUsername() { 40 | return username; 41 | } 42 | 43 | public void setUsername(String username) { 44 | this.username = username; 45 | } 46 | 47 | public String getEmail() { 48 | return email; 49 | } 50 | 51 | public void setEmail(String email) { 52 | this.email = email; 53 | } 54 | 55 | public String getPassword() { 56 | return password; 57 | } 58 | 59 | public void setPassword(String password) { 60 | this.password = password; 61 | } 62 | 63 | public String getFirstName() { 64 | return firstName; 65 | } 66 | 67 | public void setFirstName(String firstName) { 68 | this.firstName = firstName; 69 | } 70 | 71 | public String getLastName() { 72 | return lastName; 73 | } 74 | 75 | public void setLastName(String lastName) { 76 | this.lastName = lastName; 77 | } 78 | } 79 | -------------------------------------------------------------------------------- /src/test/resources/data.sql: -------------------------------------------------------------------------------- 1 | -- This file allows us to load static data into the test database before tests are run. 2 | 3 | -- Passwords are in the format: Password123. Unless specified otherwise. 4 | -- Encrypted using https://www.javainuse.com/onlineBcrypt 5 | INSERT INTO local_user (email, first_name, last_name, password, username, email_verified) 6 | VALUES ('UserA@junit.com', 'UserA-FirstName', 'UserA-LastName', '$2a$10$hBn5gu6cGelJNiE6DDsaBOmZgyumCSzVwrOK/37FWgJ6aLIdZSSI2', 'UserA', true) 7 | , ('UserB@junit.com', 'UserB-FirstName', 'UserB-LastName', '$2a$10$TlYbg57fqOy/1LJjispkjuSIvFJXbh3fy0J9fvHnCpuntZOITAjVG', 'UserB', false) 8 | , ('UserC@junit.com', 'UserC-FirstName', 'UserC-LastName', '$2a$10$SYiYAIW80gDh39jwSaPyiuKGuhrLi7xTUjocL..NOx/1COWe5P03.', 'UserC', false); 9 | 10 | INSERT INTO address(address_line_1, city, country, user_id) 11 | VALUES ('123 Tester Hill', 'Testerton', 'England', 1) 12 | , ('312 Spring Boot', 'Hibernate', 'England', 3); 13 | 14 | INSERT INTO product (name, short_description, long_description, price) 15 | VALUES ('Product #1', 'Product one short description.', 'This is a very long description of product #1.', 5.50) 16 | , ('Product #2', 'Product two short description.', 'This is a very long description of product #2.', 10.56) 17 | , ('Product #3', 'Product three short description.', 'This is a very long description of product #3.', 2.74) 18 | , ('Product #4', 'Product four short description.', 'This is a very long description of product #4.', 15.69) 19 | , ('Product #5', 'Product five short description.', 'This is a very long description of product #5.', 42.59); 20 | 21 | INSERT INTO inventory (product_id, quantity) 22 | VALUES (1, 5) 23 | , (2, 8) 24 | , (3, 12) 25 | , (4, 73) 26 | , (5, 2); 27 | 28 | INSERT INTO web_order (address_id, user_id) 29 | VALUES (1, 1) 30 | , (1, 1) 31 | , (1, 1) 32 | , (2, 3) 33 | , (2, 3); 34 | 35 | INSERT INTO web_order_quantities (order_id, product_id, quantity) 36 | VALUES (1, 1, 5) 37 | , (1, 2, 5) 38 | , (2, 3, 5) 39 | , (2, 2, 5) 40 | , (2, 5, 5) 41 | , (3, 3, 5) 42 | , (4, 4, 5) 43 | , (4, 2, 5) 44 | , (5, 3, 5) 45 | , (5, 1, 5); -------------------------------------------------------------------------------- /src/main/java/com/youtube/tutorial/ecommercebackend/model/Inventory.java: -------------------------------------------------------------------------------- 1 | package com.youtube.tutorial.ecommercebackend.model; 2 | 3 | import com.fasterxml.jackson.annotation.JsonIgnore; 4 | import jakarta.persistence.Column; 5 | import jakarta.persistence.Entity; 6 | import jakarta.persistence.GeneratedValue; 7 | import jakarta.persistence.GenerationType; 8 | import jakarta.persistence.Id; 9 | import jakarta.persistence.JoinColumn; 10 | import jakarta.persistence.OneToOne; 11 | import jakarta.persistence.Table; 12 | 13 | /** 14 | * Inventory of a product that available for purchase. 15 | */ 16 | @Entity 17 | @Table(name = "inventory") 18 | public class Inventory { 19 | 20 | /** Unique id for the inventory. */ 21 | @Id 22 | @GeneratedValue(strategy = GenerationType.IDENTITY) 23 | @Column(name = "id", nullable = false) 24 | private Long id; 25 | /** The product this inventory is of. */ 26 | @JsonIgnore 27 | @OneToOne(optional = false, orphanRemoval = true) 28 | @JoinColumn(name = "product_id", nullable = false, unique = true) 29 | private Product product; 30 | /** The quantity in stock. */ 31 | @Column(name = "quantity", nullable = false) 32 | private Integer quantity; 33 | 34 | /** 35 | * Gets the quantity in stock. 36 | * @return The quantity. 37 | */ 38 | public Integer getQuantity() { 39 | return quantity; 40 | } 41 | 42 | /** 43 | * Sets the quantity in stock of the product. 44 | * @param quantity The quantity to be set. 45 | */ 46 | public void setQuantity(Integer quantity) { 47 | this.quantity = quantity; 48 | } 49 | 50 | /** 51 | * Gets the product. 52 | * @return The product. 53 | */ 54 | public Product getProduct() { 55 | return product; 56 | } 57 | 58 | /** 59 | * Sets the product. 60 | * @param product The product to be set. 61 | */ 62 | public void setProduct(Product product) { 63 | this.product = product; 64 | } 65 | 66 | /** 67 | * Gets the ID of the inventory. 68 | * @return The ID. 69 | */ 70 | public Long getId() { 71 | return id; 72 | } 73 | 74 | /** 75 | * Sets the ID of the inventory. 76 | * @param id The ID. 77 | */ 78 | public void setId(Long id) { 79 | this.id = id; 80 | } 81 | 82 | } -------------------------------------------------------------------------------- /tools/websocket-tool/app.js: -------------------------------------------------------------------------------- 1 | var stompClient = null; 2 | 3 | function setConnected(connected) { 4 | $("#connect").prop("disabled", connected); 5 | $("#disconnect").prop("disabled", !connected); 6 | if (connected) { 7 | $("#messages").show(); 8 | $("#messages").html(""); 9 | } 10 | } 11 | 12 | function connect() { 13 | var socket = new SockJS($("#websocketURL").val()); 14 | stompClient = Stomp.over(socket); 15 | var headers = {}; 16 | if ($("#websocketToken").val() != null) { 17 | headers["Authorization"] = "Bearer " + $("#websocketToken").val(); 18 | } 19 | stompClient.connect(headers, 20 | function (frame) { 21 | setConnected(true); 22 | console.log('Connected: ' + frame); 23 | var headers = {}; 24 | if ($("#websocketToken").val() != null) { 25 | headers["Authorization"] = "Bearer " + $("#websocketToken").val(); 26 | } 27 | stompClient.subscribe($("#websocketTopic").val(), function (message) { 28 | console.log(message) 29 | showMessage("[ in ] " + message.body); 30 | }, headers); 31 | }, (err) => { 32 | console.log(err) 33 | showMessage("[ err ] " + err) 34 | setConnected(false); 35 | }); 36 | } 37 | 38 | function disconnect() { 39 | if (stompClient !== null) { 40 | stompClient.disconnect(); 41 | } 42 | setConnected(false); 43 | console.log("Disconnected"); 44 | } 45 | 46 | function sendMessage() { 47 | var headers = {}; 48 | if ($("#websocketToken").val() != null) { 49 | headers["Authorization"] = "Bearer " + $("#websocketToken").val(); 50 | } 51 | stompClient.send($("#websocketTopic").val(), headers, JSON.stringify($("#message").val())); 52 | showMessage("[ out ] " + $("#message").val()); 53 | } 54 | 55 | function showMessage(message) { 56 | $("#messages").append("" + message + ""); 57 | } 58 | 59 | $(function () { 60 | $("form").on('submit', function (e) { 61 | e.preventDefault(); 62 | }); 63 | $( "#connect" ).click(function() { connect(); }); 64 | $( "#disconnect" ).click(function() { disconnect(); }); 65 | $( "#send" ).click(function() { sendMessage(); }); 66 | }); -------------------------------------------------------------------------------- /src/main/java/com/youtube/tutorial/ecommercebackend/model/WebOrderQuantities.java: -------------------------------------------------------------------------------- 1 | package com.youtube.tutorial.ecommercebackend.model; 2 | 3 | import com.fasterxml.jackson.annotation.JsonIgnore; 4 | import jakarta.persistence.Column; 5 | import jakarta.persistence.Entity; 6 | import jakarta.persistence.GeneratedValue; 7 | import jakarta.persistence.GenerationType; 8 | import jakarta.persistence.Id; 9 | import jakarta.persistence.JoinColumn; 10 | import jakarta.persistence.ManyToOne; 11 | import jakarta.persistence.Table; 12 | 13 | /** 14 | * The quantity ordered of a product. 15 | */ 16 | @Entity 17 | @Table(name = "web_order_quantities") 18 | public class WebOrderQuantities { 19 | 20 | /** The unqiue id of the order quantity. */ 21 | @Id 22 | @GeneratedValue(strategy = GenerationType.IDENTITY) 23 | @Column(name = "id", nullable = false) 24 | private Long id; 25 | /** The product being ordered. */ 26 | @ManyToOne(optional = false) 27 | @JoinColumn(name = "product_id", nullable = false) 28 | private Product product; 29 | /** The quantity being ordered. */ 30 | @Column(name = "quantity", nullable = false) 31 | private Integer quantity; 32 | /** The order itself. */ 33 | @JsonIgnore 34 | @ManyToOne(optional = false) 35 | @JoinColumn(name = "order_id", nullable = false) 36 | private WebOrder order; 37 | 38 | /** 39 | * Gets the order. 40 | * @return The order. 41 | */ 42 | public WebOrder getOrder() { 43 | return order; 44 | } 45 | 46 | /** 47 | * Sets the order. 48 | * @param order The order. 49 | */ 50 | public void setOrder(WebOrder order) { 51 | this.order = order; 52 | } 53 | 54 | /** 55 | * Gets the quantity ordered. 56 | * @return The quantity. 57 | */ 58 | public Integer getQuantity() { 59 | return quantity; 60 | } 61 | 62 | /** 63 | * Sets the quantity ordered. 64 | * @param quantity The quantity. 65 | */ 66 | public void setQuantity(Integer quantity) { 67 | this.quantity = quantity; 68 | } 69 | 70 | /** 71 | * Gets the product ordered. 72 | * @return The product. 73 | */ 74 | public Product getProduct() { 75 | return product; 76 | } 77 | 78 | /** 79 | * Sets the product. 80 | * @param product The product. 81 | */ 82 | public void setProduct(Product product) { 83 | this.product = product; 84 | } 85 | 86 | /** 87 | * Gets the id. 88 | * @return The id. 89 | */ 90 | public Long getId() { 91 | return id; 92 | } 93 | 94 | /** 95 | * Sets the id. 96 | * @param id The id. 97 | */ 98 | public void setId(Long id) { 99 | this.id = id; 100 | } 101 | 102 | } -------------------------------------------------------------------------------- /tools/websocket-tool/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Hello WebSocket 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 15 |
16 |
17 |
18 |
19 |
20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 30 |
31 |
32 |
33 |
34 |
35 |
36 | 37 | 38 |
39 | 40 |
41 |
42 |
43 |
44 |
45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 |
Messages
54 |
55 |
56 |
57 | 58 | -------------------------------------------------------------------------------- /src/main/java/com/youtube/tutorial/ecommercebackend/model/VerificationToken.java: -------------------------------------------------------------------------------- 1 | package com.youtube.tutorial.ecommercebackend.model; 2 | 3 | import jakarta.persistence.Column; 4 | import jakarta.persistence.Entity; 5 | import jakarta.persistence.GeneratedValue; 6 | import jakarta.persistence.GenerationType; 7 | import jakarta.persistence.Id; 8 | import jakarta.persistence.JoinColumn; 9 | import jakarta.persistence.Lob; 10 | import jakarta.persistence.ManyToOne; 11 | import jakarta.persistence.Table; 12 | 13 | import java.sql.Timestamp; 14 | 15 | /** 16 | * Token that has been sent to the users email for verification. 17 | */ 18 | @Entity 19 | @Table(name = "verification_token") 20 | public class VerificationToken { 21 | 22 | /** The unique id for the record. */ 23 | @Id 24 | @GeneratedValue(strategy = GenerationType.IDENTITY) 25 | @Column(name = "id", nullable = false) 26 | private Long id; 27 | /** The token that was sent to the user. */ 28 | @Lob 29 | @Column(name = "token", nullable = false, unique = true) 30 | private String token; 31 | /** The timestamp of when the token was created. */ 32 | @Column(name = "created_timestamp", nullable = false) 33 | private Timestamp createdTimestamp; 34 | /** The user this verification token is for. */ 35 | @ManyToOne(optional = false) 36 | @JoinColumn(name = "user_id", nullable = false) 37 | private LocalUser user; 38 | 39 | 40 | /** 41 | * Gets the user. 42 | * @return The user. 43 | */ 44 | public LocalUser getUser() { 45 | return user; 46 | } 47 | 48 | /** 49 | * Sets the user. 50 | * @param user The user. 51 | */ 52 | public void setUser(LocalUser user) { 53 | this.user = user; 54 | } 55 | 56 | /** 57 | * The timestamp when the token was created. 58 | * @return The timestamp. 59 | */ 60 | public Timestamp getCreatedTimestamp() { 61 | return createdTimestamp; 62 | } 63 | 64 | /** 65 | * Sets the timestamp of when the token was created. 66 | * @param createdTimestamp The timestamp. 67 | */ 68 | public void setCreatedTimestamp(Timestamp createdTimestamp) { 69 | this.createdTimestamp = createdTimestamp; 70 | } 71 | 72 | /** 73 | * Gets the token. 74 | * @return The token. 75 | */ 76 | public String getToken() { 77 | return token; 78 | } 79 | 80 | /** 81 | * Sets the token. 82 | * @param token The token. 83 | */ 84 | public void setToken(String token) { 85 | this.token = token; 86 | } 87 | 88 | /** 89 | * Gets the id. 90 | * @return The id. 91 | */ 92 | public Long getId() { 93 | return id; 94 | } 95 | 96 | /** 97 | * Sets the id. 98 | * @param id The id. 99 | */ 100 | public void setId(Long id) { 101 | this.id = id; 102 | } 103 | 104 | } -------------------------------------------------------------------------------- /src/main/java/com/youtube/tutorial/ecommercebackend/model/WebOrder.java: -------------------------------------------------------------------------------- 1 | package com.youtube.tutorial.ecommercebackend.model; 2 | 3 | import jakarta.persistence.CascadeType; 4 | import jakarta.persistence.Column; 5 | import jakarta.persistence.Entity; 6 | import jakarta.persistence.GeneratedValue; 7 | import jakarta.persistence.GenerationType; 8 | import jakarta.persistence.Id; 9 | import jakarta.persistence.JoinColumn; 10 | import jakarta.persistence.ManyToOne; 11 | import jakarta.persistence.OneToMany; 12 | import jakarta.persistence.Table; 13 | 14 | import java.util.ArrayList; 15 | import java.util.List; 16 | 17 | /** 18 | * Order generated from the website. 19 | */ 20 | @Entity 21 | @Table(name = "web_order") 22 | public class WebOrder { 23 | 24 | /** Unique id for the order. */ 25 | @Id 26 | @GeneratedValue(strategy = GenerationType.IDENTITY) 27 | @Column(name = "id", nullable = false) 28 | private Long id; 29 | /** The user of the order. */ 30 | @ManyToOne(optional = false) 31 | @JoinColumn(name = "user_id", nullable = false) 32 | private LocalUser user; 33 | /** The shipping address of the order. */ 34 | @ManyToOne(optional = false) 35 | @JoinColumn(name = "address_id", nullable = false) 36 | private Address address; 37 | /** The quantities ordered. */ 38 | @OneToMany(mappedBy = "order", cascade = CascadeType.REMOVE, orphanRemoval = true) 39 | private List quantities = new ArrayList<>(); 40 | 41 | /** 42 | * Gets the quantities ordered. 43 | * @return The quantities. 44 | */ 45 | public List getQuantities() { 46 | return quantities; 47 | } 48 | 49 | /** 50 | * Sets the quantities ordered. 51 | * @param quantities The quantities. 52 | */ 53 | public void setQuantities(List quantities) { 54 | this.quantities = quantities; 55 | } 56 | 57 | /** 58 | * Gets the address of the order. 59 | * @return The address. 60 | */ 61 | public Address getAddress() { 62 | return address; 63 | } 64 | 65 | /** 66 | * Sets the address of the order. 67 | * @param address The address. 68 | */ 69 | public void setAddress(Address address) { 70 | this.address = address; 71 | } 72 | 73 | /** 74 | * Gets the user of the order. 75 | * @return The user. 76 | */ 77 | public LocalUser getUser() { 78 | return user; 79 | } 80 | 81 | /** 82 | * Sets the user of the order. 83 | * @param user The user. 84 | */ 85 | public void setUser(LocalUser user) { 86 | this.user = user; 87 | } 88 | 89 | /** 90 | * Gets the id of the order. 91 | * @return The id. 92 | */ 93 | public Long getId() { 94 | return id; 95 | } 96 | 97 | /** 98 | * Sets the id of the order. 99 | * @param id The id. 100 | */ 101 | public void setId(Long id) { 102 | this.id = id; 103 | } 104 | 105 | } -------------------------------------------------------------------------------- /src/test/java/com/youtube/tutorial/ecommercebackend/api/controller/order/OrderControllerTest.java: -------------------------------------------------------------------------------- 1 | package com.youtube.tutorial.ecommercebackend.api.controller.order; 2 | 3 | import com.fasterxml.jackson.core.type.TypeReference; 4 | import com.fasterxml.jackson.databind.ObjectMapper; 5 | import com.youtube.tutorial.ecommercebackend.model.WebOrder; 6 | import org.junit.jupiter.api.Assertions; 7 | import org.junit.jupiter.api.Test; 8 | import org.springframework.beans.factory.annotation.Autowired; 9 | import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc; 10 | import org.springframework.boot.test.context.SpringBootTest; 11 | import org.springframework.http.HttpStatus; 12 | import org.springframework.security.test.context.support.WithUserDetails; 13 | import org.springframework.test.web.servlet.MockMvc; 14 | 15 | import java.util.List; 16 | 17 | import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; 18 | import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; 19 | 20 | /** 21 | * Class for testing OrderController. 22 | */ 23 | @SpringBootTest 24 | @AutoConfigureMockMvc 25 | public class OrderControllerTest { 26 | 27 | /** Mocked MVC. */ 28 | @Autowired 29 | private MockMvc mvc; 30 | 31 | /** 32 | * Tests that when requested authenticated order list it belongs to user A. 33 | * @throws Exception 34 | */ 35 | @Test 36 | @WithUserDetails("UserA") 37 | public void testUserAAuthenticatedOrderList() throws Exception { 38 | testAuthenticatedListBelongsToUser("UserA"); 39 | } 40 | 41 | /** 42 | * Tests that when requested authenticated order list it belongs to user B. 43 | * @throws Exception 44 | */ 45 | @Test 46 | @WithUserDetails("UserB") 47 | public void testUserBAuthenticatedOrderList() throws Exception { 48 | testAuthenticatedListBelongsToUser("UserB"); 49 | } 50 | 51 | 52 | /** 53 | * Tests that when requested authenticated order list it belongs to the given user. 54 | * @param username the username to test for. 55 | * @throws Exception 56 | */ 57 | private void testAuthenticatedListBelongsToUser(String username) throws Exception { 58 | mvc.perform(get("/order")).andExpect(status().is(HttpStatus.OK.value())) 59 | .andExpect(result -> { 60 | String json = result.getResponse().getContentAsString(); 61 | List orders = new ObjectMapper().readValue(json, new TypeReference>() {}); 62 | for (WebOrder order : orders) { 63 | Assertions.assertEquals(username, order.getUser().getUsername(), "Order list should only be orders belonging to the user."); 64 | } 65 | }); 66 | } 67 | 68 | /** 69 | * Tests the unauthenticated users do not receive data. 70 | * @throws Exception 71 | */ 72 | @Test 73 | public void testUnauthenticatedOrderList() throws Exception { 74 | mvc.perform(get("/order")).andExpect(status().is(HttpStatus.FORBIDDEN.value())); 75 | } 76 | 77 | } 78 | -------------------------------------------------------------------------------- /src/test/java/com/youtube/tutorial/ecommercebackend/api/security/JWTRequestFilterTest.java: -------------------------------------------------------------------------------- 1 | package com.youtube.tutorial.ecommercebackend.api.security; 2 | 3 | import com.youtube.tutorial.ecommercebackend.model.LocalUser; 4 | import com.youtube.tutorial.ecommercebackend.model.dao.LocalUserDAO; 5 | import com.youtube.tutorial.ecommercebackend.service.JWTService; 6 | import org.junit.jupiter.api.Test; 7 | import org.springframework.beans.factory.annotation.Autowired; 8 | import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc; 9 | import org.springframework.boot.test.context.SpringBootTest; 10 | import org.springframework.http.HttpStatus; 11 | import org.springframework.test.web.servlet.MockMvc; 12 | 13 | import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; 14 | import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; 15 | 16 | /** 17 | * Class for testing the JWTRequestFilter. 18 | */ 19 | @SpringBootTest 20 | @AutoConfigureMockMvc 21 | public class JWTRequestFilterTest { 22 | 23 | /** Mocked MVC. */ 24 | @Autowired 25 | private MockMvc mvc; 26 | /** The JWT Service. */ 27 | @Autowired 28 | private JWTService jwtService; 29 | /** The Local User DAO. */ 30 | @Autowired 31 | private LocalUserDAO localUserDAO; 32 | /** The path that should only allow authenticated users. */ 33 | private static final String AUTHENTICATED_PATH = "/auth/me"; 34 | 35 | /** 36 | * Tests that unauthenticated requests are rejected. 37 | * @throws Exception 38 | */ 39 | @Test 40 | public void testUnauthenticatedRequest() throws Exception { 41 | mvc.perform(get(AUTHENTICATED_PATH)).andExpect(status().is(HttpStatus.FORBIDDEN.value())); 42 | } 43 | 44 | /** 45 | * Tests that bad tokens are rejected. 46 | * @throws Exception 47 | */ 48 | @Test 49 | public void testBadToken() throws Exception { 50 | mvc.perform(get(AUTHENTICATED_PATH).header("Authorization", "BadTokenThatIsNotValid")) 51 | .andExpect(status().is(HttpStatus.FORBIDDEN.value())); 52 | mvc.perform(get(AUTHENTICATED_PATH).header("Authorization", "Bearer BadTokenThatIsNotValid")) 53 | .andExpect(status().is(HttpStatus.FORBIDDEN.value())); 54 | } 55 | 56 | /** 57 | * Tests unverified users who somehow get a jwt are rejected. 58 | * @throws Exception 59 | */ 60 | @Test 61 | public void testUnverifiedUser() throws Exception { 62 | LocalUser user = localUserDAO.findByUsernameIgnoreCase("UserB").get(); 63 | String token = jwtService.generateJWT(user); 64 | mvc.perform(get(AUTHENTICATED_PATH).header("Authorization", "Bearer " + token)) 65 | .andExpect(status().is(HttpStatus.FORBIDDEN.value())); 66 | } 67 | 68 | /** 69 | * Tests the successful authentication. 70 | * @throws Exception 71 | */ 72 | @Test 73 | public void testSuccessful() throws Exception { 74 | LocalUser user = localUserDAO.findByUsernameIgnoreCase("UserA").get(); 75 | String token = jwtService.generateJWT(user); 76 | mvc.perform(get(AUTHENTICATED_PATH).header("Authorization", "Bearer " + token)) 77 | .andExpect(status().is(HttpStatus.OK.value())); 78 | } 79 | 80 | } 81 | -------------------------------------------------------------------------------- /src/main/java/com/youtube/tutorial/ecommercebackend/service/EmailService.java: -------------------------------------------------------------------------------- 1 | package com.youtube.tutorial.ecommercebackend.service; 2 | 3 | import com.youtube.tutorial.ecommercebackend.exception.EmailFailureException; 4 | import com.youtube.tutorial.ecommercebackend.model.LocalUser; 5 | import com.youtube.tutorial.ecommercebackend.model.VerificationToken; 6 | import org.springframework.beans.factory.annotation.Value; 7 | import org.springframework.mail.MailException; 8 | import org.springframework.mail.SimpleMailMessage; 9 | import org.springframework.mail.javamail.JavaMailSender; 10 | import org.springframework.stereotype.Service; 11 | 12 | /** 13 | * Service for handling emails being sent. 14 | */ 15 | @Service 16 | public class EmailService { 17 | 18 | /** The from address to use on emails. */ 19 | @Value("${email.from}") 20 | private String fromAddress; 21 | /** The url of the front end for links. */ 22 | @Value("${app.frontend.url}") 23 | private String url; 24 | /** The JavaMailSender instance. */ 25 | private JavaMailSender javaMailSender; 26 | 27 | /** 28 | * Constructor for spring injection. 29 | * @param javaMailSender 30 | */ 31 | public EmailService(JavaMailSender javaMailSender) { 32 | this.javaMailSender = javaMailSender; 33 | } 34 | 35 | /** 36 | * Makes a SimpleMailMessage for sending. 37 | * @return The SimpleMailMessage created. 38 | */ 39 | private SimpleMailMessage makeMailMessage() { 40 | SimpleMailMessage simpleMailMessage = new SimpleMailMessage(); 41 | simpleMailMessage.setFrom(fromAddress); 42 | return simpleMailMessage; 43 | } 44 | 45 | /** 46 | * Sends a verification email to the user. 47 | * @param verificationToken The verification token to be sent. 48 | * @throws EmailFailureException Thrown if are unable to send the email. 49 | */ 50 | public void sendVerificationEmail(VerificationToken verificationToken) throws EmailFailureException { 51 | SimpleMailMessage message = makeMailMessage(); 52 | message.setTo(verificationToken.getUser().getEmail()); 53 | message.setSubject("Verify your email to active your account."); 54 | message.setText("Please follow the link below to verify your email to active your account.\n" + 55 | url + "/auth/verify?token=" + verificationToken.getToken()); 56 | try { 57 | javaMailSender.send(message); 58 | } catch (MailException ex) { 59 | throw new EmailFailureException(); 60 | } 61 | } 62 | 63 | /** 64 | * Sends a password reset request email to the user. 65 | * @param user The user to send to. 66 | * @param token The token to send the user for reset. 67 | * @throws EmailFailureException 68 | */ 69 | public void sendPasswordResetEmail(LocalUser user, String token) throws EmailFailureException { 70 | SimpleMailMessage message = makeMailMessage(); 71 | message.setTo(user.getEmail()); 72 | message.setSubject("Your password reset request link."); 73 | message.setText("You requested a password reset on our website. Please " + 74 | "find the link below to be able to reset your password.\n" + url + 75 | "/auth/reset?token=" + token); 76 | try { 77 | javaMailSender.send(message); 78 | } catch (MailException ex) { 79 | throw new EmailFailureException(); 80 | } 81 | } 82 | 83 | } 84 | -------------------------------------------------------------------------------- /pom.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 4.0.0 5 | 6 | org.springframework.boot 7 | spring-boot-starter-parent 8 | 3.0.0 9 | 10 | 11 | com.youtube.tutorial 12 | ecommerce-backend 13 | 0.0.1-SNAPSHOT 14 | ecommerce-backend 15 | Our REST API Backend for the e-commerce solution. 16 | 17 | 17 18 | 19 | 20 | 21 | org.springframework.boot 22 | spring-boot-starter-actuator 23 | 24 | 25 | org.springframework.boot 26 | spring-boot-starter-web 27 | 28 | 29 | org.springframework.boot 30 | spring-boot-starter-data-jpa 31 | 32 | 33 | org.springframework.boot 34 | spring-boot-starter-validation 35 | 36 | 37 | org.springframework.boot 38 | spring-boot-starter-security 39 | 40 | 41 | org.springframework.boot 42 | spring-boot-starter-mail 43 | 44 | 45 | org.springframework.boot 46 | spring-boot-starter-websocket 47 | 48 | 49 | org.springframework.security 50 | spring-security-messaging 51 | 52 | 53 | 54 | com.auth0 55 | java-jwt 56 | 4.2.1 57 | 58 | 59 | com.microsoft.sqlserver 60 | mssql-jdbc 61 | runtime 62 | 63 | 64 | 65 | org.springframework.boot 66 | spring-boot-devtools 67 | runtime 68 | true 69 | 70 | 71 | 72 | org.springframework.boot 73 | spring-boot-starter-test 74 | test 75 | 76 | 77 | com.h2database 78 | h2 79 | test 80 | 81 | 82 | com.icegreen 83 | greenmail-junit5 84 | 2.0.0-alpha-2 85 | 86 | 87 | org.springframework.security 88 | spring-security-test 89 | test 90 | 91 | 92 | 93 | 94 | 95 | 96 | org.springframework.boot 97 | spring-boot-maven-plugin 98 | 99 | 100 | 101 | 102 | 103 | -------------------------------------------------------------------------------- /src/main/java/com/youtube/tutorial/ecommercebackend/model/Address.java: -------------------------------------------------------------------------------- 1 | package com.youtube.tutorial.ecommercebackend.model; 2 | 3 | import com.fasterxml.jackson.annotation.JsonIgnore; 4 | import jakarta.persistence.Column; 5 | import jakarta.persistence.Entity; 6 | import jakarta.persistence.GeneratedValue; 7 | import jakarta.persistence.GenerationType; 8 | import jakarta.persistence.Id; 9 | import jakarta.persistence.JoinColumn; 10 | import jakarta.persistence.ManyToOne; 11 | import jakarta.persistence.Table; 12 | 13 | /** 14 | * Address for the user to be billed/delivered to. 15 | */ 16 | @Entity 17 | @Table(name = "address") 18 | public class Address { 19 | 20 | /** Unique id for the address. */ 21 | @Id 22 | @GeneratedValue(strategy = GenerationType.IDENTITY) 23 | @Column(name = "id", nullable = false) 24 | private Long id; 25 | /** The first line of address. */ 26 | @Column(name = "address_line_1", nullable = false, length = 512) 27 | private String addressLine1; 28 | /** The second line of address. */ 29 | @Column(name = "address_line_2", length = 512) 30 | private String addressLine2; 31 | /** The city of the address. */ 32 | @Column(name = "city", nullable = false) 33 | private String city; 34 | /** The country of the address. */ 35 | @Column(name = "country", nullable = false, length = 75) 36 | private String country; 37 | /** The user the address is associated with. */ 38 | @JsonIgnore 39 | @ManyToOne(optional = false) 40 | @JoinColumn(name = "user_id", nullable = false) 41 | private LocalUser user; 42 | 43 | /** 44 | * Gets the user. 45 | * @return The user. 46 | */ 47 | public LocalUser getUser() { 48 | return user; 49 | } 50 | 51 | /** 52 | * Sets the user. 53 | * @param user User to be set. 54 | */ 55 | public void setUser(LocalUser user) { 56 | this.user = user; 57 | } 58 | 59 | /** 60 | * Gets the country. 61 | * @return The country. 62 | */ 63 | public String getCountry() { 64 | return country; 65 | } 66 | 67 | /** 68 | * Sets the country. 69 | * @param country The country to be set. 70 | */ 71 | public void setCountry(String country) { 72 | this.country = country; 73 | } 74 | 75 | /** 76 | * Gets the city. 77 | * @return The city. 78 | */ 79 | public String getCity() { 80 | return city; 81 | } 82 | 83 | /** 84 | * Sets the city. 85 | * @param city The city to be set. 86 | */ 87 | public void setCity(String city) { 88 | this.city = city; 89 | } 90 | 91 | /** 92 | * Gets the Address Line 2. 93 | * @return The second line of the address. 94 | */ 95 | public String getAddressLine2() { 96 | return addressLine2; 97 | } 98 | 99 | /** 100 | * Sets the second line of address. 101 | * @param addressLine2 The second line of address. 102 | */ 103 | public void setAddressLine2(String addressLine2) { 104 | this.addressLine2 = addressLine2; 105 | } 106 | 107 | /** 108 | * Gets the Address Line 1. 109 | * @return The first line of the address. 110 | */ 111 | public String getAddressLine1() { 112 | return addressLine1; 113 | } 114 | 115 | /** 116 | * Sets the first line of address. 117 | * @param addressLine1 The first line of address. 118 | */ 119 | public void setAddressLine1(String addressLine1) { 120 | this.addressLine1 = addressLine1; 121 | } 122 | 123 | /** 124 | * Gets the ID of the address. 125 | * @return The ID. 126 | */ 127 | public Long getId() { 128 | return id; 129 | } 130 | 131 | /** 132 | * Sets the ID of the address. 133 | * @param id The ID. 134 | */ 135 | public void setId(Long id) { 136 | this.id = id; 137 | } 138 | 139 | } -------------------------------------------------------------------------------- /src/main/java/com/youtube/tutorial/ecommercebackend/service/JWTService.java: -------------------------------------------------------------------------------- 1 | package com.youtube.tutorial.ecommercebackend.service; 2 | 3 | import com.auth0.jwt.JWT; 4 | import com.auth0.jwt.algorithms.Algorithm; 5 | import com.auth0.jwt.interfaces.DecodedJWT; 6 | import com.youtube.tutorial.ecommercebackend.model.LocalUser; 7 | import jakarta.annotation.PostConstruct; 8 | import org.springframework.beans.factory.annotation.Value; 9 | import org.springframework.stereotype.Service; 10 | 11 | import java.util.Date; 12 | 13 | /** 14 | * Service for handling JWTs for user authentication. 15 | */ 16 | @Service 17 | public class JWTService { 18 | 19 | /** The secret key to encrypt the JWTs with. */ 20 | @Value("${jwt.algorithm.key}") 21 | private String algorithmKey; 22 | /** The issuer the JWT is signed with. */ 23 | @Value("${jwt.issuer}") 24 | private String issuer; 25 | /** How many seconds from generation should the JWT expire? */ 26 | @Value("${jwt.expiryInSeconds}") 27 | private int expiryInSeconds; 28 | /** The algorithm generated post construction. */ 29 | private Algorithm algorithm; 30 | /** The JWT claim key for the username. */ 31 | private static final String USERNAME_KEY = "USERNAME"; 32 | private static final String VERIFICATION_EMAIL_KEY = "VERIFICATION_EMAIL"; 33 | private static final String RESET_PASSWORD_EMAIL_KEY = "RESET_PASSWORD_EMAIL"; 34 | 35 | /** 36 | * Post construction method. 37 | */ 38 | @PostConstruct 39 | public void postConstruct() { 40 | algorithm = Algorithm.HMAC256(algorithmKey); 41 | } 42 | 43 | /** 44 | * Generates a JWT based on the given user. 45 | * @param user The user to generate for. 46 | * @return The JWT. 47 | */ 48 | public String generateJWT(LocalUser user) { 49 | return JWT.create() 50 | .withClaim(USERNAME_KEY, user.getUsername()) 51 | .withExpiresAt(new Date(System.currentTimeMillis() + (1000 * expiryInSeconds))) 52 | .withIssuer(issuer) 53 | .sign(algorithm); 54 | } 55 | 56 | /** 57 | * Generates a special token for verification of an email. 58 | * @param user The user to create the token for. 59 | * @return The token generated. 60 | */ 61 | public String generateVerificationJWT(LocalUser user) { 62 | return JWT.create() 63 | .withClaim(VERIFICATION_EMAIL_KEY, user.getEmail()) 64 | .withExpiresAt(new Date(System.currentTimeMillis() + (1000 * expiryInSeconds))) 65 | .withIssuer(issuer) 66 | .sign(algorithm); 67 | } 68 | 69 | /** 70 | * Generates a JWT for use when resetting a password. 71 | * @param user The user to generate for. 72 | * @return The generated JWT token. 73 | */ 74 | public String generatePasswordResetJWT(LocalUser user) { 75 | return JWT.create() 76 | .withClaim(RESET_PASSWORD_EMAIL_KEY, user.getEmail()) 77 | .withExpiresAt(new Date(System.currentTimeMillis() + (1000 * 60 * 30))) 78 | .withIssuer(issuer) 79 | .sign(algorithm); 80 | } 81 | 82 | /** 83 | * Gets the email from a password reset token. 84 | * @param token The token to use. 85 | * @return The email in the token if valid. 86 | */ 87 | public String getResetPasswordEmail(String token) { 88 | DecodedJWT jwt = JWT.require(algorithm).withIssuer(issuer).build().verify(token); 89 | return jwt.getClaim(RESET_PASSWORD_EMAIL_KEY).asString(); 90 | } 91 | 92 | /** 93 | * Gets the username out of a given JWT. 94 | * @param token The JWT to decode. 95 | * @return The username stored inside. 96 | */ 97 | public String getUsername(String token) { 98 | DecodedJWT jwt = JWT.require(algorithm).withIssuer(issuer).build().verify(token); 99 | return jwt.getClaim(USERNAME_KEY).asString(); 100 | } 101 | 102 | } 103 | -------------------------------------------------------------------------------- /src/main/java/com/youtube/tutorial/ecommercebackend/model/Product.java: -------------------------------------------------------------------------------- 1 | package com.youtube.tutorial.ecommercebackend.model; 2 | 3 | import jakarta.persistence.CascadeType; 4 | import jakarta.persistence.Column; 5 | import jakarta.persistence.Entity; 6 | import jakarta.persistence.GeneratedValue; 7 | import jakarta.persistence.GenerationType; 8 | import jakarta.persistence.Id; 9 | import jakarta.persistence.OneToOne; 10 | import jakarta.persistence.Table; 11 | 12 | /** 13 | * A product available for purchasing. 14 | */ 15 | @Entity 16 | @Table(name = "product") 17 | public class Product { 18 | 19 | /** Unique id for the product. */ 20 | @Id 21 | @GeneratedValue(strategy = GenerationType.IDENTITY) 22 | @Column(name = "id", nullable = false) 23 | private Long id; 24 | /** The name of the product. */ 25 | @Column(name = "name", nullable = false, unique = true) 26 | private String name; 27 | /** The short description of the product. */ 28 | @Column(name = "short_description", nullable = false) 29 | private String shortDescription; 30 | /** The long description of the product. */ 31 | @Column(name = "long_description") 32 | private String longDescription; 33 | /** The price of the product. */ 34 | @Column(name = "price", nullable = false) 35 | private Double price; 36 | /** The inventory of the product. */ 37 | @OneToOne(mappedBy = "product", cascade = CascadeType.REMOVE, optional = false, orphanRemoval = true) 38 | private Inventory inventory; 39 | 40 | /** 41 | * Gets the inventory of the product. 42 | * @return The inventory. 43 | */ 44 | public Inventory getInventory() { 45 | return inventory; 46 | } 47 | 48 | /** 49 | * Sets the inventory of the product. 50 | * @param inventory The inventory. 51 | */ 52 | public void setInventory(Inventory inventory) { 53 | this.inventory = inventory; 54 | } 55 | 56 | /** 57 | * Gets the price of the product. 58 | * @return The price. 59 | */ 60 | public Double getPrice() { 61 | return price; 62 | } 63 | 64 | /** 65 | * Sets the price of the product. 66 | * @param price The price. 67 | */ 68 | public void setPrice(Double price) { 69 | this.price = price; 70 | } 71 | 72 | /** 73 | * Gets the long description of the product. 74 | * @return The long description. 75 | */ 76 | public String getLongDescription() { 77 | return longDescription; 78 | } 79 | 80 | /** 81 | * Sets the long description of the product. 82 | * @param longDescription The long description. 83 | */ 84 | public void setLongDescription(String longDescription) { 85 | this.longDescription = longDescription; 86 | } 87 | 88 | /** 89 | * Gets the short description of the product. 90 | * @return The short description. 91 | */ 92 | public String getShortDescription() { 93 | return shortDescription; 94 | } 95 | 96 | /** 97 | * Sets the short description of the product. 98 | * @param shortDescription The short description. 99 | */ 100 | public void setShortDescription(String shortDescription) { 101 | this.shortDescription = shortDescription; 102 | } 103 | 104 | /** 105 | * Gets the name of the product. 106 | * @return The name. 107 | */ 108 | public String getName() { 109 | return name; 110 | } 111 | 112 | /** 113 | * Sets the name of the product. 114 | * @param name The name. 115 | */ 116 | public void setName(String name) { 117 | this.name = name; 118 | } 119 | 120 | /** 121 | * Gets the id of the product. 122 | * @return The id. 123 | */ 124 | public Long getId() { 125 | return id; 126 | } 127 | 128 | /** 129 | * Sets the id of the product. 130 | * @param id The id. 131 | */ 132 | public void setId(Long id) { 133 | this.id = id; 134 | } 135 | 136 | } -------------------------------------------------------------------------------- /src/main/java/com/youtube/tutorial/ecommercebackend/api/security/JWTRequestFilter.java: -------------------------------------------------------------------------------- 1 | package com.youtube.tutorial.ecommercebackend.api.security; 2 | 3 | import com.auth0.jwt.exceptions.JWTDecodeException; 4 | import com.youtube.tutorial.ecommercebackend.model.LocalUser; 5 | import com.youtube.tutorial.ecommercebackend.model.dao.LocalUserDAO; 6 | import com.youtube.tutorial.ecommercebackend.service.JWTService; 7 | import jakarta.servlet.FilterChain; 8 | import jakarta.servlet.ServletException; 9 | import jakarta.servlet.http.HttpServletRequest; 10 | import jakarta.servlet.http.HttpServletResponse; 11 | import org.springframework.messaging.Message; 12 | import org.springframework.messaging.MessageChannel; 13 | import org.springframework.messaging.simp.SimpMessageType; 14 | import org.springframework.messaging.support.ChannelInterceptor; 15 | import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; 16 | import org.springframework.security.core.context.SecurityContextHolder; 17 | import org.springframework.security.web.authentication.WebAuthenticationDetailsSource; 18 | import org.springframework.stereotype.Component; 19 | import org.springframework.web.filter.OncePerRequestFilter; 20 | 21 | import java.io.IOException; 22 | import java.util.ArrayList; 23 | import java.util.List; 24 | import java.util.Map; 25 | import java.util.Optional; 26 | 27 | /** 28 | * Filter for decoding a JWT in the Authorization header and loading the user 29 | * object into the authentication context. 30 | */ 31 | @Component 32 | public class JWTRequestFilter extends OncePerRequestFilter implements ChannelInterceptor { 33 | 34 | /** The JWT Service. */ 35 | private JWTService jwtService; 36 | /** The Local User DAO. */ 37 | private LocalUserDAO localUserDAO; 38 | 39 | /** 40 | * Constructor for spring injection. 41 | * @param jwtService 42 | * @param localUserDAO 43 | */ 44 | public JWTRequestFilter(JWTService jwtService, LocalUserDAO localUserDAO) { 45 | this.jwtService = jwtService; 46 | this.localUserDAO = localUserDAO; 47 | } 48 | 49 | /** 50 | * {@inheritDoc} 51 | */ 52 | @Override 53 | protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException { 54 | String tokenHeader = request.getHeader("Authorization"); 55 | UsernamePasswordAuthenticationToken token = checkToken(tokenHeader); 56 | if (token != null) { 57 | token.setDetails(new WebAuthenticationDetailsSource().buildDetails(request)); 58 | } 59 | filterChain.doFilter(request, response); 60 | } 61 | 62 | /** 63 | * Method to authenticate a token and return the Authentication object 64 | * written to the spring security context. 65 | * @param token The token to test. 66 | * @return The Authentication object if set. 67 | */ 68 | private UsernamePasswordAuthenticationToken checkToken(String token) { 69 | if (token != null && token.startsWith("Bearer ")) { 70 | token = token.substring(7); 71 | try { 72 | String username = jwtService.getUsername(token); 73 | Optional opUser = localUserDAO.findByUsernameIgnoreCase(username); 74 | if (opUser.isPresent()) { 75 | LocalUser user = opUser.get(); 76 | if (user.isEmailVerified()) { 77 | UsernamePasswordAuthenticationToken authentication = new UsernamePasswordAuthenticationToken(user, null, new ArrayList()); 78 | SecurityContextHolder.getContext().setAuthentication(authentication); 79 | return authentication; 80 | } 81 | } 82 | } catch (JWTDecodeException ex) { 83 | } 84 | } 85 | SecurityContextHolder.getContext().setAuthentication(null); 86 | return null; 87 | } 88 | 89 | /** 90 | * {@inheritDoc} 91 | */ 92 | @Override 93 | public Message preSend(Message message, MessageChannel channel) { 94 | SimpMessageType messageType = 95 | (SimpMessageType) message.getHeaders().get("simpMessageType"); 96 | if (messageType.equals(SimpMessageType.SUBSCRIBE) 97 | || messageType.equals(SimpMessageType.MESSAGE)) { 98 | Map nativeHeaders = (Map) message.getHeaders().get("nativeHeaders"); 99 | if (nativeHeaders != null) { 100 | List authTokenList = (List) nativeHeaders.get("Authorization"); 101 | if (authTokenList != null) { 102 | String tokenHeader = (String) authTokenList.get(0); 103 | checkToken(tokenHeader); 104 | } 105 | } 106 | } 107 | return message; 108 | } 109 | } 110 | -------------------------------------------------------------------------------- /src/test/java/com/youtube/tutorial/ecommercebackend/service/JWTServiceTest.java: -------------------------------------------------------------------------------- 1 | package com.youtube.tutorial.ecommercebackend.service; 2 | 3 | import com.auth0.jwt.JWT; 4 | import com.auth0.jwt.algorithms.Algorithm; 5 | import com.auth0.jwt.exceptions.MissingClaimException; 6 | import com.auth0.jwt.exceptions.SignatureVerificationException; 7 | import com.youtube.tutorial.ecommercebackend.model.LocalUser; 8 | import com.youtube.tutorial.ecommercebackend.model.dao.LocalUserDAO; 9 | import org.junit.jupiter.api.Assertions; 10 | import org.junit.jupiter.api.Test; 11 | import org.springframework.beans.factory.annotation.Autowired; 12 | import org.springframework.beans.factory.annotation.Value; 13 | import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc; 14 | import org.springframework.boot.test.context.SpringBootTest; 15 | 16 | /** 17 | * Class to test the JWTService. 18 | */ 19 | @SpringBootTest 20 | @AutoConfigureMockMvc 21 | public class JWTServiceTest { 22 | 23 | /** The JWTService to test. */ 24 | @Autowired 25 | private JWTService jwtService; 26 | /** The Local User DAO. */ 27 | @Autowired 28 | private LocalUserDAO localUserDAO; 29 | /** The algorithm key we're using in the properties file. */ 30 | @Value("${jwt.algorithm.key}") 31 | private String algorithmKey; 32 | 33 | /** 34 | * Tests that the verification token is not usable for login. 35 | */ 36 | @Test 37 | public void testVerificationTokenNotUsableForLogin() { 38 | LocalUser user = localUserDAO.findByUsernameIgnoreCase("UserA").get(); 39 | String token = jwtService.generateVerificationJWT(user); 40 | Assertions.assertNull(jwtService.getUsername(token), "Verification token should not contain username."); 41 | } 42 | 43 | /** 44 | * Tests that the authentication token generate still returns the username. 45 | */ 46 | @Test 47 | public void testAuthTokenReturnsUsername() { 48 | LocalUser user = localUserDAO.findByUsernameIgnoreCase("UserA").get(); 49 | String token = jwtService.generateJWT(user); 50 | Assertions.assertEquals(user.getUsername(), jwtService.getUsername(token), "Token for auth should contain users username."); 51 | } 52 | 53 | /** 54 | * Tests that when someone generates a JWT with an algorithm different to 55 | * ours the verification rejects the token as the signature is not verified. 56 | */ 57 | @Test 58 | public void testLoginJWTNotGeneratedByUs() { 59 | String token = 60 | JWT.create().withClaim("USERNAME", "UserA").sign(Algorithm.HMAC256( 61 | "NotTheRealSecret")); 62 | Assertions.assertThrows(SignatureVerificationException.class, 63 | () -> jwtService.getUsername(token)); 64 | } 65 | 66 | /** 67 | * Tests that when a JWT token is generated if it does not contain us as 68 | * the issuer we reject it. 69 | */ 70 | @Test 71 | public void testLoginJWTCorrectlySignedNoIssuer() { 72 | String token = 73 | JWT.create().withClaim("USERNAME", "UserA") 74 | .sign(Algorithm.HMAC256(algorithmKey)); 75 | Assertions.assertThrows(MissingClaimException.class, 76 | () -> jwtService.getUsername(token)); 77 | } 78 | 79 | /** 80 | * Tests that when someone generates a JWT with an algorithm different to 81 | * ours the verification rejects the token as the signature is not verified. 82 | */ 83 | @Test 84 | public void testResetPasswordJWTNotGeneratedByUs() { 85 | String token = 86 | JWT.create().withClaim("RESET_PASSWORD_EMAIL", "UserA@junit.com").sign(Algorithm.HMAC256( 87 | "NotTheRealSecret")); 88 | Assertions.assertThrows(SignatureVerificationException.class, 89 | () -> jwtService.getResetPasswordEmail(token)); 90 | } 91 | 92 | /** 93 | * Tests that when a JWT token is generated if it does not contain us as 94 | * the issuer we reject it. 95 | */ 96 | @Test 97 | public void testResetPasswordJWTCorrectlySignedNoIssuer() { 98 | String token = 99 | JWT.create().withClaim("RESET_PASSWORD_EMAIL", "UserA@junit.com") 100 | .sign(Algorithm.HMAC256(algorithmKey)); 101 | Assertions.assertThrows(MissingClaimException.class, 102 | () -> jwtService.getResetPasswordEmail(token)); 103 | } 104 | 105 | /** 106 | * Tests the password reset generation and verification. 107 | */ 108 | @Test 109 | public void testPasswordResetToken() { 110 | LocalUser user = localUserDAO.findByUsernameIgnoreCase("UserA").get(); 111 | String token = jwtService.generatePasswordResetJWT(user); 112 | Assertions.assertEquals(user.getEmail(), 113 | jwtService.getResetPasswordEmail(token), "Email should match inside " + 114 | "JWT."); 115 | } 116 | 117 | } 118 | -------------------------------------------------------------------------------- /insertDummyDevData.sql: -------------------------------------------------------------------------------- 1 | -- Script to insert dummy dev data into the database. 2 | 3 | -- You first need to register two users into the system before running this scirpt. 4 | 5 | -- Replace the id here with the first user id you want to have ownership of the orders. 6 | DECLARE @userId1 AS INT = 8; 7 | -- Replace the id here with the second user id you want to have ownership of the orders. 8 | DECLARE @userId2 AS INT = 9; 9 | 10 | DELETE FROM web_order_quantities; 11 | DELETE FROM web_order; 12 | DELETE FROM inventory; 13 | DELETE FROM product; 14 | DELETE FROM address; 15 | 16 | INSERT INTO product ([name], short_description, long_description, price) VALUES ('Product #1', 'Product one short description.', 'This is a very long description of product #1.', 5.50); 17 | INSERT INTO product ([name], short_description, long_description, price) VALUES ('Product #2', 'Product two short description.', 'This is a very long description of product #2.', 10.56); 18 | INSERT INTO product ([name], short_description, long_description, price) VALUES ('Product #3', 'Product three short description.', 'This is a very long description of product #3.', 2.74); 19 | INSERT INTO product ([name], short_description, long_description, price) VALUES ('Product #4', 'Product four short description.', 'This is a very long description of product #4.', 15.69); 20 | INSERT INTO product ([name], short_description, long_description, price) VALUES ('Product #5', 'Product five short description.', 'This is a very long description of product #5.', 42.59); 21 | 22 | DECLARE @product1 INT, @product2 INT, @product3 INT, @product4 INT, @product5 AS INT; 23 | 24 | SELECT @product1 = id FROM product WHERE [name] = 'Product #1'; 25 | SELECT @product2 = id FROM product WHERE [name] = 'Product #2'; 26 | SELECT @product3 = id FROM product WHERE [name] = 'Product #3'; 27 | SELECT @product4 = id FROM product WHERE [name] = 'Product #4'; 28 | SELECT @product5 = id FROM product WHERE [name] = 'Product #5'; 29 | 30 | INSERT INTO inventory (product_id, quantity) VALUES (@product1, 5); 31 | INSERT INTO inventory (product_id, quantity) VALUES (@product2, 8); 32 | INSERT INTO inventory (product_id, quantity) VALUES (@product3, 12); 33 | INSERT INTO inventory (product_id, quantity) VALUES (@product4, 73); 34 | INSERT INTO inventory (product_id, quantity) VALUES (@product5, 2); 35 | 36 | INSERT INTO address (address_line_1, city, country, user_id) VALUES ('123 Tester Hill', 'Testerton', 'England', @userId1); 37 | INSERT INTO address (address_line_1, city, country, user_id) VALUES ('312 Spring Boot', 'Hibernate', 'England', @userId2); 38 | 39 | DECLARE @address1 INT, @address2 INT; 40 | 41 | SELECT TOP 1 @address1 = id FROM address WHERE user_id = @userId1 ORDER BY id DESC; 42 | SELECT TOP 1 @address2 = id FROM address WHERE user_id = @userId2 ORDER BY id DESC; 43 | 44 | INSERT INTO web_order (address_id, user_id) VALUES (@address1, @userId1); 45 | INSERT INTO web_order (address_id, user_id) VALUES (@address1, @userId1); 46 | INSERT INTO web_order (address_id, user_id) VALUES (@address1, @userId1); 47 | INSERT INTO web_order (address_id, user_id) VALUES (@address2, @userId2); 48 | INSERT INTO web_order (address_id, user_id) VALUES (@address2, @userId2); 49 | 50 | DECLARE @order1 INT, @order2 INT, @order3 INT, @order4 INT, @order5 INT; 51 | 52 | SELECT TOP 1 @order1 = id FROM web_order WHERE address_id = @address1 AND user_id = @userId1 ORDER BY id DESC 53 | SELECT @order2 = id FROM web_order WHERE address_id = @address1 AND user_id = @userId1 ORDER BY id DESC OFFSET 1 ROW FETCH FIRST 1 ROW ONLY 54 | SELECT @order3 = id FROM web_order WHERE address_id = @address1 AND user_id = @userId1 ORDER BY id DESC OFFSET 2 ROW FETCH FIRST 1 ROW ONLY 55 | SELECT TOP 1 @order4 = id FROM web_order WHERE address_id = @address2 AND user_id = @userId2 ORDER BY id DESC 56 | SELECT @order5 = id FROM web_order WHERE address_id = @address2 AND user_id = @userId2 ORDER BY id DESC OFFSET 1 ROW FETCH FIRST 1 ROW ONLY 57 | 58 | INSERT INTO web_order_quantities (order_id, product_id, quantity) VALUES (@order1, @product1, 5); 59 | INSERT INTO web_order_quantities (order_id, product_id, quantity) VALUES (@order1, @product2, 5); 60 | INSERT INTO web_order_quantities (order_id, product_id, quantity) VALUES (@order2, @product3, 5); 61 | INSERT INTO web_order_quantities (order_id, product_id, quantity) VALUES (@order2, @product2, 5); 62 | INSERT INTO web_order_quantities (order_id, product_id, quantity) VALUES (@order2, @product5, 5); 63 | INSERT INTO web_order_quantities (order_id, product_id, quantity) VALUES (@order3, @product3, 5); 64 | INSERT INTO web_order_quantities (order_id, product_id, quantity) VALUES (@order4, @product4, 5); 65 | INSERT INTO web_order_quantities (order_id, product_id, quantity) VALUES (@order4, @product2, 5); 66 | INSERT INTO web_order_quantities (order_id, product_id, quantity) VALUES (@order5, @product3, 5); 67 | INSERT INTO web_order_quantities (order_id, product_id, quantity) VALUES (@order5, @product1, 5); -------------------------------------------------------------------------------- /src/main/java/com/youtube/tutorial/ecommercebackend/api/controller/user/UserController.java: -------------------------------------------------------------------------------- 1 | package com.youtube.tutorial.ecommercebackend.api.controller.user; 2 | 3 | import com.youtube.tutorial.ecommercebackend.api.model.DataChange; 4 | import com.youtube.tutorial.ecommercebackend.model.Address; 5 | import com.youtube.tutorial.ecommercebackend.model.LocalUser; 6 | import com.youtube.tutorial.ecommercebackend.model.dao.AddressDAO; 7 | import com.youtube.tutorial.ecommercebackend.service.UserService; 8 | import org.springframework.http.HttpStatus; 9 | import org.springframework.http.ResponseEntity; 10 | import org.springframework.messaging.simp.SimpMessagingTemplate; 11 | import org.springframework.security.core.annotation.AuthenticationPrincipal; 12 | import org.springframework.web.bind.annotation.GetMapping; 13 | import org.springframework.web.bind.annotation.PatchMapping; 14 | import org.springframework.web.bind.annotation.PathVariable; 15 | import org.springframework.web.bind.annotation.PutMapping; 16 | import org.springframework.web.bind.annotation.RequestBody; 17 | import org.springframework.web.bind.annotation.RequestMapping; 18 | import org.springframework.web.bind.annotation.RestController; 19 | 20 | import java.util.List; 21 | import java.util.Optional; 22 | 23 | /** 24 | * Rest Controller for user data interactions. 25 | */ 26 | @RestController 27 | @RequestMapping("/user") 28 | public class UserController { 29 | 30 | /** The Address DAO. */ 31 | private AddressDAO addressDAO; 32 | private SimpMessagingTemplate simpMessagingTemplate; 33 | private UserService userService; 34 | 35 | /** 36 | * Constructor for spring injection. 37 | * @param addressDAO 38 | * @param simpMessagingTemplate 39 | * @param userService 40 | */ 41 | public UserController(AddressDAO addressDAO, 42 | SimpMessagingTemplate simpMessagingTemplate, 43 | UserService userService) { 44 | this.addressDAO = addressDAO; 45 | this.simpMessagingTemplate = simpMessagingTemplate; 46 | this.userService = userService; 47 | } 48 | 49 | /** 50 | * Gets all addresses for the given user and presents them. 51 | * @param user The authenticated user account. 52 | * @param userId The user ID to get the addresses of. 53 | * @return The list of addresses. 54 | */ 55 | @GetMapping("/{userId}/address") 56 | public ResponseEntity> getAddress( 57 | @AuthenticationPrincipal LocalUser user, @PathVariable Long userId) { 58 | if (!userService.userHasPermissionToUser(user, userId)) { 59 | return ResponseEntity.status(HttpStatus.FORBIDDEN).build(); 60 | } 61 | return ResponseEntity.ok(addressDAO.findByUser_Id(userId)); 62 | } 63 | 64 | /** 65 | * Allows the user to add a new address. 66 | * @param user The authenticated user. 67 | * @param userId The user id for the new address. 68 | * @param address The Address to be added. 69 | * @return The saved address. 70 | */ 71 | @PutMapping("/{userId}/address") 72 | public ResponseEntity
putAddress( 73 | @AuthenticationPrincipal LocalUser user, @PathVariable Long userId, 74 | @RequestBody Address address) { 75 | if (!userService.userHasPermissionToUser(user, userId)) { 76 | return ResponseEntity.status(HttpStatus.FORBIDDEN).build(); 77 | } 78 | address.setId(null); 79 | LocalUser refUser = new LocalUser(); 80 | refUser.setId(userId); 81 | address.setUser(refUser); 82 | Address savedAddress = addressDAO.save(address); 83 | simpMessagingTemplate.convertAndSend("/topic/user/" + userId + "/address", 84 | new DataChange<>(DataChange.ChangeType.INSERT, address)); 85 | return ResponseEntity.ok(savedAddress); 86 | } 87 | 88 | /** 89 | * Updates the given address. 90 | * @param user The authenticated user. 91 | * @param userId The user ID the address belongs to. 92 | * @param addressId The address ID to alter. 93 | * @param address The updated address object. 94 | * @return The saved address object. 95 | */ 96 | @PatchMapping("/{userId}/address/{addressId}") 97 | public ResponseEntity
patchAddress( 98 | @AuthenticationPrincipal LocalUser user, @PathVariable Long userId, 99 | @PathVariable Long addressId, @RequestBody Address address) { 100 | if (!userService.userHasPermissionToUser(user, userId)) { 101 | return ResponseEntity.status(HttpStatus.FORBIDDEN).build(); 102 | } 103 | if (address.getId() == addressId) { 104 | Optional
opOriginalAddress = addressDAO.findById(addressId); 105 | if (opOriginalAddress.isPresent()) { 106 | LocalUser originalUser = opOriginalAddress.get().getUser(); 107 | if (originalUser.getId() == userId) { 108 | address.setUser(originalUser); 109 | Address savedAddress = addressDAO.save(address); 110 | simpMessagingTemplate.convertAndSend("/topic/user/" + userId + "/address", 111 | new DataChange<>(DataChange.ChangeType.UPDATE, address)); 112 | return ResponseEntity.ok(savedAddress); 113 | } 114 | } 115 | } 116 | return ResponseEntity.badRequest().build(); 117 | } 118 | 119 | } 120 | -------------------------------------------------------------------------------- /src/test/java/com/youtube/tutorial/ecommercebackend/api/controller/auth/AuthenticationControllerTest.java: -------------------------------------------------------------------------------- 1 | package com.youtube.tutorial.ecommercebackend.api.controller.auth; 2 | 3 | import com.fasterxml.jackson.databind.ObjectMapper; 4 | import com.icegreen.greenmail.configuration.GreenMailConfiguration; 5 | import com.icegreen.greenmail.junit5.GreenMailExtension; 6 | import com.icegreen.greenmail.util.ServerSetupTest; 7 | import com.youtube.tutorial.ecommercebackend.api.model.RegistrationBody; 8 | import jakarta.transaction.Transactional; 9 | import org.junit.jupiter.api.Test; 10 | import org.junit.jupiter.api.extension.RegisterExtension; 11 | import org.springframework.beans.factory.annotation.Autowired; 12 | import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc; 13 | import org.springframework.boot.test.context.SpringBootTest; 14 | import org.springframework.http.HttpStatus; 15 | import org.springframework.http.MediaType; 16 | import org.springframework.test.web.servlet.MockMvc; 17 | 18 | import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post; 19 | import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; 20 | 21 | /** 22 | * Tests the endpoints in the AuthenticationController. 23 | */ 24 | @SpringBootTest 25 | @AutoConfigureMockMvc 26 | public class AuthenticationControllerTest { 27 | 28 | /** Extension for mocking email sending. */ 29 | @RegisterExtension 30 | private static GreenMailExtension greenMailExtension = new GreenMailExtension(ServerSetupTest.SMTP) 31 | .withConfiguration(GreenMailConfiguration.aConfig().withUser("springboot", "secret")) 32 | .withPerMethodLifecycle(true); 33 | /** The Mocked MVC. */ 34 | @Autowired 35 | private MockMvc mvc; 36 | 37 | /** 38 | * Tests the register endpoint. 39 | * @throws Exception 40 | */ 41 | @Test 42 | @Transactional 43 | public void testRegister() throws Exception { 44 | ObjectMapper mapper = new ObjectMapper(); 45 | RegistrationBody body = new RegistrationBody(); 46 | body.setEmail("AuthenticationControllerTest$testRegister@junit.com"); 47 | body.setFirstName("FirstName"); 48 | body.setLastName("LastName"); 49 | body.setPassword("Password123"); 50 | // Null or blank username. 51 | body.setUsername(null); 52 | mvc.perform(post("/auth/register") 53 | .contentType(MediaType.APPLICATION_JSON) 54 | .content(mapper.writeValueAsString(body))) 55 | .andExpect(status().is(HttpStatus.BAD_REQUEST.value())); 56 | body.setUsername(""); 57 | mvc.perform(post("/auth/register") 58 | .contentType(MediaType.APPLICATION_JSON) 59 | .content(mapper.writeValueAsString(body))) 60 | .andExpect(status().is(HttpStatus.BAD_REQUEST.value())); 61 | body.setUsername("AuthenticationControllerTest$testRegister"); 62 | // Null or blank email. 63 | body.setEmail(null); 64 | mvc.perform(post("/auth/register") 65 | .contentType(MediaType.APPLICATION_JSON) 66 | .content(mapper.writeValueAsString(body))) 67 | .andExpect(status().is(HttpStatus.BAD_REQUEST.value())); 68 | body.setEmail(""); 69 | mvc.perform(post("/auth/register") 70 | .contentType(MediaType.APPLICATION_JSON) 71 | .content(mapper.writeValueAsString(body))) 72 | .andExpect(status().is(HttpStatus.BAD_REQUEST.value())); 73 | body.setEmail("AuthenticationControllerTest$testRegister@junit.com"); 74 | // Null or blank password. 75 | body.setPassword(null); 76 | mvc.perform(post("/auth/register") 77 | .contentType(MediaType.APPLICATION_JSON) 78 | .content(mapper.writeValueAsString(body))) 79 | .andExpect(status().is(HttpStatus.BAD_REQUEST.value())); 80 | body.setPassword(""); 81 | mvc.perform(post("/auth/register") 82 | .contentType(MediaType.APPLICATION_JSON) 83 | .content(mapper.writeValueAsString(body))) 84 | .andExpect(status().is(HttpStatus.BAD_REQUEST.value())); 85 | body.setPassword("Password123"); 86 | // Null or blank first name. 87 | body.setFirstName(null); 88 | mvc.perform(post("/auth/register") 89 | .contentType(MediaType.APPLICATION_JSON) 90 | .content(mapper.writeValueAsString(body))) 91 | .andExpect(status().is(HttpStatus.BAD_REQUEST.value())); 92 | body.setFirstName(""); 93 | mvc.perform(post("/auth/register") 94 | .contentType(MediaType.APPLICATION_JSON) 95 | .content(mapper.writeValueAsString(body))) 96 | .andExpect(status().is(HttpStatus.BAD_REQUEST.value())); 97 | body.setFirstName("FirstName"); 98 | // Null or blank last name. 99 | body.setLastName(null); 100 | mvc.perform(post("/auth/register") 101 | .contentType(MediaType.APPLICATION_JSON) 102 | .content(mapper.writeValueAsString(body))) 103 | .andExpect(status().is(HttpStatus.BAD_REQUEST.value())); 104 | body.setLastName(""); 105 | mvc.perform(post("/auth/register") 106 | .contentType(MediaType.APPLICATION_JSON) 107 | .content(mapper.writeValueAsString(body))) 108 | .andExpect(status().is(HttpStatus.BAD_REQUEST.value())); 109 | body.setLastName("LastName"); 110 | //TODO: Test password characters, username length & email validity. 111 | // Valid registration. 112 | mvc.perform(post("/auth/register") 113 | .contentType(MediaType.APPLICATION_JSON) 114 | .content(mapper.writeValueAsString(body))) 115 | .andExpect(status().is(HttpStatus.OK.value())); 116 | } 117 | 118 | } 119 | -------------------------------------------------------------------------------- /src/main/java/com/youtube/tutorial/ecommercebackend/api/controller/auth/AuthenticationController.java: -------------------------------------------------------------------------------- 1 | package com.youtube.tutorial.ecommercebackend.api.controller.auth; 2 | 3 | import com.youtube.tutorial.ecommercebackend.api.model.LoginBody; 4 | import com.youtube.tutorial.ecommercebackend.api.model.LoginResponse; 5 | import com.youtube.tutorial.ecommercebackend.api.model.PasswordResetBody; 6 | import com.youtube.tutorial.ecommercebackend.api.model.RegistrationBody; 7 | import com.youtube.tutorial.ecommercebackend.exception.EmailFailureException; 8 | import com.youtube.tutorial.ecommercebackend.exception.EmailNotFoundException; 9 | import com.youtube.tutorial.ecommercebackend.exception.UserAlreadyExistsException; 10 | import com.youtube.tutorial.ecommercebackend.exception.UserNotVerifiedException; 11 | import com.youtube.tutorial.ecommercebackend.model.LocalUser; 12 | import com.youtube.tutorial.ecommercebackend.service.UserService; 13 | import jakarta.validation.Valid; 14 | import org.apache.coyote.Response; 15 | import org.springframework.boot.actuate.autoconfigure.observation.ObservationProperties; 16 | import org.springframework.http.HttpStatus; 17 | import org.springframework.http.ResponseEntity; 18 | import org.springframework.security.core.annotation.AuthenticationPrincipal; 19 | import org.springframework.web.bind.annotation.GetMapping; 20 | import org.springframework.web.bind.annotation.PostMapping; 21 | import org.springframework.web.bind.annotation.RequestBody; 22 | import org.springframework.web.bind.annotation.RequestMapping; 23 | import org.springframework.web.bind.annotation.RequestParam; 24 | import org.springframework.web.bind.annotation.RestController; 25 | 26 | /** 27 | * Rest Controller for handling authentication requests. 28 | */ 29 | @RestController 30 | @RequestMapping("/auth") 31 | public class AuthenticationController { 32 | 33 | /** The user service. */ 34 | private UserService userService; 35 | 36 | /** 37 | * Spring injected constructor. 38 | * @param userService 39 | */ 40 | public AuthenticationController(UserService userService) { 41 | this.userService = userService; 42 | } 43 | 44 | /** 45 | * Post Mapping to handle registering users. 46 | * @param registrationBody The registration information. 47 | * @return Response to front end. 48 | */ 49 | @PostMapping("/register") 50 | public ResponseEntity registerUser(@Valid @RequestBody RegistrationBody registrationBody) { 51 | try { 52 | userService.registerUser(registrationBody); 53 | return ResponseEntity.ok().build(); 54 | } catch (UserAlreadyExistsException ex) { 55 | return ResponseEntity.status(HttpStatus.CONFLICT).build(); 56 | } catch (EmailFailureException e) { 57 | return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).build(); 58 | } 59 | } 60 | 61 | /** 62 | * Post Mapping to handle user logins to provide authentication token. 63 | * @param loginBody The login information. 64 | * @return The authentication token if successful. 65 | */ 66 | @PostMapping("/login") 67 | public ResponseEntity loginUser(@Valid @RequestBody LoginBody loginBody) { 68 | String jwt = null; 69 | try { 70 | jwt = userService.loginUser(loginBody); 71 | } catch (UserNotVerifiedException ex) { 72 | LoginResponse response = new LoginResponse(); 73 | response.setSuccess(false); 74 | String reason = "USER_NOT_VERIFIED"; 75 | if (ex.isNewEmailSent()) { 76 | reason += "_EMAIL_RESENT"; 77 | } 78 | response.setFailureReason(reason); 79 | return ResponseEntity.status(HttpStatus.FORBIDDEN).body(response); 80 | } catch (EmailFailureException ex) { 81 | return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).build(); 82 | } 83 | if (jwt == null) { 84 | return ResponseEntity.status(HttpStatus.BAD_REQUEST).build(); 85 | } else { 86 | LoginResponse response = new LoginResponse(); 87 | response.setJwt(jwt); 88 | response.setSuccess(true); 89 | return ResponseEntity.ok(response); 90 | } 91 | } 92 | 93 | /** 94 | * Post mapping to verify the email of an account using the emailed token. 95 | * @param token The token emailed for verification. This is not the same as a 96 | * authentication JWT. 97 | * @return 200 if successful. 409 if failure. 98 | */ 99 | @PostMapping("/verify") 100 | public ResponseEntity verifyEmail(@RequestParam String token) { 101 | if (userService.verifyUser(token)) { 102 | return ResponseEntity.ok().build(); 103 | } else { 104 | return ResponseEntity.status(HttpStatus.CONFLICT).build(); 105 | } 106 | } 107 | 108 | /** 109 | * Gets the profile of the currently logged-in user and returns it. 110 | * @param user The authentication principal object. 111 | * @return The user profile. 112 | */ 113 | @GetMapping("/me") 114 | public LocalUser getLoggedInUserProfile(@AuthenticationPrincipal LocalUser user) { 115 | return user; 116 | } 117 | 118 | /** 119 | * Sends an email to the user with a link to reset their password. 120 | * @param email The email to reset. 121 | * @return Ok if sent, bad request if email not found. 122 | */ 123 | @PostMapping("/forgot") 124 | public ResponseEntity forgotPassword(@RequestParam String email) { 125 | try { 126 | userService.forgotPassword(email); 127 | return ResponseEntity.ok().build(); 128 | } catch (EmailNotFoundException ex) { 129 | return ResponseEntity.status(HttpStatus.BAD_REQUEST).build(); 130 | } catch (EmailFailureException e) { 131 | return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).build(); 132 | } 133 | } 134 | 135 | /** 136 | * Resets the users password with the given token and password. 137 | * @param body The information for the password reset. 138 | * @return Okay if password was set. 139 | */ 140 | @PostMapping("/reset") 141 | public ResponseEntity resetPassword(@Valid @RequestBody PasswordResetBody body) { 142 | userService.resetPassword(body); 143 | return ResponseEntity.ok().build(); 144 | } 145 | 146 | } 147 | -------------------------------------------------------------------------------- /src/main/java/com/youtube/tutorial/ecommercebackend/model/LocalUser.java: -------------------------------------------------------------------------------- 1 | package com.youtube.tutorial.ecommercebackend.model; 2 | 3 | import com.fasterxml.jackson.annotation.JsonIgnore; 4 | import jakarta.persistence.CascadeType; 5 | import jakarta.persistence.Column; 6 | import jakarta.persistence.Entity; 7 | import jakarta.persistence.GeneratedValue; 8 | import jakarta.persistence.GenerationType; 9 | import jakarta.persistence.Id; 10 | import jakarta.persistence.OneToMany; 11 | import jakarta.persistence.OrderBy; 12 | import jakarta.persistence.Table; 13 | import org.springframework.security.core.GrantedAuthority; 14 | import org.springframework.security.core.userdetails.UserDetails; 15 | 16 | import java.util.ArrayList; 17 | import java.util.Collection; 18 | import java.util.List; 19 | 20 | /** 21 | * User for authentication with our website. 22 | */ 23 | @Entity 24 | @Table(name = "local_user") 25 | public class LocalUser implements UserDetails { 26 | 27 | /** Unique id for the user. */ 28 | @Id 29 | @GeneratedValue(strategy = GenerationType.IDENTITY) 30 | @Column(name = "id", nullable = false) 31 | private Long id; 32 | /** The username of the user. */ 33 | @Column(name = "username", nullable = false, unique = true) 34 | private String username; 35 | /** The encrypted password of the user. */ 36 | @JsonIgnore 37 | @Column(name = "password", nullable = false, length = 1000) 38 | private String password; 39 | /** The email of the user. */ 40 | @Column(name = "email", nullable = false, unique = true, length = 320) 41 | private String email; 42 | /** The first name of the user. */ 43 | @Column(name = "first_name", nullable = false) 44 | private String firstName; 45 | /** The last name of the user. */ 46 | @Column(name = "last_name", nullable = false) 47 | private String lastName; 48 | /** The addresses associated with the user. */ 49 | @JsonIgnore 50 | @OneToMany(mappedBy = "user", cascade = CascadeType.REMOVE, orphanRemoval = true) 51 | private List
addresses = new ArrayList<>(); 52 | /** Verification tokens sent to the user. */ 53 | @JsonIgnore 54 | @OneToMany(mappedBy = "user", cascade = CascadeType.ALL, orphanRemoval = true) 55 | @OrderBy("id desc") 56 | private List verificationTokens = new ArrayList<>(); 57 | /** Has the users email been verified? */ 58 | @Column(name = "email_verified", nullable = false) 59 | private Boolean emailVerified = false; 60 | 61 | /** 62 | * Is the email verified? 63 | * @return True if it is, false otherwise. 64 | */ 65 | public Boolean isEmailVerified() { 66 | return emailVerified; 67 | } 68 | 69 | /** 70 | * Sets the email verified state. 71 | * @param emailVerified The verified state. 72 | */ 73 | public void setEmailVerified(Boolean emailVerified) { 74 | this.emailVerified = emailVerified; 75 | } 76 | 77 | /** 78 | * Gets the list of VerificationTokens sent to the user. 79 | * @return The list. 80 | */ 81 | public List getVerificationTokens() { 82 | return verificationTokens; 83 | } 84 | 85 | /** 86 | * Sets the list of VerificationTokens sent to the user. 87 | * @param verificationTokens The list. 88 | */ 89 | public void setVerificationTokens(List verificationTokens) { 90 | this.verificationTokens = verificationTokens; 91 | } 92 | 93 | /** 94 | * Gets the addresses. 95 | * @return The addresses. 96 | */ 97 | public List
getAddresses() { 98 | return addresses; 99 | } 100 | 101 | /** 102 | * Sets the addresses. 103 | * @param addresses The addresses. 104 | */ 105 | public void setAddresses(List
addresses) { 106 | this.addresses = addresses; 107 | } 108 | 109 | /** 110 | * Gets the last name. 111 | * @return The last name. 112 | */ 113 | public String getLastName() { 114 | return lastName; 115 | } 116 | 117 | /** 118 | * Sets the last name. 119 | * @param lastName The last name. 120 | */ 121 | public void setLastName(String lastName) { 122 | this.lastName = lastName; 123 | } 124 | 125 | /** 126 | * Gets the first name. 127 | * @return The first name. 128 | */ 129 | public String getFirstName() { 130 | return firstName; 131 | } 132 | 133 | /** 134 | * Sets the first name. 135 | * @param firstName The first name. 136 | */ 137 | public void setFirstName(String firstName) { 138 | this.firstName = firstName; 139 | } 140 | 141 | /** 142 | * Gets the email. 143 | * @return The email. 144 | */ 145 | public String getEmail() { 146 | return email; 147 | } 148 | 149 | /** 150 | * Sets the email. 151 | * @param email The email. 152 | */ 153 | public void setEmail(String email) { 154 | this.email = email; 155 | } 156 | 157 | /** 158 | * {@inheritDoc} 159 | */ 160 | @JsonIgnore 161 | public Collection getAuthorities() { 162 | return List.of(); 163 | } 164 | 165 | /** 166 | * Gets the encrypted password. 167 | * @return The password. 168 | */ 169 | public String getPassword() { 170 | return password; 171 | } 172 | 173 | /** 174 | * Sets the password, this should be pre-encrypted. 175 | * @param password The password. 176 | */ 177 | public void setPassword(String password) { 178 | this.password = password; 179 | } 180 | 181 | /** 182 | * Gets the username. 183 | * @return The username. 184 | */ 185 | public String getUsername() { 186 | return username; 187 | } 188 | 189 | /** 190 | * {@inheritDoc} 191 | */ 192 | @JsonIgnore 193 | public boolean isAccountNonExpired() { 194 | return true; 195 | } 196 | 197 | /** 198 | * {@inheritDoc} 199 | */ 200 | @JsonIgnore 201 | public boolean isAccountNonLocked() { 202 | return true; 203 | } 204 | 205 | /** 206 | * {@inheritDoc} 207 | */ 208 | @JsonIgnore 209 | public boolean isCredentialsNonExpired() { 210 | return true; 211 | } 212 | 213 | /** 214 | * {@inheritDoc} 215 | */ 216 | @JsonIgnore 217 | public boolean isEnabled() { 218 | return true; 219 | } 220 | 221 | /** 222 | * Sets the username. 223 | * @param username The username. 224 | */ 225 | public void setUsername(String username) { 226 | this.username = username; 227 | } 228 | 229 | /** 230 | * Gets the id. 231 | * @return The id. 232 | */ 233 | public Long getId() { 234 | return id; 235 | } 236 | 237 | /** 238 | * Sets the id. 239 | * @param id The id. 240 | */ 241 | public void setId(Long id) { 242 | this.id = id; 243 | } 244 | 245 | } -------------------------------------------------------------------------------- /src/main/java/com/youtube/tutorial/ecommercebackend/api/security/WebsocketConfiguration.java: -------------------------------------------------------------------------------- 1 | package com.youtube.tutorial.ecommercebackend.api.security; 2 | 3 | import com.youtube.tutorial.ecommercebackend.model.LocalUser; 4 | import com.youtube.tutorial.ecommercebackend.service.UserService; 5 | import org.springframework.context.ApplicationContext; 6 | import org.springframework.context.annotation.Configuration; 7 | import org.springframework.messaging.Message; 8 | import org.springframework.messaging.MessageChannel; 9 | import org.springframework.messaging.simp.SimpMessageType; 10 | import org.springframework.messaging.simp.config.ChannelRegistration; 11 | import org.springframework.messaging.simp.config.MessageBrokerRegistry; 12 | import org.springframework.messaging.support.ChannelInterceptor; 13 | import org.springframework.security.authorization.AuthorizationEventPublisher; 14 | import org.springframework.security.authorization.AuthorizationManager; 15 | import org.springframework.security.authorization.SpringAuthorizationEventPublisher; 16 | import org.springframework.security.core.Authentication; 17 | import org.springframework.security.core.context.SecurityContextHolder; 18 | import org.springframework.security.messaging.access.intercept.AuthorizationChannelInterceptor; 19 | import org.springframework.security.messaging.access.intercept.MessageMatcherDelegatingAuthorizationManager; 20 | import org.springframework.util.AntPathMatcher; 21 | import org.springframework.web.socket.config.annotation.EnableWebSocket; 22 | import org.springframework.web.socket.config.annotation.EnableWebSocketMessageBroker; 23 | import org.springframework.web.socket.config.annotation.StompEndpointRegistry; 24 | import org.springframework.web.socket.config.annotation.WebSocketMessageBrokerConfigurer; 25 | 26 | import java.util.Map; 27 | 28 | /** 29 | * Class to configur spring websockets. 30 | */ 31 | @Configuration 32 | @EnableWebSocket 33 | @EnableWebSocketMessageBroker 34 | public class WebsocketConfiguration 35 | implements WebSocketMessageBrokerConfigurer { 36 | 37 | /** The Application Context. */ 38 | private ApplicationContext context; 39 | /** The JWT Request Filter. */ 40 | private JWTRequestFilter jwtRequestFilter; 41 | /** The User Service. */ 42 | private UserService userService; 43 | /** Matcher instance. */ 44 | private static final AntPathMatcher MATCHER = new AntPathMatcher(); 45 | 46 | /** 47 | * Default constructor for spring injection. 48 | * @param context 49 | * @param jwtRequestFilter 50 | * @param userService 51 | */ 52 | public WebsocketConfiguration(ApplicationContext context, 53 | JWTRequestFilter jwtRequestFilter, 54 | UserService userService) { 55 | this.context = context; 56 | this.jwtRequestFilter = jwtRequestFilter; 57 | this.userService = userService; 58 | } 59 | 60 | /** 61 | * {@inheritDoc} 62 | */ 63 | @Override 64 | public void registerStompEndpoints(StompEndpointRegistry registry) { 65 | registry.addEndpoint("/websocket").setAllowedOriginPatterns("**").withSockJS(); 66 | } 67 | 68 | /** 69 | * {@inheritDoc} 70 | */ 71 | @Override 72 | public void configureMessageBroker(MessageBrokerRegistry registry) { 73 | registry.enableSimpleBroker("/topic"); 74 | registry.setApplicationDestinationPrefixes("/app"); 75 | } 76 | 77 | /** 78 | * Creates an AuthorizationManager for managing authentication required for 79 | * specific channels. 80 | * @return The AuthorizationManager object. 81 | */ 82 | private AuthorizationManager> makeMessageAuthorizationManager() { 83 | MessageMatcherDelegatingAuthorizationManager.Builder messages = 84 | new MessageMatcherDelegatingAuthorizationManager.Builder(); 85 | messages. 86 | simpDestMatchers("/topic/user/**").authenticated() 87 | .anyMessage().permitAll(); 88 | return messages.build(); 89 | } 90 | 91 | /** 92 | * {@inheritDoc} 93 | */ 94 | @Override 95 | public void configureClientInboundChannel(ChannelRegistration registration) { 96 | AuthorizationManager> authorizationManager = 97 | makeMessageAuthorizationManager(); 98 | AuthorizationChannelInterceptor authInterceptor = 99 | new AuthorizationChannelInterceptor(authorizationManager); 100 | AuthorizationEventPublisher publisher = 101 | new SpringAuthorizationEventPublisher(context); 102 | authInterceptor.setAuthorizationEventPublisher(publisher); 103 | registration.interceptors(jwtRequestFilter, authInterceptor, 104 | new RejectClientMessagesOnChannelsChannelInterceptor(), 105 | new DestinationLevelAuthorizationChannelInterceptor()); 106 | } 107 | 108 | /** 109 | * Interceptor for rejecting client messages on specific channels. 110 | */ 111 | private class RejectClientMessagesOnChannelsChannelInterceptor 112 | implements ChannelInterceptor { 113 | 114 | /** Paths that do not allow client messages. */ 115 | private String[] paths = new String[] { 116 | "/topic/user/*/address" 117 | }; 118 | 119 | /** 120 | * {@inheritDoc} 121 | */ 122 | @Override 123 | public Message preSend(Message message, MessageChannel channel) { 124 | if (message.getHeaders().get("simpMessageType").equals(SimpMessageType.MESSAGE)) { 125 | String destination = (String) message.getHeaders().get( 126 | "simpDestination"); 127 | for (String path: paths) { 128 | if (MATCHER.match(path, destination)) 129 | message = null; 130 | } 131 | } 132 | return message; 133 | } 134 | 135 | } 136 | 137 | /** 138 | * Interceptor to apply authorization and permissions onto specific 139 | * channels and path variables. 140 | */ 141 | private class DestinationLevelAuthorizationChannelInterceptor 142 | implements ChannelInterceptor { 143 | 144 | /** 145 | * {@inheritDoc} 146 | */ 147 | @Override 148 | public Message preSend(Message message, MessageChannel channel) { 149 | if (message.getHeaders().get("simpMessageType").equals(SimpMessageType.SUBSCRIBE)) { 150 | String destination = (String) message.getHeaders().get( 151 | "simpDestination"); 152 | String userTopicMatcher = "/topic/user/{userId}/**"; 153 | if (MATCHER.match(userTopicMatcher, destination)) { 154 | Map params = MATCHER.extractUriTemplateVariables( 155 | userTopicMatcher, destination); 156 | try { 157 | Long userId = Long.valueOf(params.get("userId")); 158 | Authentication authentication = 159 | SecurityContextHolder.getContext().getAuthentication(); 160 | if (authentication != null) { 161 | LocalUser user = (LocalUser) authentication.getPrincipal(); 162 | if (!userService.userHasPermissionToUser(user, userId)) { 163 | message = null; 164 | } 165 | } else { 166 | message = null; 167 | } 168 | } catch (NumberFormatException ex) { 169 | message = null; 170 | } 171 | } 172 | } 173 | return message; 174 | } 175 | } 176 | } 177 | -------------------------------------------------------------------------------- /mvnw.cmd: -------------------------------------------------------------------------------- 1 | @REM ---------------------------------------------------------------------------- 2 | @REM Licensed to the Apache Software Foundation (ASF) under one 3 | @REM or more contributor license agreements. See the NOTICE file 4 | @REM distributed with this work for additional information 5 | @REM regarding copyright ownership. The ASF licenses this file 6 | @REM to you under the Apache License, Version 2.0 (the 7 | @REM "License"); you may not use this file except in compliance 8 | @REM with the License. You may obtain a copy of the License at 9 | @REM 10 | @REM https://www.apache.org/licenses/LICENSE-2.0 11 | @REM 12 | @REM Unless required by applicable law or agreed to in writing, 13 | @REM software distributed under the License is distributed on an 14 | @REM "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY 15 | @REM KIND, either express or implied. See the License for the 16 | @REM specific language governing permissions and limitations 17 | @REM under the License. 18 | @REM ---------------------------------------------------------------------------- 19 | 20 | @REM ---------------------------------------------------------------------------- 21 | @REM Maven Start Up Batch script 22 | @REM 23 | @REM Required ENV vars: 24 | @REM JAVA_HOME - location of a JDK home dir 25 | @REM 26 | @REM Optional ENV vars 27 | @REM M2_HOME - location of maven2's installed home dir 28 | @REM MAVEN_BATCH_ECHO - set to 'on' to enable the echoing of the batch commands 29 | @REM MAVEN_BATCH_PAUSE - set to 'on' to wait for a keystroke before ending 30 | @REM MAVEN_OPTS - parameters passed to the Java VM when running Maven 31 | @REM e.g. to debug Maven itself, use 32 | @REM set MAVEN_OPTS=-Xdebug -Xrunjdwp:transport=dt_socket,server=y,suspend=y,address=8000 33 | @REM MAVEN_SKIP_RC - flag to disable loading of mavenrc files 34 | @REM ---------------------------------------------------------------------------- 35 | 36 | @REM Begin all REM lines with '@' in case MAVEN_BATCH_ECHO is 'on' 37 | @echo off 38 | @REM set title of command window 39 | title %0 40 | @REM enable echoing by setting MAVEN_BATCH_ECHO to 'on' 41 | @if "%MAVEN_BATCH_ECHO%" == "on" echo %MAVEN_BATCH_ECHO% 42 | 43 | @REM set %HOME% to equivalent of $HOME 44 | if "%HOME%" == "" (set "HOME=%HOMEDRIVE%%HOMEPATH%") 45 | 46 | @REM Execute a user defined script before this one 47 | if not "%MAVEN_SKIP_RC%" == "" goto skipRcPre 48 | @REM check for pre script, once with legacy .bat ending and once with .cmd ending 49 | if exist "%USERPROFILE%\mavenrc_pre.bat" call "%USERPROFILE%\mavenrc_pre.bat" %* 50 | if exist "%USERPROFILE%\mavenrc_pre.cmd" call "%USERPROFILE%\mavenrc_pre.cmd" %* 51 | :skipRcPre 52 | 53 | @setlocal 54 | 55 | set ERROR_CODE=0 56 | 57 | @REM To isolate internal variables from possible post scripts, we use another setlocal 58 | @setlocal 59 | 60 | @REM ==== START VALIDATION ==== 61 | if not "%JAVA_HOME%" == "" goto OkJHome 62 | 63 | echo. 64 | echo Error: JAVA_HOME not found in your environment. >&2 65 | echo Please set the JAVA_HOME variable in your environment to match the >&2 66 | echo location of your Java installation. >&2 67 | echo. 68 | goto error 69 | 70 | :OkJHome 71 | if exist "%JAVA_HOME%\bin\java.exe" goto init 72 | 73 | echo. 74 | echo Error: JAVA_HOME is set to an invalid directory. >&2 75 | echo JAVA_HOME = "%JAVA_HOME%" >&2 76 | echo Please set the JAVA_HOME variable in your environment to match the >&2 77 | echo location of your Java installation. >&2 78 | echo. 79 | goto error 80 | 81 | @REM ==== END VALIDATION ==== 82 | 83 | :init 84 | 85 | @REM Find the project base dir, i.e. the directory that contains the folder ".mvn". 86 | @REM Fallback to current working directory if not found. 87 | 88 | set MAVEN_PROJECTBASEDIR=%MAVEN_BASEDIR% 89 | IF NOT "%MAVEN_PROJECTBASEDIR%"=="" goto endDetectBaseDir 90 | 91 | set EXEC_DIR=%CD% 92 | set WDIR=%EXEC_DIR% 93 | :findBaseDir 94 | IF EXIST "%WDIR%"\.mvn goto baseDirFound 95 | cd .. 96 | IF "%WDIR%"=="%CD%" goto baseDirNotFound 97 | set WDIR=%CD% 98 | goto findBaseDir 99 | 100 | :baseDirFound 101 | set MAVEN_PROJECTBASEDIR=%WDIR% 102 | cd "%EXEC_DIR%" 103 | goto endDetectBaseDir 104 | 105 | :baseDirNotFound 106 | set MAVEN_PROJECTBASEDIR=%EXEC_DIR% 107 | cd "%EXEC_DIR%" 108 | 109 | :endDetectBaseDir 110 | 111 | IF NOT EXIST "%MAVEN_PROJECTBASEDIR%\.mvn\jvm.config" goto endReadAdditionalConfig 112 | 113 | @setlocal EnableExtensions EnableDelayedExpansion 114 | for /F "usebackq delims=" %%a in ("%MAVEN_PROJECTBASEDIR%\.mvn\jvm.config") do set JVM_CONFIG_MAVEN_PROPS=!JVM_CONFIG_MAVEN_PROPS! %%a 115 | @endlocal & set JVM_CONFIG_MAVEN_PROPS=%JVM_CONFIG_MAVEN_PROPS% 116 | 117 | :endReadAdditionalConfig 118 | 119 | SET MAVEN_JAVA_EXE="%JAVA_HOME%\bin\java.exe" 120 | set WRAPPER_JAR="%MAVEN_PROJECTBASEDIR%\.mvn\wrapper\maven-wrapper.jar" 121 | set WRAPPER_LAUNCHER=org.apache.maven.wrapper.MavenWrapperMain 122 | 123 | set DOWNLOAD_URL="https://repo.maven.apache.org/maven2/org/apache/maven/wrapper/maven-wrapper/3.1.0/maven-wrapper-3.1.0.jar" 124 | 125 | FOR /F "usebackq tokens=1,2 delims==" %%A IN ("%MAVEN_PROJECTBASEDIR%\.mvn\wrapper\maven-wrapper.properties") DO ( 126 | IF "%%A"=="wrapperUrl" SET DOWNLOAD_URL=%%B 127 | ) 128 | 129 | @REM Extension to allow automatically downloading the maven-wrapper.jar from Maven-central 130 | @REM This allows using the maven wrapper in projects that prohibit checking in binary data. 131 | if exist %WRAPPER_JAR% ( 132 | if "%MVNW_VERBOSE%" == "true" ( 133 | echo Found %WRAPPER_JAR% 134 | ) 135 | ) else ( 136 | if not "%MVNW_REPOURL%" == "" ( 137 | SET DOWNLOAD_URL="%MVNW_REPOURL%/org/apache/maven/wrapper/maven-wrapper/3.1.0/maven-wrapper-3.1.0.jar" 138 | ) 139 | if "%MVNW_VERBOSE%" == "true" ( 140 | echo Couldn't find %WRAPPER_JAR%, downloading it ... 141 | echo Downloading from: %DOWNLOAD_URL% 142 | ) 143 | 144 | powershell -Command "&{"^ 145 | "$webclient = new-object System.Net.WebClient;"^ 146 | "if (-not ([string]::IsNullOrEmpty('%MVNW_USERNAME%') -and [string]::IsNullOrEmpty('%MVNW_PASSWORD%'))) {"^ 147 | "$webclient.Credentials = new-object System.Net.NetworkCredential('%MVNW_USERNAME%', '%MVNW_PASSWORD%');"^ 148 | "}"^ 149 | "[Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12; $webclient.DownloadFile('%DOWNLOAD_URL%', '%WRAPPER_JAR%')"^ 150 | "}" 151 | if "%MVNW_VERBOSE%" == "true" ( 152 | echo Finished downloading %WRAPPER_JAR% 153 | ) 154 | ) 155 | @REM End of extension 156 | 157 | @REM Provide a "standardized" way to retrieve the CLI args that will 158 | @REM work with both Windows and non-Windows executions. 159 | set MAVEN_CMD_LINE_ARGS=%* 160 | 161 | %MAVEN_JAVA_EXE% ^ 162 | %JVM_CONFIG_MAVEN_PROPS% ^ 163 | %MAVEN_OPTS% ^ 164 | %MAVEN_DEBUG_OPTS% ^ 165 | -classpath %WRAPPER_JAR% ^ 166 | "-Dmaven.multiModuleProjectDirectory=%MAVEN_PROJECTBASEDIR%" ^ 167 | %WRAPPER_LAUNCHER% %MAVEN_CONFIG% %* 168 | if ERRORLEVEL 1 goto error 169 | goto end 170 | 171 | :error 172 | set ERROR_CODE=1 173 | 174 | :end 175 | @endlocal & set ERROR_CODE=%ERROR_CODE% 176 | 177 | if not "%MAVEN_SKIP_RC%"=="" goto skipRcPost 178 | @REM check for post script, once with legacy .bat ending and once with .cmd ending 179 | if exist "%USERPROFILE%\mavenrc_post.bat" call "%USERPROFILE%\mavenrc_post.bat" 180 | if exist "%USERPROFILE%\mavenrc_post.cmd" call "%USERPROFILE%\mavenrc_post.cmd" 181 | :skipRcPost 182 | 183 | @REM pause the script if MAVEN_BATCH_PAUSE is set to 'on' 184 | if "%MAVEN_BATCH_PAUSE%"=="on" pause 185 | 186 | if "%MAVEN_TERMINATE_CMD%"=="on" exit %ERROR_CODE% 187 | 188 | cmd /C exit /B %ERROR_CODE% 189 | -------------------------------------------------------------------------------- /src/test/java/com/youtube/tutorial/ecommercebackend/service/UserServiceTest.java: -------------------------------------------------------------------------------- 1 | package com.youtube.tutorial.ecommercebackend.service; 2 | 3 | import com.icegreen.greenmail.configuration.GreenMailConfiguration; 4 | import com.icegreen.greenmail.junit5.GreenMailExtension; 5 | import com.icegreen.greenmail.util.ServerSetupTest; 6 | import com.youtube.tutorial.ecommercebackend.api.model.LoginBody; 7 | import com.youtube.tutorial.ecommercebackend.api.model.PasswordResetBody; 8 | import com.youtube.tutorial.ecommercebackend.api.model.RegistrationBody; 9 | import com.youtube.tutorial.ecommercebackend.exception.EmailFailureException; 10 | import com.youtube.tutorial.ecommercebackend.exception.EmailNotFoundException; 11 | import com.youtube.tutorial.ecommercebackend.exception.UserAlreadyExistsException; 12 | import com.youtube.tutorial.ecommercebackend.exception.UserNotVerifiedException; 13 | import com.youtube.tutorial.ecommercebackend.model.LocalUser; 14 | import com.youtube.tutorial.ecommercebackend.model.VerificationToken; 15 | import com.youtube.tutorial.ecommercebackend.model.dao.LocalUserDAO; 16 | import com.youtube.tutorial.ecommercebackend.model.dao.VerificationTokenDAO; 17 | import jakarta.mail.Message; 18 | import jakarta.mail.MessagingException; 19 | import jakarta.transaction.Transactional; 20 | import org.junit.jupiter.api.Assertions; 21 | import org.junit.jupiter.api.Test; 22 | import org.junit.jupiter.api.extension.RegisterExtension; 23 | import org.springframework.beans.factory.annotation.Autowired; 24 | import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc; 25 | import org.springframework.boot.test.context.SpringBootTest; 26 | 27 | import java.util.List; 28 | 29 | /** 30 | * Test class to unit test the UserService class. 31 | */ 32 | @SpringBootTest 33 | @AutoConfigureMockMvc 34 | public class UserServiceTest { 35 | 36 | /** Extension for mocking email sending. */ 37 | @RegisterExtension 38 | private static GreenMailExtension greenMailExtension = new GreenMailExtension(ServerSetupTest.SMTP) 39 | .withConfiguration(GreenMailConfiguration.aConfig().withUser("springboot", "secret")) 40 | .withPerMethodLifecycle(true); 41 | /** The UserService to test. */ 42 | @Autowired 43 | private UserService userService; 44 | /** The JWT Service. */ 45 | @Autowired 46 | private JWTService jwtService; 47 | /** The Local User DAO. */ 48 | @Autowired 49 | private LocalUserDAO localUserDAO; 50 | /** The encryption Service. */ 51 | @Autowired 52 | private EncryptionService encryptionService; 53 | /** The Verification Token DAO. */ 54 | @Autowired 55 | private VerificationTokenDAO verificationTokenDAO; 56 | 57 | /** 58 | * Tests the registration process of the user. 59 | * @throws MessagingException Thrown if the mocked email service fails somehow. 60 | */ 61 | @Test 62 | @Transactional 63 | public void testRegisterUser() throws MessagingException { 64 | RegistrationBody body = new RegistrationBody(); 65 | body.setUsername("UserA"); 66 | body.setEmail("UserServiceTest$testRegisterUser@junit.com"); 67 | body.setFirstName("FirstName"); 68 | body.setLastName("LastName"); 69 | body.setPassword("MySecretPassword123"); 70 | Assertions.assertThrows(UserAlreadyExistsException.class, 71 | () -> userService.registerUser(body), "Username should already be in use."); 72 | body.setUsername("UserServiceTest$testRegisterUser"); 73 | body.setEmail("UserA@junit.com"); 74 | Assertions.assertThrows(UserAlreadyExistsException.class, 75 | () -> userService.registerUser(body), "Email should already be in use."); 76 | body.setEmail("UserServiceTest$testRegisterUser@junit.com"); 77 | Assertions.assertDoesNotThrow(() -> userService.registerUser(body), 78 | "User should register successfully."); 79 | Assertions.assertEquals(body.getEmail(), greenMailExtension.getReceivedMessages()[0] 80 | .getRecipients(Message.RecipientType.TO)[0].toString()); 81 | } 82 | 83 | /** 84 | * Tests the loginUser method. 85 | * @throws UserNotVerifiedException 86 | * @throws EmailFailureException 87 | */ 88 | @Test 89 | @Transactional 90 | public void testLoginUser() throws UserNotVerifiedException, EmailFailureException { 91 | LoginBody body = new LoginBody(); 92 | body.setUsername("UserA-NotExists"); 93 | body.setPassword("PasswordA123-BadPassword"); 94 | Assertions.assertNull(userService.loginUser(body), "The user should not exist."); 95 | body.setUsername("UserA"); 96 | Assertions.assertNull(userService.loginUser(body), "The password should be incorrect."); 97 | body.setPassword("PasswordA123"); 98 | Assertions.assertNotNull(userService.loginUser(body), "The user should login successfully."); 99 | body.setUsername("UserB"); 100 | body.setPassword("PasswordB123"); 101 | try { 102 | userService.loginUser(body); 103 | Assertions.assertTrue(false, "User should not have email verified."); 104 | } catch (UserNotVerifiedException ex) { 105 | Assertions.assertTrue(ex.isNewEmailSent(), "Email verification should be sent."); 106 | Assertions.assertEquals(1, greenMailExtension.getReceivedMessages().length); 107 | } 108 | try { 109 | userService.loginUser(body); 110 | Assertions.assertTrue(false, "User should not have email verified."); 111 | } catch (UserNotVerifiedException ex) { 112 | Assertions.assertFalse(ex.isNewEmailSent(), "Email verification should not be resent."); 113 | Assertions.assertEquals(1, greenMailExtension.getReceivedMessages().length); 114 | } 115 | } 116 | 117 | /** 118 | * Tests the verifyUser method. 119 | * @throws EmailFailureException 120 | */ 121 | @Test 122 | @Transactional 123 | public void testVerifyUser() throws EmailFailureException { 124 | Assertions.assertFalse(userService.verifyUser("Bad Token"), "Token that is bad or does not exist should return false."); 125 | LoginBody body = new LoginBody(); 126 | body.setUsername("UserB"); 127 | body.setPassword("PasswordB123"); 128 | try { 129 | userService.loginUser(body); 130 | Assertions.assertTrue(false, "User should not have email verified."); 131 | } catch (UserNotVerifiedException ex) { 132 | List tokens = verificationTokenDAO.findByUser_IdOrderByIdDesc(2L); 133 | String token = tokens.get(0).getToken(); 134 | Assertions.assertTrue(userService.verifyUser(token), "Token should be valid."); 135 | Assertions.assertNotNull(body, "The user should now be verified."); 136 | } 137 | } 138 | 139 | /** 140 | * Tests the forgotPassword method in the User Service. 141 | * @throws MessagingException 142 | */ 143 | @Test 144 | @Transactional 145 | public void testForgotPassword() throws MessagingException { 146 | Assertions.assertThrows(EmailNotFoundException.class, 147 | () -> userService.forgotPassword("UserNotExist@junit.com")); 148 | Assertions.assertDoesNotThrow(() -> userService.forgotPassword( 149 | "UserA@junit.com"), "Non existing email should be rejected."); 150 | Assertions.assertEquals("UserA@junit.com", 151 | greenMailExtension.getReceivedMessages()[0] 152 | .getRecipients(Message.RecipientType.TO)[0].toString(), "Password " + 153 | "reset email should be sent."); 154 | } 155 | 156 | /** 157 | * Tests the resetPassword method in the User Service. 158 | * @throws MessagingException 159 | */ 160 | public void testResetPassword() { 161 | LocalUser user = localUserDAO.findByUsernameIgnoreCase("UserA").get(); 162 | String token = jwtService.generatePasswordResetJWT(user); 163 | PasswordResetBody body = new PasswordResetBody(); 164 | body.setToken(token); 165 | body.setPassword("Password123456"); 166 | userService.resetPassword(body); 167 | user = localUserDAO.findByUsernameIgnoreCase("UserA").get(); 168 | Assertions.assertTrue(encryptionService.verifyPassword("Password123456", 169 | user.getPassword()), "Password change should be written to DB."); 170 | } 171 | 172 | } 173 | -------------------------------------------------------------------------------- /src/main/java/com/youtube/tutorial/ecommercebackend/service/UserService.java: -------------------------------------------------------------------------------- 1 | package com.youtube.tutorial.ecommercebackend.service; 2 | 3 | import com.youtube.tutorial.ecommercebackend.api.model.LoginBody; 4 | import com.youtube.tutorial.ecommercebackend.api.model.PasswordResetBody; 5 | import com.youtube.tutorial.ecommercebackend.api.model.RegistrationBody; 6 | import com.youtube.tutorial.ecommercebackend.exception.EmailFailureException; 7 | import com.youtube.tutorial.ecommercebackend.exception.EmailNotFoundException; 8 | import com.youtube.tutorial.ecommercebackend.exception.UserAlreadyExistsException; 9 | import com.youtube.tutorial.ecommercebackend.exception.UserNotVerifiedException; 10 | import com.youtube.tutorial.ecommercebackend.model.LocalUser; 11 | import com.youtube.tutorial.ecommercebackend.model.VerificationToken; 12 | import com.youtube.tutorial.ecommercebackend.model.dao.LocalUserDAO; 13 | import com.youtube.tutorial.ecommercebackend.model.dao.VerificationTokenDAO; 14 | import jakarta.transaction.Transactional; 15 | import org.springframework.stereotype.Service; 16 | 17 | import java.sql.Timestamp; 18 | import java.util.List; 19 | import java.util.Optional; 20 | 21 | /** 22 | * Service for handling user actions. 23 | */ 24 | @Service 25 | public class UserService { 26 | 27 | /** The LocalUserDAO. */ 28 | private LocalUserDAO localUserDAO; 29 | /** The VerificationTokenDAO. */ 30 | private VerificationTokenDAO verificationTokenDAO; 31 | /** The encryption service. */ 32 | private EncryptionService encryptionService; 33 | /** The JWT service. */ 34 | private JWTService jwtService; 35 | /** The email service. */ 36 | private EmailService emailService; 37 | 38 | /** 39 | * Constructor injected by spring. 40 | * 41 | * @param localUserDAO 42 | * @param verificationTokenDAO 43 | * @param encryptionService 44 | * @param jwtService 45 | * @param emailService 46 | */ 47 | public UserService(LocalUserDAO localUserDAO, VerificationTokenDAO verificationTokenDAO, EncryptionService encryptionService, 48 | JWTService jwtService, EmailService emailService) { 49 | this.localUserDAO = localUserDAO; 50 | this.verificationTokenDAO = verificationTokenDAO; 51 | this.encryptionService = encryptionService; 52 | this.jwtService = jwtService; 53 | this.emailService = emailService; 54 | } 55 | 56 | /** 57 | * Attempts to register a user given the information provided. 58 | * @param registrationBody The registration information. 59 | * @return The local user that has been written to the database. 60 | * @throws UserAlreadyExistsException Thrown if there is already a user with the given information. 61 | */ 62 | public LocalUser registerUser(RegistrationBody registrationBody) throws UserAlreadyExistsException, EmailFailureException { 63 | if (localUserDAO.findByEmailIgnoreCase(registrationBody.getEmail()).isPresent() 64 | || localUserDAO.findByUsernameIgnoreCase(registrationBody.getUsername()).isPresent()) { 65 | throw new UserAlreadyExistsException(); 66 | } 67 | LocalUser user = new LocalUser(); 68 | user.setEmail(registrationBody.getEmail()); 69 | user.setUsername(registrationBody.getUsername()); 70 | user.setFirstName(registrationBody.getFirstName()); 71 | user.setLastName(registrationBody.getLastName()); 72 | user.setPassword(encryptionService.encryptPassword(registrationBody.getPassword())); 73 | VerificationToken verificationToken = createVerificationToken(user); 74 | emailService.sendVerificationEmail(verificationToken); 75 | return localUserDAO.save(user); 76 | } 77 | 78 | /** 79 | * Creates a VerificationToken object for sending to the user. 80 | * @param user The user the token is being generated for. 81 | * @return The object created. 82 | */ 83 | private VerificationToken createVerificationToken(LocalUser user) { 84 | VerificationToken verificationToken = new VerificationToken(); 85 | verificationToken.setToken(jwtService.generateVerificationJWT(user)); 86 | verificationToken.setCreatedTimestamp(new Timestamp(System.currentTimeMillis())); 87 | verificationToken.setUser(user); 88 | user.getVerificationTokens().add(verificationToken); 89 | return verificationToken; 90 | } 91 | 92 | /** 93 | * Logins in a user and provides an authentication token back. 94 | * @param loginBody The login request. 95 | * @return The authentication token. Null if the request was invalid. 96 | */ 97 | public String loginUser(LoginBody loginBody) throws UserNotVerifiedException, EmailFailureException { 98 | Optional opUser = localUserDAO.findByUsernameIgnoreCase(loginBody.getUsername()); 99 | if (opUser.isPresent()) { 100 | LocalUser user = opUser.get(); 101 | if (encryptionService.verifyPassword(loginBody.getPassword(), user.getPassword())) { 102 | if (user.isEmailVerified()) { 103 | return jwtService.generateJWT(user); 104 | } else { 105 | List verificationTokens = user.getVerificationTokens(); 106 | boolean resend = verificationTokens.size() == 0 || 107 | verificationTokens.get(0).getCreatedTimestamp().before(new Timestamp(System.currentTimeMillis() - (60 * 60 * 1000))); 108 | if (resend) { 109 | VerificationToken verificationToken = createVerificationToken(user); 110 | verificationTokenDAO.save(verificationToken); 111 | emailService.sendVerificationEmail(verificationToken); 112 | } 113 | throw new UserNotVerifiedException(resend); 114 | } 115 | } 116 | } 117 | return null; 118 | } 119 | 120 | /** 121 | * Verifies a user from the given token. 122 | * @param token The token to use to verify a user. 123 | * @return True if it was verified, false if already verified or token invalid. 124 | */ 125 | @Transactional 126 | public boolean verifyUser(String token) { 127 | Optional opToken = verificationTokenDAO.findByToken(token); 128 | if (opToken.isPresent()) { 129 | VerificationToken verificationToken = opToken.get(); 130 | LocalUser user = verificationToken.getUser(); 131 | if (!user.isEmailVerified()) { 132 | user.setEmailVerified(true); 133 | localUserDAO.save(user); 134 | verificationTokenDAO.deleteByUser(user); 135 | return true; 136 | } 137 | } 138 | return false; 139 | } 140 | 141 | /** 142 | * Sends the user a forgot password reset based on the email provided. 143 | * @param email The email to send to. 144 | * @throws EmailNotFoundException Thrown if there is no user with that email. 145 | * @throws EmailFailureException 146 | */ 147 | public void forgotPassword(String email) throws EmailNotFoundException, EmailFailureException { 148 | Optional opUser = localUserDAO.findByEmailIgnoreCase(email); 149 | if (opUser.isPresent()) { 150 | LocalUser user = opUser.get(); 151 | String token = jwtService.generatePasswordResetJWT(user); 152 | emailService.sendPasswordResetEmail(user, token); 153 | } else { 154 | throw new EmailNotFoundException(); 155 | } 156 | } 157 | 158 | /** 159 | * Resets the users password using a given token and email. 160 | * @param body The password reset information. 161 | */ 162 | public void resetPassword(PasswordResetBody body) { 163 | String email = jwtService.getResetPasswordEmail(body.getToken()); 164 | Optional opUser = localUserDAO.findByEmailIgnoreCase(email); 165 | if (opUser.isPresent()) { 166 | LocalUser user = opUser.get(); 167 | user.setPassword(encryptionService.encryptPassword(body.getPassword())); 168 | localUserDAO.save(user); 169 | } 170 | } 171 | 172 | /** 173 | * Method to check if an authenticated user has permission to a user ID. 174 | * @param user The authenticated user. 175 | * @param id The user ID. 176 | * @return True if they have permission, false otherwise. 177 | */ 178 | public boolean userHasPermissionToUser(LocalUser user, Long id) { 179 | return user.getId() == id; 180 | } 181 | 182 | } 183 | -------------------------------------------------------------------------------- /mvnw: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | # ---------------------------------------------------------------------------- 3 | # Licensed to the Apache Software Foundation (ASF) under one 4 | # or more contributor license agreements. See the NOTICE file 5 | # distributed with this work for additional information 6 | # regarding copyright ownership. The ASF licenses this file 7 | # to you under the Apache License, Version 2.0 (the 8 | # "License"); you may not use this file except in compliance 9 | # with the License. You may obtain a copy of the License at 10 | # 11 | # https://www.apache.org/licenses/LICENSE-2.0 12 | # 13 | # Unless required by applicable law or agreed to in writing, 14 | # software distributed under the License is distributed on an 15 | # "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY 16 | # KIND, either express or implied. See the License for the 17 | # specific language governing permissions and limitations 18 | # under the License. 19 | # ---------------------------------------------------------------------------- 20 | 21 | # ---------------------------------------------------------------------------- 22 | # Maven Start Up Batch script 23 | # 24 | # Required ENV vars: 25 | # ------------------ 26 | # JAVA_HOME - location of a JDK home dir 27 | # 28 | # Optional ENV vars 29 | # ----------------- 30 | # M2_HOME - location of maven2's installed home dir 31 | # MAVEN_OPTS - parameters passed to the Java VM when running Maven 32 | # e.g. to debug Maven itself, use 33 | # set MAVEN_OPTS=-Xdebug -Xrunjdwp:transport=dt_socket,server=y,suspend=y,address=8000 34 | # MAVEN_SKIP_RC - flag to disable loading of mavenrc files 35 | # ---------------------------------------------------------------------------- 36 | 37 | if [ -z "$MAVEN_SKIP_RC" ] ; then 38 | 39 | if [ -f /usr/local/etc/mavenrc ] ; then 40 | . /usr/local/etc/mavenrc 41 | fi 42 | 43 | if [ -f /etc/mavenrc ] ; then 44 | . /etc/mavenrc 45 | fi 46 | 47 | if [ -f "$HOME/.mavenrc" ] ; then 48 | . "$HOME/.mavenrc" 49 | fi 50 | 51 | fi 52 | 53 | # OS specific support. $var _must_ be set to either true or false. 54 | cygwin=false; 55 | darwin=false; 56 | mingw=false 57 | case "`uname`" in 58 | CYGWIN*) cygwin=true ;; 59 | MINGW*) mingw=true;; 60 | Darwin*) darwin=true 61 | # Use /usr/libexec/java_home if available, otherwise fall back to /Library/Java/Home 62 | # See https://developer.apple.com/library/mac/qa/qa1170/_index.html 63 | if [ -z "$JAVA_HOME" ]; then 64 | if [ -x "/usr/libexec/java_home" ]; then 65 | export JAVA_HOME="`/usr/libexec/java_home`" 66 | else 67 | export JAVA_HOME="/Library/Java/Home" 68 | fi 69 | fi 70 | ;; 71 | esac 72 | 73 | if [ -z "$JAVA_HOME" ] ; then 74 | if [ -r /etc/gentoo-release ] ; then 75 | JAVA_HOME=`java-config --jre-home` 76 | fi 77 | fi 78 | 79 | if [ -z "$M2_HOME" ] ; then 80 | ## resolve links - $0 may be a link to maven's home 81 | PRG="$0" 82 | 83 | # need this for relative symlinks 84 | while [ -h "$PRG" ] ; do 85 | ls=`ls -ld "$PRG"` 86 | link=`expr "$ls" : '.*-> \(.*\)$'` 87 | if expr "$link" : '/.*' > /dev/null; then 88 | PRG="$link" 89 | else 90 | PRG="`dirname "$PRG"`/$link" 91 | fi 92 | done 93 | 94 | saveddir=`pwd` 95 | 96 | M2_HOME=`dirname "$PRG"`/.. 97 | 98 | # make it fully qualified 99 | M2_HOME=`cd "$M2_HOME" && pwd` 100 | 101 | cd "$saveddir" 102 | # echo Using m2 at $M2_HOME 103 | fi 104 | 105 | # For Cygwin, ensure paths are in UNIX format before anything is touched 106 | if $cygwin ; then 107 | [ -n "$M2_HOME" ] && 108 | M2_HOME=`cygpath --unix "$M2_HOME"` 109 | [ -n "$JAVA_HOME" ] && 110 | JAVA_HOME=`cygpath --unix "$JAVA_HOME"` 111 | [ -n "$CLASSPATH" ] && 112 | CLASSPATH=`cygpath --path --unix "$CLASSPATH"` 113 | fi 114 | 115 | # For Mingw, ensure paths are in UNIX format before anything is touched 116 | if $mingw ; then 117 | [ -n "$M2_HOME" ] && 118 | M2_HOME="`(cd "$M2_HOME"; pwd)`" 119 | [ -n "$JAVA_HOME" ] && 120 | JAVA_HOME="`(cd "$JAVA_HOME"; pwd)`" 121 | fi 122 | 123 | if [ -z "$JAVA_HOME" ]; then 124 | javaExecutable="`which javac`" 125 | if [ -n "$javaExecutable" ] && ! [ "`expr \"$javaExecutable\" : '\([^ ]*\)'`" = "no" ]; then 126 | # readlink(1) is not available as standard on Solaris 10. 127 | readLink=`which readlink` 128 | if [ ! `expr "$readLink" : '\([^ ]*\)'` = "no" ]; then 129 | if $darwin ; then 130 | javaHome="`dirname \"$javaExecutable\"`" 131 | javaExecutable="`cd \"$javaHome\" && pwd -P`/javac" 132 | else 133 | javaExecutable="`readlink -f \"$javaExecutable\"`" 134 | fi 135 | javaHome="`dirname \"$javaExecutable\"`" 136 | javaHome=`expr "$javaHome" : '\(.*\)/bin'` 137 | JAVA_HOME="$javaHome" 138 | export JAVA_HOME 139 | fi 140 | fi 141 | fi 142 | 143 | if [ -z "$JAVACMD" ] ; then 144 | if [ -n "$JAVA_HOME" ] ; then 145 | if [ -x "$JAVA_HOME/jre/sh/java" ] ; then 146 | # IBM's JDK on AIX uses strange locations for the executables 147 | JAVACMD="$JAVA_HOME/jre/sh/java" 148 | else 149 | JAVACMD="$JAVA_HOME/bin/java" 150 | fi 151 | else 152 | JAVACMD="`\\unset -f command; \\command -v java`" 153 | fi 154 | fi 155 | 156 | if [ ! -x "$JAVACMD" ] ; then 157 | echo "Error: JAVA_HOME is not defined correctly." >&2 158 | echo " We cannot execute $JAVACMD" >&2 159 | exit 1 160 | fi 161 | 162 | if [ -z "$JAVA_HOME" ] ; then 163 | echo "Warning: JAVA_HOME environment variable is not set." 164 | fi 165 | 166 | CLASSWORLDS_LAUNCHER=org.codehaus.plexus.classworlds.launcher.Launcher 167 | 168 | # traverses directory structure from process work directory to filesystem root 169 | # first directory with .mvn subdirectory is considered project base directory 170 | find_maven_basedir() { 171 | 172 | if [ -z "$1" ] 173 | then 174 | echo "Path not specified to find_maven_basedir" 175 | return 1 176 | fi 177 | 178 | basedir="$1" 179 | wdir="$1" 180 | while [ "$wdir" != '/' ] ; do 181 | if [ -d "$wdir"/.mvn ] ; then 182 | basedir=$wdir 183 | break 184 | fi 185 | # workaround for JBEAP-8937 (on Solaris 10/Sparc) 186 | if [ -d "${wdir}" ]; then 187 | wdir=`cd "$wdir/.."; pwd` 188 | fi 189 | # end of workaround 190 | done 191 | echo "${basedir}" 192 | } 193 | 194 | # concatenates all lines of a file 195 | concat_lines() { 196 | if [ -f "$1" ]; then 197 | echo "$(tr -s '\n' ' ' < "$1")" 198 | fi 199 | } 200 | 201 | BASE_DIR=`find_maven_basedir "$(pwd)"` 202 | if [ -z "$BASE_DIR" ]; then 203 | exit 1; 204 | fi 205 | 206 | ########################################################################################## 207 | # Extension to allow automatically downloading the maven-wrapper.jar from Maven-central 208 | # This allows using the maven wrapper in projects that prohibit checking in binary data. 209 | ########################################################################################## 210 | if [ -r "$BASE_DIR/.mvn/wrapper/maven-wrapper.jar" ]; then 211 | if [ "$MVNW_VERBOSE" = true ]; then 212 | echo "Found .mvn/wrapper/maven-wrapper.jar" 213 | fi 214 | else 215 | if [ "$MVNW_VERBOSE" = true ]; then 216 | echo "Couldn't find .mvn/wrapper/maven-wrapper.jar, downloading it ..." 217 | fi 218 | if [ -n "$MVNW_REPOURL" ]; then 219 | jarUrl="$MVNW_REPOURL/org/apache/maven/wrapper/maven-wrapper/3.1.0/maven-wrapper-3.1.0.jar" 220 | else 221 | jarUrl="https://repo.maven.apache.org/maven2/org/apache/maven/wrapper/maven-wrapper/3.1.0/maven-wrapper-3.1.0.jar" 222 | fi 223 | while IFS="=" read key value; do 224 | case "$key" in (wrapperUrl) jarUrl="$value"; break ;; 225 | esac 226 | done < "$BASE_DIR/.mvn/wrapper/maven-wrapper.properties" 227 | if [ "$MVNW_VERBOSE" = true ]; then 228 | echo "Downloading from: $jarUrl" 229 | fi 230 | wrapperJarPath="$BASE_DIR/.mvn/wrapper/maven-wrapper.jar" 231 | if $cygwin; then 232 | wrapperJarPath=`cygpath --path --windows "$wrapperJarPath"` 233 | fi 234 | 235 | if command -v wget > /dev/null; then 236 | if [ "$MVNW_VERBOSE" = true ]; then 237 | echo "Found wget ... using wget" 238 | fi 239 | if [ -z "$MVNW_USERNAME" ] || [ -z "$MVNW_PASSWORD" ]; then 240 | wget "$jarUrl" -O "$wrapperJarPath" || rm -f "$wrapperJarPath" 241 | else 242 | wget --http-user=$MVNW_USERNAME --http-password=$MVNW_PASSWORD "$jarUrl" -O "$wrapperJarPath" || rm -f "$wrapperJarPath" 243 | fi 244 | elif command -v curl > /dev/null; then 245 | if [ "$MVNW_VERBOSE" = true ]; then 246 | echo "Found curl ... using curl" 247 | fi 248 | if [ -z "$MVNW_USERNAME" ] || [ -z "$MVNW_PASSWORD" ]; then 249 | curl -o "$wrapperJarPath" "$jarUrl" -f 250 | else 251 | curl --user $MVNW_USERNAME:$MVNW_PASSWORD -o "$wrapperJarPath" "$jarUrl" -f 252 | fi 253 | 254 | else 255 | if [ "$MVNW_VERBOSE" = true ]; then 256 | echo "Falling back to using Java to download" 257 | fi 258 | javaClass="$BASE_DIR/.mvn/wrapper/MavenWrapperDownloader.java" 259 | # For Cygwin, switch paths to Windows format before running javac 260 | if $cygwin; then 261 | javaClass=`cygpath --path --windows "$javaClass"` 262 | fi 263 | if [ -e "$javaClass" ]; then 264 | if [ ! -e "$BASE_DIR/.mvn/wrapper/MavenWrapperDownloader.class" ]; then 265 | if [ "$MVNW_VERBOSE" = true ]; then 266 | echo " - Compiling MavenWrapperDownloader.java ..." 267 | fi 268 | # Compiling the Java class 269 | ("$JAVA_HOME/bin/javac" "$javaClass") 270 | fi 271 | if [ -e "$BASE_DIR/.mvn/wrapper/MavenWrapperDownloader.class" ]; then 272 | # Running the downloader 273 | if [ "$MVNW_VERBOSE" = true ]; then 274 | echo " - Running MavenWrapperDownloader.java ..." 275 | fi 276 | ("$JAVA_HOME/bin/java" -cp .mvn/wrapper MavenWrapperDownloader "$MAVEN_PROJECTBASEDIR") 277 | fi 278 | fi 279 | fi 280 | fi 281 | ########################################################################################## 282 | # End of extension 283 | ########################################################################################## 284 | 285 | export MAVEN_PROJECTBASEDIR=${MAVEN_BASEDIR:-"$BASE_DIR"} 286 | if [ "$MVNW_VERBOSE" = true ]; then 287 | echo $MAVEN_PROJECTBASEDIR 288 | fi 289 | MAVEN_OPTS="$(concat_lines "$MAVEN_PROJECTBASEDIR/.mvn/jvm.config") $MAVEN_OPTS" 290 | 291 | # For Cygwin, switch paths to Windows format before running java 292 | if $cygwin; then 293 | [ -n "$M2_HOME" ] && 294 | M2_HOME=`cygpath --path --windows "$M2_HOME"` 295 | [ -n "$JAVA_HOME" ] && 296 | JAVA_HOME=`cygpath --path --windows "$JAVA_HOME"` 297 | [ -n "$CLASSPATH" ] && 298 | CLASSPATH=`cygpath --path --windows "$CLASSPATH"` 299 | [ -n "$MAVEN_PROJECTBASEDIR" ] && 300 | MAVEN_PROJECTBASEDIR=`cygpath --path --windows "$MAVEN_PROJECTBASEDIR"` 301 | fi 302 | 303 | # Provide a "standardized" way to retrieve the CLI args that will 304 | # work with both Windows and non-Windows executions. 305 | MAVEN_CMD_LINE_ARGS="$MAVEN_CONFIG $@" 306 | export MAVEN_CMD_LINE_ARGS 307 | 308 | WRAPPER_LAUNCHER=org.apache.maven.wrapper.MavenWrapperMain 309 | 310 | exec "$JAVACMD" \ 311 | $MAVEN_OPTS \ 312 | $MAVEN_DEBUG_OPTS \ 313 | -classpath "$MAVEN_PROJECTBASEDIR/.mvn/wrapper/maven-wrapper.jar" \ 314 | "-Dmaven.home=${M2_HOME}" \ 315 | "-Dmaven.multiModuleProjectDirectory=${MAVEN_PROJECTBASEDIR}" \ 316 | ${WRAPPER_LAUNCHER} $MAVEN_CONFIG "$@" 317 | --------------------------------------------------------------------------------