Skip to content

Commit

Permalink
feat: allow users to define custom session factory + types
Browse files Browse the repository at this point in the history
  • Loading branch information
danielroe committed Nov 7, 2023
1 parent 74f452c commit 915e4f1
Show file tree
Hide file tree
Showing 11 changed files with 58 additions and 36 deletions.
4 changes: 2 additions & 2 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -65,8 +65,8 @@ jobs:
- name: Run test suite
run: pnpm test

# - name: Test types
# run: pnpm test:types
- name: Test types
run: pnpm test:types

# - name: Test playground types
# run: pnpm test:types:playground
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -111,7 +111,7 @@ const session = await requireUserSession(event)

All helpers are exposed from the `oauth` global variable and can be used in your server routes or API routes.

The pattern is `oauth.<provider>EventHandler({ onSuccess, config?, onError? })`, example: `oauth.githubEventHandler`.
The pattern is `oauth.<provider>EventHandler({ onSuccess?, config?, onError? })`, example: `oauth.githubEventHandler`.

The helper returns an event handler that automatically redirects to the provider authorization page and then call `onSuccess` or `onError` depending on the result.

Expand Down
6 changes: 6 additions & 0 deletions playground/app/auth-utils-session.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
// This is only used if an oauth provider doesn't provide an `onSuccess` callback
export default defineSession((_event, { provider, user }) => ({
user: {
[provider]: user
},
}))
12 changes: 1 addition & 11 deletions playground/server/routes/auth/github.get.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1 @@
export default oauth.githubEventHandler({
async onSuccess(event, { user }) {
await setUserSession(event, {
user: {
github: user,
}
})

return sendRedirect(event, '/')
}
})
export default oauth.githubEventHandler()
12 changes: 1 addition & 11 deletions playground/server/routes/auth/spotify.get.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1 @@
export default oauth.spotifyEventHandler({
async onSuccess(event, { user }) {
await setUserSession(event, {
user: {
spotify: user,
}
})

return sendRedirect(event, '/')
}
})
export default oauth.spotifyEventHandler()
17 changes: 15 additions & 2 deletions src/module.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { defineNuxtModule, addPlugin, createResolver, addImportsDir, addServerHandler } from '@nuxt/kit'
import { defineNuxtModule, addPlugin, createResolver, addImportsDir, addServerHandler, findPath } from '@nuxt/kit'
import { sha256 } from 'ohash'
import { defu } from 'defu'
import { resolve } from 'pathe'

// Module options TypeScript interface definition
export interface ModuleOptions {}
Expand All @@ -12,7 +13,7 @@ export default defineNuxtModule<ModuleOptions>({
},
// Default configuration options of the Nuxt module
defaults: {},
setup (options, nuxt) {
async setup (options, nuxt) {
const resolver = createResolver(import.meta.url)

if (!process.env.NUXT_SESSION_PASSWORD) {
Expand All @@ -22,6 +23,11 @@ export default defineNuxtModule<ModuleOptions>({
process.env.NUXT_SESSION_PASSWORD = randomPassword
}

// Allow user to define custom session/user
nuxt.options.watch.push('app/auth-utils-session.ts', 'app/auth-utils-session.js', 'app/auth-utils-session.mjs')

nuxt.options.alias['#auth-utils-session'] = await findFirstExisting(nuxt.options._layers.map(layer => resolve(layer.config.srcDir || layer.cwd, 'app/auth-utils-session'))) || resolver.resolve('./runtime/app/auth-utils-session')

// App
addImportsDir(resolver.resolve('./runtime/composables'))
addPlugin(resolver.resolve('./runtime/plugins/session.server'))
Expand Down Expand Up @@ -62,3 +68,10 @@ export default defineNuxtModule<ModuleOptions>({
})
}
})

async function findFirstExisting (paths: string[]) {
for (const path of paths) {
const resolvedPath = await findPath(path)
if (resolvedPath) { return resolvedPath }
}
}
9 changes: 9 additions & 0 deletions src/runtime/app/auth-utils-session.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
import { defineSession } from '../server/utils/session'

// Default session provider (only used when user does not provide their own provider or `onSuccess` handler)
export default defineSession((event, { provider, user }) => ({
user: {
[provider]: user
},
loggedInAt: new Date()
}))
6 changes: 3 additions & 3 deletions src/runtime/composables/session.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
import { useState, computed, useRequestFetch } from '#imports'
// import { UserSession } from '../server/utils/session'
interface UserSession {}
import type { default as UserSessionFactory } from '#auth-utils-session'
type UserSession = ReturnType<typeof UserSessionFactory>

const useSessionState = () => useState<UserSession>('nuxt-session', () => ({}))
const useSessionState = () => useState<UserSession | Record<string, unknown>>('nuxt-session', () => ({}))

export const useUserSession = () => {
const sessionState = useSessionState()
Expand Down
10 changes: 9 additions & 1 deletion src/runtime/server/lib/oauth/github.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@ import { eventHandler, createError, getQuery, getRequestURL, sendRedirect } from
import { ofetch } from 'ofetch'
import { withQuery } from 'ufo'
import { defu } from 'defu'
import { default as createUserSession } from '#auth-utils-session'
import { setUserSession } from '../../utils/session'

export interface OAuthGitHubConfig {
/**
Expand Down Expand Up @@ -43,7 +45,7 @@ export interface OAuthGitHubConfig {

interface OAuthConfig {
config?: OAuthGitHubConfig
onSuccess: (event: H3Event, result: { user: any, tokens: any }) => Promise<void> | void
onSuccess?: (event: H3Event, result: { user: any, tokens: any }) => Promise<void> | void
onError?: (event: H3Event, error: H3Error) => Promise<void> | void
}

Expand Down Expand Up @@ -127,6 +129,12 @@ export function githubEventHandler({ config, onSuccess, onError }: OAuthConfig)
user.email = primaryEmail.email
}

if (!onSuccess) {
const session = await createUserSession(event, { provider: 'github', user, tokens })
await setUserSession(event, session)
return sendRedirect(event, '/')
}

return onSuccess(event, {
user,
tokens,
Expand Down
9 changes: 8 additions & 1 deletion src/runtime/server/lib/oauth/spotify.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import { eventHandler, createError, getQuery, getRequestURL, sendRedirect } from
import { withQuery, parsePath } from 'ufo'
import { ofetch } from 'ofetch'
import { defu } from 'defu'
import { default as createUserSession } from '#auth-utils-session'

export interface OAuthSpotifyConfig {
/**
Expand Down Expand Up @@ -43,7 +44,7 @@ export interface OAuthSpotifyConfig {

interface OAuthConfig {
config?: OAuthSpotifyConfig
onSuccess: (event: H3Event, result: { user: any, tokens: any }) => Promise<void> | void
onSuccess?: (event: H3Event, result: { user: any, tokens: any }) => Promise<void> | void
onError?: (event: H3Event, error: H3Error) => Promise<void> | void
}

Expand Down Expand Up @@ -118,6 +119,12 @@ export function spotifyEventHandler({ config, onSuccess, onError }: OAuthConfig)
}
})

if (!onSuccess) {
const session = await createUserSession(event, { provider: 'spotify', user, tokens })
await setUserSession(event, session)
return sendRedirect(event, '/')
}

return onSuccess(event, {
tokens,
user
Expand Down
7 changes: 3 additions & 4 deletions src/runtime/server/utils/session.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,10 @@ import type { H3Event } from 'h3'
import { useSession, createError } from 'h3'
import { defu } from 'defu'
import { useRuntimeConfig } from '#imports'
import type { default as UserSessionFactory } from '#auth-utils-session'
type UserSession = ReturnType<typeof UserSessionFactory>

export interface UserSession {
user?: any
[key: string]: any
}
export const defineSession = <T extends Record<string, unknown> & { user?: unknown }>(definition: (event: H3Event, result: { provider: string, user: any, tokens: any }) => T) => definition

export async function getUserSession (event: H3Event) {
return (await _useSession(event)).data as UserSession
Expand Down

0 comments on commit 915e4f1

Please sign in to comment.