From a43711be70f199e1d4142253d39c337b53edfd8e Mon Sep 17 00:00:00 2001 From: Alejandro Alonso Date: Wed, 8 Oct 2014 09:18:34 +0200 Subject: [PATCH 1/6] Adding cancel_token generation to user on creation --- taiga/users/admin.py | 2 +- .../migrations/0006_user_cancel_token.py | 20 +++++++++++++++++++ taiga/users/models.py | 9 +++++++++ 3 files changed, 30 insertions(+), 1 deletion(-) create mode 100644 taiga/users/migrations/0006_user_cancel_token.py diff --git a/taiga/users/admin.py b/taiga/users/admin.py index a3452616..701e9da6 100644 --- a/taiga/users/admin.py +++ b/taiga/users/admin.py @@ -48,7 +48,7 @@ class UserAdmin(DjangoUserAdmin): fieldsets = ( (None, {'fields': ('username', 'password')}), (_('Personal info'), {'fields': ('full_name', 'email', 'bio', 'photo')}), - (_('Extra info'), {'fields': ('color', 'default_language', 'default_timezone', 'token', 'colorize_tags', 'email_token', 'new_email')}), + (_('Extra info'), {'fields': ('color', 'default_language', 'default_timezone', 'token', 'colorize_tags', 'email_token', 'new_email', 'cancel_token')}), (_('Permissions'), {'fields': ('is_active', 'is_superuser',)}), (_('Important dates'), {'fields': ('last_login', 'date_joined')}), ) diff --git a/taiga/users/migrations/0006_user_cancel_token.py b/taiga/users/migrations/0006_user_cancel_token.py new file mode 100644 index 00000000..d854a31a --- /dev/null +++ b/taiga/users/migrations/0006_user_cancel_token.py @@ -0,0 +1,20 @@ +# -*- coding: utf-8 -*- +from __future__ import unicode_literals + +from django.db import models, migrations + + +class Migration(migrations.Migration): + + dependencies = [ + ('users', '0005_alter_user_photo'), + ] + + operations = [ + migrations.AddField( + model_name='user', + name='cancel_token', + field=models.CharField(default=None, max_length=200, blank=True, null=True, verbose_name='email token'), + preserve_default=True, + ), + ] diff --git a/taiga/users/models.py b/taiga/users/models.py index 2a3c35eb..08423515 100644 --- a/taiga/users/models.py +++ b/taiga/users/models.py @@ -19,6 +19,7 @@ import os import os.path as path import random import re +import uuid from django.db import models from django.dispatch import receiver @@ -123,6 +124,9 @@ class User(AbstractBaseUser, PermissionsMixin): github_id = models.IntegerField(null=True, blank=True, verbose_name=_("github ID")) + cancel_token = models.CharField(max_length=200, null=True, blank=True, default=None, + verbose_name=_("cancel account token")) + USERNAME_FIELD = 'username' REQUIRED_FIELDS = ['email'] @@ -146,6 +150,11 @@ class User(AbstractBaseUser, PermissionsMixin): def get_full_name(self): return self.full_name or self.username or self.email + def save(self, *args, **kwargs): + if not self.cancel_token: + self.cancel_token = str(uuid.uuid1()) + + super().save(*args, **kwargs) class Role(models.Model): name = models.CharField(max_length=200, null=False, blank=False, From 2bfc09b2ee1b1e5c258ebded6bf859dc794cb002 Mon Sep 17 00:00:00 2001 From: Alejandro Alonso Date: Wed, 8 Oct 2014 10:33:08 +0200 Subject: [PATCH 2/6] Adding cancel account by token API --- taiga/users/api.py | 31 +++++++++++++++++------------ taiga/users/models.py | 15 ++++++++++++++ tests/integration/test_users.py | 35 +++++++++++++++++++++++++++++++++ 3 files changed, 68 insertions(+), 13 deletions(-) diff --git a/taiga/users/api.py b/taiga/users/api.py index 8c602b89..6a38ab35 100644 --- a/taiga/users/api.py +++ b/taiga/users/api.py @@ -258,20 +258,25 @@ class UsersViewSet(ModelCrudViewSet): return Response(status=status.HTTP_204_NO_CONTENT) + @list_route(methods=["POST"]) + def cancel(self, request, pk=None): + """ + Cancel an account via token + """ + serializer = serializers.CancelAccountSerializer(data=request.DATA, many=False) + if not serializer.is_valid(): + raise exc.WrongArguments(_("Invalid, are you sure the token is correct?")) + + try: + user = models.User.objects.get(cancel_token=serializer.data["cancel_token"]) + except models.User.DoesNotExist: + raise exc.WrongArguments(_("Invalid, are you sure the token is correct?")) + + user.cancel() + return Response(status=status.HTTP_204_NO_CONTENT) + def destroy(self, request, pk=None): user = self.get_object() self.check_permissions(request, "destroy", user) - user.username = slugify_uniquely("deleted-user", models.User, slugfield="username") - user.email = "{}@taiga.io".format(user.username) - user.is_active = False - user.full_name = "Deleted user" - user.color = "" - user.bio = "" - user.default_language = "" - user.default_timezone = "" - user.colorize_tags = True - user.token = None - user.github_id = None - user.set_unusable_password() - user.save() + user.cancel() return Response(status=status.HTTP_204_NO_CONTENT) diff --git a/taiga/users/models.py b/taiga/users/models.py index 08423515..782dd0b7 100644 --- a/taiga/users/models.py +++ b/taiga/users/models.py @@ -156,6 +156,21 @@ class User(AbstractBaseUser, PermissionsMixin): super().save(*args, **kwargs) + def cancel(self): + self.username = slugify_uniquely("deleted-user", User, slugfield="username") + self.email = "{}@taiga.io".format(self.username) + self.is_active = False + self.full_name = "Deleted user" + self.color = "" + self.bio = "" + self.default_language = "" + self.default_timezone = "" + self.colorize_tags = True + self.token = None + self.github_id = None + self.set_unusable_password() + self.save() + class Role(models.Model): name = models.CharField(max_length=200, null=False, blank=False, verbose_name=_("name")) diff --git a/tests/integration/test_users.py b/tests/integration/test_users.py index 6354fbad..fb79ef24 100644 --- a/tests/integration/test_users.py +++ b/tests/integration/test_users.py @@ -113,3 +113,38 @@ def test_api_user_action_change_email_invalid_token(client): assert response.status_code == 400 assert response.data['_error_message'] == 'Invalid, are you sure the token is correct and you didn\'t use it before?' + + +def test_api_user_delete(client): + user = f.UserFactory.create() + url = reverse('users-detail', kwargs={"pk": user.pk}) + + client.login(user) + response = client.delete(url) + + assert response.status_code == 204 + user = models.User.objects.get(pk=user.id) + assert user.full_name == "Deleted user" + + +def test_api_user_cancel_valid_token(client): + user = f.UserFactory.create() + url = reverse('users-cancel') + data = {"cancel_token": user.cancel_token} + client.login(user) + response = client.post(url, json.dumps(data), content_type="application/json") + + assert response.status_code == 204 + user = models.User.objects.get(pk=user.id) + assert user.full_name == "Deleted user" + + +def test_api_user_cancel_invalid_token(client): + user = f.UserFactory.create() + url = reverse('users-cancel') + data = {"cancel_token": "invalid_cancel_token"} + client.login(user) + response = client.post(url, json.dumps(data), content_type="application/json") + + assert response.status_code == 400 + assert response.data['_error_message'] == "Invalid, are you sure the token is correct?" From cceef80dbd8352727502e540aab79524f6139751 Mon Sep 17 00:00:00 2001 From: Alejandro Alonso Date: Wed, 8 Oct 2014 11:27:43 +0200 Subject: [PATCH 3/6] Adding serializer --- taiga/users/serializers.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/taiga/users/serializers.py b/taiga/users/serializers.py index 2c76b154..8132c7db 100644 --- a/taiga/users/serializers.py +++ b/taiga/users/serializers.py @@ -69,3 +69,7 @@ class RecoverySerializer(serializers.Serializer): class ChangeEmailSerializer(serializers.Serializer): email_token = serializers.CharField(max_length=200) + + +class CancelAccountSerializer(serializers.Serializer): + cancel_token = serializers.CharField(max_length=200) From 227add34f065743ef6757ab614e7a4d93de864ce Mon Sep 17 00:00:00 2001 From: Alejandro Alonso Date: Wed, 8 Oct 2014 13:08:54 +0200 Subject: [PATCH 4/6] Sending email on the registry of user --- taiga/auth/services.py | 25 +++++------------- taiga/front/__init__.py | 1 + .../emails/registered_user-body-html.jinja | 12 +++++++++ .../emails/registered_user-body-text.jinja | 12 +++++++++ .../emails/registered_user-subject.jinja | 1 + tests/integration/test_auth_api.py | 26 ++++++++++++++++++- 6 files changed, 58 insertions(+), 19 deletions(-) create mode 100644 taiga/users/templates/emails/registered_user-body-html.jinja create mode 100644 taiga/users/templates/emails/registered_user-body-text.jinja create mode 100644 taiga/users/templates/emails/registered_user-subject.jinja diff --git a/taiga/auth/services.py b/taiga/auth/services.py index 10c4e573..17209711 100644 --- a/taiga/auth/services.py +++ b/taiga/auth/services.py @@ -38,28 +38,15 @@ from taiga.users.services import get_and_validate_user from .backends import get_token_for_user from .signals import user_registered as user_registered_signal -def send_public_register_email(user) -> bool: +def send_register_email(user) -> bool: """ - Given a user, send public register welcome email + Given a user, send register welcome email message to specified user. """ context = {"user": user} mbuilder = MagicMailBuilder() - email = mbuilder.public_register_user(user.email, context) - return bool(email.send()) - - -def send_private_register_email(user, **kwargs) -> bool: - """ - Given a user, send private register welcome - email message to specified user. - """ - context = {"user": user} - context.update(kwargs) - - mbuilder = MagicMailBuilder() - email = mbuilder.private_register_user(user.email, context) + email = mbuilder.registered_user(user.email, context) return bool(email.send()) @@ -125,7 +112,7 @@ def public_register(username:str, password:str, email:str, full_name:str): except IntegrityError: raise exc.WrongArguments("User is already register.") - # send_public_register_email(user) + send_register_email(user) user_registered_signal.send(sender=user.__class__, user=user) return user @@ -149,7 +136,7 @@ def private_register_for_existing_user(token:str, username:str, password:str): except IntegrityError: raise exc.IntegrityError("Membership with user is already exists.") - # send_private_register_email(user) + send_register_email(user) return user @@ -178,6 +165,7 @@ def private_register_for_new_user(token:str, username:str, email:str, membership = get_membership_by_token(token) membership.user = user membership.save(update_fields=["user"]) + send_register_email(user) user_registered_signal.send(sender=user.__class__, user=user) return user @@ -205,6 +193,7 @@ def github_register(username:str, email:str, full_name:str, github_id:int, bio:s membership.save(update_fields=["user"]) if created: + send_register_email(user) user_registered_signal.send(sender=user.__class__, user=user) return user diff --git a/taiga/front/__init__.py b/taiga/front/__init__.py index be3ca27e..1b2d3535 100644 --- a/taiga/front/__init__.py +++ b/taiga/front/__init__.py @@ -22,6 +22,7 @@ urls = { "login": "/login", "change-password": "/change-password/{0}", "change-email": "/change-email/{0}", + "cancel-account": "/cancel-account/{0}", "invitation": "/invitation/{0}", "project": "/project/{0}", diff --git a/taiga/users/templates/emails/registered_user-body-html.jinja b/taiga/users/templates/emails/registered_user-body-html.jinja new file mode 100644 index 00000000..9db5151a --- /dev/null +++ b/taiga/users/templates/emails/registered_user-body-html.jinja @@ -0,0 +1,12 @@ +Welcome to Taiga, an Open Source, Agile Project Management Tool + +You, or someone you know has invited you to Taiga. You may remove your account from this service by clicking here: + +{{ resolve_front_url('cancel-account', user.cancel_token) }} + +We built Taiga because we wanted the project management tool that sits open on our computers all day long, to serve as a continued reminder of why we love to collaborate, code and design. We built it to be beautiful, elegant, simple to use and fun - without forsaking flexibility and power. We named it Taiga after the forest biome, because like its namesake, our tool is at the center of an ecosystem - your team, your projects. A great project management tool also helps you see the forest for the trees. We couldn't think of a more appropriate name for what we've done. + +We hope you enjoy it. + +-- +The Taiga development team. diff --git a/taiga/users/templates/emails/registered_user-body-text.jinja b/taiga/users/templates/emails/registered_user-body-text.jinja new file mode 100644 index 00000000..9db5151a --- /dev/null +++ b/taiga/users/templates/emails/registered_user-body-text.jinja @@ -0,0 +1,12 @@ +Welcome to Taiga, an Open Source, Agile Project Management Tool + +You, or someone you know has invited you to Taiga. You may remove your account from this service by clicking here: + +{{ resolve_front_url('cancel-account', user.cancel_token) }} + +We built Taiga because we wanted the project management tool that sits open on our computers all day long, to serve as a continued reminder of why we love to collaborate, code and design. We built it to be beautiful, elegant, simple to use and fun - without forsaking flexibility and power. We named it Taiga after the forest biome, because like its namesake, our tool is at the center of an ecosystem - your team, your projects. A great project management tool also helps you see the forest for the trees. We couldn't think of a more appropriate name for what we've done. + +We hope you enjoy it. + +-- +The Taiga development team. diff --git a/taiga/users/templates/emails/registered_user-subject.jinja b/taiga/users/templates/emails/registered_user-subject.jinja new file mode 100644 index 00000000..527a27a8 --- /dev/null +++ b/taiga/users/templates/emails/registered_user-subject.jinja @@ -0,0 +1 @@ +You've been Taigatized! diff --git a/tests/integration/test_auth_api.py b/tests/integration/test_auth_api.py index 56a8140f..6db03f83 100644 --- a/tests/integration/test_auth_api.py +++ b/tests/integration/test_auth_api.py @@ -20,10 +20,13 @@ from unittest.mock import patch, Mock from django.apps import apps from django.core.urlresolvers import reverse +from django.core import mail + from .. import factories from taiga.base.connectors import github - +from taiga.front import resolve as resolve_front_url +from taiga.users import models pytestmark = pytest.mark.django_db @@ -68,6 +71,27 @@ def test_respond_201_with_invitation_without_public_registration(client, registe assert response.status_code == 201, response.data +def test_response_200_in_public_registration(client, settings): + settings.PUBLIC_REGISTER_ENABLED = True + form = { + "type": "public", + "username": "mmcfly", + "full_name": "martin seamus mcfly", + "email": "mmcfly@bttf.com", + "password": "password", + } + + response = client.post(reverse("auth-register"), form) + assert response.status_code == 201 + assert response.data["username"] == "mmcfly" + assert response.data["email"] == "mmcfly@bttf.com" + assert response.data["full_name"] == "martin seamus mcfly" + assert len(mail.outbox) == 1 + assert mail.outbox[0].subject == "You've been Taigatized!" + user = models.User.objects.get(username="mmcfly") + cancel_url = resolve_front_url("cancel-account", user.cancel_token) + assert mail.outbox[0].body.index(cancel_url) > 0 + def test_response_200_in_registration_with_github_account(client, settings): settings.PUBLIC_REGISTER_ENABLED = False form = {"type": "github", From 4404a58b45375f7df5995a6679cfd0c6f8c44c10 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Xavier=20Juli=C3=A1n?= Date: Thu, 9 Oct 2014 09:59:10 +0200 Subject: [PATCH 5/6] Basic email template layout --- .../emails/registered_user-body-html.jinja | 29 ++++++++++++++----- 1 file changed, 22 insertions(+), 7 deletions(-) diff --git a/taiga/users/templates/emails/registered_user-body-html.jinja b/taiga/users/templates/emails/registered_user-body-html.jinja index 9db5151a..ba48feb9 100644 --- a/taiga/users/templates/emails/registered_user-body-html.jinja +++ b/taiga/users/templates/emails/registered_user-body-html.jinja @@ -1,12 +1,27 @@ -Welcome to Taiga, an Open Source, Agile Project Management Tool +{% extends "emails/base.jinja" %} -You, or someone you know has invited you to Taiga. You may remove your account from this service by clicking here: +{% block body %} + + + + +
+

Welcome to Taiga, an Open Source, Agile Project Management Tool

-{{ resolve_front_url('cancel-account', user.cancel_token) }} +

You, or someone you know has invited you to Taiga. You may remove your account from this service by clicking here:

-We built Taiga because we wanted the project management tool that sits open on our computers all day long, to serve as a continued reminder of why we love to collaborate, code and design. We built it to be beautiful, elegant, simple to use and fun - without forsaking flexibility and power. We named it Taiga after the forest biome, because like its namesake, our tool is at the center of an ecosystem - your team, your projects. A great project management tool also helps you see the forest for the trees. We couldn't think of a more appropriate name for what we've done. + {{ resolve_front_url('cancel-account', user.cancel_token) }} + +

We built Taiga because we wanted the project management tool that sits open on our computers all day long, to serve as a continued reminder of why we love to collaborate, code and design. We built it to be beautiful, elegant, simple to use and fun - without forsaking flexibility and power. We named it Taiga after the forest biome, because like its namesake, our tool is at the center of an ecosystem - your team, your projects. A great project management tool also helps you see the forest for the trees. We couldn't think of a more appropriate name for what we've done.

+ +

We hope you enjoy it.

+
+{% endblock %} + +{% block footer %} +

+ The Taiga development team. +

+{% endblock %} -We hope you enjoy it. --- -The Taiga development team. From 4b859bbde98be15279959e36737fe8d38659ed57 Mon Sep 17 00:00:00 2001 From: Alejandro Alonso Date: Thu, 9 Oct 2014 18:06:17 +0200 Subject: [PATCH 6/6] Removing cancel_token and using django.core.signing stuff --- settings/common.py | 3 ++ taiga/auth/backends.py | 43 +++------------ taiga/auth/services.py | 8 +-- taiga/auth/tokens.py | 54 +++++++++++++++++++ taiga/users/admin.py | 2 +- taiga/users/api.py | 8 ++- .../migrations/0006_user_cancel_token.py | 20 ------- taiga/users/models.py | 8 +-- .../emails/registered_user-body-html.jinja | 5 +- .../emails/registered_user-body-text.jinja | 2 +- tests/integration/test_auth_api.py | 4 +- tests/integration/test_users.py | 4 +- 12 files changed, 84 insertions(+), 77 deletions(-) create mode 100644 taiga/auth/tokens.py delete mode 100644 taiga/users/migrations/0006_user_cancel_token.py diff --git a/settings/common.py b/settings/common.py index d726741f..9834a2de 100644 --- a/settings/common.py +++ b/settings/common.py @@ -271,6 +271,9 @@ AUTHENTICATION_BACKENDS = ( "django.contrib.auth.backends.ModelBackend", # default ) +MAX_AGE_AUTH_TOKEN = None +MAX_AGE_CANCEL_ACCOUNT = 30 * 24 * 60 * 60 # 30 days in seconds + ANONYMOUS_USER_ID = -1 MAX_SEARCH_RESULTS = 100 diff --git a/taiga/auth/backends.py b/taiga/auth/backends.py index d0331fdf..b514f1e7 100644 --- a/taiga/auth/backends.py +++ b/taiga/auth/backends.py @@ -35,11 +35,10 @@ fraudulent modifications. import base64 import re -from django.core import signing -from django.apps import apps +from django.conf import settings from rest_framework.authentication import BaseAuthentication -from taiga.base import exceptions as exc +from .tokens import get_user_for_token class Session(BaseAuthentication): """ @@ -62,39 +61,6 @@ class Session(BaseAuthentication): return (user, None) -def get_token_for_user(user): - """ - Generate a new signed token containing - a specified user. - """ - data = {"user_id": user.id} - return signing.dumps(data) - - -def get_user_for_token(token): - """ - Given a selfcontained token, try parse and - unsign it. - - If token passes a validation, returns - a user instance corresponding with user_id stored - in the incoming token. - """ - try: - data = signing.loads(token) - except signing.BadSignature: - raise exc.NotAuthenticated("Invalid token") - - model_cls = apps.get_model("users", "User") - - try: - user = model_cls.objects.get(pk=data["user_id"]) - except model_cls.DoesNotExist: - raise exc.NotAuthenticated("Invalid token") - else: - return user - - class Token(BaseAuthentication): """ Self-contained stateles authentication implementatrion @@ -114,7 +80,10 @@ class Token(BaseAuthentication): return None token = token_rx_match.group(1) - user = get_user_for_token(token) + max_age_auth_token = getattr(settings, "MAX_AGE_AUTH_TOKEN", None) + user = get_user_for_token(token, "authentication", + max_age=max_age_auth_token) + return (user, token) def authenticate_header(self, request): diff --git a/taiga/auth/services.py b/taiga/auth/services.py index 17209711..348e41d4 100644 --- a/taiga/auth/services.py +++ b/taiga/auth/services.py @@ -35,7 +35,7 @@ from taiga.base import exceptions as exc from taiga.users.serializers import UserSerializer from taiga.users.services import get_and_validate_user -from .backends import get_token_for_user +from .tokens import get_token_for_user from .signals import user_registered as user_registered_signal def send_register_email(user) -> bool: @@ -43,8 +43,8 @@ def send_register_email(user) -> bool: Given a user, send register welcome email message to specified user. """ - - context = {"user": user} + cancel_token = get_token_for_user(user, "cancel_account") + context = {"user": user, "cancel_token": cancel_token} mbuilder = MagicMailBuilder() email = mbuilder.registered_user(user.email, context) return bool(email.send()) @@ -207,5 +207,5 @@ def make_auth_response_data(user) -> dict: """ serializer = UserSerializer(user) data = dict(serializer.data) - data["auth_token"] = get_token_for_user(user) + data["auth_token"] = get_token_for_user(user, "authentication") return data diff --git a/taiga/auth/tokens.py b/taiga/auth/tokens.py new file mode 100644 index 00000000..6b5afd7b --- /dev/null +++ b/taiga/auth/tokens.py @@ -0,0 +1,54 @@ +# Copyright (C) 2014 Andrey Antukh +# Copyright (C) 2014 Jesús Espino +# Copyright (C) 2014 David Barragán +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License as +# published by the Free Software Foundation, either version 3 of the +# License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Affero General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see . + +from taiga.base import exceptions as exc + +from django.apps import apps +from django.core import signing + +def get_token_for_user(user, scope): + """ + Generate a new signed token containing + a specified user limited for a scope (identified as a string). + """ + data = {"user_%s_id"%(scope): user.id} + return signing.dumps(data) + + +def get_user_for_token(token, scope, max_age=None): + """ + Given a selfcontained token and a scope try to parse and + unsign it. + + If max_age is specified it checks token expiration. + + If token passes a validation, returns + a user instance corresponding with user_id stored + in the incoming token. + """ + try: + data = signing.loads(token, max_age=max_age) + except signing.BadSignature: + raise exc.NotAuthenticated("Invalid token") + + model_cls = apps.get_model("users", "User") + + try: + user = model_cls.objects.get(pk=data["user_%s_id"%(scope)]) + except model_cls.DoesNotExist: + raise exc.NotAuthenticated("Invalid token") + else: + return user diff --git a/taiga/users/admin.py b/taiga/users/admin.py index 701e9da6..a3452616 100644 --- a/taiga/users/admin.py +++ b/taiga/users/admin.py @@ -48,7 +48,7 @@ class UserAdmin(DjangoUserAdmin): fieldsets = ( (None, {'fields': ('username', 'password')}), (_('Personal info'), {'fields': ('full_name', 'email', 'bio', 'photo')}), - (_('Extra info'), {'fields': ('color', 'default_language', 'default_timezone', 'token', 'colorize_tags', 'email_token', 'new_email', 'cancel_token')}), + (_('Extra info'), {'fields': ('color', 'default_language', 'default_timezone', 'token', 'colorize_tags', 'email_token', 'new_email')}), (_('Permissions'), {'fields': ('is_active', 'is_superuser',)}), (_('Important dates'), {'fields': ('last_login', 'date_joined')}), ) diff --git a/taiga/users/api.py b/taiga/users/api.py index 6a38ab35..7e2a0105 100644 --- a/taiga/users/api.py +++ b/taiga/users/api.py @@ -22,6 +22,7 @@ from django.shortcuts import get_object_or_404 from django.utils.translation import ugettext_lazy as _ from django.core.validators import validate_email from django.core.exceptions import ValidationError +from django.conf import settings from easy_thumbnails.source_generators import pil_image @@ -32,6 +33,7 @@ from rest_framework import status from djmail.template_mail import MagicMailBuilder +from taiga.auth.tokens import get_user_for_token from taiga.base.decorators import list_route, detail_route from taiga.base import exceptions as exc from taiga.base.api import ModelCrudViewSet @@ -268,8 +270,10 @@ class UsersViewSet(ModelCrudViewSet): raise exc.WrongArguments(_("Invalid, are you sure the token is correct?")) try: - user = models.User.objects.get(cancel_token=serializer.data["cancel_token"]) - except models.User.DoesNotExist: + max_age_cancel_account = getattr(settings, "MAX_AGE_CANCEL_ACCOUNT", None) + user = get_user_for_token(serializer.data["cancel_token"], "cancel_account", + max_age=max_age_cancel_account) + except exc.NotAuthenticated: raise exc.WrongArguments(_("Invalid, are you sure the token is correct?")) user.cancel() diff --git a/taiga/users/migrations/0006_user_cancel_token.py b/taiga/users/migrations/0006_user_cancel_token.py deleted file mode 100644 index d854a31a..00000000 --- a/taiga/users/migrations/0006_user_cancel_token.py +++ /dev/null @@ -1,20 +0,0 @@ -# -*- coding: utf-8 -*- -from __future__ import unicode_literals - -from django.db import models, migrations - - -class Migration(migrations.Migration): - - dependencies = [ - ('users', '0005_alter_user_photo'), - ] - - operations = [ - migrations.AddField( - model_name='user', - name='cancel_token', - field=models.CharField(default=None, max_length=200, blank=True, null=True, verbose_name='email token'), - preserve_default=True, - ), - ] diff --git a/taiga/users/models.py b/taiga/users/models.py index 782dd0b7..e38f5417 100644 --- a/taiga/users/models.py +++ b/taiga/users/models.py @@ -31,6 +31,7 @@ from django.utils.encoding import force_bytes from djorm_pgarray.fields import TextArrayField +from taiga.auth.tokens import get_token_for_user from taiga.base.utils.slug import slugify_uniquely from taiga.base.utils.iterators import split_by_n from taiga.permissions.permissions import MEMBERS_PERMISSIONS @@ -124,9 +125,6 @@ class User(AbstractBaseUser, PermissionsMixin): github_id = models.IntegerField(null=True, blank=True, verbose_name=_("github ID")) - cancel_token = models.CharField(max_length=200, null=True, blank=True, default=None, - verbose_name=_("cancel account token")) - USERNAME_FIELD = 'username' REQUIRED_FIELDS = ['email'] @@ -151,9 +149,7 @@ class User(AbstractBaseUser, PermissionsMixin): return self.full_name or self.username or self.email def save(self, *args, **kwargs): - if not self.cancel_token: - self.cancel_token = str(uuid.uuid1()) - + get_token_for_user(self, "cancel_account") super().save(*args, **kwargs) def cancel(self): diff --git a/taiga/users/templates/emails/registered_user-body-html.jinja b/taiga/users/templates/emails/registered_user-body-html.jinja index ba48feb9..e82d0606 100644 --- a/taiga/users/templates/emails/registered_user-body-html.jinja +++ b/taiga/users/templates/emails/registered_user-body-html.jinja @@ -7,8 +7,7 @@

Welcome to Taiga, an Open Source, Agile Project Management Tool

You, or someone you know has invited you to Taiga. You may remove your account from this service by clicking here:

- - {{ resolve_front_url('cancel-account', user.cancel_token) }} + {{ resolve_front_url('cancel-account', cancel_token) }}

We built Taiga because we wanted the project management tool that sits open on our computers all day long, to serve as a continued reminder of why we love to collaborate, code and design. We built it to be beautiful, elegant, simple to use and fun - without forsaking flexibility and power. We named it Taiga after the forest biome, because like its namesake, our tool is at the center of an ecosystem - your team, your projects. A great project management tool also helps you see the forest for the trees. We couldn't think of a more appropriate name for what we've done.

@@ -23,5 +22,3 @@ The Taiga development team.

{% endblock %} - - diff --git a/taiga/users/templates/emails/registered_user-body-text.jinja b/taiga/users/templates/emails/registered_user-body-text.jinja index 9db5151a..dde7e8b0 100644 --- a/taiga/users/templates/emails/registered_user-body-text.jinja +++ b/taiga/users/templates/emails/registered_user-body-text.jinja @@ -2,7 +2,7 @@ Welcome to Taiga, an Open Source, Agile Project Management Tool You, or someone you know has invited you to Taiga. You may remove your account from this service by clicking here: -{{ resolve_front_url('cancel-account', user.cancel_token) }} +{{ resolve_front_url('cancel-account', cancel_token) }} We built Taiga because we wanted the project management tool that sits open on our computers all day long, to serve as a continued reminder of why we love to collaborate, code and design. We built it to be beautiful, elegant, simple to use and fun - without forsaking flexibility and power. We named it Taiga after the forest biome, because like its namesake, our tool is at the center of an ecosystem - your team, your projects. A great project management tool also helps you see the forest for the trees. We couldn't think of a more appropriate name for what we've done. diff --git a/tests/integration/test_auth_api.py b/tests/integration/test_auth_api.py index 6db03f83..60dced10 100644 --- a/tests/integration/test_auth_api.py +++ b/tests/integration/test_auth_api.py @@ -27,6 +27,7 @@ from .. import factories from taiga.base.connectors import github from taiga.front import resolve as resolve_front_url from taiga.users import models +from taiga.auth.tokens import get_token_for_user pytestmark = pytest.mark.django_db @@ -89,7 +90,8 @@ def test_response_200_in_public_registration(client, settings): assert len(mail.outbox) == 1 assert mail.outbox[0].subject == "You've been Taigatized!" user = models.User.objects.get(username="mmcfly") - cancel_url = resolve_front_url("cancel-account", user.cancel_token) + cancel_token = get_token_for_user(user, "cancel_account") + cancel_url = resolve_front_url("cancel-account", cancel_token) assert mail.outbox[0].body.index(cancel_url) > 0 def test_response_200_in_registration_with_github_account(client, settings): diff --git a/tests/integration/test_users.py b/tests/integration/test_users.py index fb79ef24..2beb3762 100644 --- a/tests/integration/test_users.py +++ b/tests/integration/test_users.py @@ -6,6 +6,7 @@ from django.core.urlresolvers import reverse from .. import factories as f from taiga.users import models +from taiga.auth.tokens import get_token_for_user pytestmark = pytest.mark.django_db @@ -130,7 +131,8 @@ def test_api_user_delete(client): def test_api_user_cancel_valid_token(client): user = f.UserFactory.create() url = reverse('users-cancel') - data = {"cancel_token": user.cancel_token} + cancel_token = get_token_for_user(user, "cancel_account") + data = {"cancel_token": cancel_token} client.login(user) response = client.post(url, json.dumps(data), content_type="application/json")