mirror of
https://github.com/RayLabsHQ/gitea-mirror.git
synced 2026-03-22 21:58:21 +03:00
* fix: resolve CVEs, upgrade to Astro v6, and harden API security
Docker image CVE fixes:
- Install git-lfs v3.7.1 from GitHub releases (Go 1.25) instead of
Debian apt (Go 1.23.12), fixing CVE-2025-68121 and 8 other Go stdlib CVEs
- Strip build-only packages (esbuild, vite, rollup, svgo, tailwindcss)
from production image, eliminating 9 esbuild Go stdlib CVEs
Dependency upgrades:
- Astro v5 → v6 (includes Vite 7, Zod 4)
- Remove legacy content config (src/content/config.ts)
- Update HealthResponse type for simplified health endpoint
- npm overrides for fast-xml-parser ≥5.3.6, devalue ≥5.6.2,
node-forge ≥1.3.2, svgo ≥4.0.1, rollup ≥4.59.0
API security hardening:
- /api/auth/debug: dev-only, require auth, remove user-creation POST,
strip trustedOrigins/databaseConfig from response
- /api/auth/check-users: return boolean hasUsers instead of exact count
- /api/cleanup/auto: require authentication, remove per-user details
- /api/health: remove OS version, memory, uptime from response
- /api/config: validate Gitea URL protocol (http/https only)
- BETTER_AUTH_SECRET: log security warning when using insecure defaults
- generateRandomString: replace Math.random() with crypto.getRandomValues()
- hashValue: add random salt and timing-safe verification
* repositories: migrate table to tanstack
* Revert "repositories: migrate table to tanstack"
This reverts commit a544b29e6d.
* fixed lock file
169 lines
4.8 KiB
TypeScript
169 lines
4.8 KiB
TypeScript
import type { APIRoute } from "astro";
|
|
import { jsonResponse, createSecureErrorResponse } from "@/lib/utils";
|
|
import { db } from "@/lib/db";
|
|
import { getRecoveryStatus, hasJobsNeedingRecovery } from "@/lib/recovery";
|
|
import { httpGet } from "@/lib/http-client";
|
|
|
|
// Cache for the latest version to avoid frequent GitHub API calls
|
|
interface VersionCache {
|
|
latestVersion: string;
|
|
timestamp: number;
|
|
}
|
|
|
|
let versionCache: VersionCache | null = null;
|
|
const CACHE_TTL = 3600000; // 1 hour in milliseconds
|
|
|
|
export const GET: APIRoute = async () => {
|
|
try {
|
|
// Check database connection by running a simple query
|
|
const dbStatus = await checkDatabaseConnection();
|
|
|
|
// Get current and latest versions
|
|
const currentVersion = process.env.npm_package_version || "unknown";
|
|
const latestVersion = await checkLatestVersion();
|
|
|
|
// Get recovery system status
|
|
const recoveryStatus = await getRecoverySystemStatus();
|
|
|
|
// Determine overall health status
|
|
let overallStatus = "ok";
|
|
if (!dbStatus.connected) {
|
|
overallStatus = "error";
|
|
} else if (recoveryStatus.jobsNeedingRecovery > 0 && !recoveryStatus.inProgress) {
|
|
overallStatus = "degraded";
|
|
}
|
|
|
|
// Build response (no OS/memory details to avoid information disclosure)
|
|
const healthData = {
|
|
status: overallStatus,
|
|
timestamp: new Date().toISOString(),
|
|
version: currentVersion,
|
|
latestVersion: latestVersion,
|
|
updateAvailable: latestVersion !== "unknown" &&
|
|
currentVersion !== "unknown" &&
|
|
compareVersions(currentVersion, latestVersion) < 0,
|
|
database: { connected: dbStatus.connected },
|
|
recovery: {
|
|
status: recoveryStatus.status,
|
|
jobsNeedingRecovery: recoveryStatus.jobsNeedingRecovery,
|
|
},
|
|
};
|
|
|
|
return jsonResponse({
|
|
data: healthData,
|
|
status: 200,
|
|
});
|
|
} catch (error) {
|
|
return createSecureErrorResponse(error, "health check", 503);
|
|
}
|
|
};
|
|
|
|
/**
|
|
* Check database connection by running a simple query
|
|
*/
|
|
async function checkDatabaseConnection() {
|
|
try {
|
|
// Run a simple query to check if the database is accessible
|
|
const result = await db.select({ test: sql`1` }).from(sql`sqlite_master`).limit(1);
|
|
|
|
return {
|
|
connected: true,
|
|
message: "Database connection successful",
|
|
};
|
|
} catch (error) {
|
|
console.error("Database connection check failed:", error);
|
|
|
|
return {
|
|
connected: false,
|
|
message: error instanceof Error ? error.message : "Database connection failed",
|
|
};
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Get recovery system status
|
|
*/
|
|
async function getRecoverySystemStatus() {
|
|
try {
|
|
const recoveryStatus = getRecoveryStatus();
|
|
const needsRecovery = await hasJobsNeedingRecovery();
|
|
|
|
return {
|
|
status: needsRecovery ? 'jobs-pending' : 'healthy',
|
|
inProgress: recoveryStatus.inProgress,
|
|
lastAttempt: recoveryStatus.lastAttempt?.toISOString() || null,
|
|
jobsNeedingRecovery: needsRecovery ? 1 : 0, // Simplified count for health check
|
|
message: needsRecovery
|
|
? 'Jobs found that need recovery'
|
|
: 'No jobs need recovery',
|
|
};
|
|
} catch (error) {
|
|
console.error('Recovery system status check failed:', error);
|
|
|
|
return {
|
|
status: 'error',
|
|
inProgress: false,
|
|
lastAttempt: null,
|
|
jobsNeedingRecovery: -1,
|
|
message: error instanceof Error ? error.message : 'Recovery status check failed',
|
|
};
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Compare semantic versions
|
|
* Returns:
|
|
* -1 if v1 < v2
|
|
* 0 if v1 = v2
|
|
* 1 if v1 > v2
|
|
*/
|
|
function compareVersions(v1: string, v2: string): number {
|
|
const parts1 = v1.split('.').map(Number);
|
|
const parts2 = v2.split('.').map(Number);
|
|
|
|
for (let i = 0; i < Math.max(parts1.length, parts2.length); i++) {
|
|
const part1 = parts1[i] || 0;
|
|
const part2 = parts2[i] || 0;
|
|
|
|
if (part1 < part2) return -1;
|
|
if (part1 > part2) return 1;
|
|
}
|
|
|
|
return 0;
|
|
}
|
|
|
|
/**
|
|
* Check for the latest version from GitHub releases
|
|
*/
|
|
async function checkLatestVersion(): Promise<string> {
|
|
// Return cached version if available and not expired
|
|
if (versionCache && (Date.now() - versionCache.timestamp) < CACHE_TTL) {
|
|
return versionCache.latestVersion;
|
|
}
|
|
|
|
try {
|
|
// Fetch the latest release from GitHub
|
|
const response = await httpGet(
|
|
'https://api.github.com/repos/RayLabsHQ/gitea-mirror/releases/latest',
|
|
{ 'Accept': 'application/vnd.github.v3+json' }
|
|
);
|
|
|
|
// Extract version from tag_name (remove 'v' prefix if present)
|
|
const latestVersion = response.data.tag_name.replace(/^v/, '');
|
|
|
|
// Update cache
|
|
versionCache = {
|
|
latestVersion,
|
|
timestamp: Date.now()
|
|
};
|
|
|
|
return latestVersion;
|
|
} catch (error) {
|
|
console.error('Failed to check for latest version:', error);
|
|
return 'unknown';
|
|
}
|
|
}
|
|
|
|
// Import sql tag for raw SQL queries
|
|
import { sql } from "drizzle-orm";
|