mirror of
https://github.com/lukevella/rallly.git
synced 2025-08-03 16:38:34 +02:00
♻️ Simplify trpc context (#1322)
This commit is contained in:
parent
92b3115896
commit
316ebe34a0
7 changed files with 90 additions and 116 deletions
|
@ -1,23 +1,13 @@
|
|||
import * as Sentry from "@sentry/nextjs";
|
||||
import { TRPCError } from "@trpc/server";
|
||||
import { createNextApiHandler } from "@trpc/server/adapters/next";
|
||||
import { Ratelimit } from "@upstash/ratelimit";
|
||||
import { kv } from "@vercel/kv";
|
||||
import requestIp from "request-ip";
|
||||
|
||||
import { posthog, posthogApiHandler } from "@/app/posthog";
|
||||
import { createTRPCContext } from "@/trpc/context";
|
||||
import { posthogApiHandler } from "@/app/posthog";
|
||||
import { AppRouter, appRouter } from "@/trpc/routers";
|
||||
import { absoluteUrl, shortUrl } from "@/utils/absolute-url";
|
||||
import { getServerSession, isEmailBlocked } from "@/utils/auth";
|
||||
import { isSelfHosted } from "@/utils/constants";
|
||||
import { getServerSession } from "@/utils/auth";
|
||||
import { getEmailClient } from "@/utils/emails";
|
||||
import { composeApiHandlers } from "@/utils/next";
|
||||
|
||||
const ratelimit = new Ratelimit({
|
||||
redis: kv,
|
||||
limiter: Ratelimit.slidingWindow(5, "1 m"),
|
||||
});
|
||||
|
||||
export const config = {
|
||||
api: {
|
||||
externalResolver: true,
|
||||
|
@ -27,42 +17,25 @@ export const config = {
|
|||
const trpcApiHandler = createNextApiHandler<AppRouter>({
|
||||
router: appRouter,
|
||||
createContext: async (opts) => {
|
||||
const res = createTRPCContext(opts, {
|
||||
async getUser({ req, res }) {
|
||||
const session = await getServerSession(req, res);
|
||||
const session = await getServerSession(opts.req, opts.res);
|
||||
|
||||
if (!session) {
|
||||
return null;
|
||||
}
|
||||
if (!session) {
|
||||
throw new TRPCError({
|
||||
code: "UNAUTHORIZED",
|
||||
message: "Unauthorized",
|
||||
});
|
||||
}
|
||||
|
||||
return {
|
||||
id: session.user.id,
|
||||
isGuest: session.user.email === null,
|
||||
locale: session.user.locale ?? undefined,
|
||||
getEmailClient: () =>
|
||||
getEmailClient(session.user.locale ?? undefined),
|
||||
};
|
||||
const res = {
|
||||
user: {
|
||||
id: session.user.id,
|
||||
isGuest: session.user.email === null,
|
||||
locale: session.user.locale ?? undefined,
|
||||
getEmailClient: () => getEmailClient(session.user.locale ?? undefined),
|
||||
},
|
||||
posthogClient: posthog || undefined,
|
||||
isSelfHosted,
|
||||
isEmailBlocked,
|
||||
absoluteUrl,
|
||||
getEmailClient,
|
||||
shortUrl,
|
||||
ratelimit: async () => {
|
||||
if (!process.env.KV_REST_API_URL) {
|
||||
return { success: true };
|
||||
}
|
||||
|
||||
const clientIp = requestIp.getClientIp(opts.req);
|
||||
|
||||
if (!clientIp) {
|
||||
return { success: false };
|
||||
}
|
||||
|
||||
return ratelimit.limit(clientIp);
|
||||
},
|
||||
});
|
||||
req: opts.req,
|
||||
res: opts.res,
|
||||
};
|
||||
|
||||
return res;
|
||||
},
|
||||
|
|
|
@ -1,48 +1,13 @@
|
|||
import { EmailClient } from "@rallly/emails";
|
||||
import { inferAsyncReturnType, TRPCError } from "@trpc/server";
|
||||
import { CreateNextContextOptions } from "@trpc/server/adapters/next";
|
||||
import type { PostHog } from "posthog-node";
|
||||
import type { NextApiRequest, NextApiResponse } from "next";
|
||||
|
||||
export type GetUserFn = (opts: CreateNextContextOptions) => Promise<{
|
||||
id: string;
|
||||
isGuest: boolean;
|
||||
locale?: string;
|
||||
getEmailClient: (locale?: string) => EmailClient;
|
||||
} | null>;
|
||||
|
||||
export interface TRPCContextParams {
|
||||
getUser: GetUserFn;
|
||||
isSelfHosted: boolean;
|
||||
isEmailBlocked?: (email: string) => boolean;
|
||||
posthogClient?: PostHog;
|
||||
getEmailClient: (locale?: string) => EmailClient;
|
||||
/**
|
||||
* Takes a relative path and returns an absolute URL to the app
|
||||
* @param path
|
||||
* @returns absolute URL
|
||||
*/
|
||||
absoluteUrl: (path?: string) => string;
|
||||
shortUrl: (path?: string) => string;
|
||||
ratelimit: () => Promise<{ success: boolean }>;
|
||||
}
|
||||
|
||||
export const createTRPCContext = async (
|
||||
opts: CreateNextContextOptions,
|
||||
{ getUser, ...params }: TRPCContextParams,
|
||||
) => {
|
||||
const user = await getUser(opts);
|
||||
|
||||
if (!user) {
|
||||
throw new TRPCError({
|
||||
code: "BAD_REQUEST",
|
||||
message: "Request has no session",
|
||||
});
|
||||
}
|
||||
|
||||
return {
|
||||
user,
|
||||
...params,
|
||||
export type TRPCContext = {
|
||||
user: {
|
||||
id: string;
|
||||
isGuest: boolean;
|
||||
locale?: string;
|
||||
getEmailClient: (locale?: string) => EmailClient;
|
||||
};
|
||||
req: NextApiRequest;
|
||||
res: NextApiResponse;
|
||||
};
|
||||
|
||||
export type TRPCContext = inferAsyncReturnType<typeof createTRPCContext>;
|
||||
|
|
|
@ -1,6 +1,8 @@
|
|||
import { prisma } from "@rallly/database";
|
||||
import { z } from "zod";
|
||||
|
||||
import { posthog } from "@/app/posthog";
|
||||
import { isEmailBlocked } from "@/utils/auth";
|
||||
import { generateOtp } from "@/utils/nanoid";
|
||||
import { createToken, decryptToken } from "@/utils/session";
|
||||
|
||||
|
@ -24,7 +26,7 @@ export const auth = router({
|
|||
| { ok: true; token: string }
|
||||
| { ok: false; reason: "userAlreadyExists" | "emailNotAllowed" }
|
||||
> => {
|
||||
if (ctx.isEmailBlocked?.(input.email)) {
|
||||
if (isEmailBlocked?.(input.email)) {
|
||||
return { ok: false, reason: "emailNotAllowed" };
|
||||
}
|
||||
|
||||
|
@ -68,7 +70,7 @@ export const auth = router({
|
|||
locale: z.string().optional(),
|
||||
}),
|
||||
)
|
||||
.mutation(async ({ input, ctx }) => {
|
||||
.mutation(async ({ input }) => {
|
||||
const payload = await decryptToken<RegistrationTokenPayload>(input.token);
|
||||
|
||||
if (!payload) {
|
||||
|
@ -90,7 +92,7 @@ export const auth = router({
|
|||
},
|
||||
});
|
||||
|
||||
ctx.posthogClient?.capture({
|
||||
posthog?.capture({
|
||||
event: "register",
|
||||
distinctId: user.id,
|
||||
properties: {
|
||||
|
|
|
@ -4,6 +4,10 @@ import dayjs from "dayjs";
|
|||
import * as ics from "ics";
|
||||
import { z } from "zod";
|
||||
|
||||
import { posthog } from "@/app/posthog";
|
||||
import { absoluteUrl, shortUrl } from "@/utils/absolute-url";
|
||||
import { getEmailClient } from "@/utils/emails";
|
||||
|
||||
import { getTimeZoneAbbreviation } from "../../utils/date";
|
||||
import { nanoid } from "../../utils/nanoid";
|
||||
import {
|
||||
|
@ -222,9 +226,9 @@ export const polls = router({
|
|||
},
|
||||
});
|
||||
|
||||
const pollLink = ctx.absoluteUrl(`/poll/${pollId}`);
|
||||
const pollLink = absoluteUrl(`/poll/${pollId}`);
|
||||
|
||||
const participantLink = ctx.shortUrl(`/invite/${pollId}`);
|
||||
const participantLink = shortUrl(`/invite/${pollId}`);
|
||||
|
||||
if (ctx.user.isGuest === false) {
|
||||
const user = await prisma.user.findUnique({
|
||||
|
@ -505,7 +509,7 @@ export const polls = router({
|
|||
message: "Poll not found",
|
||||
});
|
||||
}
|
||||
const inviteLink = ctx.shortUrl(`/invite/${res.id}`);
|
||||
const inviteLink = shortUrl(`/invite/${res.id}`);
|
||||
|
||||
if (ctx.user.id === res.userId || res.adminUrlId === input.adminToken) {
|
||||
return { ...res, inviteLink };
|
||||
|
@ -921,7 +925,7 @@ export const polls = router({
|
|||
to: poll.user.email,
|
||||
props: {
|
||||
name: poll.user.name,
|
||||
pollUrl: ctx.absoluteUrl(`/poll/${poll.id}`),
|
||||
pollUrl: absoluteUrl(`/poll/${poll.id}`),
|
||||
location: poll.location,
|
||||
title: poll.title,
|
||||
attendees: poll.participants
|
||||
|
@ -940,12 +944,12 @@ export const polls = router({
|
|||
});
|
||||
|
||||
for (const p of participantsToEmail) {
|
||||
ctx
|
||||
.getEmailClient(p.locale ?? undefined)
|
||||
.queueTemplate("FinalizeParticipantEmail", {
|
||||
getEmailClient(p.locale ?? undefined).queueTemplate(
|
||||
"FinalizeParticipantEmail",
|
||||
{
|
||||
to: p.email,
|
||||
props: {
|
||||
pollUrl: ctx.absoluteUrl(`/invite/${poll.id}`),
|
||||
pollUrl: absoluteUrl(`/invite/${poll.id}`),
|
||||
title: poll.title,
|
||||
hostName: poll.user?.name ?? "",
|
||||
date,
|
||||
|
@ -954,10 +958,11 @@ export const polls = router({
|
|||
time,
|
||||
},
|
||||
attachments: [{ filename: "event.ics", content: event.value }],
|
||||
});
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
ctx.posthogClient?.capture({
|
||||
posthog?.capture({
|
||||
distinctId: ctx.user.id,
|
||||
event: "finalize poll",
|
||||
properties: {
|
||||
|
|
|
@ -1,6 +1,8 @@
|
|||
import { prisma } from "@rallly/database";
|
||||
import { z } from "zod";
|
||||
|
||||
import { absoluteUrl } from "@/utils/absolute-url";
|
||||
import { getEmailClient } from "@/utils/emails";
|
||||
import { createToken } from "@/utils/session";
|
||||
|
||||
import { publicProcedure, router } from "../../trpc";
|
||||
|
@ -79,19 +81,21 @@ export const comments = router({
|
|||
{ watcherId: watcher.id, pollId },
|
||||
{ ttl: 0 },
|
||||
);
|
||||
ctx
|
||||
.getEmailClient(watcher.user.locale ?? undefined)
|
||||
.queueTemplate("NewCommentEmail", {
|
||||
|
||||
getEmailClient(watcher.user.locale ?? undefined).queueTemplate(
|
||||
"NewCommentEmail",
|
||||
{
|
||||
to: email,
|
||||
props: {
|
||||
authorName,
|
||||
pollUrl: ctx.absoluteUrl(`/poll/${poll.id}`),
|
||||
disableNotificationsUrl: ctx.absoluteUrl(
|
||||
pollUrl: absoluteUrl(`/poll/${poll.id}`),
|
||||
disableNotificationsUrl: absoluteUrl(
|
||||
`/auth/disable-notifications?token=${token}`,
|
||||
),
|
||||
title: poll.title,
|
||||
},
|
||||
});
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
return newComment;
|
||||
|
|
|
@ -2,6 +2,7 @@ import { prisma } from "@rallly/database";
|
|||
import { TRPCError } from "@trpc/server";
|
||||
import { z } from "zod";
|
||||
|
||||
import { absoluteUrl } from "@/utils/absolute-url";
|
||||
import { createToken } from "@/utils/session";
|
||||
|
||||
import { publicProcedure, rateLimitMiddleware, router } from "../../trpc";
|
||||
|
@ -121,7 +122,7 @@ export const participants = router({
|
|||
to: email,
|
||||
props: {
|
||||
title: poll.title,
|
||||
editSubmissionUrl: ctx.absoluteUrl(
|
||||
editSubmissionUrl: absoluteUrl(
|
||||
`/invite/${poll.id}?token=${token}`,
|
||||
),
|
||||
},
|
||||
|
@ -154,8 +155,8 @@ export const participants = router({
|
|||
to: email,
|
||||
props: {
|
||||
participantName: participant.name,
|
||||
pollUrl: ctx.absoluteUrl(`/poll/${poll.id}`),
|
||||
disableNotificationsUrl: ctx.absoluteUrl(
|
||||
pollUrl: absoluteUrl(`/poll/${poll.id}`),
|
||||
disableNotificationsUrl: absoluteUrl(
|
||||
`/auth/disable-notifications?token=${token}`,
|
||||
),
|
||||
title: poll.title,
|
||||
|
|
|
@ -1,6 +1,10 @@
|
|||
import { initTRPC, TRPCError } from "@trpc/server";
|
||||
import { Ratelimit } from "@upstash/ratelimit";
|
||||
import { kv } from "@vercel/kv";
|
||||
import requestIp from "request-ip";
|
||||
import superjson from "superjson";
|
||||
|
||||
import { isSelfHosted } from "@/utils/constants";
|
||||
import { getSubscriptionStatus } from "@/utils/subscription";
|
||||
|
||||
import { TRPCContext } from "./context";
|
||||
|
@ -21,7 +25,7 @@ export const middleware = t.middleware;
|
|||
export const possiblyPublicProcedure = t.procedure.use(
|
||||
middleware(async ({ ctx, next }) => {
|
||||
// On self-hosted instances, these procedures require login
|
||||
if (ctx.isSelfHosted && ctx.user.isGuest) {
|
||||
if (isSelfHosted && ctx.user.isGuest) {
|
||||
throw new TRPCError({
|
||||
code: "UNAUTHORIZED",
|
||||
message: "Login is required",
|
||||
|
@ -40,7 +44,7 @@ export const proProcedure = t.procedure.use(
|
|||
});
|
||||
}
|
||||
|
||||
if (ctx.isSelfHosted) {
|
||||
if (isSelfHosted) {
|
||||
// Self-hosted instances don't have paid subscriptions
|
||||
return next();
|
||||
}
|
||||
|
@ -72,13 +76,33 @@ export const privateProcedure = t.procedure.use(
|
|||
);
|
||||
|
||||
export const rateLimitMiddleware = middleware(async ({ ctx, next }) => {
|
||||
const { success } = await ctx.ratelimit();
|
||||
if (!success) {
|
||||
const ratelimit = new Ratelimit({
|
||||
redis: kv,
|
||||
limiter: Ratelimit.slidingWindow(5, "1 m"),
|
||||
});
|
||||
|
||||
if (!process.env.KV_REST_API_URL) {
|
||||
return next();
|
||||
}
|
||||
|
||||
const clientIp = requestIp.getClientIp(ctx.req);
|
||||
|
||||
if (!clientIp) {
|
||||
throw new TRPCError({
|
||||
code: "INTERNAL_SERVER_ERROR",
|
||||
message: "Failed to get client IP",
|
||||
});
|
||||
}
|
||||
|
||||
const res = await ratelimit.limit(clientIp);
|
||||
|
||||
if (!res.success) {
|
||||
throw new TRPCError({
|
||||
code: "TOO_MANY_REQUESTS",
|
||||
message: "Too many requests",
|
||||
});
|
||||
}
|
||||
|
||||
return next();
|
||||
});
|
||||
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue