Go 1.27.1 (Gin+GORM) + Vue 3 文件快传服务: - 安全审计全部修复(docs/security-audit-2026-09-05.md): bcrypt 密码哈希与自动升级、presign 直传服务端大小/内容校验、 全局请求体上限、依赖升级(govulncheck 0 命中)、janitor 后台清理、 管理端审计动作落库、/admin CORS 收紧、通知内容白名单净化、 会话默认 7 天、限流缓存故障降级、robots.txt 端点等 - 前端:取件链接复制修复(不再重复拼接提取码)、markdown 净化器加固 - Redis 支持库号(FCB_REDIS_DB / redis://…/db URL) - 文档:docs/api/* 与 openapi.yaml 同步最新行为(robots.txt、 提码 5 位起、chunk 32MiB 上限、admin 审计动作等) 验证:gofmt/go vet/go test 全绿;二进制端到端冒烟通过
162 lines
6.6 KiB
TypeScript
162 lines
6.6 KiB
TypeScript
/**
|
||
* 站点配置 store:公共配置 + Logo/favicon(需求④)+ 背景/页脚/通知(⑤⑥⑦)+ 策略范围(⑧)。
|
||
* 远程 URL 默认值已删除:Logo/favicon 使用本地打包资源兜底(config 自定义仍优先)。
|
||
*/
|
||
import { defineStore } from 'pinia'
|
||
import { request } from '@/api/http'
|
||
import { paths } from '@/api/paths'
|
||
import { pick } from '@/utils/format'
|
||
import logoLocal from '@/assets/brand/logo.svg'
|
||
import faviconLocal from '@/assets/brand/favicon.png'
|
||
|
||
/** 需求④:默认 Logo(导航栏)与 favicon(浏览器标签)——本地打包资源,无远程依赖 */
|
||
export const DEFAULT_LOGO_URL = logoLocal
|
||
export const DEFAULT_FAVICON_URL = faviconLocal
|
||
export const DEFAULT_SITE_NAME = '文件快传'
|
||
|
||
export interface ConfigState {
|
||
loaded: boolean
|
||
loading: boolean
|
||
siteName: string
|
||
siteDomain: string
|
||
description: string
|
||
explain: string
|
||
uploadSize: number
|
||
allowedFileTypes: string[]
|
||
expireStyle: string[]
|
||
enableChunk: boolean
|
||
openUpload: boolean
|
||
notifyEnabled: boolean
|
||
notifyTitle: string
|
||
notifyContent: string
|
||
logoUrl: string
|
||
faviconUrl: string
|
||
backgroundUrl: string
|
||
footerText: string
|
||
footerBeian: string
|
||
maxFileSize: number
|
||
maxSaveSeconds: number
|
||
maxSaveCount: number
|
||
uploadCount: number
|
||
uploadMinute: number
|
||
}
|
||
|
||
function toBool(v: unknown, fallback = true): boolean {
|
||
if (v === undefined || v === null) return fallback
|
||
if (typeof v === 'boolean') return v
|
||
if (typeof v === 'number') return v !== 0
|
||
return String(v) !== '0' && String(v) !== 'false' && String(v) !== ''
|
||
}
|
||
|
||
function toList(v: unknown): string[] {
|
||
if (Array.isArray(v)) return v.map((x) => String(x).trim()).filter(Boolean)
|
||
if (typeof v === 'string') {
|
||
return v
|
||
.split(',')
|
||
.map((x) => x.trim())
|
||
.filter(Boolean)
|
||
}
|
||
return []
|
||
}
|
||
|
||
function toNum(v: unknown, fallback: number): number {
|
||
const n = Number(v)
|
||
return Number.isFinite(n) ? n : fallback
|
||
}
|
||
|
||
export const useConfigStore = defineStore('config', {
|
||
state: (): ConfigState => ({
|
||
loaded: false,
|
||
loading: false,
|
||
siteName: DEFAULT_SITE_NAME,
|
||
siteDomain: '',
|
||
description: '',
|
||
explain: '',
|
||
uploadSize: 10 * 1024 * 1024,
|
||
allowedFileTypes: [],
|
||
expireStyle: ['day', 'hour', 'minute', 'forever', 'count'],
|
||
enableChunk: false,
|
||
openUpload: true,
|
||
notifyEnabled: false,
|
||
notifyTitle: '',
|
||
notifyContent: '',
|
||
logoUrl: '',
|
||
faviconUrl: '',
|
||
backgroundUrl: '',
|
||
footerText: '',
|
||
footerBeian: '',
|
||
maxFileSize: 0,
|
||
maxSaveSeconds: 0,
|
||
maxSaveCount: 0,
|
||
uploadCount: 0,
|
||
uploadMinute: 0,
|
||
}),
|
||
getters: {
|
||
/** 需求④:自定义优先,本地打包资源兜底 */
|
||
displayLogoUrl: (s) => (s.logoUrl?.trim() ? s.logoUrl : DEFAULT_LOGO_URL),
|
||
displayFaviconUrl: (s) => (s.faviconUrl?.trim() ? s.faviconUrl : DEFAULT_FAVICON_URL),
|
||
displayName: (s) => (s.siteName?.trim() ? s.siteName : DEFAULT_SITE_NAME),
|
||
/** v3.1:分享链接基础地址——配置了对外域名用之,否则当前访问地址 */
|
||
shareLinkBase: (s): string => (s.siteDomain?.trim() ? s.siteDomain.trim().replace(/\/$/, '') : location.origin),
|
||
/** ⑧ 存储策略:max_file_size>0 时优先,否则回落 uploadSize */
|
||
effectiveMaxFileSize(): number {
|
||
return this.maxFileSize > 0 ? this.maxFileSize : this.uploadSize
|
||
},
|
||
},
|
||
actions: {
|
||
/** 拉取公共配置;失败静默使用默认值(未部署后端时前端仍可浏览) */
|
||
async load(): Promise<void> {
|
||
this.loading = true
|
||
try {
|
||
const raw = await request<Record<string, unknown>>(paths.publicConfig, { timeout: 8000 })
|
||
const cfg = (pick<Record<string, unknown>>(raw, ['config']) ?? raw) as Record<string, unknown>
|
||
this.siteName = String(pick<string>(cfg, ['name', 'site_name', 'siteName']) ?? DEFAULT_SITE_NAME)
|
||
// v3.1:站点对外域名(空=当前访问地址)
|
||
this.siteDomain = String(pick<string>(cfg, ['site_domain', 'siteDomain']) ?? '').trim()
|
||
this.description = String(pick<string>(cfg, ['description']) ?? '')
|
||
this.explain = String(pick<string>(cfg, ['explain', 'page_explain']) ?? '')
|
||
this.uploadSize = toNum(pick<unknown>(cfg, ['uploadSize', 'upload_size']), 10 * 1024 * 1024)
|
||
this.allowedFileTypes = toList(pick<unknown>(cfg, ['allowedFileTypes', 'allowed_file_types']))
|
||
const styles = toList(pick<unknown>(cfg, ['expireStyle', 'expire_style']))
|
||
if (styles.length) this.expireStyle = styles
|
||
this.enableChunk = toBool(pick<unknown>(cfg, ['enableChunk', 'enable_chunk']), false)
|
||
this.openUpload = toBool(pick<unknown>(cfg, ['openUpload', 'open_upload']), true)
|
||
this.notifyTitle = String(pick<string>(cfg, ['notify_title', 'notifyTitle']) ?? '')
|
||
this.notifyContent = String(pick<string>(cfg, ['notify_content', 'notifyContent']) ?? '')
|
||
// 需求⑦:通知开关(1/0)
|
||
this.notifyEnabled = toBool(pick<unknown>(cfg, ['notify_enabled', 'notifyEnabled']), false)
|
||
// 需求⑤:背景图
|
||
this.backgroundUrl = String(pick<string>(cfg, ['background_url', 'backgroundUrl']) ?? '').trim()
|
||
// 需求⑥:页脚
|
||
this.footerText = String(pick<string>(cfg, ['footer_text', 'footerText']) ?? '')
|
||
this.footerBeian = String(pick<string>(cfg, ['footer_beian', 'footerBeian']) ?? '')
|
||
// 需求⑧:保存策略与上传频率
|
||
this.maxFileSize = toNum(pick<unknown>(cfg, ['max_file_size', 'maxFileSize', 'maxFileSize']), 0)
|
||
this.maxSaveSeconds = toNum(pick<unknown>(cfg, ['max_save_seconds', 'maxSaveSeconds']), 0)
|
||
this.maxSaveCount = toNum(pick<unknown>(cfg, ['max_save_count', 'maxSaveCount']), 0)
|
||
this.uploadCount = toNum(pick<unknown>(cfg, ['uploadCount', 'upload_count']), 0)
|
||
this.uploadMinute = toNum(pick<unknown>(cfg, ['uploadMinute', 'upload_minute']), 0)
|
||
// 需求④:运行时优先读 config 的 logo_url/favicon_url,空值回退本地默认
|
||
this.logoUrl = String(pick<string>(cfg, ['logo_url', 'logoUrl']) ?? '').trim()
|
||
this.faviconUrl = String(pick<string>(cfg, ['favicon_url', 'faviconUrl']) ?? '').trim()
|
||
this.loaded = true
|
||
this.applyToDocument()
|
||
} catch {
|
||
// 保持默认值
|
||
} finally {
|
||
this.loading = false
|
||
}
|
||
},
|
||
/** 将站点名与 favicon 应用到 document(管理端保存配置后调用即可全站生效) */
|
||
applyToDocument(): void {
|
||
let link = document.querySelector<HTMLLinkElement>('link[rel="icon"]')
|
||
if (!link) {
|
||
link = document.createElement('link')
|
||
link.rel = 'icon'
|
||
document.head.appendChild(link)
|
||
}
|
||
link.href = this.displayFaviconUrl
|
||
},
|
||
},
|
||
})
|