refactor: move into api/db packages

This commit is contained in:
Henry
2026-01-25 22:01:49 +00:00
parent 327089e10e
commit 4353b9620b
17 changed files with 32 additions and 27 deletions

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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