♻️ 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;
title: string;
createdAt: Date;
userId: string;
user: {
id: string;
name: string;
} | null;
guestId?: string | null;
participants: {
id: string;
name: string;

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -6,7 +6,13 @@ export const dashboard = router({
info: possiblyPublicProcedure.query(async ({ ctx }) => {
const activePollCount = await prisma.poll.count({
where: {
userId: ctx.user.id,
...(ctx.user.isGuest
? {
guestId: ctx.user.id,
}
: {
userId: ctx.user.id,
}),
status: "live",
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 polls = await prisma.poll.findMany({
where: {
userId: ctx.user.id,
...(ctx.user.isGuest
? { guestId: ctx.user.id }
: { userId: ctx.user.id }),
deletedAt: null,
status: status === "all" ? undefined : status,
},
@ -94,7 +96,13 @@ export const polls = router({
timeZone: true,
createdAt: true,
status: true,
userId: true,
guestId: true,
user: {
select: {
id: true,
name: true,
},
},
participants: {
where: {
deletedAt: null,
@ -164,7 +172,9 @@ export const polls = router({
description: input.description,
adminUrlId: adminToken,
participantUrlId,
userId: ctx.user.id,
...(ctx.user.isGuest
? { guestId: ctx.user.id }
: { userId: ctx.user.id }),
watchers: !ctx.user.isGuest
? {
create: {
@ -452,6 +462,7 @@ export const polls = router({
},
user: true,
userId: true,
guestId: true,
deleted: true,
event: {
select: {
@ -476,7 +487,11 @@ export const polls = router({
}
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 };
} else {
return { ...res, adminUrlId: "", inviteLink };

View file

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

View file

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