some more fixes

This commit is contained in:
Arunavo Ray
2025-07-17 23:31:45 +05:30
parent 4430625319
commit e6a31512ac
9 changed files with 291 additions and 164 deletions

View File

@@ -1,4 +1,3 @@
import React from "react";
import { Button } from "@/components/ui/button";
import { Card, CardContent, CardHeader } from "@/components/ui/card";
import { Home, ArrowLeft, GitBranch, BookOpen, Settings, FileQuestion } from "lucide-react";

View File

@@ -114,7 +114,7 @@ export function AuthProvider({ children }: { children: React.ReactNode }) {
// Create the context value
const contextValue = {
user: user as AuthUser | null,
session,
session: session as Session | null,
isLoading: isLoading || betterAuthSession.isPending,
error,
login,

View File

@@ -1,6 +1,7 @@
import { createAuthClient } from "better-auth/react";
import { oidcClient } from "better-auth/client/plugins";
import { ssoClient } from "better-auth/client/plugins";
import type { Session as BetterAuthSession, User as BetterAuthUser } from "better-auth";
export const authClient = createAuthClient({
// The base URL is optional when running on the same domain
@@ -23,6 +24,12 @@ export const {
getSession
} = authClient;
// Export types
export type Session = Awaited<ReturnType<typeof authClient.getSession>>["data"];
export type AuthUser = Session extends { user: infer U } ? U : never;
// Export types - directly use the types from better-auth
export type Session = BetterAuthSession & {
user: BetterAuthUser & {
username?: string | null;
};
};
export type AuthUser = BetterAuthUser & {
username?: string | null;
};

View File

@@ -47,6 +47,13 @@ export const giteaConfigSchema = z.object({
forkStrategy: z
.enum(["skip", "reference", "full-copy"])
.default("reference"),
// Mirror options
mirrorReleases: z.boolean().default(false),
mirrorMetadata: z.boolean().default(false),
mirrorIssues: z.boolean().default(false),
mirrorPullRequests: z.boolean().default(false),
mirrorLabels: z.boolean().default(false),
mirrorMilestones: z.boolean().default(false),
});
export const scheduleConfigSchema = z.object({

View File

@@ -272,6 +272,9 @@ export const mirrorGithubRepoToGitea = async ({
throw new Error("Gitea username is required.");
}
// Decrypt config tokens for API usage
const decryptedConfig = decryptConfigTokens(config as Config);
// Get the correct owner based on the strategy (with organization overrides)
const repoOwner = await getGiteaRepoOwnerAsync({ config, repository });
@@ -347,7 +350,7 @@ export const mirrorGithubRepoToGitea = async ({
cloneAddress = repository.cloneUrl.replace(
"https://",
`https://${config.githubConfig.token}@`
`https://${decryptedConfig.githubConfig.token}@`
);
}
@@ -644,6 +647,9 @@ export async function mirrorGitHubRepoToGiteaOrg({
throw new Error("Gitea config is required.");
}
// Decrypt config tokens for API usage
const decryptedConfig = decryptConfigTokens(config as Config);
const isExisting = await isRepoPresentInGitea({
config,
owner: orgName,
@@ -698,7 +704,7 @@ export async function mirrorGitHubRepoToGiteaOrg({
cloneAddress = repository.cloneUrl.replace(
"https://",
`https://${config.githubConfig.token}@`
`https://${decryptedConfig.githubConfig.token}@`
);
}
@@ -1125,7 +1131,7 @@ export const syncGiteaRepo = async ({
const apiUrl = `${config.giteaConfig.url}/api/v1/repos/${actualOwner}/${repository.name}/mirror-sync`;
const response = await httpPost(apiUrl, undefined, {
Authorization: `token ${config.giteaConfig.token}`,
Authorization: `token ${decryptedConfig.giteaConfig.token}`,
});
// Mark repo as "synced" in DB
@@ -1243,7 +1249,7 @@ export const mirrorGitRepoIssuesToGitea = async ({
const giteaLabelsRes = await httpGet(
`${config.giteaConfig.url}/api/v1/repos/${giteaOwner}/${repository.name}/labels`,
{
Authorization: `token ${config.giteaConfig.token}`,
Authorization: `token ${decryptedConfig.giteaConfig.token}`,
}
);

View File

@@ -9,35 +9,14 @@ import type {
AdvancedOptions,
SaveConfigApiRequest
} from "@/types/config";
import { z } from "zod";
import { githubConfigSchema, giteaConfigSchema, scheduleConfigSchema, cleanupConfigSchema } from "@/lib/db/schema";
interface DbGitHubConfig {
username: string;
token?: string;
skipForks: boolean;
privateRepositories: boolean;
mirrorIssues: boolean;
mirrorWiki: boolean;
mirrorStarred: boolean;
useSpecificUser: boolean;
singleRepo?: string;
includeOrgs: string[];
excludeOrgs: string[];
mirrorPublicOrgs: boolean;
publicOrgs: string[];
skipStarredIssues: boolean;
}
interface DbGiteaConfig {
username: string;
url: string;
token: string;
organization?: string;
visibility: "public" | "private" | "limited";
starredReposOrg: string;
preserveOrgStructure: boolean;
mirrorStrategy?: "preserve" | "single-org" | "flat-user" | "mixed";
personalReposOrg?: string;
}
// Use the actual database schema types
type DbGitHubConfig = z.infer<typeof githubConfigSchema>;
type DbGiteaConfig = z.infer<typeof giteaConfigSchema>;
type DbScheduleConfig = z.infer<typeof scheduleConfigSchema>;
type DbCleanupConfig = z.infer<typeof cleanupConfigSchema>;
/**
* Maps UI config structure to database schema structure
@@ -48,32 +27,67 @@ export function mapUiToDbConfig(
mirrorOptions: MirrorOptions,
advancedOptions: AdvancedOptions
): { githubConfig: DbGitHubConfig; giteaConfig: DbGiteaConfig } {
// Map GitHub config with fields from mirrorOptions and advancedOptions
// Map GitHub config to match database schema fields
const dbGithubConfig: DbGitHubConfig = {
username: githubConfig.username,
token: githubConfig.token,
privateRepositories: githubConfig.privateRepositories,
mirrorStarred: githubConfig.mirrorStarred,
// Map username to owner field
owner: githubConfig.username,
type: "personal", // Default to personal, could be made configurable
token: githubConfig.token || "",
// From mirrorOptions
mirrorIssues: mirrorOptions.mirrorMetadata && mirrorOptions.metadataComponents.issues,
mirrorWiki: mirrorOptions.mirrorMetadata && mirrorOptions.metadataComponents.wiki,
// Map checkbox fields with proper names
includeStarred: githubConfig.mirrorStarred,
includePrivate: githubConfig.privateRepositories,
includeForks: !advancedOptions.skipForks, // Note: UI has skipForks, DB has includeForks
includeArchived: false, // Not in UI yet, default to false
includePublic: true, // Not in UI yet, default to true
// From advancedOptions
skipForks: advancedOptions.skipForks,
skipStarredIssues: advancedOptions.skipStarredIssues,
// Organization related fields
includeOrganizations: [], // Not in UI yet
// Default values for fields not in UI
useSpecificUser: false,
includeOrgs: [],
excludeOrgs: [],
mirrorPublicOrgs: false,
publicOrgs: [],
// Starred repos organization
starredReposOrg: giteaConfig.starredReposOrg,
// Mirror strategy
mirrorStrategy: giteaConfig.mirrorStrategy || "preserve",
defaultOrg: giteaConfig.organization,
};
// Gitea config remains mostly the same
// Map Gitea config to match database schema
const dbGiteaConfig: DbGiteaConfig = {
...giteaConfig,
url: giteaConfig.url,
token: giteaConfig.token,
defaultOwner: giteaConfig.username, // Map username to defaultOwner
// Mirror interval and options
mirrorInterval: "8h", // Default value, could be made configurable
lfs: false, // Not in UI yet
wiki: mirrorOptions.mirrorMetadata && mirrorOptions.metadataComponents.wiki,
// Visibility settings
visibility: giteaConfig.visibility || "default",
preserveVisibility: giteaConfig.preserveOrgStructure,
// Organization creation
createOrg: true, // Default to true
// Template settings (not in UI yet)
templateOwner: undefined,
templateRepo: undefined,
// Topics
addTopics: true, // Default to true
topicPrefix: undefined,
// Fork strategy
forkStrategy: advancedOptions.skipForks ? "skip" : "reference",
// Mirror options from UI
mirrorReleases: mirrorOptions.mirrorReleases,
mirrorMetadata: mirrorOptions.mirrorMetadata,
mirrorIssues: mirrorOptions.mirrorMetadata && mirrorOptions.metadataComponents.issues,
mirrorPullRequests: mirrorOptions.mirrorMetadata && mirrorOptions.metadataComponents.pullRequests,
mirrorLabels: mirrorOptions.mirrorMetadata && mirrorOptions.metadataComponents.labels,
mirrorMilestones: mirrorOptions.mirrorMetadata && mirrorOptions.metadataComponents.milestones,
};
return {
@@ -91,40 +105,44 @@ export function mapDbToUiConfig(dbConfig: any): {
mirrorOptions: MirrorOptions;
advancedOptions: AdvancedOptions;
} {
// Map from database GitHub config to UI fields
const githubConfig: GitHubConfig = {
username: dbConfig.githubConfig?.username || "",
username: dbConfig.githubConfig?.owner || "", // Map owner to username
token: dbConfig.githubConfig?.token || "",
privateRepositories: dbConfig.githubConfig?.privateRepositories || false,
mirrorStarred: dbConfig.githubConfig?.mirrorStarred || false,
privateRepositories: dbConfig.githubConfig?.includePrivate || false, // Map includePrivate to privateRepositories
mirrorStarred: dbConfig.githubConfig?.includeStarred || false, // Map includeStarred to mirrorStarred
};
// Map from database Gitea config to UI fields
const giteaConfig: GiteaConfig = {
url: dbConfig.giteaConfig?.url || "",
username: dbConfig.giteaConfig?.username || "",
username: dbConfig.giteaConfig?.defaultOwner || "", // Map defaultOwner to username
token: dbConfig.giteaConfig?.token || "",
organization: dbConfig.giteaConfig?.organization || "github-mirrors",
visibility: dbConfig.giteaConfig?.visibility || "public",
starredReposOrg: dbConfig.giteaConfig?.starredReposOrg || "github",
preserveOrgStructure: dbConfig.giteaConfig?.preserveOrgStructure || false,
mirrorStrategy: dbConfig.giteaConfig?.mirrorStrategy,
personalReposOrg: dbConfig.giteaConfig?.personalReposOrg,
organization: dbConfig.githubConfig?.defaultOrg || "github-mirrors", // Get from GitHub config
visibility: dbConfig.giteaConfig?.visibility === "default" ? "public" : dbConfig.giteaConfig?.visibility || "public",
starredReposOrg: dbConfig.githubConfig?.starredReposOrg || "github", // Get from GitHub config
preserveOrgStructure: dbConfig.giteaConfig?.preserveVisibility || false, // Map preserveVisibility
mirrorStrategy: dbConfig.githubConfig?.mirrorStrategy || "preserve", // Get from GitHub config
personalReposOrg: undefined, // Not stored in current schema
};
// Map mirror options from various database fields
const mirrorOptions: MirrorOptions = {
mirrorReleases: false, // Not stored in DB yet
mirrorMetadata: dbConfig.githubConfig?.mirrorIssues || dbConfig.githubConfig?.mirrorWiki || false,
mirrorReleases: dbConfig.giteaConfig?.mirrorReleases || false,
mirrorMetadata: dbConfig.giteaConfig?.mirrorMetadata || false,
metadataComponents: {
issues: dbConfig.githubConfig?.mirrorIssues || false,
pullRequests: false, // Not stored in DB yet
labels: false, // Not stored in DB yet
milestones: false, // Not stored in DB yet
wiki: dbConfig.githubConfig?.mirrorWiki || false,
issues: dbConfig.giteaConfig?.mirrorIssues || false,
pullRequests: dbConfig.giteaConfig?.mirrorPullRequests || false,
labels: dbConfig.giteaConfig?.mirrorLabels || false,
milestones: dbConfig.giteaConfig?.mirrorMilestones || false,
wiki: dbConfig.giteaConfig?.wiki || false,
},
};
// Map advanced options
const advancedOptions: AdvancedOptions = {
skipForks: dbConfig.githubConfig?.skipForks || false,
skipStarredIssues: dbConfig.githubConfig?.skipStarredIssues || false,
skipForks: !(dbConfig.githubConfig?.includeForks ?? true), // Invert includeForks to get skipForks
skipStarredIssues: false, // Not stored in current schema
};
return {
@@ -133,4 +151,73 @@ export function mapDbToUiConfig(dbConfig: any): {
mirrorOptions,
advancedOptions,
};
}
/**
* Maps UI schedule config to database schema
*/
export function mapUiScheduleToDb(uiSchedule: any): DbScheduleConfig {
return {
enabled: uiSchedule.enabled || false,
interval: uiSchedule.interval ? `0 */${Math.floor(uiSchedule.interval / 3600)} * * *` : "0 2 * * *", // Convert seconds to cron expression
concurrent: false,
batchSize: 10,
pauseBetweenBatches: 5000,
retryAttempts: 3,
retryDelay: 60000,
timeout: 3600000,
autoRetry: true,
cleanupBeforeMirror: false,
notifyOnFailure: true,
notifyOnSuccess: false,
logLevel: "info",
timezone: "UTC",
onlyMirrorUpdated: false,
updateInterval: 86400000,
skipRecentlyMirrored: true,
recentThreshold: 3600000,
};
}
/**
* Maps database schedule config to UI format
*/
export function mapDbScheduleToUi(dbSchedule: DbScheduleConfig): any {
// Extract hours from cron expression if possible
let intervalSeconds = 3600; // Default 1 hour
const cronMatch = dbSchedule.interval.match(/0 \*\/(\d+) \* \* \*/);
if (cronMatch) {
intervalSeconds = parseInt(cronMatch[1]) * 3600;
}
return {
enabled: dbSchedule.enabled,
interval: intervalSeconds,
};
}
/**
* Maps UI cleanup config to database schema
*/
export function mapUiCleanupToDb(uiCleanup: any): DbCleanupConfig {
return {
enabled: uiCleanup.enabled || false,
deleteFromGitea: false,
deleteIfNotInGitHub: true,
protectedRepos: [],
dryRun: true,
orphanedRepoAction: "archive",
batchSize: 10,
pauseBetweenDeletes: 2000,
};
}
/**
* Maps database cleanup config to UI format
*/
export function mapDbCleanupToUi(dbCleanup: DbCleanupConfig): any {
return {
enabled: dbCleanup.enabled,
retentionDays: 604800, // 7 days in seconds (kept for compatibility)
};
}

View File

@@ -4,7 +4,14 @@ 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 } from "@/lib/utils/config-mapper";
import {
mapUiToDbConfig,
mapDbToUiConfig,
mapUiScheduleToDb,
mapUiCleanupToDb,
mapDbScheduleToUi,
mapDbCleanupToUi
} from "@/lib/utils/config-mapper";
import { encrypt, decrypt, migrateToken } from "@/lib/utils/encryption";
export const POST: APIRoute = async ({ request }) => {
@@ -78,62 +85,9 @@ export const POST: APIRoute = async ({ request }) => {
mappedGiteaConfig.token = encrypt(mappedGiteaConfig.token);
}
// Process schedule config - set/update nextRun if enabled, clear if disabled
const processedScheduleConfig = { ...scheduleConfig };
if (scheduleConfig.enabled) {
const now = new Date();
const interval = scheduleConfig.interval || 3600; // Default to 1 hour
// Check if we need to recalculate nextRun
// Recalculate if: no nextRun exists, or interval changed from existing config
let shouldRecalculate = !scheduleConfig.nextRun;
if (existingConfig && existingConfig.scheduleConfig) {
const existingScheduleConfig = existingConfig.scheduleConfig;
const existingInterval = existingScheduleConfig.interval || 3600;
// If interval changed, recalculate nextRun
if (interval !== existingInterval) {
shouldRecalculate = true;
}
}
if (shouldRecalculate) {
processedScheduleConfig.nextRun = new Date(now.getTime() + interval * 1000);
}
} else {
// Clear nextRun when disabled
processedScheduleConfig.nextRun = null;
}
// Process cleanup config - set/update nextRun if enabled, clear if disabled
const processedCleanupConfig = { ...cleanupConfig };
if (cleanupConfig.enabled) {
const now = new Date();
const retentionSeconds = cleanupConfig.retentionDays || 604800; // Default 7 days in seconds
const cleanupIntervalHours = calculateCleanupInterval(retentionSeconds);
// Check if we need to recalculate nextRun
// Recalculate if: no nextRun exists, or retention period changed from existing config
let shouldRecalculate = !cleanupConfig.nextRun;
if (existingConfig && existingConfig.cleanupConfig) {
const existingCleanupConfig = existingConfig.cleanupConfig;
const existingRetentionSeconds = existingCleanupConfig.retentionDays || 604800;
// If retention period changed, recalculate nextRun
if (retentionSeconds !== existingRetentionSeconds) {
shouldRecalculate = true;
}
}
if (shouldRecalculate) {
processedCleanupConfig.nextRun = new Date(now.getTime() + cleanupIntervalHours * 60 * 60 * 1000);
}
} else {
// Clear nextRun when disabled
processedCleanupConfig.nextRun = null;
}
// Map schedule and cleanup configs to database schema
const processedScheduleConfig = mapUiScheduleToDb(scheduleConfig);
const processedCleanupConfig = mapUiCleanupToDb(cleanupConfig);
if (existingConfig) {
// Update path
@@ -234,28 +188,34 @@ export const GET: APIRoute = async ({ request }) => {
.limit(1);
if (config.length === 0) {
// Return a default empty configuration with UI structure
// Return a default empty configuration with database structure
const defaultDbConfig = {
githubConfig: {
username: "",
owner: "",
type: "personal",
token: "",
skipForks: false,
privateRepositories: false,
mirrorIssues: false,
mirrorWiki: false,
mirrorStarred: false,
useSpecificUser: false,
preserveOrgStructure: false,
skipStarredIssues: false,
includeStarred: false,
includeForks: true,
includeArchived: false,
includePrivate: false,
includePublic: true,
includeOrganizations: [],
starredReposOrg: "github",
mirrorStrategy: "preserve",
defaultOrg: "github-mirrors",
},
giteaConfig: {
url: "",
token: "",
username: "",
organization: "github-mirrors",
defaultOwner: "",
mirrorInterval: "8h",
lfs: false,
wiki: false,
visibility: "public",
starredReposOrg: "github",
preserveOrgStructure: false,
createOrg: true,
addTopics: true,
preserveVisibility: false,
forkStrategy: "reference",
},
};
@@ -319,9 +279,23 @@ export const GET: APIRoute = async ({ request }) => {
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" },
@@ -330,9 +304,22 @@ export const GET: APIRoute = async ({ request }) => {
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" },

View File

@@ -45,17 +45,10 @@ export const GET: APIRoute = async ({ request }) => {
// Build query conditions based on config
const conditions = [eq(repositories.userId, userId)];
if (!githubConfig.mirrorStarred) {
conditions.push(eq(repositories.isStarred, false));
}
if (githubConfig.skipForks) {
conditions.push(eq(repositories.isForked, false));
}
if (!githubConfig.privateRepositories) {
conditions.push(eq(repositories.isPrivate, false));
}
// Note: We show ALL repositories in the list
// The mirrorStarred and privateRepositories flags only control what gets mirrored,
// not what's displayed in the repository list
// Only skipForks is used for filtering the display since forked repos are often noise
const rawRepositories = await db
.select()

View File

@@ -9,6 +9,8 @@ import type {
} from "@/types/organizations";
import type { RepositoryVisibility, RepoStatus } from "@/types/Repository";
import { v4 as uuidv4 } from "uuid";
import { decryptConfigTokens } from "@/lib/utils/config-encryption";
import { createGitHubClient } from "@/lib/github";
export const POST: APIRoute = async ({ request }) => {
try {
@@ -44,32 +46,71 @@ export const POST: APIRoute = async ({ request }) => {
const [config] = await db
.select()
.from(configs)
.where(eq(configs.userId, userId))
.where(and(eq(configs.userId, userId), eq(configs.isActive, true)))
.limit(1);
if (!config) {
return jsonResponse({
data: { error: "No configuration found for this user" },
data: { error: "No active configuration found for this user" },
status: 404,
});
}
const configId = config.id;
const octokit = new Octokit();
// Decrypt the config to get tokens
const decryptedConfig = decryptConfigTokens(config);
// Check if we have a GitHub token
if (!decryptedConfig.githubConfig?.token) {
return jsonResponse({
data: { error: "GitHub token not configured" },
status: 401,
});
}
// Create authenticated Octokit instance
const octokit = createGitHubClient(decryptedConfig.githubConfig.token);
// Fetch org metadata
const { data: orgData } = await octokit.orgs.get({ org });
// Fetch public repos using Octokit paginator
// Fetch repos based on config settings
const allRepos = [];
// Always fetch public repos
const publicRepos = await octokit.paginate(octokit.repos.listForOrg, {
org,
type: "public",
per_page: 100,
});
allRepos.push(...publicRepos);
// Fetch private repos if enabled in config
if (decryptedConfig.githubConfig?.includePrivate) {
const privateRepos = await octokit.paginate(octokit.repos.listForOrg, {
org,
type: "private",
per_page: 100,
});
allRepos.push(...privateRepos);
}
// Also fetch member repos (includes private repos the user has access to)
if (decryptedConfig.githubConfig?.includePrivate) {
const memberRepos = await octokit.paginate(octokit.repos.listForOrg, {
org,
type: "member",
per_page: 100,
});
// Filter out duplicates
const existingIds = new Set(allRepos.map(r => r.id));
const uniqueMemberRepos = memberRepos.filter(r => !existingIds.has(r.id));
allRepos.push(...uniqueMemberRepos);
}
// Insert repositories
const repoRecords = publicRepos.map((repo) => ({
const repoRecords = allRepos.map((repo) => ({
id: uuidv4(),
userId,
configId,
@@ -110,7 +151,7 @@ export const POST: APIRoute = async ({ request }) => {
membershipRole: role,
isIncluded: false,
status: "imported" as RepoStatus,
repositoryCount: publicRepos.length,
repositoryCount: allRepos.length,
createdAt: orgData.created_at ? new Date(orgData.created_at) : new Date(),
updatedAt: orgData.updated_at ? new Date(orgData.updated_at) : new Date(),
};