modified: .gitignore
This commit is contained in:
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\Menuette\Documents\Programme\MClogger\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\Menuette\Documents\Programme\MClogger\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\Menuette\Documents\Programme\MClogger\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\Menuette\Documents\Programme\MClogger\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\Menuette\Documents\Programme\MClogger\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\Menuette\Documents\Programme\MClogger\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\Menuette\Documents\Programme\MClogger\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\Menuette\Documents\Programme\MClogger\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\Menuette\Documents\Programme\MClogger\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\Menuette\Documents\Programme\MClogger\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\Menuette\Documents\Programme\MClogger\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\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\Menuette\Documents\Programme\MClogger\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\Menuette\Documents\Programme\MClogger\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\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 flask import Flask, abort, render_template, request, session, url_for
|
||||||
from config import Config
|
from config import Config
|
||||||
from panel_db import init_databases, get_user_groups
|
from panel_db import init_databases, get_user_groups
|
||||||
|
from roles import can_manage_group
|
||||||
|
|
||||||
from blueprints.auth import auth
|
from blueprints.auth import auth
|
||||||
from blueprints.site_admin import site_admin
|
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"})
|
links.append({"label": "Panel Dashboard", "href": url_for("panel.dashboard"), "btn": "btn-success"})
|
||||||
if is_site_admin:
|
if is_site_admin:
|
||||||
links.append({"label": "Site Admin", "href": url_for("site_admin.dashboard"), "btn": "btn-outline-danger"})
|
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"})
|
links.append({"label": "Group Admin", "href": url_for("group_admin.dashboard"), "btn": "btn-outline-warning"})
|
||||||
|
|
||||||
return render_template(
|
return render_template(
|
||||||
|
|||||||
@@ -126,5 +126,5 @@ def _apply_group(group):
|
|||||||
perms = {}
|
perms = {}
|
||||||
session["group_id"] = group["id"]
|
session["group_id"] = group["id"]
|
||||||
session["group_name"] = group["name"]
|
session["group_name"] = group["name"]
|
||||||
session["role"] = group.get("role", "member")
|
session["role"] = group.get("role", "viewer")
|
||||||
session["permissions"] = perms
|
session["permissions"] = perms
|
||||||
|
|||||||
@@ -3,11 +3,13 @@ MCLogger – Gruppen-Admin-Bereich
|
|||||||
Gruppen-Admins können ihre Mitglieder und MC-DB-Verbindung verwalten.
|
Gruppen-Admins können ihre Mitglieder und MC-DB-Verbindung verwalten.
|
||||||
"""
|
"""
|
||||||
import json
|
import json
|
||||||
|
from datetime import datetime, timedelta
|
||||||
from functools import wraps
|
from functools import wraps
|
||||||
from flask import Blueprint, render_template, request, redirect, url_for, session, flash
|
from flask import Blueprint, render_template, request, redirect, url_for, session, flash
|
||||||
from config import Config
|
from config import Config
|
||||||
from mailer import send_mail
|
from mailer import send_mail
|
||||||
import panel_db as db
|
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")
|
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"))
|
return redirect(url_for("auth.login"))
|
||||||
if session.get("is_site_admin"):
|
if session.get("is_site_admin"):
|
||||||
return redirect(url_for("site_admin.dashboard"))
|
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")
|
flash("You do not have group admin permission.", "danger")
|
||||||
return redirect(url_for("panel.dashboard"))
|
return redirect(url_for("panel.dashboard"))
|
||||||
return f(*args, **kwargs)
|
return f(*args, **kwargs)
|
||||||
@@ -48,7 +50,7 @@ def dashboard():
|
|||||||
has_db = db.has_db_configured(group_id)
|
has_db = db.has_db_configured(group_id)
|
||||||
stats = {
|
stats = {
|
||||||
"member_count": len(members),
|
"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),
|
"db_configured": bool(has_db),
|
||||||
}
|
}
|
||||||
return render_template("group_admin/dashboard.html",
|
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"]]
|
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",
|
return render_template("group_admin/members.html",
|
||||||
group=group, members=members, non_members=non_members, pending_invites=pending_invites,
|
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"])
|
@group_admin.route("/members/add", methods=["POST"])
|
||||||
@@ -79,7 +83,10 @@ def members():
|
|||||||
def member_add():
|
def member_add():
|
||||||
group_id = session["group_id"]
|
group_id = session["group_id"]
|
||||||
user_id = request.form.get("user_id", type=int)
|
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:
|
if user_id:
|
||||||
db.add_group_member(user_id, group_id, role)
|
db.add_group_member(user_id, group_id, role)
|
||||||
flash("Member added.", "success")
|
flash("Member added.", "success")
|
||||||
@@ -92,7 +99,7 @@ def member_invite():
|
|||||||
group_id = session["group_id"]
|
group_id = session["group_id"]
|
||||||
username = request.form.get("username", "").strip()
|
username = request.form.get("username", "").strip()
|
||||||
email = request.form.get("email", "").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:
|
if not username or not email:
|
||||||
flash("Username and email are required.", "danger")
|
flash("Username and email are required.", "danger")
|
||||||
@@ -102,14 +109,22 @@ def member_invite():
|
|||||||
flash("Please provide a valid email address.", "danger")
|
flash("Please provide a valid email address.", "danger")
|
||||||
return redirect(url_for("group_admin.members"))
|
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")
|
flash("Invalid role selected.", "danger")
|
||||||
return redirect(url_for("group_admin.members"))
|
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):
|
if db.get_user_by_username(username):
|
||||||
flash("Username already exists.", "danger")
|
flash("Username already exists.", "danger")
|
||||||
return redirect(url_for("group_admin.members"))
|
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):
|
if db.get_user_by_email(email):
|
||||||
flash("Email address is already in use.", "danger")
|
flash("Email address is already in use.", "danger")
|
||||||
return redirect(url_for("group_admin.members"))
|
return redirect(url_for("group_admin.members"))
|
||||||
@@ -119,6 +134,7 @@ def member_invite():
|
|||||||
return redirect(url_for("group_admin.members"))
|
return redirect(url_for("group_admin.members"))
|
||||||
|
|
||||||
token = db.create_group_invite(group_id, username, email, role, session["user_id"])
|
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)
|
invite_url = url_for("auth.accept_invite", token=token, _external=True)
|
||||||
mail_settings = db.get_site_mail_settings()
|
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')}"
|
subject = f"Invitation to join {session.get('group_name', 'your group')}"
|
||||||
text_body = (
|
text_body = (
|
||||||
f"Hello {username},\n\n"
|
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"Open this link to create your account:\n\n{invite_url}\n\n"
|
||||||
f"This invite expires in {Config.INVITE_EXPIRY_HOURS} hours.\n"
|
f"This invite expires in {Config.INVITE_EXPIRY_HOURS} hours.\n"
|
||||||
)
|
)
|
||||||
try:
|
try:
|
||||||
send_mail(mail_settings, email, subject, text_body)
|
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")
|
flash(f"Invitation email sent to '{email}'.", "success")
|
||||||
except Exception:
|
except Exception:
|
||||||
flash(f"Invitation created, but email delivery failed. Share this link manually: {invite_url}", "warning")
|
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"))
|
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.route("/invites/<int:invite_id>/revoke", methods=["POST"])
|
||||||
@group_admin_required
|
@group_admin_required
|
||||||
def revoke_invite(invite_id):
|
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 {})
|
current_perms = json.loads(raw_perms) if isinstance(raw_perms, str) else (raw_perms or {})
|
||||||
|
|
||||||
if request.method == "POST":
|
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}
|
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)
|
db.update_member(user_id, group_id, role, new_perms)
|
||||||
flash("Permissions updated.", "success")
|
flash("Permissions updated.", "success")
|
||||||
@@ -171,7 +232,9 @@ def member_edit(user_id):
|
|||||||
|
|
||||||
return render_template("group_admin/member_edit.html",
|
return render_template("group_admin/member_edit.html",
|
||||||
group=group, user=user, member=member,
|
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"])
|
@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
|
||||||
import pymysql.cursors
|
import pymysql.cursors
|
||||||
import panel_db as pdb
|
import panel_db as pdb
|
||||||
|
from roles import can_manage_group
|
||||||
|
|
||||||
panel = Blueprint("panel", __name__)
|
panel = Blueprint("panel", __name__)
|
||||||
|
|
||||||
@@ -34,7 +35,7 @@ def perm_required(perm):
|
|||||||
def decorator(f):
|
def decorator(f):
|
||||||
@wraps(f)
|
@wraps(f)
|
||||||
def wrapped(*args, **kwargs):
|
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)
|
return f(*args, **kwargs)
|
||||||
perms = session.get("permissions", {})
|
perms = session.get("permissions", {})
|
||||||
if not perms.get(perm, False):
|
if not perms.get(perm, False):
|
||||||
@@ -191,7 +192,7 @@ def player_detail(uuid):
|
|||||||
flash("Player not found.", "danger")
|
flash("Player not found.", "danger")
|
||||||
return redirect(url_for("panel.players"))
|
return redirect(url_for("panel.players"))
|
||||||
perms = session.get("permissions", {})
|
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",
|
return render_template("panel/player_detail.html",
|
||||||
player=player,
|
player=player,
|
||||||
sessions = query("SELECT * FROM player_sessions WHERE player_uuid=%s ORDER BY login_time DESC LIMIT 20", (uuid,)),
|
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 flask import Blueprint, render_template, request, redirect, url_for, session, flash
|
||||||
from mailer import send_mail
|
from mailer import send_mail
|
||||||
import panel_db as db
|
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")
|
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}
|
member_ids = {m["id"] for m in members}
|
||||||
non_members = [u for u in all_users if u["id"] not in member_ids]
|
non_members = [u for u in all_users if u["id"] not in member_ids]
|
||||||
return render_template("admin/group_members.html",
|
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"])
|
@site_admin.route("/groups/<int:group_id>/members/add", methods=["POST"])
|
||||||
@admin_required
|
@admin_required
|
||||||
def group_member_add(group_id):
|
def group_member_add(group_id):
|
||||||
user_id = request.form.get("user_id", type=int)
|
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:
|
if user_id:
|
||||||
db.add_group_member(user_id, group_id, role)
|
db.add_group_member(user_id, group_id, role)
|
||||||
flash("Member added.", "success")
|
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))
|
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
|
@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)
|
member = db.get_group_member(user_id, group_id)
|
||||||
if member:
|
if member:
|
||||||
import json as _json
|
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 {})
|
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)
|
db.update_member(user_id, group_id, new_role, perms)
|
||||||
flash(f"Role changed to '{new_role}'.", "success")
|
flash(f"Role changed to '{new_role}'.", "success")
|
||||||
@@ -317,7 +327,7 @@ def view_group(group_id):
|
|||||||
"view_proxy","view_server_events","view_perms"]}
|
"view_proxy","view_server_events","view_perms"]}
|
||||||
session["group_id"] = group_id
|
session["group_id"] = group_id
|
||||||
session["group_name"] = group["name"]
|
session["group_name"] = group["name"]
|
||||||
session["role"] = "admin"
|
session["role"] = "group_owner"
|
||||||
session["permissions"] = all_perms
|
session["permissions"] = all_perms
|
||||||
session["admin_viewing"] = True
|
session["admin_viewing"] = True
|
||||||
return redirect(url_for("panel.dashboard"))
|
return redirect(url_for("panel.dashboard"))
|
||||||
|
|||||||
@@ -55,6 +55,8 @@ class Config:
|
|||||||
|
|
||||||
# ── Standard-Berechtigungen neuer Gruppenmitglieder ───────
|
# ── Standard-Berechtigungen neuer Gruppenmitglieder ───────
|
||||||
INVITE_EXPIRY_HOURS = int(os.getenv("INVITE_EXPIRY_HOURS") or "72")
|
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 = {
|
DEFAULT_PERMISSIONS = {
|
||||||
"view_dashboard": True,
|
"view_dashboard": True,
|
||||||
|
|||||||
@@ -93,7 +93,7 @@ PANEL_SCHEMA = [
|
|||||||
id INT AUTO_INCREMENT PRIMARY KEY,
|
id INT AUTO_INCREMENT PRIMARY KEY,
|
||||||
user_id INT NOT NULL,
|
user_id INT NOT NULL,
|
||||||
group_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,
|
permissions JSON,
|
||||||
joined_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
joined_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||||
UNIQUE KEY uq_user_group (user_id, group_id),
|
UNIQUE KEY uq_user_group (user_id, group_id),
|
||||||
@@ -106,11 +106,13 @@ PANEL_SCHEMA = [
|
|||||||
group_id INT NOT NULL,
|
group_id INT NOT NULL,
|
||||||
invited_username VARCHAR(50) NOT NULL,
|
invited_username VARCHAR(50) NOT NULL,
|
||||||
invited_email VARCHAR(255) 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,
|
token VARCHAR(128) UNIQUE NOT NULL,
|
||||||
created_by_user_id INT NOT NULL,
|
created_by_user_id INT NOT NULL,
|
||||||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||||
expires_at DATETIME NOT NULL,
|
expires_at DATETIME NOT NULL,
|
||||||
|
last_sent_at DATETIME NULL,
|
||||||
|
send_count INT NOT NULL DEFAULT 0,
|
||||||
accepted_at DATETIME NULL,
|
accepted_at DATETIME NULL,
|
||||||
revoked_at DATETIME NULL,
|
revoked_at DATETIME NULL,
|
||||||
UNIQUE KEY uq_group_pending_invite_email (group_id, invited_email, revoked_at, accepted_at),
|
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:
|
with panel.cursor() as cur:
|
||||||
for stmt in PANEL_SCHEMA:
|
for stmt in PANEL_SCHEMA:
|
||||||
cur.execute(stmt)
|
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:
|
finally:
|
||||||
panel.close()
|
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."""
|
"""Create a non-site-admin user and assign them to a group atomically."""
|
||||||
permissions = Config.DEFAULT_PERMISSIONS
|
permissions = Config.DEFAULT_PERMISSIONS
|
||||||
salt = generate_salt()
|
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)
|
expires_at = datetime.utcnow() + timedelta(hours=Config.INVITE_EXPIRY_HOURS)
|
||||||
token = secrets.token_urlsafe(32)
|
token = secrets.token_urlsafe(32)
|
||||||
_panel_query(
|
_panel_query(
|
||||||
"INSERT INTO group_invites (group_id, invited_username, invited_email, role, token, created_by_user_id, expires_at) "
|
"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)",
|
"VALUES (%s,%s,%s,%s,%s,%s,%s,NULL,0)",
|
||||||
(group_id, username, email, role, token, created_by_user_id, expires_at),
|
(group_id, username, email, role, token, created_by_user_id, expires_at),
|
||||||
write=True,
|
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):
|
def get_active_invite_by_email(group_id: int, email: str):
|
||||||
return _panel_query(
|
return _panel_query(
|
||||||
"SELECT * FROM group_invites WHERE group_id=%s AND invited_email=%s "
|
"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):
|
def get_invite_by_token(token: str):
|
||||||
return _panel_query(
|
return _panel_query(
|
||||||
"SELECT gi.*, g.name AS group_name, u.username AS created_by_username "
|
"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:
|
def accept_group_invite(token: str, password: str) -> dict | None:
|
||||||
invite = get_invite_by_token(token)
|
invite = get_invite_by_token(token)
|
||||||
if not invite:
|
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:
|
if permissions is None:
|
||||||
permissions = Config.DEFAULT_PERMISSIONS
|
permissions = Config.DEFAULT_PERMISSIONS
|
||||||
_panel_query(
|
_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>
|
<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') %}
|
{% 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>
|
<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>
|
<p class="mb-0 text-secondary">You are a group admin. Use Panel or Group Admin to return to valid sections.</p>
|
||||||
{% else %}
|
{% else %}
|
||||||
<p class="mb-0 text-secondary">Use the dashboard to navigate back to known sections.</p>
|
<p class="mb-0 text-secondary">Use the dashboard to navigate back to known sections.</p>
|
||||||
|
|||||||
@@ -9,7 +9,7 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="row g-3">
|
<div class="row g-3">
|
||||||
<!-- Aktuelle Mitglieder -->
|
<!-- Current members -->
|
||||||
<div class="col-md-7">
|
<div class="col-md-7">
|
||||||
<div class="card border-secondary">
|
<div class="card border-secondary">
|
||||||
<div class="card-header"><i class="bi bi-people-fill me-2"></i>Current Members ({{ members|length }})</div>
|
<div class="card-header"><i class="bi bi-people-fill me-2"></i>Current Members ({{ members|length }})</div>
|
||||||
@@ -21,17 +21,22 @@
|
|||||||
<tr>
|
<tr>
|
||||||
<td>{{ m.username }}</td>
|
<td>{{ m.username }}</td>
|
||||||
<td>
|
<td>
|
||||||
{% if m.role == 'admin' %}
|
{% if m.role in management_roles %}
|
||||||
<span class="badge bg-warning text-dark"><i class="bi bi-star-fill me-1"></i>Admin</span>
|
<span class="badge bg-warning text-dark"><i class="bi bi-star-fill me-1"></i>{{ role_label(m.role) }}</span>
|
||||||
{% else %}
|
{% else %}
|
||||||
<span class="badge bg-secondary">Member</span>
|
<span class="badge bg-secondary">{{ role_label(m.role) }}</span>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</td>
|
</td>
|
||||||
<td class="text-end">
|
<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() }}">
|
<input type="hidden" name="_csrf_token" value="{{ csrf_token() }}">
|
||||||
<button type="submit" class="btn btn-sm btn-outline-warning" title="Toggle role">
|
<select name="role" class="form-select form-select-sm" style="width: 150px;">
|
||||||
<i class="bi bi-arrow-left-right"></i>
|
{% 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>
|
</button>
|
||||||
</form>
|
</form>
|
||||||
<form method="post" action="{{ url_for('site_admin.group_member_remove', group_id=group.id, user_id=m.id) }}" class="d-inline"
|
<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>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Benutzer hinzufügen -->
|
<!-- Add user -->
|
||||||
<div class="col-md-5">
|
<div class="col-md-5">
|
||||||
<div class="card border-secondary">
|
<div class="card border-secondary">
|
||||||
<div class="card-header"><i class="bi bi-person-plus-fill me-2"></i>Add User</div>
|
<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">
|
<div class="mb-3">
|
||||||
<label class="form-label">Role</label>
|
<label class="form-label">Role</label>
|
||||||
<select name="role" class="form-select">
|
<select name="role" class="form-select">
|
||||||
<option value="member">Member</option>
|
{% for role, label in role_options %}
|
||||||
<option value="admin">Admin</option>
|
<option value="{{ role }}" {{ 'selected' if role == 'viewer' }}>{{ label }}</option>
|
||||||
|
{% endfor %}
|
||||||
</select>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
<button type="submit" class="btn btn-success w-100">
|
<button type="submit" class="btn btn-success w-100">
|
||||||
|
|||||||
@@ -21,7 +21,7 @@
|
|||||||
<td>
|
<td>
|
||||||
{% for g in u.groups %}
|
{% for g in u.groups %}
|
||||||
<span class="badge bg-secondary me-1">{{ g.name }}
|
<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>
|
</span>
|
||||||
{% else %}<span class="text-muted small">None</span>{% endfor %}
|
{% else %}<span class="text-muted small">None</span>{% endfor %}
|
||||||
</td>
|
</td>
|
||||||
|
|||||||
@@ -21,7 +21,7 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
{% set perms = session.get('permissions', {}) %}
|
{% 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">
|
<ul class="nav flex-column gap-1">
|
||||||
{% if perms.get('view_dashboard', True) or is_admin %}
|
{% if perms.get('view_dashboard', True) or is_admin %}
|
||||||
@@ -115,7 +115,7 @@
|
|||||||
{% endif %}
|
{% endif %}
|
||||||
|
|
||||||
<!-- Admin-Links -->
|
<!-- 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">
|
<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>
|
<i class="bi bi-gear-fill"></i> <span>Manage Group</span>
|
||||||
</a>
|
</a>
|
||||||
|
|||||||
@@ -20,10 +20,11 @@
|
|||||||
<div class="mb-3">
|
<div class="mb-3">
|
||||||
<label class="form-label">Role</label>
|
<label class="form-label">Role</label>
|
||||||
<select name="role" class="form-select">
|
<select name="role" class="form-select">
|
||||||
<option value="member" {{ 'selected' if member.role == 'member' }}>Member</option>
|
{% for role, label in role_options %}
|
||||||
<option value="admin" {{ 'selected' if member.role == 'admin' }}>Admin</option>
|
<option value="{{ role }}" {{ 'selected' if member.role == role }}>{{ label }}</option>
|
||||||
|
{% endfor %}
|
||||||
</select>
|
</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>
|
</div>
|
||||||
|
|
||||||
<hr>
|
<hr>
|
||||||
|
|||||||
@@ -16,10 +16,10 @@
|
|||||||
<tr>
|
<tr>
|
||||||
<td>{{ m.username }}</td>
|
<td>{{ m.username }}</td>
|
||||||
<td>
|
<td>
|
||||||
{% if m.role == 'admin' %}
|
{% 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>Admin</span>
|
<span class="badge bg-warning text-dark"><i class="bi bi-star-fill me-1"></i>{{ role_label(m.role) }}</span>
|
||||||
{% else %}
|
{% else %}
|
||||||
<span class="badge bg-secondary">Member</span>
|
<span class="badge bg-secondary">{{ role_label(m.role) }}</span>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</td>
|
</td>
|
||||||
<td class="text-end">
|
<td class="text-end">
|
||||||
@@ -61,17 +61,24 @@
|
|||||||
<div class="small text-muted" id="invite-link-{{ invite.id }}">{{ invite.invited_email }}</div>
|
<div class="small text-muted" id="invite-link-{{ invite.id }}">{{ invite.invited_email }}</div>
|
||||||
</td>
|
</td>
|
||||||
<td>
|
<td>
|
||||||
{% if invite.role == 'admin' %}
|
{% 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>Admin</span>
|
<span class="badge bg-warning text-dark"><i class="bi bi-star-fill me-1"></i>{{ role_label(invite.role) }}</span>
|
||||||
{% else %}
|
{% else %}
|
||||||
<span class="badge bg-secondary">Member</span>
|
<span class="badge bg-secondary">{{ role_label(invite.role) }}</span>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
<div class="small text-muted mt-1">Sent: {{ invite.send_count or 0 }}</div>
|
||||||
</td>
|
</td>
|
||||||
<td class="small text-muted">{{ invite.expires_at | fmt_dt }}</td>
|
<td class="small text-muted">{{ invite.expires_at | fmt_dt }}</td>
|
||||||
<td class="text-end">
|
<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">
|
<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>
|
<i class="bi bi-clipboard"></i>
|
||||||
</button>
|
</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"
|
<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 }}?')">
|
onsubmit="return confirm('Revoke invitation for {{ invite.invited_username }}?')">
|
||||||
<input type="hidden" name="_csrf_token" value="{{ csrf_token() }}">
|
<input type="hidden" name="_csrf_token" value="{{ csrf_token() }}">
|
||||||
@@ -110,8 +117,9 @@
|
|||||||
<div class="mb-3">
|
<div class="mb-3">
|
||||||
<label class="form-label">Role</label>
|
<label class="form-label">Role</label>
|
||||||
<select name="role" class="form-select">
|
<select name="role" class="form-select">
|
||||||
<option value="member">Member</option>
|
{% for role, label in role_options %}
|
||||||
<option value="admin">Admin</option>
|
<option value="{{ role }}" {{ 'selected' if role == 'viewer' }}>{{ label }}</option>
|
||||||
|
{% endfor %}
|
||||||
</select>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
<button type="submit" class="btn btn-outline-success w-100">
|
<button type="submit" class="btn btn-outline-success w-100">
|
||||||
@@ -141,8 +149,9 @@
|
|||||||
<div class="mb-3">
|
<div class="mb-3">
|
||||||
<label class="form-label">Role</label>
|
<label class="form-label">Role</label>
|
||||||
<select name="role" class="form-select">
|
<select name="role" class="form-select">
|
||||||
<option value="member">Member</option>
|
{% for role, label in role_options %}
|
||||||
<option value="admin">Admin</option>
|
<option value="{{ role }}" {{ 'selected' if role == 'viewer' }}>{{ label }}</option>
|
||||||
|
{% endfor %}
|
||||||
</select>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
<button type="submit" class="btn btn-success w-100">
|
<button type="submit" class="btn btn-success w-100">
|
||||||
|
|||||||
@@ -8,13 +8,13 @@
|
|||||||
<h3 class="mb-3">No database configured</h3>
|
<h3 class="mb-3">No database configured</h3>
|
||||||
<p class="text-muted mb-4">
|
<p class="text-muted mb-4">
|
||||||
No MC database has been set up for this group.
|
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.
|
You can configure the connection as group admin.
|
||||||
{% else %}
|
{% else %}
|
||||||
Please contact your group admin.
|
Please contact your group admin.
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</p>
|
</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">
|
<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
|
<i class="bi bi-database-fill-gear me-2"></i>Configure Database
|
||||||
</a>
|
</a>
|
||||||
|
|||||||
Reference in New Issue
Block a user