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.
Related
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.
I've gone through the docs of Provider and Resource of Django OAuth Toolkit, but all I'm able to find is how to 'authenticate' a user, not how to register a user.
I'm able to set up everything on my machine, but not sure how to register a user using username & password. I know I'm missing something very subtle. How do I exactly register a user and get an access token in return to talk to my resource servers.
OR
Is it like that I've to first register the user using normal Django mechanism and then get the token of the same?
You can do what you are asking, and its your lucky day. I faced that problem when I first started working with django and oauth-toolkit.
The following is my implementation using django-rest-framework. It will register the user, authenticate and return an oauth response.
The thought is this:
Using django models we save the new user using the proper serializers and models.
In the same response, we create a new oauth token and return it to the user.
serializers.py
from rest_framework import serializers
import models
from django.utils.translation import gettext_lazy as _
class RegisterSerializer(serializers.ModelSerializer):
confirm_password = serializers.CharField()
def validate(self, data):
try:
user = models.User.objects.filter(username=data.get('username'))
if len(user) > 0:
raise serializers.ValidationError(_("Username already exists"))
except models.User.DoesNotExist:
pass
if not data.get('password') or not data.get('confirm_password'):
raise serializers.ValidationError(_("Empty Password"))
if data.get('password') != data.get('confirm_password'):
raise serializers.ValidationError(_("Mismatch"))
return data
class Meta:
model = models.User
fields = ('username', 'first_name', 'last_name', 'password', 'confirm_password', 'is_active')
extra_kwargs = {'confirm_password': {'read_only': True}}
view.py
from rest_framework.views import APIView
from rest_framework.response import Response
from rest_framework import status, permissions
from oauth2_provider.settings import oauth2_settings
from braces.views import CsrfExemptMixin
from oauth2_provider.views.mixins import OAuthLibMixin
import json
import models
import serializers
from django.utils.decorators import method_decorator
from django.http import HttpResponse
from django.views.generic import View
from django.views.decorators.debug import sensitive_post_parameters
from django.utils.translation import gettext_lazy as _
from django.db import transaction
class UserRegister(CsrfExemptMixin, OAuthLibMixin, APIView):
permission_classes = (permissions.AllowAny,)
server_class = oauth2_settings.OAUTH2_SERVER_CLASS
validator_class = oauth2_settings.OAUTH2_VALIDATOR_CLASS
oauthlib_backend_class = oauth2_settings.OAUTH2_BACKEND_CLASS
def post(self, request):
if request.auth is None:
data = request.data
data = data.dict()
serializer = serializers.RegisterSerializer(data=data)
if serializer.is_valid():
try:
with transaction.atomic():
user = serializer.save()
url, headers, body, token_status = self.create_token_response(request)
if token_status != 200:
raise Exception(json.loads(body).get("error_description", ""))
return Response(json.loads(body), status=token_status)
except Exception as e:
return Response(data={"error": e.message}, status=status.HTTP_400_BAD_REQUEST)
return Response(data=serializer.errors, status=status.HTTP_400_BAD_REQUEST)
return Response(status=status.HTTP_403_FORBIDDEN)
urls.py
rom django.conf.urls import url
from oauth2_provider import views as oauth2_views
import views
urlpatterns = [
url(r'^user/register/$', views.UserRegister.as_view()),
]
You have to create the user using normal Django mechanism (For example, you can add new users from admin or from django shell). However, to get access token, OAuth consumer should send a request to OAuth server where user will authorize it, once the server validates the authorization, it will return the access token.
You must register users separately.
django-oauth-toolkit is necessary if you're going to, e.g., support an Alexa Skill that requires "linking" accounts. Amazon needs a token representing an existing user on your system.
django-allauth makes it easy to provide signup via third parties, e.g. Amazon, Google or Slack. You might consider using it to streamline user registration, e.g. during Alexa account "linking".
For a toy Slack command integration I wrote, I added custom code to create new django users based on their unique Slack user id, skipping the OAuth "sign in with Slack" workflow entirely. Only once those django users exist can django-oauth-toolkit issue tokens for them.
I'm registering user with regular django mechanism combined with django-oauth-toolkit's application client details (client id and client secret key).
I have separate UserRegisterApiView which is not restricted with token authentication but it checks for client id and client secret key while making post request to register a new user. In this way we are restricting register url access to only registered OAuth clients.
Here is the registration workflow:
User registration request from React/Angular/View app with client_id and client_secret.
Django will check if client_id and client_secret are valid if not respond 401 unauthorized.
If valid and register user data is valid, register the user.
On successful response redirect user to login page.
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']})
Hi i am using python flask Flask-Security.
I want to make users confirm their emails but not their passwords.
It doesn't ask the user to enter in their password and another input to confirm passwords are matching.
Instead it just asks the user for one password.
SECURITY_CONFIRMABLE asks users are required to confirm their email address when registering a new account.
I want to users to confirm thier email but not check if passwords are matching on signup.
The RegisterForm from Flask-Security is inherit from ConfirmRegisterForm, PasswordConfirmFormMixin and NextFormMixin. You should import ConfirmRegisterForm and NextFormMixin from Flask-Security and define a custom RegisterForm which only inherit from ConfirmRegisterForm and NextFormMixin.
from flask-security import ConfirmRegisterForm, NextFormMixin
class CustomRegisterForm(ConfirmRegisterForm, NextFormMixin):
def __init__(self, *args, **kwargs):
super(CustomRegisterForm, self).__init__(*args, **kwargs)
if not self.next.data:
self.next.data = request.args.get('next', '')
Rewrite the configuration of Flask-Security:
app["SECURITY_REGISTER_FORM"] = CustomRegisterForm
If you use SECURITY_CONFIRMABLE = True and want to use password confirmation while registration simply add to the config file or application configuration:
from flask_security import RegisterForm
SECURITY_CONFIRM_REGISTER_FORM = RegisterForm
# or use this: app["SECURITY_CONFIRM_REGISTER_FORM"] = RegisterForm
This works with Flask-Security==3.0.0
I am trying to extend SignupForm of django allauth for customizing my signup form. The need is because i want to check if submitted email is accepted for signup or not and it is done by checking if the invite from that email is accepted or not by admin.
I have not define my form in forms.py instead i have used another name 'custom_sign_up_form.py'. Below is my full code with import.
settings.py
ACCOUNT_SIGNUP_FORM_CLASS = 'pages.custom_sign_up_form.CustomSignupForm'
custom_sign_up_form.py
from allauth.account.adapter import DefaultAccountAdapter, get_adapter
from allauth.account.forms import SignupForm
from allauth.account import app_settings
# from django import forms
from invitation.models import Invitation
class CustomSignupForm(SignupForm):
errors = {
'not_invited': "Sorry! You are not yet invited.",
}
def __init__(self, *args, **kwargs):
super(CustomSignupForm, self).__init__(*args, **kwargs)
def clean(self):
super(CustomSignupForm, self).clean()
email = self.cleaned_data.get('email')
email = get_adapter().clean_email(email)
if email and app_settings.UNIQUE_EMAIL:
email = self.validate_unique_email(email)
try:
Invitation.objects.get(email=email, request_approved=True)
except Invitation.DoesNotExist:
# raise forms.ValidationError(errors['Sorry! you are not yet invited'])
self.add_error('email','Sorry! You are not yet invited')
return email
ACCOUNT_SIGNUP_FORM_CLASS is the parent class of SignupForm (BaseSignupForm)...
https://github.com/pennersr/django-allauth/blob/master/allauth/account/forms.py#L197
If you want to change the Form validation logic, you need to use ACCOUNT_ADAPTER.
# project/settings.py:
ACCOUNT_ADAPTER = 'project.users.adapter.MyAccountAdapter'
# project/users/adapter.py:
from django.conf import settings
from allauth.account.adapter import DefaultAccountAdapter
class MyAccountAdapter(DefaultAccountAdapter):
def clean_email(self, email):
"""
Validates an email value. You can hook into this if you want to
(dynamically) restrict what email addresses can be chosen.
"""
try:
Invitation.objects.get(email=email, request_approved=True)
except Invitation.DoesNotExist:
raise forms.ValidationError('Sorry! you are not yet invited')
return email
see also https://stackoverflow.com/a/23328414/7724457