How to setup user authentication with Nuxt 3, Supabase and Tailwind CSS

We sometimes need user authentication in our web application depending on the situation. In this article, we’ll see how to create a login and register pages with Supabase authentication and a route middleware to avoid access to certain pages if we are not logged in.

I’m assuming that you’ve followed my previous post about setting up your project with Tailwind and Supabase.

Creating the index page

We start up by deleting the content of our index.vue file or if you don’t have it you can create a pages folder and the index.vue file inside.

Now inside our <script setup> tag we are going to bring the destructured auth method from the useSupabaseClient composable and the user object from useSupabaseUser and it should look like this:

<script setup>
const { auth } = useSupabaseClient();
const user = useSupabaseUser();
</script>

Next, we are going to create a constant named userLogout (you can call it however you like) which is going to be equal to an asynchronous arrow function and we are going to await the signOut method from auth:

<script setup>
// Some code

const userLogout = async () => {
  await auth.signOut();
};
</script>

Validating if we are logged in

Still, in the <script setup> tag we are now going to validate if the user is logged in and redirect it to the login page if it’s not.

<script setup>
// Some code

watchEffect(() => {
  if (!user.value) {
    return navigateTo('/login');
  }
});

definePageMeta({
  middleware: 'auth',
});
</script>

Altogether is going to look like this:

<script setup>
const { auth } = useSupabaseClient();
const user = useSupabaseUser();

const userLogout = async () => {
  await auth.signOut();
};

watchEffect(() => {
  if (!user.value) {
    return navigateTo('/login');
  }
});

definePageMeta({
  middleware: 'auth',
});
</script>

The template tag inside the index page

Now we are ready to add some markup to show an interface that in this case is going to be a basic profile page showing a fake profile image with the logged-in email address and a button to logout with some Tailwind utility-classes.

<template>
  <main class="text-gray-400">
    <section v-if="user" class="mt-10 flex justify-center">
      <div class="text-center">
        <figure
          class="inline-flex h-20 w-20 items-center justify-center rounded-full bg-gray-800 text-gray-600"
        >
          <img
            class="rounded-full"
            src="https://xsgames.co/randomusers/avatar.php?g=pixel"
            alt=""
          />
        </figure>
        <div class="flex flex-col items-center justify-center text-center">
          <h1 class="tagline mt-4 text-5xl font-black">Welcome</h1>
          <div class="mt-2 mb-4 h-1 w-12 rounded bg-[#42b883]"></div>

          <h3 class="text-2xl font-bold text-[#aac8e4]">Email</h3>
          <p class="text-base text-gray-400">{{ user.email }}</p>

          <button
            @click="userLogout"
            class="mt-8 rounded-md bg-[#42b883] px-4 py-2 font-sans font-bold text-[#213547] transition-colors duration-500 hover:bg-[#42d392] focus:outline-none"
          >
            logout
          </button>
        </div>
      </div>
    </section>
  </main>
</template>

Register a new user

In order to register a new user, we are going to create a new page called register.vue and create the <script setup> tag. Inside the script, we are going to bring the auth method and the user object again, and also we are going to define our state that is going to be the email, password, confirmPassword, and errorMsg.

<script setup>
const user = useSupabaseUser();
const email = ref('');
const password = ref('');
const confirmPassword = ref('');
const errorMsg = ref('');

const { auth } = useSupabaseClient();
</script>

Next, we are going to create a constant named userRegister which is going to be an asynchronous arrow function that is going to validate first if the password and confirmPassword are the same, if they are not is going to display an error message and delete both fields. If they do, it’s going to run a try-catch block that is going to await the signUp method from auth with our email and password state values and clear the input fields or display an error message if there are any errors.

<script setup>
// Some code

const userRegister = async () => {
  if (password.value !== confirmPassword.value) {
    errorMsg.value = 'Passwords do not match!';
    password.value = '';
    confirmPassword.value = '';
    setTimeout(() => {
      errorMsg.value = '';
    }, 3000);
    return;
  }

  try {
    const { error } = await auth.signUp({
      email: email.value,
      password: password.value,
    });
    email.value = '';
    password.value = '';
    confirmPassword.value = '';

    if (error) throw error;
  } catch (error) {
    errorMsg.value = error.message;
    setTimeout(() => {
      errorMsg.value = '';
    }, 3000);
  }
};
</script>

Finally, we are going to create a watchEffect that will take us to the index page if the user is created.

<script setup>
// Some code

watchEffect(() => {
  if (user.value) {
    return navigateTo('/');
  }
});
</script>

Everything together should look like this:

<script setup>
const user = useSupabaseUser();
const email = ref('');
const password = ref('');
const confirmPassword = ref('');
const errorMsg = ref('');

const { auth } = useSupabaseClient();
const userRegister = async () => {
  if (password.value !== confirmPassword.value) {
    errorMsg.value = 'Passwords do not match!';
    password.value = '';
    confirmPassword.value = '';
    setTimeout(() => {
      errorMsg.value = '';
    }, 3000);
    return;
  }

  try {
    const { error } = await auth.signUp({
      email: email.value,
      password: password.value,
    });
    email.value = '';
    password.value = '';
    confirmPassword.value = '';

    if (error) throw error;
  } catch (error) {
    errorMsg.value = error.message;
    setTimeout(() => {
      errorMsg.value = '';
    }, 3000);
  }
};

watchEffect(() => {
  if (user.value) {
    return navigateTo('/');
  }
});
</script>

Supabase automatically logs in the new user when we create it.

The template tag inside the register page

Now we need to create the template tag with the registration form (with TailwindCSS classes) that is going to run the userRegister function when we submit it, we also need to add the v-models to each input with the email, password, and confirmPassword respectively, and a little notification card that is going to let us know if there are any errors.

<template>
  <main>
    <section
      class="container mx-auto flex flex-wrap items-center justify-center px-5 py-24 text-gray-400"
    >
      <form
        @submit.prevent="userRegister"
        class="bg-opacity-50 mt-10 flex w-full flex-col rounded-lg bg-[#242424] p-8 md:mt-0 md:w-1/2 lg:w-2/6"
      >
        <h2 class="mb-5 text-lg font-medium text-[#aac8e4]">Register</h2>

        <div class="relative mb-4">
          <label for="email" class="text-sm leading-7 text-gray-400">Email</label>
          <input
            v-model="email"
            type="email"
            id="email"
            name="email"
            class="bg-opacity-20 w-full rounded border border-gray-600 bg-transparent py-1 px-3 text-base leading-8 text-gray-100 outline-none transition-colors duration-200 ease-in-out focus:border-[#42d392] focus:bg-transparent focus:ring-2 focus:ring-transparent"
            required
          />
        </div>
        <div class="relative mb-4">
          <label for="password" class="text-sm leading-7 text-gray-400">Password</label>
          <input
            v-model="password"
            type="password"
            id="password"
            name="password"
            class="bg-opacity-20 w-full rounded border border-gray-600 bg-transparent py-1 px-3 text-base leading-8 text-gray-100 outline-none transition-colors duration-200 ease-in-out focus:border-[#42d392] focus:bg-transparent focus:ring-2 focus:ring-transparent"
            required
          />
        </div>
        <div class="relative mb-4">
          <label for="confirmPassword" class="text-sm leading-7 text-gray-400"
            >Confirm password</label
          >
          <input
            v-model="confirmPassword"
            type="password"
            id="confirmPassword"
            name="confirmPassword"
            class="bg-opacity-20 w-full rounded border border-gray-600 bg-transparent py-1 px-3 text-base leading-8 text-gray-100 outline-none transition-colors duration-200 ease-in-out focus:border-[#42d392] focus:bg-transparent focus:ring-2 focus:ring-transparent"
            required
          />
        </div>
        <button
          type="submit"
          class="rounded border-0 bg-[#42b883] py-2 px-8 font-sans font-bold text-[#213547] transition-colors duration-500 hover:bg-[#42d392] focus:outline-none"
        >
          Submit
        </button>
        <span
          class="bg-opacity-50 absolute right-8 top-8 rounded-lg bg-[#242424] p-8 px-4 py-2 text-red-500"
          v-if="errorMsg"
          >{{ errorMsg }}</span
        >
        <p class="mt-3 text-xs">Do you have an account yet?</p>
        <nuxt-link class="w-fit text-sm text-[#aac8e4] hover:text-[#42b883]" to="/login"
          >Login</nuxt-link
        >
      </form>
    </section>
  </main>
</template>

Log in with an existing user

Now that we have a user created we need a login page. Like the previous pages we are going to start with the <script setup> tag. We are going to bring the same things in our state except for the confirmPassword state.

<script setup>
const user = useSupabaseUser();
const email = ref('');
const password = ref('');
const errorMsg = ref('');
const { auth } = useSupabaseClient();
</script>

Now we need to create the userLogin asynchronous function that is going to run a try-catch block and await the signIn method from auth and use the email and password state as parameters. Then it’s going to clear the fields and also it will display an error message if there are any of them.

<script setup>
// Some code

const userLogin = async () => {
  try {
    const { error } = await auth.signIn({
      email: email.value,
      password: password.value,
    });

    email.value = '';
    password.value = '';

    if (error) throw error;
  } catch (error) {
    errorMsg.value = error.message;
    setTimeout(() => {
      errorMsg.value = '';
    }, 3000);
  }
};
</script>

Lastly, we are going to add the same watchEffect that will take us to the index page when we are logged in.

<script setup>
// Some code

watchEffect(() => {
  if (user.value) {
    return navigateTo('/');
  }
});
</script>

Here the entire script tag:

<script setup>
const user = useSupabaseUser();
const email = ref('');
const password = ref('');
const errorMsg = ref('');
const { auth } = useSupabaseClient();

const userLogin = async () => {
  try {
    const { error } = await auth.signIn({
      email: email.value,
      password: password.value,
    });

    email.value = '';
    password.value = '';

    if (error) throw error;
  } catch (error) {
    errorMsg.value = error.message;
    setTimeout(() => {
      errorMsg.value = '';
    }, 3000);
  }
};

watchEffect(() => {
  if (user.value) {
    return navigateTo('/');
  }
});
</script>

The template tag inside the login page

The template tag for this page is going to be pretty similar to the registration form but we get rid of the confirmPassword field and it’s going to run the userLogin function instead.

<template>
  <main>
    <section
      class="container mx-auto flex flex-wrap items-center justify-center px-5 py-24 text-gray-400"
    >
      <form
        @submit.prevent="userLogin"
        class="bg-opacity-50 mt-10 flex w-full flex-col rounded-lg bg-[#242424] p-8 md:mt-0 md:w-1/2 lg:w-2/6"
      >
        <h2 class="mb-5 text-lg font-medium text-[#aac8e4]">Login</h2>
        <div class="relative mb-4">
          <label for="full-name" class="text-sm leading-7 text-gray-400">Email</label>
          <input
            v-model="email"
            type="email"
            id="email"
            name="email"
            class="bg-opacity-20 w-full rounded border border-gray-600 bg-transparent py-1 px-3 text-base leading-8 text-gray-100 outline-none transition-colors duration-200 ease-in-out focus:border-[#42b883] focus:bg-transparent focus:ring-2 focus:ring-transparent"
            required
          />
        </div>

        <div class="relative mb-4">
          <label for="password" class="text-sm leading-7 text-gray-400">Password</label>
          <input
            id="password"
            v-model="password"
            name="password"
            type="password"
            class="bg-opacity-20 w-full rounded border border-gray-600 bg-transparent py-1 px-3 text-base leading-8 text-gray-100 outline-none transition-colors duration-200 ease-in-out focus:border-[#42b883] focus:bg-transparent focus:ring-2 focus:ring-transparent"
            required
          />
        </div>

        <button
          class="rounded border-0 bg-[#42b883] py-2 px-8 font-sans font-bold text-[#213547] transition-colors duration-500 hover:bg-[#42d392] focus:outline-none"
        >
          Submit
        </button>

        <span
          class="bg-opacity-50 absolute right-8 top-8 rounded-lg bg-[#242424] p-8 px-4 py-2 text-red-500"
          v-if="errorMsg"
          >{{ errorMsg }}</span
        >

        <p class="mt-3 text-xs">You don't have an account yet?</p>
        <nuxt-link class="w-fit text-sm text-[#aac8e4] hover:text-[#42b883]" to="/register"
          >Register</nuxt-link
        >
      </form>
    </section>
  </main>
</template>

Adding the auth middleware

In order to get redirected if the user is not logged in, we need to create a middleware folder in the root directory and create an auth.ts file

auth middleware folder

Inside this file, we are going to add the following code that is going to validate if the user object from supabase has a value and if not is going to take us to the login page.

export default defineNuxtRouteMiddleware(() => {
  const user = useSupabaseUser();

  if (!user.value) {
    return navigateTo('/login');
  }
});

Now we can run our dev server with npm run dev or yarn dev to give this a try

register pagelogin pageindex page

Now you are ready to add this feature to any existing or new Nuxt 3 app to add authentication very easily.

Source

Look at the Supabase documentation to learn more.

Look at the @nuxtjs/supabase module documentation to learn more.

Related posts

What are the differences between client side rendering, server side rendering and static site generation

What are the differences between client side rendering, server side rendering and static site generation

Unsure which method of rendering is best for your website? Read this blog post on the differences between client-side rendering, server-side rendering, and static site generation to make an informed decision.

#frontend
How to add form validation to your Nuxt 3 application

How to add form validation to your Nuxt 3 application

When it comes to form validation I think it is better if we handle it on our own with custom validations and error messages. In this post, we will see how to create custom form validations with vuelidate an open source library create for this.

#frontend
How to setup Nuxt 3 with Tailwind CSS, Pinia and Supabase

How to setup Nuxt 3 with Tailwind CSS, Pinia and Supabase

Learn how to create a boilerplate project in order to start your next project faster

#frontend