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>
38 lines
1.2 KiB
JavaScript
38 lines
1.2 KiB
JavaScript
// Built-in Discord provider (OAuth2). Endpoints hardcoded — admins configure only
|
|
// Enabled + Client ID + Client Secret. `identify` yields the stable user id;
|
|
// `email` yields the address. Discord's id is the stable per-user subject.
|
|
|
|
const OAuth2Provider = require('./oauth2.provider')
|
|
|
|
class DiscordProvider extends OAuth2Provider {
|
|
constructor(config = {}) {
|
|
super({ kind: 'discord', name: 'Discord', ...config, id: config.id || 'discord' })
|
|
}
|
|
|
|
authEndpoint() {
|
|
return 'https://discord.com/oauth2/authorize'
|
|
}
|
|
tokenEndpoint() {
|
|
return 'https://discord.com/api/oauth2/token'
|
|
}
|
|
userinfoEndpoint() {
|
|
return 'https://discord.com/api/users/@me'
|
|
}
|
|
scopeString() {
|
|
return 'identify email'
|
|
}
|
|
normalizeProfile(p = {}) {
|
|
// global_name is the new display name; fall back to the legacy username.
|
|
return {
|
|
subject: p.id,
|
|
email: p.email || null,
|
|
// Discord spells the claim `verified` rather than `email_verified`, and it
|
|
// means exactly this: the user confirmed the address with Discord.
|
|
emailVerified: p.verified === true,
|
|
name: p.global_name || p.username || null,
|
|
}
|
|
}
|
|
}
|
|
|
|
module.exports = DiscordProvider
|