diff --git a/pom.xml b/pom.xml
index 2ea7b2c..ab61ac4 100644
--- a/pom.xml
+++ b/pom.xml
@@ -69,6 +69,18 @@
8.7.0
+
+ org.springdoc
+ springdoc-openapi-starter-webmvc-ui
+ 2.5.0
+
+
+
+ com.auth0
+ java-jwt
+ 4.4.0
+
+
diff --git a/src/main/java/com/vodacom/falcon/config/SwaggerConfig.java b/src/main/java/com/vodacom/falcon/config/SwaggerConfig.java
new file mode 100644
index 0000000..a94e077
--- /dev/null
+++ b/src/main/java/com/vodacom/falcon/config/SwaggerConfig.java
@@ -0,0 +1,29 @@
+package com.vodacom.falcon.config;
+
+import io.swagger.v3.oas.annotations.OpenAPIDefinition;
+import io.swagger.v3.oas.models.OpenAPI;
+import io.swagger.v3.oas.models.info.Contact;
+import io.swagger.v3.oas.models.info.Info;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.Configuration;
+
+@Configuration
+@OpenAPIDefinition
+@Slf4j
+public class SwaggerConfig {
+ @Bean
+ public OpenAPI myOpenAPI() {
+ Contact contact = new Contact()
+ .email("faruquebraimo@gmail.com ")
+ .name("Faruque Braimo");
+
+ Info info = new Info()
+ .title("Falcon-API")
+ .version("1.0.0")
+ .contact(contact)
+ .description("Your travel assistant api");
+
+ return new OpenAPI().info(info);
+ }
+}
diff --git a/src/main/java/com/vodacom/falcon/config/security/CustomFilter.java b/src/main/java/com/vodacom/falcon/config/security/CustomFilter.java
index bde1c24..2a6ae37 100644
--- a/src/main/java/com/vodacom/falcon/config/security/CustomFilter.java
+++ b/src/main/java/com/vodacom/falcon/config/security/CustomFilter.java
@@ -2,6 +2,7 @@
import com.vodacom.falcon.model.User;
import com.vodacom.falcon.repository.UserRepository;
+import com.vodacom.falcon.service.TokenService;
import jakarta.servlet.FilterChain;
import jakarta.servlet.ServletException;
import jakarta.servlet.http.HttpServletRequest;
@@ -9,64 +10,43 @@
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
-import org.springframework.security.core.Authentication;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.stereotype.Component;
import org.springframework.web.filter.OncePerRequestFilter;
import java.io.IOException;
-import java.util.Base64;
-import java.util.Objects;
import static com.vodacom.falcon.util.FalconDefaults.AUTHORIZATION;
-import static com.vodacom.falcon.util.FalconDefaults.BASIC;
@Slf4j
@Component
@RequiredArgsConstructor
public class CustomFilter extends OncePerRequestFilter {
private final UserRepository userRepository;
- private final PasswordEncoderProvider encoderProvider;
+ private final TokenService tokenService;
+
@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
- if (isBasicAuth(request)) {
- String base64 = this.getHeader(request)
- .replace(BASIC, "");
- String[] credentials = new String(Base64.getDecoder()
- .decode(base64))
- .split(":");
-
- String username = credentials[0];
- String pass = credentials[1];
+ String token = getToken(request);
+ if (token != null) {
+ var username = tokenService.validateToken(token);
User user = userRepository.findByUsername(username);
- if (Objects.isNull(user)) {
- response.setStatus(HttpServletResponse.SC_UNAUTHORIZED);
- response.getWriter().write("User not found!");
- return;
- }
-
- if (encoderProvider.passwordEncoder().matches(user.getPassword(), pass)) {
- response.setStatus(HttpServletResponse.SC_UNAUTHORIZED);
- response.getWriter().write("Wrong Password");
- return;
+ if (user != null) {
+ var authentication = new UsernamePasswordAuthenticationToken(user, null, user.getAuthorities());
+ SecurityContextHolder.getContext().setAuthentication(authentication);
}
-
- Authentication authentication = new UsernamePasswordAuthenticationToken(user, null, null);
- SecurityContextHolder.getContext().setAuthentication(authentication);
}
filterChain.doFilter(request, response);
}
- private boolean isBasicAuth(HttpServletRequest request) {
- String header = getHeader(request);
- return header != null && header.startsWith(BASIC);
- }
- private String getHeader(HttpServletRequest request) {
- return request.getHeader(AUTHORIZATION);
+ private String getToken(HttpServletRequest request) {
+ String header = request.getHeader(AUTHORIZATION);
+ if (header == null) return null;
+ return header.replace("Bearer ", "");
}
}
diff --git a/src/main/java/com/vodacom/falcon/config/security/SecurityConfig.java b/src/main/java/com/vodacom/falcon/config/security/SecurityConfig.java
index 4ca5e20..468b59f 100644
--- a/src/main/java/com/vodacom/falcon/config/security/SecurityConfig.java
+++ b/src/main/java/com/vodacom/falcon/config/security/SecurityConfig.java
@@ -4,10 +4,14 @@
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
+import org.springframework.security.authentication.AuthenticationManager;
+import org.springframework.security.config.annotation.authentication.configuration.AuthenticationConfiguration;
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.AbstractHttpConfigurer;
+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;
@@ -20,6 +24,12 @@
public class SecurityConfig {
@Autowired
private CustomFilter filter;
+ private static final String[] AUTH_WHITELIST = {
+ "falcon/auth/**",
+ "falcon/insight/",
+ "/v3/api-docs/**",
+ "/swagger-ui/**"
+ };
@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
@@ -28,11 +38,22 @@ public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
.authorizeHttpRequests(authorize -> authorize
.dispatcherTypeMatchers(FORWARD, ERROR)
.permitAll()
- .requestMatchers("falcon/auth/**", "falcon/insight/**")
+ .requestMatchers(AUTH_WHITELIST)
.permitAll()
- .anyRequest().denyAll()
+ .anyRequest()
+ .authenticated()
)
.addFilterBefore(filter, UsernamePasswordAuthenticationFilter.class)
.build();
}
+
+ @Bean
+ public AuthenticationManager authenticationManager(AuthenticationConfiguration authenticationConfiguration) throws Exception {
+ return authenticationConfiguration.getAuthenticationManager();
+ }
+
+ @Bean
+ public PasswordEncoder passwordEncoder() {
+ return new BCryptPasswordEncoder();
+ }
}
diff --git a/src/main/java/com/vodacom/falcon/config/security/UserDetails.java b/src/main/java/com/vodacom/falcon/config/security/UserDetails.java
deleted file mode 100644
index 2bebf4e..0000000
--- a/src/main/java/com/vodacom/falcon/config/security/UserDetails.java
+++ /dev/null
@@ -1,11 +0,0 @@
-package com.vodacom.falcon.config.security;
-
-import lombok.Getter;
-import lombok.Setter;
-
-@Getter
-@Setter
-public class UserDetails {
- private String username;
- private String password;
-}
diff --git a/src/main/java/com/vodacom/falcon/controller/AuthController.java b/src/main/java/com/vodacom/falcon/controller/AuthController.java
index 2ab577f..4f8f276 100644
--- a/src/main/java/com/vodacom/falcon/controller/AuthController.java
+++ b/src/main/java/com/vodacom/falcon/controller/AuthController.java
@@ -1,10 +1,18 @@
package com.vodacom.falcon.controller;
+import com.vodacom.falcon.model.request.AuthLoginRequest;
import com.vodacom.falcon.model.request.UserRegistrationRequest;
+import com.vodacom.falcon.model.response.TokenResponse;
import com.vodacom.falcon.service.AuthService;
-import lombok.RequiredArgsConstructor;
+import io.swagger.v3.oas.annotations.Operation;
+import io.swagger.v3.oas.annotations.media.Content;
+import io.swagger.v3.oas.annotations.media.Schema;
+import io.swagger.v3.oas.annotations.responses.ApiResponse;
+import io.swagger.v3.oas.annotations.responses.ApiResponses;
+import jakarta.validation.Valid;
import lombok.extern.slf4j.Slf4j;
+import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.CrossOrigin;
@@ -15,15 +23,34 @@
@RestController
@CrossOrigin(value = "*")
-@RequiredArgsConstructor
+
@Slf4j
@RequestMapping("falcon/auth")
public class AuthController {
- private final AuthService service;
+ @Autowired
+ private AuthService service;
- @PostMapping("/signup")
- public ResponseEntity signup(@RequestBody UserRegistrationRequest userRegistrationRequest) {
+ @PostMapping("/signUp")
+ @Operation(summary = "Sign Up", description = "For user registration")
+ @ApiResponses(value = {
+ @ApiResponse(responseCode = "200", content = {@Content(schema = @Schema(implementation = String.class), mediaType = "application/json")}),
+ @ApiResponse(responseCode = "400", content = {@Content(schema = @Schema(implementation = String.class), mediaType = "application/json")}),
+ @ApiResponse(responseCode = "500", content = {@Content(schema = @Schema(implementation = String.class), mediaType = "application/json")})
+ })
+ public ResponseEntity signUp(@RequestBody @Valid UserRegistrationRequest userRegistrationRequest) {
service.createUser(userRegistrationRequest);
return new ResponseEntity<>(HttpStatus.ACCEPTED);
}
+
+ @PostMapping("/signIn")
+ @Operation(summary = "Sign In", description = "For user login")
+ @ApiResponses(value = {
+ @ApiResponse(responseCode = "200", content = {@Content(schema = @Schema(implementation = String.class), mediaType = "application/json")}),
+ @ApiResponse(responseCode = "400", content = {@Content(schema = @Schema(implementation = String.class), mediaType = "application/json")}),
+ @ApiResponse(responseCode = "500", content = {@Content(schema = @Schema(implementation = String.class), mediaType = "application/json")})
+ })
+ public ResponseEntity signIn(@RequestBody @Valid AuthLoginRequest authLoginRequest) {
+ service.login(authLoginRequest);
+ return ResponseEntity.ok(service.login(authLoginRequest));
+ }
}
diff --git a/src/main/java/com/vodacom/falcon/controller/InsightController.java b/src/main/java/com/vodacom/falcon/controller/InsightController.java
index eb22700..d06f05e 100644
--- a/src/main/java/com/vodacom/falcon/controller/InsightController.java
+++ b/src/main/java/com/vodacom/falcon/controller/InsightController.java
@@ -3,6 +3,12 @@
import com.vodacom.falcon.model.response.HistoricalEconomyInsightResponse;
import com.vodacom.falcon.model.response.InsightResponse;
import com.vodacom.falcon.service.InsightService;
+import io.swagger.v3.oas.annotations.Operation;
+import io.swagger.v3.oas.annotations.media.Content;
+import io.swagger.v3.oas.annotations.media.Schema;
+import io.swagger.v3.oas.annotations.responses.ApiResponse;
+import io.swagger.v3.oas.annotations.responses.ApiResponses;
+import jakarta.validation.Valid;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.cache.annotation.Cacheable;
@@ -25,14 +31,26 @@ public class InsightController {
private final InsightService falconInsightService;
@GetMapping()
- public ResponseEntity getInsight(@RequestParam("city") String city) {
+ @Operation(summary = "Get insight by city", description = "Should return population, gdp, exchange rates, weather forecast. The last 2 if the user is authenticated")
+ @ApiResponses(value = {
+ @ApiResponse(responseCode = "200", content = {@Content(schema = @Schema(implementation = String.class), mediaType = "application/json")}),
+ @ApiResponse(responseCode = "400", content = {@Content(schema = @Schema(implementation = String.class), mediaType = "application/json")}),
+ @ApiResponse(responseCode = "500", content = {@Content(schema = @Schema(implementation = String.class), mediaType = "application/json")})
+ })
+ public ResponseEntity getInsight(@RequestParam("city") @Valid String city) {
InsightResponse response = falconInsightService.getInsight(city);
return new ResponseEntity<>(response, HttpStatus.OK);
}
@GetMapping("/historical")
+ @Operation(summary = "Get historical insights by country", description = "Should return population and gdp from 2012 to 2022 for given country")
+ @ApiResponses(value = {
+ @ApiResponse(responseCode = "200", content = {@Content(schema = @Schema(implementation = String.class), mediaType = "application/json")}),
+ @ApiResponse(responseCode = "400", content = {@Content(schema = @Schema(implementation = String.class), mediaType = "application/json")}),
+ @ApiResponse(responseCode = "500", content = {@Content(schema = @Schema(implementation = String.class), mediaType = "application/json")})
+ })
@Cacheable(key = "#city", value = "historical")
- public ResponseEntity getHistoricalInsights(@RequestParam("city") String city) throws InterruptedException {
+ public ResponseEntity getHistoricalInsights(@RequestParam("city") @Valid String city) {
HistoricalEconomyInsightResponse response = falconInsightService.getHistoricalInsights(city);
return new ResponseEntity<>(response, HttpStatus.OK);
}
diff --git a/src/main/java/com/vodacom/falcon/model/User.java b/src/main/java/com/vodacom/falcon/model/User.java
index bca6b66..0604e22 100644
--- a/src/main/java/com/vodacom/falcon/model/User.java
+++ b/src/main/java/com/vodacom/falcon/model/User.java
@@ -11,8 +11,11 @@
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.Setter;
+import org.springframework.security.core.GrantedAuthority;
+import org.springframework.security.core.userdetails.UserDetails;
import java.time.LocalDateTime;
+import java.util.Collection;
import java.util.UUID;
@Entity
@@ -22,11 +25,16 @@
@NoArgsConstructor
@Builder
@Table(name = "falcon_user")
-public class User {
+public class User implements UserDetails {
@Id
@GeneratedValue(strategy = GenerationType.AUTO)
private UUID id;
private String username;
private String password;
private LocalDateTime createdAt;
+
+ @Override
+ public Collection extends GrantedAuthority> getAuthorities() {
+ return null;
+ }
}
diff --git a/src/main/java/com/vodacom/falcon/model/request/AuthLoginRequest.java b/src/main/java/com/vodacom/falcon/model/request/AuthLoginRequest.java
new file mode 100644
index 0000000..98394d1
--- /dev/null
+++ b/src/main/java/com/vodacom/falcon/model/request/AuthLoginRequest.java
@@ -0,0 +1,4 @@
+package com.vodacom.falcon.model.request;
+
+public record AuthLoginRequest(String username, String password) {
+}
diff --git a/src/main/java/com/vodacom/falcon/model/response/TokenResponse.java b/src/main/java/com/vodacom/falcon/model/response/TokenResponse.java
new file mode 100644
index 0000000..eb09132
--- /dev/null
+++ b/src/main/java/com/vodacom/falcon/model/response/TokenResponse.java
@@ -0,0 +1,4 @@
+package com.vodacom.falcon.model.response;
+
+public record TokenResponse (String token) {
+}
diff --git a/src/main/java/com/vodacom/falcon/service/AuthService.java b/src/main/java/com/vodacom/falcon/service/AuthService.java
index c0faca7..6cbc692 100644
--- a/src/main/java/com/vodacom/falcon/service/AuthService.java
+++ b/src/main/java/com/vodacom/falcon/service/AuthService.java
@@ -3,20 +3,33 @@
import com.vodacom.falcon.config.security.PasswordEncoderProvider;
import com.vodacom.falcon.model.User;
+import com.vodacom.falcon.model.request.AuthLoginRequest;
import com.vodacom.falcon.model.request.UserRegistrationRequest;
+import com.vodacom.falcon.model.response.TokenResponse;
import com.vodacom.falcon.repository.UserRepository;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
+import org.springframework.context.ApplicationContext;
+import org.springframework.security.authentication.AuthenticationManager;
+import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
+import org.springframework.security.core.Authentication;
+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 java.time.LocalDateTime;
@Service
-@RequiredArgsConstructor
@Slf4j
-public class AuthService {
+@RequiredArgsConstructor
+public class AuthService implements UserDetailsService {
+
private final PasswordEncoderProvider encoderProvider;
+
private final UserRepository userRepository;
+ private final ApplicationContext context;
+ private final TokenService tokenService;
public void createUser(UserRegistrationRequest auth) {
User existingUser = userRepository.findByUsername(auth.username());
@@ -30,5 +43,19 @@ public void createUser(UserRegistrationRequest auth) {
.createdAt(LocalDateTime.now())
.build();
userRepository.save(user);
+
+ }
+
+ public TokenResponse login(AuthLoginRequest login) {
+ AuthenticationManager authenticationManager = context.getBean(AuthenticationManager.class);
+ UsernamePasswordAuthenticationToken authToken = new UsernamePasswordAuthenticationToken(login.username(), login.password());
+ Authentication auth = authenticationManager.authenticate(authToken);
+ var token = tokenService.generateToken((User) auth.getPrincipal());
+ return new TokenResponse(token);
+ }
+
+ @Override
+ public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
+ return userRepository.findByUsername(username);
}
}
diff --git a/src/main/java/com/vodacom/falcon/service/TokenService.java b/src/main/java/com/vodacom/falcon/service/TokenService.java
new file mode 100644
index 0000000..887ff13
--- /dev/null
+++ b/src/main/java/com/vodacom/falcon/service/TokenService.java
@@ -0,0 +1,53 @@
+package com.vodacom.falcon.service;
+
+import com.auth0.jwt.JWT;
+import com.auth0.jwt.algorithms.Algorithm;
+import com.auth0.jwt.exceptions.JWTCreationException;
+import com.auth0.jwt.exceptions.JWTVerificationException;
+import com.vodacom.falcon.model.User;
+import org.springframework.beans.factory.annotation.Value;
+import org.springframework.stereotype.Service;
+
+import java.time.Instant;
+import java.time.LocalDateTime;
+import java.time.ZoneOffset;
+
+@Service
+public class TokenService {
+
+ @Value("${jwt.key}")
+ private String key;
+
+ public String generateToken(User user) {
+ try {
+ Algorithm algorithm = Algorithm.HMAC256(key);
+
+ return JWT.create()
+ .withIssuer("falcon")
+ .withSubject(user.getUsername())
+ .withExpiresAt(getExpirationDate())
+ .sign(algorithm);
+ } catch (IllegalArgumentException | JWTCreationException e) {
+ throw new RuntimeException("Error generating token", e);
+ }
+ }
+
+ public String validateToken(String token) {
+ try {
+ Algorithm algorithm = Algorithm.HMAC256(key);
+
+ return JWT.require(algorithm)
+ .withIssuer("falcon")
+ .build()
+ .verify(token)
+ .getSubject();
+ } catch (JWTVerificationException exception) {
+ throw new RuntimeException("Token Not recognized");
+ }
+ }
+
+
+ private Instant getExpirationDate() {
+ return LocalDateTime.now().plusHours(2).toInstant(ZoneOffset.of("-03:00"));
+ }
+}
diff --git a/src/main/resources/application.properties b/src/main/resources/application.properties
deleted file mode 100644
index 67d251f..0000000
--- a/src/main/resources/application.properties
+++ /dev/null
@@ -1 +0,0 @@
-pring.application.name=voda-travel
diff --git a/src/main/resources/application.yml b/src/main/resources/application.yml
index e448fb8..79b257d 100644
--- a/src/main/resources/application.yml
+++ b/src/main/resources/application.yml
@@ -23,4 +23,7 @@ open-weather-map:
main-exchange-rates-api:
apiKey: c771abe4cb5cfe85674fae5142290f1a # ${exchangeRatesApiKey}
optional-exchange-rates-api:
- apiKey: dc7d9a4fe0e74557b0b83a1fcf66dd2a # ${exchangeRatesApiKey}
\ No newline at end of file
+ apiKey: dc7d9a4fe0e74557b0b83a1fcf66dd2a # ${exchangeRatesApiKey}
+
+jwt:
+ key: 0cd1833b-62b4-415c-902f-1e69138fd2a8