Compare commits
2 Commits
63ce0f9c5b
...
be26484606
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
be26484606 | ||
|
|
d25536e9c4 |
2
.gitignore
vendored
2
.gitignore
vendored
@@ -61,3 +61,5 @@ logs/
|
||||
# OS specific
|
||||
Thumbs.db
|
||||
.DS_Store
|
||||
|
||||
target/
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -1,12 +1,12 @@
|
||||
C:\Users\Simon.Speedy\Documents\dev projekte\Minecarft\Tools\Log\paper-plugin\src\main\java\de\simolzimol\mclogger\paper\commands\MCLoggerCommand.java
|
||||
C:\Users\Simon.Speedy\Documents\dev projekte\Minecarft\Tools\Log\paper-plugin\src\main\java\de\simolzimol\mclogger\paper\database\DatabaseManager.java
|
||||
C:\Users\Simon.Speedy\Documents\dev projekte\Minecarft\Tools\Log\paper-plugin\src\main\java\de\simolzimol\mclogger\paper\listeners\BlockListener.java
|
||||
C:\Users\Simon.Speedy\Documents\dev projekte\Minecarft\Tools\Log\paper-plugin\src\main\java\de\simolzimol\mclogger\paper\listeners\EntityListener.java
|
||||
C:\Users\Simon.Speedy\Documents\dev projekte\Minecarft\Tools\Log\paper-plugin\src\main\java\de\simolzimol\mclogger\paper\listeners\InventoryListener.java
|
||||
C:\Users\Simon.Speedy\Documents\dev projekte\Minecarft\Tools\Log\paper-plugin\src\main\java\de\simolzimol\mclogger\paper\listeners\LuckPermsListener.java
|
||||
C:\Users\Simon.Speedy\Documents\dev projekte\Minecarft\Tools\Log\paper-plugin\src\main\java\de\simolzimol\mclogger\paper\listeners\PlayerChatCommandListener.java
|
||||
C:\Users\Simon.Speedy\Documents\dev projekte\Minecarft\Tools\Log\paper-plugin\src\main\java\de\simolzimol\mclogger\paper\listeners\PlayerDeathListener.java
|
||||
C:\Users\Simon.Speedy\Documents\dev projekte\Minecarft\Tools\Log\paper-plugin\src\main\java\de\simolzimol\mclogger\paper\listeners\PlayerMiscListener.java
|
||||
C:\Users\Simon.Speedy\Documents\dev projekte\Minecarft\Tools\Log\paper-plugin\src\main\java\de\simolzimol\mclogger\paper\listeners\PlayerSessionListener.java
|
||||
C:\Users\Simon.Speedy\Documents\dev projekte\Minecarft\Tools\Log\paper-plugin\src\main\java\de\simolzimol\mclogger\paper\listeners\WorldListener.java
|
||||
C:\Users\Simon.Speedy\Documents\dev projekte\Minecarft\Tools\Log\paper-plugin\src\main\java\de\simolzimol\mclogger\paper\PaperLoggerPlugin.java
|
||||
C:\Users\Menuette\Documents\Programme\MClogger\paper-plugin\src\main\java\de\simolzimol\mclogger\paper\commands\MCLoggerCommand.java
|
||||
C:\Users\Menuette\Documents\Programme\MClogger\paper-plugin\src\main\java\de\simolzimol\mclogger\paper\database\DatabaseManager.java
|
||||
C:\Users\Menuette\Documents\Programme\MClogger\paper-plugin\src\main\java\de\simolzimol\mclogger\paper\listeners\BlockListener.java
|
||||
C:\Users\Menuette\Documents\Programme\MClogger\paper-plugin\src\main\java\de\simolzimol\mclogger\paper\listeners\EntityListener.java
|
||||
C:\Users\Menuette\Documents\Programme\MClogger\paper-plugin\src\main\java\de\simolzimol\mclogger\paper\listeners\InventoryListener.java
|
||||
C:\Users\Menuette\Documents\Programme\MClogger\paper-plugin\src\main\java\de\simolzimol\mclogger\paper\listeners\LuckPermsListener.java
|
||||
C:\Users\Menuette\Documents\Programme\MClogger\paper-plugin\src\main\java\de\simolzimol\mclogger\paper\listeners\PlayerChatCommandListener.java
|
||||
C:\Users\Menuette\Documents\Programme\MClogger\paper-plugin\src\main\java\de\simolzimol\mclogger\paper\listeners\PlayerDeathListener.java
|
||||
C:\Users\Menuette\Documents\Programme\MClogger\paper-plugin\src\main\java\de\simolzimol\mclogger\paper\listeners\PlayerMiscListener.java
|
||||
C:\Users\Menuette\Documents\Programme\MClogger\paper-plugin\src\main\java\de\simolzimol\mclogger\paper\listeners\PlayerSessionListener.java
|
||||
C:\Users\Menuette\Documents\Programme\MClogger\paper-plugin\src\main\java\de\simolzimol\mclogger\paper\listeners\WorldListener.java
|
||||
C:\Users\Menuette\Documents\Programme\MClogger\paper-plugin\src\main\java\de\simolzimol\mclogger\paper\PaperLoggerPlugin.java
|
||||
|
||||
BIN
paper-plugin/target/mclogger-paper-1.0.0-shaded.jar
Normal file
BIN
paper-plugin/target/mclogger-paper-1.0.0-shaded.jar
Normal file
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -1,3 +1,3 @@
|
||||
C:\Users\Simon.Speedy\Documents\dev projekte\Minecarft\Tools\Log\velocity-plugin\src\main\java\de\simolzimol\mclogger\velocity\database\VelocityDatabaseManager.java
|
||||
C:\Users\Simon.Speedy\Documents\dev projekte\Minecarft\Tools\Log\velocity-plugin\src\main\java\de\simolzimol\mclogger\velocity\listeners\VelocityEventListener.java
|
||||
C:\Users\Simon.Speedy\Documents\dev projekte\Minecarft\Tools\Log\velocity-plugin\src\main\java\de\simolzimol\mclogger\velocity\VelocityLoggerPlugin.java
|
||||
C:\Users\Menuette\Documents\Programme\MClogger\velocity-plugin\src\main\java\de\simolzimol\mclogger\velocity\database\VelocityDatabaseManager.java
|
||||
C:\Users\Menuette\Documents\Programme\MClogger\velocity-plugin\src\main\java\de\simolzimol\mclogger\velocity\listeners\VelocityEventListener.java
|
||||
C:\Users\Menuette\Documents\Programme\MClogger\velocity-plugin\src\main\java\de\simolzimol\mclogger\velocity\VelocityLoggerPlugin.java
|
||||
|
||||
BIN
velocity-plugin/target/mclogger-velocity-1.0.0-shaded.jar
Normal file
BIN
velocity-plugin/target/mclogger-velocity-1.0.0-shaded.jar
Normal file
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -8,6 +8,7 @@ from datetime import datetime
|
||||
from flask import Flask, abort, render_template, request, session, url_for
|
||||
from config import Config
|
||||
from panel_db import init_databases, get_user_groups
|
||||
from roles import can_manage_group
|
||||
|
||||
from blueprints.auth import auth
|
||||
from blueprints.site_admin import site_admin
|
||||
@@ -89,7 +90,7 @@ def create_app() -> Flask:
|
||||
links.append({"label": "Panel Dashboard", "href": url_for("panel.dashboard"), "btn": "btn-success"})
|
||||
if is_site_admin:
|
||||
links.append({"label": "Site Admin", "href": url_for("site_admin.dashboard"), "btn": "btn-outline-danger"})
|
||||
if role == "admin" and not is_site_admin:
|
||||
if can_manage_group(role) and not is_site_admin:
|
||||
links.append({"label": "Group Admin", "href": url_for("group_admin.dashboard"), "btn": "btn-outline-warning"})
|
||||
|
||||
return render_template(
|
||||
|
||||
@@ -126,5 +126,5 @@ def _apply_group(group):
|
||||
perms = {}
|
||||
session["group_id"] = group["id"]
|
||||
session["group_name"] = group["name"]
|
||||
session["role"] = group.get("role", "member")
|
||||
session["role"] = group.get("role", "viewer")
|
||||
session["permissions"] = perms
|
||||
|
||||
@@ -3,11 +3,13 @@ MCLogger – Gruppen-Admin-Bereich
|
||||
Gruppen-Admins können ihre Mitglieder und MC-DB-Verbindung verwalten.
|
||||
"""
|
||||
import json
|
||||
from datetime import datetime, timedelta
|
||||
from functools import wraps
|
||||
from flask import Blueprint, render_template, request, redirect, url_for, session, flash
|
||||
from config import Config
|
||||
from mailer import send_mail
|
||||
import panel_db as db
|
||||
from roles import GROUP_MANAGEMENT_ROLES, GROUP_ROLE_OPTIONS, GROUP_ROLE_SET, role_label
|
||||
|
||||
group_admin = Blueprint("group_admin", __name__, url_prefix="/group-admin")
|
||||
|
||||
@@ -32,7 +34,7 @@ def group_admin_required(f):
|
||||
return redirect(url_for("auth.login"))
|
||||
if session.get("is_site_admin"):
|
||||
return redirect(url_for("site_admin.dashboard"))
|
||||
if session.get("role") != "admin":
|
||||
if session.get("role") not in GROUP_MANAGEMENT_ROLES:
|
||||
flash("You do not have group admin permission.", "danger")
|
||||
return redirect(url_for("panel.dashboard"))
|
||||
return f(*args, **kwargs)
|
||||
@@ -48,7 +50,7 @@ def dashboard():
|
||||
has_db = db.has_db_configured(group_id)
|
||||
stats = {
|
||||
"member_count": len(members),
|
||||
"admin_count": sum(1 for m in members if m.get("role") == "admin"),
|
||||
"admin_count": sum(1 for m in members if m.get("role") in GROUP_MANAGEMENT_ROLES),
|
||||
"db_configured": bool(has_db),
|
||||
}
|
||||
return render_template("group_admin/dashboard.html",
|
||||
@@ -71,7 +73,9 @@ def members():
|
||||
non_members = [u for u in all_users if u["id"] not in member_ids and not u["is_site_admin"]]
|
||||
return render_template("group_admin/members.html",
|
||||
group=group, members=members, non_members=non_members, pending_invites=pending_invites,
|
||||
all_permissions=ALL_PERMISSIONS)
|
||||
all_permissions=ALL_PERMISSIONS,
|
||||
role_options=GROUP_ROLE_OPTIONS,
|
||||
role_label=role_label)
|
||||
|
||||
|
||||
@group_admin.route("/members/add", methods=["POST"])
|
||||
@@ -79,7 +83,10 @@ def members():
|
||||
def member_add():
|
||||
group_id = session["group_id"]
|
||||
user_id = request.form.get("user_id", type=int)
|
||||
role = request.form.get("role", "member")
|
||||
role = request.form.get("role", "viewer")
|
||||
if role not in GROUP_ROLE_SET:
|
||||
flash("Invalid role selected.", "danger")
|
||||
return redirect(url_for("group_admin.members"))
|
||||
if user_id:
|
||||
db.add_group_member(user_id, group_id, role)
|
||||
flash("Member added.", "success")
|
||||
@@ -92,7 +99,7 @@ def member_invite():
|
||||
group_id = session["group_id"]
|
||||
username = request.form.get("username", "").strip()
|
||||
email = request.form.get("email", "").strip()
|
||||
role = request.form.get("role", "member")
|
||||
role = request.form.get("role", "viewer")
|
||||
|
||||
if not username or not email:
|
||||
flash("Username and email are required.", "danger")
|
||||
@@ -102,14 +109,22 @@ def member_invite():
|
||||
flash("Please provide a valid email address.", "danger")
|
||||
return redirect(url_for("group_admin.members"))
|
||||
|
||||
if role not in {"member", "admin"}:
|
||||
if role not in GROUP_ROLE_SET:
|
||||
flash("Invalid role selected.", "danger")
|
||||
return redirect(url_for("group_admin.members"))
|
||||
|
||||
if db.count_active_group_invites(group_id) >= Config.INVITE_MAX_ACTIVE_PER_GROUP:
|
||||
flash("Active invite limit reached for this group. Revoke old invites or wait for expiry.", "danger")
|
||||
return redirect(url_for("group_admin.members"))
|
||||
|
||||
if db.get_user_by_username(username):
|
||||
flash("Username already exists.", "danger")
|
||||
return redirect(url_for("group_admin.members"))
|
||||
|
||||
if db.get_active_invite_by_username(group_id, username):
|
||||
flash("There is already an active invitation for this username in the group.", "danger")
|
||||
return redirect(url_for("group_admin.members"))
|
||||
|
||||
if db.get_user_by_email(email):
|
||||
flash("Email address is already in use.", "danger")
|
||||
return redirect(url_for("group_admin.members"))
|
||||
@@ -119,6 +134,7 @@ def member_invite():
|
||||
return redirect(url_for("group_admin.members"))
|
||||
|
||||
token = db.create_group_invite(group_id, username, email, role, session["user_id"])
|
||||
invite = db.get_invite_by_token(token)
|
||||
invite_url = url_for("auth.accept_invite", token=token, _external=True)
|
||||
mail_settings = db.get_site_mail_settings()
|
||||
|
||||
@@ -126,12 +142,14 @@ def member_invite():
|
||||
subject = f"Invitation to join {session.get('group_name', 'your group')}"
|
||||
text_body = (
|
||||
f"Hello {username},\n\n"
|
||||
f"You have been invited to join the group '{session.get('group_name', 'your group')}' on MCLogger as {role}.\n"
|
||||
f"You have been invited to join the group '{session.get('group_name', 'your group')}' on MCLogger as {role_label(role)}.\n"
|
||||
f"Open this link to create your account:\n\n{invite_url}\n\n"
|
||||
f"This invite expires in {Config.INVITE_EXPIRY_HOURS} hours.\n"
|
||||
)
|
||||
try:
|
||||
send_mail(mail_settings, email, subject, text_body)
|
||||
if invite:
|
||||
db.mark_group_invite_sent(invite["id"], group_id)
|
||||
flash(f"Invitation email sent to '{email}'.", "success")
|
||||
except Exception:
|
||||
flash(f"Invitation created, but email delivery failed. Share this link manually: {invite_url}", "warning")
|
||||
@@ -140,6 +158,46 @@ def member_invite():
|
||||
return redirect(url_for("group_admin.members"))
|
||||
|
||||
|
||||
@group_admin.route("/invites/<int:invite_id>/resend", methods=["POST"])
|
||||
@group_admin_required
|
||||
def resend_invite(invite_id):
|
||||
group_id = session["group_id"]
|
||||
invite = db.get_group_invite_by_id(invite_id, group_id)
|
||||
if not invite:
|
||||
flash("Invitation not found.", "danger")
|
||||
return redirect(url_for("group_admin.members"))
|
||||
|
||||
if invite.get("accepted_at") or invite.get("revoked_at") or invite["expires_at"] <= datetime.utcnow():
|
||||
flash("Invitation is no longer active.", "danger")
|
||||
return redirect(url_for("group_admin.members"))
|
||||
|
||||
last_sent_at = invite.get("last_sent_at")
|
||||
if last_sent_at and (datetime.utcnow() - last_sent_at) < timedelta(seconds=Config.INVITE_RESEND_COOLDOWN_SECONDS):
|
||||
flash("Please wait before resending this invite again.", "warning")
|
||||
return redirect(url_for("group_admin.members"))
|
||||
|
||||
mail_settings = db.get_site_mail_settings()
|
||||
if not mail_settings:
|
||||
flash("No SMTP settings configured by Site Admin.", "danger")
|
||||
return redirect(url_for("group_admin.members"))
|
||||
|
||||
invite_url = url_for("auth.accept_invite", token=invite["token"], _external=True)
|
||||
subject = f"Invitation to join {session.get('group_name', 'your group')}"
|
||||
text_body = (
|
||||
f"Hello {invite['invited_username']},\n\n"
|
||||
f"You have been invited to join the group '{session.get('group_name', 'your group')}' on MCLogger as {role_label(invite['role'])}.\n"
|
||||
f"Open this link to create your account:\n\n{invite_url}\n\n"
|
||||
f"This invite expires on {invite['expires_at']}.\n"
|
||||
)
|
||||
try:
|
||||
send_mail(mail_settings, invite["invited_email"], subject, text_body)
|
||||
db.mark_group_invite_sent(invite_id, group_id)
|
||||
flash("Invitation email resent.", "success")
|
||||
except Exception:
|
||||
flash("Resend failed. Please verify SMTP settings and try again.", "danger")
|
||||
return redirect(url_for("group_admin.members"))
|
||||
|
||||
|
||||
@group_admin.route("/invites/<int:invite_id>/revoke", methods=["POST"])
|
||||
@group_admin_required
|
||||
def revoke_invite(invite_id):
|
||||
@@ -163,7 +221,10 @@ def member_edit(user_id):
|
||||
current_perms = json.loads(raw_perms) if isinstance(raw_perms, str) else (raw_perms or {})
|
||||
|
||||
if request.method == "POST":
|
||||
role = request.form.get("role", "member")
|
||||
role = request.form.get("role", "viewer")
|
||||
if role not in GROUP_ROLE_SET:
|
||||
flash("Invalid role selected.", "danger")
|
||||
return redirect(url_for("group_admin.members"))
|
||||
new_perms = {key: bool(request.form.get(f"perm_{key}")) for key, _ in ALL_PERMISSIONS}
|
||||
db.update_member(user_id, group_id, role, new_perms)
|
||||
flash("Permissions updated.", "success")
|
||||
@@ -171,7 +232,9 @@ def member_edit(user_id):
|
||||
|
||||
return render_template("group_admin/member_edit.html",
|
||||
group=group, user=user, member=member,
|
||||
current_perms=current_perms, all_permissions=ALL_PERMISSIONS)
|
||||
current_perms=current_perms, all_permissions=ALL_PERMISSIONS,
|
||||
role_options=GROUP_ROLE_OPTIONS,
|
||||
role_label=role_label)
|
||||
|
||||
|
||||
@group_admin.route("/members/<int:user_id>/remove", methods=["POST"])
|
||||
|
||||
@@ -9,6 +9,7 @@ from flask import Blueprint, render_template, request, redirect, url_for, sessio
|
||||
import pymysql
|
||||
import pymysql.cursors
|
||||
import panel_db as pdb
|
||||
from roles import can_manage_group
|
||||
|
||||
panel = Blueprint("panel", __name__)
|
||||
|
||||
@@ -34,7 +35,7 @@ def perm_required(perm):
|
||||
def decorator(f):
|
||||
@wraps(f)
|
||||
def wrapped(*args, **kwargs):
|
||||
if session.get("is_site_admin") or session.get("role") == "admin":
|
||||
if session.get("is_site_admin") or can_manage_group(session.get("role")):
|
||||
return f(*args, **kwargs)
|
||||
perms = session.get("permissions", {})
|
||||
if not perms.get(perm, False):
|
||||
@@ -191,7 +192,7 @@ def player_detail(uuid):
|
||||
flash("Player not found.", "danger")
|
||||
return redirect(url_for("panel.players"))
|
||||
perms = session.get("permissions", {})
|
||||
is_admin = session.get("is_site_admin") or session.get("role") == "admin"
|
||||
is_admin = session.get("is_site_admin") or can_manage_group(session.get("role"))
|
||||
return render_template("panel/player_detail.html",
|
||||
player=player,
|
||||
sessions = query("SELECT * FROM player_sessions WHERE player_uuid=%s ORDER BY login_time DESC LIMIT 20", (uuid,)),
|
||||
|
||||
@@ -6,6 +6,7 @@ from functools import wraps
|
||||
from flask import Blueprint, render_template, request, redirect, url_for, session, flash
|
||||
from mailer import send_mail
|
||||
import panel_db as db
|
||||
from roles import GROUP_MANAGEMENT_ROLES, GROUP_ROLE_OPTIONS, GROUP_ROLE_SET, role_label
|
||||
|
||||
site_admin = Blueprint("site_admin", __name__, url_prefix="/admin")
|
||||
|
||||
@@ -190,14 +191,20 @@ def group_members(group_id):
|
||||
member_ids = {m["id"] for m in members}
|
||||
non_members = [u for u in all_users if u["id"] not in member_ids]
|
||||
return render_template("admin/group_members.html",
|
||||
group=group, members=members, non_members=non_members)
|
||||
group=group, members=members, non_members=non_members,
|
||||
role_options=GROUP_ROLE_OPTIONS,
|
||||
role_label=role_label,
|
||||
management_roles=GROUP_MANAGEMENT_ROLES)
|
||||
|
||||
|
||||
@site_admin.route("/groups/<int:group_id>/members/add", methods=["POST"])
|
||||
@admin_required
|
||||
def group_member_add(group_id):
|
||||
user_id = request.form.get("user_id", type=int)
|
||||
role = request.form.get("role", "member")
|
||||
role = request.form.get("role", "viewer")
|
||||
if role not in GROUP_ROLE_SET:
|
||||
flash("Invalid role selected.", "danger")
|
||||
return redirect(url_for("site_admin.group_members", group_id=group_id))
|
||||
if user_id:
|
||||
db.add_group_member(user_id, group_id, role)
|
||||
flash("Member added.", "success")
|
||||
@@ -212,13 +219,16 @@ def group_member_remove(group_id, user_id):
|
||||
return redirect(url_for("site_admin.group_members", group_id=group_id))
|
||||
|
||||
|
||||
@site_admin.route("/groups/<int:group_id>/members/<int:user_id>/toggle-role", methods=["POST"])
|
||||
@site_admin.route("/groups/<int:group_id>/members/<int:user_id>/set-role", methods=["POST"])
|
||||
@admin_required
|
||||
def group_member_toggle_role(group_id, user_id):
|
||||
def group_member_set_role(group_id, user_id):
|
||||
member = db.get_group_member(user_id, group_id)
|
||||
if member:
|
||||
import json as _json
|
||||
new_role = "member" if member["role"] == "admin" else "admin"
|
||||
new_role = request.form.get("role", "viewer")
|
||||
if new_role not in GROUP_ROLE_SET:
|
||||
flash("Invalid role selected.", "danger")
|
||||
return redirect(url_for("site_admin.group_members", group_id=group_id))
|
||||
perms = member["permissions"] if isinstance(member["permissions"], dict) else (_json.loads(member["permissions"]) if member["permissions"] else {})
|
||||
db.update_member(user_id, group_id, new_role, perms)
|
||||
flash(f"Role changed to '{new_role}'.", "success")
|
||||
@@ -317,7 +327,7 @@ def view_group(group_id):
|
||||
"view_proxy","view_server_events","view_perms"]}
|
||||
session["group_id"] = group_id
|
||||
session["group_name"] = group["name"]
|
||||
session["role"] = "admin"
|
||||
session["role"] = "group_owner"
|
||||
session["permissions"] = all_perms
|
||||
session["admin_viewing"] = True
|
||||
return redirect(url_for("panel.dashboard"))
|
||||
|
||||
@@ -55,6 +55,8 @@ class Config:
|
||||
|
||||
# ── Standard-Berechtigungen neuer Gruppenmitglieder ───────
|
||||
INVITE_EXPIRY_HOURS = int(os.getenv("INVITE_EXPIRY_HOURS") or "72")
|
||||
INVITE_MAX_ACTIVE_PER_GROUP = int(os.getenv("INVITE_MAX_ACTIVE_PER_GROUP") or "200")
|
||||
INVITE_RESEND_COOLDOWN_SECONDS = int(os.getenv("INVITE_RESEND_COOLDOWN_SECONDS") or "120")
|
||||
|
||||
DEFAULT_PERMISSIONS = {
|
||||
"view_dashboard": True,
|
||||
|
||||
@@ -93,7 +93,7 @@ PANEL_SCHEMA = [
|
||||
id INT AUTO_INCREMENT PRIMARY KEY,
|
||||
user_id INT NOT NULL,
|
||||
group_id INT NOT NULL,
|
||||
role ENUM('admin','member') DEFAULT 'member',
|
||||
role ENUM('group_owner','group_admin','moderator','viewer','auditor','admin','member') DEFAULT 'viewer',
|
||||
permissions JSON,
|
||||
joined_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||
UNIQUE KEY uq_user_group (user_id, group_id),
|
||||
@@ -106,11 +106,13 @@ PANEL_SCHEMA = [
|
||||
group_id INT NOT NULL,
|
||||
invited_username VARCHAR(50) NOT NULL,
|
||||
invited_email VARCHAR(255) NOT NULL,
|
||||
role ENUM('admin','member') DEFAULT 'member',
|
||||
role ENUM('group_owner','group_admin','moderator','viewer','auditor','admin','member') DEFAULT 'viewer',
|
||||
token VARCHAR(128) UNIQUE NOT NULL,
|
||||
created_by_user_id INT NOT NULL,
|
||||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||
expires_at DATETIME NOT NULL,
|
||||
last_sent_at DATETIME NULL,
|
||||
send_count INT NOT NULL DEFAULT 0,
|
||||
accepted_at DATETIME NULL,
|
||||
revoked_at DATETIME NULL,
|
||||
UNIQUE KEY uq_group_pending_invite_email (group_id, invited_email, revoked_at, accepted_at),
|
||||
@@ -153,6 +155,27 @@ def init_databases():
|
||||
with panel.cursor() as cur:
|
||||
for stmt in PANEL_SCHEMA:
|
||||
cur.execute(stmt)
|
||||
# Best-effort migrations for existing installs.
|
||||
try:
|
||||
cur.execute(
|
||||
"ALTER TABLE group_members MODIFY role ENUM('group_owner','group_admin','moderator','viewer','auditor','admin','member') DEFAULT 'viewer'"
|
||||
)
|
||||
except Exception:
|
||||
pass
|
||||
try:
|
||||
cur.execute(
|
||||
"ALTER TABLE group_invites MODIFY role ENUM('group_owner','group_admin','moderator','viewer','auditor','admin','member') DEFAULT 'viewer'"
|
||||
)
|
||||
except Exception:
|
||||
pass
|
||||
try:
|
||||
cur.execute("ALTER TABLE group_invites ADD COLUMN last_sent_at DATETIME NULL")
|
||||
except Exception:
|
||||
pass
|
||||
try:
|
||||
cur.execute("ALTER TABLE group_invites ADD COLUMN send_count INT NOT NULL DEFAULT 0")
|
||||
except Exception:
|
||||
pass
|
||||
finally:
|
||||
panel.close()
|
||||
|
||||
@@ -178,7 +201,7 @@ def create_user(username: str, email: str, password: str, is_site_admin: bool =
|
||||
)
|
||||
|
||||
|
||||
def create_user_for_group(username: str, email: str, password: str, group_id: int, role: str = "member") -> int:
|
||||
def create_user_for_group(username: str, email: str, password: str, group_id: int, role: str = "viewer") -> int:
|
||||
"""Create a non-site-admin user and assign them to a group atomically."""
|
||||
permissions = Config.DEFAULT_PERMISSIONS
|
||||
salt = generate_salt()
|
||||
@@ -210,8 +233,8 @@ def create_group_invite(group_id: int, username: str, email: str, role: str, cre
|
||||
expires_at = datetime.utcnow() + timedelta(hours=Config.INVITE_EXPIRY_HOURS)
|
||||
token = secrets.token_urlsafe(32)
|
||||
_panel_query(
|
||||
"INSERT INTO group_invites (group_id, invited_username, invited_email, role, token, created_by_user_id, expires_at) "
|
||||
"VALUES (%s,%s,%s,%s,%s,%s,%s)",
|
||||
"INSERT INTO group_invites (group_id, invited_username, invited_email, role, token, created_by_user_id, expires_at, last_sent_at, send_count) "
|
||||
"VALUES (%s,%s,%s,%s,%s,%s,%s,NULL,0)",
|
||||
(group_id, username, email, role, token, created_by_user_id, expires_at),
|
||||
write=True,
|
||||
)
|
||||
@@ -229,6 +252,15 @@ def list_active_group_invites(group_id: int):
|
||||
)
|
||||
|
||||
|
||||
def count_active_group_invites(group_id: int) -> int:
|
||||
row = _panel_query(
|
||||
"SELECT COUNT(*) AS c FROM group_invites WHERE group_id=%s AND accepted_at IS NULL AND revoked_at IS NULL AND expires_at > UTC_TIMESTAMP()",
|
||||
(group_id,),
|
||||
fetchone=True,
|
||||
)
|
||||
return int(row["c"]) if row else 0
|
||||
|
||||
|
||||
def get_active_invite_by_email(group_id: int, email: str):
|
||||
return _panel_query(
|
||||
"SELECT * FROM group_invites WHERE group_id=%s AND invited_email=%s "
|
||||
@@ -238,6 +270,23 @@ def get_active_invite_by_email(group_id: int, email: str):
|
||||
)
|
||||
|
||||
|
||||
def get_active_invite_by_username(group_id: int, username: str):
|
||||
return _panel_query(
|
||||
"SELECT * FROM group_invites WHERE group_id=%s AND invited_username=%s "
|
||||
"AND accepted_at IS NULL AND revoked_at IS NULL AND expires_at > UTC_TIMESTAMP()",
|
||||
(group_id, username),
|
||||
fetchone=True,
|
||||
)
|
||||
|
||||
|
||||
def get_group_invite_by_id(invite_id: int, group_id: int):
|
||||
return _panel_query(
|
||||
"SELECT * FROM group_invites WHERE id=%s AND group_id=%s",
|
||||
(invite_id, group_id),
|
||||
fetchone=True,
|
||||
)
|
||||
|
||||
|
||||
def get_invite_by_token(token: str):
|
||||
return _panel_query(
|
||||
"SELECT gi.*, g.name AS group_name, u.username AS created_by_username "
|
||||
@@ -258,6 +307,14 @@ def revoke_group_invite(invite_id: int, group_id: int):
|
||||
)
|
||||
|
||||
|
||||
def mark_group_invite_sent(invite_id: int, group_id: int):
|
||||
_panel_query(
|
||||
"UPDATE group_invites SET last_sent_at=UTC_TIMESTAMP(), send_count=send_count+1 WHERE id=%s AND group_id=%s",
|
||||
(invite_id, group_id),
|
||||
write=True,
|
||||
)
|
||||
|
||||
|
||||
def accept_group_invite(token: str, password: str) -> dict | None:
|
||||
invite = get_invite_by_token(token)
|
||||
if not invite:
|
||||
@@ -422,7 +479,7 @@ def get_group_members(group_id: int):
|
||||
)
|
||||
|
||||
|
||||
def add_group_member(user_id: int, group_id: int, role: str = "member", permissions: dict = None):
|
||||
def add_group_member(user_id: int, group_id: int, role: str = "viewer", permissions: dict = None):
|
||||
if permissions is None:
|
||||
permissions = Config.DEFAULT_PERMISSIONS
|
||||
_panel_query(
|
||||
|
||||
29
web/roles.py
Normal file
29
web/roles.py
Normal file
@@ -0,0 +1,29 @@
|
||||
# Legacy values (admin/member) are kept for backward compatibility.
|
||||
GROUP_ROLE_LABELS = {
|
||||
"group_owner": "Group Owner",
|
||||
"group_admin": "Group Admin",
|
||||
"moderator": "Moderator",
|
||||
"viewer": "Viewer",
|
||||
"auditor": "Auditor",
|
||||
"admin": "Admin",
|
||||
"member": "Member",
|
||||
}
|
||||
|
||||
GROUP_ROLE_OPTIONS = [
|
||||
("group_owner", GROUP_ROLE_LABELS["group_owner"]),
|
||||
("group_admin", GROUP_ROLE_LABELS["group_admin"]),
|
||||
("moderator", GROUP_ROLE_LABELS["moderator"]),
|
||||
("viewer", GROUP_ROLE_LABELS["viewer"]),
|
||||
("auditor", GROUP_ROLE_LABELS["auditor"]),
|
||||
]
|
||||
|
||||
GROUP_ROLE_SET = {role for role, _ in GROUP_ROLE_OPTIONS} | {"admin", "member"}
|
||||
GROUP_MANAGEMENT_ROLES = {"group_owner", "group_admin", "admin"}
|
||||
|
||||
|
||||
def can_manage_group(role: str | None) -> bool:
|
||||
return role in GROUP_MANAGEMENT_ROLES
|
||||
|
||||
|
||||
def role_label(role: str | None) -> str:
|
||||
return GROUP_ROLE_LABELS.get(role or "", "Unknown")
|
||||
@@ -57,7 +57,7 @@
|
||||
<p class="mb-0 text-secondary">You are currently not signed in. Start from the login page.</p>
|
||||
{% elif is_site_admin and not session.get('group_id') %}
|
||||
<p class="mb-0 text-secondary">You are signed in as Site Admin. You can manage groups and users from there.</p>
|
||||
{% elif role == 'admin' %}
|
||||
{% elif role in ['group_owner', 'group_admin', 'admin'] %}
|
||||
<p class="mb-0 text-secondary">You are a group admin. Use Panel or Group Admin to return to valid sections.</p>
|
||||
{% else %}
|
||||
<p class="mb-0 text-secondary">Use the dashboard to navigate back to known sections.</p>
|
||||
|
||||
@@ -9,7 +9,7 @@
|
||||
</div>
|
||||
|
||||
<div class="row g-3">
|
||||
<!-- Aktuelle Mitglieder -->
|
||||
<!-- Current members -->
|
||||
<div class="col-md-7">
|
||||
<div class="card border-secondary">
|
||||
<div class="card-header"><i class="bi bi-people-fill me-2"></i>Current Members ({{ members|length }})</div>
|
||||
@@ -21,17 +21,22 @@
|
||||
<tr>
|
||||
<td>{{ m.username }}</td>
|
||||
<td>
|
||||
{% if m.role == 'admin' %}
|
||||
<span class="badge bg-warning text-dark"><i class="bi bi-star-fill me-1"></i>Admin</span>
|
||||
{% if m.role in management_roles %}
|
||||
<span class="badge bg-warning text-dark"><i class="bi bi-star-fill me-1"></i>{{ role_label(m.role) }}</span>
|
||||
{% else %}
|
||||
<span class="badge bg-secondary">Member</span>
|
||||
<span class="badge bg-secondary">{{ role_label(m.role) }}</span>
|
||||
{% endif %}
|
||||
</td>
|
||||
<td class="text-end">
|
||||
<form method="post" action="{{ url_for('site_admin.group_member_toggle_role', group_id=group.id, user_id=m.id) }}" class="d-inline">
|
||||
<form method="post" action="{{ url_for('site_admin.group_member_set_role', group_id=group.id, user_id=m.id) }}" class="d-inline-flex align-items-center gap-1">
|
||||
<input type="hidden" name="_csrf_token" value="{{ csrf_token() }}">
|
||||
<button type="submit" class="btn btn-sm btn-outline-warning" title="Toggle role">
|
||||
<i class="bi bi-arrow-left-right"></i>
|
||||
<select name="role" class="form-select form-select-sm" style="width: 150px;">
|
||||
{% for role, label in role_options %}
|
||||
<option value="{{ role }}" {{ 'selected' if m.role == role }}>{{ label }}</option>
|
||||
{% endfor %}
|
||||
</select>
|
||||
<button type="submit" class="btn btn-sm btn-outline-warning" title="Set role">
|
||||
<i class="bi bi-check2"></i>
|
||||
</button>
|
||||
</form>
|
||||
<form method="post" action="{{ url_for('site_admin.group_member_remove', group_id=group.id, user_id=m.id) }}" class="d-inline"
|
||||
@@ -52,7 +57,7 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Benutzer hinzufügen -->
|
||||
<!-- Add user -->
|
||||
<div class="col-md-5">
|
||||
<div class="card border-secondary">
|
||||
<div class="card-header"><i class="bi bi-person-plus-fill me-2"></i>Add User</div>
|
||||
@@ -71,8 +76,9 @@
|
||||
<div class="mb-3">
|
||||
<label class="form-label">Role</label>
|
||||
<select name="role" class="form-select">
|
||||
<option value="member">Member</option>
|
||||
<option value="admin">Admin</option>
|
||||
{% for role, label in role_options %}
|
||||
<option value="{{ role }}" {{ 'selected' if role == 'viewer' }}>{{ label }}</option>
|
||||
{% endfor %}
|
||||
</select>
|
||||
</div>
|
||||
<button type="submit" class="btn btn-success w-100">
|
||||
|
||||
@@ -21,7 +21,7 @@
|
||||
<td>
|
||||
{% for g in u.groups %}
|
||||
<span class="badge bg-secondary me-1">{{ g.name }}
|
||||
{% if g.role == 'admin' %}<i class="bi bi-star-fill ms-1 text-warning"></i>{% endif %}
|
||||
{% if g.role in ['group_owner', 'group_admin', 'admin'] %}<i class="bi bi-star-fill ms-1 text-warning"></i>{% endif %}
|
||||
</span>
|
||||
{% else %}<span class="text-muted small">None</span>{% endfor %}
|
||||
</td>
|
||||
|
||||
@@ -21,7 +21,7 @@
|
||||
</div>
|
||||
|
||||
{% set perms = session.get('permissions', {}) %}
|
||||
{% set is_admin = session.get('is_site_admin') or session.get('role') == 'admin' %}
|
||||
{% set is_admin = session.get('is_site_admin') or session.get('role') in ['group_owner', 'group_admin', 'admin'] %}
|
||||
|
||||
<ul class="nav flex-column gap-1">
|
||||
{% if perms.get('view_dashboard', True) or is_admin %}
|
||||
@@ -115,7 +115,7 @@
|
||||
{% endif %}
|
||||
|
||||
<!-- Admin-Links -->
|
||||
{% if session.get('role') == 'admin' and not session.get('is_site_admin') %}
|
||||
{% if session.get('role') in ['group_owner', 'group_admin', 'admin'] and not session.get('is_site_admin') %}
|
||||
<a href="{{ url_for('group_admin.dashboard') }}" class="btn btn-outline-warning btn-sm mb-1">
|
||||
<i class="bi bi-gear-fill"></i> <span>Manage Group</span>
|
||||
</a>
|
||||
|
||||
@@ -20,10 +20,11 @@
|
||||
<div class="mb-3">
|
||||
<label class="form-label">Role</label>
|
||||
<select name="role" class="form-select">
|
||||
<option value="member" {{ 'selected' if member.role == 'member' }}>Member</option>
|
||||
<option value="admin" {{ 'selected' if member.role == 'admin' }}>Admin</option>
|
||||
{% for role, label in role_options %}
|
||||
<option value="{{ role }}" {{ 'selected' if member.role == role }}>{{ label }}</option>
|
||||
{% endfor %}
|
||||
</select>
|
||||
<div class="form-text">Admins can manage members and the DB connection.</div>
|
||||
<div class="form-text">Group Owner and Group Admin can manage members and database settings.</div>
|
||||
</div>
|
||||
|
||||
<hr>
|
||||
|
||||
@@ -16,10 +16,10 @@
|
||||
<tr>
|
||||
<td>{{ m.username }}</td>
|
||||
<td>
|
||||
{% if m.role == 'admin' %}
|
||||
<span class="badge bg-warning text-dark"><i class="bi bi-star-fill me-1"></i>Admin</span>
|
||||
{% if m.role in ['group_owner', 'group_admin', 'admin'] %}
|
||||
<span class="badge bg-warning text-dark"><i class="bi bi-star-fill me-1"></i>{{ role_label(m.role) }}</span>
|
||||
{% else %}
|
||||
<span class="badge bg-secondary">Member</span>
|
||||
<span class="badge bg-secondary">{{ role_label(m.role) }}</span>
|
||||
{% endif %}
|
||||
</td>
|
||||
<td class="text-end">
|
||||
@@ -61,17 +61,24 @@
|
||||
<div class="small text-muted" id="invite-link-{{ invite.id }}">{{ invite.invited_email }}</div>
|
||||
</td>
|
||||
<td>
|
||||
{% if invite.role == 'admin' %}
|
||||
<span class="badge bg-warning text-dark"><i class="bi bi-star-fill me-1"></i>Admin</span>
|
||||
{% if invite.role in ['group_owner', 'group_admin', 'admin'] %}
|
||||
<span class="badge bg-warning text-dark"><i class="bi bi-star-fill me-1"></i>{{ role_label(invite.role) }}</span>
|
||||
{% else %}
|
||||
<span class="badge bg-secondary">Member</span>
|
||||
<span class="badge bg-secondary">{{ role_label(invite.role) }}</span>
|
||||
{% endif %}
|
||||
<div class="small text-muted mt-1">Sent: {{ invite.send_count or 0 }}</div>
|
||||
</td>
|
||||
<td class="small text-muted">{{ invite.expires_at | fmt_dt }}</td>
|
||||
<td class="text-end">
|
||||
<button type="button" class="btn btn-sm btn-outline-primary copy-btn" data-target="#invite-url-{{ invite.id }}" title="Copy invite link">
|
||||
<i class="bi bi-clipboard"></i>
|
||||
</button>
|
||||
<form method="post" action="{{ url_for('group_admin.resend_invite', invite_id=invite.id) }}" class="d-inline">
|
||||
<input type="hidden" name="_csrf_token" value="{{ csrf_token() }}">
|
||||
<button type="submit" class="btn btn-sm btn-outline-info" title="Resend">
|
||||
<i class="bi bi-send"></i>
|
||||
</button>
|
||||
</form>
|
||||
<form method="post" action="{{ url_for('group_admin.revoke_invite', invite_id=invite.id) }}" class="d-inline"
|
||||
onsubmit="return confirm('Revoke invitation for {{ invite.invited_username }}?')">
|
||||
<input type="hidden" name="_csrf_token" value="{{ csrf_token() }}">
|
||||
@@ -110,8 +117,9 @@
|
||||
<div class="mb-3">
|
||||
<label class="form-label">Role</label>
|
||||
<select name="role" class="form-select">
|
||||
<option value="member">Member</option>
|
||||
<option value="admin">Admin</option>
|
||||
{% for role, label in role_options %}
|
||||
<option value="{{ role }}" {{ 'selected' if role == 'viewer' }}>{{ label }}</option>
|
||||
{% endfor %}
|
||||
</select>
|
||||
</div>
|
||||
<button type="submit" class="btn btn-outline-success w-100">
|
||||
@@ -141,8 +149,9 @@
|
||||
<div class="mb-3">
|
||||
<label class="form-label">Role</label>
|
||||
<select name="role" class="form-select">
|
||||
<option value="member">Member</option>
|
||||
<option value="admin">Admin</option>
|
||||
{% for role, label in role_options %}
|
||||
<option value="{{ role }}" {{ 'selected' if role == 'viewer' }}>{{ label }}</option>
|
||||
{% endfor %}
|
||||
</select>
|
||||
</div>
|
||||
<button type="submit" class="btn btn-success w-100">
|
||||
|
||||
@@ -8,13 +8,13 @@
|
||||
<h3 class="mb-3">No database configured</h3>
|
||||
<p class="text-muted mb-4">
|
||||
No MC database has been set up for this group.
|
||||
{% if session.get('role') == 'admin' %}
|
||||
{% if session.get('role') in ['group_owner', 'group_admin', 'admin'] %}
|
||||
You can configure the connection as group admin.
|
||||
{% else %}
|
||||
Please contact your group admin.
|
||||
{% endif %}
|
||||
</p>
|
||||
{% if session.get('role') == 'admin' %}
|
||||
{% if session.get('role') in ['group_owner', 'group_admin', 'admin'] %}
|
||||
<a href="{{ url_for('group_admin.database') }}" class="btn btn-success btn-lg">
|
||||
<i class="bi bi-database-fill-gear me-2"></i>Configure Database
|
||||
</a>
|
||||
|
||||
Reference in New Issue
Block a user