Intro
In this article we’re going to take a look at how to implement client-side authentication with Vue3 + Vite + Apollo + GraphQL. We will implement authentication using json web tokens and make it reactive so that our application can react to authentication state changes. As the focus of this article is on the frontend we will use a very basic GraphQL mock auth server that just satisfies our minimal requirements of signing users up and in and validating jwts.
Before you start make sure that you have NodeJS of version at least 14.18.0
installed as it is a requirement for projects running on vite@3.1.0
or newer.
Setup
Scaffold a new project using vite
Let’s start by creating an empty Vue 3 project
yarn create vite
Pick Vue
as framework and typescript
as variant. We can run our application using yarn dev
command.
We will add 2 pages to our application. Home
page will be available only to unauthenticated users and Protected
page will be accessible only by logged in users. First let’s add vue-router
yarn add vue-router@4
Then create src/plugins/router.ts
file.
import { createRouter, createWebHistory } from "vue-router";
const router = createRouter({
history: createWebHistory(),
routes: [
{
path: "/",
component: () => import("@/components/pages/Home.vue"),
},
{
path: "/protected",
component: () => import("@/components/pages/Protected.vue"),
},
],
});
export default router;
And update src/main.ts
and src/App.vue
import { createApp, provide, h } from "vue";
import "./style.css";
import App from "./App.vue";
import router from "@/plugins/router";
const app = createApp(App);
app.use(router);
app.mount("#app");
<template>
<RouterView />
</template>
Home page will contain an authentication form with email and password inputs and 2 buttons: Sign Up
and Sign In
. Let’s prepare all the components for it. We’re going to use tailwindcss
framework for styling, so let’s install it.
yarn add -D tailwindcss postcss autoprefixer
npx tailwindcss init -p
Add tailwind.config.js
file with the following content:
/** @type {import('tailwindcss').Config} */
module.exports = {
content: [
"./index.html",
"./src/**/*.{vue,js,ts,jsx,tsx}",
],
theme: {
extend: {},
},
plugins: [],
}
And update src/style.css
@tailwind base;
@tailwind components;
@tailwind utilities;
Now we’re ready to create our BaseButton
and TextInput
components!
<script lang="ts" setup>
type TextInputProps = {
modelValue: string;
};
type TextInputEmits = {
(event: "update:modelValue", payload: string): void;
};
defineProps<TextInputProps>();
const emit = defineEmits<TextInputEmits>();
function onInput(e: Event) {
emit("update:modelValue", (e.target as HTMLInputElement).value);
}
</script>
<template>
<label class="block text-sm font-medium text-gray-900 dark:text-gray-300">
<slot />
<input
class="mt-2 bg-gray-50 border border-gray-300 text-gray-900 text-sm rounded focus:ring-blue-500 focus:border-blue-500 block w-full p-2.5 dark:bg-gray-700 dark:border-gray-600 dark:placeholder-gray-400 dark:text-white dark:focus:ring-blue-500 dark:focus:border-blue-500"
v-bind="$attrs"
:value="modelValue"
@input="onInput"
/>
</label>
</template>
<template>
<button
class="text-white bg-blue-700 hover:bg-blue-800 focus:ring-4 focus:ring-blue-300 font-medium rounded text-sm px-5 py-2.5 mr-2 mb-2 dark:bg-blue-600 dark:hover:bg-blue-700 focus:outline-none dark:focus:ring-blue-800"
v-bind="$attrs"
>
<slot />
</button>
</template>
Add these components to AuthForm
<script setup lang="ts">
import { reactive } from "vue";
import TextInput from "@/components/TextInput/TextInput.vue";
import BaseButton from "@/components/BaseButton/BaseButton.vue";
const form = reactive({
input: {
email: "",
password: "",
},
});
</script>
<template>
<div class="flex flex-col items-center justify-center h-screen">
<section class="p-4 bg-gray-100 rounded-lg border">
<form class="grid gap-6">
<div>
<TextInput
placeholder="john@doe.com"
v-model="form.input.email"
autofocus
>
Email:
</TextInput>
</div>
<div>
<TextInput v-model="form.input.password" type="password">
Password:
</TextInput>
</div>
<div>
<BaseButton>Sign Up</BaseButton>
<BaseButton>Sign In</BaseButton>
</div>
</form>
</section>
</div>
</template>
And use AuthForm
on our homepage:
<script setup lang="ts">
import AuthForm from "@/components/AuthForm/AuthForm.vue";
</script>
<template>
<div class="container mx-auto">
<AuthForm />
</div>
</template>
For Protected
page let’s use a placholder for now, we’re going to update it later, when we have our authentication set up.
<template>Protected page</template>
At this point, when you start your application you should see this simple form on the home page.
Preparing auth server
In order to be able to authenticate users, we will need a GraphQL auth server. The following server enables 3 GraphQL operations:
query currentUser
- it validates the jwt and returnsUser
objectmutation signUp
- creates a new user if it doesn’t exist and returns a signed jwtmutation signIn
- authenticates a user and returns a signed jwt
Additionally it runs a GraphQL sandbox on [<http://localhost:4000/graphql>](<http://localhost:4000/graphql>)
url so you can test these queries and mutations.
Create a separate folder for our server application, install dependencies and add the following code to server.js
yarn init && yarn add express express-graphql express-jwt graphql jsonwebtoken
const express = require("express");
const { graphqlHTTP } = require("express-graphql");
const { buildSchema } = require("graphql");
const jwt = require("jsonwebtoken");
const cors = require("cors");
const NOT_SO_SECRET_JWT_KEY = "shhhhh";
const fakeDb = new Map();
const schema = buildSchema(`
type User {
id: ID
email: String
}
type Token {
token: String!
}
input AuthInput {
email: String
password: String
}
type Mutation {
signUp(input: AuthInput): Token
signIn(input: AuthInput): Token
}
type Query {
currentUser: User
}
`);
const rootValue = {
signUp({ input }) {
if (!input.email) throw new Error("Email is required");
if (!input.password) throw new Error("Password is required");
if (fakeDb.get(input.email)) throw new Error("User already exists");
const newUser = {
id: fakeDb.size + 1,
email: input.email,
};
const token = jwt.sign({ email: newUser.email }, NOT_SO_SECRET_JWT_KEY);
fakeDb.set(input.email, { ...newUser, ...input });
return {
token,
};
},
signIn({ input }) {
if (!input.email) throw new Error("Email is required");
if (!input.password) throw new Error("Password is required");
const user = fakeDb.get(input.email);
if (!user) throw new Error("User not found");
if (input.password !== user.password) throw new Error("Incorrect password");
const token = jwt.sign(
{ id: user.id, email: user.email },
NOT_SO_SECRET_JWT_KEY
);
return { token };
},
currentUser(args, request) {
const token = request.headers.authorization.split(" ").pop();
if (!token) return {};
const userData = jwt.verify(token, NOT_SO_SECRET_JWT_KEY);
const user = fakeDb.get(userData.email);
if (!user) return {};
return user;
},
};
const app = express();
app.use(cors());
app.use(
"/graphql",
graphqlHTTP({
schema,
rootValue,
graphiql: true,
})
);
app.listen(4000);
console.log("Running a GraphQL API server at localhost:4000/graphql");
Now we can run this server with
node server.js
Please take a note that this is just a mock server and it is not suitable for production.
Install apollo
To integrate with GraphQL backend we’re going to use Apollo client with vue-apollo implementation in particular. Let’s head back to our Vue3 project and install the required dependencies.
yarn add graphql graphql-tag @apollo/client @vue/apollo-composable
yarn add -D @rollup/plugin-graphql
Update vite.config.ts
by adding GraphQL rollup plugin. This will allow us to write our GraphQL queries and mutations in separate files with .gql
extension
import {defineConfig} from 'vite'
import vue from '@vitejs/plugin-vue'
import graphql from '@rollup/plugin-graphql';
// <https://vitejs.dev/config/>
export default defineConfig({
plugins: [vue(), graphql()]
})
Create .env.local
file. Vite will load env variables during development from this file so we can use it to store the URL of our GraphQL endpoint. Take a note that only variables with prefix VITE_
will be exposed to your client-side code.
VITE_GRAPHQL_URL=http://localhost:4000/graphql
Create src/plugins/apollo.ts
file. We instantiate a new ApolloClient
by providing 2 options to the constructor:
link
- an instance of ApolloLink
that will hold the URL of our GraphQL endpoint, cache
- storage for all the data returned from GraphQL, in our case, it's an instance of InMemoryCache
import {
ApolloClient,
createHttpLink,
InMemoryCache,
} from "@apollo/client/core";
const httpLink = createHttpLink({
uri: import.meta.env.VITE_GRAPHQL_URL,
});
const cache = new InMemoryCache();
const apolloClient = new ApolloClient({
link: httpLink,
cache,
});
export default apolloClient;
And connect apolloClient
in main.ts
.
import { createApp, provide, h } from "vue";
import "./style.css";
import App from "./App.vue";
import { DefaultApolloClient } from "@vue/apollo-composable";
import apolloClient from "@/plugins/apollo";
import router from "@/plugins/router";
const app = createApp({
setup() {
provide(DefaultApolloClient, apolloClient);
},
render: () => h(App),
});
app.use(router);
app.mount("#app");
I also highly recommend installing Apollo Client Devtools extension. We’re not going to use it in this tutorial, but it will make it much easier to debug your GraphQL operations and see a snapshot of your Apollo cache.
Implementing authentication
Provide/inject pattern
To implement our authentication layer we will use provide/inject pattern. It allows to provide data in one component and then inject this data in any part of its components subtree. Let’s say we have the following structure:
<AuthProvider>
<ChildComponent>
<GrandChildComponent />
</ChildComponent>
<SiblingComponent>
<SiblingsChildComponent />
</SiblingComponent>
</AuthProvider>
Using provide/inject we can access data from AuthProvider
in any of its descendents without props drilling. The provided data will also be reactive.
AuthProvider
Let’s start by creating our src/providers/AuthProvider/AuthProvider.vue
component
<script lang="ts" setup>
import { AUTH_PROVIDER_KEY } from "@/constants";
provide(AUTH_PROVIDER_KEY, {});
</script>
<template>
<slot />
</template>
define AUTH_PROVIDER_KEY
in src/constants.ts
and export
export const AUTH_PROVIDER_KEY = Symbol() as InjectionKey<{}>;
Provide function accepts 2 arguments: key
(we can later inject the data using this same key) and value
. Take a note at how we define our provider key in src/constants.ts
; Casting to InjectionKey
will allow us to add correct types to the injected data.
To be able to inject auth data into our components, we’re adding a custom composable useAuth
import { inject } from "vue";
import { AUTH_PROVIDER_KEY } from "@/constants";
export function useAuth() {
const store = inject(AUTH_PROVIDER_KEY);
if (!store) {
throw new Error("Auth store has not been instantiated");
}
return store;
}
If this composable is used outside of AuthProvider
subtree then store
will be undefined
and we’ll throw an error in this case. We can now inject our auth store like this:
<script setup lang="ts">
import useAuth from "@/providers/AuthProvider/useAuth"
const authStore = useAuth() // authStore is fully typed here
</script>
Let’s also wrap our whole application with this provider. Update src/App.vue
<script lang="ts" setup>
import AuthProvider from "@/providers/AuthProvider/AuthProvider.vue";
</script>
<template>
<AuthProvider>
<RouterView />
</AuthProvider>
</template>
Now our full component tree will have access to the data from AuthProvider
Prepare graphql utils
We’re ready to start integrating with our mock auth server. But to make working with GraphQL a little easier let’s install one additional util: GraphQL Code Generator, and 2 plugins for it: typescript
and typescript-operations
. This will allow us to generate typescript types based on our GraphQL schema.
Install dependencies:
yarn add -D @graphql-codegen/cli @graphql-codegen/typescript @graphql-codegen/typescript-operations
Add codegen.yml
file with the following settings
schema: '<http://localhost:4000/graphql>'
documents: 'src/**/*.gql'
generates:
src/types/graphql.ts:
plugins:
- 'typescript'
- 'typescript-operations'
To break down options used:
schema
- it tellsgraphql-codegen
where it should take GraphQL schema from. Here, we'll specify our GraphQL endpoint.documents
- a glob pattern specifying where our queries and mutations are.graphql-codegen
will scan these files and create typescript types based on themgenerates
- location of the output types file and which plugins should be used
Since we still don’t have any client GraphQL operations, running graphql-codegen
will not provide any meaningful output. We’ll get back to it in a minute.
SignUp Mutation
Let’s start by adding a mutation to sign up a new user. Add src/providers/AuthProvider/apollo/SignUp.gql
. signUp
mutation accepts email
and password
as input and returns a json web token if the user was successfully created
mutation SignUp($input: AuthInput!) {
signUp(input: $input) {
token
}
}
Now run yarn graphql-codegen
. You will find a new file src/types/graphql.ts
with exported members SignUpMutation
and SignUpMutationVariables
, we can now use them to type our GraphQL operations. Let’s update our AuthProvider.vue
<script lang="ts" setup>
import { useMutation } from "@vue/apollo-composable";
import type {
SignUpMutation,
SignUpMutationVariables,
} from "@/types/graphql";
import SignUp from "./apollo/SignUp.gql";
import { provide } from "vue";
import { AUTH_PROVIDER_KEY } from "@/constants";
const { mutate: signUp } = useMutation<SignUpMutation, SignUpMutationVariables>(
SignUp,
{
update(cache, { data }) {
if (data?.signUp?.token) {
localStorage.setItem(LOCALSTORAGE_TOKEN_KEY, data.signUp.token);
}
},
}
);
provide(AUTH_PROVIDER_KEY, {
signUp,
});
</script>
<template>
<slot />
</template>
And update src/constants.ts
with LOCALSTORAGE_TOKEN_KEY
and a type for our signUp
function
import { ComputedRef, InjectionKey, Ref } from "vue";
import { MutateFunction } from "@vue/apollo-composable";
import {
SignUpMutation,
SignUpMutationVariables,
} from "./types/graphql";
// Provide/inject keys
export const AUTH_PROVIDER_KEY = Symbol() as InjectionKey<{
signUp: MutateFunction<SignUpMutation, SignUpMutationVariables>;
}>;
// app constants
export const LOCALSTORAGE_TOKEN_KEY = "token";
We pass additional options object with update
function to useMutation
composable. This update
function will be called after the mutation is resolved and has access to the data returned from the mutation. After we receive the response, we save our token in the localStorage
.
The standard way of using JSON web tokens is by attaching them to Authorization
header, so we're going to attach this token to every subsequent GraphQL request.
In order to do this we need to update src/plugins/apollo.ts
import { setContext } from "@apollo/client/link/context";
import {
ApolloClient,
createHttpLink,
InMemoryCache,
} from "@apollo/client/core";
import { LOCALSTORAGE_TOKEN_KEY } from "@/constants";
const authLink = setContext((_, { headers }) => {
const token = localStorage.getItem(LOCALSTORAGE_TOKEN_KEY);
return {
headers: {
...headers,
authorization: token ? `Bearer ${token}` : "",
},
};
});
const httpLink = createHttpLink({
uri: import.meta.env.VITE_GRAPHQL_URL,
});
const cache = new InMemoryCache();
const apolloClient = new ApolloClient({
link: authLink.concat(httpLink),
cache,
});
export default apolloClient;
We’ve created a series of ApolloLink
s here. You can think about ApolloLinks chain as a series of middlewares. Before our GraphQL operation hit the server it passes through a series of intermediary handlers and each of them can modify the operation in a certain way. What our authLink
does is, it looks for the token in localStorage
and if it finds one, it updates authorization
request header with it.
Let’s now use our signUp
mutation in AuthForm.vue
. We can also use SignUpMutationVariables
to corectly type our form
reactive variable.
<script setup lang="ts">
import { reactive } from "vue";
import { useAuth } from "@/providers/AuthProvider/useAuth";
import TextInput from "@/components/TextInput/TextInput.vue";
import BaseButton from "@/components/BaseButton/BaseButton.vue";
import { SignUpMutationVariables } from "@/types/graphql";
const { signUp } = useAuth();
const form = reactive<SignUpMutationVariables>({
input: {
email: "",
password: "",
},
});
</script>
<template>
<div class="flex flex-col items-center justify-center h-screen">
<section class="p-4 bg-gray-100 rounded-lg border">
<form class="grid gap-6">
<div>
<TextInput
placeholder="john@doe.com"
v-model="form.input.email"
autofocus
>Email:
</TextInput>
</div>
<div>
<TextInput v-model="form.input.password" type="password"
>Password:
</TextInput>
</div>
<div>
<BaseButton @click.prevent="signUp(form)">Sign Up</BaseButton>
<BaseButton>Sign In</BaseButton>
</div>
</form>
</section>
</div>
</template>
Now when we click on Sign Up
button we will send a gql mutation and get a token in the response
We can also see that our token is correctly saved in localStorage
CurrentUser query and SignIn mutation
Next, let’s add a new SignIn
mutation. It is very similar to signUp
as it also accepts an email and password and returns a token.
mutation SignIn($input: AuthInput!) {
signIn(input: $input) {
token
}
}
Our server also knows how to validate jwt from Authorization
header. If the token is valid it will return a user object, if it’s not, it will return an object with empty id
and email
, meaning that the user is not authenticated. Let’s add src/providers/AuthProvider/apollo/CurrentUser.gql
query to fetch current user.
query CurrentUser {
currentUser {
id
email
}
}
Run yarn graphql-codegen
again to update our types and then we can use these new operations in our AuthProvider
<script lang="ts" setup>
import { computed, provide } from "vue";
import { useMutation, useQuery } from "@vue/apollo-composable";
import type {
CurrentUserQuery,
CurrentUserQueryVariables,
SignUpMutation,
SignUpMutationVariables,
SignInMutation,
SignInMutationVariables,
} from "@/types/graphql";
import { AUTH_PROVIDER_KEY, LOCALSTORAGE_TOKEN_KEY } from "@/constants";
import CurrentUser from "./apollo/CurrentUser.gql";
import SignUp from "./apollo/SignUp.gql";
import SignIn from "./apollo/SignIn.gql";
const { result: user, loading: authLoading } = useQuery<
CurrentUserQuery,
CurrentUserQueryVariables
>(CurrentUser);
const { mutate: signUp } = useMutation<SignUpMutation, SignUpMutationVariables>(
SignUp,
{
update(cache, { data }) {
if (data?.signUp?.token) {
localStorage.setItem(LOCALSTORAGE_TOKEN_KEY, data.signUp.token);
}
},
refetchQueries: ["CurrentUser"],
}
);
const { mutate: signIn } = useMutation<SignInMutation, SignInMutationVariables>(
SignIn,
{
update(cache, { data }) {
if (data?.signIn?.token) {
localStorage.setItem(LOCALSTORAGE_TOKEN_KEY, data.signIn.token);
}
},
refetchQueries: ["CurrentUser"],
}
);
provide(AUTH_PROVIDER_KEY, {
user: computed(() => user.value?.currentUser),
isLoggedIn: computed(() => !!user.value?.currentUser?.id),
authLoading,
signUp,
signIn,
});
</script>
<template>
<slot />
</template>
And let’s also update authStore
types in src/constants.ts
import { ComputedRef, InjectionKey, Ref } from "vue";
import { MutateFunction } from "@vue/apollo-composable";
import {
CurrentUserQuery,
SignInMutation,
SignInMutationVariables,
SignUpMutation,
SignUpMutationVariables,
} from "./types/graphql";
// Provide/inject keys
export const AUTH_PROVIDER_KEY = Symbol() as InjectionKey<{
user: ComputedRef<CurrentUserQuery["currentUser"] | undefined>;
isLoggedIn: ComputedRef<boolean>;
authLoading: Ref<boolean>;
signUp: MutateFunction<SignUpMutation, SignUpMutationVariables>;
signIn: MutateFunction<SignInMutation, SignInMutationVariables>;
}>;
// app constants
export const LOCALSTORAGE_TOKEN_KEY = "token";
Since AuthProvider
is at the very root of our components tree, CurrentUser
query will be invoked as soon as the application loads.
Notice that we’ve also added a new option to signUp
and signIn
mutations: refetchQueries
. It tells the Apollo client which queries we need to refetch after a particular mutation. We’re passing “CurrentUser"
query in both cases since we want to fetch user data immediately after we sign them in or sign them up.
We can now add the signIn
mutation to our AuthForm
. It’s as simple as just injecting signIn
from our AuthProvider
<script setup lang="ts">
import { reactive } from "vue";
import { useAuth } from "@/providers/AuthProvider/useAuth";
import TextInput from "@/components/TextInput/TextInput.vue";
import BaseButton from "@/components/BaseButton/BaseButton.vue";
const { signUp, signIn } = useAuth();
const form = reactive({
input: {
email: "",
password: "",
},
});
</script>
<template>
<div class="flex flex-col items-center justify-center h-screen">
<section class="p-4 bg-gray-100 rounded-lg border">
<form class="grid gap-6">
<div>
<TextInput
placeholder="john@doe.com"
v-model="form.input.email"
autofocus
>Email:
</TextInput>
</div>
<div>
<TextInput v-model="form.input.password" type="password"
>Password:
</TextInput>
</div>
<div>
<BaseButton @click.prevent="signUp(form)">Sign Up</BaseButton>
<BaseButton @click.prevent="signIn(form)">Sign In</BaseButton>
</div>
</form>
</section>
</div>
</template>
Remember, we wanted to close Protected
page to logged out users and Home
page to logged in users? Well, now we can do that!
Let’s update src/pages/Home.vue
and src/pages/Protected.vue
<script setup lang="ts">
import { useRouter } from "vue-router";
import { useAuth } from "@/providers/AuthProvider/useAuth";
import AuthForm from "@/components/AuthForm/AuthForm.vue";
import { watchEffect } from "vue";
const router = useRouter();
const { isLoggedIn } = useAuth();
watchEffect(() => {
if (isLoggedIn.value) router.push("/protected");
});
</script>
<template>
<div class="container mx-auto">
<AuthForm />
</div>
</template>
<script setup lang="ts">
import { useRouter } from "vue-router";
import { useAuth } from "@/providers/AuthProvider/useAuth";
import { watchEffect } from "vue";
const router = useRouter();
const { user, isLoggedIn } = useAuth();
watchEffect(() => {
if (!isLoggedIn.value) router.push("/");
});
</script>
<template>
<section class="flex h-screen flex-col items-center justify-center">
<h1 class="mb-2 text-2xl">Protected page</h1>
<pre class="mb-2">{{ user }}</pre>
</section>
</template>
The pages are watching isLoggedIn
, a computed variable from AuthProvider
and would redirect the user to the next page if the state of this variable changes according to the predefined condition. This means that now, our pages are properly hidden behind auth and also the users are correctly redirected after a successful log in/log out.
Log out action
Let's not forget that we still need need to implement the logOut
action. Let's update AuthProvider.vue
component and types in constants.ts
<script lang="ts" setup>
import { useMutation, useQuery, useApolloClient } from "@vue/apollo-composable";
import type {
CurrentUserQuery,
CurrentUserQueryVariables,
SignUpMutation,
SignUpMutationVariables,
SignInMutation,
SignInMutationVariables,
} from "@/types/graphql";
import CurrentUser from "./apollo/CurrentUser.gql";
import SignUp from "./apollo/SignUp.gql";
import SignIn from "./apollo/SignIn.gql";
import { computed, provide } from "vue";
import { AUTH_PROVIDER_KEY, LOCALSTORAGE_TOKEN_KEY } from "@/constants";
const { resolveClient } = useApolloClient();
const { result: user, loading: authLoading } = useQuery<
CurrentUserQuery,
CurrentUserQueryVariables
>(CurrentUser);
const { mutate: signUp } = useMutation<SignUpMutation, SignUpMutationVariables>(
SignUp,
{
update(cache, { data }) {
if (data?.signUp?.token) {
localStorage.setItem(LOCALSTORAGE_TOKEN_KEY, data.signUp.token);
}
},
refetchQueries: ["CurrentUser"],
}
);
const { mutate: signIn } = useMutation<SignInMutation, SignInMutationVariables>(
SignIn,
{
update(cache, { data }) {
if (data?.signIn?.token) {
localStorage.setItem(LOCALSTORAGE_TOKEN_KEY, data.signIn.token);
}
},
refetchQueries: ["CurrentUser"],
}
);
async function logOut() {
localStorage.removeItem(LOCALSTORAGE_TOKEN_KEY);
const apolloClient = resolveClient();
await apolloClient.clearStore();
await apolloClient.resetStore();
}
provide(AUTH_PROVIDER_KEY, {
user: computed(() => user.value?.currentUser),
isLoggedIn: computed(() => !!user.value?.currentUser?.id),
authLoading,
signUp,
signIn,
logOut,
});
</script>
<template>
<slot />
</template>
import { ComputedRef, InjectionKey, Ref } from "vue";
import { MutateFunction } from "@vue/apollo-composable";
import {
CurrentUserQuery,
SignInMutation,
SignInMutationVariables,
SignUpMutation,
SignUpMutationVariables,
} from "./types/graphql";
// Provide/inject keys
export const AUTH_PROVIDER_KEY = Symbol() as InjectionKey<{
user: ComputedRef<CurrentUserQuery["currentUser"] | undefined>;
isLoggedIn: ComputedRef<boolean>;
authLoading: Ref<boolean>;
signUp: MutateFunction<SignUpMutation, SignUpMutationVariables>;
signIn: MutateFunction<SignInMutation, SignInMutationVariables>;
logOut: () => void;
}>;
// app constants
export const LOCALSTORAGE_TOKEN_KEY = "token";
Our logOut
function does 2 things:
- removes the auth token from
localStorage
. After we remove the token all, subsequent requests to GraphQL endpoint will be sent without anAuthorization
header. - clears the Apollo cache and refetches active queries. Once that is done, our
user
object will be cleared and the user will be redirected to the homepage with the auth form.
Now, the only thing left is to add this button to our Protected.vue
page:
<script setup lang="ts">
import { useRouter } from "vue-router";
import { useAuth } from "@/providers/AuthProvider/useAuth";
import { watchEffect } from "vue";
import BaseButton from "@/components/BaseButton/BaseButton.vue";
const router = useRouter();
const { user, isLoggedIn, logOut } = useAuth();
watchEffect(() => {
if (!isLoggedIn.value) router.push("/");
});
</script>
<template>
<section class="flex h-screen flex-col items-center justify-center">
<h1 class="mb-2 text-2xl">Protected page</h1>
<pre class="mb-2">{{ user }}</pre>
<BaseButton type="submit" @click="logOut">Log out</BaseButton>
</section>
</template>
With these changes once the user clicks on the Log out button on the Protected
page, they will be immediately redirected to the Home
page.
And that's it, we have successfully implemented client-side authentication with Vue3 + Vite + Apollo + GraphQL!
Links
You can find the full code of this application in the following repositories: