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
This commit is contained in:
@@ -1,6 +1,59 @@
|
||||
import { initTRPC } from "@trpc/server";
|
||||
import { initTRPC, TRPCError } from "@trpc/server";
|
||||
import type { TRPCContext } from "./trpc";
|
||||
|
||||
export const t = initTRPC.create();
|
||||
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);
|
||||
|
||||
Reference in New Issue
Block a user