From cd9a46b4039de59ac97ae6e185513b5300cb1307 Mon Sep 17 00:00:00 2001
From: simon
Date: Fri, 17 Apr 2026 12:04:00 +0200
Subject: [PATCH] modified: web/app.py modified:
web/blueprints/group_admin.py modified: web/panel_db.py modified:
web/templates/group_admin/privacy_policy.html
---
web/app.py | 19 +++++---
web/blueprints/group_admin.py | 3 +-
web/panel_db.py | 48 +++++++++++++++----
web/templates/group_admin/privacy_policy.html | 7 +++
4 files changed, 59 insertions(+), 18 deletions(-)
diff --git a/web/app.py b/web/app.py
index 92d9412..e6d0af8 100644
--- a/web/app.py
+++ b/web/app.py
@@ -148,14 +148,19 @@ def create_app() -> Flask:
policy_version=Config.PRIVACY_POLICY_VERSION,
)
- @app.route("/policy/")
- def public_group_policy(group_id):
- """Public, unauthenticated URL for a group's server privacy policy."""
+ @app.route("/policy/")
+ def public_group_policy(token):
+ """Public, unauthenticated URL for a group's server privacy policy.
+
+ The token is an opaque UUID so the group ID is never exposed.
+ """
import panel_db as db
- policy = db.get_group_policy(group_id)
- group = db.get_group_by_id(group_id)
- if not group:
- return "Group not found", 404
+ row = db.get_group_policy_by_token(token)
+ if not row:
+ return render_template("404.html"), 404
+ # Build lightweight dicts that the template expects
+ policy = row
+ group = {"name": row["group_name"]}
return render_template("group_policy.html", policy=policy, group=group)
@app.errorhandler(400)
diff --git a/web/blueprints/group_admin.py b/web/blueprints/group_admin.py
index 0e5120f..8ac31bd 100644
--- a/web/blueprints/group_admin.py
+++ b/web/blueprints/group_admin.py
@@ -572,7 +572,8 @@ def privacy_policy():
return redirect(url_for("group_admin.privacy_policy"))
group = db.get_group_by_id(group_id)
- public_url = url_for("public_group_policy", group_id=group_id, _external=True)
+ token = policy["public_token"] if policy else None
+ public_url = url_for("public_group_policy", token=token, _external=True) if token else None
return render_template("group_admin/privacy_policy.html",
policy=policy, group=group, public_url=public_url)
diff --git a/web/panel_db.py b/web/panel_db.py
index b42c6e6..0fb2c56 100644
--- a/web/panel_db.py
+++ b/web/panel_db.py
@@ -177,13 +177,22 @@ PANEL_MIGRATIONS = [
"Add users.consented_at for GDPR consent timestamp"),
(9,
"""CREATE TABLE IF NOT EXISTS group_privacy_policy (
- group_id INT PRIMARY KEY,
- policy_text LONGTEXT,
- policy_url VARCHAR(500),
- updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
+ group_id INT PRIMARY KEY,
+ policy_text LONGTEXT,
+ policy_url VARCHAR(500),
+ public_token CHAR(36) NULL UNIQUE,
+ updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
FOREIGN KEY (group_id) REFERENCES user_groups(id) ON DELETE CASCADE
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4""",
"Add group_privacy_policy table for group-hosted privacy policies"),
+ (10,
+ "ALTER TABLE group_privacy_policy ADD COLUMN IF NOT EXISTS "
+ "public_token CHAR(36) NULL",
+ "Add group_privacy_policy.public_token for opaque public URL"),
+ (11,
+ "ALTER TABLE group_privacy_policy ADD CONSTRAINT IF NOT EXISTS "
+ "uq_gpp_public_token UNIQUE (public_token)",
+ "Unique index on group_privacy_policy.public_token"),
]
CREDS_SCHEMA = [
@@ -769,20 +778,39 @@ def set_user_consent(user_id: int, policy_version: str) -> None:
def get_group_policy(group_id: int):
"""Returns the group_privacy_policy row for *group_id*, or None if not set."""
rows = _panel_query(
- "SELECT group_id, policy_text, policy_url, updated_at "
+ "SELECT group_id, policy_text, policy_url, public_token, updated_at "
"FROM group_privacy_policy WHERE group_id = %s",
(group_id,),
)
return rows[0] if rows else None
+def get_group_policy_by_token(token: str):
+ """Returns the policy row and group for the given opaque public_token, or None."""
+ rows = _panel_query(
+ "SELECT p.group_id, p.policy_text, p.policy_url, p.public_token, p.updated_at, "
+ "g.name AS group_name "
+ "FROM group_privacy_policy p "
+ "JOIN user_groups g ON g.id = p.group_id "
+ "WHERE p.public_token = %s",
+ (token,),
+ )
+ return rows[0] if rows else None
+
+
def set_group_policy(group_id: int, policy_text: str | None, policy_url: str | None) -> None:
- """Upserts the privacy policy for a group."""
+ """Upserts the privacy policy for a group.
+
+ The *public_token* (opaque UUID) is generated once on first INSERT and
+ never changed on subsequent updates, so existing URLs stay valid.
+ """
_panel_query(
- "INSERT INTO group_privacy_policy (group_id, policy_text, policy_url) "
- "VALUES (%s, %s, %s) "
- "ON DUPLICATE KEY UPDATE policy_text = VALUES(policy_text), "
- "policy_url = VALUES(policy_url), updated_at = UTC_TIMESTAMP()",
+ "INSERT INTO group_privacy_policy (group_id, policy_text, policy_url, public_token) "
+ "VALUES (%s, %s, %s, UUID()) "
+ "ON DUPLICATE KEY UPDATE "
+ "policy_text = VALUES(policy_text), "
+ "policy_url = VALUES(policy_url), "
+ "updated_at = UTC_TIMESTAMP()",
(group_id, policy_text, policy_url),
write=True,
)
diff --git a/web/templates/group_admin/privacy_policy.html b/web/templates/group_admin/privacy_policy.html
index 6dc09d1..500188e 100644
--- a/web/templates/group_admin/privacy_policy.html
+++ b/web/templates/group_admin/privacy_policy.html
@@ -12,6 +12,7 @@
{# ── Public URL banner ────────────────────────────────────── #}
+ {% if public_url %}
+ {% else %}
+
+
+ Save the policy once to generate a secret public URL.
+
+ {% endif %}
{# ── Last updated ─────────────────────────────────────────── #}
{% if policy and policy.updated_at %}