mirror of
https://github.com/lukevella/rallly.git
synced 2025-04-30 18:56:45 +02:00
♻️ Keep guest ids in separate column (#1468)
This commit is contained in:
parent
2d7315f45a
commit
5b3c4ad2f6
15 changed files with 171 additions and 58 deletions
|
@ -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;
|
||||||
|
|
|
@ -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;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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,
|
||||||
},
|
},
|
||||||
|
}),
|
||||||
|
]);
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
|
@ -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}>
|
||||||
|
|
|
@ -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,
|
||||||
}}
|
}}
|
||||||
|
|
|
@ -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);
|
||||||
|
|
|
@ -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 });
|
||||||
},
|
},
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
|
|
|
@ -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;
|
||||||
}
|
}
|
||||||
|
|
|
@ -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
|
||||||
},
|
},
|
||||||
|
|
|
@ -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 };
|
||||||
|
|
|
@ -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,
|
||||||
|
|
|
@ -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: {
|
||||||
|
|
10
apps/web/src/utils/permissions.ts
Normal file
10
apps/web/src/utils/permissions.ts
Normal 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;
|
||||||
|
}
|
|
@ -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;
|
|
@ -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")
|
||||||
}
|
}
|
||||||
|
|
Loading…
Add table
Reference in a new issue