From 9efd7b3fb4d2ecdd9dcfeb3c7f3f3fc73d626b6b Mon Sep 17 00:00:00 2001 From: Viktoria Date: Wed, 31 Aug 2022 11:37:17 +0200 Subject: [PATCH] security --- build.gradle | 6 +- .../web/EasyStartupApplication.java | 1 - .../web/auth/config/WebSecurityConfig.java | 100 +++++++++--------- .../web/auth/controller/AuthController.java | 72 +++++++++++++ .../domain/annotation/PasswordMatches.java | 19 ++++ .../domain/exception/UserExistException.java | 11 ++ .../easystartup/web/domain/model/Role.java | 8 +- .../easystartup/web/domain/model/User.java | 36 ++++++- .../web/domain/request/LoginRequest.java | 14 +++ .../web/domain/request/SignupRequest.java | 31 ++++++ .../domain/response/InvalidLoginResponse.java | 15 +++ ...ponseMessage.java => MessageResponse.java} | 4 +- .../SuccessAuthenticationResponse.java | 12 +++ .../validation/PasswordMatchesValidator.java | 21 ++++ .../web/forum/controller/ForumController.java | 13 +-- .../project/controller/ProjectController.java | 4 +- .../web/project/domain/model/Project.java | 2 +- .../easystartup/web/project/dto/UserDto.java | 26 ++--- .../web/project/repo/ParticipantRepo.java | 2 +- .../web/project/repo/ProjectRepo.java | 5 +- .../web/project/service/ProjectService.java | 2 +- .../web/project/service/TagService.java | 4 + .../site/easystartup/web/repo/UserRepo.java | 4 + .../security/JWTAuthenticationEntryPoint.java | 25 +++++ .../easystartup/web/security/JWTFilter.java | 56 ++++++++++ .../easystartup/web/security/JWTUtil.java | 37 +++++++ .../web/security/SecurityConfig.java | 60 +++++++++++ .../web/security/SecurityConstants.java | 13 +++ .../easystartup/web/service/AuthService.java | 36 +++++++ .../web/service/CustomUserDetailService.java | 39 +++++++ .../easystartup/web/service/UserService.java | 4 + .../web/storage/service/StorageService.java | 3 +- src/main/resources/application.properties | 1 + 33 files changed, 595 insertions(+), 91 deletions(-) create mode 100644 src/main/java/site/easystartup/web/auth/controller/AuthController.java create mode 100644 src/main/java/site/easystartup/web/domain/annotation/PasswordMatches.java create mode 100644 src/main/java/site/easystartup/web/domain/exception/UserExistException.java create mode 100644 src/main/java/site/easystartup/web/domain/request/LoginRequest.java create mode 100644 src/main/java/site/easystartup/web/domain/request/SignupRequest.java create mode 100644 src/main/java/site/easystartup/web/domain/response/InvalidLoginResponse.java rename src/main/java/site/easystartup/web/domain/response/{ResponseMessage.java => MessageResponse.java} (65%) create mode 100644 src/main/java/site/easystartup/web/domain/response/SuccessAuthenticationResponse.java create mode 100644 src/main/java/site/easystartup/web/domain/validation/PasswordMatchesValidator.java create mode 100644 src/main/java/site/easystartup/web/security/JWTAuthenticationEntryPoint.java create mode 100644 src/main/java/site/easystartup/web/security/JWTFilter.java create mode 100644 src/main/java/site/easystartup/web/security/JWTUtil.java create mode 100644 src/main/java/site/easystartup/web/security/SecurityConfig.java create mode 100644 src/main/java/site/easystartup/web/security/SecurityConstants.java create mode 100644 src/main/java/site/easystartup/web/service/AuthService.java create mode 100644 src/main/java/site/easystartup/web/service/CustomUserDetailService.java diff --git a/build.gradle b/build.gradle index 7084371..cd82116 100644 --- a/build.gradle +++ b/build.gradle @@ -7,7 +7,7 @@ plugins { group = 'site.easy-startup' version = '0.0.1-SNAPSHOT' -sourceCompatibility = "16" +sourceCompatibility = "17" configurations { compileOnly { @@ -20,6 +20,8 @@ repositories { } dependencies { + implementation 'com.auth0:java-jwt:4.0.0' + implementation 'com.google.code.gson:gson:2.9.1' implementation 'org.modelmapper:modelmapper:3.1.0' implementation 'org.springframework.boot:spring-boot-starter-validation' implementation 'com.google.guava:guava:31.1-jre' @@ -43,4 +45,4 @@ dependencies { tasks.named('test') { useJUnitPlatform() } -targetCompatibility = JavaVersion.VERSION_16 +targetCompatibility = JavaVersion.VERSION_17 diff --git a/src/main/java/site/easystartup/web/EasyStartupApplication.java b/src/main/java/site/easystartup/web/EasyStartupApplication.java index 9046c6f..ac0472b 100644 --- a/src/main/java/site/easystartup/web/EasyStartupApplication.java +++ b/src/main/java/site/easystartup/web/EasyStartupApplication.java @@ -4,7 +4,6 @@ import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; import org.springframework.web.bind.annotation.RestController; -@RestController @SpringBootApplication public class EasyStartupApplication { diff --git a/src/main/java/site/easystartup/web/auth/config/WebSecurityConfig.java b/src/main/java/site/easystartup/web/auth/config/WebSecurityConfig.java index 4c59848..4cec7d8 100644 --- a/src/main/java/site/easystartup/web/auth/config/WebSecurityConfig.java +++ b/src/main/java/site/easystartup/web/auth/config/WebSecurityConfig.java @@ -1,50 +1,50 @@ -package site.easystartup.web.auth.config; - -import lombok.RequiredArgsConstructor; -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.context.annotation.Configuration; -import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder; -import org.springframework.security.config.annotation.web.builders.HttpSecurity; -import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity; -import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter; -import org.springframework.security.crypto.password.PasswordEncoder; - -import javax.sql.DataSource; - -@Configuration -@EnableWebSecurity -@RequiredArgsConstructor(onConstructor_ = @Autowired) -public class WebSecurityConfig extends WebSecurityConfigurerAdapter { - private final DataSource dataSource; - - private final PasswordEncoder passwordEncoder; - - @Override - protected void configure(HttpSecurity http) throws Exception { - http - .authorizeRequests() - .antMatchers("/", "/home", "/registration").permitAll() - .anyRequest().authenticated() - .and() - .formLogin() - .loginPage("/login") - .permitAll() - .and() - .logout() - .permitAll() - .and() - .rememberMe().userDetailsService(this.userDetailsService()) - .rememberMeServices(null) - .tokenValiditySeconds(86400) - .useSecureCookie(true); - } - - @Override - protected void configure(AuthenticationManagerBuilder auth) throws Exception { - auth.jdbcAuthentication() - .dataSource(dataSource) - .passwordEncoder(passwordEncoder) - .usersByUsernameQuery("SELECT username, password, active FROM usr WHERE username = ?;") - .authoritiesByUsernameQuery("SELECT u.username, ur.roles FROM usr u INNER JOIN user_role ur ON u.id = ur.user_id WHERE username = ?;"); - } -} +//package site.easystartup.web.auth.config; +// +//import lombok.RequiredArgsConstructor; +//import org.springframework.beans.factory.annotation.Autowired; +//import org.springframework.context.annotation.Configuration; +//import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder; +//import org.springframework.security.config.annotation.web.builders.HttpSecurity; +//import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity; +//import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter; +//import org.springframework.security.crypto.password.PasswordEncoder; +// +//import javax.sql.DataSource; +// +//@Configuration +//@EnableWebSecurity +//@RequiredArgsConstructor(onConstructor_ = @Autowired) +//public class WebSecurityConfig extends WebSecurityConfigurerAdapter { +// private final DataSource dataSource; +// +// private final PasswordEncoder passwordEncoder; +// +// @Override +// protected void configure(HttpSecurity http) throws Exception { +// http +// .authorizeRequests() +// .antMatchers("/", "/home", "/registration").permitAll() +// .anyRequest().authenticated() +// .and() +// .formLogin() +// .loginPage("/login") +// .permitAll() +// .and() +// .logout() +// .permitAll() +// .and() +// .rememberMe().userDetailsService(this.userDetailsService()) +// .rememberMeServices(null) +// .tokenValiditySeconds(86400) +// .useSecureCookie(true); +// } +// +// @Override +// protected void configure(AuthenticationManagerBuilder auth) throws Exception { +// auth.jdbcAuthentication() +// .dataSource(dataSource) +// .passwordEncoder(passwordEncoder) +// .usersByUsernameQuery("SELECT username, password, active FROM usr WHERE username = ?;") +// .authoritiesByUsernameQuery("SELECT u.username, ur.roles FROM usr u INNER JOIN user_role ur ON u.id = ur.user_id WHERE username = ?;"); +// } +//} diff --git a/src/main/java/site/easystartup/web/auth/controller/AuthController.java b/src/main/java/site/easystartup/web/auth/controller/AuthController.java new file mode 100644 index 0000000..ed4193b --- /dev/null +++ b/src/main/java/site/easystartup/web/auth/controller/AuthController.java @@ -0,0 +1,72 @@ +package site.easystartup.web.auth.controller; + +import lombok.RequiredArgsConstructor; +import org.modelmapper.ModelMapper; +import org.springframework.http.ResponseEntity; +import org.springframework.security.authentication.AuthenticationManager; +import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; +import org.springframework.security.core.Authentication; +import org.springframework.security.core.context.SecurityContextHolder; +import org.springframework.util.ObjectUtils; +import org.springframework.validation.BindingResult; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; +import site.easystartup.web.domain.model.User; +import site.easystartup.web.domain.request.LoginRequest; +import site.easystartup.web.domain.request.SignupRequest; +import site.easystartup.web.domain.response.MessageResponse; +import site.easystartup.web.domain.response.SuccessAuthenticationResponse; +import site.easystartup.web.domain.validation.ResponseErrorValidation; +import site.easystartup.web.project.dto.UserDto; +import site.easystartup.web.security.JWTUtil; +import site.easystartup.web.security.SecurityConstants; +import site.easystartup.web.service.AuthService; +import site.easystartup.web.service.UserService; + +import javax.validation.Valid; + +@RestController +@RequestMapping("/api/auth") +@RequiredArgsConstructor +public class AuthController { + private final JWTUtil jwtUtil; + private final UserService userService; + private final AuthService authService; + private final AuthenticationManager authenticationManager; + private final ResponseErrorValidation responseErrorValidation; + private final ModelMapper modelMapper; + + @PostMapping("/signup") + public ResponseEntity registration(@Valid @RequestBody SignupRequest signupRequest, + BindingResult bindingResult) { + ResponseEntity errors = responseErrorValidation.mapValidationService(bindingResult); + if (!ObjectUtils.isEmpty(errors)) return errors; + + authService.createUser(signupRequest); + + return ResponseEntity.ok(new MessageResponse("Account registered successfully!")); + } + + @PostMapping("/login") + public ResponseEntity authentication(@Valid @RequestBody LoginRequest loginRequest, + BindingResult bindingResult) { + ResponseEntity errors = responseErrorValidation.mapValidationService(bindingResult); + if (!ObjectUtils.isEmpty(errors)) return errors; + + Authentication authentication = authenticationManager.authenticate(new UsernamePasswordAuthenticationToken( + loginRequest.getUsername(), + loginRequest.getPassword() + )); + + SecurityContextHolder.getContext().setAuthentication(authentication); + + String jwt = SecurityConstants.TOKEN_PREFIX + jwtUtil.generateToken(loginRequest.getUsername()); + User user = userService.getUserByUsername(loginRequest.getUsername()); + SuccessAuthenticationResponse response = new SuccessAuthenticationResponse(jwt, modelMapper.map(user, UserDto.class)); + return ResponseEntity.ok().body(response); + } + + +} diff --git a/src/main/java/site/easystartup/web/domain/annotation/PasswordMatches.java b/src/main/java/site/easystartup/web/domain/annotation/PasswordMatches.java new file mode 100644 index 0000000..0309218 --- /dev/null +++ b/src/main/java/site/easystartup/web/domain/annotation/PasswordMatches.java @@ -0,0 +1,19 @@ +package site.easystartup.web.domain.annotation; + +import site.easystartup.web.domain.validation.PasswordMatchesValidator; + +import javax.validation.Constraint; +import javax.validation.Payload; +import java.lang.annotation.*; + +@Target({ElementType.TYPE, ElementType.ANNOTATION_TYPE}) +@Retention(RetentionPolicy.RUNTIME) +@Constraint(validatedBy = PasswordMatchesValidator.class) +@Documented +public @interface PasswordMatches { + String message() default "Password do not match"; + + Class[] groups() default{}; + + Class[] payload() default {}; +} diff --git a/src/main/java/site/easystartup/web/domain/exception/UserExistException.java b/src/main/java/site/easystartup/web/domain/exception/UserExistException.java new file mode 100644 index 0000000..d2782e7 --- /dev/null +++ b/src/main/java/site/easystartup/web/domain/exception/UserExistException.java @@ -0,0 +1,11 @@ +package site.easystartup.web.domain.exception; + +import org.springframework.http.HttpStatus; +import org.springframework.web.bind.annotation.ResponseStatus; + +@ResponseStatus(HttpStatus.BAD_REQUEST) +public class UserExistException extends RuntimeException { + public UserExistException(String message) { + super(message); + } +} diff --git a/src/main/java/site/easystartup/web/domain/model/Role.java b/src/main/java/site/easystartup/web/domain/model/Role.java index 4ef9c82..36dcae3 100644 --- a/src/main/java/site/easystartup/web/domain/model/Role.java +++ b/src/main/java/site/easystartup/web/domain/model/Role.java @@ -1,5 +1,11 @@ package site.easystartup.web.domain.model; public enum Role { - USER, PREMIUM, MODER, ADMIN; + USER("USER"), PREMIUM("PREMIUM"), MODER("MODER"), ADMIN("ADMIN"); + + private String role; + + Role(String role) { + this.role = role; + } } diff --git a/src/main/java/site/easystartup/web/domain/model/User.java b/src/main/java/site/easystartup/web/domain/model/User.java index 614093b..429bc4f 100644 --- a/src/main/java/site/easystartup/web/domain/model/User.java +++ b/src/main/java/site/easystartup/web/domain/model/User.java @@ -2,12 +2,13 @@ package site.easystartup.web.domain.model; import lombok.*; import org.hibernate.Hibernate; +import org.springframework.security.core.GrantedAuthority; +import org.springframework.security.core.authority.SimpleGrantedAuthority; +import org.springframework.security.core.userdetails.UserDetails; import site.easystartup.web.project.domain.model.Participant; import javax.persistence.*; -import java.util.List; -import java.util.Objects; -import java.util.Set; +import java.util.*; @Getter @Setter @@ -15,7 +16,7 @@ import java.util.Set; @RequiredArgsConstructor @Entity @Table(name = "usr") -public class User { +public class User implements UserDetails { @Id @GeneratedValue(strategy = GenerationType.AUTO) @Column(name = "user_id") @@ -75,4 +76,31 @@ public class User { if (roles.contains(Role.PREMIUM) || roles.contains(Role.ADMIN)) return true; return false; } + + @Override + public Collection getAuthorities() { + Collection authorities = new ArrayList<>(); + getRoles().forEach(role -> authorities.add(new SimpleGrantedAuthority(role.name()))); + return authorities; + } + + @Override + public boolean isAccountNonExpired() { + return true; + } + + @Override + public boolean isAccountNonLocked() { + return true; + } + + @Override + public boolean isCredentialsNonExpired() { + return true; + } + + @Override + public boolean isEnabled() { + return true; + } } diff --git a/src/main/java/site/easystartup/web/domain/request/LoginRequest.java b/src/main/java/site/easystartup/web/domain/request/LoginRequest.java new file mode 100644 index 0000000..ec674ce --- /dev/null +++ b/src/main/java/site/easystartup/web/domain/request/LoginRequest.java @@ -0,0 +1,14 @@ +package site.easystartup.web.domain.request; + +import lombok.Data; + +import javax.validation.constraints.Email; +import javax.validation.constraints.NotEmpty; + +@Data +public class LoginRequest { + @NotEmpty(message = "Username cannot be empty") + private String username; + @NotEmpty(message = "Password cannot be empty") + private String password; +} diff --git a/src/main/java/site/easystartup/web/domain/request/SignupRequest.java b/src/main/java/site/easystartup/web/domain/request/SignupRequest.java new file mode 100644 index 0000000..1119baf --- /dev/null +++ b/src/main/java/site/easystartup/web/domain/request/SignupRequest.java @@ -0,0 +1,31 @@ +package site.easystartup.web.domain.request; + +import lombok.Data; +import site.easystartup.web.domain.annotation.PasswordMatches; +import site.easystartup.web.domain.model.Role; + +import javax.validation.constraints.Email; +import javax.validation.constraints.NotBlank; +import javax.validation.constraints.NotEmpty; +import javax.validation.constraints.Size; +import java.util.Set; + +@Data +@PasswordMatches +public class SignupRequest { + @NotEmpty + @Size(min = 3, max = 30, message = "Username should be no less 3 and no more 30 signs") + private String username; + + @Email(message = "It should have email format") + @NotBlank(message = "Email is required") + private String email; + + @NotEmpty(message = "Password is required") + @Size(min = 6, max = 30, message = "Password should be no less 6 and no more 30 signs") + private String password; + + private String confirmPassword; + + private Set roles; +} \ No newline at end of file diff --git a/src/main/java/site/easystartup/web/domain/response/InvalidLoginResponse.java b/src/main/java/site/easystartup/web/domain/response/InvalidLoginResponse.java new file mode 100644 index 0000000..1f46758 --- /dev/null +++ b/src/main/java/site/easystartup/web/domain/response/InvalidLoginResponse.java @@ -0,0 +1,15 @@ +package site.easystartup.web.domain.response; + +import lombok.Getter; +import lombok.RequiredArgsConstructor; + +@Getter +public class InvalidLoginResponse { + private String username; + private String password; + + public InvalidLoginResponse() { + this.username = "Invalid username"; + this.password = "Invalid password"; + } +} diff --git a/src/main/java/site/easystartup/web/domain/response/ResponseMessage.java b/src/main/java/site/easystartup/web/domain/response/MessageResponse.java similarity index 65% rename from src/main/java/site/easystartup/web/domain/response/ResponseMessage.java rename to src/main/java/site/easystartup/web/domain/response/MessageResponse.java index 039902c..3710120 100644 --- a/src/main/java/site/easystartup/web/domain/response/ResponseMessage.java +++ b/src/main/java/site/easystartup/web/domain/response/MessageResponse.java @@ -3,10 +3,10 @@ package site.easystartup.web.domain.response; import lombok.Data; @Data -public class ResponseMessage { +public class MessageResponse { private String message; - public ResponseMessage(String message) { + public MessageResponse(String message) { this.message = message; } } diff --git a/src/main/java/site/easystartup/web/domain/response/SuccessAuthenticationResponse.java b/src/main/java/site/easystartup/web/domain/response/SuccessAuthenticationResponse.java new file mode 100644 index 0000000..ec6a4b7 --- /dev/null +++ b/src/main/java/site/easystartup/web/domain/response/SuccessAuthenticationResponse.java @@ -0,0 +1,12 @@ +package site.easystartup.web.domain.response; + +import lombok.AllArgsConstructor; +import lombok.Getter; +import site.easystartup.web.project.dto.UserDto; + +@Getter +@AllArgsConstructor +public class SuccessAuthenticationResponse { + private String jwt; + private UserDto userDto; +} diff --git a/src/main/java/site/easystartup/web/domain/validation/PasswordMatchesValidator.java b/src/main/java/site/easystartup/web/domain/validation/PasswordMatchesValidator.java new file mode 100644 index 0000000..85129db --- /dev/null +++ b/src/main/java/site/easystartup/web/domain/validation/PasswordMatchesValidator.java @@ -0,0 +1,21 @@ +package site.easystartup.web.domain.validation; + +import site.easystartup.web.domain.annotation.PasswordMatches; +import site.easystartup.web.domain.request.SignupRequest; + +import javax.validation.ConstraintValidator; +import javax.validation.ConstraintValidatorContext; + +public class PasswordMatchesValidator implements ConstraintValidator { + @Override + public void initialize(PasswordMatches constraintAnnotation) { + ConstraintValidator.super.initialize(constraintAnnotation); + } + + @Override + public boolean isValid(Object o, ConstraintValidatorContext constraintValidatorContext) { + SignupRequest signupRequest = (SignupRequest) o; + + return signupRequest.getPassword().equals(signupRequest.getConfirmPassword()); + } +} \ No newline at end of file diff --git a/src/main/java/site/easystartup/web/forum/controller/ForumController.java b/src/main/java/site/easystartup/web/forum/controller/ForumController.java index 6af7b20..fec129f 100644 --- a/src/main/java/site/easystartup/web/forum/controller/ForumController.java +++ b/src/main/java/site/easystartup/web/forum/controller/ForumController.java @@ -6,17 +6,13 @@ import org.modelmapper.ModelMapper; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.http.ResponseEntity; import org.springframework.security.access.annotation.Secured; -import org.springframework.stereotype.Controller; -import org.springframework.ui.Model; import org.springframework.util.ObjectUtils; import org.springframework.validation.BindingResult; import org.springframework.web.bind.annotation.*; import site.easystartup.web.domain.model.User; -import site.easystartup.web.domain.response.ResponseMessage; +import site.easystartup.web.domain.response.MessageResponse; import site.easystartup.web.domain.validation.ResponseErrorValidation; -import site.easystartup.web.forum.domain.model.Discussion; import site.easystartup.web.forum.domain.model.DiscussionMessage; -import site.easystartup.web.forum.domain.model.DiscussionStatus; import site.easystartup.web.forum.domain.model.Topic; import site.easystartup.web.forum.domain.request.DiscussionMessageRequest; import site.easystartup.web.forum.domain.request.DiscussionRequest; @@ -25,10 +21,7 @@ import site.easystartup.web.forum.dto.DiscussionDto; import site.easystartup.web.forum.dto.DiscussionMessageDto; import site.easystartup.web.forum.dto.TopicDto; import site.easystartup.web.forum.service.ForumService; -import site.easystartup.web.forum.repo.TopicRepo; -import site.easystartup.web.project.domain.model.Project; import site.easystartup.web.project.domain.requst.ProjectRequest; -import site.easystartup.web.project.dto.ProjectDto; import site.easystartup.web.service.UserService; import javax.validation.Valid; @@ -151,9 +144,9 @@ public class ForumController { DiscussionMessage message = forumService.getDiscussionMessageById(message_id); User user = userService.getUserByPrincipal(principal); if(!(message.getSender().equals(user.getUsername()) || user.isModer())) - return ResponseEntity.ok(new ResponseMessage("No permissions!")); + return ResponseEntity.ok(new MessageResponse("No permissions!")); forumService.deleteDiscussionMessage(message_id, user); - return ResponseEntity.ok(new ResponseMessage("Message was deleted!")); + return ResponseEntity.ok(new MessageResponse("Message was deleted!")); } diff --git a/src/main/java/site/easystartup/web/project/controller/ProjectController.java b/src/main/java/site/easystartup/web/project/controller/ProjectController.java index 3fefcb2..13a3b43 100644 --- a/src/main/java/site/easystartup/web/project/controller/ProjectController.java +++ b/src/main/java/site/easystartup/web/project/controller/ProjectController.java @@ -9,7 +9,7 @@ import org.springframework.web.bind.annotation.*; import site.easystartup.web.project.domain.model.Project; import site.easystartup.web.project.domain.requst.ParticipantRequest; import site.easystartup.web.project.domain.requst.ProjectRequest; -import site.easystartup.web.domain.response.ResponseMessage; +import site.easystartup.web.domain.response.MessageResponse; import site.easystartup.web.domain.validation.ResponseErrorValidation; import site.easystartup.web.project.dto.ParticipantDto; import site.easystartup.web.project.dto.ProjectDto; @@ -55,7 +55,7 @@ public class ProjectController { public ResponseEntity deleteProject(@PathVariable("projectId") Long projectId, Principal principal) { projectService.deleteProject(projectId, principal); - return ResponseEntity.ok(new ResponseMessage("Project was deleted!")); + return ResponseEntity.ok(new MessageResponse("Project was deleted!")); } @GetMapping("/{projectId}") diff --git a/src/main/java/site/easystartup/web/project/domain/model/Project.java b/src/main/java/site/easystartup/web/project/domain/model/Project.java index 49ed292..348aad8 100644 --- a/src/main/java/site/easystartup/web/project/domain/model/Project.java +++ b/src/main/java/site/easystartup/web/project/domain/model/Project.java @@ -41,7 +41,7 @@ public class Project { private int commercialStatus; @ManyToMany - @JoinTable( + @JoinTable(name = "project_tag", joinColumns = @JoinColumn(name = "project_id"), inverseJoinColumns = @JoinColumn(name = "tag_id") ) diff --git a/src/main/java/site/easystartup/web/project/dto/UserDto.java b/src/main/java/site/easystartup/web/project/dto/UserDto.java index 655ba7f..a1d8034 100644 --- a/src/main/java/site/easystartup/web/project/dto/UserDto.java +++ b/src/main/java/site/easystartup/web/project/dto/UserDto.java @@ -1,23 +1,25 @@ package site.easystartup.web.project.dto; import lombok.Data; +import lombok.NoArgsConstructor; import site.easystartup.web.domain.model.Role; import java.io.Serializable; import java.util.Set; @Data +@NoArgsConstructor public class UserDto implements Serializable { - private final long id; - private final String username; - private final String password; - private final boolean active; - private final String full_name; - private final String status; - private final String iconPath; - private final String email; - private final Set tags; - private final Set professions; - private final Set projects; - private final Set roles; + private long id; + private String username; + private String password; + private boolean active; + private String full_name; + private String status; + private String iconPath; + private String email; + private Set tags; + private Set professions; + private Set projects; + private Set roles; } diff --git a/src/main/java/site/easystartup/web/project/repo/ParticipantRepo.java b/src/main/java/site/easystartup/web/project/repo/ParticipantRepo.java index b7e4b6f..710721e 100644 --- a/src/main/java/site/easystartup/web/project/repo/ParticipantRepo.java +++ b/src/main/java/site/easystartup/web/project/repo/ParticipantRepo.java @@ -11,6 +11,6 @@ import java.util.LinkedHashSet; @Repository public interface ParticipantRepo extends JpaRepository { - @Query ("SELECT Project FROM Participant WHERE nameOfPosition = :nameOfPosition") + @Query ("SELECT p FROM Participant p WHERE p.nameOfPosition = :nameOfPosition") LinkedHashSet findAllByNameOfPosition(@Param("nameOfPosition") String name); } diff --git a/src/main/java/site/easystartup/web/project/repo/ProjectRepo.java b/src/main/java/site/easystartup/web/project/repo/ProjectRepo.java index 508fc8c..ce612f8 100644 --- a/src/main/java/site/easystartup/web/project/repo/ProjectRepo.java +++ b/src/main/java/site/easystartup/web/project/repo/ProjectRepo.java @@ -6,6 +6,7 @@ import org.springframework.data.repository.query.Param; import org.springframework.stereotype.Repository; import site.easystartup.web.domain.model.User; import site.easystartup.web.project.domain.model.Project; +import site.easystartup.web.project.domain.model.Tag; import java.util.List; @@ -13,9 +14,7 @@ import java.util.List; public interface ProjectRepo extends JpaRepository { List findAllByOwnerOrderByCreatedDate(User owner); - - @Query("SELECT p from Project p WHERE p.technology LIKE %:technology%") - List findAllByTechnology(@Param("technology") String tech); + List findAllByTechnology(Tag tag); List findAllByCommercialStatusOrderByCreatedDateAsc(int commercialStatus); diff --git a/src/main/java/site/easystartup/web/project/service/ProjectService.java b/src/main/java/site/easystartup/web/project/service/ProjectService.java index d28acab..64b9619 100644 --- a/src/main/java/site/easystartup/web/project/service/ProjectService.java +++ b/src/main/java/site/easystartup/web/project/service/ProjectService.java @@ -66,7 +66,7 @@ public class ProjectService { } public List getAllProjectsWithTechnology(String technology) { - return projectRepo.findAllByTechnology(technology.trim()); + return projectRepo.findAllByTechnology(tagService.getTechnologyByName(technology.trim())); } public List getAllProjectsForCurrentUser(Principal principal) { diff --git a/src/main/java/site/easystartup/web/project/service/TagService.java b/src/main/java/site/easystartup/web/project/service/TagService.java index c1d5051..70ced97 100644 --- a/src/main/java/site/easystartup/web/project/service/TagService.java +++ b/src/main/java/site/easystartup/web/project/service/TagService.java @@ -31,4 +31,8 @@ public class TagService { }); return tags; } + + public Tag getTechnologyByName(String tag) { + return tagRepo.findTagByTagNameIgnoreCase(tag).orElseThrow(() -> new RuntimeException("Technolohy not found")); + } } diff --git a/src/main/java/site/easystartup/web/repo/UserRepo.java b/src/main/java/site/easystartup/web/repo/UserRepo.java index 54391bf..aaafefb 100644 --- a/src/main/java/site/easystartup/web/repo/UserRepo.java +++ b/src/main/java/site/easystartup/web/repo/UserRepo.java @@ -3,6 +3,10 @@ package site.easystartup.web.repo; import org.springframework.data.jpa.repository.JpaRepository; import site.easystartup.web.domain.model.User; +import java.util.Optional; + public interface UserRepo extends JpaRepository { User findByUsername(String username); + + Optional findUserByUsername(String username); } diff --git a/src/main/java/site/easystartup/web/security/JWTAuthenticationEntryPoint.java b/src/main/java/site/easystartup/web/security/JWTAuthenticationEntryPoint.java new file mode 100644 index 0000000..1dab036 --- /dev/null +++ b/src/main/java/site/easystartup/web/security/JWTAuthenticationEntryPoint.java @@ -0,0 +1,25 @@ +package site.easystartup.web.security; + +import com.google.gson.Gson; +import org.springframework.security.core.AuthenticationException; +import org.springframework.security.web.AuthenticationEntryPoint; +import org.springframework.stereotype.Component; +import site.easystartup.web.domain.response.InvalidLoginResponse; + +import javax.servlet.ServletException; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; +import java.io.IOException; +@Component +public class JWTAuthenticationEntryPoint implements AuthenticationEntryPoint { + @Override + public void commence(HttpServletRequest request, + HttpServletResponse response, + AuthenticationException authException) throws IOException, ServletException { + InvalidLoginResponse invalidLoginResponse = new InvalidLoginResponse(); + String jsonLoginResponse = new Gson().toJson(invalidLoginResponse); + response.setContentType(SecurityConstants.HEADER_STRING); + response.setStatus(HttpServletResponse.SC_UNAUTHORIZED); + response.getWriter().println(jsonLoginResponse); + } +} diff --git a/src/main/java/site/easystartup/web/security/JWTFilter.java b/src/main/java/site/easystartup/web/security/JWTFilter.java new file mode 100644 index 0000000..eda6c4b --- /dev/null +++ b/src/main/java/site/easystartup/web/security/JWTFilter.java @@ -0,0 +1,56 @@ +package site.easystartup.web.security; + +import com.auth0.jwt.exceptions.JWTVerificationException; +import lombok.RequiredArgsConstructor; +import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; +import org.springframework.security.core.context.SecurityContextHolder; +import org.springframework.security.core.userdetails.UserDetails; +import org.springframework.stereotype.Component; +import org.springframework.util.StringUtils; +import org.springframework.web.filter.OncePerRequestFilter; +import site.easystartup.web.service.CustomUserDetailService; + +import javax.servlet.FilterChain; +import javax.servlet.ServletException; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; +import java.io.IOException; + +@Component +@RequiredArgsConstructor +public class JWTFilter extends OncePerRequestFilter { + private final JWTUtil jwtUtil; + private final CustomUserDetailService userDetailService; + + @Override + protected void doFilterInternal(HttpServletRequest request, + HttpServletResponse response, + FilterChain filterChain) throws ServletException, IOException { + String header = request.getHeader(SecurityConstants.HEADER_STRING); + + if (StringUtils.hasText(header) && header.startsWith(SecurityConstants.TOKEN_PREFIX)) { + String jwt = header.split(" ")[1]; + + if (jwt.isEmpty()) { + response.sendError(HttpServletResponse.SC_BAD_REQUEST, "Invalid JWT token in Bearer Header"); + } else { + try { + String email = jwtUtil.validateTokenAndRetrieveClaim(jwt); + UserDetails userDetails = userDetailService.loadUserByUsername(email); + + UsernamePasswordAuthenticationToken authenticationToken = + new UsernamePasswordAuthenticationToken(userDetails, + userDetails.getPassword(), + userDetails.getAuthorities()); + + if (SecurityContextHolder.getContext().getAuthentication() == null) { + SecurityContextHolder.getContext().setAuthentication(authenticationToken); + } + } catch (JWTVerificationException ex) { + response.sendError(HttpServletResponse.SC_BAD_REQUEST, " Invalid JWT token"); + } + } + } + filterChain.doFilter(request, response); + } +} diff --git a/src/main/java/site/easystartup/web/security/JWTUtil.java b/src/main/java/site/easystartup/web/security/JWTUtil.java new file mode 100644 index 0000000..9b9d192 --- /dev/null +++ b/src/main/java/site/easystartup/web/security/JWTUtil.java @@ -0,0 +1,37 @@ +package site.easystartup.web.security; + +import com.auth0.jwt.JWT; +import com.auth0.jwt.JWTVerifier; +import com.auth0.jwt.algorithms.Algorithm; +import com.auth0.jwt.exceptions.JWTVerificationException; +import com.auth0.jwt.interfaces.DecodedJWT; +import org.springframework.stereotype.Component; + +import java.time.ZonedDateTime; +import java.util.Date; +@Component +public class JWTUtil { + public String generateToken(String username) { + + Date expirationDate = Date.from(ZonedDateTime.now().plusDays(30).toInstant()); + + return JWT.create() + .withSubject("User details") + .withClaim("username", username) + .withIssuedAt(Date.from(ZonedDateTime.now().toInstant())) + .withIssuer("site.easystartup") + .withExpiresAt(expirationDate) + .sign(Algorithm.HMAC256(SecurityConstants.SECRET)); + } + + public String validateTokenAndRetrieveClaim(String token) throws JWTVerificationException { + JWTVerifier verifier = JWT.require(Algorithm.HMAC256(SecurityConstants.SECRET)) + .withSubject("User details") + .withIssuer("site.easystartup") + .build(); + + DecodedJWT decodedJWT = verifier.verify(token); + return decodedJWT.getClaim("username").asString(); + + } +} diff --git a/src/main/java/site/easystartup/web/security/SecurityConfig.java b/src/main/java/site/easystartup/web/security/SecurityConfig.java new file mode 100644 index 0000000..2019843 --- /dev/null +++ b/src/main/java/site/easystartup/web/security/SecurityConfig.java @@ -0,0 +1,60 @@ +package site.easystartup.web.security; + +import lombok.RequiredArgsConstructor; +import org.springframework.context.annotation.Bean; +import org.springframework.security.authentication.AuthenticationManager; +import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder; +import org.springframework.security.config.annotation.method.configuration.EnableGlobalMethodSecurity; +import org.springframework.security.config.annotation.web.builders.HttpSecurity; +import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity; +import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter; +import org.springframework.security.config.http.SessionCreationPolicy; +import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder; +import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter; +import site.easystartup.web.service.CustomUserDetailService; + +@EnableWebSecurity +@RequiredArgsConstructor +@EnableGlobalMethodSecurity( + securedEnabled = true, + jsr250Enabled = true, + prePostEnabled = true +) +public class SecurityConfig extends WebSecurityConfigurerAdapter { + + private final CustomUserDetailService customUserDetailService; + private final JWTAuthenticationEntryPoint jwtAuthenticationEntryPoint; + private final JWTFilter jwtFilter; + + @Override + protected void configure(AuthenticationManagerBuilder auth) throws Exception { + auth.userDetailsService(customUserDetailService).passwordEncoder(bCryptPasswordEncoder()); + } + + @Override + protected void configure(HttpSecurity http) throws Exception { + http.cors().and().csrf().disable() + .exceptionHandling().authenticationEntryPoint(jwtAuthenticationEntryPoint) + .and() + .sessionManagement() + .sessionCreationPolicy(SessionCreationPolicy.STATELESS) + .and() + .authorizeRequests() + .antMatchers(SecurityConstants.SIGN_UP_URLS).permitAll() +// .antMatchers("/").permitAll(); + .anyRequest().authenticated(); + + http.addFilterBefore(jwtFilter, UsernamePasswordAuthenticationFilter.class); + } + + @Bean + @Override + public AuthenticationManager authenticationManagerBean() throws Exception { + return super.authenticationManagerBean(); + } + + @Bean + BCryptPasswordEncoder bCryptPasswordEncoder() { + return new BCryptPasswordEncoder(); + } +} diff --git a/src/main/java/site/easystartup/web/security/SecurityConstants.java b/src/main/java/site/easystartup/web/security/SecurityConstants.java new file mode 100644 index 0000000..238be15 --- /dev/null +++ b/src/main/java/site/easystartup/web/security/SecurityConstants.java @@ -0,0 +1,13 @@ +package site.easystartup.web.security; + +public class SecurityConstants { + public static final String SIGN_UP_URLS = "/api/auth/*"; + + public static final String SECRET = "SecretKeyGenJWT"; + public static final String TOKEN_PREFIX = "Bearer "; + public static final String HEADER_STRING = "Authorization"; + public static final String CONTENT_TYPE = "application/json"; + // public static final long EXPIRATION_TIME = 600_000_000; //10min + public static final long EXPIRATION_TIME = Long.MAX_VALUE; + +} diff --git a/src/main/java/site/easystartup/web/service/AuthService.java b/src/main/java/site/easystartup/web/service/AuthService.java new file mode 100644 index 0000000..9ffaec4 --- /dev/null +++ b/src/main/java/site/easystartup/web/service/AuthService.java @@ -0,0 +1,36 @@ +package site.easystartup.web.service; + +import lombok.RequiredArgsConstructor; +import org.springframework.security.crypto.password.PasswordEncoder; +import org.springframework.stereotype.Service; +import site.easystartup.web.domain.exception.UserExistException; +import site.easystartup.web.domain.model.Role; +import site.easystartup.web.domain.model.User; +import site.easystartup.web.domain.request.SignupRequest; +import site.easystartup.web.repo.UserRepo; + +import java.util.HashSet; +import java.util.Set; + +@Service +@RequiredArgsConstructor +public class AuthService { + private final UserRepo userRepo; + private final PasswordEncoder passwordEncoder; + + public User createUser(SignupRequest signupRequest) { + if (userRepo.findUserByUsername(signupRequest.getUsername()).isPresent()) { + throw new UserExistException("An account with this username already exists."); + } + + User user = new User(); + user.setEmail(signupRequest.getEmail()); + user.setUsername(signupRequest.getUsername()); + user.setPassword(passwordEncoder.encode(signupRequest.getPassword())); + Set roles = new HashSet<>(); + signupRequest.getRoles().forEach(role -> roles.add(Role.valueOf(role))); + user.setRoles(roles); + + return userRepo.save(user); + } +} diff --git a/src/main/java/site/easystartup/web/service/CustomUserDetailService.java b/src/main/java/site/easystartup/web/service/CustomUserDetailService.java new file mode 100644 index 0000000..89a5d71 --- /dev/null +++ b/src/main/java/site/easystartup/web/service/CustomUserDetailService.java @@ -0,0 +1,39 @@ +package site.easystartup.web.service; + +import lombok.RequiredArgsConstructor; +import org.springframework.security.core.authority.SimpleGrantedAuthority; + +import org.springframework.security.core.userdetails.UserDetails; +import org.springframework.security.core.userdetails.UserDetailsService; +import org.springframework.security.core.userdetails.UsernameNotFoundException; +import org.springframework.stereotype.Service; +import site.easystartup.web.domain.model.User; +import site.easystartup.web.repo.UserRepo; + +import java.util.ArrayList; +import java.util.Collection; + +@Service +@RequiredArgsConstructor +public class CustomUserDetailService implements UserDetailsService { + private final UserRepo userRepo; + @Override + public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException { + User user = userRepo.findUserByUsername(username) + .orElseThrow(() -> new UsernameNotFoundException("User not found")); + + Collection authorities = new ArrayList<>(); + user.getRoles().forEach(role -> { + authorities.add(new SimpleGrantedAuthority(role.name())); + }); + + return new org.springframework.security.core.userdetails.User(user.getUsername(), user.getPassword(), authorities); + } + + public UserDetails loadUserById(Long userId) { + User user = userRepo.findById(userId) + .orElseThrow(() -> new UsernameNotFoundException("User not found")); + return user; + + } +} diff --git a/src/main/java/site/easystartup/web/service/UserService.java b/src/main/java/site/easystartup/web/service/UserService.java index 6c66cb0..2700184 100644 --- a/src/main/java/site/easystartup/web/service/UserService.java +++ b/src/main/java/site/easystartup/web/service/UserService.java @@ -22,4 +22,8 @@ public class UserService { public User getUserById(Long userId) { return userRepo.findById(userId).orElseThrow(() -> new UsernameNotFoundException("User not found")); } + + public User getUserByUsername(String username) { + return userRepo.findUserByUsername(username).orElseThrow(() -> new UsernameNotFoundException("User not found")); + } } diff --git a/src/main/java/site/easystartup/web/storage/service/StorageService.java b/src/main/java/site/easystartup/web/storage/service/StorageService.java index e1ca5c5..a64620a 100644 --- a/src/main/java/site/easystartup/web/storage/service/StorageService.java +++ b/src/main/java/site/easystartup/web/storage/service/StorageService.java @@ -1,11 +1,12 @@ package site.easystartup.web.storage.service; import org.springframework.core.io.Resource; +import org.springframework.stereotype.Service; import org.springframework.web.multipart.MultipartFile; import java.nio.file.Path; import java.util.stream.Stream; - +@Service public interface StorageService { void init(); void save(MultipartFile file); diff --git a/src/main/resources/application.properties b/src/main/resources/application.properties index 96ccdf5..b6e4ffc 100644 --- a/src/main/resources/application.properties +++ b/src/main/resources/application.properties @@ -9,3 +9,4 @@ spring.datasource.url=jdbc:postgresql://localhost:5432/testdb spring.datasource.username=postgres spring.datasource.password=root +spring.security.filter.order=10 \ No newline at end of file