feat(registration): add email domain whitelist policy
This commit is contained in:
@@ -324,6 +324,56 @@
|
||||
<Toggle v-model="form.email_verify_enabled" />
|
||||
</div>
|
||||
|
||||
<!-- Email Suffix Whitelist -->
|
||||
<div class="border-t border-gray-100 pt-4 dark:border-dark-700">
|
||||
<label class="font-medium text-gray-900 dark:text-white">{{
|
||||
t('admin.settings.registration.emailSuffixWhitelist')
|
||||
}}</label>
|
||||
<p class="mt-1 text-sm text-gray-500 dark:text-gray-400">
|
||||
{{ t('admin.settings.registration.emailSuffixWhitelistHint') }}
|
||||
</p>
|
||||
<div
|
||||
class="mt-3 rounded-lg border border-gray-300 bg-white p-2 dark:border-dark-500 dark:bg-dark-700"
|
||||
>
|
||||
<div class="flex flex-wrap items-center gap-2">
|
||||
<span
|
||||
v-for="suffix in registrationEmailSuffixWhitelistTags"
|
||||
:key="suffix"
|
||||
class="inline-flex items-center gap-1 rounded bg-gray-100 px-2 py-1 text-xs font-mono text-gray-700 dark:bg-dark-600 dark:text-gray-200"
|
||||
>
|
||||
<span class="text-gray-400 dark:text-gray-500">@</span>
|
||||
<span>{{ suffix }}</span>
|
||||
<button
|
||||
type="button"
|
||||
class="rounded-full text-gray-500 hover:bg-gray-200 hover:text-gray-700 dark:text-gray-300 dark:hover:bg-dark-500 dark:hover:text-white"
|
||||
@click="removeRegistrationEmailSuffixWhitelistTag(suffix)"
|
||||
>
|
||||
<Icon name="x" size="xs" class="h-3.5 w-3.5" :stroke-width="2" />
|
||||
</button>
|
||||
</span>
|
||||
|
||||
<div
|
||||
class="flex min-w-[220px] flex-1 items-center gap-1 rounded border border-transparent px-2 py-1 focus-within:border-primary-300 dark:focus-within:border-primary-700"
|
||||
>
|
||||
<span class="font-mono text-sm text-gray-400 dark:text-gray-500">@</span>
|
||||
<input
|
||||
v-model="registrationEmailSuffixWhitelistDraft"
|
||||
type="text"
|
||||
class="w-full bg-transparent text-sm font-mono text-gray-900 outline-none placeholder:text-gray-400 dark:text-white dark:placeholder:text-gray-500"
|
||||
:placeholder="t('admin.settings.registration.emailSuffixWhitelistPlaceholder')"
|
||||
@input="handleRegistrationEmailSuffixWhitelistDraftInput"
|
||||
@keydown="handleRegistrationEmailSuffixWhitelistDraftKeydown"
|
||||
@blur="commitRegistrationEmailSuffixWhitelistDraft"
|
||||
@paste="handleRegistrationEmailSuffixWhitelistPaste"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<p class="mt-2 text-xs text-gray-500 dark:text-gray-400">
|
||||
{{ t('admin.settings.registration.emailSuffixWhitelistInputHint') }}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<!-- Promo Code -->
|
||||
<div
|
||||
class="flex items-center justify-between border-t border-gray-100 pt-4 dark:border-dark-700"
|
||||
@@ -1364,6 +1414,12 @@ import ImageUpload from '@/components/common/ImageUpload.vue'
|
||||
import { useClipboard } from '@/composables/useClipboard'
|
||||
import { useAppStore } from '@/stores'
|
||||
import { useAdminSettingsStore } from '@/stores/adminSettings'
|
||||
import {
|
||||
isRegistrationEmailSuffixDomainValid,
|
||||
normalizeRegistrationEmailSuffixDomain,
|
||||
normalizeRegistrationEmailSuffixDomains,
|
||||
parseRegistrationEmailSuffixWhitelistInput
|
||||
} from '@/utils/registrationEmailPolicy'
|
||||
|
||||
const { t } = useI18n()
|
||||
const appStore = useAppStore()
|
||||
@@ -1375,6 +1431,9 @@ const saving = ref(false)
|
||||
const testingSmtp = ref(false)
|
||||
const sendingTestEmail = ref(false)
|
||||
const testEmailAddress = ref('')
|
||||
const logoError = ref('')
|
||||
const registrationEmailSuffixWhitelistTags = ref<string[]>([])
|
||||
const registrationEmailSuffixWhitelistDraft = ref('')
|
||||
|
||||
// Admin API Key 状态
|
||||
const adminApiKeyLoading = ref(true)
|
||||
@@ -1414,6 +1473,7 @@ type SettingsForm = SystemSettings & {
|
||||
const form = reactive<SettingsForm>({
|
||||
registration_enabled: true,
|
||||
email_verify_enabled: false,
|
||||
registration_email_suffix_whitelist: [],
|
||||
promo_code_enabled: true,
|
||||
invitation_code_enabled: false,
|
||||
password_reset_enabled: false,
|
||||
@@ -1484,6 +1544,74 @@ const defaultSubscriptionGroupOptions = computed<DefaultSubscriptionGroupOption[
|
||||
}))
|
||||
)
|
||||
|
||||
const registrationEmailSuffixWhitelistSeparatorKeys = new Set([' ', ',', ',', 'Enter', 'Tab'])
|
||||
|
||||
function removeRegistrationEmailSuffixWhitelistTag(suffix: string) {
|
||||
registrationEmailSuffixWhitelistTags.value = registrationEmailSuffixWhitelistTags.value.filter(
|
||||
(item) => item !== suffix
|
||||
)
|
||||
}
|
||||
|
||||
function addRegistrationEmailSuffixWhitelistTag(raw: string) {
|
||||
const suffix = normalizeRegistrationEmailSuffixDomain(raw)
|
||||
if (
|
||||
!isRegistrationEmailSuffixDomainValid(suffix) ||
|
||||
registrationEmailSuffixWhitelistTags.value.includes(suffix)
|
||||
) {
|
||||
return
|
||||
}
|
||||
registrationEmailSuffixWhitelistTags.value = [
|
||||
...registrationEmailSuffixWhitelistTags.value,
|
||||
suffix
|
||||
]
|
||||
}
|
||||
|
||||
function commitRegistrationEmailSuffixWhitelistDraft() {
|
||||
if (!registrationEmailSuffixWhitelistDraft.value) {
|
||||
return
|
||||
}
|
||||
addRegistrationEmailSuffixWhitelistTag(registrationEmailSuffixWhitelistDraft.value)
|
||||
registrationEmailSuffixWhitelistDraft.value = ''
|
||||
}
|
||||
|
||||
function handleRegistrationEmailSuffixWhitelistDraftInput() {
|
||||
registrationEmailSuffixWhitelistDraft.value = normalizeRegistrationEmailSuffixDomain(
|
||||
registrationEmailSuffixWhitelistDraft.value
|
||||
)
|
||||
}
|
||||
|
||||
function handleRegistrationEmailSuffixWhitelistDraftKeydown(event: KeyboardEvent) {
|
||||
if (event.isComposing) {
|
||||
return
|
||||
}
|
||||
|
||||
if (registrationEmailSuffixWhitelistSeparatorKeys.has(event.key)) {
|
||||
event.preventDefault()
|
||||
commitRegistrationEmailSuffixWhitelistDraft()
|
||||
return
|
||||
}
|
||||
|
||||
if (
|
||||
event.key === 'Backspace' &&
|
||||
!registrationEmailSuffixWhitelistDraft.value &&
|
||||
registrationEmailSuffixWhitelistTags.value.length > 0
|
||||
) {
|
||||
registrationEmailSuffixWhitelistTags.value.pop()
|
||||
}
|
||||
}
|
||||
|
||||
function handleRegistrationEmailSuffixWhitelistPaste(event: ClipboardEvent) {
|
||||
const text = event.clipboardData?.getData('text') || ''
|
||||
if (!text.trim()) {
|
||||
return
|
||||
}
|
||||
event.preventDefault()
|
||||
const tokens = parseRegistrationEmailSuffixWhitelistInput(text)
|
||||
for (const token of tokens) {
|
||||
addRegistrationEmailSuffixWhitelistTag(token)
|
||||
}
|
||||
}
|
||||
|
||||
// LinuxDo OAuth redirect URL suggestion
|
||||
const linuxdoRedirectUrlSuggestion = computed(() => {
|
||||
if (typeof window === 'undefined') return ''
|
||||
@@ -1546,6 +1674,10 @@ async function loadSettings() {
|
||||
validity_days: item.validity_days
|
||||
}))
|
||||
: []
|
||||
registrationEmailSuffixWhitelistTags.value = normalizeRegistrationEmailSuffixDomains(
|
||||
settings.registration_email_suffix_whitelist
|
||||
)
|
||||
registrationEmailSuffixWhitelistDraft.value = ''
|
||||
form.smtp_password = ''
|
||||
form.turnstile_secret_key = ''
|
||||
form.linuxdo_connect_client_secret = ''
|
||||
@@ -1615,6 +1747,9 @@ async function saveSettings() {
|
||||
const payload: UpdateSettingsRequest = {
|
||||
registration_enabled: form.registration_enabled,
|
||||
email_verify_enabled: form.email_verify_enabled,
|
||||
registration_email_suffix_whitelist: registrationEmailSuffixWhitelistTags.value.map(
|
||||
(suffix) => `@${suffix}`
|
||||
),
|
||||
promo_code_enabled: form.promo_code_enabled,
|
||||
invitation_code_enabled: form.invitation_code_enabled,
|
||||
password_reset_enabled: form.password_reset_enabled,
|
||||
@@ -1660,6 +1795,10 @@ async function saveSettings() {
|
||||
}
|
||||
const updated = await adminAPI.settings.updateSettings(payload)
|
||||
Object.assign(form, updated)
|
||||
registrationEmailSuffixWhitelistTags.value = normalizeRegistrationEmailSuffixDomains(
|
||||
updated.registration_email_suffix_whitelist
|
||||
)
|
||||
registrationEmailSuffixWhitelistDraft.value = ''
|
||||
form.smtp_password = ''
|
||||
form.turnstile_secret_key = ''
|
||||
form.linuxdo_connect_client_secret = ''
|
||||
|
||||
Reference in New Issue
Block a user