Database
 sql >> Base de Dados >  >> RDS >> Database

Manipulando a confirmação de e-mail durante o registro no frasco


Este tutorial detalha como validar endereços de e-mail durante o registro do usuário.

Atualizado em 30/04/2015 :Adicionado suporte para Python 3.

Em termos de fluxo de trabalho, depois que um usuário registra uma nova conta, um e-mail de confirmação é enviado. A conta do usuário é marcada como “não confirmada” até que o usuário, bem, “confirme” a conta através das instruções no e-mail. Este é um fluxo de trabalho simples que a maioria dos aplicativos da Web segue.

Uma coisa importante a ser levada em consideração é o que os usuários não confirmados podem fazer. Em outras palavras, eles têm acesso total ao seu aplicativo, acesso limitado/restrito ou nenhum acesso? Para o aplicativo neste tutorial, os usuários não confirmados podem fazer login, mas são imediatamente redirecionados para uma página lembrando-os de que precisam confirmar sua conta antes de acessar o aplicativo.

Antes de começar, a maioria das funcionalidades que iremos adicionar faz parte das extensões Flask-User e Flask-Security - o que levanta a questão por que não usar apenas as extensões? Bem, em primeiro lugar, esta é uma oportunidade de aprender. Além disso, ambas as extensões têm limitações, como os bancos de dados suportados. E se você quisesse usar o RethinkDB, por exemplo?

Vamos começar.

Registro básico do frasco


Vamos começar com um clichê do Flask que inclui o registro básico do usuário. Pegue o código do repositório. Depois de criar e ativar um virtualenv, execute os seguintes comandos para começar rapidamente:
$ pip install -r requirements.txt
$ export APP_SETTINGS="project.config.DevelopmentConfig"
$ python manage.py create_db
$ python manage.py db init
$ python manage.py db migrate
$ python manage.py create_admin
$ python manage.py runserver

Confira o leia-me para mais informações.

Com o aplicativo em execução, navegue até http://localhost:5000/register e registre um novo usuário. Observe que, após o registro, o aplicativo faz seu login automaticamente e o redireciona para a página principal. Dê uma olhada ao redor e, em seguida, execute o código - especificamente o blueprint do "usuário".

Mate o servidor quando terminar.


Atualizar o aplicativo atual


Modelos


Primeiro, vamos adicionar o confirmed campo para nosso User model em project/models.py :
class User(db.Model):

    __tablename__ = "users"

    id = db.Column(db.Integer, primary_key=True)
    email = db.Column(db.String, unique=True, nullable=False)
    password = db.Column(db.String, nullable=False)
    registered_on = db.Column(db.DateTime, nullable=False)
    admin = db.Column(db.Boolean, nullable=False, default=False)
    confirmed = db.Column(db.Boolean, nullable=False, default=False)
    confirmed_on = db.Column(db.DateTime, nullable=True)

    def __init__(self, email, password, confirmed,
                 paid=False, admin=False, confirmed_on=None):
        self.email = email
        self.password = bcrypt.generate_password_hash(password)
        self.registered_on = datetime.datetime.now()
        self.admin = admin
        self.confirmed = confirmed
        self.confirmed_on = confirmed_on

Observe como este campo tem como padrão ‘False’. Também adicionamos um confirmed_on campo, que é um [datetime ] (https://realpython.com/python-datetime/). Eu gosto de incluir este campo também para analisar a diferença entre o registered_on e confirmed_on datas usando análise de coorte.

Vamos recomeçar completamente com nosso banco de dados e migrações. Então, vá em frente e exclua o banco de dados, dev.sqlite , bem como a pasta “migrações”.


Gerenciar comando


Em seguida, em manage.py , atualize o create_admin comando para levar em consideração os novos campos do banco de dados:
@manager.command
def create_admin():
    """Creates the admin user."""
    db.session.add(User(
        email="[email protected]",
        password="admin",
        admin=True,
        confirmed=True,
        confirmed_on=datetime.datetime.now())
    )
    db.session.commit()

Certifique-se de importar datetime . Agora, vá em frente e execute os seguintes comandos novamente:
$ python manage.py create_db
$ python manage.py db init
$ python manage.py db migrate
$ python manage.py create_admin


register() função de visualização


Finalmente, antes que possamos registrar um usuário novamente, precisamos fazer uma alteração rápida no register() função de visualização em project/user/views.py

Mudar:
user = User(
    email=form.email.data,
    password=form.password.data
)

Para:
user = User(
    email=form.email.data,
    password=form.password.data,
    confirmed=False
)

Faz sentido? Pense em por que gostaríamos de usar o padrão confirmed para False .

OK. Execute o aplicativo novamente. Navegue até http://localhost:5000/register e registre um novo usuário novamente. Se você abrir seu banco de dados SQLite no navegador SQLite, deverá ver:

Então, o novo usuário que registrei, [email protected] , não está confirmado. Vamos mudar isso.



Adicionar confirmação de e-mail


Gerar token de confirmação


A confirmação por e-mail deve conter um URL exclusivo que o usuário simplesmente precisa clicar para confirmar sua conta. Idealmente, o URL deve ser algo assim - http://yourapp.com/confirm/<id> . A chave aqui é o id . Vamos codificar o e-mail do usuário (junto com um carimbo de data/hora) no id usando o seu pacote perigoso.

Crie um arquivo chamado project/token.py e adicione o seguinte código:
# project/token.py

from itsdangerous import URLSafeTimedSerializer

from project import app


def generate_confirmation_token(email):
    serializer = URLSafeTimedSerializer(app.config['SECRET_KEY'])
    return serializer.dumps(email, salt=app.config['SECURITY_PASSWORD_SALT'])


def confirm_token(token, expiration=3600):
    serializer = URLSafeTimedSerializer(app.config['SECRET_KEY'])
    try:
        email = serializer.loads(
            token,
            salt=app.config['SECURITY_PASSWORD_SALT'],
            max_age=expiration
        )
    except:
        return False
    return email

Então, no generate_confirmation_token() função usamos o URLSafeTimedSerializer para gerar um token usando o endereço de e-mail obtido durante o registro do usuário. O real email está codificado no token. Em seguida, para confirmar o token, dentro do confirm_token() função, podemos usar a função loads() método, que recebe o token e a expiração - válido por uma hora (3.600 segundos) - como argumentos. Contanto que o token não tenha expirado, ele retornará um e-mail.

Certifique-se de adicionar o SECURITY_PASSWORD_SALT para a configuração do seu aplicativo (BaseConfig() ):
SECURITY_PASSWORD_SALT = 'my_precious_two'


Atualizar register() função de visualização


Agora vamos atualizar o register() veja a função novamente em project/user/views.py :
@user_blueprint.route('/register', methods=['GET', 'POST'])
def register():
    form = RegisterForm(request.form)
    if form.validate_on_submit():
        user = User(
            email=form.email.data,
            password=form.password.data,
            confirmed=False
        )
        db.session.add(user)
        db.session.commit()

        token = generate_confirmation_token(user.email)

Além disso, certifique-se de atualizar as importações:
from project.token import generate_confirmation_token, confirm_token


Gerenciar confirmação de e-mail


Em seguida, vamos adicionar uma nova visualização para lidar com a confirmação por e-mail:
@user_blueprint.route('/confirm/<token>')
@login_required
def confirm_email(token):
    try:
        email = confirm_token(token)
    except:
        flash('The confirmation link is invalid or has expired.', 'danger')
    user = User.query.filter_by(email=email).first_or_404()
    if user.confirmed:
        flash('Account already confirmed. Please login.', 'success')
    else:
        user.confirmed = True
        user.confirmed_on = datetime.datetime.now()
        db.session.add(user)
        db.session.commit()
        flash('You have confirmed your account. Thanks!', 'success')
    return redirect(url_for('main.home'))

Adicione isto a project/user/views.py . Além disso, certifique-se de atualizar as importações:
import datetime

Aqui, chamamos o confirm_token() função, passando o token. Se for bem-sucedido, atualizamos o usuário, alterando o email_confirmed atributo para True e definindo o datetime para quando a confirmação ocorreu. Além disso, caso o usuário já tenha passado pelo processo de confirmação - e seja confirmado - então alertamos o usuário sobre isso.


Crie o modelo de e-mail


Em seguida, vamos adicionar um modelo de email básico:
<p>Welcome! Thanks for signing up. Please follow this link to activate your account:</p>
<p><a href="{{ confirm_url }}">{{ confirm_url }}</a></p>
<br>
<p>Cheers!</p>

Salve como activate.html em “projeto/modelos/usuário”. Isso leva uma única variável chamada confirm_url , que será criado no register() função de visualização.


Enviar e-mail


Vamos criar uma função básica para envio de e-mails com uma pequena ajuda do Flask-Mail, que já está instalado e configurado em project/__init__.py .

Crie um arquivo chamado email.py :
# project/email.py

from flask.ext.mail import Message

from project import app, mail


def send_email(to, subject, template):
    msg = Message(
        subject,
        recipients=[to],
        html=template,
        sender=app.config['MAIL_DEFAULT_SENDER']
    )
    mail.send(msg)

Salve isso na pasta “projeto”.

Então, basta passar uma lista de destinatários, um assunto e um modelo. Nós vamos lidar com as configurações de e-mail daqui a pouco.


Atualizar register() função de visualização em project/user/views.py (de novo!)

@user_blueprint.route('/register', methods=['GET', 'POST'])
def register():
    form = RegisterForm(request.form)
    if form.validate_on_submit():
        user = User(
            email=form.email.data,
            password=form.password.data,
            confirmed=False
        )
        db.session.add(user)
        db.session.commit()

        token = generate_confirmation_token(user.email)
        confirm_url = url_for('user.confirm_email', token=token, _external=True)
        html = render_template('user/activate.html', confirm_url=confirm_url)
        subject = "Please confirm your email"
        send_email(user.email, subject, html)

        login_user(user)

        flash('A confirmation email has been sent via email.', 'success')
        return redirect(url_for("main.home"))

    return render_template('user/register.html', form=form)

Adicione a seguinte importação também:
from project.email import send_email

Aqui, estamos juntando tudo. Esta função basicamente atua como um controlador (direta ou indiretamente) para todo o processo:
  • Gerenciar o registro inicial,
  • Gerar token e URL de confirmação,
  • Enviar e-mail de confirmação,
  • Confirmação do Flash,
  • Faça login do usuário e
  • Redirecionar usuário.

Você notou o _external=True argumento? Isso adiciona a URL absoluta completa que inclui o nome do host e a porta (http://localhost:5000, em nosso caso).

Antes de podermos testar isso, precisamos configurar nossas configurações de e-mail.


E-mail


Comece atualizando o BaseConfig() em project/config.py :
class BaseConfig(object):
    """Base configuration."""

    # main config
    SECRET_KEY = 'my_precious'
    SECURITY_PASSWORD_SALT = 'my_precious_two'
    DEBUG = False
    BCRYPT_LOG_ROUNDS = 13
    WTF_CSRF_ENABLED = True
    DEBUG_TB_ENABLED = False
    DEBUG_TB_INTERCEPT_REDIRECTS = False

    # mail settings
    MAIL_SERVER = 'smtp.googlemail.com'
    MAIL_PORT = 465
    MAIL_USE_TLS = False
    MAIL_USE_SSL = True

    # gmail authentication
    MAIL_USERNAME = os.environ['APP_MAIL_USERNAME']
    MAIL_PASSWORD = os.environ['APP_MAIL_PASSWORD']

    # mail accounts
    MAIL_DEFAULT_SENDER = '[email protected]'

Confira a documentação oficial do Flask-Mail para mais informações.

Se você já possui uma conta GMAIL, pode usá-la ou registrar uma conta GMAIL de teste. Em seguida, defina as variáveis ​​de ambiente temporariamente na sessão atual do shell:
$ export APP_MAIL_USERNAME="foo"
$ export APP_MAIL_PASSWORD="bar"

Se sua conta do GMAIL tiver autenticação em duas etapas, o Google bloqueará a tentativa.

Agora vamos testar!



Primeiro teste


Inicie o aplicativo e navegue até http://localhost:5000/register. Em seguida, registre-se com um endereço de e-mail ao qual você tenha acesso. Se tudo correu bem, você deve ter um e-mail em sua caixa de entrada parecido com isto:

Clique na URL e você deverá ser direcionado para http://localhost:5000/. Certifique-se de que o usuário esteja no banco de dados, o campo ‘confirmed’ é True , e há um datetime associado ao confirmed_on campo.

Legal!


Gerenciar permissões


Se você se lembra, no início deste tutorial, decidimos que “usuários não confirmados podem fazer login, mas devem ser redirecionados imediatamente para uma página - vamos chamar a rota /unconfirmed - lembrando aos usuários que eles precisam confirmar sua conta antes de acessar o aplicativo”.

Então, precisamos-
  1. Adicione o /unconfirmed rota
  2. Adicione um unconfirmed.html modelo
  3. Atualize o register() função de visualização
  4. Criar um decorador
  5. Atualizar navigation.html modelo

Adicionar /unconfirmed rota


Adicione a seguinte rota a project/user/views.py :
@user_blueprint.route('/unconfirmed')
@login_required
def unconfirmed():
    if current_user.confirmed:
        return redirect('main.home')
    flash('Please confirm your account!', 'warning')
    return render_template('user/unconfirmed.html')

Você já viu código semelhante antes, então vamos seguir em frente.


Adicionar unconfirmed.html modelo

{% extends "_base.html" %}

{% block content %}

<h1>Welcome!</h1>
<br>
<p>You have not confirmed your account. Please check your inbox (and your spam folder) - you should have received an email with a confirmation link.</p>
<p>Didn't get the email? <a href="/">Resend</a>.</p>

{% endblock %}

Salve como unconfirmed.html em “projeto/modelos/usuário”. Novamente, tudo isso deve ser simples. Por enquanto, apenas adicionamos um URL fictício para reenviar o e-mail de confirmação. Abordaremos isso mais abaixo.


Atualize o register() função de visualização


Agora basta alterar:
return redirect(url_for("main.home"))

Para:
return redirect(url_for("user.unconfirmed"))

Assim, após o envio do e-mail de confirmação, o usuário agora é redirecionado para o /unconfirmed rota.


Criar um decorador

# project/decorators.py
from functools import wraps

from flask import flash, redirect, url_for
from flask.ext.login import current_user


def check_confirmed(func):
    @wraps(func)
    def decorated_function(*args, **kwargs):
        if current_user.confirmed is False:
            flash('Please confirm your account!', 'warning')
            return redirect(url_for('user.unconfirmed'))
        return func(*args, **kwargs)

    return decorated_function

Aqui temos uma função básica para verificar se um usuário não está confirmado. Se não confirmado, o usuário é redirecionado para o /unconfirmed rota. Salve como decorators.py no diretório “projeto”.

Agora, decore o profile() função de visualização:
@user_blueprint.route('/profile', methods=['GET', 'POST'])
@login_required
@check_confirmed
def profile():
    # ... snip ...

Certifique-se de importar o decorador:
from project.decorators import check_confirmed


Atualizar navigation.html modelo


Por fim, atualize a seguinte parte do navigation.html modelo-

Mudar:
<ul class="nav navbar-nav">
  {% if current_user.is_authenticated() %}
    <li><a href="{{ url_for('user.profile') }}">Profile</a></li>
  {% endif %}
</ul>

Para:
<ul class="nav navbar-nav">
  {% if current_user.confirmed and current_user.is_authenticated() %}
    <li><a href="{{ url_for('user.profile') }}">Profile</a></li>
  {% elif current_user.is_authenticated() %}
    <li><a href="{{ url_for('user.unconfirmed') }}">Confirm</a></li>
  {% endif %}
</ul>

Hora de testar novamente!



Segundo teste


Inicie o aplicativo e registre-se novamente com um endereço de e-mail ao qual você tenha acesso. (Sinta-se à vontade para excluir o usuário antigo que você registrou antes do banco de dados para usar novamente.) Agora você deve ser redirecionado para http://localhost:5000/unconfirmed após o registro.

Certifique-se de testar a rota http://localhost:5000/profile. Isso deve redirecioná-lo para http://localhost:5000/unconfirmed.

Vá em frente e confirme o e-mail, e você terá acesso a todas as páginas. Estrondo!


Reenviar e-mail


Finalmente, vamos fazer o link de reenvio funcionar. Adicione a seguinte função de visualização a project/user/views.py :
@user_blueprint.route('/resend')
@login_required
def resend_confirmation():
    token = generate_confirmation_token(current_user.email)
    confirm_url = url_for('user.confirm_email', token=token, _external=True)
    html = render_template('user/activate.html', confirm_url=confirm_url)
    subject = "Please confirm your email"
    send_email(current_user.email, subject, html)
    flash('A new confirmation email has been sent.', 'success')
    return redirect(url_for('user.unconfirmed'))

Agora atualize o unconfirmed.html modelo:
{% extends "_base.html" %}

{% block content %}

<h1>Welcome!</h1>
<br>
<p>You have not confirmed your account. Please check your inbox (and your spam folder) - you should have received an email with a confirmation link.</p>
<p>Didn't get the email? <a href="{{ url_for('user.resend_confirmation') }}">Resend</a>.</p>

{% endblock %}


Terceiro teste


Você sabe o que fazer. Desta vez, certifique-se de reenviar um novo e-mail de confirmação e testar o link. Deve funcionar.

Finalmente, o que acontece se você enviar alguns links de confirmação? Cada um é válido? Teste-o. Registre um novo usuário e envie alguns novos e-mails de confirmação. Tente confirmar com o primeiro e-mail. Funcionou? Deveria. Isso está bem? Você acha que esses outros e-mails devem expirar se um novo for enviado?

Faça alguma pesquisa sobre isso. E teste outros aplicativos da Web que você usa. Como eles lidam com tal comportamento?


Atualizar conjunto de testes


Tudo bem. Então é isso para a funcionalidade principal. Que tal atualizarmos o conjunto de testes atual, já que está quebrado.

Execute os testes:
$ python manage.py test

Você deve ver o seguinte erro:
TypeError: __init__() takes at least 4 arguments (3 given)

Para corrigir isso, basta atualizar o setUp() método em project/util.py :
def setUp(self):
    db.create_all()
    user = User(email="[email protected]", password="admin_user", confirmed=False)
    db.session.add(user)
    db.session.commit()

Agora execute os testes novamente. Todos devem passar!


Conclusão


Há claramente muito mais que podemos fazer:
  1. E-mails ricos x de texto simples - devemos enviar ambos.
  2. E-mail de redefinição de senha - Estes devem ser enviados para usuários que esqueceram suas senhas.
  3. Gerenciamento de usuários - Devemos permitir que os usuários atualizem seus e-mails e senhas e, quando um e-mail for alterado, ele deverá ser confirmado novamente.
  4. Teste - Precisamos escrever mais testes para cobrir os novos recursos.

Baixe todo o código-fonte do repositório do Github. Comente abaixo com perguntas. Confira a parte 2.

Boas férias!