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 uuid
import sqlite3
import mimetypes
import secrets
import re
from datetime import datetime, timedelta
from io import BytesIO
@@ -13,6 +16,8 @@ from flask import (
from werkzeug.utils import secure_filename
from werkzeug.middleware.proxy_fix import ProxyFix
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 pygments import highlight
@@ -30,6 +35,9 @@ load_dotenv()
app = Flask(__name__)
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['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['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_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) ---
def i2p_key_func():
@@ -77,7 +95,7 @@ EXPIRY_MAP = {
"24h": timedelta(hours=24), "48h": timedelta(hours=48)
}
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',
'typescript', 'xml', 'yaml'
]
@@ -146,13 +164,70 @@ def cleanup_expired_content():
try:
os.remove(path)
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,))
conn.commit()
conn.close()
# --- 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):
"""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
def get_time_left(expiry_str):
@@ -194,7 +269,7 @@ def process_and_encrypt_image(stream, orig_fn, keep_exif=False):
f.write(encrypted)
return new_fn
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
@app.context_processor
@@ -242,7 +317,7 @@ def healthz():
conn.close()
db_status = "ok"
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"
sched_status = "running" if scheduler.running and scheduler.state == 1 else "stopped"
return jsonify(database=db_status, scheduler=sched_status)
@@ -253,9 +328,11 @@ def index():
db = get_db()
rows = db.execute("SELECT stat_key, stat_value FROM stats").fetchall()
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(
'index.html',
languages=POPULAR_LANGUAGES,
languages=index_languages,
stats=stats,
allowed_extensions=list(ALLOWED_EXTENSIONS)
)
@@ -297,7 +374,7 @@ def upload_image():
return redirect(url_for('index', _anchor='image'))
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'))
new_fn = process_and_encrypt_image(file.stream, file.filename, keep_exif)
if not new_fn:
@@ -319,6 +396,7 @@ def upload_image():
db.commit()
update_stat('total_images')
flash('Image uploaded successfully! This is your shareable link.', 'success')
return redirect(url_for('view_image', filename=new_fn))
flash('Invalid file type.', 'error')
@@ -333,6 +411,11 @@ def upload_paste():
flash('Paste content cannot be empty.', 'error')
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()
expiry = now + EXPIRY_MAP.get(request.form.get('expiry', '1h'), timedelta(hours=1))
pw = request.form.get('password') or None
@@ -350,6 +433,7 @@ def upload_paste():
db.commit()
update_stat('total_pastes')
flash('Paste created successfully! This is your shareable link.', 'success')
return redirect(url_for('view_paste', paste_id=paste_id))
@@ -365,10 +449,11 @@ def view_image(filename):
abort(404)
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 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))
flash('Incorrect password.', 'error')
return render_template('view_image.html', password_required=True, filename=filename)
@@ -392,10 +477,11 @@ def view_paste(paste_id):
abort(404)
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 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))
flash('Incorrect password.', 'error')
return render_template('view_paste.html', password_required=True, paste_id=paste_id)
@@ -405,14 +491,22 @@ def view_paste(paste_id):
db.commit()
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.commit()
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:
lexer = get_lexer_by_name(row['language'])
lexer = get_lexer_by_name(selected_language)
except:
lexer = get_lexer_by_name('text')
fmt = HtmlFormatter(style='monokai', cssclass='syntax', linenos='table')
highlighted = highlight(content, lexer, fmt)
@@ -420,7 +514,9 @@ def view_paste(paste_id):
password_required=False,
paste_id=paste_id,
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>')
def get_upload(filename):
# Enhanced security validation
if not validate_filename_security(filename):
abort(404)
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()
row = db.execute("SELECT * FROM images WHERE id = ?", (safe_fn,)).fetchone()
@@ -480,7 +592,7 @@ def get_upload(filename):
data = fernet.decrypt(encrypted)
return send_file(BytesIO(data), mimetype='image/webp')
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)
@app.route('/admin/delete/image/<filename>', methods=['POST'])
@@ -495,7 +607,8 @@ def delete_image(filename):
db.commit()
flash(f'Image "{safe}" has been deleted.', 'success')
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'))
@app.route('/admin/delete/paste/<paste_id>', methods=['POST'])
@@ -507,18 +620,20 @@ def delete_paste(paste_id):
db.commit()
flash(f'Paste "{paste_id}" has been deleted.', 'success')
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'))
# --- API Routes ---
@app.route('/api/upload/image', methods=['POST'])
@limiter.limit("50 per hour")
@csrf.exempt
def api_upload_image():
if 'file' not in request.files or request.files['file'].filename == '':
return jsonify(error="No file selected"), 400
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')))
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'])
@limiter.limit("100 per hour")
@csrf.exempt
def api_upload_paste():
if not request.is_json: return jsonify(error="Request must be JSON"), 400
data = request.get_json()
if not isinstance(data, dict):
return jsonify(error="Invalid JSON data"), 400
content = data.get('content', '').strip()
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()
expiry = now + EXPIRY_MAP.get(data.get('expiry', '1h'), timedelta(hours=1))
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.
# 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)

View File

@@ -1,418 +1,152 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8"/>
<meta name="viewport" content="width=device-width, initial-scale=1.0"/>
<title>I2P Secure Share</title>
<link rel="stylesheet" href="{{ url_for('static', filename='css/tailwind.css') }}"/>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Admin Dashboard - I2P Secure Share</title>
<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">
<style>
body { background-color: #1a202c; color: #cbd5e0; }
.content-container { background-color: #2d3748; border:1px solid #4a5568; border-radius:0.5rem; }
.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; }
.content-container { background-color: #2d3748; border: 1px solid #4a5568; }
.btn { background-color: #4299e1; transition: background-color 0.3s ease; }
.btn:hover { background-color: #3182ce; }
select,textarea,input[type="text"],input[type="password"],input[type="number"] {
background-color:#4a5568; border:1px solid #718096; color:#cbd5e0;
}
.alert-success { background-color:#38a169; }
.btn-danger { background-color: #e53e3e; }
.btn-danger:hover { background-color: #c53030; }
input { background-color: #4a5568; border: 1px solid #718096; }
table { width: 100%; border-collapse: collapse; }
th, td { border: 1px solid #4a5568; padding: 0.75rem; text-align: left; }
th { background-color: #1a202c; }
tr:nth-child(even) { background-color: #2d3748; }
.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>
<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>
<body class="font-sans">
{% if announcement_enabled and announcement_message %}
<div id="announcement-bar" class="announcement-bar text-white text-center p-2 relative shadow-lg">
<span>{{ announcement_message }}</span>
<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>
<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>
</div>
{% endif %}
<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">
<header class="text-center mb-8">
<a href="/" class="inline-block mb-4">
<img src="/static/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>
<h1 class="text-4xl font-bold text-white">I2P Secure Share</h1>
<p class="text-gray-400">Anonymously share images and text pastes.</p>
<h1 class="text-4xl font-bold text-white">Admin Dashboard</h1>
</header>
<main>
<div id="js-error-container"
class="hidden alert-error text-white p-3 rounded-md shadow-lg mb-4"
role="alert"></div>
<main class="content-container rounded-lg p-8 shadow-lg">
{% if auth_success %}
<h2 class="text-2xl font-semibold text-white mb-6">Active Images</h2>
{% 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) %}
{% if messages %}
<div class="mb-4">
{% for category, message in messages %}
<div class="alert-{{category}} text-white p-3 rounded-md shadow-lg"
role="alert">{{ message }}</div>
<div class="alert-{{ category }} text-white p-3 rounded-md shadow-lg" role="alert">
{{ message }}
</div>
{% endfor %}
</div>
{% endif %}
{% endwith %}
<div class="mb-4 border-b border-gray-700 tab-nav-container">
<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>
<form method="POST" action="{{ url_for('admin_dashboard') }}">
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}"/>
<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>
<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 focus:outline-none focus:ring-2 focus:ring-blue-500" required>
</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>
<div><button type="submit" class="btn w-full text-white font-bold py-3 px-5 rounded-md focus:shadow-outline">Login</button></div>
</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>
<!-- 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>
</main>
<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>
<span class="mx-2">|</span>
<a href="/donate" class="hover:text-gray-400">Donate</a>
</footer>
</main>
</div>
</div>
<script>
document.addEventListener('DOMContentLoaded', () => {
const contentDivs = {
'#image': document.querySelector('#image-form'),
'#paste': document.querySelector('#paste-form'),
'#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');
const announcementBar = document.getElementById('announcement-bar');
const closeButton = document.getElementById('close-announcement');
if (closeButton) {
closeButton.addEventListener('click', () => {
announcementBar.style.display = 'none';
});
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');
if (closeBtn) closeBtn.addEventListener('click', ()=>
document.getElementById('announcement-bar').style.display = 'none'
);
});
</script>
</body>
</html>

View File

@@ -4,8 +4,9 @@
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<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">
<style>
body { background-color: #1a202c; color: #cbd5e0; }
.content-container { background-color: #2d3748; border: 1px solid #4a5568; }
@@ -26,7 +27,7 @@
<div class="w-full max-w-2xl mx-auto p-4">
<header class="text-center mb-8">
<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>
<h1 class="text-3xl font-bold text-white">Support the Service</h1>
</header>

View File

@@ -4,8 +4,9 @@
<meta charset="UTF-8"/>
<meta name="viewport" content="width=device-width, initial-scale=1.0"/>
<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">
<style>
body { background-color: #1a202c; color: #cbd5e0; }
.content-container { background-color: #2d3748; border:1px solid #4a5568; border-radius:0.5rem; }
@@ -100,10 +101,7 @@
<div id="main-container" class="w-full max-w-2xl mx-auto p-4">
<header class="text-center mb-8">
<a href="/" class="inline-block mb-4">
<img src="/static/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>
<h1 class="text-4xl font-bold text-white">I2P Secure Share</h1>
<p class="text-gray-400">Anonymously share images and text pastes.</p>
@@ -140,9 +138,9 @@
</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">
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}"/>
<h2 class="text-2xl font-semibold mb-6 text-white">Upload an Image</h2>
<div class="mb-6">
@@ -196,9 +194,9 @@
</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">
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}"/>
<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>
@@ -250,7 +248,6 @@
</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>
@@ -281,7 +278,6 @@
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">
@@ -300,7 +296,6 @@ http://{{ request.host }}/api/upload/paste</pre>
</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>
@@ -319,14 +314,12 @@ http://{{ request.host }}/api/upload/paste</pre>
</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">
<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
</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">

View File

@@ -3,116 +3,58 @@
<head>
<meta charset="UTF-8"/>
<meta name="viewport" content="width=device-width, initial-scale=1.0"/>
<title>I2P Secure Share</title>
<link rel="stylesheet" href="{{ url_for('static', filename='css/tailwind.css') }}"/>
<title>View Image - I2P Secure Share</title>
<link rel="stylesheet" href="/static/css/tailwind.css"/>
<link rel="icon" href="{{ url_for('static', filename='favicon.ico') }}" type="image/x-icon">
<style>
body { background-color: #1a202c; color: #cbd5e0; }
.content-container { background-color: #2d3748; border:1px solid #4a5568; border-radius:0.5rem; }
.tab { border-bottom:2px solid transparent; cursor:pointer; }
.tab.active { border-bottom-color:#63b3ed; color:#ffffff; }
.content-container { background-color: #2d3748; border: 1px solid #4a5568; }
.link-box { background-color: #2d3748; border:1px solid #4a5568; word-break:break-all; }
.btn { background-color:#4299e1; transition:background-color .3s ease; }
.btn:hover { background-color:#3182ce; }
select,textarea,input[type="text"],input[type="password"],input[type="number"] {
background-color:#4a5568; border:1px solid #718096; color:#cbd5e0;
}
.thumbnail-container { border:2px dashed #4a5568; max-width:100%; }
.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-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>
<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>
<body class="font-sans">
{% if announcement_enabled and announcement_message %}
<div id="announcement-bar" class="announcement-bar text-white text-center p-2 relative shadow-lg">
<span>{{ announcement_message }}</span>
<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>
<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>
</div>
{% endif %}
<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">
<a href="/" class="inline-block mb-4">
<img src="/static/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>
<h1 class="text-4xl font-bold text-white">I2P Secure Share</h1>
<p class="text-gray-400">Anonymously share images and text pastes.</p>
<h1 class="text-3xl font-bold text-white">View Image</h1>
<p class="text-gray-400 mt-2 text-xl">Expires in: {{ time_left }}</p>
</header>
<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) %}
{% if messages %}
<div class="mb-4">
@@ -124,295 +66,56 @@
{% endif %}
{% endwith %}
<div class="mb-4 border-b border-gray-700 tab-nav-container">
<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 class="thumbnail-container rounded-lg p-4 mb-6 flex justify-center items-center">
<img src="/uploads/{{ filename }}"
alt="Uploaded Image" class="thumbnail rounded-md"/>
</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 %}
<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 class="link-box rounded-lg p-4 mb-4">
<label for="share-link" class="block text-gray-300 text-sm font-bold mb-2">Direct Image Link:</label>
<div class="flex items-center space-x-2">
<input type="text" id="share-link"
class="bg-gray-700 text-white w-full p-2 border border-gray-600 rounded-md"
value="{{ request.host_url }}uploads/{{ filename }}" readonly>
<button id="copy-button"
class="btn whitespace-nowrap flex-shrink-0 text-white font-bold py-3 px-5 rounded-md">
Copy
</button>
</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 class="text-center mt-8">
<a href="/" class="text-blue-400 hover:text-blue-300">Upload another file</a>
</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>
<!-- 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>
</main>
{% endif %}
<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>
<span class="mx-2">|</span>
<a href="/donate" class="hover:text-gray-400">Donate</a>
</footer>
</main>
</div>
</div>
<script>
document.addEventListener('DOMContentLoaded', () => {
const contentDivs = {
'#image': document.querySelector('#image-form'),
'#paste': document.querySelector('#paste-form'),
'#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');
const copyBtn = document.getElementById('copy-button');
if (copyBtn) {
copyBtn.addEventListener('click', () => {
const inp = document.getElementById('share-link');
inp.select(); document.execCommand('copy');
copyBtn.textContent = 'Copied!';
setTimeout(() => copyBtn.textContent = 'Copy', 2000);
});
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');
if (closeBtn) closeBtn.addEventListener('click', () =>
document.getElementById('announcement-bar').style.display = 'none'
);
});
</script>
</body>
</html>

View File

@@ -3,116 +3,134 @@
<head>
<meta charset="UTF-8"/>
<meta name="viewport" content="width=device-width, initial-scale=1.0"/>
<title>I2P Secure Share</title>
<link rel="stylesheet" href="{{ url_for('static', filename='css/tailwind.css') }}"/>
<title>View Paste - I2P Secure Share</title>
<link rel="stylesheet" href="/static/css/tailwind.css"/>
<link rel="icon" href="{{ url_for('static', filename='favicon.ico') }}" type="image/x-icon">
<style>
body { background-color: #1a202c; color: #cbd5e0; }
.content-container { background-color: #2d3748; border:1px solid #4a5568; border-radius:0.5rem; }
.tab { border-bottom:2px solid transparent; cursor:pointer; }
.tab.active { border-bottom-color:#63b3ed; color:#ffffff; }
.link-box { background-color: #2d3748; border:1px solid #4a5568; word-break:break-all; }
.btn { background-color:#4299e1; transition:background-color .3s ease; }
.btn:hover { background-color:#3182ce; }
select,textarea,input[type="text"],input[type="password"],input[type="number"] {
background-color:#4a5568; border:1px solid #718096; color:#cbd5e0;
.code-container {
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-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;
}
/* Pygments 'monokai' theme CSS */
.syntax .hll { background-color: #49483e }
.syntax { background: #272822; color: #f8f8f2 }
.syntax .c { color: #75715e } /* Comment */
.syntax .err { color: #960050; background-color: #1e0010 } /* Error */
.syntax .k { color: #66d9ef } /* Keyword */
.syntax .l { color: #ae81ff } /* Literal */
.syntax .n { color: #f8f8f2 } /* Name */
.syntax .o { color: #f92672 } /* Operator */
.syntax .p { color: #f8f8f2 } /* Punctuation */
.syntax .ch { color: #75715e } /* Comment.Hashbang */
.syntax .cm { color: #75715e } /* Comment.Multiline */
.syntax .cp { color: #75715e } /* Comment.Preproc */
.syntax .cpf { color: #75715e } /* Comment.PreprocFile */
.syntax .c1 { color: #75715e } /* Comment.Single */
.syntax .cs { color: #75715e } /* Comment.Special */
.syntax .gd { color: #f92672 } /* Generic.Deleted */
.syntax .ge { font-style: italic } /* Generic.Emph */
.syntax .gi { color: #a6e22e } /* Generic.Inserted */
.syntax .gs { font-weight: bold } /* Generic.Strong */
.syntax .gu { color: #75715e } /* Generic.Subheading */
.syntax .kc { color: #66d9ef } /* Keyword.Constant */
.syntax .kd { color: #66d9ef } /* Keyword.Declaration */
.syntax .kn { color: #f92672 } /* Keyword.Namespace */
.syntax .kp { color: #66d9ef } /* Keyword.Pseudo */
.syntax .kr { color: #66d9ef } /* Keyword.Reserved */
.syntax .kt { color: #66d9ef } /* Keyword.Type */
.syntax .ld { color: #e6db74 } /* Literal.Date */
.syntax .m { color: #ae81ff } /* Literal.Number */
.syntax .s { color: #e6db74 } /* Literal.String */
.syntax .na { color: #a6e22e } /* Name.Attribute */
.syntax .nb { color: #f8f8f2 } /* Name.Builtin */
.syntax .nc { color: #a6e22e } /* Name.Class */
.syntax .no { color: #66d9ef } /* Name.Constant */
.syntax .nd { color: #a6e22e } /* Name.Decorator */
.syntax .ni { color: #f8f8f2 } /* Name.Entity */
.syntax .ne { color: #a6e22e } /* Name.Exception */
.syntax .nf { color: #a6e22e } /* Name.Function */
.syntax .nl { color: #f8f8f2 } /* Name.Label */
.syntax .nn { color: #f8f8f2 } /* Name.Namespace */
.syntax .nx { color: #a6e22e } /* Name.Other */
.syntax .py { color: #f8f8f2 } /* Name.Property */
.syntax .nt { color: #f92672 } /* Name.Tag */
.syntax .nv { color: #f8f8f2 } /* Name.Variable */
.syntax .ow { color: #f92672 } /* Operator.Word */
.syntax .w { color: #f8f8f2 } /* Text.Whitespace */
.syntax .mb { color: #ae81ff } /* Literal.Number.Bin */
.syntax .mf { color: #ae81ff } /* Literal.Number.Float */
.syntax .mh { color: #ae81ff } /* Literal.Number.Hex */
.syntax .mi { color: #ae81ff } /* Literal.Number.Integer */
.syntax .mo { color: #ae81ff } /* Literal.Number.Oct */
.syntax .sa { color: #e6db74 } /* Literal.String.Affix */
.syntax .sb { color: #e6db74 } /* Literal.String.Backtick */
.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>
<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>
<body class="font-sans">
{% if announcement_enabled and announcement_message %}
<div id="announcement-bar" class="announcement-bar text-white text-center p-2 relative shadow-lg">
<span>{{ announcement_message }}</span>
<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>
<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>
</div>
{% endif %}
<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">
<a href="/" class="inline-block mb-4">
<img src="/static/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>
<h1 class="text-4xl font-bold text-white">I2P Secure Share</h1>
<p class="text-gray-400">Anonymously share images and text pastes.</p>
<h1 class="text-3xl font-bold text-white">View Paste</h1>
<p class="text-gray-400 mt-2 text-xl">Expires in: {{ time_left }}</p>
</header>
<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) %}
{% if messages %}
<div class="mb-4">
@@ -124,295 +142,74 @@
{% endif %}
{% endwith %}
<div class="mb-4 border-b border-gray-700 tab-nav-container">
<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">
<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>
<div class="flex justify-end mb-2">
<form method="GET" action="" class="flex items-center space-x-2">
<label for="language-switcher" class="text-sm text-gray-400">Syntax:</label>
<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">
{% for lang in languages %}
<option value="{{ lang }}">{{ lang|capitalize }}</option>
<option value="{{ lang }}" {% if lang == selected_language %}selected{% endif %}>
{{ 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>
<noscript><button type="submit" class="btn text-sm py-1 px-3">Update</button></noscript>
</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 class="code-container mb-6 syntax">
{{ highlighted_content|safe }}
</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
<div class="link-box rounded-lg p-4 mb-4">
<label for="share-link" class="block text-gray-300 text-sm font-bold mb-2">Shareable Link:</label>
<div class="flex items-center space-x-2">
<input type="text" id="share-link"
class="bg-gray-700 text-white w-full p-2 border border-gray-600 rounded-md"
value="{{ request.url }}" readonly>
<button id="copy-button"
class="btn whitespace-nowrap flex-shrink-0 text-white font-bold py-3 px-5 rounded-md">
Copy
</button>
<a href="/paste/{{ paste_id }}/raw"
target="_blank"
class="btn whitespace-nowrap flex-shrink-0 text-white font-bold py-3 px-5 rounded-md">
Raw
</a>
</div>
</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 class="text-center mt-8">
<a href="/" class="text-blue-400 hover:text-blue-300">Create another paste</a>
</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 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 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>
</main>
{% endif %}
<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>
<span class="mx-2">|</span>
<a href="/donate" class="hover:text-gray-400">Donate</a>
</footer>
</main>
</div>
</div>
<script>
document.addEventListener('DOMContentLoaded', () => {
const contentDivs = {
'#image': document.querySelector('#image-form'),
'#paste': document.querySelector('#paste-form'),
'#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');
const copyBtn = document.getElementById('copy-button');
if (copyBtn) {
copyBtn.addEventListener('click', () => {
const inp = document.getElementById('share-link');
inp.select(); document.execCommand('copy');
copyBtn.textContent = 'Copied!';
setTimeout(() => copyBtn.textContent = 'Copy', 2000);
});
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');
if (closeBtn) closeBtn.addEventListener('click', () =>
document.getElementById('announcement-bar').style.display = 'none'
);
});
</script>
</body>
</html>