diff --git a/src/lib/auth-guards.test.ts b/src/lib/auth-guards.test.ts new file mode 100644 index 0000000..f93544b --- /dev/null +++ b/src/lib/auth-guards.test.ts @@ -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); + } + }); +}); diff --git a/src/lib/auth-guards.ts b/src/lib/auth-guards.ts new file mode 100644 index 0000000..64b9b6b --- /dev/null +++ b/src/lib/auth-guards.ts @@ -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 +): Promise<{ userId: string } | { response: Response }> { + const localUserId = + context.locals?.session?.userId || context.locals?.user?.id; + + if (localUserId) { + return { userId: localUserId }; + } + + let session: Awaited> | 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 }; +} diff --git a/src/pages/api/activities/cleanup.ts b/src/pages/api/activities/cleanup.ts index 186c35a..3f5b4cb 100644 --- a/src/pages/api/activities/cleanup.ts +++ b/src/pages/api/activities/cleanup.ts @@ -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) => { diff --git a/src/pages/api/activities/index.ts b/src/pages/api/activities/index.ts index 8d6014f..c76d363 100644 --- a/src/pages/api/activities/index.ts +++ b/src/pages/api/activities/index.ts @@ -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 diff --git a/src/pages/api/config/index.ts b/src/pages/api/config/index.ts index 5b59745..37b14a0 100644 --- a/src/pages/api/config/index.ts +++ b/src/pages/api/config/index.ts @@ -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 diff --git a/src/pages/api/dashboard/index.ts b/src/pages/api/dashboard/index.ts index b225b7c..9963988 100644 --- a/src/pages/api/dashboard/index.ts +++ b/src/pages/api/dashboard/index.ts @@ -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, diff --git a/src/pages/api/events/index.ts b/src/pages/api/events/index.ts index dd26e73..781cd5a 100644 --- a/src/pages/api/events/index.ts +++ b/src/pages/api/events/index.ts @@ -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({ @@ -66,4 +64,4 @@ export const GET: APIRoute = async ({ request }) => { "X-Accel-Buffering": "no", // Disable nginx buffering }, }); -}; \ No newline at end of file +}; diff --git a/src/pages/api/github/organizations.ts b/src/pages/api/github/organizations.ts index 5310fd8..7c3bff2 100644 --- a/src/pages/api/github/organizations.ts +++ b/src/pages/api/github/organizations.ts @@ -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() diff --git a/src/pages/api/github/repositories.ts b/src/pages/api/github/repositories.ts index fced8d3..fd05a34 100644 --- a/src/pages/api/github/repositories.ts +++ b/src/pages/api/github/repositories.ts @@ -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() diff --git a/src/pages/api/job/mirror-org.ts b/src/pages/api/job/mirror-org.ts index 488b80d..9342c96 100644 --- a/src/pages/api/job/mirror-org.ts +++ b/src/pages/api/job/mirror-org.ts @@ -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( diff --git a/src/pages/api/job/mirror-repo.test.ts b/src/pages/api/job/mirror-repo.test.ts index 8c3e7ec..bad938e 100644 --- a/src/pages/api/job/mirror-repo.test.ts +++ b/src/pages/api/job/mirror-repo.test.ts @@ -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); diff --git a/src/pages/api/job/mirror-repo.ts b/src/pages/api/job/mirror-repo.ts index 185927f..9cf4908 100644 --- a/src/pages/api/job/mirror-repo.ts +++ b/src/pages/api/job/mirror-repo.ts @@ -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( diff --git a/src/pages/api/job/reset-metadata.ts b/src/pages/api/job/reset-metadata.ts index a0b299f..7203ac9 100644 --- a/src/pages/api/job/reset-metadata.ts +++ b/src/pages/api/job/reset-metadata.ts @@ -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" } } ); diff --git a/src/pages/api/job/retry-repo.ts b/src/pages/api/job/retry-repo.ts index 443eb31..06d5a33 100644 --- a/src/pages/api/job/retry-repo.ts +++ b/src/pages/api/job/retry-repo.ts @@ -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( diff --git a/src/pages/api/job/schedule-sync-repo.ts b/src/pages/api/job/schedule-sync-repo.ts index 3cac25e..a2223c5 100644 --- a/src/pages/api/job/schedule-sync-repo.ts +++ b/src/pages/api/job/schedule-sync-repo.ts @@ -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) { diff --git a/src/pages/api/job/sync-repo.ts b/src/pages/api/job/sync-repo.ts index a24ad2d..7b2cc79 100644 --- a/src/pages/api/job/sync-repo.ts +++ b/src/pages/api/job/sync-repo.ts @@ -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( diff --git a/src/pages/api/organizations/[id]/status.ts b/src/pages/api/organizations/[id]/status.ts index 6cd3f41..9253c8a 100644 --- a/src/pages/api/organizations/[id]/status.ts +++ b/src/pages/api/organizations/[id]/status.ts @@ -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, @@ -78,4 +83,4 @@ export async function PATCH({ params, request }: APIContext) { } catch (error) { return createSecureErrorResponse(error); } -} \ No newline at end of file +} diff --git a/src/pages/api/rate-limit/index.ts b/src/pages/api/rate-limit/index.ts index dabb45e..241f21c 100644 --- a/src/pages/api/rate-limit/index.ts +++ b/src/pages/api/rate-limit/index.ts @@ -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) { @@ -101,4 +98,4 @@ export const GET: APIRoute = async ({ request }) => { } catch (error) { return createSecureErrorResponse(error, "rate limit check", 500); } -}; \ No newline at end of file +}; diff --git a/src/pages/api/repositories/[id]/status.ts b/src/pages/api/repositories/[id]/status.ts index 1b49343..1d12296 100644 --- a/src/pages/api/repositories/[id]/status.ts +++ b/src/pages/api/repositories/[id]/status.ts @@ -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, @@ -79,4 +84,4 @@ export async function PATCH({ params, request }: APIContext) { } catch (error) { return createSecureErrorResponse(error); } -} \ No newline at end of file +} diff --git a/src/pages/api/sse/index.ts b/src/pages/api/sse/index.ts index 55e06a5..ce47816 100644 --- a/src/pages/api/sse/index.ts +++ b/src/pages/api/sse/index.ts @@ -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; diff --git a/src/pages/api/sync/index.ts b/src/pages/api/sync/index.ts index 2fb6924..4a28f90 100644 --- a/src/pages/api/sync/index.ts +++ b/src/pages/api/sync/index.ts @@ -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 diff --git a/src/pages/api/sync/organization.ts b/src/pages/api/sync/organization.ts index a849399..5f60b16 100644 --- a/src/pages/api/sync/organization.ts +++ b/src/pages/api/sync/organization.ts @@ -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, }); } diff --git a/src/pages/api/sync/repository.ts b/src/pages/api/sync/repository.ts index 96d6c93..1192405 100644 --- a/src/pages/api/sync/repository.ts +++ b/src/pages/api/sync/repository.ts @@ -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, }); diff --git a/src/pages/api/test-event.ts b/src/pages/api/test-event.ts index f0ae050..2a8cd08 100644 --- a/src/pages/api/test-event.ts +++ b/src/pages/api/test-event.ts @@ -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 } );