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
9 changes: 8 additions & 1 deletion pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -138,6 +138,13 @@
<artifactId>htmx-spring-boot</artifactId>
<version>5.0.0</version>
</dependency>
<dependency>
<groupId>com.webauthn4j</groupId>
<artifactId>webauthn4j-core</artifactId>
<version>0.30.2.RELEASE</version>
<scope>compile</scope>
</dependency>

</dependencies>


Expand All @@ -155,4 +162,4 @@



</project>
</project>
26 changes: 20 additions & 6 deletions src/main/java/backendlab/team4you/Team4youApplication.java
Original file line number Diff line number Diff line change
Expand Up @@ -23,17 +23,31 @@ ApplicationRunner init(UserRepository repository, BCryptPasswordEncoder encoder)
return args -> {
if (repository.count() == 0) {

UserEntity devUser = new UserEntity(
Bytes.fromBase64("01"),
UserEntity devAdmin = new UserEntity(
Bytes.fromBase64("YWRtaW4="),
"dev", // name (username)
"Developer" // displayName
"admin" // displayName
);

devAdmin.setPasswordHash(encoder.encode("123456"));
devAdmin.setRole("ROLE_ADMIN");
devAdmin.setEmail("devadmin@gmail.com");

repository.save(devAdmin);
System.out.println("✅ Admin created");

UserEntity devUser = new UserEntity(
Bytes.fromBase64("dXNlcg=="),
"user", // name (username)
"user" // displayName
);

devUser.setPasswordHash(encoder.encode("123456"));
devUser.setRole("USER");
devUser.setEmail("dev@gmail.com");
devUser.setPasswordHash(encoder.encode("1234"));
devUser.setRole("ROLE_USER");
devUser.setEmail("devuser@gmail.com");

repository.save(devUser);
System.out.println("✅ User created");
Comment on lines 24 to +50

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Inspect available repository methods before choosing the exact idempotency check.
rg -n -C3 'interface UserRepository|findByEmail|existsByEmail|findByName|existsByName' src/main/java

Repository: ithsjava25/project-backend-team4you

Length of output: 9666


🏁 Script executed:

cat -n src/main/java/backendlab/team4you/Team4youApplication.java | head -60

Repository: ithsjava25/project-backend-team4you

Length of output: 2069


Seed each dev account idempotently rather than gating both on repository count.

With repository.count() == 0, developers with an existing database will never receive the new admin account after pulling this PR. Check for each account by username using repository.findByName() instead, so each account is created independently if missing.

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

In `@src/main/java/backendlab/team4you/Team4youApplication.java` around lines 24 -
50, The seeding currently runs only when repository.count() == 0 so existing DBs
won't get the new admin; change Team4youApplication to check and create each
account idempotently by calling repository.findByName("dev") and
repository.findByName("user") (or equivalent find method) and only
constructing/saving the corresponding UserEntity (devAdmin / devUser) when the
find returns empty; reuse encoder.encode(...) and repository.save(...) as in the
diff, and keep role/email/password setup identical but guarded per-account
instead of a single repository.count() gate.

}
};
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
import org.springframework.context.annotation.Lazy;
import org.springframework.security.core.Authentication;
import org.springframework.security.web.authentication.SimpleUrlAuthenticationSuccessHandler;
import org.springframework.security.web.webauthn.management.PublicKeyCredentialUserEntityRepository;
import org.springframework.security.web.webauthn.authentication.WebAuthnAuthenticationRequestToken;
import org.springframework.security.web.webauthn.management.UserCredentialRepository;
import org.springframework.stereotype.Component;

Expand All @@ -31,17 +31,26 @@ public void onAuthenticationSuccess(HttpServletRequest request,
) throws IOException {

String username = authentication.getName();

var userEntity = userService.findByName(username);

if (userEntity != null){
var credentials = userCredentialRepository.findByUserId(userEntity.getId());

if (!credentials.isEmpty()){
getRedirectStrategy().sendRedirect(request, response, "/webauthn-check");
getRedirectStrategy().sendRedirect(request, response, "/login/webauthn");
return;
}
}
getRedirectStrategy().sendRedirect(request, response, "/dashboard");

var authorities = authentication.getAuthorities();

boolean isAdmin = authorities.stream()
.anyMatch(a -> a.getAuthority().equals("ROLE_ADMIN"));

if (isAdmin)
getRedirectStrategy().sendRedirect(request, response, "/admin");
else {
getRedirectStrategy().sendRedirect(request, response, "/home");
}
}
}
25 changes: 12 additions & 13 deletions src/main/java/backendlab/team4you/config/SecurityConfig.java
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@
import backendlab.team4you.user.UserEntity;
import backendlab.team4you.user.UserService;
import org.springframework.security.core.userdetails.UsernameNotFoundException;
import org.springframework.security.core.userdetails.User;
import org.springframework.security.config.annotation.web.configurers.WebAuthnConfigurer;

@Configuration
public class SecurityConfig {
Expand All @@ -25,23 +25,20 @@ SecurityFilterChain securityFilterChain(HttpSecurity http,
CustomAuthenticationSuccessHandler successHandler) throws Exception {

return http
.csrf(csrf -> csrf.disable())
.csrf(csrf -> csrf.ignoringRequestMatchers("/webauthn/**", "/api/files/**"))
.authorizeHttpRequests(
authorizeHttp -> authorizeHttp
// Public endpoints
.requestMatchers("/css/**", "/js/**", "/images/**").permitAll()
.requestMatchers( "/","/login", "/login/webauthn", "/signup", "/error").permitAll()
.requestMatchers("/webauthn/authenticate/**").permitAll()
.requestMatchers("/api/files/**").permitAll()

.requestMatchers("/api/files/**", "/webauthn/authenticate/**").permitAll()

// .requestMatchers("/profile", "/logout").authenticated()
.requestMatchers("/webauthn-check").authenticated()
.requestMatchers("/webauthn/**").hasAnyRole("USER", "ADMIN")

.requestMatchers("/admin/**").hasRole("ADMIN")
.requestMatchers("/dashboard", "/profile/**").hasAnyRole("USER", "ADMIN")
.requestMatchers("/add-passkey").hasAnyRole("USER", "ADMIN")
.requestMatchers("/webauthn/register/**").hasAnyRole("USER", "ADMIN")
.requestMatchers("/home", "/profile/**").hasRole("USER")
.requestMatchers("/add-passkey", "/webauthn/register/**").hasAnyRole("USER", "ADMIN")
Comment on lines 33 to +41

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Check controller mappings vs. security matchers for the /login/webauthn paths
rg -nP --type=java -C2 '"/login/webauthn/?"'
rg -nP --type=java -C2 '"/webauthn/login'

Repository: ithsjava25/project-backend-team4you

Length of output: 2502


🏁 Script executed:

#!/bin/bash
# 1. Check for /home endpoint mappings
echo "=== Searching for /home endpoint mapping ==="
rg -nP --type=java '"/home' src/

# 2. Check for /webauthn/login endpoint (not the matcher, but actual endpoints)
echo "=== Searching for /webauthn/login endpoint ==="
rg -nP --type=java '@GetMapping|@PostMapping|@RequestMapping' src/ | grep -i webauthn

# 3. Check CustomAuthenticationSuccessHandler to see admin routing
echo "=== Examining CustomAuthenticationSuccessHandler ==="
rg -nP --type=java -A 10 'isAdmin|ADMIN' src/main/java/backendlab/team4you/config/CustomAuthenticationSuccessHandler.java

Repository: ithsjava25/project-backend-team4you

Length of output: 1091


🏁 Script executed:

#!/bin/bash
# Search for WebAuthn configuration
echo "=== Searching for WebAuthn configuration ==="
fd -t f '\.java$' src/ | xargs grep -l -i 'webauthn' | head -20

echo "=== Check what WebAuthn library is being used ==="
grep -i webauthn pom.xml build.gradle 2>/dev/null || echo "Checking for pom.xml/build.gradle..."

# Search for WebAuthn endpoints more broadly
echo "=== All webauthn-related endpoint mappings ==="
rg -nP --type=java '@(Get|Post|Put|Delete)Mapping.*webauthn' -i

# Check for WebAuthnConfigurer or similar Spring Security WebAuthn configuration
echo "=== WebAuthnConfigurer or security configuration for webauthn ==="
rg -nP --type=java 'WebAuthConfigurer|webauthnConfigurer|/webauthn' src/main/java/backendlab/team4you/config/

Repository: ithsjava25/project-backend-team4you

Length of output: 2360


🏁 Script executed:

#!/bin/bash
# Read the full SecurityConfig.java file
echo "=== Full SecurityConfig.java ==="
cat -n src/main/java/backendlab/team4you/config/SecurityConfig.java

Repository: ithsjava25/project-backend-team4you

Length of output: 4893


Path matcher inconsistencies worth tightening.

A few issues in the authorize chain:

  1. /login/webauthn vs /login/webauthn/SignupController maps @GetMapping("/login/webauthn/") (trailing slash), but the matcher on line 33 is /login/webauthn without a trailing slash. With MVC useTrailingSlashMatch disabled by default since Spring 6, the two are distinct paths. CustomAuthenticationSuccessHandler redirects to /login/webauthn/ (with slash), confirming the mismatch. This creates a subtle inconsistency:

    -.requestMatchers( "/","/login", "/login/webauthn", "/signup", "/error").permitAll()
    +.requestMatchers("/", "/login", "/login/webauthn/", "/signup", "/error").permitAll()
  2. /home requires hasRole("USER") — Users with only ROLE_ADMIN are denied at /home. While CustomAuthenticationSuccessHandler routes admins to /admin so this doesn't affect normal flow, the config is semantically inconsistent. Any future link to /home from an admin session would still 403. Consider hasAnyRole("USER","ADMIN") unless that exclusion is intentional.

  3. /webauthn/login/** permitAll — No controller endpoint for /webauthn/login exists. Spring Security's WebAuthn library provides /webauthn/authenticate and /webauthn/register endpoints, but not /webauthn/login/**. This matcher appears to be dead configuration; remove it if unneeded.

📝 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
.requestMatchers( "/","/login", "/login/webauthn", "/signup", "/error").permitAll()
.requestMatchers("/webauthn/authenticate/**").permitAll()
.requestMatchers("/api/files/**").permitAll()
.requestMatchers("/api/files/**", "/webauthn/authenticate/**", "/webauthn/login/**").permitAll()
// .requestMatchers("/profile", "/logout").authenticated()
.requestMatchers("/webauthn-check").authenticated()
.requestMatchers("/webauthn/**").hasAnyRole("USER", "ADMIN")
.requestMatchers("/admin/**").hasRole("ADMIN")
.requestMatchers("/dashboard", "/profile/**").hasAnyRole("USER", "ADMIN")
.requestMatchers("/add-passkey").hasAnyRole("USER", "ADMIN")
.requestMatchers("/webauthn/register/**").hasAnyRole("USER", "ADMIN")
.requestMatchers("/home", "/profile/**").hasRole("USER")
.requestMatchers("/add-passkey", "/webauthn/register/**").hasAnyRole("USER", "ADMIN")
.requestMatchers("/", "/login", "/login/webauthn/", "/signup", "/error").permitAll()
.requestMatchers("/api/files/**", "/webauthn/authenticate/**", "/webauthn/login/**").permitAll()
.requestMatchers("/webauthn/**").hasAnyRole("USER", "ADMIN")
.requestMatchers("/admin/**").hasRole("ADMIN")
.requestMatchers("/home", "/profile/**").hasRole("USER")
.requestMatchers("/add-passkey", "/webauthn/register/**").hasAnyRole("USER", "ADMIN")
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/main/java/backendlab/team4you/config/SecurityConfig.java` around lines 33
- 41, SecurityConfig has inconsistent requestMatcher paths and role checks:
align the "/login/webauthn" matcher with the SignupController mapping and
success handler by permitting "/login/webauthn/" (include the trailing slash) or
add both "/login/webauthn" and "/login/webauthn/"; change the "/home" matcher
from hasRole("USER") to hasAnyRole("USER","ADMIN") if admins should access /home
(adjust in SecurityConfig where requestMatchers("/home").hasRole("USER") is
declared) ; and remove the unused permitAll matcher for "/webauthn/login/**" (or
replace it with the correct WebAuthn endpoints like "/webauthn/authenticate/**"
if needed) so SecurityConfig, SignupController, and
CustomAuthenticationSuccessHandler paths are consistent.


.anyRequest().authenticated()
)
Expand All @@ -52,8 +49,10 @@ SecurityFilterChain securityFilterChain(HttpSecurity http,
)
.formLogin(form -> form
.loginPage("/login")
.successHandler(successHandler))

.loginProcessingUrl("/login")
.successHandler(successHandler)
.permitAll()
)
.logout(logout -> logout.logoutSuccessUrl("/").permitAll())
.build();
}
Expand All @@ -79,7 +78,7 @@ public UserDetailsService userDetailsService(UserService userService){
return User.builder()
.username(user.getName())
.password(user.getPasswordHash())
.roles(user.getRole())
.authorities(user.getRole())
.accountLocked(false)
.build();
Comment on lines 78 to 83

Copy link
Copy Markdown
Contributor

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
# Inspect the role enum and any string concatenation around roles in the codebase.
rg -nP --type=java -C2 '\bUserRole\.'
rg -nP --type=java -C2 '"ROLE_'
ast-grep --pattern 'enum UserRole { $$$ }'

Repository: ithsjava25/project-backend-team4you

Length of output: 2540


🏁 Script executed:

# Find and inspect the UserRole enum definition
fd -t f UserRole.java

Repository: ithsjava25/project-backend-team4you

Length of output: 131


🏁 Script executed:

# Read the UserEntity.getRole() method to see its return type
rg -n "getRole" src/main/java/backendlab/team4you/user/UserEntity.java -A 3

Repository: ithsjava25/project-backend-team4you

Length of output: 159


🏁 Script executed:

# Search for the UserRole enum definition with ast-grep and also cat the file if found
find . -name "UserRole.java" -type f

Repository: ithsjava25/project-backend-team4you

Length of output: 133


🏁 Script executed:

# Read the full UserRole enum definition
cat -n src/main/java/backendlab/team4you/user/UserRole.java

Repository: ithsjava25/project-backend-team4you

Length of output: 227


Fix double-prefix bug in SignupController authentication setup.

The concern is partially addressed: UserEntity.getRole() does return ROLE_-prefixed strings because the UserRole enum constants are named ROLE_USER and ROLE_ADMIN. The SecurityConfig change from .roles(...) to .authorities(user.getRole()) is therefore safe and correct.

However, verification reveals a critical bug in SignupController.java:61 where the authentication is created with a double-prefixed role:

"ROLE_" + userEntity.getRole()  // produces "ROLE_ROLE_USER" or "ROLE_ROLE_ADMIN"

This must be corrected to just userEntity.getRole() to match the role format used elsewhere (Team4youApplication seeding and now SecurityConfig). The mismatched role prefix will cause authentication and authorization checks to fail silently for signup users.

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

In `@src/main/java/backendlab/team4you/config/SecurityConfig.java` around lines 78
- 83, The signup flow is creating a double-prefixed role string; in
SignupController where authentication is constructed using "ROLE_" +
userEntity.getRole(), remove the extra "ROLE_" prefix and use
userEntity.getRole() directly so it matches SecurityConfig's
User.builder().authorities(user.getRole()) and the seeded roles (e.g.,
ROLE_USER/ROLE_ADMIN); update the authentication creation to pass the existing
role string and ensure any related checks expect the single-prefixed format.

};
Expand All @@ -89,4 +88,4 @@ public UserDetailsService userDetailsService(UserService userService){
public BCryptPasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -10,15 +10,11 @@
import backendlab.team4you.user.UserService;
import groovy.util.logging.Slf4j;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.PageRequest;
import org.springframework.data.domain.Sort;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.security.config.annotation.method.configuration.EnableMethodSecurity;
import org.springframework.security.core.Authentication;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
Expand All @@ -28,7 +24,6 @@
import org.springframework.web.server.ResponseStatusException;

import java.util.List;
import java.util.Scanner;


@Slf4j
Expand Down Expand Up @@ -174,5 +169,3 @@ public String deleteApplication(@RequestParam Long id, Model model) {
return "fragments/alert :: success";
}
}


Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
package backendlab.team4you.controller;

import backendlab.team4you.user.UserService;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.GetMapping;

@Controller
public class DashboardController {

private final UserService userService;

public DashboardController(UserService userService){
this.userService = userService;
}

@GetMapping("/dashboard")
public String dashboard(){
return "dashboard";
}

@GetMapping("/dashboard/home")
public String dashboardHome() {
return "dashboard :: content";
}
}
14 changes: 4 additions & 10 deletions src/main/java/backendlab/team4you/controller/SignupController.java
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,6 @@
import backendlab.team4you.user.UserEntity;
import backendlab.team4you.user.UserService;

import backendlab.team4you.user.UserRepository;

import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
Expand Down Expand Up @@ -36,13 +34,9 @@ public SignupController(PublicKeyCredentialUserEntityRepository users,
this.userService = userService;
}

@GetMapping("/webauthn-check")
public String showWebAuthnCheck(){
return "webauthn-check";
}
@GetMapping("/dashboard")
public String dashboard() {
return "dashboard";
@GetMapping("/login/webauthn")
public String webauthnCheck() {
return "check";
Comment on lines +37 to +39

Copy link
Copy Markdown
Contributor

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
# Description: Verify dashboard links still have controller mappings.

echo "Controller mappings for /dashboard:"
rg -n -C3 '@(GetMapping|PostMapping|RequestMapping)\s*\([^)]*["'\'']/dashboard\b' src/main/java

echo
echo "Template/static references to /dashboard:"
rg -n -C2 '(/dashboard\b|@\{/dashboard\})' src/main/resources

Repository: ithsjava25/project-backend-team4you

Length of output: 1215


🏁 Script executed:

#!/bin/bash
# Search for any dashboard-related mappings that might exist
echo "Searching for all `@GetMapping/`@PostMapping/@RequestMapping containing 'dashboard':"
rg -i 'dashboard' src/main/java --type java -B2 -A2

echo
echo "Searching for any /dashboard* patterns in Java files:"
rg '"/dashboard' src/main/java --type java

Repository: ithsjava25/project-backend-team4you

Length of output: 216


Add a /dashboard controller endpoint or update the template navigation.

The /login/webauthn mapping is correctly aligned with SecurityConfig, but the removal of the /dashboard endpoint creates a broken navigation link. The template at src/main/resources/templates/dashboard-layout.html:27 references @{/dashboard} and uses hx-get="/dashboard/home", but no controller currently maps either route. Users navigating via the sidebar will encounter a 404 error.

Either restore the /dashboard controller mapping or update the template to point to a valid endpoint.

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

In `@src/main/java/backendlab/team4you/controller/SignupController.java` around
lines 37 - 39, The dashboard routes referenced by the template are missing; add
controller mappings in SignupController (or a new DashboardController) for
`@GetMapping`("/dashboard") and `@GetMapping`("/dashboard/home") that return the
dashboard view names (or redirect to an existing template), so the template's
@{/dashboard} and hx-get="/dashboard/home" resolve; alternatively, update the
template navigation to point to existing endpoints if you prefer not to restore
these routes—locate SignupController and either restore/create methods named
e.g. dashboard() and dashboardHome() to fix the broken links.

}

@GetMapping("/signup")
Expand All @@ -64,7 +58,7 @@ public void signup(@RequestBody SignupRequest req, HttpServletRequest request, H
);

Authentication auth = new UsernamePasswordAuthenticationToken(
userEntity.getName(), null, List.of(new SimpleGrantedAuthority("ROLE_" + userEntity.getRole())));
userEntity.getName(), null, List.of(new SimpleGrantedAuthority(userEntity.getRole())));

SecurityContext context = SecurityContextHolder.createEmptyContext();
context.setAuthentication(auth);
Expand Down
13 changes: 4 additions & 9 deletions src/main/java/backendlab/team4you/controller/UserController.java
Original file line number Diff line number Diff line change
Expand Up @@ -23,20 +23,15 @@ public UserController(UserService userService) {

}



@GetMapping("/")
public String homepage(){
return "home";
}








@GetMapping("/home")
public String userHome() {
return "home";
}

@DeleteMapping("/account")
public String deleteAccount(Principal principal) {
Expand Down
4 changes: 2 additions & 2 deletions src/main/java/backendlab/team4you/user/UserRole.java
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

public enum UserRole {

USER,
ADMIN,
ROLE_USER,
ROLE_ADMIN,
Comment on lines +5 to +6

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Description: Verify how UserRole is persisted and whether a Flyway migration updates old role values.
# Expectation:
# - If UserRole is persisted as a string, SQL migrations should update USER -> ROLE_USER and ADMIN -> ROLE_ADMIN.
# - Spring Security should use hasAuthority("ROLE_ADMIN") or hasRole("ADMIN"), not hasRole("ROLE_ADMIN").

echo "UserRole usages and persistence annotations:"
rg -n -C4 --type=java '\bUserRole\b|@Enumerated|EnumType\.STRING|hasRole\s*\(|hasAuthority\s*\(|authorities\s*\('

echo
echo "SQL migrations touching role values:"
fd -e sql -x sh -c 'echo "--- $1"; rg -n -C3 "USER|ADMIN|ROLE_USER|ROLE_ADMIN|role" "$1"' sh {}

Repository: ithsjava25/project-backend-team4you

Length of output: 6685


Add a migration to backfill existing role values before deployment.

The UserRole enum constants have been renamed from USER/ADMIN to ROLE_USER/ROLE_ADMIN. Since @Enumerated(EnumType.STRING) is used in UserEntity, the role column stores enum constant names as strings. Existing database rows contain 'USER' or 'ADMIN', but the new enum constant names are ROLE_USER and ROLE_ADMIN. When JPA attempts to load these rows, the enum name mismatch will cause IllegalArgumentException. Additionally, the setRole(String role) method uses valueOf(), which will fail on old values.

Add a Flyway migration to update existing role values before this change is deployed:

Required migration
UPDATE user_entities SET role = 'ROLE_USER' WHERE role = 'USER';
UPDATE user_entities SET role = 'ROLE_ADMIN' WHERE role = 'ADMIN';
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/main/java/backendlab/team4you/user/UserRole.java` around lines 5 - 6, Add
a Flyway SQL migration that backfills old enum names to the new ones before
deploying the enum rename: create a new migration (e.g.,
Vx__backfill_user_roles.sql) containing the two updates "UPDATE user_entities
SET role = 'ROLE_USER' WHERE role = 'USER';" and "UPDATE user_entities SET role
= 'ROLE_ADMIN' WHERE role = 'ADMIN';" and ensure it runs prior to the code
deploy; also review UserRole and UserEntity.setRole(String role) to ensure they
rely on the migrated values (or add a temporary tolerant mapping from
'USER'/'ADMIN' to 'ROLE_USER'/'ROLE_ADMIN' inside setRole to avoid valueOf()
failures until the migration is applied).


}
2 changes: 1 addition & 1 deletion src/main/resources/application.properties
Original file line number Diff line number Diff line change
Expand Up @@ -19,4 +19,4 @@ aws.access-key=${AWS_ACCESS_KEY}
aws.secret-key=${AWS_SECRET_KEY}
aws.region=${AWS_REGION}
aws.bucket-name=${AWS_BUCKET_NAME}
aws.endpoint-url=${AWS_ENDPOINT_URL}
aws.endpoint-url=${AWS_ENDPOINT_URL}
43 changes: 43 additions & 0 deletions src/main/resources/static/js/abort-controller.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
/*
* Copyright 2004-present the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

"use strict";

const holder = {
controller: new AbortController(),
};

/**
* Returns a new AbortSignal to be used in the options for the registration and authentication ceremonies.
* Aborts the existing AbortController if it exists, cancelling any existing ceremony.
*
* The authentication ceremony, when triggered with conditional mediation, shows a non-modal
* interaction. If the user does not interact with the non-modal dialog, the existing ceremony MUST
* be cancelled before initiating a new one, hence the need for a singleton AbortController.
*
* @returns {AbortSignal} a new, non-aborted AbortSignal
*/
function newSignal() {
if (!!holder.controller) {
holder.controller.abort("Initiating new WebAuthN ceremony, cancelling current ceremony");
}
holder.controller = new AbortController();
return holder.controller.signal;
}

export default {
newSignal,
};
33 changes: 33 additions & 0 deletions src/main/resources/static/js/base64url.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
/*
* Copyright 2004-present the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

"use strict";

export default {
encode: function (buffer) {
const base64 = window.btoa(String.fromCharCode(...new Uint8Array(buffer)));
return base64.replace(/=/g, "").replace(/\+/g, "-").replace(/\//g, "_");
Comment on lines +20 to +22

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

🧩 Analysis chain

🏁 Script executed:

git ls-files | grep -i base64

Repository: ithsjava25/project-backend-team4you

Length of output: 120


🏁 Script executed:

find . -name "base64url.js" -o -name "*base64*"

Repository: ithsjava25/project-backend-team4you

Length of output: 122


🏁 Script executed:

cat -n src/main/resources/static/js/base64url.js

Repository: ithsjava25/project-backend-team4you

Length of output: 1513


🌐 Web query:

String.fromCharCode argument limit JavaScript browser maximum arguments

💡 Result:

There is no specified maximum number of arguments for String.fromCharCode in the ECMAScript standard or official documentation like MDN. The method accepts a variable number of numeric arguments (num1, ..., numN), each representing a UTF-16 code unit between 0 and 65535.

Citations:


🌐 Web query:

WebAuthn attestation credential data maximum size bytes

💡 Result:

There is no defined maximum size in the WebAuthn specification for attestation credential data. The WebAuthn Level 3 specification (as of 2026) does not specify a maximum size for the attestationObject, which contains the authenticatorData (including attestedCredentialData with credentialPublicKey) and attStmt. The credential ID within attestedCredentialData is limited to at most 1023 bytes. The credentialPublicKey is a variable-length COSE_Key whose length must be parsed from CBOR, with no explicit maximum. CTAP specifications require authenticators to support messages of at least 1024 bytes by default (via maxMsgSize), but responses like authenticatorMakeCredential can be larger depending on transport (e.g., up to ~7600 bytes for USB HID). A recent proposal suggests non-normative guidance of attestationObject <= 8 KB, but this is not official. In practice, sizes are typically a few hundred to a few thousand bytes, limited by transport and authenticator capabilities.

Citations:


🌐 Web query:

JavaScript engine String.fromCharCode stack overflow maximum arguments V8 SpiderMonkey

💡 Result:

JavaScript engines impose practical limits on the maximum number of arguments that can be passed to String.fromCharCode, particularly when using spread syntax (...) or Function.prototype.apply. These limits arise from implementation constraints like stack size, FixedArray capacity in V8, and resource allocation policies, not from the ECMAScript specification, which allows arbitrary-length argument lists. For V8 (Chrome, Node.js): - Spread syntax or apply typically fails around 125,000-130,000 arguments with "RangeError: too many function arguments" or "Maximum call stack size exceeded". - Tied to FixedArray::kMaxLength, approximately 64MB or 128MB worth of elements (around 8-16 million in some configs, but practical stack limits reduce it to ~125k). - Older tests showed ~65k-131k depending on version and call method. For SpiderMonkey (Firefox): - Higher tolerance, up to ~500,000 arguments via apply in older tests (Firefox 30/106). - May throw stack overflow for very large lists but generally handles more than V8. Workarounds for large inputs (e.g., Uint8Array to string): - Chunk the array and concatenate results: process in batches of ~65k or less. - Use TextDecoder: new TextDecoder.decode(uint8Array) - efficient, no argument limit issues. - Avoid spread/apply for large arrays; loop or reduce instead. No official documentation specifies exact current (2026) limits as they are internal and version-dependent. Test in target environment or use chunking/TextDecoder for safety. MDN notes no validity checks on inputs but omits argument count limits.

Citations:


🌐 Web query:

String.fromCharCode too many arguments error RangeError JavaScript

💡 Result:

The RangeError "too many arguments" with String.fromCharCode occurs when passing too many arguments to the function, typically via the spread operator (...) on large arrays (e.g., Uint8Array from file buffers > ~500KB) or Function.prototype.apply with large arrays. JavaScript engines impose implementation-specific limits on the number of arguments a function call can accept, often around 65K-500K depending on the browser and stack state (e.g., Chrome ~65K literal, higher with apply; Firefox ~65K literal, 500K apply). The ECMAScript spec does not define a hard limit, but practical constraints exist due to stack size and memory. This error is common when converting binary data (ArrayBuffer, Uint8Array) to strings for base64 encoding, like btoa(String.fromCharCode(...new Uint8Array(buffer))), which fails for files larger than a few hundred KB. To fix, process the array in smaller chunks to stay under the argument limit: function uint8ArrayToStringChunked(uint8Array, chunkSize = 32768) { let result = ''; for (let i = 0; i < uint8Array.length; i += chunkSize) { const chunk = uint8Array.subarray(i, i + chunkSize); result += String.fromCharCode.apply(null, chunk); } return result; } // Usage for base64: function bufferToBase64(buffer) { return btoa(uint8ArrayToStringChunked(new Uint8Array(buffer))); } Using subarray and apply(null, chunk) is efficient and avoids quadratic string concatenation. Chunk sizes like 32K-65K work across browsers; test for your environment. Alternatives include TextEncoder/Decoder for UTF-8 (if data is text) or modern TypedArray methods, but for latin1/binary-to-string, this is standard. MDN docs confirm String.fromCharCode accepts a variable number of UTF-16 code units (0-65535) with no explicit limit mentioned.

Citations:


Use chunking to avoid potential argument limit issues with spread operator.

While String.fromCharCode(...bytes) works fine for typical WebAuthn attestation buffers (usually a few KB), JavaScript engines impose practical argument limits on spread operator calls—around 125K in Chrome and higher in Firefox. Chunking the buffer is a defensive best practice that ensures compatibility regardless of buffer size.

Proposed fix
     encode: function (buffer) {
-        const base64 = window.btoa(String.fromCharCode(...new Uint8Array(buffer)));
+        const bytes = new Uint8Array(buffer);
+        let binary = "";
+        const chunkSize = 0x8000;
+        for (let i = 0; i < bytes.length; i += chunkSize) {
+            binary += String.fromCharCode(...bytes.subarray(i, i + chunkSize));
+        }
+        const base64 = window.btoa(binary);
         return base64.replace(/=/g, "").replace(/\+/g, "-").replace(/\//g, "_");
     },
📝 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
encode: function (buffer) {
const base64 = window.btoa(String.fromCharCode(...new Uint8Array(buffer)));
return base64.replace(/=/g, "").replace(/\+/g, "-").replace(/\//g, "_");
encode: function (buffer) {
const bytes = new Uint8Array(buffer);
let binary = "";
const chunkSize = 0x8000;
for (let i = 0; i < bytes.length; i += chunkSize) {
binary += String.fromCharCode(...bytes.subarray(i, i + chunkSize));
}
const base64 = window.btoa(binary);
return base64.replace(/=/g, "").replace(/\+/g, "-").replace(/\//g, "_");
},
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/main/resources/static/js/base64url.js` around lines 20 - 22, The encode
function uses String.fromCharCode(...new Uint8Array(buffer)) which can hit
argument count limits for large buffers; replace the spread usage with a chunked
conversion: create a Uint8Array bytes = new Uint8Array(buffer), iterate in
slices (e.g. step = 0x8000), build a string by concatenating
String.fromCharCode.apply(null, bytes.subarray(i, i+step)) or using
String.fromCharCode(...slice) per chunk, then call window.btoa on the assembled
string and keep the existing replace chain to produce base64url; update the
encode function (and the local base64 variable/window.btoa call) to use this
chunking approach.

},
decode: function (base64url) {
const base64 = base64url.replace(/-/g, "+").replace(/_/g, "/");
const binStr = window.atob(base64);
const bin = new Uint8Array(binStr.length);
for (let i = 0; i < binStr.length; i++) {
bin[i] = binStr.charCodeAt(i);
}
return bin.buffer;
},
};
Loading