release: v1.2.2 — 双域名配置与 Host 隔离修复
GitHub Release / release (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
ci / ci (22, ubuntu-latest) (push) Canceled after 0s

修复设置页误清空管理域、配置与生效 URL 混淆及第三 Host 绕过隔离;加固 Settings API 与中间件,并更新文档站。
This commit is contained in:
O96u committed 2026-08-30 10:57:26 +08:00
1 parent 6c6766221b
commit c9c34515dd
36 files changed
+2578 -180

No files matched your search

+8 -5
View File
@@ -100,9 +100,12 @@ function storageIcon(storage: ActivityLogStorage | null) {
}
function publicPathFromKey(key: string) {
if (!hideFolderInUrl.value) return key
const slash = key.lastIndexOf('/')
return slash >= 0 ? key.slice(slash + 1) : key
if (hideFolderInUrl.value) {
const slash = key.lastIndexOf('/')
return slash >= 0 ? key.slice(slash + 1) : key
}
const prefix = 'images/'
return key.startsWith(prefix) ? key.slice(prefix.length) : key
}
function imageUrl(key: string) {
@@ -115,10 +118,10 @@ function imageUrl(key: string) {
async function loadImageLinkBase() {
try {
if (isAdmin.value) {
const data = await $fetch<{ imageBaseUrl: string, hideFolderInUrl: boolean }>('/api/settings', {
const data = await $fetch<{ effectiveImageBaseUrl: string, hideFolderInUrl: boolean }>('/api/settings', {
credentials: 'include'
})
imageLinkBase.value = data.imageBaseUrl
imageLinkBase.value = data.effectiveImageBaseUrl
hideFolderInUrl.value = data.hideFolderInUrl
return
}
+105 -24
View File
@@ -14,10 +14,17 @@ interface SettingsResponse {
imageBaseUrl: string
imageBaseUrlConfigured: string
imageBaseUrlSource: SettingSource
effectiveImageBaseUrl: string
siteBaseUrl: string
siteBaseUrlConfigured: string
siteBaseUrlSource: SettingSource
effectiveSiteBaseUrl: string
domainSeparation: boolean
runtime: {
currentOrigin: string
currentHost: string
hostRole: 'site' | 'image' | 'unknown' | 'single'
}
hideFolderInUrl: boolean
hideFolderInUrlSource: SettingSource
storageUseDatePath: boolean
@@ -46,6 +53,8 @@ const hideFolderInUrlDraft = ref(false)
const storageUseDatePathDraft = ref(true)
const allowRegistrationDraft = ref(false)
const domainSeparationDraft = ref(false)
const disableDomainSeparationOpen = ref(false)
const pendingDomainSeparationDisable = ref(false)
interface ReleaseCheckResponse {
currentVersion: string
@@ -60,8 +69,8 @@ const hasServerChanges = computed(() => {
if (!settings.value) return false
return refererDraft.value !== settings.value.allowedRefererHosts
|| domainSeparationDraft.value !== settings.value.domainSeparation
|| siteBaseUrlDraft.value !== (settings.value.domainSeparation ? settings.value.siteBaseUrlConfigured : '')
|| imageBaseUrlDraft.value !== settings.value.imageBaseUrlConfigured
|| siteBaseUrlDraft.value !== (settings.value.domainSeparation ? settings.value.siteBaseUrl : '')
|| imageBaseUrlDraft.value !== settings.value.imageBaseUrl
|| hideFolderInUrlDraft.value !== settings.value.hideFolderInUrl
|| storageUseDatePathDraft.value !== settings.value.storageUseDatePath
|| allowRegistrationDraft.value !== settings.value.allowRegistration
@@ -101,8 +110,8 @@ function errorStatus(error: unknown): number {
function applySettings(data: SettingsResponse) {
settings.value = data
refererDraft.value = data.allowedRefererHosts
siteBaseUrlDraft.value = data.domainSeparation ? data.siteBaseUrlConfigured : ''
imageBaseUrlDraft.value = data.imageBaseUrlConfigured
siteBaseUrlDraft.value = data.domainSeparation ? data.siteBaseUrl : ''
imageBaseUrlDraft.value = data.imageBaseUrl
hideFolderInUrlDraft.value = data.hideFolderInUrl
storageUseDatePathDraft.value = data.storageUseDatePath
allowRegistrationDraft.value = data.allowRegistration
@@ -130,25 +139,49 @@ function handlePatchError(error: unknown, fallback: string) {
toast.add({ title: fallback, color: 'error' })
}
function fillDetectedSiteOrigin() {
if (settings.value?.runtime.currentOrigin) {
siteBaseUrlDraft.value = settings.value.runtime.currentOrigin
}
}
function onDomainSeparationDraftChange(next: boolean | 'indeterminate') {
const enabled = next === true
domainSeparationDraft.value = enabled
if (!enabled) {
siteBaseUrlDraft.value = ''
if (!enabled && domainSeparationDraft.value) {
pendingDomainSeparationDisable.value = true
disableDomainSeparationOpen.value = true
return
}
if (!siteBaseUrlDraft.value && import.meta.client) {
siteBaseUrlDraft.value = window.location.origin
}
domainSeparationDraft.value = enabled
}
function confirmDisableDomainSeparation() {
domainSeparationDraft.value = false
siteBaseUrlDraft.value = ''
disableDomainSeparationOpen.value = false
pendingDomainSeparationDisable.value = false
}
function cancelDisableDomainSeparation() {
disableDomainSeparationOpen.value = false
pendingDomainSeparationDisable.value = false
}
async function saveServerSettings() {
if (!isAuthenticated.value || !hasServerChanges.value) return
if (domainSeparationDraft.value) {
if (!siteBaseUrlDraft.value.trim() || !imageBaseUrlDraft.value.trim()) {
toast.add({ title: t('settings.domainSeparationRequired'), color: 'error' })
return
}
}
savingServer.value = true
try {
await patchSettings({
allowedRefererHosts: refererDraft.value,
domainSeparation: domainSeparationDraft.value,
siteBaseUrl: domainSeparationDraft.value ? siteBaseUrlDraft.value : '',
imageBaseUrl: imageBaseUrlDraft.value,
hideFolderInUrl: hideFolderInUrlDraft.value,
@@ -375,6 +408,20 @@ watch(isAuthenticated, async (authed, prev) => {
</a>
</p>
<p
v-if="settings.runtime.currentOrigin"
class="text-xs text-muted"
>
{{ t('settings.runtimeDetected', { url: settings.runtime.currentOrigin }) }}
</p>
<p
v-if="domainSeparationDraft && settings.runtime.hostRole === 'unknown'"
class="rounded-lg border border-error/25 bg-error/5 px-3 py-2.5 text-xs leading-relaxed text-error"
>
{{ t('settings.hostRoleUnknown') }}
</p>
<div
v-if="domainSeparationDraft"
class="grid gap-4 sm:grid-cols-2"
@@ -393,17 +440,28 @@ watch(isAuthenticated, async (authed, prev) => {
<p class="text-xs leading-relaxed text-muted">
{{ t('settings.siteBaseUrlHint') }}
</p>
<UInput
v-model="siteBaseUrlDraft"
:placeholder="t('settings.siteBaseUrlPlaceholder')"
class="w-full font-mono text-sm"
/>
<div class="flex gap-2">
<UInput
v-model="siteBaseUrlDraft"
:placeholder="t('settings.siteBaseUrlPlaceholder')"
class="min-w-0 flex-1 font-mono text-sm"
/>
<UButton
v-if="settings.runtime.currentOrigin"
:label="t('settings.fillDetectedOrigin')"
color="neutral"
variant="outline"
size="sm"
class="shrink-0"
@click="fillDetectedSiteOrigin"
/>
</div>
<p
v-if="settings.siteBaseUrl"
v-if="settings.effectiveSiteBaseUrl"
class="truncate text-xs text-muted"
:title="settings.siteBaseUrl"
:title="settings.effectiveSiteBaseUrl"
>
{{ t('settings.siteBaseUrlActive', { url: settings.siteBaseUrl }) }}
{{ t('settings.siteBaseUrlActive', { url: settings.effectiveSiteBaseUrl }) }}
</p>
</div>
@@ -427,11 +485,11 @@ watch(isAuthenticated, async (authed, prev) => {
class="w-full font-mono text-sm"
/>
<p
v-if="settings.imageBaseUrl"
v-if="settings.effectiveImageBaseUrl"
class="truncate text-xs text-muted"
:title="settings.imageBaseUrl"
:title="settings.effectiveImageBaseUrl"
>
{{ t('settings.imageBaseUrlActive', { url: settings.imageBaseUrl }) }}
{{ t('settings.imageBaseUrlActive', { url: settings.effectiveImageBaseUrl }) }}
</p>
</div>
</div>
@@ -459,11 +517,11 @@ watch(isAuthenticated, async (authed, prev) => {
class="w-full font-mono text-sm"
/>
<p
v-if="settings.imageBaseUrl"
v-if="settings.effectiveImageBaseUrl"
class="truncate text-xs text-muted"
:title="settings.imageBaseUrl"
:title="settings.effectiveImageBaseUrl"
>
{{ t('settings.imageBaseUrlActive', { url: settings.imageBaseUrl }) }}
{{ t('settings.imageBaseUrlActive', { url: settings.effectiveImageBaseUrl }) }}
</p>
</div>
</div>
@@ -570,6 +628,29 @@ watch(isAuthenticated, async (authed, prev) => {
</div>
</section>
</AppShell>
<UModal
:open="disableDomainSeparationOpen"
:title="t('settings.disableDomainSeparationTitle')"
:description="t('settings.disableDomainSeparationDesc')"
@update:open="(v) => { if (!v) cancelDisableDomainSeparation() }"
>
<template #footer>
<div class="flex justify-end gap-2">
<UButton
:label="t('common.cancel')"
color="neutral"
variant="outline"
@click="cancelDisableDomainSeparation"
/>
<UButton
:label="t('settings.disableDomainSeparationConfirm')"
color="warning"
@click="confirmDisableDomainSeparation"
/>
</div>
</template>
</UModal>
</div>
</template>
+5 -4
View File
@@ -49,11 +49,12 @@ onMounted(async () => {
await router.replace('/')
return
}
if (import.meta.client) {
siteBaseUrl.value = window.location.origin
}
})
const detectedOriginPlaceholder = computed(() =>
import.meta.client ? window.location.origin : ''
)
async function submit() {
if (!canSubmit.value || loading.value) return
@@ -217,7 +218,7 @@ async function submit() {
</p>
<UInput
v-model="siteBaseUrl"
:placeholder="t('setup.siteBaseUrlPlaceholder')"
:placeholder="detectedOriginPlaceholder || t('setup.siteBaseUrlPlaceholder')"
class="w-full font-mono text-sm"
/>
</div>