Search code examples
sveltekitsupabasesupabase-js

supabase auth ui not redirecting after successful sign in


I have followed: https://supabase.com/docs/guides/getting-started/tutorials/with-sveltekit?language=ts and verified that everything runs correctly and all files are in the right place/have the correct content.

The application works and updates/communicates to supabase. However when you sign in correctly with the email/pwd, it just stays on the authui screen.

There is no redirection to /account. only when refreshing the page/going to any of the endpoints it is successfully authed. The endpoints, when not signed in also redirect back to the auth ui.

What am I not getting here?

A custom AuthUI/login flow would also be accepted, I don't need to use authUI

Supabase SQL Editor

-- Create a table for public profiles
create table profiles (
  id uuid references auth.users not null primary key,
  updated_at timestamp with time zone,
  username text unique,
  full_name text,
  avatar_url text,
  website text,

  constraint username_length check (char_length(username) >= 3)
);
-- Set up Row Level Security (RLS)
-- See https://supabase.com/docs/guides/auth/row-level-security for more details.
alter table profiles
  enable row level security;

create policy "Public profiles are viewable by everyone." on profiles
  for select using (true);

create policy "Users can insert their own profile." on profiles
  for insert with check (auth.uid() = id);

create policy "Users can update own profile." on profiles
  for update using (auth.uid() = id);

-- This trigger automatically creates a profile entry when a new user signs up via Supabase Auth.
-- See https://supabase.com/docs/guides/auth/managing-user-data#using-triggers for more details.
create function public.handle_new_user()
returns trigger as $$
begin
  insert into public.profiles (id, full_name, avatar_url)
  values (new.id, new.raw_user_meta_data->>'full_name', new.raw_user_meta_data->>'avatar_url');
  return new;
end;
$$ language plpgsql security definer;
create trigger on_auth_user_created
  after insert on auth.users
  for each row execute procedure public.handle_new_user();

-- Set up Storage!
insert into storage.buckets (id, name)
  values ('avatars', 'avatars');

-- Set up access controls for storage.
-- See https://supabase.com/docs/guides/storage/security/access-control#policy-examples for more details.
create policy "Avatar images are publicly accessible." on storage.objects
  for select using (bucket_id = 'avatars');

create policy "Anyone can upload an avatar." on storage.objects
  for insert with check (bucket_id = 'avatars');

create policy "Anyone can update their own avatar." on storage.objects
  for update using (auth.uid() = owner) with check (bucket_id = 'avatars');

Init svelte app

npm create svelte@latest supabase-sveltekit
cd supabase-sveltekit
npm install

defined .ENV

PUBLIC_SUPABASE_URL="YOUR_SUPABASE_URL"
PUBLIC_SUPABASE_ANON_KEY="YOUR_SUPABASE_KEY"

installed: npm install @supabase/ssr @supabase/supabase-js

// 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: {
      get: (key) => event.cookies.get(key),
      /**
       * Note: You have to add the `path` variable to the
       * set and remove method due to sveltekit's cookie API
       * requiring this to be set, setting the path to `/`
       * will replicate previous/standard behaviour (https://kit.svelte.dev/docs/types#public-types-cookies)
       */
      set: (key, value, options) => {
        event.cookies.set(key, value, { ...options, path: '/' })
      },
      remove: (key, options) => {
        event.cookies.delete(key, { ...options, path: '/' })
      },
    },
  })

  /**
   * A convenience helper so we can just call await getSession() instead const { data: { session } } = await supabase.auth.getSession()
   */
  event.locals.getSession = async () => {
    const {
      data: { session },
    } = await event.locals.supabase.auth.getSession()
    return session
  }

  return resolve(event, {
    filterSerializedResponseHeaders(name) {
      return name === 'content-range'
    },
  })
}
// src/app.d.ts

import { SupabaseClient, Session } from '@supabase/supabase-js'

declare global {
  namespace App {
    interface Locals {
      supabase: SupabaseClient
      getSession(): Promise<Session | null>
    }
    interface PageData {
      session: Session | null
    }
    // interface Error {}
    // interface Platform {}
  }
}
// src/routes/+layout.server.ts
import type { LayoutServerLoad } from './$types'

export const load: LayoutServerLoad = async ({ locals: { getSession } }) => {
  return {
    session: await getSession(),
  }
}
// src/routes/+layout.ts
import { PUBLIC_SUPABASE_ANON_KEY, PUBLIC_SUPABASE_URL } from '$env/static/public'
import type { LayoutLoad } from './$types'
import { createBrowserClient, isBrowser, parse } from '@supabase/ssr'

export const load: LayoutLoad = async ({ fetch, data, depends }) => {
  depends('supabase:auth')

  const supabase = createBrowserClient(PUBLIC_SUPABASE_URL, PUBLIC_SUPABASE_ANON_KEY, {
    global: {
      fetch,
    },
    cookies: {
      get(key) {
        if (!isBrowser()) {
          return JSON.stringify(data.session)
        }

        const cookie = parse(document.cookie)
        return cookie[key]
      },
    },
  })

  const {
    data: { session },
  } = await supabase.auth.getSession()

  return { supabase, session }
}
<!-- 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, _session) => {
            if (_session?.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>
npm install @supabase/auth-ui-svelte @supabase/auth-ui-shared
<!-- src/routes/+page.svelte -->
<script lang="ts">
    import { Auth } from '@supabase/auth-ui-svelte'
    import { ThemeSupa } from '@supabase/auth-ui-shared'

    export let data
</script>

<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}/auth/callback`}
            showLinks={false}
            appearance={{ theme: ThemeSupa, style: { input: 'color: #fff' } }}
        />
    </div>
</div>
// src/routes/auth/callback/+server.ts
import { redirect } from '@sveltejs/kit'

export const GET = async ({ url, locals: { supabase } }) => {
  const code = url.searchParams.get('code')

  if (code) {
    await supabase.auth.exchangeCodeForSession(code)
  }

  throw redirect(303, '/account')
}
<!-- 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>
import { fail, redirect } from '@sveltejs/kit'
import type { Actions, PageServerLoad } from './$types'

export const load: PageServerLoad = 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 }
}

export const actions: 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, '/')
    }
  },
}

And launch npm run dev -- --open --host


Solution

  • As I read from the post you have most likely switched the Auth component to sign_in instead of magic_link like the example given.

    You need to listen to the auth event as per the documentation and apply it to your example.

    <!-- src/routes/+page.svelte -->
    <script lang="ts">
        import { Auth } from '@supabase/auth-ui-svelte';
        import { ThemeSupa } from '@supabase/auth-ui-shared';
    
        export let data;
        
        let { supabase } = data;
        $: ({ supabase } = data);
        
        // listen to the auth signed_in event
        supabase.auth.onAuthStateChange((event, session) => {
            if (event === 'SIGNED_IN') {
                window.location.href = '/account'; // redirect to account page
            }
        });
    
    </script>
    
    <svelte:head>
        <title>User Management</title>
    </svelte:head>
    
    <div class="row flex-center flex">
        <div class="col-6 form-widget">
           <!-- view="sign_in" instead of magic_link for email+password auth -->
            <Auth
                supabaseClient={data.supabase}
                view="sign_in"
                redirectTo={`${data.url}/auth/callback`}
                showLinks={false}
                appearance={{ theme: ThemeSupa, style: { input: 'color: #fff' } }}
            />
        </div>
    </div>