From 02607728ab7c9bd157aaee4fbed41d4904080295 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Mon, 13 Apr 2026 15:35:17 +0000 Subject: [PATCH 1/4] feat: integrate distributed tracing with OpenTelemetry and Jaeger Agent-Logs-Url: https://github.com/vitorhugo-java/SpringBoot-JobApplyTracker/sessions/609122bb-e5ee-4c48-ad22-bec8f3d6a72f Co-authored-by: vitorhugo-java <65777252+vitorhugo-java@users.noreply.github.com> --- .env.example | 14 +++++ .gitignore | 3 + backend/Dockerfile | 12 ++++ backend/pom.xml | 24 ++++++++ .../service/ApplicationService.java | 24 ++++++-- .../com/jobtracker/service/AuthService.java | 53 +++++++++++------ backend/src/main/resources/application.yml | 15 +++++ backend/src/main/resources/logback-spring.xml | 20 +++++++ .../JobTrackerApplicationTests.java | 3 +- docker-compose.yml | 57 +++++++++++++++++++ 10 files changed, 203 insertions(+), 22 deletions(-) create mode 100644 .env.example create mode 100644 backend/Dockerfile create mode 100644 backend/src/main/resources/logback-spring.xml create mode 100644 docker-compose.yml diff --git a/.env.example b/.env.example new file mode 100644 index 0000000..3014572 --- /dev/null +++ b/.env.example @@ -0,0 +1,14 @@ +# Copy this file to .env and fill in real values. +# The .env file is excluded from version control. + +# Database +DB_NAME=jobtracker +DB_USERNAME=jobtracker +DB_PASSWORD=change_me +DB_ROOT_PASSWORD=change_me_root + +# JWT +JWT_SECRET=replace_with_a_256_bit_random_string + +# Tracing (1.0 = 100 %; lower for production) +TRACING_SAMPLING_PROBABILITY=1.0 diff --git a/.gitignore b/.gitignore index e6e4424..021fc5d 100644 --- a/.gitignore +++ b/.gitignore @@ -28,3 +28,6 @@ target/ # virtual machine crash logs, see http://www.java.com/en/download/help/error_hotspot.xml hs_err_pid* replay_pid* + +# Environment variables – never commit secrets +.env diff --git a/backend/Dockerfile b/backend/Dockerfile new file mode 100644 index 0000000..f52209c --- /dev/null +++ b/backend/Dockerfile @@ -0,0 +1,12 @@ +FROM maven:3.9-eclipse-temurin-21 AS builder +WORKDIR /app +COPY pom.xml . +RUN mvn -B -q dependency:go-offline +COPY src ./src +RUN mvn -B -q package -DskipTests + +FROM eclipse-temurin:21-jre +WORKDIR /app +COPY --from=builder /app/target/*.jar app.jar +EXPOSE 8080 +ENTRYPOINT ["java", "-jar", "app.jar"] diff --git a/backend/pom.xml b/backend/pom.xml index 73940b7..091513e 100644 --- a/backend/pom.xml +++ b/backend/pom.xml @@ -84,6 +84,30 @@ 2.5.0 + + + org.springframework.boot + spring-boot-starter-actuator + + + + + io.micrometer + micrometer-tracing-bridge-otel + + + + + io.opentelemetry + opentelemetry-exporter-otlp + + + + + io.micrometer + micrometer-registry-prometheus + + org.springframework.boot diff --git a/backend/src/main/java/com/jobtracker/service/ApplicationService.java b/backend/src/main/java/com/jobtracker/service/ApplicationService.java index 2bc4094..6cf5ab2 100644 --- a/backend/src/main/java/com/jobtracker/service/ApplicationService.java +++ b/backend/src/main/java/com/jobtracker/service/ApplicationService.java @@ -8,6 +8,8 @@ import com.jobtracker.mapper.ApplicationMapper; import com.jobtracker.repository.ApplicationRepository; import com.jobtracker.util.SecurityUtils; +import io.micrometer.tracing.Span; +import io.micrometer.tracing.Tracer; import jakarta.persistence.criteria.Predicate; import org.springframework.data.domain.Page; import org.springframework.data.domain.PageRequest; @@ -34,21 +36,33 @@ public class ApplicationService { private final ApplicationRepository applicationRepository; private final ApplicationMapper applicationMapper; private final SecurityUtils securityUtils; + private final Tracer tracer; public ApplicationService(ApplicationRepository applicationRepository, ApplicationMapper applicationMapper, - SecurityUtils securityUtils) { + SecurityUtils securityUtils, + Tracer tracer) { this.applicationRepository = applicationRepository; this.applicationMapper = applicationMapper; this.securityUtils = securityUtils; + this.tracer = tracer; } @Transactional public ApplicationResponse create(ApplicationRequest request) { - JobApplication app = new JobApplication(); - mapRequestToEntity(request, app); - app.setUser(securityUtils.getCurrentUser()); - return applicationMapper.toResponse(applicationRepository.save(app)); + Span span = tracer.nextSpan().name("create-application").start(); + try (Tracer.SpanInScope ignored = tracer.withSpan(span)) { + span.tag("vacancy", request.vacancyName() != null ? request.vacancyName() : "unknown"); + JobApplication app = new JobApplication(); + mapRequestToEntity(request, app); + app.setUser(securityUtils.getCurrentUser()); + return applicationMapper.toResponse(applicationRepository.save(app)); + } catch (Exception e) { + span.error(e); + throw e; + } finally { + span.end(); + } } @Transactional(readOnly = true) diff --git a/backend/src/main/java/com/jobtracker/service/AuthService.java b/backend/src/main/java/com/jobtracker/service/AuthService.java index 103c707..71cf2c8 100644 --- a/backend/src/main/java/com/jobtracker/service/AuthService.java +++ b/backend/src/main/java/com/jobtracker/service/AuthService.java @@ -10,6 +10,8 @@ import com.jobtracker.exception.ResourceNotFoundException; import com.jobtracker.mapper.AuthMapper; import com.jobtracker.repository.UserRepository; +import io.micrometer.tracing.Span; +import io.micrometer.tracing.Tracer; import org.springframework.security.authentication.BadCredentialsException; import org.springframework.security.core.userdetails.UserDetails; import org.springframework.security.crypto.password.PasswordEncoder; @@ -27,19 +29,22 @@ public class AuthService { private final RefreshTokenService refreshTokenService; private final PasswordResetService passwordResetService; private final AuthMapper authMapper; + private final Tracer tracer; public AuthService(UserRepository userRepository, PasswordEncoder passwordEncoder, JwtService jwtService, RefreshTokenService refreshTokenService, PasswordResetService passwordResetService, - AuthMapper authMapper) { + AuthMapper authMapper, + Tracer tracer) { this.userRepository = userRepository; this.passwordEncoder = passwordEncoder; this.jwtService = jwtService; this.refreshTokenService = refreshTokenService; this.passwordResetService = passwordResetService; this.authMapper = authMapper; + this.tracer = tracer; } @Transactional @@ -62,26 +67,42 @@ public AuthResponse register(RegisterRequest request) { @Transactional public AuthResponse login(LoginRequest request) { - User user = userRepository.findByEmail(request.email()) - .orElseThrow(() -> new BadCredentialsException("Invalid credentials")); - - if (!passwordEncoder.matches(request.password(), user.getPasswordHash())) { - throw new BadCredentialsException("Invalid credentials"); + Span span = tracer.nextSpan().name("login").start(); + try (Tracer.SpanInScope ignored = tracer.withSpan(span)) { + User user = userRepository.findByEmail(request.email()) + .orElseThrow(() -> new BadCredentialsException("Invalid credentials")); + + if (!passwordEncoder.matches(request.password(), user.getPasswordHash())) { + throw new BadCredentialsException("Invalid credentials"); + } + + return buildAuthResponse(user); + } catch (Exception e) { + span.error(e); + throw e; + } finally { + span.end(); } - - return buildAuthResponse(user); } @Transactional public RefreshResponse refresh(RefreshTokenRequest request) { - RefreshToken newRefreshToken = refreshTokenService.verifyAndRotate(request.refreshToken()); - User user = newRefreshToken.getUser(); - - UserDetails userDetails = new org.springframework.security.core.userdetails.User( - user.getEmail(), user.getPasswordHash(), Collections.emptyList()); - String accessToken = jwtService.generateToken(userDetails); - - return new RefreshResponse(accessToken, newRefreshToken.getToken()); + Span span = tracer.nextSpan().name("token-refresh").start(); + try (Tracer.SpanInScope ignored = tracer.withSpan(span)) { + RefreshToken newRefreshToken = refreshTokenService.verifyAndRotate(request.refreshToken()); + User user = newRefreshToken.getUser(); + + UserDetails userDetails = new org.springframework.security.core.userdetails.User( + user.getEmail(), user.getPasswordHash(), Collections.emptyList()); + String accessToken = jwtService.generateToken(userDetails); + + return new RefreshResponse(accessToken, newRefreshToken.getToken()); + } catch (Exception e) { + span.error(e); + throw e; + } finally { + span.end(); + } } @Transactional diff --git a/backend/src/main/resources/application.yml b/backend/src/main/resources/application.yml index 3b46e74..e05d7d8 100644 --- a/backend/src/main/resources/application.yml +++ b/backend/src/main/resources/application.yml @@ -35,3 +35,18 @@ springdoc: path: /v3/api-docs swagger-ui: path: /swagger-ui.html + +management: + endpoints: + web: + exposure: + include: health,info,prometheus,metrics,tracing + tracing: + sampling: + probability: ${TRACING_SAMPLING_PROBABILITY:1.0} + otlp: + tracing: + endpoint: ${OTEL_EXPORTER_OTLP_ENDPOINT:http://localhost:4317} + metrics: + tags: + application: ${spring.application.name} diff --git a/backend/src/main/resources/logback-spring.xml b/backend/src/main/resources/logback-spring.xml new file mode 100644 index 0000000..bd5a90c --- /dev/null +++ b/backend/src/main/resources/logback-spring.xml @@ -0,0 +1,20 @@ + + + + + + + + + %d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level [%X{traceId:-}] [%X{spanId:-}] %logger{36} - %msg%n + + + + + + + + + + + diff --git a/backend/src/test/java/com/jobtracker/JobTrackerApplicationTests.java b/backend/src/test/java/com/jobtracker/JobTrackerApplicationTests.java index 95cb03b..2f16b81 100644 --- a/backend/src/test/java/com/jobtracker/JobTrackerApplicationTests.java +++ b/backend/src/test/java/com/jobtracker/JobTrackerApplicationTests.java @@ -15,7 +15,8 @@ "jwt.secret=TestSecretKeyThatIsAtLeast256BitsLongForTestingPurposesOnly", "jwt.access-token-expiration-ms=900000", "jwt.refresh-token-expiration-ms=604800000", - "cors.allowed-origins=http://localhost:3000" + "cors.allowed-origins=http://localhost:3000", + "management.tracing.sampling.probability=0.0" }) class JobTrackerApplicationTests { diff --git a/docker-compose.yml b/docker-compose.yml new file mode 100644 index 0000000..ddea3fd --- /dev/null +++ b/docker-compose.yml @@ -0,0 +1,57 @@ +version: "3.9" + +services: + + db: + image: mariadb:11 + container_name: jobtracker-db + restart: unless-stopped + environment: + MARIADB_DATABASE: ${DB_NAME:-jobtracker} + MARIADB_USER: ${DB_USERNAME:-jobtracker} + MARIADB_PASSWORD: ${DB_PASSWORD:-jobtracker} + MARIADB_ROOT_PASSWORD: ${DB_ROOT_PASSWORD:-root} + ports: + - "3306:3306" + volumes: + - db_data:/var/lib/mysql + healthcheck: + test: ["CMD", "healthcheck.sh", "--connect", "--innodb_initialized"] + interval: 10s + timeout: 5s + retries: 5 + + jaeger: + image: jaegertracing/all-in-one:latest + container_name: jobtracker-jaeger + restart: unless-stopped + ports: + - "16686:16686" # Jaeger UI + - "4317:4317" # OTLP gRPC receiver + - "4318:4318" # OTLP HTTP receiver + environment: + - COLLECTOR_OTLP_ENABLED=true + + app: + build: + context: ./backend + dockerfile: Dockerfile + container_name: jobtracker-app + restart: unless-stopped + depends_on: + db: + condition: service_healthy + jaeger: + condition: service_started + ports: + - "8080:8080" + environment: + DB_URL: jdbc:mariadb://db:3306/${DB_NAME:-jobtracker}?createDatabaseIfNotExist=true&characterEncoding=UTF-8&useUnicode=true + DB_USERNAME: ${DB_USERNAME:-jobtracker} + DB_PASSWORD: ${DB_PASSWORD:-jobtracker} + OTEL_EXPORTER_OTLP_ENDPOINT: http://jaeger:4317 + TRACING_SAMPLING_PROBABILITY: ${TRACING_SAMPLING_PROBABILITY:-1.0} + SERVER_PORT: 8080 + +volumes: + db_data: From a03d5d6cfffc01349b462af7c768ede8c44a63d2 Mon Sep 17 00:00:00 2001 From: Vitor Hugo Date: Mon, 13 Apr 2026 17:56:42 +0000 Subject: [PATCH 2/4] feat: refine error handling in AuthService, adjust logging levels, and update Jaeger image version --- .../java/com/jobtracker/service/AuthService.java | 8 ++++++-- backend/src/main/resources/application.yml | 2 +- backend/src/main/resources/logback-spring.xml | 12 +++++++++--- docker-compose.yml | 2 +- 4 files changed, 17 insertions(+), 7 deletions(-) diff --git a/backend/src/main/java/com/jobtracker/service/AuthService.java b/backend/src/main/java/com/jobtracker/service/AuthService.java index 71cf2c8..8b0ff34 100644 --- a/backend/src/main/java/com/jobtracker/service/AuthService.java +++ b/backend/src/main/java/com/jobtracker/service/AuthService.java @@ -76,7 +76,9 @@ public AuthResponse login(LoginRequest request) { throw new BadCredentialsException("Invalid credentials"); } - return buildAuthResponse(user); + return buildAuthResponse(user); + } catch (BadCredentialsException e) { + throw e; } catch (Exception e) { span.error(e); throw e; @@ -96,7 +98,9 @@ public RefreshResponse refresh(RefreshTokenRequest request) { user.getEmail(), user.getPasswordHash(), Collections.emptyList()); String accessToken = jwtService.generateToken(userDetails); - return new RefreshResponse(accessToken, newRefreshToken.getToken()); + return buildAuthResponse(user); + } catch (BadCredentialsException e) { + throw e; } catch (Exception e) { span.error(e); throw e; diff --git a/backend/src/main/resources/application.yml b/backend/src/main/resources/application.yml index e05d7d8..9901d74 100644 --- a/backend/src/main/resources/application.yml +++ b/backend/src/main/resources/application.yml @@ -40,7 +40,7 @@ management: endpoints: web: exposure: - include: health,info,prometheus,metrics,tracing + include: health,info,prometheus,metrics tracing: sampling: probability: ${TRACING_SAMPLING_PROBABILITY:1.0} diff --git a/backend/src/main/resources/logback-spring.xml b/backend/src/main/resources/logback-spring.xml index bd5a90c..b88b9c2 100644 --- a/backend/src/main/resources/logback-spring.xml +++ b/backend/src/main/resources/logback-spring.xml @@ -14,7 +14,13 @@ - - - + + + + + + + + + diff --git a/docker-compose.yml b/docker-compose.yml index ddea3fd..31b16be 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -22,7 +22,7 @@ services: retries: 5 jaeger: - image: jaegertracing/all-in-one:latest + image: jaegertracing/all-in-one:1.57.0 container_name: jobtracker-jaeger restart: unless-stopped ports: From 9e1af50941197533414d7d2d885059c5b6940d03 Mon Sep 17 00:00:00 2001 From: Vitor Hugo Date: Mon, 13 Apr 2026 17:59:01 +0000 Subject: [PATCH 3/4] conflict --- Dockerfile.orig | 22 +- .../JobTrackerApplicationTests.java.orig | 26 +++ docker-compose.yml.orig | 69 +++++- pom.xml.orig | 197 ++++++++++++++++++ .../jobtracker/service/AuthService.java.orig | 183 ++++++++++++++++ src/main/resources/application.yml.orig | 82 ++++++++ src/main/resources/logback-spring.xml.orig | 31 ++- 7 files changed, 600 insertions(+), 10 deletions(-) create mode 100644 backend/src/test/java/com/jobtracker/JobTrackerApplicationTests.java.orig create mode 100644 pom.xml.orig create mode 100644 src/main/java/com/jobtracker/service/AuthService.java.orig create mode 100644 src/main/resources/application.yml.orig diff --git a/Dockerfile.orig b/Dockerfile.orig index 06d3ddf..acb89d8 100644 --- a/Dockerfile.orig +++ b/Dockerfile.orig @@ -1,3 +1,17 @@ +<<<<<<< HEAD:backend/Dockerfile +FROM maven:3.9-eclipse-temurin-21 AS builder +WORKDIR /app +COPY pom.xml . +RUN mvn -B -q dependency:go-offline +COPY src ./src +RUN mvn -B -q package -DskipTests + +FROM eclipse-temurin:21-jre +WORKDIR /app +COPY --from=builder /app/target/*.jar app.jar +EXPOSE 8080 +ENTRYPOINT ["java", "-jar", "app.jar"] +======= # ============================================================ # Stage 1: Build # ============================================================ @@ -36,12 +50,13 @@ RUN chown -R appuser:appgroup /app USER appuser -# Expose application port +# Expose application port and management port EXPOSE 8080 +EXPOSE 8081 -# Health check – relies on Spring Actuator +# Health check – relies on Spring Actuator management port HEALTHCHECK --interval=30s --timeout=10s --start-period=60s --retries=3 \ - CMD wget -qO- http://localhost:8080/actuator/health || exit 1 + CMD wget -qO- http://localhost:8081/actuator/health || exit 1 # JVM tuning flags for containers (respects cgroup memory limits) ENV JAVA_OPTS="-XX:+UseContainerSupport \ @@ -50,3 +65,4 @@ ENV JAVA_OPTS="-XX:+UseContainerSupport \ -Djava.security.egd=file:/dev/./urandom" ENTRYPOINT ["sh", "-c", "exec java $JAVA_OPTS -jar app.jar"] +>>>>>>> origin/main:Dockerfile diff --git a/backend/src/test/java/com/jobtracker/JobTrackerApplicationTests.java.orig b/backend/src/test/java/com/jobtracker/JobTrackerApplicationTests.java.orig new file mode 100644 index 0000000..2f16b81 --- /dev/null +++ b/backend/src/test/java/com/jobtracker/JobTrackerApplicationTests.java.orig @@ -0,0 +1,26 @@ +package com.jobtracker; + +import org.junit.jupiter.api.Test; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.test.context.TestPropertySource; + +@SpringBootTest +@TestPropertySource(properties = { + "spring.datasource.url=jdbc:h2:mem:testdb;MODE=MySQL;DB_CLOSE_DELAY=-1", + "spring.datasource.driver-class-name=org.h2.Driver", + "spring.datasource.username=sa", + "spring.datasource.password=", + "spring.jpa.hibernate.ddl-auto=create-drop", + "spring.flyway.enabled=false", + "jwt.secret=TestSecretKeyThatIsAtLeast256BitsLongForTestingPurposesOnly", + "jwt.access-token-expiration-ms=900000", + "jwt.refresh-token-expiration-ms=604800000", + "cors.allowed-origins=http://localhost:3000", + "management.tracing.sampling.probability=0.0" +}) +class JobTrackerApplicationTests { + + @Test + void contextLoads() { + } +} diff --git a/docker-compose.yml.orig b/docker-compose.yml.orig index 5600e15..6d4c574 100644 --- a/docker-compose.yml.orig +++ b/docker-compose.yml.orig @@ -2,6 +2,21 @@ version: "3.9" services: +<<<<<<< HEAD + db: + image: mariadb:11 + container_name: jobtracker-db + restart: unless-stopped + environment: + MARIADB_DATABASE: ${DB_NAME:-jobtracker} + MARIADB_USER: ${DB_USERNAME:-jobtracker} + MARIADB_PASSWORD: ${DB_PASSWORD:-jobtracker} + MARIADB_ROOT_PASSWORD: ${DB_ROOT_PASSWORD:-root} + ports: + - "3306:3306" + volumes: + - db_data:/var/lib/mysql +======= # ── Spring Boot application ─────────────────────────────── app: build: @@ -21,7 +36,7 @@ services: mariadb: condition: service_healthy healthcheck: - test: ["CMD", "wget", "-qO-", "http://localhost:8080/actuator/health"] + test: ["CMD", "wget", "-qO-", "http://localhost:8081/actuator/health"] interval: 30s timeout: 10s retries: 5 @@ -41,11 +56,49 @@ services: MARIADB_DATABASE: jobtracker volumes: - mariadb_data:/var/lib/mysql +>>>>>>> origin/main healthcheck: test: ["CMD", "healthcheck.sh", "--connect", "--innodb_initialized"] interval: 10s timeout: 5s retries: 5 +<<<<<<< HEAD + + jaeger: + image: jaegertracing/all-in-one:1.57.0 + container_name: jobtracker-jaeger + restart: unless-stopped + ports: + - "16686:16686" # Jaeger UI + - "4317:4317" # OTLP gRPC receiver + - "4318:4318" # OTLP HTTP receiver + environment: + - COLLECTOR_OTLP_ENABLED=true + + app: + build: + context: ./backend + dockerfile: Dockerfile + container_name: jobtracker-app + restart: unless-stopped + depends_on: + db: + condition: service_healthy + jaeger: + condition: service_started + ports: + - "8080:8080" + environment: + DB_URL: jdbc:mariadb://db:3306/${DB_NAME:-jobtracker}?createDatabaseIfNotExist=true&characterEncoding=UTF-8&useUnicode=true + DB_USERNAME: ${DB_USERNAME:-jobtracker} + DB_PASSWORD: ${DB_PASSWORD:-jobtracker} + OTEL_EXPORTER_OTLP_ENDPOINT: http://jaeger:4317 + TRACING_SAMPLING_PROBABILITY: ${TRACING_SAMPLING_PROBABILITY:-1.0} + SERVER_PORT: 8080 + +volumes: + db_data: +======= start_period: 30s networks: - job-tracker-net @@ -53,7 +106,7 @@ services: # ── Prometheus ──────────────────────────────────────────── prometheus: - image: prom/prometheus:latest + image: prom/prometheus:v2.54.1 container_name: job-tracker-prometheus ports: - "9090:9090" @@ -65,13 +118,19 @@ services: - "--storage.tsdb.path=/prometheus" - "--web.console.libraries=/usr/share/prometheus/console_libraries" - "--web.console.templates=/usr/share/prometheus/consoles" + healthcheck: + test: ["CMD", "wget", "-qO-", "http://localhost:9090/-/healthy"] + interval: 15s + timeout: 5s + retries: 3 + start_period: 15s networks: - job-tracker-net restart: unless-stopped # ── Grafana ─────────────────────────────────────────────── grafana: - image: grafana/grafana:latest + image: grafana/grafana:10.4.8 container_name: job-tracker-grafana ports: - "3000:3000" @@ -84,7 +143,8 @@ services: - ./grafana/provisioning/dashboards:/etc/grafana/provisioning/dashboards:ro - ./grafana/dashboards:/etc/grafana/dashboards:ro depends_on: - - prometheus + prometheus: + condition: service_healthy networks: - job-tracker-net restart: unless-stopped @@ -97,3 +157,4 @@ volumes: networks: job-tracker-net: driver: bridge +>>>>>>> origin/main diff --git a/pom.xml.orig b/pom.xml.orig new file mode 100644 index 0000000..96ffbcd --- /dev/null +++ b/pom.xml.orig @@ -0,0 +1,197 @@ + + + 4.0.0 + + + org.springframework.boot + spring-boot-starter-parent + 3.2.5 + + + + com.jobtracker + job-tracker + 1.0.0 + job-tracker + Spring Boot Job Application Tracker API + + + 21 + 0.12.5 + 1.19.8 + 5.4.0 + + + + + + org.springframework.boot + spring-boot-starter-web + + + org.springframework.boot + spring-boot-starter-validation + + + org.springframework.boot + spring-boot-starter-data-jpa + + + org.springframework.boot + spring-boot-starter-security + + + + + org.mariadb.jdbc + mariadb-java-client + runtime + + + + + org.flywaydb + flyway-core + + + org.flywaydb + flyway-mysql + + + + + io.jsonwebtoken + jjwt-api + ${jjwt.version} + + + io.jsonwebtoken + jjwt-impl + ${jjwt.version} + runtime + + + io.jsonwebtoken + jjwt-jackson + ${jjwt.version} + runtime + + + + + org.springdoc + springdoc-openapi-starter-webmvc-ui + 2.5.0 + + +<<<<<<< HEAD:backend/pom.xml + +======= + +>>>>>>> origin/main:pom.xml + + org.springframework.boot + spring-boot-starter-actuator + + +<<<<<<< HEAD:backend/pom.xml + + + io.micrometer + micrometer-tracing-bridge-otel + + + + + io.opentelemetry + opentelemetry-exporter-otlp + + + +======= + +>>>>>>> origin/main:pom.xml + + io.micrometer + micrometer-registry-prometheus + + +<<<<<<< HEAD:backend/pom.xml +======= + + + net.logstash.logback + logstash-logback-encoder + 7.4 + + +>>>>>>> origin/main:pom.xml + + + org.springframework.boot + spring-boot-starter-test + test + + + org.springframework.security + spring-security-test + test + + + + + org.testcontainers + junit-jupiter + ${testcontainers.version} + test + + + org.testcontainers + mariadb + ${testcontainers.version} + test + + + + + io.rest-assured + rest-assured + ${rest-assured.version} + test + + + io.rest-assured + json-path + ${rest-assured.version} + test + + + io.rest-assured + spring-mock-mvc + ${rest-assured.version} + test + + + + + + + org.springframework.boot + spring-boot-maven-plugin + + + org.apache.maven.plugins + maven-surefire-plugin + + + **/*Test.java + **/*Tests.java + **/*IT.java + + + + + + diff --git a/src/main/java/com/jobtracker/service/AuthService.java.orig b/src/main/java/com/jobtracker/service/AuthService.java.orig new file mode 100644 index 0000000..a83cd0c --- /dev/null +++ b/src/main/java/com/jobtracker/service/AuthService.java.orig @@ -0,0 +1,183 @@ +package com.jobtracker.service; + +import com.jobtracker.config.JwtService; +import com.jobtracker.dto.auth.*; +import com.jobtracker.entity.PasswordResetToken; +import com.jobtracker.entity.RefreshToken; +import com.jobtracker.entity.User; +import com.jobtracker.exception.BadRequestException; +import com.jobtracker.exception.ConflictException; +import com.jobtracker.exception.ResourceNotFoundException; +import com.jobtracker.mapper.AuthMapper; +import com.jobtracker.repository.UserRepository; +<<<<<<< HEAD:backend/src/main/java/com/jobtracker/service/AuthService.java +import io.micrometer.tracing.Span; +import io.micrometer.tracing.Tracer; +======= +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +>>>>>>> origin/main:src/main/java/com/jobtracker/service/AuthService.java +import org.springframework.security.authentication.BadCredentialsException; +import org.springframework.security.core.Authentication; +import org.springframework.security.core.context.SecurityContextHolder; +import org.springframework.security.core.userdetails.UserDetails; +import org.springframework.security.crypto.password.PasswordEncoder; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import java.util.Collections; + +@Service +public class AuthService { + + private static final Logger log = LoggerFactory.getLogger(AuthService.class); + + private final UserRepository userRepository; + private final PasswordEncoder passwordEncoder; + private final JwtService jwtService; + private final RefreshTokenService refreshTokenService; + private final PasswordResetService passwordResetService; + private final AuthMapper authMapper; + private final Tracer tracer; + + public AuthService(UserRepository userRepository, + PasswordEncoder passwordEncoder, + JwtService jwtService, + RefreshTokenService refreshTokenService, + PasswordResetService passwordResetService, + AuthMapper authMapper, + Tracer tracer) { + this.userRepository = userRepository; + this.passwordEncoder = passwordEncoder; + this.jwtService = jwtService; + this.refreshTokenService = refreshTokenService; + this.passwordResetService = passwordResetService; + this.authMapper = authMapper; + this.tracer = tracer; + } + + @Transactional + public AuthResponse register(RegisterRequest request) { + if (!request.password().equals(request.confirmPassword())) { + throw new BadRequestException("Passwords do not match"); + } + if (userRepository.existsByEmail(request.email())) { + throw new ConflictException("Email already registered"); + } + + User user = new User(); + user.setName(request.name()); + user.setEmail(request.email()); + user.setPasswordHash(passwordEncoder.encode(request.password())); + user = userRepository.save(user); + log.info("event=REGISTRATION_SUCCESS email={} userId={}", user.getEmail(), user.getId()); + return buildAuthResponse(user); + } + + @Transactional + public AuthResponse login(LoginRequest request) { +<<<<<<< HEAD:backend/src/main/java/com/jobtracker/service/AuthService.java + Span span = tracer.nextSpan().name("login").start(); + try (Tracer.SpanInScope ignored = tracer.withSpan(span)) { + User user = userRepository.findByEmail(request.email()) + .orElseThrow(() -> new BadCredentialsException("Invalid credentials")); + + if (!passwordEncoder.matches(request.password(), user.getPasswordHash())) { + throw new BadCredentialsException("Invalid credentials"); + } + + return buildAuthResponse(user); + } catch (BadCredentialsException e) { + throw e; + } catch (Exception e) { + span.error(e); + throw e; + } finally { + span.end(); + } +======= + User user = userRepository.findByEmail(request.email()) + .orElseThrow(() -> { + log.warn("event=LOGIN_FAILURE reason=USER_NOT_FOUND email={}", request.email()); + return new BadCredentialsException("Invalid credentials"); + }); + + if (!passwordEncoder.matches(request.password(), user.getPasswordHash())) { + log.warn("event=LOGIN_FAILURE reason=WRONG_PASSWORD userId={}", user.getId()); + throw new BadCredentialsException("Invalid credentials"); + } + + log.info("event=LOGIN_SUCCESS userId={}", user.getId()); + return buildAuthResponse(user); +>>>>>>> origin/main:src/main/java/com/jobtracker/service/AuthService.java + } + + @Transactional + public RefreshResponse refresh(RefreshTokenRequest request) { + Span span = tracer.nextSpan().name("token-refresh").start(); + try (Tracer.SpanInScope ignored = tracer.withSpan(span)) { + RefreshToken newRefreshToken = refreshTokenService.verifyAndRotate(request.refreshToken()); + User user = newRefreshToken.getUser(); + + UserDetails userDetails = new org.springframework.security.core.userdetails.User( + user.getEmail(), user.getPasswordHash(), Collections.emptyList()); + String accessToken = jwtService.generateToken(userDetails); + + return buildAuthResponse(user); + } catch (BadCredentialsException e) { + throw e; + } catch (Exception e) { + span.error(e); + throw e; + } finally { + span.end(); + } + } + + @Transactional + public MessageResponse forgotPassword(ForgotPasswordRequest request) { + 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) + }); + // Always return success to prevent email enumeration + return new MessageResponse("If an account with that email exists, a password reset link has been sent"); + } + + @Transactional + public MessageResponse resetPassword(ResetPasswordRequest request) { + if (!request.newPassword().equals(request.confirmPassword())) { + throw new BadRequestException("Passwords do not match"); + } + + PasswordResetToken resetToken = passwordResetService.verifyToken(request.token()); + User user = resetToken.getUser(); + user.setPasswordHash(passwordEncoder.encode(request.newPassword())); + userRepository.save(user); + + passwordResetService.markTokenAsUsed(resetToken); + // Revoke all existing refresh tokens for security + refreshTokenService.revokeAllByUserId(user.getId()); + + return new MessageResponse("Password has been reset successfully"); + } + + @Transactional + public MessageResponse logout(LogoutRequest request) { + refreshTokenService.revokeToken(request.refreshToken()); + Authentication auth = SecurityContextHolder.getContext().getAuthentication(); + String userEmail = (auth != null && auth.isAuthenticated()) ? auth.getName() : "unknown"; + log.info("event=LOGOUT_SUCCESS userEmail={}", userEmail); + return new MessageResponse("Logged out successfully"); + } + + private AuthResponse buildAuthResponse(User user) { + UserDetails userDetails = new org.springframework.security.core.userdetails.User( + user.getEmail(), user.getPasswordHash(), Collections.emptyList()); + String accessToken = jwtService.generateToken(userDetails); + RefreshToken refreshToken = refreshTokenService.createRefreshToken(user); + + return new AuthResponse(accessToken, refreshToken.getToken(), authMapper.toUserResponse(user)); + } +} diff --git a/src/main/resources/application.yml.orig b/src/main/resources/application.yml.orig new file mode 100644 index 0000000..fc49375 --- /dev/null +++ b/src/main/resources/application.yml.orig @@ -0,0 +1,82 @@ +spring: + application: + name: job-tracker + datasource: + url: ${DB_URL:jdbc:mariadb://localhost:3306/jobtracker?createDatabaseIfNotExist=true&characterEncoding=UTF-8&useUnicode=true} + username: ${DB_USERNAME:jobtracker} + password: ${DB_PASSWORD:jobtracker} + driver-class-name: org.mariadb.jdbc.Driver + jpa: + hibernate: + ddl-auto: validate + show-sql: false + properties: + hibernate: + dialect: org.hibernate.dialect.MariaDBDialect + format_sql: true + flyway: + enabled: true + locations: classpath:db/migration + baseline-on-migrate: true + +server: + port: ${SERVER_PORT:8080} + +jwt: + secret: ${JWT_SECRET:ThisIsAVeryLongSecretKeyForJWTTokensAndItMustBeAtLeast256BitsLong} + access-token-expiration-ms: ${JWT_ACCESS_TOKEN_EXPIRATION_MS:900000} + refresh-token-expiration-ms: ${JWT_REFRESH_TOKEN_EXPIRATION_MS:604800000} + +cors: + allowed-origins: ${CORS_ALLOWED_ORIGINS:http://localhost:3000,http://localhost:5173} + +management: + server: + port: ${MANAGEMENT_PORT:8081} + endpoints: + web: + exposure: + include: health,info,metrics,prometheus + endpoint: + health: + show-details: when_authorized + metrics: + tags: + application: ${spring.application.name} + distribution: + percentiles-histogram: + http.server.requests: true + +springdoc: + api-docs: + path: /v3/api-docs + swagger-ui: + path: /swagger-ui.html +<<<<<<< HEAD:backend/src/main/resources/application.yml + +management: + endpoints: + web: + exposure: + include: health,info,prometheus,metrics + tracing: + sampling: + probability: ${TRACING_SAMPLING_PROBABILITY:1.0} + otlp: + tracing: + endpoint: ${OTEL_EXPORTER_OTLP_ENDPOINT:http://localhost:4317} + metrics: + tags: + application: ${spring.application.name} +======= + group-configs: + - group: auth + display-name: Auth API + paths-to-match: /api/auth/** + - group: applications + display-name: Application API + paths-to-match: /api/applications/** + - group: dashboard + display-name: Dashboard API + paths-to-match: /api/dashboard/** +>>>>>>> origin/main:src/main/resources/application.yml diff --git a/src/main/resources/logback-spring.xml.orig b/src/main/resources/logback-spring.xml.orig index 3e3368c..a4cc835 100644 --- a/src/main/resources/logback-spring.xml.orig +++ b/src/main/resources/logback-spring.xml.orig @@ -1,12 +1,36 @@ +<<<<<<< HEAD:backend/src/main/resources/logback-spring.xml + + + + + + + %d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level [%X{traceId:-}] [%X{spanId:-}] %logger{36} - %msg%n + + + + + + + + + + + + + + + +======= - + - - {"timestamp":"%d{yyyy-MM-dd'T'HH:mm:ss.SSSZ}","level":"%level","app":"${appName}","logger":"%logger{36}","thread":"%thread","message":"%msg"}%n + + {"app":"${appName}"} @@ -37,5 +61,6 @@ +>>>>>>> origin/main:src/main/resources/logback-spring.xml From c74fb51095f340c1eacbc9d3241cf47f53fcfc08 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Mon, 13 Apr 2026 18:21:48 +0000 Subject: [PATCH 4/4] fix: resolve merge conflicts and apply reviewer feedback MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Resolve all 6 conflicted files (pom.xml, application.yml, logback-spring.xml, AuthService.java, Dockerfile, docker-compose.yml) - Remove 4 leftover merge-artifact files (docker-compose_BACKUP/BASE/LOCAL/REMOTE) - Remove all .orig backup files - pom.xml: keep tracing deps + logstash + testcontainers (both sides merged) - application.yml: single management block with tracing/OTLP config + springdoc groups - logback-spring.xml: origin/main structure with traceId/spanId added to CONSOLE pattern - AuthService: do not call span.error() for BadCredentialsException; fix refresh() to correctly return RefreshResponse instead of AuthResponse - ApplicationService: remove high-cardinality vacancy tag from span - Dockerfile: use maven:3.9-eclipse-temurin-21 image; add non-root user; root-level paths - docker-compose.yml: full merge – Jaeger 1.57.0 + Prometheus/Grafana; require DB passwords via :? syntax; env-var driven JWT_SECRET - ApplicationServiceTest/AuthServiceTest: add @Mock(RETURNS_DEEP_STUBS) Tracer - application-test.yml: disable tracing (probability=0.0) in test profile Agent-Logs-Url: https://github.com/vitorhugo-java/SpringBoot-JobApplyTracker/sessions/1f21e07a-4b0e-477b-b5db-c700a3631326 Co-authored-by: vitorhugo-java <65777252+vitorhugo-java@users.noreply.github.com> --- Dockerfile | 29 +-- Dockerfile.orig | 68 ------ .../JobTrackerApplicationTests.java.orig | 26 --- docker-compose.yml | 80 ++----- docker-compose.yml.orig | 160 -------------- docker-compose_BACKUP_20088.yml | 99 --------- docker-compose_BASE_20088.yml | 0 docker-compose_LOCAL_20088.yml | 99 --------- docker-compose_REMOTE_20088.yml | 41 ---- pom.xml | 12 -- pom.xml.orig | 197 ------------------ .../config/RequestLoggingFilter.java.orig | 48 ----- .../GlobalExceptionHandler.java.orig | 81 ------- .../service/ApplicationService.java | 1 - .../com/jobtracker/service/AuthService.java | 37 +--- .../jobtracker/service/AuthService.java.orig | 183 ---------------- src/main/resources/application.yml | 24 +-- src/main/resources/application.yml.orig | 82 -------- src/main/resources/logback-spring.xml | 33 +-- src/main/resources/logback-spring.xml.orig | 66 ------ .../unit/ApplicationServiceTest.java | 3 + .../com/jobtracker/unit/AuthServiceTest.java | 3 + src/test/resources/application-test.yml | 5 + 23 files changed, 59 insertions(+), 1318 deletions(-) delete mode 100644 Dockerfile.orig delete mode 100644 backend/src/test/java/com/jobtracker/JobTrackerApplicationTests.java.orig delete mode 100644 docker-compose.yml.orig delete mode 100644 docker-compose_BACKUP_20088.yml delete mode 100644 docker-compose_BASE_20088.yml delete mode 100644 docker-compose_LOCAL_20088.yml delete mode 100644 docker-compose_REMOTE_20088.yml delete mode 100644 pom.xml.orig delete mode 100644 src/main/java/com/jobtracker/config/RequestLoggingFilter.java.orig delete mode 100644 src/main/java/com/jobtracker/exception/GlobalExceptionHandler.java.orig delete mode 100644 src/main/java/com/jobtracker/service/AuthService.java.orig delete mode 100644 src/main/resources/application.yml.orig delete mode 100644 src/main/resources/logback-spring.xml.orig diff --git a/Dockerfile b/Dockerfile index acb89d8..994a491 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,35 +1,18 @@ -<<<<<<< HEAD:backend/Dockerfile -FROM maven:3.9-eclipse-temurin-21 AS builder -WORKDIR /app -COPY pom.xml . -RUN mvn -B -q dependency:go-offline -COPY src ./src -RUN mvn -B -q package -DskipTests - -FROM eclipse-temurin:21-jre -WORKDIR /app -COPY --from=builder /app/target/*.jar app.jar -EXPOSE 8080 -ENTRYPOINT ["java", "-jar", "app.jar"] -======= # ============================================================ # Stage 1: Build # ============================================================ -FROM eclipse-temurin:21-jdk-alpine AS build - -# Install Maven -RUN apk add --no-cache maven +FROM maven:3.9-eclipse-temurin-21 AS build WORKDIR /workspace # Copy POM first to cache dependency layer -COPY backend/pom.xml . +COPY pom.xml . # Download dependencies (cached unless pom.xml changes) RUN mvn dependency:go-offline -B --no-transfer-progress # Copy source code and build the fat JAR, skipping tests -COPY backend/src src +COPY src src RUN mvn package -DskipTests -B --no-transfer-progress # ============================================================ @@ -43,10 +26,7 @@ RUN addgroup -S appgroup && adduser -S appuser -G appgroup WORKDIR /app # Copy the built JAR from the build stage -COPY --from=build /workspace/target/*.jar app.jar - -# Ensure the app directory is owned by the non-root user -RUN chown -R appuser:appgroup /app +COPY --from=build --chown=appuser:appgroup /workspace/target/*.jar app.jar USER appuser @@ -65,4 +45,3 @@ ENV JAVA_OPTS="-XX:+UseContainerSupport \ -Djava.security.egd=file:/dev/./urandom" ENTRYPOINT ["sh", "-c", "exec java $JAVA_OPTS -jar app.jar"] ->>>>>>> origin/main:Dockerfile diff --git a/Dockerfile.orig b/Dockerfile.orig deleted file mode 100644 index acb89d8..0000000 --- a/Dockerfile.orig +++ /dev/null @@ -1,68 +0,0 @@ -<<<<<<< HEAD:backend/Dockerfile -FROM maven:3.9-eclipse-temurin-21 AS builder -WORKDIR /app -COPY pom.xml . -RUN mvn -B -q dependency:go-offline -COPY src ./src -RUN mvn -B -q package -DskipTests - -FROM eclipse-temurin:21-jre -WORKDIR /app -COPY --from=builder /app/target/*.jar app.jar -EXPOSE 8080 -ENTRYPOINT ["java", "-jar", "app.jar"] -======= -# ============================================================ -# Stage 1: Build -# ============================================================ -FROM eclipse-temurin:21-jdk-alpine AS build - -# Install Maven -RUN apk add --no-cache maven - -WORKDIR /workspace - -# Copy POM first to cache dependency layer -COPY backend/pom.xml . - -# Download dependencies (cached unless pom.xml changes) -RUN mvn dependency:go-offline -B --no-transfer-progress - -# Copy source code and build the fat JAR, skipping tests -COPY backend/src src -RUN mvn package -DskipTests -B --no-transfer-progress - -# ============================================================ -# Stage 2: Runtime -# ============================================================ -FROM eclipse-temurin:21-jre-alpine AS runtime - -# Create a non-root user for security -RUN addgroup -S appgroup && adduser -S appuser -G appgroup - -WORKDIR /app - -# Copy the built JAR from the build stage -COPY --from=build /workspace/target/*.jar app.jar - -# Ensure the app directory is owned by the non-root user -RUN chown -R appuser:appgroup /app - -USER appuser - -# Expose application port and management port -EXPOSE 8080 -EXPOSE 8081 - -# Health check – relies on Spring Actuator management port -HEALTHCHECK --interval=30s --timeout=10s --start-period=60s --retries=3 \ - CMD wget -qO- http://localhost:8081/actuator/health || exit 1 - -# JVM tuning flags for containers (respects cgroup memory limits) -ENV JAVA_OPTS="-XX:+UseContainerSupport \ - -XX:MaxRAMPercentage=75.0 \ - -XX:+ExitOnOutOfMemoryError \ - -Djava.security.egd=file:/dev/./urandom" - -ENTRYPOINT ["sh", "-c", "exec java $JAVA_OPTS -jar app.jar"] ->>>>>>> origin/main:Dockerfile diff --git a/backend/src/test/java/com/jobtracker/JobTrackerApplicationTests.java.orig b/backend/src/test/java/com/jobtracker/JobTrackerApplicationTests.java.orig deleted file mode 100644 index 2f16b81..0000000 --- a/backend/src/test/java/com/jobtracker/JobTrackerApplicationTests.java.orig +++ /dev/null @@ -1,26 +0,0 @@ -package com.jobtracker; - -import org.junit.jupiter.api.Test; -import org.springframework.boot.test.context.SpringBootTest; -import org.springframework.test.context.TestPropertySource; - -@SpringBootTest -@TestPropertySource(properties = { - "spring.datasource.url=jdbc:h2:mem:testdb;MODE=MySQL;DB_CLOSE_DELAY=-1", - "spring.datasource.driver-class-name=org.h2.Driver", - "spring.datasource.username=sa", - "spring.datasource.password=", - "spring.jpa.hibernate.ddl-auto=create-drop", - "spring.flyway.enabled=false", - "jwt.secret=TestSecretKeyThatIsAtLeast256BitsLongForTestingPurposesOnly", - "jwt.access-token-expiration-ms=900000", - "jwt.refresh-token-expiration-ms=604800000", - "cors.allowed-origins=http://localhost:3000", - "management.tracing.sampling.probability=0.0" -}) -class JobTrackerApplicationTests { - - @Test - void contextLoads() { - } -} diff --git a/docker-compose.yml b/docker-compose.yml index 6d4c574..abb7187 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -2,21 +2,6 @@ version: "3.9" services: -<<<<<<< HEAD - db: - image: mariadb:11 - container_name: jobtracker-db - restart: unless-stopped - environment: - MARIADB_DATABASE: ${DB_NAME:-jobtracker} - MARIADB_USER: ${DB_USERNAME:-jobtracker} - MARIADB_PASSWORD: ${DB_PASSWORD:-jobtracker} - MARIADB_ROOT_PASSWORD: ${DB_ROOT_PASSWORD:-root} - ports: - - "3306:3306" - volumes: - - db_data:/var/lib/mysql -======= # ── Spring Boot application ─────────────────────────────── app: build: @@ -26,15 +11,18 @@ services: ports: - "8080:8080" environment: - SPRING_DATASOURCE_URL: jdbc:mariadb://mariadb:3306/jobtracker?createDatabaseIfNotExist=true&characterEncoding=UTF-8&useUnicode=true - SPRING_DATASOURCE_USERNAME: root - SPRING_DATASOURCE_PASSWORD: root - # WARNING: Replace this secret before any production use. This value is for development only. - JWT_SECRET: ChangeThisToAVeryLongSecretKeyForJWTTokensInDevelopment - CORS_ALLOWED_ORIGINS: "http://localhost:3000,http://localhost:5173" + DB_URL: jdbc:mariadb://mariadb:3306/${DB_NAME:-jobtracker}?createDatabaseIfNotExist=true&characterEncoding=UTF-8&useUnicode=true + DB_USERNAME: ${DB_USERNAME:-jobtracker} + DB_PASSWORD: ${DB_PASSWORD:?DB_PASSWORD is required} + JWT_SECRET: ${JWT_SECRET:?JWT_SECRET is required} + CORS_ALLOWED_ORIGINS: ${CORS_ALLOWED_ORIGINS:-"http://localhost:3000,http://localhost:5173"} + OTEL_EXPORTER_OTLP_ENDPOINT: http://jaeger:4317 + TRACING_SAMPLING_PROBABILITY: ${TRACING_SAMPLING_PROBABILITY:-1.0} depends_on: mariadb: condition: service_healthy + jaeger: + condition: service_started healthcheck: test: ["CMD", "wget", "-qO-", "http://localhost:8081/actuator/health"] interval: 30s @@ -52,54 +40,32 @@ services: ports: - "3306:3306" environment: - MARIADB_ROOT_PASSWORD: root - MARIADB_DATABASE: jobtracker + MARIADB_ROOT_PASSWORD: ${DB_ROOT_PASSWORD:?DB_ROOT_PASSWORD is required} + MARIADB_DATABASE: ${DB_NAME:-jobtracker} + MARIADB_USER: ${DB_USERNAME:-jobtracker} + MARIADB_PASSWORD: ${DB_PASSWORD:?DB_PASSWORD is required} volumes: - mariadb_data:/var/lib/mysql ->>>>>>> origin/main healthcheck: test: ["CMD", "healthcheck.sh", "--connect", "--innodb_initialized"] interval: 10s timeout: 5s retries: 5 -<<<<<<< HEAD + start_period: 30s + networks: + - job-tracker-net + restart: unless-stopped + # ── Jaeger (distributed tracing) ───────────────────────── jaeger: - image: jaegertracing/all-in-one:1.57.0 - container_name: jobtracker-jaeger - restart: unless-stopped + image: jaegertracing/all-in-one:1.57.0 + container_name: job-tracker-jaeger ports: - "16686:16686" # Jaeger UI - "4317:4317" # OTLP gRPC receiver - "4318:4318" # OTLP HTTP receiver environment: - COLLECTOR_OTLP_ENABLED=true - - app: - build: - context: ./backend - dockerfile: Dockerfile - container_name: jobtracker-app - restart: unless-stopped - depends_on: - db: - condition: service_healthy - jaeger: - condition: service_started - ports: - - "8080:8080" - environment: - DB_URL: jdbc:mariadb://db:3306/${DB_NAME:-jobtracker}?createDatabaseIfNotExist=true&characterEncoding=UTF-8&useUnicode=true - DB_USERNAME: ${DB_USERNAME:-jobtracker} - DB_PASSWORD: ${DB_PASSWORD:-jobtracker} - OTEL_EXPORTER_OTLP_ENDPOINT: http://jaeger:4317 - TRACING_SAMPLING_PROBABILITY: ${TRACING_SAMPLING_PROBABILITY:-1.0} - SERVER_PORT: 8080 - -volumes: - db_data: -======= - start_period: 30s networks: - job-tracker-net restart: unless-stopped @@ -135,8 +101,8 @@ volumes: ports: - "3000:3000" environment: - GF_SECURITY_ADMIN_USER: admin - GF_SECURITY_ADMIN_PASSWORD: admin + GF_SECURITY_ADMIN_USER: ${GRAFANA_ADMIN_USER:-admin} + GF_SECURITY_ADMIN_PASSWORD: ${GRAFANA_ADMIN_PASSWORD:-admin} volumes: - grafana_data:/var/lib/grafana - ./grafana/provisioning/datasources:/etc/grafana/provisioning/datasources:ro @@ -157,4 +123,4 @@ volumes: networks: job-tracker-net: driver: bridge ->>>>>>> origin/main + diff --git a/docker-compose.yml.orig b/docker-compose.yml.orig deleted file mode 100644 index 6d4c574..0000000 --- a/docker-compose.yml.orig +++ /dev/null @@ -1,160 +0,0 @@ -version: "3.9" - -services: - -<<<<<<< HEAD - db: - image: mariadb:11 - container_name: jobtracker-db - restart: unless-stopped - environment: - MARIADB_DATABASE: ${DB_NAME:-jobtracker} - MARIADB_USER: ${DB_USERNAME:-jobtracker} - MARIADB_PASSWORD: ${DB_PASSWORD:-jobtracker} - MARIADB_ROOT_PASSWORD: ${DB_ROOT_PASSWORD:-root} - ports: - - "3306:3306" - volumes: - - db_data:/var/lib/mysql -======= - # ── Spring Boot application ─────────────────────────────── - app: - build: - context: . - dockerfile: Dockerfile - container_name: job-tracker-app - ports: - - "8080:8080" - environment: - SPRING_DATASOURCE_URL: jdbc:mariadb://mariadb:3306/jobtracker?createDatabaseIfNotExist=true&characterEncoding=UTF-8&useUnicode=true - SPRING_DATASOURCE_USERNAME: root - SPRING_DATASOURCE_PASSWORD: root - # WARNING: Replace this secret before any production use. This value is for development only. - JWT_SECRET: ChangeThisToAVeryLongSecretKeyForJWTTokensInDevelopment - CORS_ALLOWED_ORIGINS: "http://localhost:3000,http://localhost:5173" - depends_on: - mariadb: - condition: service_healthy - healthcheck: - test: ["CMD", "wget", "-qO-", "http://localhost:8081/actuator/health"] - interval: 30s - timeout: 10s - retries: 5 - start_period: 60s - networks: - - job-tracker-net - restart: unless-stopped - - # ── MariaDB ─────────────────────────────────────────────── - mariadb: - image: mariadb:11 - container_name: job-tracker-mariadb - ports: - - "3306:3306" - environment: - MARIADB_ROOT_PASSWORD: root - MARIADB_DATABASE: jobtracker - volumes: - - mariadb_data:/var/lib/mysql ->>>>>>> origin/main - healthcheck: - test: ["CMD", "healthcheck.sh", "--connect", "--innodb_initialized"] - interval: 10s - timeout: 5s - retries: 5 -<<<<<<< HEAD - - jaeger: - image: jaegertracing/all-in-one:1.57.0 - container_name: jobtracker-jaeger - restart: unless-stopped - ports: - - "16686:16686" # Jaeger UI - - "4317:4317" # OTLP gRPC receiver - - "4318:4318" # OTLP HTTP receiver - environment: - - COLLECTOR_OTLP_ENABLED=true - - app: - build: - context: ./backend - dockerfile: Dockerfile - container_name: jobtracker-app - restart: unless-stopped - depends_on: - db: - condition: service_healthy - jaeger: - condition: service_started - ports: - - "8080:8080" - environment: - DB_URL: jdbc:mariadb://db:3306/${DB_NAME:-jobtracker}?createDatabaseIfNotExist=true&characterEncoding=UTF-8&useUnicode=true - DB_USERNAME: ${DB_USERNAME:-jobtracker} - DB_PASSWORD: ${DB_PASSWORD:-jobtracker} - OTEL_EXPORTER_OTLP_ENDPOINT: http://jaeger:4317 - TRACING_SAMPLING_PROBABILITY: ${TRACING_SAMPLING_PROBABILITY:-1.0} - SERVER_PORT: 8080 - -volumes: - db_data: -======= - start_period: 30s - networks: - - job-tracker-net - restart: unless-stopped - - # ── Prometheus ──────────────────────────────────────────── - prometheus: - image: prom/prometheus:v2.54.1 - container_name: job-tracker-prometheus - ports: - - "9090:9090" - volumes: - - ./prometheus.yml:/etc/prometheus/prometheus.yml:ro - - prometheus_data:/prometheus - command: - - "--config.file=/etc/prometheus/prometheus.yml" - - "--storage.tsdb.path=/prometheus" - - "--web.console.libraries=/usr/share/prometheus/console_libraries" - - "--web.console.templates=/usr/share/prometheus/consoles" - healthcheck: - test: ["CMD", "wget", "-qO-", "http://localhost:9090/-/healthy"] - interval: 15s - timeout: 5s - retries: 3 - start_period: 15s - networks: - - job-tracker-net - restart: unless-stopped - - # ── Grafana ─────────────────────────────────────────────── - grafana: - image: grafana/grafana:10.4.8 - container_name: job-tracker-grafana - ports: - - "3000:3000" - environment: - GF_SECURITY_ADMIN_USER: admin - GF_SECURITY_ADMIN_PASSWORD: admin - volumes: - - grafana_data:/var/lib/grafana - - ./grafana/provisioning/datasources:/etc/grafana/provisioning/datasources:ro - - ./grafana/provisioning/dashboards:/etc/grafana/provisioning/dashboards:ro - - ./grafana/dashboards:/etc/grafana/dashboards:ro - depends_on: - prometheus: - condition: service_healthy - networks: - - job-tracker-net - restart: unless-stopped - -volumes: - mariadb_data: - prometheus_data: - grafana_data: - -networks: - job-tracker-net: - driver: bridge ->>>>>>> origin/main diff --git a/docker-compose_BACKUP_20088.yml b/docker-compose_BACKUP_20088.yml deleted file mode 100644 index 5600e15..0000000 --- a/docker-compose_BACKUP_20088.yml +++ /dev/null @@ -1,99 +0,0 @@ -version: "3.9" - -services: - - # ── Spring Boot application ─────────────────────────────── - app: - build: - context: . - dockerfile: Dockerfile - container_name: job-tracker-app - ports: - - "8080:8080" - environment: - SPRING_DATASOURCE_URL: jdbc:mariadb://mariadb:3306/jobtracker?createDatabaseIfNotExist=true&characterEncoding=UTF-8&useUnicode=true - SPRING_DATASOURCE_USERNAME: root - SPRING_DATASOURCE_PASSWORD: root - # WARNING: Replace this secret before any production use. This value is for development only. - JWT_SECRET: ChangeThisToAVeryLongSecretKeyForJWTTokensInDevelopment - CORS_ALLOWED_ORIGINS: "http://localhost:3000,http://localhost:5173" - depends_on: - mariadb: - condition: service_healthy - healthcheck: - test: ["CMD", "wget", "-qO-", "http://localhost:8080/actuator/health"] - interval: 30s - timeout: 10s - retries: 5 - start_period: 60s - networks: - - job-tracker-net - restart: unless-stopped - - # ── MariaDB ─────────────────────────────────────────────── - mariadb: - image: mariadb:11 - container_name: job-tracker-mariadb - ports: - - "3306:3306" - environment: - MARIADB_ROOT_PASSWORD: root - MARIADB_DATABASE: jobtracker - volumes: - - mariadb_data:/var/lib/mysql - healthcheck: - test: ["CMD", "healthcheck.sh", "--connect", "--innodb_initialized"] - interval: 10s - timeout: 5s - retries: 5 - start_period: 30s - networks: - - job-tracker-net - restart: unless-stopped - - # ── Prometheus ──────────────────────────────────────────── - prometheus: - image: prom/prometheus:latest - container_name: job-tracker-prometheus - ports: - - "9090:9090" - volumes: - - ./prometheus.yml:/etc/prometheus/prometheus.yml:ro - - prometheus_data:/prometheus - command: - - "--config.file=/etc/prometheus/prometheus.yml" - - "--storage.tsdb.path=/prometheus" - - "--web.console.libraries=/usr/share/prometheus/console_libraries" - - "--web.console.templates=/usr/share/prometheus/consoles" - networks: - - job-tracker-net - restart: unless-stopped - - # ── Grafana ─────────────────────────────────────────────── - grafana: - image: grafana/grafana:latest - container_name: job-tracker-grafana - ports: - - "3000:3000" - environment: - GF_SECURITY_ADMIN_USER: admin - GF_SECURITY_ADMIN_PASSWORD: admin - volumes: - - grafana_data:/var/lib/grafana - - ./grafana/provisioning/datasources:/etc/grafana/provisioning/datasources:ro - - ./grafana/provisioning/dashboards:/etc/grafana/provisioning/dashboards:ro - - ./grafana/dashboards:/etc/grafana/dashboards:ro - depends_on: - - prometheus - networks: - - job-tracker-net - restart: unless-stopped - -volumes: - mariadb_data: - prometheus_data: - grafana_data: - -networks: - job-tracker-net: - driver: bridge diff --git a/docker-compose_BASE_20088.yml b/docker-compose_BASE_20088.yml deleted file mode 100644 index e69de29..0000000 diff --git a/docker-compose_LOCAL_20088.yml b/docker-compose_LOCAL_20088.yml deleted file mode 100644 index 5600e15..0000000 --- a/docker-compose_LOCAL_20088.yml +++ /dev/null @@ -1,99 +0,0 @@ -version: "3.9" - -services: - - # ── Spring Boot application ─────────────────────────────── - app: - build: - context: . - dockerfile: Dockerfile - container_name: job-tracker-app - ports: - - "8080:8080" - environment: - SPRING_DATASOURCE_URL: jdbc:mariadb://mariadb:3306/jobtracker?createDatabaseIfNotExist=true&characterEncoding=UTF-8&useUnicode=true - SPRING_DATASOURCE_USERNAME: root - SPRING_DATASOURCE_PASSWORD: root - # WARNING: Replace this secret before any production use. This value is for development only. - JWT_SECRET: ChangeThisToAVeryLongSecretKeyForJWTTokensInDevelopment - CORS_ALLOWED_ORIGINS: "http://localhost:3000,http://localhost:5173" - depends_on: - mariadb: - condition: service_healthy - healthcheck: - test: ["CMD", "wget", "-qO-", "http://localhost:8080/actuator/health"] - interval: 30s - timeout: 10s - retries: 5 - start_period: 60s - networks: - - job-tracker-net - restart: unless-stopped - - # ── MariaDB ─────────────────────────────────────────────── - mariadb: - image: mariadb:11 - container_name: job-tracker-mariadb - ports: - - "3306:3306" - environment: - MARIADB_ROOT_PASSWORD: root - MARIADB_DATABASE: jobtracker - volumes: - - mariadb_data:/var/lib/mysql - healthcheck: - test: ["CMD", "healthcheck.sh", "--connect", "--innodb_initialized"] - interval: 10s - timeout: 5s - retries: 5 - start_period: 30s - networks: - - job-tracker-net - restart: unless-stopped - - # ── Prometheus ──────────────────────────────────────────── - prometheus: - image: prom/prometheus:latest - container_name: job-tracker-prometheus - ports: - - "9090:9090" - volumes: - - ./prometheus.yml:/etc/prometheus/prometheus.yml:ro - - prometheus_data:/prometheus - command: - - "--config.file=/etc/prometheus/prometheus.yml" - - "--storage.tsdb.path=/prometheus" - - "--web.console.libraries=/usr/share/prometheus/console_libraries" - - "--web.console.templates=/usr/share/prometheus/consoles" - networks: - - job-tracker-net - restart: unless-stopped - - # ── Grafana ─────────────────────────────────────────────── - grafana: - image: grafana/grafana:latest - container_name: job-tracker-grafana - ports: - - "3000:3000" - environment: - GF_SECURITY_ADMIN_USER: admin - GF_SECURITY_ADMIN_PASSWORD: admin - volumes: - - grafana_data:/var/lib/grafana - - ./grafana/provisioning/datasources:/etc/grafana/provisioning/datasources:ro - - ./grafana/provisioning/dashboards:/etc/grafana/provisioning/dashboards:ro - - ./grafana/dashboards:/etc/grafana/dashboards:ro - depends_on: - - prometheus - networks: - - job-tracker-net - restart: unless-stopped - -volumes: - mariadb_data: - prometheus_data: - grafana_data: - -networks: - job-tracker-net: - driver: bridge diff --git a/docker-compose_REMOTE_20088.yml b/docker-compose_REMOTE_20088.yml deleted file mode 100644 index bdf6d43..0000000 --- a/docker-compose_REMOTE_20088.yml +++ /dev/null @@ -1,41 +0,0 @@ -version: '3.8' - -services: - db: - image: mariadb:11.2 - container_name: jobtracker-db - environment: - MYSQL_ROOT_PASSWORD: root - MYSQL_DATABASE: jobtracker - MYSQL_USER: jobtracker - MYSQL_PASSWORD: jobtracker - ports: - - "3306:3306" - volumes: - - mariadb_data:/var/lib/mysql - healthcheck: - test: ["CMD", "healthcheck.sh", "--connect", "--innodb_initialized"] - interval: 10s - timeout: 5s - retries: 5 - - app: - build: . - container_name: jobtracker-app - ports: - - "8080:8080" - environment: - DB_URL: jdbc:mariadb://db:3306/jobtracker?createDatabaseIfNotExist=true&characterEncoding=UTF-8&useUnicode=true - DB_USERNAME: jobtracker - DB_PASSWORD: jobtracker - JWT_SECRET: ${JWT_SECRET:-ChangeThisToASecureRandomSecretKeyInProductionAtLeast256BitsLong} - JWT_ACCESS_TOKEN_EXPIRATION_MS: 900000 - JWT_REFRESH_TOKEN_EXPIRATION_MS: 604800000 - CORS_ALLOWED_ORIGINS: ${CORS_ALLOWED_ORIGINS:-http://localhost:3000} - depends_on: - db: - condition: service_healthy - restart: unless-stopped - -volumes: - mariadb_data: diff --git a/pom.xml b/pom.xml index 96ffbcd..cd5b9bb 100644 --- a/pom.xml +++ b/pom.xml @@ -86,17 +86,12 @@ 2.5.0 -<<<<<<< HEAD:backend/pom.xml - -======= ->>>>>>> origin/main:pom.xml org.springframework.boot spring-boot-starter-actuator -<<<<<<< HEAD:backend/pom.xml io.micrometer @@ -109,25 +104,18 @@ opentelemetry-exporter-otlp - -======= ->>>>>>> origin/main:pom.xml io.micrometer micrometer-registry-prometheus -<<<<<<< HEAD:backend/pom.xml -======= net.logstash.logback logstash-logback-encoder 7.4 - ->>>>>>> origin/main:pom.xml org.springframework.boot diff --git a/pom.xml.orig b/pom.xml.orig deleted file mode 100644 index 96ffbcd..0000000 --- a/pom.xml.orig +++ /dev/null @@ -1,197 +0,0 @@ - - - 4.0.0 - - - org.springframework.boot - spring-boot-starter-parent - 3.2.5 - - - - com.jobtracker - job-tracker - 1.0.0 - job-tracker - Spring Boot Job Application Tracker API - - - 21 - 0.12.5 - 1.19.8 - 5.4.0 - - - - - - org.springframework.boot - spring-boot-starter-web - - - org.springframework.boot - spring-boot-starter-validation - - - org.springframework.boot - spring-boot-starter-data-jpa - - - org.springframework.boot - spring-boot-starter-security - - - - - org.mariadb.jdbc - mariadb-java-client - runtime - - - - - org.flywaydb - flyway-core - - - org.flywaydb - flyway-mysql - - - - - io.jsonwebtoken - jjwt-api - ${jjwt.version} - - - io.jsonwebtoken - jjwt-impl - ${jjwt.version} - runtime - - - io.jsonwebtoken - jjwt-jackson - ${jjwt.version} - runtime - - - - - org.springdoc - springdoc-openapi-starter-webmvc-ui - 2.5.0 - - -<<<<<<< HEAD:backend/pom.xml - -======= - ->>>>>>> origin/main:pom.xml - - org.springframework.boot - spring-boot-starter-actuator - - -<<<<<<< HEAD:backend/pom.xml - - - io.micrometer - micrometer-tracing-bridge-otel - - - - - io.opentelemetry - opentelemetry-exporter-otlp - - - -======= - ->>>>>>> origin/main:pom.xml - - io.micrometer - micrometer-registry-prometheus - - -<<<<<<< HEAD:backend/pom.xml -======= - - - net.logstash.logback - logstash-logback-encoder - 7.4 - - ->>>>>>> origin/main:pom.xml - - - org.springframework.boot - spring-boot-starter-test - test - - - org.springframework.security - spring-security-test - test - - - - - org.testcontainers - junit-jupiter - ${testcontainers.version} - test - - - org.testcontainers - mariadb - ${testcontainers.version} - test - - - - - io.rest-assured - rest-assured - ${rest-assured.version} - test - - - io.rest-assured - json-path - ${rest-assured.version} - test - - - io.rest-assured - spring-mock-mvc - ${rest-assured.version} - test - - - - - - - org.springframework.boot - spring-boot-maven-plugin - - - org.apache.maven.plugins - maven-surefire-plugin - - - **/*Test.java - **/*Tests.java - **/*IT.java - - - - - - diff --git a/src/main/java/com/jobtracker/config/RequestLoggingFilter.java.orig b/src/main/java/com/jobtracker/config/RequestLoggingFilter.java.orig deleted file mode 100644 index d270a3a..0000000 --- a/src/main/java/com/jobtracker/config/RequestLoggingFilter.java.orig +++ /dev/null @@ -1,48 +0,0 @@ -package com.jobtracker.config; - -import jakarta.servlet.FilterChain; -import jakarta.servlet.ServletException; -import jakarta.servlet.http.HttpServletRequest; -import jakarta.servlet.http.HttpServletResponse; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; -import org.springframework.lang.NonNull; -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; - -@Component -public class RequestLoggingFilter extends OncePerRequestFilter { - - private static final Logger log = LoggerFactory.getLogger(RequestLoggingFilter.class); - - @Override - protected void doFilterInternal(@NonNull HttpServletRequest request, - @NonNull HttpServletResponse response, - @NonNull FilterChain filterChain) throws ServletException, IOException { - long start = System.currentTimeMillis(); - try { - filterChain.doFilter(request, response); - } finally { - long duration = System.currentTimeMillis() - start; - String userId = resolveUserId(); - log.info("method={} path={} status={} duration={}ms userId={}", - request.getMethod(), - request.getRequestURI(), - response.getStatus(), - duration, - userId); - } - } - - private String resolveUserId() { - Authentication auth = SecurityContextHolder.getContext().getAuthentication(); - if (auth != null && auth.isAuthenticated() && !"anonymousUser".equals(auth.getPrincipal())) { - return auth.getName(); - } - return "anonymous"; - } -} diff --git a/src/main/java/com/jobtracker/exception/GlobalExceptionHandler.java.orig b/src/main/java/com/jobtracker/exception/GlobalExceptionHandler.java.orig deleted file mode 100644 index 0d4b520..0000000 --- a/src/main/java/com/jobtracker/exception/GlobalExceptionHandler.java.orig +++ /dev/null @@ -1,81 +0,0 @@ -package com.jobtracker.exception; - -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; -import org.springframework.security.authentication.BadCredentialsException; -import org.springframework.security.core.AuthenticationException; -import org.springframework.http.HttpStatus; -import org.springframework.http.ResponseEntity; -import org.springframework.validation.FieldError; -import org.springframework.web.bind.MethodArgumentNotValidException; -import org.springframework.web.bind.annotation.ExceptionHandler; -import org.springframework.web.bind.annotation.RestControllerAdvice; - -import java.time.LocalDateTime; -import java.util.HashMap; -import java.util.Map; - -@RestControllerAdvice -public class GlobalExceptionHandler { - - private static final Logger log = LoggerFactory.getLogger(GlobalExceptionHandler.class); - - @ExceptionHandler(ResourceNotFoundException.class) - public ResponseEntity> handleNotFound(ResourceNotFoundException ex) { - log.warn("event=RESOURCE_NOT_FOUND message={}", ex.getMessage()); - return buildResponse(HttpStatus.NOT_FOUND, ex.getMessage()); - } - - @ExceptionHandler(BadRequestException.class) - public ResponseEntity> handleBadRequest(BadRequestException ex) { - log.warn("event=BAD_REQUEST message={}", ex.getMessage()); - return buildResponse(HttpStatus.BAD_REQUEST, ex.getMessage()); - } - - @ExceptionHandler(UnauthorizedException.class) - public ResponseEntity> handleUnauthorized(UnauthorizedException ex) { - log.warn("event=UNAUTHORIZED message={}", ex.getMessage()); - return buildResponse(HttpStatus.UNAUTHORIZED, ex.getMessage()); - } - - @ExceptionHandler(ConflictException.class) - public ResponseEntity> handleConflict(ConflictException ex) { - log.warn("event=CONFLICT message={}", ex.getMessage()); - return buildResponse(HttpStatus.CONFLICT, ex.getMessage()); - } - - @ExceptionHandler({BadCredentialsException.class, AuthenticationException.class}) - public ResponseEntity> handleBadCredentials(RuntimeException ex) { - return buildResponse(HttpStatus.UNAUTHORIZED, "Invalid credentials"); - } - - @ExceptionHandler(MethodArgumentNotValidException.class) - public ResponseEntity> handleValidation(MethodArgumentNotValidException ex) { - Map fieldErrors = new HashMap<>(); - for (FieldError error : ex.getBindingResult().getFieldErrors()) { - fieldErrors.put(error.getField(), error.getDefaultMessage()); - } - log.warn("event=VALIDATION_FAILURE fieldErrors={}", fieldErrors); - Map body = new HashMap<>(); - body.put("timestamp", LocalDateTime.now().toString()); - body.put("status", HttpStatus.BAD_REQUEST.value()); - body.put("error", "Validation failed"); - body.put("fieldErrors", fieldErrors); - return ResponseEntity.status(HttpStatus.BAD_REQUEST).body(body); - } - - @ExceptionHandler(Exception.class) - public ResponseEntity> handleGeneral(Exception ex) { - log.error("event=UNEXPECTED_ERROR message={}", ex.getMessage(), ex); - return buildResponse(HttpStatus.INTERNAL_SERVER_ERROR, "An unexpected error occurred"); - } - - private ResponseEntity> buildResponse(HttpStatus status, String message) { - Map body = new HashMap<>(); - body.put("timestamp", LocalDateTime.now().toString()); - body.put("status", status.value()); - body.put("error", status.getReasonPhrase()); - body.put("message", message); - return ResponseEntity.status(status).body(body); - } -} diff --git a/src/main/java/com/jobtracker/service/ApplicationService.java b/src/main/java/com/jobtracker/service/ApplicationService.java index 6cf5ab2..50e7613 100644 --- a/src/main/java/com/jobtracker/service/ApplicationService.java +++ b/src/main/java/com/jobtracker/service/ApplicationService.java @@ -52,7 +52,6 @@ public ApplicationService(ApplicationRepository applicationRepository, public ApplicationResponse create(ApplicationRequest request) { Span span = tracer.nextSpan().name("create-application").start(); try (Tracer.SpanInScope ignored = tracer.withSpan(span)) { - span.tag("vacancy", request.vacancyName() != null ? request.vacancyName() : "unknown"); JobApplication app = new JobApplication(); mapRequestToEntity(request, app); app.setUser(securityUtils.getCurrentUser()); diff --git a/src/main/java/com/jobtracker/service/AuthService.java b/src/main/java/com/jobtracker/service/AuthService.java index a83cd0c..45bffaa 100644 --- a/src/main/java/com/jobtracker/service/AuthService.java +++ b/src/main/java/com/jobtracker/service/AuthService.java @@ -10,13 +10,10 @@ import com.jobtracker.exception.ResourceNotFoundException; import com.jobtracker.mapper.AuthMapper; import com.jobtracker.repository.UserRepository; -<<<<<<< HEAD:backend/src/main/java/com/jobtracker/service/AuthService.java import io.micrometer.tracing.Span; import io.micrometer.tracing.Tracer; -======= import org.slf4j.Logger; import org.slf4j.LoggerFactory; ->>>>>>> origin/main:src/main/java/com/jobtracker/service/AuthService.java import org.springframework.security.authentication.BadCredentialsException; import org.springframework.security.core.Authentication; import org.springframework.security.core.context.SecurityContextHolder; @@ -76,40 +73,30 @@ public AuthResponse register(RegisterRequest request) { @Transactional public AuthResponse login(LoginRequest request) { -<<<<<<< HEAD:backend/src/main/java/com/jobtracker/service/AuthService.java Span span = tracer.nextSpan().name("login").start(); try (Tracer.SpanInScope ignored = tracer.withSpan(span)) { User user = userRepository.findByEmail(request.email()) - .orElseThrow(() -> new BadCredentialsException("Invalid credentials")); + .orElseThrow(() -> { + log.warn("event=LOGIN_FAILURE reason=USER_NOT_FOUND email={}", request.email()); + return new BadCredentialsException("Invalid credentials"); + }); if (!passwordEncoder.matches(request.password(), user.getPasswordHash())) { + log.warn("event=LOGIN_FAILURE reason=WRONG_PASSWORD userId={}", user.getId()); throw new BadCredentialsException("Invalid credentials"); } - return buildAuthResponse(user); - } catch (BadCredentialsException e) { - throw e; + log.info("event=LOGIN_SUCCESS userId={}", user.getId()); + return buildAuthResponse(user); + } catch (BadCredentialsException e) { + // Expected auth failure – do not mark as span error + throw e; } catch (Exception e) { span.error(e); throw e; } finally { span.end(); } -======= - User user = userRepository.findByEmail(request.email()) - .orElseThrow(() -> { - log.warn("event=LOGIN_FAILURE reason=USER_NOT_FOUND email={}", request.email()); - return new BadCredentialsException("Invalid credentials"); - }); - - if (!passwordEncoder.matches(request.password(), user.getPasswordHash())) { - log.warn("event=LOGIN_FAILURE reason=WRONG_PASSWORD userId={}", user.getId()); - throw new BadCredentialsException("Invalid credentials"); - } - - log.info("event=LOGIN_SUCCESS userId={}", user.getId()); - return buildAuthResponse(user); ->>>>>>> origin/main:src/main/java/com/jobtracker/service/AuthService.java } @Transactional @@ -123,9 +110,7 @@ public RefreshResponse refresh(RefreshTokenRequest request) { user.getEmail(), user.getPasswordHash(), Collections.emptyList()); String accessToken = jwtService.generateToken(userDetails); - return buildAuthResponse(user); - } catch (BadCredentialsException e) { - throw e; + return new RefreshResponse(accessToken, newRefreshToken.getToken()); } catch (Exception e) { span.error(e); throw e; diff --git a/src/main/java/com/jobtracker/service/AuthService.java.orig b/src/main/java/com/jobtracker/service/AuthService.java.orig deleted file mode 100644 index a83cd0c..0000000 --- a/src/main/java/com/jobtracker/service/AuthService.java.orig +++ /dev/null @@ -1,183 +0,0 @@ -package com.jobtracker.service; - -import com.jobtracker.config.JwtService; -import com.jobtracker.dto.auth.*; -import com.jobtracker.entity.PasswordResetToken; -import com.jobtracker.entity.RefreshToken; -import com.jobtracker.entity.User; -import com.jobtracker.exception.BadRequestException; -import com.jobtracker.exception.ConflictException; -import com.jobtracker.exception.ResourceNotFoundException; -import com.jobtracker.mapper.AuthMapper; -import com.jobtracker.repository.UserRepository; -<<<<<<< HEAD:backend/src/main/java/com/jobtracker/service/AuthService.java -import io.micrometer.tracing.Span; -import io.micrometer.tracing.Tracer; -======= -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; ->>>>>>> origin/main:src/main/java/com/jobtracker/service/AuthService.java -import org.springframework.security.authentication.BadCredentialsException; -import org.springframework.security.core.Authentication; -import org.springframework.security.core.context.SecurityContextHolder; -import org.springframework.security.core.userdetails.UserDetails; -import org.springframework.security.crypto.password.PasswordEncoder; -import org.springframework.stereotype.Service; -import org.springframework.transaction.annotation.Transactional; - -import java.util.Collections; - -@Service -public class AuthService { - - private static final Logger log = LoggerFactory.getLogger(AuthService.class); - - private final UserRepository userRepository; - private final PasswordEncoder passwordEncoder; - private final JwtService jwtService; - private final RefreshTokenService refreshTokenService; - private final PasswordResetService passwordResetService; - private final AuthMapper authMapper; - private final Tracer tracer; - - public AuthService(UserRepository userRepository, - PasswordEncoder passwordEncoder, - JwtService jwtService, - RefreshTokenService refreshTokenService, - PasswordResetService passwordResetService, - AuthMapper authMapper, - Tracer tracer) { - this.userRepository = userRepository; - this.passwordEncoder = passwordEncoder; - this.jwtService = jwtService; - this.refreshTokenService = refreshTokenService; - this.passwordResetService = passwordResetService; - this.authMapper = authMapper; - this.tracer = tracer; - } - - @Transactional - public AuthResponse register(RegisterRequest request) { - if (!request.password().equals(request.confirmPassword())) { - throw new BadRequestException("Passwords do not match"); - } - if (userRepository.existsByEmail(request.email())) { - throw new ConflictException("Email already registered"); - } - - User user = new User(); - user.setName(request.name()); - user.setEmail(request.email()); - user.setPasswordHash(passwordEncoder.encode(request.password())); - user = userRepository.save(user); - log.info("event=REGISTRATION_SUCCESS email={} userId={}", user.getEmail(), user.getId()); - return buildAuthResponse(user); - } - - @Transactional - public AuthResponse login(LoginRequest request) { -<<<<<<< HEAD:backend/src/main/java/com/jobtracker/service/AuthService.java - Span span = tracer.nextSpan().name("login").start(); - try (Tracer.SpanInScope ignored = tracer.withSpan(span)) { - User user = userRepository.findByEmail(request.email()) - .orElseThrow(() -> new BadCredentialsException("Invalid credentials")); - - if (!passwordEncoder.matches(request.password(), user.getPasswordHash())) { - throw new BadCredentialsException("Invalid credentials"); - } - - return buildAuthResponse(user); - } catch (BadCredentialsException e) { - throw e; - } catch (Exception e) { - span.error(e); - throw e; - } finally { - span.end(); - } -======= - User user = userRepository.findByEmail(request.email()) - .orElseThrow(() -> { - log.warn("event=LOGIN_FAILURE reason=USER_NOT_FOUND email={}", request.email()); - return new BadCredentialsException("Invalid credentials"); - }); - - if (!passwordEncoder.matches(request.password(), user.getPasswordHash())) { - log.warn("event=LOGIN_FAILURE reason=WRONG_PASSWORD userId={}", user.getId()); - throw new BadCredentialsException("Invalid credentials"); - } - - log.info("event=LOGIN_SUCCESS userId={}", user.getId()); - return buildAuthResponse(user); ->>>>>>> origin/main:src/main/java/com/jobtracker/service/AuthService.java - } - - @Transactional - public RefreshResponse refresh(RefreshTokenRequest request) { - Span span = tracer.nextSpan().name("token-refresh").start(); - try (Tracer.SpanInScope ignored = tracer.withSpan(span)) { - RefreshToken newRefreshToken = refreshTokenService.verifyAndRotate(request.refreshToken()); - User user = newRefreshToken.getUser(); - - UserDetails userDetails = new org.springframework.security.core.userdetails.User( - user.getEmail(), user.getPasswordHash(), Collections.emptyList()); - String accessToken = jwtService.generateToken(userDetails); - - return buildAuthResponse(user); - } catch (BadCredentialsException e) { - throw e; - } catch (Exception e) { - span.error(e); - throw e; - } finally { - span.end(); - } - } - - @Transactional - public MessageResponse forgotPassword(ForgotPasswordRequest request) { - 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) - }); - // Always return success to prevent email enumeration - return new MessageResponse("If an account with that email exists, a password reset link has been sent"); - } - - @Transactional - public MessageResponse resetPassword(ResetPasswordRequest request) { - if (!request.newPassword().equals(request.confirmPassword())) { - throw new BadRequestException("Passwords do not match"); - } - - PasswordResetToken resetToken = passwordResetService.verifyToken(request.token()); - User user = resetToken.getUser(); - user.setPasswordHash(passwordEncoder.encode(request.newPassword())); - userRepository.save(user); - - passwordResetService.markTokenAsUsed(resetToken); - // Revoke all existing refresh tokens for security - refreshTokenService.revokeAllByUserId(user.getId()); - - return new MessageResponse("Password has been reset successfully"); - } - - @Transactional - public MessageResponse logout(LogoutRequest request) { - refreshTokenService.revokeToken(request.refreshToken()); - Authentication auth = SecurityContextHolder.getContext().getAuthentication(); - String userEmail = (auth != null && auth.isAuthenticated()) ? auth.getName() : "unknown"; - log.info("event=LOGOUT_SUCCESS userEmail={}", userEmail); - return new MessageResponse("Logged out successfully"); - } - - private AuthResponse buildAuthResponse(User user) { - UserDetails userDetails = new org.springframework.security.core.userdetails.User( - user.getEmail(), user.getPasswordHash(), Collections.emptyList()); - String accessToken = jwtService.generateToken(userDetails); - RefreshToken refreshToken = refreshTokenService.createRefreshToken(user); - - return new AuthResponse(accessToken, refreshToken.getToken(), authMapper.toUserResponse(user)); - } -} diff --git a/src/main/resources/application.yml b/src/main/resources/application.yml index fc49375..d872ac7 100644 --- a/src/main/resources/application.yml +++ b/src/main/resources/application.yml @@ -40,6 +40,12 @@ management: endpoint: health: show-details: when_authorized + tracing: + sampling: + probability: ${TRACING_SAMPLING_PROBABILITY:1.0} + otlp: + tracing: + endpoint: ${OTEL_EXPORTER_OTLP_ENDPOINT:http://localhost:4317} metrics: tags: application: ${spring.application.name} @@ -52,23 +58,6 @@ springdoc: path: /v3/api-docs swagger-ui: path: /swagger-ui.html -<<<<<<< HEAD:backend/src/main/resources/application.yml - -management: - endpoints: - web: - exposure: - include: health,info,prometheus,metrics - tracing: - sampling: - probability: ${TRACING_SAMPLING_PROBABILITY:1.0} - otlp: - tracing: - endpoint: ${OTEL_EXPORTER_OTLP_ENDPOINT:http://localhost:4317} - metrics: - tags: - application: ${spring.application.name} -======= group-configs: - group: auth display-name: Auth API @@ -79,4 +68,3 @@ management: - group: dashboard display-name: Dashboard API paths-to-match: /api/dashboard/** ->>>>>>> origin/main:src/main/resources/application.yml diff --git a/src/main/resources/application.yml.orig b/src/main/resources/application.yml.orig deleted file mode 100644 index fc49375..0000000 --- a/src/main/resources/application.yml.orig +++ /dev/null @@ -1,82 +0,0 @@ -spring: - application: - name: job-tracker - datasource: - url: ${DB_URL:jdbc:mariadb://localhost:3306/jobtracker?createDatabaseIfNotExist=true&characterEncoding=UTF-8&useUnicode=true} - username: ${DB_USERNAME:jobtracker} - password: ${DB_PASSWORD:jobtracker} - driver-class-name: org.mariadb.jdbc.Driver - jpa: - hibernate: - ddl-auto: validate - show-sql: false - properties: - hibernate: - dialect: org.hibernate.dialect.MariaDBDialect - format_sql: true - flyway: - enabled: true - locations: classpath:db/migration - baseline-on-migrate: true - -server: - port: ${SERVER_PORT:8080} - -jwt: - secret: ${JWT_SECRET:ThisIsAVeryLongSecretKeyForJWTTokensAndItMustBeAtLeast256BitsLong} - access-token-expiration-ms: ${JWT_ACCESS_TOKEN_EXPIRATION_MS:900000} - refresh-token-expiration-ms: ${JWT_REFRESH_TOKEN_EXPIRATION_MS:604800000} - -cors: - allowed-origins: ${CORS_ALLOWED_ORIGINS:http://localhost:3000,http://localhost:5173} - -management: - server: - port: ${MANAGEMENT_PORT:8081} - endpoints: - web: - exposure: - include: health,info,metrics,prometheus - endpoint: - health: - show-details: when_authorized - metrics: - tags: - application: ${spring.application.name} - distribution: - percentiles-histogram: - http.server.requests: true - -springdoc: - api-docs: - path: /v3/api-docs - swagger-ui: - path: /swagger-ui.html -<<<<<<< HEAD:backend/src/main/resources/application.yml - -management: - endpoints: - web: - exposure: - include: health,info,prometheus,metrics - tracing: - sampling: - probability: ${TRACING_SAMPLING_PROBABILITY:1.0} - otlp: - tracing: - endpoint: ${OTEL_EXPORTER_OTLP_ENDPOINT:http://localhost:4317} - metrics: - tags: - application: ${spring.application.name} -======= - group-configs: - - group: auth - display-name: Auth API - paths-to-match: /api/auth/** - - group: applications - display-name: Application API - paths-to-match: /api/applications/** - - group: dashboard - display-name: Dashboard API - paths-to-match: /api/dashboard/** ->>>>>>> origin/main:src/main/resources/application.yml diff --git a/src/main/resources/logback-spring.xml b/src/main/resources/logback-spring.xml index a4cc835..94558c9 100644 --- a/src/main/resources/logback-spring.xml +++ b/src/main/resources/logback-spring.xml @@ -1,29 +1,5 @@ -<<<<<<< HEAD:backend/src/main/resources/logback-spring.xml - - - - - - - %d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level [%X{traceId:-}] [%X{spanId:-}] %logger{36} - %msg%n - - - - - - - - - - - - - - - -======= @@ -34,14 +10,14 @@ - + - %d{HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n + %d{HH:mm:ss.SSS} [%thread] %-5level [%X{traceId:-}] [%X{spanId:-}] %logger{36} - %msg%n - + @@ -51,7 +27,7 @@ - + @@ -61,6 +37,5 @@ ->>>>>>> origin/main:src/main/resources/logback-spring.xml diff --git a/src/main/resources/logback-spring.xml.orig b/src/main/resources/logback-spring.xml.orig deleted file mode 100644 index a4cc835..0000000 --- a/src/main/resources/logback-spring.xml.orig +++ /dev/null @@ -1,66 +0,0 @@ - - -<<<<<<< HEAD:backend/src/main/resources/logback-spring.xml - - - - - - - %d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level [%X{traceId:-}] [%X{spanId:-}] %logger{36} - %msg%n - - - - - - - - - - - - - - - -======= - - - - - - - {"app":"${appName}"} - - - - - - - %d{HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n - - - - - - - - - - - - - - - - - - - - - - - ->>>>>>> origin/main:src/main/resources/logback-spring.xml - - diff --git a/src/test/java/com/jobtracker/unit/ApplicationServiceTest.java b/src/test/java/com/jobtracker/unit/ApplicationServiceTest.java index b35c963..b8ef10c 100644 --- a/src/test/java/com/jobtracker/unit/ApplicationServiceTest.java +++ b/src/test/java/com/jobtracker/unit/ApplicationServiceTest.java @@ -10,6 +10,7 @@ import com.jobtracker.repository.ApplicationRepository; import com.jobtracker.service.ApplicationService; import com.jobtracker.util.SecurityUtils; +import io.micrometer.tracing.Tracer; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; @@ -30,6 +31,7 @@ import static org.assertj.core.api.Assertions.assertThatThrownBy; import static org.mockito.ArgumentMatchers.*; import static org.mockito.Mockito.*; +import static org.mockito.Answers.RETURNS_DEEP_STUBS; @ExtendWith(MockitoExtension.class) class ApplicationServiceTest { @@ -37,6 +39,7 @@ class ApplicationServiceTest { @Mock private ApplicationRepository applicationRepository; @Mock private ApplicationMapper applicationMapper; @Mock private SecurityUtils securityUtils; + @Mock(answer = RETURNS_DEEP_STUBS) private Tracer tracer; @InjectMocks private ApplicationService applicationService; diff --git a/src/test/java/com/jobtracker/unit/AuthServiceTest.java b/src/test/java/com/jobtracker/unit/AuthServiceTest.java index af666df..ffd9bc1 100644 --- a/src/test/java/com/jobtracker/unit/AuthServiceTest.java +++ b/src/test/java/com/jobtracker/unit/AuthServiceTest.java @@ -12,6 +12,7 @@ import com.jobtracker.service.AuthService; import com.jobtracker.service.PasswordResetService; import com.jobtracker.service.RefreshTokenService; +import io.micrometer.tracing.Tracer; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; import org.mockito.InjectMocks; @@ -30,6 +31,7 @@ import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.anyString; import static org.mockito.Mockito.*; +import static org.mockito.Answers.RETURNS_DEEP_STUBS; @ExtendWith(MockitoExtension.class) class AuthServiceTest { @@ -40,6 +42,7 @@ class AuthServiceTest { @Mock private RefreshTokenService refreshTokenService; @Mock private PasswordResetService passwordResetService; @Mock private AuthMapper authMapper; + @Mock(answer = RETURNS_DEEP_STUBS) private Tracer tracer; @InjectMocks private AuthService authService; diff --git a/src/test/resources/application-test.yml b/src/test/resources/application-test.yml index b755bc5..0f78e78 100644 --- a/src/test/resources/application-test.yml +++ b/src/test/resources/application-test.yml @@ -19,6 +19,11 @@ jwt: cors: allowed-origins: http://localhost:3000 +management: + tracing: + sampling: + probability: 0.0 + logging: level: root: WARN