mirror of
https://github.com/kjanat/livedash-node.git
synced 2026-01-16 14:12:10 +01:00
feat: complete tRPC integration and fix platform UI issues
- Implement comprehensive tRPC setup with type-safe API - Create tRPC routers for dashboard, admin, and auth endpoints - Migrate frontend components to use tRPC client - Fix platform dashboard Settings button functionality - Add platform settings page with profile and security management - Create OpenAI API mocking infrastructure for cost-safe testing - Update tests to work with new tRPC architecture - Sync database schema to fix AIBatchRequest table errors
This commit is contained in:
163
lib/trpc.ts
Normal file
163
lib/trpc.ts
Normal file
@ -0,0 +1,163 @@
|
||||
/**
|
||||
* tRPC Server Configuration
|
||||
*
|
||||
* This file sets up the core tRPC configuration including:
|
||||
* - Server context creation with authentication
|
||||
* - Router initialization
|
||||
* - Middleware for authentication and error handling
|
||||
*/
|
||||
|
||||
import { initTRPC, TRPCError } from "@trpc/server";
|
||||
import type { FetchCreateContextFnOptions } from "@trpc/server/adapters/fetch";
|
||||
import { getServerSession } from "next-auth/next";
|
||||
import superjson from "superjson";
|
||||
import type { z } from "zod";
|
||||
import { authOptions } from "./auth";
|
||||
import { prisma } from "./prisma";
|
||||
import { validateInput } from "./validation";
|
||||
|
||||
/**
|
||||
* Create context for tRPC requests
|
||||
* This runs on every request and provides:
|
||||
* - Database access
|
||||
* - User session information
|
||||
* - Request/response objects
|
||||
*/
|
||||
export async function createTRPCContext(opts: FetchCreateContextFnOptions) {
|
||||
const session = await getServerSession(authOptions);
|
||||
|
||||
return {
|
||||
prisma,
|
||||
session,
|
||||
req: opts.req,
|
||||
};
|
||||
}
|
||||
|
||||
export type Context = Awaited<ReturnType<typeof createTRPCContext>>;
|
||||
|
||||
/**
|
||||
* Initialize tRPC with superjson for date serialization
|
||||
*/
|
||||
const t = initTRPC.context<Context>().create({
|
||||
transformer: superjson,
|
||||
errorFormatter({ shape }) {
|
||||
return shape;
|
||||
},
|
||||
});
|
||||
|
||||
/**
|
||||
* Base router and middleware exports
|
||||
*/
|
||||
export const router = t.router;
|
||||
export const publicProcedure = t.procedure;
|
||||
|
||||
/**
|
||||
* Authentication middleware
|
||||
* Throws error if user is not authenticated
|
||||
*/
|
||||
const enforceUserIsAuthed = t.middleware(({ ctx, next }) => {
|
||||
if (!ctx.session?.user?.email) {
|
||||
throw new TRPCError({ code: "UNAUTHORIZED" });
|
||||
}
|
||||
return next({
|
||||
ctx: {
|
||||
...ctx,
|
||||
session: { ...ctx.session, user: ctx.session.user },
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
/**
|
||||
* Company access middleware
|
||||
* Ensures user has access to their company's data
|
||||
*/
|
||||
const enforceCompanyAccess = t.middleware(async ({ ctx, next }) => {
|
||||
if (!ctx.session?.user?.email) {
|
||||
throw new TRPCError({ code: "UNAUTHORIZED" });
|
||||
}
|
||||
|
||||
const user = await ctx.prisma.user.findUnique({
|
||||
where: { email: ctx.session.user.email },
|
||||
include: { company: true },
|
||||
});
|
||||
|
||||
if (!user || !user.company) {
|
||||
throw new TRPCError({
|
||||
code: "FORBIDDEN",
|
||||
message: "User does not have company access",
|
||||
});
|
||||
}
|
||||
|
||||
return next({
|
||||
ctx: {
|
||||
...ctx,
|
||||
user,
|
||||
company: user.company,
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
/**
|
||||
* Admin access middleware
|
||||
* Ensures user has admin role
|
||||
*/
|
||||
const enforceAdminAccess = t.middleware(async ({ ctx, next }) => {
|
||||
if (!ctx.session?.user?.email) {
|
||||
throw new TRPCError({ code: "UNAUTHORIZED" });
|
||||
}
|
||||
|
||||
const user = await ctx.prisma.user.findUnique({
|
||||
where: { email: ctx.session.user.email },
|
||||
include: { company: true },
|
||||
});
|
||||
|
||||
if (!user || user.role !== "ADMIN") {
|
||||
throw new TRPCError({
|
||||
code: "FORBIDDEN",
|
||||
message: "Admin access required",
|
||||
});
|
||||
}
|
||||
|
||||
return next({
|
||||
ctx: {
|
||||
...ctx,
|
||||
user,
|
||||
company: user.company,
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
/**
|
||||
* Input validation middleware
|
||||
* Automatically validates inputs using Zod schemas
|
||||
*/
|
||||
const createValidatedProcedure = <T>(schema: z.ZodSchema<T>) =>
|
||||
publicProcedure.input(schema).use(({ input, next }) => {
|
||||
const validation = validateInput(schema, input);
|
||||
if (!validation.success) {
|
||||
throw new TRPCError({
|
||||
code: "BAD_REQUEST",
|
||||
message: validation.errors.join(", "),
|
||||
});
|
||||
}
|
||||
return next({ ctx: {}, input: validation.data });
|
||||
});
|
||||
|
||||
/**
|
||||
* Procedure variants for different access levels
|
||||
*/
|
||||
export const protectedProcedure = publicProcedure.use(enforceUserIsAuthed);
|
||||
export const companyProcedure = publicProcedure.use(enforceCompanyAccess);
|
||||
export const adminProcedure = publicProcedure.use(enforceAdminAccess);
|
||||
export const validatedProcedure = createValidatedProcedure;
|
||||
|
||||
/**
|
||||
* Rate limiting middleware for sensitive operations
|
||||
*/
|
||||
export const rateLimitedProcedure = publicProcedure.use(
|
||||
async ({ ctx, next }) => {
|
||||
// Rate limiting logic would go here
|
||||
// For now, just pass through
|
||||
return next({ ctx });
|
||||
}
|
||||
);
|
||||
Reference in New Issue
Block a user