aboutsummaryrefslogtreecommitdiff
path: root/ihatemoney
diff options
context:
space:
mode:
author0livd <github@destras.fr>2017-12-15 17:10:28 +0100
committerAlexis Metaireau <alexis@notmyidea.org>2017-12-15 17:10:28 +0100
commit8a68ac0d5b85f896dd59042c207bc63c3d026f7d (patch)
tree888f9729d656eb0bec4f2e329301776bd31e1a9e /ihatemoney
parent2866c868d55d197de8c39c34debc878b38929d98 (diff)
downloadihatemoney-mirror-8a68ac0d5b85f896dd59042c207bc63c3d026f7d.zip
ihatemoney-mirror-8a68ac0d5b85f896dd59042c207bc63c3d026f7d.tar.gz
ihatemoney-mirror-8a68ac0d5b85f896dd59042c207bc63c3d026f7d.tar.bz2
Use token based auth in invitation e-mails (#280)
* Use token based auth in invitation e-mails Invitation e-mails no longer contain the clear text project password * Skip invite page after project creation - Replace ``The project identifier is demo, remember it!`` by ``Invite other people to join this project!`` (linking to the invite page) - Encourage users to share the project password via other communication means in the reminder email
Diffstat (limited to 'ihatemoney')
-rw-r--r--ihatemoney/models.py24
-rw-r--r--ihatemoney/templates/authenticate.html5
-rw-r--r--ihatemoney/templates/invitation_mail.en4
-rw-r--r--ihatemoney/templates/invitation_mail.fr4
-rw-r--r--ihatemoney/templates/list_bills.html2
-rw-r--r--ihatemoney/templates/reminder_mail.en4
-rw-r--r--ihatemoney/templates/reminder_mail.fr2
-rw-r--r--ihatemoney/templates/send_invites.html14
-rw-r--r--ihatemoney/tests/tests.py23
-rw-r--r--ihatemoney/translations/fr/LC_MESSAGES/messages.mobin9559 -> 9762 bytes
-rw-r--r--ihatemoney/translations/fr/LC_MESSAGES/messages.po40
-rw-r--r--ihatemoney/web.py32
12 files changed, 97 insertions, 57 deletions
diff --git a/ihatemoney/models.py b/ihatemoney/models.py
index c801b74..9e11054 100644
--- a/ihatemoney/models.py
+++ b/ihatemoney/models.py
@@ -5,8 +5,8 @@ from flask_sqlalchemy import SQLAlchemy, BaseQuery
from flask import g, current_app
from sqlalchemy import orm
-from itsdangerous import (TimedJSONWebSignatureSerializer
- as Serializer, BadSignature, SignatureExpired)
+from itsdangerous import (TimedJSONWebSignatureSerializer, URLSafeSerializer,
+ BadSignature, SignatureExpired)
db = SQLAlchemy()
@@ -201,22 +201,32 @@ class Project(db.Model):
db.session.delete(self)
db.session.commit()
- def generate_token(self, expiration):
+ def generate_token(self, expiration=0):
"""Generate a timed and serialized JsonWebToken
:param expiration: Token expiration time (in seconds)
"""
- serializer = Serializer(current_app.config['SECRET_KEY'], expiration)
- return serializer.dumps({'project_id': self.id}).decode('utf-8')
+ if expiration:
+ serializer = TimedJSONWebSignatureSerializer(
+ current_app.config['SECRET_KEY'],
+ expiration)
+ token = serializer.dumps({'project_id': self.id}).decode('utf-8')
+ else:
+ serializer = URLSafeSerializer(current_app.config['SECRET_KEY'])
+ token = serializer.dumps({'project_id': self.id})
+ return token
@staticmethod
- def verify_token(token):
+ def verify_token(token, token_type="timed_token"):
"""Return the project id associated to the provided token,
None if the provided token is expired or not valid.
:param token: Serialized TimedJsonWebToken
"""
- serializer = Serializer(current_app.config['SECRET_KEY'])
+ if token_type == "timed_token":
+ serializer = TimedJSONWebSignatureSerializer(current_app.config['SECRET_KEY'])
+ else:
+ serializer = URLSafeSerializer(current_app.config['SECRET_KEY'])
try:
data = serializer.loads(token)
except SignatureExpired:
diff --git a/ihatemoney/templates/authenticate.html b/ihatemoney/templates/authenticate.html
index 98914d0..4e8eb77 100644
--- a/ihatemoney/templates/authenticate.html
+++ b/ihatemoney/templates/authenticate.html
@@ -3,8 +3,9 @@
<h2>Authentication</h2>
{% if create_project %}
-<p class="info">{{ _("The project you are trying to access do not exist, do you want
-to") }} <a href="{{ url_for(".create_project", project_id=create_project) }}">{{ _("create it") }}</a>{{ _("?") }}
+<p class="info">{{ _("The project you are trying to access do not exist, do you want to") }}
+<a href="{{ url_for(".create_project", project_id=create_project) }}">
+ {{ _("create it") }}</a>{{ _("?") }}
</p>
{% endif %}
<form class="form-horizontal" method="POST" accept-charset="utf-8">
diff --git a/ihatemoney/templates/invitation_mail.en b/ihatemoney/templates/invitation_mail.en
index 03f5141..eeaafdb 100644
--- a/ihatemoney/templates/invitation_mail.en
+++ b/ihatemoney/templates/invitation_mail.en
@@ -4,7 +4,9 @@ Someone using the email address {{ g.project.contact_email }} invited you to sha
It's as simple as saying what did you paid for, for who, and how much did it cost you, we are caring about the rest.
-You can access it here: {{ config['SITE_URL'] }}{{ url_for(".list_bills") }} and the private code is "{{ g.project.password }}".
+You can log in using this link: {{ url_for(".authenticate", _external=True, token=g.project.generate_token()) }}.
+Once logged in you can use the following link which is easier to remember: {{ url_for(".list_bills", _external=True) }}
+If your cookie gets deleted or if you log out, you will need to log back in using the first link.
Enjoy,
Some weird guys (with beards)
diff --git a/ihatemoney/templates/invitation_mail.fr b/ihatemoney/templates/invitation_mail.fr
index 53698dd..a95f9e9 100644
--- a/ihatemoney/templates/invitation_mail.fr
+++ b/ihatemoney/templates/invitation_mail.fr
@@ -4,6 +4,8 @@ Quelqu'un avec l'addresse email "{{ g.project.contact_email }}" vous à invité
C'est aussi simple que de dire qui à payé pour quoi, pour qui, et combien celà à coûté, on s'occuppe du reste.
-Vous pouvez accéder à la page ici: {{ config['SITE_URL'] }}{{ url_for(".list_bills") }} et le code est "{{ g.project.password }}".
+Vous pouvez vous authentifier avec le lien suivant: {{ url_for(".authenticate", _external=True, token=g.project.generate_token()) }}.
+Une fois authentifié, vous pouvez utiliser le lien suivant qui est plus facile à mémoriser: {{ url_for(".list_bills", _external=True) }}
+Si votre cookie est supprimé ou si vous vous déconnectez, voous devrez vous réauthentifier en utilisant le premier lien.
Have fun,
diff --git a/ihatemoney/templates/list_bills.html b/ihatemoney/templates/list_bills.html
index 1386636..e4034d4 100644
--- a/ihatemoney/templates/list_bills.html
+++ b/ihatemoney/templates/list_bills.html
@@ -92,7 +92,7 @@
{% endblock %}
{% block content %}
-<div class="identifier">{{ _("The project identifier is") }} <a href="{{ url_for(".list_bills") }}">{{ g.project.id }}</a>, {{ _("remember it!") }}</div>
+<div class="identifier"><a href="{{ url_for(".invite") }}">{{ _("Invite people to join this project!") }}</a></div>
<a id="new-bill" href="{{ url_for(".add_bill") }}" class="btn btn-primary" data-toggle="modal" data-target="#bill-form">{{ _("Add a new bill") }}</a>
<div id="bill-form" class="modal fade show" role="dialog">
diff --git a/ihatemoney/templates/reminder_mail.en b/ihatemoney/templates/reminder_mail.en
index f13da5d..5f9b7d8 100644
--- a/ihatemoney/templates/reminder_mail.en
+++ b/ihatemoney/templates/reminder_mail.en
@@ -3,7 +3,9 @@ Hi,
You have just (or someone else using your email address) created the project "{{ g.project.name }}" to share your expenses.
You can access it here: {{ url_for(".list_bills", _external=True) }} (the identifier is {{ g.project.id }}),
-and the private code is "{{ g.project.password }}".
+and the shared password is "{{ g.project.password }}".
+If you want to share this project with your friends, you can share the identifier and the shared password with them or send them invitations with the following link:
+{{ url_for(".invite", _external=True) }}
Enjoy,
Some weird guys (with beards)
diff --git a/ihatemoney/templates/reminder_mail.fr b/ihatemoney/templates/reminder_mail.fr
index 86c00ff..fbe299a 100644
--- a/ihatemoney/templates/reminder_mail.fr
+++ b/ihatemoney/templates/reminder_mail.fr
@@ -4,5 +4,7 @@ Vous venez de créer le projet "{{ g.project.name }}" pour partager vos dépense
Vous pouvez y accéder ici: {{ url_for(".list_bills", _external=True) }} (l'identifieur est {{ g.project.id }}),
et le code d'accès "{{ g.project.password }}".
+Si vous voulez partager ce projet avec vos amis, vous pouvez partager son identifiant et son code d'accès avec eux ou leur envoyer une invitation avec le lien suivant :
+{{ url_for(".invite", _external=True) }}
Faites en bon usage !
diff --git a/ihatemoney/templates/send_invites.html b/ihatemoney/templates/send_invites.html
index 7b3bdc5..cd96380 100644
--- a/ihatemoney/templates/send_invites.html
+++ b/ihatemoney/templates/send_invites.html
@@ -1,17 +1,15 @@
{% extends "layout.html" %}
-{% block sidebar %}
-<ol>
- <li>{{ _("Create the project") }}</li>
- <li><strong>{{ _("Invite people") }}</strong></li>
- <li><a href="{{ url_for(".list_bills") }}">{{ _("Use it!") }}</a></li>
-</ol>
-{% endblock %}
{% block content %}
<h2>{{ _("Invite people to join this project") }}</h2>
<p>{{ _("Specify a (comma separated) list of email adresses you want to notify about the
creation of this budget management project and we will send them an email for you.") }}</p>
-<p>{{ _("If you prefer, you can") }} <a href="{{ url_for(".list_bills") }}">{{ _("skip this step") }}</a> {{ _("and notify them yourself") }}</p>
+<p>{{ _("If you prefer, you can share the project identifier and the shared
+password by other communication means. Or even directly share the following link:") }}</br>
+<a href="{{ url_for(".authenticate", _external=True, token=g.project.generate_token()) }}">
+ {{ url_for(".authenticate", _external=True, token=g.project.generate_token()) }}
+</a>
+</p>
{% include "display_errors.html" %}
<form class="invites form-horizontal" method="post" accept-charset="utf-8">
diff --git a/ihatemoney/tests/tests.py b/ihatemoney/tests/tests.py
index 123ea36..a421762 100644
--- a/ihatemoney/tests/tests.py
+++ b/ihatemoney/tests/tests.py
@@ -152,6 +152,29 @@ class BudgetTestCase(IhatemoneyTestCase):
# only one message is sent to multiple persons
self.assertEqual(len(outbox), 0)
+ def test_invite(self):
+ """Test that invitation e-mails are sent properly
+ """
+ self.login("raclette")
+ self.post_project("raclette")
+ with self.app.mail.record_messages() as outbox:
+ self.client.post("/raclette/invite",
+ data={"emails": 'toto@notmyidea.org'})
+ self.assertEqual(len(outbox), 1)
+ url_start = outbox[0].body.find('You can log in using this link: ') + 32
+ url_end = outbox[0].body.find('.\n', url_start)
+ url = outbox[0].body[url_start:url_end]
+ self.client.get("/exit")
+ # Test that we got a valid token
+ resp = self.client.get(url, follow_redirects=True)
+ self.assertIn('You probably want to <a href="/raclette/add"', resp.data.decode('utf-8'))
+ # Test empty and invalid tokens
+ self.client.get("/exit")
+ resp = self.client.get("/authenticate")
+ self.assertIn("You either provided a bad token", resp.data.decode('utf-8'))
+ resp = self.client.get("/authenticate?token=token")
+ self.assertIn("You either provided a bad token", resp.data.decode('utf-8'))
+
def test_password_reminder(self):
# test that it is possible to have an email cotaining the password of a
# project in case people forget it (and it happens!)
diff --git a/ihatemoney/translations/fr/LC_MESSAGES/messages.mo b/ihatemoney/translations/fr/LC_MESSAGES/messages.mo
index 249c996..47b801d 100644
--- a/ihatemoney/translations/fr/LC_MESSAGES/messages.mo
+++ b/ihatemoney/translations/fr/LC_MESSAGES/messages.mo
Binary files differ
diff --git a/ihatemoney/translations/fr/LC_MESSAGES/messages.po b/ihatemoney/translations/fr/LC_MESSAGES/messages.po
index 5e030bf..b344098 100644
--- a/ihatemoney/translations/fr/LC_MESSAGES/messages.po
+++ b/ihatemoney/translations/fr/LC_MESSAGES/messages.po
@@ -175,6 +175,10 @@ msgid "Export file format"
msgstr "Format du fichier d'export"
#: web.py:95
+msgid "You either provided a bad token or no project identifier."
+msgstr "L'identifiant du projet ou le token fourni n'est pas correct."
+
+#: web.py:95
msgid "This private code is not the right one"
msgstr "Le code que vous avez entré n'est pas correct"
@@ -271,8 +275,7 @@ msgstr "Retourner à la liste"
#: templates/authenticate.html:6
msgid ""
-"The project you are trying to access do not exist, do you want \n"
-"to"
+"The project you are trying to access do not exist, do you want to"
msgstr "Le projet auquel vous essayez d'acceder n'existe pas. Souhaitez vous"
#: templates/authenticate.html:7
@@ -477,12 +480,12 @@ msgid "reactivate"
msgstr "ré-activer"
#: templates/list_bills.html:88
-msgid "The project identifier is"
-msgstr "L'identifiant de ce projet est"
+msgid "Invite"
+msgstr "Invitez"
#: templates/list_bills.html:88
-msgid "remember it!"
-msgstr "souvenez vous en !"
+msgid "Invite people to join this project!"
+msgstr "Invitez d'autres personnes à rejoindre ce projet !"
#: templates/list_bills.html:89
msgid "Add a new bill"
@@ -536,14 +539,6 @@ msgstr "Vos projets"
msgid "Reset your password"
msgstr "Changez votre mot de passe"
-#: templates/send_invites.html:6
-msgid "Invite people"
-msgstr "Invitez des gens"
-
-#: templates/send_invites.html:7
-msgid "Use it!"
-msgstr "Utilisez le !"
-
#: templates/send_invites.html:11
msgid "Invite people to join this project"
msgstr "Invitez des personnes à rejoindre ce projet"
@@ -551,7 +546,7 @@ msgstr "Invitez des personnes à rejoindre ce projet"
#: templates/send_invites.html:12
msgid ""
"Specify a (comma separated) list of email adresses you want to notify "
-"about the \n"
+"about the\n"
"creation of this budget management project and we will send them an email"
" for you."
msgstr ""
@@ -559,16 +554,11 @@ msgstr ""
"par des virgules. On s'occupe de leur envoyer un email."
#: templates/send_invites.html:14
-msgid "If you prefer, you can"
-msgstr "Si vous préférez vous pouvez"
-
-#: templates/send_invites.html:14
-msgid "skip this step"
-msgstr "sauter cette étape"
-
-#: templates/send_invites.html:14
-msgid "and notify them yourself"
-msgstr "et les avertir vous même"
+msgid "If you prefer, you can share the project identifier and the shared\n"
+"password by other communication means. Or even directly share the following link:"
+msgstr "Si vous préférez vous pouvez partager l'identifiant du projet et son mot "
+"de passe par un autre moyen de communication. Ou directement partager le lien "
+"suivant :"
#: templates/settle_bills.html:31
msgid "Who pays?"
diff --git a/ihatemoney/web.py b/ihatemoney/web.py
index efd843c..c1b1093 100644
--- a/ihatemoney/web.py
+++ b/ihatemoney/web.py
@@ -151,12 +151,20 @@ def admin():
def authenticate(project_id=None):
"""Authentication form"""
form = AuthenticationForm()
- if not form.id.data and request.args.get('project_id'):
- form.id.data = request.args['project_id']
- project_id = form.id.data
+ # Try to get project_id from token first
+ token = request.args.get('token')
+ if token:
+ project_id = Project.verify_token(token, token_type='non_timed_token')
+ token_auth = True
+ else:
+ if not form.id.data and request.args.get('project_id'):
+ form.id.data = request.args['project_id']
+ project_id = form.id.data
+ token_auth = False
if project_id is None:
- # User doesn't provide project identifier, return to authenticate form
- msg = _("You need to enter a project identifier")
+ # User doesn't provide project identifier or a valid token
+ # return to authenticate form
+ msg = _("You either provided a bad token or no project identifier.")
form.errors["id"] = [msg]
return render_template("authenticate.html", form=form)
@@ -171,11 +179,10 @@ def authenticate(project_id=None):
setattr(g, 'project', project)
return redirect(url_for(".list_bills"))
- if request.method == "POST" and form.validate():
- if not form.password.data == project.password:
- msg = _("This private code is not the right one")
- form.errors['password'] = [msg]
- return render_template("authenticate.html", form=form)
+ # else do form authentication or token authentication
+ is_post_auth = request.method == "POST" and form.validate()
+ is_valid_password = form.password.data == project.password
+ if is_post_auth and is_valid_password or token_auth:
# maintain a list of visited projects
if "projects" not in session:
session["projects"] = []
@@ -185,6 +192,9 @@ def authenticate(project_id=None):
session.update()
setattr(g, 'project', project)
return redirect(url_for(".list_bills"))
+ if is_post_auth and not is_valid_password:
+ msg = _("This private code is not the right one")
+ form.errors['password'] = [msg]
return render_template("authenticate.html", form=form)
@@ -250,7 +260,7 @@ def create_project():
# redirect the user to the next step (invite)
flash(_("%(msg_compl)sThe project identifier is %(project)s",
msg_compl=msg_compl, project=project.id))
- return redirect(url_for(".invite", project_id=project.id))
+ return redirect(url_for(".list_bills", project_id=project.id))
return render_template("create_project.html", form=form)