release: v1.2.8 — 双域名保存确认与 clear-domains 恢复
GitHub Release / release (push) Canceled after 0s
ci / ci (22, ubuntu-latest) (push) Canceled after 0s
Publish Docker image / build (linux/amd64, ubuntu-latest, amd64) (push) Canceled after 0s
Publish Docker image / build (linux/arm64, ubuntu-24.04-arm, arm64) (push) Canceled after 0s
Publish Docker image / merge (push) Canceled after 0s

启用双域名时以确认框提示锁死风险与恢复方式,不再强制校验 Host。
新增 clear-domains 运维 CLI 与相关文档。
This commit is contained in:
O96u committed 2026-09-04 14:46:37 +08:00
1 parent 4789d19d06
commit b05d572221
24 files changed
+349 -20

No files matched your search

+64 -5
View File
@@ -1,6 +1,7 @@
<script setup lang="ts">
import type { SettingsTab } from '~/types/settings'
import { ADMIN_SETTINGS_TABS, USER_SETTINGS_TABS } from '~/types/settings'
import { willActivateDomainSeparation } from '~/utils/domain-separation'
type SettingSource = 'env' | 'db' | 'none'
type WebpQualitySource = 'env' | 'db' | 'default'
@@ -112,6 +113,7 @@ const capSecretDraft = ref('')
const domainSeparationDraft = ref(false)
const disableDomainSeparationOpen = ref(false)
const pendingDomainSeparationDisable = ref(false)
const domainSeparationSaveConfirmOpen = ref(false)
interface ReleaseCheckResponse {
currentVersion: string
@@ -138,6 +140,25 @@ const hasServerChanges = computed(() => {
|| capSecretDraft.value !== settings.value.capSecret
})
const domainSeparationWouldActivate = computed(() =>
willActivateDomainSeparation(
domainSeparationDraft.value,
siteBaseUrlDraft.value,
imageBaseUrlDraft.value
)
)
const domainSeparationFieldsChanged = computed(() => {
if (!settings.value) return false
return domainSeparationDraft.value !== settings.value.domainSeparation
|| siteBaseUrlDraft.value !== (settings.value.domainSeparation ? settings.value.siteBaseUrl : '')
|| imageBaseUrlDraft.value !== settings.value.imageBaseUrl
})
const needsDomainSeparationSaveConfirm = computed(() =>
domainSeparationWouldActivate.value && domainSeparationFieldsChanged.value
)
function sourceBadge(source: SettingSource | WebpQualitySource) {
switch (source) {
case 'env':
@@ -233,6 +254,18 @@ async function saveServerSettings() {
}
}
if (needsDomainSeparationSaveConfirm.value) {
domainSeparationSaveConfirmOpen.value = true
return
}
await performSaveServerSettings()
}
async function performSaveServerSettings() {
domainSeparationSaveConfirmOpen.value = false
if (!isAuthenticated.value || !hasServerChanges.value) return
savingServer.value = true
try {
await patchSettings({
@@ -496,12 +529,14 @@ watch(isAdmin, () => {
</a>
</p>
<p
<UAlert
v-if="domainSeparationDraft && settings.runtime.hostRole === 'unknown'"
class="rounded-xl border border-error/25 bg-error/5 px-4 py-3 text-xs leading-relaxed text-error"
>
{{ t('settings.hostRoleUnknown') }}
</p>
color="warning"
variant="subtle"
icon="i-lucide-triangle-alert"
:title="t('settings.hostRoleUnknown')"
class="mb-4"
/>
<SettingsSection
:title="t('settings.pathAndLink')"
@@ -876,6 +911,30 @@ watch(isAdmin, () => {
</div>
</template>
</UModal>
<UModal
:open="domainSeparationSaveConfirmOpen"
:title="t('domainSeparationConfirm.title')"
:description="t('domainSeparationConfirm.description')"
@update:open="(v) => { if (!v) domainSeparationSaveConfirmOpen = false }"
>
<template #footer>
<div class="flex justify-end gap-2">
<UButton
:label="t('common.cancel')"
color="neutral"
variant="outline"
@click="() => { domainSeparationSaveConfirmOpen = false }"
/>
<UButton
:label="t('domainSeparationConfirm.confirm')"
color="warning"
:loading="savingServer"
@click="performSaveServerSettings"
/>
</div>
</template>
</UModal>
</div>
</template>
+47 -1
View File
@@ -2,6 +2,7 @@
import logoLight from '~/assets/image/logo-light.png'
import logoDark from '~/assets/image/logo-dark.png'
import { isPasswordValid } from '~/utils/password-strength'
import { willActivateDomainSeparation } from '~/utils/domain-separation'
const route = useRoute()
const router = useRouter()
@@ -26,6 +27,15 @@ const domainSeparation = ref(false)
const siteBaseUrl = ref('')
const imageBaseUrl = ref('')
const loading = ref(false)
const domainSeparationConfirmOpen = ref(false)
const domainSeparationWouldActivate = computed(() =>
willActivateDomainSeparation(
domainSeparation.value,
siteBaseUrl.value,
imageBaseUrl.value
)
)
const canSubmit = computed(() => {
if (!username.value || !password.value || !confirmPassword.value) return false
@@ -33,7 +43,7 @@ const canSubmit = computed(() => {
if (!isPasswordValid(password.value)) return false
if (!AUTH_USERNAME_PATTERN.test(username.value.trim())) return false
if (domainSeparation.value) {
return Boolean(siteBaseUrl.value.trim() && imageBaseUrl.value.trim())
if (!siteBaseUrl.value.trim() || !imageBaseUrl.value.trim()) return false
}
return true
})
@@ -63,6 +73,18 @@ async function submit() {
return
}
if (domainSeparationWouldActivate.value) {
domainSeparationConfirmOpen.value = true
return
}
await performSubmit()
}
async function performSubmit() {
domainSeparationConfirmOpen.value = false
if (!canSubmit.value || loading.value) return
loading.value = true
try {
const payload = {
@@ -248,5 +270,29 @@ async function submit() {
/>
</form>
</div>
<UModal
:open="domainSeparationConfirmOpen"
:title="t('domainSeparationConfirm.title')"
:description="t('domainSeparationConfirm.description')"
@update:open="(v) => { if (!v) domainSeparationConfirmOpen = false }"
>
<template #footer>
<div class="flex justify-end gap-2">
<UButton
:label="t('common.cancel')"
color="neutral"
variant="outline"
@click="() => { domainSeparationConfirmOpen = false }"
/>
<UButton
:label="t('domainSeparationConfirm.confirm')"
color="warning"
:loading="loading"
@click="performSubmit"
/>
</div>
</template>
</UModal>
</div>
</template>
+24
View File
@@ -0,0 +1,24 @@
export function hostnameFromBaseUrl(value: string): string | null {
const trimmed = value.trim()
if (!trimmed) return null
try {
return new URL(trimmed).hostname.toLowerCase()
} catch {
return null
}
}
/** 勾选分离且网站域、图片域主机名不同 → 保存后将启用双域名中间件隔离 */
export function willActivateDomainSeparation(
enabled: boolean,
siteBaseUrl: string,
imageBaseUrl: string
): boolean {
if (!enabled) return false
const site = siteBaseUrl.trim()
const image = imageBaseUrl.trim()
if (!site || !image) return false
const siteHost = hostnameFromBaseUrl(site)
const imageHost = hostnameFromBaseUrl(image)
return Boolean(siteHost && imageHost && siteHost !== imageHost)
}