Resend verification email in Django using Django-Verify-Email - python

I am using Django-Verify-Email 1.0.6 in order to verify my email address.
I verified my email using this function:
send_verification_email(request, form)
if the user verifies its email then it fine. The problem arises when the email link gets expired and the user needs to verify email by resending the email.
This package contain a function resend_verification_email(request, encoded_email, encoded_token)
the description of the function says to pass encoded_email and encoded_token which was generated previously.
I don't think whether our system has that encoded_token and encoded_email save in DB. Can anyone help me with how to solve this issue?

Well, I guess here goes my first ever answer on Stack Overflow.
After I found this post I continued trying to look for a solution, and found it here: https://github.com/foo290/Django-Verify-Email/issues/6#issuecomment-782689288
While the post is certainly revealing, simply copy pasting the code did not work. So here is what I did in detail.
Firstly you need to extend the class:
from django.template.loader import render_to_string
from django.core.mail import send_mail
from django.core.mail import BadHeaderError
from smtplib import SMTPException
from django.utils.html import strip_tags
class VerifyEmail(_VerifyEmail):
def send_verification_link(self, request, user):
try:
useremail = user.email
verification_url = self.token_manager.generate_link(request, user, useremail)
subject = self.settings.get("subject")
msg = render_to_string(
self.settings.get("html_message_template", raise_exception=True),
{"link": verification_url},
)
try:
send_mail(
subject,
strip_tags(msg),
from_email=self.settings.get("from_alias"),
recipient_list=[useremail],
html_message=msg,
)
return user
except (BadHeaderError, SMTPException):
# user.delete()
return False
except Exception as error:
# user.delete()
if self.settings.get("debug_settings"):
raise Exception(error)
As you can see the method requires a request object. That is actually essential, as it is used to generate the link. The quickest way to do this then is to actually create a view. The following sample is more or less the page I needed - I had to find a way to verify and activate all the users that are not activated yet (we were not activating them at all before this), so what the script does is cycle through all the User objects marked as inactive and send the verification email:
from django.views import View
from VerifyEmail import VerifyEmail
from django.http import HttpResponse, HttpResponseForbidden
class VerifyInactiveUsersSender(View):
def get(self, request):
if(request.user.is_staff or request.user.is_superuser):
inactive_users = User.objects.filter(is_active=False)
for u in inactive_users:
inactive_user = VerifyEmail().send_verification_link(request, u)
return HttpResponse("Go check")
else:
return HttpResponseForbidden()
Given this code sends an email to all the inactive accounts I needed to add the security check, if(request.user.is_staff or request.user.is_superuser):, but that is not needed at all for simply creating a form where the user can get a second email. Hope this helps anybody.

Related

Django password reset token doesn't work after upgrade to v3.1?

I'm scratching my head on this one all day.
I have a Wagtail site that was previously running on Django 2.2 that I'm in the process of upgrading to Django 3.1. I'm using the Django internal password reset token functions to generate unsubscribe links for marketing email being sent through Django.
Since six was deprecated in 3.x versions of Django I've had to re-write my repurposing of the one-time login links a bit, and despite mimicking the behavior of Django internally the view simply doesn't work, despite working manually in the shell. The last bit is the kicker, if I input the values manually and step through my code in the shell, it seems to work.
from django.shortcuts import render
from django.contrib.auth import get_user_model
from django.utils.http import urlsafe_base64_decode
from django.core.exceptions import ValidationError
from django.contrib.auth.tokens import default_token_generator
UserModel = get_user_model()
def unsubscribe(request, uidb64, token):
try:
uid = urlsafe_base64_decode(uidb64).decode()
user = UserModel.objects.get(email=uid)
except (TypeError, ValueError, OverflowError, UserModel.DoesNotExist, ValidationError):
user = None
if user is not None:
token_generator = default_token_generator
if token_generator.check_token(user, token):
# If valid token, set is_subscribed = False and show confirmation.
user.is_subscribed = False
user.save()
return render(request, 'account/unsubscribe.html')
else:
# Else, return invalid token page.
return render(request, 'account/invalid.html')
If I click on an unsubscribe link in an email, I always get returned to my "invalid token" page with the above view, and the user is not set to false on is_subscribed.
However, if I run through the same function manually, the token comes back True.
>>> uid = 'me#gmail.com'
>>> user = UserModel.objects.get(email=uid)
>>> token_generator = default_token_generator
>>> token_generator.check_token(user, token='an08ff-atoken123456789020f3b65c9')
True
So the only thing that can be wrong here is that conditional if on check_token must not be coming back True for some reason, but why? When it works manually?
I traced this bug today and found that UWSGI is breaking it. When I launched my server using production settings without uwsgi, it worked fine.
This is an interesting catch-22 considering Django's CSRF implementation doesn't work properly with SSL-passthrough on sockets, from what I can tell, and now I find that tokens don't work on HTTP-passthrough without SSL.
joy...

Resetting Password inside Django Save Function

I am currently using django rest password reset urls for my password reset needs and I have it attributed to a URL:
url("^password-reset/", include("django_rest_passwordreset.urls", namespace="password_reset")),
However, I would like to call this endpoint in a save method of a model using the email attributed to the user model. Is there any way to do something like this?
You can't 'call an endpoint in a save method', but you can make use of Signals to achieve this.
send_password_reset_token is a receiver function that triggered when a new user is created and sends a password reset link to the newly registered user's email automatically.
from datetime import timedelta
from django.urls import reverse
from django.conf import settings
from django.utils import timezone
from django.dispatch import receiver
from django.core.mail import send_mail
from django.contrib.auth.models import User
from django.db.models.signals import post_save
from django.utils.translation import ugettext_lazy as _
from rest_framework import exceptions
from django_rest_passwordreset.models import ResetPasswordToken, clear_expired, get_password_reset_token_expiry_time, \
get_password_reset_lookup_field
from django_rest_passwordreset.signals import reset_password_token_created
#receiver(post_save, sender=User)
def send_password_reset_token(sender, instance, created, *args, **kwargs):
if created:
email = instance.email
# before we continue, delete all existing expired tokens
password_reset_token_validation_time = get_password_reset_token_expiry_time()
# datetime.now minus expiry hours
now_minus_expiry_time = timezone.now() - timedelta(hours=password_reset_token_validation_time)
# delete all tokens where created_at < now - 24 hours
clear_expired(now_minus_expiry_time)
# find a user by email address (case insensitive search)
users = User.objects.filter(**{'{}__iexact'.format(get_password_reset_lookup_field()): email})
active_user_found = False
# iterate overall users and check if there is any user that is active
# also check whether the password can be changed (is useable), as there could be users that are not allowed
# to change their password (e.g., LDAP user)
for user in users:
if user.eligible_for_reset():
active_user_found = True
# No active user found, raise a validation error
# but not if DJANGO_REST_PASSWORDRESET_NO_INFORMATION_LEAKAGE == True
if not active_user_found and not getattr(settings, 'DJANGO_REST_PASSWORDRESET_NO_INFORMATION_LEAKAGE', False):
raise exceptions.ValidationError({
'email': [_(
"There is no active user associated with this e-mail address or the password can not be changed")],
})
# last but not least: iterate over all users that are active and can change their password
# and create a Reset Password Token and send a signal with the created token
for user in users:
if user.eligible_for_reset():
# define the token as none for now
token = None
# check if the user already has a token
if user.password_reset_tokens.all().count() > 0:
# yes, already has a token, re-use this token
token = user.password_reset_tokens.all()[0]
else:
# no token exists, generate a new token
token = ResetPasswordToken.objects.create(user=user)
# send an e-mail to the user
context = {
'current_user': token.user,
'username': token.user.username,
'email': token.user.email,
'reset_password_url': "{}?token={}".format(reverse('password_reset:reset-password-request'), token.key)
}
send_mail(
'Subject here',
f'Your password_reset link is {context["reset_password_url"]}',
'from#example.com',
['to#example.com'],
fail_silently=False,
)
ResetPasswordRequestToken generation
Django email documentation
You can make use of signal to send email:
considering a Django app named profiles(you need to find and replace all occurrence of profiles with your app name).
create new file profiles/signals.py:
from django.dispatch import receiver
from django_rest_passwordreset.signals import reset_password_token_created
from django.urls import reverse
#receiver(reset_password_token_created)
def send_token_email(sender, instance, reset_password_token, *args, **kwargs):
reset_password_url = f"{instance.request.build_absolute_uri(reverse('password_reset:reset-password-confirm'))}"
message = f"Goto <a href='{reset_password_url}'>{reset_password_url}</a> and enter new password and token({reset_password_token.key})"
# send an e-mail to the user
from django.core.mail import send_mail
send_mail('Password reset',message,'noreply#somehost.com', [reset_password_token.user.email])
update profiles/app.py add ready method:
# ...
class ProfilesConfig(AppConfig):
# ...
def ready(self):
import profiles.signals
I understand that you want to send user an email after you add a user from admin.
You can do that by adding a save_model function in the admin. You can also send the password reset email using PasswordResetForm.
The full code cane be something like this:
from django.contrib import admin
from django.contrib.auth.forms import PasswordResetForm
class UserAdmin(admin.ModelAdmin):
def save_model(self, request, obj, form, change):
super().save_model(request, obj, form, change)
form = PasswordResetForm(data={"email": obj.email})
# calling save will send the email
form.save()
admin.site.register(User, UserAdmin)
Documentation for PasswordResetForm: https://docs.djangoproject.com/en/3.2/topics/auth/default/#django.contrib.auth.forms.PasswordResetForm
All you need do is send an email to them with a token and require that token from them.
if you use a save method within your model, that method will be called every time there is a saved change to an entry in that model which may not be what you want.

How to test if email was sent after executing celery task

I'm using Django 1.10 and Celery 4.1
I have a shared_task which sends an email to the user.
# myapp/tasks.py
#shared_task
def notify_user(user_id):
# TODO: send email and do other stuff here
user = get_object_or_404(User, pk=user_id)
send_mail(
'Subject',
'Body',
'from#example.com',
[user.email],
)
I have another file which contains a function that calls puts that tasks into the queue.
# myapp/utils.py
# ...
def update_queue(self):
# increment no_of_used_referrals by 1
if no_of_used_referrals == 5:
notify_user.apply_async((self.user_id,))
else:
notify_user.apply_async((self.user_id,), eta=new_eta)
Now I am trying to test whether calling update_queue() (where all required checks passes) sends an email to the user when its executed.
I tried to do the following:
# myapp/tests.py
def update_queue_should_call_notify_user_immediately_after_five_referrals_were_used(self):
with unittest.mock.patch('myapp.tasks.notify_user.apply_async') as notify_user_mock:
# ...
for _ in range(5):
entry.update_queue()
self.assertTrue(notify_user_mock.called)
notify_user_mock.assert_called_with((user_id,))
# TODO: check if email was sent
# I tried using :
# self.assertEqual(len(mail.outbox), 1)
# but it fails with error saying 0 != 1
def test_notify_user_should_send_an_email(self):
notify_user.apply_async((user_id,))
# I tried using:
# self.assertEqual(len(mail.outbox), 1)
# but it fails with error saying 0 != 1
I have set EMAIL_BACKEND = 'django.core.mail.backends.locmem.EmailBackend' in my project settings.
Can someone please tell me what is wrong with what I am doing and how to correctly test this case?
EDIT
I have updated my code where I excluded mocking - as suggested by #DanielRoseman.
EDIT2
Please see updated files above.
I am simulating referral system. Once 5 referral links associated with a particular user have been used, user get's some nice feature to their profile. Otherwise they have to wait for a specific time, which I set using eta argument on apply_async.
Every time I call update_queue I check if the number of referals is equal to 5(please see updated code above).
If it is - I want to call notify_user immediately, without passing eta argument value.
If it is not - I increment number of used referral links, revoke old notify_user task, create new notify_user task with new eta argument value.
In order to test that I am simulating that behaviour in for-loop, and I want to test whether after 5 iterations(equal to 5 used referral links) an email was sent to the user (for test purposes I use in-memory backend for email).
I put it here for someone that will face the same problem.
I've solved it with
TEST_RUNNER = 'djcelery.contrib.test_runner.CeleryTestSuiteRunner'
https://stackoverflow.com/a/46531472/7396169
I think this solution is suitable for unit testing.
tasks.py
from django.core.mail import EmailMessage
from django.template.loader import render_to_string
from django.contrib.auth import get_user_model
from accounts.models import Token
from celery import shared_task
#shared_task(bind=True)
def send_login_email_task(self, email):
try:
uid = str(uuid.uuid4())
Token.objects.create(email=email, uid=uid)
current_site = 'localhost:8000'
mail_subject = 'Activate your account.'
message = render_to_string('accounts/login_activation_email.html', {
'domain': current_site,
'uid': uid
})
print('called')
email = EmailMessage(mail_subject, message, to=[email])
email.send()
except Token.DoesNotExist:
logging.warning(
"Tried to send activation email to non-existing user '%s'", email)
except smtplib.SMTPException as exc:
raise self.retry(exc=exc)
test_tasks.py
from django.test import TestCase
from unittest.mock import patch
from django.contrib.auth import get_user_model
from celery.exceptions import Retry
from proj.celery import App
import smtplib
import uuid
import accounts.tasks
from accounts.models import Token
#patch('accounts.tasks.EmailMessage')
def test_send_login_email_task(self, mock_email_message):
# call task
token = Token.objects.get(email=self.email, uid=self.uid)
print(token.email)
accounts.tasks.send_login_email_task.apply_async((token.email,))
self.assertEqual(mock_email_message.called, True)
# patch EmailMessage
print(mock_email_message.call_args)
args, kwargs = mock_email_message.call_args
subject = args[0]
self.assertEqual(subject, 'Activate your account.')
self.assertEqual(kwargs, {'to': ['ama#example.com']})

How to clean up Django login message from framework

There is a project setup with Django 1.6 and Django allauth. when user logged in django saves a login message to users session and its stack in there. While user reached to any page included messages framework, login message shows up with other message.
Because of this reason, I want to remove login message from message queue after user logged in.
I tried remove login message in django's and allauth's user_logged_in signal, but I discovered the message is not created there.
The example of the message removal code is below:
# from allauth.account.signals import user_logged_in
# First I tried allauth signal above.
from django.contrib.auth.signals import user_logged_in
#receiver(user_logged_in)
def user_logged_in_(request, **kwargs):
storage = messages.get_messages(request)
storage.used = True
Edit: The workaround below is working. But I feel it is not right way to do.
After this, I decided to make a workaround. After user logged in, user redirected to index view. I removed signal and append storage.used = True method in index view. Also It is not worked too.
def clear_messages(request):
storage = messages.get_messages(request)
storage.used = True
def index(request):
clear_messages(request)
return render_to_response('website/index.html', {}, context_instance=RequestContext(request, {}))
From django-allauth's documentation:
All messages (as in django.contrib.messages) are configurable by
overriding their respective template. If you want to disable a message
simply override the message template with a blank one.

Django 1.3 post login/logout signals in relation with authentication

First of all both methods below return True. I'd expect the second one to return False using the django standard admin authentication procedure or am I wrong?
def post_login(sender, **kwargs):
"""
Django 1.3 post login signal handler
"""
# do stuff
user = kwargs['user']
print user.is_authenticated()
user_logged_in.connect(post_login)
def post_logout(sender, **kwargs):
"""
Django 1.3 post logout signal handler
"""
# do stuff
user = kwargs['user']
print user.is_authenticated()
user_logged_out.connect(post_logout)
Anyway I'm trying to understand why django doesn't have a hook on authentication failure also.. I can use my own backend for users to login and out of their account, however I would like to hook onto the admin procedure as well to cover everything in one portion of code.. I found some topics but no real awnser how to fix this.
I came up with:
import settings
from django.dispatch import Signal
failed_login = Signal(providing_args=['user'])
from django.contrib.auth.backends import ModelBackend
from django.contrib.auth.models import User
class AuthSignalBackend(ModelBackend):
def authenticate(self, username=None, password=None):
try:
user = User.objects.get(username=username)
if user.check_password(password):
return user
else:
failed_login.send(sender=None, user=user)
except User.DoesNotExist:
return None
def login_handler(sender, **kwargs):
if settings.DEBUG:
print "failed login detected...!"
failed_login.connect(login_handler)
That works great, however there's no request in the ModelBackend, while the post_login and logout signals do have the request.. This is unfortunate because it would be great for IP logging
Any advise is welcome, I'm pretty sure people should have come across this one before..
If user is an instance of User model, user.is_authenticated() will always return True. Models instance can't know what's going on on request level. This method is for views.
If you want to deal with failed login attempts, take a look at django-axes. You can just use it, or look at the code and reimplement some ideas as you like.

Categories