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
|
import os
|
||||||
|
|
||||||
from dotenv import load_dotenv
|
from flask import Flask, Response, render_template, request
|
||||||
from flask import Flask, redirect, render_template, request, session, url_for
|
|
||||||
|
|
||||||
load_dotenv()
|
|
||||||
|
|
||||||
app = Flask(__name__)
|
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
|
@app.after_request
|
||||||
def add_noindex_header(response):
|
def add_noindex_header(response):
|
||||||
response.headers["X-Robots-Tag"] = "noindex, nofollow"
|
response.headers["X-Robots-Tag"] = "noindex, nofollow"
|
||||||
|
response.headers["Cache-Control"] = "no-store"
|
||||||
return response
|
return response
|
||||||
|
|
||||||
|
|
||||||
@app.route("/")
|
@app.route("/")
|
||||||
def index():
|
def index():
|
||||||
if not session.get("authenticated"):
|
if not _authorized(request):
|
||||||
return redirect(url_for("login"))
|
return _unauthorized()
|
||||||
return render_template("dashboard.html")
|
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")
|
@app.route("/robots.txt")
|
||||||
def robots():
|
def robots():
|
||||||
return "User-agent: *\nDisallow: /\n", 200, {"Content-Type": "text/plain"}
|
return "User-agent: *\nDisallow: /\n", 200, {"Content-Type": "text/plain"}
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,2 @@
|
||||||
flask
|
flask
|
||||||
gunicorn
|
gunicorn
|
||||||
python-dotenv
|
|
||||||
|
|
|
||||||
|
|
@ -97,7 +97,7 @@
|
||||||
|
|
||||||
<div style="display:flex;justify-content:space-between;align-items:baseline;">
|
<div style="display:flex;justify-content:space-between;align-items:baseline;">
|
||||||
<h1>FBN Residential — Executive Summary</h1>
|
<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>
|
</div>
|
||||||
<p class="subtitle">As of March 27, 2026 · FY 2023–2025 · Source: GP</p>
|
<p class="subtitle">As of March 27, 2026 · FY 2023–2025 · Source: GP</p>
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue