aboutsummaryrefslogtreecommitdiff
path: root/app
diff options
context:
space:
mode:
authorGertjan van den Burg <gertjanvandenburg@gmail.com>2019-03-26 16:17:11 +0000
committerGertjan van den Burg <gertjanvandenburg@gmail.com>2019-03-26 16:17:11 +0000
commit98f0fcdcbdbbd91a2a4da6b44229a178ddb38d31 (patch)
treeec6f1254bab7e65954694704a20c947fa268649e /app
parentAdd email confirmation field (diff)
downloadAnnotateChange-98f0fcdcbdbbd91a2a4da6b44229a178ddb38d31.tar.gz
AnnotateChange-98f0fcdcbdbbd91a2a4da6b44229a178ddb38d31.zip
Add support for email confirmation
Diffstat (limited to 'app')
-rw-r--r--app/admin/decorators.py21
-rw-r--r--app/admin/routes.py2
-rw-r--r--app/auth/email.py16
-rw-r--r--app/auth/routes.py64
-rw-r--r--app/decorators.py40
-rw-r--r--app/main/routes.py3
-rw-r--r--app/models.py17
-rw-r--r--app/templates/auth/not_confirmed.html9
-rw-r--r--app/templates/email/confirm_email.html8
-rw-r--r--app/templates/email/confirm_email.txt11
10 files changed, 164 insertions, 27 deletions
diff --git a/app/admin/decorators.py b/app/admin/decorators.py
deleted file mode 100644
index f42b582..0000000
--- a/app/admin/decorators.py
+++ /dev/null
@@ -1,21 +0,0 @@
-# -*- coding: utf-8 -*-
-
-from functools import wraps
-
-from flask import current_app, request
-from flask_login import current_user
-from flask_login.config import EXEMPT_METHODS
-
-
-def admin_required(func):
- @wraps(func)
- def decorated_view(*args, **kwargs):
- if request.method in EXEMPT_METHODS:
- return func(*args, **kwargs)
- elif current_app.config.get("LOGIN_DISABLED"):
- return func(*args, **kwargs)
- elif not current_user.is_admin:
- return current_app.login_manager.unauthorized()
- return func(*args, **kwargs)
-
- return decorated_view
diff --git a/app/admin/routes.py b/app/admin/routes.py
index 8657c35..06a7795 100644
--- a/app/admin/routes.py
+++ b/app/admin/routes.py
@@ -9,7 +9,7 @@ from werkzeug.utils import secure_filename
from app import db
from app.admin import bp
from app.admin.datasets import get_name_from_dataset, md5sum
-from app.admin.decorators import admin_required
+from app.decorators import admin_required
from app.admin.forms import AdminManageTaskForm, AdminAddDatasetForm
from app.models import User, Dataset, Task, Annotation
diff --git a/app/auth/email.py b/app/auth/email.py
index c071518..581c9ce 100644
--- a/app/auth/email.py
+++ b/app/auth/email.py
@@ -4,6 +4,7 @@ from flask import current_app, render_template
from app.email import send_email
+
def send_password_reset_email(user):
token = user.get_reset_password_token()
send_email(
@@ -17,3 +18,18 @@ def send_password_reset_email(user):
"email/reset_password.html", user=user, token=token
),
)
+
+
+def send_email_confirmation_email(user):
+ token = user.get_email_confirmation_token()
+ send_email(
+ "[AnnotateChange] Confirm your email",
+ sender=current_app.config["ADMINS"][0],
+ recipients=[user.email],
+ text_body=render_template(
+ "email/confirm_email.txt", user=user, token=token
+ ),
+ html_body=render_template(
+ "email/confirm_email.html", user=user, token=token
+ ),
+ )
diff --git a/app/auth/routes.py b/app/auth/routes.py
index 7f7229e..7de091e 100644
--- a/app/auth/routes.py
+++ b/app/auth/routes.py
@@ -3,7 +3,7 @@
import datetime
from flask import render_template, flash, redirect, url_for, request
-from flask_login import current_user, login_user, logout_user, login_required
+from flask_login import current_user, login_user, logout_user
from werkzeug.urls import url_parse
@@ -16,8 +16,12 @@ from app.auth.forms import (
ResetPasswordRequestForm,
ResetPasswordForm,
)
+from app.decorators import login_required
from app.models import User
-from app.auth.email import send_password_reset_email
+from app.auth.email import (
+ send_password_reset_email,
+ send_email_confirmation_email,
+)
@bp.route("/login", methods=("GET", "POST"))
@@ -33,6 +37,8 @@ def login():
flash("Invalid username or password", "error")
return redirect(url_for("auth.login"))
login_user(user, remember=form.remember_me.data)
+ if not user.is_confirmed:
+ return redirect(url_for("auth.not_confirmed"))
next_page = request.args.get("next")
if not next_page or url_parse(next_page).netloc != "":
next_page = url_for("main.index")
@@ -56,8 +62,14 @@ def register():
user.set_password(form.password.data)
db.session.add(user)
db.session.commit()
- flash("Thank you, you are now a registered user!", "info")
- return redirect(url_for("auth.login"))
+
+ send_email_confirmation_email(user)
+ flash(
+ "An email has been sent to confirm your account, please check your email.",
+ "info",
+ )
+
+ return redirect(url_for("auth.not_confirmed"))
return render_template("auth/register.html", title="Register", form=form)
@@ -94,3 +106,47 @@ def reset_password(token):
flash("Your password has been reset.", "info")
return redirect(url_for("auth.login"))
return render_template("auth/reset_password.html", form=form)
+
+
+@bp.route("/confirm/<token>")
+def confirm_email(token):
+ if current_user.is_authenticated and current_user.is_confirmed:
+ flash("Account is already confirmed.")
+ return redirect(url_for("main.index"))
+ user = User.verify_email_confirmation_token(token)
+ if not user:
+ flash("The confirmation link is invalid or has expired.", "error")
+ return redirect(url_for("main.index"))
+ if user.is_confirmed:
+ flash("Account is already confirmed, please login.", "success")
+ else:
+ user.is_confirmed = True
+ db.session.commit()
+ flash("Account confirmed successfully. Thank you!", "success")
+ return redirect(url_for("main.index"))
+
+
+@bp.route("/not_confirmed")
+def not_confirmed():
+ if current_user.is_anonymous:
+ flash("Please login before accessing this page.")
+ return redirect(url_for("auth.login"))
+ if current_user.is_confirmed:
+ flash("Account is already confirmed.")
+ return redirect(url_for("main.index"))
+ flash("Please confirm your account before moving on.", "info")
+ return render_template("auth/not_confirmed.html")
+
+
+@bp.route("/resend")
+def resend_confirmation():
+ if current_user.is_anonymous:
+ flash("Please login before accessing this page.")
+ return redirect(url_for("auth.login"))
+ if current_user.is_confirmed:
+ flash("Account is already confirmed.")
+ return redirect(url_for("main.index"))
+ send_email_confirmation_email(current_user)
+ email = current_user.email
+ flash("A new confirmation has been sent to %s." % email, "success")
+ return redirect(url_for("auth.not_confirmed"))
diff --git a/app/decorators.py b/app/decorators.py
new file mode 100644
index 0000000..d5b8821
--- /dev/null
+++ b/app/decorators.py
@@ -0,0 +1,40 @@
+# -*- coding: utf-8 -*-
+
+from functools import wraps
+
+from flask import current_app, request, redirect, flash, url_for
+from flask_login import current_user
+from flask_login.config import EXEMPT_METHODS
+
+
+def admin_required(func):
+ @wraps(func)
+ def decorated_view(*args, **kwargs):
+ if request.method in EXEMPT_METHODS:
+ return func(*args, **kwargs)
+ elif current_app.config.get("LOGIN_DISABLED"):
+ return func(*args, **kwargs)
+ elif not current_user.is_authenticated:
+ return current_app.login_manager.unauthorized()
+ elif not current_user.is_admin:
+ return current_app.login_manager.unauthorized()
+ return func(*args, **kwargs)
+
+ return decorated_view
+
+
+def login_required(func):
+ @wraps(func)
+ def decorated_view(*args, **kwargs):
+ if request.method in EXEMPT_METHODS:
+ return func(*args, **kwargs)
+ elif current_app.config.get("LOGIN_DISABLED"):
+ return func(*args, **kwargs)
+ elif not current_user.is_authenticated:
+ return current_app.login_manager.unauthorized()
+ elif not current_user.is_confirmed:
+ flash("hello world")
+ return redirect(url_for("auth.not_confirmed"))
+ return func(*args, **kwargs)
+
+ return decorated_view
diff --git a/app/main/routes.py b/app/main/routes.py
index 5879b28..7e9505a 100644
--- a/app/main/routes.py
+++ b/app/main/routes.py
@@ -3,9 +3,10 @@
import datetime
from flask import render_template, flash, url_for, redirect, request
-from flask_login import current_user, login_required
+from flask_login import current_user
from app import db
+from app.decorators import login_required
from app.main import bp
from app.models import Annotation, Task
from app.main.datasets import load_data_for_chart
diff --git a/app/models.py b/app/models.py
index 86e15f0..3bc394c 100644
--- a/app/models.py
+++ b/app/models.py
@@ -50,6 +50,23 @@ class User(UserMixin, db.Model):
return None
return User.query.get(_id)
+ def get_email_confirmation_token(self, expires_in=3600):
+ return jwt.encode(
+ {"email": self.email, "exp": time.time() + expires_in},
+ current_app.config["SECRET_KEY"],
+ algorithm="HS256",
+ ).decode("utf-8")
+
+ @staticmethod
+ def verify_email_confirmation_token(token):
+ try:
+ _email = jwt.decode(
+ token, current_app.config["SECRET_KEY"], algorithms=["HS256"]
+ )["email"]
+ except:
+ return None
+ return User.query.filter_by(email=_email).first()
+
class Dataset(db.Model):
id = db.Column(db.Integer, primary_key=True)
diff --git a/app/templates/auth/not_confirmed.html b/app/templates/auth/not_confirmed.html
new file mode 100644
index 0000000..3b55afd
--- /dev/null
+++ b/app/templates/auth/not_confirmed.html
@@ -0,0 +1,9 @@
+{% extends "base.html" %}
+
+{% block app_content %}
+<h1>Welcome to AnnotateChange</h1>
+<p>You have not yet confirmed your email address. Please check your inbox for
+the confirmation email. The email might have landed in your spam folder.</p>
+<p>If you haven't received and email, please <a href="{{ url_for('auth.resend_confirmation') }}">click here</a> to resend it.</p>
+
+{% endblock %}
diff --git a/app/templates/email/confirm_email.html b/app/templates/email/confirm_email.html
new file mode 100644
index 0000000..4581804
--- /dev/null
+++ b/app/templates/email/confirm_email.html
@@ -0,0 +1,8 @@
+<p>Dear {{ user.username }},
+<p>Welcome to AnnotateChange!</p>
+<p>Please confirm your email by <a href="{{ url_for('auth.confirm_email', token=token, _external=True) }}">clicking here</a>.
+</p>
+<p>Alternatively, you can paste the following link in your browser's address bar:</p>
+<p>{{ url_for('auth.confirm_email', token=token, _external=True) }}</p>
+<p>Sincerely,</p>
+<p>The AnnotateChange Team</p>
diff --git a/app/templates/email/confirm_email.txt b/app/templates/email/confirm_email.txt
new file mode 100644
index 0000000..b17c66d
--- /dev/null
+++ b/app/templates/email/confirm_email.txt
@@ -0,0 +1,11 @@
+Dear {{ user.username }},
+
+Welcome to AnnotateChange!
+
+Please confirm your email by clicking on the following link:
+
+{{ url_for('auth.confirm_email', token=token, _external=True) }}
+
+Sincerely,
+
+The AnnotateChange Team