-
Notifications
You must be signed in to change notification settings - Fork 599
[Eng-32] feat :create otp based reset for password #3630
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
vigneshhari
merged 28 commits into
develop
from
ENG-32-create-otp-based-reset-for-password
Jun 5, 2026
Merged
Changes from 8 commits
Commits
Show all changes
28 commits
Select commit
Hold shift + click to select a range
4ec016d
feat:added user otp model
nandkishorr 667e54e
feat:added pydantic specs for apis
nandkishorr 3dfdd03
feat:created send and confirm apis
nandkishorr 8021e7a
feat:added reset password template
nandkishorr f666b68
feat:added routes and migrations
nandkishorr 520e877
Merge branch 'develop' into ENG-32-create-otp-based-reset-for-password
nandkishorr 8a9b522
fix:cleanup - review suggestions
nandkishorr 164721e
Merge branch 'ENG-32-create-otp-based-reset-for-password' of https://…
nandkishorr 54cdcda
Merge branch 'develop' into ENG-32-create-otp-based-reset-for-password
nandkishorr 7454a65
Merge branch 'develop' into ENG-32-create-otp-based-reset-for-password
nandkishorr e15ae5a
refact:updated the api and otp sending method
nandkishorr afeb68c
cleanup
nandkishorr fafad78
refact:message contex files to env
nandkishorr ab6042d
Merge branch 'develop' into ENG-32-create-otp-based-reset-for-password
nandkishorr 0a9937b
Merge branch 'develop' into ENG-32-create-otp-based-reset-for-password
nandkishorr fc1da34
Merge branch 'develop' into ENG-32-create-otp-based-reset-for-password
nandkishorr 7215c1e
refact:updated the otp validation to api function
nandkishorr 9347046
refact:review changes added
nandkishorr f532197
Merge branch 'develop' into ENG-32-create-otp-based-reset-for-password
nandkishorr 447ce22
Merge branch 'develop' into ENG-32-create-otp-based-reset-for-password
nandkishorr 6fe3ccd
fix:added review changes
nandkishorr 97d1998
Merge branch 'develop' into ENG-32-create-otp-based-reset-for-password
nandkishorr 1638abe
Merge branch 'develop' into ENG-32-create-otp-based-reset-for-password
nandkishorr 7f8e3d9
Merge branch 'ENG-32-create-otp-based-reset-for-password' of https://…
nandkishorr 02e5abc
Merge branch 'develop' into ENG-32-create-otp-based-reset-for-password
nandkishorr 21dc945
Merge branch 'develop' into ENG-32-create-otp-based-reset-for-password
nandkishorr acfb02a
feat:added testcases for otp reset password api
nandkishorr 8e03058
Merge branch 'ENG-32-create-otp-based-reset-for-password' of https://…
nandkishorr File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Some comments aren't visible on the classic Files Changed page.
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1 @@ | ||
| Your CARE password reset OTP is {{random_otp}}. Do not share this with anyone. |
Empty file.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,156 @@ | ||
| import logging | ||
| import secrets | ||
| import string | ||
| from datetime import timedelta | ||
|
|
||
| from django.conf import settings | ||
| from django.contrib.auth.password_validation import ( | ||
| get_password_validators, | ||
| validate_password, | ||
| ) | ||
| from django.utils import timezone | ||
| from drf_spectacular.utils import extend_schema | ||
| from pydantic import BaseModel, Field, field_validator | ||
| from pydantic import ValidationError as PydanticValidationError | ||
| from rest_framework.exceptions import ValidationError | ||
| from rest_framework.generics import GenericAPIView | ||
| from rest_framework.response import Response | ||
|
|
||
| from care.users.models import User, UserMobileOTP | ||
| from care.utils import sms | ||
| from care.utils.models.validators import mobile_validator | ||
| from care.utils.sms.utils import get_sms_content | ||
| from config.ratelimit import ratelimit | ||
|
|
||
| logger = logging.getLogger(__name__) | ||
|
|
||
|
|
||
| def rand_pass(size): | ||
| if not settings.USE_SMS: | ||
| return "45612" | ||
|
|
||
| return "".join(secrets.choice(string.digits) for _ in range(size)) | ||
|
|
||
|
|
||
| class OTPBaseSpec(BaseModel): | ||
| phone_number: str | ||
|
|
||
| @field_validator("phone_number") | ||
| @classmethod | ||
| def validate_phone_number(cls, value): | ||
| try: | ||
| mobile_validator(value) | ||
| except Exception as e: | ||
| msg = "Invalid phone number" | ||
| raise ValueError(msg) from e | ||
| return value | ||
|
nandkishorr marked this conversation as resolved.
Outdated
|
||
|
|
||
|
|
||
| class OTPResetSendSpec(OTPBaseSpec): | ||
| pass | ||
|
|
||
|
|
||
| class OTPResetConfirmSpec(OTPBaseSpec): | ||
| otp: str = Field(min_length=settings.OTP_LENGTH, max_length=settings.OTP_LENGTH) | ||
| password: str = Field(min_length=8) | ||
|
|
||
|
|
||
| class OTPResetSendView(GenericAPIView): | ||
|
nandkishorr marked this conversation as resolved.
Outdated
|
||
| authentication_classes = [] | ||
| permission_classes = [] | ||
|
|
||
| @extend_schema(request=OTPResetSendSpec) | ||
| def post(self, request): | ||
| try: | ||
| data = OTPResetSendSpec(**request.data) | ||
| except PydanticValidationError as e: | ||
| raise ValidationError(e.errors()) from e | ||
|
|
||
| if ratelimit(request, "otp-password-reset", ["ip"]): | ||
| error_message = "Too many requests. Please try again later." | ||
| return Response( | ||
| {"detail": error_message}, | ||
| status=429, | ||
| ) | ||
|
|
||
| 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"}) | ||
|
|
||
| random_otp = rand_pass(settings.OTP_LENGTH) | ||
| if settings.USE_SMS: | ||
| try: | ||
| content = get_sms_content( | ||
| settings.OTP_SMS_RESET_PASSWORD_TEMPLATE_PATH, | ||
| {"random_otp": random_otp}, | ||
| ) | ||
| sms.send_text_message( | ||
| content=content, | ||
| recipients=[data.phone_number], | ||
| ) | ||
| except Exception as e: | ||
| logger.error(e) | ||
| return Response( | ||
| {"error": "Error while sending OTP. Contact admin."}, status=400 | ||
| ) | ||
|
|
||
| UserMobileOTP.objects.create(phone_number=data.phone_number, otp=random_otp) | ||
| return Response({"otp": "generated"}) | ||
|
|
||
|
|
||
| class OTPResetConfirmView(GenericAPIView): | ||
| authentication_classes = [] | ||
| permission_classes = [] | ||
|
|
||
| @extend_schema(request=OTPResetConfirmSpec) | ||
| def post(self, request): | ||
| try: | ||
| data = OTPResetConfirmSpec(**request.data) | ||
| except PydanticValidationError as e: | ||
| raise ValidationError(e.errors()) from e | ||
| if ratelimit(request, "otp-password-confirm", ["ip"]): | ||
| error_message = "Too many requests. Please try again later." | ||
| return Response( | ||
| {"detail": error_message}, | ||
| status=429, | ||
| ) | ||
| user = User.objects.filter(phone_number=data.phone_number).first() | ||
|
nandkishorr marked this conversation as resolved.
Outdated
|
||
| if not user: | ||
| raise ValidationError({"error": "No User linked to this phone number"}) | ||
| otp_obj = ( | ||
| UserMobileOTP.objects.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() | ||
| ) | ||
| if not otp_obj or otp_obj.otp != data.otp: | ||
| raise ValidationError({"otp": "Invalid OTP"}) | ||
|
|
||
| validate_password( | ||
| data.password, | ||
| user=user, | ||
| password_validators=get_password_validators( | ||
| settings.AUTH_PASSWORD_VALIDATORS | ||
| ), | ||
| ) | ||
| user.set_password(data.password) | ||
| user.save() | ||
| UserMobileOTP.objects.filter( | ||
| phone_number=data.phone_number, | ||
| ).delete() | ||
|
nandkishorr marked this conversation as resolved.
|
||
| return Response({"message": "Password reset successful"}) | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,31 @@ | ||
| # Generated by Django 6.0 on 2026-04-27 08:54 | ||
|
|
||
| import care.utils.models.validators | ||
| import uuid | ||
| from django.db import migrations, models | ||
|
|
||
|
|
||
| class Migration(migrations.Migration): | ||
|
|
||
| dependencies = [ | ||
| ('users', '0027_user_cached_role_orgs'), | ||
| ] | ||
|
|
||
| operations = [ | ||
| migrations.CreateModel( | ||
| name='UserMobileOTP', | ||
| fields=[ | ||
| ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), | ||
| ('external_id', models.UUIDField(db_index=True, default=uuid.uuid4, unique=True)), | ||
| ('created_date', models.DateTimeField(auto_now_add=True, db_index=True, null=True)), | ||
| ('modified_date', models.DateTimeField(auto_now=True, db_index=True, null=True)), | ||
| ('deleted', models.BooleanField(db_index=True, default=False)), | ||
| ('is_used', models.BooleanField(default=False)), | ||
| ('phone_number', models.CharField(max_length=14, validators=[care.utils.models.validators.PhoneNumberValidator(types=('mobile', 'landline'))])), | ||
| ('otp', models.CharField(max_length=10)), | ||
| ], | ||
| options={ | ||
| 'abstract': False, | ||
| }, | ||
| ), | ||
| ] |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.