diff --git a/docs/data/toolpad/core/components/sign-in-page/MagicLinkAlertSignInPage.js b/docs/data/toolpad/core/components/sign-in-page/MagicLinkAlertSignInPage.js new file mode 100644 index 00000000000..384a818f17f --- /dev/null +++ b/docs/data/toolpad/core/components/sign-in-page/MagicLinkAlertSignInPage.js @@ -0,0 +1,31 @@ +import * as React from 'react'; +import { SignInPage } from '@toolpad/core/SignInPage'; +import { AppProvider } from '@toolpad/core/AppProvider'; +import { useTheme } from '@mui/material/styles'; + +const providers = [{ id: 'nodemailer', name: 'Email' }]; + +const signIn = async (provider) => { + const promise = new Promise((resolve) => { + setTimeout(() => { + console.log(`Sign in with ${provider.id}`); + // preview-start + resolve({ + success: 'Check your email for a verification link.', + }); + // preview-end + }, 500); + }); + return promise; +}; + +export default function MagicLinkAlertSignInPage() { + const theme = useTheme(); + return ( + // preview-start + + + + // preview-end + ); +} diff --git a/docs/data/toolpad/core/components/sign-in-page/MagicLinkAlertSignInPage.tsx b/docs/data/toolpad/core/components/sign-in-page/MagicLinkAlertSignInPage.tsx new file mode 100644 index 00000000000..760101ef225 --- /dev/null +++ b/docs/data/toolpad/core/components/sign-in-page/MagicLinkAlertSignInPage.tsx @@ -0,0 +1,40 @@ +import * as React from 'react'; +import { + AuthProvider, + SignInPage, + SupportedAuthProvider, + AuthResponse, +} from '@toolpad/core/SignInPage'; +import { AppProvider } from '@toolpad/core/AppProvider'; +import { useTheme } from '@mui/material/styles'; + +const providers: { id: SupportedAuthProvider; name: string }[] = [ + { id: 'nodemailer', name: 'Email' }, +]; + +const signIn: (provider: AuthProvider) => Promise = async ( + provider, +) => { + const promise = new Promise((resolve) => { + setTimeout(() => { + console.log(`Sign in with ${provider.id}`); + // preview-start + resolve({ + success: 'Check your email for a verification link.', + }); + // preview-end + }, 500); + }); + return promise; +}; + +export default function MagicLinkAlertSignInPage() { + const theme = useTheme(); + return ( + // preview-start + + + + // preview-end + ); +} diff --git a/docs/data/toolpad/core/components/sign-in-page/MagicLinkAlertSignInPage.tsx.preview b/docs/data/toolpad/core/components/sign-in-page/MagicLinkAlertSignInPage.tsx.preview new file mode 100644 index 00000000000..074047956fc --- /dev/null +++ b/docs/data/toolpad/core/components/sign-in-page/MagicLinkAlertSignInPage.tsx.preview @@ -0,0 +1,9 @@ +resolve({ + success: 'Check your email for a verification link.', +}); + +// ... + + + + \ No newline at end of file diff --git a/docs/data/toolpad/core/components/sign-in-page/MagicLinkSignInPage.js b/docs/data/toolpad/core/components/sign-in-page/MagicLinkSignInPage.js new file mode 100644 index 00000000000..6a7dd19e672 --- /dev/null +++ b/docs/data/toolpad/core/components/sign-in-page/MagicLinkSignInPage.js @@ -0,0 +1,30 @@ +import * as React from 'react'; +import { SignInPage } from '@toolpad/core/SignInPage'; +import { AppProvider } from '@toolpad/core/AppProvider'; +import { useTheme } from '@mui/material/styles'; + +// preview-start +const providers = [{ id: 'nodemailer', name: 'Email' }]; + +// preview-end + +const signIn = async (provider) => { + const promise = new Promise((resolve) => { + setTimeout(() => { + console.log(`Sign in with ${provider.id}`); + resolve(); + }, 500); + }); + return promise; +}; + +export default function MagicLinkSignInPage() { + const theme = useTheme(); + return ( + // preview-start + + + + // preview-end + ); +} diff --git a/docs/data/toolpad/core/components/sign-in-page/MagicLinkSignInPage.tsx b/docs/data/toolpad/core/components/sign-in-page/MagicLinkSignInPage.tsx new file mode 100644 index 00000000000..6565c3faca1 --- /dev/null +++ b/docs/data/toolpad/core/components/sign-in-page/MagicLinkSignInPage.tsx @@ -0,0 +1,35 @@ +import * as React from 'react'; +import { + AuthProvider, + SignInPage, + SupportedAuthProvider, +} from '@toolpad/core/SignInPage'; +import { AppProvider } from '@toolpad/core/AppProvider'; +import { useTheme } from '@mui/material/styles'; + +// preview-start +const providers: { id: SupportedAuthProvider; name: string }[] = [ + { id: 'nodemailer', name: 'Email' }, +]; +// preview-end + +const signIn: (provider: AuthProvider) => void = async (provider) => { + const promise = new Promise((resolve) => { + setTimeout(() => { + console.log(`Sign in with ${provider.id}`); + resolve(); + }, 500); + }); + return promise; +}; + +export default function MagicLinkSignInPage() { + const theme = useTheme(); + return ( + // preview-start + + + + // preview-end + ); +} diff --git a/docs/data/toolpad/core/components/sign-in-page/MagicLinkSignInPage.tsx.preview b/docs/data/toolpad/core/components/sign-in-page/MagicLinkSignInPage.tsx.preview new file mode 100644 index 00000000000..4d6d5fff426 --- /dev/null +++ b/docs/data/toolpad/core/components/sign-in-page/MagicLinkSignInPage.tsx.preview @@ -0,0 +1,9 @@ +const providers: { id: SupportedAuthProvider; name: string }[] = [ + { id: 'nodemailer', name: 'Email' }, +]; + +// ... + + + + \ No newline at end of file diff --git a/docs/data/toolpad/core/components/sign-in-page/OAuthSignInPage.js b/docs/data/toolpad/core/components/sign-in-page/OAuthSignInPage.js index 7c68f46bef6..4a2c5fbf1b5 100644 --- a/docs/data/toolpad/core/components/sign-in-page/OAuthSignInPage.js +++ b/docs/data/toolpad/core/components/sign-in-page/OAuthSignInPage.js @@ -2,6 +2,7 @@ import * as React from 'react'; import { AppProvider } from '@toolpad/core/AppProvider'; import { SignInPage } from '@toolpad/core/SignInPage'; import { useTheme } from '@mui/material/styles'; + // preview-start const providers = [ { id: 'github', name: 'GitHub' }, diff --git a/docs/data/toolpad/core/components/sign-in-page/OAuthSignInPage.tsx b/docs/data/toolpad/core/components/sign-in-page/OAuthSignInPage.tsx index 0ce2d8f081a..b32b9ed5750 100644 --- a/docs/data/toolpad/core/components/sign-in-page/OAuthSignInPage.tsx +++ b/docs/data/toolpad/core/components/sign-in-page/OAuthSignInPage.tsx @@ -2,6 +2,7 @@ import * as React from 'react'; import { AppProvider } from '@toolpad/core/AppProvider'; import { SignInPage, type AuthProvider } from '@toolpad/core/SignInPage'; import { useTheme } from '@mui/material/styles'; + // preview-start const providers = [ { id: 'github', name: 'GitHub' }, diff --git a/docs/data/toolpad/core/components/sign-in-page/sign-in-page.md b/docs/data/toolpad/core/components/sign-in-page/sign-in-page.md index cb8e7084712..4899f9708ee 100644 --- a/docs/data/toolpad/core/components/sign-in-page/sign-in-page.md +++ b/docs/data/toolpad/core/components/sign-in-page/sign-in-page.md @@ -22,7 +22,7 @@ The `SignInPage` component can be set up with an OAuth provider by passing in a :::info -The following providers are supported and maintained by default: +The following OAuth providers are supported and maintained by default: - Google - GitHub @@ -44,11 +44,29 @@ The following providers are supported and maintained by default: - Twitch - Discord - Keycloak -- Credentials (username/password) Find details on how to set up each provider in the [Auth.js documentation](https://authjs.dev/getting-started/authentication/oauth). ::: +## Magic Link + +The `SignIn` page component supports magic links. To enable this, you have to set up a provider such as Auth.js NodeMailer. See more details in the Auth.js docs on [database setup for email](https://authjs.dev/getting-started/authentication/email) and [Nodemailer configuration](https://authjs.dev/getting-started/providers/nodemailer/). + +To render a magic link form, pass in a provider with `nodemailer` as the `id` property. + +{{"demo": "MagicLinkSignInPage.js", "iframe": true, "height": 400}} + +### Alerts + +The `SignInPage` component can display a success alert if the email is sent successfully. You can enable this by passing a `success` property in the +response object of the `signIn` prop. + +{{"demo": "MagicLinkAlertSignInPage.js", "iframe": true, "height": 400}} + +:::info +Check out the complete [Next.js Auth.js Magic Link example](https://github.com/mui/mui-toolpad/tree/master/examples/core-auth-nextjs-email/) example for a working implementation of a magic link sign-in page with Auth.js, Nodemailer, Prisma and PostgreSQL. +::: + ## Passkey The `SignInPage` component can be set up to use [Passkeys](https://passkeys.dev) by passing in a provider with `passkey` as the `id`: @@ -64,7 +82,7 @@ The [Toolpad Core Passkey example app](https://github.com/mui/mui-toolpad/tree/m ## Credentials :::warning -It is recommended to use the OAuth or Passkey provider for more robust maintenance, support, and security. +The Credentials provider is not the most secure way to authenticate users. It's recommended to use any of the other providers for a more robust solution. ::: To render a username password form, pass in a provider with `credentials` as the `id` property. The `signIn` function accepts a `formData` parameter in this case. @@ -231,4 +249,4 @@ The `SignInPage` component has versions with different layouts for authenticatio ## 🚧 Other authentication Flows -The `SignInPage` will be accompanied by other components to allow users to sign up, verify emails and reset passwords. This is in progress. +Besides the `SignInPage` , the team is planning work on several other components that enable new workflows such as [sign up](https://github.com/mui/toolpad/issues/4068) and [password reset](https://github.com/mui/toolpad/issues/4265). diff --git a/docs/pages/toolpad/core/api/sign-in-page.json b/docs/pages/toolpad/core/api/sign-in-page.json index f1f2069f591..aa3d713e361 100644 --- a/docs/pages/toolpad/core/api/sign-in-page.json +++ b/docs/pages/toolpad/core/api/sign-in-page.json @@ -3,7 +3,7 @@ "providers": { "type": { "name": "arrayOf", - "description": "Array<{ id: 'apple'
| 'auth0'
| 'cognito'
| 'credentials'
| 'discord'
| 'facebook'
| 'fusionauth'
| 'github'
| 'gitlab'
| 'google'
| 'instagram'
| 'keycloak'
| 'line'
| 'linkedin'
| 'microsoft-entra-id'
| 'okta'
| 'passkey'
| 'slack'
| 'spotify'
| 'tiktok'
| 'twitch'
| 'twitter', name: string }>" + "description": "Array<{ id: 'apple'
| 'auth0'
| 'cognito'
| 'credentials'
| 'discord'
| 'facebook'
| 'fusionauth'
| 'github'
| 'gitlab'
| 'google'
| 'instagram'
| 'keycloak'
| 'line'
| 'linkedin'
| 'microsoft-entra-id'
| 'nodemailer'
| 'okta'
| 'passkey'
| 'slack'
| 'spotify'
| 'tiktok'
| 'twitch'
| 'twitter', name: string }>" }, "default": "[]" }, diff --git a/examples/core-auth-nextjs-email/.eslintrc.json b/examples/core-auth-nextjs-email/.eslintrc.json new file mode 100644 index 00000000000..bffb357a712 --- /dev/null +++ b/examples/core-auth-nextjs-email/.eslintrc.json @@ -0,0 +1,3 @@ +{ + "extends": "next/core-web-vitals" +} diff --git a/examples/core-auth-nextjs-email/.gitignore b/examples/core-auth-nextjs-email/.gitignore new file mode 100644 index 00000000000..68c5d18f00d --- /dev/null +++ b/examples/core-auth-nextjs-email/.gitignore @@ -0,0 +1,5 @@ +node_modules/ +/test-results/ +/playwright-report/ +/blob-report/ +/playwright/.cache/ diff --git a/examples/core-auth-nextjs-email/Dockerfile b/examples/core-auth-nextjs-email/Dockerfile new file mode 100644 index 00000000000..a79b85781fd --- /dev/null +++ b/examples/core-auth-nextjs-email/Dockerfile @@ -0,0 +1,45 @@ +# Use Node.js 20 Alpine as the base image +FROM node:20-alpine AS builder + +# Set working directory +WORKDIR /app + +# Copy package.json and package-lock.json +COPY package*.json ./ + +# Install dependencies +RUN npm install + +# Copy all files +COPY . . + +# Build the Next.js app +RUN npm run build + +# Start a new stage for a smaller final image +FROM node:20-alpine AS runner + +WORKDIR /app + +# Copy built assets from the builder stage +COPY --from=builder /app/.next ./.next +COPY --from=builder /app/node_modules ./node_modules +COPY --from=builder /app/package.json ./package.json +COPY --from=builder /app/src/prisma ./prisma + +# Set environment variables +ENV NODE_ENV production +ENV PORT 3000 + +# Copy the entrypoint script +COPY entrypoint.sh /entrypoint.sh +RUN chmod +x /entrypoint.sh + +# Set the entrypoint +ENTRYPOINT ["/entrypoint.sh"] + +# Expose the port Next.js runs on +EXPOSE 3000 + +# Run the Next.js app +CMD ["npm", "start"] diff --git a/examples/core-auth-nextjs-email/README.md b/examples/core-auth-nextjs-email/README.md new file mode 100644 index 00000000000..1e5da9d8c8d --- /dev/null +++ b/examples/core-auth-nextjs-email/README.md @@ -0,0 +1,34 @@ +# Toolpad Core Next.js App Router app with email provider + +This is a [Next.js](https://nextjs.org/) project bootstrapped with [`create-next-app`](https://github.com/vercel/next.js/tree/canary/packages/create-next-app). + +## Getting Started + +First, run the development server: + +```bash +npm run dev +# or +yarn dev +# or +pnpm dev +# or +bun dev +``` + +Open [http://localhost:3000](http://localhost:3000) with your browser to see the result. + +## Learn More + +To learn more about Next.js, take a look at the following resources: + +- [Next.js Documentation](https://nextjs.org/docs) - learn about Next.js features and API. +- [Learn Next.js](https://nextjs.org/learn) - an interactive Next.js tutorial. + +You can check out [the Next.js GitHub repository](https://github.com/vercel/next.js/) - your feedback and contributions are welcome! + +## Deploy on Vercel + +The easiest way to deploy your Next.js app is to use the [Vercel Platform](https://vercel.com/new?utm_medium=default-template&filter=next.js&utm_source=create-next-app&utm_campaign=create-next-app-readme) from the creators of Next.js. + +Check out our [Next.js deployment documentation](https://nextjs.org/docs/deployment) for more details. diff --git a/examples/core-auth-nextjs-email/docker-compose.yml b/examples/core-auth-nextjs-email/docker-compose.yml new file mode 100644 index 00000000000..29b8ce23cc1 --- /dev/null +++ b/examples/core-auth-nextjs-email/docker-compose.yml @@ -0,0 +1,52 @@ +version: '3.8' + +services: + app: + build: + context: . + dockerfile: Dockerfile + ports: + - '3000:3000' + environment: + - DATABASE_URL=${DATABASE_URL} + - AUTH_URL=${AUTH_URL} + - AUTH_TRUST_HOST=true + - NODE_ENV=production + - GITHUB_CLIENT_ID=${GITHUB_CLIENT_ID} + - GITHUB_CLIENT_SECRET=${GITHUB_CLIENT_SECRET} + - AUTH_SECRET=${AUTH_SECRET} + - EMAIL_SERVER_HOST=${EMAIL_SERVER_HOST} + - EMAIL_SERVER_PORT=${EMAIL_SERVER_PORT} + - EMAIL_SERVER_USER=${EMAIL_SERVER_USER} + - EMAIL_SERVER_PASSWORD=${EMAIL_SERVER_PASSWORD} + - EMAIL_FROM=${EMAIL_FROM} + depends_on: + db: + condition: service_healthy + networks: + - app-network + + db: + image: postgres:13 + environment: + - POSTGRES_USER=${POSTGRES_USER} + - POSTGRES_PASSWORD=${POSTGRES_PASSWORD} + - POSTGRES_DB=${POSTGRES_DB} + ports: + - '${POSTGRES_PORT}:5432' + volumes: + - postgres_data:/var/lib/postgresql/data + healthcheck: + test: ['CMD-SHELL', 'pg_isready -U ${POSTGRES_USER} -d ${POSTGRES_DB}'] + interval: 5s + timeout: 5s + retries: 5 + networks: + - app-network + +volumes: + postgres_data: + +networks: + app-network: + driver: bridge diff --git a/examples/core-auth-nextjs-email/entrypoint.sh b/examples/core-auth-nextjs-email/entrypoint.sh new file mode 100644 index 00000000000..3b20a40d898 --- /dev/null +++ b/examples/core-auth-nextjs-email/entrypoint.sh @@ -0,0 +1,14 @@ +#!/bin/sh + +# Wait for the database to be ready +until nc -z db 5432; do + echo "Waiting for database to be ready..." + sleep 2 +done + +# Run migrations +npx prisma migrate deploy +npx prisma generate + +# Start the application +exec npm start \ No newline at end of file diff --git a/examples/core-auth-nextjs-email/next-env.d.ts b/examples/core-auth-nextjs-email/next-env.d.ts new file mode 100644 index 00000000000..40c3d68096c --- /dev/null +++ b/examples/core-auth-nextjs-email/next-env.d.ts @@ -0,0 +1,5 @@ +/// +/// + +// NOTE: This file should not be edited +// see https://nextjs.org/docs/app/building-your-application/configuring/typescript for more information. diff --git a/examples/core-auth-nextjs-email/next.config.mjs b/examples/core-auth-nextjs-email/next.config.mjs new file mode 100644 index 00000000000..4678774e6d6 --- /dev/null +++ b/examples/core-auth-nextjs-email/next.config.mjs @@ -0,0 +1,4 @@ +/** @type {import('next').NextConfig} */ +const nextConfig = {}; + +export default nextConfig; diff --git a/examples/core-auth-nextjs-email/package.json b/examples/core-auth-nextjs-email/package.json new file mode 100644 index 00000000000..384cc7c8a26 --- /dev/null +++ b/examples/core-auth-nextjs-email/package.json @@ -0,0 +1,33 @@ +{ + "name": "playground-nextjs-email", + "version": "0.5.2", + "private": true, + "scripts": { + "dev": "next dev", + "lint": "next lint" + }, + "dependencies": { + "@emotion/react": "^11", + "@emotion/styled": "^11", + "@mui/icons-material": "^6", + "@mui/material": "^6", + "@mui/material-nextjs": "^6", + "@toolpad/core": "latest", + "@prisma/client": "^5", + "prisma": "^5", + "@auth/prisma-adapter": "^2", + "next": "^14", + "next-auth": "5.0.0-beta.20", + "nodemailer": "^6", + "react": "^18", + "react-dom": "^18" + }, + "devDependencies": { + "typescript": "^5", + "@types/node": "^20", + "@types/react": "^18", + "@types/react-dom": "^18", + "eslint": "^8", + "eslint-config-next": "^14" + } +} diff --git a/examples/core-auth-nextjs-email/src/app/(dashboard)/layout.tsx b/examples/core-auth-nextjs-email/src/app/(dashboard)/layout.tsx new file mode 100644 index 00000000000..e481f628f1c --- /dev/null +++ b/examples/core-auth-nextjs-email/src/app/(dashboard)/layout.tsx @@ -0,0 +1,11 @@ +import * as React from 'react'; +import { DashboardLayout } from '@toolpad/core/DashboardLayout'; +import { PageContainer } from '@toolpad/core/PageContainer'; + +export default function DashboardPagesLayout(props: { children: React.ReactNode }) { + return ( + + {props.children} + + ); +} diff --git a/examples/core-auth-nextjs-email/src/app/(dashboard)/orders/page.tsx b/examples/core-auth-nextjs-email/src/app/(dashboard)/orders/page.tsx new file mode 100644 index 00000000000..467e27cc6b3 --- /dev/null +++ b/examples/core-auth-nextjs-email/src/app/(dashboard)/orders/page.tsx @@ -0,0 +1,19 @@ +import * as React from 'react'; +import Typography from '@mui/material/Typography'; +import { redirect } from 'next/navigation'; +import { headers } from 'next/headers'; +import { auth } from '../../../auth'; + +export default async function OrdersPage() { + const session = await auth(); + const currentUrl = headers().get('referer') || 'http://localhost:3000'; + + if (!session) { + // Get the current URL to redirect to signIn with `callbackUrl` + const redirectUrl = new URL('/auth/signin', currentUrl); + redirectUrl.searchParams.set('callbackUrl', currentUrl); + + redirect(redirectUrl.toString()); + } + return Welcome to the Toolpad orders!; +} diff --git a/examples/core-auth-nextjs-email/src/app/(dashboard)/page.tsx b/examples/core-auth-nextjs-email/src/app/(dashboard)/page.tsx new file mode 100644 index 00000000000..5e100e4e4f6 --- /dev/null +++ b/examples/core-auth-nextjs-email/src/app/(dashboard)/page.tsx @@ -0,0 +1,23 @@ +import * as React from 'react'; +import Typography from '@mui/material/Typography'; +import { redirect } from 'next/navigation'; +import { headers } from 'next/headers'; +import { auth } from '../../auth'; + +export default async function HomePage() { + const session = await auth(); + const currentUrl = headers().get('referer') || 'http://localhost:3000'; + + if (!session) { + // Get the current URL to redirect to signIn with `callbackUrl` + const redirectUrl = new URL('/auth/signin', currentUrl); + redirectUrl.searchParams.set('callbackUrl', currentUrl); + + redirect(redirectUrl.toString()); + } + return ( + + Welcome to Toolpad, {session?.user?.name || session?.user?.email || 'User'}! + + ); +} diff --git a/examples/core-auth-nextjs-email/src/app/api/auth/[...nextauth]/route.ts b/examples/core-auth-nextjs-email/src/app/api/auth/[...nextauth]/route.ts new file mode 100644 index 00000000000..ca225652075 --- /dev/null +++ b/examples/core-auth-nextjs-email/src/app/api/auth/[...nextauth]/route.ts @@ -0,0 +1,3 @@ +import { handlers } from '../../../../auth'; + +export const { GET, POST } = handlers; diff --git a/examples/core-auth-nextjs-email/src/app/auth/signin/actions.ts b/examples/core-auth-nextjs-email/src/app/auth/signin/actions.ts new file mode 100644 index 00000000000..7672bd32886 --- /dev/null +++ b/examples/core-auth-nextjs-email/src/app/auth/signin/actions.ts @@ -0,0 +1,47 @@ +'use server'; +import { AuthError } from 'next-auth'; +import type { AuthProvider } from '@toolpad/core'; +import { signIn as signInAction } from '../../../auth'; + +async function signIn(provider: AuthProvider, formData: FormData, callbackUrl?: string) { + try { + return await signInAction(provider.id, { + ...(formData && { email: formData.get('email'), password: formData.get('password') }), + redirectTo: callbackUrl ?? '/', + }); + } catch (error) { + // The desired flow for successful sign in in all cases + // and unsuccessful sign in for OAuth providers will cause a `redirect`, + // and `redirect` is a throwing function, so we need to re-throw + // to allow the redirect to happen + // Source: https://github.com/vercel/next.js/issues/49298#issuecomment-1542055642 + // Detect a `NEXT_REDIRECT` error and re-throw it + if (error instanceof Error && error.message === 'NEXT_REDIRECT') { + // For the nodemailer provider, we want to return a success message + // instead of redirecting to a `verify-request` page + if (provider.id === 'nodemailer' && (error as any).digest?.includes('verify-request')) { + return { + success: 'Check your email for a verification link.', + }; + } + throw error; + } + // Handle Auth.js errors + if (error instanceof AuthError) { + return { + error: + error.type === 'CredentialsSignin' + ? 'Invalid credentials.' + : 'An error with Auth.js occurred.', + type: error.type, + }; + } + // An error boundary must exist to handle unknown errors + return { + error: 'Something went wrong.', + type: 'UnknownError', + }; + } +} + +export default signIn; diff --git a/examples/core-auth-nextjs-email/src/app/auth/signin/page.tsx b/examples/core-auth-nextjs-email/src/app/auth/signin/page.tsx new file mode 100644 index 00000000000..e1838f9807e --- /dev/null +++ b/examples/core-auth-nextjs-email/src/app/auth/signin/page.tsx @@ -0,0 +1,12 @@ +import * as React from 'react'; +import { SignInPage } from '@toolpad/core/SignInPage'; +import { providerMap } from '../../../auth'; +import signIn from './actions'; + +export default function SignIn() { + return ( + + ; + + ); +} diff --git a/examples/core-auth-nextjs-email/src/app/layout.tsx b/examples/core-auth-nextjs-email/src/app/layout.tsx new file mode 100644 index 00000000000..96a76406401 --- /dev/null +++ b/examples/core-auth-nextjs-email/src/app/layout.tsx @@ -0,0 +1,57 @@ +import * as React from 'react'; +import { AppProvider } from '@toolpad/core/nextjs'; +import { AppRouterCacheProvider } from '@mui/material-nextjs/v14-appRouter'; +import DashboardIcon from '@mui/icons-material/Dashboard'; +import ShoppingCartIcon from '@mui/icons-material/ShoppingCart'; +import type { Navigation } from '@toolpad/core'; +import { SessionProvider, signIn, signOut } from 'next-auth/react'; +import { auth } from '../auth'; + +const NAVIGATION: Navigation = [ + { + kind: 'header', + title: 'Main items', + }, + { + segment: '', + title: 'Dashboard', + icon: , + }, + { + segment: 'orders', + title: 'Orders', + icon: , + }, +]; + +const BRANDING = { + title: 'My Toolpad Core Next.js App', +}; + +const AUTHENTICATION = { + signIn, + signOut, +}; + +export default async function RootLayout(props: { children: React.ReactNode }) { + const session = await auth(); + + return ( + + + + + + {props.children} + + + + + + ); +} diff --git a/examples/core-auth-nextjs-email/src/app/public/layout.tsx b/examples/core-auth-nextjs-email/src/app/public/layout.tsx new file mode 100644 index 00000000000..5dee163b753 --- /dev/null +++ b/examples/core-auth-nextjs-email/src/app/public/layout.tsx @@ -0,0 +1,11 @@ +import * as React from 'react'; +import { DashboardLayout } from '@toolpad/core/DashboardLayout'; +import { PageContainer } from '@toolpad/core/PageContainer'; + +export default async function DashboardPagesLayout(props: { children: React.ReactNode }) { + return ( + + {props.children} + + ); +} diff --git a/examples/core-auth-nextjs-email/src/app/public/page.tsx b/examples/core-auth-nextjs-email/src/app/public/page.tsx new file mode 100644 index 00000000000..9eb1ae52478 --- /dev/null +++ b/examples/core-auth-nextjs-email/src/app/public/page.tsx @@ -0,0 +1,6 @@ +import * as React from 'react'; +import Typography from '@mui/material/Typography'; + +export default async function HomePage() { + return Public page; +} diff --git a/examples/core-auth-nextjs-email/src/auth.ts b/examples/core-auth-nextjs-email/src/auth.ts new file mode 100644 index 00000000000..af08addd63f --- /dev/null +++ b/examples/core-auth-nextjs-email/src/auth.ts @@ -0,0 +1,66 @@ +import NextAuth from 'next-auth'; +import GitHub from 'next-auth/providers/github'; + +import Nodemailer from 'next-auth/providers/nodemailer'; +import { PrismaAdapter } from '@auth/prisma-adapter'; +import type { Provider } from 'next-auth/providers'; +import { prisma } from './prisma'; + +const providers: Provider[] = [ + GitHub({ + clientId: process.env.GITHUB_CLIENT_ID, + clientSecret: process.env.GITHUB_CLIENT_SECRET, + }), + Nodemailer({ + server: { + host: process.env.EMAIL_SERVER_HOST, + port: process.env.EMAIL_SERVER_PORT, + auth: { + user: process.env.EMAIL_SERVER_USER, + pass: process.env.EMAIL_SERVER_PASSWORD, + }, + secure: true, + }, + from: process.env.EMAIL_FROM, + }), +]; + +export const providerMap = providers.map((provider) => { + if (typeof provider === 'function') { + const providerData = provider(); + return { + id: providerData.id, + name: providerData.name, + }; + } + return { id: provider.id, name: provider.name }; +}); + +if (!process.env.GITHUB_CLIENT_ID) { + console.warn('Missing environment variable "GITHUB_CLIENT_ID"'); +} +if (!process.env.GITHUB_CLIENT_SECRET) { + console.warn('Missing environment variable "GITHUB_CLIENT_ID"'); +} + +export const { handlers, auth, signIn, signOut } = NextAuth({ + providers, + adapter: PrismaAdapter(prisma), + session: { strategy: 'jwt' }, + secret: process.env.AUTH_SECRET, + pages: { + signIn: '/auth/signin', + }, + callbacks: { + authorized({ auth: session, request: { nextUrl } }) { + const isLoggedIn = !!session?.user; + const isPublicPage = nextUrl.pathname.startsWith('/public'); + + if (isPublicPage || isLoggedIn) { + return true; + } + + return false; // Redirect unauthenticated users to login page + }, + }, +}); diff --git a/examples/core-auth-nextjs-email/src/prisma.ts b/examples/core-auth-nextjs-email/src/prisma.ts new file mode 100644 index 00000000000..ee2f97d763c --- /dev/null +++ b/examples/core-auth-nextjs-email/src/prisma.ts @@ -0,0 +1,9 @@ +import { PrismaClient } from '@prisma/client'; + +const globalForPrisma = globalThis as unknown as { prisma: PrismaClient }; + +export const prisma = globalForPrisma.prisma || new PrismaClient(); + +if (process.env.NODE_ENV !== 'production') { + globalForPrisma.prisma = prisma; +} diff --git a/examples/core-auth-nextjs-email/src/prisma/migrations/20240913094851_init/migration.sql b/examples/core-auth-nextjs-email/src/prisma/migrations/20240913094851_init/migration.sql new file mode 100644 index 00000000000..9a11d832759 --- /dev/null +++ b/examples/core-auth-nextjs-email/src/prisma/migrations/20240913094851_init/migration.sql @@ -0,0 +1,61 @@ +-- CreateTable +CREATE TABLE "User" ( + "id" TEXT NOT NULL, + "name" TEXT, + "email" TEXT NOT NULL, + "emailVerified" TIMESTAMP(3), + "image" TEXT, + "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updatedAt" TIMESTAMP(3) NOT NULL, + + CONSTRAINT "User_pkey" PRIMARY KEY ("id") +); + +-- CreateTable +CREATE TABLE "Account" ( + "userId" TEXT NOT NULL, + "type" TEXT NOT NULL, + "provider" TEXT NOT NULL, + "providerAccountId" TEXT NOT NULL, + "refresh_token" TEXT, + "access_token" TEXT, + "expires_at" INTEGER, + "token_type" TEXT, + "scope" TEXT, + "id_token" TEXT, + "session_state" TEXT, + "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updatedAt" TIMESTAMP(3) NOT NULL, + + CONSTRAINT "Account_pkey" PRIMARY KEY ("provider","providerAccountId") +); + +-- CreateTable +CREATE TABLE "Session" ( + "sessionToken" TEXT NOT NULL, + "userId" TEXT NOT NULL, + "expires" TIMESTAMP(3) NOT NULL, + "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updatedAt" TIMESTAMP(3) NOT NULL +); + +-- CreateTable +CREATE TABLE "VerificationToken" ( + "identifier" TEXT NOT NULL, + "token" TEXT NOT NULL, + "expires" TIMESTAMP(3) NOT NULL, + + CONSTRAINT "VerificationToken_pkey" PRIMARY KEY ("identifier","token") +); + +-- CreateIndex +CREATE UNIQUE INDEX "User_email_key" ON "User"("email"); + +-- CreateIndex +CREATE UNIQUE INDEX "Session_sessionToken_key" ON "Session"("sessionToken"); + +-- AddForeignKey +ALTER TABLE "Account" ADD CONSTRAINT "Account_userId_fkey" FOREIGN KEY ("userId") REFERENCES "User"("id") ON DELETE CASCADE ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "Session" ADD CONSTRAINT "Session_userId_fkey" FOREIGN KEY ("userId") REFERENCES "User"("id") ON DELETE CASCADE ON UPDATE CASCADE; diff --git a/examples/core-auth-nextjs-email/src/prisma/migrations/migration_lock.toml b/examples/core-auth-nextjs-email/src/prisma/migrations/migration_lock.toml new file mode 100644 index 00000000000..fbffa92c2bb --- /dev/null +++ b/examples/core-auth-nextjs-email/src/prisma/migrations/migration_lock.toml @@ -0,0 +1,3 @@ +# Please do not edit this file manually +# It should be added in your version-control system (i.e. Git) +provider = "postgresql" \ No newline at end of file diff --git a/examples/core-auth-nextjs-email/src/prisma/schema.prisma b/examples/core-auth-nextjs-email/src/prisma/schema.prisma new file mode 100644 index 00000000000..9b35bb87717 --- /dev/null +++ b/examples/core-auth-nextjs-email/src/prisma/schema.prisma @@ -0,0 +1,60 @@ +datasource db { + provider = "postgresql" + url = env("DATABASE_URL") +} + +generator client { + provider = "prisma-client-js" +} + +model User { + id String @id @default(cuid()) + name String? + email String @unique + emailVerified DateTime? + image String? + accounts Account[] + sessions Session[] + + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt +} + +model Account { + userId String + type String + provider String + providerAccountId String + refresh_token String? + access_token String? + expires_at Int? + token_type String? + scope String? + id_token String? + session_state String? + + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt + + user User @relation(fields: [userId], references: [id], onDelete: Cascade) + + @@id([provider, providerAccountId]) +} + +model Session { + sessionToken String @unique + userId String + expires DateTime + user User @relation(fields: [userId], references: [id], onDelete: Cascade) + + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt +} + +model VerificationToken { + identifier String + token String + expires DateTime + + @@id([identifier, token]) +} \ No newline at end of file diff --git a/examples/core-auth-nextjs-email/tsconfig.json b/examples/core-auth-nextjs-email/tsconfig.json new file mode 100644 index 00000000000..bb5584ed1a4 --- /dev/null +++ b/examples/core-auth-nextjs-email/tsconfig.json @@ -0,0 +1,23 @@ +{ + "compilerOptions": { + "lib": ["dom", "dom.iterable", "esnext"], + "allowJs": true, + "skipLibCheck": true, + "strict": true, + "noEmit": true, + "esModuleInterop": true, + "module": "esnext", + "moduleResolution": "bundler", + "resolveJsonModule": true, + "isolatedModules": true, + "jsx": "preserve", + "incremental": true, + "plugins": [ + { + "name": "next" + } + ] + }, + "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"], + "exclude": ["node_modules"] +} diff --git a/examples/core-auth-nextjs-pages/src/auth.ts b/examples/core-auth-nextjs-pages/src/auth.ts index a377904d3e0..5d3c3452b05 100644 --- a/examples/core-auth-nextjs-pages/src/auth.ts +++ b/examples/core-auth-nextjs-pages/src/auth.ts @@ -26,23 +26,11 @@ const providers: Provider[] = [ }), ]; -const missingVars: string[] = []; - if (!process.env.GITHUB_CLIENT_ID) { - missingVars.push('GITHUB_CLIENT_ID'); + console.warn('Missing environment variable "GITHUB_CLIENT_ID"'); } if (!process.env.GITHUB_CLIENT_SECRET) { - missingVars.push('GITHUB_CLIENT_SECRET'); -} - -if (missingVars.length > 0) { - const message = `Authentication is configured but the following environment variables are missing: ${missingVars.join(', ')}`; - - if (process.env.NODE_ENV === 'production') { - throw new Error(message); - } else { - console.warn(message); - } + console.warn('Missing environment variable "GITHUB_CLIENT_SECRET"'); } export const providerMap = providers.map((provider) => { diff --git a/examples/core-auth-nextjs/src/auth.ts b/examples/core-auth-nextjs/src/auth.ts index 74dd44ac883..c9874f0a52e 100644 --- a/examples/core-auth-nextjs/src/auth.ts +++ b/examples/core-auth-nextjs/src/auth.ts @@ -26,26 +26,11 @@ const providers: Provider[] = [ }), ]; -const missingVars: string[] = []; - if (!process.env.GITHUB_CLIENT_ID) { - missingVars.push('GITHUB_CLIENT_ID'); + console.warn('Missing environment variable "GITHUB_CLIENT_ID"'); } if (!process.env.GITHUB_CLIENT_SECRET) { - missingVars.push('GITHUB_CLIENT_SECRET'); -} - -if (missingVars.length > 0) { - const baseMessage = - 'Authentication is configured but the following environment variables are missing:'; - - if (process.env.NODE_ENV === 'production') { - throw new Error(`error - ${baseMessage} ${missingVars.join(', ')}`); - } else { - console.warn( - `\u001b[33mwarn\u001b[0m - ${baseMessage} \u001b[31m${missingVars.join(', ')}\u001b[0m`, - ); - } + console.warn('Missing environment variable "GITHUB_CLIENT_SECRET"'); } export const providerMap = providers.map((provider) => { diff --git a/packages/create-toolpad-app/src/templates/auth/auth.ts b/packages/create-toolpad-app/src/templates/auth/auth.ts index 89678eaf477..668f65f5eaf 100644 --- a/packages/create-toolpad-app/src/templates/auth/auth.ts +++ b/packages/create-toolpad-app/src/templates/auth/auth.ts @@ -23,40 +23,40 @@ const CredentialsProviderTemplate = `Credentials({ const oAuthProviderTemplate = (provider: SupportedAuthProvider) => ` ${kebabToPascal(provider)}({ clientId: process.env.${kebabToConstant(provider)}_CLIENT_ID, - clientSecret: process.env.${kebabToConstant(provider)}_CLIENT_SECRET,${requiresIssuer(provider) ? `\nissuer: process.env.${kebabToConstant(provider)}_ISSUER,\n` : ''}${requiresTenantId(provider) ? `tenantId: process.env.${kebabToConstant(provider)}_TENANT_ID,` : ''} + clientSecret: process.env.${kebabToConstant(provider)}_CLIENT_SECRET,${requiresIssuer(provider) ? `\n\t\tissuer: process.env.${kebabToConstant(provider)}_ISSUER,` : ''}${requiresTenantId(provider) ? `\n\t\ttenantId: process.env.${kebabToConstant(provider)}_TENANT_ID,` : ''} }),`; -const checkEnvironmentVariables = ( - providers: SupportedAuthProvider[] | undefined, -) => `const missingVars: string[] = []; - -const isMissing = (name: string, envVar: string | undefined) => { - if (!envVar) { - missingVars.push(name); - } -}; - -${providers +const checkEnvironmentVariables = (providers: SupportedAuthProvider[] | undefined) => `${providers ?.filter((p) => p !== 'credentials') .map( (provider) => - `isMissing('${kebabToConstant(provider)}_CLIENT_ID', process.env.${kebabToConstant(provider)}_CLIENT_ID);\nisMissing('${kebabToConstant(provider)}_CLIENT_SECRET', process.env.${kebabToConstant(provider)}_CLIENT_SECRET)`, + `if(!process.env.${kebabToConstant(provider)}_CLIENT_ID) { + console.warn('Missing environment variable "${kebabToConstant(provider)}_CLIENT_ID"'); +} +if(!process.env.${kebabToConstant(provider)}_CLIENT_SECRET) { + console.warn('Missing environment variable "${kebabToConstant(provider)}_CLIENT_SECRET"'); +}${ + requiresTenantId(provider) + ? ` +if(!process.env.${kebabToConstant(provider)}_TENANT_ID) { + console.warn('Missing environment variable "${kebabToConstant(provider)}_TENANT_ID"'); +}` + : '' + }${ + requiresIssuer(provider) + ? ` +if(!process.env.${kebabToConstant(provider)}_ISSUER) { + console.warn('Missing environment variable "${kebabToConstant(provider)}_ISSUER"'); +}` + : '' + }`, ) .join('\n')} - -if (missingVars.length > 0) { - const baseMessage = 'Authentication is configured but the following environment variables are missing:'; - - if (process.env.NODE_ENV === 'production') { - console.warn(\`warn: \${baseMessage} \${missingVars.join(', ')}\`); - } else { - console.warn(\`\\u001b[33mwarn:\\u001b[0m \${baseMessage} \\u001b[31m\${missingVars.join(', ')}\\u001b[0m\`); - } -}`; +`; const auth: Template = (options) => { const providers = options.authProviders; - return `import NextAuth from 'next-auth';\nimport { AuthProvider, SupportedAuthProvider } from '@toolpad/core/SignInPage';\n${providers + return `import NextAuth from 'next-auth';\n${providers ?.map( (provider) => `import ${kebabToPascal(provider)} from 'next-auth/providers/${provider.toLowerCase()}';`, @@ -79,7 +79,7 @@ ${checkEnvironmentVariables(providers)} export const providerMap = providers.map((provider) => { if (typeof provider === 'function') { const providerData = provider(); - return { id: providerData.id, name: providerData.name }; + return { id: providerData.id, name: providerData.name }; } return { id: provider.id, name: provider.name }; }); diff --git a/packages/toolpad-core/src/SignInPage/SignInPage.test.tsx b/packages/toolpad-core/src/SignInPage/SignInPage.test.tsx index 0d1c82ee7b6..e96ccd03421 100644 --- a/packages/toolpad-core/src/SignInPage/SignInPage.test.tsx +++ b/packages/toolpad-core/src/SignInPage/SignInPage.test.tsx @@ -46,6 +46,21 @@ describe('SignInPage', () => { expect(signIn.mock.calls[0][1].get('password')).toBe('thepassword'); }); + test('renders nodemailer provider', async () => { + const signIn = vi.fn(); + render(); + + const emailField = screen.getByRole('textbox', { name: 'Email Address' }); + const signInButton = screen.getByRole('button', { name: 'Sign in with Email' }); + + await userEvent.type(emailField, 'john@example.com'); + await userEvent.click(signInButton); + + expect(signIn).toHaveBeenCalled(); + expect(signIn.mock.calls[0][0]).toHaveProperty('id', 'nodemailer'); + expect(signIn.mock.calls[0][1].get('email')).toBe('john@example.com'); + }); + test('renders passkey sign-in option when available', async () => { const signIn = vi.fn(); diff --git a/packages/toolpad-core/src/SignInPage/SignInPage.tsx b/packages/toolpad-core/src/SignInPage/SignInPage.tsx index c30ef91955b..99661325bb4 100644 --- a/packages/toolpad-core/src/SignInPage/SignInPage.tsx +++ b/packages/toolpad-core/src/SignInPage/SignInPage.tsx @@ -62,7 +62,11 @@ type SupportedOAuthProvider = | 'fusionauth' | 'microsoft-entra-id'; -export type SupportedAuthProvider = SupportedOAuthProvider | 'credentials' | 'passkey'; +export type SupportedAuthProvider = + | SupportedOAuthProvider + | 'credentials' + | 'passkey' + | 'nodemailer'; const IconProviderMap = new Map([ ['github', ], @@ -113,10 +117,17 @@ export interface AuthResponse { */ error?: string; /** - * The type of error that occurred. + * The type of error if the sign-in failed. * @default '' */ type?: string; + /** + * The success notification if the sign-in was successful. + * @default '' + * Only used for magic link sign-in. + * @example 'Check your email for a magic link.' + */ + success?: string; } export interface SignInPageSlots { @@ -206,14 +217,17 @@ function SignInPage(props: SignInPageProps) { const router = React.useContext(RouterContext); const passkeyProvider = providers?.find((provider) => provider.id === 'passkey'); const credentialsProvider = providers?.find((provider) => provider.id === 'credentials'); - const [{ loading, selectedProviderId, error }, setFormStatus] = React.useState<{ + const emailProvider = providers?.find((provider) => provider.id === 'nodemailer'); + const [{ loading, selectedProviderId, error, success }, setFormStatus] = React.useState<{ loading: boolean; selectedProviderId?: SupportedAuthProvider; error?: string; + success?: string; }>({ selectedProviderId: undefined, loading: false, error: '', + success: '', }); const callbackUrl = router?.searchParams.get('callbackUrl') ?? '/'; @@ -242,13 +256,20 @@ function SignInPage(props: SignInPageProps) { Welcome, please sign in to continue - + - {error && selectedProviderId !== 'credentials' && selectedProviderId !== 'passkey' ? ( + {error && + selectedProviderId !== 'credentials' && + selectedProviderId !== 'passkey' && + selectedProviderId !== 'nodemailer' ? ( {error} ) : null} {Object.values(providers ?? {}).map((provider) => { - if (provider.id === 'credentials' || provider.id === 'passkey') { + if ( + provider.id === 'credentials' || + provider.id === 'passkey' || + provider.id === 'nodemailer' + ) { return null; } return ( @@ -498,6 +519,89 @@ function SignInPage(props: SignInPageProps) { ) : null} + + {emailProvider ? ( + + {singleProvider ? null : or} + {error && selectedProviderId === 'nodemailer' ? ( + + {error} + + ) : null} + {success && selectedProviderId === 'nodemailer' ? ( + + {success} + + ) : null} + { + event.preventDefault(); + setFormStatus({ + error: '', + selectedProviderId: emailProvider.id, + loading: true, + }); + const formData = new FormData(event.currentTarget); + const emailResponse = await signIn?.(emailProvider, formData, callbackUrl); + setFormStatus((prev) => ({ + ...prev, + loading: false, + error: emailResponse?.error, + success: emailResponse?.success, + })); + }} + > + + {slots?.submitButton ? ( + + ) : ( + + Sign in with {emailProvider.name || 'Email'} + + )} + + + ) : null} @@ -531,6 +635,7 @@ SignInPage.propTypes /* remove-proptypes */ = { 'line', 'linkedin', 'microsoft-entra-id', + 'nodemailer', 'okta', 'passkey', 'slack', diff --git a/playground/nextjs/src/auth.ts b/playground/nextjs/src/auth.ts index 323ac97b646..d801fb81b70 100644 --- a/playground/nextjs/src/auth.ts +++ b/playground/nextjs/src/auth.ts @@ -62,6 +62,7 @@ export const { handlers, auth, signIn, signOut } = NextAuth({ pages: { signIn: '/auth/signin', }, + debug: true, callbacks: { authorized({ auth: session, request: { nextUrl } }) { const isLoggedIn = !!session?.user; diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 03aff3c5020..43730c26db3 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -195,7 +195,7 @@ importers: version: 7.37.1(eslint@8.57.1) eslint-plugin-react-compiler: specifier: latest - version: 19.0.0-beta-9ee70a1-20241017(eslint@8.57.1) + version: 19.0.0-beta-8a03594-20241020(eslint@8.57.1) eslint-plugin-react-hooks: specifier: 4.6.2 version: 4.6.2(eslint@8.57.1) @@ -683,7 +683,7 @@ importers: dependencies: '@auth/core': specifier: 0.37.0 - version: 0.37.0(@simplewebauthn/browser@9.0.1)(@simplewebauthn/server@9.0.3(encoding@0.1.13)) + version: 0.37.0(@simplewebauthn/browser@9.0.1)(@simplewebauthn/server@9.0.3(encoding@0.1.13))(nodemailer@6.9.15) '@emotion/cache': specifier: 11.13.1 version: 11.13.1 @@ -1099,7 +1099,7 @@ importers: dependencies: '@auth/core': specifier: 0.37.0 - version: 0.37.0(@simplewebauthn/browser@9.0.1)(@simplewebauthn/server@9.0.3(encoding@0.1.13)) + version: 0.37.0(@simplewebauthn/browser@9.0.1)(@simplewebauthn/server@9.0.3(encoding@0.1.13))(nodemailer@6.9.15) '@mui/material': specifier: 6.1.4 version: 6.1.4(@emotion/react@11.13.3(@types/react@18.3.11)(react@18.3.1))(@emotion/styled@11.13.0(@emotion/react@11.13.3(@types/react@18.3.11)(react@18.3.1))(@types/react@18.3.11)(react@18.3.1))(@types/react@18.3.11)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) @@ -1239,7 +1239,7 @@ importers: version: 14.2.15(@babel/core@7.25.8)(@playwright/test@1.47.2)(babel-plugin-macros@3.1.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) next-auth: specifier: 5.0.0-beta.22 - version: 5.0.0-beta.22(@simplewebauthn/browser@9.0.1)(@simplewebauthn/server@9.0.3(encoding@0.1.13))(next@14.2.15(@babel/core@7.25.8)(@playwright/test@1.47.2)(babel-plugin-macros@3.1.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(react@18.3.1) + version: 5.0.0-beta.22(@simplewebauthn/browser@9.0.1)(@simplewebauthn/server@9.0.3(encoding@0.1.13))(next@14.2.15(@babel/core@7.25.8)(@playwright/test@1.47.2)(babel-plugin-macros@3.1.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(nodemailer@6.9.15)(react@18.3.1) react: specifier: 18.3.1 version: 18.3.1 @@ -1281,7 +1281,7 @@ importers: version: 14.2.15(@babel/core@7.25.8)(@playwright/test@1.47.2)(babel-plugin-macros@3.1.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) next-auth: specifier: 5.0.0-beta.22 - version: 5.0.0-beta.22(@simplewebauthn/browser@9.0.1)(@simplewebauthn/server@9.0.3(encoding@0.1.13))(next@14.2.15(@babel/core@7.25.8)(@playwright/test@1.47.2)(babel-plugin-macros@3.1.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(react@18.3.1) + version: 5.0.0-beta.22(@simplewebauthn/browser@9.0.1)(@simplewebauthn/server@9.0.3(encoding@0.1.13))(next@14.2.15(@babel/core@7.25.8)(@playwright/test@1.47.2)(babel-plugin-macros@3.1.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(nodemailer@6.9.15)(react@18.3.1) react: specifier: 18.3.1 version: 18.3.1 @@ -2052,7 +2052,7 @@ packages: resolution: {integrity: sha512-GjV0/mUEEXpi1U5ZgDprMRRgajGMRW3G5FjMr5KLKD8nT2fTG8+h/klV3+6Dm5739QE+K5+2e91qFKAYI3pmRg==} engines: {node: '>=6.9.0'} peerDependencies: - '@babel/core': ^7.25.8 + '@babel/core': ^7.0.0-0 '@babel/preset-typescript@7.25.7': resolution: {integrity: sha512-rkkpaXJZOFN45Fb+Gki0c+KMIglk4+zZXOoMJuyEK8y8Kkc8Jd3BDmP7qPsz0zQMJj+UD7EprF+AqAXcILnexw==} @@ -2122,7 +2122,7 @@ packages: '@docsearch/react@3.6.2': resolution: {integrity: sha512-rtZce46OOkVflCQH71IdbXSFK+S8iJZlUF56XBW5rIgx/eG5qoomC7Ag3anZson1bBac/JFQn7XOBfved/IMRA==} peerDependencies: - '@types/react': ^18.3.11 + '@types/react': '>= 16.8.0 < 19.0.0' react: '>= 16.8.0 < 19.0.0' react-dom: '>= 16.8.0 < 19.0.0' search-insights: '>= 1 < 3' @@ -2944,7 +2944,7 @@ packages: resolution: {integrity: sha512-LQZ2907rPMut/2Lq6qSnyP+nqOHLO3buMv91m7SdLpqp/lXU5+8vUXcf5oOwTNis6hfSvYGSQJ493Q00OzxDmQ==} engines: {node: '>=14.0.0'} peerDependencies: - '@types/react': ^18.3.11 + '@types/react': ^17.0.0 || ^18.0.0 react: ^17.0.0 || ^18.0.0 react-dom: ^17.0.0 || ^18.0.0 peerDependenciesMeta: @@ -6058,8 +6058,8 @@ packages: peerDependencies: eslint: '>=7.0.0' - eslint-plugin-react-compiler@19.0.0-beta-9ee70a1-20241017: - resolution: {integrity: sha512-GdJHMa9Wqfc/JPiv4WW5JjQsuSISdBo7oM/6IjRO8uxaZncDrKK/RyFqbPvgEiNFzDcX8ZZvR8dgfSGvxh2Qpw==} + eslint-plugin-react-compiler@19.0.0-beta-8a03594-20241020: + resolution: {integrity: sha512-bYg1COih1s3r14IV/AKdQs/SN7CQmNI0ZaMtPdgZ6gp1S1Q/KGP9P43w7R6dHJ4wYpuMBvekNJHQdVu+x6UM+A==} engines: {node: ^14.17.0 || ^16.0.0 || >= 18.0.0} peerDependencies: eslint: '>=7' @@ -8051,6 +8051,10 @@ packages: node-releases@2.0.18: resolution: {integrity: sha512-d9VeXT4SJ7ZeOqGX6R5EM022wpL+eWPooLI+5UpWn2jCT1aosUQEhQP214x33Wkwx3JQMvIm+tIoVOdodFS40g==} + nodemailer@6.9.15: + resolution: {integrity: sha512-AHf04ySLC6CIfuRtRiEYtGEXgRfa6INgWGluDhnxTZhHSKvrBu7lc1VVchQ0d8nPc4cFaZoPq8vkyNoZr0TpGQ==} + engines: {node: '>=6.0.0'} + nopt@7.2.1: resolution: {integrity: sha512-taM24ViiimT/XntxbPyJQzCG+p4EKOpgD3mxFwW38mGjVUrfERQOeY4EDHjdnptttfHuHQXFx+lTP08Q+mLa/w==} engines: {node: ^14.17.0 || ^16.13.0 || >=18.0.0} @@ -10600,7 +10604,7 @@ snapshots: '@argos-ci/util@2.1.1': {} - '@auth/core@0.35.3(@simplewebauthn/browser@9.0.1)(@simplewebauthn/server@9.0.3(encoding@0.1.13))': + '@auth/core@0.35.3(@simplewebauthn/browser@9.0.1)(@simplewebauthn/server@9.0.3(encoding@0.1.13))(nodemailer@6.9.15)': dependencies: '@panva/hkdf': 1.2.1 '@types/cookie': 0.6.0 @@ -10612,8 +10616,9 @@ snapshots: optionalDependencies: '@simplewebauthn/browser': 9.0.1 '@simplewebauthn/server': 9.0.3(encoding@0.1.13) + nodemailer: 6.9.15 - '@auth/core@0.37.0(@simplewebauthn/browser@9.0.1)(@simplewebauthn/server@9.0.3(encoding@0.1.13))': + '@auth/core@0.37.0(@simplewebauthn/browser@9.0.1)(@simplewebauthn/server@9.0.3(encoding@0.1.13))(nodemailer@6.9.15)': dependencies: '@panva/hkdf': 1.2.1 '@types/cookie': 0.6.0 @@ -10625,6 +10630,7 @@ snapshots: optionalDependencies: '@simplewebauthn/browser': 9.0.1 '@simplewebauthn/server': 9.0.3(encoding@0.1.13) + nodemailer: 6.9.15 '@babel/cli@7.25.7(@babel/core@7.25.8)': dependencies: @@ -15931,7 +15937,7 @@ snapshots: globals: 13.24.0 rambda: 7.5.0 - eslint-plugin-react-compiler@19.0.0-beta-9ee70a1-20241017(eslint@8.57.1): + eslint-plugin-react-compiler@19.0.0-beta-8a03594-20241020(eslint@8.57.1): dependencies: '@babel/core': 7.25.8 '@babel/parser': 7.25.8 @@ -18254,14 +18260,15 @@ snapshots: nested-error-stacks@2.1.1: {} - next-auth@5.0.0-beta.22(@simplewebauthn/browser@9.0.1)(@simplewebauthn/server@9.0.3(encoding@0.1.13))(next@14.2.15(@babel/core@7.25.8)(@playwright/test@1.47.2)(babel-plugin-macros@3.1.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(react@18.3.1): + next-auth@5.0.0-beta.22(@simplewebauthn/browser@9.0.1)(@simplewebauthn/server@9.0.3(encoding@0.1.13))(next@14.2.15(@babel/core@7.25.8)(@playwright/test@1.47.2)(babel-plugin-macros@3.1.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(nodemailer@6.9.15)(react@18.3.1): dependencies: - '@auth/core': 0.35.3(@simplewebauthn/browser@9.0.1)(@simplewebauthn/server@9.0.3(encoding@0.1.13)) + '@auth/core': 0.35.3(@simplewebauthn/browser@9.0.1)(@simplewebauthn/server@9.0.3(encoding@0.1.13))(nodemailer@6.9.15) next: 14.2.15(@babel/core@7.25.8)(@playwright/test@1.47.2)(babel-plugin-macros@3.1.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) react: 18.3.1 optionalDependencies: '@simplewebauthn/browser': 9.0.1 '@simplewebauthn/server': 9.0.3(encoding@0.1.13) + nodemailer: 6.9.15 next-router-mock@0.9.13(next@14.2.15(@babel/core@7.25.8)(@playwright/test@1.47.2)(babel-plugin-macros@3.1.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(react@18.3.1): dependencies: @@ -18349,6 +18356,9 @@ snapshots: node-releases@2.0.18: {} + nodemailer@6.9.15: + optional: true + nopt@7.2.1: dependencies: abbrev: 2.0.0