├── .eslintrc ├── next.config.js ├── public ├── favicon.ico └── vercel.svg ├── components ├── profile │ ├── user-profile.module.css │ ├── profile-form.module.css │ ├── user-profile.js │ └── profile-form.js ├── starting-page │ ├── starting-page.module.css │ └── starting-page.js ├── layout │ ├── layout.js │ ├── main-navigation.module.css │ └── main-navigation.js └── auth │ ├── auth-form.module.css │ └── auth-form.js ├── pages ├── api │ ├── hello.js │ ├── auth │ │ ├── [...nextauth].js │ │ └── signup.js │ └── user │ │ └── change-password.js ├── index.js ├── _app.js ├── profile.js └── auth.js ├── lib ├── db.js └── auth.js ├── styles └── globals.css ├── .gitignore ├── package.json └── README.md /.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "extends": ["next", "next/core-web-vitals"] 3 | } 4 | -------------------------------------------------------------------------------- /next.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | reactStrictMode: true, 3 | } 4 | -------------------------------------------------------------------------------- /public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DawnMD/next-auth-credentials/HEAD/public/favicon.ico -------------------------------------------------------------------------------- /components/profile/user-profile.module.css: -------------------------------------------------------------------------------- 1 | .profile { 2 | margin: 3rem auto; 3 | text-align: center; 4 | } 5 | 6 | .profile h1 { 7 | font-size: 5rem; 8 | } -------------------------------------------------------------------------------- /components/starting-page/starting-page.module.css: -------------------------------------------------------------------------------- 1 | .starting { 2 | margin: 3rem auto; 3 | text-align: center; 4 | } 5 | 6 | .starting h1 { 7 | font-size: 5rem; 8 | } -------------------------------------------------------------------------------- /pages/api/hello.js: -------------------------------------------------------------------------------- 1 | // Next.js API route support: https://nextjs.org/docs/api-routes/introduction 2 | 3 | export default function handler(req, res) { 4 | res.status(200).json({ name: 'John Doe' }) 5 | } 6 | -------------------------------------------------------------------------------- /pages/index.js: -------------------------------------------------------------------------------- 1 | import StartingPageContent from '../components/starting-page/starting-page'; 2 | 3 | function HomePage() { 4 | return ; 5 | } 6 | 7 | export default HomePage; 8 | -------------------------------------------------------------------------------- /lib/db.js: -------------------------------------------------------------------------------- 1 | import { MongoClient } from 'mongodb'; 2 | 3 | export async function connectToDatabase() { 4 | const client = await MongoClient.connect(process.env.MONGO_URI, { 5 | useNewUrlParser: true, 6 | useUnifiedTopology: true, 7 | }); 8 | 9 | return client; 10 | } 11 | -------------------------------------------------------------------------------- /components/layout/layout.js: -------------------------------------------------------------------------------- 1 | import { Fragment } from 'react'; 2 | 3 | import MainNavigation from './main-navigation'; 4 | 5 | function Layout(props) { 6 | return ( 7 | 8 | 9 |
{props.children}
10 |
11 | ); 12 | } 13 | 14 | export default Layout; 15 | -------------------------------------------------------------------------------- /lib/auth.js: -------------------------------------------------------------------------------- 1 | import { hash, compare } from 'bcryptjs'; 2 | 3 | export async function hashPassword(password) { 4 | const hashedPassword = await hash(password, 12); 5 | return hashedPassword; 6 | } 7 | 8 | export async function verifyPassword(password, hashedPassword) { 9 | const isValid = await compare(password, hashedPassword); 10 | return isValid; 11 | } -------------------------------------------------------------------------------- /styles/globals.css: -------------------------------------------------------------------------------- 1 | html, 2 | body { 3 | padding: 0; 4 | margin: 0; 5 | font-family: -apple-system, BlinkMacSystemFont, Segoe UI, Roboto, Oxygen, 6 | Ubuntu, Cantarell, Fira Sans, Droid Sans, Helvetica Neue, sans-serif; 7 | } 8 | 9 | a { 10 | color: inherit; 11 | text-decoration: none; 12 | } 13 | 14 | * { 15 | box-sizing: border-box; 16 | } 17 | -------------------------------------------------------------------------------- /components/starting-page/starting-page.js: -------------------------------------------------------------------------------- 1 | import classes from './starting-page.module.css'; 2 | 3 | function StartingPageContent() { 4 | // Show Link to Login page if NOT auth 5 | 6 | return ( 7 |
8 |

Welcome on Board!

9 |
10 | ); 11 | } 12 | 13 | export default StartingPageContent; 14 | -------------------------------------------------------------------------------- /pages/_app.js: -------------------------------------------------------------------------------- 1 | import { Provider } from 'next-auth/client'; 2 | 3 | import Layout from '../components/layout/layout'; 4 | import '../styles/globals.css'; 5 | 6 | function MyApp({ Component, pageProps }) { 7 | return ( 8 | 9 | 10 | 11 | 12 | 13 | ); 14 | } 15 | 16 | export default MyApp; 17 | -------------------------------------------------------------------------------- /.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 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "next-auth-credentials", 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 | "bcryptjs": "^2.4.3", 13 | "mongodb": "^4.0.1", 14 | "next": "11.1.1", 15 | "next-auth": "^3.27.3", 16 | "react": "17.0.2", 17 | "react-dom": "17.0.2" 18 | }, 19 | "devDependencies": { 20 | "eslint": "7.32.0", 21 | "eslint-config-next": "11.0.1" 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /pages/profile.js: -------------------------------------------------------------------------------- 1 | import { getSession } from 'next-auth/client'; 2 | 3 | import UserProfile from '../components/profile/user-profile'; 4 | 5 | function ProfilePage() { 6 | return ; 7 | } 8 | 9 | export async function getServerSideProps(context) { 10 | const session = await getSession({ req: context.req }); 11 | 12 | if (!session) { 13 | return { 14 | redirect: { 15 | destination: '/auth', 16 | permanent: false, 17 | }, 18 | }; 19 | } 20 | 21 | return { 22 | props: { session }, 23 | }; 24 | } 25 | 26 | export default ProfilePage; 27 | -------------------------------------------------------------------------------- /pages/auth.js: -------------------------------------------------------------------------------- 1 | import { useRouter } from 'next/router'; 2 | import { getSession } from 'next-auth/client'; 3 | import { useEffect, useState } from 'react'; 4 | 5 | import AuthForm from '../components/auth/auth-form'; 6 | 7 | function AuthPage() { 8 | const [isLoading, setIsLoading] = useState(true); 9 | const router = useRouter(); 10 | 11 | useEffect(() => { 12 | getSession().then((session) => { 13 | if (session) { 14 | router.replace('/'); 15 | } else { 16 | setIsLoading(false); 17 | } 18 | }); 19 | }, [router]); 20 | 21 | if (isLoading) { 22 | return

Loading...

; 23 | } 24 | 25 | return ; 26 | } 27 | 28 | export default AuthPage; 29 | -------------------------------------------------------------------------------- /components/profile/profile-form.module.css: -------------------------------------------------------------------------------- 1 | .form { 2 | width: 95%; 3 | max-width: 25rem; 4 | margin: 2rem auto; 5 | } 6 | 7 | .control { 8 | margin-bottom: 0.5rem; 9 | } 10 | 11 | .control label { 12 | font-weight: bold; 13 | margin-bottom: 0.5rem; 14 | color: #353336; 15 | display: block; 16 | } 17 | 18 | .control input { 19 | display: block; 20 | font: inherit; 21 | width: 100%; 22 | border-radius: 4px; 23 | border: 1px solid #38015c; 24 | padding: 0.25rem; 25 | background-color: #f7f0fa; 26 | } 27 | 28 | .action { 29 | margin-top: 1.5rem; 30 | } 31 | 32 | .action button { 33 | font: inherit; 34 | cursor: pointer; 35 | padding: 0.5rem 1.5rem; 36 | border-radius: 4px; 37 | background-color: #38015c; 38 | color: white; 39 | border: 1px solid #38015c; 40 | } 41 | 42 | .action button:hover { 43 | background-color: #540d83; 44 | border-color: #540d83; 45 | } -------------------------------------------------------------------------------- /public/vercel.svg: -------------------------------------------------------------------------------- 1 | 3 | 4 | -------------------------------------------------------------------------------- /components/layout/main-navigation.module.css: -------------------------------------------------------------------------------- 1 | .header { 2 | width: 100%; 3 | height: 5rem; 4 | background-color: #38015c; 5 | box-shadow: 0 1px 4px rgba(0, 0, 0, 0.2); 6 | display: flex; 7 | justify-content: space-between; 8 | align-items: center; 9 | padding: 0 10%; 10 | } 11 | 12 | .logo { 13 | font-family: 'Lato', sans-serif; 14 | font-size: 2rem; 15 | color: white; 16 | margin: 0; 17 | } 18 | 19 | .header ul { 20 | list-style: none; 21 | margin: 0; 22 | padding: 0; 23 | display: flex; 24 | align-items: baseline; 25 | } 26 | 27 | .header li { 28 | margin: 0 1rem; 29 | } 30 | 31 | .header a { 32 | text-decoration: none; 33 | color: white; 34 | font-weight: bold; 35 | } 36 | 37 | .header button { 38 | font: inherit; 39 | background-color: transparent; 40 | border: 1px solid white; 41 | color: white; 42 | font-weight: bold; 43 | padding: 0.5rem 1.5rem; 44 | border-radius: 6px; 45 | cursor: pointer; 46 | } 47 | 48 | .header a:hover { 49 | color: #c291e2; 50 | } 51 | 52 | .header button:hover { 53 | background-color: #c291e2; 54 | color: #38015c; 55 | } -------------------------------------------------------------------------------- /components/layout/main-navigation.js: -------------------------------------------------------------------------------- 1 | import Link from 'next/link'; 2 | import { useSession, signOut } from 'next-auth/client'; 3 | 4 | import classes from './main-navigation.module.css'; 5 | 6 | function MainNavigation() { 7 | const [session, loading] = useSession(); 8 | 9 | function logoutHandler() { 10 | signOut(); 11 | } 12 | 13 | return ( 14 |
15 | 16 | 17 |
Next Auth
18 |
19 | 20 | 39 |
40 | ); 41 | } 42 | 43 | export default MainNavigation; 44 | -------------------------------------------------------------------------------- /pages/api/auth/[...nextauth].js: -------------------------------------------------------------------------------- 1 | import NextAuth from 'next-auth'; 2 | import Providers from 'next-auth/providers'; 3 | 4 | import { verifyPassword } from '../../../lib/auth'; 5 | import { connectToDatabase } from '../../../lib/db'; 6 | 7 | export default NextAuth({ 8 | session: { 9 | jwt: true, 10 | }, 11 | providers: [ 12 | Providers.Credentials({ 13 | async authorize(credentials) { 14 | const client = await connectToDatabase(); 15 | 16 | const usersCollection = client.db().collection('users'); 17 | 18 | const user = await usersCollection.findOne({ 19 | email: credentials.email, 20 | }); 21 | 22 | if (!user) { 23 | client.close(); 24 | throw new Error('No user found!'); 25 | } 26 | 27 | const isValid = await verifyPassword( 28 | credentials.password, 29 | user.password 30 | ); 31 | 32 | if (!isValid) { 33 | client.close(); 34 | throw new Error('Could not log you in!'); 35 | } 36 | 37 | client.close(); 38 | return { email: user.email }; 39 | 40 | }, 41 | }), 42 | ], 43 | }); 44 | -------------------------------------------------------------------------------- /components/profile/user-profile.js: -------------------------------------------------------------------------------- 1 | import ProfileForm from './profile-form'; 2 | import classes from './user-profile.module.css'; 3 | 4 | function UserProfile() { 5 | // const [isLoading, setIsLoading] = useState(true); 6 | 7 | // useEffect(() => { 8 | // getSession().then((session) => { 9 | // if (!session) { 10 | // window.location.href = '/auth'; 11 | // } else { 12 | // setIsLoading(false); 13 | // } 14 | // }); 15 | // }, []); 16 | 17 | // if (isLoading) { 18 | // return

Loading...

; 19 | // } 20 | 21 | async function changePasswordHandler(passwordData) { 22 | const response = await fetch('/api/user/change-password', { 23 | method: 'PATCH', 24 | body: JSON.stringify(passwordData), 25 | headers: { 26 | 'Content-Type': 'application/json' 27 | } 28 | }); 29 | 30 | const data = await response.json(); 31 | 32 | console.log(data); 33 | } 34 | 35 | return ( 36 |
37 |

Your User Profile

38 | 39 |
40 | ); 41 | } 42 | 43 | export default UserProfile; 44 | -------------------------------------------------------------------------------- /pages/api/auth/signup.js: -------------------------------------------------------------------------------- 1 | import { hashPassword } from '../../../lib/auth'; 2 | import { connectToDatabase } from '../../../lib/db'; 3 | 4 | async function handler(req, res) { 5 | if (req.method !== 'POST') { 6 | return; 7 | } 8 | 9 | const data = req.body; 10 | 11 | const { email, password } = data; 12 | 13 | if ( 14 | !email || 15 | !email.includes('@') || 16 | !password || 17 | password.trim().length < 7 18 | ) { 19 | res.status(422).json({ 20 | message: 21 | 'Invalid input - password should also be at least 7 characters long.', 22 | }); 23 | return; 24 | } 25 | 26 | const client = await connectToDatabase(); 27 | 28 | const db = client.db(); 29 | 30 | const existingUser = await db.collection('users').findOne({ email: email }); 31 | 32 | if (existingUser) { 33 | res.status(422).json({ message: 'User exists already!' }); 34 | client.close(); 35 | return; 36 | } 37 | 38 | const hashedPassword = await hashPassword(password); 39 | 40 | const result = await db.collection('users').insertOne({ 41 | email: email, 42 | password: hashedPassword, 43 | }); 44 | 45 | res.status(201).json({ message: 'Created user!' }); 46 | client.close(); 47 | } 48 | 49 | export default handler; 50 | -------------------------------------------------------------------------------- /components/profile/profile-form.js: -------------------------------------------------------------------------------- 1 | import { useRef } from 'react'; 2 | 3 | import classes from './profile-form.module.css'; 4 | 5 | function ProfileForm(props) { 6 | const oldPasswordRef = useRef(); 7 | const newPasswordRef = useRef(); 8 | 9 | function submitHandler(event) { 10 | event.preventDefault(); 11 | 12 | const enteredOldPassword = oldPasswordRef.current.value; 13 | const enteredNewPassword = newPasswordRef.current.value; 14 | 15 | // optional: Add validation 16 | 17 | props.onChangePassword({ 18 | oldPassword: enteredOldPassword, 19 | newPassword: enteredNewPassword 20 | }); 21 | } 22 | 23 | return ( 24 |
25 |
26 | 27 | 28 |
29 |
30 | 31 | 32 |
33 |
34 | 35 |
36 |
37 | ); 38 | } 39 | 40 | export default ProfileForm; 41 | -------------------------------------------------------------------------------- /components/auth/auth-form.module.css: -------------------------------------------------------------------------------- 1 | .auth { 2 | margin: 3rem auto; 3 | width: 95%; 4 | max-width: 25rem; 5 | border-radius: 6px; 6 | background-color: #38015c; 7 | box-shadow: 0 1px 4px rgba(0, 0, 0, 0.2); 8 | padding: 1rem; 9 | text-align: center; 10 | } 11 | 12 | .auth h1 { 13 | text-align: center; 14 | color: white; 15 | } 16 | 17 | .control { 18 | margin-bottom: 0.5rem; 19 | } 20 | 21 | .control label { 22 | display: block; 23 | color: white; 24 | font-weight: bold; 25 | margin-bottom: 0.5rem; 26 | } 27 | 28 | .control input { 29 | font: inherit; 30 | background-color: #f1e1fc; 31 | color: #38015c; 32 | border-radius: 4px; 33 | border: 1px solid white; 34 | width: 100%; 35 | text-align: left; 36 | padding: 0.25rem; 37 | } 38 | 39 | .actions { 40 | margin-top: 1.5rem; 41 | display: flex; 42 | flex-direction: column; 43 | align-items: center; 44 | } 45 | 46 | .actions button { 47 | cursor: pointer; 48 | font: inherit; 49 | color: white; 50 | background-color: #9f5ccc; 51 | border: 1px solid #9f5ccc; 52 | border-radius: 4px; 53 | padding: 0.5rem 2.5rem; 54 | } 55 | 56 | .actions button:hover { 57 | background-color: #873abb; 58 | border-color: #873abb; 59 | } 60 | 61 | .actions .toggle { 62 | margin-top: 1rem; 63 | background-color: transparent; 64 | color: #9f5ccc; 65 | border: none; 66 | padding: 0.15rem 1.5rem; 67 | } 68 | 69 | .actions .toggle:hover { 70 | background-color: transparent; 71 | color: #ae82cc; 72 | } -------------------------------------------------------------------------------- /pages/api/user/change-password.js: -------------------------------------------------------------------------------- 1 | import { getSession } from 'next-auth/client'; 2 | 3 | import { hashPassword, verifyPassword } from '../../../lib/auth'; 4 | import { connectToDatabase } from '../../../lib/db'; 5 | 6 | async function handler(req, res) { 7 | if (req.method !== 'PATCH') { 8 | return; 9 | } 10 | 11 | const session = await getSession({ req: req }); 12 | 13 | if (!session) { 14 | res.status(401).json({ message: 'Not authenticated!' }); 15 | return; 16 | } 17 | 18 | const userEmail = session.user.email; 19 | const oldPassword = req.body.oldPassword; 20 | const newPassword = req.body.newPassword; 21 | 22 | const client = await connectToDatabase(); 23 | 24 | const usersCollection = client.db().collection('users'); 25 | 26 | const user = await usersCollection.findOne({ email: userEmail }); 27 | 28 | if (!user) { 29 | res.status(404).json({ message: 'User not found.' }); 30 | client.close(); 31 | return; 32 | } 33 | 34 | const currentPassword = user.password; 35 | 36 | const passwordsAreEqual = await verifyPassword(oldPassword, currentPassword); 37 | 38 | if (!passwordsAreEqual) { 39 | res.status(403).json({ message: 'Invalid password.' }); 40 | client.close(); 41 | return; 42 | } 43 | 44 | const hashedPassword = await hashPassword(newPassword); 45 | 46 | const result = await usersCollection.updateOne( 47 | { email: userEmail }, 48 | { $set: { password: hashedPassword } } 49 | ); 50 | 51 | client.close(); 52 | res.status(200).json({ message: 'Password updated!' }); 53 | } 54 | 55 | export default handler; 56 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | 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). 2 | 3 | ## Getting Started 4 | 5 | First, run the development server: 6 | 7 | ```bash 8 | npm run dev 9 | # or 10 | yarn dev 11 | ``` 12 | 13 | Open [http://localhost:3000](http://localhost:3000) with your browser to see the result. 14 | 15 | You can start editing the page by modifying `pages/index.js`. The page auto-updates as you edit the file. 16 | 17 | [API routes](https://nextjs.org/docs/api-routes/introduction) can be accessed on [http://localhost:3000/api/hello](http://localhost:3000/api/hello). This endpoint can be edited in `pages/api/hello.js`. 18 | 19 | The `pages/api` directory is mapped to `/api/*`. Files in this directory are treated as [API routes](https://nextjs.org/docs/api-routes/introduction) instead of React pages. 20 | 21 | ## Learn More 22 | 23 | To learn more about Next.js, take a look at the following resources: 24 | 25 | - [Next.js Documentation](https://nextjs.org/docs) - learn about Next.js features and API. 26 | - [Learn Next.js](https://nextjs.org/learn) - an interactive Next.js tutorial. 27 | 28 | You can check out [the Next.js GitHub repository](https://github.com/vercel/next.js/) - your feedback and contributions are welcome! 29 | 30 | ## Deploy on Vercel 31 | 32 | 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. 33 | 34 | Check out our [Next.js deployment documentation](https://nextjs.org/docs/deployment) for more details. 35 | -------------------------------------------------------------------------------- /components/auth/auth-form.js: -------------------------------------------------------------------------------- 1 | import { useState, useRef } from 'react'; 2 | import { signIn } from 'next-auth/client'; 3 | import { useRouter } from 'next/router'; 4 | 5 | import classes from './auth-form.module.css'; 6 | 7 | async function createUser(email, password) { 8 | const response = await fetch('/api/auth/signup', { 9 | method: 'POST', 10 | body: JSON.stringify({ email, password }), 11 | headers: { 12 | 'Content-Type': 'application/json', 13 | }, 14 | }); 15 | 16 | const data = await response.json(); 17 | 18 | if (!response.ok) { 19 | throw new Error(data.message || 'Something went wrong!'); 20 | } 21 | 22 | return data; 23 | } 24 | 25 | function AuthForm() { 26 | const emailInputRef = useRef(); 27 | const passwordInputRef = useRef(); 28 | 29 | const [isLogin, setIsLogin] = useState(true); 30 | const router = useRouter(); 31 | 32 | function switchAuthModeHandler() { 33 | setIsLogin((prevState) => !prevState); 34 | } 35 | 36 | async function submitHandler(event) { 37 | event.preventDefault(); 38 | 39 | const enteredEmail = emailInputRef.current.value; 40 | const enteredPassword = passwordInputRef.current.value; 41 | 42 | // optional: Add validation 43 | 44 | if (isLogin) { 45 | const result = await signIn('credentials', { 46 | redirect: false, 47 | email: enteredEmail, 48 | password: enteredPassword, 49 | }); 50 | 51 | if (!result.error) { 52 | // set some auth state 53 | router.replace('/profile'); 54 | } 55 | } else { 56 | try { 57 | const result = await createUser(enteredEmail, enteredPassword); 58 | console.log(result); 59 | } catch (error) { 60 | console.log(error); 61 | } 62 | } 63 | } 64 | 65 | return ( 66 |
67 |

{isLogin ? 'Login' : 'Sign Up'}

68 |
69 |
70 | 71 | 72 |
73 |
74 | 75 | 81 |
82 |
83 | 84 | 91 |
92 |
93 |
94 | ); 95 | } 96 | 97 | export default AuthForm; 98 | --------------------------------------------------------------------------------