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.