snapshot: preserve role-aware notification preferences and operational alerts
This commit is contained in:
@@ -18,7 +18,11 @@
|
||||
<div class="account-hero-copy">
|
||||
<span class="account-kicker">{% trans "Konto" %}</span>
|
||||
<h1>{% trans "Profil" %}</h1>
|
||||
<p>{% trans "Ihre aktuelle Workdock-Kontoübersicht und wichtige Sicherheitsaktionen." %}</p>
|
||||
<p>{% trans "Ihre aktuelle Kontoübersicht und wichtige Sicherheitsaktionen." %}</p>
|
||||
<p class="account-hero-submeta">
|
||||
{% trans "Letzte Anmeldung" %}:
|
||||
<strong>{% if account_user.last_login %}{{ account_user.last_login|date:"d.m.Y H:i" }}{% else %}-{% endif %}</strong>
|
||||
</p>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
@@ -53,24 +57,36 @@
|
||||
<h2>{{ account_user.get_full_name|default:account_user.username }}</h2>
|
||||
<p>{{ account_user.email|default:account_user.username }}</p>
|
||||
</div>
|
||||
<div class="account-profile-meta">
|
||||
<div>
|
||||
<span>{% trans "Position" %}</span>
|
||||
<strong>{{ account_user_profile.job_title|default:"-" }}</strong>
|
||||
</div>
|
||||
<div>
|
||||
<span>{% trans "Abteilung" %}</span>
|
||||
<strong>{{ account_user_profile.department|default:"-" }}</strong>
|
||||
</div>
|
||||
<div>
|
||||
<span>{% trans "Letzte Anmeldung" %}</span>
|
||||
<strong>{% if account_user.last_login %}{{ account_user.last_login|date:"d.m.Y H:i" }}{% else %}-{% endif %}</strong>
|
||||
</div>
|
||||
<div class="account-nav">
|
||||
<button
|
||||
class="account-nav-item is-active"
|
||||
type="button"
|
||||
data-account-tab="details"
|
||||
aria-expanded="true"
|
||||
>
|
||||
{% trans "Kontodaten" %}
|
||||
</button>
|
||||
<button
|
||||
class="account-nav-item"
|
||||
type="button"
|
||||
data-account-tab="security"
|
||||
aria-expanded="false"
|
||||
>
|
||||
{% trans "Sicherheit & Aktionen" %}
|
||||
</button>
|
||||
<button
|
||||
class="account-nav-item"
|
||||
type="button"
|
||||
data-account-tab="notifications"
|
||||
aria-expanded="false"
|
||||
>
|
||||
{% trans "Benachrichtigungen" %}
|
||||
</button>
|
||||
</div>
|
||||
</aside>
|
||||
|
||||
<div class="account-main">
|
||||
<section class="account-panel">
|
||||
<section class="account-panel" data-account-panel="details">
|
||||
<div class="account-panel-head">
|
||||
<div>
|
||||
<h2>{% trans "Kontodaten" %}</h2>
|
||||
@@ -156,13 +172,92 @@
|
||||
</form>
|
||||
</section>
|
||||
|
||||
<section class="account-panel">
|
||||
<section class="account-panel" data-account-panel="notifications" hidden>
|
||||
<div class="account-panel-head">
|
||||
<div>
|
||||
<h2>{% trans "Benachrichtigungen" %}</h2>
|
||||
<p>{% trans "Legen Sie fest, welche Workflow-Ereignisse im Header als Benachrichtigung erscheinen sollen." %}</p>
|
||||
</div>
|
||||
<button
|
||||
class="btn btn-secondary account-inline-edit-trigger"
|
||||
type="button"
|
||||
data-account-edit-toggle="notifications"
|
||||
aria-expanded="{% if notifications_edit_open %}true{% else %}false{% endif %}"
|
||||
>
|
||||
{% trans "Bearbeiten" %}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div class="account-inline-view{% if notifications_edit_open %} is-hidden{% endif %}" data-account-edit-view="notifications">
|
||||
{% for group in notification_preference_groups %}
|
||||
<section class="account-notification-group">
|
||||
<h3>{{ group.label }}</h3>
|
||||
<div class="account-notification-pref-grid">
|
||||
{% for field in group.fields %}
|
||||
<div class="account-notification-pref-item">
|
||||
<span>{{ field.label }}</span>
|
||||
<strong>{% if field.value %}{% trans "Aktiv" %}{% else %}{% trans "Aus" %}{% endif %}</strong>
|
||||
</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
</section>
|
||||
{% endfor %}
|
||||
</div>
|
||||
|
||||
<form
|
||||
class="account-inline-form{% if not notifications_edit_open %} is-hidden{% endif %}"
|
||||
method="post"
|
||||
data-account-edit-form="notifications"
|
||||
>
|
||||
{% csrf_token %}
|
||||
<input type="hidden" name="account_form" value="notification_preferences" />
|
||||
{% for group in notification_preference_groups %}
|
||||
<section class="account-notification-group">
|
||||
<h3>{{ group.label }}</h3>
|
||||
<div class="account-notification-pref-grid account-notification-pref-grid-edit">
|
||||
{% for field in group.fields %}
|
||||
<label class="account-notification-pref-toggle{% if field.errors %} has-error{% endif %}" for="{{ field.id_for_label }}">
|
||||
<span class="account-notification-pref-copy">
|
||||
<strong>{{ field.label }}</strong>
|
||||
<small>
|
||||
{% if field.name == 'onboarding_success' %}{% trans "Benachrichtigung nach erfolgreich abgeschlossenem Onboarding." %}
|
||||
{% elif field.name == 'onboarding_failure' %}{% trans "Benachrichtigung wenn ein Onboarding fehlschlägt." %}
|
||||
{% elif field.name == 'offboarding_success' %}{% trans "Benachrichtigung nach erfolgreich abgeschlossenem Offboarding." %}
|
||||
{% elif field.name == 'offboarding_failure' %}{% trans "Benachrichtigung wenn ein Offboarding fehlschlägt." %}
|
||||
{% elif field.name == 'backup_success' %}{% trans "Benachrichtigung bei erfolgreicher Backup-Erstellung oder Verifikation." %}
|
||||
{% elif field.name == 'backup_failure' %}{% trans "Benachrichtigung wenn Backup-Aktionen fehlschlagen." %}
|
||||
{% elif field.name == 'welcome_email_success' %}{% trans "Benachrichtigung wenn eine geplante Welcome E-Mail erfolgreich gesendet wurde." %}
|
||||
{% elif field.name == 'welcome_email_failure' %}{% trans "Benachrichtigung wenn eine geplante Welcome E-Mail fehlschlägt." %}
|
||||
{% elif field.name == 'trial_alerts' %}{% trans "Hinweise zu Trial-Ablauf, Ablaufdatum oder Deaktivierung." %}
|
||||
{% else %}{% trans "Hinweise aus Systemtests wie SMTP oder Nextcloud." %}{% endif %}
|
||||
</small>
|
||||
</span>
|
||||
<span class="account-toggle-control">
|
||||
{{ field }}
|
||||
<span class="account-toggle-slider" aria-hidden="true"></span>
|
||||
</span>
|
||||
</label>
|
||||
{% if field.errors %}
|
||||
<div class="account-form-error">{{ field.errors|join:", " }}</div>
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
</div>
|
||||
</section>
|
||||
{% endfor %}
|
||||
<div class="account-inline-actions">
|
||||
<button class="btn btn-primary" type="submit">{% trans "Speichern" %}</button>
|
||||
<button class="btn btn-secondary" type="button" data-account-edit-cancel="notifications">{% trans "Abbrechen" %}</button>
|
||||
</div>
|
||||
</form>
|
||||
</section>
|
||||
|
||||
<section class="account-panel" data-account-panel="security" hidden>
|
||||
<div class="account-panel-head">
|
||||
<h2>{% trans "Sicherheit & Aktionen" %}</h2>
|
||||
</div>
|
||||
|
||||
<div class="account-security-overview">
|
||||
<div class="account-security-item">
|
||||
<div class="account-security-item{% if account_user_profile.totp_enabled %} account-security-item-active{% endif %}">
|
||||
<span>{% trans "TOTP" %}</span>
|
||||
<strong>{% if account_user_profile.totp_enabled %}{% trans "Aktiv" %}{% else %}{% trans "Aus" %}{% endif %}</strong>
|
||||
<p>
|
||||
@@ -198,60 +293,97 @@
|
||||
<h3>{% trans "Zwei-Faktor-Authentifizierung" %}</h3>
|
||||
<p>{% trans "Aktivieren Sie TOTP mit einer Authenticator-App. Standardmäßig bleibt es ausgeschaltet." %}</p>
|
||||
</div>
|
||||
<span class="account-chip{% if not account_user_profile.totp_enabled %} account-chip-muted{% endif %}">
|
||||
{% if account_user_profile.totp_enabled %}
|
||||
{% trans "Aktiv" %}
|
||||
{% else %}
|
||||
{% trans "Aus" %}
|
||||
{% endif %}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
{% if account_user_profile.totp_enabled %}
|
||||
<div class="account-detail-grid">
|
||||
<div class="account-detail">
|
||||
<span>{% trans "Status" %}</span>
|
||||
<div class="account-totp-status-row">
|
||||
<div class="account-totp-status-copy">
|
||||
<strong>{% trans "TOTP ist aktiviert." %}</strong>
|
||||
<p>{% trans "Bestätigt am" %}: {% if account_user_profile.totp_confirmed_at %}{{ account_user_profile.totp_confirmed_at|date:"d.m.Y H:i" }}{% else %}-{% endif %}</p>
|
||||
</div>
|
||||
<div class="account-detail">
|
||||
<span>{% trans "Bestätigt am" %}</span>
|
||||
<strong>{% if account_user_profile.totp_confirmed_at %}{{ account_user_profile.totp_confirmed_at|date:"d.m.Y H:i" }}{% else %}-{% endif %}</strong>
|
||||
</div>
|
||||
<button
|
||||
class="btn btn-secondary"
|
||||
type="button"
|
||||
data-disable-toggle
|
||||
aria-expanded="{% if totp_disable_form.errors %}true{% else %}false{% endif %}"
|
||||
>
|
||||
{% trans "TOTP deaktivieren" %}
|
||||
</button>
|
||||
</div>
|
||||
<form class="account-totp-form" method="post">
|
||||
<form class="account-totp-form{% if not totp_disable_form.errors %} is-hidden{% endif %}" method="post" data-disable-form>
|
||||
{% csrf_token %}
|
||||
<input type="hidden" name="account_form" value="totp_disable" />
|
||||
<div class="account-form-grid">
|
||||
{% for field in totp_disable_form %}
|
||||
<div class="account-form-field{% if field.errors %} has-error{% endif %}">
|
||||
<label for="{{ field.id_for_label }}">{{ field.label }}</label>
|
||||
{{ field }}
|
||||
{% if field.errors %}
|
||||
<div class="account-form-error">{{ field.errors|join:", " }}</div>
|
||||
<div class="account-form-field">
|
||||
<label for="{{ totp_disable_form.current_password.id_for_label }}">{{ totp_disable_form.current_password.label }}</label>
|
||||
{{ totp_disable_form.current_password }}
|
||||
{% if totp_disable_form.current_password.errors %}
|
||||
<div class="account-form-error">{{ totp_disable_form.current_password.errors|join:", " }}</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
{% endfor %}
|
||||
{% if totp_disable_form.non_field_errors %}
|
||||
<div class="account-form-field account-form-field-wide">
|
||||
<div class="account-form-error">{{ totp_disable_form.non_field_errors|join:", " }}</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
<div class="account-inline-actions">
|
||||
<button class="btn btn-secondary" type="submit">{% trans "TOTP deaktivieren" %}</button>
|
||||
<button class="btn btn-secondary" type="submit">{% trans "Deaktivierung bestätigen" %}</button>
|
||||
<button class="btn btn-secondary" type="button" data-disable-cancel>{% trans "Abbrechen" %}</button>
|
||||
</div>
|
||||
</form>
|
||||
<form class="account-totp-form" method="post">
|
||||
{% csrf_token %}
|
||||
<input type="hidden" name="account_form" value="totp_regenerate_codes" />
|
||||
<div class="account-form-grid">
|
||||
{% for field in totp_regenerate_form %}
|
||||
<div class="account-form-field{% if field.errors %} has-error{% endif %}">
|
||||
<label for="{{ field.id_for_label }}">{{ field.label }}</label>
|
||||
{{ field }}
|
||||
{% if field.errors %}
|
||||
<div class="account-form-error">{{ field.errors|join:", " }}</div>
|
||||
<div class="account-totp-action-row">
|
||||
<div class="account-totp-action-copy">
|
||||
<strong>{% trans "Recovery-Codes neu erzeugen" %}</strong>
|
||||
<p>{% trans "Neue Recovery-Codes sollten nur erzeugt werden, wenn die bisherigen Codes nicht mehr sicher sind." %}</p>
|
||||
</div>
|
||||
<button
|
||||
class="btn btn-primary"
|
||||
type="button"
|
||||
data-regenerate-toggle
|
||||
aria-expanded="{% if totp_regenerate_form.errors %}true{% else %}false{% endif %}"
|
||||
>
|
||||
{% trans "Recovery-Codes neu erzeugen" %}
|
||||
</button>
|
||||
</div>
|
||||
<div class="account-form-grid{% if not totp_regenerate_form.errors %} is-hidden{% endif %}" data-regenerate-form>
|
||||
<div class="account-form-field{% if totp_regenerate_form.verification_code.errors %} has-error{% endif %}">
|
||||
<label for="{{ totp_regenerate_form.verification_code.id_for_label }}">{{ totp_regenerate_form.verification_code.label }}</label>
|
||||
{{ totp_regenerate_form.verification_code }}
|
||||
{% if totp_regenerate_form.verification_code.errors %}
|
||||
<div class="account-form-error">{{ totp_regenerate_form.verification_code.errors|join:", " }}</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
{% endfor %}
|
||||
<div class="account-form-field account-form-field-wide">
|
||||
<button
|
||||
class="btn btn-secondary account-recovery-toggle"
|
||||
type="button"
|
||||
data-recovery-toggle
|
||||
aria-expanded="{% if totp_regenerate_form.recovery_code.value %}true{% else %}false{% endif %}"
|
||||
aria-controls="account-regenerate-recovery-code"
|
||||
>
|
||||
{% trans "Stattdessen Recovery-Code verwenden" %}
|
||||
</button>
|
||||
</div>
|
||||
<div class="account-form-field account-form-field-wide{% if not totp_regenerate_form.recovery_code.value %} is-hidden{% endif %}" id="account-regenerate-recovery-code" data-recovery-field>
|
||||
<label for="{{ totp_regenerate_form.recovery_code.id_for_label }}">{{ totp_regenerate_form.recovery_code.label }}</label>
|
||||
{{ totp_regenerate_form.recovery_code }}
|
||||
{% if totp_regenerate_form.recovery_code.errors %}
|
||||
<div class="account-form-error">{{ totp_regenerate_form.recovery_code.errors|join:", " }}</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
{% if totp_regenerate_form.non_field_errors %}
|
||||
<div class="account-form-field account-form-field-wide">
|
||||
<div class="account-form-error">{{ totp_regenerate_form.non_field_errors|join:", " }}</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
<div class="account-inline-actions">
|
||||
<button class="btn btn-primary" type="submit">{% trans "Recovery-Codes neu erzeugen" %}</button>
|
||||
<div class="account-inline-actions{% if not totp_regenerate_form.errors %} is-hidden{% endif %}" data-regenerate-actions>
|
||||
<button class="btn btn-primary" type="submit">{% trans "Erzeugung bestätigen" %}</button>
|
||||
<button class="btn btn-secondary" type="button" data-regenerate-cancel>{% trans "Abbrechen" %}</button>
|
||||
</div>
|
||||
</form>
|
||||
{% else %}
|
||||
@@ -309,8 +441,11 @@
|
||||
<h3>{% trans "Recovery-Codes" %}</h3>
|
||||
<p>{% trans "Diese Codes werden nur jetzt im Klartext angezeigt. Jeden Code können Sie genau einmal verwenden." %}</p>
|
||||
</div>
|
||||
<button class="btn btn-secondary" type="button" data-recovery-download>
|
||||
{% trans "Herunterladen" %}
|
||||
</button>
|
||||
</div>
|
||||
<div class="account-recovery-grid">
|
||||
<div class="account-recovery-grid" data-recovery-codes>
|
||||
{% for code in totp_recovery_codes %}
|
||||
<div class="account-recovery-code">{{ code }}</div>
|
||||
{% endfor %}
|
||||
@@ -328,27 +463,68 @@
|
||||
{% block extra_scripts %}
|
||||
<script>
|
||||
(function () {
|
||||
var toggle = document.querySelector('[data-account-edit-toggle="details"]');
|
||||
var cancel = document.querySelector('[data-account-edit-cancel="details"]');
|
||||
var view = document.querySelector('[data-account-edit-view="details"]');
|
||||
var form = document.querySelector('[data-account-edit-form="details"]');
|
||||
var editConfigs = ['details', 'notifications'].map(function (key) {
|
||||
return {
|
||||
key: key,
|
||||
toggle: document.querySelector('[data-account-edit-toggle="' + key + '"]'),
|
||||
cancel: document.querySelector('[data-account-edit-cancel="' + key + '"]'),
|
||||
view: document.querySelector('[data-account-edit-view="' + key + '"]'),
|
||||
form: document.querySelector('[data-account-edit-form="' + key + '"]')
|
||||
};
|
||||
});
|
||||
var secretToggle = document.querySelector('[data-secret-toggle]');
|
||||
var secretBody = document.querySelector('[data-secret-body]');
|
||||
var secretIcon = document.querySelector('[data-secret-toggle-icon]');
|
||||
if (!toggle || !cancel || !view || !form) return;
|
||||
|
||||
function setMode(editing) {
|
||||
view.classList.toggle('is-hidden', editing);
|
||||
form.classList.toggle('is-hidden', !editing);
|
||||
toggle.setAttribute('aria-expanded', editing ? 'true' : 'false');
|
||||
}
|
||||
|
||||
toggle.addEventListener('click', function () {
|
||||
setMode(true);
|
||||
var recoveryToggle = document.querySelector('[data-recovery-toggle]');
|
||||
var recoveryField = document.querySelector('[data-recovery-field]');
|
||||
var disableToggle = document.querySelector('[data-disable-toggle]');
|
||||
var disableForm = document.querySelector('[data-disable-form]');
|
||||
var disableCancel = document.querySelector('[data-disable-cancel]');
|
||||
var regenerateToggle = document.querySelector('[data-regenerate-toggle]');
|
||||
var regenerateForm = document.querySelector('[data-regenerate-form]');
|
||||
var regenerateActions = document.querySelector('[data-regenerate-actions]');
|
||||
var regenerateCancel = document.querySelector('[data-regenerate-cancel]');
|
||||
var recoveryDownload = document.querySelector('[data-recovery-download]');
|
||||
var recoveryCodes = document.querySelector('[data-recovery-codes]');
|
||||
var tabButtons = document.querySelectorAll('[data-account-tab]');
|
||||
var panels = document.querySelectorAll('[data-account-panel]');
|
||||
editConfigs.forEach(function (config) {
|
||||
if (!config.toggle || !config.cancel || !config.view || !config.form) return;
|
||||
function setMode(editing) {
|
||||
config.view.classList.toggle('is-hidden', editing);
|
||||
config.form.classList.toggle('is-hidden', !editing);
|
||||
config.toggle.setAttribute('aria-expanded', editing ? 'true' : 'false');
|
||||
}
|
||||
config.toggle.addEventListener('click', function () {
|
||||
setMode(true);
|
||||
});
|
||||
config.cancel.addEventListener('click', function () {
|
||||
setMode(false);
|
||||
});
|
||||
});
|
||||
|
||||
cancel.addEventListener('click', function () {
|
||||
setMode(false);
|
||||
function setTab(tabKey) {
|
||||
tabButtons.forEach(function (button) {
|
||||
var isActive = button.getAttribute('data-account-tab') === tabKey;
|
||||
button.classList.toggle('is-active', isActive);
|
||||
button.setAttribute('aria-expanded', isActive ? 'true' : 'false');
|
||||
});
|
||||
panels.forEach(function (panel) {
|
||||
var shouldShow = panel.getAttribute('data-account-panel') === tabKey;
|
||||
panel.hidden = !shouldShow;
|
||||
if (shouldShow) {
|
||||
panel.classList.remove('is-entering');
|
||||
window.requestAnimationFrame(function () {
|
||||
panel.classList.add('is-entering');
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
tabButtons.forEach(function (button) {
|
||||
button.addEventListener('click', function () {
|
||||
setTab(button.getAttribute('data-account-tab'));
|
||||
});
|
||||
});
|
||||
|
||||
if (secretToggle && secretBody) {
|
||||
@@ -361,6 +537,74 @@
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
if (recoveryToggle && recoveryField) {
|
||||
recoveryToggle.addEventListener('click', function () {
|
||||
var isOpen = recoveryToggle.getAttribute('aria-expanded') === 'true';
|
||||
recoveryToggle.setAttribute('aria-expanded', isOpen ? 'false' : 'true');
|
||||
recoveryField.classList.toggle('is-hidden', isOpen);
|
||||
});
|
||||
}
|
||||
|
||||
if (disableToggle && disableForm) {
|
||||
disableToggle.addEventListener('click', function () {
|
||||
var isOpen = disableToggle.getAttribute('aria-expanded') === 'true';
|
||||
disableToggle.setAttribute('aria-expanded', isOpen ? 'false' : 'true');
|
||||
disableForm.classList.toggle('is-hidden', isOpen);
|
||||
});
|
||||
}
|
||||
|
||||
if (disableCancel && disableToggle && disableForm) {
|
||||
disableCancel.addEventListener('click', function () {
|
||||
disableToggle.setAttribute('aria-expanded', 'false');
|
||||
disableForm.classList.add('is-hidden');
|
||||
});
|
||||
}
|
||||
|
||||
if (regenerateToggle && regenerateForm && regenerateActions) {
|
||||
regenerateToggle.addEventListener('click', function () {
|
||||
var isOpen = regenerateToggle.getAttribute('aria-expanded') === 'true';
|
||||
regenerateToggle.setAttribute('aria-expanded', isOpen ? 'false' : 'true');
|
||||
regenerateForm.classList.toggle('is-hidden', isOpen);
|
||||
regenerateActions.classList.toggle('is-hidden', isOpen);
|
||||
});
|
||||
}
|
||||
|
||||
if (regenerateCancel && regenerateToggle && regenerateForm && regenerateActions) {
|
||||
regenerateCancel.addEventListener('click', function () {
|
||||
regenerateToggle.setAttribute('aria-expanded', 'false');
|
||||
regenerateForm.classList.add('is-hidden');
|
||||
regenerateActions.classList.add('is-hidden');
|
||||
});
|
||||
}
|
||||
|
||||
if (recoveryDownload && recoveryCodes) {
|
||||
recoveryDownload.addEventListener('click', function () {
|
||||
var codes = Array.from(recoveryCodes.querySelectorAll('.account-recovery-code')).map(function (node) {
|
||||
return node.textContent.trim();
|
||||
}).filter(Boolean);
|
||||
if (!codes.length) return;
|
||||
var blob = new Blob([codes.join('\n') + '\n'], { type: 'text/plain;charset=utf-8' });
|
||||
var url = URL.createObjectURL(blob);
|
||||
var link = document.createElement('a');
|
||||
link.href = url;
|
||||
link.download = 'workdock-recovery-codes.txt';
|
||||
document.body.appendChild(link);
|
||||
link.click();
|
||||
document.body.removeChild(link);
|
||||
URL.revokeObjectURL(url);
|
||||
});
|
||||
}
|
||||
|
||||
if ({{ account_edit_open|yesno:"true,false" }}) {
|
||||
setTab('details');
|
||||
} else if ({{ notifications_edit_open|yesno:"true,false" }}) {
|
||||
setTab('notifications');
|
||||
} else if ({{ totp_edit_open|yesno:"true,false" }} || {{ totp_disable_form.errors|yesno:"true,false" }} || {{ totp_regenerate_form.errors|yesno:"true,false" }}) {
|
||||
setTab('security');
|
||||
} else {
|
||||
setTab('details');
|
||||
}
|
||||
}());
|
||||
</script>
|
||||
{% endblock %}
|
||||
|
||||
@@ -14,10 +14,21 @@
|
||||
{% block shell_body %}
|
||||
<section class="login-shell-body">
|
||||
<div class="login-card">
|
||||
{% if login_step == 'totp' %}
|
||||
<h1>{% trans "Zwei-Faktor-Prüfung" %}</h1>
|
||||
<p>{% trans "Geben Sie Ihren TOTP-Code ein, um die Anmeldung abzuschließen." %}</p>
|
||||
{% if login_totp_user %}
|
||||
<div class="login-step-caption">
|
||||
<strong>{{ login_totp_user.get_full_name|default:login_totp_user.username }}</strong>
|
||||
<span>{{ login_totp_user.email|default:login_totp_user.username }}</span>
|
||||
</div>
|
||||
{% endif %}
|
||||
{% else %}
|
||||
<h1>{% trans "Anmeldung" %}</h1>
|
||||
<p>{{ portal_login_subtitle }}</p>
|
||||
{% endif %}
|
||||
|
||||
<form method="post" action="/accounts/login/">
|
||||
<form method="post" action="{% if login_step == 'totp' %}/accounts/login/totp/{% else %}/accounts/login/{% endif %}">
|
||||
{% csrf_token %}
|
||||
{% if next %}
|
||||
<input type="hidden" name="next" value="{{ next }}" />
|
||||
@@ -25,23 +36,54 @@
|
||||
{% if form.errors %}
|
||||
<div class="app-alert app-alert-error" role="alert" aria-live="assertive">
|
||||
<div class="app-alert-body">
|
||||
{% if login_step == 'totp' %}
|
||||
<strong>{% trans "Code ungültig" %}</strong><br />
|
||||
<span>{% trans "Der eingegebene TOTP- oder Recovery-Code ist nicht korrekt. Bitte versuchen Sie es erneut." %}</span>
|
||||
{% else %}
|
||||
<strong>{% trans "Anmeldung fehlgeschlagen" %}</strong><br />
|
||||
<span>{% trans "Anmeldedaten oder TOTP-Code sind nicht korrekt. Bitte versuchen Sie es erneut." %}</span>
|
||||
<span>{% trans "Benutzername oder Passwort sind nicht korrekt. Bitte versuchen Sie es erneut." %}</span>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
{% if login_step == 'totp' %}
|
||||
<div class="field{% if form.otp_code.errors or form.errors %} has-error{% endif %}">
|
||||
{{ form.otp_code.label_tag }}{{ form.otp_code }}
|
||||
</div>
|
||||
<div class="login-recovery-toggle-row">
|
||||
<button
|
||||
class="btn btn-secondary btn-inline-toggle"
|
||||
type="button"
|
||||
data-toggle-target="login-recovery-box"
|
||||
aria-expanded="{% if show_recovery_code %}true{% else %}false{% endif %}"
|
||||
>
|
||||
{% trans "Recovery-Code verwenden" %}
|
||||
</button>
|
||||
</div>
|
||||
<div class="login-recovery-box{% if not show_recovery_code %} is-hidden{% endif %}" id="login-recovery-box">
|
||||
<div class="field{% if form.recovery_code.errors %} has-error{% endif %}">
|
||||
{{ form.recovery_code.label_tag }}{{ form.recovery_code }}
|
||||
<div class="mini">{% trans "Nutzen Sie stattdessen einen einmaligen Recovery-Code." %}</div>
|
||||
</div>
|
||||
</div>
|
||||
<button class="btn btn-primary" type="submit">{% trans "Code prüfen" %}</button>
|
||||
<a class="login-back-link" href="/accounts/login/">{% trans "Zurück zur Anmeldung" %}</a>
|
||||
{% else %}
|
||||
<div class="field{% if form.username.errors or form.errors %} has-error{% endif %}">{{ form.username.label_tag }}{{ form.username }}</div>
|
||||
<div class="field{% if form.password.errors or form.errors %} has-error{% endif %}">{{ form.password.label_tag }}{{ form.password }}</div>
|
||||
<div class="field{% if form.otp_code.errors %} has-error{% endif %}">
|
||||
{{ form.otp_code.label_tag }}{{ form.otp_code }}
|
||||
<div class="mini">{% trans "Nur erforderlich, wenn TOTP für Ihr Konto aktiviert ist." %}</div>
|
||||
</div>
|
||||
<div class="field{% if form.recovery_code.errors %} has-error{% endif %}">
|
||||
{{ form.recovery_code.label_tag }}{{ form.recovery_code }}
|
||||
<div class="mini">{% trans "Alternativ können Sie einen einmaligen Recovery-Code verwenden." %}</div>
|
||||
</div>
|
||||
<button class="btn btn-primary" type="submit">{% trans "Anmelden" %}</button>
|
||||
{% endif %}
|
||||
</form>
|
||||
</div>
|
||||
</section>
|
||||
<script>
|
||||
document.querySelectorAll('[data-toggle-target]').forEach((trigger) => {
|
||||
trigger.addEventListener('click', () => {
|
||||
const target = document.getElementById(trigger.getAttribute('data-toggle-target'));
|
||||
if (!target) return;
|
||||
const hidden = target.classList.toggle('is-hidden');
|
||||
trigger.setAttribute('aria-expanded', hidden ? 'false' : 'true');
|
||||
});
|
||||
});
|
||||
</script>
|
||||
{% endblock %}
|
||||
|
||||
@@ -20,6 +20,58 @@
|
||||
<a class="btn btn-secondary" href="/">{% trans "Zur Startseite" %}</a>
|
||||
{% endif %}
|
||||
{% if request.user.is_authenticated %}
|
||||
<details class="app-notification-menu">
|
||||
<summary class="app-notification-trigger" aria-label="{% trans 'Benachrichtigungen' %}">
|
||||
<span class="app-notification-bell" aria-hidden="true">
|
||||
<svg viewBox="0 0 24 24" fill="none" focusable="false" aria-hidden="true">
|
||||
<path d="M12 4.25a4.25 4.25 0 0 0-4.25 4.25v2.04c0 .83-.24 1.65-.69 2.35l-1.2 1.88a1.5 1.5 0 0 0 1.27 2.31h9.74a1.5 1.5 0 0 0 1.27-2.31l-1.2-1.88a4.34 4.34 0 0 1-.69-2.35V8.5A4.25 4.25 0 0 0 12 4.25Z" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<path d="M9.75 18.25a2.25 2.25 0 0 0 4.5 0" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
</svg>
|
||||
</span>
|
||||
{% if header_unread_notification_count %}
|
||||
<span class="app-notification-count">{{ header_unread_notification_count }}</span>
|
||||
{% endif %}
|
||||
</summary>
|
||||
<div class="app-notification-panel">
|
||||
<div class="app-notification-panel-head">
|
||||
<strong>{% trans "Benachrichtigungen" %}</strong>
|
||||
{% if header_unread_notification_count %}
|
||||
<form method="post" action="{% url 'mark_all_notifications_read' %}">
|
||||
{% csrf_token %}
|
||||
<input type="hidden" name="next" value="{{ request.get_full_path }}" />
|
||||
<button type="submit">{% trans "Alle als gelesen" %}</button>
|
||||
</form>
|
||||
{% endif %}
|
||||
</div>
|
||||
{% if header_notifications %}
|
||||
<div class="app-notification-list">
|
||||
{% for notification in header_notifications %}
|
||||
<article class="app-notification-item app-notification-{{ notification.level }}{% if notification.is_unread %} is-unread{% endif %}">
|
||||
<div class="app-notification-copy">
|
||||
<strong>{{ notification.title }}</strong>
|
||||
{% if notification.body %}<p>{{ notification.body|truncatechars:140 }}</p>{% endif %}
|
||||
<span>{{ notification.created_at|date:"d.m.Y H:i" }}</span>
|
||||
</div>
|
||||
<div class="app-notification-actions">
|
||||
{% if notification.link_url %}
|
||||
<a href="{{ notification.link_url }}">{% trans "Öffnen" %}</a>
|
||||
{% endif %}
|
||||
{% if notification.is_unread %}
|
||||
<form method="post" action="{% url 'mark_notification_read' notification.id %}">
|
||||
{% csrf_token %}
|
||||
<input type="hidden" name="next" value="{{ request.get_full_path }}" />
|
||||
<button type="submit">{% trans "Gelesen" %}</button>
|
||||
</form>
|
||||
{% endif %}
|
||||
</div>
|
||||
</article>
|
||||
{% endfor %}
|
||||
</div>
|
||||
{% else %}
|
||||
<div class="app-notification-empty">{% trans "Keine Benachrichtigungen vorhanden." %}</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
</details>
|
||||
<details class="app-user-menu">
|
||||
<summary class="app-user-trigger">
|
||||
<span class="app-user-avatar" aria-hidden="true">
|
||||
|
||||
Reference in New Issue
Block a user