[PM-31942] Handle load/save Access Intelligence reports as files (pt. 1) (#19922)

- Includes model updates for making requests to the updated Access Intelligence report file APIs (changes here)

- Adds AccessIntelligenceApiService for calls to new APIs. Includes default implementation + tests
This commit is contained in:
Brad 2026-04-17 14:30:19 -07:00 committed by GitHub
parent 086c625762
commit ee291b41b7
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
8 changed files with 903 additions and 0 deletions

View File

@ -0,0 +1,29 @@
import { BaseResponse } from "@bitwarden/common/models/response/base.response";
import { FileUploadType } from "@bitwarden/common/platform/enums";
import { AccessReportApi } from "./access-report.api";
/**
* Response model returned when creating an Access Intelligence report to be stored as a file.
* Contains a presigned URL that is used to upload the file for the report.
*
* - See {@link AccessReportApi} for the nested report response model
*/
export class AccessReportFileApi extends BaseResponse {
reportFileUploadUrl: string = "";
reportResponse: AccessReportApi = new AccessReportApi();
fileUploadType: FileUploadType = FileUploadType.Direct;
constructor(data: any = null) {
super(data);
if (data == null) {
return;
}
this.reportFileUploadUrl = this.getResponseProperty("reportFileUploadUrl") ?? "";
const reportResponse = this.getResponseProperty("reportResponse");
this.reportResponse =
reportResponse != null ? new AccessReportApi(reportResponse) : new AccessReportApi();
this.fileUploadType = this.getResponseProperty("fileUploadType") ?? FileUploadType.Direct;
}
}

View File

@ -7,6 +7,8 @@ import { AccessReport } from "../domain/access-report";
// eslint-disable-next-line @typescript-eslint/no-unused-vars
import { AccessReportView } from "../view/access-report.view";
import { ReportFileApi } from "./report-file.api";
/**
* Converts an AccessReport API response
*
@ -24,6 +26,8 @@ export class AccessReportApi extends BaseResponse {
memberRegistry: string = "";
creationDate: string = "";
contentEncryptionKey: string = "";
reportFile?: ReportFileApi;
reportFileDownloadUrl?: string;
constructor(data: any = null) {
super(data);
@ -39,6 +43,10 @@ export class AccessReportApi extends BaseResponse {
this.summary = this.getResponseProperty("summaryData");
this.memberRegistry = this.getResponseProperty("memberRegistry") ?? "";
this.contentEncryptionKey = this.getResponseProperty("contentEncryptionKey");
this.reportFileDownloadUrl = this.getResponseProperty("reportFileDownloadUrl") ?? undefined;
const reportFile = this.getResponseProperty("reportFile");
this.reportFile = reportFile != null ? new ReportFileApi(reportFile) : undefined;
// Use when individual values are encrypted
// const summary = this.getResponseProperty("summaryData");

View File

@ -0,0 +1,20 @@
import { BaseResponse } from "@bitwarden/common/models/response/base.response";
/**
* Metadata for an uploaded report file.
*/
export class ReportFileApi extends BaseResponse {
id: string;
fileName: string = "";
/** File size in bytes. Serialized as a string by the server. */
size: number = 0;
validated: boolean = false;
constructor(data: any) {
super(data);
this.id = this.getResponseProperty("id");
this.fileName = this.getResponseProperty("fileName") ?? "";
this.size = Number(this.getResponseProperty("size") ?? 0);
this.validated = this.getResponseProperty("validated") ?? false;
}
}

View File

@ -4,6 +4,7 @@ export * from "./api/application-health.api";
export * from "./api/access-report-settings.api";
export * from "./api/access-report-summary.api";
export * from "./api/access-report-metrics.api";
export * from "./api/access-report-file.api";
// Data layer
export * from "./data/access-report.data";

View File

@ -0,0 +1,167 @@
import { Observable } from "rxjs";
import { OrganizationId, OrganizationReportId } from "@bitwarden/common/types/guid";
import {
AccessReportApi,
AccessReportFileApi,
AccessReportMetricsApi,
AccessReportSummaryApi,
} from "../../models";
export interface AccessReportCreateRequest {
contentEncryptionKey: string;
summaryData: string;
applicationData: string;
metrics: AccessReportMetricsApi;
fileSize: number;
}
export interface AccessReportLegacyCreateRequest {
reportData: string;
contentEncryptionKey: string;
summaryData: string;
applicationData: string;
metrics: AccessReportMetricsApi;
}
export interface AccessReportSettingsUpdateRequest {
summaryData: string;
applicationData: string;
metrics: AccessReportMetricsApi;
}
/**
* Service handling server communication/API calls for Access Intelligence endpoints.
*
* Handles making HTTP requests to the Bitwarden server and transforms all responses into Api models. Source of truth for retrieving and updating Access Intelligence report data using the Bitwarden API.
*/
export abstract class AccessIntelligenceApiService {
/**
* Retrieves the latest Access Intelligence report for an Organization.
* @param orgId - the ID of the Organization to retrieve the report for
* @returns the latest Access Intelligence report
*/
abstract getLatestReport$(orgId: OrganizationId): Observable<AccessReportApi>;
/**
* Creates an Access Intelligence report on the server, where the report contents are stored as a file.
* @param orgId - the ID of the Organization to create the report for
* @param request - contains data used to create the report
* @returns observable emitting the server's response, which includes the created Access Intelligence report
*/
abstract createReport$(
orgId: OrganizationId,
request: AccessReportCreateRequest,
): Observable<AccessReportFileApi>;
/**
* Creates an Access Intelligence report on the server, where report contents are included directly in the request body.
* @param orgId - the ID of the Organization to create the report for
* @param request - contains the report data and metadata used to create the report
* @returns observable emitting the created Access Intelligence report
*/
abstract createLegacyReport$(
orgId: OrganizationId,
request: AccessReportLegacyCreateRequest,
): Observable<AccessReportApi>;
/**
* Self-hosted only. Uploads a file containing the Access Intelligence report data directly to a Bitwarden self-hosted server.
* @param orgId - the ID of the Organization the report belongs to
* @param reportId - the ID of the report to upload the file for
* @param file - the file containing the Access Intelligence report data
* @param reportFileId - the ID of the report file returned from the server upon report creation
* @returns observable that completes when the upload is successful
*/
abstract uploadReportFile$(
orgId: OrganizationId,
reportId: OrganizationReportId,
file: File,
reportFileId: string,
): Observable<void>;
/**
* Retrieves Access Intelligence summary data for an Organization within a date range.
* @param orgId - the ID of the Organization to retrieve summary data for
* @param startDate - the start of the date range (inclusive)
* @param endDate - the end of the date range (inclusive)
* @returns observable emitting an array of summary data records within the given date range
*/
abstract getSummaryDataByDateRange$(
orgId: OrganizationId,
startDate: Date,
endDate: Date,
): Observable<AccessReportSummaryApi[]>;
/**
* Updates the summary data for an existing Access Intelligence report.
* @param orgId - the ID of the Organization the report belongs to
* @param reportId - the ID of the report to update
* @param summaryData - the encrypted summary data to store on the report
* @param metrics - optional map of metric names to their values
* @returns observable emitting the updated Access Intelligence report
*/
abstract updateSummaryData$(
orgId: OrganizationId,
reportId: OrganizationReportId,
summaryData: string,
metrics?: AccessReportMetricsApi,
): Observable<AccessReportApi>;
/**
* Updates the application data for an existing Access Intelligence report.
* @param orgId - the ID of the Organization the report belongs to
* @param reportId - the ID of the report to update
* @param applicationData - the encrypted application data to store on the report
* @returns observable emitting the updated Access Intelligence report
*/
abstract updateApplicationData$(
orgId: OrganizationId,
reportId: OrganizationReportId,
applicationData: string,
): Observable<AccessReportApi>;
/**
* Renews the upload link for an Access Intelligence report file. Used when a prior upload attempt failed or expired.
* @param orgId - the ID of the Organization the report belongs to
* @param reportId - the ID of the report whose upload link should be renewed
* @returns observable emitting the renewed report file metadata, including a fresh upload URL
*/
abstract renewReportFileUploadLink$(
orgId: OrganizationId,
reportId: OrganizationReportId,
): Observable<AccessReportFileApi>;
/**
* Deletes an Access Intelligence report from the server.
* @param orgId - the ID of the Organization the report belongs to
* @param reportId - the ID of the report to delete
* @returns observable that completes when the report has been deleted
*/
abstract deleteReport$(orgId: OrganizationId, reportId: OrganizationReportId): Observable<void>;
/**
* Self-hosted only. Downloads the file for an Access Intelligence report.
* @param orgId - the ID of the Organization the report belongs to
* @param reportId - the ID of the report whose file to download
* @returns observable emitting the file blob and its filename
*/
abstract downloadReportFile$(
orgId: OrganizationId,
reportId: OrganizationReportId,
): Observable<{ blob: Blob; fileName: string }>;
/**
* Update the settings properties for an existing Access Intelligence report.
* @param orgId - the ID of the Organization the report belongs to
* @param reportId - the ID of the report to update
* @param request - the data to update on the report
* @returns observable emitting the updated Access Intelligence report
*/
abstract updateReportSettings$(
orgId: OrganizationId,
reportId: OrganizationReportId,
request: AccessReportSettingsUpdateRequest,
): Observable<AccessReportApi>;
}

View File

@ -0,0 +1,470 @@
import { mock, MockProxy } from "jest-mock-extended";
import { firstValueFrom } from "rxjs";
import { ApiService } from "@bitwarden/common/abstractions/api.service";
import { ErrorResponse } from "@bitwarden/common/models/response/error.response";
import { FileUploadType } from "@bitwarden/common/platform/enums";
import { OrganizationId, OrganizationReportId } from "@bitwarden/common/types/guid";
import {
AccessReportApi,
AccessReportFileApi,
AccessReportMetricsApi,
AccessReportSummaryApi,
} from "../../../models";
import {
AccessReportCreateRequest,
AccessReportLegacyCreateRequest,
AccessReportSettingsUpdateRequest,
} from "../../abstractions/access-intelligence-api.service";
import { DefaultAccessIntelligenceApiService } from "./default-access-intelligence-api.service";
describe("DefaultAccessIntelligenceApiService", () => {
let service: DefaultAccessIntelligenceApiService;
let mockApiService: MockProxy<ApiService>;
const orgId = "org-123" as OrganizationId;
const reportId = "report-456" as OrganizationReportId;
const reportFileId = "file-789";
const mockMetrics = new AccessReportMetricsApi({
totalApplicationCount: 10,
totalAtRiskApplicationCount: 3,
totalCriticalApplicationCount: 2,
totalCriticalAtRiskApplicationCount: 1,
totalMemberCount: 50,
totalAtRiskMemberCount: 12,
totalCriticalMemberCount: 5,
totalCriticalAtRiskMemberCount: 2,
totalPasswordCount: 200,
totalAtRiskPasswordCount: 40,
totalCriticalPasswordCount: 15,
totalCriticalAtRiskPasswordCount: 5,
});
beforeEach(() => {
mockApiService = mock<ApiService>();
service = new DefaultAccessIntelligenceApiService(mockApiService);
});
afterEach(() => {
jest.restoreAllMocks();
});
describe("getLatestReport$", () => {
it("should call GET /reports/organizations/{orgId}/latest and return AccessReportApi", async () => {
const rawResponse = {
id: reportId,
organizationId: orgId,
creationDate: "2024-01-01T00:00:00Z",
reportData: "encrypted-reports",
summaryData: "encrypted-summary",
applicationData: "encrypted-apps",
contentEncryptionKey: "enc-key",
};
mockApiService.send.mockResolvedValue(rawResponse);
const result = await firstValueFrom(service.getLatestReport$(orgId));
expect(mockApiService.send).toHaveBeenCalledWith(
"GET",
`/reports/organizations/${orgId}/latest`,
null,
true,
true,
);
expect(result).toBeInstanceOf(AccessReportApi);
expect(result.id).toBe(reportId);
expect(result.organizationId).toBe(orgId);
});
it("should propagate API errors", async () => {
mockApiService.send.mockRejectedValue(new Error("Network error"));
await expect(firstValueFrom(service.getLatestReport$(orgId))).rejects.toThrow(
"Network error",
);
});
});
describe("createReport$", () => {
it("should call POST /reports/organizations/{orgId} and return AccessReportFileApi", async () => {
const rawResponse = {
reportFileUploadUrl: "https://storage.example.com/upload",
fileUploadType: FileUploadType.Azure,
reportResponse: {
id: reportId,
organizationId: orgId,
creationDate: "2024-01-01T00:00:00Z",
},
};
mockApiService.send.mockResolvedValue(rawResponse);
const request: AccessReportCreateRequest = {
fileSize: 1024,
contentEncryptionKey: "enc-key",
summaryData: "encrypted-summary",
applicationData: "encrypted-apps",
metrics: mockMetrics,
};
const result = await firstValueFrom(service.createReport$(orgId, request));
expect(mockApiService.send).toHaveBeenCalledWith(
"POST",
`/reports/organizations/${orgId}`,
request,
true,
true,
);
expect(result).toBeInstanceOf(AccessReportFileApi);
expect(result.reportFileUploadUrl).toBe("https://storage.example.com/upload");
expect(result.fileUploadType).toBe(FileUploadType.Azure);
expect(result.reportResponse).toBeInstanceOf(AccessReportApi);
});
it("should propagate API errors", async () => {
mockApiService.send.mockRejectedValue(new Error("API error"));
await expect(firstValueFrom(service.createReport$(orgId, {} as any))).rejects.toThrow(
"API error",
);
});
});
describe("createLegacyReport$", () => {
it("should call POST /reports/organizations/{orgId} and return AccessReportApi", async () => {
const rawResponse = {
id: reportId,
organizationId: orgId,
creationDate: "2024-01-01T00:00:00Z",
reportData: "encrypted-report-data",
summaryData: "encrypted-summary",
applicationData: "encrypted-apps",
contentEncryptionKey: "enc-key",
};
mockApiService.send.mockResolvedValue(rawResponse);
const request: AccessReportLegacyCreateRequest = {
reportData: "encrypted-report-data",
contentEncryptionKey: "enc-key",
summaryData: "encrypted-summary",
applicationData: "encrypted-apps",
metrics: mockMetrics,
};
const result = await firstValueFrom(service.createLegacyReport$(orgId, request));
expect(mockApiService.send).toHaveBeenCalledWith(
"POST",
`/reports/organizations/${orgId}`,
request,
true,
true,
);
expect(result).toBeInstanceOf(AccessReportApi);
expect(result.id).toBe(reportId);
expect(result.organizationId).toBe(orgId);
});
it("should propagate API errors", async () => {
mockApiService.send.mockRejectedValue(new Error("API error"));
await expect(firstValueFrom(service.createLegacyReport$(orgId, {} as any))).rejects.toThrow(
"API error",
);
});
});
describe("updateSummaryData$", () => {
it("should call PATCH /reports/organizations/{orgId}/data/summary/{reportId} and return AccessReportApi", async () => {
const rawResponse = {
id: reportId,
organizationId: orgId,
creationDate: "2024-01-01T00:00:00Z",
summaryData: "encrypted-summary",
contentEncryptionKey: "enc-key",
};
mockApiService.send.mockResolvedValue(rawResponse);
const summaryData = "encrypted-summary-data";
const metrics = mockMetrics;
const result = await firstValueFrom(
service.updateSummaryData$(orgId, reportId, summaryData, metrics),
);
expect(mockApiService.send).toHaveBeenCalledWith(
"PATCH",
`/reports/organizations/${orgId}/data/summary/${reportId}`,
expect.objectContaining({ summaryData, metrics }),
true,
true,
);
expect(result).toBeInstanceOf(AccessReportApi);
expect(result.id).toBe(reportId);
});
it("should propagate API errors", async () => {
mockApiService.send.mockRejectedValue(new Error("Update failed"));
await expect(
firstValueFrom(service.updateSummaryData$(orgId, reportId, "data")),
).rejects.toThrow("Update failed");
});
});
describe("updateApplicationData$", () => {
it("should call PATCH /reports/organizations/{orgId}/data/application/{reportId} and return AccessReportApi", async () => {
const rawResponse = {
id: reportId,
organizationId: orgId,
creationDate: "2024-01-01T00:00:00Z",
applicationData: "encrypted-apps",
contentEncryptionKey: "enc-key",
};
mockApiService.send.mockResolvedValue(rawResponse);
const applicationData = "encrypted-app-data";
const result = await firstValueFrom(
service.updateApplicationData$(orgId, reportId, applicationData),
);
expect(mockApiService.send).toHaveBeenCalledWith(
"PATCH",
`/reports/organizations/${orgId}/data/application/${reportId}`,
expect.objectContaining({ applicationData }),
true,
true,
);
expect(result).toBeInstanceOf(AccessReportApi);
expect(result.id).toBe(reportId);
});
it("should propagate API errors", async () => {
mockApiService.send.mockRejectedValue(new Error("Update failed"));
await expect(
firstValueFrom(service.updateApplicationData$(orgId, reportId, "data")),
).rejects.toThrow("Update failed");
});
});
describe("getSummaryDataByDateRange$", () => {
const startDate = new Date("2024-01-01");
const endDate = new Date("2024-01-31");
it("should call GET with date range params and return AccessReportSummaryApi[]", async () => {
const rawResponse = [
{ EncryptedData: "enc-data-1", EncryptionKey: "key-1", Date: "2024-01-15" },
{ EncryptedData: "enc-data-2", EncryptionKey: "key-2", Date: "2024-01-20" },
];
mockApiService.send.mockResolvedValue(rawResponse);
const result = await firstValueFrom(
service.getSummaryDataByDateRange$(orgId, startDate, endDate),
);
expect(mockApiService.send).toHaveBeenCalledWith(
"GET",
`/reports/organizations/${orgId}/data/summary?startDate=2024-01-01&endDate=2024-01-31`,
null,
true,
true,
);
expect(result).toHaveLength(2);
expect(result[0]).toBeInstanceOf(AccessReportSummaryApi);
expect(result[0].encryptedData).toBe("enc-data-1");
});
it("should return empty array when response is not an array", async () => {
mockApiService.send.mockResolvedValue(null);
const result = await firstValueFrom(
service.getSummaryDataByDateRange$(orgId, startDate, endDate),
);
expect(result).toEqual([]);
});
it("should return empty array on 404 error", async () => {
const notFoundError = new ErrorResponse({ Message: "Not found" }, 404);
mockApiService.send.mockRejectedValue(notFoundError);
const result = await firstValueFrom(
service.getSummaryDataByDateRange$(orgId, startDate, endDate),
);
expect(result).toEqual([]);
});
it("should propagate non-404 errors", async () => {
const serverError = new ErrorResponse({ Message: "Server error" }, 500);
mockApiService.send.mockRejectedValue(serverError);
await expect(
firstValueFrom(service.getSummaryDataByDateRange$(orgId, startDate, endDate)),
).rejects.toBeInstanceOf(ErrorResponse);
});
});
describe("renewReportFileUpload$", () => {
it("should call GET /reports/organizations/{orgId}/{reportId}/renew-upload and return AccessReportFileApi", async () => {
const rawResponse = {
reportFileUploadUrl: "https://storage.example.com/renewed-upload",
fileUploadType: FileUploadType.Azure,
reportResponse: {
id: reportId,
organizationId: orgId,
creationDate: "2024-01-01T00:00:00Z",
contentEncryptionKey: "enc-key",
},
};
mockApiService.send.mockResolvedValue(rawResponse);
const result = await firstValueFrom(service.renewReportFileUploadLink$(orgId, reportId));
expect(mockApiService.send).toHaveBeenCalledWith(
"GET",
`/reports/organizations/${orgId}/${reportId}/file/renew`,
null,
true,
true,
);
expect(result).toBeInstanceOf(AccessReportFileApi);
expect(result.reportResponse.id).toBe(reportId);
expect(result.reportFileUploadUrl).toBe("https://storage.example.com/renewed-upload");
});
it("should propagate API errors", async () => {
mockApiService.send.mockRejectedValue(new Error("Renew failed"));
await expect(
firstValueFrom(service.renewReportFileUploadLink$(orgId, reportId)),
).rejects.toThrow("Renew failed");
});
});
describe("deleteReport$", () => {
it("should call DELETE /reports/organizations/{orgId}/{reportId}", async () => {
mockApiService.send.mockResolvedValue(undefined);
await firstValueFrom(service.deleteReport$(orgId, reportId));
expect(mockApiService.send).toHaveBeenCalledWith(
"DELETE",
`/reports/organizations/${orgId}/${reportId}`,
null,
true,
false,
);
});
it("should propagate API errors", async () => {
mockApiService.send.mockRejectedValue(new Error("Delete failed"));
await expect(firstValueFrom(service.deleteReport$(orgId, reportId))).rejects.toThrow(
"Delete failed",
);
});
});
describe("uploadReportFile$", () => {
it("should call POST /reports/organizations/{orgId}/{reportId}/file/report-data with FormData", async () => {
mockApiService.send.mockResolvedValue(undefined);
const file = new File(["file content"], "report.bin", { type: "application/octet-stream" });
await firstValueFrom(service.uploadReportFile$(orgId, reportId, file, reportFileId));
expect(mockApiService.send).toHaveBeenCalledWith(
"POST",
`/reports/organizations/${orgId}/${reportId}/file?reportFileId=${reportFileId}`,
expect.any(FormData),
true,
false,
);
const sentFormData: FormData = mockApiService.send.mock.calls[0][2] as FormData;
expect(sentFormData.get("file")).toBeInstanceOf(File);
expect((sentFormData.get("file") as File).name).toBe("report.bin");
});
it("should propagate API errors", async () => {
mockApiService.send.mockRejectedValue(new Error("Upload failed"));
const file = new File(["content"], "report.bin");
await expect(
firstValueFrom(service.uploadReportFile$(orgId, reportId, file, reportFileId)),
).rejects.toThrow("Upload failed");
});
});
describe("downloadReportFile$", () => {
it("should call GET /reports/organizations/{orgId}/{reportId}/file/download and return blob with fileName", async () => {
const blob = new Blob(["file content"], { type: "application/octet-stream" });
const sendResponse = { blob, fileName: "report.bin" };
mockApiService.send.mockResolvedValue(sendResponse);
const result = await firstValueFrom(service.downloadReportFile$(orgId, reportId));
expect(mockApiService.send).toHaveBeenCalledWith(
"GET",
`/reports/organizations/${orgId}/${reportId}/file/download`,
null,
true,
true,
);
expect(result.blob).toBe(blob);
expect(result.fileName).toBe("report.bin");
});
it("should propagate API errors", async () => {
mockApiService.send.mockRejectedValue(new Error("Download failed"));
await expect(firstValueFrom(service.downloadReportFile$(orgId, reportId))).rejects.toThrow(
"Download failed",
);
});
});
describe("updateReportSettings$", () => {
it("should call PATCH /reports/organizations/{orgId}/{reportId} and return AccessReportApi", async () => {
const rawResponse = {
id: reportId,
organizationId: orgId,
creationDate: "2024-01-01T00:00:00Z",
summaryData: "encrypted-summary",
};
mockApiService.send.mockResolvedValue(rawResponse);
const request: AccessReportSettingsUpdateRequest = {
summaryData: "encrypted-summary",
applicationData: "encrypted-apps",
metrics: mockMetrics,
};
const result = await firstValueFrom(service.updateReportSettings$(orgId, reportId, request));
expect(mockApiService.send).toHaveBeenCalledWith(
"PATCH",
`/reports/organizations/${orgId}/${reportId}`,
request,
true,
true,
);
expect(result).toBeInstanceOf(AccessReportApi);
expect(result.id).toBe(reportId);
});
it("should propagate API errors", async () => {
mockApiService.send.mockRejectedValue(new Error("Update failed"));
await expect(
firstValueFrom(service.updateReportSettings$(orgId, reportId, {} as any)),
).rejects.toThrow("Update failed");
});
});
});

View File

@ -0,0 +1,200 @@
import { catchError, from, map, Observable, of, throwError } from "rxjs";
import { ApiService } from "@bitwarden/common/abstractions/api.service";
import { ErrorResponse } from "@bitwarden/common/models/response/error.response";
import { OrganizationId, OrganizationReportId } from "@bitwarden/common/types/guid";
import {
AccessReportApi,
AccessReportFileApi,
AccessReportMetricsApi,
AccessReportSummaryApi,
} from "../../../models";
import {
AccessIntelligenceApiService,
AccessReportCreateRequest,
AccessReportLegacyCreateRequest,
AccessReportSettingsUpdateRequest,
} from "../../abstractions/access-intelligence-api.service";
export class DefaultAccessIntelligenceApiService extends AccessIntelligenceApiService {
constructor(private apiService: ApiService) {
super();
}
getLatestReport$(orgId: OrganizationId): Observable<AccessReportApi> {
const response = this.apiService.send(
"GET",
`/reports/organizations/${orgId.toString()}/latest`,
null,
true,
true,
);
return from(response).pipe(map((res) => new AccessReportApi(res)));
}
createReport$(
orgId: OrganizationId,
request: AccessReportCreateRequest,
): Observable<AccessReportFileApi> {
const response = this.apiService.send(
"POST",
`/reports/organizations/${orgId.toString()}`,
request,
true,
true,
);
return from(response).pipe(map((response) => new AccessReportFileApi(response)));
}
createLegacyReport$(
orgId: OrganizationId,
request: AccessReportLegacyCreateRequest,
): Observable<AccessReportApi> {
const response = this.apiService.send(
"POST",
`/reports/organizations/${orgId.toString()}`,
request,
true,
true,
);
return from(response).pipe(map((res) => new AccessReportApi(res)));
}
updateSummaryData$(
orgId: OrganizationId,
reportId: OrganizationReportId,
summaryData: string,
metrics?: AccessReportMetricsApi,
): Observable<AccessReportApi> {
const response = this.apiService.send(
"PATCH",
`/reports/organizations/${orgId.toString()}/data/summary/${reportId.toString()}`,
{ summaryData, metrics, reportId: reportId, organizationId: orgId },
true,
true,
);
return from(response).pipe(map((response) => new AccessReportApi(response)));
}
updateApplicationData$(
orgId: OrganizationId,
reportId: OrganizationReportId,
applicationData: string,
): Observable<AccessReportApi> {
const response = this.apiService.send(
"PATCH",
`/reports/organizations/${orgId.toString()}/data/application/${reportId.toString()}`,
{ applicationData, id: reportId, organizationId: orgId },
true,
true,
);
return from(response).pipe(map((response) => new AccessReportApi(response)));
}
getSummaryDataByDateRange$(
orgId: OrganizationId,
startDate: Date,
endDate: Date,
): Observable<AccessReportSummaryApi[]> {
const startDateStr = startDate.toISOString().split("T")[0];
const endDateStr = endDate.toISOString().split("T")[0];
const dbResponse = this.apiService.send(
"GET",
`/reports/organizations/${orgId.toString()}/data/summary?startDate=${startDateStr}&endDate=${endDateStr}`,
null,
true,
true,
);
return from(dbResponse).pipe(
map((response: any[]) =>
Array.isArray(response) ? response.map((r) => new AccessReportSummaryApi(r)) : [],
),
catchError((error: unknown) => {
if (error instanceof ErrorResponse && error.statusCode === 404) {
return of([]);
}
return throwError(() => error);
}),
);
}
renewReportFileUploadLink$(
orgId: OrganizationId,
reportId: OrganizationReportId,
): Observable<AccessReportFileApi> {
const response = this.apiService.send(
"GET",
`/reports/organizations/${orgId}/${reportId}/file/renew`,
null,
true,
true,
);
return from(response).pipe(map((res) => new AccessReportFileApi(res)));
}
deleteReport$(orgId: OrganizationId, reportId: OrganizationReportId): Observable<void> {
const response = this.apiService.send(
"DELETE",
`/reports/organizations/${orgId}/${reportId}`,
null,
true,
false,
);
return from(response);
}
uploadReportFile$(
orgId: OrganizationId,
reportId: OrganizationReportId,
file: File,
reportFileId: string,
): Observable<void> {
const formData = new FormData();
formData.append("file", file, file.name);
const response = this.apiService.send(
"POST",
`/reports/organizations/${orgId}/${reportId}/file?reportFileId=${reportFileId}`,
formData,
true,
false,
);
return from(response);
}
downloadReportFile$(
orgId: OrganizationId,
reportId: OrganizationReportId,
): Observable<{ blob: Blob; fileName: string }> {
const response = this.apiService.send(
"GET",
`/reports/organizations/${orgId}/${reportId}/file/download`,
null,
true,
true,
);
return from(response);
}
updateReportSettings$(
orgId: OrganizationId,
reportId: OrganizationReportId,
request: AccessReportSettingsUpdateRequest,
): Observable<AccessReportApi> {
const response = this.apiService.send(
"PATCH",
`/reports/organizations/${orgId}/${reportId}`,
request,
true,
true,
);
return from(response).pipe(map((response) => new AccessReportApi(response)));
}
}

View File

@ -1694,11 +1694,19 @@ export class ApiService implements ApiServiceAbstraction {
const responseType = response.headers.get("content-type");
const responseIsJson = responseType != null && responseType.indexOf("application/json") !== -1;
const responseIsCsv = responseType != null && responseType.indexOf("text/csv") !== -1;
const responseIsBlob =
responseType != null && responseType.indexOf("application/octet-stream") !== -1;
if (hasResponse && response.status === HttpStatusCode.Ok && responseIsJson) {
const responseJson = await response.json();
return responseJson;
} else if (hasResponse && response.status === HttpStatusCode.Ok && responseIsCsv) {
return await response.text();
} else if (hasResponse && response.status === HttpStatusCode.Ok && responseIsBlob) {
const disposition = response.headers.get("Content-Disposition") ?? "";
const match = disposition.match(/filename[^;=\n]*=((['"]).*?\2|[^;\n]*)/);
const fileName = match ? match[1].replace(/['"]/g, "") : "download";
const blob = await response.blob();
return { blob, fileName };
} else if (
response.status !== HttpStatusCode.Ok &&
response.status !== HttpStatusCode.NoContent