modified: .gitignore
This commit is contained in:
@@ -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