diff --git a/src/middleware/auth/authMiddleware.spec.ts b/src/middleware/auth/authMiddleware.spec.ts new file mode 100644 index 0000000..2403b72 --- /dev/null +++ b/src/middleware/auth/authMiddleware.spec.ts @@ -0,0 +1,58 @@ +import { fetch } from 'cross-fetch'; +import { + http, + HttpResponse +} from 'msw'; +import { setupServer } from 'msw/node'; +import { suite } from 'uvu'; +import * as assert from 'uvu/assert'; + +import { createHttpClient } from '../../createHttpClient'; +import { + authRefreshEndpoint, + endpoint, + getTokens, + queryInvalid +} from '../../mock'; +import { BasicObject } from '../../types'; + +import { initAuthMiddleware } from './authMiddleware'; + +const clientSuite = suite('Authorization Middleware'); + +const server = setupServer( + http.post(endpoint, async ({ request }) => { + const body = await request.json().catch(() => ''); + const headers = request.headers; + const isInvalid = String(body) === queryInvalid; + + return HttpResponse.json( + body, + { headers, status: isInvalid ? 401 : 200 }, + ); + }), + http.post(authRefreshEndpoint, ({ request }) => { + const headers = request.headers; + + return HttpResponse.json( + { accessToken: '4321', refreshToken: 'refresh4321' }, + { headers, status: 200 }, + ); + }), +); + +global.fetch = fetch; + +clientSuite.before(() => server.listen()); +clientSuite.after.each(() => server.resetHandlers()); +clientSuite.after(() => server.close()); + +clientSuite.only('should apply auth middleware', async () => { + const authParams = { url: authRefreshEndpoint, getTokens, setTokens: () => {} }; + const { post } = createHttpClient({ middleware: { response: [initAuthMiddleware(authParams)] } }); + const response = await post({ url: endpoint, query: queryInvalid }); + // @ts-expect-error header assertion + assert.is(response.headers.Authorization, 'Bearer 4321'); +}); + +clientSuite.run(); diff --git a/src/middleware/auth/authMiddleware.ts b/src/middleware/auth/authMiddleware.ts new file mode 100644 index 0000000..c82a32e --- /dev/null +++ b/src/middleware/auth/authMiddleware.ts @@ -0,0 +1,57 @@ +import { MiddlewareHandler } from '../../types'; +import { getBody, is } from '../../utils'; + +import { AuthMiddlewareParams } from './types'; + +export function initAuthMiddleware(initParams: AuthMiddlewareParams) { + const { + url, + errorCodes = [401], + getTokens, + setTokens, + getHeaders, + handleAuthError, + } = initParams; + + return async (...params: Parameters) => { + const [options, meta] = params; + const currentSessionTokens = getTokens(); + const headers = + typeof getHeaders === 'function' ? + getHeaders(meta) : + { Authorization: `Bearer ${currentSessionTokens.accessToken}` }; + const shouldProcessAuth = errorCodes.some(errorCode => errorCode === meta.status); + + if (shouldProcessAuth) { + const body = getBody({ refreshToken: currentSessionTokens.refreshToken }); + const sanitizedOptions = is.Object(options) ? options : {}; + const sanitizedHeaders = sanitizedOptions?.headers ?? {}; + const errorHandler = + typeof handleAuthError === 'function' ? + handleAuthError : + // eslint-disable-next-line no-console + () => console.warn('Failed to refresh authorization token'); + + return fetch(url, { method: 'post', body, headers }) + .then(async (response) => { + const tokens = await response.json().catch(() => null) as ReturnType | null; + const { accessToken } = getTokens(tokens); + const optionsWithAuth = { + ...sanitizedOptions, + ok: response.ok, + headers: { + ...sanitizedHeaders, + Authorization: `Bearer ${accessToken}`, + }, + }; + + setTokens(tokens); + + return optionsWithAuth; + }) + .catch(errorHandler); + } + + return { ...options ?? {}, headers }; + }; +} diff --git a/src/middleware/auth/index.ts b/src/middleware/auth/index.ts new file mode 100644 index 0000000..5db8ae4 --- /dev/null +++ b/src/middleware/auth/index.ts @@ -0,0 +1 @@ +export { initAuthMiddleware } from './authMiddleware'; diff --git a/src/middleware/auth/types.d.ts b/src/middleware/auth/types.d.ts new file mode 100644 index 0000000..78807c0 --- /dev/null +++ b/src/middleware/auth/types.d.ts @@ -0,0 +1,8 @@ +export interface AuthMiddlewareParams { + url: string; + errorCodes?: Response['status'][]; + getTokens: (tokens?: unknown) => { accessToken: string; refreshToken: string }; + setTokens: (tokens?: unknown) => void; + getHeaders?: (meta: Partial) => Response['headers']; + handleAuthError?: (error: unknown) => void; +} diff --git a/src/middleware/index.ts b/src/middleware/index.ts new file mode 100644 index 0000000..46687e0 --- /dev/null +++ b/src/middleware/index.ts @@ -0,0 +1 @@ +export { initAuthMiddleware } from './auth';