Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
24 changes: 23 additions & 1 deletion review/routes.py
Original file line number Diff line number Diff line change
Expand Up @@ -1894,9 +1894,16 @@ def accountdeletionrequests(current_user):
@reviewer_bp.route('/approveaccountdeletion', methods=['POST'])
@admin_required
def approveaccountdeletion(current_user):
"""Approve an account deletion request and delete the account (admin only)."""
"""Preview and approve an account deletion request (admin only).

Approval is a two-step flow that mirrors the manual delete-user tool: the
first POST resolves the target account and shows a full preview of what
deletion would remove; a second POST (``action=confirm_delete``) performs
the irreversible deletion.
"""
validate_csrf_token()
req_hexid = request.form.get('uuid')
action = request.form.get('action', 'preview')

try:
del_request = account_deletion_request_by_hexid(req_hexid)
Expand Down Expand Up @@ -1924,6 +1931,21 @@ def approveaccountdeletion(current_user):
message="Request has no email on file; cannot delete"
))

if action != 'confirm_delete':
# Step 1: show the same deletion impact preview the manual delete-user
# tool shows, so the admin sees exactly what approval will remove.
preview_data, error = preview_user_deletion(delete_email)
if error:
return redirect(url_for('reviewer.accountdeletionrequests', message=error))
return render_template(
'reviewer/approveaccountdeletion.html',
user=current_user['username'],
is_admin=current_user['is_admin'],
req=del_request,
preview=preview_data,
)

# Step 2: admin confirmed the preview -- perform the irreversible deletion.
result = delete_user_account(delete_email, admin_username=current_user['username'])
success = "successful" in result.lower()

Expand Down
105 changes: 105 additions & 0 deletions review/templates/reviewer/_deletion_preview.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,105 @@
{# Shared account-deletion impact preview.

Renders the breakdown produced by ``preview_user_deletion`` so both the
manual delete-user tool and the deletion-request approval flow show the
exact same "what will be removed" summary before a reviewer confirms.
Expects a ``preview`` dict in context. #}
<style>
.preview-section {
padding: 20px;
margin: 20px 0;
}
.preview-header {
color: #ff9800;
font-weight: bold;
font-size: 1.2em;
margin-bottom: 15px;
}
.preview-item {
margin: 8px 0;
padding-left: 20px;
}
.crackme-detail {
background-color: #3c3c3c;
border-left: 3px solid #ff9800;
padding: 10px;
margin: 10px 0;
}
.crackme-name {
font-weight: bold;
color: #fff;
}
.cascade-info {
font-size: 0.9em;
color: #aaa;
margin-left: 15px;
}
.final-confirm-section {
border: 3px solid #e85600;
border-radius: 4px;
padding: 20px;
margin: 20px 0;
}
</style>

<div class="preview-section">
<div class="preview-header">DELETION PREVIEW for {{ preview.username }} ({{ preview.email }})</div>

<p><strong>The following items will be permanently deleted:</strong></p>

<div class="preview-item">
<strong>1 User Account:</strong> {{ preview.username }} ({{ preview.email }})
</div>

<div class="preview-item">
<strong>{{ preview.notifications }} Notifications</strong>
</div>

<div class="preview-item">
<strong>{{ preview.solutions }} Solutions</strong> posted by user
</div>

<div class="preview-item">
<strong>{{ preview.user_comments }} Comments</strong> by user on other crackmes
</div>

<div class="preview-item">
<strong>{{ preview.difficulty_ratings }} Difficulty Ratings</strong> by user
</div>

<div class="preview-item">
<strong>{{ preview.quality_ratings }} Quality Ratings</strong> by user
</div>

<div class="preview-item">
<strong>{{ preview.crackmes }} Crackmes</strong> by user (with CASCADE deletion):
</div>

{% if preview.crackme_details %}
{% for crackme in preview.crackme_details %}
<div class="crackme-detail">
<div class="crackme-name">{{ crackme.name }} ({{ crackme.hexid }})</div>
<div class="cascade-info">
-> {{ crackme.solutions }} solutions will be cascade deleted<br>
-> {{ crackme.comments }} comments will be cascade deleted<br>
-> {{ crackme.difficulty_ratings }} difficulty ratings will be cascade deleted<br>
-> {{ crackme.quality_ratings }} quality ratings will be cascade deleted
</div>
</div>
{% endfor %}
{% endif %}

<hr>

<div style="margin-top: 20px; font-size: 1.1em;">
<strong>TOTAL ITEMS TO BE DELETED:</strong>
<ul>
<li>1 user account</li>
<li>{{ preview.notifications }} notifications</li>
<li>{{ preview.solutions + preview.total_solutions_on_user_crackmes }} solutions ({{ preview.solutions }} by user + {{ preview.total_solutions_on_user_crackmes }} on user's crackmes)</li>
<li>{{ preview.total_comments }} comments ({{ preview.user_comments }} by user + {{ preview.total_comments - preview.user_comments }} on user's crackmes)</li>
<li>{{ preview.crackmes }} crackmes</li>
<li>Difficulty/quality ratings will be recalculated for affected crackmes</li>
</ul>
</div>
</div>
5 changes: 2 additions & 3 deletions review/templates/reviewer/accountdeletionrequests.html
Original file line number Diff line number Diff line change
Expand Up @@ -54,11 +54,10 @@ <h3>Pending account deletion requests</h3>
{% endif %}

{% if is_admin %}
<form action="{{ url_for('reviewer.approveaccountdeletion') }}" method="POST" style="display:inline;"
onsubmit="return confirm('Permanently delete account &quot;{{ req.requester }}&quot; and all associated data? This cannot be undone.');">
<form action="{{ url_for('reviewer.approveaccountdeletion') }}" method="POST" style="display:inline;">
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}">
<input type="hidden" name="uuid" value="{{ req.hexid }}">
<button type="submit" class="btn btn-error">Approve &amp; delete account</button>
<button type="submit" class="btn btn-error">Review &amp; approve deletion</button>
</form>
{% endif %}

Expand Down
83 changes: 83 additions & 0 deletions review/templates/reviewer/approveaccountdeletion.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
<!DOCTYPE html>
<html>

<head>
<title>Approve account deletion</title>
<link rel="stylesheet" href="{{ url_for('static', filename='css/spectre.min.css') }}">
<link rel="stylesheet" href="{{ url_for('static', filename='css/spectre-exp.min.css') }}">
<link rel="stylesheet" href="{{ url_for('static', filename='css/spectre-icons.min.css') }}">
<link rel="stylesheet" href="{{ url_for('static', filename='css/custom.css') }}">
</head>

<body>
<header class="navbar hide-xs">
<section class="navbar-section">
<h2><a href="{{ url_for('reviewer.dashboard') }}" class="title-navbar">Dashboard</a></h2>
</section>
<section class="navbar-center">
<h2>Welcome, {{ user }}!{% if is_admin %} (Admin){% endif %}</h2>
</section>
<section class="navbar-section">
<a href="/" class="btn btn-link">crackmes.one</a>
<a href="{{ url_for('reviewer.logout') }}" class="btn btn-link">Logout</a>
</section>
</header>

<div class="container grid-lg wrapper">
<div class="panel panel-danger" style="border: 2px solid #e85600; margin-bottom: 20px;">
<div class="panel-header">
<h3 style="color: #e85600;">Approve account deletion request</h3>
</div>
<div class="panel-body">
<p>
<strong>{{ req.requester }}</strong> requested deletion of their account on
{{ req.created_at }}.
{% if req.note %}<br><em>Reason:</em> {{ req.note }}{% endif %}
</p>
<p><strong>WARNING:</strong> Approving runs the same irreversible account
deletion as the manual delete-user tool. Review the impact below before
confirming.</p>
</div>
</div>

<!-- Same impact breakdown the manual delete-user tool shows -->
{% include 'reviewer/_deletion_preview.html' %}

<div class="final-confirm-section">
<h3 style="color: #e85600;">FINAL CONFIRMATION REQUIRED</h3>
<p><strong>This action is IRREVERSIBLE!</strong> Once you click the button below, all the data listed above will be permanently deleted and the requester will be emailed a confirmation.</p>

<form method="post" id="confirmApproveForm" action="{{ url_for('reviewer.approveaccountdeletion') }}">
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}">
<input type="hidden" name="action" value="confirm_delete">
<input type="hidden" name="uuid" value="{{ req.hexid }}">

<div style="margin-top: 20px;">
<a href="{{ url_for('reviewer.accountdeletionrequests') }}" class="btn btn-secondary">Cancel</a>
<input type="submit" class="btn btn-error float-right" value="APPROVE &amp; DELETE ACCOUNT" id="confirmApproveButton">
</div>
</form>
</div>

<script>
document.getElementById('confirmApproveForm').addEventListener('submit', function(e) {
e.preventDefault();

const finalConfirm = confirm("Are you ABSOLUTELY SURE you want to DELETE ALL DATA for user: {{ preview.username }} ({{ preview.email }})?\n\nThis is your LAST CHANCE to cancel!\n\nClick OK to proceed with deletion, or Cancel to abort.");

if (finalConfirm) {
const deleteConfirmation = prompt('Type "DELETE" (without quotes, all capitals) to confirm:');
if (deleteConfirmation === "DELETE") {
this.submit();
} else {
alert('Deletion cancelled. You must type "DELETE" exactly to confirm.');
}
} else {
alert('Deletion cancelled.');
}
});
</script>
</div>
</body>

</html>
99 changes: 1 addition & 98 deletions review/templates/reviewer/deleteuser.html
Original file line number Diff line number Diff line change
Expand Up @@ -7,43 +7,6 @@
<link rel="stylesheet" href="{{ url_for('static', filename='css/spectre-exp.min.css') }}">
<link rel="stylesheet" href="{{ url_for('static', filename='css/spectre-icons.min.css') }}">
<link rel="stylesheet" href="{{ url_for('static', filename='css/custom.css') }}">
<style>
.preview-section {
padding: 20px;
margin: 20px 0;
}
.preview-header {
color: #ff9800;
font-weight: bold;
font-size: 1.2em;
margin-bottom: 15px;
}
.preview-item {
margin: 8px 0;
padding-left: 20px;
}
.crackme-detail {
background-color: #3c3c3c;
border-left: 3px solid #ff9800;
padding: 10px;
margin: 10px 0;
}
.crackme-name {
font-weight: bold;
color: #fff;
}
.cascade-info {
font-size: 0.9em;
color: #aaa;
margin-left: 15px;
}
.final-confirm-section {
border: 3px solid #e85600;
border-radius: 4px;
padding: 20px;
margin: 20px 0;
}
</style>
</head>

<body>
Expand Down Expand Up @@ -120,67 +83,7 @@ <h3 style="color: #e85600;">DANGER: IRREVERSIBLE OPERATION</h3>
</script>
{% else %}
<!-- Step 2: Show preview and final confirmation -->
<div class="preview-section">
<div class="preview-header">DELETION PREVIEW for {{ preview.username }} ({{ preview.email }})</div>

<p><strong>The following items will be permanently deleted:</strong></p>

<div class="preview-item">
<strong>1 User Account:</strong> {{ preview.username }} ({{ preview.email }})
</div>

<div class="preview-item">
<strong>{{ preview.notifications }} Notifications</strong>
</div>

<div class="preview-item">
<strong>{{ preview.solutions }} Solutions</strong> posted by user
</div>

<div class="preview-item">
<strong>{{ preview.user_comments }} Comments</strong> by user on other crackmes
</div>

<div class="preview-item">
<strong>{{ preview.difficulty_ratings }} Difficulty Ratings</strong> by user
</div>

<div class="preview-item">
<strong>{{ preview.quality_ratings }} Quality Ratings</strong> by user
</div>

<div class="preview-item">
<strong>{{ preview.crackmes }} Crackmes</strong> by user (with CASCADE deletion):
</div>

{% if preview.crackme_details %}
{% for crackme in preview.crackme_details %}
<div class="crackme-detail">
<div class="crackme-name">{{ crackme.name }} ({{ crackme.hexid }})</div>
<div class="cascade-info">
-> {{ crackme.solutions }} solutions will be cascade deleted<br>
-> {{ crackme.comments }} comments will be cascade deleted<br>
-> {{ crackme.difficulty_ratings }} difficulty ratings will be cascade deleted<br>
-> {{ crackme.quality_ratings }} quality ratings will be cascade deleted
</div>
</div>
{% endfor %}
{% endif %}

<hr>

<div style="margin-top: 20px; font-size: 1.1em;">
<strong>TOTAL ITEMS TO BE DELETED:</strong>
<ul>
<li>1 user account</li>
<li>{{ preview.notifications }} notifications</li>
<li>{{ preview.solutions + preview.total_solutions_on_user_crackmes }} solutions ({{ preview.solutions }} by user + {{ preview.total_solutions_on_user_crackmes }} on user's crackmes)</li>
<li>{{ preview.total_comments }} comments ({{ preview.user_comments }} by user + {{ preview.total_comments - preview.user_comments }} on user's crackmes)</li>
<li>{{ preview.crackmes }} crackmes</li>
<li>Difficulty/quality ratings will be recalculated for affected crackmes</li>
</ul>
</div>
</div>
{% include 'reviewer/_deletion_preview.html' %}

<div class="final-confirm-section">
<h3 style="color: #e85600;">FINAL CONFIRMATION REQUIRED</h3>
Expand Down
14 changes: 14 additions & 0 deletions tests/test_account_deletion.py
Original file line number Diff line number Diff line change
Expand Up @@ -92,8 +92,22 @@ def test_admin_approves_deletion_and_emails_user(app, db, alice, monkeypatch):
monkeypatch.setattr(routes, 'log_reviewer_operation', lambda *a, **k: None)

client = _admin_client(app)

# Step 1: the first POST shows a deletion impact preview without deleting.
preview = client.post('/review/approveaccountdeletion', data={
'uuid': req['hexid'], 'csrf_token': 'admin-csrf',
})

assert preview.status_code == 200
assert b'DELETION PREVIEW' in preview.data
assert db.user.find_one({'name': 'alice'}) is not None
assert db.account_deletion_request.find_one(
{'hexid': req['hexid']})['status'] == 'pending'

# Step 2: confirming the preview performs the irreversible deletion.
response = client.post('/review/approveaccountdeletion', data={
'uuid': req['hexid'], 'csrf_token': 'admin-csrf',
'action': 'confirm_delete',
})

assert response.status_code == 302
Expand Down
Loading