feat: Enhance CSRF protection and support for CSV file uploads across multiple templates
This commit is contained in:
@@ -14,6 +14,7 @@
|
||||
<meta name="mobile-web-app-capable" content="yes">
|
||||
<meta name="apple-mobile-web-app-capable" content="yes">
|
||||
<meta name="apple-mobile-web-app-status-bar-style" content="black-translucent">
|
||||
<meta name="csrf-token" content="{{ csrf_token }}">
|
||||
<title>{% block title %}Inventarsystem{% endblock %}</title>
|
||||
{% block head %}
|
||||
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0-alpha1/dist/css/bootstrap.min.css" rel="stylesheet">
|
||||
@@ -24,6 +25,74 @@
|
||||
<link rel="stylesheet" href="{{ url_for('static', filename='css/planned_appointments.css', v=ASSET_VERSION) }}">
|
||||
<link rel="icon" href="{{ url_for('static', filename='favicon.ico') }}">
|
||||
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0-alpha1/dist/js/bootstrap.bundle.min.js"></script>
|
||||
<script>
|
||||
(function () {
|
||||
const csrfMeta = document.querySelector('meta[name="csrf-token"]');
|
||||
const csrfToken = csrfMeta ? csrfMeta.content : '';
|
||||
if (!csrfToken) {
|
||||
return;
|
||||
}
|
||||
|
||||
const safeMethods = new Set(['GET', 'HEAD', 'OPTIONS', 'TRACE']);
|
||||
|
||||
function sameOrigin(url) {
|
||||
try {
|
||||
return new URL(url, window.location.href).origin === window.location.origin;
|
||||
} catch (error) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
function ensureFormToken(form) {
|
||||
const method = (form.getAttribute('method') || 'GET').toUpperCase();
|
||||
if (safeMethods.has(method)) {
|
||||
return;
|
||||
}
|
||||
|
||||
const action = form.getAttribute('action') || window.location.href;
|
||||
if (!sameOrigin(action)) {
|
||||
return;
|
||||
}
|
||||
|
||||
let tokenInput = form.querySelector('input[name="csrf_token"]');
|
||||
if (!tokenInput) {
|
||||
tokenInput = document.createElement('input');
|
||||
tokenInput.type = 'hidden';
|
||||
tokenInput.name = 'csrf_token';
|
||||
form.appendChild(tokenInput);
|
||||
}
|
||||
tokenInput.value = csrfToken;
|
||||
}
|
||||
|
||||
document.addEventListener('submit', function (event) {
|
||||
const form = event.target;
|
||||
if (form && form.tagName === 'FORM') {
|
||||
ensureFormToken(form);
|
||||
}
|
||||
}, true);
|
||||
|
||||
const originalFetch = window.fetch.bind(window);
|
||||
window.fetch = function (resource, init) {
|
||||
const options = init ? { ...init } : {};
|
||||
const method = (options.method || 'GET').toUpperCase();
|
||||
const targetUrl = resource instanceof Request ? resource.url : String(resource);
|
||||
if (!safeMethods.has(method) && sameOrigin(targetUrl)) {
|
||||
const headers = new Headers(resource instanceof Request ? resource.headers : undefined);
|
||||
if (options.headers) {
|
||||
new Headers(options.headers).forEach((value, key) => headers.set(key, value));
|
||||
}
|
||||
headers.set('X-CSRFToken', csrfToken);
|
||||
headers.set('X-Requested-With', 'fetch');
|
||||
options.headers = headers;
|
||||
}
|
||||
return originalFetch(resource, options);
|
||||
};
|
||||
|
||||
document.addEventListener('DOMContentLoaded', function () {
|
||||
document.querySelectorAll('form[method="post"], form[method="POST"]').forEach(ensureFormToken);
|
||||
});
|
||||
})();
|
||||
</script>
|
||||
<style>
|
||||
/* ===== MODULE DETECTION & SETUP ===== */
|
||||
:root {
|
||||
|
||||
@@ -3609,9 +3609,9 @@ document.addEventListener('DOMContentLoaded', ()=>{
|
||||
:
|
||||
`<button class="ausleihen disabled-button" disabled>${item.BlockedNow ? 'Reserviert' : 'Ausgeliehen'}</button>`
|
||||
}
|
||||
<a href="{{ url_for('delete_item', id='') }}${item._id}" onclick="return confirm('Sind Sie sicher, dass Sie dieses Objekt löschen möchten?')">
|
||||
<button class="delete-button">Löschen</button>
|
||||
</a>
|
||||
<form method="POST" action="{{ url_for('delete_item', id='') }}${item._id}" style="display:inline;" onsubmit="return confirm('Sind Sie sicher, dass Sie dieses Objekt löschen möchten?')">
|
||||
<button class="delete-button" type="submit">Löschen</button>
|
||||
</form>
|
||||
<button class="edit-button" onclick="openEditModalForSelectedUnit('${item._id}', 'specific-item-card-${item._id}')">Bearbeiten</button>
|
||||
<button class="duplicate-button" onclick="duplicateItem('${item._id}')">Duplizieren</button>
|
||||
${canScheduleItem ? `<button class="schedule-button" onclick="openScheduleModal('${item._id}')">Termin planen</button>` : ''}
|
||||
@@ -4420,9 +4420,9 @@ document.addEventListener('DOMContentLoaded', ()=>{
|
||||
<button class="duplicate-button" onclick="duplicateItem('${item._id}')">Duplizieren</button>
|
||||
${damageReports.length > 0 ? `<button class="damage-button" onclick="markDamageAsRepaired('${item._id}')">Repariert</button>` : `<button class="damage-button" onclick="registerDamage('${item._id}')">Schaden melden</button>`}
|
||||
${canScheduleItem ? `<button class="schedule-button" onclick="openScheduleModal('${item._id}')">Termin planen</button>` : ''}
|
||||
<a href="/delete_item/${item._id}" onclick="return confirm('Sind Sie sicher?')">
|
||||
<button class="delete-button">Löschen</button>
|
||||
</a>
|
||||
<form method="POST" action="/delete_item/${item._id}" style="display:inline;" onsubmit="return confirm('Sind Sie sicher?')">
|
||||
<button class="delete-button" type="submit">Löschen</button>
|
||||
</form>
|
||||
</div>
|
||||
`;
|
||||
|
||||
|
||||
@@ -239,9 +239,9 @@
|
||||
|
||||
<div style="border:1px solid #dbe4ee; border-radius:8px; padding:14px; margin-bottom:16px; background:#f8fbff;">
|
||||
<h3 style="margin:0 0 8px 0;">Excel-Import Bibliotheksausweise</h3>
|
||||
<p style="margin:0 0 10px 0; color:#555;">Laden Sie eine <strong>.xlsx</strong>-Datei hoch, zum Beispiel aus <strong>ASV (Amtliche Schuldaten)</strong>. Erkannt werden automatisch Spalten wie <strong>Name</strong>, <strong>Klasse</strong>, <strong>Ausweis-ID</strong>, <strong>Notizen</strong> und <strong>Standard-Ausleihdauer</strong>. Fehlt die Ausweis-ID, wird sie automatisch aus Name und Klasse erzeugt.</p>
|
||||
<p style="margin:0 0 10px 0; color:#555;">Laden Sie eine <strong>.xlsx</strong>- oder <strong>.csv</strong>-Datei hoch, zum Beispiel aus <strong>ASV (Amtliche Schuldaten)</strong>. Erkannt werden automatisch Spalten wie <strong>Name</strong>, <strong>Klasse</strong>, <strong>Ausweis-ID</strong>, <strong>Notizen</strong> und <strong>Standard-Ausleihdauer</strong>. Fehlt die Ausweis-ID, wird sie automatisch aus Name und Klasse erzeugt.</p>
|
||||
<form method="POST" action="{{ url_for('upload_student_cards_excel') }}" enctype="multipart/form-data" style="display:flex; gap:10px; flex-wrap:wrap; align-items:center;">
|
||||
<input type="file" name="student_cards_excel" accept=".xlsx" required>
|
||||
<input type="file" name="student_cards_excel" accept=".xlsx,.csv" required>
|
||||
<button type="submit" class="btn btn-secondary" name="excel_action" value="validate">Nur validieren</button>
|
||||
<button type="submit" class="btn btn-primary" name="excel_action" value="import">Ausweise importieren</button>
|
||||
</form>
|
||||
|
||||
@@ -714,9 +714,9 @@
|
||||
{% if show_library_features %}
|
||||
<div style="border:1px solid #dbe4ee; border-radius:8px; padding:14px; margin-bottom:16px; background:#f8fbff;">
|
||||
<h3 style="margin:0 0 8px 0;">Excel-Import Bibliothek (Mehrere Bücher)</h3>
|
||||
<p style="margin:0 0 10px 0; color:#555;">Laden Sie eine <strong>.xlsx</strong>-Datei hoch. Spalten werden automatisch erkannt (z.B. Name, Ort, Beschreibung, ISBN, Code, Anzahl). Für den Bibliotheksimport ist eine gültige ISBN je Zeile erforderlich.</p>
|
||||
<p style="margin:0 0 10px 0; color:#555;">Laden Sie eine <strong>.xlsx</strong>- oder <strong>.csv</strong>-Datei hoch. Spalten werden automatisch erkannt (z.B. Name, Ort, Beschreibung, ISBN, Code, Anzahl). Für den Bibliotheksimport ist eine gültige ISBN je Zeile erforderlich.</p>
|
||||
<form method="POST" action="{{ url_for('upload_library_excel') }}" enctype="multipart/form-data" style="display:flex; gap:10px; flex-wrap:wrap; align-items:center;">
|
||||
<input type="file" name="library_excel" accept=".xlsx" required>
|
||||
<input type="file" name="library_excel" accept=".xlsx,.csv" required>
|
||||
<button type="submit" class="btn btn-secondary" name="excel_action" value="validate">Nur validieren</button>
|
||||
<button type="submit" class="btn btn-primary" name="excel_action" value="import">Bibliothek importieren</button>
|
||||
</form>
|
||||
@@ -724,9 +724,9 @@
|
||||
{% else %}
|
||||
<div style="border:1px solid #dbe4ee; border-radius:8px; padding:14px; margin-bottom:16px; background:#f8fbff;">
|
||||
<h3 style="margin:0 0 8px 0;">Excel-Import Inventar (Mehrere Artikel)</h3>
|
||||
<p style="margin:0 0 10px 0; color:#555;">Laden Sie eine <strong>.xlsx</strong>-Datei hoch. Spalten werden automatisch erkannt (z.B. Name, Ort, Beschreibung, Filter1/2/3, Kosten, Jahr, Code, Anzahl).</p>
|
||||
<p style="margin:0 0 10px 0; color:#555;">Laden Sie eine <strong>.xlsx</strong>- oder <strong>.csv</strong>-Datei hoch. Spalten werden automatisch erkannt (z.B. Name, Ort, Beschreibung, Filter1/2/3, Kosten, Jahr, Code, Anzahl).</p>
|
||||
<form method="POST" action="{{ url_for('upload_inventory_excel') }}" enctype="multipart/form-data" style="display:flex; gap:10px; flex-wrap:wrap; align-items:center;">
|
||||
<input type="file" name="inventory_excel" accept=".xlsx" required>
|
||||
<input type="file" name="inventory_excel" accept=".xlsx,.csv" required>
|
||||
<button type="submit" class="btn btn-secondary" name="excel_action" value="validate">Nur validieren</button>
|
||||
<button type="submit" class="btn btn-primary" name="excel_action" value="import">Inventar importieren</button>
|
||||
</form>
|
||||
|
||||
Reference in New Issue
Block a user