feat: allow custom subdomains in create UI (#1109)

* feat: allow custom subdomains in create UI

* test: stabilize custom subdomain browser flow

* test: fix normalized address expectation

* fix: gate custom subdomain input by setting

* fix: scope custom subdomains to random domains

* test: recreate random subdomain manually

* refactor: minimize custom subdomain changes

* refactor: reduce custom subdomain changes

* test: fix custom subdomain locator

* refactor: clarify manual subdomain validation

* test: target visible custom subdomain input

* refactor: simplify manual subdomain validation

* refactor: use subdomain mode selector

* style: stack subdomain modes vertically

* test: click visible subdomain mode labels
This commit is contained in:
Dream Hunter
2026-08-19 12:10:09 +08:00
committed by GitHub
parent 624fc9bb96
commit a3c62de42f
17 changed files with 225 additions and 66 deletions
+5 -1
View File
@@ -526,7 +526,7 @@ export const deMessages = {
"views.index.LocalAddress.tip": "Diese Adressen werden in deinem Browser gespeichert und können verloren gehen, wenn du den Browser-Cache leerst.",
"views.admin.UserOauth2Settings.tip": "Drittanbieter-Login verwendet automatisch die E-Mail-Adresse des Benutzers zur Registrierung eines Kontos (dieselbe E-Mail gilt als dasselbe Konto). Das Konto entspricht dem regulär registrierten Konto, und das Passwort kann auch über „Passwort vergessen“ gesetzt werden.",
"views.admin.AccountSettings.send_mail_limit_tip": "Dies gilt für alle Sendekanäle. Verwende -1 für unbegrenzt und 0, um das Senden zu blockieren.",
"views.admin.AccountSettings.create_address_subdomain_match_note": "Dies unterscheidet sich von RANDOM_SUBDOMAIN_DOMAINS: Dieser Schalter erlaubt API-Aufrufern, benutzerdefinierte Subdomains direkt anzugeben, während die zufällige Subdomain nur bei der Erstellung automatisch erzeugt wird.",
"views.admin.AccountSettings.create_address_subdomain_match_note": "RANDOM_SUBDOMAIN_DOMAINS erlaubt bereits zufällige oder manuelle Subdomains für gelistete Basisdomains. Dieser Schalter erlaubt API-Aufrufern zusätzlich Subdomains unter anderen erlaubten Basisdomains.",
"views.index.SendMail.tooLarge": "Datei zu groß; bitte eine Datei unter 1 MB hochladen.",
"views.admin.SendMail.tooLarge": "Datei zu groß; bitte eine Datei unter 1 MB hochladen.",
"views.common.Appearance.top": "oben",
@@ -582,6 +582,10 @@ export const deMessages = {
"views.admin.AiExtractSettings.disabledTip": "Wenn deaktiviert, verarbeitet die KI-Extraktion alle E-Mail-Adressen",
"views.admin.AiExtractSettings.enableAllowListTip": "Wenn aktiviert, verarbeitet die KI-Extraktion nur E-Mails an Adressen auf der Freigabeliste",
"views.admin.CreateAccount.randomSubdomainTip": "Wenn aktiviert, verwendet die erstellte Adresse eine zufällige Subdomain. Nur für den Empfang empfohlen. Erfordert einen Wildcard-MX-DNS-Eintrag auf der Basisdomain — siehe die Dokumentation zu zufälligen Subdomains.",
"views.admin.CreateAccount.enableCustomSubdomain": "Benutzerdefinierte Subdomain verwenden",
"views.admin.CreateAccount.normalSubdomain": "Normale Domain",
"views.common.Login.enableCustomSubdomain": "Benutzerdefinierte Subdomain verwenden",
"views.common.Login.normalSubdomain": "Normale Domain",
"views.common.Login.randomSubdomainTip": "Wenn aktiviert, verwendet die erstellte Adresse eine zufällige Subdomain. Nur für den Empfang empfohlen. Erfordert einen Wildcard-MX-DNS-Eintrag auf der Basisdomain — siehe die Dokumentation zu zufälligen Subdomains.",
"views.admin.AiExtractSettings.allowListTip": "Der Platzhalter * passt auf beliebige Zeichen; z. B. passt *{'@'}example.com auf alle Adressen der Domain example.com",
"views.Admin.workerconfig": "Worker-Konfiguration",
+5 -1
View File
@@ -526,7 +526,7 @@ export const esMessages = {
"views.index.LocalAddress.tip": "Estas direcciones se guardan en tu navegador y podrían perderse si borras la caché.",
"views.admin.UserOauth2Settings.tip": "El inicio de sesión de terceros usará automáticamente el correo del usuario para registrar una cuenta (el mismo correo se considera la misma cuenta). También puedes establecer la contraseña con “olvidé mi contraseña”.",
"views.admin.AccountSettings.send_mail_limit_tip": "Se aplica a todos los canales de envío. Usa -1 para ilimitado y 0 para bloquear el envío.",
"views.admin.AccountSettings.create_address_subdomain_match_note": "Esto es diferente de RANDOM_SUBDOMAIN_DOMAINS: este interruptor permite indicar subdominios personalizados; el subdominio aleatorio solo genera uno al crear.",
"views.admin.AccountSettings.create_address_subdomain_match_note": "RANDOM_SUBDOMAIN_DOMAINS ya permite subdominios aleatorios o manuales en los dominios base listados. Este interruptor permite además que la API especifique subdominios en otros dominios base permitidos.",
"views.index.SendMail.tooLarge": "Archivo demasiado grande; sube uno de menos de 1 MB.",
"views.admin.SendMail.tooLarge": "Archivo demasiado grande; sube uno de menos de 1 MB.",
"views.common.Appearance.top": "arriba",
@@ -582,6 +582,10 @@ export const esMessages = {
"views.admin.AiExtractSettings.disabledTip": "Si está desactivado, la extracción IA procesará todas las direcciones",
"views.admin.AiExtractSettings.enableAllowListTip": "Si está activado, la extracción IA solo procesará correos enviados a direcciones permitidas",
"views.admin.CreateAccount.randomSubdomainTip": "Si está activado, la dirección creada usará un subdominio aleatorio. Recomendado solo para recibir. Requiere un registro MX comodín en el DNS del dominio base — consulta la documentación de subdominios aleatorios.",
"views.admin.CreateAccount.enableCustomSubdomain": "Usar subdominio personalizado",
"views.admin.CreateAccount.normalSubdomain": "Dominio normal",
"views.common.Login.enableCustomSubdomain": "Usar subdominio personalizado",
"views.common.Login.normalSubdomain": "Dominio normal",
"views.common.Login.randomSubdomainTip": "Si está activado, la dirección creada usará un subdominio aleatorio. Recomendado solo para recibir. Requiere un registro MX comodín en el DNS del dominio base — consulta la documentación de subdominios aleatorios.",
"views.admin.AiExtractSettings.allowListTip": "El comodín * coincide con cualquier carácter; p. ej., *{'@'}example.com coincide con todas las direcciones del dominio example.com",
"views.Admin.workerconfig": "Configuración del Worker",
+5 -1
View File
@@ -526,7 +526,7 @@ export const jaMessages = {
"views.index.LocalAddress.tip": "これらのアドレスはブラウザに保存されており、キャッシュを消すと失われる可能性があります。",
"views.admin.UserOauth2Settings.tip": "サードパーティログインではユーザーのメールアドレスで自動的にアカウント登録されます(同じメールは同一アカウントとして扱われます)。「パスワードを忘れた」からパスワード設定も可能です。",
"views.admin.AccountSettings.send_mail_limit_tip": "すべての送信チャネルに適用されます。-1 は無制限、0 は送信禁止です。",
"views.admin.AccountSettings.create_address_subdomain_match_note": "これは RANDOM_SUBDOMAIN_DOMAINS と異なります。この設定では API 呼び出し側が独自サブドメインを指定でき、ランダムサブドメインは作成時に自動生成されるだけです。",
"views.admin.AccountSettings.create_address_subdomain_match_note": "RANDOM_SUBDOMAIN_DOMAINS に列挙したベースドメインでは、ランダムまたは手動のサブドメインを利用できます。この設定は、他の許可済みベースドメインでも API からサブドメインを指定できるようにします。",
"views.index.SendMail.tooLarge": "ファイルが大きすぎます。1MB 未満のファイルをアップロードしてください。",
"views.admin.SendMail.tooLarge": "ファイルが大きすぎます。1MB 未満のファイルをアップロードしてください。",
"views.common.Appearance.top": "上",
@@ -582,6 +582,10 @@ export const jaMessages = {
"views.admin.AiExtractSettings.disabledTip": "無効時は AI 抽出がすべてのメールアドレスを処理します",
"views.admin.AiExtractSettings.enableAllowListTip": "有効時は AI 抽出は許可リストのアドレス宛メールのみ処理します",
"views.admin.CreateAccount.randomSubdomainTip": "有効時は作成されるアドレスがランダムなサブドメインを使用します。受信専用として推奨されます。ベースドメインの DNS にワイルドカード MX レコードの設定が必要です — ランダムサブドメインのドキュメントを参照してください。",
"views.admin.CreateAccount.enableCustomSubdomain": "カスタムサブドメインを使用",
"views.admin.CreateAccount.normalSubdomain": "通常ドメイン",
"views.common.Login.enableCustomSubdomain": "カスタムサブドメインを使用",
"views.common.Login.normalSubdomain": "通常ドメイン",
"views.common.Login.randomSubdomainTip": "有効時は作成されるアドレスがランダムなサブドメインを使用します。受信専用として推奨されます。ベースドメインの DNS にワイルドカード MX レコードの設定が必要です — ランダムサブドメインのドキュメントを参照してください。",
"views.admin.AiExtractSettings.allowListTip": "ワイルドカード * は任意の文字に一致します。例: *{'@'}example.com は example.com ドメイン配下のすべてのアドレスに一致します",
"views.Admin.workerconfig": "Worker設定",
+5 -1
View File
@@ -526,7 +526,7 @@ export const ptBRMessages = {
"views.index.LocalAddress.tip": "Esses endereços ficam armazenados no navegador e podem ser perdidos se você limpar o cache.",
"views.admin.UserOauth2Settings.tip": "O login de terceiros usará automaticamente o e-mail do usuário para registrar uma conta (o mesmo e-mail será considerado a mesma conta). Você também pode definir a senha via “esqueci minha senha”.",
"views.admin.AccountSettings.send_mail_limit_tip": "Aplica-se a todos os canais de envio. Use -1 para ilimitado e 0 para bloquear o envio.",
"views.admin.AccountSettings.create_address_subdomain_match_note": "Isso é diferente de RANDOM_SUBDOMAIN_DOMAINS: esta opção permite informar subdomínios personalizados; o subdomínio aleatório apenas gera um durante a criação.",
"views.admin.AccountSettings.create_address_subdomain_match_note": "RANDOM_SUBDOMAIN_DOMAINS já permite subdomínios aleatórios ou manuais nos domínios base listados. Esta opção também permite que a API informe subdomínios em outros domínios base permitidos.",
"views.index.SendMail.tooLarge": "Arquivo muito grande; envie um arquivo menor que 1 MB.",
"views.admin.SendMail.tooLarge": "Arquivo muito grande; envie um arquivo menor que 1 MB.",
"views.common.Appearance.top": "topo",
@@ -582,6 +582,10 @@ export const ptBRMessages = {
"views.admin.AiExtractSettings.disabledTip": "Quando desativado, a extração por IA processará todos os endereços",
"views.admin.AiExtractSettings.enableAllowListTip": "Quando ativado, a extração por IA só processará e-mails enviados aos endereços permitidos",
"views.admin.CreateAccount.randomSubdomainTip": "Quando ativado, o endereço criado usará um subdomínio aleatório. Recomendado apenas para recebimento. Requer um registro MX curinga no DNS do domínio base — consulte a documentação de subdomínios aleatórios.",
"views.admin.CreateAccount.enableCustomSubdomain": "Usar subdomínio personalizado",
"views.admin.CreateAccount.normalSubdomain": "Domínio normal",
"views.common.Login.enableCustomSubdomain": "Usar subdomínio personalizado",
"views.common.Login.normalSubdomain": "Domínio normal",
"views.common.Login.randomSubdomainTip": "Quando ativado, o endereço criado usará um subdomínio aleatório. Recomendado apenas para recebimento. Requer um registro MX curinga no DNS do domínio base — consulte a documentação de subdomínios aleatórios.",
"views.admin.AiExtractSettings.allowListTip": "O curinga * corresponde a quaisquer caracteres; ex.: *{'@'}example.com corresponde a todos os endereços do domínio example.com",
"views.Admin.workerconfig": "Configuração do Worker",
+18 -2
View File
@@ -2000,6 +2000,10 @@ export const MESSAGE_REGISTRY = {
"en": "Create New Email",
"zh": "创建新邮箱"
},
"enableCustomSubdomain": {
"en": "Use Custom Subdomain",
"zh": "使用自定义子域名"
},
"enablePrefix": {
"en": "If enable Prefix",
"zh": "是否启用前缀"
@@ -2016,6 +2020,10 @@ export const MESSAGE_REGISTRY = {
"en": "Open to auto login email link",
"zh": "打开即可自动登录邮箱的链接"
},
"normalSubdomain": {
"en": "Normal Domain",
"zh": "普通域名"
},
"randomSubdomainTip": {
"en": "When enabled, the created address will use a random subdomain. Recommended for receiving only. Requires a wildcard MX DNS record on the base domain — see the random subdomain docs.",
"zh": "启用后,创建出来的地址会自动挂在随机子域名下,建议仅用于收件。需要在基础域名 DNS 中配置通配 MX 记录,详见随机子域名文档。"
@@ -2225,8 +2233,8 @@ export const MESSAGE_REGISTRY = {
"zh": "强制开启"
},
"create_address_subdomain_match_note": {
"en": "This is different from RANDOM_SUBDOMAIN_DOMAINS: this switch allows API callers to specify custom subdomains directly, while random subdomain only auto-generates one during creation.",
"zh": "这与 RANDOM_SUBDOMAIN_DOMAINS 不同:这里允许 API 调用方直接指定自定义子域名随机子域名功能只是在创建时自动补一个随机子域名。"
"en": "RANDOM_SUBDOMAIN_DOMAINS already allows random or manual subdomains for listed base domains. This switch additionally allows API callers to specify subdomains under other allowed base domains.",
"zh": "RANDOM_SUBDOMAIN_DOMAINS 已允许在所列基础域名随机生成或手动输入子域名;此开关额外允许 API 在其他已授权基础域名下指定子域名。"
},
"create_address_subdomain_match_tip": {
"en": "Only affects /api/new_address and /admin/new_address domain validation. Example: when enabled, foo.example.com can match configured base domain example.com.",
@@ -2506,6 +2514,10 @@ export const MESSAGE_REGISTRY = {
"en": "Use Random Subdomain",
"zh": "启用随机子域名"
},
"enableCustomSubdomain": {
"en": "Use Custom Subdomain",
"zh": "使用自定义子域名"
},
"generateName": {
"en": "Generate Fake Name",
"zh": "生成随机名字"
@@ -2514,6 +2526,10 @@ export const MESSAGE_REGISTRY = {
"en": "Create New Email",
"zh": "创建新邮箱"
},
"normalSubdomain": {
"en": "Normal Domain",
"zh": "普通域名"
},
"getNewEmailTip1": {
"en": "Please input the email you want to use. only allow: ",
"zh": "请输入你想要使用的邮箱地址, 只允许: "
+26 -11
View File
@@ -14,7 +14,8 @@ const message = useMessage()
const { t } = useScopedI18n('views.admin.CreateAccount')
const enablePrefix = ref(true)
const enableRandomSubdomain = ref(false)
const subdomainMode = ref("normal")
const customSubdomain = ref("")
const emailName = ref("")
const emailDomain = ref("")
const showReultModal = ref(false)
@@ -31,7 +32,7 @@ const canUseRandomSubdomain = computed(() => {
watch(canUseRandomSubdomain, (enabled) => {
if (!enabled) {
enableRandomSubdomain.value = false
subdomainMode.value = "normal"
}
})
@@ -41,13 +42,16 @@ const newEmail = async () => {
return
}
try {
const domain = subdomainMode.value === "custom"
? `${customSubdomain.value.trim()}.${emailDomain.value}`
: emailDomain.value
const res = await api.fetch(`/admin/new_address`, {
method: 'POST',
body: JSON.stringify({
enablePrefix: enablePrefix.value,
enableRandomSubdomain: enableRandomSubdomain.value,
enableRandomSubdomain: subdomainMode.value === "random",
name: emailName.value,
domain: emailDomain.value,
domain,
})
})
result.value = res["jwt"];
@@ -88,14 +92,25 @@ onMounted(async () => {
</n-input-group>
</n-form-item-row>
<n-form-item-row v-if="canUseRandomSubdomain">
<n-checkbox v-model:checked="enableRandomSubdomain">
{{ t('enableRandomSubdomain') }}
</n-checkbox>
<p style="margin: 8px 0 0; opacity: 0.75;">
{{ t('randomSubdomainTip') }}
</p>
<div style="width: 100%;">
<n-radio-group v-model:value="subdomainMode">
<n-space vertical>
<n-radio value="normal">{{ t('normalSubdomain') }}</n-radio>
<n-radio value="random">{{ t('enableRandomSubdomain') }}</n-radio>
<n-radio value="custom">{{ t('enableCustomSubdomain') }}</n-radio>
</n-space>
</n-radio-group>
<p v-if="subdomainMode === 'random'" style="margin: 8px 0 0; opacity: 0.75;">
{{ t('randomSubdomainTip') }}
</p>
<n-input-group v-if="subdomainMode === 'custom'" style="margin-top: 8px;">
<n-input v-model:value="customSubdomain" />
<n-input-group-label>.{{ emailDomain }}</n-input-group-label>
</n-input-group>
</div>
</n-form-item-row>
<n-button @click="newEmail" type="primary" block :loading="loading">
<n-button @click="newEmail" type="primary" block :loading="loading"
:disabled="subdomainMode === 'custom' && !customSubdomain.trim()">
{{ t('creatNewEmail') }}
</n-button>
</n-card>
+26 -11
View File
@@ -48,7 +48,8 @@ const credential = ref('')
const emailName = ref("")
const emailDomain = ref("")
const cfToken = ref("")
const enableRandomSubdomain = ref(false)
const subdomainMode = ref("normal")
const customSubdomain = ref("")
const loginCfToken = ref("")
const loginTurnstileRef = ref(null)
const loginMethod = ref('credential') // 'credential' or 'password'
@@ -167,11 +168,14 @@ const newEmail = async () => {
try {
// If custom names are disabled, send empty name to trigger backend auto-generation
const nameToSend = openSettings.value.disableCustomAddressName ? "" : emailName.value;
const domainToSend = subdomainMode.value === "custom"
? `${customSubdomain.value.trim()}.${emailDomain.value}`
: emailDomain.value;
const res = await props.newAddressPath(
nameToSend,
emailDomain.value,
domainToSend,
cfToken.value,
enableRandomSubdomain.value
subdomainMode.value === "random"
);
jwt.value = res["jwt"];
addressPassword.value = res["password"] || '';
@@ -206,7 +210,7 @@ const canUseRandomSubdomain = computed(() => {
watch(canUseRandomSubdomain, (enabled) => {
if (!enabled) {
enableRandomSubdomain.value = false;
subdomainMode.value = "normal";
}
});
@@ -321,15 +325,26 @@ onMounted(async () => {
:options="domainsOptions" />
</n-input-group>
<n-form-item-row v-if="canUseRandomSubdomain">
<n-checkbox v-model:checked="enableRandomSubdomain">
{{ t('enableRandomSubdomain') }}
</n-checkbox>
<p style="margin: 8px 0 0; opacity: 0.75;">
{{ t('randomSubdomainTip') }}
</p>
<div style="width: 100%;">
<n-radio-group v-model:value="subdomainMode">
<n-space vertical>
<n-radio value="normal">{{ t('normalSubdomain') }}</n-radio>
<n-radio value="random">{{ t('enableRandomSubdomain') }}</n-radio>
<n-radio value="custom">{{ t('enableCustomSubdomain') }}</n-radio>
</n-space>
</n-radio-group>
<p v-if="subdomainMode === 'random'" style="margin: 8px 0 0; opacity: 0.75;">
{{ t('randomSubdomainTip') }}
</p>
<n-input-group v-if="subdomainMode === 'custom'" style="margin-top: 8px;">
<n-input v-model:value="customSubdomain" />
<n-input-group-label>.{{ emailDomain }}</n-input-group-label>
</n-input-group>
</div>
</n-form-item-row>
<Turnstile v-model:value="cfToken" />
<n-button type="primary" block secondary strong @click="newEmail" :loading="loading">
<n-button type="primary" block secondary strong @click="newEmail" :loading="loading"
:disabled="subdomainMode === 'custom' && !customSubdomain.trim()">
<template #icon>
<n-icon :component="NewLabelOutlined" />
</template>