Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
package org.example.visacasemanagementsystem.comment.controller;

import jakarta.validation.Valid;
import org.example.visacasemanagementsystem.comment.dto.CommentDTO;
import org.example.visacasemanagementsystem.comment.dto.CreateCommentDTO;
import org.example.visacasemanagementsystem.comment.service.CommentService;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;

import java.util.List;

@RestController
@RequestMapping("/api/comments")
public class CommentController {

private final CommentService commentService;

public CommentController(CommentService commentService) {
this.commentService = commentService;
}

// Create new comment
@PostMapping
public ResponseEntity<CommentDTO> createComment(@Valid @RequestBody CreateCommentDTO dto){
CommentDTO createComment = commentService.createComment(dto);
return new ResponseEntity<>(createComment, HttpStatus.CREATED);
}

// Get all comments for a selected visa case
@GetMapping("/visa/{visaId}")
public ResponseEntity<List<CommentDTO>> getCommentsByVisa(@PathVariable Long visaId){
List<CommentDTO> comments = commentService.getCommentsByVisaId(visaId);
return new ResponseEntity<>(comments, HttpStatus.OK);

}
}
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@

public record CreateCommentDTO(
@NotNull(message = "Visa ID is required") Long visaId,
@NotNull(message = "Author ID is required") Long authorId,
@NotNull(message = "Author ID is required") Long authorId, // Todo: Remove this and let Spring Security handle it
@NotBlank(message = "Comment text cannot be empty") String text
) {
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
package org.example.visacasemanagementsystem.comment.repository;

import org.example.visacasemanagementsystem.comment.entity.Comment;
import org.springframework.data.jpa.repository.JpaRepository;

import java.util.List;

public interface CommentRepository extends JpaRepository<Comment, Long> {

List<Comment> findByVisaIdOrderByCreatedAtDesc(Long visaId);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
package org.example.visacasemanagementsystem.comment.service;

import org.example.visacasemanagementsystem.comment.dto.CommentDTO;
import org.example.visacasemanagementsystem.comment.dto.CreateCommentDTO;
import org.example.visacasemanagementsystem.comment.entity.Comment;
import org.example.visacasemanagementsystem.comment.mapper.CommentMapper;
import org.example.visacasemanagementsystem.comment.repository.CommentRepository;
import org.example.visacasemanagementsystem.exception.ResourceNotFoundException;
import org.example.visacasemanagementsystem.user.entity.User;
import org.example.visacasemanagementsystem.user.repository.UserRepository;
import org.example.visacasemanagementsystem.visa.entity.Visa;
import org.example.visacasemanagementsystem.visa.repository.VisaRepository;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

import java.util.List;

@Service
public class CommentService {

private final CommentRepository commentRepository;
private final CommentMapper commentMapper;
private final UserRepository userRepository;
private final VisaRepository visaRepository;

public CommentService(CommentRepository commentRepository, CommentMapper commentMapper , UserRepository userRepository, VisaRepository visaRepository) {
this.commentRepository = commentRepository;
this.commentMapper = commentMapper;
this.userRepository = userRepository;
this.visaRepository = visaRepository;
}


// Create Comment
// TODO: Security Risk - Replace authorId from DTO with authenticated user from
// SecurityContext once Spring Security is integrated to prevent IDOR vulnerabilities.
@Transactional
public CommentDTO createComment(CreateCommentDTO dto) {
if (dto == null) {
throw new IllegalArgumentException("Comment payload cannot be null");
}

if (dto.text() == null || dto.text().isBlank()) {
throw new IllegalArgumentException("Comment text cannot be empty");
Comment thread
coderabbitai[bot] marked this conversation as resolved.
}

// Get User and Visa from database
User author = userRepository.findById(dto.authorId())
.orElseThrow(() -> new ResourceNotFoundException("User not found with id: " + dto.authorId()));
Comment on lines +48 to +49

@coderabbitai coderabbitai Bot Apr 9, 2026

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🔴 Critical

Do not trust client-supplied authorId for comment ownership.

Using dto.authorId() directly lets a caller create comments as another user. Resolve author from authenticated context and ignore client-provided identity.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@src/main/java/org/example/visacasemanagementsystem/comment/service/CommentService.java`
around lines 42 - 43, The code currently uses dto.authorId() to set comment
ownership (see User author assignment in CommentService), which allows clients
to impersonate others; instead resolve the author from the authenticated context
and ignore the client-supplied id: obtain the current user's id via the app's
auth utility (e.g., SecurityContextHolder/your AuthService method) inside the
CommentService create method, call
userRepository.findById(currentUserId()).orElseThrow(...) to load the User, and
remove/ignore dto.authorId() so ownership is always set from the authenticated
principal rather than client input.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

This is a valid point. This issue will be resolved later on when Spring Security is integrated in the project.

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

@eeebbaandersson, understood! That makes sense — resolving the author from the authenticated principal is the right approach once Spring Security is in place (e.g., via SecurityContextHolder.getContext().getAuthentication() or a custom AuthService). At that point, dto.authorId() can be removed from CreateCommentDTO entirely, or kept only for admin-level use cases with proper authorization checks.

I'll note this for future reviews.


✏️ Learnings added
Learnt from: eeebbaandersson
Repo: ithsjava25/visa-case-management-system PR: 16
File: src/main/java/org/example/visacasemanagementsystem/comment/service/CommentService.java:42-43
Timestamp: 2026-04-09T11:12:57.537Z
Learning: In the `visa-case-management-system` repository, the `authorId` field in `CreateCommentDTO` is currently trusted directly in `CommentService.createComment()` (file: `src/main/java/org/example/visacasemanagementsystem/comment/service/CommentService.java`, around line 42). This is a known security gap intentionally deferred until Spring Security is integrated. Once Spring Security is added, the author should be resolved from the authenticated principal (e.g., via `SecurityContextHolder`) rather than from `dto.authorId()`, and the `authorId` field should be removed from `CreateCommentDTO` or restricted to admin use only.

Note: Learnings are effective only in the context of similar code segments. To apply general coding guidelines, please configure review instructions. You can manage existing learnings in the app.


🧠 Learnings used
Learnt from: Martin-E-Karlsson
Repo: ithsjava25/visa-case-management-system PR: 14
File: src/main/java/org/example/visacasemanagementsystem/user/service/UserService.java:53-53
Timestamp: 2026-04-08T23:24:51.563Z
Learning: In the `visa-case-management-system` repository, password encryption in `UserService.createUser()` (file: `src/main/java/org/example/visacasemanagementsystem/user/service/UserService.java`, line ~53) is intentionally deferred until Spring Security is integrated. The current `user.setPassword(dto.password())` is a temporary plaintext placeholder. When Spring Security is added, a `BCryptPasswordEncoder` bean should be configured and injected into `UserService`, replacing the direct assignment with `passwordEncoder.encode(dto.password())`.


Visa visa = visaRepository.findById(dto.visaId())
.orElseThrow(() -> new ResourceNotFoundException("Visa case not found with id: " + dto.visaId()));

// Map DTO to Entity
Comment comment = commentMapper.toEntity(dto);

// Set author and visaCase
comment.setAuthor(author);
comment.setVisa(visa);

// Save and return
Comment savedComment = commentRepository.save(comment);
return commentMapper.toDTO(savedComment);
}

// Get all comments for a selected visa case
@Transactional (readOnly = true)
public List<CommentDTO> getCommentsByVisaId(Long visaId) {
if (visaId == null || visaId <= 0) {
throw new IllegalArgumentException("Visa ID must be a positive number");
}

List<Comment> comments = commentRepository.findByVisaIdOrderByCreatedAtDesc(visaId);
if(comments.isEmpty() && !visaRepository.existsById(visaId)) {
throw new ResourceNotFoundException("Visa case not found with id: " + visaId);
Comment thread
coderabbitai[bot] marked this conversation as resolved.
}

return comments.stream()
.map(commentMapper::toDTO)
.toList();
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -18,9 +18,16 @@ public String handleUnauthorizedException(UnauthorizedException exception) {
}

@ResponseStatus(value = HttpStatus.NOT_FOUND)
@ExceptionHandler(EntityNotFoundException.class)
@ExceptionHandler({EntityNotFoundException.class, ResourceNotFoundException.class})
@ResponseBody
public String handleNotFoundException(EntityNotFoundException exception) {
public String handleNotFoundException(RuntimeException exception) {
return exception.getMessage();
Comment thread
coderabbitai[bot] marked this conversation as resolved.
}

@ResponseStatus(value = HttpStatus.BAD_REQUEST)
@ExceptionHandler(IllegalArgumentException.class)
@ResponseBody
public String handleIllegalArgumentException(IllegalArgumentException exception) {
return "Invalid Request: " + exception.getMessage();
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
package org.example.visacasemanagementsystem.exception;

public class ResourceNotFoundException extends RuntimeException {
public ResourceNotFoundException(String message) {
super(message);
}
}