supabase
supabase copied to clipboard
User only available after middleware has run
Version
@nuxtjs/supabase: v0.1.9 nuxt: v3.0.0-rc.1
Steps to reproduce
create global auth guard like this:
export default defineNuxtRouteMiddleware((to) => {
const user = useSupabaseUser()
if (!(user.value) && to.name !== 'login')
return navigateTo('/login')
})
create login page with logic like this
const client = useSupabaseClient()
const user = useSupabaseUser()
const email = ref('')
const signIn = async() => {
const { session, error } = await client.auth.signIn({ email: email.value })
}
You'll get a link like this which redirects you to your page with the credentials to setup a Supabase User.
https://pjbhhvurmrikesghutkr.supabase.co/auth/v1/verify?token=myToken&type=magiclink&redirect_to=http://localhost:3000/
What is Expected?
I should be redirected to the url in the redirect_to query param in the link
What is actually happening?
The user is undefined when the authguard is running its logic and therefore the user is redirected to the login page instead of the original destination. The user only becomes available after the navigation has finished, leaving the user on the login page, even though they are logged in. We could watch the user on the login route and redirect, but this seems unnecessary.
Edit:
Also getting this error on the server side when using the link in the email:
[nuxt] [request error] Auth session missing!
at ./.nuxt/dev/index.mjs:651:13
at processTicksAndRejections (node:internal/process/task_queues:96:5)
at async ./node_modules/.pnpm/[email protected]/node_modules/h3/dist/index.mjs:417:19
at async Server.nodeHandler (./node_modules/.pnpm/[email protected]/node_modules/h3/dist/index.mjs:367:7)
This seems to be related to supabase/supabase-js#25, I get exactly the same results.
The user seems to be registered too late by the onAuthStateChange
handler for the middleware to get the data.
To prevent that I'm setting the user manually in my repo when I get the response of the signIn
or signUp
. This allows the navigation to work. See here for the temporary implementation I've tried.
This doesn't fix the error message about the session. I'm still trying to get it to work and to get a cleaner implementation.
EDIT: I'm not using magic links but emails confirmations, I get the exact same case with email confirmation links.
This issue from the supabase repository also seems to be related.
Shouldn't the module already do that for you, what you are doing in your pinia store ? The module already watches the auth events and sets the user, or am I missing something ?
@mwohlan The module listens to the onAuthStateChange
function to change the user value. This happens to late for the middleware get the user data.
Just as you mentioned, the navigation already has finished when this event is received.
So the solution I've come with is setting the user directly from the response of the signIn
or signUp
method.
I'm not super happy with the way I've done it and I'm searching for another way too.
I think this is a supabase timing issue. This example call:
await client.auth.signIn()
navigateTo('auth protected route')
is done before the user is set here:
// Once Nuxt app is mounted
nuxtApp.hooks.hook('app:mounted', () => {
// Listen to Supabase auth changes
client.auth.onAuthStateChange(async (event: AuthChangeEvent, session: Session | null) => {
await setServerSession(event, session)
user.value = client.auth.user()
})
})
})
This explains the need for a workaround in any case (also when logging in using a magic link). It's all doable but feels kinda hacky.
Current workaround for me that works with auth protected routes and magic links:
middleware
export default defineNuxtRouteMiddleware((to) => {
const user = useSupabaseUser()
if (!(user.value))
//reroute to login saving the current destination in the redirect query param
return navigateTo({ name: 'login', query: { redirect: to.path } })
})
plugin to watch route and user
export default defineNuxtPlugin(() => {
const user = useSupabaseUser()
// globally watch user and route. If a user and a redirect query param exist:
watchEffect(() => {
if (user.value) {
const route = useRoute()
if (route.query.redirect)
navigateTo({ path: route.query.redirect as string })
}
})
})
sign in logic on the login page:
const signIn = async() => {
// the redirectTo param is for magic/confirmation links
const { session, error } = await client.auth.signIn({ email: email.value }, { redirectTo: `http://localhost:3000${route.query?.redirect}` })
// this is for username + password sign in to trigger the watchEffect. The component doesnt get rerendered afaik
navigateTo({ name: 'login', query: { redirect: route.query.redirect } })
}
It works for both use cases but is also quite hacky.
Thanks for that, I'll try it too. Does that solves the session error issue too ?
Thanks for that, I'll try it too. Does that solves the session error issue too ?
@ColinEspinas I commented on that in your original issue #25. Maybe they shouldnt throw an error but simply return in that case ?
@mwohlan Tried it and it works, its unfortunate that we need to use tricks like those but at least it gets the job done.
I think this is a supabase timing issue. This example call:
await client.auth.signIn() navigateTo('auth protected route')
is done before the user is set here:
// Once Nuxt app is mounted nuxtApp.hooks.hook('app:mounted', () => { // Listen to Supabase auth changes client.auth.onAuthStateChange(async (event: AuthChangeEvent, session: Session | null) => { await setServerSession(event, session) user.value = client.auth.user() }) }) })
This explains the need for a workaround in any case (also when logging in using a magic link). It's all doable but feels kinda hacky.
I opened an issue on the supabase-js repository for the client side sync issue
Hello everyone,
I have this problem too. My global auth middleware send me to /login on refresh. This because useSupabaseUser() state is set after middleware has run on page refresh (F5).
I made some experiments with firebase and came to a similar problem wich made me think it is Nuxt 3 related. My supposition is onAuthStateChange functions are watchers, and watcher always run after middleware. But I have to say I am a "noob" dev, so I mightmiss a piece of the puzzle. Should I copy this issue on Nuxt3 github ?
As a temporary fix, I manually register useSupabaseUser.value on signIn() and manually reset it on signOut() but it does not seem the cleanest way.
Using setTimeout fixed it for me.
const signIn = async () => {
await client.auth.signIn({
email: email.value,
password: password.value,
})
await new Promise((resolve) => {
setTimeout(resolve, 50)
})
console.log(user.value)
navigateTo('/')
}
Using setTimeout fixed it for me.
const signIn = async () => { await client.auth.signIn({ email: email.value, password: password.value, }) await new Promise((resolve) => { setTimeout(resolve, 50) }) console.log(user.value) navigateTo('/') }
Doesn't work on my end...
@IIFelix has the right idea I think, but instead of waiting for 50ms, wait until the auth handler callback has completed. This works for me. Add data to the component for useSupabaseUser(), and when the callback comletes that data will be updated. Wait to redirect until we see the user session populate. Here's rough example:
export default defaultNuxtComponent({
setup() {
return {
loginUser: useSupabaseUser(),
supabase: getSupabaseClient(),
},
},
data() {
return {
email: '[email protected]',
password: 'they-might-be-giants',
},
}
methods: {
async tryLogin() {
const { user, error } = await this.supabase.auth.signIn({
email: this.email,
password: this.password,
});
if (error) {
console.error('oh no, anyway', error)
} else if (user) {
const timer = setInterval(() => {
if (this.loginUser && this.loginUser.id) {
// Wait for @nuxtjs/supabase auth callback to complete before redirect
clearInterval(timer)
await navigateTo({ path: '/hello-friend' });
}
}, 100)
}
}
},
});
For anyone who is still having this issue, I was able to resolve it by using a watchEffect
:
const user = useSupabaseUser();
async function signin() {
const { data, error } = await client.auth.signInWithPassword({email, password});
}
watchEffect(async () => {
if (user.value) await navigateTo("/browse");
});