From 7a080504aaf238e4b06c6154966856c8278ce728 Mon Sep 17 00:00:00 2001 From: harish Date: Wed, 21 Aug 2024 18:21:20 +0530 Subject: [PATCH] Security implementation --- pom.xml | 24 ++++ .../config/SecurityConfig.java | 111 ++++++++++++++++++ .../config/jwt/JWTConfigurer.java | 23 ++++ .../config/jwt/JWTFilter.java | 43 +++++++ .../config/jwt/TokenProvider.java | 93 +++++++++++++++ .../constants/GepafinConstant.java | 4 + .../tendermanagement/dao/RegionDao.java | 2 +- .../gepafin/tendermanagement/dao/RoleDao.java | 10 +- .../gepafin/tendermanagement/dao/UserDao.java | 52 ++++++-- .../tendermanagement/model/BaseBean.java | 16 +++ .../model/request/LoginReq.java | 18 +++ .../model/request/UserReq.java | 16 +-- .../model/response/LoginResponse.java | 42 +++++++ .../model/response/RegionResponseBean.java | 6 +- .../model/response/RoleResponseBean.java | 8 +- .../model/response/UserResponseBean.java | 12 +- .../tendermanagement/model/util/JWTToken.java | 26 ++++ .../repositories/UserRepository.java | 2 + .../tendermanagement/service/UserService.java | 7 ++ .../service/impl/AuthenticationService.java | 60 ++++++++++ .../service/impl/RegionServiceImpl.java | 7 ++ .../service/impl/RoleServiceImpl.java | 6 + .../service/impl/UserServiceImpl.java | 17 ++- .../web/rest/api/UserApi.java | 43 ++++--- .../api/errors/GlobalExceptionHandler.java | 23 ++++ .../api/impl/CustomUserDetailsService.java | 55 +++++++++ .../web/rest/api/impl/UserApiController.java | 48 ++++---- src/main/resources/application.properties | 11 +- src/main/resources/message_en.properties | 7 ++ src/main/resources/message_it.properties | 7 ++ 30 files changed, 721 insertions(+), 78 deletions(-) create mode 100644 src/main/java/net/gepafin/tendermanagement/config/SecurityConfig.java create mode 100644 src/main/java/net/gepafin/tendermanagement/config/jwt/JWTConfigurer.java create mode 100644 src/main/java/net/gepafin/tendermanagement/config/jwt/JWTFilter.java create mode 100644 src/main/java/net/gepafin/tendermanagement/config/jwt/TokenProvider.java create mode 100644 src/main/java/net/gepafin/tendermanagement/model/BaseBean.java create mode 100644 src/main/java/net/gepafin/tendermanagement/model/request/LoginReq.java create mode 100644 src/main/java/net/gepafin/tendermanagement/model/response/LoginResponse.java create mode 100644 src/main/java/net/gepafin/tendermanagement/model/util/JWTToken.java create mode 100644 src/main/java/net/gepafin/tendermanagement/service/impl/AuthenticationService.java create mode 100644 src/main/java/net/gepafin/tendermanagement/web/rest/api/errors/GlobalExceptionHandler.java create mode 100644 src/main/java/net/gepafin/tendermanagement/web/rest/api/impl/CustomUserDetailsService.java diff --git a/pom.xml b/pom.xml index 1572627b..d2022b37 100644 --- a/pom.xml +++ b/pom.xml @@ -80,6 +80,30 @@ spring-boot-starter-test test + + org.springframework.boot + spring-boot-starter-security + + + io.jsonwebtoken + jjwt-api + 0.11.5 + + + io.jsonwebtoken + jjwt-impl + 0.11.5 + + + io.jsonwebtoken + jjwt-jackson + 0.11.5 + + + org.springframework.boot + spring-boot-starter-web + + diff --git a/src/main/java/net/gepafin/tendermanagement/config/SecurityConfig.java b/src/main/java/net/gepafin/tendermanagement/config/SecurityConfig.java new file mode 100644 index 00000000..1c97a2de --- /dev/null +++ b/src/main/java/net/gepafin/tendermanagement/config/SecurityConfig.java @@ -0,0 +1,111 @@ +package net.gepafin.tendermanagement.config; + +import io.swagger.v3.oas.models.Components; +import io.swagger.v3.oas.models.OpenAPI; +import io.swagger.v3.oas.models.security.SecurityRequirement; +import io.swagger.v3.oas.models.security.SecurityScheme; +import io.swagger.v3.oas.models.servers.Server; +import net.gepafin.tendermanagement.config.jwt.JWTConfigurer; +import net.gepafin.tendermanagement.config.jwt.JWTFilter; +import net.gepafin.tendermanagement.config.jwt.TokenProvider; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.http.HttpMethod; +import org.springframework.security.authentication.AuthenticationManager; +import org.springframework.security.config.annotation.authentication.configuration.AuthenticationConfiguration; +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.WebSecurityCustomizer; +import org.springframework.security.config.annotation.web.configurers.AbstractHttpConfigurer; +import org.springframework.security.config.http.SessionCreationPolicy; +import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder; +import org.springframework.security.crypto.password.PasswordEncoder; +import org.springframework.security.web.SecurityFilterChain; +import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter; +import org.springframework.security.web.servlet.util.matcher.MvcRequestMatcher; +import org.springframework.security.web.util.matcher.AntPathRequestMatcher; +import org.springframework.web.cors.CorsConfiguration; +import org.springframework.web.cors.UrlBasedCorsConfigurationSource; +import org.springframework.web.filter.CorsFilter; +import org.springframework.web.servlet.handler.HandlerMappingIntrospector; + +@Configuration +@EnableWebSecurity +public class SecurityConfig { + + private final TokenProvider tokenProvider; + + @Autowired + public SecurityConfig(TokenProvider tokenProvider) { + this.tokenProvider = tokenProvider; + } + + @Bean + public AuthenticationManager authenticationManager(AuthenticationConfiguration config) throws Exception { + return config.getAuthenticationManager(); + } + + @Bean + public PasswordEncoder passwordEncoder() { + return new BCryptPasswordEncoder(); + } + + @Bean + MvcRequestMatcher.Builder mvc(HandlerMappingIntrospector introspector) { + return new MvcRequestMatcher.Builder(introspector); + } + + @Bean + public WebSecurityCustomizer webSecurityCustomizer(MvcRequestMatcher.Builder mvc) { + return (web) -> web.ignoring() + .requestMatchers(mvc.pattern(HttpMethod.OPTIONS, "/**")) + .requestMatchers(new AntPathRequestMatcher("/i18n/**")) + .requestMatchers(new AntPathRequestMatcher("/content/**")) + .requestMatchers(new AntPathRequestMatcher("/swagger-ui/index.html")) + .requestMatchers(new AntPathRequestMatcher("/swagger-ui/**")); + } + + @Bean + public CorsFilter corsFilter() { + UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource(); + CorsConfiguration config = new CorsConfiguration(); + config.setAllowCredentials(true); + config.addAllowedOrigin("*"); + config.addAllowedHeader("*"); + config.addAllowedMethod("*"); + source.registerCorsConfiguration("/**", config); + return new CorsFilter(source); + } + + @Bean + public SecurityFilterChain securityFilterChain(HttpSecurity http, MvcRequestMatcher.Builder mvc) throws Exception { + http + .csrf(AbstractHttpConfigurer::disable) + .authorizeHttpRequests(auth -> auth + .requestMatchers(mvc.pattern(HttpMethod.POST, "/v1/user/login")).permitAll() + .requestMatchers(mvc.pattern(HttpMethod.POST, "/v1/user")).permitAll() + .requestMatchers("/swagger-ui/**").permitAll() + .requestMatchers("/v1/api-docs/**").permitAll() + .anyRequest().authenticated() + ) + .sessionManagement(session -> session + .sessionCreationPolicy(SessionCreationPolicy.STATELESS) + ) + .apply(new JWTConfigurer(tokenProvider)) + .and() + .addFilterBefore(new JWTFilter(tokenProvider), UsernamePasswordAuthenticationFilter.class); + + return http.build(); + } + + @Bean + public OpenAPI customOpenAPI() { + return new OpenAPI() + .addServersItem(new Server().url("/")) + .addSecurityItem(new SecurityRequirement().addList("bearer-key")) + .components(new Components().addSecuritySchemes("bearer-key", + new SecurityScheme().type(SecurityScheme.Type.HTTP) + .scheme("bearer").bearerFormat("JWT"))); + } +} diff --git a/src/main/java/net/gepafin/tendermanagement/config/jwt/JWTConfigurer.java b/src/main/java/net/gepafin/tendermanagement/config/jwt/JWTConfigurer.java new file mode 100644 index 00000000..1ee7efe6 --- /dev/null +++ b/src/main/java/net/gepafin/tendermanagement/config/jwt/JWTConfigurer.java @@ -0,0 +1,23 @@ +package net.gepafin.tendermanagement.config.jwt; + +import org.springframework.security.config.annotation.SecurityConfigurerAdapter; +import org.springframework.security.config.annotation.web.builders.HttpSecurity; +import org.springframework.security.web.DefaultSecurityFilterChain; +import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter; + +public class JWTConfigurer extends SecurityConfigurerAdapter { + + public static final String AUTHORIZATION_HEADER = "Authorization"; + + private TokenProvider tokenProvider; + + public JWTConfigurer(TokenProvider tokenProvider) { + this.tokenProvider = tokenProvider; + } + + @Override + public void configure(HttpSecurity http) throws Exception { + JWTFilter customFilter = new JWTFilter(tokenProvider); + http.addFilterBefore(customFilter, UsernamePasswordAuthenticationFilter.class); + } +} diff --git a/src/main/java/net/gepafin/tendermanagement/config/jwt/JWTFilter.java b/src/main/java/net/gepafin/tendermanagement/config/jwt/JWTFilter.java new file mode 100644 index 00000000..4d8d5948 --- /dev/null +++ b/src/main/java/net/gepafin/tendermanagement/config/jwt/JWTFilter.java @@ -0,0 +1,43 @@ +package net.gepafin.tendermanagement.config.jwt; + +import jakarta.servlet.FilterChain; +import jakarta.servlet.ServletException; +import jakarta.servlet.ServletRequest; +import jakarta.servlet.ServletResponse; +import jakarta.servlet.http.HttpServletRequest; +import org.springframework.security.core.Authentication; +import org.springframework.security.core.context.SecurityContextHolder; +import org.springframework.util.StringUtils; +import org.springframework.web.filter.GenericFilterBean; + +import java.io.IOException; + +public class JWTFilter extends GenericFilterBean { + + private final TokenProvider tokenProvider; + + public JWTFilter(TokenProvider tokenProvider) { + this.tokenProvider = tokenProvider; + } + + @Override + public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) + throws IOException, ServletException { + HttpServletRequest httpServletRequest = (HttpServletRequest) servletRequest; + String token = resolveToken(httpServletRequest); + + if (StringUtils.hasText(token) && tokenProvider.validateToken(token)) { + Authentication authentication = tokenProvider.getAuthentication(token); + if (authentication != null) { + SecurityContextHolder.getContext().setAuthentication(authentication); + } + } + + filterChain.doFilter(servletRequest, servletResponse); + } + + private String resolveToken(HttpServletRequest request) { + String bearerToken = request.getHeader("Authorization"); + return StringUtils.hasText(bearerToken) && bearerToken.startsWith("Bearer ") ? bearerToken.substring(7) : null; + } +} diff --git a/src/main/java/net/gepafin/tendermanagement/config/jwt/TokenProvider.java b/src/main/java/net/gepafin/tendermanagement/config/jwt/TokenProvider.java new file mode 100644 index 00000000..7ac7948e --- /dev/null +++ b/src/main/java/net/gepafin/tendermanagement/config/jwt/TokenProvider.java @@ -0,0 +1,93 @@ +package net.gepafin.tendermanagement.config.jwt; + +import io.jsonwebtoken.Claims; +import io.jsonwebtoken.Jwts; +import io.jsonwebtoken.SignatureAlgorithm; +import io.jsonwebtoken.security.Keys; +import jakarta.annotation.PostConstruct; +import org.apache.commons.lang3.time.DateUtils; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; +import org.springframework.security.core.Authentication; +import org.springframework.security.core.GrantedAuthority; +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; + +import javax.crypto.SecretKey; +import java.nio.charset.StandardCharsets; +import java.util.*; +import java.util.stream.Collectors; +@Component +public class TokenProvider { + private final Logger log = LoggerFactory.getLogger(TokenProvider.class); + + @Value("${security.authentication.jwt.secret}") + private String secretKey; + + @Value("${security.authentication.jwt.token-validity-in-seconds}") + private long tokenValidityInSeconds; + + private SecretKey key; + + @PostConstruct + public void init() { + this.key = Keys.hmacShaKeyFor(secretKey.getBytes(StandardCharsets.UTF_8)); + } + public String createToken(Authentication authentication,Boolean rememberMe) { + String authorities = authentication.getAuthorities().stream() + .map(GrantedAuthority::getAuthority) + .collect(Collectors.joining(",")); + Long now = null; + Date validity=null; + if(Boolean.TRUE.equals(rememberMe)) { + now= DateUtils.addMonths(new Date(), 2).getTime(); + validity = new Date(now); + }else { + now=(new Date()).getTime(); + validity = new Date(now + (this.tokenValidityInSeconds * 1000)); + } + return Jwts.builder() + .setSubject(authentication.getName()) + .claim("auth", authorities) + .signWith(key, SignatureAlgorithm.HS512) + .setExpiration(validity) + .compact(); + } + + public Authentication getAuthentication(String token) { + Claims claims = Jwts.parserBuilder() + .setSigningKey(key) + .build() + .parseClaimsJws(token) + .getBody(); + UserDetails principal = new User(claims.getSubject(), "", Collections.emptyList()); + + return new UsernamePasswordAuthenticationToken(principal, token, principal.getAuthorities()); + } + + + private Collection ClaimsToAuthorities(Object authClaim) { + return authClaim == null || ((String) authClaim).isEmpty() ? + Collections.emptyList() : + Arrays.stream(((String) authClaim).split(",")) + .map(SimpleGrantedAuthority::new) + .collect(Collectors.toList()); + } + + public boolean validateToken(String authToken) { + try { + Jwts.parserBuilder() + .setSigningKey(key) + .build() + .parseClaimsJws(authToken); + return true; + } catch (Exception e) { + log.info("Token validation failed: " + e.getMessage()); + return false; + } + } +} diff --git a/src/main/java/net/gepafin/tendermanagement/constants/GepafinConstant.java b/src/main/java/net/gepafin/tendermanagement/constants/GepafinConstant.java index 7d00913f..af5b5475 100644 --- a/src/main/java/net/gepafin/tendermanagement/constants/GepafinConstant.java +++ b/src/main/java/net/gepafin/tendermanagement/constants/GepafinConstant.java @@ -24,4 +24,8 @@ public class GepafinConstant { public static final String DELETE_REGION_SUCCESS_MSG = "delete.region.success"; public static final String REGION_NOT_FOUND_MSG = "user.region.not.found"; public static final String PASSWORD_DOESNT_MATCH ="password.doesnt.match"; + public static final String LOGIN_SUCCESS_MSG="login.successfully"; + public static final String PASSWORD_MIN_LEN ="pass.min.len.msg"; + public static final String EMAIL_ALREADY_EXISTS = "email.already.exists"; + } diff --git a/src/main/java/net/gepafin/tendermanagement/dao/RegionDao.java b/src/main/java/net/gepafin/tendermanagement/dao/RegionDao.java index 27f7f250..3cc3d997 100644 --- a/src/main/java/net/gepafin/tendermanagement/dao/RegionDao.java +++ b/src/main/java/net/gepafin/tendermanagement/dao/RegionDao.java @@ -50,7 +50,7 @@ public class RegionDao { regionEntity.setEducationLevel(regionReq.getEducationLevel()); return regionEntity; } - private RegionResponseBean convertRegionEntityToRegionResponse(RegionEntity regionEntity) { + public RegionResponseBean convertRegionEntityToRegionResponse(RegionEntity regionEntity) { RegionResponseBean regionResponseBean = new RegionResponseBean(); regionResponseBean.setId(regionEntity.getId()); regionResponseBean.setCreatedDate(regionEntity.getCreatedDate()); diff --git a/src/main/java/net/gepafin/tendermanagement/dao/RoleDao.java b/src/main/java/net/gepafin/tendermanagement/dao/RoleDao.java index 08c1a40f..fe25a923 100644 --- a/src/main/java/net/gepafin/tendermanagement/dao/RoleDao.java +++ b/src/main/java/net/gepafin/tendermanagement/dao/RoleDao.java @@ -4,6 +4,7 @@ import net.gepafin.tendermanagement.config.Translator; import net.gepafin.tendermanagement.constants.GepafinConstant; import net.gepafin.tendermanagement.entities.RoleEntity; import net.gepafin.tendermanagement.model.request.RoleReq; +import net.gepafin.tendermanagement.model.response.RegionResponseBean; import net.gepafin.tendermanagement.model.response.RoleResponseBean; import net.gepafin.tendermanagement.repositories.RoleRepository; import net.gepafin.tendermanagement.service.RegionService; @@ -23,9 +24,13 @@ public class RoleDao { @Autowired private RoleRepository roleRepository; + @Autowired private RegionService regionService; + @Autowired + private RegionDao regionDao; + public RoleResponseBean createRole(RoleReq roleReq) { RoleEntity roleEntity = convertRoleRequestToRoleEntity(roleReq); roleEntity = roleRepository.save(roleEntity); @@ -39,7 +44,7 @@ public class RoleDao { roleEntity.setRegion(regionService.getRegionById(roleReq.getRegionId())); return roleEntity; } - private RoleResponseBean convertRoleEntityToRoleResponse(RoleEntity roleEntity) { + public RoleResponseBean convertRoleEntityToRoleResponse(RoleEntity roleEntity) { RoleResponseBean roleResponseBean=new RoleResponseBean(); roleResponseBean.setId(roleEntity.getId()); roleResponseBean.setCreatedDate(roleEntity.getCreatedDate()); @@ -47,7 +52,8 @@ public class RoleDao { roleResponseBean.setRoleName(roleEntity.getRoleName()); roleResponseBean.setDescription(roleEntity.getDescription()); roleResponseBean.setPermissions(roleEntity.getPermissions()); - roleResponseBean.setRegion(roleEntity.getRegion()); + RegionResponseBean regionResponseBean = regionDao.convertRegionEntityToRegionResponse(roleEntity.getRegion()); + roleResponseBean.setRegion(regionResponseBean); return roleResponseBean; } public RoleResponseBean updateRole(Long id, RoleReq roleReq) { diff --git a/src/main/java/net/gepafin/tendermanagement/dao/UserDao.java b/src/main/java/net/gepafin/tendermanagement/dao/UserDao.java index 63a7c353..eecdd4e0 100644 --- a/src/main/java/net/gepafin/tendermanagement/dao/UserDao.java +++ b/src/main/java/net/gepafin/tendermanagement/dao/UserDao.java @@ -1,33 +1,53 @@ package net.gepafin.tendermanagement.dao; import net.gepafin.tendermanagement.config.Translator; +import net.gepafin.tendermanagement.config.jwt.TokenProvider; import net.gepafin.tendermanagement.constants.GepafinConstant; import net.gepafin.tendermanagement.entities.RoleEntity; import net.gepafin.tendermanagement.entities.UserEntity; +import net.gepafin.tendermanagement.model.request.LoginReq; import net.gepafin.tendermanagement.model.request.UpdateUserReq; import net.gepafin.tendermanagement.model.request.UserReq; +import net.gepafin.tendermanagement.model.response.LoginResponse; +import net.gepafin.tendermanagement.model.response.RoleResponseBean; import net.gepafin.tendermanagement.model.response.UserResponseBean; +import net.gepafin.tendermanagement.model.util.JWTToken; import net.gepafin.tendermanagement.repositories.UserRepository; import net.gepafin.tendermanagement.service.RoleService; +import net.gepafin.tendermanagement.service.impl.AuthenticationService; import net.gepafin.tendermanagement.web.rest.api.errors.CustomValidationException; import net.gepafin.tendermanagement.web.rest.api.errors.ResourceNotFoundException; import net.gepafin.tendermanagement.web.rest.api.errors.Status; import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.security.crypto.password.PasswordEncoder; import org.springframework.stereotype.Repository; import static net.gepafin.tendermanagement.util.ObjectUtils.setIfUpdated; - @Repository public class UserDao { + @Autowired private UserRepository userRepository; - + @Autowired + private AuthenticationService authService; @Autowired private RoleService roleService; + @Autowired + private PasswordEncoder passwordEncoder; + + @Autowired + private RoleDao roleDao; + public UserResponseBean createUser(UserReq userReq) { + if (userRepository.existsByEmail(userReq.getEmail())) { + throw new CustomValidationException(Status.VALIDATION_ERROR, Translator.toLocale(GepafinConstant.EMAIL_ALREADY_EXISTS)); + } if (!userReq.getPassword().equals(userReq.getConfPassword())) { - throw new CustomValidationException(Status.VALIDATION_ERROR,Translator.toLocale(GepafinConstant.PASSWORD_DOESNT_MATCH)); + throw new CustomValidationException(Status.VALIDATION_ERROR, Translator.toLocale(GepafinConstant.PASSWORD_DOESNT_MATCH)); + } + if (userReq.getPassword().length() < 8) { + throw new CustomValidationException(Status.VALIDATION_ERROR, Translator.toLocale(GepafinConstant.PASSWORD_MIN_LEN)); } UserEntity userEntity = convertUserRequestToUserEntity(userReq); userEntity = userRepository.save(userEntity); @@ -53,15 +73,15 @@ public class UserDao { private UserEntity convertUserRequestToUserEntity(UserReq userReq) { UserEntity userEntity = new UserEntity(); - userEntity.setPassword(userReq.getPassword()); - userEntity.setEmail(userReq.getEmail()); - userEntity.setFirstName(userReq.getFirstName()); - userEntity.setStatus(userReq.getStatus()); - userEntity.setLastName(userReq.getLastName()); - userEntity.setOrganization(userReq.getOrganization()); - userEntity.setAddress(userReq.getAddress()); - userEntity.setPhoneNumber(userReq.getPhoneNumber()); - userEntity.setRoleEntity(roleService.getRoleById(userReq.getRoleId())); + userEntity.setPassword(passwordEncoder.encode(userReq.getPassword())); + userEntity.setEmail(userReq.getEmail()); + userEntity.setFirstName(userReq.getFirstName()); + userEntity.setStatus(userReq.getStatus()); + userEntity.setLastName(userReq.getLastName()); + userEntity.setOrganization(userReq.getOrganization()); + userEntity.setAddress(userReq.getAddress()); + userEntity.setPhoneNumber(userReq.getPhoneNumber()); + userEntity.setRoleEntity(roleService.getRoleById(userReq.getRoleId())); return userEntity; } @@ -80,7 +100,8 @@ public class UserDao { userResponseBean.setCity(userEntity.getCity()); userResponseBean.setCountry(userEntity.getCountry()); userResponseBean.setStatus(userEntity.getStatus()); - userResponseBean.setRole(userEntity.getRoleEntity()); + RoleResponseBean roleResponseBean = roleDao.convertRoleEntityToRoleResponse(userEntity.getRoleEntity()); + userResponseBean.setRole(roleResponseBean); userResponseBean.setLastLogin(userEntity.getLastLogin()); return userResponseBean; } @@ -96,4 +117,9 @@ public class UserDao { .orElseThrow(() -> new ResourceNotFoundException(Status.NOT_FOUND,Translator.toLocale(GepafinConstant.USER_NOT_FOUND_MSG))); userRepository.deleteById(id); } + + + public JWTToken login(LoginReq loginReq) { + return authService.login(loginReq); + } } diff --git a/src/main/java/net/gepafin/tendermanagement/model/BaseBean.java b/src/main/java/net/gepafin/tendermanagement/model/BaseBean.java new file mode 100644 index 00000000..49eb0d41 --- /dev/null +++ b/src/main/java/net/gepafin/tendermanagement/model/BaseBean.java @@ -0,0 +1,16 @@ +package net.gepafin.tendermanagement.model; + +import java.time.LocalDateTime; + +import lombok.Data; + +@Data +public class BaseBean { + + private Long id; + + private LocalDateTime createdDate; + + private LocalDateTime updatedDate; + +} diff --git a/src/main/java/net/gepafin/tendermanagement/model/request/LoginReq.java b/src/main/java/net/gepafin/tendermanagement/model/request/LoginReq.java new file mode 100644 index 00000000..d337d231 --- /dev/null +++ b/src/main/java/net/gepafin/tendermanagement/model/request/LoginReq.java @@ -0,0 +1,18 @@ +package net.gepafin.tendermanagement.model.request; + +import jakarta.validation.constraints.Email; +import jakarta.validation.constraints.NotBlank; +import jakarta.validation.constraints.NotEmpty; +import lombok.Getter; +import lombok.Setter; + +@Getter +@Setter +public class LoginReq { + @NotBlank(message = "{email.not.blank}") + @Email(message = "{email.invalid}") + private String email; + @NotEmpty + private String password; + private Boolean rememberMe; +} diff --git a/src/main/java/net/gepafin/tendermanagement/model/request/UserReq.java b/src/main/java/net/gepafin/tendermanagement/model/request/UserReq.java index f866ac99..d7d7041b 100644 --- a/src/main/java/net/gepafin/tendermanagement/model/request/UserReq.java +++ b/src/main/java/net/gepafin/tendermanagement/model/request/UserReq.java @@ -3,6 +3,7 @@ package net.gepafin.tendermanagement.model.request; import com.fasterxml.jackson.annotation.JsonProperty; import com.fasterxml.jackson.annotation.JsonValue; import jakarta.validation.constraints.Email; +import jakarta.validation.constraints.NotBlank; import jakarta.validation.constraints.NotEmpty; import jakarta.validation.constraints.NotNull; import lombok.Getter; @@ -14,19 +15,20 @@ import java.time.LocalDateTime; @Setter public class UserReq { - - private String password; - - private String confPassword; - + @NotBlank(message = "{email.not.blank}") + @Email(message = "{email.invalid}") private String email; + @NotEmpty + private String password; + @NotEmpty + private String confPassword; private String firstName; private String lastName; private String phoneNumber; - + @NotEmpty private Long roleId; private String organization; @@ -39,6 +41,4 @@ public class UserReq { private String status; - private LocalDateTime lastLogin; - } diff --git a/src/main/java/net/gepafin/tendermanagement/model/response/LoginResponse.java b/src/main/java/net/gepafin/tendermanagement/model/response/LoginResponse.java new file mode 100644 index 00000000..64af6b1a --- /dev/null +++ b/src/main/java/net/gepafin/tendermanagement/model/response/LoginResponse.java @@ -0,0 +1,42 @@ +package net.gepafin.tendermanagement.model.response; + +import lombok.AllArgsConstructor; +import lombok.Getter; +import lombok.NoArgsConstructor; +import lombok.Setter; + +import java.time.LocalDateTime; + +@Getter +@Setter +@AllArgsConstructor +@NoArgsConstructor +public class LoginResponse { + private Long id; + + private String email; + + private String firstName; + + private String lastName; + + private RoleResponseBean role; + + private String phoneNumber; + + private String organization; + + private String address; + + private String city; + + private String country; + + private String status; + + private LocalDateTime lastLogin; + + private LocalDateTime createdDate; + + private LocalDateTime updatedDate; +} diff --git a/src/main/java/net/gepafin/tendermanagement/model/response/RegionResponseBean.java b/src/main/java/net/gepafin/tendermanagement/model/response/RegionResponseBean.java index c16d4c94..55182822 100644 --- a/src/main/java/net/gepafin/tendermanagement/model/response/RegionResponseBean.java +++ b/src/main/java/net/gepafin/tendermanagement/model/response/RegionResponseBean.java @@ -2,14 +2,14 @@ package net.gepafin.tendermanagement.model.response; import lombok.Getter; import lombok.Setter; +import net.gepafin.tendermanagement.model.BaseBean; import java.math.BigDecimal; import java.time.LocalDateTime; @Getter @Setter -public class RegionResponseBean { - private Long id; +public class RegionResponseBean extends BaseBean { private String regionName; private String description; private String country; @@ -23,6 +23,4 @@ public class RegionResponseBean { private BigDecimal educationLevel; private BigDecimal healthcareAccess; private BigDecimal environmentalScore; - private LocalDateTime createdDate; - private LocalDateTime updatedDate; } diff --git a/src/main/java/net/gepafin/tendermanagement/model/response/RoleResponseBean.java b/src/main/java/net/gepafin/tendermanagement/model/response/RoleResponseBean.java index 1e708b58..d5d579e5 100644 --- a/src/main/java/net/gepafin/tendermanagement/model/response/RoleResponseBean.java +++ b/src/main/java/net/gepafin/tendermanagement/model/response/RoleResponseBean.java @@ -4,16 +4,14 @@ import lombok.Data; import lombok.Getter; import lombok.Setter; import net.gepafin.tendermanagement.entities.RegionEntity; +import net.gepafin.tendermanagement.model.BaseBean; import java.time.LocalDateTime; @Data -public class RoleResponseBean { - private Long id; +public class RoleResponseBean extends BaseBean { private String roleName; private String description; - private LocalDateTime createdDate; - private LocalDateTime updatedDate; private String permissions; - private RegionEntity region; + private RegionResponseBean region; } diff --git a/src/main/java/net/gepafin/tendermanagement/model/response/UserResponseBean.java b/src/main/java/net/gepafin/tendermanagement/model/response/UserResponseBean.java index 2c036804..1dcaa944 100644 --- a/src/main/java/net/gepafin/tendermanagement/model/response/UserResponseBean.java +++ b/src/main/java/net/gepafin/tendermanagement/model/response/UserResponseBean.java @@ -6,22 +6,23 @@ import jakarta.validation.constraints.NotNull; import lombok.Getter; import lombok.Setter; import net.gepafin.tendermanagement.entities.RoleEntity; +import net.gepafin.tendermanagement.model.BaseBean; import java.time.LocalDateTime; @Getter @Setter -public class UserResponseBean { - private Long id; +public class UserResponseBean extends BaseBean { + private String email; private String firstName; private String lastName; - private String phoneNumber; + private RoleResponseBean role; - private RoleEntity role; + private String phoneNumber; private String organization; @@ -35,7 +36,4 @@ public class UserResponseBean { private LocalDateTime lastLogin; - private LocalDateTime createdDate; - - private LocalDateTime updatedDate; } diff --git a/src/main/java/net/gepafin/tendermanagement/model/util/JWTToken.java b/src/main/java/net/gepafin/tendermanagement/model/util/JWTToken.java new file mode 100644 index 00000000..53596d25 --- /dev/null +++ b/src/main/java/net/gepafin/tendermanagement/model/util/JWTToken.java @@ -0,0 +1,26 @@ +package net.gepafin.tendermanagement.model.util; + +import com.fasterxml.jackson.annotation.JsonProperty; +import lombok.Data; +import lombok.Getter; +import lombok.Setter; +import net.gepafin.tendermanagement.model.response.LoginResponse; +import net.gepafin.tendermanagement.model.response.UserResponseBean; + +/** + * JWTToken + */ +@Data +public class JWTToken { + @JsonProperty("token") + private String token; + + @JsonProperty("user") + private LoginResponse loginResponse; + public JWTToken(String token, LoginResponse loginResponse) { + this.token = token; + this.loginResponse = loginResponse; + } + +} + diff --git a/src/main/java/net/gepafin/tendermanagement/repositories/UserRepository.java b/src/main/java/net/gepafin/tendermanagement/repositories/UserRepository.java index 2fdee718..dc0aad37 100644 --- a/src/main/java/net/gepafin/tendermanagement/repositories/UserRepository.java +++ b/src/main/java/net/gepafin/tendermanagement/repositories/UserRepository.java @@ -6,4 +6,6 @@ import org.springframework.data.jpa.repository.JpaRepository; import java.util.Optional; public interface UserRepository extends JpaRepository { + UserEntity findByEmail(String email); + boolean existsByEmail(String email); } diff --git a/src/main/java/net/gepafin/tendermanagement/service/UserService.java b/src/main/java/net/gepafin/tendermanagement/service/UserService.java index 71faf129..68912240 100644 --- a/src/main/java/net/gepafin/tendermanagement/service/UserService.java +++ b/src/main/java/net/gepafin/tendermanagement/service/UserService.java @@ -1,12 +1,19 @@ package net.gepafin.tendermanagement.service; +import net.gepafin.tendermanagement.model.request.LoginReq; import net.gepafin.tendermanagement.model.request.UpdateUserReq; import net.gepafin.tendermanagement.model.request.UserReq; import net.gepafin.tendermanagement.model.response.UserResponseBean; +import net.gepafin.tendermanagement.model.util.JWTToken; public interface UserService { UserResponseBean createUser(UserReq userReq); + UserResponseBean updateUser(Long userId, UpdateUserReq userReq); + UserResponseBean getUserById(Long userId); + void deleteUser(Long userId); + + JWTToken login(LoginReq loginReq); } diff --git a/src/main/java/net/gepafin/tendermanagement/service/impl/AuthenticationService.java b/src/main/java/net/gepafin/tendermanagement/service/impl/AuthenticationService.java new file mode 100644 index 00000000..1b5137ce --- /dev/null +++ b/src/main/java/net/gepafin/tendermanagement/service/impl/AuthenticationService.java @@ -0,0 +1,60 @@ +package net.gepafin.tendermanagement.service.impl; + +import net.gepafin.tendermanagement.config.Translator; +import net.gepafin.tendermanagement.config.jwt.TokenProvider; +import net.gepafin.tendermanagement.constants.GepafinConstant; +import net.gepafin.tendermanagement.dao.RoleDao; +import net.gepafin.tendermanagement.entities.UserEntity; +import net.gepafin.tendermanagement.model.request.LoginReq; +import net.gepafin.tendermanagement.model.response.LoginResponse; +import net.gepafin.tendermanagement.model.response.RoleResponseBean; +import net.gepafin.tendermanagement.model.util.JWTToken; +import net.gepafin.tendermanagement.repositories.UserRepository; +import net.gepafin.tendermanagement.web.rest.api.errors.CustomValidationException; +import net.gepafin.tendermanagement.web.rest.api.errors.Status; +import org.springframework.beans.factory.annotation.Autowired; +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.security.crypto.password.PasswordEncoder; +import org.springframework.stereotype.Service; + +import java.util.Collections; + +@Service +public class AuthenticationService { + + private final TokenProvider tokenProvider; + private final AuthenticationManager authenticationManager; + + @Autowired + private UserRepository userRepository; + @Autowired + private PasswordEncoder passwordEncoder; + @Autowired + private RoleDao roleDao; + + @Autowired + public AuthenticationService(TokenProvider tokenProvider, AuthenticationManager authenticationManager) { + this.tokenProvider = tokenProvider; + this.authenticationManager = authenticationManager; + } + + public JWTToken login(LoginReq loginReq) { + UsernamePasswordAuthenticationToken authenticationToken = new UsernamePasswordAuthenticationToken(loginReq.getEmail(),loginReq.getPassword()); + Authentication authentication = this.authenticationManager.authenticate(authenticationToken); + SecurityContextHolder.getContext().setAuthentication(authentication); + String token = tokenProvider.createToken(authentication,loginReq.getRememberMe()); + UserEntity user = userRepository.findByEmail(loginReq.getEmail()); + if (user == null) { + throw new CustomValidationException(Status.NOT_FOUND, Translator.toLocale(GepafinConstant.USER_NOT_FOUND_MSG)); + } + RoleResponseBean roleResponseBean = roleDao.convertRoleEntityToRoleResponse(user.getRoleEntity()); + return new JWTToken(token, new LoginResponse(user.getId(),user.getEmail(), + user.getFirstName(),user.getLastName(),roleResponseBean, user.getPhoneNumber(), user.getAddress(), user.getOrganization(), user.getCountry(),user.getStatus() + ,user.getCity(),user.getLastLogin(),user.getCreatedDate(),user.getUpdatedDate())); + } + } + + diff --git a/src/main/java/net/gepafin/tendermanagement/service/impl/RegionServiceImpl.java b/src/main/java/net/gepafin/tendermanagement/service/impl/RegionServiceImpl.java index 63921ef1..345362d3 100644 --- a/src/main/java/net/gepafin/tendermanagement/service/impl/RegionServiceImpl.java +++ b/src/main/java/net/gepafin/tendermanagement/service/impl/RegionServiceImpl.java @@ -10,6 +10,8 @@ import net.gepafin.tendermanagement.model.request.UpdateRegionReq; import net.gepafin.tendermanagement.service.RegionService; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + @Service public class RegionServiceImpl implements RegionService { @@ -17,26 +19,31 @@ public class RegionServiceImpl implements RegionService { private RegionDao regionDao; @Override + @Transactional(rollbackFor = Exception.class) public RegionResponseBean createRegion(RegionReq regionReq) { return regionDao.createRegion(regionReq); } @Override + @Transactional(rollbackFor = Exception.class) public RegionResponseBean updateRegion(Long regionId, RegionReq regionReq) { return regionDao.updateRegion(regionId,regionReq); } @Override + @Transactional(readOnly = true) public RegionEntity getRegionById(Long regionId) { return regionDao.getRegionById(regionId); } @Override + @Transactional(rollbackFor = Exception.class) public void deleteRegion(Long regionId) { regionDao.deleteById(regionId); } @Override + @Transactional(readOnly = true) public List getAllRegions() { return regionDao.getAllRegions(); } diff --git a/src/main/java/net/gepafin/tendermanagement/service/impl/RoleServiceImpl.java b/src/main/java/net/gepafin/tendermanagement/service/impl/RoleServiceImpl.java index 41f3f6ba..9f66415a 100644 --- a/src/main/java/net/gepafin/tendermanagement/service/impl/RoleServiceImpl.java +++ b/src/main/java/net/gepafin/tendermanagement/service/impl/RoleServiceImpl.java @@ -9,6 +9,7 @@ import net.gepafin.tendermanagement.model.response.RoleResponseBean; import net.gepafin.tendermanagement.service.RoleService; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; @Service @@ -18,25 +19,30 @@ public class RoleServiceImpl implements RoleService { private RoleDao roleDao; @Override + @Transactional(rollbackFor = Exception.class) public RoleResponseBean createRole(RoleReq roleReq) { return roleDao.createRole(roleReq); } @Override + @Transactional(rollbackFor = Exception.class) public RoleResponseBean updateRole(Long roleId, RoleReq roleReq) { return roleDao.updateRole(roleId,roleReq); } @Override + @Transactional(readOnly = true) public RoleEntity getRoleById(Long roleId) { return roleDao.getRoleById(roleId); } @Override + @Transactional(rollbackFor = Exception.class) public void deleteRole(Long roleId) { roleDao.deleteById(roleId); } @Override + @Transactional(readOnly = true) public List getAllRoles() { return roleDao.getAllRoles(); diff --git a/src/main/java/net/gepafin/tendermanagement/service/impl/UserServiceImpl.java b/src/main/java/net/gepafin/tendermanagement/service/impl/UserServiceImpl.java index 4d39ffc5..c6205aae 100644 --- a/src/main/java/net/gepafin/tendermanagement/service/impl/UserServiceImpl.java +++ b/src/main/java/net/gepafin/tendermanagement/service/impl/UserServiceImpl.java @@ -1,12 +1,16 @@ package net.gepafin.tendermanagement.service.impl; import net.gepafin.tendermanagement.dao.UserDao; +import net.gepafin.tendermanagement.model.request.LoginReq; import net.gepafin.tendermanagement.model.request.UpdateUserReq; import net.gepafin.tendermanagement.model.request.UserReq; +import net.gepafin.tendermanagement.model.response.LoginResponse; import net.gepafin.tendermanagement.model.response.UserResponseBean; +import net.gepafin.tendermanagement.model.util.JWTToken; import net.gepafin.tendermanagement.service.UserService; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; import java.time.format.DateTimeFormatter; @@ -15,24 +19,35 @@ public class UserServiceImpl implements UserService { @Autowired private UserDao userDao; + + @Transactional(rollbackFor = Exception.class) public UserResponseBean createUser(UserReq userReq) { return userDao.createUser(userReq); } @Override + @Transactional(rollbackFor = Exception.class) public UserResponseBean updateUser(Long userId, UpdateUserReq userReq) { return userDao.updateUser(userId, userReq); } @Override + @Transactional(readOnly = true) public UserResponseBean getUserById(Long userId) { return userDao.getUserById(userId); } @Override + @Transactional(rollbackFor = Exception.class) public void deleteUser(Long userId) { userDao.deleteUser(userId); } -} + @Override + public JWTToken login(LoginReq loginReq) { + return userDao.login(loginReq); + + } + +} \ No newline at end of file diff --git a/src/main/java/net/gepafin/tendermanagement/web/rest/api/UserApi.java b/src/main/java/net/gepafin/tendermanagement/web/rest/api/UserApi.java index 3c9eadc6..beae1296 100644 --- a/src/main/java/net/gepafin/tendermanagement/web/rest/api/UserApi.java +++ b/src/main/java/net/gepafin/tendermanagement/web/rest/api/UserApi.java @@ -6,9 +6,11 @@ import io.swagger.v3.oas.annotations.media.Content; import io.swagger.v3.oas.annotations.media.ExampleObject; import io.swagger.v3.oas.annotations.responses.ApiResponse; import jakarta.validation.Valid; +import net.gepafin.tendermanagement.model.request.LoginReq; import net.gepafin.tendermanagement.model.request.UpdateUserReq; import net.gepafin.tendermanagement.model.request.UserReq; import net.gepafin.tendermanagement.model.response.UserResponseBean; +import net.gepafin.tendermanagement.model.util.JWTToken; import net.gepafin.tendermanagement.model.util.Response; import net.gepafin.tendermanagement.web.rest.api.errors.ErrorConstants; import org.springframework.http.HttpStatus; @@ -28,13 +30,13 @@ public interface UserApi { responses = { @ApiResponse(responseCode = "200", description = "OK"), @ApiResponse(responseCode = "404", description = "Not Found", content = @Content(mediaType = MediaType.APPLICATION_JSON_VALUE, examples = { - @ExampleObject(value = ErrorConstants.NOTFOUND_ERROR_EXAMPLE) })), + @ExampleObject(value = ErrorConstants.NOTFOUND_ERROR_EXAMPLE)})), @ApiResponse(responseCode = "401", description = "Unauthorized", content = @Content(mediaType = MediaType.APPLICATION_JSON_VALUE, examples = { - @ExampleObject(value = ErrorConstants.UNAUTHORIZED_ERROR_EXAMPLE) })), + @ExampleObject(value = ErrorConstants.UNAUTHORIZED_ERROR_EXAMPLE)})), @ApiResponse(responseCode = "400", description = "Bad Request", content = @Content(mediaType = MediaType.APPLICATION_JSON_VALUE, examples = { - @ExampleObject(value = ErrorConstants.BADREQUEST_ERROR_EXAMPLE) })) }) + @ExampleObject(value = ErrorConstants.BADREQUEST_ERROR_EXAMPLE)}))}) @RequestMapping(value = "", - produces = { "application/json" }, + produces = {"application/json"}, method = RequestMethod.POST) default ResponseEntity> createUser( @Parameter(description = "User request object", required = true) @Valid @RequestBody UserReq userReq) { @@ -45,13 +47,13 @@ public interface UserApi { responses = { @ApiResponse(responseCode = "200", description = "OK"), @ApiResponse(responseCode = "404", description = "Not Found", content = @Content(mediaType = MediaType.APPLICATION_JSON_VALUE, examples = { - @ExampleObject(value = ErrorConstants.NOTFOUND_ERROR_EXAMPLE) })), + @ExampleObject(value = ErrorConstants.NOTFOUND_ERROR_EXAMPLE)})), @ApiResponse(responseCode = "401", description = "Unauthorized", content = @Content(mediaType = MediaType.APPLICATION_JSON_VALUE, examples = { - @ExampleObject(value = ErrorConstants.UNAUTHORIZED_ERROR_EXAMPLE) })), + @ExampleObject(value = ErrorConstants.UNAUTHORIZED_ERROR_EXAMPLE)})), @ApiResponse(responseCode = "400", description = "Bad Request", content = @Content(mediaType = MediaType.APPLICATION_JSON_VALUE, examples = { - @ExampleObject(value = ErrorConstants.BADREQUEST_ERROR_EXAMPLE) })) }) + @ExampleObject(value = ErrorConstants.BADREQUEST_ERROR_EXAMPLE)}))}) @RequestMapping(value = "/{userId}", - produces = { "application/json" }, + produces = {"application/json"}, method = RequestMethod.PUT) default ResponseEntity> updateUser( @Parameter(description = "The user id", required = true) @PathVariable("userId") Long userId, @@ -63,13 +65,13 @@ public interface UserApi { responses = { @ApiResponse(responseCode = "200", description = "OK"), @ApiResponse(responseCode = "404", description = "Not Found", content = @Content(mediaType = MediaType.APPLICATION_JSON_VALUE, examples = { - @ExampleObject(value = ErrorConstants.NOTFOUND_ERROR_EXAMPLE) })), + @ExampleObject(value = ErrorConstants.NOTFOUND_ERROR_EXAMPLE)})), @ApiResponse(responseCode = "401", description = "Unauthorized", content = @Content(mediaType = MediaType.APPLICATION_JSON_VALUE, examples = { - @ExampleObject(value = ErrorConstants.UNAUTHORIZED_ERROR_EXAMPLE) })), + @ExampleObject(value = ErrorConstants.UNAUTHORIZED_ERROR_EXAMPLE)})), @ApiResponse(responseCode = "400", description = "Bad Request", content = @Content(mediaType = MediaType.APPLICATION_JSON_VALUE, examples = { - @ExampleObject(value = ErrorConstants.BADREQUEST_ERROR_EXAMPLE) })) }) + @ExampleObject(value = ErrorConstants.BADREQUEST_ERROR_EXAMPLE)}))}) @RequestMapping(value = "/{userId}", - produces = { "application/json" }, + produces = {"application/json"}, method = RequestMethod.GET) default ResponseEntity> getUserById( @Parameter(description = "The user id", required = true) @PathVariable("userId") Long userId) { @@ -80,15 +82,26 @@ public interface UserApi { responses = { @ApiResponse(responseCode = "200", description = "OK"), @ApiResponse(responseCode = "404", description = "Not Found", content = @Content(mediaType = MediaType.APPLICATION_JSON_VALUE, examples = { - @ExampleObject(value = ErrorConstants.NOTFOUND_ERROR_EXAMPLE) })), + @ExampleObject(value = ErrorConstants.NOTFOUND_ERROR_EXAMPLE)})), @ApiResponse(responseCode = "401", description = "Unauthorized", content = @Content(mediaType = MediaType.APPLICATION_JSON_VALUE, examples = { - @ExampleObject(value = ErrorConstants.UNAUTHORIZED_ERROR_EXAMPLE) })), + @ExampleObject(value = ErrorConstants.UNAUTHORIZED_ERROR_EXAMPLE)})), @ApiResponse(responseCode = "400", description = "Bad Request", content = @Content(mediaType = MediaType.APPLICATION_JSON_VALUE, examples = { - @ExampleObject(value = ErrorConstants.BADREQUEST_ERROR_EXAMPLE) })) }) + @ExampleObject(value = ErrorConstants.BADREQUEST_ERROR_EXAMPLE)}))}) @RequestMapping(value = "/{userId}", method = RequestMethod.DELETE) default ResponseEntity> deleteUser( @Parameter(description = "The user id", required = true) @PathVariable("userId") Long userId) { return new ResponseEntity<>(HttpStatus.NOT_IMPLEMENTED); } + + @Operation(summary = "Api to login user", + responses = { + @ApiResponse(responseCode = "200", description = "OK"), + @ApiResponse(responseCode = "400", description = "Bad Request"), + @ApiResponse(responseCode = "401", description = "Unauthorized")}) + @RequestMapping(value = "/login", + produces = {"application/json"}, + method = RequestMethod.POST) + ResponseEntity> login( + @Parameter(description = "Login request object", required = true) @Valid @RequestBody LoginReq loginReq); } diff --git a/src/main/java/net/gepafin/tendermanagement/web/rest/api/errors/GlobalExceptionHandler.java b/src/main/java/net/gepafin/tendermanagement/web/rest/api/errors/GlobalExceptionHandler.java new file mode 100644 index 00000000..6a6b00ef --- /dev/null +++ b/src/main/java/net/gepafin/tendermanagement/web/rest/api/errors/GlobalExceptionHandler.java @@ -0,0 +1,23 @@ +package net.gepafin.tendermanagement.web.rest.api.errors; + +import net.gepafin.tendermanagement.model.util.Response; +import org.springframework.http.HttpStatus; +import org.springframework.web.bind.annotation.ControllerAdvice; +import org.springframework.web.bind.annotation.ExceptionHandler; +import org.springframework.http.ResponseEntity; + +@ControllerAdvice +public class GlobalExceptionHandler { + + @ExceptionHandler(CustomValidationException.class) + public ResponseEntity> handleCustomValidationException(CustomValidationException ex) { + return ResponseEntity.status(HttpStatus.BAD_REQUEST) + .body(new Response<>(null, ex.getStatus(), ex.getMessage())); + } + + @ExceptionHandler(ResourceNotFoundException.class) + public ResponseEntity> handleResourceNotFoundException(ResourceNotFoundException ex) { + return ResponseEntity.status(HttpStatus.NOT_FOUND) + .body(new Response<>(null, ex.getStatus(), ex.getMessage())); + } +} diff --git a/src/main/java/net/gepafin/tendermanagement/web/rest/api/impl/CustomUserDetailsService.java b/src/main/java/net/gepafin/tendermanagement/web/rest/api/impl/CustomUserDetailsService.java new file mode 100644 index 00000000..102a388c --- /dev/null +++ b/src/main/java/net/gepafin/tendermanagement/web/rest/api/impl/CustomUserDetailsService.java @@ -0,0 +1,55 @@ +package net.gepafin.tendermanagement.web.rest.api.impl; + +import net.gepafin.tendermanagement.entities.RoleEntity; +import net.gepafin.tendermanagement.entities.UserEntity; +import net.gepafin.tendermanagement.repositories.RoleRepository; +import net.gepafin.tendermanagement.repositories.UserRepository; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.security.core.GrantedAuthority; +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 org.springframework.transaction.annotation.Transactional; + +import java.util.Collections; + +@Service +public class CustomUserDetailsService implements UserDetailsService { + + private final Logger log = LoggerFactory.getLogger(CustomUserDetailsService.class); + + private final UserRepository userRepository; + private final RoleRepository roleRepository; + + public CustomUserDetailsService(UserRepository userRepository, RoleRepository roleRepository) { + this.userRepository = userRepository; + this.roleRepository = roleRepository; + } + + @Override + @Transactional + public UserDetails loadUserByUsername(final String email) throws UsernameNotFoundException { + log.debug("Authenticating {}", email); + + UserEntity user = userRepository.findByEmail(email); + if (user == null) { + throw new UsernameNotFoundException("User " + email + " was not found in the database"); + } + + return createSpringSecurityUser(user); + } + + private org.springframework.security.core.userdetails.User createSpringSecurityUser(UserEntity user) { + RoleEntity role = user.getRoleEntity(); + GrantedAuthority grantedAuthority = new SimpleGrantedAuthority(role.getRoleName()); + + return new org.springframework.security.core.userdetails.User( + user.getEmail(), + user.getPassword(), + Collections.singletonList(grantedAuthority) + ); + } +} diff --git a/src/main/java/net/gepafin/tendermanagement/web/rest/api/impl/UserApiController.java b/src/main/java/net/gepafin/tendermanagement/web/rest/api/impl/UserApiController.java index ad169e42..caf3c61a 100644 --- a/src/main/java/net/gepafin/tendermanagement/web/rest/api/impl/UserApiController.java +++ b/src/main/java/net/gepafin/tendermanagement/web/rest/api/impl/UserApiController.java @@ -3,14 +3,16 @@ package net.gepafin.tendermanagement.web.rest.api.impl; import jakarta.validation.Valid; import net.gepafin.tendermanagement.config.Translator; import net.gepafin.tendermanagement.constants.GepafinConstant; +import net.gepafin.tendermanagement.model.request.LoginReq; import net.gepafin.tendermanagement.model.request.UpdateUserReq; import net.gepafin.tendermanagement.model.request.UserReq; import net.gepafin.tendermanagement.model.response.UserResponseBean; +import net.gepafin.tendermanagement.model.util.JWTToken; import net.gepafin.tendermanagement.model.util.Response; import net.gepafin.tendermanagement.service.UserService; import net.gepafin.tendermanagement.web.rest.api.UserApi; -import net.gepafin.tendermanagement.web.rest.api.errors.Status; +import net.gepafin.tendermanagement.web.rest.api.errors.Status; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.beans.factory.annotation.Autowired; @@ -21,6 +23,7 @@ import org.springframework.web.bind.annotation.*; @RestController @RequestMapping("${openapi.gepafin.base-path:/v1/user}") +@Validated public class UserApiController implements UserApi { private final Logger log = LoggerFactory.getLogger(UserApiController.class); @@ -31,42 +34,45 @@ public class UserApiController implements UserApi { @Override public ResponseEntity> createUser( @Valid @RequestBody UserReq userReq) { - log.info("Create User with - Request Body: {}", userReq); - UserResponseBean createdUser = userService.createUser(userReq); - return ResponseEntity.status(HttpStatus.CREATED) - .body(new Response<>(createdUser, Status.SUCCESS, Translator.toLocale(GepafinConstant.USER_CREATED_SUCCESS_MSG))); + log.info("Create User with - Request Body: {}", userReq); + UserResponseBean createdUser = userService.createUser(userReq); + return ResponseEntity.status(HttpStatus.CREATED) + .body(new Response<>(createdUser, Status.SUCCESS, Translator.toLocale(GepafinConstant.USER_CREATED_SUCCESS_MSG))); } @Override public ResponseEntity> updateUser( @PathVariable("userId") Long userId, @Valid @RequestBody UpdateUserReq userReq) { - log.info("Update User - User ID: {}, Request Body: {}", userId, userReq); - UserResponseBean updatedUser = userService.updateUser(userId, userReq); - - return ResponseEntity.status(HttpStatus.OK) - .body(new Response<>(updatedUser, Status.SUCCESS, Translator.toLocale(GepafinConstant.USER_UPDATED_SUCCESS_MSG))); - + log.info("Update User - User ID: {}, Request Body: {}", userId, userReq); + UserResponseBean updatedUser = userService.updateUser(userId, userReq); + return ResponseEntity.status(HttpStatus.OK) + .body(new Response<>(updatedUser, Status.SUCCESS, Translator.toLocale(GepafinConstant.USER_UPDATED_SUCCESS_MSG))); } @Override public ResponseEntity> getUserById( @PathVariable("userId") Long userId) { - log.info("Get User by ID - User ID: {}", userId); - UserResponseBean user = userService.getUserById(userId); - return ResponseEntity.status(HttpStatus.OK) - .body(new Response<>(user, Status.SUCCESS, Translator.toLocale(GepafinConstant.GET_USER_SUCCESS_MSG))); - + log.info("Get User by ID - User ID: {}", userId); + UserResponseBean user = userService.getUserById(userId); + return ResponseEntity.status(HttpStatus.OK) + .body(new Response<>(user, Status.SUCCESS, Translator.toLocale(GepafinConstant.GET_USER_SUCCESS_MSG))); } @Override public ResponseEntity> deleteUser( @PathVariable("userId") Long userId) { - log.info("Delete User By- User ID: {}", userId); - userService.deleteUser(userId); - - return ResponseEntity.status(HttpStatus.OK) - .body(new Response<>(null, Status.SUCCESS, Translator.toLocale(GepafinConstant.USER_DELETED_SUCCESS_MSG))); + log.info("Delete User - User ID: {}", userId); + userService.deleteUser(userId); + return ResponseEntity.status(HttpStatus.OK) + .body(new Response<>(null, Status.SUCCESS, Translator.toLocale(GepafinConstant.USER_DELETED_SUCCESS_MSG))); + } + @Override + public ResponseEntity> login( + @Valid @RequestBody LoginReq loginReq) { + log.info("User login attempt "); + JWTToken jwtToken = userService.login(loginReq); + return ResponseEntity.ok(new Response<>(jwtToken, Status.SUCCESS, Translator.toLocale(GepafinConstant.LOGIN_SUCCESS_MSG))); } } diff --git a/src/main/resources/application.properties b/src/main/resources/application.properties index 9126bd0a..54a465f5 100644 --- a/src/main/resources/application.properties +++ b/src/main/resources/application.properties @@ -23,4 +23,13 @@ spring.liquibase.enabled=true # Debugging and SQL Output logging.level.org.springframework.boot.autoconfigure.liquibase=ERROR -logging.level.liquibase=ERROR \ No newline at end of file +logging.level.liquibase=ERROR +# JWT configuration +# Ensure these values match your expectations +security.authentication.jwt.secret=my-secret-token-to-change-in-prod-environment-your-super-secure-randomly-generated-key +security.authentication.jwt.token-validity-in-seconds=86400 + + +spring.main.allow-circular-references=true + + diff --git a/src/main/resources/message_en.properties b/src/main/resources/message_en.properties index bdbcf92b..3149b6fc 100644 --- a/src/main/resources/message_en.properties +++ b/src/main/resources/message_en.properties @@ -27,3 +27,10 @@ create.regiom.error=Error occurred while creating the region. update.region.error=Error occurred while updating the region. password.doesnt.match=Password and confirm password do not match. +login.successfully=Login successfully. +pass.min.len.msg=Password must be at least 8 characters long. +email.already.exists=A user with this email already exists. + + + + diff --git a/src/main/resources/message_it.properties b/src/main/resources/message_it.properties index bfd0d8f3..d6b29447 100644 --- a/src/main/resources/message_it.properties +++ b/src/main/resources/message_it.properties @@ -26,3 +26,10 @@ user.region.not.found=Regione non trovata. create.regiom.error=Errore durante la creazione della regione. update.region.error=Errore durante l'aggiornamento della regione. password.doesnt.match=La password e la conferma della password non corrispondono. + +# Login-related messages +login.successfully=Accesso effettuato con successo. +pass.min.len.msg=La password deve essere lunga almeno 8 caratteri. +email.already.exists=Esiste già un utente con questa email. + +