diff --git a/ihatemoney/forms.py b/ihatemoney/forms.py
index f1e852e7..315a2ab7 100644
--- a/ihatemoney/forms.py
+++ b/ihatemoney/forms.py
@@ -443,6 +443,10 @@ class InviteForm(FlaskForm):
)
+class LogoutForm(FlaskForm):
+ submit = SubmitField(_("Logout"))
+
+
class EmptyForm(FlaskForm):
"""Used for CSRF validation"""
diff --git a/ihatemoney/templates/layout.html b/ihatemoney/templates/layout.html
index f6c8f4a2..6767ee8c 100644
--- a/ihatemoney/templates/layout.html
+++ b/ihatemoney/templates/layout.html
@@ -119,9 +119,10 @@
{{ _("Dashboard") }}
{% endif %}
-
- {{ _("Logout") }}
-
+
diff --git a/ihatemoney/tests/budget_test.py b/ihatemoney/tests/budget_test.py
index d94c6187..b4fab7c4 100644
--- a/ihatemoney/tests/budget_test.py
+++ b/ihatemoney/tests/budget_test.py
@@ -79,7 +79,7 @@ class BudgetTestCase(IhatemoneyTestCase):
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")
+ self.client.post("/exit")
# Test that we got a valid token
resp = self.client.get(url, follow_redirects=True)
self.assertIn(
@@ -87,7 +87,7 @@ class BudgetTestCase(IhatemoneyTestCase):
resp.data.decode("utf-8"),
)
# Test empty and invalid tokens
- self.client.get("/exit")
+ self.client.post("/exit")
# Use another project_id
parsed_url = urlparse(url)
resp = self.client.get(
@@ -111,7 +111,7 @@ class BudgetTestCase(IhatemoneyTestCase):
response = self.client.get("/raclette/invite").data.decode("utf-8")
link = extract_link(response, "share the following link")
- self.client.get("/exit")
+ self.client.post("/exit")
response = self.client.get(link)
# Link is valid
assert response.status_code == 302
@@ -131,7 +131,7 @@ class BudgetTestCase(IhatemoneyTestCase):
assert response.status_code == 200
assert "alert-danger" not in response.data.decode("utf-8")
- self.client.get("/exit")
+ self.client.post("/exit")
response = self.client.get(link, follow_redirects=True)
# Link is invalid
self.assertIn("Provided token is invalid", response.data.decode("utf-8"))
@@ -498,8 +498,12 @@ class BudgetTestCase(IhatemoneyTestCase):
self.assertIn("raclette", session)
self.assertTrue(session["raclette"])
+ # logout should work with POST only
+ resp = c.get("/exit")
+ self.assertStatus(405, resp)
+
# logout should wipe the session out
- c.get("/exit")
+ c.post("/exit")
self.assertNotIn("raclette", session)
# test that with admin credentials, one can access every project
@@ -1225,7 +1229,7 @@ class BudgetTestCase(IhatemoneyTestCase):
self.assertEqual(raclette.get_bills().count(), 1)
# Log out
- self.client.get("/exit")
+ self.client.post("/exit")
# Create and log in as another project
self.post_project("tartiflette")
@@ -1263,7 +1267,7 @@ class BudgetTestCase(IhatemoneyTestCase):
# Use the correct credentials to modify and delete the bill.
# This ensures that modifying and deleting the bill can actually work
- self.client.get("/exit")
+ self.client.post("/exit")
self.client.post(
"/authenticate", data={"id": "raclette", "password": "raclette"}
)
@@ -1276,7 +1280,7 @@ class BudgetTestCase(IhatemoneyTestCase):
self.assertEqual(bill, None)
# Switch back to the second project
- self.client.get("/exit")
+ self.client.post("/exit")
self.client.post(
"/authenticate", data={"id": "tartiflette", "password": "tartiflette"}
)
@@ -1311,7 +1315,7 @@ class BudgetTestCase(IhatemoneyTestCase):
# Use the correct credentials to modify and delete the member.
# This ensures that modifying and deleting the member can actually work
- self.client.get("/exit")
+ self.client.post("/exit")
self.client.post(
"/authenticate", data={"id": "raclette", "password": "raclette"}
)
diff --git a/ihatemoney/web.py b/ihatemoney/web.py
index 4ecca084..46d7146c 100644
--- a/ihatemoney/web.py
+++ b/ihatemoney/web.py
@@ -45,6 +45,7 @@ from ihatemoney.forms import (
EmptyForm,
ImportProjectForm,
InviteForm,
+ LogoutForm,
MemberForm,
PasswordReminder,
ProjectForm,
@@ -122,6 +123,7 @@ def set_show_admin_dashboard_link(endpoint, values):
current_app.config["ACTIVATE_ADMIN_DASHBOARD"]
and current_app.config["ADMIN_PASSWORD"]
)
+ g.logout_form = LogoutForm()
@main.url_value_preprocessor
@@ -534,11 +536,23 @@ def export_project(file, format):
)
-@main.route("/exit")
+@main.route("/exit", methods=["GET", "POST"])
def exit():
- # delete the session
- session.clear()
- return redirect(url_for(".home"))
+ # We must test it manually, because otherwise, it creates a project "exit"
+ if request.method == "GET":
+ abort(405)
+
+ form = LogoutForm()
+ if form.validate():
+ # delete the session
+ session.clear()
+ return redirect(url_for(".home"))
+ else:
+ flash(
+ format_form_errors(form, _("Unable to logout")),
+ category="danger",
+ )
+ return redirect(request.headers.get("Referer") or url_for(".home"))
@main.route("/demo")