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 %}