shadimage
supabase
blockpassword-based-auth-nuxtjssource: ok · hit

Password Based Auth flow for Nuxt.js and Supabase

Password Based Auth flow for Nuxt.js and Supabase

npx shadcn add "https://supabase.com/ui/r/password-based-auth-nuxtjs.json"
free

No selection tags yet — empty means no evidence, not lack of support.

Dependencies

Bundle weight · light
@supabase/ssr@latest@supabase/supabase-js@latest

Registry dependencies

buttoncardinputlabel

Files12 · source cached 10/8/2026, 8:15:59 PM

registry/default/password-based-auth/nuxtjs/app/components/login-form.vue
<script setup lang="ts">
import { ref } from "vue"
import { createClient } from "@/lib/supabase/client"
import { Button } from "@/components/ui/button"
import {
  Card,
  CardContent,
  CardDescription,
  CardHeader,
  CardTitle,
} from "@/components/ui/card"
import { Input } from "@/components/ui/input"
import { Label } from "@/components/ui/label"

const email = ref("")
const error = ref<string | null>(null)
const success = ref(false)
const isLoading = ref(false)

const handleForgotPassword = async (e: Event) => {
  e.preventDefault()
  const supabase = createClient()
  isLoading.value = true
  error.value = null

  try {
    const { error: supabaseError } = await supabase.auth.resetPasswordForEmail(email.value, {
      redirectTo: "http://localhost:3000/update-password",
    })
    if (supabaseError) throw supabaseError
    success.value = true
  } catch (err: unknown) {
    error.value = err instanceof Error ? err.message : "An error occurred"
  } finally {
    isLoading.value = false
  }
}
</script>

<template>
  <div class="flex flex-col gap-6">
    <Card v-if="success">
      <CardHeader>
        <CardTitle class="text-2xl">Check Your Email</CardTitle>
        <CardDescription>Password reset instructions sent</CardDescription>
      </CardHeader>
      <CardContent>
        <p class="text-sm text-muted-foreground">
          If you registered using your email and password, you will receive a password reset email.
        </p>
      </CardContent>
    </Card>

    <Card v-else>
      <CardHeader>
        <CardTitle class="text-2xl">Reset Your Password</CardTitle>
        <CardDescription>
          Type in your email and we&apos;ll send you a link to reset your password
        </CardDescription>
      </CardHeader>
      <CardContent>
        <form @submit="handleForgotPassword">
          <div class="flex flex-col gap-6">
            <div class="grid gap-2">
              <Label for="email">Email</Label>
              <Input
                id="email"
                type="email"
                placeholder="m@example.com"
                required
                v-model="email"
              />
            </div>
            <p v-if="error" class="text-sm text-red-500">{{ error }}</p>
            <Button type="submit" class="w-full" :disabled="isLoading">
              {{ isLoading ? "Sending..." : "Send reset email" }}
            </Button>
          </div>
          <div class="mt-4 text-center text-sm">
            Already have an account?
            <a href="/login" class="underline underline-offset-4">Sign in</a>
          </div>
        </form>
      </CardContent>
    </Card>
  </div>
</template>
registry/default/password-based-auth/nuxtjs/app/components/sign-up-form.vue
<script setup lang="ts">
import { ref } from "vue"
import { createClient } from "@/lib/supabase/client"
import { Button } from "@/components/ui/button"
import {
  Card,
  CardContent,
  CardDescription,
  CardHeader,
  CardTitle,
} from "@/components/ui/card"
import { Input } from "@/components/ui/input"
import { Label } from "@/components/ui/label"

const email = ref("")
const password = ref("")
const repeatPassword = ref("")
const error = ref<string | null>(null)
const isLoading = ref(false)
const success = ref(false)

const handleSignUp = async () => {
  const supabase = createClient()
  error.value = null

  if (password.value !== repeatPassword.value) {
    error.value = "Passwords do not match"
    return
  }

  isLoading.value = true
  try {
    const { error: supabaseError } = await supabase.auth.signUp({
      email: email.value,
      password: password.value,
    })
    if (supabaseError) throw supabaseError
    success.value = true
  } catch (err: unknown) {
    error.value = err instanceof Error ? err.message : "An error occurred"
  } finally {
    isLoading.value = false
  }
}
</script>

<template>
  <div class="flex flex-col gap-6">
    <Card v-if="success">
      <CardHeader>
        <CardTitle class="text-2xl">Thank you for signing up!</CardTitle>
        <CardDescription>Check your email to confirm</CardDescription>
      </CardHeader>
      <CardContent>
        <p class="text-sm text-muted-foreground">
          You've successfully signed up. Please check your email to confirm your account before
          signing in.
        </p>
      </CardContent>
    </Card>

    <Card v-else>
      <CardHeader>
        <CardTitle class="text-2xl">Sign up</CardTitle>
        <CardDescription>Create a new account</CardDescription>
      </CardHeader>
      <CardContent>
        <form @submit.prevent="handleSignUp">
          <div class="flex flex-col gap-6">
            <!-- Email -->
            <div class="grid gap-2">
              <Label for="email">Email</Label>
              <Input
                id="email"
                type="email"
                placeholder="m@example.com"
                required
                v-model="email"
              />
            </div>

            <!-- Password -->
            <div class="grid gap-2">
              <div class="flex items-center">
                <Label for="password">Password</Label>
              </div>
              <Input
                id="password"
                type="password"
                required
                v-model="password"
              />
            </div>

            <!-- Repeat Password -->
            <div class="grid gap-2">
              <div class="flex items-center">
                <Label for="repeat-password">Repeat Password</Label>
              </div>
              <Input
                id="repeat-password"
                type="password"
                required
                v-model="repeatPassword"
              />
            </div>

            <!-- Error -->
            <p v-if="error" class="text-sm text-red-500">{{ error }}</p>

            <!-- Submit -->
            <Button type="submit" class="w-full" :disabled="isLoading">
              {{ isLoading ? "Creating an account..." : "Sign up" }}
            </Button>
          </div>

          <div class="mt-4 text-center text-sm">
            Already have an account?
            <a href="/login" class="underline underline-offset-4">Sign in</a>
          </div>
        </form>
      </CardContent>
    </Card>
  </div>
</template>
registry/default/password-based-auth/nuxtjs/app/components/forgot-password-form.vue
<script setup lang="ts">
import { ref } from "vue"
import { createClient } from "@/lib/supabase/client"
import { Button } from "@/components/ui/button"
import {
  Card,
  CardContent,
  CardDescription,
  CardHeader,
  CardTitle,
} from "@/components/ui/card"
import { Input } from "@/components/ui/input"
import { Label } from "@/components/ui/label"


const email = ref("")
const error = ref<string | null>(null)
const success = ref(false)
const isLoading = ref(false)

const handleForgotPassword = async (e: Event) => {
  e.preventDefault()
  const supabase = createClient()
  isLoading.value = true
  error.value = null

  try {
    const { error: supabaseError } = await supabase.auth.resetPasswordForEmail(email.value, {
      redirectTo: "http://localhost:3000/update-password",
    })
    if (supabaseError) throw supabaseError
    success.value = true
  } catch (err: unknown) {
    error.value = err instanceof Error ? err.message : "An error occurred"
  } finally {
    isLoading.value = false
  }
}
</script>

<template>
  <div class="flex flex-col gap-6">
    <Card v-if="success">
      <CardHeader>
        <CardTitle class="text-2xl">Check Your Email</CardTitle>
        <CardDescription>Password reset instructions sent</CardDescription>
      </CardHeader>
      <CardContent>
        <p class="text-sm text-muted-foreground">
          If you registered using your email and password, you will receive a password reset email.
        </p>
      </CardContent>
    </Card>

    <Card v-else>
      <CardHeader>
        <CardTitle class="text-2xl">Reset Your Password</CardTitle>
        <CardDescription>
          Type in your email and we&apos;ll send you a link to reset your password
        </CardDescription>
      </CardHeader>
      <CardContent>
        <form @submit="handleForgotPassword">
          <div class="flex flex-col gap-6">
            <div class="grid gap-2">
              <Label for="email">Email</Label>
              <Input
                id="email"
                type="email"
                placeholder="m@example.com"
                required
                v-model="email"
              />
            </div>
            <p v-if="error" class="text-sm text-red-500">{{ error }}</p>
            <Button type="submit" class="w-full" :disabled="isLoading">
              {{ isLoading ? "Sending..." : "Send reset email" }}
            </Button>
          </div>
          <div class="mt-4 text-center text-sm">
            Already have an account?
            <a href="/login" class="underline underline-offset-4">Sign in</a>
          </div>
        </form>
      </CardContent>
    </Card>
  </div>
</template>
registry/default/password-based-auth/nuxtjs/app/components/update-password-form.vue
<script setup lang="ts">
import { ref } from "vue"
import { createClient } from "@/lib/supabase/client"
import { Button } from "@/components/ui/button"
import {
  Card,
  CardContent,
  CardDescription,
  CardHeader,
  CardTitle,
} from "@/components/ui/card"
import { Input } from "@/components/ui/input"
import { Label } from "@/components/ui/label"

const password = ref("")
const error = ref<string | null>(null)
const isLoading = ref(false)

const handleUpdatePassword = async () => {
  const supabase = createClient()
  isLoading.value = true
  error.value = null

  try {
    const { error: supabaseError } = await supabase.auth.updateUser({
      password: password.value,
    })
    if (supabaseError) throw supabaseError
    // Redirect user after successful password update
    location.href = "/protected"
  } catch (err: unknown) {
    error.value = err instanceof Error ? err.message : "An error occurred"
  } finally {
    isLoading.value = false
  }
}
</script>

<template>
  <div class="flex flex-col gap-6">
    <Card>
      <CardHeader>
        <CardTitle class="text-2xl">Reset Your Password</CardTitle>
        <CardDescription>Please enter your new password below.</CardDescription>
      </CardHeader>
      <CardContent>
        <form @submit.prevent="handleUpdatePassword">
          <div class="flex flex-col gap-6">
            <div class="grid gap-2">
              <Label for="password">New password</Label>
              <Input
                id="password"
                type="password"
                placeholder="New password"
                required
                v-model="password"
              />
            </div>
            <p v-if="error" class="text-sm text-red-500">{{ error }}</p>
            <Button type="submit" class="w-full" :disabled="isLoading">
              {{ isLoading ? "Saving..." : "Save new password" }}
            </Button>
          </div>
        </form>
      </CardContent>
    </Card>
  </div>
</template>
registry/default/password-based-auth/nuxtjs/app/pages/auth/login.vue
<script setup lang="ts">
import LoginForm from "@/components/login-form.vue"
</script>

<template>
  <div class="flex min-h-screen w-full items-center justify-center p-6 md:p-10">
    <div class="w-full max-w-sm">
      <LoginForm />
    </div>
  </div>
</template>
registry/default/password-based-auth/nuxtjs/app/pages/auth/sign-up.vue
<script setup lang="ts">
import SignUpForm from "@/components/sign-up-form.vue"
</script>

<template>
  <div class="flex min-h-screen w-full items-center justify-center p-6 md:p-10">
    <div class="w-full max-w-sm">
      <SignUpForm />
    </div>
  </div>
</template>
registry/default/password-based-auth/nuxtjs/app/pages/auth/forgot-password.vue
<script setup lang="ts">
import ForgotPasswordForm from "@/components/forgot-password-form.vue"
</script>

<template>
  <div class="flex min-h-screen w-full items-center justify-center p-6 md:p-10">
    <div class="w-full max-w-sm">
      <ForgotPasswordForm />
    </div>
  </div>
</template>
registry/default/password-based-auth/nuxtjs/app/pages/auth/update-password.vue
<script setup lang="ts">
import UpdatePasswordForm from "@/components/update-password-form.vue"
</script>

<template>
  <div class="flex min-h-screen w-full items-center justify-center p-6 md:p-10">
    <div class="w-full max-w-sm">
      <UpdatePasswordForm />
    </div>
  </div>
</template>
registry/default/password-based-auth/nuxtjs/app/pages/auth/error.vue
<script setup lang="ts">
import { useRoute } from "vue-router"
import { computed } from "vue"
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"

const route = useRoute()
const errorMessage = computed(() => route.query.error as string || null)
</script>

<template>
  <div class="flex min-h-screen w-full items-center justify-center p-6 md:p-10">
    <div class="w-full max-w-sm">
      <div class="flex flex-col gap-6">
        <Card>
          <CardHeader>
            <CardTitle class="text-2xl">Sorry, something went wrong.</CardTitle>
          </CardHeader>
          <CardContent>
            <p v-if="errorMessage" class="text-sm text-muted-foreground">
              Code error: {{ errorMessage }}
            </p>
            <p v-else class="text-sm text-muted-foreground">
              An unspecified error occurred.
            </p>
          </CardContent>
        </Card>
      </div>
    </div>
  </div>
</template>
registry/default/password-based-auth/nuxtjs/app/pages/auth/sign-up-success.vue
<script setup lang="ts">
import {
  Card,
  CardContent,
  CardDescription,
  CardHeader,
  CardTitle,
} from "@/components/ui/card"
</script>

<template>
  <div class="flex min-h-screen w-full items-center justify-center p-6 md:p-10">
    <div class="w-full max-w-sm">
      <div class="flex flex-col gap-6">
        <Card>
          <CardHeader>
            <CardTitle class="text-2xl">Thank you for signing up!</CardTitle>
            <CardDescription>Check your email to confirm</CardDescription>
          </CardHeader>
          <CardContent>
            <p class="text-sm text-muted-foreground">
              You’ve successfully signed up. Please check your email to confirm your account
              before signing in.
            </p>
          </CardContent>
        </Card>
      </div>
    </div>
  </div>
</template>
registry/default/password-based-auth/nuxtjs/app/pages/protected/index.vue
<script setup lang="ts">
import { onMounted, ref } from "vue"
import { useRouter } from "vue-router"
import { createClient } from "@/lib/supabase/client"

const router = useRouter()
const supabase = createClient()
const email = ref<string | null>(null)
const loading = ref(true)

onMounted(async () => {
  const { data, error } = await supabase.auth.getUser()

  if (error || !data?.user) {
    router.replace("/auth/login")
    return
  }

  email.value = data.user.email
  loading.value = false
})

const handleLogout = async () => {
  await supabase.auth.signOut()
  router.replace("/auth/login")
}
</script>

<template>
  <div class="flex h-screen w-full items-center justify-center">
    <div v-if="loading" class="text-muted-foreground">Checking authentication...</div>

    <div v-else class="flex items-center gap-2">
      <p>
        Hello <span class="font-semibold">{{ email }}</span>
      </p>
      <button
        class="rounded-md bg-primary text-primary-foreground px-3 py-2 text-sm font-medium hover:bg-primary/90"
        @click="handleLogout"
      >
        Sign out
      </button>
    </div>
  </div>
</template>
registry/default/password-based-auth/nuxtjs/server/routes/auth/confirm.get.ts
import type { EmailOtpType } from '@supabase/supabase-js'
import { defineEventHandler, getQuery, sendRedirect } from 'h3'

import { createSupabaseServerClient } from '@/registry/default/clients/nuxtjs/server/supabase/client'

export default defineEventHandler(async (event) => {
  const query = getQuery(event)
  const token_hash = query.token_hash as string | null
  const type = query.type as EmailOtpType | null
  const _next = query.next as string | undefined
  const next = _next?.startsWith('/') ? _next : '/'

  if (token_hash && type) {
    const supabase = createSupabaseServerClient(event)

    const { error } = await supabase.auth.verifyOtp({
      type,
      token_hash,
    })

    if (!error) {
      return sendRedirect(event, next)
    } else {
      return sendRedirect(event, `/auth/error?error=${encodeURIComponent(error.message)}`)
    }
  }

  return sendRedirect(event, `/auth/error?error=${encodeURIComponent('No token hash or type')}`)
})

More from supabase

Browse all components in this library →