mirror of
https://gitbruv.vercel.app/api/git/bruv/gitbruv.git
synced 2025-12-20 23:24:09 +01:00
rate limit and auth checks
This commit is contained in:
parent
125c6fdd6a
commit
91208e44a1
9 changed files with 403 additions and 257 deletions
69
lib/api-auth.ts
Normal file
69
lib/api-auth.ts
Normal file
|
|
@ -0,0 +1,69 @@
|
|||
import { NextRequest } from "next/server";
|
||||
import { db } from "@/db";
|
||||
import { users } from "@/db/schema";
|
||||
import { eq } from "drizzle-orm";
|
||||
import { auth } from "@/lib/auth";
|
||||
import { getSession } from "@/lib/session";
|
||||
|
||||
export interface AuthenticatedUser {
|
||||
id: string;
|
||||
username: string;
|
||||
}
|
||||
|
||||
export async function authenticateRequest(request: NextRequest): Promise<AuthenticatedUser | null> {
|
||||
const session = await getSession();
|
||||
if (session?.user) {
|
||||
const user = await db.query.users.findFirst({
|
||||
where: eq(users.id, session.user.id),
|
||||
});
|
||||
if (user) {
|
||||
return { id: user.id, username: user.username };
|
||||
}
|
||||
}
|
||||
|
||||
const authHeader = request.headers.get("authorization");
|
||||
if (authHeader?.startsWith("Basic ")) {
|
||||
const credentials = Buffer.from(authHeader.split(" ")[1], "base64").toString("utf-8");
|
||||
const [email, password] = credentials.split(":");
|
||||
if (email && password) {
|
||||
try {
|
||||
const result = await auth.api.signInEmail({
|
||||
body: { email, password },
|
||||
asResponse: false,
|
||||
});
|
||||
|
||||
if (result?.user) {
|
||||
const user = await db.query.users.findFirst({
|
||||
where: eq(users.email, email),
|
||||
});
|
||||
if (user) {
|
||||
return { id: user.id, username: user.username };
|
||||
}
|
||||
}
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const bearerMatch = request.headers.get("authorization")?.match(/^Bearer (.+)$/);
|
||||
if (bearerMatch) {
|
||||
try {
|
||||
const tokenSession = await auth.api.getSession({
|
||||
headers: request.headers,
|
||||
});
|
||||
if (tokenSession?.user) {
|
||||
const user = await db.query.users.findFirst({
|
||||
where: eq(users.id, tokenSession.user.id),
|
||||
});
|
||||
if (user) {
|
||||
return { id: user.id, username: user.username };
|
||||
}
|
||||
}
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
|
@ -178,3 +178,5 @@ export function createR2Fs(repoPrefix: string) {
|
|||
export function getRepoPrefix(userId: string, repoName: string): string {
|
||||
return `repos/${userId}/${repoName}`;
|
||||
}
|
||||
|
||||
export type R2Fs = ReturnType<typeof createR2Fs>;
|
||||
|
|
|
|||
96
lib/rate-limit.ts
Normal file
96
lib/rate-limit.ts
Normal file
|
|
@ -0,0 +1,96 @@
|
|||
import { NextRequest, NextResponse } from "next/server";
|
||||
|
||||
interface RateLimitEntry {
|
||||
count: number;
|
||||
resetAt: number;
|
||||
}
|
||||
|
||||
const store = new Map<string, RateLimitEntry>();
|
||||
|
||||
setInterval(() => {
|
||||
const now = Date.now();
|
||||
for (const [key, entry] of store) {
|
||||
if (entry.resetAt < now) {
|
||||
store.delete(key);
|
||||
}
|
||||
}
|
||||
}, 60000);
|
||||
|
||||
export interface RateLimitConfig {
|
||||
limit: number;
|
||||
windowMs: number;
|
||||
}
|
||||
|
||||
const DEFAULT_CONFIG: RateLimitConfig = {
|
||||
limit: 60,
|
||||
windowMs: 60 * 1000,
|
||||
};
|
||||
|
||||
function getClientIp(request: NextRequest): string {
|
||||
const xff = request.headers.get("x-forwarded-for");
|
||||
if (xff) {
|
||||
return xff.split(",")[0].trim();
|
||||
}
|
||||
const realIp = request.headers.get("x-real-ip");
|
||||
if (realIp) {
|
||||
return realIp;
|
||||
}
|
||||
return "unknown";
|
||||
}
|
||||
|
||||
export function rateLimit(
|
||||
request: NextRequest,
|
||||
identifier?: string,
|
||||
config: Partial<RateLimitConfig> = {}
|
||||
): { success: boolean; remaining: number; resetAt: number } {
|
||||
const { limit, windowMs } = { ...DEFAULT_CONFIG, ...config };
|
||||
const ip = getClientIp(request);
|
||||
const key = identifier ? `${ip}:${identifier}` : ip;
|
||||
const now = Date.now();
|
||||
|
||||
let entry = store.get(key);
|
||||
|
||||
if (!entry || entry.resetAt < now) {
|
||||
entry = { count: 0, resetAt: now + windowMs };
|
||||
store.set(key, entry);
|
||||
}
|
||||
|
||||
entry.count++;
|
||||
|
||||
return {
|
||||
success: entry.count <= limit,
|
||||
remaining: Math.max(0, limit - entry.count),
|
||||
resetAt: entry.resetAt,
|
||||
};
|
||||
}
|
||||
|
||||
export function withRateLimit(
|
||||
handler: (request: NextRequest, context: any) => Promise<NextResponse>,
|
||||
config: Partial<RateLimitConfig> & { identifier?: string } = {}
|
||||
) {
|
||||
return async (request: NextRequest, context: any): Promise<NextResponse> => {
|
||||
const { identifier, ...rateLimitConfig } = config;
|
||||
const result = rateLimit(request, identifier, rateLimitConfig);
|
||||
|
||||
if (!result.success) {
|
||||
return new NextResponse("Too Many Requests", {
|
||||
status: 429,
|
||||
headers: {
|
||||
"Retry-After": Math.ceil((result.resetAt - Date.now()) / 1000).toString(),
|
||||
"X-RateLimit-Limit": (rateLimitConfig.limit || DEFAULT_CONFIG.limit).toString(),
|
||||
"X-RateLimit-Remaining": "0",
|
||||
"X-RateLimit-Reset": result.resetAt.toString(),
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
const response = await handler(request, context);
|
||||
|
||||
response.headers.set("X-RateLimit-Limit", (rateLimitConfig.limit || DEFAULT_CONFIG.limit).toString());
|
||||
response.headers.set("X-RateLimit-Remaining", result.remaining.toString());
|
||||
response.headers.set("X-RateLimit-Reset", result.resetAt.toString());
|
||||
|
||||
return response;
|
||||
};
|
||||
}
|
||||
|
||||
Loading…
Add table
Add a link
Reference in a new issue