Skip to content

Kirill9m/exercise8#206

Open
Kirill9m wants to merge 34 commits intomainfrom
kirill9m/exercise8
Open

Kirill9m/exercise8#206
Kirill9m wants to merge 34 commits intomainfrom
kirill9m/exercise8

Conversation

@Kirill9m
Copy link
Copy Markdown

@Kirill9m Kirill9m commented Nov 15, 2025

🧪 Laboration: Spring Boot REST API with JPA, MySQL 9+, and Thymeleaf

Routes documentation:
https://documenter.getpostman.com/view/41629037/2sB3WsNKCk
image

All /api/ routes are protected with a JWT token.
Generating a new token requires a refresh token and the last used access token.

Bonus:
Server is running on a free Oracle Cloud instance for easy testing:
http://79.72.17.10:8080

Summary by CodeRabbit

  • New Features

    • User registration, login, refresh tokens and JWT-based authentication; secured API endpoints for notes and users
    • Note management endpoints: create, delete, and fetch user notes
    • Application converted to Spring Boot with runtime configuration and JWT utilities
  • Chores

    • Added Docker Compose for a MySQL service
    • Migrated build to Spring Boot parent and updated runtime dependencies
    • Added dev profile and runtime application properties
  • Tests

    • Added integration tests covering user and note flows with MySQL/Testcontainers

@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented Nov 15, 2025

Warning

Rate limit exceeded

@Kirill9m has exceeded the limit for the number of commits or files that can be reviewed per hour. Please wait 11 minutes and 37 seconds before requesting another review.

⌛ How to resolve this issue?

After the wait time has elapsed, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

We recommend that you space out your commits to avoid hitting the rate limit.

🚦 How do rate limits work?

CodeRabbit enforces hourly rate limits for each developer per organization.

Our paid plans have higher rate limits than the trial, open-source and free plans. In all cases, we re-allow further reviews after a brief timeout.

Please see our FAQ for further information.

📥 Commits

Reviewing files that changed from the base of the PR and between dc70534 and cc6cbf1.

📒 Files selected for processing (7)
  • src/main/java/org/example/dto/user/User.java (1 hunks)
  • src/main/java/org/example/dto/user/UserCheck.java (1 hunks)
  • src/main/java/org/example/entity/NoteEntity.java (1 hunks)
  • src/main/java/org/example/entity/TokenEntity.java (1 hunks)
  • src/main/java/org/example/entity/UserEntity.java (1 hunks)
  • src/main/java/org/example/exceptionHandler/GlobalExceptionHandler.java (1 hunks)
  • src/main/java/org/example/service/TokenService.java (1 hunks)

Walkthrough

Replaces a simple Java app with a Spring Boot auth service: adds Spring Boot parent/pom changes, Docker Compose for MySQL, application properties and dev profile, JPA entities/repositories, controllers, services, JWT/BCrypt utilities, security filter/config, global exception handling, and integration tests; removes original App and legacy unit tests.

Changes

Cohort / File(s) Summary
Build & IDE
pom.xml, JavaTemplate.iml, docker-compose.yml
Switch to Spring Boot parent and boot plugin, replace dependencies with Spring Boot stack (web, security, data-jpa, validation, jwt, testcontainers, etc.), add IntelliJ JPA facet config, add MySQL service in docker-compose.
Application config
src/main/resources/application.properties, src/main/resources/application-dev.properties
Add environment-driven datasource and JWT settings; dev profile provides local MySQL credentials, ddl-auto=update and SQL logging.
Application entry
src/main/java/org/example/Exercise2025AuthApplication.java
Add @SpringBootApplication class with a non-static void main(String[] args) method.
Security
src/main/java/org/example/config/SecurityConfig.java, src/main/java/org/example/filter/ApiKeyFilter.java
Add SecurityConfig wiring ApiKeyFilter into the chain; filter validates Bearer JWT, extracts email, loads user, sets authentication principal and enforces /api/**.
Controllers
src/main/java/org/example/controller/UserController.java, src/main/java/org/example/controller/NoteController.java
Add REST controllers for user register/login/refresh and note create/delete/list, using DTOs and authenticated principal.
Services
src/main/java/org/example/service/UserService.java, src/main/java/org/example/service/TokenService.java, src/main/java/org/example/service/NoteService.java
Add services: registration/login/token refresh with JWT generation and BCrypt hashing, token lifecycle, note create/list/soft-delete with transactional boundaries.
Entities
src/main/java/org/example/entity/UserEntity.java, src/main/java/org/example/entity/NoteEntity.java, src/main/java/org/example/entity/TokenEntity.java
Add JPA entities mapping users, notes, tokens with relationships, generated IDs, timestamps, and Hibernate-aware equals/hashCode in UserEntity.
Repositories
src/main/java/org/example/repository/*Repository.java
Add Spring Data JPA repositories: UserRepository, NoteRepository (non-deleted queries and a native query), TokenRepository (find by token/refreshToken).
DTOs
src/main/java/org/example/dto/**
Add immutable records for user DTOs (User, UserCheck, UserNew, UserNotes, ApiPrincipal), note DTOs (Note, NoteNew, NoteResponse), token DTOs (TokenRequest, UpdatedToken), and ErrorResponse.
Utilities
src/main/java/org/example/utils/JwtUtil.java, src/main/java/org/example/utils/BCryptUtil.java
Add JwtUtil for token generation/validation/extraction with secret initialization; add BCryptUtil for hashing and checking with null-safety.
Exception handling
src/main/java/org/example/exceptionHandler/ConflictException.java, src/main/java/org/example/exceptionHandler/GlobalExceptionHandler.java
Add ConflictException and @RestControllerAdvice handling EntityNotFoundException (404), SecurityException (403), ConflictException (409) returning ErrorResponse.
Tests
src/test/java/org/example/**, src/test/java/org/example/controller/*
Remove legacy AppTest/AppIT; add Spring Boot integration tests using Testcontainers MySQL for user and note flows (Exercise2025ApplicationTest, UserControllerTest, NoteControllerTest).
Docs
README.md
Simplify README to concise run instructions and dev profile notes.
Removals
src/main/java/org/example/App.java, src/test/java/org/example/AppTest.java, src/test/java/org/example/AppIT.java
Remove original simple main class and legacy unit tests.

Sequence Diagram(s)

sequenceDiagram
    participant Client
    participant UserController
    participant UserService
    participant UserRepository
    participant TokenRepository
    participant JwtUtil
    participant BCryptUtil

    Note over Client,UserService: User registration
    Client->>UserController: POST /user/register {name,email,password}
    UserController->>UserService: registerUser(User)
    UserService->>UserRepository: existsByEmail(email)
    alt email exists
        UserService-->>UserController: throw ConflictException
        UserController-->>Client: 409 Conflict
    else create user
        UserService->>BCryptUtil: hashPassword(password)
        BCryptUtil-->>UserService: hashed
        UserService->>UserRepository: save(UserEntity)
        UserService->>JwtUtil: generateToken(email, accessExp)
        JwtUtil-->>UserService: accessToken
        UserService->>JwtUtil: generateToken(email, refreshExp)
        JwtUtil-->>UserService: refreshToken
        UserService->>TokenRepository: save(TokenEntity)
        UserService-->>UserController: UserNew(...)
        UserController-->>Client: 201 Created
    end
Loading
sequenceDiagram
    participant Client
    participant SecurityChain
    participant ApiKeyFilter
    participant JwtUtil
    participant UserRepository
    participant Controller

    Note over Client,Controller: Authenticated API request
    Client->>SecurityChain: GET /api/notes/user (Authorization: Bearer token)
    SecurityChain->>ApiKeyFilter: doFilterInternal()
    ApiKeyFilter->>JwtUtil: validateJwtToken(token)
    alt invalid token
        JwtUtil-->>ApiKeyFilter: false
        ApiKeyFilter-->>Client: 401 Unauthorized
    else valid token
        JwtUtil-->>ApiKeyFilter: true
        ApiKeyFilter->>JwtUtil: getUsernameFromToken(token)
        JwtUtil-->>ApiKeyFilter: email
        ApiKeyFilter->>UserRepository: findByEmail(email)
        alt user not found
            UserRepository-->>ApiKeyFilter: empty
            ApiKeyFilter-->>Client: 401 Unauthorized
        else user found
            ApiKeyFilter-->>SecurityChain: set Authentication (ApiPrincipal)
            SecurityChain->>Controller: forward request with auth
            Controller-->>Client: 200 OK
        end
    end
Loading

Estimated code review effort

🎯 5 (Critical) | ⏱️ ~120 minutes

  • Pay special attention to:
    • src/main/java/org/example/filter/ApiKeyFilter.java — token parsing, error responses, SecurityContext setup.
    • src/main/java/org/example/utils/JwtUtil.java — secret/key initialization, token generation and validation exceptions.
    • src/main/java/org/example/service/UserService.java, TokenService.java, NoteService.java — transactional boundaries, token lifecycle, password hashing, exception handling.
    • src/main/java/org/example/entity/* and src/main/java/org/example/repository/* — JPA mappings, relationships, cascade, soft-delete semantics, and native query correctness.
    • pom.xml and docker-compose.yml — dependency versions and Testcontainers / MySQL compatibility.
    • src/main/java/org/example/Exercise2025AuthApplication.javamain is non-static; verify startup expectations and packaging/runtime behavior.
✨ Finishing touches
  • 📝 Generate docstrings
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch kirill9m/exercise8

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 15

🧹 Nitpick comments (19)
src/main/java/org/example/dto/user/UserApi.java (1)

5-7: Consider more descriptive naming for future integration.

This record is not currently used in any endpoint, but the naming UserApi is generic and doesn't clearly convey its purpose (token usage tracking based on the fields). If/when this record is integrated into API responses, consider a more descriptive name like TokenUsageInfo, UserTokenResponse, or ApiTokenDetails.

Additionally, note that the token field contains sensitive data; if this record is ever returned in responses or logged, ensure the token is excluded or redacted to prevent accidental exposure.

src/main/java/org/example/dto/user/User.java (1)

6-8: Remove redundant @NotNull annotations.

The @NotBlank annotation already implies @NotNull for String fields, making the explicit @NotNull annotation redundant.

Apply this diff to simplify the validation:

-public record User(@NotNull @NotBlank String name,
-                   @NotNull @NotBlank String password,
-                   @NotNull @NotBlank String email) {
+public record User(@NotBlank String name,
+                   @NotBlank String password,
+                   @NotBlank String email) {
src/main/java/org/example/dto/user/UserCheck.java (1)

6-6: Remove redundant @NotNull annotations.

Same as in User.java, the @NotBlank annotation already implies @NotNull, making the explicit @NotNull redundant.

Apply this diff:

-public record UserCheck(@NotNull @NotBlank String password, @NotNull @NotBlank String email) {
+public record UserCheck(@NotBlank String password, @NotBlank String email) {
src/main/java/org/example/dto/token/TokenRequest.java (1)

3-4: Consider adding validation annotations for required fields.

The record structure is clean. However, for security-sensitive operations like login and token refresh, consider adding @NotBlank annotations to ensure these required fields are validated at the API boundary.

Example:

-public record TokenRequest(String token, String refreshToken) {
+public record TokenRequest(@NotBlank String token, @NotBlank String refreshToken) {
src/main/java/org/example/Exercise2025Application.java (1)

5-5: Remove unused import.

The SecurityAutoConfiguration import is not used in this file.

Apply this diff:

-import org.springframework.boot.autoconfigure.security.servlet.SecurityAutoConfiguration;
-
 @SpringBootApplication
src/main/java/org/example/utils/BCryptUtil.java (2)

6-9: Add input validation to prevent null pointer exceptions.

Both methods should validate that input strings are not null before processing to avoid runtime NPEs.

Apply this diff:

 public static String hashPassword(String password) {
+  if (password == null) {
+    throw new IllegalArgumentException("Password cannot be null");
+  }
   String salt = BCrypt.gensalt(10);
   return BCrypt.hashpw(password, salt);
 }

11-13: Add input validation to prevent null pointer exceptions.

Validate inputs to avoid NPEs when either parameter is null.

Apply this diff:

 public static boolean checkPassword(String plainPassword, String hashedPassword) {
+  if (plainPassword == null || hashedPassword == null) {
+    return false;
+  }
   return BCrypt.checkpw(plainPassword, hashedPassword);
 }
src/main/java/org/example/exceptionHandler/GlobalExceptionHandler.java (1)

41-48: Use ErrorResponse DTO for consistency.

This handler returns an inline map while other handlers use the ErrorResponse DTO. Using the same DTO across all handlers improves consistency and type safety.

Apply this diff:

 @ExceptionHandler(ConflictException.class)
-public ResponseEntity<?> handleExists(ConflictException ex) {
-  return ResponseEntity.status(HttpStatus.CONFLICT)
-          .body(Map.of(
-                  "status", 409,
-                  "message", ex.getMessage()
-          ));
+public ResponseEntity<ErrorResponse> handleExists(ConflictException ex) {
+  ErrorResponse errorResponse = new ErrorResponse(ex.getMessage(), HttpStatus.CONFLICT.value());
+  return ResponseEntity.status(HttpStatus.CONFLICT).body(errorResponse);
 }
src/main/resources/application.properties (2)

5-5: Make database URL configurable for different environments.

The database URL is hardcoded to localhost:3306, which makes it difficult to deploy to different environments (staging, production, containers).

Apply this diff:

-spring.datasource.url=jdbc:mysql://localhost:3306/auth
+spring.datasource.url=${DATABASE_URL:jdbc:mysql://localhost:3306/auth}

11-11: Make SQL logging configurable for different environments.

SQL query logging enabled in production can cause performance overhead, log bloat, and potential information disclosure.

Apply this diff:

-spring.jpa.show-sql=true
+spring.jpa.show-sql=${SQL_LOGGING:false}
src/main/java/org/example/service/TokenService.java (2)

32-34: Consider using SecurityException for invalid tokens.

Throwing EntityNotFoundException for an invalid token is semantically imprecise. An invalid or expired token is an authentication/authorization failure, not a missing resource.

Apply this diff:

     if (!jwtUtil.validateJwtToken(refreshToken)) {
-      throw new EntityNotFoundException("Invalid refresh token");
+      throw new SecurityException("Invalid or expired refresh token");
     }

36-36: Consider making token expiry configurable.

The access token expiry (5 minutes) is hardcoded, making it difficult to adjust for different environments or security requirements without code changes.

Consider moving token expiry durations to application.properties:

jwt.access-token-expiry=300000
jwt.refresh-token-expiry=604800000

Then inject these values using @Value annotations.

src/main/java/org/example/service/NoteService.java (1)

47-52: Consider moving deleted-filtering into a repository query

user.getNotes().stream().filter(note -> note.getDeletedAt() == null) loads all notes into memory before filtering. For users with many notes, a repository method like findAllByUserAndDeletedAtIsNull (or equivalent) would be more scalable and explicit.

src/main/java/org/example/filter/ApiKeyFilter.java (1)

30-71: JWT filter behavior is correct; a few optional hardening tweaks

The filter correctly:

  • Skips non-/api/ URIs,
  • Validates the Bearer token,
  • Loads the user, and
  • Populates the SecurityContext with an ApiPrincipal.

Optional improvements:

  • Consider using @AuthenticationPrincipal ApiPrincipal downstream to avoid manual casting.
  • If you ever add /api (without trailing slash) endpoints, !path.startsWith("/api/") will bypass the filter for them; you might want a more robust path check.
  • If clients expect JSON error bodies, you could delegate 401 responses to a centralized handler instead of sendError.
src/main/java/org/example/controller/NoteController.java (1)

25-54: Endpoints are wired correctly; consider using @AuthenticationPrincipal and renaming the getter

The note endpoints correctly delegate to NoteService using the authenticated user id/email. Two small polish items:

  • Instead of accepting Authentication and casting, you can inject ApiPrincipal directly with @AuthenticationPrincipal ApiPrincipal principal to avoid potential ClassCastException if other auth types are ever introduced.
  • getUserById actually returns notes for the current user; a name like getUserNotes would better reflect its behavior.
src/main/java/org/example/service/UserService.java (1)

46-47: Externalize and de-duplicate token expiry configuration

The expiry durations (1000 * 60 * 5L and 1000 * 60 * 60 * 7 * 24L) are hard-coded here and (per other files) also in TokenService. Centralizing them (e.g., in configuration properties or a shared constants class) would:

  • Avoid divergence between registration, login, and refresh flows,
  • Make it easier to tune token lifetimes without code changes.

You might also annotate loginUser with @Transactional for clearer transactional boundaries around token updates, even though repository-level transactions already ensure atomic saves.

Also applies to: 66-67

src/test/java/org/example/controller/NoteControllerTest.java (1)

95-112: Make the expired-token test less timing-sensitive

Using jwtUtil.generateToken("test@email.com", 1L) relies on the request being processed after that 1 ms window has passed. That’s almost always true but still timing-dependent. Consider making the token definitively expired (e.g., by passing a negative or zero duration) so the test can’t ever pass or fail based on timing.

Example:

-    String token = jwtUtil.generateToken("test@email.com", 1L);
+    String token = jwtUtil.generateToken("test@email.com", -1_000L);
src/main/java/org/example/entity/UserEntity.java (1)

13-15: Consider encapsulating the notes collection to avoid direct external mutation

The bidirectional UserEntityNoteEntity mapping is fine, but exposing the mutable List<NoteEntity> directly allows callers to arbitrarily modify the persistence-managed collection.

You might later want to:

  • Keep the field private and
  • Add addNote/removeNote helpers and/or return an unmodifiable view from getNotes() to better control relationship consistency.

Also applies to: 67-73

pom.xml (1)

121-132: Reconsider the Mockito javaagent argLine configuration

The Surefire config injects a Mockito javaagent using:

-javaagent:"${settings.localRepository}/org/mockito/mockito-core/${mockito.version}/mockito-core-${mockito.version}.jar"

Potential issues:

  • It assumes mockito.version is defined and matches the jar in the local repo.
  • If the jar is missing, tests may fail to start due to an invalid -javaagent path.
  • Modern Mockito + JUnit setups usually don’t need this agent at all.

Unless you have a specific need for the agent, consider dropping this argLine entirely and relying on the default Spring Boot test setup.

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 8618163 and 11784cf.

📒 Files selected for processing (40)
  • JavaTemplate.iml (1 hunks)
  • docker-compose.yml (1 hunks)
  • pom.xml (1 hunks)
  • src/main/java/org/example/App.java (0 hunks)
  • src/main/java/org/example/Exercise2025Application.java (1 hunks)
  • src/main/java/org/example/config/SecurityConfig.java (1 hunks)
  • src/main/java/org/example/controller/NoteController.java (1 hunks)
  • src/main/java/org/example/controller/UserController.java (1 hunks)
  • src/main/java/org/example/dto/ErrorResponse.java (1 hunks)
  • src/main/java/org/example/dto/note/Note.java (1 hunks)
  • src/main/java/org/example/dto/note/NoteNew.java (1 hunks)
  • src/main/java/org/example/dto/note/NoteResponse.java (1 hunks)
  • src/main/java/org/example/dto/token/TokenRequest.java (1 hunks)
  • src/main/java/org/example/dto/token/UpdatedToken.java (1 hunks)
  • src/main/java/org/example/dto/user/ApiPrincipal.java (1 hunks)
  • src/main/java/org/example/dto/user/User.java (1 hunks)
  • src/main/java/org/example/dto/user/UserApi.java (1 hunks)
  • src/main/java/org/example/dto/user/UserCheck.java (1 hunks)
  • src/main/java/org/example/dto/user/UserNew.java (1 hunks)
  • src/main/java/org/example/dto/user/UserNotes.java (1 hunks)
  • src/main/java/org/example/entity/NoteEntity.java (1 hunks)
  • src/main/java/org/example/entity/TokenEntity.java (1 hunks)
  • src/main/java/org/example/entity/UserEntity.java (1 hunks)
  • src/main/java/org/example/exceptionHandler/ConflictException.java (1 hunks)
  • src/main/java/org/example/exceptionHandler/GlobalExceptionHandler.java (1 hunks)
  • src/main/java/org/example/filter/ApiKeyFilter.java (1 hunks)
  • src/main/java/org/example/repository/NoteRepository.java (1 hunks)
  • src/main/java/org/example/repository/TokenRepository.java (1 hunks)
  • src/main/java/org/example/repository/UserRepository.java (1 hunks)
  • src/main/java/org/example/service/NoteService.java (1 hunks)
  • src/main/java/org/example/service/TokenService.java (1 hunks)
  • src/main/java/org/example/service/UserService.java (1 hunks)
  • src/main/java/org/example/utils/BCryptUtil.java (1 hunks)
  • src/main/java/org/example/utils/JwtUtil.java (1 hunks)
  • src/main/resources/application.properties (1 hunks)
  • src/test/java/org/example/AppIT.java (0 hunks)
  • src/test/java/org/example/AppTest.java (0 hunks)
  • src/test/java/org/example/Exercise2025ApplicationTest.java (1 hunks)
  • src/test/java/org/example/controller/NoteControllerTest.java (1 hunks)
  • src/test/java/org/example/controller/UserControllerTest.java (1 hunks)
💤 Files with no reviewable changes (3)
  • src/test/java/org/example/AppIT.java
  • src/main/java/org/example/App.java
  • src/test/java/org/example/AppTest.java
🧰 Additional context used
🧠 Learnings (1)
📚 Learning: 2025-09-10T14:02:54.365Z
Learnt from: alfredbrannare
Repo: fungover/exercise2025 PR: 66
File: src/test/java/org/example/service/WarehouseTest.java:1-21
Timestamp: 2025-09-10T14:02:54.365Z
Learning: In the fungover/exercise2025 project, static mocking of LocalDateTime.now() works successfully with the existing Mockito setup without requiring mockito-inline dependency.

Applied to files:

  • src/test/java/org/example/Exercise2025ApplicationTest.java
🧬 Code graph analysis (10)
src/main/java/org/example/entity/TokenEntity.java (1)
src/main/java/org/example/entity/UserEntity.java (1)
  • Entity (10-98)
src/main/java/org/example/service/TokenService.java (2)
src/main/java/org/example/exceptionHandler/ConflictException.java (1)
  • ConflictException (3-8)
src/main/java/org/example/service/UserService.java (1)
  • Service (21-78)
src/main/java/org/example/controller/NoteController.java (1)
src/main/java/org/example/controller/UserController.java (1)
  • RestController (16-46)
src/main/java/org/example/utils/JwtUtil.java (1)
src/main/java/org/example/filter/ApiKeyFilter.java (1)
  • Component (19-72)
src/main/java/org/example/entity/UserEntity.java (2)
src/main/java/org/example/entity/NoteEntity.java (1)
  • Entity (8-73)
src/main/java/org/example/entity/TokenEntity.java (1)
  • Entity (6-89)
src/main/java/org/example/controller/UserController.java (1)
src/main/java/org/example/controller/NoteController.java (1)
  • RestController (15-55)
src/main/java/org/example/entity/NoteEntity.java (1)
src/main/java/org/example/entity/UserEntity.java (1)
  • Entity (10-98)
src/main/java/org/example/service/UserService.java (3)
src/main/java/org/example/exceptionHandler/ConflictException.java (1)
  • ConflictException (3-8)
src/main/java/org/example/service/TokenService.java (1)
  • Service (13-44)
src/main/java/org/example/utils/BCryptUtil.java (1)
  • BCryptUtil (4-14)
src/main/java/org/example/filter/ApiKeyFilter.java (1)
src/main/java/org/example/utils/JwtUtil.java (1)
  • Component (12-59)
src/test/java/org/example/controller/UserControllerTest.java (1)
src/test/java/org/example/Exercise2025ApplicationTest.java (1)
  • SpringBootTest (6-13)
🔇 Additional comments (24)
src/main/java/org/example/dto/user/ApiPrincipal.java (1)

1-3: LGTM!

Clean immutable record representing the authenticated user identity. The design is appropriate for use as a security principal.

src/main/java/org/example/exceptionHandler/ConflictException.java (1)

3-8: LGTM!

Clean exception design for handling user registration conflicts. The message format is clear and provides useful context.

src/main/java/org/example/dto/user/UserNotes.java (1)

1-8: LGTM!

Clean DTO design for returning user notes. The structure is straightforward and appropriate for the response payload.

src/main/java/org/example/dto/token/UpdatedToken.java (1)

1-4: LGTM!

Clean, minimal record for token refresh responses. The design is appropriate and straightforward.

src/main/java/org/example/dto/note/NoteResponse.java (1)

3-4: LGTM! Clean and concise DTO design.

The record provides a simple, immutable data carrier for note responses. The design is appropriate for its use case.

src/main/java/org/example/dto/ErrorResponse.java (1)

3-4: LGTM! Consistent error response structure.

The record provides a standardized format for error responses across the API. The design is clean and appropriate.

src/main/java/org/example/dto/note/NoteNew.java (1)

5-6: LGTM! Well-structured creation response DTO.

Including the createdAt timestamp in the response is good practice, allowing clients to display creation time without additional queries.

src/main/java/org/example/dto/user/UserNew.java (1)

3-4: LGTM! Appropriate registration response structure.

Returning both access token and refresh token in the registration response aligns with the PR objectives for JWT-based authentication. Ensure this endpoint is served over HTTPS to protect the tokens in transit.

src/main/java/org/example/dto/note/Note.java (1)

7-12: LGTM! Good validation and soft delete support.

The @NotNull validation on the value field ensures data integrity, and the deletedAt field supports soft delete patterns, which is valuable for audit trails and data recovery.

src/test/java/org/example/Exercise2025ApplicationTest.java (1)

6-11: LGTM! Standard Spring Boot smoke test.

This context load test is a valuable sanity check that ensures the application context can be initialized successfully, catching configuration issues early.

src/main/java/org/example/repository/TokenRepository.java (1)

8-11: Database indexes are properly configured—no action needed.

The TokenEntity already has appropriate indexes via unique = true constraints on both the token and refreshToken columns. In JPA/Hibernate, UNIQUE constraints automatically create database indexes, which will efficiently support the findByToken() and findByRefreshToken() queries. The implementation follows best practices.

src/main/java/org/example/config/SecurityConfig.java (2)

27-31: LGTM! Well-structured authorization rules.

The authorization configuration follows security best practices:

  • Public access for authentication endpoints (/user/**)
  • JWT-protected API endpoints (/api/**)
  • Explicit deny-all for unspecified paths (secure default)

25-25: CSRF protection is correctly disabled for this JWT authentication flow.

Verification confirms all three requirements are met:

  1. JWT tokens are passed via the Authorization header with Bearer scheme (not cookies) — confirmed in ApiKeyFilter.java and test cases
  2. No session-based authentication is configured — no session management or cookie-related code found in the codebase
  3. The application is purely stateless — JWT validation is performed statelessly through the filter chain in SecurityConfig.java

The CSRF disabling at line 25 is appropriate and secure for this implementation.

src/main/java/org/example/repository/UserRepository.java (1)

8-11: LGTM! Clean Spring Data JPA repository.

The repository interface follows Spring Data JPA best practices with properly named derived query methods and appropriate return types.

src/main/java/org/example/repository/NoteRepository.java (1)

11-13: LGTM! Well-implemented soft delete pattern.

The derived query methods correctly implement soft delete filtering with proper user scoping for security.

src/main/java/org/example/service/NoteService.java (1)

27-71: Note lifecycle and ownership logic looks solid

User existence checks, per-user note creation, and soft-delete via deletedAt with findByIdAndUserIdAndDeletedAtIsNull enforce correct ownership and avoid hard deletes. Transactional boundaries are appropriate.

src/main/java/org/example/service/UserService.java (1)

33-75: User registration & login flows align with the token model

Registration correctly:

  • Enforces unique email via existsByEmail,
  • Hashes the password before persisting the UserEntity, and
  • Creates an associated TokenEntity with access and refresh tokens, returning them in UserNew.

Login correctly:

  • Looks up the user by email,
  • Verifies the password via checkPassword,
  • Rotates both access and refresh tokens, persists them, and
  • Returns the new pair as TokenRequest.

Behavior matches the described “refresh requires refresh token + last access token” model when combined with TokenService.

src/main/java/org/example/controller/UserController.java (1)

28-38: User registration and login endpoints look consistent

DTO usage, validation, and returned statuses align with the intended flows; nothing blocking here.

src/main/java/org/example/entity/NoteEntity.java (1)

8-73: NoteEntity mapping looks consistent with UserEntity

The JPA mapping (ID, user relation, value, timestamps) aligns with the usage shown in UserEntity; no blocking issues spotted here.

src/main/java/org/example/entity/TokenEntity.java (1)

6-88: TokenEntity one-to-one mapping and constraints look good

The one-to-one association with UserEntity and the non-null/unique constraints on token and refreshToken match the described token lifecycle; no blockers here.

src/main/java/org/example/utils/JwtUtil.java (1)

42-56: The review comment incorrectly identifies the exception type

The review correctly identifies that signature verification exceptions are not being caught, but it misidentifies the exception class. For JWS signature verification failures, parseClaimsJws throws io.jsonwebtoken.security.SignatureException, not SecurityException. The bare SecurityException caught on line 46 is java.lang.SecurityException, which JJWT does not throw from parseClaimsJws for signature failures.

The correct fix is to catch io.jsonwebtoken.security.SignatureException:

-    } catch (SecurityException e) {
+    } catch (io.jsonwebtoken.security.SignatureException e) {
       System.out.println("Invalid JWT signature: " + e.getMessage());

Likely an incorrect or invalid review comment.

src/main/java/org/example/entity/UserEntity.java (2)

21-30: One-to-one token mapping looks correct; consider lifecycle expectations

The inverse one-to-one mapping to TokenEntity with cascade = CascadeType.ALL is consistent with TokenEntity owning the user_id column. Just make sure this matches your lifecycle expectations (e.g., deleting a user should indeed delete their token; clearing token may leave orphans unless explicitly removed).


83-97: equals/hashCode implementation is Hibernate‑friendly and id‑based

Using the Hibernate proxy‑aware pattern and basing equality solely on non‑null id is a solid choice for entities. Marking both methods final also prevents accidental overrides.

pom.xml (1)

29-31: No issue found. Java 25 is supported by Spring Boot 3.5.6.

Spring Boot 3.5.6 officially requires Java 17 as minimum and includes support for Java 24–25. The CI workflow correctly reads the maven.compiler.release property (automatically set by the Spring Boot parent based on <java.version>) and provisions the appropriate JDK. No build incompatibility exists.

Likely an incorrect or invalid review comment.

Comment thread docker-compose.yml
Comment thread JavaTemplate.iml
Comment thread src/main/java/org/example/controller/UserController.java
Comment thread src/main/java/org/example/entity/UserEntity.java
Comment thread src/main/java/org/example/exceptionHandler/GlobalExceptionHandler.java Outdated
Comment thread src/main/java/org/example/service/TokenService.java
Comment thread src/main/resources/application.properties Outdated
Comment thread src/main/resources/application.properties Outdated
Comment thread src/test/java/org/example/controller/NoteControllerTest.java
Comment thread src/test/java/org/example/controller/UserControllerTest.java
Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 6

♻️ Duplicate comments (2)
src/test/java/org/example/controller/UserControllerTest.java (1)

53-149: Tests remain order-dependent and fragile.

The previous review identified that tests rely on shared DB state (the user created in UserCanRegister at line 55), making them order-dependent. This issue has not been addressed—there's still no @BeforeEach or per-test isolation, so running tests individually or in a different order will fail.

Ensure each test seeds its required user state:

@BeforeEach
void setUp() {
  // Register the test user that other tests depend on
  User testUser = new User("User", "password", "user@user.com");
  try {
    mockMvc.perform(post("/user/register")
      .contentType(MediaType.APPLICATION_JSON)
      .content(objectMapper.writeValueAsString(testUser)));
  } catch (Exception e) {
    // User may already exist; ignore for idempotency
  }
}

Alternatively, use unique emails per test or add a cleanup method with @DynamicPropertySource to reset the DB between tests.

src/main/java/org/example/Exercise2025AuthApplication.java (1)

9-11: This issue has already been flagged in a previous review.

The main method is missing the public static modifiers, preventing the application from starting. Please refer to the existing review comment above for the fix.

🧹 Nitpick comments (13)
src/main/java/org/example/dto/user/UserCheck.java (1)

3-4: Remove unused import.

The @NotNull import is unused—only @NotBlank is applied.

Apply this diff:

 import jakarta.validation.constraints.NotBlank;
-import jakarta.validation.constraints.NotNull;
src/main/java/org/example/controller/UserController.java (1)

19-20: Consider making service fields private.

Public fields are unconventional for Spring-injected dependencies. Private fields with constructor injection are the standard practice.

Apply this diff:

-  public final UserService userService;
-  public final TokenService tokenService;
+  private final UserService userService;
+  private final TokenService tokenService;
src/main/java/org/example/dto/user/User.java (1)

3-4: Remove unused import.

The @NotNull import is unused—only @NotBlank is applied.

Apply this diff:

 import jakarta.validation.constraints.NotBlank;
-import jakarta.validation.constraints.NotNull;
src/main/java/org/example/entity/TokenEntity.java (3)

9-11: Remove redundant nullable = false on primary key.

Primary keys are implicitly non-null, so @Column(nullable = false) on @Id is redundant.

Apply this diff:

   @Id
-  @Column(nullable = false)
   @GeneratedValue(strategy = GenerationType.IDENTITY)
   private Long id;

24-25: Document the purpose of the counter field.

The counter field's purpose is unclear. Add a JavaDoc comment explaining its role (e.g., token version, usage count, or replay protection).


8-89: Consider implementing equals and hashCode for entity consistency.

UserEntity implements proxy-aware equals and hashCode, but TokenEntity does not. For consistency and to avoid collection/caching issues with Hibernate proxies, consider adding the same pattern here.

Add at the end of the class:

  @Override
  public final boolean equals(Object o) {
    if (this == o) return true;
    if (o == null) return false;
    Class<?> oEffectiveClass = o instanceof HibernateProxy ? ((HibernateProxy) o).getHibernateLazyInitializer().getPersistentClass() : o.getClass();
    Class<?> thisEffectiveClass = this instanceof HibernateProxy ? ((HibernateProxy) this).getHibernateLazyInitializer().getPersistentClass() : this.getClass();
    if (thisEffectiveClass != oEffectiveClass) return false;
    TokenEntity that = (TokenEntity) o;
    return getId() != null && Objects.equals(getId(), that.getId());
  }

  @Override
  public final int hashCode() {
    return this instanceof HibernateProxy ? ((HibernateProxy) this).getHibernateLazyInitializer().getPersistentClass().hashCode() : getClass().hashCode();
  }

Don't forget to import:

import org.hibernate.proxy.HibernateProxy;
import java.util.Objects;
src/main/java/org/example/entity/UserEntity.java (2)

35-35: Constructor parameter name misleading.

The parameter is named passwordHashed, but the field is password. This suggests the password is already hashed when constructing the entity, but the parameter name could mislead callers. Consider renaming the parameter to password for consistency, or ensure documentation clarifies that this expects a hashed value.

Apply this diff:

-  public UserEntity(Long id, String name, String passwordHashed, String email, List<NoteEntity> notes) {
+  public UserEntity(Long id, String name, String password, String email, List<NoteEntity> notes) {
     this.id = id;
     this.name = name;
-    this.password = passwordHashed;
+    this.password = password;
     this.email = email;
     this.notes = notes;
   }

20-20: Consider adding unique constraint on email field.

If the business logic requires unique user emails (typical for authentication systems), add a @Column(unique = true) constraint on the email field to enforce this at the database level.

Apply this diff:

   private String name;
   private String password;
-  private String email;
+  @Column(unique = true)
+  private String email;
src/main/java/org/example/entity/NoteEntity.java (3)

15-17: Consider specifying FetchType.LAZY on ManyToOne.

The @ManyToOne relationship defaults to FetchType.EAGER, which can cause N+1 query problems when loading collections of notes. Explicitly set fetch = FetchType.LAZY to avoid unnecessary joins.

Apply this diff:

-  @ManyToOne
+  @ManyToOne(fetch = FetchType.LAZY)
   @JoinColumn(name = "user_id")
   private UserEntity user;

18-18: Consider adding validation or constraints on value field.

The value field has no @Column constraints or validation. If notes should never be empty or null, add nullable = false or a length constraint.

Apply this diff:

-  private String value;
+  @Column(nullable = false)
+  private String value;

10-73: Consider implementing equals and hashCode.

For consistency with UserEntity and to avoid collection/caching issues with Hibernate proxies, consider adding proxy-aware equals and hashCode methods.

Add at the end of the class:

  @Override
  public final boolean equals(Object o) {
    if (this == o) return true;
    if (o == null) return false;
    Class<?> oEffectiveClass = o instanceof HibernateProxy ? ((HibernateProxy) o).getHibernateLazyInitializer().getPersistentClass() : o.getClass();
    Class<?> thisEffectiveClass = this instanceof HibernateProxy ? ((HibernateProxy) this).getHibernateLazyInitializer().getPersistentClass() : this.getClass();
    if (thisEffectiveClass != oEffectiveClass) return false;
    NoteEntity that = (NoteEntity) o;
    return getId() != null && Objects.equals(getId(), that.getId());
  }

  @Override
  public final int hashCode() {
    return this instanceof HibernateProxy ? ((HibernateProxy) this).getHibernateLazyInitializer().getPersistentClass().hashCode() : getClass().hashCode();
  }

Don't forget to import:

import org.hibernate.proxy.HibernateProxy;
import java.util.Objects;
README.md (1)

1-9: Provide more detailed setup instructions.

The current documentation is too minimal to be actionable. Consider adding:

  • Specific Docker commands (e.g., docker-compose up)
  • How to run the application (e.g., mvn spring-boot:run)
  • Required environment variables or configuration
  • Default port and endpoints
  • Any prerequisites (Java version, Maven version)
pom.xml (1)

17-28: Remove or populate empty metadata sections.

The <licenses>, <developers>, and <scm> sections contain empty elements. Either remove these sections or populate them with actual information to maintain clean project metadata.

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 11784cf and 6e54a6e.

📒 Files selected for processing (17)
  • README.md (1 hunks)
  • pom.xml (1 hunks)
  • src/main/java/org/example/Exercise2025AuthApplication.java (1 hunks)
  • src/main/java/org/example/controller/UserController.java (1 hunks)
  • src/main/java/org/example/dto/token/TokenRequest.java (1 hunks)
  • src/main/java/org/example/dto/user/User.java (1 hunks)
  • src/main/java/org/example/dto/user/UserCheck.java (1 hunks)
  • src/main/java/org/example/entity/NoteEntity.java (1 hunks)
  • src/main/java/org/example/entity/TokenEntity.java (1 hunks)
  • src/main/java/org/example/entity/UserEntity.java (1 hunks)
  • src/main/java/org/example/exceptionHandler/GlobalExceptionHandler.java (1 hunks)
  • src/main/java/org/example/service/TokenService.java (1 hunks)
  • src/main/java/org/example/utils/BCryptUtil.java (1 hunks)
  • src/main/resources/application-dev.properties (1 hunks)
  • src/main/resources/application.properties (1 hunks)
  • src/test/java/org/example/controller/NoteControllerTest.java (1 hunks)
  • src/test/java/org/example/controller/UserControllerTest.java (1 hunks)
🚧 Files skipped from review as they are similar to previous changes (5)
  • src/main/resources/application.properties
  • src/main/java/org/example/exceptionHandler/GlobalExceptionHandler.java
  • src/main/java/org/example/utils/BCryptUtil.java
  • src/main/java/org/example/service/TokenService.java
  • src/main/java/org/example/dto/token/TokenRequest.java
🧰 Additional context used
🧬 Code graph analysis (6)
src/test/java/org/example/controller/NoteControllerTest.java (1)
src/test/java/org/example/controller/UserControllerTest.java (1)
  • Testcontainers (24-150)
src/main/java/org/example/controller/UserController.java (1)
src/main/java/org/example/controller/NoteController.java (1)
  • RestController (15-55)
src/main/java/org/example/entity/NoteEntity.java (1)
src/main/java/org/example/entity/UserEntity.java (1)
  • Entity (10-98)
src/main/java/org/example/entity/TokenEntity.java (2)
src/main/java/org/example/entity/NoteEntity.java (1)
  • Entity (8-73)
src/main/java/org/example/entity/UserEntity.java (1)
  • Entity (10-98)
src/test/java/org/example/controller/UserControllerTest.java (1)
src/test/java/org/example/controller/NoteControllerTest.java (1)
  • Testcontainers (24-173)
src/main/java/org/example/entity/UserEntity.java (2)
src/main/java/org/example/entity/NoteEntity.java (1)
  • Entity (8-73)
src/main/java/org/example/entity/TokenEntity.java (1)
  • Entity (6-89)
🔇 Additional comments (4)
src/test/java/org/example/controller/NoteControllerTest.java (1)

132-132: LGTM! Hardcoded IDs replaced with dynamic retrieval.

The previous review flagged hardcoded note IDs. This has been fixed—both tests now retrieve the actual persisted note ID from the repository.

Also applies to: 164-164

src/main/java/org/example/entity/UserEntity.java (1)

11-11: LGTM! Reserved keyword issue resolved.

The previous review flagged the use of reserved SQL keyword "user". This has been fixed—the table is now named "users".

src/main/resources/application-dev.properties (1)

1-15: Note: Inconsistency between AI summary and actual code.

The AI summary indicates this file uses environment variables like ${DATABASE_URL}, but the actual code contains hardcoded values. This inconsistency has been addressed in the above comments.

pom.xml (1)

30-30: No issues found—Java 25 is released and stable for production use.

Java 25 reached General Availability on September 16, 2025, with GA builds available for production use from major vendors including Oracle and Microsoft. The configuration in pom.xml is valid.

Likely an incorrect or invalid review comment.

Comment thread pom.xml
Comment thread pom.xml
Comment thread src/main/java/org/example/controller/UserController.java
Comment thread src/main/java/org/example/controller/UserController.java
Comment thread src/main/resources/application-dev.properties
Comment thread src/main/resources/application-dev.properties
Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 2

♻️ Duplicate comments (1)
pom.xml (1)

64-68: Address the outdated jbcrypt dependency (unresolved from previous review).

This is a carryover from the previous review. jbcrypt 0.4 was released in February 2017 and the original project is no longer actively maintained. For improved security and maintenance, migrate to a modern alternative: either use a maintained fork like at.favre.lib:bcrypt, or for new projects, prefer a memory-hard algorithm like Argon2 via argon2-jvm or Password4j.

🧹 Nitpick comments (3)
pom.xml (1)

16-28: Fill in empty placeholder values in project metadata.

Lines 16–28 contain empty or placeholder values for <url/>, <license/>, <developer/>, and SCM connection details. While this doesn't affect functionality, it's good practice to populate these fields with accurate project information for documentation and build metadata clarity.

src/main/java/org/example/utils/JwtUtil.java (2)

20-23: Consider validating JWT secret strength during initialization.

The init() method initializes the key from the injected secret but doesn't validate that the secret meets minimum security requirements (e.g., sufficient length/entropy). A weak or missing secret could compromise the entire authentication system.

Consider adding validation in the init() method:

 @PostConstruct
 public void init() {
+  if (jwtSecret == null || jwtSecret.isEmpty()) {
+    throw new IllegalStateException("JWT secret is not configured");
+  }
+  byte[] decodedKey = Decoders.BASE64.decode(jwtSecret);
+  if (decodedKey.length < 32) {
+    throw new IllegalStateException("JWT secret must be at least 256 bits (32 bytes)");
+  }
-  this.key = Keys.hmacShaKeyFor(Decoders.BASE64.decode(jwtSecret));
+  this.key = Keys.hmacShaKeyFor(decodedKey);
 }

25-36: Add input validation for token generation.

The method doesn't validate input parameters. A null email or negative expiration time could lead to unexpected behavior or security issues.

Add parameter validation:

 public String generateToken(String email, Long jwtExpirationMs) {
+  if (email == null || email.isEmpty()) {
+    throw new IllegalArgumentException("Email cannot be null or empty");
+  }
+  if (jwtExpirationMs == null || jwtExpirationMs <= 0) {
+    throw new IllegalArgumentException("Expiration time must be positive");
+  }
+
   long nowMillis = System.currentTimeMillis();
   Date now = new Date(nowMillis);
   Date expirationDate = new Date(nowMillis + jwtExpirationMs);
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 6e54a6e and 7d167d9.

📒 Files selected for processing (2)
  • pom.xml (1 hunks)
  • src/main/java/org/example/utils/JwtUtil.java (1 hunks)
🧰 Additional context used
🧬 Code graph analysis (1)
src/main/java/org/example/utils/JwtUtil.java (1)
src/main/java/org/example/filter/ApiKeyFilter.java (1)
  • Component (19-72)
🔇 Additional comments (2)
pom.xml (2)

83-98: Excellent: JJWT dependencies upgraded to latest stable version.

The three JJWT dependencies (jjwt-api, jjwt-impl, jjwt-jackson) have been correctly updated to version 0.13.0 (the latest stable release as of August 2025), addressing the previous review feedback. This ensures you benefit from the latest security patches and bug fixes.


30-30: Java 25 is officially supported by Spring Boot 3.5.6.

Spring Boot 3.5.x is documented as compatible with Java 25 (requires Java 17+ and supports up through Java 25). No compatibility issues with this configuration.

Comment thread src/main/java/org/example/utils/JwtUtil.java
Comment thread src/main/java/org/example/utils/JwtUtil.java
Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 3

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 7d167d9 and 4d089ef.

📒 Files selected for processing (1)
  • README.md (1 hunks)

Comment thread README.md Outdated
Comment thread README.md
Comment thread README.md Outdated
@fungover fungover deleted a comment from coderabbitai Bot Nov 24, 2025
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.

2 participants