diff options
| -rw-r--r-- | CHANGELOG.rst | 1 | ||||
| -rw-r--r-- | ihatemoney/models.py | 24 | ||||
| -rw-r--r-- | ihatemoney/templates/authenticate.html | 5 | ||||
| -rw-r--r-- | ihatemoney/templates/invitation_mail.en | 4 | ||||
| -rw-r--r-- | ihatemoney/templates/invitation_mail.fr | 4 | ||||
| -rw-r--r-- | ihatemoney/templates/list_bills.html | 2 | ||||
| -rw-r--r-- | ihatemoney/templates/reminder_mail.en | 4 | ||||
| -rw-r--r-- | ihatemoney/templates/reminder_mail.fr | 2 | ||||
| -rw-r--r-- | ihatemoney/templates/send_invites.html | 14 | ||||
| -rw-r--r-- | ihatemoney/tests/tests.py | 23 | ||||
| -rw-r--r-- | ihatemoney/translations/fr/LC_MESSAGES/messages.mo | bin | 9559 -> 9762 bytes | |||
| -rw-r--r-- | ihatemoney/translations/fr/LC_MESSAGES/messages.po | 40 | ||||
| -rw-r--r-- | ihatemoney/web.py | 32 |
13 files changed, 98 insertions, 57 deletions
diff --git a/CHANGELOG.rst b/CHANGELOG.rst index 32e4adf..b91de94 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -20,6 +20,7 @@ Changed - Simpler and safer authentication logic (#270) - Use token based auth to reset passwords (#269) - Better install doc (#275) +- Use token based auth in invitation e-mails (#280) Added ===== 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 Binary files differindex 249c996..47b801d 100644 --- a/ihatemoney/translations/fr/LC_MESSAGES/messages.mo +++ b/ihatemoney/translations/fr/LC_MESSAGES/messages.mo 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) |
