From 2bb6f2b6a7e56a781c5041a9cccae5a8fb0508fc Mon Sep 17 00:00:00 2001 From: Baptiste Jonglez Date: Wed, 14 Jul 2021 10:34:48 +0200 Subject: [PATCH] Rework project deletion to add CSRF validation It requires reworking the user interface, but it's probably for the best. --- ihatemoney/forms.py | 20 ++++++++++++++++++++ ihatemoney/static/css/main.css | 5 ----- ihatemoney/templates/edit_project.html | 18 +++++++++++++++--- ihatemoney/templates/forms.html | 13 ++++++++++++- ihatemoney/web.py | 18 ++++++++++++++---- 5 files changed, 61 insertions(+), 13 deletions(-) diff --git a/ihatemoney/forms.py b/ihatemoney/forms.py index 356450f7b..de2d4f1bd 100644 --- a/ihatemoney/forms.py +++ b/ihatemoney/forms.py @@ -221,6 +221,26 @@ def validate_id(form, field): raise ValidationError(Markup(message)) +class DeleteProjectForm(FlaskForm): + password = PasswordField( + _("Private code"), + description=_("Enter private code to confirm deletion"), + validators=[DataRequired()], + ) + + def __init__(self, *args, **kwargs): + # Same trick as EditProjectForm: we need to know the project ID + self.id = SimpleNamespace(data=kwargs.pop("id", "")) + super().__init__(*args, **kwargs) + + def validate_password(form, field): + project = Project.query.get(form.id.data) + if project is None: + raise ValidationError(_("Unknown error")) + if not check_password_hash(project.password, form.password.data): + raise ValidationError(_("Invalid private code.")) + + class AuthenticationForm(FlaskForm): id = StringField(_("Project identifier"), validators=[DataRequired()]) password = PasswordField(_("Private code"), validators=[DataRequired()]) diff --git a/ihatemoney/static/css/main.css b/ihatemoney/static/css/main.css index 3a6630bf7..ef459c428 100644 --- a/ihatemoney/static/css/main.css +++ b/ihatemoney/static/css/main.css @@ -303,11 +303,6 @@ footer .footer-left { color: #fff; } -.confirm, -.confirm:hover { - color: red; -} - .bill-actions { padding-top: 10px; text-align: center; diff --git a/ihatemoney/templates/edit_project.html b/ihatemoney/templates/edit_project.html index 478b7b422..f2fffb20a 100644 --- a/ihatemoney/templates/edit_project.html +++ b/ihatemoney/templates/edit_project.html @@ -1,9 +1,17 @@ {% extends "layout.html" %} {% block js %} - $('#delete-project').click(function () - { - $(this).html("{{_("you sure?")}}"); + + $('#delete-project').click(function(){ + var link = $(this).find('button'); + link.click(function(){ + if ($(this).hasClass("confirm")){ + return true; + } + $(this).html("{{_("Are you sure?")}}"); + $(this).addClass("confirm"); + return false; + }); }); $('.custom-file-input').on('change', function(event) { @@ -21,6 +29,10 @@

{{ _("Edit project") }}

{{ forms.edit_project(edit_form) }} +

{{ _("Delete project") }}

+
+ {{ forms.delete_project(delete_form) }} +

{{ _("Import JSON") }}

diff --git a/ihatemoney/templates/forms.html b/ihatemoney/templates/forms.html index c07f443bc..7c2fdeb13 100644 --- a/ihatemoney/templates/forms.html +++ b/ihatemoney/templates/forms.html @@ -100,7 +100,18 @@ {{ input(form.default_currency) }}
- {{ _("delete") }} +
+ +{% endmacro %} + +{% macro delete_project(form) %} + + {% include "display_errors.html" %} +

{{ _("This will remove all bills and participants in this project!") }}

+ {{ form.hidden_tag() }} + {{ input(form.password) }} +
+
{% endmacro %} diff --git a/ihatemoney/web.py b/ihatemoney/web.py index 667da61ee..98d6b3990 100644 --- a/ihatemoney/web.py +++ b/ihatemoney/web.py @@ -39,6 +39,7 @@ from ihatemoney.forms import ( AdminAuthenticationForm, AuthenticationForm, + DeleteProjectForm, EditProjectForm, EmptyForm, InviteForm, @@ -401,6 +402,7 @@ def reset_password(): @main.route("//edit", methods=["GET", "POST"]) def edit_project(): edit_form = EditProjectForm(id=g.project.id) + delete_form = DeleteProjectForm(id=g.project.id) import_form = UploadForm() # Import form if import_form.validate_on_submit(): @@ -434,6 +436,7 @@ def edit_project(): return render_template( "edit_project.html", edit_form=edit_form, + delete_form=delete_form, import_form=import_form, current_view="edit_project", ) @@ -512,11 +515,18 @@ def import_project(file, project): db.session.commit() -@main.route("//delete") +@main.route("//delete", methods=["POST"]) def delete_project(): - g.project.remove_project() - flash(_("Project successfully deleted")) - + form = DeleteProjectForm(id=g.project.id) + if form.validate(): + g.project.remove_project() + flash(_("Project successfully deleted")) + return redirect(url_for(".home")) + else: + flash( + _("Error deleting project: wrong private code or wrong CSRF token"), + category="danger", + ) return redirect(request.headers.get("Referer") or url_for(".home"))