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
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:
1 parent
6c6766221b
commit
c9c34515dd
36 files changed
+2578
-180
No files matched your search
+8
-5
@@ -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
@@ -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
@@ -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>
|
||||
|
||||
Reference in new issue
Block a user