Docs/vue nuxt social auth (#40662)

* docs: add vue social auth

* docs: add nuxt social auth

* docs: nuxt social auth typo

* docs: vue-nuxt-social-auth cr fix

* Apply suggestion from @saltcod

---------

Co-authored-by: Ivan Vasilov <vasilov.ivan@gmail.com>
Co-authored-by: Terry Sutton <saltcod@gmail.com>
This commit is contained in:
Jakub Andrzejewski
2025-12-03 19:59:18 +01:00
committed by GitHub
parent 380dfbc91d
commit c538489ab2
22 changed files with 646 additions and 16 deletions

View File

@@ -68,7 +68,7 @@ export const componentPages: SidebarNavGroup = {
},
{
title: 'Social Auth',
supportedFrameworks: ['nextjs', 'react-router', 'tanstack', 'react'],
supportedFrameworks: ['nextjs', 'react-router', 'tanstack', 'react', 'vue', 'nuxtjs'],
href: '/docs/nextjs/social-auth',
items: [],
new: true,

View File

@@ -70,7 +70,7 @@ NUXT_PUBLIC_SUPABASE_PUBLISHABLE_OR_ANON_KEY=
### Setting up routes and redirect URLs
1. Set the site URL in the [URL Configuration](https://supabase.com/dashboard/project/_/auth/url-configuration) settings in the Supabase Dashboard.
1. Set up the Next.js route that users will visit to reset or update their password. Go to the [URL Configuration](https://supabase.com/dashboard/project/_/auth/url-configuration) settings and add the `forgot-password` route to the list of Redirect URLs. It should look something like: `http://example.com/auth/forgot-password`.
1. Set up the Nuxt.js route that users will visit to reset or update their password. Go to the [URL Configuration](https://supabase.com/dashboard/project/_/auth/url-configuration) settings and add the `forgot-password` route to the list of Redirect URLs. It should look something like: `http://example.com/auth/forgot-password`.
1. Update the redirect paths in `login-form.vue` and `update-password-form.vue` components to point to the logged-in routes in your app. Our examples use `/protected`, but you can set this to whatever fits your app.

View File

@@ -0,0 +1,61 @@
---
title: Social Authentication
description: Social authentication block for Nuxt.js
---
<BlockPreview name="social-auth/auth/login" />
<Callout className="mt-4">
The block is using Github provider by default, but can be easily switched by changing a single
parameter.
</Callout>
## Installation
<BlockItem name="social-auth-nuxtjs" description="All needed components for the social auth flow" />
## Folder structure
This block assumes that you have already installed a Supabase client for Nuxt from the previous step.
<RegistryBlock itemName="social-auth-nuxtjs" />
## Usage
Once you install the block in your Nuxt.js project, you'll get all the necessary pages and components to set up a social authentication flow.
### Getting started
After installing the block, you'll have the following environment variables in your `.env.local` file:
```env
NUXT_PUBLIC_SUPABASE_URL=
NUXT_PUBLIC_SUPABASE_PUBLISHABLE_OR_ANON_KEY=
```
- If you're using supabase.com, you can find these values in the [Connect modal](https://supabase.com/dashboard/project/_?showConnect=true&connectTab=frameworks&framework=nuxtjs&using=supabasejs) under App Frameworks or in your project's [API settings](https://supabase.com/dashboard/project/_/settings/api).
- If you're using a local instance of Supabase, you can find these values by running `supabase start` or `supabase status` (if you already have it running).
### Setting up third party providers
We support a wide variety of social providers that you can use to integrate with your application. The full list is available [here](https://supabase.com/docs/guides/auth/social-login).
This block uses the PKCE flow with GitHub as the provider. To switch providers, just update the `provider` field in the `supabase.auth.signInWithOAuth` call. Enable the provider you want to use under [Auth Providers](https://supabase.com/dashboard/project/_/auth/providers) in the Supabase Dashboard and add the necessary credentials.
### Setting up routes and redirect URLs
1. Set the site URL in the [URL Configuration](https://supabase.com/dashboard/project/_/auth/url-configuration) settings in the Supabase Dashboard.
1. Update the redirect paths in `login-form.vue` to point to your apps logged-in routes. Our examples use `/protected`, but you can set this to whatever fits your app.
1. Visit `http://your-site-url/auth/login` to see this component in action.
### Combining social auth with password-based auth
If you want to combine this block with the password-based auth, you need to:
- Copy the `handleSocialLogin` function into the password-based `login-form.vue` component and bind it to a "Login with ..." button.
- Copy the `@/server/routes/auth/oauth.ts` in your app under the same route.
## Further reading
- [Social login](https://supabase.com/docs/guides/auth/social-login)
- [Authentication error codes](https://supabase.com/docs/guides/auth/debugging/error-codes)

View File

@@ -0,0 +1,57 @@
---
title: Social Authentication
description: Social authentication block for Vue Single Page Applications
---
<BlockPreview name="social-auth/auth/login" />
<Callout className="mt-4">
The block is using GitHub provider by default, but can be easily switched by changing a single
parameter.
</Callout>
## Installation
<BlockItem name="social-auth-vue" description="All needed components for the social auth flow" />
## Folder structure
This block assumes that you have already installed a Supabase client for Vue from the previous step.
<RegistryBlock itemName="social-auth-vue" />
## Usage
Once you install the block in your Vue project, you'll get all the necessary pages and components to set up a social authentication flow.
### Getting started
After installing the block, you'll have the following environment variables in your `.env.local` file:
```env
VITE_SUPABASE_URL=
VITE_SUPABASE_PUBLISHABLE_OR_ANON_KEY=
```
- If you're using supabase.com, you can find these values in the [Connect modal](https://supabase.com/dashboard/project/_?showConnect=true&connectTab=frameworks&framework=react&using=vite&with=supabasejs) under App Frameworks or in your project's [API settings](https://supabase.com/dashboard/project/_/settings/api).
- If you're using a local instance of Supabase, you can find these values by running `supabase start` or `supabase status` (if you already have it running).
### Setting up third party providers
We support a wide variety of social providers that you can use to integrate with your application. The full list is available [here](https://supabase.com/docs/guides/auth/social-login).
This block uses the implicit flow with GitHub as the provider. To switch providers, just update the `provider` field in the `supabase.auth.signInWithOAuth` call. Enable the provider you want to use under [Auth Providers](https://supabase.com/dashboard/project/_/auth/providers) in the Supabase Dashboard and add the necessary credentials.
### Setting up routes and redirect URLs
1. Set the site URL in the [URL Configuration](https://supabase.com/dashboard/project/_/auth/url-configuration) settings in the Supabase Dashboard.
1. Update the redirect paths in `login-form.vue` to point to your apps logged-in routes. Our examples use `/protected`, but you can set this to whatever fits your app.
### Combining social auth with password-based auth
If you want to combine this block with the password-based auth, you need to copy the `handleSocialLogin` function into the password-based `login-form.vue` component and bind it to a button.
## Further reading
- [Social login](https://supabase.com/docs/guides/auth/social-login)
- [Authentication error codes](https://supabase.com/docs/guides/auth/debugging/error-codes)

View File

@@ -1,5 +1,5 @@
# Supabase UI Library
Last updated: 2025-11-06T16:46:13.786Z
Last updated: 2025-11-20T11:14:14.146Z
## Overview
Library of components for your project. The components integrate with Supabase and are shadcn compatible.
@@ -35,6 +35,8 @@ Library of components for your project. The components integrate with Supabase a
- Supabase client for Nuxt.js
- [Password-based Authentication](https://supabase.com/ui/docs/nuxtjs/password-based-auth)
- Password-based authentication block for Nuxt.js
- [Social Authentication](https://supabase.com/ui/docs/nuxtjs/social-auth)
- Social authentication block for Nuxt.js
- [Platform Kit](https://supabase.com/ui/docs/platform/platform-kit)
- The easiest way to build platforms on top of Supabase
- [Supabase Client Libraries](https://supabase.com/ui/docs/react-router/client)
@@ -89,3 +91,5 @@ Library of components for your project. The components integrate with Supabase a
- Supabase client for Vue Single Page Applications
- [Password-based Authentication](https://supabase.com/ui/docs/vue/password-based-auth)
- Password-based authentication block for Vue Single Page Applications
- [Social Authentication](https://supabase.com/ui/docs/vue/social-auth)
- Social authentication block for Vue Single Page Applications

View File

@@ -17,22 +17,26 @@
{
"path": "registry/default/password-based-auth/vue/components/login-form.vue",
"content": "<script setup lang=\"ts\">\nimport { ref } from \"vue\"\nimport { createClient } from \"@/lib/supabase/client\"\nimport { Button } from \"@/components/ui/button\"\nimport {\n Card,\n CardContent,\n CardDescription,\n CardHeader,\n CardTitle,\n} from \"@/components/ui/card\"\nimport { Input } from \"@/components/ui/input\"\nimport { Label } from \"@/components/ui/label\"\n\nconst email = ref(\"\")\nconst error = ref<string | null>(null)\nconst success = ref(false)\nconst isLoading = ref(false)\n\nconst handleForgotPassword = async (e: Event) => {\n e.preventDefault()\n const supabase = createClient()\n isLoading.value = true\n error.value = null\n\n try {\n const { error: supabaseError } = await supabase.auth.resetPasswordForEmail(email.value, {\n redirectTo: \"http://localhost:3000/update-password\",\n })\n if (supabaseError) throw supabaseError\n success.value = true\n } catch (err: unknown) {\n error.value = err instanceof Error ? err.message : \"An error occurred\"\n } finally {\n isLoading.value = false\n }\n}\n</script>\n\n<template>\n <div class=\"flex flex-col gap-6\">\n <Card v-if=\"success\">\n <CardHeader>\n <CardTitle class=\"text-2xl\">Check Your Email</CardTitle>\n <CardDescription>Password reset instructions sent</CardDescription>\n </CardHeader>\n <CardContent>\n <p class=\"text-sm text-muted-foreground\">\n If you registered using your email and password, you will receive a password reset email.\n </p>\n </CardContent>\n </Card>\n\n <Card v-else>\n <CardHeader>\n <CardTitle class=\"text-2xl\">Reset Your Password</CardTitle>\n <CardDescription>\n Type in your email and we&apos;ll send you a link to reset your password\n </CardDescription>\n </CardHeader>\n <CardContent>\n <form @submit=\"handleForgotPassword\">\n <div class=\"flex flex-col gap-6\">\n <div class=\"grid gap-2\">\n <Label for=\"email\">Email</Label>\n <Input\n id=\"email\"\n type=\"email\"\n placeholder=\"m@example.com\"\n required\n v-model=\"email\"\n />\n </div>\n <p v-if=\"error\" class=\"text-sm text-red-500\">{{ error }}</p>\n <Button type=\"submit\" class=\"w-full\" :disabled=\"isLoading\">\n {{ isLoading ? \"Sending...\" : \"Send reset email\" }}\n </Button>\n </div>\n <div class=\"mt-4 text-center text-sm\">\n Already have an account?\n <a href=\"/login\" class=\"underline underline-offset-4\">Login</a>\n </div>\n </form>\n </CardContent>\n </Card>\n </div>\n</template>\n",
"type": "registry:component"
"type": "registry:component",
"target": "components/login-form.vue"
},
{
"path": "registry/default/password-based-auth/vue/components/sign-up-form.vue",
"content": "<script setup lang=\"ts\">\nimport { ref } from \"vue\"\nimport { createClient } from \"@/lib/supabase/client\"\nimport { Button } from \"@/components/ui/button\"\nimport {\n Card,\n CardContent,\n CardDescription,\n CardHeader,\n CardTitle,\n} from \"@/components/ui/card\"\nimport { Input } from \"@/components/ui/input\"\nimport { Label } from \"@/components/ui/label\"\n\nconst email = ref(\"\")\nconst password = ref(\"\")\nconst repeatPassword = ref(\"\")\nconst error = ref<string | null>(null)\nconst isLoading = ref(false)\nconst success = ref(false)\n\nconst handleSignUp = async () => {\n const supabase = createClient()\n error.value = null\n\n if (password.value !== repeatPassword.value) {\n error.value = \"Passwords do not match\"\n return\n }\n\n isLoading.value = true\n try {\n const { error: supabaseError } = await supabase.auth.signUp({\n email: email.value,\n password: password.value,\n })\n if (supabaseError) throw supabaseError\n success.value = true\n } catch (err: unknown) {\n error.value = err instanceof Error ? err.message : \"An error occurred\"\n } finally {\n isLoading.value = false\n }\n}\n</script>\n\n<template>\n <div class=\"flex flex-col gap-6\">\n <Card v-if=\"success\">\n <CardHeader>\n <CardTitle class=\"text-2xl\">Thank you for signing up!</CardTitle>\n <CardDescription>Check your email to confirm</CardDescription>\n </CardHeader>\n <CardContent>\n <p class=\"text-sm text-muted-foreground\">\n You've successfully signed up. Please check your email to confirm your account before\n signing in.\n </p>\n </CardContent>\n </Card>\n\n <Card v-else>\n <CardHeader>\n <CardTitle class=\"text-2xl\">Sign up</CardTitle>\n <CardDescription>Create a new account</CardDescription>\n </CardHeader>\n <CardContent>\n <form @submit.prevent=\"handleSignUp\">\n <div class=\"flex flex-col gap-6\">\n <!-- Email -->\n <div class=\"grid gap-2\">\n <Label for=\"email\">Email</Label>\n <Input\n id=\"email\"\n type=\"email\"\n placeholder=\"m@example.com\"\n required\n v-model=\"email\"\n />\n </div>\n\n <!-- Password -->\n <div class=\"grid gap-2\">\n <div class=\"flex items-center\">\n <Label for=\"password\">Password</Label>\n </div>\n <Input\n id=\"password\"\n type=\"password\"\n required\n v-model=\"password\"\n />\n </div>\n\n <!-- Repeat Password -->\n <div class=\"grid gap-2\">\n <div class=\"flex items-center\">\n <Label for=\"repeat-password\">Repeat Password</Label>\n </div>\n <Input\n id=\"repeat-password\"\n type=\"password\"\n required\n v-model=\"repeatPassword\"\n />\n </div>\n\n <!-- Error -->\n <p v-if=\"error\" class=\"text-sm text-red-500\">{{ error }}</p>\n\n <!-- Submit -->\n <Button type=\"submit\" class=\"w-full\" :disabled=\"isLoading\">\n {{ isLoading ? \"Creating an account...\" : \"Sign up\" }}\n </Button>\n </div>\n\n <div class=\"mt-4 text-center text-sm\">\n Already have an account?\n <a href=\"/login\" class=\"underline underline-offset-4\">Login</a>\n </div>\n </form>\n </CardContent>\n </Card>\n </div>\n</template>\n",
"type": "registry:component"
"type": "registry:component",
"target": "components/sign-up-form.vue"
},
{
"path": "registry/default/password-based-auth/vue/components/forgot-password-form.vue",
"content": "<script setup lang=\"ts\">\nimport { ref } from \"vue\"\nimport { createClient } from \"@/lib/supabase/client\"\nimport { Button } from \"@/components/ui/button\"\nimport {\n Card,\n CardContent,\n CardDescription,\n CardHeader,\n CardTitle,\n} from \"@/components/ui/card\"\nimport { Input } from \"@/components/ui/input\"\nimport { Label } from \"@/components/ui/label\"\n\n\nconst email = ref(\"\")\nconst error = ref<string | null>(null)\nconst success = ref(false)\nconst isLoading = ref(false)\n\nconst handleForgotPassword = async (e: Event) => {\n e.preventDefault()\n const supabase = createClient()\n isLoading.value = true\n error.value = null\n\n try {\n const { error: supabaseError } = await supabase.auth.resetPasswordForEmail(email.value, {\n redirectTo: \"http://localhost:3000/update-password\",\n })\n if (supabaseError) throw supabaseError\n success.value = true\n } catch (err: unknown) {\n error.value = err instanceof Error ? err.message : \"An error occurred\"\n } finally {\n isLoading.value = false\n }\n}\n</script>\n\n<template>\n <div class=\"flex flex-col gap-6\">\n <Card v-if=\"success\">\n <CardHeader>\n <CardTitle class=\"text-2xl\">Check Your Email</CardTitle>\n <CardDescription>Password reset instructions sent</CardDescription>\n </CardHeader>\n <CardContent>\n <p class=\"text-sm text-muted-foreground\">\n If you registered using your email and password, you will receive a password reset email.\n </p>\n </CardContent>\n </Card>\n\n <Card v-else>\n <CardHeader>\n <CardTitle class=\"text-2xl\">Reset Your Password</CardTitle>\n <CardDescription>\n Type in your email and we&apos;ll send you a link to reset your password\n </CardDescription>\n </CardHeader>\n <CardContent>\n <form @submit=\"handleForgotPassword\">\n <div class=\"flex flex-col gap-6\">\n <div class=\"grid gap-2\">\n <Label for=\"email\">Email</Label>\n <Input\n id=\"email\"\n type=\"email\"\n placeholder=\"m@example.com\"\n required\n v-model=\"email\"\n />\n </div>\n <p v-if=\"error\" class=\"text-sm text-red-500\">{{ error }}</p>\n <Button type=\"submit\" class=\"w-full\" :disabled=\"isLoading\">\n {{ isLoading ? \"Sending...\" : \"Send reset email\" }}\n </Button>\n </div>\n <div class=\"mt-4 text-center text-sm\">\n Already have an account?\n <a href=\"/login\" class=\"underline underline-offset-4\">Login</a>\n </div>\n </form>\n </CardContent>\n </Card>\n </div>\n</template>\n",
"type": "registry:component"
"type": "registry:component",
"target": "components/forgot-password-form.vue"
},
{
"path": "registry/default/password-based-auth/vue/components/update-password-form.vue",
"content": "<script setup lang=\"ts\">\nimport { ref } from \"vue\"\nimport { createClient } from \"@/lib/supabase/client\"\nimport { Button } from \"@/components/ui/button\"\nimport {\n Card,\n CardContent,\n CardDescription,\n CardHeader,\n CardTitle,\n} from \"@/components/ui/card\"\nimport { Input } from \"@/components/ui/input\"\nimport { Label } from \"@/components/ui/label\"\n\nconst password = ref(\"\")\nconst error = ref<string | null>(null)\nconst isLoading = ref(false)\n\nconst handleUpdatePassword = async () => {\n const supabase = createClient()\n isLoading.value = true\n error.value = null\n\n try {\n const { error: supabaseError } = await supabase.auth.updateUser({\n password: password.value,\n })\n if (supabaseError) throw supabaseError\n // Redirect user after successful password update\n location.href = \"/protected\"\n } catch (err: unknown) {\n error.value = err instanceof Error ? err.message : \"An error occurred\"\n } finally {\n isLoading.value = false\n }\n}\n</script>\n\n<template>\n <div class=\"flex flex-col gap-6\">\n <Card>\n <CardHeader>\n <CardTitle class=\"text-2xl\">Reset Your Password</CardTitle>\n <CardDescription>Please enter your new password below.</CardDescription>\n </CardHeader>\n <CardContent>\n <form @submit.prevent=\"handleUpdatePassword\">\n <div class=\"flex flex-col gap-6\">\n <div class=\"grid gap-2\">\n <Label for=\"password\">New password</Label>\n <Input\n id=\"password\"\n type=\"password\"\n placeholder=\"New password\"\n required\n v-model=\"password\"\n />\n </div>\n <p v-if=\"error\" class=\"text-sm text-red-500\">{{ error }}</p>\n <Button type=\"submit\" class=\"w-full\" :disabled=\"isLoading\">\n {{ isLoading ? \"Saving...\" : \"Save new password\" }}\n </Button>\n </div>\n </form>\n </CardContent>\n </Card>\n </div>\n</template>\n",
"type": "registry:component"
"type": "registry:component",
"target": "components/update-password-form.vue"
}
]
}

View File

@@ -1853,25 +1853,102 @@
"files": [
{
"path": "registry/default/password-based-auth/vue/components/login-form.vue",
"type": "registry:component"
"type": "registry:component",
"target": "components/login-form.vue"
},
{
"path": "registry/default/password-based-auth/vue/components/sign-up-form.vue",
"type": "registry:component"
"type": "registry:component",
"target": "components/sign-up-form.vue"
},
{
"path": "registry/default/password-based-auth/vue/components/forgot-password-form.vue",
"type": "registry:component"
"type": "registry:component",
"target": "components/forgot-password-form.vue"
},
{
"path": "registry/default/password-based-auth/vue/components/update-password-form.vue",
"type": "registry:component"
"type": "registry:component",
"target": "components/update-password-form.vue"
}
],
"dependencies": [
"@supabase/supabase-js@latest"
]
},
{
"name": "social-auth-vue",
"type": "registry:block",
"title": "Social Auth flow for Vue and Supabase",
"description": "Social Auth flow for Vue and Supabase",
"registryDependencies": [
"button",
"card"
],
"files": [
{
"path": "registry/default/social-auth/vue/components/login-form.vue",
"type": "registry:component",
"target": "components/login-form.vue"
}
],
"dependencies": [
"@supabase/supabase-js@latest"
]
},
{
"name": "social-auth-nuxtjs",
"type": "registry:block",
"title": "Social Auth flow for Nuxt and Supabase",
"description": "Social Auth flow for Nuxt and Supabase",
"registryDependencies": [
"button",
"card",
"input",
"label"
],
"files": [
{
"path": "registry/default/social-auth/nuxtjs/app/components/login-form.vue",
"type": "registry:file",
"target": "app/components/login-form.vue"
},
{
"path": "registry/default/social-auth/nuxtjs/app/components/logout-button.vue",
"type": "registry:file",
"target": "app/components/logout-button.vue"
},
{
"path": "registry/default/social-auth/nuxtjs/app/pages/auth/login.vue",
"type": "registry:file",
"target": "app/pages/auth/login.vue"
},
{
"path": "registry/default/social-auth/nuxtjs/app/pages/auth/error.vue",
"type": "registry:file",
"target": "app/pages/auth/error.vue"
},
{
"path": "registry/default/social-auth/nuxtjs/app/pages/protected/index.vue",
"type": "registry:file",
"target": "app/pages/protected/index.vue"
},
{
"path": "registry/default/social-auth/nuxtjs/server/middleware/auth.ts",
"type": "registry:file",
"target": "server/middleware/auth.ts"
},
{
"path": "registry/default/social-auth/nuxtjs/server/routes/auth/oauth.ts",
"type": "registry:file",
"target": "server/routes/auth/oauth.ts"
}
],
"dependencies": [
"@supabase/ssr@latest",
"@supabase/supabase-js@latest"
]
},
{
"$schema": "https://ui.shadcn.com/schema/registry-item.json",
"name": "ai-editor-rules",

View File

@@ -0,0 +1,61 @@
{
"$schema": "https://ui.shadcn.com/schema/registry-item.json",
"name": "social-auth-nuxtjs",
"type": "registry:block",
"title": "Social Auth flow for Nuxt and Supabase",
"description": "Social Auth flow for Nuxt and Supabase",
"dependencies": [
"@supabase/ssr@latest",
"@supabase/supabase-js@latest"
],
"registryDependencies": [
"button",
"card",
"input",
"label"
],
"files": [
{
"path": "registry/default/social-auth/nuxtjs/app/components/login-form.vue",
"content": "<script setup lang=\"ts\">\nimport { ref } from \"vue\"\nimport { createClient } from \"@/lib/supabase/client\"\nimport { cn } from \"@/lib/utils\"\n\nimport { Button } from \"@/components/ui/button.vue\"\nimport {\n Card,\n CardContent,\n CardDescription,\n CardHeader,\n CardTitle,\n} from \"@/components/ui/card.vue\"\n\n// Reactive state\nconst error = ref<string | null>(null)\nconst isLoading = ref(false)\n\n// GitHub OAuth login handler\nconst handleSocialLogin = async (e: Event) => {\n e.preventDefault()\n const supabase = createClient()\n isLoading.value = true\n error.value = null\n\n try {\n const { error: supabaseError } = await supabase.auth.signInWithOAuth({\n provider: \"github\",\n options: {\n redirectTo: `${window.location.origin}/api/routes/oauth?next=/protected`,\n },\n })\n\n if (supabaseError) throw supabaseError\n } catch (err: unknown) {\n error.value = err instanceof Error ? err.message : \"An error occurred\"\n isLoading.value = false\n }\n}\n</script>\n\n<template>\n <div :class=\"cn('flex flex-col gap-6')\">\n <Card>\n <CardHeader>\n <CardTitle class=\"text-2xl\">Welcome!</CardTitle>\n <CardDescription>Sign in to your account to continue</CardDescription>\n </CardHeader>\n <CardContent>\n <form @submit=\"handleSocialLogin\">\n <div class=\"flex flex-col gap-6\">\n <p v-if=\"error\" class=\"text-sm text-destructive-500\">{{ error }}</p>\n <Button type=\"submit\" class=\"w-full\" :disabled=\"isLoading\">\n {{ isLoading ? \"Logging in...\" : \"Continue with GitHub\" }}\n </Button>\n </div>\n </form>\n </CardContent>\n </Card>\n </div>\n</template>\n",
"type": "registry:file",
"target": "app/components/login-form.vue"
},
{
"path": "registry/default/social-auth/nuxtjs/app/components/logout-button.vue",
"content": "<script setup lang=\"ts\">\nimport { createClient } from \"@/lib/supabase/client\"\nimport { useRouter } from \"vue-router\"\nimport { Button } from \"@/components/ui/button.vue\"\n\nconst router = useRouter()\n\nconst logout = async () => {\n const supabase = createClient()\n await supabase.auth.signOut()\n router.push(\"/auth/login\")\n}\n</script>\n\n<template>\n <Button @click=\"logout\">Logout</Button>\n</template>\n",
"type": "registry:file",
"target": "app/components/logout-button.vue"
},
{
"path": "registry/default/social-auth/nuxtjs/app/pages/auth/login.vue",
"content": "<script setup lang=\"ts\">\nimport LoginForm from \"@/components/login-form.vue\"\n</script>\n\n<template>\n <div class=\"flex min-h-screen w-full items-center justify-center p-6 md:p-10\">\n <div class=\"w-full max-w-sm\">\n <LoginForm />\n </div>\n </div>\n</template>",
"type": "registry:file",
"target": "app/pages/auth/login.vue"
},
{
"path": "registry/default/social-auth/nuxtjs/app/pages/auth/error.vue",
"content": "<script setup lang=\"ts\">\nimport { useRoute } from \"vue-router\"\nimport { computed } from \"vue\"\nimport { Card, CardContent, CardHeader, CardTitle } from \"@/components/ui/card\"\n\nconst route = useRoute()\nconst errorMessage = computed(() => route.query.error as string || null)\n</script>\n\n<template>\n <div class=\"flex min-h-screen w-full items-center justify-center p-6 md:p-10\">\n <div class=\"w-full max-w-sm\">\n <div class=\"flex flex-col gap-6\">\n <Card>\n <CardHeader>\n <CardTitle class=\"text-2xl\">Sorry, something went wrong.</CardTitle>\n </CardHeader>\n <CardContent>\n <p v-if=\"errorMessage\" class=\"text-sm text-muted-foreground\">\n Code error: {{ errorMessage }}\n </p>\n <p v-else class=\"text-sm text-muted-foreground\">\n An unspecified error occurred.\n </p>\n </CardContent>\n </Card>\n </div>\n </div>\n </div>\n</template>",
"type": "registry:file",
"target": "app/pages/auth/error.vue"
},
{
"path": "registry/default/social-auth/nuxtjs/app/pages/protected/index.vue",
"content": "<script setup lang=\"ts\">\nimport { onMounted, ref } from \"vue\"\nimport { useRouter } from \"vue-router\"\nimport { createClient } from \"@/lib/supabase/client\"\nimport LogoutButton from \"@/components/logout-button.vue\"\n\nconst router = useRouter()\nconst supabase = createClient()\nconst email = ref<string | null>(null)\nconst loading = ref(true)\n\nonMounted(async () => {\n const { data, error } = await supabase.auth.getUser()\n\n if (error || !data?.user) {\n router.replace(\"/auth/login\")\n return\n }\n\n email.value = data.user.email\n loading.value = false\n})\n</script>\n\n<template>\n <div class=\"flex h-screen w-full items-center justify-center\">\n <div v-if=\"loading\" class=\"text-muted-foreground\">Checking authentication...</div>\n\n <div v-else class=\"flex items-center gap-2\">\n <p>\n Hello <span class=\"font-semibold\">{{ email }}</span>\n </p>\n <LogoutButton />\n </div>\n </div>\n</template>\n",
"type": "registry:file",
"target": "app/pages/protected/index.vue"
},
{
"path": "registry/default/social-auth/nuxtjs/server/middleware/auth.ts",
"content": "import { defineEventHandler, sendRedirect } from 'h3'\nimport { createSupabaseServerClient } from '@/registry/default/clients/nuxtjs/server/supabase/client'\n\nexport default defineEventHandler(async (event) => {\n const supabase = createSupabaseServerClient(event)\n\n // Get user claims\n const { data } = await supabase.auth.getClaims()\n const user = data?.claims\n\n const pathname = event.node.req.url || '/'\n\n // Redirect if no user and not already on login/auth route\n if (\n !user &&\n !pathname.startsWith('/login') &&\n !pathname.startsWith('/auth')\n ) {\n return sendRedirect(event, '/auth/login')\n }\n\n // Return event as-is (you could return any object if needed)\n return { user }\n})\n",
"type": "registry:file",
"target": "server/middleware/auth.ts"
},
{
"path": "registry/default/social-auth/nuxtjs/server/routes/auth/oauth.ts",
"content": "import { createSupabaseServerClient } from '@/registry/default/clients/nuxtjs/server/supabase/client'\nimport { defineEventHandler, getQuery, sendRedirect, getRequestURL } from \"h3\"\n\nexport default defineEventHandler(async (event) => {\n const url = getRequestURL(event) // URL object of the current request\n const query = getQuery(event)\n\n const code = query.code as string | undefined\n let next = (query.next as string | undefined) ?? \"/\"\n\n if (!next.startsWith(\"/\")) {\n next = \"/\"\n }\n\n if (code) {\n const supabase = createSupabaseServerClient(event)\n const { error } = await supabase.auth.exchangeCodeForSession(code)\n\n if (!error) {\n // Determine origin\n const forwardedHost = event.node.req.headers[\"x-forwarded-host\"] as string | undefined\n const isLocalEnv = process.env.NODE_ENV === \"development\"\n const origin = `${url.protocol}//${url.host}`\n\n if (isLocalEnv) {\n return sendRedirect(event, `${origin}${next}`)\n } else if (forwardedHost) {\n return sendRedirect(event, `https://${forwardedHost}${next}`)\n } else {\n return sendRedirect(event, `${origin}${next}`)\n }\n }\n }\n\n // fallback to error page\n const origin = `${url.protocol}//${url.host}`\n return sendRedirect(event, `${origin}/auth/error`)\n})\n",
"type": "registry:file",
"target": "server/routes/auth/oauth.ts"
}
]
}

View File

@@ -0,0 +1,22 @@
{
"$schema": "https://ui.shadcn.com/schema/registry-item.json",
"name": "social-auth-vue",
"type": "registry:block",
"title": "Social Auth flow for Vue and Supabase",
"description": "Social Auth flow for Vue and Supabase",
"dependencies": [
"@supabase/supabase-js@latest"
],
"registryDependencies": [
"button",
"card"
],
"files": [
{
"path": "registry/default/social-auth/vue/components/login-form.vue",
"content": "<script setup lang=\"ts\">\nimport { ref } from \"vue\"\nimport { createClient } from \"@/lib/supabase/client\"\nimport { cn } from \"@/lib/utils\"\n\nimport { Button } from \"@/components/ui/button\"\nimport {\n Card,\n CardContent,\n CardDescription,\n CardHeader,\n CardTitle,\n} from \"@/components/ui/card\"\n\nconst error = ref<string | null>(null)\nconst isLoading = ref(false)\n\nconst handleSocialLogin = async (e: Event) => {\n e.preventDefault()\n const supabase = createClient()\n isLoading.value = true\n error.value = null\n\n try {\n const { error: supabaseError } = await supabase.auth.signInWithOAuth({\n provider: \"github\",\n })\n\n if (supabaseError) throw supabaseError\n window.location.href = \"/protected\"\n } catch (err: unknown) {\n error.value = err instanceof Error ? err.message : \"An error occurred\"\n isLoading.value = false\n }\n}\n</script>\n\n<template>\n <div :class=\"cn('flex flex-col gap-6')\">\n <Card>\n <CardHeader>\n <CardTitle class=\"text-2xl\">Welcome!</CardTitle>\n <CardDescription>Sign in to your account to continue</CardDescription>\n </CardHeader>\n <CardContent>\n <form @submit=\"handleSocialLogin\">\n <div class=\"flex flex-col gap-6\">\n <p v-if=\"error\" class=\"text-sm text-destructive-500\">{{ error }}</p>\n <Button type=\"submit\" class=\"w-full\" :disabled=\"isLoading\">\n {{ isLoading ? \"Logging in...\" : \"Continue with GitHub\" }}\n </Button>\n </div>\n </form>\n </CardContent>\n </Card>\n </div>\n</template>\n",
"type": "registry:component",
"target": "components/login-form.vue"
}
]
}

View File

@@ -7,19 +7,23 @@
"files": [
{
"path": "registry/default/password-based-auth/vue/components/login-form.vue",
"type": "registry:component"
"type": "registry:component",
"target": "components/login-form.vue"
},
{
"path": "registry/default/password-based-auth/vue/components/sign-up-form.vue",
"type": "registry:component"
"type": "registry:component",
"target": "components/sign-up-form.vue"
},
{
"path": "registry/default/password-based-auth/vue/components/forgot-password-form.vue",
"type": "registry:component"
"type": "registry:component",
"target": "components/forgot-password-form.vue"
},
{
"path": "registry/default/password-based-auth/vue/components/update-password-form.vue",
"type": "registry:component"
"type": "registry:component",
"target": "components/update-password-form.vue"
}
],
"dependencies": ["@supabase/supabase-js@latest"]

View File

@@ -0,0 +1,61 @@
<script setup lang="ts">
import { ref } from "vue"
import { createClient } from "@/lib/supabase/client"
import { cn } from "@/lib/utils"
import { Button } from "@/components/ui/button.vue"
import {
Card,
CardContent,
CardDescription,
CardHeader,
CardTitle,
} from "@/components/ui/card.vue"
// Reactive state
const error = ref<string | null>(null)
const isLoading = ref(false)
// GitHub OAuth login handler
const handleSocialLogin = async (e: Event) => {
e.preventDefault()
const supabase = createClient()
isLoading.value = true
error.value = null
try {
const { error: supabaseError } = await supabase.auth.signInWithOAuth({
provider: "github",
options: {
redirectTo: `${window.location.origin}/api/routes/oauth?next=/protected`,
},
})
if (supabaseError) throw supabaseError
} catch (err: unknown) {
error.value = err instanceof Error ? err.message : "An error occurred"
isLoading.value = false
}
}
</script>
<template>
<div :class="cn('flex flex-col gap-6')">
<Card>
<CardHeader>
<CardTitle class="text-2xl">Welcome!</CardTitle>
<CardDescription>Sign in to your account to continue</CardDescription>
</CardHeader>
<CardContent>
<form @submit="handleSocialLogin">
<div class="flex flex-col gap-6">
<p v-if="error" class="text-sm text-destructive-500">{{ error }}</p>
<Button type="submit" class="w-full" :disabled="isLoading">
{{ isLoading ? "Logging in..." : "Continue with GitHub" }}
</Button>
</div>
</form>
</CardContent>
</Card>
</div>
</template>

View File

@@ -0,0 +1,17 @@
<script setup lang="ts">
import { createClient } from "@/lib/supabase/client"
import { useRouter } from "vue-router"
import { Button } from "@/components/ui/button.vue"
const router = useRouter()
const logout = async () => {
const supabase = createClient()
await supabase.auth.signOut()
router.push("/auth/login")
}
</script>
<template>
<Button @click="logout">Logout</Button>
</template>

View File

@@ -0,0 +1,30 @@
<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>

View File

@@ -0,0 +1,11 @@
<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>

View File

@@ -0,0 +1,36 @@
<script setup lang="ts">
import { onMounted, ref } from "vue"
import { useRouter } from "vue-router"
import { createClient } from "@/lib/supabase/client"
import LogoutButton from "@/components/logout-button.vue"
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
})
</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>
<LogoutButton />
</div>
</div>
</template>

View File

@@ -0,0 +1,45 @@
{
"name": "social-auth-nuxtjs",
"type": "registry:block",
"title": "Social Auth flow for Nuxt and Supabase",
"description": "Social Auth flow for Nuxt and Supabase",
"registryDependencies": ["button", "card", "input", "label"],
"files": [
{
"path": "registry/default/social-auth/nuxtjs/app/components/login-form.vue",
"type": "registry:file",
"target": "app/components/login-form.vue"
},
{
"path": "registry/default/social-auth/nuxtjs/app/components/logout-button.vue",
"type": "registry:file",
"target": "app/components/logout-button.vue"
},
{
"path": "registry/default/social-auth/nuxtjs/app/pages/auth/login.vue",
"type": "registry:file",
"target": "app/pages/auth/login.vue"
},
{
"path": "registry/default/social-auth/nuxtjs/app/pages/auth/error.vue",
"type": "registry:file",
"target": "app/pages/auth/error.vue"
},
{
"path": "registry/default/social-auth/nuxtjs/app/pages/protected/index.vue",
"type": "registry:file",
"target": "app/pages/protected/index.vue"
},
{
"path": "registry/default/social-auth/nuxtjs/server/middleware/auth.ts",
"type": "registry:file",
"target": "server/middleware/auth.ts"
},
{
"path": "registry/default/social-auth/nuxtjs/server/routes/auth/oauth.ts",
"type": "registry:file",
"target": "server/routes/auth/oauth.ts"
}
],
"dependencies": ["@supabase/ssr@latest", "@supabase/supabase-js@latest"]
}

View File

@@ -0,0 +1,24 @@
import { defineEventHandler, sendRedirect } from 'h3'
import { createSupabaseServerClient } from '@/registry/default/clients/nuxtjs/server/supabase/client'
export default defineEventHandler(async (event) => {
const supabase = createSupabaseServerClient(event)
// Get user claims
const { data } = await supabase.auth.getClaims()
const user = data?.claims
const pathname = event.node.req.url || '/'
// Redirect if no user and not already on login/auth route
if (
!user &&
!pathname.startsWith('/login') &&
!pathname.startsWith('/auth')
) {
return sendRedirect(event, '/auth/login')
}
// Return event as-is (you could return any object if needed)
return { user }
})

View File

@@ -0,0 +1,38 @@
import { createSupabaseServerClient } from '@/registry/default/clients/nuxtjs/server/supabase/client'
import { defineEventHandler, getQuery, sendRedirect, getRequestURL } from "h3"
export default defineEventHandler(async (event) => {
const url = getRequestURL(event) // URL object of the current request
const query = getQuery(event)
const code = query.code as string | undefined
let next = (query.next as string | undefined) ?? "/"
if (!next.startsWith("/")) {
next = "/"
}
if (code) {
const supabase = createSupabaseServerClient(event)
const { error } = await supabase.auth.exchangeCodeForSession(code)
if (!error) {
// Determine origin
const forwardedHost = event.node.req.headers["x-forwarded-host"] as string | undefined
const isLocalEnv = process.env.NODE_ENV === "development"
const origin = `${url.protocol}//${url.host}`
if (isLocalEnv) {
return sendRedirect(event, `${origin}${next}`)
} else if (forwardedHost) {
return sendRedirect(event, `https://${forwardedHost}${next}`)
} else {
return sendRedirect(event, `${origin}${next}`)
}
}
}
// fallback to error page
const origin = `${url.protocol}//${url.host}`
return sendRedirect(event, `${origin}/auth/error`)
})

View File

@@ -0,0 +1,57 @@
<script setup lang="ts">
import { ref } from "vue"
import { createClient } from "@/lib/supabase/client"
import { cn } from "@/lib/utils"
import { Button } from "@/components/ui/button"
import {
Card,
CardContent,
CardDescription,
CardHeader,
CardTitle,
} from "@/components/ui/card"
const error = ref<string | null>(null)
const isLoading = ref(false)
const handleSocialLogin = async (e: Event) => {
e.preventDefault()
const supabase = createClient()
isLoading.value = true
error.value = null
try {
const { error: supabaseError } = await supabase.auth.signInWithOAuth({
provider: "github",
})
if (supabaseError) throw supabaseError
window.location.href = "/protected"
} catch (err: unknown) {
error.value = err instanceof Error ? err.message : "An error occurred"
isLoading.value = false
}
}
</script>
<template>
<div :class="cn('flex flex-col gap-6')">
<Card>
<CardHeader>
<CardTitle class="text-2xl">Welcome!</CardTitle>
<CardDescription>Sign in to your account to continue</CardDescription>
</CardHeader>
<CardContent>
<form @submit="handleSocialLogin">
<div class="flex flex-col gap-6">
<p v-if="error" class="text-sm text-destructive-500">{{ error }}</p>
<Button type="submit" class="w-full" :disabled="isLoading">
{{ isLoading ? "Logging in..." : "Continue with GitHub" }}
</Button>
</div>
</form>
</CardContent>
</Card>
</div>
</template>

View File

@@ -0,0 +1,15 @@
{
"name": "social-auth-vue",
"type": "registry:block",
"title": "Social Auth flow for Vue and Supabase",
"description": "Social Auth flow for Vue and Supabase",
"registryDependencies": ["button", "card"],
"files": [
{
"path": "registry/default/social-auth/vue/components/login-form.vue",
"type": "registry:component",
"target": "components/login-form.vue"
}
],
"dependencies": ["@supabase/supabase-js@latest"]
}

View File

@@ -1,6 +1,7 @@
import { clients } from './clients'
import { passwordBasedAuth } from './password-based-auth'
import { socialAuth } from './social-auth'
const blocks = [...clients, ...passwordBasedAuth]
const blocks = [...clients, ...passwordBasedAuth, ...socialAuth]
export { blocks }

View File

@@ -0,0 +1,5 @@
import { type Registry } from 'shadcn/schema'
import vue from './default/social-auth/vue/registry-item.json' with { type: 'json' }
import nuxt from './default/social-auth/nuxtjs/registry-item.json' with { type: 'json' }
export const socialAuth = [vue, nuxt] as Registry['items']