Force guest user (#210)

This commit is contained in:
Luke Vella 2022-06-28 10:33:17 +01:00 committed by GitHub
parent 000d105983
commit 1d768083ee
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
17 changed files with 111 additions and 146 deletions

View file

@ -2,7 +2,7 @@ import "iron-session";
declare module "iron-session" { declare module "iron-session" {
export interface IronSessionData { export interface IronSessionData {
user?: user:
| { | {
id: string; id: string;
name: string; name: string;

View file

@ -16,7 +16,7 @@
"@floating-ui/react-dom-interactions": "^0.4.0", "@floating-ui/react-dom-interactions": "^0.4.0",
"@headlessui/react": "^1.5.0", "@headlessui/react": "^1.5.0",
"@next/bundle-analyzer": "^12.1.0", "@next/bundle-analyzer": "^12.1.0",
"@prisma/client": "^3.14.0", "@prisma/client": "^3.15.2",
"@sentry/nextjs": "^7.0.0", "@sentry/nextjs": "^7.0.0",
"@svgr/webpack": "^6.2.1", "@svgr/webpack": "^6.2.1",
"@tailwindcss/forms": "^0.4.0", "@tailwindcss/forms": "^0.4.0",
@ -40,7 +40,7 @@
"next-i18next": "^10.5.0", "next-i18next": "^10.5.0",
"next-plausible": "^3.1.9", "next-plausible": "^3.1.9",
"nodemailer": "^6.7.2", "nodemailer": "^6.7.2",
"prisma": "^3.14.0", "prisma": "^3.15.2",
"react": "17.0.2", "react": "17.0.2",
"react-big-calendar": "^0.38.9", "react-big-calendar": "^0.38.9",
"react-dom": "17.0.2", "react-dom": "17.0.2",

View file

@ -0,0 +1,22 @@
/*
Warnings:
- You are about to drop the column `guest_id` on the `comments` table. All the data in the column will be lost.
- You are about to drop the column `guest_id` on the `participants` table. All the data in the column will be lost.
*/
-- Set user_id to guest_id
UPDATE "comments"
SET "user_id" = "guest_id"
WHERE "user_id" IS NULL;
-- AlterTable
ALTER TABLE "comments" DROP COLUMN "guest_id";
-- Set user_id to guest_id
UPDATE "participants"
SET "user_id" = "guest_id"
WHERE "user_id" IS NULL;
-- AlterTable
ALTER TABLE "participants" DROP COLUMN "guest_id";

View file

@ -16,8 +16,6 @@ model User {
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")
polls Poll[] polls Poll[]
participants Participant[]
comments Comment[]
@@map("users") @@map("users")
} }
@ -62,9 +60,7 @@ model Poll {
model Participant { model Participant {
id String @id @default(cuid()) id String @id @default(cuid())
name String name String
user User? @relation(fields: [userId], references: [id])
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[]
@ -116,9 +112,7 @@ model Comment {
poll Poll @relation(fields: [pollId], references: [id]) poll Poll @relation(fields: [pollId], references: [id])
pollId String @map("poll_id") pollId String @map("poll_id")
authorName String @map("author_name") authorName String @map("author_name")
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")

View file

@ -110,11 +110,7 @@ export const PollContextProvider: React.VoidFunctionComponent<{
const userAlreadyVoted = const userAlreadyVoted =
user && participants user && participants
? participants.some((participant) => ? participants.some((participant) => participant.userId === user.id)
user.isGuest
? participant.guestId === user.id
: participant.userId === user.id,
)
: false; : false;
const optionIds = parsedOptions.options.map(({ optionId }) => optionId); const optionIds = parsedOptions.options.map(({ optionId }) => optionId);

View file

@ -113,9 +113,9 @@ const ParticipantRow: React.VoidFunctionComponent<ParticipantRowProps> = ({
const isYou = session.user && session.ownsObject(participant) ? true : false; const isYou = session.user && session.ownsObject(participant) ? true : false;
const isAnonymous = !participant.userId && !participant.guestId; const isUnclaimed = !participant.userId;
const canEdit = !poll.closed && (poll.admin || isYou || isAnonymous); const canEdit = !poll.closed && (poll.admin || isYou || isUnclaimed);
if (editMode) { if (editMode) {
return ( return (

View file

@ -69,10 +69,8 @@ const MobilePoll: React.VoidFunctionComponent = () => {
} }
const { user } = session; const { user } = session;
if (user) { if (user) {
const userParticipant = participants.find((participant) => const userParticipant = participants.find(
user.isGuest (participant) => participant.userId === user.id,
? participant.guestId === user.id
: participant.userId === user.id,
); );
return userParticipant?.id; return userParticipant?.id;
} }

View file

@ -9,12 +9,11 @@ import { useRequiredContext } from "./use-required-context";
export type UserSessionData = NonNullable<IronSessionData["user"]>; export type UserSessionData = NonNullable<IronSessionData["user"]>;
export type SessionProps = { export type SessionProps = {
user: UserSessionData | null; user: UserSessionData;
}; };
type ParticipantOrComment = { type ParticipantOrComment = {
userId: string | null; userId: string | null;
guestId: string | null;
}; };
export type UserSessionDataExtended = UserSessionData & { export type UserSessionDataExtended = UserSessionData & {
@ -36,35 +35,33 @@ SessionContext.displayName = "SessionContext";
export const SessionProvider: React.VoidFunctionComponent<{ export const SessionProvider: React.VoidFunctionComponent<{
children?: React.ReactNode; children?: React.ReactNode;
session: UserSessionData | null; defaultUser: UserSessionData;
}> = ({ children, session }) => { }> = ({ children, defaultUser }) => {
const queryClient = trpc.useContext(); const queryClient = trpc.useContext();
const { const {
data: user = session, data: user = defaultUser,
refetch, refetch,
isLoading, isLoading,
} = trpc.useQuery(["session.get"]); } = trpc.useQuery(["session.get"]);
const logout = trpc.useMutation(["session.destroy"], { const logout = trpc.useMutation(["session.destroy"], {
onSuccess: () => { onSuccess: () => {
queryClient.setQueryData(["session.get"], null); queryClient.invalidateQueries(["session.get"]);
}, },
}); });
const sessionData: SessionContextValue = { const sessionData: SessionContextValue = {
user: user user: {
? { ...user,
...user, shortName:
shortName: // try to get the first name in the event
// try to get the first name in the event // that the user entered a full name
// that the user entered a full name user.isGuest
user.isGuest ? user.id.substring(0, 10)
? user.id.substring(0, 12) : user.name.length > 12 && user.name.indexOf(" ") !== -1
: user.name.length > 12 && user.name.indexOf(" ") !== -1 ? user.name.substring(0, user.name.indexOf(" "))
? user.name.substring(0, user.name.indexOf(" ")) : user.name,
: user.name, },
}
: null,
refresh: () => { refresh: () => {
refetch(); refetch();
}, },
@ -77,14 +74,6 @@ export const SessionProvider: React.VoidFunctionComponent<{
}); });
}, },
ownsObject: (obj) => { ownsObject: (obj) => {
if (!user) {
return false;
}
if (user.isGuest) {
return obj.guestId === user.id;
}
return obj.userId === user.id; return obj.userId === user.id;
}, },
}; };
@ -106,7 +95,7 @@ export const withSession = <P extends SessionProps>(
const ComposedComponent: React.VoidFunctionComponent<P> = (props: P) => { const ComposedComponent: React.VoidFunctionComponent<P> = (props: P) => {
const Component = component; const Component = component;
return ( return (
<SessionProvider session={props.user}> <SessionProvider defaultUser={props.user}>
<Component {...props} /> <Component {...props} />
</SessionProvider> </SessionProvider>
); );
@ -115,5 +104,4 @@ export const withSession = <P extends SessionProps>(
return ComposedComponent; return ComposedComponent;
}; };
export const isUnclaimed = (obj: ParticipantOrComment) => export const isUnclaimed = (obj: ParticipantOrComment) => !obj.userId;
!obj.guestId && !obj.userId;

View file

@ -2,26 +2,20 @@ import { GetServerSideProps } from "next";
import dynamic from "next/dynamic"; import dynamic from "next/dynamic";
import { serverSideTranslations } from "next-i18next/serverSideTranslations"; import { serverSideTranslations } from "next-i18next/serverSideTranslations";
import { CreatePollPageProps } from "@/components/create-poll"; import { withSessionSsr } from "../utils/auth";
import { withSessionSsr } from "@/utils/auth";
const getProps: GetServerSideProps<CreatePollPageProps> = async ({ export const getServerSideProps: GetServerSideProps = withSessionSsr(
locale = "en", async ({ locale = "en", query, req }) => {
query, return {
req, props: {
}) => { ...(await serverSideTranslations(locale, ["app"])),
return { ...query,
props: { user: req.session.user ?? null,
...(await serverSideTranslations(locale, ["app"])), },
...query, };
user: req.session.user ?? null, },
}, );
};
};
export const getServerSideProps = withSessionSsr(getProps);
// We disable SSR because the data on this page relies on sessionStore
export default dynamic(() => import("@/components/create-poll"), { export default dynamic(() => import("@/components/create-poll"), {
ssr: false, ssr: false,
}); });

View file

@ -11,7 +11,7 @@ import StandardLayout from "../components/standard-layout";
const Page: NextPage<{ user: UserSessionData }> = ({ user }) => { const Page: NextPage<{ user: UserSessionData }> = ({ user }) => {
const name = user.isGuest ? user.id : user.name; const name = user.isGuest ? user.id : user.name;
return ( return (
<SessionProvider session={user}> <SessionProvider defaultUser={user}>
<Head> <Head>
<title>Profile - {name}</title> <title>Profile - {name}</title>
</Head> </Head>

View file

@ -13,9 +13,11 @@ export const login = createRouter().mutation("login", {
resolve: async ({ ctx, input }) => { resolve: async ({ ctx, input }) => {
const { email, path } = input; const { email, path } = input;
const homePageUrl = absoluteUrl(); const homePageUrl = absoluteUrl();
const user = ctx.session.user;
const token = await createToken({ const token = await createToken({
email, email,
guestId: ctx.session.user?.isGuest ? ctx.session.user.id : undefined, guestId: user.id,
path, path,
}); });

View file

@ -3,7 +3,6 @@ import { z } from "zod";
import { prisma } from "~/prisma/db"; import { prisma } from "~/prisma/db";
import { sendNotification } from "../../../utils/api-utils"; import { sendNotification } from "../../../utils/api-utils";
import { createGuestUser } from "../../../utils/auth";
import { createRouter } from "../../createRouter"; import { createRouter } from "../../createRouter";
export const comments = createRouter() export const comments = createRouter()
@ -29,17 +28,14 @@ export const comments = createRouter()
content: z.string(), content: z.string(),
}), }),
resolve: async ({ ctx, input: { pollId, authorName, content } }) => { resolve: async ({ ctx, input: { pollId, authorName, content } }) => {
if (!ctx.session.user) { const user = ctx.session.user;
await createGuestUser(ctx.session);
}
const newComment = await prisma.comment.create({ const newComment = await prisma.comment.create({
data: { data: {
content, content,
pollId, pollId,
authorName, authorName,
userId: ctx.session.user?.isGuest ? undefined : ctx.session.user?.id, userId: user.id,
guestId: ctx.session.user?.isGuest ? ctx.session.user.id : undefined,
}, },
}); });

View file

@ -42,7 +42,7 @@ export const demo = createRouter().mutation("create", {
const participants: Array<{ const participants: Array<{
name: string; name: string;
id: string; id: string;
guestId: string; userId: string;
createdAt: Date; createdAt: Date;
}> = []; }> = [];
@ -58,7 +58,7 @@ export const demo = createRouter().mutation("create", {
participants.push({ participants.push({
id: participantId, id: participantId,
name, name,
guestId: "user-demo", userId: "user-demo",
createdAt: addMinutes(today, i * -1), createdAt: addMinutes(today, i * -1),
}); });

View file

@ -3,7 +3,6 @@ import { z } from "zod";
import { prisma } from "~/prisma/db"; import { prisma } from "~/prisma/db";
import { sendNotification } from "../../../utils/api-utils"; import { sendNotification } from "../../../utils/api-utils";
import { createGuestUser } from "../../../utils/auth";
import { createRouter } from "../../createRouter"; import { createRouter } from "../../createRouter";
export const participants = createRouter() export const participants = createRouter()
@ -57,22 +56,12 @@ export const participants = createRouter()
.array(), .array(),
}), }),
resolve: async ({ ctx, input: { pollId, votes, name } }) => { resolve: async ({ ctx, input: { pollId, votes, name } }) => {
if (!ctx.session.user) { const user = ctx.session.user;
await createGuestUser(ctx.session);
}
const participant = await prisma.participant.create({ const participant = await prisma.participant.create({
data: { data: {
pollId: pollId, pollId: pollId,
name: name, name: name,
userId: userId: user.id,
ctx.session.user?.isGuest === false
? ctx.session.user.id
: undefined,
guestId:
ctx.session.user?.isGuest === true
? ctx.session.user.id
: undefined,
votes: { votes: {
createMany: { createMany: {
data: votes.map(({ optionId, type }) => ({ data: votes.map(({ optionId, type }) => ({

View file

@ -1,30 +1,8 @@
import { prisma } from "~/prisma/db";
import { createRouter } from "../createRouter"; import { createRouter } from "../createRouter";
export const session = createRouter() export const session = createRouter()
.query("get", { .query("get", {
async resolve({ ctx }) { async resolve({ ctx }) {
if (ctx.session.user?.isGuest === false) {
const user = await prisma.user.findUnique({
where: { id: ctx.session.user.id },
});
if (!user) {
ctx.session.destroy();
return null;
}
ctx.session.user = {
id: user.id,
name: user.name,
email: user.email,
isGuest: false,
};
await ctx.session.save();
}
return ctx.session.user; return ctx.session.user;
}, },
}) })

View file

@ -1,9 +1,4 @@
import { import { IronSessionOptions, sealData, unsealData } from "iron-session";
IronSession,
IronSessionOptions,
sealData,
unsealData,
} from "iron-session";
import { withIronSessionApiRoute, withIronSessionSsr } from "iron-session/next"; import { withIronSessionApiRoute, withIronSessionSsr } from "iron-session/next";
import { GetServerSideProps, NextApiHandler } from "next"; import { GetServerSideProps, NextApiHandler } from "next";
@ -21,11 +16,24 @@ const sessionOptions: IronSessionOptions = {
}; };
export function withSessionRoute(handler: NextApiHandler) { export function withSessionRoute(handler: NextApiHandler) {
return withIronSessionApiRoute(handler, sessionOptions); return withIronSessionApiRoute(async (req, res) => {
if (!req.session.user) {
req.session.user = await createGuestUser();
await req.session.save();
}
return await handler(req, res);
}, sessionOptions);
} }
export function withSessionSsr(handler: GetServerSideProps) { export function withSessionSsr(handler: GetServerSideProps) {
return withIronSessionSsr(handler, sessionOptions); return withIronSessionSsr(async (context) => {
const { req } = context;
if (!req.session.user) {
req.session.user = await createGuestUser();
await req.session.save();
}
return await handler(context);
}, sessionOptions);
} }
export const decryptToken = async <P extends Record<string, unknown>>( export const decryptToken = async <P extends Record<string, unknown>>(
@ -43,12 +51,14 @@ export const createToken = async <T extends Record<string, unknown>>(
}); });
}; };
export const createGuestUser = async (session: IronSession) => { const createGuestUser = async (): Promise<{
session.user = { isGuest: true;
id: string;
}> => {
return {
id: `user-${await randomid()}`, id: `user-${await randomid()}`,
isGuest: true, isGuest: true,
}; };
await session.save();
}; };
// assigns participants and comments created by guests to a user // assigns participants and comments created by guests to a user
@ -60,24 +70,22 @@ export const mergeGuestsIntoUser = async (
) => { ) => {
await prisma.participant.updateMany({ await prisma.participant.updateMany({
where: { where: {
guestId: { userId: {
in: guestIds, in: guestIds,
}, },
}, },
data: { data: {
guestId: null,
userId: userId, userId: userId,
}, },
}); });
await prisma.comment.updateMany({ await prisma.comment.updateMany({
where: { where: {
guestId: { userId: {
in: guestIds, in: guestIds,
}, },
}, },
data: { data: {
guestId: null,
userId: userId, userId: userId,
}, },
}); });

View file

@ -1299,22 +1299,22 @@
resolved "https://registry.npmjs.org/@popperjs/core/-/core-2.9.2.tgz" resolved "https://registry.npmjs.org/@popperjs/core/-/core-2.9.2.tgz"
integrity sha512-VZMYa7+fXHdwIq1TDhSXoVmSPEGM/aa+6Aiq3nVVJ9bXr24zScr+NlKFKC3iPljA7ho/GAZr+d2jOf5GIRC30Q== integrity sha512-VZMYa7+fXHdwIq1TDhSXoVmSPEGM/aa+6Aiq3nVVJ9bXr24zScr+NlKFKC3iPljA7ho/GAZr+d2jOf5GIRC30Q==
"@prisma/client@^3.14.0": "@prisma/client@^3.15.2":
version "3.14.0" version "3.15.2"
resolved "https://registry.yarnpkg.com/@prisma/client/-/client-3.14.0.tgz#bb90405c012fcca11f4647d91153ed4c58f3bd48" resolved "https://registry.yarnpkg.com/@prisma/client/-/client-3.15.2.tgz#2181398147afc79bfe0d83c03a88dc45b49bd365"
integrity sha512-atb41UpgTR1MCst0VIbiHTMw8lmXnwUvE1KyUCAkq08+wJyjRE78Due+nSf+7uwqQn+fBFYVmoojtinhlLOSaA== integrity sha512-ErqtwhX12ubPhU4d++30uFY/rPcyvjk+mdifaZO5SeM21zS3t4jQrscy8+6IyB0GIYshl5ldTq6JSBo1d63i8w==
dependencies: dependencies:
"@prisma/engines-version" "3.14.0-36.2b0c12756921c891fec4f68d9444e18c7d5d4a6a" "@prisma/engines-version" "3.15.1-1.461d6a05159055555eb7dfb337c9fb271cbd4d7e"
"@prisma/engines-version@3.14.0-36.2b0c12756921c891fec4f68d9444e18c7d5d4a6a": "@prisma/engines-version@3.15.1-1.461d6a05159055555eb7dfb337c9fb271cbd4d7e":
version "3.14.0-36.2b0c12756921c891fec4f68d9444e18c7d5d4a6a" version "3.15.1-1.461d6a05159055555eb7dfb337c9fb271cbd4d7e"
resolved "https://registry.yarnpkg.com/@prisma/engines-version/-/engines-version-3.14.0-36.2b0c12756921c891fec4f68d9444e18c7d5d4a6a.tgz#4edae57cf6527f35e22cebe75e49214fc0e99ac9" resolved "https://registry.yarnpkg.com/@prisma/engines-version/-/engines-version-3.15.1-1.461d6a05159055555eb7dfb337c9fb271cbd4d7e.tgz#bf5e2373ca68ce7556b967cb4965a7095e93fe53"
integrity sha512-D+yHzq4a2r2Rrd0ZOW/mTZbgDIkUkD8ofKgusEI1xPiZz60Daks+UM7Me2ty5FzH3p/TgyhBpRrfIHx+ha20RQ== integrity sha512-e3k2Vd606efd1ZYy2NQKkT4C/pn31nehyLhVug6To/q8JT8FpiMrDy7zmm3KLF0L98NOQQcutaVtAPhzKhzn9w==
"@prisma/engines@3.14.0-36.2b0c12756921c891fec4f68d9444e18c7d5d4a6a": "@prisma/engines@3.15.1-1.461d6a05159055555eb7dfb337c9fb271cbd4d7e":
version "3.14.0-36.2b0c12756921c891fec4f68d9444e18c7d5d4a6a" version "3.15.1-1.461d6a05159055555eb7dfb337c9fb271cbd4d7e"
resolved "https://registry.yarnpkg.com/@prisma/engines/-/engines-3.14.0-36.2b0c12756921c891fec4f68d9444e18c7d5d4a6a.tgz#7fa11bc26a51d450185c816cc0ab8cac673fb4bf" resolved "https://registry.yarnpkg.com/@prisma/engines/-/engines-3.15.1-1.461d6a05159055555eb7dfb337c9fb271cbd4d7e.tgz#f691893df506b93e3cb1ccc15ec6e5ac64e8e570"
integrity sha512-LwZvI3FY6f43xFjQNRuE10JM5R8vJzFTSmbV9X0Wuhv9kscLkjRlZt0BEoiHmO+2HA3B3xxbMfB5du7ZoSFXGg== integrity sha512-NHlojO1DFTsSi3FtEleL9QWXeSF/UjhCW0fgpi7bumnNZ4wj/eQ+BJJ5n2pgoOliTOGv9nX2qXvmHap7rJMNmg==
"@restart/hooks@^0.3.25": "@restart/hooks@^0.3.25":
version "0.3.26" version "0.3.26"
@ -4503,12 +4503,12 @@ prettier@^2.3.0:
resolved "https://registry.npmjs.org/prettier/-/prettier-2.3.0.tgz" resolved "https://registry.npmjs.org/prettier/-/prettier-2.3.0.tgz"
integrity sha512-kXtO4s0Lz/DW/IJ9QdWhAf7/NmPWQXkFr/r/WkR3vyI+0v8amTDxiaQSLzs8NBlytfLWX/7uQUMIW677yLKl4w== integrity sha512-kXtO4s0Lz/DW/IJ9QdWhAf7/NmPWQXkFr/r/WkR3vyI+0v8amTDxiaQSLzs8NBlytfLWX/7uQUMIW677yLKl4w==
prisma@^3.14.0: prisma@^3.15.2:
version "3.14.0" version "3.15.2"
resolved "https://registry.yarnpkg.com/prisma/-/prisma-3.14.0.tgz#dd67ece37d7b5373e9fd9588971de0024b49be81" resolved "https://registry.yarnpkg.com/prisma/-/prisma-3.15.2.tgz#4ebe32fb284da3ac60c49fbc16c75e56ecf32067"
integrity sha512-l9MOgNCn/paDE+i1K2fp9NZ+Du4trzPTJsGkaQHVBufTGqzoYHuNk8JfzXuIn0Gte6/ZjyKj652Jq/Lc1tp2yw== integrity sha512-nMNSMZvtwrvoEQ/mui8L/aiCLZRCj5t6L3yujKpcDhIPk7garp8tL4nMx2+oYsN0FWBacevJhazfXAbV1kfBzA==
dependencies: dependencies:
"@prisma/engines" "3.14.0-36.2b0c12756921c891fec4f68d9444e18c7d5d4a6a" "@prisma/engines" "3.15.1-1.461d6a05159055555eb7dfb337c9fb271cbd4d7e"
process-nextick-args@~2.0.0: process-nextick-args@~2.0.0:
version "2.0.1" version "2.0.1"