mirror of
https://github.com/lukaszraczylo/gohoarder.git
synced 2026-06-10 23:29:22 +00:00
chore(schema): migrate to GORM V2 with multi-database support
- [x] Implement GORM V2 metadata store with SQLite, PostgreSQL, and MySQL support - [x] Add database migration system using gormigrate for schema versioning - [x] Create migration CLI tool with support for migrate, rollback, and status commands - [x] Add Docker support for migration container (Dockerfile.migrate) - [x] Implement automatic partition management for PostgreSQL time-series tables - [x] Add background aggregation worker for download statistics - [x] Support connection pooling configuration (max_open_conns, max_idle_conns, conn_max_lifetime) - [x] Add blocking mechanism based on vulnerability thresholds in stats and handlers - [x] Update Helm charts with migration init containers and multi-database configuration - [x] Replace deprecated SQLite store with optimized GORM implementation - [x] Add comprehensive integration tests for MySQL and PostgreSQL - [x] Update frontend to display blocked packages and storage utilization - [x] Add goreleaser configuration for migrate binary and container image - [x] Update configuration examples with database backend options and recommendations
This commit is contained in:
@@ -128,6 +128,7 @@
|
||||
:counts="version.vulnerabilities.counts"
|
||||
:total="version.vulnerabilities.total"
|
||||
:scannedAt="version.vulnerabilities.scannedAt"
|
||||
:isBlocked="version.vulnerabilities.isBlocked"
|
||||
@click="showVulnerabilityDetails(group.registry, group.name, version.version)"
|
||||
/>
|
||||
</div>
|
||||
|
||||
@@ -29,11 +29,26 @@
|
||||
</p>
|
||||
<p class="text-sm text-gray-600">Total Packages</p>
|
||||
</div>
|
||||
<div class="text-center p-6 bg-gray-50 rounded-lg">
|
||||
<p class="text-4xl font-bold text-blue-600 mb-2">
|
||||
{{ formatBytes(stats?.total_size || 0) }}
|
||||
</p>
|
||||
<p class="text-sm text-gray-600">Total Storage Used</p>
|
||||
<div class="p-6 bg-gray-50 rounded-lg">
|
||||
<div class="text-center mb-3">
|
||||
<p class="text-2xl font-bold text-blue-600">
|
||||
{{ formatBytes(stats?.total_size || 0) }} / {{ formatBytes(stats?.max_cache_size || 0) }}
|
||||
</p>
|
||||
<p class="text-sm text-gray-600 mt-1">Storage Used</p>
|
||||
</div>
|
||||
<div class="w-full bg-gray-200 rounded-full h-3 overflow-hidden">
|
||||
<div
|
||||
class="h-full bg-blue-600 rounded-full transition-all duration-300"
|
||||
:style="{ width: storagePercentage + '%' }"
|
||||
:class="{
|
||||
'bg-green-600': storagePercentage < 50,
|
||||
'bg-yellow-600': storagePercentage >= 50 && storagePercentage < 80,
|
||||
'bg-orange-600': storagePercentage >= 80 && storagePercentage < 90,
|
||||
'bg-red-600': storagePercentage >= 90
|
||||
}"
|
||||
></div>
|
||||
</div>
|
||||
<p class="text-xs text-gray-500 text-center mt-1">{{ storagePercentage.toFixed(1) }}% used</p>
|
||||
</div>
|
||||
<div class="text-center p-6 bg-gray-50 rounded-lg">
|
||||
<p class="text-4xl font-bold text-green-600 mb-2">
|
||||
@@ -51,7 +66,7 @@
|
||||
<h3 class="text-xl font-semibold text-gray-900 mb-6">
|
||||
<i class="fas fa-shield-alt mr-2"></i>Security Scanning
|
||||
</h3>
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 gap-6">
|
||||
<div class="grid grid-cols-1 md:grid-cols-3 gap-6">
|
||||
<div class="flex items-center justify-between p-6 bg-green-50 rounded-lg border border-green-200">
|
||||
<div>
|
||||
<p class="text-3xl font-bold text-green-600">
|
||||
@@ -63,11 +78,11 @@
|
||||
</div>
|
||||
<div
|
||||
@click="showVulnerablePackages"
|
||||
class="flex items-center justify-between p-6 bg-red-50 rounded-lg border border-red-200 cursor-pointer hover:bg-red-100 transition-colors"
|
||||
class="flex items-center justify-between p-6 bg-orange-50 rounded-lg border border-orange-200 cursor-pointer hover:bg-orange-100 transition-colors"
|
||||
:class="{ 'opacity-50': (stats?.vulnerable_packages || 0) === 0 }"
|
||||
>
|
||||
<div>
|
||||
<p class="text-3xl font-bold text-red-600">
|
||||
<p class="text-3xl font-bold text-orange-600">
|
||||
{{ formatNumber(stats?.vulnerable_packages || 0) }}
|
||||
</p>
|
||||
<p class="text-sm text-gray-600 mt-1">
|
||||
@@ -75,7 +90,23 @@
|
||||
<span v-if="(stats?.vulnerable_packages || 0) > 0" class="text-xs ml-1">(click to view)</span>
|
||||
</p>
|
||||
</div>
|
||||
<i class="fas fa-exclamation-triangle text-5xl text-red-400"></i>
|
||||
<i class="fas fa-exclamation-triangle text-5xl text-orange-400"></i>
|
||||
</div>
|
||||
<div
|
||||
@click="showBlockedPackages"
|
||||
class="flex items-center justify-between p-6 bg-red-50 rounded-lg border border-red-200 cursor-pointer hover:bg-red-100 transition-colors"
|
||||
:class="{ 'opacity-50': (stats?.blocked_packages || 0) === 0 }"
|
||||
>
|
||||
<div>
|
||||
<p class="text-3xl font-bold text-red-600">
|
||||
{{ formatNumber(stats?.blocked_packages || 0) }}
|
||||
</p>
|
||||
<p class="text-sm text-gray-600 mt-1">
|
||||
Blocked Packages
|
||||
<span v-if="(stats?.blocked_packages || 0) > 0" class="text-xs ml-1">(click to view)</span>
|
||||
</p>
|
||||
</div>
|
||||
<i class="fas fa-hand text-5xl text-red-400"></i>
|
||||
</div>
|
||||
</div>
|
||||
</CardContent>
|
||||
@@ -141,6 +172,14 @@ function showVulnerablePackages() {
|
||||
router.push('/vulnerable-packages')
|
||||
}
|
||||
|
||||
function showBlockedPackages() {
|
||||
if ((stats.value?.blocked_packages || 0) === 0) {
|
||||
return
|
||||
}
|
||||
|
||||
router.push('/blocked-packages')
|
||||
}
|
||||
|
||||
// Registry configuration for icons and colors
|
||||
const registryConfig: Record<string, {label: string, icon: string, color: string}> = {
|
||||
npm: {
|
||||
@@ -180,6 +219,12 @@ const registries = computed(() => {
|
||||
})
|
||||
})
|
||||
|
||||
const storagePercentage = computed(() => {
|
||||
const totalSize = stats.value?.total_size || 0
|
||||
const maxSize = stats.value?.max_cache_size || 1
|
||||
return (totalSize / maxSize) * 100
|
||||
})
|
||||
|
||||
function formatNumber(num: number): string {
|
||||
return new Intl.NumberFormat().format(num)
|
||||
}
|
||||
|
||||
@@ -1,5 +1,15 @@
|
||||
<template>
|
||||
<div class="flex items-center gap-2">
|
||||
<!-- Blocked Icon (if package exceeds thresholds) -->
|
||||
<span
|
||||
v-if="isBlocked"
|
||||
class="inline-flex items-center px-2.5 py-1 rounded-md text-xs font-medium bg-red-600 text-white border border-red-700"
|
||||
title="Download blocked - exceeds vulnerability thresholds"
|
||||
>
|
||||
<i class="fas fa-hand mr-1"></i>
|
||||
BLOCKED
|
||||
</span>
|
||||
|
||||
<!-- Critical Vulnerabilities -->
|
||||
<button
|
||||
v-if="counts.critical > 0"
|
||||
@@ -89,12 +99,14 @@ interface Props {
|
||||
counts?: VulnerabilityCounts
|
||||
total?: number
|
||||
scannedAt?: string // ISO 8601 timestamp
|
||||
isBlocked?: boolean // Whether download is blocked due to vulnerabilities
|
||||
}
|
||||
|
||||
const props = withDefaults(defineProps<Props>(), {
|
||||
scanned: false,
|
||||
status: 'not_scanned',
|
||||
total: 0,
|
||||
isBlocked: false,
|
||||
})
|
||||
|
||||
const emit = defineEmits<{
|
||||
|
||||
@@ -7,11 +7,16 @@
|
||||
Back to Stats
|
||||
</Button>
|
||||
<div class="flex items-center gap-3">
|
||||
<i class="fas fa-exclamation-triangle text-3xl text-red-600"></i>
|
||||
<i :class="showOnlyBlocked ? 'fas fa-hand' : 'fas fa-exclamation-triangle'" class="text-3xl text-red-600"></i>
|
||||
<div>
|
||||
<h1 class="text-3xl font-bold text-gray-900">Vulnerable Packages</h1>
|
||||
<h1 class="text-3xl font-bold text-gray-900">
|
||||
{{ showOnlyBlocked ? 'Blocked Packages' : 'Vulnerable Packages' }}
|
||||
</h1>
|
||||
<p class="text-gray-600 mt-1">
|
||||
Packages with known security vulnerabilities, sorted by risk
|
||||
{{ showOnlyBlocked
|
||||
? 'Packages blocked from download due to exceeding vulnerability thresholds'
|
||||
: 'Packages with known security vulnerabilities, sorted by risk'
|
||||
}}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
@@ -132,6 +137,7 @@
|
||||
:counts="version.vulnerabilities.counts"
|
||||
:total="version.vulnerabilities.total"
|
||||
:scannedAt="version.vulnerabilities.scannedAt"
|
||||
:isBlocked="version.vulnerabilities.isBlocked"
|
||||
@click.stop="navigateToPackage(version)"
|
||||
/>
|
||||
</div>
|
||||
@@ -170,11 +176,15 @@ import { getRegistryBadgeClass } from '@/composables/useBadgeStyles'
|
||||
|
||||
const store = usePackageStore()
|
||||
const router = useRouter()
|
||||
const route = useRoute()
|
||||
|
||||
const loading = ref(false)
|
||||
const error = ref<string | null>(null)
|
||||
const vulnerablePackages = ref<Package[]>([])
|
||||
|
||||
// Check if we should filter to show only blocked packages
|
||||
const showOnlyBlocked = computed(() => route.path === '/blocked-packages')
|
||||
|
||||
onMounted(async () => {
|
||||
await fetchVulnerablePackages()
|
||||
})
|
||||
@@ -185,9 +195,13 @@ async function fetchVulnerablePackages() {
|
||||
|
||||
try {
|
||||
await store.fetchPackages()
|
||||
vulnerablePackages.value = store.packages.filter(
|
||||
pkg => pkg.vulnerabilities?.status === 'vulnerable'
|
||||
)
|
||||
vulnerablePackages.value = store.packages.filter(pkg => {
|
||||
const isVulnerable = pkg.vulnerabilities?.status === 'vulnerable'
|
||||
if (showOnlyBlocked.value) {
|
||||
return isVulnerable && pkg.vulnerabilities?.isBlocked === true
|
||||
}
|
||||
return isVulnerable
|
||||
})
|
||||
} catch (err: any) {
|
||||
console.error('Failed to load vulnerable packages:', err)
|
||||
error.value = err.message || 'Failed to load vulnerable packages'
|
||||
|
||||
Reference in New Issue
Block a user