Co-authored-by: gorbenko.pavel <gorbenko.pavel@tansu.kz> Reviewed-on: http://git.tnsu.kz/gorbenko.pavel/tnsu-approval-route/pulls/5
This commit was merged in pull request #5.
This commit is contained in:
@@ -0,0 +1,160 @@
|
||||
import { AadHttpClient, HttpClientResponse } from '@microsoft/sp-http';
|
||||
|
||||
// Ответ бэкенда всегда в конверте { data: T } на успехе (Api/Common/ApiResponse.cs)
|
||||
// и { messages: string[] } на ошибке (ApiError). 204 No Content возвращается без тела.
|
||||
interface IApiEnvelope<T> {
|
||||
data?: T;
|
||||
messages?: string[];
|
||||
}
|
||||
|
||||
export class ApiClientError extends Error {
|
||||
public readonly status: number;
|
||||
public readonly messages: string[];
|
||||
|
||||
constructor(status: number, messages: string[]) {
|
||||
super(messages.join('; ') || `HTTP ${status}`);
|
||||
this.status = status;
|
||||
this.messages = messages;
|
||||
}
|
||||
}
|
||||
|
||||
export type QueryParams = Record<string, string | number | boolean | undefined | null>;
|
||||
|
||||
export interface IHttpClient {
|
||||
get<T>(path: string, query?: QueryParams): Promise<T>;
|
||||
post<T>(path: string, body?: unknown): Promise<T>;
|
||||
put<T>(path: string, body?: unknown): Promise<T>;
|
||||
patch<T>(path: string, body?: unknown): Promise<T>;
|
||||
delete(path: string): Promise<void>;
|
||||
}
|
||||
|
||||
function buildUrl(baseUrl: string, path: string, query?: QueryParams): string {
|
||||
const normalizedBase = baseUrl.endsWith('/') ? baseUrl : `${baseUrl}/`;
|
||||
const url = new URL(path.replace(/^\//, ''), normalizedBase);
|
||||
if (query) {
|
||||
Object.keys(query).forEach(key => {
|
||||
const value = query[key];
|
||||
if (value !== undefined && value !== null && value !== '') {
|
||||
url.searchParams.set(key, String(value));
|
||||
}
|
||||
});
|
||||
}
|
||||
return url.toString();
|
||||
}
|
||||
|
||||
interface IMinimalResponse {
|
||||
ok: boolean;
|
||||
status: number;
|
||||
text(): Promise<string>;
|
||||
}
|
||||
|
||||
async function parseResponse<T>(response: IMinimalResponse): Promise<T> {
|
||||
if (response.status === 204) {
|
||||
return undefined as T;
|
||||
}
|
||||
|
||||
const text = await response.text();
|
||||
let json: IApiEnvelope<T> | undefined;
|
||||
try {
|
||||
json = text ? JSON.parse(text) : undefined;
|
||||
} catch {
|
||||
// Ответ пришёл не в конверте ApiResponse/ApiError (например, "сырая" страница
|
||||
// необработанного исключения ASP.NET Core или ошибка от прокси перед API).
|
||||
if (!response.ok) {
|
||||
throw new ApiClientError(response.status, [text || `HTTP ${response.status}`]);
|
||||
}
|
||||
throw new ApiClientError(response.status, ['Сервер вернул не-JSON ответ.']);
|
||||
}
|
||||
|
||||
if (!response.ok) {
|
||||
const messages = json?.messages ?? [`HTTP ${response.status}`];
|
||||
throw new ApiClientError(response.status, messages);
|
||||
}
|
||||
|
||||
return (json?.data as T) ?? (undefined as T);
|
||||
}
|
||||
|
||||
/**
|
||||
* Прод/staging: вызовы через AAD-защищённое SPFx-подключение (AadHttpClient).
|
||||
* Требует webApiPermissionRequests в package-solution.json и админского
|
||||
* согласия в SharePoint admin center → Advanced → API access.
|
||||
*/
|
||||
export class AadApiHttpClient implements IHttpClient {
|
||||
constructor(private readonly client: AadHttpClient, private readonly baseUrl: string) {}
|
||||
|
||||
public get<T>(path: string, query?: QueryParams): Promise<T> {
|
||||
const url = buildUrl(this.baseUrl, path, query);
|
||||
return this.client
|
||||
.get(url, AadHttpClient.configurations.v1)
|
||||
.then((r: HttpClientResponse) => parseResponse<T>(r));
|
||||
}
|
||||
|
||||
public post<T>(path: string, body?: unknown): Promise<T> {
|
||||
return this.send<T>('POST', path, body);
|
||||
}
|
||||
|
||||
public put<T>(path: string, body?: unknown): Promise<T> {
|
||||
return this.send<T>('PUT', path, body);
|
||||
}
|
||||
|
||||
public patch<T>(path: string, body?: unknown): Promise<T> {
|
||||
return this.send<T>('PATCH', path, body);
|
||||
}
|
||||
|
||||
public async delete(path: string): Promise<void> {
|
||||
await this.send<void>('DELETE', path);
|
||||
}
|
||||
|
||||
private send<T>(method: string, path: string, body?: unknown): Promise<T> {
|
||||
const url = buildUrl(this.baseUrl, path);
|
||||
return this.client
|
||||
.fetch(url, AadHttpClient.configurations.v1, {
|
||||
method,
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: body !== undefined ? JSON.stringify(body) : undefined
|
||||
})
|
||||
.then((r: HttpClientResponse) => parseResponse<T>(r));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Локальная отладка: обычный fetch с заголовком X-Dev-User-Email.
|
||||
* Работает только если бэкенд поднят с пустыми Entra:TenantId/Entra:Audience
|
||||
* (см. backend/.../Auth/AuthSetup.cs — тогда включается DevAuthHandler).
|
||||
*/
|
||||
export class DevApiHttpClient implements IHttpClient {
|
||||
constructor(private readonly baseUrl: string, private readonly devUserEmail: string) {}
|
||||
|
||||
public get<T>(path: string, query?: QueryParams): Promise<T> {
|
||||
return this.send<T>('GET', path, undefined, query);
|
||||
}
|
||||
|
||||
public post<T>(path: string, body?: unknown): Promise<T> {
|
||||
return this.send<T>('POST', path, body);
|
||||
}
|
||||
|
||||
public put<T>(path: string, body?: unknown): Promise<T> {
|
||||
return this.send<T>('PUT', path, body);
|
||||
}
|
||||
|
||||
public patch<T>(path: string, body?: unknown): Promise<T> {
|
||||
return this.send<T>('PATCH', path, body);
|
||||
}
|
||||
|
||||
public async delete(path: string): Promise<void> {
|
||||
await this.send<void>('DELETE', path);
|
||||
}
|
||||
|
||||
private async send<T>(method: string, path: string, body?: unknown, query?: QueryParams): Promise<T> {
|
||||
const url = buildUrl(this.baseUrl, path, query);
|
||||
const response = await fetch(url, {
|
||||
method,
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'X-Dev-User-Email': this.devUserEmail
|
||||
},
|
||||
body: body !== undefined ? JSON.stringify(body) : undefined
|
||||
});
|
||||
return parseResponse<T>(response);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user