modified: .gitignore

This commit is contained in:
simon
2026-04-13 11:44:30 +02:00
parent 63ce0f9c5b
commit d25536e9c4
40 changed files with 248 additions and 69 deletions

View File

@@ -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(

View File

@@ -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

View File

@@ -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"])

View File

@@ -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,)),

View File

@@ -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"))

View File

@@ -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,

View File

@@ -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
View 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")

View File

@@ -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>

View File

@@ -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">

View File

@@ -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>

View File

@@ -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>

View File

@@ -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>

View File

@@ -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">

View File

@@ -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>