History: also ask for private code to confirm deletion

This is the same idea as deleting a project: deleting history is also a
major destructive action.  We reuse the same form as for project deletion
to ask for the private code and provide CSRF validation.
This commit is contained in:
Baptiste Jonglez 2021-07-14 16:07:52 +02:00 committed by zorun
parent 969029a811
commit db982572aa
4 changed files with 54 additions and 16 deletions

View file

@ -221,7 +221,17 @@ class ProjectForm(EditProjectForm):
raise ValidationError(Markup(message)) raise ValidationError(Markup(message))
class DeleteProjectForm(FlaskForm): class DestructiveActionProjectForm(FlaskForm):
"""Used for any important "delete" action linked to a project:
- delete project itself
- delete history
- delete IP addresses in history
- possibly others in the future
It asks the user to enter the private code to confirm deletion.
"""
password = PasswordField( password = PasswordField(
_("Private code"), _("Private code"),
description=_("Enter private code to confirm deletion"), description=_("Enter private code to confirm deletion"),

View file

@ -125,6 +125,28 @@
</div> </div>
{% endmacro %} {% endmacro %}
{% macro delete_project_history(form) %}
{% include "display_errors.html" %}
{{ form.hidden_tag() }}
{{ input(form.password, inline=True) }}
<div class="actions">
<button class="btn btn-danger">{{ _("Confirm deletion") }}</button>
</div>
{% endmacro %}
{% macro delete_ip_addresses(form) %}
{% include "display_errors.html" %}
{{ form.hidden_tag() }}
{{ input(form.password) }}
<div class="actions">
<button class="btn btn-danger">{{ _("Confirm deletion") }}</button>
</div>
{% endmacro %}
{% macro add_bill(form, edit=False, title=True) %} {% macro add_bill(form, edit=False, title=True) %}
<fieldset> <fieldset>

View file

@ -55,8 +55,7 @@
<div class="modal-footer"> <div class="modal-footer">
<button type="button" class="btn btn-secondary" data-dismiss="modal">{{ _("Close") }}</button> <button type="button" class="btn btn-secondary" data-dismiss="modal">{{ _("Close") }}</button>
<form action="{{ url_for(".strip_ip_addresses") }}" method="post"> <form action="{{ url_for(".strip_ip_addresses") }}" method="post">
{{ csrf_form.csrf_token }} {{ forms.delete_ip_addresses(delete_form) }}
<input type="submit" class="btn btn-danger" value="{{ _("Confirm Delete") }}" name="{{ _("Confirm Delete") }}"/>
</form> </form>
</div> </div>
</div> </div>
@ -76,8 +75,7 @@
<div class="modal-footer"> <div class="modal-footer">
<button type="button" class="btn btn-secondary" data-dismiss="modal">{{ _("Close") }}</button> <button type="button" class="btn btn-secondary" data-dismiss="modal">{{ _("Close") }}</button>
<form action="{{ url_for(".erase_history") }}" method="post"> <form action="{{ url_for(".erase_history") }}" method="post">
{{ csrf_form.csrf_token }} {{ forms.delete_project_history(delete_form) }}
<input type="submit" class="btn btn-danger" value="{{ _("Confirm Delete") }}" name="{{ _("Confirm Delete") }}"/>
</form> </form>
</div> </div>
</div> </div>

View file

@ -39,7 +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, DestructiveActionProjectForm,
EditProjectForm, EditProjectForm,
EmptyForm, EmptyForm,
InviteForm, InviteForm,
@ -402,7 +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) delete_form = DestructiveActionProjectForm(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():
@ -517,7 +517,7 @@ def import_project(file, project):
@main.route("/<project_id>/delete", methods=["POST"]) @main.route("/<project_id>/delete", methods=["POST"])
def delete_project(): def delete_project():
form = DeleteProjectForm(id=g.project.id) form = DestructiveActionProjectForm(id=g.project.id)
if form.validate(): if form.validate():
g.project.remove_project() g.project.remove_project()
flash(_("Project successfully deleted")) flash(_("Project successfully deleted"))
@ -799,11 +799,11 @@ def settle_bill():
@main.route("/<project_id>/history") @main.route("/<project_id>/history")
def history(): def history():
"""Query for the version entries associated with this project.""" """Query for the version entries associated with this project."""
csrf_form = EmptyForm()
history = get_history(g.project, human_readable_names=True) history = get_history(g.project, human_readable_names=True)
any_ip_addresses = any(event["ip"] for event in history) any_ip_addresses = any(event["ip"] for event in history)
delete_form = DestructiveActionProjectForm()
return render_template( return render_template(
"history.html", "history.html",
current_view="history", current_view="history",
@ -812,33 +812,40 @@ def history():
LoggingMode=LoggingMode, LoggingMode=LoggingMode,
OperationType=Operation, OperationType=Operation,
current_log_pref=g.project.logging_preference, current_log_pref=g.project.logging_preference,
csrf_form=csrf_form, delete_form=delete_form,
) )
@main.route("/<project_id>/erase_history", methods=["POST"]) @main.route("/<project_id>/erase_history", methods=["POST"])
def erase_history(): def erase_history():
"""Erase all history entries associated with this project.""" """Erase all history entries associated with this project."""
# Used for CSRF validation form = DestructiveActionProjectForm(id=g.project.id)
form = EmptyForm()
if not form.validate(): if not form.validate():
flash(_("CSRF Token: The CSRF token is invalid."), category="danger") flash(
_("Error deleting project history: wrong private code or wrong CSRF token"),
category="danger",
)
return redirect(url_for(".history")) return redirect(url_for(".history"))
for query in get_history_queries(g.project): for query in get_history_queries(g.project):
query.delete(synchronize_session="fetch") query.delete(synchronize_session="fetch")
db.session.commit() db.session.commit()
flash(_("Deleted project history."))
return redirect(url_for(".history")) return redirect(url_for(".history"))
@main.route("/<project_id>/strip_ip_addresses", methods=["POST"]) @main.route("/<project_id>/strip_ip_addresses", methods=["POST"])
def strip_ip_addresses(): def strip_ip_addresses():
"""Strip ip addresses from history entries associated with this project.""" """Strip ip addresses from history entries associated with this project."""
# Used for CSRF validation form = DestructiveActionProjectForm(id=g.project.id)
form = EmptyForm()
if not form.validate(): if not form.validate():
flash(_("CSRF Token: The CSRF token is invalid."), category="danger") flash(
_(
"Error deleting recorded IP addresses: wrong private code or wrong CSRF token"
),
category="danger",
)
return redirect(url_for(".history")) return redirect(url_for(".history"))
for query in get_history_queries(g.project): for query in get_history_queries(g.project):
@ -846,6 +853,7 @@ def strip_ip_addresses():
version_object.transaction.remote_addr = None version_object.transaction.remote_addr = None
db.session.commit() db.session.commit()
flash(_("Deleted recorded IP addresses in project history."))
return redirect(url_for(".history")) return redirect(url_for(".history"))