Skip to content

Commit fcc789a

Browse files
authored
Merge pull request #45 from allnightru/44-sanctum-token-auth
Add sanctum token auth support
2 parents fcf2458 + 2e07089 commit fcc789a

File tree

9 files changed

+161
-22
lines changed

9 files changed

+161
-22
lines changed

src/module.ts

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,8 @@ import {
66
addImportsDir,
77
} from '@nuxt/kit'
88
import { defu } from 'defu'
9-
import type { ModuleOptions } from './runtime/types'
9+
import type { ModuleOptions } from './runtime/types/options'
10+
import { registerTypeTemplates } from './templates'
1011

1112
const MODULE_NAME = 'nuxt-laravel-echo'
1213

@@ -19,6 +20,7 @@ const defaultModuleOptions: ModuleOptions = {
1920
scheme: 'https',
2021
transports: ['ws', 'wss'],
2122
authentication: {
23+
mode: 'cookie',
2224
baseUrl: 'http://localhost:80',
2325
authEndpoint: '/broadcasting/auth',
2426
csrfEndpoint: '/sanctum/csrf-cookie',
@@ -50,6 +52,8 @@ export default defineNuxtModule<ModuleOptions>({
5052
addPlugin(resolver.resolve('./runtime/plugin.client'))
5153
addImportsDir(resolver.resolve('./runtime/composables'))
5254

55+
registerTypeTemplates(resolver)
56+
5357
logger.info('Laravel Echo module initialized!')
5458
},
5559
})

src/runtime/composables/useEcho.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import type Echo from 'laravel-echo'
2-
import type { SupportedBroadcaster } from '../types'
2+
import type { SupportedBroadcaster } from '../types/options'
33
import { useNuxtApp } from '#app'
44

55
export const useEcho = (): Echo<SupportedBroadcaster> => {
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
import type { EchoAppConfig } from '../types/config'
2+
import { useAppConfig } from '#app'
3+
4+
export const useEchoAppConfig = (): EchoAppConfig => {
5+
return (useAppConfig().echo ?? {}) as EchoAppConfig
6+
}

src/runtime/composables/useEchoConfig.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import type { ModuleOptions } from '../types'
1+
import type { ModuleOptions } from '../types/options'
22
import { useRuntimeConfig } from '#imports'
33

44
export const useEchoConfig = (): ModuleOptions => {

src/runtime/plugin.client.ts

Lines changed: 64 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -4,8 +4,9 @@ import type { ChannelAuthorizationData } from 'pusher-js/types/src/core/auth/opt
44
import { type ConsolaInstance, createConsola } from 'consola'
55
import type { FetchOptions } from 'ofetch'
66
import { useEchoConfig } from './composables/useEchoConfig'
7-
import type { Authentication, ModuleOptions, SupportedBroadcaster } from './types'
8-
import { createError, defineNuxtPlugin, useCookie } from '#app'
7+
import type { Authentication, ModuleOptions, SupportedBroadcaster } from './types/options'
8+
import { useEchoAppConfig } from './composables/useEchoAppConfig'
9+
import { createError, defineNuxtPlugin, useCookie, updateAppConfig, type NuxtApp } from '#app'
910

1011
// eslint-disable-next-line @typescript-eslint/no-explicit-any
1112
const Pusher = (PusherPkg as any).default || PusherPkg
@@ -26,6 +27,7 @@ function createEchoLogger(logLevel: number) {
2627
const readCsrfCookie = (name: string) => useCookie(name, { readonly: true })
2728

2829
function createFetchClient(
30+
app: NuxtApp,
2931
authentication: Required<Authentication>,
3032
logger: ConsolaInstance
3133
) {
@@ -35,35 +37,55 @@ function createFetchClient(
3537
retry: false,
3638

3739
async onRequest(context) {
38-
let csrfToken = readCsrfCookie(authentication.csrfCookie)
40+
if (authentication.mode === 'cookie') {
41+
let csrfToken = readCsrfCookie(authentication.csrfCookie)
3942

40-
if (!csrfToken.value) {
41-
await $fetch(authentication.csrfEndpoint, {
42-
baseURL: authentication.baseUrl,
43-
credentials: 'include',
44-
retry: false,
45-
})
43+
if (!csrfToken.value) {
44+
await $fetch(authentication.csrfEndpoint, {
45+
baseURL: authentication.baseUrl,
46+
credentials: 'include',
47+
retry: false,
48+
})
4649

47-
csrfToken = readCsrfCookie(authentication.csrfCookie)
48-
}
50+
csrfToken = readCsrfCookie(authentication.csrfCookie)
51+
}
4952

50-
if (!csrfToken.value) {
51-
logger.warn(`${authentication.csrfCookie} cookie is missing, unable to set ${authentication.csrfHeader} header`)
52-
return
53+
if (!csrfToken.value) {
54+
logger.warn(`${authentication.csrfCookie} cookie is missing, unable to set ${authentication.csrfHeader} header`)
55+
return
56+
}
57+
58+
context.options.headers.set(authentication.csrfHeader, csrfToken.value)
5359
}
5460

55-
context.options.headers.set(authentication.csrfHeader, csrfToken.value)
61+
if (authentication.mode === 'token') {
62+
const { tokenStorage } = useEchoAppConfig()
63+
64+
if (!tokenStorage) {
65+
throw createError('Token storage is not defined')
66+
}
67+
68+
const token = await tokenStorage.get(app)
69+
70+
if (!token) {
71+
logger.debug('Authorization token is missing, unable to set header')
72+
return
73+
}
74+
75+
context.options.headers.set('Authorization', `Bearer ${token}`)
76+
}
5677
},
5778
}
5879

5980
return $fetch.create(fetchOptions)
6081
}
6182

6283
function createAuthorizer(
84+
app: NuxtApp,
6385
authentication: Required<Authentication>,
6486
logger: ConsolaInstance
6587
) {
66-
const client = createFetchClient(authentication, logger)
88+
const client = createFetchClient(app, authentication, logger)
6789

6890
return (channel: Channel, _: Options) => {
6991
return {
@@ -86,12 +108,13 @@ function createAuthorizer(
86108
}
87109
}
88110

89-
function prepareEchoOptions(config: ModuleOptions, logger: ConsolaInstance) {
111+
function prepareEchoOptions(app: NuxtApp, config: ModuleOptions, logger: ConsolaInstance) {
90112
const forceTLS = config.scheme === 'https'
91113
const additionalOptions = config.properties || {}
92114

93115
const authorizer = config.authentication
94116
? createAuthorizer(
117+
app,
95118
config.authentication as Required<Authentication>,
96119
logger
97120
)
@@ -127,12 +150,34 @@ function prepareEchoOptions(config: ModuleOptions, logger: ConsolaInstance) {
127150
}
128151
}
129152

130-
export default defineNuxtPlugin((_nuxtApp) => {
153+
async function setupDefaultTokenStorage(nuxtApp: NuxtApp, logger: ConsolaInstance) {
154+
logger.debug(
155+
'Token storage is not defined, switch to default cookie storage',
156+
)
157+
158+
const defaultStorage = await import('./storages/cookieTokenStorage')
159+
160+
nuxtApp.runWithContext(() => {
161+
updateAppConfig({
162+
echo: {
163+
tokenStorage: defaultStorage.cookieTokenStorage,
164+
},
165+
})
166+
})
167+
}
168+
169+
export default defineNuxtPlugin(async (_nuxtApp) => {
170+
const nuxtApp = _nuxtApp as NuxtApp
131171
const config = useEchoConfig()
172+
const appConfig = useEchoAppConfig()
132173
const logger = createEchoLogger(config.logLevel)
133174

175+
if (config.authentication?.mode === 'token' && !appConfig.tokenStorage) {
176+
await setupDefaultTokenStorage(nuxtApp, logger)
177+
}
178+
134179
window.Pusher = Pusher
135-
window.Echo = new Echo(prepareEchoOptions(config, logger))
180+
window.Echo = new Echo(prepareEchoOptions(nuxtApp, config, logger))
136181

137182
logger.debug('Laravel Echo client initialized')
138183

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
import { unref } from 'vue'
2+
import type { TokenStorage } from '../types/config'
3+
import { useCookie, type NuxtApp } from '#app'
4+
5+
const cookieTokenKey = 'sanctum.token.cookie'
6+
7+
/**
8+
* Token storage using a secure cookie.
9+
* Works with both CSR/SSR modes.
10+
*/
11+
export const cookieTokenStorage: TokenStorage = {
12+
async get(app: NuxtApp) {
13+
return app.runWithContext(() => {
14+
const cookie = useCookie(cookieTokenKey, { readonly: true })
15+
return unref(cookie.value) ?? undefined
16+
})
17+
},
18+
19+
async set(app: NuxtApp, token?: string) {
20+
await app.runWithContext(() => {
21+
const cookie = useCookie(cookieTokenKey, { secure: true })
22+
cookie.value = token
23+
})
24+
},
25+
}

src/runtime/types/config.ts

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
import type { NuxtApp } from '#app'
2+
3+
/**
4+
* Handlers to work with authentication token.
5+
*/
6+
export interface TokenStorage {
7+
/**
8+
* Function to load a token from the storage.
9+
*/
10+
get: (app: NuxtApp) => Promise<string | undefined>
11+
/**
12+
* Function to save a token to the storage.
13+
*/
14+
set: (app: NuxtApp, token?: string) => Promise<void>
15+
}
16+
17+
/**
18+
* Echo configuration for the application side with user-defined handlers.
19+
*/
20+
export interface EchoAppConfig {
21+
/**
22+
* Token storage handlers to be used by the client.
23+
*/
24+
tokenStorage?: TokenStorage
25+
}

src/runtime/types.ts renamed to src/runtime/types/options.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,11 @@
11
export type SupportedBroadcaster = 'reverb' | 'pusher'
22

33
export interface Authentication {
4+
/**
5+
* Authentication mode 'cookie' or 'token'
6+
* @default 'cookie'
7+
*/
8+
mode: 'cookie' | 'token'
49
/**
510
* The base URL of Laravel application.
611
* @default 'http://localhost:80'

src/templates.ts

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
import { addTypeTemplate, type Resolver } from '@nuxt/kit'
2+
3+
export const registerTypeTemplates = (resolver: Resolver) => {
4+
addTypeTemplate({
5+
filename: 'types/echo.d.ts',
6+
getContents: () => `// Generated by nuxt-laravel-echo module
7+
import type { EchoAppConfig } from '${resolver.resolve('./runtime/types/config.ts')}';
8+
9+
declare module 'nuxt/schema' {
10+
interface AppConfig {
11+
echo?: EchoAppConfig;
12+
}
13+
interface AppConfigInput {
14+
echo?: EchoAppConfig;
15+
}
16+
}
17+
18+
declare module '@nuxt/schema' {
19+
interface AppConfig {
20+
echo?: EchoAppConfig;
21+
}
22+
interface AppConfigInput {
23+
echo?: EchoAppConfig;
24+
}
25+
}
26+
27+
export {};`,
28+
})
29+
}

0 commit comments

Comments
 (0)