From 31ea4766485a3e0e5fe8ed5f1750f894596a3643 Mon Sep 17 00:00:00 2001 From: Bharat Kashyap Date: Fri, 13 Sep 2024 18:57:48 +0530 Subject: [PATCH 01/25] feat: Add `nodemailer` provider to `SignInPage` --- .../sign-in-page/MagicLinkAlertSignInPage.js | 30 +++++ .../sign-in-page/MagicLinkAlertSignInPage.tsx | 40 +++++++ .../MagicLinkAlertSignInPage.tsx.preview | 9 ++ .../sign-in-page/MagicLinkSignInPage.js | 29 +++++ .../sign-in-page/MagicLinkSignInPage.tsx | 35 ++++++ .../MagicLinkSignInPage.tsx.preview | 9 ++ .../sign-in-page/OAuthSignInPage.js | 1 + .../sign-in-page/OAuthSignInPage.tsx | 1 + .../components/sign-in-page/sign-in-page.md | 77 ++++++++++++- docs/next-env.d.ts | 2 +- docs/pages/toolpad/core/api/sign-in-page.json | 2 +- .../core-auth-nextjs-email/.eslintrc.json | 3 + examples/core-auth-nextjs-email/.gitignore | 5 + examples/core-auth-nextjs-email/README.md | 34 ++++++ examples/core-auth-nextjs-email/next-env.d.ts | 5 + .../core-auth-nextjs-email/next.config.mjs | 4 + examples/core-auth-nextjs-email/package.json | 34 ++++++ .../src/app/(dashboard)/layout.tsx | 11 ++ .../src/app/(dashboard)/orders/page.tsx | 19 +++ .../src/app/(dashboard)/page.tsx | 23 ++++ .../src/app/api/auth/[...nextauth]/route.ts | 3 + .../src/app/auth/signin/actions.ts | 48 ++++++++ .../src/app/auth/signin/page.tsx | 12 ++ .../core-auth-nextjs-email/src/app/layout.tsx | 57 +++++++++ .../src/app/public/layout.tsx | 11 ++ .../src/app/public/page.tsx | 6 + examples/core-auth-nextjs-email/src/auth.ts | 99 ++++++++++++++++ examples/core-auth-nextjs-email/src/prisma.ts | 7 ++ .../20240913094851_init/migration.sql | 61 ++++++++++ .../src/prisma/migrations/migration_lock.toml | 3 + .../src/prisma/schema.prisma | 60 ++++++++++ examples/core-auth-nextjs-email/tsconfig.json | 23 ++++ .../README.md | 2 +- examples/core-auth-nextjs-pages/README.md | 2 +- examples/core-auth-nextjs/README.md | 2 +- .../src/templates/auth/auth.ts | 5 +- .../src/SignInPage/SignInPage.tsx | 109 ++++++++++++++++-- playground/nextjs/src/auth.ts | 1 + pnpm-lock.yaml | 32 ++--- 39 files changed, 883 insertions(+), 33 deletions(-) create mode 100644 docs/data/toolpad/core/components/sign-in-page/MagicLinkAlertSignInPage.js create mode 100644 docs/data/toolpad/core/components/sign-in-page/MagicLinkAlertSignInPage.tsx create mode 100644 docs/data/toolpad/core/components/sign-in-page/MagicLinkAlertSignInPage.tsx.preview create mode 100644 docs/data/toolpad/core/components/sign-in-page/MagicLinkSignInPage.js create mode 100644 docs/data/toolpad/core/components/sign-in-page/MagicLinkSignInPage.tsx create mode 100644 docs/data/toolpad/core/components/sign-in-page/MagicLinkSignInPage.tsx.preview create mode 100644 examples/core-auth-nextjs-email/.eslintrc.json create mode 100644 examples/core-auth-nextjs-email/.gitignore create mode 100644 examples/core-auth-nextjs-email/README.md create mode 100644 examples/core-auth-nextjs-email/next-env.d.ts create mode 100644 examples/core-auth-nextjs-email/next.config.mjs create mode 100644 examples/core-auth-nextjs-email/package.json create mode 100644 examples/core-auth-nextjs-email/src/app/(dashboard)/layout.tsx create mode 100644 examples/core-auth-nextjs-email/src/app/(dashboard)/orders/page.tsx create mode 100644 examples/core-auth-nextjs-email/src/app/(dashboard)/page.tsx create mode 100644 examples/core-auth-nextjs-email/src/app/api/auth/[...nextauth]/route.ts create mode 100644 examples/core-auth-nextjs-email/src/app/auth/signin/actions.ts create mode 100644 examples/core-auth-nextjs-email/src/app/auth/signin/page.tsx create mode 100644 examples/core-auth-nextjs-email/src/app/layout.tsx create mode 100644 examples/core-auth-nextjs-email/src/app/public/layout.tsx create mode 100644 examples/core-auth-nextjs-email/src/app/public/page.tsx create mode 100644 examples/core-auth-nextjs-email/src/auth.ts create mode 100644 examples/core-auth-nextjs-email/src/prisma.ts create mode 100644 examples/core-auth-nextjs-email/src/prisma/migrations/20240913094851_init/migration.sql create mode 100644 examples/core-auth-nextjs-email/src/prisma/migrations/migration_lock.toml create mode 100644 examples/core-auth-nextjs-email/src/prisma/schema.prisma create mode 100644 examples/core-auth-nextjs-email/tsconfig.json 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..2e4b01c98f2 --- /dev/null +++ b/docs/data/toolpad/core/components/sign-in-page/MagicLinkAlertSignInPage.js @@ -0,0 +1,30 @@ +import * as React from 'react'; +import { AppProvider, SignInPage } from '@toolpad/core'; +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..159ab538973 --- /dev/null +++ b/docs/data/toolpad/core/components/sign-in-page/MagicLinkAlertSignInPage.tsx @@ -0,0 +1,40 @@ +import * as React from 'react'; +import { + AuthProvider, + AppProvider, + SignInPage, + AuthResponse, + SupportedAuthProvider, +} from '@toolpad/core'; +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..f8813edf7ad --- /dev/null +++ b/docs/data/toolpad/core/components/sign-in-page/MagicLinkSignInPage.js @@ -0,0 +1,29 @@ +import * as React from 'react'; +import { AppProvider, SignInPage } from '@toolpad/core'; +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..916391315ca --- /dev/null +++ b/docs/data/toolpad/core/components/sign-in-page/MagicLinkSignInPage.tsx @@ -0,0 +1,35 @@ +import * as React from 'react'; +import { + AuthProvider, + AppProvider, + SignInPage, + SupportedAuthProvider, +} from '@toolpad/core'; +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 2b83616fe94..ce6538a5282 100644 --- a/docs/data/toolpad/core/components/sign-in-page/OAuthSignInPage.js +++ b/docs/data/toolpad/core/components/sign-in-page/OAuthSignInPage.js @@ -1,6 +1,7 @@ import * as React from 'react'; import { AppProvider, SignInPage } from '@toolpad/core'; 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 5f670f3d696..30fb8d131a1 100644 --- a/docs/data/toolpad/core/components/sign-in-page/OAuthSignInPage.tsx +++ b/docs/data/toolpad/core/components/sign-in-page/OAuthSignInPage.tsx @@ -1,6 +1,7 @@ import * as React from 'react'; import { AuthProvider, AppProvider, SignInPage } from '@toolpad/core'; 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 f36cd7dfadf..f284b97fe48 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 @@ -18,7 +18,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 @@ -40,15 +40,86 @@ 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 + +You can use the `SignInPage` component to quickly set up authentication via one-time verification links. It uses Nodemailer under the hood to send the verification link to the user's email address. See more details in the [Auth.js docs](https://authjs.dev/getting-started/providers/nodemailer/) on configuration and customization. + +To render a magic link form, pass in a provider with `nodemailer` as the `id` property. + +{{"demo": "MagicLinkSignInPage.js", "iframe": true, "height": 500}} + +### 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": 500}} + +To get the magic link working, you need to add the following code to your custom sign-in page: + +```tsx title="app/auth/signin/page.tsx" +import * as React from 'react'; +import { SignInPage } from '@toolpad/core/SignInPage'; +import { AuthError } from 'next-auth'; +import type { AuthProvider } from '@toolpad/core'; +import { signIn, providerMap } from '../../../auth'; + +export default function SignIn() { + return ( + + ; + + ); +} +``` + +:::info +Check out the complete [Next.js App Router Nodemailer 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. +::: + ## Credentials :::warning -It is recommended to use the OAuth provider for more robust maintenance, support, and security. +The Credentials provider is not the most secure way to authenticate users. We recommend using 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. diff --git a/docs/next-env.d.ts b/docs/next-env.d.ts index 4f11a03dc6c..a4a7b3f5cfa 100644 --- a/docs/next-env.d.ts +++ b/docs/next-env.d.ts @@ -2,4 +2,4 @@ /// // NOTE: This file should not be edited -// see https://nextjs.org/docs/basic-features/typescript for more information. +// see https://nextjs.org/docs/pages/building-your-application/configuring/typescript for more information. diff --git a/docs/pages/toolpad/core/api/sign-in-page.json b/docs/pages/toolpad/core/api/sign-in-page.json index 5b65fa61ec8..67beafa3790 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'
| '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'
| '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/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/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..1d726a2a125 --- /dev/null +++ b/examples/core-auth-nextjs-email/package.json @@ -0,0 +1,34 @@ +{ + "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/lab": "^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..2158cef03a9 --- /dev/null +++ b/examples/core-auth-nextjs-email/src/app/auth/signin/actions.ts @@ -0,0 +1,48 @@ +'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 + // if the error is a 'verify-request' error, 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..bb87d8a1217 --- /dev/null +++ b/examples/core-auth-nextjs-email/src/auth.ts @@ -0,0 +1,99 @@ +import NextAuth from 'next-auth'; +import { AuthProvider, SupportedAuthProvider } from '@toolpad/core'; +import GitHub from 'next-auth/providers/github'; +// import Credentials from 'next-auth/providers/credentials'; + +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, + }), + // Credentials({ + // credentials: { + // email: { label: 'Email Address', type: 'email' }, + // password: { label: 'Password', type: 'password' }, + // }, + // authorize(c) { + // if (c.password !== 'password') { + // return null; + // } + // return { + // id: 'test', + // name: 'Test User', + // email: String(c.email), + // }; + // }, + // }), + 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 as SupportedAuthProvider, + name: providerData.name, + } satisfies AuthProvider; + } + return { id: provider.id as SupportedAuthProvider, name: provider.name } satisfies AuthProvider; +}); + +const missingVars: string[] = []; + +if (!process.env.GITHUB_CLIENT_ID) { + missingVars.push('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`, + ); + } +} + +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..5aa98b4dc15 --- /dev/null +++ b/examples/core-auth-nextjs-email/src/prisma.ts @@ -0,0 +1,7 @@ +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-nextauth-4/README.md b/examples/core-auth-nextjs-pages-nextauth-4/README.md index 713d86babaf..52855b68196 100644 --- a/examples/core-auth-nextjs-pages-nextauth-4/README.md +++ b/examples/core-auth-nextjs-pages-nextauth-4/README.md @@ -1,4 +1,4 @@ -# Toolpad Core Playground - Next.js Pages Router with Next Auth 4 +# Toolpad Core Next.js Pages Router App with GitHub provider and NextAuth 4 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). diff --git a/examples/core-auth-nextjs-pages/README.md b/examples/core-auth-nextjs-pages/README.md index 18729facd52..a1fd2197650 100644 --- a/examples/core-auth-nextjs-pages/README.md +++ b/examples/core-auth-nextjs-pages/README.md @@ -1,4 +1,4 @@ -# Toolpad Core Playground - Next.js Pages Router +# Toolpad Core Next.js Pages Router App with GitHub 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). diff --git a/examples/core-auth-nextjs/README.md b/examples/core-auth-nextjs/README.md index 38b913c6863..4cdb95b3dc5 100644 --- a/examples/core-auth-nextjs/README.md +++ b/examples/core-auth-nextjs/README.md @@ -1,4 +1,4 @@ -# Toolpad Core Playground - Next.js App Router +# Toolpad Core Next.js App Router app with GitHub 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). diff --git a/packages/create-toolpad-app/src/templates/auth/auth.ts b/packages/create-toolpad-app/src/templates/auth/auth.ts index 6b8b6e8eaab..aec659d54a6 100644 --- a/packages/create-toolpad-app/src/templates/auth/auth.ts +++ b/packages/create-toolpad-app/src/templates/auth/auth.ts @@ -59,6 +59,7 @@ const auth: Template = (options) => { const providers = options.authProviders; return `import NextAuth from 'next-auth'; + import { AuthProvider, SupportedAuthProvider } from '@toolpad/core'; ${providers ?.map( (provider) => @@ -83,9 +84,9 @@ ${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 as SupportedAuthProvider, name: providerData.name } satisfies AuthProvider; } - return { id: provider.id, name: provider.name }; + return { id: provider.id as SupportedAuthProvider, name: provider.name } satisfies AuthProvider; }); export const { handlers, auth, signIn, signOut } = NextAuth({ diff --git a/packages/toolpad-core/src/SignInPage/SignInPage.tsx b/packages/toolpad-core/src/SignInPage/SignInPage.tsx index 1f35c8f3e79..aa46f52e9ba 100644 --- a/packages/toolpad-core/src/SignInPage/SignInPage.tsx +++ b/packages/toolpad-core/src/SignInPage/SignInPage.tsx @@ -60,7 +60,7 @@ type SupportedOAuthProvider = | 'fusionauth' | 'microsoft-entra-id'; -export type SupportedAuthProvider = SupportedOAuthProvider | 'credentials'; +export type SupportedAuthProvider = SupportedOAuthProvider | 'credentials' | 'nodemailer'; const IconProviderMap = new Map([ ['github', ], @@ -110,10 +110,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 { @@ -202,14 +209,17 @@ function SignInPage(props: SignInPageProps) { const docs = React.useContext(DocsContext); const router = React.useContext(RouterContext); 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') ?? '/'; @@ -236,15 +246,16 @@ function SignInPage(props: SignInPageProps) { Sign in {branding?.title ? `to ${branding.title}` : null} - Welcome user, please sign in to continue + Welcome, please sign in to continue - + - {error && selectedProviderId !== 'credentials' ? ( + {error && + !(selectedProviderId === 'credentials' || selectedProviderId === 'nodemailer') ? ( {error} ) : null} {Object.values(providers ?? {}).map((provider) => { - if (provider.id === 'credentials') { + if (provider.id === 'credentials' || provider.id === 'nodemailer') { return null; } return ( @@ -412,6 +423,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 email + + )} + + + ) : null} @@ -445,6 +539,7 @@ SignInPage.propTypes /* remove-proptypes */ = { 'line', 'linkedin', 'microsoft-entra-id', + 'nodemailer', 'okta', 'slack', 'spotify', 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 f508103961d..4600d8563b5 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -195,7 +195,7 @@ importers: version: 7.35.2(eslint@8.57.0) eslint-plugin-react-compiler: specifier: latest - version: 0.0.0-experimental-5c9a529-20240911(eslint@8.57.0) + version: 0.0.0-experimental-5c9a529-20240912(eslint@8.57.0) eslint-plugin-react-hooks: specifier: 4.6.2 version: 4.6.2(eslint@8.57.0) @@ -677,7 +677,7 @@ importers: dependencies: '@auth/core': specifier: 0.34.2 - version: 0.34.2(nodemailer@6.9.14) + version: 0.34.2(nodemailer@6.9.15) '@emotion/cache': specifier: 11.13.1 version: 11.13.1 @@ -1090,7 +1090,7 @@ importers: dependencies: '@auth/core': specifier: 0.34.2 - version: 0.34.2(nodemailer@6.9.14) + version: 0.34.2(nodemailer@6.9.15) '@mui/material': specifier: 6.1.0 version: 6.1.0(@emotion/react@11.13.3(@types/react@18.3.5)(react@18.3.1))(@emotion/styled@11.13.0(@emotion/react@11.13.3(@types/react@18.3.5)(react@18.3.1))(@types/react@18.3.5)(react@18.3.1))(@types/react@18.3.5)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) @@ -1230,7 +1230,7 @@ importers: version: 14.2.8(@babel/core@7.25.2)(@opentelemetry/api@1.9.0)(@playwright/test@1.46.1)(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.20 - version: 5.0.0-beta.20(next@14.2.8(@babel/core@7.25.2)(@opentelemetry/api@1.9.0)(@playwright/test@1.46.1)(babel-plugin-macros@3.1.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(nodemailer@6.9.14)(react@18.3.1) + version: 5.0.0-beta.20(next@14.2.8(@babel/core@7.25.2)(@opentelemetry/api@1.9.0)(@playwright/test@1.46.1)(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 @@ -1272,7 +1272,7 @@ importers: version: 14.2.8(@babel/core@7.25.2)(@opentelemetry/api@1.9.0)(@playwright/test@1.46.1)(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.20 - version: 5.0.0-beta.20(next@14.2.8(@babel/core@7.25.2)(@opentelemetry/api@1.9.0)(@playwright/test@1.46.1)(babel-plugin-macros@3.1.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(nodemailer@6.9.14)(react@18.3.1) + version: 5.0.0-beta.20(next@14.2.8(@babel/core@7.25.2)(@opentelemetry/api@1.9.0)(@playwright/test@1.46.1)(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 @@ -5796,8 +5796,8 @@ packages: peerDependencies: eslint: '>=7.0.0' - eslint-plugin-react-compiler@0.0.0-experimental-5c9a529-20240911: - resolution: {integrity: sha512-QZV0rNLl4bKS3PIyApBeH/3EoMVh9NgpPFJGpKpfG2WWQWF/jkb9buRSxvyNhS0zSJeSR7pTPQFsSKDXNQNMhw==} + eslint-plugin-react-compiler@0.0.0-experimental-5c9a529-20240912: + resolution: {integrity: sha512-/fi7oVQvdq1rlnGOrqXog0jK1YTnn4Z+cgyYCi48hmo1oh3J+3fVsbz6TOGqRICBIgi7RKN+Ikt/hPycYvLr0A==} engines: {node: ^14.17.0 || ^16.0.0 || >= 18.0.0} peerDependencies: eslint: '>=7' @@ -7784,8 +7784,8 @@ packages: node-releases@2.0.18: resolution: {integrity: sha512-d9VeXT4SJ7ZeOqGX6R5EM022wpL+eWPooLI+5UpWn2jCT1aosUQEhQP214x33Wkwx3JQMvIm+tIoVOdodFS40g==} - nodemailer@6.9.14: - resolution: {integrity: sha512-Dobp/ebDKBvz91sbtRKhcznLThrKxKt97GI2FAlAyy+fk19j73Uz3sBXolVtmcXjaorivqsbbbjDY+Jkt4/bQA==} + nodemailer@6.9.15: + resolution: {integrity: sha512-AHf04ySLC6CIfuRtRiEYtGEXgRfa6INgWGluDhnxTZhHSKvrBu7lc1VVchQ0d8nPc4cFaZoPq8vkyNoZr0TpGQ==} engines: {node: '>=6.0.0'} nopt@7.2.1: @@ -10317,7 +10317,7 @@ snapshots: '@argos-ci/util@2.1.1': {} - '@auth/core@0.34.2(nodemailer@6.9.14)': + '@auth/core@0.34.2(nodemailer@6.9.15)': dependencies: '@panva/hkdf': 1.2.0 '@types/cookie': 0.6.0 @@ -10327,7 +10327,7 @@ snapshots: preact: 10.11.3 preact-render-to-string: 5.2.3(preact@10.11.3) optionalDependencies: - nodemailer: 6.9.14 + nodemailer: 6.9.15 '@babel/cli@7.25.6(@babel/core@7.25.2)': dependencies: @@ -15563,7 +15563,7 @@ snapshots: globals: 13.24.0 rambda: 7.5.0 - eslint-plugin-react-compiler@0.0.0-experimental-5c9a529-20240911(eslint@8.57.0): + eslint-plugin-react-compiler@0.0.0-experimental-5c9a529-20240912(eslint@8.57.0): dependencies: '@babel/core': 7.25.2 '@babel/parser': 7.25.6 @@ -17841,13 +17841,13 @@ snapshots: nested-error-stacks@2.1.1: {} - next-auth@5.0.0-beta.20(next@14.2.8(@babel/core@7.25.2)(@opentelemetry/api@1.9.0)(@playwright/test@1.46.1)(babel-plugin-macros@3.1.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(nodemailer@6.9.14)(react@18.3.1): + next-auth@5.0.0-beta.20(next@14.2.8(@babel/core@7.25.2)(@opentelemetry/api@1.9.0)(@playwright/test@1.46.1)(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.34.2(nodemailer@6.9.14) + '@auth/core': 0.34.2(nodemailer@6.9.15) next: 14.2.8(@babel/core@7.25.2)(@opentelemetry/api@1.9.0)(@playwright/test@1.46.1)(babel-plugin-macros@3.1.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) react: 18.3.1 optionalDependencies: - nodemailer: 6.9.14 + nodemailer: 6.9.15 next-router-mock@0.9.13(next@14.2.8(@babel/core@7.25.2)(@opentelemetry/api@1.9.0)(@playwright/test@1.46.1)(babel-plugin-macros@3.1.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(react@18.3.1): dependencies: @@ -17948,7 +17948,7 @@ snapshots: node-releases@2.0.18: {} - nodemailer@6.9.14: + nodemailer@6.9.15: optional: true nopt@7.2.1: From 131b44348a4625f5b94f726010b0d7e7e4b5ec3f Mon Sep 17 00:00:00 2001 From: Bharat Kashyap Date: Fri, 13 Sep 2024 19:34:46 +0530 Subject: [PATCH 02/25] fix: eslint --- examples/core-auth-nextjs-email/src/prisma.ts | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/examples/core-auth-nextjs-email/src/prisma.ts b/examples/core-auth-nextjs-email/src/prisma.ts index 5aa98b4dc15..ee2f97d763c 100644 --- a/examples/core-auth-nextjs-email/src/prisma.ts +++ b/examples/core-auth-nextjs-email/src/prisma.ts @@ -4,4 +4,6 @@ const globalForPrisma = globalThis as unknown as { prisma: PrismaClient }; export const prisma = globalForPrisma.prisma || new PrismaClient(); -if (process.env.NODE_ENV !== 'production') globalForPrisma.prisma = prisma; +if (process.env.NODE_ENV !== 'production') { + globalForPrisma.prisma = prisma; +} From 70ef2aa18b21f493a6aee9f7c8ecedc26171ca9e Mon Sep 17 00:00:00 2001 From: Bharat Kashyap Date: Thu, 10 Oct 2024 14:35:14 +0530 Subject: [PATCH 03/25] fix: Add example link to docs --- docs/data/toolpad/core/components/sign-in-page/sign-in-page.md | 3 +++ 1 file changed, 3 insertions(+) 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 7e768c0413b..1a8b9d6621e 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 @@ -56,6 +56,9 @@ To render a magic link form, pass in a provider with `nodemailer` as the `id` pr {{"demo": "MagicLinkSignInPage.js", "iframe": true, "height": 500}} +:::info +For a complete implementation, refer to the [Toolpad Core with Auth.js and Nodemailer](https://github.com/mui/mui-toolpad/tree/master/examples/core-auth-nextjs-email) example in our GitHub repository. + ### 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 From d390f71206979f7c8e736b1abcd09db7e3f22201 Mon Sep 17 00:00:00 2001 From: Bharat Kashyap Date: Fri, 11 Oct 2024 15:09:34 +0530 Subject: [PATCH 04/25] fix: CI --- pnpm-lock.yaml | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index cfc6657a441..7b2f864761d 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: 0.0.0-experimental-7c1344f-20241009(eslint@8.57.1) + version: 0.0.0-experimental-3c9c57d-20241010(eslint@8.57.1) eslint-plugin-react-hooks: specifier: 4.6.2 version: 4.6.2(eslint@8.57.1) @@ -6040,8 +6040,8 @@ packages: peerDependencies: eslint: '>=7.0.0' - eslint-plugin-react-compiler@0.0.0-experimental-7c1344f-20241009: - resolution: {integrity: sha512-ZhUn6vMU5OJjP1kJhuGA+NDrVPy3pcbok5CblWJaRXH+JzD6bqh5hy1ieep/I92S3ikWTn/5I6KqcXchAYfL4g==} + eslint-plugin-react-compiler@0.0.0-experimental-3c9c57d-20241010: + resolution: {integrity: sha512-F2wtvMUx6p8KrV6Ydyy4XdzYkmAHWGcZear0I3LPYqzBKV+Wx2E/Chrnb4sLq7NFh68BIT+35kIuZhxFKlPzfw==} engines: {node: ^14.17.0 || ^16.0.0 || >= 18.0.0} peerDependencies: eslint: '>=7' @@ -15913,7 +15913,7 @@ snapshots: globals: 13.24.0 rambda: 7.5.0 - eslint-plugin-react-compiler@0.0.0-experimental-7c1344f-20241009(eslint@8.57.1): + eslint-plugin-react-compiler@0.0.0-experimental-3c9c57d-20241010(eslint@8.57.1): dependencies: '@babel/core': 7.25.7 '@babel/parser': 7.25.7 From 55b316f83059a35fb2f3c523f10f3185c09c5f51 Mon Sep 17 00:00:00 2001 From: Bharat Kashyap Date: Fri, 11 Oct 2024 21:43:38 +0530 Subject: [PATCH 05/25] fix: Simplify magic links docs and fix example --- .../components/sign-in-page/sign-in-page.md | 67 +++---------------- examples/core-auth-nextjs-email/package.json | 3 +- 2 files changed, 9 insertions(+), 61 deletions(-) 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 1a8b9d6621e..2689b16b8ca 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 @@ -50,74 +50,23 @@ Find details on how to set up each provider in the [Auth.js documentation](https ## Magic Link -You can use the `SignInPage` component to quickly set up authentication via one-time verification links. It uses Nodemailer under the hood to send the verification link to the user's email address. See more details in the [Auth.js docs](https://authjs.dev/getting-started/providers/nodemailer/) on configuration and customization. +:::warning +To use magic links, you must configure a database and pass in the required environment variables to send emails. 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/). +::: + +You can use the `SignInPage` component to quickly set up authentication via one-time verification links. It uses Nodemailer under the hood to send the verification link to the user's email address. To render a magic link form, pass in a provider with `nodemailer` as the `id` property. -{{"demo": "MagicLinkSignInPage.js", "iframe": true, "height": 500}} +{{"demo": "MagicLinkSignInPage.js", "iframe": true, "height": 400}} -:::info -For a complete implementation, refer to the [Toolpad Core with Auth.js and Nodemailer](https://github.com/mui/mui-toolpad/tree/master/examples/core-auth-nextjs-email) example in our GitHub repository. ### 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": 500}} - -To get the magic link working, you need to add the following code to your custom sign-in page: - -```tsx title="app/auth/signin/page.tsx" -import * as React from 'react'; -import { SignInPage } from '@toolpad/core/SignInPage'; -import { AuthError } from 'next-auth'; -import type { AuthProvider } from '@toolpad/core'; -import { signIn, providerMap } from '../../../auth'; - -export default function SignIn() { - return ( - - ; - - ); -} -``` +{{"demo": "MagicLinkAlertSignInPage.js", "iframe": true, "height": 400}} :::info Check out the complete [Next.js App Router Nodemailer 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. @@ -294,4 +243,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. +The `SignInPage` will be accompanied by other components to allow users to sign up, and reset passwords. This is in progress. diff --git a/examples/core-auth-nextjs-email/package.json b/examples/core-auth-nextjs-email/package.json index 1d726a2a125..062cef0183e 100644 --- a/examples/core-auth-nextjs-email/package.json +++ b/examples/core-auth-nextjs-email/package.json @@ -9,8 +9,7 @@ "dependencies": { "@emotion/react": "^11", "@emotion/styled": "^11", - "@mui/icons-material": "^6", - "@mui/lab": "^6", + "@mui/icons-material": "^6", "@mui/material": "^6", "@mui/material-nextjs": "^6", "@toolpad/core": "latest", From cf82dc85091f0b892395abc8316f16782d8d9c1b Mon Sep 17 00:00:00 2001 From: Bharat Kashyap Date: Fri, 11 Oct 2024 21:47:03 +0530 Subject: [PATCH 06/25] fix: lint --- docs/data/toolpad/core/components/sign-in-page/sign-in-page.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 2689b16b8ca..37a28f41991 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 @@ -54,7 +54,7 @@ Find details on how to set up each provider in the [Auth.js documentation](https To use magic links, you must configure a database and pass in the required environment variables to send emails. 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/). ::: -You can use the `SignInPage` component to quickly set up authentication via one-time verification links. It uses Nodemailer under the hood to send the verification link to the user's email address. +You can use the `SignInPage` component to quickly set up authentication via one-time verification links. It uses Nodemailer under the hood to send the verification link to the user's email address. To render a magic link form, pass in a provider with `nodemailer` as the `id` property. From b1392845311835367c6e55d1c653802a35f0ac23 Mon Sep 17 00:00:00 2001 From: Bharat Kashyap Date: Fri, 11 Oct 2024 21:47:09 +0530 Subject: [PATCH 07/25] fix: Add test --- .../src/SignInPage/SignInPage.test.tsx | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/packages/toolpad-core/src/SignInPage/SignInPage.test.tsx b/packages/toolpad-core/src/SignInPage/SignInPage.test.tsx index f0962f859ba..600436caba8 100644 --- a/packages/toolpad-core/src/SignInPage/SignInPage.test.tsx +++ b/packages/toolpad-core/src/SignInPage/SignInPage.test.tsx @@ -45,4 +45,19 @@ describe('SignInPage', () => { expect(signIn.mock.calls[0][1].get('email')).toBe('john@example.com'); 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'); + }); }); From 2ee806bb739018318434189e1ad738a047b8bf6d Mon Sep 17 00:00:00 2001 From: Bharat Kashyap Date: Fri, 11 Oct 2024 23:56:04 +0530 Subject: [PATCH 08/25] wip: Test this build --- .../src/app/auth/signin/actions.ts | 10 +++++----- .../toolpad-core/src/PageContainer/PageContainer.tsx | 2 +- 2 files changed, 6 insertions(+), 6 deletions(-) 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 index 2158cef03a9..1ee716f53ce 100644 --- a/examples/core-auth-nextjs-email/src/app/auth/signin/actions.ts +++ b/examples/core-auth-nextjs-email/src/app/auth/signin/actions.ts @@ -9,18 +9,18 @@ async function signIn(provider: AuthProvider, formData: FormData, callbackUrl?: ...(formData && { email: formData.get('email'), password: formData.get('password') }), redirectTo: callbackUrl ?? '/', }); - } catch (error) { + } 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') { + if (error instanceof Error && error.message === 'NEXT_REDIRECT') { // For the nodemailer provider, we want to return a success message - // if the error is a 'verify-request' error, instead of redirecting - // to a `verify-request` page - if (provider.id === 'nodemailer' && (error as any).digest?.includes('verify-request')) { + // instead of redirecting to a `verify-request` page + console.log("error", Object.keys(error), error.message, JSON.stringify(error), (error as any).digest) + if (provider.id === 'nodemailer' && (error as any).digest?.includes('verify-request')) { return { success: 'Check your email for a verification link.', }; diff --git a/packages/toolpad-core/src/PageContainer/PageContainer.tsx b/packages/toolpad-core/src/PageContainer/PageContainer.tsx index 4f8d3dd56d4..d9f3e347dff 100644 --- a/packages/toolpad-core/src/PageContainer/PageContainer.tsx +++ b/packages/toolpad-core/src/PageContainer/PageContainer.tsx @@ -1,7 +1,7 @@ 'use client'; import * as React from 'react'; import PropTypes from 'prop-types'; -import warnOnce from '@toolpad/utils/warnOnce'; +// import warnOnce from '@toolpad/utils/warnOnce'; import Breadcrumbs from '@mui/material/Breadcrumbs'; import Container, { ContainerProps } from '@mui/material/Container'; import Link from '@mui/material/Link'; From f80bfab57291de391abdc3636dbae09c1a9e2870 Mon Sep 17 00:00:00 2001 From: Bharat Kashyap Date: Sat, 12 Oct 2024 00:09:18 +0530 Subject: [PATCH 09/25] fix: CI --- packages/toolpad-core/src/PageContainer/PageContainer.tsx | 2 +- pnpm-lock.yaml | 8 ++++---- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/packages/toolpad-core/src/PageContainer/PageContainer.tsx b/packages/toolpad-core/src/PageContainer/PageContainer.tsx index d9f3e347dff..4f8d3dd56d4 100644 --- a/packages/toolpad-core/src/PageContainer/PageContainer.tsx +++ b/packages/toolpad-core/src/PageContainer/PageContainer.tsx @@ -1,7 +1,7 @@ 'use client'; import * as React from 'react'; import PropTypes from 'prop-types'; -// import warnOnce from '@toolpad/utils/warnOnce'; +import warnOnce from '@toolpad/utils/warnOnce'; import Breadcrumbs from '@mui/material/Breadcrumbs'; import Container, { ContainerProps } from '@mui/material/Container'; import Link from '@mui/material/Link'; diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index e05dc2d4d30..ec8b0e7f851 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: 0.0.0-experimental-3c9c57d-20241010(eslint@8.57.1) + version: 0.0.0-experimental-45ae4c3-20241011(eslint@8.57.1) eslint-plugin-react-hooks: specifier: 4.6.2 version: 4.6.2(eslint@8.57.1) @@ -6060,8 +6060,8 @@ packages: peerDependencies: eslint: '>=7.0.0' - eslint-plugin-react-compiler@0.0.0-experimental-3c9c57d-20241010: - resolution: {integrity: sha512-F2wtvMUx6p8KrV6Ydyy4XdzYkmAHWGcZear0I3LPYqzBKV+Wx2E/Chrnb4sLq7NFh68BIT+35kIuZhxFKlPzfw==} + eslint-plugin-react-compiler@0.0.0-experimental-45ae4c3-20241011: + resolution: {integrity: sha512-m+BmeFtVWzrHt87sb5g5jLttHdo9YScPiuiingdEqLYtUv7pdVi6pQgY3nCOI4h09C4wmWS9xzpaVNEgiODOBg==} engines: {node: ^14.17.0 || ^16.0.0 || >= 18.0.0} peerDependencies: eslint: '>=7' @@ -15948,7 +15948,7 @@ snapshots: globals: 13.24.0 rambda: 7.5.0 - eslint-plugin-react-compiler@0.0.0-experimental-3c9c57d-20241010(eslint@8.57.1): + eslint-plugin-react-compiler@0.0.0-experimental-45ae4c3-20241011(eslint@8.57.1): dependencies: '@babel/core': 7.25.7 '@babel/parser': 7.25.7 From dc5ebf4684564b3e973d9fd60bafbf8594db9b5d Mon Sep 17 00:00:00 2001 From: Bharat Kashyap Date: Sat, 12 Oct 2024 13:44:32 +0530 Subject: [PATCH 10/25] fix: CI, test build --- .../core/components/sign-in-page/sign-in-page.md | 1 - .../src/app/auth/signin/actions.ts | 14 ++++++++++---- .../toolpad-core/src/SignInPage/SignInPage.tsx | 2 +- 3 files changed, 11 insertions(+), 6 deletions(-) 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 37a28f41991..cbe8bcf2f81 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 @@ -60,7 +60,6 @@ To render a magic link form, pass in a provider with `nodemailer` as the `id` pr {{"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 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 index 1ee716f53ce..a98dc5f7cbe 100644 --- a/examples/core-auth-nextjs-email/src/app/auth/signin/actions.ts +++ b/examples/core-auth-nextjs-email/src/app/auth/signin/actions.ts @@ -9,18 +9,24 @@ async function signIn(provider: AuthProvider, formData: FormData, callbackUrl?: ...(formData && { email: formData.get('email'), password: formData.get('password') }), redirectTo: callbackUrl ?? '/', }); - } catch (error) { + } 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') { + 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 - console.log("error", Object.keys(error), error.message, JSON.stringify(error), (error as any).digest) - if (provider.id === 'nodemailer' && (error as any).digest?.includes('verify-request')) { + console.log( + 'error', + Object.keys(error), + error.message, + JSON.stringify(error), + (error as any).digest, + ); + if (provider.id === 'nodemailer' && (error as any).digest?.includes('verify-request')) { return { success: 'Check your email for a verification link.', }; diff --git a/packages/toolpad-core/src/SignInPage/SignInPage.tsx b/packages/toolpad-core/src/SignInPage/SignInPage.tsx index aa46f52e9ba..3da6066b14d 100644 --- a/packages/toolpad-core/src/SignInPage/SignInPage.tsx +++ b/packages/toolpad-core/src/SignInPage/SignInPage.tsx @@ -500,7 +500,7 @@ function SignInPage(props: SignInPageProps) { }} {...slotProps?.submitButton} > - Sign in with email + Sign in with {emailProvider.name} )} From c0bf20b29ff142cd9052ce05fda562ccbe9f1a74 Mon Sep 17 00:00:00 2001 From: Bharat Kashyap Date: Sat, 12 Oct 2024 14:42:57 +0530 Subject: [PATCH 11/25] fix: CI --- examples/core-auth-nextjs-email/package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/examples/core-auth-nextjs-email/package.json b/examples/core-auth-nextjs-email/package.json index 062cef0183e..384cc7c8a26 100644 --- a/examples/core-auth-nextjs-email/package.json +++ b/examples/core-auth-nextjs-email/package.json @@ -9,7 +9,7 @@ "dependencies": { "@emotion/react": "^11", "@emotion/styled": "^11", - "@mui/icons-material": "^6", + "@mui/icons-material": "^6", "@mui/material": "^6", "@mui/material-nextjs": "^6", "@toolpad/core": "latest", From 2422a15fba2631e36f80f660374d22e26a2072db Mon Sep 17 00:00:00 2001 From: Bharat Kashyap Date: Sun, 13 Oct 2024 12:55:06 +0530 Subject: [PATCH 12/25] fix: Test docker deployment and add it to example --- examples/core-auth-nextjs-email/Dockerfile | 45 ++++++++++++++++ .../core-auth-nextjs-email/docker-compose.yml | 52 +++++++++++++++++++ examples/core-auth-nextjs-email/entrypoint.sh | 14 +++++ .../src/app/auth/signin/actions.ts | 9 +--- 4 files changed, 112 insertions(+), 8 deletions(-) create mode 100644 examples/core-auth-nextjs-email/Dockerfile create mode 100644 examples/core-auth-nextjs-email/docker-compose.yml create mode 100644 examples/core-auth-nextjs-email/entrypoint.sh 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/docker-compose.yml b/examples/core-auth-nextjs-email/docker-compose.yml new file mode 100644 index 00000000000..239d258b9c1 --- /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/src/app/auth/signin/actions.ts b/examples/core-auth-nextjs-email/src/app/auth/signin/actions.ts index a98dc5f7cbe..20da3c7379a 100644 --- a/examples/core-auth-nextjs-email/src/app/auth/signin/actions.ts +++ b/examples/core-auth-nextjs-email/src/app/auth/signin/actions.ts @@ -18,14 +18,7 @@ async function signIn(provider: AuthProvider, formData: FormData, callbackUrl?: // 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 - console.log( - 'error', - Object.keys(error), - error.message, - JSON.stringify(error), - (error as any).digest, - ); + // 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.', From 7f685213164a23fb2d17f436fb7bbfd8041a2fc1 Mon Sep 17 00:00:00 2001 From: Bharat Kashyap Date: Sun, 13 Oct 2024 15:59:01 +0530 Subject: [PATCH 13/25] fix: CI --- examples/core-auth-nextjs-email/docker-compose.yml | 8 ++++---- .../core-auth-nextjs-email/src/app/auth/signin/actions.ts | 2 +- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/examples/core-auth-nextjs-email/docker-compose.yml b/examples/core-auth-nextjs-email/docker-compose.yml index 239d258b9c1..29b8ce23cc1 100644 --- a/examples/core-auth-nextjs-email/docker-compose.yml +++ b/examples/core-auth-nextjs-email/docker-compose.yml @@ -1,4 +1,4 @@ -version: "3.8" +version: '3.8' services: app: @@ -6,7 +6,7 @@ services: context: . dockerfile: Dockerfile ports: - - "3000:3000" + - '3000:3000' environment: - DATABASE_URL=${DATABASE_URL} - AUTH_URL=${AUTH_URL} @@ -33,11 +33,11 @@ services: - POSTGRES_PASSWORD=${POSTGRES_PASSWORD} - POSTGRES_DB=${POSTGRES_DB} ports: - - "${POSTGRES_PORT}:5432" + - '${POSTGRES_PORT}:5432' volumes: - postgres_data:/var/lib/postgresql/data healthcheck: - test: ["CMD-SHELL", "pg_isready -U ${POSTGRES_USER} -d ${POSTGRES_DB}"] + test: ['CMD-SHELL', 'pg_isready -U ${POSTGRES_USER} -d ${POSTGRES_DB}'] interval: 5s timeout: 5s retries: 5 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 index 20da3c7379a..7672bd32886 100644 --- a/examples/core-auth-nextjs-email/src/app/auth/signin/actions.ts +++ b/examples/core-auth-nextjs-email/src/app/auth/signin/actions.ts @@ -18,7 +18,7 @@ async function signIn(provider: AuthProvider, formData: FormData, callbackUrl?: // 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 + // 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.', From 56b4466cc07825a6b7512f362e64d1cf5175fd31 Mon Sep 17 00:00:00 2001 From: Bharat Kashyap Date: Mon, 14 Oct 2024 12:51:40 +0530 Subject: [PATCH 14/25] fix: Minor tweaks --- docs/data/toolpad/core/components/sign-in-page/sign-in-page.md | 2 +- packages/toolpad-core/src/SignInPage/SignInPage.tsx | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) 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 cbe8bcf2f81..88670725688 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 @@ -68,7 +68,7 @@ response object of the `signIn` prop. {{"demo": "MagicLinkAlertSignInPage.js", "iframe": true, "height": 400}} :::info -Check out the complete [Next.js App Router Nodemailer 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. +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. ::: ## Credentials diff --git a/packages/toolpad-core/src/SignInPage/SignInPage.tsx b/packages/toolpad-core/src/SignInPage/SignInPage.tsx index 3da6066b14d..e434692551c 100644 --- a/packages/toolpad-core/src/SignInPage/SignInPage.tsx +++ b/packages/toolpad-core/src/SignInPage/SignInPage.tsx @@ -500,7 +500,7 @@ function SignInPage(props: SignInPageProps) { }} {...slotProps?.submitButton} > - Sign in with {emailProvider.name} + Sign in with {emailProvider.name || 'Email'} )} From 91d95f0f992a726ea76dd6d1e8c4fefa6cb4acca Mon Sep 17 00:00:00 2001 From: Bharat Kashyap Date: Tue, 15 Oct 2024 14:37:36 +0530 Subject: [PATCH 15/25] fix: Jan review --- .../components/sign-in-page/sign-in-page.md | 6 +-- examples/core-auth-nextjs-email/src/auth.ts | 43 ++------------- examples/core-auth-nextjs-pages/src/auth.ts | 16 +----- examples/core-auth-nextjs/src/auth.ts | 19 +------ .../src/templates/auth/auth.ts | 52 +++++++++---------- 5 files changed, 36 insertions(+), 100 deletions(-) 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 88670725688..7209080ae57 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 @@ -50,11 +50,7 @@ Find details on how to set up each provider in the [Auth.js documentation](https ## Magic Link -:::warning -To use magic links, you must configure a database and pass in the required environment variables to send emails. 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/). -::: - -You can use the `SignInPage` component to quickly set up authentication via one-time verification links. It uses Nodemailer under the hood to send the verification link to the user's email address. +The `SignIn` page component supports magic links. To enable this, you will 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. diff --git a/examples/core-auth-nextjs-email/src/auth.ts b/examples/core-auth-nextjs-email/src/auth.ts index bb87d8a1217..af08addd63f 100644 --- a/examples/core-auth-nextjs-email/src/auth.ts +++ b/examples/core-auth-nextjs-email/src/auth.ts @@ -1,7 +1,5 @@ import NextAuth from 'next-auth'; -import { AuthProvider, SupportedAuthProvider } from '@toolpad/core'; import GitHub from 'next-auth/providers/github'; -// import Credentials from 'next-auth/providers/credentials'; import Nodemailer from 'next-auth/providers/nodemailer'; import { PrismaAdapter } from '@auth/prisma-adapter'; @@ -13,22 +11,6 @@ const providers: Provider[] = [ clientId: process.env.GITHUB_CLIENT_ID, clientSecret: process.env.GITHUB_CLIENT_SECRET, }), - // Credentials({ - // credentials: { - // email: { label: 'Email Address', type: 'email' }, - // password: { label: 'Password', type: 'password' }, - // }, - // authorize(c) { - // if (c.password !== 'password') { - // return null; - // } - // return { - // id: 'test', - // name: 'Test User', - // email: String(c.email), - // }; - // }, - // }), Nodemailer({ server: { host: process.env.EMAIL_SERVER_HOST, @@ -47,33 +29,18 @@ export const providerMap = providers.map((provider) => { if (typeof provider === 'function') { const providerData = provider(); return { - id: providerData.id as SupportedAuthProvider, + id: providerData.id, name: providerData.name, - } satisfies AuthProvider; + }; } - return { id: provider.id as SupportedAuthProvider, name: provider.name } satisfies AuthProvider; + return { id: provider.id, name: provider.name }; }); -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_ID"'); } export const { handlers, auth, signIn, signOut } = NextAuth({ 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 3b6274b5688..361a187d0e7 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';\n${providers + return `import NextAuth from 'next-auth';\n${providers ?.map( (provider) => `import ${kebabToPascal(provider)} from 'next-auth/providers/${provider.toLowerCase()}';`, @@ -79,9 +79,9 @@ ${checkEnvironmentVariables(providers)} export const providerMap = providers.map((provider) => { if (typeof provider === 'function') { const providerData = provider(); - return { id: providerData.id as SupportedAuthProvider, name: providerData.name } satisfies AuthProvider; + return { id: providerData.id, name: providerData.name }; } - return { id: provider.id as SupportedAuthProvider, name: provider.name } satisfies AuthProvider; + return { id: provider.id, name: provider.name }; }); export const { handlers, auth, signIn, signOut } = NextAuth({ From f139401f3b400a7e9d18e9b296037f985508ef5c Mon Sep 17 00:00:00 2001 From: Bharat Kashyap Date: Tue, 15 Oct 2024 14:44:03 +0530 Subject: [PATCH 16/25] fix: CI --- pnpm-lock.yaml | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 401e0b748ea..be047dfc663 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: 0.0.0-experimental-45ae4c3-20241011(eslint@8.57.1) + version: 0.0.0-experimental-fa06e2c-20241014(eslint@8.57.1) eslint-plugin-react-hooks: specifier: 4.6.2 version: 4.6.2(eslint@8.57.1) @@ -2013,7 +2013,7 @@ packages: resolution: {integrity: sha512-GjV0/mUEEXpi1U5ZgDprMRRgajGMRW3G5FjMr5KLKD8nT2fTG8+h/klV3+6Dm5739QE+K5+2e91qFKAYI3pmRg==} engines: {node: '>=6.9.0'} peerDependencies: - '@babel/core': ^7.0.0-0 + '@babel/core': ^7.25.8 '@babel/preset-typescript@7.25.7': resolution: {integrity: sha512-rkkpaXJZOFN45Fb+Gki0c+KMIglk4+zZXOoMJuyEK8y8Kkc8Jd3BDmP7qPsz0zQMJj+UD7EprF+AqAXcILnexw==} @@ -2899,7 +2899,7 @@ packages: resolution: {integrity: sha512-P0E7ZrxOuyYqBvVv9w8k7wm+Xzx/KRu+BGgFcR2htTsGCpJNQJCSUXNUZ50MUmSU9hzqhwbQWNXhV1MBTl6F7A==} engines: {node: '>=14.0.0'} peerDependencies: - '@types/react': ^17.0.0 || ^18.0.0 + '@types/react': ^18.3.11 react: ^17.0.0 || ^18.0.0 react-dom: ^17.0.0 || ^18.0.0 peerDependenciesMeta: @@ -3126,7 +3126,7 @@ packages: '@mui/types@7.2.18': resolution: {integrity: sha512-uvK9dWeyCJl/3ocVnTOS6nlji/Knj8/tVqVX03UVTpdmTJYu/s4jtDd9Kvv0nRGE0CUSNW1UYAci7PYypjealg==} peerDependencies: - '@types/react': ^17.0.0 || ^18.0.0 || ^19.0.0 + '@types/react': ^18.3.11 peerDependenciesMeta: '@types/react': optional: true @@ -5991,8 +5991,8 @@ packages: peerDependencies: eslint: '>=7.0.0' - eslint-plugin-react-compiler@0.0.0-experimental-45ae4c3-20241011: - resolution: {integrity: sha512-m+BmeFtVWzrHt87sb5g5jLttHdo9YScPiuiingdEqLYtUv7pdVi6pQgY3nCOI4h09C4wmWS9xzpaVNEgiODOBg==} + eslint-plugin-react-compiler@0.0.0-experimental-fa06e2c-20241014: + resolution: {integrity: sha512-tHntZz8Kx/6RgCLn7aDGfBQizqTUUfHEDaBcrvJi1GhKzgDxmAbdn85Y6z8eGSh4s0gufNWyO9WRCYLf0hP0ow==} engines: {node: ^14.17.0 || ^16.0.0 || >= 18.0.0} peerDependencies: eslint: '>=7' @@ -15788,7 +15788,7 @@ snapshots: globals: 13.24.0 rambda: 7.5.0 - eslint-plugin-react-compiler@0.0.0-experimental-45ae4c3-20241011(eslint@8.57.1): + eslint-plugin-react-compiler@0.0.0-experimental-fa06e2c-20241014(eslint@8.57.1): dependencies: '@babel/core': 7.25.8 '@babel/parser': 7.25.8 From 16f2a3b14e752fcb63d7d6609f8fd068da8f8d2c Mon Sep 17 00:00:00 2001 From: Bharat Kashyap Date: Wed, 16 Oct 2024 01:17:46 -0700 Subject: [PATCH 17/25] Update docs/data/toolpad/core/components/sign-in-page/sign-in-page.md Co-authored-by: Jan Potoms <2109932+Janpot@users.noreply.github.com> Signed-off-by: Bharat Kashyap --- docs/data/toolpad/core/components/sign-in-page/sign-in-page.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 7209080ae57..f565e3beff3 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 @@ -70,7 +70,7 @@ Check out the complete [Next.js Auth.js Magic Link example](https://github.com/m ## Credentials :::warning -The Credentials provider is not the most secure way to authenticate users. We recommend using any of the other providers for a more robust solution. +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. From 5a1b0b4773269d2ffcc70ee284ba590e90ee3916 Mon Sep 17 00:00:00 2001 From: Bharat Kashyap Date: Wed, 16 Oct 2024 01:17:57 -0700 Subject: [PATCH 18/25] Update docs/data/toolpad/core/components/sign-in-page/sign-in-page.md Co-authored-by: Jan Potoms <2109932+Janpot@users.noreply.github.com> Signed-off-by: Bharat Kashyap --- docs/data/toolpad/core/components/sign-in-page/sign-in-page.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 f565e3beff3..ce340e22d2b 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 @@ -50,7 +50,7 @@ Find details on how to set up each provider in the [Auth.js documentation](https ## Magic Link -The `SignIn` page component supports magic links. To enable this, you will 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/). +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. From 040fcafb88033b03bdf5ebb38be451d5483853f0 Mon Sep 17 00:00:00 2001 From: Bharat Kashyap Date: Wed, 16 Oct 2024 14:53:33 +0530 Subject: [PATCH 19/25] fix: Jan review --- docs/data/toolpad/core/components/sign-in-page/sign-in-page.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 ce340e22d2b..6226e4dc99b 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 @@ -238,4 +238,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, 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). From dc9f68867c882552fc99929f58822ab6fc78d242 Mon Sep 17 00:00:00 2001 From: Bharat Kashyap Date: Wed, 16 Oct 2024 14:54:44 +0530 Subject: [PATCH 20/25] fix: CI --- pnpm-lock.yaml | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index be047dfc663..a90eb805574 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: 0.0.0-experimental-fa06e2c-20241014(eslint@8.57.1) + version: 0.0.0-experimental-605e95c-20241015(eslint@8.57.1) eslint-plugin-react-hooks: specifier: 4.6.2 version: 4.6.2(eslint@8.57.1) @@ -3126,7 +3126,7 @@ packages: '@mui/types@7.2.18': resolution: {integrity: sha512-uvK9dWeyCJl/3ocVnTOS6nlji/Knj8/tVqVX03UVTpdmTJYu/s4jtDd9Kvv0nRGE0CUSNW1UYAci7PYypjealg==} peerDependencies: - '@types/react': ^18.3.11 + '@types/react': ^17.0.0 || ^18.0.0 || ^19.0.0 peerDependenciesMeta: '@types/react': optional: true @@ -5991,8 +5991,8 @@ packages: peerDependencies: eslint: '>=7.0.0' - eslint-plugin-react-compiler@0.0.0-experimental-fa06e2c-20241014: - resolution: {integrity: sha512-tHntZz8Kx/6RgCLn7aDGfBQizqTUUfHEDaBcrvJi1GhKzgDxmAbdn85Y6z8eGSh4s0gufNWyO9WRCYLf0hP0ow==} + eslint-plugin-react-compiler@0.0.0-experimental-605e95c-20241015: + resolution: {integrity: sha512-YefQd/ZGYx5UWRRusUyf8PDmf2MPHrOGj2vZQbhJc9zUoGD3RGEJueTsIl4EZTGXjJOIe/TnuznDAa1laUyMYQ==} engines: {node: ^14.17.0 || ^16.0.0 || >= 18.0.0} peerDependencies: eslint: '>=7' @@ -15788,7 +15788,7 @@ snapshots: globals: 13.24.0 rambda: 7.5.0 - eslint-plugin-react-compiler@0.0.0-experimental-fa06e2c-20241014(eslint@8.57.1): + eslint-plugin-react-compiler@0.0.0-experimental-605e95c-20241015(eslint@8.57.1): dependencies: '@babel/core': 7.25.8 '@babel/parser': 7.25.8 From ffde5f01c428e4e3141dfcd28d8eb701c49e09c6 Mon Sep 17 00:00:00 2001 From: Bharat Kashyap Date: Fri, 18 Oct 2024 13:13:14 +0530 Subject: [PATCH 21/25] fix: lint --- .../components/sign-in-page/MagicLinkAlertSignInPage.js | 3 ++- .../components/sign-in-page/MagicLinkAlertSignInPage.tsx | 6 +++--- .../core/components/sign-in-page/MagicLinkSignInPage.js | 3 ++- .../core/components/sign-in-page/MagicLinkSignInPage.tsx | 4 ++-- 4 files changed, 9 insertions(+), 7 deletions(-) diff --git a/docs/data/toolpad/core/components/sign-in-page/MagicLinkAlertSignInPage.js b/docs/data/toolpad/core/components/sign-in-page/MagicLinkAlertSignInPage.js index 2e4b01c98f2..384a818f17f 100644 --- a/docs/data/toolpad/core/components/sign-in-page/MagicLinkAlertSignInPage.js +++ b/docs/data/toolpad/core/components/sign-in-page/MagicLinkAlertSignInPage.js @@ -1,5 +1,6 @@ import * as React from 'react'; -import { AppProvider, SignInPage } from '@toolpad/core'; +import { SignInPage } from '@toolpad/core/SignInPage'; +import { AppProvider } from '@toolpad/core/AppProvider'; import { useTheme } from '@mui/material/styles'; const providers = [{ id: 'nodemailer', name: 'Email' }]; diff --git a/docs/data/toolpad/core/components/sign-in-page/MagicLinkAlertSignInPage.tsx b/docs/data/toolpad/core/components/sign-in-page/MagicLinkAlertSignInPage.tsx index 159ab538973..760101ef225 100644 --- a/docs/data/toolpad/core/components/sign-in-page/MagicLinkAlertSignInPage.tsx +++ b/docs/data/toolpad/core/components/sign-in-page/MagicLinkAlertSignInPage.tsx @@ -1,11 +1,11 @@ import * as React from 'react'; import { AuthProvider, - AppProvider, SignInPage, - AuthResponse, SupportedAuthProvider, -} from '@toolpad/core'; + AuthResponse, +} from '@toolpad/core/SignInPage'; +import { AppProvider } from '@toolpad/core/AppProvider'; import { useTheme } from '@mui/material/styles'; const providers: { id: SupportedAuthProvider; name: string }[] = [ diff --git a/docs/data/toolpad/core/components/sign-in-page/MagicLinkSignInPage.js b/docs/data/toolpad/core/components/sign-in-page/MagicLinkSignInPage.js index f8813edf7ad..6a7dd19e672 100644 --- a/docs/data/toolpad/core/components/sign-in-page/MagicLinkSignInPage.js +++ b/docs/data/toolpad/core/components/sign-in-page/MagicLinkSignInPage.js @@ -1,5 +1,6 @@ import * as React from 'react'; -import { AppProvider, SignInPage } from '@toolpad/core'; +import { SignInPage } from '@toolpad/core/SignInPage'; +import { AppProvider } from '@toolpad/core/AppProvider'; import { useTheme } from '@mui/material/styles'; // preview-start diff --git a/docs/data/toolpad/core/components/sign-in-page/MagicLinkSignInPage.tsx b/docs/data/toolpad/core/components/sign-in-page/MagicLinkSignInPage.tsx index 916391315ca..6565c3faca1 100644 --- a/docs/data/toolpad/core/components/sign-in-page/MagicLinkSignInPage.tsx +++ b/docs/data/toolpad/core/components/sign-in-page/MagicLinkSignInPage.tsx @@ -1,10 +1,10 @@ import * as React from 'react'; import { AuthProvider, - AppProvider, SignInPage, SupportedAuthProvider, -} from '@toolpad/core'; +} from '@toolpad/core/SignInPage'; +import { AppProvider } from '@toolpad/core/AppProvider'; import { useTheme } from '@mui/material/styles'; // preview-start From 745221eff6cc20908b415275997e2b815e85ac0c Mon Sep 17 00:00:00 2001 From: Bharat Kashyap Date: Wed, 23 Oct 2024 17:28:44 +0530 Subject: [PATCH 22/25] fix: Merge --- examples/core/tutorial/next-env.d.ts | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 examples/core/tutorial/next-env.d.ts diff --git a/examples/core/tutorial/next-env.d.ts b/examples/core/tutorial/next-env.d.ts new file mode 100644 index 00000000000..40c3d68096c --- /dev/null +++ b/examples/core/tutorial/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. From 4ba416825234f88fd7f7dca6734befe6030761fd Mon Sep 17 00:00:00 2001 From: Bharat Kashyap Date: Wed, 23 Oct 2024 17:28:54 +0530 Subject: [PATCH 23/25] fix: Pedro review --- .../src/SignInPage/SignInPage.test.tsx | 26 ++-- .../src/SignInPage/SignInPage.tsx | 132 ++++++++++-------- 2 files changed, 88 insertions(+), 70 deletions(-) diff --git a/packages/toolpad-core/src/SignInPage/SignInPage.test.tsx b/packages/toolpad-core/src/SignInPage/SignInPage.test.tsx index e96ccd03421..34f502860c6 100644 --- a/packages/toolpad-core/src/SignInPage/SignInPage.test.tsx +++ b/packages/toolpad-core/src/SignInPage/SignInPage.test.tsx @@ -24,8 +24,7 @@ describe('SignInPage', () => { await userEvent.click(signInButton); - expect(signIn).toHaveBeenCalled(); - expect(signIn.mock.calls[0][0]).toHaveProperty('id', 'github'); + expect(signIn).toHaveBeenCalledWith({ id: 'github' }); }); test('renders credentials provider', async () => { @@ -40,10 +39,11 @@ describe('SignInPage', () => { await userEvent.type(passwordField, 'thepassword'); await userEvent.click(signInButton); - expect(signIn).toHaveBeenCalled(); - expect(signIn.mock.calls[0][0]).toHaveProperty('id', 'credentials'); - expect(signIn.mock.calls[0][1].get('email')).toBe('john@example.com'); - expect(signIn.mock.calls[0][1].get('password')).toBe('thepassword'); + const expectedFormData = new FormData(); + expectedFormData.append('email', 'john@example.com'); + expectedFormData.append('password', 'thepassword'); + + expect(signIn).toHaveBeenCalledWith({ id: 'credentials' }, expectedFormData); }); test('renders nodemailer provider', async () => { @@ -56,9 +56,10 @@ describe('SignInPage', () => { 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'); + const expectedFormData = new FormData(); + expectedFormData.append('email', 'john@example.com'); + + expect(signIn).toHaveBeenCalledWith({ id: 'nodemailer' }, expectedFormData); }); test('renders passkey sign-in option when available', async () => { @@ -72,8 +73,9 @@ describe('SignInPage', () => { await userEvent.type(emailField, 'john@example.com'); await userEvent.click(signInButton); - expect(signIn).toHaveBeenCalled(); - expect(signIn.mock.calls[0][0]).toHaveProperty('id', 'passkey'); - expect(signIn.mock.calls[0][1].get('email')).toBe('john@example.com'); + const expectedFormData = new FormData(); + expectedFormData.append('email', 'john@example.com'); + + expect(signIn).toHaveBeenCalledWith({ id: 'passkey' }, expectedFormData); }); }); diff --git a/packages/toolpad-core/src/SignInPage/SignInPage.tsx b/packages/toolpad-core/src/SignInPage/SignInPage.tsx index 99661325bb4..45d6f84887e 100644 --- a/packages/toolpad-core/src/SignInPage/SignInPage.tsx +++ b/packages/toolpad-core/src/SignInPage/SignInPage.tsx @@ -231,8 +231,12 @@ function SignInPage(props: SignInPageProps) { }); const callbackUrl = router?.searchParams.get('callbackUrl') ?? '/'; - const singleProvider = React.useMemo(() => providers?.length === 1, [providers]); + const isOauthProvider = React.useCallback( + (provider?: SupportedAuthProvider) => + provider && provider !== 'credentials' && provider !== 'nodemailer' && provider !== 'passkey', + [], + ); return ( @@ -258,60 +262,52 @@ function SignInPage(props: SignInPageProps) { - {error && - selectedProviderId !== 'credentials' && - selectedProviderId !== 'passkey' && - selectedProviderId !== 'nodemailer' ? ( + {error && isOauthProvider(selectedProviderId) ? ( {error} ) : null} - {Object.values(providers ?? {}).map((provider) => { - if ( - provider.id === 'credentials' || - provider.id === 'passkey' || - provider.id === 'nodemailer' - ) { - return null; - } - return ( -
{ - event.preventDefault(); - setFormStatus({ error: '', selectedProviderId: provider.id, loading: true }); - const oauthResponse = await signIn?.(provider, undefined, callbackUrl); - setFormStatus((prev) => ({ - ...prev, - loading: oauthResponse?.error || docs ? false : prev.loading, - error: oauthResponse?.error, - })); - }} - > - isOauthProvider(provider.id)) + .map((provider) => { + return ( + { + event.preventDefault(); + setFormStatus({ error: '', selectedProviderId: provider.id, loading: true }); + const oauthResponse = await signIn?.(provider, undefined, callbackUrl); + setFormStatus((prev) => ({ + ...prev, + loading: oauthResponse?.error || docs ? false : prev.loading, + error: oauthResponse?.error, + })); }} > - Sign in with {provider.name} - -
- ); - })} + + Sign in with {provider.name} + + + ); + })}
{passkeyProvider ? ( @@ -348,10 +344,15 @@ function SignInPage(props: SignInPageProps) { required slotProps={{ htmlInput: { - sx: { paddingTop: '12px', paddingBottom: '12px' }, + sx: (theme) => ({ + paddingTop: theme.spacing(1.5), + paddingBottom: theme.spacing(1.5), + }), }, inputLabel: { - sx: { lineHeight: '1rem' }, + sx: (theme) => ({ + lineHeight: theme.typography.pxToRem(16), + }), }, }} fullWidth @@ -434,10 +435,15 @@ function SignInPage(props: SignInPageProps) { required slotProps={{ htmlInput: { - sx: { paddingTop: '12px', paddingBottom: '12px' }, + sx: (theme) => ({ + paddingTop: theme.spacing(1.5), + paddingBottom: theme.spacing(1.5), + }), }, inputLabel: { - sx: { lineHeight: '1rem' }, + sx: (theme) => ({ + lineHeight: theme.typography.pxToRem(16), + }), }, }} fullWidth @@ -460,10 +466,15 @@ function SignInPage(props: SignInPageProps) { fullWidth slotProps={{ htmlInput: { - sx: { paddingTop: '12px', paddingBottom: '12px' }, + sx: (theme) => ({ + paddingTop: theme.spacing(1.5), + paddingBottom: theme.spacing(1.5), + }), }, inputLabel: { - sx: { lineHeight: '1rem' }, + sx: (theme) => ({ + lineHeight: theme.typography.pxToRem(16), + }), }, }} name="password" @@ -558,10 +569,15 @@ function SignInPage(props: SignInPageProps) { fullWidth slotProps={{ htmlInput: { - sx: { paddingTop: '12px', paddingBottom: '12px' }, + sx: (theme) => ({ + paddingTop: theme.spacing(1.5), + paddingBottom: theme.spacing(1.5), + }), }, inputLabel: { - sx: { lineHeight: '1rem' }, + sx: (theme) => ({ + lineHeight: theme.typography.pxToRem(16), + }), }, }} name="email" From 6d540a722726b83151c649baa0c4beabee51f047 Mon Sep 17 00:00:00 2001 From: Bharat Kashyap Date: Wed, 23 Oct 2024 17:30:26 +0530 Subject: [PATCH 24/25] fix: Add example to docs page --- docs/src/modules/components/ExamplesGrid/core-examples.ts | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/docs/src/modules/components/ExamplesGrid/core-examples.ts b/docs/src/modules/components/ExamplesGrid/core-examples.ts index b99860b01a8..0ecdc9a6a7b 100644 --- a/docs/src/modules/components/ExamplesGrid/core-examples.ts +++ b/docs/src/modules/components/ExamplesGrid/core-examples.ts @@ -22,6 +22,13 @@ export default function examples() { src: '/static/toolpad/docs/core/auth-next.png', source: 'https://github.com/mui/toolpad/tree/master/examples/core-auth-nextjs-pages', }, + { + title: 'Auth.js Magic Link with Next.js App router', + description: + 'This app shows you to how to get started using Toolpad Core with Auth.js Magic Links and the Next.js App router', + src: '/static/toolpad/docs/core/auth-next.png', + source: 'https://github.com/mui/toolpad/tree/master/examples/core-auth-nextjs-email', + }, { title: 'Vite with React Router', description: From 5949071f55d6f86e548c25d2b6a311ea8ace9b2c Mon Sep 17 00:00:00 2001 From: Bharat Kashyap Date: Wed, 23 Oct 2024 17:48:07 +0530 Subject: [PATCH 25/25] fix: test --- .../src/SignInPage/SignInPage.test.tsx | 14 +++++++++----- 1 file changed, 9 insertions(+), 5 deletions(-) diff --git a/packages/toolpad-core/src/SignInPage/SignInPage.test.tsx b/packages/toolpad-core/src/SignInPage/SignInPage.test.tsx index 34f502860c6..664e0f38fd7 100644 --- a/packages/toolpad-core/src/SignInPage/SignInPage.test.tsx +++ b/packages/toolpad-core/src/SignInPage/SignInPage.test.tsx @@ -24,7 +24,7 @@ describe('SignInPage', () => { await userEvent.click(signInButton); - expect(signIn).toHaveBeenCalledWith({ id: 'github' }); + expect(signIn).toHaveBeenCalledWith({ id: 'github', name: 'GitHub' }, undefined, '/'); }); test('renders credentials provider', async () => { @@ -43,7 +43,11 @@ describe('SignInPage', () => { expectedFormData.append('email', 'john@example.com'); expectedFormData.append('password', 'thepassword'); - expect(signIn).toHaveBeenCalledWith({ id: 'credentials' }, expectedFormData); + expect(signIn).toHaveBeenCalledWith( + { id: 'credentials', name: 'Credentials' }, + expectedFormData, + '/', + ); }); test('renders nodemailer provider', async () => { @@ -59,13 +63,13 @@ describe('SignInPage', () => { const expectedFormData = new FormData(); expectedFormData.append('email', 'john@example.com'); - expect(signIn).toHaveBeenCalledWith({ id: 'nodemailer' }, expectedFormData); + expect(signIn).toHaveBeenCalledWith({ id: 'nodemailer', name: 'Email' }, expectedFormData, '/'); }); test('renders passkey sign-in option when available', async () => { const signIn = vi.fn(); - render(); + render(); const emailField = screen.getByRole('textbox', { name: 'Email Address' }); const signInButton = screen.getByRole('button', { name: 'Sign in with Passkey' }); @@ -76,6 +80,6 @@ describe('SignInPage', () => { const expectedFormData = new FormData(); expectedFormData.append('email', 'john@example.com'); - expect(signIn).toHaveBeenCalledWith({ id: 'passkey' }, expectedFormData); + expect(signIn).toHaveBeenCalledWith({ id: 'passkey', name: 'Passkey' }, expectedFormData, '/'); }); });