Rework project deletion to add CSRF validation

It requires reworking the user interface, but it's probably for the best.
This commit is contained in:
Baptiste Jonglez 2021-07-14 10:34:48 +02:00 committed by zorun
parent 109d7fca17
commit 2bb6f2b6a7
5 changed files with 61 additions and 13 deletions

View file

@ -221,6 +221,26 @@ class ProjectForm(EditProjectForm):
raise ValidationError(Markup(message)) 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): class AuthenticationForm(FlaskForm):
id = StringField(_("Project identifier"), validators=[DataRequired()]) id = StringField(_("Project identifier"), validators=[DataRequired()])
password = PasswordField(_("Private code"), validators=[DataRequired()]) password = PasswordField(_("Private code"), validators=[DataRequired()])

View file

@ -303,11 +303,6 @@ footer .footer-left {
color: #fff; color: #fff;
} }
.confirm,
.confirm:hover {
color: red;
}
.bill-actions { .bill-actions {
padding-top: 10px; padding-top: 10px;
text-align: center; text-align: center;

View file

@ -1,9 +1,17 @@
{% extends "layout.html" %} {% extends "layout.html" %}
{% block js %} {% block js %}
$('#delete-project').click(function ()
{ $('#delete-project').click(function(){
$(this).html("<a style='color:red; ' href='{{ url_for('.delete_project') }}' >{{_("you sure?")}}</a>"); 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) { $('.custom-file-input').on('change', function(event) {
@ -21,6 +29,10 @@
{{ forms.edit_project(edit_form) }} {{ forms.edit_project(edit_form) }}
</form> </form>
<h2>{{ _("Delete project") }}</h2>
<form id="delete-project" class="form-horizontal" action="{{ url_for(".delete_project") }}" method="post" >
{{ forms.delete_project(delete_form) }}
</form>
<h2>{{ _("Import JSON") }}</h2> <h2>{{ _("Import JSON") }}</h2>
<form class="form-horizontal" method="post" enctype="multipart/form-data"> <form class="form-horizontal" method="post" enctype="multipart/form-data">

View file

@ -100,7 +100,18 @@
{{ input(form.default_currency) }} {{ input(form.default_currency) }}
<div class="actions"> <div class="actions">
<button class="btn btn-primary">{{ _("Edit the project") }}</button> <button class="btn btn-primary">{{ _("Edit the project") }}</button>
<a id="delete-project" style="color:red; margin-left:10px; cursor:pointer; ">{{ _("delete") }}</a> </div>
{% endmacro %}
{% macro delete_project(form) %}
{% include "display_errors.html" %}
<p><strong>{{ _("This will remove all bills and participants in this project!") }}</strong></p>
{{ form.hidden_tag() }}
{{ input(form.password) }}
<div class="actions">
<button class="btn btn-danger">{{ _("Delete project") }}</button>
</div> </div>
{% endmacro %} {% endmacro %}

View file

@ -39,6 +39,7 @@ from werkzeug.security import check_password_hash, generate_password_hash
from ihatemoney.forms import ( from ihatemoney.forms import (
AdminAuthenticationForm, AdminAuthenticationForm,
AuthenticationForm, AuthenticationForm,
DeleteProjectForm,
EditProjectForm, EditProjectForm,
EmptyForm, EmptyForm,
InviteForm, InviteForm,
@ -401,6 +402,7 @@ def reset_password():
@main.route("/<project_id>/edit", methods=["GET", "POST"]) @main.route("/<project_id>/edit", methods=["GET", "POST"])
def edit_project(): def edit_project():
edit_form = EditProjectForm(id=g.project.id) edit_form = EditProjectForm(id=g.project.id)
delete_form = DeleteProjectForm(id=g.project.id)
import_form = UploadForm() import_form = UploadForm()
# Import form # Import form
if import_form.validate_on_submit(): if import_form.validate_on_submit():
@ -434,6 +436,7 @@ def edit_project():
return render_template( return render_template(
"edit_project.html", "edit_project.html",
edit_form=edit_form, edit_form=edit_form,
delete_form=delete_form,
import_form=import_form, import_form=import_form,
current_view="edit_project", current_view="edit_project",
) )
@ -512,11 +515,18 @@ def import_project(file, project):
db.session.commit() db.session.commit()
@main.route("/<project_id>/delete") @main.route("/<project_id>/delete", methods=["POST"])
def delete_project(): def delete_project():
form = DeleteProjectForm(id=g.project.id)
if form.validate():
g.project.remove_project() g.project.remove_project()
flash(_("Project successfully deleted")) 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")) return redirect(request.headers.get("Referer") or url_for(".home"))