/** * YOU PROBABLY DON'T NEED TO EDIT THIS FILE, UNLESS: * 1. You want to modify request context (see Part 1). * 2. You want to create a new middleware or type of procedure (see Part 3). * * TL;DR - This is where all the tRPC server stuff is created and plugged in. The pieces you will * need to use are documented accordingly near the end. */ import { initTRPC, TRPCError } from "@trpc/server"; import type { CreateNextContextOptions } from "@trpc/server/adapters/next"; import superjson from "superjson"; import z, { ZodError } from "zod/v4"; import { env } from "~/env"; import { verifyToken } from "~/server/auth/jwt"; import { zUserId } from "~/server/utils/zod_types"; import { TOKEN_CONFIG } from "../auth/configs"; /** * 1. CONTEXT * * This section defines the "contexts" that are available in the backend API. * * These allow you to access things when processing a request, like the database, the session, etc. */ export const sessionSchema = z.object({ email: z.email(), id: zUserId, isAdmin: z.boolean(), isBlocked: z.boolean(), nome: z.string(), username: z.string(), mustChangePassword: z.boolean(), }); export type Session = z.infer; export type SessionStatus = | "AUTHENTICATED" | "EXPIRED" | "INVALID" | "NO_SESSION"; interface CreateInnerContextOptions extends Partial { accessToken?: string; } export const createInnerTRPCContext = async ({ accessToken, ...opts }: CreateInnerContextOptions) => { let sessionData: Session | null = null; let sessionStatus: SessionStatus = "NO_SESSION"; if (accessToken) { const { expired, session } = await verifyToken(accessToken); if (expired) { sessionStatus = "EXPIRED"; } else if (session) { sessionData = session; sessionStatus = "AUTHENTICATED"; } else { sessionStatus = "INVALID"; } } return { ...opts, session: sessionData, sessionStatus, }; }; export const createTRPCContext = async ({ req, res, }: Partial) => { if (!req || !res) { throw new Error( "You are missing `req` or `res` in your call to createTRPCContext.", ); } const accessToken = req.cookies[TOKEN_CONFIG.ACCESS_TOKEN_COOKIE_NAME]; return await createInnerTRPCContext({ accessToken, req, res }); }; export type Context = Awaited>; export const addApiAuthToContext = (ctx: Context) => { if (!ctx.req || !ctx.res) { throw new Error( "You are missing `req` or `res` in your call to addApiAuthToContext.", ); } const authString = Buffer.from( `${env.EXP_API_USER}:${env.EXP_API_PASS}`, ).toString("base64"); ctx.req.headers.authorization = `Basic ${authString}`; return ctx; }; /** * 2. INITIALIZATION * * This is where the tRPC API is initialized, connecting the context and transformer. We also parse * ZodErrors so that you get typesafety on the frontend if your procedure fails due to validation * errors on the backend. */ export const t = initTRPC.context().create({ errorFormatter({ shape, error }) { return { ...shape, data: { ...shape.data, zodError: error.cause instanceof ZodError ? z.treeifyError(error.cause) : null, }, }; }, sse: { client: { reconnectAfterInactivityMs: 5_000, }, //maxDurationMs: 5 * 60 * 1_000, // 5 minutes ping: { enabled: true, intervalMs: 3_000, }, }, transformer: superjson, }); /** * 3. ROUTER & PROCEDURE (THE IMPORTANT BIT) * * These are the pieces you use to build your tRPC API. You should import these a lot in the * "/src/server/api/routers" directory. */ /** * This is how you create new routers and sub-routers in your tRPC API. * * @see https://trpc.io/docs/router */ export const createTRPCRouter = t.router; /** * Public (unauthenticated) procedure * * This is the base piece you use to build new queries and mutations on your tRPC API. It does not * guarantee that a user querying is authorized, but you can still access user session data if they * are logged in. */ export const publicProcedure = t.procedure; /** Reusable middleware that enforces users are logged in before running the procedure. */ const enforceUserIsAuthed = t.middleware(({ ctx, next, path }) => { if (ctx.sessionStatus === "EXPIRED") { throw new TRPCError({ code: "UNAUTHORIZED", message: `SESSION_EXPIRED, error in path: ${path} [${ctx.req?.url}]`, }); } if (!ctx.session) { throw new TRPCError({ code: "UNAUTHORIZED", message: `User is not logged, error in path: ${path} [${ctx.req?.url}]`, }); } return next({ ctx: { // infers the `session` as non-nullable session: { ...ctx.session }, }, }); }); const enforceUserIsAuthedOrOverride = t.middleware(({ ctx, next, path }) => { const overrideToken = ctx.req?.cookies[TOKEN_CONFIG.OVERRIDE_TOKEN_COOKIE_NAME]; if (overrideToken && overrideToken === env.EXP_API_PASS) { return next({ ctx: { // infers the `session` as non-nullable session: { ...ctx.session }, }, }); } if (!ctx.session) { throw new TRPCError({ code: "UNAUTHORIZED", message: `User is not logged, error in path: ${path} [${ctx.req?.url}]`, }); } return next({ ctx: { // infers the `session` as non-nullable session: { ...ctx.session }, }, }); }); /** * Protected (authenticated) procedure * * If you want a query or mutation to ONLY be accessible to logged in users, use this. It verifies * the session is valid and guarantees `ctx.session.user` is not null. * * @see https://trpc.io/docs/procedures */ export const protectedProcedure = t.procedure.use(enforceUserIsAuthed); export const overridableProcedure = t.procedure.use( enforceUserIsAuthedOrOverride, ); /** Reusable middleware that enforces users are logged in before running the procedure. */ const enforceUserIsAdmin = t.middleware(({ ctx, next, path }) => { if (ctx.sessionStatus === "EXPIRED") { throw new TRPCError({ code: "UNAUTHORIZED", message: `SESSION_EXPIRED, error in path: ${path} [${ctx.req?.url}]`, }); } if (!ctx.session?.isAdmin) { throw new TRPCError({ code: "UNAUTHORIZED", message: `User is not Admin, error in path: ${path} [${ctx.req?.url}]`, }); } return next({ ctx: { // infers the `session` as non-nullable session: { ...ctx.session }, }, }); }); /** * Protected (authenticated) procedure * * If you want a query or mutation to ONLY be accessible to logged in users, use this. It verifies * the session is valid and guarantees `ctx.session.user` is not null. * * @see https://trpc.io/docs/procedures */ export const adminProcedure = t.procedure.use(enforceUserIsAdmin); export const apiProcedure = publicProcedure.use((opts) => { if (!opts.ctx.req || !opts.ctx.res) { throw new Error("You are missing `req` or `res` in your call."); } return opts.next({ ctx: { // We overwrite the context with the truthy `req` & `res`, which will also overwrite the types used in your procedure. req: opts.ctx.req, res: opts.ctx.res, }, }); }); const enforceProtectedApi = t.middleware(async ({ ctx, next, path }) => { if (!ctx.req || !ctx.res) { throw new TRPCError({ code: "UNAUTHORIZED", message: `No request or response object, error in path: ${path} [${ctx.req?.url}]`, }); } const basicAuth = ctx.req.headers.authorization; if (!basicAuth) { throw new TRPCError({ code: "UNAUTHORIZED", message: `No Auth header found: ${path} [${ctx.req?.url}]`, }); } const authValue = basicAuth.split(" ")[1]; if (!authValue) { throw new TRPCError({ code: "UNAUTHORIZED", message: `Auth value invalid: ${path} [${ctx.req?.url}]`, }); } const [user, pwd] = atob(authValue).split(":"); if (user !== env.EXP_API_USER || pwd !== env.EXP_API_PASS) { throw new TRPCError({ code: "UNAUTHORIZED", message: `User or Secret not valid: ${path} [${ctx.req?.url}]`, }); } return next({ ctx: { // infers the `session` as non-nullable req: ctx.req, res: ctx.res, }, }); }); export const protectedApiProcedure = publicProcedure.use(enforceProtectedApi);