В качестве хобби, я решил открыть для себя мир веб-разработки, попутно освещая свои успехи, а может и неудачи на пути к просветлению…

В настоящее время я публично описываю процесс создания веб-сайта с нуля на Django и Bootstrap, где задача разработать такие приложения как: аккаунты, вопросы, статьи и книги.

В проекте решаются только реальные задачи, которые почему-то так любят обходить стороной в учебной литературе.

Создание веб-сайта с нуля на Django и Bootstrap. Аккаунты. Смена пароля и адреса электронной почты. Формы и представления

Сергей Серов

Если с причиной смены адреса электронной почты все просто — пользователь решил пользоваться другим почтовым ящиком, то желание поменять пароль может возникнуть по нескольким причинам.

Например, пользователь в целях профилактики может периодически изменять пароль будучи авторизированном в системе. Или пароль пользователя мог быть скомпрометирован или попросту забыт.

В последнем случае обычно система высылает новый сгенерированный сервисом пароль на адрес электронной почты, а затем "заботливо" советует сменить его в личном кабинете после успешной авторизации.

От подобных пагубных практик необходимо отходить, пропуская ненужные шаги и давая пользователю возможность сразу задать новый пароль с подтверждением данной операции по электронной почте, указанной при регистрации данного аккаунта.

Формы

Переходим в каталог accounts и открываем forms.py для добавления следующих строк:

class ChangePasswordForm(forms.Form):
    password = forms.CharField(label='Новый пароль', max_length=255, strip=False,
                               widget=forms.PasswordInput(attrs={'autofocus': ''}))

    def __init__(self, *args, **kwargs):
        self.user = kwargs.pop('user', None)
        super(ChangePasswordForm, self).__init__(*args, **kwargs)

    def clean_password(self):
        # проверим пароль с помощью списка валидаторов объявленного в settings.AUTH_PASSWORD_VALIDATORS
        password = self.cleaned_data.get('password')
        validate_password(password, self.user)
        return password

    def send_vcode(self, request):
        Vcode(request).send(self.user, 'change_password', {
            'password': self.cleaned_data['password'],
        })


class AnonymousChangePasswordForm(ChangePasswordForm):
    email = forms.EmailField(label='Адрес электронной почты', max_length=254,
                             widget=forms.EmailInput(attrs={'autofocus': ''}))

    field_order = ['email', 'password']

    def clean_email(self):
        email = self.cleaned_data.get('email')
        try:
            self.user = User.active.get(email=email)
        except User.DoesNotExist:
            raise forms.ValidationError('Пользователя с данным адресом электронной почты не существует '
                                        'или он неактивен')
        return email


class ChangeEmailForm(forms.ModelForm):
    email = forms.EmailField(label='Новый адрес электронной почты', max_length=254,
                             widget=forms.EmailInput(attrs={'autofocus': ''}),
                             error_messages={'unique': 'Данный адрес электронной почты уже используется'})

    class Meta:
        model = User
        fields = ['email']

    def send_vcode(self, request, user):
        Vcode(request).send(user, 'change_email', {
            'email': self.cleaned_data['email'],
        })

ChangePasswordForm предлагает задать новый пароль и данную форму следует использовать, когда человек прошел процедуру авторизации.

AnonymousChangePasswordForm мы разработали для смены пароля, когда доступ к аккаунту по каким-то причинам был утерян. Мы наследуемся от предыдущей формы, поскольку функционал схож с прошлой, за исключением того, что необходимо добавить поле адреса электронной почты и проверить является ли пользователь активным на данный момент.

И в заключении ChangeEmailForm предлагает сменить адрес электронной почты на новый соответственно.

Во всех трех формах, как и при регистрации нового пользователя мы используем заранее разработанный функционал по отправке ссылок для подтверждения операций на закрепленный адрес электронной почты.

Представления

Открываем файл views.py и добавляем следующее:

from .decorators import auth_required


def change_password_view(request):
    if request.user.is_authenticated:
        form = forms.ChangePasswordForm(request.POST or None, user=request.user)
    else:
        form = forms.AnonymousChangePasswordForm(request.POST or None)
    if form.is_valid():
        form.send_vcode(request)
        return redirect('accounts:change_password_confirm')
    return render(request, 'accounts/change_password.html', {
        'form': form,
    })


def change_password_confirm_view(request, code):
    status = 'waiting'
    if code:
        vcode = Vcode(request)
        if vcode.is_valid(code, 'change_password'):
            status = 'success'
            user, data = vcode.get_content()
            # меняем пароль
            user.set_password(data['password'])
            user.save()
            # авторизуем пользователя
            login(request, user, settings.ACCOUNTS_EMAILBACKEND)
        else:
            status = 'invalid'
    return render(request, 'accounts/change_password_confirm.html', {
        'status': status,
    })


@auth_required
def change_email_view(request):
    form = forms.ChangeEmailForm(request.POST or None)
    if form.is_valid():
        form.send_vcode(request, request.user)
        return redirect('accounts:change_email_confirm')
    return render(request, 'accounts/change_email.html', {
        'form': form,
    })


@auth_required
def change_email_confirm_view(request, code):
    status = 'waiting'
    if code:
        vcode = Vcode(request)
        if vcode.is_valid(code, 'change_email'):
            status = 'success'
            user, data = vcode.get_content()
            # меняем email
            user.email = data['email']
            user.save()
        else:
            status = 'invalid'
    return render(request, 'accounts/change_email_confirm.html', {
        'status': status,
    })

В первом представлении мы объединяем функционал смены пароля и для того, чтобы отобразить нужную форму, мы проверяем авторизирован ли пользователь на данный момент или нет. В случае правильности заполнения — отправляем подтверждение на почту или выводим шаблон с рекомендациями как исправить допущенные ошибки.

Если в представлении мы удачно подали запрос на смену тогда мы перенаправляем пользователя на change_password_confirm_view где сверяем коды, заканчиваем процедуру смены и авторизуем пользователя в системе.

Процедура авторизации нужна даже в том случае если пользователь на момент смены пароля был в системе. Все дело в том, что Django, в целях безопасности при смене пароля, приостанавливает сессию пользователя.

Представления по смене адреса электронной почты действуют аналогично.

Декоратор

Переходим к файлу decorators.py:

def auth_required(view_func):
    def wrapped_view(request, *args, **kwargs):
        if request.user.is_authenticated:
            return view_func(request, *args, **kwargs)
        return render(request, 'accounts/auth_required.html')
    return wrapped_view

Декоратор просто проверяет авторизован ли пользователь на данный момент, поскольку представления по смене адреса электронной почты следуют показывать только зашедшим в систему.

Маршруты

Файл urls.py теперь должен соответствовать следующему:

from django.conf.urls import url

from . import views

app_name = 'accounts'
urlpatterns = [
    url(r'^join/$', views.join_view, name='join'),
    url(r'^join/confirm/(?:(?P<code>\w+)/)?$', views.join_confirm_view, name='join_confirm'),
    url(r'^join/resend/$', views.join_resend_view, name='join_resend'),
    url(r'^login/$', views.login_view, name='login'),
    url(r'^logout/$', views.logout_view, name='logout'),
    url(r'^change-password/$', views.change_password_view, name='change_password'),
    url(r'^change-password/confirm/(?:(?P<code>\w+)/)?$', views.change_password_confirm_view,
        name='change_password_confirm'),
    url(r'^change-email/$', views.change_email_view, name='change_email'),
    url(r'^change-email/confirm/(?:(?P<code>\w+)/)?$', views.change_email_confirm_view,
        name='change_email_confirm'),
]