feat(auth): unique, changeable, verifiable email addresses (engagement Phase 1b)
Makes `users.email` unique, de-duplicates the addresses an upgrade will find, and builds the self-service change-and-verify flow that did not exist. The uniqueness index is on a generated `email_norm AS (LOWER(email)) STORED` column under `utf8mb4_bin`, NOT on `email` under a `_ci` collation as the plan specified. Every case-insensitive collation this server offers is also accent-insensitive: `josé@x.com` and `jose@x.com` compare equal, and those are two different mailboxes. The plan's index would have refused the second address forever and the de-duplication would have nulled a legitimate account's. A requested address is STAGED in `email_pending` and only a tokened link installs it, so a typo cannot silently redirect account-recovery mail. `isDuplicateUsername()` now distinguishes the two indexes. All five call sites branch on it; each answers differently on purpose, because a public form, an IdP callback, a half-completed invite and an admin screen do not owe the same person the same amount of truth. SSO reads the IdP's actual `email_verified`/`verified` claim instead of inferring verification from an address merely being present. Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
@@ -4,6 +4,7 @@ import ProviderIcon from '../../../components/ProviderIcon.jsx'
|
||||
import RecoveryCodesDisplay from '../../../components/security/RecoveryCodesDisplay.jsx'
|
||||
import TrustedDevicesPanel from '../../../components/security/TrustedDevicesPanel.jsx'
|
||||
import RecoveryCodesPanel from '../../../components/security/RecoveryCodesPanel.jsx'
|
||||
import EmailAddressPanel from '../../../components/security/EmailAddressPanel.jsx'
|
||||
import { api } from '../../../api/client.js'
|
||||
|
||||
// Link/unlink external SSO identities to this account. Linking redirects through
|
||||
@@ -322,6 +323,10 @@ export default function AccountAdmin() {
|
||||
</>
|
||||
)}
|
||||
|
||||
{/* The self-service address, from the same component the player portal
|
||||
renders — /auth/me/account is one surface for every role. */}
|
||||
{account && <EmailAddressPanel account={account} reload={load} />}
|
||||
|
||||
<LinkedAccounts />
|
||||
</section>
|
||||
)
|
||||
|
||||
Reference in New Issue
Block a user