Skip to content

Add production-ready Spring Boot REST API for Job Apply Tracker#1

Merged
vitorhugo-java merged 3 commits into
mainfrom
copilot/create-job-application-api
Apr 13, 2026
Merged

Add production-ready Spring Boot REST API for Job Apply Tracker#1
vitorhugo-java merged 3 commits into
mainfrom
copilot/create-job-application-api

Conversation

Copy link
Copy Markdown
Contributor

Copilot AI commented Apr 13, 2026

Implements a full backend from scratch for a PWA job application tracker: JWT auth with refresh token rotation, job application CRUD with filtering/pagination, dashboard summary, and password reset flow — all against MariaDB via Flyway migrations.

Auth (/api/auth/*)

  • POST register/login → returns access + refresh token + user
  • POST refresh → token rotation (old token revoked on use)
  • POST forgot-password / reset-password → token-scoped, 30 min expiry, invalidates existing tokens
  • POST logout → revokes refresh token
  • GET me → returns authenticated user

Applications (/api/applications/*)

  • Full CRUD with owner-scoped access (each record bound to authenticated user)
  • GET /api/applications — paginated, filterable by status, recruiterName, applicationDateFrom/To, interviewScheduled, recruiterDmReminderEnabled; sort validated against a whitelist
  • PATCH /{id}/status, PATCH /{id}/reminder — targeted field updates
  • GET /upcoming / GET /overdue — server-side computed from nextStepDateTime

Dashboard (/api/dashboard/summary)

Aggregates total applications, waiting responses, scheduled interviews, overdue follow-ups, and active DM reminders for the current user.

Infrastructure

  • JWT: JJWT 0.12, configurable expiry via jwt.access-token-expiration-ms / jwt.refresh-token-expiration-ms
  • Security: stateless SessionCreationPolicy.STATELESS, BCryptPasswordEncoder, CSRF disabled (JWT REST API)
  • DB: MariaDB + Flyway V1__initial_schema.sql with indexed foreign keys; ddl-auto: validate
  • Errors: GlobalExceptionHandler returns structured { timestamp, status, error, message } with per-field validation errors on 400s

Status enum mapping

API surfaces human-readable Portuguese strings; internally stored as enum names:

"RH" → RH
"Fiz a RH - Aguardando Atualização" → FIZ_A_RH_AGUARDANDO_ATUALIZACAO
"Fiz a Hiring Manager - Aguardando Atualização" → FIZ_A_HIRING_MANAGER_AGUARDANDO_ATUALIZACAO
"Teste Técnico" → TESTE_TECNICO
"Fiz teste Técnico - aguardando atualização" → FIZ_TESTE_TECNICO_AGUARDANDO_ATUALIZACAO
"RH (Negociação)" → RH_NEGOCIACAO
Original prompt

Act as an expert Java 21 backend engineer and architect. Build a production-ready REST API for a Progressive Web App that tracks job applications, user authentication, recruiter reminders, and recruitment stages.

PROJECT GOAL
Create a robust backend for managing authentication, password recovery, job applications, status tracking, reminder flags, dashboard summaries, JWT authentication, refresh tokens, and secure access control.

TECH STACK (MANDATORY)

  • Java 21
  • Spring Boot
  • Spring Web
  • Spring Validation
  • Spring Data JPA
  • Spring Security
  • JWT authentication
  • Refresh token authentication
  • MariaDB
  • GraalVM native-image friendly design
  • REST API
  • Clean layered architecture
  • DTO-based request/response design
  • Strong validation and exception handling

IMPORTANT RULE
Use ONLY the API contracts defined below. Do not invent endpoints, request fields, response fields, query parameters, or business rules outside these contracts. Do not omit any endpoint or payload defined here. Do not create any extra status values.

==================================================================
ARCHITECTURE AND FOLDER STRUCTURE

Use a clean package structure similar to this:

backend/
├── src/main/java/com/jobtracker/
│ ├── JobTrackerApplication.java
│ ├── config/
│ │ ├── CorsConfig.java
│ │ ├── SecurityConfig.java
│ │ ├── JwtAuthenticationFilter.java
│ │ ├── JwtService.java
│ │ └── OpenApiConfig.java
│ ├── controller/
│ │ ├── AuthController.java
│ │ ├── ApplicationController.java
│ │ └── DashboardController.java
│ ├── dto/
│ │ ├── auth/
│ │ │ ├── RegisterRequest.java
│ │ │ ├── LoginRequest.java
│ │ │ ├── RefreshTokenRequest.java
│ │ │ ├── ForgotPasswordRequest.java
│ │ │ ├── ResetPasswordRequest.java
│ │ │ ├── LogoutRequest.java
│ │ │ ├── AuthResponse.java
│ │ │ └── UserResponse.java
│ │ ├── application/
│ │ │ ├── ApplicationRequest.java
│ │ │ ├── ApplicationResponse.java
│ │ │ ├── ApplicationPageResponse.java
│ │ │ ├── UpdateStatusRequest.java
│ │ │ └── UpdateReminderRequest.java
│ │ └── dashboard/
│ │ └── DashboardSummaryResponse.java
│ ├── entity/
│ │ ├── User.java
│ │ ├── JobApplication.java
│ │ ├── RefreshToken.java
│ │ ├── PasswordResetToken.java
│ │ └── enums/
│ │ └── ApplicationStatus.java
│ ├── repository/
│ │ ├── UserRepository.java
│ │ ├── ApplicationRepository.java
│ │ ├── RefreshTokenRepository.java
│ │ └── PasswordResetTokenRepository.java
│ ├── service/
│ │ ├── AuthService.java
│ │ ├── ApplicationService.java
│ │ ├── DashboardService.java
│ │ ├── RefreshTokenService.java
│ │ ├── PasswordResetService.java
│ │ └── JwtService.java
│ ├── mapper/
│ │ ├── AuthMapper.java
│ │ └── ApplicationMapper.java
│ ├── exception/
│ │ ├── GlobalExceptionHandler.java
│ │ ├── ResourceNotFoundException.java
│ │ ├── BadRequestException.java
│ │ ├── UnauthorizedException.java
│ │ └── ConflictException.java
│ └── util/
│ └── SecurityUtils.java
├── src/main/resources/
│ ├── application.yml
│ └── db/migration/
└── pom.xml

DATABASE

  • Use MariaDB
  • Use JPA/Hibernate
  • Use Flyway migrations if appropriate
  • Design the schema for production use
  • Ensure all entities are well indexed where needed

AUTHENTICATION REQUIREMENTS

  • Implement registration
  • Implement login
  • Implement logout
  • Implement forgot password
  • Implement reset password
  • Implement refresh token rotation
  • Use JWT access tokens
  • Persist refresh tokens in the database
  • Hash passwords securely
  • Support protected endpoints
  • Prepare the project for GraalVM native-image compatibility

AUTH API CONTRACTS
Base endpoints:

  • POST /api/auth/register
  • POST /api/auth/login
  • POST /api/auth/refresh
  • POST /api/auth/forgot-password
  • POST /api/auth/reset-password
  • POST /api/auth/logout
  • GET /api/auth/me
  1. POST /api/auth/register
    Request:
    {
    "name": "string",
    "email": "string",
    "password": "string",
    "confirmPassword": "string"
    }

Response:
{
"accessToken": "string",
"refreshToken": "string",
"user": {
"id": "number",
"name": "string",
"email": "string"
}
}

  1. POST /api/auth/login
    Request:
    {
    "email": "string",
    "password": "string"
    }

Response:
{
"accessToken": "string",
"refreshToken": "string",
"user": {
"id": "number",
"name": "string",
"email": "string"
}
}

  1. POST /api/auth/refresh
    Request:
    {
    "refreshToken": "string"
    }

Response:
{
"accessToken": "string",
"refreshToken": "string"
}

  1. POST /api/auth/forgot-password
    Request:
    {
    "email": "string"
    }

Response:
{
"message": "string"
}

  1. POST /api/auth/reset-password
    Request:
    {
    "token": "string",
    "newPassword": "string",
    "confirmPassword": "string"
    }

Response:
{
"message": "string"
}

  1. POST /api/auth/logout
    Request:
    {
    "...

Copilot AI and others added 2 commits April 13, 2026 14:42
Copilot AI changed the title [WIP] Add production-ready REST API for job applications management Add production-ready Spring Boot REST API for Job Apply Tracker Apr 13, 2026
Copilot AI requested a review from vitorhugo-java April 13, 2026 14:47
@vitorhugo-java vitorhugo-java marked this pull request as ready for review April 13, 2026 15:00
Copilot AI review requested due to automatic review settings April 13, 2026 15:00
@vitorhugo-java vitorhugo-java merged commit 03e8e6a into main Apr 13, 2026
@vitorhugo-java vitorhugo-java deleted the copilot/create-job-application-api branch April 13, 2026 15:00
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Adds a new Spring Boot (Java 21) backend for a Job Application Tracker PWA, including JWT-based auth (with refresh token rotation), password reset flow, job application CRUD + filtering, and a dashboard summary, backed by MariaDB/Flyway.

Changes:

  • Introduces JWT auth endpoints (register/login/refresh/logout/me) plus password reset token flow.
  • Implements job application persistence + CRUD, filtering/pagination/sorting, and upcoming/overdue queries.
  • Adds Flyway V1 schema, Spring Security/CORS configuration, and OpenAPI (Swagger UI) setup.

Reviewed changes

Copilot reviewed 51 out of 52 changed files in this pull request and generated 13 comments.

Show a summary per file
File Description
.gitignore Adds Maven/target ignores.
backend/pom.xml Defines Spring Boot/JPA/Security/Flyway/JJWT/OpenAPI dependencies.
backend/src/main/java/com/jobtracker/JobTrackerApplication.java Spring Boot application entrypoint.
backend/src/main/java/com/jobtracker/config/CorsConfig.java Configures CORS origins/methods/headers.
backend/src/main/java/com/jobtracker/config/JwtAuthenticationFilter.java Adds bearer JWT auth filter populating SecurityContext.
backend/src/main/java/com/jobtracker/config/JwtService.java JWT signing/verification + claim extraction utilities.
backend/src/main/java/com/jobtracker/config/OpenApiConfig.java Configures OpenAPI + bearer auth scheme.
backend/src/main/java/com/jobtracker/config/SecurityConfig.java Configures stateless Spring Security + route authorization.
backend/src/main/java/com/jobtracker/controller/ApplicationController.java Exposes application CRUD/filtering + upcoming/overdue endpoints.
backend/src/main/java/com/jobtracker/controller/AuthController.java Exposes auth/password reset endpoints + /me.
backend/src/main/java/com/jobtracker/controller/DashboardController.java Exposes dashboard summary endpoint.
backend/src/main/java/com/jobtracker/dto/application/ApplicationPageResponse.java Paging response DTO.
backend/src/main/java/com/jobtracker/dto/application/ApplicationRequest.java Application create/update request DTO + validations.
backend/src/main/java/com/jobtracker/dto/application/ApplicationResponse.java Application response DTO.
backend/src/main/java/com/jobtracker/dto/application/UpdateReminderRequest.java Reminder toggle request DTO.
backend/src/main/java/com/jobtracker/dto/application/UpdateStatusRequest.java Status update request DTO.
backend/src/main/java/com/jobtracker/dto/auth/AuthResponse.java Auth response DTO (access/refresh/user).
backend/src/main/java/com/jobtracker/dto/auth/ForgotPasswordRequest.java Forgot-password request DTO.
backend/src/main/java/com/jobtracker/dto/auth/LoginRequest.java Login request DTO.
backend/src/main/java/com/jobtracker/dto/auth/LogoutRequest.java Logout request DTO.
backend/src/main/java/com/jobtracker/dto/auth/MessageResponse.java Generic message response DTO.
backend/src/main/java/com/jobtracker/dto/auth/RefreshResponse.java Refresh response DTO.
backend/src/main/java/com/jobtracker/dto/auth/RefreshTokenRequest.java Refresh request DTO.
backend/src/main/java/com/jobtracker/dto/auth/RegisterRequest.java Register request DTO.
backend/src/main/java/com/jobtracker/dto/auth/ResetPasswordRequest.java Reset-password request DTO.
backend/src/main/java/com/jobtracker/dto/auth/UserResponse.java User response DTO.
backend/src/main/java/com/jobtracker/dto/dashboard/DashboardSummaryResponse.java Dashboard summary DTO.
backend/src/main/java/com/jobtracker/entity/JobApplication.java JPA entity for job applications.
backend/src/main/java/com/jobtracker/entity/PasswordResetToken.java JPA entity for password reset tokens.
backend/src/main/java/com/jobtracker/entity/RefreshToken.java JPA entity for refresh tokens.
backend/src/main/java/com/jobtracker/entity/User.java JPA entity for users.
backend/src/main/java/com/jobtracker/entity/enums/ApplicationStatus.java Status enum with PT display-name mapping.
backend/src/main/java/com/jobtracker/exception/BadRequestException.java Custom 400 exception.
backend/src/main/java/com/jobtracker/exception/ConflictException.java Custom 409 exception.
backend/src/main/java/com/jobtracker/exception/GlobalExceptionHandler.java Global exception-to-JSON response mapping.
backend/src/main/java/com/jobtracker/exception/ResourceNotFoundException.java Custom 404 exception.
backend/src/main/java/com/jobtracker/exception/UnauthorizedException.java Custom 401 exception.
backend/src/main/java/com/jobtracker/mapper/ApplicationMapper.java Maps application entity → response DTO.
backend/src/main/java/com/jobtracker/mapper/AuthMapper.java Maps user entity → response DTO.
backend/src/main/java/com/jobtracker/repository/ApplicationRepository.java JPA repository + custom queries for applications.
backend/src/main/java/com/jobtracker/repository/PasswordResetTokenRepository.java JPA repository + invalidation query for reset tokens.
backend/src/main/java/com/jobtracker/repository/RefreshTokenRepository.java JPA repository + bulk revoke query for refresh tokens.
backend/src/main/java/com/jobtracker/repository/UserRepository.java JPA repository for users.
backend/src/main/java/com/jobtracker/service/ApplicationService.java Application business logic + filtering/sort spec.
backend/src/main/java/com/jobtracker/service/AuthService.java Auth, refresh rotation, forgot/reset password logic.
backend/src/main/java/com/jobtracker/service/DashboardService.java Computes dashboard summary counts.
backend/src/main/java/com/jobtracker/service/PasswordResetService.java Manages reset token creation/validation/use.
backend/src/main/java/com/jobtracker/service/RefreshTokenService.java Manages refresh token issuance/rotation/revocation.
backend/src/main/java/com/jobtracker/util/SecurityUtils.java Fetches current user/current user id from SecurityContext.
backend/src/main/resources/application.yml App configuration for DB/JPA/Flyway/JWT/CORS/OpenAPI.
backend/src/main/resources/db/migration/V1__initial_schema.sql Initial MariaDB schema for users/tokens/applications.
backend/src/test/java/com/jobtracker/JobTrackerApplicationTests.java Spring context smoke test with H2 overrides.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment on lines +37 to +55
public RefreshToken verifyAndRotate(String tokenValue) {
RefreshToken token = refreshTokenRepository.findByToken(tokenValue)
.orElseThrow(() -> new UnauthorizedException("Invalid refresh token"));

if (token.isRevoked()) {
throw new UnauthorizedException("Refresh token has been revoked");
}

if (token.getExpiryDate().isBefore(LocalDateTime.now())) {
throw new UnauthorizedException("Refresh token has expired");
}

// Revoke old token (rotation)
token.setRevoked(true);
refreshTokenRepository.save(token);

// Create new token
return createRefreshToken(token.getUser());
}
Copy link

Copilot AI Apr 13, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

verifyAndRotate revokes the old refresh token and then creates a new one, but there’s no locking/atomic update to prevent two concurrent refresh requests from rotating the same token twice (both can pass revoked=false and expiryDate checks). This enables refresh-token replay and can mint multiple valid tokens from a single refresh token. Consider adding optimistic locking (@Version), a SELECT ... FOR UPDATE/@Lock(PESSIMISTIC_WRITE) on lookup, or an atomic UPDATE ... WHERE token=:token AND revoked=false with affected-row check before issuing the new token.

Copilot uses AI. Check for mistakes.
Comment on lines +28 to +33
RefreshToken refreshToken = new RefreshToken();
refreshToken.setToken(UUID.randomUUID().toString());
refreshToken.setExpiryDate(LocalDateTime.now().plusNanos(refreshTokenExpirationMs * 1_000_000));
refreshToken.setRevoked(false);
refreshToken.setUser(user);
return refreshTokenRepository.save(refreshToken);
Copy link

Copilot AI Apr 13, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Refresh token expiry is computed via plusNanos(refreshTokenExpirationMs * 1_000_000). This mixes units and can overflow for larger values; it’s also harder to read than a duration-based approach. Prefer LocalDateTime.now().plus(Duration.ofMillis(refreshTokenExpirationMs)) (or plusMillis) for correctness and clarity.

Copilot uses AI. Check for mistakes.
Comment on lines +101 to +106
int page, int size, String sort) {
Long userId = securityUtils.getCurrentUserId();

Sort sortObj = buildSort(sort);
Pageable pageable = PageRequest.of(page, size, sortObj);

Copy link

Copilot AI Apr 13, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

PageRequest.of(page, size, ...) will throw IllegalArgumentException for negative page or non-positive size (and very large size can be abused). Since these request params aren’t validated, this can currently surface as a 500 via the generic exception handler. Add bean validation (e.g., @Min(0) / @Min(1) / @Max(...) with @Validated), or catch/translate IllegalArgumentException to a 400.

Copilot uses AI. Check for mistakes.
Comment on lines +37 to +41
long interviewsScheduled = applicationRepository.countByUserIdAndInterviewScheduledTrue(userId);

long overdueFollowUps = applicationRepository.findOverdueByUserId(userId, LocalDateTime.now()).size();

long dmRemindersEnabled = applicationRepository.countByUserIdAndRecruiterDmReminderEnabledTrue(userId);
Copy link

Copilot AI Apr 13, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

overdueFollowUps is computed by loading the full overdue list and calling .size(), which can become expensive for users with many applications. Prefer a repository COUNT query (e.g., countOverdueByUserId(...)) so the DB counts without materializing rows.

Copilot uses AI. Check for mistakes.
Comment on lines +21 to +29
CorsConfiguration configuration = new CorsConfiguration();
List<String> origins = Arrays.stream(allowedOrigins.split(","))
.map(String::trim)
.toList();
configuration.setAllowedOriginPatterns(origins);
configuration.setAllowedMethods(List.of("GET", "POST", "PUT", "PATCH", "DELETE", "OPTIONS"));
configuration.setAllowedHeaders(List.of("*"));
configuration.setAllowCredentials(true);
configuration.setMaxAge(3600L);
Copy link

Copilot AI Apr 13, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Using setAllowedOriginPatterns together with allowCredentials(true) is risky because origin patterns can unintentionally broaden to wildcard-like matches, effectively allowing credentialed requests from more origins than intended. If you expect exact origins, prefer setAllowedOrigins (exact match) or validate/restrict patterns to avoid */overly broad patterns.

Copilot uses AI. Check for mistakes.
Comment on lines +89 to +93
userRepository.findByEmail(request.email()).ifPresent(user -> {
PasswordResetToken token = passwordResetService.createResetToken(user);
// In a real application, you would send an email with the reset token
// For now, we log it (do not expose token in response for security)
});
Copy link

Copilot AI Apr 13, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The comment says the reset token is logged, but the code neither logs it nor uses the token variable. This is misleading and may trigger unused-variable warnings. Either remove the variable/comment, or add an explicit (ideally debug-level) log/event hook for delivering the token (email) in real deployments.

Copilot uses AI. Check for mistakes.
Comment on lines +18 to +22
public User getCurrentUser() {
String email = SecurityContextHolder.getContext().getAuthentication().getName();
return userRepository.findByEmail(email)
.orElseThrow(() -> new UnauthorizedException("User not found"));
}
Copy link

Copilot AI Apr 13, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

getCurrentUser() assumes SecurityContextHolder.getContext().getAuthentication() is non-null; if it’s called without an authenticated request context this will NPE rather than returning a controlled 401/Unauthorized exception. Consider explicitly checking for a null/unauthenticated Authentication and throwing UnauthorizedException with a clearer message.

Copilot uses AI. Check for mistakes.
Comment on lines +27 to +41
@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
http
.cors(cors -> cors.configurationSource(corsConfig.corsConfigurationSource()))
.csrf(AbstractHttpConfigurer::disable) // CSRF protection disabled for stateless JWT REST API
.sessionManagement(session -> session.sessionCreationPolicy(SessionCreationPolicy.STATELESS))
.authorizeHttpRequests(auth -> auth
.requestMatchers(HttpMethod.OPTIONS, "/**").permitAll()
.requestMatchers("/api/auth/register", "/api/auth/login",
"/api/auth/refresh", "/api/auth/forgot-password",
"/api/auth/reset-password").permitAll()
.requestMatchers("/v3/api-docs/**", "/swagger-ui/**", "/swagger-ui.html").permitAll()
.anyRequest().authenticated()
)
.addFilterBefore(jwtAuthenticationFilter, UsernamePasswordAuthenticationFilter.class);
Copy link

Copilot AI Apr 13, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Unauthenticated/forbidden responses generated by Spring Security (e.g., missing/invalid JWT) will use default handlers and may not match the structured error format used by GlobalExceptionHandler. If clients expect consistent JSON error bodies, configure exceptionHandling with a custom AuthenticationEntryPoint and AccessDeniedHandler that returns the same shape.

Copilot uses AI. Check for mistakes.
Comment on lines +17 to +24
@Column(nullable = false, unique = true, length = 512)
private String token;

@Column(name = "expiry_date", nullable = false)
private LocalDateTime expiryDate;

@Column(nullable = false)
private boolean revoked = false;
Copy link

Copilot AI Apr 13, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Refresh tokens are stored in plaintext (token column). If the database is ever leaked, these bearer tokens can be used to mint new access tokens until expiry. Consider storing only a hashed token (e.g., SHA-256 of the token value) and comparing hashes on lookup, similar to how passwords are handled, so the raw token is only ever shown once to the client.

Copilot uses AI. Check for mistakes.
Comment on lines +17 to +21
flyway:
enabled: true
locations: classpath:db/migration
baseline-on-migrate: true

Copy link

Copilot AI Apr 13, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

spring.flyway.baseline-on-migrate: true can hide problems by baselining an unexpected pre-existing schema (e.g., pointing at the wrong DB) instead of failing fast. For a greenfield service, consider disabling this (or making it environment-specific) so migrations fail when the schema history table is missing but objects already exist.

Copilot uses AI. Check for mistakes.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants