mirror of
https://github.com/supabase/supabase.git
synced 2026-10-05 09:25:06 +03:00
fix(docs): update SvelteKit tutorial to use Svelte 5 syntax (#31229)
* docs: Update SvelteKit auth tutorial project creation syntax (#31114) * docs: Update link to SvelteKit docs (#31114) * Update +layout.svelte to Svelte 5 syntax (#31114) * Update +page.svelte to use Svelte 5 syntax (#31114) * Update account +page.svelte to use Svelte 5 syntax (#31114) * Update account Avatar.svelte to use Svelte 5 syntax (#31114) * Update SvelteKit example to Svelte 5 syntax (#31114) * docs: Update npm dependencies (#31114) * Start review * Finish review * Update code for SvelteKit 5 * Convert to codesamples --------- Co-authored-by: Chris Chinchilla <chris@chrischinchilla.com> Co-authored-by: Chris Chinchilla <chris.ward@supabase.io>
This commit is contained in:
9 files changed
+917
-1593
No files matched your search
@@ -17,14 +17,14 @@ If you get stuck while working through this guide, refer to the [full example on
|
||||
|
||||
## Building the app
|
||||
|
||||
Let's start building the Svelte app from scratch.
|
||||
Start building the Svelte app from scratch.
|
||||
|
||||
### Initialize a Svelte app
|
||||
|
||||
We can use the [SvelteKit Skeleton Project](https://kit.svelte.dev/docs) to initialize an app called `supabase-sveltekit` (for this tutorial we will be using TypeScript):
|
||||
Use the [SvelteKit Skeleton Project](https://svelte.dev/docs/kit) to initialize an app called `supabase-sveltekit` (for this tutorial, select "SvelteKit minimal" and use TypeScript):
|
||||
|
||||
```bash
|
||||
npm create svelte@latest supabase-sveltekit
|
||||
npx sv create supabase-sveltekit
|
||||
cd supabase-sveltekit
|
||||
npm install
|
||||
```
|
||||
@@ -35,8 +35,8 @@ Then install the Supabase client library: [supabase-js](https://github.com/supab
|
||||
npm install @supabase/supabase-js
|
||||
```
|
||||
|
||||
And finally we want to save the environment variables in a `.env`.
|
||||
All we need are the `SUPABASE_URL` and the `SUPABASE_KEY` key that you copied [earlier](#get-the-api-keys).
|
||||
And finally, save the environment variables in a `.env` file.
|
||||
All you need are the `PUBLIC_SUPABASE_URL` and the `PUBLIC_SUPABASE_ANON_KEY` key that you copied [earlier](#get-the-api-keys).
|
||||
|
||||
<$CodeTabs>
|
||||
|
||||
@@ -47,108 +47,46 @@ PUBLIC_SUPABASE_ANON_KEY="YOUR_SUPABASE_KEY"
|
||||
|
||||
</$CodeTabs>
|
||||
|
||||
Optionally, add `src/styles.css` with the [CSS from the example](https://raw.githubusercontent.com/supabase/supabase/master/examples/user-management/sveltekit-user-management/src/styles.css).
|
||||
### App styling (optional)
|
||||
|
||||
An optional step is to update the CSS file `src/styles.css` to make the app look nice.
|
||||
You can find the full contents of this file [in the example repository](https://raw.githubusercontent.com/supabase/supabase/master/examples/user-management/sveltekit-user-management/src/styles.css).
|
||||
|
||||
### Creating a Supabase client for SSR
|
||||
|
||||
The `ssr` package configures Supabase to use Cookies, which is required for server-side languages and frameworks.
|
||||
The `ssr` package configures Supabase to use Cookies, which are required for server-side languages and frameworks.
|
||||
|
||||
Install the Supabase packages:
|
||||
Install the SSR package:
|
||||
|
||||
```bash
|
||||
npm install @supabase/ssr @supabase/supabase-js
|
||||
npm install @supabase/ssr
|
||||
```
|
||||
|
||||
Creating a Supabase client with the `ssr` package automatically configures it to use Cookies. This means your user's session is available throughout the entire SvelteKit stack - page, layout, server, hooks.
|
||||
Creating a Supabase client with the `ssr` package automatically configures it to use Cookies. This means the user's session is available throughout the entire SvelteKit stack - page, layout, server, and hooks.
|
||||
|
||||
Add the code below to your `src/hooks.server.ts` to initialize the client on the server:
|
||||
Add the code below to a `src/hooks.server.ts` file to initialize the client on the server:
|
||||
|
||||
<$CodeTabs>
|
||||
|
||||
```ts name=src/hooks.server.ts
|
||||
// src/hooks.server.ts
|
||||
import { PUBLIC_SUPABASE_URL, PUBLIC_SUPABASE_ANON_KEY } from '$env/static/public'
|
||||
import { createServerClient } from '@supabase/ssr'
|
||||
import type { Handle } from '@sveltejs/kit'
|
||||
|
||||
export const handle: Handle = async ({ event, resolve }) => {
|
||||
event.locals.supabase = createServerClient(PUBLIC_SUPABASE_URL, PUBLIC_SUPABASE_ANON_KEY, {
|
||||
cookies: {
|
||||
getAll: () => event.cookies.getAll(),
|
||||
/**
|
||||
* SvelteKit's cookies API requires `path` to be explicitly set in
|
||||
* the cookie options. Setting `path` to `/` replicates previous/
|
||||
* standard behavior.
|
||||
*/
|
||||
setAll: (cookiesToSet) => {
|
||||
cookiesToSet.forEach(({ name, value, options }) => {
|
||||
event.cookies.set(name, value, { ...options, path: '/' })
|
||||
})
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
/**
|
||||
* Unlike `supabase.auth.getSession()`, which returns the session _without_
|
||||
* validating the JWT, this function also calls `getUser()` to validate the
|
||||
* JWT before returning the session.
|
||||
*/
|
||||
event.locals.safeGetSession = async () => {
|
||||
const {
|
||||
data: { session },
|
||||
} = await event.locals.supabase.auth.getSession()
|
||||
if (!session) {
|
||||
return { session: null, user: null }
|
||||
}
|
||||
|
||||
const {
|
||||
data: { user },
|
||||
error,
|
||||
} = await event.locals.supabase.auth.getUser()
|
||||
if (error) {
|
||||
// JWT validation has failed
|
||||
return { session: null, user: null }
|
||||
}
|
||||
|
||||
return { session, user }
|
||||
}
|
||||
|
||||
return resolve(event, {
|
||||
filterSerializedResponseHeaders(name) {
|
||||
return name === 'content-range' || name === 'x-supabase-api-version'
|
||||
},
|
||||
})
|
||||
}
|
||||
```
|
||||
<$CodeSample
|
||||
path="/user-management/sveltekit-user-management/src/hooks.server.ts"
|
||||
lines={[[1, -1]]}
|
||||
meta="name=src/hooks.server.ts"
|
||||
/>
|
||||
|
||||
</$CodeTabs>
|
||||
|
||||
<$Partial path="get_session_warning.mdx" />
|
||||
|
||||
If you are using TypeScript the compiler might complain about `event.locals.supabase` and `event.locals.safeGetSession`, this can be fixed by updating your `src/app.d.ts` with the content below:
|
||||
{/* TODO: Change when adding JS autoconversion */}
|
||||
As this tutorial uses TypeScript the compiler complains about `event.locals.supabase` and `event.locals.safeGetSession`, you can fix this by updating the `src/app.d.ts` with the content below:
|
||||
|
||||
<$CodeTabs>
|
||||
|
||||
```ts name=src/app.d.ts
|
||||
// src/app.d.ts
|
||||
|
||||
import { SupabaseClient, Session } from '@supabase/supabase-js'
|
||||
|
||||
declare global {
|
||||
namespace App {
|
||||
interface Locals {
|
||||
supabase: SupabaseClient
|
||||
safeGetSession(): Promise<{ session: Session | null; user: User | null }>
|
||||
}
|
||||
interface PageData {
|
||||
session: Session | null
|
||||
user: User | null
|
||||
}
|
||||
// interface Error {}
|
||||
// interface Platform {}
|
||||
}
|
||||
}
|
||||
```
|
||||
<$CodeSample
|
||||
path="/user-management/sveltekit-user-management/src/app.d.ts"
|
||||
lines={[[1, -1]]}
|
||||
meta="name=src/app.d.ts"
|
||||
/>
|
||||
|
||||
</$CodeTabs>
|
||||
|
||||
@@ -156,26 +94,17 @@ Create a new `src/routes/+layout.server.ts` file to handle the session on the se
|
||||
|
||||
<$CodeTabs>
|
||||
|
||||
```ts name=src/routes/+layout.server.ts
|
||||
// src/routes/+layout.server.ts
|
||||
import type { LayoutServerLoad } from './$types'
|
||||
|
||||
export const load: LayoutServerLoad = async ({ locals: { safeGetSession }, cookies }) => {
|
||||
const { session, user } = await safeGetSession()
|
||||
|
||||
return {
|
||||
session,
|
||||
user,
|
||||
cookies: cookies.getAll(),
|
||||
}
|
||||
}
|
||||
```
|
||||
<$CodeSample
|
||||
path="/user-management/sveltekit-user-management/src/routes/+layout.server.ts"
|
||||
lines={[[1, -1]]}
|
||||
meta="name=src/routes/+layout.server.ts"
|
||||
/>
|
||||
|
||||
</$CodeTabs>
|
||||
|
||||
<Admonition type="tip">
|
||||
|
||||
Start your dev server (`npm run dev`) in order to generate the `./$types` files we are referencing in our project.
|
||||
Start the dev server (`npm run dev`) to generate the `./$types` files we are referencing in our project.
|
||||
|
||||
</Admonition>
|
||||
|
||||
@@ -183,202 +112,49 @@ Create a new `src/routes/+layout.ts` file to handle the session and the `supabas
|
||||
|
||||
<$CodeTabs>
|
||||
|
||||
```ts name=src/routes/+layout.ts
|
||||
// src/routes/+layout.ts
|
||||
import { createBrowserClient, createServerClient, isBrowser } from '@supabase/ssr'
|
||||
import { PUBLIC_SUPABASE_ANON_KEY, PUBLIC_SUPABASE_URL } from '$env/static/public'
|
||||
import type { LayoutLoad } from './$types'
|
||||
|
||||
export const load: LayoutLoad = async ({ fetch, data, depends }) => {
|
||||
depends('supabase:auth')
|
||||
|
||||
const supabase = isBrowser()
|
||||
? createBrowserClient(PUBLIC_SUPABASE_URL, PUBLIC_SUPABASE_ANON_KEY, {
|
||||
global: {
|
||||
fetch,
|
||||
},
|
||||
})
|
||||
: createServerClient(PUBLIC_SUPABASE_URL, PUBLIC_SUPABASE_ANON_KEY, {
|
||||
global: {
|
||||
fetch,
|
||||
},
|
||||
cookies: {
|
||||
getAll() {
|
||||
return data.cookies
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
/**
|
||||
* It's fine to use `getSession` here, because on the client, `getSession` is
|
||||
* safe, and on the server, it reads `session` from the `LayoutData`, which
|
||||
* safely checked the session using `safeGetSession`.
|
||||
*/
|
||||
const {
|
||||
data: { session },
|
||||
} = await supabase.auth.getSession()
|
||||
|
||||
return { supabase, session }
|
||||
}
|
||||
```
|
||||
<$CodeSample
|
||||
path="/user-management/sveltekit-user-management/src/routes/+layout.ts"
|
||||
lines={[[1, -1]]}
|
||||
meta="name=src/routes/+layout.ts"
|
||||
/>
|
||||
|
||||
</$CodeTabs>
|
||||
|
||||
Update your `src/routes/+layout.svelte`:
|
||||
Create `src/routes/+layout.svelte`:
|
||||
|
||||
<$CodeTabs>
|
||||
|
||||
```svelte name=src/routes/+layout.svelte
|
||||
<!-- src/routes/+layout.svelte -->
|
||||
<script lang="ts">
|
||||
import '../styles.css'
|
||||
import { invalidate } from '$app/navigation'
|
||||
import { onMount } from 'svelte'
|
||||
|
||||
export let data
|
||||
|
||||
let { supabase, session } = data
|
||||
$: ({ supabase, session } = data)
|
||||
|
||||
onMount(() => {
|
||||
const { data } = supabase.auth.onAuthStateChange((event, newSession) => {
|
||||
if (newSession?.expires_at !== session?.expires_at) {
|
||||
invalidate('supabase:auth')
|
||||
}
|
||||
})
|
||||
|
||||
return () => data.subscription.unsubscribe()
|
||||
})
|
||||
</script>
|
||||
|
||||
<svelte:head>
|
||||
<title>User Management</title>
|
||||
</svelte:head>
|
||||
|
||||
<div class="container" style="padding: 50px 0 100px 0">
|
||||
<slot />
|
||||
</div>
|
||||
```
|
||||
<$CodeSample
|
||||
path="/user-management/sveltekit-user-management/src/routes/+layout.svelte"
|
||||
lines={[[1, -1]]}
|
||||
meta="name=src/routes/+layout.svelte"
|
||||
/>
|
||||
|
||||
</$CodeTabs>
|
||||
|
||||
## Set up a login page
|
||||
|
||||
Create a magic link login/signup page for your application:
|
||||
Create a magic link login/signup page for your application by updating the `routes/+page.svelte` file:
|
||||
|
||||
<$CodeTabs>
|
||||
|
||||
```svelte name=src/routes/+page.svelte
|
||||
<!-- src/routes/+page.svelte -->
|
||||
<script lang="ts">
|
||||
import { enhance } from '$app/forms'
|
||||
import type { ActionData, SubmitFunction } from './$types.js'
|
||||
|
||||
export let form: ActionData;
|
||||
|
||||
let loading = false
|
||||
|
||||
const handleSubmit: SubmitFunction = () => {
|
||||
loading = true
|
||||
return async ({ update }) => {
|
||||
update()
|
||||
loading = false
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<svelte:head>
|
||||
<title>User Management</title>
|
||||
</svelte:head>
|
||||
|
||||
<form class="row flex flex-center" method="POST" use:enhance={handleSubmit}>
|
||||
<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>
|
||||
{#if form?.message !== undefined}
|
||||
<div class="success {form?.success ? '' : 'fail'}">
|
||||
{form?.message}
|
||||
</div>
|
||||
{/if}
|
||||
<div>
|
||||
<label for="email">Email address</label>
|
||||
<input
|
||||
id="email"
|
||||
name="email"
|
||||
class="inputField"
|
||||
type="email"
|
||||
placeholder="Your email"
|
||||
value={form?.email ?? ''}
|
||||
/>
|
||||
</div>
|
||||
{#if form?.errors?.email}
|
||||
<span class="flex items-center text-sm error">
|
||||
{form?.errors?.email}
|
||||
</span>
|
||||
{/if}
|
||||
<div>
|
||||
<button class="button primary block">
|
||||
{ loading ? 'Loading' : 'Send magic link' }
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
```
|
||||
<$CodeSample
|
||||
path="/user-management/sveltekit-user-management/src/routes/+page.svelte"
|
||||
lines={[[1, -1]]}
|
||||
meta="name=src/routes/+page.svelte"
|
||||
/>
|
||||
|
||||
</$CodeTabs>
|
||||
|
||||
Create a `src/routes/+page.server.ts` file that will handle our magic link form when submitted.
|
||||
Create a `src/routes/+page.server.ts` file that handles the magic link form when submitted.
|
||||
|
||||
<$CodeTabs>
|
||||
|
||||
```ts name=src/routes/+page.server.ts
|
||||
// src/routes/+page.server.ts
|
||||
import { fail, redirect } from '@sveltejs/kit'
|
||||
import type { Actions, PageServerLoad } from './$types'
|
||||
|
||||
export const load: PageServerLoad = async ({ url, locals: { safeGetSession } }) => {
|
||||
const { session } = await safeGetSession()
|
||||
|
||||
// if the user is already logged in return them to the account page
|
||||
if (session) {
|
||||
redirect(303, '/account')
|
||||
}
|
||||
|
||||
return { url: url.origin }
|
||||
}
|
||||
|
||||
export const actions: Actions = {
|
||||
default: async (event) => {
|
||||
const {
|
||||
url,
|
||||
request,
|
||||
locals: { supabase },
|
||||
} = event
|
||||
const formData = await request.formData()
|
||||
const email = formData.get('email') as string
|
||||
const validEmail = /^[\w-\.+]+@([\w-]+\.)+[\w-]{2,8}$/.test(email)
|
||||
|
||||
if (!validEmail) {
|
||||
return fail(400, { errors: { email: 'Please enter a valid email address' }, email })
|
||||
}
|
||||
|
||||
const { error } = await supabase.auth.signInWithOtp({ email })
|
||||
|
||||
if (error) {
|
||||
return fail(400, {
|
||||
success: false,
|
||||
email,
|
||||
message: `There was an issue, Please contact support.`,
|
||||
})
|
||||
}
|
||||
|
||||
return {
|
||||
success: true,
|
||||
message: 'Please check your email for a magic link to log into the website.',
|
||||
}
|
||||
},
|
||||
}
|
||||
```
|
||||
<$CodeSample
|
||||
path="/user-management/sveltekit-user-management/src/routes/+page.server.ts"
|
||||
lines={[[1, -1]]}
|
||||
meta="name=src/routes/+page.server.ts"
|
||||
/>
|
||||
|
||||
</$CodeTabs>
|
||||
|
||||
@@ -388,239 +164,77 @@ Change the email template to support a server-side authentication flow.
|
||||
|
||||
Before we proceed, let's change the email template to support sending a token hash:
|
||||
|
||||
- Go to the [Auth templates](/dashboard/project/_/auth/templates) page in your dashboard.
|
||||
- Select `Confirm signup` template.
|
||||
- Go to the [**Auth** > **Emails**](/dashboard/project/_/auth/templates) page in the project dashboard.
|
||||
- Select the **Confirm signup** template.
|
||||
- Change `{{ .ConfirmationURL }}` to `{{ .SiteURL }}/auth/confirm?token_hash={{ .TokenHash }}&type=email`.
|
||||
- Repeat the previous step for `Magic link` template.
|
||||
- Repeat the previous step for **Magic link** template.
|
||||
|
||||
<Admonition type="tip">
|
||||
|
||||
Did you know? You could also customize emails sent out to new users, including the email's looks, content, and query parameters. Check out the [settings of your project](/dashboard/project/_/auth/templates).
|
||||
**Did you know?** You can also customize emails sent out to new users, including the email's looks, content, and query parameters. Check out the [settings of your project](/dashboard/project/_/auth/templates).
|
||||
|
||||
</Admonition>
|
||||
|
||||
### Confirmation endpoint
|
||||
|
||||
As we are working in a server-side rendering (SSR) environment, it is necessary to create a server endpoint responsible for exchanging the `token_hash` for a session.
|
||||
As this is a server-side rendering (SSR) environment, you need to create a server endpoint responsible for exchanging the `token_hash` for a session.
|
||||
|
||||
In the following code snippet, we perform the following steps:
|
||||
The following code snippet performs the following steps:
|
||||
|
||||
- Retrieve the `token_hash` sent back from the Supabase Auth server using the `token_hash` query parameter.
|
||||
- Exchange this `token_hash` for a session, which we store in storage (in this case, cookies).
|
||||
- Finally, the user is redirected to the `account` page or the `error` page.
|
||||
- Retrieves the `token_hash` sent back from the Supabase Auth server using the `token_hash` query parameter.
|
||||
- Exchanges this `token_hash` for a session, which you store in storage (in this case, cookies).
|
||||
- Finally, redirect the user to the `account` page or the `error` page.
|
||||
|
||||
<$CodeTabs>
|
||||
|
||||
```ts name=src/routes/auth/confirm/+server.ts
|
||||
// src/routes/auth/confirm/+server.ts
|
||||
import type { EmailOtpType } from '@supabase/supabase-js'
|
||||
import { redirect } from '@sveltejs/kit'
|
||||
|
||||
import type { RequestHandler } from './$types'
|
||||
|
||||
export const GET: RequestHandler = async ({ url, locals: { supabase } }) => {
|
||||
const token_hash = url.searchParams.get('token_hash')
|
||||
const type = url.searchParams.get('type') as EmailOtpType | null
|
||||
const next = url.searchParams.get('next') ?? '/account'
|
||||
|
||||
/**
|
||||
* Clean up the redirect URL by deleting the Auth flow parameters.
|
||||
*
|
||||
* `next` is preserved for now, because it's needed in the error case.
|
||||
*/
|
||||
const redirectTo = new URL(url)
|
||||
redirectTo.pathname = next
|
||||
redirectTo.searchParams.delete('token_hash')
|
||||
redirectTo.searchParams.delete('type')
|
||||
|
||||
if (token_hash && type) {
|
||||
const { error } = await supabase.auth.verifyOtp({ type, token_hash })
|
||||
if (!error) {
|
||||
redirectTo.searchParams.delete('next')
|
||||
redirect(303, redirectTo)
|
||||
}
|
||||
}
|
||||
|
||||
redirectTo.pathname = '/auth/error'
|
||||
redirect(303, redirectTo)
|
||||
}
|
||||
```
|
||||
<$CodeSample
|
||||
path="/user-management/sveltekit-user-management/src/routes/auth/confirm/+server.ts"
|
||||
lines={[[1, -1]]}
|
||||
meta="name=src/routes/auth/confirm/+server.ts"
|
||||
/>
|
||||
|
||||
</$CodeTabs>
|
||||
|
||||
### Authentication error page
|
||||
|
||||
If there is an error with confirming the token you will be redirect to this error page.
|
||||
If there is an error with confirming the token, redirect the user to an error page.
|
||||
|
||||
<$CodeTabs>
|
||||
|
||||
```svelte name=src/routes/auth/error/+page.svelte
|
||||
<p>Login error</p>
|
||||
```
|
||||
<$CodeSample
|
||||
path="/user-management/sveltekit-user-management/src/routes/auth/error/+page.svelte"
|
||||
lines={[[1, -1]]}
|
||||
meta="name=src/routes/auth/error/+page.svelte"
|
||||
/>
|
||||
|
||||
</$CodeTabs>
|
||||
|
||||
### Account page
|
||||
|
||||
After a user is signed in, they need to be able to edit their profile details and manage their account.
|
||||
After a user signs in, they need to be able to edit their profile details page.
|
||||
Create a new `src/routes/account/+page.svelte` file with the content below.
|
||||
|
||||
<$CodeTabs>
|
||||
|
||||
```svelte name=src/routes/account/+page.svelte
|
||||
<!-- src/routes/account/+page.svelte -->
|
||||
<script lang="ts">
|
||||
import { enhance } from '$app/forms';
|
||||
import type { SubmitFunction } from '@sveltejs/kit';
|
||||
|
||||
export let data
|
||||
export let form
|
||||
|
||||
let { session, supabase, profile } = data
|
||||
$: ({ session, supabase, profile } = data)
|
||||
|
||||
let profileForm: HTMLFormElement
|
||||
let loading = false
|
||||
let fullName: string = profile?.full_name ?? ''
|
||||
let username: string = profile?.username ?? ''
|
||||
let website: string = profile?.website ?? ''
|
||||
let avatarUrl: string = profile?.avatar_url ?? ''
|
||||
|
||||
const handleSubmit: SubmitFunction = () => {
|
||||
loading = true
|
||||
return async () => {
|
||||
loading = false
|
||||
}
|
||||
}
|
||||
|
||||
const handleSignOut: SubmitFunction = () => {
|
||||
loading = true
|
||||
return async ({ update }) => {
|
||||
loading = false
|
||||
update()
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<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>
|
||||
<label for="fullName">Full Name</label>
|
||||
<input id="fullName" name="fullName" type="text" value={form?.fullName ?? fullName} />
|
||||
</div>
|
||||
|
||||
<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="url" 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={handleSignOut}>
|
||||
<div>
|
||||
<button class="button block" disabled={loading}>Sign Out</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
```
|
||||
<$CodeSample
|
||||
path="/user-management/sveltekit-user-management/src/routes/account/+page.svelte"
|
||||
lines={[[1, 3],[6,12],[15,38],[47,-1]]}
|
||||
meta="name=src/routes/account/+page.svelte"
|
||||
/>
|
||||
|
||||
</$CodeTabs>
|
||||
|
||||
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.
|
||||
Now, create the associated `src/routes/account/+page.server.ts` file that handles loading data from the server through the `load` function
|
||||
and handle all form actions through the `actions` object.
|
||||
|
||||
```ts name=src/routes/account/+page.server.ts
|
||||
import { fail, redirect } from '@sveltejs/kit'
|
||||
import type { Actions, PageServerLoad } from './$types'
|
||||
|
||||
export const load: PageServerLoad = async ({ locals: { supabase, safeGetSession } }) => {
|
||||
const { session } = await safeGetSession()
|
||||
|
||||
if (!session) {
|
||||
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 }
|
||||
}
|
||||
|
||||
export const actions: Actions = {
|
||||
update: async ({ request, locals: { supabase, safeGetSession } }) => {
|
||||
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 safeGetSession()
|
||||
|
||||
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, safeGetSession } }) => {
|
||||
const { session } = await safeGetSession()
|
||||
if (session) {
|
||||
await supabase.auth.signOut()
|
||||
redirect(303, '/')
|
||||
}
|
||||
},
|
||||
}
|
||||
```
|
||||
<$CodeSample
|
||||
path="/user-management/sveltekit-user-management/src/routes/+page.server.ts"
|
||||
lines={[[1, -1]]}
|
||||
meta="name=src/routes/+page.server.ts"
|
||||
/>
|
||||
|
||||
### Launch!
|
||||
|
||||
Now that we have all the pages in place, run this in a terminal window:
|
||||
With all the pages in place, run this command in a terminal:
|
||||
|
||||
```bash
|
||||
npm run dev
|
||||
@@ -636,144 +250,29 @@ 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/account` directory:
|
||||
Create an avatar for the user so that they can upload a profile photo. Start by creating a new component called `Avatar.svelte` in the `src/routes/account` directory:
|
||||
|
||||
<$CodeTabs>
|
||||
|
||||
```svelte name=src/routes/account/Avatar.svelte
|
||||
<!-- src/routes/account/Avatar.svelte -->
|
||||
<script lang="ts">
|
||||
import type { SupabaseClient } from '@supabase/supabase-js'
|
||||
import { createEventDispatcher } from 'svelte'
|
||||
|
||||
export let size = 10
|
||||
export let url: string
|
||||
export let supabase: SupabaseClient
|
||||
|
||||
let avatarUrl: string | null = null
|
||||
let uploading = false
|
||||
let files: FileList
|
||||
|
||||
const dispatch = createEventDispatcher()
|
||||
|
||||
const downloadImage = async (path: string) => {
|
||||
try {
|
||||
const { data, error } = await supabase.storage.from('avatars').download(path)
|
||||
|
||||
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 uploadAvatar = async () => {
|
||||
try {
|
||||
uploading = true
|
||||
|
||||
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 { error } = await supabase.storage.from('avatars').upload(filePath, file)
|
||||
|
||||
if (error) {
|
||||
throw error
|
||||
}
|
||||
|
||||
url = filePath
|
||||
setTimeout(() => {
|
||||
dispatch('upload')
|
||||
}, 100)
|
||||
} catch (error) {
|
||||
if (error instanceof Error) {
|
||||
alert(error.message)
|
||||
}
|
||||
} finally {
|
||||
uploading = false
|
||||
}
|
||||
}
|
||||
|
||||
$: 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}
|
||||
<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>
|
||||
```
|
||||
<$CodeSample
|
||||
path="/user-management/sveltekit-user-management/src/routes/account/Avatar.svelte"
|
||||
lines={[[1, -1]]}
|
||||
meta="name=src/routes/account/Avatar.svelte"
|
||||
/>
|
||||
|
||||
</$CodeTabs>
|
||||
|
||||
### Add the new widget
|
||||
|
||||
And then we can add the widget to the Account page:
|
||||
Add the widget to the Account page:
|
||||
|
||||
<$CodeTabs>
|
||||
|
||||
```svelte name=src/routes/account/+page.svelte
|
||||
<!-- src/routes/account/+page.svelte -->
|
||||
<script lang="ts">
|
||||
// Import the new component
|
||||
import Avatar from './Avatar.svelte'
|
||||
</script>
|
||||
|
||||
<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>
|
||||
</div>
|
||||
```
|
||||
<$CodeSample
|
||||
path="/user-management/sveltekit-user-management/src/routes/account/+page.svelte"
|
||||
lines={[[1, 1], [4,4], [31,31],[39,46],[82,82]]}
|
||||
meta="name=src/routes/account/+page.svelte"
|
||||
/>
|
||||
|
||||
</$CodeTabs>
|
||||
|
||||
|
||||
+766
-948
File diff suppressed because it is too large.
Load diff
@@ -10,20 +10,20 @@
|
||||
"check:watch": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json --watch"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@sveltejs/adapter-auto": "^3.2.2",
|
||||
"@sveltejs/kit": "^2.5.18",
|
||||
"@sveltejs/vite-plugin-svelte": "^3.1.1",
|
||||
"svelte": "^4.2.18",
|
||||
"svelte-check": "^3.8.4",
|
||||
"@sveltejs/adapter-auto": "^6.0.1",
|
||||
"@sveltejs/kit": "^2.5.27",
|
||||
"@sveltejs/vite-plugin-svelte": "^6.1.0",
|
||||
"svelte": "^5.14.4",
|
||||
"svelte-check": "^4.1.1",
|
||||
"tslib": "^2.6.3",
|
||||
"typescript": "^5.5.3",
|
||||
"vite": "^5.3.3"
|
||||
"vite": "^7.0.5"
|
||||
},
|
||||
"type": "module",
|
||||
"dependencies": {
|
||||
"@supabase/auth-ui-shared": "^0.1.8",
|
||||
"@supabase/auth-ui-svelte": "^0.2.9",
|
||||
"@supabase/ssr": "^0.4.0",
|
||||
"@supabase/ssr": "^0.6.1",
|
||||
"@supabase/supabase-js": "^2.44.2"
|
||||
}
|
||||
}
|
||||
@@ -4,10 +4,8 @@
|
||||
import { invalidate } from '$app/navigation'
|
||||
import { onMount } from 'svelte'
|
||||
|
||||
export let data
|
||||
|
||||
let { supabase, session } = data
|
||||
$: ({ supabase, session } = data)
|
||||
let { data, children } = $props()
|
||||
let { supabase, session } = $derived(data)
|
||||
|
||||
onMount(() => {
|
||||
const { data } = supabase.auth.onAuthStateChange((event, _session) => {
|
||||
@@ -25,5 +23,5 @@
|
||||
</svelte:head>
|
||||
|
||||
<div class="container" style="padding: 50px 0 100px 0">
|
||||
<slot />
|
||||
{@render children()}
|
||||
</div>
|
||||
@@ -3,9 +3,12 @@
|
||||
import { enhance } from '$app/forms'
|
||||
import type { ActionData, SubmitFunction } from './$types.js'
|
||||
|
||||
export let form: ActionData;
|
||||
|
||||
let loading = false
|
||||
interface Props {
|
||||
form: ActionData
|
||||
}
|
||||
let { form }: Props = $props()
|
||||
|
||||
let loading = $state(false)
|
||||
|
||||
const handleSubmit: SubmitFunction = () => {
|
||||
loading = true
|
||||
|
||||
@@ -1,21 +1,16 @@
|
||||
<!-- src/routes/account/+page.svelte -->
|
||||
<script lang="ts">
|
||||
import { enhance } from '$app/forms';
|
||||
import type { SubmitFunction } from '@sveltejs/kit';
|
||||
import Avatar from './Avatar.svelte'
|
||||
|
||||
export let data
|
||||
export let form
|
||||
|
||||
let { session, supabase, profile } = data
|
||||
$: ({ session, supabase, profile } = data)
|
||||
|
||||
let { data, form } = $props()
|
||||
let { session, supabase, profile } = $derived(data)
|
||||
let profileForm: HTMLFormElement
|
||||
let loading = false
|
||||
let loading = $state(false)
|
||||
let fullName: string = profile?.full_name ?? ''
|
||||
let username: string = profile?.username ?? ''
|
||||
let website: string = profile?.website ?? ''
|
||||
let avatarUrl: string = profile?.avatar_url ?? ''
|
||||
let avatarUrl: string = $state(profile?.avatar_url ?? '')
|
||||
|
||||
const handleSubmit: SubmitFunction = () => {
|
||||
loading = true
|
||||
@@ -45,7 +40,7 @@
|
||||
{supabase}
|
||||
bind:url={avatarUrl}
|
||||
size={10}
|
||||
on:upload={() => {
|
||||
onupload={() => {
|
||||
profileForm.requestSubmit();
|
||||
}}
|
||||
/>
|
||||
|
||||
+16
-13
@@ -1,17 +1,18 @@
|
||||
<!-- src/routes/account/Avatar.svelte -->
|
||||
<script lang="ts">
|
||||
import type { SupabaseClient } from '@supabase/supabase-js'
|
||||
import { createEventDispatcher } from 'svelte'
|
||||
|
||||
export let size = 10
|
||||
export let url: string
|
||||
export let supabase: SupabaseClient
|
||||
interface Props {
|
||||
size?: number
|
||||
url?: string
|
||||
supabase: SupabaseClient
|
||||
onupload?: () => void
|
||||
}
|
||||
let { size = 10, url = $bindable(), supabase, onupload }: Props = $props()
|
||||
|
||||
let avatarUrl: string | null = null
|
||||
let uploading = false
|
||||
let files: FileList
|
||||
|
||||
const dispatch = createEventDispatcher()
|
||||
let avatarUrl: string | null = $state(null)
|
||||
let uploading = $state(false)
|
||||
let files: FileList = $state()
|
||||
|
||||
const downloadImage = async (path: string) => {
|
||||
try {
|
||||
@@ -50,7 +51,7 @@
|
||||
|
||||
url = filePath
|
||||
setTimeout(() => {
|
||||
dispatch('upload')
|
||||
onupload?.()
|
||||
}, 100)
|
||||
} catch (error) {
|
||||
if (error instanceof Error) {
|
||||
@@ -61,7 +62,9 @@
|
||||
}
|
||||
}
|
||||
|
||||
$: if (url) downloadImage(url)
|
||||
$effect(() => {
|
||||
if (url) downloadImage(url)
|
||||
})
|
||||
</script>
|
||||
|
||||
<div>
|
||||
@@ -73,7 +76,7 @@
|
||||
style="height: {size}em; width: {size}em;"
|
||||
/>
|
||||
{:else}
|
||||
<div class="avatar no-image" style="height: {size}em; width: {size}em;" />
|
||||
<div class="avatar no-image" style="height: {size}em; width: {size}em;"></div>
|
||||
{/if}
|
||||
<input type="hidden" name="avatarUrl" value={url} />
|
||||
|
||||
@@ -87,7 +90,7 @@
|
||||
id="single"
|
||||
accept="image/*"
|
||||
bind:files
|
||||
on:change={uploadAvatar}
|
||||
onchange={uploadAvatar}
|
||||
disabled={uploading}
|
||||
/>
|
||||
</div>
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
<p>Login error</p>
|
||||
+12
-5
@@ -29,7 +29,6 @@
|
||||
"test:ui-patterns": "turbo run test --filter=ui-patterns",
|
||||
"test:studio": "turbo run test --filter=studio",
|
||||
"test:studio:watch": "turbo run test --filter=studio -- watch",
|
||||
|
||||
"e2e": "pnpm --prefix e2e/studio run e2e",
|
||||
"e2e:dev-hosted": "pnpm --prefix e2e/studio run e2e:dev-hosted",
|
||||
"e2e:dev-selfhosted": "pnpm --prefix e2e/studio run e2e:dev-selfhosted",
|
||||
@@ -39,7 +38,6 @@
|
||||
"e2e:ci": "pnpm --prefix e2e/studio run e2e:ci",
|
||||
"e2e:supabase:start": "pnpm --prefix e2e/studio run supabase:start",
|
||||
"e2e:supabase:stop": "pnpm --prefix e2e/studio run supabase:stop",
|
||||
|
||||
"perf:kong": "ab -t 5 -c 20 -T application/json http://localhost:8000/",
|
||||
"perf:meta": "ab -t 5 -c 20 -T application/json http://localhost:5555/tables",
|
||||
"setup:cli": "supabase start -x studio && supabase status --output json > keys.json && node scripts/generateLocalEnv.js",
|
||||
@@ -72,12 +70,21 @@
|
||||
"esbuild": "^0.25.2",
|
||||
"@redocly/respect-core>form-data": "^4.0.4"
|
||||
},
|
||||
"neverBuiltDependencies": ["libpg-query"]
|
||||
"neverBuiltDependencies": [
|
||||
"libpg-query"
|
||||
]
|
||||
},
|
||||
"engines": {
|
||||
"pnpm": ">=9",
|
||||
"node": ">=22"
|
||||
},
|
||||
"keywords": ["postgres", "firebase", "storage", "functions", "database", "auth"],
|
||||
"keywords": [
|
||||
"postgres",
|
||||
"firebase",
|
||||
"storage",
|
||||
"functions",
|
||||
"database",
|
||||
"auth"
|
||||
],
|
||||
"packageManager": "pnpm@9.15.5"
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user