Skip to content

Commit

Permalink
refactor_mail_confirmation
Browse files Browse the repository at this point in the history
  • Loading branch information
fabianfreyer committed Oct 27, 2020
1 parent 9413217 commit 5ac6ee9
Show file tree
Hide file tree
Showing 8 changed files with 218 additions and 176 deletions.
12 changes: 0 additions & 12 deletions app/user/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -47,18 +47,6 @@ def login_required(f):
def wrapped(*args, **kwargs):
if not current_user or current_user.is_anonymous:
return redirect(url_for("user.login"))
if not current_user.confirm_mail or not current_user.confirm_mail["confirmed"]:
if current_user.confirm_mail and current_user.confirm_mail["token"]:
flash("Your mail is not confirmed yet.", "warning")
return redirect(url_for("user.confirm_mail_resend"))
else:
current_user.confirm_mail_start()
flash(
"Your mail is not confirmed yet. A confirmation mail has been sent now",
"warning",
)
logout_user()
return redirect(url_for("user.login"))
return f(*args, **kwargs)

return wrapped
Expand Down
6 changes: 4 additions & 2 deletions app/user/admin.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
from . import groups_required, user_blueprint, login_required
from .models import User
from .views import MailInUseValidator
from .helpers import UserEmailChangeRequest


class UserEditForm(FlaskForm):
Expand Down Expand Up @@ -100,9 +101,10 @@ def edit_user(username, back_url=None):
)

if not current_user.is_admin and old_mail != form.mail.data:
user.confirm_mail_start()
UserEmailChangeRequest(user.username, form.mail.data)
flash(
"E-Mail changed, new confirmation required. Check your mails", "warning"
"A confirmation email will be sent to your updated email address.",
"warning",
)

flash("User information changed", "success")
Expand Down
226 changes: 167 additions & 59 deletions app/user/helpers.py
Original file line number Diff line number Diff line change
@@ -1,60 +1,168 @@
from flask import current_app, url_for
from flask import current_app, url_for, render_template, flash, redirect
from flask_mail import Message


def send_password_reset_mail(user):
msg = Message(
f"{current_app.config['BRANDING']}: Passwort zurücksetzen",
recipients=[user.mail],
sender=current_app.config["MAIL_DEFAULT_SENDER"],
)
url = url_for(
"user.reset_password_finish",
username=user.username,
token=user.reset_password[0],
_external=True,
)

msg.body = """Hallo Benutzer "{0.username}",
du kannst dein Passwort auf folgender Website zurücksetzen:
{1}
Der Link ist für 1 Tag gültig.
Viele Grüße
Dein {2}}""".format(
user, url, current_app.config["BRANDING"]
)

current_app.mail.send(msg)


def send_confirm_mail(user):
msg = Message(
f"{current_app.config['BRANDING']}: E-Mail bestätigen",
recipients=[user.mail],
sender=current_app.config["MAIL_DEFAULT_SENDER"],
)
url = url_for(
"user.confirm_mail_finish",
username=user.username,
token=user.confirm_mail["token"],
_external=True,
)

msg.body = """Hallo Benutzer "{0.username}",
um deine E-Mail zu bestätigen, gehe bitte auf folgende Website:
{1}
Der Link ist für 1 Tag gültig.
Viele Grüße
Dein {2}}""".format(
user, url, current_app.config["BRANDING"]
)

current_app.mail.send(msg)
from .models import User


class EmailConfirmation(object):
_confirm_mail_token = None
TOKEN_NAMESPACE = "confirm_mail"

@classmethod
def get(cls, token):
key = cls.cache_key(token)
print(key)
instance = current_app.cache.get(key)
return instance

@classmethod
def cache_key(cls, token):
return "user/{namespace}/{token}".format(
namespace=cls.TOKEN_NAMESPACE, token=token
)

def __init__(self, username, mail, subject, body):
self.subject = subject
self.body = body
self.send_mail(username, mail)
cache_key = self.cache_key(self.confirm_mail_token)
print(cache_key)
current_app.cache.set(cache_key, self)

@property
def confirm_mail_token(self):
if self._confirm_mail_token is None:
import secrets

self._confirm_mail_token = secrets.token_urlsafe(
current_app.config["CONFIRM_MAIL_TOKEN_LENGTH"]
)
return self._confirm_mail_token

def send_mail(self, username, mail):
msg = Message(
self.subject,
recipients=[mail],
sender=current_app.config["MAIL_DEFAULT_SENDER"],
)

msg.body = self.body

if current_app.config["MAIL_SUPPRESS_SEND"]:
# log mail instead
print(" * Would send the following mail:")
print(msg)

current_app.mail.send(msg)

def confirm(self):
raise NotImplementedError()


class UserCreationRequest(EmailConfirmation):
"""
Represents a user with an unconfirmed email. Not added to the LDAP yet, but
saved in the cache.
"""

def __init__(self, username, password, mail, *args, **kwargs):
kwargs.update(
{
"username": username,
"password": password,
"mail": mail,
}
)

self.params = (args, kwargs)
subject = "{branding}: E-Mail bestätigen".format(
branding=current_app.config["BRANDING"]
)
url = url_for(
"user.confirm_mail_finish",
token=self.confirm_mail_token,
_external=True,
)
body = render_template(
"confirm_mail.j2",
username=username,
url=url,
branding=current_app.config["BRANDING"],
)
super(UserCreationRequest, self).__init__(username, mail, subject, body)

def confirm(self):
User.create(*self.params[0], **self.params[1])
flash("Your user account has been created!", "success")
return redirect("/")


class UserEmailChangeRequest(EmailConfirmation):
def __init__(self, username, mail):
self.username = username
self.mail = mail
subject = "{branding}: E-Mail bestätigen".format(
branding=current_app.config["BRANDING"]
)

url = url_for(
"user.confirm_mail_finish",
token=self.confirm_mail_token,
_external=True,
)

body = render_template(
"confirm_mail.j2",
username=username,
url=url,
branding=current_app.config["BRANDING"],
)
super(UserEmailChangeRequest, self).__init__(username, mail, subject, body)

def confirm(self):
user = User.get(self.username)

if not user:
raise Exception("Invalid user in email change request!")

user.mail = self.mail
user.save()
flash("E-Mail confirmed", "success")
return redirect("/")


class PasswordResetRequest(EmailConfirmation):
TOKEN_NAMESPACE = "reset_password"

def __init__(self, username):
self._user = None
self.username = username
subject = "{branding}: Passwort zurücksetzen".format(
branding=current_app.config["BRANDING"]
)
url = url_for(
"user.reset_password_finish",
token=self.confirm_mail_token,
_external=True,
)

body = render_template(
"password_reset_mail.j2",
username=username,
url=url,
branding=current_app.config["BRANDING"],
)
super(PasswordResetRequest, self).__init__(
username, self.user.mail, subject, body
)

@property
def user(self):
if not self._user:
self._user = User.get(self.username)
return self._user

def confirm(self, password):
self.user.password = password
self.user.save()
flash("New password has been set.", "info")
return redirect(url_for("user.login"))
41 changes: 0 additions & 41 deletions app/user/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,11 +4,8 @@
import time
from flask_login import UserMixin, AnonymousUserMixin
from flask import current_app

from app.orm import LDAPOrm

from .helpers import send_password_reset_mail, send_confirm_mail


class AnonymousUser(AnonymousUserMixin):
@property
Expand Down Expand Up @@ -40,8 +37,6 @@ def __init__(self, username=None, dn=None, firstName=None, surname=None, mail=No
self._full_name = None
self._password = None
self.mail = mail
self.reset_password = None
self.confirm_mail = None

# FIXME: This could be simplified to just create a User object, populate it,
@staticmethod
Expand Down Expand Up @@ -126,15 +121,13 @@ def _orm_mapping_load(self, entry):
self.surname = entry.sn.value
self._full_name = entry.cn.value
self.mail = entry.mail.value
self.data = entry.description.value

def _orm_mapping_save(self, entry):
# FIXME: It would be nice if the ORM could somehow automagically
# build up this mapping.
entry.sn = self.surname
entry.cn = self.full_name
entry.givenName = self.firstName
entry.description = self.data
if self.password:
entry.userPassword = self._password
if self.mail:
Expand All @@ -152,40 +145,6 @@ def full_name(self):
firstName=self.firstName, surname=self.surname
)

@property
def data(self):
return json.dumps(
{"confirm_mail": self.confirm_mail, "reset_password": self.reset_password}
)

@data.setter
def data(self, value):
try:
jsonData = json.loads(value)
except:
self.confirm_mail = None
self.reset_password = None
return
self.confirm_mail = jsonData["confirm_mail"]
self.reset_password = jsonData["reset_password"]

def reset_password_start(self):
self.reset_password = (
binascii.hexlify(os.urandom(20)).decode("ascii"),
int(time.time()) + 24 * 60 * 60,
)
send_password_reset_mail(self)
self.save()

def confirm_mail_start(self):
self.confirm_mail = {
"confirmed": False,
"token": binascii.hexlify(os.urandom(20)).decode("ascii"),
"valid_till": int(time.time()) + 24 * 60 * 60,
}
send_confirm_mail(self)
self.save()


class Group(LDAPOrm):
# This doesn't really work either, so we have to overload _basedn
Expand Down
10 changes: 10 additions & 0 deletions app/user/templates/confirm_mail.j2
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
Hallo {{username}},

um deine E-Mail zu bestätigen, gehe bitte auf folgende Website:

{{url}}

Der Link ist für 1 Tag gültig.

Viele Grüße
Dein {{branding}}
10 changes: 10 additions & 0 deletions app/user/templates/password_reset_mail.j2
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
Hallo {{username}},

du kannst dein Passwort auf folgender Website zurücksetzen:

{{ url }}

Der Link ist für 1 Tag gültig.

Viele Grüße
Dein {{ branding }}
Loading

0 comments on commit 5ac6ee9

Please sign in to comment.