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": "", "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("/.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//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() 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": "", "sections": [], } save_data(data) return redirect(url_for("admin_dashboard")) @app.route("/admin/page//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//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//section//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//section//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//section//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//link//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//link//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//link//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)