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
相关产品推荐
相关产品推荐

