Skip to content

Commit 86401c9

Browse files
authored
Merge pull request #2517 from yadhukrishnx/production-launchpad-fix
feat(launchpad) : reset password
2 parents c38c39b + bfa19c3 commit 86401c9

File tree

5 files changed

+293
-2
lines changed

5 files changed

+293
-2
lines changed

api/launchpad/launchpad_views.py

Lines changed: 181 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@
1212
from .serializers import (
1313
LaunchpadJobTaskSerializer, LaunchpadLeaderBoardSerializer, LaunchpadParticipantsSerializer, LaunchpadUserListSerializer,
1414
CollegeDataSerializer, LaunchpadUserSerializer, UserProfileUpdateSerializer,LaunchpadJobUpdateSerializer,
15-
LaunchpadUpdateUserSerializer, LaunchPadRankSerializer, TaskCompletedLeaderBoardSerializer, TaskVerificationSerializer, LaunchpadCompanyPublicSerializer
15+
LaunchpadUpdateUserSerializer, LaunchPadRankSerializer, TaskCompletedLeaderBoardSerializer, TaskVerificationSerializer, LaunchpadCompanyPublicSerializer,ForgotPasswordSerializer, ResetPasswordSerializer, ChangePasswordSerializer
1616
)
1717
from api.dashboard.profile.profile_serializer import (
1818
UserProfileSerializer, LinkSocials, UserLevelSerializer, UserLogSerializer,
@@ -39,6 +39,8 @@
3939
from django.core.mail import send_mail, EmailMessage
4040
from django.template.loader import render_to_string
4141
import decouple
42+
import secrets
43+
4244

4345
def send_template_mail(context: dict, subject: str, address: List[str], attachment: str = None):
4446
"""
@@ -717,6 +719,11 @@ def post(self, request):
717719
if not check_password(password, recruiter.password):
718720
return CustomResponse(general_message="Invalid credentials.").get_failure_response()
719721

722+
if not recruiter.company.is_verified:
723+
return CustomResponse(
724+
general_message="Account does not exist"
725+
).get_failure_response()
726+
720727
access_token, refresh_token = generate_launchpad_jwt(recruiter, "recruiter")
721728

722729
return CustomResponse(response={
@@ -2875,3 +2882,176 @@ def get(self, request):
28752882
return CustomResponse().paginated_response(
28762883
data=data, pagination=paginated_queryset.get("pagination")
28772884
)
2885+
2886+
class ForgotPasswordAPI(APIView):
2887+
def post(self, request):
2888+
serializer = ForgotPasswordSerializer(data=request.data)
2889+
2890+
if not serializer.is_valid():
2891+
return CustomResponse(
2892+
message=serializer.errors,
2893+
general_message="Invalid request"
2894+
).get_failure_response()
2895+
2896+
email = serializer.validated_data['email']
2897+
user_type = serializer.validated_data['user_type']
2898+
2899+
# Generate reset token
2900+
reset_token = secrets.token_urlsafe(32)
2901+
expires_at = timezone.now() + timedelta(hours=1) # Token expires in 1 hour
2902+
2903+
try:
2904+
if user_type == 'company':
2905+
user = LaunchpadCompanies.objects.get(poc_email=email)
2906+
user.reset_token = reset_token
2907+
user.reset_token_expires = expires_at
2908+
user.save()
2909+
2910+
user_name = user.name or user.poc_name
2911+
user_email = user.poc_email
2912+
2913+
else: # recruiter
2914+
user = LaunchpadRecruiters.objects.get(email=email)
2915+
user.reset_token = reset_token
2916+
user.reset_token_expires = expires_at
2917+
user.save()
2918+
2919+
user_name = user.name
2920+
user_email = user.email
2921+
2922+
# Send reset email
2923+
try:
2924+
reset_link = f"{decouple.config('FR_DOMAIN_NAME')}/reset-password?token={reset_token}&type={user_type}"
2925+
send_template_mail(
2926+
context={
2927+
"email": user_email,
2928+
"full_name": user_name,
2929+
"reset_link": reset_link,
2930+
"expires_in": "1 hour"
2931+
},
2932+
subject="Password Reset - MuLearn Launchpad",
2933+
address=["launchpad-password-reset.html"]
2934+
)
2935+
except Exception as e:
2936+
return CustomResponse(
2937+
general_message="Failed to send reset email. Please try again."
2938+
).get_failure_response()
2939+
2940+
return CustomResponse(
2941+
general_message="Password reset link has been sent to your email."
2942+
).get_success_response()
2943+
2944+
except (LaunchpadCompanies.DoesNotExist, LaunchpadRecruiters.DoesNotExist):
2945+
# Don't reveal if user exists or not for security
2946+
return CustomResponse(
2947+
general_message="If an account with this email exists, a password reset link has been sent."
2948+
).get_success_response()
2949+
2950+
class ResetPasswordAPI(APIView):
2951+
def post(self, request):
2952+
serializer = ResetPasswordSerializer(data=request.data)
2953+
2954+
if not serializer.is_valid():
2955+
return CustomResponse(
2956+
message=serializer.errors,
2957+
general_message="Invalid request"
2958+
).get_failure_response()
2959+
2960+
user = serializer.validated_data['user']
2961+
new_password = serializer.validated_data['new_password']
2962+
2963+
# Reset password
2964+
user.password = make_password(new_password)
2965+
user.reset_token = None
2966+
user.reset_token_expires = None
2967+
user.save()
2968+
2969+
return CustomResponse(
2970+
general_message="Password has been reset successfully. You can now login with your new password."
2971+
).get_success_response()
2972+
2973+
class VerifyResetTokenAPI(APIView):
2974+
def post(self, request):
2975+
token = request.data.get('token')
2976+
user_type = request.data.get('user_type')
2977+
2978+
if not token or not user_type:
2979+
return CustomResponse(
2980+
general_message="Token and user type are required."
2981+
).get_failure_response()
2982+
2983+
now = timezone.now()
2984+
2985+
try:
2986+
if user_type == 'company':
2987+
user = LaunchpadCompanies.objects.get(
2988+
reset_token=token,
2989+
reset_token_expires__gt=now
2990+
)
2991+
user_name = user.name or user.poc_name
2992+
else:
2993+
user = LaunchpadRecruiters.objects.get(
2994+
reset_token=token,
2995+
reset_token_expires__gt=now
2996+
)
2997+
user_name = user.name
2998+
2999+
return CustomResponse(
3000+
response={
3001+
'valid': True,
3002+
'user_name': user_name,
3003+
'expires_at': user.reset_token_expires
3004+
},
3005+
general_message="Token is valid"
3006+
).get_success_response()
3007+
3008+
except (LaunchpadCompanies.DoesNotExist, LaunchpadRecruiters.DoesNotExist):
3009+
return CustomResponse(
3010+
response={'valid': False},
3011+
general_message="Invalid or expired token"
3012+
).get_failure_response()
3013+
3014+
class ChangePasswordAPI(APIView):
3015+
authentication_classes = [LaunchpadJWTPermission]
3016+
3017+
def post(self, request):
3018+
user_type = request.auth["user_type"]
3019+
user_id = request.auth["id"]
3020+
3021+
serializer = ChangePasswordSerializer(data=request.data)
3022+
3023+
if not serializer.is_valid():
3024+
return CustomResponse(
3025+
message=serializer.errors,
3026+
general_message="Invalid request"
3027+
).get_failure_response()
3028+
3029+
current_password = serializer.validated_data['current_password']
3030+
new_password = serializer.validated_data['new_password']
3031+
3032+
try:
3033+
if user_type == 'company':
3034+
user = LaunchpadCompanies.objects.get(id=user_id)
3035+
else:
3036+
user = LaunchpadRecruiters.objects.get(id=user_id)
3037+
3038+
# Verify current password
3039+
if not check_password(current_password, user.password):
3040+
return CustomResponse(
3041+
message={'current_password': ['Current password is incorrect.']},
3042+
general_message="Password change failed"
3043+
).get_failure_response()
3044+
3045+
# Update password
3046+
user.password = make_password(new_password)
3047+
user.save()
3048+
3049+
return CustomResponse(
3050+
general_message="Password changed successfully."
3051+
).get_success_response()
3052+
3053+
except (LaunchpadCompanies.DoesNotExist, LaunchpadRecruiters.DoesNotExist):
3054+
return CustomResponse(
3055+
general_message="User not found."
3056+
).get_failure_response()
3057+

api/launchpad/serializers.py

Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -608,3 +608,63 @@ def get_colleges(self, obj):
608608
return LaunchPadUserCollegeLink.objects.filter(user=obj).values_list(
609609
"college_id", "college__title"
610610
)
611+
612+
613+
class ForgotPasswordSerializer(serializers.Serializer):
614+
email = serializers.EmailField(required=True)
615+
user_type = serializers.ChoiceField(choices=[('company', 'Company'), ('recruiter', 'Recruiter')])
616+
617+
def validate(self, data):
618+
email = data.get('email')
619+
user_type = data.get('user_type')
620+
621+
if user_type == 'company':
622+
if not LaunchpadCompanies.objects.filter(poc_email=email).exists():
623+
raise serializers.ValidationError("No company found with this email.")
624+
else:
625+
if not LaunchpadRecruiters.objects.filter(email=email).exists():
626+
raise serializers.ValidationError("No recruiter found with this email.")
627+
628+
return data
629+
630+
class ResetPasswordSerializer(serializers.Serializer):
631+
token = serializers.CharField(required=True)
632+
new_password = serializers.CharField(min_length=8, required=True)
633+
confirm_password = serializers.CharField(min_length=8, required=True)
634+
user_type = serializers.ChoiceField(choices=[('company', 'Company'), ('recruiter', 'Recruiter')])
635+
636+
def validate(self, data):
637+
if data['new_password'] != data['confirm_password']:
638+
raise serializers.ValidationError("Passwords don't match.")
639+
640+
token = data.get('token')
641+
user_type = data.get('user_type')
642+
643+
now = timezone.now()
644+
645+
if user_type == 'company':
646+
user = LaunchpadCompanies.objects.filter(
647+
reset_token=token,
648+
reset_token_expires__gt=now
649+
).first()
650+
else:
651+
user = LaunchpadRecruiters.objects.filter(
652+
reset_token=token,
653+
reset_token_expires__gt=now
654+
).first()
655+
656+
if not user:
657+
raise serializers.ValidationError("Invalid or expired reset token.")
658+
659+
data['user'] = user
660+
return data
661+
662+
class ChangePasswordSerializer(serializers.Serializer):
663+
current_password = serializers.CharField(required=True)
664+
new_password = serializers.CharField(min_length=8, required=True)
665+
confirm_password = serializers.CharField(min_length=8, required=True)
666+
667+
def validate(self, data):
668+
if data['new_password'] != data['confirm_password']:
669+
raise serializers.ValidationError("New passwords don't match.")
670+
return data

api/launchpad/urls.py

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,8 @@
55
LoginCompanyAPI, LoginRecruiterAPI, GetCompanyInfoAPI, GetRecruiterInfoAPI,
66
RefreshTokenAPI, CompanyVerifyAPI, ListJobsAPI, VerifyTaskAPI, ListLaunchpadStudentsAPI,
77
SendJobInvitationsAPI, StudentJobInvitationsAPI, StudentApplyToJobAPI, AcceptedStudentsAPI,
8-
ScheduleInterviewAPI, ApplicationFinalDecisionAPI, CompanyListVerifiedAPI, DeleteCompanyAPI,JobAPI
8+
ScheduleInterviewAPI, ApplicationFinalDecisionAPI, CompanyListVerifiedAPI, DeleteCompanyAPI,JobAPI,ForgotPasswordAPI, ResetPasswordAPI, VerifyResetTokenAPI, ChangePasswordAPI,
9+
DeleteCompanyAPI,
910
)
1011

1112
urlpatterns = [
@@ -59,4 +60,10 @@
5960
"get-user-levels/<str:launchpad_id>/", launchpad_views.UserLevelsAPI.as_view()
6061
),
6162
path("ig-leaderboard/", launchpad_views.IGLeaderboardView.as_view()),
63+
64+
path('forgot-password/', ForgotPasswordAPI.as_view(), name='forgot-password'),
65+
path('reset-password/', ResetPasswordAPI.as_view(), name='reset-password'),
66+
path('verify-reset-token/', VerifyResetTokenAPI.as_view(), name='verify-reset-token'),
67+
path('change-password/', ChangePasswordAPI.as_view(), name='change-password'),
68+
6269
]
Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
{% extends "mails/base_mail.html" %} {% block content %}
2+
<div style="background: transparent; width: 100%; height: auto">
3+
<center>
4+
<h1
5+
style="
6+
color: black;
7+
font-family: 'Poppins';
8+
font-weight: 600;
9+
font-size: 30px;
10+
"
11+
>
12+
REFRESH YOUR SECRETS
13+
</h1>
14+
<p
15+
>
16+
Hey there, we got a request to fix up your password! Don't worry, click
17+
the button below to reset your password!
18+
</p>
19+
<div style="width: 100%; height: 10px; background: transparent"></div>
20+
<a href="{{ user.reset_link }}" style="width: 160px; height: 40px">
21+
<img
22+
style="width: 160px; height: 40px"
23+
src="https://iili.io/HQFXsFp.png"
24+
alt=""
25+
/></a>
26+
<div style="width: 100%; height: 10px; background: transparent"></div>
27+
<p><strong>This link will expire in {{ user.expires_in }}.</strong></p>
28+
<p
29+
style="
30+
color: black;
31+
font-family: 'Poppins';
32+
line-height: 1.2;
33+
font-size: 13px;
34+
"
35+
>
36+
Didn't request a password reset? You can ignore this email then.
37+
</p>
38+
</center>
39+
</div>
40+
{% endblock %}

db/launchpad.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,8 @@ class LaunchpadCompanies(models.Model):
1616
username = models.CharField(max_length=50, unique=True)
1717
password = models.CharField(max_length=255)
1818
is_verified = models.BooleanField(default=False)
19+
reset_token = models.CharField(max_length=100, null=True, blank=True)
20+
reset_token_expires = models.DateTimeField(null=True, blank=True)
1921
created_at = models.DateTimeField(auto_now_add=True)
2022
updated_at = models.DateTimeField(auto_now=True)
2123

@@ -37,6 +39,8 @@ class LaunchpadRecruiters(models.Model):
3739
phone = models.CharField(max_length=20)
3840
password = models.CharField(max_length=255)
3941
role = models.CharField(max_length=50, null=True)
42+
reset_token = models.CharField(max_length=100, null=True, blank=True)
43+
reset_token_expires = models.DateTimeField(null=True, blank=True)
4044
created_at = models.DateTimeField(auto_now_add=True)
4145
updated_at = models.DateTimeField(auto_now=True)
4246

0 commit comments

Comments
 (0)