From a5b9b68b763417a67443a4288573cfb851c2318c Mon Sep 17 00:00:00 2001 From: wucm667 Date: Thu, 21 May 2026 21:02:26 +0800 Subject: [PATCH] =?UTF-8?q?feat(registration):=20=E6=94=AF=E6=8C=81?= =?UTF-8?q?=E9=82=AE=E7=AE=B1=E7=99=BD=E5=90=8D=E5=8D=95=E5=90=8E=E7=BC=80?= =?UTF-8?q?=E9=80=9A=E9=85=8D=E7=AC=A6?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../service/registration_email_policy.go | 35 ++++++++-- .../service/registration_email_policy_test.go | 25 +++++-- .../service/setting_service_public_test.go | 4 +- .../service/setting_service_update_test.go | 4 +- frontend/src/i18n/locales/en.ts | 7 +- frontend/src/i18n/locales/zh.ts | 7 +- .../__tests__/registrationEmailPolicy.spec.ts | 58 +++++++++++++-- frontend/src/utils/registrationEmailPolicy.ts | 70 ++++++++++++++++--- frontend/src/views/admin/SettingsView.vue | 9 +-- frontend/src/views/auth/EmailVerifyView.vue | 6 +- frontend/src/views/auth/RegisterView.vue | 6 +- 11 files changed, 185 insertions(+), 46 deletions(-) diff --git a/backend/internal/service/registration_email_policy.go b/backend/internal/service/registration_email_policy.go index 875668c7..0c910d62 100644 --- a/backend/internal/service/registration_email_policy.go +++ b/backend/internal/service/registration_email_policy.go @@ -26,12 +26,17 @@ func IsRegistrationEmailSuffixAllowed(email string, whitelist []string) bool { if len(whitelist) == 0 { return true } - suffix := RegistrationEmailSuffix(email) - if suffix == "" { + _, domain, ok := splitEmailForPolicy(email) + if !ok { return false } + suffix := "@" + domain for _, allowed := range whitelist { - if suffix == allowed { + allowed = strings.ToLower(strings.TrimSpace(allowed)) + if strings.HasPrefix(allowed, "@") && suffix == allowed { + return true + } + if strings.HasPrefix(allowed, "*.") && registrationEmailDomainMatchesWildcard(domain, allowed) { return true } } @@ -98,6 +103,14 @@ func normalizeRegistrationEmailSuffix(raw string) (string, error) { return "", nil } + if strings.HasPrefix(value, "*.") { + domain := strings.TrimPrefix(value, "*.") + if !isValidRegistrationEmailDomain(domain) { + return "", fmt.Errorf("invalid email suffix: %q", raw) + } + return "*." + domain, nil + } + domain := value if strings.Contains(value, "@") { if !strings.HasPrefix(value, "@") || strings.Count(value, "@") != 1 { @@ -106,13 +119,27 @@ func normalizeRegistrationEmailSuffix(raw string) (string, error) { domain = strings.TrimPrefix(value, "@") } - if domain == "" || strings.Contains(domain, "@") || !registrationEmailDomainPattern.MatchString(domain) { + if !isValidRegistrationEmailDomain(domain) { return "", fmt.Errorf("invalid email suffix: %q", raw) } return "@" + domain, nil } +func isValidRegistrationEmailDomain(domain string) bool { + return domain != "" && + !strings.Contains(domain, "@") && + registrationEmailDomainPattern.MatchString(domain) +} + +func registrationEmailDomainMatchesWildcard(domain string, allowed string) bool { + base := strings.TrimPrefix(allowed, "*.") + if !isValidRegistrationEmailDomain(base) { + return false + } + return domain == base || strings.HasSuffix(domain, "."+base) +} + func splitEmailForPolicy(raw string) (local string, domain string, ok bool) { email := strings.ToLower(strings.TrimSpace(raw)) local, domain, found := strings.Cut(email, "@") diff --git a/backend/internal/service/registration_email_policy_test.go b/backend/internal/service/registration_email_policy_test.go index f0c46642..79f47be0 100644 --- a/backend/internal/service/registration_email_policy_test.go +++ b/backend/internal/service/registration_email_policy_test.go @@ -9,23 +9,36 @@ import ( ) func TestNormalizeRegistrationEmailSuffixWhitelist(t *testing.T) { - got, err := NormalizeRegistrationEmailSuffixWhitelist([]string{"example.com", "@EXAMPLE.COM", " @foo.bar "}) + got, err := NormalizeRegistrationEmailSuffixWhitelist([]string{"example.com", "@EXAMPLE.COM", " @foo.bar ", "*.EDU.CN"}) require.NoError(t, err) - require.Equal(t, []string{"@example.com", "@foo.bar"}, got) + require.Equal(t, []string{"@example.com", "@foo.bar", "*.edu.cn"}, got) } func TestNormalizeRegistrationEmailSuffixWhitelist_Invalid(t *testing.T) { - _, err := NormalizeRegistrationEmailSuffixWhitelist([]string{"@invalid_domain"}) - require.Error(t, err) + for _, item := range []string{"@invalid_domain", "*.", "*", "*.@", "*.foo"} { + t.Run(item, func(t *testing.T) { + _, err := NormalizeRegistrationEmailSuffixWhitelist([]string{item}) + require.Error(t, err) + }) + } } func TestParseRegistrationEmailSuffixWhitelist(t *testing.T) { - got := ParseRegistrationEmailSuffixWhitelist(`["example.com","@foo.bar","@invalid_domain"]`) - require.Equal(t, []string{"@example.com", "@foo.bar"}, got) + got := ParseRegistrationEmailSuffixWhitelist(`["example.com","@foo.bar","*.EDU.CN","@invalid_domain","*.foo"]`) + require.Equal(t, []string{"@example.com", "@foo.bar", "*.edu.cn"}, got) } func TestIsRegistrationEmailSuffixAllowed(t *testing.T) { require.True(t, IsRegistrationEmailSuffixAllowed("user@example.com", []string{"@example.com"})) require.False(t, IsRegistrationEmailSuffixAllowed("user@sub.example.com", []string{"@example.com"})) + require.True(t, IsRegistrationEmailSuffixAllowed("user@qq.com", []string{"@qq.com"})) + require.False(t, IsRegistrationEmailSuffixAllowed("user@sub.qq.com", []string{"@qq.com"})) + require.True(t, IsRegistrationEmailSuffixAllowed("student@cs.edu.cn", []string{"*.edu.cn"})) + require.True(t, IsRegistrationEmailSuffixAllowed("student@edu.cn", []string{"*.edu.cn"})) + require.False(t, IsRegistrationEmailSuffixAllowed("student@foo.cn", []string{"*.edu.cn"})) + require.True(t, IsRegistrationEmailSuffixAllowed("user@a.com", []string{"@a.com", "*.b.cn"})) + require.True(t, IsRegistrationEmailSuffixAllowed("user@school.b.cn", []string{"@a.com", "*.b.cn"})) + require.True(t, IsRegistrationEmailSuffixAllowed("user@b.cn", []string{"@a.com", "*.b.cn"})) + require.False(t, IsRegistrationEmailSuffixAllowed("user@c.cn", []string{"@a.com", "*.b.cn"})) require.True(t, IsRegistrationEmailSuffixAllowed("user@any.com", []string{})) } diff --git a/backend/internal/service/setting_service_public_test.go b/backend/internal/service/setting_service_public_test.go index 1ecd4e6f..2faa4d82 100644 --- a/backend/internal/service/setting_service_public_test.go +++ b/backend/internal/service/setting_service_public_test.go @@ -53,14 +53,14 @@ func TestSettingService_GetPublicSettings_ExposesRegistrationEmailSuffixWhitelis values: map[string]string{ SettingKeyRegistrationEnabled: "true", SettingKeyEmailVerifyEnabled: "true", - SettingKeyRegistrationEmailSuffixWhitelist: `["@EXAMPLE.com"," @foo.bar ","@invalid_domain",""]`, + SettingKeyRegistrationEmailSuffixWhitelist: `["@EXAMPLE.com"," @foo.bar ","*.EDU.CN","@invalid_domain",""]`, }, } svc := NewSettingService(repo, &config.Config{}) settings, err := svc.GetPublicSettings(context.Background()) require.NoError(t, err) - require.Equal(t, []string{"@example.com", "@foo.bar"}, settings.RegistrationEmailSuffixWhitelist) + require.Equal(t, []string{"@example.com", "@foo.bar", "*.edu.cn"}, settings.RegistrationEmailSuffixWhitelist) } func TestSettingService_GetPublicSettings_ExposesTablePreferences(t *testing.T) { diff --git a/backend/internal/service/setting_service_update_test.go b/backend/internal/service/setting_service_update_test.go index 07add066..379bf9bc 100644 --- a/backend/internal/service/setting_service_update_test.go +++ b/backend/internal/service/setting_service_update_test.go @@ -213,10 +213,10 @@ func TestSettingService_UpdateSettings_RegistrationEmailSuffixWhitelist_Normaliz svc := NewSettingService(repo, &config.Config{}) err := svc.UpdateSettings(context.Background(), &SystemSettings{ - RegistrationEmailSuffixWhitelist: []string{"example.com", "@EXAMPLE.com", " @foo.bar "}, + RegistrationEmailSuffixWhitelist: []string{"example.com", "@EXAMPLE.com", " @foo.bar ", "*.EDU.CN"}, }) require.NoError(t, err) - require.Equal(t, `["@example.com","@foo.bar"]`, repo.updates[SettingKeyRegistrationEmailSuffixWhitelist]) + require.Equal(t, `["@example.com","@foo.bar","*.edu.cn"]`, repo.updates[SettingKeyRegistrationEmailSuffixWhitelist]) } func TestSettingService_UpdateSettings_RegistrationEmailSuffixWhitelist_Invalid(t *testing.T) { diff --git a/frontend/src/i18n/locales/en.ts b/frontend/src/i18n/locales/en.ts index 1b538b92..224d1b9d 100644 --- a/frontend/src/i18n/locales/en.ts +++ b/frontend/src/i18n/locales/en.ts @@ -423,6 +423,7 @@ export default { emailSuffixNotAllowed: 'This email domain is not allowed for registration.', emailSuffixNotAllowedWithAllowed: 'This email domain is not allowed. Allowed domains: {suffixes}', + emailSuffixAllowedMore: 'and {count} more', loginSuccess: 'Login successful! Welcome back.', accountCreatedSuccess: 'Account created successfully! Welcome to {siteName}.', reloginRequired: 'Session expired. Please log in again.', @@ -5282,9 +5283,9 @@ export default { emailVerificationHint: 'Require email verification for new registrations', emailSuffixWhitelist: 'Email Domain Whitelist', emailSuffixWhitelistHint: - "Only email addresses from the specified domains can register (for example, {'@'}qq.com, {'@'}gmail.com)", - emailSuffixWhitelistPlaceholder: 'example.com', - emailSuffixWhitelistInputHint: 'Leave empty for no restriction', + "Only email addresses from the specified domains can register (for example, {'@'}qq.com, {'@'}gmail.com, *.edu.cn)", + emailSuffixWhitelistPlaceholder: '@example.com, *.edu.cn', + emailSuffixWhitelistInputHint: 'Leave empty for no restriction. Use *.edu.cn to match edu.cn and its subdomains.', promoCode: 'Promo Code', promoCodeHint: 'Allow users to use promo codes during registration', invitationCode: 'Invitation Code Registration', diff --git a/frontend/src/i18n/locales/zh.ts b/frontend/src/i18n/locales/zh.ts index 67285c3f..e574bf15 100644 --- a/frontend/src/i18n/locales/zh.ts +++ b/frontend/src/i18n/locales/zh.ts @@ -422,6 +422,7 @@ export default { registrationFailed: '注册失败,请重试。', emailSuffixNotAllowed: '该邮箱域名不在允许注册范围内。', emailSuffixNotAllowedWithAllowed: '该邮箱域名不被允许。可用域名:{suffixes}', + emailSuffixAllowedMore: '等 {count} 项', loginSuccess: '登录成功!欢迎回来。', accountCreatedSuccess: '账户创建成功!欢迎使用 {siteName}。', reloginRequired: '会话已过期,请重新登录。', @@ -5445,9 +5446,9 @@ export default { emailVerificationHint: '新用户注册时需要验证邮箱', emailSuffixWhitelist: '邮箱域名白名单', emailSuffixWhitelistHint: - "仅允许使用指定域名的邮箱注册账号(例如 {'@'}qq.com, {'@'}gmail.com)", - emailSuffixWhitelistPlaceholder: 'example.com', - emailSuffixWhitelistInputHint: '留空则不限制', + "仅允许使用指定域名的邮箱注册账号(例如 {'@'}qq.com, {'@'}gmail.com, *.edu.cn)", + emailSuffixWhitelistPlaceholder: '@example.com, *.edu.cn', + emailSuffixWhitelistInputHint: '留空则不限制。使用 *.edu.cn 可匹配 edu.cn 及其子域名。', promoCode: '优惠码', promoCodeHint: '允许用户在注册时使用优惠码', invitationCode: '邀请码注册', diff --git a/frontend/src/utils/__tests__/registrationEmailPolicy.spec.ts b/frontend/src/utils/__tests__/registrationEmailPolicy.spec.ts index 021f0fc4..492a9a3d 100644 --- a/frontend/src/utils/__tests__/registrationEmailPolicy.spec.ts +++ b/frontend/src/utils/__tests__/registrationEmailPolicy.spec.ts @@ -1,5 +1,6 @@ import { describe, expect, it } from 'vitest' import { + formatRegistrationEmailSuffixWhitelistForMessage, isRegistrationEmailSuffixAllowed, isRegistrationEmailSuffixDomainValid, normalizeRegistrationEmailSuffixDomain, @@ -11,6 +12,7 @@ import { describe('registrationEmailPolicy utils', () => { it('normalizeRegistrationEmailSuffixDomain lowercases, strips @, and ignores invalid chars', () => { expect(normalizeRegistrationEmailSuffixDomain(' @Exa!mple.COM ')).toBe('example.com') + expect(normalizeRegistrationEmailSuffixDomain(' *.EDU!.CN ')).toBe('*.edu.cn') }) it('normalizeRegistrationEmailSuffixDomains deduplicates normalized domains', () => { @@ -22,14 +24,20 @@ describe('registrationEmailPolicy utils', () => { '-invalid.com', 'foo..bar.com', ' @foo.bar ', - '@foo.bar' + '@foo.bar', + '*.EDU.CN', + '*.edu.cn' ]) - ).toEqual(['example.com', 'foo.bar']) + ).toEqual(['example.com', 'foo.bar', '*.edu.cn']) }) it('parseRegistrationEmailSuffixWhitelistInput supports separators and deduplicates', () => { - const input = '\n @example.com,example.com,@foo.bar\t@FOO.bar ' - expect(parseRegistrationEmailSuffixWhitelistInput(input)).toEqual(['example.com', 'foo.bar']) + const input = '\n @example.com,example.com,@foo.bar\t@FOO.bar *.EDU.CN ' + expect(parseRegistrationEmailSuffixWhitelistInput(input)).toEqual([ + 'example.com', + 'foo.bar', + '*.edu.cn' + ]) }) it('parseRegistrationEmailSuffixWhitelistInput drops tokens containing invalid chars', () => { @@ -38,7 +46,7 @@ describe('registrationEmailPolicy utils', () => { }) it('parseRegistrationEmailSuffixWhitelistInput drops structurally invalid domains', () => { - const input = '@-bad.com, @foo..bar.com, @foo.bar, @xn--ok.com' + const input = '@-bad.com, @foo..bar.com, @foo.bar, @xn--ok.com, *., *, *.@, *.foo' expect(parseRegistrationEmailSuffixWhitelistInput(input)).toEqual(['foo.bar', 'xn--ok.com']) }) @@ -53,17 +61,22 @@ describe('registrationEmailPolicy utils', () => { 'foo.bar', '', '-invalid.com', - ' @foo.bar ' + ' @foo.bar ', + '*.EDU.CN' ]) - ).toEqual(['@example.com', '@foo.bar']) + ).toEqual(['@example.com', '@foo.bar', '*.edu.cn']) }) it('isRegistrationEmailSuffixDomainValid matches backend-compatible domain rules', () => { expect(isRegistrationEmailSuffixDomainValid('example.com')).toBe(true) expect(isRegistrationEmailSuffixDomainValid('foo-bar.example.com')).toBe(true) + expect(isRegistrationEmailSuffixDomainValid('*.edu.cn')).toBe(true) expect(isRegistrationEmailSuffixDomainValid('-bad.com')).toBe(false) expect(isRegistrationEmailSuffixDomainValid('foo..bar.com')).toBe(false) expect(isRegistrationEmailSuffixDomainValid('localhost')).toBe(false) + expect(isRegistrationEmailSuffixDomainValid('*.foo')).toBe(false) + expect(isRegistrationEmailSuffixDomainValid('*')).toBe(false) + expect(isRegistrationEmailSuffixDomainValid('*.@')).toBe(false) }) it('isRegistrationEmailSuffixAllowed allows any email when whitelist is empty', () => { @@ -73,5 +86,36 @@ describe('registrationEmailPolicy utils', () => { it('isRegistrationEmailSuffixAllowed applies exact suffix matching', () => { expect(isRegistrationEmailSuffixAllowed('user@example.com', ['@example.com'])).toBe(true) expect(isRegistrationEmailSuffixAllowed('user@sub.example.com', ['@example.com'])).toBe(false) + expect(isRegistrationEmailSuffixAllowed('user@qq.com', ['@qq.com'])).toBe(true) + expect(isRegistrationEmailSuffixAllowed('user@sub.qq.com', ['@qq.com'])).toBe(false) + }) + + it('isRegistrationEmailSuffixAllowed applies wildcard suffix matching', () => { + expect(isRegistrationEmailSuffixAllowed('student@cs.edu.cn', ['*.edu.cn'])).toBe(true) + expect(isRegistrationEmailSuffixAllowed('student@edu.cn', ['*.edu.cn'])).toBe(true) + expect(isRegistrationEmailSuffixAllowed('student@foo.cn', ['*.edu.cn'])).toBe(false) + }) + + it('isRegistrationEmailSuffixAllowed supports mixed exact and wildcard entries', () => { + const whitelist = ['@a.com', '*.b.cn'] + expect(isRegistrationEmailSuffixAllowed('user@a.com', whitelist)).toBe(true) + expect(isRegistrationEmailSuffixAllowed('user@school.b.cn', whitelist)).toBe(true) + expect(isRegistrationEmailSuffixAllowed('user@b.cn', whitelist)).toBe(true) + expect(isRegistrationEmailSuffixAllowed('user@c.cn', whitelist)).toBe(false) + }) + + it('formatRegistrationEmailSuffixWhitelistForMessage lists up to five entries', () => { + expect( + formatRegistrationEmailSuffixWhitelistForMessage( + ['@a.com', '@b.com', '@c.com', '@d.com', '@e.com'], + { separator: ', ', more: (count) => `and ${count} more` } + ) + ).toBe('@a.com, @b.com, @c.com, @d.com, @e.com') + expect( + formatRegistrationEmailSuffixWhitelistForMessage( + ['@a.com', '@b.com', '@c.com', '@d.com', '@e.com', '*.edu.cn', '@f.com'], + { separator: ', ', more: (count) => `and ${count} more` } + ) + ).toBe('@a.com, @b.com, @c.com, @d.com, @e.com, and 2 more') }) }) diff --git a/frontend/src/utils/registrationEmailPolicy.ts b/frontend/src/utils/registrationEmailPolicy.ts index 74d63fc4..bdb3dbc5 100644 --- a/frontend/src/utils/registrationEmailPolicy.ts +++ b/frontend/src/utils/registrationEmailPolicy.ts @@ -2,19 +2,21 @@ const EMAIL_SUFFIX_TOKEN_SPLIT_RE = /[\s,,]+/ const EMAIL_SUFFIX_INVALID_CHAR_RE = /[^a-z0-9.-]/g const EMAIL_SUFFIX_INVALID_CHAR_CHECK_RE = /[^a-z0-9.-]/ const EMAIL_SUFFIX_PREFIX_RE = /^@+/ +const EMAIL_SUFFIX_WILDCARD_PREFIX = '*.' +const EMAIL_SUFFIX_MESSAGE_VISIBLE_LIMIT = 5 const EMAIL_SUFFIX_DOMAIN_PATTERN = /^[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?(?:\.[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?)+$/ // normalizeRegistrationEmailSuffixDomain converts raw input into a canonical domain token. -// It removes leading "@", lowercases input, and strips all invalid characters. +// Exact domains are returned without "@"; wildcard domains keep the "*." prefix. export function normalizeRegistrationEmailSuffixDomain(raw: string): string { let value = String(raw || '').trim().toLowerCase() if (!value) { return '' } + value = value.replace(EMAIL_SUFFIX_PREFIX_RE, '') - value = value.replace(EMAIL_SUFFIX_INVALID_CHAR_RE, '') - return value + return normalizeRegistrationEmailSuffixToken(value, false) } export function normalizeRegistrationEmailSuffixDomains( @@ -60,7 +62,7 @@ export function parseRegistrationEmailSuffixWhitelistInput(input: string): strin export function normalizeRegistrationEmailSuffixWhitelist( items: string[] | null | undefined ): string[] { - return normalizeRegistrationEmailSuffixDomains(items).map((domain) => `@${domain}`) + return normalizeRegistrationEmailSuffixDomains(items).map(toCanonicalRegistrationEmailSuffix) } function extractRegistrationEmailDomain(email: string): string { @@ -91,7 +93,32 @@ export function isRegistrationEmailSuffixAllowed( return false } const emailSuffix = `@${emailDomain}` - return normalizedWhitelist.includes(emailSuffix) + return normalizedWhitelist.some((allowed) => { + if (allowed.startsWith('@')) { + return allowed === emailSuffix + } + if (allowed.startsWith(EMAIL_SUFFIX_WILDCARD_PREFIX)) { + const base = allowed.slice(EMAIL_SUFFIX_WILDCARD_PREFIX.length) + return emailDomain === base || emailDomain.endsWith(`.${base}`) + } + return false + }) +} + +export function formatRegistrationEmailSuffixWhitelistForMessage( + whitelist: string[] | null | undefined, + options: { + separator: string + more: (count: number) => string + } +): string { + const normalizedWhitelist = normalizeRegistrationEmailSuffixWhitelist(whitelist) + const visible = normalizedWhitelist.slice(0, EMAIL_SUFFIX_MESSAGE_VISIBLE_LIMIT) + const hiddenCount = normalizedWhitelist.length - visible.length + if (hiddenCount > 0) { + visible.push(options.more(hiddenCount)) + } + return visible.join(options.separator) } // Pasted domains should be strict: any invalid character drops the whole token. @@ -101,15 +128,38 @@ function normalizeRegistrationEmailSuffixDomainStrict(raw: string): string { return '' } value = value.replace(EMAIL_SUFFIX_PREFIX_RE, '') - if (!value || EMAIL_SUFFIX_INVALID_CHAR_CHECK_RE.test(value)) { - return '' - } - return value + return normalizeRegistrationEmailSuffixToken(value, true) } export function isRegistrationEmailSuffixDomainValid(domain: string): boolean { if (!domain) { return false } - return EMAIL_SUFFIX_DOMAIN_PATTERN.test(domain) + if (domain.startsWith(EMAIL_SUFFIX_WILDCARD_PREFIX)) { + return EMAIL_SUFFIX_DOMAIN_PATTERN.test(domain.slice(EMAIL_SUFFIX_WILDCARD_PREFIX.length)) + } + return !domain.includes('*') && EMAIL_SUFFIX_DOMAIN_PATTERN.test(domain) +} + +function normalizeRegistrationEmailSuffixToken(value: string, strict: boolean): string { + if (value.startsWith(EMAIL_SUFFIX_WILDCARD_PREFIX)) { + const domain = value.slice(EMAIL_SUFFIX_WILDCARD_PREFIX.length) + if (strict && (!domain || EMAIL_SUFFIX_INVALID_CHAR_CHECK_RE.test(domain))) { + return '' + } + return `${EMAIL_SUFFIX_WILDCARD_PREFIX}${domain.replace(EMAIL_SUFFIX_INVALID_CHAR_RE, '')}` + } + + if (value === '*') { + return strict ? '' : value + } + + if (strict && EMAIL_SUFFIX_INVALID_CHAR_CHECK_RE.test(value)) { + return '' + } + return value.replace(/[*]/g, '').replace(EMAIL_SUFFIX_INVALID_CHAR_RE, '') +} + +function toCanonicalRegistrationEmailSuffix(domain: string): string { + return domain.startsWith(EMAIL_SUFFIX_WILDCARD_PREFIX) ? domain : `@${domain}` } diff --git a/frontend/src/views/admin/SettingsView.vue b/frontend/src/views/admin/SettingsView.vue index 886de3ad..d3fac4d5 100644 --- a/frontend/src/views/admin/SettingsView.vue +++ b/frontend/src/views/admin/SettingsView.vue @@ -1415,7 +1415,6 @@ :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" > - @ {{ suffix }}