mirror of
https://github.com/better-auth/better-auth.git
synced 2026-05-25 00:22:43 -05:00
583 lines
13 KiB
TypeScript
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>
|
|
`;
|