Files
Kordant/web/src/server/api/utils.ts
Michael Freno 71972436b6 feat: add tRPC auth context, middleware, and protected procedures
- Install jose (JWT) and bcryptjs (password hashing) dependencies
- Create auth utilities: JWT sign/verify, password hash/verify, session management
- Create createTRPCContext that extracts auth from session cookie, Bearer JWT, or x-api-key
- Add publicProcedure, protectedProcedure, adminProcedure, rateLimitedProcedure with middleware
- Wire context builder into SolidStart tRPC API handler
- Update tRPC client to inject auth tokens and handle 401 redirects
- Add unit tests for JWT, password, context builder, and middleware
2026-05-25 15:46:52 -04:00

60 lines
1.5 KiB
TypeScript

import { initTRPC, TRPCError } from "@trpc/server";
import type { TRPCContext } from "./trpc";
const t = initTRPC.context<TRPCContext>().create();
export const createTRPCRouter = t.router;
export const publicProcedure = t.procedure;
const isAuthed = t.middleware(({ ctx, next }) => {
if (!ctx.user) {
throw new TRPCError({ code: "UNAUTHORIZED" });
}
return next({
ctx: { user: ctx.user },
});
});
export const protectedProcedure = t.procedure.use(isAuthed);
const isAdmin = t.middleware(({ ctx, next }) => {
if (!ctx.user) {
throw new TRPCError({ code: "UNAUTHORIZED" });
}
if ((ctx.user.role as string) !== "admin") {
throw new TRPCError({ code: "FORBIDDEN" });
}
return next({
ctx: { user: ctx.user },
});
});
export const adminProcedure = t.procedure.use(isAdmin);
const rateLimitMap = new Map<string, { count: number; resetAt: number }>();
const isRateLimited = t.middleware(({ ctx, next }) => {
const identifier = ctx.user?.id ?? ctx.apiKey ?? "anonymous";
const now = Date.now();
const entry = rateLimitMap.get(identifier);
const limit = 100;
const windowMs = 60_000;
if (!entry || now > entry.resetAt) {
rateLimitMap.set(identifier, { count: 1, resetAt: now + windowMs });
return next();
}
if (entry.count >= limit) {
throw new TRPCError({
code: "TOO_MANY_REQUESTS",
message: "Rate limit exceeded",
});
}
entry.count++;
return next();
});
export const rateLimitedProcedure = t.procedure.use(isRateLimited);