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:
@@ -0,0 +1,48 @@
|
||||
"""outgoing system mail (password reset) configured in the admin panel
|
||||
|
||||
The server sends its own mail — today only password-reset links — through an
|
||||
SMTP account the administrator sets up in the admin panel. The password is
|
||||
envelope-encrypted with MASTER_KEY like every mail-account secret. ``public_url``
|
||||
is where users reach the app; reset links are built from it, never from the
|
||||
request's Host header, which an attacker controls.
|
||||
|
||||
Revision ID: 0020
|
||||
Revises: 0019
|
||||
Create Date: 2026-09-30
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from collections.abc import Sequence
|
||||
|
||||
from alembic import op
|
||||
|
||||
revision: str = "0020"
|
||||
down_revision: str | None = "0019"
|
||||
branch_labels: str | Sequence[str] | None = None
|
||||
depends_on: str | Sequence[str] | None = None
|
||||
|
||||
_COLUMNS = (
|
||||
("smtp_host", "text NOT NULL DEFAULT ''"),
|
||||
("smtp_port", "integer NOT NULL DEFAULT 587"),
|
||||
("smtp_security", "text NOT NULL DEFAULT 'starttls'"),
|
||||
("smtp_username", "text NOT NULL DEFAULT ''"),
|
||||
("smtp_secret_ct", "bytea"),
|
||||
("smtp_from", "text NOT NULL DEFAULT ''"),
|
||||
("public_url", "text NOT NULL DEFAULT ''"),
|
||||
)
|
||||
|
||||
|
||||
def upgrade() -> None:
|
||||
for name, ddl in _COLUMNS:
|
||||
op.execute(f"ALTER TABLE app_settings ADD COLUMN {name} {ddl}")
|
||||
op.execute(
|
||||
"ALTER TABLE app_settings ADD CONSTRAINT ck_app_settings_smtp_security "
|
||||
"CHECK (smtp_security IN ('starttls', 'ssl', 'none'))"
|
||||
)
|
||||
|
||||
|
||||
def downgrade() -> None:
|
||||
op.execute("ALTER TABLE app_settings DROP CONSTRAINT IF EXISTS ck_app_settings_smtp_security")
|
||||
for name, _ in reversed(_COLUMNS):
|
||||
op.execute(f"ALTER TABLE app_settings DROP COLUMN IF EXISTS {name}")
|
||||
Reference in New Issue
Block a user