From 052899307ec8ca3f1b5835db5a2b3d727f5d936e Mon Sep 17 00:00:00 2001 From: Chneemann Date: Tue, 6 Aug 2024 18:18:40 +0200 Subject: [PATCH] send forgot password email --- backend/videoflix/auth/views.py | 48 +++++++++++++++++- .../templates/forgot_password_mail.html | 48 ++++++++++++++++++ .../{mail.html => register_mail.html} | 0 backend/videoflix/videoflix/urls.py | 5 +- .../forgot-password.component.ts | 50 +++++++++++++++++-- frontend/src/app/services/auth.service.ts | 6 +++ 6 files changed, 149 insertions(+), 8 deletions(-) create mode 100644 backend/videoflix/templates/forgot_password_mail.html rename backend/videoflix/templates/{mail.html => register_mail.html} (100%) diff --git a/backend/videoflix/auth/views.py b/backend/videoflix/auth/views.py index b108a75..512f73e 100644 --- a/backend/videoflix/auth/views.py +++ b/backend/videoflix/auth/views.py @@ -59,7 +59,7 @@ class RegisterView(APIView): 'email': user.email, 'token': user.verify_email_token } - html_body = render_to_string("mail.html", merge_data) + html_body = render_to_string("register_mail.html", merge_data) message = EmailMultiAlternatives( subject='Confirm your email', @@ -88,6 +88,52 @@ class VerifyEmailView(APIView): return Response(status=status.HTTP_200_OK) else: return Response({"error": "Invalid email or token."}, status=status.HTTP_400_BAD_REQUEST) + +class ForgotPasswordView(APIView): + def post(self, request): + email = request.data.get('email') + if not email: + return Response({"error": "Email required."}, status=status.HTTP_400_BAD_REQUEST) + + try: + user = CustomUser.objects.get(email=email) + user.verify_email_token = CustomUser.generate_verification_token() + user.save() + self.send_email(user) + except CustomUser.DoesNotExist: + pass + return Response(status=status.HTTP_200_OK) + + def send_email(self, user): + merge_data = { + 'name': user.username, + 'email': user.email, + 'token': user.verify_email_token + } + html_body = render_to_string("forgot_password_mail.html", merge_data) + + message = EmailMultiAlternatives( + subject='Reset your Password', + body=strip_tags(html_body), # Plain text fallback + from_email='noreply@videoflix.com', + to=['andre.kempf.dev@gmail.com'] + ) + message.attach_alternative(html_body, "text/html") + message.send(fail_silently=False) + +class ChangePasswordView(APIView): + def post(self, request): + email = request.data.get('email') + token = request.data.get('token') + + if not email or not token: + return Response({"error": "Email and token are required."}, status=status.HTTP_400_BAD_REQUEST) + + user = CustomUser.objects.filter(Q(email=email) & Q(verify_email_token=token)).first() + if user: + return Response(status=status.HTTP_200_OK) + else: + return Response({"error": "Invalid email or token."}, status=status.HTTP_400_BAD_REQUEST) class AuthView(ObtainAuthToken): authentication_classes = [authentication.TokenAuthentication] diff --git a/backend/videoflix/templates/forgot_password_mail.html b/backend/videoflix/templates/forgot_password_mail.html new file mode 100644 index 0000000..c7923d4 --- /dev/null +++ b/backend/videoflix/templates/forgot_password_mail.html @@ -0,0 +1,48 @@ + + + + +
+

Dear {{ name }},

+

+ We recently received a request to reset your password. If you made this request, please click on the following link to reset your password: +

+ Reset Password +

Please note that for security reasons, this link is only valid for 24 hours.

+

If you did not request a password reset, please ignore this email.

+

Best regards,

+

Your Videoflix team!

+ + diff --git a/backend/videoflix/templates/mail.html b/backend/videoflix/templates/register_mail.html similarity index 100% rename from backend/videoflix/templates/mail.html rename to backend/videoflix/templates/register_mail.html diff --git a/backend/videoflix/videoflix/urls.py b/backend/videoflix/videoflix/urls.py index e172799..0d23654 100644 --- a/backend/videoflix/videoflix/urls.py +++ b/backend/videoflix/videoflix/urls.py @@ -17,7 +17,7 @@ Including another URLconf from django.contrib import admin from django.urls import path, include from users import views -from auth.views import LoginView, RegisterView, VerifyEmailView, AuthView +from auth.views import LoginView, RegisterView, VerifyEmailView, AuthView, ForgotPasswordView urlpatterns = [ path('admin/', admin.site.urls), @@ -27,5 +27,6 @@ urlpatterns = [ path('auth/login/', LoginView.as_view()), path('auth/register/', RegisterView.as_view()), path('auth/verify-email/', VerifyEmailView.as_view()), - + path('auth/forgot-password/', ForgotPasswordView.as_view()), + ] diff --git a/frontend/src/app/components/auth/forgot-password/forgot-password.component.ts b/frontend/src/app/components/auth/forgot-password/forgot-password.component.ts index e40c5a2..ffbb111 100644 --- a/frontend/src/app/components/auth/forgot-password/forgot-password.component.ts +++ b/frontend/src/app/components/auth/forgot-password/forgot-password.component.ts @@ -3,6 +3,9 @@ import { Component } from '@angular/core'; import { BtnLargeComponent } from '../../../shared/components/btn-large/btn-large.component'; import { FormsModule, NgForm } from '@angular/forms'; import { ActivatedRoute, RouterLink } from '@angular/router'; +import { AuthService } from '../../../services/auth.service'; +import { ErrorService } from '../../../services/error.service'; +import { HttpErrorResponse } from '@angular/common/http'; @Component({ selector: 'app-forgot-password', @@ -14,6 +17,7 @@ import { ActivatedRoute, RouterLink } from '@angular/router'; export class ForgotPasswordComponent { authData = { mail: '', + token: '', password: '', passwordConfirm: '', }; @@ -22,13 +26,21 @@ export class ForgotPasswordComponent { queryEmail: boolean = false; queryEmailSuccess: boolean = false; - constructor(private route: ActivatedRoute) {} + constructor( + private route: ActivatedRoute, + private authService: AuthService, + public errorService: ErrorService + ) {} ngOnInit(): void { this.route.queryParams.subscribe((params) => { - this.authData.mail = params['mail'] || ''; + this.authData.mail = params['email'] || ''; + this.authData.token = params['token'] || ''; this.queryEmailSuccess = params['pw-change'] || ''; }); + if (this.authData.mail && this.authData.token) { + this.queryEmail = true; + } } isUserEmailValid(emailValue: string) { @@ -39,16 +51,44 @@ export class ForgotPasswordComponent { onSubmit(ngForm: NgForm, mailInput: any) { if (ngForm.submitted && ngForm.form.valid) { if (mailInput.name === 'mail') { - ngForm.form.reset(); - this.sendMailSuccess = true; + try { + this.verifyEmail(); + ngForm.form.reset(); + } catch {} } else if (mailInput.name === 'password') { ngForm.form.reset(); this.queryEmail = false; this.queryEmailSuccess = true; } - console.log(this.authData); } else { mailInput.control.markAsTouched(); } } + + async verifyEmail() { + const body = { + email: this.authData.mail, + }; + try { + await this.authService.forgotPassword(body); + this.sendMailSuccess = true; + this.errorService.clearError(); + } catch (error) { + this.sendMailSuccess = false; + this.errorMsg(error); + } + } + + errorMsg(error: any) { + if (error instanceof HttpErrorResponse) { + const errorTypes = ['error']; + for (const type of errorTypes) { + if (error.error[type]) { + this.errorService.setError(type, error.error[type]); + return; + } + } + this.errorService.clearError(); + } + } } diff --git a/frontend/src/app/services/auth.service.ts b/frontend/src/app/services/auth.service.ts index 8fa6e87..9d9198f 100644 --- a/frontend/src/app/services/auth.service.ts +++ b/frontend/src/app/services/auth.service.ts @@ -30,6 +30,12 @@ export class AuthService { ); } + async forgotPassword(body: any) { + await lastValueFrom( + this.http.post(`${environment.baseUrl}/auth/forgot-password/`, body) + ); + } + storeAuthToken(data: any, storage: boolean) { storage ? localStorage.setItem('authToken', data.toString())