mirror of
https://github.com/bartvdbraak/omnidash.git
synced 2025-06-28 20:29:13 +00:00
feat: add initial skeleton for authentication in sveltekit with pocketbase
This commit is contained in:
parent
69ccab8129
commit
88884a69ac
30 changed files with 682 additions and 197 deletions
6
apps/web/src/app.d.ts
vendored
6
apps/web/src/app.d.ts
vendored
|
@ -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 {}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
22
apps/web/src/hooks.server.ts
Normal file
22
apps/web/src/hooks.server.ts
Normal 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;
|
||||
};
|
25
apps/web/src/lib/components/ui/button/button.svelte
Normal file
25
apps/web/src/lib/components/ui/button/button.svelte
Normal 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>
|
49
apps/web/src/lib/components/ui/button/index.ts
Normal file
49
apps/web/src/lib/components/ui/button/index.ts
Normal 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
|
||||
};
|
27
apps/web/src/lib/components/ui/input/index.ts
Normal file
27
apps/web/src/lib/components/ui/input/index.ts
Normal 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
|
||||
};
|
35
apps/web/src/lib/components/ui/input/input.svelte
Normal file
35
apps/web/src/lib/components/ui/input/input.svelte
Normal 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}
|
||||
/>
|
7
apps/web/src/lib/components/ui/label/index.ts
Normal file
7
apps/web/src/lib/components/ui/label/index.ts
Normal file
|
@ -0,0 +1,7 @@
|
|||
import Root from './label.svelte';
|
||||
|
||||
export {
|
||||
Root,
|
||||
//
|
||||
Root as Label
|
||||
};
|
19
apps/web/src/lib/components/ui/label/label.svelte
Normal file
19
apps/web/src/lib/components/ui/label/label.svelte
Normal 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>
|
1
apps/web/src/lib/constants.ts
Normal file
1
apps/web/src/lib/constants.ts
Normal file
|
@ -0,0 +1 @@
|
|||
export const AUTH_COOKIE_REF = 'auth-cookie';
|
14
apps/web/src/lib/pocketbase.ts
Normal file
14
apps/web/src/lib/pocketbase.ts
Normal 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);
|
||||
});
|
|
@ -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);
|
||||
};
|
||||
|
|
10
apps/web/src/routes/(auth)/+layout.server.ts
Normal file
10
apps/web/src/routes/(auth)/+layout.server.ts
Normal 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;
|
48
apps/web/src/routes/(auth)/login/+page.server.ts
Normal file
48
apps/web/src/routes/(auth)/login/+page.server.ts
Normal 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, '/');
|
||||
}
|
||||
};
|
25
apps/web/src/routes/(auth)/login/+page.svelte
Normal file
25
apps/web/src/routes/(auth)/login/+page.svelte
Normal 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>
|
65
apps/web/src/routes/(auth)/signup/+page.server.ts
Normal file
65
apps/web/src/routes/(auth)/signup/+page.server.ts
Normal 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, '/');
|
||||
}
|
||||
};
|
23
apps/web/src/routes/(auth)/signup/+page.svelte
Normal file
23
apps/web/src/routes/(auth)/signup/+page.svelte
Normal 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>
|
10
apps/web/src/routes/(board)/+layout.server.ts
Normal file
10
apps/web/src/routes/(board)/+layout.server.ts
Normal 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;
|
8
apps/web/src/routes/(board)/board/+page.svelte
Normal file
8
apps/web/src/routes/(board)/board/+page.svelte
Normal 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>
|
|
@ -1 +1,5 @@
|
|||
<script>import "../app.pcss";</script><slot></slot>
|
||||
<script>
|
||||
import '../app.pcss';
|
||||
</script>
|
||||
|
||||
<slot />
|
||||
|
|
7
apps/web/src/routes/+page.server.ts
Normal file
7
apps/web/src/routes/+page.server.ts
Normal file
|
@ -0,0 +1,7 @@
|
|||
import type { PageServerLoad } from './$types';
|
||||
|
||||
export const load = (async ({ locals }) => {
|
||||
return {
|
||||
authenticated: locals.pocketBase.authStore.isValid
|
||||
};
|
||||
}) satisfies PageServerLoad;
|
|
@ -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>
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue