Add web admin panel for accounts/characters (login, GM rights, live stats)

New "webadmin" service (Flask + PyMySQL, port 8081) backing a small
server-rendered admin UI:
- Login gated on an existing game account: only accounts with
  accesslevel >= ADMIN_MIN_ACCESSLEVEL (default 900) get in. Password
  is MD5-hashed before comparison since the game client hashes it
  client-side before it ever hits the wire -- accounts.password is
  always an MD5 hex digest, confirmed against existing DB rows.
- Accounts: list/search, create, edit accesslevel/active/password.
- Characters: list/search, create (mirrors the real char-creation
  INSERT in Src/Char Server/charpackets.cpp -- only account_name,
  char_name, face, hairStyle, sex are set, everything else relies on
  the table's own defaults), edit core stats (level, zuly, attributes,
  hp/mp, stat/skill points) and isgm/isdev flags directly.

All queries are parameterized (no string-formatted SQL). No CSRF
protection or rate limiting yet -- acceptable for a small
already-behind-a-high-accesslevel-gate private server, worth
revisiting before ever exposing this port publicly.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
ThomasandClaude Sonnet 5 committed 2026-07-09 07:46:07 +02:00
1 parent f053a06920
commit 9829688a85
14 files changed
+610

No files matched your search

+25
View File
@@ -0,0 +1,25 @@
{% extends "base.html" %}
{% block title %}Accounts - ROSS Admin{% endblock %}
{% block content %}
<div class="searchbar">
<form method="get" style="display:flex;gap:10px;flex:1;">
<input type="text" name="q" placeholder="Username suchen..." value="{{ q }}">
<button type="submit">Suchen</button>
</form>
<a class="btn" href="{{ url_for('new_account') }}">+ Neuer Account</a>
</div>
<table>
<tr><th>ID</th><th>Username</th><th>Accesslevel</th><th>Aktiv</th><th>Online</th><th>Last IP</th><th></th></tr>
{% for a in accounts %}
<tr>
<td>{{ a.id }}</td>
<td>{{ a.username }}</td>
<td>{{ a.accesslevel }}</td>
<td>{% if a.active %}<span class="badge ok">ja</span>{% else %}<span class="badge off">nein</span>{% endif %}</td>
<td>{% if a.online %}<span class="badge ok">online</span>{% else %}<span class="badge off">offline</span>{% endif %}</td>
<td>{{ a.lastip }}</td>
<td class="row-actions"><a class="btn small" href="{{ url_for('edit_account', account_id=a.id) }}">bearbeiten</a></td>
</tr>
{% endfor %}
</table>
{% endblock %}
+96
View File
@@ -0,0 +1,96 @@
<!doctype html>
<html lang="de">
<head>
<meta charset="utf-8">
<title>{% block title %}ROSS Admin{% endblock %}</title>
<style>
:root {
color-scheme: dark;
--bg: #14161c;
--panel: #1d2029;
--border: #2c3040;
--text: #e4e6ec;
--muted: #9098ab;
--accent: #5b8cff;
--danger: #e0556f;
--ok: #4fbf7a;
}
* { box-sizing: border-box; }
body {
margin: 0;
font-family: -apple-system, Segoe UI, Roboto, sans-serif;
background: var(--bg);
color: var(--text);
}
header {
display: flex;
align-items: center;
justify-content: space-between;
padding: 14px 24px;
background: var(--panel);
border-bottom: 1px solid var(--border);
}
header a { color: var(--text); text-decoration: none; font-weight: 600; }
nav a { color: var(--muted); text-decoration: none; margin-right: 18px; font-size: 14px; }
nav a:hover { color: var(--text); }
main { max-width: 960px; margin: 0 auto; padding: 24px; }
table { width: 100%; border-collapse: collapse; margin-top: 12px; }
th, td { text-align: left; padding: 8px 10px; border-bottom: 1px solid var(--border); font-size: 14px; }
th { color: var(--muted); font-weight: 600; font-size: 12px; text-transform: uppercase; }
tr:hover td { background: #ffffff08; }
a.btn, button {
display: inline-block;
background: var(--accent);
color: white;
border: none;
padding: 8px 14px;
border-radius: 6px;
font-size: 14px;
cursor: pointer;
text-decoration: none;
}
a.btn.secondary, button.secondary { background: #33384a; }
a.btn.small { padding: 4px 10px; font-size: 12px; }
input, select {
background: #10121a;
border: 1px solid var(--border);
color: var(--text);
padding: 8px 10px;
border-radius: 6px;
font-size: 14px;
width: 100%;
}
label { display: block; font-size: 12px; color: var(--muted); margin: 12px 0 4px; }
form.inline { display: inline; }
.flash { background: #3a2f1a; border: 1px solid #6b4f1f; color: #ffd889; padding: 10px 14px; border-radius: 6px; margin-bottom: 16px; }
.card { background: var(--panel); border: 1px solid var(--border); border-radius: 10px; padding: 20px; margin-bottom: 20px; }
.grid { display: grid; grid-template-columns: 1fr 1fr; gap: 0 20px; }
.searchbar { display: flex; gap: 10px; margin-bottom: 10px; }
.searchbar input { flex: 1; }
.badge { display: inline-block; padding: 2px 8px; border-radius: 999px; font-size: 11px; font-weight: 600; }
.badge.ok { background: #163a26; color: var(--ok); }
.badge.off { background: #3a1620; color: var(--danger); }
.row-actions a { margin-right: 8px; }
</style>
</head>
<body>
<header>
<a href="{{ url_for('dashboard') }}">ROSS Admin</a>
{% if session.get('username') %}
<nav>
<a href="{{ url_for('accounts') }}">Accounts</a>
<a href="{{ url_for('characters') }}">Charaktere</a>
<a href="{{ url_for('logout') }}">Logout ({{ session['username'] }})</a>
</nav>
{% endif %}
</header>
<main>
{% with messages = get_flashed_messages() %}
{% if messages %}
{% for m in messages %}<div class="flash">{{ m }}</div>{% endfor %}
{% endif %}
{% endwith %}
{% block content %}{% endblock %}
</main>
</body>
</html>
+25
View File
@@ -0,0 +1,25 @@
{% extends "base.html" %}
{% block title %}Charaktere - ROSS Admin{% endblock %}
{% block content %}
<div class="searchbar">
<form method="get" style="display:flex;gap:10px;flex:1;">
<input type="text" name="q" placeholder="Charakter- oder Accountname suchen..." value="{{ q }}">
<button type="submit">Suchen</button>
</form>
<a class="btn" href="{{ url_for('new_character') }}">+ Neuer Charakter</a>
</div>
<table>
<tr><th>ID</th><th>Name</th><th>Account</th><th>Level</th><th>GM</th><th>Dev</th><th></th></tr>
{% for c in chars %}
<tr>
<td>{{ c.id }}</td>
<td>{{ c.char_name }}</td>
<td>{{ c.account_name }}</td>
<td>{{ c.level }}</td>
<td>{% if c.isgm %}<span class="badge ok">GM</span>{% endif %}</td>
<td>{% if c.isdev %}<span class="badge ok">Dev</span>{% endif %}</td>
<td class="row-actions"><a class="btn small" href="{{ url_for('edit_character', char_id=c.id) }}">bearbeiten</a></td>
</tr>
{% endfor %}
</table>
{% endblock %}
+18
View File
@@ -0,0 +1,18 @@
{% extends "base.html" %}
{% block title %}Dashboard - ROSS Admin{% endblock %}
{% block content %}
<div class="grid">
<div class="card">
<h3 style="margin-top:0;">Accounts</h3>
<p style="font-size:32px;margin:0;">{{ accounts_n }}</p>
<p><a class="btn small" href="{{ url_for('accounts') }}">verwalten</a>
<a class="btn small secondary" href="{{ url_for('new_account') }}">+ neu</a></p>
</div>
<div class="card">
<h3 style="margin-top:0;">Charaktere</h3>
<p style="font-size:32px;margin:0;">{{ chars_n }}</p>
<p><a class="btn small" href="{{ url_for('characters') }}">verwalten</a>
<a class="btn small secondary" href="{{ url_for('new_character') }}">+ neu</a></p>
</div>
</div>
{% endblock %}
+33
View File
@@ -0,0 +1,33 @@
{% extends "base.html" %}
{% block title %}{{ account.username }} - ROSS Admin{% endblock %}
{% block content %}
<div class="card" style="max-width:480px;">
<h2 style="margin-top:0;">Account: {{ account.username }}</h2>
<form method="post">
<label>Accesslevel (100 = normaler Spieler, 900 = Admin)</label>
<input type="number" name="accesslevel" value="{{ account.accesslevel }}">
<label><input type="checkbox" name="active" style="width:auto;" {% if account.active %}checked{% endif %}> Aktiv</label>
<label>Neues Passwort (leer lassen = unverändert)</label>
<input type="password" name="password" placeholder="unverändert">
<div style="margin-top:20px;">
<button type="submit">Speichern</button>
<a class="btn secondary" href="{{ url_for('accounts') }}">Zurück</a>
</div>
</form>
</div>
<div class="card">
<h3 style="margin-top:0;">Charaktere auf diesem Account</h3>
<table>
<tr><th>Name</th><th>Level</th><th></th></tr>
{% for c in chars %}
<tr>
<td>{{ c.char_name }}</td>
<td>{{ c.level }}</td>
<td><a class="btn small" href="{{ url_for('edit_character', char_id=c.id) }}">bearbeiten</a></td>
</tr>
{% else %}
<tr><td colspan="3">Keine Charaktere.</td></tr>
{% endfor %}
</table>
</div>
{% endblock %}
+23
View File
@@ -0,0 +1,23 @@
{% extends "base.html" %}
{% block title %}{{ char.char_name }} - ROSS Admin{% endblock %}
{% block content %}
<div class="card" style="max-width:600px;">
<h2 style="margin-top:0;">Charakter: {{ char.char_name }} <span class="badge ok">Account: {{ char.account_name }}</span></h2>
<form method="post">
<div class="grid">
{% for field in stat_fields %}
<div>
<label>{{ field.lstrip('_') }}</label>
<input type="number" name="{{ field }}" value="{{ char[field] }}">
</div>
{% endfor %}
</div>
<label style="margin-top:16px;"><input type="checkbox" name="isgm" style="width:auto;" {% if char.isgm %}checked{% endif %}> GM</label>
<label><input type="checkbox" name="isdev" style="width:auto;" {% if char.isdev %}checked{% endif %}> Dev (Debugmode-fähig)</label>
<div style="margin-top:20px;">
<button type="submit">Speichern</button>
<a class="btn secondary" href="{{ url_for('characters') }}">Zurück</a>
</div>
</form>
</div>
{% endblock %}
+16
View File
@@ -0,0 +1,16 @@
{% extends "base.html" %}
{% block title %}Login - ROSS Admin{% endblock %}
{% block content %}
<div class="card" style="max-width:360px;margin:60px auto;">
<h2 style="margin-top:0;">ROSS Admin Login</h2>
<form method="post">
<label>Username</label>
<input type="text" name="username" autofocus required>
<label>Passwort</label>
<input type="password" name="password" required>
<div style="margin-top:20px;">
<button type="submit">Login</button>
</div>
</form>
</div>
{% endblock %}
+19
View File
@@ -0,0 +1,19 @@
{% extends "base.html" %}
{% block title %}Neuer Account - ROSS Admin{% endblock %}
{% block content %}
<div class="card" style="max-width:420px;">
<h2 style="margin-top:0;">Neuer Account</h2>
<form method="post">
<label>Username</label>
<input type="text" name="username" required autofocus>
<label>Passwort</label>
<input type="password" name="password" required>
<label>Accesslevel (100 = normaler Spieler, 900 = Admin)</label>
<input type="number" name="accesslevel" value="100">
<div style="margin-top:20px;">
<button type="submit">Anlegen</button>
<a class="btn secondary" href="{{ url_for('accounts') }}">Abbrechen</a>
</div>
</form>
</div>
{% endblock %}
+27
View File
@@ -0,0 +1,27 @@
{% extends "base.html" %}
{% block title %}Neuer Charakter - ROSS Admin{% endblock %}
{% block content %}
<div class="card" style="max-width:420px;">
<h2 style="margin-top:0;">Neuer Charakter</h2>
<form method="post">
<label>Account</label>
<select name="account_name" required>
{% for name in account_names %}
<option value="{{ name }}">{{ name }}</option>
{% endfor %}
</select>
<label>Charaktername</label>
<input type="text" name="char_name" required autofocus>
<label>Geschlecht (0 = männlich, 1 = weiblich)</label>
<input type="number" name="sex" value="0" min="0" max="1">
<label>Face</label>
<input type="number" name="face" value="1">
<label>Frisur</label>
<input type="number" name="hairstyle" value="1">
<div style="margin-top:20px;">
<button type="submit">Anlegen</button>
<a class="btn secondary" href="{{ url_for('characters') }}">Abbrechen</a>
</div>
</form>
</div>
{% endblock %}