diff --git a/app.py b/app.py
index ee7c35b..c7888e4 100644
--- a/app.py
+++ b/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
diff --git a/static/js/app.js b/static/js/app.js
index 6e5e557..5ee1074 100644
--- a/static/js/app.js
+++ b/static/js/app.js
@@ -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');
}
diff --git a/static/js/paste_create.js b/static/js/paste_create.js
index bdb3566..aca2348 100644
--- a/static/js/paste_create.js
+++ b/static/js/paste_create.js
@@ -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) {
diff --git a/templates/index.html b/templates/index.html
index b8c77c5..fcd4d5e 100644
--- a/templates/index.html
+++ b/templates/index.html
@@ -13,6 +13,7 @@
+
{% if cfg.theme.allow_user_toggle %}