diff --git a/paper-plugin/target/classes/de/simolzimol/mclogger/paper/PaperLoggerPlugin.class b/paper-plugin/target/classes/de/simolzimol/mclogger/paper/PaperLoggerPlugin.class index b0b432d..d8ff90d 100644 Binary files a/paper-plugin/target/classes/de/simolzimol/mclogger/paper/PaperLoggerPlugin.class and b/paper-plugin/target/classes/de/simolzimol/mclogger/paper/PaperLoggerPlugin.class differ diff --git a/paper-plugin/target/classes/de/simolzimol/mclogger/paper/commands/MCLoggerCommand$RsConsumer.class b/paper-plugin/target/classes/de/simolzimol/mclogger/paper/commands/MCLoggerCommand$RsConsumer.class index 51ef32e..1e71e1c 100644 Binary files a/paper-plugin/target/classes/de/simolzimol/mclogger/paper/commands/MCLoggerCommand$RsConsumer.class and b/paper-plugin/target/classes/de/simolzimol/mclogger/paper/commands/MCLoggerCommand$RsConsumer.class differ diff --git a/paper-plugin/target/classes/de/simolzimol/mclogger/paper/commands/MCLoggerCommand.class b/paper-plugin/target/classes/de/simolzimol/mclogger/paper/commands/MCLoggerCommand.class index 76d4855..fb94b29 100644 Binary files a/paper-plugin/target/classes/de/simolzimol/mclogger/paper/commands/MCLoggerCommand.class and b/paper-plugin/target/classes/de/simolzimol/mclogger/paper/commands/MCLoggerCommand.class differ diff --git a/paper-plugin/target/classes/de/simolzimol/mclogger/paper/database/DatabaseManager$ThrowingRunnable.class b/paper-plugin/target/classes/de/simolzimol/mclogger/paper/database/DatabaseManager$ThrowingRunnable.class index 3466ed8..31301cd 100644 Binary files a/paper-plugin/target/classes/de/simolzimol/mclogger/paper/database/DatabaseManager$ThrowingRunnable.class and b/paper-plugin/target/classes/de/simolzimol/mclogger/paper/database/DatabaseManager$ThrowingRunnable.class differ diff --git a/paper-plugin/target/classes/de/simolzimol/mclogger/paper/database/DatabaseManager.class b/paper-plugin/target/classes/de/simolzimol/mclogger/paper/database/DatabaseManager.class index d12bf92..b00b9f8 100644 Binary files a/paper-plugin/target/classes/de/simolzimol/mclogger/paper/database/DatabaseManager.class and b/paper-plugin/target/classes/de/simolzimol/mclogger/paper/database/DatabaseManager.class differ diff --git a/paper-plugin/target/classes/de/simolzimol/mclogger/paper/listeners/BlockListener.class b/paper-plugin/target/classes/de/simolzimol/mclogger/paper/listeners/BlockListener.class index 44b28ff..9a49f96 100644 Binary files a/paper-plugin/target/classes/de/simolzimol/mclogger/paper/listeners/BlockListener.class and b/paper-plugin/target/classes/de/simolzimol/mclogger/paper/listeners/BlockListener.class differ diff --git a/paper-plugin/target/classes/de/simolzimol/mclogger/paper/listeners/EntityListener.class b/paper-plugin/target/classes/de/simolzimol/mclogger/paper/listeners/EntityListener.class index c57f28c..dd77528 100644 Binary files a/paper-plugin/target/classes/de/simolzimol/mclogger/paper/listeners/EntityListener.class and b/paper-plugin/target/classes/de/simolzimol/mclogger/paper/listeners/EntityListener.class differ diff --git a/paper-plugin/target/classes/de/simolzimol/mclogger/paper/listeners/InventoryListener$1.class b/paper-plugin/target/classes/de/simolzimol/mclogger/paper/listeners/InventoryListener$1.class new file mode 100644 index 0000000..c13e7d1 Binary files /dev/null and b/paper-plugin/target/classes/de/simolzimol/mclogger/paper/listeners/InventoryListener$1.class differ diff --git a/paper-plugin/target/classes/de/simolzimol/mclogger/paper/listeners/InventoryListener.class b/paper-plugin/target/classes/de/simolzimol/mclogger/paper/listeners/InventoryListener.class index 46d535f..fe0d1d8 100644 Binary files a/paper-plugin/target/classes/de/simolzimol/mclogger/paper/listeners/InventoryListener.class and b/paper-plugin/target/classes/de/simolzimol/mclogger/paper/listeners/InventoryListener.class differ diff --git a/paper-plugin/target/classes/de/simolzimol/mclogger/paper/listeners/LuckPermsListener$1.class b/paper-plugin/target/classes/de/simolzimol/mclogger/paper/listeners/LuckPermsListener$1.class new file mode 100644 index 0000000..fb67412 Binary files /dev/null and b/paper-plugin/target/classes/de/simolzimol/mclogger/paper/listeners/LuckPermsListener$1.class differ diff --git a/paper-plugin/target/classes/de/simolzimol/mclogger/paper/listeners/LuckPermsListener.class b/paper-plugin/target/classes/de/simolzimol/mclogger/paper/listeners/LuckPermsListener.class index 3c45df6..84bccc3 100644 Binary files a/paper-plugin/target/classes/de/simolzimol/mclogger/paper/listeners/LuckPermsListener.class and b/paper-plugin/target/classes/de/simolzimol/mclogger/paper/listeners/LuckPermsListener.class differ diff --git a/paper-plugin/target/classes/de/simolzimol/mclogger/paper/listeners/PlayerChatCommandListener.class b/paper-plugin/target/classes/de/simolzimol/mclogger/paper/listeners/PlayerChatCommandListener.class index e43476e..efeed14 100644 Binary files a/paper-plugin/target/classes/de/simolzimol/mclogger/paper/listeners/PlayerChatCommandListener.class and b/paper-plugin/target/classes/de/simolzimol/mclogger/paper/listeners/PlayerChatCommandListener.class differ diff --git a/paper-plugin/target/classes/de/simolzimol/mclogger/paper/listeners/PlayerDeathListener.class b/paper-plugin/target/classes/de/simolzimol/mclogger/paper/listeners/PlayerDeathListener.class index f44b639..8cc798d 100644 Binary files a/paper-plugin/target/classes/de/simolzimol/mclogger/paper/listeners/PlayerDeathListener.class and b/paper-plugin/target/classes/de/simolzimol/mclogger/paper/listeners/PlayerDeathListener.class differ diff --git a/paper-plugin/target/classes/de/simolzimol/mclogger/paper/listeners/PlayerMiscListener.class b/paper-plugin/target/classes/de/simolzimol/mclogger/paper/listeners/PlayerMiscListener.class index fbdba30..617a57b 100644 Binary files a/paper-plugin/target/classes/de/simolzimol/mclogger/paper/listeners/PlayerMiscListener.class and b/paper-plugin/target/classes/de/simolzimol/mclogger/paper/listeners/PlayerMiscListener.class differ diff --git a/paper-plugin/target/classes/de/simolzimol/mclogger/paper/listeners/PlayerSessionListener.class b/paper-plugin/target/classes/de/simolzimol/mclogger/paper/listeners/PlayerSessionListener.class index e18deb8..6e35d91 100644 Binary files a/paper-plugin/target/classes/de/simolzimol/mclogger/paper/listeners/PlayerSessionListener.class and b/paper-plugin/target/classes/de/simolzimol/mclogger/paper/listeners/PlayerSessionListener.class differ diff --git a/paper-plugin/target/classes/de/simolzimol/mclogger/paper/listeners/WorldListener.class b/paper-plugin/target/classes/de/simolzimol/mclogger/paper/listeners/WorldListener.class index c69e54b..4ac21d4 100644 Binary files a/paper-plugin/target/classes/de/simolzimol/mclogger/paper/listeners/WorldListener.class and b/paper-plugin/target/classes/de/simolzimol/mclogger/paper/listeners/WorldListener.class differ diff --git a/paper-plugin/target/maven-status/maven-compiler-plugin/compile/default-compile/inputFiles.lst b/paper-plugin/target/maven-status/maven-compiler-plugin/compile/default-compile/inputFiles.lst index 4f0a330..4029fe6 100644 --- a/paper-plugin/target/maven-status/maven-compiler-plugin/compile/default-compile/inputFiles.lst +++ b/paper-plugin/target/maven-status/maven-compiler-plugin/compile/default-compile/inputFiles.lst @@ -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 diff --git a/paper-plugin/target/mclogger-paper-1.0.0-shaded.jar b/paper-plugin/target/mclogger-paper-1.0.0-shaded.jar new file mode 100644 index 0000000..f1547f5 Binary files /dev/null and b/paper-plugin/target/mclogger-paper-1.0.0-shaded.jar differ diff --git a/paper-plugin/target/mclogger-paper-1.0.0.jar b/paper-plugin/target/mclogger-paper-1.0.0.jar index 67b1afc..f1547f5 100644 Binary files a/paper-plugin/target/mclogger-paper-1.0.0.jar and b/paper-plugin/target/mclogger-paper-1.0.0.jar differ diff --git a/paper-plugin/target/original-mclogger-paper-1.0.0.jar b/paper-plugin/target/original-mclogger-paper-1.0.0.jar index 31841ea..707a36c 100644 Binary files a/paper-plugin/target/original-mclogger-paper-1.0.0.jar and b/paper-plugin/target/original-mclogger-paper-1.0.0.jar differ diff --git a/velocity-plugin/target/classes/de/simolzimol/mclogger/velocity/listeners/VelocityEventListener.class b/velocity-plugin/target/classes/de/simolzimol/mclogger/velocity/listeners/VelocityEventListener.class index d70be93..a6a4f10 100644 Binary files a/velocity-plugin/target/classes/de/simolzimol/mclogger/velocity/listeners/VelocityEventListener.class and b/velocity-plugin/target/classes/de/simolzimol/mclogger/velocity/listeners/VelocityEventListener.class differ diff --git a/velocity-plugin/target/maven-status/maven-compiler-plugin/compile/default-compile/inputFiles.lst b/velocity-plugin/target/maven-status/maven-compiler-plugin/compile/default-compile/inputFiles.lst index 5c5fcdf..81f2be4 100644 --- a/velocity-plugin/target/maven-status/maven-compiler-plugin/compile/default-compile/inputFiles.lst +++ b/velocity-plugin/target/maven-status/maven-compiler-plugin/compile/default-compile/inputFiles.lst @@ -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 diff --git a/velocity-plugin/target/mclogger-velocity-1.0.0-shaded.jar b/velocity-plugin/target/mclogger-velocity-1.0.0-shaded.jar new file mode 100644 index 0000000..2282f9f Binary files /dev/null and b/velocity-plugin/target/mclogger-velocity-1.0.0-shaded.jar differ diff --git a/velocity-plugin/target/mclogger-velocity-1.0.0.jar b/velocity-plugin/target/mclogger-velocity-1.0.0.jar index a99bde3..2282f9f 100644 Binary files a/velocity-plugin/target/mclogger-velocity-1.0.0.jar and b/velocity-plugin/target/mclogger-velocity-1.0.0.jar differ diff --git a/velocity-plugin/target/original-mclogger-velocity-1.0.0.jar b/velocity-plugin/target/original-mclogger-velocity-1.0.0.jar index dcd1ef6..7745133 100644 Binary files a/velocity-plugin/target/original-mclogger-velocity-1.0.0.jar and b/velocity-plugin/target/original-mclogger-velocity-1.0.0.jar differ diff --git a/web/app.py b/web/app.py index ea9b153..f4fdcfa 100644 --- a/web/app.py +++ b/web/app.py @@ -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( diff --git a/web/blueprints/auth.py b/web/blueprints/auth.py index 432c35d..49fe99d 100644 --- a/web/blueprints/auth.py +++ b/web/blueprints/auth.py @@ -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 diff --git a/web/blueprints/group_admin.py b/web/blueprints/group_admin.py index 7bd1f8f..d2555e1 100644 --- a/web/blueprints/group_admin.py +++ b/web/blueprints/group_admin.py @@ -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//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//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//remove", methods=["POST"]) diff --git a/web/blueprints/panel.py b/web/blueprints/panel.py index a66a5f8..7ce3699 100644 --- a/web/blueprints/panel.py +++ b/web/blueprints/panel.py @@ -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,)), diff --git a/web/blueprints/site_admin.py b/web/blueprints/site_admin.py index c242ee2..b2a5d9e 100644 --- a/web/blueprints/site_admin.py +++ b/web/blueprints/site_admin.py @@ -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//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//members//toggle-role", methods=["POST"]) +@site_admin.route("/groups//members//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")) diff --git a/web/config.py b/web/config.py index f33744d..dafd17d 100644 --- a/web/config.py +++ b/web/config.py @@ -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, diff --git a/web/panel_db.py b/web/panel_db.py index 8a348cd..246365b 100644 --- a/web/panel_db.py +++ b/web/panel_db.py @@ -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( diff --git a/web/roles.py b/web/roles.py new file mode 100644 index 0000000..6dfb9c8 --- /dev/null +++ b/web/roles.py @@ -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") diff --git a/web/templates/404.html b/web/templates/404.html index 0d68538..7bfaa14 100644 --- a/web/templates/404.html +++ b/web/templates/404.html @@ -57,7 +57,7 @@

You are currently not signed in. Start from the login page.

{% elif is_site_admin and not session.get('group_id') %}

You are signed in as Site Admin. You can manage groups and users from there.

- {% elif role == 'admin' %} + {% elif role in ['group_owner', 'group_admin', 'admin'] %}

You are a group admin. Use Panel or Group Admin to return to valid sections.

{% else %}

Use the dashboard to navigate back to known sections.

diff --git a/web/templates/admin/group_members.html b/web/templates/admin/group_members.html index a61b2e5..165840a 100644 --- a/web/templates/admin/group_members.html +++ b/web/templates/admin/group_members.html @@ -9,7 +9,7 @@
- +
Current Members ({{ members|length }})
@@ -21,17 +21,22 @@ {{ m.username }} - {% if m.role == 'admin' %} - Admin + {% if m.role in management_roles %} + {{ role_label(m.role) }} {% else %} - Member + {{ role_label(m.role) }} {% endif %} -
+ -
- +
Add User
@@ -71,8 +76,9 @@
{% 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'] %}