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,66 @@
|
||||
import { useId, useState } from "react";
|
||||
|
||||
// Keystrokes and IME composition; everything else that changes the field — a
|
||||
// browser or password-manager autofill (an input event without an inputType, or
|
||||
// "insertReplacementText"), a paste, a drop — is not the user typing.
|
||||
const TYPED = /^(insertText|insertCompositionText|insertLineBreak|delete)/;
|
||||
|
||||
/**
|
||||
* A password field that only accepts what is typed into it: for confirmations
|
||||
* where the point is that the person present knows the password, which an
|
||||
* autofilled saved password does not prove.
|
||||
*
|
||||
* `autoComplete="new-password"` and the password managers' opt-out attributes
|
||||
* keep most autofill away to begin with; whatever still gets in is thrown out
|
||||
* here, and the field says to type it.
|
||||
*/
|
||||
export function TypedPasswordInput({
|
||||
value,
|
||||
onChange,
|
||||
className,
|
||||
hint,
|
||||
}: {
|
||||
value: string;
|
||||
onChange: (value: string) => void;
|
||||
className?: string;
|
||||
/** Shown when a fill or paste was refused. */
|
||||
hint: string;
|
||||
}) {
|
||||
const id = useId();
|
||||
const [refused, setRefused] = useState(false);
|
||||
return (
|
||||
<>
|
||||
<input
|
||||
className={className}
|
||||
type="password"
|
||||
// A fresh name per field, so nothing matches it to a saved login form.
|
||||
name={`typed-${id}`}
|
||||
autoComplete="new-password"
|
||||
data-1p-ignore
|
||||
data-lpignore="true"
|
||||
data-bwignore
|
||||
data-form-type="other"
|
||||
value={value}
|
||||
onPaste={(e) => {
|
||||
e.preventDefault();
|
||||
setRefused(true);
|
||||
}}
|
||||
onDrop={(e) => {
|
||||
e.preventDefault();
|
||||
setRefused(true);
|
||||
}}
|
||||
onChange={(e) => {
|
||||
const inputType = (e.nativeEvent as InputEvent).inputType ?? "";
|
||||
if (!TYPED.test(inputType)) {
|
||||
onChange("");
|
||||
setRefused(true);
|
||||
return;
|
||||
}
|
||||
setRefused(false);
|
||||
onChange(e.target.value);
|
||||
}}
|
||||
/>
|
||||
{refused && <span className="block text-xs text-amber-600">{hint}</span>}
|
||||
</>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user