diff --git a/apps/docs/pages/guides/getting-started/tutorials/with-sveltekit.mdx b/apps/docs/pages/guides/getting-started/tutorials/with-sveltekit.mdx index 1daf9a881e..b6aa1f7a35 100644 --- a/apps/docs/pages/guides/getting-started/tutorials/with-sveltekit.mdx +++ b/apps/docs/pages/guides/getting-started/tutorials/with-sveltekit.mdx @@ -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; + getSession(): Promise; + } + 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 + + + User Management + +
- +
``` -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({ + 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 + -
-
-

Supabase + SvelteKit

-

Sign in via magic link with your email below

-
- -
-
- -
-
-
+ + User Management + + +
+
+ +
+
``` -### 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 + + + +
+ "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 +
+``` + +### 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 + -
-
- - -
-
- - -
-
- - -
+
+ +
+ + +
-
- -
+
+ + +
-
- -
- +
+ + +
+ +
+ + +
+ +
+ +
+ + +
+
+ +
+
+
+``` + +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 - - - - Supabase + SvelteKit - - - -{#if !$page.data.session} - -{:else} - -{/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 +
- {#if avatarUrl} {avatarUrl {:else} -
- {/if} + {#if avatarUrl} + {avatarUrl + {:else} +
+ {/if} + -
- - -
+
+ + +
``` @@ -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 + -
- - +
+ + + { + profileForm.requestSubmit(); + }} + /> - - + + +
``` ### Storage management diff --git a/package-lock.json b/package-lock.json index afbf9e4575..b0bcd07d8d 100644 --- a/package-lock.json +++ b/package-lock.json @@ -5,6 +5,7 @@ "requires": true, "packages": { "": { + "name": "supabase", "version": "0.0.0", "license": "Apache-2.0", "workspaces": [