Add password protection to all admin routes
All /admin/* routes now require login via /admin/login. The password is set via the ADMIN_PASS environment variable on the server (default: hiacts2026 — change this in PythonAnywhere env vars before the conference). /admin/logout clears the session. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
1 parent
a4e36586d0
commit
7e83c4f2c0
1 file changed
+59
-9
@@ -285,14 +285,23 @@ _session_version = [1] # incremented on reset to invalidate all existing sessio
|
||||
# MAIL_PASS — your email password or app password
|
||||
# =============================================================================
|
||||
|
||||
MAIL_HOST = os.environ.get('MAIL_HOST', '')
|
||||
MAIL_PORT = int(os.environ.get('MAIL_PORT', '587'))
|
||||
MAIL_USER = os.environ.get('MAIL_USER', '')
|
||||
MAIL_PASS = os.environ.get('MAIL_PASS', '')
|
||||
MAIL_HOST = os.environ.get('MAIL_HOST', '')
|
||||
MAIL_PORT = int(os.environ.get('MAIL_PORT', '587'))
|
||||
MAIL_USER = os.environ.get('MAIL_USER', '')
|
||||
MAIL_PASS = os.environ.get('MAIL_PASS', '')
|
||||
BASE_URL = os.environ.get('BASE_URL', 'http://localhost:5001')
|
||||
ADMIN_PASS = os.environ.get('ADMIN_PASS', 'hiacts2026') # set this in PythonAnywhere env vars
|
||||
|
||||
# Set this to your public URL so keep-links in emails work correctly.
|
||||
# On PythonAnywhere: Web tab -> Environment variables -> BASE_URL = https://yourusername.pythonanywhere.com
|
||||
BASE_URL = os.environ.get('BASE_URL', 'http://localhost:5001')
|
||||
|
||||
def admin_required(f):
|
||||
"""Decorator that redirects to /admin/login if the session is not authenticated."""
|
||||
from functools import wraps
|
||||
@wraps(f)
|
||||
def decorated(*args, **kwargs):
|
||||
if not session.get('admin_ok'):
|
||||
return redirect('/admin/login')
|
||||
return f(*args, **kwargs)
|
||||
return decorated
|
||||
|
||||
|
||||
def get_respondents(snapshot):
|
||||
@@ -1246,7 +1255,45 @@ To change your mind at any time, email <a href="mailto:mblack@txproducts.de">mbl
|
||||
|
||||
|
||||
# Sends the full PDF analysis report to every collected email address (run this after the conference)
|
||||
@app.route('/admin/login', methods=['GET', 'POST'])
|
||||
def admin_login():
|
||||
error = ''
|
||||
if request.method == 'POST':
|
||||
if request.form.get('password') == ADMIN_PASS:
|
||||
session['admin_ok'] = True
|
||||
return redirect('/admin')
|
||||
error = 'Incorrect password.'
|
||||
return f'''<!DOCTYPE html>
|
||||
<html><head><meta charset="UTF-8"><title>Admin Login</title>
|
||||
<style>
|
||||
body{{font-family:sans-serif;display:flex;align-items:center;justify-content:center;min-height:100vh;margin:0;background:#f0f4f8}}
|
||||
.box{{background:white;border-radius:16px;padding:40px 48px;max-width:360px;width:90%;box-shadow:0 8px 40px rgba(0,0,0,.12)}}
|
||||
h2{{color:#1a365d;margin:0 0 6px;font-size:20px}}
|
||||
p{{color:#8a9baf;font-size:13px;margin:0 0 24px}}
|
||||
input{{width:100%;padding:12px 14px;border:1.5px solid #dde5ee;border-radius:9px;font-size:15px;font-family:inherit;box-sizing:border-box;margin-bottom:12px;outline:none}}
|
||||
input:focus{{border-color:#2563eb}}
|
||||
button{{width:100%;padding:13px;background:linear-gradient(135deg,#0f2744,#1a4a8a);color:white;border:none;border-radius:9px;font-size:15px;font-weight:700;cursor:pointer;font-family:inherit}}
|
||||
.err{{color:#dc2626;font-size:13px;margin-bottom:10px}}
|
||||
</style></head>
|
||||
<body><div class="box">
|
||||
<h2>Admin Access</h2>
|
||||
<p>Hi-Acts Innovation Challenge Arena</p>
|
||||
<form method="POST">
|
||||
{"<div class='err'>" + error + "</div>" if error else ""}
|
||||
<input type="password" name="password" placeholder="Password" autofocus>
|
||||
<button type="submit">Enter</button>
|
||||
</form>
|
||||
</div></body></html>'''
|
||||
|
||||
|
||||
@app.route('/admin/logout')
|
||||
def admin_logout():
|
||||
session.pop('admin_ok', None)
|
||||
return redirect('/admin/login')
|
||||
|
||||
|
||||
@app.route('/admin/send_all_reports', methods=['POST'])
|
||||
@admin_required
|
||||
def send_all_reports():
|
||||
emails = load_emails()
|
||||
if not emails:
|
||||
@@ -1259,6 +1306,7 @@ def send_all_reports():
|
||||
|
||||
# Sends a deletion confirmation to every stored email, then wipes emails.json
|
||||
@app.route('/admin/delete_and_notify', methods=['POST'])
|
||||
@admin_required
|
||||
def delete_and_notify():
|
||||
records = load_email_records()
|
||||
to_delete = [r for r in records if not r.get('retained', False)]
|
||||
@@ -1283,6 +1331,7 @@ def delete_and_notify():
|
||||
|
||||
# Downloads all poll responses as a CSV file — one row per respondent
|
||||
@app.route('/admin/export')
|
||||
@admin_required
|
||||
def export_csv():
|
||||
n = max((len(answers[q['id']]) for q in QUESTIONS), default=0)
|
||||
output = io.StringIO()
|
||||
@@ -1356,9 +1405,8 @@ def demo_status():
|
||||
|
||||
|
||||
# Admin page — shows all raw responses per question with bar charts
|
||||
# WARNING: this page has no password protection.
|
||||
# The web dev team should restrict access to this URL in production.
|
||||
@app.route('/admin')
|
||||
@admin_required
|
||||
def admin():
|
||||
processed = []
|
||||
for q in QUESTIONS:
|
||||
@@ -1377,6 +1425,7 @@ def admin():
|
||||
|
||||
# Clears all answers — used by the admin page reset button
|
||||
@app.route('/admin/reset', methods=['POST'])
|
||||
@admin_required
|
||||
def reset():
|
||||
answers.clear()
|
||||
save_answers()
|
||||
@@ -1399,6 +1448,7 @@ def seed():
|
||||
|
||||
# Generates the analysis PDF on demand and returns it as a download
|
||||
@app.route('/admin/report')
|
||||
@admin_required
|
||||
def report():
|
||||
pdf_bytes = generate_analysis_pdf()
|
||||
if not pdf_bytes:
|
||||
|
||||
Reference in new issue
Block a user