Next.js (Part 3)

Neil HaddleyNovember 7, 2021

API routes and next-auth

Get Request

API routes let you create an API endpoint inside a Next.js app.

To create an API endpoint inside a Next.js app add a file (or folder) to the /pages/api directory

Get Request

Get Request

Dynamic API routes

API routes can be dynamic, just like regular Next.js pages

Dynamic API Route

Dynamic API Route

Unprotected pages

User login is not required to access all of the pages.

Unprotected page

Unprotected page

next-auth

next-auth can be used to prevent unauthorized access to protected api methods and protected pages.

$npm install next-auth

GitHub Id and GitHub Secret

Here I used GitHub as an authentication provider.

GitHub Developer Settings

GitHub Developer Settings

Register a new OAuth application

Register a new OAuth application

Note the Client ID and Client Secret

Note the Client ID and Client Secret

.env

Maintain the Client ID and Client Secret values in an .env file

[...nextauth]

Add a [...nextauth] api method

_app.js

Add a 'next-auth/client' Provider to _app.js

Provider

Provider

Add code to prevent unauthorized access

Ensure that a valid session exists before returning articles or article details (using a REST API call or a web page request)

/api/articles is now protected

/api/articles is now protected

/api/articles/4 is now protected

/api/articles/4 is now protected

page /protected is now protected (server-side)

page /protected is now protected (server-side)

page /protected is now protected (client-side)

page /protected is now protected (client-side)

Adding login and logout to home page

Adding "Sign In" and "Sign Out" to home page.

Sign in

Sign in

Sign in with GitHub provider

Sign in with GitHub provider

Signed in

Signed in

authorized

authorized

authorized

authorized

authorized

authorized

next-auth database (optional)

Specifying a database is optional if you don't need to persist user data or support email sign in. If you don't specify a database then JSON Web Tokens will be enabled for session storage and used to store session data.

To specify a database update the [...nextauth].js file (and a few environment variables).

accounts

accounts

users

users

session (with user id and provider)

session (with user id and provider)

pages

pages

articles.js and pagesapiarticlesindex.js

TEXT
1// articles.js
2
3export const articles = [
4  {
5    "userId": 1,
6    "id": 1,
7    "title": "sunt aut facere repellat provident occaecati excepturi optio reprehenderit",
8    "body": "quia et suscipit\nsuscipit recusandae consequuntur expedita et cum\nreprehenderit molestiae ut ut quas totam\nnostrum rerum est autem sunt rem eveniet architecto"
9  },
10  {
11    "userId": 1,
12    "id": 2,
13    "title": "qui est esse",
14    "body": "est rerum tempore vitae\nsequi sint nihil reprehenderit dolor beatae ea dolores neque\nfugiat blanditiis voluptate porro vel nihil molestiae ut reiciendis\nqui aperiam non debitis possimus qui neque nisi nulla"
15  }
16]
17
18// /pages/api/articles/index.js
19import { articles } from '../../../articles'
20
21const index = async (req, res) => {
22
23    res.status(200).json(articles)
24
25}
26
27export default index

pagesapiarticlesid.js

TEXT
1// /pages/api/articles/[id].js
2import {articles} from '../../../articles'
3
4const article = async (req, res) => {
5
6    const { query: { id } } = req
7
8    const article = articles.find(a => a.id.toString() === id)
9
10    if (!article) {
11        res.status(404).json({ message: `article ${id} not found` })
12    }
13
14    res.status(200).json(article)
15
16}
17
18export default article

pagesunprotected

TEXT
1import { articles } from '../articles'
2
3function index() {
4
5    return (
6        <div>
7            <ul>
8                {articles.map(article => (<li key={article.id}>{article.title}</li>))}
9            </ul>
10        </div>
11    )
12}
13
14export default index

.env

TEXT
1//.env
2NEXTAUTH_GITHUB_ID=e95a2816a93e7daabc6c
3NEXTAUTH_GITHUB_SECRET=<secret>

apiauth...nextauth.js

TEXT
1// /api/auth/[...nextauth].js
2import NextAuth from "next-auth";
3import Providers from "next-auth/providers";
4
5const options = {
6    // @link https://next-auth.js.org/configuration/providers
7    providers: [
8        Providers.GitHub({
9            clientId: process.env.NEXTAUTH_GITHUB_ID,
10            clientSecret: process.env.NEXTAUTH_GITHUB_SECRET,            
11        }),
12        /*Providers.Google({
13            clientId: process.env.NEXTAUTH_GOOGLE_ID,
14            clientSecret: process.env.NEXTAUTH_GOOGLE_SECRET,
15        }),
16        Providers.Facebook({
17            clientId: process.env.NEXTAUTH_FACEBOOK_ID,
18            clientSecret: process.env.NEXTAUTH_FACEBOOK_SECRET,
19        }),*/
20    ]
21}
22
23export default (req,res) => NextAuth(req,res,options)

_app.js

TEXT
1import { createGlobalStyle, ThemeProvider } from 'styled-components'
2import { Provider } from "next-auth/client";
3
4const GlobalStyle = createGlobalStyle`
5  body {
6    margin: 0;
7    padding: 0;
8    box-sizing: border-box;
9  }
10`
11
12const theme = {
13    colors: {
14        primary: '#0070f3',
15    },
16}
17
18export default function App({ Component, pageProps }) {
19    return (
20        <>
21            <Provider session={pageProps.session}>
22                <GlobalStyle />
23                <ThemeProvider theme={theme}>
24                    <Component {...pageProps} />
25                </ThemeProvider>
26            </Provider>
27        </>
28    )
29}

pagesapiarticlesindex.js updated

TEXT
1import { articles } from '../../../articles'
2import { getSession } from 'next-auth/client'
3
4const index = async (req, res) => {
5
6    const session = await getSession({ req });
7    if (session) {
8        res.status(200).json(articles)
9    } else {
10        res.status(401).json({ message: `please log in` })
11    }
12
13}
14
15export default index

pagesapiarticlesid.js updated

TEXT
1import { articles } from '../../../articles'
2import { getSession } from 'next-auth/client'
3
4const article = async (req, res) => {
5
6    const session = await getSession({ req });
7    if (session) {
8        const { query: { id } } = req
9        const article = articles.find(a => a.id.toString() === id)
10        if (!article) {
11            res.status(404).json({ message: `article ${id} not found` })
12        }
13        res.status(200).json(article)
14    } else {
15        res.status(401).json({ message: `please log in` })
16    }
17
18}
19
20export default article

pagesprotected.js

TEXT
1import { getSession } from "next-auth/client"
2import { articles } from '../articles'
3
4function index({ session, articles }) {
5
6    // If no session exists, display access denied message
7    if (!session) { return (<div>You need to be logged on</div>) }
8
9    return (
10        <div>
11            <ul>
12                {articles.map(article => (<li key={article.id}>{article.title}</li>))}
13            </ul>
14        </div>
15    )
16}
17
18export default index
19
20export const getServerSideProps = async (context) => {
21
22    const session = await getSession(context);
23
24    // If no session exists, display access denied message
25    if (!session) {
26        return {
27            props: {
28                session
29            }
30        }
31    }
32
33    return {
34        props: {
35            session,
36            articles
37        }
38    }
39}

pagesprotected.js

TEXT
1import { useSession } from "next-auth/client"
2import useSWR from 'swr'
3
4function index() {
5    
6    const [ session, loading ] = useSession()
7
8  // When rendering client side don't display anything until loading is complete
9  if (typeof window !== 'undefined' && loading) return null
10
11  // If no session exists, display access denied message
12  if (!session) { return  (<div>You need to be logged on</div>) }
13
14
15  const fetcher = (...args) => fetch(...args).then(res => res.json())
16
17    const { data, error } = useSWR('/api/articles', fetcher)
18
19    if (error) return <div>failed to load</div>
20    if (!data) return <div>loading...</div>
21    
22    return (
23        <div>
24            <ul>
25                {data.map(article => (<li key={article.id}>{article.title}</li>))}
26            </ul>
27        </div>
28    )
29
30}
31
32export default index

pagesindex.js

TEXT
1import { signIn, signOut, useSession } from 'next-auth/client'
2
3export default function Home() {
4  const [session, loading] = useSession()
5
6  return (
7    <>
8      {!session && (<>
9        Not signed in
10        <br />
11        <button onClick={signIn}>Sign In</button>
12      </>)}
13      {session && (<>
14        Signed in as {session.user.email}
15        <br />
16        <button onClick={signOut}>Sign Out</button>
17      </>)}
18    </>
19  )
20}

...nextauth.js

TEXT
1import NextAuth from "next-auth";
2import { session } from "next-auth/client";
3import Providers from "next-auth/providers";
4
5const options = {
6    // @link https://next-auth.js.org/configuration/providers
7    providers: [
8        Providers.GitHub({
9            clientId: process.env.NEXTAUTH_GITHUB_ID,
10            clientSecret: process.env.NEXTAUTH_GITHUB_SECRET,
11        }),
12        /*Providers.Google({
13            clientId: process.env.NEXTAUTH_GOOGLE_ID,
14            clientSecret: process.env.NEXTAUTH_GOOGLE_SECRET,
15        }),
16        Providers.Facebook({
17            clientId: process.env.NEXTAUTH_FACEBOOK_ID,
18            clientSecret: process.env.NEXTAUTH_FACEBOOK_SECRET,
19        }),*/
20    ],
21    database: process.env.DB_URL,
22    session: {
23        jwt: true
24    },
25    jwt: {
26        secret: process.env.JWT_SECRET
27    },
28    callbacks: {
29        async jwt(token, user, account, profile, isNewUser) {
30            console.log("token", token)
31            console.log("user", user)
32            console.log("account", account)
33            console.log("profile", profile)
34            console.log("isNewUser", isNewUser)
35            if (user && user.id) {
36                token.id = user.id
37            }
38            if (account && account.provider) {
39                token.provider = account.provider
40            }
41            return token
42        },
43        async session(session, token) {
44            session.user.id = token.id
45            session.user.provider = token.provider
46            return session
47        }
48    }
49}
50
51export default (req, res) => NextAuth(req, res, options)

.env

TEXT
1NEXTAUTH_GITHUB_ID=e95a2816a93e7daabc6c
2NEXTAUTH_GITHUB_SECRET=<secret>
3
4DB_USER=<user>
5DB_PASSWORD=<password>
6DB_URL=mongodb+srv://$DB_USER:$DB_PASSWORD@cluster0.gdnd5.mongodb.net/nextauthDB?retryWrites=true&w=majority
7JWT_SECRET=<secret>
8NEXTAUTH_URL=http://localhost:3000