diff --git a/REQUIREMENTS.md b/REQUIREMENTS.md index a5419e1..c65f0fc 100644 --- a/REQUIREMENTS.md +++ b/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 diff --git a/app/__init__.py b/app/__init__.py index c5580f4..8a53d17 100644 --- a/app/__init__.py +++ b/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' diff --git a/app/models/users.py b/app/models/users.py index 1b7f4c9..1fc14ad 100644 --- a/app/models/users.py +++ b/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 diff --git a/app/routes/account/account.py b/app/routes/account/account.py index 6b085ab..51a4318 100644 --- a/app/routes/account/account.py +++ b/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/', 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')) diff --git a/app/routes/account/login.py b/app/routes/account/login.py index 5f952c3..37d1f03 100644 --- a/app/routes/account/login.py +++ b/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, diff --git a/app/templates/email/activate.html b/app/templates/email/activate.html new file mode 100644 index 0000000..7d9ed2c --- /dev/null +++ b/app/templates/email/activate.html @@ -0,0 +1,14 @@ + + +
Bonjour {{ name }},
+ +
Bienvenue sur Planète Casio !
+ +
Afin de pouvoir vous connecter et utiliser nos services, merci de confirmer cette adresse email en cliquant ici.
+
Si ça ne fonctionne pas, utilisez ce lien : {{ confirm_url }}
+ +
À bientôt sur Planète Casio !
+ +
L'équipe du site.
+ + diff --git a/app/templates/email/activate.md b/app/templates/email/activate.md new file mode 100644 index 0000000..0d9345a --- /dev/null +++ b/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. diff --git a/app/utils/send_mail.py b/app/utils/send_mail.py new file mode 100644 index 0000000..7a32c3a --- /dev/null +++ b/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}") diff --git a/config.py b/config.py index 69c5deb..f0c0e54 100644 --- a/config.py +++ b/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 diff --git a/migrations/versions/acf72cf31eea_added_email_validated_property_in_model_.py b/migrations/versions/acf72cf31eea_added_email_validated_property_in_model_.py new file mode 100644 index 0000000..566f3cc --- /dev/null +++ b/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 ###