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 }}