From d7b518f4f1e2a87665c37314d829ce8b4fd8a64d Mon Sep 17 00:00:00 2001 From: Luke Vella Date: Thu, 6 Feb 2025 11:34:50 +0700 Subject: [PATCH 01/18] Reorganize provider --- apps/web/src/auth.ts | 176 ++---------------- apps/web/src/auth/providers/email.ts | 40 ++++ apps/web/src/auth/providers/google.ts | 11 ++ apps/web/src/auth/providers/guest.ts | 14 ++ apps/web/src/auth/providers/microsoft.ts | 19 ++ apps/web/src/auth/providers/oidc.ts | 34 ++++ .../src/auth/providers/registration-token.ts | 47 +++++ 7 files changed, 184 insertions(+), 157 deletions(-) create mode 100644 apps/web/src/auth/providers/email.ts create mode 100644 apps/web/src/auth/providers/google.ts create mode 100644 apps/web/src/auth/providers/guest.ts create mode 100644 apps/web/src/auth/providers/microsoft.ts create mode 100644 apps/web/src/auth/providers/oidc.ts create mode 100644 apps/web/src/auth/providers/registration-token.ts diff --git a/apps/web/src/auth.ts b/apps/web/src/auth.ts index 04c9833324a..0c45fb2fbc7 100644 --- a/apps/web/src/auth.ts +++ b/apps/web/src/auth.ts @@ -1,172 +1,29 @@ import { prisma } from "@rallly/database"; import { posthog } from "@rallly/posthog/server"; -import { absoluteUrl } from "@rallly/utils/absolute-url"; -import { generateOtp, randomid } from "@rallly/utils/nanoid"; import type { GetServerSidePropsContext, NextApiRequest, NextApiResponse, } from "next"; -import type { NextAuthOptions, User } from "next-auth"; +import type { NextAuthOptions } from "next-auth"; import NextAuth, { getServerSession as getServerSessionWithOptions, } from "next-auth/next"; -import AzureADProvider from "next-auth/providers/azure-ad"; -import CredentialsProvider from "next-auth/providers/credentials"; -import EmailProvider from "next-auth/providers/email"; -import GoogleProvider from "next-auth/providers/google"; import type { Provider } from "next-auth/providers/index"; -import { env } from "@/env"; -import type { RegistrationTokenPayload } from "@/trpc/types"; -import { getEmailClient } from "@/utils/emails"; -import { getValueByPath } from "@/utils/get-value-by-path"; -import { decryptToken } from "@/utils/session"; - import { CustomPrismaAdapter } from "./auth/custom-prisma-adapter"; import { mergeGuestsIntoUser } from "./auth/merge-user"; - -const providers: Provider[] = [ - // When a user registers, we don't want to go through the email verification process - // so this provider allows us exchange the registration token for a session token - CredentialsProvider({ - id: "registration-token", - name: "Registration Token", - credentials: { - token: { - label: "Token", - type: "text", - }, - }, - async authorize(credentials) { - if (credentials?.token) { - const payload = await decryptToken( - credentials.token, - ); - if (payload) { - const user = await prisma.user.findUnique({ - where: { - email: payload.email, - }, - select: { - id: true, - email: true, - name: true, - locale: true, - timeFormat: true, - timeZone: true, - image: true, - }, - }); - - if (user) { - return user; - } - } - } - - return null; - }, - }), - CredentialsProvider({ - id: "guest", - name: "Guest", - credentials: {}, - async authorize() { - return { - id: `user-${randomid()}`, - email: null, - }; - }, - }), - EmailProvider({ - server: "", - from: process.env.NOREPLY_EMAIL, - generateVerificationToken() { - return generateOtp(); - }, - async sendVerificationRequest({ identifier: email, token, url }) { - const user = await prisma.user.findUnique({ - where: { - email, - }, - select: { - name: true, - locale: true, - }, - }); - - if (user) { - await getEmailClient(user.locale ?? undefined).sendTemplate( - "LoginEmail", - { - to: email, - props: { - magicLink: absoluteUrl("/auth/login", { - magicLink: url, - }), - code: token, - }, - }, - ); - } - }, - }), -]; - -// If we have an OAuth provider configured, we add it to the list of providers -if ( - process.env.OIDC_DISCOVERY_URL && - process.env.OIDC_CLIENT_ID && - process.env.OIDC_CLIENT_SECRET -) { - providers.push({ - id: "oidc", - name: process.env.OIDC_NAME ?? "OpenID Connect", - type: "oauth", - wellKnown: process.env.OIDC_DISCOVERY_URL, - authorization: { params: { scope: "openid email profile" } }, - clientId: process.env.OIDC_CLIENT_ID, - clientSecret: process.env.OIDC_CLIENT_SECRET, - idToken: true, - checks: ["state"], - allowDangerousEmailAccountLinking: true, - profile(profile) { - return { - id: profile.sub, - name: getValueByPath(profile, env.OIDC_NAME_CLAIM_PATH), - email: getValueByPath(profile, env.OIDC_EMAIL_CLAIM_PATH), - image: getValueByPath(profile, env.OIDC_PICTURE_CLAIM_PATH), - } as User; - }, - }); -} - -if (process.env.GOOGLE_CLIENT_ID && process.env.GOOGLE_CLIENT_SECRET) { - providers.push( - GoogleProvider({ - clientId: process.env.GOOGLE_CLIENT_ID, - clientSecret: process.env.GOOGLE_CLIENT_SECRET, - allowDangerousEmailAccountLinking: true, - }), - ); -} - -if ( - process.env.MICROSOFT_TENANT_ID && - process.env.MICROSOFT_CLIENT_ID && - process.env.MICROSOFT_CLIENT_SECRET -) { - providers.push( - AzureADProvider({ - name: "Microsoft", - tenantId: process.env.MICROSOFT_TENANT_ID, - clientId: process.env.MICROSOFT_CLIENT_ID, - clientSecret: process.env.MICROSOFT_CLIENT_SECRET, - wellKnown: - "https://login.microsoftonline.com/common/v2.0/.well-known/openid-configuration", - }), - ); +import { EmailProvider } from "./auth/providers/email"; +import { GoogleProvider } from "./auth/providers/google"; +import { GuestProvider } from "./auth/providers/guest"; +import { MicrosoftProvider } from "./auth/providers/microsoft"; +import { OIDCProvider } from "./auth/providers/oidc"; +import { RegistrationTokenProvider } from "./auth/providers/registration-token"; + +function getOptionalProviders() { + return [OIDCProvider(), GoogleProvider(), MicrosoftProvider()].filter( + Boolean, + ) as Provider[]; } const getAuthOptions = (...args: GetServerSessionParams) => @@ -183,7 +40,12 @@ const getAuthOptions = (...args: GetServerSessionParams) => session: { strategy: "jwt", }, - providers: providers, + providers: [ + RegistrationTokenProvider, + GuestProvider, + EmailProvider, + ...getOptionalProviders(), + ], pages: { signIn: "/login", verifyRequest: "/login/verify", @@ -360,7 +222,7 @@ export function getOAuthProviders(): { id: string; name: string; }[] { - return providers + return getOptionalProviders() .filter((provider) => provider.type === "oauth") .map((provider) => { return { diff --git a/apps/web/src/auth/providers/email.ts b/apps/web/src/auth/providers/email.ts new file mode 100644 index 00000000000..69bbf95225c --- /dev/null +++ b/apps/web/src/auth/providers/email.ts @@ -0,0 +1,40 @@ +import { prisma } from "@rallly/database"; +import { absoluteUrl } from "@rallly/utils/absolute-url"; +import { generateOtp } from "@rallly/utils/nanoid"; +import BaseEmailProvider from "next-auth/providers/email"; + +import { getEmailClient } from "@/utils/emails"; + +export const EmailProvider = BaseEmailProvider({ + server: "", + from: process.env.NOREPLY_EMAIL, + generateVerificationToken() { + return generateOtp(); + }, + async sendVerificationRequest({ identifier: email, token, url }) { + const user = await prisma.user.findUnique({ + where: { + email, + }, + select: { + name: true, + locale: true, + }, + }); + + if (user) { + await getEmailClient(user.locale ?? undefined).sendTemplate( + "LoginEmail", + { + to: email, + props: { + magicLink: absoluteUrl("/auth/login", { + magicLink: url, + }), + code: token, + }, + }, + ); + } + }, +}); diff --git a/apps/web/src/auth/providers/google.ts b/apps/web/src/auth/providers/google.ts new file mode 100644 index 00000000000..d344aae9cae --- /dev/null +++ b/apps/web/src/auth/providers/google.ts @@ -0,0 +1,11 @@ +import BaseGoogleProvider from "next-auth/providers/google"; + +export function GoogleProvider() { + if (process.env.GOOGLE_CLIENT_ID && process.env.GOOGLE_CLIENT_SECRET) { + return BaseGoogleProvider({ + clientId: process.env.GOOGLE_CLIENT_ID, + clientSecret: process.env.GOOGLE_CLIENT_SECRET, + allowDangerousEmailAccountLinking: true, + }); + } +} diff --git a/apps/web/src/auth/providers/guest.ts b/apps/web/src/auth/providers/guest.ts new file mode 100644 index 00000000000..4894a98aaab --- /dev/null +++ b/apps/web/src/auth/providers/guest.ts @@ -0,0 +1,14 @@ +import { randomid } from "@rallly/utils/nanoid"; +import CredentialsProvider from "next-auth/providers/credentials"; + +export const GuestProvider = CredentialsProvider({ + id: "guest", + name: "Guest", + credentials: {}, + async authorize() { + return { + id: `user-${randomid()}`, + email: null, + }; + }, +}); diff --git a/apps/web/src/auth/providers/microsoft.ts b/apps/web/src/auth/providers/microsoft.ts new file mode 100644 index 00000000000..2738bf369af --- /dev/null +++ b/apps/web/src/auth/providers/microsoft.ts @@ -0,0 +1,19 @@ +import AzureADProvider from "next-auth/providers/azure-ad"; + +export function MicrosoftProvider() { + if ( + process.env.MICROSOFT_TENANT_ID && + process.env.MICROSOFT_CLIENT_ID && + process.env.MICROSOFT_CLIENT_SECRET + ) { + return AzureADProvider({ + name: "Microsoft", + tenantId: process.env.MICROSOFT_TENANT_ID, + clientId: process.env.MICROSOFT_CLIENT_ID, + clientSecret: process.env.MICROSOFT_CLIENT_SECRET, + wellKnown: + "https://login.microsoftonline.com/common/v2.0/.well-known/openid-configuration", + }); + } + return null; +} diff --git a/apps/web/src/auth/providers/oidc.ts b/apps/web/src/auth/providers/oidc.ts new file mode 100644 index 00000000000..e6ac62fc1e3 --- /dev/null +++ b/apps/web/src/auth/providers/oidc.ts @@ -0,0 +1,34 @@ +import type { User } from "next-auth"; +import type { Provider } from "next-auth/providers/index"; + +import { env } from "@/env"; +import { getValueByPath } from "@/utils/get-value-by-path"; + +export const OIDCProvider = () => { + if ( + process.env.OIDC_DISCOVERY_URL && + process.env.OIDC_CLIENT_ID && + process.env.OIDC_CLIENT_SECRET + ) { + return { + id: "oidc", + name: process.env.OIDC_NAME ?? "OpenID Connect", + type: "oauth", + wellKnown: process.env.OIDC_DISCOVERY_URL, + authorization: { params: { scope: "openid email profile" } }, + clientId: process.env.OIDC_CLIENT_ID, + clientSecret: process.env.OIDC_CLIENT_SECRET, + idToken: true, + checks: ["state"], + allowDangerousEmailAccountLinking: true, + profile(profile) { + return { + id: profile.sub, + name: getValueByPath(profile, env.OIDC_NAME_CLAIM_PATH), + email: getValueByPath(profile, env.OIDC_EMAIL_CLAIM_PATH), + image: getValueByPath(profile, env.OIDC_PICTURE_CLAIM_PATH), + } as User; + }, + } satisfies Provider; + } +}; diff --git a/apps/web/src/auth/providers/registration-token.ts b/apps/web/src/auth/providers/registration-token.ts new file mode 100644 index 00000000000..c1d2fb56956 --- /dev/null +++ b/apps/web/src/auth/providers/registration-token.ts @@ -0,0 +1,47 @@ +import { prisma } from "@rallly/database"; +import CredentialsProvider from "next-auth/providers/credentials"; + +import type { RegistrationTokenPayload } from "@/trpc/types"; +import { decryptToken } from "@/utils/session"; + +// When a user registers, we don't want to go through the email verification process +// so this provider allows us exchange the registration token for a session token +export const RegistrationTokenProvider = CredentialsProvider({ + id: "registration-token", + name: "Registration Token", + credentials: { + token: { + label: "Token", + type: "text", + }, + }, + async authorize(credentials) { + if (credentials?.token) { + const payload = await decryptToken( + credentials.token, + ); + if (payload) { + const user = await prisma.user.findUnique({ + where: { + email: payload.email, + }, + select: { + id: true, + email: true, + name: true, + locale: true, + timeFormat: true, + timeZone: true, + image: true, + }, + }); + + if (user) { + return user; + } + } + } + + return null; + }, +}); From 88b9de5fc2901372cf5ddb5cdce1ae0a80c3b055 Mon Sep 17 00:00:00 2001 From: Luke Vella Date: Thu, 6 Feb 2025 15:32:01 +0700 Subject: [PATCH 02/18] =?UTF-8?q?=E2=99=BB=EF=B8=8F=20Upgrade=20to=20lates?= =?UTF-8?q?t=20next-auth?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- apps/web/declarations/next-auth.d.ts | 1 + apps/web/package.json | 4 +- .../profile/delete-account-dialog.tsx | 2 +- .../login/components/login-email-form.tsx | 4 +- .../login/components/login-with-oidc.tsx | 2 +- .../(auth)/login/components/sso-provider.tsx | 4 +- .../src/app/[locale]/(auth)/login/page.tsx | 36 ++-- apps/web/src/app/[locale]/layout.tsx | 4 +- .../src/app/api/auth/[...nextauth]/route.ts | 6 + .../api/notifications/unsubscribe/route.ts | 4 +- apps/web/src/app/api/stripe/checkout/route.ts | 4 +- apps/web/src/app/api/stripe/portal/route.ts | 4 +- apps/web/src/app/api/trpc/[trpc]/route.ts | 4 +- .../app/api/user/verify-email-change/route.ts | 4 +- apps/web/src/auth.ts | 17 +- .../prisma.ts} | 21 ++- apps/web/src/auth/get-optional-providers.ts | 11 ++ apps/web/src/auth/is-email-blocked.ts | 19 ++ apps/web/src/auth/providers/email.ts | 7 +- apps/web/src/auth/providers/microsoft.ts | 6 +- apps/web/src/auth/providers/oidc.ts | 6 +- .../quick-create/lib/get-guest-polls.ts | 4 +- apps/web/src/middleware.ts | 104 ++++------- apps/web/src/next-auth.confg.ts | 9 + apps/web/src/next-auth.ts | 166 ++++++++++++++++++ apps/web/src/pages/api/auth/[...nextauth].ts | 14 -- apps/web/src/trpc/server/create-ssr-helper.ts | 4 +- packages/posthog/src/server/index.ts | 8 + yarn.lock | 138 +++++++-------- 29 files changed, 386 insertions(+), 231 deletions(-) create mode 100644 apps/web/src/app/api/auth/[...nextauth]/route.ts rename apps/web/src/auth/{custom-prisma-adapter.ts => adapters/prisma.ts} (69%) create mode 100644 apps/web/src/auth/get-optional-providers.ts create mode 100644 apps/web/src/auth/is-email-blocked.ts create mode 100644 apps/web/src/next-auth.confg.ts create mode 100644 apps/web/src/next-auth.ts delete mode 100644 apps/web/src/pages/api/auth/[...nextauth].ts diff --git a/apps/web/declarations/next-auth.d.ts b/apps/web/declarations/next-auth.d.ts index cfcdd3bfaee..9f5f276a164 100644 --- a/apps/web/declarations/next-auth.d.ts +++ b/apps/web/declarations/next-auth.d.ts @@ -20,6 +20,7 @@ declare module "next-auth" { } interface User extends DefaultUser { + id: string; locale?: string | null; timeZone?: string | null; timeFormat?: TimeFormat | null; diff --git a/apps/web/package.json b/apps/web/package.json index 8632f442eb6..4588f352eb5 100644 --- a/apps/web/package.json +++ b/apps/web/package.json @@ -18,7 +18,7 @@ "docker:start": "./scripts/docker-start.sh" }, "dependencies": { - "@auth/prisma-adapter": "^1.0.3", + "@auth/prisma-adapter": "^2.7.4", "@aws-sdk/client-s3": "^3.645.0", "@aws-sdk/s3-request-presigner": "^3.645.0", "@hookform/resolvers": "^3.3.1", @@ -67,7 +67,7 @@ "lucide-react": "^0.387.0", "micro": "^10.0.1", "nanoid": "^5.0.9", - "next-auth": "^4.24.5", + "next-auth": "^5.0.0-beta.25", "next-i18next": "^13.0.3", "php-serialize": "^4.1.1", "postcss": "^8.4.31", diff --git a/apps/web/src/app/[locale]/(admin)/settings/profile/delete-account-dialog.tsx b/apps/web/src/app/[locale]/(admin)/settings/profile/delete-account-dialog.tsx index b4503d09760..a291862414b 100644 --- a/apps/web/src/app/[locale]/(admin)/settings/profile/delete-account-dialog.tsx +++ b/apps/web/src/app/[locale]/(admin)/settings/profile/delete-account-dialog.tsx @@ -38,7 +38,7 @@ export function DeleteAccountDialog({ onSuccess() { posthog?.capture("delete account"); signOut({ - callbackUrl: "/login", + redirectTo: "/login", }); }, }); diff --git a/apps/web/src/app/[locale]/(auth)/login/components/login-email-form.tsx b/apps/web/src/app/[locale]/(auth)/login/components/login-email-form.tsx index b0e4e9de238..eac42707025 100644 --- a/apps/web/src/app/[locale]/(auth)/login/components/login-email-form.tsx +++ b/apps/web/src/app/[locale]/(auth)/login/components/login-email-form.tsx @@ -53,13 +53,13 @@ export function LoginWithEmailForm() { if (doesExist) { await signIn("email", { email: identifier, - callbackUrl: searchParams?.get("callbackUrl") ?? undefined, + redirectTo: searchParams?.get("callbackUrl") ?? undefined, redirect: false, }); // redirect to verify page with callbackUrl router.push( `/login/verify?callbackUrl=${encodeURIComponent( - searchParams?.get("callbackUrl") ?? "", + searchParams?.get("callbac`kUrl") ?? "", )}`, ); } else { diff --git a/apps/web/src/app/[locale]/(auth)/login/components/login-with-oidc.tsx b/apps/web/src/app/[locale]/(auth)/login/components/login-with-oidc.tsx index 67f9bd237f5..083124ddb78 100644 --- a/apps/web/src/app/[locale]/(auth)/login/components/login-with-oidc.tsx +++ b/apps/web/src/app/[locale]/(auth)/login/components/login-with-oidc.tsx @@ -15,7 +15,7 @@ export async function LoginWithOIDC({