feat: add initial skeleton for authentication in sveltekit with pocketbase

This commit is contained in:
Bart van der Braak 2024-01-28 22:21:47 +01:00
parent 69ccab8129
commit 88884a69ac
30 changed files with 682 additions and 197 deletions

View file

@ -1,11 +1,13 @@
import PocketBase from 'pocketbase';
// See https://kit.svelte.dev/docs/types#app
// for information about these interfaces
declare global {
namespace App {
interface Locals {
pocketBase: PocketBase;
}
// interface Error {}
// interface Locals {}
// interface PageData {}
// interface PageState {}
// interface Platform {}
}
}

View file

@ -1,78 +1,78 @@
@tailwind base;
@tailwind components;
@tailwind utilities;
@layer base {
:root {
--background: 0 0% 100%;
--foreground: 0 0% 3.9%;
--muted: 0 0% 96.1%;
--muted-foreground: 0 0% 45.1%;
--popover: 0 0% 100%;
--popover-foreground: 0 0% 3.9%;
--card: 0 0% 100%;
--card-foreground: 0 0% 3.9%;
--border: 0 0% 89.8%;
--input: 0 0% 89.8%;
--primary: 0 0% 9%;
--primary-foreground: 0 0% 98%;
--secondary: 0 0% 96.1%;
--secondary-foreground: 0 0% 9%;
--accent: 0 0% 96.1%;
--accent-foreground: 0 0% 9%;
--destructive: 0 72.2% 50.6%;
--destructive-foreground: 0 0% 98%;
--ring: 0 0% 3.9%;
--radius: 0.5rem;
}
.dark {
--background: 0 0% 3.9%;
--foreground: 0 0% 98%;
--muted: 0 0% 14.9%;
--muted-foreground: 0 0% 63.9%;
--popover: 0 0% 3.9%;
--popover-foreground: 0 0% 98%;
--card: 0 0% 3.9%;
--card-foreground: 0 0% 98%;
--border: 0 0% 14.9%;
--input: 0 0% 14.9%;
--primary: 0 0% 98%;
--primary-foreground: 0 0% 9%;
--secondary: 0 0% 14.9%;
--secondary-foreground: 0 0% 98%;
--accent: 0 0% 14.9%;
--accent-foreground: 0 0% 98%;
--destructive: 0 62.8% 30.6%;
--destructive-foreground: 0 0% 98%;
--ring: 0 0% 83.1%;
}
:root {
--background: 0 0% 100%;
--foreground: 0 0% 3.9%;
--muted: 0 0% 96.1%;
--muted-foreground: 0 0% 45.1%;
--popover: 0 0% 100%;
--popover-foreground: 0 0% 3.9%;
--card: 0 0% 100%;
--card-foreground: 0 0% 3.9%;
--border: 0 0% 89.8%;
--input: 0 0% 89.8%;
--primary: 0 0% 9%;
--primary-foreground: 0 0% 98%;
--secondary: 0 0% 96.1%;
--secondary-foreground: 0 0% 9%;
--accent: 0 0% 96.1%;
--accent-foreground: 0 0% 9%;
--destructive: 0 72.2% 50.6%;
--destructive-foreground: 0 0% 98%;
--ring: 0 0% 3.9%;
--radius: 0.5rem;
}
.dark {
--background: 0 0% 3.9%;
--foreground: 0 0% 98%;
--muted: 0 0% 14.9%;
--muted-foreground: 0 0% 63.9%;
--popover: 0 0% 3.9%;
--popover-foreground: 0 0% 98%;
--card: 0 0% 3.9%;
--card-foreground: 0 0% 98%;
--border: 0 0% 14.9%;
--input: 0 0% 14.9%;
--primary: 0 0% 98%;
--primary-foreground: 0 0% 9%;
--secondary: 0 0% 14.9%;
--secondary-foreground: 0 0% 98%;
--accent: 0 0% 14.9%;
--accent-foreground: 0 0% 98%;
--destructive: 0 62.8% 30.6%;
--destructive-foreground: 0 0% 98%;
--ring: 0 0% 83.1%;
}
}
@layer base {
* {
@apply border-border;
}
body {
@apply bg-background text-foreground;
}
}
* {
@apply border-border;
}
body {
@apply bg-background text-foreground;
}
}

View file

@ -0,0 +1,22 @@
import type { Handle } from '@sveltejs/kit';
import PocketBase from 'pocketbase';
import { pb } from '$lib/pocketbase';
import { PUBLIC_CLIENT_PB } from '$env/static/public';
/** @type {import('@sveltejs/kit').Handle} */
export const handle: Handle = async ({ event, resolve }) => {
event.locals.pocketBase = new PocketBase(PUBLIC_CLIENT_PB);
pb.set(event.locals.pocketBase);
event.locals.pocketBase.authStore.loadFromCookie(event.request.headers.get('cookie') ?? '');
const response = await resolve(event);
response.headers.set(
'set-cookie',
event.locals.pocketBase.authStore.exportToCookie({ secure: false })
);
return response;
};

View file

@ -0,0 +1,25 @@
<script lang="ts">
import { Button as ButtonPrimitive } from 'bits-ui';
import { cn } from '$lib/utils';
import { buttonVariants, type Props, type Events } from '.';
type $$Props = Props;
type $$Events = Events;
let className: $$Props['class'] = undefined;
export let variant: $$Props['variant'] = 'default';
export let size: $$Props['size'] = 'default';
export let builders: $$Props['builders'] = [];
export { className as class };
</script>
<ButtonPrimitive.Root
{builders}
class={cn(buttonVariants({ variant, size, className }))}
type="button"
{...$$restProps}
on:click
on:keydown
>
<slot />
</ButtonPrimitive.Root>

View file

@ -0,0 +1,49 @@
import type { Button as ButtonPrimitive } from 'bits-ui';
import { tv, type VariantProps } from 'tailwind-variants';
import Root from './button.svelte';
const buttonVariants = tv({
base: 'inline-flex items-center justify-center rounded-md text-sm font-medium whitespace-nowrap transition-colors focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:pointer-events-none disabled:opacity-50',
variants: {
variant: {
default: 'bg-primary text-primary-foreground shadow hover:bg-primary/90',
destructive: 'bg-destructive text-destructive-foreground shadow-sm hover:bg-destructive/90',
outline:
'border border-input bg-transparent shadow-sm hover:bg-accent hover:text-accent-foreground',
secondary: 'bg-secondary text-secondary-foreground shadow-sm hover:bg-secondary/80',
ghost: 'hover:bg-accent hover:text-accent-foreground',
link: 'text-primary underline-offset-4 hover:underline'
},
size: {
default: 'h-9 px-4 py-2',
sm: 'h-8 rounded-md px-3 text-xs',
lg: 'h-10 rounded-md px-8',
icon: 'h-9 w-9'
}
},
defaultVariants: {
variant: 'default',
size: 'default'
}
});
type Variant = VariantProps<typeof buttonVariants>['variant'];
type Size = VariantProps<typeof buttonVariants>['size'];
type Props = ButtonPrimitive.Props & {
variant?: Variant;
size?: Size;
};
type Events = ButtonPrimitive.Events;
export {
Root,
type Props,
type Events,
//
Root as Button,
type Props as ButtonProps,
type Events as ButtonEvents,
buttonVariants
};

View file

@ -0,0 +1,27 @@
import Root from './input.svelte';
type FormInputEvent<T extends Event = Event> = T & {
currentTarget: EventTarget & HTMLInputElement;
};
export type InputEvents = {
blur: FormInputEvent<FocusEvent>;
change: FormInputEvent<Event>;
click: FormInputEvent<MouseEvent>;
focus: FormInputEvent<FocusEvent>;
focusin: FormInputEvent<FocusEvent>;
focusout: FormInputEvent<FocusEvent>;
keydown: FormInputEvent<KeyboardEvent>;
keypress: FormInputEvent<KeyboardEvent>;
keyup: FormInputEvent<KeyboardEvent>;
mouseover: FormInputEvent<MouseEvent>;
mouseenter: FormInputEvent<MouseEvent>;
mouseleave: FormInputEvent<MouseEvent>;
paste: FormInputEvent<ClipboardEvent>;
input: FormInputEvent<InputEvent>;
};
export {
Root,
//
Root as Input
};

View file

@ -0,0 +1,35 @@
<script lang="ts">
import type { HTMLInputAttributes } from 'svelte/elements';
import { cn } from '$lib/utils';
import type { InputEvents } from '.';
type $$Props = HTMLInputAttributes;
type $$Events = InputEvents;
let className: $$Props['class'] = undefined;
export let value: $$Props['value'] = undefined;
export { className as class };
</script>
<input
class={cn(
'flex h-9 w-full rounded-md border border-input bg-background px-3 py-1 text-sm shadow-sm transition-colors file:border-0 file:bg-transparent file:text-sm file:font-medium file:text-foreground placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:cursor-not-allowed disabled:opacity-50',
className
)}
bind:value
on:blur
on:change
on:click
on:focus
on:focusin
on:focusout
on:keydown
on:keypress
on:keyup
on:mouseover
on:mouseenter
on:mouseleave
on:paste
on:input
{...$$restProps}
/>

View file

@ -0,0 +1,7 @@
import Root from './label.svelte';
export {
Root,
//
Root as Label
};

View file

@ -0,0 +1,19 @@
<script lang="ts">
import { Label as LabelPrimitive } from 'bits-ui';
import { cn } from '$lib/utils';
type $$Props = LabelPrimitive.Props;
let className: $$Props['class'] = undefined;
export { className as class };
</script>
<LabelPrimitive.Root
class={cn(
'text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70',
className
)}
{...$$restProps}
>
<slot />
</LabelPrimitive.Root>

View file

@ -0,0 +1 @@
export const AUTH_COOKIE_REF = 'auth-cookie';

View file

@ -0,0 +1,14 @@
import PocketBase from 'pocketbase';
import { browser } from '$app/environment';
import { writable } from 'svelte/store';
import { PUBLIC_CLIENT_PB } from '$env/static/public';
export const pb = writable<PocketBase | undefined>(undefined, (set) => {
if (!browser) {
return;
}
const pocketbaseInstance = new PocketBase(PUBLIC_CLIENT_PB);
set(pocketbaseInstance);
});

View file

@ -1,62 +1,60 @@
import { type ClassValue, clsx } from "clsx";
import { twMerge } from "tailwind-merge";
import { cubicOut } from "svelte/easing";
import type { TransitionConfig } from "svelte/transition";
import { type ClassValue, clsx } from 'clsx';
import { twMerge } from 'tailwind-merge';
import { cubicOut } from 'svelte/easing';
import type { TransitionConfig } from 'svelte/transition';
export function cn(...inputs: ClassValue[]) {
return twMerge(clsx(inputs));
return twMerge(clsx(inputs));
}
type FlyAndScaleParams = {
y?: number;
x?: number;
start?: number;
duration?: number;
y?: number;
x?: number;
start?: number;
duration?: number;
};
export const flyAndScale = (
node: Element,
params: FlyAndScaleParams = { y: -8, x: 0, start: 0.95, duration: 150 }
node: Element,
params: FlyAndScaleParams = { y: -8, x: 0, start: 0.95, duration: 150 }
): TransitionConfig => {
const style = getComputedStyle(node);
const transform = style.transform === "none" ? "" : style.transform;
const style = getComputedStyle(node);
const transform = style.transform === 'none' ? '' : style.transform;
const scaleConversion = (
valueA: number,
scaleA: [number, number],
scaleB: [number, number]
) => {
const [minA, maxA] = scaleA;
const [minB, maxB] = scaleB;
const scaleConversion = (valueA: number, scaleA: [number, number], scaleB: [number, number]) => {
const [minA, maxA] = scaleA;
const [minB, maxB] = scaleB;
const percentage = (valueA - minA) / (maxA - minA);
const valueB = percentage * (maxB - minB) + minB;
const percentage = (valueA - minA) / (maxA - minA);
const valueB = percentage * (maxB - minB) + minB;
return valueB;
};
return valueB;
};
const styleToString = (
style: Record<string, number | string | undefined>
): string => {
return Object.keys(style).reduce((str, key) => {
if (style[key] === undefined) return str;
return str + `${key}:${style[key]};`;
}, "");
};
const styleToString = (style: Record<string, number | string | undefined>): string => {
return Object.keys(style).reduce((str, key) => {
if (style[key] === undefined) return str;
return str + `${key}:${style[key]};`;
}, '');
};
return {
duration: params.duration ?? 200,
delay: 0,
css: (t) => {
const y = scaleConversion(t, [0, 1], [params.y ?? 5, 0]);
const x = scaleConversion(t, [0, 1], [params.x ?? 0, 0]);
const scale = scaleConversion(t, [0, 1], [params.start ?? 0.95, 1]);
return {
duration: params.duration ?? 200,
delay: 0,
css: (t) => {
const y = scaleConversion(t, [0, 1], [params.y ?? 5, 0]);
const x = scaleConversion(t, [0, 1], [params.x ?? 0, 0]);
const scale = scaleConversion(t, [0, 1], [params.start ?? 0.95, 1]);
return styleToString({
transform: `${transform} translate3d(${x}px, ${y}px, 0) scale(${scale})`,
opacity: t
});
},
easing: cubicOut
};
};
return styleToString({
transform: `${transform} translate3d(${x}px, ${y}px, 0) scale(${scale})`,
opacity: t
});
},
easing: cubicOut
};
};
export const serializeNonPOJOs = (obj: unknown) => {
return structuredClone(obj);
};

View file

@ -0,0 +1,10 @@
import { redirect } from '@sveltejs/kit';
import type { LayoutServerLoad } from './$types';
export const load = (async ({ locals }) => {
if (locals.pocketBase.authStore.isValid) {
throw redirect(303, '/');
}
return {};
}) satisfies LayoutServerLoad;

View file

@ -0,0 +1,48 @@
import { redirect } from '@sveltejs/kit';
export const actions = {
default: async ({ request, locals }: { request: Request; locals: App.Locals }) => {
if (locals.pocketBase.authStore.isValid) {
return;
}
const formData = await request.formData();
const email = formData.get('email');
const password = formData.get('password');
try {
if (typeof email !== 'string') {
throw new Error('Email must be a string');
}
if (email.length < 5) {
throw new Error('Please enter a valid e-mail address');
}
if (typeof password !== 'string') {
throw new Error('Password must be a string');
}
if (password.length < 8) {
throw new Error('Password must be at least 8 characters in length');
}
await locals.pocketBase.collection('users').authWithPassword(email, password);
} catch (error) {
console.error(error);
if (!(error instanceof Error)) {
return {
email,
password,
error: 'Unknown error occured when signing up user'
};
}
return { error: error.message, email, password };
}
throw redirect(303, '/');
}
};

View file

@ -0,0 +1,25 @@
<script lang="ts">
import type { ActionData } from './$types';
export let form: ActionData;
export const data = {
title: 'Log in'
};
</script>
<h1>Log in</h1>
<form method="POST">
<input type="email" name="email" placeholder="E-mail" value={form?.email ?? ''} />
<input type="password" name="password" placeholder="Password" value={form?.password ?? ''} />
<button type="submit">Log in</button>
</form>
{#if form?.error}
<p>{form.error}</p>
{/if}
<p>Don't have an account?</p>
<a href="/signup">Sign up</a>

View file

@ -0,0 +1,65 @@
import { redirect } from '@sveltejs/kit';
export const actions = {
default: async ({ request, locals }: { request: Request; locals: App.Locals }) => {
if (locals.pocketBase.authStore.isValid) {
return;
}
const formData = await request.formData();
const name = formData.get('name');
const email = formData.get('email');
const password = formData.get('password');
try {
if (typeof name !== 'string') {
throw new Error('Name must be a string');
}
if (name.length === 0) {
throw new Error('Please enter a valid name');
}
if (typeof email !== 'string') {
throw new Error('Email must be a string');
}
if (email.length < 5) {
throw new Error('Please enter a valid e-mail address');
}
if (typeof password !== 'string') {
throw new Error('Password must be a string');
}
if (password.length < 8) {
throw new Error('Password must be at least 8 characters in length');
}
await locals.pocketBase.collection('users').create({
email,
password,
name,
passwordConfirm: password
});
await locals.pocketBase.collection('users').authWithPassword(email, password);
} catch (error) {
console.error(error);
if (!(error instanceof Error)) {
return {
name,
email,
password,
error: 'Unknown error occured when signing up user'
};
}
return { error: error.message, name, email, password };
}
throw redirect(303, '/');
}
};

View file

@ -0,0 +1,23 @@
<script lang="ts">
import type { ActionData } from './$types';
export let form: ActionData;
</script>
<h1>Sign up</h1>
<form method="POST">
<input type="text" name="name" placeholder="name" value={form?.name ?? ''} />
<input type="email" name="email" placeholder="E-mail" value={form?.email ?? ''} />
<input type="password" name="password" placeholder="Password" value={form?.password ?? ''} />
<button type="submit">Sign up</button>
</form>
{#if form?.error}
<p>{form.error}</p>
{/if}
<p>Already have an account?</p>
<a href="/login">Log in</a>

View file

@ -0,0 +1,10 @@
import { redirect } from '@sveltejs/kit';
import type { LayoutServerLoad } from './$types';
export const load = (async ({ locals }) => {
if (!locals.pocketBase.authStore.isValid) {
throw redirect(303, '/signup');
}
return {};
}) satisfies LayoutServerLoad;

View file

@ -0,0 +1,8 @@
<script lang="ts">
</script>
<h1>Create a new dashboard</h1>
<form method="POST">
<button type="submit">Create dashboard</button>
</form>

View file

@ -1 +1,5 @@
<script>import "../app.pcss";</script><slot></slot>
<script>
import '../app.pcss';
</script>
<slot />

View file

@ -0,0 +1,7 @@
import type { PageServerLoad } from './$types';
export const load = (async ({ locals }) => {
return {
authenticated: locals.pocketBase.authStore.isValid
};
}) satisfies PageServerLoad;

View file

@ -1,2 +1,26 @@
<h1>Welcome to SvelteKit</h1>
<p>Visit <a href="https://kit.svelte.dev">kit.svelte.dev</a> to read the documentation</p>
<script lang="ts">
import type { PageData } from './$types';
export let data: PageData;
</script>
<h1>Login with SvelteKit and Pocketbase</h1>
{#if data.authenticated}
<a href="/board/">Create a new game</a>
{:else}
<div class="links">
<a href="/login">Log in</a>
<p>or</p>
<a href="/signup">Sign up</a>
</div>
{/if}
<style>
.links {
display: flex;
gap: 1rem;
align-items: center;
font-size: 1.5rem;
}
</style>