From 1d07784f38e39060ae8307f445e451adfb8ac0ce Mon Sep 17 00:00:00 2001 From: Kirill Petrov Date: Wed, 30 Oct 2024 15:34:11 +0700 Subject: [PATCH 1/5] add sanctum token auth --- src/runtime/plugin.client.ts | 65 ++++++++++++++++------ src/runtime/storages/cookieTokenStorage.ts | 25 +++++++++ src/runtime/types.ts | 16 ++++++ 3 files changed, 90 insertions(+), 16 deletions(-) create mode 100644 src/runtime/storages/cookieTokenStorage.ts diff --git a/src/runtime/plugin.client.ts b/src/runtime/plugin.client.ts index 75baadd..27ca097 100644 --- a/src/runtime/plugin.client.ts +++ b/src/runtime/plugin.client.ts @@ -35,24 +35,34 @@ function createFetchClient( retry: false, async onRequest(context) { - let csrfToken = readCsrfCookie(authentication.csrfCookie) - - if (!csrfToken.value) { - await $fetch(authentication.csrfEndpoint, { - baseURL: authentication.baseUrl, - credentials: 'include', - retry: false, - }) - - csrfToken = readCsrfCookie(authentication.csrfCookie) + // todo: move this to interceptors + if (authentication.mode === 'token') { + let csrfToken = readCsrfCookie(authentication.csrfCookie) + + if (!csrfToken.value) { + await $fetch(authentication.csrfEndpoint, { + baseURL: authentication.baseUrl, + credentials: 'include', + retry: false, + }) + + csrfToken = readCsrfCookie(authentication.csrfCookie) + } + + if (!csrfToken.value) { + logger.warn(`${authentication.csrfCookie} cookie is missing, unable to set ${authentication.csrfHeader} header`) + return + } + + context.options.headers.set(authentication.csrfHeader, csrfToken.value) } - if (!csrfToken.value) { - logger.warn(`${authentication.csrfCookie} cookie is missing, unable to set ${authentication.csrfHeader} header`) - return + // todo: move this to interceptors + if (authentication.mode === 'token') { + const { tokenStorage } = useAppConfig().echo.authentication + const token = await tokenStorage.get() + context.options.headers.set('Authorization', 'Bearer ' + token) } - - context.options.headers.set(authentication.csrfHeader, csrfToken.value) }, } @@ -127,10 +137,33 @@ function prepareEchoOptions(config: ModuleOptions, logger: ConsolaInstance) { } } -export default defineNuxtPlugin((_nuxtApp) => { +async function setupDefaultTokenStorage(nuxtApp: NuxtApp, logger: ConsolaInstance) { + logger.debug( + 'Token storage is not defined, switch to default cookie storage', + ) + + const defaultStorage = await import('./storages/cookieTokenStorage') + + nuxtApp.runWithContext(() => { + updateAppConfig({ + echo: { + authentication: { + tokenStorage: defaultStorage.cookieTokenStorage, + } + }, + }) + }) +} + +export default defineNuxtPlugin(async (_nuxtApp) => { const config = useEchoConfig() + const appConfig = useAppConfig() const logger = createEchoLogger(config.logLevel) + if (config.authentication?.mode === 'token' && !appConfig.echo?.authentication?.tokenStorage) { + await setupDefaultTokenStorage(_nuxtApp, logger) + } + window.Pusher = Pusher window.Echo = new Echo(prepareEchoOptions(config, logger)) diff --git a/src/runtime/storages/cookieTokenStorage.ts b/src/runtime/storages/cookieTokenStorage.ts new file mode 100644 index 0000000..d646d72 --- /dev/null +++ b/src/runtime/storages/cookieTokenStorage.ts @@ -0,0 +1,25 @@ +import { unref } from 'vue' +import type { TokenStorage } from '~/src/runtime/types' +import { useCookie, type NuxtApp } from '#app' + +const cookieTokenKey = 'sanctum.token.cookie' + +/** + * Token storage using a secure cookie. + * Works with both CSR/SSR modes. + */ +export const cookieTokenStorage: TokenStorage = { + async get(app: NuxtApp) { + return app.runWithContext(() => { + const cookie = useCookie(cookieTokenKey, { readonly: true }) + return unref(cookie.value) ?? undefined + }) + }, + + async set(app: NuxtApp, token?: string) { + await app.runWithContext(() => { + const cookie = useCookie(cookieTokenKey, { secure: true }) + cookie.value = token + }) + }, +} diff --git a/src/runtime/types.ts b/src/runtime/types.ts index c860adc..0ace17d 100644 --- a/src/runtime/types.ts +++ b/src/runtime/types.ts @@ -1,4 +1,9 @@ export interface Authentication { + /** + * Authentication mode 'cookie' or 'token' + * @default 'cookie' + */ + mode: 'cookie' | 'token' /** * The base URL of Laravel application. * @default 'http://localhost:80' @@ -86,3 +91,14 @@ export interface ModuleOptions { */ properties?: object } + +export interface TokenStorage { + /** + * Function to load a token from the storage. + */ + get: (app: NuxtApp) => Promise + /** + * Function to save a token to the storage. + */ + set: (app: NuxtApp, token?: string) => Promise +} From 9b6f061140c8a345120f8c90fbe36946ca420be9 Mon Sep 17 00:00:00 2001 From: Kirill Petrov Date: Wed, 30 Oct 2024 15:35:46 +0700 Subject: [PATCH 2/5] fix: add sanctum token auth --- src/module.ts | 1 + src/runtime/plugin.client.ts | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/src/module.ts b/src/module.ts index 02e2fdd..0003fc3 100644 --- a/src/module.ts +++ b/src/module.ts @@ -19,6 +19,7 @@ const defaultModuleOptions: ModuleOptions = { scheme: 'https', transports: ['ws', 'wss'], authentication: { + mode: 'cookie', baseUrl: 'http://localhost:80', authEndpoint: '/broadcasting/auth', csrfEndpoint: '/sanctum/csrf-cookie', diff --git a/src/runtime/plugin.client.ts b/src/runtime/plugin.client.ts index 27ca097..7f7da4c 100644 --- a/src/runtime/plugin.client.ts +++ b/src/runtime/plugin.client.ts @@ -36,7 +36,7 @@ function createFetchClient( async onRequest(context) { // todo: move this to interceptors - if (authentication.mode === 'token') { + if (authentication.mode === 'cookie') { let csrfToken = readCsrfCookie(authentication.csrfCookie) if (!csrfToken.value) { From 65efc1a6fc3667a965f183fcd1fbd20f053d997f Mon Sep 17 00:00:00 2001 From: Kirill Petrov Date: Wed, 30 Oct 2024 15:51:23 +0700 Subject: [PATCH 3/5] fix: add tokenStorage parameter into types --- src/runtime/types.ts | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/runtime/types.ts b/src/runtime/types.ts index 0ace17d..1b9d593 100644 --- a/src/runtime/types.ts +++ b/src/runtime/types.ts @@ -29,6 +29,10 @@ export interface Authentication { * @default 'X-XSRF-TOKEN' */ csrfHeader?: string + /** + * Token storage handlers to be used by the client. + */ + tokenStorage?: TokenStorage } export interface ModuleOptions { From 392a7c516d3fe8e8ac2b5fc94186cac3d556dad2 Mon Sep 17 00:00:00 2001 From: manchenkoff Date: Thu, 19 Dec 2024 23:42:21 +0100 Subject: [PATCH 4/5] feat: add support for token authentication mode --- package.json | 2 +- src/module.ts | 5 ++- src/runtime/composables/useEchoAppConfig.ts | 6 +++ src/runtime/composables/useEchoConfig.ts | 2 +- src/runtime/plugin.client.ts | 44 +++++++++++++-------- src/runtime/storages/cookieTokenStorage.ts | 2 +- src/runtime/types/config.ts | 25 ++++++++++++ src/runtime/{types.ts => types/options.ts} | 15 ------- src/templates.ts | 29 ++++++++++++++ 9 files changed, 95 insertions(+), 35 deletions(-) create mode 100644 src/runtime/composables/useEchoAppConfig.ts create mode 100644 src/runtime/types/config.ts rename src/runtime/{types.ts => types/options.ts} (84%) create mode 100644 src/templates.ts diff --git a/package.json b/package.json index b3939ba..e157344 100644 --- a/package.json +++ b/package.json @@ -59,5 +59,5 @@ "vitest": "^2.0.5", "vue-tsc": "^2.1.2" }, - "packageManager": "pnpm@9.6.0+sha512.38dc6fba8dba35b39340b9700112c2fe1e12f10b17134715a4aa98ccf7bb035e76fd981cf0bb384dfa98f8d6af5481c2bef2f4266a24bfa20c34eb7147ce0b5e" + "packageManager": "pnpm@9.15.0+sha512.76e2379760a4328ec4415815bcd6628dee727af3779aaa4c914e3944156c4299921a89f976381ee107d41f12cfa4b66681ca9c718f0668fa0831ed4c6d8ba56c" } diff --git a/src/module.ts b/src/module.ts index 0003fc3..724688a 100644 --- a/src/module.ts +++ b/src/module.ts @@ -6,7 +6,8 @@ import { addImportsDir, } from '@nuxt/kit' import { defu } from 'defu' -import type { ModuleOptions } from './runtime/types' +import type { ModuleOptions } from './runtime/types/options' +import { registerTypeTemplates } from './templates' const MODULE_NAME = 'nuxt-laravel-echo' @@ -51,6 +52,8 @@ export default defineNuxtModule({ addPlugin(resolver.resolve('./runtime/plugin.client')) addImportsDir(resolver.resolve('./runtime/composables')) + registerTypeTemplates(resolver) + logger.info('Laravel Echo module initialized!') }, }) diff --git a/src/runtime/composables/useEchoAppConfig.ts b/src/runtime/composables/useEchoAppConfig.ts new file mode 100644 index 0000000..7208ec9 --- /dev/null +++ b/src/runtime/composables/useEchoAppConfig.ts @@ -0,0 +1,6 @@ +import type { EchoAppConfig } from '../types/config' +import { useAppConfig } from '#app' + +export const useEchoAppConfig = (): EchoAppConfig => { + return (useAppConfig().echo ?? {}) as EchoAppConfig +} diff --git a/src/runtime/composables/useEchoConfig.ts b/src/runtime/composables/useEchoConfig.ts index f1eeb22..4ebc8f8 100644 --- a/src/runtime/composables/useEchoConfig.ts +++ b/src/runtime/composables/useEchoConfig.ts @@ -1,4 +1,4 @@ -import type { ModuleOptions } from '../types' +import type { ModuleOptions } from '../types/options' import { useRuntimeConfig } from '#imports' export const useEchoConfig = (): ModuleOptions => { diff --git a/src/runtime/plugin.client.ts b/src/runtime/plugin.client.ts index 7f7da4c..e97f3cf 100644 --- a/src/runtime/plugin.client.ts +++ b/src/runtime/plugin.client.ts @@ -4,8 +4,9 @@ import type { ChannelAuthorizationData } from 'pusher-js/types/src/core/auth/opt import { type ConsolaInstance, createConsola } from 'consola' import type { FetchOptions } from 'ofetch' import { useEchoConfig } from './composables/useEchoConfig' -import type { Authentication, ModuleOptions } from './types' -import { createError, defineNuxtPlugin, useCookie } from '#app' +import type { Authentication, ModuleOptions } from './types/options' +import { useEchoAppConfig } from './composables/useEchoAppConfig' +import { createError, defineNuxtPlugin, useCookie, updateAppConfig, type NuxtApp } from '#app' // eslint-disable-next-line @typescript-eslint/no-explicit-any const Pusher = (PusherPkg as any).default || PusherPkg @@ -26,6 +27,7 @@ function createEchoLogger(logLevel: number) { const readCsrfCookie = (name: string) => useCookie(name, { readonly: true }) function createFetchClient( + app: NuxtApp, authentication: Required, logger: ConsolaInstance ) { @@ -35,7 +37,6 @@ function createFetchClient( retry: false, async onRequest(context) { - // todo: move this to interceptors if (authentication.mode === 'cookie') { let csrfToken = readCsrfCookie(authentication.csrfCookie) @@ -57,11 +58,21 @@ function createFetchClient( context.options.headers.set(authentication.csrfHeader, csrfToken.value) } - // todo: move this to interceptors if (authentication.mode === 'token') { - const { tokenStorage } = useAppConfig().echo.authentication - const token = await tokenStorage.get() - context.options.headers.set('Authorization', 'Bearer ' + token) + const { tokenStorage } = useEchoAppConfig() + + if (!tokenStorage) { + throw createError('Token storage is not defined') + } + + const token = await tokenStorage.get(app) + + if (!token) { + logger.debug('Authorization token is missing, unable to set header') + return + } + + context.options.headers.set('Authorization', `Bearer ${token}`) } }, } @@ -70,10 +81,11 @@ function createFetchClient( } function createAuthorizer( + app: NuxtApp, authentication: Required, logger: ConsolaInstance ) { - const client = createFetchClient(authentication, logger) + const client = createFetchClient(app, authentication, logger) return (channel: Channel, _: Options) => { return { @@ -96,12 +108,13 @@ function createAuthorizer( } } -function prepareEchoOptions(config: ModuleOptions, logger: ConsolaInstance) { +function prepareEchoOptions(app: NuxtApp, config: ModuleOptions, logger: ConsolaInstance) { const forceTLS = config.scheme === 'https' const additionalOptions = config.properties || {} const authorizer = config.authentication ? createAuthorizer( + app, config.authentication as Required, logger ) @@ -147,25 +160,24 @@ async function setupDefaultTokenStorage(nuxtApp: NuxtApp, logger: ConsolaInstanc nuxtApp.runWithContext(() => { updateAppConfig({ echo: { - authentication: { - tokenStorage: defaultStorage.cookieTokenStorage, - } + tokenStorage: defaultStorage.cookieTokenStorage, }, }) }) } export default defineNuxtPlugin(async (_nuxtApp) => { + const nuxtApp = _nuxtApp as NuxtApp const config = useEchoConfig() - const appConfig = useAppConfig() + const appConfig = useEchoAppConfig() const logger = createEchoLogger(config.logLevel) - if (config.authentication?.mode === 'token' && !appConfig.echo?.authentication?.tokenStorage) { - await setupDefaultTokenStorage(_nuxtApp, logger) + if (config.authentication?.mode === 'token' && !appConfig.tokenStorage) { + await setupDefaultTokenStorage(nuxtApp, logger) } window.Pusher = Pusher - window.Echo = new Echo(prepareEchoOptions(config, logger)) + window.Echo = new Echo(prepareEchoOptions(nuxtApp, config, logger)) logger.debug('Laravel Echo client initialized') diff --git a/src/runtime/storages/cookieTokenStorage.ts b/src/runtime/storages/cookieTokenStorage.ts index d646d72..6312480 100644 --- a/src/runtime/storages/cookieTokenStorage.ts +++ b/src/runtime/storages/cookieTokenStorage.ts @@ -1,5 +1,5 @@ import { unref } from 'vue' -import type { TokenStorage } from '~/src/runtime/types' +import type { TokenStorage } from '../types/config' import { useCookie, type NuxtApp } from '#app' const cookieTokenKey = 'sanctum.token.cookie' diff --git a/src/runtime/types/config.ts b/src/runtime/types/config.ts new file mode 100644 index 0000000..07d0c0e --- /dev/null +++ b/src/runtime/types/config.ts @@ -0,0 +1,25 @@ +import type { NuxtApp } from '#app' + +/** + * Handlers to work with authentication token. + */ +export interface TokenStorage { + /** + * Function to load a token from the storage. + */ + get: (app: NuxtApp) => Promise + /** + * Function to save a token to the storage. + */ + set: (app: NuxtApp, token?: string) => Promise +} + +/** + * Echo configuration for the application side with user-defined handlers. + */ +export interface EchoAppConfig { + /** + * Token storage handlers to be used by the client. + */ + tokenStorage?: TokenStorage +} diff --git a/src/runtime/types.ts b/src/runtime/types/options.ts similarity index 84% rename from src/runtime/types.ts rename to src/runtime/types/options.ts index 1b9d593..8dc1392 100644 --- a/src/runtime/types.ts +++ b/src/runtime/types/options.ts @@ -29,10 +29,6 @@ export interface Authentication { * @default 'X-XSRF-TOKEN' */ csrfHeader?: string - /** - * Token storage handlers to be used by the client. - */ - tokenStorage?: TokenStorage } export interface ModuleOptions { @@ -95,14 +91,3 @@ export interface ModuleOptions { */ properties?: object } - -export interface TokenStorage { - /** - * Function to load a token from the storage. - */ - get: (app: NuxtApp) => Promise - /** - * Function to save a token to the storage. - */ - set: (app: NuxtApp, token?: string) => Promise -} diff --git a/src/templates.ts b/src/templates.ts new file mode 100644 index 0000000..6708cf4 --- /dev/null +++ b/src/templates.ts @@ -0,0 +1,29 @@ +import { addTypeTemplate, type Resolver } from '@nuxt/kit' + +export const registerTypeTemplates = (resolver: Resolver) => { + addTypeTemplate({ + filename: 'types/echo.d.ts', + getContents: () => `// Generated by nuxt-laravel-echo module +import type { EchoAppConfig } from '${resolver.resolve('./runtime/types/config.ts')}'; + +declare module 'nuxt/schema' { + interface AppConfig { + echo?: EchoAppConfig; + } + interface AppConfigInput { + echo?: EchoAppConfig; + } +} + +declare module '@nuxt/schema' { + interface AppConfig { + echo?: EchoAppConfig; + } + interface AppConfigInput { + echo?: EchoAppConfig; + } +} + +export {};`, + }) +} From 2e070890581320da349dff8c6e47813f8b214442 Mon Sep 17 00:00:00 2001 From: manchenkoff Date: Thu, 19 Dec 2024 23:51:57 +0100 Subject: [PATCH 5/5] merged main branch --- src/runtime/composables/useEcho.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/runtime/composables/useEcho.ts b/src/runtime/composables/useEcho.ts index 1250c5c..ed9a79c 100644 --- a/src/runtime/composables/useEcho.ts +++ b/src/runtime/composables/useEcho.ts @@ -1,5 +1,5 @@ import type Echo from 'laravel-echo' -import type { SupportedBroadcaster } from '../types' +import type { SupportedBroadcaster } from '../types/options' import { useNuxtApp } from '#app' export const useEcho = (): Echo => {