Skip to content

Commit

Permalink
Merge pull request #3 from musabbozkurt/refactor
Browse files Browse the repository at this point in the history
refactor
  • Loading branch information
musabbozkurt authored Jul 20, 2024
2 parents 114b68d + 46ec2f4 commit f664b97
Show file tree
Hide file tree
Showing 19 changed files with 438 additions and 31 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
package com.mb.inventorymanagementservice.common.context;

import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;

import java.time.ZoneOffset;
import java.util.Locale;

@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class Context {

private String username;
private String email;
private String mobileNumber;
private Locale language;
private String ipAddress;
private String userAgent;
private boolean admin;
private ZoneOffset preferredZoneOffset;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,139 @@
package com.mb.inventorymanagementservice.common.context;

import com.fasterxml.jackson.databind.ObjectMapper;
import com.mb.inventorymanagementservice.exception.BaseException;
import com.mb.inventorymanagementservice.exception.InvalidParameterException;
import com.mb.inventorymanagementservice.exception.LocalizedException;
import com.mb.inventorymanagementservice.utils.Constants;
import jakarta.servlet.FilterChain;
import jakarta.servlet.ServletException;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils;
import org.jboss.logging.MDC;
import org.springframework.core.annotation.Order;
import org.springframework.http.MediaType;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.stereotype.Component;
import org.springframework.web.filter.OncePerRequestFilter;

import java.io.IOException;
import java.time.Instant;
import java.time.ZoneOffset;
import java.util.Collection;
import java.util.Locale;
import java.util.Objects;
import java.util.TimeZone;

@Slf4j
@Order(1)
@Component
@RequiredArgsConstructor
public class ContextFilter extends OncePerRequestFilter {

public static final String ACCEPT_LANGUAGE_HEADER = "Accept-Language";
public static final String FORWARDED_FOR_HEADER = "x-forwarded-for";
public static final String USER_AGENT_HEADER = "user-agent";
public static final String ZONE_ID_HEADER = "X-ZONE-ID";
public static final String ADMIN_PERMISSION = "ADMIN";

private final ObjectMapper objectMapper;

@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
try {
Context context = ContextHolder.get();
populateHeaderDetails(context, request);
populateSecurityContext(context);
ContextHolder.set(context);
populateTracing();
filterChain.doFilter(request, response);
} catch (BaseException e) {
response.setContentType(MediaType.APPLICATION_JSON_VALUE);
response.setStatus(e.getErrorCode().getHttpStatus().value());
response.getWriter().write(objectMapper.writeValueAsString(new LocalizedException(e.getErrorCode().getCode())));
response.getWriter().flush();
response.getWriter().close();
} finally {
ContextHolder.clear();
MDC.clear();
}
}

private void populateHeaderDetails(Context context, HttpServletRequest httpServletRequest) {
Locale language = getLanguageHeader(httpServletRequest);
context.setLanguage(language);

context.setIpAddress(getIpAddress(httpServletRequest));
context.setUserAgent(getHeader(httpServletRequest, USER_AGENT_HEADER));

String zoneId = getHeader(httpServletRequest, ZONE_ID_HEADER);
context.setPreferredZoneOffset(zoneId == null ? ZoneOffset.UTC : TimeZone.getTimeZone(zoneId).toZoneId().getRules().getOffset(Instant.now()));
}

private String getHeader(HttpServletRequest httpServletRequest, String headerName) {
return httpServletRequest.getHeader(headerName);
}

private Locale getLanguageHeader(HttpServletRequest request) {
String value = request.getHeader(ACCEPT_LANGUAGE_HEADER);
if (StringUtils.isNotBlank(value)) {
try {
return Locale.of(value);
} catch (Exception ex) {
throw new InvalidParameterException();
}
}
return Locale.of("EN");
}

private String getIpAddress(HttpServletRequest request) {
final String xForwardedForHeader = request.getHeader(FORWARDED_FOR_HEADER);
if (xForwardedForHeader == null) {
return request.getRemoteAddr();
}
final String[] tokenized = xForwardedForHeader.trim().split(",");
return tokenized.length == 0 ? null : tokenized[0].trim();
}

private void populateSecurityContext(Context context) {
Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
if (authentication instanceof UsernamePasswordAuthenticationToken authenticationToken) {
if (authenticationToken.getDetails() != null) {
UserDetails userDetails = (UserDetails) authentication.getPrincipal();
context.setAdmin(isAdmin(authentication.getAuthorities()));
context.setUsername(userDetails.getUsername());
}
}
}

private boolean isAdmin(Collection<? extends GrantedAuthority> authorities) {
return authorities.stream().map(GrantedAuthority::getAuthority).anyMatch(ADMIN_PERMISSION::equals);
}

private void populateTracing() {
if (Objects.nonNull(ContextHolder.get().getUsername())) {
MDC.put("userName", ContextHolder.get().getUsername());
}

if (Objects.nonNull(ContextHolder.get().getIpAddress())) {
MDC.put("ipAddress", ContextHolder.get().getIpAddress());
}
}

@Override
public void destroy() {
log.info("ContextFilter is destroyed.");
}

@Override
protected boolean shouldNotFilter(HttpServletRequest request) {
return Constants.isUriExcluded(request.getRequestURI());
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
package com.mb.inventorymanagementservice.common.context;

import lombok.AccessLevel;
import lombok.NoArgsConstructor;

import java.util.Objects;

@NoArgsConstructor(access = AccessLevel.PRIVATE)
public class ContextHolder {

private static final ThreadLocal<Context> THREAD_LOCAL = new InheritableThreadLocal<>();

public static Context get() {
return Objects.isNull(THREAD_LOCAL.get()) ? Context.builder().build() : THREAD_LOCAL.get();
}

public static void set(Context context) {
THREAD_LOCAL.set(context);
}

public static void clear() {
THREAD_LOCAL.remove();
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -7,20 +7,26 @@
import io.swagger.v3.core.util.Json;
import io.swagger.v3.oas.models.Components;
import io.swagger.v3.oas.models.OpenAPI;
import io.swagger.v3.oas.models.examples.Example;
import io.swagger.v3.oas.models.info.Contact;
import io.swagger.v3.oas.models.info.Info;
import io.swagger.v3.oas.models.media.NumberSchema;
import io.swagger.v3.oas.models.media.ObjectSchema;
import io.swagger.v3.oas.models.media.Schema;
import io.swagger.v3.oas.models.media.StringSchema;
import io.swagger.v3.oas.models.parameters.Parameter;
import io.swagger.v3.oas.models.security.SecurityRequirement;
import io.swagger.v3.oas.models.security.SecurityScheme;
import org.springdoc.core.customizers.OperationCustomizer;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

import javax.money.MonetaryAmount;
import java.util.Arrays;
import java.util.HashMap;
import java.util.Iterator;
import java.util.Locale;
import java.util.stream.Collectors;

@Configuration
public class OpenAPIConfig implements ModelConverter {
Expand All @@ -41,6 +47,25 @@ public OpenAPI openAPI() {
.addSecurityItem(new SecurityRequirement().addList("bearer-jwt", Arrays.asList("read", "write")));
}

@Bean
public OperationCustomizer operationCustomizer() {
HashMap<String, Example> examples = Arrays.stream(Locale.getISOLanguages())
.collect(Collectors.toMap(
languageCode -> languageCode, // key: language code
languageCode -> new Example().value(languageCode), // value: Example instance created with language code
(existing, replacement) -> replacement, // merge function (if needed, here we keep the replacement one)
HashMap::new // map supplier
));

Parameter parameter = new Parameter()
.in("header")
.description("Accept-Language")
.name("Accept-Language");
parameter.setExamples(examples);

return (operation, handlerMethod) -> operation.addParametersItem(parameter);
}

@Override
public Schema<?> resolve(AnnotatedType annotatedType, ModelConverterContext modelConverterContext, Iterator<ModelConverter> modelConverterIterator) {
if (annotatedType.isSchemaProperty()) {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,37 +1,34 @@
package com.mb.inventorymanagementservice.config.security;

import com.mb.inventorymanagementservice.service.impl.UserDetailsServiceImpl;
import com.mb.inventorymanagementservice.utils.Constants;
import com.mb.inventorymanagementservice.utils.JwtUtils;
import jakarta.servlet.FilterChain;
import jakarta.servlet.ServletException;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils;
import org.apache.commons.lang3.exception.ExceptionUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.web.authentication.WebAuthenticationDetailsSource;
import org.springframework.util.StringUtils;
import org.springframework.stereotype.Service;
import org.springframework.web.filter.OncePerRequestFilter;
import org.springframework.web.servlet.HandlerExceptionResolver;

import java.io.IOException;

@Slf4j
@Service
@RequiredArgsConstructor
public class AuthTokenFilter extends OncePerRequestFilter {

@Autowired
private JwtUtils jwtUtils;

@Autowired
private UserDetailsServiceImpl userDetailsService;

@Autowired
@Qualifier("handlerExceptionResolver")
private HandlerExceptionResolver resolver;
private final JwtUtils jwtUtils;
private final UserDetailsService userDetailsService;
private final HandlerExceptionResolver handlerExceptionResolver;

@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
Expand All @@ -48,17 +45,22 @@ protected void doFilterInternal(HttpServletRequest request, HttpServletResponse
}
} catch (Exception e) {
log.error("Exception occurred while validating jwt token. doFilterInternal - Exception: {}", ExceptionUtils.getStackTrace(e));
resolver.resolveException(request, response, null, e);
handlerExceptionResolver.resolveException(request, response, null, e);
}
filterChain.doFilter(request, response);
}

private String parseJwt(HttpServletRequest request) {
String headerAuth = request.getHeader("Authorization");

if (StringUtils.hasText(headerAuth) && headerAuth.startsWith("Bearer ")) {
if (StringUtils.isNotBlank(headerAuth) && headerAuth.startsWith("Bearer ")) {
return headerAuth.substring(7);
}
return null;
}

@Override
protected boolean shouldNotFilter(HttpServletRequest request) {
return Constants.isUriExcluded(request.getRequestURI());
}
}
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
package com.mb.inventorymanagementservice.config.security;

import com.mb.inventorymanagementservice.utils.JwtUtils;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.context.annotation.Bean;
Expand All @@ -21,6 +22,7 @@
import org.springframework.security.provisioning.JdbcUserDetailsManager;
import org.springframework.security.web.SecurityFilterChain;
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;
import org.springframework.web.servlet.HandlerExceptionResolver;

import javax.sql.DataSource;

Expand All @@ -33,11 +35,15 @@ public class WebSecurityConfig {

private static final String[] ALLOWED_ENDPOINT_PATTERNS = {"/h2-console/**", "/swagger-resources", "/swagger-ui.html",
"/swagger-ui/index.html", "/swagger-ui/**", "/api-docs/**", "/actuator/**", "/auth/**"};

private final AuthEntryPointJwt unauthorizedHandler;
private final JwtUtils jwtUtils;
private final UserDetailsService userDetailsService;
private final HandlerExceptionResolver handlerExceptionResolver;

@Bean
public AuthTokenFilter authenticationJwtTokenFilter() {
return new AuthTokenFilter();
return new AuthTokenFilter(jwtUtils, userDetailsService, handlerExceptionResolver);
}

@Bean
Expand Down
Loading

0 comments on commit f664b97

Please sign in to comment.