Convert to Flask app with password-protected admin panel
Links now live in data/links.json instead of hardcoded HTML, editable via /admin (add/rename/reorder/delete sections and links, per-link color and half-width side-by-side layout). New pages can be added without code changes. Docker image only needs rebuilding on code changes now, not on content changes (data/ is volume-mounted). Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
1 parent
56dc37843a
commit
61c4701115
13 files changed
+797
-173
No files matched your search
@@ -0,0 +1,249 @@
|
||||
import json
|
||||
import os
|
||||
import re
|
||||
import secrets
|
||||
from functools import wraps
|
||||
from pathlib import Path
|
||||
|
||||
from flask import Flask, abort, redirect, render_template, request, session, url_for
|
||||
|
||||
app = Flask(__name__)
|
||||
app.secret_key = os.environ.get("SECRET_KEY") or secrets.token_hex(32)
|
||||
app.config["SESSION_COOKIE_SAMESITE"] = "Lax"
|
||||
app.config["SESSION_COOKIE_SECURE"] = os.environ.get("COOKIE_SECURE", "true").lower() != "false"
|
||||
|
||||
ADMIN_PASSWORD = os.environ.get("ADMIN_PASSWORD")
|
||||
DATA_FILE = Path(os.environ.get("DATA_FILE", "data/links.json"))
|
||||
|
||||
COLORS = ["blue", "green", "orange", "pink"]
|
||||
|
||||
|
||||
def load_data():
|
||||
with open(DATA_FILE, "r", encoding="utf-8") as f:
|
||||
return json.load(f)
|
||||
|
||||
|
||||
def save_data(data):
|
||||
tmp = DATA_FILE.with_suffix(".tmp")
|
||||
with open(tmp, "w", encoding="utf-8") as f:
|
||||
json.dump(data, f, ensure_ascii=False, indent=2)
|
||||
tmp.replace(DATA_FILE)
|
||||
|
||||
|
||||
def login_required(view):
|
||||
@wraps(view)
|
||||
def wrapped(*args, **kwargs):
|
||||
if not session.get("admin"):
|
||||
return redirect(url_for("admin_dashboard"))
|
||||
return view(*args, **kwargs)
|
||||
|
||||
return wrapped
|
||||
|
||||
|
||||
def get_page_or_404(data, page_key):
|
||||
if page_key not in data:
|
||||
abort(404)
|
||||
return data[page_key]
|
||||
|
||||
|
||||
def get_section_or_404(page, section_id):
|
||||
for section in page["sections"]:
|
||||
if section["id"] == section_id:
|
||||
return section
|
||||
abort(404)
|
||||
|
||||
|
||||
@app.route("/")
|
||||
def index():
|
||||
data = load_data()
|
||||
return render_template("page.html", page=data["index"])
|
||||
|
||||
|
||||
@app.route("/<slug>.html")
|
||||
def page(slug):
|
||||
data = load_data()
|
||||
page_data = data.get(slug)
|
||||
if not page_data:
|
||||
abort(404)
|
||||
return render_template("page.html", page=page_data)
|
||||
|
||||
|
||||
@app.route("/admin", methods=["GET"])
|
||||
def admin_dashboard():
|
||||
if not session.get("admin"):
|
||||
return render_template("admin_login.html", error=None)
|
||||
data = load_data()
|
||||
return render_template("admin.html", data=data, colors=COLORS)
|
||||
|
||||
|
||||
@app.route("/admin/login", methods=["POST"])
|
||||
def admin_login():
|
||||
if not ADMIN_PASSWORD:
|
||||
return render_template(
|
||||
"admin_login.html", error="ADMIN_PASSWORD ist nicht gesetzt (siehe .env)."
|
||||
), 500
|
||||
if secrets.compare_digest(request.form.get("password", ""), ADMIN_PASSWORD):
|
||||
session["admin"] = True
|
||||
return redirect(url_for("admin_dashboard"))
|
||||
return render_template("admin_login.html", error="Falsches Passwort"), 401
|
||||
|
||||
|
||||
@app.route("/admin/logout", methods=["POST"])
|
||||
def admin_logout():
|
||||
session.clear()
|
||||
return redirect(url_for("admin_dashboard"))
|
||||
|
||||
|
||||
@app.route("/admin/<page_key>/header", methods=["POST"])
|
||||
@login_required
|
||||
def edit_header(page_key):
|
||||
data = load_data()
|
||||
page = get_page_or_404(data, page_key)
|
||||
page["title"] = request.form.get("title", page.get("title", "")).strip()
|
||||
page["subtitle"] = request.form.get("subtitle", page.get("subtitle", "")).strip()
|
||||
page["subtitle2"] = request.form.get("subtitle2", page.get("subtitle2", "")).strip()
|
||||
page["footer_label"] = request.form.get("footer_label", page.get("footer_label", "")).strip()
|
||||
page["footer_href"] = request.form.get("footer_href", page.get("footer_href", "")).strip()
|
||||
save_data(data)
|
||||
return redirect(url_for("admin_dashboard"))
|
||||
|
||||
|
||||
@app.route("/admin/page/add", methods=["POST"])
|
||||
@login_required
|
||||
def add_page():
|
||||
data = load_data()
|
||||
title = request.form.get("title", "").strip()
|
||||
slug = request.form.get("slug", "").strip().lower()
|
||||
slug = re.sub(r"[^a-z0-9-]+", "-", slug).strip("-")
|
||||
if slug and title and slug not in data:
|
||||
data[slug] = {
|
||||
"title": title,
|
||||
"subtitle": "",
|
||||
"subtitle2": "",
|
||||
"footer_label": "",
|
||||
"footer_href": "",
|
||||
"sections": [],
|
||||
}
|
||||
save_data(data)
|
||||
return redirect(url_for("admin_dashboard"))
|
||||
|
||||
|
||||
@app.route("/admin/page/<page_key>/delete", methods=["POST"])
|
||||
@login_required
|
||||
def delete_page(page_key):
|
||||
if page_key == "index":
|
||||
abort(400)
|
||||
data = load_data()
|
||||
data.pop(page_key, None)
|
||||
save_data(data)
|
||||
return redirect(url_for("admin_dashboard"))
|
||||
|
||||
|
||||
@app.route("/admin/<page_key>/section/add", methods=["POST"])
|
||||
@login_required
|
||||
def add_section(page_key):
|
||||
data = load_data()
|
||||
page = get_page_or_404(data, page_key)
|
||||
name = request.form.get("name", "").strip()
|
||||
if name:
|
||||
page["sections"].append({"id": secrets.token_hex(4), "name": name, "links": []})
|
||||
save_data(data)
|
||||
return redirect(url_for("admin_dashboard"))
|
||||
|
||||
|
||||
@app.route("/admin/<page_key>/section/<section_id>/rename", methods=["POST"])
|
||||
@login_required
|
||||
def rename_section(page_key, section_id):
|
||||
data = load_data()
|
||||
page = get_page_or_404(data, page_key)
|
||||
section = get_section_or_404(page, section_id)
|
||||
name = request.form.get("name", "").strip()
|
||||
if name:
|
||||
section["name"] = name
|
||||
save_data(data)
|
||||
return redirect(url_for("admin_dashboard"))
|
||||
|
||||
|
||||
@app.route("/admin/<page_key>/section/<section_id>/delete", methods=["POST"])
|
||||
@login_required
|
||||
def delete_section(page_key, section_id):
|
||||
data = load_data()
|
||||
page = get_page_or_404(data, page_key)
|
||||
page["sections"] = [s for s in page["sections"] if s["id"] != section_id]
|
||||
save_data(data)
|
||||
return redirect(url_for("admin_dashboard"))
|
||||
|
||||
|
||||
@app.route("/admin/<page_key>/section/<section_id>/link/add", methods=["POST"])
|
||||
@login_required
|
||||
def add_link(page_key, section_id):
|
||||
data = load_data()
|
||||
page = get_page_or_404(data, page_key)
|
||||
section = get_section_or_404(page, section_id)
|
||||
title = request.form.get("title", "").strip()
|
||||
url = request.form.get("url", "").strip()
|
||||
if title and url:
|
||||
section["links"].append(
|
||||
{
|
||||
"id": secrets.token_hex(4),
|
||||
"title": title,
|
||||
"note": request.form.get("note", "").strip(),
|
||||
"url": url,
|
||||
"color": request.form.get("color", "blue"),
|
||||
"half": bool(request.form.get("half")),
|
||||
}
|
||||
)
|
||||
save_data(data)
|
||||
return redirect(url_for("admin_dashboard"))
|
||||
|
||||
|
||||
@app.route("/admin/<page_key>/link/<link_id>/edit", methods=["POST"])
|
||||
@login_required
|
||||
def edit_link(page_key, link_id):
|
||||
data = load_data()
|
||||
page = get_page_or_404(data, page_key)
|
||||
for section in page["sections"]:
|
||||
for link in section["links"]:
|
||||
if link["id"] == link_id:
|
||||
link["title"] = request.form.get("title", link["title"]).strip()
|
||||
link["note"] = request.form.get("note", link["note"]).strip()
|
||||
link["url"] = request.form.get("url", link["url"]).strip()
|
||||
link["color"] = request.form.get("color", link["color"])
|
||||
link["half"] = bool(request.form.get("half"))
|
||||
save_data(data)
|
||||
return redirect(url_for("admin_dashboard"))
|
||||
|
||||
|
||||
@app.route("/admin/<page_key>/link/<link_id>/delete", methods=["POST"])
|
||||
@login_required
|
||||
def delete_link(page_key, link_id):
|
||||
data = load_data()
|
||||
page = get_page_or_404(data, page_key)
|
||||
for section in page["sections"]:
|
||||
section["links"] = [l for l in section["links"] if l["id"] != link_id]
|
||||
save_data(data)
|
||||
return redirect(url_for("admin_dashboard"))
|
||||
|
||||
|
||||
@app.route("/admin/<page_key>/link/<link_id>/move", methods=["POST"])
|
||||
@login_required
|
||||
def move_link(page_key, link_id):
|
||||
data = load_data()
|
||||
page = get_page_or_404(data, page_key)
|
||||
direction = request.form.get("direction")
|
||||
for section in page["sections"]:
|
||||
ids = [l["id"] for l in section["links"]]
|
||||
if link_id in ids:
|
||||
idx = ids.index(link_id)
|
||||
new_idx = idx - 1 if direction == "up" else idx + 1
|
||||
if 0 <= new_idx < len(section["links"]):
|
||||
section["links"][idx], section["links"][new_idx] = (
|
||||
section["links"][new_idx],
|
||||
section["links"][idx],
|
||||
)
|
||||
save_data(data)
|
||||
return redirect(url_for("admin_dashboard"))
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
app.run(host="0.0.0.0", port=80)
|
||||
Reference in new issue
Block a user