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 全绿;二进制端到端冒烟通过
This commit is contained in:
2026-09-05 04:22:41 +08:00
commit 9686fe887a
173 changed files with 32455 additions and 0 deletions
+122
View File
@@ -0,0 +1,122 @@
<script setup lang="ts">
/**
* 有效期选择器(需求⑧:按 config 的 expireStyle / max_save_seconds / max_save_count 动态渲染):
* - 选项来自 config.expireStyle 白名单
* - style=count 时数值上限 = max_save_count0=不限),并展示提示
* - style=day/hour/minute 时按 max_save_seconds 折算上限并展示提示
* - 策略最终由服务端强制(超限 403/400 中文错误),这里仅体验层提示
*/
import { computed } from 'vue'
import { useI18n } from 'vue-i18n'
import { useConfigStore } from '@/stores/config'
import { EXPIRE_STYLES } from '@/utils/format'
const props = defineProps<{
value: number
style: string
compact?: boolean
}>()
const emit = defineEmits<{
'update:value': [v: number]
'update:style': [v: string]
}>()
const { t, te } = useI18n()
const config = useConfigStore()
const styles = computed(() => {
const list = config.expireStyle.length ? config.expireStyle : ['day', 'hour', 'minute', 'forever', 'count']
const known = EXPIRE_STYLES.filter((s) => list.includes(s.value))
const extra = list.filter((s) => !EXPIRE_STYLES.some((k) => k.value === s)).map((s) => ({ value: s, label: s }))
return [...known, ...extra]
})
const isForever = computed(() => props.style === 'forever')
const isCount = computed(() => props.style === 'count')
/** 需求⑧:count 上限 = max_save_count0=不限) */
const countMax = computed(() => (config.maxSaveCount > 0 ? config.maxSaveCount : 9999))
/** 需求⑧:时间类上限按 max_save_seconds 折算(0=仅默认 7 天兜底,不额外提示) */
const secondsMax = computed(() => (config.maxSaveSeconds > 0 ? config.maxSaveSeconds : 0))
const unitSeconds: Record<string, number> = { day: 86400, hour: 3600, minute: 60 }
/** 时间型选项的数值上限(按 max_save_seconds 折算;无限制时 9999 */
function maxFor(style: string): number {
const sec = secondsMax.value
const unit = unitSeconds[style]
if (!unit) return style === 'count' ? countMax.value : 9999
if (sec <= 0) return 9999
return Math.max(1, Math.floor(sec / unit))
}
/** 需求⑧:超出上限时给出体验层提示(不拦截提交,服务端强制) */
const overPolicy = computed(() => {
if (isCount.value && config.maxSaveCount > 0 && props.value > config.maxSaveCount) {
return t('expire.maxCountHint', { n: config.maxSaveCount })
}
if (!isForever.value && !isCount.value && secondsMax.value > 0) {
const max = maxFor(props.style)
if (props.value > max) {
const unitLabel = EXPIRE_STYLES.find((s) => s.value === props.style)?.label ?? props.style
return t('expire.maxSecondsHint', { value: `${max} ${unitLabel}` })
}
}
return ''
})
const unitLabel = computed(() => EXPIRE_STYLES.find((s) => s.value === props.style)?.label ?? props.style)
/** 需求⑩:已知样式走 i18n,未知样式原样展示 */
function styleLabel(value: string, raw: string): string {
const key = `expireStyle.${value}`
return te(key) ? t(key) : raw
}
function onStyleChange(e: Event): void {
const v = (e.target as HTMLSelectElement).value
emit('update:style', v)
if (v === 'count' && props.value > countMax.value) emit('update:value', 1)
if (v === 'minute' && props.value < 1) emit('update:value', 10)
}
</script>
<template>
<div class="field-row">
<label v-if="!isForever" class="expire-value" :class="{ compact }" :style="compact ? 'flex:0 0 110px' : ''">
<span class="field-sub">{{ t('expire.value') }}</span>
<input
class="input"
type="number"
:min="1"
:max="maxFor(style)"
:value="value"
@input="emit('update:value', Math.max(1, Number(($event.target as HTMLInputElement).value) || 1))"
/>
</label>
<label :style="isForever ? 'flex:1' : ''">
<span class="field-sub">{{ t('expire.label') }}</span>
<select class="select" :value="style" @change="onStyleChange">
<option v-for="s in styles" :key="s.value" :value="s.value">
{{ isForever && s.value === 'forever' ? t('expire.foreverOption') : s.value === 'count' ? t('expire.countOption') : styleLabel(s.value, s.label) }}
</option>
</select>
</label>
</div>
<p v-if="overPolicy" class="hint" style="color: var(--c-warn)">{{ overPolicy }}</p>
<p v-else-if="style === 'count'" class="hint">{{ t('expire.countHint') }}</p>
<p v-else-if="!isForever" class="hint">{{ t('expire.timeHint', { value, unit: unitLabel }) }}</p>
<p v-else class="hint">{{ t('expire.foreverHint') }}</p>
</template>
<style scoped>
.field-sub {
display: block;
font-size: 13px;
font-weight: 600;
color: var(--c-text-2);
margin-bottom: 6px;
}
</style>