diff --git a/playground-local/app.vue b/playground-local/app.vue index 1f770a6f..982c2c9a 100644 --- a/playground-local/app.vue +++ b/playground-local/app.vue @@ -41,9 +41,23 @@ const password = ref('hunter2') refresh session (required: true)
- - navigate to Login Page - +

Navigation

+

Navigate to different pages below to test out different things:

+
+ + -> API endpoint protected inline + +
+ + -> API endpoint protected middleware + +
+ + -> navigate to Login Page + +
+
+
diff --git a/playground-local/server/api/protected/inline.ts b/playground-local/server/api/protected/inline.ts new file mode 100644 index 00000000..8a7e77d9 --- /dev/null +++ b/playground-local/server/api/protected/inline.ts @@ -0,0 +1,10 @@ +import { eventHandler } from 'h3' +import { getServerSession } from '#auth' + +export default eventHandler(async (event) => { + const session = await getServerSession(event) + if (!session) { + return { status: 'unauthenticated!' } + } + return { status: 'authenticated!', text: 'im protected by an in-endpoint check', session } +}) diff --git a/playground-local/server/api/protected/middleware.ts b/playground-local/server/api/protected/middleware.ts new file mode 100644 index 00000000..485da1ac --- /dev/null +++ b/playground-local/server/api/protected/middleware.ts @@ -0,0 +1,3 @@ +import { eventHandler } from 'h3' + +export default eventHandler(() => ({ status: 'authenticated', text: 'you only see me if you are logged in, as a server-middleware protects me' })) diff --git a/playground-local/server/api/session.get.ts b/playground-local/server/api/session.get.ts new file mode 100644 index 00000000..5d5e4b5b --- /dev/null +++ b/playground-local/server/api/session.get.ts @@ -0,0 +1,4 @@ +import { defineEventHandler } from 'h3' +import { getServerSession } from '#auth' + +export default defineEventHandler(event => getServerSession(event)) diff --git a/playground-local/server/api/token.get.ts b/playground-local/server/api/token.get.ts new file mode 100644 index 00000000..cd349d63 --- /dev/null +++ b/playground-local/server/api/token.get.ts @@ -0,0 +1,4 @@ +import { defineEventHandler } from 'h3' +import { getToken } from '#auth' + +export default defineEventHandler(event => getToken(event)) diff --git a/playground-local/server/middleware/auth.ts b/playground-local/server/middleware/auth.ts new file mode 100644 index 00000000..8b6ce9a9 --- /dev/null +++ b/playground-local/server/middleware/auth.ts @@ -0,0 +1,14 @@ +import { createError, eventHandler } from 'h3' +import { getServerSession } from '#auth' + +export default eventHandler(async (event) => { + // Only protect a certain backend route + if (!event.node.req.url?.startsWith('/api/protected/middleware')) { + return + } + + const session = await getServerSession(event) + if (!session) { + throw createError({ statusMessage: 'Unauthenticated', statusCode: 403 }) + } +}) diff --git a/src/module.ts b/src/module.ts index 1ec9f43a..bbbe2f53 100644 --- a/src/module.ts +++ b/src/module.ts @@ -12,7 +12,6 @@ import { } from '@nuxt/kit' import { defu } from 'defu' import { joinURL } from 'ufo' -import { genInterface } from 'knitwork' import type { DeepRequired } from 'ts-essentials' import type { NuxtModule } from 'nuxt/schema' import { getOriginAndPathnameFromURL, isProduction } from './runtime/helpers' @@ -23,6 +22,7 @@ import type { RefreshHandler, SupportedAuthProviders } from './runtime/types' +import { generateModuleTypes } from './runtime/utils/generateTypes' const topLevelDefaults = { isEnabled: true, @@ -101,7 +101,10 @@ const PACKAGE_NAME = 'sidebase-auth' export default defineNuxtModule({ meta: { name: PACKAGE_NAME, - configKey: 'auth' + configKey: 'auth', + compatibility: { + nuxt: '>=3.0.0' + } }, setup(userOptions, nuxt) { const logger = useLogger(PACKAGE_NAME) @@ -178,26 +181,13 @@ export default defineNuxtModule({ inline: [resolve('./runtime')] } ) - nitroConfig.alias['#auth'] = resolve('./runtime/server/services') + + nitroConfig.alias['#auth'] = resolve(`./runtime/server/services/${options.provider.type}`) }) addTypeTemplate({ filename: 'types/auth.d.ts', - getContents: () => - [ - '// AUTO-GENERATED BY @sidebase/nuxt-auth', - 'declare module \'#auth\' {', - ` const { getServerSession, getToken, NuxtAuthHandler }: typeof import('${resolve('./runtime/server/services')}')`, - ...(options.provider.type === 'local' - ? [genInterface( - 'SessionData', - (options.provider as any).session.dataType - )] - : [] - ), - '}', - '' - ].join('\n') + getContents: () => generateModuleTypes(options.provider) }) addTypeTemplate({ diff --git a/src/runtime/server/services/authjs/index.ts b/src/runtime/server/services/authjs/index.ts new file mode 100644 index 00000000..f6ee14c7 --- /dev/null +++ b/src/runtime/server/services/authjs/index.ts @@ -0,0 +1 @@ +export { NuxtAuthHandler, getServerSession, getToken } from './nuxtAuthHandler' diff --git a/src/runtime/server/services/index.ts b/src/runtime/server/services/index.ts deleted file mode 100644 index 423ab845..00000000 --- a/src/runtime/server/services/index.ts +++ /dev/null @@ -1 +0,0 @@ -export { NuxtAuthHandler, getServerSession, getToken } from './authjs/nuxtAuthHandler' diff --git a/src/runtime/server/services/local/getServerSession.ts b/src/runtime/server/services/local/getServerSession.ts new file mode 100644 index 00000000..7147767a --- /dev/null +++ b/src/runtime/server/services/local/getServerSession.ts @@ -0,0 +1,51 @@ +import { type H3Event, createError } from 'h3' +import getURL from 'requrl' +import { joinURL } from 'ufo' +import { jsonPointerGet, useTypedBackendConfig } from '../../../helpers' +import { getToken } from './getToken' + +// @ts-expect-error - #auth not defined +import type { SessionData } from '#auth' +import { useRuntimeConfig } from '#imports' + +function joinPathToApiURL(event: H3Event, path: string) { + const { origin, pathname, fullBaseUrl } = useRuntimeConfig().public.auth.computed + + let baseURL + if (origin) { + // Case 1: An origin was supplied by the developer in the runtime-config. Use it by returning the already assembled full base url that contains it + baseURL = fullBaseUrl + } + else { + // Case 2: An origin was not supplied, we determine it from the request + const determinedOrigin = getURL(event.node.req, false) + baseURL = joinURL(determinedOrigin, pathname) + } + + const base = path.startsWith('/') ? pathname : baseURL + return joinURL(base, path) +} + +export async function getServerSession(event: H3Event): Promise { + const token = getToken(event) + if (!token) { + return null + } + + const config = useTypedBackendConfig(useRuntimeConfig(), 'local') + const { path, method } = config.endpoints.getSession + + // Compose heads to request the session + const headers = new Headers({ [config.token.headerName]: token } as HeadersInit) + + try { + const url = joinPathToApiURL(event, path) + const result = await $fetch(url, { method, headers }) + const { dataResponsePointer: sessionDataResponsePointer } = config.session + return jsonPointerGet(result, sessionDataResponsePointer) + } + catch (err) { + console.error(err) + throw createError({ statusCode: 401, statusMessage: 'Session could not be retrieved.' }) + } +} diff --git a/src/runtime/server/services/local/getToken.ts b/src/runtime/server/services/local/getToken.ts new file mode 100644 index 00000000..2a269607 --- /dev/null +++ b/src/runtime/server/services/local/getToken.ts @@ -0,0 +1,15 @@ +import { type H3Event, getCookie } from 'h3' +import { useTypedBackendConfig } from '../../../helpers' +import { formatToken } from '../../../utils/local' +import { useRuntimeConfig } from '#imports' + +export function getToken(event: H3Event) { + const config = useTypedBackendConfig(useRuntimeConfig(), 'local') + const rawToken = getCookie(event, config.token.cookieName) + const token = formatToken(rawToken, config) + + if (!token) { + return null + } + return token +} diff --git a/src/runtime/server/services/local/index.ts b/src/runtime/server/services/local/index.ts new file mode 100644 index 00000000..d6d0f4b0 --- /dev/null +++ b/src/runtime/server/services/local/index.ts @@ -0,0 +1,2 @@ +export { getToken } from './getToken' +export { getServerSession } from './getServerSession' diff --git a/src/runtime/utils/generateTypes.ts b/src/runtime/utils/generateTypes.ts new file mode 100644 index 00000000..7834fd3d --- /dev/null +++ b/src/runtime/utils/generateTypes.ts @@ -0,0 +1,26 @@ +import { genInterface } from 'knitwork' +import { createResolver } from '@nuxt/kit' +import type { AuthProviders } from '../types' + +export function generateModuleTypes(provider: AuthProviders) { + const { resolve } = createResolver(import.meta.url) + + const providerSpecificTypes: string[] = [] + + if (provider.type === 'authjs') { + providerSpecificTypes.push(` const { getServerSession, getToken, NuxtAuthHandler }: typeof import('${resolve('./runtime/server/services/authjs')}')`) + } + + if (provider.type === 'local') { + providerSpecificTypes.push(` const { getServerSession, getToken }: typeof import('${resolve('./runtime/server/services/local')}')`) + providerSpecificTypes.push(genInterface('SessionData', (provider as any).session.dataType)) + } + + return [ + '// AUTO-GENERATED BY @sidebase/nuxt-auth', + `declare module '#auth' {`, + ...providerSpecificTypes, + '}', + '' + ].join('\n') +}