release: v1.1.3 — dual-domain separation and URL options
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
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
GitHub Release / release (push) Canceled after 0s
Add admin/image domain separation with host isolation middleware, optional hide-folder public URLs, GitHub release update check, and Lucky/Nginx deployment docs. Switch license to GPL-3.0-only.
This commit is contained in:
1 parent
55effa204e
commit
d8c1303c32
41 files changed
+2170
-299
No files matched your search
+137
-2
@@ -6,7 +6,9 @@ import {
|
||||
SETTINGS_API_UPLOAD_TOKEN,
|
||||
SETTINGS_AUTO_DELETE_DAYS,
|
||||
SETTINGS_AUTO_DELETE_ENABLED_AT,
|
||||
SETTINGS_HIDE_FOLDER_IN_URL,
|
||||
SETTINGS_IMAGE_BASE_URL,
|
||||
SETTINGS_SITE_BASE_URL,
|
||||
SETTINGS_WEBP_QUALITY,
|
||||
isAllowRegistration
|
||||
} from './db'
|
||||
@@ -47,10 +49,27 @@ export function getAdminSecret(event: H3Event): string {
|
||||
|
||||
export type SettingSource = 'env' | 'db' | 'none'
|
||||
|
||||
export function normalizeImageBaseUrl(raw: string): string {
|
||||
export function normalizeBaseUrl(raw: string): string {
|
||||
return raw.trim().replace(/\/$/, '')
|
||||
}
|
||||
|
||||
export function normalizeImageBaseUrl(raw: string): string {
|
||||
return normalizeBaseUrl(raw)
|
||||
}
|
||||
|
||||
export function normalizeSiteBaseUrl(raw: string): string {
|
||||
return normalizeBaseUrl(raw)
|
||||
}
|
||||
|
||||
export function hostnameFromBaseUrl(value: string): string | null {
|
||||
if (!value) return null
|
||||
try {
|
||||
return new URL(value).hostname.toLowerCase()
|
||||
} catch {
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
||||
export function getImageBaseUrlFromEnv(event: H3Event): string {
|
||||
return normalizeImageBaseUrl(
|
||||
getRuntimeEnv(event, 'IMAGE_BASE_URL', 'imageBaseUrl')
|
||||
@@ -73,6 +92,45 @@ export function getImageBaseUrlConfigured(event: H3Event): string {
|
||||
return getImageBaseUrlFromEnv(event)
|
||||
}
|
||||
|
||||
export function getSiteBaseUrlFromEnv(event: H3Event): string {
|
||||
return normalizeSiteBaseUrl(
|
||||
getRuntimeEnv(event, 'SITE_BASE_URL', 'siteBaseUrl')
|
||||
)
|
||||
}
|
||||
|
||||
export function isSiteBaseUrlEnvConfigured(event: H3Event): boolean {
|
||||
return getSiteBaseUrlFromEnv(event).length > 0
|
||||
}
|
||||
|
||||
export function getSiteBaseUrlSource(event: H3Event): SettingSource {
|
||||
if (getSetting(SETTINGS_SITE_BASE_URL) !== null) return 'db'
|
||||
if (isSiteBaseUrlEnvConfigured(event)) return 'env'
|
||||
return 'none'
|
||||
}
|
||||
|
||||
export function getSiteBaseUrlConfigured(event: H3Event): string {
|
||||
const dbRaw = getSetting(SETTINGS_SITE_BASE_URL)
|
||||
if (dbRaw !== null) return normalizeSiteBaseUrl(dbRaw)
|
||||
return getSiteBaseUrlFromEnv(event)
|
||||
}
|
||||
|
||||
export function getSiteBaseUrl(event: H3Event): string {
|
||||
const dbRaw = getSetting(SETTINGS_SITE_BASE_URL)
|
||||
if (dbRaw !== null) {
|
||||
const configured = normalizeSiteBaseUrl(dbRaw)
|
||||
if (configured) return configured
|
||||
} else {
|
||||
const fromEnv = getSiteBaseUrlFromEnv(event)
|
||||
if (fromEnv) return fromEnv
|
||||
}
|
||||
|
||||
const requestUrl = getRequestURL(event, {
|
||||
xForwardedHost: true,
|
||||
xForwardedProto: true
|
||||
})
|
||||
return requestUrl.origin
|
||||
}
|
||||
|
||||
export function getImageBaseUrl(event: H3Event): string {
|
||||
const dbRaw = getSetting(SETTINGS_IMAGE_BASE_URL)
|
||||
if (dbRaw !== null) {
|
||||
@@ -91,6 +149,36 @@ export function getImageBaseUrl(event: H3Event): string {
|
||||
return requestUrl.origin
|
||||
}
|
||||
|
||||
export function isHideFolderInUrlFromEnv(): boolean {
|
||||
try {
|
||||
return process.env.HIDE_FOLDER_IN_URL?.trim().toLowerCase() === 'true'
|
||||
} catch {
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
export function isHideFolderInUrlEnvConfigured(): boolean {
|
||||
try {
|
||||
const raw = process.env.HIDE_FOLDER_IN_URL?.trim().toLowerCase()
|
||||
return raw === 'true' || raw === 'false'
|
||||
} catch {
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
export function getHideFolderInUrlSource(): SettingSource {
|
||||
if (getSetting(SETTINGS_HIDE_FOLDER_IN_URL) !== null) return 'db'
|
||||
if (isHideFolderInUrlEnvConfigured()) return 'env'
|
||||
return 'none'
|
||||
}
|
||||
|
||||
/** 优先级:SQLite settings → 环境变量 → 默认 false */
|
||||
export function isHideFolderInUrl(_event?: H3Event): boolean {
|
||||
const dbRaw = getSetting(SETTINGS_HIDE_FOLDER_IN_URL)
|
||||
if (dbRaw !== null) return dbRaw === 'true'
|
||||
return isHideFolderInUrlFromEnv()
|
||||
}
|
||||
|
||||
export function getApiUploadTokenFromEnv(event: H3Event): string {
|
||||
return (
|
||||
getRuntimeEnv(event, 'API_UPLOAD_TOKEN', 'apiUploadToken')
|
||||
@@ -294,7 +382,7 @@ export function isValidRefererHost(host: string): boolean {
|
||||
return /^[a-z0-9]([a-z0-9-]*[a-z0-9])?(\.[a-z0-9]([a-z0-9-]*[a-z0-9])?)*$/i.test(host)
|
||||
}
|
||||
|
||||
export function isValidImageBaseUrl(value: string): boolean {
|
||||
export function isValidBaseUrl(value: string): boolean {
|
||||
if (!value) return true
|
||||
try {
|
||||
const url = new URL(value)
|
||||
@@ -304,13 +392,46 @@ export function isValidImageBaseUrl(value: string): boolean {
|
||||
}
|
||||
}
|
||||
|
||||
export function isValidImageBaseUrl(value: string): boolean {
|
||||
return isValidBaseUrl(value)
|
||||
}
|
||||
|
||||
export function isValidSiteBaseUrl(value: string): boolean {
|
||||
return isValidBaseUrl(value)
|
||||
}
|
||||
|
||||
export function validateDomainSeparationPair(
|
||||
siteBaseUrl: string,
|
||||
imageBaseUrl: string
|
||||
): string | null {
|
||||
if (!siteBaseUrl || !imageBaseUrl) {
|
||||
return '启用域名分离时需同时填写网站域名与图片域名'
|
||||
}
|
||||
if (!isValidSiteBaseUrl(siteBaseUrl) || !isValidImageBaseUrl(imageBaseUrl)) {
|
||||
return '域名需为 http(s) 地址,且末尾不加 /'
|
||||
}
|
||||
const siteHost = hostnameFromBaseUrl(siteBaseUrl)
|
||||
const imageHost = hostnameFromBaseUrl(imageBaseUrl)
|
||||
if (!siteHost || !imageHost) {
|
||||
return '域名格式无效'
|
||||
}
|
||||
if (siteHost === imageHost) {
|
||||
return '网站域名与图片域名不能使用相同主机名'
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
||||
export function getSettingsPayload(event: H3Event) {
|
||||
const config = useRuntimeConfig(event)
|
||||
const webpQuality = getWebpQuality(event)
|
||||
const allowedRefererHosts = getAllowedRefererHostsRaw(event)
|
||||
const siteBaseUrl = getSiteBaseUrl(event)
|
||||
const imageBaseUrl = getImageBaseUrl(event)
|
||||
const autoDeleteDays = getAutoDeleteDays(event)
|
||||
const appVersion = config.appVersion as string
|
||||
const domainSeparation = isDomainSeparationActive(event)
|
||||
const hideFolderInUrl = isHideFolderInUrl(event)
|
||||
const hideFolderInUrlSource = getHideFolderInUrlSource()
|
||||
|
||||
return {
|
||||
apiUploadToken: getApiUploadToken(event),
|
||||
@@ -321,9 +442,15 @@ export function getSettingsPayload(event: H3Event) {
|
||||
allowedRefererHosts,
|
||||
refererSource: getRefererSource(),
|
||||
refererEnvFallback: getRefererHostsFromEnv(),
|
||||
siteBaseUrl,
|
||||
siteBaseUrlConfigured: getSiteBaseUrlConfigured(event),
|
||||
siteBaseUrlSource: getSiteBaseUrlSource(event),
|
||||
imageBaseUrl,
|
||||
imageBaseUrlConfigured: getImageBaseUrlConfigured(event),
|
||||
imageBaseUrlSource: getImageBaseUrlSource(event),
|
||||
domainSeparation,
|
||||
hideFolderInUrl,
|
||||
hideFolderInUrlSource,
|
||||
autoDeleteDays,
|
||||
autoDeleteDaysSource: getAutoDeleteDaysSource(),
|
||||
autoDeleteEnvFallback: parseAutoDeleteDays(getAutoDeleteDaysFromEnv()) ?? 0,
|
||||
@@ -332,8 +459,16 @@ export function getSettingsPayload(event: H3Event) {
|
||||
env: {
|
||||
webpQuality,
|
||||
refererConfigured: allowedRefererHosts.length > 0,
|
||||
siteBaseUrl,
|
||||
imageBaseUrl,
|
||||
hideFolderInUrl,
|
||||
appVersion
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export function isDomainSeparationActive(event: H3Event): boolean {
|
||||
const siteHost = hostnameFromBaseUrl(getSiteBaseUrlConfigured(event))
|
||||
const imageHost = hostnameFromBaseUrl(getImageBaseUrlConfigured(event))
|
||||
return Boolean(siteHost && imageHost && siteHost !== imageHost)
|
||||
}
|
||||
Reference in new issue
Block a user