6

I'm using Django 2.0.10 with rest-framework, rest-auth and allauth. I have a custom user model.

I've got email verification working by using the allauth view. The verification email is sent when a user registers. If I click the link in the email, I'm taken to a page with a button to click to verify the email. This all works without error. However what I can't find out is what this actually does. No field in the user's data seems to change.

The behaviour I want is for users to be able to register and login, but only to be able to add content to the site after they have verified their email.

Edit: this post gives part of the answer but doesn't say how to save the verification status as a property of the user so that you can check it in the front end when you load the user data.

settings.py

# django rest auth
ACCOUNT_AUTHENTICATION_METHOD = 'email'
ACCOUNT_EMAIL_REQUIRED = True
ACCOUNT_USERNAME_REQUIRED = False
OLD_PASSWORD_FIELD_ENABLED = True
LOGOUT_ON_PASSWORD_CHANGE = False
ACCOUNT_EMAIL_VERIFICATION = 'optional'

api/urls.py

from allauth.account.views import confirm_email

urlpatterns = [
    re_path(r'^rest-auth/registration/account-confirm-email/(?P<key>[-:\w]+)/$', confirm_email,
     name='account_confirm_email'),
...
]

users/models.py

import uuid 

from django.contrib.auth.models import AbstractUser, UserManager
from django.db import models
from django.utils.http import int_to_base36

class CustomUserManager(UserManager):
    def get_by_natural_key(self, username):
        case_insensitive_username_field = '{}__iexact'.format(self.model.USERNAME_FIELD)
        return self.get(**{case_insensitive_username_field: username})

ID_LENGTH = 12

def pkgen():
    from base64 import b32encode
    from hashlib import sha1
    from random import random

    pk = int_to_base36(uuid.uuid4().int)[:ID_LENGTH]
    return pk

class CustomUser(AbstractUser):
    objects = CustomUserManager()
    slug = models.CharField(max_length=ID_LENGTH, default=pkgen, editable=False)
    id = models.UUIDField(primary_key=True, default=uuid.uuid4, editable=False)

    def __str__(self):
        return self.email

When a user logs in, how can I find out if they have verified their email address? Thanks for any help!

user202729
  • 3,358
  • 3
  • 25
  • 36
Little Brain
  • 2,647
  • 1
  • 30
  • 54
  • Simplest solution would be to set `user.is_active` to `False` when you send an email confirmation to the user upon registration. And set the `user.is_active ` to `True` when he/she confirms the email. Also before a registered user without an confirmed email tries to login you check to see if that user has `is_status`.. `True` or `False`. If its `False` then you don't allow them to login. – Ahtisham Jan 31 '19 at 19:19
  • Thanks, but I want users to be able to log in immediately, just not create content. One reason is so that they can request a new verification email, for example if it didn't arrive or they lost it in spam. – Little Brain Jan 31 '19 at 19:27
  • Then you can simply add an extra field of type boolean in their profile namely `verified` and then set it to `True` or `False` based on if they have confirmed the email. You can then use this information to not allow them to edit the content. – Ahtisham Jan 31 '19 at 19:29
  • Can you explain please how I would do that? I am happy to change my code but I don't want to rewrite the allauth code. – Little Brain Jan 31 '19 at 19:32
  • That post is where I got the information how to check the email address's status, but it doesn't explain how you would change the user object when the email address is verified? – Little Brain Jan 31 '19 at 19:53
  • But you haven't included it in your question. You should have included the link to that question. And then explaining why doesn't it help. – Ahtisham Jan 31 '19 at 19:57
  • Let us [continue this discussion in chat](https://chat.stackoverflow.com/rooms/187678/discussion-between-little-brain-and-ahtisham). – Little Brain Jan 31 '19 at 20:01

4 Answers4

15

Aha! Thanks to this post and this post, I think I have an answer.

The email address's status is saved in a separate table EmailAdress, not as part of the User model. This can be accessed in a modelviewset as follows:

api.py

from allauth.account.admin import EmailAddress

class ListViewSet(viewsets.ModelViewSet):
    ...

    def get_queryset(self):
        # can view public lists and lists the user created
        if self.request.user.is_authenticated:
            print('is there a verified email address?')
            print(EmailAddress.objects.filter(user=self.request.user, verified=True).exists())

            ...

This will return True if the user has any verified email address.

However, it's much more useful to add the verification status to the user. This can be done with a signal as explained here.

views.py

from allauth.account.signals import email_confirmed
from django.dispatch import receiver

@receiver(email_confirmed)
def email_confirmed_(request, email_address, **kwargs):
    user = email_address.user
    user.email_verified = True

    user.save()

Now in api.py you can check like this:

print(self.request.user.email_verified)

This works if you have a single email address that can't be changed or deleted. If you allow multiple email addresses I guess you'd need to make more checks and update the user's status accordingly. But I have only a single email address which is used for login, so I think that's OK.

I think it would be better practice to make 'email_verified' part of a user profile, but this is a working demo.

Little Brain
  • 2,647
  • 1
  • 30
  • 54
6

There's a function for this.

Syntax:

from allauth.account.utils import has_verified_email
has_verified_email(user, email=None) -> bool

According to the source code this does the same thing as what the accepted answer does when email is None. Otherwise it checks if the user has that verified email.

user202729
  • 3,358
  • 3
  • 25
  • 36
0

I had the same problem, I was able to solve this by using the code below:

#Project-level folder urls.py
from django.contrib import admin
from django.urls import path, include
from allauth.account.views import ConfirmEmailView, EmailVerificationSentView 
#These ConfirmEmailView, EmailVerificationSentView are very important
#I used other allauth/ dj-rest-auth views and they didn't automatically verify the email.

urlpatterns = [
path('admin/', admin.site.urls),
path('dj-rest-auth/registration/account-confirm-email/<str:key>/',
    ConfirmEmailView.as_view()), #This is at the top because apparently won't work if below. #Integral to problem solution

path('dj-rest-auth/', include('dj_rest_auth.urls')),
path('dj-rest-auth/registration/', include('dj_rest_auth.registration.urls')),
path('api-auth', include('rest_framework.urls')),

 path('dj-rest-auth/registration/account-confirm-email/', 
  EmailVerificationSentView.as_view(),
        name='account_email_verification_sent'),#Integral to problem solution

]

The code above allowed me to create new users with the registration url. After which are sent an email with a link. When users click on the link they are redirected to the login page, with their email now verified in the database.

0

Try SerializerMethodField

The official example:

from django.contrib.auth.models import User
from django.utils.timezone import now
from rest_framework import serializers

class UserSerializer(serializers.ModelSerializer):
    days_since_joined = serializers.SerializerMethodField()

    class Meta:
        model = User
        fields = '__all__'

    def get_days_since_joined(self, obj):
        return (now() - obj.date_joined).days
CSSer
  • 2,131
  • 2
  • 18
  • 37