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
+102
View File
@@ -0,0 +1,102 @@
<script setup lang="ts">
/** 文件拖拽/点选组件(需求⑧:allowedFileTypes 过滤文件选择器 accept;大小提示对齐 max_file_size */
import { computed, ref } from 'vue'
import { useI18n } from 'vue-i18n'
import { formatBytes } from '@/utils/format'
const props = defineProps<{
modelValue: File | null
/** 最大字节数,超限提示 */
maxSize?: number
disabled?: boolean
/** 需求⑧:类型白名单(星号表示不限制;含斜杠按 MIME 匹配,否则按扩展名) */
acceptTypes?: string[]
}>()
const emit = defineEmits<{
'update:modelValue': [f: File | null]
}>()
const { t } = useI18n()
const inputRef = ref<HTMLInputElement | null>(null)
const dragover = ref(false)
const error = ref('')
/** 需求⑧:把白名单映射为 <input accept> 值("*" → 空 = 不限制) */
const acceptAttr = computed(() => {
const rules = (props.acceptTypes ?? []).map((r) => r.trim()).filter(Boolean)
if (!rules.length || rules.some((r) => r === '*' || r === '*/*')) return ''
return rules
.map((r) => (r.includes('/') ? r : r.startsWith('.') ? r : `.${r.toLowerCase()}`))
.join(',')
})
const acceptHint = computed(() => {
const rules = (props.acceptTypes ?? []).filter((r) => r && r !== '*' && r !== '*/*')
return rules.length ? t('drop.typeHint', { types: rules.join(', ') }) : ''
})
function accept(f: File | null | undefined): void {
error.value = ''
if (!f) return
if (props.maxSize && f.size > props.maxSize) {
error.value = t('drop.tooLarge', { size: formatBytes(f.size), limit: formatBytes(props.maxSize) })
emit('update:modelValue', null)
return
}
emit('update:modelValue', f)
}
function onDrop(e: DragEvent): void {
dragover.value = false
if (props.disabled) return
accept(e.dataTransfer?.files?.[0])
}
function onPick(e: Event): void {
const input = e.target as HTMLInputElement
accept(input.files?.[0])
input.value = ''
}
</script>
<template>
<div>
<div
v-if="!modelValue"
class="dropzone"
:class="{ dragover, disabled }"
role="button"
tabindex="0"
:aria-label="t('drop.aria')"
@click="!disabled && inputRef?.click()"
@keydown.enter.prevent="!disabled && inputRef?.click()"
@dragover.prevent="dragover = true"
@dragleave="dragover = false"
@drop.prevent="onDrop"
>
<div class="dz-icon" aria-hidden="true">📦</div>
<div class="dz-main">{{ t('drop.zone') }}</div>
<div class="dz-sub">
<template v-if="maxSize">{{ t('drop.maxSize', { size: formatBytes(maxSize) }) }}</template>
<template v-else>{{ t('drop.noLimit') }}</template>
<template v-if="acceptHint"> · {{ acceptHint }}</template>
</div>
</div>
<div v-else class="file-chip">
<span aria-hidden="true">📄</span>
<span class="fc-name">{{ modelValue.name }}</span>
<span class="fc-size">{{ formatBytes(modelValue.size) }}</span>
<button class="fc-remove" type="button" :title="t('drop.remove')" @click="emit('update:modelValue', null)"></button>
</div>
<p v-if="error" class="hint" style="color: var(--c-danger)">{{ error }}</p>
<input ref="inputRef" type="file" hidden :accept="acceptAttr" @change="onPick" />
</div>
</template>
<style scoped>
.dropzone.disabled { opacity: 0.55; cursor: not-allowed; }
</style>