feat: add default-off switch for email domain registration quota

PR #5423 relaxed the email suffix whitelist: once a whitelist is
configured, non-whitelisted registrable domains are each allowed to
register one account. That behavior activated unconditionally.

Add registration_email_domain_quota_enabled (default false) to gate it:

- Off (default): restore pre-#5423 strict whitelist semantics — with a
  non-empty whitelist, non-whitelisted domains are rejected with
  EMAIL_SUFFIX_NOT_ALLOWED; the register/verify views restore the
  client-side whitelist pre-check and allowed-domain hint.
- On: keep #5423 behavior — one account per non-whitelisted registrable
  domain (EMAIL_DOMAIN_REGISTRATION_LIMIT).
- Empty whitelist keeps allowing all domains in both states.

Gating lives in validateRegistrationEmailQuota and (as a race-safety
backstop) createUserWithRegistrationEmailGuard; the repository-level
domain lock + in-tx recheck is unchanged. The admin update field is
*bool (omitted = keep current) so stale full-payload saves cannot
silently flip the switch. Email binding and OAuth auto-signup keep
their strict policy, and pending-OAuth bind-login for existing
accounts is unaffected because the handler resolves existing emails
before the quota check.

Frontend adds the toggle to admin settings (zh/en copy; whitelist hint
restored to strict wording, quota wording moved to the new toggle) and
exposes the flag via public settings + SSR injection payload.

Tests: #5423 quota tests now enable the switch explicitly; new
default-off regression tests cover register/send-code/async/pending
OAuth/OIDC create-account plus both register views; API contract JSON
and the injection drift guard are updated.
This commit is contained in:
shaw
2026-08-09 15:53:40 +08:00
parent f2da30bcd9
commit 563a72ca73
27 changed files with 836 additions and 444 deletions
+20
View File
@@ -1486,6 +1486,23 @@
</p>
</div>
<!-- Email Domain Quota -->
<div
class="flex items-center justify-between border-t border-gray-100 pt-4 dark:border-dark-700"
>
<div>
<label class="font-medium text-gray-900 dark:text-white">{{
t("admin.settings.registration.emailDomainQuota")
}}</label>
<p class="text-sm text-gray-500 dark:text-gray-400">
{{ t("admin.settings.registration.emailDomainQuotaHint") }}
</p>
</div>
<Toggle
v-model="form.registration_email_domain_quota_enabled"
/>
</div>
<!-- Promo Code -->
<div
class="flex items-center justify-between border-t border-gray-100 pt-4 dark:border-dark-700"
@@ -9401,6 +9418,7 @@ const form = reactive<SettingsForm>({
registration_enabled: true,
email_verify_enabled: false,
registration_email_suffix_whitelist: [],
registration_email_domain_quota_enabled: false,
promo_code_enabled: true,
invitation_code_enabled: false,
password_reset_enabled: false,
@@ -11023,6 +11041,8 @@ async function saveSettings() {
registrationEmailSuffixWhitelistTags.value.map((suffix) =>
suffix.startsWith("*.") ? suffix : `@${suffix}`,
),
registration_email_domain_quota_enabled:
form.registration_email_domain_quota_enabled,
promo_code_enabled: form.promo_code_enabled,
invitation_code_enabled: form.invitation_code_enabled,
password_reset_enabled: form.password_reset_enabled,
@@ -605,12 +605,18 @@ describe("admin SettingsView email domain quota copy", () => {
expect(enCommon.auth.emailDomainRegistrationLimit).toContain("mainstream email");
expect(enCommon.auth.emailDomainRegistrationLimit).toContain("contact support");
const zhHint = zhSettings.settings.registration.emailSuffixWhitelistHint;
const enHint = enSettings.settings.registration.emailSuffixWhitelistHint;
expect(zhHint).toContain("其他可注册主域名各限注册一个账户");
expect(zhHint).toContain("清空白名单");
expect(enHint).toContain("one account");
expect(enHint).toContain("empty");
// 白名单 hint 描述严格默认语义;额度语义移入独立开关的 hint。
const zhWhitelistHint = zhSettings.settings.registration.emailSuffixWhitelistHint;
const enWhitelistHint = enSettings.settings.registration.emailSuffixWhitelistHint;
expect(zhWhitelistHint).toContain("留空则不限制");
expect(enWhitelistHint).toContain("leave empty for no restriction");
const zhQuotaHint = zhSettings.settings.registration.emailDomainQuotaHint;
const enQuotaHint = enSettings.settings.registration.emailDomainQuotaHint;
expect(zhQuotaHint).toContain("其他可注册主域名各限注册一个账户");
expect(zhQuotaHint).toContain("关闭时非白名单域名直接拒绝");
expect(enQuotaHint).toContain("one account");
expect(enQuotaHint).toContain("When disabled");
});
});