mirror of
https://github.com/lukevella/rallly.git
synced 2025-04-29 10:16:32 +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;
|
||||
title: string;
|
||||
createdAt: Date;
|
||||
userId: string;
|
||||
user: {
|
||||
id: string;
|
||||
name: string;
|
||||
} | null;
|
||||
guestId?: string | null;
|
||||
participants: {
|
||||
id: string;
|
||||
name: string;
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
||||
|
|
|
@ -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,
|
||||
},
|
||||
}),
|
||||
]);
|
||||
});
|
||||
};
|
||||
|
|
|
@ -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}>
|
||||
|
|
|
@ -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,
|
||||
}}
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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 });
|
||||
},
|
||||
}}
|
||||
>
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -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
|
||||
},
|
||||
|
|
|
@ -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 };
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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: {
|
||||
|
|
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
|
||||
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")
|
||||
}
|
||||
|
|
Loading…
Add table
Reference in a new issue