Skip to content

Commit

Permalink
✨ handle 401 return false dataresult #132
Browse files Browse the repository at this point in the history
  • Loading branch information
trydofor committed Dec 29, 2024
1 parent e71d40d commit a296da0
Show file tree
Hide file tree
Showing 5 changed files with 75 additions and 18 deletions.
6 changes: 6 additions & 0 deletions .changeset/tricky-lamps-pull.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
---
"@fessional/razor-common": patch
"@fessional/razor-mobile": patch
---

✨ handle 401 return false dataresult #132
41 changes: 32 additions & 9 deletions layers/common/composables/UseApiRoute.ts
Original file line number Diff line number Diff line change
@@ -1,24 +1,40 @@
import { useEventBus, type EventBusKey, type UseEventBusReturn } from '@vueuse/core';
import { useEventBus, type EventBusKey } from '@vueuse/core';
import { FetchError } from 'ofetch';

// import type { FetchOptions } from 'ofetch';
type ApiRouteOptions = NonNullable<Parameters<typeof $fetch>[1]>;

type ApiRouteAuthEvent = {
export type ApiRouteAuthEvent = {
status: number;
session?: string;
headers?: Record<string, string>;
};

/**
* https://github.com/unjs/ofetch/blob/main/src/fetch.ts
* handle the event and return
* - null - nop
* - non-null - to set the response._data, options.ignoreResponseError=true
* - FetchError - to set context.error, get it from error.cause
*/
export type ApiRouteAuthHandle = (event: ApiRouteAuthEvent) => SafeAny | FetchError;

export const apiRouteAuthEventKey: EventBusKey<ApiRouteAuthEvent> = Symbol('apiRouteResponseEventKey');
export const apiRouteAuthEventBus = useEventBus(apiRouteAuthEventKey);

/**
* emit event by apiRouteAuthEventBus, handle 401 to return `{success:false}`
*/
export const apiRouteAuthEmitter: ApiRouteAuthHandle = (evt) => {
apiRouteAuthEventBus.emit(evt);
return evt.status === 401 ? { success: false } : null;
};
/**
* construct a onResponse by eventBus and listen status or header
* @param eventBus the event bus
* @param statusOrHeader status or header(case insensitive)
* @param statusOrHeader status or header(case insensitive), `[401]` as default.
* @param handler the event bus, `apiRouteAuthEmitter` as default.
*/
export function apiRouteEmitOptions(statusOrHeader: (number | string)[] = [401, 403], eventBus: UseEventBusReturn<ApiRouteAuthEvent, SafeAny> = apiRouteAuthEventBus): ApiRouteOptions {
export function apiRouteAuthOptions(statusOrHeader: (number | string)[] = [401], handler: ApiRouteAuthHandle = apiRouteAuthEmitter): ApiRouteOptions {
const status: number[] = [];
const header: string[] = [];
for (const k of statusOrHeader) {
Expand All @@ -31,9 +47,9 @@ export function apiRouteEmitOptions(statusOrHeader: (number | string)[] = [401,
}

return {
onResponse: ({ response }) => {
const evt: ApiRouteAuthEvent = { status: response.status };
const hds = response.headers;
onResponse: (ctx) => {
const evt: ApiRouteAuthEvent = { status: ctx.response.status };
const hds = ctx.response.headers;

const sn = hds.get('session');
if (sn) evt.session = sn;
Expand All @@ -47,7 +63,14 @@ export function apiRouteEmitOptions(statusOrHeader: (number | string)[] = [401,
}

if (status.includes(evt.status) || evt.session != null || evt.headers != null) {
eventBus.emit(evt);
const rst = handler(evt);
if (rst instanceof FetchError) {
ctx.error = rst;
}
else if (rst != null) {
ctx.options.ignoreResponseError = true;
ctx.response._data = rst;
}
}
},
};
Expand Down
37 changes: 30 additions & 7 deletions layers/common/tests/UseApiRoute.test.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,9 @@
import { describe, it, expect, beforeAll, afterAll, afterEach, vi } from 'vitest';
import { setupServer } from 'msw/node';
import { http, HttpResponse } from 'msw';
import { createFetch } from 'ofetch';
import { useApiRoute, apiRouteEmitOptions, apiRouteAuthEventBus, apiRouteFetchError } from '../composables/UseApiRoute';
import { createFetch, FetchError } from 'ofetch';
import { useEventBus } from '@vueuse/core';
import { useApiRoute, apiRouteAuthOptions, apiRouteAuthEventBus, apiRouteFetchError } from '../composables/UseApiRoute';

const session = 'wings-session';
const cookie = `session=${session}; expires=Fri, 31 Dec 9999 23:59:59 GMT; path=/`;
Expand Down Expand Up @@ -114,18 +115,15 @@ describe('useApiRoute with real $fetch requests', () => {
});

it('should send POST with Auth Event', async () => {
const opts = apiRouteEmitOptions([401, 403, 'set-cookie']);
const opts = apiRouteAuthOptions([401, 403, 'set-cookie']);
const { post } = useApiRoute(opts);
const eventSpy = vi.fn();

apiRouteAuthEventBus.on((ev) => {
eventSpy(ev);
});

await expect(post('/test-401.json')).rejects.toMatchObject({
status: 401,
});

await expect(post('/test-401.json')).resolves.toMatchObject({ success: false });
await expect(post('/test-403.json')).rejects.toSatisfy((error: SafeAny) => {
const fe = apiRouteFetchError(error);
return fe?.statusCode === 403;
Expand All @@ -136,4 +134,29 @@ describe('useApiRoute with real $fetch requests', () => {
expect(eventSpy).toHaveBeenNthCalledWith(2, { status: 403 });
expect(eventSpy).toHaveBeenNthCalledWith(3, { status: 200, session: session, headers: { 'set-cookie': cookie } });
});

it('should send POST with Auth Event replace error or result', async () => {
const authEventBus = useEventBus('test-useapi-event-bus');

const opts = apiRouteAuthOptions([401, 403], (evt) => {
authEventBus.emit(evt);
if (evt.status === 403) return new FetchError('error-403');
});
const { post } = useApiRoute(opts);
const eventSpy = vi.fn();

authEventBus.on((ev) => {
eventSpy(ev);
});

await expect(post('/test-401.json')).rejects.toMatchObject({
status: 401,
});
await expect(post('/test-403.json')).rejects.toSatisfy((err: SafeAny) => {
return err.cause.message === 'error-403';
});

expect(eventSpy).toHaveBeenNthCalledWith(1, { status: 401 });
expect(eventSpy).toHaveBeenNthCalledWith(2, { status: 403 });
});
});
2 changes: 1 addition & 1 deletion layers/common/utils/typed-fetcher.ts
Original file line number Diff line number Diff line change
Expand Up @@ -62,7 +62,7 @@ export async function fetchTypedDataAsync<T>(
* @param fetching - function/data of DataResult
* @param options - options to handle loading, failure, error
*/
export function fetchTypedResult<T>(
export function fetchTypedResult<T = DataResult<SafeAny>>(
fetching: T | (() => T),
options: TypedFetchOptions<T> = { },
): T | null {
Expand Down
7 changes: 6 additions & 1 deletion layers/mobile/utils/ionic-fetcher.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,20 +19,25 @@ const _alerter: AlertHandler<SafeAny> = (result, error) => {
};
}
else {
let header = 'Network Request Error';
let message = error.message || 'Network or Server Error';
const fe = apiRouteFetchError(error);
if (fe != null) {
// TODO i18n message
if (fe.statusCode === 401) {
if (result?.success === false) return false; // handled by emit

header = 'Unauthorized';
message = 'Please login to continue.';
}
else if (fe.statusCode === 403) {
header = 'Forbidden';
message = 'No permission to access';
}
}

return {
header: 'Network Request Error',
header,
message,
buttons: ['Close'],
};
Expand Down

0 comments on commit a296da0

Please sign in to comment.