web

Nuxt

Nuxt

In this quickstart, you are going to build an application with Nuxt and integrate it with FusionAuth. You’ll be building it for ChangeBank, a global leader in converting dollars into coins. It’ll have areas reserved for users who have logged in as well as public facing sections.

The Docker Compose file and source code for a complete application are available at https://github.com/FusionAuth/fusionauth-quickstart-javascript-nuxt-web.

Prerequisites

  • Node LTS
  • Docker: The quickest way to stand up FusionAuth. (There are other ways).
  • git: Not required but recommended if you want to track your changes.

General Architecture

While this sample application doesn't have login functionality without FusionAuth, a more typical integration will replace an existing login system with FusionAuth.

In that case, the system might look like this before FusionAuth is introduced.

UserApplicationView HomepageClick Login LinkShow Login FormFill Out and Submit Login FormAuthenticates UserDisplay User's Account or OtherInfoUserApplication

Request flow during login before FusionAuth

The login flow will look like this after FusionAuth is introduced.

UserApplicationFusionAuthView HomepageClick Login Link (to FusionAuth)View Login FormShow Login FormFill Out and Submit Login FormAuthenticates UserGo to Redirect URIRequest the Redirect URIIs User Authenticated?User is AuthenticatedDisplay User's Account or OtherInfoUserApplicationFusionAuth

Request flow during login after FusionAuth

In general, you are introducing FusionAuth in order to normalize and consolidate user data. This helps make sure it is consistent and up-to-date as well as offloading your login security and functionality to FusionAuth.

Getting Started

In this section, you’ll get FusionAuth up and running, and configured with the ChangeBank application.

Clone The Code

First off, grab the code from the repository and change into that directory.

git clone https://github.com/FusionAuth/fusionauth-quickstart-javascript-nuxt-web.git
cd fusionauth-quickstart-javascript-nuxt-web

Run FusionAuth Via Docker

You'll find a Docker Compose file (docker-compose.yml) and an environment variables configuration file (.env) in the root directory of the repo.

Assuming you have Docker installed, you can stand up FusionAuth on your machine with the following.

docker compose up -d

Here you are using a bootstrapping feature of FusionAuth called Kickstart. When FusionAuth comes up for the first time, it will look at the kickstart/kickstart.json file and configure FusionAuth to your specified state.

If you ever want to reset the FusionAuth application, you need to delete the volumes created by Docker Compose by executing docker compose down -v, then re-run docker compose up -d.

FusionAuth will be initially configured with these settings:

  • Your client Id is e9fdb985-9173-4e01-9d73-ac2d60d1dc8e.
  • Your client secret is super-secret-secret-that-should-be-regenerated-for-production.
  • Your example username is richard@example.com and the password is password.
  • Your admin username is admin@example.com and the password is password.
  • The base URL of FusionAuth is http://localhost:9011/.

You can log in to the FusionAuth admin UI and look around if you want to, but with Docker and Kickstart, everything will already be configured correctly.

If you want to see where the FusionAuth values came from, they can be found in the FusionAuth app. The tenant Id is found on the Tenants page. To see the Client Id and Client Secret, go to the Applications page and click the View icon under the actions for the ChangeBank application. You'll find the Client Id and Client Secret values in the OAuth configuration section.

The .env file contains passwords. In a real application, always add this file to your .gitignore file and never commit secrets to version control.

Create Nuxt Application

In this section, you’ll set up a basic Nuxt application with three pages.

  1. Homepage
  2. Account - protected
  3. Makechange - protected

Create a new application using the npx.

If prompted you can choose npm for the package manager. For quickstarts we use npm.

npx nuxi@latest init changebank

Make sure you are in your new directory changebank.

cd changebank

Install @sidebase/nuxt-auth, which simplifies integrating with FusionAuth and creating a secure web application.

At the time of writing @sidebase/nuxt-auth was requiring a lower version of next-auth you may find that you can update both of these components.

At some point you may also find that nuxt/auth also supports Nuxt 3, please see the Nuxt 3 Support section for more information.

npm install next-auth@~4.21.1 @sidebase/nuxt-auth@^0.7.2

Copy environment variables from the complete application example.

cp ../complete-application/.env.example .env

Also copy an image file into a new directory within public called img.

mkdir ./public/img && cp ../complete-application/public/img/money.jpg ./public/img/money.jpg

Authentication

Update nuxt.config.ts to include the new @sidebase/nuxt-auth and include the provider type of authjs just like below.

If you would like to code with any of the pages prior to enabling authentication, just set globalAppMiddleware to false.

// https://nuxt.com/docs/api/configuration/nuxt-config
export default defineNuxtConfig({
  devtools: { enabled: true },
  modules: ['@sidebase/nuxt-auth'],
  auth: {
    globalAppMiddleware: {
      isEnabled: true,
    },
    provider: {
      type: 'authjs',
    },
  },
});

Nuxt 3 includes Route Handlers, which are the preferred way to handle REST-like requests. In the Changebank application you can configure NextAuth.js FusionAuth’s provider in a new route handler by creating a file within /server/api/auth/[...].ts.

On first load of Nuxt this file will make sure that you have all of the correct environment variables. The variables are then exported in an object called authOptions which can be imported on the server when you need to get your session using getServerSession.

The FusionAuthProvider is then provided to NextAuth as a provider for any GET or POST commands that are sent to the /api/auth/* route.

Create a new file named /server/api/auth/[...].ts and copy the following code for the ChangeBank application.

// file: ~/server/api/auth/[...].ts
import { NuxtAuthHandler } from '#auth';
import FusionAuthProvider from 'next-auth/providers/fusionauth';

const fusionAuthIssuer = process.env.FUSIONAUTH_ISSUER;
const fusionAuthClientId = process.env.FUSIONAUTH_CLIENT_ID;
const fusionAuthClientSecret = process.env.FUSIONAUTH_CLIENT_SECRET;
const fusionAuthUrl = process.env.FUSIONAUTH_URL;
const fusionAuthTenantId = process.env.FUSIONAUTH_TENANT_ID;

const missingError = 'missing in environment variables.';
if (!fusionAuthIssuer) {
  throw Error('FUSIONAUTH_ISSUER' + missingError);
}
if (!fusionAuthClientId) {
  throw Error('FUSIONAUTH_CLIENT_ID' + missingError);
}
if (!fusionAuthClientSecret) {
  throw Error('FUSIONAUTH_CLIENT_SECRET' + missingError);
}
if (!fusionAuthUrl) {
  throw Error('FUSIONAUTH_URL' + missingError);
}
if (!fusionAuthTenantId) {
  throw Error('FUSIONAUTH_TENANT_ID' + missingError);
}

export default NuxtAuthHandler({
  providers: [
    // @ts-expect-error You need to use .default here for it to work during SSR. May be fixed via Vite at some point
    FusionAuthProvider.default({
      issuer: fusionAuthIssuer,
      clientId: fusionAuthClientId,
      clientSecret: fusionAuthClientSecret,
      wellKnown: `${fusionAuthUrl}/.well-known/openid-configuration/${fusionAuthTenantId}`,
      tenantId: fusionAuthTenantId, // Only required if you're using multi-tenancy
      authorization: {
        params: {
          scope: 'openid offline_access email profile',
        },
      },
    }),
  ],
});

App Customization

Styles

Create a new file named /changebank/assets/css/global.css and copy the below CSS for the ChangeBank application.

h1 {
  color: #096324;
}

h3 {
  color: #096324;
  margin-top: 20px;
  margin-bottom: 40px;
}

a {
  color: #096324;
}

p {
  font-size: 18px;
}

.header-email {
  color: #096324;
  margin-right: 20px;
}

.fine-print {
  font-size: 16px;
}

body {
  font-family: sans-serif;
  padding: 0px;
  margin: 0px;
}

.h-row {
  display: flex;
  align-items: center;
}

#page-container {
  display: flex;
  flex-direction: column;
  width: 100%;
  height: 100%;
}

#page-header {
  flex: 0;
  display: flex;
  flex-direction: column;
}

#logo-header {
  display: flex;
  justify-content: space-between;
  align-items: center;
  padding: 10px;
}

.menu-bar {
  display: flex;
  flex-direction: row-reverse;
  align-items: center;
  height: 35px;
  padding: 15px 50px 15px 30px;
  background-color: #096324;
  font-size: 20px;
}

.menu-link {
  font-weight: 600;
  color: #ffffff;
  margin-left: 40px;
}

.menu-link {
  font-weight: 600;
  color: #ffffff;
  margin-left: 40px;
}

.inactive {
  text-decoration-line: none;
}

.button-lg {
  width: 150px;
  height: 30px;
  background-color: #096324;
  color: #ffffff;
  font-size: 16px;
  font-weight: 700;
  border-radius: 10px;
  text-align: center;
  text-decoration-line: none;
  cursor: pointer;
}

.column-container {
  display: flex;
  flex-direction: row;
  justify-content: space-between;
}

.content-container {
  flex: 1;
  display: flex;
  flex-direction: column;
  padding: 60px 20px 20px 40px;
}

.balance {
  font-size: 50px;
  font-weight: 800;
}

.change-label {
  font-size: 20px;
  margin-right: 5px;
}

.change-input {
  font-size: 20px;
  height: 40px;
  text-align: end;
  padding-right: 10px;
}

.change-submit {
  font-size: 15px;
  height: 40px;
  margin-left: 15px;
  border-radius: 5px;
}

.change-message {
  font-size: 20px;
  margin-bottom: 15px;
}

.error-message {
  font-size: 20px;
  color: #ff0000;
  margin-bottom: 15px;
}

.app-container {
  flex: 0;
  min-width: 440px;
  display: flex;
  flex-direction: column;
  margin-top: 40px;
  margin-left: 80px;
}

.change-container {
  flex: 1;
}

The above styles will be imported into a new layout that you will create called /changebank/layouts/default.vue. Update this file to match the code below. This will allow the rest of the pages within your application and the layout itself to use the global styles. This is the first time that you will use the useAuth Composable from Sidebase. The status can then be used to determine what links should be shown in the layout.

<script setup>
import '~/assets/css/global.css';

const { status } = useAuth();
</script>

<template>
  <div id="page-container">
    <div id="page-header">
      <div id="logo-header">
        <img
          src="https://fusionauth.io/cdn/samplethemes/changebank/changebank.svg"
          alt="change bank logo"
          width="257"
          height="55"
        />
        <LoginButton />
      </div>

      <div v-if="status === 'authenticated'" id="menu-bar" class="menu-bar">
        <a href="/makechange" class="menu-link"> Make Change </a>
        <a href="/account" class="menu-link"> Account </a>
      </div>
      <div v-else id="menu-bar" class="menu-bar">
        <a class="menu-link">About</a>
        <a class="menu-link">Services</a>
        <a class="menu-link">Products</a>
        <a href="/" class="menu-link"> Home </a>
      </div>
    </div>
    <NuxtPage />
  </div>
</template>

Login Button

Create a new file in /changebank/components/LoginButton.vue that will be used for a button component.

For this button you can use the signIn and signOut functions from the useAuth composable. Copy the below code for the ChangeBank application into /changebank/components/LoginButton.vue.

<script setup>
const { status, signIn, signOut, data } = useAuth();
</script>

<template>
  <section v-if="status === 'authenticated'">
    Status: Logged in as {{ data?.user?.email }} <br />
    <button class="button-lg" @click="signOut({ callbackUrl: '/' })">
      Log out
    </button>
  </section>
  <section v-else>
    <button
      class="button-lg"
      @click="signIn('fusionauth', { callbackUrl: '/account' })"
    >
      Log in
    </button>
  </section>
</template>

Create a new file in /changebank/components/LoginLink.vue that will be used for a link component. For this link you can use the same signIn function from the useAuth composable as you did in the button above.

Copy the below code for the ChangeBank application into /changebank/components/LoginLink.vue.

<script setup>
const { status, signIn, signOut, data } = useAuth();
</script>
<template>
  <p>
    To get started,
    <a
      @click="signIn('fusionauth', { callbackUrl: '/account' })"
      :style="{ textDecoration: 'underline', cursor: 'pointer' }"
    >
      log in or create a new account.
    </a>
  </p>
</template>

Home Page

Update the file /changebank/app.vue to remove the default Nuxt Welcome and add our layout.

<template>
  <NuxtLayout>
    <NuxtPage />
  </NuxtLayout>
</template>

Create the file /changebank/pages/index.vue which will have the Homepage details. Not much here just an image and a Login Link.

<script setup lang="ts">
definePageMeta({ auth: false })
</script>

<template>
    <main>
      <div :style="{ flex: '1' }"">
        <div class="column-container">
          <div class="content-container">
            <div :style="{ marginBottom: '100px' }">
              <h1>Welcome to Changebank</h1>
              <LoginLink session={session} />
            </div>
          </div>
          <div :style="{ width: '100%', maxWidth: '800px' }">
            <img
              src="/img/money.jpg"
              alt="money"
              width="1512"
              height="2016"
              :style="{
                objectFit: 'contain',
                width: '100%',
                position: 'relative',
                height: 'unset',
              }"
            />
          </div>
        </div>
      </div>
    </main>
</template>

Account Page

Create a new file /changebank/pages/account.vue which will have some fake Account details.

One special note here is that if you leave the auth globalAppMiddleware enabled in nuxt.config.ts you will need to authenticate to access this page.

Here’s the contents of /changebank/pages/account.vue.

<script lang="ts" setup>
// get random numbers and round to integers
const randomDollars = Math.floor(Math.random() * 100);
const randomCents = Math.floor(Math.random() * 100);
// convert string to number
const randomMonies = parseFloat(randomDollars + '.' + randomCents);

// substract random number from 100 and round to 2 decimals
const result = Math.round((100 - randomMonies) * 100) / 100;
</script>

<template>
  <section>
    <div :style="{ flex: '1' }">
      <div class="column-container">
        <div class="app-container">
          <h3>Your balance</h3>
          <div class="balance">${{ result }}</div>
        </div>
      </div>
    </div>
  </section>
</template>

Make Change Page

Finally, add some business logic for logged in users to make change with the following code in /changebank/pages/makechange.vue:

<template>
  <MakeChangeForm />
</template>

If the user session is not present the user is redirected back to the homepage at the base route. If the user is present then the MakeChangeForm is presented. Create a new file located at /changebank/components/MakeChangeForm.vue with the below code. This component has all of the business logic needed for taking in a dollar amount of money and returning the correct amount of each coin.

<script setup>
import { ref } from 'vue';

var coins = {
  quarters: 0.25,
  dimes: 0.1,
  nickels: 0.05,
  pennies: 0.01,
};

const message = ref('');
const amount = ref(0);

const onMakeChange = (event) => {
  event.preventDefault();

  try {
    message.value = 'We can make change for';

    let remainingAmount = amount.value;
    for (const [name, nominal] of Object.entries(coins)) {
      let count = Math.floor(remainingAmount / nominal);
      remainingAmount =
        Math.ceil((remainingAmount - count * nominal) * 100) / 100;

      message.value = `${message.value} ${count} ${name}`;
    }
    message.value = `${message.value}!`;
  } catch (ex) {
    message.value = `There was a problem converting the amount submitted. ${ex.message}`;
  }
};
</script>
<template>
  <section>
    <div :style="{ flex: '1' }">
      <div class="column-container">
        <div class="app-container change-container">
          <h3>We Make Change</h3>
          <div class="change-message">{{ message }}</div>
          <form @submit="onMakeChange">
            <div class="h-row">
              <div class="change-label">Amount in USD: $</div>
              <input
                class="change-input"
                type="number"
                step="0.01"
                name="amount"
                :value="amount"
                @input="(event) => (amount = event.target.value)"
              />
              <input class="change-submit" type="submit" value="Make Change" />
            </div>
          </form>
        </div>
      </div>
    </div>
  </section>
</template>

Run the Application

You can now open up an incognito window and visit the Nuxt app at http://localhost:3000/. Log in with the user account you created when setting up FusionAuth, and you’ll see the email of the user next to a logout button.

npm run dev

Try clicking the Login button at the top or the link at the center of the screen.

This will take you through the NextAuth.js authentication flow. Since fusionauth is set as the provider in the Login Button signIn('fusionauth', { callbackUrl: '/account' }), the FusionAuth Login page will be automatically presented. If you would like to see all providers you can set fusionauth to undefined.

You can then login to FusionAuth with Email: richard@example.com Password: password (as you might expect not ideal for production.)

This will then take you back to the application, which will check for a session and appropriately redirect you to the /account route when your session has been established.

Made it this far? Get a free t-shirt, on us!

Thank you for spending some time getting familiar with FusionAuth.

*Offer only valid in the United States and Canada, while supplies last.

fusionauth tshirt

Next Steps

This quickstart is a great way to get a proof of concept up and running quickly, but to run your application in production, there are some things you're going to want to do.

FusionAuth Customization

FusionAuth gives you the ability to customize just about everything to do with the user's experience and the integration of your application. This includes:

Security

Tenant and Application Management