Skip to content

feat: improve error handling and lifecycle management for tabCapture - #669

Open
vansh2604-star wants to merge 1 commit into
Kuldeeep18:mainfrom
vansh2604-star:Security-Hardening--Authentication-&-Application-Safeguards
Open

feat: improve error handling and lifecycle management for tabCapture#669
vansh2604-star wants to merge 1 commit into
Kuldeeep18:mainfrom
vansh2604-star:Security-Hardening--Authentication-&-Application-Safeguards

Conversation

@vansh2604-star

@vansh2604-star vansh2604-star commented Jul 12, 2026

Copy link
Copy Markdown

Pull Request

🔗 Related Issue

Closes #643


📝 Summary of Changes

This PR introduces critical security hardening and safeguards to the backend:

  • Authentication Enhancements:

    • Implemented strong password policy validation enforcing uppercase, lowercase, numbers, and special characters with a minimum length of 10.
    • Added support for TOTP multi-factor authentication (MFA/2FA) during login with endpoints for enabling, confirming, and verifying MFA challenge tokens.
    • Implemented brute-force mitigation using a custom LoginRateThrottle class.
    • Configured secure JWT handling with signature rotation and token revocation/blacklisting.
    • Enforced a blocker for unverified emails or inactive accounts attempting authentication.
  • Application Safeguards:

    • Restricted CORS to trusted origins, disabling CORS_ALLOW_ALL_ORIGINS.
    • Secured session and CSRF cookies with HttpOnly, Secure, and SameSite attributes.
    • Integrated strict Content-Security-Policy (CSP) headers into the security headers middleware.
    • Moved the /admin/ panel to a configurable path via the ADMIN_PATH setting.
  • User Verification:

    • Created email verification flow sending tokens to registered users and added /verify-email/ view endpoint.

🏷️ Type of Change

  • ✨ New feature
  • ♻️ Refactor
  • 🔧 Chore

🧪 Testing

The changes were verified using Django's test framework. Comprehensive test suites were added to users/tests.py to validate:

  1. Password complexity requirements.
  2. Email verification token generation, login restrictions, and activation.
  3. MFA setup (enable/confirm) and validation flow.
  4. Throttling and rate limit middleware.

Steps to test:

  1. Navigate to backend directory: cd backend
  2. Run test suite: python manage.py test
  3. All 95 tests pass successfully.

✅ Checklist

  • No merge conflicts
  • Changes follow the project guidelines
  • Documentation updated (if applicable)
  • Related issue linked
  • Changes tested locally (if applicable)

Summary by CodeRabbit

  • New Features
    • Added email verification during registration before account login.
    • Added optional multi-factor authentication setup and verification using authenticator codes.
    • Added configurable administration access path.
  • Security Enhancements
    • Strengthened password requirements and login protection against repeated attempts.
    • Improved session, token, CORS, and browser security protections.
    • Added token invalidation support.
  • Bug Fixes
    • Prevented unverified accounts from signing in.
    • Added clearer validation for passwords that do not meet complexity requirements.

@coderabbitai

coderabbitai Bot commented Jul 12, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: c733a666-e063-4be6-a166-b1bcaf58de29

📥 Commits

Reviewing files that changed from the base of the PR and between 4a33158 and 7d50148.

📒 Files selected for processing (13)
  • backend/backend/settings.py
  • backend/backend/urls.py
  • backend/db.sqlite3
  • backend/tenants/security.py
  • backend/users/jwt.py
  • backend/users/migrations/0003_user_is_email_verified_user_mfa_enabled_and_more.py
  • backend/users/models.py
  • backend/users/serializers.py
  • backend/users/tests.py
  • backend/users/throttling.py
  • backend/users/validators.py
  • backend/users/views.py
  • requirements.txt

📝 Walkthrough

Walkthrough

Adds email verification, password complexity checks, MFA enrollment and login verification, login throttling, JWT revocation support, restricted CORS, secure cookies, CSP headers, and configurable admin routing.

Changes

Authentication Security

Layer / File(s) Summary
Runtime security controls
backend/backend/settings.py, backend/backend/urls.py, backend/tenants/security.py, backend/users/throttling.py
Adds secure cookie settings, restricted CORS, configurable JWT signing, token blacklisting, throttling, CSP headers, and a configurable admin path.
Password and email verification
backend/users/models.py, backend/users/migrations/..., backend/users/validators.py, backend/users/serializers.py, backend/users/views.py, backend/users/tests.py
Adds password complexity validation, email verification state, verification emails, verification endpoints, and integration tests.
MFA authentication flow
backend/users/jwt.py, backend/users/views.py, backend/users/tests.py, requirements.txt
Adds TOTP enrollment and confirmation, MFA challenge tokens, MFA verification, final JWT issuance, tests, and the pyotp dependency.

Estimated code review effort: 4 (Complex) | ~45 minutes

Sequence Diagram(s)

sequenceDiagram
  participant Client
  participant AuthViewSet
  participant CustomTokenObtainSerializer
  participant User
  Client->>AuthViewSet: register with email and password
  AuthViewSet->>User: create unverified account
  AuthViewSet-->>Client: send verification message
  Client->>AuthViewSet: verify email token
  AuthViewSet->>User: mark email as verified
  Client->>CustomTokenObtainSerializer: submit login credentials
  CustomTokenObtainSerializer->>User: check verification and MFA state
  CustomTokenObtainSerializer-->>Client: return MFA challenge or JWT tokens
  Client->>AuthViewSet: submit MFA token and TOTP code
  AuthViewSet->>User: validate MFA secret
  AuthViewSet-->>Client: return refresh and access tokens
Loading
🚥 Pre-merge checks | ✅ 3 | ❌ 2

❌ Failed checks (2 warnings)

Check name Status Explanation Resolution
Title check ⚠️ Warning The title refers to tabCapture error handling, but the PR is about backend security hardening and authentication safeguards. Rename the PR to reflect the main backend security changes, such as authentication hardening and account protection.
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (3 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Linked Issues check ✅ Passed The changes cover the linked security hardening goals: password policy, MFA, throttling, JWT revocation, CORS, cookies, CSP, admin path, and email verification.
Out of Scope Changes check ✅ Passed No clearly unrelated code changes are shown; the diff stays focused on the security and authentication objectives from issue #643.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

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.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Actionable comments posted: 10

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
backend/backend/settings.py (1)

195-201: 🔒 Security & Privacy | 🟠 Major | ⚡ Quick win

JWT signing should not fall back to the hardcoded default, and refresh-token revocation is still disabled backend/backend/settings.py currently signs JWTs with SECRET_KEY when JWT_SIGNING_KEY is unset, and SECRET_KEY itself defaults to a source-controlled fallback. Also, rest_framework_simplejwt.token_blacklist is installed but rotation/blacklisting isn’t enabled, so refresh tokens won’t be revoked automatically.

🤖 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/backend/settings.py` around lines 195 - 201, Update the SIMPLE_JWT
configuration so SIGNING_KEY requires JWT_SIGNING_KEY and never falls back to
SECRET_KEY or its source-controlled default. Enable refresh-token rotation and
blacklist-after-rotation in SIMPLE_JWT, reusing the existing
rest_framework_simplejwt.token_blacklist installation so rotated refresh tokens
are revoked automatically.
🧹 Nitpick comments (6)
requirements.txt (1)

17-17: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Consider bounding the pyotp version.

pyotp>=2.9.0 has no upper bound, unlike the neighboring gunicorn>=20.1,<21.0 pin. This package backs the MFA/TOTP flow, so an unexpected future major bump pulled in on a fresh install could silently change auth behavior. PyOTP's own docs recommend it: it is recommended that application developers pin the package version and manage it using pip-tools or similar.

-pyotp>=2.9.0
+pyotp>=2.9.0,<3.0
🤖 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 `@requirements.txt` at line 17, Update the pyotp dependency specification in
requirements.txt to add an appropriate upper version bound, following the
neighboring gunicorn constraint style and preserving the existing minimum
version.
backend/users/views.py (2)

51-70: 🔒 Security & Privacy | 🔵 Trivial | ⚡ Quick win

Reusing default_token_generator (password-reset generator) for email verification.

default_token_generator hashes user.pk + user.password + last_login + timestamp under a fixed key_salt tied to password resets. It works today since there's no separate password-reset flow, but it means a token isn't cryptographically scoped to "email verification" specifically. Consider subclassing PasswordResetTokenGenerator with a distinct hash/salt (a common Django pattern for account-activation tokens) to avoid any future cross-purpose token confusion if a password-reset feature is added later.

🤖 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/users/views.py` around lines 51 - 70, Update verify_email to validate
tokens with a dedicated email-verification PasswordResetTokenGenerator subclass
rather than the shared default_token_generator. Define a distinct key_salt for
that generator and use the same dedicated generator when creating verification
tokens, preserving the existing verification response behavior.

33-34: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Silent localhost fallback for FRONTEND_BASE_URL in production.

If the env var is unset in a deployed environment, verification emails link to http://localhost:3000, silently breaking the entire verification flow instead of failing loudly. This directly relates to the PR's goal of improving env-var validation/defaults.

🤖 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/users/views.py` around lines 33 - 34, The verification-link
construction in the relevant user email flow should not silently default
FRONTEND_BASE_URL to localhost. Require a configured FRONTEND_BASE_URL in
deployed environments and fail loudly when it is missing, while preserving an
explicit development fallback only if the existing environment configuration
supports it.
backend/users/validators.py (1)

6-26: 🎯 Functional Correctness | 🔵 Trivial | ⚡ Quick win

Only the first failed rule is reported; aggregate all violations instead.

If a password fails multiple checks (e.g. no uppercase and no digit), the caller only ever sees the first error, forcing repeated submit-and-fix cycles. Django's ValidationError supports a list of errors — collect and raise them together.

♻️ Proposed refactor
     def validate(self, password, user=None):
-        if not re.search(r'[A-Z]', password):
-            raise ValidationError(
-                _("The password must contain at least one uppercase letter."),
-                code='password_no_upper',
-            )
-        if not re.search(r'[a-z]', password):
-            raise ValidationError(
-                _("The password must contain at least one lowercase letter."),
-                code='password_no_lower',
-            )
-        if not re.search(r'\d', password):
-            raise ValidationError(
-                _("The password must contain at least one digit."),
-                code='password_no_digit',
-            )
-        if not re.search(r'[!@#$%^&*(),.?":{}|<>\-_=+\[\];:\']', password):
-            raise ValidationError(
-                _("The password must contain at least one special character."),
-                code='password_no_special',
-            )
+        errors = []
+        if not re.search(r'[A-Z]', password):
+            errors.append(ValidationError(
+                _("The password must contain at least one uppercase letter."),
+                code='password_no_upper',
+            ))
+        if not re.search(r'[a-z]', password):
+            errors.append(ValidationError(
+                _("The password must contain at least one lowercase letter."),
+                code='password_no_lower',
+            ))
+        if not re.search(r'\d', password):
+            errors.append(ValidationError(
+                _("The password must contain at least one digit."),
+                code='password_no_digit',
+            ))
+        if not re.search(r'[!@#$%^&*(),.?":{}|<>\-_=+\[\];:\']', password):
+            errors.append(ValidationError(
+                _("The password must contain at least one special character."),
+                code='password_no_special',
+            ))
+        if errors:
+            raise ValidationError(errors)
🤖 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/users/validators.py` around lines 6 - 26, Update validate to collect
every failed password rule in a list rather than raising immediately for each
check, then raise one Django ValidationError containing all collected errors
after all checks run. Preserve each existing message and error code, and leave
valid passwords without raising.
backend/users/serializers.py (2)

28-35: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Chain the original exception (raise ... from exc).

Ruff (B904) flags this: re-raising inside an except block without from exc/from None discards the original traceback context, making production debugging of password-validation failures harder.

🔧 Proposed fix
-        except DjangoValidationError as exc:
-            raise serializers.ValidationError({'password': list(exc.messages)})
+        except DjangoValidationError as exc:
+            raise serializers.ValidationError({'password': list(exc.messages)}) from exc
🤖 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/users/serializers.py` around lines 28 - 35, Update the exception
handling in the serializer’s validate method to chain the DjangoValidationError
when raising serializers.ValidationError, using the caught exc as the explicit
cause while preserving the existing password error payload.

Source: Linters/SAST tools


31-33: 🔒 Security & Privacy | 🔵 Trivial | ⚡ Quick win

UserAttributeSimilarityValidator only sees email; first_name/last_name are silently skipped.

attrs already contains first_name/last_name, but the temporary User(email=...) passed to validate_password doesn't set them, so UserAttributeSimilarityValidator can't reject passwords similar to the user's actual name — weakening the complexity requirement this PR is adding.

🔧 Proposed fix
-        user = User(email=attrs.get('email'))
+        user = User(
+            email=attrs.get('email'),
+            first_name=attrs.get('first_name', ''),
+            last_name=attrs.get('last_name', ''),
+        )
🤖 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/users/serializers.py` around lines 31 - 33, Update the temporary User
instance in the password-validation flow to populate first_name and last_name
from attrs alongside email before calling validate_password. Keep the existing
validation and error handling unchanged so UserAttributeSimilarityValidator
evaluates all available user attributes.
🤖 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 `@backend/db.sqlite3`:
- Line 272: Remove the tracked backend/db.sqlite3 database from source control
and purge it from repository history. Revoke and rotate all exposed OAuth
tokens, password hashes, SMTP credentials, and IMAP credentials, then replace
the committed snapshot workflow with migrations and sanitized fixtures that
contain no real PII or credential-bearing values.

In `@backend/tenants/security.py`:
- Line 61: Update the CSP assignment in the security middleware to remove
script-src 'unsafe-inline'; use nonces or hashes for any required inline
scripts, or move them to external files while preserving legitimate script
execution. Also add object-src 'none' and base-uri 'self' to the policy.

In `@backend/users/jwt.py`:
- Around line 18-33: Update the MFA token flow in the JWT token-generation
method and the corresponding decode logic in users/views.py to use the
configured JWT signing key, such as JWT_SIGNING_KEY, instead of
settings.SECRET_KEY. Ensure both encoding and decoding use the same key
consistently, or introduce and apply a dedicated MFA key in both paths.

In
`@backend/users/migrations/0003_user_is_email_verified_user_mfa_enabled_and_more.py`:
- Around line 13-17: Update the migration containing the is_email_verified
AddField to backfill existing users as verified before enforcing the new JWT
behavior. Add a data-migration step targeting the User model that sets
is_email_verified=True for all pre-existing users, while retaining the field’s
default=False for newly created users.

In `@backend/users/models.py`:
- Around line 36-38: The mfa_secret field in the user model stores TOTP secrets
in plaintext; replace it with the project’s established encrypted-field
mechanism or add Fernet encryption/decryption around persistence while
preserving the existing plaintext value returned to verify_mfa and enable_mfa
for pyotp.TOTP. Include the required key/configuration and migration so existing
data and field access remain compatible.

In `@backend/users/throttling.py`:
- Around line 13-14: Update the request-data handling around the visible request
throttling logic to catch DRF ParseError when evaluating request.data; treat
malformed input as unavailable data so the throttle flow continues and returns
its normal throttled 429 response instead of propagating the exception.
- Around line 6-24: Update get_cache_key so the existing throttle always uses
the client identifier (typically IP) without appending request.data email,
ensuring all login attempts from one IP share a bucket. If per-account
protection is required, add a separate LoginPerEmailThrottle with its own scope
and register both throttles in throttle_classes.

In `@backend/users/views.py`:
- Around line 72-88: Protect the TOTP secret throughout the MFA flow by
encrypting it before assigning `user.mfa_secret` in `enable_mfa`, then
decrypting it only when constructing `pyotp.TOTP` in `confirm_mfa` and
`verify_mfa`. Use a dedicated Fernet key from configuration and a shared
encryption/decryption helper, while preserving the existing provisioning URI and
verification behavior.
- Around line 26-43: Update the registration flow around the persisted user and
send_mail call so email-delivery failures cannot turn a successful account
creation into an unhandled 500. Add appropriate error handling around send_mail,
preserving the saved user response while recording the failure through the
project’s established logging or notification mechanism; do not change token or
verification-link generation.
- Around line 58-64: Update the exception tuple in the uid decoding and
User.objects.get lookup to also catch DjangoValidationError, ensuring malformed
UUID primary keys set user to None instead of propagating an error.

---

Outside diff comments:
In `@backend/backend/settings.py`:
- Around line 195-201: Update the SIMPLE_JWT configuration so SIGNING_KEY
requires JWT_SIGNING_KEY and never falls back to SECRET_KEY or its
source-controlled default. Enable refresh-token rotation and
blacklist-after-rotation in SIMPLE_JWT, reusing the existing
rest_framework_simplejwt.token_blacklist installation so rotated refresh tokens
are revoked automatically.

---

Nitpick comments:
In `@backend/users/serializers.py`:
- Around line 28-35: Update the exception handling in the serializer’s validate
method to chain the DjangoValidationError when raising
serializers.ValidationError, using the caught exc as the explicit cause while
preserving the existing password error payload.
- Around line 31-33: Update the temporary User instance in the
password-validation flow to populate first_name and last_name from attrs
alongside email before calling validate_password. Keep the existing validation
and error handling unchanged so UserAttributeSimilarityValidator evaluates all
available user attributes.

In `@backend/users/validators.py`:
- Around line 6-26: Update validate to collect every failed password rule in a
list rather than raising immediately for each check, then raise one Django
ValidationError containing all collected errors after all checks run. Preserve
each existing message and error code, and leave valid passwords without raising.

In `@backend/users/views.py`:
- Around line 51-70: Update verify_email to validate tokens with a dedicated
email-verification PasswordResetTokenGenerator subclass rather than the shared
default_token_generator. Define a distinct key_salt for that generator and use
the same dedicated generator when creating verification tokens, preserving the
existing verification response behavior.
- Around line 33-34: The verification-link construction in the relevant user
email flow should not silently default FRONTEND_BASE_URL to localhost. Require a
configured FRONTEND_BASE_URL in deployed environments and fail loudly when it is
missing, while preserving an explicit development fallback only if the existing
environment configuration supports it.

In `@requirements.txt`:
- Line 17: Update the pyotp dependency specification in requirements.txt to add
an appropriate upper version bound, following the neighboring gunicorn
constraint style and preserving the existing minimum version.
🪄 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: c733a666-e063-4be6-a166-b1bcaf58de29

📥 Commits

Reviewing files that changed from the base of the PR and between 4a33158 and 7d50148.

📒 Files selected for processing (13)
  • backend/backend/settings.py
  • backend/backend/urls.py
  • backend/db.sqlite3
  • backend/tenants/security.py
  • backend/users/jwt.py
  • backend/users/migrations/0003_user_is_email_verified_user_mfa_enabled_and_more.py
  • backend/users/models.py
  • backend/users/serializers.py
  • backend/users/tests.py
  • backend/users/throttling.py
  • backend/users/validators.py
  • backend/users/views.py
  • requirements.txt

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Caution

Inline review comments failed to post. This is likely due to GitHub's internal server error or limits when posting large numbers of comments. If you are seeing this consistently it is likely a permissions issue. Please check "Moderation" -> "Code review limits" under your organization settings.

Actionable comments posted: 10

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
backend/backend/settings.py (1)

195-201: 🔒 Security & Privacy | 🟠 Major | ⚡ Quick win

JWT signing should not fall back to the hardcoded default, and refresh-token revocation is still disabled backend/backend/settings.py currently signs JWTs with SECRET_KEY when JWT_SIGNING_KEY is unset, and SECRET_KEY itself defaults to a source-controlled fallback. Also, rest_framework_simplejwt.token_blacklist is installed but rotation/blacklisting isn’t enabled, so refresh tokens won’t be revoked automatically.

🤖 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/backend/settings.py` around lines 195 - 201, Update the SIMPLE_JWT
configuration so SIGNING_KEY requires JWT_SIGNING_KEY and never falls back to
SECRET_KEY or its source-controlled default. Enable refresh-token rotation and
blacklist-after-rotation in SIMPLE_JWT, reusing the existing
rest_framework_simplejwt.token_blacklist installation so rotated refresh tokens
are revoked automatically.
🧹 Nitpick comments (6)
requirements.txt (1)

17-17: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Consider bounding the pyotp version.

pyotp>=2.9.0 has no upper bound, unlike the neighboring gunicorn>=20.1,<21.0 pin. This package backs the MFA/TOTP flow, so an unexpected future major bump pulled in on a fresh install could silently change auth behavior. PyOTP's own docs recommend it: it is recommended that application developers pin the package version and manage it using pip-tools or similar.

-pyotp>=2.9.0
+pyotp>=2.9.0,<3.0
🤖 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 `@requirements.txt` at line 17, Update the pyotp dependency specification in
requirements.txt to add an appropriate upper version bound, following the
neighboring gunicorn constraint style and preserving the existing minimum
version.
backend/users/views.py (2)

51-70: 🔒 Security & Privacy | 🔵 Trivial | ⚡ Quick win

Reusing default_token_generator (password-reset generator) for email verification.

default_token_generator hashes user.pk + user.password + last_login + timestamp under a fixed key_salt tied to password resets. It works today since there's no separate password-reset flow, but it means a token isn't cryptographically scoped to "email verification" specifically. Consider subclassing PasswordResetTokenGenerator with a distinct hash/salt (a common Django pattern for account-activation tokens) to avoid any future cross-purpose token confusion if a password-reset feature is added later.

🤖 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/users/views.py` around lines 51 - 70, Update verify_email to validate
tokens with a dedicated email-verification PasswordResetTokenGenerator subclass
rather than the shared default_token_generator. Define a distinct key_salt for
that generator and use the same dedicated generator when creating verification
tokens, preserving the existing verification response behavior.

33-34: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Silent localhost fallback for FRONTEND_BASE_URL in production.

If the env var is unset in a deployed environment, verification emails link to http://localhost:3000, silently breaking the entire verification flow instead of failing loudly. This directly relates to the PR's goal of improving env-var validation/defaults.

🤖 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/users/views.py` around lines 33 - 34, The verification-link
construction in the relevant user email flow should not silently default
FRONTEND_BASE_URL to localhost. Require a configured FRONTEND_BASE_URL in
deployed environments and fail loudly when it is missing, while preserving an
explicit development fallback only if the existing environment configuration
supports it.
backend/users/validators.py (1)

6-26: 🎯 Functional Correctness | 🔵 Trivial | ⚡ Quick win

Only the first failed rule is reported; aggregate all violations instead.

If a password fails multiple checks (e.g. no uppercase and no digit), the caller only ever sees the first error, forcing repeated submit-and-fix cycles. Django's ValidationError supports a list of errors — collect and raise them together.

♻️ Proposed refactor
     def validate(self, password, user=None):
-        if not re.search(r'[A-Z]', password):
-            raise ValidationError(
-                _("The password must contain at least one uppercase letter."),
-                code='password_no_upper',
-            )
-        if not re.search(r'[a-z]', password):
-            raise ValidationError(
-                _("The password must contain at least one lowercase letter."),
-                code='password_no_lower',
-            )
-        if not re.search(r'\d', password):
-            raise ValidationError(
-                _("The password must contain at least one digit."),
-                code='password_no_digit',
-            )
-        if not re.search(r'[!@#$%^&*(),.?":{}|<>\-_=+\[\];:\']', password):
-            raise ValidationError(
-                _("The password must contain at least one special character."),
-                code='password_no_special',
-            )
+        errors = []
+        if not re.search(r'[A-Z]', password):
+            errors.append(ValidationError(
+                _("The password must contain at least one uppercase letter."),
+                code='password_no_upper',
+            ))
+        if not re.search(r'[a-z]', password):
+            errors.append(ValidationError(
+                _("The password must contain at least one lowercase letter."),
+                code='password_no_lower',
+            ))
+        if not re.search(r'\d', password):
+            errors.append(ValidationError(
+                _("The password must contain at least one digit."),
+                code='password_no_digit',
+            ))
+        if not re.search(r'[!@#$%^&*(),.?":{}|<>\-_=+\[\];:\']', password):
+            errors.append(ValidationError(
+                _("The password must contain at least one special character."),
+                code='password_no_special',
+            ))
+        if errors:
+            raise ValidationError(errors)
🤖 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/users/validators.py` around lines 6 - 26, Update validate to collect
every failed password rule in a list rather than raising immediately for each
check, then raise one Django ValidationError containing all collected errors
after all checks run. Preserve each existing message and error code, and leave
valid passwords without raising.
backend/users/serializers.py (2)

28-35: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Chain the original exception (raise ... from exc).

Ruff (B904) flags this: re-raising inside an except block without from exc/from None discards the original traceback context, making production debugging of password-validation failures harder.

🔧 Proposed fix
-        except DjangoValidationError as exc:
-            raise serializers.ValidationError({'password': list(exc.messages)})
+        except DjangoValidationError as exc:
+            raise serializers.ValidationError({'password': list(exc.messages)}) from exc
🤖 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/users/serializers.py` around lines 28 - 35, Update the exception
handling in the serializer’s validate method to chain the DjangoValidationError
when raising serializers.ValidationError, using the caught exc as the explicit
cause while preserving the existing password error payload.

Source: Linters/SAST tools


31-33: 🔒 Security & Privacy | 🔵 Trivial | ⚡ Quick win

UserAttributeSimilarityValidator only sees email; first_name/last_name are silently skipped.

attrs already contains first_name/last_name, but the temporary User(email=...) passed to validate_password doesn't set them, so UserAttributeSimilarityValidator can't reject passwords similar to the user's actual name — weakening the complexity requirement this PR is adding.

🔧 Proposed fix
-        user = User(email=attrs.get('email'))
+        user = User(
+            email=attrs.get('email'),
+            first_name=attrs.get('first_name', ''),
+            last_name=attrs.get('last_name', ''),
+        )
🤖 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/users/serializers.py` around lines 31 - 33, Update the temporary User
instance in the password-validation flow to populate first_name and last_name
from attrs alongside email before calling validate_password. Keep the existing
validation and error handling unchanged so UserAttributeSimilarityValidator
evaluates all available user attributes.
🤖 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 `@backend/db.sqlite3`:
- Line 272: Remove the tracked backend/db.sqlite3 database from source control
and purge it from repository history. Revoke and rotate all exposed OAuth
tokens, password hashes, SMTP credentials, and IMAP credentials, then replace
the committed snapshot workflow with migrations and sanitized fixtures that
contain no real PII or credential-bearing values.

In `@backend/tenants/security.py`:
- Line 61: Update the CSP assignment in the security middleware to remove
script-src 'unsafe-inline'; use nonces or hashes for any required inline
scripts, or move them to external files while preserving legitimate script
execution. Also add object-src 'none' and base-uri 'self' to the policy.

In `@backend/users/jwt.py`:
- Around line 18-33: Update the MFA token flow in the JWT token-generation
method and the corresponding decode logic in users/views.py to use the
configured JWT signing key, such as JWT_SIGNING_KEY, instead of
settings.SECRET_KEY. Ensure both encoding and decoding use the same key
consistently, or introduce and apply a dedicated MFA key in both paths.

In
`@backend/users/migrations/0003_user_is_email_verified_user_mfa_enabled_and_more.py`:
- Around line 13-17: Update the migration containing the is_email_verified
AddField to backfill existing users as verified before enforcing the new JWT
behavior. Add a data-migration step targeting the User model that sets
is_email_verified=True for all pre-existing users, while retaining the field’s
default=False for newly created users.

In `@backend/users/models.py`:
- Around line 36-38: The mfa_secret field in the user model stores TOTP secrets
in plaintext; replace it with the project’s established encrypted-field
mechanism or add Fernet encryption/decryption around persistence while
preserving the existing plaintext value returned to verify_mfa and enable_mfa
for pyotp.TOTP. Include the required key/configuration and migration so existing
data and field access remain compatible.

In `@backend/users/throttling.py`:
- Around line 13-14: Update the request-data handling around the visible request
throttling logic to catch DRF ParseError when evaluating request.data; treat
malformed input as unavailable data so the throttle flow continues and returns
its normal throttled 429 response instead of propagating the exception.
- Around line 6-24: Update get_cache_key so the existing throttle always uses
the client identifier (typically IP) without appending request.data email,
ensuring all login attempts from one IP share a bucket. If per-account
protection is required, add a separate LoginPerEmailThrottle with its own scope
and register both throttles in throttle_classes.

In `@backend/users/views.py`:
- Around line 72-88: Protect the TOTP secret throughout the MFA flow by
encrypting it before assigning `user.mfa_secret` in `enable_mfa`, then
decrypting it only when constructing `pyotp.TOTP` in `confirm_mfa` and
`verify_mfa`. Use a dedicated Fernet key from configuration and a shared
encryption/decryption helper, while preserving the existing provisioning URI and
verification behavior.
- Around line 26-43: Update the registration flow around the persisted user and
send_mail call so email-delivery failures cannot turn a successful account
creation into an unhandled 500. Add appropriate error handling around send_mail,
preserving the saved user response while recording the failure through the
project’s established logging or notification mechanism; do not change token or
verification-link generation.
- Around line 58-64: Update the exception tuple in the uid decoding and
User.objects.get lookup to also catch DjangoValidationError, ensuring malformed
UUID primary keys set user to None instead of propagating an error.

---

Outside diff comments:
In `@backend/backend/settings.py`:
- Around line 195-201: Update the SIMPLE_JWT configuration so SIGNING_KEY
requires JWT_SIGNING_KEY and never falls back to SECRET_KEY or its
source-controlled default. Enable refresh-token rotation and
blacklist-after-rotation in SIMPLE_JWT, reusing the existing
rest_framework_simplejwt.token_blacklist installation so rotated refresh tokens
are revoked automatically.

---

Nitpick comments:
In `@backend/users/serializers.py`:
- Around line 28-35: Update the exception handling in the serializer’s validate
method to chain the DjangoValidationError when raising
serializers.ValidationError, using the caught exc as the explicit cause while
preserving the existing password error payload.
- Around line 31-33: Update the temporary User instance in the
password-validation flow to populate first_name and last_name from attrs
alongside email before calling validate_password. Keep the existing validation
and error handling unchanged so UserAttributeSimilarityValidator evaluates all
available user attributes.

In `@backend/users/validators.py`:
- Around line 6-26: Update validate to collect every failed password rule in a
list rather than raising immediately for each check, then raise one Django
ValidationError containing all collected errors after all checks run. Preserve
each existing message and error code, and leave valid passwords without raising.

In `@backend/users/views.py`:
- Around line 51-70: Update verify_email to validate tokens with a dedicated
email-verification PasswordResetTokenGenerator subclass rather than the shared
default_token_generator. Define a distinct key_salt for that generator and use
the same dedicated generator when creating verification tokens, preserving the
existing verification response behavior.
- Around line 33-34: The verification-link construction in the relevant user
email flow should not silently default FRONTEND_BASE_URL to localhost. Require a
configured FRONTEND_BASE_URL in deployed environments and fail loudly when it is
missing, while preserving an explicit development fallback only if the existing
environment configuration supports it.

In `@requirements.txt`:
- Line 17: Update the pyotp dependency specification in requirements.txt to add
an appropriate upper version bound, following the neighboring gunicorn
constraint style and preserving the existing minimum version.
🪄 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: c733a666-e063-4be6-a166-b1bcaf58de29

📥 Commits

Reviewing files that changed from the base of the PR and between 4a33158 and 7d50148.

📒 Files selected for processing (13)
  • backend/backend/settings.py
  • backend/backend/urls.py
  • backend/db.sqlite3
  • backend/tenants/security.py
  • backend/users/jwt.py
  • backend/users/migrations/0003_user_is_email_verified_user_mfa_enabled_and_more.py
  • backend/users/models.py
  • backend/users/serializers.py
  • backend/users/tests.py
  • backend/users/throttling.py
  • backend/users/validators.py
  • backend/users/views.py
  • requirements.txt
🛑 Comments failed to post (10)
backend/db.sqlite3 (1)

272-272: 🔒 Security & Privacy | 🔴 Critical | 🏗️ Heavy lift

Remove this database from source control and rotate exposed credentials.

This snapshot contains OAuth bearer tokens (Line [272]), password hashes and user PII (Line [522]), plus credential-bearing fields such as access_token, refresh_token, smtp_password, and imap_password (Line [480]). Even if these are test accounts, any still-valid tokens can enable account access or unauthorized mail activity.

Remove backend/db.sqlite3 from the repository and its history, revoke/rotate all exposed OAuth and mail credentials, and use migrations with sanitized fixtures instead.

Also applies to: 480-480, 522-522

🤖 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/db.sqlite3` at line 272, Remove the tracked backend/db.sqlite3
database from source control and purge it from repository history. Revoke and
rotate all exposed OAuth tokens, password hashes, SMTP credentials, and IMAP
credentials, then replace the committed snapshot workflow with migrations and
sanitized fixtures that contain no real PII or credential-bearing values.
backend/tenants/security.py (1)

61-61: 🔒 Security & Privacy | 🟠 Major | ⚡ Quick win

script-src 'unsafe-inline' significantly weakens CSP's XSS protection.

The PR's goal is security hardening, but unsafe-inline in script-src effectively disables CSP's primary defense against injected inline scripts. While style-src 'unsafe-inline' is often necessary for Django's admin and form media, script-src should avoid it. Consider using nonces or hashes for any legitimate inline scripts instead.

Also consider adding object-src 'none' and base-uri 'self' to further harden the policy.

🔒 Proposed CSP hardening
-        response['Content-Security-Policy'] = "default-src 'self'; script-src 'self' 'unsafe-inline'; style-src 'self' 'unsafe-inline'; img-src 'self' data:; frame-ancestors 'none';"
+        response['Content-Security-Policy'] = "default-src 'self'; script-src 'self'; style-src 'self' 'unsafe-inline'; img-src 'self' data:; object-src 'none'; base-uri 'self'; frame-ancestors 'none';"

If inline scripts are currently required, migrate to CSP nonces or move scripts to external files. Django's django-csp package can help manage nonces.

📝 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.

        response['Content-Security-Policy'] = "default-src 'self'; script-src 'self'; style-src 'self' 'unsafe-inline'; img-src 'self' data:; object-src 'none'; base-uri 'self'; frame-ancestors 'none';"
🤖 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/tenants/security.py` at line 61, Update the CSP assignment in the
security middleware to remove script-src 'unsafe-inline'; use nonces or hashes
for any required inline scripts, or move them to external files while preserving
legitimate script execution. Also add object-src 'none' and base-uri 'self' to
the policy.
backend/users/jwt.py (1)

18-33: 🔒 Security & Privacy | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
rg -n "SIMPLE_JWT|SIGNING_KEY|SECRET_KEY" backend/backend/settings.py -A3 -B1

Repository: Kuldeeep18/LeadOrbit

Length of output: 742


🏁 Script executed:

#!/bin/bash
sed -n '1,120p' backend/users/jwt.py

Repository: Kuldeeep18/LeadOrbit

Length of output: 1372


🏁 Script executed:

#!/bin/bash
rg -n "mfa_token|mfa_pending|mfa_required" backend

Repository: Kuldeeep18/LeadOrbit

Length of output: 1208


Use the configured JWT signing key for MFA tokens

backend/users/jwt.py hard-codes settings.SECRET_KEY, while backend/backend/settings.py already exposes SIMPLE_JWT['SIGNING_KEY'] via JWT_SIGNING_KEY. backend/users/views.py also decodes the MFA token with settings.SECRET_KEY, so this flow bypasses the configurable JWT secret entirely. Use a dedicated MFA key, or the shared JWT signing key, in both encode and decode paths.

🤖 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/users/jwt.py` around lines 18 - 33, Update the MFA token flow in the
JWT token-generation method and the corresponding decode logic in users/views.py
to use the configured JWT signing key, such as JWT_SIGNING_KEY, instead of
settings.SECRET_KEY. Ensure both encoding and decoding use the same key
consistently, or introduce and apply a dedicated MFA key in both paths.
backend/users/migrations/0003_user_is_email_verified_user_mfa_enabled_and_more.py (1)

13-17: 🗄️ Data Integrity & Integration | 🟠 Major | 🏗️ Heavy lift

Backfill existing users when adding is_email_verified. Without a data migration, pre-existing accounts stay false and JWT issuance will reject them, blocking logins after deploy.

🤖 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/users/migrations/0003_user_is_email_verified_user_mfa_enabled_and_more.py`
around lines 13 - 17, Update the migration containing the is_email_verified
AddField to backfill existing users as verified before enforcing the new JWT
behavior. Add a data-migration step targeting the User model that sets
is_email_verified=True for all pre-existing users, while retaining the field’s
default=False for newly created users.
backend/users/models.py (1)

36-38: 🔒 Security & Privacy | 🟠 Major | 🏗️ Heavy lift

mfa_secret is stored in plaintext.

The TOTP shared secret is persisted as a plain CharField with no encryption at rest. Per the verify_mfa/enable_mfa context (views.py), this raw value is read directly into pyotp.TOTP(user.mfa_secret). A DB dump or read-only SQL injection would let an attacker generate valid TOTP codes indefinitely for any user, defeating the purpose of MFA. Consider an encrypted field (e.g. django-cryptography's EncryptedCharField, or Fernet-encrypt/decrypt around get/set) so the secret is unreadable from a raw DB dump.

🧰 Tools
🪛 ast-grep (0.44.1)

[info] 37-37: use help_text to document model columns
Context: models.CharField(max_length=32, blank=True, null=True)
Note: [CWE-710] Improper Adherence to Coding Standards.

(model-help-text)

🤖 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/users/models.py` around lines 36 - 38, The mfa_secret field in the
user model stores TOTP secrets in plaintext; replace it with the project’s
established encrypted-field mechanism or add Fernet encryption/decryption around
persistence while preserving the existing plaintext value returned to verify_mfa
and enable_mfa for pyotp.TOTP. Include the required key/configuration and
migration so existing data and field access remain compatible.
backend/users/throttling.py (2)

6-24: 🔒 Security & Privacy | 🔴 Critical | ⚡ Quick win

Composite cache key allows brute-force bypass by rotating email addresses.

The cache key f"{ident}_{username}" creates a separate rate-limit bucket for each unique (IP, email) combination. An attacker from a single IP performing credential stuffing (trying many email/password pairs) gets a fresh bucket for each email, effectively bypassing the throttle entirely. This defeats the PR's stated goal of "login throttling and brute-force protection."

The throttle should enforce a per-IP limit regardless of the submitted email, and optionally a separate per-email limit to protect individual accounts.

🔒 Proposed fix: dual-key throttling
 from rest_framework.throttling import SimpleRateThrottle
 
-class LoginRateThrottle(SimpleRateThrottle):
+class LoginRateThrottle(SimpleRateThrottle):
     scope = 'login'
 
     def get_cache_key(self, request, view):
-        if request.user and request.user.is_authenticated:
-            ident = request.user.pk
-        else:
-            ident = self.get_ident(request)
-
-        username = None
-        if hasattr(request, 'data') and isinstance(request.data, dict):
-            username = request.data.get('email')
-
-        if username:
-            return self.cache_format % {
-                'scope': self.scope,
-                'ident': f"{ident}_{username}"
-            }
-        return self.cache_format % {
-            'scope': self.scope,
-            'ident': ident
-        }
+        # Always throttle by IP to prevent credential stuffing
+        return self.cache_format % {
+            'scope': self.scope,
+            'ident': self.get_ident(request),
+        }

If per-account protection is also needed, create a second throttle class (e.g., LoginPerEmailThrottle) with its own scope and add both to throttle_classes.

📝 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.

    def get_cache_key(self, request, view):
        # Always throttle by IP to prevent credential stuffing
        return self.cache_format % {
            'scope': self.scope,
            'ident': self.get_ident(request),
        }
🤖 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/users/throttling.py` around lines 6 - 24, Update get_cache_key so the
existing throttle always uses the client identifier (typically IP) without
appending request.data email, ensuring all login attempts from one IP share a
bucket. If per-account protection is required, add a separate
LoginPerEmailThrottle with its own scope and register both throttles in
throttle_classes.

13-14: 🩺 Stability & Availability | 🟡 Minor | ⚡ Quick win

Accessing request.data can raise ParseError on malformed input.

request.data triggers DRF's parser, which raises ParseError (HTTP 400) for invalid JSON bodies. In a throttle class, this unhandled exception would surface as a 500 error instead of a throttled 429 response. Wrap the access in a try/except.

🛡️ Proposed fix
-        if hasattr(request, 'data') and isinstance(request.data, dict):
-            username = request.data.get('email')
+        try:
+            if hasattr(request, 'data') and isinstance(request.data, dict):
+                username = request.data.get('email')
+        except Exception:
+            username = None
📝 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.

        try:
            if hasattr(request, 'data') and isinstance(request.data, dict):
                username = request.data.get('email')
        except Exception:
            username = None
🤖 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/users/throttling.py` around lines 13 - 14, Update the request-data
handling around the visible request throttling logic to catch DRF ParseError
when evaluating request.data; treat malformed input as unavailable data so the
throttle flow continues and returns its normal throttled 429 response instead of
propagating the exception.
backend/users/views.py (3)

26-43: 🩺 Stability & Availability | 🔴 Critical | ⚡ Quick win

Unhandled, blocking send_mail call can 500 a successful registration.

user = serializer.save() already committed the user+org to the DB. send_mail(..., fail_silently=False) then runs synchronously on the request thread with no timeout and no error handling — if the mail backend is slow or errors, the client gets an unhandled 500 despite the account existing, and they can't re-register (email now taken) or know to check their inbox.

🔧 Proposed fix
-            send_mail(
-                'Verify Your LeadOrbit Email',
-                f'Please verify your email by clicking the following link: {verification_link}',
-                'noreply@leadorbit.com',
-                [user.email],
-                fail_silently=False,
-            )
+            try:
+                send_mail(
+                    'Verify Your LeadOrbit Email',
+                    f'Please verify your email by clicking the following link: {verification_link}',
+                    'noreply@leadorbit.com',
+                    [user.email],
+                    fail_silently=False,
+                )
+            except Exception:
+                logger.exception('Failed to send verification email to %s', user.pk)

Consider moving email delivery to a background task/queue longer-term so registration latency doesn't depend on the mail server.

📝 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.

            try:
                send_mail(
                    'Verify Your LeadOrbit Email',
                    f'Please verify your email by clicking the following link: {verification_link}',
                    'noreply@leadorbit.com',
                    [user.email],
                    fail_silently=False,
                )
            except Exception:
                logger.exception('Failed to send verification email to %s', user.pk)
🤖 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/users/views.py` around lines 26 - 43, Update the registration flow
around the persisted user and send_mail call so email-delivery failures cannot
turn a successful account creation into an unhandled 500. Add appropriate error
handling around send_mail, preserving the saved user response while recording
the failure through the project’s established logging or notification mechanism;
do not change token or verification-link generation.

58-64: 🩺 Stability & Availability | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
rg -n "class User\(|class TenantModel|id = models\.|UUIDField|AutoField" backend/users/models.py backend/tenants/models.py -A3

Repository: Kuldeeep18/LeadOrbit

Length of output: 1219


🏁 Script executed:

python3 - <<'PY'
# Read-only probe: inspect the exact pk type and the lookup path around the user retrieval.
from pathlib import Path
for p in ["backend/tenants/models.py", "backend/users/models.py", "backend/users/views.py"]:
    print(f"\n## {p}")
    text = Path(p).read_text()
    for i, line in enumerate(text.splitlines(), 1):
        if any(k in line for k in ["UUIDField", "class User", "get(pk=uid)", "ValidationError", "DoesNotExist"]):
            print(f"{i}: {line}")
PY

Repository: Kuldeeep18/LeadOrbit

Length of output: 868


Catch DjangoValidationError for UUID PK lookups
User uses a UUID primary key, so a malformed decoded uid can raise DjangoValidationError in User.objects.get(pk=uid) before DoesNotExist. Add it to the except tuple to avoid a 500 on this endpoint.

🔧 Proposed defensive fix
-        except (TypeError, ValueError, OverflowError, User.DoesNotExist):
+        except (TypeError, ValueError, OverflowError, User.DoesNotExist, DjangoValidationError):
             user = None
🤖 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/users/views.py` around lines 58 - 64, Update the exception tuple in
the uid decoding and User.objects.get lookup to also catch
DjangoValidationError, ensuring malformed UUID primary keys set user to None
instead of propagating an error.

72-88: 🔒 Security & Privacy | 🟠 Major | 🏗️ Heavy lift

MFA secrets are persisted in plaintext.

user.mfa_secret (a plain CharField per models.py) stores the raw TOTP secret from pyotp.random_base32(). confirm_mfa (Lines 89-104 / Line 98) and verify_mfa (Line 121) later read it back in the clear too. A database compromise would expose every user's TOTP seed, letting an attacker fully bypass MFA for every enrolled account — undermining the very control this PR adds. Consider encrypting the secret at rest (e.g. Fernet with a dedicated key) before save(), decrypting only when constructing pyotp.TOTP(...).

🤖 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/users/views.py` around lines 72 - 88, Protect the TOTP secret
throughout the MFA flow by encrypting it before assigning `user.mfa_secret` in
`enable_mfa`, then decrypting it only when constructing `pyotp.TOTP` in
`confirm_mfa` and `verify_mfa`. Use a dedicated Fernet key from configuration
and a shared encryption/decryption helper, while preserving the existing
provisioning URI and verification behavior.

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.

Security Hardening: Authentication & Application Safeguards

1 participant