2

So, I'm trying to make a blog website. I'm with the authentication, and I'm struggling a bit with the security part. I'm trying to make email verification security a thing in my auth security. I've done all the basic stuff like user registration, login & logout functions, view profile, and edit profile. I have done the email verification part in the user registration, but I am somehow struggling a lot when trying to add email verification after users change their email.

What I want my code to do is when a user edits his/her email, the code has to save the user's old email address temporarily but change the email to the new one. I also want the code to send an email to the old one saying that the email has been edited and a link should be forwarded that can change back the email to the old one(which will only be usable for the next 24 hours) automatically, and an email to the new one asking for verification if he/she were the one to request to change the email.

Oh, and I just wanted to mention that I'm trying to do this without using any models. I know I can do this with allauth, but I'm looking for a way to do this without it.

views.py

from django.shortcuts import render, redirect, get_object_or_404
from django.contrib.auth import authenticate, login, logout, update_session_auth_hash
from django.contrib.auth.forms import UserCreationForm, UserChangeForm, PasswordChangeForm
from django.contrib.auth.views import PasswordChangeView
from .forms import RegisterUserForm, EditProfileForm, EditProfileFormNormal, PasswordChangingForm
from django.views import generic
from django.urls import reverse_lazy
from django.contrib import messages
from django.http import HttpResponse
from mysite import settings
from django.core.mail import send_mail, EmailMessage
from django.contrib.auth.models import User
from .tokens import generate_token
from django.template.loader import render_to_string
from django.utils.http import urlsafe_base64_encode, urlsafe_base64_decode
from django.utils.encoding import force_bytes, force_str
from django.contrib.auth.mixins import LoginRequiredMixin, UserPassesTestMixin
from django.contrib.sites.shortcuts import get_current_site
from django.contrib.sessions.backends.db import SessionStore
from django.views.generic.edit import FormView
from django.core.exceptions import ObjectDoesNotExist


class UserEditView(LoginRequiredMixin, UserPassesTestMixin, generic.UpdateView):
    model = User
    form_class = EditProfileForm
    template_name = 'authstuff/edit_profile.html'
    success_url = reverse_lazy('profile')

    def test_func(self):
        user = self.get_object()
        return self.request.user.is_authenticated and self.request.user.pk == user.pk

    def form_valid(self, form):
        user = form.save(commit=False)
        new_email = form.cleaned_data['email']
        if user.email != new_email:
            session = SessionStore()
            session['pending_email'] = new_email
            session.save()

            current_site = get_current_site(self.request)
            email_subject = "Confirm Your New Email - My Blog Site"
            email_message = render_to_string('authstuff/email_edit_confirmation.html', {
                'user': user,
                'domain': current_site.domain,
                'uid': urlsafe_base64_encode(force_bytes(user.pk)),
                'token': generate_token.make_token(user),
            })
            email = EmailMessage(
                email_subject,
                email_message,
                settings.EMAIL_HOST_USER,
                [user.email],  # Send the email to the old email address
            )
            email.send()

            messages.success(self.request, "Email Change Request: Successful. Please check your email for confirmation instructions.")

        return super().form_valid(form)

    def get_success_url(self):
        uidb64 = urlsafe_base64_encode(force_bytes(self.request.user.pk))
        token = generate_token.make_token(self.request.user)
        return f'/account/confirm_email/{uidb64}/{token}/'

    def get_object(self, queryset=None):
        return get_object_or_404(User, pk=self.kwargs['pk'])

def initiate_email_change(request):
    # Retrieve the user object
    user = request.user

    # Get the new email from the form data or any other source
    new_email = request.POST.get('new_email')

    # Generate a unique token for the email change request
    token = default_token_generator.make_token(user)

    # Store the new email and token in the user's session
    request.session['email_change_data'] = {
        'new_email': new_email,
        'token': token
    }

    # Generate the confirmation URL with uidb64 and token
    uidb64 = urlsafe_base64_encode(force_bytes(user.pk))
    confirmation_url = request.build_absolute_uri(
        f'/confirm_email/{uidb64}/{token}/'
    )

    # Send the confirmation URL to the user via email or display it in the UI
    # You can use Django's email sending mechanism or any other method

    return HttpResponse('Email change request initiated successfully')


def confirm_email(request, uidb64, token):
    try:
        uid = force_str(urlsafe_base64_decode(uidb64))
        user = get_object_or_404(User, pk=uid)
    except (TypeError, ValueError, OverflowError, User.DoesNotExist):
        user = None

    if user is not None and generate_token.check_token(user, token):
        # Retrieve the email change data from the user's session
        email_change_data = request.session.get('email_change_data')
        if email_change_data:
            new_email = email_change_data['new_email']

            # Update the user's email address
            user.email = new_email
            user.save()

            # Clear the email change data from the session
            del request.session['email_change_data']

            return render(request, 'authstuff/email_change_success.html')

    return HttpResponse('Invalid confirmation link')

urls.py

from django.urls import path
from . import views
from .views import UserEditView, from django.utils.http import urlsafe_base64_encode

urlpatterns = [
    path('edit_user_profile/<int:pk>/', UserEditView.as_view(), name="edit-profile"),
]

tokens.py

from django.contrib.auth.tokens import PasswordResetTokenGenerator

from six import text_type

class TokenGenerator(PasswordResetTokenGenerator):
    def _make_hash_value(self,user,timestamp):
        return (
        text_type(user.pk) + text_type(timestamp) 
        # text_type(user.profile.signup_confirmation)
        )

generate_token = TokenGenerator()

email_edit_confirmation.html

{% autoescape off %}  

Hi {{ user.username }},  

We noticed a change in your account's email address, was that you? If so, please click on the link below. If not, It will be better for you to change your password

Confirmation Link:

http://{{ domain }}{% url 'confirm-email' uidb64=uid token=token %}

{% endautoescape %}

I know the code is a bit wrong because my question was different before StefanoTrv (in the comments) suggested changing my logic, but I don't know how to imply his suggestion.

Tanmay
  • 46
  • 9
  • Please refer to this guide on how to provide a [mre]. This is far more code than is necessary to provide an example of how you're attempting email validation. Please try to pare this down to only the pertinent code. – JRiggles May 26 '23 at 17:26
  • 1
    You say you are trying to do this without using any models. Does it mean that you wouldn't like a solution that requires creating a new model? If so, may I ask you why? Also, I think there's a big problem in the flow of your process: if a user asks to change the email associated with their account, it may be because they don't have access to the old email anymore. I'd suggest you send the confirmation email to the new address, and send to the old address a notification that also allows the user to block the process. – StefanoTrv May 26 '23 at 17:30
  • A very simple way to do this is to have a column in the db "isVerified" set to a boolean value(1 if verified and 0 if not) and before updating trigger a mail with already existing mail. – Ajay Managaon May 26 '23 at 17:40
  • JRiggles, I've edited my question... @StefanoTrv Yes I'd like a solution without having to create models. It's because there are a few things linked to the other parts of my code in another app, I'm only looking for a temporary solution right now. Also thank you for your suggestion. I'll edit my question – Tanmay May 26 '23 at 17:44
  • It seems like the only step you haven't done is send the email. Is that what your question is about? If not, I'm not sure what your question is. – Nick ODell May 26 '23 at 17:54
  • @NickODell I did try to...I'm trying to figure out why it isn't working – Tanmay May 26 '23 at 17:56
  • @AjayManagaon Won't that require models? I'm pretty sure mentioned that I don't want to use models. – Tanmay May 27 '23 at 07:12

0 Answers0