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 sqlite3
|
||||||
import sys
|
import sys
|
||||||
import threading
|
import threading
|
||||||
|
import time
|
||||||
import uuid
|
import uuid
|
||||||
import datetime
|
import datetime
|
||||||
from flask import Flask, render_template, request, jsonify, abort, Response, g
|
from flask import Flask, render_template, request, jsonify, abort, Response, g
|
||||||
|
|
@ -128,10 +129,50 @@ def init_db():
|
||||||
views INTEGER DEFAULT 0
|
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()
|
conn.commit()
|
||||||
finally:
|
finally:
|
||||||
conn.close()
|
conn.close()
|
||||||
_db_initialized = True
|
_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 ───────────────────────────────────────────────────────────────────
|
# ── Helpers ───────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
|
@ -171,6 +212,27 @@ def index():
|
||||||
|
|
||||||
@app.route('/create', methods=['POST'])
|
@app.route('/create', methods=['POST'])
|
||||||
def create_paste():
|
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)
|
data = request.get_json(silent=True)
|
||||||
if not data:
|
if not data:
|
||||||
return jsonify({'error': 'JSON body required'}), 400
|
return jsonify({'error': 'JSON body required'}), 400
|
||||||
|
|
|
||||||
|
|
@ -23,23 +23,38 @@ const _UI_VAR_MAP = {
|
||||||
};
|
};
|
||||||
|
|
||||||
// Fetch config and initialise application when ready
|
// Fetch config and initialise application when ready
|
||||||
document.addEventListener('DOMContentLoaded', async () => {
|
document.addEventListener('DOMContentLoaded', () => {
|
||||||
try {
|
// 1. Initialise DOM-dependent listeners IMMEDIATELY
|
||||||
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();
|
|
||||||
initAutoSave();
|
initAutoSave();
|
||||||
|
|
||||||
// Attach theme toggle listener (fixing CSP blocking of inline onclick)
|
|
||||||
const toggleBtn = document.getElementById('themeToggle');
|
const toggleBtn = document.getElementById('themeToggle');
|
||||||
if (toggleBtn) {
|
if (toggleBtn) {
|
||||||
toggleBtn.addEventListener('click', toggleTheme);
|
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 ────────────────────────────────────────────────────────
|
// ── Theme Management ────────────────────────────────────────────────────────
|
||||||
|
|
@ -113,7 +128,7 @@ function swapPrismTheme(theme) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function toggleTheme() {
|
window.toggleTheme = function () {
|
||||||
const current = document.documentElement.getAttribute('data-theme') || 'light';
|
const current = document.documentElement.getAttribute('data-theme') || 'light';
|
||||||
const newTheme = current === 'light' ? 'dark' : 'light';
|
const newTheme = current === 'light' ? 'dark' : 'light';
|
||||||
applyTheme(newTheme);
|
applyTheme(newTheme);
|
||||||
|
|
@ -226,7 +241,8 @@ function loadDraft() {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function clearDraft() {
|
window.clearDraft = function () {
|
||||||
|
console.log('[Draft] Clearing localStorage draft...');
|
||||||
localStorage.removeItem('paste_draft');
|
localStorage.removeItem('paste_draft');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -46,7 +46,31 @@ document.addEventListener('DOMContentLoaded', function () {
|
||||||
|
|
||||||
submitBtn.addEventListener('click', submitPaste);
|
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 ───────────────────────────────────────────────────────────────
|
// ── Submit ───────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
async function submitPaste() {
|
async function submitPaste() {
|
||||||
const content = textarea.value;
|
const content = textarea.value;
|
||||||
const title = document.getElementById('title').value || 'Untitled';
|
const title = document.getElementById('title').value || 'Untitled';
|
||||||
|
|
@ -85,7 +109,9 @@ document.addEventListener('DOMContentLoaded', function () {
|
||||||
submitBtn.disabled = false;
|
submitBtn.disabled = false;
|
||||||
submitBtn.textContent = 'Save';
|
submitBtn.textContent = 'Save';
|
||||||
} else {
|
} else {
|
||||||
clearDraft();
|
if (typeof window.clearDraft === 'function') {
|
||||||
|
window.clearDraft();
|
||||||
|
}
|
||||||
window.location.href = result.url + (keyBase64 ? '#' + keyBase64 : '');
|
window.location.href = result.url + (keyBase64 ? '#' + keyBase64 : '');
|
||||||
}
|
}
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
|
|
|
||||||
|
|
@ -13,6 +13,7 @@
|
||||||
<option value="1week">1 Week</option>
|
<option value="1week">1 Week</option>
|
||||||
<option value="1month">1 Month</option>
|
<option value="1month">1 Month</option>
|
||||||
</select>
|
</select>
|
||||||
|
<button id="clearBtn" class="nav-btn">Clear</button>
|
||||||
<button id="submitBtn" class="nav-btn nav-btn-save">Save</button>
|
<button id="submitBtn" class="nav-btn nav-btn-save">Save</button>
|
||||||
{% if cfg.theme.allow_user_toggle %}
|
{% if cfg.theme.allow_user_toggle %}
|
||||||
<button id="themeToggle" class="theme-toggle">🌙</button>
|
<button id="themeToggle" class="theme-toggle">🌙</button>
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue