fix(classifieds,admin): blokada duplikatów przez double/triple-click
Some checks are pending
NordaBiz Tests / Unit & Integration Tests (push) Waiting to run
NordaBiz Tests / E2E Tests (Playwright) (push) Blocked by required conditions
NordaBiz Tests / Smoke Tests (Production) (push) Blocked by required conditions
NordaBiz Tests / Send Failure Notification (push) Blocked by required conditions
Some checks are pending
NordaBiz Tests / Unit & Integration Tests (push) Waiting to run
NordaBiz Tests / E2E Tests (Playwright) (push) Blocked by required conditions
NordaBiz Tests / Smoke Tests (Production) (push) Blocked by required conditions
NordaBiz Tests / Send Failure Notification (push) Blocked by required conditions
B2B ogłoszenia mogły zostać stworzone 3x (user 81 Bormax 14.04.2026 w ciągu 2 sekund) — brak dedup window server-side i disable submit button. Rozszerzam zabezpieczenie także na announcements i board meeting form. - classifieds POST /nowe: odrzuć duplikat z ostatnich 60s (ten sam author+company+title) → redirect do istniejącego z flash info - classifieds new.html: disable submitBtn + "Wysyłanie..." po walidacji; ponowne kliknięcie blokowane event.preventDefault - announcements_form.html + board/meeting_form.html: jednolity handler disable wszystkich button[type="submit"] po pierwszym submit Forum topic/reply już miały analogiczne zabezpieczenie (bez zmian). Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
958b967df2
commit
6c248b4773
@ -116,6 +116,18 @@ def new():
|
||||
).first():
|
||||
form_company_id = current_user.company_id
|
||||
|
||||
# Deduplikacja double/triple-click: jeśli ten sam autor+firma+tytuł
|
||||
# wpadły w ostatnich 60 sekundach — traktuj jako powtórkę przesyłu formularza.
|
||||
recent_duplicate = db.query(Classified).filter(
|
||||
Classified.author_id == current_user.id,
|
||||
Classified.company_id == form_company_id,
|
||||
Classified.title == title,
|
||||
Classified.created_at >= datetime.now() - timedelta(seconds=60),
|
||||
).order_by(Classified.created_at.desc()).first()
|
||||
if recent_duplicate:
|
||||
flash('To ogłoszenie właśnie dodano — wyświetlamy istniejące.', 'info')
|
||||
return redirect(url_for('.classifieds_view', classified_id=recent_duplicate.id))
|
||||
|
||||
classified = Classified(
|
||||
author_id=current_user.id,
|
||||
company_id=form_company_id,
|
||||
|
||||
@ -169,7 +169,7 @@
|
||||
{% endif %}
|
||||
|
||||
<div class="form-section">
|
||||
<form method="POST">
|
||||
<form method="POST" id="announcementForm">
|
||||
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}">
|
||||
|
||||
<!-- Basic Info -->
|
||||
@ -301,6 +301,24 @@
|
||||
{% endblock %}
|
||||
|
||||
{% block extra_js %}
|
||||
// Anty double/triple-click — blokada wielokrotnego submitu
|
||||
(function() {
|
||||
const form = document.getElementById('announcementForm');
|
||||
if (!form) return;
|
||||
form.addEventListener('submit', function(e) {
|
||||
const btns = form.querySelectorAll('button[type="submit"]');
|
||||
if (Array.from(btns).some(function(b) { return b.disabled; })) {
|
||||
e.preventDefault();
|
||||
return;
|
||||
}
|
||||
btns.forEach(function(b) {
|
||||
b.dataset.originalText = b.textContent.trim();
|
||||
b.disabled = true;
|
||||
b.textContent = 'Wysyłanie...';
|
||||
});
|
||||
});
|
||||
})();
|
||||
|
||||
// Character counter for excerpt
|
||||
const excerptTextarea = document.getElementById('excerpt');
|
||||
const excerptCounter = document.getElementById('excerpt-counter');
|
||||
|
||||
@ -723,6 +723,24 @@
|
||||
{% endblock %}
|
||||
|
||||
{% block extra_js %}
|
||||
// Anty double/triple-click — blokada wielokrotnego submitu
|
||||
(function() {
|
||||
const form = document.getElementById('meetingForm');
|
||||
if (!form) return;
|
||||
form.addEventListener('submit', function(e) {
|
||||
const btns = form.querySelectorAll('button[type="submit"]');
|
||||
if (Array.from(btns).some(function(b) { return b.disabled; })) {
|
||||
e.preventDefault();
|
||||
return;
|
||||
}
|
||||
btns.forEach(function(b) {
|
||||
b.dataset.originalText = b.innerHTML;
|
||||
b.disabled = true;
|
||||
b.textContent = 'Wysyłanie...';
|
||||
});
|
||||
});
|
||||
})();
|
||||
|
||||
// Tab switching
|
||||
document.querySelectorAll('.form-tab').forEach(tab => {
|
||||
tab.addEventListener('click', function() {
|
||||
|
||||
@ -435,7 +435,13 @@ var quill = new Quill('#quill-editor', {
|
||||
// their green check immediately on page load.
|
||||
refreshTitle(); refreshCat(); refreshRadios(); refreshDesc();
|
||||
|
||||
var submitBtn = document.getElementById('submitBtn');
|
||||
document.getElementById('classifiedForm').addEventListener('submit', function(e) {
|
||||
// Anty double/triple-click — jeśli już wysyłamy, blokuj kolejny submit
|
||||
if (submitBtn && submitBtn.disabled) {
|
||||
e.preventDefault();
|
||||
return;
|
||||
}
|
||||
var html = quill.root.innerHTML;
|
||||
var empty = (html === '<p><br></p>' || quill.getText().trim() === '');
|
||||
if (empty) {
|
||||
@ -447,6 +453,10 @@ var quill = new Quill('#quill-editor', {
|
||||
}
|
||||
qc && qc.classList.remove('field-error');
|
||||
document.getElementById('description').value = html;
|
||||
if (submitBtn) {
|
||||
submitBtn.disabled = true;
|
||||
submitBtn.textContent = 'Wysyłanie...';
|
||||
}
|
||||
});
|
||||
})();
|
||||
|
||||
@ -565,7 +575,11 @@ var quill = new Quill('#quill-editor', {
|
||||
filesMap.forEach(file => formData.append('attachments[]', file));
|
||||
fetch(form.action, { method: 'POST', body: formData })
|
||||
.then(resp => { if (resp.redirected) { window.location.href = resp.url; } else { window.location.reload(); } })
|
||||
.catch(() => alert('Błąd wysyłania'));
|
||||
.catch(() => {
|
||||
alert('Błąd wysyłania');
|
||||
var btn = document.getElementById('submitBtn');
|
||||
if (btn) { btn.disabled = false; btn.textContent = 'Dodaj ogłoszenie'; }
|
||||
});
|
||||
});
|
||||
|
||||
function formatFileSize(bytes) {
|
||||
|
||||
Loading…
Reference in New Issue
Block a user