feat(admin): an account panel, and a set-password link that needs no email

Design option C, chosen by the user 2026-09-05. Nothing in the admin UI
ever showed an account's GATE STATE: an admin could see a "Pending
approval" chip but not whether the address was confirmed, whether SEC-10
had cleared the password, or which Google account was attached — so
every support conversation started by guessing. Clicking a row now opens
a drawer that says which gate the person is actually stuck behind, with
the recovery actions under that readout rather than in the row.

The drawer is `Modal placement="drawer"`, not a second dialog: the layer
stack, ESC, the focus trap, Back-to-close and the scroll lock are the
hard parts, and duplicating them is how two shells drift apart. It
slides in, and honours prefers-reduced-motion like the rest.

The link itself (SEC-15, SEC-16, and the everyday "no SMTP and someone
forgot theirs") reuses the existing token machinery and the SAME
`#reset=` entry the mailed link uses, so the signed-out card consumes it
with no new flow. It is a distinct token KIND for one reason: a mailed
reset arrives in the mailbox, so completing it proves ownership and
confirms the address; an admin-issued link proves nothing about the
mailbox — the admin hands it over by whatever means they choose — so it
must not satisfy the email gate. Claiming otherwise would be the same
false attestation SEC-15 is about.

Issuing does not revoke the current password: a misclick must not lock
anyone out. USING it rewrites the hash and bumps the session epoch,
which is what evicts a squatter — SEC-16's actual remedy. Spending
either kind burns the siblings of both, so a completed reset cannot
leave a live admin link behind. The non-consuming probe learned the new
kind too, or every admin link would have opened onto "this link is
spent". The audit log records that a link was issued, by whom, for whom
— never the link, which is a live credential-setting secret and the last
thing that belongs in a long-lived log a human reads.

Not gated on account state on purpose: it is useful for a pre-empted
address, a revoked password, a Google-only account that wants one, and a
plain forgotten password. Narrowing it would only make it refuse exactly
when the admin needs it. The shared demo account is the one exception.

Verified on localdev, both languages, end to end: issue from the panel
-> the signed-in tab shows the honest "reset links open in a signed-out
window" banner with the token still in the address bar -> signing out
draws the "Set a new password" form, which is only possible because the
probe recognises the new kind. Gate: pytest 925 -> 942, vitest 676.
This commit is contained in:
2026-09-05 01:46:58 +02:00
parent bc9e34d0ae
commit f98b060e2b
14 changed files with 778 additions and 10 deletions
@@ -5,6 +5,7 @@
"user_delete": "User deleted",
"user_activate": "Account activated",
"user_verify_resend": "Confirmation link re-sent",
"user_setpw_link": "Set-password link issued",
"invite_approve": "Access request approved",
"invite_deny": "Access request denied",
"invite_add": "Email whitelisted",
+33
View File
@@ -59,5 +59,38 @@
"title": "Delete account?",
"confirm": "Permanently delete {{email}} and all their data — subscriptions, tags, watch history, playlists and settings? This can't be undone.",
"done": "{{email}} deleted"
},
"account": {
"access": "Access",
"activate": "Let this account in",
"alreadyConfirmed": "already confirmed",
"approval": "Admin approval",
"approvalDone": "let in",
"approvalNotRequested": "no approved invite yet",
"approvalWaitingOnYou": "waiting on you",
"copied": "Copied",
"copy": "Copy",
"copyFailed": "Couldn't copy — select the link and copy it by hand.",
"email": "Email",
"emailConfirmed": "confirmed",
"emailUnconfirmed": "not confirmed",
"google": "Google",
"googleAs": "connected as {{email}}",
"googleConnected": "connected",
"googleNone": "not connected",
"invite": "Invite",
"inviteMissing": "this address is not on the allow-list",
"linkNote": "Valid 24 hours, works once. Opening it lets this person set a password and signs out anyone already using the account. It does not confirm their address.",
"mailOff": "no mail configured",
"open": "Open the account panel",
"password": "Password",
"passwordNone": "none — signs in with Google",
"passwordRevoked": "cleared for security",
"passwordSet": "set",
"recovery": "Recovery",
"resend": "Send a confirmation link",
"resendSent": "Confirmation link sent.",
"setPassword": "Create a set-password link",
"setPasswordAgain": "Create another link"
}
}
@@ -5,6 +5,7 @@
"user_delete": "Felhasználó törölve",
"user_activate": "Fiók aktiválva",
"user_verify_resend": "Megerősítő link újraküldve",
"user_setpw_link": "Jelszóbeállító link kiadva",
"invite_approve": "Hozzáférési kérelem jóváhagyva",
"invite_deny": "Hozzáférési kérelem elutasítva",
"invite_add": "Email fehérlistázva",
+33
View File
@@ -59,5 +59,38 @@
"title": "Törlöd a fiókot?",
"confirm": "Véglegesen törlöd: {{email}} és minden adatát — feliratkozások, címkék, megtekintési előzmény, lejátszási listák és beállítások? Ez nem visszavonható.",
"done": "{{email}} törölve"
},
"account": {
"access": "Hozzáférés",
"activate": "Fiók beengedése",
"alreadyConfirmed": "már megerősítve",
"approval": "Admin jóváhagyás",
"approvalDone": "beengedve",
"approvalNotRequested": "még nincs jóváhagyott meghívó",
"approvalWaitingOnYou": "rád vár",
"copied": "Másolva",
"copy": "Másolás",
"copyFailed": "Nem sikerült másolni — jelöld ki a linket, és másold kézzel.",
"email": "E-mail",
"emailConfirmed": "megerősítve",
"emailUnconfirmed": "nincs megerősítve",
"google": "Google",
"googleAs": "összekötve mint {{email}}",
"googleConnected": "összekötve",
"googleNone": "nincs összekötve",
"invite": "Meghívó",
"inviteMissing": "ez a cím nincs az engedélyezési listán",
"linkNote": "24 óráig érvényes, egyszer használható. Aki megnyitja, beállíthat egy jelszót, és ezzel kilép mindenki, aki eddig használta a fiókot. A címet nem erősíti meg.",
"mailOff": "nincs beállítva levelezés",
"open": "Fiókpanel megnyitása",
"password": "Jelszó",
"passwordNone": "nincs — Google-lel lép be",
"passwordRevoked": "biztonsági okból törölve",
"passwordSet": "be van állítva",
"recovery": "Helyreállítás",
"resend": "Megerősítő link küldése",
"resendSent": "Megerősítő link elküldve.",
"setPassword": "Jelszóbeállító link készítése",
"setPasswordAgain": "Új link készítése"
}
}