fix: restore security hardening compatibility and validation

Restore authorized rule reveal, explicit full HTTP capture and retention, video task business fields, and valid payment URLs. Add opt-in credential preservation for trusted recovery, fix frontend type contracts and async races, and eliminate PostgreSQL test fixture resource leaks. Document audit coverage and successful fmt and CI-scoped Clippy checks.
This commit is contained in:
elky
2026-09-07 21:14:27 +08:00
parent a5c3699ae9
commit a90d564931
191 changed files with 6785 additions and 1643 deletions
+13 -13
View File
@@ -46,6 +46,7 @@
</template>
<script setup lang="ts">
import type { TimeScatterChartData, TimeScatterPoint } from './types'
import { getI18nLocale, useI18n } from '@/i18n'
import { ref, onMounted, onUnmounted, watch, nextTick, computed } from 'vue'
import {
@@ -57,7 +58,6 @@ import {
Title,
Tooltip,
Legend,
type ChartData,
type ChartOptions,
type Plugin,
type Scale
@@ -84,7 +84,7 @@ ChartJS.register(
)
interface Props {
data: ChartData<'scatter'>
data: TimeScatterChartData
options?: ChartOptions<'scatter'>
height?: number
compressGaps?: boolean
@@ -117,13 +117,13 @@ interface GapInfo {
const chartRef = ref<HTMLCanvasElement>()
const { locale, t } = useI18n()
let chart: ChartJS<'scatter'> | null = null
let chart: ChartJS<'scatter', TimeScatterPoint[]> | null = null
const crosshairY = ref<number | null>(null)
const gapInfoList = ref<GapInfo[]>([])
interface PreparedRenderData {
chartData: ChartData<'scatter'>
chartData: TimeScatterChartData
gaps: GapInfo[]
}
@@ -141,7 +141,7 @@ const crosshairStats = computed<CrosshairStats | null>(() => {
let dsTotal = 0
for (const point of dataset.data) {
const p = point as { x: string; y: number }
const p = point
if (typeof p.y === 'number') {
dsTotal++
totalCount++
@@ -205,8 +205,8 @@ function toRealValue(displayValue: number): number {
}
// 压缩时间间隙的数据转换
function compressTimeGaps(data: ChartData<'scatter'>): {
data: ChartData<'scatter'>
function compressTimeGaps(data: TimeScatterChartData): {
data: TimeScatterChartData
gaps: GapInfo[]
timeMapping: Map<number, number> // 原始时间 -> 压缩后时间
} {
@@ -216,7 +216,7 @@ function compressTimeGaps(data: ChartData<'scatter'>): {
// 收集所有数据点的时间戳并排序
const allTimestamps: number[] = []
for (const dataset of data.datasets) {
for (const point of dataset.data as Array<{ x: string; y: number }>) {
for (const point of dataset.data) {
allTimestamps.push(new Date(point.x).getTime())
}
}
@@ -268,11 +268,11 @@ function compressTimeGaps(data: ChartData<'scatter'>): {
}
// 转换数据
const compressedData: ChartData<'scatter'> = {
const compressedData: TimeScatterChartData = {
...data,
datasets: data.datasets.map(dataset => ({
...dataset,
data: (dataset.data as Array<{ x: string; y: number }>).map(point => {
data: (dataset.data).map(point => {
const originalTs = new Date(point.x).getTime()
const compressedTs = timeMapping.get(originalTs) ?? originalTs
return {
@@ -288,12 +288,12 @@ function compressTimeGaps(data: ChartData<'scatter'>): {
}
// 转换数据点的 Y 值
function transformData(data: ChartData<'scatter'>): ChartData<'scatter'> {
function transformData(data: TimeScatterChartData): TimeScatterChartData {
return {
...data,
datasets: data.datasets.map(dataset => ({
...dataset,
data: (dataset.data as Array<{ x: string; y: number; _originalX?: string; _originalY?: number }>).map(point => ({
data: (dataset.data).map(point => ({
...point,
y: toDisplayValue(Math.min(point.y, 120)),
_originalY: point._originalY ?? point.y // 保存原始值用于 tooltip
@@ -547,7 +547,7 @@ function createChart() {
const { chartData, gaps } = prepareRenderData()
gapInfoList.value = gaps
chart = new ChartJS(chartRef.value, {
chart = new ChartJS<'scatter', TimeScatterPoint[]>(chartRef.value, {
type: 'scatter',
data: chartData,
options: {
+10
View File
@@ -0,0 +1,10 @@
import type { ChartData } from 'chart.js'
export interface TimeScatterPoint {
x: string | number
y: number
_originalX?: string | number
_originalY?: number
}
export type TimeScatterChartData = ChartData<'scatter', TimeScatterPoint[]>
@@ -21,7 +21,7 @@
: 'border-border hover:border-muted-foreground/40',
showManualInput ? 'opacity-0 pointer-events-none' : 'opacity-100',
]"
:inert="showManualInput ? '' : undefined"
:inert="showManualInput ? true : undefined"
:aria-hidden="showManualInput"
data-testid="json-import-file-panel"
@click="fileInputRef?.click()"
@@ -47,7 +47,7 @@
<div
class="space-y-1.5 transition-opacity duration-150"
:class="showManualInput ? 'opacity-100' : 'opacity-0 pointer-events-none'"
:inert="showManualInput ? undefined : ''"
:inert="showManualInput ? undefined : true"
:aria-hidden="!showManualInput"
data-testid="json-import-manual-panel"
>
@@ -30,7 +30,7 @@ describe('JsonImportInput', () => {
expect(filePanel?.getAttribute('aria-hidden')).toBe('false')
expect(filePanel?.hasAttribute('inert')).toBe(false)
expect(manualPanel?.getAttribute('aria-hidden')).toBe('true')
expect(manualPanel?.getAttribute('inert')).toBe('')
expect(manualPanel?.hasAttribute('inert')).toBe(true)
root.querySelector<HTMLButtonElement>('[data-testid="json-import-mode-toggle"]')?.click()
await nextTick()
@@ -38,7 +38,7 @@ describe('JsonImportInput', () => {
expect(root.querySelector('[data-testid="json-import-file-panel"]')).toBe(filePanel)
expect(root.querySelector('[data-testid="json-import-manual-panel"]')).toBe(manualPanel)
expect(filePanel?.getAttribute('aria-hidden')).toBe('true')
expect(filePanel?.getAttribute('inert')).toBe('')
expect(filePanel?.hasAttribute('inert')).toBe(true)
expect(manualPanel?.getAttribute('aria-hidden')).toBe('false')
expect(manualPanel?.hasAttribute('inert')).toBe(false)
@@ -53,7 +53,7 @@ describe('JsonImportInput', () => {
await nextTick()
expect(value.value).toBe('')
expect(filePanel?.hasAttribute('inert')).toBe(false)
expect(manualPanel?.getAttribute('inert')).toBe('')
expect(manualPanel?.hasAttribute('inert')).toBe(true)
app.unmount()
root.remove()
@@ -91,7 +91,7 @@ const chartOptions = computed(() => ({
scales: {
y: {
ticks: {
callback: (value: number) => `${value}s`
callback: (value: string | number) => `${value}s`
}
}
}
+2 -1
View File
@@ -10,6 +10,7 @@
</template>
<script setup lang="ts">
import type { ClassValue } from 'clsx'
import { computed } from 'vue'
import { cn } from '@/lib/utils'
@@ -17,7 +18,7 @@ interface Props {
variant?: 'default' | 'destructive' | 'outline' | 'secondary' | 'ghost' | 'link'
size?: 'default' | 'sm' | 'lg' | 'icon'
disabled?: boolean
class?: string
class?: ClassValue
type?: 'button' | 'submit' | 'reset'
}
+3 -2
View File
@@ -59,6 +59,7 @@
</template>
<script setup lang="ts">
import type { ClassValue } from 'clsx'
import { computed, useAttrs, ref } from 'vue'
import { Eye, EyeOff } from 'lucide-vue-next'
import { cn } from '@/lib/utils'
@@ -70,8 +71,8 @@ const emit = defineEmits<{
}>()
interface Props {
modelValue?: string | number
class?: string
modelValue?: string | number | null
class?: ClassValue
autocomplete?: string
/**
* 输入框尺寸
+2 -1
View File
@@ -5,11 +5,12 @@
</template>
<script setup lang="ts">
import type { ClassValue } from 'clsx'
import { computed } from 'vue'
import { cn } from '@/lib/utils'
interface Props {
class?: string
class?: ClassValue
}
const props = defineProps<Props>()
@@ -1,11 +1,12 @@
<script setup lang="ts">
import type { ClassValue } from 'clsx'
import { SelectTrigger as SelectTriggerPrimitive } from 'radix-vue'
import { ChevronDown } from 'lucide-vue-next'
import { cn } from '@/lib/utils'
import { computed } from 'vue'
interface Props {
class?: string
class?: ClassValue
disabled?: boolean
}
+2 -2
View File
@@ -4,7 +4,7 @@ import { SelectRoot as SelectRootPrimitive } from 'radix-vue'
interface Props {
defaultValue?: string
modelValue?: string
modelValue?: string | null
open?: boolean
defaultOpen?: boolean
dir?: 'ltr' | 'rtl'
@@ -35,7 +35,7 @@ const openProp = computed(() =>
// modelValue 未传入时不绑定,让 radix-vue 走 uncontrolled 模式
const modelValueProp = computed(() =>
props.modelValue !== undefined ? { modelValue: props.modelValue } : {}
props.modelValue !== undefined ? { modelValue: props.modelValue ?? '' } : {}
)
</script>
@@ -1,4 +1,5 @@
<script setup lang="ts">
import type { ClassValue } from 'clsx'
import { computed, nextTick, onBeforeUnmount, onMounted, ref, useAttrs, useSlots } from 'vue'
import { ArrowDown, ArrowUp, ArrowUpDown, ListFilter } from 'lucide-vue-next'
@@ -9,7 +10,7 @@ import TableHead from './table-head.vue'
type SortDirection = 'asc' | 'desc'
const props = withDefaults(defineProps<{
class?: string
class?: ClassValue
columnKey?: string
sortable?: boolean
activeKey?: string | null
+2 -1
View File
@@ -1,9 +1,10 @@
<script setup lang="ts">
import type { ClassValue } from 'clsx'
import { cn } from '@/lib/utils'
import { computed } from 'vue'
interface Props {
class?: string
class?: ClassValue
}
const props = defineProps<Props>()
+2 -1
View File
@@ -1,9 +1,10 @@
<script setup lang="ts">
import type { ClassValue } from 'clsx'
import { cn } from '@/lib/utils'
import { computed } from 'vue'
interface Props {
class?: string
class?: ClassValue
}
const props = defineProps<Props>()
+2 -1
View File
@@ -1,9 +1,10 @@
<script setup lang="ts">
import type { ClassValue } from 'clsx'
import { cn } from '@/lib/utils'
import { computed } from 'vue'
interface Props {
class?: string
class?: ClassValue
}
const props = defineProps<Props>()
+3 -2
View File
@@ -13,12 +13,13 @@
</template>
<script setup lang="ts">
import type { ClassValue } from 'clsx'
import { computed, ref, watch, onMounted, onUnmounted, nextTick, inject, type Ref } from 'vue'
import { cn } from '@/lib/utils'
import { useI18n } from '@/i18n'
interface Props {
class?: string
class?: ClassValue
}
const props = defineProps<Props>()
@@ -41,7 +42,7 @@ const activeTab = inject<Ref<string>>('activeTab')
// 检查是否有 grid 类(由外部传入)
const hasGridClass = computed(() => {
return props.class?.includes('grid')
return cn(props.class).includes('grid')
})
const listClass = computed(() => {