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 { Button } from "@/components/ui/button";
import { Card, CardContent, CardHeader } from "@/components/ui/card"; import { Card, CardContent, CardHeader } from "@/components/ui/card";
import { Home, ArrowLeft, GitBranch, BookOpen, Settings, FileQuestion } from "lucide-react"; 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 // Create the context value
const contextValue = { const contextValue = {
user: user as AuthUser | null, user: user as AuthUser | null,
session, session: session as Session | null,
isLoading: isLoading || betterAuthSession.isPending, isLoading: isLoading || betterAuthSession.isPending,
error, error,
login, login,

View File

@@ -1,6 +1,7 @@
import { createAuthClient } from "better-auth/react"; import { createAuthClient } from "better-auth/react";
import { oidcClient } from "better-auth/client/plugins"; import { oidcClient } from "better-auth/client/plugins";
import { ssoClient } 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({ export const authClient = createAuthClient({
// The base URL is optional when running on the same domain // The base URL is optional when running on the same domain
@@ -23,6 +24,12 @@ export const {
getSession getSession
} = authClient; } = authClient;
// Export types // Export types - directly use the types from better-auth
export type Session = Awaited<ReturnType<typeof authClient.getSession>>["data"]; export type Session = BetterAuthSession & {
export type AuthUser = Session extends { user: infer U } ? U : never; 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 forkStrategy: z
.enum(["skip", "reference", "full-copy"]) .enum(["skip", "reference", "full-copy"])
.default("reference"), .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({ export const scheduleConfigSchema = z.object({

View File

@@ -272,6 +272,9 @@ export const mirrorGithubRepoToGitea = async ({
throw new Error("Gitea username is required."); 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) // Get the correct owner based on the strategy (with organization overrides)
const repoOwner = await getGiteaRepoOwnerAsync({ config, repository }); const repoOwner = await getGiteaRepoOwnerAsync({ config, repository });
@@ -347,7 +350,7 @@ export const mirrorGithubRepoToGitea = async ({
cloneAddress = repository.cloneUrl.replace( cloneAddress = repository.cloneUrl.replace(
"https://", "https://",
`https://${config.githubConfig.token}@` `https://${decryptedConfig.githubConfig.token}@`
); );
} }
@@ -644,6 +647,9 @@ export async function mirrorGitHubRepoToGiteaOrg({
throw new Error("Gitea config is required."); throw new Error("Gitea config is required.");
} }
// Decrypt config tokens for API usage
const decryptedConfig = decryptConfigTokens(config as Config);
const isExisting = await isRepoPresentInGitea({ const isExisting = await isRepoPresentInGitea({
config, config,
owner: orgName, owner: orgName,
@@ -698,7 +704,7 @@ export async function mirrorGitHubRepoToGiteaOrg({
cloneAddress = repository.cloneUrl.replace( cloneAddress = repository.cloneUrl.replace(
"https://", "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 apiUrl = `${config.giteaConfig.url}/api/v1/repos/${actualOwner}/${repository.name}/mirror-sync`;
const response = await httpPost(apiUrl, undefined, { const response = await httpPost(apiUrl, undefined, {
Authorization: `token ${config.giteaConfig.token}`, Authorization: `token ${decryptedConfig.giteaConfig.token}`,
}); });
// Mark repo as "synced" in DB // Mark repo as "synced" in DB
@@ -1243,7 +1249,7 @@ export const mirrorGitRepoIssuesToGitea = async ({
const giteaLabelsRes = await httpGet( const giteaLabelsRes = await httpGet(
`${config.giteaConfig.url}/api/v1/repos/${giteaOwner}/${repository.name}/labels`, `${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, AdvancedOptions,
SaveConfigApiRequest SaveConfigApiRequest
} from "@/types/config"; } from "@/types/config";
import { z } from "zod";
import { githubConfigSchema, giteaConfigSchema, scheduleConfigSchema, cleanupConfigSchema } from "@/lib/db/schema";
interface DbGitHubConfig { // Use the actual database schema types
username: string; type DbGitHubConfig = z.infer<typeof githubConfigSchema>;
token?: string; type DbGiteaConfig = z.infer<typeof giteaConfigSchema>;
skipForks: boolean; type DbScheduleConfig = z.infer<typeof scheduleConfigSchema>;
privateRepositories: boolean; type DbCleanupConfig = z.infer<typeof cleanupConfigSchema>;
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;
}
/** /**
* Maps UI config structure to database schema structure * Maps UI config structure to database schema structure
@@ -48,32 +27,67 @@ export function mapUiToDbConfig(
mirrorOptions: MirrorOptions, mirrorOptions: MirrorOptions,
advancedOptions: AdvancedOptions advancedOptions: AdvancedOptions
): { githubConfig: DbGitHubConfig; giteaConfig: DbGiteaConfig } { ): { githubConfig: DbGitHubConfig; giteaConfig: DbGiteaConfig } {
// Map GitHub config with fields from mirrorOptions and advancedOptions // Map GitHub config to match database schema fields
const dbGithubConfig: DbGitHubConfig = { const dbGithubConfig: DbGitHubConfig = {
username: githubConfig.username, // Map username to owner field
token: githubConfig.token, owner: githubConfig.username,
privateRepositories: githubConfig.privateRepositories, type: "personal", // Default to personal, could be made configurable
mirrorStarred: githubConfig.mirrorStarred, token: githubConfig.token || "",
// From mirrorOptions // Map checkbox fields with proper names
mirrorIssues: mirrorOptions.mirrorMetadata && mirrorOptions.metadataComponents.issues, includeStarred: githubConfig.mirrorStarred,
mirrorWiki: mirrorOptions.mirrorMetadata && mirrorOptions.metadataComponents.wiki, 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 // Organization related fields
skipForks: advancedOptions.skipForks, includeOrganizations: [], // Not in UI yet
skipStarredIssues: advancedOptions.skipStarredIssues,
// Default values for fields not in UI // Starred repos organization
useSpecificUser: false, starredReposOrg: giteaConfig.starredReposOrg,
includeOrgs: [],
excludeOrgs: [], // Mirror strategy
mirrorPublicOrgs: false, mirrorStrategy: giteaConfig.mirrorStrategy || "preserve",
publicOrgs: [], defaultOrg: giteaConfig.organization,
}; };
// Gitea config remains mostly the same // Map Gitea config to match database schema
const dbGiteaConfig: DbGiteaConfig = { 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 { return {
@@ -91,40 +105,44 @@ export function mapDbToUiConfig(dbConfig: any): {
mirrorOptions: MirrorOptions; mirrorOptions: MirrorOptions;
advancedOptions: AdvancedOptions; advancedOptions: AdvancedOptions;
} { } {
// Map from database GitHub config to UI fields
const githubConfig: GitHubConfig = { const githubConfig: GitHubConfig = {
username: dbConfig.githubConfig?.username || "", username: dbConfig.githubConfig?.owner || "", // Map owner to username
token: dbConfig.githubConfig?.token || "", token: dbConfig.githubConfig?.token || "",
privateRepositories: dbConfig.githubConfig?.privateRepositories || false, privateRepositories: dbConfig.githubConfig?.includePrivate || false, // Map includePrivate to privateRepositories
mirrorStarred: dbConfig.githubConfig?.mirrorStarred || false, mirrorStarred: dbConfig.githubConfig?.includeStarred || false, // Map includeStarred to mirrorStarred
}; };
// Map from database Gitea config to UI fields
const giteaConfig: GiteaConfig = { const giteaConfig: GiteaConfig = {
url: dbConfig.giteaConfig?.url || "", url: dbConfig.giteaConfig?.url || "",
username: dbConfig.giteaConfig?.username || "", username: dbConfig.giteaConfig?.defaultOwner || "", // Map defaultOwner to username
token: dbConfig.giteaConfig?.token || "", token: dbConfig.giteaConfig?.token || "",
organization: dbConfig.giteaConfig?.organization || "github-mirrors", organization: dbConfig.githubConfig?.defaultOrg || "github-mirrors", // Get from GitHub config
visibility: dbConfig.giteaConfig?.visibility || "public", visibility: dbConfig.giteaConfig?.visibility === "default" ? "public" : dbConfig.giteaConfig?.visibility || "public",
starredReposOrg: dbConfig.giteaConfig?.starredReposOrg || "github", starredReposOrg: dbConfig.githubConfig?.starredReposOrg || "github", // Get from GitHub config
preserveOrgStructure: dbConfig.giteaConfig?.preserveOrgStructure || false, preserveOrgStructure: dbConfig.giteaConfig?.preserveVisibility || false, // Map preserveVisibility
mirrorStrategy: dbConfig.giteaConfig?.mirrorStrategy, mirrorStrategy: dbConfig.githubConfig?.mirrorStrategy || "preserve", // Get from GitHub config
personalReposOrg: dbConfig.giteaConfig?.personalReposOrg, personalReposOrg: undefined, // Not stored in current schema
}; };
// Map mirror options from various database fields
const mirrorOptions: MirrorOptions = { const mirrorOptions: MirrorOptions = {
mirrorReleases: false, // Not stored in DB yet mirrorReleases: dbConfig.giteaConfig?.mirrorReleases || false,
mirrorMetadata: dbConfig.githubConfig?.mirrorIssues || dbConfig.githubConfig?.mirrorWiki || false, mirrorMetadata: dbConfig.giteaConfig?.mirrorMetadata || false,
metadataComponents: { metadataComponents: {
issues: dbConfig.githubConfig?.mirrorIssues || false, issues: dbConfig.giteaConfig?.mirrorIssues || false,
pullRequests: false, // Not stored in DB yet pullRequests: dbConfig.giteaConfig?.mirrorPullRequests || false,
labels: false, // Not stored in DB yet labels: dbConfig.giteaConfig?.mirrorLabels || false,
milestones: false, // Not stored in DB yet milestones: dbConfig.giteaConfig?.mirrorMilestones || false,
wiki: dbConfig.githubConfig?.mirrorWiki || false, wiki: dbConfig.giteaConfig?.wiki || false,
}, },
}; };
// Map advanced options
const advancedOptions: AdvancedOptions = { const advancedOptions: AdvancedOptions = {
skipForks: dbConfig.githubConfig?.skipForks || false, skipForks: !(dbConfig.githubConfig?.includeForks ?? true), // Invert includeForks to get skipForks
skipStarredIssues: dbConfig.githubConfig?.skipStarredIssues || false, skipStarredIssues: false, // Not stored in current schema
}; };
return { return {
@@ -133,4 +151,73 @@ export function mapDbToUiConfig(dbConfig: any): {
mirrorOptions, mirrorOptions,
advancedOptions, 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 { eq } from "drizzle-orm";
import { calculateCleanupInterval } from "@/lib/cleanup-service"; import { calculateCleanupInterval } from "@/lib/cleanup-service";
import { createSecureErrorResponse } from "@/lib/utils"; 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"; import { encrypt, decrypt, migrateToken } from "@/lib/utils/encryption";
export const POST: APIRoute = async ({ request }) => { export const POST: APIRoute = async ({ request }) => {
@@ -78,62 +85,9 @@ export const POST: APIRoute = async ({ request }) => {
mappedGiteaConfig.token = encrypt(mappedGiteaConfig.token); mappedGiteaConfig.token = encrypt(mappedGiteaConfig.token);
} }
// Process schedule config - set/update nextRun if enabled, clear if disabled // Map schedule and cleanup configs to database schema
const processedScheduleConfig = { ...scheduleConfig }; const processedScheduleConfig = mapUiScheduleToDb(scheduleConfig);
if (scheduleConfig.enabled) { const processedCleanupConfig = mapUiCleanupToDb(cleanupConfig);
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;
}
if (existingConfig) { if (existingConfig) {
// Update path // Update path
@@ -234,28 +188,34 @@ export const GET: APIRoute = async ({ request }) => {
.limit(1); .limit(1);
if (config.length === 0) { if (config.length === 0) {
// Return a default empty configuration with UI structure // Return a default empty configuration with database structure
const defaultDbConfig = { const defaultDbConfig = {
githubConfig: { githubConfig: {
username: "", owner: "",
type: "personal",
token: "", token: "",
skipForks: false, includeStarred: false,
privateRepositories: false, includeForks: true,
mirrorIssues: false, includeArchived: false,
mirrorWiki: false, includePrivate: false,
mirrorStarred: false, includePublic: true,
useSpecificUser: false, includeOrganizations: [],
preserveOrgStructure: false, starredReposOrg: "github",
skipStarredIssues: false, mirrorStrategy: "preserve",
defaultOrg: "github-mirrors",
}, },
giteaConfig: { giteaConfig: {
url: "", url: "",
token: "", token: "",
username: "", defaultOwner: "",
organization: "github-mirrors", mirrorInterval: "8h",
lfs: false,
wiki: false,
visibility: "public", visibility: "public",
starredReposOrg: "github", createOrg: true,
preserveOrgStructure: false, addTopics: true,
preserveVisibility: false,
forkStrategy: "reference",
}, },
}; };
@@ -319,9 +279,23 @@ export const GET: APIRoute = async ({ request }) => {
const uiConfig = mapDbToUiConfig(decryptedConfig); 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({ return new Response(JSON.stringify({
...dbConfig, ...dbConfig,
...uiConfig, ...uiConfig,
scheduleConfig: {
...uiScheduleConfig,
lastRun: dbConfig.scheduleConfig.lastRun,
nextRun: dbConfig.scheduleConfig.nextRun,
},
cleanupConfig: {
...uiCleanupConfig,
lastRun: dbConfig.cleanupConfig.lastRun,
nextRun: dbConfig.cleanupConfig.nextRun,
},
}), { }), {
status: 200, status: 200,
headers: { "Content-Type": "application/json" }, headers: { "Content-Type": "application/json" },
@@ -330,9 +304,22 @@ export const GET: APIRoute = async ({ request }) => {
console.error("Failed to decrypt tokens:", error); console.error("Failed to decrypt tokens:", error);
// Return config without decrypting tokens if there's an error // Return config without decrypting tokens if there's an error
const uiConfig = mapDbToUiConfig(dbConfig); const uiConfig = mapDbToUiConfig(dbConfig);
const uiScheduleConfig = mapDbScheduleToUi(dbConfig.scheduleConfig);
const uiCleanupConfig = mapDbCleanupToUi(dbConfig.cleanupConfig);
return new Response(JSON.stringify({ return new Response(JSON.stringify({
...dbConfig, ...dbConfig,
...uiConfig, ...uiConfig,
scheduleConfig: {
...uiScheduleConfig,
lastRun: dbConfig.scheduleConfig.lastRun,
nextRun: dbConfig.scheduleConfig.nextRun,
},
cleanupConfig: {
...uiCleanupConfig,
lastRun: dbConfig.cleanupConfig.lastRun,
nextRun: dbConfig.cleanupConfig.nextRun,
},
}), { }), {
status: 200, status: 200,
headers: { "Content-Type": "application/json" }, headers: { "Content-Type": "application/json" },

View File

@@ -45,17 +45,10 @@ export const GET: APIRoute = async ({ request }) => {
// Build query conditions based on config // Build query conditions based on config
const conditions = [eq(repositories.userId, userId)]; const conditions = [eq(repositories.userId, userId)];
if (!githubConfig.mirrorStarred) { // Note: We show ALL repositories in the list
conditions.push(eq(repositories.isStarred, false)); // 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
if (githubConfig.skipForks) {
conditions.push(eq(repositories.isForked, false));
}
if (!githubConfig.privateRepositories) {
conditions.push(eq(repositories.isPrivate, false));
}
const rawRepositories = await db const rawRepositories = await db
.select() .select()

View File

@@ -9,6 +9,8 @@ import type {
} from "@/types/organizations"; } from "@/types/organizations";
import type { RepositoryVisibility, RepoStatus } from "@/types/Repository"; import type { RepositoryVisibility, RepoStatus } from "@/types/Repository";
import { v4 as uuidv4 } from "uuid"; import { v4 as uuidv4 } from "uuid";
import { decryptConfigTokens } from "@/lib/utils/config-encryption";
import { createGitHubClient } from "@/lib/github";
export const POST: APIRoute = async ({ request }) => { export const POST: APIRoute = async ({ request }) => {
try { try {
@@ -44,32 +46,71 @@ export const POST: APIRoute = async ({ request }) => {
const [config] = await db const [config] = await db
.select() .select()
.from(configs) .from(configs)
.where(eq(configs.userId, userId)) .where(and(eq(configs.userId, userId), eq(configs.isActive, true)))
.limit(1); .limit(1);
if (!config) { if (!config) {
return jsonResponse({ return jsonResponse({
data: { error: "No configuration found for this user" }, data: { error: "No active configuration found for this user" },
status: 404, status: 404,
}); });
} }
const configId = config.id; 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 // Fetch org metadata
const { data: orgData } = await octokit.orgs.get({ org }); 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, { const publicRepos = await octokit.paginate(octokit.repos.listForOrg, {
org, org,
type: "public", type: "public",
per_page: 100, 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 // Insert repositories
const repoRecords = publicRepos.map((repo) => ({ const repoRecords = allRepos.map((repo) => ({
id: uuidv4(), id: uuidv4(),
userId, userId,
configId, configId,
@@ -110,7 +151,7 @@ export const POST: APIRoute = async ({ request }) => {
membershipRole: role, membershipRole: role,
isIncluded: false, isIncluded: false,
status: "imported" as RepoStatus, status: "imported" as RepoStatus,
repositoryCount: publicRepos.length, repositoryCount: allRepos.length,
createdAt: orgData.created_at ? new Date(orgData.created_at) : new Date(), createdAt: orgData.created_at ? new Date(orgData.created_at) : new Date(),
updatedAt: orgData.updated_at ? new Date(orgData.updated_at) : new Date(), updatedAt: orgData.updated_at ? new Date(orgData.updated_at) : new Date(),
}; };