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>
271 lines
8.5 KiB
Python
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)
|