diff --git a/pom.xml b/pom.xml index 9cff8df..56549a5 100644 --- a/pom.xml +++ b/pom.xml @@ -23,7 +23,7 @@ org.springframework.boot - spring-boot-starter-oauth2-client + spring-boot-starter-oauth2-resource-server org.springframework.boot diff --git a/src/main/java/com/provedcode/config/InitConfig.java b/src/main/java/com/provedcode/config/InitConfig.java new file mode 100644 index 0000000..fe066d2 --- /dev/null +++ b/src/main/java/com/provedcode/config/InitConfig.java @@ -0,0 +1,29 @@ +package com.provedcode.config; + +import com.provedcode.user.mapper.UserInfoMapper; +import com.provedcode.user.repo.UserInfoRepository; +import lombok.AllArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.boot.CommandLineRunner; +import org.springframework.security.crypto.password.PasswordEncoder; +import org.springframework.stereotype.Component; + +@Slf4j +@Component +@AllArgsConstructor +public class InitConfig implements CommandLineRunner { + UserInfoRepository userInfoRepository; + PasswordEncoder passwordEncoder; + UserInfoMapper userInfoMapper; + + @Override + public void run(String... args) throws Exception { + // TODO: Method to change passwords for already created users from data.sql + userInfoRepository.saveAll( + userInfoRepository.findAll().stream() + .map(i -> { + i.setPassword(passwordEncoder.encode(i.getPassword())); + return i; + }).toList()); + } +} diff --git a/src/main/java/com/provedcode/config/SecurityConfig.java b/src/main/java/com/provedcode/config/SecurityConfig.java index 819c2af..6591a05 100644 --- a/src/main/java/com/provedcode/config/SecurityConfig.java +++ b/src/main/java/com/provedcode/config/SecurityConfig.java @@ -1,12 +1,36 @@ package com.provedcode.config; +import com.nimbusds.jose.jwk.JWKSet; +import com.nimbusds.jose.jwk.RSAKey; +import com.nimbusds.jose.jwk.source.ImmutableJWKSet; +import com.provedcode.user.mapper.UserInfoMapper; +import com.provedcode.user.repo.UserInfoRepository; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; +import org.springframework.security.config.Customizer; import org.springframework.security.config.annotation.method.configuration.EnableMethodSecurity; 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.configurers.oauth2.server.resource.OAuth2ResourceServerConfigurer; +import org.springframework.security.core.userdetails.UserDetailsService; +import org.springframework.security.core.userdetails.UsernameNotFoundException; +import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder; +import org.springframework.security.crypto.password.PasswordEncoder; +import org.springframework.security.oauth2.jwt.JwtDecoder; +import org.springframework.security.oauth2.jwt.JwtEncoder; +import org.springframework.security.oauth2.jwt.NimbusJwtDecoder; +import org.springframework.security.oauth2.jwt.NimbusJwtEncoder; +import org.springframework.security.oauth2.server.resource.authentication.JwtAuthenticationConverter; +import org.springframework.security.oauth2.server.resource.authentication.JwtGrantedAuthoritiesConverter; +import org.springframework.security.oauth2.server.resource.web.BearerTokenAuthenticationEntryPoint; +import org.springframework.security.oauth2.server.resource.web.access.BearerTokenAccessDeniedHandler; import org.springframework.security.web.SecurityFilterChain; +import java.security.KeyPair; +import java.security.KeyPairGenerator; +import java.security.NoSuchAlgorithmException; +import java.security.interfaces.RSAPublicKey; + import static org.springframework.security.config.http.SessionCreationPolicy.STATELESS; import static org.springframework.security.web.util.matcher.AntPathRequestMatcher.antMatcher; @@ -19,11 +43,65 @@ public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Excepti http.authorizeHttpRequests(c -> c .requestMatchers("/actuator/health").permitAll() // for DevOps .requestMatchers(antMatcher("/h2/**")).permitAll() - .requestMatchers(antMatcher("/api/**")).permitAll() - .anyRequest().denyAll() + .requestMatchers(antMatcher("/api/talents/**")).permitAll() + .anyRequest().authenticated() ); - http.csrf().disable().headers().frameOptions().disable(); + + http.httpBasic(Customizer.withDefaults()); + http.csrf().disable().headers().disable(); + http.sessionManagement().sessionCreationPolicy(STATELESS); + + http.oauth2ResourceServer(OAuth2ResourceServerConfigurer::jwt) + .exceptionHandling(c -> c + .authenticationEntryPoint(new BearerTokenAuthenticationEntryPoint()) + .accessDeniedHandler(new BearerTokenAccessDeniedHandler()) + ); + return http.build(); } + + @Bean + public PasswordEncoder passwordEncoder() { + return new BCryptPasswordEncoder(); + } + + @Bean + public KeyPair keyPair() throws NoSuchAlgorithmException { + KeyPairGenerator keyPairGenerator = KeyPairGenerator.getInstance("RSA"); + keyPairGenerator.initialize(2048); + return keyPairGenerator.generateKeyPair(); + } + + @Bean + public JwtAuthenticationConverter jwtAuthenticationConverter() { + JwtGrantedAuthoritiesConverter grantedAuthoritiesConverter = new JwtGrantedAuthoritiesConverter(); + grantedAuthoritiesConverter.setAuthorityPrefix(""); + + JwtAuthenticationConverter jwtAuthenticationConverter = new JwtAuthenticationConverter(); + jwtAuthenticationConverter.setJwtGrantedAuthoritiesConverter(grantedAuthoritiesConverter); + return jwtAuthenticationConverter; + } + + @Bean + UserDetailsService userDetailsService( + UserInfoRepository repository, + UserInfoMapper mapper + ) { + return login -> repository.findByLogin(login) + .map(mapper::toUserDetails) + .orElseThrow(() -> new UsernameNotFoundException(login + " not found")); + } + + @Bean + JwtDecoder jwtDecoder(KeyPair keyPair) { + return NimbusJwtDecoder.withPublicKey((RSAPublicKey) keyPair.getPublic()).build(); + } + + @Bean + JwtEncoder jwtEncoder(KeyPair keyPair) { + var jwk = new RSAKey.Builder((RSAPublicKey) keyPair.getPublic()).privateKey(keyPair.getPrivate()).build(); + var jwkSet = new ImmutableJWKSet<>(new JWKSet(jwk)); + return new NimbusJwtEncoder(jwkSet); + } } diff --git a/src/main/java/com/provedcode/handlers/TalentExceptionHandler.java b/src/main/java/com/provedcode/handlers/TalentExceptionHandler.java index 86300b8..575e4a5 100644 --- a/src/main/java/com/provedcode/handlers/TalentExceptionHandler.java +++ b/src/main/java/com/provedcode/handlers/TalentExceptionHandler.java @@ -8,7 +8,7 @@ @ControllerAdvice public class TalentExceptionHandler { @ExceptionHandler(ResponseStatusException.class) - private ResponseEntity responseStatusExceptionHandler(ResponseStatusException exception) { + private ResponseEntity responseStatusExceptionHandler(ResponseStatusException exception) { return ResponseEntity.status(exception.getStatusCode()).body(exception.getBody()); } } diff --git a/src/main/java/com/provedcode/talent/TalentController.java b/src/main/java/com/provedcode/talent/TalentController.java index db226e9..a372bdb 100644 --- a/src/main/java/com/provedcode/talent/TalentController.java +++ b/src/main/java/com/provedcode/talent/TalentController.java @@ -4,28 +4,33 @@ import com.provedcode.talent.model.dto.ShortTalentDTO; import com.provedcode.talent.service.TalentService; import lombok.AllArgsConstructor; +import lombok.extern.slf4j.Slf4j; import org.springframework.data.domain.Page; import org.springframework.http.HttpStatus; +import org.springframework.security.access.prepost.PreAuthorize; import org.springframework.web.bind.annotation.*; import java.util.Optional; +@Slf4j @RestController @AllArgsConstructor -@CrossOrigin(origins = "*", allowedHeaders = "*", methods = {RequestMethod.GET, RequestMethod.POST, RequestMethod.PUT, - RequestMethod.DELETE}) +@CrossOrigin(origins = "*", allowedHeaders = "*", methods = {RequestMethod.GET, RequestMethod.POST, RequestMethod.PUT, RequestMethod.DELETE}) +@RequestMapping("/api") public class TalentController { TalentService talentService; - @GetMapping("/api/talents/{id}") + @PreAuthorize("hasRole('TALENT')") + @GetMapping("/talents/{id}") FullTalentDTO getTalent(@PathVariable("id") long id) { return talentService.getTalentById(id); } - @GetMapping("/api/talents") + @GetMapping("/talents") @ResponseStatus(HttpStatus.OK) Page getTalents(@RequestParam(value = "page") Optional page, @RequestParam(value = "size") Optional size) { return talentService.getTalentsPage(page, size); } + } diff --git a/src/main/java/com/provedcode/talent/mapper/impl/TalentMapperImpl.java b/src/main/java/com/provedcode/talent/mapper/impl/TalentMapperImpl.java index 90516ac..2b0794b 100644 --- a/src/main/java/com/provedcode/talent/mapper/impl/TalentMapperImpl.java +++ b/src/main/java/com/provedcode/talent/mapper/impl/TalentMapperImpl.java @@ -11,28 +11,32 @@ public class TalentMapperImpl implements TalentMapper { @Override public ShortTalentDTO talentToShortTalentDTO(Talent talent) { return ShortTalentDTO.builder() - .id(talent.getId()) - .image(talent.getImage()) - .firstname(talent.getFirstName()) - .lastname(talent.getLastName()) - .specialization(talent.getSpecialization()) - .skills(talent.getTalentSkills().stream().map(TalentSkill::getSkill).toList()) - .build(); + .id(talent.getId()) + .image(talent.getImage()) + .firstName(talent.getFirstName()) + .lastName(talent.getLastName()) + .specialization(talent.getSpecialization()) + .skills(talent.getTalentSkills().stream().map(TalentSkill::getSkill).toList()) + .build(); } + @Override public FullTalentDTO talentToFullTalentDTO(Talent talent) { return FullTalentDTO.builder() - .id(talent.getId()) - .firstname(talent.getFirstName()) - .lastname(talent.getLastName()) - .bio(talent.getTalentDescription().getBio()) - .additionalInfo(talent.getTalentDescription().getAdditionalInfo()) - .image(talent.getImage()) - .specialization(talent.getSpecialization()) - .links(talent.getTalentLinks().stream().map(TalentLink::getLink).toList()) - .contacts(talent.getTalentContacts().stream().map(TalentContact::getContact).toList()) - .skills(talent.getTalentSkills().stream().map(TalentSkill::getSkill).toList()) - .attachedFiles(talent.getTalentAttachedFiles().stream().map(TalentAttachedFile::getAttachedFile).toList()) - .build(); + .id(talent.getId()) + .firstName(talent.getFirstName()) + .lastName(talent.getLastName()) + .bio(talent.getTalentDescription() != null ? talent.getTalentDescription().getBio() : null) + .additionalInfo(talent.getTalentDescription() != null ? talent.getTalentDescription() + .getAdditionalInfo() : null) + .image(talent.getImage()) + .specialization(talent.getSpecialization()) + .links(talent.getTalentLinks().stream().map(TalentLink::getLink).toList()) + .contacts(talent.getTalentContacts().stream().map(TalentContact::getContact).toList()) + .skills(talent.getTalentSkills().stream().map(TalentSkill::getSkill).toList()) + .attachedFiles( + talent.getTalentAttachedFiles().stream().map(TalentAttachedFile::getAttachedFile) + .toList()) + .build(); } } diff --git a/src/main/java/com/provedcode/talent/model/dto/FullTalentDTO.java b/src/main/java/com/provedcode/talent/model/dto/FullTalentDTO.java index a8a72c2..4bd9394 100644 --- a/src/main/java/com/provedcode/talent/model/dto/FullTalentDTO.java +++ b/src/main/java/com/provedcode/talent/model/dto/FullTalentDTO.java @@ -7,8 +7,8 @@ @Builder public record FullTalentDTO ( Long id, - String firstname, - String lastname, + String firstName, + String lastName, String image, String specialization, String additionalInfo, diff --git a/src/main/java/com/provedcode/talent/model/dto/ShortTalentDTO.java b/src/main/java/com/provedcode/talent/model/dto/ShortTalentDTO.java index a9ca9a7..491bcdb 100644 --- a/src/main/java/com/provedcode/talent/model/dto/ShortTalentDTO.java +++ b/src/main/java/com/provedcode/talent/model/dto/ShortTalentDTO.java @@ -8,8 +8,8 @@ public record ShortTalentDTO( Long id, String image, - String firstname, - String lastname, + String firstName, + String lastName, String specialization, List skills ) { diff --git a/src/main/java/com/provedcode/talent/model/entity/Talent.java b/src/main/java/com/provedcode/talent/model/entity/Talent.java index f52f6f1..232de47 100644 --- a/src/main/java/com/provedcode/talent/model/entity/Talent.java +++ b/src/main/java/com/provedcode/talent/model/entity/Talent.java @@ -46,5 +46,4 @@ public class Talent { private List talentContacts = new ArrayList<>(); @OneToMany(fetch = FetchType.EAGER, mappedBy = "talent", orphanRemoval = true) private List talentAttachedFiles = new ArrayList<>(); - } \ No newline at end of file diff --git a/src/main/java/com/provedcode/talent/service/impl/TalentServiceImpl.java b/src/main/java/com/provedcode/talent/service/impl/TalentServiceImpl.java index af20a7f..eb1217d 100644 --- a/src/main/java/com/provedcode/talent/service/impl/TalentServiceImpl.java +++ b/src/main/java/com/provedcode/talent/service/impl/TalentServiceImpl.java @@ -1,23 +1,21 @@ package com.provedcode.talent.service.impl; import com.provedcode.config.PageProperties; -import com.provedcode.talent.service.TalentService; import com.provedcode.talent.mapper.TalentMapper; import com.provedcode.talent.model.dto.FullTalentDTO; import com.provedcode.talent.model.dto.ShortTalentDTO; import com.provedcode.talent.model.entity.Talent; import com.provedcode.talent.repo.TalentRepository; +import com.provedcode.talent.service.TalentService; import lombok.AllArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.springframework.data.domain.Page; -import org.springframework.data.domain.PageImpl; import org.springframework.data.domain.PageRequest; import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; import org.springframework.web.server.ResponseStatusException; -import java.util.List; import java.util.Optional; -import java.util.stream.Collectors; import static org.springframework.http.HttpStatus.BAD_REQUEST; import static org.springframework.http.HttpStatus.NOT_FOUND; @@ -25,12 +23,14 @@ @Service @Slf4j @AllArgsConstructor +@Transactional public class TalentServiceImpl implements TalentService { TalentMapper talentMapper; TalentRepository talentRepository; PageProperties pageProperties; @Override + @Transactional(readOnly = true) public Page getTalentsPage(Optional page, Optional size) { if (page.orElse(pageProperties.defaultPageNum()) < 0) { throw new ResponseStatusException(BAD_REQUEST, "'page' query parameter must be greater than or equal to 0"); @@ -38,15 +38,17 @@ public Page getTalentsPage(Optional page, Optional talent = talentRepository.findById(id); - if (talent.isEmpty()){ + if (talent.isEmpty()) { throw new ResponseStatusException(NOT_FOUND, String.format("talent with id = %d not found", id)); } return talentMapper.talentToFullTalentDTO(talent.get()); diff --git a/src/main/java/com/provedcode/user/controller/AuthenticationController.java b/src/main/java/com/provedcode/user/controller/AuthenticationController.java new file mode 100644 index 0000000..1111d1e --- /dev/null +++ b/src/main/java/com/provedcode/user/controller/AuthenticationController.java @@ -0,0 +1,32 @@ +package com.provedcode.user.controller; + +import com.provedcode.user.model.dto.RegistrationDTO; +import com.provedcode.user.model.dto.SessionInfoDTO; +import com.provedcode.user.service.AuthenticationService; +import jakarta.validation.Valid; +import lombok.AllArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.http.HttpStatus; +import org.springframework.security.core.Authentication; +import org.springframework.web.bind.annotation.*; + +@RestController +@AllArgsConstructor +@Slf4j +@RequestMapping("/api/talents") +@CrossOrigin(origins = "*", allowedHeaders = "*", methods = {RequestMethod.GET, RequestMethod.POST, RequestMethod.PUT, RequestMethod.DELETE}) +public class AuthenticationController { + AuthenticationService authenticationService; + + @PostMapping("/login") + SessionInfoDTO login(Authentication authentication) { + return authenticationService.login(authentication.getName(), authentication.getAuthorities()); + } + + @PostMapping("/register") + @ResponseStatus(HttpStatus.OK) + SessionInfoDTO register(@RequestBody @Valid RegistrationDTO user) { + return authenticationService.register(user); + } + +} diff --git a/src/main/java/com/provedcode/user/mapper/UserInfoMapper.java b/src/main/java/com/provedcode/user/mapper/UserInfoMapper.java new file mode 100644 index 0000000..d7138b4 --- /dev/null +++ b/src/main/java/com/provedcode/user/mapper/UserInfoMapper.java @@ -0,0 +1,8 @@ +package com.provedcode.user.mapper; + +import com.provedcode.user.model.entity.UserInfo; +import org.springframework.security.core.userdetails.UserDetails; + +public interface UserInfoMapper { + UserDetails toUserDetails(UserInfo user); +} diff --git a/src/main/java/com/provedcode/user/mapper/impl/UserInfoMapperImpl.java b/src/main/java/com/provedcode/user/mapper/impl/UserInfoMapperImpl.java new file mode 100644 index 0000000..9db05e9 --- /dev/null +++ b/src/main/java/com/provedcode/user/mapper/impl/UserInfoMapperImpl.java @@ -0,0 +1,24 @@ +package com.provedcode.user.mapper.impl; + +import com.provedcode.user.mapper.UserInfoMapper; +import com.provedcode.user.model.entity.UserInfo; +import org.springframework.security.core.authority.SimpleGrantedAuthority; +import org.springframework.security.core.userdetails.User; +import org.springframework.security.core.userdetails.UserDetails; +import org.springframework.stereotype.Component; + +@Component +public class UserInfoMapperImpl implements UserInfoMapper { + @Override + public UserDetails toUserDetails(UserInfo user) { + return User.withUsername(user.getLogin()) + .password(user.getPassword()) + .authorities(user.getUserAuthorities() + .stream() + .map(i -> new SimpleGrantedAuthority( + i.getAuthority() + .getAuthority())) + .toList()) + .build(); + } +} diff --git a/src/main/java/com/provedcode/user/model/Role.java b/src/main/java/com/provedcode/user/model/Role.java new file mode 100644 index 0000000..d1f1e35 --- /dev/null +++ b/src/main/java/com/provedcode/user/model/Role.java @@ -0,0 +1,15 @@ +package com.provedcode.user.model; + +public enum Role { + TALENT("ROLE_TALENT"); + private final String userRole; + + Role(String role) { + this.userRole = role; + } + + @Override + public String toString() { + return this.userRole; + } +} diff --git a/src/main/java/com/provedcode/user/model/dto/RegistrationDTO.java b/src/main/java/com/provedcode/user/model/dto/RegistrationDTO.java new file mode 100644 index 0000000..ae3ae5f --- /dev/null +++ b/src/main/java/com/provedcode/user/model/dto/RegistrationDTO.java @@ -0,0 +1,19 @@ +package com.provedcode.user.model.dto; + +import com.fasterxml.jackson.annotation.JsonProperty; +import jakarta.validation.constraints.NotEmpty; +import lombok.Builder; + +@Builder +public record RegistrationDTO( + @NotEmpty String login, + @NotEmpty String password, + @JsonProperty("first_name") + @NotEmpty + String firstName, + @JsonProperty("last_name") + @NotEmpty + String lastName, + @NotEmpty String specialization +) { +} diff --git a/src/main/java/com/provedcode/user/model/dto/SessionInfoDTO.java b/src/main/java/com/provedcode/user/model/dto/SessionInfoDTO.java new file mode 100644 index 0000000..17686d8 --- /dev/null +++ b/src/main/java/com/provedcode/user/model/dto/SessionInfoDTO.java @@ -0,0 +1,10 @@ +package com.provedcode.user.model.dto; + +import lombok.Builder; + +@Builder +public record SessionInfoDTO( + String status, + String token +) { +} diff --git a/src/main/java/com/provedcode/user/model/entity/Authority.java b/src/main/java/com/provedcode/user/model/entity/Authority.java index 384c35a..dc8a965 100644 --- a/src/main/java/com/provedcode/user/model/entity/Authority.java +++ b/src/main/java/com/provedcode/user/model/entity/Authority.java @@ -6,9 +6,6 @@ import lombok.Getter; import lombok.Setter; -import java.util.LinkedHashSet; -import java.util.Set; - @Getter @Setter @Entity @@ -20,6 +17,6 @@ public class Authority { private Long id; @NotEmpty @NotNull - @Column(name = "authority", length = 50) + @Column(name = "authority", length = 20) private String authority; } \ No newline at end of file diff --git a/src/main/java/com/provedcode/user/model/entity/UserAuthority.java b/src/main/java/com/provedcode/user/model/entity/UserAuthority.java index 5a71b00..d8bbcf7 100644 --- a/src/main/java/com/provedcode/user/model/entity/UserAuthority.java +++ b/src/main/java/com/provedcode/user/model/entity/UserAuthority.java @@ -1,12 +1,12 @@ package com.provedcode.user.model.entity; import jakarta.persistence.*; -import lombok.Getter; -import lombok.Setter; - -import java.util.LinkedHashSet; -import java.util.Set; +import jakarta.validation.constraints.NotNull; +import lombok.*; +@Builder +@AllArgsConstructor +@NoArgsConstructor @Getter @Setter @Entity @@ -16,9 +16,11 @@ public class UserAuthority { @GeneratedValue(strategy = GenerationType.IDENTITY) @Column(name = "id", nullable = false) private Long id; + @NotNull @ManyToOne @JoinColumn(name = "user_id") private UserInfo userInfo; + @NotNull @ManyToOne @JoinColumn(name = "authority_id") private Authority authority; diff --git a/src/main/java/com/provedcode/user/model/entity/UserInfo.java b/src/main/java/com/provedcode/user/model/entity/UserInfo.java index 27f9a8e..2f7efca 100644 --- a/src/main/java/com/provedcode/user/model/entity/UserInfo.java +++ b/src/main/java/com/provedcode/user/model/entity/UserInfo.java @@ -2,29 +2,39 @@ import com.provedcode.talent.model.entity.Talent; import jakarta.persistence.*; +import jakarta.validation.constraints.NotEmpty; import jakarta.validation.constraints.NotNull; -import lombok.Getter; -import lombok.Setter; +import lombok.*; import java.util.LinkedHashSet; import java.util.Set; +@Builder +@AllArgsConstructor +@NoArgsConstructor @Getter @Setter @Entity @Table(name = "user_info") public class UserInfo { - @NotNull @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) @Column(name = "id", nullable = false) private Long id; + @NotNull + @Column(name = "user_id") + private Long userId; + @NotEmpty + @NotNull @Column(name = "login", length = 100) private String login; + @NotEmpty + @NotNull @Column(name = "password") private String password; @OneToOne(orphanRemoval = true) - @JoinColumn(name = "id") + @JoinColumn(name = "user_id", insertable = false, updatable = false) private Talent talent; - @OneToMany(mappedBy = "userInfo", orphanRemoval = true) + @OneToMany(mappedBy = "userInfo", orphanRemoval = true, fetch = FetchType.EAGER) private Set userAuthorities = new LinkedHashSet<>(); } \ No newline at end of file diff --git a/src/main/java/com/provedcode/user/repo/AuthorityRepository.java b/src/main/java/com/provedcode/user/repo/AuthorityRepository.java new file mode 100644 index 0000000..dbb713e --- /dev/null +++ b/src/main/java/com/provedcode/user/repo/AuthorityRepository.java @@ -0,0 +1,10 @@ +package com.provedcode.user.repo; + +import com.provedcode.user.model.entity.Authority; +import org.springframework.data.jpa.repository.JpaRepository; + +import java.util.Optional; + +public interface AuthorityRepository extends JpaRepository { + Optional findByAuthority(String authority); +} \ No newline at end of file diff --git a/src/main/java/com/provedcode/user/repo/UserAuthorityRepository.java b/src/main/java/com/provedcode/user/repo/UserAuthorityRepository.java new file mode 100644 index 0000000..ef91bfc --- /dev/null +++ b/src/main/java/com/provedcode/user/repo/UserAuthorityRepository.java @@ -0,0 +1,10 @@ +package com.provedcode.user.repo; + +import com.provedcode.user.model.entity.UserAuthority; +import org.springframework.data.jpa.repository.JpaRepository; + +import java.util.Optional; +import java.util.Set; + +public interface UserAuthorityRepository extends JpaRepository { +} \ No newline at end of file diff --git a/src/main/java/com/provedcode/user/repo/UserInfoRepository.java b/src/main/java/com/provedcode/user/repo/UserInfoRepository.java index bb340ee..50f93ee 100644 --- a/src/main/java/com/provedcode/user/repo/UserInfoRepository.java +++ b/src/main/java/com/provedcode/user/repo/UserInfoRepository.java @@ -3,5 +3,10 @@ import com.provedcode.user.model.entity.UserInfo; import org.springframework.data.jpa.repository.JpaRepository; +import java.util.Optional; + public interface UserInfoRepository extends JpaRepository { + boolean existsByLogin(String login); + Optional findByLogin(String login); + } \ No newline at end of file diff --git a/src/main/java/com/provedcode/user/service/AuthenticationService.java b/src/main/java/com/provedcode/user/service/AuthenticationService.java new file mode 100644 index 0000000..743015d --- /dev/null +++ b/src/main/java/com/provedcode/user/service/AuthenticationService.java @@ -0,0 +1,12 @@ +package com.provedcode.user.service; + +import com.provedcode.user.model.dto.RegistrationDTO; +import com.provedcode.user.model.dto.SessionInfoDTO; +import org.springframework.security.core.GrantedAuthority; + +import java.util.Collection; + +public interface AuthenticationService { + SessionInfoDTO login(String name, Collection authorities); + SessionInfoDTO register(RegistrationDTO user); +} diff --git a/src/main/java/com/provedcode/user/service/impl/AuthenticationServiceImpl.java b/src/main/java/com/provedcode/user/service/impl/AuthenticationServiceImpl.java new file mode 100644 index 0000000..b195ccd --- /dev/null +++ b/src/main/java/com/provedcode/user/service/impl/AuthenticationServiceImpl.java @@ -0,0 +1,100 @@ +package com.provedcode.user.service.impl; + +import com.provedcode.talent.model.entity.Talent; +import com.provedcode.talent.repo.db.TalentEntityRepository; +import com.provedcode.user.model.Role; +import com.provedcode.user.model.dto.RegistrationDTO; +import com.provedcode.user.model.dto.SessionInfoDTO; +import com.provedcode.user.model.entity.UserAuthority; +import com.provedcode.user.model.entity.UserInfo; +import com.provedcode.user.repo.AuthorityRepository; +import com.provedcode.user.repo.UserAuthorityRepository; +import com.provedcode.user.repo.UserInfoRepository; +import com.provedcode.user.service.AuthenticationService; +import lombok.AllArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.http.HttpStatus; +import org.springframework.security.core.GrantedAuthority; +import org.springframework.security.core.authority.SimpleGrantedAuthority; +import org.springframework.security.crypto.password.PasswordEncoder; +import org.springframework.security.oauth2.jwt.JwtClaimsSet; +import org.springframework.security.oauth2.jwt.JwtEncoder; +import org.springframework.security.oauth2.jwt.JwtEncoderParameters; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; +import org.springframework.web.server.ResponseStatusException; + +import java.time.Instant; +import java.util.Collection; +import java.util.Set; +import java.util.stream.Collectors; + +import static java.time.temporal.ChronoUnit.MINUTES; + +@Service +@AllArgsConstructor +@Slf4j +public class AuthenticationServiceImpl implements AuthenticationService { + JwtEncoder jwtEncoder; + UserInfoRepository userInfoRepository; + TalentEntityRepository talentEntityRepository; + UserAuthorityRepository userAuthorityRepository; + AuthorityRepository authorityRepository; + PasswordEncoder passwordEncoder; + + @Transactional + public SessionInfoDTO login(String name, Collection authorities) { + return new SessionInfoDTO("User {%s} log-in".formatted(name), generateJWTToken(name, authorities)); + } + + @Transactional + public SessionInfoDTO register(RegistrationDTO user) { + if (userInfoRepository.existsByLogin(user.login())) { + throw new ResponseStatusException(HttpStatus.CONFLICT, + String.format("user with login = {%s} already exists", user.login())); + } + Talent talent = Talent.builder() + .firstName(user.firstName()) + .lastName(user.lastName()) + .specialization(user.specialization()) + .build(); + talentEntityRepository.save(talent); + + UserInfo userInfo = UserInfo.builder() + .userId(talent.getId()) + .login(user.login()) + .password(passwordEncoder.encode(user.password())) + .build(); + UserAuthority userAuthority = UserAuthority.builder() + .userInfo(userInfo) + .authority(authorityRepository.findByAuthority(Role.TALENT.toString()) + .orElseThrow(() -> new ResponseStatusException(HttpStatus.BAD_REQUEST, "this authority does`t exist"))) + .build(); + + userInfo.setUserAuthorities(Set.of(userAuthority)); + userAuthority.setUserInfo(userInfoRepository.save(userInfo)); + userAuthorityRepository.save(userAuthority); + + String userLogin = userInfo.getLogin(); + Collection userAuthorities = userInfo.getUserAuthorities().stream().map(i -> new SimpleGrantedAuthority(i.getAuthority().getAuthority())).toList(); + + log.info("user with login {%s} was saved, his authorities: %s".formatted(userLogin, userAuthorities)); + + return new SessionInfoDTO("User: {%s} was registered".formatted(userLogin), generateJWTToken(userLogin, userAuthorities)); + } + + private String generateJWTToken(String name, Collection authorities) { + log.info("=== POST /login === auth.name = {}", name); + log.info("=== POST /login === auth = {}", authorities); + var now = Instant.now(); + var claims = JwtClaimsSet.builder() + .issuer("self") + .issuedAt(now) + .expiresAt(now.plus(60, MINUTES)) + .subject(name) + .claim("scope", authorities.stream().map(GrantedAuthority::getAuthority).collect(Collectors.joining(" "))) + .build(); + return jwtEncoder.encode(JwtEncoderParameters.from(claims)).getTokenValue(); + } + +} diff --git a/src/main/resources/data.sql b/src/main/resources/data.sql index 6b0f1ae..7e1c2fb 100644 --- a/src/main/resources/data.sql +++ b/src/main/resources/data.sql @@ -1,3 +1,12 @@ +insert into authority (id, authority) +values (1, 'ROLE_TALENT'); +-- FOR USER AUTHORITY +-- SELECT USER_INFO.ID , LOGIN , PASSWORD, USER_ID , AUTHORITY FROM +-- USER_INFO +-- JOIN USER_AUTHORITY ON USER_ID = USER_INFO.ID +-- JOIN AUTHORITY ON AUTHORITY.ID = AUTHORITY_ID + + insert into talent (first_name, last_name, specialization, image) values ('Serhii', 'Soloviov', 'Java-Developer', 'https://i.pinimg.com/564x/e1/08/49/e10849923a8b2e85a7adf494ebd063e6.jpg'); insert into talent_description (talent_id, BIO, addition_info) @@ -29,6 +38,12 @@ values ((select id from talent order by id desc limit 1), 'second_file'); insert into talent_attached_file (talent_id, attached_file) values ((select id from talent order by id desc limit 1), 'third_file'); +insert into user_info (user_id, login, password) +values ((select id from talent order by id desc limit 1), 'SerhiiSoloviov', 'password'); +insert into user_authority (user_id, authority_id) +values ((select id from user_info order by id desc limit 1), + (select authority.id from authority where id = 1)); + insert into talent (first_name, last_name, specialization, image) values ('Mykhailo', 'Ordyntsev', 'Java-Developer', 'https://i.pinimg.com/564x/c2/41/31/c24131fe00218467721ba5bacdf0a256.jpg'); insert into talent_description (talent_id, BIO, addition_info) @@ -60,6 +75,12 @@ values ((select id from talent order by id desc limit 1), 'MykhailoOrdyntsev_sec insert into talent_attached_file (talent_id, attached_file) values ((select id from talent order by id desc limit 1), 'MykhailoOrdyntsev_third_file'); +insert into user_info (user_id, login, password) +values ((select id from talent order by id desc limit 1), 'MykhailoOrdyntsev', 'password'); +insert into user_authority (user_id, authority_id) +values ((select id from user_info order by id desc limit 1), + (select authority.id from authority where id = 1)); + insert into talent (first_name, last_name, specialization, image) values ('Denis', 'Boyko', 'Java-Developer', 'https://i.pinimg.com/564x/2a/0c/08/2a0c08c421e253ca895c3fdc8c9e08d9.jpg'); insert into talent_description (talent_id, BIO, addition_info) @@ -89,6 +110,12 @@ values ((select id from talent order by id desc limit 1), 'DenisBoyko_second_fil insert into talent_attached_file (talent_id, attached_file) values ((select id from talent order by id desc limit 1), 'DenisBoyko_third_file'); +insert into user_info (user_id, login, password) +values ((select id from talent order by id desc limit 1), 'DenisBoyko', 'password'); +insert into user_authority (user_id, authority_id) +values ((select id from user_info order by id desc limit 1), + (select authority.id from authority where id = 1)); + insert into talent (first_name, last_name, specialization, image) values ('Ihor', 'Schurenko', 'Java-Developer', 'https://i.pinimg.com/564x/e1/11/2f/e1112f0b7b63644dc3e313084936dedb.jpg'); insert into talent_description (talent_id, BIO, addition_info) @@ -115,6 +142,13 @@ insert into talent_attached_file (talent_id, attached_file) values ((select id from talent order by id desc limit 1), 'IhorShchurenko_second_file'); insert into talent_attached_file (talent_id, attached_file) values ((select id from talent order by id desc limit 1), 'IhorShchurenko_third_file'); + +insert into user_info (user_id, login, password) +values ((select id from talent order by id desc limit 1), 'DmytroUzun', 'password'); +insert into user_authority (user_id, authority_id) +values ((select id from user_info order by id desc limit 1), + (select authority.id from authority where id = 1)); + insert into talent (first_name, last_name, specialization, image) values ('Dmytro', 'Uzun', 'Dev-Ops', 'https://i.pinimg.com/564x/1c/af/87/1caf8771ef3edf351f6f2bf6f1c0a276.jpg'); insert into talent_description (talent_id, BIO, addition_info) @@ -144,6 +178,12 @@ values ((select id from talent order by id desc limit 1), 'DmytroUzun_second_fil insert into talent_attached_file (talent_id, attached_file) values ((select id from talent order by id desc limit 1), 'DmytroUzun_third_file'); +insert into user_info (user_id, login, password) +values ((select id from talent order by id desc limit 1), 'DmytroUzun', 'password'); +insert into user_authority (user_id, authority_id) +values ((select id from user_info order by id desc limit 1), + (select authority.id from authority where id = 1)); + insert into talent (first_name, last_name, specialization, image) values ('Viktor', 'Voloshko', 'Dev-Ops', 'https://i.pinimg.com/564x/a9/51/ab/a951ab682413b89617235e65564c1e5e.jpg'); insert into talent_description (talent_id, BIO, addition_info) @@ -171,6 +211,12 @@ values ((select id from talent order by id desc limit 1), 'ViktorVoloshko_second insert into talent_attached_file (talent_id, attached_file) values ((select id from talent order by id desc limit 1), 'ViktorVoloshko_third_file'); +insert into user_info (user_id, login, password) +values ((select id from talent order by id desc limit 1), 'ViktorVoloshko', 'password'); +insert into user_authority (user_id, authority_id) +values ((select id from user_info order by id desc limit 1), + (select authority.id from authority where id = 1)); + insert into talent (first_name, last_name, specialization, image) values ('Olha', 'Moiseienko', 'QA', 'https://i.pinimg.com/564x/6d/9d/43/6d9d437baf4db114c047d927307beb84.jpg'); insert into talent_description (talent_id, BIO, addition_info) @@ -200,6 +246,12 @@ values ((select id from talent order by id desc limit 1), 'OlhaMoiseienko_second insert into talent_attached_file (talent_id, attached_file) values ((select id from talent order by id desc limit 1), 'OlhaMoiseienko _third_file'); +insert into user_info (user_id, login, password) +values ((select id from talent order by id desc limit 1), 'OlhaMoiseienko', 'password'); +insert into user_authority (user_id, authority_id) +values ((select id from user_info order by id desc limit 1), + (select authority.id from authority where id = 1)); + insert into talent (first_name, last_name, specialization, image) values ('Maxim', 'Kiyashko', 'QA', 'https://i.pinimg.com/564x/80/2d/58/802d58b0302985f9486893d499d3634d.jpg'); insert into talent_description (talent_id, BIO, addition_info) @@ -227,6 +279,12 @@ values ((select id from talent order by id desc limit 1), 'MaximKiyashko_second_ insert into talent_attached_file (talent_id, attached_file) values ((select id from talent order by id desc limit 1), 'MaximKiyashko_third_file'); +insert into user_info (user_id, login, password) +values ((select id from talent order by id desc limit 1), 'MaximKiyashko', 'password'); +insert into user_authority (user_id, authority_id) +values ((select id from user_info order by id desc limit 1), + (select authority.id from authority where id = 1)); + insert into talent (first_name, last_name, specialization, image) values ('Nikolaiev', 'Oleksii', 'QA', 'https://i.pinimg.com/564x/54/d1/0d/54d10dfce64afefabc9fbbce5de82c87.jpg'); insert into talent_description (talent_id, BIO, addition_info) @@ -256,6 +314,12 @@ values ((select id from talent order by id desc limit 1), 'NikolaievOleksii_seco insert into talent_attached_file (talent_id, attached_file) values ((select id from talent order by id desc limit 1), 'NikolaievOleksiio_third_file'); +insert into user_info (user_id, login, password) +values ((select id from talent order by id desc limit 1), 'NikolaievOleksiio', 'password'); +insert into user_authority (user_id, authority_id) +values ((select id from user_info order by id desc limit 1), + (select authority.id from authority where id = 1)); + insert into talent (first_name, last_name, specialization, image) values ('Artem', 'Lytvynenko', 'QA', 'https://i.pinimg.com/564x/87/63/55/87635509c5fa7ee496ec351fa7e67eaa.jpg'); insert into talent_description (talent_id, BIO, addition_info) @@ -283,6 +347,12 @@ values ((select id from talent order by id desc limit 1), 'ArtemLytvynenko_secon insert into talent_attached_file (talent_id, attached_file) values ((select id from talent order by id desc limit 1), 'ArtemLytvynenko_third_file'); +insert into user_info (user_id, login, password) +values ((select id from talent order by id desc limit 1), 'ArtemLytvynenko', 'password'); +insert into user_authority (user_id, authority_id) +values ((select id from user_info order by id desc limit 1), + (select authority.id from authority where id = 1)); + insert into talent (first_name, last_name, specialization, image) values ('Daniil', 'Yevtukhov', 'Java-Script-Developer', 'https://i.pinimg.com/564x/fe/b1/37/feb137d88a3d1c8fb28796db6cbc576f.jpg'); insert into talent_description (talent_id, BIO, addition_info) @@ -310,6 +380,12 @@ values ((select id from talent order by id desc limit 1), 'DaniilYevtukhov_secon insert into talent_attached_file (talent_id, attached_file) values ((select id from talent order by id desc limit 1), 'DaniilYevtukhov_third_file'); +insert into user_info (user_id, login, password) +values ((select id from talent order by id desc limit 1), 'DaniilYevtukhov', 'password'); +insert into user_authority (user_id, authority_id) +values ((select id from user_info order by id desc limit 1), + (select authority.id from authority where id = 1)); + insert into talent (first_name, last_name, specialization, image) values ('Ruslan', 'Morozov', 'Java-Script-Developer', 'https://i.pinimg.com/736x/36/ae/0e/36ae0ea4aad656f7c3d3175bc33b8399.jpg'); insert into talent_description (talent_id, BIO, addition_info) @@ -339,6 +415,12 @@ values ((select id from talent order by id desc limit 1), 'RuslanMorozov_second_ insert into talent_attached_file (talent_id, attached_file) values ((select id from talent order by id desc limit 1), 'RuslanMorozov_third_file'); +insert into user_info (user_id, login, password) +values ((select id from talent order by id desc limit 1), 'RuslanMorozov', 'password'); +insert into user_authority (user_id, authority_id) +values ((select id from user_info order by id desc limit 1), + (select authority.id from authority where id = 1)); + insert into talent (first_name, last_name, specialization, image) values ('Ihor', 'Kopieichykov', 'Java-Script-Developer', 'https://i.pinimg.com/564x/0d/f0/83/0df083121bac75f64e3d93c7c5682d04.jpg'); insert into talent_description (talent_id, BIO, addition_info) @@ -366,4 +448,10 @@ values ((select id from talent order by id desc limit 1), 'IhorKopieichykov_firs insert into talent_attached_file (talent_id, attached_file) values ((select id from talent order by id desc limit 1), 'IhorKopieichykov_second_file'); insert into talent_attached_file (talent_id, attached_file) -values ((select id from talent order by id desc limit 1), 'IhorKopieichykov_third_file'); \ No newline at end of file +values ((select id from talent order by id desc limit 1), 'IhorKopieichykov_third_file'); + +insert into user_info (user_id, login, password) +values ((select id from talent order by id desc limit 1), 'IhorKopieichykov', 'password'); +insert into user_authority (user_id, authority_id) +values ((select id from user_info order by id desc limit 1), + (select authority.id from authority where id = 1)); diff --git a/src/main/resources/schema.sql b/src/main/resources/schema.sql index 05c6a9e..2166cfd 100644 --- a/src/main/resources/schema.sql +++ b/src/main/resources/schema.sql @@ -1,14 +1,14 @@ -DROP TABLE IF EXISTS talent CASCADE ; -DROP TABLE IF EXISTS talent_description CASCADE ; -DROP TABLE IF EXISTS talent_link CASCADE ; -DROP TABLE IF EXISTS talent_contact CASCADE ; -DROP TABLE IF EXISTS talent_attached_file CASCADE ; -DROP TABLE IF EXISTS talent_skill CASCADE ; -DROP TABLE IF EXISTS user_authority CASCADE ; -DROP TABLE IF EXISTS user_info CASCADE ; -DROP TABLE IF EXISTS authority CASCADE ; - -CREATE TABLE talent ( +drop table IF EXISTS talent CASCADE ; +drop table IF EXISTS talent_description CASCADE ; +drop table IF EXISTS talent_link CASCADE ; +drop table IF EXISTS talent_contact CASCADE ; +drop table IF EXISTS talent_attached_file CASCADE ; +drop table IF EXISTS talent_skill CASCADE ; +drop table IF EXISTS user_authority CASCADE ; +drop table IF EXISTS user_info CASCADE ; +drop table IF EXISTS authority CASCADE ; + +create TABLE talent ( id BIGINT GENERATED BY DEFAULT AS IDENTITY NOT NULL, first_name VARCHAR(20) NOT NULL, last_name VARCHAR(20) NOT NULL, @@ -64,28 +64,29 @@ create TABLE talent_attached_file ( alter table talent_attached_file add CONSTRAINT FK_TALENT_ATTACHED_FILE_ON_TALENT FOREIGN KEY (talent_id) REFERENCES talent (id); --user tables-- -CREATE TABLE user_info ( - id BIGINT NOT NULL, - login VARCHAR(100), - password VARCHAR(255), +create TABLE user_info ( + id BIGINT AUTO_INCREMENT NOT NULL, + user_id BIGINT NOT NULL, + login VARCHAR(100) NOT NULL, + password VARCHAR(255) NOT NULL, CONSTRAINT pk_user_info PRIMARY KEY (id) ); -ALTER TABLE user_info ADD CONSTRAINT FK_USER_INFO_ON_ID FOREIGN KEY (id) REFERENCES talent (id); +alter table user_info add CONSTRAINT FK_USER_INFO_ON_USER FOREIGN KEY (user_id) REFERENCES talent (id); -CREATE TABLE authority ( +create TABLE authority ( id BIGINT GENERATED BY DEFAULT AS IDENTITY NOT NULL, - authority VARCHAR(50) NOT NULL, + authority VARCHAR(20) NOT NULL, CONSTRAINT pk_authority PRIMARY KEY (id) ); -CREATE TABLE user_authority ( +create TABLE user_authority ( id BIGINT GENERATED BY DEFAULT AS IDENTITY NOT NULL, user_id BIGINT, authority_id BIGINT, CONSTRAINT pk_user_authority PRIMARY KEY (id) ); -ALTER TABLE user_authority ADD CONSTRAINT FK_USER_AUTHORITY_ON_AUTHORITY FOREIGN KEY (authority_id) REFERENCES authority (id); +alter table user_authority add CONSTRAINT FK_USER_AUTHORITY_ON_AUTHORITY FOREIGN KEY (authority_id) REFERENCES authority (id); -ALTER TABLE user_authority ADD CONSTRAINT FK_USER_AUTHORITY_ON_USER FOREIGN KEY (user_id) REFERENCES user_info (id); \ No newline at end of file +alter table user_authority add CONSTRAINT FK_USER_AUTHORITY_ON_USER FOREIGN KEY (user_id) REFERENCES user_info (id); \ No newline at end of file