From 2a500b3e4a7dfab349b146136a0925e5bc88d760 Mon Sep 17 00:00:00 2001 From: Jens Langhammer Date: Mon, 10 Dec 2018 13:51:38 +0100 Subject: [PATCH] core: add placeholders for forms, add sign-up view --- passbook/core/forms/authentication.py | 62 ++++++++++++++++- passbook/core/views/authentication.py | 99 ++++++++++++++++++++++++++- 2 files changed, 157 insertions(+), 4 deletions(-) diff --git a/passbook/core/forms/authentication.py b/passbook/core/forms/authentication.py index 72c76136c..89b7520a0 100644 --- a/passbook/core/forms/authentication.py +++ b/passbook/core/forms/authentication.py @@ -1,16 +1,22 @@ """passbook core authentication forms""" +from logging import getLogger from django import forms +from django.core.exceptions import ValidationError from django.core.validators import validate_email +from django.utils.translation import gettext_lazy as _ +from passbook.core.models import User from passbook.lib.config import CONFIG +LOGGER = getLogger(__name__) class LoginForm(forms.Form): """Allow users to login""" - uid_field = forms.CharField() - password = forms.CharField(widget=forms.PasswordInput()) + title = _('Log in to your account') + uid_field = forms.CharField(widget=forms.TextInput(attrs={'placeholder': _('UID')})) + password = forms.CharField(widget=forms.PasswordInput(attrs={'placeholder': _('Password')})) remember_me = forms.BooleanField(required=False) def clean_uid_field(self): @@ -18,3 +24,55 @@ class LoginForm(forms.Form): if CONFIG.y('passbook.uid_fields') == ['email']: validate_email(self.cleaned_data.get('uid_field')) return self.cleaned_data.get('uid_field') + +class SignUpForm(forms.Form): + """SignUp Form""" + + title = _('Sign Up') + first_name = forms.CharField(label=_('First Name'), + widget=forms.TextInput(attrs={'placeholder': _('First Name')})) + last_name = forms.CharField(label=_('Last Name'), + widget=forms.TextInput(attrs={'placeholder': _('Last Name')})) + username = forms.CharField(label=_('Username'), + widget=forms.TextInput(attrs={'placeholder': _('Username')})) + email = forms.EmailField(label=_('E-Mail'), + widget=forms.TextInput(attrs={'placeholder': _('E-Mail')})) + password = forms.CharField(label=_('Password'), + widget=forms.PasswordInput(attrs={'placeholder': _('Password')})) + password_repeat = forms.CharField(label=_('Repeat Password'), + widget=forms.PasswordInput(attrs={ + 'placeholder': _('Repeat Password') + })) + # captcha = ReCaptchaField( + # required=(not settings.DEBUG and not settings.TEST), + # private_key=Setting.get('recaptcha:private'), + # public_key=Setting.get('recaptcha:public')) + + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + # TODO: Dynamically add captcha here + # if not Setting.get_bool('recaptcha:enabled'): + # self.fields.pop('captcha') + + def clean_username(self): + """Check if username is used already""" + username = self.cleaned_data.get('username') + if User.objects.filter(username=username).exists(): + LOGGER.warning("Username %s already exists", username) + raise ValidationError(_("Username already exists")) + return username + + def clean_email(self): + """Check if email is already used in django or other auth sources""" + email = self.cleaned_data.get('email') + # Check if user exists already, error early + if User.objects.filter(email=email).exists(): + LOGGER.debug("email %s exists in django", email) + raise ValidationError(_("Email already exists")) + return email + + def clean_password_repeat(self): + """Check if Password adheres to filter and if passwords matche""" + # TODO: Password policy? Via Plugin? via Policy? + # return check_password(self) + return self.cleaned_data.get('password_repeat') diff --git a/passbook/core/views/authentication.py b/passbook/core/views/authentication.py index 83bc01603..3681bc89a 100644 --- a/passbook/core/views/authentication.py +++ b/passbook/core/views/authentication.py @@ -11,8 +11,8 @@ from django.utils.translation import ugettext as _ from django.views import View from django.views.generic import FormView -from passbook.core.forms.authentication import LoginForm -from passbook.core.models import User +from passbook.core.forms.authentication import LoginForm, SignUpForm +from passbook.core.models import Invite, User from passbook.lib.config import CONFIG LOGGER = getLogger(__name__) @@ -41,6 +41,10 @@ class LoginView(UserPassesTestMixin, FormView): def get_context_data(self, **kwargs): kwargs['config'] = CONFIG.get('passbook') kwargs['is_login'] = True + kwargs['title'] = _('Log in to your account') + kwargs['primary_action'] = _('Log in') + kwargs['show_sign_up_notice'] = CONFIG.y('passbook.sign_up.enabled') + kwargs['show_password_forget_notice'] = CONFIG.y('passbook.password_reset.enabled') return super().get_context_data(**kwargs) def get_user(self, uid_value) -> User: @@ -105,3 +109,94 @@ class LogoutView(LoginRequiredMixin, View): logout(request) messages.success(request, _("You've successfully been logged out.")) return redirect(reverse('passbook_core:auth-login')) + + +class SignUpView(UserPassesTestMixin, FormView): + """Sign up new user, optionally consume one-use invite link.""" + + template_name = 'login/form.html' + form_class = SignUpForm + success_url = '.' + _invite = None + + # Allow only not authenticated users to login + def test_func(self): + return self.request.user.is_authenticated is False + + def handle_no_permission(self): + return redirect(reverse('passbook_core:overview')) + + def dispatch(self, request, *args, **kwargs): + """Check if sign-up is enabled or invite link given""" + allowed = False + if 'invite' in request.GET: + invites = Invite.objects.filter(uuid=request.GET.get('invite')) + allowed = invites.exists() + if allowed: + self._invite = invites.first() + if CONFIG.y('passbook.sign_up.enabled'): + allowed = True + if not allowed: + messages.error(request, _('Sign-ups are currently disabled.')) + return redirect(reverse('passbook_core:auth-login')) + return super().dispatch(request, *args, **kwargs) + + def get_context_data(self, **kwargs): + kwargs['config'] = CONFIG.get('passbook') + kwargs['is_login'] = True + kwargs['title'] = _('Sign Up') + kwargs['primary_action'] = _('Sign up') + return super().get_context_data(**kwargs) + + def form_valid(self, form: SignUpForm) -> HttpResponse: + """Create user""" + SignUpView.create_user(form.cleaned_data, self.request) + if self._invite: + self._invite.delete() + messages.success(self.request, _("Successfully signed up!")) + LOGGER.debug("Successfully signed up %s", + form.cleaned_data.get('email')) + return redirect(reverse('passbook_core:auth-login')) + + @staticmethod + def create_user(data: Dict, request: HttpRequest = None) -> User: + """Create user from data + + Args: + data: Dictionary as returned by SignupForm's cleaned_data + request: Optional current request. + + Returns: + The user created + + Raises: + SignalException: if any signals raise an exception. This also deletes the created user. + """ + # Create user + new_user = User.objects.create_user( + username=data.get('username'), + email=data.get('email'), + first_name=data.get('first_name'), + last_name=data.get('last_name'), + ) + new_user.is_active = True + new_user.set_password(data.get('password')) + new_user.save() + # Send signal for other auth sources + # try: + # TODO: Create signal for signup + # on_user_sign_up.send( + # sender=None, + # user=new_user, + # request=request, + # password=data.get('password'), + # needs_confirmation=needs_confirmation) + # TODO: Implement Verification, via email or others + # if needs_confirmation: + # Create Account Confirmation UUID + # AccountConfirmation.objects.create(user=new_user) + # except SignalException as exception: + # LOGGER.warning("Failed to sign up user %s", exception, exc_info=exception) + # new_user.delete() + # raise + return new_user