release: v1.2.8 — 双域名保存确认与 clear-domains 恢复
启用双域名时以确认框提示锁死风险与恢复方式,不再强制校验 Host。 新增 clear-domains 运维 CLI 与相关文档。
This commit is contained in:
1 parent
4789d19d06
commit
b05d572221
24 files changed
+349
-20
No files matched your search
@@ -4,6 +4,20 @@
|
||||
|
||||
## [Unreleased]
|
||||
|
||||
## [1.2.8] - 2026-09-04
|
||||
|
||||
### 改进
|
||||
|
||||
- 双域名:保存或初始化时弹出风险确认(说明可能锁死及 `clear-domains` 恢复方式),不再强制校验当前 Host
|
||||
|
||||
### 新增
|
||||
|
||||
- 运维 CLI `clear-domains`:清除数据库中的网站域 / 图片域配置(`docker exec pichost clear-domains`)
|
||||
|
||||
### 文档
|
||||
|
||||
- FAQ、双域名、反向代理文档补充锁死原因与恢复步骤
|
||||
|
||||
## [1.2.7] - 2026-09-03
|
||||
|
||||
### 新增
|
||||
|
||||
@@ -26,6 +26,7 @@ COPY --from=build /app/.output ./.output
|
||||
COPY server/cli/reset-password.mjs ./server/cli/reset-password.mjs
|
||||
COPY server/cli/migrate-to-single-images.mjs ./server/cli/migrate-to-single-images.mjs
|
||||
COPY server/cli/slider.mjs ./server/cli/slider.mjs
|
||||
COPY server/cli/clear-domains.mjs ./server/cli/clear-domains.mjs
|
||||
COPY docker-entrypoint.sh /docker-entrypoint.sh
|
||||
RUN chmod +x /docker-entrypoint.sh
|
||||
|
||||
|
||||
@@ -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
@@ -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>
|
||||
@@ -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)
|
||||
}
|
||||
+1
-1
@@ -10,7 +10,7 @@ services:
|
||||
- "6892:6892"
|
||||
# 首次访问 Web 引导创建管理员,无需预先配置环境变量
|
||||
# 可选:environment 中设置 API_UPLOAD_TOKEN、ALLOWED_REFERER_HOSTS 等
|
||||
# 运维:docker exec pichost reset-password | migrate [--apply] | slider
|
||||
# 运维:docker exec pichost reset-password | migrate [--apply] | slider | clear-domains [--site]
|
||||
volumes:
|
||||
# 冒号左边改成 NAS 上的实际存图目录(飞牛示例:/vol1/1000/pichost-data)
|
||||
- ./data:/data
|
||||
@@ -13,4 +13,8 @@ if [ "$1" = "slider" ]; then
|
||||
exec node /app/server/cli/slider.mjs "${@:2}"
|
||||
fi
|
||||
|
||||
if [ "$1" = "clear-domains" ]; then
|
||||
exec node /app/server/cli/clear-domains.mjs "${@:2}"
|
||||
fi
|
||||
|
||||
exec node /app/.output/server/index.mjs "$@"
|
||||
@@ -4,6 +4,20 @@ Version history and notable changes. The full log is also in the repo root [CHAN
|
||||
|
||||
---
|
||||
|
||||
## [1.2.8] — 2026-09-04
|
||||
|
||||
### Improved
|
||||
|
||||
- **Dual-domain**: confirm dialog on save/setup with lockout risks and `clear-domains` recovery; no longer blocks save based on current Host
|
||||
|
||||
### Added
|
||||
|
||||
- **Ops CLI**: `clear-domains` clears site/image URL settings from DB (`docker exec pichost clear-domains`)
|
||||
|
||||
### Docs
|
||||
|
||||
- FAQ and dual-domain / reverse-proxy guides updated with recovery steps
|
||||
|
||||
## [1.2.7] — 2026-09-03
|
||||
|
||||
### Added
|
||||
|
||||
@@ -69,10 +69,21 @@ This is independent of hyphenated or long domain names. Older versions did not b
|
||||
|
||||
1. Configure `server_name` only for the site and image hosts; add a **default server** to reject other Host values and bare IP access (see Nginx example above; complements app-layer blocking)
|
||||
2. With Cloudflare **orange-cloud** DNS, restrict the origin firewall to **[Cloudflare IP ranges](https://www.cloudflare.com/ips/)** so traffic cannot bypass the CDN
|
||||
3. Sign in only on the **site hostname** from Settings; do not mix `localhost`, IP, or unlisted domains
|
||||
4. Do **not** put Cloudflare Pages / Workers in front of PicHost as a **full-site reverse proxy** (`IMAGE_BASE_URL` may point at a CDN URL, but the app itself should run on Docker/VPS)
|
||||
5. If you must proxy at the edge, preserve the client **`Host` or `X-Forwarded-Host`** or host isolation will fail
|
||||
6. PicHost **cannot** run as-is on Pages/Workers (needs `node-server`, SQLite, `sharp`, local `data/`). Keep Docker/VPS; use orange-cloud DNS and optional R2 storage on CF
|
||||
3. **Before saving**, read the risk confirmation in Settings / setup; saving via IP, LAN, or a proxy that does not forward Host correctly may lock you out — see **Recovery when locked out** below
|
||||
4. Sign in only on the **site hostname** from Settings; do not mix `localhost`, IP, or unlisted domains
|
||||
5. Do **not** put Cloudflare Pages / Workers in front of PicHost as a **full-site reverse proxy** (`IMAGE_BASE_URL` may point at a CDN URL, but the app itself should run on Docker/VPS)
|
||||
6. If you must proxy at the edge, preserve the client **`Host` or `X-Forwarded-Host`** (Lucky/NPM “force hostname” must match PicHost settings) or isolation fails or you get locked out after save
|
||||
7. PicHost **cannot** run as-is on Pages/Workers (needs `node-server`, SQLite, `sharp`, local `data/`). Keep Docker/VPS; use orange-cloud DNS and optional R2 storage on CF
|
||||
|
||||
### Recovery when locked out
|
||||
|
||||
If IP / LAN access returns 404 after a bad save:
|
||||
|
||||
```bash
|
||||
docker exec pichost clear-domains
|
||||
```
|
||||
|
||||
Local: `npm run clear-domains`. Then reconfigure using the site hostname and verify proxy `Host` headers.
|
||||
|
||||
### Cloudflare orange cloud (incl. preferred edge IPs)
|
||||
|
||||
|
||||
@@ -19,6 +19,24 @@ Local: `npm run reset-password`. See [Quick start](./getting-started.md#forgot-p
|
||||
|
||||
Check the **Referer whitelist** (`ALLOWED_REFERER_HOSTS` or Settings). Referring sites must be listed; PicHost’s own hosts and dual-domain pair are allowed automatically.
|
||||
|
||||
## Admin 404 / locked out after dual-domain setup?
|
||||
|
||||
**Cause:** With dual-domain enabled, PicHost returns **404 for every request** whose Host is not the configured site or image hostname. Common triggers:
|
||||
|
||||
1. Saving dual-domain URLs while browsing via IP or an internal address
|
||||
2. Reverse proxy “force hostname” or wrong `Host` header so PicHost sees a different host than configured
|
||||
|
||||
**Recovery (when locked out):**
|
||||
|
||||
```bash
|
||||
docker exec pichost clear-domains
|
||||
# Site URL only: docker exec pichost clear-domains --site
|
||||
```
|
||||
|
||||
Local dev: `npm run clear-domains`. Then reopen admin via your previous IP/internal URL and reconfigure using the [dual-domain guide](./domain-separation.md#middleware-isolation-and-third-hosts).
|
||||
|
||||
**Prevention:** Read the risk confirmation when saving dual-domain settings; proxy must send `Host $host` and `X-Forwarded-Proto`. See [Dual-domain separation](./domain-separation.md#middleware-isolation-and-third-hosts).
|
||||
|
||||
## Why can admin load on pages.dev / workers.dev / IP with dual-domain?
|
||||
|
||||
**From v1.2.2:** when dual-domain is on, PicHost returns **404** for Host values **other than** the configured site and image hostnames (`localhost` / `127.0.0.1` exempt in development). See [Changelog](./changelog.md#1-2-2-2026-08-30).
|
||||
|
||||
@@ -70,6 +70,16 @@ A random password is printed. Change it after login.
|
||||
|
||||
Local dev: `npm run reset-password` or `npm run reset-password -- <username>`
|
||||
|
||||
## Dual-domain lockout
|
||||
|
||||
If dual-domain is enabled and IP / LAN access returns 404:
|
||||
|
||||
```bash
|
||||
docker exec pichost clear-domains
|
||||
```
|
||||
|
||||
Local: `npm run clear-domains`. See [FAQ — Admin 404 after dual-domain setup](./faq.md#admin-404--locked-out-after-dual-domain-setup).
|
||||
|
||||
## Upgrading to v1.2.0
|
||||
|
||||
If `data/` has legacy top-level folders next to `images` (e.g. `blog/`, `twikoo/`), read the [v1.2 migration guide](./migration.md) first.
|
||||
|
||||
@@ -29,6 +29,7 @@ Open `http://localhost:3000/setup` to create the admin account. Data directory d
|
||||
| `npm test` | Unit tests |
|
||||
| `npm run reset-password` | Reset password (optional username) |
|
||||
| `npm run slider` | Reset login verification to local slider |
|
||||
| `npm run clear-domains` | Clear site/image URL settings from DB (recover from dual-domain lockout) |
|
||||
| `npm run migrate` | Preview legacy dir migration (`-- --apply` to run) |
|
||||
|
||||
## Dev-only variables
|
||||
|
||||
@@ -57,12 +57,14 @@ See [Cloudflare deployment](./cloudflare-deployment.md).
|
||||
|
||||
## Origin hardening (dual-domain)
|
||||
|
||||
PicHost middleware does **not** reject unlisted Host values. Complement with:
|
||||
Complement PicHost middleware with:
|
||||
|
||||
1. **`server_name` allowlist** — site + image hosts only; `default_server` blocks bare IP and unknown Host
|
||||
2. **Do not expose 6892 on the public Internet** — only local reverse proxy; public 80/443 only
|
||||
3. **Orange cloud** — origin firewall limited to Cloudflare IP ranges
|
||||
|
||||
**From v1.2.2**, when dual-domain is active PicHost also returns **404 at the app layer** for Host values other than the configured site and image names (complementing, not replacing, a default server block). If misconfiguration locks you out, run `docker exec pichost clear-domains` — see [FAQ](./faq.md#admin-404--locked-out-after-dual-domain-setup).
|
||||
|
||||
## Caddy
|
||||
|
||||
```caddy
|
||||
|
||||
@@ -4,6 +4,20 @@ PicHost 版本历史与重要变更说明。完整记录亦见仓库根目录 [C
|
||||
|
||||
---
|
||||
|
||||
## [1.2.8] — 2026-09-04
|
||||
|
||||
### 改进
|
||||
|
||||
- **双域名**:保存 / 初始化时弹出风险确认(含锁死说明与 `clear-domains` 恢复方式),不再强制校验当前 Host
|
||||
|
||||
### 新增
|
||||
|
||||
- **运维 CLI**:`clear-domains` 清除数据库域名配置(`docker exec pichost clear-domains`)
|
||||
|
||||
### 文档
|
||||
|
||||
- FAQ 与双域名、反代文档补充恢复步骤
|
||||
|
||||
## [1.2.7] — 2026-09-03
|
||||
|
||||
### 新增
|
||||
|
||||
@@ -72,10 +72,21 @@ PicHost 根据请求 **Host**(反代会转发为 `X-Forwarded-Host`,应用
|
||||
|
||||
1. 反代层仅为网站域、图片域配置 `server_name`,并增加 **default server** 拒绝其它 Host 与裸 IP(见上方 Nginx 示例,与应用层拦截互补)
|
||||
2. 使用 Cloudflare **橙云** 时,源站防火墙 **仅放行 [Cloudflare IP 段](https://www.cloudflare.com/ips/)**,避免绕过 CDN 直连源站
|
||||
3. **仅使用** 设置中的网站域名登录后台,不要用 `localhost`、IP 或未配置的域名混用
|
||||
4. **不要** 用 Cloudflare Pages / Workers 对 PicHost **整站反代**(会多出 `pages.dev` / `workers.dev` 等第三入口;`IMAGE_BASE_URL` 可填 CDN 公网地址,但应用本体应 Docker/VPS 部署)
|
||||
5. 若必须在边缘做反代,须保留客户端 **`Host` 或 `X-Forwarded-Host`**,否则 Host 隔离会失效
|
||||
6. PicHost **不能** 作为 Node 应用直接部署到 Pages/Workers(依赖 `node-server`、SQLite、`sharp` 与本地 `data/`);应用请继续 Docker/VPS 部署,CF 侧用橙云 DNS + 可选 R2 存储即可
|
||||
3. **保存前**阅读设置 / 初始化页的风险确认;若通过 IP、内网或未正确传递 Host 的反代保存,可能锁死后台;恢复见下方「已锁死时的恢复」
|
||||
4. **仅使用** 设置中的网站域名登录后台,不要用 `localhost`、IP 或未配置的域名混用
|
||||
5. **不要** 用 Cloudflare Pages / Workers 对 PicHost **整站反代**(会多出 `pages.dev` / `workers.dev` 等第三入口;`IMAGE_BASE_URL` 可填 CDN 公网地址,但应用本体应 Docker/VPS 部署)
|
||||
6. 若必须在边缘做反代,须保留客户端 **`Host` 或 `X-Forwarded-Host`**(Lucky/NPM「强制域名」须与 PicHost 配置一致),否则 Host 隔离会失效或保存后被锁死
|
||||
7. PicHost **不能** 作为 Node 应用直接部署到 Pages/Workers(依赖 `node-server`、SQLite、`sharp` 与本地 `data/`);应用请继续 Docker/VPS 部署,CF 侧用橙云 DNS + 可选 R2 存储即可
|
||||
|
||||
### 已锁死时的恢复
|
||||
|
||||
若已误保存导致 IP / 内网地址全站 404:
|
||||
|
||||
```bash
|
||||
docker exec pichost clear-domains
|
||||
```
|
||||
|
||||
本地:`npm run clear-domains`。清除后请用网站域名重新配置,并确认反代 `Host` 头正确。
|
||||
|
||||
### Cloudflare 橙云(含 DNS 优选)
|
||||
|
||||
|
||||
@@ -20,6 +20,24 @@ docker exec pichost reset-password 用户名
|
||||
|
||||
检查 **防盗链白名单**(`ALLOWED_REFERER_HOSTS` 或设置页)。引用图片的站点域名需在白名单中;PicHost 自身域名与双域名场景下的网站域、图片域会自动放行。
|
||||
|
||||
## 双域名配置后后台 404 / 进不去?
|
||||
|
||||
**原因:** 启用双域名后,PicHost 会对**未配置的网站域、图片域之外**的 Host 全站返回 404。常见触发方式:
|
||||
|
||||
1. 通过 IP 或内网地址打开设置,保存了公网网站域 / 图片域
|
||||
2. 反代「强制域名」或 Host 头配置错误,PicHost 收到的 Host 与设置中的网站域不一致
|
||||
|
||||
**恢复(已锁死时):**
|
||||
|
||||
```bash
|
||||
docker exec pichost clear-domains
|
||||
# 仅清除网站域:docker exec pichost clear-domains --site
|
||||
```
|
||||
|
||||
本地开发:`npm run clear-domains`。执行后用原来的 IP / 内网地址刷新即可重新进入后台,再按文档正确配置双域名。
|
||||
|
||||
**预防:** 保存双域名时注意确认框中的风险提示;反代须 `proxy_set_header Host $host;` 并保留 `X-Forwarded-Proto`。详见 [双域名分离](./domain-separation.md#中间件隔离与第三-host)。
|
||||
|
||||
## 双域名下为什么用 pages.dev / workers.dev / IP 也能进后台?
|
||||
|
||||
**v1.2.2 起:** 双域名开启时,PicHost 中间件会对**未配置的网站域、图片域之外**的 Host 返回 **404**(开发环境 `localhost` / `127.0.0.1` 例外)。详见 [更新日志](./changelog.md#1-2-2-2026-08-30)。
|
||||
|
||||
@@ -73,6 +73,16 @@ docker exec pichost reset-password 用户名
|
||||
|
||||
本地开发:`npm run reset-password` 或 `npm run reset-password -- 用户名`
|
||||
|
||||
## 双域名配置锁死
|
||||
|
||||
若启用双域名后通过 IP / 内网地址无法进入后台:
|
||||
|
||||
```bash
|
||||
docker exec pichost clear-domains
|
||||
```
|
||||
|
||||
本地:`npm run clear-domains`。详见 [常见问题 — 双域名配置后后台 404](./faq.md#双域名配置后后台-404-进不去)。
|
||||
|
||||
## 升级到 v1.2.0
|
||||
|
||||
若 `data/` 下存在与 `images` 并列的遗留目录(如 `blog/`、`twikoo/`),升级前请阅读 [v1.2 迁移指南](./migration.md)。
|
||||
|
||||
@@ -29,6 +29,7 @@ npm run dev
|
||||
| `npm test` | 单元测试 |
|
||||
| `npm run reset-password` | 重置密码(可选用户名) |
|
||||
| `npm run slider` | 将登录验证重置为本地滑块 |
|
||||
| `npm run clear-domains` | 清除数据库中的网站域 / 图片域配置(双域名锁死时恢复) |
|
||||
| `npm run migrate` | 预览遗留目录迁移(加 `-- --apply` 执行) |
|
||||
|
||||
## 开发专用环境变量
|
||||
|
||||
@@ -63,7 +63,7 @@ IMAGE_BASE_URL=https://pic.example.com
|
||||
2. **禁止公网暴露 6892** — 仅本机或内网反代访问;公网只开 80/443
|
||||
3. **橙云时限制源站来源 IP** — 仅 Cloudflare 回源 IP 段
|
||||
|
||||
双域名场景下 PicHost **不会** 拒绝未配置的 Host;上述措施由运维层补齐。
|
||||
**v1.2.2 起**,双域名开启时 PicHost 应用层也会对**未配置的网站域、图片域之外**的 Host 全站返回 404(与反代 default server 互补,而非替代)。误配置导致锁死时执行 `docker exec pichost clear-domains` 恢复,见 [常见问题](./faq.md#双域名配置后后台-404-进不去)。
|
||||
|
||||
## Caddy 示例
|
||||
|
||||
|
||||
@@ -84,6 +84,11 @@
|
||||
"invalidPassword": "Password must be at least 8 characters"
|
||||
}
|
||||
},
|
||||
"domainSeparationConfirm": {
|
||||
"title": "Save dual-domain settings?",
|
||||
"description": "After saving, only the site URL can serve the admin UI and API, and only the image URL can serve public image links. Access via IP, an unlisted Host, or a reverse proxy that does not forward Host correctly may lock you out of the admin UI.\n\nIf locked out, run docker exec pichost clear-domains (local dev: npm run clear-domains), or disable domain separation in Settings when you can access it again.\n\nSave anyway?",
|
||||
"confirm": "Save anyway"
|
||||
},
|
||||
"setup": {
|
||||
"migrateTitle": "Migrate to username/password",
|
||||
"initTitle": "First run — create admin account",
|
||||
|
||||
@@ -84,6 +84,11 @@
|
||||
"invalidPassword": "密码至少 8 位"
|
||||
}
|
||||
},
|
||||
"domainSeparationConfirm": {
|
||||
"title": "确认保存双域名配置?",
|
||||
"description": "保存后,仅网站域名可访问管理后台与 API,仅图片域名可提供图片直链。若通过 IP、未配置的 Host,或反代未正确传递 Host 访问,可能无法打开后台。\n\n若发生锁死,可执行 docker exec pichost clear-domains(本地开发:npm run clear-domains)清除域名配置,或在能访问时于设置中关闭「域名分离」。\n\n确定仍要保存吗?",
|
||||
"confirm": "确认保存"
|
||||
},
|
||||
"setup": {
|
||||
"migrateTitle": "迁移到账号密码登录",
|
||||
"initTitle": "首次使用,创建管理员账号",
|
||||
|
||||
Generated
+2
-2
@@ -1,12 +1,12 @@
|
||||
{
|
||||
"name": "pichost",
|
||||
"version": "1.2.7",
|
||||
"version": "1.2.8",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "pichost",
|
||||
"version": "1.2.7",
|
||||
"version": "1.2.8",
|
||||
"hasInstallScript": true,
|
||||
"license": "GPL-3.0-only",
|
||||
"dependencies": {
|
||||
|
||||
+2
-1
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "pichost",
|
||||
"version": "1.2.7",
|
||||
"version": "1.2.8",
|
||||
"description": "Self-hosted image hosting for your NAS / home server — Nuxt admin UI, local disk or S3-compatible storage, server-side WebP compression, referer protection, Twikoo/EasyImage compatible.",
|
||||
"homepage": "https://github.com/O96u/PicHost#readme",
|
||||
"repository": {
|
||||
@@ -36,6 +36,7 @@
|
||||
"reset-password": "node server/cli/reset-password.mjs",
|
||||
"migrate": "node server/cli/migrate-to-single-images.mjs",
|
||||
"slider": "node server/cli/slider.mjs",
|
||||
"clear-domains": "node server/cli/clear-domains.mjs",
|
||||
"docs:prepare": "node docs-site/copy-screenshots.mjs",
|
||||
"docs:dev": "npm run docs:prepare && vitepress dev docs-site",
|
||||
"docs:build": "npm run docs:prepare && vitepress build docs-site",
|
||||
|
||||
@@ -0,0 +1,60 @@
|
||||
import { mkdirSync } from 'node:fs'
|
||||
import { join } from 'node:path'
|
||||
import { DatabaseSync } from 'node:sqlite'
|
||||
|
||||
const SITE_KEY = 'site_base_url'
|
||||
const IMAGE_KEY = 'image_base_url'
|
||||
|
||||
const args = process.argv.slice(2)
|
||||
const siteOnly = args.includes('--site')
|
||||
const imageOnly = args.includes('--image')
|
||||
|
||||
if (args.some(arg => arg.startsWith('--') && arg !== '--site' && arg !== '--image')) {
|
||||
console.error('Usage: clear-domains [--site] [--image]')
|
||||
console.error(' (no flags) Clear both site and image URLs from settings')
|
||||
console.error(' --site Clear site URL only')
|
||||
console.error(' --image Clear image URL only')
|
||||
process.exit(1)
|
||||
}
|
||||
|
||||
if (siteOnly && imageOnly) {
|
||||
console.error('Usage: specify at most one of --site or --image')
|
||||
process.exit(1)
|
||||
}
|
||||
|
||||
const dataDir = process.env.DATA_DIR || join(process.cwd(), 'data')
|
||||
mkdirSync(dataDir, { recursive: true })
|
||||
const db = new DatabaseSync(join(dataDir, 'pichost.db'))
|
||||
|
||||
function readSetting(key) {
|
||||
const row = db.prepare(`
|
||||
SELECT value
|
||||
FROM settings
|
||||
WHERE key = ?
|
||||
`).get(key)
|
||||
return row?.value ?? null
|
||||
}
|
||||
|
||||
const beforeSite = readSetting(SITE_KEY)
|
||||
const beforeImage = readSetting(IMAGE_KEY)
|
||||
|
||||
const keysToClear = siteOnly
|
||||
? [SITE_KEY]
|
||||
: imageOnly
|
||||
? [IMAGE_KEY]
|
||||
: [SITE_KEY, IMAGE_KEY]
|
||||
|
||||
for (const key of keysToClear) {
|
||||
db.prepare('DELETE FROM settings WHERE key = ?').run(key)
|
||||
}
|
||||
|
||||
db.close()
|
||||
|
||||
console.log('[PicHost] Domain settings cleared from database.')
|
||||
if (keysToClear.includes(SITE_KEY)) {
|
||||
console.log(` site_base_url: ${beforeSite ?? '(unset)'} → (removed)`)
|
||||
}
|
||||
if (keysToClear.includes(IMAGE_KEY)) {
|
||||
console.log(` image_base_url: ${beforeImage ?? '(unset)'} → (removed)`)
|
||||
}
|
||||
console.log('[PicHost] Restart is not required. Refresh the admin page or use your previous access URL.')
|
||||
Reference in new issue
Block a user