import type { CreateNextContextOptions } from "@trpc/server/adapters/next"; import type { NextApiRequest } from "next"; import type { OpenApiMeta } from "trpc-to-openapi"; import { initTRPC, TRPCError } from "@trpc/server"; import { env } from "next-runtime-env"; import superjson from "superjson"; import { ZodError } from "zod"; import type { dbClient } from "@kan/db/client"; import { initAuth } from "@kan/auth/server"; import { createDrizzleClient } from "@kan/db/client"; import { createLogger } from "@kan/logger"; const log = createLogger("trpc"); export interface User { id: string; name: string; email: string; emailVerified: boolean; createdAt: Date; updatedAt: Date; image?: string | null | undefined; stripeCustomerId?: string | null | undefined; } const createAuthWithHeaders = ( auth: ReturnType, headers: Headers, ) => { return { api: { getSession: () => auth.api.getSession({ headers }), signInMagicLink: (input: { email: string; callbackURL: string }) => auth.api.signInMagicLink({ headers, body: { email: input.email, callbackURL: input.callbackURL }, }), listActiveSubscriptions: (input: { workspacePublicId: string }) => auth.api.listActiveSubscriptions({ headers, query: { referenceId: input.workspacePublicId }, }), }, }; }; interface CreateContextOptions { user: User | null | undefined; db: dbClient; auth: ReturnType; headers: Headers; } export const createInnerTRPCContext = (opts: CreateContextOptions) => { return { user: opts.user, db: opts.db, auth: opts.auth, headers: opts.headers, }; }; export const createTRPCContext = async ({ req }: CreateNextContextOptions) => { const db = createDrizzleClient(); const baseAuth = initAuth(db); const headers = new Headers(req.headers as Record); const auth = createAuthWithHeaders(baseAuth, headers); const session = await auth.api.getSession(); return createInnerTRPCContext({ db, user: session?.user, auth, headers }); }; export const createNextApiContext = async (req: NextApiRequest) => { const db = createDrizzleClient(); const baseAuth = initAuth(db); const headers = new Headers(req.headers as Record); const auth = createAuthWithHeaders(baseAuth, headers); const session = await auth.api.getSession(); return createInnerTRPCContext({ db, user: session?.user, auth, headers }); }; export const createRESTContext = async ({ req }: CreateNextContextOptions) => { const db = createDrizzleClient(); const baseAuth = initAuth(db); const headers = new Headers(req.headers as Record); const auth = createAuthWithHeaders(baseAuth, headers); let session; try { session = await auth.api.getSession(); } catch (error) { log.error({ err: error }, "Error getting session"); throw error; } return createInnerTRPCContext({ db, user: session?.user, auth, headers }); }; const t = initTRPC .context() .meta() .create({ transformer: superjson, errorFormatter({ shape, error }) { return { ...shape, data: { ...shape.data, zodError: error.cause instanceof ZodError ? error.cause.flatten() : null, }, }; }, }); export const createTRPCRouter = t.router; export const createCallerFactory = t.createCallerFactory; const loggingMiddleware = t.middleware(async ({ path, type, next, ctx }) => { const start = Date.now(); const result = await next(); const duration = Date.now() - start; const meta = { procedure: path, type, duration, userId: (ctx as { user?: { id: string } }).user?.id }; if (result.ok) { log.info(meta, "tRPC OK"); } else { log.error({ ...meta, err: result.error }, "tRPC error"); } return result; }); export const publicProcedure = t.procedure.use(loggingMiddleware); const enforceUserIsAuthed = t.middleware(async ({ ctx, next }) => { if (!ctx.user) { throw new TRPCError({ code: "UNAUTHORIZED" }); } return next({ ctx, }); }); const enforceUserIsAdmin = t.middleware(async ({ ctx, next }) => { if (ctx.headers.get("x-admin-api-key") !== env("KAN_ADMIN_API_KEY")) { throw new TRPCError({ code: "UNAUTHORIZED" }); } return next({ ctx, }); }); export const protectedProcedure = t.procedure .use(loggingMiddleware) .use(enforceUserIsAuthed); export const adminProtectedProcedure = t.procedure .use(loggingMiddleware) .use(enforceUserIsAdmin) .meta({ openapi: { method: "GET", path: "/admin/protected", }, });