├── .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 |
--------------------------------------------------------------------------------
/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 |
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 |
93 |
94 | );
95 | }
96 |
97 | export default AuthForm;
98 |
--------------------------------------------------------------------------------