DEV Community

Sebastian Rubina
Sebastian Rubina

Posted on

Setting up Supabase Auth with Nuxt v3

Implementing authentication is something that you do on most projects, but still something that you may not remember how to do by memory because of how often you actually do it.

Here is a quick how-to about implementing Supabase Auth with Nuxt v3. In this example, we will be using OTP, but it applies to every case.

You will first want to start your project by going to Supabase's website.

After creating a project in Supabase and starting your project on Nuxt, we want to then install the Supabase Nuxt package by doing:

npx nuxi@latest module add supabase

We will then create our .env file and add the following environment variables:

SUPABASE_URL=<your_supabase_url>
SUPABASE_KEY=<your_supabase_key>
Enter fullscreen mode Exit fullscreen mode

You can find these on the Supabase dashboard for your project, under Settings -> API

Supabase Dashboard

Afterwards, we can being setting up our project. I have made 2 very basic files so far:

  1. auth.ts (I used a Pinia store, but feel free to use just a regular file)
import { defineStore } from "pinia";

export const useAuthStore = defineStore("auth", () => {
  const supabase = useSupabaseClient();

  const sendOtp = async (email: string) => {
    const { error } = await supabase.auth.signInWithOtp({
      email,
    });

    if (error) {
      throw error;
    }

    return true;
  };

  const verifyOtp = async (email: string, otp: string) => {
    const { error } = await supabase.auth.verifyOtp({
      type: "email",
      token: otp,
      email,
    });

    if (error) {
      throw error;
    }

    return true;
  };

  return {
    sendOtp,
    verifyOtp,
  };
});
Enter fullscreen mode Exit fullscreen mode
  1. LoginForm.vue
<template>
  <div class="max-w-md mx-auto bg-white p-8 rounded-lg shadow-md">
    <h2 class="text-3xl font-bold mb-6 text-center text-gray-800">Welcome</h2>
    <form @submit.prevent="handleSubmit" class="space-y-6">
      <div v-if="mode === 'email'">
        <label for="email" class="block mb-2 font-medium text-gray-700"
          >Email</label
        >
        <input
          type="email"
          id="email"
          v-model="email"
          required
          placeholder="Enter your email"
          class="w-full px-4 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500 transition duration-200"
        />
      </div>

      <div v-else-if="mode === 'code'">
        <p class="mb-2 font-medium text-gray-700">
          Enter the 6-digit code sent to {{ email }}
        </p>
        <input
          type="text"
          v-model="otpCode"
          required
          placeholder="Enter 6-digit code"
          maxlength="6"
          class="w-full px-4 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500 transition duration-200"
        />
      </div>

      <UButton
        icon="i-heroicons-paper-airplane"
        size="lg"
        color="primary"
        variant="solid"
        :label="buttonLabel"
        :trailing="true"
        block
      />
    </form>
  </div>
</template>

<script setup lang="ts">
import { ref, computed } from "vue";
import { useAuthStore } from "~/stores/auth";

const authStore = useAuthStore();

const email = ref("");
const otpCode = ref("");
const mode = ref("email");

const buttonLabel = computed(() => {
  return mode.value === "email" ? "Send One-Time Password" : "Verify Code";
});

const handleSubmit = async () => {
  if (mode.value === "email") {
    try {
      await authStore.sendOtp(email.value);
      mode.value = "code";
    } catch (error) {
      console.log("Error sending OTP: ", error);
    }
  } else {
    try {
      await authStore.verifyOtp(email.value, otpCode.value);
    } catch (error) {
      console.log("Error verifying OTP: ", error);
    }
  }
};
</script>

<style scoped></style>
Enter fullscreen mode Exit fullscreen mode

Note that I am also using NuxtUI, in case you get any errors.

Because by default, the signInWithOtp function sends a magic link, you will have to change the email template on Supabase's dashboard to send a token:

Supabase dasahboard
This is found under Authentication -> Email Templates -> Change Confirm Signup and Magic Link templates to use {{ .Token }}

And that is pretty much it, you have a working auth!
If you want to add signout, you can also add a method to your previous file like such:

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

  if (error) {
    throw error;
  }

  return true;
};
Enter fullscreen mode Exit fullscreen mode

However, if you want to protect certain routes, we can also do that adding middleware.

Create a folder on the root called middleware (name is key) and a file called auth.ts.

You can then add something like this:

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

  const protectedRoutes = ["/app"];

  if (!user.value && protectedRoutes.includes(to.path)) {
    return navigateTo("/auth");
  }

  if (user.value && to.path === "/auth") {
    return navigateTo("/");
  }
});
Enter fullscreen mode Exit fullscreen mode

This will basically protect from the server your /app route, so if you attempt to go to /app without being signed in, you will be redirected to /auth.

Likewise, if you try to visit /auth while already being signed in, you will be redirected to the home page /.

Now, to use this, you can place it inside the <script> tag of any of your components like this:

<script setup lang="ts">
definePageMeta({
  middleware: "auth", // this is the name of the file, minus the extension
});
</script>
Enter fullscreen mode Exit fullscreen mode

And that is it, it is that easy!

Top comments (0)