sql >> Database >  >> RDS >> Database

E-mailbevestiging afhandelen tijdens registratie in Flask

In deze zelfstudie wordt beschreven hoe u e-mailadressen valideert tijdens gebruikersregistratie.

Bijgewerkt 30/04/2015 :Ondersteuning voor Python 3 toegevoegd.

Wat de workflow betreft, wordt nadat een gebruiker een nieuw account heeft geregistreerd, een bevestigingsmail verzonden. Het gebruikersaccount wordt gemarkeerd als "onbevestigd" totdat de gebruiker, nou ja, het account "bevestigt" via de instructies in de e-mail. Dit is een eenvoudige workflow die de meeste webapplicaties volgen.

Een belangrijk ding om rekening mee te houden is wat onbevestigde gebruikers mogen doen. Met andere woorden, hebben ze volledige toegang tot uw applicatie, beperkte/beperkte toegang of helemaal geen toegang? Voor de applicatie in deze tutorial kunnen niet-bevestigde gebruikers inloggen, maar ze worden onmiddellijk doorgestuurd naar een pagina die hen eraan herinnert dat ze hun account moeten bevestigen voordat ze toegang krijgen tot de applicatie.

Voordat we beginnen, maakt de meeste functionaliteit die we gaan toevoegen deel uit van de extensies Flask-User en Flask-Security - wat de vraag oproept waarom niet gewoon de extensies gebruiken? Dit is in de eerste plaats een kans om te leren. Beide extensies hebben ook beperkingen, zoals de ondersteunde databases. Wat als u bijvoorbeeld RethinkDB zou willen gebruiken?

Laten we beginnen.


Flas basisregistratie

We beginnen met een Flask-boilerplate met basisregistratie voor gebruikers. Pak de code uit de repository. Nadat u een virtualenv heeft gemaakt en geactiveerd, voert u de volgende opdrachten uit om snel aan de slag te gaan:

$ 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

Bekijk de readme voor meer informatie.

Terwijl de app actief is, navigeert u naar http://localhost:5000/register en registreert u een nieuwe gebruiker. Merk op dat de app je na registratie automatisch inlogt en je doorverwijst naar de hoofdpagina. Kijk eens rond en doorloop de code, met name de blauwdruk van de 'gebruiker'.

Dood de server als je klaar bent.



De huidige app bijwerken


Modellen

Laten we eerst de confirmed . toevoegen veld naar onze User model in 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

Merk op hoe dit veld standaard op 'False' staat. We hebben ook een confirmed_on . toegevoegd veld, dat een [datetime . is ] (https://realpython.com/python-datetime/). Ik wil dit veld ook graag opnemen om het verschil te analyseren tussen de registered_on en confirmed_on datums met behulp van cohortanalyse.

Laten we helemaal opnieuw beginnen met onze database en migraties. Dus ga je gang en verwijder de database, dev.sqlite , evenals de map "migraties".



Opdracht beheren

Vervolgens, binnen manage.py , update de create_admin commando om rekening te houden met de nieuwe databasevelden:

@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()

Zorg ervoor dat u datetime importeert . Ga je gang en voer de volgende opdrachten opnieuw uit:

$ python manage.py create_db
$ python manage.py db init
$ python manage.py db migrate
$ python manage.py create_admin


register() bekijk functie

Ten slotte, voordat we een gebruiker opnieuw kunnen registreren, moeten we een snelle wijziging aanbrengen in de register() weergavefunctie in project/user/views.py

Wijzigen:

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

Aan:

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

Zin? Bedenk waarom we standaard confirmed . zouden willen hebben naar False .

Oké. Voer de app opnieuw uit. Navigeer naar http://localhost:5000/register en registreer een nieuwe gebruiker opnieuw. Als u uw SQLite-database in de SQLite-browser opent, zou u het volgende moeten zien:

Dus de nieuwe gebruiker die ik heb geregistreerd, [email protected] , wordt niet bevestigd. Laten we daar verandering in brengen.




E-mailbevestiging toevoegen


Bevestigingstoken genereren

De e-mailbevestiging moet een unieke URL bevatten waarop een gebruiker hoeft te klikken om zijn/haar account te bevestigen. Idealiter zou de URL er ongeveer zo uit moeten zien - http://yourapp.com/confirm/<id> . De sleutel hier is de id . We gaan het e-mailadres van de gebruiker coderen (samen met een tijdstempel) in de id met behulp van het zijngevaarlijke pakket.

Maak een bestand met de naam project/token.py en voeg de volgende code toe:

# 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

Dus, in de generate_confirmation_token() functie gebruiken we de URLSafeTimedSerializer om een ​​token te genereren met behulp van het e-mailadres dat is verkregen tijdens de gebruikersregistratie. De echte e-mail is gecodeerd in de token. Om vervolgens het token te bevestigen, binnen de confirm_token() functie, kunnen we de loads() . gebruiken methode, die het token en de vervaldatum - geldig voor één uur (3.600 seconden) - als argumenten gebruikt. Zolang het token niet is verlopen, wordt er een e-mail geretourneerd.

Zorg ervoor dat u de SECURITY_PASSWORD_SALT . toevoegt naar de configuratie van uw app (BaseConfig() ):

SECURITY_PASSWORD_SALT = 'my_precious_two'


Update register() bekijk functie

Laten we nu het register() bijwerken bekijk de functie opnieuw vanuit 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)

Zorg er ook voor dat u de invoer bijwerkt:

from project.token import generate_confirmation_token, confirm_token


E-mailbevestiging afhandelen

Laten we vervolgens een nieuwe weergave toevoegen om de e-mailbevestiging af te handelen:

@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'))

Voeg dit toe aan project/user/views.py . Zorg er ook voor dat u de invoer bijwerkt:

import datetime

Hier noemen we de confirm_token() functie, het doorgeven van het token. Als dit lukt, werken we de gebruiker bij en wijzigen we de email_confirmed toeschrijven aan True en het instellen van de datetime voor wanneer de bevestiging heeft plaatsgevonden. Ook als de gebruiker het bevestigingsproces al heeft doorlopen - en wordt bevestigd - dan waarschuwen we de gebruiker hiervan.



Maak de e-mailsjabloon

Laten we vervolgens een basis-e-mailsjabloon toevoegen:

<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>

Bewaar dit als activate.html in “project/sjablonen/gebruiker”. Hiervoor is een enkele variabele nodig met de naam confirm_url , die zal worden aangemaakt in het register() weergavefunctie.



E-mail verzenden

Laten we een basisfunctie maken voor het verzenden van e-mails met een beetje hulp van Flask-Mail, die al is geïnstalleerd en ingesteld in project/__init__.py .

Maak een bestand met de naam 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)

Bewaar dit in de “project” map.

We hoeven dus alleen een lijst met ontvangers, een onderwerp en een sjabloon door te geven. We zullen de instellingen voor de e-mailconfiguratie zo dadelijk behandelen.



Update register() bekijk de functie in project/user/views.py (opnieuw!)

@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)

Voeg ook de volgende import toe:

from project.email import send_email

Hier brengen we alles samen. Deze functie fungeert in feite als een controller (direct of indirect) voor het hele proces:

  • Eerste registratie afhandelen,
  • Genereer token en bevestigings-URL,
  • Stuur bevestigingsmail,
  • Flash-bevestiging,
  • Log in de gebruiker, en
  • Gebruiker omleiden.

Heb je de _external=True . opgemerkt argument? Dit voegt de volledige absolute URL toe die de hostnaam en poort bevat (http://localhost:5000, in ons geval.)

Voordat we dit kunnen testen, moeten we onze e-mailinstellingen instellen.



Mail

Begin met het bijwerken van de BaseConfig() in 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]'

Bekijk de officiële Flask-Mail-documentatie voor meer info.

Als u al een GMAIL-account heeft, kunt u dat gebruiken of een test-GMAIL-account registreren. Stel vervolgens de omgevingsvariabelen tijdelijk in in de huidige shell-sessie:

$ export APP_MAIL_USERNAME="foo"
$ export APP_MAIL_PASSWORD="bar"

Als uw GMAIL-account authenticatie in twee stappen heeft, blokkeert Google de poging.

Laten we nu testen!




Eerste test

Start de app en ga naar http://localhost:5000/register. Registreer je dan met een e-mailadres waar je toegang toe hebt. Als alles goed is gegaan, zou je een e-mail in je inbox moeten hebben die er ongeveer zo uitziet:

Klik op de URL en u zou naar http://localhost:5000/ moeten gaan. Zorg ervoor dat de gebruiker in de database staat, het veld 'bevestigd' is True , en er is een datetime gekoppeld aan de confirmed_on veld.

Leuk!



Rechten afhandelen

Als je het je herinnert, hebben we aan het begin van deze tutorial besloten dat "onbevestigde gebruikers kunnen inloggen, maar ze moeten onmiddellijk worden doorgestuurd naar een pagina - laten we de route /unconfirmed noemen. - gebruikers eraan herinneren dat ze hun account moeten bevestigen voordat ze toegang krijgen tot de applicatie”.

Dus we moeten-

  1. Voeg de /unconfirmed . toe route
  2. Voeg een unconfirmed.html toe sjabloon
  3. Update het register() bekijk functie
  4. Maak een decorateur
  5. Update navigatie.html sjabloon

Voeg /unconfirmed toe route

Voeg de volgende route toe aan 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')

Je hebt soortgelijke code eerder gezien, dus laten we verder gaan.



Voeg unconfirmed.html toe sjabloon

{% 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 %}

Bewaar dit als unconfirmed.html in “project/sjablonen/gebruiker”. Nogmaals, dit moet allemaal eenvoudig zijn. Voor nu hebben we zojuist een dummy-URL toegevoegd voor het opnieuw verzenden van de bevestigingsmail. We zullen dit verderop bespreken.



Update het register() bekijk functie

Verander nu eenvoudig:

return redirect(url_for("main.home"))

Aan:

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

Dus nadat de bevestigingsmail is verzonden, wordt de gebruiker nu doorgestuurd naar de /unconfirmed route.



Maak een decorateur

# 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

Hier hebben we een basisfunctie om te controleren of een gebruiker onbevestigd is. Indien niet bevestigd, wordt de gebruiker doorgestuurd naar de /unconfirmed route. Bewaar dit als decorators.py in de map "project".

Versier nu het profile() bekijk functie:

@user_blueprint.route('/profile', methods=['GET', 'POST'])
@login_required
@check_confirmed
def profile():
    # ... snip ...

Zorg ervoor dat u de decorateur importeert:

from project.decorators import check_confirmed


Update navigation.html sjabloon

Werk ten slotte het volgende deel van de navigation.html . bij sjabloon-

Wijzigen:

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

Aan:

<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>

Tijd om opnieuw te testen!




Tweede test

Start de app en registreer opnieuw met een e-mailadres waartoe u toegang hebt. (Voel je vrij om de oude gebruiker die je eerder hebt geregistreerd eerst uit de database te verwijderen om deze opnieuw te gebruiken.) Nu zou je moeten worden doorgestuurd naar http://localhost:5000/unconfirmed na registratie.

Zorg ervoor dat u de http://localhost:5000/profile-route test. Dit zou je moeten omleiden naar http://localhost:5000/unconfirmed.

Ga je gang en bevestig de e-mail, en je hebt toegang tot alle pagina's. Boem!



E-mail opnieuw verzenden

Laten we ten slotte de link voor opnieuw verzenden werkend krijgen. Voeg de volgende weergavefunctie toe aan 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'))

Update nu de unconfirmed.html sjabloon:

{% 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 %}


Derde toets

Je weet hoe het gaat. Zorg er deze keer voor dat u een nieuwe bevestigingsmail verzendt en test de link. Het zou moeten werken.

Tot slot, wat gebeurt er als je jezelf een paar bevestigingslinks stuurt? Zijn elk geldig? Test het uit. Registreer een nieuwe gebruiker en stuur vervolgens een paar nieuwe bevestigingsmails. Probeer te bevestigen met de eerste e-mail. Werkte het? Het zou moeten. Is dit goed? Denk je dat die andere e-mails moeten verlopen als er een nieuwe wordt verzonden?

Doe hier wat onderzoek naar. En test andere webapplicaties die u gebruikt. Hoe gaan ze om met dergelijk gedrag?



Testsuite bijwerken

Akkoord. Dus dat is het voor de hoofdfunctionaliteit. Wat als we de huidige testsuite updaten, want die is, nou ja, kapot.

Voer de tests uit:

$ python manage.py test

U zou de volgende fout moeten zien:

TypeError: __init__() takes at least 4 arguments (3 given)

Om dit te corrigeren, hoeven we alleen de setUp() . bij te werken methode in 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()

Voer nu de tests opnieuw uit. Alles zou moeten slagen!



Conclusie

Er is duidelijk nog veel meer dat we kunnen doen:

  1. Rich vs. platte tekst e-mails - We zouden beide moeten versturen.
  2. Reset wachtwoord e-mail - Deze moeten worden verzonden naar gebruikers die hun wachtwoord zijn vergeten.
  3. Gebruikersbeheer - We moeten gebruikers toestaan ​​hun e-mails en wachtwoorden bij te werken, en wanneer een e-mail wordt gewijzigd, moet dit opnieuw worden bevestigd.
  4. Testen - We moeten meer tests schrijven om de nieuwe functies te dekken.

Download de volledige broncode uit de Github-repository. Reageer hieronder met vragen. Bekijk deel 2.

Fijne feestdagen!



  1. Maak een ER-diagram in pgAdmin

  2. Een gids voor MySQL-indexen

  3. Problemen met UTF-8-tekens; wat ik zie is niet wat ik heb opgeslagen

  4. Verbinding maken met een database met behulp van de Workbench MySQL-client