Files
GibSend/apps/web/src/server/api/trpc.ts
2025-09-18 21:00:59 +10:00

273 lines
7.3 KiB
TypeScript

/**
* 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 superjson from "superjson";
import { z, ZodError } from "zod";
import { env } from "~/env";
import { getServerAuthSession } from "~/server/auth";
import { db } from "~/server/db";
import { getChildLogger, logger, withLogger } from "../logger/log";
import { randomUUID } from "crypto";
/**
* 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.
*
* This helper generates the "internals" for a tRPC context. The API handler and RSC clients each
* wrap this and provides the required context.
*
* @see https://trpc.io/docs/server/context
*/
export const createTRPCContext = async (opts: { headers: Headers }) => {
const session = await getServerAuthSession();
return {
db,
session,
...opts,
};
};
/**
* 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.
*/
const t = initTRPC.context<typeof createTRPCContext>().create({
transformer: superjson,
errorFormatter({ shape, error }) {
return {
...shape,
data: {
...shape.data,
zodError:
error.cause instanceof ZodError ? error.cause.flatten() : null,
},
};
},
});
/**
* Create a server-side caller.
*
* @see https://trpc.io/docs/server/server-side-calls
*/
export const createCallerFactory = t.createCallerFactory;
/**
* 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;
/**
* Authenticated (session-required) procedure
*
* Ensures a session exists but does not enforce waitlist status. Useful for flows where waitlisted
* users should still have access (e.g., waitlist management).
*/
export const authedProcedure = t.procedure.use(({ ctx, next }) => {
if (!ctx.session || !ctx.session.user) {
throw new TRPCError({ code: "UNAUTHORIZED" });
}
return next({
ctx: {
session: { ...ctx.session, user: ctx.session.user },
},
});
});
/**
* 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 = authedProcedure.use(({ ctx, next }) => {
if (ctx.session.user.isWaitlisted) {
throw new TRPCError({ code: "UNAUTHORIZED" });
}
return next();
});
export const teamProcedure = protectedProcedure.use(async ({ ctx, next }) => {
const teamUser = await db.teamUser.findFirst({
where: { userId: ctx.session.user.id },
include: { team: true },
});
if (!teamUser) {
throw new TRPCError({ code: "NOT_FOUND", message: "Team not found" });
}
return withLogger(
getChildLogger({
teamId: teamUser.team.id,
requestId: randomUUID(),
}),
async () => {
return next({
ctx: {
team: teamUser.team,
teamUser,
session: { ...ctx.session, user: ctx.session.user },
},
});
}
);
});
export const teamAdminProcedure = teamProcedure.use(async ({ ctx, next }) => {
if (ctx.teamUser.role !== "ADMIN") {
throw new TRPCError({
code: "UNAUTHORIZED",
message: "You are not authorized to perform this action",
});
}
return next();
});
export const domainProcedure = teamProcedure
.input(z.object({ id: z.number() }))
.use(async ({ ctx, next, input }) => {
const domain = await db.domain.findUnique({
where: { id: input.id, teamId: ctx.team.id },
});
if (!domain) {
throw new TRPCError({ code: "NOT_FOUND", message: "Domain not found" });
}
return next({ ctx: { ...ctx, domain } });
});
export const emailProcedure = teamProcedure
.input(z.object({ id: z.string() }))
.use(async ({ ctx, next, input }) => {
const email = await db.email.findUnique({
where: { id: input.id, teamId: ctx.team.id },
});
if (!email) {
throw new TRPCError({ code: "NOT_FOUND", message: "Email not found" });
}
return next({ ctx: { ...ctx, email } });
});
export const apiKeyProcedure = teamProcedure
.input(z.object({ id: z.number() }))
.use(async ({ ctx, next, input }) => {
const apiKey = await db.apiKey.findUnique({
where: { id: input.id, teamId: ctx.team.id },
});
if (!apiKey) {
throw new TRPCError({ code: "NOT_FOUND", message: "API key not found" });
}
return next({ ctx: { ...ctx, apiKey } });
});
export const contactBookProcedure = teamProcedure
.input(
z.object({
contactBookId: z.string(),
})
)
.use(async ({ ctx, next, input }) => {
const contactBook = await db.contactBook.findUnique({
where: { id: input.contactBookId, teamId: ctx.team.id },
});
if (!contactBook) {
throw new TRPCError({
code: "NOT_FOUND",
message: "Contact book not found",
});
}
return next({ ctx: { ...ctx, contactBook } });
});
export const campaignProcedure = teamProcedure
.input(
z.object({
campaignId: z.string(),
})
)
.use(async ({ ctx, next, input }) => {
const campaign = await db.campaign.findUnique({
where: { id: input.campaignId, teamId: ctx.team.id },
});
if (!campaign) {
throw new TRPCError({
code: "NOT_FOUND",
message: "Campaign not found",
});
}
return next({ ctx: { ...ctx, campaign } });
});
export const templateProcedure = teamProcedure
.input(
z.object({
templateId: z.string(),
})
)
.use(async ({ ctx, next, input }) => {
const template = await db.template.findUnique({
where: { id: input.templateId, teamId: ctx.team.id },
});
if (!template) {
throw new TRPCError({
code: "NOT_FOUND",
message: "Template not found",
});
}
return next({ ctx: { ...ctx, template } });
});
/**
* To manage application settings, for hosted version, authenticated users will be considered as admin
*/
export const adminProcedure = protectedProcedure.use(async ({ ctx, next }) => {
if (env.NEXT_PUBLIC_IS_CLOUD && ctx.session.user.email !== env.ADMIN_EMAIL) {
throw new TRPCError({ code: "UNAUTHORIZED" });
}
return next();
});