0.6.14 • Published 2 years ago

@rychunpm/auth-helpers-sveltekit v0.6.14

Weekly downloads
-
License
MIT
Repository
github
Last release
2 years ago

@supabase/auth-helpers-sveltekit (BETA)

This submodule provides convenience helpers for implementing user authentication in SvelteKit applications.

Installation

Using npm:

npm install @supabase/auth-helpers-sveltekit

Using yarn:

yarn add @supabase/auth-helpers-sveltekit

This library supports the following tooling versions:

  • Node.js: ^16.15.0

Getting Started

Configuration

Set up the fillowing env vars. For local development you can set them in a .env file. See an example here.

# Find these in your Supabase project settings > API
PUBLIC_SUPABASE_URL=https://your-project.supabase.co
PUBLIC_SUPABASE_ANON_KEY=your-anon-key

SupabaseClient setup

We will start off by creating a db.ts file inside of our src/lib directory. Now lets instantiate our supabaseClient.

// src/lib/db.ts
import { createClient } from '@supabase/supabase-js';
import { setupSupabaseHelpers } from '@supabase/auth-helpers-sveltekit';
import { dev } from '$app/environment';
import { env } from '$env/dynamic/public';
// or use the static env
// import { PUBLIC_SUPABASE_URL, PUBLIC_SUPABASE_ANON_KEY } from '$env/static/public';

export const supabaseClient = createClient(env.PUBLIC_SUPABASE_URL, env.PUBLIC_SUPABASE_ANON_KEY, {
	persistSession: false,
	autoRefreshToken: false
});

setupSupabaseHelpers({
	supabaseClient,
	cookieOptions: {
		secure: !dev
	}
});

Initialize the client

Edit your +layout.svelte file and set up the client side.

<!-- src/routes/+layout.svelte -->
<script lang="ts">
  // we need to make sure the supabase instance is initialized on the client
  import '$lib/db';
  import { startSupabaseSessionSync } from '@supabase/auth-helpers-sveltekit';

  // this sets up automatic token refreshing
  startSupabaseSessionSync();
</script>

<slot />

Hooks setup

Our hooks.ts file is where the heavy lifting of this library happens:

// src/hooks.server.ts

// we need to make sure the supabase instance is initialized on the server
import '$lib/db';
import { dev } from '$app/environment';
import { auth } from '@supabase/auth-helpers-sveltekit/server';

export const handle = auth();

// use the sequence helper if you have additional Handle methods
import { sequence } from '@sveltejs/kit/hooks';

export const handle = sequence(auth(), yourHandler);

There are three handle methods available:

  • callback():

    This will create a handler for /api/auth/callback. The client forwards the session details here every time onAuthStateChange fires on the client side. This is needed to set up the cookies for your application so that SSR works seamlessly.

  • session():

    This will parse the session from the cookie and populate it in locals

  • auth():

    a shorthand for sequence(callback(), session()) that uses both handlers

Send session to client

In order to make the session available to the UI (pages, layouts) we need to pass the session in the root layout load function:

// src/routes/+layout.server.ts
import type { LayoutServerLoad } from './$types';

export const load: LayoutServerLoad = async ({ locals }) => {
  return {
    session: locals.session
  };
};

Typings

In order to get the most out of TypeScript and it´s intellisense, you should import our types into the app.d.ts type definition file that comes with your SvelteKit project.

// src/app.d.ts

/// <reference types="@sveltejs/kit" />

// See https://kit.svelte.dev/docs/types#app
// for information about these interfaces
// and what to do when importing types
declare namespace App {
  interface Locals {
    session: import('@supabase/auth-helpers-sveltekit').SupabaseSession;
  }
  interface PageData {
    session: import('@supabase/auth-helpers-sveltekit').SupabaseSession;
  }
  // interface Error {}
  // interface Platform {}
}

Basic Setup

You can now determine if a user is authenticated on the client-side by checking that the user object in $page.data.session is defined.

<!-- src/routes/+page.svelte -->
<script>
  import { page } from '$app/stores';
</script>

{#if !$page.data.session.user}
  <h1>I am not logged in</h1>
{:else}
  <h1>Welcome {$page.data.session.user.email}</h1>
  <p>I am logged in!</p>
{/if}

Client-side data fetching with RLS

For row level security to work properly when fetching data client-side, you need to make sure to import the { supabaseClient } from $lib/db and only run your query once the user is defined client-side in $page.data.session:

<script>
  import { supabaseClient } from '$lib/db';
  import { page } from '$app/stores';

  let loadedData = [];
  async function loadData() {
    const { data } = await supabaseClient.from('test').select('*').limit(20);
    loadedData = data;
  }

  $: if ($page.data.session.user) {
    loadData();
  }
</script>

{#if $page.data.session.user}
  <p>client-side data fetching with RLS</p>
  <pre>{JSON.stringify(loadedData, null, 2)}</pre>
{/if}

Server-side data fetching with RLS

<!-- src/routes/profile/+page.svelte -->
<script>
  /** @type {import('./$types').PageData} */
  export let data;
  $: ({ user, tableData } = data);
</script>

<div>Protected content for {user.email}</div>
<pre>{JSON.stringify(tableData, null, 2)}</pre>
<pre>{JSON.stringify(user, null, 2)}</pre>

For row level security to work in a server environment, you need to use the withAuth helper to check if the user is authenticated. The helper extends the event with session and getSupabaseClient():

// src/routes/profile/+page.ts
import type { PageLoad } from './$types';
import { withAuth } from '@supabase/auth-helpers-sveltekit';
import { redirect } from '@sveltejs/kit';

interface TestTable {
  id: string;
  created_at: string;
}

export const load: PageLoad = withAuth(async ({ getSupabaseClient, session }) => {
  if (!session.user) {
    throw redirect(303, '/');
  }
  const { data: tableData } = await getSupabaseClient()
    .from<TestTable>('test')
    .select('*');

  return {
    user: session.user,
    tableData
  };
);

Caution:

Always use the instance returned by getSupabaseClient() directly!

// Bad
const supabaseClient = getSupabaseClient();

await supabaseClient.from('table1').select();
await supabaseClient.from('table2').select();

// Good
await getSupabaseClient().from('table1').select();
await getSupabaseClient().from('table2').select();

Protecting API routes

Wrap an API Route to check that the user has a valid session. If they're not logged in the session is null.

// src/routes/api/protected-route/+server.ts
import type { RequestHandler } from './$types';
import { withAuth } from '@supabase/auth-helpers-sveltekit';
import { json, redirect } from '@sveltejs/kit';

interface TestTable {
  id: string;
  created_at: string;
}

export const GET: RequestHandler = withAuth(async ({ session, getSupabaseClient }) => {
  if (!session.user) {
    throw redirect(303, '/');
  }
  const { data } = await getSupabaseClient()
    .from<TestTable>('test')
    .select('*');

  return json({ data });
);

If you visit /api/protected-route without a valid session cookie, you will get a 303 response.

Protecting Actions

Wrap an Action to check that the user has a valid session. If they're not logged in the session is null.

// src/routes/posts/+page.server.ts
import type { Actions } from './$types';
import { withAuth } from '@supabase/auth-helpers-sveltekit';
import { error, invalid } from '@sveltejs/kit';

export const actions: Actions = {
  createPost: withAuth(async ({ session, getSupabaseClient, request }) => {
    if (!session.user) {
      // the user is not signed in
      throw error(403, { message: 'Unauthorized' });
    }
    // we are save, let the user create the post
    const formData = await request.formData();
    const content = formData.get('content');

    const { error: createPostError, data: newPost } = await getSupabaseClient()
      .from('posts')
      .insert({ content });

    if (createPostError) {
      return invalid(500, {
        supabaseErrorMessage: createPostError.message
      });
    }
    return {
      newPost
    };
  })
};

If you try to submit a form with the action ?/createPost without a valid session cookie, you will get a 403 error response.

Saving and deleting the session

Use saveSession to save the session cookies:

import type { Actions } from './$types';
import { supabaseClient } from '$lib/db';
import { invalid, redirect } from '@sveltejs/kit';
import { saveSession } from '@supabase/auth-helpers-sveltekit/server';

export const actions: Actions = {
  async signin({ request, cookies, url }) {
    const formData = await request.formData();

    const email = formData.get('email') as string;
    const password = formData.get('password') as string;

    const { data, error } = await supabaseClient.auth.api.signInWithEmail(
      email,
      password,
      {
        redirectTo: `${url.origin}/logging-in`
      }
    );

    if (error || !data) {
      if (error?.status === 400) {
        return invalid(400, {
          error: 'Invalid credentials',
          values: {
            email
          }
        });
      }
      return invalid(500, {
        error: 'Server error. Try again later.',
        values: {
          email
        }
      });
    }

    saveSession(cookies, data);
    throw redirect(303, '/dashboard');
  }
};

Use deleteSession to delete the session cookies:

import type { Actions } from './$types';
import { deleteSession } from '@supabase/auth-helpers-sveltekit/server';
import { redirect } from '@sveltejs/kit';

export const actions: Actions = {
  async logout({ cookies }) {
    deleteSession(cookies);
    throw redirect(303, '/');
  }
};

Custom session namespace

If you wan´t to use something else than locals.session and $page.data.session you can do so by updating the types and creating three helper functions:

// src/app.d.ts
declare namespace App {
  interface Locals {
    mySupabaseSession: import('@supabase/auth-helpers-sveltekit').SupabaseSession;
  }
  interface PageData {
    mySupabaseSession: import('@supabase/auth-helpers-sveltekit').SupabaseSession;
  }
}

// src/hooks.server.ts
setupSupabaseServer({
  supabaseClient,
  cookieOptions: {
    secure: !dev
  },
  // --- change location within locals ---
  getSessionFromLocals: (locals) => locals.mySupabaseSession,
  setSessionToLocals: (locals, session) => (locals.mySupabaseSession = session)
});

// src/lib/db.ts
setupSupabaseClient({
  supabaseClient,
  // --- change location within pageData ---
  getSessionFromPageData: (data) => data.mySupabaseSession
});