Skip to content

Commit

Permalink
Merge pull request #76 from manchenkoff/51-feature-add-module-logger-…
Browse files Browse the repository at this point in the history
…with-optional-configuration

feat: add module logger with optional configuration
  • Loading branch information
manchenkoff authored Apr 17, 2024
2 parents e400d2a + 79e2558 commit 0d58e6e
Show file tree
Hide file tree
Showing 7 changed files with 83 additions and 9 deletions.
1 change: 1 addition & 0 deletions playground/nuxt.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ export default defineNuxtConfig({

sanctum: {
baseUrl: 'http://localhost:80',
logLevel: 5,
redirect: {
keepRequestedRoute: true,
onAuthOnly: '/login',
Expand Down
1 change: 1 addition & 0 deletions src/constants.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export const LOGGER_NAME = 'nuxt-auth-sanctum';
11 changes: 11 additions & 0 deletions src/module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,11 @@ import {
createResolver,
addImportsDir,
addRouteMiddleware,
useLogger,
} from '@nuxt/kit';
import { defu } from 'defu';
import type { SanctumModuleOptions } from './types';
import { LOGGER_NAME } from './constants';

type DeepPartial<T> = {
[P in keyof T]?: T[P] extends object ? DeepPartial<T[P]> : T[P];
Expand Down Expand Up @@ -48,6 +50,7 @@ export default defineNuxtModule<DeepPartial<SanctumModuleOptions>>({
enabled: false,
allow404WithoutAuth: true,
},
logLevel: 3,
},

setup(options, nuxt) {
Expand All @@ -60,6 +63,10 @@ export default defineNuxtModule<DeepPartial<SanctumModuleOptions>>({

nuxt.options.runtimeConfig.public.sanctum = sanctumConfig;

const logger = useLogger(LOGGER_NAME, {
level: sanctumConfig.logLevel,
});

addPlugin(resolver.resolve('./runtime/plugin'));
addImportsDir(resolver.resolve('./runtime/composables'));

Expand All @@ -69,6 +76,8 @@ export default defineNuxtModule<DeepPartial<SanctumModuleOptions>>({
path: resolver.resolve('./runtime/middleware/sanctum.global'),
global: true,
});

logger.info('Sanctum module initialized with global middleware');
} else {
addRouteMiddleware({
name: 'sanctum:auth',
Expand All @@ -78,6 +87,8 @@ export default defineNuxtModule<DeepPartial<SanctumModuleOptions>>({
name: 'sanctum:guest',
path: resolver.resolve('./runtime/middleware/sanctum.guest'),
});

logger.info('Sanctum module initialized w/o global middleware');
}
},
});
1 change: 0 additions & 1 deletion src/runtime/composables/useSanctumUser.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,6 @@ import { useSanctumConfig } from './useSanctumConfig';
*/
export const useSanctumUser = <T>(): Ref<T | null> => {
const options = useSanctumConfig();

const user = useState<T | null>(options.userStateKey, () => null);

return user;
Expand Down
42 changes: 39 additions & 3 deletions src/runtime/httpFactory.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,13 +9,14 @@ import {
} from '#app';
import { useSanctumUser } from './composables/useSanctumUser';
import { useSanctumConfig } from './composables/useSanctumConfig';
import { type ConsolaInstance } from 'consola';

type Headers = HeadersInit | undefined;

const SECURE_METHODS = new Set(['post', 'delete', 'put', 'patch']);
const COOKIE_OPTIONS: { readonly: true } = { readonly: true };

export function createHttpClient(): $Fetch {
export function createHttpClient(logger: ConsolaInstance): $Fetch {
const options = useSanctumConfig();
const event = useRequestEvent();
const user = useSanctumUser();
Expand All @@ -34,6 +35,12 @@ export function createHttpClient(): $Fetch {

const csrfToken = useCookie(options.csrf.cookie, COOKIE_OPTIONS).value;

if (!csrfToken) {
logger.warn(
'CSRF cookie is missing in response, check your API configuration'
);
}

return {
...headers,
...(csrfToken && { [options.csrf.header]: csrfToken }),
Expand All @@ -50,6 +57,12 @@ export function createHttpClient(): $Fetch {
const clientCookies = useRequestHeaders(['cookie']);
const origin = options.origin ?? useRequestURL().origin;

if (!csrfToken) {
logger.warn(
`Unable to set ${options.csrf.header} header, CSRF cookie is missing`
);
}

return {
...headers,
Referer: origin,
Expand All @@ -65,7 +78,7 @@ export function createHttpClient(): $Fetch {
redirect: 'manual',
retry: options.client.retry,

async onRequest({ options }): Promise<void> {
async onRequest({ request, options }): Promise<void> {
const method = options.method?.toLowerCase() ?? 'get';

options.headers = {
Expand All @@ -85,23 +98,35 @@ export function createHttpClient(): $Fetch {

if (import.meta.client) {
if (!SECURE_METHODS.has(method)) {
logger.debug(
`Skipping CSRF token header for safe method [${request}]`
);

return;
}

options.headers = await buildClientHeaders(options.headers);
}
},

async onResponse({ response }): Promise<void> {
async onResponse({ request, response }): Promise<void> {
// pass all cookies from the API to the client on SSR response
if (import.meta.server) {
const serverCookieName = 'set-cookie';
const cookie = response.headers.get(serverCookieName);

if (cookie === null || event === undefined) {
logger.debug(
`No cookies to pass to the client [${request}]`
);
return;
}

logger.debug(
`Passing API cookies from Nuxt server to the client response [${request}]`,
cookie
);

event.headers.append(serverCookieName, cookie);
}

Expand All @@ -112,10 +137,21 @@ export function createHttpClient(): $Fetch {
},

async onResponseError({ request, response }): Promise<void> {
if (response.status === 419) {
logger.warn(
'CSRF token mismatch, check your API configuration'
);

return;
}

if (
response.status === 401 &&
request.toString().endsWith(options.endpoints.user)
) {
logger.warn(
'User session is not set in API or expired, resetting identity'
);
user.value = null;
}
},
Expand Down
23 changes: 18 additions & 5 deletions src/runtime/plugin.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,29 +3,42 @@ import { defineNuxtPlugin } from '#app';
import { createHttpClient } from './httpFactory';
import { useSanctumUser } from './composables/useSanctumUser';
import { useSanctumConfig } from './composables/useSanctumConfig';
import { createConsola, type ConsolaInstance } from 'consola';
import { LOGGER_NAME } from '../constants';

function handleIdentityLoadError(error: Error) {
function createSanctumLogger(logLevel: number) {
const envSuffix = import.meta.env.SSR ? 'ssr' : 'csr';
const loggerName = LOGGER_NAME + ':' + envSuffix;

return createConsola({ level: logLevel }).withTag(loggerName);
}

function handleIdentityLoadError(error: Error, logger: ConsolaInstance) {
if (
error instanceof FetchError &&
error.response &&
[401, 419].includes(error.response.status)
) {
// unauthenticated user, unable to get information
logger.debug(
'User is not authenticated on plugin initialization, status:',
error.response.status
);
} else {
console.error('Unable to load user identity', error);
logger.error('Unable to load user identity from API', error);
}
}

export default defineNuxtPlugin(async () => {
const user = useSanctumUser();
const options = useSanctumConfig();
const client = createHttpClient();
const logger = createSanctumLogger(options.logLevel);
const client = createHttpClient(logger);

if (user.value === null) {
try {
user.value = await client(options.endpoints.user);
} catch (error) {
handleIdentityLoadError(error as Error);
handleIdentityLoadError(error as Error, logger);
}
}

Expand Down
13 changes: 13 additions & 0 deletions src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -128,4 +128,17 @@ export interface SanctumModuleOptions {
* Behavior of the global middleware.
*/
globalMiddleware: GlobalMiddlewareOptions;
/**
* The log level to use for the logger.
*
* 0: Fatal and Error
* 1: Warnings
* 2: Normal logs
* 3: Informational logs
* 4: Debug logs
* 5: Trace logs
*
* More details at https://github.com/unjs/consola?tab=readme-ov-file#log-level
*/
logLevel: number;
}

0 comments on commit 0d58e6e

Please sign in to comment.