♻️ Keep guest ids in separate column (#1468)

This commit is contained in:
Luke Vella 2024-12-27 11:12:19 +01:00 committed by GitHub
parent 2d7315f45a
commit 5b3c4ad2f6
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
15 changed files with 171 additions and 58 deletions

View file

@ -182,7 +182,11 @@ function PollsListView({
status: PollStatus; status: PollStatus;
title: string; title: string;
createdAt: Date; createdAt: Date;
userId: string; user: {
id: string;
name: string;
} | null;
guestId?: string | null;
participants: { participants: {
id: string; id: string;
name: string; name: string;

View file

@ -16,8 +16,8 @@ import { usePoll } from "@/contexts/poll";
const GoToApp = () => { const GoToApp = () => {
const poll = usePoll(); const poll = usePoll();
const { user } = useUser(); const { ownsObject } = useUser();
if (poll.userId !== user.id) { if (!ownsObject(poll)) {
return null; return null;
} }

View file

@ -4,36 +4,43 @@ export const mergeGuestsIntoUser = async (
userId: string, userId: string,
guestIds: string[], guestIds: string[],
) => { ) => {
await prisma.poll.updateMany({ return await prisma.$transaction(async (tx) => {
await Promise.all([
tx.poll.updateMany({
where: { where: {
userId: { guestId: {
in: guestIds, in: guestIds,
}, },
}, },
data: { data: {
guestId: null,
userId: userId, userId: userId,
}, },
}); }),
await prisma.participant.updateMany({ tx.participant.updateMany({
where: { where: {
userId: { guestId: {
in: guestIds, in: guestIds,
}, },
}, },
data: { data: {
guestId: null,
userId: userId, userId: userId,
}, },
}); }),
await prisma.comment.updateMany({ tx.comment.updateMany({
where: { where: {
userId: { guestId: {
in: guestIds, in: guestIds,
}, },
}, },
data: { data: {
guestId: null,
userId: userId, userId: userId,
}, },
}),
]);
}); });
}; };

View file

@ -34,7 +34,6 @@ import { OptimizedAvatarImage } from "@/components/optimized-avatar-image";
import { Participant, ParticipantName } from "@/components/participant"; import { Participant, ParticipantName } from "@/components/participant";
import { useParticipants } from "@/components/participants-provider"; import { useParticipants } from "@/components/participants-provider";
import { Trans } from "@/components/trans"; import { Trans } from "@/components/trans";
import { usePermissions } from "@/contexts/permissions";
import { usePoll } from "@/contexts/poll"; import { usePoll } from "@/contexts/poll";
import { useRole } from "@/contexts/role"; import { useRole } from "@/contexts/role";
import { trpc } from "@/trpc/client"; import { trpc } from "@/trpc/client";
@ -73,7 +72,6 @@ function NewCommentForm({
const posthog = usePostHog(); const posthog = usePostHog();
const session = useUser(); const session = useUser();
const { register, reset, control, handleSubmit, formState } = const { register, reset, control, handleSubmit, formState } =
@ -162,9 +160,7 @@ function DiscussionInner() {
const pollId = poll.id; const pollId = poll.id;
const { data: comments } = trpc.polls.comments.list.useQuery( const { data: comments } = trpc.polls.comments.list.useQuery({ pollId });
{ pollId },
);
const posthog = usePostHog(); const posthog = usePostHog();
const queryClient = trpc.useUtils(); const queryClient = trpc.useUtils();
@ -187,7 +183,6 @@ function DiscussionInner() {
const [isWriting, setIsWriting] = React.useState(false); const [isWriting, setIsWriting] = React.useState(false);
const role = useRole(); const role = useRole();
const { isUser } = usePermissions();
if (!comments) { if (!comments) {
return null; return null;
@ -205,8 +200,7 @@ function DiscussionInner() {
<CardContent className="border-b"> <CardContent className="border-b">
<div className="space-y-4"> <div className="space-y-4">
{comments.map((comment) => { {comments.map((comment) => {
const canDelete = const canDelete = role === "admin" || session.ownsObject(comment);
role === "admin" || (comment.userId && isUser(comment.userId));
return ( return (
<div className="" key={comment.id}> <div className="" key={comment.id}>

View file

@ -308,6 +308,7 @@ const DesktopPoll: React.FunctionComponent = () => {
id: participant.id, id: participant.id,
name: participant.name, name: participant.name,
userId: participant.userId ?? undefined, userId: participant.userId ?? undefined,
guestId: participant.guestId ?? undefined,
email: participant.email ?? undefined, email: participant.email ?? undefined,
votes: participant.votes, votes: participant.votes,
}} }}

View file

@ -23,6 +23,7 @@ export interface ParticipantRowProps {
id: string; id: string;
name: string; name: string;
userId?: string; userId?: string;
guestId?: string;
email?: string; email?: string;
votes: Vote[]; votes: Vote[];
}; };
@ -105,10 +106,10 @@ const ParticipantRow: React.FunctionComponent<ParticipantRowProps> = ({
className, className,
onChangeEditMode, onChangeEditMode,
}) => { }) => {
const { user, ownsObject } = useUser(); const { ownsObject } = useUser();
const { getVote, optionIds } = usePoll(); const { getVote, optionIds } = usePoll();
const isYou = user && ownsObject(participant) ? true : false; const isYou = ownsObject(participant) ? true : false;
const { canEditParticipant } = usePermissions(); const { canEditParticipant } = usePermissions();
const canEdit = canEditParticipant(participant.id); const canEdit = canEditParticipant(participant.id);

View file

@ -9,6 +9,7 @@ import { useSubscription } from "@/contexts/plan";
import { PreferencesProvider } from "@/contexts/preferences"; import { PreferencesProvider } from "@/contexts/preferences";
import { useTranslation } from "@/i18n/client"; import { useTranslation } from "@/i18n/client";
import { trpc } from "@/trpc/client"; import { trpc } from "@/trpc/client";
import { isOwner } from "@/utils/permissions";
import { useRequiredContext } from "./use-required-context"; import { useRequiredContext } from "./use-required-context";
@ -28,7 +29,10 @@ type UserData = {
export const UserContext = React.createContext<{ export const UserContext = React.createContext<{
user: UserData; user: UserData;
refresh: (data?: Record<string, unknown>) => Promise<Session | null>; refresh: (data?: Record<string, unknown>) => Promise<Session | null>;
ownsObject: (obj: { userId?: string | null }) => boolean; ownsObject: (obj: {
userId?: string | null;
guestId?: string | null;
}) => boolean;
} | null>(null); } | null>(null);
export const useUser = () => { export const useUser = () => {
@ -101,8 +105,8 @@ export const UserProvider = (props: { children?: React.ReactNode }) => {
locale: user.locale ?? i18n.language, locale: user.locale ?? i18n.language,
}, },
refresh: session.update, refresh: session.update,
ownsObject: ({ userId }) => { ownsObject: (resource) => {
return userId ? [user.id].includes(userId) : false; return isOwner(resource, { id: user.id, isGuest });
}, },
}} }}
> >

View file

@ -26,7 +26,7 @@ export const PermissionProvider = ({
export const usePermissions = () => { export const usePermissions = () => {
const poll = usePoll(); const poll = usePoll();
const context = React.useContext(PermissionsContext); const context = React.useContext(PermissionsContext);
const { user } = useUser(); const { user, ownsObject } = useUser();
const role = useRole(); const role = useRole();
const { participants } = useParticipants(); const { participants } = useParticipants();
const isClosed = poll.closed === true || poll.event !== null; const isClosed = poll.closed === true || poll.event !== null;
@ -37,7 +37,8 @@ export const usePermissions = () => {
if (isClosed) { if (isClosed) {
return false; return false;
} }
if (role === "admin" && user.id === poll.userId) {
if (role === "admin" && ownsObject(poll)) {
return true; return true;
} }
@ -45,10 +46,15 @@ export const usePermissions = () => {
(participant) => participant.id === participantId, (participant) => participant.id === participantId,
); );
if (!participant) {
return false;
}
if ( if (
participant && ownsObject(participant) ||
(participant.userId === user.id || (context.userId &&
(context.userId && participant.userId === context.userId)) (participant.userId === context.userId ||
participant.guestId === context.userId))
) { ) {
return true; return true;
} }

View file

@ -6,7 +6,13 @@ export const dashboard = router({
info: possiblyPublicProcedure.query(async ({ ctx }) => { info: possiblyPublicProcedure.query(async ({ ctx }) => {
const activePollCount = await prisma.poll.count({ const activePollCount = await prisma.poll.count({
where: { where: {
...(ctx.user.isGuest
? {
guestId: ctx.user.id,
}
: {
userId: ctx.user.id, userId: ctx.user.id,
}),
status: "live", status: "live",
deleted: false, // TODO (Luke Vella) [2024-06-16]: We should add deleted/cancelled to the status enum deleted: false, // TODO (Luke Vella) [2024-06-16]: We should add deleted/cancelled to the status enum
}, },

View file

@ -73,7 +73,9 @@ export const polls = router({
const { cursor, limit, status } = input; const { cursor, limit, status } = input;
const polls = await prisma.poll.findMany({ const polls = await prisma.poll.findMany({
where: { where: {
userId: ctx.user.id, ...(ctx.user.isGuest
? { guestId: ctx.user.id }
: { userId: ctx.user.id }),
deletedAt: null, deletedAt: null,
status: status === "all" ? undefined : status, status: status === "all" ? undefined : status,
}, },
@ -94,7 +96,13 @@ export const polls = router({
timeZone: true, timeZone: true,
createdAt: true, createdAt: true,
status: true, status: true,
userId: true, guestId: true,
user: {
select: {
id: true,
name: true,
},
},
participants: { participants: {
where: { where: {
deletedAt: null, deletedAt: null,
@ -164,7 +172,9 @@ export const polls = router({
description: input.description, description: input.description,
adminUrlId: adminToken, adminUrlId: adminToken,
participantUrlId, participantUrlId,
userId: ctx.user.id, ...(ctx.user.isGuest
? { guestId: ctx.user.id }
: { userId: ctx.user.id }),
watchers: !ctx.user.isGuest watchers: !ctx.user.isGuest
? { ? {
create: { create: {
@ -452,6 +462,7 @@ export const polls = router({
}, },
user: true, user: true,
userId: true, userId: true,
guestId: true,
deleted: true, deleted: true,
event: { event: {
select: { select: {
@ -476,7 +487,11 @@ export const polls = router({
} }
const inviteLink = shortUrl(`/invite/${res.id}`); const inviteLink = shortUrl(`/invite/${res.id}`);
if (ctx.user.id === res.userId || res.adminUrlId === input.adminToken) { const isOwner = ctx.user.isGuest
? ctx.user.id === res.guestId
: ctx.user.id === res.userId;
if (isOwner || res.adminUrlId === input.adminToken) {
return { ...res, inviteLink }; return { ...res, inviteLink };
} else { } else {
return { ...res, adminUrlId: "", inviteLink }; return { ...res, adminUrlId: "", inviteLink };

View file

@ -41,7 +41,9 @@ export const comments = router({
content, content,
pollId, pollId,
authorName, authorName,
userId: ctx.user.id, ...(ctx.user.isGuest
? { guestId: ctx.user.id }
: { userId: ctx.user.id }),
}, },
select: { select: {
id: true, id: true,

View file

@ -95,7 +95,7 @@ export const participants = router({
pollId: pollId, pollId: pollId,
name: name, name: name,
email, email,
userId: user.id, ...(user.isGuest ? { guestId: user.id } : { userId: user.id }),
locale: user.locale ?? undefined, locale: user.locale ?? undefined,
}, },
include: { include: {

View file

@ -0,0 +1,10 @@
export function isOwner(
resource: { userId?: string | null; guestId?: string | null },
user: { id: string; isGuest: boolean },
) {
if (user.isGuest) {
return resource.guestId === user.id;
}
return resource.userId === user.id;
}

View file

@ -0,0 +1,57 @@
-- AlterTable
ALTER TABLE "comments" ADD COLUMN "guest_id" TEXT;
-- AlterTable
ALTER TABLE "participants" ADD COLUMN "guest_id" TEXT;
-- AlterTable
ALTER TABLE "polls" ADD COLUMN "guest_id" TEXT,
ALTER COLUMN "user_id" DROP NOT NULL;
-- CreateIndex
CREATE INDEX "comments_guest_id_idx" ON "comments" USING HASH ("guest_id");
-- CreateIndex
CREATE INDEX "participants_guest_id_idx" ON "participants" USING HASH ("guest_id");
-- CreateIndex
CREATE INDEX "polls_guest_id_idx" ON "polls" USING HASH ("guest_id");
-- Migrate polls
UPDATE "polls" p
SET
"guest_id" = CASE
WHEN NOT EXISTS (SELECT 1 FROM "users" u WHERE u.id = p.user_id) THEN p.user_id
ELSE NULL
END,
"user_id" = CASE
WHEN NOT EXISTS (SELECT 1 FROM "users" u WHERE u.id = p.user_id) THEN NULL
ELSE p.user_id
END
WHERE p.user_id IS NOT NULL;
-- Migrate participants
UPDATE "participants" p
SET
"guest_id" = CASE
WHEN NOT EXISTS (SELECT 1 FROM "users" u WHERE u.id = p.user_id) THEN p.user_id
ELSE NULL
END,
"user_id" = CASE
WHEN NOT EXISTS (SELECT 1 FROM "users" u WHERE u.id = p.user_id) THEN NULL
ELSE p.user_id
END
WHERE p.user_id IS NOT NULL;
-- Migrate comments
UPDATE "comments" c
SET
"guest_id" = CASE
WHEN NOT EXISTS (SELECT 1 FROM "users" u WHERE u.id = c.user_id) THEN c.user_id
ELSE NULL
END,
"user_id" = CASE
WHEN NOT EXISTS (SELECT 1 FROM "users" u WHERE u.id = c.user_id) THEN NULL
ELSE c.user_id
END
WHERE c.user_id IS NOT NULL;

View file

@ -124,7 +124,8 @@ model Poll {
title String title String
description String? description String?
location String? location String?
userId String @map("user_id") userId String? @map("user_id")
guestId String? @map("guest_id")
timeZone String? @map("time_zone") timeZone String? @map("time_zone")
closed Boolean @default(false) // @deprecated closed Boolean @default(false) // @deprecated
status PollStatus @default(live) status PollStatus @default(live)
@ -147,6 +148,7 @@ model Poll {
comments Comment[] comments Comment[]
@@index([userId], type: Hash) @@index([userId], type: Hash)
@@index([guestId], type: Hash)
@@map("polls") @@map("polls")
} }
@ -184,6 +186,7 @@ model Participant {
name String name String
email String? email String?
userId String? @map("user_id") userId String? @map("user_id")
guestId String? @map("guest_id")
poll Poll @relation(fields: [pollId], references: [id]) poll Poll @relation(fields: [pollId], references: [id])
pollId String @map("poll_id") pollId String @map("poll_id")
votes Vote[] votes Vote[]
@ -194,6 +197,7 @@ model Participant {
deletedAt DateTime? @map("deleted_at") deletedAt DateTime? @map("deleted_at")
@@index([pollId], type: Hash) @@index([pollId], type: Hash)
@@index([guestId], type: Hash)
@@map("participants") @@map("participants")
} }
@ -241,10 +245,12 @@ model Comment {
authorName String @map("author_name") authorName String @map("author_name")
user User? @relation(fields: [userId], references: [id]) user User? @relation(fields: [userId], references: [id])
userId String? @map("user_id") userId String? @map("user_id")
guestId String? @map("guest_id")
createdAt DateTime @default(now()) @map("created_at") createdAt DateTime @default(now()) @map("created_at")
updatedAt DateTime? @updatedAt @map("updated_at") updatedAt DateTime? @updatedAt @map("updated_at")
@@index([userId], type: Hash) @@index([userId], type: Hash)
@@index([guestId], type: Hash)
@@index([pollId], type: Hash) @@index([pollId], type: Hash)
@@map("comments") @@map("comments")
} }