new file: consent-plugin/pom.xml
new file: consent-plugin/src/main/java/de/simolzimol/mclogger/consent/ConsentConfig.java new file: consent-plugin/src/main/java/de/simolzimol/mclogger/consent/ConsentPlugin.java new file: consent-plugin/src/main/java/de/simolzimol/mclogger/consent/commands/ConsentCommand.java new file: consent-plugin/src/main/java/de/simolzimol/mclogger/consent/database/ConsentDatabase.java new file: consent-plugin/src/main/java/de/simolzimol/mclogger/consent/listeners/ConsentListener.java new file: consent-plugin/src/main/java/de/simolzimol/mclogger/consent/util/MessageUtil.java new file: consent-plugin/src/main/resources/config.yml new file: consent-plugin/src/main/resources/plugin.yml modified: web/app.py modified: web/blueprints/group_admin.py modified: web/panel_db.py modified: web/templates/group_admin/base.html new file: web/templates/group_admin/privacy_policy.html new file: web/templates/group_policy.html
This commit is contained in:
10
web/app.py
10
web/app.py
@@ -148,6 +148,16 @@ def create_app() -> Flask:
|
||||
policy_version=Config.PRIVACY_POLICY_VERSION,
|
||||
)
|
||||
|
||||
@app.route("/policy/<int:group_id>")
|
||||
def public_group_policy(group_id):
|
||||
"""Public, unauthenticated URL for a group's server privacy policy."""
|
||||
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
|
||||
return render_template("group_policy.html", policy=policy, group=group)
|
||||
|
||||
@app.errorhandler(400)
|
||||
def bad_request(_):
|
||||
return "Bad request", 400
|
||||
|
||||
@@ -542,3 +542,37 @@ def player_delete(uuid):
|
||||
|
||||
return render_template("group_admin/player_delete_confirm.html",
|
||||
player=player, group=group)
|
||||
|
||||
|
||||
# ─── Group Privacy Policy ─────────────────────────────────────────────────────
|
||||
|
||||
@group_admin.route("/privacy-policy", methods=["GET", "POST"])
|
||||
@group_admin_required
|
||||
def privacy_policy():
|
||||
"""Group admins can write and publish their own server privacy policy."""
|
||||
from roles import OWNER_ONLY_ROLES as _OWNER_ONLY
|
||||
if session.get("role") not in _OWNER_ONLY:
|
||||
flash("Only the Group Owner can edit the privacy policy.", "danger")
|
||||
return redirect(url_for("group_admin.dashboard"))
|
||||
|
||||
group_id = session["group_id"]
|
||||
policy = db.get_group_policy(group_id)
|
||||
|
||||
if request.method == "POST":
|
||||
policy_text = request.form.get("policy_text", "").strip() or None
|
||||
policy_url = request.form.get("policy_url", "").strip() or None
|
||||
db.set_group_policy(group_id, policy_text, policy_url)
|
||||
db.log_audit_event(
|
||||
session["user_id"], session["username"], "group.policy_updated",
|
||||
entity_type="group", entity_id=str(group_id),
|
||||
details={"policy_url": policy_url},
|
||||
group_id=group_id, ip_address=request.remote_addr,
|
||||
)
|
||||
flash("Privacy policy saved.", "success")
|
||||
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)
|
||||
return render_template("group_admin/privacy_policy.html",
|
||||
policy=policy, group=group, public_url=public_url)
|
||||
|
||||
|
||||
@@ -175,6 +175,15 @@ PANEL_MIGRATIONS = [
|
||||
(8,
|
||||
"ALTER TABLE users ADD COLUMN IF NOT EXISTS consented_at DATETIME NULL",
|
||||
"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,
|
||||
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"),
|
||||
]
|
||||
|
||||
CREDS_SCHEMA = [
|
||||
@@ -753,6 +762,32 @@ def set_user_consent(user_id: int, policy_version: str) -> None:
|
||||
)
|
||||
|
||||
|
||||
# ─────────────────────────────────────────────────────────────
|
||||
# Group Privacy Policy
|
||||
# ─────────────────────────────────────────────────────────────
|
||||
|
||||
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 "
|
||||
"FROM group_privacy_policy WHERE group_id = %s",
|
||||
(group_id,),
|
||||
)
|
||||
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."""
|
||||
_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()",
|
||||
(group_id, policy_text, policy_url),
|
||||
write=True,
|
||||
)
|
||||
|
||||
|
||||
# ─────────────────────────────────────────────────────────────
|
||||
# Audit-Log
|
||||
# ─────────────────────────────────────────────────────────────
|
||||
|
||||
@@ -18,6 +18,7 @@
|
||||
<a href="{{ url_for('group_admin.dashboard') }}" class="nav-link text-dark {{ 'fw-bold' if request.endpoint == 'group_admin.dashboard' }}">Dashboard</a>
|
||||
<a href="{{ url_for('group_admin.members') }}" class="nav-link text-dark {{ 'fw-bold' if request.endpoint == 'group_admin.members' }}">Members</a>
|
||||
<a href="{{ url_for('group_admin.database') }}" class="nav-link text-dark {{ 'fw-bold' if request.endpoint == 'group_admin.database' }}">Database</a>
|
||||
<a href="{{ url_for('group_admin.privacy_policy') }}" class="nav-link text-dark {{ 'fw-bold' if request.endpoint == 'group_admin.privacy_policy' }}">Privacy Policy</a>
|
||||
<a href="{{ url_for('panel.dashboard') }}" class="btn btn-outline-dark btn-sm">
|
||||
<i class="bi bi-grid me-1"></i>Panel
|
||||
</a>
|
||||
|
||||
74
web/templates/group_admin/privacy_policy.html
Normal file
74
web/templates/group_admin/privacy_policy.html
Normal file
@@ -0,0 +1,74 @@
|
||||
{% extends "group_admin/base.html" %}
|
||||
{% block title %}Privacy Policy{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<div class="row justify-content-center">
|
||||
<div class="col-lg-9">
|
||||
|
||||
<h2 class="mb-1"><i class="bi bi-file-earmark-lock2 me-2"></i>Server Privacy Policy</h2>
|
||||
<p class="text-muted mb-4">
|
||||
Write your Minecraft server's privacy policy here. Players will be shown this page when
|
||||
the <strong>MCConsent</strong> plugin asks them to consent before playing.
|
||||
</p>
|
||||
|
||||
{# ── Public URL banner ────────────────────────────────────── #}
|
||||
<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>
|
||||
<div>
|
||||
<strong>Public URL</strong> — paste this into your <code>consent-plugin/config.yml</code>
|
||||
as the <code>policy-url</code> value:<br>
|
||||
<code id="publicUrl">{{ public_url }}</code>
|
||||
<button class="btn btn-sm btn-outline-light ms-2" onclick="navigator.clipboard.writeText('{{ public_url }}')">
|
||||
<i class="bi bi-clipboard"></i> Copy
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{# ── Last updated ─────────────────────────────────────────── #}
|
||||
{% if policy and policy.updated_at %}
|
||||
<p class="text-muted small">Last updated: {{ policy.updated_at.strftime('%Y-%m-%d %H:%M UTC') }}</p>
|
||||
{% endif %}
|
||||
|
||||
{# ── Editor form ──────────────────────────────────────────── #}
|
||||
<form method="post">
|
||||
<input type="hidden" name="_csrf_token" value="{{ csrf_token() }}">
|
||||
|
||||
<div class="mb-3">
|
||||
<label for="policy_url" class="form-label fw-semibold">
|
||||
Additional / External Policy URL <span class="text-muted fw-normal">(optional)</span>
|
||||
</label>
|
||||
<input type="url" class="form-control font-monospace"
|
||||
id="policy_url" name="policy_url" maxlength="500"
|
||||
placeholder="https://your-website.example.com/privacy"
|
||||
value="{{ (policy.policy_url or '') if policy else '' }}">
|
||||
<div class="form-text">
|
||||
If you host a full policy on your own website you can link it here. It will be
|
||||
displayed as a button on the public policy page.
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="mb-3">
|
||||
<label for="policy_text" class="form-label fw-semibold">Policy Text</label>
|
||||
<textarea class="form-control font-monospace" id="policy_text" name="policy_text"
|
||||
rows="24" placeholder="Enter your privacy policy here…"
|
||||
style="resize: vertical;">{{ (policy.policy_text or '') if policy else '' }}</textarea>
|
||||
<div class="form-text">
|
||||
Plain text or basic Markdown is accepted. HTML is <strong>not</strong> rendered.
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="d-flex gap-2">
|
||||
<button type="submit" class="btn btn-warning">
|
||||
<i class="bi bi-floppy me-1"></i>Save Policy
|
||||
</button>
|
||||
{% if policy %}
|
||||
<a href="{{ public_url }}" target="_blank" class="btn btn-outline-light">
|
||||
<i class="bi bi-box-arrow-up-right me-1"></i>Preview public page
|
||||
</a>
|
||||
{% endif %}
|
||||
</div>
|
||||
</form>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
64
web/templates/group_policy.html
Normal file
64
web/templates/group_policy.html
Normal file
@@ -0,0 +1,64 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en" data-bs-theme="dark">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Privacy Policy — {{ group.name }}</title>
|
||||
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/css/bootstrap.min.css">
|
||||
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.11.3/font/bootstrap-icons.min.css">
|
||||
</head>
|
||||
<body class="bg-dark text-light">
|
||||
|
||||
<nav class="navbar navbar-dark bg-secondary bg-opacity-25 border-bottom border-secondary mb-4">
|
||||
<div class="container">
|
||||
<span class="navbar-brand">
|
||||
<i class="bi bi-file-earmark-lock2 me-2 text-warning"></i>
|
||||
<strong>{{ group.name }}</strong> — Privacy Policy
|
||||
</span>
|
||||
<span class="text-muted small">Powered by MCLogger</span>
|
||||
</div>
|
||||
</nav>
|
||||
|
||||
<div class="container" style="max-width: 860px;">
|
||||
|
||||
{% if not policy or not policy.policy_text %}
|
||||
<div class="alert alert-warning">
|
||||
<i class="bi bi-exclamation-triangle me-2"></i>
|
||||
This server has not yet published a privacy policy.
|
||||
Please contact the server administrator for more information.
|
||||
</div>
|
||||
{% else %}
|
||||
|
||||
{% if policy.updated_at %}
|
||||
<p class="text-muted small mb-4">
|
||||
<i class="bi bi-clock me-1"></i>
|
||||
Last updated: {{ policy.updated_at.strftime('%B %d, %Y') }}
|
||||
</p>
|
||||
{% endif %}
|
||||
|
||||
{% if policy.policy_url %}
|
||||
<a href="{{ policy.policy_url }}" target="_blank" rel="noopener noreferrer"
|
||||
class="btn btn-outline-info btn-sm mb-4">
|
||||
<i class="bi bi-box-arrow-up-right me-1"></i>View full policy on our website
|
||||
</a>
|
||||
{% endif %}
|
||||
|
||||
<div class="card bg-secondary bg-opacity-10 border-secondary">
|
||||
<div class="card-body">
|
||||
<pre class="mb-0 text-light" style="white-space: pre-wrap; word-break: break-word; font-family: inherit;">{{ policy.policy_text }}</pre>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{% endif %}
|
||||
|
||||
<hr class="border-secondary mt-5">
|
||||
<p class="text-muted small text-center mb-4">
|
||||
This page is hosted by <a href="https://github.com/simolzimol/MCLogger" class="text-secondary">MCLogger</a>
|
||||
on behalf of <em>{{ group.name }}</em>.
|
||||
</p>
|
||||
|
||||
</div>
|
||||
|
||||
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/js/bootstrap.bundle.min.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
Reference in New Issue
Block a user