mirror of
https://github.com/RayLabsHQ/gitea-mirror.git
synced 2025-12-06 19:46:44 +03:00
- Prevent Automation UI from overriding schedule:
- mapDbScheduleToUi now parses intervals robustly (cron/duration/seconds) via parseInterval
- mapUiScheduleToDb merges with existing config and stores interval as seconds (no lossy cron conversion)
- /api/config passes existing scheduleConfig to preserve ENV-sourced values
- schedule-sync endpoint uses parseInterval for nextRun calculation
- Add AUTO_MIRROR_REPOS support and scheduled auto-mirror phase:
- scheduleConfig schema includes autoImport and autoMirror
- env-config-loader reads AUTO_MIRROR_REPOS and carries through to DB
- scheduler auto-mirrors imported/pending/failed repos when autoMirror is enabled before regular sync
- docker-compose and ENV docs updated with AUTO_MIRROR_REPOS
- Tests pass and build succeeds
298 lines
8.8 KiB
TypeScript
298 lines
8.8 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 { calculateCleanupInterval } from "@/lib/cleanup-service";
|
|
import { createSecureErrorResponse } from "@/lib/utils";
|
|
import {
|
|
mapUiToDbConfig,
|
|
mapDbToUiConfig,
|
|
mapUiScheduleToDb,
|
|
mapUiCleanupToDb,
|
|
mapDbScheduleToUi,
|
|
mapDbCleanupToUi
|
|
} from "@/lib/utils/config-mapper";
|
|
import { encrypt, decrypt, migrateToken } from "@/lib/utils/encryption";
|
|
import { createDefaultConfig } from "@/lib/utils/config-defaults";
|
|
|
|
export const POST: APIRoute = async ({ request }) => {
|
|
try {
|
|
const body = await request.json();
|
|
const { userId, githubConfig, giteaConfig, scheduleConfig, cleanupConfig, mirrorOptions, advancedOptions } = body;
|
|
|
|
if (!userId || !githubConfig || !giteaConfig || !scheduleConfig || !cleanupConfig || !mirrorOptions || !advancedOptions) {
|
|
return new Response(
|
|
JSON.stringify({
|
|
success: false,
|
|
message:
|
|
"userId, githubConfig, giteaConfig, scheduleConfig, cleanupConfig, mirrorOptions, and advancedOptions are required.",
|
|
}),
|
|
{
|
|
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 }) => {
|
|
try {
|
|
const url = new URL(request.url);
|
|
const userId = url.searchParams.get("userId");
|
|
|
|
if (!userId) {
|
|
return new Response(JSON.stringify({ error: "User ID is required" }), {
|
|
status: 400,
|
|
headers: { "Content-Type": "application/json" },
|
|
});
|
|
}
|
|
|
|
// 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);
|
|
}
|
|
};
|