Files
marcandClaude Sonnet 5 3a49d91c2e Make favicon editable per page in /admin
Same emoji-based inline SVG favicon as before, but the emoji is now
a per-page "favicon" field editable in the header form instead of
hardcoded in the template.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-07-24 12:15:03 +02:00

271 lines
8.5 KiB
Python

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"]
DEFAULT_DATA = {
"index": {
"title": "My Links",
"subtitle": "",
"subtitle2": "",
"footer_label": "",
"footer_href": "",
"profile_image": "",
"favicon": "🍯",
"sections": [],
}
}
def load_data():
if not DATA_FILE.exists():
DATA_FILE.parent.mkdir(parents=True, exist_ok=True)
save_data(DEFAULT_DATA)
return json.loads(json.dumps(DEFAULT_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()
page["profile_image"] = request.form.get("profile_image", page.get("profile_image", "")).strip()
page["favicon"] = request.form.get("favicon", page.get("favicon", "")).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": "",
"profile_image": "",
"favicon": "🍯",
"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)