Skip to content

Commit 03e8e6a

Browse files
Merge pull request #1 from vitorhugo-java/copilot/create-job-application-api
Add production-ready Spring Boot REST API for Job Apply Tracker
2 parents 7a74261 + 2051f77 commit 03e8e6a

52 files changed

Lines changed: 1991 additions & 0 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

.gitignore

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,9 @@
1+
# Maven
2+
target/
3+
!.mvn/wrapper/maven-wrapper.jar
4+
!**/src/main/**/target/
5+
!**/src/test/**/target/
6+
17
# Compiled class file
28
*.class
39

backend/pom.xml

Lines changed: 113 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,113 @@
1+
<?xml version="1.0" encoding="UTF-8"?>
2+
<project xmlns="http://maven.apache.org/POM/4.0.0"
3+
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
4+
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
5+
<modelVersion>4.0.0</modelVersion>
6+
7+
<parent>
8+
<groupId>org.springframework.boot</groupId>
9+
<artifactId>spring-boot-starter-parent</artifactId>
10+
<version>3.2.5</version>
11+
<relativePath/>
12+
</parent>
13+
14+
<groupId>com.jobtracker</groupId>
15+
<artifactId>job-tracker</artifactId>
16+
<version>1.0.0</version>
17+
<name>job-tracker</name>
18+
<description>Spring Boot Job Application Tracker API</description>
19+
20+
<properties>
21+
<java.version>21</java.version>
22+
<jjwt.version>0.12.5</jjwt.version>
23+
</properties>
24+
25+
<dependencies>
26+
<!-- Spring Boot Starters -->
27+
<dependency>
28+
<groupId>org.springframework.boot</groupId>
29+
<artifactId>spring-boot-starter-web</artifactId>
30+
</dependency>
31+
<dependency>
32+
<groupId>org.springframework.boot</groupId>
33+
<artifactId>spring-boot-starter-validation</artifactId>
34+
</dependency>
35+
<dependency>
36+
<groupId>org.springframework.boot</groupId>
37+
<artifactId>spring-boot-starter-data-jpa</artifactId>
38+
</dependency>
39+
<dependency>
40+
<groupId>org.springframework.boot</groupId>
41+
<artifactId>spring-boot-starter-security</artifactId>
42+
</dependency>
43+
44+
<!-- MariaDB Driver -->
45+
<dependency>
46+
<groupId>org.mariadb.jdbc</groupId>
47+
<artifactId>mariadb-java-client</artifactId>
48+
<scope>runtime</scope>
49+
</dependency>
50+
51+
<!-- Flyway -->
52+
<dependency>
53+
<groupId>org.flywaydb</groupId>
54+
<artifactId>flyway-core</artifactId>
55+
</dependency>
56+
<dependency>
57+
<groupId>org.flywaydb</groupId>
58+
<artifactId>flyway-mysql</artifactId>
59+
</dependency>
60+
61+
<!-- JWT -->
62+
<dependency>
63+
<groupId>io.jsonwebtoken</groupId>
64+
<artifactId>jjwt-api</artifactId>
65+
<version>${jjwt.version}</version>
66+
</dependency>
67+
<dependency>
68+
<groupId>io.jsonwebtoken</groupId>
69+
<artifactId>jjwt-impl</artifactId>
70+
<version>${jjwt.version}</version>
71+
<scope>runtime</scope>
72+
</dependency>
73+
<dependency>
74+
<groupId>io.jsonwebtoken</groupId>
75+
<artifactId>jjwt-jackson</artifactId>
76+
<version>${jjwt.version}</version>
77+
<scope>runtime</scope>
78+
</dependency>
79+
80+
<!-- OpenAPI / Swagger -->
81+
<dependency>
82+
<groupId>org.springdoc</groupId>
83+
<artifactId>springdoc-openapi-starter-webmvc-ui</artifactId>
84+
<version>2.5.0</version>
85+
</dependency>
86+
87+
<!-- Test -->
88+
<dependency>
89+
<groupId>org.springframework.boot</groupId>
90+
<artifactId>spring-boot-starter-test</artifactId>
91+
<scope>test</scope>
92+
</dependency>
93+
<dependency>
94+
<groupId>org.springframework.security</groupId>
95+
<artifactId>spring-security-test</artifactId>
96+
<scope>test</scope>
97+
</dependency>
98+
<dependency>
99+
<groupId>com.h2database</groupId>
100+
<artifactId>h2</artifactId>
101+
<scope>test</scope>
102+
</dependency>
103+
</dependencies>
104+
105+
<build>
106+
<plugins>
107+
<plugin>
108+
<groupId>org.springframework.boot</groupId>
109+
<artifactId>spring-boot-maven-plugin</artifactId>
110+
</plugin>
111+
</plugins>
112+
</build>
113+
</project>
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
package com.jobtracker;
2+
3+
import org.springframework.boot.SpringApplication;
4+
import org.springframework.boot.autoconfigure.SpringBootApplication;
5+
6+
@SpringBootApplication
7+
public class JobTrackerApplication {
8+
9+
public static void main(String[] args) {
10+
SpringApplication.run(JobTrackerApplication.class, args);
11+
}
12+
}
Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
package com.jobtracker.config;
2+
3+
import org.springframework.beans.factory.annotation.Value;
4+
import org.springframework.context.annotation.Bean;
5+
import org.springframework.context.annotation.Configuration;
6+
import org.springframework.web.cors.CorsConfiguration;
7+
import org.springframework.web.cors.CorsConfigurationSource;
8+
import org.springframework.web.cors.UrlBasedCorsConfigurationSource;
9+
10+
import java.util.Arrays;
11+
import java.util.List;
12+
13+
@Configuration
14+
public class CorsConfig {
15+
16+
@Value("${cors.allowed-origins}")
17+
private String allowedOrigins;
18+
19+
@Bean
20+
public CorsConfigurationSource corsConfigurationSource() {
21+
CorsConfiguration configuration = new CorsConfiguration();
22+
List<String> origins = Arrays.stream(allowedOrigins.split(","))
23+
.map(String::trim)
24+
.toList();
25+
configuration.setAllowedOriginPatterns(origins);
26+
configuration.setAllowedMethods(List.of("GET", "POST", "PUT", "PATCH", "DELETE", "OPTIONS"));
27+
configuration.setAllowedHeaders(List.of("*"));
28+
configuration.setAllowCredentials(true);
29+
configuration.setMaxAge(3600L);
30+
31+
UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
32+
source.registerCorsConfiguration("/**", configuration);
33+
return source;
34+
}
35+
}
Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,65 @@
1+
package com.jobtracker.config;
2+
3+
import com.jobtracker.repository.UserRepository;
4+
import jakarta.servlet.FilterChain;
5+
import jakarta.servlet.ServletException;
6+
import jakarta.servlet.http.HttpServletRequest;
7+
import jakarta.servlet.http.HttpServletResponse;
8+
import org.slf4j.Logger;
9+
import org.slf4j.LoggerFactory;
10+
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
11+
import org.springframework.security.core.context.SecurityContextHolder;
12+
import org.springframework.security.core.userdetails.UserDetails;
13+
import org.springframework.security.web.authentication.WebAuthenticationDetailsSource;
14+
import org.springframework.stereotype.Component;
15+
import org.springframework.web.filter.OncePerRequestFilter;
16+
17+
import java.io.IOException;
18+
19+
@Component
20+
public class JwtAuthenticationFilter extends OncePerRequestFilter {
21+
22+
private static final Logger log = LoggerFactory.getLogger(JwtAuthenticationFilter.class);
23+
24+
private final JwtService jwtService;
25+
private final UserRepository userRepository;
26+
27+
public JwtAuthenticationFilter(JwtService jwtService, UserRepository userRepository) {
28+
this.jwtService = jwtService;
29+
this.userRepository = userRepository;
30+
}
31+
32+
@Override
33+
protected void doFilterInternal(HttpServletRequest request,
34+
HttpServletResponse response,
35+
FilterChain filterChain) throws ServletException, IOException {
36+
final String authHeader = request.getHeader("Authorization");
37+
if (authHeader == null || !authHeader.startsWith("Bearer ")) {
38+
filterChain.doFilter(request, response);
39+
return;
40+
}
41+
42+
final String jwt = authHeader.substring(7);
43+
try {
44+
final String userEmail = jwtService.extractUsername(jwt);
45+
if (userEmail != null && SecurityContextHolder.getContext().getAuthentication() == null) {
46+
UserDetails userDetails = userRepository.findByEmail(userEmail)
47+
.map(user -> (UserDetails) new org.springframework.security.core.userdetails.User(
48+
user.getEmail(), user.getPasswordHash(), java.util.Collections.emptyList()))
49+
.orElse(null);
50+
51+
if (userDetails != null && jwtService.isTokenValid(jwt, userDetails)) {
52+
UsernamePasswordAuthenticationToken authToken = new UsernamePasswordAuthenticationToken(
53+
userDetails, null, userDetails.getAuthorities());
54+
authToken.setDetails(new WebAuthenticationDetailsSource().buildDetails(request));
55+
SecurityContextHolder.getContext().setAuthentication(authToken);
56+
}
57+
}
58+
} catch (Exception e) {
59+
// Log invalid JWT token at debug level and continue without authentication
60+
log.debug("JWT authentication failed: {}", e.getMessage());
61+
}
62+
63+
filterChain.doFilter(request, response);
64+
}
65+
}
Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,74 @@
1+
package com.jobtracker.config;
2+
3+
import io.jsonwebtoken.Claims;
4+
import io.jsonwebtoken.Jwts;
5+
import io.jsonwebtoken.security.Keys;
6+
import org.springframework.beans.factory.annotation.Value;
7+
import org.springframework.security.core.userdetails.UserDetails;
8+
import org.springframework.stereotype.Service;
9+
10+
import javax.crypto.SecretKey;
11+
import java.nio.charset.StandardCharsets;
12+
import java.util.Date;
13+
import java.util.HashMap;
14+
import java.util.Map;
15+
import java.util.function.Function;
16+
17+
@Service
18+
public class JwtService {
19+
20+
@Value("${jwt.secret}")
21+
private String secretKey;
22+
23+
@Value("${jwt.access-token-expiration-ms}")
24+
private long accessTokenExpirationMs;
25+
26+
public String extractUsername(String token) {
27+
return extractClaim(token, Claims::getSubject);
28+
}
29+
30+
public <T> T extractClaim(String token, Function<Claims, T> claimsResolver) {
31+
final Claims claims = extractAllClaims(token);
32+
return claimsResolver.apply(claims);
33+
}
34+
35+
public String generateToken(UserDetails userDetails) {
36+
return generateToken(new HashMap<>(), userDetails);
37+
}
38+
39+
public String generateToken(Map<String, Object> extraClaims, UserDetails userDetails) {
40+
return Jwts.builder()
41+
.claims(extraClaims)
42+
.subject(userDetails.getUsername())
43+
.issuedAt(new Date())
44+
.expiration(new Date(System.currentTimeMillis() + accessTokenExpirationMs))
45+
.signWith(getSigningKey())
46+
.compact();
47+
}
48+
49+
public boolean isTokenValid(String token, UserDetails userDetails) {
50+
final String username = extractUsername(token);
51+
return username.equals(userDetails.getUsername()) && !isTokenExpired(token);
52+
}
53+
54+
private boolean isTokenExpired(String token) {
55+
return extractExpiration(token).before(new Date());
56+
}
57+
58+
private Date extractExpiration(String token) {
59+
return extractClaim(token, Claims::getExpiration);
60+
}
61+
62+
private Claims extractAllClaims(String token) {
63+
return Jwts.parser()
64+
.verifyWith(getSigningKey())
65+
.build()
66+
.parseSignedClaims(token)
67+
.getPayload();
68+
}
69+
70+
private SecretKey getSigningKey() {
71+
byte[] keyBytes = secretKey.getBytes(StandardCharsets.UTF_8);
72+
return Keys.hmacShaKeyFor(keyBytes);
73+
}
74+
}
Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
package com.jobtracker.config;
2+
3+
import io.swagger.v3.oas.models.Components;
4+
import io.swagger.v3.oas.models.OpenAPI;
5+
import io.swagger.v3.oas.models.info.Info;
6+
import io.swagger.v3.oas.models.security.SecurityRequirement;
7+
import io.swagger.v3.oas.models.security.SecurityScheme;
8+
import org.springframework.context.annotation.Bean;
9+
import org.springframework.context.annotation.Configuration;
10+
11+
@Configuration
12+
public class OpenApiConfig {
13+
14+
@Bean
15+
public OpenAPI openAPI() {
16+
final String securitySchemeName = "bearerAuth";
17+
return new OpenAPI()
18+
.info(new Info()
19+
.title("Job Tracker API")
20+
.description("REST API for Job Application Tracker PWA")
21+
.version("1.0.0"))
22+
.addSecurityItem(new SecurityRequirement().addList(securitySchemeName))
23+
.components(new Components()
24+
.addSecuritySchemes(securitySchemeName, new SecurityScheme()
25+
.name(securitySchemeName)
26+
.type(SecurityScheme.Type.HTTP)
27+
.scheme("bearer")
28+
.bearerFormat("JWT")));
29+
}
30+
}
Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
package com.jobtracker.config;
2+
3+
import org.springframework.context.annotation.Bean;
4+
import org.springframework.context.annotation.Configuration;
5+
import org.springframework.http.HttpMethod;
6+
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
7+
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
8+
import org.springframework.security.config.annotation.web.configurers.AbstractHttpConfigurer;
9+
import org.springframework.security.config.http.SessionCreationPolicy;
10+
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
11+
import org.springframework.security.crypto.password.PasswordEncoder;
12+
import org.springframework.security.web.SecurityFilterChain;
13+
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;
14+
15+
@Configuration
16+
@EnableWebSecurity
17+
public class SecurityConfig {
18+
19+
private final JwtAuthenticationFilter jwtAuthenticationFilter;
20+
private final CorsConfig corsConfig;
21+
22+
public SecurityConfig(JwtAuthenticationFilter jwtAuthenticationFilter, CorsConfig corsConfig) {
23+
this.jwtAuthenticationFilter = jwtAuthenticationFilter;
24+
this.corsConfig = corsConfig;
25+
}
26+
27+
@Bean
28+
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
29+
http
30+
.cors(cors -> cors.configurationSource(corsConfig.corsConfigurationSource()))
31+
.csrf(AbstractHttpConfigurer::disable) // CSRF protection disabled for stateless JWT REST API
32+
.sessionManagement(session -> session.sessionCreationPolicy(SessionCreationPolicy.STATELESS))
33+
.authorizeHttpRequests(auth -> auth
34+
.requestMatchers(HttpMethod.OPTIONS, "/**").permitAll()
35+
.requestMatchers("/api/auth/register", "/api/auth/login",
36+
"/api/auth/refresh", "/api/auth/forgot-password",
37+
"/api/auth/reset-password").permitAll()
38+
.requestMatchers("/v3/api-docs/**", "/swagger-ui/**", "/swagger-ui.html").permitAll()
39+
.anyRequest().authenticated()
40+
)
41+
.addFilterBefore(jwtAuthenticationFilter, UsernamePasswordAuthenticationFilter.class);
42+
43+
return http.build();
44+
}
45+
46+
@Bean
47+
public PasswordEncoder passwordEncoder() {
48+
return new BCryptPasswordEncoder();
49+
}
50+
}

0 commit comments

Comments
 (0)