release: v1.2.8 — 双域名保存确认与 clear-domains 恢复

启用双域名时以确认框提示锁死风险与恢复方式,不再强制校验 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

+14
View File
@@ -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
### 新增
+1
View File
@@ -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
+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)
}
+1 -1
View File
@@ -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
+4
View File
@@ -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 "$@"
+14
View File
@@ -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
+15 -4
View File
@@ -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)
+18
View File
@@ -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).
+10
View File
@@ -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.
+1
View File
@@ -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
+3 -1
View File
@@ -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
+14
View File
@@ -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
### 新增
+15 -4
View File
@@ -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 优选)
+18
View File
@@ -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)。
+10
View File
@@ -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)。
+1
View File
@@ -29,6 +29,7 @@ npm run dev
| `npm test` | 单元测试 |
| `npm run reset-password` | 重置密码(可选用户名) |
| `npm run slider` | 将登录验证重置为本地滑块 |
| `npm run clear-domains` | 清除数据库中的网站域 / 图片域配置(双域名锁死时恢复) |
| `npm run migrate` | 预览遗留目录迁移(加 `-- --apply` 执行) |
## 开发专用环境变量
+1 -1
View File
@@ -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 示例
+5
View File
@@ -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",
+5
View File
@@ -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": "首次使用,创建管理员账号",
+2 -2
View File
@@ -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
View File
@@ -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",
+60
View File
@@ -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.')