feat: add password change

Password page with the AD policy listed first, ordered service (throttle limit below the lockout threshold, verification, local hints, directory change, ended sessions, audit and confirmation mail in one transaction), plain-language errors, German text and tests.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01N3t4WL7bd9bdHHNjEsxzqC
This commit is contained in:
2026-10-18 10:08:17 +02:00
co-authored by Claude Sonnet 5.5
parent a1a766adef
commit 49c7fd4a0b
22 changed files with 1346 additions and 17 deletions
+148
View File
@@ -14,6 +14,8 @@
--text-secondary: #616161;
--line: #e0e0e0;
--control-line: #8a8a8a;
--danger: #b10e1c;
--success: #107c10;
--font: "Segoe UI Variable", "Segoe UI", system-ui, sans-serif;
--radius-sm: 4px;
--radius-lg: 8px;
@@ -37,6 +39,8 @@
--text-secondary: #c8c8c8;
--line: #3d3d3d;
--control-line: #9a9a9a;
--danger: #ff99a4;
--success: #6ccb5f;
color-scheme: dark;
}
@@ -55,6 +59,8 @@
--text-secondary: #c8c8c8;
--line: #3d3d3d;
--control-line: #9a9a9a;
--danger: #ff99a4;
--success: #6ccb5f;
color-scheme: dark;
}
}
@@ -430,3 +436,145 @@ main button {
display: block;
}
}
/* Forms, notices and buttons */
.card {
max-width: 520px;
margin: 0 0 14px;
padding: 16px 18px;
background: var(--surface);
border: 1px solid var(--line);
border-radius: var(--radius-lg);
}
.field {
margin-bottom: 14px;
}
.field label {
display: block;
margin-bottom: 4px;
font-weight: 600;
}
.field input {
width: 100%;
min-height: 36px;
padding: 6px 10px;
border: 1px solid var(--control-line);
border-bottom-width: 2px;
border-radius: var(--radius-sm);
background: var(--bg);
color: var(--text);
font: inherit;
}
.field.has-error input {
border-color: var(--danger);
}
.hint {
margin: 4px 0 0;
font-size: 12px;
color: var(--text-secondary);
}
.error {
margin: 4px 0 0;
font-size: 13px;
color: var(--danger);
}
.error::before {
content: "\26A0\FE0E ";
}
.rules ul {
margin: 4px 0 14px;
padding-left: 20px;
font-size: 13px;
color: var(--text-secondary);
}
.error-summary,
.notice {
max-width: 520px;
margin: 0 0 14px;
padding: 12px 16px;
border: 1px solid var(--danger);
border-left-width: 5px;
border-radius: 6px;
background: var(--surface);
}
.notice.success {
border-color: var(--success);
}
.error-summary p,
.notice p {
margin: 0;
}
.error-summary ul {
margin: 6px 0 0;
padding-left: 20px;
}
.actions {
margin: 0;
}
a.button,
main button.primary {
display: inline-flex;
align-items: center;
justify-content: center;
gap: 8px;
min-height: var(--touch);
padding: 0 16px;
border: 1px solid var(--control-line);
border-radius: var(--radius-sm);
color: var(--text);
font: inherit;
text-decoration: none;
}
main button.primary {
border-color: var(--brand);
background: var(--brand);
color: var(--on-brand);
font-weight: 600;
}
main button.primary:disabled {
opacity: 0.7;
cursor: progress;
}
.spinner {
display: none;
width: 14px;
height: 14px;
border: 2px solid currentcolor;
border-top-color: transparent;
border-radius: 50%;
animation: spin 0.8s linear infinite;
}
.busy .spinner {
display: inline-block;
}
@keyframes spin {
to {
transform: rotate(360deg);
}
}
@media (prefers-reduced-motion: reduce) {
.spinner {
animation-duration: 3s;
}
}
+19
View File
@@ -50,6 +50,25 @@
}
});
// A form that is being sent shows a spinner and cannot be sent twice.
document.addEventListener("submit", function (event) {
var form = event.target;
if (!form.hasAttribute("data-busy-on-submit")) {
return;
}
form.classList.add("busy");
setTimeout(function () {
form.querySelectorAll("button[type=submit]").forEach(function (button) {
button.disabled = true;
});
}, 0);
});
var focusTarget = document.querySelector("[data-focus-on-load]");
if (focusTarget) {
focusTarget.focus();
}
if (menuButton) {
menuButton.addEventListener("click", function () {
var open = root.classList.toggle("menu-open");