From 73fa5d11866e1ab36463c3a773b8ca0d3671a709 Mon Sep 17 00:00:00 2001 From: rubenwardy Date: Mon, 14 May 2018 00:40:34 +0100 Subject: [PATCH] Add email support --- app/__init__.py | 4 +-- app/models.py | 9 ++++++ app/tasks/__init__.py | 2 +- app/tasks/emails.py | 11 +++++++ app/templates/emails/verify.html | 17 +++++++++++ app/templates/users/user_profile_page.html | 26 ++++------------ app/views/users.py | 35 +++++++++++++++++++++- config.example.cfg | 12 +++++++- 8 files changed, 91 insertions(+), 25 deletions(-) create mode 100644 app/tasks/emails.py create mode 100644 app/templates/emails/verify.html diff --git a/app/__init__.py b/app/__init__.py index 11d7cca7..506ed27d 100644 --- a/app/__init__.py +++ b/app/__init__.py @@ -1,13 +1,12 @@ from flask import * from flask_user import * import flask_menu as menu +from flask_mail import Mail from flask.ext import markdown from flask_github import GitHub from flask_wtf.csrf import CsrfProtect import os - - app = Flask(__name__) app.config.from_pyfile(os.environ["FLASK_CONFIG"]) @@ -15,6 +14,7 @@ menu.Menu(app=app) markdown.Markdown(app, extensions=["fenced_code"], safe_mode=True, output_format="html5") github = GitHub(app) csrf = CsrfProtect(app) +mail = Mail(app) from . import models, tasks from .views import * diff --git a/app/models.py b/app/models.py index e0bcb2e6..eaa44e80 100644 --- a/app/models.py +++ b/app/models.py @@ -49,6 +49,7 @@ class Permission(enum.Enum): APPROVE_NEW = "APPROVE_NEW" CHANGE_RELEASE_URL = "CHANGE_RELEASE_URL" CHANGE_RANK = "CHANGE_RANK" + CHANGE_EMAIL = "CHANGE_EMAIL" EDIT_EDITREQUEST = "EDIT_EDITREQUEST" # Only return true if the permission is valid for *all* contexts @@ -119,9 +120,17 @@ class User(db.Model, UserMixin): return user.rank.atLeast(UserRank.EDITOR) elif perm == Permission.CHANGE_RANK: return user.rank.atLeast(UserRank.MODERATOR) + elif perm == Permission.CHANGE_EMAIL: + return user == self or user.rank.atLeast(UserRank.MODERATOR) else: raise Exception("Permission {} is not related to users".format(perm.name)) +class UserEmailVerification(db.Model): + id = db.Column(db.Integer, primary_key=True) + user_id = db.Column(db.Integer, db.ForeignKey("user.id")) + email = db.Column(db.String(100)) + token = db.Column(db.String(32)) + user = db.relationship("User", foreign_keys=[user_id]) class Notification(db.Model): id = db.Column(db.Integer, primary_key=True) diff --git a/app/tasks/__init__.py b/app/tasks/__init__.py index 9ee293ea..40a824c4 100644 --- a/app/tasks/__init__.py +++ b/app/tasks/__init__.py @@ -41,4 +41,4 @@ def make_celery(app): celery = make_celery(app) -from . import importtasks, forumtasks +from . import importtasks, forumtasks, emails diff --git a/app/tasks/emails.py b/app/tasks/emails.py new file mode 100644 index 00000000..7040d380 --- /dev/null +++ b/app/tasks/emails.py @@ -0,0 +1,11 @@ +from flask import * +from flask_mail import Message +from app import mail +from app.tasks import celery + +@celery.task() +def sendVerifyEmail(newEmail, token): + msg = Message("Verify email address", recipients=[newEmail]) + msg.body = "This is a verification email!" + msg.html = render_template("emails/verify.html", token=token) + mail.send(msg) diff --git a/app/templates/emails/verify.html b/app/templates/emails/verify.html new file mode 100644 index 00000000..a08b1747 --- /dev/null +++ b/app/templates/emails/verify.html @@ -0,0 +1,17 @@ +

Hello!

+ +

+ This email has been sent to you because someone (hopefully you) + has entered your email address as a user's email. +

+ +

+ If this was you, then please click this link to verify the address: + + {{ url_for('verify_email_page', token=token, _external=True) }} + +

+ +

+ If it wasn't you, then just delete this email. +

diff --git a/app/templates/users/user_profile_page.html b/app/templates/users/user_profile_page.html index 7e280e1a..2d429f15 100644 --- a/app/templates/users/user_profile_page.html +++ b/app/templates/users/user_profile_page.html @@ -42,24 +42,6 @@ {% endif %} - {% if user == current_user %} - - Email: - - {{ user.email }} | - {% if user.email %}change{% else %}add{% endif %} - 🔒 - - - - Password: - - - {% if user.password %}Change password{% else %}Add password{% endif %} - 🔒 - - - {% endif %} @@ -90,10 +72,14 @@
{{ form.hidden_tag() }} - {{ render_field(form.display_name, tabindex=240) }} + {{ render_field(form.display_name, tabindex=230) }} + + {% if user.checkPerm(current_user, "CHANGE_EMAIL") %} + {{ render_field(form.email, tabindex=240) }} + {% endif %} {% if user.checkPerm(current_user, "CHANGE_RANK") %} - {{ render_field(form.rank, tabindex=240) }} + {{ render_field(form.rank, tabindex=250) }} {% endif %} {{ render_submit_field(form.submit, tabindex=280) }} diff --git a/app/views/users.py b/app/views/users.py index e7306c18..a4fb90b9 100644 --- a/app/views/users.py +++ b/app/views/users.py @@ -10,10 +10,12 @@ from wtforms import * from wtforms.validators import * from .utils import rank_required, randomString from app.tasks.forumtasks import checkForumAccount +from app.tasks.emails import sendVerifyEmail # Define the User profile form class UserProfileForm(FlaskForm): - display_name = StringField("Display name") + display_name = StringField("Display name", [InputRequired(), Length(2, 20)]) + email = StringField("Email") rank = SelectField("Rank", [InputRequired()], choices=UserRank.choices(), coerce=UserRank.coerce, default=UserRank.NEW_MEMBER) submit = SubmitField("Save") @@ -48,6 +50,21 @@ def user_profile_page(username): else: flash("Can't promote a user to a rank higher than yourself!", "error") + if user.checkPerm(current_user, Permission.CHANGE_EMAIL): + newEmail = form["email"].data + if newEmail != user.email: + token = randomString(32) + + ver = UserEmailVerification() + ver.user = user + ver.token = token + ver.email = newEmail + db.session.add(ver) + db.session.commit() + + task = sendVerifyEmail.delay(newEmail, token) + return redirect(url_for("check_task", id=task.id, r=url_for("user_profile_page", username=username))) + # Save user_profile db.session.commit() @@ -96,3 +113,19 @@ def user_claim_page(): flash("Unknown claim type", "error") return render_template("users/claim.html", username=username, key=randomString(32)) + +@app.route("/users/verify/") +def verify_email_page(): + token = request.args.get("token") + ver = UserEmailVerification.query.filter_by(token=token).first() + if ver is None: + flash("Unknown verification token!", "error") + else: + ver.user.email = ver.email + db.session.delete(ver) + db.session.commit() + + if current_user.is_authenticated: + return redirect(url_for("user_profile_page", username=current_user.username)) + else: + return redirect(url_for("home_page")) diff --git a/config.example.cfg b/config.example.cfg index 925da5c3..53313c6d 100644 --- a/config.example.cfg +++ b/config.example.cfg @@ -1,4 +1,6 @@ USER_APP_NAME="Content DB" +SERVER_NAME="content.minetest.net" +BASE_URL="http://" + SERVER_NAME SECRET_KEY="" WTF_CSRF_SECRET_KEY="" @@ -8,9 +10,17 @@ SQLALCHEMY_DATABASE_URI = "sqlite:///../db.sqlite" GITHUB_CLIENT_ID = "" GITHUB_CLIENT_SECRET = "" -BASE_URL="http://localhost:5000/" +CELERY_BROKER_URL='redis://localhost:6379' +CELERY_RESULT_BACKEND='redis://localhost:6379' UPLOAD_FOLDER="tmp" USER_ENABLE_REGISTER = False USER_ENABLE_CHANGE_USERNAME = False +s +MAIL_USERNAME="" +MAIL_PASSWORD="" +MAIL_DEFAULT_SENDER="" +MAIL_SERVER="" +MAIL_PORT=587 +MAIL_USE_TLS=True