- 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
60 lines
1.5 KiB
TypeScript
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);
|