Complete ircquotes application with all features

- Added copy quote functionality with clipboard integration
- Implemented bulk moderation actions for admin
- Created mobile responsive design with bash.org styling
- Added API rate limiting per IP address
- Implemented dark mode toggle with flash prevention
- Enhanced error messages throughout application
- Fixed all security vulnerabilities (SQL injection, XSS, CSRF)
- Added comprehensive rate limiting on all endpoints
- Implemented secure session configuration
- Added input validation and length limits
- Created centralized configuration system with config.json
- Set up production deployment with Gunicorn
- Added security headers and production hardening
- Added password generation and config management tools
This commit is contained in:
2025-09-20 19:41:23 +01:00
parent 0b1241714d
commit f409977257
21 changed files with 1936 additions and 304 deletions

217
static/voting.js Normal file
View File

@@ -0,0 +1,217 @@
// AJAX voting functionality
function vote(quoteId, action, buttonElement) {
// Prevent multiple clicks
if (buttonElement.disabled) {
return false;
}
// Disable button temporarily
buttonElement.disabled = true;
// Make AJAX request
fetch(`/vote/${quoteId}/${action}`, {
method: 'GET',
headers: {
'X-Requested-With': 'XMLHttpRequest'
}
})
.then(response => response.json())
.then(data => {
if (data.success) {
// Update vote count display
const voteElement = document.getElementById(`votes-${quoteId}`);
if (voteElement) {
voteElement.textContent = data.votes;
}
// Update button states based on user's voting history
updateButtonStates(quoteId, data.user_vote);
} else {
alert(data.message || 'Sorry, your vote could not be recorded. Please try again.');
}
})
.catch(error => {
console.error('Error:', error);
alert('Connection error while voting. Please check your internet connection and try again.');
})
.finally(() => {
// Re-enable button
buttonElement.disabled = false;
});
return false; // Prevent default link behavior
}
function updateButtonStates(quoteId, userVote) {
const upButton = document.getElementById(`up-${quoteId}`);
const downButton = document.getElementById(`down-${quoteId}`);
if (upButton && downButton) {
// Reset button styles
upButton.style.backgroundColor = '';
downButton.style.backgroundColor = '';
// Highlight the voted button
if (userVote === 'upvote') {
upButton.style.backgroundColor = '#90EE90'; // Light green
} else if (userVote === 'downvote') {
downButton.style.backgroundColor = '#FFB6C1'; // Light pink
}
}
}
// Flag quote functionality
function flag(quoteId, buttonElement) {
if (buttonElement.disabled) {
return false;
}
if (!confirm('Are you sure you want to flag this quote as inappropriate?')) {
return false;
}
buttonElement.disabled = true;
fetch(`/flag/${quoteId}`, {
method: 'GET',
headers: {
'X-Requested-With': 'XMLHttpRequest'
}
})
.then(response => response.json())
.then(data => {
if (data.success) {
alert(data.message || 'Thank you! This quote has been flagged for review by moderators.');
buttonElement.style.backgroundColor = '#FFB6C1'; // Light pink
buttonElement.textContent = '✓';
} else {
alert(data.message || 'Sorry, we could not flag this quote. Please try again.');
}
})
.catch(error => {
console.error('Error:', error);
alert('Connection error while flagging. Please check your internet connection and try again.');
})
.finally(() => {
buttonElement.disabled = false;
});
return false;
}
// Copy quote functionality
function copyQuote(quoteId, buttonElement) {
if (buttonElement.disabled) {
return false;
}
// Get the quote text
const quoteElement = document.querySelector(`#quote-${quoteId} .qt, [data-quote-id="${quoteId}"] .qt`);
let quoteText = '';
if (quoteElement) {
quoteText = quoteElement.textContent || quoteElement.innerText;
} else {
// Fallback: look for quote text in any element after the quote header
const allQuotes = document.querySelectorAll('.qt');
const quoteHeaders = document.querySelectorAll('.quote');
for (let i = 0; i < quoteHeaders.length; i++) {
const header = quoteHeaders[i];
if (header.innerHTML.includes(`#${quoteId}`)) {
if (allQuotes[i]) {
quoteText = allQuotes[i].textContent || allQuotes[i].innerText;
}
break;
}
}
}
if (!quoteText) {
alert('Sorry, we could not find the quote text to copy. Please try selecting and copying the text manually.');
return false;
}
// Format the text with quote number
const formattedText = `#${quoteId}: ${quoteText.trim()}`;
// Copy to clipboard
if (navigator.clipboard && window.isSecureContext) {
// Modern approach
navigator.clipboard.writeText(formattedText).then(() => {
showCopySuccess(buttonElement);
}).catch(() => {
fallbackCopy(formattedText, buttonElement);
});
} else {
// Fallback for older browsers
fallbackCopy(formattedText, buttonElement);
}
return false;
}
function fallbackCopy(text, buttonElement) {
// Create temporary textarea
const textArea = document.createElement('textarea');
textArea.value = text;
textArea.style.position = 'fixed';
textArea.style.left = '-999999px';
textArea.style.top = '-999999px';
document.body.appendChild(textArea);
textArea.focus();
textArea.select();
try {
document.execCommand('copy');
showCopySuccess(buttonElement);
} catch (err) {
console.error('Could not copy text: ', err);
alert('Copy to clipboard failed. Please manually select and copy the quote text using Ctrl+C (or Cmd+C on Mac).');
}
document.body.removeChild(textArea);
}
function showCopySuccess(buttonElement) {
const originalText = buttonElement.textContent;
buttonElement.textContent = '✓';
buttonElement.style.backgroundColor = '#90EE90'; // Light green
setTimeout(() => {
buttonElement.textContent = originalText;
buttonElement.style.backgroundColor = '';
}, 1500);
}
// Load user vote states when page loads
document.addEventListener('DOMContentLoaded', function() {
// Get all vote elements and check their states
const voteElements = document.querySelectorAll('[id^="votes-"]');
// Get user's voting history from cookies
const votes = getCookie('votes');
if (votes) {
try {
const voteData = JSON.parse(votes);
// Update button states for each quote
voteElements.forEach(element => {
const quoteId = element.id.replace('votes-', '');
const userVote = voteData[quoteId];
if (userVote) {
updateButtonStates(quoteId, userVote);
}
});
} catch (e) {
console.log('Could not parse vote cookie');
}
}
});
// Helper function to get cookie value
function getCookie(name) {
const value = `; ${document.cookie}`;
const parts = value.split(`; ${name}=`);
if (parts.length === 2) return parts.pop().split(';').shift();
}