Skip to content

Feature/password reset - #202

Merged
MichaelChennn merged 3 commits into
devfrom
feature/password-reset
Jul 6, 2026
Merged

Feature/password reset#202
MichaelChennn merged 3 commits into
devfrom
feature/password-reset

Conversation

@Arpad-H

@Arpad-H Arpad-H commented Jul 6, 2026

Copy link
Copy Markdown
Collaborator

password reset feature issue #174

Summary by CodeRabbit

  • New Features

    • Added a new password reset flow with emailed verification codes and a follow-up step to set a new password.
    • Updated the login modal to guide users through request, code verification, resend, and password update steps.
    • Added support for sending reset emails in local and production environments.
  • Bug Fixes

    • Improved reset handling to avoid revealing whether an account exists.
    • Added clearer error handling for invalid or expired reset codes.

@coderabbitai

coderabbitai Bot commented Jul 6, 2026

Copy link
Copy Markdown

Important

Review skipped

Auto reviews are disabled on base/target branches other than the default branch.

Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: 4b2bf681-596d-469b-bc48-67c7fd8e5ee8

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feature/password-reset

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.

@Arpad-H
Arpad-H requested review from ByudH and MichaelChennn July 6, 2026 17:03
@Arpad-H Arpad-H self-assigned this Jul 6, 2026

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 7

🧹 Nitpick comments (3)
docker-compose.yml (1)

143-150: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Pin the Mailpit image tag.

axllent/mailpit:latest is unpinned, so rebuilds can silently pick up a different image. Low risk since this is dev-only, but worth pinning for reproducibility.

Pin to a specific tag
-    image: axllent/mailpit:latest
+    image: axllent/mailpit:v1.20.5
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@docker-compose.yml` around lines 143 - 150, The Mailpit service image is
using an unpinned tag, so update the mailpit service definition to use a
specific axllent/mailpit version instead of latest. Make the change in the
mailpit block so the docker-compose setup remains reproducible across rebuilds
and references the same service name and image field.
backend/user-service/src/test/java/com/verita/userservice/service/PasswordResetServiceTests.java (1)

179-198: 🎯 Functional Correctness | 🔵 Trivial | ⚡ Quick win

Missing coverage for a null/blank token in resetPassword.

Given the null-token matching risk flagged in PasswordResetService.resetPassword (Spring Data JPA's derived-query null → IS NULL semantics), add a regression test asserting resetPassword(resetRequest(null, ...)) throws InvalidPasswordResetException once the guard is added.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In
`@backend/user-service/src/test/java/com/verita/userservice/service/PasswordResetServiceTests.java`
around lines 179 - 198, Add regression coverage for the null/blank token case in
PasswordResetServiceTests by extending the resetPassword scenarios around
service.resetPassword and resetRequest. The issue is that a null token can fall
through to tokenRepository.findByResetToken with Spring Data JPA null-to-IS NULL
behavior, so add a test asserting resetPassword(resetRequest(null, ...)) throws
InvalidPasswordResetException once the input guard is in place. Keep the
existing unknown-token and expired-token tests unchanged, and mirror their
assertion/verification style to confirm userRepository.save is not called.
backend/user-service/src/main/resources/application.properties (1)

32-44: 🧹 Nitpick | 🔵 Trivial

LGTM! Env-driven SMTP config with sane local defaults (Mailpit, no auth/TLS) and fail-open (rather than fail-closed) style for mail, which is acceptable here since mail failures are already swallowed and non-blocking for the flow. Consider adding a metric/alert on MailService send failures so a prod SMTP misconfiguration (e.g. blank MAIL_USERNAME/MAIL_PASSWORD) doesn't go unnoticed.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@backend/user-service/src/main/resources/application.properties` around lines
32 - 44, Add observability for SMTP send failures in the password-reset mail
flow so production misconfigurations don’t go unnoticed. Update the mail sending
path in MailService (and any wrapper used by the reset flow) to emit a metric
and/or alert when a send attempt fails, including the failure reason and
relevant SMTP context. Keep the existing non-blocking behavior, but make sure
failures are counted and visible for ops monitoring.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In @.github/workflows/ansible-deploy.yml:
- Around line 39-42: The ansible-playbook invocation in the deploy workflow is
building --extra-vars with inline GitHub Actions expressions, which is
vulnerable to template-injection. Move all secrets and expressions currently
embedded in the shell command into env: values for the job/step, then reference
those environment variables in the ansible-playbook command so deploy.yml
receives safely expanded values without ${{
}} interpolation inside the script.

In `@backend/user-service/api/openapi.yaml`:
- Around line 542-547: Align the password policy between RegisterRequest and
ResetPasswordRequest in openapi.yaml by adding the same complexity requirement
to ResetPasswordRequest.newPassword that RegisterRequest.password uses. Update
the ResetPasswordRequest schema so newPassword keeps minLength: 8 and also
enforces the letter-plus-digit pattern, matching the existing register
validation and preventing weaker passwords during reset.

In
`@backend/user-service/src/main/java/com/verita/userservice/entity/PasswordResetTokenEntity.java`:
- Around line 14-16: Add a database-level unique constraint for the user
identifier so the one-active-reset-token-per-user rule is enforced even under
concurrent forgot-password requests. Update the PasswordResetTokenEntity userId
mapping to declare it unique, and add the matching UNIQUE(user_id) constraint in
the migration so deleteByUserId/save cannot create duplicate rows that would
break findByUserId. Reference PasswordResetTokenEntity and its userId field when
making the change.

In
`@backend/user-service/src/main/java/com/verita/userservice/repository/PasswordResetTokenRepository.java`:
- Line 15: The reset-token lookup path in PasswordResetService.resetPassword
should guard against null or blank tokens before calling
PasswordResetTokenRepository.findByResetToken, since Spring Data will otherwise
treat a null parameter as IS NULL. Add an early validation check using the
request token in resetPassword so the repository is only queried with a real
token value, and keep the lookup behavior in findByResetToken unchanged.

In
`@backend/user-service/src/main/java/com/verita/userservice/service/PasswordResetService.java`:
- Around line 87-111: The verifyResetCode flow in PasswordResetService updates
PasswordResetTokenEntity.attempts without protecting against concurrent
requests, so parallel wrong-code attempts can overwrite each other. Change the
token lookup in verifyResetCode/tokenRepository access to acquire a row lock
before checking isExpired, maxAttempts, and incrementing attempts, then keep the
save in the same transactional flow so each failed attempt is serialized.
- Around line 120-137: The reset flow in PasswordResetService.resetPassword
currently relies on tokenRepository.findByResetToken(...) even when
ResetPasswordRequest.getToken() is empty or null, which can lead to an
unintended repository lookup. Add a local guard at the start of resetPassword to
reject missing/blank tokens and throw InvalidPasswordResetException before
touching tokenRepository, while keeping the existing expiration and user lookup
logic unchanged.

In `@frontend/src/components/modals/AuthModal/index.tsx`:
- Around line 694-711: The OTP resend UI in AuthModal stays stuck on the notice
because resendNotice is never cleared after handleResend replaces the Resend
button. Update the AuthModal flow so resendNotice is reset when the user starts
a new verification attempt or edits the OTP, likely in handleVerify or
handleOtpInput, so the otpResend section can show the Resend button again after
another attempt.

---

Nitpick comments:
In `@backend/user-service/src/main/resources/application.properties`:
- Around line 32-44: Add observability for SMTP send failures in the
password-reset mail flow so production misconfigurations don’t go unnoticed.
Update the mail sending path in MailService (and any wrapper used by the reset
flow) to emit a metric and/or alert when a send attempt fails, including the
failure reason and relevant SMTP context. Keep the existing non-blocking
behavior, but make sure failures are counted and visible for ops monitoring.

In
`@backend/user-service/src/test/java/com/verita/userservice/service/PasswordResetServiceTests.java`:
- Around line 179-198: Add regression coverage for the null/blank token case in
PasswordResetServiceTests by extending the resetPassword scenarios around
service.resetPassword and resetRequest. The issue is that a null token can fall
through to tokenRepository.findByResetToken with Spring Data JPA null-to-IS NULL
behavior, so add a test asserting resetPassword(resetRequest(null, ...)) throws
InvalidPasswordResetException once the input guard is in place. Keep the
existing unknown-token and expired-token tests unchanged, and mirror their
assertion/verification style to confirm userRepository.save is not called.

In `@docker-compose.yml`:
- Around line 143-150: The Mailpit service image is using an unpinned tag, so
update the mailpit service definition to use a specific axllent/mailpit version
instead of latest. Make the change in the mailpit block so the docker-compose
setup remains reproducible across rebuilds and references the same service name
and image field.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: b4ae6338-a878-4ba6-9960-7c2b6aecde5d

📥 Commits

Reviewing files that changed from the base of the PR and between a6358f2 and 8ceedde.

📒 Files selected for processing (29)
  • .github/workflows/ansible-deploy.yml
  • .github/workflows/helm-deploy.yml
  • .gitignore
  • backend/user-service/api/openapi.yaml
  • backend/user-service/build.gradle
  • backend/user-service/src/main/java/com/verita/userservice/controller/AuthController.java
  • backend/user-service/src/main/java/com/verita/userservice/entity/PasswordResetTokenEntity.java
  • backend/user-service/src/main/java/com/verita/userservice/exception/GlobalExceptionHandler.java
  • backend/user-service/src/main/java/com/verita/userservice/exception/InvalidPasswordResetException.java
  • backend/user-service/src/main/java/com/verita/userservice/repository/PasswordResetTokenRepository.java
  • backend/user-service/src/main/java/com/verita/userservice/service/AuthService.java
  • backend/user-service/src/main/java/com/verita/userservice/service/MailService.java
  • backend/user-service/src/main/java/com/verita/userservice/service/PasswordResetService.java
  • backend/user-service/src/main/resources/application.properties
  • backend/user-service/src/main/resources/db/migration/V3__password_reset_tokens.sql
  • backend/user-service/src/test/java/com/verita/userservice/controller/AuthControllerTests.java
  • backend/user-service/src/test/java/com/verita/userservice/exception/GlobalExceptionHandlerTests.java
  • backend/user-service/src/test/java/com/verita/userservice/service/PasswordResetServiceTests.java
  • docker-compose.prod.yml
  • docker-compose.yml
  • docs/Infrastructure_Design.md
  • frontend/src/components/modals/AuthModal/index.tsx
  • frontend/src/errors/AuthError.ts
  • frontend/src/services/auth.service.ts
  • infra/ansible/deploy.yml
  • infra/helm/verita/templates/deployment.yaml
  • infra/helm/verita/templates/secret-mail.yaml
  • infra/helm/verita/values-prod.yaml
  • infra/helm/verita/values.yaml
💤 Files with no reviewable changes (1)
  • backend/user-service/src/main/java/com/verita/userservice/service/AuthService.java

Comment on lines 39 to +42
ansible-playbook \
-i infra/ansible/inventory.ini \
infra/ansible/deploy.yml \
--extra-vars "github_actor=${{ github.actor }} github_token=${{ secrets.GITHUB_TOKEN }} user_db_password=${{ secrets.USER_DB_PASSWORD }} content_db_password=${{ secrets.CONTENT_DB_PASSWORD }} recommendation_db_password=${{ secrets.RECOMMENDATION_DB_PASSWORD }} jwt_secret=${{ secrets.JWT_SECRET }} minio_root_password=${{ secrets.MINIO_ROOT_PASSWORD }} user_service_s3_access_key=${{ secrets.USER_SERVICE_S3_ACCESS_KEY }} user_service_s3_secret_key=${{ secrets.USER_SERVICE_S3_SECRET_KEY }} content_service_s3_access_key=${{ secrets.CONTENT_SERVICE_S3_ACCESS_KEY }} content_service_s3_secret_key=${{ secrets.CONTENT_SERVICE_S3_SECRET_KEY }} internal_service_token=${{ secrets.INTERNAL_SERVICE_TOKEN }}"
--extra-vars "github_actor=${{ github.actor }} github_token=${{ secrets.GITHUB_TOKEN }} user_db_password=${{ secrets.USER_DB_PASSWORD }} content_db_password=${{ secrets.CONTENT_DB_PASSWORD }} recommendation_db_password=${{ secrets.RECOMMENDATION_DB_PASSWORD }} jwt_secret=${{ secrets.JWT_SECRET }} minio_root_password=${{ secrets.MINIO_ROOT_PASSWORD }} user_service_s3_access_key=${{ secrets.USER_SERVICE_S3_ACCESS_KEY }} user_service_s3_secret_key=${{ secrets.USER_SERVICE_S3_SECRET_KEY }} content_service_s3_access_key=${{ secrets.CONTENT_SERVICE_S3_ACCESS_KEY }} content_service_s3_secret_key=${{ secrets.CONTENT_SERVICE_S3_SECRET_KEY }} internal_service_token=${{ secrets.INTERNAL_SERVICE_TOKEN }} mail_username=${{ secrets.MAIL_USERNAME }} mail_password=${{ secrets.MAIL_PASSWORD }} mail_from=${{ secrets.MAIL_FROM }}"

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🔒 Security & Privacy | 🟠 Major | ⚡ Quick win

Pass secrets/expressions via env: instead of inline ${{ }} interpolation in the shell script.

zizmor flags this line for template-injection: GitHub Actions expands ${{ }} before the shell parses the script, so any interpolated value (e.g. github.actor) becomes part of the shell command text rather than a safely-quoted argument. This PR extends an already-flagged line with three more values.

🔒 Proposed fix using env vars
       - name: Run Ansible playbook
+        env:
+          GH_ACTOR: ${{ github.actor }}
+          GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
+          MAIL_USERNAME: ${{ secrets.MAIL_USERNAME }}
+          MAIL_PASSWORD: ${{ secrets.MAIL_PASSWORD }}
+          MAIL_FROM: ${{ secrets.MAIL_FROM }}
         run: |
           ansible-playbook \
             -i infra/ansible/inventory.ini \
             infra/ansible/deploy.yml \
-            --extra-vars "github_actor=${{ github.actor }} github_token=${{ secrets.GITHUB_TOKEN }} ... mail_username=${{ secrets.MAIL_USERNAME }} mail_password=${{ secrets.MAIL_PASSWORD }} mail_from=${{ secrets.MAIL_FROM }}"
+            --extra-vars "github_actor=$GH_ACTOR github_token=$GH_TOKEN ... mail_username=$MAIL_USERNAME mail_password=$MAIL_PASSWORD mail_from=$MAIL_FROM"
📝 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
ansible-playbook \
-i infra/ansible/inventory.ini \
infra/ansible/deploy.yml \
--extra-vars "github_actor=${{ github.actor }} github_token=${{ secrets.GITHUB_TOKEN }} user_db_password=${{ secrets.USER_DB_PASSWORD }} content_db_password=${{ secrets.CONTENT_DB_PASSWORD }} recommendation_db_password=${{ secrets.RECOMMENDATION_DB_PASSWORD }} jwt_secret=${{ secrets.JWT_SECRET }} minio_root_password=${{ secrets.MINIO_ROOT_PASSWORD }} user_service_s3_access_key=${{ secrets.USER_SERVICE_S3_ACCESS_KEY }} user_service_s3_secret_key=${{ secrets.USER_SERVICE_S3_SECRET_KEY }} content_service_s3_access_key=${{ secrets.CONTENT_SERVICE_S3_ACCESS_KEY }} content_service_s3_secret_key=${{ secrets.CONTENT_SERVICE_S3_SECRET_KEY }} internal_service_token=${{ secrets.INTERNAL_SERVICE_TOKEN }}"
--extra-vars "github_actor=${{ github.actor }} github_token=${{ secrets.GITHUB_TOKEN }} user_db_password=${{ secrets.USER_DB_PASSWORD }} content_db_password=${{ secrets.CONTENT_DB_PASSWORD }} recommendation_db_password=${{ secrets.RECOMMENDATION_DB_PASSWORD }} jwt_secret=${{ secrets.JWT_SECRET }} minio_root_password=${{ secrets.MINIO_ROOT_PASSWORD }} user_service_s3_access_key=${{ secrets.USER_SERVICE_S3_ACCESS_KEY }} user_service_s3_secret_key=${{ secrets.USER_SERVICE_S3_SECRET_KEY }} content_service_s3_access_key=${{ secrets.CONTENT_SERVICE_S3_ACCESS_KEY }} content_service_s3_secret_key=${{ secrets.CONTENT_SERVICE_S3_SECRET_KEY }} internal_service_token=${{ secrets.INTERNAL_SERVICE_TOKEN }} mail_username=${{ secrets.MAIL_USERNAME }} mail_password=${{ secrets.MAIL_PASSWORD }} mail_from=${{ secrets.MAIL_FROM }}"
env:
GH_ACTOR: ${{ github.actor }}
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
MAIL_USERNAME: ${{ secrets.MAIL_USERNAME }}
MAIL_PASSWORD: ${{ secrets.MAIL_PASSWORD }}
MAIL_FROM: ${{ secrets.MAIL_FROM }}
run: |
ansible-playbook \
-i infra/ansible/inventory.ini \
infra/ansible/deploy.yml \
--extra-vars "github_actor=$GH_ACTOR github_token=$GH_TOKEN user_db_password=${{ secrets.USER_DB_PASSWORD }} content_db_password=${{ secrets.CONTENT_DB_PASSWORD }} recommendation_db_password=${{ secrets.RECOMMENDATION_DB_PASSWORD }} jwt_secret=${{ secrets.JWT_SECRET }} minio_root_password=${{ secrets.MINIO_ROOT_PASSWORD }} user_service_s3_access_key=${{ secrets.USER_SERVICE_S3_ACCESS_KEY }} user_service_s3_secret_key=${{ secrets.USER_SERVICE_S3_SECRET_KEY }} content_service_s3_access_key=${{ secrets.CONTENT_SERVICE_S3_ACCESS_KEY }} content_service_s3_secret_key=${{ secrets.CONTENT_SERVICE_S3_SECRET_KEY }} internal_service_token=${{ secrets.INTERNAL_SERVICE_TOKEN }} mail_username=$MAIL_USERNAME mail_password=$MAIL_PASSWORD mail_from=$MAIL_FROM"
🧰 Tools
🪛 zizmor (1.26.1)

[error] 42-42: code injection via template expansion (template-injection): may expand into attacker-controllable code

(template-injection)

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In @.github/workflows/ansible-deploy.yml around lines 39 - 42, The
ansible-playbook invocation in the deploy workflow is building --extra-vars with
inline GitHub Actions expressions, which is vulnerable to template-injection.
Move all secrets and expressions currently embedded in the shell command into
env: values for the job/step, then reference those environment variables in the
ansible-playbook command so deploy.yml receives safely expanded values without
${{
}} interpolation inside the script.

Source: Linters/SAST tools

Comment thread backend/user-service/api/openapi.yaml Outdated
Comment on lines 542 to 547
ResetPasswordRequest:
type: object
required: [token, newPassword]
properties:
token: { type: string }
token: { type: string, description: The reset token returned by /verify-reset-code. }
newPassword: { type: string, minLength: 8 }

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🔒 Security & Privacy | 🟡 Minor | ⚡ Quick win

Password-reset flow allows weaker passwords than registration.

RegisterRequest.password enforces pattern: '^(?=.*[a-zA-Z])(?=.*\d).+$' (letter + digit) at line 496, but ResetPasswordRequest.newPassword only enforces minLength: 8 with no complexity pattern. Users could set a purely numeric or purely alphabetic 8-character password via reset, bypassing the complexity rule enforced at registration.

🔒 Proposed fix to align password policy
     ResetPasswordRequest:
       type: object
       required: [token, newPassword]
       properties:
         token: { type: string, description: The reset token returned by /verify-reset-code. }
-        newPassword: { type: string, minLength: 8 }
+        newPassword: { type: string, minLength: 8, pattern: '^(?=.*[a-zA-Z])(?=.*\d).+$' }
📝 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
ResetPasswordRequest:
type: object
required: [token, newPassword]
properties:
token: { type: string }
token: { type: string, description: The reset token returned by /verify-reset-code. }
newPassword: { type: string, minLength: 8 }
ResetPasswordRequest:
type: object
required: [token, newPassword]
properties:
token: { type: string, description: The reset token returned by /verify-reset-code. }
newPassword: { type: string, minLength: 8, pattern: '^(?=.*[a-zA-Z])(?=.*\d).+$' }
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@backend/user-service/api/openapi.yaml` around lines 542 - 547, Align the
password policy between RegisterRequest and ResetPasswordRequest in openapi.yaml
by adding the same complexity requirement to ResetPasswordRequest.newPassword
that RegisterRequest.password uses. Update the ResetPasswordRequest schema so
newPassword keeps minLength: 8 and also enforces the letter-plus-digit pattern,
matching the existing register validation and preventing weaker passwords during
reset.

Comment on lines +14 to +16
* <p>{@code forgot-password} creates one row per request holding a hashed 6-digit code; a matching
* {@code verify-reset-code} call stamps {@link #resetToken}, which {@code reset-password} then
* consumes. At most one active row exists per user — a new request deletes the prior rows.

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Verify whether V3 migration enforces uniqueness/index on user_id
fd 'V3__password_reset_tokens.sql' | xargs cat

Repository: AET-DevOps26/team-colleague-md

Length of output: 1179


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Inspect the entity, repository, and service usage around password reset tokens
for f in \
  backend/user-service/src/main/java/com/verita/userservice/entity/PasswordResetTokenEntity.java \
  backend/user-service/src/main/java/com/verita/userservice/repository/PasswordResetTokenRepository.java \
  backend/user-service/src/main/java/com/verita/userservice/service/PasswordResetService.java
do
  echo "===== $f ====="
  sed -n '1,220p' "$f"
  echo
done

Repository: AET-DevOps26/team-colleague-md

Length of output: 8654


Add a DB-level unique constraint on user_id
deleteByUserId + save only enforces “one active row per user” in application code. A concurrent forgot-password request can insert a second row, and findByUserId(...) will then fail with multiple matches instead of the expected reset flow. Add UNIQUE(user_id) in the migration and mirror it on PasswordResetTokenEntity.userId.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In
`@backend/user-service/src/main/java/com/verita/userservice/entity/PasswordResetTokenEntity.java`
around lines 14 - 16, Add a database-level unique constraint for the user
identifier so the one-active-reset-token-per-user rule is enforced even under
concurrent forgot-password requests. Update the PasswordResetTokenEntity userId
mapping to declare it unique, and add the matching UNIQUE(user_id) constraint in
the migration so deleteByUserId/save cannot create duplicate rows that would
break findByUserId. Reference PasswordResetTokenEntity and its userId field when
making the change.


Optional<PasswordResetTokenEntity> findByUserId(UUID userId);

Optional<PasswordResetTokenEntity> findByResetToken(String resetToken);

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🔒 Security & Privacy | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Locate the relevant files
git ls-files | rg 'backend/user-service/src/main/java/com/verita/userservice/(repository|service|entity)/.*PasswordReset.*|backend/user-service/src/main/java/com/verita/userservice/.*PasswordReset.*'

# Show the repository interface, entity, and service with line numbers
for f in \
  backend/user-service/src/main/java/com/verita/userservice/repository/PasswordResetTokenRepository.java \
  backend/user-service/src/main/java/com/verita/userservice/entity/PasswordResetTokenEntity.java \
  backend/user-service/src/main/java/com/verita/userservice/service/PasswordResetService.java
do
  echo "===== $f ====="
  wc -l "$f"
  cat -n "$f" | sed -n '1,260p'
done

Repository: AET-DevOps26/team-colleague-md

Length of output: 10856


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Find the request DTO and reset-password endpoint
git ls-files | rg 'ResetPasswordRequest|PasswordReset.*Controller|AuthController|.*Controller.java|.*Controller.kt'

# Inspect the request model and any controller entrypoint that calls PasswordResetService.resetPassword
for f in \
  backend/model/src/main/java/com/verita/model/ResetPasswordRequest.java \
  backend/user-service/src/main/java/com/verita/userservice/controller/PasswordResetController.java \
  backend/user-service/src/main/java/com/verita/userservice/controller/AuthController.java
do
  if [ -f "$f" ]; then
    echo "===== $f ====="
    wc -l "$f"
    cat -n "$f" | sed -n '1,260p'
  fi
done

# Search for validation annotations or null guards around the reset token field and resetPassword usage
rg -n -S 'resetPassword\(|getToken\(\)|`@NotNull`|`@NotBlank`|`@Valid`|ResetPasswordRequest' backend -g '!**/target/**'

Repository: AET-DevOps26/team-colleague-md

Length of output: 14014


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Inspect the OpenAPI schema and generated request model for reset token validation/nullability
for f in \
  backend/user-service/api/openapi.yaml \
  backend/model/src/main/java/com/verita/model/ResetPasswordRequest.java
do
  if [ -f "$f" ]; then
    echo "===== $f ====="
    wc -l "$f"
    cat -n "$f" | sed -n '520,590p;1,220p'
  fi
done

# Also inspect any Bean Validation annotations on request fields in the generated model
rg -n -S 'class ResetPasswordRequest|token|`@NotNull`|`@NotBlank`|`@Nullable`|`@Schema`' backend/model/src/main/java/com/verita/model/ResetPasswordRequest.java backend/user-service/api/openapi.yaml

Repository: AET-DevOps26/team-colleague-md

Length of output: 12948


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Find the generated ResetPasswordRequest model
fd -a 'ResetPasswordRequest.java|ResetPasswordRequest.kt|ResetPasswordRequest.*' backend

# Read the generated model if present
for f in $(fd -a 'ResetPasswordRequest.java|ResetPasswordRequest.kt|ResetPasswordRequest.*' backend); do
  echo "===== $f ====="
  wc -l "$f"
  cat -n "$f" | sed -n '1,220p'
done

# Inspect any tests or validation setup around auth request bodies
rg -n -S '`@Validated`|`@Valid`|`@NotNull`|`@NotBlank`|ResetPasswordRequest' backend/user-service/src/main/java backend/user-service/src/test/java backend/model/src/main/java -g '!**/target/**'

Repository: AET-DevOps26/team-colleague-md

Length of output: 2054


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Inspect user-service build/generator config and generated API interface
for f in \
  backend/user-service/pom.xml \
  backend/user-service/api/.openapi-generator-ignore \
  backend/user-service/api/openapi-generator-config.yaml \
  backend/user-service/src/main/java/com/verita/api/AuthenticationApi.java
do
  if [ -f "$f" ]; then
    echo "===== $f ====="
    wc -l "$f"
    cat -n "$f" | sed -n '1,260p'
  fi
done

# Search for bean validation / generator settings relevant to request nullability
rg -n -S 'useBeanValidation|performBeanValidation|spring-boot|openapi-generator|validation|`@Valid`|`@Validated`|`@NotNull`|`@NotBlank`|`@Schema`' backend/user-service -g '!**/target/**'

Repository: AET-DevOps26/team-colleague-md

Length of output: 1539


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Locate the generated API interface and request DTO source
git ls-files | rg 'AuthenticationApi\.java|ResetPasswordRequest\.java|ResetPasswordRequest\.kt|com/verita/api/AuthenticationApi\.java|com/verita/model/ResetPasswordRequest\.java'

# Read any matching files with line numbers
for f in $(git ls-files | rg 'AuthenticationApi\.java|ResetPasswordRequest\.java|ResetPasswordRequest\.kt|com/verita/api/AuthenticationApi\.java|com/verita/model/ResetPasswordRequest\.java'); do
  echo "===== $f ====="
  wc -l "$f"
  cat -n "$f" | sed -n '1,260p'
done

# Inspect validation/annotation usage around the auth API and reset request types
rg -n -S '`@Valid`|`@Validated`|`@NotNull`|`@NotBlank`|`@Schema`|ResetPasswordRequest|AuthenticationApi' backend/user-service/src/main/java backend/user-service/src/test/java backend/user-service/api -g '!**/target/**'

Repository: AET-DevOps26/team-colleague-md

Length of output: 168


🌐 Web query:

Spring Data JPA derived query method null parameter IS NULL Optional multiple matches IncorrectResultSizeDataAccessException documentation

💡 Result:

In Spring Data JPA, derived query methods handle null parameters and multiple results according to specific rules: 1. Null Parameter Handling: When you pass a null value to a derived query method for an equality predicate, Spring Data JPA automatically interprets this as an IS NULL condition in the generated SQL [1][2][3][4]. For example, a method findByFirstname(String firstname) will execute "... where x.firstname =?1" if a value is provided, but will be converted to "... where x.firstname IS NULL" if the argument passed is null [1][2]. 2. Multiple Results and IncorrectResultSizeDataAccessException: When a repository query method is defined to return a single entity (or an Optional), it expects the query to return at most one result [5]. If the query finds more than one record, Spring Data JPA throws an IncorrectResultSizeDataAccessException [6][5][7]. This exception is intended to enforce the constraint that the operation should only return a unique result; if you expect or intend to handle multiple results, the method should be defined to return a collection type (e.g., List or Set) instead [5][7]. 3. Optional Return Types: Using Optional as a return type for a query method changes how the absence of a result is handled—returning Optional.empty instead of null—but it does not change the behavior regarding multiple results [5]. It still expects a single result and will throw an IncorrectResultSizeDataAccessException if multiple records are found [5][7]. 4. Important Nuance: Occasionally, a query might appear to return multiple rows but still not trigger this exception. This can occur if the underlying provider (like Hibernate) identifies the multiple results as functionally equivalent based on the object's equals implementation, in which case it may consolidate them into a single result [8].

Citations:


Guard the reset token before the repository lookup
Spring Data turns a null equality parameter into IS NULL, so findByResetToken(request.getToken()) can resolve against pending reset rows — or fail if more than one exists. Add a null/blank check in PasswordResetService.resetPassword before the lookup.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In
`@backend/user-service/src/main/java/com/verita/userservice/repository/PasswordResetTokenRepository.java`
at line 15, The reset-token lookup path in PasswordResetService.resetPassword
should guard against null or blank tokens before calling
PasswordResetTokenRepository.findByResetToken, since Spring Data will otherwise
treat a null parameter as IS NULL. Add an early validation check using the
request token in resetPassword so the repository is only queried with a real
token value, and keep the lookup behavior in findByResetToken unchanged.

Comment on lines +87 to +111
@Transactional
public VerifyResetCodeResponse verifyResetCode(VerifyResetCodeRequest request) {
UserEntity user = userRepository.findByEmail(request.getEmail())
.orElseThrow(InvalidPasswordResetException::new);

PasswordResetTokenEntity entity = tokenRepository.findByUserId(user.getId())
.orElseThrow(InvalidPasswordResetException::new);

if (isExpired(entity) || entity.getAttempts() >= maxAttempts) {
throw new InvalidPasswordResetException();
}

if (!encoder.matches(request.getCode(), entity.getCodeHash())) {
entity.setAttempts(entity.getAttempts() + 1);
tokenRepository.save(entity);
throw new InvalidPasswordResetException();
}

entity.setResetToken(UUID.randomUUID().toString());
tokenRepository.save(entity);

VerifyResetCodeResponse response = new VerifyResetCodeResponse();
response.setResetToken(entity.getResetToken());
return response;
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🩺 Stability & Availability | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Locate the service, repository, and entity involved in password reset.
git ls-files | rg 'PasswordReset(Service|TokenEntity|TokenRepository|.*Request|.*Response)\.java$|UserEntity\.java$'

# Show relevant outlines for structure and annotations.
for f in \
  backend/user-service/src/main/java/com/verita/userservice/service/PasswordResetService.java \
  backend/user-service/src/main/java/com/verita/userservice/repository/PasswordResetTokenRepository.java \
  backend/user-service/src/main/java/com/verita/userservice/model/PasswordResetTokenEntity.java
do
  echo "===== $f ====="
  ast-grep outline "$f" --view expanded || true
done

# Print the relevant slices with line numbers.
for f in \
  backend/user-service/src/main/java/com/verita/userservice/service/PasswordResetService.java \
  backend/user-service/src/main/java/com/verita/userservice/repository/PasswordResetTokenRepository.java \
  backend/user-service/src/main/java/com/verita/userservice/model/PasswordResetTokenEntity.java
do
  echo "===== $f (excerpt) ====="
  wc -l "$f"
done

sed -n '1,220p' backend/user-service/src/main/java/com/verita/userservice/service/PasswordResetService.java | cat -n
echo "===== repository ====="
sed -n '1,220p' backend/user-service/src/main/java/com/verita/userservice/repository/PasswordResetTokenRepository.java | cat -n
echo "===== entity ====="
sed -n '1,260p' backend/user-service/src/main/java/com/verita/userservice/model/PasswordResetTokenEntity.java | cat -n

# Search for locking/versioning/query hints on the token entity/repository.
rg -n "`@Version`|`@Lock`|PESSIMISTIC|OPTIMISTIC|LockModeType|SELECT .*FOR UPDATE|attempts" backend/user-service/src/main/java/com/verita/userservice -g '!**/target/**'

Repository: AET-DevOps26/team-colleague-md

Length of output: 2312


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Inspect the entity and any persistence/transaction settings that could affect concurrency.
sed -n '1,260p' backend/user-service/src/main/java/com/verita/userservice/entity/PasswordResetTokenEntity.java | cat -n

echo "===== search for version/locking/isolation ====="
rg -n "`@Version`|`@Lock`|PESSIMISTIC|OPTIMISTIC|Isolation|isolation|hibernate\.jdbc|spring\.jpa|transaction-isolation|READ_COMMITTED|READ COMMITTED" \
  backend/user-service/src/main/resources backend/user-service/src/main/java -g '!**/target/**'

echo "===== related repository/service references ====="
rg -n "findByUserId\(|save\(entity\)|setAttempts\(|getAttempts\(" backend/user-service/src/main/java/com/verita/userservice -g '!**/target/**'

Repository: AET-DevOps26/team-colleague-md

Length of output: 4329


Lock the reset-token row before incrementing attempts. Concurrent wrong-code requests can read the same attempts value and overwrite each other’s increments under READ COMMITTED, which lets an attacker bypass maxAttempts with enough parallel guesses.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In
`@backend/user-service/src/main/java/com/verita/userservice/service/PasswordResetService.java`
around lines 87 - 111, The verifyResetCode flow in PasswordResetService updates
PasswordResetTokenEntity.attempts without protecting against concurrent
requests, so parallel wrong-code attempts can overwrite each other. Change the
token lookup in verifyResetCode/tokenRepository access to acquire a row lock
before checking isExpired, maxAttempts, and incrementing attempts, then keep the
save in the same transactional flow so each failed attempt is serialized.

Comment on lines +120 to +137
public void resetPassword(ResetPasswordRequest request) {
PasswordResetTokenEntity entity = tokenRepository.findByResetToken(request.getToken())
.orElseThrow(InvalidPasswordResetException::new);

if (isExpired(entity)) {
throw new InvalidPasswordResetException();
}

UserEntity user = userRepository.findById(entity.getUserId())
.orElseThrow(InvalidPasswordResetException::new);

user.setPassword(encoder.encode(request.getNewPassword()));
user.setRefreshToken(null);
user.setRefreshTokenExpiry(null);
userRepository.save(user);

tokenRepository.deleteByUserId(user.getId());
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🔒 Security & Privacy | 🔴 Critical | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

printf '\n## Files matching ResetPasswordRequest / PasswordResetController / findByResetToken\n'
git ls-files | rg 'ResetPasswordRequest|PasswordResetController|PasswordResetService|PasswordResetTokenRepository|openapi|swagger|yaml|yml'

printf '\n## Search for token validation annotations and reset password endpoints\n'
rg -n --hidden --glob '!**/target/**' --glob '!**/build/**' --glob '!**/dist/**' \
  'ResetPasswordRequest|PasswordResetController|findByResetToken|`@NotNull`|`@NotBlank`|`@Valid`|resetPassword' \
  backend/user-service/src/main/java backend/user-service/src/main/resources . 2>/dev/null || true

Repository: AET-DevOps26/team-colleague-md

Length of output: 11736


🏁 Script executed:

#!/bin/bash
set -euo pipefail

printf '\n## AuthController excerpt\n'
sed -n '1,130p' backend/user-service/src/main/java/com/verita/userservice/controller/AuthController.java | cat -n

printf '\n## PasswordResetTokenRepository excerpt\n'
sed -n '1,80p' backend/user-service/src/main/java/com/verita/userservice/repository/PasswordResetTokenRepository.java | cat -n

printf '\n## OpenAPI ResetPasswordRequest schema excerpt\n'
sed -n '520,590p' backend/user-service/api/openapi.yaml | cat -n

printf '\n## Generated ResetPasswordRequest model excerpt\n'
fd -a 'ResetPasswordRequest.java' backend/user-service | xargs -r sed -n '1,220p' | cat -n

Repository: AET-DevOps26/team-colleague-md

Length of output: 8725


🏁 Script executed:

#!/bin/bash
set -euo pipefail

printf '\n## Generated API interface and model locations\n'
git ls-files | rg 'AuthenticationApi\.java|ResetPasswordRequest\.java|VerifyResetCodeRequest\.java|ForgotPasswordRequest\.java|OpenApi|ApiController|model/ResetPasswordRequest'

printf '\n## Search for validation annotations on reset-password types\n'
rg -n --hidden --glob '!**/target/**' --glob '!**/build/**' --glob '!**/dist/**' \
  '`@NotNull`|`@NotBlank`|`@Valid`|class ResetPasswordRequest|interface AuthenticationApi|resetPassword\(' \
  backend/user-service/src/main/java backend/user-service/src/generated backend/user-service/target 2>/dev/null || true

Repository: AET-DevOps26/team-colleague-md

Length of output: 216


🏁 Script executed:

#!/bin/bash
set -euo pipefail

printf '\n## user-service tree around potential generated sources\n'
fd -a -t d 'src|generated|openapi|api' backend/user-service | sed -n '1,200p'

printf '\n## Search for OpenAPI generator and validation config\n'
rg -n --hidden --glob '!**/target/**' --glob '!**/build/**' --glob '!**/dist/**' \
  'openapi-generator|springdoc|swagger|`@Validated`|`@Valid`|`@NotNull`|`@NotBlank`|bean validation|validation' \
  backend/user-service pom.xml .github/workflows backend/user-service/api backend/user-service/src/main 2>/dev/null || true

printf '\n## Find any AuthenticationApi or ResetPasswordRequest sources anywhere\n'
fd -a 'AuthenticationApi.java|ResetPasswordRequest.java' . | sed -n '1,200p'

Repository: AET-DevOps26/team-colleague-md

Length of output: 1529


🏁 Script executed:

#!/bin/bash
set -euo pipefail

printf '\n## backend/user-service/build.gradle excerpt\n'
sed -n '1,220p' backend/user-service/build.gradle | cat -n

Repository: AET-DevOps26/team-colleague-md

Length of output: 4319


🌐 Web query:

OpenAPI Generator spring generator interfaceOnly requestBody @Validrequired property@NotNull useBeanValidation default

💡 Result:

In the OpenAPI Generator Spring generator, the inclusion of @NotNull on request bodies and path variables is controlled primarily by the additionalNotNullAnnotations configuration option, rather than relying solely on useBeanValidation [1][2]. Key Configuration Details: 1. additionalNotNullAnnotations: Introduced to provide explicit control, this option defaults to false [2]. When set to true, the generator includes @NotNull annotations on request bodies and required path variables [1][2]. If you require these annotations, you must explicitly enable this property in your generator configuration (e.g., via configOptions or --additional-properties) [2]. 2. useBeanValidation: This property controls the generation of general Jakarta/JSR-303 Bean Validation API annotations (e.g., @Size, @Pattern, @Min, @Max) [1]. While historically linked to @NotNull, recent updates have decoupled the specific injection of @NotNull for request bodies and path variables to allow for more granular control [2]. Setting useBeanValidation to true will generate validation annotations on your models and parameters, provided the library supports it [3][4]. 3. interfaceOnly: When interfaceOnly is set to true, the generator produces API interface stubs [1]. In this mode, the generation of @Valid and @NotNull still adheres to the configuration of useBeanValidation and additionalNotNullAnnotations [3][4]. Note that for specific libraries like spring-http-interface, recent updates have improved support for honoring these bean validation flags, allowing you to use bean validation even when generating interfaces [3][4]. If you find that @NotNull or @Valid annotations are being generated when they should not be (or vice versa), ensure that both useBeanValidation and additionalNotNullAnnotations are explicitly set in your configuration, as behavior can sometimes depend on the specific library (e.g., spring-boot, spring-http-interface) and the version of the generator you are using [2][5][6].

Citations:


🌐 Web query:

OpenAPI Generator spring generator performBeanValidation @Valid @NotNull required schema properties

💡 Result:

To enable Bean Validation (such as @Valid and @NotNull) for your Spring Boot application using the OpenAPI Generator, you must configure the following options in your generator configuration (e.g., in your Maven or Gradle plugin configuration) [1][2]: 1. useBeanValidation: Set this to true to enable the generation of Bean Validation API annotations (like @NotNull, @Size, @Email) on your model objects based on your OpenAPI schema definitions [3][1][2]. 2. performBeanValidation: Set this to true to enable the generation of code that triggers bean validation logic within the Spring controller layer [3][1][2]. These settings ensure that when an API request is received, Spring automatically validates the incoming data against the constraints defined in your OpenAPI specification [2]. Important configuration details: - If you are using a modern version of Spring Boot (Spring Boot 3.x+), you should also ensure that useJakartaEe is set to true (default in many newer versions) so that the generator uses jakarta.validation annotations instead of the deprecated javax.validation packages [4]. - When enabled, the controller classes are typically annotated with @Validated, which instructs Spring to perform the validation [5][6]. - For Spring Boot 6.x or later, if you encounter conflicts with class-level @Validated, you may optionally set useSpringBuiltInValidation to true to disable it, aligning with updated Spring Framework guidelines [6]. Example configuration (Maven): true true true By including these options, the OpenAPI Generator will map required schema properties to @NotNull annotations and apply other relevant constraints, providing automatic validation for your request bodies and parameters [2].

Citations:


🏁 Script executed:

#!/bin/bash
set -euo pipefail

rg -n --hidden --glob '!**/target/**' --glob '!**/build/**' --glob '!**/dist/**' \
  'spring-boot-starter-validation|hibernate-validator|jakarta.validation-api|jakarta.validation' \
  backend/user-service/build.gradle backend/user-service/pom.xml . 2>/dev/null || true

Repository: AET-DevOps26/team-colleague-md

Length of output: 1882


🌐 Web query:

Spring Boot 4 spring-boot-starter-web includes validation dependency or not

💡 Result:

No, the spring-boot-starter-web (or its successor, spring-boot-starter-webmvc, in Spring Boot 4) does not include the validation dependency by default [1][2]. If your application requires Bean Validation, you must explicitly add the spring-boot-starter-validation dependency to your project's build file [1][3]. This has been the standard behavior since Spring Boot 2.3 [3][4]. In Spring Boot 4, the web starter was renamed to spring-boot-starter-webmvc as part of a modularization effort, but it maintains the same behavior regarding the validation starter [2]. To include validation, add the following dependency to your project: Maven: org.springframework.boot spring-boot-starter-validation Gradle: implementation 'org.springframework.boot:spring-boot-starter-validation'

Citations:


🌐 Web query:

OpenAPI Generator spring interfaceOnly generated interface @Valid @NotNull required request body model

💡 Result:

To ensure the OpenAPI Generator for Spring correctly generates @Valid and @NotNull annotations for request bodies when using interfaceOnly, you must configure specific generator properties. Configuration Requirements: 1. Enable Bean Validation: Set useBeanValidation to true in your configuration [1][2]. While this was previously restricted in some spring-http-interface library configurations, recent updates allow it to be enabled [3][4]. 2. Enable Request Body Nullability Annotations: By default, the generator may not emit @NotNull for request bodies even if required in the OpenAPI specification [5]. You must set the additionalNotNullAnnotations property to true to ensure these are generated on request bodies and path variables [6][7][5]. Example Configuration (Maven/Gradle): In your configuration block, include these properties: true true true true Key Considerations: - Bean Validation: Setting useBeanValidation=true triggers the generation of Jakarta/Javax validation annotations (such as @NotNull) on your model fields based on the OpenAPI schema definition [3][4]. - Interface @Valid: When useBeanValidation is enabled, the generator typically adds @Valid to method parameters (like the request body) in the interface to trigger Spring's validation mechanism when the method is invoked [3]. - Additional Annotations: The additionalNotNullAnnotations option was specifically introduced to provide explicit control over adding @NotNull to required request bodies and path variables, as this behavior changed in recent versions to be disabled by default [5]. Ensure your project includes the necessary validation dependencies (e.g., spring-boot-starter-validation) in your build file (Maven or Gradle) to support these annotations at runtime [3].

Citations:


🌐 Web query:

Spring Data JPA derived query null parameter translates to IS NULL

💡 Result:

Yes, in Spring Data JPA, when using derived query methods, passing a null value for an equality condition (e.g., using Is or Equals keywords) automatically translates the generated query to use IS NULL instead of an equality check [1][2][3]. For example, a method defined as: List findByFirstname(String firstname); If you call this method with a null argument, the generated SQL clause will be:... where x.firstname IS NULL Similarly, for inequality checks using the Not keyword, passing a null value translates to IS NOT NULL [1][4][5]. If you prefer to be explicit or if you do not want to pass parameters, you can use the dedicated keywords IsNull or IsNotNull in your method name, which do not accept arguments [6][3][7]: List findByNameIsNull; List findByNameIsNotNull; This behavior for equality and inequality keywords ensures that Spring Data JPA handles null parameters consistently with SQL semantics [3].

Citations:


🏁 Script executed:

#!/bin/bash
set -euo pipefail

printf '\n## PasswordResetService excerpt around forgot/verify/reset\n'
sed -n '1,220p' backend/user-service/src/main/java/com/verita/userservice/service/PasswordResetService.java | cat -n

printf '\n## PasswordResetTokenEntity excerpt\n'
sed -n '1,220p' backend/user-service/src/main/java/com/verita/userservice/entity/PasswordResetTokenEntity.java | cat -n

Repository: AET-DevOps26/team-colleague-md

Length of output: 9146


Reject empty reset tokens before the repository lookup
ResetPasswordRequest.token is required in the OpenAPI spec, but this module doesn’t enable bean-validation enforcement, so a missing token can still reach findByResetToken(...). Spring Data JPA treats a null equality parameter as IS NULL, and resetToken is nullable before verification, so guard this locally.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In
`@backend/user-service/src/main/java/com/verita/userservice/service/PasswordResetService.java`
around lines 120 - 137, The reset flow in PasswordResetService.resetPassword
currently relies on tokenRepository.findByResetToken(...) even when
ResetPasswordRequest.getToken() is empty or null, which can lead to an
unintended repository lookup. Add a local guard at the start of resetPassword to
reject missing/blank tokens and throw InvalidPasswordResetException before
touching tokenRepository, while keeping the existing expiration and user lookup
logic unchanged.

Comment on lines 694 to 711
<div className={styles.otpResend}>
Didn't get a code?{' '}
<button className={styles.switchLinkBtn}>Resend</button>
{resendNotice ? (
<span data-testid="otp-resend-notice">{resendNotice}</span>
) : (
<>
Didn't get a code?{' '}
<button
type="button"
className={styles.switchLinkBtn}
data-testid="otp-resend-btn"
onClick={handleResend}
disabled={loading}
>
Resend
</button>
</>
)}
</div>

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Resend button is permanently hidden after the first resend.

Once handleResend sets resendNotice, this ternary swaps the "Resend" button for the notice text, and nothing clears resendNotice while the user stays on the OTP screen (handleVerify clears error but not resendNotice). A user who resent once but still didn't receive a code (or whose second code also fails) cannot request another one without leaving and re-entering the flow.

Consider clearing resendNotice when the user next edits the OTP (in handleOtpInput) or on the next verify attempt so the Resend affordance returns.

Proposed fix (clear notice on verify attempt)
   async function handleVerify() {
     const code = otpDigits.join('');
     if (code.length < 6 || loading) return;
     setError('');
+    setResendNotice('');
     setLoading(true);
📝 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
<div className={styles.otpResend}>
Didn't get a code?{' '}
<button className={styles.switchLinkBtn}>Resend</button>
{resendNotice ? (
<span data-testid="otp-resend-notice">{resendNotice}</span>
) : (
<>
Didn't get a code?{' '}
<button
type="button"
className={styles.switchLinkBtn}
data-testid="otp-resend-btn"
onClick={handleResend}
disabled={loading}
>
Resend
</button>
</>
)}
</div>
async function handleVerify() {
const code = otpDigits.join('');
if (code.length < 6 || loading) return;
setError('');
setResendNotice('');
setLoading(true);
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@frontend/src/components/modals/AuthModal/index.tsx` around lines 694 - 711,
The OTP resend UI in AuthModal stays stuck on the notice because resendNotice is
never cleared after handleResend replaces the Resend button. Update the
AuthModal flow so resendNotice is reset when the user starts a new verification
attempt or edits the OTP, likely in handleVerify or handleOtpInput, so the
otpResend section can show the Resend button again after another attempt.

- reject null/blank reset tokens before the repository lookup so a missing
  token cannot resolve to `reset_token IS NULL` and match an unverified row
- enforce UNIQUE(user_id) on password_reset_tokens (migration + entity) so
  concurrent forgot-password requests cannot create duplicate rows
- align reset newPassword complexity (letter + digit) with registration
- clear the OTP resend notice on re-verify so the Resend button returns

@MichaelChennn MichaelChennn left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Fix some issued suggested by coderabbitai.

@MichaelChennn
MichaelChennn merged commit debcb31 into dev Jul 6, 2026
9 checks passed
@MichaelChennn
MichaelChennn deleted the feature/password-reset branch July 6, 2026 21:03
MichaelChennn added a commit that referenced this pull request Jul 17, 2026
## Linked Issues
<!-- Final sprint-end release PR: dev → main -->
Resolves #

## Summary of Changes
Final release promoting ~30 commits from `dev` to `main`, including:
- **Digest**: standalone digest entity with structured events &
public/private management (ADR-0019, #203), latest-digest-per-day
history fix (#208), deterministic digest titles, daily-digest default
for new users
- **Admin**: admin panel with user management + GenAI ops surface
(ADR-0020) & 404 page (#206), per-day digest generation with tracked
status, verified-callable model suggestions
- **GenAI**: output sanitization + local Ollama inference support
(#210), runtime LLM provider/model selection, LLM env vars across deploy
configs (#207)
- **User**: password reset via Brevo SMTP (#202), profile
postCount/likeReceivedCount write-back fixes (#178/#204)
- **Infra/Deploy**: GenAI & mail secrets wired into Helm deploy,
`docker-compose.prod.yml` LLM/mail env vars, manual production demo
seeding workflow (#209)
- **Docs/Tests**: review-ready docs + restructured frontend e2e suite
(#211)

## Testing Performed
- [x] PR targets `main` intentionally — this is the sprint-end release
PR (`dev` → `main`)
- [x] `npm run lint` + `npm run build` passed (frontend)
- [x] `./gradlew test` passed for user-service, content-service,
recommendation-service
- [x] GenAI service tests green in CI (`CI - GenAI Service`); all latest
per-service CI runs on `dev` are green
- [x] Merge `dev` → `main` verified conflict-free locally
- [x] All deploy-required GitHub Secrets present (MAIL_*,
NVIDIA_NIM_API_KEY, LOGOS_API_KEY, GNEWS_API_KEY, etc.)
- [x] Helm Deploy (Rancher/K8s) run on 2026-07-16 succeeded

## Review Notes
**Draft reason — one open deployment issue:** the Ansible Deploy to the
Azure VM (68.210.231.173) failed on 2026-07-16 with an SSH connection
timeout (host unreachable). This is environmental (VM stopped or
NSG/firewall), not a code issue — the Helm/Rancher deploy path is
healthy. Once the VM is reachable (or if the VM path is out of scope for
this release), mark this PR ready for review.

🤖 Generated with [Claude Code](https://claude.com/claude-code)
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