Admin user management, password reset by email, and the Ultimo Email name
Branding: the page titles and app name are now "Ultimo Email". Admin panel (now two columns with a divider): - Users: search by login or by a mail account's address or name (results only, nothing listed until something is typed). A user opens a window to change their quota, block/unblock them, or delete them; block and delete ask for confirmation, and an admin cannot do either to their own account. - Blocking (users.blocked_at, migration 0019) refuses sign-in with a clear message and revokes every session; a new user.signed_out event sends the user's open tabs to the sign-in screen at once. Any "not signed in" answer now does the same, not only /auth/me. Deleting removes mail accounts (sync stops), plugin rows (cleared inside the user's RLS context, since plugin tables have no FK to users), blobs, the search index, sessions and the user. - Outgoing system mail (migration 0020): SMTP server, port, security, login, password (vault-encrypted, never sent back), From address and the public address reset links point to, plus a "Send test" that reports the server's error. Password reset: - "Forgot password?" on the sign-in page asks for the user name and always answers "If this account exists you will receive an email to your recovery email address"; the mail goes out after the response, so neither the reply nor its timing reveals an account, and it is capped at 3 per hour. - The emailed link (token in the URL fragment, single use, 30 minutes) opens a page to set a new password. With 2-step verification on, the code is checked before anything changes; five wrong codes burn the link. - Settings > Profile: the recovery email can be added, changed or removed, confirmed with the current password, which must be typed — autofill and paste are refused (TypedPasswordInput). Also adds frontend/icons/ultimo-logo-bimi.svg, an SVG Tiny PS version of the logo for a BIMI record. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -53,6 +53,10 @@ class TotpInvalid(Exception):
|
||||
"""Password verified but the supplied TOTP code was wrong."""
|
||||
|
||||
|
||||
class AccountBlocked(Exception):
|
||||
"""Password verified but an administrator has blocked the account."""
|
||||
|
||||
|
||||
def resolve_handle(handle_input: str) -> str:
|
||||
"""Normalize a login identifier to the full ``localpart@APP_DOMAIN`` form."""
|
||||
text = handle_input.strip().lower()
|
||||
@@ -164,6 +168,9 @@ async def authenticate(
|
||||
ok = verify_password(stored_hash, password)
|
||||
if user is None or not ok:
|
||||
return None
|
||||
# Only after the password: a stranger must not learn that a handle is blocked.
|
||||
if user.blocked_at is not None:
|
||||
raise AccountBlocked
|
||||
|
||||
if user.totp_secret is not None:
|
||||
from app.identity.totp import decrypt_totp_secret
|
||||
|
||||
Reference in New Issue
Block a user