Dica 18 - Esqueci a senha
Versões usadas no vídeo: Django 4.1.3, Python 3.10, Tailwind CSS e MailHog.
Importante: nas versões antigas desta página as tags de template apareciam com uma \ no meio ({%), por causa do GitBook. Aqui elas já estão escritas do jeito certo, sem a barra.
Código: https://github.com/rg3915/dicas-de-django (branch aula18)
Documentação:
- https://docs.djangoproject.com/en/4.1/topics/auth/default/#django.contrib.auth.views.PasswordResetView
- https://docs.djangoproject.com/en/4.1/topics/auth/default/#django.contrib.auth.views.PasswordResetDoneView
Nesta dica vamos implementar o Esqueci a senha na tela de login: o usuário clica em "Esqueceu a senha?", informa o e-mail, recebe um e-mail com um link e, por esse link, cadastra uma senha nova.
O Django já traz tudo pronto em django.contrib.auth.views. O fluxo completo usa quatro views:
| View | O que faz | Template |
|---|---|---|
PasswordResetView |
formulário para digitar o e-mail; envia o e-mail com o link | password_reset_form.html, password_reset_email.html |
PasswordResetDoneView |
página "enviamos um e-mail para você" | password_reset_done.html |
PasswordResetConfirmView |
o link do e-mail abre esta view, que pede a nova senha | password_reset_confirm.html |
PasswordResetCompleteView |
página "senha alterada" | password_reset_complete.html |
As duas últimas já foram feitas na Dica 17 - Cadastro de Usuários (lá usamos o mesmo link de troca de senha para ativar a conta do usuário recém-cadastrado). Agora faltam as duas primeiras.
Pré-requisitos
Esta dica continua o Projeto Dicas de Django do ponto em que a Dica 17 parou. Você precisa ter:
- o app
backend/accountscom o usuário customizado que faz login com e-mail (Dica 14 e Dica 15); - as rotas
password_reset_confirmepassword_reset_completeda Dica 17; - o MailHog rodando pelo
docker-compose(Dica 07) e o envio de e-mail configurado nosettings.py(Dica 12).
A configuração de e-mail no settings.py, que já existe desde a Dica 12, é esta:
# backend/settings.py (trecho)
LANGUAGE_CODE = 'pt-br'
# Email config
EMAIL_BACKEND = 'django.core.mail.backends.smtp.EmailBackend'
DEFAULT_FROM_EMAIL = config('DEFAULT_FROM_EMAIL', 'webmaster@localhost')
EMAIL_HOST = config('EMAIL_HOST', 'localhost')
EMAIL_PORT = config('EMAIL_PORT', 1025, cast=int)
EMAIL_HOST_USER = config('EMAIL_HOST_USER', '')
EMAIL_HOST_PASSWORD = config('EMAIL_HOST_PASSWORD', '')
EMAIL_USE_TLS = config('EMAIL_USE_TLS', default=False, cast=bool)
A porta 1025 é a do SMTP do MailHog, e a caixa de entrada dele fica em http://localhost:8025.
O link "Esqueceu a senha?" no login
Em backend/accounts/templates/registration/login.html já existia o link "Esqueceu a senha?", mas com o href vazio. Aponte-o para a rota password_reset, que vamos criar a seguir:
<!-- backend/accounts/templates/registration/login.html (trecho) -->
<a href="{% url 'password_reset' %}" class="text-sm text-teal-500 hover:underline ml-auto">Esqueceu a senha?</a>
As rotas
Acrescente as duas rotas no fim de backend/accounts/urls.py. O arquivo completo fica assim:
# backend/accounts/urls.py
from django.contrib.auth.views import LoginView, LogoutView
from django.urls import path
from backend.accounts import views as v
urlpatterns = [
path('login/', LoginView.as_view(), name='login'), # noqa E501
path('logout/', LogoutView.as_view(), name='logout'), # noqa E501
path('register/', v.signup, name='signup'), # noqa E501
path('reset/<uidb64>/<token>/', v.MyPasswordResetConfirm.as_view(), name='password_reset_confirm'), # noqa E501
path('reset/done/', v.MyPasswordResetComplete.as_view(), name='password_reset_complete'), # noqa E501
path('password_reset/', v.MyPasswordReset.as_view(), name='password_reset'), # noqa E501
path('password_reset/done/', v.MyPasswordResetDone.as_view(), name='password_reset_done'), # noqa E501
]
Os nomes password_reset e password_reset_done são importantes: a PasswordResetView, depois de enviar o e-mail, redireciona para reverse_lazy('password_reset_done'), e o template do e-mail usa a rota password_reset_confirm. Como o app é incluído em backend/urls.py com path('accounts/', include('backend.accounts.urls')), sem namespace, a página fica em http://localhost:8000/accounts/password_reset/.
As views
Em backend/accounts/views.py, importe PasswordResetDoneView e PasswordResetView junto com as views que já estavam lá e crie as duas classes. Elas só herdam das views do Django; a docstring lembra quais templates cada uma precisa. O trecho principal (o arquivo completo está no link logo abaixo):
# backend/accounts/views.py
from django.contrib.auth.views import (
PasswordResetCompleteView,
PasswordResetConfirmView,
PasswordResetDoneView,
PasswordResetView
)
# ... (veja o arquivo completo no GitHub)
class MyPasswordReset(PasswordResetView):
'''
Requer
registration/password_reset_form.html
registration/password_reset_email.html
registration/password_reset_subject.txt Opcional
'''
...
class MyPasswordResetDone(PasswordResetDoneView):
'''
Requer
registration/password_reset_done.html
'''
...
Código completo: backend/accounts/views.py
Os templates padrão dessas views ficam em registration/, com os nomes da docstring. Basta criar arquivos com esses nomes em backend/accounts/templates/registration/ para que os nossos, com o visual do projeto, sejam usados no lugar dos templates do admin do Django. O password_reset_subject.txt é opcional: sem ele, o Django usa o assunto padrão traduzido, "Redefinição de senha em localhost:8000".
O formulário de e-mail: password_reset_form.html
A PasswordResetView usa o PasswordResetForm, que tem um único campo, email. Por isso o input tem name="email". O layout é o mesmo da tela de login (Tailwind CSS, estendendo base_login.html):
<!-- password_reset_form.html -->
{% extends "base_login.html" %}
{% block content %}
<main class="bg-gray-50">
<div class="mx-auto md:h-screen flex flex-col justify-center items-center px-6 pt-8 pt:mt-0">
<a href="https://demo.themesberg.com/windster/" class="text-2xl font-semibold flex justify-center items-center mb-8 lg:mb-10">
<img src="https://demo.themesberg.com/windster/images/logo.svg" class="h-10 mr-4" alt="Windster Logo">
<span class="self-center text-2xl font-bold whitespace-nowrap">Dicas de Django</span>
</a>
<!-- Card -->
<div class="bg-white shadow rounded-lg md:mt-0 w-full sm:max-w-screen-sm xl:p-0">
<div class="p-6 sm:p-8 lg:p-16 space-y-8">
<h2 class="text-2xl lg:text-3xl font-bold text-gray-900">
Redefinição de senha
</h2>
<p class="text-sm font-medium text-gray-500">
Esqueceu sua senha? Informe seu e-mail abaixo, e nós te enviaremos um e-mail com instruções para configurar uma nova.
</p>
<form class="mt-8 space-y-6" action="." method="POST">
{% csrf_token %}
<div>
<label for="id_email" class="text-sm font-medium text-gray-900 block mb-2">E-mail</label>
<input
id="id_email"
type="email"
name="email"
class="bg-gray-50 border border-gray-300 text-gray-900 sm:text-sm rounded-lg focus:ring-cyan-600 focus:border-cyan-600 block w-full p-2.5"
placeholder="nome@example.com"
required
>
</div>
<button type="submit" class="text-white bg-cyan-600 hover:bg-cyan-700 focus:ring-4 focus:ring-cyan-200 font-medium rounded-lg text-base px-5 py-3 w-full sm:w-auto text-center">Enviar</button>
<div class="text-sm font-medium text-gray-500">
Não cadastrado? <a href="{% url 'signup' %}" class="text-teal-500 hover:underline">Criar conta</a>
</div>
</form>
</div>
</div>
</div>
</main>
{% endblock content %}
O arquivo fica em backend/accounts/templates/registration/password_reset_form.html.
O corpo do e-mail: password_reset_email.html
Este é o texto do e-mail. A view passa para o template o user, o protocol (http ou https), o domain (aqui, localhost:8000), o uid (o id do usuário em base64) e o token. Com uid e token montamos o link para a rota password_reset_confirm. O {% autoescape off %} evita que o Django escape caracteres do texto, já que o e-mail é texto puro.
{% autoescape off %}
Para iniciar o processo de redefinição de senha para sua conta {{ user.get_username }}, clique no link abaixo:
{{ protocol }}://{{ domain }}{% url 'password_reset_confirm' uidb64=uid token=token %}
Se clicar no link acima não funcionar, por favor copie e cole a URL no navegador.
Atenciosamente,
Equipe Dev.
{% endautoescape %}
O arquivo fica em backend/accounts/templates/registration/password_reset_email.html. Repare que ele não tem a linha de comentário <!-- ... --> no topo, como os outros templates: ela apareceria no corpo do e-mail.
A confirmação de envio: password_reset_done.html
Depois de enviar o formulário, o usuário é redirecionado para esta página:
<!-- password_reset_done.html -->
{% extends "base_login.html" %}
{% block content %}
<main class="bg-gray-50">
<div class="mx-auto md:h-screen flex flex-col justify-center items-center px-6 pt-8 pt:mt-0">
<a href="https://demo.themesberg.com/windster/" class="text-2xl font-semibold flex justify-center items-center mb-8 lg:mb-10">
<img src="https://demo.themesberg.com/windster/images/logo.svg" class="h-10 mr-4" alt="Windster Logo">
<span class="self-center text-2xl font-bold whitespace-nowrap">Dicas de Django</span>
</a>
<!-- Card -->
<div class="bg-white shadow rounded-lg md:mt-0 w-full sm:max-w-screen-sm xl:p-0">
<div class="p-6 sm:p-8 lg:p-16 space-y-8">
<h2 class="text-2xl lg:text-3xl font-bold text-gray-900">
Redefinição de senha enviada
</h2>
<p class="text-sm font-medium text-gray-500">
Nós te enviamos um e-mail com instruções para configurar sua senha, se uma conta existe com o e-mail fornecido. Você receberá a mensagem em breve.
</p>
<p class="text-sm font-medium text-gray-500">
Se você não recebeu um e-mail, por favor certifique-se que você forneceu o endereço que você está cadastrado, e verifique sua pasta de spam.
</p>
</div>
</div>
</div>
</main>
{% endblock content %}
A mensagem é propositalmente vaga ("se uma conta existe com o e-mail fornecido"): a PasswordResetView mostra a mesma página mesmo quando o e-mail não está cadastrado, para não revelar quais e-mails existem no sistema.
A pasta de templates do app fica assim:
backend/accounts/templates/registration/
├── login.html
├── password_reset_complete.html # Dica 17
├── password_reset_confirm.html # Dica 17
├── password_reset_done.html # novo
├── password_reset_email.html # novo
├── password_reset_form.html # novo
└── registration_form.html # Dica 17
Testando
Com os contêineres no ar (make up, que roda docker-compose up -d) e o servidor rodando:
- Saia do sistema e, na tela de login, clique em Esqueceu a senha?.
- Informe o e-mail de um usuário cadastrado (no vídeo,
regis@email.com) e clique em Enviar. Aparece a página "Redefinição de senha enviada". -
Abra o MailHog em
http://localhost:8025. Chegou um e-mail dewebmaster@localhostcom o assunto Redefinição de senha em localhost:8000:``` Para iniciar o processo de redefinição de senha para sua conta regis@email.com, clique no link abaixo:
http://localhost:8000/accounts/reset/Mw/bg42lm-984f6c848c7b6aea3247183735028c72/
Se clicar no link acima não funcionar, por favor copie e cole a URL no navegador.
Atenciosamente, Equipe Dev. ```
O
Mwé o id do usuário (3) em base64, e o resto é o token, que só pode ser usado uma vez. -
Clique no link. A
PasswordResetConfirmViewvalida o token, guarda-o na sessão e redireciona para/accounts/reset/Mw/set-password/(assim o token não fica na barra de endereço), onde abre a tela Trocar senha da Dica 17. Digite a nova senha duas vezes e clique em Trocar senha; você é levado para/accounts/reset/done/. - Volte ao login e entre com o e-mail e a senha nova.
Lembrando que o MailHog só simula o recebimento: nenhum e-mail sai de verdade. Em produção, troque as variáveis EMAIL_* do .env pelas do seu servidor SMTP.
Com isso o ciclo de autenticação do projeto fica completo: cadastro, login, logout e redefinição de senha, todos com as views prontas do Django e templates próprios.