Files
gitea-mirror/src/pages/api/health.ts
ARUNAVO RAY 299659eca2 fix: resolve CVEs, upgrade to Astro v6, and harden API security (#227)
* 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
2026-03-15 09:19:24 +05:30

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";