From 47866a9f5d90f612c122b4f824092aedf4ea2233 Mon Sep 17 00:00:00 2001 From: PEDSF Date: Sun, 22 Nov 2020 12:11:07 +0100 Subject: [PATCH] add password modification --- pom.xml | 7 + .../petclinic/common/CommonAttribute.java | 7 +- .../petclinic/common/CommonEndPoint.java | 6 +- .../petclinic/common/CommonParameter.java | 4 + .../samples/petclinic/common/CommonView.java | 8 +- .../petclinic/configuration/MailConfig.java | 110 ++++++++ .../configuration/WebSecurityConfig.java | 18 +- .../petclinic/controller/OwnerController.java | 4 +- .../petclinic/controller/PetController.java | 4 +- .../petclinic/controller/UserController.java | 247 +++++++++++++----- .../petclinic/controller/VetController.java | 2 +- .../petclinic/controller/VisitController.java | 4 +- .../petclinic/dto/common/AuthProviderDTO.java | 12 + .../petclinic/dto/common/CredentialDTO.java | 122 +++++++++ .../petclinic/dto/common/MessageDTO.java | 126 +++++++++ .../petclinic/dto/{ => common}/RoleDTO.java | 5 +- .../petclinic/dto/{ => common}/UserDTO.java | 154 +++++++++-- .../petclinic/formatter/PetTypeFormatter.java | 3 +- .../petclinic/model/common/AuthProvider.java | 12 +- .../petclinic/model/common/Credential.java | 112 ++++++++ .../samples/petclinic/model/common/Role.java | 2 - .../samples/petclinic/model/common/User.java | 118 ++++++--- .../repository/AuthProviderRepository.java | 43 +++ .../repository/CredentialRepository.java | 51 ++++ .../service/{ => business}/BaseService.java | 2 +- .../service/{ => business}/OwnerService.java | 2 +- .../service/{ => business}/PetService.java | 2 +- .../{ => business}/PetTypeService.java | 2 +- .../{ => business}/SpecialtyService.java | 2 +- .../service/{ => business}/VetService.java | 2 +- .../service/{ => business}/VisitService.java | 2 +- .../service/common/AuthProviderService.java | 87 ++++++ .../service/common/CredentialService.java | 129 +++++++++ .../service/common/EmailService.java | 97 +++++++ .../service/{ => common}/RoleService.java | 5 +- .../service/{ => common}/SecurityService.java | 2 +- .../{ => common}/SecurityServiceImpl.java | 2 +- .../{ => common}/UserDetailsServiceImpl.java | 20 +- .../service/{ => common}/UserService.java | 18 +- src/main/resources/application.properties | 16 +- src/main/resources/db/h2/data.sql | 36 ++- src/main/resources/db/h2/schema.sql | 54 ++-- .../resources/images/mail-background.png | Bin 0 -> 9162 bytes .../static/resources/images/mail-banner.png | Bin 0 -> 2818 bytes .../static/resources/images/mail-logo.png | Bin 0 -> 67721 bytes src/main/resources/templates/email.html | 49 ++++ .../resources/templates/fragments/layout.html | 2 +- .../owners/createOrUpdateOwnerForm.html | 5 +- .../users/userChangePasswordForm.html | 53 ++++ ...serForm.html => userRegistrationForm.html} | 17 +- .../templates/users/userUpdateForm.html | 49 ++++ src/main/resources/templates/welcome.html | 7 +- .../OwnerControllerIntegrationTest.java | 2 +- .../controller/OwnerControllerTest.java | 4 +- .../PetControllerIntegrationTest.java | 4 +- .../controller/PetControllerTest.java | 6 +- .../VetControllerIntegrationTest.java | 2 +- .../controller/VetControllerTest.java | 4 +- .../VisitControllerIntegrationTest.java | 2 +- .../controller/VisitControllerTest.java | 4 +- .../formater/PetTypeDTOFormatterTest.java | 2 +- .../formater/PetTypeFormatterTest.java | 2 +- .../petclinic/service/OwnerServiceTest.java | 3 + .../petclinic/service/PetServiceTest.java | 3 + .../petclinic/service/VetServiceTest.java | 1 + 65 files changed, 1632 insertions(+), 250 deletions(-) create mode 100644 src/main/java/org/springframework/samples/petclinic/configuration/MailConfig.java create mode 100644 src/main/java/org/springframework/samples/petclinic/dto/common/AuthProviderDTO.java create mode 100644 src/main/java/org/springframework/samples/petclinic/dto/common/CredentialDTO.java create mode 100644 src/main/java/org/springframework/samples/petclinic/dto/common/MessageDTO.java rename src/main/java/org/springframework/samples/petclinic/dto/{ => common}/RoleDTO.java (64%) rename src/main/java/org/springframework/samples/petclinic/dto/{ => common}/UserDTO.java (54%) create mode 100644 src/main/java/org/springframework/samples/petclinic/model/common/Credential.java create mode 100644 src/main/java/org/springframework/samples/petclinic/repository/AuthProviderRepository.java create mode 100644 src/main/java/org/springframework/samples/petclinic/repository/CredentialRepository.java rename src/main/java/org/springframework/samples/petclinic/service/{ => business}/BaseService.java (94%) rename src/main/java/org/springframework/samples/petclinic/service/{ => business}/OwnerService.java (98%) rename src/main/java/org/springframework/samples/petclinic/service/{ => business}/PetService.java (98%) rename src/main/java/org/springframework/samples/petclinic/service/{ => business}/PetTypeService.java (96%) rename src/main/java/org/springframework/samples/petclinic/service/{ => business}/SpecialtyService.java (96%) rename src/main/java/org/springframework/samples/petclinic/service/{ => business}/VetService.java (97%) rename src/main/java/org/springframework/samples/petclinic/service/{ => business}/VisitService.java (97%) create mode 100644 src/main/java/org/springframework/samples/petclinic/service/common/AuthProviderService.java create mode 100644 src/main/java/org/springframework/samples/petclinic/service/common/CredentialService.java create mode 100644 src/main/java/org/springframework/samples/petclinic/service/common/EmailService.java rename src/main/java/org/springframework/samples/petclinic/service/{ => common}/RoleService.java (90%) rename src/main/java/org/springframework/samples/petclinic/service/{ => common}/SecurityService.java (66%) rename src/main/java/org/springframework/samples/petclinic/service/{ => common}/SecurityServiceImpl.java (96%) rename src/main/java/org/springframework/samples/petclinic/service/{ => common}/UserDetailsServiceImpl.java (59%) rename src/main/java/org/springframework/samples/petclinic/service/{ => common}/UserService.java (86%) create mode 100644 src/main/resources/static/resources/images/mail-background.png create mode 100644 src/main/resources/static/resources/images/mail-banner.png create mode 100644 src/main/resources/static/resources/images/mail-logo.png create mode 100644 src/main/resources/templates/email.html create mode 100644 src/main/resources/templates/users/userChangePasswordForm.html rename src/main/resources/templates/users/{createOrUpdateUserForm.html => userRegistrationForm.html} (79%) create mode 100644 src/main/resources/templates/users/userUpdateForm.html diff --git a/pom.xml b/pom.xml index 2e5afb68f..6b51459a6 100644 --- a/pom.xml +++ b/pom.xml @@ -98,6 +98,13 @@ ${spring-cloud-starter-security.version} + + + org.springframework.boot + spring-boot-starter-mail + + + com.h2database diff --git a/src/main/java/org/springframework/samples/petclinic/common/CommonAttribute.java b/src/main/java/org/springframework/samples/petclinic/common/CommonAttribute.java index 9c7beb87e..15890f0e0 100644 --- a/src/main/java/org/springframework/samples/petclinic/common/CommonAttribute.java +++ b/src/main/java/org/springframework/samples/petclinic/common/CommonAttribute.java @@ -8,6 +8,7 @@ package org.springframework.samples.petclinic.common; public final class CommonAttribute { public static final String DESCRIPTION = "description"; + public static final String ID = "id"; public static final String NAME = "name"; @@ -15,7 +16,7 @@ public final class CommonAttribute { public static final String OWNER = "owner"; - public static final String OWNER_ID = "id"; + public static final String OWNER_ID = "ownerId"; public static final String OWNER_LAST_NAME = "lastName"; @@ -39,9 +40,11 @@ public final class CommonAttribute { public static final String PET_TYPE = "type"; + public static final String TOKEN = "token"; + public static final String USER = "user"; - public static final String USER_ID = "id"; + public static final String USER_ID = "userId"; public static final String VETS = "vets"; diff --git a/src/main/java/org/springframework/samples/petclinic/common/CommonEndPoint.java b/src/main/java/org/springframework/samples/petclinic/common/CommonEndPoint.java index b292df05e..e148b08d5 100644 --- a/src/main/java/org/springframework/samples/petclinic/common/CommonEndPoint.java +++ b/src/main/java/org/springframework/samples/petclinic/common/CommonEndPoint.java @@ -25,7 +25,9 @@ public final class CommonEndPoint { public static final String USERS_EDIT = "/users/edit"; - public static final String USERS_ID_EDIT = "/users/{ownerId}/edit"; + public static final String USERS_ID_EDIT = "/users/{userId}/edit"; + + public static final String USERS_ID_EDIT_PASSWORD = "/users/{userId}/edit/password"; public static final String USERS_NEW = "/users/new"; @@ -35,6 +37,8 @@ public final class CommonEndPoint { public static final String OAUTH2_SUCCESS = "/oauth2/success"; + public static final String CONFIRM_ACCOUNT = "/confirm-account"; + public static final String LOGOUT = "/logout"; public static final String LOGOUT_SUCCESS = "/logout/success"; diff --git a/src/main/java/org/springframework/samples/petclinic/common/CommonParameter.java b/src/main/java/org/springframework/samples/petclinic/common/CommonParameter.java index 478e12567..33b6bf576 100644 --- a/src/main/java/org/springframework/samples/petclinic/common/CommonParameter.java +++ b/src/main/java/org/springframework/samples/petclinic/common/CommonParameter.java @@ -6,6 +6,8 @@ public class CommonParameter { public static final int COUNTRY_MAX = 50; + public static final String DEFAULT_PROVIDER = "local"; + public static final int EMAIL_MAX = 255; public static final int EMAIL_MIN = 4; @@ -32,6 +34,8 @@ public class CommonParameter { public static final int STREET_MAX = 50; + public static final int TOKEN_EXPIRATION = 60 * 24; + public static final int ROLE_MAX = 10; public static final int ZIP_MAX = 6; diff --git a/src/main/java/org/springframework/samples/petclinic/common/CommonView.java b/src/main/java/org/springframework/samples/petclinic/common/CommonView.java index 5059e39b9..f9869ea64 100644 --- a/src/main/java/org/springframework/samples/petclinic/common/CommonView.java +++ b/src/main/java/org/springframework/samples/petclinic/common/CommonView.java @@ -23,7 +23,7 @@ public final class CommonView { public static final String PET_CREATE_OR_UPDATE = "pets/createOrUpdatePetForm"; - public static final String USER_REGISTRATION = "users/registration"; + public static final String USER_REGISTRATION = "users/userRegistrationForm"; public static final String USER_LOGIN = "/login"; @@ -31,7 +31,9 @@ public final class CommonView { public static final String USER_USERS_ID_R = "redirect:/users/{userId}"; - public static final String USER_CREATE_OR_UPDATE = "users/createOrUpdateUserForm"; + public static final String USER_UPDATE = "users/userUpdateForm"; + + public static final String USER_CHANGE_PASSWORD = "users/userChangePasswordForm"; public static final String USER_DETAILS = "users/userDetails"; @@ -41,8 +43,6 @@ public final class CommonView { public static final String USER_READ_R = "redirect:/users/read/"; - public static final String USER_UPDATE = "users/user-update"; - public static final String USER_UPDATE_R = "redirect:/users/edit/"; public static final String USER_UPDATE_PASSWORD = "users/user-password"; diff --git a/src/main/java/org/springframework/samples/petclinic/configuration/MailConfig.java b/src/main/java/org/springframework/samples/petclinic/configuration/MailConfig.java new file mode 100644 index 000000000..2dfbb7c7b --- /dev/null +++ b/src/main/java/org/springframework/samples/petclinic/configuration/MailConfig.java @@ -0,0 +1,110 @@ +package org.springframework.samples.petclinic.configuration; + +import org.springframework.beans.factory.annotation.Value; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.context.support.ResourceBundleMessageSource; +import org.springframework.mail.javamail.JavaMailSender; +import org.springframework.mail.javamail.JavaMailSenderImpl; +import org.thymeleaf.ITemplateEngine; +import org.thymeleaf.spring5.SpringTemplateEngine; +import org.thymeleaf.templatemode.TemplateMode; +import org.thymeleaf.templateresolver.ClassLoaderTemplateResolver; +import org.thymeleaf.templateresolver.ITemplateResolver; +import org.thymeleaf.templateresolver.StringTemplateResolver; + +import java.util.Collections; + +@Configuration +public class MailConfig { + + public static final String EMAIL_TEMPLATE_ENCODING = "UTF-8"; + + @Value("${spring.mail.host}") + private String mailHost; + + @Value("${spring.mail.port}") + private String mailPort; + + @Value("${spring.mail.protocol}") + private String mailProtocol; + + @Value("${spring.mail.username}") + private String mailUsername; + + @Value("${spring.mail.password}") + private String mailPassword; + + @Bean + public JavaMailSender mailSender() { + + final JavaMailSenderImpl mailSender = new JavaMailSenderImpl(); + + // Basic mail sender configuration, based on emailconfig.properties + mailSender.setHost(mailHost); + mailSender.setPort(Integer.parseInt(mailPort)); + mailSender.setProtocol(mailProtocol); + mailSender.setUsername(mailUsername); + mailSender.setPassword(mailPassword); + + return mailSender; + } + + @Bean + public ResourceBundleMessageSource emailMessageSource() { + final ResourceBundleMessageSource messageSource = new ResourceBundleMessageSource(); + messageSource.setBasename("mail/MailMessages"); + return messageSource; + } + + @Bean + public ITemplateEngine emailTemplateEngine() { + final SpringTemplateEngine emailTemplateEngine = new SpringTemplateEngine(); + + // Resolver for TEXT emails + emailTemplateEngine.addTemplateResolver(textTemplateResolver()); + // Resolver for HTML emails (except the editable one) + emailTemplateEngine.addTemplateResolver(htmlTemplateResolver()); + // Resolver for HTML editable emails (which will be treated as a String) + emailTemplateEngine.addTemplateResolver(stringTemplateResolver()); + // Message source, internationalization specific to emails + emailTemplateEngine.setTemplateEngineMessageSource(emailMessageSource()); + + return emailTemplateEngine; + } + + private ITemplateResolver textTemplateResolver() { + final ClassLoaderTemplateResolver templateResolver = new ClassLoaderTemplateResolver(); + templateResolver.setOrder(Integer.valueOf(1)); + templateResolver.setResolvablePatterns(Collections.singleton("text/*")); + templateResolver.setPrefix("/mail/"); + templateResolver.setSuffix(".txt"); + templateResolver.setTemplateMode(TemplateMode.TEXT); + templateResolver.setCharacterEncoding(EMAIL_TEMPLATE_ENCODING); + templateResolver.setCacheable(false); + return templateResolver; + } + + private ITemplateResolver htmlTemplateResolver() { + final ClassLoaderTemplateResolver templateResolver = new ClassLoaderTemplateResolver(); + templateResolver.setOrder(Integer.valueOf(2)); + templateResolver.setResolvablePatterns(Collections.singleton("html/*")); + templateResolver.setPrefix("/mail/"); + templateResolver.setSuffix(".html"); + templateResolver.setTemplateMode(TemplateMode.HTML); + templateResolver.setCharacterEncoding(EMAIL_TEMPLATE_ENCODING); + templateResolver.setCacheable(false); + return templateResolver; + } + + private ITemplateResolver stringTemplateResolver() { + final StringTemplateResolver templateResolver = new StringTemplateResolver(); + templateResolver.setOrder(Integer.valueOf(3)); + // No resolvable pattern, will simply process as a String template everything not + // previously matched + templateResolver.setTemplateMode("HTML5"); + templateResolver.setCacheable(false); + return templateResolver; + } + +} diff --git a/src/main/java/org/springframework/samples/petclinic/configuration/WebSecurityConfig.java b/src/main/java/org/springframework/samples/petclinic/configuration/WebSecurityConfig.java index 3238935cd..9461ca86f 100644 --- a/src/main/java/org/springframework/samples/petclinic/configuration/WebSecurityConfig.java +++ b/src/main/java/org/springframework/samples/petclinic/configuration/WebSecurityConfig.java @@ -6,7 +6,6 @@ import org.springframework.context.annotation.Configuration; import org.springframework.context.annotation.PropertySource; import org.springframework.core.env.Environment; -import org.springframework.samples.petclinic.model.common.AuthProvider; import org.springframework.security.authentication.AuthenticationManager; import org.springframework.security.config.annotation.web.builders.HttpSecurity; import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity; @@ -59,12 +58,13 @@ public class WebSecurityConfig extends WebSecurityConfigurerAdapter { // @formatter:off http.authorizeRequests() - .antMatchers("/").anonymous() - .antMatchers("/login", "/logout", "/register").permitAll() + .antMatchers("/").permitAll() + .antMatchers("/login", "/logout", "/register","/confirm-account").permitAll() .antMatchers("/websocket/**", "/topic/**", "/app/**").permitAll() .antMatchers("/resources/**").permitAll() - .antMatchers("/**").authenticated() .antMatchers("/h2-console/**").permitAll() + .antMatchers("/**").authenticated() + .antMatchers("/edit/**").authenticated() .anyRequest().authenticated() .and() .formLogin() @@ -94,10 +94,12 @@ public class WebSecurityConfig extends WebSecurityConfigurerAdapter { // @formatter:on } - private static final List clients = Arrays.asList("google", "facebook", "github"); + @Bean public ClientRegistrationRepository clientRegistrationRepository() { + List clients = Arrays.asList("google", "facebook", "github"); + List registrations = clients.stream().map(c -> getRegistration(c)) .filter(registration -> registration != null).collect(Collectors.toList()); @@ -113,14 +115,14 @@ public class WebSecurityConfig extends WebSecurityConfigurerAdapter { String clientSecret = env.getProperty(CLIENT_PROPERTY_KEY + client + ".client-secret"); - if (client.equals(AuthProvider.google.name())) { + if (client.equals("google")) { return CommonOAuth2Provider.GOOGLE.getBuilder(client).clientId(clientId).clientSecret(clientSecret).build(); } - if (client.equals(AuthProvider.facebook.name())) { + if (client.equals("facebook")) { return CommonOAuth2Provider.FACEBOOK.getBuilder(client).clientId(clientId).clientSecret(clientSecret) .build(); } - if (client.equals(AuthProvider.github.name())) { + if (client.equals("github")) { return CommonOAuth2Provider.GITHUB.getBuilder(client).clientId(clientId).clientSecret(clientSecret).build(); } diff --git a/src/main/java/org/springframework/samples/petclinic/controller/OwnerController.java b/src/main/java/org/springframework/samples/petclinic/controller/OwnerController.java index a47289e54..cfdbd9a5b 100644 --- a/src/main/java/org/springframework/samples/petclinic/controller/OwnerController.java +++ b/src/main/java/org/springframework/samples/petclinic/controller/OwnerController.java @@ -18,8 +18,8 @@ package org.springframework.samples.petclinic.controller; import org.springframework.samples.petclinic.common.*; import org.springframework.samples.petclinic.controller.common.WebSocketSender; import org.springframework.samples.petclinic.dto.*; -import org.springframework.samples.petclinic.service.OwnerService; -import org.springframework.samples.petclinic.service.VisitService; +import org.springframework.samples.petclinic.service.business.OwnerService; +import org.springframework.samples.petclinic.service.business.VisitService; import org.springframework.stereotype.Controller; import org.springframework.ui.Model; import org.springframework.validation.BindingResult; diff --git a/src/main/java/org/springframework/samples/petclinic/controller/PetController.java b/src/main/java/org/springframework/samples/petclinic/controller/PetController.java index 1cae0d2e3..b49786ccd 100644 --- a/src/main/java/org/springframework/samples/petclinic/controller/PetController.java +++ b/src/main/java/org/springframework/samples/petclinic/controller/PetController.java @@ -19,8 +19,10 @@ import org.springframework.beans.factory.annotation.Autowired; import org.springframework.samples.petclinic.common.*; import org.springframework.samples.petclinic.controller.common.WebSocketSender; import org.springframework.samples.petclinic.dto.*; +import org.springframework.samples.petclinic.service.business.OwnerService; +import org.springframework.samples.petclinic.service.business.PetService; +import org.springframework.samples.petclinic.service.business.PetTypeService; import org.springframework.samples.petclinic.validator.PetDTOValidator; -import org.springframework.samples.petclinic.service.*; import org.springframework.stereotype.Controller; import org.springframework.ui.ModelMap; import org.springframework.util.StringUtils; diff --git a/src/main/java/org/springframework/samples/petclinic/controller/UserController.java b/src/main/java/org/springframework/samples/petclinic/controller/UserController.java index e0d5b93a4..3555e68a2 100644 --- a/src/main/java/org/springframework/samples/petclinic/controller/UserController.java +++ b/src/main/java/org/springframework/samples/petclinic/controller/UserController.java @@ -3,23 +3,15 @@ package org.springframework.samples.petclinic.controller; import lombok.extern.slf4j.Slf4j; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.core.ResolvableType; -import org.springframework.http.HttpEntity; -import org.springframework.http.HttpHeaders; -import org.springframework.http.HttpMethod; -import org.springframework.http.ResponseEntity; -import org.springframework.samples.petclinic.common.CommonAttribute; -import org.springframework.samples.petclinic.common.CommonEndPoint; -import org.springframework.samples.petclinic.common.CommonView; -import org.springframework.samples.petclinic.common.CommonWebSocket; +import org.springframework.data.repository.query.Param; +import org.springframework.samples.petclinic.common.*; import org.springframework.samples.petclinic.controller.common.WebSocketSender; -import org.springframework.samples.petclinic.dto.UserDTO; -import org.springframework.samples.petclinic.service.RoleService; -import org.springframework.samples.petclinic.service.SecurityServiceImpl; -import org.springframework.samples.petclinic.service.UserService; +import org.springframework.samples.petclinic.dto.common.CredentialDTO; +import org.springframework.samples.petclinic.dto.common.MessageDTO; +import org.springframework.samples.petclinic.dto.common.UserDTO; +import org.springframework.samples.petclinic.service.common.*; import org.springframework.security.core.Authentication; import org.springframework.security.core.context.SecurityContextHolder; -import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder; -import org.springframework.security.oauth2.client.OAuth2AuthorizedClient; import org.springframework.security.oauth2.client.OAuth2AuthorizedClientService; import org.springframework.security.oauth2.client.authentication.OAuth2AuthenticationToken; import org.springframework.security.oauth2.client.registration.ClientRegistration; @@ -27,18 +19,16 @@ import org.springframework.security.oauth2.client.registration.ClientRegistratio import org.springframework.security.web.authentication.logout.SecurityContextLogoutHandler; import org.springframework.stereotype.Controller; import org.springframework.ui.Model; -import org.springframework.util.StringUtils; import org.springframework.validation.BindingResult; import org.springframework.web.bind.WebDataBinder; import org.springframework.web.bind.annotation.*; -import org.springframework.web.client.RestTemplate; import org.springframework.web.servlet.ModelAndView; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import javax.validation.Valid; -import java.security.Principal; import java.util.HashMap; +import java.util.Locale; import java.util.Map; /** @@ -48,30 +38,25 @@ import java.util.Map; @Controller public class UserController extends WebSocketSender { - private static final String ROLE_ADMIN = "ADMIN"; - - private static final String ROLE_STAFF = "STAFF"; - - private static final String ROLE_USER = "USER"; - - private static final String authorizationRequestBaseUri = "oauth2/authorization"; - private final UserService userService; + private final CredentialService credentialService; + private final RoleService roleService; private final SecurityServiceImpl securityService; - private final BCryptPasswordEncoder bCryptPasswordEncoder; + private final EmailService emailService; - public UserController(UserService userService, RoleService roleService, SecurityServiceImpl securityService, - BCryptPasswordEncoder bCryptPasswordEncoder) { + public UserController(UserService userService, CredentialService credentialService, RoleService roleService, SecurityServiceImpl securityService, EmailService emailService) { this.userService = userService; + this.credentialService = credentialService; this.roleService = roleService; this.securityService = securityService; - this.bCryptPasswordEncoder = bCryptPasswordEncoder; + this.emailService = emailService; } + @InitBinder("user") public void setAllowedFields(WebDataBinder dataBinder) { dataBinder.setDisallowedFields(CommonAttribute.USER_ID); @@ -89,36 +74,48 @@ public class UserController extends WebSocketSender { public String initCreationForm(Map model) { UserDTO user = new UserDTO(); model.put(CommonAttribute.USER, user); - return CommonView.USER_CREATE_OR_UPDATE; + return CommonView.USER_REGISTRATION; } @PostMapping(CommonEndPoint.REGISTER) public String processCreationForm(@ModelAttribute(CommonAttribute.USER) @Valid UserDTO user, BindingResult result) { if (result.hasErrors()) { sendErrorMessage(CommonWebSocket.USER_CREATION_ERROR); - return CommonView.USER_CREATE_OR_UPDATE; + return CommonView.USER_REGISTRATION; } - try { - userService.findByEmail(user.getEmail()); + if(userService.existByEmail(user.getEmail())) { result.rejectValue("email", "5", "Email already exist !"); sendErrorMessage(CommonWebSocket.USER_CREATION_ERROR); - return CommonView.USER_CREATE_OR_UPDATE; - } - catch (Exception ex) { + return CommonView.USER_REGISTRATION; } // set default role - user.addRole(roleService.findByName(ROLE_USER)); + user.addRole(roleService.findByName("ROLE_USER")); // encode password because we get clear password - user.setPassword(bCryptPasswordEncoder.encode(user.getPassword())); - user.setMatchingPassword(user.getPassword()); - + user.encode(user.getPassword()); user = this.userService.save(user); - sendSuccessMessage(CommonWebSocket.USER_CREATED); - return CommonView.HOME + user.getId(); + CredentialDTO credential = new CredentialDTO(user); + credential = credentialService.save(credential); + + sendSuccessMessage(CommonWebSocket.USER_CREATED); + + // send confirmation mail + MessageDTO message = new MessageDTO( + user.getFirstName(), user.getLastName(), + "admin@petclinic.com", + user.getEmail(), + "New connexion", + "Your attempt to create new account. To confirm your account, please click here : ", + "http://localhost:8080/confirm-account?token=" + credential.getToken()); + + // emailService.sendMailAsynch(message, Locale.getDefault()); + + log.info(message.toString()); + + return CommonView.HOME + user.getId(); } @GetMapping(CommonEndPoint.LOGIN) @@ -140,7 +137,7 @@ public class UserController extends WebSocketSender { } clientRegistrations.forEach(registration -> oauth2AuthenticationUrls.put(registration.getClientName(), - authorizationRequestBaseUri + "/" + registration.getRegistrationId())); + "oauth2/authorization/" + registration.getRegistrationId())); model.put("urls", oauth2AuthenticationUrls); return CommonView.USER_LOGIN; @@ -148,30 +145,85 @@ public class UserController extends WebSocketSender { @GetMapping(CommonEndPoint.LOGIN_SUCCESS) public String postLogin(Model model, Authentication authentication) { - UserDTO user = userService.findByEmail(authentication.getName()); + UserDTO user = (UserDTO) authentication.getPrincipal(); model.addAttribute(CommonAttribute.USER, user); String message = String.format(CommonWebSocket.USER_LOGGED_IN, user.getFirstName(), user.getLastName()); - sendSuccessMessage(message ); + sendSuccessMessage(message); return CommonView.HOME; } @GetMapping(CommonEndPoint.OAUTH2_SUCCESS) public String postLogin(Model model, OAuth2AuthenticationToken authentication) { + String firstName = authentication.getPrincipal().getAttribute("given_name"); + String lastName = authentication.getPrincipal().getAttribute("family_name"); - OAuth2AuthorizedClient client = authorizedClientService .loadAuthorizedClient(authentication.getAuthorizedClientRegistrationId(), authentication.getName()); + CredentialDTO credential = credentialService.findByAuthentication(authentication); - String userInfoEndpointUri = client.getClientRegistration().getProviderDetails().getUserInfoEndpoint().getUri(); + if( credential.isNew()) { - UserDTO user = userService.findByEmail(authentication.getName()); + // first time authentification with this provider + credential = credentialService.saveNew(authentication); + String email = credential.getEmail(); - if( user!=null) { - model.addAttribute(CommonAttribute.USER, user); + UserDTO user = userService.findByEmail(email); - String message = String.format(CommonWebSocket.USER_LOGGED_IN, user.getFirstName(), user.getLastName()); + if(user == null) { + user = new UserDTO(); + user.setEmail(email); + user.encode(credential.getPassword()); + user.setFirstName(firstName); + user.setLastName(lastName); + user.setEnabled(true); + user.addRole(roleService.findByName("ROLE_USER")); + user = userService.save(user); + } + + // send confirmation mail + MessageDTO message = new MessageDTO( + firstName, lastName, + "admin@petclinic.com", + credential.getEmail(), + "New connexion from " + credential.getProvider(), + "Your attempt to connect from " + credential.getProvider() + " To confirm this connection, please click the link below : ", + "http://localhost:8080/confirm-account?token=" + credential.getToken()); + + log.info(message.toString()); + emailService.sendMailAsynch(message, Locale.getDefault()); + + // disconnect + authentication.eraseCredentials(); + SecurityContextHolder.clearContext(); + + } else if( credential.isVerified()) { + securityService.autoLogin(credential.getEmail(),credential.getPassword()); + String message = String.format(CommonWebSocket.USER_LOGGED_IN, firstName, lastName); sendSuccessMessage(message); } + + + return CommonView.HOME; + } + + @RequestMapping(value = CommonEndPoint.CONFIRM_ACCOUNT, method = { RequestMethod.GET, RequestMethod.POST }) + public String confirmUserAccount(@RequestParam(CommonAttribute.TOKEN) String token, Model model) { + CredentialDTO credential = credentialService.findByToken(token); + + if (!credential.isNew() && credential.isNotExpired()) { + credential.setVerified(true); + credential.setToken(""); + credential.setExpiration(null); + credential = credentialService.save(credential); + + // find corresponding user + UserDTO user = userService.findByEmail(credential.getEmail()); + + securityService.autoLogin(credential.getEmail(),credential.getPassword()); + model.addAttribute(CommonAttribute.USER, user); + return CommonView.USER_UPDATE; + } + return CommonView.HOME; } @@ -188,43 +240,48 @@ public class UserController extends WebSocketSender { @GetMapping(CommonEndPoint.LOGOUT_SUCCESS) public String postLogout(Model model) { - sendSuccessMessage(CommonWebSocket.USER_LOGGED_OUT); return CommonView.HOME; } @GetMapping(CommonEndPoint.USERS_EDIT) public String initUpdateOwnerForm(Model model) { - Authentication authentication = SecurityContextHolder.getContext().getAuthentication(); - - if (!authentication.getName().equals("anonymousUser")) { - - UserDTO user = userService.findByEmail(authentication.getName()); + try { + UserDTO user = (UserDTO) SecurityContextHolder.getContext().getAuthentication().getPrincipal(); model.addAttribute(CommonAttribute.USER, user); - return CommonView.USER_CREATE_OR_UPDATE; + model.addAttribute(CommonAttribute.USER_ID, user.getId()); + return CommonView.USER_UPDATE; + } catch (Exception exception) { + // user don't have profile } return CommonView.HOME; } - @PostMapping(CommonEndPoint.USERS_ID_EDIT) + @PostMapping(CommonEndPoint.USERS_EDIT) public String processUpdateOwnerForm(@ModelAttribute(CommonAttribute.USER) @Valid UserDTO user, - BindingResult result, @PathVariable("userId") int userId) { + BindingResult result, Model model) { if (result.hasErrors()) { sendErrorMessage(CommonWebSocket.USER_UPDATED_ERROR); - return CommonView.USER_CREATE_OR_UPDATE; + return CommonView.USER_UPDATE; } - else { - user.setId(userId); - this.userService.save(user); + if(!user.getPassword().equals(user.getMatchingPassword())) { + sendErrorMessage(CommonWebSocket.USER_UPDATED_ERROR); + return CommonView.USER_UPDATE; + } + + else { + user = userService.save(user); + + model.addAttribute(CommonAttribute.USER, user); sendSuccessMessage(CommonWebSocket.USER_UPDATED); - return CommonView.USER_USERS_ID_R; + return CommonView.HOME; } } @GetMapping(CommonEndPoint.USERS_ID) - public ModelAndView showOwner(@PathVariable("userId") int userId) { + public ModelAndView showOwner(@PathVariable("userId") Integer userId) { ModelAndView modelAndView = new ModelAndView(CommonView.USER_DETAILS); UserDTO user = this.userService.findById(userId); @@ -232,4 +289,62 @@ public class UserController extends WebSocketSender { return modelAndView; } + @GetMapping("/user/{userId}/edit/password") + public String editPassword(@PathVariable("userId") Integer userId, Model model){ + try { + UserDTO operator = (UserDTO) SecurityContextHolder.getContext().getAuthentication().getPrincipal(); + UserDTO user = userService.findById(userId); + + if (user.equals(operator) || operator.getRoles().contains(roleService.findByName("ROLE_ADMIN"))) { + model.addAttribute(CommonAttribute.USER, user); + model.addAttribute(CommonAttribute.USER_ID, user.getId()); + return CommonView.USER_CHANGE_PASSWORD; + } + } catch (Exception exception) { + // user don't have profile + } + + return CommonView.HOME; + } + + @PostMapping("/user/{userId}/edit/password") + public String updatePassword(@ModelAttribute(CommonAttribute.USER) @Valid UserDTO user, BindingResult bindingResult, + @PathVariable(CommonAttribute.USER_ID) Integer userId, + @Param("oldPassword") String oldPassword, + @Param("newPassword") String newPassword, + @Param("newMatchingPassword") String newMatchingPassword, Model model) { + + // verify the matching with old password + if(!user.matches(oldPassword)){ + bindingResult.rejectValue("password", "6", "Bad password !"); + model.addAttribute(CommonAttribute.USER, user); + return CommonView.USER_CHANGE_PASSWORD; + } + + // verify matching between two password + if(!newPassword.equals(newMatchingPassword)){ + bindingResult.rejectValue("password", "7", "Bad matching password !"); + model.addAttribute(CommonAttribute.USER, user); + return CommonView.USER_CHANGE_PASSWORD; + } + + try { + UserDTO operator = (UserDTO) SecurityContextHolder.getContext().getAuthentication().getPrincipal(); + + if (user.equals(operator) || operator.getRoles().contains(roleService.findByName("ROLE_ADMIN"))) { + // encode password + user.encode(newPassword); + user = userService.save(user); + + model.addAttribute(CommonAttribute.USER, user); + return CommonView.USER_UPDATE_R; + } + } catch (NullPointerException exception) { + log.error(exception.getMessage()); + } + + return CommonView.HOME; + } + + } diff --git a/src/main/java/org/springframework/samples/petclinic/controller/VetController.java b/src/main/java/org/springframework/samples/petclinic/controller/VetController.java index b10b42caa..d4f44c895 100644 --- a/src/main/java/org/springframework/samples/petclinic/controller/VetController.java +++ b/src/main/java/org/springframework/samples/petclinic/controller/VetController.java @@ -21,7 +21,7 @@ import org.springframework.samples.petclinic.common.CommonView; import org.springframework.samples.petclinic.common.CommonWebSocket; import org.springframework.samples.petclinic.controller.common.WebSocketSender; import org.springframework.samples.petclinic.dto.VetsDTO; -import org.springframework.samples.petclinic.service.VetService; +import org.springframework.samples.petclinic.service.business.VetService; import org.springframework.stereotype.Controller; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.ResponseBody; diff --git a/src/main/java/org/springframework/samples/petclinic/controller/VisitController.java b/src/main/java/org/springframework/samples/petclinic/controller/VisitController.java index 1709fd43d..a8d05e654 100644 --- a/src/main/java/org/springframework/samples/petclinic/controller/VisitController.java +++ b/src/main/java/org/springframework/samples/petclinic/controller/VisitController.java @@ -26,8 +26,8 @@ import org.springframework.samples.petclinic.common.CommonWebSocket; import org.springframework.samples.petclinic.controller.common.WebSocketSender; import org.springframework.samples.petclinic.dto.PetDTO; import org.springframework.samples.petclinic.dto.VisitDTO; -import org.springframework.samples.petclinic.service.PetService; -import org.springframework.samples.petclinic.service.VisitService; +import org.springframework.samples.petclinic.service.business.PetService; +import org.springframework.samples.petclinic.service.business.VisitService; import org.springframework.samples.petclinic.validator.VisitDTOValidator; import org.springframework.stereotype.Controller; import org.springframework.validation.BindingResult; diff --git a/src/main/java/org/springframework/samples/petclinic/dto/common/AuthProviderDTO.java b/src/main/java/org/springframework/samples/petclinic/dto/common/AuthProviderDTO.java new file mode 100644 index 000000000..480a7714e --- /dev/null +++ b/src/main/java/org/springframework/samples/petclinic/dto/common/AuthProviderDTO.java @@ -0,0 +1,12 @@ +package org.springframework.samples.petclinic.dto.common; + +import org.springframework.samples.petclinic.dto.NamedDTO; + +/** + * Simple Data Transfert Object representing a Authorization Provider. + * + * @author Paul-Emmanuel DOS SANTOS FACAO + */ +public class AuthProviderDTO extends NamedDTO { + +} diff --git a/src/main/java/org/springframework/samples/petclinic/dto/common/CredentialDTO.java b/src/main/java/org/springframework/samples/petclinic/dto/common/CredentialDTO.java new file mode 100644 index 000000000..d505778ab --- /dev/null +++ b/src/main/java/org/springframework/samples/petclinic/dto/common/CredentialDTO.java @@ -0,0 +1,122 @@ +package org.springframework.samples.petclinic.dto.common; + +import org.springframework.samples.petclinic.common.CommonError; +import org.springframework.samples.petclinic.common.CommonParameter; +import org.springframework.samples.petclinic.dto.BaseDTO; +import javax.validation.constraints.NotNull; +import javax.validation.constraints.Pattern; +import javax.validation.constraints.Size; +import java.sql.Timestamp; +import java.time.Instant; +import java.util.Calendar; +import java.util.Date; +import java.util.UUID; + +/** + * Simple Data Transfert Object representing a Credential. + * + * @author Paul-Emmanuel DOS SANTOS FACAO + */ +public class CredentialDTO extends BaseDTO { + + @NotNull + private String provider; + + @NotNull + @Size(min = CommonParameter.EMAIL_MIN, max = CommonParameter.EMAIL_MAX, message = CommonError.FORMAT_BETWEEN + + CommonParameter.EMAIL_MIN + " AND " + CommonParameter.EMAIL_MAX + " !") + @Pattern(regexp = CommonParameter.EMAIL_REGEXP, message = CommonError.EMAIL_FORMAT) + private String email; + + @NotNull + private Boolean verified; + + private String token; + + private Date expiration; + + @NotNull + @Size(min = CommonParameter.PASSWORD_MIN, max = CommonParameter.PASSWORD_MAX, message = CommonError.FORMAT_BETWEEN + + CommonParameter.PASSWORD_MIN + " AND " + CommonParameter.PASSWORD_MAX + " !") + private String password; + + public CredentialDTO(UserDTO user) { + this.verified = false; + this.setToken(); + this.setExpiration(); + this.setProvider(CommonParameter.DEFAULT_PROVIDER); + this.email = user.getEmail(); + this.password = user.getId().toString(); + } + + public String getProvider() { + return provider; + } + + public void setProvider(String provider) { + this.provider = provider; + } + + public void setDefaultProvider() { + this.provider = CommonParameter.DEFAULT_PROVIDER; + } + + + public String getEmail() { + return email; + } + + public void setEmail(String email) { + this.email = email; + } + + public Boolean isVerified() { + return verified; + } + + public void setVerified(Boolean verified) { + this.verified = verified; + } + + public String getToken() { + return token; + } + + public void setToken(String token) { + this.token = token; + } + + public Date getExpiration() { + return expiration; + } + + public void setExpiration(Date expiration) { + this.expiration = expiration; + } + + public String getPassword() { + return password; + } + + public void setPassword(String password) { + this.password = password; + } + + public void setExpiration() { + Calendar cal = Calendar.getInstance(); + cal.setTime(new Timestamp(cal.getTime().getTime())); + cal.add(Calendar.MINUTE, CommonParameter.TOKEN_EXPIRATION); + this.expiration = cal.getTime(); + } + + public void setToken() { + this.token = UUID.randomUUID().toString(); + } + + public boolean isNotExpired() { + Calendar cal = Calendar.getInstance(); + cal.setTime(new Timestamp(cal.getTime().getTime())); + + return this.expiration.after(Date.from(Instant.now())); + } +} diff --git a/src/main/java/org/springframework/samples/petclinic/dto/common/MessageDTO.java b/src/main/java/org/springframework/samples/petclinic/dto/common/MessageDTO.java new file mode 100644 index 000000000..f9602a61e --- /dev/null +++ b/src/main/java/org/springframework/samples/petclinic/dto/common/MessageDTO.java @@ -0,0 +1,126 @@ +package org.springframework.samples.petclinic.dto.common; + +import com.fasterxml.jackson.annotation.JsonCreator; +import com.fasterxml.jackson.annotation.JsonProperty; +import org.springframework.samples.petclinic.common.CommonError; +import org.springframework.samples.petclinic.common.CommonParameter; + +import javax.validation.constraints.NotNull; +import javax.validation.constraints.Pattern; +import javax.validation.constraints.Size; +import java.io.Serializable; + +public class MessageDTO implements Serializable { + + @NotNull + @Size(min = CommonParameter.FIRSTNAME_MIN, max = CommonParameter.FIRSTNAME_MAX, message = CommonError.FORMAT_BETWEEN + + CommonParameter.FIRSTNAME_MIN + " AND " + CommonParameter.FIRSTNAME_MAX + " !") + private String firstName; + + @NotNull + @Size(min = CommonParameter.LASTNAME_MIN, max = CommonParameter.LASTNAME_MAX, message = CommonError.FORMAT_BETWEEN + + CommonParameter.LASTNAME_MIN + " AND " + CommonParameter.LASTNAME_MAX + " !") + private String lastName; + + @NotNull + @Size(min = CommonParameter.EMAIL_MIN, max = CommonParameter.EMAIL_MAX, message = CommonError.FORMAT_BETWEEN + + CommonParameter.EMAIL_MIN + " AND " + CommonParameter.EMAIL_MAX + " !") + @Pattern(regexp = CommonParameter.EMAIL_REGEXP, message = CommonError.EMAIL_FORMAT) + private String from; + + @NotNull + @Size(min = CommonParameter.EMAIL_MIN, max = CommonParameter.EMAIL_MAX, message = CommonError.FORMAT_BETWEEN + + CommonParameter.EMAIL_MIN + " AND " + CommonParameter.EMAIL_MAX + " !") + @Pattern(regexp = CommonParameter.EMAIL_REGEXP, message = CommonError.EMAIL_FORMAT) + private String to; + + @NotNull + private String subject; + + @NotNull + private String content; + + private String link; + + @JsonCreator + public MessageDTO(@JsonProperty("firstName") String firstName, @JsonProperty("lastName") String lastName, + @JsonProperty("from") String from, @JsonProperty("to") String to, @JsonProperty("subject") String subject, + @JsonProperty("content") String content, @JsonProperty("link") String link) { + this.firstName = firstName; + this.lastName = lastName; + this.from = from; + this.to = to; + this.subject = subject; + this.content = content; + this.link = link; + } + + public String getFirstName() { + return firstName; + } + + public void setFirstName(String firstName) { + this.firstName = firstName; + } + + public String getLastName() { + return lastName; + } + + public void setLastName(String lastName) { + this.lastName = lastName; + } + + public String getFrom() { + return from; + } + + public void setFrom(String from) { + this.from = from; + } + + public String getTo() { + return to; + } + + public void setTo(String to) { + this.to = to; + } + + public String getSubject() { + return subject; + } + + public void setSubject(String subject) { + this.subject = subject; + } + + public String getContent() { + return content; + } + + public void setContent(String content) { + this.content = content; + } + + public String getLink() { + return link; + } + + public void setLink(String link) { + this.link = link; + } + + @Override + public String toString() { + return "MessageDTO{" + + "first name='" + firstName + '\'' + + ", last name='" + lastName + '\'' + + ", from='" + from + '\'' + + ", to='" + to + '\'' + + ", subject='" + subject + '\'' + + ", content='" + content + '\'' + + ", link='" + link + '\'' + + '}'; + } +} diff --git a/src/main/java/org/springframework/samples/petclinic/dto/RoleDTO.java b/src/main/java/org/springframework/samples/petclinic/dto/common/RoleDTO.java similarity index 64% rename from src/main/java/org/springframework/samples/petclinic/dto/RoleDTO.java rename to src/main/java/org/springframework/samples/petclinic/dto/common/RoleDTO.java index ed12efc05..b578c8764 100644 --- a/src/main/java/org/springframework/samples/petclinic/dto/RoleDTO.java +++ b/src/main/java/org/springframework/samples/petclinic/dto/common/RoleDTO.java @@ -1,4 +1,6 @@ -package org.springframework.samples.petclinic.dto; +package org.springframework.samples.petclinic.dto.common; + +import org.springframework.samples.petclinic.dto.NamedDTO; import java.io.Serializable; @@ -8,5 +10,4 @@ import java.io.Serializable; * @author Paul-Emmanuel DOS SANTOS FACAO */ public class RoleDTO extends NamedDTO implements Serializable { - } diff --git a/src/main/java/org/springframework/samples/petclinic/dto/UserDTO.java b/src/main/java/org/springframework/samples/petclinic/dto/common/UserDTO.java similarity index 54% rename from src/main/java/org/springframework/samples/petclinic/dto/UserDTO.java rename to src/main/java/org/springframework/samples/petclinic/dto/common/UserDTO.java index da81f2e9c..b39570f18 100644 --- a/src/main/java/org/springframework/samples/petclinic/dto/UserDTO.java +++ b/src/main/java/org/springframework/samples/petclinic/dto/common/UserDTO.java @@ -1,37 +1,48 @@ -package org.springframework.samples.petclinic.dto; +package org.springframework.samples.petclinic.dto.common; import org.springframework.beans.support.MutableSortDefinition; import org.springframework.beans.support.PropertyComparator; import org.springframework.samples.petclinic.common.CommonError; import org.springframework.samples.petclinic.common.CommonParameter; +import org.springframework.samples.petclinic.dto.PersonDTO; +import org.springframework.security.core.GrantedAuthority; +import org.springframework.security.core.authority.SimpleGrantedAuthority; +import org.springframework.security.core.userdetails.UserDetails; +import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder; import javax.validation.constraints.Pattern; import javax.validation.constraints.Size; import javax.xml.bind.annotation.XmlElement; import java.io.Serializable; + import java.util.*; -public class UserDTO extends PersonDTO implements Serializable { - - @Size(min = CommonParameter.PASSWORD_MIN, max = CommonParameter.PASSWORD_MAX, message = CommonError.FORMAT_BETWEEN - + CommonParameter.PASSWORD_MIN + " AND " + CommonParameter.PASSWORD_MAX + " !") - private String password; - - @Size(min = CommonParameter.PASSWORD_MIN, max = CommonParameter.PASSWORD_MAX, message = CommonError.FORMAT_BETWEEN - + CommonParameter.PASSWORD_MIN + " AND " + CommonParameter.PASSWORD_MAX + " !") - private String matchingPassword; +public class UserDTO extends PersonDTO implements Serializable, UserDetails { @Size(min = CommonParameter.EMAIL_MIN, max = CommonParameter.EMAIL_MAX, message = CommonError.FORMAT_BETWEEN + CommonParameter.EMAIL_MIN + " AND " + CommonParameter.EMAIL_MAX + " !") @Pattern(regexp = CommonParameter.EMAIL_REGEXP, message = CommonError.EMAIL_FORMAT) private String email; - @Size(max = CommonParameter.PHONE_MAX, message = CommonError.FORMAT_LESS + CommonParameter.PHONE_MAX) - @Pattern(regexp = CommonParameter.PHONE_REGEXP, message = CommonError.PHONE_FORMAT) - private String telephone; + @Size(min = CommonParameter.PASSWORD_MIN, max = CommonParameter.PASSWORD_MAX, message = CommonError.FORMAT_BETWEEN + + CommonParameter.PASSWORD_MIN + " AND " + CommonParameter.PASSWORD_MAX + " !") + private String password; + @Size(min = CommonParameter.PASSWORD_MIN, max = CommonParameter.PASSWORD_MAX, message = CommonError.FORMAT_BETWEEN + + CommonParameter.PASSWORD_MIN + " AND " + CommonParameter.PASSWORD_MAX + " !") + private String matchingPassword; + + private boolean enabled; + private boolean accountNonExpired; + private boolean accountNonLocked; + private boolean credentialsNonExpired; private Set roles; + + @Size(max = CommonParameter.PHONE_MAX, message = CommonError.FORMAT_LESS + CommonParameter.PHONE_MAX) +// @Pattern(regexp = CommonParameter.PHONE_REGEXP, message = CommonError.PHONE_FORMAT) + private String telephone; + @Size(max = CommonParameter.STREET_MAX, message = CommonError.FORMAT_LESS + CommonParameter.STREET_MAX + " !") private String street1; @@ -51,6 +62,28 @@ public class UserDTO extends PersonDTO implements Serializable { @Size(max = CommonParameter.COUNTRY_MAX, message = CommonError.FORMAT_LESS + CommonParameter.COUNTRY_MAX + " !") private String country; + public UserDTO() { + super(); + this.enabled = false; + this.accountNonLocked = true; + this.accountNonExpired = true; + this.credentialsNonExpired = true; + } + + @Override + public String getUsername() { + return email; + } + + public String getEmail() { + return email; + } + + public void setEmail(String email) { + this.email = email; + } + + @Override public String getPassword() { return password; } @@ -67,20 +100,49 @@ public class UserDTO extends PersonDTO implements Serializable { this.matchingPassword = matchingPassword; } - public String getEmail() { - return email; + @Override + public boolean isEnabled() { + return enabled; } - public void setEmail(String email) { - this.email = email; + public void setEnabled(boolean enabled) { + this.enabled = enabled; } - public String getTelephone() { - return telephone; + @Override + public boolean isAccountNonExpired() { + return accountNonExpired; } - public void setTelephone(String telephone) { - this.telephone = telephone; + public void setAccountNonExpired(boolean accountNonExpired) { + this.accountNonExpired = accountNonExpired; + } + + @Override + public boolean isAccountNonLocked() { + return accountNonLocked; + } + + public void setAccountNonLocked(boolean accountNonLocked) { + this.accountNonLocked = accountNonLocked; + } + + @Override + public boolean isCredentialsNonExpired() { + return credentialsNonExpired; + } + + public void setCredentialsNonExpired(boolean credentialsNonExpired) { + this.credentialsNonExpired = credentialsNonExpired; + } + + @Override + public Collection getAuthorities() { + Set grantedAuthorities = new HashSet<>(); + + this.roles.forEach(role -> grantedAuthorities.add(new SimpleGrantedAuthority(role.getName()))); + + return grantedAuthorities; } protected Set getRolesInternal() { @@ -101,10 +163,27 @@ public class UserDTO extends PersonDTO implements Serializable { return Collections.unmodifiableList(sortedRoles); } + public int getNrOfRoles() { + return getRolesInternal().size(); + } + public void addRole(RoleDTO role) { getRolesInternal().add(role); } + public void setRoles(Set roles) { + this.roles = roles; + } + + public String getTelephone() { + return telephone; + } + + public void setTelephone(String telephone) { + this.telephone = telephone; + } + + public String getStreet1() { return street1; } @@ -153,4 +232,37 @@ public class UserDTO extends PersonDTO implements Serializable { this.country = country; } + @Override + public String toString() { + return "UserDTO{" + + "email='" + email + '\'' + + ", password='" + password + '\'' + + ", matchingPassword='" + matchingPassword + '\'' + + ", user enabled=" + enabled + + ", account not expired=" + accountNonExpired + + ", account not locked=" + accountNonLocked + + ", credentials not xxpired=" + credentialsNonExpired + + ", roles=" + roles + + ", telephone='" + telephone + '\'' + + ", street1='" + street1 + '\'' + + ", street2='" + street2 + '\'' + + ", street3='" + street3 + '\'' + + ", zipCode='" + zipCode + '\'' + + ", city='" + city + '\'' + + ", country='" + country + '\'' + + '}'; + } + + public void encode(String rawPassword) { + BCryptPasswordEncoder bCryptPasswordEncoder = new BCryptPasswordEncoder(); + + this.password = bCryptPasswordEncoder.encode(rawPassword); + this.matchingPassword = this.password; + } + + public boolean matches(String rawPassword) { + BCryptPasswordEncoder bCryptPasswordEncoder = new BCryptPasswordEncoder(); + + return bCryptPasswordEncoder.matches(rawPassword, this.password); + } } diff --git a/src/main/java/org/springframework/samples/petclinic/formatter/PetTypeFormatter.java b/src/main/java/org/springframework/samples/petclinic/formatter/PetTypeFormatter.java index a93537845..418980d03 100644 --- a/src/main/java/org/springframework/samples/petclinic/formatter/PetTypeFormatter.java +++ b/src/main/java/org/springframework/samples/petclinic/formatter/PetTypeFormatter.java @@ -21,8 +21,7 @@ import java.util.Locale; import org.springframework.format.Formatter; import org.springframework.samples.petclinic.dto.PetTypeDTO; -import org.springframework.samples.petclinic.service.PetService; -import org.springframework.samples.petclinic.service.PetTypeService; +import org.springframework.samples.petclinic.service.business.PetService; import org.springframework.stereotype.Component; /** diff --git a/src/main/java/org/springframework/samples/petclinic/model/common/AuthProvider.java b/src/main/java/org/springframework/samples/petclinic/model/common/AuthProvider.java index d6fe764f0..946c79f01 100644 --- a/src/main/java/org/springframework/samples/petclinic/model/common/AuthProvider.java +++ b/src/main/java/org/springframework/samples/petclinic/model/common/AuthProvider.java @@ -1,7 +1,15 @@ package org.springframework.samples.petclinic.model.common; -public enum AuthProvider { +import javax.persistence.Entity; +import javax.persistence.Table; - local, facebook, google, github +/** + * Class used to manage Authorization providers + * + * @author Paul-Emmanuel DOS SANTOS FACAO + */ +@Entity(name = "AuthProvider") +@Table(name = "auth_providers") +public class AuthProvider extends NamedEntity { } diff --git a/src/main/java/org/springframework/samples/petclinic/model/common/Credential.java b/src/main/java/org/springframework/samples/petclinic/model/common/Credential.java new file mode 100644 index 000000000..aa396018b --- /dev/null +++ b/src/main/java/org/springframework/samples/petclinic/model/common/Credential.java @@ -0,0 +1,112 @@ +package org.springframework.samples.petclinic.model.common; + +import org.springframework.samples.petclinic.common.CommonError; +import org.springframework.samples.petclinic.common.CommonParameter; + +import javax.persistence.*; +import javax.validation.constraints.NotNull; +import javax.validation.constraints.Pattern; +import javax.validation.constraints.Size; +import java.sql.Timestamp; +import java.util.Calendar; +import java.util.Date; +import java.util.UUID; + +/** + * Class used to manage Credentials for users + * + * @author Paul-Emmanuel DOS SANTOS FACAO + */ +@Entity(name = "Credential") +@Table(name = "credentials") +public class Credential extends BaseEntity { + private static final int TOKEN_EXPIRATION = 60 * 24; + + @NotNull + @Column(name = "provider_id") + private Integer providerId; + + @NotNull + @Size(min = CommonParameter.EMAIL_MIN, max = CommonParameter.EMAIL_MAX, message = CommonError.FORMAT_BETWEEN + + CommonParameter.EMAIL_MIN + " AND " + CommonParameter.EMAIL_MAX + " !") + @Pattern(regexp = CommonParameter.EMAIL_REGEXP, message = CommonError.EMAIL_FORMAT) + @Column(name = "email", length = CommonParameter.EMAIL_MAX) + private String email; + + @NotNull + @Size(min = CommonParameter.PASSWORD_MIN, max = CommonParameter.PASSWORD_MAX, message = CommonError.FORMAT_BETWEEN + + CommonParameter.PASSWORD_MIN + " AND " + CommonParameter.PASSWORD_MAX + " !") + @Column(name = "password", length = CommonParameter.PASSWORD_MAX) + private String password; + + @NotNull + @Column(name = "verified") + private Boolean verified; + + @Column(name = "token") + private String token; + + @Column(name = "expiration") + private Date expiration; + + + public Integer getProviderId() { + return providerId; + } + + public void setProviderId(Integer providerId) { + this.providerId = providerId; + } + + public String getEmail() { + return email; + } + + public void setEmail(String email) { + this.email = email; + } + + public Boolean isVerified() { + return verified; + } + + public void setVerified(Boolean verified) { + this.verified = verified; + } + + public String getToken() { + return token; + } + + public void setToken(String token) { + this.token = token; + } + + public Date getExpiration() { + return expiration; + } + + public void setExpiration(Date expirationDate) { + this.expiration = expirationDate; + } + + public String getPassword() { + return password; + } + + public void setPassword(String password) { + this.password = password; + } + + public void setExpiration() { + Calendar cal = Calendar.getInstance(); + cal.setTime(new Timestamp(cal.getTime().getTime())); + cal.add(Calendar.MINUTE, TOKEN_EXPIRATION); + this.expiration = cal.getTime(); + } + + public void setToken() { + this.token = UUID.randomUUID().toString(); + } + +} diff --git a/src/main/java/org/springframework/samples/petclinic/model/common/Role.java b/src/main/java/org/springframework/samples/petclinic/model/common/Role.java index 4aefcda52..353a14955 100644 --- a/src/main/java/org/springframework/samples/petclinic/model/common/Role.java +++ b/src/main/java/org/springframework/samples/petclinic/model/common/Role.java @@ -1,7 +1,5 @@ package org.springframework.samples.petclinic.model.common; -import org.springframework.samples.petclinic.model.common.NamedEntity; - import javax.persistence.*; import java.io.Serializable; diff --git a/src/main/java/org/springframework/samples/petclinic/model/common/User.java b/src/main/java/org/springframework/samples/petclinic/model/common/User.java index 61cae26bc..16bce7241 100644 --- a/src/main/java/org/springframework/samples/petclinic/model/common/User.java +++ b/src/main/java/org/springframework/samples/petclinic/model/common/User.java @@ -4,6 +4,9 @@ import org.springframework.beans.support.MutableSortDefinition; import org.springframework.beans.support.PropertyComparator; import org.springframework.samples.petclinic.common.CommonError; import org.springframework.samples.petclinic.common.CommonParameter; +import org.springframework.security.core.GrantedAuthority; +import org.springframework.security.core.authority.SimpleGrantedAuthority; +import org.springframework.security.core.userdetails.UserDetails; import javax.persistence.*; import javax.validation.constraints.NotNull; @@ -20,7 +23,7 @@ import java.util.*; */ @Entity(name = "User") @Table(name = "users") -public class User extends Person implements Serializable { +public class User extends Person implements Serializable, UserDetails { @NotNull @Size(min = CommonParameter.EMAIL_MIN, max = CommonParameter.EMAIL_MAX, message = CommonError.FORMAT_BETWEEN @@ -29,34 +32,38 @@ public class User extends Person implements Serializable { @Column(name = "email", unique = true, length = CommonParameter.EMAIL_MAX) private String email; - @NotNull - @Column(name = "email_verified") - private Boolean emailVerified = false; - - @NotNull @Size(min = CommonParameter.PASSWORD_MIN, max = CommonParameter.PASSWORD_MAX, message = CommonError.FORMAT_BETWEEN - + CommonParameter.PASSWORD_MIN + " AND " + CommonParameter.PASSWORD_MAX + " !") + + CommonParameter.PASSWORD_MIN + " AND " + CommonParameter.PASSWORD_MAX + " !") @Column(name = "password", length = CommonParameter.PASSWORD_MAX) private String password; @NotNull - @Enumerated(EnumType.STRING) - private AuthProvider provider; + @Column(name = "enabled") + private boolean enabled; - @Column(name = "provider_id") - private String providerId; + @NotNull + @Column(name = "account_unexpired") + private boolean accountNonExpired; - @Size(max = CommonParameter.PHONE_MAX, message = CommonError.FORMAT_LESS + CommonParameter.PHONE_MAX) - @Pattern(regexp = CommonParameter.PHONE_REGEXP, message = CommonError.PHONE_FORMAT) - @Column(name = "telephone", length = CommonParameter.EMAIL_MAX) - private String telephone; + @NotNull + @Column(name = "account_unlocked") + private boolean accountNonLocked; + + @NotNull + @Column(name = "credential_unexpired") + private boolean credentialsNonExpired; @ManyToMany(fetch = FetchType.EAGER) @JoinTable(name = "users_roles", joinColumns = @JoinColumn(name = "user_id"), - inverseJoinColumns = @JoinColumn(name = "role_id")) + inverseJoinColumns = @JoinColumn(name = "role_id")) private Set roles; - @NotNull + @Size(max = CommonParameter.PHONE_MAX, message = CommonError.FORMAT_LESS + CommonParameter.PHONE_MAX) +// @Pattern(regexp = CommonParameter.PHONE_REGEXP, message = CommonError.PHONE_FORMAT) + @Column(name = "telephone", length = CommonParameter.EMAIL_MAX) + private String telephone; + + @Size(max = CommonParameter.STREET_MAX, message = CommonError.FORMAT_LESS + CommonParameter.STREET_MAX + " !") @Column(name = "street1", length = CommonParameter.STREET_MAX) private String street1; @@ -69,12 +76,10 @@ public class User extends Person implements Serializable { @Column(name = "street3", length = CommonParameter.STREET_MAX) private String street3; - @NotNull @Size(max = CommonParameter.ZIP_MAX, message = CommonError.FORMAT_LESS + CommonParameter.ZIP_MAX + " !") @Column(name = "zip_code", length = CommonParameter.ZIP_MAX) private String zipCode; - @NotNull @Size(max = CommonParameter.CITY_MAX, message = CommonError.FORMAT_LESS + CommonParameter.CITY_MAX + " !") @Column(name = "city", length = CommonParameter.CITY_MAX) private String city; @@ -83,6 +88,11 @@ public class User extends Person implements Serializable { @Column(name = "country", length = CommonParameter.COUNTRY_MAX) private String country; + @Override + public String getUsername() { + return email; + } + public String getEmail() { return email; } @@ -91,14 +101,7 @@ public class User extends Person implements Serializable { this.email = email; } - public Boolean getEmailVerified() { - return emailVerified; - } - - public void setEmailVerified(Boolean emailVerified) { - this.emailVerified = emailVerified; - } - + @Override public String getPassword() { return password; } @@ -107,28 +110,50 @@ public class User extends Person implements Serializable { this.password = password; } - public AuthProvider getProvider() { - return provider; + @Override + public boolean isEnabled() { + return enabled; } - public void setProvider(AuthProvider provider) { - this.provider = provider; + public void setEnabled(boolean enabled) { + this.enabled = enabled; } - public String getProviderId() { - return providerId; + @Override + public boolean isAccountNonExpired() { + return accountNonExpired; } - public void setProviderId(String providerId) { - this.providerId = providerId; + public void setAccountNonExpired(boolean accountNonExpired) { + this.accountNonExpired = accountNonExpired; } - public String getTelephone() { - return telephone; + @Override + public boolean isAccountNonLocked() { + return accountNonLocked; } - public void setTelephone(String telephone) { - this.telephone = telephone; + public void setAccountNonLocked(boolean accountNonLocked) { + this.accountNonLocked = accountNonLocked; + } + + @Override + public boolean isCredentialsNonExpired() { + return credentialsNonExpired; + } + + public void setCredentialsNonExpired(boolean credentialsNonExpired) { + this.credentialsNonExpired = credentialsNonExpired; + } + + + @Override + public Collection getAuthorities() { + Set grantedAuthorities = new HashSet<>(); + + this.roles.forEach(role -> grantedAuthorities.add(new SimpleGrantedAuthority(role.getName()))); + + return grantedAuthorities; } protected Set getRolesInternal() { @@ -157,6 +182,20 @@ public class User extends Person implements Serializable { getRolesInternal().add(role); } + + public void setRoles(Set roles) { + this.roles = roles; + } + + public String getTelephone() { + return telephone; + } + + public void setTelephone(String telephone) { + this.telephone = telephone; + } + + public String getStreet1() { return street1; } @@ -205,4 +244,5 @@ public class User extends Person implements Serializable { this.country = country; } + } diff --git a/src/main/java/org/springframework/samples/petclinic/repository/AuthProviderRepository.java b/src/main/java/org/springframework/samples/petclinic/repository/AuthProviderRepository.java new file mode 100644 index 000000000..97d35b93c --- /dev/null +++ b/src/main/java/org/springframework/samples/petclinic/repository/AuthProviderRepository.java @@ -0,0 +1,43 @@ +package org.springframework.samples.petclinic.repository; + +import org.springframework.data.repository.Repository; +import org.springframework.samples.petclinic.model.common.AuthProvider; + +import java.util.List; + +/** + * Repository class for AuthProvider domain objects All method names are + * compliant with Spring Data naming conventions so this interface can easily be extended + * for Spring + * + * @author Paul-Emmanuel DOS SANTOS FACAO + */ +public interface AuthProviderRepository extends Repository { + + /** + * Retrieve a {@link AuthProvider} from the data store by id. + * @param providerId the id to search for + * @return the {@link AuthProvider} if found + */ + AuthProvider findById(Integer providerId); + + /** + * Retrieve a {@link AuthProvider} from the data store by id. + * @param providerName the name to search for + * @return the {@link AuthProvider} if found + */ + AuthProvider findByName(String providerName); + + /** + * Retrieve all {@link AuthProvider}s from the data store + * @return a Collection of {@link AuthProvider}s (or an empty Collection if none + */ + List findAll(); + + /** + * Save a {@link AuthProvider} to the data store, either inserting or updating it. + * @param authProvider the {@link AuthProvider} to save + */ + AuthProvider save(AuthProvider authProvider); + +} diff --git a/src/main/java/org/springframework/samples/petclinic/repository/CredentialRepository.java b/src/main/java/org/springframework/samples/petclinic/repository/CredentialRepository.java new file mode 100644 index 000000000..18ca70364 --- /dev/null +++ b/src/main/java/org/springframework/samples/petclinic/repository/CredentialRepository.java @@ -0,0 +1,51 @@ +package org.springframework.samples.petclinic.repository; + +import org.springframework.data.jpa.repository.Query; +import org.springframework.data.repository.Repository; +import org.springframework.data.repository.query.Param; +import org.springframework.samples.petclinic.model.common.Credential; +import org.springframework.transaction.annotation.Transactional; + +import java.util.List; + +/** + * Repository class for Credential domain objects All method names are + * compliant with Spring Data naming conventions so this interface can easily be extended + * for Spring + * + * @author Paul-Emmanuel DOS SANTOS FACAO + */ +public interface CredentialRepository extends Repository { + + /** + * Retrieve a {@link Credential} from the data store by email. + * @param email the email to search for + * @param providerId the provider to search for authorization + * @return the {@link Credential} if found + */ + @Query("SELECT c FROM Credential c WHERE c.email = :email AND c.providerId = :providerId") + @Transactional(readOnly = true) + Credential findByEmailAndProvider(@Param("email") String email, @Param("providerId") Integer providerId); + + /** + * * Retrieve a {@link Credential} from the data store by token. + * @param token the token to search for + * @return the {@link Credential} if found + */ + @Query("SELECT DISTINCT c FROM Credential c WHERE c.token = :token") + @Transactional(readOnly = true) + Credential findByToken(@Param("token") String token); + + /** + * Retrieve all {@link Credential}s from the data store + * @return a Collection of {@link Credential}s (or an empty Collection if none + */ + List findAll(); + + /** + * Save a {@link Credential} to the data store, either inserting or updating it. + * @param credential the {@link Credential} to save + */ + Credential save(Credential credential); + +} diff --git a/src/main/java/org/springframework/samples/petclinic/service/BaseService.java b/src/main/java/org/springframework/samples/petclinic/service/business/BaseService.java similarity index 94% rename from src/main/java/org/springframework/samples/petclinic/service/BaseService.java rename to src/main/java/org/springframework/samples/petclinic/service/business/BaseService.java index 3578d4ab2..d32206054 100644 --- a/src/main/java/org/springframework/samples/petclinic/service/BaseService.java +++ b/src/main/java/org/springframework/samples/petclinic/service/business/BaseService.java @@ -1,4 +1,4 @@ -package org.springframework.samples.petclinic.service; +package org.springframework.samples.petclinic.service.business; import java.util.List; diff --git a/src/main/java/org/springframework/samples/petclinic/service/OwnerService.java b/src/main/java/org/springframework/samples/petclinic/service/business/OwnerService.java similarity index 98% rename from src/main/java/org/springframework/samples/petclinic/service/OwnerService.java rename to src/main/java/org/springframework/samples/petclinic/service/business/OwnerService.java index b79cff504..05dfa5858 100644 --- a/src/main/java/org/springframework/samples/petclinic/service/OwnerService.java +++ b/src/main/java/org/springframework/samples/petclinic/service/business/OwnerService.java @@ -1,4 +1,4 @@ -package org.springframework.samples.petclinic.service; +package org.springframework.samples.petclinic.service.business; import org.modelmapper.ModelMapper; import org.modelmapper.internal.util.Lists; diff --git a/src/main/java/org/springframework/samples/petclinic/service/PetService.java b/src/main/java/org/springframework/samples/petclinic/service/business/PetService.java similarity index 98% rename from src/main/java/org/springframework/samples/petclinic/service/PetService.java rename to src/main/java/org/springframework/samples/petclinic/service/business/PetService.java index e331284a9..82c43182b 100644 --- a/src/main/java/org/springframework/samples/petclinic/service/PetService.java +++ b/src/main/java/org/springframework/samples/petclinic/service/business/PetService.java @@ -1,4 +1,4 @@ -package org.springframework.samples.petclinic.service; +package org.springframework.samples.petclinic.service.business; import org.modelmapper.ModelMapper; import org.springframework.samples.petclinic.dto.OwnerDTO; diff --git a/src/main/java/org/springframework/samples/petclinic/service/PetTypeService.java b/src/main/java/org/springframework/samples/petclinic/service/business/PetTypeService.java similarity index 96% rename from src/main/java/org/springframework/samples/petclinic/service/PetTypeService.java rename to src/main/java/org/springframework/samples/petclinic/service/business/PetTypeService.java index 498e61d6b..8afbb41db 100644 --- a/src/main/java/org/springframework/samples/petclinic/service/PetTypeService.java +++ b/src/main/java/org/springframework/samples/petclinic/service/business/PetTypeService.java @@ -1,4 +1,4 @@ -package org.springframework.samples.petclinic.service; +package org.springframework.samples.petclinic.service.business; import org.modelmapper.ModelMapper; import org.springframework.samples.petclinic.dto.PetTypeDTO; diff --git a/src/main/java/org/springframework/samples/petclinic/service/SpecialtyService.java b/src/main/java/org/springframework/samples/petclinic/service/business/SpecialtyService.java similarity index 96% rename from src/main/java/org/springframework/samples/petclinic/service/SpecialtyService.java rename to src/main/java/org/springframework/samples/petclinic/service/business/SpecialtyService.java index 98234a027..79f9de533 100644 --- a/src/main/java/org/springframework/samples/petclinic/service/SpecialtyService.java +++ b/src/main/java/org/springframework/samples/petclinic/service/business/SpecialtyService.java @@ -1,4 +1,4 @@ -package org.springframework.samples.petclinic.service; +package org.springframework.samples.petclinic.service.business; import org.modelmapper.ModelMapper; import org.springframework.samples.petclinic.dto.SpecialtyDTO; diff --git a/src/main/java/org/springframework/samples/petclinic/service/VetService.java b/src/main/java/org/springframework/samples/petclinic/service/business/VetService.java similarity index 97% rename from src/main/java/org/springframework/samples/petclinic/service/VetService.java rename to src/main/java/org/springframework/samples/petclinic/service/business/VetService.java index 11a7967fe..601969e5e 100644 --- a/src/main/java/org/springframework/samples/petclinic/service/VetService.java +++ b/src/main/java/org/springframework/samples/petclinic/service/business/VetService.java @@ -1,4 +1,4 @@ -package org.springframework.samples.petclinic.service; +package org.springframework.samples.petclinic.service.business; import org.modelmapper.ModelMapper; import org.modelmapper.internal.util.Lists; diff --git a/src/main/java/org/springframework/samples/petclinic/service/VisitService.java b/src/main/java/org/springframework/samples/petclinic/service/business/VisitService.java similarity index 97% rename from src/main/java/org/springframework/samples/petclinic/service/VisitService.java rename to src/main/java/org/springframework/samples/petclinic/service/business/VisitService.java index 76be1578c..5612565d7 100644 --- a/src/main/java/org/springframework/samples/petclinic/service/VisitService.java +++ b/src/main/java/org/springframework/samples/petclinic/service/business/VisitService.java @@ -1,4 +1,4 @@ -package org.springframework.samples.petclinic.service; +package org.springframework.samples.petclinic.service.business; import org.modelmapper.ModelMapper; import org.springframework.samples.petclinic.dto.VisitDTO; diff --git a/src/main/java/org/springframework/samples/petclinic/service/common/AuthProviderService.java b/src/main/java/org/springframework/samples/petclinic/service/common/AuthProviderService.java new file mode 100644 index 000000000..953b9bcc7 --- /dev/null +++ b/src/main/java/org/springframework/samples/petclinic/service/common/AuthProviderService.java @@ -0,0 +1,87 @@ +package org.springframework.samples.petclinic.service.common; + +import org.modelmapper.ModelMapper; +import org.springframework.samples.petclinic.dto.common.AuthProviderDTO; +import org.springframework.samples.petclinic.model.common.AuthProvider; +import org.springframework.samples.petclinic.repository.AuthProviderRepository; +import org.springframework.samples.petclinic.service.business.BaseService; +import org.springframework.stereotype.Service; + +import java.util.ArrayList; +import java.util.List; + +/** + * Simple Service between AuthProvider entity and AuthProviderDTO Data Transfert Object. + * + * @author Paul-Emmanuel DOS SANTOS FACAO + */ +@Service("AuthProviderService") +public class AuthProviderService implements BaseService { + + private final AuthProviderRepository authProviderRepository; + + private final ModelMapper modelMapper = new ModelMapper(); + + public AuthProviderService(AuthProviderRepository authProviderRepository) { + this.authProviderRepository = authProviderRepository; + } + + @Override + public AuthProvider dtoToEntity(AuthProviderDTO dto) { + if (dto != null) { + return modelMapper.map(dto, AuthProvider.class); + } + + return new AuthProvider(); + } + + @Override + public AuthProviderDTO entityToDTO(AuthProvider entity) { + if (entity != null) { + return modelMapper.map(entity, AuthProviderDTO.class); + } + + return new AuthProviderDTO(); + } + + @Override + public List entitiesToDTOS(List entities) { + List dtos = new ArrayList<>(); + + entities.forEach(entity -> dtos.add(entityToDTO(entity))); + + return dtos; + } + + @Override + public List dtosToEntities(List dtos) { + List entities = new ArrayList<>(); + + dtos.forEach(dto -> entities.add(dtoToEntity(dto))); + + return entities; + } + + @Override + public AuthProviderDTO findById(int providerId) { + return entityToDTO(authProviderRepository.findById(providerId)); + } + + @Override + public List findAll() { + return entitiesToDTOS(authProviderRepository.findAll()); + } + + @Override + public AuthProviderDTO save(AuthProviderDTO dto) { + AuthProvider authProvider = dtoToEntity(dto); + authProvider = authProviderRepository.save(authProvider); + + return entityToDTO(authProvider); + } + + public AuthProviderDTO findByName(String providerName) { + return entityToDTO(authProviderRepository.findByName(providerName)); + } + +} diff --git a/src/main/java/org/springframework/samples/petclinic/service/common/CredentialService.java b/src/main/java/org/springframework/samples/petclinic/service/common/CredentialService.java new file mode 100644 index 000000000..e9f5d2f15 --- /dev/null +++ b/src/main/java/org/springframework/samples/petclinic/service/common/CredentialService.java @@ -0,0 +1,129 @@ +package org.springframework.samples.petclinic.service.common; + +import org.modelmapper.ModelMapper; +import org.springframework.samples.petclinic.common.CommonParameter; +import org.springframework.samples.petclinic.dto.common.CredentialDTO; +import org.springframework.samples.petclinic.dto.common.UserDTO; +import org.springframework.samples.petclinic.model.common.AuthProvider; +import org.springframework.samples.petclinic.model.common.Credential; +import org.springframework.samples.petclinic.repository.AuthProviderRepository; +import org.springframework.samples.petclinic.repository.CredentialRepository; +import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder; +import org.springframework.security.oauth2.client.authentication.OAuth2AuthenticationToken; +import org.springframework.stereotype.Service; + +/** + * Simple Service between User entity and UserDTO Data Transfert Object. + * + * @author Paul-Emmanuel DOS SANTOS FACAO + */ +@Service("CredentialService") +public class CredentialService { + + private final CredentialRepository credentialRepository; + + private final BCryptPasswordEncoder bCryptPasswordEncoder; + + private final AuthProviderRepository authProviderRepository; + + private final ModelMapper modelMapper = new ModelMapper(); + + public CredentialService(CredentialRepository credentialRepository, BCryptPasswordEncoder bCryptPasswordEncoder, AuthProviderRepository authProviderRepository) { + this.credentialRepository = credentialRepository; + this.bCryptPasswordEncoder = bCryptPasswordEncoder; + this.authProviderRepository = authProviderRepository; + } + + public Credential dtoToEntity(CredentialDTO dto) { + if (dto != null) { + Credential entity = modelMapper.map(dto, Credential.class); + AuthProvider authProvider = authProviderRepository.findByName(dto.getProvider()); + if (authProvider == null) { + authProvider = authProviderRepository.findByName(CommonParameter.DEFAULT_PROVIDER); + } + entity.setProviderId(authProvider.getId()); + return entity; + } + + return new Credential(); + } + + public CredentialDTO entityToDTO(Credential entity) { + if (entity != null) { + CredentialDTO dto = modelMapper.map(entity, CredentialDTO.class); + AuthProvider authProvider = authProviderRepository.findById(entity.getProviderId()); + + if (authProvider == null) { + dto.setProvider(CommonParameter.DEFAULT_PROVIDER); + } + else { + dto.setProvider(authProvider.getName()); + } + + return dto; + } + + return new CredentialDTO(); + } + + public CredentialDTO findByEmailAndProvider(String email, String provider) { + AuthProvider authProvider = authProviderRepository.findByName(provider); + Credential credential = credentialRepository.findByEmailAndProvider(email, authProvider.getId()); + return entityToDTO(credential); + } + + public CredentialDTO findByToken(String token) { + Credential credential = credentialRepository.findByToken(token); + return entityToDTO(credential); + } + + public CredentialDTO findByAuthentication(OAuth2AuthenticationToken authentication) { + String email = authentication.getPrincipal().getAttribute("email"); + String provider = authentication.getAuthorizedClientRegistrationId(); + + AuthProvider authProvider = authProviderRepository.findByName(provider); + Credential credential = credentialRepository.findByEmailAndProvider(email, authProvider.getId()); + return entityToDTO(credential); + } + + + public CredentialDTO save(CredentialDTO dto) { + Credential credential = dtoToEntity(dto); + credential = credentialRepository.save(credential); + return entityToDTO(credential); + } + + public CredentialDTO saveNew(UserDTO user) { + Credential credential = new Credential(); + + AuthProvider authProvider = authProviderRepository.findByName(CommonParameter.DEFAULT_PROVIDER); + + credential.setEmail(user.getEmail()); + credential.setProviderId(authProvider.getId()); + credential.setPassword(user.getPassword()); + credential.setVerified(false); + credential.setToken(); + credential.setExpiration(); + + credential = credentialRepository.save(credential); + return entityToDTO(credential); + } + + public CredentialDTO saveNew(OAuth2AuthenticationToken authentication) { + Credential credential = new Credential(); + + AuthProvider authProvider = authProviderRepository.findByName(authentication.getAuthorizedClientRegistrationId()); + + credential.setEmail(authentication.getPrincipal().getAttribute("email")); + credential.setProviderId(authProvider.getId()); + credential.setPassword(authentication.getPrincipal().getAttribute("sub")); + credential.setVerified(false); + credential.setToken(); + credential.setExpiration(); + + credential = credentialRepository.save(credential); + return entityToDTO(credential); + } + + +} diff --git a/src/main/java/org/springframework/samples/petclinic/service/common/EmailService.java b/src/main/java/org/springframework/samples/petclinic/service/common/EmailService.java new file mode 100644 index 000000000..c9730478a --- /dev/null +++ b/src/main/java/org/springframework/samples/petclinic/service/common/EmailService.java @@ -0,0 +1,97 @@ +package org.springframework.samples.petclinic.service.common; + +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.core.io.ClassPathResource; +import org.springframework.mail.javamail.JavaMailSender; +import org.springframework.mail.javamail.MimeMessageHelper; +import org.springframework.samples.petclinic.dto.common.MessageDTO; +import org.springframework.scheduling.annotation.Async; +import org.springframework.stereotype.Service; +import org.thymeleaf.ITemplateEngine; +import org.thymeleaf.context.Context; + +import javax.mail.MessagingException; +import javax.mail.internet.MimeMessage; +import java.util.Date; +import java.util.Locale; + +@Slf4j +@Service("EmailService") +public class EmailService { + + private static final String EMAIL_TEMPLATE = "email.html"; + + private static final String PNG_MIME = "image/png"; + + @Value("${mail.background}") + private String mailBackground; + + @Value("${mail.banner}") + private String mailBanner; + + @Value("${mail.logo}") + private String mailLogo; + + @Autowired + protected JavaMailSender mailSender; + + @Autowired + protected ITemplateEngine templateEngine; + + + /** + * sendMailAsynch : for the controller MailController + * send mail asynchronously + * + * @param messageDTO : message to be send by mail + * @param locale : not used now + */ + @Async + public void sendMailAsynch(MessageDTO messageDTO, Locale locale){ + sendMail(messageDTO, locale); + } + + /** + * sendMail : build mail according to a Thymeleaf template and a MessageDTO object + * @param messageDTO : message to be send by mail + * @param locale : not used now + */ + public void sendMail(MessageDTO messageDTO, Locale locale) { + MimeMessage mimeMessage = mailSender.createMimeMessage(); + MimeMessageHelper message; + + // Prepare the evaluation context + Context ctx = new Context(locale); + ctx.setVariable("toFirstName", messageDTO.getFirstName()); + ctx.setVariable("toLastName", messageDTO.getLastName()); + ctx.setVariable("mailSubject", messageDTO.getSubject()); + ctx.setVariable("mailContent", messageDTO.getContent()); + ctx.setVariable("mailLink", messageDTO.getLink()); + ctx.setVariable("mailDate", new Date()); + + // Create the HTML body using Thymeleaf + String output = templateEngine.process(EMAIL_TEMPLATE, ctx); + + try { + message = new MimeMessageHelper(mimeMessage, true /* multipart */, "UTF-8"); + + message.setFrom(messageDTO.getFrom()); + message.setTo(messageDTO.getTo()); + message.setSubject(messageDTO.getSubject()); + message.setText(output, true /* isHtml */); + + message.addInline("mailBackground", new ClassPathResource(mailBackground), PNG_MIME); + message.addInline("mailBanner", new ClassPathResource(mailBanner), PNG_MIME); + message.addInline("mailLogo", new ClassPathResource(mailLogo), PNG_MIME); + } + catch (MessagingException ex) { + log.error("Error sending mail !", ex); + } + + // Send mail + this.mailSender.send(mimeMessage); + } + +} diff --git a/src/main/java/org/springframework/samples/petclinic/service/RoleService.java b/src/main/java/org/springframework/samples/petclinic/service/common/RoleService.java similarity index 90% rename from src/main/java/org/springframework/samples/petclinic/service/RoleService.java rename to src/main/java/org/springframework/samples/petclinic/service/common/RoleService.java index 2194bc21b..d0f2e43ce 100644 --- a/src/main/java/org/springframework/samples/petclinic/service/RoleService.java +++ b/src/main/java/org/springframework/samples/petclinic/service/common/RoleService.java @@ -1,9 +1,10 @@ -package org.springframework.samples.petclinic.service; +package org.springframework.samples.petclinic.service.common; import org.modelmapper.ModelMapper; -import org.springframework.samples.petclinic.dto.RoleDTO; +import org.springframework.samples.petclinic.dto.common.RoleDTO; import org.springframework.samples.petclinic.model.common.Role; import org.springframework.samples.petclinic.repository.RoleRepository; +import org.springframework.samples.petclinic.service.business.BaseService; import org.springframework.stereotype.Service; import java.util.ArrayList; diff --git a/src/main/java/org/springframework/samples/petclinic/service/SecurityService.java b/src/main/java/org/springframework/samples/petclinic/service/common/SecurityService.java similarity index 66% rename from src/main/java/org/springframework/samples/petclinic/service/SecurityService.java rename to src/main/java/org/springframework/samples/petclinic/service/common/SecurityService.java index 1e173e61c..60818f5fd 100644 --- a/src/main/java/org/springframework/samples/petclinic/service/SecurityService.java +++ b/src/main/java/org/springframework/samples/petclinic/service/common/SecurityService.java @@ -1,4 +1,4 @@ -package org.springframework.samples.petclinic.service; +package org.springframework.samples.petclinic.service.common; public interface SecurityService { diff --git a/src/main/java/org/springframework/samples/petclinic/service/SecurityServiceImpl.java b/src/main/java/org/springframework/samples/petclinic/service/common/SecurityServiceImpl.java similarity index 96% rename from src/main/java/org/springframework/samples/petclinic/service/SecurityServiceImpl.java rename to src/main/java/org/springframework/samples/petclinic/service/common/SecurityServiceImpl.java index a5f5936eb..639e9675d 100644 --- a/src/main/java/org/springframework/samples/petclinic/service/SecurityServiceImpl.java +++ b/src/main/java/org/springframework/samples/petclinic/service/common/SecurityServiceImpl.java @@ -1,4 +1,4 @@ -package org.springframework.samples.petclinic.service; +package org.springframework.samples.petclinic.service.common; import lombok.extern.slf4j.Slf4j; import org.springframework.beans.factory.annotation.Autowired; diff --git a/src/main/java/org/springframework/samples/petclinic/service/UserDetailsServiceImpl.java b/src/main/java/org/springframework/samples/petclinic/service/common/UserDetailsServiceImpl.java similarity index 59% rename from src/main/java/org/springframework/samples/petclinic/service/UserDetailsServiceImpl.java rename to src/main/java/org/springframework/samples/petclinic/service/common/UserDetailsServiceImpl.java index 86a249205..1858299f8 100644 --- a/src/main/java/org/springframework/samples/petclinic/service/UserDetailsServiceImpl.java +++ b/src/main/java/org/springframework/samples/petclinic/service/common/UserDetailsServiceImpl.java @@ -1,20 +1,14 @@ -package org.springframework.samples.petclinic.service; +package org.springframework.samples.petclinic.service.common; import lombok.extern.slf4j.Slf4j; import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.samples.petclinic.dto.RoleDTO; -import org.springframework.samples.petclinic.dto.UserDTO; -import org.springframework.security.core.GrantedAuthority; -import org.springframework.security.core.authority.SimpleGrantedAuthority; +import org.springframework.samples.petclinic.dto.common.UserDTO; import org.springframework.security.core.userdetails.UserDetails; import org.springframework.security.core.userdetails.UserDetailsService; import org.springframework.security.core.userdetails.UsernameNotFoundException; import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder; import org.springframework.stereotype.Service; -import java.util.HashSet; -import java.util.Set; - @Slf4j @Service("UserDetailsService") public class UserDetailsServiceImpl implements UserDetailsService { @@ -36,15 +30,7 @@ public class UserDetailsServiceImpl implements UserDetailsService { if (userDTO == null) throw new UsernameNotFoundException("User not found with email :" + email); - Set grantedAuthorities = new HashSet<>(); - - for (RoleDTO role : userDTO.getRoles()) { - grantedAuthorities.add(new SimpleGrantedAuthority(role.getName())); - } - - return new org.springframework.security.core.userdetails.User(userDTO.getEmail(), userDTO.getMatchingPassword(), - grantedAuthorities); - + return userDTO; } } diff --git a/src/main/java/org/springframework/samples/petclinic/service/UserService.java b/src/main/java/org/springframework/samples/petclinic/service/common/UserService.java similarity index 86% rename from src/main/java/org/springframework/samples/petclinic/service/UserService.java rename to src/main/java/org/springframework/samples/petclinic/service/common/UserService.java index b15ff430f..7b1243b77 100644 --- a/src/main/java/org/springframework/samples/petclinic/service/UserService.java +++ b/src/main/java/org/springframework/samples/petclinic/service/common/UserService.java @@ -1,11 +1,12 @@ -package org.springframework.samples.petclinic.service; +package org.springframework.samples.petclinic.service.common; import org.modelmapper.ModelMapper; -import org.springframework.samples.petclinic.dto.RoleDTO; -import org.springframework.samples.petclinic.dto.UserDTO; +import org.springframework.samples.petclinic.dto.common.RoleDTO; +import org.springframework.samples.petclinic.dto.common.UserDTO; import org.springframework.samples.petclinic.model.common.Role; import org.springframework.samples.petclinic.model.common.User; import org.springframework.samples.petclinic.repository.UserRepository; +import org.springframework.samples.petclinic.service.business.BaseService; import org.springframework.stereotype.Service; import java.util.ArrayList; @@ -37,13 +38,14 @@ public class UserService implements BaseService { User user = modelMapper.map(dto, User.class); user.setPassword(dto.getPassword()); + /* if (dto.getRoles() != null) { for (RoleDTO roleDTO : dto.getRoles()) { Role role = modelMapper.map(roleDTO, Role.class); user.addRole(role); } } - +*/ return user; } @@ -56,14 +58,14 @@ public class UserService implements BaseService { UserDTO userDto = modelMapper.map(entity, UserDTO.class); userDto.setPassword(entity.getPassword()); userDto.setMatchingPassword(entity.getPassword()); - +/* if (entity.getRoles() != null) { for (Role role : entity.getRoles()) { RoleDTO roleDTO = modelMapper.map(role, RoleDTO.class); userDto.addRole(roleDTO); } } - +*/ return userDto; } @@ -114,4 +116,8 @@ public class UserService implements BaseService { return entityToDTO(user); } + public boolean existByEmail(String email) { + return userRepository.existsByEmail(email); + } + } diff --git a/src/main/resources/application.properties b/src/main/resources/application.properties index 11809f262..2d00e9be3 100644 --- a/src/main/resources/application.properties +++ b/src/main/resources/application.properties @@ -30,8 +30,6 @@ spring.resources.cache.cachecontrol.max-age=12h #logging.level.org.hibernate: DEBUG #logging.level.org.springframework.context.annotation=TRACE - - spring.datasource.hikari.connectionTimeout=20000 spring.datasource.hikari.maximumPoolSize=5 spring.datasource.initialize=true @@ -42,6 +40,7 @@ spring.datasource.password= spring.h2.console.enabled=true spring.h2.console.path=/h2-console +######################################################################### OAUTH2 spring.security.oauth2.client.registration.google.client-id=${OAUTH2_GOOGLE_CLIENT_ID} spring.security.oauth2.client.registration.google.client-secret=${OAUTH2_GOOGLE_CLIENT_SECRET} @@ -52,6 +51,17 @@ spring.security.oauth2.client.registration.github.client-secret=${OAUTH2_GITHUB_ #spring.security.oauth2.client.registration.facebook.client-id= #spring.security.oauth2.client.registration.facebook.client-secret= - #spring.security.oauth2.client.registration.twitter.client-id= #spring.security.oauth2.client.registration.twitter.client-secret= + +#################################################################### SPRING MAIL +spring.mail.host=smtp.mailtrap.io +spring.mail.port=2525 +spring.mail.protocol=smtp +spring.mail.username=${SPRING_MAIL_USERNAME} +spring.mail.password=${SPRING_MAIL_PASSWORD} +mail.background=static/resources/images/mail-background.png +mail.banner=static/resources/images/mail-banner.png +mail.logo=static/resources/images/mail-logo.png + + diff --git a/src/main/resources/db/h2/data.sql b/src/main/resources/db/h2/data.sql index a5aa7dd2d..56734e0f6 100644 --- a/src/main/resources/db/h2/data.sql +++ b/src/main/resources/db/h2/data.sql @@ -52,16 +52,30 @@ INSERT INTO visits VALUES (2, 8, '2013-01-02', 'rabies shot'); INSERT INTO visits VALUES (3, 8, '2013-01-03', 'neutered'); INSERT INTO visits VALUES (4, 7, '2013-01-04', 'spayed'); -INSERT INTO roles VALUES (1,'ADMIN'); -INSERT INTO roles VALUES (2,'STAFF'); -INSERT INTO roles VALUES (3,'USER'); +INSERT INTO roles (id, name) VALUES + (1,'ROLE_ADMIN'), + (2,'ROLE_STAFF'), + (3,'ROLE_USER'); -INSERT INTO users VALUES (1, 'George', 'Franklin', 'georges.franklin@petclinic.com', true,'$2a$10$8KypNYtPopFo8Sk5jbKJ4.lCKeBhdApsrkmFfhwjB8nCls8qpzjZG', 'local', null, '6085551023', '110 W. Liberty St.','','',12354,'Madison','USA'); -INSERT INTO users VALUES (2, 'Betty', 'Davis', 'betty.davis@petclinic.com', true, '$2a$10$InKx/fhX3CmLi8zKpHYx/.ETHUlZwvT1xn.Za/pp2JR0iEtYV9a9O', 'local', null, '6085551749','638 Cardinal Ave.', '', '', 6546, 'Sun Prairie', 'USA'); -INSERT INTO users VALUES (3, 'Eduardo', 'Rodriquez', 'eduardo.rodriguez@petclinic.com', true, '$2a$10$P55nbvVibHpoyWzenHngjOf.oEmcj74mI/VJaUZwGX9v8klctzsNW', 'local', null, '6085558763','2693 Commerce St.', '', '', 65454, 'McFarland', 'USA'); +INSERT INTO users (id, first_name, last_name, email, password, enabled, telephone, street1, zip_code, city, country) VALUES + (1, 'George', 'Franklin', 'georges.franklin@petclinic.com', '$2a$10$8KypNYtPopFo8Sk5jbKJ4.lCKeBhdApsrkmFfhwjB8nCls8qpzjZG', true, '6085551023', '110 W. Liberty St.',12354,'Madison','USA'), + (2, 'Betty', 'Davis', 'betty.davis@petclinic.com', '$2a$10$InKx/fhX3CmLi8zKpHYx/.ETHUlZwvT1xn.Za/pp2JR0iEtYV9a9O', true, '6085551749','638 Cardinal Ave.', 6546, 'Sun Prairie', 'USA'), + (3, 'Eduardo', 'Rodriquez', 'eduardo.rodriguez@petclinic.com', '$2a$10$P55nbvVibHpoyWzenHngjOf.oEmcj74mI/VJaUZwGX9v8klctzsNW', true, '6085558763','2693 Commerce St.', 65454, 'McFarland', 'USA'), + (4, 'Paul-Emmanuel','DOS SANTOS FACAO','pedsf.fullstack@gmail.com','$2a$10$AzoUxi1IQFJMzLHcCGmDjuDHAQqAcAiRLz6UMeItdTL3mMWxMZEPC', true, '6085558763','2693 Commerce St.', 65454, 'McFarland', 'USA'); + +INSERT INTO users_roles (user_id, role_id) VALUES + (1,1),(1,2),(1,3), + (2,3),(3,3); + +INSERT INTO auth_providers (id, name) VALUES + (1,'local'), + (2,'google'), + (3,'github'), + (4,'twitter'); + +INSERT INTO credentials (provider_id, email, password, verified) VALUES + (1, 'georges.franklin@petclinic.com', '$2a$10$8KypNYtPopFo8Sk5jbKJ4.lCKeBhdApsrkmFfhwjB8nCls8qpzjZG', true), + (1, 'betty.davis@petclinic.com', '$2a$10$InKx/fhX3CmLi8zKpHYx/.ETHUlZwvT1xn.Za/pp2JR0iEtYV9a9O', true), + (1, 'eduardo.rodriguez@petclinic.com', '$2a$10$P55nbvVibHpoyWzenHngjOf.oEmcj74mI/VJaUZwGX9v8klctzsNW', true), + (2, 'pedsf.fullstack@gmail.com','117496521794255275093', true); -INSERT INTO users_roles VALUES (1,1); -INSERT INTO users_roles VALUES (1,2); -INSERT INTO users_roles VALUES (1,3); -INSERT INTO users_roles VALUES (2,3); -INSERT INTO users_roles VALUES (3,3); diff --git a/src/main/resources/db/h2/schema.sql b/src/main/resources/db/h2/schema.sql index 3c80a0076..8aa68cc4f 100644 --- a/src/main/resources/db/h2/schema.sql +++ b/src/main/resources/db/h2/schema.sql @@ -7,7 +7,10 @@ DROP TABLE types IF EXISTS; DROP TABLE owners IF EXISTS; DROP TABLE roles IF EXISTS; DROP TABLE users IF EXISTS; - +DROP TABLE users_email IF EXISTS; +DROP TABLE users_roles IF EXISTS; +DROP TABLE auth_providers IF EXISTS; +DROP TABLE credentials IF EXISTS; CREATE TABLE vets ( id INTEGER IDENTITY PRIMARY KEY, @@ -72,21 +75,22 @@ CREATE TABLE roles ( CREATE INDEX roles_name ON roles (name); CREATE TABLE users ( - id INTEGER IDENTITY PRIMARY KEY, - first_name VARCHAR(30) NOT NULL, - last_name VARCHAR_IGNORECASE(30) NOT NULL, - email VARCHAR(50) NOT NULL, - email_verified BOOLEAN NOT NULL, - password VARCHAR(255) NOT NULL, - provider VARCHAR(20), - provider_id VARCHAR(20), - telephone VARCHAR(20), - street1 VARCHAR(50), - street2 VARCHAR(50), - street3 VARCHAR(50), - zip_code VARCHAR(6), - city VARCHAR(80), - country VARCHAR(50) + id INTEGER IDENTITY PRIMARY KEY, + first_name VARCHAR(30) NOT NULL, + last_name VARCHAR_IGNORECASE(30) NOT NULL, + email VARCHAR(50) NOT NULL, + password VARCHAR(255) NOT NULL, + enabled BOOLEAN NOT NULL, + account_unexpired BOOLEAN NOT NULL DEFAULT true, + account_unlocked BOOLEAN NOT NULL DEFAULT true, + credential_unexpired BOOLEAN NOT NULL DEFAULT true, + telephone VARCHAR(20), + street1 VARCHAR(50), + street2 VARCHAR(50), + street3 VARCHAR(50), + zip_code VARCHAR(6), + city VARCHAR(80), + country VARCHAR(50) ); CREATE INDEX users_email ON users (email); @@ -97,3 +101,21 @@ CREATE TABLE public.users_roles ( ALTER TABLE users_roles ADD CONSTRAINT fk_users_roles_user_id FOREIGN KEY (user_id) REFERENCES users (id); ALTER TABLE users_roles ADD CONSTRAINT fk_users_roles_role_id FOREIGN KEY (role_id) REFERENCES roles (id); CREATE INDEX users_roles_user_id ON users_roles (user_id); + +CREATE TABLE auth_providers ( + id INTEGER IDENTITY PRIMARY KEY, + name VARCHAR(20) NOT NULL +); +CREATE INDEX auth_providers_name ON auth_providers (name); + +CREATE TABLE credentials ( + id INTEGER IDENTITY PRIMARY KEY, + provider_id INTEGER NOT NULL, + email VARCHAR(50) NOT NULL, + password VARCHAR(255) NOT NULL, + verified BOOLEAN NOT NULL, + token VARCHAR(255) DEFAULT NULL, + expiration DATE DEFAULT NULL +); +ALTER TABLE credentials ADD CONSTRAINT fk_credentials_provider_id FOREIGN KEY (provider_id) REFERENCES auth_providers (id); +CREATE INDEX credentials_email ON credentials (email); diff --git a/src/main/resources/static/resources/images/mail-background.png b/src/main/resources/static/resources/images/mail-background.png new file mode 100644 index 0000000000000000000000000000000000000000..512185839bdd94afc996e737d61041d9600e8eb9 GIT binary patch literal 9162 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

-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 + + + Template for HTML email (editable) + + + +

+
+
+
+
+ +
+
+ +
+

+ Subject

+

+

+ Hello + FirstName + toLastName! +

+ +

mail content

+ Approve the connection + +

+ date +

+

Regards

+
+
+
+ +
+
+
+
+
+ + diff --git a/src/main/resources/templates/fragments/layout.html b/src/main/resources/templates/fragments/layout.html index 663ab0d8c..72de108c6 100755 --- a/src/main/resources/templates/fragments/layout.html +++ b/src/main/resources/templates/fragments/layout.html @@ -19,7 +19,7 @@ - + diff --git a/src/main/resources/templates/owners/createOrUpdateOwnerForm.html b/src/main/resources/templates/owners/createOrUpdateOwnerForm.html index c835f8ccb..d290c7d36 100644 --- a/src/main/resources/templates/owners/createOrUpdateOwnerForm.html +++ b/src/main/resources/templates/owners/createOrUpdateOwnerForm.html @@ -21,8 +21,9 @@
+ class="btn btn-default" type="submit" th:text="${text}">Add Owner + + Cancel
diff --git a/src/main/resources/templates/users/userChangePasswordForm.html b/src/main/resources/templates/users/userChangePasswordForm.html new file mode 100644 index 000000000..36c70d389 --- /dev/null +++ b/src/main/resources/templates/users/userChangePasswordForm.html @@ -0,0 +1,53 @@ + + + + +

Change password

+
+ + + + + + + + + ; + + + + + + + + + +
+
+ + + We'll never share your emailConfiguration with anyone else. +
+
+ + +
+
+ + +
+
+ + +
+
+
+ + Cancel +
+
+
+
+ + diff --git a/src/main/resources/templates/users/createOrUpdateUserForm.html b/src/main/resources/templates/users/userRegistrationForm.html similarity index 79% rename from src/main/resources/templates/users/createOrUpdateUserForm.html rename to src/main/resources/templates/users/userRegistrationForm.html index 9fba098f1..b0d8c008c 100644 --- a/src/main/resources/templates/users/createOrUpdateUserForm.html +++ b/src/main/resources/templates/users/userRegistrationForm.html @@ -3,9 +3,16 @@ -

User

+

User registration

+ + + + +
+ -
- - Cancel + + Cancel
diff --git a/src/main/resources/templates/users/userUpdateForm.html b/src/main/resources/templates/users/userUpdateForm.html new file mode 100644 index 000000000..5fef33bb4 --- /dev/null +++ b/src/main/resources/templates/users/userUpdateForm.html @@ -0,0 +1,49 @@ + + + + +

User update

+
+ + + + + + + + + + +
+ + + + + + + + + + +
+
+
+ + Change password + Cancel +
+
+
+ + diff --git a/src/main/resources/templates/welcome.html b/src/main/resources/templates/welcome.html index 4fa1cd328..6e6206792 100644 --- a/src/main/resources/templates/welcome.html +++ b/src/main/resources/templates/welcome.html @@ -1,6 +1,5 @@ - - - + @@ -13,4 +12,4 @@ - \ No newline at end of file + diff --git a/src/test/java/org/springframework/samples/petclinic/controller/OwnerControllerIntegrationTest.java b/src/test/java/org/springframework/samples/petclinic/controller/OwnerControllerIntegrationTest.java index 55ff114ca..cdaef709b 100644 --- a/src/test/java/org/springframework/samples/petclinic/controller/OwnerControllerIntegrationTest.java +++ b/src/test/java/org/springframework/samples/petclinic/controller/OwnerControllerIntegrationTest.java @@ -12,7 +12,7 @@ import org.springframework.samples.petclinic.dto.OwnerDTO; import org.springframework.samples.petclinic.dto.PetDTO; import org.springframework.samples.petclinic.dto.PetTypeDTO; import org.springframework.samples.petclinic.dto.VisitDTO; -import org.springframework.samples.petclinic.service.OwnerService; +import org.springframework.samples.petclinic.service.business.OwnerService; import org.springframework.test.web.servlet.MockMvc; import org.springframework.test.web.servlet.MvcResult; import org.springframework.test.web.servlet.result.MockMvcResultMatchers; diff --git a/src/test/java/org/springframework/samples/petclinic/controller/OwnerControllerTest.java b/src/test/java/org/springframework/samples/petclinic/controller/OwnerControllerTest.java index 62d7c9540..25b1d60d5 100644 --- a/src/test/java/org/springframework/samples/petclinic/controller/OwnerControllerTest.java +++ b/src/test/java/org/springframework/samples/petclinic/controller/OwnerControllerTest.java @@ -40,8 +40,8 @@ import org.springframework.samples.petclinic.dto.OwnerDTO; import org.springframework.samples.petclinic.dto.PetDTO; import org.springframework.samples.petclinic.dto.PetTypeDTO; import org.springframework.samples.petclinic.dto.VisitDTO; -import org.springframework.samples.petclinic.service.OwnerService; -import org.springframework.samples.petclinic.service.VisitService; +import org.springframework.samples.petclinic.service.business.OwnerService; +import org.springframework.samples.petclinic.service.business.VisitService; import org.springframework.test.web.servlet.MockMvc; import static org.hamcrest.Matchers.empty; diff --git a/src/test/java/org/springframework/samples/petclinic/controller/PetControllerIntegrationTest.java b/src/test/java/org/springframework/samples/petclinic/controller/PetControllerIntegrationTest.java index 113c7f16b..db30e66d0 100644 --- a/src/test/java/org/springframework/samples/petclinic/controller/PetControllerIntegrationTest.java +++ b/src/test/java/org/springframework/samples/petclinic/controller/PetControllerIntegrationTest.java @@ -11,8 +11,8 @@ import org.springframework.samples.petclinic.common.CommonView; import org.springframework.samples.petclinic.dto.OwnerDTO; import org.springframework.samples.petclinic.dto.PetDTO; import org.springframework.samples.petclinic.repository.PetRepository; -import org.springframework.samples.petclinic.service.OwnerService; -import org.springframework.samples.petclinic.service.PetService; +import org.springframework.samples.petclinic.service.business.OwnerService; +import org.springframework.samples.petclinic.service.business.PetService; import org.springframework.test.web.servlet.MockMvc; import org.springframework.test.web.servlet.MvcResult; diff --git a/src/test/java/org/springframework/samples/petclinic/controller/PetControllerTest.java b/src/test/java/org/springframework/samples/petclinic/controller/PetControllerTest.java index af989653b..1e9583481 100644 --- a/src/test/java/org/springframework/samples/petclinic/controller/PetControllerTest.java +++ b/src/test/java/org/springframework/samples/petclinic/controller/PetControllerTest.java @@ -42,9 +42,9 @@ import org.springframework.samples.petclinic.dto.OwnerDTO; import org.springframework.samples.petclinic.dto.PetDTO; import org.springframework.samples.petclinic.dto.PetTypeDTO; import org.springframework.samples.petclinic.formatter.PetTypeFormatter; -import org.springframework.samples.petclinic.service.OwnerService; -import org.springframework.samples.petclinic.service.PetService; -import org.springframework.samples.petclinic.service.PetTypeService; +import org.springframework.samples.petclinic.service.business.OwnerService; +import org.springframework.samples.petclinic.service.business.PetService; +import org.springframework.samples.petclinic.service.business.PetTypeService; import org.springframework.test.web.servlet.MockMvc; /** diff --git a/src/test/java/org/springframework/samples/petclinic/controller/VetControllerIntegrationTest.java b/src/test/java/org/springframework/samples/petclinic/controller/VetControllerIntegrationTest.java index 8e630a475..c4ce76a16 100644 --- a/src/test/java/org/springframework/samples/petclinic/controller/VetControllerIntegrationTest.java +++ b/src/test/java/org/springframework/samples/petclinic/controller/VetControllerIntegrationTest.java @@ -15,7 +15,7 @@ import org.springframework.samples.petclinic.common.CommonEndPoint; import org.springframework.samples.petclinic.common.CommonView; import org.springframework.samples.petclinic.dto.VetsDTO; import org.springframework.samples.petclinic.repository.VetRepository; -import org.springframework.samples.petclinic.service.VetService; +import org.springframework.samples.petclinic.service.business.VetService; import org.springframework.test.web.servlet.MockMvc; import org.springframework.test.web.servlet.MvcResult; diff --git a/src/test/java/org/springframework/samples/petclinic/controller/VetControllerTest.java b/src/test/java/org/springframework/samples/petclinic/controller/VetControllerTest.java index 79c1d6548..d24a7caa9 100644 --- a/src/test/java/org/springframework/samples/petclinic/controller/VetControllerTest.java +++ b/src/test/java/org/springframework/samples/petclinic/controller/VetControllerTest.java @@ -19,7 +19,6 @@ package org.springframework.samples.petclinic.controller; import static org.assertj.core.api.Assertions.assertThat; import static org.mockito.BDDMockito.given; import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; -import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.content; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.model; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; @@ -40,10 +39,9 @@ import org.springframework.samples.petclinic.common.CommonView; import org.springframework.samples.petclinic.dto.SpecialtyDTO; import org.springframework.samples.petclinic.dto.VetDTO; import org.springframework.samples.petclinic.dto.VetsDTO; -import org.springframework.samples.petclinic.service.VetService; +import org.springframework.samples.petclinic.service.business.VetService; import org.springframework.test.web.servlet.MockMvc; import org.springframework.test.web.servlet.MvcResult; -import org.springframework.test.web.servlet.ResultActions; import java.nio.charset.StandardCharsets; import java.util.List; diff --git a/src/test/java/org/springframework/samples/petclinic/controller/VisitControllerIntegrationTest.java b/src/test/java/org/springframework/samples/petclinic/controller/VisitControllerIntegrationTest.java index 0ed46b527..b5ad7fcaf 100644 --- a/src/test/java/org/springframework/samples/petclinic/controller/VisitControllerIntegrationTest.java +++ b/src/test/java/org/springframework/samples/petclinic/controller/VisitControllerIntegrationTest.java @@ -12,7 +12,7 @@ import org.springframework.samples.petclinic.dto.PetDTO; import org.springframework.samples.petclinic.dto.VisitDTO; import org.springframework.samples.petclinic.model.business.Visit; import org.springframework.samples.petclinic.repository.VisitRepository; -import org.springframework.samples.petclinic.service.PetService; +import org.springframework.samples.petclinic.service.business.PetService; import org.springframework.test.web.servlet.MockMvc; import org.springframework.test.web.servlet.MvcResult; diff --git a/src/test/java/org/springframework/samples/petclinic/controller/VisitControllerTest.java b/src/test/java/org/springframework/samples/petclinic/controller/VisitControllerTest.java index ca39cb720..e6d3dcab9 100644 --- a/src/test/java/org/springframework/samples/petclinic/controller/VisitControllerTest.java +++ b/src/test/java/org/springframework/samples/petclinic/controller/VisitControllerTest.java @@ -40,8 +40,8 @@ import org.springframework.samples.petclinic.dto.PetDTO; import org.springframework.samples.petclinic.dto.PetTypeDTO; import org.springframework.samples.petclinic.dto.VisitDTO; import org.springframework.samples.petclinic.model.business.Visit; -import org.springframework.samples.petclinic.service.PetService; -import org.springframework.samples.petclinic.service.VisitService; +import org.springframework.samples.petclinic.service.business.PetService; +import org.springframework.samples.petclinic.service.business.VisitService; import org.springframework.test.web.servlet.MockMvc; import org.springframework.test.web.servlet.MvcResult; diff --git a/src/test/java/org/springframework/samples/petclinic/formater/PetTypeDTOFormatterTest.java b/src/test/java/org/springframework/samples/petclinic/formater/PetTypeDTOFormatterTest.java index 7aa7f2d3c..80b06a4bb 100644 --- a/src/test/java/org/springframework/samples/petclinic/formater/PetTypeDTOFormatterTest.java +++ b/src/test/java/org/springframework/samples/petclinic/formater/PetTypeDTOFormatterTest.java @@ -25,7 +25,7 @@ import org.mockito.junit.jupiter.MockitoExtension; import org.springframework.samples.petclinic.dto.PetTypeDTO; import org.springframework.samples.petclinic.formatter.PetTypeFormatter; import org.springframework.samples.petclinic.model.business.PetType; -import org.springframework.samples.petclinic.service.PetService; +import org.springframework.samples.petclinic.service.business.PetService; import java.text.ParseException; import java.util.ArrayList; diff --git a/src/test/java/org/springframework/samples/petclinic/formater/PetTypeFormatterTest.java b/src/test/java/org/springframework/samples/petclinic/formater/PetTypeFormatterTest.java index 7aecc89d8..f52be257a 100644 --- a/src/test/java/org/springframework/samples/petclinic/formater/PetTypeFormatterTest.java +++ b/src/test/java/org/springframework/samples/petclinic/formater/PetTypeFormatterTest.java @@ -31,7 +31,7 @@ import org.mockito.junit.jupiter.MockitoExtension; import org.springframework.samples.petclinic.dto.PetTypeDTO; import org.springframework.samples.petclinic.formatter.PetTypeFormatter; import org.springframework.samples.petclinic.model.business.PetType; -import org.springframework.samples.petclinic.service.PetService; +import org.springframework.samples.petclinic.service.business.PetService; import static org.assertj.core.api.Assertions.assertThat; import static org.mockito.BDDMockito.given; diff --git a/src/test/java/org/springframework/samples/petclinic/service/OwnerServiceTest.java b/src/test/java/org/springframework/samples/petclinic/service/OwnerServiceTest.java index f3c1dfbe1..a12b7a600 100644 --- a/src/test/java/org/springframework/samples/petclinic/service/OwnerServiceTest.java +++ b/src/test/java/org/springframework/samples/petclinic/service/OwnerServiceTest.java @@ -19,6 +19,9 @@ import org.springframework.samples.petclinic.repository.OwnerRepository; import org.springframework.samples.petclinic.repository.PetRepository; import org.springframework.samples.petclinic.repository.PetTypeRepository; import org.springframework.samples.petclinic.repository.VisitRepository; +import org.springframework.samples.petclinic.service.business.OwnerService; +import org.springframework.samples.petclinic.service.business.PetService; +import org.springframework.samples.petclinic.service.business.PetTypeService; import org.springframework.stereotype.Service; import java.time.LocalDate; diff --git a/src/test/java/org/springframework/samples/petclinic/service/PetServiceTest.java b/src/test/java/org/springframework/samples/petclinic/service/PetServiceTest.java index e599eca39..dfa9d981c 100644 --- a/src/test/java/org/springframework/samples/petclinic/service/PetServiceTest.java +++ b/src/test/java/org/springframework/samples/petclinic/service/PetServiceTest.java @@ -17,6 +17,9 @@ import org.springframework.samples.petclinic.model.business.PetType; import org.springframework.samples.petclinic.repository.PetRepository; import org.springframework.samples.petclinic.repository.PetTypeRepository; import org.springframework.samples.petclinic.repository.VisitRepository; +import org.springframework.samples.petclinic.service.business.OwnerService; +import org.springframework.samples.petclinic.service.business.PetService; +import org.springframework.samples.petclinic.service.business.PetTypeService; import org.springframework.stereotype.Service; import java.time.LocalDate; diff --git a/src/test/java/org/springframework/samples/petclinic/service/VetServiceTest.java b/src/test/java/org/springframework/samples/petclinic/service/VetServiceTest.java index cbe2c985f..135448577 100644 --- a/src/test/java/org/springframework/samples/petclinic/service/VetServiceTest.java +++ b/src/test/java/org/springframework/samples/petclinic/service/VetServiceTest.java @@ -12,6 +12,7 @@ import org.springframework.samples.petclinic.dto.VetDTO; import org.springframework.samples.petclinic.model.business.Vet; import org.springframework.samples.petclinic.repository.SpecialtyRepository; import org.springframework.samples.petclinic.repository.VetRepository; +import org.springframework.samples.petclinic.service.business.VetService; import org.springframework.stereotype.Service; import java.util.ArrayList;