🎉 Gitea Mirror: Added

This commit is contained in:
Arunavo Ray
2025-05-18 09:31:23 +05:30
commit 5d40023de0
139 changed files with 22033 additions and 0 deletions

View File

@@ -0,0 +1,58 @@
import type { APIRoute } from "astro";
import { db, mirrorJobs, configs } from "@/lib/db";
import { eq, sql } from "drizzle-orm";
import type { MirrorJob } from "@/lib/db/schema";
import { repoStatusEnum } from "@/types/Repository";
export const GET: APIRoute = async ({ url }) => {
try {
const searchParams = new URL(url).searchParams;
const userId = searchParams.get("userId");
if (!userId) {
return new Response(
JSON.stringify({ error: "Missing 'userId' in query parameters." }),
{ status: 400, headers: { "Content-Type": "application/json" } }
);
}
// Fetch mirror jobs associated with the user
const jobs = await db
.select()
.from(mirrorJobs)
.where(eq(mirrorJobs.userId, userId))
.orderBy(sql`${mirrorJobs.timestamp} DESC`);
const activities: MirrorJob[] = jobs.map((job) => ({
id: job.id,
userId: job.userId,
repositoryId: job.repositoryId ?? undefined,
repositoryName: job.repositoryName ?? undefined,
organizationId: job.organizationId ?? undefined,
organizationName: job.organizationName ?? undefined,
status: repoStatusEnum.parse(job.status),
details: job.details ?? undefined,
message: job.message,
timestamp: job.timestamp,
}));
return new Response(
JSON.stringify({
success: true,
message: "Mirror job activities retrieved successfully.",
activities,
}),
{ status: 200, headers: { "Content-Type": "application/json" } }
);
} catch (error) {
console.error("Error fetching mirror job activities:", error);
return new Response(
JSON.stringify({
success: false,
error:
error instanceof Error ? error.message : "An unknown error occurred.",
}),
{ status: 500, headers: { "Content-Type": "application/json" } }
);
}
};

View File

@@ -0,0 +1,83 @@
import type { APIRoute } from "astro";
import { db, users, configs, client } from "@/lib/db";
import { eq, and } from "drizzle-orm";
import jwt from "jsonwebtoken";
const JWT_SECRET = process.env.JWT_SECRET || "your-secret-key";
export const GET: APIRoute = async ({ request, cookies }) => {
const authHeader = request.headers.get("Authorization");
const token = authHeader?.split(" ")[1] || cookies.get("token")?.value;
if (!token) {
const userCountResult = await client.execute(
`SELECT COUNT(*) as count FROM users`
);
const userCount = userCountResult.rows[0].count;
if (userCount === 0) {
return new Response(JSON.stringify({ error: "No users found" }), {
status: 404,
headers: { "Content-Type": "application/json" },
});
}
return new Response(JSON.stringify({ error: "Unauthorized" }), {
status: 401,
headers: { "Content-Type": "application/json" },
});
}
try {
const decoded = jwt.verify(token, JWT_SECRET) as { id: string };
const userResult = await db
.select()
.from(users)
.where(eq(users.id, decoded.id))
.limit(1);
if (!userResult.length) {
return new Response(JSON.stringify({ error: "User not found" }), {
status: 404,
headers: { "Content-Type": "application/json" },
});
}
const { password, ...userWithoutPassword } = userResult[0];
const configResult = await db
.select({
scheduleConfig: configs.scheduleConfig,
})
.from(configs)
.where(and(eq(configs.userId, decoded.id), eq(configs.isActive, true)))
.limit(1);
const scheduleConfig = configResult[0]?.scheduleConfig;
const syncEnabled = scheduleConfig?.enabled ?? false;
const syncInterval = scheduleConfig?.interval ?? 3600;
const lastSync = scheduleConfig?.lastRun ?? null;
const nextSync = scheduleConfig?.nextRun ?? null;
return new Response(
JSON.stringify({
...userWithoutPassword,
syncEnabled,
syncInterval,
lastSync,
nextSync,
}),
{
status: 200,
headers: { "Content-Type": "application/json" },
}
);
} catch (error) {
return new Response(JSON.stringify({ error: "Invalid token" }), {
status: 401,
headers: { "Content-Type": "application/json" },
});
}
};

View File

@@ -0,0 +1,62 @@
import type { APIRoute } from "astro";
import bcrypt from "bcryptjs";
import jwt from "jsonwebtoken";
import { db, users } from "@/lib/db";
import { eq } from "drizzle-orm";
const JWT_SECRET = process.env.JWT_SECRET || "your-secret-key";
export const POST: APIRoute = async ({ request }) => {
const { username, password } = await request.json();
if (!username || !password) {
return new Response(
JSON.stringify({ error: "Username and password are required" }),
{
status: 400,
headers: { "Content-Type": "application/json" },
}
);
}
const user = await db
.select()
.from(users)
.where(eq(users.username, username))
.limit(1);
if (!user.length) {
return new Response(
JSON.stringify({ error: "Invalid username or password" }),
{
status: 401,
headers: { "Content-Type": "application/json" },
}
);
}
const isPasswordValid = await bcrypt.compare(password, user[0].password);
if (!isPasswordValid) {
return new Response(
JSON.stringify({ error: "Invalid username or password" }),
{
status: 401,
headers: { "Content-Type": "application/json" },
}
);
}
const { password: _, ...userWithoutPassword } = user[0];
const token = jwt.sign({ id: user[0].id }, JWT_SECRET, { expiresIn: "7d" });
return new Response(JSON.stringify({ token, user: userWithoutPassword }), {
status: 200,
headers: {
"Content-Type": "application/json",
"Set-Cookie": `token=${token}; Path=/; HttpOnly; SameSite=Strict; Max-Age=${
60 * 60 * 24 * 7
}`,
},
});
};

View File

@@ -0,0 +1,11 @@
import type { APIRoute } from "astro";
export const POST: APIRoute = async () => {
return new Response(JSON.stringify({ success: true }), {
status: 200,
headers: {
"Content-Type": "application/json",
"Set-Cookie": "token=; Path=/; HttpOnly; SameSite=Strict; Max-Age=0",
},
});
};

View File

@@ -0,0 +1,72 @@
import type { APIRoute } from "astro";
import bcrypt from "bcryptjs";
import jwt from "jsonwebtoken";
import { db, users } from "@/lib/db";
import { eq, or } from "drizzle-orm";
const JWT_SECRET = process.env.JWT_SECRET || "your-secret-key";
export const POST: APIRoute = async ({ request }) => {
const { username, email, password } = await request.json();
if (!username || !email || !password) {
return new Response(
JSON.stringify({ error: "Username, email, and password are required" }),
{
status: 400,
headers: { "Content-Type": "application/json" },
}
);
}
// Check if username or email already exists
const existingUser = await db
.select()
.from(users)
.where(or(eq(users.username, username), eq(users.email, email)))
.limit(1);
if (existingUser.length) {
return new Response(
JSON.stringify({ error: "Username or email already exists" }),
{
status: 409,
headers: { "Content-Type": "application/json" },
}
);
}
// Hash password
const hashedPassword = await bcrypt.hash(password, 10);
// Generate UUID
const id = crypto.randomUUID();
// Create user
const newUser = await db
.insert(users)
.values({
id,
username,
email,
password: hashedPassword,
createdAt: new Date(),
updatedAt: new Date(),
})
.returning();
const { password: _, ...userWithoutPassword } = newUser[0];
const token = jwt.sign({ id: newUser[0].id }, JWT_SECRET, {
expiresIn: "7d",
});
return new Response(JSON.stringify({ token, user: userWithoutPassword }), {
status: 201,
headers: {
"Content-Type": "application/json",
"Set-Cookie": `token=${token}; Path=/; HttpOnly; SameSite=Strict; Max-Age=${
60 * 60 * 24 * 7
}`,
},
});
};

View File

@@ -0,0 +1,225 @@
import type { APIRoute } from "astro";
import { db, configs, users } from "@/lib/db";
import { v4 as uuidv4 } from "uuid";
import { eq } from "drizzle-orm";
export const POST: APIRoute = async ({ request }) => {
try {
const body = await request.json();
const { userId, githubConfig, giteaConfig, scheduleConfig } = body;
if (!userId || !githubConfig || !giteaConfig || !scheduleConfig) {
return new Response(
JSON.stringify({
success: false,
message:
"userId, githubConfig, giteaConfig, and scheduleConfig 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];
// 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;
if (!githubConfig.token && existingGithub.token) {
githubConfig.token = existingGithub.token;
}
if (!giteaConfig.token && existingGitea.token) {
giteaConfig.token = existingGitea.token;
}
} catch (tokenError) {
console.error("Failed to preserve tokens:", tokenError);
}
}
if (existingConfig) {
// Update path
await db
.update(configs)
.set({
githubConfig,
giteaConfig,
scheduleConfig,
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,
giteaConfig,
include: [],
exclude: [],
scheduleConfig,
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) {
console.error("Error saving configuration:", error);
return new Response(
JSON.stringify({
success: false,
message:
"Error saving configuration: " +
(error instanceof Error ? error.message : "Unknown error"),
}),
{
status: 500,
headers: { "Content-Type": "application/json" },
}
);
}
};
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) {
// Return a default empty configuration instead of a 404 error
return new Response(
JSON.stringify({
id: null,
userId: userId,
name: "Default Configuration",
isActive: true,
githubConfig: {
username: "",
token: "",
skipForks: false,
privateRepositories: false,
mirrorIssues: false,
mirrorStarred: true,
useSpecificUser: false,
preserveOrgStructure: true,
skipStarredIssues: false,
},
giteaConfig: {
url: "",
token: "",
username: "",
organization: "github-mirrors",
visibility: "public",
starredReposOrg: "github",
},
scheduleConfig: {
enabled: false,
interval: 3600,
lastRun: null,
nextRun: null,
},
}),
{
status: 200,
headers: { "Content-Type": "application/json" },
}
);
}
return new Response(JSON.stringify(config[0]), {
status: 200,
headers: { "Content-Type": "application/json" },
});
} catch (error) {
console.error("Error fetching configuration:", error);
return new Response(
JSON.stringify({
error: error instanceof Error ? error.message : "Something went wrong",
}),
{
status: 500,
headers: { "Content-Type": "application/json" },
}
);
}
};

View File

@@ -0,0 +1,122 @@
import type { APIRoute } from "astro";
import { db, repositories, organizations, mirrorJobs, configs } from "@/lib/db";
import { eq, count, and, sql, or } from "drizzle-orm";
import { jsonResponse } from "@/lib/utils";
import type { DashboardApiResponse } from "@/types/dashboard";
import { repositoryVisibilityEnum, repoStatusEnum } from "@/types/Repository";
import { membershipRoleEnum } from "@/types/organizations";
export const GET: APIRoute = async ({ request }) => {
const url = new URL(request.url);
const userId = url.searchParams.get("userId");
if (!userId) {
return jsonResponse({
data: {
success: false,
error: "Missing userId",
},
status: 400,
});
}
try {
const [
userRepos,
userOrgs,
userLogs,
[userConfig],
[{ value: repoCount }],
[{ value: orgCount }],
[{ value: mirroredCount }],
] = await Promise.all([
db
.select()
.from(repositories)
.where(eq(repositories.userId, userId))
.orderBy(sql`${repositories.updatedAt} DESC`)
.limit(10),
db
.select()
.from(organizations)
.where(eq(organizations.userId, userId))
.orderBy(sql`${organizations.updatedAt} DESC`)
.limit(10), // not really needed in the frontend but just in case
db
.select()
.from(mirrorJobs)
.where(eq(mirrorJobs.userId, userId))
.orderBy(sql`${mirrorJobs.timestamp} DESC`)
.limit(10),
db.select().from(configs).where(eq(configs.userId, userId)).limit(1),
db
.select({ value: count() })
.from(repositories)
.where(eq(repositories.userId, userId)),
db
.select({ value: count() })
.from(organizations)
.where(eq(organizations.userId, userId)),
db
.select({ value: count() })
.from(repositories)
.where(
and(
eq(repositories.userId, userId),
or(
eq(repositories.status, "mirrored"),
eq(repositories.status, "synced")
)
)
),
]);
const successResponse: DashboardApiResponse = {
success: true,
message: "Dashboard data loaded successfully",
repoCount: repoCount ?? 0,
orgCount: orgCount ?? 0,
mirroredCount: mirroredCount ?? 0,
repositories: userRepos.map((repo) => ({
...repo,
organization: repo.organization ?? undefined,
lastMirrored: repo.lastMirrored ?? undefined,
errorMessage: repo.errorMessage ?? undefined,
forkedFrom: repo.forkedFrom ?? undefined,
status: repoStatusEnum.parse(repo.status),
visibility: repositoryVisibilityEnum.parse(repo.visibility),
})),
organizations: userOrgs.map((org) => ({
...org,
status: repoStatusEnum.parse(org.status),
membershipRole: membershipRoleEnum.parse(org.membershipRole),
lastMirrored: org.lastMirrored ?? undefined,
errorMessage: org.errorMessage ?? undefined,
})),
activities: userLogs.map((job) => ({
id: job.id,
userId: job.userId,
repositoryName: job.repositoryName ?? undefined,
organizationName: job.organizationName ?? undefined,
status: repoStatusEnum.parse(job.status),
details: job.details ?? undefined,
message: job.message,
timestamp: job.timestamp,
})),
lastSync: userConfig?.scheduleConfig.lastRun ?? null,
};
return jsonResponse({ data: successResponse });
} catch (error) {
console.error("Error loading dashboard for user:", userId, error);
return jsonResponse({
data: {
success: false,
error: error instanceof Error ? error.message : "Internal server error",
message: "Failed to fetch dashboard data",
},
status: 500,
});
}
};

View File

@@ -0,0 +1,135 @@
import type { APIRoute } from 'astro';
import axios from 'axios';
export const POST: APIRoute = async ({ request }) => {
try {
const body = await request.json();
const { url, token, username } = body;
if (!url || !token) {
return new Response(
JSON.stringify({
success: false,
message: 'Gitea URL and token are required',
}),
{
status: 400,
headers: {
'Content-Type': 'application/json',
},
}
);
}
// Normalize the URL (remove trailing slash if present)
const baseUrl = url.endsWith('/') ? url.slice(0, -1) : url;
// Test the connection by fetching the authenticated user
const response = await axios.get(`${baseUrl}/api/v1/user`, {
headers: {
'Authorization': `token ${token}`,
'Accept': 'application/json',
},
});
const data = response.data;
// Verify that the authenticated user matches the provided username (if provided)
if (username && data.login !== username) {
return new Response(
JSON.stringify({
success: false,
message: `Token belongs to ${data.login}, not ${username}`,
}),
{
status: 400,
headers: {
'Content-Type': 'application/json',
},
}
);
}
// Return success response with user data
return new Response(
JSON.stringify({
success: true,
message: `Successfully connected to Gitea as ${data.login}`,
user: {
login: data.login,
name: data.full_name,
avatar_url: data.avatar_url,
},
}),
{
status: 200,
headers: {
'Content-Type': 'application/json',
},
}
);
} catch (error) {
console.error('Gitea connection test failed:', error);
// Handle specific error types
if (axios.isAxiosError(error) && error.response) {
if (error.response.status === 401) {
return new Response(
JSON.stringify({
success: false,
message: 'Invalid Gitea token',
}),
{
status: 401,
headers: {
'Content-Type': 'application/json',
},
}
);
} else if (error.response.status === 404) {
return new Response(
JSON.stringify({
success: false,
message: 'Gitea API endpoint not found. Please check the URL.',
}),
{
status: 404,
headers: {
'Content-Type': 'application/json',
},
}
);
}
}
// Handle connection errors
if (axios.isAxiosError(error) && (error.code === 'ECONNREFUSED' || error.code === 'ENOTFOUND')) {
return new Response(
JSON.stringify({
success: false,
message: 'Could not connect to Gitea server. Please check the URL.',
}),
{
status: 500,
headers: {
'Content-Type': 'application/json',
},
}
);
}
// Generic error response
return new Response(
JSON.stringify({
success: false,
message: `Gitea connection test failed: ${error instanceof Error ? error.message : 'Unknown error'}`,
}),
{
status: 500,
headers: {
'Content-Type': 'application/json',
},
}
);
}
};

View File

@@ -0,0 +1,60 @@
import type { APIRoute } from "astro";
import { db } from "@/lib/db";
import { organizations } from "@/lib/db";
import { eq, sql } from "drizzle-orm";
import {
membershipRoleEnum,
type OrganizationsApiResponse,
} from "@/types/organizations";
import type { Organization } from "@/lib/db/schema";
import { repoStatusEnum } from "@/types/Repository";
import { jsonResponse } from "@/lib/utils";
export const GET: APIRoute = async ({ request }) => {
const url = new URL(request.url);
const userId = url.searchParams.get("userId");
if (!userId) {
return jsonResponse({
data: {
success: false,
error: "Missing userId",
},
status: 400,
});
}
try {
const rawOrgs = await db
.select()
.from(organizations)
.where(eq(organizations.userId, userId))
.orderBy(sql`name COLLATE NOCASE`);
const orgsWithIds: Organization[] = rawOrgs.map((org) => ({
...org,
status: repoStatusEnum.parse(org.status),
membershipRole: membershipRoleEnum.parse(org.membershipRole),
lastMirrored: org.lastMirrored ?? undefined,
errorMessage: org.errorMessage ?? undefined,
}));
const resPayload: OrganizationsApiResponse = {
success: true,
message: "Organizations fetched successfully",
organizations: orgsWithIds,
};
return jsonResponse({ data: resPayload, status: 200 });
} catch (error) {
console.error("Error fetching organizations:", error);
return jsonResponse({
data: {
success: false,
error: error instanceof Error ? error.message : "Something went wrong",
},
status: 500,
});
}
};

View File

@@ -0,0 +1,96 @@
import type { APIRoute } from "astro";
import { db, repositories, configs } from "@/lib/db";
import { and, eq, sql } from "drizzle-orm";
import {
repositoryVisibilityEnum,
repoStatusEnum,
type RepositoryApiResponse,
} from "@/types/Repository";
import { jsonResponse } from "@/lib/utils";
export const GET: APIRoute = async ({ request }) => {
const url = new URL(request.url);
const userId = url.searchParams.get("userId");
if (!userId) {
return jsonResponse({
data: { success: false, error: "Missing userId" },
status: 400,
});
}
try {
// Fetch the user's active configuration
const [config] = await db
.select()
.from(configs)
.where(and(eq(configs.userId, userId), eq(configs.isActive, true)));
if (!config) {
return jsonResponse({
data: {
success: false,
error: "No active configuration found for this user",
},
status: 404,
});
}
const githubConfig = config.githubConfig as {
mirrorStarred: boolean;
skipForks: boolean;
privateRepositories: boolean;
};
// 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));
}
const rawRepositories = await db
.select()
.from(repositories)
.where(and(...conditions))
.orderBy(sql`name COLLATE NOCASE`);
const response: RepositoryApiResponse = {
success: true,
message: "Repositories fetched successfully",
repositories: rawRepositories.map((repo) => ({
...repo,
organization: repo.organization ?? undefined,
lastMirrored: repo.lastMirrored ?? undefined,
errorMessage: repo.errorMessage ?? undefined,
forkedFrom: repo.forkedFrom ?? undefined,
status: repoStatusEnum.parse(repo.status),
visibility: repositoryVisibilityEnum.parse(repo.visibility),
})),
};
return jsonResponse({
data: response,
status: 200,
});
} catch (error) {
console.error("Error fetching repositories:", error);
return jsonResponse({
data: {
success: false,
error: error instanceof Error ? error.message : "Something went wrong",
message: "An error occurred while fetching repositories.",
},
status: 500,
});
}
};

View File

@@ -0,0 +1,101 @@
import type { APIRoute } from "astro";
import { Octokit } from "@octokit/rest";
export const POST: APIRoute = async ({ request }) => {
try {
const body = await request.json();
const { token, username } = body;
if (!token) {
return new Response(
JSON.stringify({
success: false,
message: "GitHub token is required",
}),
{
status: 400,
headers: {
"Content-Type": "application/json",
},
}
);
}
// Create an Octokit instance with the provided token
const octokit = new Octokit({
auth: token,
});
// Test the connection by fetching the authenticated user
const { data } = await octokit.users.getAuthenticated();
// Verify that the authenticated user matches the provided username (if provided)
if (username && data.login !== username) {
return new Response(
JSON.stringify({
success: false,
message: `Token belongs to ${data.login}, not ${username}`,
}),
{
status: 400,
headers: {
"Content-Type": "application/json",
},
}
);
}
// Return success response with user data
return new Response(
JSON.stringify({
success: true,
message: `Successfully connected to GitHub as ${data.login}`,
user: {
login: data.login,
name: data.name,
avatar_url: data.avatar_url,
},
}),
{
status: 200,
headers: {
"Content-Type": "application/json",
},
}
);
} catch (error) {
console.error("GitHub connection test failed:", error);
// Handle specific error types
if (error instanceof Error && (error as any).status === 401) {
return new Response(
JSON.stringify({
success: false,
message: "Invalid GitHub token",
}),
{
status: 401,
headers: {
"Content-Type": "application/json",
},
}
);
}
// Generic error response
return new Response(
JSON.stringify({
success: false,
message: `GitHub connection test failed: ${
error instanceof Error ? error.message : "Unknown error"
}`,
}),
{
status: 500,
headers: {
"Content-Type": "application/json",
},
}
);
}
};

View File

@@ -0,0 +1,118 @@
import type { APIRoute } from "astro";
import type { MirrorOrgRequest, MirrorOrgResponse } from "@/types/mirror";
import { db, configs, organizations } from "@/lib/db";
import { eq, inArray } from "drizzle-orm";
import { createGitHubClient } from "@/lib/github";
import { mirrorGitHubOrgToGitea } from "@/lib/gitea";
import { repoStatusEnum } from "@/types/Repository";
import { type MembershipRole } from "@/types/organizations";
export const POST: APIRoute = async ({ request }) => {
try {
const body: MirrorOrgRequest = await request.json();
const { userId, organizationIds } = body;
if (!userId || !organizationIds || !Array.isArray(organizationIds)) {
return new Response(
JSON.stringify({
success: false,
message: "userId and organizationIds are required.",
}),
{ status: 400, headers: { "Content-Type": "application/json" } }
);
}
if (organizationIds.length === 0) {
return new Response(
JSON.stringify({
success: false,
message: "No organization IDs provided.",
}),
{ status: 400, headers: { "Content-Type": "application/json" } }
);
}
// Fetch config
const configResult = await db
.select()
.from(configs)
.where(eq(configs.userId, userId))
.limit(1);
const config = configResult[0];
if (!config || !config.githubConfig.token) {
return new Response(
JSON.stringify({ error: "Config missing for the user or token." }),
{ status: 400, headers: { "Content-Type": "application/json" } }
);
}
// Fetch organizations
const orgs = await db
.select()
.from(organizations)
.where(inArray(organizations.id, organizationIds));
if (!orgs.length) {
return new Response(
JSON.stringify({ error: "No organizations found for the given IDs." }),
{ status: 404, headers: { "Content-Type": "application/json" } }
);
}
// Fire async mirroring without blocking response
setTimeout(async () => {
for (const org of orgs) {
if (!config.githubConfig.token) {
throw new Error("GitHub token is missing in config.");
}
const octokit = createGitHubClient(config.githubConfig.token);
try {
await mirrorGitHubOrgToGitea({
config,
octokit,
organization: {
...org,
status: repoStatusEnum.parse("imported"),
membershipRole: org.membershipRole as MembershipRole,
lastMirrored: org.lastMirrored ?? undefined,
errorMessage: org.errorMessage ?? undefined,
},
});
} catch (error) {
console.error(`Mirror failed for organization ${org.name}:`, error);
}
}
}, 0);
const responsePayload: MirrorOrgResponse = {
success: true,
message: "Mirror job started.",
organizations: orgs.map((org) => ({
...org,
status: repoStatusEnum.parse(org.status),
membershipRole: org.membershipRole as MembershipRole,
lastMirrored: org.lastMirrored ?? undefined,
errorMessage: org.errorMessage ?? undefined,
})),
};
// Immediate response
return new Response(JSON.stringify(responsePayload), {
status: 200,
headers: { "Content-Type": "application/json" },
});
} catch (error) {
console.error("Error in mirroring organization:", error);
return new Response(
JSON.stringify({
error:
error instanceof Error ? error.message : "An unknown error occurred.",
}),
{ status: 500, headers: { "Content-Type": "application/json" } }
);
}
};

View File

@@ -0,0 +1,144 @@
import type { APIRoute } from "astro";
import type { MirrorRepoRequest, MirrorRepoResponse } from "@/types/mirror";
import { db, configs, repositories } from "@/lib/db";
import { eq, inArray } from "drizzle-orm";
import { repositoryVisibilityEnum, repoStatusEnum } from "@/types/Repository";
import {
mirrorGithubRepoToGitea,
mirrorGitHubOrgRepoToGiteaOrg,
} from "@/lib/gitea";
import { createGitHubClient } from "@/lib/github";
export const POST: APIRoute = async ({ request }) => {
try {
const body: MirrorRepoRequest = await request.json();
const { userId, repositoryIds } = body;
if (!userId || !repositoryIds || !Array.isArray(repositoryIds)) {
return new Response(
JSON.stringify({
success: false,
message: "userId and repositoryIds are required.",
}),
{ status: 400, headers: { "Content-Type": "application/json" } }
);
}
if (repositoryIds.length === 0) {
return new Response(
JSON.stringify({
success: false,
message: "No repository IDs provided.",
}),
{ status: 400, headers: { "Content-Type": "application/json" } }
);
}
// Fetch config
const configResult = await db
.select()
.from(configs)
.where(eq(configs.userId, userId))
.limit(1);
const config = configResult[0];
if (!config || !config.githubConfig.token) {
return new Response(
JSON.stringify({ error: "Config missing for the user or token." }),
{ status: 400, headers: { "Content-Type": "application/json" } }
);
}
// Fetch repos
const repos = await db
.select()
.from(repositories)
.where(inArray(repositories.id, repositoryIds));
if (!repos.length) {
return new Response(
JSON.stringify({ error: "No repositories found for the given IDs." }),
{ status: 404, headers: { "Content-Type": "application/json" } }
);
}
// Start async mirroring in background
setTimeout(async () => {
for (const repo of repos) {
if (!config.githubConfig.token) {
throw new Error("GitHub token is missing.");
}
const octokit = createGitHubClient(config.githubConfig.token);
try {
if (repo.organization && config.githubConfig.preserveOrgStructure) {
await mirrorGitHubOrgRepoToGiteaOrg({
config,
octokit,
orgName: repo.organization,
repository: {
...repo,
status: repoStatusEnum.parse("imported"),
organization: repo.organization ?? undefined,
lastMirrored: repo.lastMirrored ?? undefined,
errorMessage: repo.errorMessage ?? undefined,
forkedFrom: repo.forkedFrom ?? undefined,
visibility: repositoryVisibilityEnum.parse(repo.visibility),
mirroredLocation: repo.mirroredLocation || "",
},
});
} else {
await mirrorGithubRepoToGitea({
octokit,
repository: {
...repo,
status: repoStatusEnum.parse("imported"),
organization: repo.organization ?? undefined,
lastMirrored: repo.lastMirrored ?? undefined,
errorMessage: repo.errorMessage ?? undefined,
forkedFrom: repo.forkedFrom ?? undefined,
visibility: repositoryVisibilityEnum.parse(repo.visibility),
mirroredLocation: repo.mirroredLocation || "",
},
config,
});
}
} catch (error) {
console.error(`Mirror failed for repo ${repo.name}:`, error);
}
}
}, 0);
const responsePayload: MirrorRepoResponse = {
success: true,
message: "Mirror job started.",
repositories: repos.map((repo) => ({
...repo,
status: repoStatusEnum.parse(repo.status),
organization: repo.organization ?? undefined,
lastMirrored: repo.lastMirrored ?? undefined,
errorMessage: repo.errorMessage ?? undefined,
forkedFrom: repo.forkedFrom ?? undefined,
visibility: repositoryVisibilityEnum.parse(repo.visibility),
mirroredLocation: repo.mirroredLocation || "",
})),
};
// Return the updated repo list to the user
return new Response(JSON.stringify(responsePayload), {
status: 200,
headers: { "Content-Type": "application/json" },
});
} catch (error) {
console.error("Error mirroring repositories:", error);
return new Response(
JSON.stringify({
error:
error instanceof Error ? error.message : "An unknown error occurred",
}),
{ status: 500, headers: { "Content-Type": "application/json" } }
);
}
};

View File

@@ -0,0 +1,160 @@
import type { APIRoute } from "astro";
import { db, configs, repositories } from "@/lib/db";
import { eq, inArray } from "drizzle-orm";
import { getGiteaRepoOwner, isRepoPresentInGitea } from "@/lib/gitea";
import {
mirrorGithubRepoToGitea,
mirrorGitHubOrgRepoToGiteaOrg,
syncGiteaRepo,
} from "@/lib/gitea";
import { createGitHubClient } from "@/lib/github";
import { repoStatusEnum, repositoryVisibilityEnum } from "@/types/Repository";
import type { RetryRepoRequest, RetryRepoResponse } from "@/types/retry";
export const POST: APIRoute = async ({ request }) => {
try {
const body: RetryRepoRequest = await request.json();
const { userId, repositoryIds } = body;
if (!userId || !repositoryIds || !Array.isArray(repositoryIds)) {
return new Response(
JSON.stringify({
success: false,
message: "userId and repositoryIds are required.",
}),
{ status: 400, headers: { "Content-Type": "application/json" } }
);
}
if (repositoryIds.length === 0) {
return new Response(
JSON.stringify({
success: false,
message: "No repository IDs provided.",
}),
{ status: 400, headers: { "Content-Type": "application/json" } }
);
}
// Fetch user config
const configResult = await db
.select()
.from(configs)
.where(eq(configs.userId, userId))
.limit(1);
const config = configResult[0];
if (!config || !config.githubConfig.token || !config.giteaConfig?.token) {
return new Response(
JSON.stringify({ error: "Missing GitHub or Gitea configuration." }),
{ status: 400, headers: { "Content-Type": "application/json" } }
);
}
// Fetch repositories
const repos = await db
.select()
.from(repositories)
.where(inArray(repositories.id, repositoryIds));
if (!repos.length) {
return new Response(
JSON.stringify({ error: "No repositories found for the given IDs." }),
{ status: 404, headers: { "Content-Type": "application/json" } }
);
}
// Start background retry
setTimeout(async () => {
for (const repo of repos) {
try {
const visibility = repositoryVisibilityEnum.parse(repo.visibility);
const status = repoStatusEnum.parse(repo.status);
const repoData = {
...repo,
visibility,
status,
organization: repo.organization ?? undefined,
lastMirrored: repo.lastMirrored ?? undefined,
errorMessage: repo.errorMessage ?? undefined,
forkedFrom: repo.forkedFrom ?? undefined,
};
let owner = getGiteaRepoOwner({
config,
repository: repoData,
});
const present = await isRepoPresentInGitea({
config,
owner,
repoName: repo.name,
});
if (present) {
await syncGiteaRepo({ config, repository: repoData });
console.log(`Synced existing repo: ${repo.name}`);
} else {
if (!config.githubConfig.token) {
throw new Error("GitHub token is missing.");
}
console.log(`Importing repo: ${repo.name} ${owner}`);
const octokit = createGitHubClient(config.githubConfig.token);
if (repo.organization && config.githubConfig.preserveOrgStructure) {
await mirrorGitHubOrgRepoToGiteaOrg({
config,
octokit,
orgName: repo.organization,
repository: {
...repoData,
status: repoStatusEnum.parse("imported"),
},
});
} else {
await mirrorGithubRepoToGitea({
config,
octokit,
repository: {
...repoData,
status: repoStatusEnum.parse("imported"),
},
});
}
}
} catch (err) {
console.error(`Failed to retry repo ${repo.name}:`, err);
}
}
}, 0);
const responsePayload: RetryRepoResponse = {
success: true,
message: "Retry job (sync/mirror) started.",
repositories: repos.map((repo) => ({
...repo,
status: repoStatusEnum.parse(repo.status),
organization: repo.organization ?? undefined,
lastMirrored: repo.lastMirrored ?? undefined,
errorMessage: repo.errorMessage ?? undefined,
forkedFrom: repo.forkedFrom ?? undefined,
visibility: repositoryVisibilityEnum.parse(repo.visibility),
})),
};
return new Response(JSON.stringify(responsePayload), {
status: 200,
headers: { "Content-Type": "application/json" },
});
} catch (err) {
console.error("Error retrying repo:", err);
return new Response(
JSON.stringify({
error: err instanceof Error ? err.message : "An unknown error occurred",
}),
{ status: 500, headers: { "Content-Type": "application/json" } }
);
}
};

View File

@@ -0,0 +1,154 @@
import type { APIRoute } from "astro";
import { db, configs, repositories } from "@/lib/db";
import { eq, or } from "drizzle-orm";
import { repoStatusEnum, repositoryVisibilityEnum } from "@/types/Repository";
import { isRepoPresentInGitea, syncGiteaRepo } from "@/lib/gitea";
import type {
ScheduleSyncRepoRequest,
ScheduleSyncRepoResponse,
} from "@/types/sync";
export const POST: APIRoute = async ({ request }) => {
try {
const body: ScheduleSyncRepoRequest = await request.json();
const { userId } = body;
if (!userId) {
return new Response(
JSON.stringify({
success: false,
error: "Missing userId in request body.",
repositories: [],
} satisfies ScheduleSyncRepoResponse),
{ status: 400, headers: { "Content-Type": "application/json" } }
);
}
// Fetch config for the user
const configResult = await db
.select()
.from(configs)
.where(eq(configs.userId, userId))
.limit(1);
const config = configResult[0];
if (!config || !config.githubConfig.token) {
return new Response(
JSON.stringify({
success: false,
error: "Config missing for the user or GitHub token not found.",
repositories: [],
} satisfies ScheduleSyncRepoResponse),
{ status: 400, headers: { "Content-Type": "application/json" } }
);
}
// Fetch repositories with status 'mirrored' or 'synced'
const repos = await db
.select()
.from(repositories)
.where(
eq(repositories.userId, userId) &&
or(
eq(repositories.status, "mirrored"),
eq(repositories.status, "synced"),
eq(repositories.status, "failed")
)
);
if (!repos.length) {
return new Response(
JSON.stringify({
success: false,
error:
"No repositories found with status mirrored, synced or failed.",
repositories: [],
} satisfies ScheduleSyncRepoResponse),
{ status: 404, headers: { "Content-Type": "application/json" } }
);
}
// Calculate nextRun and update lastRun and nextRun in the config
const currentTime = new Date();
const interval = config.scheduleConfig?.interval ?? 3600;
const nextRun = new Date(currentTime.getTime() + interval * 1000);
// Update the full giteaConfig object
await db
.update(configs)
.set({
scheduleConfig: {
...config.scheduleConfig,
lastRun: currentTime,
nextRun: nextRun,
},
})
.where(eq(configs.userId, userId));
// Start async sync in background
setTimeout(async () => {
for (const repo of repos) {
try {
// Only check Gitea presence if the repo failed previously
if (repo.status === "failed") {
const isPresent = await isRepoPresentInGitea({
config,
owner: repo.owner,
repoName: repo.name,
});
if (!isPresent) {
continue; //silently skip if repo is not present in Gitea
}
}
await syncGiteaRepo({
config,
repository: {
...repo,
status: repoStatusEnum.parse(repo.status),
organization: repo.organization ?? undefined,
lastMirrored: repo.lastMirrored ?? undefined,
errorMessage: repo.errorMessage ?? undefined,
forkedFrom: repo.forkedFrom ?? undefined,
visibility: repositoryVisibilityEnum.parse(repo.visibility),
},
});
} catch (error) {
console.error(`Sync failed for repo ${repo.name}:`, error);
}
}
}, 0);
const resPayload: ScheduleSyncRepoResponse = {
success: true,
message: "Sync job scheduled for eligible repositories.",
repositories: repos.map((repo) => ({
...repo,
status: repoStatusEnum.parse(repo.status),
organization: repo.organization ?? undefined,
lastMirrored: repo.lastMirrored ?? undefined,
errorMessage: repo.errorMessage ?? undefined,
forkedFrom: repo.forkedFrom ?? undefined,
visibility: repositoryVisibilityEnum.parse(repo.visibility),
})),
};
return new Response(JSON.stringify(resPayload), {
status: 200,
headers: { "Content-Type": "application/json" },
});
} catch (error) {
console.error("Error in scheduling sync:", error);
return new Response(
JSON.stringify({
success: false,
error:
error instanceof Error ? error.message : "An unknown error occurred",
repositories: [],
} satisfies ScheduleSyncRepoResponse),
{ status: 500, headers: { "Content-Type": "application/json" } }
);
}
};

View File

@@ -0,0 +1,114 @@
import type { APIRoute } from "astro";
import type { MirrorRepoRequest } from "@/types/mirror";
import { db, configs, repositories } from "@/lib/db";
import { eq, inArray } from "drizzle-orm";
import { repositoryVisibilityEnum, repoStatusEnum } from "@/types/Repository";
import { syncGiteaRepo } from "@/lib/gitea";
import type { SyncRepoResponse } from "@/types/sync";
export const POST: APIRoute = async ({ request }) => {
try {
const body: MirrorRepoRequest = await request.json();
const { userId, repositoryIds } = body;
if (!userId || !repositoryIds || !Array.isArray(repositoryIds)) {
return new Response(
JSON.stringify({
success: false,
message: "userId and repositoryIds are required.",
}),
{ status: 400, headers: { "Content-Type": "application/json" } }
);
}
if (repositoryIds.length === 0) {
return new Response(
JSON.stringify({
success: false,
message: "No repository IDs provided.",
}),
{ status: 400, headers: { "Content-Type": "application/json" } }
);
}
// Fetch config
const configResult = await db
.select()
.from(configs)
.where(eq(configs.userId, userId))
.limit(1);
const config = configResult[0];
if (!config || !config.githubConfig.token) {
return new Response(
JSON.stringify({ error: "Config missing for the user or token." }),
{ status: 400, headers: { "Content-Type": "application/json" } }
);
}
// Fetch repos
const repos = await db
.select()
.from(repositories)
.where(inArray(repositories.id, repositoryIds));
if (!repos.length) {
return new Response(
JSON.stringify({ error: "No repositories found for the given IDs." }),
{ status: 404, headers: { "Content-Type": "application/json" } }
);
}
// Start async mirroring in background
setTimeout(async () => {
for (const repo of repos) {
try {
await syncGiteaRepo({
config,
repository: {
...repo,
status: repoStatusEnum.parse(repo.status),
organization: repo.organization ?? undefined,
lastMirrored: repo.lastMirrored ?? undefined,
errorMessage: repo.errorMessage ?? undefined,
forkedFrom: repo.forkedFrom ?? undefined,
visibility: repositoryVisibilityEnum.parse(repo.visibility),
},
});
} catch (error) {
console.error(`Sync failed for repo ${repo.name}:`, error);
}
}
}, 0);
const responsePayload: SyncRepoResponse = {
success: true,
message: "Sync job started.",
repositories: repos.map((repo) => ({
...repo,
status: repoStatusEnum.parse(repo.status),
organization: repo.organization ?? undefined,
lastMirrored: repo.lastMirrored ?? undefined,
errorMessage: repo.errorMessage ?? undefined,
forkedFrom: repo.forkedFrom ?? undefined,
visibility: repositoryVisibilityEnum.parse(repo.visibility),
})),
};
// Return the updated repo list to the user
return new Response(JSON.stringify(responsePayload), {
status: 200,
headers: { "Content-Type": "application/json" },
});
} catch (error) {
console.error("Error in syncing repositories:", error);
return new Response(
JSON.stringify({
error:
error instanceof Error ? error.message : "An unknown error occurred",
}),
{ status: 500, headers: { "Content-Type": "application/json" } }
);
}
};

75
src/pages/api/rough.ts Normal file
View File

@@ -0,0 +1,75 @@
//this is a rough api route that will delete all the orgs and repos in gitea. can be used for some other testing purposes as well
import { configs, db } from "@/lib/db";
import { deleteAllOrgs, deleteAllReposInGitea } from "@/lib/rough";
import type { APIRoute } from "astro";
import { eq } from "drizzle-orm";
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: "Missing userId" }), {
status: 400,
headers: { "Content-Type": "application/json" },
});
}
// Fetch user configuration
const userConfig = await db
.select()
.from(configs)
.where(eq(configs.userId, userId))
.limit(1);
if (userConfig.length === 0) {
return new Response(
JSON.stringify({ error: "No configuration found for this user" }),
{
status: 404,
headers: { "Content-Type": "application/json" },
}
);
}
const config = userConfig[0];
if (!config.githubConfig || !config.githubConfig.token) {
return new Response(
JSON.stringify({ error: "GitHub token is missing in config" }),
{ status: 400, headers: { "Content-Type": "application/json" } }
);
}
//adjust this based on the orgs you you want to delete
await deleteAllOrgs({
config,
orgs: [
"Neucruit",
"initify",
"BitBustersx719",
"uiastra",
"conductor-oss",
"HackForge-JUSL",
"vercel",
],
});
await deleteAllReposInGitea({
config,
});
return new Response(JSON.stringify({ message: "Process completed." }), {
status: 200,
headers: { "Content-Type": "application/json" },
});
} catch (error) {
console.error("Error in long-running process:", error);
return new Response(JSON.stringify({ error: "Internal server error" }), {
status: 500,
headers: { "Content-Type": "application/json" },
});
}
};

View File

@@ -0,0 +1,68 @@
import type { APIRoute } from "astro";
import { redisSubscriber } from "@/lib/redis";
export const GET: APIRoute = async ({ request }) => {
const url = new URL(request.url);
const userId = url.searchParams.get("userId");
if (!userId) {
return new Response("Missing userId", { status: 400 });
}
const channel = `mirror-status:${userId}`;
let isClosed = false;
const stream = new ReadableStream({
start(controller) {
const encoder = new TextEncoder();
const handleMessage = (ch: string, message: string) => {
if (isClosed || ch !== channel) return;
try {
controller.enqueue(encoder.encode(`data: ${message}\n\n`));
} catch (err) {
console.error("Stream enqueue error:", err);
}
};
redisSubscriber.subscribe(channel, (err) => {
if (err) {
isClosed = true;
controller.error(err);
}
});
redisSubscriber.on("message", handleMessage);
try {
controller.enqueue(encoder.encode(": connected\n\n"));
} catch (err) {
console.error("Initial enqueue error:", err);
}
request.signal?.addEventListener("abort", () => {
if (!isClosed) {
isClosed = true;
redisSubscriber.off("message", handleMessage);
redisSubscriber.unsubscribe(channel);
controller.close();
}
});
},
cancel() {
// extra safety in case cancel is triggered
if (!isClosed) {
isClosed = true;
redisSubscriber.unsubscribe(channel);
}
},
});
return new Response(stream, {
headers: {
"Content-Type": "text/event-stream",
"Cache-Control": "no-cache",
Connection: "keep-alive",
},
});
};

177
src/pages/api/sync/index.ts Normal file
View File

@@ -0,0 +1,177 @@
import type { APIRoute } from "astro";
import { db, organizations, repositories, configs } from "@/lib/db";
import { eq } from "drizzle-orm";
import { v4 as uuidv4 } from "uuid";
import { createMirrorJob } from "@/lib/helpers";
import {
createGitHubClient,
getGithubOrganizations,
getGithubRepositories,
getGithubStarredRepositories,
} from "@/lib/github";
import { jsonResponse } from "@/lib/utils";
export const POST: APIRoute = async ({ request }) => {
const url = new URL(request.url);
const userId = url.searchParams.get("userId");
if (!userId) {
return jsonResponse({ data: { error: "Missing userId" }, status: 400 });
}
try {
const [config] = await db
.select()
.from(configs)
.where(eq(configs.userId, userId))
.limit(1);
if (!config) {
return jsonResponse({
data: { error: "No configuration found for this user" },
status: 404,
});
}
const token = config.githubConfig?.token;
if (!token) {
return jsonResponse({
data: { error: "GitHub token is missing in config" },
status: 400,
});
}
const octokit = createGitHubClient(token);
// Fetch GitHub data in parallel
const [basicAndForkedRepos, starredRepos, gitOrgs] = await Promise.all([
getGithubRepositories({ octokit, config }),
config.githubConfig?.mirrorStarred
? getGithubStarredRepositories({ octokit, config })
: Promise.resolve([]),
getGithubOrganizations({ octokit, config }),
]);
const allGithubRepos = [...basicAndForkedRepos, ...starredRepos];
// Prepare full list of repos and orgs
const newRepos = allGithubRepos.map((repo) => ({
id: uuidv4(),
userId,
configId: config.id,
name: repo.name,
fullName: repo.fullName,
url: repo.url,
cloneUrl: repo.cloneUrl,
owner: repo.owner,
organization: repo.organization,
isPrivate: repo.isPrivate,
isForked: repo.isForked,
forkedFrom: repo.forkedFrom,
hasIssues: repo.hasIssues,
isStarred: repo.isStarred,
isArchived: repo.isArchived,
size: repo.size,
hasLFS: repo.hasLFS,
hasSubmodules: repo.hasSubmodules,
defaultBranch: repo.defaultBranch,
visibility: repo.visibility,
status: repo.status,
lastMirrored: repo.lastMirrored,
errorMessage: repo.errorMessage,
createdAt: repo.createdAt,
updatedAt: repo.updatedAt,
}));
const newOrgs = gitOrgs.map((org) => ({
id: uuidv4(),
userId,
configId: config.id,
name: org.name,
avatarUrl: org.avatarUrl,
membershipRole: org.membershipRole,
isIncluded: false,
status: org.status,
repositoryCount: org.repositoryCount,
createdAt: new Date(),
updatedAt: new Date(),
}));
let insertedRepos: typeof newRepos = [];
let insertedOrgs: typeof newOrgs = [];
// Transaction to insert only new items
await db.transaction(async (tx) => {
const [existingRepos, existingOrgs] = await Promise.all([
tx
.select({ fullName: repositories.fullName })
.from(repositories)
.where(eq(repositories.userId, userId)),
tx
.select({ name: organizations.name })
.from(organizations)
.where(eq(organizations.userId, userId)),
]);
const existingRepoNames = new Set(existingRepos.map((r) => r.fullName));
const existingOrgNames = new Set(existingOrgs.map((o) => o.name));
insertedRepos = newRepos.filter(
(r) => !existingRepoNames.has(r.fullName)
);
insertedOrgs = newOrgs.filter((o) => !existingOrgNames.has(o.name));
if (insertedRepos.length > 0) {
await tx.insert(repositories).values(insertedRepos);
}
if (insertedOrgs.length > 0) {
await tx.insert(organizations).values(insertedOrgs);
}
});
// Create mirror jobs only for newly inserted items
const mirrorJobPromises = [
...insertedRepos.map((repo) =>
createMirrorJob({
userId,
repositoryId: repo.id,
repositoryName: repo.name,
status: "imported",
message: `Repository ${repo.name} fetched successfully`,
details: `Repository ${repo.name} was fetched from GitHub`,
})
),
...insertedOrgs.map((org) =>
createMirrorJob({
userId,
organizationId: org.id,
organizationName: org.name,
status: "imported",
message: `Organization ${org.name} fetched successfully`,
details: `Organization ${org.name} was fetched from GitHub`,
})
),
];
await Promise.all(mirrorJobPromises);
return jsonResponse({
data: {
success: true,
message: "Repositories and organizations synced successfully",
newRepositories: insertedRepos.length,
newOrganizations: insertedOrgs.length,
},
});
} catch (error) {
console.error("Error syncing GitHub data for user:", userId, error);
return jsonResponse({
data: {
error: error instanceof Error ? error.message : "Something went wrong",
},
status: 500,
});
}
};

View File

@@ -0,0 +1,136 @@
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 } from "@/lib/utils";
import type {
AddOrganizationApiRequest,
AddOrganizationApiResponse,
} from "@/types/organizations";
import type { RepositoryVisibility, RepoStatus } from "@/types/Repository";
import { v4 as uuidv4 } from "uuid";
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(eq(configs.userId, userId))
.limit(1);
if (!config) {
return jsonResponse({
data: { error: "No configuration found for this user" },
status: 404,
});
}
const configId = config.id;
const octokit = new Octokit();
// Fetch org metadata
const { data: orgData } = await octokit.orgs.get({ org });
// Fetch public repos using Octokit paginator
const publicRepos = await octokit.paginate(octokit.repos.listForOrg, {
org,
type: "public",
per_page: 100,
});
// Insert repositories
const repoRecords = publicRepos.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,
isPrivate: repo.private,
isForked: repo.fork,
forkedFrom: undefined,
hasIssues: repo.has_issues,
isStarred: false,
isArchived: repo.archived,
size: repo.size,
hasLFS: false,
hasSubmodules: false,
defaultBranch: repo.default_branch ?? "main",
visibility: (repo.visibility ?? "public") as RepositoryVisibility,
status: "imported" as RepoStatus,
lastMirrored: undefined,
errorMessage: undefined,
createdAt: repo.created_at ? new Date(repo.created_at) : new Date(),
updatedAt: repo.updated_at ? new Date(repo.updated_at) : new Date(),
}));
await db.insert(repositories).values(repoRecords);
// 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: publicRepos.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) {
console.error("Error inserting organization/repositories:", error);
return jsonResponse({
data: {
error: error instanceof Error ? error.message : "Something went wrong",
},
status: 500,
});
}
};

View File

@@ -0,0 +1,137 @@
import type { APIRoute } from "astro";
import { Octokit } from "@octokit/rest";
import { configs, db, repositories } from "@/lib/db";
import { v4 as uuidv4 } from "uuid";
import { and, eq } from "drizzle-orm";
import { type Repository } from "@/lib/db/schema";
import { jsonResponse } from "@/lib/utils";
import type {
AddRepositoriesApiRequest,
AddRepositoriesApiResponse,
RepositoryVisibility,
} from "@/types/Repository";
import { createMirrorJob } from "@/lib/helpers";
export const POST: APIRoute = async ({ request }) => {
try {
const body: AddRepositoriesApiRequest = await request.json();
const { owner, repo, userId } = body;
if (!owner || !repo || !userId) {
return new Response(
JSON.stringify({
success: false,
error: "Missing owner, repo, or userId",
}),
{ status: 400 }
);
}
// Check if repository with the same owner, name, and userId already exists
const existingRepo = await db
.select()
.from(repositories)
.where(
and(
eq(repositories.owner, owner),
eq(repositories.name, repo),
eq(repositories.userId, userId)
)
);
if (existingRepo.length > 0) {
return jsonResponse({
data: {
success: false,
error:
"Repository with this name and owner already exists for this user",
},
status: 400,
});
}
// Get user's active config
const [config] = await db
.select()
.from(configs)
.where(eq(configs.userId, userId))
.limit(1);
if (!config) {
return jsonResponse({
data: { error: "No configuration found for this user" },
status: 404,
});
}
const configId = config.id;
const octokit = new Octokit(); // No auth for public repos
const { data: repoData } = await octokit.rest.repos.get({ owner, repo });
const metadata = {
id: uuidv4(),
userId,
configId,
name: repoData.name,
fullName: repoData.full_name,
url: repoData.html_url,
cloneUrl: repoData.clone_url,
owner: repoData.owner.login,
organization:
repoData.owner.type === "Organization"
? repoData.owner.login
: undefined,
isPrivate: repoData.private,
isForked: repoData.fork,
forkedFrom: undefined,
hasIssues: repoData.has_issues,
isStarred: false,
isArchived: repoData.archived,
size: repoData.size,
hasLFS: false,
hasSubmodules: false,
defaultBranch: repoData.default_branch,
visibility: (repoData.visibility ?? "public") as RepositoryVisibility,
status: "imported" as Repository["status"],
lastMirrored: undefined,
errorMessage: undefined,
createdAt: repoData.created_at
? new Date(repoData.created_at)
: new Date(),
updatedAt: repoData.updated_at
? new Date(repoData.updated_at)
: new Date(),
};
await db.insert(repositories).values(metadata);
createMirrorJob({
userId,
organizationId: metadata.organization,
organizationName: metadata.organization,
repositoryId: metadata.id,
repositoryName: metadata.name,
status: "imported",
message: `Repository ${metadata.name} fetched successfully`,
details: `Repository ${metadata.name} was fetched from GitHub`,
});
const resPayload: AddRepositoriesApiResponse = {
success: true,
repository: metadata,
message: "Repository added successfully",
};
return jsonResponse({ data: resPayload, status: 200 });
} catch (error) {
console.error("Error inserting repository:", error);
return jsonResponse({
data: {
error: error instanceof Error ? error.message : "Something went wrong",
},
status: 500,
});
}
};