12 Commits

Author SHA1 Message Date
StormyCloud
1a42d0b988 added CSRF 2025-08-17 20:45:38 -05:00
Stormycloud
599e331754 added security updates 2025-08-17 20:06:17 -05:00
5abc6c80ec Update templates/index.html 2025-06-25 09:47:15 -04:00
9ff1e0b0b9 Update templates/index.html 2025-06-25 09:35:06 -04:00
StormyCloud
4b3b8c9013 Merge branch 'main' of https://i2pgit.org/stormycloud/drop.i2p 2025-06-24 16:26:19 -05:00
StormyCloud
d4ae29b5ba Merge branch 'v1.1' 2025-06-24 16:24:37 -05:00
StormyCloud
79d715f3bd fixed syntax and view page issues 2025-06-24 16:22:48 -05:00
StormyCloud
4dacd8863d increased pastes viewing window to 85rem 2025-06-23 19:55:18 -05:00
9565a2f9b9 Delete wsgi.py 2025-06-21 14:58:32 -04:00
331d2d493a Delete .env 2025-06-21 14:58:20 -04:00
StormyCloud
9f56a71456 added favicon 2025-06-21 11:59:57 -05:00
StormyCloud
6d496eb99f nothing 2025-06-21 11:19:22 -05:00
9 changed files with 1359 additions and 2012 deletions

View File

@@ -1,5 +1,2 @@
#i2p-drop # i2pcake
to do:
pastebin increase width

160
app.py
View File

@@ -3,6 +3,9 @@
import os import os
import uuid import uuid
import sqlite3 import sqlite3
import mimetypes
import secrets
import re
from datetime import datetime, timedelta from datetime import datetime, timedelta
from io import BytesIO from io import BytesIO
@@ -13,6 +16,8 @@ from flask import (
from werkzeug.utils import secure_filename from werkzeug.utils import secure_filename
from werkzeug.middleware.proxy_fix import ProxyFix from werkzeug.middleware.proxy_fix import ProxyFix
from werkzeug.security import generate_password_hash, check_password_hash from werkzeug.security import generate_password_hash, check_password_hash
# Note: For production, consider adding Flask-WTF for CSRF protection
from flask_wtf.csrf import CSRFProtect
from PIL import Image from PIL import Image
from pygments import highlight from pygments import highlight
@@ -30,6 +35,9 @@ load_dotenv()
app = Flask(__name__) app = Flask(__name__)
app.wsgi_app = ProxyFix(app.wsgi_app, x_for=1, x_proto=1, x_host=1, x_prefix=1) app.wsgi_app = ProxyFix(app.wsgi_app, x_for=1, x_proto=1, x_host=1, x_prefix=1)
# Note: CSRF protection would be initialized here if Flask-WTF is available
csrf = CSRFProtect(app)
app.config['SECRET_KEY'] = os.getenv('SSP_SECRET_KEY') app.config['SECRET_KEY'] = os.getenv('SSP_SECRET_KEY')
app.config['ADMIN_PASSWORD_HASH'] = os.getenv('SSP_ADMIN_PASSWORD_HASH') app.config['ADMIN_PASSWORD_HASH'] = os.getenv('SSP_ADMIN_PASSWORD_HASH')
@@ -44,9 +52,19 @@ app.config['UPLOAD_FOLDER'] = os.getenv('SSP_UPLOAD_FOLDER', 'uploads')
app.config['DATABASE_PATH'] = os.getenv('SSP_DATABASE_PATH', 'database.db') app.config['DATABASE_PATH'] = os.getenv('SSP_DATABASE_PATH', 'database.db')
app.config['MAX_CONTENT_LENGTH'] = 10 * 1024 * 1024 # 10MB app.config['MAX_CONTENT_LENGTH'] = 10 * 1024 * 1024 # 10MB
app.config['FLASK_DEBUG'] = os.getenv('SSP_FLASK_DEBUG', 'False').lower() in ('true', '1', 't') # Ensure debug mode is never enabled in production
debug_env = os.getenv('SSP_FLASK_DEBUG', 'False').lower()
app.config['FLASK_DEBUG'] = debug_env in ('true', '1', 't') and os.getenv('FLASK_ENV') != 'production'
ALLOWED_EXTENSIONS = {'png', 'jpg', 'jpeg', 'gif', 'webp', 'bmp', 'ico', 'tiff'} ALLOWED_EXTENSIONS = {'png', 'jpg', 'jpeg', 'gif', 'webp', 'bmp', 'ico', 'tiff'}
ALLOWED_MIME_TYPES = {
'image/png', 'image/jpeg', 'image/gif', 'image/webp',
'image/bmp', 'image/x-icon', 'image/tiff'
}
# Maximum filename length and allowed characters
MAX_FILENAME_LENGTH = 255
SAFE_FILENAME_REGEX = re.compile(r'^[a-zA-Z0-9._-]+$')
# --- Rate Limiting (I2P-aware) --- # --- Rate Limiting (I2P-aware) ---
def i2p_key_func(): def i2p_key_func():
@@ -77,7 +95,7 @@ EXPIRY_MAP = {
"24h": timedelta(hours=24), "48h": timedelta(hours=48) "24h": timedelta(hours=24), "48h": timedelta(hours=48)
} }
POPULAR_LANGUAGES = [ POPULAR_LANGUAGES = [
'bash', 'c', 'cpp', 'csharp', 'css', 'go', 'html', 'java', 'javascript', 'json', 'text', 'bash', 'c', 'cpp', 'csharp', 'css', 'go', 'html', 'java', 'javascript', 'json',
'kotlin', 'lua', 'markdown', 'php', 'python', 'ruby', 'rust', 'sql', 'swift', 'kotlin', 'lua', 'markdown', 'php', 'python', 'ruby', 'rust', 'sql', 'swift',
'typescript', 'xml', 'yaml' 'typescript', 'xml', 'yaml'
] ]
@@ -146,13 +164,70 @@ def cleanup_expired_content():
try: try:
os.remove(path) os.remove(path)
except OSError as e: except OSError as e:
app.logger.error(f"Error removing expired image file {path}: {e}") app.logger.error(f"Error removing expired image file: {sanitize_error_message(e)}")
cur.execute("DELETE FROM images WHERE id = ?", (img_id,)) cur.execute("DELETE FROM images WHERE id = ?", (img_id,))
conn.commit() conn.commit()
conn.close() conn.close()
# --- Utility Functions --- # --- Utility Functions ---
def sanitize_error_message(error_msg):
"""Sanitize error messages to prevent information disclosure"""
# Remove file paths and sensitive information
sanitized = re.sub(r'/[\w/.-]+', '[path]', str(error_msg))
sanitized = re.sub(r'\b\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}\b', '[ip]', sanitized)
return sanitized
def secure_session_key(prefix, identifier):
"""Generate cryptographically secure session keys"""
random_token = secrets.token_hex(16)
return f"{prefix}_{identifier}_{random_token}"
def validate_filename_security(filename):
"""Enhanced filename validation for security"""
if not filename or len(filename) > MAX_FILENAME_LENGTH:
return False
# Check for path traversal attempts
if '..' in filename or '/' in filename or '\\' in filename:
return False
# Check for null bytes and control characters
if '\x00' in filename or any(ord(c) < 32 for c in filename if c != '\t'):
return False
# Ensure filename matches safe pattern
if not SAFE_FILENAME_REGEX.match(filename):
return False
return True
def validate_file_content(file_stream, filename):
"""Validate file content matches expected image format"""
try:
# Reset stream position
file_stream.seek(0)
# Check MIME type
mime_type, _ = mimetypes.guess_type(filename)
if mime_type not in ALLOWED_MIME_TYPES:
return False
# Try to open as image to verify it's actually an image
file_stream.seek(0)
img = Image.open(file_stream)
img.verify() # Verify it's a valid image
# Reset stream for later use
file_stream.seek(0)
return True
except Exception:
return False
def allowed_file(fn): def allowed_file(fn):
"""Enhanced file validation with security checks"""
if not fn or not validate_filename_security(fn):
return False
return '.' in fn and fn.rsplit('.', 1)[1].lower() in ALLOWED_EXTENSIONS return '.' in fn and fn.rsplit('.', 1)[1].lower() in ALLOWED_EXTENSIONS
def get_time_left(expiry_str): def get_time_left(expiry_str):
@@ -194,7 +269,7 @@ def process_and_encrypt_image(stream, orig_fn, keep_exif=False):
f.write(encrypted) f.write(encrypted)
return new_fn return new_fn
except Exception as e: except Exception as e:
app.logger.error(f"Image processing failed ({orig_fn}): {e}") app.logger.error(f"Image processing failed: {sanitize_error_message(e)}")
return None return None
@app.context_processor @app.context_processor
@@ -242,7 +317,7 @@ def healthz():
conn.close() conn.close()
db_status = "ok" db_status = "ok"
except Exception as e: except Exception as e:
app.logger.error(f"Health check DB error: {e}") app.logger.error(f"Health check DB error: {sanitize_error_message(e)}")
db_status = "error" db_status = "error"
sched_status = "running" if scheduler.running and scheduler.state == 1 else "stopped" sched_status = "running" if scheduler.running and scheduler.state == 1 else "stopped"
return jsonify(database=db_status, scheduler=sched_status) return jsonify(database=db_status, scheduler=sched_status)
@@ -253,9 +328,11 @@ def index():
db = get_db() db = get_db()
rows = db.execute("SELECT stat_key, stat_value FROM stats").fetchall() rows = db.execute("SELECT stat_key, stat_value FROM stats").fetchall()
stats = {r['stat_key']: r['stat_value'] for r in rows} stats = {r['stat_key']: r['stat_value'] for r in rows}
# We want 'text' to be at the top of the list in the index page dropdown
index_languages = [lang for lang in POPULAR_LANGUAGES if lang != 'text']
return render_template( return render_template(
'index.html', 'index.html',
languages=POPULAR_LANGUAGES, languages=index_languages,
stats=stats, stats=stats,
allowed_extensions=list(ALLOWED_EXTENSIONS) allowed_extensions=list(ALLOWED_EXTENSIONS)
) )
@@ -297,7 +374,7 @@ def upload_image():
return redirect(url_for('index', _anchor='image')) return redirect(url_for('index', _anchor='image'))
file = request.files['file'] file = request.files['file']
if file and allowed_file(file.filename): if file and allowed_file(file.filename) and validate_file_content(file.stream, file.filename):
keep_exif = bool(request.form.get('keep_exif')) keep_exif = bool(request.form.get('keep_exif'))
new_fn = process_and_encrypt_image(file.stream, file.filename, keep_exif) new_fn = process_and_encrypt_image(file.stream, file.filename, keep_exif)
if not new_fn: if not new_fn:
@@ -319,6 +396,7 @@ def upload_image():
db.commit() db.commit()
update_stat('total_images') update_stat('total_images')
flash('Image uploaded successfully! This is your shareable link.', 'success')
return redirect(url_for('view_image', filename=new_fn)) return redirect(url_for('view_image', filename=new_fn))
flash('Invalid file type.', 'error') flash('Invalid file type.', 'error')
@@ -333,6 +411,11 @@ def upload_paste():
flash('Paste content cannot be empty.', 'error') flash('Paste content cannot be empty.', 'error')
return redirect(url_for('index', _anchor='paste')) return redirect(url_for('index', _anchor='paste'))
# Input validation and size limits
if len(content) > 1024 * 1024: # 1MB limit for pastes
flash('Paste content is too large (max 1MB).', 'error')
return redirect(url_for('index', _anchor='paste'))
now = datetime.now() now = datetime.now()
expiry = now + EXPIRY_MAP.get(request.form.get('expiry', '1h'), timedelta(hours=1)) expiry = now + EXPIRY_MAP.get(request.form.get('expiry', '1h'), timedelta(hours=1))
pw = request.form.get('password') or None pw = request.form.get('password') or None
@@ -350,6 +433,7 @@ def upload_paste():
db.commit() db.commit()
update_stat('total_pastes') update_stat('total_pastes')
flash('Paste created successfully! This is your shareable link.', 'success')
return redirect(url_for('view_paste', paste_id=paste_id)) return redirect(url_for('view_paste', paste_id=paste_id))
@@ -365,10 +449,11 @@ def view_image(filename):
abort(404) abort(404)
pw_hash = row['password_hash'] pw_hash = row['password_hash']
if pw_hash and not session.get(f'unlocked_image_{filename}'): session_key = f'unlocked_image_{filename}'
if pw_hash and not session.get(session_key):
if request.method == 'POST': if request.method == 'POST':
if check_password_hash(pw_hash, request.form.get('password', '')): if check_password_hash(pw_hash, request.form.get('password', '')):
session[f'unlocked_image_{filename}'] = True session[session_key] = secrets.token_hex(16)
return redirect(url_for('view_image', filename=filename)) return redirect(url_for('view_image', filename=filename))
flash('Incorrect password.', 'error') flash('Incorrect password.', 'error')
return render_template('view_image.html', password_required=True, filename=filename) return render_template('view_image.html', password_required=True, filename=filename)
@@ -392,10 +477,11 @@ def view_paste(paste_id):
abort(404) abort(404)
pw_hash = row['password_hash'] pw_hash = row['password_hash']
if pw_hash and not session.get(f'unlocked_paste_{paste_id}'): session_key = f'unlocked_paste_{paste_id}'
if pw_hash and not session.get(session_key):
if request.method == 'POST': if request.method == 'POST':
if check_password_hash(pw_hash, request.form.get('password', '')): if check_password_hash(pw_hash, request.form.get('password', '')):
session[f'unlocked_paste_{paste_id}'] = True session[session_key] = secrets.token_hex(16)
return redirect(url_for('view_paste', paste_id=paste_id)) return redirect(url_for('view_paste', paste_id=paste_id))
flash('Incorrect password.', 'error') flash('Incorrect password.', 'error')
return render_template('view_paste.html', password_required=True, paste_id=paste_id) return render_template('view_paste.html', password_required=True, paste_id=paste_id)
@@ -405,14 +491,22 @@ def view_paste(paste_id):
db.commit() db.commit()
abort(410) abort(410)
# Only increment view count on the initial, non-overridden view
if 'lang' not in request.args:
db.execute("UPDATE pastes SET view_count = view_count + 1 WHERE id = ?", (paste_id,)) db.execute("UPDATE pastes SET view_count = view_count + 1 WHERE id = ?", (paste_id,))
db.commit() db.commit()
content = fernet.decrypt(row['content']).decode('utf-8') content = fernet.decrypt(row['content']).decode('utf-8')
# Get the language, allowing for a user override via URL parameter
default_language = row['language']
selected_language = request.args.get('lang', default_language)
try: try:
lexer = get_lexer_by_name(row['language']) lexer = get_lexer_by_name(selected_language)
except: except:
lexer = get_lexer_by_name('text') lexer = get_lexer_by_name('text')
fmt = HtmlFormatter(style='monokai', cssclass='syntax', linenos='table') fmt = HtmlFormatter(style='monokai', cssclass='syntax', linenos='table')
highlighted = highlight(content, lexer, fmt) highlighted = highlight(content, lexer, fmt)
@@ -420,7 +514,9 @@ def view_paste(paste_id):
password_required=False, password_required=False,
paste_id=paste_id, paste_id=paste_id,
highlighted_content=highlighted, highlighted_content=highlighted,
time_left=get_time_left(row['expiry_date']) time_left=get_time_left(row['expiry_date']),
languages=POPULAR_LANGUAGES,
selected_language=selected_language
) )
@@ -449,8 +545,24 @@ def paste_raw(paste_id):
@app.route('/uploads/<filename>') @app.route('/uploads/<filename>')
def get_upload(filename): def get_upload(filename):
# Enhanced security validation
if not validate_filename_security(filename):
abort(404)
safe_fn = secure_filename(filename) safe_fn = secure_filename(filename)
path = os.path.join(app.config['UPLOAD_FOLDER'], safe_fn)
# Additional path traversal protection
if safe_fn != filename or not safe_fn:
abort(404)
# Ensure the file path is within the upload directory
upload_dir = os.path.abspath(app.config['UPLOAD_FOLDER'])
file_path = os.path.abspath(os.path.join(upload_dir, safe_fn))
if not file_path.startswith(upload_dir + os.sep):
abort(404)
path = file_path
db = get_db() db = get_db()
row = db.execute("SELECT * FROM images WHERE id = ?", (safe_fn,)).fetchone() row = db.execute("SELECT * FROM images WHERE id = ?", (safe_fn,)).fetchone()
@@ -480,7 +592,7 @@ def get_upload(filename):
data = fernet.decrypt(encrypted) data = fernet.decrypt(encrypted)
return send_file(BytesIO(data), mimetype='image/webp') return send_file(BytesIO(data), mimetype='image/webp')
except Exception as e: except Exception as e:
app.logger.error(f"Error serving image {safe_fn}: {e}") app.logger.error(f"Error serving image: {sanitize_error_message(e)}")
abort(500) abort(500)
@app.route('/admin/delete/image/<filename>', methods=['POST']) @app.route('/admin/delete/image/<filename>', methods=['POST'])
@@ -495,7 +607,8 @@ def delete_image(filename):
db.commit() db.commit()
flash(f'Image "{safe}" has been deleted.', 'success') flash(f'Image "{safe}" has been deleted.', 'success')
except Exception as e: except Exception as e:
flash(f'Error deleting image file: {e}', 'error') flash('Error deleting image file.', 'error')
app.logger.error(f'Error deleting image file: {sanitize_error_message(e)}')
return redirect(url_for('admin_dashboard')) return redirect(url_for('admin_dashboard'))
@app.route('/admin/delete/paste/<paste_id>', methods=['POST']) @app.route('/admin/delete/paste/<paste_id>', methods=['POST'])
@@ -507,18 +620,20 @@ def delete_paste(paste_id):
db.commit() db.commit()
flash(f'Paste "{paste_id}" has been deleted.', 'success') flash(f'Paste "{paste_id}" has been deleted.', 'success')
except Exception as e: except Exception as e:
flash(f'Error deleting paste: {e}', 'error') flash('Error deleting paste.', 'error')
app.logger.error(f'Error deleting paste: {sanitize_error_message(e)}')
return redirect(url_for('admin_dashboard')) return redirect(url_for('admin_dashboard'))
# --- API Routes --- # --- API Routes ---
@app.route('/api/upload/image', methods=['POST']) @app.route('/api/upload/image', methods=['POST'])
@limiter.limit("50 per hour") @limiter.limit("50 per hour")
@csrf.exempt
def api_upload_image(): def api_upload_image():
if 'file' not in request.files or request.files['file'].filename == '': if 'file' not in request.files or request.files['file'].filename == '':
return jsonify(error="No file selected"), 400 return jsonify(error="No file selected"), 400
file = request.files['file'] file = request.files['file']
if file and allowed_file(file.filename): if file and allowed_file(file.filename) and validate_file_content(file.stream, file.filename):
new_fn = process_and_encrypt_image(file.stream, file.filename, bool(request.form.get('keep_exif'))) new_fn = process_and_encrypt_image(file.stream, file.filename, bool(request.form.get('keep_exif')))
if not new_fn: return jsonify(error="Failed to process image"), 500 if not new_fn: return jsonify(error="Failed to process image"), 500
@@ -543,13 +658,21 @@ def api_upload_image():
@app.route('/api/upload/paste', methods=['POST']) @app.route('/api/upload/paste', methods=['POST'])
@limiter.limit("100 per hour") @limiter.limit("100 per hour")
@csrf.exempt
def api_upload_paste(): def api_upload_paste():
if not request.is_json: return jsonify(error="Request must be JSON"), 400 if not request.is_json: return jsonify(error="Request must be JSON"), 400
data = request.get_json() data = request.get_json()
if not isinstance(data, dict):
return jsonify(error="Invalid JSON data"), 400
content = data.get('content', '').strip() content = data.get('content', '').strip()
if not content: return jsonify(error="Paste content is missing"), 400 if not content: return jsonify(error="Paste content is missing"), 400
# Input validation and size limits
if len(content) > 1024 * 1024: # 1MB limit for pastes
return jsonify(error="Paste content is too large (max 1MB)"), 400
now = datetime.now() now = datetime.now()
expiry = now + EXPIRY_MAP.get(data.get('expiry', '1h'), timedelta(hours=1)) expiry = now + EXPIRY_MAP.get(data.get('expiry', '1h'), timedelta(hours=1))
pw = data.get('password') pw = data.get('password')
@@ -587,4 +710,3 @@ if __name__ == '__main__':
# Run the app. Debug mode is controlled by the SSP_FLASK_DEBUG environment variable. # Run the app. Debug mode is controlled by the SSP_FLASK_DEBUG environment variable.
# For production, it's recommended to use a proper WSGI server like Gunicorn or uWSGI. # For production, it's recommended to use a proper WSGI server like Gunicorn or uWSGI.
app.run(debug=app.config['FLASK_DEBUG'], use_reloader=False) app.run(debug=app.config['FLASK_DEBUG'], use_reloader=False)

View File

@@ -1,418 +1,152 @@
<!DOCTYPE html> <!DOCTYPE html>
<html lang="en"> <html lang="en">
<head> <head>
<meta charset="UTF-8"/> <meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0"/> <meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>I2P Secure Share</title> <title>Admin Dashboard - I2P Secure Share</title>
<link rel="stylesheet" href="{{ url_for('static', filename='css/tailwind.css') }}"/> <link rel="stylesheet" href="{{ url_for('static', filename='css/tailwind.css') }}">
<link rel="icon" href="{{ url_for('static', filename='favicon.ico') }}" type="image/x-icon"> <link rel="icon" href="{{ url_for('static', filename='favicon.ico') }}" type="image/x-icon">
<style> <style>
body { background-color: #1a202c; color: #cbd5e0; } body { background-color: #1a202c; color: #cbd5e0; }
.content-container { background-color: #2d3748; border:1px solid #4a5568; border-radius:0.5rem; } .content-container { background-color: #2d3748; border: 1px solid #4a5568; }
.tab { border-bottom:2px solid transparent; cursor:pointer; } .btn { background-color: #4299e1; transition: background-color 0.3s ease; }
.tab.active { border-bottom-color:#63b3ed; color:#ffffff; } .btn:hover { background-color: #3182ce; }
.btn { background-color:#4299e1; transition:background-color .3s ease; } .btn-danger { background-color: #e53e3e; }
.btn:hover { background-color:#3182ce; } .btn-danger:hover { background-color: #c53030; }
select,textarea,input[type="text"],input[type="password"],input[type="number"] { input { background-color: #4a5568; border: 1px solid #718096; }
background-color:#4a5568; border:1px solid #718096; color:#cbd5e0; table { width: 100%; border-collapse: collapse; }
} th, td { border: 1px solid #4a5568; padding: 0.75rem; text-align: left; }
.alert-success { background-color:#38a169; } th { background-color: #1a202c; }
.alert-error { background-color:#e53e3e; } tr:nth-child(even) { background-color: #2d3748; }
.announcement-bar { background-color:#2563eb; border-bottom:1px solid #1e3a8a; } .alert-error { background-color: #e53e3e; }
.reveal { display:none; } .announcement-bar { background-color: #2563eb; border-bottom: 1px solid #1e3a8a; }
input[type="file"] {
width:100%; padding:0.5rem 1rem; border-radius:0.375rem;
background-color:#4a5568; border:1px solid #718096; color:#cbd5e0; cursor:pointer;
}
input[type="file"]::file-selector-button {
background-color:#2d3748; color:#cbd5e0; border:none;
padding:0.5rem 1rem; margin-right:1rem; border-radius:0.375rem; cursor:pointer;
transition:background-color .3s ease;
}
input[type="file"]::file-selector-button:hover { background-color:#3a4a5a; }
.stat-card {
background-color:#2d3748; border:1px solid #4a5568; border-radius:0.5rem;
}
.stat-value { color:#63b3ed; }
.label-with-icon {
display:inline-flex; align-items:center; gap:0.5rem; font-size:0.875rem; color:#cbd5e0;
}
.label-with-icon svg {
width:1rem; height:1rem; color:#4299e1; flex-shrink:0;
}
.feature-card {
background-color: #2d3748;
border: 1px solid #4a5568;
border-radius: 0.5rem;
}
/* API Docs Styling */
.docs-container h3 {
font-size: 1.5rem;
font-weight: 600;
color: #ffffff;
margin-top: 2rem;
margin-bottom: 1rem;
padding-bottom: 0.5rem;
border-bottom: 1px solid #4a5568;
}
.docs-container p { margin-bottom: 1rem; color: #a0aec0; }
.docs-container ul { list-style-position: inside; margin-bottom: 1rem; }
.docs-container li { margin-bottom: 0.5rem; color: #cbd5e0;}
.docs-container code {
background-color:#1a202c;
color:#f7fafc;
padding:0.2rem 0.4rem;
border-radius:0.25rem;
font-family:monospace;
font-size: 0.875rem;
}
.docs-container pre {
background-color:#1a202c;
padding:1rem;
border-radius:0.5rem;
overflow-x:auto;
color:#f7fafc;
font-family:monospace;
}
</style> </style>
<noscript>
<style>
.tab-nav-container{display:none;}
.tab-content{display:none!important;}
#image-form,#paste-form{display:block!important;margin-bottom:2rem;}
#api-docs,#stats-content,#tos-content,.features-section{display:none!important;}
@media(min-width:1024px){.noscript-forms-container{display:flex;gap:1.5rem;} .noscript-forms-container>div{flex:1;}}
.reveal{display:block!important;}
</style>
</noscript>
</head> </head>
<body class="font-sans"> <body class="font-sans">
{% if announcement_enabled and announcement_message %} {% if announcement_enabled and announcement_message %}
<div id="announcement-bar" class="announcement-bar text-white text-center p-2 relative shadow-lg"> <div id="announcement-bar" class="announcement-bar text-white text-center p-2 relative shadow-lg">
<span>{{ announcement_message }}</span> <span>{{ announcement_message }}</span>
<button id="close-announcement" <button id="close-announcement" class="absolute top-0 right-0 mt-2 mr-4 text-white hover:text-gray-200 text-2xl leading-none">&times;</button>
class="absolute top-0 right-0 mt-2 mr-4 text-white hover:text-gray-200 text-2xl leading-none">&times;</button>
</div> </div>
{% endif %} {% endif %}
<div class="flex items-center justify-center min-h-screen py-8"> <div class="flex items-center justify-center min-h-screen py-8">
<div id="main-container" class="w-full max-w-2xl mx-auto p-4"> <a href="/" class="inline-block mb-4">
<header class="text-center mb-8"> <img src="/static/images/stormycloud.svg" alt="StormyCloud Logo" style="width:350px; max-width:100%;" class="mx-auto"/>
<a href="/" class="inline-block mb-4"> </a>
<img src="/static/images/stormycloud.svg" <h1 class="text-4xl font-bold text-white">Admin Dashboard</h1>
alt="StormyCloud Logo"
style="width:550px;max-width:100%;"
class="mx-auto"/>
</a>
<h1 class="text-4xl font-bold text-white">I2P Secure Share</h1>
<p class="text-gray-400">Anonymously share images and text pastes.</p>
</header> </header>
<main> <main class="content-container rounded-lg p-8 shadow-lg">
<div id="js-error-container" {% if auth_success %}
class="hidden alert-error text-white p-3 rounded-md shadow-lg mb-4" <h2 class="text-2xl font-semibold text-white mb-6">Active Images</h2>
role="alert"></div> {% if images %}
<div class="overflow-x-auto">
<table>
<thead>
<tr>
<th>Filename</th>
<th>Expires On (UTC)</th>
<th>Time Left</th>
<th>Action</th>
</tr>
</thead>
<tbody>
{% for image in images %}
<tr>
<td class="font-mono text-sm break-all">{{ image[0] }}</td>
<td class="font-mono text-sm">{{ image[1] }}</td>
<td class="font-mono text-sm">{{ image[2] }}</td>
<td>
<form action="{{ url_for('delete_image', filename=image[0]) }}" method="POST" onsubmit="return confirm('Are you sure you want to delete this image?');">
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}"/>
<button type="submit" class="btn-danger text-white font-bold py-1 px-3 rounded text-sm">Delete</button>
</form>
</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
{% else %}
<p class="text-gray-400">No active images.</p>
{% endif %}
<h2 class="text-2xl font-semibold text-white mt-12 mb-6">Active Pastes</h2>
{% if pastes %}
<div class="overflow-x-auto">
<table>
<thead>
<tr>
<th>ID</th>
<th>Language</th>
<th>Expires On (UTC)</th>
<th>Time Left</th>
<th>Action</th>
</tr>
</thead>
<tbody>
{% for paste in pastes %}
<tr>
<td class="font-mono text-sm break-all">{{ paste[0] }}</td>
<td class="font-mono text-sm">{{ paste[1] }}</td>
<td class="font-mono text-sm">{{ paste[2] }}</td>
<td class="font-mono text-sm">{{ paste[3] }}</td>
<td>
<form action="{{ url_for('delete_paste', paste_id=paste[0]) }}" method="POST" onsubmit="return confirm('Are you sure you want to delete this paste?');">
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}"/>
<button type="submit" class="btn-danger text-white font-bold py-1 px-3 rounded text-sm">Delete</button>
</form>
</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
{% else %}
<p class="text-gray-400">No active pastes.</p>
{% endif %}
{% else %}
<h2 class="text-2xl font-semibold text-white mb-6">Admin Login</h2>
{% with messages = get_flashed_messages(with_categories=true) %} {% with messages = get_flashed_messages(with_categories=true) %}
{% if messages %} {% if messages %}
<div class="mb-4"> <div class="mb-4">
{% for category,message in messages %} {% for category, message in messages %}
<div class="alert-{{category}} text-white p-3 rounded-md shadow-lg" <div class="alert-{{ category }} text-white p-3 rounded-md shadow-lg" role="alert">
role="alert">{{ message }}</div> {{ message }}
</div>
{% endfor %} {% endfor %}
</div> </div>
{% endif %} {% endif %}
{% endwith %} {% endwith %}
<form method="POST" action="{{ url_for('admin_dashboard') }}">
<div class="mb-4 border-b border-gray-700 tab-nav-container"> <input type="hidden" name="csrf_token" value="{{ csrf_token() }}"/>
<nav class="flex -mb-px" id="tab-nav">
<a href="#image"
class="tab active text-gray-300 py-4 px-6 block hover:text-white">Image Uploader</a>
<a href="#paste"
class="tab text-gray-300 py-4 px-6 block hover:text-white">Pastebin</a>
<a href="#api"
class="tab text-gray-300 py-4 px-6 block hover:text-white">API</a>
<a href="#stats"
class="tab text-gray-300 py-4 px-6 block hover:text-white">Stats</a>
<a href="#tos"
class="tab text-gray-300 py-4 px-6 block hover:text-white">Terms</a>
</nav>
</div>
<div class="noscript-forms-container">
<!-- IMAGE FORM -->
<div id="image-form" class="content-container rounded-lg p-8 shadow-lg tab-content">
<form action="/upload/image" method="POST" enctype="multipart/form-data">
<h2 class="text-2xl font-semibold mb-6 text-white">Upload an Image</h2>
<div class="mb-6"> <div class="mb-6">
<label for="image-file" class="block text-gray-300 text-sm font-bold mb-2">Image File:</label> <label for="password" class="block text-gray-300 text-sm font-bold mb-2">Password:</label>
<input type="file" name="file" id="image-file" required> <input type="password" name="password" id="password" class="w-full p-2 rounded-md text-white focus:outline-none focus:ring-2 focus:ring-blue-500" required>
<p class="text-xs text-gray-500 mt-1">Max 10MB; WebP conversion.</p>
</div> </div>
<div><button type="submit" class="btn w-full text-white font-bold py-3 px-5 rounded-md focus:shadow-outline">Login</button></div>
<div class="mb-6">
<label for="image-expiry" class="block text-gray-300 text-sm font-bold mb-2">Delete after:</label>
<select name="expiry" id="image-expiry" class="w-full p-2 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500">
<option value="15m">15 minutes</option>
<option value="1h" selected>1 hour</option>
<option value="2h">2 hours</option>
<option value="4h">4 hours</option>
<option value="8h">8 hours</option>
<option value="12h">12 hours</option>
<option value="24h">24 hours</option>
<option value="48h">48 hours</option>
</select>
</div>
<div class="mb-6 text-gray-300" style="display:grid;grid-template-columns:repeat(3,1fr);gap:1.5rem;align-items:start;">
<label class="label-with-icon">
<input type="checkbox" name="keep_exif">
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke="currentColor"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 12h6m-6 4h6M7 7h10M4 6h16M4 6a2 2 0 012-2h8l2 2h6a2 2 0 012 2v12a2 2 0 01-2 2H6a2 2 0 01-2-2V6z"/></svg>
<span>Keep EXIF Data</span>
</label>
<label class="label-with-icon">
<input type="checkbox" id="image-pw-protect" name="password_protect">
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke="currentColor"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 11c1.657 0 3-1.343 3-3V6a3 3 0 10-6 0v2c0 1.657 1.343 3 3 3z"/><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M5 11h14a2 2 0 012 2v8a2 2 0 01-2 2H5a2 2 0 01-2-2v-8a2 2 0 012-2z"/></svg>
<span>Password</span>
</label>
<label class="label-with-icon" title="Removed after this many successful views">
<input type="checkbox" id="image-views-protect" name="views_protect">
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke="currentColor"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 12a3 3 0 11-6 0 3 3 0 016 0z"/><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M2.458 12C3.732 7.943 7.523 5 12 5c4.477 0 8.268 2.943 9.542 7-1.274 4.057-5.065 7-9.542 7-4.477 0-8.268-2.943-9.542-7z"/></svg>
<span>Max Views</span>
</label>
</div>
<div id="image-pw-options" class="reveal mb-6">
<label for="image-password" class="block text-gray-300 text-sm font-bold mb-1">Password:</label>
<input type="password" name="password" id="image-password" class="w-full p-2 rounded-md border-gray-600 focus:outline-none focus:ring-2 focus:ring-blue-500">
</div>
<div id="image-views-options" class="reveal mb-6">
<label for="image-max-views" class="block text-gray-300 text-sm font-bold mb-1">Max views:</label>
<input type="number" name="max_views" id="image-max-views" min="1" class="w-full p-2 rounded-md border-gray-600 focus:outline-none focus:ring-2 focus:ring-blue-500">
</div>
<button type="submit" class="btn w-full text-white font-bold py-3 px-5 rounded-md focus:shadow-outline">Upload Image</button>
</form> </form>
{% endif %}
<div class="text-center mt-8 border-t border-gray-700 pt-6">
<a href="{{ url_for('index') }}" class="text-blue-400 hover:text-blue-300">Back to Homepage</a>
</div> </div>
</main>
<!-- PASTE FORM -->
<div id="paste-form" class="content-container rounded-lg p-8 shadow-lg hidden tab-content">
<form action="/upload/paste" method="POST">
<h2 class="text-2xl font-semibold mb-6 text-white">Create a Paste</h2>
<div class="mb-6">
<label for="paste-content" class="block text-gray-300 text-sm font-bold mb-2">Paste Content:</label>
<textarea name="content" id="paste-content" rows="10" class="w-full p-2 rounded-md font-mono focus:outline-none focus:ring-2 focus:ring-blue-500" required></textarea>
</div>
<div class="mb-6">
<label for="paste-language" class="block text-gray-300 text-sm font-bold mb-2">Language:</label>
<select name="language" id="paste-language" class="w-full p-2 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500">
<option value="text">Plain Text</option>
{% for lang in languages %}
<option value="{{ lang }}">{{ lang|capitalize }}</option>
{% endfor %}
</select>
</div>
<div class="mb-6">
<label for="paste-expiry" class="block text-gray-300 text-sm font-bold mb-2">Delete after:</label>
<select name="expiry" id="paste-expiry" class="w-full p-2 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500">
<option value="15m">15 minutes</option>
<option value="1h" selected>1 hour</option>
<option value="2h">2 hours</option>
<option value="4h">4 hours</option>
<option value="8h">8 hours</option>
<option value="12h">12 hours</option>
<option value="24h">24 hours</option>
<option value="48h">48 hours</option>
</select>
</div>
<div class="mb-6 text-gray-300" style="display:grid;grid-template-columns:repeat(2,1fr);gap:2rem;align-items:start;">
<label class="label-with-icon">
<input type="checkbox" id="paste-pw-protect" name="password_protect">
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke="currentColor"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 11c1.657 0 3-1.343 3-3V6a3 3 0 10-6 0v2c0 1.657 1.343 3 3 3z"/><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M5 11h14a2 2 0 012 2v8a2 2 0 01-2 2H5a2 2 0 01-2-2v-8a2 2 0 012-2z"/></svg>
<span>Password</span>
</label>
<label class="label-with-icon" title="Removed after this many successful views">
<input type="checkbox" id="paste-views-protect" name="views_protect">
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke="currentColor"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 12a3 3 0 11-6 0 3 3 0 016 0z"/><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M2.458 12C3.732 7.943 7.523 5 12 5c4.477 0 8.268 2.943 9.542 7-1.274 4.057-5.065 7-9.542 7-4.477 0-8.268-2.943-9.542-7z"/></svg>
<span>Max Views</span>
</label>
</div>
<div id="paste-pw-options" class="reveal mb-6">
<label for="paste-password" class="block text-gray-300 text-sm font-bold mb-1">Password:</label>
<input type="password" name="password" id="paste-password" class="w-full p-2 rounded-md border-gray-600 focus:outline-none focus:ring-2 focus:ring-blue-500">
</div>
<div id="paste-views-options" class="reveal mb-6">
<label for="paste-max-views" class="block text-gray-300 text-sm font-bold mb-1">Max views:</label>
<input type="number" name="max_views" id="paste-max-views" min="1" class="w-full p-2 rounded-md border-gray-600 focus:outline-none focus:ring-2 focus:ring-blue-500">
</div>
<button type="submit" class="btn w-full text-white font-bold py-3 px-5 rounded-md focus:shadow-outline">Create Paste</button>
</form>
</div>
<!-- API DOCS -->
<div id="api-docs" class="content-container docs-container rounded-lg p-8 shadow-lg hidden tab-content">
<h3>Introduction</h3>
<p>The API allows programmatic uploads. All endpoints are rate-limited. No API key is required.</p>
<h3>Uploading an Image</h3>
<p>Send a <code>POST</code> request with <code>multipart/form-data</code>.</p>
<ul>
<li><strong>Endpoint:</strong> <code>POST /api/upload/image</code></li>
<li><strong>Parameter <code>file</code>:</strong> (Required) The image file.</li>
<li><strong>Parameter <code>expiry</code>:</strong> (Optional) Values: <code>15m</code>, <code>1h</code>, <code>2h</code>, <code>4h</code>, <code>8h</code>, <code>12h</code>, <code>24h</code>, <code>48h</code>.</li>
<li><strong>Parameter <code>password</code>:</strong> (Optional) A password to protect the content.</li>
<li><strong>Parameter <code>max_views</code>:</strong> (Optional) An integer for auto-deletion after N views.</li>
</ul>
<pre>curl -X POST -F "file=@/path/to/image.jpg" http://{{ request.host }}/api/upload/image</pre>
<h3>Creating a Paste</h3>
<p>Send a <code>POST</code> request with a JSON payload.</p>
<ul>
<li><strong>Endpoint:</strong> <code>POST /api/upload/paste</code></li>
<li><strong>JSON Field <code>content</code>:</strong> (Required) The paste text.</li>
<li><strong>JSON Field <code>language</code>:</strong> (Optional) A valid language for syntax highlighting. Defaults to 'text'.</li>
<li><strong>JSON Field <code>expiry</code>:</strong> (Optional) Same values as image expiry.</li>
<li><strong>JSON Field <code>password</code>:</strong> (Optional) A password to protect the content.</li>
<li><strong>JSON Field <code>max_views</code>:</strong> (Optional) An integer for auto-deletion after N views.</li>
</ul>
<pre>curl -X POST -H "Content-Type: application/json" \
-d '{"content":"Hello World", "expiry":"1h"}' \
http://{{ request.host }}/api/upload/paste</pre>
</div>
<!-- STATS -->
<div id="stats-content" class="content-container rounded-lg p-8 shadow-lg hidden tab-content">
<h2 class="text-3xl font-bold text-white text-center mb-8">Service Statistics</h2>
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
<div class="stat-card p-6 text-center">
<h3 class="text-xl font-semibold text-white mb-2">Total Image Uploads</h3>
<p class="text-5xl font-bold stat-value">{{ stats.total_images }}</p>
</div>
<div class="stat-card p-6 text-center">
<h3 class="text-xl font-semibold text-white mb-2">Total Paste Uploads</h3>
<p class="text-5xl font-bold stat-value">{{ stats.total_pastes }}</p>
</div>
<div class="stat-card p-6 text-center">
<h3 class="text-xl font-semibold text-white mb-2">Total API Uploads</h3>
<p class="text-5xl font-bold stat-value">{{ stats.total_api_uploads }}</p>
</div>
</div>
</div>
<!-- TERMS OF SERVICE -->
<div id="tos-content" class="content-container tos-container rounded-lg p-8 shadow-lg hidden tab-content">
<h2 class="text-3xl font-bold text-white mb-4">Terms of Service</h2>
<p><strong>Last Updated: June 20, 2025</strong></p>
<p>By using this service you agree to these terms. The service is provided “as-is” with a focus on privacy.</p>
<h3 class="mt-6 text-white text-xl font-semibold mb-2">1. Privacy & Data</h3>
<ul class="list-disc list-inside text-gray-300 mb-4">
<li>No logs of your identity or IP.</li>
<li>All uploads are encrypted at rest.</li>
<li>Data auto-deletes after expiry or view limit.</li>
<li>EXIF metadata only kept if opted-in.</li>
</ul>
<h3 class="mt-6 text-white text-xl font-semibold mb-2">2. Acceptable Use</h3>
<p class="text-gray-300 mb-4">Do not upload illegal or harmful content. We reserve the right to remove content that violates these terms.</p>
<h3 class="mt-6 text-white text-xl font-semibold mb-2">3. Liability</h3>
<p class="text-gray-300">This free service comes with no warranties. We are not responsible for data loss.</p>
</div>
</div>
<!-- Version Info (Moved outside the tabbed content) -->
<div class="text-center text-xs text-gray-500 mt-6 mb-8">
<a href="https://github.com/your-username/your-repo-name" target="_blank" rel="noopener noreferrer" class="hover:text-gray-400 transition-colors">
Version 1.1
</a>
</div>
<!-- FEATURES -->
<section class="text-center features-section">
<h2 class="text-3xl font-bold text-white mb-8">Features</h2>
<div class="grid grid-cols-1 md:grid-cols-3 gap-8">
<div class="feature-card p-6 rounded-lg">
<div class="flex items-center justify-center h-12 w-12 rounded-md bg-blue-500 text-white mx-auto mb-4">
<svg xmlns="http://www.w3.org/2000/svg" class="h-6 w-6" fill="none" viewBox="0 0 24 24" stroke="currentColor"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 15v2m-6 4h12a2 2 0 002-2v-6a2 2 0 00-2-2H6a2 2 0 00-2 2v6a2 2 0 002 2zm10-10V7a4 4 0 00-8 0v4h8z" /></svg>
</div>
<h3 class="text-xl font-semibold text-white mb-2">Encrypted at Rest</h3>
<p class="text-gray-400">All uploaded files and pastes are fully encrypted on the server, ensuring your data is protected.</p>
</div>
<div class="feature-card p-6 rounded-lg">
<div class="flex items-center justify-center h-12 w-12 rounded-md bg-blue-500 text-white mx-auto mb-4">
<svg xmlns="http://www.w3.org/2000/svg" class="h-6 w-6" fill="none" viewBox="0 0 24 24" stroke="currentColor"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 11c0 3.517-1.009 6.786-2.673 9.356 m-2.673-9.356C6.673 8.214 9.327 5.5 12 5.5 c2.673 0 5.327 2.714 5.327 5.5s-1.009 6.786-2.673 9.356m-2.673-9.356h0z" /></svg>
</div>
<h3 class="text-xl font-semibold text-white mb-2">Anonymous by Design</h3>
<p class="text-gray-400">Image metadata (EXIF) is stripped and no unnecessary logs are kept. Built for the I2P network.</p>
</div>
<div class="feature-card p-6 rounded-lg">
<div class="flex items-center justify-center h-12 w-12 rounded-md bg-blue-500 text-white mx-auto mb-4">
<svg xmlns="http://www.w3.org/2000/svg" class="h-6 w-6" fill="none" viewBox="0 0 24 24" stroke="currentColor"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M3 15a4 4 0 004 4h9a5 5 0 10-.1-9.999 5.002 5.002 0 10-9.78 2.096A4.001 4.001 0 003 15z" /></svg>
</div>
<h3 class="text-xl font-semibold text-white mb-2">StormyCloud Infrastructure</h3>
<p class="text-gray-400">A fast, reliable, and secure platform dedicated to the privacy of the I2P community.</p>
</div>
</div>
</section>
<footer class="text-center text-gray-500 mt-16 border-t border-gray-700 pt-8 pb-8"> <footer class="text-center text-gray-500 mt-16 border-t border-gray-700 pt-8 pb-8">
<a href="http://stormycloud.i2p" class="hover:text-gray-400">StormyCloud</a> <a href="http://stormycloud.i2p" class="hover:text-gray-400">StormyCloud</a>
<span class="mx-2">|</span>
<a href="/donate" class="hover:text-gray-400">Donate</a>
</footer> </footer>
</main>
</div> </div>
</div> </div>
<script>
<script> const announcementBar = document.getElementById('announcement-bar');
document.addEventListener('DOMContentLoaded', () => { const closeButton = document.getElementById('close-announcement');
const contentDivs = { if (closeButton) {
'#image': document.querySelector('#image-form'), closeButton.addEventListener('click', () => {
'#paste': document.querySelector('#paste-form'), announcementBar.style.display = 'none';
'#api': document.querySelector('#api-docs'),
'#stats': document.querySelector('#stats-content'),
'#tos': document.querySelector('#tos-content')
};
const tabs = document.querySelectorAll('#tab-nav a');
function showTab(hash) {
if (!hash || !contentDivs[hash]) hash = '#image';
Object.values(contentDivs).forEach(d => {
if(d) d.classList.add('hidden');
}); });
tabs.forEach(t => t.classList.remove('active'));
if(contentDivs[hash]) {
contentDivs[hash].classList.remove('hidden');
} }
const activeTab = document.querySelector(`#tab-nav a[href="${hash}"]`); </script>
if(activeTab) {
activeTab.classList.add('active');
}
}
tabs.forEach(tab => tab.addEventListener('click', e => {
e.preventDefault();
const h = e.target.hash;
window.history.replaceState(null, null, ' ' + h);
showTab(h);
}));
showTab(window.location.hash);
function toggle(cbId, tgtId) {
const cb = document.getElementById(cbId), tgt = document.getElementById(tgtId);
if (!cb||!tgt) return;
cb.addEventListener('change', () => tgt.style.display = cb.checked ? 'block' : 'none');
tgt.style.display = cb.checked ? 'block' : 'none';
}
toggle('image-pw-protect','image-pw-options');
toggle('image-views-protect','image-views-options');
toggle('paste-pw-protect','paste-pw-options');
toggle('paste-views-protect','paste-views-options');
const closeBtn = document.getElementById('close-announcement');
if (closeBtn) closeBtn.addEventListener('click', ()=>
document.getElementById('announcement-bar').style.display = 'none'
);
});
</script>
</body> </body>
</html> </html>

View File

@@ -4,8 +4,9 @@
<meta charset="UTF-8"> <meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0"> <meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Support the Project - I2P Secure Share</title> <title>Support the Project - I2P Secure Share</title>
<link rel="stylesheet" href="{{ url_for('static', filename='css/tailwind.css') }}"> <link rel="stylesheet" href="/static/css/tailwind.css">
<link rel="icon" href="{{ url_for('static', filename='favicon.ico') }}" type="image/x-icon"> <link rel="icon" href="{{ url_for('static', filename='favicon.ico') }}" type="image/x-icon">
<style> <style>
body { background-color: #1a202c; color: #cbd5e0; } body { background-color: #1a202c; color: #cbd5e0; }
.content-container { background-color: #2d3748; border: 1px solid #4a5568; } .content-container { background-color: #2d3748; border: 1px solid #4a5568; }
@@ -25,9 +26,9 @@
<div class="flex items-center justify-center min-h-screen py-8"> <div class="flex items-center justify-center min-h-screen py-8">
<div class="w-full max-w-2xl mx-auto p-4"> <div class="w-full max-w-2xl mx-auto p-4">
<header class="text-center mb-8"> <header class="text-center mb-8">
<a href="/" class="inline-block mb-4"> <a href="/" class="inline-block mb-4">
<img src="{{ url_for('static', filename='images/stormycloud.svg') }}" alt="StormyCloud Logo" style="width: 550px; max-width: 100%;" class="mx-auto"> <img src="/static/images/stormycloud.svg" alt="StormyCloud Logo" style="width:350px; max-width:100%;" class="mx-auto"/>
</a> </a>
<h1 class="text-3xl font-bold text-white">Support the Service</h1> <h1 class="text-3xl font-bold text-white">Support the Service</h1>
</header> </header>

View File

@@ -4,8 +4,9 @@
<meta charset="UTF-8"/> <meta charset="UTF-8"/>
<meta name="viewport" content="width=device-width, initial-scale=1.0"/> <meta name="viewport" content="width=device-width, initial-scale=1.0"/>
<title>I2P Secure Share</title> <title>I2P Secure Share</title>
<link rel="stylesheet" href="{{ url_for('static', filename='css/tailwind.css') }}"/> <link rel="stylesheet" href="/static/css/tailwind.css"/>
<link rel="icon" href="{{ url_for('static', filename='favicon.ico') }}" type="image/x-icon"> <link rel="icon" href="{{ url_for('static', filename='favicon.ico') }}" type="image/x-icon">
<style> <style>
body { background-color: #1a202c; color: #cbd5e0; } body { background-color: #1a202c; color: #cbd5e0; }
.content-container { background-color: #2d3748; border:1px solid #4a5568; border-radius:0.5rem; } .content-container { background-color: #2d3748; border:1px solid #4a5568; border-radius:0.5rem; }
@@ -99,12 +100,9 @@
<div class="flex items-center justify-center min-h-screen py-8"> <div class="flex items-center justify-center min-h-screen py-8">
<div id="main-container" class="w-full max-w-2xl mx-auto p-4"> <div id="main-container" class="w-full max-w-2xl mx-auto p-4">
<header class="text-center mb-8"> <header class="text-center mb-8">
<a href="/" class="inline-block mb-4"> <a href="/" class="inline-block mb-4">
<img src="/static/images/stormycloud.svg" <img src="/static/images/stormycloud.svg" alt="StormyCloud Logo" style="width:350px; max-width:100%;" class="mx-auto"/>
alt="StormyCloud Logo" </a>
style="width:550px;max-width:100%;"
class="mx-auto"/>
</a>
<h1 class="text-4xl font-bold text-white">I2P Secure Share</h1> <h1 class="text-4xl font-bold text-white">I2P Secure Share</h1>
<p class="text-gray-400">Anonymously share images and text pastes.</p> <p class="text-gray-400">Anonymously share images and text pastes.</p>
</header> </header>
@@ -140,9 +138,9 @@
</div> </div>
<div class="noscript-forms-container"> <div class="noscript-forms-container">
<!-- IMAGE FORM -->
<div id="image-form" class="content-container rounded-lg p-8 shadow-lg tab-content"> <div id="image-form" class="content-container rounded-lg p-8 shadow-lg tab-content">
<form action="/upload/image" method="POST" enctype="multipart/form-data"> <form action="/upload/image" method="POST" enctype="multipart/form-data">
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}"/>
<h2 class="text-2xl font-semibold mb-6 text-white">Upload an Image</h2> <h2 class="text-2xl font-semibold mb-6 text-white">Upload an Image</h2>
<div class="mb-6"> <div class="mb-6">
@@ -196,9 +194,9 @@
</form> </form>
</div> </div>
<!-- PASTE FORM -->
<div id="paste-form" class="content-container rounded-lg p-8 shadow-lg hidden tab-content"> <div id="paste-form" class="content-container rounded-lg p-8 shadow-lg hidden tab-content">
<form action="/upload/paste" method="POST"> <form action="/upload/paste" method="POST">
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}"/>
<h2 class="text-2xl font-semibold mb-6 text-white">Create a Paste</h2> <h2 class="text-2xl font-semibold mb-6 text-white">Create a Paste</h2>
<div class="mb-6"> <div class="mb-6">
<label for="paste-content" class="block text-gray-300 text-sm font-bold mb-2">Paste Content:</label> <label for="paste-content" class="block text-gray-300 text-sm font-bold mb-2">Paste Content:</label>
@@ -250,7 +248,6 @@
</form> </form>
</div> </div>
<!-- API DOCS -->
<div id="api-docs" class="content-container docs-container rounded-lg p-8 shadow-lg hidden tab-content"> <div id="api-docs" class="content-container docs-container rounded-lg p-8 shadow-lg hidden tab-content">
<h3>Introduction</h3> <h3>Introduction</h3>
<p>The API allows programmatic uploads. All endpoints are rate-limited. No API key is required.</p> <p>The API allows programmatic uploads. All endpoints are rate-limited. No API key is required.</p>
@@ -281,7 +278,6 @@
http://{{ request.host }}/api/upload/paste</pre> http://{{ request.host }}/api/upload/paste</pre>
</div> </div>
<!-- STATS -->
<div id="stats-content" class="content-container rounded-lg p-8 shadow-lg hidden tab-content"> <div id="stats-content" class="content-container rounded-lg p-8 shadow-lg hidden tab-content">
<h2 class="text-3xl font-bold text-white text-center mb-8">Service Statistics</h2> <h2 class="text-3xl font-bold text-white text-center mb-8">Service Statistics</h2>
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6"> <div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
@@ -300,7 +296,6 @@ http://{{ request.host }}/api/upload/paste</pre>
</div> </div>
</div> </div>
<!-- TERMS OF SERVICE -->
<div id="tos-content" class="content-container tos-container rounded-lg p-8 shadow-lg hidden tab-content"> <div id="tos-content" class="content-container tos-container rounded-lg p-8 shadow-lg hidden tab-content">
<h2 class="text-3xl font-bold text-white mb-4">Terms of Service</h2> <h2 class="text-3xl font-bold text-white mb-4">Terms of Service</h2>
<p><strong>Last Updated: June 20, 2025</strong></p> <p><strong>Last Updated: June 20, 2025</strong></p>
@@ -319,14 +314,12 @@ http://{{ request.host }}/api/upload/paste</pre>
</div> </div>
</div> </div>
<!-- Version Info (Moved outside the tabbed content) -->
<div class="text-center text-xs text-gray-500 mt-6 mb-8"> <div class="text-center text-xs text-gray-500 mt-6 mb-8">
<a href="https://github.com/your-username/your-repo-name" target="_blank" rel="noopener noreferrer" class="hover:text-gray-400 transition-colors"> <a href="http://git.idk.i2p/stormycloud/drop.i2p/releases/tag/v1.1" target="_blank" rel="noopener noreferrer" class="hover:text-gray-400 transition-colors">
Version 1.1 Version 1.1
</a> </a>
</div> </div>
<!-- FEATURES -->
<section class="text-center features-section"> <section class="text-center features-section">
<h2 class="text-3xl font-bold text-white mb-8">Features</h2> <h2 class="text-3xl font-bold text-white mb-8">Features</h2>
<div class="grid grid-cols-1 md:grid-cols-3 gap-8"> <div class="grid grid-cols-1 md:grid-cols-3 gap-8">

View File

@@ -3,120 +3,62 @@
<head> <head>
<meta charset="UTF-8"/> <meta charset="UTF-8"/>
<meta name="viewport" content="width=device-width, initial-scale=1.0"/> <meta name="viewport" content="width=device-width, initial-scale=1.0"/>
<title>I2P Secure Share</title> <title>View Image - I2P Secure Share</title>
<link rel="stylesheet" href="{{ url_for('static', filename='css/tailwind.css') }}"/> <link rel="stylesheet" href="/static/css/tailwind.css"/>
<link rel="icon" href="{{ url_for('static', filename='favicon.ico') }}" type="image/x-icon"> <link rel="icon" href="{{ url_for('static', filename='favicon.ico') }}" type="image/x-icon">
<style> <style>
body { background-color: #1a202c; color: #cbd5e0; } body { background-color: #1a202c; color: #cbd5e0; }
.content-container { background-color: #2d3748; border:1px solid #4a5568; border-radius:0.5rem; } .content-container { background-color: #2d3748; border: 1px solid #4a5568; }
.tab { border-bottom:2px solid transparent; cursor:pointer; } .link-box { background-color: #2d3748; border:1px solid #4a5568; word-break:break-all; }
.tab.active { border-bottom-color:#63b3ed; color:#ffffff; }
.btn { background-color:#4299e1; transition:background-color .3s ease; } .btn { background-color:#4299e1; transition:background-color .3s ease; }
.btn:hover { background-color:#3182ce; } .btn:hover { background-color:#3182ce; }
select,textarea,input[type="text"],input[type="password"],input[type="number"] { .thumbnail-container { border:2px dashed #4a5568; max-width:100%; }
background-color:#4a5568; border:1px solid #718096; color:#cbd5e0; .thumbnail { max-width:100%; max-height:60vh; object-fit:contain; }
} .announcement-bar { background-color:#2563eb; border-bottom:1px solid #1e3a8a; }
.alert-success { background-color:#38a169; } .alert-success { background-color:#38a169; }
.alert-error { background-color:#e53e3e; } .alert-error { background-color:#e53e3e; }
.announcement-bar { background-color:#2563eb; border-bottom:1px solid #1e3a8a; }
.reveal { display:none; }
input[type="file"] {
width:100%; padding:0.5rem 1rem; border-radius:0.375rem;
background-color:#4a5568; border:1px solid #718096; color:#cbd5e0; cursor:pointer;
}
input[type="file"]::file-selector-button {
background-color:#2d3748; color:#cbd5e0; border:none;
padding:0.5rem 1rem; margin-right:1rem; border-radius:0.375rem; cursor:pointer;
transition:background-color .3s ease;
}
input[type="file"]::file-selector-button:hover { background-color:#3a4a5a; }
.stat-card {
background-color:#2d3748; border:1px solid #4a5568; border-radius:0.5rem;
}
.stat-value { color:#63b3ed; }
.label-with-icon {
display:inline-flex; align-items:center; gap:0.5rem; font-size:0.875rem; color:#cbd5e0;
}
.label-with-icon svg {
width:1rem; height:1rem; color:#4299e1; flex-shrink:0;
}
.feature-card {
background-color: #2d3748;
border: 1px solid #4a5568;
border-radius: 0.5rem;
}
/* API Docs Styling */
.docs-container h3 {
font-size: 1.5rem;
font-weight: 600;
color: #ffffff;
margin-top: 2rem;
margin-bottom: 1rem;
padding-bottom: 0.5rem;
border-bottom: 1px solid #4a5568;
}
.docs-container p { margin-bottom: 1rem; color: #a0aec0; }
.docs-container ul { list-style-position: inside; margin-bottom: 1rem; }
.docs-container li { margin-bottom: 0.5rem; color: #cbd5e0;}
.docs-container code {
background-color:#1a202c;
color:#f7fafc;
padding:0.2rem 0.4rem;
border-radius:0.25rem;
font-family:monospace;
font-size: 0.875rem;
}
.docs-container pre {
background-color:#1a202c;
padding:1rem;
border-radius:0.5rem;
overflow-x:auto;
color:#f7fafc;
font-family:monospace;
}
</style> </style>
<noscript>
<style>
.tab-nav-container{display:none;}
.tab-content{display:none!important;}
#image-form,#paste-form{display:block!important;margin-bottom:2rem;}
#api-docs,#stats-content,#tos-content,.features-section{display:none!important;}
@media(min-width:1024px){.noscript-forms-container{display:flex;gap:1.5rem;} .noscript-forms-container>div{flex:1;}}
.reveal{display:block!important;}
</style>
</noscript>
</head> </head>
<body class="font-sans"> <body class="font-sans">
{% if announcement_enabled and announcement_message %} {% if announcement_enabled and announcement_message %}
<div id="announcement-bar" class="announcement-bar text-white text-center p-2 relative shadow-lg"> <div id="announcement-bar" class="announcement-bar text-white text-center p-2 relative shadow-lg">
<span>{{ announcement_message }}</span> <span>{{ announcement_message }}</span>
<button id="close-announcement" <button id="close-announcement" class="absolute top-0 right-0 mt-2 mr-4 text-white hover:text-gray-200 text-2xl leading-none">&times;</button>
class="absolute top-0 right-0 mt-2 mr-4 text-white hover:text-gray-200 text-2xl leading-none">&times;</button>
</div> </div>
{% endif %} {% endif %}
<div class="flex items-center justify-center min-h-screen py-8"> <div class="flex items-center justify-center min-h-screen py-8">
<div id="main-container" class="w-full max-w-2xl mx-auto p-4"> <div class="w-full max-w-2xl mx-auto p-4">
{% if password_required %}
<div class="content-container rounded-lg p-8 shadow-lg">
<h2 class="text-2xl font-semibold text-white mb-6">Enter Password to View Image</h2>
<form method="POST">
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}"/>
<div class="mb-6">
<label for="password" class="block text-gray-300 text-sm font-bold mb-2">Password:</label>
<input type="password" name="password" id="password"
class="w-full p-2 rounded-md text-white bg-gray-700 border border-gray-600 focus:outline-none focus:ring-2 focus:ring-blue-500"
required>
</div>
<button type="submit" class="btn w-full text-white font-bold py-3 px-5 rounded-md">Unlock Image</button>
</form>
</div>
{% else %}
<header class="text-center mb-8"> <header class="text-center mb-8">
<a href="/" class="inline-block mb-4"> <a href="/" class="inline-block mb-4">
<img src="/static/images/stormycloud.svg" <img src="/static/images/stormycloud.svg" alt="StormyCloud Logo" style="width:350px; max-width:100%;" class="mx-auto"/>
alt="StormyCloud Logo" </a>
style="width:550px;max-width:100%;" <h1 class="text-3xl font-bold text-white">View Image</h1>
class="mx-auto"/> <p class="text-gray-400 mt-2 text-xl">Expires in: {{ time_left }}</p>
</a>
<h1 class="text-4xl font-bold text-white">I2P Secure Share</h1>
<p class="text-gray-400">Anonymously share images and text pastes.</p>
</header> </header>
<main> <main>
<div id="js-error-container"
class="hidden alert-error text-white p-3 rounded-md shadow-lg mb-4"
role="alert"></div>
{% with messages = get_flashed_messages(with_categories=true) %} {% with messages = get_flashed_messages(with_categories=true) %}
{% if messages %} {% if messages %}
<div class="mb-4"> <div class="mb-4">
{% for category,message in messages %} {% for category, message in messages %}
<div class="alert-{{category}} text-white p-3 rounded-md shadow-lg" <div class="alert-{{category}} text-white p-3 rounded-md shadow-lg"
role="alert">{{ message }}</div> role="alert">{{ message }}</div>
{% endfor %} {% endfor %}
@@ -124,295 +66,56 @@
{% endif %} {% endif %}
{% endwith %} {% endwith %}
<div class="mb-4 border-b border-gray-700 tab-nav-container"> <div class="thumbnail-container rounded-lg p-4 mb-6 flex justify-center items-center">
<nav class="flex -mb-px" id="tab-nav"> <img src="/uploads/{{ filename }}"
<a href="#image" alt="Uploaded Image" class="thumbnail rounded-md"/>
class="tab active text-gray-300 py-4 px-6 block hover:text-white">Image Uploader</a>
<a href="#paste"
class="tab text-gray-300 py-4 px-6 block hover:text-white">Pastebin</a>
<a href="#api"
class="tab text-gray-300 py-4 px-6 block hover:text-white">API</a>
<a href="#stats"
class="tab text-gray-300 py-4 px-6 block hover:text-white">Stats</a>
<a href="#tos"
class="tab text-gray-300 py-4 px-6 block hover:text-white">Terms</a>
</nav>
</div> </div>
<div class="noscript-forms-container"> <div class="link-box rounded-lg p-4 mb-4">
<!-- IMAGE FORM --> <label for="share-link" class="block text-gray-300 text-sm font-bold mb-2">Direct Image Link:</label>
<div id="image-form" class="content-container rounded-lg p-8 shadow-lg tab-content"> <div class="flex items-center space-x-2">
<form action="/upload/image" method="POST" enctype="multipart/form-data"> <input type="text" id="share-link"
<h2 class="text-2xl font-semibold mb-6 text-white">Upload an Image</h2> class="bg-gray-700 text-white w-full p-2 border border-gray-600 rounded-md"
value="{{ request.host_url }}uploads/{{ filename }}" readonly>
<div class="mb-6"> <button id="copy-button"
<label for="image-file" class="block text-gray-300 text-sm font-bold mb-2">Image File:</label> class="btn whitespace-nowrap flex-shrink-0 text-white font-bold py-3 px-5 rounded-md">
<input type="file" name="file" id="image-file" required> Copy
<p class="text-xs text-gray-500 mt-1">Max 10MB; WebP conversion.</p> </button>
</div>
<div class="mb-6">
<label for="image-expiry" class="block text-gray-300 text-sm font-bold mb-2">Delete after:</label>
<select name="expiry" id="image-expiry" class="w-full p-2 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500">
<option value="15m">15 minutes</option>
<option value="1h" selected>1 hour</option>
<option value="2h">2 hours</option>
<option value="4h">4 hours</option>
<option value="8h">8 hours</option>
<option value="12h">12 hours</option>
<option value="24h">24 hours</option>
<option value="48h">48 hours</option>
</select>
</div>
<div class="mb-6 text-gray-300" style="display:grid;grid-template-columns:repeat(3,1fr);gap:1.5rem;align-items:start;">
<label class="label-with-icon">
<input type="checkbox" name="keep_exif">
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke="currentColor"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 12h6m-6 4h6M7 7h10M4 6h16M4 6a2 2 0 012-2h8l2 2h6a2 2 0 012 2v12a2 2 0 01-2 2H6a2 2 0 01-2-2V6z"/></svg>
<span>Keep EXIF Data</span>
</label>
<label class="label-with-icon">
<input type="checkbox" id="image-pw-protect" name="password_protect">
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke="currentColor"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 11c1.657 0 3-1.343 3-3V6a3 3 0 10-6 0v2c0 1.657 1.343 3 3 3z"/><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M5 11h14a2 2 0 012 2v8a2 2 0 01-2 2H5a2 2 0 01-2-2v-8a2 2 0 012-2z"/></svg>
<span>Password</span>
</label>
<label class="label-with-icon" title="Removed after this many successful views">
<input type="checkbox" id="image-views-protect" name="views_protect">
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke="currentColor"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 12a3 3 0 11-6 0 3 3 0 016 0z"/><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M2.458 12C3.732 7.943 7.523 5 12 5c4.477 0 8.268 2.943 9.542 7-1.274 4.057-5.065 7-9.542 7-4.477 0-8.268-2.943-9.542-7z"/></svg>
<span>Max Views</span>
</label>
</div>
<div id="image-pw-options" class="reveal mb-6">
<label for="image-password" class="block text-gray-300 text-sm font-bold mb-1">Password:</label>
<input type="password" name="password" id="image-password" class="w-full p-2 rounded-md border-gray-600 focus:outline-none focus:ring-2 focus:ring-blue-500">
</div>
<div id="image-views-options" class="reveal mb-6">
<label for="image-max-views" class="block text-gray-300 text-sm font-bold mb-1">Max views:</label>
<input type="number" name="max_views" id="image-max-views" min="1" class="w-full p-2 rounded-md border-gray-600 focus:outline-none focus:ring-2 focus:ring-blue-500">
</div>
<button type="submit" class="btn w-full text-white font-bold py-3 px-5 rounded-md focus:shadow-outline">Upload Image</button>
</form>
</div>
<!-- PASTE FORM -->
<div id="paste-form" class="content-container rounded-lg p-8 shadow-lg hidden tab-content">
<form action="/upload/paste" method="POST">
<h2 class="text-2xl font-semibold mb-6 text-white">Create a Paste</h2>
<div class="mb-6">
<label for="paste-content" class="block text-gray-300 text-sm font-bold mb-2">Paste Content:</label>
<textarea name="content" id="paste-content" rows="10" class="w-full p-2 rounded-md font-mono focus:outline-none focus:ring-2 focus:ring-blue-500" required></textarea>
</div>
<div class="mb-6">
<label for="paste-language" class="block text-gray-300 text-sm font-bold mb-2">Language:</label>
<select name="language" id="paste-language" class="w-full p-2 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500">
<option value="text">Plain Text</option>
{% for lang in languages %}
<option value="{{ lang }}">{{ lang|capitalize }}</option>
{% endfor %}
</select>
</div>
<div class="mb-6">
<label for="paste-expiry" class="block text-gray-300 text-sm font-bold mb-2">Delete after:</label>
<select name="expiry" id="paste-expiry" class="w-full p-2 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500">
<option value="15m">15 minutes</option>
<option value="1h" selected>1 hour</option>
<option value="2h">2 hours</option>
<option value="4h">4 hours</option>
<option value="8h">8 hours</option>
<option value="12h">12 hours</option>
<option value="24h">24 hours</option>
<option value="48h">48 hours</option>
</select>
</div>
<div class="mb-6 text-gray-300" style="display:grid;grid-template-columns:repeat(2,1fr);gap:2rem;align-items:start;">
<label class="label-with-icon">
<input type="checkbox" id="paste-pw-protect" name="password_protect">
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke="currentColor"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 11c1.657 0 3-1.343 3-3V6a3 3 0 10-6 0v2c0 1.657 1.343 3 3 3z"/><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M5 11h14a2 2 0 012 2v8a2 2 0 01-2 2H5a2 2 0 01-2-2v-8a2 2 0 012-2z"/></svg>
<span>Password</span>
</label>
<label class="label-with-icon" title="Removed after this many successful views">
<input type="checkbox" id="paste-views-protect" name="views_protect">
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke="currentColor"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 12a3 3 0 11-6 0 3 3 0 016 0z"/><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M2.458 12C3.732 7.943 7.523 5 12 5c4.477 0 8.268 2.943 9.542 7-1.274 4.057-5.065 7-9.542 7-4.477 0-8.268-2.943-9.542-7z"/></svg>
<span>Max Views</span>
</label>
</div>
<div id="paste-pw-options" class="reveal mb-6">
<label for="paste-password" class="block text-gray-300 text-sm font-bold mb-1">Password:</label>
<input type="password" name="password" id="paste-password" class="w-full p-2 rounded-md border-gray-600 focus:outline-none focus:ring-2 focus:ring-blue-500">
</div>
<div id="paste-views-options" class="reveal mb-6">
<label for="paste-max-views" class="block text-gray-300 text-sm font-bold mb-1">Max views:</label>
<input type="number" name="max_views" id="paste-max-views" min="1" class="w-full p-2 rounded-md border-gray-600 focus:outline-none focus:ring-2 focus:ring-blue-500">
</div>
<button type="submit" class="btn w-full text-white font-bold py-3 px-5 rounded-md focus:shadow-outline">Create Paste</button>
</form>
</div>
<!-- API DOCS -->
<div id="api-docs" class="content-container docs-container rounded-lg p-8 shadow-lg hidden tab-content">
<h3>Introduction</h3>
<p>The API allows programmatic uploads. All endpoints are rate-limited. No API key is required.</p>
<h3>Uploading an Image</h3>
<p>Send a <code>POST</code> request with <code>multipart/form-data</code>.</p>
<ul>
<li><strong>Endpoint:</strong> <code>POST /api/upload/image</code></li>
<li><strong>Parameter <code>file</code>:</strong> (Required) The image file.</li>
<li><strong>Parameter <code>expiry</code>:</strong> (Optional) Values: <code>15m</code>, <code>1h</code>, <code>2h</code>, <code>4h</code>, <code>8h</code>, <code>12h</code>, <code>24h</code>, <code>48h</code>.</li>
<li><strong>Parameter <code>password</code>:</strong> (Optional) A password to protect the content.</li>
<li><strong>Parameter <code>max_views</code>:</strong> (Optional) An integer for auto-deletion after N views.</li>
</ul>
<pre>curl -X POST -F "file=@/path/to/image.jpg" http://{{ request.host }}/api/upload/image</pre>
<h3>Creating a Paste</h3>
<p>Send a <code>POST</code> request with a JSON payload.</p>
<ul>
<li><strong>Endpoint:</strong> <code>POST /api/upload/paste</code></li>
<li><strong>JSON Field <code>content</code>:</strong> (Required) The paste text.</li>
<li><strong>JSON Field <code>language</code>:</strong> (Optional) A valid language for syntax highlighting. Defaults to 'text'.</li>
<li><strong>JSON Field <code>expiry</code>:</strong> (Optional) Same values as image expiry.</li>
<li><strong>JSON Field <code>password</code>:</strong> (Optional) A password to protect the content.</li>
<li><strong>JSON Field <code>max_views</code>:</strong> (Optional) An integer for auto-deletion after N views.</li>
</ul>
<pre>curl -X POST -H "Content-Type: application/json" \
-d '{"content":"Hello World", "expiry":"1h"}' \
http://{{ request.host }}/api/upload/paste</pre>
</div>
<!-- STATS -->
<div id="stats-content" class="content-container rounded-lg p-8 shadow-lg hidden tab-content">
<h2 class="text-3xl font-bold text-white text-center mb-8">Service Statistics</h2>
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
<div class="stat-card p-6 text-center">
<h3 class="text-xl font-semibold text-white mb-2">Total Image Uploads</h3>
<p class="text-5xl font-bold stat-value">{{ stats.total_images }}</p>
</div>
<div class="stat-card p-6 text-center">
<h3 class="text-xl font-semibold text-white mb-2">Total Paste Uploads</h3>
<p class="text-5xl font-bold stat-value">{{ stats.total_pastes }}</p>
</div>
<div class="stat-card p-6 text-center">
<h3 class="text-xl font-semibold text-white mb-2">Total API Uploads</h3>
<p class="text-5xl font-bold stat-value">{{ stats.total_api_uploads }}</p>
</div>
</div> </div>
</div> </div>
<!-- TERMS OF SERVICE --> <div class="text-center mt-8">
<div id="tos-content" class="content-container tos-container rounded-lg p-8 shadow-lg hidden tab-content"> <a href="/" class="text-blue-400 hover:text-blue-300">Upload another file</a>
<h2 class="text-3xl font-bold text-white mb-4">Terms of Service</h2>
<p><strong>Last Updated: June 20, 2025</strong></p>
<p>By using this service you agree to these terms. The service is provided “as-is” with a focus on privacy.</p>
<h3 class="mt-6 text-white text-xl font-semibold mb-2">1. Privacy & Data</h3>
<ul class="list-disc list-inside text-gray-300 mb-4">
<li>No logs of your identity or IP.</li>
<li>All uploads are encrypted at rest.</li>
<li>Data auto-deletes after expiry or view limit.</li>
<li>EXIF metadata only kept if opted-in.</li>
</ul>
<h3 class="mt-6 text-white text-xl font-semibold mb-2">2. Acceptable Use</h3>
<p class="text-gray-300 mb-4">Do not upload illegal or harmful content. We reserve the right to remove content that violates these terms.</p>
<h3 class="mt-6 text-white text-xl font-semibold mb-2">3. Liability</h3>
<p class="text-gray-300">This free service comes with no warranties. We are not responsible for data loss.</p>
</div> </div>
<div class="text-center mt-4 text-sm">
<p class="text-gray-500">Find this service useful? <a href="/donate" class="text-blue-400 hover:underline">Consider supporting its future.</a></p>
</div> </div>
<!-- Version Info (Moved outside the tabbed content) --> </main>
<div class="text-center text-xs text-gray-500 mt-6 mb-8"> {% endif %}
<a href="https://github.com/your-username/your-repo-name" target="_blank" rel="noopener noreferrer" class="hover:text-gray-400 transition-colors">
Version 1.1
</a>
</div>
<!-- FEATURES -->
<section class="text-center features-section">
<h2 class="text-3xl font-bold text-white mb-8">Features</h2>
<div class="grid grid-cols-1 md:grid-cols-3 gap-8">
<div class="feature-card p-6 rounded-lg">
<div class="flex items-center justify-center h-12 w-12 rounded-md bg-blue-500 text-white mx-auto mb-4">
<svg xmlns="http://www.w3.org/2000/svg" class="h-6 w-6" fill="none" viewBox="0 0 24 24" stroke="currentColor"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 15v2m-6 4h12a2 2 0 002-2v-6a2 2 0 00-2-2H6a2 2 0 00-2 2v6a2 2 0 002 2zm10-10V7a4 4 0 00-8 0v4h8z" /></svg>
</div>
<h3 class="text-xl font-semibold text-white mb-2">Encrypted at Rest</h3>
<p class="text-gray-400">All uploaded files and pastes are fully encrypted on the server, ensuring your data is protected.</p>
</div>
<div class="feature-card p-6 rounded-lg">
<div class="flex items-center justify-center h-12 w-12 rounded-md bg-blue-500 text-white mx-auto mb-4">
<svg xmlns="http://www.w3.org/2000/svg" class="h-6 w-6" fill="none" viewBox="0 0 24 24" stroke="currentColor"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 11c0 3.517-1.009 6.786-2.673 9.356 m-2.673-9.356C6.673 8.214 9.327 5.5 12 5.5 c2.673 0 5.327 2.714 5.327 5.5s-1.009 6.786-2.673 9.356m-2.673-9.356h0z" /></svg>
</div>
<h3 class="text-xl font-semibold text-white mb-2">Anonymous by Design</h3>
<p class="text-gray-400">Image metadata (EXIF) is stripped and no unnecessary logs are kept. Built for the I2P network.</p>
</div>
<div class="feature-card p-6 rounded-lg">
<div class="flex items-center justify-center h-12 w-12 rounded-md bg-blue-500 text-white mx-auto mb-4">
<svg xmlns="http://www.w3.org/2000/svg" class="h-6 w-6" fill="none" viewBox="0 0 24 24" stroke="currentColor"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M3 15a4 4 0 004 4h9a5 5 0 10-.1-9.999 5.002 5.002 0 10-9.78 2.096A4.001 4.001 0 003 15z" /></svg>
</div>
<h3 class="text-xl font-semibold text-white mb-2">StormyCloud Infrastructure</h3>
<p class="text-gray-400">A fast, reliable, and secure platform dedicated to the privacy of the I2P community.</p>
</div>
</div>
</section>
<footer class="text-center text-gray-500 mt-16 border-t border-gray-700 pt-8 pb-8"> <footer class="text-center text-gray-500 mt-16 border-t border-gray-700 pt-8 pb-8">
<a href="http://stormycloud.i2p" class="hover:text-gray-400">StormyCloud</a> <a href="http://stormycloud.i2p" class="hover:text-gray-400">StormyCloud</a>
<span class="mx-2">|</span> <span class="mx-2">|</span>
<a href="/donate" class="hover:text-gray-400">Donate</a> <a href="/donate" class="hover:text-gray-400">Donate</a>
</footer> </footer>
</main>
</div> </div>
</div> </div>
<script> <script>
document.addEventListener('DOMContentLoaded', () => { const copyBtn = document.getElementById('copy-button');
const contentDivs = { if (copyBtn) {
'#image': document.querySelector('#image-form'), copyBtn.addEventListener('click', () => {
'#paste': document.querySelector('#paste-form'), const inp = document.getElementById('share-link');
'#api': document.querySelector('#api-docs'), inp.select(); document.execCommand('copy');
'#stats': document.querySelector('#stats-content'), copyBtn.textContent = 'Copied!';
'#tos': document.querySelector('#tos-content') setTimeout(() => copyBtn.textContent = 'Copy', 2000);
};
const tabs = document.querySelectorAll('#tab-nav a');
function showTab(hash) {
if (!hash || !contentDivs[hash]) hash = '#image';
Object.values(contentDivs).forEach(d => {
if(d) d.classList.add('hidden');
}); });
tabs.forEach(t => t.classList.remove('active'));
if(contentDivs[hash]) {
contentDivs[hash].classList.remove('hidden');
} }
const activeTab = document.querySelector(`#tab-nav a[href="${hash}"]`);
if(activeTab) {
activeTab.classList.add('active');
}
}
tabs.forEach(tab => tab.addEventListener('click', e => {
e.preventDefault();
const h = e.target.hash;
window.history.replaceState(null, null, ' ' + h);
showTab(h);
}));
showTab(window.location.hash);
function toggle(cbId, tgtId) {
const cb = document.getElementById(cbId), tgt = document.getElementById(tgtId);
if (!cb||!tgt) return;
cb.addEventListener('change', () => tgt.style.display = cb.checked ? 'block' : 'none');
tgt.style.display = cb.checked ? 'block' : 'none';
}
toggle('image-pw-protect','image-pw-options');
toggle('image-views-protect','image-views-options');
toggle('paste-pw-protect','paste-pw-options');
toggle('paste-views-protect','paste-views-options');
const closeBtn = document.getElementById('close-announcement'); const closeBtn = document.getElementById('close-announcement');
if (closeBtn) closeBtn.addEventListener('click', ()=> if (closeBtn) closeBtn.addEventListener('click', () =>
document.getElementById('announcement-bar').style.display = 'none' document.getElementById('announcement-bar').style.display = 'none'
); );
}); </script>
</script>
</body> </body>
</html> </html>

View File

@@ -3,120 +3,138 @@
<head> <head>
<meta charset="UTF-8"/> <meta charset="UTF-8"/>
<meta name="viewport" content="width=device-width, initial-scale=1.0"/> <meta name="viewport" content="width=device-width, initial-scale=1.0"/>
<title>I2P Secure Share</title> <title>View Paste - I2P Secure Share</title>
<link rel="stylesheet" href="{{ url_for('static', filename='css/tailwind.css') }}"/> <link rel="stylesheet" href="/static/css/tailwind.css"/>
<link rel="icon" href="{{ url_for('static', filename='favicon.ico') }}" type="image/x-icon"> <link rel="icon" href="{{ url_for('static', filename='favicon.ico') }}" type="image/x-icon">
<style> <style>
body { background-color: #1a202c; color: #cbd5e0; } body { background-color: #1a202c; color: #cbd5e0; }
.content-container { background-color: #2d3748; border:1px solid #4a5568; border-radius:0.5rem; } .link-box { background-color: #2d3748; border:1px solid #4a5568; word-break:break-all; }
.tab { border-bottom:2px solid transparent; cursor:pointer; }
.tab.active { border-bottom-color:#63b3ed; color:#ffffff; }
.btn { background-color:#4299e1; transition:background-color .3s ease; } .btn { background-color:#4299e1; transition:background-color .3s ease; }
.btn:hover { background-color:#3182ce; } .btn:hover { background-color:#3182ce; }
select,textarea,input[type="text"],input[type="password"],input[type="number"] { .code-container {
background-color:#4a5568; border:1px solid #718096; color:#cbd5e0; background-color:#2d3748; border-radius:0.5rem; border:1px solid #4a5568; overflow:hidden;
} }
.syntax pre {
margin:0; padding:1rem; white-space:pre-wrap; word-wrap:break-word;
}
.announcement-bar { background-color:#2563eb; border-bottom:1px solid #1e3a8a; }
.wide-container { max-width: 85rem; }
.alert-success { background-color:#38a169; } .alert-success { background-color:#38a169; }
.alert-error { background-color:#e53e3e; } .alert-error { background-color:#e53e3e; }
.announcement-bar { background-color:#2563eb; border-bottom:1px solid #1e3a8a; }
.reveal { display:none; } /* Pygments 'monokai' theme CSS */
input[type="file"] { .syntax .hll { background-color: #49483e }
width:100%; padding:0.5rem 1rem; border-radius:0.375rem; .syntax { background: #272822; color: #f8f8f2 }
background-color:#4a5568; border:1px solid #718096; color:#cbd5e0; cursor:pointer; .syntax .c { color: #75715e } /* Comment */
} .syntax .err { color: #960050; background-color: #1e0010 } /* Error */
input[type="file"]::file-selector-button { .syntax .k { color: #66d9ef } /* Keyword */
background-color:#2d3748; color:#cbd5e0; border:none; .syntax .l { color: #ae81ff } /* Literal */
padding:0.5rem 1rem; margin-right:1rem; border-radius:0.375rem; cursor:pointer; .syntax .n { color: #f8f8f2 } /* Name */
transition:background-color .3s ease; .syntax .o { color: #f92672 } /* Operator */
} .syntax .p { color: #f8f8f2 } /* Punctuation */
input[type="file"]::file-selector-button:hover { background-color:#3a4a5a; } .syntax .ch { color: #75715e } /* Comment.Hashbang */
.stat-card { .syntax .cm { color: #75715e } /* Comment.Multiline */
background-color:#2d3748; border:1px solid #4a5568; border-radius:0.5rem; .syntax .cp { color: #75715e } /* Comment.Preproc */
} .syntax .cpf { color: #75715e } /* Comment.PreprocFile */
.stat-value { color:#63b3ed; } .syntax .c1 { color: #75715e } /* Comment.Single */
.label-with-icon { .syntax .cs { color: #75715e } /* Comment.Special */
display:inline-flex; align-items:center; gap:0.5rem; font-size:0.875rem; color:#cbd5e0; .syntax .gd { color: #f92672 } /* Generic.Deleted */
} .syntax .ge { font-style: italic } /* Generic.Emph */
.label-with-icon svg { .syntax .gi { color: #a6e22e } /* Generic.Inserted */
width:1rem; height:1rem; color:#4299e1; flex-shrink:0; .syntax .gs { font-weight: bold } /* Generic.Strong */
} .syntax .gu { color: #75715e } /* Generic.Subheading */
.feature-card { .syntax .kc { color: #66d9ef } /* Keyword.Constant */
background-color: #2d3748; .syntax .kd { color: #66d9ef } /* Keyword.Declaration */
border: 1px solid #4a5568; .syntax .kn { color: #f92672 } /* Keyword.Namespace */
border-radius: 0.5rem; .syntax .kp { color: #66d9ef } /* Keyword.Pseudo */
} .syntax .kr { color: #66d9ef } /* Keyword.Reserved */
/* API Docs Styling */ .syntax .kt { color: #66d9ef } /* Keyword.Type */
.docs-container h3 { .syntax .ld { color: #e6db74 } /* Literal.Date */
font-size: 1.5rem; .syntax .m { color: #ae81ff } /* Literal.Number */
font-weight: 600; .syntax .s { color: #e6db74 } /* Literal.String */
color: #ffffff; .syntax .na { color: #a6e22e } /* Name.Attribute */
margin-top: 2rem; .syntax .nb { color: #f8f8f2 } /* Name.Builtin */
margin-bottom: 1rem; .syntax .nc { color: #a6e22e } /* Name.Class */
padding-bottom: 0.5rem; .syntax .no { color: #66d9ef } /* Name.Constant */
border-bottom: 1px solid #4a5568; .syntax .nd { color: #a6e22e } /* Name.Decorator */
} .syntax .ni { color: #f8f8f2 } /* Name.Entity */
.docs-container p { margin-bottom: 1rem; color: #a0aec0; } .syntax .ne { color: #a6e22e } /* Name.Exception */
.docs-container ul { list-style-position: inside; margin-bottom: 1rem; } .syntax .nf { color: #a6e22e } /* Name.Function */
.docs-container li { margin-bottom: 0.5rem; color: #cbd5e0;} .syntax .nl { color: #f8f8f2 } /* Name.Label */
.docs-container code { .syntax .nn { color: #f8f8f2 } /* Name.Namespace */
background-color:#1a202c; .syntax .nx { color: #a6e22e } /* Name.Other */
color:#f7fafc; .syntax .py { color: #f8f8f2 } /* Name.Property */
padding:0.2rem 0.4rem; .syntax .nt { color: #f92672 } /* Name.Tag */
border-radius:0.25rem; .syntax .nv { color: #f8f8f2 } /* Name.Variable */
font-family:monospace; .syntax .ow { color: #f92672 } /* Operator.Word */
font-size: 0.875rem; .syntax .w { color: #f8f8f2 } /* Text.Whitespace */
} .syntax .mb { color: #ae81ff } /* Literal.Number.Bin */
.docs-container pre { .syntax .mf { color: #ae81ff } /* Literal.Number.Float */
background-color:#1a202c; .syntax .mh { color: #ae81ff } /* Literal.Number.Hex */
padding:1rem; .syntax .mi { color: #ae81ff } /* Literal.Number.Integer */
border-radius:0.5rem; .syntax .mo { color: #ae81ff } /* Literal.Number.Oct */
overflow-x:auto; .syntax .sa { color: #e6db74 } /* Literal.String.Affix */
color:#f7fafc; .syntax .sb { color: #e6db74 } /* Literal.String.Backtick */
font-family:monospace; .syntax .sc { color: #e6db74 } /* Literal.String.Char */
} .syntax .dl { color: #e6db74 } /* Literal.String.Delimiter */
.syntax .sd { color: #e6db74 } /* Literal.String.Doc */
.syntax .s2 { color: #e6db74 } /* Literal.String.Double */
.syntax .se { color: #ae81ff } /* Literal.String.Escape */
.syntax .sh { color: #e6db74 } /* Literal.String.Heredoc */
.syntax .si { color: #e6db74 } /* Literal.String.Interpol */
.syntax .sx { color: #e6db74 } /* Literal.String.Other */
.syntax .sr { color: #e6db74 } /* Literal.String.Regex */
.syntax .s1 { color: #e6db74 } /* Literal.String.Single */
.syntax .ss { color: #e6db74 } /* Literal.String.Symbol */
.syntax .bp { color: #f8f8f2 } /* Name.Builtin.Pseudo */
.syntax .fm { color: #a6e22e } /* Name.Function.Magic */
.syntax .vc { color: #f8f8f2 } /* Name.Variable.Class */
.syntax .vg { color: #f8f8f2 } /* Name.Variable.Global */
.syntax .vi { color: #f8f8f2 } /* Name.Variable.Instance */
.syntax .vm { color: #f8f8f2 } /* Name.Variable.Magic */
.syntax .il { color: #ae81ff } /* Literal.Number.Integer.Long */
</style> </style>
<noscript>
<style>
.tab-nav-container{display:none;}
.tab-content{display:none!important;}
#image-form,#paste-form{display:block!important;margin-bottom:2rem;}
#api-docs,#stats-content,#tos-content,.features-section{display:none!important;}
@media(min-width:1024px){.noscript-forms-container{display:flex;gap:1.5rem;} .noscript-forms-container>div{flex:1;}}
.reveal{display:block!important;}
</style>
</noscript>
</head> </head>
<body class="font-sans"> <body class="font-sans">
{% if announcement_enabled and announcement_message %} {% if announcement_enabled and announcement_message %}
<div id="announcement-bar" class="announcement-bar text-white text-center p-2 relative shadow-lg"> <div id="announcement-bar" class="announcement-bar text-white text-center p-2 relative shadow-lg">
<span>{{ announcement_message }}</span> <span>{{ announcement_message }}</span>
<button id="close-announcement" <button id="close-announcement" class="absolute top-0 right-0 mt-2 mr-4 text-white hover:text-gray-200 text-2xl leading-none">&times;</button>
class="absolute top-0 right-0 mt-2 mr-4 text-white hover:text-gray-200 text-2xl leading-none">&times;</button>
</div> </div>
{% endif %} {% endif %}
<div class="flex items-center justify-center min-h-screen py-8"> <div class="flex items-center justify-center min-h-screen py-8">
<div id="main-container" class="w-full max-w-2xl mx-auto p-4"> <div class="w-full wide-container mx-auto p-6">
{% if password_required %}
<div class="content-container rounded-lg p-10 shadow-lg"></div>
<h2 class="text-2xl font-semibold text-white mb-6">Enter Password to View Paste</h2>
<form method="POST">
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}"/>
<div class="mb-6">
<label for="password" class="block text-gray-300 text-sm font-bold mb-2">Password:</label>
<input type="password" name="password" id="password"
class="w-full p-2 rounded-md text-white bg-gray-700 border border-gray-600 focus:outline-none focus:ring-2 focus:ring-blue-500"
required>
</div>
<button type="submit" class="btn w-full text-white font-bold py-3 px-5 rounded-md">Unlock Paste</button>
</form>
</div>
{% else %}
<header class="text-center mb-8"> <header class="text-center mb-8">
<a href="/" class="inline-block mb-4"> <a href="/" class="inline-block mb-4">
<img src="/static/images/stormycloud.svg" <img src="/static/images/stormycloud.svg" alt="StormyCloud Logo" style="width:350px; max-width:100%;" class="mx-auto"/>
alt="StormyCloud Logo" </a>
style="width:550px;max-width:100%;" <h1 class="text-3xl font-bold text-white">View Paste</h1>
class="mx-auto"/> <p class="text-gray-400 mt-2 text-xl">Expires in: {{ time_left }}</p>
</a>
<h1 class="text-4xl font-bold text-white">I2P Secure Share</h1>
<p class="text-gray-400">Anonymously share images and text pastes.</p>
</header> </header>
<main> <main>
<div id="js-error-container"
class="hidden alert-error text-white p-3 rounded-md shadow-lg mb-4"
role="alert"></div>
{% with messages = get_flashed_messages(with_categories=true) %} {% with messages = get_flashed_messages(with_categories=true) %}
{% if messages %} {% if messages %}
<div class="mb-4"> <div class="mb-4">
{% for category,message in messages %} {% for category, message in messages %}
<div class="alert-{{category}} text-white p-3 rounded-md shadow-lg" <div class="alert-{{category}} text-white p-3 rounded-md shadow-lg"
role="alert">{{ message }}</div> role="alert">{{ message }}</div>
{% endfor %} {% endfor %}
@@ -124,295 +142,74 @@
{% endif %} {% endif %}
{% endwith %} {% endwith %}
<div class="mb-4 border-b border-gray-700 tab-nav-container"> <div class="flex justify-end mb-2">
<nav class="flex -mb-px" id="tab-nav"> <form method="GET" action="" class="flex items-center space-x-2">
<a href="#image" <label for="language-switcher" class="text-sm text-gray-400">Syntax:</label>
class="tab active text-gray-300 py-4 px-6 block hover:text-white">Image Uploader</a> <select name="lang" id="language-switcher" onchange="this.form.submit()" class="text-sm rounded-md p-1 bg-gray-700 border border-gray-600 focus:outline-none focus:ring-1 focus:ring-blue-500 cursor-pointer">
<a href="#paste"
class="tab text-gray-300 py-4 px-6 block hover:text-white">Pastebin</a>
<a href="#api"
class="tab text-gray-300 py-4 px-6 block hover:text-white">API</a>
<a href="#stats"
class="tab text-gray-300 py-4 px-6 block hover:text-white">Stats</a>
<a href="#tos"
class="tab text-gray-300 py-4 px-6 block hover:text-white">Terms</a>
</nav>
</div>
<div class="noscript-forms-container">
<!-- IMAGE FORM -->
<div id="image-form" class="content-container rounded-lg p-8 shadow-lg tab-content">
<form action="/upload/image" method="POST" enctype="multipart/form-data">
<h2 class="text-2xl font-semibold mb-6 text-white">Upload an Image</h2>
<div class="mb-6">
<label for="image-file" class="block text-gray-300 text-sm font-bold mb-2">Image File:</label>
<input type="file" name="file" id="image-file" required>
<p class="text-xs text-gray-500 mt-1">Max 10MB; WebP conversion.</p>
</div>
<div class="mb-6">
<label for="image-expiry" class="block text-gray-300 text-sm font-bold mb-2">Delete after:</label>
<select name="expiry" id="image-expiry" class="w-full p-2 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500">
<option value="15m">15 minutes</option>
<option value="1h" selected>1 hour</option>
<option value="2h">2 hours</option>
<option value="4h">4 hours</option>
<option value="8h">8 hours</option>
<option value="12h">12 hours</option>
<option value="24h">24 hours</option>
<option value="48h">48 hours</option>
</select>
</div>
<div class="mb-6 text-gray-300" style="display:grid;grid-template-columns:repeat(3,1fr);gap:1.5rem;align-items:start;">
<label class="label-with-icon">
<input type="checkbox" name="keep_exif">
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke="currentColor"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 12h6m-6 4h6M7 7h10M4 6h16M4 6a2 2 0 012-2h8l2 2h6a2 2 0 012 2v12a2 2 0 01-2 2H6a2 2 0 01-2-2V6z"/></svg>
<span>Keep EXIF Data</span>
</label>
<label class="label-with-icon">
<input type="checkbox" id="image-pw-protect" name="password_protect">
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke="currentColor"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 11c1.657 0 3-1.343 3-3V6a3 3 0 10-6 0v2c0 1.657 1.343 3 3 3z"/><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M5 11h14a2 2 0 012 2v8a2 2 0 01-2 2H5a2 2 0 01-2-2v-8a2 2 0 012-2z"/></svg>
<span>Password</span>
</label>
<label class="label-with-icon" title="Removed after this many successful views">
<input type="checkbox" id="image-views-protect" name="views_protect">
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke="currentColor"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 12a3 3 0 11-6 0 3 3 0 016 0z"/><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M2.458 12C3.732 7.943 7.523 5 12 5c4.477 0 8.268 2.943 9.542 7-1.274 4.057-5.065 7-9.542 7-4.477 0-8.268-2.943-9.542-7z"/></svg>
<span>Max Views</span>
</label>
</div>
<div id="image-pw-options" class="reveal mb-6">
<label for="image-password" class="block text-gray-300 text-sm font-bold mb-1">Password:</label>
<input type="password" name="password" id="image-password" class="w-full p-2 rounded-md border-gray-600 focus:outline-none focus:ring-2 focus:ring-blue-500">
</div>
<div id="image-views-options" class="reveal mb-6">
<label for="image-max-views" class="block text-gray-300 text-sm font-bold mb-1">Max views:</label>
<input type="number" name="max_views" id="image-max-views" min="1" class="w-full p-2 rounded-md border-gray-600 focus:outline-none focus:ring-2 focus:ring-blue-500">
</div>
<button type="submit" class="btn w-full text-white font-bold py-3 px-5 rounded-md focus:shadow-outline">Upload Image</button>
</form>
</div>
<!-- PASTE FORM -->
<div id="paste-form" class="content-container rounded-lg p-8 shadow-lg hidden tab-content">
<form action="/upload/paste" method="POST">
<h2 class="text-2xl font-semibold mb-6 text-white">Create a Paste</h2>
<div class="mb-6">
<label for="paste-content" class="block text-gray-300 text-sm font-bold mb-2">Paste Content:</label>
<textarea name="content" id="paste-content" rows="10" class="w-full p-2 rounded-md font-mono focus:outline-none focus:ring-2 focus:ring-blue-500" required></textarea>
</div>
<div class="mb-6">
<label for="paste-language" class="block text-gray-300 text-sm font-bold mb-2">Language:</label>
<select name="language" id="paste-language" class="w-full p-2 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500">
<option value="text">Plain Text</option>
{% for lang in languages %} {% for lang in languages %}
<option value="{{ lang }}">{{ lang|capitalize }}</option> <option value="{{ lang }}" {% if lang == selected_language %}selected{% endif %}>
{{ lang|capitalize }}
</option>
{% endfor %} {% endfor %}
</select> </select>
</div> <noscript><button type="submit" class="btn text-sm py-1 px-3">Update</button></noscript>
<div class="mb-6">
<label for="paste-expiry" class="block text-gray-300 text-sm font-bold mb-2">Delete after:</label>
<select name="expiry" id="paste-expiry" class="w-full p-2 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500">
<option value="15m">15 minutes</option>
<option value="1h" selected>1 hour</option>
<option value="2h">2 hours</option>
<option value="4h">4 hours</option>
<option value="8h">8 hours</option>
<option value="12h">12 hours</option>
<option value="24h">24 hours</option>
<option value="48h">48 hours</option>
</select>
</div>
<div class="mb-6 text-gray-300" style="display:grid;grid-template-columns:repeat(2,1fr);gap:2rem;align-items:start;">
<label class="label-with-icon">
<input type="checkbox" id="paste-pw-protect" name="password_protect">
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke="currentColor"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 11c1.657 0 3-1.343 3-3V6a3 3 0 10-6 0v2c0 1.657 1.343 3 3 3z"/><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M5 11h14a2 2 0 012 2v8a2 2 0 01-2 2H5a2 2 0 01-2-2v-8a2 2 0 012-2z"/></svg>
<span>Password</span>
</label>
<label class="label-with-icon" title="Removed after this many successful views">
<input type="checkbox" id="paste-views-protect" name="views_protect">
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke="currentColor"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 12a3 3 0 11-6 0 3 3 0 016 0z"/><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M2.458 12C3.732 7.943 7.523 5 12 5c4.477 0 8.268 2.943 9.542 7-1.274 4.057-5.065 7-9.542 7-4.477 0-8.268-2.943-9.542-7z"/></svg>
<span>Max Views</span>
</label>
</div>
<div id="paste-pw-options" class="reveal mb-6">
<label for="paste-password" class="block text-gray-300 text-sm font-bold mb-1">Password:</label>
<input type="password" name="password" id="paste-password" class="w-full p-2 rounded-md border-gray-600 focus:outline-none focus:ring-2 focus:ring-blue-500">
</div>
<div id="paste-views-options" class="reveal mb-6">
<label for="paste-max-views" class="block text-gray-300 text-sm font-bold mb-1">Max views:</label>
<input type="number" name="max_views" id="paste-max-views" min="1" class="w-full p-2 rounded-md border-gray-600 focus:outline-none focus:ring-2 focus:ring-blue-500">
</div>
<button type="submit" class="btn w-full text-white font-bold py-3 px-5 rounded-md focus:shadow-outline">Create Paste</button>
</form> </form>
</div> </div>
<!-- API DOCS --> <div class="code-container mb-6 syntax">
<div id="api-docs" class="content-container docs-container rounded-lg p-8 shadow-lg hidden tab-content"> {{ highlighted_content|safe }}
<h3>Introduction</h3>
<p>The API allows programmatic uploads. All endpoints are rate-limited. No API key is required.</p>
<h3>Uploading an Image</h3>
<p>Send a <code>POST</code> request with <code>multipart/form-data</code>.</p>
<ul>
<li><strong>Endpoint:</strong> <code>POST /api/upload/image</code></li>
<li><strong>Parameter <code>file</code>:</strong> (Required) The image file.</li>
<li><strong>Parameter <code>expiry</code>:</strong> (Optional) Values: <code>15m</code>, <code>1h</code>, <code>2h</code>, <code>4h</code>, <code>8h</code>, <code>12h</code>, <code>24h</code>, <code>48h</code>.</li>
<li><strong>Parameter <code>password</code>:</strong> (Optional) A password to protect the content.</li>
<li><strong>Parameter <code>max_views</code>:</strong> (Optional) An integer for auto-deletion after N views.</li>
</ul>
<pre>curl -X POST -F "file=@/path/to/image.jpg" http://{{ request.host }}/api/upload/image</pre>
<h3>Creating a Paste</h3>
<p>Send a <code>POST</code> request with a JSON payload.</p>
<ul>
<li><strong>Endpoint:</strong> <code>POST /api/upload/paste</code></li>
<li><strong>JSON Field <code>content</code>:</strong> (Required) The paste text.</li>
<li><strong>JSON Field <code>language</code>:</strong> (Optional) A valid language for syntax highlighting. Defaults to 'text'.</li>
<li><strong>JSON Field <code>expiry</code>:</strong> (Optional) Same values as image expiry.</li>
<li><strong>JSON Field <code>password</code>:</strong> (Optional) A password to protect the content.</li>
<li><strong>JSON Field <code>max_views</code>:</strong> (Optional) An integer for auto-deletion after N views.</li>
</ul>
<pre>curl -X POST -H "Content-Type: application/json" \
-d '{"content":"Hello World", "expiry":"1h"}' \
http://{{ request.host }}/api/upload/paste</pre>
</div> </div>
<!-- STATS --> <div class="link-box rounded-lg p-4 mb-4">
<div id="stats-content" class="content-container rounded-lg p-8 shadow-lg hidden tab-content"> <label for="share-link" class="block text-gray-300 text-sm font-bold mb-2">Shareable Link:</label>
<h2 class="text-3xl font-bold text-white text-center mb-8">Service Statistics</h2> <div class="flex items-center space-x-2">
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6"> <input type="text" id="share-link"
<div class="stat-card p-6 text-center"> class="bg-gray-700 text-white w-full p-2 border border-gray-600 rounded-md"
<h3 class="text-xl font-semibold text-white mb-2">Total Image Uploads</h3> value="{{ request.url }}" readonly>
<p class="text-5xl font-bold stat-value">{{ stats.total_images }}</p> <button id="copy-button"
</div> class="btn whitespace-nowrap flex-shrink-0 text-white font-bold py-3 px-5 rounded-md">
<div class="stat-card p-6 text-center"> Copy
<h3 class="text-xl font-semibold text-white mb-2">Total Paste Uploads</h3> </button>
<p class="text-5xl font-bold stat-value">{{ stats.total_pastes }}</p> <a href="/paste/{{ paste_id }}/raw"
</div> target="_blank"
<div class="stat-card p-6 text-center"> class="btn whitespace-nowrap flex-shrink-0 text-white font-bold py-3 px-5 rounded-md">
<h3 class="text-xl font-semibold text-white mb-2">Total API Uploads</h3> Raw
<p class="text-5xl font-bold stat-value">{{ stats.total_api_uploads }}</p>
</div>
</div>
</div>
<!-- TERMS OF SERVICE -->
<div id="tos-content" class="content-container tos-container rounded-lg p-8 shadow-lg hidden tab-content">
<h2 class="text-3xl font-bold text-white mb-4">Terms of Service</h2>
<p><strong>Last Updated: June 20, 2025</strong></p>
<p>By using this service you agree to these terms. The service is provided “as-is” with a focus on privacy.</p>
<h3 class="mt-6 text-white text-xl font-semibold mb-2">1. Privacy & Data</h3>
<ul class="list-disc list-inside text-gray-300 mb-4">
<li>No logs of your identity or IP.</li>
<li>All uploads are encrypted at rest.</li>
<li>Data auto-deletes after expiry or view limit.</li>
<li>EXIF metadata only kept if opted-in.</li>
</ul>
<h3 class="mt-6 text-white text-xl font-semibold mb-2">2. Acceptable Use</h3>
<p class="text-gray-300 mb-4">Do not upload illegal or harmful content. We reserve the right to remove content that violates these terms.</p>
<h3 class="mt-6 text-white text-xl font-semibold mb-2">3. Liability</h3>
<p class="text-gray-300">This free service comes with no warranties. We are not responsible for data loss.</p>
</div>
</div>
<!-- Version Info (Moved outside the tabbed content) -->
<div class="text-center text-xs text-gray-500 mt-6 mb-8">
<a href="https://github.com/your-username/your-repo-name" target="_blank" rel="noopener noreferrer" class="hover:text-gray-400 transition-colors">
Version 1.1
</a> </a>
</div> </div>
</div>
<!-- FEATURES --> <div class="text-center mt-8">
<section class="text-center features-section"> <a href="/" class="text-blue-400 hover:text-blue-300">Create another paste</a>
<h2 class="text-3xl font-bold text-white mb-8">Features</h2>
<div class="grid grid-cols-1 md:grid-cols-3 gap-8">
<div class="feature-card p-6 rounded-lg">
<div class="flex items-center justify-center h-12 w-12 rounded-md bg-blue-500 text-white mx-auto mb-4">
<svg xmlns="http://www.w3.org/2000/svg" class="h-6 w-6" fill="none" viewBox="0 0 24 24" stroke="currentColor"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 15v2m-6 4h12a2 2 0 002-2v-6a2 2 0 00-2-2H6a2 2 0 00-2 2v6a2 2 0 002 2zm10-10V7a4 4 0 00-8 0v4h8z" /></svg>
</div> </div>
<h3 class="text-xl font-semibold text-white mb-2">Encrypted at Rest</h3> <div class="text-center mt-4 text-sm">
<p class="text-gray-400">All uploaded files and pastes are fully encrypted on the server, ensuring your data is protected.</p> <p class="text-gray-500">Find this service useful? <a href="/donate" class="text-blue-400 hover:underline">Consider supporting its future.</a></p>
</div> </div>
<div class="feature-card p-6 rounded-lg">
<div class="flex items-center justify-center h-12 w-12 rounded-md bg-blue-500 text-white mx-auto mb-4"> </main>
<svg xmlns="http://www.w3.org/2000/svg" class="h-6 w-6" fill="none" viewBox="0 0 24 24" stroke="currentColor"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 11c0 3.517-1.009 6.786-2.673 9.356 m-2.673-9.356C6.673 8.214 9.327 5.5 12 5.5 c2.673 0 5.327 2.714 5.327 5.5s-1.009 6.786-2.673 9.356m-2.673-9.356h0z" /></svg> {% endif %}
</div>
<h3 class="text-xl font-semibold text-white mb-2">Anonymous by Design</h3>
<p class="text-gray-400">Image metadata (EXIF) is stripped and no unnecessary logs are kept. Built for the I2P network.</p>
</div>
<div class="feature-card p-6 rounded-lg">
<div class="flex items-center justify-center h-12 w-12 rounded-md bg-blue-500 text-white mx-auto mb-4">
<svg xmlns="http://www.w3.org/2000/svg" class="h-6 w-6" fill="none" viewBox="0 0 24 24" stroke="currentColor"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M3 15a4 4 0 004 4h9a5 5 0 10-.1-9.999 5.002 5.002 0 10-9.78 2.096A4.001 4.001 0 003 15z" /></svg>
</div>
<h3 class="text-xl font-semibold text-white mb-2">StormyCloud Infrastructure</h3>
<p class="text-gray-400">A fast, reliable, and secure platform dedicated to the privacy of the I2P community.</p>
</div>
</div>
</section>
<footer class="text-center text-gray-500 mt-16 border-t border-gray-700 pt-8 pb-8"> <footer class="text-center text-gray-500 mt-16 border-t border-gray-700 pt-8 pb-8">
<a href="http://stormycloud.i2p" class="hover:text-gray-400">StormyCloud</a> <a href="http://stormycloud.i2p" class="hover:text-gray-400">StormyCloud</a>
<span class="mx-2">|</span> <span class="mx-2">|</span>
<a href="/donate" class="hover:text-gray-400">Donate</a> <a href="/donate" class="hover:text-gray-400">Donate</a>
</footer> </footer>
</main>
</div> </div>
</div> </div>
<script> <script>
document.addEventListener('DOMContentLoaded', () => { const copyBtn = document.getElementById('copy-button');
const contentDivs = { if (copyBtn) {
'#image': document.querySelector('#image-form'), copyBtn.addEventListener('click', () => {
'#paste': document.querySelector('#paste-form'), const inp = document.getElementById('share-link');
'#api': document.querySelector('#api-docs'), inp.select(); document.execCommand('copy');
'#stats': document.querySelector('#stats-content'), copyBtn.textContent = 'Copied!';
'#tos': document.querySelector('#tos-content') setTimeout(() => copyBtn.textContent = 'Copy', 2000);
};
const tabs = document.querySelectorAll('#tab-nav a');
function showTab(hash) {
if (!hash || !contentDivs[hash]) hash = '#image';
Object.values(contentDivs).forEach(d => {
if(d) d.classList.add('hidden');
}); });
tabs.forEach(t => t.classList.remove('active'));
if(contentDivs[hash]) {
contentDivs[hash].classList.remove('hidden');
} }
const activeTab = document.querySelector(`#tab-nav a[href="${hash}"]`);
if(activeTab) {
activeTab.classList.add('active');
}
}
tabs.forEach(tab => tab.addEventListener('click', e => {
e.preventDefault();
const h = e.target.hash;
window.history.replaceState(null, null, ' ' + h);
showTab(h);
}));
showTab(window.location.hash);
function toggle(cbId, tgtId) {
const cb = document.getElementById(cbId), tgt = document.getElementById(tgtId);
if (!cb||!tgt) return;
cb.addEventListener('change', () => tgt.style.display = cb.checked ? 'block' : 'none');
tgt.style.display = cb.checked ? 'block' : 'none';
}
toggle('image-pw-protect','image-pw-options');
toggle('image-views-protect','image-views-options');
toggle('paste-pw-protect','paste-pw-options');
toggle('paste-views-protect','paste-views-options');
const closeBtn = document.getElementById('close-announcement'); const closeBtn = document.getElementById('close-announcement');
if (closeBtn) closeBtn.addEventListener('click', ()=> if (closeBtn) closeBtn.addEventListener('click', () =>
document.getElementById('announcement-bar').style.display = 'none' document.getElementById('announcement-bar').style.display = 'none'
); );
}); </script>
</script>
</body> </body>
</html> </html>