Files
2026-10-14 13:34:37 +02:00

355 lines
22 KiB
HTML
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
{% extends "base.html" %}
{% block title %}2FA-hulp{% endblock %}
{% block page_title %}2FA-hulp{% endblock %}
{% block main_class %}{{ '' if tab == 'lijst' else 'medium' }}{% endblock %}
{% block extra_css %}
<style>
.user-head { display: flex; flex-wrap: wrap; align-items: center; gap: 14px; margin-bottom: 4px; }
.avatar { width: 46px; height: 46px; border-radius: 50%; display: grid; place-items: center; flex-shrink: 0;
background: var(--primary-light); color: var(--primary-color); font-weight: 800; font-size: 18px; }
.user-head .who { flex: 1; min-width: 220px; }
.user-head .who strong { display: block; font-size: 1.1rem; }
.badges { display: flex; flex-wrap: wrap; gap: 6px; }
.actions-grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(280px, 1fr)); gap: 16px; margin-top: 18px; }
@media (max-width: 760px) { .actions-grid { grid-template-columns: 1fr; } }
.action-box { border: 1px solid var(--border); border-radius: var(--radius); padding: 18px; background: var(--surface-muted); }
.action-box h3 { margin: 0 0 4px; font-size: 1rem; }
.action-box p { font-size: 14px; color: var(--text-muted); margin: 0 0 14px; }
.action-box .btn-row { display: flex; flex-wrap: wrap; gap: 8px; }
.code-panel { margin-top: 18px; border: 2px solid var(--success); border-radius: var(--radius-lg); background: var(--success-bg); padding: 20px; text-align: center; }
.big-code { font-family: ui-monospace, Consolas, monospace; font-size: 40px; font-weight: 800; letter-spacing: 6px; color: var(--success-text); margin: 6px 0 4px; }
.code-grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(120px, 1fr)); gap: 6px; margin-top: 12px; }
.code-grid span { font-family: ui-monospace, Consolas, monospace; background: #fff; border: 1px solid var(--border); border-radius: 6px; padding: 6px; font-size: 15px; }
details summary { cursor: pointer; color: var(--text-muted); font-size: 14px; margin-top: 12px; }
.exception-active { margin-top: 14px; }
.pick-list { list-style: none; margin: 10px 0 0; padding: 0; }
.pick-list a { display: flex; justify-content: space-between; gap: 12px; padding: 10px 12px; border-radius: var(--radius-sm); text-decoration: none; color: var(--text); }
.pick-list a:hover { background: var(--primary-light); }
.pick-list .ou { color: var(--text-muted); font-size: 13px; }
.setup-hint { font-size: 13px; }
/* Afdrukken: enkel het codebriefje */
@media print {
body.print-slip * { visibility: hidden; }
body.print-slip #codeSlip, body.print-slip #codeSlip * { visibility: visible; }
body.print-slip #codeSlip { position: absolute; left: 0; top: 0; width: 100%; border: 1px dashed #999; background: #fff; }
body.print-slip #codeSlip .no-print { display: none; }
}
</style>
{% endblock %}
{% block content %}
<div class="tabs">
<a class="tab-link {{ 'active' if tab != 'lijst' else '' }}" href="{{ url_for('tweestaps.overzicht') }}" style="text-decoration:none">🆘 Hulp bij inloggen</a>
<a class="tab-link {{ 'active' if tab == 'lijst' else '' }}" href="{{ url_for('tweestaps.overzicht', tab='lijst') }}" style="text-decoration:none" onclick="showLoader('Gebruikers ophalen bij Google...')">⚠️ Nog geen 2FA ingesteld</a>
</div>
{% if tab != 'lijst' %}
<section class="tab-content card active">
{# ---------- Zoeken ---------- #}
<form class="search-bar" method="GET" action="{{ url_for('tweestaps.overzicht') }}" onsubmit="showLoader()" style="margin:0">
<input type="text" name="zoek" placeholder="Naam of e-mailadres van de leerkracht" value="{{ zoek or (gebruiker.email if gebruiker else '') }}" autofocus>
<button type="submit">🔍 Zoeken</button>
</form>
{% if zoekresultaten %}
<ul class="pick-list">
{% for u in zoekresultaten %}
<li><a href="{{ url_for('tweestaps.overzicht', email=u.email) }}" onclick="showLoader()">
<span><strong>{{ u.naam }}</strong> · {{ u.email }}</span><span class="ou">{{ u.ou }}</span></a></li>
{% endfor %}
</ul>
{% endif %}
{% if fout %}<div class="alert alert-danger" style="margin-top:14px">{{ fout }}</div>{% endif %}
{# ---------- Gekozen gebruiker ---------- #}
{% if gebruiker %}
<div style="margin-top: 22px; padding-top: 20px; border-top: 1px solid var(--border);">
<div class="user-head">
<span class="avatar">{{ (gebruiker.naam or gebruiker.email)[:1]|upper }}</span>
<div class="who">
<strong>{{ gebruiker.naam }}</strong>
<span class="muted">{{ gebruiker.email }} · {{ gebruiker.ou }}</span>
</div>
<div class="badges">
{% if gebruiker.ingesteld %}<span class="badge badge-success">✓ 2FA ingesteld</span>
{% else %}<span class="badge badge-warning">2FA niet ingesteld</span>{% endif %}
{% if uitzondering %}<span class="badge badge-info">⏳ Uitzondering tot {{ uitzondering.Tot.strftime('%H:%M') }}</span>
{% elif gebruiker.afgedwongen %}<span class="badge">Verplicht</span>{% endif %}
{% if gebruiker.geschorst %}<span class="badge badge-danger">Geschorst</span>{% endif %}
</div>
</div>
{# ---------- Codes tonen ---------- #}
{% if codes %}
<div class="code-panel" id="codeSlip">
<div class="muted small">Back-upcode voor <strong>{{ gebruiker.email }}</strong></div>
<div class="big-code">{{ codes[0][:4] }} {{ codes[0][4:] }}</div>
<div class="small">Typ deze code bij "Probeer een andere manier" › "Voer een van je back-upcodes van 8 cijfers in". Elke code werkt maar één keer.</div>
<details class="no-print">
<summary>Alle {{ codes|length }} geldige codes tonen</summary>
<div class="code-grid">{% for c in codes %}<span>{{ c[:4] }} {{ c[4:] }}</span>{% endfor %}</div>
</details>
<div class="btn-row no-print" style="justify-content:center; margin-top:14px">
<button type="button" class="btn-light btn-sm" onclick="printSlip()">🖨️ Afdrukken</button>
</div>
{% if code_actie == 'nieuw' %}<div class="small muted no-print" style="margin-top:8px">Er is een nieuwe set aangemaakt; oudere codes van deze gebruiker werken niet meer.</div>{% endif %}
</div>
{% endif %}
<div class="actions-grid">
{% if mag('tweestaps_codes') %}
<div class="action-box">
<h3>📱 Telefoon vergeten</h3>
<p>Geef een back-upcode. 2FA blijft gewoon aan; de code werkt één keer.</p>
<form method="POST" action="{{ url_for('tweestaps.codes') }}" class="btn-row" onsubmit="showLoader()">
<input type="hidden" name="email" value="{{ gebruiker.email }}">
<button type="submit" name="actie" value="tonen">🔑 Back-upcode geven</button>
<button type="submit" name="actie" value="nieuw" class="btn-light"
onclick="return confirm('Een nieuwe set maken? Alle oudere back-upcodes van deze gebruiker werken dan niet meer.')">Nieuwe set</button>
<button type="submit" name="actie" value="ongeldig" class="btn-ghost"
onclick="return confirm('Alle back-upcodes van deze gebruiker ongeldig maken?')">Codes intrekken</button>
</form>
</div>
{% endif %}
{% if mag('tweestaps_uitzetten') and reset_wacht %}
<div class="action-box">
<h3>⏳ 2FA-reset wacht op Google</h3>
<p>Gevraagd om {{ reset_wacht.Gevraagd_op.strftime('%H:%M') }} door {{ reset_wacht.Door or '?' }}.
{{ gebruiker.email }} staat al in de uitzonderingsgroep, maar Google past dat pas na enkele minuten toe.
PIVOT probeert elke minuut opnieuw (al {{ reset_wacht.Pogingen }} poging(en)), tot {{ reset_wacht.Tot.strftime('%H:%M') }}.
Je hoeft niets te doen.</p>
<p>Google meldt nu: 2FA afgedwongen voor deze gebruiker: <strong>{{ 'ja (uitzondering nog niet toegepast)' if gebruiker.afgedwongen else 'nee, de reset volgt bij de volgende poging' }}</strong>.
{% if reset_wacht.Laatste_fout %}<br><span class="muted">Laatste antwoord van Google: {{ reset_wacht.Laatste_fout }}</span>{% endif %}</p>
<p class="field-hint">Moet de gebruiker nu meteen binnen? Toon hierboven een <strong>back-upcode</strong>: daarmee logt hij in en stelt hij zijn nieuwe telefoon in via myaccount.google.com › Beveiliging › Authenticatie in 2 stappen. Dat werkt meteen, ook als 2FA afgedwongen wordt.</p>
<a class="btn-light" href="{{ url_for('tweestaps.overzicht', email=gebruiker.email) }}">🔄 Status vernieuwen</a>
</div>
{% elif mag('tweestaps_uitzetten') and gebruiker.ingesteld %}
<div class="action-box">
<h3>📵 Nieuwe telefoon: 2FA resetten</h3>
<p class="field-hint">💡 Snelste weg bij een nieuwe telefoon: geef de gebruiker een <strong>back-upcode</strong> (hierboven). Hij logt daarmee in en koppelt zelf zijn nieuwe telefoon. Resetten is pas nodig als dat niet lukt.</p>
<p>Zet 2FA uit zodat de gebruiker het opnieuw kan instellen met een nieuw toestel. Oude back-upcodes en het oude toestel werken daarna niet meer.
{% if gebruiker.afgedwongen %}Omdat 2FA verplicht is, krijgt hij ook een tijdelijke uitzondering tot vandaag {{ einde.strftime('%H:%M') }} om opnieuw in te stellen.{% endif %}</p>
<form method="POST" action="{{ url_for('tweestaps.reset') }}" onsubmit="return confirm('2FA resetten voor {{ gebruiker.email }}? De gebruiker moet 2FA daarna opnieuw instellen.') && (showLoader(), true)">
<input type="hidden" name="email" value="{{ gebruiker.email }}">
<div class="field" style="margin-bottom:10px">
<input type="text" name="reden" maxlength="255" placeholder="Reden (optioneel), bv. nieuwe telefoon">
</div>
<button type="submit" class="btn-danger">📵 2FA resetten</button>
</form>
</div>
{% endif %}
{% if mag('tweestaps_uitzetten') %}
<div class="action-box">
<h3>⏳ Tijdelijk zonder 2FA</h3>
{% if uitzondering %}
<p>Deze gebruiker kan tot <strong>{{ uitzondering.Tot.strftime('%d-%m %H:%M') }}</strong> zonder 2FA inloggen. Daarna wordt 2FA automatisch terug verplicht.</p>
<div class="btn-row">
{% if uitzondering.Tot < einde %}
<form method="POST" action="{{ url_for('tweestaps.uitzondering') }}" style="margin:0" onsubmit="showLoader()">
<input type="hidden" name="email" value="{{ gebruiker.email }}">
<button type="submit" class="btn-light">Verlengen tot {{ einde.strftime('%H:%M') }}</button>
</form>
{% endif %}
<form method="POST" action="{{ url_for('tweestaps.stop_uitzondering', uitz_id=uitzondering.ID) }}" style="margin:0" onsubmit="showLoader()">
<input type="hidden" name="terug" value="{{ gebruiker.email }}">
<button type="submit" class="btn-danger">Nu beëindigen</button>
</form>
</div>
{% elif not groep %}
<p>Nog niet ingesteld: vul de <strong>Uitzonderingsgroep</strong> in bij
{% if mag('koppelingen') %}<a href="{{ url_for('scholen.mijn_school') }}">🔌 Koppelingen van deze school</a>{% else %}🔌 Koppelingen van deze school{% endif %}
› 🛡️ 2FA-hulp (een Google-groep waarvoor 2FA niet afgedwongen wordt).</p>
{% else %}
<p>Voor wie 2FA nog moet instellen, of als een back-upcode niet lukt. De uitzondering loopt tot
<strong>vandaag {{ einde.strftime('%H:%M') }}</strong>; daarna zet PIVOT 2FA automatisch terug aan.</p>
<form method="POST" action="{{ url_for('tweestaps.uitzondering') }}" onsubmit="showLoader()">
<input type="hidden" name="email" value="{{ gebruiker.email }}">
<div class="field" style="margin-bottom:10px">
<input type="text" name="reden" maxlength="255" placeholder="Reden (optioneel), bv. nieuwe telefoon">
</div>
<button type="submit" class="btn-warning">⏳ Uitzondering geven tot {{ einde.strftime('%H:%M') }}</button>
</form>
{% endif %}
</div>
{% endif %}
</div>
</div>
{% endif %}
</section>
{# ---------- Lopende uitzonderingen ---------- #}
<div class="card">
<h3 class="card-title">⏳ Lopende uitzonderingen</h3>
{% if actief %}
<div class="table-wrap">
<table>
<thead><tr><th>Gebruiker</th><th>Tot</th><th>Gegeven door</th><th>Reden</th><th class="actions">Actie</th></tr></thead>
<tbody>
{% for u in actief %}
<tr>
<td><a href="{{ url_for('tweestaps.overzicht', email=u.Email) }}"><strong>{{ u.Naam or u.Email }}</strong></a><div class="muted small">{{ u.Email }}</div></td>
<td class="nowrap">
{{ u.Tot.strftime('%H:%M') if u.Tot.date() == nu.date() else u.Tot.strftime('%d-%m %H:%M') }}
{% if u.Tot <= nu %}<span class="badge badge-warning">wordt beëindigd</span>{% endif %}
</td>
<td>{{ u.Door or '?' }}</td>
<td class="muted">{{ u.Reden or '' }}</td>
<td class="actions">
{% if mag('tweestaps_uitzetten') %}
<form method="POST" action="{{ url_for('tweestaps.stop_uitzondering', uitz_id=u.ID) }}" onsubmit="showLoader()">
<button type="submit" class="btn-sm btn-danger">Nu beëindigen</button>
</form>
{% endif %}
</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
{% else %}
<p class="muted" style="margin:0">Geen lopende uitzonderingen. Iedereen moet 2FA gebruiken.</p>
{% endif %}
</div>
{# ---------- Logboek ---------- #}
{% if logboek %}
<div class="card" id="logboek">
<div class="page-head" style="margin-bottom:10px">
<h3 class="card-title" style="margin:0">🕑 Recente acties</h3>
<span class="muted small">{{ log_totaal }} actie(s)</span>
</div>
<div class="table-wrap">
<table>
<thead><tr><th>Tijdstip</th><th>Door</th><th>Actie</th><th>Gebruiker</th><th>Details</th></tr></thead>
<tbody>
{% for l in logboek %}
<tr>
<td class="nowrap">{{ l.Tijdstip.strftime('%d-%m %H:%M') }}</td>
<td>{{ l.Door or '?' }}</td>
<td>{{ l.Actie }}</td>
<td>{{ l.Email }}</td>
<td class="muted">{{ l.Details or '' }}</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
{% set extra = {'email': gebruiker.email} if gebruiker else {} %}
<div style="display:flex;flex-wrap:wrap;justify-content:space-between;align-items:center;gap:10px;margin-top:12px">
<form method="GET" action="{{ url_for('tweestaps.overzicht') }}#logboek" style="margin:0">
{% if gebruiker %}<input type="hidden" name="email" value="{{ gebruiker.email }}">{% endif %}
<label class="per-pagina">Toon
<select name="logper" onchange="this.form.submit()" aria-label="Aantal per pagina">
{% for n in log_keuzes %}<option value="{{ n }}" {% if n == log_per %}selected{% endif %}>{{ n or 'alles' }}</option>{% endfor %}
</select> per pagina</label>
</form>
{% if log_paginas > 1 %}
<nav class="paginering" aria-label="Pagina's logboek" style="margin-top:0">
{% if log_pagina > 1 %}
<a class="btn btn-sm btn-light" href="{{ url_for('tweestaps.overzicht', logpagina=1, **extra) }}#logboek" title="Eerste pagina">«</a>
<a class="btn btn-sm btn-light" href="{{ url_for('tweestaps.overzicht', logpagina=log_pagina - 1, **extra) }}#logboek">‹ Vorige</a>
{% endif %}
{% for p in range([1, log_pagina - 2]|max, [log_paginas, log_pagina + 2]|min + 1) %}
<a class="btn btn-sm {{ 'btn-secondary' if p == log_pagina else 'btn-light' }}"
href="{{ url_for('tweestaps.overzicht', logpagina=p, **extra) }}#logboek" {% if p == log_pagina %}aria-current="page"{% endif %}>{{ p }}</a>
{% endfor %}
{% if log_pagina < log_paginas %}
<a class="btn btn-sm btn-light" href="{{ url_for('tweestaps.overzicht', logpagina=log_pagina + 1, **extra) }}#logboek">Volgende ›</a>
<a class="btn btn-sm btn-light" href="{{ url_for('tweestaps.overzicht', logpagina=log_paginas, **extra) }}#logboek" title="Laatste pagina">»</a>
{% endif %}
<span class="muted small" style="margin-left:6px">pagina {{ log_pagina }} van {{ log_paginas }}</span>
</nav>
{% endif %}
</div>
</div>
{% endif %}
{% else %}
{# ---------- Tab: nog geen 2FA ingesteld ---------- #}
<section class="tab-content card active">
<div class="page-head">
<div>
<h2 style="margin:0">Nog geen 2FA ingesteld</h2>
<div class="muted small">Actieve accounts in {{ ous }} zonder 2FA</div>
</div>
<div class="actions">
<a class="btn btn-light" href="{{ url_for('tweestaps.overzicht', tab='lijst', vernieuw=1) }}" onclick="showLoader('Gebruikers ophalen bij Google...')">🔄 Vernieuwen</a>
</div>
</div>
{% if lijst_fout %}<div class="alert alert-danger">{{ lijst_fout }}</div>{% endif %}
{% if lijst is not none %}
<div class="table-wrap">
<table>
<thead><tr><th class="sorteer" data-kol="0" data-type="tekst" title="Sorteren op naam">Naam <span class="pijl"></span></th>
<th class="sorteer" data-kol="1" data-type="tekst" title="Sorteren op e-mail">E-mail <span class="pijl"></span></th><th>OU</th>
<th class="sorteer" data-kol="3" data-type="datum" title="Sorteren op laatste login">Laatste login <span class="pijl"></span></th>
<th class="sorteer" data-kol="4" data-type="datum" title="Sorteren op status">Status <span class="pijl"></span></th><th class="actions"></th></tr></thead>
<tbody id="lijstBody">
{% set uitz = actief|map(attribute='Email')|list %}
{% for u in lijst %}
<tr>
<td data-sort="{{ u.naam|lower }}"><strong>{{ u.naam }}</strong></td>
<td data-sort="{{ u.email|lower }}">{{ u.email }}</td>
<td class="muted">{{ u.ou }}</td>
<td class="nowrap" data-sort="{{ u.laatste_login or '' }}">{{ u.laatste_login or 'Nooit' }}</td>
{# volgorde bij sorteren: kan niet inloggen → uitzondering → nog niet verplicht; daarbinnen op naam #}
<td data-sort="{{ '2' if u.email in uitz else ('1' if u.afgedwongen else '3') }} {{ u.naam|lower }}">
{% if u.email in uitz %}<span class="badge badge-info">Uitzondering</span>
{% elif u.afgedwongen %}<span class="badge badge-danger">Kan niet inloggen</span>
{% else %}<span class="badge badge-warning">Nog niet verplicht</span>{% endif %}
</td>
<td class="actions"><a class="btn btn-sm btn-light" href="{{ url_for('tweestaps.overzicht', email=u.email) }}">Helpen</a></td>
</tr>
{% else %}
<tr class="empty-row"><td colspan="6">🎉 Iedereen heeft 2FA ingesteld.</td></tr>
{% endfor %}
</tbody>
</table>
</div>
{% if lijst %}<p class="field-hint">{{ lijst|length }} account(s). "Kan niet inloggen": voor deze mensen wordt 2FA afgedwongen maar ze hebben het nog niet ingesteld. Geef ze een tijdelijke uitzondering zodat ze het kunnen instellen.</p>{% endif %}
{% endif %}
</section>
{% endif %}
{% endblock %}
{% block scripts %}
<style>
.paginering { display: flex; flex-wrap: wrap; gap: 6px; align-items: center; margin-top: 12px; }
th.sorteer { cursor: pointer; user-select: none; white-space: nowrap; }
th.sorteer:hover { color: var(--primary-color); }
th.sorteer .pijl::after { content: "↕"; opacity: .35; margin-left: 2px; }
th.sorteer.op .pijl::after { content: "▲"; opacity: 1; }
th.sorteer.af .pijl::after { content: "▼"; opacity: 1; }
</style>
<script>
// Sorteren via de kolomkop (Naam, Laatste login). "Nooit" telt als oudste.
document.querySelectorAll('th.sorteer').forEach(function (th) {
th.addEventListener('click', function () {
var body = document.getElementById('lijstBody');
if (!body) return;
var kol = +th.dataset.kol, op = !th.classList.contains('op');
document.querySelectorAll('th.sorteer').forEach(function (x) { x.classList.remove('op', 'af'); });
th.classList.add(op ? 'op' : 'af');
var rijen = Array.prototype.filter.call(body.rows, function (r) { return !r.classList.contains('empty-row'); });
rijen.sort(function (a, b) {
var x = a.cells[kol].dataset.sort || '', y = b.cells[kol].dataset.sort || '';
var c = th.dataset.type === 'tekst' ? x.localeCompare(y, 'nl') : (x < y ? -1 : x > y ? 1 : 0);
return op ? c : -c;
});
rijen.forEach(function (r) { body.appendChild(r); });
});
});
function printSlip() {
document.body.classList.add('print-slip');
window.print();
setTimeout(() => document.body.classList.remove('print-slip'), 500);
}
</script>
{% endblock %}