mirror of
https://github.com/RayLabsHQ/gitea-mirror.git
synced 2026-03-13 22:12:54 +03:00
security: enforce session-derived user identity on API routes (#186)
* security: enforce session user on api routes * test: harden auth guard failure path
This commit is contained in:
66
src/lib/auth-guards.test.ts
Normal file
66
src/lib/auth-guards.test.ts
Normal file
@@ -0,0 +1,66 @@
|
||||
import { describe, expect, mock, test } from "bun:test";
|
||||
|
||||
const getSessionMock = mock(async () => null);
|
||||
|
||||
mock.module("@/lib/auth", () => ({
|
||||
auth: {
|
||||
api: {
|
||||
getSession: getSessionMock,
|
||||
},
|
||||
},
|
||||
}));
|
||||
|
||||
import { requireAuthenticatedUserId } from "./auth-guards";
|
||||
|
||||
describe("requireAuthenticatedUserId", () => {
|
||||
test("returns user id from locals session without calling auth api", async () => {
|
||||
getSessionMock.mockImplementation(async () => {
|
||||
throw new Error("should not be called");
|
||||
});
|
||||
|
||||
const result = await requireAuthenticatedUserId({
|
||||
request: new Request("http://localhost/test"),
|
||||
locals: {
|
||||
session: { userId: "local-user-id" },
|
||||
} as any,
|
||||
});
|
||||
|
||||
expect("userId" in result).toBe(true);
|
||||
if ("userId" in result) {
|
||||
expect(result.userId).toBe("local-user-id");
|
||||
}
|
||||
});
|
||||
|
||||
test("returns user id from auth session when locals are empty", async () => {
|
||||
getSessionMock.mockImplementation(async () => ({
|
||||
user: { id: "session-user-id" },
|
||||
session: { id: "session-id" },
|
||||
}));
|
||||
|
||||
const result = await requireAuthenticatedUserId({
|
||||
request: new Request("http://localhost/test"),
|
||||
locals: {} as any,
|
||||
});
|
||||
|
||||
expect("userId" in result).toBe(true);
|
||||
if ("userId" in result) {
|
||||
expect(result.userId).toBe("session-user-id");
|
||||
}
|
||||
});
|
||||
|
||||
test("returns unauthorized response when auth lookup throws", async () => {
|
||||
getSessionMock.mockImplementation(async () => {
|
||||
throw new Error("session provider unavailable");
|
||||
});
|
||||
|
||||
const result = await requireAuthenticatedUserId({
|
||||
request: new Request("http://localhost/test"),
|
||||
locals: {} as any,
|
||||
});
|
||||
|
||||
expect("response" in result).toBe(true);
|
||||
if ("response" in result) {
|
||||
expect(result.response.status).toBe(401);
|
||||
}
|
||||
});
|
||||
});
|
||||
45
src/lib/auth-guards.ts
Normal file
45
src/lib/auth-guards.ts
Normal file
@@ -0,0 +1,45 @@
|
||||
import type { APIContext } from "astro";
|
||||
import { auth } from "@/lib/auth";
|
||||
|
||||
function unauthorizedResponse() {
|
||||
return new Response(
|
||||
JSON.stringify({
|
||||
success: false,
|
||||
error: "Unauthorized",
|
||||
}),
|
||||
{
|
||||
status: 401,
|
||||
headers: { "Content-Type": "application/json" },
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Ensures request is authenticated and returns the authenticated user ID.
|
||||
* Never trust client-provided userId for authorization decisions.
|
||||
*/
|
||||
export async function requireAuthenticatedUserId(
|
||||
context: Pick<APIContext, "request" | "locals">
|
||||
): Promise<{ userId: string } | { response: Response }> {
|
||||
const localUserId =
|
||||
context.locals?.session?.userId || context.locals?.user?.id;
|
||||
|
||||
if (localUserId) {
|
||||
return { userId: localUserId };
|
||||
}
|
||||
|
||||
let session: Awaited<ReturnType<typeof auth.api.getSession>> | null = null;
|
||||
try {
|
||||
session = await auth.api.getSession({
|
||||
headers: context.request.headers,
|
||||
});
|
||||
} catch {
|
||||
return { response: unauthorizedResponse() };
|
||||
}
|
||||
|
||||
if (!session?.user?.id) {
|
||||
return { response: unauthorizedResponse() };
|
||||
}
|
||||
|
||||
return { userId: session.user.id };
|
||||
}
|
||||
@@ -2,28 +2,13 @@ import type { APIRoute } from "astro";
|
||||
import { db, mirrorJobs, events } from "@/lib/db";
|
||||
import { eq, count } from "drizzle-orm";
|
||||
import { createSecureErrorResponse } from "@/lib/utils";
|
||||
import { requireAuthenticatedUserId } from "@/lib/auth-guards";
|
||||
|
||||
export const POST: APIRoute = async ({ request }) => {
|
||||
export const POST: APIRoute = async ({ request, locals }) => {
|
||||
try {
|
||||
let body;
|
||||
try {
|
||||
body = await request.json();
|
||||
} catch (jsonError) {
|
||||
console.error("Invalid JSON in request body:", jsonError);
|
||||
return new Response(
|
||||
JSON.stringify({ error: "Invalid JSON in request body." }),
|
||||
{ status: 400, headers: { "Content-Type": "application/json" } }
|
||||
);
|
||||
}
|
||||
|
||||
const { userId } = body || {};
|
||||
|
||||
if (!userId) {
|
||||
return new Response(
|
||||
JSON.stringify({ error: "Missing 'userId' in request body." }),
|
||||
{ status: 400, headers: { "Content-Type": "application/json" } }
|
||||
);
|
||||
}
|
||||
const authResult = await requireAuthenticatedUserId({ request, locals });
|
||||
if ("response" in authResult) return authResult.response;
|
||||
const userId = authResult.userId;
|
||||
|
||||
// Start a transaction to ensure all operations succeed or fail together
|
||||
const result = await db.transaction(async (tx) => {
|
||||
|
||||
@@ -1,21 +1,16 @@
|
||||
import type { APIRoute } from "astro";
|
||||
import { db, mirrorJobs, configs } from "@/lib/db";
|
||||
import { db, mirrorJobs } from "@/lib/db";
|
||||
import { eq, sql } from "drizzle-orm";
|
||||
import { createSecureErrorResponse } from "@/lib/utils";
|
||||
import type { MirrorJob } from "@/lib/db/schema";
|
||||
import { repoStatusEnum } from "@/types/Repository";
|
||||
import { requireAuthenticatedUserId } from "@/lib/auth-guards";
|
||||
|
||||
export const GET: APIRoute = async ({ url }) => {
|
||||
export const GET: APIRoute = async ({ request, locals }) => {
|
||||
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" } }
|
||||
);
|
||||
}
|
||||
const authResult = await requireAuthenticatedUserId({ request, locals });
|
||||
if ("response" in authResult) return authResult.response;
|
||||
const userId = authResult.userId;
|
||||
|
||||
// Fetch mirror jobs associated with the user
|
||||
const jobs = await db
|
||||
|
||||
@@ -2,7 +2,6 @@ import type { APIRoute } from "astro";
|
||||
import { db, configs, users } from "@/lib/db";
|
||||
import { v4 as uuidv4 } from "uuid";
|
||||
import { eq } from "drizzle-orm";
|
||||
import { calculateCleanupInterval } from "@/lib/cleanup-service";
|
||||
import { createSecureErrorResponse } from "@/lib/utils";
|
||||
import {
|
||||
mapUiToDbConfig,
|
||||
@@ -12,20 +11,25 @@ import {
|
||||
mapDbScheduleToUi,
|
||||
mapDbCleanupToUi
|
||||
} from "@/lib/utils/config-mapper";
|
||||
import { encrypt, decrypt, migrateToken } from "@/lib/utils/encryption";
|
||||
import { encrypt, decrypt } from "@/lib/utils/encryption";
|
||||
import { createDefaultConfig } from "@/lib/utils/config-defaults";
|
||||
import { requireAuthenticatedUserId } from "@/lib/auth-guards";
|
||||
|
||||
export const POST: APIRoute = async ({ request }) => {
|
||||
export const POST: APIRoute = async ({ request, locals }) => {
|
||||
try {
|
||||
const body = await request.json();
|
||||
const { userId, githubConfig, giteaConfig, scheduleConfig, cleanupConfig, mirrorOptions, advancedOptions } = body;
|
||||
const authResult = await requireAuthenticatedUserId({ request, locals });
|
||||
if ("response" in authResult) return authResult.response;
|
||||
const userId = authResult.userId;
|
||||
|
||||
if (!userId || !githubConfig || !giteaConfig || !scheduleConfig || !cleanupConfig || !mirrorOptions || !advancedOptions) {
|
||||
const body = await request.json();
|
||||
const { githubConfig, giteaConfig, scheduleConfig, cleanupConfig, mirrorOptions, advancedOptions } = body;
|
||||
|
||||
if (!githubConfig || !giteaConfig || !scheduleConfig || !cleanupConfig || !mirrorOptions || !advancedOptions) {
|
||||
return new Response(
|
||||
JSON.stringify({
|
||||
success: false,
|
||||
message:
|
||||
"userId, githubConfig, giteaConfig, scheduleConfig, cleanupConfig, mirrorOptions, and advancedOptions are required.",
|
||||
"githubConfig, giteaConfig, scheduleConfig, cleanupConfig, mirrorOptions, and advancedOptions are required.",
|
||||
}),
|
||||
{
|
||||
status: 400,
|
||||
@@ -172,17 +176,11 @@ export const POST: APIRoute = async ({ request }) => {
|
||||
}
|
||||
};
|
||||
|
||||
export const GET: APIRoute = async ({ request }) => {
|
||||
export const GET: APIRoute = async ({ request, locals }) => {
|
||||
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" },
|
||||
});
|
||||
}
|
||||
const authResult = await requireAuthenticatedUserId({ request, locals });
|
||||
if ("response" in authResult) return authResult.response;
|
||||
const userId = authResult.userId;
|
||||
|
||||
// Fetch the configuration for the user
|
||||
const config = await db
|
||||
|
||||
@@ -3,24 +3,14 @@ import { db, repositories, organizations, mirrorJobs, configs } from "@/lib/db";
|
||||
import { eq, count, and, sql, or } from "drizzle-orm";
|
||||
import { jsonResponse, createSecureErrorResponse } 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,
|
||||
});
|
||||
}
|
||||
import { requireAuthenticatedUserId } from "@/lib/auth-guards";
|
||||
|
||||
export const GET: APIRoute = async ({ request, locals }) => {
|
||||
try {
|
||||
const authResult = await requireAuthenticatedUserId({ request, locals });
|
||||
if ("response" in authResult) return authResult.response;
|
||||
const userId = authResult.userId;
|
||||
|
||||
const [
|
||||
userRepos,
|
||||
userOrgs,
|
||||
|
||||
@@ -1,13 +1,11 @@
|
||||
import type { APIRoute } from "astro";
|
||||
import { getNewEvents } from "@/lib/events";
|
||||
import { requireAuthenticatedUserId } from "@/lib/auth-guards";
|
||||
|
||||
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 });
|
||||
}
|
||||
export const GET: APIRoute = async ({ request, locals }) => {
|
||||
const authResult = await requireAuthenticatedUserId({ request, locals });
|
||||
if ("response" in authResult) return authResult.response;
|
||||
const userId = authResult.userId;
|
||||
|
||||
// Create a new ReadableStream for SSE
|
||||
const stream = new ReadableStream({
|
||||
|
||||
@@ -9,22 +9,14 @@ import {
|
||||
import type { Organization } from "@/lib/db/schema";
|
||||
import { repoStatusEnum } from "@/types/Repository";
|
||||
import { jsonResponse, createSecureErrorResponse } from "@/lib/utils";
|
||||
import { requireAuthenticatedUserId } from "@/lib/auth-guards";
|
||||
|
||||
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,
|
||||
});
|
||||
}
|
||||
|
||||
export const GET: APIRoute = async ({ request, locals }) => {
|
||||
try {
|
||||
const authResult = await requireAuthenticatedUserId({ request, locals });
|
||||
if ("response" in authResult) return authResult.response;
|
||||
const userId = authResult.userId;
|
||||
|
||||
// Fetch the user's active configuration to respect filtering settings
|
||||
const [config] = await db
|
||||
.select()
|
||||
|
||||
@@ -7,19 +7,14 @@ import {
|
||||
type RepositoryApiResponse,
|
||||
} from "@/types/Repository";
|
||||
import { jsonResponse, createSecureErrorResponse } from "@/lib/utils";
|
||||
import { requireAuthenticatedUserId } from "@/lib/auth-guards";
|
||||
|
||||
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,
|
||||
});
|
||||
}
|
||||
|
||||
export const GET: APIRoute = async ({ request, locals }) => {
|
||||
try {
|
||||
const authResult = await requireAuthenticatedUserId({ request, locals });
|
||||
if ("response" in authResult) return authResult.response;
|
||||
const userId = authResult.userId;
|
||||
|
||||
// Fetch the user's active configuration
|
||||
const [config] = await db
|
||||
.select()
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
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 { and, eq, inArray } from "drizzle-orm";
|
||||
import { createGitHubClient } from "@/lib/github";
|
||||
import { mirrorGitHubOrgToGitea } from "@/lib/gitea";
|
||||
import { repoStatusEnum } from "@/types/Repository";
|
||||
@@ -10,17 +10,22 @@ import { createSecureErrorResponse } from "@/lib/utils";
|
||||
import { processWithResilience } from "@/lib/utils/concurrency";
|
||||
import { v4 as uuidv4 } from "uuid";
|
||||
import { getDecryptedGitHubToken } from "@/lib/utils/config-encryption";
|
||||
import { requireAuthenticatedUserId } from "@/lib/auth-guards";
|
||||
|
||||
export const POST: APIRoute = async ({ request }) => {
|
||||
export const POST: APIRoute = async ({ request, locals }) => {
|
||||
try {
|
||||
const body: MirrorOrgRequest = await request.json();
|
||||
const { userId, organizationIds } = body;
|
||||
const authResult = await requireAuthenticatedUserId({ request, locals });
|
||||
if ("response" in authResult) return authResult.response;
|
||||
const userId = authResult.userId;
|
||||
|
||||
if (!userId || !organizationIds || !Array.isArray(organizationIds)) {
|
||||
const body: MirrorOrgRequest = await request.json();
|
||||
const { organizationIds } = body;
|
||||
|
||||
if (!organizationIds || !Array.isArray(organizationIds)) {
|
||||
return new Response(
|
||||
JSON.stringify({
|
||||
success: false,
|
||||
message: "userId and organizationIds are required.",
|
||||
message: "organizationIds are required.",
|
||||
}),
|
||||
{ status: 400, headers: { "Content-Type": "application/json" } }
|
||||
);
|
||||
@@ -56,7 +61,12 @@ export const POST: APIRoute = async ({ request }) => {
|
||||
const orgs = await db
|
||||
.select()
|
||||
.from(organizations)
|
||||
.where(inArray(organizations.id, organizationIds));
|
||||
.where(
|
||||
and(
|
||||
eq(organizations.userId, userId),
|
||||
inArray(organizations.id, organizationIds)
|
||||
)
|
||||
);
|
||||
|
||||
if (!orgs.length) {
|
||||
return new Response(
|
||||
|
||||
@@ -90,6 +90,7 @@ mock.module("@/lib/utils/concurrency", () => ({
|
||||
|
||||
// Mock drizzle-orm
|
||||
mock.module("drizzle-orm", () => ({
|
||||
and: mock(() => ({})),
|
||||
eq: mock(() => ({})),
|
||||
inArray: mock(() => ({}))
|
||||
}));
|
||||
@@ -121,7 +122,7 @@ describe("Repository Mirroring API", () => {
|
||||
console.error = originalConsoleError;
|
||||
});
|
||||
|
||||
test("returns 400 if userId is missing", async () => {
|
||||
test("returns 401 when request is unauthenticated", async () => {
|
||||
const request = new Request("http://localhost/api/job/mirror-repo", {
|
||||
method: "POST",
|
||||
headers: {
|
||||
@@ -134,11 +135,11 @@ describe("Repository Mirroring API", () => {
|
||||
|
||||
const response = await POST({ request } as any);
|
||||
|
||||
expect(response.status).toBe(400);
|
||||
expect(response.status).toBe(401);
|
||||
|
||||
const data = await response.json();
|
||||
expect(data.success).toBe(false);
|
||||
expect(data.message).toBe("userId and repositoryIds are required.");
|
||||
expect(data.error).toBe("Unauthorized");
|
||||
});
|
||||
|
||||
test("returns 400 if repositoryIds is missing", async () => {
|
||||
@@ -152,13 +153,18 @@ describe("Repository Mirroring API", () => {
|
||||
})
|
||||
});
|
||||
|
||||
const response = await POST({ request } as any);
|
||||
const response = await POST({
|
||||
request,
|
||||
locals: {
|
||||
session: { userId: "user-id" },
|
||||
},
|
||||
} as any);
|
||||
|
||||
expect(response.status).toBe(400);
|
||||
|
||||
const data = await response.json();
|
||||
expect(data.success).toBe(false);
|
||||
expect(data.message).toBe("userId and repositoryIds are required.");
|
||||
expect(data.message).toBe("repositoryIds are required.");
|
||||
});
|
||||
|
||||
test("returns 200 and starts mirroring repositories", async () => {
|
||||
@@ -173,7 +179,12 @@ describe("Repository Mirroring API", () => {
|
||||
})
|
||||
});
|
||||
|
||||
const response = await POST({ request } as any);
|
||||
const response = await POST({
|
||||
request,
|
||||
locals: {
|
||||
session: { userId: "user-id" },
|
||||
},
|
||||
} as any);
|
||||
|
||||
expect(response.status).toBe(200);
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
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 { and, eq, inArray } from "drizzle-orm";
|
||||
import { repositoryVisibilityEnum, repoStatusEnum } from "@/types/Repository";
|
||||
import {
|
||||
mirrorGithubRepoToGitea,
|
||||
@@ -12,17 +12,22 @@ import { createGitHubClient } from "@/lib/github";
|
||||
import { getDecryptedGitHubToken } from "@/lib/utils/config-encryption";
|
||||
import { processWithResilience } from "@/lib/utils/concurrency";
|
||||
import { createSecureErrorResponse } from "@/lib/utils";
|
||||
import { requireAuthenticatedUserId } from "@/lib/auth-guards";
|
||||
|
||||
export const POST: APIRoute = async ({ request }) => {
|
||||
export const POST: APIRoute = async ({ request, locals }) => {
|
||||
try {
|
||||
const body: MirrorRepoRequest = await request.json();
|
||||
const { userId, repositoryIds } = body;
|
||||
const authResult = await requireAuthenticatedUserId({ request, locals });
|
||||
if ("response" in authResult) return authResult.response;
|
||||
const userId = authResult.userId;
|
||||
|
||||
if (!userId || !repositoryIds || !Array.isArray(repositoryIds)) {
|
||||
const body: MirrorRepoRequest = await request.json();
|
||||
const { repositoryIds } = body;
|
||||
|
||||
if (!repositoryIds || !Array.isArray(repositoryIds)) {
|
||||
return new Response(
|
||||
JSON.stringify({
|
||||
success: false,
|
||||
message: "userId and repositoryIds are required.",
|
||||
message: "repositoryIds are required.",
|
||||
}),
|
||||
{ status: 400, headers: { "Content-Type": "application/json" } }
|
||||
);
|
||||
@@ -58,7 +63,12 @@ export const POST: APIRoute = async ({ request }) => {
|
||||
const repos = await db
|
||||
.select()
|
||||
.from(repositories)
|
||||
.where(inArray(repositories.id, repositoryIds));
|
||||
.where(
|
||||
and(
|
||||
eq(repositories.userId, userId),
|
||||
inArray(repositories.id, repositoryIds)
|
||||
)
|
||||
);
|
||||
|
||||
if (!repos.length) {
|
||||
return new Response(
|
||||
|
||||
@@ -4,17 +4,22 @@ import { db, configs, repositories } from "@/lib/db";
|
||||
import { repositoryVisibilityEnum, repoStatusEnum } from "@/types/Repository";
|
||||
import type { ResetMetadataRequest, ResetMetadataResponse } from "@/types/reset-metadata";
|
||||
import { createSecureErrorResponse } from "@/lib/utils";
|
||||
import { requireAuthenticatedUserId } from "@/lib/auth-guards";
|
||||
|
||||
export const POST: APIRoute = async ({ request }) => {
|
||||
export const POST: APIRoute = async ({ request, locals }) => {
|
||||
try {
|
||||
const body: ResetMetadataRequest = await request.json();
|
||||
const { userId, repositoryIds } = body;
|
||||
const authResult = await requireAuthenticatedUserId({ request, locals });
|
||||
if ("response" in authResult) return authResult.response;
|
||||
const userId = authResult.userId;
|
||||
|
||||
if (!userId || !repositoryIds || !Array.isArray(repositoryIds)) {
|
||||
const body: ResetMetadataRequest = await request.json();
|
||||
const { repositoryIds } = body;
|
||||
|
||||
if (!repositoryIds || !Array.isArray(repositoryIds)) {
|
||||
return new Response(
|
||||
JSON.stringify({
|
||||
success: false,
|
||||
message: "userId and repositoryIds are required.",
|
||||
message: "repositoryIds are required.",
|
||||
}),
|
||||
{ status: 400, headers: { "Content-Type": "application/json" } }
|
||||
);
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import type { APIRoute } from "astro";
|
||||
import { db, configs, repositories } from "@/lib/db";
|
||||
import { eq, inArray } from "drizzle-orm";
|
||||
import { and, eq, inArray } from "drizzle-orm";
|
||||
import { getGiteaRepoOwnerAsync, isRepoPresentInGitea } from "@/lib/gitea";
|
||||
import {
|
||||
mirrorGithubRepoToGitea,
|
||||
@@ -14,17 +14,22 @@ import { processWithRetry } from "@/lib/utils/concurrency";
|
||||
import { createMirrorJob } from "@/lib/helpers";
|
||||
import { createSecureErrorResponse } from "@/lib/utils";
|
||||
import { getDecryptedGitHubToken } from "@/lib/utils/config-encryption";
|
||||
import { requireAuthenticatedUserId } from "@/lib/auth-guards";
|
||||
|
||||
export const POST: APIRoute = async ({ request }) => {
|
||||
export const POST: APIRoute = async ({ request, locals }) => {
|
||||
try {
|
||||
const body: RetryRepoRequest = await request.json();
|
||||
const { userId, repositoryIds } = body;
|
||||
const authResult = await requireAuthenticatedUserId({ request, locals });
|
||||
if ("response" in authResult) return authResult.response;
|
||||
const userId = authResult.userId;
|
||||
|
||||
if (!userId || !repositoryIds || !Array.isArray(repositoryIds)) {
|
||||
const body: RetryRepoRequest = await request.json();
|
||||
const { repositoryIds } = body;
|
||||
|
||||
if (!repositoryIds || !Array.isArray(repositoryIds)) {
|
||||
return new Response(
|
||||
JSON.stringify({
|
||||
success: false,
|
||||
message: "userId and repositoryIds are required.",
|
||||
message: "repositoryIds are required.",
|
||||
}),
|
||||
{ status: 400, headers: { "Content-Type": "application/json" } }
|
||||
);
|
||||
@@ -60,7 +65,12 @@ export const POST: APIRoute = async ({ request }) => {
|
||||
const repos = await db
|
||||
.select()
|
||||
.from(repositories)
|
||||
.where(inArray(repositories.id, repositoryIds));
|
||||
.where(
|
||||
and(
|
||||
eq(repositories.userId, userId),
|
||||
inArray(repositories.id, repositoryIds)
|
||||
)
|
||||
);
|
||||
|
||||
if (!repos.length) {
|
||||
return new Response(
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import type { APIRoute } from "astro";
|
||||
import { db, configs, repositories } from "@/lib/db";
|
||||
import { eq, or } from "drizzle-orm";
|
||||
import { and, eq, or } from "drizzle-orm";
|
||||
import { repoStatusEnum, repositoryVisibilityEnum } from "@/types/Repository";
|
||||
import { isRepoPresentInGitea, syncGiteaRepo } from "@/lib/gitea";
|
||||
import type {
|
||||
@@ -9,22 +9,15 @@ import type {
|
||||
} from "@/types/sync";
|
||||
import { createSecureErrorResponse } from "@/lib/utils";
|
||||
import { parseInterval } from "@/lib/utils/duration-parser";
|
||||
import { requireAuthenticatedUserId } from "@/lib/auth-guards";
|
||||
|
||||
export const POST: APIRoute = async ({ request }) => {
|
||||
export const POST: APIRoute = async ({ request, locals }) => {
|
||||
try {
|
||||
const body: ScheduleSyncRepoRequest = await request.json();
|
||||
const { userId } = body;
|
||||
const authResult = await requireAuthenticatedUserId({ request, locals });
|
||||
if ("response" in authResult) return authResult.response;
|
||||
const userId = authResult.userId;
|
||||
|
||||
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" } }
|
||||
);
|
||||
}
|
||||
await request.json().catch(() => ({} as ScheduleSyncRepoRequest));
|
||||
|
||||
// Fetch config for the user
|
||||
const configResult = await db
|
||||
@@ -51,12 +44,14 @@ export const POST: APIRoute = async ({ request }) => {
|
||||
.select()
|
||||
.from(repositories)
|
||||
.where(
|
||||
eq(repositories.userId, userId) &&
|
||||
and(
|
||||
eq(repositories.userId, userId),
|
||||
or(
|
||||
eq(repositories.status, "mirrored"),
|
||||
eq(repositories.status, "synced"),
|
||||
eq(repositories.status, "failed")
|
||||
)
|
||||
)
|
||||
);
|
||||
|
||||
if (!repos.length) {
|
||||
|
||||
@@ -1,23 +1,28 @@
|
||||
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 { and, eq, inArray } from "drizzle-orm";
|
||||
import { repositoryVisibilityEnum, repoStatusEnum } from "@/types/Repository";
|
||||
import { syncGiteaRepo } from "@/lib/gitea";
|
||||
import type { SyncRepoResponse } from "@/types/sync";
|
||||
import { processWithResilience } from "@/lib/utils/concurrency";
|
||||
import { createSecureErrorResponse } from "@/lib/utils";
|
||||
import { requireAuthenticatedUserId } from "@/lib/auth-guards";
|
||||
|
||||
export const POST: APIRoute = async ({ request }) => {
|
||||
export const POST: APIRoute = async ({ request, locals }) => {
|
||||
try {
|
||||
const body: MirrorRepoRequest = await request.json();
|
||||
const { userId, repositoryIds } = body;
|
||||
const authResult = await requireAuthenticatedUserId({ request, locals });
|
||||
if ("response" in authResult) return authResult.response;
|
||||
const userId = authResult.userId;
|
||||
|
||||
if (!userId || !repositoryIds || !Array.isArray(repositoryIds)) {
|
||||
const body: MirrorRepoRequest = await request.json();
|
||||
const { repositoryIds } = body;
|
||||
|
||||
if (!repositoryIds || !Array.isArray(repositoryIds)) {
|
||||
return new Response(
|
||||
JSON.stringify({
|
||||
success: false,
|
||||
message: "userId and repositoryIds are required.",
|
||||
message: "repositoryIds are required.",
|
||||
}),
|
||||
{ status: 400, headers: { "Content-Type": "application/json" } }
|
||||
);
|
||||
@@ -53,7 +58,12 @@ export const POST: APIRoute = async ({ request }) => {
|
||||
const repos = await db
|
||||
.select()
|
||||
.from(repositories)
|
||||
.where(inArray(repositories.id, repositoryIds));
|
||||
.where(
|
||||
and(
|
||||
eq(repositories.userId, userId),
|
||||
inArray(repositories.id, repositoryIds)
|
||||
)
|
||||
);
|
||||
|
||||
if (!repos.length) {
|
||||
return new Response(
|
||||
|
||||
@@ -2,18 +2,23 @@ import type { APIContext } from "astro";
|
||||
import { db, organizations } from "@/lib/db";
|
||||
import { eq, and } from "drizzle-orm";
|
||||
import { createSecureErrorResponse } from "@/lib/utils";
|
||||
import { requireAuthenticatedUserId } from "@/lib/auth-guards";
|
||||
|
||||
export async function PATCH({ params, request }: APIContext) {
|
||||
export async function PATCH({ params, request, locals }: APIContext) {
|
||||
try {
|
||||
const authResult = await requireAuthenticatedUserId({ request, locals });
|
||||
if ("response" in authResult) return authResult.response;
|
||||
const userId = authResult.userId;
|
||||
|
||||
const { id } = params;
|
||||
const body = await request.json();
|
||||
const { status, userId } = body;
|
||||
const { status } = body;
|
||||
|
||||
if (!id || !userId) {
|
||||
if (!id) {
|
||||
return new Response(
|
||||
JSON.stringify({
|
||||
success: false,
|
||||
error: "Organization ID and User ID are required",
|
||||
error: "Organization ID is required",
|
||||
}),
|
||||
{
|
||||
status: 400,
|
||||
|
||||
@@ -6,19 +6,16 @@ import { RateLimitManager } from "@/lib/rate-limit-manager";
|
||||
import { createGitHubClient } from "@/lib/github";
|
||||
import { getDecryptedGitHubToken } from "@/lib/utils/config-encryption";
|
||||
import { configs } from "@/lib/db";
|
||||
import { requireAuthenticatedUserId } from "@/lib/auth-guards";
|
||||
|
||||
export const GET: APIRoute = async ({ request, locals }) => {
|
||||
const authResult = await requireAuthenticatedUserId({ request, locals });
|
||||
if ("response" in authResult) return authResult.response;
|
||||
const userId = authResult.userId;
|
||||
|
||||
export const GET: APIRoute = async ({ request }) => {
|
||||
const url = new URL(request.url);
|
||||
const userId = url.searchParams.get("userId");
|
||||
const refresh = url.searchParams.get("refresh") === "true";
|
||||
|
||||
if (!userId) {
|
||||
return jsonResponse({
|
||||
data: { error: "Missing userId" },
|
||||
status: 400,
|
||||
});
|
||||
}
|
||||
|
||||
try {
|
||||
// If refresh is requested, fetch current rate limit from GitHub
|
||||
if (refresh) {
|
||||
|
||||
@@ -3,18 +3,23 @@ import { db, repositories } from "@/lib/db";
|
||||
import { eq, and } from "drizzle-orm";
|
||||
import { createSecureErrorResponse } from "@/lib/utils";
|
||||
import { repoStatusEnum } from "@/types/Repository";
|
||||
import { requireAuthenticatedUserId } from "@/lib/auth-guards";
|
||||
|
||||
export async function PATCH({ params, request }: APIContext) {
|
||||
export async function PATCH({ params, request, locals }: APIContext) {
|
||||
try {
|
||||
const authResult = await requireAuthenticatedUserId({ request, locals });
|
||||
if ("response" in authResult) return authResult.response;
|
||||
const userId = authResult.userId;
|
||||
|
||||
const { id } = params;
|
||||
const body = await request.json();
|
||||
const { status, userId } = body;
|
||||
const { status } = body;
|
||||
|
||||
if (!id || !userId) {
|
||||
if (!id) {
|
||||
return new Response(
|
||||
JSON.stringify({
|
||||
success: false,
|
||||
error: "Repository ID and User ID are required",
|
||||
error: "Repository ID is required",
|
||||
}),
|
||||
{
|
||||
status: 400,
|
||||
|
||||
@@ -1,13 +1,11 @@
|
||||
import type { APIRoute } from "astro";
|
||||
import { getNewEvents } from "@/lib/events";
|
||||
import { requireAuthenticatedUserId } from "@/lib/auth-guards";
|
||||
|
||||
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 });
|
||||
}
|
||||
export const GET: APIRoute = async ({ request, locals }) => {
|
||||
const authResult = await requireAuthenticatedUserId({ request, locals });
|
||||
if ("response" in authResult) return authResult.response;
|
||||
const userId = authResult.userId;
|
||||
|
||||
const channel = `mirror-status:${userId}`;
|
||||
let isClosed = false;
|
||||
|
||||
@@ -12,14 +12,12 @@ import {
|
||||
import { jsonResponse, createSecureErrorResponse } from "@/lib/utils";
|
||||
import { mergeGitReposPreferStarred, calcBatchSizeForInsert } from "@/lib/repo-utils";
|
||||
import { getDecryptedGitHubToken } from "@/lib/utils/config-encryption";
|
||||
import { requireAuthenticatedUserId } from "@/lib/auth-guards";
|
||||
|
||||
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 });
|
||||
}
|
||||
export const POST: APIRoute = async ({ request, locals }) => {
|
||||
const authResult = await requireAuthenticatedUserId({ request, locals });
|
||||
if ("response" in authResult) return authResult.response;
|
||||
const userId = authResult.userId;
|
||||
|
||||
try {
|
||||
const [config] = await db
|
||||
|
||||
@@ -10,15 +10,20 @@ import type { RepositoryVisibility, RepoStatus } from "@/types/Repository";
|
||||
import { v4 as uuidv4 } from "uuid";
|
||||
import { decryptConfigTokens } from "@/lib/utils/config-encryption";
|
||||
import { createGitHubClient } from "@/lib/github";
|
||||
import { requireAuthenticatedUserId } from "@/lib/auth-guards";
|
||||
|
||||
export const POST: APIRoute = async ({ request }) => {
|
||||
export const POST: APIRoute = async ({ request, locals }) => {
|
||||
try {
|
||||
const body: AddOrganizationApiRequest = await request.json();
|
||||
const { role, org, userId, force = false } = body;
|
||||
const authResult = await requireAuthenticatedUserId({ request, locals });
|
||||
if ("response" in authResult) return authResult.response;
|
||||
const userId = authResult.userId;
|
||||
|
||||
if (!org || !userId || !role) {
|
||||
const body: AddOrganizationApiRequest = await request.json();
|
||||
const { role, org, force = false } = body;
|
||||
|
||||
if (!org || !role) {
|
||||
return jsonResponse({
|
||||
data: { success: false, error: "Missing org, role or userId" },
|
||||
data: { success: false, error: "Missing org or role" },
|
||||
status: 400,
|
||||
});
|
||||
}
|
||||
|
||||
@@ -11,17 +11,22 @@ import type {
|
||||
RepositoryVisibility,
|
||||
} from "@/types/Repository";
|
||||
import { createMirrorJob } from "@/lib/helpers";
|
||||
import { requireAuthenticatedUserId } from "@/lib/auth-guards";
|
||||
|
||||
export const POST: APIRoute = async ({ request }) => {
|
||||
export const POST: APIRoute = async ({ request, locals }) => {
|
||||
try {
|
||||
const body: AddRepositoriesApiRequest = await request.json();
|
||||
const { owner, repo, userId, force = false } = body;
|
||||
const authResult = await requireAuthenticatedUserId({ request, locals });
|
||||
if ("response" in authResult) return authResult.response;
|
||||
const userId = authResult.userId;
|
||||
|
||||
if (!owner || !repo || !userId) {
|
||||
const body: AddRepositoriesApiRequest = await request.json();
|
||||
const { owner, repo, force = false } = body;
|
||||
|
||||
if (!owner || !repo) {
|
||||
return new Response(
|
||||
JSON.stringify({
|
||||
success: false,
|
||||
error: "Missing owner, repo, or userId",
|
||||
error: "Missing owner or repo",
|
||||
}),
|
||||
{ status: 400 }
|
||||
);
|
||||
@@ -34,7 +39,7 @@ export const POST: APIRoute = async ({ request }) => {
|
||||
return jsonResponse({
|
||||
data: {
|
||||
success: false,
|
||||
error: "Missing owner, repo, or userId",
|
||||
error: "Missing owner or repo",
|
||||
},
|
||||
status: 400,
|
||||
});
|
||||
|
||||
@@ -2,16 +2,21 @@ import type { APIRoute } from "astro";
|
||||
import { publishEvent } from "@/lib/events";
|
||||
import { v4 as uuidv4 } from "uuid";
|
||||
import { createSecureErrorResponse } from "@/lib/utils";
|
||||
import { requireAuthenticatedUserId } from "@/lib/auth-guards";
|
||||
|
||||
export const POST: APIRoute = async ({ request }) => {
|
||||
export const POST: APIRoute = async ({ request, locals }) => {
|
||||
try {
|
||||
const body = await request.json();
|
||||
const { userId, message, status } = body;
|
||||
const authResult = await requireAuthenticatedUserId({ request, locals });
|
||||
if ("response" in authResult) return authResult.response;
|
||||
const userId = authResult.userId;
|
||||
|
||||
if (!userId || !message || !status) {
|
||||
const body = await request.json();
|
||||
const { message, status } = body;
|
||||
|
||||
if (!message || !status) {
|
||||
return new Response(
|
||||
JSON.stringify({
|
||||
error: "Missing required fields: userId, message, status",
|
||||
error: "Missing required fields: message, status",
|
||||
}),
|
||||
{ status: 400 }
|
||||
);
|
||||
|
||||
Reference in New Issue
Block a user