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