initial production version
This commit is contained in:
68
src/routes/signup/+page.server.ts
Normal file
68
src/routes/signup/+page.server.ts
Normal file
@@ -0,0 +1,68 @@
|
||||
import { fail, redirect } from '@sveltejs/kit';
|
||||
import type { Actions, PageServerLoad } from './$types';
|
||||
import { db } from '$lib/server/db';
|
||||
import { users } from '$lib/server/schema';
|
||||
import { eq } from 'drizzle-orm';
|
||||
import bcrypt from 'bcrypt';
|
||||
import { env } from '$env/dynamic/private';
|
||||
|
||||
export const load: PageServerLoad = async () => {
|
||||
// Determine which OAuth providers are available
|
||||
const oauthProviders = [];
|
||||
|
||||
if (env.GOOGLE_CLIENT_ID && env.GOOGLE_CLIENT_SECRET) {
|
||||
oauthProviders.push({ id: 'google', name: 'Google' });
|
||||
}
|
||||
|
||||
if (env.AUTHENTIK_CLIENT_ID && env.AUTHENTIK_CLIENT_SECRET && env.AUTHENTIK_ISSUER) {
|
||||
oauthProviders.push({ id: 'authentik', name: 'Authentik' });
|
||||
}
|
||||
|
||||
return {
|
||||
oauthProviders
|
||||
};
|
||||
};
|
||||
|
||||
export const actions: Actions = {
|
||||
default: async ({ request }) => {
|
||||
const formData = await request.formData();
|
||||
const name = formData.get('name') as string;
|
||||
const username = formData.get('username') as string;
|
||||
const password = formData.get('password') as string;
|
||||
const confirmPassword = formData.get('confirmPassword') as string;
|
||||
|
||||
if (!name?.trim()) {
|
||||
return fail(400, { error: 'Name is required', name, username });
|
||||
}
|
||||
|
||||
if (!username?.trim()) {
|
||||
return fail(400, { error: 'Username is required', name, username });
|
||||
}
|
||||
|
||||
if (!password || password.length < 8) {
|
||||
return fail(400, { error: 'Password must be at least 8 characters', name, username });
|
||||
}
|
||||
|
||||
if (password !== confirmPassword) {
|
||||
return fail(400, { error: 'Passwords do not match', name, username });
|
||||
}
|
||||
|
||||
const existingUser = await db.query.users.findFirst({
|
||||
where: eq(users.username, username.trim().toLowerCase())
|
||||
});
|
||||
|
||||
if (existingUser) {
|
||||
return fail(400, { error: 'Username already taken', name, username });
|
||||
}
|
||||
|
||||
const hashedPassword = await bcrypt.hash(password, 10);
|
||||
|
||||
await db.insert(users).values({
|
||||
name: name.trim(),
|
||||
username: username.trim().toLowerCase(),
|
||||
password: hashedPassword
|
||||
});
|
||||
|
||||
throw redirect(303, '/signin?registered=true');
|
||||
}
|
||||
};
|
||||
81
src/routes/signup/+page.svelte
Normal file
81
src/routes/signup/+page.svelte
Normal file
@@ -0,0 +1,81 @@
|
||||
<script lang="ts">
|
||||
import { Button } from '$lib/components/ui/button';
|
||||
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '$lib/components/ui/card';
|
||||
import { Input } from '$lib/components/ui/input';
|
||||
import { Label } from '$lib/components/ui/label';
|
||||
import { ThemeToggle } from '$lib/components/ui/theme-toggle';
|
||||
import type { ActionData, PageData } from './$types';
|
||||
import { signIn } from '@auth/sveltekit/client';
|
||||
|
||||
let { form, data }: { form: ActionData; data: PageData } = $props();
|
||||
</script>
|
||||
|
||||
<div class="min-h-screen flex items-center justify-center p-4">
|
||||
<div class="absolute top-4 right-4">
|
||||
<ThemeToggle />
|
||||
</div>
|
||||
<Card class="w-full max-w-md">
|
||||
<CardHeader>
|
||||
<CardTitle class="text-2xl">Create an Account</CardTitle>
|
||||
<CardDescription>Sign up to manage your wishlists</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent class="space-y-4">
|
||||
{#if form?.error}
|
||||
<div class="bg-red-50 border border-red-200 text-red-700 dark:bg-red-950 dark:border-red-800 dark:text-red-300 px-4 py-3 rounded">
|
||||
{form.error}
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
<form method="POST" class="space-y-4">
|
||||
<div class="space-y-2">
|
||||
<Label for="name">Name</Label>
|
||||
<Input id="name" name="name" type="text" required value={form?.name || ''} />
|
||||
</div>
|
||||
|
||||
<div class="space-y-2">
|
||||
<Label for="username">Username</Label>
|
||||
<Input id="username" name="username" type="text" required value={form?.username || ''} />
|
||||
</div>
|
||||
|
||||
<div class="space-y-2">
|
||||
<Label for="password">Password</Label>
|
||||
<Input id="password" name="password" type="password" required minlength={8} />
|
||||
</div>
|
||||
|
||||
<div class="space-y-2">
|
||||
<Label for="confirmPassword">Confirm Password</Label>
|
||||
<Input id="confirmPassword" name="confirmPassword" type="password" required minlength={8} />
|
||||
</div>
|
||||
|
||||
<Button type="submit" class="w-full">Sign Up</Button>
|
||||
</form>
|
||||
|
||||
{#if data.oauthProviders.length > 0}
|
||||
<div class="relative">
|
||||
<div class="absolute inset-0 flex items-center">
|
||||
<span class="w-full border-t"></span>
|
||||
</div>
|
||||
<div class="relative flex justify-center text-xs uppercase">
|
||||
<span class="bg-card px-2 text-muted-foreground">Or continue with</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{#each data.oauthProviders as provider}
|
||||
<Button
|
||||
type="button"
|
||||
variant="outline"
|
||||
class="w-full"
|
||||
onclick={() => signIn(provider.id, { callbackUrl: '/dashboard' })}
|
||||
>
|
||||
Sign up with {provider.name}
|
||||
</Button>
|
||||
{/each}
|
||||
{/if}
|
||||
|
||||
<div class="text-center text-sm text-muted-foreground">
|
||||
Already have an account?
|
||||
<a href="/signin" class="text-primary hover:underline">Sign in</a>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
Reference in New Issue
Block a user