From e6a31512ac2c83d3c9b39f6cf7d9bde9e8c00e9e Mon Sep 17 00:00:00 2001 From: Arunavo Ray Date: Thu, 17 Jul 2025 23:31:45 +0530 Subject: [PATCH] some more fixes --- src/components/NotFound.tsx | 1 - src/hooks/useAuth.ts | 2 +- src/lib/auth-client.ts | 13 +- src/lib/db/schema.ts | 7 + src/lib/gitea.ts | 14 +- src/lib/utils/config-mapper.ts | 219 +++++++++++++++++++-------- src/pages/api/config/index.ts | 129 +++++++--------- src/pages/api/github/repositories.ts | 15 +- src/pages/api/sync/organization.ts | 55 ++++++- 9 files changed, 291 insertions(+), 164 deletions(-) diff --git a/src/components/NotFound.tsx b/src/components/NotFound.tsx index fc92d33..8dee1cb 100644 --- a/src/components/NotFound.tsx +++ b/src/components/NotFound.tsx @@ -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"; diff --git a/src/hooks/useAuth.ts b/src/hooks/useAuth.ts index 7bab564..b1a1200 100644 --- a/src/hooks/useAuth.ts +++ b/src/hooks/useAuth.ts @@ -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, diff --git a/src/lib/auth-client.ts b/src/lib/auth-client.ts index 07424cc..33cd44f 100644 --- a/src/lib/auth-client.ts +++ b/src/lib/auth-client.ts @@ -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>["data"]; -export type AuthUser = Session extends { user: infer U } ? U : never; \ No newline at end of file +// 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; +}; \ No newline at end of file diff --git a/src/lib/db/schema.ts b/src/lib/db/schema.ts index 35dc62d..c061ce8 100644 --- a/src/lib/db/schema.ts +++ b/src/lib/db/schema.ts @@ -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({ diff --git a/src/lib/gitea.ts b/src/lib/gitea.ts index d10a195..e2e5920 100644 --- a/src/lib/gitea.ts +++ b/src/lib/gitea.ts @@ -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}`, } ); diff --git a/src/lib/utils/config-mapper.ts b/src/lib/utils/config-mapper.ts index 45f3511..59cf15a 100644 --- a/src/lib/utils/config-mapper.ts +++ b/src/lib/utils/config-mapper.ts @@ -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; +type DbGiteaConfig = z.infer; +type DbScheduleConfig = z.infer; +type DbCleanupConfig = z.infer; /** * 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) + }; } \ No newline at end of file diff --git a/src/pages/api/config/index.ts b/src/pages/api/config/index.ts index 0465f43..7176489 100644 --- a/src/pages/api/config/index.ts +++ b/src/pages/api/config/index.ts @@ -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" }, diff --git a/src/pages/api/github/repositories.ts b/src/pages/api/github/repositories.ts index 4cee8ee..fced8d3 100644 --- a/src/pages/api/github/repositories.ts +++ b/src/pages/api/github/repositories.ts @@ -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() diff --git a/src/pages/api/sync/organization.ts b/src/pages/api/sync/organization.ts index 7b3ac10..6d97965 100644 --- a/src/pages/api/sync/organization.ts +++ b/src/pages/api/sync/organization.ts @@ -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(), };