feat: add clerk auth support with convex (#548)

This commit is contained in:
Aman Varshney
2025-08-29 00:21:08 +05:30
committed by GitHub
parent 8d48ae0359
commit 54bcdf1cbc
153 changed files with 1954 additions and 771 deletions

View File

@@ -0,0 +1,77 @@
<script setup lang="ts">
import z from 'zod'
const {$authClient} = useNuxtApp()
import type { FormSubmitEvent } from '#ui/types'
const emit = defineEmits(['switchToSignUp'])
const toast = useToast()
const loading = ref(false)
const schema = z.object({
email: z.email('Invalid email address'),
password: z.string().min(8, 'Password must be at least 8 characters'),
})
type Schema = z.output<typeof schema>
const state = reactive({
email: '',
password: '',
})
async function onSubmit (event: FormSubmitEvent<Schema>) {
loading.value = true
try {
await $authClient.signIn.email(
{
email: event.data.email,
password: event.data.password,
},
{
onSuccess: () => {
toast.add({ title: 'Sign in successful' })
navigateTo('/dashboard', { replace: true })
},
onError: (error) => {
toast.add({ title: 'Sign in failed', description: error.error.message })
},
},
)
} catch (error: any) {
toast.add({ title: 'An unexpected error occurred', description: error.message || 'Please try again.' })
} finally {
loading.value = false
}
}
</script>
<template>
<div class="mx-auto w-full mt-10 max-w-md p-6">
<h1 class="mb-6 text-center text-3xl font-bold">Welcome Back</h1>
<UForm :schema="schema" :state="state" class="space-y-4" @submit="onSubmit">
<UFormField label="Email" name="email">
<UInput v-model="state.email" type="email" class="w-full" />
</UFormField>
<UFormField label="Password" name="password">
<UInput v-model="state.password" type="password" class="w-full" />
</UFormField>
<UButton type="submit" block :loading="loading">
Sign In
</UButton>
</UForm>
<div class="mt-4 text-center">
<UButton
variant="link"
@click="$emit('switchToSignUp')"
class="text-primary hover:text-primary-dark"
>
Need an account? Sign Up
</UButton>
</div>
</div>
</template>

View File

@@ -0,0 +1,84 @@
<script setup lang="ts">
import z from 'zod'
import type { FormSubmitEvent } from '#ui/types'
const {$authClient} = useNuxtApp()
const emit = defineEmits(['switchToSignIn'])
const toast = useToast()
const loading = ref(false)
const schema = z.object({
name: z.string().min(2, 'Name must be at least 2 characters'),
email: z.email('Invalid email address'),
password: z.string().min(8, 'Password must be at least 8 characters'),
})
type Schema = z.output<typeof schema>
const state = reactive({
name: '',
email: '',
password: '',
})
async function onSubmit (event: FormSubmitEvent<Schema>) {
loading.value = true
try {
await $authClient.signUp.email(
{
name: event.data.name,
email: event.data.email,
password: event.data.password,
},
{
onSuccess: () => {
toast.add({ title: 'Sign up successful' })
navigateTo('/dashboard', { replace: true })
},
onError: (error) => {
toast.add({ title: 'Sign up failed', description: error.error.message })
},
},
)
} catch (error: any) {
toast.add({ title: 'An unexpected error occurred', description: error.message || 'Please try again.' })
} finally {
loading.value = false
}
}
</script>
<template>
<div class="mx-auto w-full mt-10 max-w-md p-6">
<h1 class="mb-6 text-center text-3xl font-bold">Create Account</h1>
<UForm :schema="schema" :state="state" class="space-y-4" @submit="onSubmit">
<UFormField label="Name" name="name">
<UInput v-model="state.name" class="w-full" />
</UFormField>
<UFormField label="Email" name="email">
<UInput v-model="state.email" type="email" class="w-full" />
</UFormField>
<UFormField label="Password" name="password">
<UInput v-model="state.password" type="password" class="w-full" />
</UFormField>
<UButton type="submit" block :loading="loading">
Sign Up
</UButton>
</UForm>
<div class="mt-4 text-center">
<UButton
variant="link"
@click="$emit('switchToSignIn')"
class="text-primary hover:text-primary-dark"
>
Already have an account? Sign In
</UButton>
</div>
</div>
</template>

View File

@@ -0,0 +1,42 @@
<script setup lang="ts">
const {$authClient} = useNuxtApp()
const session = $authClient.useSession()
const toast = useToast()
const handleSignOut = async () => {
try {
await $authClient.signOut({
fetchOptions: {
onSuccess: async () => {
toast.add({ title: 'Signed out successfully' })
await navigateTo('/', { replace: true, external: true })
},
onError: (error) => {
toast.add({ title: 'Sign out failed', description: error?.error?.message || 'Unknown error'})
}
},
})
} catch (error: any) {
toast.add({ title: 'An unexpected error occurred during sign out', description: error.message || 'Please try again.'})
}
}
</script>
<template>
<div>
<USkeleton v-if="session.isPending" class="h-9 w-24" />
<UButton v-else-if="!session.data" variant="outline" to="/login">
Sign In
</UButton>
<UButton
v-else
variant="solid"
icon="i-lucide-log-out"
label="Sign out"
@click="handleSignOut()"
/>
</div>
</template>

View File

@@ -0,0 +1,12 @@
export default defineNuxtRouteMiddleware(async (to, from) => {
if (import.meta.server) return
const { $authClient } = useNuxtApp()
const session = $authClient.useSession()
if (session.value.isPending || !session.value) {
if (to.path === "/dashboard") {
return navigateTo("/login");
}
}
});

View File

@@ -0,0 +1,33 @@
<script setup lang="ts">
{{#if (eq api "orpc")}}
import { useQuery } from '@tanstack/vue-query'
{{/if}}
const {$authClient} = useNuxtApp()
definePageMeta({
middleware: ['auth']
})
const { $orpc } = useNuxtApp()
const session = $authClient.useSession()
{{#if (eq api "orpc")}}
const privateData = useQuery($orpc.privateData.queryOptions())
{{/if}}
</script>
<template>
<div class="container mx-auto p-4">
<h1 class="text-2xl font-bold mb-4">Dashboard</h1>
<div v-if="session?.data?.user">
<p class="mb-2">Welcome \{{ session.data.user.name }}</p>
</div>
{{#if (eq api "orpc")}}
<div v-if="privateData.status.value === 'pending'">Loading private data...</div>
<div v-else-if="privateData.status.value === 'error'">Error loading private data: \{{ privateData.error.value?.message }}</div>
<p v-else-if="privateData.data.value">Private Data: \{{ privateData.data.value.message }}</p>
{{/if}}
</div>
</template>

View File

@@ -0,0 +1,24 @@
<script setup lang="ts">
const { $authClient } = useNuxtApp();
import SignInForm from "~/components/SignInForm.vue";
import SignUpForm from "~/components/SignUpForm.vue";
const session = $authClient.useSession();
const showSignIn = ref(true);
watchEffect(() => {
if (!session?.value.isPending && session?.value.data) {
navigateTo("/dashboard", { replace: true });
}
});
</script>
<template>
<div>
<Loader v-if="session.isPending" />
<div v-else-if="!session.data">
<SignInForm v-if="showSignIn" @switch-to-sign-up="showSignIn = false" />
<SignUpForm v-else @switch-to-sign-in="showSignIn = true" />
</div>
</div>
</template>

View File

@@ -0,0 +1,16 @@
import { createAuthClient } from "better-auth/vue";
export default defineNuxtPlugin(nuxtApp => {
const config = useRuntimeConfig()
const serverUrl = config.public.serverURL
const authClient = createAuthClient({
baseURL: serverUrl
})
return {
provide: {
authClient: authClient
}
}
})

View File

@@ -0,0 +1,10 @@
import { createAuthClient } from "better-auth/react";
export const authClient = createAuthClient({
baseURL:
{{#if (includes frontend "next")}}
process.env.NEXT_PUBLIC_SERVER_URL,
{{else}}
import.meta.env.VITE_SERVER_URL,
{{/if}}
});

View File

@@ -0,0 +1,47 @@
"use client"
import { authClient } from "@/lib/auth-client";
{{#if (eq api "orpc")}}
import { useQuery } from "@tanstack/react-query";
import { orpc } from "@/utils/orpc";
{{/if}}
{{#if (eq api "trpc")}}
import { useQuery } from "@tanstack/react-query";
import { trpc } from "@/utils/trpc";
{{/if}}
import { useRouter } from "next/navigation";
import { useEffect } from "react";
export default function Dashboard() {
const router = useRouter();
const { data: session, isPending } = authClient.useSession();
{{#if (eq api "orpc")}}
const privateData = useQuery(orpc.privateData.queryOptions());
{{/if}}
{{#if (eq api "trpc")}}
const privateData = useQuery(trpc.privateData.queryOptions());
{{/if}}
useEffect(() => {
if (!session && !isPending) {
router.push("/login");
}
}, [session, isPending]);
if (isPending) {
return <div>Loading...</div>;
}
return (
<div>
<h1>Dashboard</h1>
<p>Welcome {session?.user.name}</p>
{{#if (eq api "orpc")}}
<p>privateData: {privateData.data?.message}</p>
{{/if}}
{{#if (eq api "trpc")}}
<p>privateData: {privateData.data?.message}</p>
{{/if}}
</div>
);
}

View File

@@ -0,0 +1,16 @@
"use client"
import SignInForm from "@/components/sign-in-form";
import SignUpForm from "@/components/sign-up-form";
import { useState } from "react";
export default function LoginPage() {
const [showSignIn, setShowSignIn] = useState(false);
return showSignIn ? (
<SignInForm onSwitchToSignUp={() => setShowSignIn(false)} />
) : (
<SignUpForm onSwitchToSignIn={() => setShowSignIn(true)} />
);
}

View File

@@ -0,0 +1,135 @@
import { authClient } from "@/lib/auth-client";
import { useForm } from "@tanstack/react-form";
import { toast } from "sonner";
import z from "zod";
import Loader from "./loader";
import { Button } from "./ui/button";
import { Input } from "./ui/input";
import { Label } from "./ui/label";
import { useRouter } from "next/navigation";
export default function SignInForm({
onSwitchToSignUp,
}: {
onSwitchToSignUp: () => void;
}) {
const router = useRouter()
const { isPending } = authClient.useSession();
const form = useForm({
defaultValues: {
email: "",
password: "",
},
onSubmit: async ({ value }) => {
await authClient.signIn.email(
{
email: value.email,
password: value.password,
},
{
onSuccess: () => {
router.push("/dashboard")
toast.success("Sign in successful");
},
onError: (error) => {
toast.error(error.error.message || error.error.statusText);
},
},
);
},
validators: {
onSubmit: z.object({
email: z.email("Invalid email address"),
password: z.string().min(8, "Password must be at least 8 characters"),
}),
},
});
if (isPending) {
return <Loader />;
}
return (
<div className="mx-auto w-full mt-10 max-w-md p-6">
<h1 className="mb-6 text-center text-3xl font-bold">Welcome Back</h1>
<form
onSubmit={(e) => {
e.preventDefault();
e.stopPropagation();
form.handleSubmit();
}}
className="space-y-4"
>
<div>
<form.Field name="email">
{(field) => (
<div className="space-y-2">
<Label htmlFor={field.name}>Email</Label>
<Input
id={field.name}
name={field.name}
type="email"
value={field.state.value}
onBlur={field.handleBlur}
onChange={(e) => field.handleChange(e.target.value)}
/>
{field.state.meta.errors.map((error) => (
<p key={error?.message} className="text-red-500">
{error?.message}
</p>
))}
</div>
)}
</form.Field>
</div>
<div>
<form.Field name="password">
{(field) => (
<div className="space-y-2">
<Label htmlFor={field.name}>Password</Label>
<Input
id={field.name}
name={field.name}
type="password"
value={field.state.value}
onBlur={field.handleBlur}
onChange={(e) => field.handleChange(e.target.value)}
/>
{field.state.meta.errors.map((error) => (
<p key={error?.message} className="text-red-500">
{error?.message}
</p>
))}
</div>
)}
</form.Field>
</div>
<form.Subscribe>
{(state) => (
<Button
type="submit"
className="w-full"
disabled={!state.canSubmit || state.isSubmitting}
>
{state.isSubmitting ? "Submitting..." : "Sign In"}
</Button>
)}
</form.Subscribe>
</form>
<div className="mt-4 text-center">
<Button
variant="link"
onClick={onSwitchToSignUp}
className="text-indigo-600 hover:text-indigo-800"
>
Need an account? Sign Up
</Button>
</div>
</div>
);
}

View File

@@ -0,0 +1,160 @@
import { authClient } from "@/lib/auth-client";
import { useForm } from "@tanstack/react-form";
import { toast } from "sonner";
import z from "zod";
import Loader from "./loader";
import { Button } from "./ui/button";
import { Input } from "./ui/input";
import { Label } from "./ui/label";
import { useRouter } from "next/navigation";
export default function SignUpForm({
onSwitchToSignIn,
}: {
onSwitchToSignIn: () => void;
}) {
const router = useRouter();
const { isPending } = authClient.useSession();
const form = useForm({
defaultValues: {
email: "",
password: "",
name: "",
},
onSubmit: async ({ value }) => {
await authClient.signUp.email(
{
email: value.email,
password: value.password,
name: value.name,
},
{
onSuccess: () => {
router.push("/dashboard");
toast.success("Sign up successful");
},
onError: (error) => {
toast.error(error.error.message || error.error.statusText);
},
},
);
},
validators: {
onSubmit: z.object({
name: z.string().min(2, "Name must be at least 2 characters"),
email: z.email("Invalid email address"),
password: z.string().min(8, "Password must be at least 8 characters"),
}),
},
});
if (isPending) {
return <Loader />;
}
return (
<div className="mx-auto w-full mt-10 max-w-md p-6">
<h1 className="mb-6 text-center text-3xl font-bold">Create Account</h1>
<form
onSubmit={(e) => {
e.preventDefault();
e.stopPropagation();
form.handleSubmit();
}}
className="space-y-4"
>
<div>
<form.Field name="name">
{(field) => (
<div className="space-y-2">
<Label htmlFor={field.name}>Name</Label>
<Input
id={field.name}
name={field.name}
value={field.state.value}
onBlur={field.handleBlur}
onChange={(e) => field.handleChange(e.target.value)}
/>
{field.state.meta.errors.map((error) => (
<p key={error?.message} className="text-red-500">
{error?.message}
</p>
))}
</div>
)}
</form.Field>
</div>
<div>
<form.Field name="email">
{(field) => (
<div className="space-y-2">
<Label htmlFor={field.name}>Email</Label>
<Input
id={field.name}
name={field.name}
type="email"
value={field.state.value}
onBlur={field.handleBlur}
onChange={(e) => field.handleChange(e.target.value)}
/>
{field.state.meta.errors.map((error) => (
<p key={error?.message} className="text-red-500">
{error?.message}
</p>
))}
</div>
)}
</form.Field>
</div>
<div>
<form.Field name="password">
{(field) => (
<div className="space-y-2">
<Label htmlFor={field.name}>Password</Label>
<Input
id={field.name}
name={field.name}
type="password"
value={field.state.value}
onBlur={field.handleBlur}
onChange={(e) => field.handleChange(e.target.value)}
/>
{field.state.meta.errors.map((error) => (
<p key={error?.message} className="text-red-500">
{error?.message}
</p>
))}
</div>
)}
</form.Field>
</div>
<form.Subscribe>
{(state) => (
<Button
type="submit"
className="w-full"
disabled={!state.canSubmit || state.isSubmitting}
>
{state.isSubmitting ? "Submitting..." : "Sign Up"}
</Button>
)}
</form.Subscribe>
</form>
<div className="mt-4 text-center">
<Button
variant="link"
onClick={onSwitchToSignIn}
className="text-indigo-600 hover:text-indigo-800"
>
Already have an account? Sign In
</Button>
</div>
</div>
);
}

View File

@@ -0,0 +1,11 @@
"use client"
import * as React from "react"
import { ThemeProvider as NextThemesProvider } from "next-themes"
export function ThemeProvider({
children,
...props
}: React.ComponentProps<typeof NextThemesProvider>) {
return <NextThemesProvider {...props}>{children}</NextThemesProvider>
}

View File

@@ -0,0 +1,60 @@
import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuLabel,
DropdownMenuSeparator,
DropdownMenuTrigger,
} from "@/components/ui/dropdown-menu";
import { authClient } from "@/lib/auth-client";
import { Button } from "./ui/button";
import { Skeleton } from "./ui/skeleton";
import { useRouter } from "next/navigation";
import Link from "next/link";
export default function UserMenu() {
const router = useRouter();
const { data: session, isPending } = authClient.useSession();
if (isPending) {
return <Skeleton className="h-9 w-24" />;
}
if (!session) {
return (
<Button variant="outline" asChild>
<Link href="/login">Sign In</Link>
</Button>
);
}
return (
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Button variant="outline">{session.user.name}</Button>
</DropdownMenuTrigger>
<DropdownMenuContent className="bg-card">
<DropdownMenuLabel>My Account</DropdownMenuLabel>
<DropdownMenuSeparator />
<DropdownMenuItem>{session.user.email}</DropdownMenuItem>
<DropdownMenuItem asChild>
<Button
variant="destructive"
className="w-full"
onClick={() => {
authClient.signOut({
fetchOptions: {
onSuccess: () => {
router.push("/");
},
},
});
}}
>
Sign Out
</Button>
</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
);
}

View File

@@ -0,0 +1,135 @@
import { authClient } from "@/lib/auth-client";
import { useForm } from "@tanstack/react-form";
import { useNavigate } from "react-router";
import { toast } from "sonner";
import z from "zod";
import Loader from "./loader";
import { Button } from "./ui/button";
import { Input } from "./ui/input";
import { Label } from "./ui/label";
export default function SignInForm({
onSwitchToSignUp,
}: {
onSwitchToSignUp: () => void;
}) {
const navigate = useNavigate();
const { isPending } = authClient.useSession();
const form = useForm({
defaultValues: {
email: "",
password: "",
},
onSubmit: async ({ value }) => {
await authClient.signIn.email(
{
email: value.email,
password: value.password,
},
{
onSuccess: () => {
navigate("/dashboard");
toast.success("Sign in successful");
},
onError: (error) => {
toast.error(error.error.message || error.error.statusText);
},
}
);
},
validators: {
onSubmit: z.object({
email: z.email("Invalid email address"),
password: z.string().min(8, "Password must be at least 8 characters"),
}),
},
});
if (isPending) {
return <Loader />;
}
return (
<div className="mx-auto w-full mt-10 max-w-md p-6">
<h1 className="mb-6 text-center text-3xl font-bold">Welcome Back</h1>
<form
onSubmit={(e) => {
e.preventDefault();
e.stopPropagation();
form.handleSubmit();
}}
className="space-y-4"
>
<div>
<form.Field name="email">
{(field) => (
<div className="space-y-2">
<Label htmlFor={field.name}>Email</Label>
<Input
id={field.name}
name={field.name}
type="email"
value={field.state.value}
onBlur={field.handleBlur}
onChange={(e) => field.handleChange(e.target.value)}
/>
{field.state.meta.errors.map((error) => (
<p key={error?.message} className="text-red-500">
{error?.message}
</p>
))}
</div>
)}
</form.Field>
</div>
<div>
<form.Field name="password">
{(field) => (
<div className="space-y-2">
<Label htmlFor={field.name}>Password</Label>
<Input
id={field.name}
name={field.name}
type="password"
value={field.state.value}
onBlur={field.handleBlur}
onChange={(e) => field.handleChange(e.target.value)}
/>
{field.state.meta.errors.map((error) => (
<p key={error?.message} className="text-red-500">
{error?.message}
</p>
))}
</div>
)}
</form.Field>
</div>
<form.Subscribe>
{(state) => (
<Button
type="submit"
className="w-full"
disabled={!state.canSubmit || state.isSubmitting}
>
{state.isSubmitting ? "Submitting..." : "Sign In"}
</Button>
)}
</form.Subscribe>
</form>
<div className="mt-4 text-center">
<Button
variant="link"
onClick={onSwitchToSignUp}
className="text-indigo-600 hover:text-indigo-800"
>
Need an account? Sign Up
</Button>
</div>
</div>
);
}

View File

@@ -0,0 +1,160 @@
import { authClient } from "@/lib/auth-client";
import { useForm } from "@tanstack/react-form";
import { useNavigate } from "react-router";
import { toast } from "sonner";
import z from "zod";
import Loader from "./loader";
import { Button } from "./ui/button";
import { Input } from "./ui/input";
import { Label } from "./ui/label";
export default function SignUpForm({
onSwitchToSignIn,
}: {
onSwitchToSignIn: () => void;
}) {
const navigate = useNavigate();
const { isPending } = authClient.useSession();
const form = useForm({
defaultValues: {
email: "",
password: "",
name: "",
},
onSubmit: async ({ value }) => {
await authClient.signUp.email(
{
email: value.email,
password: value.password,
name: value.name,
},
{
onSuccess: () => {
navigate("/dashboard");
toast.success("Sign up successful");
},
onError: (error) => {
toast.error(error.error.message || error.error.statusText);
},
}
);
},
validators: {
onSubmit: z.object({
name: z.string().min(2, "Name must be at least 2 characters"),
email: z.email("Invalid email address"),
password: z.string().min(8, "Password must be at least 8 characters"),
}),
},
});
if (isPending) {
return <Loader />;
}
return (
<div className="mx-auto w-full mt-10 max-w-md p-6">
<h1 className="mb-6 text-center text-3xl font-bold">Create Account</h1>
<form
onSubmit={(e) => {
e.preventDefault();
e.stopPropagation();
form.handleSubmit();
}}
className="space-y-4"
>
<div>
<form.Field name="name">
{(field) => (
<div className="space-y-2">
<Label htmlFor={field.name}>Name</Label>
<Input
id={field.name}
name={field.name}
value={field.state.value}
onBlur={field.handleBlur}
onChange={(e) => field.handleChange(e.target.value)}
/>
{field.state.meta.errors.map((error) => (
<p key={error?.message} className="text-red-500">
{error?.message}
</p>
))}
</div>
)}
</form.Field>
</div>
<div>
<form.Field name="email">
{(field) => (
<div className="space-y-2">
<Label htmlFor={field.name}>Email</Label>
<Input
id={field.name}
name={field.name}
type="email"
value={field.state.value}
onBlur={field.handleBlur}
onChange={(e) => field.handleChange(e.target.value)}
/>
{field.state.meta.errors.map((error) => (
<p key={error?.message} className="text-red-500">
{error?.message}
</p>
))}
</div>
)}
</form.Field>
</div>
<div>
<form.Field name="password">
{(field) => (
<div className="space-y-2">
<Label htmlFor={field.name}>Password</Label>
<Input
id={field.name}
name={field.name}
type="password"
value={field.state.value}
onBlur={field.handleBlur}
onChange={(e) => field.handleChange(e.target.value)}
/>
{field.state.meta.errors.map((error) => (
<p key={error?.message} className="text-red-500">
{error?.message}
</p>
))}
</div>
)}
</form.Field>
</div>
<form.Subscribe>
{(state) => (
<Button
type="submit"
className="w-full"
disabled={!state.canSubmit || state.isSubmitting}
>
{state.isSubmitting ? "Submitting..." : "Sign Up"}
</Button>
)}
</form.Subscribe>
</form>
<div className="mt-4 text-center">
<Button
variant="link"
onClick={onSwitchToSignIn}
className="text-indigo-600 hover:text-indigo-800"
>
Already have an account? Sign In
</Button>
</div>
</div>
);
}

View File

@@ -0,0 +1,60 @@
import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuLabel,
DropdownMenuSeparator,
DropdownMenuTrigger,
} from "@/components/ui/dropdown-menu";
import { authClient } from "@/lib/auth-client";
import { useNavigate } from "react-router";
import { Button } from "./ui/button";
import { Skeleton } from "./ui/skeleton";
import { Link } from "react-router";
export default function UserMenu() {
const navigate = useNavigate();
const { data: session, isPending } = authClient.useSession();
if (isPending) {
return <Skeleton className="h-9 w-24" />;
}
if (!session) {
return (
<Button variant="outline" asChild>
<Link to="/login">Sign In</Link>
</Button>
);
}
return (
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Button variant="outline">{session.user.name}</Button>
</DropdownMenuTrigger>
<DropdownMenuContent className="bg-card">
<DropdownMenuLabel>My Account</DropdownMenuLabel>
<DropdownMenuSeparator />
<DropdownMenuItem>{session.user.email}</DropdownMenuItem>
<DropdownMenuItem asChild>
<Button
variant="destructive"
className="w-full"
onClick={() => {
authClient.signOut({
fetchOptions: {
onSuccess: () => {
navigate("/");
},
},
});
}}
>
Sign Out
</Button>
</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
);
}

View File

@@ -0,0 +1,44 @@
import { authClient } from "@/lib/auth-client";
{{#if (eq api "orpc")}}
import { orpc } from "@/utils/orpc";
{{/if}}
{{#if (eq api "trpc")}}
import { trpc } from "@/utils/trpc";
{{/if}}
{{#if ( or (eq api "orpc") (eq api "trpc"))}}
import { useQuery } from "@tanstack/react-query";
{{/if}}
import { useEffect } from "react";
import { useNavigate } from "react-router";
export default function Dashboard() {
const { data: session, isPending } = authClient.useSession();
const navigate = useNavigate();
{{#if (eq api "orpc")}}
const privateData = useQuery(orpc.privateData.queryOptions());
{{/if}}
{{#if (eq api "trpc")}}
const privateData = useQuery(trpc.privateData.queryOptions());
{{/if}}
useEffect(() => {
if (!session && !isPending) {
navigate("/login");
}
}, [session, isPending]);
if (isPending) {
return <div>Loading...</div>;
}
return (
<div>
<h1>Dashboard</h1>
<p>Welcome {session?.user.name}</p>
{{#if ( or (eq api "orpc") (eq api "trpc"))}}
<p>privateData: {privateData.data?.message}</p>
{{/if}}
</div>
);
}

View File

@@ -0,0 +1,13 @@
import SignInForm from "@/components/sign-in-form";
import SignUpForm from "@/components/sign-up-form";
import { useState } from "react";
export default function Login() {
const [showSignIn, setShowSignIn] = useState(false);
return showSignIn ? (
<SignInForm onSwitchToSignUp={() => setShowSignIn(false)} />
) : (
<SignUpForm onSwitchToSignIn={() => setShowSignIn(true)} />
);
}

View File

@@ -0,0 +1,139 @@
import { authClient } from "@/lib/auth-client";
import { useForm } from "@tanstack/react-form";
import { useNavigate } from "@tanstack/react-router";
import { toast } from "sonner";
import z from "zod";
import Loader from "./loader";
import { Button } from "./ui/button";
import { Input } from "./ui/input";
import { Label } from "./ui/label";
export default function SignInForm({
onSwitchToSignUp,
}: {
onSwitchToSignUp: () => void;
}) {
const navigate = useNavigate({
from: "/",
});
const { isPending } = authClient.useSession();
const form = useForm({
defaultValues: {
email: "",
password: "",
},
onSubmit: async ({ value }) => {
await authClient.signIn.email(
{
email: value.email,
password: value.password,
},
{
onSuccess: () => {
navigate({
to: "/dashboard",
});
toast.success("Sign in successful");
},
onError: (error) => {
toast.error(error.error.message || error.error.statusText);
},
},
);
},
validators: {
onSubmit: z.object({
email: z.email("Invalid email address"),
password: z.string().min(8, "Password must be at least 8 characters"),
}),
},
});
if (isPending) {
return <Loader />;
}
return (
<div className="mx-auto w-full mt-10 max-w-md p-6">
<h1 className="mb-6 text-center text-3xl font-bold">Welcome Back</h1>
<form
onSubmit={(e) => {
e.preventDefault();
e.stopPropagation();
form.handleSubmit();
}}
className="space-y-4"
>
<div>
<form.Field name="email">
{(field) => (
<div className="space-y-2">
<Label htmlFor={field.name}>Email</Label>
<Input
id={field.name}
name={field.name}
type="email"
value={field.state.value}
onBlur={field.handleBlur}
onChange={(e) => field.handleChange(e.target.value)}
/>
{field.state.meta.errors.map((error) => (
<p key={error?.message} className="text-red-500">
{error?.message}
</p>
))}
</div>
)}
</form.Field>
</div>
<div>
<form.Field name="password">
{(field) => (
<div className="space-y-2">
<Label htmlFor={field.name}>Password</Label>
<Input
id={field.name}
name={field.name}
type="password"
value={field.state.value}
onBlur={field.handleBlur}
onChange={(e) => field.handleChange(e.target.value)}
/>
{field.state.meta.errors.map((error) => (
<p key={error?.message} className="text-red-500">
{error?.message}
</p>
))}
</div>
)}
</form.Field>
</div>
<form.Subscribe>
{(state) => (
<Button
type="submit"
className="w-full"
disabled={!state.canSubmit || state.isSubmitting}
>
{state.isSubmitting ? "Submitting..." : "Sign In"}
</Button>
)}
</form.Subscribe>
</form>
<div className="mt-4 text-center">
<Button
variant="link"
onClick={onSwitchToSignUp}
className="text-indigo-600 hover:text-indigo-800"
>
Need an account? Sign Up
</Button>
</div>
</div>
);
}

View File

@@ -0,0 +1,164 @@
import { authClient } from "@/lib/auth-client";
import { useForm } from "@tanstack/react-form";
import { useNavigate } from "@tanstack/react-router";
import { toast } from "sonner";
import z from "zod";
import Loader from "./loader";
import { Button } from "./ui/button";
import { Input } from "./ui/input";
import { Label } from "./ui/label";
export default function SignUpForm({
onSwitchToSignIn,
}: {
onSwitchToSignIn: () => void;
}) {
const navigate = useNavigate({
from: "/",
});
const { isPending } = authClient.useSession();
const form = useForm({
defaultValues: {
email: "",
password: "",
name: "",
},
onSubmit: async ({ value }) => {
await authClient.signUp.email(
{
email: value.email,
password: value.password,
name: value.name,
},
{
onSuccess: () => {
navigate({
to: "/dashboard",
});
toast.success("Sign up successful");
},
onError: (error) => {
toast.error(error.error.message || error.error.statusText);
},
},
);
},
validators: {
onSubmit: z.object({
name: z.string().min(2, "Name must be at least 2 characters"),
email: z.email("Invalid email address"),
password: z.string().min(8, "Password must be at least 8 characters"),
}),
},
});
if (isPending) {
return <Loader />;
}
return (
<div className="mx-auto w-full mt-10 max-w-md p-6">
<h1 className="mb-6 text-center text-3xl font-bold">Create Account</h1>
<form
onSubmit={(e) => {
e.preventDefault();
e.stopPropagation();
form.handleSubmit();
}}
className="space-y-4"
>
<div>
<form.Field name="name">
{(field) => (
<div className="space-y-2">
<Label htmlFor={field.name}>Name</Label>
<Input
id={field.name}
name={field.name}
value={field.state.value}
onBlur={field.handleBlur}
onChange={(e) => field.handleChange(e.target.value)}
/>
{field.state.meta.errors.map((error) => (
<p key={error?.message} className="text-red-500">
{error?.message}
</p>
))}
</div>
)}
</form.Field>
</div>
<div>
<form.Field name="email">
{(field) => (
<div className="space-y-2">
<Label htmlFor={field.name}>Email</Label>
<Input
id={field.name}
name={field.name}
type="email"
value={field.state.value}
onBlur={field.handleBlur}
onChange={(e) => field.handleChange(e.target.value)}
/>
{field.state.meta.errors.map((error) => (
<p key={error?.message} className="text-red-500">
{error?.message}
</p>
))}
</div>
)}
</form.Field>
</div>
<div>
<form.Field name="password">
{(field) => (
<div className="space-y-2">
<Label htmlFor={field.name}>Password</Label>
<Input
id={field.name}
name={field.name}
type="password"
value={field.state.value}
onBlur={field.handleBlur}
onChange={(e) => field.handleChange(e.target.value)}
/>
{field.state.meta.errors.map((error) => (
<p key={error?.message} className="text-red-500">
{error?.message}
</p>
))}
</div>
)}
</form.Field>
</div>
<form.Subscribe>
{(state) => (
<Button
type="submit"
className="w-full"
disabled={!state.canSubmit || state.isSubmitting}
>
{state.isSubmitting ? "Submitting..." : "Sign Up"}
</Button>
)}
</form.Subscribe>
</form>
<div className="mt-4 text-center">
<Button
variant="link"
onClick={onSwitchToSignIn}
className="text-indigo-600 hover:text-indigo-800"
>
Already have an account? Sign In
</Button>
</div>
</div>
);
}

View File

@@ -0,0 +1,62 @@
import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuLabel,
DropdownMenuSeparator,
DropdownMenuTrigger,
} from "@/components/ui/dropdown-menu";
import { authClient } from "@/lib/auth-client";
import { useNavigate } from "@tanstack/react-router";
import { Button } from "./ui/button";
import { Skeleton } from "./ui/skeleton";
import { Link } from "@tanstack/react-router";
export default function UserMenu() {
const navigate = useNavigate();
const { data: session, isPending } = authClient.useSession();
if (isPending) {
return <Skeleton className="h-9 w-24" />;
}
if (!session) {
return (
<Button variant="outline" asChild>
<Link to="/login">Sign In</Link>
</Button>
);
}
return (
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Button variant="outline">{session.user.name}</Button>
</DropdownMenuTrigger>
<DropdownMenuContent className="bg-card">
<DropdownMenuLabel>My Account</DropdownMenuLabel>
<DropdownMenuSeparator />
<DropdownMenuItem>{session.user.email}</DropdownMenuItem>
<DropdownMenuItem asChild>
<Button
variant="destructive"
className="w-full"
onClick={() => {
authClient.signOut({
fetchOptions: {
onSuccess: () => {
navigate({
to: "/",
});
},
},
});
}}
>
Sign Out
</Button>
</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
);
}

View File

@@ -0,0 +1,51 @@
import { authClient } from "@/lib/auth-client";
{{#if (eq api "orpc")}}
import { orpc } from "@/utils/orpc";
{{/if}}
{{#if (eq api "trpc")}}
import { trpc } from "@/utils/trpc";
{{/if}}
{{#if ( or (eq api "orpc") (eq api "trpc"))}}
import { useQuery } from "@tanstack/react-query";
{{/if}}
import { createFileRoute } from "@tanstack/react-router";
import { useEffect } from "react";
export const Route = createFileRoute("/dashboard")({
component: RouteComponent,
});
function RouteComponent() {
const { data: session, isPending } = authClient.useSession();
const navigate = Route.useNavigate();
{{#if (eq api "orpc")}}
const privateData = useQuery(orpc.privateData.queryOptions());
{{/if}}
{{#if (eq api "trpc")}}
const privateData = useQuery(trpc.privateData.queryOptions());
{{/if}}
useEffect(() => {
if (!session && !isPending) {
navigate({
to: "/login",
});
}
}, [session, isPending]);
if (isPending) {
return <div>Loading...</div>;
}
return (
<div>
<h1>Dashboard</h1>
<p>Welcome {session?.user.name}</p>
{{#if ( or (eq api "orpc") (eq api "trpc"))}}
<p>privateData: {privateData.data?.message}</p>
{{/if}}
</div>
);
}

View File

@@ -0,0 +1,18 @@
import SignInForm from "@/components/sign-in-form";
import SignUpForm from "@/components/sign-up-form";
import { createFileRoute } from "@tanstack/react-router";
import { useState } from "react";
export const Route = createFileRoute("/login")({
component: RouteComponent,
});
function RouteComponent() {
const [showSignIn, setShowSignIn] = useState(false);
return showSignIn ? (
<SignInForm onSwitchToSignUp={() => setShowSignIn(false)} />
) : (
<SignUpForm onSwitchToSignIn={() => setShowSignIn(true)} />
);
}

View File

@@ -0,0 +1,139 @@
import { authClient } from "@/lib/auth-client";
import { useForm } from "@tanstack/react-form";
import { useNavigate } from "@tanstack/react-router";
import { toast } from "sonner";
import z from "zod";
import Loader from "./loader";
import { Button } from "./ui/button";
import { Input } from "./ui/input";
import { Label } from "./ui/label";
export default function SignInForm({
onSwitchToSignUp,
}: {
onSwitchToSignUp: () => void;
}) {
const navigate = useNavigate({
from: "/",
});
const { isPending } = authClient.useSession();
const form = useForm({
defaultValues: {
email: "",
password: "",
},
onSubmit: async ({ value }) => {
await authClient.signIn.email(
{
email: value.email,
password: value.password,
},
{
onSuccess: () => {
navigate({
to: "/dashboard",
});
toast.success("Sign in successful");
},
onError: (error) => {
toast.error(error.error.message || error.error.statusText);
},
},
);
},
validators: {
onSubmit: z.object({
email: z.email("Invalid email address"),
password: z.string().min(8, "Password must be at least 8 characters"),
}),
},
});
if (isPending) {
return <Loader />;
}
return (
<div className="mx-auto w-full mt-10 max-w-md p-6">
<h1 className="mb-6 text-center text-3xl font-bold">Welcome Back</h1>
<form
onSubmit={(e) => {
e.preventDefault();
e.stopPropagation();
form.handleSubmit();
}}
className="space-y-4"
>
<div>
<form.Field name="email">
{(field) => (
<div className="space-y-2">
<Label htmlFor={field.name}>Email</Label>
<Input
id={field.name}
name={field.name}
type="email"
value={field.state.value}
onBlur={field.handleBlur}
onChange={(e) => field.handleChange(e.target.value)}
/>
{field.state.meta.errors.map((error) => (
<p key={error?.message} className="text-red-500">
{error?.message}
</p>
))}
</div>
)}
</form.Field>
</div>
<div>
<form.Field name="password">
{(field) => (
<div className="space-y-2">
<Label htmlFor={field.name}>Password</Label>
<Input
id={field.name}
name={field.name}
type="password"
value={field.state.value}
onBlur={field.handleBlur}
onChange={(e) => field.handleChange(e.target.value)}
/>
{field.state.meta.errors.map((error) => (
<p key={error?.message} className="text-red-500">
{error?.message}
</p>
))}
</div>
)}
</form.Field>
</div>
<form.Subscribe>
{(state) => (
<Button
type="submit"
className="w-full"
disabled={!state.canSubmit || state.isSubmitting}
>
{state.isSubmitting ? "Submitting..." : "Sign In"}
</Button>
)}
</form.Subscribe>
</form>
<div className="mt-4 text-center">
<Button
variant="link"
onClick={onSwitchToSignUp}
className="text-indigo-600 hover:text-indigo-800"
>
Need an account? Sign Up
</Button>
</div>
</div>
);
}

View File

@@ -0,0 +1,164 @@
import { authClient } from "@/lib/auth-client";
import { useForm } from "@tanstack/react-form";
import { useNavigate } from "@tanstack/react-router";
import { toast } from "sonner";
import z from "zod";
import Loader from "./loader";
import { Button } from "./ui/button";
import { Input } from "./ui/input";
import { Label } from "./ui/label";
export default function SignUpForm({
onSwitchToSignIn,
}: {
onSwitchToSignIn: () => void;
}) {
const navigate = useNavigate({
from: "/",
});
const { isPending } = authClient.useSession();
const form = useForm({
defaultValues: {
email: "",
password: "",
name: "",
},
onSubmit: async ({ value }) => {
await authClient.signUp.email(
{
email: value.email,
password: value.password,
name: value.name,
},
{
onSuccess: () => {
navigate({
to: "/dashboard",
});
toast.success("Sign up successful");
},
onError: (error) => {
toast.error(error.error.message || error.error.statusText);
},
},
);
},
validators: {
onSubmit: z.object({
name: z.string().min(2, "Name must be at least 2 characters"),
email: z.email("Invalid email address"),
password: z.string().min(8, "Password must be at least 8 characters"),
}),
},
});
if (isPending) {
return <Loader />;
}
return (
<div className="mx-auto w-full mt-10 max-w-md p-6">
<h1 className="mb-6 text-center text-3xl font-bold">Create Account</h1>
<form
onSubmit={(e) => {
e.preventDefault();
e.stopPropagation();
form.handleSubmit();
}}
className="space-y-4"
>
<div>
<form.Field name="name">
{(field) => (
<div className="space-y-2">
<Label htmlFor={field.name}>Name</Label>
<Input
id={field.name}
name={field.name}
value={field.state.value}
onBlur={field.handleBlur}
onChange={(e) => field.handleChange(e.target.value)}
/>
{field.state.meta.errors.map((error) => (
<p key={error?.message} className="text-red-500">
{error?.message}
</p>
))}
</div>
)}
</form.Field>
</div>
<div>
<form.Field name="email">
{(field) => (
<div className="space-y-2">
<Label htmlFor={field.name}>Email</Label>
<Input
id={field.name}
name={field.name}
type="email"
value={field.state.value}
onBlur={field.handleBlur}
onChange={(e) => field.handleChange(e.target.value)}
/>
{field.state.meta.errors.map((error) => (
<p key={error?.message} className="text-red-500">
{error?.message}
</p>
))}
</div>
)}
</form.Field>
</div>
<div>
<form.Field name="password">
{(field) => (
<div className="space-y-2">
<Label htmlFor={field.name}>Password</Label>
<Input
id={field.name}
name={field.name}
type="password"
value={field.state.value}
onBlur={field.handleBlur}
onChange={(e) => field.handleChange(e.target.value)}
/>
{field.state.meta.errors.map((error) => (
<p key={error?.message} className="text-red-500">
{error?.message}
</p>
))}
</div>
)}
</form.Field>
</div>
<form.Subscribe>
{(state) => (
<Button
type="submit"
className="w-full"
disabled={!state.canSubmit || state.isSubmitting}
>
{state.isSubmitting ? "Submitting..." : "Sign Up"}
</Button>
)}
</form.Subscribe>
</form>
<div className="mt-4 text-center">
<Button
variant="link"
onClick={onSwitchToSignIn}
className="text-indigo-600 hover:text-indigo-800"
>
Already have an account? Sign In
</Button>
</div>
</div>
);
}

View File

@@ -0,0 +1,62 @@
import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuLabel,
DropdownMenuSeparator,
DropdownMenuTrigger,
} from "@/components/ui/dropdown-menu";
import { authClient } from "@/lib/auth-client";
import { useNavigate } from "@tanstack/react-router";
import { Button } from "./ui/button";
import { Skeleton } from "./ui/skeleton";
import { Link } from "@tanstack/react-router";
export default function UserMenu() {
const navigate = useNavigate();
const { data: session, isPending } = authClient.useSession();
if (isPending) {
return <Skeleton className="h-9 w-24" />;
}
if (!session) {
return (
<Button variant="outline" asChild>
<Link to="/login">Sign In</Link>
</Button>
);
}
return (
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Button variant="outline">{session.user.name}</Button>
</DropdownMenuTrigger>
<DropdownMenuContent className="bg-card">
<DropdownMenuLabel>My Account</DropdownMenuLabel>
<DropdownMenuSeparator />
<DropdownMenuItem>{session.user.email}</DropdownMenuItem>
<DropdownMenuItem asChild>
<Button
variant="destructive"
className="w-full"
onClick={() => {
authClient.signOut({
fetchOptions: {
onSuccess: () => {
navigate({
to: "/",
});
},
},
});
}}
>
Sign Out
</Button>
</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
);
}

View File

@@ -0,0 +1,54 @@
import { authClient } from "@/lib/auth-client";
{{#if (eq api "trpc")}}
import { useTRPC } from "@/utils/trpc";
import { useQuery } from "@tanstack/react-query";
{{/if}}
{{#if (eq api "orpc")}}
import { orpc } from "@/utils/orpc";
import { useQuery } from "@tanstack/react-query";
{{/if}}
import { createFileRoute } from "@tanstack/react-router";
import { useEffect } from "react";
export const Route = createFileRoute("/dashboard")({
component: RouteComponent,
});
function RouteComponent() {
const navigate = Route.useNavigate();
{{#if (eq api "trpc")}}
const trpc = useTRPC();
{{/if}}
{{#if (eq api "orpc")}}
{{/if}}
const { data: session, isPending } = authClient.useSession();
{{#if (eq api "trpc")}}
const privateData = useQuery(trpc.privateData.queryOptions());
{{/if}}
{{#if (eq api "orpc")}}
const privateData = useQuery(orpc.privateData.queryOptions());
{{/if}}
useEffect(() => {
if (!session && !isPending) {
navigate({
to: "/login",
});
}
}, [session, isPending]);
if (isPending) {
return <div>Loading...</div>;
}
return (
<div>
<h1>Dashboard</h1>
<p>Welcome {session?.user.name}</p>
{{#if ( or (eq api "orpc") (eq api "trpc"))}}
<p>privateData: {privateData.data?.message}</p>
{{/if}}
</div>
);
}

View File

@@ -0,0 +1,18 @@
import SignInForm from "@/components/sign-in-form";
import SignUpForm from "@/components/sign-up-form";
import { createFileRoute } from "@tanstack/react-router";
import { useState } from "react";
export const Route = createFileRoute("/login")({
component: RouteComponent,
});
function RouteComponent() {
const [showSignIn, setShowSignIn] = useState(false);
return showSignIn ? (
<SignInForm onSwitchToSignUp={() => setShowSignIn(false)} />
) : (
<SignUpForm onSwitchToSignIn={() => setShowSignIn(true)} />
);
}

View File

@@ -0,0 +1,132 @@
import { authClient } from "@/lib/auth-client";
import { createForm } from "@tanstack/solid-form";
import { useNavigate } from "@tanstack/solid-router";
import z from "zod";
import { For } from "solid-js";
export default function SignInForm({
onSwitchToSignUp,
}: {
onSwitchToSignUp: () => void;
}) {
const navigate = useNavigate({
from: "/",
});
const form = createForm(() => ({
defaultValues: {
email: "",
password: "",
},
onSubmit: async ({ value }) => {
await authClient.signIn.email(
{
email: value.email,
password: value.password,
},
{
onSuccess: () => {
navigate({
to: "/dashboard",
});
console.log("Sign in successful");
},
onError: (error) => {
console.error(error.error.message);
},
},
);
},
validators: {
onSubmit: z.object({
email: z.email("Invalid email address"),
password: z.string().min(8, "Password must be at least 8 characters"),
}),
},
}));
return (
<div class="mx-auto w-full mt-10 max-w-md p-6">
<h1 class="mb-6 text-center text-3xl font-bold">Welcome Back</h1>
<form
onSubmit={(e) => {
e.preventDefault();
e.stopPropagation();
form.handleSubmit();
}}
class="space-y-4"
>
<div>
<form.Field name="email">
{(field) => (
<div class="space-y-2">
<label for={field().name}>Email</label>
<input
id={field().name}
name={field().name}
type="email"
value={field().state.value}
onBlur={field().handleBlur}
onInput={(e) => field().handleChange(e.currentTarget.value)}
class="w-full rounded border p-2"
/>
<For each={field().state.meta.errors}>
{(error) => (
<p class="text-sm text-red-600">{error?.message}</p>
)}
</For>
</div>
)}
</form.Field>
</div>
<div>
<form.Field name="password">
{(field) => (
<div class="space-y-2">
<label for={field().name}>Password</label>
<input
id={field().name}
name={field().name}
type="password"
value={field().state.value}
onBlur={field().handleBlur}
onInput={(e) => field().handleChange(e.currentTarget.value)}
class="w-full rounded border p-2"
/>
<For each={field().state.meta.errors}>
{(error) => (
<p class="text-sm text-red-600">{error?.message}</p>
)}
</For>
</div>
)}
</form.Field>
</div>
<form.Subscribe>
{(state) => (
<button
type="submit"
class="w-full rounded bg-indigo-600 p-2 text-white hover:bg-indigo-700 disabled:opacity-50"
disabled={!state().canSubmit || state().isSubmitting}
>
{state().isSubmitting ? "Submitting..." : "Sign In"}
</button>
)}
</form.Subscribe>
</form>
<div class="mt-4 text-center">
<button
type="button"
onClick={onSwitchToSignUp}
class="text-sm text-indigo-600 hover:text-indigo-800 hover:underline"
>
Need an account? Sign Up
</button>
</div>
</div>
);
}

View File

@@ -0,0 +1,158 @@
import { authClient } from "@/lib/auth-client";
import { createForm } from "@tanstack/solid-form";
import { useNavigate } from "@tanstack/solid-router";
import z from "zod";
import { For } from "solid-js";
export default function SignUpForm({
onSwitchToSignIn,
}: {
onSwitchToSignIn: () => void;
}) {
const navigate = useNavigate({
from: "/",
});
const form = createForm(() => ({
defaultValues: {
email: "",
password: "",
name: "",
},
onSubmit: async ({ value }) => {
await authClient.signUp.email(
{
email: value.email,
password: value.password,
name: value.name,
},
{
onSuccess: () => {
navigate({
to: "/dashboard",
});
console.log("Sign up successful");
},
onError: (error) => {
console.error(error.error.message);
},
},
);
},
validators: {
onSubmit: z.object({
name: z.string().min(2, "Name must be at least 2 characters"),
email: z.email("Invalid email address"),
password: z.string().min(8, "Password must be at least 8 characters"),
}),
},
}));
return (
<div class="mx-auto w-full mt-10 max-w-md p-6">
<h1 class="mb-6 text-center text-3xl font-bold">Create Account</h1>
<form
onSubmit={(e) => {
e.preventDefault();
e.stopPropagation();
form.handleSubmit();
}}
class="space-y-4"
>
<div>
<form.Field name="name">
{(field) => (
<div class="space-y-2">
<label for={field().name}>Name</label>
<input
id={field().name}
name={field().name}
value={field().state.value}
onBlur={field().handleBlur}
onInput={(e) => field().handleChange(e.currentTarget.value)}
class="w-full rounded border p-2"
/>
<For each={field().state.meta.errors}>
{(error) => (
<p class="text-sm text-red-600">{error?.message}</p>
)}
</For>
</div>
)}
</form.Field>
</div>
<div>
<form.Field name="email">
{(field) => (
<div class="space-y-2">
<label for={field().name}>Email</label>
<input
id={field().name}
name={field().name}
type="email"
value={field().state.value}
onBlur={field().handleBlur}
onInput={(e) => field().handleChange(e.currentTarget.value)}
class="w-full rounded border p-2"
/>
<For each={field().state.meta.errors}>
{(error) => (
<p class="text-sm text-red-600">{error?.message}</p>
)}
</For>
</div>
)}
</form.Field>
</div>
<div>
<form.Field name="password">
{(field) => (
<div class="space-y-2">
<label for={field().name}>Password</label>
<input
id={field().name}
name={field().name}
type="password"
value={field().state.value}
onBlur={field().handleBlur}
onInput={(e) => field().handleChange(e.currentTarget.value)}
class="w-full rounded border p-2"
/>
<For each={field().state.meta.errors}>
{(error) => (
<p class="text-sm text-red-600">{error?.message}</p>
)}
</For>
</div>
)}
</form.Field>
</div>
<form.Subscribe>
{(state) => (
<button
type="submit"
class="w-full rounded bg-indigo-600 p-2 text-white hover:bg-indigo-700 disabled:opacity-50"
disabled={!state().canSubmit || state().isSubmitting}
>
{state().isSubmitting ? "Submitting..." : "Sign Up"}
</button>
)}
</form.Subscribe>
</form>
<div class="mt-4 text-center">
<button
type="button"
onClick={onSwitchToSignIn}
class="text-sm text-indigo-600 hover:text-indigo-800 hover:underline"
>
Already have an account? Sign In
</button>
</div>
</div>
);
}

View File

@@ -0,0 +1,55 @@
import { authClient } from "@/lib/auth-client";
import { useNavigate, Link } from "@tanstack/solid-router";
import { createSignal, Show } from "solid-js";
export default function UserMenu() {
const navigate = useNavigate();
const session = authClient.useSession();
const [isMenuOpen, setIsMenuOpen] = createSignal(false);
return (
<div class="relative inline-block text-left">
<Show when={session().isPending}>
<div class="h-9 w-24 animate-pulse rounded" />
</Show>
<Show when={!session().isPending && !session().data}>
<Link to="/login" class="inline-block border rounded px-4 text-sm">
Sign In
</Link>
</Show>
<Show when={!session().isPending && session().data}>
<button
type="button"
class="inline-block border rounded px-4 text-sm"
onClick={() => setIsMenuOpen(!isMenuOpen())}
>
{session().data?.user.name}
</button>
<Show when={isMenuOpen()}>
<div class="absolute right-0 mt-2 w-56 rounded p-1 shadow-sm">
<div class="px-4 text-sm">{session().data?.user.email}</div>
<button
type="button"
class="mt-1 w-full border rounded px-4 text-center text-sm"
onClick={() => {
setIsMenuOpen(false);
authClient.signOut({
fetchOptions: {
onSuccess: () => {
navigate({ to: "/" });
},
},
});
}}
>
Sign Out
</button>
</div>
</Show>
</Show>
</div>
);
}

View File

@@ -0,0 +1,5 @@
import { createAuthClient } from "better-auth/solid";
export const authClient = createAuthClient({
baseURL: import.meta.env.VITE_SERVER_URL,
});

View File

@@ -0,0 +1,44 @@
import { authClient } from "@/lib/auth-client";
{{#if (eq api "orpc")}}
import { orpc } from "@/utils/orpc";
import { useQuery } from "@tanstack/solid-query";
{{/if}}
import { createFileRoute } from "@tanstack/solid-router";
import { createEffect, Show } from "solid-js";
export const Route = createFileRoute("/dashboard")({
component: RouteComponent,
});
function RouteComponent() {
const session = authClient.useSession();
const navigate = Route.useNavigate();
{{#if (eq api "orpc")}}
const privateData = useQuery(() => orpc.privateData.queryOptions());
{{/if}}
createEffect(() => {
if (!session().data && !session().isPending) {
navigate({
to: "/login",
});
}
});
return (
<div>
<Show when={session().isPending}>
<div>Loading...</div>
</Show>
<Show when={!session().isPending && session().data}>
<h1>Dashboard</h1>
<p>Welcome {session().data?.user.name}</p>
{{#if (eq api "orpc")}}
<p>privateData: {privateData.data?.message}</p>
{{/if}}
</Show>
</div>
);
}

View File

@@ -0,0 +1,23 @@
import SignInForm from "@/components/sign-in-form";
import SignUpForm from "@/components/sign-up-form";
import { createFileRoute } from "@tanstack/solid-router";
import { createSignal, Match, Switch } from "solid-js";
export const Route = createFileRoute("/login")({
component: RouteComponent,
});
function RouteComponent() {
const [showSignIn, setShowSignIn] = createSignal(false);
return (
<Switch>
<Match when={showSignIn()}>
<SignInForm onSwitchToSignUp={() => setShowSignIn(false)} />
</Match>
<Match when={!showSignIn()}>
<SignUpForm onSwitchToSignIn={() => setShowSignIn(true)} />
</Match>
</Switch>
);
}

View File

@@ -0,0 +1,108 @@
<script lang="ts">
import { createForm } from '@tanstack/svelte-form';
import z from 'zod';
import { authClient } from '$lib/auth-client';
import { goto } from '$app/navigation';
let { switchToSignUp } = $props<{ switchToSignUp: () => void }>();
const validationSchema = z.object({
email: z.email('Invalid email address'),
password: z.string().min(1, 'Password is required'),
});
const form = createForm(() => ({
defaultValues: { email: '', password: '' },
onSubmit: async ({ value }) => {
await authClient.signIn.email(
{ email: value.email, password: value.password },
{
onSuccess: () => goto('/dashboard'),
onError: (error) => {
console.log(error.error.message || 'Sign in failed. Please try again.');
},
}
);
},
validators: {
onSubmit: validationSchema,
},
}));
</script>
<div class="mx-auto mt-10 w-full max-w-md p-6">
<h1 class="mb-6 text-center font-bold text-3xl">Welcome Back</h1>
<form
class="space-y-4"
onsubmit={(e) => {
e.preventDefault();
e.stopPropagation();
form.handleSubmit();
}}
>
<form.Field name="email">
{#snippet children(field)}
<div class="space-y-1">
<label for={field.name}>Email</label>
<input
id={field.name}
name={field.name}
type="email"
class="w-full border"
onblur={field.handleBlur}
value={field.state.value}
oninput={(e: Event) => {
const target = e.target as HTMLInputElement
field.handleChange(target.value)
}} />
{#if field.state.meta.isTouched}
{#each field.state.meta.errors as error}
<p class="text-sm text-red-500" role="alert">{error}</p>
{/each}
{/if}
</div>
{/snippet}
</form.Field>
<form.Field name="password">
{#snippet children(field)}
<div class="space-y-1">
<label for={field.name}>Password</label>
<input
id={field.name}
name={field.name}
type="password"
class="w-full border"
onblur={field.handleBlur}
value={field.state.value}
oninput={(e: Event) => {
const target = e.target as HTMLInputElement
field.handleChange(target.value)
}}
/>
{#if field.state.meta.isTouched}
{#each field.state.meta.errors as error}
<p class="text-sm text-red-500" role="alert">{error}</p>
{/each}
{/if}
</div>
{/snippet}
</form.Field>
<form.Subscribe selector={(state) => ({ canSubmit: state.canSubmit, isSubmitting: state.isSubmitting })}>
{#snippet children(state)}
<button type="submit" class="w-full" disabled={!state.canSubmit || state.isSubmitting}>
{state.isSubmitting ? 'Submitting...' : 'Sign In'}
</button>
{/snippet}
</form.Subscribe>
</form>
<div class="mt-4 text-center">
<button type="button" class="text-indigo-600 hover:text-indigo-800" onclick={switchToSignUp}>
Need an account? Sign Up
</button>
</div>
</div>

View File

@@ -0,0 +1,142 @@
<script lang="ts">
import { createForm } from '@tanstack/svelte-form';
import z from 'zod';
import { authClient } from '$lib/auth-client';
import { goto } from '$app/navigation';
let { switchToSignIn } = $props<{ switchToSignIn: () => void }>();
const validationSchema = z.object({
name: z.string().min(2, 'Name must be at least 2 characters'),
email: z.email('Invalid email address'),
password: z.string().min(8, 'Password must be at least 8 characters'),
});
const form = createForm(() => ({
defaultValues: { name: '', email: '', password: '' },
onSubmit: async ({ value }) => {
await authClient.signUp.email(
{
email: value.email,
password: value.password,
name: value.name,
},
{
onSuccess: () => {
goto('/dashboard');
},
onError: (error) => {
console.log(error.error.message || 'Sign up failed. Please try again.');
},
}
);
},
validators: {
onSubmit: validationSchema,
},
}));
</script>
<div class="mx-auto mt-10 w-full max-w-md p-6">
<h1 class="mb-6 text-center font-bold text-3xl">Create Account</h1>
<form
id="form"
class="space-y-4"
onsubmit={(e) => {
e.preventDefault();
e.stopPropagation();
form.handleSubmit();
}}
>
<form.Field name="name">
{#snippet children(field)}
<div class="space-y-1">
<label for={field.name}>Name</label>
<input
id={field.name}
name={field.name}
class="w-full border"
onblur={field.handleBlur}
value={field.state.value}
oninput={(e: Event) => {
const target = e.target as HTMLInputElement
field.handleChange(target.value)
}}
/>
{#if field.state.meta.isTouched}
{#each field.state.meta.errors as error}
<p class="text-sm text-red-500" role="alert">{error}</p>
{/each}
{/if}
</div>
{/snippet}
</form.Field>
<form.Field name="email">
{#snippet children(field)}
<div class="space-y-1">
<label for={field.name}>Email</label>
<input
id={field.name}
name={field.name}
type="email"
class="w-full border"
onblur={field.handleBlur}
value={field.state.value}
oninput={(e: Event) => {
const target = e.target as HTMLInputElement
field.handleChange(target.value)
}}
/>
{#if field.state.meta.isTouched}
{#each field.state.meta.errors as error}
<p class="text-sm text-red-500" role="alert">{error}</p>
{/each}
{/if}
</div>
{/snippet}
</form.Field>
<form.Field name="password">
{#snippet children(field)}
<div class="space-y-1">
<label for={field.name}>Password</label>
<input
id={field.name}
name={field.name}
type="password"
class="w-full border"
onblur={field.handleBlur}
value={field.state.value}
oninput={(e: Event) => {
const target = e.target as HTMLInputElement
field.handleChange(target.value)
}}
/>
{#if field.state.meta.errors}
{#each field.state.meta.errors as error}
<p class="text-sm text-red-500" role="alert">{error}</p>
{/each}
{/if}
</div>
{/snippet}
</form.Field>
<form.Subscribe selector={(state) => ({ canSubmit: state.canSubmit, isSubmitting: state.isSubmitting })}>
{#snippet children(state)}
<button type="submit" class="w-full" disabled={!state.canSubmit || state.isSubmitting}>
{state.isSubmitting ? 'Submitting...' : 'Sign Up'}
</button>
{/snippet}
</form.Subscribe>
</form>
<div class="mt-4 text-center">
<button type="button" class="text-indigo-600 hover:text-indigo-800" onclick={switchToSignIn}>
Already have an account? Sign In
</button>
</div>
</div>

View File

@@ -0,0 +1,52 @@
<script lang="ts">
import { authClient } from '$lib/auth-client';
import { goto } from '$app/navigation';
const sessionQuery = authClient.useSession();
async function handleSignOut() {
await authClient.signOut({
fetchOptions: {
onSuccess: () => {
goto('/');
},
onError: (error) => {
console.error('Sign out failed:', error);
}
}
});
}
function goToLogin() {
goto('/login');
}
</script>
<div class="relative">
{#if $sessionQuery.isPending}
<div class="h-8 w-24 animate-pulse rounded bg-neutral-700"></div>
{:else if $sessionQuery.data?.user}
{@const user = $sessionQuery.data.user}
<div class="flex items-center gap-3">
<span class="text-sm text-neutral-300 hidden sm:inline" title={user.email}>
{user.name || user.email?.split('@')[0] || 'User'}
</span>
<button
onclick={handleSignOut}
class="rounded px-3 py-1 text-sm bg-red-600 hover:bg-red-700 text-white transition-colors"
>
Sign Out
</button>
</div>
{:else}
<div class="flex items-center gap-2">
<button
onclick={goToLogin}
class="rounded px-3 py-1 text-sm bg-indigo-600 hover:bg-indigo-700 text-white transition-colors"
>
Sign In
</button>
</div>
{/if}
</div>

View File

@@ -0,0 +1,6 @@
import { PUBLIC_SERVER_URL } from "$env/static/public";
import { createAuthClient } from "better-auth/svelte";
export const authClient = createAuthClient({
baseURL: PUBLIC_SERVER_URL,
});

View File

@@ -0,0 +1,36 @@
<script lang="ts">
import { onMount } from 'svelte';
import { goto } from '$app/navigation';
import { authClient } from '$lib/auth-client';
{{#if (eq api "orpc")}}
import { orpc } from '$lib/orpc';
import { createQuery } from '@tanstack/svelte-query';
{{/if}}
import { get } from 'svelte/store';
const sessionQuery = authClient.useSession();
{{#if (eq api "orpc")}}
const privateDataQuery = createQuery(orpc.privateData.queryOptions());
{{/if}}
onMount(() => {
const { data: session, isPending } = get(sessionQuery);
if (!session && !isPending) {
goto('/login');
}
});
</script>
{#if $sessionQuery.isPending}
<div>Loading...</div>
{:else if !$sessionQuery.data}
{:else}
<div>
<h1>Dashboard</h1>
<p>Welcome {$sessionQuery.data.user.name}</p>
{{#if (eq api "orpc")}}
<p>privateData: {$privateDataQuery.data?.message}</p>
{{/if}}
</div>
{/if}

View File

@@ -0,0 +1,12 @@
<script lang="ts">
import SignInForm from '../../components/SignInForm.svelte';
import SignUpForm from '../../components/SignUpForm.svelte';
let showSignIn = $state(true);
</script>
{#if showSignIn}
<SignInForm switchToSignUp={() => showSignIn = false} />
{:else}
<SignUpForm switchToSignIn={() => showSignIn = true} />
{/if}