Simplify auth to HTTP Basic Auth
This commit is contained in:
parent
38cfa587a5
commit
c2064bd943
81
app.py
81
app.py
|
|
@ -1,50 +1,73 @@
|
|||
import base64
|
||||
import hashlib
|
||||
import hmac
|
||||
import os
|
||||
|
||||
from dotenv import load_dotenv
|
||||
from flask import Flask, redirect, render_template, request, session, url_for
|
||||
|
||||
load_dotenv()
|
||||
from flask import Flask, Response, render_template, request
|
||||
|
||||
app = Flask(__name__)
|
||||
app.secret_key = os.environ["SECRET_KEY"]
|
||||
|
||||
SITE_PASSWORD = os.environ["SITE_PASSWORD"]
|
||||
PASSWORD_SALT = "fn-resi-v1"
|
||||
PASSWORD_HASH = "18194f5e151146895ddd7ff59fcac4f324b2832201df108d1d89cc0ef9d2ebe9"
|
||||
AUTH_REALM = "FBN Residential"
|
||||
|
||||
|
||||
def _password_matches(candidate):
|
||||
expected = os.environ.get("SITE_PASSWORD")
|
||||
if expected is not None:
|
||||
return hmac.compare_digest(candidate, expected)
|
||||
|
||||
derived = hashlib.pbkdf2_hmac(
|
||||
"sha256",
|
||||
candidate.encode("utf-8"),
|
||||
PASSWORD_SALT.encode("utf-8"),
|
||||
390000,
|
||||
).hex()
|
||||
return hmac.compare_digest(derived, PASSWORD_HASH)
|
||||
|
||||
|
||||
def _unauthorized():
|
||||
return Response(
|
||||
"Authentication required.\n",
|
||||
401,
|
||||
{
|
||||
"WWW-Authenticate": f'Basic realm="{AUTH_REALM}"',
|
||||
"Content-Type": "text/plain; charset=utf-8",
|
||||
},
|
||||
)
|
||||
|
||||
|
||||
def _authorized(req):
|
||||
header = req.headers.get("Authorization", "")
|
||||
if not header.startswith("Basic "):
|
||||
return False
|
||||
|
||||
try:
|
||||
decoded = base64.b64decode(header.split(None, 1)[1]).decode("utf-8")
|
||||
except Exception:
|
||||
return False
|
||||
|
||||
_, _, password = decoded.partition(":")
|
||||
if not password:
|
||||
return False
|
||||
|
||||
return _password_matches(password)
|
||||
|
||||
|
||||
@app.after_request
|
||||
def add_noindex_header(response):
|
||||
response.headers["X-Robots-Tag"] = "noindex, nofollow"
|
||||
response.headers["Cache-Control"] = "no-store"
|
||||
return response
|
||||
|
||||
|
||||
@app.route("/")
|
||||
def index():
|
||||
if not session.get("authenticated"):
|
||||
return redirect(url_for("login"))
|
||||
if not _authorized(request):
|
||||
return _unauthorized()
|
||||
return render_template("dashboard.html")
|
||||
|
||||
|
||||
@app.route("/login", methods=["GET", "POST"])
|
||||
def login():
|
||||
if session.get("authenticated"):
|
||||
return redirect(url_for("index"))
|
||||
|
||||
error = None
|
||||
if request.method == "POST":
|
||||
if request.form.get("password") == SITE_PASSWORD:
|
||||
session["authenticated"] = True
|
||||
return redirect(url_for("index"))
|
||||
error = "Wrong password."
|
||||
|
||||
return render_template("login.html", error=error)
|
||||
|
||||
|
||||
@app.route("/logout")
|
||||
def logout():
|
||||
session.clear()
|
||||
return redirect(url_for("login"))
|
||||
|
||||
|
||||
@app.route("/robots.txt")
|
||||
def robots():
|
||||
return "User-agent: *\nDisallow: /\n", 200, {"Content-Type": "text/plain"}
|
||||
|
|
|
|||
|
|
@ -1,3 +1,2 @@
|
|||
flask
|
||||
gunicorn
|
||||
python-dotenv
|
||||
|
|
|
|||
|
|
@ -97,7 +97,7 @@
|
|||
|
||||
<div style="display:flex;justify-content:space-between;align-items:baseline;">
|
||||
<h1>FBN Residential — Executive Summary</h1>
|
||||
<a href="/logout" style="color:var(--muted);font-size:12px;text-decoration:none;">Logout</a>
|
||||
<span style="color:var(--muted);font-size:12px;">Protected</span>
|
||||
</div>
|
||||
<p class="subtitle">As of March 27, 2026 · FY 2023–2025 · Source: GP</p>
|
||||
|
||||
|
|
|
|||
Loading…
Reference in New Issue