Files
better-auth/docs/components/builder/code-tabs/frameworks/nuxt.ts
2026-01-14 13:22:14 -08:00

583 lines
13 KiB
TypeScript

import { socialProviders } from "../../social-provider";
import type { SignInBoxOptions } from "../../store";
export function resolveNuxtFiles(options: SignInBoxOptions) {
const files = [
{
id: "1",
name: "auth.ts",
content: `import { betterAuth } from "better-auth";${
options.magicLink
? `
import { magicLink } from "better-auth/plugins/magic-link";`
: ""
}${
options.passkey
? `
import { passkey } from "@better-auth/passkey";`
: ""
}
export const auth = betterAuth({
${
options.email
? `emailAndPassword: {
enabled: true,
${
options.requestPasswordReset
? `async sendResetPassword(data, request) {
// Send an email to the user with a link to reset their password
},`
: ``
}
},`
: ""
}${
options.socialProviders.length
? `socialProviders: ${JSON.stringify(
options.socialProviders.reduce(
(acc, provider) => ({
...acc,
[provider]: {
clientId: `process.env.${provider.toUpperCase()}_CLIENT_ID!`,
clientSecret: `process.env.${provider.toUpperCase()}_CLIENT_SECRET!`,
},
}),
{},
),
).replace(/"/g, "")},`
: ""
}${
options.magicLink || options.passkey
? `
plugins: [
${
options.magicLink
? `magicLink({
async sendMagicLink(data) {
// Send an email to the user with a magic link
},
}),`
: `${options.passkey ? `passkey(),` : ""}`
}
${options.passkey && options.magicLink ? `passkey(),` : ""}],`
: ""
}
/** if no database is provided, the user data will be stored in memory.
* Make sure to provide a database to persist user data **/
});
`,
},
{
id: "2",
name: "auth-client.ts",
content: `import { createAuthClient } from "better-auth/react";${
options.magicLink
? `
import { magicLinkClient } from "better-auth/client/plugins";`
: ""
}${
options.passkey
? `
import { passkeyClient } from "@better-auth/passkey/client";`
: ""
}
export const authClient = createAuthClient({
baseURL: process.env.NUXT_PUBLIC_APP_URL,${
options.magicLink || options.passkey
? `
plugins: [${options.magicLink ? `magicLinkClient()${options.passkey ? "," : ""}` : ""}${
options.passkey ? `passkeyClient()` : ""
}],`
: ""
}
});
export const { signIn, signOut, signUp, useSession } = authClient;
`,
},
{
id: "3",
name: "sign-in.vue",
content: signInString(options),
},
];
if (options.signUp) {
files.push({
id: "4",
name: "sign-up.vue",
content: signUpString(options),
});
}
return files;
}
const signInString = (options: SignInBoxOptions) => `<script setup lang="ts">
import { Button } from "~/components/ui/button";
import { Card, CardContent, CardHeader, CardTitle, CardDescription, CardFooter } from "~/components/ui/card";
import { Input } from "~/components/ui/input";
import { Label } from "~/components/ui/label";
import { Checkbox } from "~/components/ui/checkbox";
import { Loader2, Key } from "lucide-vue";
import { signIn } from "~/lib/auth-client";
import { cn } from "~/lib/utils";
${
options.email || options.magicLink
? `
const email = ref("");`
: ""
}${
options.email
? `
const password = ref("");`
: ""
}
const loading = ref(false);${
options.rememberMe
? `
const rememberMe = ref(false);`
: ""
}${
options.email
? `
const handleSignIn = async () => {
await signIn.email({
email: email.value,
password: password.value,${
options.rememberMe
? `
rememberMe: rememberMe.value,`
: ""
}
fetchOptions: {
onRequest: () => {
loading.value = true;
},
onResponse: () => {
loading.value = false;
},
},
});
};`
: ""
}${
options.magicLink
? `
const handleMagicLink = async () => {
await signIn.magicLink({
email: email.value,
fetchOptions: {
onRequest: () => {
loading.value = true;
},
onResponse: () => {
loading.value = false;
},
},
});
};
`
: ""
}${
options.passkey
? `
const handlePasskey = async () => {
await signIn.passkey({
fetchOptions: {
onRequest: () => {
loading.value = true;
},
onResponse: () => {
loading.value = false;
},
},
});
};`
: ""
}${
options.socialProviders.length > 0
? `
const handleSocialSignIn = async (provider: string) => {
await signIn.social({
provider,
callbackURL: "/dashboard",
fetchOptions: {
onRequest: () => {
loading.value = true;
},
onResponse: () => {
loading.value = false;
},
},
});
}`
: ""
}
</script>
<template>
<Card class="max-w-md">
<CardHeader>
<CardTitle class="text-xs md:text-sm">Sign In</CardTitle>
<CardDescription>
Enter your email below to login to your account
</CardDescription>
</CardHeader>
<CardContent>
<div class="grid gap-4">
${
options.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>
<div class="grid gap-2">
<div class="flex items-center">
<Label for="password">Password</Label>${
options.requestPasswordReset
? `
<NuxtLink
to="#"
class="ml-auto inline-block text-sm underline"
>
Forgot your password?
</NuxtLink>`
: ""
}
</div>
<Input
id="password"
type="password"
placeholder="password"
autoComplete="password"
v-model="password"
/>
</div>${
options.rememberMe
? `
<div class="flex items-center gap-2">
<Checkbox
id="remember"
v-model="rememberMe"
/>
<Label for="remember">Remember me</Label>
</div>`
: ""
}`
: ""
}${
options.magicLink
? `<div class="grid gap-2">
<Label for="email">Email</Label>
<Input
id="email"
type="email"
placeholder="m@example.com"
required
v-model="email"
/>
<Button
:disabled="loading"
class="gap-2"
@click="handleMagicLink"
>
<Loader2 size="16" class="animate-spin" v-if="loading" />
<span v-else>
Sign-in with Magic Link
</span>
</Button>
</div>`
: ""
}${
options.email
? `
<Button
type="submit"
class="w-full"
:disabled="loading"
@click="handleSignIn"
>
<Loader2 size="16" class="animate-spin" v-if="loading" />
<p v-else>Login</p>
</Button>`
: ""
}${
options.passkey
? `
<Button
variant="secondary"
:disabled="loading"
class="gap-2"
@click="handlePasskey"
>
<Key size="16" />
Sign-in with Passkey
</Button>`
: ``
}${
options.socialProviders.length > 0
? `
<div :class="cn(
'w-full gap-2 flex items-center',
${
options.socialProviders.length > 3
? "'justify-between flex-wrap'"
: "'justify-between flex-col'"
}
)">
${options.socialProviders
.map((provider: string) => {
const icon =
socialProviders[provider as keyof typeof socialProviders]
?.stringIcon || "";
return `<Button
variant="outline"
:class="cn(
${
options.socialProviders.length > 3
? "'flex-grow'"
: "'w-full gap-2'"
}
)"
:disabled="loading"
@click="handleSocialSignIn('${provider}')"
>
${icon}
${
options.socialProviders.length <= 3
? `Sign in with ${
provider.charAt(0).toUpperCase() + provider.slice(1)
}`
: ""
}
</Button>`;
})
.join("\n\t\t\t\t\t")}
</div>`
: ""
}
</div>
</CardContent>${
options.label
? `
<CardFooter>
<div class="flex justify-center w-full border-t py-4">
<p class="text-center text-xs text-neutral-500">
built with
<NuxtLink
to="https://better-auth.com"
target="_blank"
class="underline"
>
<span class="dark:text-white/70 cursor-pointer">
better-auth.
</span>
</NuxtLink>
</p>
</div>
</CardFooter>`
: ""
}
</Card>
</template>
`;
const signUpString = (options: SignInBoxOptions) => `<script setup lang="ts">
import { Button } from "~/components/ui/button";
import {
Card,
CardContent,
CardDescription,
CardFooter,
CardHeader,
CardTitle,
} from "~/components/ui/card";
import { Input } from "~/components/ui/input";
import { Label } from "~/components/ui/label";
import { Loader2, X } from "lucide-vue";
import { signUp } from "~/lib/auth-client";
import { toast } from "vue-sonner";
import { cn } from "~/lib/utils";
const router = useRouter();
const firstName = ref("");
const lastName = ref("");
const email = ref("");
const password = ref("");
const passwordConfirmation = ref("");
const image = ref<File | null>(null);
const imagePreview = ref<string | null>(null);
const loading = ref(false);
async function convertImageToBase64(file: File): Promise<string> {
return new Promise((resolve, reject) => {
const reader = new FileReader();
reader.onloadend = () => resolve(reader.result as string);
reader.onerror = reject;
reader.readAsDataURL(file);
});
}
const handleSignUp = async () => {
await signUp.email({
email: email.value,
password: password.value,
name: \`\${firstName.value} \${lastName.value}\`,
image: image.value ? await convertImageToBase64(image.value) : "",
callbackURL: "/dashboard",
fetchOptions: {
onResponse: () => {
loading.value = false;
},
onRequest: () => {
loading.value = true;
},
onError: (ctx) => {
toast.error(ctx.error.message);
},
onSuccess: () => {
router.push("/dashboard");
},
},
})
};
const handleImageChange = (e: Event) => {
const file = (e.target as HTMLInputElement)?.files?.[0];
if (file) {
image.value = file;
const reader = new FileReader();
reader.onloadend = () => {
imagePreview.value = reader.result as string;
};
reader.readAsDataURL(file);
}
};
</script>
<template>
<Card class="z-50 rounded-md rounded-t-none max-w-md">
<CardHeader>
<CardTitle class="text-lg md:text-xl">Sign Up</CardTitle>
<CardDescription>
Enter your information to create an account
</CardDescription>
</CardHeader>
<CardContent>
<div class="grid gap-4">
<div class="grid grid-cols-2 gap-4">
<div class="grid gap-2">
<Label for="first-name">First name</Label>
<Input
id="first-name"
placeholder="Max"
required
v-model="firstName"
/>
</div>
<div class="grid gap-2">
<Label for="last-name">Last name</Label>
<Input
id="last-name"
placeholder="Robinson"
required
v-model="lastName"
/>
</div>
<div class="grid gap-2">
<Label for="email">Email</Label>
<Input
id="email"
type="email"
placeholder="m@example.com"
required
v-model="email"
/>
</div>
<div class="grid gap-2">
<Label for="password">Password</Label>
<Input
id="password"
type="password"
placeholder="Password"
autocomplete="new-password"
v-model="password"
/>
</div>
<div class="grid gap-2">
<Label for="password_confirmation">Confirm Password</Label>
<Input
id="password_confirmation"
type="password"
autocomplete="new-password"
placeholder="Confirm Password"
v-model="passwordConfirmation"
/>
</div>
<div class="grid gap-2">
<Label for="image">Profile Image (optional)</Label>
<div class="flex items-end gap-4">
<div v-if="imagePreview" class="relative w-16 h-16 rounded-sm overflow-hidden">
<NuxtImg
:src="imagePreview"
alt="Profile preview"
class="object-cover"
/>
</div>
<div class="flex items-center gap-2 w-full">
<Input
id="image"
type="file"
accept="image/*"
@change="handleImageChange"
class="w-full"
/>
<X
class="cursor-pointer"
@click="image = null; imagePreview = null"
/>
</div>
</div>
</div>
<Button
type="submit"
class="w-full"
:disabled="loading"
@click="handleSignUp"
>
<Loader2 size="16" class="animate-spin" v-if="loading" />
<span v-else>Create your account</span>
</Button>
</div>
</div>
</CardContent>
${
options.label
? `<CardFooter>
<div class="flex justify-center w-full border-t py-4">
<p class="text-center text-xs text-neutral-500">
Secured by <span class="text-orange-400">better-auth.</span>
</p>
</div>
</CardFooter>`
: ""
}
</Card>
</template>
`;