oof
This commit is contained in:
12
web/src/server/api/routers/api.ts
Normal file
12
web/src/server/api/routers/api.ts
Normal file
@@ -0,0 +1,12 @@
|
||||
import type { APIEvent } from "@solidjs/start/server";
|
||||
import { createTRPCRouter, publicProcedure } from "~/server/api/utils";
|
||||
|
||||
// Example of versioned API router
|
||||
export const apiRouter = createTRPCRouter({
|
||||
// v1 endpoints
|
||||
hello: publicProcedure.query(() => {
|
||||
return { message: "Hello from API v1" };
|
||||
}),
|
||||
});
|
||||
|
||||
export default apiRouter;
|
||||
@@ -1,5 +1,6 @@
|
||||
import { initTRPC, TRPCError } from "@trpc/server";
|
||||
import type { TRPCContext } from "./trpc";
|
||||
import { checkRateLimitOrThrow } from "~/server/lib/ratelimit";
|
||||
|
||||
const t = initTRPC.context<TRPCContext>().create();
|
||||
|
||||
@@ -31,28 +32,15 @@ const isAdmin = t.middleware(({ ctx, next }) => {
|
||||
|
||||
export const adminProcedure = t.procedure.use(isAdmin);
|
||||
|
||||
const rateLimitMap = new Map<string, { count: number; resetAt: number }>();
|
||||
|
||||
const isRateLimited = t.middleware(({ ctx, next }) => {
|
||||
const isRateLimited = t.middleware(async ({ ctx, next, path }) => {
|
||||
const identifier = ctx.user?.id ?? ctx.apiKey ?? "anonymous";
|
||||
const now = Date.now();
|
||||
const entry = rateLimitMap.get(identifier);
|
||||
const limit = 100;
|
||||
const windowMs = 60_000;
|
||||
const tier = ctx.user?.role === "admin" ? "admin" : ctx.user ? "authenticated" : "public";
|
||||
|
||||
if (!entry || now > entry.resetAt) {
|
||||
rateLimitMap.set(identifier, { count: 1, resetAt: now + windowMs });
|
||||
return next();
|
||||
}
|
||||
// Sensitive operations get stricter limits
|
||||
const sensitivePaths = ["login", "signup", "forgotPassword", "resetPassword"];
|
||||
const effectiveTier = sensitivePaths.some((p) => path.includes(p)) ? "sensitive" : tier;
|
||||
|
||||
if (entry.count >= limit) {
|
||||
throw new TRPCError({
|
||||
code: "TOO_MANY_REQUESTS",
|
||||
message: "Rate limit exceeded",
|
||||
});
|
||||
}
|
||||
|
||||
entry.count++;
|
||||
await checkRateLimitOrThrow(identifier, effectiveTier);
|
||||
return next();
|
||||
});
|
||||
|
||||
|
||||
51
web/src/server/api/validation.ts
Normal file
51
web/src/server/api/validation.ts
Normal file
@@ -0,0 +1,51 @@
|
||||
import { TRPCError } from "@trpc/server";
|
||||
|
||||
/**
|
||||
* Sanitizes string inputs to prevent XSS.
|
||||
* Escapes HTML entities and strips dangerous attributes.
|
||||
*/
|
||||
export function sanitizeHtml(input: string): string {
|
||||
return input
|
||||
.replace(/&/g, "&")
|
||||
.replace(/</g, "<")
|
||||
.replace(/>/g, ">")
|
||||
.replace(/"/g, """)
|
||||
.replace(/'/g, "'")
|
||||
.replace(/\//g, "/");
|
||||
}
|
||||
|
||||
/**
|
||||
* Validates that a string doesn't contain HTML or script tags.
|
||||
* Throws TRPCError if malicious content is detected.
|
||||
*/
|
||||
export function validateNoHtml(input: string, fieldName: string): void {
|
||||
const htmlPattern = /<[^>]*>/;
|
||||
if (htmlPattern.test(input)) {
|
||||
throw new TRPCError({
|
||||
code: "BAD_REQUEST",
|
||||
message: `${fieldName} contains invalid characters`,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Validates string length with meaningful error messages.
|
||||
*/
|
||||
export function validateStringLength(
|
||||
input: string,
|
||||
fieldName: string,
|
||||
options: { min?: number; max?: number },
|
||||
): void {
|
||||
if (options.min !== undefined && input.length < options.min) {
|
||||
throw new TRPCError({
|
||||
code: "BAD_REQUEST",
|
||||
message: `${fieldName} must be at least ${options.min} characters`,
|
||||
});
|
||||
}
|
||||
if (options.max !== undefined && input.length > options.max) {
|
||||
throw new TRPCError({
|
||||
code: "BAD_REQUEST",
|
||||
message: `${fieldName} must be at most ${options.max} characters`,
|
||||
});
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user