feat: add Clerk Auth (#43)
This commit is contained in:
parent
1c5b37b24b
commit
ed2e18310e
17 changed files with 152 additions and 248 deletions
|
|
@ -1,12 +1,17 @@
|
|||
<script lang="ts">
|
||||
import ClerkProvider from 'clerk-sveltekit/client/ClerkProvider.svelte';
|
||||
import { i18n } from '$lib/i18n';
|
||||
import { ParaglideJS } from '@inlang/paraglide-sveltekit';
|
||||
import '../app.css';
|
||||
import { PUBLIC_CLERK_PUBLISHABLE_KEY } from '$env/static/public';
|
||||
|
||||
let { children } = $props();
|
||||
</script>
|
||||
|
||||
<div class="layout">
|
||||
{@render children()}
|
||||
<ClerkProvider publishableKey={PUBLIC_CLERK_PUBLISHABLE_KEY}>
|
||||
{@render children()}
|
||||
</ClerkProvider>
|
||||
</div>
|
||||
|
||||
<ParaglideJS {i18n}>
|
||||
|
|
|
|||
|
|
@ -1,5 +1,3 @@
|
|||
import { validateSession } from '$lib/server/auth';
|
||||
|
||||
export async function load(event) {
|
||||
await validateSession(event);
|
||||
}
|
||||
export const load = async (event) => validateSession(event);
|
||||
|
|
|
|||
|
|
@ -1,10 +1,3 @@
|
|||
import { validateSession } from '$lib/server/auth';
|
||||
|
||||
export async function load(event) {
|
||||
const {
|
||||
user: { password: _, ...rest },
|
||||
} = await validateSession(event);
|
||||
return {
|
||||
user: rest,
|
||||
};
|
||||
}
|
||||
export const load = async (event) => validateSession(event);
|
||||
|
|
|
|||
|
|
@ -2,6 +2,8 @@
|
|||
import { Navbar } from '$lib/components/Navigation';
|
||||
import type { User } from '@prisma/client';
|
||||
import type { Snippet } from 'svelte';
|
||||
import { onMount } from 'svelte';
|
||||
import 'clerk-sveltekit/client';
|
||||
|
||||
type Props = {
|
||||
children: Snippet;
|
||||
|
|
@ -11,6 +13,35 @@
|
|||
};
|
||||
|
||||
let { children, data }: Props = $props();
|
||||
|
||||
let clerk;
|
||||
|
||||
onMount(async () => {
|
||||
clerk = window.Clerk;
|
||||
|
||||
if (clerk) {
|
||||
if (!clerk.user || clerk.user?.organizationMemberships.length === 0) {
|
||||
console.debug('No organizations found');
|
||||
await clerk.signOut();
|
||||
return;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the active organization to the first one in the list, this is temporary until we let the user choose the organization
|
||||
*/
|
||||
try {
|
||||
// Set the active organization to the first one in the list
|
||||
await clerk.setActive({
|
||||
organization: clerk.user?.organizationMemberships[0].organization.id,
|
||||
});
|
||||
console.debug(
|
||||
`Active organization set to ${clerk.user?.organizationMemberships[0].organization.id}`
|
||||
);
|
||||
} catch (error) {
|
||||
console.error('Error setting active organization:', error);
|
||||
}
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
<Navbar title="Svelte" username={data.user.name} />
|
||||
|
|
|
|||
|
|
@ -1,96 +0,0 @@
|
|||
import { messages } from '$lib/i18n';
|
||||
import { logger } from '$lib/server/logger';
|
||||
import { auth } from '$lib/server/lucia.js';
|
||||
import { prisma } from '$lib/server/prisma';
|
||||
import { fail, redirect, type Actions } from '@sveltejs/kit';
|
||||
import { Argon2id } from 'oslo/password';
|
||||
import { string } from 'zod';
|
||||
|
||||
export const actions = {
|
||||
login: async (event) => {
|
||||
const form = await event.request.formData();
|
||||
const email = form.get('email');
|
||||
if (typeof email !== 'string') {
|
||||
return fail(400, { email: { error: messages.login_error_email_required() } });
|
||||
}
|
||||
|
||||
const password = form.get('password') as string;
|
||||
if (!password) {
|
||||
return fail(400, { password: { error: messages.login_error_password_required() } });
|
||||
}
|
||||
|
||||
const user = await prisma.user.findUnique({
|
||||
where: {
|
||||
email: email,
|
||||
},
|
||||
});
|
||||
if (!user) {
|
||||
logger.error(`User not found! ${email}`);
|
||||
return fail(404, {
|
||||
email: { value: email, error: messages.login_error_user_not_found() },
|
||||
});
|
||||
}
|
||||
|
||||
const validPassword = await new Argon2id().verify(user.password, password);
|
||||
if (!validPassword) {
|
||||
return fail(400, {
|
||||
password: { error: messages.login_error_password_incorrect() },
|
||||
});
|
||||
}
|
||||
const session = await auth.createSession(user.id, []);
|
||||
const sessionCookie = auth.createSessionCookie(session.id);
|
||||
event.cookies.set(sessionCookie.name, sessionCookie.value, {
|
||||
path: '/',
|
||||
maxAge: 120,
|
||||
});
|
||||
redirect(302, '/');
|
||||
},
|
||||
|
||||
register: async (event) => {
|
||||
const form = await event.request.formData();
|
||||
if (!form.has('email')) {
|
||||
return fail(400, { email: { error: messages.login_error_email_required() } });
|
||||
}
|
||||
const { success, data: email, error } = string().email().safeParse(form.get('email'));
|
||||
if (!success) {
|
||||
logger.error(error);
|
||||
return fail(400, {
|
||||
email: { value: email, error: messages.login_error_email_incorrect() },
|
||||
});
|
||||
}
|
||||
|
||||
const password = form.get('password');
|
||||
if (typeof password !== 'string') {
|
||||
return fail(400, { password: { error: messages.login_error_password_required() } });
|
||||
}
|
||||
const name = form.get('name');
|
||||
if (typeof name !== 'string') {
|
||||
return fail(400, { name: { error: messages.login_error_name_required() } });
|
||||
}
|
||||
|
||||
const usersWithEmail = await prisma.user.count({ where: { email: email } });
|
||||
if (usersWithEmail !== 0) {
|
||||
return fail(409, {
|
||||
email: { value: email, error: messages.login_error_email_inuse() },
|
||||
});
|
||||
}
|
||||
|
||||
const hashedPassword = await new Argon2id().hash(password);
|
||||
const user = await prisma.user.create({
|
||||
data: {
|
||||
email: form.get('email') as string,
|
||||
name: form.get('name') as string,
|
||||
password: hashedPassword,
|
||||
},
|
||||
});
|
||||
|
||||
const session = await auth.createSession(user.id.toString(), {});
|
||||
const sessionCookie = auth.createSessionCookie(session.id);
|
||||
event.cookies.set(sessionCookie.name, sessionCookie.value, {
|
||||
path: '/',
|
||||
maxAge: 120,
|
||||
});
|
||||
|
||||
redirect(303, '/');
|
||||
},
|
||||
} satisfies Actions;
|
||||
|
|
@ -1,77 +1,23 @@
|
|||
<script lang="ts">
|
||||
import { TextInput } from '$lib/components/DataInput';
|
||||
import { Button } from '$lib/components/Actions';
|
||||
import Tabs from '$lib/components/Navigation/Tabs';
|
||||
import { messages } from '$lib/i18n/index.js';
|
||||
import { fade } from 'svelte/transition';
|
||||
|
||||
let { form } = $props();
|
||||
|
||||
let tab: 0 | 1 = $state(0);
|
||||
import SignIn from 'clerk-sveltekit/client/SignIn.svelte';
|
||||
import { dark } from '@clerk/themes';
|
||||
</script>
|
||||
|
||||
{#snippet userIcon()}
|
||||
<i class="fi fi-br-envelope"></i>
|
||||
{/snippet}
|
||||
|
||||
{#snippet passwordIcon()}
|
||||
<i class="fi fi-br-key"></i>
|
||||
{/snippet}
|
||||
|
||||
{#snippet nameIcon()}
|
||||
<i class="fi fi-rr-user"></i>
|
||||
{/snippet}
|
||||
|
||||
{#snippet alert(message: string)}
|
||||
<div role="alert" class="alert alert-error absolute -top-20" transition:fade>
|
||||
<i class="fi fi-bs-octagon-xmark h-6 w-6 shrink-0"></i>
|
||||
<span>{message}</span>
|
||||
</div>
|
||||
{/snippet}
|
||||
|
||||
{#snippet Form(variant: 'login' | 'register')}
|
||||
<form method="POST" action={`?/${variant}`}>
|
||||
<div class="card-body gap-4">
|
||||
<TextInput
|
||||
start={userIcon}
|
||||
placeholder={messages.login_label_email()}
|
||||
name="email"
|
||||
type="email"
|
||||
/>
|
||||
<TextInput
|
||||
start={passwordIcon}
|
||||
placeholder={messages.login_label_password()}
|
||||
name="password"
|
||||
type="password"
|
||||
/>
|
||||
{#if variant === 'register'}
|
||||
<TextInput
|
||||
start={nameIcon}
|
||||
placeholder={messages.login_label_name()}
|
||||
name="name"
|
||||
fade
|
||||
/>
|
||||
{/if}
|
||||
</div>
|
||||
<div class="card-actions px-4">
|
||||
<Button block type="submit" outline>{messages.login_button_submit()}</Button>
|
||||
</div>
|
||||
</form>
|
||||
{/snippet}
|
||||
|
||||
<div class="page" transition:fade>
|
||||
<div class="card bg-base-200 py-4 shadow-xl">
|
||||
<div class="card-title">
|
||||
{#if form}
|
||||
{@render alert(Object.values(form)[0].error)}
|
||||
{/if}
|
||||
<Tabs
|
||||
variant="bordered"
|
||||
bind:selected={tab}
|
||||
tabs={[messages.login_tab_login(), messages.login_tab_register()]}
|
||||
/>
|
||||
</div>
|
||||
{@render Form(tab === 0 ? 'login' : 'register')}
|
||||
<div>
|
||||
<SignIn
|
||||
appearance={{
|
||||
baseTheme: dark,
|
||||
variables: {
|
||||
colorPrimary: '#FFF',
|
||||
},
|
||||
elements: {
|
||||
// Remove the sign-up link
|
||||
footerAction: { display: 'none' },
|
||||
},
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue