mirror of
https://github.com/bitwarden/clients.git
synced 2026-07-07 21:10:55 +08:00
Add registration and execution of request middlewares (#19421)
Co-authored-by: Daniel James Smith <djsmith85@users.noreply.github.com>
This commit is contained in:
parent
d066a06caf
commit
fb01459bcb
@ -459,6 +459,12 @@ export abstract class ApiService {
|
||||
abstract fetch(request: Request): Promise<Response>;
|
||||
abstract nativeFetch(request: Request): Promise<Response>;
|
||||
|
||||
/**
|
||||
* Adds a middleware function that will be called with the Request object before each API call. This allows for dynamic header manipulation, such as adding cookies for SSO authentication.
|
||||
* @param middleware The middleware function to add
|
||||
*/
|
||||
abstract addMiddleware(middleware: (request: Request) => Promise<void>): void;
|
||||
|
||||
abstract preValidateSso(identifier: string): Promise<SsoPreValidateResponse>;
|
||||
|
||||
abstract postCreateSponsorship(
|
||||
|
||||
@ -1245,4 +1245,77 @@ describe("ApiService", () => {
|
||||
expect(logoutCallback).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
describe("fetch", () => {
|
||||
it("does not execute any middlewares when none are registered", async () => {
|
||||
const nativeFetch = jest.fn<Promise<Response>, [request: Request]>();
|
||||
nativeFetch.mockResolvedValue({
|
||||
ok: true,
|
||||
status: 200,
|
||||
} satisfies Partial<Response> as Response);
|
||||
sut.nativeFetch = nativeFetch;
|
||||
|
||||
const request = {
|
||||
url: "https://example.com/api",
|
||||
method: "POST",
|
||||
headers: { set: jest.fn() },
|
||||
} as unknown as Request;
|
||||
await sut.fetch(request);
|
||||
|
||||
expect(nativeFetch).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it("executes a registered middleware before sending the request", async () => {
|
||||
const middleware = jest.fn<Promise<void>, [Request]>().mockResolvedValue(undefined);
|
||||
sut.addMiddleware(middleware);
|
||||
|
||||
const nativeFetch = jest.fn<Promise<Response>, [request: Request]>();
|
||||
nativeFetch.mockResolvedValue({
|
||||
ok: true,
|
||||
status: 200,
|
||||
} satisfies Partial<Response> as Response);
|
||||
sut.nativeFetch = nativeFetch;
|
||||
|
||||
const request = {
|
||||
url: "https://example.com/api",
|
||||
method: "POST",
|
||||
headers: { set: jest.fn() },
|
||||
} as unknown as Request;
|
||||
await sut.fetch(request);
|
||||
|
||||
expect(middleware).toHaveBeenCalledTimes(1);
|
||||
expect(middleware).toHaveBeenCalledWith(request);
|
||||
expect(nativeFetch).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it("executes all registered middlewares before sending the request", async () => {
|
||||
const callOrder: number[] = [];
|
||||
const middleware1 = jest.fn<Promise<void>, [Request]>().mockImplementation(async () => {
|
||||
callOrder.push(1);
|
||||
});
|
||||
const middleware2 = jest.fn<Promise<void>, [Request]>().mockImplementation(async () => {
|
||||
callOrder.push(2);
|
||||
});
|
||||
sut.addMiddleware(middleware1);
|
||||
sut.addMiddleware(middleware2);
|
||||
|
||||
const nativeFetch = jest.fn<Promise<Response>, [request: Request]>();
|
||||
nativeFetch.mockResolvedValue({
|
||||
ok: true,
|
||||
status: 200,
|
||||
} satisfies Partial<Response> as Response);
|
||||
sut.nativeFetch = nativeFetch;
|
||||
|
||||
const request = {
|
||||
url: "https://example.com/api",
|
||||
method: "POST",
|
||||
headers: { set: jest.fn() },
|
||||
} as unknown as Request;
|
||||
await sut.fetch(request);
|
||||
|
||||
expect(middleware1).toHaveBeenCalledTimes(1);
|
||||
expect(middleware2).toHaveBeenCalledTimes(1);
|
||||
expect(nativeFetch).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@ -138,6 +138,11 @@ export class ApiService implements ApiServiceAbstraction {
|
||||
private static readonly NEW_DEVICE_VERIFICATION_REQUIRED_MESSAGE =
|
||||
"new device verification required";
|
||||
|
||||
/**
|
||||
* Middlewares are functions that take a Request and return a Promise that resolves when the middleware is done processing the request. Middlewares are executed in the order they are added, and can modify the request before it is sent. This is used for things like adding cookies to requests for SSO authentication.
|
||||
*/
|
||||
private middlewares: Array<(request: Request) => Promise<void>> = [];
|
||||
|
||||
constructor(
|
||||
private tokenService: TokenService,
|
||||
private platformUtilsService: PlatformUtilsService,
|
||||
@ -1319,6 +1324,10 @@ export class ApiService implements ApiServiceAbstraction {
|
||||
return accessToken;
|
||||
}
|
||||
|
||||
addMiddleware(middleware: (request: Request) => Promise<void>): void {
|
||||
this.middlewares.push(middleware);
|
||||
}
|
||||
|
||||
async fetch(request: Request): Promise<Response> {
|
||||
if (!request.url.startsWith("https://") && !this.platformUtilsService.isDev()) {
|
||||
throw new InsecureUrlNotAllowedError();
|
||||
@ -1338,6 +1347,13 @@ export class ApiService implements ApiServiceAbstraction {
|
||||
if (packageType != null) {
|
||||
request.headers.set("Bitwarden-Package-Type", packageType);
|
||||
}
|
||||
|
||||
await Promise.all(
|
||||
this.middlewares.map((middleware) => {
|
||||
return middleware(request);
|
||||
}),
|
||||
);
|
||||
|
||||
return this.nativeFetch(request);
|
||||
}
|
||||
|
||||
|
||||
Loading…
Reference in New Issue
Block a user