Update getting started guide for SvelteKit

This commit is contained in:
Andrew Smith
2023-03-07 16:35:44 +00:00
parent 8e6a501169
commit f1af6c9ee1
2 changed files with 429 additions and 273 deletions

View File

@@ -26,12 +26,12 @@ We can use the [SvelteKit Skeleton Project](https://kit.svelte.dev/docs) to init
an app called `supabase-sveltekit` (for this tutorial we will be using TypeScript):
```bash
npm init svelte@next supabase-sveltekit
npm create svelte@latest supabase-sveltekit
cd supabase-sveltekit
npm install
```
Then let's install the only additional dependency: [supabase-js](https://github.com/supabase/supabase-js)
Then install the Supabase client library: [supabase-js](https://github.com/supabase/supabase-js)
```bash
npm install @supabase/supabase-js
@@ -45,7 +45,7 @@ PUBLIC_SUPABASE_URL="YOUR_SUPABASE_URL"
PUBLIC_SUPABASE_ANON_KEY="YOUR_SUPABASE_KEY"
```
Now that we have the API credentials in place, create a `src/lib/supabaseClient.ts` helper file to initialize the Supabase client.
{/* Now that we have the API credentials in place, create a `src/lib/supabaseClient.ts` helper file to initialize the Supabase client.
These variables will be exposed on the browser, and that's completely fine since we have [Row Level Security](/docs/guides/auth#row-level-security) enabled on our Database.
```js title=src/lib/supabaseClient.ts
@@ -53,9 +53,9 @@ import { createClient } from '@supabase/auth-helpers-sveltekit'
import { PUBLIC_SUPABASE_URL, PUBLIC_SUPABASE_ANON_KEY } from '$env/static/public'
export const supabase = createClient(PUBLIC_SUPABASE_URL, PUBLIC_SUPABASE_ANON_KEY)
```
``` */}
Optionally, update `src/routes/styles.css` with the [CSS from the example](https://raw.githubusercontent.com/supabase/supabase/master/examples/user-management/svelte-user-management/src/app.css).
Optionally, add `src/styles.css` with the [CSS from the example](https://raw.githubusercontent.com/supabase/supabase/master/examples/user-management/svelte-user-management/src/app.css).
### Supabase Auth Helpers
@@ -69,249 +69,380 @@ Install the auth helpers for SvelteKit:
npm install @supabase/auth-helpers-sveltekit
```
Add the code below to your `src/hooks.server.ts` to initialize the client on the server:
```ts title=src/hooks.server.ts
// src/hooks.server.ts
import {
PUBLIC_SUPABASE_URL,
PUBLIC_SUPABASE_ANON_KEY
} from '$env/static/public';
import { createSupabaseServerClient } from '@supabase/auth-helpers-sveltekit';
import type { Handle } from '@sveltejs/kit';
export const handle: Handle = async ({ event, resolve }) => {
event.locals.supabase = createSupabaseServerClient({
supabaseUrl: PUBLIC_SUPABASE_URL,
supabaseKey: PUBLIC_SUPABASE_ANON_KEY,
event
});
/**
* a little helper that is written for convenience so that instead
* of calling `const { data: { session } } = await supabase.auth.getSession()`
* you just call this `await getSession()`
*/
event.locals.getSession = async () => {
const {
data: { session }
} = await event.locals.supabase.auth.getSession();
return session;
};
return resolve(event, {
/**
* There´s an issue with `filterSerializedResponseHeaders` not working when using `sequence`
*
* https://github.com/sveltejs/kit/issues/8061
*/
filterSerializedResponseHeaders(name) {
return name === 'content-range';
}
});
};
```
If you are using TypeScript the compiler might complain about `event.locals.supabase` and `event.locals.getSession`, this can be fixed by updating your `src/app.d.ts` with the content below:
```ts title=src/app.d.ts
// src/app.d.ts
import { SupabaseClient, Session } from '@supabase/supabase-js';
import { Database } from './DatabaseDefinitions';
declare global {
namespace App {
interface Locals {
supabase: SupabaseClient<Database>;
getSession(): Promise<Session | null>;
}
interface PageData {
session: Session | null;
}
// interface Error {}
// interface Platform {}
}
}
```
Update your `src/routes/+layout.svelte`:
```html title=src/routes/+layout.svelte
```svelte title=src/routes/+layout.svelte
<!-- src/routes/+layout.svelte -->
<script lang="ts">
import { supabase } from '$lib/supabaseClient'
import { invalidate } from '$app/navigation'
import { onMount } from 'svelte'
import './styles.css'
onMount(() => {
const {
data: { subscription },
} = supabase.auth.onAuthStateChange(() => {
invalidate('supabase:auth')
})
return () => {
subscription.unsubscribe()
}
})
import '../styles.css';
</script>
<svelte:head>
<title>User Management</title>
</svelte:head>
<div class="container" style="padding: 50px 0 100px 0">
<slot />
<slot />
</div>
```
Create a new `src/routes/+layout.ts` file to handle the session on the client-side.
Create a new `src/routes/+layout.ts` file to handle the session and the supabase object on the client-side.
```ts title=src/routes/+layout.ts
import type { LayoutLoad } from './$types'
import { getSupabase } from '@supabase/auth-helpers-sveltekit'
// src/routes/+layout.ts
import { invalidate } from '$app/navigation';
import {
PUBLIC_SUPABASE_ANON_KEY,
PUBLIC_SUPABASE_URL
} from '$env/static/public';
import { createSupabaseLoadClient } from '@supabase/auth-helpers-sveltekit';
import type { LayoutLoad } from './$types';
import type { Database } from '../DatabaseDefinitions';
export const load: LayoutLoad = async (event) => {
const { session } = await getSupabase(event)
return { session }
}
export const load: LayoutLoad = async ({ fetch, data, depends }) => {
depends('supabase:auth');
const supabase = createSupabaseLoadClient<Database>({
supabaseUrl: PUBLIC_SUPABASE_URL,
supabaseKey: PUBLIC_SUPABASE_ANON_KEY,
event: { fetch },
serverSession: data.session,
onAuthStateChange() {
invalidate('supabase:auth');
}
});
const {
data: { session }
} = await supabase.auth.getSession();
return { supabase, session };
};
```
Create a new `src/routes/+layout.server.ts` file to handle the session on the server-side.
```ts title=src/routes/+layout.server.ts
import type { LayoutServerLoad } from './$types'
import { getServerSession } from '@supabase/auth-helpers-sveltekit'
// src/routes/+layout.server.ts
import type { LayoutServerLoad } from './$types';
export const load: LayoutServerLoad = async (event) => {
export const load: LayoutServerLoad = async ({ locals: { getSession } }) => {
return {
session: await getServerSession(event),
}
}
session: getSession()
};
};
```
Be sure to create `src/hooks.client.ts` and `src/hooks.server.ts` in order to get the auth helper started on the client and server-side.
### Set up a Login page
```ts title=src/hooks.client.ts
import '$lib/supabaseClient'
#### Supabase Auth UI
We can use the [Supabase Auth UI](/docs/guides/auth/auth-helpers/auth-ui) a pre-built Svelte component for authenticating users via OAuth, email, and magic links.
Install the Supabase Auth UI for Svelte
```bash
npm install @supabase/auth-ui-svelte @supabase/auth-ui-shared
```
```ts title=src/hooks.server.ts
import '$lib/supabaseClient'
```
Add the `Auth` component to your home page
### Set up a Login component
Let's set up a Svelte component to manage logins and sign ups. We'll use Magic Links, so users can sign in with their email without using passwords.
Create a new `Auth.svelte` component in the `src/routes` directory to handle this functionality.
```html title=src/routes/Auth.svelte
```svelte title=src/routes/+page.svelte
<!-- src/routes/+page.svelte -->
<script lang="ts">
import { supabase } from '$lib/supabaseClient'
import { Auth } from '@supabase/auth-ui-svelte';
import { ThemeSupa } from '@supabase/auth-ui-shared';
import type { PageData } from './$types';
let loading = false
let email: string
const handleLogin = async () => {
try {
loading = true
const { error } = await supabase.auth.signInWithOtp({ email })
if (error) throw error
alert('Check your email for the login link!')
} catch (error) {
if (error instanceof Error) {
alert(error.message)
}
} finally {
loading = false
}
}
export let data: PageData;
</script>
<form class="row flex-center flex" on:submit|preventDefault="{handleLogin}">
<div class="col-6 form-widget">
<h1 class="header">Supabase + SvelteKit</h1>
<p class="description">Sign in via magic link with your email below</p>
<div>
<input class="inputField" type="email" placeholder="Your email" bind:value="{email}" />
</div>
<div>
<input type="submit" class="button block" value={loading ? 'Loading' : 'Send magic link'}
disabled={loading} />
</div>
</div>
</form>
<svelte:head>
<title>User Management</title>
</svelte:head>
<div class="row flex-center flex">
<div class="col-6 form-widget">
<Auth
supabaseClient={data.supabase}
view="magic_link"
redirectTo={`${data.url}/logging-in?redirect=/`}
showLinks={false}
appearance={{ theme: ThemeSupa, style: { input: 'color: #fff' } }}
/>
</div>
</div>
```
### Account component
Create a `src/routes/+page.server.ts` file that will return our website url to be used in our `redirectTo` above.
> This is necessary because the current supabase auth flow uses implicit grant which returns the tokens as part of the url fragment (#),
the redirect to a non server protected page will make sure that the client captures the url fragment as we cannot read these on the server
and send it over to the server. This all happens behind the scenes and isn't something you will need to do manually.
```ts
// src/routes/+page.server.ts
import { redirect } from '@sveltejs/kit';
import type { PageServerLoad } from './$types';
export const load: PageServerLoad = async ({ parent, url }) => {
const { session } = await parent();
// if the user is already logged in return them to the account page
if (session) {
throw redirect(303, '/account');
}
return { url: url.origin };
};
```
### Loading page
Create a `src/routes/logging-in/+page.svelte` file with the code below.
This page is used for waiting while your client-side code inside of `src/routes/+layout.ts` sends the tokens over to your server-side code to store it in a cookie.
```svelte
<!-- src/routes/logging-in/+page.svelte -->
<script lang="ts">
import { browser } from '$app/environment';
import { goto } from '$app/navigation';
import { page } from '$app/stores';
import type { PageData } from './$types';
export let data: PageData;
$: {
const redirectTo = $page.url.searchParams.get('redirect');
// check if user has been set in session store then redirect
if (browser && data.session) {
goto(redirectTo ?? '/account');
}
}
</script>
<section>
"Because as we know, there are known knowns; there are things we know we know. We also know there
are known unknowns; that is to say we know there are some things we do not know. But there are
also unknown unknowns—the ones we don't know we don't know" - Donald Rumsfeld
</section>
```
### Account page
After a user is signed in, they need to be able to edit their profile details and manage their account.
Create a new `Account.svelte` component in the `src/routes` directory to handle this functionality.
Create a new `src/routes/account/+page.svelte` file with the content below.
```html title=src/routes/Account.svelte
```svelte title=src/routes/account/+page.svelte
<!-- src/routes/account/+page.svelte -->
<script lang="ts">
import { onMount } from 'svelte'
import type { AuthSession } from '@supabase/supabase-js'
import { supabase } from '$lib/supabaseClient'
import { enhance } from '$app/forms';
import type { ActionData, PageData } from './$types';
export let session: AuthSession
export let data: PageData;
export let form: ActionData;
let loading = false
let username: string | null = null
let website: string | null = null
let avatarUrl: string | null = null
let { session, profile } = data;
onMount(() => {
getProfile()
})
let profileForm: any;
let loading = false;
let fullName: string | null = profile?.full_name;
let username: string | null = profile?.username;
let website: string | null = profile?.website;
let avatarUrl: string | null = profile?.avatar_url;
const getProfile = async () => {
try {
loading = true
const { user } = session
const { data, error, status } = await supabase
.from('profiles')
.select(`username, website, avatar_url`)
.eq('id', user.id)
.single()
if (data) {
username = data.username
website = data.website
avatarUrl = data.avatar_url
}
if (error && status !== 406) throw error
} catch (error) {
if (error instanceof Error) {
alert(error.message)
}
} finally {
loading = false
}
}
async function updateProfile() {
try {
loading = true
const { user } = session
const updates = {
id: user.id,
username,
website,
avatar_url: avatarUrl,
updated_at: new Date(),
}
let { error } = await supabase.from('profiles').upsert(updates)
if (error) throw error
} catch (error) {
if (error instanceof Error) {
alert(error.message)
}
} finally {
loading = false
}
}
async function signOut() {
try {
loading = true
let { error } = await supabase.auth.signOut()
if (error) throw error
} catch (error) {
if (error instanceof Error) {
alert(error.message)
}
} finally {
loading = false
}
}
function handleSubmit() {
loading = true;
return async () => {
loading = false;
};
}
</script>
<form class="form-widget" on:submit|preventDefault="{updateProfile}">
<div>
<label for="email">Email</label>
<input id="email" type="text" value="{session.user.email}" disabled />
</div>
<div>
<label for="username">Name</label>
<input id="username" type="text" bind:value="{username}" />
</div>
<div>
<label for="website">Website</label>
<input id="website" type="website" bind:value="{website}" />
</div>
<div class="form-widget">
<form
class="form-widget"
method="post"
action="?/update"
use:enhance={handleSubmit}
bind:this={profileForm}
>
<div>
<label for="email">Email</label>
<input id="email" type="text" value={session.user.email} disabled />
</div>
<div>
<input type="submit" class="button block primary" value={loading ? 'Loading...' : 'Update'}
disabled={loading} />
</div>
<div>
<label for="fullName">Full Name</label>
<input id="fullName" name="fullName" type="text" value={form?.fullName ?? fullName} />
</div>
<div>
<button class="button block" on:click="{signOut}" disabled="{loading}">Sign Out</button>
</div>
</form>
<div>
<label for="username">Username</label>
<input id="username" name="username" type="text" value={form?.username ?? username} />
</div>
<div>
<label for="website">Website</label>
<input id="website" name="website" type="website" value={form?.website ?? website} />
</div>
<div>
<input
type="submit"
class="button block primary"
value={loading ? 'Loading...' : 'Update'}
disabled={loading}
/>
</div>
</form>
<form method="post" action="?/signout" use:enhance={handleSubmit}>
<div>
<button class="button block" disabled={loading}>Sign Out</button>
</div>
</form>
</div>
```
Now create the associated `src/routes/account/+page.server.ts` file that will handle loading our data from the server through the `load` function
and handle all our form actions through the `actions` object.
```ts
import { fail, redirect } from '@sveltejs/kit';
import type { Actions, PageServerLoad } from './$types';
export const load = (async ({ locals: { supabase, getSession } }) => {
const session = await getSession();
if (!session) {
throw redirect(303, '/');
}
const { data: profile } = await supabase
.from('profiles')
.select(`username, full_name, website, avatar_url`)
.eq('id', session.user.id)
.single();
return { session, profile };
}) satisfies PageServerLoad;
export const actions = {
update: async ({ request, locals: { supabase, getSession } }) => {
const formData = await request.formData();
const fullName = formData.get('fullName') as string;
const username = formData.get('username') as string;
const website = formData.get('website') as string;
const avatarUrl = formData.get('avatarUrl') as string;
const session = await getSession();
const { error } = await supabase.from('profiles').upsert({
id: session?.user.id,
full_name: fullName,
username,
website,
avatar_url: avatarUrl,
updated_at: new Date()
});
if (error) {
return fail(500, {
fullName,
username,
website,
avatarUrl
});
}
return {
fullName,
username,
website,
avatarUrl
};
},
signout: async ({ locals: { supabase, getSession } }) => {
const session = await getSession();
if (session) {
await supabase.auth.signOut();
throw redirect(303, '/');
}
}
} satisfies Actions;
```
### Launch!
Now that we have all the components in place, let's update `src/routes/+page.svelte`:
```html title=src/routes/+page.svelte
<script>
import { page } from '$app/stores'
import Account from './Account.svelte'
import Auth from './Auth.svelte'
</script>
<svelte:head>
<title>Supabase + SvelteKit</title>
<meta name="description" content="SvelteKit using supabase-js v2" />
</svelte:head>
{#if !$page.data.session}
<Auth />
{:else}
<Account session="{$page.data.session}" />
{/if}
```
Once that's done, run this in a terminal window:
Now that we have all the pages in place, run this in a terminal window:
```bash
npm run dev
@@ -327,91 +458,99 @@ Every Supabase project is configured with [Storage](/docs/guides/storage) for ma
### Create an upload widget
Let's create an avatar for the user so that they can upload a profile photo. We can start by creating a new component called `Avatar.svelte` in the `src/routes` directory:
Let's create an avatar for the user so that they can upload a profile photo. We can start by creating a new component called `Avatar.svelte` in the `src/routes/account` directory:
```html title=src/routes/Avatar.svelte
```svelte title=src/routes/account/Avatar.svelte
<!-- src/routes/account/Avatar.svelte -->
<script lang="ts">
import { createEventDispatcher } from 'svelte'
import { supabase } from '$lib/supabaseClient'
import type { SupabaseClient } from '@supabase/supabase-js';
import { createEventDispatcher } from 'svelte';
export let size = 10
export let url: string
export let size = 10;
export let url: string;
export let supabase: SupabaseClient;
let avatarUrl: string | null = null
let uploading = false
let files: FileList
let avatarUrl: string | null = null;
let uploading = false;
let files: FileList;
const dispatch = createEventDispatcher()
const dispatch = createEventDispatcher();
const downloadImage = async (path: string) => {
try {
const { data, error } = await supabase.storage.from('avatars').download(path)
const downloadImage = async (path: string) => {
try {
const { data, error } = await supabase.storage.from('avatars').download(path);
if (error) {
throw error
}
if (error) {
throw error;
}
const url = URL.createObjectURL(data)
avatarUrl = url
} catch (error) {
if (error instanceof Error) {
console.log('Error downloading image: ', error.message)
}
}
}
const url = URL.createObjectURL(data);
avatarUrl = url;
} catch (error) {
if (error instanceof Error) {
console.log('Error downloading image: ', error.message);
}
}
};
const uploadAvatar = async () => {
try {
uploading = true
const uploadAvatar = async () => {
try {
uploading = true;
if (!files || files.length === 0) {
throw new Error('You must select an image to upload.')
}
if (!files || files.length === 0) {
throw new Error('You must select an image to upload.');
}
const file = files[0]
const fileExt = file.name.split('.').pop()
const filePath = `${Math.random()}.${fileExt}`
const file = files[0];
const fileExt = file.name.split('.').pop();
url = `${Math.random()}.${fileExt}`;
let { error } = await supabase.storage.from('avatars').upload(filePath, file)
let { error } = await supabase.storage.from('avatars').upload(url, file);
if (error) {
throw error
}
if (error) {
throw error;
}
url = filePath
dispatch('upload')
} catch (error) {
if (error instanceof Error) {
alert(error.message)
}
} finally {
uploading = false
}
}
dispatch('upload');
} catch (error) {
if (error instanceof Error) {
alert(error.message);
}
} finally {
uploading = false;
}
};
$: if (url) downloadImage(url)
$: if (url) downloadImage(url);
</script>
<div>
{#if avatarUrl} <img src={avatarUrl} alt={avatarUrl ? 'Avatar' : 'No image'} class="avatar image"
style="height: {size}em; width: {size}em;" /> {:else}
<div class="avatar no-image" style="height: {size}em; width: {size}em;" />
{/if}
{#if avatarUrl}
<img
src={avatarUrl}
alt={avatarUrl ? 'Avatar' : 'No image'}
class="avatar image"
style="height: {size}em; width: {size}em;"
/>
{:else}
<div class="avatar no-image" style="height: {size}em; width: {size}em;" />
{/if}
<input type="hidden" name="avatarUrl" value={url} />
<div style="width: {size}em;">
<label class="button primary block" for="single">
{uploading ? 'Uploading ...' : 'Upload'}
</label>
<input
style="visibility: hidden; position:absolute;"
type="file"
id="single"
accept="image/*"
bind:files
on:change="{uploadAvatar}"
disabled="{uploading}"
/>
</div>
<div style="width: {size}em;">
<label class="button primary block" for="single">
{uploading ? 'Uploading ...' : 'Upload'}
</label>
<input
style="visibility: hidden; position:absolute;"
type="file"
id="single"
accept="image/*"
bind:files
on:change={uploadAvatar}
disabled={uploading}
/>
</div>
</div>
```
@@ -419,18 +558,34 @@ Let's create an avatar for the user so that they can upload a profile photo. We
And then we can add the widget to the Account page:
```html title=src/routes/Account.svelte
```svelte title=src/routes/account/+page.svelte
<!-- src/routes/account/+page.svelte -->
<script>
// Import the new component
import Avatar from './Avatar.svelte'
</script>
<form use:getProfile class="form-widget" on:submit|preventDefault="{updateProfile}">
<!-- Add to body -->
<Avatar bind:url="{avatarUrl}" size="{10}" on:upload="{updateProfile}" />
<div class="form-widget">
<form
class="form-widget"
method="post"
action="?/update"
use:enhance={handleSubmit}
bind:this={profileForm}
>
<!-- Add to body -->
<Avatar
{supabase}
bind:url={avatarUrl}
size={10}
on:upload={() => {
profileForm.requestSubmit();
}}
/>
<!-- Other form elements -->
</form>
<!-- Other form elements -->
</form>
</div>
```
### Storage management

1
package-lock.json generated
View File

@@ -5,6 +5,7 @@
"requires": true,
"packages": {
"": {
"name": "supabase",
"version": "0.0.0",
"license": "Apache-2.0",
"workspaces": [