You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

Supabase Auth UI在SvelteKit中登录成功后未跳转问题排查

问题描述

我已按照Supabase官方SvelteKit教程完成配置,所有文件位置及内容均验证正确,应用能正常与Supabase通信。但使用Auth UI输入正确邮箱/密码登录后,页面停留在Auth UI界面,不会自动跳转到/account;只有刷新页面或访问其他端点时,系统才会识别已认证状态,且未登录时端点会正确重定向到Auth UI。我也接受自定义AuthUI/登录流方案,不一定要用官方AuthUI。


Supabase SQL编辑器配置

-- 创建公共用户资料表
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)
);
-- 配置行级安全(RLS)
alter table profiles
  enable row level security;

create policy "所有用户可查看公共资料。" on profiles
  for select using (true);

create policy "用户可插入自己的资料。" on profiles
  for insert with check (auth.uid() = id);

create policy "用户可更新自己的资料。" on profiles
  for update using (auth.uid() = id);

-- 当新用户通过Supabase Auth注册时,此触发器自动创建资料条目
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();

-- 配置存储!
insert into storage.buckets (id, name)
  values ('avatars', 'avatars');

-- 配置存储访问控制
create policy "头像图片可公开访问。" on storage.objects
  for select using (bucket_id = 'avatars');

create policy "任何人可上传头像。" on storage.objects
  for insert with check (bucket_id = 'avatars');

create policy "用户可更新自己的头像。" on storage.objects
  for update using (auth.uid() = owner) with check (bucket_id = 'avatars');

Svelte项目初始化步骤

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

.ENV配置

PUBLIC_SUPABASE_URL="你的Supabase地址"
PUBLIC_SUPABASE_ANON_KEY="你的Supabase匿名密钥"

依赖安装

npm install @supabase/ssr @supabase/supabase-js
npm install @supabase/auth-ui-svelte @supabase/auth-ui-shared

项目文件配置

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),
      /**
       * 注意:由于SvelteKit的Cookie API要求必须设置`path`参数,
       * 因此需要在set和remove方法中添加该参数,将path设为`/`
       * 可复现之前的标准行为
       */
      set: (key, value, options) => {
        event.cookies.set(key, value, { ...options, path: '/' })
      },
      remove: (key, options) => {
        event.cookies.delete(key, { ...options, path: '/' })
      },
    },
  })

  /**
   * 便捷工具函数,调用await getSession()即可替代
   * 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>用户管理</title>
</svelte:head>

<div class="container" style="padding: 50px 0 100px 0">
    <slot />
</div>

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>用户管理</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">邮箱</label>
            <input id="email" type="text" value={session.user.email} disabled />
        </div>

        <div>
            <label for="fullName">全名</label>
            <input id="fullName" name="fullName" type="text" value={form?.fullName ?? fullName} />
        </div>

        <div>
            <label for="username">用户名</label>
            <input id="username" name="username" type="text" value={form?.username ?? username} />
        </div>

        <div>
            <label for="website">网站</label>
            <input id="website" name="website" type="url" value={form?.website ?? website} />
        </div>

        <div>
            <input
                type="submit"
                class="button block primary"
                value={loading ? '加载中...' : '更新'}
                disabled={loading}
            />
        </div>
    </form>

    <form method="post" action="?/signout" use:enhance={handleSignOut}>
        <div>
            <button class="button block" disabled={loading}>退出登录</button>
        </div>
    </form>
</div>

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, 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, '/')
    }
  },
}

启动命令

npm run dev -- --open --host

内容的提问来源于stack exchange,提问作者alex

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.06.26 23:02:31