refactor: move into api/db packages
This commit is contained in:
@@ -48,14 +48,12 @@
|
||||
"@trpc/server": "catalog:",
|
||||
"date-fns": "^4.1.0",
|
||||
"geist": "^1.3.1",
|
||||
"ioredis": "^5.9.2",
|
||||
"jose": "^6.1.2",
|
||||
"next": "15.5.9",
|
||||
"next-runtime-env": "^1.7.2",
|
||||
"next-themes": "^0.4.6",
|
||||
"nextjs-cors": "^2.2.0",
|
||||
"posthog-js": "^1.254.0",
|
||||
"rate-limiter-flexible": "^9.0.1",
|
||||
"react": "catalog:react18",
|
||||
"react-beautiful-dnd": "^13.1.1",
|
||||
"react-contenteditable": "^3.3.7",
|
||||
|
||||
@@ -2,7 +2,7 @@ import { toNodeHandler } from "better-auth/node";
|
||||
|
||||
import { initAuth } from "@kan/auth/server";
|
||||
import { createDrizzleClient } from "@kan/db/client";
|
||||
import { withRateLimit } from "~/utils/rateLimit";
|
||||
import { withRateLimit } from "@kan/api/utils/rateLimit";
|
||||
|
||||
export const config = { api: { bodyParser: false } };
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import type { NextApiRequest, NextApiResponse } from "next";
|
||||
|
||||
import { withRateLimit } from "~/utils/rateLimit";
|
||||
import { withRateLimit } from "@kan/api/utils/rateLimit";
|
||||
|
||||
export default withRateLimit(
|
||||
{ points: 100, duration: 60 },
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import type { NextApiRequest, NextApiResponse } from "next";
|
||||
|
||||
import { withRateLimit } from "~/utils/rateLimit";
|
||||
import { withRateLimit } from "@kan/api/utils/rateLimit";
|
||||
|
||||
export default withRateLimit(
|
||||
{ points: 100, duration: 60 },
|
||||
|
||||
@@ -3,7 +3,7 @@ import { env } from "next-runtime-env";
|
||||
|
||||
import { createNextApiContext } from "@kan/api/trpc";
|
||||
import { createStripeClient } from "@kan/stripe";
|
||||
import { withRateLimit } from "~/utils/rateLimit";
|
||||
import { withRateLimit } from "@kan/api/utils/rateLimit";
|
||||
|
||||
export default withRateLimit(
|
||||
{ points: 100, duration: 60 },
|
||||
|
||||
@@ -6,7 +6,7 @@ import { createNextApiContext } from "@kan/api/trpc";
|
||||
import * as subscriptionRepo from "@kan/db/repository/subscription.repo";
|
||||
import * as workspaceRepo from "@kan/db/repository/workspace.repo";
|
||||
import { createStripeClient } from "@kan/stripe";
|
||||
import { withRateLimit } from "~/utils/rateLimit";
|
||||
import { withRateLimit } from "@kan/api/utils/rateLimit";
|
||||
|
||||
const workspaceSlugSchema = z
|
||||
.string()
|
||||
|
||||
@@ -3,7 +3,7 @@ import type { NextApiRequest, NextApiResponse } from "next";
|
||||
import { createNextApiContext } from "@kan/api/trpc";
|
||||
import { integrations } from "@kan/db/schema";
|
||||
import { addYears } from "date-fns";
|
||||
import { withRateLimit } from "~/utils/rateLimit";
|
||||
import { withRateLimit } from "@kan/api/utils/rateLimit";
|
||||
|
||||
export default withRateLimit(
|
||||
{ points: 100, duration: 60 },
|
||||
|
||||
@@ -4,7 +4,7 @@ import { createNextApiHandler } from "@trpc/server/adapters/next";
|
||||
import { appRouter } from "@kan/api/root";
|
||||
import { createTRPCContext } from "@kan/api/trpc";
|
||||
import { env } from "~/env";
|
||||
import { withRateLimit } from "~/utils/rateLimit";
|
||||
import { withRateLimit } from "@kan/api/utils/rateLimit";
|
||||
|
||||
const nextApiHandler = createNextApiHandler({
|
||||
router: appRouter,
|
||||
|
||||
@@ -4,7 +4,7 @@ import { jwtVerify } from "jose";
|
||||
import { z } from "zod";
|
||||
|
||||
import { env } from "~/env";
|
||||
import { withRateLimit } from "~/utils/rateLimit";
|
||||
import { withRateLimit } from "@kan/api/utils/rateLimit";
|
||||
|
||||
const requestSchema = z.object({
|
||||
token: z.string().min(1),
|
||||
|
||||
@@ -6,7 +6,7 @@ import { env as nextRuntimeEnv } from "next-runtime-env";
|
||||
import { createNextApiContext } from "@kan/api/trpc";
|
||||
|
||||
import { env } from "~/env";
|
||||
import { withRateLimit } from "~/utils/rateLimit";
|
||||
import { withRateLimit } from "@kan/api/utils/rateLimit";
|
||||
|
||||
const allowedContentTypes = ["image/jpeg", "image/png"];
|
||||
|
||||
|
||||
@@ -6,7 +6,7 @@ import { appRouter } from "@kan/api";
|
||||
import { createRESTContext } from "@kan/api/trpc";
|
||||
|
||||
import { env } from "~/env";
|
||||
import { withRateLimit } from "~/utils/rateLimit";
|
||||
import { withRateLimit } from "@kan/api/utils/rateLimit";
|
||||
|
||||
export default withRateLimit(
|
||||
{ points: 100, duration: 60 },
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import type { NextApiRequest, NextApiResponse } from "next";
|
||||
|
||||
import { openApiDocument } from "@kan/api/openapi";
|
||||
import { withRateLimit } from "~/utils/rateLimit";
|
||||
import { withRateLimit } from "@kan/api/utils/rateLimit";
|
||||
|
||||
export default withRateLimit(
|
||||
{ points: 100, duration: 60 },
|
||||
|
||||
@@ -1,99 +0,0 @@
|
||||
import type { NextApiRequest, NextApiResponse } from "next";
|
||||
import {
|
||||
RateLimiterRedis,
|
||||
RateLimiterMemory,
|
||||
} from "rate-limiter-flexible";
|
||||
|
||||
import { getRedisClient } from "./redis";
|
||||
|
||||
export interface RateLimitOptions {
|
||||
points?: number;
|
||||
duration?: number;
|
||||
identifier?: (req: NextApiRequest) => string | Promise<string>;
|
||||
errorMessage?: string;
|
||||
}
|
||||
|
||||
|
||||
const defaultIdentifier = (req: NextApiRequest): string => {
|
||||
// Try to identify the IP address of the request
|
||||
const forwardedFor = req.headers["x-forwarded-for"];
|
||||
const realIp = req.headers["x-real-ip"];
|
||||
const cfConnectingIp = req.headers["cf-connecting-ip"];
|
||||
|
||||
const ip =
|
||||
(typeof forwardedFor === "string"
|
||||
? forwardedFor.split(",")[0]?.trim()
|
||||
: null) ??
|
||||
(typeof realIp === "string" ? realIp : null) ??
|
||||
(typeof cfConnectingIp === "string" ? cfConnectingIp : null) ??
|
||||
req.socket.remoteAddress ??
|
||||
"unknown";
|
||||
|
||||
return ip;
|
||||
};
|
||||
|
||||
|
||||
const DEFAULT_OPTIONS = {
|
||||
points: 100,
|
||||
duration: 60,
|
||||
errorMessage: "Too many requests, please try again later.",
|
||||
identifier: defaultIdentifier,
|
||||
} as const;
|
||||
|
||||
|
||||
function createRateLimiter(options: RateLimitOptions = {}) {
|
||||
const redis = getRedisClient();
|
||||
const points = options.points ?? DEFAULT_OPTIONS.points;
|
||||
const duration = options.duration ?? DEFAULT_OPTIONS.duration;
|
||||
|
||||
// Use Redis if available, otherwise fall back to in-memory storage
|
||||
if (redis) {
|
||||
return new RateLimiterRedis({
|
||||
storeClient: redis,
|
||||
points,
|
||||
duration,
|
||||
});
|
||||
}
|
||||
|
||||
return new RateLimiterMemory({
|
||||
points,
|
||||
duration,
|
||||
});
|
||||
}
|
||||
|
||||
export function withRateLimit(
|
||||
options: RateLimitOptions,
|
||||
handler: (
|
||||
req: NextApiRequest,
|
||||
res: NextApiResponse,
|
||||
) => Promise<unknown> | unknown,
|
||||
) {
|
||||
const rateLimiter = createRateLimiter(options);
|
||||
const identifier = options.identifier ?? DEFAULT_OPTIONS.identifier;
|
||||
const errorMessage = options.errorMessage ?? DEFAULT_OPTIONS.errorMessage;
|
||||
|
||||
return async (req: NextApiRequest, res: NextApiResponse) => {
|
||||
try {
|
||||
const id = await identifier(req);
|
||||
const key = `ratelimit_${id}`;
|
||||
|
||||
await rateLimiter.consume(key);
|
||||
|
||||
return await handler(req, res);
|
||||
} catch (error) {
|
||||
// rate-limiter-flexible throws an error with msBeforeNext or remainingPoints
|
||||
// when limit is exceeded. Check for these properties directly.
|
||||
if (
|
||||
error &&
|
||||
typeof error === "object" &&
|
||||
("msBeforeNext" in error || "remainingPoints" in error)
|
||||
) {
|
||||
return res.status(429).json({
|
||||
message: errorMessage,
|
||||
});
|
||||
}
|
||||
|
||||
return await handler(req, res);
|
||||
}
|
||||
};
|
||||
}
|
||||
@@ -1,34 +0,0 @@
|
||||
import Redis from "ioredis";
|
||||
|
||||
import { env } from "~/env";
|
||||
|
||||
let redisClient: Redis | null = null;
|
||||
|
||||
export function getRedisClient(): Redis | null {
|
||||
if (redisClient) {
|
||||
return redisClient;
|
||||
}
|
||||
|
||||
const redisUrl = env.REDIS_URL;
|
||||
|
||||
if (!redisUrl) {
|
||||
return null;
|
||||
}
|
||||
|
||||
redisClient = new Redis(redisUrl, {
|
||||
maxRetriesPerRequest: 3,
|
||||
enableReadyCheck: true,
|
||||
lazyConnect: true,
|
||||
});
|
||||
|
||||
return redisClient;
|
||||
}
|
||||
|
||||
export async function closeRedisClient(): Promise<void> {
|
||||
if (redisClient) {
|
||||
await redisClient.quit();
|
||||
redisClient = null;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user