diff --git a/src/FiscalOS.Web/src/services/client.ts b/src/FiscalOS.Web/src/services/client.ts new file mode 100644 index 0000000..79f018b --- /dev/null +++ b/src/FiscalOS.Web/src/services/client.ts @@ -0,0 +1,151 @@ +import type { InjectionKey } from 'vue'; + +type ClientKeyType = InjectionKey; + +export const ClientFactoryKey: ClientKeyType = Symbol('AuthServiceFactory'); + +export interface IClientFactory { + create: (config?: ClientConfig) => IClient; +} + +export class ClientFactory implements IClientFactory { + create(config?: ClientConfig): IClient { + return new Client(config); + } +} + +export class ClientRequest { + readonly url: string; + readonly config?: RequestInit; + + constructor(url: string, config?: RequestInit) { + this.url = url; + this.config = config; + } +} + +export class ClientRequestWithBody extends ClientRequest { + body: T | undefined; + + constructor(url: string, config?: RequestInit, body?: T) { + super(url, config); + this.body = body; + } +} + +export type UnauthorizedResponseHandler = + | ((originalRequest: Request) => Promise<{ response: Response; accessToken: string }>) + | undefined; + +export class ClientConfig { + authHeader: Record | undefined; + includeCredentials: boolean | undefined; + unauthorizedResponseHandler: UnauthorizedResponseHandler; + + constructor( + authHeader?: Record | undefined, + includeCredentials?: boolean | undefined, + unauthorizedResponseHandler?: UnauthorizedResponseHandler + ) { + this.authHeader = authHeader; + this.includeCredentials = includeCredentials; + this.unauthorizedResponseHandler = unauthorizedResponseHandler; + } +} + +export interface IClient { + get: (req: ClientRequest) => Promise; + post: (req: ClientRequestWithBody) => Promise; + put: (req: ClientRequestWithBody) => Promise; + patch: (req: ClientRequestWithBody) => Promise; + delete: (req: ClientRequest) => Promise; +} + +export class Client implements IClient { + private readonly _clientConfig: ClientConfig; + + constructor(clientConfig?: ClientConfig) { + this._clientConfig = clientConfig ?? new ClientConfig(undefined, true, undefined); + } + + private async request(url: string, config?: RequestInit): Promise { + const headers = { + ...config?.headers, + ...this._clientConfig?.authHeader, + }; + + const credentials = this._clientConfig?.includeCredentials + ? ('include' as RequestCredentials) + : ('omit' as RequestCredentials); + + const requestConfig = { + ...config, + headers: headers, + credentials: credentials, + }; + + const firstTryRequest = new Request(url, requestConfig); + const secondTryRequest = new Request(url, requestConfig); + let response = await fetch(firstTryRequest); + + if (response.status === 401 && this._clientConfig?.unauthorizedResponseHandler) { + const retryResult = await this._clientConfig.unauthorizedResponseHandler(secondTryRequest); + this._clientConfig.authHeader = { Authorization: `Bearer ${retryResult.accessToken}` }; + response = retryResult.response; + } + + return response; + } + + async get({ url, config }: { url: string; config?: RequestInit }) { + const requestConfig = { ...config, method: 'GET' }; + return await this.request(url, requestConfig); + } + + async post(req: ClientRequestWithBody) { + const requestConfig = { + ...req?.config, + method: 'POST', + body: JSON.stringify(req?.body), + headers: { + ...req?.config?.headers, + 'Content-Type': 'application/json', + }, + }; + + return await this.request(req?.url, requestConfig); + } + + async put(req: ClientRequestWithBody) { + const requestConfig = { + ...req?.config, + method: 'PUT', + body: JSON.stringify(req?.body), + headers: { + ...req?.config?.headers, + 'Content-Type': 'application/json', + }, + }; + + return await this.request(req?.url, requestConfig); + } + + async patch(req: ClientRequestWithBody) { + const requestConfig = { + ...req?.config, + method: 'PATCH', + body: JSON.stringify(req?.body), + headers: { + ...req?.config?.headers, + 'Content-Type': 'application/json', + }, + }; + + return await this.request(req?.url, requestConfig); + } + + async delete(req: ClientRequest) { + const requestConfig = { ...req?.config, method: 'DELETE' }; + return await this.request(req?.url, requestConfig); + } +}