Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat!: Migrate to @supabase/ssr #357

Merged
merged 26 commits into from
Jul 5, 2024
Merged
Show file tree
Hide file tree
Changes from 19 commits
Commits
Show all changes
26 commits
Select commit Hold shift + click to select a range
74624c1
refactor: migrate to @supabase/ssr
felixgabler Apr 27, 2024
6123971
refactor: use clientOptions from @supabase/ssr by default
felixgabler Apr 27, 2024
4dcea96
fix: use h3 getCookie instead of useCookie in server plugin
felixgabler Apr 27, 2024
cdde49f
chore: update supabase-js to fix warning
felixgabler Apr 27, 2024
60384af
refactor: fix tsc & lint issues
felixgabler Apr 27, 2024
b768da8
fix: unwrap proxy object from cookie to enable message posting
felixgabler Apr 28, 2024
7f468d3
refactor(composables): useSupabaseuser and useSupabaseSession async
larbish Apr 30, 2024
1b0209b
fix(lint): useless import
larbish Apr 30, 2024
988c9a5
chore(app): remove logs
larbish Apr 30, 2024
e5062b1
refactor: fix ts errors
felixgabler Apr 30, 2024
b4b6975
fix(serverSupabaseUser): getuser instead of getSession
larbish May 2, 2024
f05024c
chore(module): optimize cookie dep
larbish May 2, 2024
660b811
Merge branch 'main' into pr/357
larbish May 2, 2024
3fd5784
Merge branch 'main' into pr/357
larbish May 13, 2024
c9a73b9
chore(deps): upgrade
larbish May 13, 2024
7a3199c
chore(deps): upgrade
larbish May 17, 2024
f7cbc2f
fix(user): populate only once in plugin
larbish May 17, 2024
4dc3828
refactor(user): simplify auth state change handler
felixgabler May 17, 2024
714dbc4
refactor(user): transform useSupabaseUser back to non-async
felixgabler May 17, 2024
b45770c
refactor: clean up cache handling of server composables
felixgabler May 17, 2024
0895ccc
refactor(session): remove async from useSupabaseSession and manage th…
felixgabler May 17, 2024
9cd48ff
fix: do not modify headers after they have been sent
felixgabler May 18, 2024
92bf923
Merge pull request #1 from felixgabler/fg/plugin_controlled_session
felixgabler May 21, 2024
72e2120
chore: update @supabase/ssr to pre-release version
felixgabler Jun 13, 2024
e2878b4
chore: upgrade @supabase/ssr to 0.4.0
felixgabler Jun 24, 2024
830161c
refactor: fix lint issue
felixgabler Jun 25, 2024
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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.3.0",
"@supabase/supabase-js": "2.43.2",
"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.2.0",
"nuxt": "^3.11.2",
"prettier": "^3.2.5",
"release-it": "^17.2.1",
Expand Down
2 changes: 1 addition & 1 deletion playground/pages/protected.vue
Original file line number Diff line number Diff line change
Expand Up @@ -8,5 +8,5 @@
</template>

<script setup lang="ts">
const session = useSupabaseSession()
const session = await useSupabaseSession()
</script>
2 changes: 1 addition & 1 deletion playground/pages/user.vue
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
const supabase = useSupabaseClient()
const user = useSupabaseUser()
const router = useRouter()
const session = useSupabaseSession()
const session = await useSupabaseSession()

if (import.meta.server) {
console.log('User on server side: ', user.value?.email)
Expand Down
4,644 changes: 2,420 additions & 2,224 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')
})
},
})
23 changes: 10 additions & 13 deletions src/runtime/composables/useSupabaseSession.ts
Original file line number Diff line number Diff line change
@@ -1,24 +1,21 @@
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> = () => {
export const useSupabaseSession = async () => {
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
}
})
// Need to manipulate session in `supabase.client` plugin when client is not yet initialized
if (!supabase) {
return sessionState
}

const { data } = await supabase.auth.getSession()
if (data) {
sessionState.value = data.session
}

return sessionState
}
12 changes: 5 additions & 7 deletions src/runtime/composables/useSupabaseUser.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,7 @@
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, updated through `onAuthStateChange` events in the client plugin.
*/
export const useSupabaseUser = () => useState<User | null>('supabase_user', () => null)
11 changes: 6 additions & 5 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 All @@ -7,7 +8,7 @@ export default defineNuxtPlugin({
setup() {
addRouteMiddleware(
'global-auth',
defineNuxtRouteMiddleware((to: RouteLocationNormalized) => {
defineNuxtRouteMiddleware(async (to: RouteLocationNormalized) => {
const config = useRuntimeConfig().public.supabase
const { login, callback, include, exclude, cookieRedirect } = config.redirectOptions
const { cookieName, cookieOptions } = config
Expand All @@ -30,10 +31,10 @@ export default defineNuxtPlugin({
})
if (isExcluded) return

const user = useSupabaseUser()
if (!user.value) {
const session = await 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
61 changes: 18 additions & 43 deletions src/runtime/plugins/supabase.client.ts
Original file line number Diff line number Diff line change
@@ -1,61 +1,36 @@
import { createClient } from '@supabase/supabase-js'
import { createBrowserClient } from '@supabase/ssr'
import { useSupabaseSession } from '../composables/useSupabaseSession'
import { defineNuxtPlugin, useRuntimeConfig, useCookie } from '#imports'
import { useSupabaseUser } from '../composables/useSupabaseUser'
import { defineNuxtPlugin, useRuntimeConfig } from '#imports'
import type { Session } from '@supabase/supabase-js'

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 } = config

const supabaseClient = createClient(url, key, clientOptions)

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 client = createBrowserClient(url, key, {
...clientOptions,
cookieOptions,
isSingleton: true,
})

// 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
}
const currentSession = await useSupabaseSession()
const currentUser = useSupabaseUser()

// 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
// Initializes and subsequently 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
50 changes: 21 additions & 29 deletions src/runtime/plugins/supabase.server.ts
Original file line number Diff line number Diff line change
@@ -1,43 +1,35 @@
import { defu } from 'defu'
import { createClient } from '@supabase/supabase-js'
import { useSupabaseSession } from '../composables/useSupabaseSession'
import { defineNuxtPlugin, useRuntimeConfig, useCookie } from '#imports'
import { createServerClient } from '@supabase/ssr'
import { getCookie, setCookie, deleteCookie } from 'h3'
import { defineNuxtPlugin, useRequestEvent, useRuntimeConfig, 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: {
get: (key: string) => getCookie(event, key),
set: (key: string, value: string, options: CookieOptions) => setCookie(event, key, value, options),
remove: (key: string, options: CookieOptions) => deleteCookie(event, key, 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
}
}
// Fetch user from `getUser` on server side to populate it in useSupaseUser
const {
data: { user },
} = await client.auth.getUser()
useSupabaseUser().value = user

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