Changeset 76ebb1b in OpenWorkouts-current


Ignore:
Timestamp:
Feb 18, 2019, 12:54:45 PM (4 years ago)
Author:
Borja Lopez <borja@…>
Branches:
current, feature/docs, master
Children:
4af38e8
Parents:
d6da99e
Message:

(#29) Add user verification by email on signup.

From now on, when a new user signs up, we set the account into an "unverified"
state. In order to complete the signup procedure, the user has to click on a
link we send by email to the email address provided on signup.

IMPORTANT: A new dependency has been added, pyramid_mailer, so remember to
install it in any existing openworkouts environment (this is done automatically
if you use the ./bin/start script):

pip install pyramid_mailer

Files:
5 added
7 edited

Legend:

Unmodified
Added
Removed
  • development.ini

    rd6da99e r76ebb1b  
    3030{BT]KrSx`b3pmRj<Z&e3QP|fgPGEZT@\#
    3131auth.secret = l9|^@~wQoVKPQoI`GHK5M9ps@S7L:QNU?pF}.jI(9RWZVc<EM)aQv/j~l\#xC++;5
     32
     33# pyramid_mailer configuration
     34mail.default_sender = noreply@openworkouts.org
     35mail.queue_path = %(here)s/var/spool/mqueue
     36mail.queue_processor_lock = %(here)s/var/run/mail-queue-processor.lock
     37mail.host = mail.openworkouts.org
     38mail.tls = True
     39mail.username = noreply@openworkouts.org
     40mail.password = PASSWORD
    3241
    3342
  • ow/__init__.py

    rd6da99e r76ebb1b  
    3737    config.include('pyramid_retry')
    3838    config.include('pyramid_zodbconn')
     39    config.include('pyramid_mailer')
    3940    config.add_static_view('static', 'static', cache_max_age=3600)
    4041    config.add_translation_dirs(
  • ow/models/user.py

    rd6da99e r76ebb1b  
    3939        self.__password = None
    4040        self.last_workout_id = 0
     41        # has this user verified his account?
     42        self.verified = False
     43        self.verification_token = None
    4144        super(User, self).__init__()
    4245
  • ow/tests/views/test_user.py

    rd6da99e r76ebb1b  
    66from unittest.mock import Mock, patch
    77from io import BytesIO
     8from uuid import UUID
    89
    910import pytest
     
    112113            })
    113114        return request
     115
     116    @patch('ow.views.user.remember')
     117    def test_verify_already_verified(self, remember, dummy_request, john):
     118        john.verified = True
     119        response = user_views.verify(john, dummy_request)
     120        assert isinstance(response, HTTPFound)
     121        assert response.location == dummy_request.resource_url(john)
     122        # user was not authenticated
     123        assert not remember.called
     124        # verified status did not change
     125        assert john.verified
     126
     127    @patch('ow.views.user.remember')
     128    def test_verify_no_subpath(self, remember, dummy_request, john):
     129        response = user_views.verify(john, dummy_request)
     130        # the verify info page is rendered, we don't pass anything to the
     131        # rendering context
     132        assert response == {}
     133        # user was not authenticated
     134        assert not remember.called
     135        # verified status did not change
     136        assert not john.verified
     137
     138    def test_verify_subpath_not_verified(self, dummy_request, john):
     139        dummy_request.subpath = ['not_the_token']
     140        response = user_views.verify(john, dummy_request)
     141        # the verify info page is rendered, we don't pass anything to the
     142        # rendering context
     143        assert response == {}
     144
     145    @patch('ow.views.user.remember')
     146    def test_verify_wrong_token(self, remember, dummy_request, john):
     147        token = 'some-uuid4'
     148        john.verification_token = 'some-other-uuid4'
     149        dummy_request.subpath = [token]
     150        response = user_views.verify(john, dummy_request)
     151        # the verify info page is rendered, we don't pass anything to the
     152        # rendering context
     153        assert response == {}
     154        # user was not authenticated
     155        assert not remember.called
     156        # verified status did not change, neither did the token
     157        assert not john.verified
     158        assert john.verification_token == 'some-other-uuid4'
     159
     160    @patch('ow.views.user.remember')
     161    def test_verify_verifying(self, remember, dummy_request, john):
     162        token = 'some-uuid4'
     163        john.verification_token = token
     164        dummy_request.subpath = [token]
     165        response = user_views.verify(john, dummy_request)
     166        # redirect to user page
     167        assert isinstance(response, HTTPFound)
     168        assert response.location == dummy_request.resource_url(john)
     169        # user was authenticated after verified
     170        remember.assert_called_with(dummy_request, str(john.uid))
     171        # user has been verified
     172        assert john.verified
    114173
    115174    def test_dashboard_redirect_unauthenticated(self, root):
     
    369428        request.POST['email'] = 'john.doe@example.net'
    370429        request.POST['password'] = 'badpassword'
     430        # verify the user first
     431        request.root.users[0].verified = True
    371432        response = user_views.login(request.root, request)
    372433        assert response['message'] == u'Wrong password'
    373434
    374435    @patch('ow.views.user.remember')
    375     def test_login_post_ok(self, rem, dummy_request, john):
     436    def test_login_post_unverified(self, rem, dummy_request, john):
    376437        request = dummy_request
    377438        request.method = 'POST'
     
    379440        request.POST['email'] = 'john.doe@example.net'
    380441        request.POST['password'] = 's3cr3t'
     442        response = user_views.login(request.root, request)
     443        assert response['message'] == u'You have to verify your account first'
     444
     445    @patch('ow.views.user.remember')
     446    def test_login_post_ok(self, rem, dummy_request, john):
     447        request = dummy_request
     448        request.method = 'POST'
     449        request.POST['submit'] = True
     450        request.POST['email'] = 'john.doe@example.net'
     451        request.POST['password'] = 's3cr3t'
     452        # verify the user first
     453        john.verified = True
    381454        response = user_views.login(request.root, request)
    382455        assert isinstance(response, HTTPFound)
     
    620693        assert response['form'].errorlist() == ''
    621694
    622     def test_signup_post_ok(self, signup_post_request):
     695    @patch('ow.views.user.send_verification_email')
     696    def test_signup_post_ok(self, sve, signup_post_request):
    623697        request = signup_post_request
    624698        assert 'jack.black@example.net' not in request.root.emails
     
    629703        assert 'jack.black@example.net' in request.root.emails
    630704        assert 'JackBlack' in request.root.all_nicknames
     705        # user is in "unverified" state
     706        user = request.root.get_user_by_email('jack.black@example.net')
     707        assert not user.verified
     708        assert isinstance(user.verification_token, UUID)
     709        # also, we sent an email to that user
     710        sve.assert_called_once_with(request, user)
    631711
    632712    def test_signup_missing_required(self, signup_post_request):
  • ow/utilities.py

    rd6da99e r76ebb1b  
    88from decimal import Decimal
    99from shutil import copyfileobj
     10from uuid import uuid4
    1011
    1112from unidecode import unidecode
     
    2021
    2122log = logging.getLogger(__name__)
     23
     24
     25def get_verification_token():
     26    """
     27    Generate a new uuid4 verification token we can give a user for
     28    verification purposes.
     29    uuid4 is a standard that generates a randomly generated token,
     30    optimized for a very low chance of collisions. But even if
     31    we had a collision, it wouldn't matter - it's simple some users
     32    getting the same token in their verification mail.
     33    """
     34    return uuid4()
    2235
    2336
  • ow/views/user.py

    rd6da99e r76ebb1b  
    2222from ..models.root import OpenWorkouts
    2323from ..views.renderers import OWFormRenderer
    24 from ..utilities import timedelta_to_hms
     24from ..utilities import timedelta_to_hms, get_verification_token
     25from ..mail import send_verification_email
    2526
    2627_ = TranslationStringFactory('OpenWorkouts')
     
    5960        user = context.get_user_by_email(email)
    6061        if user:
    61             password = request.POST.get('password', None)
    62             if password is not None and user.check_password(password):
    63                 headers = remember(request, str(user.uid))
    64                 redirect_url = return_to or request.resource_url(user)
    65                 return HTTPFound(location=redirect_url, headers=headers)
     62            if user.verified:
     63                password = request.POST.get('password', None)
     64                if password is not None and user.check_password(password):
     65                    headers = remember(request, str(user.uid))
     66                    redirect_url = return_to or request.resource_url(user)
     67                    return HTTPFound(location=redirect_url, headers=headers)
     68                else:
     69                    message = _('Wrong password')
    6670            else:
    67                 message = _('Wrong password')
     71                message = _('You have to verify your account first')
    6872        else:
    6973            message = _('Wrong email address')
     
    9498    if 'submit' in request.POST and form.validate():
    9599        user = form.bind(User(), exclude=['password_confirm'])
     100        user.verified = False
     101        user.verification_token = get_verification_token()
    96102        context.add_user(user)
     103        # send a verification link to the user email address
     104        send_verification_email(request, user)
    97105        # Send to login
    98106        return HTTPFound(location=request.resource_url(context))
     
    101109        'form': OWFormRenderer(form)
    102110    }
     111
     112
     113@view_config(
     114    context=User,
     115    name="verify",
     116    renderer='ow:templates/verify.pt')
     117def verify(context, request):
     118    redirect_url = request.resource_url(context)
     119
     120    # user has been verified already, send to dashboard
     121    if getattr(context, 'verified', False):
     122        return HTTPFound(location=redirect_url)
     123
     124    # Look for a verification token, then check if we can verify the user with
     125    # that token
     126    verified = len(request.subpath) > 0
     127    token = getattr(context, 'verification_token', False)
     128    verified = verified and token and str(token) == request.subpath[0]
     129    if verified:
     130        # verified, log in automatically and send to the dashboard
     131        context.verified = True
     132        headers = remember(request, str(context.uid))
     133        return HTTPFound(location=redirect_url, headers=headers)
     134
     135    # if we can not verify the user, show a page with some info about it
     136    return {}
    103137
    104138
  • setup.py

    rd6da99e r76ebb1b  
    1717    'pyramid_tm',
    1818    'pyramid_zodbconn',
     19    'pyramid_simpleform==0.7dev0',  # version needed for python3
     20    'pyramid_mailer',
    1921    'transaction',
    2022    'ZODB3',
     
    2527    'bcrypt',
    2628    'FormEncode',
    27     'pyramid_simpleform==0.7dev0',  # version needed for python3
    2829    'unidecode',
    2930    'gpxpy',
Note: See TracChangeset for help on using the changeset viewer.