Files
gitea-mirror/src/pages/api/sync/repository.ts
ARUNAVO RAY be46cfdffa feat: add target organization to Add Repository dialog (#202)
* feat: add target organization field to Add Repository dialog

Allow users to specify a destination Gitea organization when adding a
single repository, instead of relying solely on the default mirror
strategy. The field is optional — when left empty, the existing strategy
logic applies as before.

Closes #200

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* docs: add screenshot of target organization field in Add Repository dialog

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-01 07:55:27 +05:30

191 lines
5.5 KiB
TypeScript

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, createSecureErrorResponse } from "@/lib/utils";
import type {
AddRepositoriesApiRequest,
AddRepositoriesApiResponse,
RepositoryVisibility,
} from "@/types/Repository";
import { createMirrorJob } from "@/lib/helpers";
import { requireAuthenticatedUserId } from "@/lib/auth-guards";
export const POST: APIRoute = async ({ request, locals }) => {
try {
const authResult = await requireAuthenticatedUserId({ request, locals });
if ("response" in authResult) return authResult.response;
const userId = authResult.userId;
const body: AddRepositoriesApiRequest = await request.json();
const { owner, repo, force = false, destinationOrg } = body;
if (!owner || !repo) {
return new Response(
JSON.stringify({
success: false,
error: "Missing owner or repo",
}),
{ status: 400 }
);
}
const trimmedOwner = owner.trim();
const trimmedRepo = repo.trim();
if (!trimmedOwner || !trimmedRepo) {
return jsonResponse({
data: {
success: false,
error: "Missing owner or repo",
},
status: 400,
});
}
const normalizedOwner = trimmedOwner.toLowerCase();
const normalizedRepo = trimmedRepo.toLowerCase();
const normalizedFullName = `${normalizedOwner}/${normalizedRepo}`;
// Check if repository with the same owner, name, and userId already exists
const [existingRepo] = await db
.select()
.from(repositories)
.where(
and(
eq(repositories.userId, userId),
eq(repositories.normalizedFullName, normalizedFullName)
)
)
.limit(1);
if (existingRepo && !force) {
return jsonResponse({
data: {
success: false,
error:
"Repository with this name and owner already exists for this user",
},
status: 409,
});
}
// 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: trimmedOwner,
repo: trimmedRepo,
});
const baseMetadata = {
userId,
configId,
name: repoData.name,
fullName: repoData.full_name,
normalizedFullName,
url: repoData.html_url,
cloneUrl: repoData.clone_url,
owner: repoData.owner.login,
organization:
repoData.owner.type === "Organization" ? repoData.owner.login : null,
isPrivate: repoData.private,
isForked: repoData.fork,
forkedFrom: null,
hasIssues: repoData.has_issues,
isStarred: false,
isArchived: repoData.archived,
size: repoData.size,
hasLFS: false,
hasSubmodules: false,
language: repoData.language ?? null,
description: repoData.description ?? null,
defaultBranch: repoData.default_branch,
visibility: (repoData.visibility ?? "public") as RepositoryVisibility,
lastMirrored: existingRepo?.lastMirrored ?? null,
errorMessage: existingRepo?.errorMessage ?? null,
mirroredLocation: existingRepo?.mirroredLocation ?? "",
destinationOrg: destinationOrg?.trim() || existingRepo?.destinationOrg || null,
updatedAt: repoData.updated_at
? new Date(repoData.updated_at)
: new Date(),
};
if (existingRepo && force) {
const [updatedRepo] = await db
.update(repositories)
.set({
...baseMetadata,
normalizedFullName,
configId,
})
.where(eq(repositories.id, existingRepo.id))
.returning();
const resPayload: AddRepositoriesApiResponse = {
success: true,
repository: updatedRepo ?? existingRepo,
message: "Repository already exists; metadata refreshed.",
};
return jsonResponse({ data: resPayload, status: 200 });
}
const metadata = {
id: uuidv4(),
status: "imported" as Repository["status"],
lastMirrored: null,
errorMessage: null,
mirroredLocation: "",
destinationOrg: null,
createdAt: repoData.created_at
? new Date(repoData.created_at)
: new Date(),
...baseMetadata,
} satisfies Repository;
await db
.insert(repositories)
.values(metadata)
.onConflictDoNothing({ target: [repositories.userId, repositories.normalizedFullName] });
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) {
return createSecureErrorResponse(error, "repository sync", 500);
}
};