Thanks to visit codestin.com
Credit goes to github.com

Skip to content

Conversation

@Tia-ani
Copy link
Contributor

@Tia-ani Tia-ani commented Nov 15, 2025

Summary

  • This PR fixes the security issue where users could change their email address from the profile edit page without verification.
  • Previously, the system updated the User.email field immediately, allowing unverified email changes.

What This Fix Does:

  1. Prevents direct email updates until the new email is verified
  2. Uses EmailAddress model from django-allauth
  3. Sends a confirmation link to the new email
  4. Keeps the old email active until verification is completed
  5. Deletes any stale unverified email entries before creating a new one

Implementation Details:

Added logic in profile_edit:

  • Detects when email is changed
  • Creates a new unverified EmailAddress entry
  • Sends verification link using send_email_confirmation
  • Does not update request.user.email until user confirms
  • Preserves behavior for other profile fields
  • Adds user-facing message: “A verification link has been sent to your new email.”

How to Test:

  1. Log in
  2. Go to /profile/edit/
  3. Change the email to a new address
  4. Submit

Expect:

  • A success message about verification
  • User email should remain unchanged until the link is opened

Related Issue:
Fixes: #4783

Summary by CodeRabbit

  • New Features

    • Email verification workflow: changing your profile email sends a verification link, clears prior unverified addresses, shows an informational message, and defers updating the account email until verification.
    • Edit form prepopulates the current email for convenience.
  • Bug Fixes / Validation

    • Prevents submitting an address already registered or pending verification; if the email is unchanged, the normal success flow is preserved.
  • Tests

    • Updated tests to expect the verification message and that the account email remains unchanged until verification.

@coderabbitai
Copy link
Contributor

coderabbitai bot commented Nov 15, 2025

Walkthrough

The profile_edit view now detects and validates email changes, preserves the original User.email on save, removes the user's unverified EmailAddress records, creates a new unverified EmailAddress, sends a confirmation email, prepopulates the edit form with the current email, and tests were updated to expect the verification flow instead of immediate email replacement.

Changes

Cohort / File(s) Change Summary
Profile edit — email verification
website/views/user.py
Added local imports for EmailAddress and send_email_confirmation; capture original email before processing; validate new email uniqueness; on email change delete the user's unverified EmailAddress records, create a new unverified EmailAddress, call send_email_confirmation, redirect with an informational verification message, and prepopulate the edit form with the current email without immediately updating User.email.
Tests — expect verification flow
website/tests/test_user_profile.py
Updated assertions to expect a verification message starting with "A verification link has been sent to your new email."; assert User.email remains the original after submission; extracted messages into messages_list and adjusted assertions to reflect verification flow.
Metadata
manifest_file, requirements.txt, pyproject.toml
Listed in diff summary; no substantive code-level changes described.

Sequence Diagram(s)

sequenceDiagram
    actor User
    participant ProfileEdit as Profile Edit View
    participant DB as Database
    participant Email as Email Service

    User->>ProfileEdit: GET /profile/edit
    ProfileEdit->>DB: Read user & current email
    ProfileEdit->>User: Render form prepopulated with current email

    User->>ProfileEdit: POST form with updated data
    ProfileEdit->>ProfileEdit: Capture original email
    ProfileEdit->>DB: Save other profile fields (do not overwrite User.email)
    alt New email != original email
        ProfileEdit->>DB: Delete user's unverified EmailAddress records
        ProfileEdit->>DB: Create new unverified EmailAddress(new email)
        ProfileEdit->>Email: send_email_confirmation(new EmailAddress)
        ProfileEdit->>User: Redirect with informational message (verification sent)
    else Email unchanged
        ProfileEdit->>User: Redirect with standard success message
    end
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

  • Inspect website/views/user.py for correctness of uniqueness validation across verified and pending EmailAddress entries.
  • Verify deletion logic targets only unverified EmailAddress rows for the current user.
  • Ensure the new EmailAddress is persisted before send_email_confirmation is called.
  • Confirm website/tests/test_user_profile.py assertions exactly match redirect/message text and cover both unchanged and changed-email scenarios.

Pre-merge checks and finishing touches

❌ Failed checks (1 warning)
Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 66.67% which is insufficient. The required threshold is 80.00%. You can run @coderabbitai generate docstrings to improve docstring coverage.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title directly and clearly describes the main security fix: enforcing email verification when users change their email via profile editing.
Linked Issues check ✅ Passed The PR fully addresses issue #4783 by implementing email verification enforcement for profile email changes, preventing unverified emails from becoming active immediately.
Out of Scope Changes check ✅ Passed All changes are directly related to the email verification security fix; no unrelated modifications to other features or files were introduced.
✨ Finishing touches
  • 📝 Generate docstrings
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment

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

❤️ Share

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

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 1

🧹 Nitpick comments (1)
website/views/user.py (1)

147-147: Optional: Silence the unused variable warning.

The created variable from get_or_create is never used. Consider using _ to indicate it's intentionally ignored.

-    user_profile, created = UserProfile.objects.get_or_create(user=request.user)
+    user_profile, _ = UserProfile.objects.get_or_create(user=request.user)

Based on static analysis hints.

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

Knowledge base: Disabled due to Reviews -> Disable Knowledge Base setting

📥 Commits

Reviewing files that changed from the base of the PR and between ff701fe and 9be6bbc.

📒 Files selected for processing (1)
  • website/views/user.py (2 hunks)
🧰 Additional context used
🪛 Ruff (0.14.4)
website/views/user.py

147-147: Unpacked variable created is never used

Prefix it with an underscore or any other dummy variable pattern

(RUF059)

⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (2)
  • GitHub Check: Run Tests
  • GitHub Check: docker-test
🔇 Additional comments (4)
website/views/user.py (4)

143-144: Good use of django-allauth for email verification.

The function-scoped imports are appropriate here since these utilities are only needed in this specific view.


169-188: Email verification flow looks solid.

The implementation correctly:

  • Clears stale unverified email entries to prevent confusion
  • Creates a new unverified EmailAddress entry with primary=False
  • Sends verification email using django-allauth's utility
  • Provides clear user feedback
  • Follows the Post-Redirect-Get pattern

Minor note: There's a small TOCTOU (time-of-check to time-of-use) window between the uniqueness check (line 159) and the EmailAddress creation (line 174), but get_or_create will handle this gracefully, so this is not a significant concern.


198-201: Good UX: Form correctly pre-populates the email field.

Using initial to populate the email field ensures users see their current email address when editing their profile, which is the expected behavior.


166-167: ✓ Verified: form.save() does not update request.user.email

The concern has been validated and found to be already addressed correctly in the code:

  1. The email field in UserProfileForm is declared but not included in Meta.fields (lines 28-44 of website/forms.py)
  2. Django's ModelForm.save() only saves fields listed in Meta.fields, so email will not be updated
  3. Any custom save() method that would update User.email is commented out (lines 60-68 of website/forms.py), confirming this behavior was intentional
  4. Email is handled correctly via the EmailAddress model (lines 174-177 of user.py) as an unverified record, preventing immediate activation

The code is correct as-is. No action required.

@github-project-automation github-project-automation bot moved this from Backlog to Ready in 📌 OWASP BLT Project Board Nov 15, 2025
Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 2

🧹 Nitpick comments (2)
website/views/user.py (2)

163-165: Consider checking if the email already exists for the current user.

The uniqueness check excludes all EmailAddress entries for request.user, but this could miss edge cases:

  • If the user has this email as an existing unverified secondary email from a previous attempt, the check passes.
  • Then line 175 deletes it, which is fine.
  • But if the user has this email as a verified secondary (non-primary) EmailAddress, the check passes, but line 175 won't delete it (only deletes unverified), and then line 178's get_or_create will find the existing verified entry without updating it properly.

Consider refining the check:

+            # Check if email already exists for current user
+            existing_user_email = EmailAddress.objects.filter(user=request.user, email=new_email).first()
+            if existing_user_email:
+                if existing_user_email.verified and existing_user_email.primary:
+                    # This is their current primary email, will be caught by email_changed check
+                    pass
+                else:
+                    form.add_error("email", "This email is already associated with your account")
+                    return render(request, "profile_edit.html", {"form": form})
+
-            if EmailAddress.objects.exclude(user=request.user).filter(email=new_email).exists():
+            # Check if email exists for other users
+            if EmailAddress.objects.exclude(user=request.user).filter(email=new_email).exists():
                 form.add_error("email", "This email is already registered or pending verification")
                 return render(request, "profile_edit.html", {"form": form})

173-192: Consider guarding against race conditions in email creation.

Between the uniqueness check (line 163-165) and the EmailAddress creation (line 178-182), another concurrent request could create an EmailAddress with the same email, potentially causing an IntegrityError depending on database constraints.

Consider wrapping the email change logic in a try-except block:

if email_changed:
    try:
        # Remove any pending unverified emails
        EmailAddress.objects.filter(user=request.user, verified=False).delete()
        
        # Create new unverified email entry
        EmailAddress.objects.create(
            user=request.user,
            email=new_email,
            verified=False,
            primary=False,
        )
        
        # Send verification email
        send_email_confirmation(request, request.user, email=new_email)
        
        messages.info(
            request,
            "A verification link has been sent to your new email. "
            "Please verify to complete the update."
        )
    except IntegrityError:
        messages.error(request, "This email is already in use. Please try a different email.")
    except Exception as e:
        logger.error(f"Error during email change: {e}")
        messages.error(request, "An error occurred. Please try again.")
    
    return redirect("profile", slug=request.user.username)
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

Knowledge base: Disabled due to Reviews -> Disable Knowledge Base setting

📥 Commits

Reviewing files that changed from the base of the PR and between 9be6bbc and 62beeee.

📒 Files selected for processing (1)
  • website/views/user.py (2 hunks)
🧰 Additional context used
🪛 Ruff (0.14.4)
website/views/user.py

147-147: Unpacked variable created is never used

Prefix it with an underscore or any other dummy variable pattern

(RUF059)

⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (2)
  • GitHub Check: Run Tests
  • GitHub Check: docker-test
🔇 Additional comments (1)
website/views/user.py (1)

170-171: No issues found – the comment is accurate.

The form defines an email field for validation but excludes it from Meta.fields (lines 28-44 in website/forms.py). Since the email field is not in the form's fields list, form.save() does not persist it anywhere. The actual email update is handled separately via the EmailAddress model when email_changed is true, which is the correct approach. The comment accurately reflects this behavior.

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 0

🧹 Nitpick comments (2)
website/views/user.py (2)

147-147: Tidy up unused created from get_or_create

created is never used; to satisfy Ruff and reduce noise, consider:

-    user_profile, created = UserProfile.objects.get_or_create(user=request.user)
+    user_profile, _ = UserProfile.objects.get_or_create(user=request.user)

Based on static analysis hints.


173-205: EmailAddress flow is solid; refine exception handling & logging

Deleting any unverified EmailAddress rows, creating a fresh unverified entry for new_email, and only then sending send_email_confirmation correctly keeps request.user.email unchanged until verification, while preserving profile-field updates.

To improve robustness and satisfy Ruff:

  1. Prefer more specific exceptions for the email send failure if feasible (e.g., SMTP/email‑backend exceptions) instead of a blind except Exception.
  2. Use logger.exception so the stack trace is recorded:
-            try:
-                send_email_confirmation(request, request.user, email=new_email)
-            except Exception as e:
-                logger.error(f"Failed to send email confirmation to {new_email}: {e}")
+            try:
+                send_email_confirmation(request, request.user, email=new_email)
+            except Exception:
+                logger.exception("Failed to send email confirmation to %s", new_email)
                 messages.error(
                     request,
                     "Failed to send verification email. Please try again later."
                 )
                 return redirect("profile", slug=request.user.username)

Based on static analysis hints.

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

Knowledge base: Disabled due to Reviews -> Disable Knowledge Base setting

📥 Commits

Reviewing files that changed from the base of the PR and between 62beeee and 3620da9.

📒 Files selected for processing (1)
  • website/views/user.py (2 hunks)
🧰 Additional context used
🪛 Ruff (0.14.4)
website/views/user.py

147-147: Unpacked variable created is never used

Prefix it with an underscore or any other dummy variable pattern

(RUF059)


189-189: Do not catch blind exception: Exception

(BLE001)


190-190: Use logging.exception instead of logging.error

Replace with exception

(TRY400)

⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (2)
  • GitHub Check: Run Tests
  • GitHub Check: docker-test
🔇 Additional comments (2)
website/views/user.py (2)

143-145: Local imports for email verification helpers are fine

Importing the allauth email models/utils inside profile_edit is reasonable here (and can help avoid import cycles); no change required.


149-169: Verification confirms email-change flow is secure

The problematic save() method that was directly mutating User.email has been removed (it's now commented out at lines 60-68). Additionally, any fields not included in Meta.fields will not be set by the form's save() method, and email is not listed in UserProfileForm.Meta.fields. This means the form cannot inadvertently update the User model's email field—email changes are now isolated to the verification branch as intended.

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 1

🧹 Nitpick comments (2)
website/tests/test_user_profile.py (2)

107-109: Update the comment to reflect the new verification flow.

The comment describes the old behavior focusing on uniqueness validation. It should now explain that the implementation creates an unverified EmailAddress entry and sends a verification link, keeping the User.email unchanged until verification completes.

Apply this diff to clarify the comment:

-        # The current implementation doesn't update the User model's email
-        # It only validates that the email is unique
-        # So we just verify that the form was submitted successfully
+        # The implementation triggers email verification flow when email changes
+        # It validates uniqueness, creates an unverified EmailAddress entry,
+        # and sends a verification link while keeping User.email unchanged

110-112: Consider verifying EmailAddress creation and email sending.

The test correctly validates the user-facing message, but it doesn't verify that the underlying verification infrastructure was set up. Consider adding assertions to check:

  1. An unverified EmailAddress object was created for the new email
  2. A verification email was sent (using django.core.mail.outbox)

Example additions to enhance test coverage:

from allauth.account.models import EmailAddress
from django.core import mail

# After line 111, add:
# Verify EmailAddress object was created as unverified
email_address = EmailAddress.objects.get(user=self.user, email=new_email)
self.assertFalse(email_address.verified)
self.assertTrue(email_address.primary)  # or False depending on implementation

# Verify verification email was sent
self.assertEqual(len(mail.outbox), 1)
self.assertIn(new_email, mail.outbox[0].to)
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

Knowledge base: Disabled due to Reviews -> Disable Knowledge Base setting

📥 Commits

Reviewing files that changed from the base of the PR and between 3620da9 and d42f20e.

📒 Files selected for processing (1)
  • website/tests/test_user_profile.py (1 hunks)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (2)
  • GitHub Check: Run Tests
  • GitHub Check: docker-test

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 1

🧹 Nitpick comments (3)
website/views/user.py (3)

145-145: Consider prefixing unused variable with underscore.

The created variable from get_or_create() is never used. While this is a common pattern, prefixing it with an underscore makes the intent explicit.

Apply this diff:

-    user_profile, created = UserProfile.objects.get_or_create(user=request.user)
+    user_profile, _created = UserProfile.objects.get_or_create(user=request.user)

161-163: Consider checking if the user already has this email verified.

The uniqueness check on line 161 excludes the current user's EmailAddress entries, which means if the user already has new_email as a verified secondary email, this check won't catch it. They would then create a duplicate unverified entry for an email they've already verified.

Consider adding an additional check:

             if EmailAddress.objects.exclude(user=request.user).filter(email=new_email).exists():
                 form.add_error("email", "This email is already registered or pending verification")
                 return render(request, "profile_edit.html", {"form": form})
+            
+            # Check if user already has this email verified
+            if EmailAddress.objects.filter(user=request.user, email=new_email, verified=True).exists():
+                form.add_error("email", "You already have this email verified. Use email management to set it as primary.")
+                return render(request, "profile_edit.html", {"form": form})

168-168: Remove unnecessary quotation marks in comment.

The quotation marks around "not" are unnecessary and make the comment look odd.

Apply this diff:

-            # Save profile form (does "not" touch email in user model)
+            # Save profile form (does not touch email in user model)
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

Knowledge base: Disabled due to Reviews -> Disable Knowledge Base setting

📥 Commits

Reviewing files that changed from the base of the PR and between 4076d29 and 74a6049.

📒 Files selected for processing (1)
  • website/views/user.py (1 hunks)
🧰 Additional context used
🪛 Ruff (0.14.4)
website/views/user.py

145-145: Unpacked variable created is never used

Prefix it with an underscore or any other dummy variable pattern

(RUF059)


186-186: Do not catch blind exception: Exception

(BLE001)


187-187: Use logging.exception instead of logging.error

Replace with exception

(TRY400)

⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (2)
  • GitHub Check: Run Tests
  • GitHub Check: docker-test
🔇 Additional comments (3)
website/views/user.py (3)

141-142: LGTM! Local imports appropriately scoped.

The local imports of EmailAddress and send_email_confirmation are correctly placed within the function scope, which is a valid pattern for avoiding potential circular import issues or keeping allauth dependencies localized to this specific view.


171-199: Excellent implementation of email verification flow!

The email change handling correctly addresses the security issue described in the PR objectives:

  1. Preserves original email: User.email is not updated until verification completes
  2. Creates unverified entry: New email is added to EmailAddress table with verified=False
  3. Cleans up pending changes: Removes stale unverified emails before creating the new entry
  4. User feedback: Clear messaging differentiates between email changes and other profile updates
  5. Error resilience: Handles email sending failures gracefully

The separation of concerns between email changes (lines 171-195) and normal profile updates (lines 198-199) is well-structured.


205-208: LGTM! Form correctly prepopulated with current email.

The form initialization properly prepopulates the email field with request.user.email, ensuring users see their current email address when editing their profile.

@DonnieBLT
Copy link
Collaborator

looks good, please make sure to get a peer review and check all comments are resolved

@DonnieBLT DonnieBLT added the d15 label Nov 15, 2025
@DonnieBLT DonnieBLT added this to the Security milestone Nov 15, 2025
Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 1

🧹 Nitpick comments (2)
website/views/user.py (2)

184-189: Remove redundant exception object from logging message.

When using logger.exception(), the exception information is automatically captured and logged with the full traceback. Including {e} in the message is redundant.

Apply this diff:

             try:
                 send_email_confirmation(request, request.user, email=new_email)
             except Exception as e:
-                logger.exception(f"Failed to send email confirmation to {new_email}: {e}")
+                logger.exception(f"Failed to send email confirmation to {new_email}")
                 messages.error(request, "Failed to send verification email. Please try again later.")
                 return redirect("profile", slug=request.user.username)

Based on static analysis hints.


145-145: Consider using underscore prefix for unused variable.

The created variable is unpacked but never used. Consider prefixing with underscore to indicate it's intentionally unused.

Apply this diff:

-    user_profile, created = UserProfile.objects.get_or_create(user=request.user)
+    user_profile, _ = UserProfile.objects.get_or_create(user=request.user)

Based on static analysis hints.

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

Knowledge base: Disabled due to Reviews -> Disable Knowledge Base setting

📥 Commits

Reviewing files that changed from the base of the PR and between f50ee7e and 8b9379c.

📒 Files selected for processing (1)
  • website/views/user.py (1 hunks)
🧰 Additional context used
🪛 Ruff (0.14.4)
website/views/user.py

145-145: Unpacked variable created is never used

Prefix it with an underscore or any other dummy variable pattern

(RUF059)


187-187: Redundant exception object included in logging.exception call

(TRY401)

⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (2)
  • GitHub Check: Run Tests
  • GitHub Check: docker-test
🔇 Additional comments (5)
website/views/user.py (5)

141-142: LGTM! Local imports are appropriate.

The local imports of EmailAddress and send_email_confirmation help avoid circular dependencies and keep the dependencies clear for this specific flow.


147-148: LGTM! Capturing original email is correct.

Storing the original email before form processing enables accurate change detection later.


165-166: LGTM! Email change detection is correct.

The comparison correctly identifies whether the email has changed.


168-169: LGTM! Form save correctly preserves User.email.

The comment accurately reflects that User.email is not modified here, which is essential for the verification flow.


205-208: LGTM! Form initialization correctly prepopulates the email.

Using initial={"email": request.user.email} ensures the form displays the user's current email on the initial page load, providing a good user experience.

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 0

🧹 Nitpick comments (4)
website/views/user.py (4)

144-145: Prefix unused variable with underscore.

The created return value from get_or_create is unpacked but never used. Prefix it with an underscore to indicate it's intentionally unused.

Apply this diff:

 Tag.objects.get_or_create(name="GSOC")
-user_profile, created = UserProfile.objects.get_or_create(user=request.user)
+user_profile, _ = UserProfile.objects.get_or_create(user=request.user)

162-166: Clarify how to set email as primary.

The error message tells users to "set it as primary instead" but doesn't indicate where or how to do this. Consider either:

  1. Providing a link or instructions on how to set an email as primary
  2. Automatically setting it as primary if it's already verified
  3. Removing this suggestion if the functionality isn't available to users

Example improvement:

 if existing_email.verified:
-    form.add_error("email", "You already have this email verified. Please set it as primary instead.")
+    form.add_error("email", "This email is already verified for your account.")
     return render(request, "profile_edit.html", {"form": form})

192-197: Remove redundant exception object from log message.

When using logger.exception(), the exception and stack trace are automatically appended to the log output. Including the exception object in the message string is redundant.

Apply this diff:

 try:
     send_email_confirmation(request, request.user, email=new_email)
 except Exception as e:
-    logger.exception(f"Failed to send email confirmation to {new_email}: {e}")
+    logger.exception(f"Failed to send email confirmation to {new_email}")
     messages.error(request, "Failed to send verification email. Please try again later.")
     return redirect("profile", slug=request.user.username)

199-207: Consider consolidating the message string.

The success message on lines 200-201 uses string concatenation. For better readability, consider using a single string or parentheses for implicit concatenation.

Apply this diff:

-messages.info(
-    request,
-    "A verification link has been sent to your new email. " "Please verify to complete the update.",
-)
+messages.info(
+    request,
+    "A verification link has been sent to your new email. Please verify to complete the update.",
+)

Otherwise, the flow correctly handles both email-changed and email-unchanged cases with appropriate messages.

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

Knowledge base: Disabled due to Reviews -> Disable Knowledge Base setting

📥 Commits

Reviewing files that changed from the base of the PR and between 8b9379c and 30281c3.

📒 Files selected for processing (1)
  • website/views/user.py (1 hunks)
🧰 Additional context used
🪛 Ruff (0.14.4)
website/views/user.py

145-145: Unpacked variable created is never used

Prefix it with an underscore or any other dummy variable pattern

(RUF059)


195-195: Redundant exception object included in logging.exception call

(TRY401)

⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (2)
  • GitHub Check: Run Tests
  • GitHub Check: docker-test
🔇 Additional comments (5)
website/views/user.py (5)

141-142: LGTM: Local imports for email verification flow.

The scoped imports for EmailAddress and send_email_confirmation are appropriate and keep the dependencies explicit for this email verification feature.


147-148: LGTM: Original email captured correctly.

Capturing the user's current email before form processing is essential for detecting email changes. This is implemented correctly.


179-189: LGTM: Email change handling is correct.

The implementation correctly:

  1. Deletes pending unverified emails for the user
  2. Uses update_or_create with appropriate defaults to handle edge cases
  3. Sets the new email as unverified and non-primary

The use of update_or_create instead of create is appropriate as it gracefully handles race conditions and edge cases.


213-216: LGTM: Form correctly prepopulated with current email.

The form initialization properly prepopulates the email field with the user's current email address, providing a good user experience.


173-177: The email handling is secure and the comment is accurate.

The verification confirms that form.save() on line 177 does not touch User.email. The email field is declared in the form but excluded from Meta.fields, so it won't be saved by the ModelForm's save() method.

The email change is correctly handled separately:

  • New email is stored as unverified in the EmailAddress table (verified=False)
  • A verification email is sent via send_email_confirmation()
  • User.email is never directly updated with unverified addresses

The commented-out code in website/forms.py:60-68 shows the previous implementation that directly saved to User.email—this has been properly removed and replaced with the current secure flow.

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 0

🧹 Nitpick comments (2)
website/views/user.py (2)

145-145: Prefix unused variable with underscore.

The created variable is unpacked but never used. Follow Python conventions by prefixing it with an underscore to indicate it's intentionally unused.

Apply this diff:

-    user_profile, created = UserProfile.objects.get_or_create(user=request.user)
+    user_profile, _created = UserProfile.objects.get_or_create(user=request.user)

194-194: Remove redundant exception object from log message.

logging.exception() automatically includes the exception and stack trace, so including {e} in the formatted string is redundant.

Apply this diff:

-                    logger.exception(f"Failed to send email confirmation to {new_email}: {e}")
+                    logger.exception(f"Failed to send email confirmation to {new_email}")
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

Knowledge base: Disabled due to Reviews -> Disable Knowledge Base setting

📥 Commits

Reviewing files that changed from the base of the PR and between 30281c3 and 344e186.

📒 Files selected for processing (1)
  • website/views/user.py (1 hunks)
🧰 Additional context used
🪛 Ruff (0.14.4)
website/views/user.py

145-145: Unpacked variable created is never used

Prefix it with an underscore or any other dummy variable pattern

(RUF059)


194-194: Redundant exception object included in logging.exception call

(TRY401)

⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (2)
  • GitHub Check: Run Tests
  • GitHub Check: docker-test
🔇 Additional comments (2)
website/views/user.py (2)

156-202: Well-structured email verification flow.

The implementation demonstrates solid security practices:

  • Comprehensive validation: Three-tier uniqueness checks prevent conflicts with other users, existing verified emails, and pending verifications.
  • Proper verification flow: Uses django-allauth's EmailAddress model and send_email_confirmation following framework conventions.
  • Graceful error handling: Try-except block with logging and user-friendly error messages ensures failures don't leave the system in an inconsistent state.
  • Clear user communication: Info message explains what to expect after email change.

The conditional logic correctly separates email-change handling from normal profile updates, preserving existing behavior for other fields.


139-217: UserProfileForm correctly excludes email from model save—verification successful.

The email field is defined on UserProfileForm but is not included in Meta.fields (lines 26-44 of website/forms.py). This ensures that form.save() at line 176 will not modify User.email. The commented-out save() method (lines 60-68) confirms this was an intentional design decision—email auto-save to the User model was explicitly removed. The view correctly handles email updates separately through the EmailAddress model, preserving the verification flow.

Copy link
Contributor

@gojo-satorou-v7 gojo-satorou-v7 left a comment

Choose a reason for hiding this comment

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

Looks good to me. The fix looks thorough with older verification being removed on another new update.

It could've been more better if a rate limit was set, which is usually the case in such features on most websites.

Image

@Tia-ani
Copy link
Contributor Author

Tia-ani commented Nov 15, 2025

Thank you @DonnieBLT ! I've requested a peer review and confirmed that all comments from CodeRabbit are now resolved.

@Tia-ani
Copy link
Contributor Author

Tia-ani commented Nov 15, 2025

Thank you for the review @gojo-satorou-v7!
That's a great suggestion — I’ll look into adding a rate limit in a follow-up PR.

@DonnieBLT DonnieBLT removed this from the Security milestone Nov 15, 2025
@DonnieBLT DonnieBLT merged commit cc20d8b into OWASP-BLT:main Nov 16, 2025
16 checks passed
@github-project-automation github-project-automation bot moved this from Todo to Done in 🛡️ Security Nov 16, 2025
@DonnieBLT
Copy link
Collaborator

Thanks!

@github-actions github-actions bot added the unresolved-conversations: 0 PR has 0 unresolved conversations label Nov 16, 2025
@Tia-ani Tia-ani deleted the fix/email-verification-on-profile-edit branch November 16, 2025 06:30
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

unresolved-conversations: 0 PR has 0 unresolved conversations

Projects

Status: Done
Status: Done

Development

Successfully merging this pull request may close these issues.

Email verification is not enforced when changing via edit profile.

3 participants