modified: web/app.py
modified: web/blueprints/auth.py modified: web/blueprints/group_admin.py modified: web/blueprints/panel.py modified: web/blueprints/site_admin.py modified: web/config.py new file: web/templates/404.html modified: web/templates/admin/base.html modified: web/templates/admin/group_edit.html modified: web/templates/admin/group_members.html modified: web/templates/admin/groups.html modified: web/templates/admin/user_edit.html modified: web/templates/admin/users.html modified: web/templates/auth/admin_login.html modified: web/templates/auth/login.html modified: web/templates/base.html modified: web/templates/group_admin/base.html modified: web/templates/group_admin/database.html modified: web/templates/group_admin/member_edit.html modified: web/templates/group_admin/members.html modified: web/templates/login.html modified: web/templates/panel/dashboard.html
This commit is contained in:
73
web/app.py
73
web/app.py
@@ -3,8 +3,9 @@ MCLogger – Flask Web-Panel
|
||||
Multi-Tenant mit Gruppen, Rollen & verschlüsselten DB-Zugangsdaten.
|
||||
Coolify-kompatibel: alle Einstellungen via ENV.
|
||||
"""
|
||||
import secrets
|
||||
from datetime import datetime
|
||||
from flask import Flask, session
|
||||
from flask import Flask, abort, render_template, request, session, url_for
|
||||
from config import Config
|
||||
from panel_db import init_databases, get_user_groups
|
||||
|
||||
@@ -17,6 +18,13 @@ from blueprints.panel import panel
|
||||
def create_app() -> Flask:
|
||||
app = Flask(__name__)
|
||||
app.secret_key = Config.SECRET_KEY
|
||||
app.config.update(
|
||||
SESSION_COOKIE_HTTPONLY=Config.SESSION_COOKIE_HTTPONLY,
|
||||
SESSION_COOKIE_SAMESITE=Config.SESSION_COOKIE_SAMESITE,
|
||||
SESSION_COOKIE_SECURE=Config.SESSION_COOKIE_SECURE,
|
||||
)
|
||||
|
||||
Config.validate_security()
|
||||
|
||||
# Blueprints registrieren
|
||||
app.register_blueprint(auth)
|
||||
@@ -32,6 +40,68 @@ def create_app() -> Flask:
|
||||
|
||||
# ── Template-Filter ───────────────────────────────────────
|
||||
|
||||
def _get_or_create_csrf_token() -> str:
|
||||
token = session.get("_csrf_token")
|
||||
if not token:
|
||||
token = secrets.token_urlsafe(32)
|
||||
session["_csrf_token"] = token
|
||||
return token
|
||||
|
||||
@app.before_request
|
||||
def enforce_csrf():
|
||||
if request.method not in {"POST", "PUT", "PATCH", "DELETE"}:
|
||||
return
|
||||
|
||||
session_token = session.get("_csrf_token")
|
||||
request_token = request.form.get("_csrf_token") or request.headers.get("X-CSRF-Token")
|
||||
if not session_token or not request_token or session_token != request_token:
|
||||
abort(400)
|
||||
|
||||
@app.after_request
|
||||
def set_security_headers(resp):
|
||||
resp.headers.setdefault("X-Content-Type-Options", "nosniff")
|
||||
resp.headers.setdefault("X-Frame-Options", "DENY")
|
||||
resp.headers.setdefault("Referrer-Policy", "strict-origin-when-cross-origin")
|
||||
resp.headers.setdefault("Content-Security-Policy", "default-src 'self'; script-src 'self' 'unsafe-inline' https://cdn.jsdelivr.net; style-src 'self' 'unsafe-inline' https://cdn.jsdelivr.net; img-src 'self' data:; font-src 'self' https://cdn.jsdelivr.net; connect-src 'self'; frame-ancestors 'none';")
|
||||
return resp
|
||||
|
||||
@app.errorhandler(400)
|
||||
def bad_request(_):
|
||||
return "Bad request", 400
|
||||
|
||||
@app.errorhandler(404)
|
||||
def not_found(_):
|
||||
uid = session.get("user_id")
|
||||
is_site_admin = bool(session.get("is_site_admin"))
|
||||
role = session.get("role")
|
||||
|
||||
links = []
|
||||
if not uid:
|
||||
links = [
|
||||
{"label": "Login", "href": url_for("auth.login"), "btn": "btn-success"},
|
||||
{"label": "Site Admin Login", "href": url_for("auth.admin_login"), "btn": "btn-outline-danger"},
|
||||
]
|
||||
elif is_site_admin and not session.get("group_id"):
|
||||
links = [
|
||||
{"label": "Site Admin Dashboard", "href": url_for("site_admin.dashboard"), "btn": "btn-danger"},
|
||||
]
|
||||
else:
|
||||
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:
|
||||
links.append({"label": "Group Admin", "href": url_for("group_admin.dashboard"), "btn": "btn-outline-warning"})
|
||||
|
||||
return render_template(
|
||||
"404.html",
|
||||
requested_path=request.path,
|
||||
request_method=request.method,
|
||||
links=links,
|
||||
is_logged_in=bool(uid),
|
||||
is_site_admin=is_site_admin,
|
||||
role=role,
|
||||
), 404
|
||||
|
||||
@app.template_filter("fmt_duration")
|
||||
def fmt_duration(seconds):
|
||||
if seconds is None:
|
||||
@@ -64,6 +134,7 @@ def create_app() -> Flask:
|
||||
"app_version": "2.0.0",
|
||||
"author": "SimolZimol",
|
||||
"user_groups": groups,
|
||||
"csrf_token": _get_or_create_csrf_token,
|
||||
}
|
||||
|
||||
return app
|
||||
|
||||
@@ -52,13 +52,13 @@ def admin_login():
|
||||
return render_template("auth/admin_login.html", error=error)
|
||||
|
||||
|
||||
@auth.route("/logout")
|
||||
@auth.route("/logout", methods=["POST"])
|
||||
def logout():
|
||||
session.clear()
|
||||
return redirect(url_for("auth.login"))
|
||||
|
||||
|
||||
@auth.route("/switch-group/<int:group_id>")
|
||||
@auth.route("/switch-group/<int:group_id>", methods=["POST"])
|
||||
def switch_group(group_id):
|
||||
if not session.get("user_id") or session.get("is_site_admin"):
|
||||
return redirect(url_for("auth.login"))
|
||||
|
||||
@@ -99,7 +99,7 @@ def member_edit(user_id):
|
||||
|
||||
if request.method == "POST":
|
||||
role = request.form.get("role", "member")
|
||||
new_perms = {key: (request.form.get(key) == "1") 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)
|
||||
flash("Permissions updated.", "success")
|
||||
return redirect(url_for("group_admin.members"))
|
||||
|
||||
@@ -118,7 +118,7 @@ def dashboard():
|
||||
"proxy_events_today": query("SELECT COUNT(*) AS c FROM proxy_events WHERE timestamp >= CURDATE()", fetchone=True)["c"],
|
||||
}
|
||||
online = query("""
|
||||
SELECT p.username, ps.server_name, ps.login_time
|
||||
SELECT p.uuid AS player_uuid, p.username AS player_name, ps.server_name, ps.login_time
|
||||
FROM player_sessions ps
|
||||
JOIN players p ON p.uuid = ps.player_uuid
|
||||
WHERE ps.logout_time IS NULL
|
||||
@@ -148,8 +148,9 @@ def dashboard():
|
||||
recent = query("""
|
||||
SELECT * FROM v_recent_activity LIMIT 50
|
||||
""")
|
||||
except Exception as e:
|
||||
flash(f"Database error: {e}", "danger")
|
||||
except Exception:
|
||||
panel.logger.exception("Database error while rendering dashboard")
|
||||
flash("Database query failed. Please contact an administrator.", "danger")
|
||||
return render_template("panel/no_db.html")
|
||||
|
||||
return render_template("panel/dashboard.html",
|
||||
@@ -405,7 +406,7 @@ def perms():
|
||||
@login_required
|
||||
def api_online():
|
||||
rows = query("""
|
||||
SELECT p.username, ps.server_name, ps.login_time
|
||||
SELECT p.uuid AS player_uuid, p.username AS player_name, ps.server_name, ps.login_time
|
||||
FROM player_sessions ps
|
||||
JOIN players p ON p.uuid = ps.player_uuid
|
||||
WHERE ps.logout_time IS NULL ORDER BY ps.login_time DESC
|
||||
|
||||
@@ -216,7 +216,7 @@ def user_delete(user_id):
|
||||
# Als Gruppe anzeigen (Site-Admin liest Gruppen-DB)
|
||||
# ──────────────────────────────────────────────────────────────
|
||||
|
||||
@site_admin.route("/view-group/<int:group_id>")
|
||||
@site_admin.route("/view-group/<int:group_id>", methods=["POST"])
|
||||
@admin_required
|
||||
def view_group(group_id):
|
||||
"""Site Admin temporarily switches into a group to view its MC data."""
|
||||
@@ -239,7 +239,7 @@ def view_group(group_id):
|
||||
return redirect(url_for("panel.dashboard"))
|
||||
|
||||
|
||||
@site_admin.route("/stop-view")
|
||||
@site_admin.route("/stop-view", methods=["POST"])
|
||||
@admin_required
|
||||
def stop_view():
|
||||
"""Kehrt zum Site-Admin-Dashboard zurück."""
|
||||
|
||||
@@ -5,12 +5,29 @@ Alle Einstellungen über ENV-Variablen (Coolify-kompatibel).
|
||||
import os
|
||||
|
||||
|
||||
def _as_bool(value: str | None, default: bool = False) -> bool:
|
||||
if value is None:
|
||||
return default
|
||||
return value.strip().lower() in {"1", "true", "yes", "on"}
|
||||
|
||||
|
||||
class Config:
|
||||
DEFAULT_SECRET_KEY = "change-me-use-a-long-random-string-min-32-chars"
|
||||
DEFAULT_PASSWORD_PEPPER = "change-me-global-pepper-secret-never-change"
|
||||
|
||||
# ── Flask ──────────────────────────────────────────────────
|
||||
SECRET_KEY = os.getenv("SECRET_KEY", "change-me-use-a-long-random-string-min-32-chars")
|
||||
SECRET_KEY = os.getenv("SECRET_KEY", DEFAULT_SECRET_KEY)
|
||||
HOST = os.getenv("HOST") or "0.0.0.0"
|
||||
PORT = int(os.getenv("PORT") or "5000")
|
||||
DEBUG = (os.getenv("DEBUG") or "false").lower() == "true"
|
||||
DEBUG = _as_bool(os.getenv("DEBUG"), default=False)
|
||||
|
||||
# Session-Cookie-Hardening
|
||||
SESSION_COOKIE_HTTPONLY = True
|
||||
SESSION_COOKIE_SAMESITE = os.getenv("SESSION_COOKIE_SAMESITE") or "Lax"
|
||||
SESSION_COOKIE_SECURE = _as_bool(os.getenv("SESSION_COOKIE_SECURE"), default=not DEBUG)
|
||||
|
||||
# Erzwingt sichere Secrets in Nicht-Debug-Umgebungen
|
||||
ENFORCE_SECURE_CONFIG = _as_bool(os.getenv("ENFORCE_SECURE_CONFIG"), default=True)
|
||||
|
||||
# ── Panel-Datenbank (Nutzer, Gruppen, Mitgliedschaften) ────
|
||||
PANEL_DB_HOST = os.getenv("PANEL_DB_HOST") or "localhost"
|
||||
@@ -27,7 +44,7 @@ class Config:
|
||||
CREDS_DB_NAME = os.getenv("CREDS_DB_NAME") or "mclogger_creds"
|
||||
|
||||
# ── Sicherheit ────────────────────────────────────────────
|
||||
PASSWORD_PEPPER = os.getenv("PASSWORD_PEPPER", "change-me-global-pepper-secret-never-change")
|
||||
PASSWORD_PEPPER = os.getenv("PASSWORD_PEPPER", DEFAULT_PASSWORD_PEPPER)
|
||||
# Generieren: python -c "from cryptography.fernet import Fernet; print(Fernet.generate_key().decode())"
|
||||
FERNET_KEY = os.getenv("FERNET_KEY", "")
|
||||
|
||||
@@ -44,3 +61,20 @@ class Config:
|
||||
"view_server_events": False,
|
||||
"view_perms": False,
|
||||
}
|
||||
|
||||
@classmethod
|
||||
def validate_security(cls):
|
||||
"""Fail-fast, damit unsichere Defaults nicht in Produktion laufen."""
|
||||
if not cls.ENFORCE_SECURE_CONFIG or cls.DEBUG:
|
||||
return
|
||||
|
||||
issues = []
|
||||
if cls.SECRET_KEY == cls.DEFAULT_SECRET_KEY:
|
||||
issues.append("SECRET_KEY must be set to a strong random value.")
|
||||
if cls.PASSWORD_PEPPER == cls.DEFAULT_PASSWORD_PEPPER:
|
||||
issues.append("PASSWORD_PEPPER must be set to a strong secret value.")
|
||||
if not cls.FERNET_KEY:
|
||||
issues.append("FERNET_KEY must be configured.")
|
||||
|
||||
if issues:
|
||||
raise RuntimeError("Invalid security configuration: " + " ".join(issues))
|
||||
|
||||
76
web/templates/404.html
Normal file
76
web/templates/404.html
Normal file
@@ -0,0 +1,76 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en" data-bs-theme="dark">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>404 - Not Found</title>
|
||||
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/css/bootstrap.min.css">
|
||||
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.11.3/font/bootstrap-icons.min.css">
|
||||
<style>
|
||||
body {
|
||||
min-height: 100vh;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
background: radial-gradient(circle at top right, #1b2235 0%, #0d1117 55%, #090c12 100%);
|
||||
color: #e5e7eb;
|
||||
}
|
||||
.error-shell {
|
||||
width: min(760px, 92vw);
|
||||
}
|
||||
.error-card {
|
||||
background: rgba(23, 27, 40, 0.9);
|
||||
border: 1px solid #2a3249;
|
||||
border-radius: 16px;
|
||||
box-shadow: 0 20px 48px rgba(0, 0, 0, 0.35);
|
||||
}
|
||||
.path-chip {
|
||||
background: rgba(148, 163, 184, 0.14);
|
||||
border: 1px solid rgba(148, 163, 184, 0.35);
|
||||
border-radius: 10px;
|
||||
color: #cbd5e1;
|
||||
font-family: Consolas, 'Cascadia Code', monospace;
|
||||
font-size: 0.85rem;
|
||||
padding: 0.45rem 0.65rem;
|
||||
word-break: break-all;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="error-shell">
|
||||
<div class="error-card p-4 p-md-5">
|
||||
<div class="d-flex align-items-center gap-3 mb-3">
|
||||
<i class="bi bi-signpost-split-fill text-warning" style="font-size: 2.2rem;"></i>
|
||||
<div>
|
||||
<h1 class="h3 mb-1">404 - Seite nicht gefunden</h1>
|
||||
<p class="text-secondary mb-0">Die angeforderte Route existiert nicht oder wurde verschoben.</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="mb-3">
|
||||
<div class="small text-secondary mb-1">Anfrage</div>
|
||||
<div class="path-chip">{{ request_method }} {{ requested_path }}</div>
|
||||
</div>
|
||||
|
||||
<div class="mb-4">
|
||||
{% if not is_logged_in %}
|
||||
<p class="mb-0 text-secondary">Du bist aktuell nicht eingeloggt. Starte am besten ueber die Login-Seite.</p>
|
||||
{% elif is_site_admin and not session.get('group_id') %}
|
||||
<p class="mb-0 text-secondary">Du bist als Site Admin eingeloggt. Von dort kannst du Gruppen und Benutzer verwalten.</p>
|
||||
{% elif role == 'admin' %}
|
||||
<p class="mb-0 text-secondary">Du bist Gruppen-Admin. Nutze Panel oder Group-Admin, um wieder in gueltige Bereiche zu kommen.</p>
|
||||
{% else %}
|
||||
<p class="mb-0 text-secondary">Nutze das Dashboard, um wieder in bekannte Bereiche zu navigieren.</p>
|
||||
{% endif %}
|
||||
</div>
|
||||
|
||||
<div class="d-flex flex-wrap gap-2">
|
||||
{% for link in links %}
|
||||
<a href="{{ link.href }}" class="btn {{ link.btn }}">{{ link.label }}</a>
|
||||
{% endfor %}
|
||||
<a href="javascript:history.back()" class="btn btn-outline-secondary">Zurueck</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
@@ -18,9 +18,12 @@
|
||||
<a href="{{ url_for('site_admin.dashboard') }}" class="nav-link text-white {{ 'fw-bold' if request.endpoint == 'site_admin.dashboard' }}">Dashboard</a>
|
||||
<a href="{{ url_for('site_admin.groups') }}" class="nav-link text-white {{ 'fw-bold' if request.endpoint == 'site_admin.groups' }}">Groups</a>
|
||||
<a href="{{ url_for('site_admin.users') }}" class="nav-link text-white {{ 'fw-bold' if request.endpoint == 'site_admin.users' }}">Users</a>
|
||||
<a href="{{ url_for('auth.logout') }}" class="btn btn-outline-light btn-sm">
|
||||
<i class="bi bi-box-arrow-right"></i> Logout
|
||||
</a>
|
||||
<form method="post" action="{{ url_for('auth.logout') }}" class="d-inline">
|
||||
<input type="hidden" name="_csrf_token" value="{{ csrf_token() }}">
|
||||
<button type="submit" class="btn btn-outline-light btn-sm">
|
||||
<i class="bi bi-box-arrow-right"></i> Logout
|
||||
</button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</nav>
|
||||
|
||||
@@ -13,6 +13,7 @@
|
||||
<div class="card border-secondary">
|
||||
<div class="card-body">
|
||||
<form method="post">
|
||||
<input type="hidden" name="_csrf_token" value="{{ csrf_token() }}">
|
||||
<div class="mb-3">
|
||||
<label class="form-label">Group Name *</label>
|
||||
<input type="text" name="name" class="form-control" required
|
||||
|
||||
@@ -29,12 +29,14 @@
|
||||
</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">
|
||||
<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>
|
||||
</button>
|
||||
</form>
|
||||
<form method="post" action="{{ url_for('site_admin.group_member_remove', group_id=group.id, user_id=m.id) }}" class="d-inline"
|
||||
onsubmit="return confirm('Remove {{ m.username }} from group?')">
|
||||
<input type="hidden" name="_csrf_token" value="{{ csrf_token() }}">
|
||||
<button type="submit" class="btn btn-sm btn-outline-danger" title="Remove">
|
||||
<i class="bi bi-person-dash"></i>
|
||||
</button>
|
||||
@@ -57,6 +59,7 @@
|
||||
<div class="card-body">
|
||||
{% if non_members %}
|
||||
<form method="post" action="{{ url_for('site_admin.group_member_add', group_id=group.id) }}">
|
||||
<input type="hidden" name="_csrf_token" value="{{ csrf_token() }}">
|
||||
<div class="mb-3">
|
||||
<label class="form-label">Select User</label>
|
||||
<select name="user_id" class="form-select">
|
||||
|
||||
@@ -32,9 +32,12 @@
|
||||
</td>
|
||||
<td class="text-muted small">{{ g.created_at | fmt_dt }}</td>
|
||||
<td class="text-end">
|
||||
<a href="{{ url_for('site_admin.view_group', group_id=g.id) }}" class="btn btn-sm btn-outline-info" title="Browse data">
|
||||
<i class="bi bi-eye"></i>
|
||||
</a>
|
||||
<form method="post" action="{{ url_for('site_admin.view_group', group_id=g.id) }}" class="d-inline">
|
||||
<input type="hidden" name="_csrf_token" value="{{ csrf_token() }}">
|
||||
<button type="submit" class="btn btn-sm btn-outline-info" title="Browse data">
|
||||
<i class="bi bi-eye"></i>
|
||||
</button>
|
||||
</form>
|
||||
<a href="{{ url_for('site_admin.group_members', group_id=g.id) }}" class="btn btn-sm btn-outline-secondary" title="Members">
|
||||
<i class="bi bi-people-fill"></i>
|
||||
</a>
|
||||
@@ -43,6 +46,7 @@
|
||||
</a>
|
||||
<form method="post" action="{{ url_for('site_admin.group_delete', group_id=g.id) }}" class="d-inline"
|
||||
onsubmit="return confirm('Delete group {{ g.name }}?')">
|
||||
<input type="hidden" name="_csrf_token" value="{{ csrf_token() }}">
|
||||
<button type="submit" class="btn btn-sm btn-outline-danger" title="Delete">
|
||||
<i class="bi bi-trash3"></i>
|
||||
</button>
|
||||
|
||||
@@ -13,6 +13,7 @@
|
||||
<div class="card border-secondary">
|
||||
<div class="card-body">
|
||||
<form method="post">
|
||||
<input type="hidden" name="_csrf_token" value="{{ csrf_token() }}">
|
||||
<div class="mb-3">
|
||||
<label class="form-label">Username *</label>
|
||||
<input type="text" name="username" class="form-control" required
|
||||
|
||||
@@ -37,6 +37,7 @@
|
||||
</a>
|
||||
<form method="post" action="{{ url_for('site_admin.user_delete', user_id=u.id) }}" class="d-inline"
|
||||
onsubmit="return confirm('Delete user {{ u.username }}?')">
|
||||
<input type="hidden" name="_csrf_token" value="{{ csrf_token() }}">
|
||||
<button type="submit" class="btn btn-sm btn-outline-danger" title="Delete">
|
||||
<i class="bi bi-trash3"></i>
|
||||
</button>
|
||||
|
||||
@@ -30,6 +30,7 @@
|
||||
<div class="card border-danger">
|
||||
<div class="card-body">
|
||||
<form method="post">
|
||||
<input type="hidden" name="_csrf_token" value="{{ csrf_token() }}">
|
||||
<div class="mb-3">
|
||||
<label class="form-label">Username</label>
|
||||
<div class="input-group">
|
||||
|
||||
@@ -30,6 +30,7 @@
|
||||
<div class="card border-secondary">
|
||||
<div class="card-body">
|
||||
<form method="post">
|
||||
<input type="hidden" name="_csrf_token" value="{{ csrf_token() }}">
|
||||
<div class="mb-3">
|
||||
<label class="form-label">Username</label>
|
||||
<div class="input-group">
|
||||
|
||||
@@ -103,10 +103,13 @@
|
||||
<div class="mb-2 sidebar-hide-collapsed">
|
||||
<small class="text-muted">Switch group:</small>
|
||||
{% for g in user_groups %}
|
||||
<a href="{{ url_for('auth.switch_group', group_id=g.id) }}"
|
||||
class="btn btn-sm w-100 mt-1 {{ 'btn-success' if g.id == session.get('group_id') else 'btn-outline-secondary' }}">
|
||||
{{ g.name }}
|
||||
</a>
|
||||
<form method="post" action="{{ url_for('auth.switch_group', group_id=g.id) }}" class="mt-1">
|
||||
<input type="hidden" name="_csrf_token" value="{{ csrf_token() }}">
|
||||
<button type="submit"
|
||||
class="btn btn-sm w-100 {{ 'btn-success' if g.id == session.get('group_id') else 'btn-outline-secondary' }}">
|
||||
{{ g.name }}
|
||||
</button>
|
||||
</form>
|
||||
{% endfor %}
|
||||
</div>
|
||||
{% endif %}
|
||||
@@ -119,9 +122,12 @@
|
||||
{% endif %}
|
||||
{% if session.get('is_site_admin') %}
|
||||
{% if session.get('admin_viewing') %}
|
||||
<a href="{{ url_for('site_admin.stop_view') }}" class="btn btn-warning btn-sm mb-1">
|
||||
<i class="bi bi-arrow-left"></i> <span>Back to Admin</span>
|
||||
</a>
|
||||
<form method="post" action="{{ url_for('site_admin.stop_view') }}" class="mb-1">
|
||||
<input type="hidden" name="_csrf_token" value="{{ csrf_token() }}">
|
||||
<button type="submit" class="btn btn-warning btn-sm w-100">
|
||||
<i class="bi bi-arrow-left"></i> <span>Back to Admin</span>
|
||||
</button>
|
||||
</form>
|
||||
{% else %}
|
||||
<a href="{{ url_for('site_admin.dashboard') }}" class="btn btn-outline-danger btn-sm mb-1">
|
||||
<i class="bi bi-shield-fill"></i> <span>Site Admin</span>
|
||||
@@ -135,9 +141,12 @@
|
||||
<span id="online-count">—</span> <span class="sidebar-hide-collapsed">Online</span>
|
||||
</div>
|
||||
<small class="text-muted d-block mb-1 sidebar-hide-collapsed">{{ session.get('username', '') }}</small>
|
||||
<a href="{{ url_for('auth.logout') }}" class="btn btn-outline-danger btn-sm w-100">
|
||||
<i class="bi bi-box-arrow-right"></i> <span>Logout</span>
|
||||
</a>
|
||||
<form method="post" action="{{ url_for('auth.logout') }}">
|
||||
<input type="hidden" name="_csrf_token" value="{{ csrf_token() }}">
|
||||
<button type="submit" class="btn btn-outline-danger btn-sm w-100">
|
||||
<i class="bi bi-box-arrow-right"></i> <span>Logout</span>
|
||||
</button>
|
||||
</form>
|
||||
</div>
|
||||
</nav>
|
||||
|
||||
|
||||
@@ -21,9 +21,12 @@
|
||||
<a href="{{ url_for('panel.dashboard') }}" class="btn btn-outline-dark btn-sm">
|
||||
<i class="bi bi-grid me-1"></i>Panel
|
||||
</a>
|
||||
<a href="{{ url_for('auth.logout') }}" class="btn btn-dark btn-sm">
|
||||
<i class="bi bi-box-arrow-right"></i>
|
||||
</a>
|
||||
<form method="post" action="{{ url_for('auth.logout') }}" class="d-inline">
|
||||
<input type="hidden" name="_csrf_token" value="{{ csrf_token() }}">
|
||||
<button type="submit" class="btn btn-dark btn-sm">
|
||||
<i class="bi bi-box-arrow-right"></i>
|
||||
</button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</nav>
|
||||
|
||||
@@ -15,6 +15,7 @@
|
||||
{% endif %}
|
||||
|
||||
<form method="post">
|
||||
<input type="hidden" name="_csrf_token" value="{{ csrf_token() }}">
|
||||
<div class="row g-3">
|
||||
<div class="col-md-8">
|
||||
<label class="form-label">Host *</label>
|
||||
@@ -48,18 +49,22 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="d-flex gap-2 mt-4">
|
||||
<div class="mt-4">
|
||||
<button type="submit" name="action" value="test_save" class="btn btn-success">
|
||||
<i class="bi bi-plug-fill me-1"></i>Test & Save
|
||||
</button>
|
||||
{% if creds %}
|
||||
<button type="submit" name="action" value="delete" class="btn btn-outline-danger"
|
||||
onclick="return confirm('Delete DB configuration?')">
|
||||
<i class="bi bi-trash3 me-1"></i>Remove
|
||||
</button>
|
||||
{% endif %}
|
||||
</div>
|
||||
</form>
|
||||
|
||||
{% if creds %}
|
||||
<form method="post" action="{{ url_for('group_admin.database_delete') }}" class="d-inline">
|
||||
<input type="hidden" name="_csrf_token" value="{{ csrf_token() }}">
|
||||
<button type="submit" class="btn btn-outline-danger mt-2"
|
||||
onclick="return confirm('Delete DB configuration?')">
|
||||
<i class="bi bi-trash3 me-1"></i>Remove
|
||||
</button>
|
||||
</form>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -79,13 +84,13 @@
|
||||
<p class="small text-muted mb-1"><strong>Required tables:</strong></p>
|
||||
<ul class="small text-muted">
|
||||
<li>player_sessions</li>
|
||||
<li>chat_messages</li>
|
||||
<li>player_chat</li>
|
||||
<li>player_commands</li>
|
||||
<li>block_events</li>
|
||||
<li>player_deaths</li>
|
||||
<li>proxy_events</li>
|
||||
<li>server_events</li>
|
||||
<li>permission_changes</li>
|
||||
<li>plugin_events</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -16,6 +16,7 @@
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<form method="post">
|
||||
<input type="hidden" name="_csrf_token" value="{{ csrf_token() }}">
|
||||
<div class="mb-3">
|
||||
<label class="form-label">Role</label>
|
||||
<select name="role" class="form-select">
|
||||
@@ -33,7 +34,7 @@
|
||||
<div class="form-check form-switch">
|
||||
<input class="form-check-input" type="checkbox" role="switch"
|
||||
name="perm_{{ key }}" id="perm_{{ key }}"
|
||||
{{ 'checked' if perms.get(key, True) }}>
|
||||
{{ 'checked' if current_perms.get(key, True) }}>
|
||||
<label class="form-check-label" for="perm_{{ key }}">{{ label }}</label>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -29,6 +29,7 @@
|
||||
</a>
|
||||
<form method="post" action="{{ url_for('group_admin.member_remove', user_id=m.id) }}" class="d-inline"
|
||||
onsubmit="return confirm('Remove {{ m.username }}?')">
|
||||
<input type="hidden" name="_csrf_token" value="{{ csrf_token() }}">
|
||||
<button type="submit" class="btn btn-sm btn-outline-danger" title="Remove">
|
||||
<i class="bi bi-person-dash"></i>
|
||||
</button>
|
||||
|
||||
@@ -24,6 +24,7 @@
|
||||
{% endif %}
|
||||
|
||||
<form method="post">
|
||||
<input type="hidden" name="_csrf_token" value="{{ csrf_token() }}">
|
||||
<div class="mb-3">
|
||||
<label class="form-label">Username</label>
|
||||
<div class="input-group">
|
||||
|
||||
@@ -38,7 +38,7 @@
|
||||
<div class="card h-100">
|
||||
<div class="card-header d-flex justify-content-between align-items-center">
|
||||
<span><i class="bi bi-circle-fill text-success me-2 blink" style="font-size:.5rem"></i>Online Players</span>
|
||||
<button class="btn btn-sm btn-outline-secondary" onclick="refreshOnline()">
|
||||
<button class="btn btn-sm btn-outline-secondary" onclick="updateOnlineCount()">
|
||||
<i class="bi bi-arrow-clockwise"></i>
|
||||
</button>
|
||||
</div>
|
||||
@@ -183,12 +183,5 @@ new Chart(deathCtx, {
|
||||
},
|
||||
options: { plugins: { legend: { position: 'bottom', labels: { font: { size:10 } } } } }
|
||||
});
|
||||
function refreshOnline() {
|
||||
fetch('/api/online').then(r => r.json()).then(data => {
|
||||
document.getElementById('online-count').textContent = data.length;
|
||||
});
|
||||
}
|
||||
setInterval(refreshOnline, 30000);
|
||||
refreshOnline();
|
||||
</script>
|
||||
{% endblock %}
|
||||
|
||||
Reference in New Issue
Block a user