diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index d7094d5..61937bb 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -73,12 +73,6 @@ yarn lint yarn types ``` -To fix the issues automatically, you can run the following command: - -```bash -yarn fix:fmt -``` - # Releasing Once all the changes are merged into main branch, run the following command to release a new version: diff --git a/playground/error.vue b/playground/error.vue new file mode 100644 index 0000000..08e14bd --- /dev/null +++ b/playground/error.vue @@ -0,0 +1,46 @@ + + + + + diff --git a/playground/tsconfig.json b/playground/tsconfig.json index b575a24..f5f754b 100644 --- a/playground/tsconfig.json +++ b/playground/tsconfig.json @@ -1,3 +1,4 @@ { "extends": "./.nuxt/tsconfig.json", + "exclude": ["../dist"], } diff --git a/src/index.d.ts b/src/index.d.ts index cc7e98e..dd4e37e 100644 --- a/src/index.d.ts +++ b/src/index.d.ts @@ -5,3 +5,9 @@ declare module 'nuxt/schema' { sanctum: Partial; } } + +declare module '#app' { + interface PageMeta { + excludeFromSanctum?: boolean; + } +} diff --git a/src/module.ts b/src/module.ts index be66eb1..70dc799 100644 --- a/src/module.ts +++ b/src/module.ts @@ -8,7 +8,11 @@ import { import { defu } from 'defu'; import type { SanctumModuleOptions } from './types'; -export default defineNuxtModule>({ +type DeepPartial = { + [P in keyof T]?: T[P] extends object ? DeepPartial : T[P]; +}; + +export default defineNuxtModule>({ meta: { name: 'nuxt-auth-sanctum', configKey: 'sanctum', @@ -40,30 +44,40 @@ export default defineNuxtModule>({ onAuthOnly: '/login', onGuestOnly: '/', }, + globalMiddleware: { + enabled: false, + allow404WithoutAuth: true, + }, }, setup(options, nuxt) { const resolver = createResolver(import.meta.url); - const publicConfig = nuxt.options.runtimeConfig.public; - const userModuleConfig = publicConfig.sanctum; - - nuxt.options.runtimeConfig.public.sanctum = defu( - userModuleConfig as any, - options - ); + const runtimeConfigOverrides = + nuxt.options.runtimeConfig.public.sanctum; - addImportsDir(resolver.resolve('./runtime/composables')); + const sanctumConfig = defu(runtimeConfigOverrides as any, options); - addRouteMiddleware({ - name: 'sanctum:auth', - path: resolver.resolve('./runtime/middleware/sanctum.auth'), - }); - addRouteMiddleware({ - name: 'sanctum:guest', - path: resolver.resolve('./runtime/middleware/sanctum.guest'), - }); + nuxt.options.runtimeConfig.public.sanctum = sanctumConfig; addPlugin(resolver.resolve('./runtime/plugin')); + addImportsDir(resolver.resolve('./runtime/composables')); + + if (sanctumConfig.globalMiddleware.enabled) { + addRouteMiddleware({ + name: 'sanctum:auth:global', + path: resolver.resolve('./runtime/middleware/sanctum.global'), + global: true, + }); + } else { + addRouteMiddleware({ + name: 'sanctum:auth', + path: resolver.resolve('./runtime/middleware/sanctum.auth'), + }); + addRouteMiddleware({ + name: 'sanctum:guest', + path: resolver.resolve('./runtime/middleware/sanctum.guest'), + }); + } }, }); diff --git a/src/runtime/httpFactory.ts b/src/runtime/httpFactory.ts index 2945ec6..5ab71fd 100644 --- a/src/runtime/httpFactory.ts +++ b/src/runtime/httpFactory.ts @@ -3,14 +3,17 @@ import { useCookie, useRequestEvent, useRequestHeaders, + useRequestURL, navigateTo, useNuxtApp, } from '#app'; import { useSanctumUser } from './composables/useSanctumUser'; -import { useRequestURL } from 'nuxt/app'; import { useSanctumConfig } from './composables/useSanctumConfig'; -export const SECURE_METHODS = new Set(['post', 'delete', 'put', 'patch']); +type Headers = HeadersInit | undefined; + +const SECURE_METHODS = new Set(['post', 'delete', 'put', 'patch']); +const COOKIE_OPTIONS: { readonly: true } = { readonly: true }; export function createHttpClient(): $Fetch { const options = useSanctumConfig(); @@ -23,17 +26,13 @@ export function createHttpClient(): $Fetch { * @param headers Headers collection to extend * @returns {HeadersInit} */ - async function buildClientHeaders( - headers: HeadersInit | undefined - ): Promise { + async function buildClientHeaders(headers: Headers): Promise { await $fetch(options.endpoints.csrf, { baseURL: options.baseUrl, credentials: 'include', }); - const csrfToken = useCookie(options.csrf.cookie, { - readonly: true, - }).value; + const csrfToken = useCookie(options.csrf.cookie, COOKIE_OPTIONS).value; return { ...headers, @@ -46,10 +45,8 @@ export function createHttpClient(): $Fetch { * @param headers Headers collection to extend * @returns { HeadersInit } */ - function buildServerHeaders(headers: HeadersInit | undefined): HeadersInit { - const csrfToken = useCookie(options.csrf.cookie, { - readonly: true, - }).value; + function buildServerHeaders(headers: Headers): HeadersInit { + const csrfToken = useCookie(options.csrf.cookie, COOKIE_OPTIONS).value; const clientCookies = useRequestHeaders(['cookie']); const origin = options.origin ?? useRequestURL().origin; @@ -123,7 +120,8 @@ export function createHttpClient(): $Fetch { if ( options.redirect.onLogout === false || options.redirect.onLogout === currentRoute.path || - options.redirect.onAuthOnly === currentRoute.path + options.redirect.onAuthOnly === currentRoute.path || + options.globalMiddleware.enabled === true ) { return; } diff --git a/src/runtime/middleware/sanctum.auth.ts b/src/runtime/middleware/sanctum.auth.ts index 7aa8593..68b1aab 100644 --- a/src/runtime/middleware/sanctum.auth.ts +++ b/src/runtime/middleware/sanctum.auth.ts @@ -1,15 +1,13 @@ import { defineNuxtRouteMiddleware, navigateTo, createError } from '#app'; import type { RouteLocationRaw } from 'vue-router'; -import { useSanctumUser } from '../composables/useSanctumUser'; import { useSanctumConfig } from '../composables/useSanctumConfig'; +import { useSanctumAuth } from '../composables/useSanctumAuth'; export default defineNuxtRouteMiddleware((to) => { - const user = useSanctumUser(); const options = useSanctumConfig(); + const { isAuthenticated } = useSanctumAuth(); - const isAuthenticated = user.value !== null; - - if (isAuthenticated === true) { + if (isAuthenticated.value === true) { return; } diff --git a/src/runtime/middleware/sanctum.global.ts b/src/runtime/middleware/sanctum.global.ts new file mode 100644 index 0000000..9a81e67 --- /dev/null +++ b/src/runtime/middleware/sanctum.global.ts @@ -0,0 +1,53 @@ +import { defineNuxtRouteMiddleware, navigateTo } from '#app'; +import type { RouteLocationRaw } from 'vue-router'; +import { useSanctumConfig } from '../composables/useSanctumConfig'; +import { useSanctumAuth } from '../composables/useSanctumAuth'; + +export default defineNuxtRouteMiddleware((to) => { + const options = useSanctumConfig(); + const { isAuthenticated } = useSanctumAuth(); + + const [homePage, loginPage] = [ + options.redirect.onGuestOnly, + options.redirect.onAuthOnly, + ]; + + if (homePage === false) { + throw new Error( + 'You must define onGuestOnly route when using global middleware.' + ); + } + + if (loginPage === false) { + throw new Error( + 'You must define onAuthOnly route when using global middleware.' + ); + } + + if (isAuthenticated.value === true) { + if (to.path === loginPage) { + return navigateTo(homePage, { replace: true }); + } + + return; + } + + if (to.path === loginPage || to.meta.excludeFromSanctum === true) { + return; + } + + if ( + options.globalMiddleware.allow404WithoutAuth && + to.matched.length === 0 + ) { + return; + } + + const redirect: RouteLocationRaw = { path: loginPage }; + + if (options.redirect.keepRequestedRoute) { + redirect.query = { redirect: to.fullPath }; + } + + return navigateTo(redirect, { replace: true }); +}); diff --git a/src/runtime/middleware/sanctum.guest.ts b/src/runtime/middleware/sanctum.guest.ts index de44768..432c878 100644 --- a/src/runtime/middleware/sanctum.guest.ts +++ b/src/runtime/middleware/sanctum.guest.ts @@ -1,14 +1,12 @@ import { defineNuxtRouteMiddleware, navigateTo, createError } from '#app'; +import { useSanctumAuth } from '../composables/useSanctumAuth'; import { useSanctumConfig } from '../composables/useSanctumConfig'; -import { useSanctumUser } from '../composables/useSanctumUser'; export default defineNuxtRouteMiddleware(() => { - const user = useSanctumUser(); const options = useSanctumConfig(); + const { isAuthenticated } = useSanctumAuth(); - const isAuthenticated = user.value !== null; - - if (isAuthenticated === false) { + if (isAuthenticated.value === false) { return; } diff --git a/src/types.ts b/src/types.ts index c8f9a55..019c328 100644 --- a/src/types.ts +++ b/src/types.ts @@ -74,6 +74,20 @@ export interface RedirectOptions { onGuestOnly: string | false; } +/** + * Configuration of the global application-wide middleware. + */ +export interface GlobalMiddlewareOptions { + /** + * Determines whether the global middleware is enabled. + */ + enabled: boolean; + /** + * Determines whether to allow 404 pages without authentication. + */ + allow404WithoutAuth: boolean; +} + /** * Options to be passed to the plugin. */ @@ -110,4 +124,8 @@ export interface SanctumModuleOptions { * Behavior of the plugin redirects when user is authenticated or not. */ redirect: RedirectOptions; + /** + * Behavior of the global middleware. + */ + globalMiddleware: GlobalMiddlewareOptions; } diff --git a/tsconfig.json b/tsconfig.json index 4fd07c0..6966e2e 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -1,10 +1,4 @@ { "extends": "./.nuxt/tsconfig.json", - "exclude": [ - "./node_modules", - "./node_modules/nuxt/node_modules", - "./dist", - "./.output", - "./playground", - ], + "exclude": ["./node_modules", "./dist", "./playground"], }