diff --git a/backend/server/main/settings.py b/backend/server/main/settings.py index 2a3d60c..3df32e2 100644 --- a/backend/server/main/settings.py +++ b/backend/server/main/settings.py @@ -155,7 +155,7 @@ REST_AUTH = { 'JWT_AUTH_HTTPONLY': False, 'REGISTER_SERIALIZER': 'users.serializers.RegisterSerializer', 'USER_DETAILS_SERIALIZER': 'users.serializers.CustomUserDetailsSerializer', - + 'PASSWORD_RESET_SERIALIZER': 'users.serializers.MyPasswordResetSerializer' } STORAGES = { @@ -169,6 +169,8 @@ STORAGES = { AUTH_USER_MODEL = 'users.CustomUser' +FRONTEND_URL = 'http://localhost:5173' + EMAIL_BACKEND = 'django.core.mail.backends.console.EmailBackend' SITE_ID = 1 ACCOUNT_EMAIL_REQUIRED = True diff --git a/backend/server/templates/account/email/password_reset_key_message.txt b/backend/server/templates/account/email/password_reset_key_message.txt new file mode 100644 index 0000000..1716b1d --- /dev/null +++ b/backend/server/templates/account/email/password_reset_key_message.txt @@ -0,0 +1,13 @@ +{% extends "account/email/base_message.txt" %} +{% load i18n %} + +{% block content %}{% autoescape off %}{% blocktrans %}You're receiving this email because you or someone else has requested a password reset for your user account. + +It can be safely ignored if you did not request a password reset. Click the link below to reset your password. TEST FOR AdventurELOG{% endblocktrans %} + +{{ frontend_url }}/settings/forgot-password/confirm?token={{ temp_key }}&uid={{ user_pk }} + +{% if username %} + + +{% blocktrans %}In case you forgot, your username is {{ username }}.{% endblocktrans %}{% endif %}{% endautoescape %}{% endblock content %} \ No newline at end of file diff --git a/backend/server/users/forms.py b/backend/server/users/forms.py new file mode 100644 index 0000000..e6d8c8d --- /dev/null +++ b/backend/server/users/forms.py @@ -0,0 +1,50 @@ +from allauth.account.utils import (filter_users_by_email, user_pk_to_url_str, user_username) +from allauth.utils import build_absolute_uri +from allauth.account.adapter import get_adapter +from allauth.account.forms import default_token_generator +from allauth.account import app_settings +from django.conf import settings + +from allauth.account.forms import ResetPasswordForm as AllAuthPasswordResetForm + +class CustomAllAuthPasswordResetForm(AllAuthPasswordResetForm): + + def clean_email(self): + """ + Invalid email should not raise error, as this would leak users + for unit test: test_password_reset_with_invalid_email + """ + email = self.cleaned_data["email"] + email = get_adapter().clean_email(email) + self.users = filter_users_by_email(email, is_active=True) + return self.cleaned_data["email"] + + def save(self, request, **kwargs): + email = self.cleaned_data['email'] + token_generator = kwargs.get('token_generator', default_token_generator) + + for user in self.users: + temp_key = token_generator.make_token(user) + + path = f"custom_password_reset_url/{user_pk_to_url_str(user)}/{temp_key}/" + url = build_absolute_uri(request, path) + #Values which are passed to password_reset_key_message.txt + context = { + "frontend_url": settings.FRONTEND_URL, + "user": user, + "password_reset_url": url, + "request": request, + "path": path, + "temp_key": temp_key, + 'user_pk': user_pk_to_url_str(user), + } + + if app_settings.AUTHENTICATION_METHOD != app_settings.AuthenticationMethod.EMAIL: + context['username'] = user_username(user) + get_adapter(request).send_mail( + 'account/email/password_reset_key', email, context + ) + + return self.cleaned_data['email'] + + diff --git a/backend/server/users/serializers.py b/backend/server/users/serializers.py index 4881949..35ed373 100644 --- a/backend/server/users/serializers.py +++ b/backend/server/users/serializers.py @@ -2,6 +2,8 @@ from rest_framework import serializers from django.contrib.auth import get_user_model from adventures.models import Adventure +from users.forms import CustomAllAuthPasswordResetForm +from dj_rest_auth.serializers import PasswordResetSerializer User = get_user_model() @@ -177,3 +179,13 @@ class CustomUserDetailsSerializer(UserDetailsSerializer): public_url = public_url.replace("'", "") representation['profile_pic'] = f"{public_url}/media/{instance.profile_pic.name}" return representation + +class MyPasswordResetSerializer(PasswordResetSerializer): + + def validate_email(self, value): + # use the custom reset form + self.reset_form = CustomAllAuthPasswordResetForm(data=self.initial_data) + if not self.reset_form.is_valid(): + raise serializers.ValidationError(self.reset_form.errors) + + return value \ No newline at end of file diff --git a/frontend/src/routes/settings/forgot-password/+page.server.ts b/frontend/src/routes/settings/forgot-password/+page.server.ts index 6a0c90b..86dd787 100644 --- a/frontend/src/routes/settings/forgot-password/+page.server.ts +++ b/frontend/src/routes/settings/forgot-password/+page.server.ts @@ -22,8 +22,13 @@ export const actions: Actions = { email }) }); + if (!res.ok) { - return fail(res.status, { message: await res.json() }); + let message = await res.json(); + + const key = Object.keys(message)[0]; + + return fail(res.status, { message: message[key] }); } return { success: true }; } diff --git a/frontend/src/routes/settings/forgot-password/+page.svelte b/frontend/src/routes/settings/forgot-password/+page.svelte index d0d3857..d1a4bef 100644 --- a/frontend/src/routes/settings/forgot-password/+page.svelte +++ b/frontend/src/routes/settings/forgot-password/+page.svelte @@ -6,7 +6,7 @@

Reset Password

-
+ { const token = event.url.searchParams.get('token'); const uid = event.url.searchParams.get('uid'); - console.log('token', token); - if (!token) { - return redirect(302, '/settings/forgot-password'); - } else { - let response = await fetch(`${serverEndpoint}/auth/password/reset/confirm/`, { - method: 'POST', - headers: { - 'Content-Type': 'application/json' - }, - body: JSON.stringify({ - token: token, - uid: uid, - new_password1: 'password', - new_password2: 'password' - }) - }); - let data = await response.json(); - console.log('data', data); - } - - return {}; + return { + props: { + token, + uid + } + }; }) satisfies PageServerLoad; + +export const actions: Actions = { + reset: async (event) => { + const formData = await event.request.formData(); + + const new_password1 = formData.get('new_password1') as string; + const new_password2 = formData.get('new_password2') as string; + const token = formData.get('token') as string; + const uid = formData.get('uid') as string; + + if (!new_password1 || !new_password2) { + return fail(400, { message: 'Password is required' }); + } + + if (new_password1 !== new_password2) { + return fail(400, { message: 'Passwords do not match' }); + } + + if (!token || !uid) { + return redirect(302, '/settings/forgot-password'); + } else { + let response = await fetch(`${serverEndpoint}/auth/password/reset/confirm/`, { + method: 'POST', + headers: { + 'Content-Type': 'application/json' + }, + body: JSON.stringify({ + token: token, + uid: uid, + new_password1, + new_password2 + }) + }); + if (!response.ok) { + let responseJson = await response.json(); + const key = Object.keys(responseJson)[0]; + return fail(response.status, { message: responseJson[key] }); + } else { + return redirect(302, '/login'); + } + } + } +}; diff --git a/frontend/src/routes/settings/forgot-password/confirm/+page.svelte b/frontend/src/routes/settings/forgot-password/confirm/+page.svelte index 0d9aa7f..d2b42a6 100644 --- a/frontend/src/routes/settings/forgot-password/confirm/+page.svelte +++ b/frontend/src/routes/settings/forgot-password/confirm/+page.svelte @@ -1,5 +1,35 @@ + +

Change Password

+ + + +
+ + + + {#if $page.form?.message} +
+ {$page.form?.message} +
+ {/if} +
+