Skip to content
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

Open
wants to merge 41 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 14 commits
Commits
Show all changes
41 commits
Select commit Hold shift + click to select a range
31ea476
feat: Add `nodemailer` provider to `SignInPage`
bharatkashyap Sep 13, 2024
c952172
Merge branch 'master' into feat/magic-link
bharatkashyap Sep 13, 2024
131b443
fix: eslint
bharatkashyap Sep 13, 2024
65e0949
Merge branch 'feat/magic-link' of github.com:bharatkashyap/mui-toolpa…
bharatkashyap Sep 13, 2024
c9e3956
Merge branch 'master' into feat/magic-link
bharatkashyap Sep 14, 2024
2c61b88
Merge branch 'master' of github.com:mui/mui-toolpad into feat/magic-link
bharatkashyap Sep 17, 2024
bbdddef
Merge branch 'master' into feat/magic-link
bharatkashyap Sep 17, 2024
4d7a335
Merge branch 'master' into feat/magic-link
bharatkashyap Sep 18, 2024
c0796ab
Merge branch 'master' into feat/magic-link
bharatkashyap Sep 18, 2024
000f694
Merge branch 'master' into feat/magic-link
bharatkashyap Sep 20, 2024
7882158
Merge branch 'master' into feat/magic-link
bharatkashyap Sep 20, 2024
3b6309d
Merge branch 'master' into feat/magic-link
bharatkashyap Sep 20, 2024
2dd6554
Merge branch 'master' of github.com:mui/mui-toolpad into feat/magic-link
bharatkashyap Sep 30, 2024
31ba678
Merge branch 'master' of github.com:mui/mui-toolpad into feat/magic-link
bharatkashyap Oct 10, 2024
70ef2aa
fix: Add example link to docs
bharatkashyap Oct 10, 2024
d390f71
fix: CI
bharatkashyap Oct 11, 2024
4532fb2
Merge branch 'master' of github.com:mui/mui-toolpad into feat/magic-link
bharatkashyap Oct 11, 2024
110a06e
Merge branch 'master' of github.com:mui/mui-toolpad into feat/magic-link
bharatkashyap Oct 11, 2024
55b316f
fix: Simplify magic links docs and fix example
bharatkashyap Oct 11, 2024
cf82dc8
fix: lint
bharatkashyap Oct 11, 2024
b139284
fix: Add test
bharatkashyap Oct 11, 2024
2ee806b
wip: Test this build
bharatkashyap Oct 11, 2024
02073b5
Merge branch 'master' into feat/magic-link
bharatkashyap Oct 11, 2024
f80bfab
fix: CI
bharatkashyap Oct 11, 2024
b4ff718
Merge branch 'feat/magic-link' of github.com:bharatkashyap/mui-toolpa…
bharatkashyap Oct 11, 2024
dc5ebf4
fix: CI, test build
bharatkashyap Oct 12, 2024
c0bf20b
fix: CI
bharatkashyap Oct 12, 2024
2422a15
fix: Test docker deployment and add it to example
bharatkashyap Oct 13, 2024
56ceca0
Merge branch 'master' of github.com:mui/mui-toolpad into feat/magic-link
bharatkashyap Oct 13, 2024
7f68521
fix: CI
bharatkashyap Oct 13, 2024
56b4466
fix: Minor tweaks
bharatkashyap Oct 14, 2024
91d95f0
fix: Jan review
bharatkashyap Oct 15, 2024
f139401
fix: CI
bharatkashyap Oct 15, 2024
16f2a3b
Update docs/data/toolpad/core/components/sign-in-page/sign-in-page.md
bharatkashyap Oct 16, 2024
5a1b0b4
Update docs/data/toolpad/core/components/sign-in-page/sign-in-page.md
bharatkashyap Oct 16, 2024
040fcaf
fix: Jan review
bharatkashyap Oct 16, 2024
dc9f688
fix: CI
bharatkashyap Oct 16, 2024
d534daa
Merge branch 'master' of github.com:mui/mui-toolpad into feat/magic-link
bharatkashyap Oct 16, 2024
22de185
Merge branch 'master' of github.com:mui/mui-toolpad into feat/magic-link
bharatkashyap Oct 18, 2024
ffde5f0
fix: lint
bharatkashyap Oct 18, 2024
c72c511
Merge branch 'master' of github.com:mui/mui-toolpad into feat/magic-link
bharatkashyap Oct 21, 2024
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
@@ -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' },
Expand Down
Original file line number Diff line number Diff line change
@@ -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' },
Expand Down
77 changes: 74 additions & 3 deletions docs/data/toolpad/core/components/sign-in-page/sign-in-page.md
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@

:::info

The following providers are supported and maintained by default:
The following OAuth providers are supported and maintained by default:

- Google
- GitHub
Expand All @@ -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
Copy link
Member

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?

Copy link
Member Author

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

Copy link
Member

@apedroferreira apedroferreira Oct 16, 2024

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.


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:
Copy link
Member

Choose a reason for hiding this comment

The 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') {
Copy link
Member

Choose a reason for hiding this comment

The 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?
Ideally this code would be easier to follow.

Copy link
Member Author

Choose a reason for hiding this comment

The 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 NEXT_REDIRECT which we must catch, I'm basing this on vercel/next.js#49298 (comment)

if (provider.id === 'nodemailer' &&
(error as any).digest?.includes('verify-request')) {
return {
success: 'Check your email for a verification link.',
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Would a more specific name like successAlert or successMessage be better?

Copy link
Member Author

@bharatkashyap bharatkashyap Oct 11, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Perhaps, but that would mean we also rename the error prop in AuthResponse to errorMessage which – while better – would be a breaking change on the AuthResponse interface. What do you think?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Got it, just a thought, I think error shouldn't be renamed so we can keep success if it's more consistent with that.

};
}
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

View workflow job for this annotation

GitHub Actions / runner / vale

[vale] reported by reviewdog 🐶 [Google.We] Try to avoid using first-person plural like 'We'. Raw Output: {"message": "[Google.We] Try to avoid using first-person plural like 'We'.", "location": {"path": "docs/data/toolpad/core/components/sign-in-page/sign-in-page.md", "range": {"start": {"line": 126, "column": 76}}}, "severity": "WARNING"}
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.
Expand Down
2 changes: 1 addition & 1 deletion docs/pages/toolpad/core/api/sign-in-page.json
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
"providers": {
"type": {
"name": "arrayOf",
"description": "Array&lt;{ id: 'apple'<br>&#124;&nbsp;'auth0'<br>&#124;&nbsp;'cognito'<br>&#124;&nbsp;'credentials'<br>&#124;&nbsp;'discord'<br>&#124;&nbsp;'facebook'<br>&#124;&nbsp;'fusionauth'<br>&#124;&nbsp;'github'<br>&#124;&nbsp;'gitlab'<br>&#124;&nbsp;'google'<br>&#124;&nbsp;'instagram'<br>&#124;&nbsp;'keycloak'<br>&#124;&nbsp;'line'<br>&#124;&nbsp;'linkedin'<br>&#124;&nbsp;'microsoft-entra-id'<br>&#124;&nbsp;'okta'<br>&#124;&nbsp;'slack'<br>&#124;&nbsp;'spotify'<br>&#124;&nbsp;'tiktok'<br>&#124;&nbsp;'twitch'<br>&#124;&nbsp;'twitter', name: string }&gt;"
"description": "Array&lt;{ id: 'apple'<br>&#124;&nbsp;'auth0'<br>&#124;&nbsp;'cognito'<br>&#124;&nbsp;'credentials'<br>&#124;&nbsp;'discord'<br>&#124;&nbsp;'facebook'<br>&#124;&nbsp;'fusionauth'<br>&#124;&nbsp;'github'<br>&#124;&nbsp;'gitlab'<br>&#124;&nbsp;'google'<br>&#124;&nbsp;'instagram'<br>&#124;&nbsp;'keycloak'<br>&#124;&nbsp;'line'<br>&#124;&nbsp;'linkedin'<br>&#124;&nbsp;'microsoft-entra-id'<br>&#124;&nbsp;'nodemailer'<br>&#124;&nbsp;'okta'<br>&#124;&nbsp;'slack'<br>&#124;&nbsp;'spotify'<br>&#124;&nbsp;'tiktok'<br>&#124;&nbsp;'twitch'<br>&#124;&nbsp;'twitter', name: string }&gt;"
},
"default": "[]"
},
Expand Down
3 changes: 3 additions & 0 deletions examples/core-auth-nextjs-email/.eslintrc.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
{
"extends": "next/core-web-vitals"
}
5 changes: 5 additions & 0 deletions examples/core-auth-nextjs-email/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
node_modules/
/test-results/
/playwright-report/
/blob-report/
/playwright/.cache/
34 changes: 34 additions & 0 deletions examples/core-auth-nextjs-email/README.md
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

View workflow job for this annotation

GitHub Actions / runner / vale

[vale] reported by reviewdog 🐶 [Google.We] Try to avoid using first-person plural like 'our'. Raw Output: {"message": "[Google.We] Try to avoid using first-person plural like 'our'.", "location": {"path": "examples/core-auth-nextjs-email/README.md", "range": {"start": {"line": 34, "column": 11}}}, "severity": "WARNING"}
5 changes: 5 additions & 0 deletions examples/core-auth-nextjs-email/next-env.d.ts
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.
4 changes: 4 additions & 0 deletions examples/core-auth-nextjs-email/next.config.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
/** @type {import('next').NextConfig} */
const nextConfig = {};

export default nextConfig;
34 changes: 34 additions & 0 deletions examples/core-auth-nextjs-email/package.json
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"
}
}
11 changes: 11 additions & 0 deletions examples/core-auth-nextjs-email/src/app/(dashboard)/layout.tsx
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>
);
}
Loading
Loading