├── .env.local.example ├── .eslintrc.json ├── .gitignore ├── LICENSE ├── README.md ├── components └── Layout.js ├── emails ├── confirm-email.html └── welcome.html ├── next.config.js ├── package-lock.json ├── package.json ├── pages ├── _app.js ├── api │ └── auth │ │ └── [...nextauth].js ├── auth │ ├── confirm-request.js │ └── signin.js └── index.js ├── postcss.config.js ├── public ├── favicon.ico └── vercel.svg ├── styles └── globals.css └── tailwind.config.js /.env.local.example: -------------------------------------------------------------------------------- 1 | NEXTAUTH_URL=http://localhost:3000 2 | NEXTAUTH_SECRET= 3 | EMAIL_SERVER_HOST=smtp.example.com.com 4 | EMAIL_SERVER_PORT=465 5 | EMAIL_SERVER_USER= 6 | EMAIL_SERVER_PASSWORD= 7 | EMAIL_FROM=no-reply@example.com 8 | FAUNA_SECRET_KEY= 9 | FAUNA_DOMAIN= -------------------------------------------------------------------------------- /.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "next/core-web-vitals" 3 | } 4 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | /node_modules 5 | /.pnp 6 | .pnp.js 7 | 8 | # testing 9 | /coverage 10 | 11 | # next.js 12 | /.next/ 13 | /out/ 14 | 15 | # production 16 | /build 17 | 18 | # misc 19 | .DS_Store 20 | *.pem 21 | 22 | # debug 23 | npm-debug.log* 24 | yarn-debug.log* 25 | yarn-error.log* 26 | 27 | # local env files 28 | .env.local 29 | .env.development.local 30 | .env.test.local 31 | .env.production.local 32 | 33 | # vercel 34 | .vercel 35 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2021 AlterClass 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Magic NextAuth 2 | 3 | Magic Link Authentication in Next.js with NextAuth and Fauna by [AlterClass.io](https://alterclass.io). 4 | 5 | - Learn to build this application step-by-step by following the tutorial on [AlterClass](https://alterclass.io/tutorials/magic-link-authentication-in-nextjs-with-nextauth-and-fauna). 6 | 7 | - Preview the app live [here](https://magic-next-auth.vercel.app/). 8 | 9 | - Deploy the same app using Vercel: 10 | 11 | [![Deploy with Vercel](https://vercel.com/button)](https://vercel.com/new/git/external?repository-url=https://github.com/AlterClassIO/magic-next-auth&project-name=Magic+Auth+by+AlterClass&repository-name=Magic+Auth+by+AlterClass) 12 | 13 | ## Getting Started 14 | 15 | ### 1. Clone the repository and install dependencies 16 | 17 | ``` 18 | git clone https://github.com/AlterClassIO/magic-next-auth 19 | cd magic-next-auth 20 | npm install 21 | ``` 22 | 23 | ### 2. Configure your local environment 24 | 25 | Rename the `.env.local.example` file in this directory to `.env.local` (which will 26 | be ignored by Git): 27 | 28 | ``` 29 | cp .env.local.example .env.local 30 | ``` 31 | 32 | Add details for the SMTP server. 33 | 34 | ### 3. Start the application 35 | 36 | To run your site locally, use: 37 | 38 | ``` 39 | npm run dev 40 | ``` 41 | 42 | To run it in production mode, use: 43 | 44 | ``` 45 | npm run build 46 | npm run start 47 | ``` 48 | 49 | ### 4. Preparing for Production 50 | 51 | You must set the `NEXTAUTH_URL` environment variable with the URL of your site, 52 | before deploying to production. 53 | 54 | e.g. in your `.env.local` file - `NEXTAUTH_URL=https://example.com` 55 | 56 | To do this with Vercel, you can use the 57 | [Vercel project dashboard](https://vercel.com/dashboard) or their cli via the 58 | `vc env` command: 59 | 60 | ``` 61 | vc env add NEXTAUTH_URL production 62 | ``` 63 | 64 | ## License 65 | 66 | [MIT](https://github.com/AlterClassIO/magic-next-auth/blob/master/LICENSE) 67 | -------------------------------------------------------------------------------- /components/Layout.js: -------------------------------------------------------------------------------- 1 | import { HeartIcon } from '@heroicons/react/solid'; 2 | 3 | const Layout = ({ children = null }) => ( 4 |
5 |
6 | {children} 7 |
8 | 9 | 21 |
22 | ); 23 | 24 | export default Layout; 25 | -------------------------------------------------------------------------------- /emails/confirm-email.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 321 | 328 | 329 | 330 | This link will expire in 10 min. 331 | 338 | 339 | 458 | 459 | 460 | 461 | 462 | -------------------------------------------------------------------------------- /emails/welcome.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 321 | 328 | 329 | 330 | Thanks for trying out Magic NextAuth. We’ve pulled together some 332 | information and resources to help you get started. 334 | 341 | 342 | 423 | 424 | 425 | 426 | 427 | -------------------------------------------------------------------------------- /next.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | reactStrictMode: true, 3 | } 4 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "magic-next-auth", 3 | "version": "0.1.0", 4 | "private": true, 5 | "scripts": { 6 | "dev": "next dev", 7 | "build": "next build", 8 | "start": "next start", 9 | "lint": "next lint" 10 | }, 11 | "dependencies": { 12 | "@heroicons/react": "^1.0.5", 13 | "@next-auth/fauna-adapter": "^1.0.0", 14 | "faunadb": "^4.4.1", 15 | "handlebars": "^4.7.7", 16 | "next": "12.0.4", 17 | "next-auth": "^4.0.2", 18 | "nodemailer": "^6.7.1", 19 | "react": "17.0.2", 20 | "react-dom": "17.0.2", 21 | "react-hot-toast": "^2.1.1" 22 | }, 23 | "devDependencies": { 24 | "autoprefixer": "^10.4.0", 25 | "eslint": "7.32.0", 26 | "eslint-config-next": "12.0.4", 27 | "postcss": "^8.3.11", 28 | "tailwindcss": "^2.2.19" 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /pages/_app.js: -------------------------------------------------------------------------------- 1 | import '../styles/globals.css'; 2 | import { SessionProvider } from 'next-auth/react'; 3 | import Head from 'next/head'; 4 | import { Toaster } from 'react-hot-toast'; 5 | 6 | function MyApp({ Component, pageProps: { session, ...pageProps } }) { 7 | return ( 8 | <> 9 | 10 | Magic NextAuth | AlterClass 11 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | ); 23 | } 24 | 25 | export default MyApp; 26 | -------------------------------------------------------------------------------- /pages/api/auth/[...nextauth].js: -------------------------------------------------------------------------------- 1 | import NextAuth from 'next-auth'; 2 | import EmailProvider from 'next-auth/providers/email'; 3 | import { Client as FaunaClient } from 'faunadb'; 4 | import { FaunaAdapter } from '@next-auth/fauna-adapter'; 5 | import { readFileSync } from 'fs'; 6 | import path from 'path'; 7 | import nodemailer from 'nodemailer'; 8 | import Handlebars from 'handlebars'; 9 | 10 | const client = new FaunaClient({ 11 | secret: process.env.FAUNA_SECRET_KEY, 12 | domain: process.env.FAUNA_DOMAIN, 13 | }); 14 | 15 | const transporter = nodemailer.createTransport({ 16 | host: process.env.EMAIL_SERVER_HOST, 17 | port: process.env.EMAIL_SERVER_PORT, 18 | auth: { 19 | user: process.env.EMAIL_SERVER_USER, 20 | pass: process.env.EMAIL_SERVER_PASSWORD, 21 | }, 22 | secure: true, 23 | }); 24 | 25 | const emailsDir = path.resolve(process.cwd(), 'emails'); 26 | 27 | const sendVerificationRequest = ({ identifier, url }) => { 28 | const emailFile = readFileSync(path.join(emailsDir, 'confirm-email.html'), { 29 | encoding: 'utf8', 30 | }); 31 | const emailTemplate = Handlebars.compile(emailFile); 32 | transporter.sendMail({ 33 | from: `"⚡ Magic NextAuth" ${process.env.EMAIL_FROM}`, 34 | to: identifier, 35 | subject: 'Your sign-in link for Magic NextAuth', 36 | html: emailTemplate({ 37 | base_url: process.env.NEXTAUTH_URL, 38 | signin_url: url, 39 | email: identifier, 40 | }), 41 | }); 42 | }; 43 | 44 | const sendWelcomeEmail = async ({ user }) => { 45 | const { email } = user; 46 | 47 | try { 48 | const emailFile = readFileSync(path.join(emailsDir, 'welcome.html'), { 49 | encoding: 'utf8', 50 | }); 51 | const emailTemplate = Handlebars.compile(emailFile); 52 | await transporter.sendMail({ 53 | from: `"⚡ Magic NextAuth" ${process.env.EMAIL_FROM}`, 54 | to: email, 55 | subject: 'Welcome to Magic NextAuth! 🎉', 56 | html: emailTemplate({ 57 | base_url: process.env.NEXTAUTH_URL, 58 | support_email: 'support@alterclass.io', 59 | }), 60 | }); 61 | } catch (error) { 62 | console.log(`❌ Unable to send welcome email to user (${email})`); 63 | } 64 | }; 65 | 66 | export default NextAuth({ 67 | pages: { 68 | signIn: '/auth/signin', 69 | signOut: '/', 70 | }, 71 | providers: [ 72 | EmailProvider({ 73 | maxAge: 10 * 60, // Magic links are valid for 10 min only 74 | sendVerificationRequest, 75 | }), 76 | ], 77 | adapter: FaunaAdapter(client), 78 | events: { createUser: sendWelcomeEmail }, 79 | secret: process.env.NEXTAUTH_SECRET, 80 | }); 81 | -------------------------------------------------------------------------------- /pages/auth/confirm-request.js: -------------------------------------------------------------------------------- 1 | import Link from 'next/link'; 2 | import { useRouter } from 'next/router'; 3 | import { useSession } from 'next-auth/react'; 4 | import { CheckCircleIcon } from '@heroicons/react/outline'; 5 | import Layout from '../../components/Layout'; 6 | 7 | const ConfirmRequest = () => { 8 | const { data: session, status } = useSession(); 9 | const loading = status === 'loading'; 10 | 11 | const router = useRouter(); 12 | 13 | if (!loading && !session) { 14 | router.push('/auth/signin'); 15 | } 16 | 17 | return ( 18 | 19 | {loading ? ( 20 |

Loading...

21 | ) : !session ? ( 22 |

Redirecting...

23 | ) : ( 24 | <> 25 | 26 |

27 | You're logged in! 28 |

29 |

30 | Go back to your original tab. 31 |

32 |

33 | You can close this window or click{' '} 34 | 35 | 36 | this link 37 | 38 | {' '} 39 | to go back to the homepage. 40 |

41 | 42 | )} 43 |
44 | ); 45 | }; 46 | 47 | export default ConfirmRequest; 48 | -------------------------------------------------------------------------------- /pages/auth/signin.js: -------------------------------------------------------------------------------- 1 | import { useState, useEffect } from 'react'; 2 | import { createPortal } from 'react-dom'; 3 | import { useRouter } from 'next/router'; 4 | import { signIn, getSession } from 'next-auth/react'; 5 | import { toast } from 'react-hot-toast'; 6 | import { LightningBoltIcon, MailOpenIcon } from '@heroicons/react/outline'; 7 | import Layout from '../../components/Layout'; 8 | 9 | const MagicLinkModal = ({ show = false, email = '' }) => { 10 | if (!show) return null; 11 | 12 | return createPortal( 13 |
14 |
15 |
16 | 17 |

Confirm your email

18 |

19 | We emailed a magic link to {email}. Check your 20 | inbox and click the link in the email to login. 21 |

22 |
23 |
24 |
, 25 | document.body 26 | ); 27 | }; 28 | 29 | const SignIn = () => { 30 | const router = useRouter(); 31 | 32 | const [email, setEmail] = useState(''); 33 | const [disabled, setDisabled] = useState(false); 34 | const [showModal, setShowModal] = useState(false); 35 | 36 | useEffect(() => { 37 | let intervalId, redirecting; 38 | 39 | if (showModal) { 40 | setInterval(async () => { 41 | const session = await getSession(); 42 | if (session && !redirecting) { 43 | // User connected using the magic link -> redirect him/her 44 | redirecting = true; 45 | router.push(router.query?.callbackUrl || '/'); 46 | } 47 | }, 1000); 48 | } 49 | 50 | return () => { 51 | intervalId && clearInterval(intervalId); 52 | }; 53 | }, [showModal, router]); 54 | 55 | const handleSignIn = async e => { 56 | e.preventDefault(); 57 | let toastId; 58 | try { 59 | toastId = toast.loading('Loading...'); 60 | setDisabled(true); 61 | // Perform sign in 62 | const { error } = await signIn('email', { 63 | email, 64 | redirect: false, 65 | callbackUrl: `${window.location.origin}/auth/confirm-request`, 66 | }); 67 | // Something went wrong 68 | if (error) { 69 | throw new Error(error); 70 | } 71 | setShowModal(true); 72 | toast.success('Magic link successfully sent', { id: toastId }); 73 | } catch (error) { 74 | console.log(error); 75 | toast.error('Unable to send magic link', { id: toastId }); 76 | } finally { 77 | setDisabled(false); 78 | } 79 | }; 80 | 81 | return ( 82 | 83 | 84 |

85 | Sign in to your account 86 |

87 |
91 |
92 | 95 | setEmail(e.target.value)} 101 | placeholder="elon@spacex.com" 102 | disabled={disabled} 103 | className="py-2 px-4 w-full border rounded-md border-gray-300 focus:outline-none focus:ring-4 focus:ring-opacity-20 focus:border-blue-400 focus:ring-blue-400 transition disabled:opacity-50 disabled:cursor-not-allowed " 104 | /> 105 |
106 | 113 |
114 | 115 | 116 |
117 | ); 118 | }; 119 | 120 | export default SignIn; 121 | -------------------------------------------------------------------------------- /pages/index.js: -------------------------------------------------------------------------------- 1 | import { signOut, useSession } from 'next-auth/react'; 2 | import Link from 'next/link'; 3 | import { LightningBoltIcon } from '@heroicons/react/outline'; 4 | import Layout from '../components/Layout'; 5 | 6 | export default function Home() { 7 | const { data: session } = useSession(); 8 | 9 | return ( 10 | 11 |

12 | 13 | 14 | Magic NextAuth 15 | 16 |

17 |

18 | Magic Link Authentication in Next.js with NextAuth and Fauna 19 |

20 |
21 | {session?.user ? ( 22 |
23 |

24 | Signed in as {session.user.email} 25 |

26 | 32 |
33 | ) : ( 34 | 35 | 36 | Get started 37 | 38 | 39 | )} 40 |
41 |
42 | ); 43 | } 44 | -------------------------------------------------------------------------------- /postcss.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | plugins: { 3 | tailwindcss: {}, 4 | autoprefixer: {}, 5 | }, 6 | } 7 | -------------------------------------------------------------------------------- /public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AlterClassIO/magic-next-auth/0e2077a4e82e21ab04f09ce61668091fca4c9cc6/public/favicon.ico -------------------------------------------------------------------------------- /public/vercel.svg: -------------------------------------------------------------------------------- 1 | 3 | 4 | -------------------------------------------------------------------------------- /styles/globals.css: -------------------------------------------------------------------------------- 1 | @tailwind base; 2 | @tailwind components; 3 | @tailwind utilities; 4 | 5 | body, 6 | #__next { 7 | @apply bg-gray-100 text-gray-900; 8 | } 9 | -------------------------------------------------------------------------------- /tailwind.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | mode: 'jit', 3 | purge: ['./components/**/*.{js,ts,jsx,tsx}', './pages/**/*.{js,ts,jsx,tsx}'], 4 | darkMode: false, // or 'media' or 'class' 5 | theme: { 6 | extend: { 7 | keyframes: { 8 | appear: { 9 | '0%': { opacity: 0 }, 10 | '100%': { opacity: 1 }, 11 | }, 12 | zoomIn: { 13 | '0%': { opacity: 0, transform: 'scale(.5)' }, 14 | '100%': { opacity: 1, transform: 'scale(1)' }, 15 | }, 16 | }, 17 | animation: { 18 | appear: 'appear 300ms ease-out 150ms both', 19 | zoomIn: 'appear 300ms ease-out 150ms both', 20 | }, 21 | }, 22 | }, 23 | variants: { 24 | extend: {}, 25 | }, 26 | plugins: [], 27 | }; 28 | --------------------------------------------------------------------------------