Fix the clear button and improve rate limiting
This commit is contained in:
parent
0bb85da6bb
commit
53b908e651
62
app.py
62
app.py
|
|
@ -5,6 +5,7 @@ import secrets
|
|||
import sqlite3
|
||||
import sys
|
||||
import threading
|
||||
import time
|
||||
import uuid
|
||||
import datetime
|
||||
from flask import Flask, render_template, request, jsonify, abort, Response, g
|
||||
|
|
@ -128,10 +129,50 @@ def init_db():
|
|||
views INTEGER DEFAULT 0
|
||||
)
|
||||
''')
|
||||
cursor.execute('''
|
||||
CREATE TABLE IF NOT EXISTS rate_limits (
|
||||
ip_address TEXT,
|
||||
timestamp REAL,
|
||||
PRIMARY KEY (ip_address, timestamp)
|
||||
)
|
||||
''')
|
||||
cursor.execute('CREATE INDEX IF NOT EXISTS idx_rate_limit_ts ON rate_limits(timestamp)')
|
||||
conn.commit()
|
||||
finally:
|
||||
conn.close()
|
||||
_db_initialized = True
|
||||
_start_cleanup_task()
|
||||
|
||||
_cleanup_running = False
|
||||
|
||||
def _start_cleanup_task():
|
||||
"""Start a background thread to purge expired pastes every hour."""
|
||||
global _cleanup_running
|
||||
if _cleanup_running:
|
||||
return
|
||||
_cleanup_running = True
|
||||
def run():
|
||||
while True:
|
||||
try:
|
||||
# Sleep first to avoid running immediately on startup
|
||||
time.sleep(3600)
|
||||
now = datetime.datetime.now(_UTC).isoformat()
|
||||
conn = sqlite3.connect(DATABASE)
|
||||
try:
|
||||
with conn:
|
||||
res = conn.execute('DELETE FROM pastes WHERE expires_at < ?', (now,))
|
||||
purged_pastes = res.rowcount
|
||||
res = conn.execute('DELETE FROM rate_limits WHERE timestamp < ?', (time.time() - 3600,))
|
||||
purged_ips = res.rowcount
|
||||
if purged_pastes > 0 or purged_ips > 0:
|
||||
print(f"[Cleanup] Purged {purged_pastes} expired pastes and {purged_ips} rate limit entries.", flush=True)
|
||||
finally:
|
||||
conn.close()
|
||||
except Exception as e:
|
||||
print(f"Error in background cleanup: {e}", file=sys.stderr)
|
||||
|
||||
t = threading.Thread(target=run, daemon=True)
|
||||
t.start()
|
||||
|
||||
# ── Helpers ───────────────────────────────────────────────────────────────────
|
||||
|
||||
|
|
@ -171,6 +212,27 @@ def index():
|
|||
|
||||
@app.route('/create', methods=['POST'])
|
||||
def create_paste():
|
||||
# ── Rate limiting ────────────────────────────────────────────────────────
|
||||
# 10 pastes per 10 minutes per IP address (SQLite backed for worker safety)
|
||||
remote_ip = request.remote_addr
|
||||
now_ts = time.time()
|
||||
window = 600 # 10 minutes
|
||||
conn = get_db_connection()
|
||||
try:
|
||||
# Purge old for this IP specifically or just check current
|
||||
count = conn.execute(
|
||||
'SELECT COUNT(*) FROM rate_limits WHERE ip_address = ? AND timestamp > ?',
|
||||
(remote_ip, now_ts - window)
|
||||
).fetchone()[0]
|
||||
|
||||
if count >= 10:
|
||||
return jsonify({'error': 'Rate limit exceeded. Please wait a few minutes.'}), 429
|
||||
|
||||
conn.execute('INSERT INTO rate_limits (ip_address, timestamp) VALUES (?, ?)', (remote_ip, now_ts))
|
||||
conn.commit()
|
||||
finally:
|
||||
conn.close()
|
||||
|
||||
data = request.get_json(silent=True)
|
||||
if not data:
|
||||
return jsonify({'error': 'JSON body required'}), 400
|
||||
|
|
|
|||
|
|
@ -23,23 +23,38 @@ const _UI_VAR_MAP = {
|
|||
};
|
||||
|
||||
// Fetch config and initialise application when ready
|
||||
document.addEventListener('DOMContentLoaded', async () => {
|
||||
try {
|
||||
const resp = await fetch('/api/config');
|
||||
if (resp.ok) window.PBCFG = await resp.json();
|
||||
} catch (e) {
|
||||
console.warn('Could not load /api/config, using CSS fallbacks.', e);
|
||||
}
|
||||
|
||||
initialiseTheme();
|
||||
applyUiVars();
|
||||
document.addEventListener('DOMContentLoaded', () => {
|
||||
// 1. Initialise DOM-dependent listeners IMMEDIATELY
|
||||
initAutoSave();
|
||||
|
||||
// Attach theme toggle listener (fixing CSP blocking of inline onclick)
|
||||
|
||||
const toggleBtn = document.getElementById('themeToggle');
|
||||
if (toggleBtn) {
|
||||
toggleBtn.addEventListener('click', toggleTheme);
|
||||
}
|
||||
|
||||
// 2. Fetch config and apply theme/UI tweaks as a background enhancement
|
||||
(async function loadConfig() {
|
||||
try {
|
||||
const resp = await fetch('/api/config');
|
||||
if (resp.ok) {
|
||||
window.PBCFG = await resp.json();
|
||||
// Re-apply in case config changed defaults
|
||||
initialiseTheme();
|
||||
applyUiVars();
|
||||
// refresh autosave settings if they come from config
|
||||
if (window.PBCFG?.features?.auto_save_draft === false) {
|
||||
// If now disabled, we could stop listeners, but for simplicity
|
||||
// we just won't save next time.
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
console.warn('Could not load /api/config, using CSS fallbacks.', e);
|
||||
}
|
||||
})();
|
||||
|
||||
// Initial pass with CSS fallbacks
|
||||
initialiseTheme();
|
||||
applyUiVars();
|
||||
});
|
||||
|
||||
// ── Theme Management ────────────────────────────────────────────────────────
|
||||
|
|
@ -113,7 +128,7 @@ function swapPrismTheme(theme) {
|
|||
}
|
||||
}
|
||||
|
||||
function toggleTheme() {
|
||||
window.toggleTheme = function () {
|
||||
const current = document.documentElement.getAttribute('data-theme') || 'light';
|
||||
const newTheme = current === 'light' ? 'dark' : 'light';
|
||||
applyTheme(newTheme);
|
||||
|
|
@ -226,7 +241,8 @@ function loadDraft() {
|
|||
}
|
||||
}
|
||||
|
||||
function clearDraft() {
|
||||
window.clearDraft = function () {
|
||||
console.log('[Draft] Clearing localStorage draft...');
|
||||
localStorage.removeItem('paste_draft');
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -46,7 +46,31 @@ document.addEventListener('DOMContentLoaded', function () {
|
|||
|
||||
submitBtn.addEventListener('click', submitPaste);
|
||||
|
||||
// ── Clear Draft ──────────────────────────────────────────────────────────
|
||||
const clearBtn = document.getElementById('clearBtn');
|
||||
if (clearBtn) {
|
||||
clearBtn.addEventListener('click', () => {
|
||||
console.log('[Clear] Manual clear requested.');
|
||||
|
||||
// Clear the editor and title
|
||||
textarea.value = '';
|
||||
const title = document.getElementById('title');
|
||||
if (title) title.value = '';
|
||||
|
||||
// Wipe the localStorage draft via the global helper in app.js
|
||||
if (typeof window.clearDraft === 'function') {
|
||||
window.clearDraft();
|
||||
} else {
|
||||
localStorage.removeItem('paste_draft');
|
||||
}
|
||||
|
||||
textarea.focus();
|
||||
console.log('[Clear] Draft wiped.');
|
||||
});
|
||||
}
|
||||
|
||||
// ── Submit ───────────────────────────────────────────────────────────────
|
||||
|
||||
async function submitPaste() {
|
||||
const content = textarea.value;
|
||||
const title = document.getElementById('title').value || 'Untitled';
|
||||
|
|
@ -85,7 +109,9 @@ document.addEventListener('DOMContentLoaded', function () {
|
|||
submitBtn.disabled = false;
|
||||
submitBtn.textContent = 'Save';
|
||||
} else {
|
||||
clearDraft();
|
||||
if (typeof window.clearDraft === 'function') {
|
||||
window.clearDraft();
|
||||
}
|
||||
window.location.href = result.url + (keyBase64 ? '#' + keyBase64 : '');
|
||||
}
|
||||
} catch (err) {
|
||||
|
|
|
|||
|
|
@ -13,6 +13,7 @@
|
|||
<option value="1week">1 Week</option>
|
||||
<option value="1month">1 Month</option>
|
||||
</select>
|
||||
<button id="clearBtn" class="nav-btn">Clear</button>
|
||||
<button id="submitBtn" class="nav-btn nav-btn-save">Save</button>
|
||||
{% if cfg.theme.allow_user_toggle %}
|
||||
<button id="themeToggle" class="theme-toggle">🌙</button>
|
||||
|
|
|
|||
Loading…
Reference in New Issue