diff --git a/apps/web/public/locales/en/app.json b/apps/web/public/locales/en/app.json index 970b010f6..62643aa22 100644 --- a/apps/web/public/locales/en/app.json +++ b/apps/web/public/locales/en/app.json @@ -222,5 +222,6 @@ "hideScoresLabel": "Hide scores until after a participant has voted", "authErrorTitle": "Login Error", "authErrorDescription": "There was an error logging you in. Please try again.", - "authErrorCta": "Go to login page" + "authErrorCta": "Go to login page", + "continueAs": "Continue as" } diff --git a/apps/web/src/components/user-dropdown.tsx b/apps/web/src/components/user-dropdown.tsx index d945d4592..219d224cd 100644 --- a/apps/web/src/components/user-dropdown.tsx +++ b/apps/web/src/components/user-dropdown.tsx @@ -38,7 +38,7 @@ export const UserDropdown = () => { const { user } = useUser(); return ( - + + + + ); }; +Page.getLayout = (page) => ( + {page} +); + +export const getServerSideProps: GetServerSideProps = async ( + ctx, +) => { + const parse = params.safeParse(ctx.query); + + if (!parse.success) { + return { + notFound: true, + }; + } + + const { magicLink } = parse.data; + + const url = new URL(magicLink); + + const parseMagicLink = magicLinkParams.safeParse( + Object.fromEntries(url.searchParams), + ); + + if (!parseMagicLink.success) { + return { + notFound: true, + }; + } + + return { + props: { + magicLink, + email: parseMagicLink.data.email, + ...(await getServerSideTranslations(ctx)), + }, + }; +}; + export default Page; diff --git a/apps/web/src/utils/auth.ts b/apps/web/src/utils/auth.ts index f0817318b..cb997e4e1 100644 --- a/apps/web/src/utils/auth.ts +++ b/apps/web/src/utils/auth.ts @@ -1,3 +1,4 @@ +import { PrismaAdapter } from "@auth/prisma-adapter"; import { RegistrationTokenPayload } from "@rallly/backend"; import { decryptToken } from "@rallly/backend/session"; import { generateOtp, randomid } from "@rallly/backend/utils/nanoid"; @@ -16,14 +17,14 @@ import NextAuth, { import CredentialsProvider from "next-auth/providers/credentials"; import EmailProvider from "next-auth/providers/email"; -import { CustomPrismaAdapter } from "@/utils/auth/custom-prisma-adapter"; +import { absoluteUrl } from "@/utils/absolute-url"; import { LegacyTokenProvider } from "@/utils/auth/legacy-token-provider"; import { mergeGuestsIntoUser } from "@/utils/auth/merge-user"; import { emailClient } from "@/utils/emails"; const getAuthOptions = (...args: GetServerSessionParams) => ({ - adapter: CustomPrismaAdapter(prisma), + adapter: PrismaAdapter(prisma), secret: process.env.SECRET_PASSWORD, session: { strategy: "jwt", @@ -103,7 +104,9 @@ const getAuthOptions = (...args: GetServerSessionParams) => subject: `${token} is your 6-digit code`, props: { name: user.name, - magicLink: url, + magicLink: absoluteUrl("/auth/login", { + magicLink: url, + }), code: token, }, }); diff --git a/apps/web/src/utils/auth/custom-prisma-adapter.ts b/apps/web/src/utils/auth/custom-prisma-adapter.ts deleted file mode 100644 index 71a52551c..000000000 --- a/apps/web/src/utils/auth/custom-prisma-adapter.ts +++ /dev/null @@ -1,38 +0,0 @@ -import { PrismaAdapter } from "@auth/prisma-adapter"; -import { Prisma, PrismaClient } from "@prisma/client"; -import { Adapter } from "next-auth/adapters"; - -export function CustomPrismaAdapter(prisma: PrismaClient): Adapter { - const adapter = PrismaAdapter(prisma); - return { - ...adapter, - // NOTE: Some users have inboxes with spam filters that check all links before they are delivered. - // This means the verification link will be used before the user gets it. To get around this, we - // avoid deleting the verification token when it is used. Instead we delete all verification tokens - // for an email address when a new verification token is created. - async createVerificationToken(data) { - await prisma.verificationToken.deleteMany({ - where: { identifier: data.identifier }, - }); - - const verificationToken = await prisma.verificationToken.create({ - data, - }); - - return verificationToken; - }, - async useVerificationToken(identifier_token) { - try { - const verificationToken = await prisma.verificationToken.findUnique({ - where: { identifier_token }, - }); - return verificationToken; - } catch (error) { - // https://www.prisma.io/docs/reference/api-reference/error-reference#p2025 - if ((error as Prisma.PrismaClientKnownRequestError).code === "P2025") - return null; - throw error; - } - }, - }; -} diff --git a/apps/web/src/utils/with-page-translations.ts b/apps/web/src/utils/with-page-translations.ts index 92077639d..a4a7f54e7 100644 --- a/apps/web/src/utils/with-page-translations.ts +++ b/apps/web/src/utils/with-page-translations.ts @@ -2,10 +2,17 @@ import { GetStaticProps } from "next"; import { serverSideTranslations } from "next-i18next/serverSideTranslations"; export const getStaticTranslations: GetStaticProps = async (ctx) => { - const locale = ctx.locale ?? "en"; return { props: { - ...(await serverSideTranslations(locale)), + ...(await getServerSideTranslations(ctx)), }, }; }; + +export const getServerSideTranslations = async ({ + locale, +}: { + locale?: string; +}) => { + return await serverSideTranslations(locale ?? "en"); +}; diff --git a/apps/web/tests/authentication.spec.ts b/apps/web/tests/authentication.spec.ts index 1a8c71e2d..a736fc09d 100644 --- a/apps/web/tests/authentication.spec.ts +++ b/apps/web/tests/authentication.spec.ts @@ -125,7 +125,13 @@ test.describe.serial(() => { await page.goto(magicLink); + await page.getByRole("button", { name: "Continue" }).click(); + await page.waitForURL("/polls"); + + await page.getByTestId("user-dropdown").click(); + + await expect(page.getByText("Test User")).toBeVisible(); }); test("can login with verification code", async ({ page }) => { @@ -144,6 +150,10 @@ test.describe.serial(() => { await page.getByRole("button", { name: "Continue" }).click(); await page.waitForURL("/polls"); + + await page.getByTestId("user-dropdown").click(); + + await expect(page.getByText("Test User")).toBeVisible(); }); }); }); diff --git a/packages/backend/trpc/routers/user.ts b/packages/backend/trpc/routers/user.ts index 9068aa908..edf7b219d 100644 --- a/packages/backend/trpc/routers/user.ts +++ b/packages/backend/trpc/routers/user.ts @@ -2,7 +2,12 @@ import { prisma } from "@rallly/database"; import { z } from "zod"; import { getSubscriptionStatus } from "../../utils/auth"; -import { possiblyPublicProcedure, privateProcedure, router } from "../trpc"; +import { + possiblyPublicProcedure, + privateProcedure, + publicProcedure, + router, +} from "../trpc"; export const user = router({ getBilling: possiblyPublicProcedure.query(async ({ ctx }) => { @@ -20,6 +25,19 @@ export const user = router({ }, }); }), + getByEmail: publicProcedure + .input(z.object({ email: z.string() })) + .query(async ({ input }) => { + return await prisma.user.findUnique({ + where: { + email: input.email, + }, + select: { + name: true, + email: true, + }, + }); + }), subscription: possiblyPublicProcedure.query( async ({ ctx }): Promise<{ legacy?: boolean; active: boolean }> => { if (ctx.user.isGuest) {