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:
@@ -4,6 +4,8 @@
|
||||
import type { QueryClient } from "@tanstack/react-query";
|
||||
import { z } from "zod";
|
||||
|
||||
import { signedOut } from "./auth";
|
||||
|
||||
const frameSchema = z.object({
|
||||
event: z.string(),
|
||||
payload: z.record(z.unknown()),
|
||||
@@ -20,6 +22,7 @@ const ALLOWED_EVENTS = new Set([
|
||||
"account.state_changed",
|
||||
"draft.saved",
|
||||
"outbox.state_changed",
|
||||
"user.signed_out",
|
||||
]);
|
||||
|
||||
// Lightweight in-process fan-out of WS frames to plugin subscribers (SPEC §11.3).
|
||||
@@ -82,6 +85,12 @@ export function connectWs(queryClient: QueryClient): () => void {
|
||||
}
|
||||
}
|
||||
if (!isCore) return;
|
||||
if (name === "user.signed_out") {
|
||||
// An administrator blocked or deleted this account: leave now, rather
|
||||
// than keep showing mail that was already loaded.
|
||||
signedOut(queryClient);
|
||||
return;
|
||||
}
|
||||
// Coarse invalidation: refetch affected lists (SPEC §9.2 contract).
|
||||
if (name === "sync.progress" || name.startsWith("message.") || name === "thread.updated") {
|
||||
queryClient.invalidateQueries({ queryKey: ["threads"] });
|
||||
|
||||
Reference in New Issue
Block a user