diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index e89a0ea7..0f9dc43e 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -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 diff --git a/README.md b/README.md index 6e7f19a5..aef0146c 100644 --- a/README.md +++ b/README.md @@ -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.EventHandler({ onSuccess, config?, onError? })`, example: `oauth.githubEventHandler`. +The pattern is `oauth.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. diff --git a/playground/app/auth-utils-session.ts b/playground/app/auth-utils-session.ts new file mode 100644 index 00000000..e2b01743 --- /dev/null +++ b/playground/app/auth-utils-session.ts @@ -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 + }, +})) diff --git a/playground/server/routes/auth/github.get.ts b/playground/server/routes/auth/github.get.ts index e8599afd..845a976b 100644 --- a/playground/server/routes/auth/github.get.ts +++ b/playground/server/routes/auth/github.get.ts @@ -1,11 +1 @@ -export default oauth.githubEventHandler({ - async onSuccess(event, { user }) { - await setUserSession(event, { - user: { - github: user, - } - }) - - return sendRedirect(event, '/') - } -}) +export default oauth.githubEventHandler() diff --git a/playground/server/routes/auth/spotify.get.ts b/playground/server/routes/auth/spotify.get.ts index a33eaf08..4b97dda0 100644 --- a/playground/server/routes/auth/spotify.get.ts +++ b/playground/server/routes/auth/spotify.get.ts @@ -1,11 +1 @@ -export default oauth.spotifyEventHandler({ - async onSuccess(event, { user }) { - await setUserSession(event, { - user: { - spotify: user, - } - }) - - return sendRedirect(event, '/') - } -}) +export default oauth.spotifyEventHandler() diff --git a/src/module.ts b/src/module.ts index 99e72af4..0c0fb80f 100644 --- a/src/module.ts +++ b/src/module.ts @@ -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 {} @@ -12,7 +13,7 @@ export default defineNuxtModule({ }, // 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) { @@ -22,6 +23,11 @@ export default defineNuxtModule({ 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')) @@ -62,3 +68,10 @@ export default defineNuxtModule({ }) } }) + +async function findFirstExisting (paths: string[]) { + for (const path of paths) { + const resolvedPath = await findPath(path) + if (resolvedPath) { return resolvedPath } + } +} diff --git a/src/runtime/app/auth-utils-session.ts b/src/runtime/app/auth-utils-session.ts new file mode 100644 index 00000000..ead1ed65 --- /dev/null +++ b/src/runtime/app/auth-utils-session.ts @@ -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() +})) diff --git a/src/runtime/composables/session.ts b/src/runtime/composables/session.ts index dd96923d..dd8a2fbb 100644 --- a/src/runtime/composables/session.ts +++ b/src/runtime/composables/session.ts @@ -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 -const useSessionState = () => useState('nuxt-session', () => ({})) +const useSessionState = () => useState>('nuxt-session', () => ({})) export const useUserSession = () => { const sessionState = useSessionState() diff --git a/src/runtime/server/lib/oauth/github.ts b/src/runtime/server/lib/oauth/github.ts index c5802495..7b8d4826 100644 --- a/src/runtime/server/lib/oauth/github.ts +++ b/src/runtime/server/lib/oauth/github.ts @@ -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 { /** @@ -43,7 +45,7 @@ export interface OAuthGitHubConfig { interface OAuthConfig { config?: OAuthGitHubConfig - onSuccess: (event: H3Event, result: { user: any, tokens: any }) => Promise | void + onSuccess?: (event: H3Event, result: { user: any, tokens: any }) => Promise | void onError?: (event: H3Event, error: H3Error) => Promise | void } @@ -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, diff --git a/src/runtime/server/lib/oauth/spotify.ts b/src/runtime/server/lib/oauth/spotify.ts index ba6b4e1b..14d6d20e 100644 --- a/src/runtime/server/lib/oauth/spotify.ts +++ b/src/runtime/server/lib/oauth/spotify.ts @@ -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 { /** @@ -43,7 +44,7 @@ export interface OAuthSpotifyConfig { interface OAuthConfig { config?: OAuthSpotifyConfig - onSuccess: (event: H3Event, result: { user: any, tokens: any }) => Promise | void + onSuccess?: (event: H3Event, result: { user: any, tokens: any }) => Promise | void onError?: (event: H3Event, error: H3Error) => Promise | void } @@ -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 diff --git a/src/runtime/server/utils/session.ts b/src/runtime/server/utils/session.ts index b87fe8f8..1d1b4623 100644 --- a/src/runtime/server/utils/session.ts +++ b/src/runtime/server/utils/session.ts @@ -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 -export interface UserSession { - user?: any - [key: string]: any -} +export const defineSession = & { 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