Points of interest
Server
server/apollo/resolvers.js
server/apollo/schema.js
server/index.js
Server auth steps
First things first we will need to create a .env file to hold onto a couple things.
For now all we're going to need a is a JWT secret key to attach to all JWT's created.
[ ] Create Apollo resolver Mutations & Queries to support Register / Login / Logout flow
register: async (_, args, { prisma }) => {
const hashedPassword = await bcrypt.hash(args.password, 10);
// here you can see we are using the prisma off of the context that we
// set earlier in the ApolloClient instance. The method createUser()
// comes from the created mutation from prisma.
const newUser = await prisma.createUser({
username: args.username,
email: args.email,
password: hashedPassword
});
return newUser;
}
login: async (_, args, { prisma }) => {
// Check if the user exists using the email coming from the args
const user = await prisma.user({
email: args.email
});
// if not throw the error
if (!user) {
throw new Error("Invalid Login");
}
// We then want to use bcrypt to compare the passsword to the hashed password
const passwordMatch = await bcrypt.compare(args.password, user.password);
// if not throw an error as well
if (!passwordMatch) {
throw new Error("Invalid password");
}
// This token will let us hold a session inside of the client by creating a signature
// that is unique to the user logging in
const token = jwt.sign(
{
id: user.id,
email: args.email
},
process.env.JWT_SECRET,
{
expiresIn: "30d"
}
);
// return the user AND the token
return {
token,
user
};
},
logout: (_, args, context) => {
return { message: "Logged out!" };
},
[ ] Now we need to create supporting methods inside of the schema, so the ApolloServer knows what it's calling. Inside of server/apollo/schema.js
we need to add the mutations, queries, and some types
type User {
id: ID!
username: String!
email: String!
password: String!
playlists: [Playlist]
}
type LoginResponse {
token: String
user: User
}
type SuccessMessage {
message: String
}
type Query {
currentUser: User!
}
type Mutation {
## User Mutations
register(username: String!, password: String!, email: String!): User!
login(username: String, email: String, password: String!): LoginResponse!
logout: SuccessMessage
}
LoginResponse
and SuccessMessage
types allow us to send specific responses after the mutation is completed such as the token, user, and message we want to send on success.[ ] No all that's left to do on the server side is configure passing the authenticated user through the ApolloServer context along with the generated prisma client, so that the earlier mutations will work
// we need to bring in JWT so we can do some verifying
const jwt = require("jsonwebtoken");
// When that token is created, we need to hold onto it so we can create a session
const getUser = token => {
try {
if (token) {
return jwt.verify(token, process.env.JWT_SECRET);
}
return null;
} catch (err) {
return null;
}
};
const server = new ApolloServer({
...
// This is what pipes through the proper context for our ApolloServer resolvers
context: async ({ req }) => {
// here we create the Bearer header that we can store on the client side
const tokenWithBearer = (await req.headers.authorization) || "";
const token = tokenWithBearer.split(" ")[1];
const user = getUser(token);
// we now have access to the current user, to make sure all the resolvers
// that use it are pointed at the proper user
return {
user,
prisma
};
}
})
Points of interest
Client
We will need to create the following components —
RegistrationForm.js
LoginForm.js
LogoutButton.js
Landing.js
Other POI —
Routes.js
NoAuthLanding.js
CurrentUser.js
[ ] First we should go into our client's index.js
and plug our session setter there. This way we can move into the components.
import { setContext } from "apollo-link-context";
import { HttpLink } from "apollo-link-http";
// This allows us to utilize localstorage to decide if we should be holding
// the token we created and signed with jwt during login, and store it
const authLink = setContext((_, { headers }) => {
const token = localStorage.getItem("Authorization");
return {
headers: {
...headers,
Authorization: token ? `Bearer ${token}` : ""
}
};
});
const client = new ApolloClient({
...,
// Here we're able to send through authorization and the apollo server through
link: authLink.concat(httpLink)
})
Now inside of our client, we can set the local storage and pass that information throughout the app
[ ] Build out the components needed for the client to deal with the auth process. We will need the follow components:
Register
import React from "react";
import { useMutation } from "@apollo/react-hooks";
import gql from "graphql-tag";
import { LOGIN, saveToken } from "../LoginForm";
// First you will need to define the mutation constants.
// These will hit our endpoint and set off the resolver it's connected to
const REGISTER = gql`
mutation Register($email: String!, $username: String!, $password: String!) {
register(email: $email, username: $username, password: $password) {
id
email
password
}
}
`;
const RegistrationForm = () => {
// We will need a state to track the inputs below
const [userFields, setUserFields] = React.useState({
username: "",
email: "",
password: ""
});
// Here we make use of the apollo mutation hook `useMutation`
// this will allow us to fire the earlier mutation constant and trigger
// the resolver we defined on the server.
// It will consume a few things
// A. We pass the mutation constant as the first argument
// B. The second argument you can optionally pass to a useMutation hook
// allows you to do things like attach variables or some other included
// methods supplied to the hook like `refetchQueries` or `onCompleted`
const [register] = useMutation(REGISTER, {
onCompleted: () => {
return login({
variables: {
email: userFields.email,
password: userFields.password
}
});
}
});
return (
<form
onSubmit={e => {
e.preventDefault();
// Instead of passing the variables straight to the hook, we also
// have the opportunity to send the variables through as an arg
// to the function we got off of the hook.
return register({
variables: {
username: userFields.username,
email: userFields.email,
password: userFields.password
}
});
}}
>
// Below we just use our state setter to track the user input
<input
onChange={e =>
setUserFields({ ...userFields, username: e.target.value })
}
placeholder="Username"
/>
<input
onChange={e => setUserFields({ ...userFields, email: e.target.value })}
placeholder="email"
/>
<input
onChange={e =>
setUserFields({ ...userFields, password: e.target.value })
}
placeholder="password"
/>
<button>Finish</button>
</form>
);
};
export default RegistrationForm;
LoginForm
import React from "react";
import { useMutation } from "@apollo/react-hooks";
import gql from "graphql-tag";
// mutation constant
// We need to export this for the signup component as well
export const LOGIN = gql`
mutation Login($email: String!, $password: String!) {
login(email: $email, password: $password) {
token
user {
username
id
}
}
}
`;
// We're going to need a function to deal with setting
// the local storage that will be consumed by the ApolloClient. We also
// need to export it so our RegistrationForm can make use of these after
// a signup occurs.
export const saveToken = token => {
localStorage.setItem("Authorization", token);
return window.location.reload();
};
const LoginForm = () => {
// 2. We will need a state that tracks both the inputs
// for the email and password
const [loginValues, setLoginValues] = React.useState({
email: "",
password: ""
});
// We now call the useMutation which will consume a few things for us
// A. We pass the mutation constant as the first argument
// B. The second argument you can optionally pass to a useMutation hook
// allows you to do things like attach variables or some other included
// methods supplied to the hook like `refetchQueries` or `onCompleted`
const [login] = useMutation(LOGIN, {
onCompleted: data => {
return saveToken(data.login.token);
}
});
return (
<form
onSubmit={e => {
e.preventDefault();
// using the provided method `login` from above
// (from the useMutation) we can now call it and send the mutation
// over to the resolver. Like I said above, variables can be
// supplied to the mutation as well. Here we are supplying them
// inside of the function call since, we needed acess to user
// interaction ie the input state
login({
variables: {
email: loginValues.email,
password: loginValues.password
}
});
}}
>
// Below we just call our state setter to track the state we will
// need to submit to the mutation
<input
onChange={e =>
setLoginValues({ ...loginValues, email: e.target.value })
}
/>
<input
onChange={e =>
setLoginValues({ ...loginValues, password: e.target.value })
}
/>
<button>Login</button>
</form>
);
};
export default LoginForm;
LogoutButton
import React from "react";
import gql from "graphql-tag";
import { useMutation } from "@apollo/react-hooks";
// Mutation constant
const LOGOUT = gql`
mutation {
logout {
message
}
}
`;
// We will need to run this function to clear the session that we are
// using in local storage and bring us back to our non authorized home page
const clearSession = () => {
localStorage.removeItem("Authorization");
return window.location.reload();
};
const LogoutButton = () => {
const [logout] = useMutation(LOGOUT, {
onCompleted: () => {
return clearSession();
}
});
return <button onClick={logout}>Logout</button>;
};
export default LogoutButton;
We're going to create a CurrentUser
file to hold onto and export the CURRENT_USER
query since it will be used many times throughout the app.
CurrentUser
import gql from "graphql-tag";
const CURRENT_USER = gql`
query {
currentUser {
username
id
playlists {
id
title
media {
title
imdbRating
}
}
}
}
`;
export default CURRENT_USER;
Routes
We need to make a change to the render of the Routes components
import CURRENT_USER from "../components/Auth/CurrentUser";
import { useQuery } from "@apollo/react-hooks";
const Routes = () => {
// here we pass the query constant into the useQuery hook giving us access
// to the data & loading states of the Query
const { data, loading } = useQuery(CURRENT_USER);
if (loading) return <p>Loading..</p>;
return (
<Router>
<Switch>
// then we hook into the data to pass the currentUser and it's data through
{data && data.currentUser ? (
<Route
exact
path="/"
render={() => <Landing currentUser={data.currentUser} />}
/>
) : (
<Route path="/" render={NoAuthLanding} />
)}
</Switch>
</Router>
);
};
Landing
We are now in need of two different components to render as our home page due to having to think about auth v no auth
import React from 'react';
import LogoutButton from '../Auth/LogoutButton';
const Landing = ({ currentUser }) => {
return (
<div>
<LogoutButton />
<p>Hello {currentUser.username}</p>
</div>
)
}
export default Landing;
NoAuthLanding
Next we need to add the forms to the no auth landing page.
import React from "react";
import RegistrationForm from "../Auth/RegistrationForm";
import LoginForm from "../Auth/LoginForm";
const NoAuthLanding = () => {
return (
<div>
<RegistrationForm />
<LoginForm />
</div>
);
};
export default NoAuthLanding;
We now have a fully functional authentication flow!