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,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>
);
}