import type { APIRoute } from "astro"; import { Octokit } from "@octokit/rest"; import { configs, db, organizations, repositories } from "@/lib/db"; import { and, eq } from "drizzle-orm"; import { jsonResponse, createSecureErrorResponse } from "@/lib/utils"; import type { AddOrganizationApiRequest, AddOrganizationApiResponse, } 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 { const body: AddOrganizationApiRequest = await request.json(); const { role, org, userId } = body; if (!org || !userId || !role) { return jsonResponse({ data: { success: false, error: "Missing org, role or userId" }, status: 400, }); } // Check if org already exists const existingOrg = await db .select() .from(organizations) .where( and(eq(organizations.name, org), eq(organizations.userId, userId)) ); if (existingOrg.length > 0) { return jsonResponse({ data: { success: false, error: "Organization already exists for this user", }, status: 400, }); } // Get user's config const [config] = await db .select() .from(configs) .where(and(eq(configs.userId, userId), eq(configs.isActive, true))) .limit(1); if (!config) { return jsonResponse({ data: { error: "No active configuration found for this user" }, status: 404, }); } const configId = config.id; // 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 with rate limit tracking const githubUsername = decryptedConfig.githubConfig?.owner || undefined; const octokit = createGitHubClient(decryptedConfig.githubConfig.token, userId, githubUsername); // Fetch org metadata const { data: orgData } = await octokit.orgs.get({ org }); // Fetch repos based on config settings const allRepos = []; // Fetch all repos (public, private, and member) to show in UI const publicRepos = await octokit.paginate(octokit.repos.listForOrg, { org, type: "public", per_page: 100, }); allRepos.push(...publicRepos); // Always fetch private repos to show them in the UI 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) 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 = allRepos.map((repo) => ({ id: uuidv4(), userId, configId, name: repo.name, fullName: repo.full_name, url: repo.html_url, cloneUrl: repo.clone_url ?? "", owner: repo.owner.login, organization: repo.owner.type === "Organization" ? repo.owner.login : null, mirroredLocation: "", destinationOrg: null, isPrivate: repo.private, isForked: repo.fork, forkedFrom: null, hasIssues: repo.has_issues, isStarred: false, isArchived: repo.archived, size: repo.size, hasLFS: false, hasSubmodules: false, language: repo.language ?? null, description: repo.description ?? null, defaultBranch: repo.default_branch ?? "main", visibility: (repo.visibility ?? "public") as RepositoryVisibility, status: "imported" as RepoStatus, lastMirrored: null, errorMessage: null, createdAt: repo.created_at ? new Date(repo.created_at) : new Date(), updatedAt: repo.updated_at ? new Date(repo.updated_at) : new Date(), })); // Batch insert repositories to avoid SQLite parameter limit // Compute batch size based on column count const sample = repoRecords[0]; const columnCount = Object.keys(sample ?? {}).length || 1; const BATCH_SIZE = Math.max(1, Math.floor(999 / columnCount)); for (let i = 0; i < repoRecords.length; i += BATCH_SIZE) { const batch = repoRecords.slice(i, i + BATCH_SIZE); await db .insert(repositories) .values(batch) .onConflictDoNothing({ target: [repositories.userId, repositories.fullName] }); } // Insert organization metadata const organizationRecord = { id: uuidv4(), userId, configId, name: orgData.login, avatarUrl: orgData.avatar_url, membershipRole: role, isIncluded: false, status: "imported" as RepoStatus, 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(), }; await db.insert(organizations).values(organizationRecord); const resPayload: AddOrganizationApiResponse = { success: true, organization: organizationRecord, message: "Organization and repositories imported successfully", }; return jsonResponse({ data: resPayload, status: 200 }); } catch (error) { return createSecureErrorResponse(error, "organization sync", 500); } };