Browse Source

registration: ADD email validation

pull/61/head
Darks 1 year ago
parent
commit
8bf825f9ea
Signed by: Darks GPG Key ID: F61F10FA138E797C
  1. 2
      REQUIREMENTS.md
  2. 2
      app/__init__.py
  3. 2
      app/models/users.py
  4. 38
      app/routes/account/account.py
  5. 24
      app/routes/account/login.py
  6. 14
      app/templates/email/activate.html
  7. 9
      app/templates/email/activate.md
  8. 24
      app/utils/send_mail.py
  9. 9
      config.py
  10. 28
      migrations/versions/acf72cf31eea_added_email_validated_property_in_model_.py

2
REQUIREMENTS.md

@ -8,10 +8,12 @@ La liste de paquets fourni est pour Archlinux, les paquets peuvent avoir des nom
python3
python-flask
python-flask-login
python-flask-mail
python-flask-migrate
python-flask-script
python-flask-sqlalchemy
python-flask-wtf
python-itsdangerous
python-ldap
python-uwsgi
python-psycopg2

2
app/__init__.py

@ -2,6 +2,7 @@ from flask import Flask, g
from flask_sqlalchemy import SQLAlchemy
from flask_migrate import Migrate
from flask_login import LoginManager
from flask_mail import Mail
from config import Config
import time
@ -14,6 +15,7 @@ if Config.SECRET_KEY == "a-random-secret-key":
db = SQLAlchemy(app)
migrate = Migrate(app, db)
mail = Mail(app)
login = LoginManager(app)
login.login_view = 'login'

2
app/models/users.py

@ -76,6 +76,7 @@ class Member(User):
norm = db.Column(db.Unicode(V5Config.USER_NAME_MAXLEN), index=True,
unique=True)
email = db.Column(db.Unicode(120), index=True, unique=True)
email_confirmed = db.Column(db.Boolean)
password_hash = db.Column(db.String(255))
xp = db.Column(db.Integer)
register_date = db.Column(db.Date, default=date.today)
@ -118,6 +119,7 @@ class Member(User):
self.name = name
self.norm = unicode_names.normalize(name)
self.email = email
self.email_confirmed = not V5Config.ENABLE_EMAIL_CONFIRMATION
if not V5Config.USE_LDAP:
self.set_password(password)
# Workflow with LDAP enabled is User → Postgresql → LDAP → set password

38
app/routes/account/account.py

@ -1,10 +1,12 @@
from flask import redirect, url_for, request, flash
from flask import redirect, url_for, request, flash, abort
from flask_login import login_required, current_user, logout_user
from app import app, db
from app.forms.account import UpdateAccountForm, RegistrationForm, DeleteAccountForm
from app.models.users import Member
from app.utils.render import render
from app.utils.send_mail import send_validation_mail
import app.utils.ldap as ldap
from itsdangerous import URLSafeTimedSerializer
from config import V5Config
@ -66,13 +68,43 @@ def register():
ldap.add_member(member)
ldap.set_password(member, form.password.data)
flash('Inscription réussie', 'ok')
# Email validation message
send_validation_mail(member.name, member.email)
return redirect(url_for('validation') + "?email=" + form.email.data)
return render('account/register.html', title='Register', form=form)
@app.route('/register/validation/', methods=['GET', 'POST'])
@app.route('/inscription/validation', methods=['GET'])
def validation():
mail = request.args['email']
try:
mail = request.args['email']
except Exception as e:
print("Error: {e}")
abort(404)
if current_user.is_authenticated:
return redirect(url_for('index'))
return render('account/validation.html', mail=mail)
@app.route('/inscription/validation/<token>', methods=['GET'])
def activate_account(token):
if current_user.is_authenticated:
return redirect(url_for('index'))
try:
ts = URLSafeTimedSerializer(app.config["SECRET_KEY"])
email = ts.loads(token, salt="email-confirm-key", max_age=86400)
except Exception as e:
print(f"Error: {e}")
abort(404)
m = Member.query.filter_by(email=email).first_or_404()
m.email_confirmed = True
db.session.add(m)
db.session.commit()
flash("L'email a bien été confirmé", "ok")
return redirect(url_for('login'))

24
app/routes/account/login.py

@ -6,11 +6,19 @@ from app.forms.login import LoginForm
from app.models.users import Member
from app.models.privs import Group
from app.utils.render import render
from app.utils.send_mail import send_validation_mail
from config import V5Config
@app.route('/connexion', methods=['GET', 'POST'])
def login():
# If something failed, return abort("Message")
def _abort(msg):
flash(msg, 'error')
if request.referrer:
return redirect(request.referrer)
return redirect(url_for('index'))
if current_user.is_authenticated:
return redirect(url_for('index'))
@ -24,17 +32,17 @@ def login():
# Check if member can login
if member is not None and "No login" in [g.name for g in member.groups]:
flash('Cet utilisateur ne peut pas se connecter', 'error')
if request.referrer:
return redirect(request.referrer)
return redirect(url_for('index'))
return _abort('Cet utilisateur ne peut pas se connecter')
# Check if password is ok
if member is None or not member.check_password(form.password.data):
flash('Pseudo ou mot de passe invalide', 'error')
if request.referrer:
return redirect(request.referrer)
return redirect(url_for('index'))
return _abort('Pseudo ou mot de passe invalide')
# Check if user is activated
if member.email_confirmed == False:
# Send another mail
send_validation_mail(member.name, member.email)
return _abort(f"Email non confirmé. Un mail de confirmation a de nouveau été envoyé à l'adresse {member.email}")
# Login & update time-based trophies
login_user(member, remember=form.remember_me.data,

14
app/templates/email/activate.html

@ -0,0 +1,14 @@
<html>
<body>
<div>Bonjour {{ name }},</div>
<div>Bienvenue sur Planète Casio !</div>
<div>Afin de pouvoir vous connecter et utiliser nos services, merci de confirmer cette adresse email en cliquant <a href="{{ confirm_url }}">ici</a>.</div>
<div>Si ça ne fonctionne pas, utilisez ce lien : <a href="{{ confirm_url }}">{{ confirm_url }}</a></div>
<div>À bientôt sur Planète Casio !</div>
<div>L'équipe du site.</div>
</body>
</html>

9
app/templates/email/activate.md

@ -0,0 +1,9 @@
Bonjour {{ name }},
Bienvenue sur Planète Casio !
Afin de pouvoir vous connecter et utiliser nos services, merci de confirmer cette adresse email en cliquant sur ce lien : {{ confirm_url }}
À bientôt sur Planète Casio !
L'équipe du site.

24
app/utils/send_mail.py

@ -0,0 +1,24 @@
from flask import url_for, render_template
from flask_mail import Message
from app import app, mail
from itsdangerous import URLSafeTimedSerializer
from config import V5Config
def send_mail(dest, subject, html="", body=""):
m = Message(recipients=[dest], subject=subject, html=html, body=body)
mail.send(m)
def send_validation_mail(name, email):
ts = URLSafeTimedSerializer(app.config["SECRET_KEY"])
token = ts.dumps(email, salt='email-confirm-key')
confirm_url = f"https://{V5Config.DOMAIN}" \
+ url_for('activate_account', token=token)
subject = "Confirmez votre email pour compléter l'inscription"
html = render_template('email/activate.html', confirm_url=confirm_url,
name=name)
body = render_template('email/activate.md', confirm_url=confirm_url,
name=name)
if V5Config.SEND_MAILS:
send_mail(member.email, subject, html=html, body=body)
else:
print(f"Email confirmation url: {confirm_url}")

9
config.py

@ -16,9 +16,14 @@ class Config(object):
+ LocalConfig.DB_NAME
SQLALCHEMY_TRACK_MODIFICATIONS = False
MAIL_DEFAULT_SENDER = "noreply@v5.planet-casio.com"
MAIL_SUPPRESS_SEND = None
class DefaultConfig(object):
"""Every value here can be overrided in the local_config.py class"""
# Domain
DOMAIN = "v5.planet-casio.com"
# Length allocated to privilege names (slugs)
PRIVS_MAXLEN = 64
# Forbidden user names
@ -57,6 +62,10 @@ class DefaultConfig(object):
AVATARS_FOLDER = '/avatar/folder/'
# Enable guest post
ENABLE_GUEST_POST = True
# Disable email confimation
ENABLE_EMAIL_CONFIRMATION = True
# Send emails
SEND_MAILS = True
class V5Config(LocalConfig, DefaultConfig):
# Values put here cannot be overidden with local_config

28
migrations/versions/acf72cf31eea_added_email_validated_property_in_model_.py

@ -0,0 +1,28 @@
"""Added email_validated property in model `Member`
Revision ID: acf72cf31eea
Revises: 6fd4c15b8a7b
Create Date: 2020-07-21 20:29:44.876704
"""
from alembic import op
import sqlalchemy as sa
# revision identifiers, used by Alembic.
revision = 'acf72cf31eea'
down_revision = '6fd4c15b8a7b'
branch_labels = None
depends_on = None
def upgrade():
# ### commands auto generated by Alembic - please adjust! ###
op.add_column('member', sa.Column('email_confirmed', sa.Boolean(), nullable=True))
# ### end Alembic commands ###
def downgrade():
# ### commands auto generated by Alembic - please adjust! ###
op.drop_column('member', 'email_confirmed')
# ### end Alembic commands ###
Loading…
Cancel
Save