Skip to content

[Eng-32] feat :create otp based reset for password#3630

Merged
vigneshhari merged 28 commits into
developfrom
ENG-32-create-otp-based-reset-for-password
Jun 5, 2026
Merged

[Eng-32] feat :create otp based reset for password#3630
vigneshhari merged 28 commits into
developfrom
ENG-32-create-otp-based-reset-for-password

Conversation

@nandkishorr

@nandkishorr nandkishorr commented Apr 27, 2026

Copy link
Copy Markdown
Contributor

Proposed Changes

  • Added otp based reset password apis.
  • Api to request for an OTP to reset password
  • API that accepts the OTP and the password to reset to
  • configurable abuse limits is also implemented,

Merge Checklist

  • Tests added/fixed
  • Update docs in /docs
  • Linting Complete
  • Any other necessary step

Only PR's with test cases included and passing lint and test pipelines will be reviewed

@ohcnetwork/care-backend-maintainers @ohcnetwork/care-backend-admins

Summary by CodeRabbit

  • New Features

    • OTP-based password reset via phone number for account recovery.
  • Improvements

    • Purpose-specific SMS messages for login and password reset (customizable content).
    • Centralized OTP generation, storage, resend limits, and verification for more reliable behavior.
    • Login flow now validates and consumes OTPs from a unified OTP store.

Review Change Stack

@nandkishorr nandkishorr requested a review from a team as a code owner April 27, 2026 18:04

@greptile-apps greptile-apps 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.

Your free trial has ended. If you'd like to continue receiving code reviews, you can add a payment method here.

@coderabbitai

coderabbitai Bot commented Apr 27, 2026

Copy link
Copy Markdown
Contributor

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review
📝 Walkthrough

Walkthrough

This PR centralizes OTP generation/sending with purpose-specific renderers, renames the OTP model to MobileOTP (with migration), adapts login to use MobileOTP, adds an OTP-based password-reset send/confirm API, updates SMS message settings, and registers the new route.

Changes

OTP Authentication Refactoring and Password Reset

Layer / File(s) Summary
Model rename and database migration
care/facility/migrations/0485_rename_patientmobileotp_mobileotp.py, care/facility/models/patient.py
The PatientMobileOTP model is renamed to MobileOTP via a Django migration that depends on the prior facility migration.
SMS message content configuration
config/settings/base.py
The single template-path setting is replaced with two env-backed message content settings: OTP_SMS_LOGIN_CONTENT and OTP_SMS_RESET_PASSWORD_CONTENT, both supporting {otp} interpolation.
Centralized OTP generation & renderer
care/emr/api/otp_viewsets/login.py
Adds OTPType/LoginOTP, centralizes OTP generation/sending in send_otp(phone_number, otp_type), enforces resend windows via MobileOTP, and persists MobileOTP records.
Login endpoints using MobileOTP
care/emr/api/otp_viewsets/login.py
OTPLoginView.send now uses OTPRequestBaseSpec and calls send_otp(..., otp_type=LoginOTP()); OTPLoginView.login validates against unused MobileOTP rows and marks them used on success.
Password reset OTP API
care/users/api/otp_viewset/reset_password.py
Adds OTPResetPasswordView with send and confirm actions, Pydantic request specs (OTPResetSendSpec, OTPResetConfirmSpec), ResetPasswordOTP renderer, send_otp usage, OTP lookup within the repeat window, password validation via Django validators, user password update, and MobileOTP cleanup.
API routing for password reset endpoints
config/api_router.py
Registers OTPResetPasswordView under otp/password_reset with basename otp-password-reset.

🎯 4 (Complex) | ⏱️ ~45 minutes

🚥 Pre-merge checks | ✅ 3 | ❌ 2

❌ Failed checks (1 warning, 1 inconclusive)

Check name Status Explanation Resolution
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.
Description check ❓ Inconclusive The description covers proposed changes and mentions OTP reset APIs plus abuse limits, but lacks an Associated Issue section and doesn't explain how this solves any specific issue. Add the 'Associated Issue' section referencing issue ENG-32 to explain the problem solved, and consider noting why tests appear unchecked despite the requirement statement.
✅ Passed checks (3 passed)
Check name Status Explanation
Title check ✅ Passed The title directly summarizes the main feature: OTP-based password reset functionality. It clearly identifies the primary change across multiple files.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch ENG-32-create-otp-based-reset-for-password

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.

@nandkishorr nandkishorr self-assigned this Apr 27, 2026
@nandkishorr nandkishorr requested a review from Copilot April 27, 2026 18:08

@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: 6

🧹 Nitpick comments (3)
care/users/models.py (1)

271-276: Add an index for the OTP lookup query, and please pick a story for is_used.

A couple of things worth tightening up:

  1. Missing index on the hot path. OTPResetSendView and OTPResetConfirmView both filter on (phone_number, is_used, created_date). Without an index, this scans the table on every send/confirm. A composite index will scale much better.
  2. is_used is never set to True. The confirm view just .delete()s all OTPs for the phone number. Either keep the flag and mark it consumed (preferred — preserves an audit trail and avoids deleting unrelated rows), or drop the field entirely. Right now it's dead weight that pretends to do something.
  3. Consider an explicit expires_at (or rely strictly on OTP_REPEAT_WINDOW everywhere), and a Meta.verbose_name for admin niceties.
♻️ Proposed refactor
 class UserMobileOTP(BaseModel):
     is_used = models.BooleanField(default=False)
     phone_number = models.CharField(
         max_length=14, validators=[mobile_or_landline_number_validator]
     )
     otp = models.CharField(max_length=10)
+
+    class Meta:
+        verbose_name = "User Mobile OTP"
+        indexes = [
+            models.Index(
+                fields=["phone_number", "is_used", "-created_date"],
+                name="user_mobile_otp_lookup_idx",
+            ),
+        ]

As per coding guidelines: "Implement database indexing and query optimization techniques for better performance."

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

In `@care/users/models.py` around lines 271 - 276, Add a composite DB index on
UserMobileOTP for (phone_number, is_used, created_date) and either use or remove
the is_used flag: update the model class UserMobileOTP to include a Meta.indexes
entry for that composite index and add Meta.verbose_name; then modify
OTPResetConfirmView (and OTPResetSendView lookups) to mark matching OTP rows as
consumed by setting is_used=True (instead of deleting all rows) and/or set an
explicit expires_at timestamp on creation so queries can filter on expiry rather
than relying solely on OTP_REPEAT_WINDOW. Ensure all references to created_date
filtering in OTPResetSendView and OTPResetConfirmView are updated to use the new
index and expires_at where applicable.
care/users/api/otp_viewset/reset_password.py (2)

119-125: Recommended: invalidate active sessions/tokens and minimize the save.

Two follow-ups on the post-reset bookkeeping:

  • After resetting a password, existing JWT refresh tokens and Django sessions remain valid. A common expectation for password-reset flows is that all other sessions are killed. Consider blacklisting outstanding refresh tokens for the user (simplejwt's OutstandingToken / BlacklistedToken) and bumping a password_changed_at field if you have one.
  • user.save() writes every field; pass update_fields=["password"] to avoid spurious side-effects from any custom save overrides.
  • Per the earlier comment on UserMobileOTP.is_used, prefer marking the consumed row as is_used=True instead of deleting all rows for the number — useful for audit/forensics.
♻️ Suggested patch
-        UserMobileOTP.objects.filter(
-            phone_number=data.phone_number,
-        ).delete()
-
-        user.set_password(data.password)
-        user.save()
+        otp_obj.is_used = True
+        otp_obj.save(update_fields=["is_used"])
+
+        user.set_password(data.password)
+        user.save(update_fields=["password"])
+        # TODO: invalidate outstanding refresh tokens / sessions for this user.
         return Response({"message": "Password reset successful"})
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@care/users/api/otp_viewset/reset_password.py` around lines 119 - 125, Replace
the destructive delete and plain save with explicit post-reset bookkeeping: mark
the consumed UserMobileOTP row(s) using UserMobileOTP.is_used=True (rather than
deleting) for auditability, call user.set_password(data.password) and persist
only the password change via user.save(update_fields=["password"]), and
invalidate existing sessions/tokens by blacklisting the user's outstanding JWT
refresh tokens (use simplejwt's OutstandingToken/BlacklistedToken) and/or
updating a password_changed_at timestamp on the user record to force session
revocation. Ensure you reference UserMobileOTP, user.set_password,
user.save(update_fields=["password"]), OutstandingToken/BlacklistedToken and
password_changed_at when implementing these changes.

76-89: Narrow the exception and don't break atomicity between SMS and DB record.

A few smaller items here:

  • except Exception (Ruff BLE001) hides programming bugs. Catch the specific exception(s) raised by get_sms_content / sms.send_text_message instead.
  • logger.error(e) doesn't include the traceback — logger.exception("Failed to send reset OTP") is more useful and avoids stringifying the exception.
  • The DB record is created after a successful SMS send. If UserMobileOTP.objects.create then fails (rare but possible), you've SMS'd an OTP that nobody can verify. Consider creating the record first inside a transaction, then sending the SMS, and rolling back / marking failed on send error. Or at least swap to logger.exception and revisit ordering.
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@care/users/api/otp_viewset/reset_password.py` around lines 76 - 89, The
try/except currently catches Exception and logs without traceback and sends SMS
before creating the DB record, which risks losing atomicity; instead, wrap the
creation of UserMobileOTP.objects.create in a transaction.atomic block, create
the OTP record first, then call get_sms_content and sms.send_text_message;
narrow the except to the specific exceptions those functions raise (e.g.,
template/rendering errors and your SMS client error) and use
logger.exception("Failed to send reset OTP") to log tracebacks; if sending
fails, roll back or delete/mark the created UserMobileOTP record so an unsync'd
SMS can't be verified.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@care/users/api/otp_viewset/reset_password.py`:
- Around line 22-26: rand_pass currently returns a hard-coded OTP when
settings.USE_SMS is False, allowing account takeover; instead always generate a
secure random OTP in rand_pass (use secrets.choice over string.digits for the
requested size) and only gate delivery on settings.USE_SMS (i.e., do not send
SMS when disabled), and for deterministic tests override/mock rand_pass in test
fixtures rather than relying on USE_SMS; if you must keep a deterministic
fallback, restrict that code path to settings.DEBUG and an explicit test-mode
flag (or when running under the test runner) and add a conspicuous warning,
referencing rand_pass and the OTPResetConfirmView delivery logic so generation
and delivery are separated.
- Around line 47-49: The OTPResetConfirmSpec currently allows any 4-char
password and bypasses Django validators; in OTPResetConfirmView.post, after you
locate the user, call
django.contrib.auth.password_validation.validate_password(data.password,
user=user) and catch django.core.exceptions.ValidationError to re-raise as
rest_framework.exceptions.ValidationError({"password":
validation_error.messages}) so validation failures are returned on the
"password" field; also consider increasing OTPResetConfirmSpec.password Field
min_length to at least 8 to match MinimumLengthValidator.
- Around line 60-72: The current flow leaks user existence because the sent_otps
count check runs before the user existence check (sent_otps, User,
settings.OTP_MAX_REPEATS_WINDOW) and is racy; reorder the logic so you first
check whether the phone_number exists (User.objects.filter(...).exists()) and
always return the same silent response for non-existent numbers, then apply the
repeat-window check only for existing users. To fix the TOCTOU race on
sent_otps, wrap the rate check+creation in a DB transaction
(transaction.atomic()) and use a locking read (select_for_update() on the
UserMobileOTP queryset) or replace with an atomic Redis counter; also add/stack
a coarser IP-based rate limit via django_ratelimit for the reset_password
endpoint to mitigate attacker rotation.
- Line 75: Remove the credential leak by deleting or disabling the
logger.info(random_otp) call; do not log OTP values. If a local debug aid is
required, guard it with settings.DEBUG and use logger.debug(...) (and log only a
one-way hash or an OTP id, not the OTP itself). Also revisit the rand_pass
fallback ("45612") in the same module and replace it with a secure, non-constant
fallback or raise an error so secrets are not predictable.
- Around line 32-40: The validator OTPBaseSpec.validate_phone_number currently
calls mobile_validator and catches Exception broadly, which mismatches
User.phone_number and UserMobileOTP.phone_number (which use
mobile_or_landline_number_validator) and hides intent; update
validate_phone_number to either call mobile_or_landline_number_validator if
landlines should be accepted, or keep mobile_validator but add an inline comment
stating landline-only numbers are intentionally rejected for OTP, and replace
the broad "except Exception" with catching the specific ValidationError from
PhoneNumberValidator so only validation failures are handled and original
exceptions propagate.
- Around line 100-117: In the post method of the OTP reset flow
(OTPResetConfirmSpec input handling in post), replace the explicit "No account
found" ValidationError with the same generic OTP error used for invalid OTPs so
phone enumeration is not possible; add tracking on UserMobileOTP (e.g., a
failed_attempts integer and last_failed_at) and increment it on each mismatch,
invalidating or marking is_used=True when failed_attempts reaches a constant
(e.g., OTP_MAX_FAILED_ATTEMPTS = 5) to prevent brute-force, and perform the OTP
comparison using secrets.compare_digest(otp_obj.otp, data.otp) for
timing-safety; optionally add/annotate a rate-limit decorator (django_ratelimit)
keyed by phone_number+IP to provide a second layer of protection.

---

Nitpick comments:
In `@care/users/api/otp_viewset/reset_password.py`:
- Around line 119-125: Replace the destructive delete and plain save with
explicit post-reset bookkeeping: mark the consumed UserMobileOTP row(s) using
UserMobileOTP.is_used=True (rather than deleting) for auditability, call
user.set_password(data.password) and persist only the password change via
user.save(update_fields=["password"]), and invalidate existing sessions/tokens
by blacklisting the user's outstanding JWT refresh tokens (use simplejwt's
OutstandingToken/BlacklistedToken) and/or updating a password_changed_at
timestamp on the user record to force session revocation. Ensure you reference
UserMobileOTP, user.set_password, user.save(update_fields=["password"]),
OutstandingToken/BlacklistedToken and password_changed_at when implementing
these changes.
- Around line 76-89: The try/except currently catches Exception and logs without
traceback and sends SMS before creating the DB record, which risks losing
atomicity; instead, wrap the creation of UserMobileOTP.objects.create in a
transaction.atomic block, create the OTP record first, then call get_sms_content
and sms.send_text_message; narrow the except to the specific exceptions those
functions raise (e.g., template/rendering errors and your SMS client error) and
use logger.exception("Failed to send reset OTP") to log tracebacks; if sending
fails, roll back or delete/mark the created UserMobileOTP record so an unsync'd
SMS can't be verified.

In `@care/users/models.py`:
- Around line 271-276: Add a composite DB index on UserMobileOTP for
(phone_number, is_used, created_date) and either use or remove the is_used flag:
update the model class UserMobileOTP to include a Meta.indexes entry for that
composite index and add Meta.verbose_name; then modify OTPResetConfirmView (and
OTPResetSendView lookups) to mark matching OTP rows as consumed by setting
is_used=True (instead of deleting all rows) and/or set an explicit expires_at
timestamp on creation so queries can filter on expiry rather than relying solely
on OTP_REPEAT_WINDOW. Ensure all references to created_date filtering in
OTPResetSendView and OTPResetConfirmView are updated to use the new index and
expires_at where applicable.
🪄 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: Repository UI

Review profile: CHILL

Plan: Pro

Run ID: e4176c30-f594-496e-88f8-b595ce32a95b

📥 Commits

Reviewing files that changed from the base of the PR and between 88bee82 and f666b68.

📒 Files selected for processing (7)
  • care/templates/sms/otp_reset_sms.txt
  • care/users/api/otp_viewset/__init__.py
  • care/users/api/otp_viewset/reset_password.py
  • care/users/migrations/0028_usermobileotp.py
  • care/users/models.py
  • config/settings/base.py
  • config/urls.py

Comment thread care/users/api/otp_viewset/reset_password.py Outdated
Comment thread care/users/api/otp_viewset/reset_password.py Outdated
Comment thread care/users/api/otp_viewset/reset_password.py Outdated
Comment thread care/users/api/otp_viewset/reset_password.py Outdated
Comment thread care/users/api/otp_viewset/reset_password.py Outdated
Comment thread care/users/api/otp_viewset/reset_password.py Outdated

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Pull request overview

Adds OTP-by-SMS based password reset endpoints for users, including a new OTP persistence model and an SMS template/config setting.

Changes:

  • Added unauthenticated APIs to send a password-reset OTP and confirm OTP + set a new password.
  • Introduced UserMobileOTP model + migration to store OTPs for password reset.
  • Added reset-password OTP SMS template + settings for selecting the template and new URL routes.

Reviewed changes

Copilot reviewed 6 out of 7 changed files in this pull request and generated 7 comments.

Show a summary per file
File Description
config/urls.py Registers new OTP password reset endpoints under /api/v1/otp/password_reset/....
config/settings/base.py Adds setting/env var for reset-password OTP SMS template path.
care/users/models.py Adds UserMobileOTP model to persist OTPs for password reset.
care/users/migrations/0028_usermobileotp.py Creates DB table for UserMobileOTP.
care/users/api/otp_viewset/reset_password.py Implements OTP send/confirm API views and request specs.
care/templates/sms/otp_reset_sms.txt Adds SMS template content for reset-password OTP message.

Comment thread care/users/api/otp_viewset/reset_password.py Outdated
Comment thread care/users/api/otp_viewset/reset_password.py Outdated
Comment thread care/users/api/otp_viewset/reset_password.py Outdated
Comment thread care/users/api/otp_viewset/reset_password.py Outdated
Comment thread care/users/api/otp_viewset/reset_password.py Outdated
Comment thread care/users/api/otp_viewset/reset_password.py Outdated
Comment thread care/users/api/otp_viewset/reset_password.py
@codecov

codecov Bot commented Apr 27, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 32.43243% with 25 lines in your changes missing coverage. Please review.
✅ Project coverage is 75.85%. Comparing base (2b4ec3f) to head (02e5abc).
⚠️ Report is 1 commits behind head on develop.

Files with missing lines Patch % Lines
care/emr/api/otp_viewsets/login.py 24.24% 25 Missing ⚠️
Additional details and impacted files
@@             Coverage Diff             @@
##           develop    #3630      +/-   ##
===========================================
- Coverage    75.86%   75.85%   -0.02%     
===========================================
  Files          479      479              
  Lines        23031    23044      +13     
  Branches      2378     2378              
===========================================
+ Hits         17472    17479       +7     
- Misses        4986     4992       +6     
  Partials       573      573              

☔ View full report in Codecov by Sentry.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

@nandkishorr nandkishorr changed the title [Eng 32] feat :create otp based reset for password [Eng-32] feat :create otp based reset for password Apr 27, 2026

@vigneshhari vigneshhari left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Rewrite this whole MR only using existing models and existing code. No changes more than 150 lines allowed.

Comment thread config/settings/base.py Outdated
Comment thread care/users/api/otp_viewset/reset_password.py Outdated

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

♻️ Duplicate comments (6)
care/users/api/otp_viewset/reset_password.py (6)

128-129: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Confirm endpoint still enumerates users

Line 129 returns a user-existence-specific error ("No User linked..."). This leaks registration status and undercuts the send endpoint’s generic behavior.

🤖 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 `@care/users/api/otp_viewset/reset_password.py` around lines 128 - 129, The
confirm endpoint currently raises a user-specific ValidationError ("No User
linked to this phone number"), which leaks registration status; update the raise
in the reset_password confirm flow (the raise ValidationError call) to return
the same generic, non-enumerating error message used by the send endpoint (e.g.
a generic "If this phone number is registered, you will receive an OTP" or the
existing send-endpoint message) so both paths are indistinguishable; locate the
raise ValidationError in reset_password.py (the confirm handler) and replace the
specific message with the shared generic message constant/string used by the
send endpoint.

76-107: ⚠️ Potential issue | 🟠 Major | 🏗️ Heavy lift

Rate-limit check and OTP creation are racy (TOCTOU)

The count-then-create flow is non-atomic; concurrent requests can exceed OTP_MAX_REPEATS_WINDOW. Wrap the check+create in a transaction with locking (or move to an atomic external counter).

🤖 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 `@care/users/api/otp_viewset/reset_password.py` around lines 76 - 107, The
current count-then-create for UserMobileOTP is vulnerable to TOCTOU races; wrap
the logic that queries recent OTPs and creates a new UserMobileOTP in a single
atomic DB transaction and acquire a lock on the relevant rows to serialize
concurrent requests. Concretely, enclose the check +
UserMobileOTP.objects.create(...) inside transaction.atomic() and use
UserMobileOTP.objects.select_for_update().filter(phone_number=data.phone_number,
created_date__gte=(timezone.now() -
timedelta(hours=settings.OTP_REPEAT_WINDOW)), is_used=False) to lock the
matching rows before re-counting and deciding whether to raise the
OTP_MAX_REPEATS_WINDOW ValidationError, then create the OTP only after the
locked check passes.

28-32: ⚠️ Potential issue | 🔴 Critical | ⚡ Quick win

Critical: deterministic OTP fallback enables trivial account takeover

On Line 29-30, returning "45612" when SMS is disabled makes OTPs predictable in non-SMS environments. That’s a direct auth bypass, and yes, it’s as bad as it sounds.

Suggested fix
 def rand_pass(size):
-    if not settings.USE_SMS:
-        return "45612"
-
     return "".join(secrets.choice(string.digits) for _ in range(size))
🤖 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 `@care/users/api/otp_viewset/reset_password.py` around lines 28 - 32, The
rand_pass function currently returns a hardcoded "45612" when settings.USE_SMS
is false, creating a predictable OTP; change rand_pass to never return a
constant fallback—use a secure random generator (e.g., secrets.choice over
string.digits) to build a numeric OTP of the requested size even when
settings.USE_SMS is false (or alternatively raise an explicit exception if
non-SMS flows should not generate OTPs). Update the rand_pass implementation
(referencing the function name rand_pass and the settings.USE_SMS check and the
size parameter) so it uses secrets to generate a size-length digit string
instead of the deterministic "45612".

41-45: ⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Narrow exception handling in phone validator

Catching Exception on Line 43 is too broad and can mask unrelated failures. Catch the specific validation exception instead.

Suggested fix
+from django.core.exceptions import ValidationError as DjangoValidationError
@@
-        except Exception as e:
+        except DjangoValidationError as e:
             msg = "Invalid phone number"
             raise ValueError(msg) from e
🤖 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 `@care/users/api/otp_viewset/reset_password.py` around lines 41 - 45, The code
currently catches all Exceptions around mobile_validator(value) and re-raises
ValueError; narrow this by catching the specific exception type the validator
raises (e.g., ValidationError or the validator's custom exception) instead of
Exception. Update the except clause for the mobile_validator call to import and
catch that exact exception (for example except ValidationError as e:) and then
raise ValueError(msg) from e so only validation failures are handled while other
errors bubble up.

130-143: ⚠️ Potential issue | 🟠 Major | 🏗️ Heavy lift

No failed-attempt cap on OTP verification

There’s still no per-phone/per-OTP failed-attempt lockout in confirm flow. With a short numeric OTP, this leaves a practical brute-force window.

🤖 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 `@care/users/api/otp_viewset/reset_password.py` around lines 130 - 143, The
confirm-flow currently directly compares OTPs and lacks a per-phone/OTP
failed-attempt lockout; modify the logic around UserMobileOTP in
reset_password.py to enforce a max-failed-attempts and temporary lockout: add
fields to the UserMobileOTP model (e.g., failed_attempts integer and
lockout_until datetime) or reuse existing equivalents, check lockout_until
(timezone.now()) before validating, and if locked raise a ValidationError like
{"otp":"Too many attempts, try later"}; on failed match increment
failed_attempts atomically (use F() or select_for_update) and if failed_attempts
reaches settings.OTP_MAX_FAILED_ATTEMPTS set lockout_until = timezone.now() +
timedelta(minutes=settings.OTP_LOCKOUT_WINDOW) (and optionally mark is_used),
while on success reset failed_attempts and clear lockout_until and proceed as
before. Ensure you reference UserMobileOTP, the OTP-check block in
reset_password.py, and use settings keys (OTP_MAX_FAILED_ATTEMPTS,
OTP_LOCKOUT_WINDOW) and timezone.now() for time comparisons.

76-88: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

User enumeration still leaks via rate-limit branch ordering

Line 83-86 can return a distinct “max OTP requests exceeded” response before the non-existent-user path at Line 87-88. Attackers can still differentiate registered numbers over repeated probes.

Suggested fix
-        sent_otps = UserMobileOTP.objects.filter(
+        if not User.objects.filter(phone_number=data.phone_number).exists():
+            return Response({"otp": "generated"})
+
+        sent_otps = UserMobileOTP.objects.filter(
             created_date__gte=(
                 timezone.now() - timedelta(hours=settings.OTP_REPEAT_WINDOW)
             ),
             is_used=False,
             phone_number=data.phone_number,
         )
         if sent_otps.count() >= settings.OTP_MAX_REPEATS_WINDOW:
-            raise ValidationError(
-                {"error": "Max OTP requests exceeded. Try again later."}
-            )
-        if not User.objects.filter(phone_number=data.phone_number).exists():
             return Response({"otp": "generated"})
🤖 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 `@care/users/api/otp_viewset/reset_password.py` around lines 76 - 88, Reorder
the checks to avoid user enumeration: first verify whether the account exists
via User.objects.filter(phone_number=data.phone_number).exists() and if the user
does not exist return the same generic response ("otp": "generated") as used for
other branches; only for existing users proceed to evaluate sent_otps =
UserMobileOTP.objects.filter(...), compare against
settings.OTP_MAX_REPEATS_WINDOW and raise the rate-limit ValidationError if
exceeded. This ensures the UserMobileOTP/User existence logic (sent_otps, User,
settings.OTP_MAX_REPEATS_WINDOW) cannot be probed to distinguish registered vs
unregistered numbers.
🧹 Nitpick comments (1)
care/users/api/otp_viewset/reset_password.py (1)

59-60: ⚡ Quick win

Use immutable class attributes for DRF auth/permission lists

Using mutable lists for authentication_classes and permission_classes triggers Ruff RUF012 and is avoidable. Tuples are the safer/default choice.

Suggested fix
-    authentication_classes = []
-    permission_classes = []
+    authentication_classes = ()
+    permission_classes = ()
@@
-    authentication_classes = []
-    permission_classes = []
+    authentication_classes = ()
+    permission_classes = ()

As per coding guidelines, "**/*.py: Use Ruff for linting and formatting Python code ... After code changes, run 'ruff check --fix .' and 'ruff format .'".

Also applies to: 112-113

🤖 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 `@care/users/api/otp_viewset/reset_password.py` around lines 59 - 60, The
class-level attributes authentication_classes and permission_classes are defined
as mutable lists which triggers RUF012; change them to immutable tuples (e.g.,
authentication_classes = () and permission_classes = ()) wherever they appear
(including in reset_password.py and the other occurrence around lines 112-113)
and then run the project linter/formatter (ruff check --fix . and ruff format .)
to apply/verify fixes.
🤖 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.

Duplicate comments:
In `@care/users/api/otp_viewset/reset_password.py`:
- Around line 128-129: The confirm endpoint currently raises a user-specific
ValidationError ("No User linked to this phone number"), which leaks
registration status; update the raise in the reset_password confirm flow (the
raise ValidationError call) to return the same generic, non-enumerating error
message used by the send endpoint (e.g. a generic "If this phone number is
registered, you will receive an OTP" or the existing send-endpoint message) so
both paths are indistinguishable; locate the raise ValidationError in
reset_password.py (the confirm handler) and replace the specific message with
the shared generic message constant/string used by the send endpoint.
- Around line 76-107: The current count-then-create for UserMobileOTP is
vulnerable to TOCTOU races; wrap the logic that queries recent OTPs and creates
a new UserMobileOTP in a single atomic DB transaction and acquire a lock on the
relevant rows to serialize concurrent requests. Concretely, enclose the check +
UserMobileOTP.objects.create(...) inside transaction.atomic() and use
UserMobileOTP.objects.select_for_update().filter(phone_number=data.phone_number,
created_date__gte=(timezone.now() -
timedelta(hours=settings.OTP_REPEAT_WINDOW)), is_used=False) to lock the
matching rows before re-counting and deciding whether to raise the
OTP_MAX_REPEATS_WINDOW ValidationError, then create the OTP only after the
locked check passes.
- Around line 28-32: The rand_pass function currently returns a hardcoded
"45612" when settings.USE_SMS is false, creating a predictable OTP; change
rand_pass to never return a constant fallback—use a secure random generator
(e.g., secrets.choice over string.digits) to build a numeric OTP of the
requested size even when settings.USE_SMS is false (or alternatively raise an
explicit exception if non-SMS flows should not generate OTPs). Update the
rand_pass implementation (referencing the function name rand_pass and the
settings.USE_SMS check and the size parameter) so it uses secrets to generate a
size-length digit string instead of the deterministic "45612".
- Around line 41-45: The code currently catches all Exceptions around
mobile_validator(value) and re-raises ValueError; narrow this by catching the
specific exception type the validator raises (e.g., ValidationError or the
validator's custom exception) instead of Exception. Update the except clause for
the mobile_validator call to import and catch that exact exception (for example
except ValidationError as e:) and then raise ValueError(msg) from e so only
validation failures are handled while other errors bubble up.
- Around line 130-143: The confirm-flow currently directly compares OTPs and
lacks a per-phone/OTP failed-attempt lockout; modify the logic around
UserMobileOTP in reset_password.py to enforce a max-failed-attempts and
temporary lockout: add fields to the UserMobileOTP model (e.g., failed_attempts
integer and lockout_until datetime) or reuse existing equivalents, check
lockout_until (timezone.now()) before validating, and if locked raise a
ValidationError like {"otp":"Too many attempts, try later"}; on failed match
increment failed_attempts atomically (use F() or select_for_update) and if
failed_attempts reaches settings.OTP_MAX_FAILED_ATTEMPTS set lockout_until =
timezone.now() + timedelta(minutes=settings.OTP_LOCKOUT_WINDOW) (and optionally
mark is_used), while on success reset failed_attempts and clear lockout_until
and proceed as before. Ensure you reference UserMobileOTP, the OTP-check block
in reset_password.py, and use settings keys (OTP_MAX_FAILED_ATTEMPTS,
OTP_LOCKOUT_WINDOW) and timezone.now() for time comparisons.
- Around line 76-88: Reorder the checks to avoid user enumeration: first verify
whether the account exists via
User.objects.filter(phone_number=data.phone_number).exists() and if the user
does not exist return the same generic response ("otp": "generated") as used for
other branches; only for existing users proceed to evaluate sent_otps =
UserMobileOTP.objects.filter(...), compare against
settings.OTP_MAX_REPEATS_WINDOW and raise the rate-limit ValidationError if
exceeded. This ensures the UserMobileOTP/User existence logic (sent_otps, User,
settings.OTP_MAX_REPEATS_WINDOW) cannot be probed to distinguish registered vs
unregistered numbers.

---

Nitpick comments:
In `@care/users/api/otp_viewset/reset_password.py`:
- Around line 59-60: The class-level attributes authentication_classes and
permission_classes are defined as mutable lists which triggers RUF012; change
them to immutable tuples (e.g., authentication_classes = () and
permission_classes = ()) wherever they appear (including in reset_password.py
and the other occurrence around lines 112-113) and then run the project
linter/formatter (ruff check --fix . and ruff format .) to apply/verify fixes.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro

Run ID: 9b11e806-cb03-451b-b10d-af6b115ef9e6

📥 Commits

Reviewing files that changed from the base of the PR and between f666b68 and 7454a65.

📒 Files selected for processing (1)
  • care/users/api/otp_viewset/reset_password.py

@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: 4

🤖 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 `@care/emr/api/otp_viewsets/login.py`:
- Around line 34-73: The MobileOTP rows lack a stored purpose so different flows
can consume each other's OTPs; modify the MobileOTP model to add a purpose field
(enum/char matching OTPType) and an index on (phone_number, purpose, otp,
is_used) via a migration, then update send_otp to include purpose when creating
MobileOTP and to filter sent_otps by purpose, and update OTPLoginView.login and
OTPResetPasswordView.confirm (and any cleanup/delete logic) to include purpose
in their queryset filters and deletes so each flow only reads/deletes OTPs with
the matching purpose.
- Around line 35-41: The rate-limit window uses
timedelta(settings.OTP_REPEAT_WINDOW) which treats the value as days; update the
calculation so the window is built with hours—e.g. replace the timedelta usage
in the sent_otps query (the MobileOTP filter where created_date__gte is
computed) to use timedelta(hours=settings.OTP_REPEAT_WINDOW) so the
OTP_REPEAT_WINDOW setting is applied as hours when enforcing
OTP_MAX_REPEATS_WINDOW.
- Around line 115-117: The current lookup for MobileOTP (using
MobileOTP.objects.filter(...).first()) doesn't enforce the OTP validity window;
change the query to also filter created_date >= timezone.now() -
OTP_REPEAT_WINDOW (use Django timezone) so only recent OTPs are accepted, and
immediately expire the OTP upon successful validation by setting
otp_object.is_used = True and saving (or perform an atomic update like
MobileOTP.objects.filter(pk=otp_object.pk, is_used=False).update(is_used=True)
to avoid races). Reference MobileOTP, OTP_REPEAT_WINDOW, otp_object, and the
data.phone_number/data.otp fields when applying these changes.

In `@config/exception_handler.py`:
- Around line 12-13: The Pydantic error conversion is leaking submitted values
via exc.errors(); update the conversion where PydanticValidationError is mapped
to DRFValidationError (the block that currently does exc =
DRFValidationError(detail=exc.errors())) to filter or remap the error list and
remove any "input" keys or nested submitted values (specifically strip
password/code fields originating from PasswordVerifyRequest and
TOTPVerifyRequest) before constructing the DRFValidationError; ensure you
iterate exc.errors(), remove or redact any "input" entries (or replace them with
safe placeholders), and pass the sanitized list into
DRFValidationError(detail=...).
🪄 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: Repository UI

Review profile: CHILL

Plan: Pro

Run ID: b6380c58-61ee-4c34-87fc-aef2fdeaa6c4

📥 Commits

Reviewing files that changed from the base of the PR and between 7454a65 and e15ae5a.

📒 Files selected for processing (6)
  • care/emr/api/otp_viewsets/login.py
  • care/facility/migrations/0485_rename_patientmobileotp_mobileotp.py
  • care/facility/models/patient.py
  • care/users/api/otp_viewset/reset_password.py
  • config/api_router.py
  • config/exception_handler.py

Comment thread care/emr/api/otp_viewsets/login.py Outdated
Comment thread care/emr/api/otp_viewsets/login.py Outdated
Comment on lines +115 to 117
otp_object = MobileOTP.objects.filter(
phone_number=data.phone_number, otp=data.otp, is_used=False
).first()

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Expire login OTPs when validating them.

Lines 115-117 accept any unused row matching the phone number and OTP, with no created_date bound. That makes stale OTPs valid indefinitely until someone happens to use them, despite OTP_REPEAT_WINDOW being defined as the validity window.

Suggested fix
-        otp_object = MobileOTP.objects.filter(
-            phone_number=data.phone_number, otp=data.otp, is_used=False
-        ).first()
+        otp_object = (
+            MobileOTP.objects.filter(
+                phone_number=data.phone_number,
+                otp=data.otp,
+                is_used=False,
+                created_date__gte=(
+                    timezone.now() - timedelta(hours=settings.OTP_REPEAT_WINDOW)
+                ),
+            )
+            .order_by("-created_date")
+            .first()
+        )
🤖 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 `@care/emr/api/otp_viewsets/login.py` around lines 115 - 117, The current
lookup for MobileOTP (using MobileOTP.objects.filter(...).first()) doesn't
enforce the OTP validity window; change the query to also filter created_date >=
timezone.now() - OTP_REPEAT_WINDOW (use Django timezone) so only recent OTPs are
accepted, and immediately expire the OTP upon successful validation by setting
otp_object.is_used = True and saving (or perform an atomic update like
MobileOTP.objects.filter(pk=otp_object.pk, is_used=False).update(is_used=True)
to avoid races). Reference MobileOTP, OTP_REPEAT_WINDOW, otp_object, and the
data.phone_number/data.otp fields when applying these changes.

Comment thread config/exception_handler.py Outdated
@nandkishorr nandkishorr requested a review from vigneshhari May 6, 2026 04:43

@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: 1

🤖 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 `@care/users/api/otp_viewset/reset_password.py`:
- Around line 74-99: Make the OTP consumption transactional: wrap the OTP
read-and-claim in a transaction.atomic() and use select_for_update() to lock the
matching OTP row before validating it, then mark it consumed (is_used=True and
save) before performing validate_password, user.set_password and user.save.
Locate the MobileOTP lookup (the variable otp_obj) and replace the non-atomic
read with
MobileOTP.objects.select_for_update().filter(phone_number=data.phone_number,
is_used=False, created_date__gte=(timezone.now() -
timedelta(hours=settings.OTP_REPEAT_WINDOW))).order_by("-created_date").first()
inside a transaction.atomic() block, check otp_obj and otp match, set
otp_obj.is_used=True and save immediately to atomically claim the OTP, then
continue with password validation and user.save.
🪄 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: Repository UI

Review profile: CHILL

Plan: Pro

Run ID: a4ed46b0-4aeb-466b-8d1e-1b8baf23d5a7

📥 Commits

Reviewing files that changed from the base of the PR and between e15ae5a and 0a9937b.

📒 Files selected for processing (3)
  • care/emr/api/otp_viewsets/login.py
  • care/users/api/otp_viewset/reset_password.py
  • config/settings/base.py

Comment thread care/users/api/otp_viewset/reset_password.py
Comment thread care/users/api/otp_viewset/reset_password.py Outdated
Comment thread care/emr/api/otp_viewsets/login.py Outdated
Comment thread config/settings/base.py Outdated
Comment thread care/emr/api/otp_viewsets/login.py Outdated
Comment thread care/emr/api/otp_viewsets/login.py Fixed
Comment thread care/emr/api/otp_viewsets/login.py Fixed
Comment thread care/users/api/otp_viewset/reset_password.py Fixed
Comment thread care/users/api/otp_viewset/reset_password.py Fixed

@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: 2

Caution

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

⚠️ Outside diff range comments (1)
care/emr/api/otp_viewsets/login.py (1)

107-114: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Claim the OTP atomically before issuing the token.

The current read-then-save flow lets two concurrent requests reuse the same OTP if they both pass the is_used=False check before Line 113 flips it.

🛠️ Suggested fix
         otp_object = MobileOTP.objects.filter(
             phone_number=data.phone_number, otp=data.otp, is_used=False
         ).first()
         if not otp_object:
             raise ValidationError({"otp": "Invalid OTP"})
 
-        otp_object.is_used = True
-        otp_object.save()
+        claimed = MobileOTP.objects.filter(pk=otp_object.pk, is_used=False).update(
+            is_used=True
+        )
+        if not claimed:
+            raise ValidationError({"otp": "Invalid OTP"})
🤖 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 `@care/emr/api/otp_viewsets/login.py` around lines 107 - 114, The code performs
a non-atomic read-then-write on MobileOTP (otp_object =
MobileOTP.objects.filter(...).first(); otp_object.is_used = True;
otp_object.save()) which allows race conditions; wrap the claim in a DB
transaction and atomically claim the OTP (use either an UPDATE filter that sets
is_used=True where phone_number=data.phone_number, otp=data.otp, is_used=False
and check the affected row count, or perform a transaction.atomic with
MobileOTP.objects.select_for_update(...) to lock the row before marking
is_used=True). If the atomic update affects 0 rows, raise the same
ValidationError({"otp":"Invalid OTP"}); otherwise proceed to issue the token.
Ensure you reference MobileOTP, otp_object, and the view/function handling the
OTP claim.
♻️ Duplicate comments (2)
care/emr/api/otp_viewsets/login.py (2)

38-44: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Use hours= when building the resend window.

timedelta(settings.OTP_REPEAT_WINDOW) treats 6 as days, so once the cap is hit the user gets blocked for six days, not six hours.

🛠️ Suggested fix
     sent_otps = MobileOTP.objects.filter(
-        created_date__gte=(timezone.now() - timedelta(settings.OTP_REPEAT_WINDOW)),
+        created_date__gte=(
+            timezone.now() - timedelta(hours=settings.OTP_REPEAT_WINDOW)
+        ),
         is_used=False,
         phone_number=phone_number,
     )
🤖 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 `@care/emr/api/otp_viewsets/login.py` around lines 38 - 44, The code builds the
resend window incorrectly using timedelta(settings.OTP_REPEAT_WINDOW) which
interprets the value as days; change it to use an hours keyword so the window is
hours, e.g., use timedelta(hours=settings.OTP_REPEAT_WINDOW) when computing
created_date__gte in the MobileOTP.objects.filter call (the logic around
sent_otps and the OTP_REPEAT_WINDOW / OTP_MAX_REPEATS_WINDOW checks should
remain the same).

107-109: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Expire OTPs during login validation.

This lookup accepts any unused matching row, so an old OTP stays valid until someone eventually uses it. That quietly ignores OTP_REPEAT_WINDOW.

🛠️ Suggested fix
         otp_object = MobileOTP.objects.filter(
-            phone_number=data.phone_number, otp=data.otp, is_used=False
-        ).first()
+            phone_number=data.phone_number,
+            otp=data.otp,
+            is_used=False,
+            created_date__gte=(
+                timezone.now() - timedelta(hours=settings.OTP_REPEAT_WINDOW)
+            ),
+        ).order_by("-created_date").first()
🤖 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 `@care/emr/api/otp_viewsets/login.py` around lines 107 - 109, The login lookup
currently picks any unused MobileOTP row and ignores OTP_REPEAT_WINDOW; modify
the logic around the otp_object assignment so old OTPs are expired before/when
validating: either filter the queryset to include created_at__gte=timezone.now()
- OTP_REPEAT_WINDOW (using Django timezone) when building
MobileOTP.objects.filter(phone_number=..., otp=..., is_used=False) or
proactively mark stale rows as used with
MobileOTP.objects.filter(phone_number=data.phone_number, is_used=False,
created_at__lt=timezone.now() - OTP_REPEAT_WINDOW).update(is_used=True) and then
perform the existing lookup; reference MobileOTP, OTP_REPEAT_WINDOW, created_at
and the otp_object assignment to locate the change.
🧹 Nitpick comments (1)
config/settings/base.py (1)

693-700: ⚡ Quick win

Validate the {otp} placeholder at startup.

These settings are now part of the deployment contract, so an override without {otp} will either send a useless SMS or fail only when this path is hit. Catching that during startup is a lot less exciting.

🧩 Suggested fix
+from django.core.exceptions import ImproperlyConfigured
+
 ...
 OTP_SMS_LOGIN_CONTENT = env(
     "OTP_SMS_LOGIN_CONTENT",
     default="Care OTP for login is {otp}. Please do not share this with anyone.",
 )
+if "{otp}" not in OTP_SMS_LOGIN_CONTENT:
+    raise ImproperlyConfigured(
+        "OTP_SMS_LOGIN_CONTENT must include the '{otp}' placeholder"
+    )
 
 OTP_SMS_RESET_PASSWORD_CONTENT = env(
     "OTP_SMS_RESET_PASSWORD_CONTENT",
     default="Care OTP for password reset is {otp}. Please do not share this with anyone.",
 )
+if "{otp}" not in OTP_SMS_RESET_PASSWORD_CONTENT:
+    raise ImproperlyConfigured(
+        "OTP_SMS_RESET_PASSWORD_CONTENT must include the '{otp}' placeholder"
+    )
🤖 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 `@config/settings/base.py` around lines 693 - 700, Validate that the OTP SMS
templates OTP_SMS_LOGIN_CONTENT and OTP_SMS_RESET_PASSWORD_CONTENT contain the
"{otp}" placeholder at startup by checking the loaded values immediately after
they are read from env and raising a clear configuration error (e.g., ValueError
or Django's ImproperlyConfigured) if "{otp}" is missing; add this check in the
same module (base.py) so the process fails fast during import rather than at
runtime when sending SMS.
🤖 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 `@care/emr/api/otp_viewsets/login.py`:
- Around line 93-99: The endpoint currently returns raw exception text and maps
unexpected errors to HTTP 400; change the except Exception block in the login
OTP flow (around send_otp(...) / LoginOTP()) to log the full exception
internally (e.g., logger.exception or process_logger.exception) and return a
generic error Response with a 500 status and no internal details (e.g.,
{"error": "internal server error"}), while preserving the existing ValueError ->
ValidationError behavior for invalid phone input.

---

Outside diff comments:
In `@care/emr/api/otp_viewsets/login.py`:
- Around line 107-114: The code performs a non-atomic read-then-write on
MobileOTP (otp_object = MobileOTP.objects.filter(...).first();
otp_object.is_used = True; otp_object.save()) which allows race conditions; wrap
the claim in a DB transaction and atomically claim the OTP (use either an UPDATE
filter that sets is_used=True where phone_number=data.phone_number,
otp=data.otp, is_used=False and check the affected row count, or perform a
transaction.atomic with MobileOTP.objects.select_for_update(...) to lock the row
before marking is_used=True). If the atomic update affects 0 rows, raise the
same ValidationError({"otp":"Invalid OTP"}); otherwise proceed to issue the
token. Ensure you reference MobileOTP, otp_object, and the view/function
handling the OTP claim.

---

Duplicate comments:
In `@care/emr/api/otp_viewsets/login.py`:
- Around line 38-44: The code builds the resend window incorrectly using
timedelta(settings.OTP_REPEAT_WINDOW) which interprets the value as days; change
it to use an hours keyword so the window is hours, e.g., use
timedelta(hours=settings.OTP_REPEAT_WINDOW) when computing created_date__gte in
the MobileOTP.objects.filter call (the logic around sent_otps and the
OTP_REPEAT_WINDOW / OTP_MAX_REPEATS_WINDOW checks should remain the same).
- Around line 107-109: The login lookup currently picks any unused MobileOTP row
and ignores OTP_REPEAT_WINDOW; modify the logic around the otp_object assignment
so old OTPs are expired before/when validating: either filter the queryset to
include created_at__gte=timezone.now() - OTP_REPEAT_WINDOW (using Django
timezone) when building MobileOTP.objects.filter(phone_number=..., otp=...,
is_used=False) or proactively mark stale rows as used with
MobileOTP.objects.filter(phone_number=data.phone_number, is_used=False,
created_at__lt=timezone.now() - OTP_REPEAT_WINDOW).update(is_used=True) and then
perform the existing lookup; reference MobileOTP, OTP_REPEAT_WINDOW, created_at
and the otp_object assignment to locate the change.

---

Nitpick comments:
In `@config/settings/base.py`:
- Around line 693-700: Validate that the OTP SMS templates OTP_SMS_LOGIN_CONTENT
and OTP_SMS_RESET_PASSWORD_CONTENT contain the "{otp}" placeholder at startup by
checking the loaded values immediately after they are read from env and raising
a clear configuration error (e.g., ValueError or Django's ImproperlyConfigured)
if "{otp}" is missing; add this check in the same module (base.py) so the
process fails fast during import rather than at runtime when sending SMS.
🪄 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: Repository UI

Review profile: CHILL

Plan: Pro

Run ID: dd06d766-a027-4425-b9c6-3e0a1e50f042

📥 Commits

Reviewing files that changed from the base of the PR and between 0a9937b and 9347046.

📒 Files selected for processing (3)
  • care/emr/api/otp_viewsets/login.py
  • care/users/api/otp_viewset/reset_password.py
  • config/settings/base.py

Comment thread care/emr/api/otp_viewsets/login.py
Comment thread care/users/api/otp_viewset/reset_password.py Outdated

@vigneshhari vigneshhari left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Address all the comments and vulnerabilities commented by CodeRabbit as well.

Comment thread care/emr/api/otp_viewsets/login.py Outdated
Comment thread care/emr/api/otp_viewsets/login.py Outdated
Comment thread care/users/api/otp_viewset/reset_password.py Outdated


class PatientMobileOTP(BaseModel):
class MobileOTP(BaseModel):

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

We need a periodic job to delete all OTP's that are older than the period they are active for.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Its resolved in 3632

@vigneshhari

Copy link
Copy Markdown
Member

I also cant find any tests on this feature, all lines need to be tested with the OTP sending mechanism mocked.

@nandkishorr nandkishorr force-pushed the ENG-32-create-otp-based-reset-for-password branch from d74176e to 8e03058 Compare May 28, 2026 13:34
@nandkishorr nandkishorr requested a review from vigneshhari May 28, 2026 13:45
@vigneshhari vigneshhari merged commit b2f14a8 into develop Jun 5, 2026
9 of 11 checks passed
@vigneshhari vigneshhari deleted the ENG-32-create-otp-based-reset-for-password branch June 5, 2026 10:19
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants