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:
@@ -0,0 +1,122 @@
|
||||
<script setup lang="ts">
|
||||
/**
|
||||
* 有效期选择器(需求⑧:按 config 的 expireStyle / max_save_seconds / max_save_count 动态渲染):
|
||||
* - 选项来自 config.expireStyle 白名单
|
||||
* - style=count 时数值上限 = max_save_count(0=不限),并展示提示
|
||||
* - 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_count(0=不限) */
|
||||
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>
|
||||
Reference in New Issue
Block a user