Chapter 6: Authentication

Strongly Typed Next.js

link Auth Flow

By the end of this lesson, developers will be able to:

  • Create components and pages to handle data fetching
  • Create an authentication flow for login and registration

Introduction

With the Apollo Client initialized, we can start writing functional components with hooks. We will start with our first context provider: AuthProvider.

After creating the AuthProvider, we will implement authetication flow screens and allow users to login, register and sign out.

Following authentication, we will continue with the streaming flow and allow users to create and view their streams.

link Auth Provider

The purpose of the AuthProvider is to create a global user object, and handle any authentication logic with a single hook.

Once we declare the AuthProvider, you may notice that authentication becomes much quicker to implement.

Let's begin with a new file, app/lib/useAuth.tsx:

import { useState, useContext, createContext, useEffect } from 'react';
import { useApolloClient } from '@apollo/client';
import { useSignInMutation } from 'lib/graphql/signin.graphql';
import { useSignUpMutation } from 'lib/graphql/signup.graphql';
import { useCurrentUserQuery } from 'lib/graphql/currentUser.graphql';
import { useRouter } from 'next/router';

type AuthProps = {
  user: any;
  error: string;
  signIn: (email: any, password: any) => Promise<void>;
  signUp: (email: any, password: any) => Promise<void>;
  signOut: () => void;
}
const AuthContext = createContext<Partial<AuthProps>>({});

// You can wrap your _app.js with this provider
export function AuthProvider({ children }) {
  const auth = useProvideAuth();
  return <AuthContext.Provider value={auth}>{children}</AuthContext.Provider>;
}

// Custom React hook to access the context
export const useAuth = () => {
  return useContext(AuthContext);
};

function useProvideAuth() {
  const client = useApolloClient();
  const router = useRouter();

  const [error, setError] = useState('');
  const { data } = useCurrentUserQuery({
    fetchPolicy: 'network-only',
    errorPolicy: 'ignore', 
  });
  const user = data && data.currentUser;

  // Signing In
  const [signInMutation] = useSignInMutation();
  // Signing Up
  const [signUpMutation] = useSignUpMutation();

  const signIn = async (email, password) => {
    try {
      const { data } = await signInMutation({ variables: { email, password } });
      if (data.login.token && data.login.user) {
        sessionStorage.setItem('token', data.login.token);
        client.resetStore().then(() => {
          router.push('/');
        });
      } else {
        setError("Invalid Login");
      }
    } catch (err) {
      setError(err.message);
    }
  }

  const signUp = async (email, password) => {
    try {
      const { data } = await signUpMutation({ variables: { email, password } });
      if (data.register.token && data.register.user) {
        sessionStorage.setItem('token', data.register.token);
        client.resetStore().then(() => {
          router.push('/');
        });
      } else {
        setError("Invalid Login");
      }
    } catch (err) {
      setError(err.message);
    }
  }

  const signOut = () => {
    sessionStorage.removeItem('token');
    client.resetStore().then(() => {
      router.push('/');
    });
  }

  return {
    user,
    error,
    signIn,
    signUp,
    signOut,
  };
} 

The above code snippet creates a global auth context called AuthContext, but to access it's props we need to wrap the root app/pages/_app.tsx file:

import { AuthProvider } from 'lib/useAuth';

/* ... */

  return (
    <ApolloProvider client={apolloClient}>
      <ThemeProvider theme={darkState ? themeDark : themeLight}>
        <CssBaseline />
        <AuthProvider>
          <Component {...pageProps} />
        </AuthProvider>
      </ThemeProvider>
    </ApolloProvider>
  );

Now we can access the global user object using the useAuth hook. Let's incorporate the useAuth hook with a new Header component.

link Header Component

First, create a new directory and file, called app/components and app/components/Header.tsx:

import React from 'react';
import { makeStyles, Theme } from '@material-ui/core/styles';
import {
  AppBar,
  Toolbar,
  Typography,
  Button,
  Link as LinkText,
  Switch,
} from '@material-ui/core';
import Link from 'next/link';
import { useAuth } from 'lib/useAuth';

export default function Header({ darkState, handleThemeChange }) {
  const classes = useStyles();
  const { user } = useAuth();

  const links = [
    !user && { label: 'Sign Up', href: '/auth/signup' },
    !user && { label: 'Sign In', href: '/auth/signin' },
    user && { label: 'Create', href: '/streams/new' },
    user && { label: 'Sign Out', href: '/auth/signout' },
  ]
    .filter((link) => link)
    .map(({ label, href }) => {
      return (
        <Link href={href} key={href}>
          <Button color="inherit">{label}</Button>
        </Link>
      );
    });

  return (
    <div className={classes.root}>
      <AppBar position="static">
        <Toolbar>
          <Typography variant="h6" className={classes.title}>
            <Link href="/">
              <LinkText href="" color="inherit">
                Stream.me
              </LinkText>
            </Link>
          </Typography>
          <Switch checked={darkState} onChange={handleThemeChange} />
          {links}
        </Toolbar>
      </AppBar>
    </div>
  );
}

const useStyles = makeStyles((theme: Theme) => ({
  root: {
    flexGrow: 1,
  },
  menuButton: {
    marginRight: theme.spacing(2),
  },
  title: {
    flexGrow: 1,
  },
  list: {
    width: 250,
  },
}));

The Header will be displayed globally, so we can include it in the app/pages/_app.tsx component:

Import and render the Header component inside app/pages/_app.tsx:

import Header from 'components/Header';

/* ... */

  return (
    <ApolloProvider client={apolloClient}>
      <ThemeProvider theme={darkState ? themeDark : themeLight}>
        <CssBaseline />
        <AuthProvider>
          <Header darkState={darkState} handleThemeChange={handleThemeChange} />
          <Component {...pageProps} />
        </AuthProvider>
      </ThemeProvider>
    </ApolloProvider>
  );

Auth Screens

link Sign In

Create a new directory and file, app/pages/auth and app/pages/auth/signin, and insert the following:

import { useState } from 'react';
import Typography from '@material-ui/core/Typography';
import Container from '@material-ui/core/Container';
import TextField from '@material-ui/core/TextField';
import Box from '@material-ui/core/Box';
import Button from '@material-ui/core/Button';
import { useAuth } from 'lib/useAuth';

export default function SignIn() {
  const [email, setEmail] = useState('');
  const [password, setPassword] = useState('');
  const { error, signIn } = useAuth();

  const onSubmit = async (event) => {
    event.preventDefault();
    signIn(email, password);
  };

  return (
    <Container maxWidth="sm">
      <Box my={4}>
        <form onSubmit={onSubmit}>
          {error && <p>{error}</p>}
          <Typography variant="h4">Sign In</Typography>
          <Box pb={2.5} />
          <TextField
            value={email}
            onChange={(e) => setEmail(e.target.value)}
            className="form-control"
            label="Email"
            required
          />
          <Box pb={2.5} />
          <TextField
            value={password}
            onChange={(e) => setPassword(e.target.value)}
            type="password"
            className="form-control"
            label="Password"
            required
          />
          <Box pb={2.5} />
          <Button
            variant="contained"
            color="primary"
            size="large"
            type="submit"
          >
            Sign In
          </Button>
        </form>
      </Box>
    </Container>
  );
}

link Sign Up

Create a new file app/pages/auth/signup, and insert the following:

import { useState } from 'react';
import Typography from '@material-ui/core/Typography';
import Container from '@material-ui/core/Container';
import TextField from '@material-ui/core/TextField';
import Box from '@material-ui/core/Box';
import Button from '@material-ui/core/Button';
import { useAuth } from 'lib/useAuth';

export default function SignUp() {
  const [email, setEmail] = useState('');
  const [password, setPassword] = useState('');
  const { error, signUp } = useAuth();

  const onSubmit = async (event) => {
    event.preventDefault();
    signUp(email, password);
  };

  return (
    <Container maxWidth="sm">
      <Box my={4}>
        <form onSubmit={onSubmit}>
          {error && <p>{error}</p>}
          <Typography variant="h4">Sign Up</Typography>
          <Box pb={2.5} />
          <TextField
            value={email}
            onChange={(e) => setEmail(e.target.value)}
            className="form-control"
            label="Email"
            required
          />
          <Box pb={2.5} />
          <TextField
            value={password}
            onChange={(e) => setPassword(e.target.value)}
            type="password"
            className="form-control"
            label="Password"
            required
          />
          <Box pb={2.5} />
          <Button
            variant="contained"
            color="primary"
            size="large"
            type="submit"
          >
            Sign Up
          </Button>
        </form>
      </Box>
    </Container>
  );
}

link Sign Out

Create a new file app/pages/auth/signout.tsx, and insert the following:

import { useEffect } from 'react';
import { useAuth } from 'lib/useAuth';

export default function SignOut() {
  const { signOut } = useAuth();
  useEffect(() => {
    signOut();
  }, []);
  return <div>Signout</div>;
}

link Test Auth

It's time to fire up the Next.js app, GraphQL server and test our new authentication flow at http://localhost:3000

Heads Up! Your GraphQL API server needs to be running in order to test authentication.

npm run dev

Try reloading the Next.js application, and you will see a new header above every page. The header also displays links to Sign Up and Sign In. Let's visit and test their functionality as well.

Be sure you are able to:

  • Log in an existing user
  • Register a new user
  • Log out of the session

Congratulations!

Today we built client-side data-fetching queries and created an authentication flow. Up next, we will create the streaming flow.

link References

format_list_bulleted
help_outline