├── .dockerignore ├── .gitignore ├── AccountService ├── build.gradle └── src │ ├── main │ ├── java │ │ └── com │ │ │ └── printezisn │ │ │ └── moviestore │ │ │ └── accountservice │ │ │ ├── AccountServiceApplication.java │ │ │ └── account │ │ │ ├── controllers │ │ │ └── AccountController.java │ │ │ ├── entities │ │ │ └── Account.java │ │ │ ├── exceptions │ │ │ ├── AccountNotFoundException.java │ │ │ ├── AccountPersistenceException.java │ │ │ └── AccountValidationException.java │ │ │ ├── mappers │ │ │ └── AccountMapper.java │ │ │ ├── repositories │ │ │ └── AccountRepository.java │ │ │ └── services │ │ │ ├── AccountService.java │ │ │ └── AccountServiceImpl.java │ └── resources │ │ ├── application-test.properties │ │ ├── application.properties │ │ ├── i18n │ │ └── messages │ │ │ └── messages.properties │ │ └── logback-spring.xml │ └── test │ └── java │ └── com │ └── printezisn │ └── moviestore │ └── accountservice │ ├── account │ ├── controllers │ │ └── AccountControllerTest.java │ └── services │ │ └── AccountServiceImplTest.java │ └── integ │ └── AccountIntegrationTest.java ├── Common ├── .gitignore ├── build.gradle └── src │ ├── main │ └── java │ │ └── com │ │ └── printezisn │ │ └── moviestore │ │ └── common │ │ ├── AppUtils.java │ │ ├── CommonApplication.java │ │ ├── RegexLibrary.java │ │ ├── RetryHandler.java │ │ ├── configuration │ │ └── api │ │ │ ├── LocaleConfiguration.java │ │ │ └── SwaggerConfiguration.java │ │ ├── dto │ │ ├── account │ │ │ ├── AccountDto.java │ │ │ └── AuthDto.java │ │ └── movie │ │ │ └── MovieDto.java │ │ ├── mappers │ │ ├── InstantMapper.java │ │ └── UUIDMapper.java │ │ └── models │ │ ├── Notification.java │ │ ├── PagedResult.java │ │ ├── Result.java │ │ ├── account │ │ └── AccountResultModel.java │ │ └── movie │ │ ├── MoviePagedResultModel.java │ │ └── MovieResultModel.java │ └── test │ └── java │ └── com │ └── printezisn │ └── moviestore │ └── common │ └── AppUtilsTest.java ├── LICENSE ├── MovieService ├── build.gradle └── src │ ├── main │ ├── java │ │ └── com │ │ │ └── printezisn │ │ │ └── moviestore │ │ │ └── movieservice │ │ │ ├── MovieServiceApplication.java │ │ │ ├── configuration │ │ │ └── ElasticsearchConfiguration.java │ │ │ └── movie │ │ │ ├── controllers │ │ │ └── MovieController.java │ │ │ ├── entities │ │ │ ├── Movie.java │ │ │ ├── MovieIndex.java │ │ │ └── MovieLike.java │ │ │ ├── exceptions │ │ │ ├── MovieConditionalException.java │ │ │ ├── MovieNotFoundException.java │ │ │ ├── MoviePersistenceException.java │ │ │ └── MovieValidationException.java │ │ │ ├── helpers │ │ │ └── MovieIndexHelper.java │ │ │ ├── mappers │ │ │ └── MovieMapper.java │ │ │ ├── repositories │ │ │ ├── CustomMovieIndexRepository.java │ │ │ ├── CustomMovieIndexRepositoryImpl.java │ │ │ ├── CustomMovieRepository.java │ │ │ ├── CustomMovieRepositoryImpl.java │ │ │ ├── MovieIndexRepository.java │ │ │ ├── MovieLikeRepository.java │ │ │ └── MovieRepository.java │ │ │ └── services │ │ │ ├── MovieService.java │ │ │ └── MovieServiceImpl.java │ └── resources │ │ ├── application-test.properties │ │ ├── application.properties │ │ ├── i18n │ │ └── messages │ │ │ └── messages.properties │ │ └── logback-spring.xml │ └── test │ └── java │ └── com │ └── printezisn │ └── moviestore │ └── movieservice │ ├── integ │ └── MovieIntegrationTest.java │ └── movie │ ├── controllers │ └── MovieControllerTest.java │ ├── helpers │ └── MovieIndexHelperTest.java │ └── services │ └── MovieServiceImplTest.java ├── README.md ├── Website ├── build.gradle └── src │ ├── main │ ├── java │ │ └── com │ │ │ └── printezisn │ │ │ └── moviestore │ │ │ └── website │ │ │ ├── Constants.java │ │ │ ├── WebsiteApplication.java │ │ │ ├── account │ │ │ ├── controllers │ │ │ │ ├── AccountController.java │ │ │ │ └── AuthController.java │ │ │ ├── exceptions │ │ │ │ ├── AccountAuthenticationException.java │ │ │ │ ├── AccountNotValidatedException.java │ │ │ │ └── AccountPersistenceException.java │ │ │ ├── models │ │ │ │ ├── AuthenticatedUser.java │ │ │ │ └── ChangePasswordModel.java │ │ │ └── services │ │ │ │ ├── AccountService.java │ │ │ │ └── AccountServiceImpl.java │ │ │ ├── configuration │ │ │ ├── AccountAuthenticationProvider.java │ │ │ ├── GeneralConfiguration.java │ │ │ ├── SecurityConfiguration.java │ │ │ ├── properties │ │ │ │ └── ServiceProperties.java │ │ │ └── rest │ │ │ │ └── DefaultResponseErrorHandler.java │ │ │ └── movie │ │ │ ├── controllers │ │ │ └── MovieController.java │ │ │ ├── exceptions │ │ │ ├── MovieConditionalException.java │ │ │ ├── MovieNotFoundException.java │ │ │ └── MoviePersistenceException.java │ │ │ ├── models │ │ │ └── LikeStatus.java │ │ │ └── services │ │ │ ├── MovieService.java │ │ │ └── MovieServiceImpl.java │ └── resources │ │ ├── application-test.properties │ │ ├── application.properties │ │ ├── i18n │ │ ├── labels │ │ │ └── labels.properties │ │ ├── messages │ │ │ └── messages.properties │ │ └── pages │ │ │ └── pages.properties │ │ ├── logback-spring.xml │ │ ├── static │ │ ├── app │ │ │ ├── app.js │ │ │ ├── js │ │ │ │ ├── likeStatus.js │ │ │ │ ├── polyfills.js │ │ │ │ ├── toastr.js │ │ │ │ └── validate.js │ │ │ └── sass │ │ │ │ ├── _colors.scss │ │ │ │ ├── app.scss │ │ │ │ └── toastr-overrides.scss │ │ ├── package-lock.json │ │ ├── package.json │ │ ├── webpack.common.js │ │ ├── webpack.dev.js │ │ └── webpack.prod.js │ │ └── templates │ │ ├── account │ │ ├── changePassword.html │ │ └── register.html │ │ ├── auth │ │ └── login.html │ │ ├── fragments │ │ └── likeStatus.html │ │ ├── layout-template.html │ │ └── movie │ │ ├── create.html │ │ ├── delete.html │ │ ├── details.html │ │ ├── edit.html │ │ └── index.html │ └── test │ └── java │ └── com │ └── printezisn │ └── moviestore │ └── website │ ├── account │ ├── controllers │ │ ├── AccountControllerTest.java │ │ └── AuthControllerTest.java │ └── services │ │ └── AccountServiceImplTest.java │ ├── configuration │ ├── AccountAuthenticationProviderTest.java │ └── rest │ │ └── DefaultResponseErrorHandlerTest.java │ ├── integ │ ├── AccountIntegrationTest.java │ ├── AuthIntegrationTest.java │ └── MovieIntegrationTest.java │ └── movie │ ├── controllers │ └── MovieControllerTest.java │ └── services │ └── MovieServiceImplTest.java ├── docker-compose.yml ├── dockerfile.accountservice ├── dockerfile.movieservice ├── dockerfile.website ├── gradle └── wrapper │ ├── gradle-wrapper.jar │ └── gradle-wrapper.properties ├── gradlew ├── gradlew.bat ├── nginx ├── nginx.conf ├── ssl.key └── ssl.pem ├── settings.gradle └── wait-for-it.sh /.dockerignore: -------------------------------------------------------------------------------- 1 | .project 2 | .gradle 3 | .settings 4 | data 5 | 6 | */.gradle 7 | */!gradle/wrapper/gradle-wrapper.jar 8 | */bin 9 | */logs 10 | */.apt_generated 11 | */.classpath 12 | */.factorypath 13 | */.project 14 | */.settings 15 | */.springBeans 16 | */.idea 17 | */*.iws 18 | */*.iml 19 | */*.ipr 20 | */nbproject/private/ 21 | */build/ 22 | */nbbuild/ 23 | */dist/ 24 | */nbdist/ 25 | */.nb-gradle/ 26 | 27 | */src/main/resources/static/dist 28 | */src/main/resources/static/node_modules 29 | */src/main/resources/static/webpack-stats.json 30 | */src/main/resources/templates/layout.html 31 | */src/main/resources/templates/layout.html.gz 32 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .project 2 | .gradle 3 | .settings 4 | data 5 | 6 | */.gradle 7 | */!gradle/wrapper/gradle-wrapper.jar 8 | */bin 9 | */logs 10 | */.apt_generated 11 | */.classpath 12 | */.factorypath 13 | */.project 14 | */.settings 15 | */.springBeans 16 | */.idea 17 | */*.iws 18 | */*.iml 19 | */*.ipr 20 | */nbproject/private/ 21 | */build/ 22 | */nbbuild/ 23 | */dist/ 24 | */nbdist/ 25 | */.nb-gradle/ 26 | 27 | */src/main/resources/static/dist 28 | */src/main/resources/static/node_modules 29 | */src/main/resources/static/webpack-stats.json 30 | */src/main/resources/templates/layout.html 31 | */src/main/resources/templates/layout.html.gz 32 | -------------------------------------------------------------------------------- /AccountService/build.gradle: -------------------------------------------------------------------------------- 1 | buildscript { 2 | ext { 3 | springBootVersion = '2.1.0.RELEASE' 4 | } 5 | repositories { 6 | mavenCentral() 7 | } 8 | dependencies { 9 | classpath("org.springframework.boot:spring-boot-gradle-plugin:${springBootVersion}") 10 | } 11 | } 12 | 13 | plugins { 14 | id 'io.franzbecker.gradle-lombok' version '1.14' 15 | } 16 | 17 | apply plugin: 'java' 18 | apply plugin: 'eclipse' 19 | apply plugin: 'org.springframework.boot' 20 | apply plugin: 'io.spring.dependency-management' 21 | 22 | group = 'com.printezisn.moviestore' 23 | version = '1.0.0' 24 | sourceCompatibility = 11 25 | 26 | repositories { 27 | mavenCentral() 28 | } 29 | 30 | lombok { 31 | version = '1.18.4' 32 | sha256 = '' 33 | } 34 | 35 | dependencies { 36 | implementation('javax.xml.bind:jaxb-api:2.3.0') 37 | implementation('org.springframework.boot:spring-boot-starter-data-mongodb') 38 | implementation('org.springframework.boot:spring-boot-starter-web') 39 | implementation('org.springframework.security:spring-security-crypto') 40 | implementation('org.mapstruct:mapstruct-jdk8:1.2.0.Final') 41 | 42 | implementation project(':Common') 43 | 44 | annotationProcessor('org.projectlombok:lombok:1.18.4') 45 | annotationProcessor('org.mapstruct:mapstruct-processor:1.2.0.Final') 46 | 47 | runtimeOnly('org.springframework.boot:spring-boot-devtools') 48 | 49 | testImplementation('org.springframework.boot:spring-boot-starter-test') 50 | } 51 | 52 | test { 53 | useJUnit { 54 | exclude '**/*IntegrationTest.class' 55 | } 56 | } 57 | 58 | task integTest(type: Test) { 59 | useJUnit { 60 | include '**/*IntegrationTest.class' 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /AccountService/src/main/java/com/printezisn/moviestore/accountservice/AccountServiceApplication.java: -------------------------------------------------------------------------------- 1 | package com.printezisn.moviestore.accountservice; 2 | 3 | import org.springframework.boot.SpringApplication; 4 | import org.springframework.boot.autoconfigure.SpringBootApplication; 5 | 6 | /** 7 | * The main class of the application 8 | */ 9 | @SpringBootApplication(scanBasePackages = { "com.printezisn.moviestore.accountservice", 10 | "com.printezisn.moviestore.common" }) 11 | public class AccountServiceApplication { 12 | 13 | /** 14 | * The main method of the application 15 | * 16 | * @param args 17 | * The command-line arguments 18 | */ 19 | public static void main(final String[] args) { 20 | SpringApplication.run(AccountServiceApplication.class, args); 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /AccountService/src/main/java/com/printezisn/moviestore/accountservice/account/controllers/AccountController.java: -------------------------------------------------------------------------------- 1 | package com.printezisn.moviestore.accountservice.account.controllers; 2 | 3 | import java.util.Arrays; 4 | import java.util.List; 5 | import java.util.Optional; 6 | 7 | import javax.validation.Valid; 8 | 9 | import org.springframework.http.ResponseEntity; 10 | import org.springframework.validation.BindingResult; 11 | import org.springframework.web.bind.annotation.GetMapping; 12 | import org.springframework.web.bind.annotation.PathVariable; 13 | import org.springframework.web.bind.annotation.PostMapping; 14 | import org.springframework.web.bind.annotation.RequestBody; 15 | import org.springframework.web.bind.annotation.RestController; 16 | 17 | import com.printezisn.moviestore.accountservice.account.exceptions.AccountNotFoundException; 18 | import com.printezisn.moviestore.accountservice.account.exceptions.AccountValidationException; 19 | import com.printezisn.moviestore.accountservice.account.services.AccountService; 20 | import com.printezisn.moviestore.common.AppUtils; 21 | import com.printezisn.moviestore.common.dto.account.AccountDto; 22 | import com.printezisn.moviestore.common.dto.account.AuthDto; 23 | import com.printezisn.moviestore.common.models.account.AccountResultModel; 24 | 25 | import lombok.RequiredArgsConstructor; 26 | 27 | /** 28 | * The controller that serves accounts 29 | */ 30 | @RestController 31 | @RequiredArgsConstructor 32 | public class AccountController { 33 | 34 | private final AccountService accountService; 35 | private final AppUtils appUtils; 36 | 37 | /** 38 | * Returns an account 39 | * 40 | * @param username 41 | * The username of the account 42 | * @return The account 43 | */ 44 | @GetMapping(path = "/account/get/{username}") 45 | public ResponseEntity getAccount(@PathVariable("username") final String username) { 46 | final Optional account = accountService.getAccount(username); 47 | return account.isPresent() 48 | ? ResponseEntity.ok(account.get()) 49 | : ResponseEntity.notFound().build(); 50 | } 51 | 52 | /** 53 | * Authenticates an account based on a given username and password 54 | * 55 | * @param authDto 56 | * The authentication model 57 | * @param bindingResult 58 | * The model binding result 59 | * @return The result of the operation 60 | */ 61 | @PostMapping(path = "/account/auth") 62 | public ResponseEntity authenticate(@Valid @RequestBody final AuthDto authDto, 63 | final BindingResult bindingResult) { 64 | 65 | final List errors = appUtils.getModelErrors(bindingResult); 66 | if (!errors.isEmpty()) { 67 | return ResponseEntity.badRequest().body( 68 | AccountResultModel.builder().errors(errors).build()); 69 | } 70 | 71 | final Optional account = accountService.getAccount(authDto.getUsername(), authDto.getPassword()); 72 | 73 | return account.isPresent() 74 | ? ResponseEntity.ok( 75 | AccountResultModel.builder().result(account.get()).build()) 76 | : ResponseEntity.badRequest().body( 77 | AccountResultModel.builder().errors( 78 | appUtils.getMessages("message.account.usernameOrPasswordInvalid")).build()); 79 | } 80 | 81 | /** 82 | * Creates a new account 83 | * 84 | * @param account 85 | * The details of the account to create 86 | * @param bindingResult 87 | * The model binding result 88 | * @return The result of the operation 89 | */ 90 | @PostMapping(path = "/account/new") 91 | public ResponseEntity createAccount(@Valid @RequestBody final AccountDto account, 92 | final BindingResult bindingResult) { 93 | 94 | final List errors = appUtils.getModelErrors(bindingResult, "id"); 95 | if (!errors.isEmpty()) { 96 | return ResponseEntity.badRequest().body( 97 | AccountResultModel.builder().errors(errors).build()); 98 | } 99 | 100 | try { 101 | final AccountDto createdAccount = accountService.createAccount(account); 102 | final AccountResultModel result = AccountResultModel.builder().result(createdAccount).build(); 103 | 104 | return ResponseEntity.ok(result); 105 | } 106 | catch (final AccountValidationException ex) { 107 | final AccountResultModel result = AccountResultModel.builder() 108 | .errors(Arrays.asList(ex.getMessage())) 109 | .build(); 110 | 111 | return ResponseEntity.badRequest().body(result); 112 | } 113 | } 114 | 115 | /** 116 | * Updates an account 117 | * 118 | * @param account 119 | * The details of the account to create 120 | * @param bindingResult 121 | * The model binding result 122 | * @return The result of the operation 123 | */ 124 | @PostMapping(path = "/account/update") 125 | public ResponseEntity updateAccount(@Valid @RequestBody final AccountDto account, 126 | final BindingResult bindingResult) { 127 | 128 | final List errors = appUtils.getModelErrors(bindingResult, "username", "emailAddress"); 129 | if (!errors.isEmpty()) { 130 | return ResponseEntity.badRequest().body( 131 | AccountResultModel.builder().errors(errors).build()); 132 | } 133 | 134 | try { 135 | final AccountDto updatedAccount = accountService.updateAccount(account); 136 | final AccountResultModel result = AccountResultModel.builder().result(updatedAccount).build(); 137 | 138 | return ResponseEntity.ok(result); 139 | } 140 | catch (final AccountNotFoundException ex) { 141 | return ResponseEntity.notFound().build(); 142 | } 143 | } 144 | 145 | /** 146 | * Deletes an account 147 | * 148 | * @param username 149 | * The username of the account 150 | * @return The result of the operation 151 | */ 152 | @GetMapping(path = "/account/delete/{id}") 153 | public ResponseEntity deleteAccount(@PathVariable("id") final String username) { 154 | accountService.deleteAccount(username); 155 | 156 | return ResponseEntity.ok().build(); 157 | } 158 | } 159 | -------------------------------------------------------------------------------- /AccountService/src/main/java/com/printezisn/moviestore/accountservice/account/entities/Account.java: -------------------------------------------------------------------------------- 1 | package com.printezisn.moviestore.accountservice.account.entities; 2 | 3 | import org.springframework.data.annotation.Id; 4 | import org.springframework.data.mongodb.core.index.Indexed; 5 | import org.springframework.data.mongodb.core.mapping.Document; 6 | 7 | import lombok.Data; 8 | 9 | /** 10 | * The account entity 11 | */ 12 | @Data 13 | @Document(collection = "accounts") 14 | public class Account { 15 | @Id 16 | private String username; 17 | 18 | @Indexed(unique = true) 19 | private String emailAddress; 20 | 21 | private String password; 22 | 23 | private String passwordSalt; 24 | 25 | private long creationTimestamp; 26 | 27 | private long updateTimestamp; 28 | } 29 | -------------------------------------------------------------------------------- /AccountService/src/main/java/com/printezisn/moviestore/accountservice/account/exceptions/AccountNotFoundException.java: -------------------------------------------------------------------------------- 1 | package com.printezisn.moviestore.accountservice.account.exceptions; 2 | 3 | /** 4 | * Exception class indicating that an account was not found 5 | */ 6 | @SuppressWarnings("serial") 7 | public class AccountNotFoundException extends Exception { 8 | 9 | /** 10 | * The constructor 11 | */ 12 | public AccountNotFoundException() { 13 | super("The account was not found."); 14 | } 15 | 16 | /** 17 | * The constructor 18 | * 19 | * @param message 20 | * The exception message 21 | */ 22 | public AccountNotFoundException(final String message) { 23 | super(message); 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /AccountService/src/main/java/com/printezisn/moviestore/accountservice/account/exceptions/AccountPersistenceException.java: -------------------------------------------------------------------------------- 1 | package com.printezisn.moviestore.accountservice.account.exceptions; 2 | 3 | /** 4 | * Exception class related to persistence errors for the account entity 5 | */ 6 | @SuppressWarnings("serial") 7 | public class AccountPersistenceException extends RuntimeException { 8 | 9 | /** 10 | * The constructor 11 | * 12 | * @param message 13 | * The exception message 14 | * @param cause 15 | * The inner exception 16 | */ 17 | public AccountPersistenceException(final String message, final Throwable cause) { 18 | super(message, cause); 19 | } 20 | } -------------------------------------------------------------------------------- /AccountService/src/main/java/com/printezisn/moviestore/accountservice/account/exceptions/AccountValidationException.java: -------------------------------------------------------------------------------- 1 | package com.printezisn.moviestore.accountservice.account.exceptions; 2 | 3 | /** 4 | * Exception class indicating validation error for an account 5 | */ 6 | @SuppressWarnings("serial") 7 | public class AccountValidationException extends Exception { 8 | 9 | /** 10 | * The constructor 11 | * 12 | * @param message 13 | * The error message 14 | */ 15 | public AccountValidationException(final String message) { 16 | super(message); 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /AccountService/src/main/java/com/printezisn/moviestore/accountservice/account/mappers/AccountMapper.java: -------------------------------------------------------------------------------- 1 | package com.printezisn.moviestore.accountservice.account.mappers; 2 | 3 | import org.mapstruct.Mapper; 4 | import org.mapstruct.Mapping; 5 | import org.mapstruct.Mappings; 6 | 7 | import com.printezisn.moviestore.accountservice.account.entities.Account; 8 | import com.printezisn.moviestore.common.dto.account.AccountDto; 9 | import com.printezisn.moviestore.common.mappers.InstantMapper; 10 | import com.printezisn.moviestore.common.mappers.UUIDMapper; 11 | 12 | /** 13 | * Mapper class for Account objects 14 | */ 15 | @Mapper(componentModel = "spring", uses = { UUIDMapper.class, InstantMapper.class }) 16 | public interface AccountMapper { 17 | 18 | /** 19 | * Converts an Account object to AccountDto 20 | * 21 | * @param account 22 | * The Account object 23 | * @return The converted AccountDto object 24 | */ 25 | @Mappings({ 26 | @Mapping(source = "password", target = "password", ignore = true), 27 | @Mapping(source = "passwordSalt", target = "passwordSalt", ignore = true) 28 | }) 29 | AccountDto accountToAccountDto(final Account account); 30 | 31 | /** 32 | * Converts an AccountDto object to Account 33 | * 34 | * @param accountDto 35 | * The AccountDto object 36 | * @return The converted Account object 37 | */ 38 | Account accountDtoToAccount(final AccountDto accountDto); 39 | } 40 | -------------------------------------------------------------------------------- /AccountService/src/main/java/com/printezisn/moviestore/accountservice/account/repositories/AccountRepository.java: -------------------------------------------------------------------------------- 1 | package com.printezisn.moviestore.accountservice.account.repositories; 2 | 3 | import java.util.Optional; 4 | 5 | import org.springframework.data.mongodb.repository.MongoRepository; 6 | import org.springframework.stereotype.Repository; 7 | 8 | import com.printezisn.moviestore.accountservice.account.entities.Account; 9 | 10 | /** 11 | * The repository layer for the accounts 12 | */ 13 | @Repository 14 | public interface AccountRepository extends MongoRepository { 15 | 16 | /** 17 | * Searches for an account based on its email address 18 | * 19 | * @param emailAddress 20 | * The email address of the account 21 | * @return The account 22 | */ 23 | Optional findByEmailAddress(final String emailAddress); 24 | } 25 | -------------------------------------------------------------------------------- /AccountService/src/main/java/com/printezisn/moviestore/accountservice/account/services/AccountService.java: -------------------------------------------------------------------------------- 1 | package com.printezisn.moviestore.accountservice.account.services; 2 | 3 | import java.util.Optional; 4 | 5 | import com.printezisn.moviestore.accountservice.account.exceptions.AccountNotFoundException; 6 | import com.printezisn.moviestore.accountservice.account.exceptions.AccountValidationException; 7 | import com.printezisn.moviestore.common.dto.account.AccountDto; 8 | 9 | /** 10 | * The service layer for accounts 11 | */ 12 | public interface AccountService { 13 | 14 | /** 15 | * Returns an account 16 | * 17 | * @param username 18 | * The username of the account 19 | * @return The account 20 | */ 21 | Optional getAccount(final String username); 22 | 23 | /** 24 | * Returns an account 25 | * 26 | * @param username 27 | * The username of the account 28 | * @param password 29 | * The password of the account 30 | * @return The account 31 | */ 32 | Optional getAccount(final String username, final String password); 33 | 34 | /** 35 | * Creates a new account 36 | * 37 | * @param accountDto 38 | * The model of the account 39 | * @return The model of the new account 40 | * @throws AccountValidationException 41 | * Validation exception 42 | */ 43 | AccountDto createAccount(final AccountDto accountDto) throws AccountValidationException; 44 | 45 | /** 46 | * Updates an account 47 | * 48 | * @param accountDto 49 | * THe model of the account 50 | * @return The model of the updated account 51 | * @throws AccountNotFoundException 52 | * Exception indicating that the account was not found 53 | */ 54 | AccountDto updateAccount(final AccountDto accountDto) throws AccountNotFoundException; 55 | 56 | /** 57 | * Deletes an account 58 | * 59 | * @param id 60 | * The username of the account 61 | * @return True if the operation is successful, otherwise false 62 | */ 63 | void deleteAccount(final String username); 64 | } 65 | -------------------------------------------------------------------------------- /AccountService/src/main/java/com/printezisn/moviestore/accountservice/account/services/AccountServiceImpl.java: -------------------------------------------------------------------------------- 1 | package com.printezisn.moviestore.accountservice.account.services; 2 | 3 | import java.time.Instant; 4 | import java.util.Optional; 5 | 6 | import org.springframework.dao.DuplicateKeyException; 7 | import org.springframework.security.crypto.bcrypt.BCrypt; 8 | import org.springframework.stereotype.Service; 9 | 10 | import com.printezisn.moviestore.accountservice.account.entities.Account; 11 | import com.printezisn.moviestore.accountservice.account.exceptions.AccountNotFoundException; 12 | import com.printezisn.moviestore.accountservice.account.exceptions.AccountPersistenceException; 13 | import com.printezisn.moviestore.accountservice.account.exceptions.AccountValidationException; 14 | import com.printezisn.moviestore.accountservice.account.mappers.AccountMapper; 15 | import com.printezisn.moviestore.accountservice.account.repositories.AccountRepository; 16 | import com.printezisn.moviestore.common.AppUtils; 17 | import com.printezisn.moviestore.common.dto.account.AccountDto; 18 | 19 | import lombok.RequiredArgsConstructor; 20 | import lombok.extern.slf4j.Slf4j; 21 | 22 | /** 23 | * The implementation of the service layer for accounts 24 | */ 25 | @Service 26 | @Slf4j 27 | @RequiredArgsConstructor 28 | public class AccountServiceImpl implements AccountService { 29 | 30 | private final AccountRepository accountRepository; 31 | private final AccountMapper accountMapper; 32 | private final AppUtils appUtils; 33 | 34 | /** 35 | * {@inheritDoc} 36 | */ 37 | @Override 38 | public Optional getAccount(final String username) { 39 | try { 40 | final Optional account = accountRepository.findById(username); 41 | 42 | return account.isPresent() 43 | ? Optional.of(accountMapper.accountToAccountDto(account.get())) 44 | : Optional.empty(); 45 | } 46 | catch (final Exception ex) { 47 | final String errorMessage = String.format("An error occured while reading account %s: %s", username, 48 | ex.getMessage()); 49 | 50 | log.error(errorMessage, ex); 51 | throw new AccountPersistenceException(errorMessage, ex); 52 | } 53 | } 54 | 55 | /** 56 | * {@inheritDoc} 57 | */ 58 | @Override 59 | public Optional getAccount(final String username, final String password) { 60 | try { 61 | final Optional account = accountRepository.findById(username); 62 | if (!account.isPresent()) { 63 | return Optional.empty(); 64 | } 65 | 66 | final String hashedPassword = BCrypt.hashpw(password, account.get().getPasswordSalt()); 67 | 68 | return account.get().getPassword().equals(hashedPassword) 69 | ? Optional.of(accountMapper.accountToAccountDto(account.get())) 70 | : Optional.empty(); 71 | } 72 | catch (final Exception ex) { 73 | final String errorMessage = String.format("An error occured while reading account %s: %s", username, 74 | ex.getMessage()); 75 | 76 | log.error(errorMessage, ex); 77 | throw new AccountPersistenceException(errorMessage, ex); 78 | } 79 | } 80 | 81 | /** 82 | * {@inheritDoc} 83 | */ 84 | @Override 85 | public AccountDto createAccount(final AccountDto accountDto) throws AccountValidationException { 86 | // The username and email address must be unique 87 | if (accountRepository.findById(accountDto.getUsername()).isPresent()) { 88 | throw new AccountValidationException(appUtils.getMessage("message.account.usernameExists")); 89 | } 90 | if (accountRepository.findByEmailAddress(accountDto.getEmailAddress()).isPresent()) { 91 | throw new AccountValidationException(appUtils.getMessage("message.account.emailAddressExists")); 92 | } 93 | 94 | accountDto.setPasswordSalt(BCrypt.gensalt()); 95 | accountDto.setPassword(BCrypt.hashpw(accountDto.getPassword(), accountDto.getPasswordSalt())); 96 | accountDto.setCreationTimestamp(Instant.now()); 97 | accountDto.setUpdateTimestamp(Instant.now()); 98 | 99 | final Account account = accountMapper.accountDtoToAccount(accountDto); 100 | 101 | try { 102 | accountRepository.save(account); 103 | } 104 | catch (final DuplicateKeyException ex) { 105 | throw new AccountValidationException(appUtils.getMessage("message.account.usernameOrEmailAddressExists")); 106 | } 107 | catch (final Exception ex) { 108 | final String errorMessage = String.format("An error occured while creating a new account: %s", 109 | ex.getMessage()); 110 | 111 | log.error(errorMessage, ex); 112 | throw new AccountPersistenceException(errorMessage, ex); 113 | } 114 | 115 | return accountMapper.accountToAccountDto(account); 116 | } 117 | 118 | /** 119 | * {@inheritDoc} 120 | */ 121 | @Override 122 | public AccountDto updateAccount(final AccountDto accountDto) throws AccountNotFoundException { 123 | final Account account = accountRepository.findById(accountDto.getUsername()).orElse(null); 124 | if (account == null) { 125 | throw new AccountNotFoundException(); 126 | } 127 | 128 | account.setPasswordSalt(BCrypt.gensalt()); 129 | account.setPassword(BCrypt.hashpw(accountDto.getPassword(), account.getPasswordSalt())); 130 | account.setUpdateTimestamp(Instant.now().toEpochMilli()); 131 | 132 | try { 133 | accountRepository.save(account); 134 | } 135 | catch (final Exception ex) { 136 | final String errorMessage = String.format("An error occured while updating account %s: %s", 137 | account.getUsername(), ex.getMessage()); 138 | 139 | log.error(errorMessage, ex); 140 | throw new AccountPersistenceException(errorMessage, ex); 141 | } 142 | 143 | return accountMapper.accountToAccountDto(account); 144 | } 145 | 146 | /** 147 | * {@inheritDoc} 148 | */ 149 | @Override 150 | public void deleteAccount(final String username) { 151 | try { 152 | accountRepository.deleteById(username); 153 | } 154 | catch (final Exception ex) { 155 | final String errorMessage = String.format("An error occured while deleting account %s: %s", username, 156 | ex.getMessage()); 157 | 158 | log.error(errorMessage, ex); 159 | throw new AccountPersistenceException(errorMessage, ex); 160 | } 161 | } 162 | } 163 | -------------------------------------------------------------------------------- /AccountService/src/main/resources/application-test.properties: -------------------------------------------------------------------------------- 1 | server.port=8100 2 | 3 | spring.data.mongodb.database=moviestore_test -------------------------------------------------------------------------------- /AccountService/src/main/resources/application.properties: -------------------------------------------------------------------------------- 1 | server.port=8000 2 | 3 | spring.data.mongodb.host=localhost 4 | spring.data.mongodb.port=27017 5 | spring.data.mongodb.database=moviestore 6 | spring.data.mongodb.password=1234 -------------------------------------------------------------------------------- /AccountService/src/main/resources/i18n/messages/messages.properties: -------------------------------------------------------------------------------- 1 | message.account.usernameExists=The username is already used. 2 | message.account.emailAddressExists=The email address is already used. 3 | message.account.usernameOrEmailAddressExists=The username or email address is already used. 4 | message.account.accountNotFound=The account was not found. 5 | message.account.usernameOrPasswordInvalid=The username or password is invalid. 6 | message.account.idRequired=The id is required. 7 | 8 | message.account.error.idRequired=The id is required. 9 | message.account.error.usernameRequired=The username is required. 10 | message.account.error.usernameMaxLength=The username may have 50 characters maximum. 11 | message.account.error.usernameFormat=The username can contains only letters, digits, dashes (-) and underscores (_). 12 | message.account.error.emailAddressRequired=The email address is required. 13 | message.account.error.emailAddressMaxLength=The email address may have 250 characters maximum. 14 | message.account.error.emailAddressFormat=The email address has invalid format. 15 | message.account.error.passwordRequired=The password is required. 16 | message.account.error.passwordMaxLength=The password may have 250 characters maximum. 17 | message.account.error.passwordFormat=The password must contain at least 8 characters, one lower case and one upper case character, one digit and one special character (!._, etc). -------------------------------------------------------------------------------- /AccountService/src/main/resources/logback-spring.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 8 | 9 | 10 | %black(%d{ISO8601}) %highlight(%-5level) 11 | [%blue(%t)] %yellow(%C{1.}): %msg%n%throwable 12 | 13 | 14 | 15 | 16 | 18 | ${LOGS}/application.log 19 | 21 | %d %p %C{1.} [%t] %m%n 22 | 23 | 24 | 26 | 27 | ${LOGS}/archived/%d{yyyy-MM}/%d{dd}/%d{HH}/application-%d{yyyy-MM-dd-HH}.%i.log 28 | 29 | 31 | 10MB 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 45 | 46 | 47 | 48 | -------------------------------------------------------------------------------- /Common/.gitignore: -------------------------------------------------------------------------------- 1 | .gradle 2 | /build/ 3 | !gradle/wrapper/gradle-wrapper.jar 4 | bin 5 | 6 | ### STS ### 7 | .apt_generated 8 | .classpath 9 | .factorypath 10 | .project 11 | .settings 12 | .springBeans 13 | .sts4-cache 14 | 15 | ### IntelliJ IDEA ### 16 | .idea 17 | *.iws 18 | *.iml 19 | *.ipr 20 | /out/ 21 | 22 | ### NetBeans ### 23 | /nbproject/private/ 24 | /build/ 25 | /nbbuild/ 26 | /dist/ 27 | /nbdist/ 28 | /.nb-gradle/ 29 | -------------------------------------------------------------------------------- /Common/build.gradle: -------------------------------------------------------------------------------- 1 | buildscript { 2 | repositories { 3 | mavenCentral() 4 | } 5 | } 6 | 7 | plugins { 8 | id 'io.franzbecker.gradle-lombok' version '1.11' 9 | id "io.spring.dependency-management" version "1.0.4.RELEASE" 10 | } 11 | 12 | ext { 13 | springBootVersion = '2.1.0.RELEASE' 14 | } 15 | 16 | apply plugin: 'java' 17 | apply plugin: 'eclipse' 18 | 19 | group = 'com.printezisn.moviestore' 20 | version = '1.0.0' 21 | sourceCompatibility = 11 22 | 23 | repositories { 24 | mavenCentral() 25 | } 26 | 27 | dependencies { 28 | implementation('org.springframework.boot:spring-boot-starter-web') 29 | implementation('io.springfox:springfox-swagger2:2.8.0') 30 | implementation('io.springfox:springfox-swagger-ui:2.8.0') 31 | 32 | annotationProcessor('org.projectlombok:lombok:1.18.4') 33 | 34 | testImplementation('org.springframework.boot:spring-boot-starter-test') 35 | } 36 | 37 | dependencyManagement { 38 | imports { 39 | mavenBom("org.springframework.boot:spring-boot-dependencies:${springBootVersion}") 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /Common/src/main/java/com/printezisn/moviestore/common/CommonApplication.java: -------------------------------------------------------------------------------- 1 | package com.printezisn.moviestore.common; 2 | 3 | /** 4 | * The main class of the common library 5 | */ 6 | public class CommonApplication { 7 | 8 | /** 9 | * The main method of the common library 10 | * 11 | * @param args 12 | * The command-line arguments 13 | */ 14 | public static void main(final String[] args) { 15 | 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /Common/src/main/java/com/printezisn/moviestore/common/RegexLibrary.java: -------------------------------------------------------------------------------- 1 | package com.printezisn.moviestore.common; 2 | 3 | /** 4 | * The class that contains common regular expressions 5 | */ 6 | public class RegexLibrary { 7 | 8 | /** 9 | * Regular expression for usernames 10 | */ 11 | public static final String USERNAME_REGEX = "^[A-Za-z0-9_\\-]+$"; 12 | 13 | /** 14 | * Regular expression for passwords 15 | */ 16 | public static final String PASSWORD_REGEX = "^(?=.*[a-z])(?=.*[A-Z])(?=.*\\d)(?=.*[$@$!%*?&])[A-Za-z\\d$@$!%*?&]{8,}"; 17 | } 18 | -------------------------------------------------------------------------------- /Common/src/main/java/com/printezisn/moviestore/common/RetryHandler.java: -------------------------------------------------------------------------------- 1 | package com.printezisn.moviestore.common; 2 | 3 | import java.util.Random; 4 | import java.util.concurrent.Callable; 5 | import java.util.function.Function; 6 | 7 | import lombok.AllArgsConstructor; 8 | import lombok.Builder; 9 | import lombok.NoArgsConstructor; 10 | 11 | /** 12 | * Retry handler for operations 13 | */ 14 | @Builder 15 | @AllArgsConstructor 16 | @NoArgsConstructor 17 | public class RetryHandler { 18 | 19 | @Builder.Default 20 | private int maxRetries = 5; 21 | 22 | @Builder.Default 23 | private int delay = 1000; 24 | 25 | @Builder.Default 26 | private boolean useExponentialBackOff = false; 27 | 28 | @Builder.Default 29 | private int jitter = 0; 30 | 31 | /** 32 | * Runs an operation and retries if an exception is thrown 33 | * 34 | * @param operation 35 | * The operation to run 36 | * @param condition 37 | * The condition that indicates whether an exception is retriable or 38 | * not 39 | * @return The result of the operation 40 | * @throws Exception 41 | * Exception thrown by the operation 42 | */ 43 | public T run(final Callable operation, final Function condition) throws Exception { 44 | int retry = 0; 45 | final Random random = new Random(); 46 | 47 | while (true) { 48 | try { 49 | return operation.call(); 50 | } 51 | catch (final Exception | AssertionError ex) { 52 | retry++; 53 | if (!condition.apply(ex) || retry >= maxRetries) { 54 | throw ex; 55 | } 56 | 57 | final double totalBackOff = useExponentialBackOff ? (Math.pow(2, retry - 1) * delay) : delay; 58 | final long totalDelay = (long) totalBackOff + random.nextInt(jitter); 59 | 60 | Thread.sleep(totalDelay); 61 | } 62 | } 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /Common/src/main/java/com/printezisn/moviestore/common/configuration/api/LocaleConfiguration.java: -------------------------------------------------------------------------------- 1 | package com.printezisn.moviestore.common.configuration.api; 2 | 3 | import java.util.Locale; 4 | 5 | import org.springframework.context.annotation.Bean; 6 | import org.springframework.context.annotation.Configuration; 7 | import org.springframework.context.support.ReloadableResourceBundleMessageSource; 8 | import org.springframework.web.servlet.LocaleResolver; 9 | import org.springframework.web.servlet.config.annotation.InterceptorRegistry; 10 | import org.springframework.web.servlet.config.annotation.WebMvcConfigurer; 11 | import org.springframework.web.servlet.i18n.CookieLocaleResolver; 12 | import org.springframework.web.servlet.i18n.LocaleChangeInterceptor; 13 | 14 | /** 15 | * Contains configuration regarding localization, for REST APIs 16 | */ 17 | @Configuration 18 | public class LocaleConfiguration implements WebMvcConfigurer { 19 | 20 | /** 21 | * The LocaleChangeInterceptor bean 22 | * 23 | * @return The LocaleChangeInterceptor bean 24 | */ 25 | @Bean 26 | public LocaleChangeInterceptor localeChangeInterceptor() { 27 | final LocaleChangeInterceptor localeChangeInterceptor = new LocaleChangeInterceptor(); 28 | localeChangeInterceptor.setParamName("lang"); 29 | 30 | return localeChangeInterceptor; 31 | } 32 | 33 | /** 34 | * The LocaleResolver bean 35 | * 36 | * @return The LocaleResolver bean 37 | */ 38 | @Bean 39 | public LocaleResolver localeResolver() { 40 | final CookieLocaleResolver localeResolver = new CookieLocaleResolver(); 41 | localeResolver.setDefaultLocale(Locale.US); 42 | 43 | return localeResolver; 44 | } 45 | 46 | /** 47 | * The ReloadableResourceBundleMessageSource bean 48 | * 49 | * @return The ReloadableResourceBundleMessageSource bean 50 | */ 51 | @Bean 52 | public ReloadableResourceBundleMessageSource messageSource() { 53 | final ReloadableResourceBundleMessageSource source = new ReloadableResourceBundleMessageSource(); 54 | 55 | source.setBasenames( 56 | "classpath:i18n/messages/messages", 57 | "classpath:i18n/pages/pages", 58 | "classpath:i18n/labels/labels"); 59 | source.setCacheSeconds(0); 60 | source.setDefaultEncoding("UTF-8"); 61 | 62 | return source; 63 | } 64 | 65 | /** 66 | * {@inheritDoc} 67 | */ 68 | @Override 69 | public void addInterceptors(final InterceptorRegistry registry) { 70 | registry.addInterceptor(localeChangeInterceptor()); 71 | } 72 | } 73 | -------------------------------------------------------------------------------- /Common/src/main/java/com/printezisn/moviestore/common/configuration/api/SwaggerConfiguration.java: -------------------------------------------------------------------------------- 1 | package com.printezisn.moviestore.common.configuration.api; 2 | 3 | import org.springframework.context.annotation.Bean; 4 | import org.springframework.context.annotation.Configuration; 5 | 6 | import springfox.documentation.builders.PathSelectors; 7 | import springfox.documentation.builders.RequestHandlerSelectors; 8 | import springfox.documentation.spi.DocumentationType; 9 | import springfox.documentation.spring.web.plugins.Docket; 10 | import springfox.documentation.swagger2.annotations.EnableSwagger2; 11 | 12 | /** 13 | * Contains the configuration for Swagger 14 | */ 15 | @Configuration 16 | @EnableSwagger2 17 | public class SwaggerConfiguration { 18 | 19 | /** 20 | * The bean for the Swagger API 21 | * 22 | * @return The bean 23 | */ 24 | @Bean 25 | public Docket api() { 26 | return new Docket(DocumentationType.SWAGGER_2) 27 | .select() 28 | .apis(RequestHandlerSelectors.any()) 29 | .paths(PathSelectors.any()) 30 | .build(); 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /Common/src/main/java/com/printezisn/moviestore/common/dto/account/AccountDto.java: -------------------------------------------------------------------------------- 1 | package com.printezisn.moviestore.common.dto.account; 2 | 3 | import java.time.Instant; 4 | 5 | import javax.validation.constraints.Email; 6 | import javax.validation.constraints.NotEmpty; 7 | import javax.validation.constraints.Pattern; 8 | import javax.validation.constraints.Size; 9 | 10 | import com.printezisn.moviestore.common.RegexLibrary; 11 | 12 | import lombok.Data; 13 | 14 | /** 15 | * The data transfer object for accounts 16 | */ 17 | @Data 18 | public class AccountDto { 19 | 20 | @NotEmpty(message = "message.account.error.usernameRequired") 21 | @Size(max = 50, message = "message.account.error.usernameMaxLength") 22 | @Pattern(regexp = RegexLibrary.USERNAME_REGEX, message = "message.account.error.usernameFormat") 23 | private String username; 24 | 25 | @NotEmpty(message = "message.account.error.emailAddressRequired") 26 | @Size(max = 250, message = "message.account.error.emailAddressMaxLength") 27 | @Email(message = "message.account.error.emailAddressFormat") 28 | private String emailAddress; 29 | 30 | @NotEmpty(message = "message.account.error.passwordRequired") 31 | @Size(max = 250, message = "message.account.error.passwordMaxLength") 32 | @Pattern(regexp = RegexLibrary.PASSWORD_REGEX, message = "message.account.error.passwordFormat") 33 | private String password; 34 | 35 | private String passwordSalt; 36 | 37 | private Instant creationTimestamp; 38 | 39 | private Instant updateTimestamp; 40 | } 41 | -------------------------------------------------------------------------------- /Common/src/main/java/com/printezisn/moviestore/common/dto/account/AuthDto.java: -------------------------------------------------------------------------------- 1 | package com.printezisn.moviestore.common.dto.account; 2 | 3 | import javax.validation.constraints.NotEmpty; 4 | 5 | import lombok.Data; 6 | 7 | /** 8 | * The data transfer object for authenticating accounts 9 | */ 10 | @Data 11 | public class AuthDto { 12 | 13 | @NotEmpty(message = "message.account.error.usernameRequired") 14 | private String username; 15 | 16 | @NotEmpty(message = "message.account.error.passwordRequired") 17 | private String password; 18 | 19 | private boolean rememberMe; 20 | } 21 | -------------------------------------------------------------------------------- /Common/src/main/java/com/printezisn/moviestore/common/dto/movie/MovieDto.java: -------------------------------------------------------------------------------- 1 | package com.printezisn.moviestore.common.dto.movie; 2 | 3 | import java.time.Instant; 4 | import java.util.UUID; 5 | 6 | import javax.validation.constraints.Max; 7 | import javax.validation.constraints.Min; 8 | import javax.validation.constraints.NotEmpty; 9 | import javax.validation.constraints.NotNull; 10 | import javax.validation.constraints.Size; 11 | 12 | import lombok.Data; 13 | 14 | /** 15 | * The data transfer object for the Movie entity 16 | */ 17 | @Data 18 | public class MovieDto { 19 | 20 | @NotNull(message = "message.movie.error.idRequired") 21 | private UUID id; 22 | 23 | @NotEmpty(message = "message.movie.error.titleRequired") 24 | @Size(max = 250, message = "message.movie.error.titleMaxLength") 25 | private String title; 26 | 27 | @NotEmpty(message = "message.movie.error.descriptionRequired") 28 | private String description; 29 | 30 | @NotNull(message = "message.movie.error.ratingRequired") 31 | @Max(value = 10, message = "message.movie.error.ratingMaxValue") 32 | @Min(value = 0, message = "message.movie.error.ratingMinValue") 33 | private Double rating; 34 | 35 | @NotNull(message = "message.movie.error.releaseYearRequired") 36 | private Integer releaseYear; 37 | 38 | private int totalLikes; 39 | 40 | private Instant creationTimestamp; 41 | 42 | private Instant updateTimestamp; 43 | 44 | @NotNull(message = "message.movie.error.creatorRequired") 45 | private String creator; 46 | } 47 | -------------------------------------------------------------------------------- /Common/src/main/java/com/printezisn/moviestore/common/mappers/InstantMapper.java: -------------------------------------------------------------------------------- 1 | package com.printezisn.moviestore.common.mappers; 2 | 3 | import java.time.Instant; 4 | 5 | import org.springframework.stereotype.Component; 6 | 7 | /** 8 | * Mapper class for Instant objects 9 | */ 10 | @Component 11 | public class InstantMapper { 12 | 13 | /** 14 | * Converts an Instant object to long 15 | * 16 | * @param instant 17 | * The Instant object 18 | * @return The converted long 19 | */ 20 | public long toLong(final Instant instant) { 21 | return (instant != null) ? instant.toEpochMilli() : 0; 22 | } 23 | 24 | /** 25 | * Converts a long object to Instant 26 | * 27 | * @param epochMilli 28 | * The long object 29 | * @return The converted Instant 30 | */ 31 | public Instant toInstant(final long epochMilli) { 32 | return Instant.ofEpochMilli(epochMilli); 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /Common/src/main/java/com/printezisn/moviestore/common/mappers/UUIDMapper.java: -------------------------------------------------------------------------------- 1 | package com.printezisn.moviestore.common.mappers; 2 | 3 | import java.util.UUID; 4 | 5 | import org.springframework.stereotype.Component; 6 | 7 | /** 8 | * Mapper class for UUID objects 9 | */ 10 | @Component 11 | public class UUIDMapper { 12 | 13 | /** 14 | * Converts a UUID object to string 15 | * 16 | * @param uuid 17 | * The UUID object 18 | * @return The converted string 19 | */ 20 | public String toString(final UUID uuid) { 21 | return (uuid != null) ? uuid.toString() : null; 22 | } 23 | 24 | /** 25 | * Converts a string object to UUID 26 | * 27 | * @param str 28 | * The string object 29 | * @return The converted UUID 30 | */ 31 | public UUID toUUID(final String str) { 32 | return (str != null) ? UUID.fromString(str) : null; 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /Common/src/main/java/com/printezisn/moviestore/common/models/Notification.java: -------------------------------------------------------------------------------- 1 | package com.printezisn.moviestore.common.models; 2 | 3 | import lombok.AllArgsConstructor; 4 | import lombok.Data; 5 | import lombok.Getter; 6 | import lombok.RequiredArgsConstructor; 7 | 8 | /** 9 | * Model used for sending notifications to the application front-end 10 | */ 11 | @RequiredArgsConstructor 12 | @AllArgsConstructor 13 | @Data 14 | public class Notification { 15 | 16 | /** 17 | * The available notification types 18 | */ 19 | @RequiredArgsConstructor 20 | @Getter 21 | public static enum NotificationType { 22 | INFO("info"), 23 | SUCCESS("success"), 24 | WARNING("warning"), 25 | ERROR("error"); 26 | 27 | private final String name; 28 | } 29 | 30 | private final NotificationType type; 31 | private String title; 32 | private final String message; 33 | } 34 | -------------------------------------------------------------------------------- /Common/src/main/java/com/printezisn/moviestore/common/models/PagedResult.java: -------------------------------------------------------------------------------- 1 | package com.printezisn.moviestore.common.models; 2 | 3 | import java.util.List; 4 | 5 | /** 6 | * Model that holds paged entries 7 | * 8 | * @param 9 | * The type of entries 10 | */ 11 | public interface PagedResult { 12 | 13 | /** 14 | * Returns the entries of the result 15 | * 16 | * @return The list of entries 17 | */ 18 | List getEntries(); 19 | 20 | /** 21 | * Returns the current page of the results 22 | * 23 | * @return The current page 24 | */ 25 | int getPageNumber(); 26 | 27 | /** 28 | * Returns the total number of available pages 29 | * 30 | * @return The total number of available pages 31 | */ 32 | int getTotalPages(); 33 | 34 | /** 35 | * Returns the field name used to sort the results 36 | * 37 | * @return The sorting field name 38 | */ 39 | String getSortField(); 40 | 41 | /** 42 | * Indicates if the sorting is ascending or descending 43 | * 44 | * @return True if the sorting is ascending, otherwise false 45 | */ 46 | boolean isAscending(); 47 | } 48 | -------------------------------------------------------------------------------- /Common/src/main/java/com/printezisn/moviestore/common/models/Result.java: -------------------------------------------------------------------------------- 1 | package com.printezisn.moviestore.common.models; 2 | 3 | import java.util.List; 4 | 5 | /** 6 | * Model that holds information about the result of an operation 7 | * 8 | * @param 9 | * The type of the result 10 | */ 11 | public interface Result { 12 | 13 | /** 14 | * Returns the result of the operation 15 | * 16 | * @return The result 17 | */ 18 | T getResult(); 19 | 20 | /** 21 | * Returns the errors associated with the operation 22 | * 23 | * @return The list of errors 24 | */ 25 | List getErrors(); 26 | 27 | /** 28 | * Indicates if the operation has errors 29 | * 30 | * @return True if the operation has errors, otherwise false 31 | */ 32 | default boolean hasErrors() { 33 | return !getErrors().isEmpty(); 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /Common/src/main/java/com/printezisn/moviestore/common/models/account/AccountResultModel.java: -------------------------------------------------------------------------------- 1 | package com.printezisn.moviestore.common.models.account; 2 | 3 | import java.util.LinkedList; 4 | import java.util.List; 5 | 6 | import com.printezisn.moviestore.common.dto.account.AccountDto; 7 | import com.printezisn.moviestore.common.models.Result; 8 | 9 | import lombok.AllArgsConstructor; 10 | import lombok.Builder; 11 | import lombok.Data; 12 | import lombok.NoArgsConstructor; 13 | 14 | /** 15 | * Class that holds the result of an account service call 16 | */ 17 | @Data 18 | @Builder 19 | @NoArgsConstructor 20 | @AllArgsConstructor 21 | public class AccountResultModel implements Result { 22 | 23 | private AccountDto result; 24 | 25 | @Builder.Default 26 | private List errors = new LinkedList<>(); 27 | } 28 | -------------------------------------------------------------------------------- /Common/src/main/java/com/printezisn/moviestore/common/models/movie/MoviePagedResultModel.java: -------------------------------------------------------------------------------- 1 | package com.printezisn.moviestore.common.models.movie; 2 | 3 | import java.util.LinkedList; 4 | import java.util.List; 5 | 6 | import com.printezisn.moviestore.common.dto.movie.MovieDto; 7 | import com.printezisn.moviestore.common.models.PagedResult; 8 | 9 | import lombok.AllArgsConstructor; 10 | import lombok.Builder; 11 | import lombok.Data; 12 | import lombok.NoArgsConstructor; 13 | 14 | /** 15 | * Class that holds the paged result of a movie service call 16 | */ 17 | @Data 18 | @Builder 19 | @NoArgsConstructor 20 | @AllArgsConstructor 21 | public class MoviePagedResultModel implements PagedResult { 22 | 23 | @Builder.Default 24 | private List entries = new LinkedList(); 25 | 26 | private int pageNumber; 27 | 28 | private int totalPages; 29 | 30 | private String sortField; 31 | 32 | private boolean isAscending; 33 | } 34 | -------------------------------------------------------------------------------- /Common/src/main/java/com/printezisn/moviestore/common/models/movie/MovieResultModel.java: -------------------------------------------------------------------------------- 1 | package com.printezisn.moviestore.common.models.movie; 2 | 3 | import java.util.LinkedList; 4 | import java.util.List; 5 | 6 | import com.printezisn.moviestore.common.dto.movie.MovieDto; 7 | import com.printezisn.moviestore.common.models.Result; 8 | 9 | import lombok.AllArgsConstructor; 10 | import lombok.Builder; 11 | import lombok.Data; 12 | import lombok.NoArgsConstructor; 13 | 14 | /** 15 | * Class that holds the result of a movie service call 16 | */ 17 | @Data 18 | @Builder 19 | @NoArgsConstructor 20 | @AllArgsConstructor 21 | public class MovieResultModel implements Result { 22 | 23 | private MovieDto result; 24 | 25 | @Builder.Default 26 | private List errors = new LinkedList<>(); 27 | } 28 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2019 Nikos Printezis 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /MovieService/build.gradle: -------------------------------------------------------------------------------- 1 | buildscript { 2 | ext { 3 | springBootVersion = '2.1.0.RELEASE' 4 | } 5 | repositories { 6 | mavenCentral() 7 | } 8 | dependencies { 9 | classpath("org.springframework.boot:spring-boot-gradle-plugin:${springBootVersion}") 10 | } 11 | } 12 | 13 | plugins { 14 | id 'io.franzbecker.gradle-lombok' version '1.14' 15 | } 16 | 17 | apply plugin: 'java' 18 | apply plugin: 'eclipse' 19 | apply plugin: 'org.springframework.boot' 20 | apply plugin: 'io.spring.dependency-management' 21 | 22 | group = 'com.printezisn.moviestore' 23 | version = '1.0.0' 24 | sourceCompatibility = 11 25 | 26 | repositories { 27 | mavenCentral() 28 | } 29 | 30 | lombok { 31 | version = '1.18.4' 32 | sha256 = '' 33 | } 34 | 35 | dependencies { 36 | implementation('javax.xml.bind:jaxb-api:2.3.0') 37 | implementation('org.springframework.boot:spring-boot-starter-data-elasticsearch') 38 | implementation('org.springframework.boot:spring-boot-starter-data-mongodb') 39 | implementation('org.springframework.boot:spring-boot-starter-web') 40 | implementation('org.mapstruct:mapstruct-jdk8:1.2.0.Final') 41 | 42 | implementation project(':Common') 43 | 44 | annotationProcessor('org.projectlombok:lombok:1.18.4') 45 | annotationProcessor('org.mapstruct:mapstruct-processor:1.2.0.Final') 46 | 47 | runtimeOnly('org.springframework.boot:spring-boot-devtools') 48 | 49 | testImplementation('org.springframework.boot:spring-boot-starter-test') 50 | } 51 | 52 | test { 53 | useJUnit { 54 | exclude '**/*IntegrationTest.class' 55 | } 56 | } 57 | 58 | task integTest(type: Test) { 59 | useJUnit { 60 | include '**/*IntegrationTest.class' 61 | } 62 | } -------------------------------------------------------------------------------- /MovieService/src/main/java/com/printezisn/moviestore/movieservice/MovieServiceApplication.java: -------------------------------------------------------------------------------- 1 | package com.printezisn.moviestore.movieservice; 2 | 3 | import org.springframework.boot.SpringApplication; 4 | import org.springframework.boot.autoconfigure.SpringBootApplication; 5 | import org.springframework.data.elasticsearch.repository.config.EnableElasticsearchRepositories; 6 | import org.springframework.scheduling.annotation.EnableScheduling; 7 | 8 | /** 9 | * The main class of the application 10 | */ 11 | @SpringBootApplication(scanBasePackages = { "com.printezisn.moviestore.movieservice", 12 | "com.printezisn.moviestore.common" }) 13 | @EnableScheduling 14 | @EnableElasticsearchRepositories 15 | public class MovieServiceApplication { 16 | 17 | /** 18 | * The main method of the application 19 | * 20 | * @param args 21 | * The command-line arguments 22 | */ 23 | public static void main(final String[] args) { 24 | SpringApplication.run(MovieServiceApplication.class, args); 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /MovieService/src/main/java/com/printezisn/moviestore/movieservice/configuration/ElasticsearchConfiguration.java: -------------------------------------------------------------------------------- 1 | package com.printezisn.moviestore.movieservice.configuration; 2 | 3 | import org.springframework.beans.factory.annotation.Value; 4 | import org.springframework.context.annotation.Bean; 5 | import org.springframework.context.annotation.Configuration; 6 | 7 | /** 8 | * Class with configuration for the connection with elasticsearch 9 | */ 10 | @Configuration 11 | public class ElasticsearchConfiguration { 12 | 13 | @Value("${elasticsearch.indexName}") 14 | private String elasticSearchIndexName; 15 | 16 | /** 17 | * Returns the name of the index that is used in elasticsearch 18 | * 19 | * @return The name of the index 20 | */ 21 | @Bean 22 | public String elasticSearchIndexName() { 23 | return elasticSearchIndexName; 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /MovieService/src/main/java/com/printezisn/moviestore/movieservice/movie/entities/Movie.java: -------------------------------------------------------------------------------- 1 | package com.printezisn.moviestore.movieservice.movie.entities; 2 | 3 | import java.util.Set; 4 | 5 | import org.springframework.data.annotation.Id; 6 | import org.springframework.data.mongodb.core.index.IndexDirection; 7 | import org.springframework.data.mongodb.core.index.Indexed; 8 | import org.springframework.data.mongodb.core.mapping.Document; 9 | 10 | import lombok.Data; 11 | 12 | /** 13 | * The movie entity 14 | */ 15 | @Document(collection = "movies") 16 | @Data 17 | public class Movie { 18 | 19 | @Id 20 | private String id; 21 | 22 | private String revision; 23 | 24 | private String title; 25 | 26 | private String description; 27 | 28 | private double rating; 29 | 30 | private int releaseYear; 31 | 32 | private long totalLikes; 33 | 34 | private long creationTimestamp; 35 | 36 | private long updateTimestamp; 37 | 38 | private String creator; 39 | 40 | private Set pendingLikes; 41 | 42 | private Set pendingUnlikes; 43 | 44 | @Indexed(direction = IndexDirection.DESCENDING) 45 | private boolean updated; 46 | 47 | private boolean deleted; 48 | } 49 | -------------------------------------------------------------------------------- /MovieService/src/main/java/com/printezisn/moviestore/movieservice/movie/entities/MovieIndex.java: -------------------------------------------------------------------------------- 1 | package com.printezisn.moviestore.movieservice.movie.entities; 2 | 3 | import org.springframework.data.annotation.Id; 4 | import org.springframework.data.elasticsearch.annotations.Document; 5 | import org.springframework.data.elasticsearch.annotations.Field; 6 | import org.springframework.data.elasticsearch.annotations.FieldType; 7 | 8 | import lombok.Data; 9 | 10 | /** 11 | * Movie index entity 12 | */ 13 | @Document(indexName = "#{@elasticSearchIndexName}", type = "movies", createIndex = true) 14 | @Data 15 | public class MovieIndex { 16 | 17 | @Id 18 | private String id; 19 | 20 | @Field(type = FieldType.Text, index = true, store = true) 21 | private String title; 22 | 23 | @Field(type = FieldType.Text, index = true, store = true) 24 | private String description; 25 | 26 | @Field(type = FieldType.Double, index = true, store = true) 27 | private double rating; 28 | 29 | @Field(type = FieldType.Integer, index = true, store = true) 30 | private int releaseYear; 31 | 32 | @Field(type = FieldType.Long, index = true, store = true) 33 | private long totalLikes; 34 | 35 | @Field(type = FieldType.Text, index = false, store = true) 36 | private String creator; 37 | } 38 | -------------------------------------------------------------------------------- /MovieService/src/main/java/com/printezisn/moviestore/movieservice/movie/entities/MovieLike.java: -------------------------------------------------------------------------------- 1 | package com.printezisn.moviestore.movieservice.movie.entities; 2 | 3 | import org.springframework.data.annotation.Id; 4 | import org.springframework.data.mongodb.core.mapping.Document; 5 | 6 | import lombok.Data; 7 | 8 | /** 9 | * The MovieLike entity 10 | */ 11 | @Document(collection = "movielikes") 12 | @Data 13 | public class MovieLike { 14 | 15 | @Id 16 | private String id; 17 | 18 | private String account; 19 | 20 | private String movieId; 21 | } 22 | -------------------------------------------------------------------------------- /MovieService/src/main/java/com/printezisn/moviestore/movieservice/movie/exceptions/MovieConditionalException.java: -------------------------------------------------------------------------------- 1 | package com.printezisn.moviestore.movieservice.movie.exceptions; 2 | 3 | /** 4 | * Exception class thrown when there is a conditional exception while updating a 5 | * database document 6 | */ 7 | @SuppressWarnings("serial") 8 | public class MovieConditionalException extends Exception { 9 | 10 | /** 11 | * The constructor 12 | */ 13 | public MovieConditionalException() { 14 | super("The conditional update failed."); 15 | } 16 | 17 | /** 18 | * The constructor 19 | * 20 | * @param message 21 | * The exception message 22 | */ 23 | public MovieConditionalException(final String message) { 24 | super(message); 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /MovieService/src/main/java/com/printezisn/moviestore/movieservice/movie/exceptions/MovieNotFoundException.java: -------------------------------------------------------------------------------- 1 | package com.printezisn.moviestore.movieservice.movie.exceptions; 2 | 3 | /** 4 | * Exception class thrown when a movie is not found 5 | */ 6 | @SuppressWarnings("serial") 7 | public class MovieNotFoundException extends Exception { 8 | 9 | /** 10 | * The constructor 11 | */ 12 | public MovieNotFoundException() { 13 | super("The movie was not found."); 14 | } 15 | 16 | /** 17 | * The constructor 18 | * 19 | * @param message 20 | * The exception message 21 | */ 22 | public MovieNotFoundException(final String message) { 23 | super(message); 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /MovieService/src/main/java/com/printezisn/moviestore/movieservice/movie/exceptions/MoviePersistenceException.java: -------------------------------------------------------------------------------- 1 | package com.printezisn.moviestore.movieservice.movie.exceptions; 2 | 3 | /** 4 | * Exception class for persistence errors regarding movies 5 | */ 6 | @SuppressWarnings("serial") 7 | public class MoviePersistenceException extends RuntimeException { 8 | 9 | /** 10 | * The constructor 11 | * 12 | * @param message 13 | * The error message 14 | * @param cause 15 | * The error cause 16 | */ 17 | public MoviePersistenceException(final String message, final Throwable cause) { 18 | super(message, cause); 19 | } 20 | } -------------------------------------------------------------------------------- /MovieService/src/main/java/com/printezisn/moviestore/movieservice/movie/exceptions/MovieValidationException.java: -------------------------------------------------------------------------------- 1 | package com.printezisn.moviestore.movieservice.movie.exceptions; 2 | 3 | /** 4 | * Exception class for validation errors regarding movies 5 | */ 6 | @SuppressWarnings("serial") 7 | public class MovieValidationException extends Exception { 8 | 9 | /** 10 | * The constructor 11 | * 12 | * @param message 13 | * The validation error message 14 | */ 15 | public MovieValidationException(final String message) { 16 | super(message); 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /MovieService/src/main/java/com/printezisn/moviestore/movieservice/movie/helpers/MovieIndexHelper.java: -------------------------------------------------------------------------------- 1 | package com.printezisn.moviestore.movieservice.movie.helpers; 2 | 3 | import java.util.HashSet; 4 | import java.util.UUID; 5 | 6 | import org.springframework.stereotype.Component; 7 | 8 | import com.printezisn.moviestore.movieservice.movie.entities.Movie; 9 | import com.printezisn.moviestore.movieservice.movie.entities.MovieIndex; 10 | import com.printezisn.moviestore.movieservice.movie.entities.MovieLike; 11 | import com.printezisn.moviestore.movieservice.movie.mappers.MovieMapper; 12 | import com.printezisn.moviestore.movieservice.movie.repositories.MovieIndexRepository; 13 | import com.printezisn.moviestore.movieservice.movie.repositories.MovieLikeRepository; 14 | import com.printezisn.moviestore.movieservice.movie.repositories.MovieRepository; 15 | 16 | import lombok.RequiredArgsConstructor; 17 | import lombok.extern.slf4j.Slf4j; 18 | 19 | /** 20 | * Helper class used to index movies 21 | */ 22 | @Component 23 | @RequiredArgsConstructor 24 | @Slf4j 25 | public class MovieIndexHelper { 26 | 27 | private final MovieRepository movieRepository; 28 | private final MovieLikeRepository movieLikeRepository; 29 | private final MovieIndexRepository movieIndexRepository; 30 | private final MovieMapper movieMapper; 31 | 32 | /** 33 | * Updates a movie in the search index and the database 34 | * 35 | * @param movie 36 | * The movie to update 37 | */ 38 | public void indexMovie(final Movie movie) { 39 | try { 40 | // Deletes the movie if it's indicated as deleted 41 | if (movie.isDeleted()) { 42 | movieIndexRepository.deleteById(movie.getId()); 43 | movieLikeRepository.deleteByMovieId(movie.getId()); 44 | movieRepository.deleteById(movie.getId()); 45 | 46 | return; 47 | } 48 | 49 | // Saves the pending likes 50 | movie.getPendingLikes().forEach(account -> { 51 | final MovieLike movieLike = new MovieLike(); 52 | movieLike.setId(movie.getId() + "-" + account); 53 | movieLike.setMovieId(movie.getId()); 54 | movieLike.setAccount(account); 55 | 56 | movieLikeRepository.save(movieLike); 57 | }); 58 | movie.setPendingLikes(new HashSet<>()); 59 | 60 | // Removes the pending unlikes 61 | movie.getPendingUnlikes() 62 | .forEach(account -> movieLikeRepository.deleteById(movie.getId() + "-" + account)); 63 | movie.setPendingUnlikes(new HashSet<>()); 64 | 65 | // Calculates the total likes 66 | movie.setTotalLikes(movieLikeRepository.countByMovieId(movie.getId())); 67 | 68 | // Indexes the movie 69 | final MovieIndex movieIndex = movieMapper.movieToMovieIndex(movie); 70 | movieIndexRepository.save(movieIndex); 71 | 72 | // Updates the movie in the database 73 | final String currentRevision = movie.getRevision(); 74 | movie.setRevision(UUID.randomUUID().toString()); 75 | movie.setUpdated(false); 76 | movieRepository.updateMovie(movie, currentRevision); 77 | } 78 | catch (final Exception ex) { 79 | log.error(String.format("An error occured while indexing movie %s: %s", movie.getId(), ex.getMessage()), 80 | ex); 81 | } 82 | } 83 | } 84 | -------------------------------------------------------------------------------- /MovieService/src/main/java/com/printezisn/moviestore/movieservice/movie/mappers/MovieMapper.java: -------------------------------------------------------------------------------- 1 | package com.printezisn.moviestore.movieservice.movie.mappers; 2 | 3 | import org.mapstruct.Mapper; 4 | import org.mapstruct.Mapping; 5 | import org.mapstruct.Mappings; 6 | 7 | import com.printezisn.moviestore.common.mappers.InstantMapper; 8 | import com.printezisn.moviestore.common.mappers.UUIDMapper; 9 | import com.printezisn.moviestore.common.dto.movie.MovieDto; 10 | import com.printezisn.moviestore.movieservice.movie.entities.Movie; 11 | import com.printezisn.moviestore.movieservice.movie.entities.MovieIndex; 12 | 13 | /** 14 | * The mapper class for the Movie entity 15 | */ 16 | @Mapper(componentModel = "spring", uses = { InstantMapper.class, UUIDMapper.class }) 17 | public interface MovieMapper { 18 | 19 | /** 20 | * Converts a Movie object to MovieDto 21 | * 22 | * @param movie 23 | * The Movie object to convert 24 | * @return The converted MovieDto object 25 | */ 26 | MovieDto movieToMovieDto(final Movie movie); 27 | 28 | /** 29 | * Converts a MovieDto object to Movie 30 | * 31 | * @param movieDto 32 | * The MovieDto object to convert 33 | * @return The converted Movie object 34 | */ 35 | @Mappings({ 36 | @Mapping(target = "revision", ignore = true), 37 | @Mapping(target = "totalLikes", ignore = true), 38 | @Mapping(target = "pendingLikes", ignore = true), 39 | @Mapping(target = "pendingUnlikes", ignore = true), 40 | @Mapping(target = "updated", ignore = true), 41 | @Mapping(target = "deleted", ignore = true) 42 | }) 43 | Movie movieDtoToMovie(final MovieDto movieDto); 44 | 45 | /** 46 | * Converts a Movie object to MovieIndex 47 | * 48 | * @param movie 49 | * The Movie object to convert 50 | * @return The converted MovieIndex object 51 | */ 52 | MovieIndex movieToMovieIndex(final Movie movie); 53 | 54 | /** 55 | * Converts a MovieIndex object to MovieDto 56 | * 57 | * @param movieIndex 58 | * The MovieIndex object to convert 59 | * @return The converted MovieDto object 60 | */ 61 | @Mappings({ 62 | @Mapping(target = "creationTimestamp", ignore = true), 63 | @Mapping(target = "updateTimestamp", ignore = true) 64 | }) 65 | MovieDto movieIndexToMovieDto(final MovieIndex movieIndex); 66 | } 67 | -------------------------------------------------------------------------------- /MovieService/src/main/java/com/printezisn/moviestore/movieservice/movie/repositories/CustomMovieIndexRepository.java: -------------------------------------------------------------------------------- 1 | package com.printezisn.moviestore.movieservice.movie.repositories; 2 | 3 | import java.util.Optional; 4 | 5 | import org.springframework.data.domain.Page; 6 | import org.springframework.data.domain.Pageable; 7 | 8 | import com.printezisn.moviestore.movieservice.movie.entities.MovieIndex; 9 | 10 | /** 11 | * Interface with extra repository methods for indexing movies 12 | */ 13 | public interface CustomMovieIndexRepository { 14 | 15 | /** 16 | * Searches for movies using full text search 17 | * 18 | * @param text 19 | * The text used as filter 20 | * @param pageable 21 | * The pageable criteria 22 | * @return The movies found 23 | */ 24 | Page search(final Optional text, final Pageable pageable); 25 | } 26 | -------------------------------------------------------------------------------- /MovieService/src/main/java/com/printezisn/moviestore/movieservice/movie/repositories/CustomMovieIndexRepositoryImpl.java: -------------------------------------------------------------------------------- 1 | package com.printezisn.moviestore.movieservice.movie.repositories; 2 | 3 | import org.springframework.beans.factory.annotation.Value; 4 | import org.springframework.data.domain.Page; 5 | import org.springframework.data.domain.PageImpl; 6 | import org.springframework.data.domain.Pageable; 7 | import org.springframework.data.elasticsearch.core.ElasticsearchTemplate; 8 | import org.springframework.data.elasticsearch.core.query.NativeSearchQueryBuilder; 9 | import org.springframework.data.elasticsearch.core.query.SearchQuery; 10 | 11 | import com.fasterxml.jackson.databind.ObjectMapper; 12 | import com.printezisn.moviestore.movieservice.movie.entities.MovieIndex; 13 | 14 | import lombok.RequiredArgsConstructor; 15 | 16 | import static org.elasticsearch.index.query.QueryBuilders.multiMatchQuery; 17 | import static org.elasticsearch.index.query.QueryBuilders.matchAllQuery; 18 | 19 | import java.util.LinkedList; 20 | import java.util.List; 21 | import java.util.Optional; 22 | 23 | import org.elasticsearch.common.unit.Fuzziness; 24 | import org.elasticsearch.index.query.MultiMatchQueryBuilder; 25 | import org.elasticsearch.index.query.Operator; 26 | import org.elasticsearch.search.SearchHit; 27 | 28 | /** 29 | * Implementation of the interface with extra repository methods for indexing 30 | * movies 31 | */ 32 | @RequiredArgsConstructor 33 | public class CustomMovieIndexRepositoryImpl implements CustomMovieIndexRepository { 34 | 35 | private static final String TITLE_FIELD = "title"; 36 | private static final String DESCRIPTION_FIELD = "description"; 37 | 38 | @Value("${elasticsearch.indexName}") 39 | private final String indexName; 40 | 41 | private final ElasticsearchTemplate elasticsearchTemplate; 42 | 43 | /** 44 | * {@inheritDoc} 45 | */ 46 | @Override 47 | public Page search(final Optional text, final Pageable pageable) { 48 | final SearchQuery searchQuery; 49 | 50 | if (text.isPresent() && !text.get().isBlank()) { 51 | searchQuery = new NativeSearchQueryBuilder() 52 | .withIndices(indexName) 53 | .withQuery(multiMatchQuery("*" + text.get() + "*", TITLE_FIELD, DESCRIPTION_FIELD) 54 | .type(MultiMatchQueryBuilder.Type.BEST_FIELDS) 55 | .operator(Operator.AND) 56 | .fuzziness(Fuzziness.TWO)) 57 | .withPageable(pageable) 58 | .build(); 59 | } 60 | else { 61 | searchQuery = new NativeSearchQueryBuilder() 62 | .withIndices(indexName) 63 | .withQuery(matchAllQuery()) 64 | .withPageable(pageable) 65 | .build(); 66 | } 67 | 68 | elasticsearchTemplate.putMapping(MovieIndex.class); 69 | 70 | return elasticsearchTemplate.query(searchQuery, searchResponse -> { 71 | try { 72 | final ObjectMapper objectMapper = new ObjectMapper(); 73 | final long totalHits = searchResponse.getHits().getTotalHits(); 74 | 75 | final List results = new LinkedList<>(); 76 | for (final SearchHit hit : searchResponse.getHits().getHits()) { 77 | final MovieIndex searchMovie = objectMapper.readValue(hit.getSourceAsString(), 78 | MovieIndex.class); 79 | results.add(searchMovie); 80 | } 81 | 82 | return new PageImpl(results, pageable, totalHits); 83 | } 84 | catch (final Exception ex) { 85 | throw new RuntimeException(ex); 86 | } 87 | }); 88 | } 89 | } 90 | -------------------------------------------------------------------------------- /MovieService/src/main/java/com/printezisn/moviestore/movieservice/movie/repositories/CustomMovieRepository.java: -------------------------------------------------------------------------------- 1 | package com.printezisn.moviestore.movieservice.movie.repositories; 2 | 3 | import com.printezisn.moviestore.movieservice.movie.entities.Movie; 4 | 5 | /** 6 | * Interface with extra repository methods for movies 7 | */ 8 | public interface CustomMovieRepository { 9 | 10 | /** 11 | * Updates a movie 12 | * 13 | * @param movie 14 | * The movie 15 | * @param currentRevision 16 | * The current revision of the movie 17 | * @return The number of documents affected 18 | */ 19 | long updateMovie(final Movie movie, final String currentRevision); 20 | } 21 | -------------------------------------------------------------------------------- /MovieService/src/main/java/com/printezisn/moviestore/movieservice/movie/repositories/CustomMovieRepositoryImpl.java: -------------------------------------------------------------------------------- 1 | package com.printezisn.moviestore.movieservice.movie.repositories; 2 | 3 | import org.springframework.data.mongodb.core.MongoTemplate; 4 | import org.springframework.data.mongodb.core.query.Criteria; 5 | import org.springframework.data.mongodb.core.query.Query; 6 | import org.springframework.data.mongodb.core.query.Update; 7 | 8 | import com.mongodb.client.result.UpdateResult; 9 | import com.printezisn.moviestore.movieservice.movie.entities.Movie; 10 | 11 | import lombok.RequiredArgsConstructor; 12 | 13 | /** 14 | * The implementation of the interface with extra repository methods for movies 15 | */ 16 | @RequiredArgsConstructor 17 | public class CustomMovieRepositoryImpl implements CustomMovieRepository { 18 | 19 | private static final String ID_FIELD = "id"; 20 | private static final String TITLE_FIELD = "title"; 21 | private static final String DESCRIPTION_FIELD = "description"; 22 | private static final String RATING_FIELD = "rating"; 23 | private static final String RELEASE_YEAR_FIELD = "releaseYear"; 24 | private static final String UPDATE_TIMESTAMP_FIELD = "updateTimestamp"; 25 | private static final String REVISION_FIELD = "revision"; 26 | private static final String TOTAL_LIKES_FIELD = "totalLikes"; 27 | private static final String PENDING_LIKES_FIELD = "pendingLikes"; 28 | private static final String PENDING_UNLIKES_FIELD = "pendingUnlikes"; 29 | private static final String UPDATED_FIELD = "updated"; 30 | 31 | private final MongoTemplate mongoTemplate; 32 | 33 | /** 34 | * {@inheritDoc} 35 | */ 36 | public long updateMovie(final Movie movie, final String currentRevision) { 37 | final Criteria idCriteria = Criteria.where(ID_FIELD).is(movie.getId()); 38 | final Criteria revisionCriteria = Criteria.where(REVISION_FIELD).is(currentRevision); 39 | final Criteria finalCriteria = idCriteria.andOperator(revisionCriteria); 40 | 41 | final Query query = new Query(finalCriteria); 42 | 43 | final Update update = new Update(); 44 | update.set(REVISION_FIELD, movie.getRevision()); 45 | update.set(TITLE_FIELD, movie.getTitle()); 46 | update.set(DESCRIPTION_FIELD, movie.getDescription()); 47 | update.set(RATING_FIELD, movie.getRating()); 48 | update.set(RELEASE_YEAR_FIELD, movie.getReleaseYear()); 49 | update.set(TOTAL_LIKES_FIELD, movie.getTotalLikes()); 50 | update.set(UPDATE_TIMESTAMP_FIELD, movie.getUpdateTimestamp()); 51 | update.set(PENDING_LIKES_FIELD, movie.getPendingLikes()); 52 | update.set(PENDING_UNLIKES_FIELD, movie.getPendingUnlikes()); 53 | update.set(UPDATED_FIELD, movie.isUpdated()); 54 | 55 | final UpdateResult updateResult = mongoTemplate.updateFirst(query, update, Movie.class); 56 | 57 | return (updateResult != null) ? updateResult.getModifiedCount() : 0; 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /MovieService/src/main/java/com/printezisn/moviestore/movieservice/movie/repositories/MovieIndexRepository.java: -------------------------------------------------------------------------------- 1 | package com.printezisn.moviestore.movieservice.movie.repositories; 2 | 3 | import org.springframework.data.elasticsearch.repository.ElasticsearchRepository; 4 | import org.springframework.stereotype.Repository; 5 | 6 | import com.printezisn.moviestore.movieservice.movie.entities.MovieIndex; 7 | 8 | /** 9 | * The interface of the repository used for indexing movies 10 | */ 11 | @Repository 12 | public interface MovieIndexRepository extends ElasticsearchRepository, 13 | CustomMovieIndexRepository { 14 | 15 | } 16 | -------------------------------------------------------------------------------- /MovieService/src/main/java/com/printezisn/moviestore/movieservice/movie/repositories/MovieLikeRepository.java: -------------------------------------------------------------------------------- 1 | package com.printezisn.moviestore.movieservice.movie.repositories; 2 | 3 | import org.springframework.data.mongodb.repository.MongoRepository; 4 | import org.springframework.stereotype.Repository; 5 | 6 | import com.printezisn.moviestore.movieservice.movie.entities.MovieLike; 7 | 8 | /** 9 | * The repository layer for movie likes 10 | */ 11 | @Repository 12 | public interface MovieLikeRepository extends MongoRepository { 13 | 14 | /** 15 | * Deletes movie likes based on movie id 16 | * 17 | * @param movieId 18 | * The movie id 19 | */ 20 | void deleteByMovieId(final String movieId); 21 | 22 | /** 23 | * Returns the number of likes for a movie 24 | * 25 | * @param movieId 26 | * The id of the movie 27 | * @return The number of likes 28 | */ 29 | long countByMovieId(final String movieId); 30 | } 31 | -------------------------------------------------------------------------------- /MovieService/src/main/java/com/printezisn/moviestore/movieservice/movie/repositories/MovieRepository.java: -------------------------------------------------------------------------------- 1 | package com.printezisn.moviestore.movieservice.movie.repositories; 2 | 3 | import java.util.Collection; 4 | 5 | import org.springframework.data.mongodb.repository.MongoRepository; 6 | import org.springframework.stereotype.Repository; 7 | 8 | import com.printezisn.moviestore.movieservice.movie.entities.Movie; 9 | 10 | /** 11 | * The repository layer for movies 12 | */ 13 | @Repository 14 | public interface MovieRepository extends MongoRepository, CustomMovieRepository { 15 | 16 | /** 17 | * Filters movies based on their "updated" field 18 | * 19 | * @param updated 20 | * The value of the "updated" field 21 | * @return A list with the movies found 22 | */ 23 | Collection findByUpdated(final boolean updated); 24 | } 25 | -------------------------------------------------------------------------------- /MovieService/src/main/java/com/printezisn/moviestore/movieservice/movie/services/MovieService.java: -------------------------------------------------------------------------------- 1 | package com.printezisn.moviestore.movieservice.movie.services; 2 | 3 | import java.util.Optional; 4 | import java.util.UUID; 5 | 6 | import com.printezisn.moviestore.common.models.movie.MoviePagedResultModel; 7 | import com.printezisn.moviestore.common.dto.movie.MovieDto; 8 | import com.printezisn.moviestore.movieservice.movie.exceptions.MovieConditionalException; 9 | import com.printezisn.moviestore.movieservice.movie.exceptions.MovieNotFoundException; 10 | 11 | /** 12 | * The service layer for movies 13 | */ 14 | public interface MovieService { 15 | 16 | /** 17 | * Searches for movies 18 | * 19 | * @param text 20 | * The text to search for 21 | * @param pageNumber 22 | * The page number 23 | * @param sortField 24 | * The sorting field 25 | * @param isAscending 26 | * Indicates if the sorting is ascending or descending 27 | * @return The movies found 28 | */ 29 | MoviePagedResultModel searchMovies(final Optional text, final Optional pageNumber, 30 | final Optional sortField, final boolean isAscending); 31 | 32 | /** 33 | * Returns a movie 34 | * 35 | * @param id 36 | * The id of the movie 37 | * @return The movie 38 | * @throws MovieNotFoundException 39 | * Exception thrown if the movie is not found 40 | */ 41 | MovieDto getMovie(final UUID id) throws MovieNotFoundException; 42 | 43 | /** 44 | * Creates a new movie 45 | * 46 | * @param movieDto 47 | * The new movie model 48 | * @return The created movie 49 | */ 50 | MovieDto createMovie(final MovieDto movieDto); 51 | 52 | /** 53 | * Updates a movie 54 | * 55 | * @param movieDto 56 | * The movie model 57 | * @return The updated movie 58 | * @throws MovieNotFoundException 59 | * Exception thrown if the movie is not found 60 | * @throws MovieConditionalException 61 | * Exception thrown if the movie update conflicts with another 62 | * update 63 | */ 64 | MovieDto updateMovie(final MovieDto movieDto) throws MovieNotFoundException, MovieConditionalException; 65 | 66 | /** 67 | * Deletes a movie 68 | * 69 | * @param id 70 | * The id of the movie to delete 71 | * @throws MovieConditionalException 72 | * Exception thrown if the movie update conflicts with another 73 | * update 74 | */ 75 | void deleteMovie(final UUID id) throws MovieConditionalException; 76 | 77 | /** 78 | * Adds a like to a movie 79 | * 80 | * @param movieId 81 | * The id of the movie to like 82 | * @param account 83 | * The account that likes the movie 84 | * @throws MovieConditionalException 85 | * Exception thrown in case of conditional update failure 86 | * @throws MovieNotFoundException 87 | * Exception thrown if the movie is not found 88 | */ 89 | void likeMovie(final UUID movieId, final String account) throws MovieConditionalException, MovieNotFoundException; 90 | 91 | /** 92 | * Checks if an account has liked a movie 93 | * 94 | * @param movieId 95 | * The id of the movie 96 | * @param account 97 | * The account 98 | * @return True if the account has liked the movie, otherwise false 99 | */ 100 | boolean hasLiked(final UUID movieId, final String account); 101 | 102 | /** 103 | * Removes a like from a movie 104 | * 105 | * @param movieId 106 | * The id of the movie to unlike 107 | * @param account 108 | * The account that removes the like from the movie 109 | * @throws MovieConditionalException 110 | * Exception thrown in case of conditional update failure 111 | * @throws MovieNotFoundException 112 | * Exception thrown if the movie is not found 113 | */ 114 | void unlikeMovie(final UUID movieId, final String account) 115 | throws MovieConditionalException, MovieNotFoundException; 116 | } 117 | -------------------------------------------------------------------------------- /MovieService/src/main/resources/application-test.properties: -------------------------------------------------------------------------------- 1 | server.port=9100 2 | 3 | spring.data.mongodb.database=moviestore_test 4 | 5 | elasticsearch.indexName=moviestore_test -------------------------------------------------------------------------------- /MovieService/src/main/resources/application.properties: -------------------------------------------------------------------------------- 1 | server.port=9000 2 | 3 | spring.data.mongodb.host=localhost 4 | spring.data.mongodb.port=27017 5 | spring.data.mongodb.database=moviestore 6 | spring.data.mongodb.password=1234 7 | 8 | spring.data.elasticsearch.cluster-name=elasticsearch_pnikos 9 | spring.data.elasticsearch.cluster-nodes=localhost:9300 10 | spring.data.elasticsearch.repositories.enabled=true 11 | elasticsearch.indexName=moviestore 12 | 13 | searchIndex.fixedRate=5000 -------------------------------------------------------------------------------- /MovieService/src/main/resources/i18n/messages/messages.properties: -------------------------------------------------------------------------------- 1 | message.movie.error.idRequired=The id is required. 2 | message.movie.error.revisionRequired=The revision is required. 3 | message.movie.error.titleRequired=The title is required. 4 | message.movie.error.titleMaxLength=The title may have 250 characters maximum. 5 | message.movie.error.descriptionRequired=The description is required. 6 | message.movie.error.ratingRequired=The rating is required. 7 | message.movie.error.ratingMaxValue=The maximum value for rating is 10. 8 | message.movie.error.ratingMinValue=The minimum value for rating is 0. 9 | message.movie.error.releaseYearRequired=The release year is required. 10 | message.movie.error.creatorRequired=The creator is required. -------------------------------------------------------------------------------- /MovieService/src/main/resources/logback-spring.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 8 | 9 | 10 | %black(%d{ISO8601}) %highlight(%-5level) 11 | [%blue(%t)] %yellow(%C{1.}): %msg%n%throwable 12 | 13 | 14 | 15 | 16 | 18 | ${LOGS}/application.log 19 | 21 | %d %p %C{1.} [%t] %m%n 22 | 23 | 24 | 26 | 27 | ${LOGS}/archived/%d{yyyy-MM}/%d{dd}/%d{HH}/application-%d{yyyy-MM-dd-HH}.%i.log 28 | 29 | 31 | 10MB 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 45 | 46 | 47 | 48 | -------------------------------------------------------------------------------- /MovieService/src/test/java/com/printezisn/moviestore/movieservice/movie/helpers/MovieIndexHelperTest.java: -------------------------------------------------------------------------------- 1 | package com.printezisn.moviestore.movieservice.movie.helpers; 2 | 3 | import static org.junit.Assert.assertEquals; 4 | import static org.junit.Assert.assertFalse; 5 | import static org.junit.Assert.assertTrue; 6 | import static org.mockito.Mockito.doThrow; 7 | import static org.mockito.Mockito.never; 8 | import static org.mockito.Mockito.verify; 9 | import static org.mockito.Mockito.when; 10 | 11 | import java.util.Arrays; 12 | import java.util.HashSet; 13 | import java.util.UUID; 14 | 15 | import org.junit.Before; 16 | import org.junit.Test; 17 | import org.mockito.Mock; 18 | import org.mockito.MockitoAnnotations; 19 | 20 | import com.printezisn.moviestore.movieservice.movie.entities.Movie; 21 | import com.printezisn.moviestore.movieservice.movie.entities.MovieIndex; 22 | import com.printezisn.moviestore.movieservice.movie.entities.MovieLike; 23 | import com.printezisn.moviestore.movieservice.movie.mappers.MovieMapper; 24 | import com.printezisn.moviestore.movieservice.movie.repositories.MovieIndexRepository; 25 | import com.printezisn.moviestore.movieservice.movie.repositories.MovieLikeRepository; 26 | import com.printezisn.moviestore.movieservice.movie.repositories.MovieRepository; 27 | 28 | /** 29 | * Class that contains unit tests for the MovieIndexHelper class 30 | */ 31 | public class MovieIndexHelperTest { 32 | 33 | @Mock 34 | private MovieRepository movieRepository; 35 | 36 | @Mock 37 | private MovieLikeRepository movieLikeRepository; 38 | 39 | @Mock 40 | private MovieIndexRepository movieIndexRepository; 41 | 42 | @Mock 43 | private MovieMapper movieMapper; 44 | 45 | private MovieIndexHelper movieIndexHelper; 46 | 47 | /** 48 | * Initializes the test class 49 | */ 50 | @Before 51 | public void setUp() { 52 | MockitoAnnotations.initMocks(this); 53 | 54 | movieIndexHelper = new MovieIndexHelper(movieRepository, movieLikeRepository, movieIndexRepository, 55 | movieMapper); 56 | } 57 | 58 | /** 59 | * Tests the scenario in which a movie is deleted 60 | */ 61 | @Test 62 | public void test_indexMovie_deleteMovie() { 63 | final Movie movie = new Movie(); 64 | movie.setId(UUID.randomUUID().toString()); 65 | movie.setDeleted(true); 66 | 67 | movieIndexHelper.indexMovie(movie); 68 | 69 | verify(movieRepository).deleteById(movie.getId()); 70 | verify(movieIndexRepository).deleteById(movie.getId()); 71 | verify(movieLikeRepository).deleteByMovieId(movie.getId()); 72 | } 73 | 74 | /** 75 | * Tests the scenario in which a movie is updated 76 | */ 77 | @Test 78 | public void test_indexMovie_updateMovie() { 79 | final String currentRevision = UUID.randomUUID().toString(); 80 | final Movie movie = new Movie(); 81 | movie.setId(UUID.randomUUID().toString()); 82 | movie.setRevision(currentRevision); 83 | movie.setPendingLikes(new HashSet<>(Arrays.asList("account1"))); 84 | movie.setPendingUnlikes(new HashSet<>(Arrays.asList("account2"))); 85 | 86 | final MovieIndex movieIndex = new MovieIndex(); 87 | 88 | final MovieLike movieLike = new MovieLike(); 89 | movieLike.setId(movie.getId() + "-account1"); 90 | movieLike.setMovieId(movie.getId()); 91 | movieLike.setAccount("account1"); 92 | 93 | when(movieRepository.findByUpdated(true)).thenReturn(Arrays.asList(movie)); 94 | when(movieMapper.movieToMovieIndex(movie)).thenReturn(movieIndex); 95 | when(movieLikeRepository.countByMovieId(movie.getId())).thenReturn(5L); 96 | 97 | movieIndexHelper.indexMovie(movie); 98 | 99 | verify(movieLikeRepository).save(movieLike); 100 | verify(movieLikeRepository).deleteById(movie.getId() + "-account2"); 101 | verify(movieIndexRepository).save(movieIndex); 102 | verify(movieRepository).updateMovie(movie, currentRevision); 103 | 104 | assertEquals(5L, movie.getTotalLikes()); 105 | assertTrue(movie.getPendingLikes().isEmpty()); 106 | assertTrue(movie.getPendingUnlikes().isEmpty()); 107 | assertFalse(movie.isUpdated()); 108 | } 109 | 110 | /** 111 | * Tests the scenario in which an exception is thrown while processing a movie 112 | */ 113 | @Test 114 | public void test_indexMovie_processException() { 115 | final Movie movie = new Movie(); 116 | movie.setId(UUID.randomUUID().toString()); 117 | movie.setDeleted(true); 118 | 119 | doThrow(new RuntimeException()).when(movieLikeRepository).deleteByMovieId(movie.getId()); 120 | 121 | movieIndexHelper.indexMovie(movie); 122 | 123 | verify(movieRepository, never()).deleteById(movie.getId()); 124 | } 125 | } 126 | -------------------------------------------------------------------------------- /Website/build.gradle: -------------------------------------------------------------------------------- 1 | buildscript { 2 | ext { 3 | springBootVersion = '2.1.0.RELEASE' 4 | } 5 | repositories { 6 | mavenCentral() 7 | } 8 | dependencies { 9 | classpath("org.springframework.boot:spring-boot-gradle-plugin:${springBootVersion}") 10 | } 11 | } 12 | 13 | plugins { 14 | id 'io.franzbecker.gradle-lombok' version '1.14' 15 | } 16 | 17 | apply plugin: 'java' 18 | apply plugin: 'eclipse' 19 | apply plugin: 'org.springframework.boot' 20 | apply plugin: 'io.spring.dependency-management' 21 | 22 | group = 'com.printezisn.moviestore' 23 | version = '1.0.0' 24 | sourceCompatibility = 11 25 | 26 | repositories { 27 | mavenCentral() 28 | } 29 | 30 | lombok { 31 | version = '1.18.4' 32 | sha256 = '' 33 | } 34 | 35 | dependencies { 36 | implementation('javax.xml.bind:jaxb-api:2.3.0') 37 | implementation('org.springframework.boot:spring-boot-starter-security') 38 | implementation('org.springframework.boot:spring-boot-starter-thymeleaf') 39 | implementation('org.springframework.boot:spring-boot-starter-web') 40 | implementation('nz.net.ultraq.thymeleaf:thymeleaf-layout-dialect') 41 | implementation('org.thymeleaf.extras:thymeleaf-extras-springsecurity5') 42 | 43 | compileOnly('org.springframework.boot:spring-boot-configuration-processor') 44 | 45 | implementation project(':Common') 46 | 47 | annotationProcessor('org.projectlombok:lombok:1.18.4') 48 | 49 | runtimeOnly('org.springframework.boot:spring-boot-devtools') 50 | 51 | testImplementation('org.springframework.boot:spring-boot-starter-test') 52 | testImplementation('org.springframework.security:spring-security-test') 53 | } 54 | 55 | test { 56 | useJUnit { 57 | exclude '**/*IntegrationTest.class' 58 | } 59 | } 60 | 61 | task integTest(type: Test) { 62 | useJUnit { 63 | include '**/*IntegrationTest.class' 64 | } 65 | } -------------------------------------------------------------------------------- /Website/src/main/java/com/printezisn/moviestore/website/Constants.java: -------------------------------------------------------------------------------- 1 | package com.printezisn.moviestore.website; 2 | 3 | /** 4 | * Class that contains constants for the application 5 | */ 6 | public class Constants { 7 | 8 | /** 9 | * Class that contains constants for the pages 10 | */ 11 | public static class PageConstants { 12 | public static final String HOME_PAGE = "home"; 13 | public static final String LOGIN_PAGE = "login"; 14 | public static final String REGISTER_PAGE = "register"; 15 | public static final String NEW_MOVIE_PAGE = "newmovie"; 16 | public static final String EDIT_MOVIE_PAGE = "editmovie"; 17 | public static final String CHANGE_PASSWORD_PAGE = "changepassword"; 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /Website/src/main/java/com/printezisn/moviestore/website/WebsiteApplication.java: -------------------------------------------------------------------------------- 1 | package com.printezisn.moviestore.website; 2 | 3 | import org.springframework.boot.SpringApplication; 4 | import org.springframework.boot.autoconfigure.SpringBootApplication; 5 | 6 | /** 7 | * The main class of the application 8 | */ 9 | @SpringBootApplication(scanBasePackages = { "com.printezisn.moviestore.website", "com.printezisn.moviestore.common" }) 10 | public class WebsiteApplication { 11 | 12 | /** 13 | * The main method of the application 14 | * 15 | * @param args 16 | * The command-line arguments 17 | */ 18 | public static void main(String[] args) { 19 | SpringApplication.run(WebsiteApplication.class, args); 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /Website/src/main/java/com/printezisn/moviestore/website/account/controllers/AuthController.java: -------------------------------------------------------------------------------- 1 | package com.printezisn.moviestore.website.account.controllers; 2 | 3 | import org.springframework.stereotype.Controller; 4 | import org.springframework.ui.Model; 5 | import org.springframework.web.bind.annotation.GetMapping; 6 | 7 | import com.printezisn.moviestore.common.AppUtils; 8 | import com.printezisn.moviestore.website.Constants.PageConstants; 9 | 10 | import lombok.RequiredArgsConstructor; 11 | 12 | /** 13 | * The controller class associated with authentication 14 | */ 15 | @Controller 16 | @RequiredArgsConstructor 17 | public class AuthController { 18 | 19 | public final AppUtils appUtils; 20 | 21 | /** 22 | * Renders the login page 23 | * 24 | * @param model 25 | * The page model 26 | * @return The login page view 27 | */ 28 | @GetMapping("/auth/login") 29 | public String login(final Model model) { 30 | appUtils.setCurrentPage(model, PageConstants.LOGIN_PAGE); 31 | 32 | return "auth/login"; 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /Website/src/main/java/com/printezisn/moviestore/website/account/exceptions/AccountAuthenticationException.java: -------------------------------------------------------------------------------- 1 | package com.printezisn.moviestore.website.account.exceptions; 2 | 3 | import org.springframework.security.core.AuthenticationException; 4 | 5 | /** 6 | * Exception class for authentication errors 7 | */ 8 | @SuppressWarnings("serial") 9 | public class AccountAuthenticationException extends AuthenticationException { 10 | 11 | /** 12 | * {@inheritDoc} 13 | */ 14 | public AccountAuthenticationException(String msg) { 15 | super(msg); 16 | } 17 | 18 | /** 19 | * {@inheritDoc} 20 | */ 21 | public AccountAuthenticationException(String msg, Throwable t) { 22 | super(msg, t); 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /Website/src/main/java/com/printezisn/moviestore/website/account/exceptions/AccountNotValidatedException.java: -------------------------------------------------------------------------------- 1 | package com.printezisn.moviestore.website.account.exceptions; 2 | 3 | /** 4 | * Exception class indicating that an account was not validated 5 | */ 6 | @SuppressWarnings("serial") 7 | public class AccountNotValidatedException extends Exception { 8 | 9 | /** 10 | * The constructor 11 | */ 12 | public AccountNotValidatedException() { 13 | super(); 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /Website/src/main/java/com/printezisn/moviestore/website/account/exceptions/AccountPersistenceException.java: -------------------------------------------------------------------------------- 1 | package com.printezisn.moviestore.website.account.exceptions; 2 | 3 | /** 4 | * Exception class related to persistence errors for the account entity 5 | */ 6 | @SuppressWarnings("serial") 7 | public class AccountPersistenceException extends RuntimeException { 8 | 9 | /** 10 | * The constructor 11 | * 12 | * @param message 13 | * The exception message 14 | * @param cause 15 | * The inner exception 16 | */ 17 | public AccountPersistenceException(final String message, final Throwable cause) { 18 | super(message, cause); 19 | } 20 | } -------------------------------------------------------------------------------- /Website/src/main/java/com/printezisn/moviestore/website/account/models/AuthenticatedUser.java: -------------------------------------------------------------------------------- 1 | package com.printezisn.moviestore.website.account.models; 2 | 3 | import java.util.Collection; 4 | 5 | import org.springframework.security.core.GrantedAuthority; 6 | import org.springframework.security.core.userdetails.UserDetails; 7 | 8 | import lombok.Data; 9 | import lombok.RequiredArgsConstructor; 10 | 11 | /** 12 | * The model class for authenticated users 13 | */ 14 | @SuppressWarnings("serial") 15 | @Data 16 | @RequiredArgsConstructor 17 | public class AuthenticatedUser implements UserDetails { 18 | 19 | private final String username; 20 | private final String password; 21 | private final String emailAddress; 22 | private final Collection authorities; 23 | 24 | /** 25 | * {@inheritDoc} 26 | */ 27 | @Override 28 | public boolean isAccountNonExpired() { 29 | return false; 30 | } 31 | 32 | /** 33 | * {@inheritDoc} 34 | */ 35 | @Override 36 | public boolean isAccountNonLocked() { 37 | return true; 38 | } 39 | 40 | /** 41 | * {@inheritDoc} 42 | */ 43 | @Override 44 | public boolean isCredentialsNonExpired() { 45 | return true; 46 | } 47 | 48 | /** 49 | * {@inheritDoc} 50 | */ 51 | @Override 52 | public boolean isEnabled() { 53 | return true; 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /Website/src/main/java/com/printezisn/moviestore/website/account/models/ChangePasswordModel.java: -------------------------------------------------------------------------------- 1 | package com.printezisn.moviestore.website.account.models; 2 | 3 | import javax.validation.constraints.NotEmpty; 4 | import javax.validation.constraints.Pattern; 5 | import javax.validation.constraints.Size; 6 | 7 | import com.printezisn.moviestore.common.RegexLibrary; 8 | 9 | import lombok.Data; 10 | 11 | /** 12 | * Model class for the change password operation 13 | */ 14 | @Data 15 | public class ChangePasswordModel { 16 | 17 | @NotEmpty(message = "message.changePassword.error.currentPasswordRequired") 18 | private String currentPassword; 19 | 20 | @NotEmpty(message = "message.changePassword.error.newPasswordRequired") 21 | @Size(max = 250, message = "message.changePassword.error.newPasswordMaxLength") 22 | @Pattern(regexp = RegexLibrary.PASSWORD_REGEX, message = "message.changePassword.error.newPasswordFormat") 23 | private String newPassword; 24 | } 25 | -------------------------------------------------------------------------------- /Website/src/main/java/com/printezisn/moviestore/website/account/services/AccountService.java: -------------------------------------------------------------------------------- 1 | package com.printezisn.moviestore.website.account.services; 2 | 3 | import org.springframework.security.core.userdetails.UserDetails; 4 | import org.springframework.security.core.userdetails.UserDetailsService; 5 | 6 | import com.printezisn.moviestore.common.dto.account.AccountDto; 7 | import com.printezisn.moviestore.common.models.account.AccountResultModel; 8 | import com.printezisn.moviestore.website.account.exceptions.AccountAuthenticationException; 9 | import com.printezisn.moviestore.website.account.exceptions.AccountNotValidatedException; 10 | import com.printezisn.moviestore.website.account.models.ChangePasswordModel; 11 | 12 | /** 13 | * The interface of the account service 14 | */ 15 | public interface AccountService extends UserDetailsService { 16 | 17 | /** 18 | * Authenticates a user 19 | * 20 | * @param username 21 | * The username 22 | * @param password 23 | * The password 24 | * @return The details of the account 25 | * @throws AccountAuthenticationException 26 | * Exception thrown when there is an authentication error 27 | * @throws AccountNotValidatedException 28 | * Exception thrown when the account is not authenticated 29 | */ 30 | UserDetails authenticate(final String username, final String password) 31 | throws AccountAuthenticationException, AccountNotValidatedException; 32 | 33 | /** 34 | * Creates a new account 35 | * 36 | * @param accountDto 37 | * The model of the new account 38 | * @return The created account 39 | */ 40 | AccountResultModel createAccount(final AccountDto accountDto); 41 | 42 | /** 43 | * Changes the password for an account 44 | * 45 | * @param username 46 | * The username of the account to change password for 47 | * @param changePasswordModel 48 | * The model instance used for the change password operation 49 | * @return The updated account 50 | * @throws AccountNotValidatedException 51 | * Exception thrown when the account is not authenticated with the 52 | * current password 53 | */ 54 | AccountResultModel changePassword(final String username, final ChangePasswordModel changePasswordModel) 55 | throws AccountNotValidatedException; 56 | } 57 | -------------------------------------------------------------------------------- /Website/src/main/java/com/printezisn/moviestore/website/configuration/AccountAuthenticationProvider.java: -------------------------------------------------------------------------------- 1 | package com.printezisn.moviestore.website.configuration; 2 | 3 | import java.util.ArrayList; 4 | 5 | import org.springframework.security.authentication.AuthenticationProvider; 6 | import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; 7 | import org.springframework.security.core.Authentication; 8 | import org.springframework.security.core.AuthenticationException; 9 | import org.springframework.security.core.userdetails.UserDetails; 10 | import org.springframework.stereotype.Component; 11 | 12 | import com.printezisn.moviestore.website.account.exceptions.AccountNotValidatedException; 13 | import com.printezisn.moviestore.website.account.services.AccountService; 14 | 15 | import lombok.RequiredArgsConstructor; 16 | 17 | /** 18 | * The authentication provider 19 | */ 20 | @Component 21 | @RequiredArgsConstructor 22 | public class AccountAuthenticationProvider implements AuthenticationProvider { 23 | 24 | private final AccountService accountService; 25 | 26 | /** 27 | * {@inheritDoc} 28 | */ 29 | @Override 30 | public Authentication authenticate(final Authentication authentication) throws AuthenticationException { 31 | final String username = authentication.getName(); 32 | final String password = authentication.getCredentials().toString(); 33 | 34 | try { 35 | final UserDetails userDetails = accountService.authenticate(username, password); 36 | 37 | return new UsernamePasswordAuthenticationToken(userDetails, null, new ArrayList<>()); 38 | } 39 | catch (final AccountNotValidatedException ex) { 40 | return null; 41 | } 42 | } 43 | 44 | /** 45 | * {@inheritDoc} 46 | */ 47 | @Override 48 | public boolean supports(final Class authentication) { 49 | return authentication.equals(UsernamePasswordAuthenticationToken.class); 50 | } 51 | 52 | } 53 | -------------------------------------------------------------------------------- /Website/src/main/java/com/printezisn/moviestore/website/configuration/GeneralConfiguration.java: -------------------------------------------------------------------------------- 1 | package com.printezisn.moviestore.website.configuration; 2 | 3 | import java.time.LocalDateTime; 4 | import java.time.temporal.ChronoUnit; 5 | 6 | import org.springframework.boot.web.client.RestTemplateBuilder; 7 | import org.springframework.context.annotation.Bean; 8 | import org.springframework.context.annotation.Configuration; 9 | import org.springframework.web.client.RestTemplate; 10 | import org.springframework.web.servlet.config.annotation.ResourceHandlerRegistry; 11 | import org.springframework.web.servlet.config.annotation.WebMvcConfigurer; 12 | import org.springframework.web.servlet.resource.EncodedResourceResolver; 13 | import org.springframework.web.servlet.resource.PathResourceResolver; 14 | 15 | import com.printezisn.moviestore.website.Constants.PageConstants; 16 | import com.printezisn.moviestore.website.configuration.rest.DefaultResponseErrorHandler; 17 | 18 | /** 19 | * General bean configuration class 20 | */ 21 | @Configuration 22 | public class GeneralConfiguration implements WebMvcConfigurer { 23 | 24 | private static final int ASSETS_CACHE_SECONDS = (int) ChronoUnit.SECONDS.between(LocalDateTime.now(), 25 | LocalDateTime.now().plusMonths(3)); 26 | 27 | /** 28 | * Creates a RestTemplate bean 29 | * 30 | * @param restTemplateBuilder 31 | * The RestTemplate builder 32 | * @return The RestTemplate bean 33 | */ 34 | @Bean 35 | public RestTemplate restTemplate(final RestTemplateBuilder restTemplateBuilder) { 36 | return restTemplateBuilder 37 | .errorHandler(new DefaultResponseErrorHandler()) 38 | .build(); 39 | } 40 | 41 | /** 42 | * Creates a PageConstants bean 43 | * 44 | * @return The PageConstants bean 45 | */ 46 | @Bean 47 | public PageConstants pageConstants() { 48 | return new PageConstants(); 49 | } 50 | 51 | /** 52 | * {@inheritDoc} 53 | */ 54 | @Override 55 | public void addResourceHandlers(final ResourceHandlerRegistry registry) { 56 | registry 57 | .addResourceHandler("/*.js", "/*.css") 58 | .addResourceLocations("classpath:/static/dist/") 59 | .setCachePeriod(ASSETS_CACHE_SECONDS) 60 | .resourceChain(true) 61 | .addResolver(new EncodedResourceResolver()) 62 | .addResolver(new PathResourceResolver()); 63 | 64 | registry 65 | .addResourceHandler("/fonts/**") 66 | .addResourceLocations("classpath:/static/dist/fonts/") 67 | .setCachePeriod(ASSETS_CACHE_SECONDS) 68 | .resourceChain(true) 69 | .addResolver(new EncodedResourceResolver()) 70 | .addResolver(new PathResourceResolver()); 71 | 72 | registry 73 | .addResourceHandler("/img/**") 74 | .addResourceLocations("classpath:/static/dist/img/") 75 | .setCachePeriod(ASSETS_CACHE_SECONDS) 76 | .resourceChain(true) 77 | .addResolver(new EncodedResourceResolver()) 78 | .addResolver(new PathResourceResolver()); 79 | } 80 | } 81 | -------------------------------------------------------------------------------- /Website/src/main/java/com/printezisn/moviestore/website/configuration/SecurityConfiguration.java: -------------------------------------------------------------------------------- 1 | package com.printezisn.moviestore.website.configuration; 2 | 3 | import java.time.LocalDateTime; 4 | import java.time.temporal.ChronoUnit; 5 | 6 | import org.springframework.context.annotation.Configuration; 7 | import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder; 8 | import org.springframework.security.config.annotation.web.builders.HttpSecurity; 9 | import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity; 10 | import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter; 11 | import org.springframework.security.web.authentication.Http403ForbiddenEntryPoint; 12 | import org.springframework.security.web.authentication.LoginUrlAuthenticationEntryPoint; 13 | import org.springframework.security.web.util.matcher.AntPathRequestMatcher; 14 | 15 | import com.printezisn.moviestore.website.account.services.AccountService; 16 | 17 | import lombok.RequiredArgsConstructor; 18 | 19 | /** 20 | * The class with security configuration 21 | */ 22 | @Configuration 23 | @EnableWebSecurity 24 | @RequiredArgsConstructor 25 | public class SecurityConfiguration extends WebSecurityConfigurerAdapter { 26 | 27 | private static final String REMEMBER_ME_COOKIE_NAME = ".MOVIESTORE"; 28 | private static final int REMEMBER_ME_VALIDITY_SECONDS = (int) ChronoUnit.SECONDS.between(LocalDateTime.now(), 29 | LocalDateTime.now().plusMonths(3)); 30 | 31 | private final AccountAuthenticationProvider accountAuthenticationProvider; 32 | private final AccountService accountService; 33 | 34 | /** 35 | * Configures the permissions 36 | */ 37 | @Override 38 | protected void configure(final HttpSecurity http) throws Exception { 39 | http 40 | .formLogin() 41 | .loginPage("/auth/login") 42 | .permitAll() 43 | .and() 44 | .rememberMe() 45 | .tokenValiditySeconds(REMEMBER_ME_VALIDITY_SECONDS) 46 | .rememberMeCookieName(REMEMBER_ME_COOKIE_NAME) 47 | .and() 48 | .logout() 49 | .logoutSuccessUrl("/") 50 | .logoutUrl("/auth/logout") 51 | .permitAll() 52 | .and() 53 | .exceptionHandling() 54 | .defaultAuthenticationEntryPointFor( 55 | new LoginUrlAuthenticationEntryPoint("/auth/login"), 56 | new AntPathRequestMatcher("/account/changePassword")) 57 | .defaultAuthenticationEntryPointFor( 58 | new LoginUrlAuthenticationEntryPoint("/auth/login"), 59 | new AntPathRequestMatcher("/movie/new")) 60 | .defaultAuthenticationEntryPointFor( 61 | new LoginUrlAuthenticationEntryPoint("/auth/login"), 62 | new AntPathRequestMatcher("/movie/edit")) 63 | .defaultAuthenticationEntryPointFor( 64 | new LoginUrlAuthenticationEntryPoint("/auth/login"), 65 | new AntPathRequestMatcher("/movie/edit/*")) 66 | .defaultAuthenticationEntryPointFor( 67 | new LoginUrlAuthenticationEntryPoint("/auth/login"), 68 | new AntPathRequestMatcher("/movie/delete")) 69 | .defaultAuthenticationEntryPointFor( 70 | new LoginUrlAuthenticationEntryPoint("/auth/login"), 71 | new AntPathRequestMatcher("/movie/delete/*")) 72 | .defaultAuthenticationEntryPointFor( 73 | new Http403ForbiddenEntryPoint(), 74 | new AntPathRequestMatcher("/movie/like/**")) 75 | .defaultAuthenticationEntryPointFor( 76 | new Http403ForbiddenEntryPoint(), 77 | new AntPathRequestMatcher("/movie/unlike/**")) 78 | .and() 79 | .authorizeRequests() 80 | .antMatchers("/account/changePassword").authenticated() 81 | .antMatchers("/movie/new").authenticated() 82 | .antMatchers("/movie/edit").authenticated() 83 | .antMatchers("/movie/edit/*").authenticated() 84 | .antMatchers("/movie/delete").authenticated() 85 | .antMatchers("/movie/delete/*").authenticated() 86 | .antMatchers("/movie/like/**").authenticated() 87 | .antMatchers("/movie/unlike/**").authenticated() 88 | .and() 89 | .authorizeRequests() 90 | .anyRequest().permitAll(); 91 | } 92 | 93 | /** 94 | * Configures the authentication manager 95 | */ 96 | @Override 97 | protected void configure(final AuthenticationManagerBuilder auth) throws Exception { 98 | auth.authenticationProvider(accountAuthenticationProvider); 99 | auth.userDetailsService(accountService); 100 | } 101 | } 102 | -------------------------------------------------------------------------------- /Website/src/main/java/com/printezisn/moviestore/website/configuration/properties/ServiceProperties.java: -------------------------------------------------------------------------------- 1 | package com.printezisn.moviestore.website.configuration.properties; 2 | 3 | import org.springframework.boot.context.properties.ConfigurationProperties; 4 | import org.springframework.stereotype.Component; 5 | 6 | import lombok.Getter; 7 | import lombok.Setter; 8 | 9 | @Component 10 | @ConfigurationProperties(prefix = "service") 11 | @Getter 12 | @Setter 13 | public class ServiceProperties { 14 | private String accountServiceUrl; 15 | private String movieServiceUrl; 16 | } 17 | -------------------------------------------------------------------------------- /Website/src/main/java/com/printezisn/moviestore/website/configuration/rest/DefaultResponseErrorHandler.java: -------------------------------------------------------------------------------- 1 | package com.printezisn.moviestore.website.configuration.rest; 2 | 3 | import java.io.IOException; 4 | 5 | import org.springframework.http.client.ClientHttpResponse; 6 | import org.springframework.web.client.HttpClientErrorException; 7 | import org.springframework.web.client.ResponseErrorHandler; 8 | 9 | /** 10 | * The default response error handler for the inner rest services 11 | */ 12 | public class DefaultResponseErrorHandler implements ResponseErrorHandler { 13 | 14 | /** 15 | * {@inheritDoc} 16 | */ 17 | @Override 18 | public boolean hasError(final ClientHttpResponse response) throws IOException { 19 | return response.getStatusCode().is5xxServerError(); 20 | } 21 | 22 | /** 23 | * {@inheritDoc} 24 | */ 25 | @Override 26 | public void handleError(final ClientHttpResponse response) throws IOException { 27 | throw new HttpClientErrorException(response.getStatusCode(), response.getStatusText()); 28 | } 29 | 30 | } 31 | -------------------------------------------------------------------------------- /Website/src/main/java/com/printezisn/moviestore/website/movie/exceptions/MovieConditionalException.java: -------------------------------------------------------------------------------- 1 | package com.printezisn.moviestore.website.movie.exceptions; 2 | 3 | /** 4 | * Exception class thrown when there is a conditional exception while updating a 5 | * movie 6 | */ 7 | @SuppressWarnings("serial") 8 | public class MovieConditionalException extends Exception { 9 | 10 | /** 11 | * The constructor 12 | */ 13 | public MovieConditionalException() { 14 | super("The conditional update failed."); 15 | } 16 | 17 | /** 18 | * The constructor 19 | * 20 | * @param message 21 | * The exception message 22 | */ 23 | public MovieConditionalException(final String message) { 24 | super(message); 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /Website/src/main/java/com/printezisn/moviestore/website/movie/exceptions/MovieNotFoundException.java: -------------------------------------------------------------------------------- 1 | package com.printezisn.moviestore.website.movie.exceptions; 2 | 3 | /** 4 | * Exception class thrown when a movie is not found 5 | */ 6 | @SuppressWarnings("serial") 7 | public class MovieNotFoundException extends Exception { 8 | 9 | /** 10 | * The constructor 11 | */ 12 | public MovieNotFoundException() { 13 | super("The movie was not found."); 14 | } 15 | 16 | /** 17 | * The constructor 18 | * 19 | * @param message 20 | * The exception message 21 | */ 22 | public MovieNotFoundException(final String message) { 23 | super(message); 24 | } 25 | } -------------------------------------------------------------------------------- /Website/src/main/java/com/printezisn/moviestore/website/movie/exceptions/MoviePersistenceException.java: -------------------------------------------------------------------------------- 1 | package com.printezisn.moviestore.website.movie.exceptions; 2 | 3 | /** 4 | * Exception class related to persistence errors for the movie entity 5 | */ 6 | @SuppressWarnings("serial") 7 | public class MoviePersistenceException extends RuntimeException { 8 | 9 | /** 10 | * The constructor 11 | * 12 | * @param message 13 | * The exception message 14 | * @param cause 15 | * The inner exception 16 | */ 17 | public MoviePersistenceException(final String message, final Throwable cause) { 18 | super(message, cause); 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /Website/src/main/java/com/printezisn/moviestore/website/movie/models/LikeStatus.java: -------------------------------------------------------------------------------- 1 | package com.printezisn.moviestore.website.movie.models; 2 | 3 | import lombok.AllArgsConstructor; 4 | import lombok.Data; 5 | import lombok.NoArgsConstructor; 6 | 7 | /** 8 | * Model class used to represent the like status of a movie 9 | */ 10 | @Data 11 | @NoArgsConstructor 12 | @AllArgsConstructor 13 | public class LikeStatus { 14 | private int totalLikes; 15 | private boolean hasLiked; 16 | } 17 | -------------------------------------------------------------------------------- /Website/src/main/java/com/printezisn/moviestore/website/movie/services/MovieService.java: -------------------------------------------------------------------------------- 1 | package com.printezisn.moviestore.website.movie.services; 2 | 3 | import java.util.UUID; 4 | 5 | import com.printezisn.moviestore.common.dto.movie.MovieDto; 6 | import com.printezisn.moviestore.common.models.movie.MoviePagedResultModel; 7 | import com.printezisn.moviestore.common.models.movie.MovieResultModel; 8 | import com.printezisn.moviestore.website.movie.exceptions.MovieNotFoundException; 9 | 10 | /** 11 | * The interface of the movie service 12 | */ 13 | public interface MovieService { 14 | 15 | /** 16 | * Searches for movies 17 | * 18 | * @param text 19 | * The text to search for 20 | * @param pageNumber 21 | * The page number 22 | * @param sortField 23 | * The sorting field 24 | * @param isAscending 25 | * Indicates if the sorting is ascending or descending 26 | * @return The movies found 27 | */ 28 | MoviePagedResultModel searchMovies(final String text, final int pageNumber, final String sortField, 29 | final boolean isAscending); 30 | 31 | /** 32 | * Creates a new movie 33 | * 34 | * @param movieDto 35 | * The model of the new movie 36 | * @return The created movie 37 | */ 38 | MovieResultModel createMovie(final MovieDto movieDto); 39 | 40 | /** 41 | * Fetches a movie 42 | * 43 | * @param id 44 | * The id of the movie 45 | * @return The movie found 46 | * @throws MovieNotFoundException 47 | * Exception thrown when the movie is not found 48 | */ 49 | MovieDto getMovie(final UUID id) throws MovieNotFoundException; 50 | 51 | /** 52 | * Checks if an account is authorized to update or delete a movie 53 | * 54 | * @param account 55 | * The account to check 56 | * @param movieId 57 | * The id of the movie to check 58 | * @return True if the account is authorized, otherwise false 59 | * @throws MovieNotFoundException 60 | * Exception thrown when the movie is not found 61 | */ 62 | boolean isAuthorizedOnMovie(final String account, final UUID movieId) throws MovieNotFoundException; 63 | 64 | /** 65 | * Checks if an account is authorized to update or delete a movie 66 | * 67 | * @param account 68 | * The account to check 69 | * @param movieDto 70 | * The movie to check 71 | * @return True if the account is authorized, otherwise false 72 | */ 73 | boolean isAuthorizedOnMovie(final String account, final MovieDto movieDto); 74 | 75 | /** 76 | * Updates a movie 77 | * 78 | * @param movieDto 79 | * The model of the movie 80 | * @return The updated movie 81 | * @throws MovieNotFoundException 82 | * Exception thrown when the movie is not found 83 | */ 84 | MovieResultModel updateMovie(final MovieDto movieDto) throws MovieNotFoundException; 85 | 86 | /** 87 | * Deletes a movie 88 | * 89 | * @param movieId 90 | * The id of the movie 91 | */ 92 | void deleteMovie(final UUID movieId); 93 | 94 | /** 95 | * Adds a like to a movie 96 | * 97 | * @param account 98 | * The account that likes the movie 99 | * @param movieId 100 | * The id of the movie 101 | * @throws MovieNotFoundException 102 | * Exception thrown when the movie is not found 103 | */ 104 | void likeMovie(final String account, final UUID movieId) throws MovieNotFoundException; 105 | 106 | /** 107 | * Removes a like from a movie 108 | * 109 | * @param account 110 | * The account that unlikes the movie 111 | * @param movieId 112 | * The id of the movie 113 | * @throws MovieNotFoundException 114 | * Exception thrown when the movie is not found 115 | */ 116 | void unlikeMovie(final String account, final UUID movieId) throws MovieNotFoundException; 117 | 118 | /** 119 | * Checks if an account has liked a movie 120 | * 121 | * @param account 122 | * The account to check 123 | * @param movieId 124 | * The id of the movie to check 125 | * @return True if the account has liked the movie, otherwise false 126 | */ 127 | boolean hasLiked(final String account, final UUID movieId); 128 | } 129 | -------------------------------------------------------------------------------- /Website/src/main/resources/application-test.properties: -------------------------------------------------------------------------------- 1 | server.port=10100 2 | 3 | service.accountServiceUrl=http://localhost:8100 4 | service.movieServiceUrl=http://localhost:9100 -------------------------------------------------------------------------------- /Website/src/main/resources/application.properties: -------------------------------------------------------------------------------- 1 | server.port=10000 2 | 3 | service.accountServiceUrl=http://localhost:8000 4 | service.movieServiceUrl=http://localhost:9000 -------------------------------------------------------------------------------- /Website/src/main/resources/i18n/labels/labels.properties: -------------------------------------------------------------------------------- 1 | label.login.username=Username 2 | label.login.password=Password 3 | label.login.rememberMe=Remember me 4 | label.login.signIn=Sign In 5 | 6 | label.register.username=Username 7 | label.register.emailAddress=Email 8 | label.register.password=Password 9 | label.register.confirmPassword=Confirm Password 10 | label.register.submit=Submit 11 | 12 | label.changePassword.currentPassword=Current Password 13 | label.changePassword.newPassword=New Password 14 | label.changePassword.confirmNewPassword=Confirm New Password 15 | label.changePassword.submit=Submit 16 | 17 | label.movie.title=Title 18 | label.movie.description=Description 19 | label.movie.rating=Rating (0-10) 20 | label.movie.rating.minimal=Rating 21 | label.movie.releaseYear=Release Year 22 | label.movie.totalLikes=Total Likes 23 | label.movie.submit=Submit 24 | 25 | label.search=Search 26 | label.previous=Previous 27 | label.next=Next 28 | label.goBack=Go Back 29 | label.edit=Edit 30 | label.delete=Delete 31 | label.cancel=Cancel 32 | label.confirmDelete=Confirm Deletion 33 | label.like=Like 34 | label.unlike=Unlike -------------------------------------------------------------------------------- /Website/src/main/resources/i18n/messages/messages.properties: -------------------------------------------------------------------------------- 1 | message.error.unexpectedError = "An unexpected error occurred. Please try again." 2 | 3 | message.error.loginFailed=The username or password you entered is invalid. 4 | message.error.usernameRequired=The username is required. 5 | message.error.emailAddressRequired=The email address is required. 6 | message.error.passwordRequired=The password is required. 7 | message.error.confirmPasswordRequired=The password confirmation is required. 8 | message.error.confirmPasswordNoMatch=The password confirmation must match the password. 9 | 10 | message.changePassword.error.currentPasswordRequired=The current password is required. 11 | message.changePassword.error.newPasswordRequired=The new password is required. 12 | message.changePassword.error.newPasswordMaxLength=The new password may have 250 characters maximum. 13 | message.changePassword.error.newPasswordFormat=The new password must contain at least 8 characters, one lower case and one upper case character, one digit and one special character (!._, etc). 14 | message.error.confirmNewPasswordRequired=The new password confirmation is required. 15 | message.error.confirmNewPasswordNoMatch=The password confirmation must match the new password. 16 | 17 | message.logout=Log Out 18 | 19 | message.registerSuccess=You have registered successfully! 20 | message.changePasswordSuccess=You have changed your password successfully! 21 | message.changePassword.invalidCurrentPassword=The current password is not correct. 22 | 23 | message.movie.error.titleRequired=The title is required. 24 | message.movie.error.titleMaxLength=The title may have 250 characters maximum. 25 | message.movie.error.descriptionRequired=The description is required. 26 | message.movie.error.ratingRequired=The rating is required. 27 | message.movie.error.ratingMaxValue=The maximum value for rating is 10. 28 | message.movie.error.ratingMinValue=The minimum value for rating is 0. 29 | message.movie.error.releaseYearRequired=The release year is required. 30 | message.error.typeMismatch.rating=The rating value is invalid. 31 | message.error.movieNotFound=The movie was not found. 32 | message.error.movieEdit.notAuthorized=You are not authorized to edit this movie. 33 | message.error.movieDelete.notAuthorized=You are not authorized to delete this movie. 34 | 35 | message.createMovieSuccess=You have created the movie successfully! 36 | message.updateMovieSuccess=You have updated the movie successfully! 37 | message.deleteMovieSuccess=You have deleted the movie successfully! -------------------------------------------------------------------------------- /Website/src/main/resources/i18n/pages/pages.properties: -------------------------------------------------------------------------------- 1 | page.home=Home 2 | page.login=Sign In 3 | page.register=Register 4 | page.newMovie=New Movie 5 | page.changePassword=Change Password 6 | page.editMovie=Edit Movie 7 | page.deleteMovie=Delete Movie 8 | 9 | page.home.title=Movie Store 10 | page.home.subtitle=Where the force is always with you 11 | page.login.title=Sign In 12 | page.login.subtitle=Sign in with your account 13 | page.register.title=Register 14 | page.register.subtitle=Create a new account 15 | page.newMovie.title=New Movie 16 | page.newMovie.subtitle=Create a new movie 17 | page.changePassword.title=Change Password 18 | page.changePassword.subtitle=Change your account password 19 | page.movieDetails.title=Movie Details 20 | page.editMovie.title=Edit Movie 21 | page.deleteMovie.title=Delete Movie -------------------------------------------------------------------------------- /Website/src/main/resources/logback-spring.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 8 | 9 | 10 | %black(%d{ISO8601}) %highlight(%-5level) 11 | [%blue(%t)] %yellow(%C{1.}): %msg%n%throwable 12 | 13 | 14 | 15 | 16 | 18 | ${LOGS}/application.log 19 | 21 | %d %p %C{1.} [%t] %m%n 22 | 23 | 24 | 26 | 27 | ${LOGS}/archived/%d{yyyy-MM}/%d{dd}/%d{HH}/application-%d{yyyy-MM-dd-HH}.%i.log 28 | 29 | 31 | 10MB 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 45 | 46 | 47 | 48 | -------------------------------------------------------------------------------- /Website/src/main/resources/static/app/app.js: -------------------------------------------------------------------------------- 1 | import 'bulma/bulma.sass'; 2 | import '@fortawesome/fontawesome-free/scss/solid.scss'; 3 | import '@fortawesome/fontawesome-free/scss/fontawesome.scss'; 4 | import './sass/app.scss'; 5 | 6 | const initNavbar = () => { 7 | const burgers = Array.prototype.slice.call(document.querySelectorAll('.navbar-burger')); 8 | burgers.forEach(el => { 9 | el.addEventListener('click', () => { 10 | const targetName = el.dataset.target; 11 | const target = document.getElementById(targetName); 12 | 13 | el.classList.toggle('is-active'); 14 | target.classList.toggle('is-active'); 15 | }); 16 | }); 17 | }; 18 | 19 | const initNotifications = () => { 20 | const notifications = Array.prototype.slice.call(document.querySelectorAll('.notification')); 21 | notifications.forEach(notification => { 22 | const deleteButton = notification.querySelector('.delete'); 23 | if (deleteButton) { 24 | deleteButton.addEventListener('click', () => { 25 | notification.parentNode.removeChild(notification); 26 | }); 27 | } 28 | }); 29 | }; 30 | 31 | const initToastrNotifications = () => { 32 | const toastrNotifications = Array.prototype.slice.call(document.querySelectorAll('.toastr-notification')); 33 | toastrNotifications.forEach(toastrNotification => { 34 | const title = toastrNotification.getAttribute("notification-title"); 35 | const message = toastrNotification.innerHTML; 36 | const type = toastrNotification.getAttribute('notification-type'); 37 | 38 | import(/* webpackChunkName: 'toastr' */ './js/toastr.js').then(({ showToastrNotification }) => { 39 | showToastrNotification(type, title, message); 40 | }); 41 | }); 42 | }; 43 | 44 | const initFormValidate = () => { 45 | const forms = Array.prototype.slice.call(document.querySelectorAll('.validate-form')); 46 | forms.forEach(form => { 47 | import(/* webpackChunkName: 'validate' */ './js/validate').then(({ initValidate }) => { 48 | initValidate(form); 49 | }); 50 | }); 51 | }; 52 | 53 | const initLikeStatus = () => { 54 | const elements = Array.prototype.slice.call(document.querySelectorAll('.like-status')); 55 | elements.forEach(element => { 56 | import(/* webpackChunkName: 'likeStatus' */ './js/likeStatus').then(({ createLikeStatus }) => { 57 | createLikeStatus(element); 58 | }); 59 | }); 60 | }; 61 | 62 | const init = () => { 63 | initNavbar(); 64 | initNotifications(); 65 | initToastrNotifications(); 66 | initFormValidate(); 67 | initLikeStatus(); 68 | }; 69 | 70 | if(document.readyState !== 'loading') { 71 | init(); 72 | } 73 | else { 74 | document.addEventListener('DOMContentLoaded', () => { 75 | init(); 76 | }); 77 | } -------------------------------------------------------------------------------- /Website/src/main/resources/static/app/js/likeStatus.js: -------------------------------------------------------------------------------- 1 | const load = (movieId, totalLikes, hideOnLoad, showOnLoad, hideOnLike, showOnLike) => { 2 | initLoading(hideOnLoad, showOnLoad); 3 | 4 | fetch('/movie/likestatus/' + movieId, { 5 | method : 'GET', 6 | headers : { 7 | 'Accept' : 'application/json' 8 | }, 9 | credentials : 'same-origin' 10 | }) 11 | .then(response => response.json()) 12 | .then(response => { 13 | showOnLoad.forEach(el => el.style.display = 'none'); 14 | hideOnLoad.forEach(el => el.style.display = ''); 15 | 16 | showOnLike.forEach(el => el.style.display = (response.hasLiked ? '' : 'none')); 17 | hideOnLike.forEach(el => el.style.display = (response.hasLiked ? 'none' : '')); 18 | 19 | totalLikes.innerHTML = response.totalLikes; 20 | }) 21 | }; 22 | 23 | const initLoading = (hideOnLoad, showOnLoad) => { 24 | showOnLoad.forEach(el => el.style.display = ''); 25 | hideOnLoad.forEach(el => el.style.display = 'none'); 26 | }; 27 | 28 | export const createLikeStatus = element => { 29 | const movieId = element.getAttribute('movie-id'); 30 | const csrfHeader = document.querySelector('meta[name="_csrf_header"]').content; 31 | const csrf = document.querySelector('meta[name="_csrf"]').content; 32 | const totalLikes = element.querySelector('.total-likes'); 33 | const hideOnLoad = Array.prototype.slice.call(element.querySelectorAll('.hide-on-load')); 34 | const showOnLoad = Array.prototype.slice.call(element.querySelectorAll('.show-on-load')); 35 | const hideOnLike = Array.prototype.slice.call(element.querySelectorAll('.hide-on-like')); 36 | const showOnLike = Array.prototype.slice.call(element.querySelectorAll('.show-on-like')); 37 | const likeButtons = Array.prototype.slice.call(element.querySelectorAll('.like-button')); 38 | const unlikeButtons = Array.prototype.slice.call(element.querySelectorAll('.unlike-button')); 39 | 40 | const headers = { 41 | 'Content-Type' : 'application/x-www-form-urlencoded', 42 | 'Accept' : 'application/json' 43 | }; 44 | headers[csrfHeader] = csrf; 45 | 46 | likeButtons.forEach(button => { 47 | button.addEventListener('click', () => { 48 | initLoading(hideOnLoad, showOnLoad); 49 | 50 | fetch('/movie/like', { 51 | method : 'post', 52 | headers : headers, 53 | credentials : 'same-origin', 54 | body : `id=${movieId}`, 55 | }) 56 | .then(() => load(movieId, totalLikes, hideOnLoad, showOnLoad, hideOnLike, showOnLike)); 57 | }); 58 | }); 59 | 60 | unlikeButtons.forEach(button => { 61 | button.addEventListener('click', () => { 62 | initLoading(hideOnLoad, showOnLoad); 63 | 64 | fetch('/movie/unlike', { 65 | method : 'post', 66 | headers : headers, 67 | credentials : 'same-origin', 68 | body : `id=${movieId}`, 69 | }) 70 | .then(() => load(movieId, totalLikes, hideOnLoad, showOnLoad, hideOnLike, showOnLike)); 71 | }); 72 | }); 73 | 74 | load(movieId, totalLikes, hideOnLoad, showOnLoad, hideOnLike, showOnLike); 75 | }; -------------------------------------------------------------------------------- /Website/src/main/resources/static/app/js/polyfills.js: -------------------------------------------------------------------------------- 1 | import 'es6-promise/auto'; 2 | import 'whatwg-fetch'; -------------------------------------------------------------------------------- /Website/src/main/resources/static/app/js/toastr.js: -------------------------------------------------------------------------------- 1 | import 'toastr/toastr.scss'; 2 | import '../sass/toastr-overrides.scss'; 3 | 4 | import toastr from 'toastr'; 5 | 6 | export const showToastrNotification = (type, title, message) => { 7 | switch (type) { 8 | case "success": 9 | toastr.success(message, title); 10 | break; 11 | case "warning": 12 | toastr.warning(message, title); 13 | break; 14 | case "error": 15 | toastr.error(message, title); 16 | break; 17 | default: 18 | toastr.info(message, title); 19 | break; 20 | } 21 | }; -------------------------------------------------------------------------------- /Website/src/main/resources/static/app/js/validate.js: -------------------------------------------------------------------------------- 1 | import FormValidator from 'validate-js'; 2 | 3 | const cleanErrors = element => { 4 | element.classList.remove('is-danger'); 5 | 6 | const messageElement = document.getElementById(element.id + '-message'); 7 | messageElement.style.display = 'none'; 8 | }; 9 | 10 | const showError = (element, rule) => { 11 | element.classList.add('is-danger'); 12 | 13 | const messageElement = document.getElementById(element.id + '-message'); 14 | switch (rule) { 15 | case 'required': 16 | messageElement.innerHTML = element.getAttribute('required-field'); 17 | break; 18 | case 'matches': 19 | messageElement.innerHTML = element.getAttribute('matches-field-error'); 20 | break; 21 | } 22 | 23 | messageElement.style.display = ''; 24 | }; 25 | 26 | export const initValidate = form => { 27 | let fields = []; 28 | const elements = Array.prototype.slice.call(form.querySelectorAll('.input,.textarea')); 29 | 30 | elements.forEach(element => { 31 | let rules = []; 32 | 33 | if (element.getAttribute('required-field')) { 34 | rules.push('required'); 35 | } 36 | if (element.getAttribute('matches-field')) { 37 | rules.push('matches[' + element.getAttribute('matches-field') + ']'); 38 | } 39 | 40 | fields.push({ 41 | name : element.name, 42 | rules : rules.join('|') 43 | }); 44 | }); 45 | 46 | new FormValidator(form.name, fields, errors => { 47 | elements.forEach(element => { 48 | cleanErrors(element); 49 | errors.forEach(error => { 50 | if (error.name === element.name) { 51 | showError(element, error.rule); 52 | } 53 | }); 54 | }); 55 | }); 56 | }; -------------------------------------------------------------------------------- /Website/src/main/resources/static/app/sass/_colors.scss: -------------------------------------------------------------------------------- 1 | $moviestore-red: #ff3860; -------------------------------------------------------------------------------- /Website/src/main/resources/static/app/sass/app.scss: -------------------------------------------------------------------------------- 1 | @import "_colors"; 2 | 3 | .small-form { 4 | max-width: 700px; 5 | margin: auto; 6 | } 7 | 8 | .big-form { 9 | max-width: 900px; 10 | margin: auto; 11 | } 12 | 13 | .error-list { 14 | list-style: disc; 15 | margin-left: 10px; 16 | } 17 | 18 | .required-field span { 19 | padding-right: 5px; 20 | display: inline-block; 21 | position: relative; 22 | } 23 | 24 | .required-field span:after { 25 | content: '*'; 26 | color: $moviestore-red; 27 | position: absolute; 28 | top: 0; 29 | right: -5px; 30 | } -------------------------------------------------------------------------------- /Website/src/main/resources/static/app/sass/toastr-overrides.scss: -------------------------------------------------------------------------------- 1 | #toast-container { 2 | z-index: 100 !important; 3 | } -------------------------------------------------------------------------------- /Website/src/main/resources/static/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "moviestore", 3 | "version": "1.0.0", 4 | "description": "", 5 | "main": "index.js", 6 | "scripts": { 7 | "build": "webpack --config ./webpack.dev.js", 8 | "build-prod": "webpack --config ./webpack.prod.js", 9 | "webpack-stats": "webpack --config ./webpack.prod.js --profile --json > webpack-stats.json" 10 | }, 11 | "author": "", 12 | "license": "MIT", 13 | "devDependencies": { 14 | "@babel/core": "^7.9.0", 15 | "@babel/plugin-syntax-dynamic-import": "^7.8.3", 16 | "@babel/preset-env": "^7.9.0", 17 | "@fortawesome/fontawesome-free": "^5.13.0", 18 | "babel-loader": "^8.1.0", 19 | "bulma": "^0.7.5", 20 | "clean-webpack-plugin": "^1.0.1", 21 | "compression-webpack-plugin": "^2.0.0", 22 | "critters-webpack-plugin": "^2.5.0", 23 | "css-loader": "^1.0.1", 24 | "es6-promise": "^4.2.8", 25 | "file-loader": "^2.0.0", 26 | "glob-all": "^3.2.1", 27 | "html-loader": "^0.5.5", 28 | "html-webpack-plugin": "^3.2.0", 29 | "mini-css-extract-plugin": "^0.4.5", 30 | "node-sass": "^4.13.1", 31 | "optimize-css-assets-webpack-plugin": "^5.0.3", 32 | "sass-loader": "^7.3.1", 33 | "script-ext-html-webpack-plugin": "^2.1.4", 34 | "toastr": "^2.1.4", 35 | "uglifyjs-webpack-plugin": "^2.2.0", 36 | "validate-js": "^2.0.1", 37 | "webpack": "^4.42.1", 38 | "webpack-cli": "^3.3.11", 39 | "webpack-merge": "^4.2.2", 40 | "whatwg-fetch": "^3.0.0" 41 | }, 42 | "dependencies": {} 43 | } 44 | -------------------------------------------------------------------------------- /Website/src/main/resources/static/webpack.common.js: -------------------------------------------------------------------------------- 1 | const path = require('path'); 2 | const HtmlWebpackPlugin = require('html-webpack-plugin'); 3 | const ScriptExtHtmlWebpackPlugin = require('script-ext-html-webpack-plugin'); 4 | const CleanWebpackPlugin = require('clean-webpack-plugin'); 5 | const MiniCssExtractPlugin = require('mini-css-extract-plugin'); 6 | const webpack = require('webpack'); 7 | 8 | module.exports = { 9 | cache : true, 10 | entry : { 11 | polyfills : path.resolve(__dirname, 'app/js/polyfills.js'), 12 | app : path.resolve(__dirname, 'app/app.js') 13 | }, 14 | output : { 15 | publicPath : '/', 16 | path : path.resolve(__dirname, 'dist/'), 17 | filename : '[name]-[chunkhash].min.js' 18 | }, 19 | resolve : { 20 | extensions : [ '.js' ] 21 | }, 22 | module : { 23 | rules : [ 24 | { 25 | test : /\.js$/, 26 | use : { 27 | loader : 'babel-loader', 28 | options : { 29 | presets : [ '@babel/preset-env' ], 30 | plugins : [ '@babel/syntax-dynamic-import' ] 31 | } 32 | }, 33 | exclude : [ path.resolve(__dirname, 'node_modules') ] 34 | }, 35 | { 36 | test : /\.(scss|sass)$/, 37 | use : [ 38 | MiniCssExtractPlugin.loader, 39 | 'css-loader', 40 | 'sass-loader' 41 | ] 42 | }, 43 | { 44 | test : /\.(png|jpg|jpeg|gif|svg)$/, 45 | use : { 46 | loader : 'file-loader', 47 | options : { 48 | name : 'img/[name].[ext]' 49 | } 50 | } 51 | }, 52 | { 53 | test : /\.(ttf|woff|woff2|eot)$/, 54 | use : { 55 | loader : 'file-loader', 56 | options : { 57 | name : 'fonts/[name].[ext]' 58 | } 59 | } 60 | } 61 | ] 62 | }, 63 | plugins : [ 64 | new CleanWebpackPlugin([ 'dist' ]), 65 | new MiniCssExtractPlugin({ 66 | filename : '[name]-[chunkhash].min.css' 67 | }), 68 | new HtmlWebpackPlugin({ 69 | inject : true, 70 | template : '!!html-loader!../templates/layout-template.html', 71 | filename : '../../templates/layout.html' 72 | }), 73 | new ScriptExtHtmlWebpackPlugin({ 74 | defer : /\.+/, 75 | }), 76 | new webpack.HashedModuleIdsPlugin() 77 | ], 78 | optimization : { 79 | splitChunks : { 80 | cacheGroups : { 81 | vendors : false 82 | } 83 | }, 84 | runtimeChunk : { 85 | name : 'runtime.min.js' 86 | } 87 | } 88 | }; -------------------------------------------------------------------------------- /Website/src/main/resources/static/webpack.dev.js: -------------------------------------------------------------------------------- 1 | const merge = require('webpack-merge'); 2 | const common = require('./webpack.common.js'); 3 | 4 | module.exports = merge(common, { 5 | watch : true, 6 | mode : 'development' 7 | }); -------------------------------------------------------------------------------- /Website/src/main/resources/static/webpack.prod.js: -------------------------------------------------------------------------------- 1 | const merge = require('webpack-merge'); 2 | const common = require('./webpack.common.js'); 3 | 4 | const path = require('path'); 5 | const glob = require('glob-all'); 6 | const webpack = require('webpack'); 7 | const UglifyJSPlugin = require('uglifyjs-webpack-plugin'); 8 | const OptimizeCssAssetsPlugin = require('optimize-css-assets-webpack-plugin'); 9 | const Critters = require('critters-webpack-plugin'); 10 | const CompressionPlugin = require('compression-webpack-plugin'); 11 | 12 | module.exports = merge(common, { 13 | devtool : 'source-map', 14 | mode : 'production', 15 | plugins : [ 16 | new OptimizeCssAssetsPlugin({ 17 | assetNameRegExp : /\.min.css$/ 18 | }), 19 | new UglifyJSPlugin({ 20 | sourceMap : true, 21 | extractComments : true, 22 | parallel : true 23 | }), 24 | new CompressionPlugin(), 25 | new Critters({ 26 | preload : 'js', 27 | preloadFonts : true, 28 | noscriptFallback : false, 29 | pruneSource : false, 30 | mergeStylesheets: false 31 | }), 32 | new webpack.DefinePlugin({ 33 | 'process.env.NODE_ENV' : JSON.stringify('production') 34 | }) 35 | ] 36 | }); -------------------------------------------------------------------------------- /Website/src/main/resources/templates/account/changePassword.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 8 | 9 | 10 | 11 | 12 |

14 |

16 |
17 |
19 | 20 |
    21 |
  • 22 |
23 |
24 |
27 |
28 |
29 | 34 |
35 |
36 |
37 |
38 | 42 | 45 |
46 |
47 |
48 |
49 |
50 |
51 | 56 |
57 |
58 |
59 |
60 | 63 | 66 |
67 |
68 |
69 |
70 |
71 |
72 | 77 |
78 |
79 |
80 |
81 | 86 | 89 |
90 |
91 |
92 |
93 |
94 |
95 |
96 |
97 |
98 | 101 |
102 |
103 |
104 |
105 |
106 |
107 |
108 | 109 | -------------------------------------------------------------------------------- /Website/src/main/resources/templates/account/register.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 |

13 |

15 |
16 |
18 | 19 |
    20 |
  • 21 |
22 |
23 |
27 |
28 |
29 | 33 |
34 |
35 |
36 |
37 | 41 | 44 |
45 |
46 |
47 |
48 |
49 |
50 | 55 |
56 |
57 |
58 |
59 | 64 | 67 |
68 |
69 |
70 |
71 |
72 |
73 | 77 |
78 |
79 |
80 |
81 | 84 | 87 |
88 |
89 |
90 |
91 |
92 |
93 | 98 |
99 |
100 |
101 |
102 | 107 | 110 |
111 |
112 |
113 |
114 |
115 |
116 |
117 |
118 |
119 | 122 |
123 |
124 |
125 |
126 |
127 |
128 |
129 | 130 | -------------------------------------------------------------------------------- /Website/src/main/resources/templates/auth/login.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 |

13 |

15 |
16 |

17 | 18 | 19 |

20 |
22 |
23 |
24 | 28 |
29 |
30 |
31 |
32 | 35 | 38 |
39 |
40 |
41 |
42 |
43 |
44 | 48 |
49 |
50 |
51 |
52 | 55 | 58 |
59 |
60 |
61 |
62 |
63 |
64 |
65 |
66 |
67 | 74 |
75 |
76 |
77 |
78 |
79 |
80 |
81 |
82 |
83 | 86 |
87 |
88 |
89 |
90 |
91 |
92 |
93 | 94 | -------------------------------------------------------------------------------- /Website/src/main/resources/templates/fragments/likeStatus.html: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /Website/src/main/resources/templates/layout-template.html: -------------------------------------------------------------------------------- 1 | 2 | 4 | 5 | Movie 6 | Store 7 | 8 | 9 | 10 | 11 | 12 | 58 | 59 |
60 | 62 | 63 | 64 | 65 | 67 | 68 | 72 | 73 | -------------------------------------------------------------------------------- /Website/src/main/resources/templates/movie/delete.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 |

13 |

14 |
15 |
16 |
17 | 20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 | 32 |
33 |
34 |
35 |
37 |
38 |
39 |
40 |
41 |
42 | 45 |
46 |
47 |
48 |
50 |
51 |
52 |
53 |
54 |
55 | 58 |
59 |
60 |
61 |
63 |
64 |
65 |
66 |
67 |
68 | 71 |
72 |
73 |
74 |
76 |
77 |
78 |
79 |
80 |
81 |
82 |
83 |
84 |
86 | 89 |   90 | 92 | 94 | 101 |
102 |
103 |
104 |
105 |
106 |
107 |
108 | 109 | -------------------------------------------------------------------------------- /Website/src/main/resources/templates/movie/details.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 8 | 9 | 10 | 11 | 12 | 13 | 14 |

16 |

17 |
18 |
19 |
20 | 23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 | 35 |
36 |
37 |
38 |
40 |
41 |
42 |
43 |
44 |
45 | 48 |
49 |
50 |
51 |
53 |
54 |
55 |
56 |
57 |
58 | 61 |
62 |
63 |
64 |
66 |
67 |
68 |
69 |
70 |
71 | 74 |
75 |
76 |
77 |
79 |
80 |
81 |
82 |
83 |
84 |
85 | 110 |
111 |
112 |
113 |
114 | 115 | -------------------------------------------------------------------------------- /Website/src/test/java/com/printezisn/moviestore/website/account/controllers/AuthControllerTest.java: -------------------------------------------------------------------------------- 1 | package com.printezisn.moviestore.website.account.controllers; 2 | 3 | import org.junit.Before; 4 | import org.junit.Test; 5 | import org.mockito.Mock; 6 | import org.mockito.MockitoAnnotations; 7 | import org.springframework.context.MessageSource; 8 | import org.springframework.test.web.servlet.MockMvc; 9 | import org.springframework.test.web.servlet.setup.MockMvcBuilders; 10 | 11 | import com.printezisn.moviestore.common.AppUtils; 12 | 13 | import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; 14 | import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; 15 | import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.view; 16 | 17 | /** 18 | * Contains unit tests for the auth controller 19 | */ 20 | public class AuthControllerTest { 21 | 22 | @Mock 23 | private MessageSource messageSource; 24 | 25 | private AppUtils appUtils; 26 | 27 | private AuthController authController; 28 | 29 | private MockMvc mockMvc; 30 | 31 | /** 32 | * Initializes the test class 33 | */ 34 | @Before 35 | public void setUp() { 36 | MockitoAnnotations.initMocks(this); 37 | 38 | appUtils = new AppUtils(messageSource); 39 | 40 | authController = new AuthController(appUtils); 41 | 42 | mockMvc = MockMvcBuilders.standaloneSetup(authController).build(); 43 | } 44 | 45 | /** 46 | * Tests if the login page is rendered successfully 47 | */ 48 | @Test 49 | public void test_login_success() throws Exception { 50 | mockMvc.perform(get("/auth/login")) 51 | .andExpect(status().isOk()) 52 | .andExpect(view().name("auth/login")); 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /Website/src/test/java/com/printezisn/moviestore/website/configuration/AccountAuthenticationProviderTest.java: -------------------------------------------------------------------------------- 1 | package com.printezisn.moviestore.website.configuration; 2 | 3 | import org.junit.Before; 4 | import org.junit.Test; 5 | import org.mockito.Mock; 6 | import org.mockito.MockitoAnnotations; 7 | import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; 8 | import org.springframework.security.core.Authentication; 9 | 10 | import com.printezisn.moviestore.website.account.exceptions.AccountAuthenticationException; 11 | import com.printezisn.moviestore.website.account.exceptions.AccountNotValidatedException; 12 | import com.printezisn.moviestore.website.account.models.AuthenticatedUser; 13 | import com.printezisn.moviestore.website.account.services.AccountService; 14 | 15 | import static org.junit.Assert.assertEquals; 16 | import static org.junit.Assert.assertNull; 17 | import static org.mockito.Mockito.when; 18 | 19 | import java.util.ArrayList; 20 | 21 | /** 22 | * Contains unit tests for the AccountAuthenticationProvider class 23 | */ 24 | public class AccountAuthenticationProviderTest { 25 | 26 | private static final String USERNAME = "username"; 27 | private static final String PASSWORD = "password"; 28 | private static final String EMAIL_ADDRESS = "email"; 29 | 30 | @Mock 31 | private AccountService accountService; 32 | 33 | @Mock 34 | private Authentication authentication; 35 | 36 | private AccountAuthenticationProvider accountAuthenticationProvider; 37 | 38 | /** 39 | * Initializes the test class 40 | */ 41 | @Before 42 | public void setUp() { 43 | MockitoAnnotations.initMocks(this); 44 | 45 | when(authentication.getName()).thenReturn(USERNAME); 46 | when(authentication.getCredentials()).thenReturn(PASSWORD); 47 | 48 | accountAuthenticationProvider = new AccountAuthenticationProvider(accountService); 49 | } 50 | 51 | /** 52 | * Tests the scenario in which the authentication is successful 53 | */ 54 | @Test 55 | public void test_authenticate_success() throws Exception { 56 | final AuthenticatedUser authenticatedUser = new AuthenticatedUser( 57 | USERNAME, PASSWORD, EMAIL_ADDRESS, new ArrayList<>()); 58 | 59 | when(accountService.authenticate(USERNAME, PASSWORD)).thenReturn(authenticatedUser); 60 | 61 | final UsernamePasswordAuthenticationToken token = (UsernamePasswordAuthenticationToken) accountAuthenticationProvider 62 | .authenticate(authentication); 63 | final AuthenticatedUser result = (AuthenticatedUser) token.getPrincipal(); 64 | 65 | assertEquals(USERNAME, result.getUsername()); 66 | assertEquals(PASSWORD, result.getPassword()); 67 | assertEquals(EMAIL_ADDRESS, result.getEmailAddress()); 68 | } 69 | 70 | /** 71 | * Tests the scenario in which the authentication fails 72 | */ 73 | @Test 74 | public void test_authenticate_fail() throws Exception { 75 | when(accountService.authenticate(USERNAME, PASSWORD)).thenThrow(new AccountNotValidatedException()); 76 | 77 | final Authentication result = accountAuthenticationProvider.authenticate(authentication); 78 | 79 | assertNull(result); 80 | } 81 | 82 | /** 83 | * Tests the scenario in which the authentication throws an exception 84 | */ 85 | @Test(expected = AccountAuthenticationException.class) 86 | public void test_authenticate_exception() throws Exception { 87 | when(accountService.authenticate(USERNAME, PASSWORD)).thenThrow(new AccountAuthenticationException("test")); 88 | accountAuthenticationProvider.authenticate(authentication); 89 | } 90 | } 91 | -------------------------------------------------------------------------------- /Website/src/test/java/com/printezisn/moviestore/website/configuration/rest/DefaultResponseErrorHandlerTest.java: -------------------------------------------------------------------------------- 1 | package com.printezisn.moviestore.website.configuration.rest; 2 | 3 | import static org.junit.Assert.assertEquals; 4 | import static org.junit.Assert.assertFalse; 5 | import static org.junit.Assert.assertTrue; 6 | import static org.junit.Assert.fail; 7 | import static org.mockito.Mockito.when; 8 | 9 | import java.io.IOException; 10 | import java.util.Arrays; 11 | 12 | import org.junit.Before; 13 | import org.junit.Test; 14 | import org.mockito.Mock; 15 | import org.mockito.MockitoAnnotations; 16 | import org.springframework.http.HttpStatus; 17 | import org.springframework.http.client.ClientHttpResponse; 18 | import org.springframework.web.client.HttpClientErrorException; 19 | 20 | /** 21 | * Contains unit tests for the DefaultResponseErrorHandler class 22 | */ 23 | public class DefaultResponseErrorHandlerTest { 24 | 25 | @Mock 26 | private ClientHttpResponse clientHttpResponse; 27 | 28 | private DefaultResponseErrorHandler errorHandler; 29 | 30 | /** 31 | * Initializes the test class 32 | */ 33 | @Before 34 | public void setUp() { 35 | MockitoAnnotations.initMocks(this); 36 | 37 | errorHandler = new DefaultResponseErrorHandler(); 38 | } 39 | 40 | /** 41 | * Tests to ensure that only 5xx status codes are considered as errors 42 | */ 43 | @Test 44 | public void test_hasError_onlyServerErrors() { 45 | Arrays.stream(HttpStatus.values()).forEach(statusCode -> { 46 | try { 47 | when(clientHttpResponse.getStatusCode()).thenReturn(statusCode); 48 | 49 | final boolean result = errorHandler.hasError(clientHttpResponse); 50 | if (statusCode.is5xxServerError()) { 51 | assertTrue(result); 52 | } 53 | else { 54 | assertFalse(result); 55 | } 56 | } 57 | catch (final IOException ex) { 58 | throw new RuntimeException(ex); 59 | } 60 | }); 61 | } 62 | 63 | /** 64 | * Tests if the correct exception is thrown in case of an error 65 | */ 66 | @Test 67 | public void test_handleError_success() throws IOException { 68 | final HttpStatus statusCode = HttpStatus.BAD_REQUEST; 69 | final String statusText = "test text"; 70 | 71 | when(clientHttpResponse.getStatusCode()).thenReturn(statusCode); 72 | when(clientHttpResponse.getStatusText()).thenReturn(statusText); 73 | 74 | try { 75 | errorHandler.handleError(clientHttpResponse); 76 | fail(); 77 | } 78 | catch (final HttpClientErrorException ex) { 79 | assertEquals(statusCode, ex.getStatusCode()); 80 | assertEquals(statusText, ex.getStatusText()); 81 | } 82 | } 83 | } 84 | -------------------------------------------------------------------------------- /Website/src/test/java/com/printezisn/moviestore/website/integ/AccountIntegrationTest.java: -------------------------------------------------------------------------------- 1 | package com.printezisn.moviestore.website.integ; 2 | 3 | import static org.springframework.security.test.web.servlet.request.SecurityMockMvcRequestBuilders.formLogin; 4 | import static org.springframework.security.test.web.servlet.request.SecurityMockMvcRequestPostProcessors.csrf; 5 | import static org.springframework.security.test.web.servlet.request.SecurityMockMvcRequestPostProcessors.user; 6 | import static org.springframework.security.test.web.servlet.response.SecurityMockMvcResultMatchers.authenticated; 7 | import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; 8 | import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post; 9 | import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.model; 10 | import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.redirectedUrl; 11 | import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; 12 | import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.view; 13 | 14 | import java.util.UUID; 15 | 16 | import org.junit.Test; 17 | import org.junit.runner.RunWith; 18 | import org.springframework.beans.factory.annotation.Autowired; 19 | import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc; 20 | import org.springframework.boot.test.context.SpringBootTest; 21 | import org.springframework.boot.test.context.SpringBootTest.WebEnvironment; 22 | import org.springframework.test.context.TestPropertySource; 23 | import org.springframework.test.context.junit4.SpringRunner; 24 | import org.springframework.test.web.servlet.MockMvc; 25 | 26 | import com.printezisn.moviestore.common.dto.account.AccountDto; 27 | 28 | /** 29 | * Contains integration tests for the account controller 30 | */ 31 | @RunWith(SpringRunner.class) 32 | @SpringBootTest(webEnvironment = WebEnvironment.MOCK) 33 | @AutoConfigureMockMvc 34 | @TestPropertySource("classpath:application-test.properties") 35 | public class AccountIntegrationTest { 36 | 37 | private static final String TEST_USERNAME = "test_username_%s"; 38 | private static final String TEST_EMAIL_ADDRESS = "test_email_%s@email.com"; 39 | private static final String TEST_PASSWORD = "T3stPA$$"; 40 | private static final String TEST_NEW_PASSWORD = "T3stPA$$2"; 41 | 42 | @Autowired 43 | private MockMvc mockMvc; 44 | 45 | /** 46 | * Tests if the account registration page is rendered successfully 47 | */ 48 | @Test 49 | public void test_register_get_success() throws Exception { 50 | mockMvc.perform(get("/account/register")) 51 | .andExpect(status().isOk()) 52 | .andExpect(view().name("account/register")); 53 | } 54 | 55 | /** 56 | * Tests if the account registration is successful 57 | */ 58 | @Test 59 | public void test_register_post_success() throws Exception { 60 | final String randomString = UUID.randomUUID().toString(); 61 | final AccountDto inputAccountDto = new AccountDto(); 62 | inputAccountDto.setUsername(String.format(TEST_USERNAME, randomString)); 63 | inputAccountDto.setPassword(TEST_PASSWORD); 64 | inputAccountDto.setEmailAddress(String.format(TEST_EMAIL_ADDRESS, randomString)); 65 | 66 | mockMvc.perform(post("/account/register") 67 | .with(csrf()) 68 | .param("username", inputAccountDto.getUsername()) 69 | .param("password", inputAccountDto.getPassword()) 70 | .param("emailAddress", inputAccountDto.getEmailAddress())) 71 | .andExpect(status().is3xxRedirection()) 72 | .andExpect(redirectedUrl("/")); 73 | 74 | mockMvc.perform(post("/account/register") 75 | .with(csrf()) 76 | .param("username", inputAccountDto.getUsername()) 77 | .param("password", inputAccountDto.getPassword()) 78 | .param("emailAddress", inputAccountDto.getEmailAddress())) 79 | .andExpect(status().isOk()) 80 | .andExpect(view().name("account/register")) 81 | .andExpect(model().attributeExists("errors")); 82 | } 83 | 84 | /** 85 | * Tests if the change password page is rendered successfully 86 | */ 87 | @Test 88 | public void test_changePassword_get_success() throws Exception { 89 | mockMvc.perform(get("/account/changePassword") 90 | .with(user(String.format(TEST_USERNAME, "1")))) 91 | .andExpect(status().isOk()) 92 | .andExpect(view().name("account/changePassword")); 93 | } 94 | 95 | /** 96 | * Tests if the password is changed successfully 97 | */ 98 | @Test 99 | public void test_changePassword_post_success() throws Exception { 100 | final String randomString = UUID.randomUUID().toString(); 101 | final AccountDto inputAccountDto = new AccountDto(); 102 | inputAccountDto.setUsername(String.format(TEST_USERNAME, randomString)); 103 | inputAccountDto.setPassword(TEST_PASSWORD); 104 | inputAccountDto.setEmailAddress(String.format(TEST_EMAIL_ADDRESS, randomString)); 105 | 106 | mockMvc.perform(post("/account/register") 107 | .with(csrf()) 108 | .param("username", inputAccountDto.getUsername()) 109 | .param("password", inputAccountDto.getPassword()) 110 | .param("emailAddress", inputAccountDto.getEmailAddress())) 111 | .andExpect(status().is3xxRedirection()) 112 | .andExpect(redirectedUrl("/")); 113 | 114 | mockMvc.perform(post("/account/changePassword") 115 | .with(csrf()) 116 | .with(user(inputAccountDto.getUsername())) 117 | .param("currentPassword", TEST_PASSWORD) 118 | .param("newPassword", TEST_NEW_PASSWORD)) 119 | .andExpect(status().is3xxRedirection()) 120 | .andExpect(redirectedUrl("/")); 121 | 122 | mockMvc.perform(formLogin("/auth/login") 123 | .user("username", inputAccountDto.getUsername()) 124 | .password("password", TEST_NEW_PASSWORD)) 125 | .andExpect(authenticated()); 126 | } 127 | } 128 | -------------------------------------------------------------------------------- /Website/src/test/java/com/printezisn/moviestore/website/integ/AuthIntegrationTest.java: -------------------------------------------------------------------------------- 1 | package com.printezisn.moviestore.website.integ; 2 | 3 | import static org.junit.Assert.assertEquals; 4 | import static org.springframework.security.test.web.servlet.request.SecurityMockMvcRequestBuilders.formLogin; 5 | import static org.springframework.security.test.web.servlet.request.SecurityMockMvcRequestBuilders.logout; 6 | import static org.springframework.security.test.web.servlet.response.SecurityMockMvcResultMatchers.authenticated; 7 | import static org.springframework.security.test.web.servlet.response.SecurityMockMvcResultMatchers.unauthenticated; 8 | import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; 9 | import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; 10 | import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.view; 11 | 12 | import java.util.UUID; 13 | 14 | import org.junit.Test; 15 | import org.junit.runner.RunWith; 16 | import org.springframework.beans.factory.annotation.Autowired; 17 | import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc; 18 | import org.springframework.boot.test.context.SpringBootTest; 19 | import org.springframework.boot.test.context.SpringBootTest.WebEnvironment; 20 | import org.springframework.http.HttpStatus; 21 | import org.springframework.http.ResponseEntity; 22 | import org.springframework.test.context.TestPropertySource; 23 | import org.springframework.test.context.junit4.SpringRunner; 24 | import org.springframework.test.web.servlet.MockMvc; 25 | import org.springframework.web.client.RestTemplate; 26 | 27 | import com.printezisn.moviestore.common.dto.account.AccountDto; 28 | import com.printezisn.moviestore.common.models.account.AccountResultModel; 29 | import com.printezisn.moviestore.website.configuration.properties.ServiceProperties; 30 | 31 | /** 32 | * Contains integration tests for the auth controller 33 | */ 34 | @RunWith(SpringRunner.class) 35 | @SpringBootTest(webEnvironment = WebEnvironment.MOCK) 36 | @AutoConfigureMockMvc 37 | @TestPropertySource("classpath:application-test.properties") 38 | public class AuthIntegrationTest { 39 | 40 | private static final String TEST_USERNAME = "test_username_%s"; 41 | private static final String TEST_EMAIL_ADDRESS = "test_email_%s@email.com"; 42 | private static final String TEST_PASSWORD = "T3stPA$$"; 43 | 44 | @Autowired 45 | private ServiceProperties serviceProperties; 46 | 47 | @Autowired 48 | private RestTemplate restTemplate; 49 | 50 | @Autowired 51 | private MockMvc mockMvc; 52 | 53 | /** 54 | * Tests if the login page is rendered successfully 55 | */ 56 | @Test 57 | public void test_login_renderSuccess() throws Exception { 58 | mockMvc.perform(get("/auth/login")) 59 | .andExpect(status().isOk()) 60 | .andExpect(view().name("auth/login")); 61 | } 62 | 63 | /** 64 | * Tests if the login fails when the credentials are incorrect 65 | */ 66 | @Test 67 | public void test_login_authenticationFail() throws Exception { 68 | mockMvc.perform(formLogin("/auth/login") 69 | .user("username", "invalid_username") 70 | .password("password", "invalid_password")) 71 | .andExpect(unauthenticated()); 72 | } 73 | 74 | /** 75 | * Tests if the login succeeds when the credentials are correct 76 | */ 77 | @Test 78 | public void test_login_success() throws Exception { 79 | final AccountDto accountDto = createAccount(); 80 | 81 | mockMvc.perform(formLogin("/auth/login") 82 | .user("username", accountDto.getUsername()) 83 | .password("password", TEST_PASSWORD)) 84 | .andExpect(authenticated()); 85 | } 86 | 87 | /** 88 | * Tests if the logout is successful 89 | */ 90 | @Test 91 | public void test_logout_success() throws Exception { 92 | mockMvc.perform(logout("/auth/logout")) 93 | .andExpect(unauthenticated()); 94 | } 95 | 96 | /** 97 | * Creates an account 98 | * 99 | * @return The created account 100 | */ 101 | private AccountDto createAccount() { 102 | final String randomString = UUID.randomUUID().toString(); 103 | final AccountDto accountDto = new AccountDto(); 104 | 105 | accountDto.setUsername(String.format(TEST_USERNAME, randomString)); 106 | accountDto.setPassword(TEST_PASSWORD); 107 | accountDto.setEmailAddress(String.format(TEST_EMAIL_ADDRESS, randomString)); 108 | 109 | final String url = getAccountServiceActionUrl("/account/new"); 110 | final ResponseEntity response = restTemplate.postForEntity(url, accountDto, 111 | AccountResultModel.class); 112 | 113 | assertEquals(HttpStatus.OK, response.getStatusCode()); 114 | 115 | return response.getBody().getResult(); 116 | } 117 | 118 | /** 119 | * Returns the URL to an account service action 120 | * 121 | * @param action 122 | * The action 123 | * @return The action URL 124 | */ 125 | private String getAccountServiceActionUrl(final String action) { 126 | return String.format("%s%s", serviceProperties.getAccountServiceUrl(), action); 127 | } 128 | } 129 | -------------------------------------------------------------------------------- /docker-compose.yml: -------------------------------------------------------------------------------- 1 | version: '3' 2 | services: 3 | account-service: 4 | build: 5 | context: . 6 | dockerfile: dockerfile.accountservice 7 | volumes: 8 | - ./data/account-service-logs:/app/logs 9 | expose: 10 | - "8000" 11 | environment: 12 | - SPRING_DATA_MONGODB_HOST=mongodb 13 | depends_on: 14 | - mongodb 15 | movie-service: 16 | build: 17 | context: . 18 | dockerfile: dockerfile.movieservice 19 | volumes: 20 | - ./data/movie-service-logs:/app/logs 21 | expose: 22 | - "9000" 23 | environment: 24 | - SPRING_DATA_MONGODB_HOST=mongodb 25 | - SPRING_DATA_ELASTICSEARCH_CLUSTER-NAME=elasticsearch-docker 26 | - SPRING_DATA_ELASTICSEARCH_CLUSTER-NODES=elasticsearch:9300 27 | depends_on: 28 | - mongodb 29 | - elasticsearch 30 | command: ["./wait-for-it.sh", "elasticsearch:9300", "-t", "0", "--", "java", "-jar", "./app.jar"] 31 | website: 32 | build: 33 | context: . 34 | dockerfile: dockerfile.website 35 | volumes: 36 | - ./data/website-logs:/app/logs 37 | expose: 38 | - "10000" 39 | environment: 40 | - SERVICE_ACCOUNTSERVICEURL=http://account-service:8000 41 | - SERVICE_MOVIESERVICEURL=http://movie-service:9000 42 | depends_on: 43 | - account-service 44 | - movie-service 45 | mongodb: 46 | image: mongo:latest 47 | container_name: "mongodb" 48 | environment: 49 | - MONGO_DATA_DIR=/data/db 50 | - MONGO_LOG_DIR=/dev/null 51 | volumes: 52 | - ./data/db:/data/db 53 | ports: 54 | - 27017:27017 55 | command: mongod --smallfiles --logpath=/dev/null 56 | elasticsearch: 57 | image: docker.elastic.co/elasticsearch/elasticsearch:6.2.4 58 | container_name: "elasticsearch" 59 | environment: 60 | - cluster.name=elasticsearch-docker 61 | - bootstrap.memory_lock=true 62 | - xpack.security.enabled=false 63 | - "ES_JAVA_OPTS=-Xms512m -Xmx512m" 64 | ulimits: 65 | memlock: 66 | soft: -1 67 | hard: -1 68 | volumes: 69 | - ./data/elasticsearch:/usr/share/elasticsearch/data 70 | ports: 71 | - 9200:9200 72 | - 9300:9300 73 | nginx: 74 | image: nginx:latest 75 | ports: 76 | - "80:80" 77 | - "443:443" 78 | depends_on: 79 | - website 80 | volumes: 81 | - ./nginx/nginx.conf:/etc/nginx/nginx.conf 82 | - ./nginx/ssl.pem:/etc/ssl.pem 83 | - ./nginx/ssl.key:/etc/ssl.key 84 | -------------------------------------------------------------------------------- /dockerfile.accountservice: -------------------------------------------------------------------------------- 1 | FROM openjdk:11 2 | COPY ./AccountService /code/AccountService 3 | COPY ./Common /code/Common 4 | COPY ./gradle /code/gradle 5 | COPY ./gradlew /code/ 6 | COPY ./settings.gradle /code/ 7 | 8 | WORKDIR /code 9 | RUN ./gradlew build 10 | WORKDIR /app 11 | RUN cp /code/AccountService/build/libs/*.jar /app/app.jar 12 | 13 | EXPOSE 8000 14 | CMD ["java", "-jar", "./app.jar"] -------------------------------------------------------------------------------- /dockerfile.movieservice: -------------------------------------------------------------------------------- 1 | FROM openjdk:11 2 | COPY ./MovieService /code/MovieService 3 | COPY ./Common /code/Common 4 | COPY ./gradle /code/gradle 5 | COPY ./gradlew /code/ 6 | COPY ./settings.gradle /code/ 7 | COPY ./wait-for-it.sh /code/ 8 | 9 | WORKDIR /code 10 | RUN ./gradlew build 11 | WORKDIR /app 12 | RUN cp /code/MovieService/build/libs/*.jar /app/app.jar 13 | RUN cp /code/wait-for-it.sh /app/ 14 | RUN chmod a+x /app/wait-for-it.sh 15 | 16 | EXPOSE 9000 17 | CMD ["java", "-jar", "./app.jar"] 18 | -------------------------------------------------------------------------------- /dockerfile.website: -------------------------------------------------------------------------------- 1 | FROM openjdk:11 2 | 3 | RUN apt-get update 4 | RUN apt-get install -y curl software-properties-common 5 | RUN curl -sL https://deb.nodesource.com/setup_9.x | bash - 6 | RUN apt-get install nodejs 7 | 8 | COPY ./Website /code/Website 9 | COPY ./Common /code/Common 10 | COPY ./gradle /code/gradle 11 | COPY ./gradlew /code/ 12 | COPY ./settings.gradle /code/ 13 | 14 | WORKDIR /code/Website/src/main/resources/static 15 | RUN npm install 16 | RUN npm run build-prod 17 | 18 | WORKDIR /code 19 | RUN ./gradlew build 20 | WORKDIR /app 21 | RUN cp /code/Website/build/libs/*.jar /app/app.jar 22 | 23 | EXPOSE 10000 24 | CMD ["java", "-jar", "./app.jar"] -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/printezisn/spring-boot-microservices-starter/5faf6292427ab3e3b7199ccbe84b3291b503cc11/gradle/wrapper/gradle-wrapper.jar -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.properties: -------------------------------------------------------------------------------- 1 | #Tue Feb 06 12:27:20 CET 2018 2 | distributionBase=GRADLE_USER_HOME 3 | distributionPath=wrapper/dists 4 | zipStoreBase=GRADLE_USER_HOME 5 | zipStorePath=wrapper/dists 6 | distributionUrl=https\://services.gradle.org/distributions/gradle-4.10.2-bin.zip 7 | -------------------------------------------------------------------------------- /gradlew: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env sh 2 | 3 | ############################################################################## 4 | ## 5 | ## Gradle start up script for UN*X 6 | ## 7 | ############################################################################## 8 | 9 | # Attempt to set APP_HOME 10 | # Resolve links: $0 may be a link 11 | PRG="$0" 12 | # Need this for relative symlinks. 13 | while [ -h "$PRG" ] ; do 14 | ls=`ls -ld "$PRG"` 15 | link=`expr "$ls" : '.*-> \(.*\)$'` 16 | if expr "$link" : '/.*' > /dev/null; then 17 | PRG="$link" 18 | else 19 | PRG=`dirname "$PRG"`"/$link" 20 | fi 21 | done 22 | SAVED="`pwd`" 23 | cd "`dirname \"$PRG\"`/" >/dev/null 24 | APP_HOME="`pwd -P`" 25 | cd "$SAVED" >/dev/null 26 | 27 | APP_NAME="Gradle" 28 | APP_BASE_NAME=`basename "$0"` 29 | 30 | # Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 31 | DEFAULT_JVM_OPTS="" 32 | 33 | # Use the maximum available, or set MAX_FD != -1 to use that value. 34 | MAX_FD="maximum" 35 | 36 | warn ( ) { 37 | echo "$*" 38 | } 39 | 40 | die ( ) { 41 | echo 42 | echo "$*" 43 | echo 44 | exit 1 45 | } 46 | 47 | # OS specific support (must be 'true' or 'false'). 48 | cygwin=false 49 | msys=false 50 | darwin=false 51 | nonstop=false 52 | case "`uname`" in 53 | CYGWIN* ) 54 | cygwin=true 55 | ;; 56 | Darwin* ) 57 | darwin=true 58 | ;; 59 | MINGW* ) 60 | msys=true 61 | ;; 62 | NONSTOP* ) 63 | nonstop=true 64 | ;; 65 | esac 66 | 67 | CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar 68 | 69 | # Determine the Java command to use to start the JVM. 70 | if [ -n "$JAVA_HOME" ] ; then 71 | if [ -x "$JAVA_HOME/jre/sh/java" ] ; then 72 | # IBM's JDK on AIX uses strange locations for the executables 73 | JAVACMD="$JAVA_HOME/jre/sh/java" 74 | else 75 | JAVACMD="$JAVA_HOME/bin/java" 76 | fi 77 | if [ ! -x "$JAVACMD" ] ; then 78 | die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME 79 | 80 | Please set the JAVA_HOME variable in your environment to match the 81 | location of your Java installation." 82 | fi 83 | else 84 | JAVACMD="java" 85 | which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 86 | 87 | Please set the JAVA_HOME variable in your environment to match the 88 | location of your Java installation." 89 | fi 90 | 91 | # Increase the maximum file descriptors if we can. 92 | if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then 93 | MAX_FD_LIMIT=`ulimit -H -n` 94 | if [ $? -eq 0 ] ; then 95 | if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then 96 | MAX_FD="$MAX_FD_LIMIT" 97 | fi 98 | ulimit -n $MAX_FD 99 | if [ $? -ne 0 ] ; then 100 | warn "Could not set maximum file descriptor limit: $MAX_FD" 101 | fi 102 | else 103 | warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT" 104 | fi 105 | fi 106 | 107 | # For Darwin, add options to specify how the application appears in the dock 108 | if $darwin; then 109 | GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\"" 110 | fi 111 | 112 | # For Cygwin, switch paths to Windows format before running java 113 | if $cygwin ; then 114 | APP_HOME=`cygpath --path --mixed "$APP_HOME"` 115 | CLASSPATH=`cygpath --path --mixed "$CLASSPATH"` 116 | JAVACMD=`cygpath --unix "$JAVACMD"` 117 | 118 | # We build the pattern for arguments to be converted via cygpath 119 | ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null` 120 | SEP="" 121 | for dir in $ROOTDIRSRAW ; do 122 | ROOTDIRS="$ROOTDIRS$SEP$dir" 123 | SEP="|" 124 | done 125 | OURCYGPATTERN="(^($ROOTDIRS))" 126 | # Add a user-defined pattern to the cygpath arguments 127 | if [ "$GRADLE_CYGPATTERN" != "" ] ; then 128 | OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)" 129 | fi 130 | # Now convert the arguments - kludge to limit ourselves to /bin/sh 131 | i=0 132 | for arg in "$@" ; do 133 | CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -` 134 | CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option 135 | 136 | if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition 137 | eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"` 138 | else 139 | eval `echo args$i`="\"$arg\"" 140 | fi 141 | i=$((i+1)) 142 | done 143 | case $i in 144 | (0) set -- ;; 145 | (1) set -- "$args0" ;; 146 | (2) set -- "$args0" "$args1" ;; 147 | (3) set -- "$args0" "$args1" "$args2" ;; 148 | (4) set -- "$args0" "$args1" "$args2" "$args3" ;; 149 | (5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;; 150 | (6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;; 151 | (7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;; 152 | (8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;; 153 | (9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;; 154 | esac 155 | fi 156 | 157 | # Escape application args 158 | save ( ) { 159 | for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done 160 | echo " " 161 | } 162 | APP_ARGS=$(save "$@") 163 | 164 | # Collect all arguments for the java command, following the shell quoting and substitution rules 165 | eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS" 166 | 167 | # by default we should be in the correct project dir, but when run from Finder on Mac, the cwd is wrong 168 | if [ "$(uname)" = "Darwin" ] && [ "$HOME" = "$PWD" ]; then 169 | cd "$(dirname "$0")" 170 | fi 171 | 172 | exec "$JAVACMD" "$@" 173 | -------------------------------------------------------------------------------- /gradlew.bat: -------------------------------------------------------------------------------- 1 | @if "%DEBUG%" == "" @echo off 2 | @rem ########################################################################## 3 | @rem 4 | @rem Gradle startup script for Windows 5 | @rem 6 | @rem ########################################################################## 7 | 8 | @rem Set local scope for the variables with windows NT shell 9 | if "%OS%"=="Windows_NT" setlocal 10 | 11 | set DIRNAME=%~dp0 12 | if "%DIRNAME%" == "" set DIRNAME=. 13 | set APP_BASE_NAME=%~n0 14 | set APP_HOME=%DIRNAME% 15 | 16 | @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 17 | set DEFAULT_JVM_OPTS= 18 | 19 | @rem Find java.exe 20 | if defined JAVA_HOME goto findJavaFromJavaHome 21 | 22 | set JAVA_EXE=java.exe 23 | %JAVA_EXE% -version >NUL 2>&1 24 | if "%ERRORLEVEL%" == "0" goto init 25 | 26 | echo. 27 | echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 28 | echo. 29 | echo Please set the JAVA_HOME variable in your environment to match the 30 | echo location of your Java installation. 31 | 32 | goto fail 33 | 34 | :findJavaFromJavaHome 35 | set JAVA_HOME=%JAVA_HOME:"=% 36 | set JAVA_EXE=%JAVA_HOME%/bin/java.exe 37 | 38 | if exist "%JAVA_EXE%" goto init 39 | 40 | echo. 41 | echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 42 | echo. 43 | echo Please set the JAVA_HOME variable in your environment to match the 44 | echo location of your Java installation. 45 | 46 | goto fail 47 | 48 | :init 49 | @rem Get command-line arguments, handling Windows variants 50 | 51 | if not "%OS%" == "Windows_NT" goto win9xME_args 52 | 53 | :win9xME_args 54 | @rem Slurp the command line arguments. 55 | set CMD_LINE_ARGS= 56 | set _SKIP=2 57 | 58 | :win9xME_args_slurp 59 | if "x%~1" == "x" goto execute 60 | 61 | set CMD_LINE_ARGS=%* 62 | 63 | :execute 64 | @rem Setup the command line 65 | 66 | set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar 67 | 68 | @rem Execute Gradle 69 | "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS% 70 | 71 | :end 72 | @rem End local scope for the variables with windows NT shell 73 | if "%ERRORLEVEL%"=="0" goto mainEnd 74 | 75 | :fail 76 | rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of 77 | rem the _cmd.exe /c_ return code! 78 | if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 79 | exit /b 1 80 | 81 | :mainEnd 82 | if "%OS%"=="Windows_NT" endlocal 83 | 84 | :omega 85 | -------------------------------------------------------------------------------- /nginx/nginx.conf: -------------------------------------------------------------------------------- 1 | worker_processes 4; 2 | 3 | events { worker_connections 1024; } 4 | 5 | http { 6 | upstream moviestore { 7 | ip_hash; 8 | server website:10000; 9 | } 10 | 11 | server { 12 | listen 80; 13 | server_name moviestore; 14 | return 301 https://$host$request_uri; 15 | } 16 | 17 | server { 18 | listen 443 ssl; 19 | server_name moviestore; 20 | 21 | ssl_certificate /etc/ssl.pem; 22 | ssl_certificate_key /etc/ssl.key; 23 | 24 | location / { 25 | proxy_pass http://moviestore/; 26 | proxy_redirect off; 27 | proxy_set_header Host $host; 28 | proxy_set_header X-Real-IP $remote_addr; 29 | proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; 30 | proxy_set_header X-Forwarded-Host $server_name; 31 | } 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /nginx/ssl.key: -------------------------------------------------------------------------------- 1 | Bag Attributes 2 | friendlyName: tomcat 3 | localKeyID: 54 69 6D 65 20 31 35 34 33 31 37 37 30 36 36 36 37 31 4 | Key Attributes: 5 | -----BEGIN PRIVATE KEY----- 6 | MIIEvwIBADANBgkqhkiG9w0BAQEFAASCBKkwggSlAgEAAoIBAQCmb0HK9/WpAvZC 7 | /Kgv7ZtIICOiG/fm1nmstFPyz6CJRvPYrzeNwokGgHFmed68M994FSD/A89FQDIi 8 | MpwmWO9a6NZIOvv6pyeE5Kr6GmPPuRg8Tkj6uqhoL1JsIcSzveEtVNF9Ws+bHIIF 9 | H7bt2dPUj7EJ3e56m+r8LB6ZUvgdQnpEI1msVKoduqHtnE7azS2cbmrYuuNBEpZZ 10 | kw70e2ErJFiwvzSEQBWScZn1IA5FyCjyBpgDqWHicE5WUaAq0W2/ehHcU+ULGhto 11 | WxKPwyavO+7DO6bk+qha5eAuabTSs9d48byOyG9Y+tnvwyMhRbUS2CqzEvCde68N 12 | 4fy+IhubAgMBAAECggEBAI2KusJZhCNbMIGhi4tPICsDoipAbOB07/9vUKGD7/wv 13 | SjuyL6WwloXgABDtff5Cgz8FgQ0/eStkv/4TWnbGVbDTqUC+YQM22sYLDlkqzk2f 14 | FTxcO3Z3/ba03/yz7ywJRJIfuxVHgJ1ibjTbVuAKyzrPhPlmM2U0anBROSidxgJi 15 | 38YLkadmhFSWM9j9W7hPbMp+JSUWDPlkeTOyt30L0wzl0SBwF6m3yHqJrIHBUKdW 16 | w/NFrlJwgajfJ3X0aUJpJkn/JfAFCkCeFfxT6b6dGCJGgqy1MRBskO8UTwHy1s7Q 17 | 10zaxTxZPLxtzbu/Hy7t1reQx6q5NgJweGC2Ylp4eqECgYEA6sGf5z7q3T0pGeHD 18 | CQA13/a1ZoIddqS+b1aplvIuMHzhuxkJSsMwDumsLi5UD/xsA8NQU3oECTNK8QF1 19 | 4IKHIjOeSbDNTx8/XyZ1HeuxqWRoaXMdW7NDQvK8J7VXrUePV3IJu8hRMJxxqDVM 20 | 5YHRFm8yBJIyRzDjJs1XtKEm28sCgYEAtX7jTGtKW1MVMnOoaq3FMEUVNT/4QPk7 21 | i0ixE+EM2BWdv5qUj8npXUxz1Rm1eJ0BDo+bQddHw8yzAKCMu0IJCpkq4Y9tWXs/ 22 | MupSJIbcct+QUFWbl3mCXcwyRc2sMOIyzE/H9MN6YusZU7Xt2NblGnI15BS9CKtu 23 | xaOPiTVMZXECgYBrWamWpIXgL8Soyj1W01rPlNC1FJEGnSVcYqPgm9SVZbYPyc4e 24 | +wzx9NdAsvzL5qE6Q0lrMuO/lU4S0Zkm1mmXMUWT6x6nrOFc0IhD63DtxjWc6wAk 25 | 29/JMJjsC5gRbCTXVxWuYlcGRLQQuHb2iJulh6m2v2fweCGXr9UIi5zqawKBgQCi 26 | hdOIlPLqwIVUvljr3lubk+Ef4/6sQAJgAWIASSC1RvYRo5yw/b+pOlLnWrQ0I3PU 27 | 1CfVV6/914nbX+llrgZmpS3O+h6TaFf5gfa4msNBYozaQy6m/7oLwFSsSTaON6AB 28 | cNe/iGRJu/jcCyfHaveRLQCxExkLcGgrNwHLfhzBIQKBgQDMQA8vc0e4oRfIVUIL 29 | ewmb2J6HmB8R1yB2FgxiZmpT4SwTlkaXI6bPNfOZxrOxpsVlX6qm2MaA8MBS6QxN 30 | l7eKZfn2k3L7GE8JATJeC79xSYoLkj1rw+wvixcBTYRVD5nhFM/fqeXjlH9cXrFc 31 | 0ZJMtqefYg8nkEeeKxo/Ddgu7Q== 32 | -----END PRIVATE KEY----- 33 | -------------------------------------------------------------------------------- /nginx/ssl.pem: -------------------------------------------------------------------------------- 1 | Bag Attributes 2 | friendlyName: tomcat 3 | localKeyID: 54 69 6D 65 20 31 35 34 33 31 37 37 30 36 36 36 37 31 4 | subject=/C=Unknown/ST=Unknown/L=Unknown/O=Unknown/OU=Unknown/CN=Unknown 5 | issuer=/C=Unknown/ST=Unknown/L=Unknown/O=Unknown/OU=Unknown/CN=Unknown 6 | -----BEGIN CERTIFICATE----- 7 | MIIDdzCCAl+gAwIBAgIEJoKGyzANBgkqhkiG9w0BAQsFADBsMRAwDgYDVQQGEwdV 8 | bmtub3duMRAwDgYDVQQIEwdVbmtub3duMRAwDgYDVQQHEwdVbmtub3duMRAwDgYD 9 | VQQKEwdVbmtub3duMRAwDgYDVQQLEwdVbmtub3duMRAwDgYDVQQDEwdVbmtub3du 10 | MB4XDTE4MTEyNTIwMTc0NloXDTI4MTEyMjIwMTc0NlowbDEQMA4GA1UEBhMHVW5r 11 | bm93bjEQMA4GA1UECBMHVW5rbm93bjEQMA4GA1UEBxMHVW5rbm93bjEQMA4GA1UE 12 | ChMHVW5rbm93bjEQMA4GA1UECxMHVW5rbm93bjEQMA4GA1UEAxMHVW5rbm93bjCC 13 | ASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAKZvQcr39akC9kL8qC/tm0gg 14 | I6Ib9+bWeay0U/LPoIlG89ivN43CiQaAcWZ53rwz33gVIP8Dz0VAMiIynCZY71ro 15 | 1kg6+/qnJ4TkqvoaY8+5GDxOSPq6qGgvUmwhxLO94S1U0X1az5scggUftu3Z09SP 16 | sQnd7nqb6vwsHplS+B1CekQjWaxUqh26oe2cTtrNLZxuati640ESllmTDvR7YSsk 17 | WLC/NIRAFZJxmfUgDkXIKPIGmAOpYeJwTlZRoCrRbb96EdxT5QsaG2hbEo/DJq87 18 | 7sM7puT6qFrl4C5ptNKz13jxvI7Ib1j62e/DIyFFtRLYKrMS8J17rw3h/L4iG5sC 19 | AwEAAaMhMB8wHQYDVR0OBBYEFJEEQok5GDt+a8M49OhP2SzOdlaDMA0GCSqGSIb3 20 | DQEBCwUAA4IBAQBXrOP38+GmSXHKNeyVn2Tq+Q7B28RQ375PUcrSWOJQozQ9NNyE 21 | CTjguNjv5D4bpsxrPDiesMKuLfvqqejdpyfL99rbS8zxajqtw/g72IftQ0oKT7IU 22 | zVA5bbQ5f0dT/eQWGDgmnpm24B+om1l91x9LVV5PjD8A0xO8Bd4xtlzEGMF7G4Un 23 | uwl+8dvj42MyOIpfw3AR90LA1uI/+sbxubACDGGu+G9lC1mzalp3fdOjIZWJw8h5 24 | jiqus+EOrMBMzsnWgiIt0YLdH3D24OCiJ46qnb58gGkb0Kgs7kR69bHxeyKSNYUv 25 | 6PKyZifmOfv3HVSdlsekh5bvkt1EbfosonaP 26 | -----END CERTIFICATE----- 27 | -------------------------------------------------------------------------------- /settings.gradle: -------------------------------------------------------------------------------- 1 | rootProject.name = 'MovieStore' 2 | 3 | include 'Common' 4 | include 'AccountService' 5 | include 'MovieService' 6 | include 'Website' -------------------------------------------------------------------------------- /wait-for-it.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | # Use this script to test if a given TCP host/port are available 3 | # Origin: https://github.com/vishnubob/wait-for-it 4 | 5 | WAITFORIT_cmdname=${0##*/} 6 | 7 | echoerr() { if [[ $WAITFORIT_QUIET -ne 1 ]]; then echo "$@" 1>&2; fi } 8 | 9 | usage() 10 | { 11 | cat << USAGE >&2 12 | Usage: 13 | $WAITFORIT_cmdname host:port [-s] [-t timeout] [-- command args] 14 | -h HOST | --host=HOST Host or IP under test 15 | -p PORT | --port=PORT TCP port under test 16 | Alternatively, you specify the host and port as host:port 17 | -s | --strict Only execute subcommand if the test succeeds 18 | -q | --quiet Don't output any status messages 19 | -t TIMEOUT | --timeout=TIMEOUT 20 | Timeout in seconds, zero for no timeout 21 | -- COMMAND ARGS Execute command with args after the test finishes 22 | USAGE 23 | exit 1 24 | } 25 | 26 | wait_for() 27 | { 28 | if [[ $WAITFORIT_TIMEOUT -gt 0 ]]; then 29 | echoerr "$WAITFORIT_cmdname: waiting $WAITFORIT_TIMEOUT seconds for $WAITFORIT_HOST:$WAITFORIT_PORT" 30 | else 31 | echoerr "$WAITFORIT_cmdname: waiting for $WAITFORIT_HOST:$WAITFORIT_PORT without a timeout" 32 | fi 33 | WAITFORIT_start_ts=$(date +%s) 34 | while : 35 | do 36 | if [[ $WAITFORIT_ISBUSY -eq 1 ]]; then 37 | nc -z $WAITFORIT_HOST $WAITFORIT_PORT 38 | WAITFORIT_result=$? 39 | else 40 | (echo > /dev/tcp/$WAITFORIT_HOST/$WAITFORIT_PORT) >/dev/null 2>&1 41 | WAITFORIT_result=$? 42 | fi 43 | if [[ $WAITFORIT_result -eq 0 ]]; then 44 | WAITFORIT_end_ts=$(date +%s) 45 | echoerr "$WAITFORIT_cmdname: $WAITFORIT_HOST:$WAITFORIT_PORT is available after $((WAITFORIT_end_ts - WAITFORIT_start_ts)) seconds" 46 | break 47 | fi 48 | sleep 1 49 | done 50 | return $WAITFORIT_result 51 | } 52 | 53 | wait_for_wrapper() 54 | { 55 | # In order to support SIGINT during timeout: http://unix.stackexchange.com/a/57692 56 | if [[ $WAITFORIT_QUIET -eq 1 ]]; then 57 | timeout $WAITFORIT_BUSYTIMEFLAG $WAITFORIT_TIMEOUT $0 --quiet --child --host=$WAITFORIT_HOST --port=$WAITFORIT_PORT --timeout=$WAITFORIT_TIMEOUT & 58 | else 59 | timeout $WAITFORIT_BUSYTIMEFLAG $WAITFORIT_TIMEOUT $0 --child --host=$WAITFORIT_HOST --port=$WAITFORIT_PORT --timeout=$WAITFORIT_TIMEOUT & 60 | fi 61 | WAITFORIT_PID=$! 62 | trap "kill -INT -$WAITFORIT_PID" INT 63 | wait $WAITFORIT_PID 64 | WAITFORIT_RESULT=$? 65 | if [[ $WAITFORIT_RESULT -ne 0 ]]; then 66 | echoerr "$WAITFORIT_cmdname: timeout occurred after waiting $WAITFORIT_TIMEOUT seconds for $WAITFORIT_HOST:$WAITFORIT_PORT" 67 | fi 68 | return $WAITFORIT_RESULT 69 | } 70 | 71 | # process arguments 72 | while [[ $# -gt 0 ]] 73 | do 74 | case "$1" in 75 | *:* ) 76 | WAITFORIT_hostport=(${1//:/ }) 77 | WAITFORIT_HOST=${WAITFORIT_hostport[0]} 78 | WAITFORIT_PORT=${WAITFORIT_hostport[1]} 79 | shift 1 80 | ;; 81 | --child) 82 | WAITFORIT_CHILD=1 83 | shift 1 84 | ;; 85 | -q | --quiet) 86 | WAITFORIT_QUIET=1 87 | shift 1 88 | ;; 89 | -s | --strict) 90 | WAITFORIT_STRICT=1 91 | shift 1 92 | ;; 93 | -h) 94 | WAITFORIT_HOST="$2" 95 | if [[ $WAITFORIT_HOST == "" ]]; then break; fi 96 | shift 2 97 | ;; 98 | --host=*) 99 | WAITFORIT_HOST="${1#*=}" 100 | shift 1 101 | ;; 102 | -p) 103 | WAITFORIT_PORT="$2" 104 | if [[ $WAITFORIT_PORT == "" ]]; then break; fi 105 | shift 2 106 | ;; 107 | --port=*) 108 | WAITFORIT_PORT="${1#*=}" 109 | shift 1 110 | ;; 111 | -t) 112 | WAITFORIT_TIMEOUT="$2" 113 | if [[ $WAITFORIT_TIMEOUT == "" ]]; then break; fi 114 | shift 2 115 | ;; 116 | --timeout=*) 117 | WAITFORIT_TIMEOUT="${1#*=}" 118 | shift 1 119 | ;; 120 | --) 121 | shift 122 | WAITFORIT_CLI=("$@") 123 | break 124 | ;; 125 | --help) 126 | usage 127 | ;; 128 | *) 129 | echoerr "Unknown argument: $1" 130 | usage 131 | ;; 132 | esac 133 | done 134 | 135 | if [[ "$WAITFORIT_HOST" == "" || "$WAITFORIT_PORT" == "" ]]; then 136 | echoerr "Error: you need to provide a host and port to test." 137 | usage 138 | fi 139 | 140 | WAITFORIT_TIMEOUT=${WAITFORIT_TIMEOUT:-15} 141 | WAITFORIT_STRICT=${WAITFORIT_STRICT:-0} 142 | WAITFORIT_CHILD=${WAITFORIT_CHILD:-0} 143 | WAITFORIT_QUIET=${WAITFORIT_QUIET:-0} 144 | 145 | # check to see if timeout is from busybox? 146 | WAITFORIT_TIMEOUT_PATH=$(type -p timeout) 147 | WAITFORIT_TIMEOUT_PATH=$(realpath $WAITFORIT_TIMEOUT_PATH 2>/dev/null || readlink -f $WAITFORIT_TIMEOUT_PATH) 148 | if [[ $WAITFORIT_TIMEOUT_PATH =~ "busybox" ]]; then 149 | WAITFORIT_ISBUSY=1 150 | WAITFORIT_BUSYTIMEFLAG="-t" 151 | 152 | else 153 | WAITFORIT_ISBUSY=0 154 | WAITFORIT_BUSYTIMEFLAG="" 155 | fi 156 | 157 | if [[ $WAITFORIT_CHILD -gt 0 ]]; then 158 | wait_for 159 | WAITFORIT_RESULT=$? 160 | exit $WAITFORIT_RESULT 161 | else 162 | if [[ $WAITFORIT_TIMEOUT -gt 0 ]]; then 163 | wait_for_wrapper 164 | WAITFORIT_RESULT=$? 165 | else 166 | wait_for 167 | WAITFORIT_RESULT=$? 168 | fi 169 | fi 170 | 171 | if [[ $WAITFORIT_CLI != "" ]]; then 172 | if [[ $WAITFORIT_RESULT -ne 0 && $WAITFORIT_STRICT -eq 1 ]]; then 173 | echoerr "$WAITFORIT_cmdname: strict mode, refusing to execute subprocess" 174 | exit $WAITFORIT_RESULT 175 | fi 176 | exec "${WAITFORIT_CLI[@]}" 177 | else 178 | exit $WAITFORIT_RESULT 179 | fi 180 | --------------------------------------------------------------------------------