Skip to content

Commit

Permalink
feat(app): migrate to @supabase/ssr (#357)
Browse files Browse the repository at this point in the history
Co-authored-by: Baptiste Leproux <[email protected]>
  • Loading branch information
felixgabler and larbish authored Jul 5, 2024
1 parent 70563f9 commit 56dc39e
Show file tree
Hide file tree
Showing 14 changed files with 3,186 additions and 3,549 deletions.
17 changes: 11 additions & 6 deletions docs/content/2.get-started.md
Original file line number Diff line number Diff line change
Expand Up @@ -99,7 +99,7 @@ Default:
Default: `sb`
Cookie name used for storing access and refresh tokens, added in front of `-access-token` and `-refresh-token` to form the full cookie name e.g. `sb-access-token`
Cookie name used for storing the redirect path when using the `redirect` option, added in front of `-redirect-path` to form the full cookie name e.g. `sb-redirect-path`
### cookieOptions
Expand All @@ -115,20 +115,25 @@ Options for cookies used to share tokens between server and client, refer to [co
### `clientOptions`
Default:
Default:
```ts
clientOptions: { }
```
Supabase client options [available here](https://supabase.com/docs/reference/javascript/initializing#parameters) merged with default values from `@supabase/ssr`:
```ts
clientOptions: {
auth: {
flowType: 'pkce',
detectSessionInUrl: true,
persistSession: true,
autoRefreshToken: true
autoRefreshToken: isBrowser(),
detectSessionInUrl: isBrowser(),
persistSession: true,
},
}
```
Supabase client options [available here](https://supabase.com/docs/reference/javascript/initializing#parameters).
## Versions
Expand Down
9 changes: 5 additions & 4 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -27,17 +27,18 @@
},
"dependencies": {
"@nuxt/kit": "^3.11.2",
"@supabase/supabase-js": "2.43.0",
"@supabase/ssr": "^0.4.0",
"@supabase/supabase-js": "^2.43.5",
"defu": "^6.1.4",
"pathe": "^1.1.2"
},
"devDependencies": {
"@nuxt/eslint": "^0.3.10",
"@nuxt/eslint": "^0.3.12",
"@nuxt/module-builder": "^0.6.0",
"@nuxt/schema": "^3.11.2",
"@release-it/conventional-changelog": "^8.0.1",
"@types/node": "^20.12.8",
"eslint": "^9.1.1",
"@types/node": "^20.12.12",
"eslint": "^9.4.0",
"nuxt": "^3.11.2",
"prettier": "^3.2.5",
"release-it": "^17.2.1",
Expand Down
6,405 changes: 3,054 additions & 3,351 deletions pnpm-lock.yaml

Large diffs are not rendered by default.

35 changes: 6 additions & 29 deletions src/module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -54,7 +54,7 @@ export interface ModuleOptions {
redirectOptions?: RedirectOptions

/**
* Cookie name, used for storing access and refresh tokens, added in front of `-access-token` and `-refresh-token` to form the full cookie name e.g. `sb-access-token`
* Cookie name used for storing the redirect path when using the `redirect` option, added in front of `-redirect-path` to form the full cookie name e.g. `sb-redirect-path`
* @default 'sb'
* @type string
*/
Expand All @@ -73,14 +73,8 @@ export interface ModuleOptions {
cookieOptions?: CookieOptions

/**
* Supabase Client options
* @default {
auth: {
flowType: 'pkce',
detectSessionInUrl: true,
persistSession: true,
},
}
* Supabase client options (overrides default options from `@supabase/ssr`)
* @default { }
* @type object
* @docs https://supabase.com/docs/reference/javascript/initializing#parameters
*/
Expand Down Expand Up @@ -112,14 +106,7 @@ export default defineNuxtModule<ModuleOptions>({
sameSite: 'lax',
secure: true,
} as CookieOptions,
clientOptions: {
auth: {
flowType: 'pkce',
detectSessionInUrl: true,
persistSession: true,
autoRefreshToken: true,
},
} as SupabaseClientOptions<string>,
clientOptions: {} as SupabaseClientOptions<string>,
},
setup(options, nuxt) {
const { resolve } = createResolver(import.meta.url)
Expand Down Expand Up @@ -211,21 +198,11 @@ export default defineNuxtModule<ModuleOptions>({
nuxt.options.build.transpile.push('websocket')
}

// Optimize @supabase/ packages for dev
// TODO: Remove when packages fixed with valid ESM exports
// https://github.com/supabase/gotrue/issues/1013
// Needed to fix https://github.com/supabase/auth-helpers/issues/725
extendViteConfig((config) => {
config.optimizeDeps = config.optimizeDeps || {}
config.optimizeDeps.include = config.optimizeDeps.include || []
config.optimizeDeps.exclude = config.optimizeDeps.exclude || []
config.optimizeDeps.include.push(
'@supabase/functions-js',
'@supabase/gotrue-js',
'@supabase/postgrest-js',
'@supabase/realtime-js',
'@supabase/storage-js',
'@supabase/supabase-js',
)
config.optimizeDeps.include.push('cookie')
})
},
})
26 changes: 5 additions & 21 deletions src/runtime/composables/useSupabaseSession.ts
Original file line number Diff line number Diff line change
@@ -1,24 +1,8 @@
import type { Session } from '@supabase/supabase-js'
import type { Ref } from 'vue'
import { useSupabaseClient } from './useSupabaseClient'
import { useState } from '#imports'

export const useSupabaseSession: () => Ref<Session | null> = () => {
const supabase = useSupabaseClient()

const sessionState = useState<Session | null>('supabase_session', () => null)

// Asyncronous refresh session and ensure user is still logged in
supabase?.auth.getSession().then(({ data: { session } }) => {
if (session) {
if (JSON.stringify(sessionState.value) !== JSON.stringify(session)) {
sessionState.value = session
}
}
else {
sessionState.value = null
}
})

return sessionState
}
/**
* Reactive `Session` state from Supabase. This is initialized in both client and server plugin
* and, on the client, also updated through `onAuthStateChange` events.
*/
export const useSupabaseSession = () => useState<Session | null>('supabase_session', () => null)
13 changes: 6 additions & 7 deletions src/runtime/composables/useSupabaseUser.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,8 @@
import type { User } from '@supabase/supabase-js'
import { useSupabaseSession } from './useSupabaseSession'
import { computed, type ComputedRef } from '#imports'
import { useState } from '#imports'

export const useSupabaseUser: () => ComputedRef<User | null> = () => {
const session = useSupabaseSession()
const userState = computed(() => session.value?.user)
return userState
}
/**
* Reactive `User` state from Supabase. This is initialized in both client and server plugin
* and, on the client, also updated through `onAuthStateChange` events.
*/
export const useSupabaseUser = () => useState<User | null>('supabase_user', () => null)
9 changes: 5 additions & 4 deletions src/runtime/plugins/auth-redirect.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { useSupabaseUser } from '../composables/useSupabaseUser'
import { useSupabaseSession } from '../composables/useSupabaseSession'
import type { Ref } from '#imports'
import { defineNuxtPlugin, addRouteMiddleware, defineNuxtRouteMiddleware, useCookie, useRuntimeConfig, navigateTo } from '#imports'
import type { RouteLocationNormalized } from '#vue-router'

Expand Down Expand Up @@ -30,10 +31,10 @@ export default defineNuxtPlugin({
})
if (isExcluded) return

const user = useSupabaseUser()
if (!user.value) {
const session = useSupabaseSession()
if (!session.value) {
if (cookieRedirect) {
useCookie(`${cookieName}-redirect-path`, cookieOptions).value = to.fullPath
(useCookie(`${cookieName}-redirect-path`, { ...cookieOptions, readonly: false }) as Ref).value = to.fullPath
}

return navigateTo(login)
Expand Down
67 changes: 24 additions & 43 deletions src/runtime/plugins/supabase.client.ts
Original file line number Diff line number Diff line change
@@ -1,61 +1,42 @@
import { createClient } from '@supabase/supabase-js'
import { createBrowserClient } from '@supabase/ssr'
import type { Session } from '@supabase/supabase-js'
import { useSupabaseSession } from '../composables/useSupabaseSession'
import { defineNuxtPlugin, useRuntimeConfig, useCookie } from '#imports'
import { useSupabaseUser } from '../composables/useSupabaseUser'
import { defineNuxtPlugin, useRuntimeConfig } from '#imports'

export default defineNuxtPlugin({
name: 'supabase',
enforce: 'pre',
async setup() {
const currentSession = useSupabaseSession()
const config = useRuntimeConfig().public.supabase
const { url, key, cookieName, cookieOptions, clientOptions } = config
const { url, key, cookieOptions, clientOptions } = useRuntimeConfig().public.supabase

const supabaseClient = createClient(url, key, clientOptions)
const client = createBrowserClient(url, key, {
...clientOptions,
cookieOptions,
isSingleton: true,
})

const accessToken = useCookie(`${cookieName}-access-token`, cookieOptions)
const refreshToken = useCookie(`${cookieName}-refresh-token`, cookieOptions)
const providerToken = useCookie(`${cookieName}-provider-token`, cookieOptions)
const providerRefreshToken = useCookie(`${cookieName}-provider-refresh-token`, cookieOptions)
const currentSession = useSupabaseSession()
const currentUser = useSupabaseUser()

// Handle auth event client side
supabaseClient.auth.onAuthStateChange((event, session) => {
// Update states based on received session
if (session) {
if (JSON.stringify(currentSession) !== JSON.stringify(session)) {
currentSession.value = session
}
}
else {
currentSession.value = null
}
// Initialize user and session states
const {
data: { session },
} = await client.auth.getSession()
currentSession.value = session
currentUser.value = session?.user ?? null

// Use cookies to share session state between server and client
if (event === 'SIGNED_IN' || event === 'TOKEN_REFRESHED') {
accessToken.value = session?.access_token
refreshToken.value = session?.refresh_token
if (session.provider_token) {
providerToken.value = session.provider_token
}
if (session.provider_refresh_token) {
providerRefreshToken.value = session.provider_refresh_token
}
}
if (event === 'SIGNED_OUT') {
accessToken.value = null
refreshToken.value = null
providerToken.value = null
providerRefreshToken.value = null
// Updates the session and user states through auth events
client.auth.onAuthStateChange((_, session: Session | null) => {
if (JSON.stringify(currentSession.value) !== JSON.stringify(session)) {
currentSession.value = session
currentUser.value = session?.user ?? null
}
})

// Attempt to retrieve existing session from local storage
await supabaseClient.auth.getSession()

return {
provide: {
supabase: {
client: supabaseClient,
},
supabase: { client },
},
}
},
Expand Down
61 changes: 32 additions & 29 deletions src/runtime/plugins/supabase.server.ts
Original file line number Diff line number Diff line change
@@ -1,43 +1,46 @@
import { defu } from 'defu'
import { createClient } from '@supabase/supabase-js'
import { useSupabaseSession } from '../composables/useSupabaseSession'
import { defineNuxtPlugin, useRuntimeConfig, useCookie } from '#imports'
import { createServerClient, parseCookieHeader } from '@supabase/ssr'
import { getHeader, setCookie } from 'h3'
import { defineNuxtPlugin, useRequestEvent, useRuntimeConfig, useSupabaseSession, useSupabaseUser } from '#imports'
import type { CookieOptions } from '#app'

export default defineNuxtPlugin({
name: 'supabase',
enforce: 'pre',
async setup() {
const { url, key, cookieName, clientOptions } = useRuntimeConfig().public.supabase
const accessToken = useCookie(`${cookieName}-access-token`).value
const refreshToken = useCookie(`${cookieName}-refresh-token`).value
const { url, key, cookieOptions, clientOptions } = useRuntimeConfig().public.supabase

const options = defu({
auth: {
flowType: clientOptions.auth.flowType,
detectSessionInUrl: false,
persistSession: false,
autoRefreshToken: false,
},
}, clientOptions)
const event = useRequestEvent()!

const supabaseClient = createClient(url, key, options)
const client = createServerClient(url, key, {
...clientOptions,
cookies: {
getAll: () => parseCookieHeader(getHeader(event, 'Cookie') ?? ''),
setAll: (
cookies: {
name: string
value: string
options: CookieOptions
}[],
) => cookies.forEach(({ name, value, options }) => setCookie(event, name, value, options)),
},
cookieOptions,
})

// Set user & session server side
if (accessToken && refreshToken) {
const { data } = await supabaseClient.auth.setSession({
refresh_token: refreshToken,
access_token: accessToken,
})
if (data) {
useSupabaseSession().value = data.session
}
}
// Initialize user and session states
const [
{
data: { session },
},
{
data: { user },
},
] = await Promise.all([client.auth.getSession(), client.auth.getUser()])
useSupabaseSession().value = session
useSupabaseUser().value = user

return {
provide: {
supabase: {
client: supabaseClient,
},
supabase: { client },
},
}
},
Expand Down
Loading

0 comments on commit 56dc39e

Please sign in to comment.