-
-
Notifications
You must be signed in to change notification settings - Fork 270
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
[core] Support magic links in SignInPage
#4085
base: master
Are you sure you want to change the base?
Changes from 14 commits
31ea476
c952172
131b443
65e0949
c9e3956
2c61b88
bbdddef
4d7a335
c0796ab
000f694
7882158
3b6309d
2dd6554
31ba678
70ef2aa
d390f71
4532fb2
110a06e
55b316f
cf82dc8
b139284
2ee806b
02073b5
f80bfab
b4ff718
dc5ebf4
c0bf20b
2422a15
56ceca0
7f68521
56b4466
91d95f0
f139401
16f2a3b
5a1b0b4
040fcaf
dc9f688
d534daa
22de185
ffde5f0
c72c511
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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 | ||
<AppProvider theme={theme}> | ||
<SignInPage signIn={signIn} providers={providers} /> | ||
</AppProvider> | ||
// preview-end | ||
); | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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<AuthResponse> = async ( | ||
provider, | ||
) => { | ||
const promise = new Promise<AuthResponse>((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 | ||
<AppProvider theme={theme}> | ||
<SignInPage signIn={signIn} providers={providers} /> | ||
</AppProvider> | ||
// preview-end | ||
); | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,9 @@ | ||
resolve({ | ||
success: 'Check your email for a verification link.', | ||
}); | ||
|
||
// ... | ||
|
||
<AppProvider theme={theme}> | ||
<SignInPage signIn={signIn} providers={providers} /> | ||
</AppProvider> |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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 | ||
<AppProvider theme={theme}> | ||
<SignInPage signIn={signIn} providers={providers} /> | ||
</AppProvider> | ||
// preview-end | ||
); | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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<void>((resolve) => { | ||
setTimeout(() => { | ||
console.log(`Sign in with ${provider.id}`); | ||
resolve(); | ||
}, 500); | ||
}); | ||
return promise; | ||
}; | ||
|
||
export default function MagicLinkSignInPage() { | ||
const theme = useTheme(); | ||
return ( | ||
// preview-start | ||
<AppProvider theme={theme}> | ||
<SignInPage signIn={signIn} providers={providers} /> | ||
</AppProvider> | ||
// preview-end | ||
); | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,9 @@ | ||
const providers: { id: SupportedAuthProvider; name: string }[] = [ | ||
{ id: 'nodemailer', name: 'Email' }, | ||
]; | ||
|
||
// ... | ||
|
||
<AppProvider theme={theme}> | ||
<SignInPage signIn={signIn} providers={providers} /> | ||
</AppProvider> |
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -22,7 +22,7 @@ | |
|
||
:::info | ||
|
||
The following providers are supported and maintained by default: | ||
The following OAuth providers are supported and maintained by default: | ||
|
||
- GitHub | ||
|
@@ -44,15 +44,86 @@ | |
- 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: | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I guess they don't really "need" to but it's a way that users can do it, right? So maybe we can say they "can" add that code instead, as this is an example of how they can do it with Auth.js? |
||
|
||
```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 ( | ||
<React.Fragment> | ||
<SignInPage providers={providerMap} signIn={ | ||
async (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) { | ||
if (error instanceof Error && error.message === 'NEXT_REDIRECT') { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Not super clean that this is how "success" works (by catching an error) but if that's just how Auth.js does it I guess there's nothing we can do? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This is how Next.js + Auth.js server compoents redirects work - throwing a |
||
if (provider.id === 'nodemailer' && | ||
(error as any).digest?.includes('verify-request')) { | ||
return { | ||
success: 'Check your email for a verification link.', | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Would a more specific name like There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Perhaps, but that would mean we also rename the There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Got it, just a thought, I think |
||
}; | ||
} | ||
throw error; | ||
} | ||
|
||
if (error instanceof AuthError) { | ||
return { | ||
error: | ||
error.type === 'CredentialsSignin' | ||
? 'Invalid credentials.' | ||
: 'An error with Auth.js occurred.', | ||
type: error.type, | ||
}; | ||
} | ||
return { | ||
error: 'Something went wrong.', | ||
type: 'UnknownError', | ||
}; | ||
} | ||
}} />; | ||
</React.Fragment> | ||
); | ||
} | ||
``` | ||
|
||
:::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. | ||
Check warning on line 126 in docs/data/toolpad/core/components/sign-in-page/sign-in-page.md GitHub Actions / runner / vale
|
||
bharatkashyap marked this conversation as resolved.
Show resolved
Hide resolved
|
||
::: | ||
|
||
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. | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,3 @@ | ||
{ | ||
"extends": "next/core-web-vitals" | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,5 @@ | ||
node_modules/ | ||
/test-results/ | ||
/playwright-report/ | ||
/blob-report/ | ||
/playwright/.cache/ |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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. | ||
Check warning on line 34 in examples/core-auth-nextjs-email/README.md GitHub Actions / runner / vale
|
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,5 @@ | ||
/// <reference types="next" /> | ||
/// <reference types="next/image-types/global" /> | ||
|
||
// NOTE: This file should not be edited | ||
// see https://nextjs.org/docs/app/building-your-application/configuring/typescript for more information. |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,4 @@ | ||
/** @type {import('next').NextConfig} */ | ||
const nextConfig = {}; | ||
|
||
export default nextConfig; |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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" | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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 ( | ||
<DashboardLayout> | ||
<PageContainer>{props.children}</PageContainer> | ||
</DashboardLayout> | ||
); | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Should there be a default success alert that would already show in this demo?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We have a separate demo on the Alerts just below this one, so I think this one is okay? If you think strongly that we should merge them, I can do that
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Not a huge deal, it's just that the first demo doesn't provide any feedback when you press the submit button so maybe that will feel a bit weird if it's the first thing a user tries.
But I just noticed that other demos in the same page work the same... maybe they could always show a standard browser alert or something (instead of a console log that most people will miss), but I guess that's something you can add at any time, doesn't need to block this PR.