feat: add simple password auth gate
- Flask session-based login with styled dark-theme login page - All routes gated behind password (configurable via SITE_PASSWORD env) - /login and /api/health are public - Wrong password shows red error, correct redirects to original page - 30-day session persistence - /logout to clear session - Password: jv2026 (set in docker-compose.yml)
This commit is contained in:
90
app.py
90
app.py
@@ -2,10 +2,12 @@
|
|||||||
Live proposal site with real Gemini-powered demos.
|
Live proposal site with real Gemini-powered demos.
|
||||||
PostgreSQL-backed data dashboard."""
|
PostgreSQL-backed data dashboard."""
|
||||||
|
|
||||||
import os, json, time, traceback, logging
|
import os, json, time, traceback, logging, secrets
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
|
from functools import wraps
|
||||||
from flask import (Flask, render_template, jsonify, request,
|
from flask import (Flask, render_template, jsonify, request,
|
||||||
send_from_directory, redirect)
|
send_from_directory, redirect, session, url_for,
|
||||||
|
make_response)
|
||||||
|
|
||||||
from scraper import scrape_product, scrape_competitor
|
from scraper import scrape_product, scrape_competitor
|
||||||
from ai_engine import (generate_asset_pack, competitor_xray, pdp_surgeon,
|
from ai_engine import (generate_asset_pack, competitor_xray, pdp_surgeon,
|
||||||
@@ -18,6 +20,9 @@ app = Flask(__name__,
|
|||||||
static_folder="static",
|
static_folder="static",
|
||||||
template_folder="templates")
|
template_folder="templates")
|
||||||
|
|
||||||
|
app.secret_key = os.environ.get("SECRET_KEY", secrets.token_hex(32))
|
||||||
|
SITE_PASSWORD = os.environ.get("SITE_PASSWORD", "jv2026")
|
||||||
|
|
||||||
GEN_DIR = Path(__file__).parent / "generated"
|
GEN_DIR = Path(__file__).parent / "generated"
|
||||||
GEN_DIR.mkdir(exist_ok=True)
|
GEN_DIR.mkdir(exist_ok=True)
|
||||||
|
|
||||||
@@ -28,6 +33,87 @@ with app.app_context():
|
|||||||
except Exception as e:
|
except Exception as e:
|
||||||
logging.error(f"DB init failed: {e}")
|
logging.error(f"DB init failed: {e}")
|
||||||
|
|
||||||
|
|
||||||
|
# ── Auth ─────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
PUBLIC_PATHS = {"/login", "/api/health"}
|
||||||
|
|
||||||
|
@app.before_request
|
||||||
|
def require_auth():
|
||||||
|
"""Gate every request behind a simple password session."""
|
||||||
|
path = request.path
|
||||||
|
# Allow login page, health check, and static login assets
|
||||||
|
if path in PUBLIC_PATHS or path.startswith("/static/"):
|
||||||
|
return
|
||||||
|
if not session.get("authed"):
|
||||||
|
if request.is_json:
|
||||||
|
return jsonify({"error": "Authentication required"}), 401
|
||||||
|
return redirect(url_for("login", next=request.path))
|
||||||
|
|
||||||
|
|
||||||
|
LOGIN_HTML = """<!DOCTYPE html>
|
||||||
|
<html lang="en"><head>
|
||||||
|
<meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1">
|
||||||
|
<title>Login — JustVitamins × QuikCue</title>
|
||||||
|
<style>
|
||||||
|
*{margin:0;padding:0;box-sizing:border-box}
|
||||||
|
body{min-height:100vh;display:flex;align-items:center;justify-content:center;
|
||||||
|
background:#0a0e1a;font-family:-apple-system,BlinkMacSystemFont,'Segoe UI',sans-serif;color:#e0e6f0}
|
||||||
|
.card{background:rgba(255,255,255,.04);border:1px solid rgba(255,255,255,.08);
|
||||||
|
border-radius:16px;padding:48px 40px;max-width:400px;width:100%;text-align:center}
|
||||||
|
.card h1{font-size:1.5rem;margin-bottom:8px;background:linear-gradient(135deg,#22d3ee,#a78bfa);
|
||||||
|
-webkit-background-clip:text;-webkit-text-fill-color:transparent}
|
||||||
|
.card p{font-size:.85rem;color:#8892a8;margin-bottom:28px}
|
||||||
|
.field{position:relative;margin-bottom:20px}
|
||||||
|
.field input{width:100%;padding:14px 16px;background:rgba(255,255,255,.06);
|
||||||
|
border:1px solid rgba(255,255,255,.12);border-radius:10px;color:#e0e6f0;
|
||||||
|
font-size:1rem;outline:none;transition:border .2s}
|
||||||
|
.field input:focus{border-color:#22d3ee}
|
||||||
|
.field input::placeholder{color:#5a6478}
|
||||||
|
button{width:100%;padding:14px;border:none;border-radius:10px;cursor:pointer;
|
||||||
|
font-size:1rem;font-weight:600;color:#fff;
|
||||||
|
background:linear-gradient(135deg,#7c3aed,#6d28d9);transition:opacity .2s}
|
||||||
|
button:hover{opacity:.9}
|
||||||
|
.err{color:#f87171;font-size:.85rem;margin-bottom:16px}
|
||||||
|
.footer{margin-top:24px;font-size:.75rem;color:#4a5568}
|
||||||
|
</style>
|
||||||
|
</head><body>
|
||||||
|
<div class="card">
|
||||||
|
<h1>JustVitamins × QuikCue</h1>
|
||||||
|
<p>Confidential proposal — enter password to continue</p>
|
||||||
|
{{ERR}}
|
||||||
|
<form method="post">
|
||||||
|
<div class="field">
|
||||||
|
<input type="password" name="password" placeholder="Password" autofocus autocomplete="current-password">
|
||||||
|
</div>
|
||||||
|
<button type="submit">Enter</button>
|
||||||
|
</form>
|
||||||
|
<div class="footer">Prepared for Just Vitamins · March 2026</div>
|
||||||
|
</div>
|
||||||
|
</body></html>"""
|
||||||
|
|
||||||
|
|
||||||
|
@app.route("/login", methods=["GET", "POST"])
|
||||||
|
def login():
|
||||||
|
err = ""
|
||||||
|
if request.method == "POST":
|
||||||
|
pw = request.form.get("password", "")
|
||||||
|
if pw == SITE_PASSWORD:
|
||||||
|
session["authed"] = True
|
||||||
|
session.permanent = True
|
||||||
|
app.permanent_session_lifetime = __import__("datetime").timedelta(days=30)
|
||||||
|
dest = request.args.get("next", "/")
|
||||||
|
return redirect(dest)
|
||||||
|
err = '<div class="err">Wrong password</div>'
|
||||||
|
html = LOGIN_HTML.replace("{{ERR}}", err)
|
||||||
|
return make_response(html)
|
||||||
|
|
||||||
|
|
||||||
|
@app.route("/logout")
|
||||||
|
def logout():
|
||||||
|
session.clear()
|
||||||
|
return redirect("/login")
|
||||||
|
|
||||||
# ── Page routes ──────────────────────────────────────────────
|
# ── Page routes ──────────────────────────────────────────────
|
||||||
|
|
||||||
@app.route("/")
|
@app.route("/")
|
||||||
|
|||||||
@@ -7,6 +7,8 @@ services:
|
|||||||
environment:
|
environment:
|
||||||
- GEMINI_API_KEY=AIzaSyCHnesXLjPw-UgeZaQotut66bgjFdvy12E
|
- GEMINI_API_KEY=AIzaSyCHnesXLjPw-UgeZaQotut66bgjFdvy12E
|
||||||
- DATABASE_URL=postgresql://jv:jvpass@tasks.db:5432/justvitamin
|
- DATABASE_URL=postgresql://jv:jvpass@tasks.db:5432/justvitamin
|
||||||
|
- SECRET_KEY=c4f8a2e91b0d7f3e5a6c9d2b8e1f4a7d
|
||||||
|
- SITE_PASSWORD=jv2026
|
||||||
volumes:
|
volumes:
|
||||||
- jv-generated:/app/generated
|
- jv-generated:/app/generated
|
||||||
networks:
|
networks:
|
||||||
|
|||||||
Reference in New Issue
Block a user