Info Tab und MFA

This commit is contained in:
Erik Thiele
2026-06-28 15:14:06 +02:00
parent 1243910d4b
commit 297b52e50c
15 changed files with 5203 additions and 6363 deletions

View File

@@ -17,6 +17,7 @@
<!-- Aktionen: Standort-Dropdown, +Button, Theme, Logout -->
<div class="navbar-nav flex-row order-md-last top-actions">
<!-- Standort-Auswahl -->
{% if current_user.is_authenticated %}
<div class="dropdown me-2">
<a class="btn btn-outline-secondary" href="#" data-bs-toggle="dropdown">
<i class="ti ti-building me-1"></i>{{ current_site | capitalize }} <i class="ti ti-chevron-down"></i>
@@ -34,6 +35,7 @@
onclick="var n=prompt('Name des neuen Standorts:'); if(n&&n.trim()) location.href='/add-site?name='+encodeURIComponent(n.trim().toLowerCase());"
title="Neuen Standort anlegen"
style="display:inline-flex;align-items:center;justify-content:center;min-width:2.5rem;min-height:2.5rem;padding:0;"><i class="ti ti-plus"></i></a>
{% endif %}
<!-- Dark-Mode-Toggle (persistiert in localStorage("signage-theme")) -->
<div>
@@ -50,6 +52,7 @@
</a>
<div class="dropdown-menu dropdown-menu-end">
<a class="dropdown-item" href="/change-password"><i class="ti ti-key me-1"></i>Passwort ändern</a>
<a class="dropdown-item" href="/mfa/setup"><i class="ti ti-shield me-1"></i>MFA einrichten{% if current_user.mfa_enabled %} <span class="badge bg-green text-green-fg ms-1" style="font-size:0.6rem;">Aktiv</span>{% endif %}</a>
<a class="dropdown-item" href="/admin/help"><i class="ti ti-help me-1"></i>Hilfe</a>
{% if current_user.is_admin %}
<a class="dropdown-item" href="/admin/dashboard"><i class="ti ti-dashboard me-1"></i>Admin Dashboard</a>

View File

@@ -176,7 +176,8 @@
</span>
<div class="d-flex align-items-center gap-2">
{% if screen_status[screen] == "online" %}
<button type="button" class="btn btn-sm btn-success" onclick="return false;">Online</button>
<button type="button" class="btn btn-sm btn-success" onclick="return false;"
title="Browser: {{ client_info[screen].get('ua', '?')[:60] }}&#10;Auflösung: {{ client_info[screen].get('resolution', '?') }}&#10;IP: {{ client_info[screen].get('ip', '?') }}&#10;Zuletzt: {{ client_info[screen].get('last_seen', '?') }}">Online</button>
{% else %}
<button type="button" class="btn btn-sm btn-danger" onclick="return false;">Offline</button>
{% endif %}
@@ -188,7 +189,7 @@
</div>
</div>
<!-- Card-Body: Tabs (Playlist | Einstellungen | Aktionen | Digital Voice Agent | Medien) -->
<!-- Card-Body: Tabs (Playlist | Einstellungen | Aktionen | Digital Voice Agent | Medien | Info) -->
<div class="card-body">
<ul class="nav nav-tabs" data-bs-toggle="tabs" role="tablist">
@@ -217,6 +218,11 @@
<i class="ti ti-upload me-1"></i>Medien
</a>
</li>
<li class="nav-item" role="presentation">
<a href="#info-{{ screen }}" class="nav-link" data-bs-toggle="tab" role="tab">
<i class="ti ti-info-circle me-1"></i>Info
</a>
</li>
</ul>
<div class="tab-content mt-3">
@@ -473,6 +479,35 @@
</form>
</div>
<!-- ═══ Tab: Info (Client-Informationen) ═══ -->
<div class="tab-pane" id="info-{{ screen }}" role="tabpanel">
<h3>Client-Informationen</h3>
{% if client_info[screen] %}
<table class="table table-sm">
<tbody>
<tr>
<th style="width:140px">IP-Adresse</th>
<td><code>{{ client_info[screen].get('ip', '') }}</code></td>
</tr>
<tr>
<th>Browser</th>
<td style="word-break:break-all"><small>{{ client_info[screen].get('ua', '') }}</small></td>
</tr>
<tr>
<th>Auflösung</th>
<td>{{ client_info[screen].get('resolution', '') }}</td>
</tr>
<tr>
<th>Zuletzt gesehen</th>
<td>{{ client_info[screen].get('last_seen', '') }}</td>
</tr>
</tbody>
</table>
{% else %}
<p class="text-muted">Noch keine Client-Daten vorhanden. Der Player muss mindestens einmal die Playlist aufgerufen haben.</p>
{% endif %}
</div>
</div> <!-- /tab-content -->
</div> <!-- /card-body -->

View File

@@ -41,9 +41,36 @@
<div class="page-wrapper">
<div class="container-xl mt-4">
<div class="row g-4">
<!-- Sidebar Navigation -->
<div class="col-md-3">
<div class="card" style="position:sticky;top:1rem;">
<div class="card-header"><h4 class="card-title" style="font-size:0.9rem;"><i class="ti ti-menu-2 me-1"></i>Themen</h4></div>
<div class="card-body p-2">
<nav class="nav flex-column">
<a class="nav-link py-1 px-2 small" href="#help-standort"><i class="ti ti-building-community me-1"></i>Standort</a>
<a class="nav-link py-1 px-2 small" href="#help-screen"><i class="ti ti-devices me-1"></i>Screen</a>
<a class="nav-link py-1 px-2 small" href="#help-playlist"><i class="ti ti-list me-1"></i>Playlist</a>
<a class="nav-link py-1 px-2 small" href="#help-priority"><i class="ti ti-player-play me-1"></i>Priority</a>
<a class="nav-link py-1 px-2 small" href="#help-actions"><i class="ti ti-external-link me-1"></i>Action Button</a>
<a class="nav-link py-1 px-2 small" href="#help-voice"><i class="ti ti-robot me-1"></i>Voice Agent</a>
<a class="nav-link py-1 px-2 small" href="#help-einstellungen"><i class="ti ti-settings me-1"></i>Einstellungen</a>
<a class="nav-link py-1 px-2 small" href="#help-info"><i class="ti ti-info-circle me-1"></i>Info-Tab</a>
<a class="nav-link py-1 px-2 small" href="#help-willkommen"><i class="ti ti-users me-1"></i>Willkommen</a>
<a class="nav-link py-1 px-2 small" href="#help-userverwaltung"><i class="ti ti-shield me-1"></i>User</a>
<a class="nav-link py-1 px-2 small" href="#help-dashboard"><i class="ti ti-dashboard me-1"></i>Dashboard</a>
<a class="nav-link py-1 px-2 small" href="#help-darkmode"><i class="ti ti-moon me-1"></i>Dark Mode</a>
<a class="nav-link py-1 px-2 small" href="#help-passwort"><i class="ti ti-key me-1"></i>Passwort</a>
<a class="nav-link py-1 px-2 small" href="#help-mfa"><i class="ti ti-shield-lock me-1"></i>MFA</a>
</nav>
</div>
</div>
</div>
<!-- Main Content -->
<div class="col-md-9">
<div class="card mb-3">
<div class="card-header"><h3 class="card-title"><i class="ti ti-building-community me-1"></i>Standort anlegen</h3></div>
<div class="card-header"><h3 class="card-title" id="help-standort"><i class="ti ti-building-community me-1"></i>Standort anlegen</h3></div>
<div class="card-body">
<p>Ein Standort (Site) gruppiert mehrere Screens, z.&thinsp;B. <code>stuttgart</code> oder <code>karlsruhe</code>.</p>
<ol>
@@ -56,7 +83,7 @@
</div>
<div class="card mb-3">
<div class="card-header"><h3 class="card-title"><i class="ti ti-devices me-1"></i>Screen hinzufügen</h3></div>
<div class="card-header"><h3 class="card-title" id="help-screen"><i class="ti ti-devices me-1"></i>Screen hinzufügen</h3></div>
<div class="card-body">
<p>Ein Screen ist ein einzelner Player (z.&thinsp;B. <code>lobby</code>, <code>casino</code>), der eine Playlist abspielt.</p>
<ol>
@@ -69,7 +96,7 @@
</div>
<div class="card mb-3">
<div class="card-header"><h3 class="card-title"><i class="ti ti-list me-1"></i>Playlist verwalten</h3></div>
<div class="card-header"><h3 class="card-title" id="help-playlist"><i class="ti ti-list me-1"></i>Playlist verwalten</h3></div>
<div class="card-body">
<p>Jeder Screen hat eine eigene Playlist mit Medien und URLs.</p>
<ul>
@@ -84,7 +111,7 @@
</div>
<div class="card mb-3">
<div class="card-header"><h3 class="card-title"><i class="ti ti-player-play me-1"></i>Priority-Playlist</h3></div>
<div class="card-header"><h3 class="card-title" id="help-priority"><i class="ti ti-player-play me-1"></i>Priority-Playlist</h3></div>
<div class="card-body">
<p>Die Priority-Playlist ist <strong>global</strong> und wird auf <em>allen</em> Playern eines Standorts eingeblendet zwischen den normalen Playlist-Einträgen.</p>
<ul>
@@ -96,7 +123,7 @@
</div>
<div class="card mb-3">
<div class="card-header"><h3 class="card-title"><i class="ti ti-external-link me-1"></i>Action Button (Custom-URL)</h3></div>
<div class="card-header"><h3 class="card-title" id="help-actions"><i class="ti ti-external-link me-1"></i>Action Button (Custom-URL)</h3></div>
<div class="card-body">
<p>Der Action Button ist ein konfigurierbarer Button im Player, der eine frei wählbare URL öffnet.</p>
<ul>
@@ -116,7 +143,7 @@
</div>
<div class="card mb-3">
<div class="card-header"><h3 class="card-title"><i class="ti ti-robot me-1"></i>Digital Voice Agent</h3></div>
<div class="card-header"><h3 class="card-title" id="help-voice"><i class="ti ti-robot me-1"></i>Digital Voice Agent</h3></div>
<div class="card-body">
<p>Der Digital Voice Agent zeigt einen Button im Player, der eine Typewriter-Animation mit wechselnden mehrsprachigen Texten anzeigt.</p>
<ul>
@@ -132,7 +159,7 @@
</div>
<div class="card mb-3">
<div class="card-header"><h3 class="card-title"><i class="ti ti-settings me-1"></i>Screen-Einstellungen</h3></div>
<div class="card-header"><h3 class="card-title" id="help-einstellungen"><i class="ti ti-settings me-1"></i>Screen-Einstellungen</h3></div>
<div class="card-body">
<p>Im Tab <strong>Einstellungen</strong> können pro Screen konfiguriert werden:</p>
<ul>
@@ -145,7 +172,21 @@
</div>
<div class="card mb-3">
<div class="card-header"><h3 class="card-title"><i class="ti ti-users me-1"></i>Willkommensseite</h3></div>
<div class="card-header"><h3 class="card-title" id="help-info"><i class="ti ti-info-circle me-1"></i>Info-Tab im Screen</h3></div>
<div class="card-body">
<p>Jeder Screen hat einen <strong>Info-Tab</strong> (Tab 6), der detaillierte Client-Informationen zum aktuell verbundenen Player anzeigt:</p>
<ul>
<li><strong>IP-Adresse</strong> des Players</li>
<li><strong>Browser</strong> (User-Agent) des Players</li>
<li><strong>Bildschirmauflösung</strong> (Breite × Höhe)</li>
<li><strong>Zuletzt gesehen</strong> Zeitstempel des letzten Heartbeats</li>
</ul>
<p class="text-muted small mb-0"><i class="ti ti-info-circle me-1"></i>Die Daten werden alle 5 Sekunden vom Player gesendet und im Heartbeat-JSON gespeichert. Der Online-Status (grüner/roter Button im Screen-Header) basiert auf dem gleichen Mechanismus.</p>
</div>
</div>
<div class="card mb-3">
<div class="card-header"><h3 class="card-title" id="help-willkommen"><i class="ti ti-users me-1"></i>Willkommensseite</h3></div>
<div class="card-body">
<p>Auf der Willkommensseite können bis zu 3 Kundennamen eingegeben werden. Die Logos werden automatisch via OpenAI + Brandfetch gesucht.</p>
<ul>
@@ -159,7 +200,7 @@
{% if current_user.is_admin %}
<div class="card mb-3">
<div class="card-header"><h3 class="card-title"><i class="ti ti-shield me-1"></i>User-Verwaltung (Admin)</h3></div>
<div class="card-header"><h3 class="card-title" id="help-userverwaltung"><i class="ti ti-shield me-1"></i>User-Verwaltung (Admin)</h3></div>
<div class="card-body">
<p>Admins können über <code>/admin/users</code> die User-Verwaltung aufrufen.</p>
<ul>
@@ -179,7 +220,7 @@
</div>
<div class="card mb-3">
<div class="card-header"><h3 class="card-title"><i class="ti ti-dashboard me-1"></i>Admin Dashboard</h3></div>
<div class="card-header"><h3 class="card-title" id="help-dashboard"><i class="ti ti-dashboard me-1"></i>Admin Dashboard</h3></div>
<div class="card-body">
<p>Das Admin Dashboard (<code>/admin/dashboard</code>) bietet eine Übersicht über das gesamte System:</p>
<ul>
@@ -193,24 +234,40 @@
{% endif %}
<div class="card mb-3">
<div class="card-header"><h3 class="card-title"><i class="ti ti-moon me-1"></i>Dark Mode</h3></div>
<div class="card-header"><h3 class="card-title" id="help-darkmode"><i class="ti ti-moon me-1"></i>Dark Mode</h3></div>
<div class="card-body">
<p>Der Dark Mode kann über den Mond-Button im Header umgeschaltet werden. Die Einstellung wird im Browser gespeichert (localStorage) und bleibt auch nach Seitenwechsel erhalten.</p>
</div>
</div>
<div class="card mb-3">
<div class="card-header"><h3 class="card-title"><i class="ti ti-key me-1"></i>Passwort ändern</h3></div>
<div class="card-header"><h3 class="card-title" id="help-passwort"><i class="ti ti-key me-1"></i>Passwort ändern</h3></div>
<div class="card-body">
<p>Das Passwort kann über das User-Menü (oben rechts) → <strong>Passwort ändern</strong> geändert werden. Nach einem Admin-Reset muss der User beim ersten Login ein neues Passwort vergeben.</p>
</div>
</div>
</div>
</div>
<div class="card mb-3">
<div class="card-header"><h3 class="card-title" id="help-mfa"><i class="ti ti-shield-lock me-1"></i>MFA (Multi-Faktor-Authentifizierung)</h3></div>
<div class="card-body">
<p>MFA bietet eine optionale zweite Sicherheitsstufe per TOTP (Google Authenticator, Authy).</p>
<ul>
<li><strong>Aktivierung:</strong> Über das User-Menü → <strong>MFA einrichten</strong> → QR-Code mit einer Authenticator-App scannen → 6-stelligen Code eingeben → Bestätigen</li>
<li><strong>Recovery-Codes:</strong> Nach erfolgreicher Aktivierung werden 8 Recovery-Codes (einmalig) angezeigt. Jeder Code kann nur einmal verwendet werden. Nach Einsatz eines Recovery-Codes muss MFA neu eingerichtet werden.</li>
<li><strong>Login mit MFA:</strong> Nach Eingabe von E-Mail + Passwort erscheint ein zweites Formular für den TOTP-Code. Erst nach erfolgreichem Code wird der Zugriff gewährt.</li>
<li><strong>Deaktivierung:</strong> Über <code>/mfa/setup</code> → Button "MFA deaktivieren" → aktuelles Passwort eingeben</li>
<li><strong>MFA-Status:</strong> In der Userliste (<code>/admin/users</code>) wird der MFA-Status als grünes "Aktiv"-Badge oder "" angezeigt.</li>
</ul>
</div>
</div>
</div> <!-- /col-md-9 main -->
</div> <!-- /row -->
</div> <!-- /container-xl -->
</div> <!-- /page-wrapper -->
{% include "_footer.html" %}
</div>
</div> <!-- /page -->
</body>
</html>

View File

@@ -2,50 +2,60 @@
<html lang="de">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>CANCOM Simple Signage Admin</title>
<link rel="icon" href="{{ url_for('static', filename='favicon.ico') }}">
<link href="https://unpkg.com/@tabler/core@1.0.0-beta20/dist/css/tabler.min.css" rel="stylesheet">
<style>
body {
background-color: #F4F6F8;
color: #212121;
}
.btn-primary {
background-color: #DA002D;
border-color: #DA002D;
}
.btn-primary:hover {
background-color: #B00024;
border-color: #B00024;
}
</style>
<link rel="stylesheet"
href="https://unpkg.com/@tabler/core@1.0.0-beta20/dist/css/tabler.min.css">
<link rel="stylesheet"
href="https://cdn.jsdelivr.net/npm/@tabler/icons-webfont@latest/tabler-icons.min.css">
{% include "_styles.html" %}
<script>
(() => {
const stored = window.localStorage.getItem("signage-theme");
const theme = stored === "dark" || stored === "light" ? stored : "light";
document.documentElement.setAttribute("data-bs-theme", theme);
})();
</script>
</head>
<body>
<div class="page">
<!-- ─── Login-Seite (zentriert) ─── -->
<body class="d-flex align-items-center justify-content-center" style="height:100vh;">
<div class="card card-md">
<div class="card-body">
<h2 class="text-center mb-4">Signage Admin</h2>
{% include "_header.html" %}
{% if error %}
<div class="alert alert-danger text-center" role="alert">
{{ error }}
<div class="page-wrapper d-flex align-items-center justify-content-center">
<div class="container-xl">
<div class="row justify-content-center">
<div class="col-md-5">
<div class="card card-md">
<div class="card-body">
<h2 class="text-center mb-4">Signage Admin</h2>
{% if error %}
<div class="alert alert-danger text-center" role="alert">
{{ error }}
</div>
{% endif %}
<form method="post">
<div class="mb-3">
<label class="form-label">E-Mail / Benutzername</label>
<input class="form-control" name="username" placeholder="admin" required>
</div>
<div class="mb-3">
<label class="form-label">Passwort</label>
<input type="password" class="form-control" name="password" required>
</div>
<button class="btn btn-primary w-100">Login</button>
</form>
</div>
</div>
</div>
</div>
{% endif %}
<form method="post">
<div class="mb-3">
<label class="form-label">E-Mail / Benutzername</label>
<input class="form-control" name="username" placeholder="admin" required>
</div>
<div class="mb-3">
<label class="form-label">Passwort</label>
<input type="password" class="form-control" name="password" required>
</div>
<button class="btn btn-primary w-100">Login</button>
</form>
</div>
</div>
{% include "_footer.html" %}
</div>
</body>
</html>
</html>

View File

@@ -0,0 +1,64 @@
<!doctype html>
<html lang="de">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1, viewport-fit=cover">
<title>MFA eingerichtet CANCOM Signage</title>
<link rel="icon" href="{{ url_for('static', filename='favicon.ico') }}">
<script>
(() => {
const stored = window.localStorage.getItem("signage-theme");
const theme = stored === "dark" || stored === "light" ? stored : "light";
document.documentElement.setAttribute("data-bs-theme", theme);
})();
</script>
<link rel="stylesheet" href="https://unpkg.com/@tabler/core@1.0.0-beta20/dist/css/tabler.min.css">
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/@tabler/icons-webfont@latest/tabler-icons.min.css">
<script defer src="https://unpkg.com/@tabler/core@1.0.0-beta20/dist/js/tabler.min.js"></script>
{% include '_styles.html' %}
</head>
<body>
{% include '_header.html' %}
<div class="page-wrapper">
<div class="page-body" style="min-height:calc(100vh - 120px);display:flex;align-items:center;justify-content:center;">
<div class="container-xl">
<div class="row justify-content-center">
<div class="col-lg-8 col-xl-6">
<div class="card">
<div class="card-body text-center py-5">
<h2 class="mb-2">✅ MFA aktiviert</h2>
<p class="text-muted mb-3">
Speichere diese Recovery-Codes an einem sicheren Ort.<br>
Jeder Code kann <strong>einmal</strong> verwendet werden.
</p>
<div class="alert alert-warning">
<strong>⚠️ Wichtig:</strong> Diese Codes werden nur <strong>einmal</strong> angezeigt.
</div>
<div class="my-4 d-flex flex-wrap justify-content-center gap-2">
{% for code in codes %}
<kbd style="font-size:1.1rem;padding:8px 16px;border-radius:6px;user-select:all;cursor:pointer;" onclick="navigator.clipboard.writeText(this.textContent);this.textContent='✓ Kopiert!';setTimeout(()=>{this.textContent='{{ code }}';},1500);">{{ code }}</kbd>
{% endfor %}
</div>
<p class="text-muted small mb-4">Klicke auf einen Code, um ihn zu kopieren.</p>
<a href="/admin" class="btn btn-primary">← Zum Admin</a>
</div>
</div>
</div>
</div>
</div>
</div>
{% include '_footer.html' %}
</div>
</body>
</html>

101
templates/mfa_setup.html Normal file
View File

@@ -0,0 +1,101 @@
<!doctype html>
<html lang="de">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1, viewport-fit=cover">
<title>MFA einrichten CANCOM Signage</title>
<link rel="icon" href="{{ url_for('static', filename='favicon.ico') }}">
<script>
(() => {
const stored = window.localStorage.getItem("signage-theme");
const theme = stored === "dark" || stored === "light" ? stored : "light";
document.documentElement.setAttribute("data-bs-theme", theme);
})();
</script>
<link rel="stylesheet" href="https://unpkg.com/@tabler/core@1.0.0-beta20/dist/css/tabler.min.css">
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/@tabler/icons-webfont@latest/tabler-icons.min.css">
<script defer src="https://unpkg.com/@tabler/core@1.0.0-beta20/dist/js/tabler.min.js"></script>
{% include '_styles.html' %}
</head>
<body>
{% include '_header.html' %}
<div class="page-wrapper">
<div class="page-body" style="min-height:calc(100vh - 120px);display:flex;align-items:center;justify-content:center;">
<div class="container-xl">
<div class="row justify-content-center">
<div class="col-lg-8 col-xl-6">
{% if recovered %}
<div class="alert alert-warning">
<strong>Hinweis:</strong> Du hast einen Recovery-Code verwendet. Bitte richte MFA neu ein.
</div>
{% endif %}
{% if disabled %}
<div class="alert alert-info">MFA wurde deaktiviert.</div>
{% endif %}
<div class="card">
<div class="card-body text-center py-5">
{% if current_user.mfa_enabled %}
<h2 class="mb-2">✅ MFA ist aktiv</h2>
<p class="text-muted mb-4">Dein Konto ist durch Zwei-Faktor-Authentifizierung geschützt.</p>
<p class="text-muted small mb-2">Möchtest du MFA deaktivieren? Gib dein Passwort ein:</p>
<form method="post" action="/mfa/disable" class="row justify-content-center">
<div class="col-auto">
<div class="input-group">
<input type="password" name="password" class="form-control" placeholder="Passwort" required>
<button class="btn btn-danger" type="submit">Deaktivieren</button>
</div>
</div>
</form>
{% else %}
<h2 class="mb-2">MFA einrichten</h2>
<p class="text-muted mb-4">Scanne den QR-Code mit deiner Authenticator-App (z.B. Google Authenticator, Authy).</p>
{% if error %}
<div class="alert alert-danger" role="alert">{{ error }}</div>
{% endif %}
{% if qr_data %}
<div class="mb-4">
<img src="data:image/png;base64,{{ qr_data }}" alt="QR-Code"
style="width:220px;height:220px;border:1px solid #ddd;border-radius:8px;">
</div>
<p class="text-muted small">Alternativ: Secret manuell eingeben</p>
<div class="mb-4">
<kbd style="font-size:1.1rem;padding:6px 14px;border-radius:6px;user-select:all;">{{ secret }}</kbd>
</div>
<hr class="my-4" style="max-width:300px;margin-left:auto;margin-right:auto;">
<h4>Code verifizieren</h4>
<p class="text-muted small mb-3">Gib den 6-stelligen Code aus der App ein, um MFA zu aktivieren.</p>
<form method="post" class="row justify-content-center">
<div class="col-auto">
<div class="d-flex gap-2">
<input type="text" name="code" class="form-control form-control-lg text-center"
placeholder="000000" inputmode="numeric" maxlength="6"
pattern="[0-9]{6}" required autofocus style="max-width:160px;">
<button class="btn btn-success" type="submit">Aktivieren</button>
</div>
</div>
</form>
{% endif %}
{% endif %}
</div>
</div>
</div>
</div>
</div>
</div>
{% include '_footer.html' %}
</div>
</body>
</html>

70
templates/mfa_verify.html Normal file
View File

@@ -0,0 +1,70 @@
<!doctype html>
<html lang="de">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Sicherheitscode eingeben - CANCOM Signage</title>
<link rel="stylesheet"
href="https://unpkg.com/@tabler/core@1.0.0-beta20/dist/css/tabler.min.css">
<link rel="stylesheet"
href="https://cdn.jsdelivr.net/npm/@tabler/icons-webfont@latest/tabler-icons.min.css">
{% include "_styles.html" %}
<script>
(() => {
const stored = window.localStorage.getItem("signage-theme");
const theme = stored === "dark" || stored === "light" ? stored : "light";
document.documentElement.setAttribute("data-bs-theme", theme);
})();
</script>
</head>
<body>
<div class="page">
{% include "_header.html" %}
<div class="page-wrapper d-flex align-items-center justify-content-center">
<div class="container-xl">
<div class="row justify-content-center">
<div class="col-md-5">
<div class="card card-md">
<div class="card-body">
<h2 class="text-center mb-2">Sicherheitscode</h2>
<p class="text-center text-muted mb-4">Gib den 6-stelligen Code aus deiner Authenticator-App ein.</p>
{% if error %}
<div class="alert alert-danger text-center" role="alert">{{ error }}</div>
{% endif %}
<form method="post">
<div class="mb-3">
<input type="text" name="code" class="form-control form-control-lg text-center"
placeholder="000000" inputmode="numeric" autocomplete="one-time-code"
maxlength="6" pattern="[0-9]{6}" required autofocus>
</div>
<button class="btn btn-primary w-100 mb-2" type="submit">Bestätigen</button>
</form>
<p class="text-center text-muted mt-3 small">
<a href="#" onclick="document.getElementById('recovery-form').style.display='block';this.style.display='none';return false;">
Code verloren?
</a>
</p>
<form id="recovery-form" method="post" style="display:none;">
<input type="hidden" name="action" value="recovery">
<div class="mb-2">
<label class="form-label">Recovery-Code eingeben</label>
<input type="text" name="code" class="form-control" placeholder="ABCD-123">
</div>
<button class="btn btn-warning w-100" type="submit">Mit Recovery-Code anmelden</button>
</form>
</div>
</div>
</div>
</div>
</div>
</div>
{% include "_footer.html" %}
</div>
</body>
</html>

View File

@@ -488,7 +488,9 @@ let lastHash = null;
async function checkForUpdates() {
try {
const res = await fetch(`/playlist/${site}/${screen}/hash`, { cache: "no-store" });
const w = window.screen ? window.screen.width : '?';
const h = window.screen ? window.screen.height : '?';
const res = await fetch(`/playlist/${site}/${screen}/hash?w=${w}&h=${h}`, { cache: "no-store" });
const data = await res.json();
const hash = data.hash;
const serverStayOnFirst = data.stay_on_first;

View File

@@ -76,6 +76,7 @@
<th>Rolle</th>
<th>Standorte</th>
<th>Abteilung</th>
<th>MFA</th>
<th>Status</th>
<th>Aktionen</th>
</tr>
@@ -106,6 +107,13 @@
{% endif %}
</td>
<td class="text-muted small">{{ u.department }}</td>
<td>
{% if u.mfa_enabled %}
<span class="badge bg-green text-green-fg">Aktiv</span>
{% else %}
<span class="text-muted"></span>
{% endif %}
</td>
<td>
{% if u.must_change_password %}
<span class="badge bg-warning text-white">Passwort-Änderung erforderlich</span>