modified: web/app.py
modified: web/blueprints/group_admin.py modified: web/panel_db.py modified: web/templates/group_admin/privacy_policy.html
This commit is contained in:
19
web/app.py
19
web/app.py
@@ -148,14 +148,19 @@ def create_app() -> Flask:
|
|||||||
policy_version=Config.PRIVACY_POLICY_VERSION,
|
policy_version=Config.PRIVACY_POLICY_VERSION,
|
||||||
)
|
)
|
||||||
|
|
||||||
@app.route("/policy/<int:group_id>")
|
@app.route("/policy/<token>")
|
||||||
def public_group_policy(group_id):
|
def public_group_policy(token):
|
||||||
"""Public, unauthenticated URL for a group's server privacy policy."""
|
"""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
|
import panel_db as db
|
||||||
policy = db.get_group_policy(group_id)
|
row = db.get_group_policy_by_token(token)
|
||||||
group = db.get_group_by_id(group_id)
|
if not row:
|
||||||
if not group:
|
return render_template("404.html"), 404
|
||||||
return "Group not found", 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)
|
return render_template("group_policy.html", policy=policy, group=group)
|
||||||
|
|
||||||
@app.errorhandler(400)
|
@app.errorhandler(400)
|
||||||
|
|||||||
@@ -572,7 +572,8 @@ def privacy_policy():
|
|||||||
return redirect(url_for("group_admin.privacy_policy"))
|
return redirect(url_for("group_admin.privacy_policy"))
|
||||||
|
|
||||||
group = db.get_group_by_id(group_id)
|
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",
|
return render_template("group_admin/privacy_policy.html",
|
||||||
policy=policy, group=group, public_url=public_url)
|
policy=policy, group=group, public_url=public_url)
|
||||||
|
|
||||||
|
|||||||
@@ -177,13 +177,22 @@ PANEL_MIGRATIONS = [
|
|||||||
"Add users.consented_at for GDPR consent timestamp"),
|
"Add users.consented_at for GDPR consent timestamp"),
|
||||||
(9,
|
(9,
|
||||||
"""CREATE TABLE IF NOT EXISTS group_privacy_policy (
|
"""CREATE TABLE IF NOT EXISTS group_privacy_policy (
|
||||||
group_id INT PRIMARY KEY,
|
group_id INT PRIMARY KEY,
|
||||||
policy_text LONGTEXT,
|
policy_text LONGTEXT,
|
||||||
policy_url VARCHAR(500),
|
policy_url VARCHAR(500),
|
||||||
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
|
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
|
FOREIGN KEY (group_id) REFERENCES user_groups(id) ON DELETE CASCADE
|
||||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4""",
|
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4""",
|
||||||
"Add group_privacy_policy table for group-hosted privacy policies"),
|
"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 = [
|
CREDS_SCHEMA = [
|
||||||
@@ -769,20 +778,39 @@ def set_user_consent(user_id: int, policy_version: str) -> None:
|
|||||||
def get_group_policy(group_id: int):
|
def get_group_policy(group_id: int):
|
||||||
"""Returns the group_privacy_policy row for *group_id*, or None if not set."""
|
"""Returns the group_privacy_policy row for *group_id*, or None if not set."""
|
||||||
rows = _panel_query(
|
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",
|
"FROM group_privacy_policy WHERE group_id = %s",
|
||||||
(group_id,),
|
(group_id,),
|
||||||
)
|
)
|
||||||
return rows[0] if rows else None
|
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:
|
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(
|
_panel_query(
|
||||||
"INSERT INTO group_privacy_policy (group_id, policy_text, policy_url) "
|
"INSERT INTO group_privacy_policy (group_id, policy_text, policy_url, public_token) "
|
||||||
"VALUES (%s, %s, %s) "
|
"VALUES (%s, %s, %s, UUID()) "
|
||||||
"ON DUPLICATE KEY UPDATE policy_text = VALUES(policy_text), "
|
"ON DUPLICATE KEY UPDATE "
|
||||||
"policy_url = VALUES(policy_url), updated_at = UTC_TIMESTAMP()",
|
"policy_text = VALUES(policy_text), "
|
||||||
|
"policy_url = VALUES(policy_url), "
|
||||||
|
"updated_at = UTC_TIMESTAMP()",
|
||||||
(group_id, policy_text, policy_url),
|
(group_id, policy_text, policy_url),
|
||||||
write=True,
|
write=True,
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -12,6 +12,7 @@
|
|||||||
</p>
|
</p>
|
||||||
|
|
||||||
{# ── Public URL banner ────────────────────────────────────── #}
|
{# ── Public URL banner ────────────────────────────────────── #}
|
||||||
|
{% if public_url %}
|
||||||
<div class="alert alert-info d-flex align-items-center gap-2 mb-4">
|
<div class="alert alert-info d-flex align-items-center gap-2 mb-4">
|
||||||
<i class="bi bi-link-45deg fs-5 flex-shrink-0"></i>
|
<i class="bi bi-link-45deg fs-5 flex-shrink-0"></i>
|
||||||
<div>
|
<div>
|
||||||
@@ -23,6 +24,12 @@
|
|||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
{% else %}
|
||||||
|
<div class="alert alert-secondary mb-4">
|
||||||
|
<i class="bi bi-info-circle me-2"></i>
|
||||||
|
Save the policy once to generate a secret public URL.
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
{# ── Last updated ─────────────────────────────────────────── #}
|
{# ── Last updated ─────────────────────────────────────────── #}
|
||||||
{% if policy and policy.updated_at %}
|
{% if policy and policy.updated_at %}
|
||||||
|
|||||||
Reference in New Issue
Block a user