mirror of
https://github.com/RayLabsHQ/gitea-mirror.git
synced 2025-12-17 03:43:46 +03:00
🎉 Gitea Mirror: Added
This commit is contained in:
65
src/pages/activity.astro
Normal file
65
src/pages/activity.astro
Normal file
@@ -0,0 +1,65 @@
|
||||
---
|
||||
import '../styles/global.css';
|
||||
import App from '@/components/layout/MainLayout';
|
||||
import { db, mirrorJobs } from '@/lib/db';
|
||||
import ThemeScript from '@/components/theme/ThemeScript.astro';
|
||||
|
||||
// Fetch activity data from the database
|
||||
let activityData = [];
|
||||
|
||||
try {
|
||||
// Fetch activity from mirror jobs
|
||||
const jobs = await db.select().from(mirrorJobs).limit(20);
|
||||
activityData = jobs.flatMap((job: any) => {
|
||||
// Check if log exists before parsing
|
||||
if (!job.log) {
|
||||
console.warn(`Job ${job.id} has no log data`);
|
||||
return [];
|
||||
}
|
||||
|
||||
try {
|
||||
const log = JSON.parse(job.log);
|
||||
if (!Array.isArray(log)) {
|
||||
console.warn(`Job ${job.id} log is not an array`);
|
||||
return [];
|
||||
}
|
||||
|
||||
return log.map((entry: any) => ({
|
||||
id: `${job.id}-${entry.timestamp}`,
|
||||
message: entry.message,
|
||||
timestamp: new Date(entry.timestamp),
|
||||
status: entry.level,
|
||||
details: entry.details,
|
||||
repositoryName: entry.repositoryName,
|
||||
}));
|
||||
} catch (parseError) {
|
||||
console.error(`Failed to parse log for job ${job.id}:`, parseError);
|
||||
return [];
|
||||
}
|
||||
}).slice(0, 20);
|
||||
} catch (error) {
|
||||
console.error('Error fetching activity:', error);
|
||||
// Fallback to empty array if database access fails
|
||||
activityData = [];
|
||||
}
|
||||
|
||||
// Client-side function to handle refresh
|
||||
const handleRefresh = () => {
|
||||
console.log('Refreshing activity log');
|
||||
// In a real implementation, this would call the API to refresh the activity log
|
||||
};
|
||||
---
|
||||
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<meta name="viewport" content="width=device-width" />
|
||||
<link rel="icon" type="image/svg+xml" href="/favicon.svg" />
|
||||
<meta name="generator" content={Astro.generator} />
|
||||
<title>Activity Log - Gitea Mirror</title>
|
||||
<ThemeScript />
|
||||
</head>
|
||||
<body>
|
||||
<App page='activity-log'client:load />
|
||||
</body>
|
||||
</html>
|
||||
58
src/pages/api/activities/index.ts
Normal file
58
src/pages/api/activities/index.ts
Normal 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" } }
|
||||
);
|
||||
}
|
||||
};
|
||||
83
src/pages/api/auth/index.ts
Normal file
83
src/pages/api/auth/index.ts
Normal 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" },
|
||||
});
|
||||
}
|
||||
};
|
||||
62
src/pages/api/auth/login.ts
Normal file
62
src/pages/api/auth/login.ts
Normal 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
|
||||
}`,
|
||||
},
|
||||
});
|
||||
};
|
||||
11
src/pages/api/auth/logout.ts
Normal file
11
src/pages/api/auth/logout.ts
Normal 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",
|
||||
},
|
||||
});
|
||||
};
|
||||
72
src/pages/api/auth/register.ts
Normal file
72
src/pages/api/auth/register.ts
Normal 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
|
||||
}`,
|
||||
},
|
||||
});
|
||||
};
|
||||
225
src/pages/api/config/index.ts
Normal file
225
src/pages/api/config/index.ts
Normal 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" },
|
||||
}
|
||||
);
|
||||
}
|
||||
};
|
||||
122
src/pages/api/dashboard/index.ts
Normal file
122
src/pages/api/dashboard/index.ts
Normal 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,
|
||||
});
|
||||
}
|
||||
};
|
||||
135
src/pages/api/gitea/test-connection.ts
Normal file
135
src/pages/api/gitea/test-connection.ts
Normal 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',
|
||||
},
|
||||
}
|
||||
);
|
||||
}
|
||||
};
|
||||
60
src/pages/api/github/organizations.ts
Normal file
60
src/pages/api/github/organizations.ts
Normal 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,
|
||||
});
|
||||
}
|
||||
};
|
||||
96
src/pages/api/github/repositories.ts
Normal file
96
src/pages/api/github/repositories.ts
Normal 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,
|
||||
});
|
||||
}
|
||||
};
|
||||
101
src/pages/api/github/test-connection.ts
Normal file
101
src/pages/api/github/test-connection.ts
Normal 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",
|
||||
},
|
||||
}
|
||||
);
|
||||
}
|
||||
};
|
||||
118
src/pages/api/job/mirror-org.ts
Normal file
118
src/pages/api/job/mirror-org.ts
Normal 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" } }
|
||||
);
|
||||
}
|
||||
};
|
||||
144
src/pages/api/job/mirror-repo.ts
Normal file
144
src/pages/api/job/mirror-repo.ts
Normal 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" } }
|
||||
);
|
||||
}
|
||||
};
|
||||
160
src/pages/api/job/retry-repo.ts
Normal file
160
src/pages/api/job/retry-repo.ts
Normal 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" } }
|
||||
);
|
||||
}
|
||||
};
|
||||
154
src/pages/api/job/schedule-sync-repo.ts
Normal file
154
src/pages/api/job/schedule-sync-repo.ts
Normal 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" } }
|
||||
);
|
||||
}
|
||||
};
|
||||
114
src/pages/api/job/sync-repo.ts
Normal file
114
src/pages/api/job/sync-repo.ts
Normal 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
75
src/pages/api/rough.ts
Normal 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" },
|
||||
});
|
||||
}
|
||||
};
|
||||
68
src/pages/api/sse/index.ts
Normal file
68
src/pages/api/sse/index.ts
Normal 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
177
src/pages/api/sync/index.ts
Normal 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,
|
||||
});
|
||||
}
|
||||
};
|
||||
136
src/pages/api/sync/organization.ts
Normal file
136
src/pages/api/sync/organization.ts
Normal 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,
|
||||
});
|
||||
}
|
||||
};
|
||||
137
src/pages/api/sync/repository.ts
Normal file
137
src/pages/api/sync/repository.ts
Normal 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,
|
||||
});
|
||||
}
|
||||
};
|
||||
24
src/pages/config.astro
Normal file
24
src/pages/config.astro
Normal file
@@ -0,0 +1,24 @@
|
||||
---
|
||||
import '../styles/global.css';
|
||||
import App, { MainLayout } from '@/components/layout/MainLayout';
|
||||
import { ConfigTabs } from '@/components/config/ConfigTabs';
|
||||
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card';
|
||||
import { db, configs } from '@/lib/db';
|
||||
import ThemeScript from '@/components/theme/ThemeScript.astro';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import type { SaveConfigApiRequest,SaveConfigApiResponse } from '@/types/config';
|
||||
---
|
||||
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<meta name="viewport" content="width=device-width" />
|
||||
<link rel="icon" type="image/svg+xml" href="/favicon.svg" />
|
||||
<meta name="generator" content={Astro.generator} />
|
||||
<title>Configuration - Gitea Mirror</title>
|
||||
<ThemeScript />
|
||||
</head>
|
||||
<body>
|
||||
<App page='configuration' client:load />
|
||||
</body>
|
||||
</html>
|
||||
63
src/pages/docs/[slug].astro
Normal file
63
src/pages/docs/[slug].astro
Normal file
@@ -0,0 +1,63 @@
|
||||
---
|
||||
import { getCollection } from 'astro:content';
|
||||
import MainLayout from '../../layouts/main.astro';
|
||||
|
||||
// Enable prerendering for this dynamic route
|
||||
export const prerender = true;
|
||||
|
||||
// Generate static paths for all documentation pages
|
||||
export async function getStaticPaths() {
|
||||
const docs = await getCollection('docs');
|
||||
return docs.map(entry => ({
|
||||
params: { slug: entry.slug },
|
||||
props: { entry },
|
||||
}));
|
||||
}
|
||||
|
||||
// Get the documentation entry from props
|
||||
const { entry } = Astro.props;
|
||||
const { Content } = await entry.render();
|
||||
---
|
||||
|
||||
<MainLayout title={entry.data.title}>
|
||||
<main class="max-w-5xl mx-auto px-4 py-12">
|
||||
<div class="sticky top-4 z-10 mb-6">
|
||||
<a
|
||||
href="/docs/"
|
||||
class="inline-flex items-center gap-2 px-3 py-1.5 rounded-md bg-card text-foreground hover:bg-muted transition-colors border border-border focus:ring-2 focus:ring-ring outline-none"
|
||||
>
|
||||
<span aria-hidden="true">←</span> Back to Documentation
|
||||
</a>
|
||||
</div>
|
||||
<article class="bg-card rounded-2xl shadow-lg p-6 border border-border">
|
||||
<div class="prose prose-neutral dark:prose-invert prose-code:bg-muted prose-code:text-foreground prose-pre:bg-muted prose-pre:text-foreground prose-pre:rounded-lg prose-pre:p-4 prose-table:rounded-lg prose-table:bg-muted prose-th:text-foreground prose-td:text-muted-foreground prose-blockquote:border-l-4 prose-blockquote:border-muted prose-blockquote:bg-muted/50 prose-blockquote:p-4">
|
||||
<Content />
|
||||
</div>
|
||||
</article>
|
||||
<script type="module">
|
||||
// Mermaid diagram rendering for code blocks
|
||||
import mermaid from 'https://cdn.jsdelivr.net/npm/mermaid@10/dist/mermaid.esm.min.mjs';
|
||||
mermaid.initialize({ startOnLoad: false, theme: document.documentElement.classList.contains('dark') ? 'dark' : 'default' });
|
||||
function renderMermaidDiagrams() {
|
||||
document.querySelectorAll('pre code.language-mermaid').forEach((block, i) => {
|
||||
const parent = block.parentElement;
|
||||
if (!parent) return;
|
||||
const code = block.textContent;
|
||||
const id = `mermaid-diagram-${i}`;
|
||||
const container = document.createElement('div');
|
||||
container.className = 'my-6';
|
||||
container.id = id;
|
||||
parent.replaceWith(container);
|
||||
mermaid.render(id, code, (svgCode) => {
|
||||
container.innerHTML = svgCode;
|
||||
});
|
||||
});
|
||||
}
|
||||
if (document.readyState === 'loading') {
|
||||
document.addEventListener('DOMContentLoaded', renderMermaidDiagrams);
|
||||
} else {
|
||||
renderMermaidDiagrams();
|
||||
}
|
||||
</script>
|
||||
</main>
|
||||
</MainLayout>
|
||||
44
src/pages/docs/index.astro
Normal file
44
src/pages/docs/index.astro
Normal file
@@ -0,0 +1,44 @@
|
||||
---
|
||||
import { getCollection } from 'astro:content';
|
||||
import MainLayout from '../../layouts/main.astro';
|
||||
import { LuSettings, LuRocket, LuBookOpen } from 'react-icons/lu';
|
||||
|
||||
// Helper to pick an icon based on doc.slug
|
||||
// We'll use inline conditional rendering instead of this function
|
||||
|
||||
// Get all documentation entries, sorted by order
|
||||
const docs = await getCollection('docs');
|
||||
const sortedDocs = docs.sort((a, b) => {
|
||||
const orderA = a.data.order || 999;
|
||||
const orderB = b.data.order || 999;
|
||||
return orderA - orderB;
|
||||
});
|
||||
---
|
||||
|
||||
<MainLayout title="Documentation">
|
||||
<main class="max-w-5xl mx-auto px-4 py-12">
|
||||
<h1 class="text-3xl font-bold mb-2 text-center text-foreground">Gitea Mirror Documentation</h1>
|
||||
<p class="mb-10 text-lg text-muted-foreground text-center">Browse guides and technical docs for Gitea Mirror.</p>
|
||||
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 gap-8">
|
||||
{sortedDocs.map(doc => (
|
||||
<a
|
||||
href={`/docs/${doc.slug}`}
|
||||
class="group block p-7 border border-border rounded-2xl bg-card hover:bg-muted transition-colors shadow-lg focus:ring-2 focus:ring-ring outline-none"
|
||||
tabindex="0"
|
||||
>
|
||||
<div class="flex items-center gap-3 mb-2">
|
||||
<div class="w-10 h-10 bg-muted rounded-full flex items-center justify-center text-muted-foreground">
|
||||
{doc.slug === 'architecture' && <LuBookOpen className="w-5 h-5" />}
|
||||
{doc.slug === 'configuration' && <LuSettings className="w-5 h-5" />}
|
||||
{doc.slug === 'quickstart' && <LuRocket className="w-5 h-5" />}
|
||||
{!['architecture', 'configuration', 'quickstart'].includes(doc.slug) && <LuBookOpen className="w-5 h-5" />}
|
||||
</div>
|
||||
<h2 class="text-xl font-semibold group-hover:text-foreground transition">{doc.data.title}</h2>
|
||||
</div>
|
||||
<p class="text-muted-foreground">{doc.data.description}</p>
|
||||
</a>
|
||||
))}
|
||||
</div>
|
||||
</main>
|
||||
</MainLayout>
|
||||
70
src/pages/index.astro
Normal file
70
src/pages/index.astro
Normal file
@@ -0,0 +1,70 @@
|
||||
---
|
||||
import '../styles/global.css';
|
||||
import App from '@/components/layout/MainLayout';
|
||||
import { db, repositories, mirrorJobs, client } from '@/lib/db';
|
||||
import ThemeScript from '@/components/theme/ThemeScript.astro';
|
||||
|
||||
// Check if any users exist in the database
|
||||
const userCountResult = await client.execute(`SELECT COUNT(*) as count FROM users`);
|
||||
const userCount = userCountResult.rows[0].count;
|
||||
|
||||
// Redirect to signup if no users exist
|
||||
if (userCount === 0) {
|
||||
return Astro.redirect('/signup');
|
||||
}
|
||||
|
||||
// Fetch data from the database
|
||||
let repoData:any[] = [];
|
||||
let activityData = [];
|
||||
|
||||
try {
|
||||
// Fetch repositories from database
|
||||
const dbRepos = await db.select().from(repositories).limit(10);
|
||||
repoData = dbRepos;
|
||||
|
||||
// Fetch recent activity from mirror jobs
|
||||
const jobs = await db.select().from(mirrorJobs).limit(10);
|
||||
activityData = jobs.flatMap((job: any) => {
|
||||
// Check if log exists before parsing
|
||||
if (!job.log) {
|
||||
console.warn(`Job ${job.id} has no log data`);
|
||||
return [];
|
||||
}
|
||||
try {
|
||||
const log = JSON.parse(job.log);
|
||||
if (!Array.isArray(log)) {
|
||||
console.warn(`Job ${job.id} log is not an array`);
|
||||
return [];
|
||||
}
|
||||
return log.map((entry: any) => ({
|
||||
id: `${job.id}-${entry.timestamp}`,
|
||||
message: entry.message,
|
||||
timestamp: new Date(entry.timestamp),
|
||||
status: entry.level,
|
||||
}));
|
||||
} catch (parseError) {
|
||||
console.error(`Failed to parse log for job ${job.id}:`, parseError);
|
||||
return [];
|
||||
}
|
||||
}).slice(0, 10);
|
||||
} catch (error) {
|
||||
console.error('Error fetching data:', error);
|
||||
// Fallback to empty arrays if database access fails
|
||||
repoData = [];
|
||||
activityData = [];
|
||||
}
|
||||
---
|
||||
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<meta name="viewport" content="width=device-width" />
|
||||
<link rel="icon" type="image/svg+xml" href="/favicon.svg" />
|
||||
<meta name="generator" content={Astro.generator} />
|
||||
<title>Dashboard - Gitea Mirror</title>
|
||||
<ThemeScript />
|
||||
</head>
|
||||
<body>
|
||||
<App page='dashboard' client:load />
|
||||
</body>
|
||||
</html>
|
||||
33
src/pages/login.astro
Normal file
33
src/pages/login.astro
Normal file
@@ -0,0 +1,33 @@
|
||||
---
|
||||
import '../styles/global.css';
|
||||
import ThemeScript from '@/components/theme/ThemeScript.astro';
|
||||
import { LoginForm } from '@/components/auth/LoginForm';
|
||||
import { client } from '../lib/db';
|
||||
|
||||
// Check if any users exist in the database
|
||||
const userCountResult = await client.execute(`SELECT COUNT(*) as count FROM users`);
|
||||
const userCount = userCountResult.rows[0].count;
|
||||
|
||||
// Redirect to signup if no users exist
|
||||
if (userCount === 0) {
|
||||
return Astro.redirect('/signup');
|
||||
}
|
||||
|
||||
const generator = Astro.generator;
|
||||
---
|
||||
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<meta name="viewport" content="width=device-width" />
|
||||
<link rel="icon" type="image/svg+xml" href="/favicon.svg" />
|
||||
<meta name="generator" content={generator} />
|
||||
<title>Login - Gitea Mirror</title>
|
||||
<ThemeScript />
|
||||
</head>
|
||||
<body>
|
||||
<div class="h-dvh flex items-center justify-center bg-muted/30 p-4">
|
||||
<LoginForm client:load />
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
16
src/pages/markdown-page.md
Normal file
16
src/pages/markdown-page.md
Normal file
@@ -0,0 +1,16 @@
|
||||
---
|
||||
title: 'Markdown + Tailwind'
|
||||
layout: ../layouts/main.astro
|
||||
---
|
||||
|
||||
<div class="grid place-items-center h-screen content-center">
|
||||
<div class="py-2 px-4 bg-purple-500 text-white font-semibold rounded-lg shadow-md">
|
||||
Tailwind classes also work in Markdown!
|
||||
</div>
|
||||
<a
|
||||
href="/"
|
||||
class="p-4 underline hover:text-purple-500 transition-colors ease-in-out duration-200"
|
||||
>
|
||||
Go home
|
||||
</a>
|
||||
</div>
|
||||
22
src/pages/organizations.astro
Normal file
22
src/pages/organizations.astro
Normal file
@@ -0,0 +1,22 @@
|
||||
---
|
||||
import '../styles/global.css';
|
||||
import App from '@/components/layout/MainLayout';
|
||||
import ThemeScript from '@/components/theme/ThemeScript.astro';
|
||||
|
||||
---
|
||||
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<meta name="viewport" content="width=device-width" />
|
||||
<link rel="icon" type="image/svg+xml" href="/favicon.svg" />
|
||||
<meta name="generator" content={Astro.generator} />
|
||||
<title>Organizations - Gitea Mirror</title>
|
||||
<ThemeScript />
|
||||
</head>
|
||||
<body>
|
||||
|
||||
<App page="organizations" client:load />
|
||||
|
||||
</body>
|
||||
</html>
|
||||
21
src/pages/repositories.astro
Normal file
21
src/pages/repositories.astro
Normal file
@@ -0,0 +1,21 @@
|
||||
---
|
||||
import '../styles/global.css';
|
||||
import App from '@/components/layout/MainLayout';
|
||||
import ThemeScript from '@/components/theme/ThemeScript.astro';
|
||||
---
|
||||
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<meta name="viewport" content="width=device-width" />
|
||||
<link rel="icon" type="image/svg+xml" href="/favicon.svg" />
|
||||
<meta name="generator" content={Astro.generator} />
|
||||
<title>Repositories - Gitea Mirror</title>
|
||||
<ThemeScript />
|
||||
</head>
|
||||
<body>
|
||||
|
||||
<App page='repositories' client:load />
|
||||
|
||||
</body>
|
||||
</html>
|
||||
37
src/pages/signup.astro
Normal file
37
src/pages/signup.astro
Normal file
@@ -0,0 +1,37 @@
|
||||
---
|
||||
import '../styles/global.css';
|
||||
import ThemeScript from '@/components/theme/ThemeScript.astro';
|
||||
import { SignupForm } from '@/components/auth/SignupForm';
|
||||
import { client } from '../lib/db';
|
||||
|
||||
// Check if any users exist in the database
|
||||
const userCountResult = await client.execute(`SELECT COUNT(*) as count FROM users`);
|
||||
const userCount = userCountResult.rows[0]?.count;
|
||||
|
||||
// Redirect to login if users already exist
|
||||
if (userCount !== null && Number(userCount) > 0) {
|
||||
return Astro.redirect('/login');
|
||||
}
|
||||
|
||||
const generator = Astro.generator;
|
||||
---
|
||||
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<meta name="viewport" content="width=device-width" />
|
||||
<link rel="icon" type="image/svg+xml" href="/favicon.svg" />
|
||||
<meta name="generator" content={generator} />
|
||||
<title>Setup Admin Account - Gitea Mirror</title>
|
||||
<ThemeScript />
|
||||
</head>
|
||||
<body>
|
||||
<div class="h-dvh flex flex-col items-center justify-center bg-muted/30 p-4">
|
||||
<div class="mb-8 text-center">
|
||||
<h1 class="text-3xl font-bold mb-2">Welcome to Gitea Mirror</h1>
|
||||
<p class="text-muted-foreground">Let's set up your administrator account to get started.</p>
|
||||
</div>
|
||||
<SignupForm client:load />
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
Reference in New Issue
Block a user