Implement tamper-evident audit logging and invoice correction features
- Introduced a new audit logging system that creates a tamper-evident chain of events in MongoDB. - Added functions for appending audit events, ensuring index integrity, and verifying the audit chain. - Implemented soft-delete functionality for inventory and borrowing records to comply with GoBD regulations. - Added UI components for displaying invoice corrections and audit dashboard, including mismatch reporting. - Created a CLI utility for verifying the integrity of the audit chain. - Enhanced invoice management by preventing overwrites and allowing correction entries with reasons and optional deltas.
This commit is contained in:
@@ -0,0 +1,89 @@
|
||||
{% extends 'base.html' %}
|
||||
{% block title %}Audit Dashboard - {{ APP_VERSION }}{% endblock %}
|
||||
{% block content %}
|
||||
<div class="container" style="max-width:1400px; margin:0 auto; padding:18px;">
|
||||
<h1>Audit Dashboard</h1>
|
||||
<p>Integritätsstatus der Audit-Chain und letzte Audit-Ereignisse (max. 200 Einträge).</p>
|
||||
|
||||
<div style="display:grid; grid-template-columns:repeat(auto-fit,minmax(220px,1fr)); gap:12px; margin:16px 0 20px;">
|
||||
<div style="padding:14px; border:1px solid #e2e8f0; border-radius:10px; background:#fff;">
|
||||
<div style="font-size:0.85rem; color:#64748b;">Chain Status</div>
|
||||
{% if verify_result.ok %}
|
||||
<div style="font-size:1.35rem; font-weight:700; color:#166534;">OK</div>
|
||||
{% else %}
|
||||
<div style="font-size:1.35rem; font-weight:700; color:#991b1b;">FEHLER</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
<div style="padding:14px; border:1px solid #e2e8f0; border-radius:10px; background:#fff;">
|
||||
<div style="font-size:0.85rem; color:#64748b;">Einträge</div>
|
||||
<div style="font-size:1.35rem; font-weight:700;">{{ verify_result.count }}</div>
|
||||
</div>
|
||||
<div style="padding:14px; border:1px solid #e2e8f0; border-radius:10px; background:#fff;">
|
||||
<div style="font-size:0.85rem; color:#64748b;">Letzter Index</div>
|
||||
<div style="font-size:1.35rem; font-weight:700;">{{ verify_result.last_chain_index }}</div>
|
||||
</div>
|
||||
<div style="padding:14px; border:1px solid #e2e8f0; border-radius:10px; background:#fff;">
|
||||
<div style="font-size:0.85rem; color:#64748b;">Mismatch Count</div>
|
||||
<div style="font-size:1.35rem; font-weight:700;">{{ verify_result.mismatches|length }}</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{% if verify_result.mismatches %}
|
||||
<div style="margin-bottom:20px; border:1px solid #fecaca; background:#fff7f7; border-radius:10px; padding:12px;">
|
||||
<h3 style="margin-top:0; color:#991b1b;">Integritätsabweichungen</h3>
|
||||
<div style="max-height:280px; overflow:auto;">
|
||||
<table class="table" style="width:100%; border-collapse:collapse;">
|
||||
<thead>
|
||||
<tr>
|
||||
<th style="text-align:left; padding:8px; border-bottom:1px solid #ddd;">Index</th>
|
||||
<th style="text-align:left; padding:8px; border-bottom:1px solid #ddd;">Typ</th>
|
||||
<th style="text-align:left; padding:8px; border-bottom:1px solid #ddd;">Expected</th>
|
||||
<th style="text-align:left; padding:8px; border-bottom:1px solid #ddd;">Found</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for m in verify_result.mismatches %}
|
||||
<tr>
|
||||
<td style="padding:8px; border-bottom:1px solid #eee;">{{ m.chain_index }}</td>
|
||||
<td style="padding:8px; border-bottom:1px solid #eee;">{{ m.error }}</td>
|
||||
<td style="padding:8px; border-bottom:1px solid #eee; font-family:monospace;">{{ m.expected }}</td>
|
||||
<td style="padding:8px; border-bottom:1px solid #eee; font-family:monospace;">{{ m.found }}</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
<div style="border:1px solid #e2e8f0; border-radius:10px; background:#fff; padding:12px;">
|
||||
<h3 style="margin-top:0;">Letzte Audit-Ereignisse</h3>
|
||||
<div style="max-height:560px; overflow:auto;">
|
||||
<table class="table" style="width:100%; border-collapse:collapse;">
|
||||
<thead>
|
||||
<tr>
|
||||
<th style="text-align:left; padding:8px; border-bottom:1px solid #ddd;">Index</th>
|
||||
<th style="text-align:left; padding:8px; border-bottom:1px solid #ddd;">Zeit</th>
|
||||
<th style="text-align:left; padding:8px; border-bottom:1px solid #ddd;">Event</th>
|
||||
<th style="text-align:left; padding:8px; border-bottom:1px solid #ddd;">Actor</th>
|
||||
<th style="text-align:left; padding:8px; border-bottom:1px solid #ddd;">Hash</th>
|
||||
<th style="text-align:left; padding:8px; border-bottom:1px solid #ddd;">Payload</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for row in audit_rows %}
|
||||
<tr>
|
||||
<td style="padding:8px; border-bottom:1px solid #eee;">{{ row.chain_index }}</td>
|
||||
<td style="padding:8px; border-bottom:1px solid #eee;">{{ row.timestamp or row.created_at }}</td>
|
||||
<td style="padding:8px; border-bottom:1px solid #eee;">{{ row.event_type }}</td>
|
||||
<td style="padding:8px; border-bottom:1px solid #eee;">{{ row.actor }}</td>
|
||||
<td style="padding:8px; border-bottom:1px solid #eee; font-family:monospace; font-size:0.8rem;">{{ row.entry_hash }}</td>
|
||||
<td style="padding:8px; border-bottom:1px solid #eee;"><pre style="margin:0; white-space:pre-wrap; font-size:0.8rem;">{{ row.payload }}</pre></td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
@@ -100,6 +100,9 @@
|
||||
{% if e.invoice_number %}
|
||||
<div style="font-weight:600;">{{ e.invoice_number }}</div>
|
||||
<div style="font-size:0.85rem; color:#666;">{{ e.invoice_amount }}</div>
|
||||
{% if e.invoice_corrections_count %}
|
||||
<div style="font-size:0.78rem; color:#7c2d12; margin-top:4px;">{{ e.invoice_corrections_count }} Korrektur(en)</div>
|
||||
{% endif %}
|
||||
{% if e.invoice_paid %}
|
||||
<div style="display:inline-block; margin-top:6px; padding:2px 8px; border-radius:999px; background:#dcfce7; color:#166534; font-size:0.75rem; font-weight:700;">Bezahlt</div>
|
||||
{% if e.invoice_paid_at %}
|
||||
@@ -129,6 +132,13 @@
|
||||
</button>
|
||||
</form>
|
||||
{% endif %}
|
||||
{% if e.invoice_number %}
|
||||
<form method="post" action="{{ url_for('admin_add_invoice_correction', borrow_id=e.id) }}" onsubmit="return confirm('Korrekturbuchung hinzufügen?');">
|
||||
<input type="text" name="correction_reason" placeholder="Korrekturgrund" required style="padding:6px; border:1px solid #ddd; border-radius:6px; min-width:160px;">
|
||||
<input type="text" name="amount_delta" placeholder="Delta (optional)" style="padding:6px; border:1px solid #ddd; border-radius:6px; width:120px;">
|
||||
<button type="submit" class="btn btn-outline-danger">Korrektur</button>
|
||||
</form>
|
||||
{% endif %}
|
||||
<form method="post" action="{{ url_for('admin_reset_borrowing', borrow_id=e.id) }}" onsubmit="return confirm('Ausleihe zurücksetzen?');">
|
||||
<button type="submit" class="btn btn-warning">Zurücksetzen</button>
|
||||
</form>
|
||||
|
||||
@@ -418,6 +418,7 @@
|
||||
<li><a class="dropdown-item" href="{{ url_for('manage_filters') }}">Filter verwalten</a></li>
|
||||
<li><a class="dropdown-item" href="{{ url_for('manage_locations') }}">Orte verwalten</a></li>
|
||||
<li><a class="dropdown-item" href="{{ url_for('admin_borrowings') }}">Ausleihen</a></li>
|
||||
<li><a class="dropdown-item" href="{{ url_for('admin_audit_dashboard') }}">Audit Dashboard</a></li>
|
||||
<li><a class="dropdown-item" href="{{ url_for('logs') }}">Logs</a></li>
|
||||
<li><hr class="dropdown-divider"></li>
|
||||
<li><h6 class="dropdown-header">System</h6></li>
|
||||
|
||||
@@ -282,6 +282,9 @@
|
||||
{% if e.invoice_number %}
|
||||
<div class="mono">{{ e.invoice_number }}</div>
|
||||
<div class="muted">{{ e.invoice_amount }}</div>
|
||||
{% if e.invoice_corrections_count %}
|
||||
<div class="muted" style="color:#7c2d12;">{{ e.invoice_corrections_count }} Korrektur(en)</div>
|
||||
{% endif %}
|
||||
<div style="margin-top:6px;">
|
||||
<a class="btn btn-outline-primary btn-sm" href="{{ url_for('admin_view_invoice_pdf', borrow_id=e.id) }}" target="_blank" rel="noopener">PDF öffnen</a>
|
||||
</div>
|
||||
@@ -326,6 +329,14 @@
|
||||
</form>
|
||||
{% endif %}
|
||||
|
||||
{% if e.invoice_number %}
|
||||
<form method="post" action="{{ url_for('admin_add_invoice_correction', borrow_id=e.id) }}" onsubmit="return confirm('Korrekturbuchung hinzufügen?');">
|
||||
<input type="text" name="correction_reason" placeholder="Korrekturgrund" required style="padding:6px; border:1px solid #ddd; border-radius:6px; min-width:140px;">
|
||||
<input type="text" name="amount_delta" placeholder="Delta optional" style="padding:6px; border:1px solid #ddd; border-radius:6px; width:120px;">
|
||||
<button type="submit" class="btn btn-outline-danger btn-sm">Korrektur</button>
|
||||
</form>
|
||||
{% endif %}
|
||||
|
||||
{% if e.status in ['active', 'planned'] %}
|
||||
<form method="post" action="{{ url_for('admin_reset_borrowing', borrow_id=e.id) }}" onsubmit="return confirm('Ausleihe zurücksetzen?');">
|
||||
<button type="submit" class="btn btn-warning btn-sm">Zurücksetzen</button>
|
||||
|
||||
Reference in New Issue
Block a user