Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
22 commits
Select commit Hold shift + click to select a range
88b923c
feat: implement endpoints for VisaViewController and basic Thymeleaf …
eeebbaandersson Apr 10, 2026
30ce174
feat: extend Global exception handler to handle Internal Server Error
eeebbaandersson Apr 11, 2026
a25a984
feat: update visa workflow with assigned status, handler mapping, and…
eeebbaandersson Apr 11, 2026
0568740
fix: finalize parameter binding for info requests and add controller …
eeebbaandersson Apr 11, 2026
7237829
feat: implement role-based sorting for applications
eeebbaandersson Apr 12, 2026
9717435
feat: implement dark theme and extend visa application with travel de…
eeebbaandersson Apr 12, 2026
47dfe1a
feat: add field validation to visa application form
eeebbaandersson Apr 12, 2026
64a70cb
test: Implement tests for VisaMapper
eeebbaandersson Apr 12, 2026
c5ec8b2
fix: resolve validation error on visa application submit
eeebbaandersson Apr 13, 2026
584de54
feat: add visa application update functionality and handler feedback
eeebbaandersson Apr 13, 2026
378519f
fix: relocate visaMapper
eeebbaandersson Apr 13, 2026
1322f3c
fix: resolve compilation errors in pipeline due to package mismatch
eeebbaandersson Apr 13, 2026
2fd457a
test: Initial start for VisaServiceTest
eeebbaandersson Apr 13, 2026
77e4e58
test: extend test suite for VisaServiceTest
eeebbaandersson Apr 13, 2026
d16cec6
fix: suggested fixes from CodeRabbit
eeebbaandersson Apr 14, 2026
ea89dab
fix: more CodeRabbit feedback
eeebbaandersson Apr 14, 2026
0f4002a
fix: removed unused import
eeebbaandersson Apr 14, 2026
f17034a
merge: sync with main and resolve conflicts in Visarepository and Vis…
eeebbaandersson Apr 14, 2026
b84820b
fix: more suggested fixed from CodeRabbit
eeebbaandersson Apr 14, 2026
03701f6
fix: update tests in visaServiceTest to align with recent change in V…
eeebbaandersson Apr 14, 2026
4ffd190
fix: update visa validation and apply/edit-form to handle global errors
eeebbaandersson Apr 14, 2026
5f4d789
fix: last suggested fix from CodeRabbit
eeebbaandersson Apr 14, 2026
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
@@ -1,7 +1,12 @@
package org.example.visacasemanagementsystem;

import org.example.visacasemanagementsystem.user.UserAuthorization;
import org.example.visacasemanagementsystem.user.entity.User;
import org.example.visacasemanagementsystem.user.repository.UserRepository;
import org.springframework.boot.CommandLineRunner;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.context.annotation.Bean;
import org.springframework.data.jpa.repository.config.EnableJpaAuditing;

@SpringBootApplication
Expand All @@ -12,4 +17,39 @@ public static void main(String[] args) {
SpringApplication.run(VisaCaseManagementSystemApplication.class, args);
}

@Bean
public CommandLineRunner initData(UserRepository userRepository) {
return args -> {
// Skapa en vanlig användare (Applicant)
if (userRepository.findByEmail("user@test.com").isEmpty()) {
User user = new User();
user.setFullName("USER");
user.setEmail("user@test.com");
user.setPassword("password"); // Notera: Du behöver sätta lösenord om din entitet kräver det
user.setUserAuthorization(UserAuthorization.USER);
userRepository.save(user);
System.out.println("Testanvändare skapad med ID: " + user.getId());

User admin = new User();
admin.setFullName("ADMIN");
admin.setEmail("user@test.com2");
admin.setPassword("password"); // Notera: Du behöver sätta lösenord om din entitet kräver det
admin.setUserAuthorization(UserAuthorization.ADMIN);
userRepository.save(admin);
System.out.println("Test-admin skapad med ID: " + admin.getId());
Comment on lines +24 to +39

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 | 🟠 Major

Seed each account behind its own lookup.

Lines 24-39 gate both the applicant and admin inserts on findByEmail("user@test.com"), even though the admin is saved with a different email on Line 35. If the applicant exists but the admin row is missing, the admin will never be recreated.

🧩 Suggested fix
-            if (userRepository.findByEmail("user@test.com").isEmpty()) {
+            if (userRepository.findByEmail("user@test.com").isEmpty()) {
                 User user = new User();
                 user.setFullName("USER");
                 user.setEmail("user@test.com");
                 user.setPassword("password");
                 user.setUserAuthorization(UserAuthorization.USER);
                 userRepository.save(user);
                 System.out.println("Testanvändare skapad med ID: " + user.getId());
+            }
 
+            if (userRepository.findByEmail("user@test.com2").isEmpty()) {
                 User admin = new User();
                 admin.setFullName("ADMIN");
                 admin.setEmail("user@test.com2");
                 admin.setPassword("password");
                 admin.setUserAuthorization(UserAuthorization.ADMIN);
                 userRepository.save(admin);
                 System.out.println("Test-admin skapad med ID: " + admin.getId());
             }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
if (userRepository.findByEmail("user@test.com").isEmpty()) {
User user = new User();
user.setFullName("USER");
user.setEmail("user@test.com");
user.setPassword("password"); // Notera: Du behöver sätta lösenord om din entitet kräver det
user.setUserAuthorization(UserAuthorization.USER);
userRepository.save(user);
System.out.println("Testanvändare skapad med ID: " + user.getId());
User admin = new User();
admin.setFullName("ADMIN");
admin.setEmail("user@test.com2");
admin.setPassword("password"); // Notera: Du behöver sätta lösenord om din entitet kräver det
admin.setUserAuthorization(UserAuthorization.ADMIN);
userRepository.save(admin);
System.out.println("Test-admin skapad med ID: " + admin.getId());
if (userRepository.findByEmail("user@test.com").isEmpty()) {
User user = new User();
user.setFullName("USER");
user.setEmail("user@test.com");
user.setPassword("password"); // Notera: Du behöver sätta lösenord om din entitet kräver det
user.setUserAuthorization(UserAuthorization.USER);
userRepository.save(user);
System.out.println("Testanvändare skapad med ID: " + user.getId());
}
if (userRepository.findByEmail("user@test.com2").isEmpty()) {
User admin = new User();
admin.setFullName("ADMIN");
admin.setEmail("user@test.com2");
admin.setPassword("password"); // Notera: Du behöver sätta lösenord om din entitet kräver det
admin.setUserAuthorization(UserAuthorization.ADMIN);
userRepository.save(admin);
System.out.println("Test-admin skapad med ID: " + admin.getId());
}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@src/main/java/org/example/visacasemanagementsystem/VisaCaseManagementSystemApplication.java`
around lines 24 - 39, The current seeding block checks only
userRepository.findByEmail("user@test.com") before creating both the regular
User and the Admin, so if the user exists but the admin (created as
admin.setEmail("user@test.com2")) is missing it will never be created; update
the logic to perform separate existence checks and inserts per email by calling
userRepository.findByEmail("user@test.com") before creating the User and
userRepository.findByEmail("user@test.com2") before creating the Admin (refer to
userRepository.findByEmail(...) and the User/ admin creation blocks) so each
account is seeded independently.

}

// Skapa en admin (Handler)
if (userRepository.findByEmail("admin@test.com").isEmpty()) {
User sysadmin = new User();
sysadmin.setFullName("SYSTEM ADMIN");
sysadmin.setEmail("admin@test.com");
sysadmin.setPassword("password");
sysadmin.setUserAuthorization(UserAuthorization.SYSADMIN);
userRepository.save(sysadmin);
System.out.println("Test-sysadmin skapad med ID: " + sysadmin.getId());
}
};
Comment on lines +20 to +52

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

Guard this seed runner behind a non-production profile.

This bean creates known accounts with predictable credentials on every startup. In any shared or production-like environment, that becomes a built-in backdoor rather than test data.

🔒 Safer baseline
+import org.springframework.context.annotation.Profile;
 ...
-    `@Bean`
+    `@Bean`
+    `@Profile`("dev")
     public CommandLineRunner initData(UserRepository userRepository) {
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
@Bean
public CommandLineRunner initData(UserRepository userRepository) {
return args -> {
// Skapa en vanlig användare (Applicant)
if (userRepository.findByEmail("user@test.com").isEmpty()) {
User user = new User();
user.setFullName("USER");
user.setEmail("user@test.com");
user.setPassword("password"); // Notera: Du behöver sätta lösenord om din entitet kräver det
user.setUserAuthorization(UserAuthorization.USER);
userRepository.save(user);
System.out.println("Testanvändare skapad med ID: " + user.getId());
User admin = new User();
admin.setFullName("ADMIN");
admin.setEmail("user@test.com2");
admin.setPassword("password"); // Notera: Du behöver sätta lösenord om din entitet kräver det
admin.setUserAuthorization(UserAuthorization.ADMIN);
userRepository.save(admin);
System.out.println("Test-admin skapad med ID: " + admin.getId());
}
// Skapa en admin (Handler)
if (userRepository.findByEmail("admin@test.com").isEmpty()) {
User sysadmin = new User();
sysadmin.setFullName("SYSTEM ADMIN");
sysadmin.setEmail("admin@test.com");
sysadmin.setPassword("password");
sysadmin.setUserAuthorization(UserAuthorization.SYSADMIN);
userRepository.save(sysadmin);
System.out.println("Test-sysadmin skapad med ID: " + sysadmin.getId());
}
};
`@Bean`
`@Profile`("dev")
public CommandLineRunner initData(UserRepository userRepository) {
return args -> {
// Skapa en vanlig användare (Applicant)
if (userRepository.findByEmail("user@test.com").isEmpty()) {
User user = new User();
user.setFullName("USER");
user.setEmail("user@test.com");
user.setPassword("password"); // Notera: Du behöver sätta lösenord om din entitet kräver det
user.setUserAuthorization(UserAuthorization.USER);
userRepository.save(user);
System.out.println("Testanvändare skapad med ID: " + user.getId());
User admin = new User();
admin.setFullName("ADMIN");
admin.setEmail("user@test.com2");
admin.setPassword("password"); // Notera: Du behöver sätta lösenord om din entitet kräver det
admin.setUserAuthorization(UserAuthorization.ADMIN);
userRepository.save(admin);
System.out.println("Test-admin skapad med ID: " + admin.getId());
}
// Skapa en admin (Handler)
if (userRepository.findByEmail("admin@test.com").isEmpty()) {
User sysadmin = new User();
sysadmin.setFullName("SYSTEM ADMIN");
sysadmin.setEmail("admin@test.com");
sysadmin.setPassword("password");
sysadmin.setUserAuthorization(UserAuthorization.SYSADMIN);
userRepository.save(sysadmin);
System.out.println("Test-sysadmin skapad med ID: " + sysadmin.getId());
}
};
}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@src/main/java/org/example/visacasemanagementsystem/VisaCaseManagementSystemApplication.java`
around lines 20 - 52, The initData CommandLineRunner currently seeds predictable
accounts on every startup; guard it behind a non-production profile by making
the bean conditional so it only runs in dev/test environments (e.g., annotate
the initData bean or method with `@Profile`("!prod") or use
`@Profile`({"dev","test"}) / `@ConditionalOnProperty`) and/or check the active
Spring Environment inside initData before creating users; update references to
UserRepository and the initData method so seeding occurs only when the active
profile is not production and remove or externalize hard-coded credentials.

}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
package org.example.visacasemanagementsystem.config;

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.web.SecurityFilterChain;

@Configuration
public class SecurityConfig {

@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
http
.authorizeHttpRequests(auth -> auth
.anyRequest().permitAll()
)
.csrf(csrf -> csrf.disable())
.headers(headers -> headers.frameOptions(frame -> frame.disable()));
Comment on lines +13 to +18

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

🧩 Analysis chain

🏁 Script executed:

# First, locate and examine SecurityConfig.java
fd -t f "SecurityConfig.java"

Repository: ithsjava25/visa-case-management-system

Length of output: 159


🏁 Script executed:

# Read the SecurityConfig.java file to verify the exact code
fd -t f "SecurityConfig.java" -x cat -n {}

Repository: ithsjava25/visa-case-management-system

Length of output: 984


🏁 Script executed:

# Examine VisaViewController to confirm currentUserId RequestParam usage
fd -t f "VisaViewController.java" -x head -100 {}

Repository: ithsjava25/visa-case-management-system

Length of output: 4013


🏁 Script executed:

# Search for all handler methods in VisaViewController that use currentUserId
fd -t f "VisaViewController.java" -x rg -A 2 "@PostMapping|@GetMapping|@PutMapping" {}

Repository: ithsjava25/visa-case-management-system

Length of output: 1686


This config exposes every visa mutation to unauthenticated request parameter substitution.

The combination of anyRequest().permitAll() with CSRF disabled and controller methods accepting currentUserId as a request parameter creates an immediate security vulnerability. An attacker can submit POST requests to approve, reject, assign, or apply for visas as any user ID without authentication. The frame options are also disabled globally rather than scoped to the H2 console.

Require authentication for application routes, limit CSRF exceptions to H2 only, and use sameOrigin for frame options:

🛡️ Safer baseline
         http
                 .authorizeHttpRequests(auth -> auth
-                        .anyRequest().permitAll()
+                        .requestMatchers("/h2-console/**", "/error").permitAll()
+                        .anyRequest().authenticated()
                 )
-                .csrf(csrf -> csrf.disable())
-                .headers(headers -> headers.frameOptions(frame -> frame.disable()));
+                .csrf(csrf -> csrf.ignoringRequestMatchers("/h2-console/**"))
+                .headers(headers -> headers.frameOptions(frame -> frame.sameOrigin()));
         return http.build();
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@src/main/java/org/example/visacasemanagementsystem/config/SecurityConfig.java`
around lines 13 - 18, The security config currently permits all requests and
disables CSRF/frame protections (anyRequest().permitAll(), csrf.disable(),
headers.frameOptions(...).disable()), exposing mutation endpoints to
unauthenticated parameter substitution; update the HttpSecurity DSL to require
authentication for application routes (replace anyRequest().permitAll() with
explicit matchers so that endpoints like "/api/**" require authenticated() while
allowing "/h2-console/**" and static resources to permitAll()), re-enable CSRF
but ignore the H2 console only (use csrf().ignoringRequestMatchers(new
AntPathRequestMatcher("/h2-console/**")) or
ignoringAntMatchers("/h2-console/**")), and set frameOptions to sameOrigin()
instead of disable() (headers().frameOptions().sameOrigin()) so the H2 console
still works without opening frames globally.

return http.build();
}
}
Original file line number Diff line number Diff line change
@@ -1,13 +1,15 @@
package org.example.visacasemanagementsystem.exception;

import jakarta.persistence.EntityNotFoundException;
import lombok.extern.slf4j.Slf4j;
import org.springframework.http.HttpStatus;
import org.springframework.web.bind.annotation.ControllerAdvice;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.ResponseBody;
import org.springframework.web.bind.annotation.ResponseStatus;

@ControllerAdvice
@Slf4j
public class GlobalExceptionHandler {

@ResponseStatus(value = HttpStatus.FORBIDDEN)
Expand All @@ -30,4 +32,12 @@ public String handleNotFoundException(RuntimeException exception) {
public String handleIllegalArgumentException(IllegalArgumentException exception) {
return "Invalid Request: " + exception.getMessage();
}

@ResponseStatus(value = HttpStatus.INTERNAL_SERVER_ERROR)
@ExceptionHandler(Exception.class)
@ResponseBody
public String handleAllUncaughtErrors(Exception exception) {
log.error("Unexpected Error: ", exception);
return "Unexpected server error.";
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@

public enum VisaStatus {
SUBMITTED,
UNASSIGNED,
ASSIGNED,
INCOMPLETE,
GRANTED,
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,230 @@
package org.example.visacasemanagementsystem.visa.controller;

import jakarta.persistence.EntityNotFoundException;
import jakarta.validation.Valid;
import org.example.visacasemanagementsystem.comment.service.CommentService;
import org.example.visacasemanagementsystem.exception.UnauthorizedException;
import org.example.visacasemanagementsystem.user.UserAuthorization;
import org.example.visacasemanagementsystem.user.dto.UserDTO;
import org.example.visacasemanagementsystem.user.service.UserService;
import org.example.visacasemanagementsystem.visa.VisaType;
import org.example.visacasemanagementsystem.visa.dto.CreateVisaDTO;
import org.example.visacasemanagementsystem.visa.dto.UpdateVisaDTO;
import org.example.visacasemanagementsystem.visa.dto.VisaDTO;
import org.example.visacasemanagementsystem.visa.service.VisaService;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.validation.BindingResult;
import org.springframework.web.bind.annotation.*;
import org.springframework.web.servlet.mvc.support.RedirectAttributes;

import java.util.List;

@Controller
@RequestMapping("/visas")
public class VisaViewController {

private final VisaService visaService;
private final CommentService commentService;
private final UserService userService;

public VisaViewController(VisaService visaService, CommentService commentService, UserService userService) {
this.visaService = visaService;
this.commentService = commentService;
this.userService = userService;
}

@GetMapping("/dashboard")
public String showDashboard(@RequestParam Long currentUserId, Model model) {
// Find current user
UserDTO user = userService.findById(currentUserId)
.orElseThrow(() -> new EntityNotFoundException("User not found."));
Comment on lines +38 to +41

@coderabbitai coderabbitai Bot Apr 14, 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

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

printf 'Spring Security artifacts:\n'
fd -i 'SecurityUser|UserDetailsServiceImpl|SecurityConfig' src/main/java

printf '\nRequest-supplied identity in VisaViewController:\n'
rg -n --type=java '@RequestParam\s+Long\s+currentUserId|findById\(currentUserId\)|findVisasByApplicant\(currentUserId\)|approveVisa\(id,\s*currentUserId\)|requestMoreInformation\(id,\s*currentUserId\)|rejectVisa\(id,\s*currentUserId\)|assignHandler\(id,\s*currentUserId\)' src/main/java/org/example/visacasemanagementsystem/visa/controller/VisaViewController.java

printf '\nAuthenticated-principal lookups in the web layer:\n'
rg -n --type=java '@AuthenticationPrincipal|SecurityContextHolder|getAuthentication\(' src/main/java

Repository: ithsjava25/visa-case-management-system

Length of output: 1532


🏁 Script executed:

# Check for build configuration and Spring Security dependency
fd -i 'pom\.xml|build\.gradle' src/ . | head -20

# Search for authentication/security-related code that might indicate integration
rg -n 'PasswordEncoder|BCrypt|UserDetailsService|AuthenticationManager|WebSecurityConfigurerAdapter' src/main/java --type=java

# Look for any Spring Security imports
rg -n 'import.*springframework\.security' src/main/java --type=java

# Check if user service has security-related methods
rg -n 'class.*UserService|implements.*UserDetailsService' src/main/java --type=java -A 5

Repository: ithsjava25/visa-case-management-system

Length of output: 781


🏁 Script executed:

# Check pom.xml for Spring Security dependency
cat pom.xml | grep -A 2 -B 2 'security\|spring-security' || echo "No Spring Security found in pom.xml"

# Also verify the actual state of authentication in the application
rg -n 'public class.*Controller' src/main/java/org/example/visacasemanagementsystem/visa/controller/ --type=java -A 3 | head -40

Repository: ithsjava25/visa-case-management-system

Length of output: 1539


🏁 Script executed:

# View the specific lines mentioned in the review to confirm the vulnerability
sed -n '38,41p' src/main/java/org/example/visacasemanagementsystem/visa/controller/VisaViewController.java

# Check if there's any authentication context usage in this controller
rg -n 'SecurityContextHolder|@AuthenticationPrincipal|getAuthentication|getPrincipal' src/main/java/org/example/visacasemanagementsystem/visa/controller/VisaViewController.java

Repository: ithsjava25/visa-case-management-system

Length of output: 334


🏁 Script executed:

# Check the processUpdate method and the missing statusInformation regression (around lines 146-152)
sed -n '140,160p' src/main/java/org/example/visacasemanagementsystem/visa/controller/VisaViewController.java

# Also check if there's a similar pattern in other methods to see the full scope of the issue
rg -n 'model\.addAttribute.*statusInformation|BindingResult|IllegalArgumentException' src/main/java/org/example/visacasemanagementsystem/visa/controller/VisaViewController.java

Repository: ithsjava25/visa-case-management-system

Length of output: 1422


Stop trusting currentUserId from the request.

Every ownership/admin check in this controller is derived from a query/form parameter. A caller can impersonate another applicant—or an admin—just by supplying an admin id in the request, and VisaService.validateHandler(Long) will authorize the looked-up user. Spring Security is declared as a dependency but not integrated here. Resolve the current user from Spring Security instead and remove currentUserId from the request surface.

Also add statusInformation to the model in the error catch block (lines 148–152). When visaService.updateVisa() throws IllegalArgumentException, the error handler calls prepareApplyModel() and sets isEdit to true but omits statusInformation, causing the edit form template to render with a missing dropdown.

Applies to all handler methods: 38–41, 62–64, 77–80, 101–103, 131–135, 157–166, 174–185, 189–196, 216–218.

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

In
`@src/main/java/org/example/visacasemanagementsystem/visa/controller/VisaViewController.java`
around lines 38 - 41, Stop trusting request-supplied currentUserId: remove the
`@RequestParam` Long currentUserId from all affected controller handler methods
(showDashboard, and the methods referenced around lines 62–64, 77–80, 101–103,
131–135, 157–166, 174–185, 189–196, 216–218) and instead resolve the
authenticated user via Spring Security
(SecurityContextHolder.getContext().getAuthentication() or an injected
Principal) to obtain the current user's id/username, then use userService (e.g.,
userService.findById or findByUsername) to load UserDTO and pass that into
visaService.validateHandler(...) and other authorization/ownership checks; also
update method signatures to remove currentUserId from request surfaces.
Additionally, in the catch block that handles IllegalArgumentException from
visaService.updateVisa(), ensure you add model.addAttribute("statusInformation",
...) so the edit form has the missing dropdown (use the same source used
elsewhere for statusInformation or call the method that prepares it before
returning prepareApplyModel(...) with isEdit=true).

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 will later be resolved when we implement the missing Spring Security

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 — noted for future reviews. Just to flag one distinction: the missing statusInformation attribute in the IllegalArgumentException catch block of processUpdate() (lines 148–152) is independent of Spring Security — it's a pure model-population bug that would cause the edit form's status information banner to render empty regardless of how the user is authenticated. Worth addressing in isolation even before the Spring Security work lands, but I'll leave that to your discretion.


✏️ Learnings added
Learnt from: eeebbaandersson
Repo: ithsjava25/visa-case-management-system PR: 27
File: src/main/java/org/example/visacasemanagementsystem/visa/controller/VisaViewController.java:38-41
Timestamp: 2026-04-14T13:13:26.963Z
Learning: In `visa-case-management-system`, `VisaViewController` (`src/main/java/org/example/visacasemanagementsystem/visa/controller/VisaViewController.java`) currently accepts `currentUserId` as a `RequestParam` in all handler methods (showDashboard, showApplyForm, submitApplication, showEditForm, processUpdate, approveVisa, requestMoreInformation, rejectVisa, assignCaseToHandler, viewDetails) instead of resolving the authenticated user from the Spring Security context. This is intentionally deferred until Spring Security is fully wired into the web layer. When implemented, `RequestParam Long currentUserId` should be removed from all these methods and replaced with resolution via `SecurityContextHolder.getContext().getAuthentication()` or `AuthenticationPrincipal`, followed by a `userService` lookup to obtain the `UserDTO`.

Learnt from: eeebbaandersson
Repo: ithsjava25/visa-case-management-system PR: 27
File: src/main/java/org/example/visacasemanagementsystem/visa/controller/VisaViewController.java:38-41
Timestamp: 2026-04-14T13:13:26.963Z
Learning: In `visa-case-management-system`, the `processUpdate()` method in `VisaViewController` (`src/main/java/org/example/visacasemanagementsystem/visa/controller/VisaViewController.java`, around lines 148–152) is missing `model.addAttribute("statusInformation", ...)` in the `IllegalArgumentException` catch block. When `visaService.updateVisa()` throws this exception, the edit form (`edit-form.html`) re-renders without `statusInformation`, causing the "Feedback from Handler" banner to display incorrectly. This is a UI/model bug independent of Spring Security and should be fixed by fetching the current visa's `statusInformation` (e.g., via `visaService.findVisaDtoById(id).statusInformation()`) and adding it to the model before returning the edit view.

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: eeebbaandersson
Repo: ithsjava25/visa-case-management-system PR: 27
File: src/main/resources/templates/visa/dashboard.html:125-126
Timestamp: 2026-04-14T12:26:05.436Z
Learning: In the `visa-case-management-system` repository, the "Sign Out" link in `src/main/resources/templates/visa/dashboard.html` (line ~126) points to `/visas/logout`, which is not yet implemented. This is intentionally deferred until Spring Security is fully configured. When implemented, the link should either use Spring Security's default `/logout` endpoint (with CSRF token handling via a POST form) or a dedicated `VisaViewController` method that delegates to `SecurityContextLogoutHandler`.

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:13:00.080Z
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.

Learnt from: eeebbaandersson
Repo: ithsjava25/visa-case-management-system PR: 27
File: src/main/resources/templates/visa/details.html:237-244
Timestamp: 2026-04-14T12:40:25.740Z
Learning: In `visa-case-management-system`, the comment form in `src/main/resources/templates/visa/details.html` (around lines 237-244) currently submits to `/api/comments` with form fields `visaId`, `authorId`, and `content` (wrong field name). The intended fix is to replace the HTML form with JavaScript/fetch to POST JSON to the existing REST endpoint `/api/comments`. The correct JSON body field for the comment text is `text` (matching `CreateCommentDTO`), not `content`. This is a known deferred issue to be addressed in a future PR.

Learnt from: Martin-E-Karlsson
Repo: ithsjava25/visa-case-management-system PR: 26
File: src/main/java/org/example/visacasemanagementsystem/user/dto/CreateUserDTO.java:11-11
Timestamp: 2026-04-13T17:12:15.296Z
Learning: In the `visa-case-management-system` repository, Spring Security has been integrated (via `SecurityUser` and `UserDetailsServiceImpl` in PR `#26`), but password hashing in `UserService.createUser()` (file: `src/main/java/org/example/visacasemanagementsystem/user/service/UserService.java`, line ~54) still uses plaintext (`user.setPassword(dto.password())`). BCrypt encoding is intentionally deferred to a future pull request. When implemented, a `BCryptPasswordEncoder` `Bean` should be configured and injected into `UserService` via constructor injection, replacing the plaintext assignment with `passwordEncoder.encode(dto.password())`.


List<VisaDTO> visas;

// If the current user = ADMIN/SYSADMIN show everything
if (user.userAuthorization() == UserAuthorization.ADMIN ||
user.userAuthorization() == UserAuthorization.SYSADMIN) {
visas = visaService.findAll();
} else {
// Normal user/applicant can only se their own visa applications
visas = visaService.findVisasByApplicant(currentUserId);
}

// Send data to Thymeleaf
model.addAttribute("visas", visas);
model.addAttribute("currentUser", user);

return "visa/dashboard";
}

@GetMapping("/apply")
public String showApplyForm(@RequestParam Long currentUserId, Model model) {
UserDTO user = userService.findById(currentUserId)
.orElseThrow(() -> new EntityNotFoundException("User not found."));

model.addAttribute("currentUser", user);
model.addAttribute("visaTypes", VisaType.values());

if (!model.containsAttribute("createVisaDTO")) {
model.addAttribute("createVisaDTO", new CreateVisaDTO(null, "", "", null, currentUserId));
}

return "visa/apply-form";
}

@PostMapping("/apply")
public String submitApplication(
@Valid @ModelAttribute("createVisaDTO") CreateVisaDTO createVisaDTO,
BindingResult bindingResult,
@RequestParam Long currentUserId,
RedirectAttributes redirectAttributes,
Model model) {

if (bindingResult.hasErrors()) {
prepareApplyModel(currentUserId, model);
return "visa/apply-form";
}

try {
visaService.applyForVisa(createVisaDTO, currentUserId);
} catch (IllegalArgumentException e) {
bindingResult.rejectValue("travelDate", "error.travelDate", e.getMessage());
prepareApplyModel(currentUserId, model);
return "visa/apply-form";
}

return "redirect:/visas/dashboard?currentUserId=" + currentUserId;
}

@GetMapping("/{id}/edit")
public String showEditForm(@PathVariable Long id, @RequestParam Long currentUserId, Model model) {
UserDTO userDTO = userService.findById(currentUserId)
.orElseThrow(() -> new EntityNotFoundException("User not found."));

VisaDTO visa = visaService.findVisaDtoById(id);

if (!visa.applicantId().equals(currentUserId)) {
throw new UnauthorizedException("You can only edit your own applications.");
}
Comment thread
coderabbitai[bot] marked this conversation as resolved.

UpdateVisaDTO updateDto = new UpdateVisaDTO(
visa.id(),
visa.visaType(),
visa.visaStatus(),
visa.nationality(),
visa.passportNumber(),
visa.travelDate(),
visa.handlerId()
);

model.addAttribute("updateVisaDto", updateDto);
model.addAttribute("currentUser", userDTO);
model.addAttribute("visaTypes", VisaType.values());
model.addAttribute("isEdit", true);
model.addAttribute("statusInformation", visa.statusInformation());

return "visa/edit-form";
}

@PostMapping("/{id}/edit")
public String processUpdate(
@PathVariable Long id,
@Valid @ModelAttribute ("updateVisaDto") UpdateVisaDTO updateVisaDTO,
BindingResult bindingResult,
@RequestParam Long currentUserId,
Model model) {

if (bindingResult.hasErrors()) {
prepareApplyModel(currentUserId, model);
model.addAttribute("isEdit", true);
VisaDTO visa = visaService.findVisaDtoById(id);
model.addAttribute("statusInformation", visa.statusInformation());
return "visa/edit-form";
}

try {
visaService.updateVisa(id, updateVisaDTO, currentUserId);
} catch (IllegalArgumentException e) {
if (e.getMessage().contains("date")) {
bindingResult.rejectValue("travelDate", "error.travelDate", e.getMessage());
} else {
bindingResult.reject("globalError",e.getMessage());
}

prepareApplyModel(currentUserId, model);
model.addAttribute("isEdit", true);
VisaDTO visa = visaService.findVisaDtoById(id);
model.addAttribute("statusInformation", visa.statusInformation());

return "visa/edit-form";
}
Comment thread
coderabbitai[bot] marked this conversation as resolved.
Comment thread
coderabbitai[bot] marked this conversation as resolved.
return "redirect:/visas/" + id + "?currentUserId=" + currentUserId;
}

@PostMapping("/{id}/approve")
public String approveVisa(@PathVariable Long id, @RequestParam Long currentUserId) {
visaService.approveVisa(id, currentUserId);
return "redirect:/visas/" + id + "?currentUserId=" + currentUserId;
}

@PostMapping("/{id}/request-info")
public String requestMoreInformation(@PathVariable Long id,
@RequestParam Long currentUserId,
@RequestParam String reason) {

visaService.requestMoreInformation(id, currentUserId, reason);

return "redirect:/visas/" + id + "?currentUserId=" + currentUserId;
}


@PostMapping("/{id}/reject")
public String rejectVisa(@PathVariable Long id,
@RequestParam Long currentUserId,
@RequestParam String reason) {
visaService.rejectVisa(id, currentUserId, reason);
return "redirect:/visas/" + id + "?currentUserId=" + currentUserId;
}


@PostMapping("/{id}/assign")
public String assignCaseToHandler(@PathVariable Long id, @RequestParam Long currentUserId) {
visaService.assignHandler(id, currentUserId);
return "redirect:/visas/" + id + "?currentUserId=" + currentUserId;
}

@GetMapping("/{id}")
public String viewDetails(@PathVariable Long id, @RequestParam Long currentUserId, Model model) {
// Get visa
VisaDTO visa = visaService.findVisaDtoById(id);

// Get user
UserDTO user = userService.findById(currentUserId)
.orElseThrow(() -> new EntityNotFoundException("User not found."));

// Authorization check: regular users can only view their own applications
if (user.userAuthorization() == UserAuthorization.USER && !visa.applicantId().equals(currentUserId)) {
throw new UnauthorizedException("You can only view your own applications.");
}

// Get comments
var comments = commentService.getCommentsByVisaId(id);


model.addAttribute("visa", visa);
model.addAttribute("comments", comments);
model.addAttribute("currentUser", user);

return "visa/details";
}
Comment thread
coderabbitai[bot] marked this conversation as resolved.

// Helper methods

public void prepareApplyModel(Long currentUserId, Model model) {
UserDTO user = userService.findById(currentUserId)
.orElseThrow(() -> new EntityNotFoundException("User not found."));
model.addAttribute("currentUser", user);
model.addAttribute("visaTypes", VisaType.values());
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -3,10 +3,15 @@
import jakarta.validation.constraints.NotBlank;
import jakarta.validation.constraints.NotNull;
import org.example.visacasemanagementsystem.visa.VisaType;
import org.springframework.format.annotation.DateTimeFormat;

import java.time.LocalDate;

public record CreateVisaDTO(
@NotNull(message = "Visa type must be specified") VisaType visaType,
@NotBlank(message = "Nationality must be specified") String nationality,
@NotNull Long applicantId // Användaren som ansöker om visa
@NotBlank(message = "Passport number must be provided") String passportNumber,
@NotNull(message = "Travel date must be provided") @DateTimeFormat(iso = DateTimeFormat.ISO.DATE) LocalDate travelDate,
Long applicantId
) {
}
Original file line number Diff line number Diff line change
Expand Up @@ -4,12 +4,17 @@
import jakarta.validation.constraints.NotNull;
import org.example.visacasemanagementsystem.visa.VisaStatus;
import org.example.visacasemanagementsystem.visa.VisaType;
import org.springframework.format.annotation.DateTimeFormat;

import java.time.LocalDate;

public record UpdateVisaDTO(
@NotNull Long id,
@NotBlank(message = "Visa type must be specified") VisaType visaType,
@NotNull(message = "Visa type must be specified") VisaType visaType,
@NotNull(message = "Application must have a status") VisaStatus visaStatus,
@NotBlank(message = "Nationality must be specified") String nationality,
Long handlerId // Handläggaren som uppdaterar visat eller om användaren gör en komplettering?
@NotBlank(message = "Passport number must be provided") String passportNumber,
@NotNull (message = "Travel date must be provided") @DateTimeFormat(iso = DateTimeFormat.ISO.DATE) LocalDate travelDate,
Long handlerId
) {
}
Loading