mirror of
https://github.com/RayLabsHQ/gitea-mirror.git
synced 2026-04-05 12:38:43 +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
314 lines
9.5 KiB
TypeScript
314 lines
9.5 KiB
TypeScript
import type { APIRoute } from "astro";
|
|
import { db, configs, users } from "@/lib/db";
|
|
import { v4 as uuidv4 } from "uuid";
|
|
import { eq } from "drizzle-orm";
|
|
import { createSecureErrorResponse } from "@/lib/utils";
|
|
import {
|
|
mapUiToDbConfig,
|
|
mapDbToUiConfig,
|
|
mapUiScheduleToDb,
|
|
mapUiCleanupToDb,
|
|
mapDbScheduleToUi,
|
|
mapDbCleanupToUi
|
|
} from "@/lib/utils/config-mapper";
|
|
import { encrypt, decrypt } from "@/lib/utils/encryption";
|
|
import { createDefaultConfig } from "@/lib/utils/config-defaults";
|
|
import { requireAuthenticatedUserId } from "@/lib/auth-guards";
|
|
|
|
export const POST: APIRoute = async ({ request, locals }) => {
|
|
try {
|
|
const authResult = await requireAuthenticatedUserId({ request, locals });
|
|
if ("response" in authResult) return authResult.response;
|
|
const userId = authResult.userId;
|
|
|
|
const body = await request.json();
|
|
const { githubConfig, giteaConfig, scheduleConfig, cleanupConfig, mirrorOptions, advancedOptions } = body;
|
|
|
|
if (!githubConfig || !giteaConfig || !scheduleConfig || !cleanupConfig || !mirrorOptions || !advancedOptions) {
|
|
return new Response(
|
|
JSON.stringify({
|
|
success: false,
|
|
message:
|
|
"githubConfig, giteaConfig, scheduleConfig, cleanupConfig, mirrorOptions, and advancedOptions are required.",
|
|
}),
|
|
{
|
|
status: 400,
|
|
headers: { "Content-Type": "application/json" },
|
|
}
|
|
);
|
|
}
|
|
|
|
// Validate Gitea URL format and protocol
|
|
if (giteaConfig.url) {
|
|
try {
|
|
const giteaUrl = new URL(giteaConfig.url);
|
|
if (!['http:', 'https:'].includes(giteaUrl.protocol)) {
|
|
return new Response(
|
|
JSON.stringify({ success: false, message: "Gitea URL must use http or https protocol." }),
|
|
{ status: 400, headers: { "Content-Type": "application/json" } }
|
|
);
|
|
}
|
|
} catch {
|
|
return new Response(
|
|
JSON.stringify({ success: false, message: "Invalid Gitea URL format." }),
|
|
{ status: 400, headers: { "Content-Type": "application/json" } }
|
|
);
|
|
}
|
|
}
|
|
|
|
// Fetch existing config
|
|
const existingConfigResult = await db
|
|
.select()
|
|
.from(configs)
|
|
.where(eq(configs.userId, userId))
|
|
.limit(1);
|
|
|
|
const existingConfig = existingConfigResult[0];
|
|
|
|
// Map UI structure to database schema structure first
|
|
const { githubConfig: mappedGithubConfig, giteaConfig: mappedGiteaConfig } = mapUiToDbConfig(
|
|
githubConfig,
|
|
giteaConfig,
|
|
mirrorOptions,
|
|
advancedOptions
|
|
);
|
|
|
|
// Preserve tokens if fields are empty
|
|
if (existingConfig) {
|
|
try {
|
|
const existingGithub =
|
|
typeof existingConfig.githubConfig === "string"
|
|
? JSON.parse(existingConfig.githubConfig)
|
|
: existingConfig.githubConfig;
|
|
|
|
const existingGitea =
|
|
typeof existingConfig.giteaConfig === "string"
|
|
? JSON.parse(existingConfig.giteaConfig)
|
|
: existingConfig.giteaConfig;
|
|
|
|
// Decrypt existing tokens before preserving
|
|
if (!mappedGithubConfig.token && existingGithub.token) {
|
|
mappedGithubConfig.token = decrypt(existingGithub.token);
|
|
}
|
|
|
|
if (!mappedGiteaConfig.token && existingGitea.token) {
|
|
mappedGiteaConfig.token = decrypt(existingGitea.token);
|
|
}
|
|
} catch (tokenError) {
|
|
console.error("Failed to preserve tokens:", tokenError);
|
|
}
|
|
}
|
|
|
|
// Encrypt tokens before saving
|
|
if (mappedGithubConfig.token) {
|
|
mappedGithubConfig.token = encrypt(mappedGithubConfig.token);
|
|
}
|
|
|
|
if (mappedGiteaConfig.token) {
|
|
mappedGiteaConfig.token = encrypt(mappedGiteaConfig.token);
|
|
}
|
|
|
|
// Map schedule and cleanup configs to database schema
|
|
const processedScheduleConfig = mapUiScheduleToDb(
|
|
scheduleConfig,
|
|
existingConfig ? existingConfig.scheduleConfig : undefined
|
|
);
|
|
const processedCleanupConfig = mapUiCleanupToDb(cleanupConfig);
|
|
|
|
if (existingConfig) {
|
|
// Update path
|
|
await db
|
|
.update(configs)
|
|
.set({
|
|
githubConfig: mappedGithubConfig,
|
|
giteaConfig: mappedGiteaConfig,
|
|
scheduleConfig: processedScheduleConfig,
|
|
cleanupConfig: processedCleanupConfig,
|
|
updatedAt: new Date(),
|
|
})
|
|
.where(eq(configs.id, existingConfig.id));
|
|
|
|
return new Response(
|
|
JSON.stringify({
|
|
success: true,
|
|
message: "Configuration updated successfully",
|
|
configId: existingConfig.id,
|
|
}),
|
|
{
|
|
status: 200,
|
|
headers: { "Content-Type": "application/json" },
|
|
}
|
|
);
|
|
}
|
|
|
|
// Fallback user check (optional if you're always passing userId)
|
|
const userExists = await db
|
|
.select()
|
|
.from(users)
|
|
.where(eq(users.id, userId))
|
|
.limit(1);
|
|
|
|
if (userExists.length === 0) {
|
|
return new Response(
|
|
JSON.stringify({
|
|
success: false,
|
|
message: "Invalid userId. No matching user found.",
|
|
}),
|
|
{
|
|
status: 404,
|
|
headers: { "Content-Type": "application/json" },
|
|
}
|
|
);
|
|
}
|
|
|
|
// Create new config
|
|
const configId = uuidv4();
|
|
await db.insert(configs).values({
|
|
id: configId,
|
|
userId,
|
|
name: "Default Configuration",
|
|
isActive: true,
|
|
githubConfig: mappedGithubConfig,
|
|
giteaConfig: mappedGiteaConfig,
|
|
include: [],
|
|
exclude: [],
|
|
scheduleConfig: processedScheduleConfig,
|
|
cleanupConfig: processedCleanupConfig,
|
|
createdAt: new Date(),
|
|
updatedAt: new Date(),
|
|
});
|
|
|
|
return new Response(
|
|
JSON.stringify({
|
|
success: true,
|
|
message: "Configuration created successfully",
|
|
configId,
|
|
}),
|
|
{
|
|
status: 201,
|
|
headers: { "Content-Type": "application/json" },
|
|
}
|
|
);
|
|
} catch (error) {
|
|
return createSecureErrorResponse(error, "config save", 500);
|
|
}
|
|
};
|
|
|
|
export const GET: APIRoute = async ({ request, locals }) => {
|
|
try {
|
|
const authResult = await requireAuthenticatedUserId({ request, locals });
|
|
if ("response" in authResult) return authResult.response;
|
|
const userId = authResult.userId;
|
|
|
|
// Fetch the configuration for the user
|
|
const config = await db
|
|
.select()
|
|
.from(configs)
|
|
.where(eq(configs.userId, userId))
|
|
.limit(1);
|
|
|
|
if (config.length === 0) {
|
|
// Create default configuration for the user
|
|
const defaultConfig = await createDefaultConfig({ userId });
|
|
|
|
// Map the created config to UI format
|
|
const uiConfig = mapDbToUiConfig(defaultConfig);
|
|
const uiScheduleConfig = mapDbScheduleToUi(defaultConfig.scheduleConfig);
|
|
const uiCleanupConfig = mapDbCleanupToUi(defaultConfig.cleanupConfig);
|
|
|
|
return new Response(
|
|
JSON.stringify({
|
|
...defaultConfig,
|
|
...uiConfig,
|
|
scheduleConfig: uiScheduleConfig,
|
|
cleanupConfig: uiCleanupConfig,
|
|
}),
|
|
{
|
|
status: 200,
|
|
headers: { "Content-Type": "application/json" },
|
|
}
|
|
);
|
|
}
|
|
|
|
// Map database structure to UI structure
|
|
const dbConfig = config[0];
|
|
|
|
// Decrypt tokens before sending to UI
|
|
try {
|
|
const githubConfig = typeof dbConfig.githubConfig === "string"
|
|
? JSON.parse(dbConfig.githubConfig)
|
|
: dbConfig.githubConfig;
|
|
|
|
const giteaConfig = typeof dbConfig.giteaConfig === "string"
|
|
? JSON.parse(dbConfig.giteaConfig)
|
|
: dbConfig.giteaConfig;
|
|
|
|
// Decrypt tokens
|
|
if (githubConfig.token) {
|
|
githubConfig.token = decrypt(githubConfig.token);
|
|
}
|
|
|
|
if (giteaConfig.token) {
|
|
giteaConfig.token = decrypt(giteaConfig.token);
|
|
}
|
|
|
|
// Create modified config with decrypted tokens
|
|
const decryptedConfig = {
|
|
...dbConfig,
|
|
githubConfig,
|
|
giteaConfig
|
|
};
|
|
|
|
const uiConfig = mapDbToUiConfig(decryptedConfig);
|
|
|
|
// Map schedule and cleanup configs to UI format
|
|
const uiScheduleConfig = mapDbScheduleToUi(dbConfig.scheduleConfig);
|
|
const uiCleanupConfig = mapDbCleanupToUi(dbConfig.cleanupConfig);
|
|
|
|
return new Response(JSON.stringify({
|
|
...dbConfig,
|
|
...uiConfig,
|
|
scheduleConfig: {
|
|
...uiScheduleConfig,
|
|
lastRun: dbConfig.scheduleConfig.lastRun,
|
|
nextRun: dbConfig.scheduleConfig.nextRun,
|
|
},
|
|
cleanupConfig: {
|
|
...uiCleanupConfig,
|
|
lastRun: dbConfig.cleanupConfig.lastRun,
|
|
nextRun: dbConfig.cleanupConfig.nextRun,
|
|
},
|
|
}), {
|
|
status: 200,
|
|
headers: { "Content-Type": "application/json" },
|
|
});
|
|
} catch (error) {
|
|
console.error("Failed to decrypt tokens:", error);
|
|
// Return config without decrypting tokens if there's an error
|
|
const uiConfig = mapDbToUiConfig(dbConfig);
|
|
const uiScheduleConfig = mapDbScheduleToUi(dbConfig.scheduleConfig);
|
|
const uiCleanupConfig = mapDbCleanupToUi(dbConfig.cleanupConfig);
|
|
|
|
return new Response(JSON.stringify({
|
|
...dbConfig,
|
|
...uiConfig,
|
|
scheduleConfig: {
|
|
...uiScheduleConfig,
|
|
lastRun: dbConfig.scheduleConfig.lastRun,
|
|
nextRun: dbConfig.scheduleConfig.nextRun,
|
|
},
|
|
cleanupConfig: {
|
|
...uiCleanupConfig,
|
|
lastRun: dbConfig.cleanupConfig.lastRun,
|
|
nextRun: dbConfig.cleanupConfig.nextRun,
|
|
},
|
|
}), {
|
|
status: 200,
|
|
headers: { "Content-Type": "application/json" },
|
|
});
|
|
}
|
|
} catch (error) {
|
|
return createSecureErrorResponse(error, "config fetch", 500);
|
|
}
|
|
};
|