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:
ARUNAVO RAY
2026-02-24 11:47:29 +05:30
committed by GitHub
parent f28ac8fa09
commit 6a548e3dac
24 changed files with 334 additions and 201 deletions

View 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
View 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 };
}

View File

@@ -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) => {

View File

@@ -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

View File

@@ -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

View File

@@ -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,

View File

@@ -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
},
});
};
};

View File

@@ -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()

View File

@@ -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()

View File

@@ -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(

View File

@@ -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);

View File

@@ -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(

View File

@@ -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" } }
);

View File

@@ -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(

View File

@@ -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) {

View File

@@ -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(

View File

@@ -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);
}
}
}

View File

@@ -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);
}
};
};

View File

@@ -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);
}
}
}

View File

@@ -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;

View File

@@ -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

View File

@@ -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,
});
}

View File

@@ -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,
});

View File

@@ -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 }
);