Files
FileShare/web/src/stores/config.ts
T
SKYMirror 9686fe887a FileCodeBox Go 重写版 v2.5.6(安全审计修复版)
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 全绿;二进制端到端冒烟通过
2026-09-05 04:22:41 +08:00

162 lines
6.6 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/**
* 站点配置 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
},
},
})