-
- {poll.title}
-
+
{poll.title}
diff --git a/apps/web/src/middleware.ts b/apps/web/src/middleware.ts
index a458d58b5da..549a4e11173 100644
--- a/apps/web/src/middleware.ts
+++ b/apps/web/src/middleware.ts
@@ -3,7 +3,7 @@ import { withPostHog } from "@rallly/posthog/next/middleware";
import { NextResponse } from "next/server";
import withAuth from "next-auth/middleware";
-import { getLocaleFromHeader, initGuest } from "@/app/guest";
+import { getLocaleFromHeader } from "@/app/guest";
import { isSelfHosted } from "@/utils/constants";
const supportedLocales = Object.keys(languages);
@@ -55,10 +55,9 @@ export const middleware = withAuth(
const res = NextResponse.rewrite(newUrl);
res.headers.set("x-pathname", newUrl.pathname);
- const jwt = await initGuest(req, res);
- if (jwt?.sub) {
- await withPostHog(res, { distinctID: jwt.sub });
+ if (req.nextauth.token) {
+ await withPostHog(res, { distinctID: req.nextauth.token.sub });
}
return res;
diff --git a/apps/web/src/trpc/context.ts b/apps/web/src/trpc/context.ts
index 39daec76d2e..ffba2156f07 100644
--- a/apps/web/src/trpc/context.ts
+++ b/apps/web/src/trpc/context.ts
@@ -1,12 +1,15 @@
import type { EmailClient } from "@rallly/emails";
+type User = {
+ id: string;
+ isGuest: boolean;
+ locale?: string;
+ getEmailClient: (locale?: string) => EmailClient;
+ image?: string;
+};
+
export type TRPCContext = {
- user: {
- id: string;
- isGuest: boolean;
- locale?: string;
- getEmailClient: (locale?: string) => EmailClient;
- image?: string;
- };
+ user?: User;
+ locale?: string;
ip?: string;
};
diff --git a/apps/web/src/trpc/routers/auth.ts b/apps/web/src/trpc/routers/auth.ts
index 1ff86b430e6..2196313951c 100644
--- a/apps/web/src/trpc/routers/auth.ts
+++ b/apps/web/src/trpc/routers/auth.ts
@@ -4,6 +4,7 @@ import { generateOtp } from "@rallly/utils/nanoid";
import { z } from "zod";
import { isEmailBlocked } from "@/auth";
+import { getEmailClient } from "@/utils/emails";
import { createToken, decryptToken } from "@/utils/session";
import { publicProcedure, rateLimitMiddleware, router } from "../trpc";
@@ -66,7 +67,7 @@ export const auth = router({
code,
});
- await ctx.user.getEmailClient().sendTemplate("RegisterEmail", {
+ await getEmailClient(ctx.locale).sendTemplate("RegisterEmail", {
to: input.email,
props: {
code,
diff --git a/apps/web/src/trpc/routers/polls.ts b/apps/web/src/trpc/routers/polls.ts
index 6bca31efdc8..b634d0c5d15 100644
--- a/apps/web/src/trpc/routers/polls.ts
+++ b/apps/web/src/trpc/routers/polls.ts
@@ -17,6 +17,7 @@ import {
proProcedure,
publicProcedure,
rateLimitMiddleware,
+ requireUserMiddleware,
router,
} from "../trpc";
import { comments } from "./polls/comments";
@@ -130,6 +131,7 @@ export const polls = router({
// START LEGACY ROUTES
create: possiblyPublicProcedure
.use(rateLimitMiddleware)
+ .use(requireUserMiddleware)
.input(
z.object({
title: z.string().trim().min(1),
@@ -332,7 +334,7 @@ export const polls = router({
});
}),
// END LEGACY ROUTES
- getWatchers: possiblyPublicProcedure
+ getWatchers: publicProcedure
.input(
z.object({
pollId: z.string(),
@@ -348,16 +350,9 @@ export const polls = router({
},
});
}),
- watch: possiblyPublicProcedure
+ watch: privateProcedure
.input(z.object({ pollId: z.string() }))
.mutation(async ({ input, ctx }) => {
- if (ctx.user.isGuest) {
- throw new TRPCError({
- code: "BAD_REQUEST",
- message: "Guests can't watch polls",
- });
- }
-
await prisma.watcher.create({
data: {
pollId: input.pollId,
@@ -365,16 +360,9 @@ export const polls = router({
},
});
}),
- unwatch: possiblyPublicProcedure
+ unwatch: privateProcedure
.input(z.object({ pollId: z.string() }))
.mutation(async ({ input, ctx }) => {
- if (ctx.user.isGuest) {
- throw new TRPCError({
- code: "BAD_REQUEST",
- message: "Guests can't unwatch polls",
- });
- }
-
const watcher = await prisma.watcher.findFirst({
where: {
pollId: input.pollId,
@@ -393,31 +381,6 @@ export const polls = router({
});
}
}),
- getByAdminUrlId: possiblyPublicProcedure
- .input(
- z.object({
- urlId: z.string(),
- }),
- )
- .query(async ({ input }) => {
- const res = await prisma.poll.findUnique({
- select: {
- id: true,
- },
- where: {
- adminUrlId: input.urlId,
- },
- });
-
- if (!res) {
- throw new TRPCError({
- code: "NOT_FOUND",
- message: "Poll not found",
- });
- }
-
- return res;
- }),
get: publicProcedure
.input(
z.object({
@@ -479,9 +442,11 @@ export const polls = router({
}
const inviteLink = shortUrl(`/invite/${res.id}`);
- const isOwner = ctx.user.isGuest
- ? ctx.user.id === res.guestId
- : ctx.user.id === res.userId;
+ const userId = ctx.user?.id;
+
+ const isOwner = ctx.user?.isGuest
+ ? userId === res.guestId
+ : userId === res.userId;
if (isOwner || res.adminUrlId === input.adminToken) {
return { ...res, inviteLink };
@@ -489,93 +454,6 @@ export const polls = router({
return { ...res, adminUrlId: "", inviteLink };
}
}),
- transfer: possiblyPublicProcedure
- .input(
- z.object({
- pollId: z.string(),
- }),
- )
- .mutation(async ({ input, ctx }) => {
- await prisma.poll.update({
- where: {
- id: input.pollId,
- },
- data: {
- userId: ctx.user.id,
- },
- });
- }),
- getParticipating: possiblyPublicProcedure
- .input(
- z.object({
- pagination: z.object({
- pageIndex: z.number(),
- pageSize: z.number(),
- }),
- }),
- )
- .query(async ({ ctx, input }) => {
- const [total, rows] = await Promise.all([
- prisma.poll.count({
- where: {
- participants: {
- some: {
- userId: ctx.user.id,
- },
- },
- },
- }),
- prisma.poll.findMany({
- where: {
- deletedAt: null,
- participants: {
- some: {
- userId: ctx.user.id,
- },
- },
- },
- select: {
- id: true,
- title: true,
- location: true,
- createdAt: true,
- timeZone: true,
- adminUrlId: true,
- participantUrlId: true,
- status: true,
- event: {
- select: {
- start: true,
- duration: true,
- },
- },
- closed: true,
- participants: {
- select: {
- id: true,
- name: true,
- },
- orderBy: [
- {
- createdAt: "desc",
- },
- { name: "desc" },
- ],
- },
- },
- orderBy: [
- {
- createdAt: "desc",
- },
- { title: "asc" },
- ],
- skip: input.pagination.pageIndex * input.pagination.pageSize,
- take: input.pagination.pageSize,
- }),
- ]);
-
- return { total, rows };
- }),
book: proProcedure
.input(
z.object({
diff --git a/apps/web/src/trpc/routers/polls/comments.ts b/apps/web/src/trpc/routers/polls/comments.ts
index 18e4efde03d..af4bc75ed40 100644
--- a/apps/web/src/trpc/routers/polls/comments.ts
+++ b/apps/web/src/trpc/routers/polls/comments.ts
@@ -5,7 +5,12 @@ import { z } from "zod";
import { getEmailClient } from "@/utils/emails";
import { createToken } from "@/utils/session";
-import { publicProcedure, rateLimitMiddleware, router } from "../../trpc";
+import {
+ publicProcedure,
+ rateLimitMiddleware,
+ requireUserMiddleware,
+ router,
+} from "../../trpc";
import type { DisableNotificationsPayload } from "../../types";
export const comments = router({
@@ -13,12 +18,52 @@ export const comments = router({
.input(
z.object({
pollId: z.string(),
- hideParticipants: z.boolean().optional(),
+ hideParticipants: z.boolean().optional(), // @deprecated
}),
)
- .query(async ({ input: { pollId, hideParticipants }, ctx }) => {
+ .query(async ({ input: { pollId }, ctx }) => {
+ const poll = await prisma.poll.findUnique({
+ where: {
+ id: pollId,
+ },
+ select: {
+ userId: true,
+ guestId: true,
+ hideParticipants: true,
+ },
+ });
+
+ const isOwner = ctx.user?.isGuest
+ ? poll?.guestId === ctx.user.id
+ : poll?.userId === ctx.user?.id;
+
+ const hideParticipants = poll?.hideParticipants && !isOwner;
+
+ if (hideParticipants && !isOwner) {
+ // if hideParticipants is enabled and the user is not the owner
+ if (!ctx.user) {
+ // cannot see any comments if there is no user
+ return [];
+ } else {
+ // only show comments created by the current users
+ return await prisma.comment.findMany({
+ where: {
+ pollId,
+ ...(ctx.user.isGuest
+ ? { guestId: ctx.user.id }
+ : { userId: ctx.user.id }),
+ },
+ orderBy: [
+ {
+ createdAt: "asc",
+ },
+ ],
+ });
+ }
+ }
+ // return all comments
return await prisma.comment.findMany({
- where: { pollId, userId: hideParticipants ? ctx.user.id : undefined },
+ where: { pollId },
orderBy: [
{
createdAt: "asc",
@@ -28,6 +73,7 @@ export const comments = router({
}),
add: publicProcedure
.use(rateLimitMiddleware)
+ .use(requireUserMiddleware)
.input(
z.object({
pollId: z.string(),
diff --git a/apps/web/src/trpc/routers/polls/participants.ts b/apps/web/src/trpc/routers/polls/participants.ts
index 74c5e34c7e9..0e33b52e9a3 100644
--- a/apps/web/src/trpc/routers/polls/participants.ts
+++ b/apps/web/src/trpc/routers/polls/participants.ts
@@ -5,7 +5,12 @@ import { z } from "zod";
import { createToken } from "@/utils/session";
-import { publicProcedure, rateLimitMiddleware, router } from "../../trpc";
+import {
+ publicProcedure,
+ rateLimitMiddleware,
+ requireUserMiddleware,
+ router,
+} from "../../trpc";
import type { DisableNotificationsPayload } from "../../types";
const MAX_PARTICIPANTS = 1000;
@@ -59,6 +64,7 @@ export const participants = router({
}),
add: publicProcedure
.use(rateLimitMiddleware)
+ .use(requireUserMiddleware)
.input(
z.object({
pollId: z.string(),
diff --git a/apps/web/src/trpc/routers/user.ts b/apps/web/src/trpc/routers/user.ts
index dbc6ebb4135..e20974eb026 100644
--- a/apps/web/src/trpc/routers/user.ts
+++ b/apps/web/src/trpc/routers/user.ts
@@ -12,7 +12,6 @@ import { createToken } from "@/utils/session";
import { getSubscriptionStatus } from "@/utils/subscription";
import {
- possiblyPublicProcedure,
privateProcedure,
publicProcedure,
rateLimitMiddleware,
@@ -68,9 +67,9 @@ export const user = router({
},
});
}),
- subscription: possiblyPublicProcedure.query(
+ subscription: publicProcedure.query(
async ({ ctx }): Promise<{ legacy?: boolean; active: boolean }> => {
- if (ctx.user.isGuest) {
+ if (!ctx.user || ctx.user.isGuest) {
// guest user can't have an active subscription
return {
active: false,
diff --git a/apps/web/src/trpc/server/create-ssr-helper.ts b/apps/web/src/trpc/server/create-ssr-helper.ts
index 11805fc582d..052ab234d37 100644
--- a/apps/web/src/trpc/server/create-ssr-helper.ts
+++ b/apps/web/src/trpc/server/create-ssr-helper.ts
@@ -1,5 +1,4 @@
import { createServerSideHelpers } from "@trpc/react-query/server";
-import { TRPCError } from "@trpc/server";
import { redirect } from "next/navigation";
import superjson from "superjson";
@@ -11,22 +10,17 @@ import { appRouter } from "../routers";
async function createContext(): Promise
{
const session = await getServerSession();
-
- if (!session) {
- throw new TRPCError({
- code: "UNAUTHORIZED",
- message: "Unauthorized",
- });
- }
-
return {
- user: {
- id: session.user.id,
- isGuest: session.user.email === null,
- locale: session.user.locale ?? undefined,
- image: session.user.image ?? undefined,
- getEmailClient: () => getEmailClient(session.user.locale ?? undefined),
- },
+ user: session?.user
+ ? {
+ id: session.user.id,
+ isGuest: !session.user.email,
+ locale: session.user.locale ?? undefined,
+ image: session.user.image ?? undefined,
+ getEmailClient: () =>
+ getEmailClient(session.user?.locale ?? undefined),
+ }
+ : undefined,
};
}
diff --git a/apps/web/src/trpc/trpc.ts b/apps/web/src/trpc/trpc.ts
index 3be7a483a0e..7ad2b4fe8ae 100644
--- a/apps/web/src/trpc/trpc.ts
+++ b/apps/web/src/trpc/trpc.ts
@@ -3,6 +3,7 @@ import { Ratelimit } from "@upstash/ratelimit";
import { kv } from "@vercel/kv";
import superjson from "superjson";
+import { isQuickCreateEnabled } from "@/features/quick-create";
import { isSelfHosted } from "@/utils/constants";
import { getSubscriptionStatus } from "@/utils/subscription";
@@ -23,57 +24,70 @@ export const middleware = t.middleware;
export const possiblyPublicProcedure = t.procedure.use(
middleware(async ({ ctx, next }) => {
- // On self-hosted instances, these procedures require login
- if (isSelfHosted && ctx.user.isGuest) {
+ // These procedurs are public if Quick Create is enabled
+ const isGuest = !ctx.user || ctx.user.isGuest;
+ if (isGuest && !isQuickCreateEnabled) {
throw new TRPCError({
code: "UNAUTHORIZED",
message: "Login is required",
});
}
+
return next();
}),
);
-export const proProcedure = t.procedure.use(
- middleware(async ({ ctx, next }) => {
- if (ctx.user.isGuest) {
- throw new TRPCError({
- code: "UNAUTHORIZED",
- message: "Login is required",
- });
- }
+// This procedure guarantees that a user will exist in the context by
+// creating a guest user if needed
+export const requireUserMiddleware = middleware(async ({ ctx, next }) => {
+ if (!ctx.user) {
+ throw new TRPCError({
+ code: "UNAUTHORIZED",
+ message: "This method requires a user",
+ });
+ }
- if (isSelfHosted) {
- // Self-hosted instances don't have paid subscriptions
- return next();
- }
+ return next({
+ ctx: {
+ user: ctx.user,
+ },
+ });
+});
- const { active: isPro } = await getSubscriptionStatus(ctx.user.id);
+export const privateProcedure = t.procedure.use(async ({ ctx, next }) => {
+ const { user } = ctx;
+ if (!user || user.isGuest !== false) {
+ throw new TRPCError({
+ code: "UNAUTHORIZED",
+ message: "Login is required",
+ });
+ }
- if (!isPro) {
- throw new TRPCError({
- code: "UNAUTHORIZED",
- message:
- "You must have an active paid subscription to perform this action",
- });
- }
+ return next({
+ ctx: {
+ user,
+ },
+ });
+});
+export const proProcedure = privateProcedure.use(async ({ ctx, next }) => {
+ if (isSelfHosted) {
+ // Self-hosted instances don't have paid subscriptions
return next();
- }),
-);
+ }
-export const privateProcedure = t.procedure.use(
- middleware(async ({ ctx, next }) => {
- if (ctx.user.isGuest !== false) {
- throw new TRPCError({
- code: "UNAUTHORIZED",
- message: "Login is required",
- });
- }
+ const { active: isPro } = await getSubscriptionStatus(ctx.user.id);
- return next();
- }),
-);
+ if (!isPro) {
+ throw new TRPCError({
+ code: "UNAUTHORIZED",
+ message:
+ "You must have an active paid subscription to perform this action",
+ });
+ }
+
+ return next();
+});
export const rateLimitMiddleware = middleware(async ({ ctx, next }) => {
if (!process.env.KV_REST_API_URL) {