From 045dc3f54d194615b6d2647e76b2d9067b4921a3 Mon Sep 17 00:00:00 2001 From: RiteshHMCTS Date: Mon, 25 May 2026 13:34:14 +0100 Subject: [PATCH 1/9] redact sensitive infromation from logs --- package.json | 2 +- projects/ccd-case-ui-toolkit/package.json | 2 +- .../src/lib/app-config.mock.ts | 5 +- .../case-edit-page.component.ts | 6 +- .../case-editor/services/cases.service.ts | 14 +- .../event-completion-state-machine.service.ts | 1 - .../services/work-allocation.service.ts | 1 - .../case-history/case-history.component.ts | 5 +- .../case-event-trigger.component.ts | 1 - .../case-view/case-view.component.ts | 6 +- .../case-viewer/case-viewer.component.ts | 5 +- .../case-viewer/services/case.resolver.ts | 13 +- .../event-start-state-machine.service.ts | 3 - .../write-address-field.component.spec.ts | 10 +- .../address/write-address-field.component.ts | 5 +- .../write-collection-field.component.ts | 5 +- .../write-document-field.component.ts | 1 - ...query-check-your-answers.component.spec.ts | 7 +- .../query-check-your-answers.component.ts | 13 +- .../query-confirmation.component.spec.ts | 20 ++- .../query-confirmation.component.ts | 8 +- .../query-write-raise-query.component.spec.ts | 7 +- .../query-write-raise-query.component.ts | 5 +- ...y-write-respond-to-query.component.spec.ts | 35 ++-- .../query-write-respond-to-query.component.ts | 13 +- .../services/query-management.service.spec.ts | 10 +- .../services/query-management.service.ts | 16 +- .../search-filters.component.ts | 9 +- .../workbasket-filters.component.ts | 7 +- .../services/condition-parser.service.ts | 9 +- .../domain/definition/case-field.model.ts | 5 +- .../src/lib/shared/json-utils.ts | 7 +- .../activity/activity.polling.service.ts | 9 +- .../services/activity/activity.service.ts | 11 +- .../shared/services/fields/fields.utils.ts | 5 +- .../services/http/http-error.service.ts | 7 +- .../src/lib/shared/services/index.ts | 1 + .../src/lib/shared/services/logging/index.ts | 1 + .../logging/structured-logger.service.spec.ts | 75 ++++++++ .../logging/structured-logger.service.ts | 165 ++++++++++++++++++ .../organisation/organisation.service.ts | 4 +- .../services/session/session-storage.guard.ts | 6 +- .../services/utils/retry/retry.service.ts | 27 +-- 43 files changed, 451 insertions(+), 116 deletions(-) create mode 100644 projects/ccd-case-ui-toolkit/src/lib/shared/services/logging/index.ts create mode 100644 projects/ccd-case-ui-toolkit/src/lib/shared/services/logging/structured-logger.service.spec.ts create mode 100644 projects/ccd-case-ui-toolkit/src/lib/shared/services/logging/structured-logger.service.ts diff --git a/package.json b/package.json index 7cdcbde8c5..ae26055ccd 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@hmcts/ccd-case-ui-toolkit", - "version": "7.3.53", + "version": "7.3.53-exui-4648-rc1", "engines": { "node": ">=20.19.0" }, diff --git a/projects/ccd-case-ui-toolkit/package.json b/projects/ccd-case-ui-toolkit/package.json index 366713c27f..2a80eff8da 100644 --- a/projects/ccd-case-ui-toolkit/package.json +++ b/projects/ccd-case-ui-toolkit/package.json @@ -1,6 +1,6 @@ { "name": "@hmcts/ccd-case-ui-toolkit", - "version": "7.3.53", + "version": "7.3.53-exui-4648-rc1", "engines": { "node": ">=20.19.0" }, diff --git a/projects/ccd-case-ui-toolkit/src/lib/app-config.mock.ts b/projects/ccd-case-ui-toolkit/src/lib/app-config.mock.ts index cf17289af2..83ca64ed14 100644 --- a/projects/ccd-case-ui-toolkit/src/lib/app-config.mock.ts +++ b/projects/ccd-case-ui-toolkit/src/lib/app-config.mock.ts @@ -5,8 +5,11 @@ import { AccessManagementBasicViewMockModel, AccessManagementRequestReviewMockModel } from './app.config'; +import { StructuredLoggerService } from './shared/services/logging'; export class AppMockConfig implements AbstractAppConfig { + private readonly logger = new StructuredLoggerService(); + public getActivityBatchCollectionDelayMs(): number { return 0; } @@ -220,7 +223,7 @@ export class AppMockConfig implements AbstractAppConfig { } public logMessage(msg: string): void { - console.log(msg); + this.logger.info('Application log message.', { message: msg }); } public getEnableServiceSpecificMultiFollowups(): string[] { diff --git a/projects/ccd-case-ui-toolkit/src/lib/shared/components/case-editor/case-edit-page/case-edit-page.component.ts b/projects/ccd-case-ui-toolkit/src/lib/shared/components/case-editor/case-edit-page/case-edit-page.component.ts index 15469dba6a..b4c503c54e 100644 --- a/projects/ccd-case-ui-toolkit/src/lib/shared/components/case-editor/case-edit-page/case-edit-page.component.ts +++ b/projects/ccd-case-ui-toolkit/src/lib/shared/components/case-editor/case-edit-page/case-edit-page.component.ts @@ -9,7 +9,7 @@ import { CaseEventData } from '../../../domain/case-event-data.model'; import { CaseEventTrigger } from '../../../domain/case-view/case-event-trigger.model'; import { CaseField } from '../../../domain/definition'; import { DRAFT_PREFIX } from '../../../domain/draft.model'; -import { AddressesService, LoadingService, MultipageComponentStateService } from '../../../services'; +import { AddressesService, LoadingService, MultipageComponentStateService, StructuredLoggerService } from '../../../services'; import { CaseFieldService } from '../../../services/case-fields/case-field.service'; import { FieldsUtils } from '../../../services/fields'; import { FormErrorService } from '../../../services/form/form-error.service'; @@ -65,6 +65,7 @@ export class CaseEditPageComponent implements OnInit, AfterViewChecked, OnDestro public dialogRefAfterClosedSub: Subscription; public saveDraftSub: Subscription; public caseFormValidationErrorsSub: Subscription; + private readonly logger = new StructuredLoggerService(); private static scrollToTop(): void { window.scrollTo(0, 0); @@ -409,7 +410,6 @@ export class CaseEditPageComponent implements OnInit, AfterViewChecked, OnDestro if (!this.caseEdit.isSubmitting && !this.currentPageIsNotValid()) { this.addressService.setMandatoryError(false); - console.log('Case Edit Error', this.caseEdit.error); if (this.caseEdit.validPageList.findIndex(page=> page.id === this.currentPage.id) === -1) { this.caseEdit.validPageList.push(this.currentPage); } @@ -648,7 +648,7 @@ export class CaseEditPageComponent implements OnInit, AfterViewChecked, OnDestro this.formErrorService .mapFieldErrors(this.caseEdit.error.details.field_errors, this.editForm?.controls?.['data'] as FormGroup, 'validation'); } - console.log('handleError ', error); + this.logger.error('Case edit page handled an error.', { error }); } private resetErrors(): void { diff --git a/projects/ccd-case-ui-toolkit/src/lib/shared/components/case-editor/services/cases.service.ts b/projects/ccd-case-ui-toolkit/src/lib/shared/components/case-editor/services/cases.service.ts index 64eb15236a..14bd5aeabe 100644 --- a/projects/ccd-case-ui-toolkit/src/lib/shared/components/case-editor/services/cases.service.ts +++ b/projects/ccd-case-ui-toolkit/src/lib/shared/components/case-editor/services/cases.service.ts @@ -16,7 +16,7 @@ import { RoleRequestPayload, SpecificAccessRequest } from '../../../domain'; import { UserInfo } from '../../../domain/user/user-info.model'; -import { FieldsUtils, HttpErrorService, HttpService, LoadingService, OrderService, RetryUtil, SessionStorageService } from '../../../services'; +import { FieldsUtils, HttpErrorService, HttpService, LoadingService, OrderService, RetryUtil, SessionStorageService, StructuredLoggerService } from '../../../services'; import { LinkedCasesResponse } from '../../palette/linked-cases/domain/linked-cases.model'; import { CaseAccessUtils } from '../case-access-utils'; import { WizardPage } from '../domain'; @@ -26,6 +26,8 @@ import { CaseEditComponent } from '../case-edit'; @Injectable() export class CasesService { + private readonly logger = new StructuredLoggerService(); + // Internal (UI) API public static readonly V2_MEDIATYPE_CASE_VIEW = 'application/vnd.uk.gov.hmcts.ccd-data-store-api.ui-case-view.v2+json'; public static readonly V2_MEDIATYPE_START_CASE_TRIGGER = @@ -107,11 +109,10 @@ export class CasesService { const artificialDelay: number = this.appConfig.getTimeoutsCaseRetrievalArtificialDelay(); const timeoutPeriods = this.appConfig.getTimeoutsForCaseRetrieval(); - console.log(`Timeout periods: ${timeoutPeriods} seconds.`); if (timeoutPeriods && timeoutPeriods.length > 0 && timeoutPeriods[0] > 0) { http$ = this.retryUtil.pipeTimeoutMechanismOn(http$, artificialDelay, timeoutPeriods); } else { - console.warn('Skipping to pipe a retry mechanism!'); + this.logger.warn('Skipping retry mechanism for case view retrieval.'); } http$ = this.pipeErrorProcessor(http$); @@ -123,8 +124,11 @@ export class CasesService { private pipeErrorProcessor(in$: Observable): Observable { const out$ = in$.pipe(catchError(error => { - console.error(`Error while getting case view with getCaseViewV2! Error type: '${typeof error}, Error name: '${error?.name}'`); - console.error(error); + this.logger.error('Error while getting case view with getCaseViewV2.', { + error, + errorName: error?.name, + errorType: typeof error + }); this.errorService.setError(error); return throwError(error); })); diff --git a/projects/ccd-case-ui-toolkit/src/lib/shared/components/case-editor/services/event-completion-state-machine.service.ts b/projects/ccd-case-ui-toolkit/src/lib/shared/components/case-editor/services/event-completion-state-machine.service.ts index 7ebee515b0..37069c38fb 100644 --- a/projects/ccd-case-ui-toolkit/src/lib/shared/components/case-editor/services/event-completion-state-machine.service.ts +++ b/projects/ccd-case-ui-toolkit/src/lib/shared/components/case-editor/services/event-completion-state-machine.service.ts @@ -186,7 +186,6 @@ export class EventCompletionStateMachineService { public entryActionForStateFinal(state: State, context: EventCompletionStateMachineContext): void { // Final actions can be performed here, the state machine finished running - console.log('FINAL'); } public addTransitionsForStateCheckTasksCanBeCompleted(): void { diff --git a/projects/ccd-case-ui-toolkit/src/lib/shared/components/case-editor/services/work-allocation.service.ts b/projects/ccd-case-ui-toolkit/src/lib/shared/components/case-editor/services/work-allocation.service.ts index 822965ac6b..7ddce35372 100644 --- a/projects/ccd-case-ui-toolkit/src/lib/shared/components/case-editor/services/work-allocation.service.ts +++ b/projects/ccd-case-ui-toolkit/src/lib/shared/components/case-editor/services/work-allocation.service.ts @@ -52,7 +52,6 @@ export class WorkAllocationService { // explicitly eat away 401 error and 400 error if (error && error.status && (error.status === 401 || error.status === 400)) { // do nothing - console.log('error status 401 or 400', error); } else { return throwError(error); } diff --git a/projects/ccd-case-ui-toolkit/src/lib/shared/components/case-history/case-history.component.ts b/projects/ccd-case-ui-toolkit/src/lib/shared/components/case-history/case-history.component.ts index a2ef385f1f..c0bc89f23b 100644 --- a/projects/ccd-case-ui-toolkit/src/lib/shared/components/case-history/case-history.component.ts +++ b/projects/ccd-case-ui-toolkit/src/lib/shared/components/case-history/case-history.component.ts @@ -7,6 +7,7 @@ import { CaseTab } from '../../domain/case-view/case-tab.model'; import { CaseView } from '../../domain/case-view/case-view.model'; import { HttpError } from '../../domain/http/http-error.model'; import { AlertService } from '../../services/alert/alert.service'; +import { StructuredLoggerService } from '../../services/logging'; import { OrderService } from '../../services/order/order.service'; import { CaseNotifier } from '../case-editor/services/case.notifier'; import { CaseHistory } from './domain/case-history.model'; @@ -19,6 +20,8 @@ import { CaseHistoryService } from './services/case-history.service'; standalone: false }) export class CaseHistoryComponent implements OnInit, OnDestroy { + private readonly logger = new StructuredLoggerService(); + public static readonly PARAM_EVENT_ID = 'eid'; private static readonly ERROR_MESSAGE = 'No case history to show'; @@ -59,7 +62,7 @@ export class CaseHistoryComponent implements OnInit, OnDestroy { this.tabs = this.sortTabFieldsAndFilterTabs(this.tabs); }), catchError(error => { - console.error(error); + this.logger.error('Error while getting case history.', { error }); if (error.status !== 401 && error.status !== 403) { this.alertService.error(error.message); } diff --git a/projects/ccd-case-ui-toolkit/src/lib/shared/components/case-viewer/case-event-trigger/case-event-trigger.component.ts b/projects/ccd-case-ui-toolkit/src/lib/shared/components/case-viewer/case-event-trigger/case-event-trigger.component.ts index f2552880b9..0e88e8eb00 100644 --- a/projects/ccd-case-ui-toolkit/src/lib/shared/components/case-viewer/case-event-trigger/case-event-trigger.component.ts +++ b/projects/ccd-case-ui-toolkit/src/lib/shared/components/case-viewer/case-event-trigger/case-event-trigger.component.ts @@ -56,7 +56,6 @@ export class CaseEventTriggerComponent implements OnInit, OnDestroy { if (this.activityPollingService.isEnabled) { this.ngZone.runOutsideAngular( () => { this.activitySubscription = this.postEditActivity().subscribe(() => { - // console.log('Posted EDIT activity and result is: ' + JSON.stringify(_resolved)); }); }); } diff --git a/projects/ccd-case-ui-toolkit/src/lib/shared/components/case-viewer/case-view/case-view.component.ts b/projects/ccd-case-ui-toolkit/src/lib/shared/components/case-viewer/case-view/case-view.component.ts index e86d27ac8e..bdafc6b419 100644 --- a/projects/ccd-case-ui-toolkit/src/lib/shared/components/case-viewer/case-view/case-view.component.ts +++ b/projects/ccd-case-ui-toolkit/src/lib/shared/components/case-viewer/case-view/case-view.component.ts @@ -6,6 +6,7 @@ import { CaseView } from '../../../domain/case-view/case-view.model'; import { Draft } from '../../../domain/draft.model'; import { AlertService } from '../../../services/alert/alert.service'; import { DraftService } from '../../../services/draft/draft.service'; +import { StructuredLoggerService } from '../../../services/logging'; import { NavigationNotifierService } from '../../../services/navigation/navigation-notifier.service'; import { CaseNotifier } from '../../case-editor/services/case.notifier'; import { CasesService } from '../../case-editor/services/cases.service'; @@ -17,6 +18,8 @@ import { CasesService } from '../../case-editor/services/cases.service'; standalone: false }) export class CaseViewComponent implements OnInit, OnDestroy { + private readonly logger = new StructuredLoggerService(); + @Input() public case: string; @@ -79,8 +82,7 @@ export class CaseViewComponent implements OnInit, OnDestroy { private checkErrorGettingCaseView(error: any) { // TODO Should be logged to remote logging infrastructure - console.error('Called checkErrorGettingCaseView.'); - console.error(error); + this.logger.error('Error while getting case view.', { error }); if (error.status !== 401 && error.status !== 403) { this.alertService.error(error.message); } diff --git a/projects/ccd-case-ui-toolkit/src/lib/shared/components/case-viewer/case-viewer.component.ts b/projects/ccd-case-ui-toolkit/src/lib/shared/components/case-viewer/case-viewer.component.ts index 09ed75122e..1b6b5b63fc 100644 --- a/projects/ccd-case-ui-toolkit/src/lib/shared/components/case-viewer/case-viewer.component.ts +++ b/projects/ccd-case-ui-toolkit/src/lib/shared/components/case-viewer/case-viewer.component.ts @@ -4,7 +4,7 @@ import { Subscription } from 'rxjs'; import { AbstractAppConfig } from '../../../app.config'; import { CaseTab, CaseView } from '../../domain'; import { CaseNotifier } from '../case-editor'; -import { OrderService } from '../../services'; +import { OrderService, StructuredLoggerService } from '../../services'; @Component({ selector: 'ccd-case-viewer', @@ -16,6 +16,7 @@ export class CaseViewerComponent implements OnInit, OnDestroy { public static readonly METADATA_FIELD_ACCESS_GRANTED_ID = '[ACCESS_GRANTED]'; public static readonly NON_STANDARD_USER_ACCESS_TYPES = ['CHALLENGED', 'SPECIFIC']; public static readonly BASIC_USER_ACCESS_TYPES = 'BASIC'; + private readonly logger = new StructuredLoggerService(); @Input() public hasPrint = true; @Input() public hasEventSelector = true; @@ -52,7 +53,7 @@ export class CaseViewerComponent implements OnInit, OnDestroy { this.setUserAccessType(this.caseDetails); } else { this.caseSubscription = this.caseNotifier.caseView.subscribe((caseDetails) => { - console.info('Setting the case into case viewer component as retrieved from XHR request.'); + this.logger.info('Setting the case into case viewer component as retrieved from XHR request.'); this.caseDetails = caseDetails; this.setUserAccessType(this.caseDetails); }); diff --git a/projects/ccd-case-ui-toolkit/src/lib/shared/components/case-viewer/services/case.resolver.ts b/projects/ccd-case-ui-toolkit/src/lib/shared/components/case-viewer/services/case.resolver.ts index 23a9d2e262..e26ab3fbfe 100644 --- a/projects/ccd-case-ui-toolkit/src/lib/shared/components/case-viewer/services/case.resolver.ts +++ b/projects/ccd-case-ui-toolkit/src/lib/shared/components/case-viewer/services/case.resolver.ts @@ -5,7 +5,7 @@ import { of, throwError } from 'rxjs'; import { catchError, filter, map } from 'rxjs/operators'; import { AbstractAppConfig } from '../../../../app.config'; import { CaseView, Draft } from '../../../domain'; -import { DraftService, NavigationOrigin, SessionStorageService } from '../../../services'; +import { DraftService, NavigationOrigin, SessionStorageService, StructuredLoggerService } from '../../../services'; import { NavigationNotifierService } from '../../../services/navigation/navigation-notifier.service'; import { PUI_CASE_MANAGER, USER_DETAILS } from '../../../utils'; import { safeJsonParse } from '../../../json-utils'; @@ -21,6 +21,8 @@ export class CaseResolver implements Resolve { public static defaultWAPage = '/work/my-work/list'; public static defaultPage = '/cases'; + private readonly logger = new StructuredLoggerService(); + // we need to run the CaseResolver on every child route of 'case/:jid/:ctid/:cid' // this is achieved with runGuardsAndResolvers: 'always' configuration // we cache the case view to avoid retrieving it for each child route @@ -42,12 +44,12 @@ export class CaseResolver implements Resolve { // Prevent resolving if eventId=queryManagementRespondQuery is in the URL if (currentUrl.includes(CaseResolver.EVENT_ID_QM_RESPOND_TO_QUERY)) { - console.info('Skipping resolve for event queryManagementRespondQuery.'); + this.logger.info('Skipping resolve for event queryManagementRespondQuery.'); this.goToDefaultPage(); } if (!cid) { - console.info('No case ID available in the route. Will navigate to case list.'); + this.logger.info('No case ID available in the route. Will navigate to case list.'); // when redirected to case view after a case created, and the user has no READ access, // the post returns no id this.navigateToCaseList(); @@ -112,8 +114,7 @@ export class CaseResolver implements Resolve { } private processErrorInCaseFetch(error: any, caseReference: string) { - console.error('!!! processErrorInCaseFetch !!!'); - console.error(error); + this.logger.error('Error while fetching case view.', { caseReference, error }); // TODO Should be logged to remote logging infrastructure if (error.status === 400) { this.router.navigate(['/search/noresults']); @@ -142,7 +143,7 @@ export class CaseResolver implements Resolve { // as discussed for EUI-5456, need functionality to go to default page private goToDefaultPage(): void { - console.info('Going to default page!'); + this.logger.info('Going to default page.'); const userDetails = safeJsonParse(this.sessionStorage.getItem(USER_DETAILS)); userDetails && userDetails.roles && !userDetails.roles.includes(PUI_CASE_MANAGER) diff --git a/projects/ccd-case-ui-toolkit/src/lib/shared/components/event-start/services/event-start-state-machine.service.ts b/projects/ccd-case-ui-toolkit/src/lib/shared/components/event-start/services/event-start-state-machine.service.ts index aca909de2c..4deec79839 100644 --- a/projects/ccd-case-ui-toolkit/src/lib/shared/components/event-start/services/event-start-state-machine.service.ts +++ b/projects/ccd-case-ui-toolkit/src/lib/shared/components/event-start/services/event-start-state-machine.service.ts @@ -187,9 +187,7 @@ export class EventStartStateMachineService { task = context.tasks[0]; } - const taskStr = JSON.stringify(task); this.abstractConfig?.logMessage?.(`entryActionForStateOneTaskAssignedToUser: task_state ${task?.task_state} for task id ${task?.id}`); - console.log('entryActionForStateOneTaskAssignedToUser: setting client context task_data to ' + taskStr); // Store task to session const currentLanguage = context.cookieService.getCookie('exui-preferred-language'); const clientContext = { @@ -232,7 +230,6 @@ export class EventStartStateMachineService { public finalAction(state: State): void { // Final actions can be performed here, the state machine finished running - // console.log('FINAL', state); return; } diff --git a/projects/ccd-case-ui-toolkit/src/lib/shared/components/palette/address/write-address-field.component.spec.ts b/projects/ccd-case-ui-toolkit/src/lib/shared/components/palette/address/write-address-field.component.spec.ts index b3fd2c90ba..0c60fbab80 100644 --- a/projects/ccd-case-ui-toolkit/src/lib/shared/components/palette/address/write-address-field.component.spec.ts +++ b/projects/ccd-case-ui-toolkit/src/lib/shared/components/palette/address/write-address-field.component.spec.ts @@ -382,13 +382,19 @@ describe('WriteAddressFieldComponent', () => { const errorMessage = 'Service error'; const errorSubject = new Subject(); addressesService.getAddressesForPostcode.and.returnValue(errorSubject.asObservable()); - spyOn(console, 'log'); + const logSpy = spyOn(console, 'log'); + const errorSpy = spyOn(console, 'error'); writeAddressFieldComponent.postcode.setValue(POSTCODE); writeAddressFieldComponent.findAddress(); expect(writeAddressFieldComponent.loadingAddresses).toBeTruthy(); errorSubject.error(errorMessage); expect(writeAddressFieldComponent.loadingAddresses).toBeFalsy(); - expect(console.log).toHaveBeenCalledWith(`An error occurred retrieving addresses for postcode ${POSTCODE}. ${errorMessage}`); + expect(logSpy).not.toHaveBeenCalled(); + expect(errorSpy.calls.mostRecent().args[0]).toEqual(jasmine.objectContaining({ + level: 'error', + message: 'An error occurred retrieving addresses for postcode.' + })); + expect(JSON.stringify(errorSpy.calls.mostRecent().args[0])).not.toContain(POSTCODE); }); it('should not set loadingAddresses to true when postcode is missing', () => { diff --git a/projects/ccd-case-ui-toolkit/src/lib/shared/components/palette/address/write-address-field.component.ts b/projects/ccd-case-ui-toolkit/src/lib/shared/components/palette/address/write-address-field.component.ts index ad17967e3b..159a5e3ba4 100644 --- a/projects/ccd-case-ui-toolkit/src/lib/shared/components/palette/address/write-address-field.component.ts +++ b/projects/ccd-case-ui-toolkit/src/lib/shared/components/palette/address/write-address-field.component.ts @@ -4,6 +4,7 @@ import { AddressValidationConstants } from '../../../commons/address-validation- import { FocusElementDirective } from '../../../directives/focus-element'; import { AddressModel } from '../../../domain/addresses/address.model'; import { AddressesService } from '../../../services/addresses/addresses.service'; +import { StructuredLoggerService } from '../../../services/logging'; import { AbstractFieldWriteComponent } from '../base-field/abstract-field-write.component'; import { WriteComplexFieldComponent } from '../complex/write-complex-field.component'; import { IsCompoundPipe } from '../utils/is-compound.pipe'; @@ -16,6 +17,8 @@ import { AddressOption } from './address-option.model'; standalone: false }) export class WriteAddressFieldComponent extends AbstractFieldWriteComponent implements OnInit, OnChanges { + private readonly logger = new StructuredLoggerService(); + @ViewChild('writeComplexFieldComponent', { static: false }) public writeComplexFieldComponent: WriteComplexFieldComponent; @@ -81,7 +84,7 @@ export class WriteAddressFieldComponent extends AbstractFieldWriteComponent impl ); }, (error) => { this.loadingAddresses = false; - console.log(`An error occurred retrieving addresses for postcode ${postcode}. ${error}`); + this.logger.error('An error occurred retrieving addresses for postcode.', { error }); }); this.addressList.setValue(undefined); this.refocusElement(); diff --git a/projects/ccd-case-ui-toolkit/src/lib/shared/components/palette/collection/write-collection-field.component.ts b/projects/ccd-case-ui-toolkit/src/lib/shared/components/palette/collection/write-collection-field.component.ts index 9f6e798a55..2839e23fbd 100644 --- a/projects/ccd-case-ui-toolkit/src/lib/shared/components/palette/collection/write-collection-field.component.ts +++ b/projects/ccd-case-ui-toolkit/src/lib/shared/components/palette/collection/write-collection-field.component.ts @@ -11,6 +11,7 @@ import { CaseField } from '../../../domain/definition/case-field.model'; import { Profile } from '../../../domain/profile/profile.model'; import { FieldsUtils } from '../../../services/fields/fields.utils'; import { FormValidatorsService } from '../../../services/form/form-validators.service'; +import { StructuredLoggerService } from '../../../services/logging'; import { ProfileNotifier } from '../../../services/profile/profile.notifier'; import { RemoveDialogComponent } from '../../dialogs/remove-dialog/remove-dialog.component'; import { AbstractFieldWriteComponent } from '../base-field/abstract-field-write.component'; @@ -31,6 +32,8 @@ type CollectionItem = { standalone: false }) export class WriteCollectionFieldComponent extends AbstractFieldWriteComponent implements OnInit, OnDestroy { + private readonly logger = new StructuredLoggerService(); + @Input() public caseFields: CaseField[] = []; @@ -195,7 +198,7 @@ export class WriteCollectionFieldComponent extends AbstractFieldWriteComponent i duration: 1000, offset: -150, }) - .subscribe(() => { }, console.error); + .subscribe(() => { }, error => this.logger.error('Error while scrolling collection item into view.', { error })); } this.focusLastItem(); diff --git a/projects/ccd-case-ui-toolkit/src/lib/shared/components/palette/document/write-document-field.component.ts b/projects/ccd-case-ui-toolkit/src/lib/shared/components/palette/document/write-document-field.component.ts index e7940f56f2..cedec6c9da 100644 --- a/projects/ccd-case-ui-toolkit/src/lib/shared/components/palette/document/write-document-field.component.ts +++ b/projects/ccd-case-ui-toolkit/src/lib/shared/components/palette/document/write-document-field.component.ts @@ -95,7 +95,6 @@ export class WriteDocumentFieldComponent extends AbstractFieldWriteComponent imp this.jurisdictionId = parts[parts.indexOf('case-create') + 1]; this.caseTypeId = parts[parts.indexOf('case-create') + 2]; this.caseId = null; - console.log(this.jurisdictionId); console.log(this.caseTypeId); } // use the documentManagement service to check if the document upload should use CDAM diff --git a/projects/ccd-case-ui-toolkit/src/lib/shared/components/palette/query-management/components/query-check-your-answers/query-check-your-answers.component.spec.ts b/projects/ccd-case-ui-toolkit/src/lib/shared/components/palette/query-management/components/query-check-your-answers/query-check-your-answers.component.spec.ts index 21133bddb9..a0004da75b 100644 --- a/projects/ccd-case-ui-toolkit/src/lib/shared/components/palette/query-management/components/query-check-your-answers/query-check-your-answers.component.spec.ts +++ b/projects/ccd-case-ui-toolkit/src/lib/shared/components/palette/query-management/components/query-check-your-answers/query-check-your-answers.component.spec.ts @@ -873,12 +873,15 @@ describe('QueryCheckYourAnswersComponent', () => { component.fieldId = 'anyField'; component.qmCaseQueriesCollectionData = { anyField: { partyName: '', roleOnCase: '', caseMessages: [] } }; - spyOn(console, 'error'); + const errorSpy = spyOn(console, 'error'); spyOn(window, 'scrollTo'); component.submit(); - expect(console.error).toHaveBeenCalledWith('Error: No task to complete was found'); + expect(errorSpy.calls.mostRecent().args[0]).toEqual(jasmine.objectContaining({ + level: 'error', + message: 'No task to complete was found.' + })); // errorMessages should be set with fallback content expect(component.errorMessages?.length).toBeGreaterThan(0); diff --git a/projects/ccd-case-ui-toolkit/src/lib/shared/components/palette/query-management/components/query-check-your-answers/query-check-your-answers.component.ts b/projects/ccd-case-ui-toolkit/src/lib/shared/components/palette/query-management/components/query-check-your-answers/query-check-your-answers.component.ts index ea0aedffc5..9793ba103c 100644 --- a/projects/ccd-case-ui-toolkit/src/lib/shared/components/palette/query-management/components/query-check-your-answers/query-check-your-answers.component.ts +++ b/projects/ccd-case-ui-toolkit/src/lib/shared/components/palette/query-management/components/query-check-your-answers/query-check-your-answers.component.ts @@ -20,7 +20,8 @@ import { Task } from '../../../../../domain/work-allocation/Task'; import { QueryManagementService } from '../../services/query-management.service'; import { AlertService, - ErrorNotifierService + ErrorNotifierService, + StructuredLoggerService } from '../../../../../services'; @Component({ @@ -30,6 +31,8 @@ import { standalone: false }) export class QueryCheckYourAnswersComponent implements OnInit, OnDestroy { + private readonly logger = new StructuredLoggerService(); + private readonly RAISE_A_QUERY_EVENT_TRIGGER_ID = 'queryManagementRaiseQuery'; private readonly RESPOND_TO_QUERY_EVENT_TRIGGER_ID = 'queryManagementRespondQuery'; private readonly CASE_QUERIES_COLLECTION_ID = 'CaseQueriesCollection'; @@ -134,7 +137,7 @@ export class QueryCheckYourAnswersComponent implements OnInit, OnDestroy { if (error.status !== 401 && error.status !== 403) { this.errorNotifierService.announceError(error); this.alertService.error({ phrase: error.message }); - console.error('Error occurred while fetching event data:', error); + this.logger.error('Error occurred while fetching event data.', { error }); this.callbackErrorsSubject.next(error); } else { this.errorMessages = [ @@ -199,7 +202,7 @@ export class QueryCheckYourAnswersComponent implements OnInit, OnDestroy { error: (error) => this.handleError(error) }); } else { - console.error('Error: No task to complete was found'); + this.logger.error('No task to complete was found.'); this.errorMessages = [ { title: 'Error', @@ -245,7 +248,7 @@ export class QueryCheckYourAnswersComponent implements OnInit, OnDestroy { } private handleError(error: any): void { - console.error('Error in API calls:', error); + this.logger.error('Error in query management API calls.', { error }); this.isSubmitting = false; if (this.isServiceErrorFound(error)){ @@ -276,7 +279,7 @@ export class QueryCheckYourAnswersComponent implements OnInit, OnDestroy { public setCaseQueriesCollectionData(): void { if (!this.eventData) { - console.warn('Event data not available; skipping collection setup.'); + this.logger.warn('Event data not available; skipping collection setup.'); } this.queryManagementService.setCaseQueriesCollectionData( diff --git a/projects/ccd-case-ui-toolkit/src/lib/shared/components/palette/query-management/components/query-confirmation/query-confirmation.component.spec.ts b/projects/ccd-case-ui-toolkit/src/lib/shared/components/palette/query-management/components/query-confirmation/query-confirmation.component.spec.ts index bd85d69580..a92ede35b4 100644 --- a/projects/ccd-case-ui-toolkit/src/lib/shared/components/palette/query-management/components/query-confirmation/query-confirmation.component.spec.ts +++ b/projects/ccd-case-ui-toolkit/src/lib/shared/components/palette/query-management/components/query-confirmation/query-confirmation.component.spec.ts @@ -168,11 +168,14 @@ describe('QueryConfirmationComponent', () => { component.eventResponseData = undefined as any; component.queryCreateContext = QueryCreateContext.RESPOND; - spyOn(console, 'warn'); + const warnSpy = spyOn(console, 'warn'); expect(() => component.resolveHmctsStaffRaisedQuery()).not.toThrow(); - expect(console.warn).toHaveBeenCalledWith('No event response data available.'); + expect(warnSpy.calls.mostRecent().args[0]).toEqual(jasmine.objectContaining({ + level: 'warn', + message: 'No event response data available.' + })); }); it('should call sessionStorageService.getItem when computing HMCTS staff flags on ngOnInit', () => { @@ -249,14 +252,17 @@ describe('QueryConfirmationComponent', () => { component.queryCreateContext = QueryCreateContext.RESPOND; component.eventResponseData = dummyCaseQueriesCollection; - spyOn(console, 'warn'); + const warnSpy = spyOn(console, 'warn'); component.resolveHmctsStaffRaisedQuery(); - expect(console.warn).toHaveBeenCalledWith( - 'No matching child found for messageId:', - 'nonexistent-id' - ); + expect(warnSpy.calls.mostRecent().args[0]).toEqual(jasmine.objectContaining({ + level: 'warn', + message: 'No matching child found for messageId.', + context: jasmine.objectContaining({ + messageId: 'nonexistent-id' + }) + })); // Component should exit early and not set isHmctsStaffRaisedQuery expect(component.isHmctsStaffRaisedQuery).toBeUndefined(); diff --git a/projects/ccd-case-ui-toolkit/src/lib/shared/components/palette/query-management/components/query-confirmation/query-confirmation.component.ts b/projects/ccd-case-ui-toolkit/src/lib/shared/components/palette/query-management/components/query-confirmation/query-confirmation.component.ts index 8dc4321757..8830f7372a 100644 --- a/projects/ccd-case-ui-toolkit/src/lib/shared/components/palette/query-management/components/query-confirmation/query-confirmation.component.ts +++ b/projects/ccd-case-ui-toolkit/src/lib/shared/components/palette/query-management/components/query-confirmation/query-confirmation.component.ts @@ -1,7 +1,7 @@ import { Component, Input, OnInit } from '@angular/core'; import { ActivatedRoute } from '@angular/router'; import { QueryCreateContext } from '../../models/query-create-context.enum'; -import { SessionStorageService } from '../../../../../services'; +import { SessionStorageService, StructuredLoggerService } from '../../../../../services'; import { isInternalUser, isJudiciaryUser } from '../../../../../utils'; import { CaseQueriesCollection, QueryListData } from '../../models'; @@ -11,6 +11,8 @@ import { CaseQueriesCollection, QueryListData } from '../../models'; standalone: false }) export class QueryConfirmationComponent implements OnInit { + private readonly logger = new StructuredLoggerService(); + @Input() public queryCreateContext: QueryCreateContext; @Input() public callbackConfirmationMessageText: { [key: string]: string } = {}; @Input() public eventResponseData: CaseQueriesCollection; @@ -49,7 +51,7 @@ export class QueryConfirmationComponent implements OnInit { public resolveHmctsStaffRaisedQuery(): void { const messageId = this.route.snapshot.params.dataid; if (!this.eventResponseData) { - console.warn('No event response data available.'); + this.logger.warn('No event response data available.'); return; } @@ -68,7 +70,7 @@ export class QueryConfirmationComponent implements OnInit { .find((c) => c.parentId === messageId); if (!child) { - console.warn('No matching child found for messageId:', messageId); + this.logger.warn('No matching child found for messageId.', { messageId }); return; } diff --git a/projects/ccd-case-ui-toolkit/src/lib/shared/components/palette/query-management/components/query-write/query-write-raise-query/query-write-raise-query.component.spec.ts b/projects/ccd-case-ui-toolkit/src/lib/shared/components/palette/query-management/components/query-write/query-write-raise-query/query-write-raise-query.component.spec.ts index 3f70cf153a..ae51a8f348 100644 --- a/projects/ccd-case-ui-toolkit/src/lib/shared/components/palette/query-management/components/query-write/query-write-raise-query/query-write-raise-query.component.spec.ts +++ b/projects/ccd-case-ui-toolkit/src/lib/shared/components/palette/query-management/components/query-write/query-write-raise-query/query-write-raise-query.component.spec.ts @@ -154,10 +154,13 @@ describe('QueryWriteRaiseQueryComponent', () => { it('should warn and return false when eventData is null in setCaseQueriesCollectionData()', () => { - spyOn(console, 'warn'); + const warnSpy = spyOn(console, 'warn'); component.eventData = null; const result = component.setCaseQueriesCollectionData(); - expect(console.warn).toHaveBeenCalledWith('Event data not available; skipping collection setup.'); + expect(warnSpy.calls.mostRecent().args[0]).toEqual(jasmine.objectContaining({ + level: 'warn', + message: 'Event data not available; skipping collection setup.' + })); expect(result).toBeFalsy(); }); diff --git a/projects/ccd-case-ui-toolkit/src/lib/shared/components/palette/query-management/components/query-write/query-write-raise-query/query-write-raise-query.component.ts b/projects/ccd-case-ui-toolkit/src/lib/shared/components/palette/query-management/components/query-write/query-write-raise-query/query-write-raise-query.component.ts index 2d44b655a0..f778afb701 100644 --- a/projects/ccd-case-ui-toolkit/src/lib/shared/components/palette/query-management/components/query-write/query-write-raise-query/query-write-raise-query.component.ts +++ b/projects/ccd-case-ui-toolkit/src/lib/shared/components/palette/query-management/components/query-write/query-write-raise-query/query-write-raise-query.component.ts @@ -11,6 +11,7 @@ import { QmCaseQueriesCollection, QueryCreateContext, QueryListItem } from '../. import { EventCompletionParams } from '../../../../../case-editor/domain/event-completion-params.model'; import { QueryManagementService } from '../../../services'; import { ActivatedRoute } from '@angular/router'; +import { StructuredLoggerService } from '../../../../../../services'; @Component({ selector: 'ccd-query-write-raise-query', @@ -18,6 +19,8 @@ import { ActivatedRoute } from '@angular/router'; standalone: false }) export class QueryWriteRaiseQueryComponent implements OnChanges { + private readonly logger = new StructuredLoggerService(); + @Input() public formGroup: FormGroup; @Input() public submitted: boolean; @Input() public caseDetails: CaseView; @@ -72,7 +75,7 @@ export class QueryWriteRaiseQueryComponent implements OnChanges { public setCaseQueriesCollectionData(): boolean { if (!this.eventData) { - console.warn('Event data not available; skipping collection setup.'); + this.logger.warn('Event data not available; skipping collection setup.'); return false; } diff --git a/projects/ccd-case-ui-toolkit/src/lib/shared/components/palette/query-management/components/query-write/query-write-respond-to-query/query-write-respond-to-query.component.spec.ts b/projects/ccd-case-ui-toolkit/src/lib/shared/components/palette/query-management/components/query-write/query-write-respond-to-query/query-write-respond-to-query.component.spec.ts index b82cb60dcc..723b92da3c 100644 --- a/projects/ccd-case-ui-toolkit/src/lib/shared/components/palette/query-management/components/query-write/query-write-respond-to-query/query-write-respond-to-query.component.spec.ts +++ b/projects/ccd-case-ui-toolkit/src/lib/shared/components/palette/query-management/components/query-write/query-write-respond-to-query/query-write-respond-to-query.component.spec.ts @@ -139,16 +139,19 @@ describe('QueryWriteRespondToQueryComponent', () => { expect(component.caseDetails).toEqual(CASE_VIEW); })); - it('should log error if caseNotifier emits an error', fakeAsync(() => { + it('should log a redacted structured error if caseNotifier emits an error', fakeAsync(() => { const errorMessage = 'Error retrieving case details'; - spyOn(console, 'error'); + const consoleSpy = spyOn(console, 'error'); mockCaseNotifier.fetchAndRefresh.and.returnValue(throwError(() => errorMessage)); component.ngOnInit(); tick(); - expect(console.error).toHaveBeenCalledWith('Error retrieving case details:', errorMessage); + expect(consoleSpy.calls.mostRecent().args[0]).toEqual(jasmine.objectContaining({ + level: 'error', + message: 'Error retrieving case details.' + })); })); it('should emit value when hasResponded is called', () => { @@ -189,11 +192,14 @@ describe('QueryWriteRespondToQueryComponent', () => { expect(consoleSpy).not.toHaveBeenCalled(); }); - it('should log error if caseQueriesCollections[0] is undefined', () => { + it('should log a redacted structured error if caseQueriesCollections[0] is undefined', () => { component.caseQueriesCollections = [undefined as any]; const consoleSpy = spyOn(console, 'error'); component.ngOnChanges(); - expect(consoleSpy).toHaveBeenCalledWith('caseQueriesCollections[0] is undefined!', component.caseQueriesCollections); + expect(consoleSpy.calls.mostRecent().args[0]).toEqual(jasmine.objectContaining({ + level: 'error', + message: 'Case queries collection is undefined.' + })); }); it('should warn and return if no messageId is found', () => { @@ -203,7 +209,13 @@ describe('QueryWriteRespondToQueryComponent', () => { const warnSpy = spyOn(console, 'warn'); component.ngOnChanges(); - expect(warnSpy).toHaveBeenCalledWith('No messageId found in route params:', activatedRoute.snapshot.params); + expect(warnSpy.calls.mostRecent().args[0]).toEqual(jasmine.objectContaining({ + level: 'warn', + message: 'No messageId found in route params.', + context: jasmine.objectContaining({ + routeParams: activatedRoute.snapshot.params + }) + })); }); it('should filter parent query when responding to a child message', () => { @@ -248,10 +260,13 @@ describe('QueryWriteRespondToQueryComponent', () => { component.ngOnChanges(); - expect(warnSpy).toHaveBeenCalledWith( - 'No matching message found for ID:', - unmatchedMessageId - ); + expect(warnSpy.calls.mostRecent().args[0]).toEqual(jasmine.objectContaining({ + level: 'warn', + message: 'No matching message found for ID.', + context: jasmine.objectContaining({ + messageId: unmatchedMessageId + }) + })); expect(component.queryListData).toBeUndefined(); expect(component.queryItem).toBeUndefined(); diff --git a/projects/ccd-case-ui-toolkit/src/lib/shared/components/palette/query-management/components/query-write/query-write-respond-to-query/query-write-respond-to-query.component.ts b/projects/ccd-case-ui-toolkit/src/lib/shared/components/palette/query-management/components/query-write/query-write-respond-to-query/query-write-respond-to-query.component.ts index 044fb41485..c6d5bc0997 100644 --- a/projects/ccd-case-ui-toolkit/src/lib/shared/components/palette/query-management/components/query-write/query-write-respond-to-query/query-write-respond-to-query.component.ts +++ b/projects/ccd-case-ui-toolkit/src/lib/shared/components/palette/query-management/components/query-write/query-write-respond-to-query/query-write-respond-to-query.component.ts @@ -9,6 +9,7 @@ import { CaseNotifier } from '../../../../../case-editor/services'; import { RaiseQueryErrorMessage } from '../../../enums'; import { CaseQueriesCollection, QmCaseQueriesCollection, QueryCreateContext, QueryListData, QueryListItem } from '../../../models'; import { QueryManagementService } from '../../../services'; +import { StructuredLoggerService } from '../../../../../../services'; @Component({ selector: 'ccd-query-write-respond-to-query', templateUrl: './query-write-respond-to-query.component.html', @@ -17,6 +18,8 @@ import { QueryManagementService } from '../../../services'; }) export class QueryWriteRespondToQueryComponent implements OnInit, OnChanges { + private readonly logger = new StructuredLoggerService(); + @Input() public queryItem: QueryListItem; @Input() public formGroup: FormGroup; @Input() public queryCreateContext: QueryCreateContext; @@ -55,7 +58,7 @@ export class QueryWriteRespondToQueryComponent implements OnInit, OnChanges { this.caseDetails = caseDetails; }, error: (err) => { - console.error('Error retrieving case details:', err); + this.logger.error('Error retrieving case details.', { error: err }); } }); } @@ -67,13 +70,13 @@ export class QueryWriteRespondToQueryComponent implements OnInit, OnChanges { } if (!this.caseQueriesCollections[0]) { - console.error('caseQueriesCollections[0] is undefined!', this.caseQueriesCollections); + this.logger.error('Case queries collection is undefined.'); return; } this.messageId = this.route.snapshot.params?.dataid; if (!this.messageId) { - console.warn('No messageId found in route params:', this.route.snapshot.params); + this.logger.warn('No messageId found in route params.', { routeParams: this.route.snapshot.params }); return; } @@ -85,7 +88,7 @@ export class QueryWriteRespondToQueryComponent implements OnInit, OnChanges { )?.value; if (!matchingMessage) { - console.warn('No matching message found for ID:', this.messageId); + this.logger.warn('No matching message found for ID.', { messageId: this.messageId }); return; } @@ -116,7 +119,7 @@ export class QueryWriteRespondToQueryComponent implements OnInit, OnChanges { public setCaseQueriesCollectionData(): boolean { if (!this.eventData) { - console.warn('Event data not available; skipping collection setup.'); + this.logger.warn('Event data not available; skipping collection setup.'); return false; } diff --git a/projects/ccd-case-ui-toolkit/src/lib/shared/components/palette/query-management/services/query-management.service.spec.ts b/projects/ccd-case-ui-toolkit/src/lib/shared/components/palette/query-management/services/query-management.service.spec.ts index 953203c4ca..ebd1a8f4c8 100644 --- a/projects/ccd-case-ui-toolkit/src/lib/shared/components/palette/query-management/services/query-management.service.spec.ts +++ b/projects/ccd-case-ui-toolkit/src/lib/shared/components/palette/query-management/services/query-management.service.spec.ts @@ -144,9 +144,13 @@ describe('QueryManagementService', () => { service.setCaseQueriesCollectionData(eventData as any, QueryCreateContext.NEW_QUERY, caseDetails as any); // resolveFieldId logs the jurisdiction-specific error, and setCaseQueriesCollectionData logs a second message. - expect((console.error as jasmine.Spy).calls.argsFor(0)[0]).toBe( - 'Error: Multiple CaseQueriesCollections are not supported yet for the PUBLICLAW jurisdiction' - ); + expect((console.error as jasmine.Spy).calls.argsFor(0)[0]).toEqual(jasmine.objectContaining({ + level: 'error', + message: 'Multiple CaseQueriesCollections are not supported yet for this jurisdiction.', + context: jasmine.objectContaining({ + jurisdictionId: 'PUBLICLAW' + }) + })); expect(service.fieldId).toBeUndefined(); }); }); diff --git a/projects/ccd-case-ui-toolkit/src/lib/shared/components/palette/query-management/services/query-management.service.ts b/projects/ccd-case-ui-toolkit/src/lib/shared/components/palette/query-management/services/query-management.service.ts index 995585b4a4..49607ba8fe 100644 --- a/projects/ccd-case-ui-toolkit/src/lib/shared/components/palette/query-management/services/query-management.service.ts +++ b/projects/ccd-case-ui-toolkit/src/lib/shared/components/palette/query-management/services/query-management.service.ts @@ -2,7 +2,7 @@ import { Injectable } from '@angular/core'; import { Router } from '@angular/router'; import { CaseField, CaseEventTrigger, CaseView } from '../../../../../../lib/shared/domain'; import { QmCaseQueriesCollection, QueryCreateContext, QueryListItem, CaseQueriesCollection } from '../models'; -import { SessionStorageService } from '../../../../services'; +import { SessionStorageService, StructuredLoggerService } from '../../../../services'; import { isInternalUser, isJudiciaryUser, USER_DETAILS } from '../../../../utils'; import { safeJsonParse } from '../../../../json-utils'; import { QueryManagementUtils } from '../utils/query-management.utils'; @@ -19,6 +19,8 @@ import { FormGroup } from '@angular/forms'; @Injectable({ providedIn: 'root' }) export class QueryManagementService { + private readonly logger = new StructuredLoggerService(); + public caseQueriesCollections: CaseQueriesCollection[]; public fieldId: string; @@ -46,7 +48,7 @@ export class QueryManagementService { try { currentUserDetails = safeJsonParse(this.sessionStorageService.getItem(USER_DETAILS), {}); } catch (e) { - console.error('Could not parse USER_DETAILS from session storage:', e); + this.logger.error('Could not parse USER_DETAILS from session storage.', { error: e }); currentUserDetails = {}; } @@ -59,7 +61,7 @@ export class QueryManagementService { // Check if the field ID has been set dynamically if (!this.fieldId) { - console.error('Error: Field ID for CaseQueriesCollection not found. Cannot proceed with data generation.'); + this.logger.error('Field ID for CaseQueriesCollection not found. Cannot proceed with data generation.'); this.router.navigate(['/', 'service-down']); throw new Error('Field ID for CaseQueriesCollection not found. Aborting query data generation.'); } @@ -135,7 +137,7 @@ export class QueryManagementService { const resolvedFieldId = this.resolveFieldId(eventData, queryCreateContext, caseDetails, messageId); if (!resolvedFieldId) { - console.error('Failed to resolve fieldId for CaseQueriesCollection. Cannot proceed.'); + this.logger.error('Failed to resolve fieldId for CaseQueriesCollection. Cannot proceed.'); return; } @@ -167,7 +169,7 @@ export class QueryManagementService { ); if (!candidateFields?.length) { - console.warn('No editable CaseQueriesCollection fields found.'); + this.logger.warn('No editable CaseQueriesCollection fields found.'); return null; } @@ -199,13 +201,13 @@ export class QueryManagementService { return firstOrdered.id; } } else { - console.error(`Error: Multiple CaseQueriesCollections are not supported yet for the ${jurisdictionId} jurisdiction`); + this.logger.error('Multiple CaseQueriesCollections are not supported yet for this jurisdiction.', { jurisdictionId }); return null; } } // Step 4: Fallback — if none of the above succeeded - console.warn('Could not determine fieldId for context:', queryCreateContext); + this.logger.warn('Could not determine fieldId for context.', { queryCreateContext }); return null; } diff --git a/projects/ccd-case-ui-toolkit/src/lib/shared/components/search-filters/search-filters.component.ts b/projects/ccd-case-ui-toolkit/src/lib/shared/components/search-filters/search-filters.component.ts index 9c3b3dc1b1..b4fa55e5ac 100644 --- a/projects/ccd-case-ui-toolkit/src/lib/shared/components/search-filters/search-filters.component.ts +++ b/projects/ccd-case-ui-toolkit/src/lib/shared/components/search-filters/search-filters.component.ts @@ -9,6 +9,7 @@ import { CaseTypeLite } from '../../domain/definition/case-type-lite.model'; import { Jurisdiction } from '../../domain/definition/jurisdiction.model'; import { FieldsUtils } from '../../services/fields/fields.utils'; import { JurisdictionService } from '../../services/jurisdiction/jurisdiction.service'; +import { StructuredLoggerService } from '../../services/logging'; import { OrderService } from '../../services/order/order.service'; import { SearchService } from '../../services/search/search.service'; import { WindowService } from '../../services/window/window.service'; @@ -25,6 +26,8 @@ const CASE_TYPE_LOC_STORAGE = 'search-caseType'; }) export class SearchFiltersComponent implements OnInit { + private readonly logger = new StructuredLoggerService(); + public readonly PARAM_JURISDICTION = 'jurisdiction'; public readonly PARAM_CASE_TYPE = 'case-type'; public readonly PARAM_CASE_STATE = 'case-state'; @@ -145,7 +148,7 @@ export class SearchFiltersComponent implements OnInit { return localStorageJurisdiction; } } catch (e) { - console.log("Failed to retrieve jurisdiction from local storage"); + this.logger.error('Failed to retrieve jurisdiction from local storage.', { error: e }); this.windowService.setLocalStorage(JURISDICTION_LOC_STORAGE, null) } } @@ -210,9 +213,7 @@ export class SearchFiltersComponent implements OnInit { } }); this.getCaseFields(); - }, error => { - console.log('Search input fields request will be discarded reason: ', error.message); - }); + }, error => this.logger.error('Search input fields request will be discarded.', { error })); } public isJurisdictionSelected(): boolean { diff --git a/projects/ccd-case-ui-toolkit/src/lib/shared/components/workbasket-filters/workbasket-filters.component.ts b/projects/ccd-case-ui-toolkit/src/lib/shared/components/workbasket-filters/workbasket-filters.component.ts index 4e5abc71a4..be89f91737 100644 --- a/projects/ccd-case-ui-toolkit/src/lib/shared/components/workbasket-filters/workbasket-filters.component.ts +++ b/projects/ccd-case-ui-toolkit/src/lib/shared/components/workbasket-filters/workbasket-filters.component.ts @@ -10,6 +10,7 @@ import { WorkbasketInputModel } from '../../domain/workbasket/workbasket-input.m import { AlertService } from '../../services/alert/alert.service'; import { FieldsUtils } from '../../services/fields/fields.utils'; import { JurisdictionService } from '../../services/jurisdiction/jurisdiction.service'; +import { StructuredLoggerService } from '../../services/logging'; import { OrderService } from '../../services/order/order.service'; import { WindowService } from '../../services/window/window.service'; import { WorkbasketInputFilterService } from '../../services/workbasket/workbasket-input-filter.service'; @@ -28,6 +29,8 @@ export class WorkbasketFiltersComponent implements OnInit { public static readonly PARAM_JURISDICTION = 'jurisdiction'; public static readonly PARAM_CASE_TYPE = 'case-type'; public static readonly PARAM_CASE_STATE = 'case-state'; + private readonly logger = new StructuredLoggerService(); + public caseFields: CaseField[]; @Input() @@ -237,9 +240,7 @@ export class WorkbasketFiltersComponent implements OnInit { } }); this.getCaseFields(); - }, error => { - console.log('Workbasket input fields request will be discarded reason: ', error.message); - }); + }, error => this.logger.error('Workbasket input fields request will be discarded.', { error })); } } else { this.resetCaseState(); diff --git a/projects/ccd-case-ui-toolkit/src/lib/shared/directives/conditional-show/services/condition-parser.service.ts b/projects/ccd-case-ui-toolkit/src/lib/shared/directives/conditional-show/services/condition-parser.service.ts index 81d13cf95c..af9bd4676d 100644 --- a/projects/ccd-case-ui-toolkit/src/lib/shared/directives/conditional-show/services/condition-parser.service.ts +++ b/projects/ccd-case-ui-toolkit/src/lib/shared/directives/conditional-show/services/condition-parser.service.ts @@ -1,9 +1,12 @@ import * as _score from 'underscore'; +import { StructuredLoggerService } from '../../../services/logging'; import { FieldsUtils } from '../../../services/fields/fields.utils'; import { ShowCondition } from '../../conditional-show/domain/conditional-show.model'; import peg from './condition.peg'; export class ConditionParser { + private static readonly logger = new StructuredLoggerService(); + /** * Parse the raw formula and output structured condition data * that can be used in evaluating show/hide logic @@ -186,13 +189,13 @@ export class ConditionParser { return (fields[head][arrayIndex] !== undefined) ? this.findValueForComplexCondition( fields[head][arrayIndex]['value'], tail[0], tail.slice(1), dropNumberPath.join('_')) : null; } catch (e) { - console.error('Error while parsing number', pathTail[0], e); + this.logger.error('Error while parsing form array path index.', { error: e, pathIndex: pathTail[0] }); } } } else { // EXUI-2460 - if path present then show error, otherwise log message to stop unnecessary console errors - path ? console.error('Path in formArray should start with ', head, ', full path: ', path) : - console.log('Path not present in formArray'); + path ? this.logger.error('Path in formArray should start with the expected field.', { expectedHead: head, path }) : + this.logger.info('Path not present in formArray.'); } } diff --git a/projects/ccd-case-ui-toolkit/src/lib/shared/domain/definition/case-field.model.ts b/projects/ccd-case-ui-toolkit/src/lib/shared/domain/definition/case-field.model.ts index a90e8a89f3..eb489564c8 100644 --- a/projects/ccd-case-ui-toolkit/src/lib/shared/domain/definition/case-field.model.ts +++ b/projects/ccd-case-ui-toolkit/src/lib/shared/domain/definition/case-field.model.ts @@ -2,6 +2,7 @@ import { Exclude, Expose, Type } from 'class-transformer'; import * as _ from 'underscore'; import { WizardPageField } from '../../components/case-editor/domain/wizard-page-field.model'; +import { StructuredLoggerService } from '../../services/logging'; import { Orderable } from '../order'; import { AccessControlList } from './access-control-list.model'; import { FieldTypeEnum } from './field-type-enum.model'; @@ -9,6 +10,8 @@ import { FieldType } from './field-type.model'; // @dynamic export class CaseField implements Orderable { + private static readonly logger = new StructuredLoggerService(); + public id: string; public hidden: boolean; public hiddenCannotChange: boolean; @@ -184,7 +187,7 @@ export class CaseField implements Orderable { return prefix + this.id; } } else { - console.log("Path too long, possible circular reference in case field hierarchy"); + CaseField.logger.error('Path too long, possible circular reference in case field hierarchy.'); return this.id; } } diff --git a/projects/ccd-case-ui-toolkit/src/lib/shared/json-utils.ts b/projects/ccd-case-ui-toolkit/src/lib/shared/json-utils.ts index ad5bb5d30e..f729460c44 100644 --- a/projects/ccd-case-ui-toolkit/src/lib/shared/json-utils.ts +++ b/projects/ccd-case-ui-toolkit/src/lib/shared/json-utils.ts @@ -1,3 +1,7 @@ +import { StructuredLoggerService } from './services/logging'; + +const logger = new StructuredLoggerService(); + export function safeJsonParse(value: string | null, fallback: T | null = null): T | null { if (!value) { return fallback; @@ -7,8 +11,7 @@ export function safeJsonParse(value: string | null, fallback: T | null = null return JSON.parse(value) as T; } catch (error) { // Log for diagnostics, then return fallback to avoid UI crashes. - // eslint-disable-next-line no-console - console.error('safeJsonParse failed to parse JSON', error); + logger.error('safeJsonParse failed to parse JSON.', { error }); return fallback; } } diff --git a/projects/ccd-case-ui-toolkit/src/lib/shared/services/activity/activity.polling.service.ts b/projects/ccd-case-ui-toolkit/src/lib/shared/services/activity/activity.polling.service.ts index 69a48bd96e..b4f1b99940 100644 --- a/projects/ccd-case-ui-toolkit/src/lib/shared/services/activity/activity.polling.service.ts +++ b/projects/ccd-case-ui-toolkit/src/lib/shared/services/activity/activity.polling.service.ts @@ -2,12 +2,15 @@ import { Injectable, NgZone } from '@angular/core'; import { EMPTY, Observable, Subject, Subscription } from 'rxjs'; import { AbstractAppConfig } from '../../../app.config'; import { Activity } from '../../domain/activity/activity.model'; +import { StructuredLoggerService } from '../logging'; import { ActivityService } from './activity.service'; import { polling, IOptions } from 'rx-polling-hmcts'; // @dynamic @Injectable() export class ActivityPollingService { + private readonly logger = new StructuredLoggerService(); + private readonly pendingRequests = new Map>(); private currentTimeoutHandle: any; private pollActivitiesSubscription: Subscription; @@ -46,7 +49,6 @@ export class ActivityPollingService { this.ngZone.runOutsideAngular(() => { this.currentTimeoutHandle = setTimeout( () => this.ngZone.run(() => { - // console.log('timeout: flushing requests') this.flushRequests(); }), this.batchCollectionDelayMs); @@ -54,7 +56,6 @@ export class ActivityPollingService { } if (this.pendingRequests.size >= this.maxRequestsPerBatch) { - // console.log('max pending hit: flushing requests'); this.flushRequests(); } return subject; @@ -95,19 +96,17 @@ export class ActivityPollingService { protected performBatchRequest(requests: Map>): void { const caseIds = Array.from(requests.keys()).join(); - // console.log('issuing batch request for cases: ' + caseIds); this.ngZone.runOutsideAngular( () => { // run polling outside angular zone so it does not trigger change detection this.pollActivitiesSubscription = this.pollActivities(caseIds).subscribe( // process activity inside zone so it triggers change detection for activity.component.ts (activities: Activity[]) => this.ngZone.run( () => { activities.forEach((activity) => { - // console.log('pushing activity: ' + activity.caseId); requests.get(activity.caseId).next(activity); }); }, (err) => { - console.log(`error: ${err}`); + this.logger.error('Error while polling activities.', { error: err }); Array.from(requests.values()).forEach((subject) => subject.error(err)); } ) diff --git a/projects/ccd-case-ui-toolkit/src/lib/shared/services/activity/activity.service.ts b/projects/ccd-case-ui-toolkit/src/lib/shared/services/activity/activity.service.ts index 702d61e8e8..63f1d7f060 100644 --- a/projects/ccd-case-ui-toolkit/src/lib/shared/services/activity/activity.service.ts +++ b/projects/ccd-case-ui-toolkit/src/lib/shared/services/activity/activity.service.ts @@ -7,6 +7,7 @@ import { AbstractAppConfig } from '../../../app.config'; import { Activity } from '../../domain/activity/activity.model'; import { HttpError } from '../../domain/http/http-error.model'; import { HttpErrorService, HttpService, OptionsType } from '../http'; +import { StructuredLoggerService } from '../logging'; import { SessionStorageService } from '../session'; import { USER_DETAILS } from '../../utils'; import { safeJsonParse } from '../../json-utils'; @@ -17,6 +18,8 @@ export class ActivityService { public static get ACTIVITY_VIEW() { return 'view'; } public static get ACTIVITY_EDIT() { return 'edit'; } + private readonly logger = new StructuredLoggerService(); + constructor( private readonly http: HttpService, private readonly appConfig: AbstractAppConfig, @@ -61,7 +64,7 @@ export class ActivityService { map(response => response) ); } catch (error) { - console.log(`user may not be authenticated.${error}`); + this.logUserMayNotBeAuthenticated(error); } } @@ -76,7 +79,7 @@ export class ActivityService { map(response => response) ); } catch (error) { - console.log(`user may not be authenticated.${error}`); + this.logUserMayNotBeAuthenticated(error); } } @@ -94,4 +97,8 @@ export class ActivityService { private activityUrl(): string { return this.appConfig.getActivityUrl(); } + + private logUserMayNotBeAuthenticated(error: unknown): void { + this.logger.error('User may not be authenticated. Activity request was not sent.', { error }); + } } diff --git a/projects/ccd-case-ui-toolkit/src/lib/shared/services/fields/fields.utils.ts b/projects/ccd-case-ui-toolkit/src/lib/shared/services/fields/fields.utils.ts index 2ab42b88a3..471e7e4d83 100644 --- a/projects/ccd-case-ui-toolkit/src/lib/shared/services/fields/fields.utils.ts +++ b/projects/ccd-case-ui-toolkit/src/lib/shared/services/fields/fields.utils.ts @@ -11,10 +11,13 @@ import { CaseEventTrigger, CaseField, CaseTab, CaseView, FieldType, FieldTypeEnu import { UserTask } from '../../domain/work-allocation/Task'; import { FormatTranslatorService } from '../case-fields/format-translator.service'; import { safeJsonParse } from '../../json-utils'; +import { StructuredLoggerService } from '../logging'; // @dynamic @Injectable() export class FieldsUtils { + private static readonly logger = new StructuredLoggerService(); + private static readonly caseLevelCaseFlagsFieldId = 'caseFlags'; private static readonly currencyPipe: CurrencyPipe = new CurrencyPipe('en-GB'); private static readonly datePipe: DatePipe = new DatePipe(new FormatTranslatorService()); @@ -333,7 +336,7 @@ export class FieldsUtils { this.setDynamicListDefinition(field, field.field_type, rootCaseField); } } catch (error) { - console.log(error); + FieldsUtils.logger.error('Error setting dynamic list definition.', { error }); } }); } else if (caseFieldType.type === FieldsUtils.SERVER_RESPONSE_FIELD_TYPE_COLLECTION) { diff --git a/projects/ccd-case-ui-toolkit/src/lib/shared/services/http/http-error.service.ts b/projects/ccd-case-ui-toolkit/src/lib/shared/services/http/http-error.service.ts index 1654013566..4ef6298e00 100644 --- a/projects/ccd-case-ui-toolkit/src/lib/shared/services/http/http-error.service.ts +++ b/projects/ccd-case-ui-toolkit/src/lib/shared/services/http/http-error.service.ts @@ -4,10 +4,12 @@ import { Observable, throwError } from 'rxjs'; import { HttpError } from '../../domain/http/http-error.model'; import { AuthService } from '../auth/auth.service'; import { LoadingService } from '../loading'; +import { StructuredLoggerService } from '../logging'; @Injectable() export class HttpErrorService { + private static readonly logger = new StructuredLoggerService(); constructor(private readonly authService: AuthService, private readonly loadingService: LoadingService @@ -27,7 +29,7 @@ export class HttpErrorService { try { httpError = HttpError.from(error); } catch (e) { - console.error(e, e.message); + HttpErrorService.logger.error('Unable to convert HTTP error response.', { error: e }); } } if (!httpError.status) { @@ -55,8 +57,7 @@ export class HttpErrorService { } public handle(error: HttpErrorResponse | any, redirectIfNotAuthorised = true): Observable { - console.error('Handling error in http error service.'); - console.error(error); + HttpErrorService.logger.error('Handling error in http error service.', { error }); if (this.loadingService.hasSharedSpinner()){ this.loadingService.unregisterSharedSpinner(); } diff --git a/projects/ccd-case-ui-toolkit/src/lib/shared/services/index.ts b/projects/ccd-case-ui-toolkit/src/lib/shared/services/index.ts index 738f1ca0c2..523adbd544 100644 --- a/projects/ccd-case-ui-toolkit/src/lib/shared/services/index.ts +++ b/projects/ccd-case-ui-toolkit/src/lib/shared/services/index.ts @@ -16,6 +16,7 @@ export * from './http'; export * from './journey'; export * from './jurisdiction'; export * from './banners'; +export * from './logging'; export * from './navigation'; export * from './order'; export * from './profile'; diff --git a/projects/ccd-case-ui-toolkit/src/lib/shared/services/logging/index.ts b/projects/ccd-case-ui-toolkit/src/lib/shared/services/logging/index.ts new file mode 100644 index 0000000000..e78efd5b54 --- /dev/null +++ b/projects/ccd-case-ui-toolkit/src/lib/shared/services/logging/index.ts @@ -0,0 +1 @@ +export * from './structured-logger.service'; diff --git a/projects/ccd-case-ui-toolkit/src/lib/shared/services/logging/structured-logger.service.spec.ts b/projects/ccd-case-ui-toolkit/src/lib/shared/services/logging/structured-logger.service.spec.ts new file mode 100644 index 0000000000..db43ca052f --- /dev/null +++ b/projects/ccd-case-ui-toolkit/src/lib/shared/services/logging/structured-logger.service.spec.ts @@ -0,0 +1,75 @@ +import { StructuredLogEntry, StructuredLoggerService } from './structured-logger.service'; + +describe('StructuredLoggerService', () => { + let logger: StructuredLoggerService; + + beforeEach(() => { + logger = new StructuredLoggerService(); + }); + + it('should write structured warning logs with redacted sensitive fields', () => { + const warnSpy = spyOn(console, 'warn'); + + logger.warn('Request failed', { + safeField: 'visible', + authContext: { userId: 'sensitive-user' }, + authorisation: 'sensitive-authorisation', + token: 'secret-token', + nested: { + password: 'secret-password', + clientSecret: 'secret-client' + }, + headers: [ + { name: 'Authorization', value: 'Bearer header-token' }, + { key: 'refreshToken', values: ['refresh-token'] } + ] + }); + + const entry = warnSpy.calls.mostRecent().args[0] as StructuredLogEntry; + const context = entry.context as any; + + expect(entry.level).toBe('warn'); + expect(entry.message).toBe('Request failed'); + expect(entry.timestamp).toEqual(jasmine.any(String)); + expect(context.safeField).toBe('visible'); + expect(context.authContext).toBe('[REDACTED]'); + expect(context.authorisation).toBe('[REDACTED]'); + expect(context.token).toBe('[REDACTED]'); + expect(context.nested.password).toBe('[REDACTED]'); + expect(context.nested.clientSecret).toBe('[REDACTED]'); + expect(context.headers[0].value).toBe('[REDACTED]'); + expect(context.headers[1].values).toBe('[REDACTED]'); + expect(JSON.stringify(entry)).not.toContain('secret-token'); + expect(JSON.stringify(entry)).not.toContain('sensitive-user'); + expect(JSON.stringify(entry)).not.toContain('sensitive-authorisation'); + expect(JSON.stringify(entry)).not.toContain('secret-password'); + expect(JSON.stringify(entry)).not.toContain('secret-client'); + expect(JSON.stringify(entry)).not.toContain('header-token'); + expect(JSON.stringify(entry)).not.toContain('refresh-token'); + }); + + it('should redact sensitive values inside error messages', () => { + const errorSpy = spyOn(console, 'error'); + const error = new Error('Authorization: Bearer secret-token'); + (error as any).token = 'secret-token'; + + logger.error('Activity request failed', { error }); + + const entry = errorSpy.calls.mostRecent().args[0] as StructuredLogEntry; + + expect(JSON.stringify(entry)).toContain('Bearer [REDACTED]'); + expect(JSON.stringify(entry)).not.toContain('secret-token'); + }); + + it('should handle circular context safely', () => { + const warnSpy = spyOn(console, 'warn'); + const context: any = { safeField: 'visible' }; + context.self = context; + + logger.warn('Circular context', context); + + const entry = warnSpy.calls.mostRecent().args[0] as StructuredLogEntry; + + expect((entry.context as any).self).toBe('[Circular]'); + }); +}); diff --git a/projects/ccd-case-ui-toolkit/src/lib/shared/services/logging/structured-logger.service.ts b/projects/ccd-case-ui-toolkit/src/lib/shared/services/logging/structured-logger.service.ts new file mode 100644 index 0000000000..c5c72d4ca4 --- /dev/null +++ b/projects/ccd-case-ui-toolkit/src/lib/shared/services/logging/structured-logger.service.ts @@ -0,0 +1,165 @@ +import { Injectable } from '@angular/core'; + +export type StructuredLogLevel = 'error' | 'warn' | 'info' | 'debug'; + +export interface StructuredLogEntry { + level: StructuredLogLevel; + message: string; + timestamp: string; + context?: unknown; +} + +@Injectable({ + providedIn: 'root' +}) +export class StructuredLoggerService { + private static readonly CIRCULAR_VALUE = '[Circular]'; + private static readonly MAX_DEPTH_VALUE = '[MaxDepth]'; + private static readonly MAX_REDACTION_DEPTH = 10; + private static readonly REDACTED_VALUE = '[REDACTED]'; + private static readonly SENSITIVE_KEY_PATTERN = /(password|passcode|pwd|secret|token|authori[sz]ation|authentication|auth[-_]?context|^auth$|api[-_]?key|cookie|session|credential)/i; + private static readonly SENSITIVE_STRING_PATTERN = /((?:password|passcode|pwd|secret|token|authori[sz]ation|auth(?:entication|[-_ ]?context)?|api[-_]?key|cookie|session|credential)\s*[:=]\s*)((?:Bearer\s+)?)([^,;&\s]+)/gi; + private static readonly BEARER_TOKEN_PATTERN = /\bBearer\s+([A-Za-z0-9\-._~+/]+=*)/gi; + + public debug(message: string, context?: unknown): void { + this.write('debug', message, context); + } + + public error(message: string, context?: unknown): void { + this.write('error', message, context); + } + + public info(message: string, context?: unknown): void { + this.write('info', message, context); + } + + public warn(message: string, context?: unknown): void { + this.write('warn', message, context); + } + + public redact(value: unknown): unknown { + return this.redactValue(value, new WeakSet(), false, 0); + } + + private write(level: StructuredLogLevel, message: string, context?: unknown): void { + const entry: StructuredLogEntry = { + level, + message, + timestamp: new Date().toISOString() + }; + + if (context !== undefined) { + entry.context = this.redact(context); + } + + switch (level) { + case 'error': + console.error(entry); + break; + case 'warn': + console.warn(entry); + break; + default: + console.log(entry); + } + } + + private redactValue(value: unknown, seen: WeakSet, redactCurrentValue: boolean, depth: number): unknown { + if (redactCurrentValue) { + return StructuredLoggerService.REDACTED_VALUE; + } + + if (value === null || value === undefined) { + return value; + } + + if (typeof value === 'string') { + return this.redactSensitiveString(value); + } + + if (typeof value !== 'object') { + return value; + } + + if (seen.has(value)) { + return StructuredLoggerService.CIRCULAR_VALUE; + } + + if (depth >= StructuredLoggerService.MAX_REDACTION_DEPTH) { + return StructuredLoggerService.MAX_DEPTH_VALUE; + } + + seen.add(value); + + let redactedValue: unknown; + if (value instanceof Date) { + redactedValue = value.toISOString(); + } else if (value instanceof Error) { + redactedValue = this.redactError(value, seen, depth); + } else if (Array.isArray(value)) { + redactedValue = value.map(item => this.redactValue(item, seen, false, depth + 1)); + } else { + redactedValue = this.redactObject(value as Record, seen, depth); + } + + seen.delete(value); + return redactedValue; + } + + private redactError(error: Error, seen: WeakSet, depth: number): Record { + const redactedError: Record = { + name: this.redactValue(error.name, seen, false, depth + 1), + message: this.redactValue(error.message, seen, false, depth + 1) + }; + + if (error.stack) { + redactedError.stack = this.redactValue(error.stack, seen, false, depth + 1); + } + + const errorContext = error as unknown as Record; + Object.keys(errorContext).forEach(key => { + redactedError[key] = this.redactValue(errorContext[key], seen, this.isSensitiveKey(key), depth + 1); + }); + + return redactedError; + } + + private redactObject(value: Record, seen: WeakSet, depth: number): Record { + const redactedValue: Record = {}; + const hasSensitiveNamedValue = this.hasSensitiveNamedValue(value); + + Object.keys(value).forEach(key => { + redactedValue[key] = this.redactValue( + value[key], + seen, + this.isSensitiveKey(key) || (hasSensitiveNamedValue && this.isValueKey(key)), + depth + 1 + ); + }); + + return redactedValue; + } + + private redactSensitiveString(value: string): string { + return value + .replace(StructuredLoggerService.SENSITIVE_STRING_PATTERN, '$1$2[REDACTED]') + .replace(StructuredLoggerService.BEARER_TOKEN_PATTERN, 'Bearer [REDACTED]'); + } + + private hasSensitiveNamedValue(value: Record): boolean { + return Object.keys(value) + .some(key => this.isNameKey(key) && typeof value[key] === 'string' && this.isSensitiveKey(value[key] as string)); + } + + private isNameKey(key: string): boolean { + return ['key', 'name'].indexOf(key.toLowerCase()) > -1; + } + + private isSensitiveKey(key: string): boolean { + return StructuredLoggerService.SENSITIVE_KEY_PATTERN.test(key); + } + + private isValueKey(key: string): boolean { + return ['value', 'values'].indexOf(key.toLowerCase()) > -1; + } +} diff --git a/projects/ccd-case-ui-toolkit/src/lib/shared/services/organisation/organisation.service.ts b/projects/ccd-case-ui-toolkit/src/lib/shared/services/organisation/organisation.service.ts index eb52ae926b..0a40ca705f 100644 --- a/projects/ccd-case-ui-toolkit/src/lib/shared/services/organisation/organisation.service.ts +++ b/projects/ccd-case-ui-toolkit/src/lib/shared/services/organisation/organisation.service.ts @@ -3,6 +3,7 @@ import { Injectable } from '@angular/core'; import { Observable, of } from 'rxjs'; import { catchError, map, publishReplay, refCount, take } from 'rxjs/operators'; import { AbstractAppConfig } from '../../../app.config'; +import { StructuredLoggerService } from '../logging'; export interface OrganisationSuperUser { firstName: string; @@ -48,6 +49,7 @@ export interface OrganisationVm { @Injectable() export class OrganisationService { + private readonly logger = new StructuredLoggerService(); constructor(private readonly http: HttpClient, private readonly appconfig: AbstractAppConfig) {} @@ -82,7 +84,7 @@ export class OrganisationService { this.organisations$ = this.http.get(url) .pipe(map((orgs) => OrganisationService.mapOrganisation(orgs)), publishReplay(1, cacheTimeOut), refCount(), take(1), catchError(e => { - console.log(e); + this.logger.error('Error while retrieving active organisations.', { error: e }); // Handle error and return blank Observable array return of([]); })); diff --git a/projects/ccd-case-ui-toolkit/src/lib/shared/services/session/session-storage.guard.ts b/projects/ccd-case-ui-toolkit/src/lib/shared/services/session/session-storage.guard.ts index ed7bea3358..ce4b45e3c2 100644 --- a/projects/ccd-case-ui-toolkit/src/lib/shared/services/session/session-storage.guard.ts +++ b/projects/ccd-case-ui-toolkit/src/lib/shared/services/session/session-storage.guard.ts @@ -1,6 +1,7 @@ import { Inject, Injectable, Optional, InjectionToken } from '@angular/core'; import { CanActivate, Router } from '@angular/router'; import { safeJsonParse } from '../../json-utils'; +import { StructuredLoggerService } from '../logging'; import { SessionStorageService } from './session-storage.service'; export type SessionJsonErrorLogger = (error: unknown) => void; @@ -12,6 +13,8 @@ export const SessionJsonErrorLogger = new InjectionToken providedIn: 'root', }) export class SessionStorageGuard implements CanActivate { + private readonly logger = new StructuredLoggerService(); + constructor( private readonly sessionStorageService: SessionStorageService, private readonly router: Router, @@ -34,8 +37,7 @@ export class SessionStorageGuard implements CanActivate { if (this.errorLogger) { this.errorLogger(error); } else { - // eslint-disable-next-line no-console - console.error('Invalid userDetails in session storage', error); + this.logger.error('Invalid userDetails in session storage.', { error }); } this.router.navigate([this.errorRoute || '/session-error']); return false; diff --git a/projects/ccd-case-ui-toolkit/src/lib/shared/services/utils/retry/retry.service.ts b/projects/ccd-case-ui-toolkit/src/lib/shared/services/utils/retry/retry.service.ts index fb18684b04..cb4289d774 100644 --- a/projects/ccd-case-ui-toolkit/src/lib/shared/services/utils/retry/retry.service.ts +++ b/projects/ccd-case-ui-toolkit/src/lib/shared/services/utils/retry/retry.service.ts @@ -1,6 +1,7 @@ import { Injectable } from '@angular/core'; import { Observable, throwError, timer } from 'rxjs'; import { delayWhen, finalize, mergeMap, retryWhen, tap, timeout } from 'rxjs/operators'; +import { StructuredLoggerService } from '../../logging'; class ArtificialDelayContext { private artificialDelayOn = true; @@ -35,14 +36,19 @@ class ArtificialDelayContext { @Injectable() export class RetryUtil { + private readonly logger = new StructuredLoggerService(); + public pipeTimeoutMechanismOn(in$: Observable, preferredArtificialDelay: number, timeoutPeriods: number[]): Observable { const artificialDelayContext = new ArtificialDelayContext(preferredArtificialDelay); - console.info(`Piping a retry mechanism with timeouts {${timeoutPeriods}}.`); - console.info(`Artificial delay will be applied: ${artificialDelayContext.shouldApplyArtificialDelay()}.`); + this.logger.info('Piping a retry mechanism with timeouts.', { timeoutPeriods }); + this.logger.info('Artificial delay setting resolved.', { artificialDelayApplied: artificialDelayContext.shouldApplyArtificialDelay() }); let out$ = in$; if (artificialDelayContext.shouldApplyArtificialDelay()) { - console.info(`Preferred artificial delay: ${preferredArtificialDelay} seconds. Actual delay selected: ${artificialDelayContext.getActualDelay()}`); + this.logger.info('Preferred artificial delay selected.', { + actualDelaySeconds: artificialDelayContext.getActualDelay(), + preferredDelaySeconds: preferredArtificialDelay + }); out$ = this.pipeArtificialDelayOn(out$, artificialDelayContext); } out$ = this.pipeTimeOutControlOn(out$, timeoutPeriods); @@ -52,7 +58,7 @@ export class RetryUtil { private pipeTimeOutControlOn(in$: Observable, timeoutPeriods: number[]): Observable { const timeOutAfterSeconds = timeoutPeriods[0]; - console.info(`Piping timeout control with ${timeOutAfterSeconds} seconds.`); + this.logger.info('Piping timeout control.', { timeoutSeconds: timeOutAfterSeconds }); const out$ = in$.pipe(timeout(timeOutAfterSeconds * 1000)); return out$; } @@ -61,19 +67,18 @@ export class RetryUtil { const retryStrategy = (errors) => { return errors.pipe( mergeMap((error: Error, i) => { - console.error(`Mapping error ${error?.name}, ${i}`); - console.error(error); + this.logger.error('Mapping retry error.', { error, errorName: error?.name, attempt: i }); if (error?.name === 'TimeoutError' && i === 0) { artificialDelayContext.turnOffArtificialDelays(); - console.info('Will retry, after a timeout error.'); + this.logger.info('Will retry after a timeout error.'); } else { - console.error('Will NOT retry.'); + this.logger.error('Will not retry request after error.', { error, errorName: error?.name, attempt: i }); throw error; } return timer(0); }), - finalize(() => console.log('We are done!'))); + finalize(() => undefined)); }; const out$ = in$.pipe(retryWhen(retryStrategy)); return out$; @@ -81,11 +86,11 @@ export class RetryUtil { private pipeArtificialDelayOn(in$: Observable, artificialDelayContext: ArtificialDelayContext): Observable { let out$ = in$.pipe(tap(() => { - console.log(`Artificially delaying for ${artificialDelayContext.getActualDelay()} seconds..`); + this.logger.info('Artificial delay started.', { delaySeconds: artificialDelayContext.getActualDelay() }); })); out$ = out$.pipe(delayWhen(() => timer(artificialDelayContext.getActualDelay() * 1000))); out$ = out$.pipe(tap(() => { - console.log(`Artificially delayed for ${artificialDelayContext.getActualDelay()} seconds..`); + this.logger.info('Artificial delay completed.', { delaySeconds: artificialDelayContext.getActualDelay() }); })); return out$; } From 36516277d37107bdcddbc4c0b50ddefefb0a9516 Mon Sep 17 00:00:00 2001 From: RiteshHMCTS Date: Mon, 25 May 2026 14:09:33 +0100 Subject: [PATCH 2/9] cve --- yarn-audit-known-issues | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/yarn-audit-known-issues b/yarn-audit-known-issues index 1bc429f7ac..631305a6ac 100644 --- a/yarn-audit-known-issues +++ b/yarn-audit-known-issues @@ -1,5 +1,5 @@ {"value":"@nicky-lenaers/ngx-scroll-to","children":{"ID":"@nicky-lenaers/ngx-scroll-to (deprecation)","Issue":"Package no longer supported. Contact Support at https://www.npmjs.com/support for more info.","Severity":"moderate","Vulnerable Versions":"14.0.0","Tree Versions":["14.0.0"],"Dependents":["@hmcts/ccd-case-ui-toolkit@workspace:."]}} -{"value":"@tootallnate/once","children":{"ID":1113977,"Issue":"@tootallnate/once vulnerable to Incorrect Control Flow Scoping","URL":"https://github.com/advisories/GHSA-vpq2-c234-7xj6","Severity":"low","Vulnerable Versions":"<3.0.1","Tree Versions":["2.0.0"],"Dependents":["http-proxy-agent@npm:5.0.0"]}} +{"value":"@tootallnate/once","children":{"ID":1119438,"Issue":"@tootallnate/once vulnerable to Incorrect Control Flow Scoping","URL":"https://github.com/advisories/GHSA-vpq2-c234-7xj6","Severity":"low","Vulnerable Versions":"<2.0.1","Tree Versions":["2.0.0"],"Dependents":["http-proxy-agent@npm:5.0.0"]}} {"value":"abab","children":{"ID":"abab (deprecation)","Issue":"Use your platform's native atob() and btoa() methods instead","Severity":"moderate","Vulnerable Versions":"2.0.6","Tree Versions":["2.0.6"],"Dependents":["jsdom@virtual:ce56289c4b7a2e9003d709997e253c1c80dcaee4c6fbe440cbe9ba5de5db8af3a7b7ad41bbdec5a5e3d40dc9c3c54bef92dd6885ff84cd436d636d5a1b380a61#npm:20.0.3"]}} {"value":"domexception","children":{"ID":"domexception (deprecation)","Issue":"Use your platform's native DOMException instead","Severity":"moderate","Vulnerable Versions":"4.0.0","Tree Versions":["4.0.0"],"Dependents":["jsdom@virtual:ce56289c4b7a2e9003d709997e253c1c80dcaee4c6fbe440cbe9ba5de5db8af3a7b7ad41bbdec5a5e3d40dc9c3c54bef92dd6885ff84cd436d636d5a1b380a61#npm:20.0.3"]}} {"value":"dompurify","children":{"ID":1115668,"Issue":"DOMPurify contains a Cross-site Scripting vulnerability","URL":"https://github.com/advisories/GHSA-v2wj-7wpq-c8vv","Severity":"moderate","Vulnerable Versions":">=3.1.3 <=3.3.1","Tree Versions":["3.3.1"],"Dependents":["mermaid@npm:11.12.2"]}} @@ -22,5 +22,6 @@ {"value":"picomatch","children":{"ID":1115554,"Issue":"Picomatch has a ReDoS vulnerability via extglob quantifiers","URL":"https://github.com/advisories/GHSA-c2c7-rcm5-vvqj","Severity":"high","Vulnerable Versions":">=4.0.0 <4.0.4","Tree Versions":["4.0.3"],"Dependents":["tinyglobby@npm:0.2.15"]}} {"value":"socket.io-parser","children":{"ID":1115154,"Issue":"socket.io allows an unbounded number of binary attachments","URL":"https://github.com/advisories/GHSA-677m-j7p3-52f9","Severity":"high","Vulnerable Versions":">=4.0.0 <4.2.6","Tree Versions":["4.2.4"],"Dependents":["socket.io-client@npm:4.8.1"]}} {"value":"underscore","children":{"ID":1117689,"Issue":"Underscore has unlimited recursion in _.flatten and _.isEqual, potential for DoS attack","URL":"https://github.com/advisories/GHSA-qpx9-hpmf-5gmw","Severity":"high","Vulnerable Versions":"<=1.13.7","Tree Versions":["1.13.7"],"Dependents":["@hmcts/ccd-case-ui-toolkit@workspace:."]}} -{"value":"uuid","children":{"ID":1117637,"Issue":"uuid: Missing buffer bounds check in v3/v5/v6 when buf is provided","URL":"https://github.com/advisories/GHSA-w5hq-g745-h8pq","Severity":"moderate","Vulnerable Versions":">=11.0.0 <11.1.1","Tree Versions":["11.1.0"],"Dependents":["@hmcts/media-viewer@virtual:6ff8c2a3aef81417d9f60600e3255d97c9c6c863d8733a87ed99d869392767523e0e28c07db1eb2a034bc9265813386132447698258584d621a7fd0e13d93585#npm:4.2.16"]}} +{"value":"uuid","children":{"ID":1119441,"Issue":"uuid: Missing buffer bounds check in v3/v5/v6 when buf is provided","URL":"https://github.com/advisories/GHSA-w5hq-g745-h8pq","Severity":"moderate","Vulnerable Versions":"<11.1.1","Tree Versions":["11.1.0"],"Dependents":["@hmcts/media-viewer@virtual:6ff8c2a3aef81417d9f60600e3255d97c9c6c863d8733a87ed99d869392767523e0e28c07db1eb2a034bc9265813386132447698258584d621a7fd0e13d93585#npm:4.2.16"]}} {"value":"whatwg-encoding","children":{"ID":"whatwg-encoding (deprecation)","Issue":"Use @exodus/bytes instead for a more spec-conformant and faster implementation","Severity":"moderate","Vulnerable Versions":"2.0.0","Tree Versions":["2.0.0"],"Dependents":["jsdom@virtual:ce56289c4b7a2e9003d709997e253c1c80dcaee4c6fbe440cbe9ba5de5db8af3a7b7ad41bbdec5a5e3d40dc9c3c54bef92dd6885ff84cd436d636d5a1b380a61#npm:20.0.3"]}} +{"value":"ws","children":{"ID":1119108,"Issue":"ws: Uninitialized memory disclosure","URL":"https://github.com/advisories/GHSA-58qx-3vcg-4xpx","Severity":"moderate","Vulnerable Versions":">=8.0.0 <8.20.1","Tree Versions":["8.17.1","8.18.3"],"Dependents":["engine.io-client@npm:6.6.3","jsdom@virtual:ce56289c4b7a2e9003d709997e253c1c80dcaee4c6fbe440cbe9ba5de5db8af3a7b7ad41bbdec5a5e3d40dc9c3c54bef92dd6885ff84cd436d636d5a1b380a61#npm:20.0.3"]}} From 3abf9a28df777fcd8c3837ba3969963f16b5e7e4 Mon Sep 17 00:00:00 2001 From: RiteshHMCTS Date: Tue, 26 May 2026 15:57:58 +0100 Subject: [PATCH 3/9] sonar issue fixes --- .../logging/structured-logger.service.spec.ts | 33 +++++++++++++++++++ .../logging/structured-logger.service.ts | 20 +++++++---- 2 files changed, 46 insertions(+), 7 deletions(-) diff --git a/projects/ccd-case-ui-toolkit/src/lib/shared/services/logging/structured-logger.service.spec.ts b/projects/ccd-case-ui-toolkit/src/lib/shared/services/logging/structured-logger.service.spec.ts index db43ca052f..2d17c510ed 100644 --- a/projects/ccd-case-ui-toolkit/src/lib/shared/services/logging/structured-logger.service.spec.ts +++ b/projects/ccd-case-ui-toolkit/src/lib/shared/services/logging/structured-logger.service.spec.ts @@ -61,6 +61,39 @@ describe('StructuredLoggerService', () => { expect(JSON.stringify(entry)).not.toContain('secret-token'); }); + it('should redact sensitive key value pairs inside strings', () => { + const warnSpy = spyOn(console, 'warn'); + + logger.warn('Request failed', { + safeField: 'caseId=123 password=secret-password auth context: secret-auth api key=secret-api-key' + }); + + const entry = warnSpy.calls.mostRecent().args[0] as StructuredLogEntry; + const context = entry.context as any; + + expect(context.safeField).toContain('caseId=123'); + expect(context.safeField).toContain('password=[REDACTED]'); + expect(context.safeField).toContain('auth context: [REDACTED]'); + expect(context.safeField).toContain('api key=[REDACTED]'); + expect(JSON.stringify(entry)).not.toContain('secret-password'); + expect(JSON.stringify(entry)).not.toContain('secret-auth'); + expect(JSON.stringify(entry)).not.toContain('secret-api-key'); + }); + + it('should redact standalone bearer tokens in string values', () => { + const warnSpy = spyOn(console, 'warn'); + + logger.warn('Request failed', { + safeField: 'Bearer abc.DEF_123-456~+/=' + }); + + const entry = warnSpy.calls.mostRecent().args[0] as StructuredLogEntry; + const context = entry.context as any; + + expect(context.safeField).toBe('Bearer [REDACTED]'); + expect(JSON.stringify(entry)).not.toContain('abc.DEF_123-456'); + }); + it('should handle circular context safely', () => { const warnSpy = spyOn(console, 'warn'); const context: any = { safeField: 'visible' }; diff --git a/projects/ccd-case-ui-toolkit/src/lib/shared/services/logging/structured-logger.service.ts b/projects/ccd-case-ui-toolkit/src/lib/shared/services/logging/structured-logger.service.ts index c5c72d4ca4..962ba61cd6 100644 --- a/projects/ccd-case-ui-toolkit/src/lib/shared/services/logging/structured-logger.service.ts +++ b/projects/ccd-case-ui-toolkit/src/lib/shared/services/logging/structured-logger.service.ts @@ -17,9 +17,9 @@ export class StructuredLoggerService { private static readonly MAX_DEPTH_VALUE = '[MaxDepth]'; private static readonly MAX_REDACTION_DEPTH = 10; private static readonly REDACTED_VALUE = '[REDACTED]'; - private static readonly SENSITIVE_KEY_PATTERN = /(password|passcode|pwd|secret|token|authori[sz]ation|authentication|auth[-_]?context|^auth$|api[-_]?key|cookie|session|credential)/i; - private static readonly SENSITIVE_STRING_PATTERN = /((?:password|passcode|pwd|secret|token|authori[sz]ation|auth(?:entication|[-_ ]?context)?|api[-_]?key|cookie|session|credential)\s*[:=]\s*)((?:Bearer\s+)?)([^,;&\s]+)/gi; - private static readonly BEARER_TOKEN_PATTERN = /\bBearer\s+([A-Za-z0-9\-._~+/]+=*)/gi; + private static readonly SENSITIVE_KEY_PATTERN = /(password|passcode|pwd|secret|token|authori[sz]ation|authentication|auth[-_ ]?context|^auth$|api[-_ ]?key|cookie|session|credential)/i; + private static readonly SENSITIVE_STRING_PATTERN = /([\w -]+\s*[:=]\s*)((?:Bearer\s+)?)([^,;&\s]+)/gi; + private static readonly BEARER_TOKEN_PATTERN = /\bBearer\s+([\w.~+/-]+=*)/gi; public debug(message: string, context?: unknown): void { this.write('debug', message, context); @@ -142,17 +142,23 @@ export class StructuredLoggerService { private redactSensitiveString(value: string): string { return value - .replace(StructuredLoggerService.SENSITIVE_STRING_PATTERN, '$1$2[REDACTED]') + .replace(StructuredLoggerService.SENSITIVE_STRING_PATTERN, (match: string, prefix: string, bearerPrefix: string) => { + const key = prefix.replace(/\s*[:=]\s*$/u, '').trim(); + return this.isSensitiveKey(key) ? `${prefix}${bearerPrefix}[REDACTED]` : match; + }) .replace(StructuredLoggerService.BEARER_TOKEN_PATTERN, 'Bearer [REDACTED]'); } private hasSensitiveNamedValue(value: Record): boolean { return Object.keys(value) - .some(key => this.isNameKey(key) && typeof value[key] === 'string' && this.isSensitiveKey(value[key] as string)); + .some(key => { + const namedValue = value[key]; + return this.isNameKey(key) && typeof namedValue === 'string' && this.isSensitiveKey(namedValue); + }); } private isNameKey(key: string): boolean { - return ['key', 'name'].indexOf(key.toLowerCase()) > -1; + return ['key', 'name'].includes(key.toLowerCase()); } private isSensitiveKey(key: string): boolean { @@ -160,6 +166,6 @@ export class StructuredLoggerService { } private isValueKey(key: string): boolean { - return ['value', 'values'].indexOf(key.toLowerCase()) > -1; + return ['value', 'values'].includes(key.toLowerCase()); } } From 9dabc7b0b7c407b395767665bd5d19b9c0843e1d Mon Sep 17 00:00:00 2001 From: RiteshHMCTS Date: Tue, 26 May 2026 16:28:46 +0100 Subject: [PATCH 4/9] sonar fix --- .../shared/services/logging/structured-logger.service.ts | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/projects/ccd-case-ui-toolkit/src/lib/shared/services/logging/structured-logger.service.ts b/projects/ccd-case-ui-toolkit/src/lib/shared/services/logging/structured-logger.service.ts index 962ba61cd6..48878d11b7 100644 --- a/projects/ccd-case-ui-toolkit/src/lib/shared/services/logging/structured-logger.service.ts +++ b/projects/ccd-case-ui-toolkit/src/lib/shared/services/logging/structured-logger.service.ts @@ -17,8 +17,8 @@ export class StructuredLoggerService { private static readonly MAX_DEPTH_VALUE = '[MaxDepth]'; private static readonly MAX_REDACTION_DEPTH = 10; private static readonly REDACTED_VALUE = '[REDACTED]'; + private static readonly KEY_VALUE_PATTERN = /\b([A-Za-z][\w-]*(?:[ _-][A-Za-z][\w-]*)?)([ \t]*[:=][ \t]*)((?:Bearer[ \t]+)?)([^,;&\s]+)/gi; private static readonly SENSITIVE_KEY_PATTERN = /(password|passcode|pwd|secret|token|authori[sz]ation|authentication|auth[-_ ]?context|^auth$|api[-_ ]?key|cookie|session|credential)/i; - private static readonly SENSITIVE_STRING_PATTERN = /([\w -]+\s*[:=]\s*)((?:Bearer\s+)?)([^,;&\s]+)/gi; private static readonly BEARER_TOKEN_PATTERN = /\bBearer\s+([\w.~+/-]+=*)/gi; public debug(message: string, context?: unknown): void { @@ -142,9 +142,8 @@ export class StructuredLoggerService { private redactSensitiveString(value: string): string { return value - .replace(StructuredLoggerService.SENSITIVE_STRING_PATTERN, (match: string, prefix: string, bearerPrefix: string) => { - const key = prefix.replace(/\s*[:=]\s*$/u, '').trim(); - return this.isSensitiveKey(key) ? `${prefix}${bearerPrefix}[REDACTED]` : match; + .replace(StructuredLoggerService.KEY_VALUE_PATTERN, (match: string, key: string, separator: string, bearerPrefix: string) => { + return this.isSensitiveKey(key) ? `${key}${separator}${bearerPrefix}${StructuredLoggerService.REDACTED_VALUE}` : match; }) .replace(StructuredLoggerService.BEARER_TOKEN_PATTERN, 'Bearer [REDACTED]'); } From a15be94ec41f2fe4f2ea2c674b27219e6fb5f1d5 Mon Sep 17 00:00:00 2001 From: RiteshHMCTS Date: Tue, 26 May 2026 17:00:39 +0100 Subject: [PATCH 5/9] sonar issue --- .../lib/shared/services/logging/structured-logger.service.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/projects/ccd-case-ui-toolkit/src/lib/shared/services/logging/structured-logger.service.ts b/projects/ccd-case-ui-toolkit/src/lib/shared/services/logging/structured-logger.service.ts index 48878d11b7..2286ca7dcd 100644 --- a/projects/ccd-case-ui-toolkit/src/lib/shared/services/logging/structured-logger.service.ts +++ b/projects/ccd-case-ui-toolkit/src/lib/shared/services/logging/structured-logger.service.ts @@ -17,7 +17,7 @@ export class StructuredLoggerService { private static readonly MAX_DEPTH_VALUE = '[MaxDepth]'; private static readonly MAX_REDACTION_DEPTH = 10; private static readonly REDACTED_VALUE = '[REDACTED]'; - private static readonly KEY_VALUE_PATTERN = /\b([A-Za-z][\w-]*(?:[ _-][A-Za-z][\w-]*)?)([ \t]*[:=][ \t]*)((?:Bearer[ \t]+)?)([^,;&\s]+)/gi; + private static readonly KEY_VALUE_PATTERN = /\b([a-z][\w-]*(?:[ _-][a-z][\w-]*)?)([ \t]*[:=][ \t]*)((?:Bearer[ \t]+)?)([^,;&\s]+)/gi; private static readonly SENSITIVE_KEY_PATTERN = /(password|passcode|pwd|secret|token|authori[sz]ation|authentication|auth[-_ ]?context|^auth$|api[-_ ]?key|cookie|session|credential)/i; private static readonly BEARER_TOKEN_PATTERN = /\bBearer\s+([\w.~+/-]+=*)/gi; From 6a80f7ac83bc82fe47bdc8f9cfc2b0a9abf30e29 Mon Sep 17 00:00:00 2001 From: RiteshHMCTS Date: Thu, 4 Jun 2026 10:08:28 +0100 Subject: [PATCH 6/9] removed logs --- .../lib/shared/components/case-viewer/services/case.resolver.ts | 1 - .../src/lib/shared/services/http/http-error.service.ts | 1 - 2 files changed, 2 deletions(-) diff --git a/projects/ccd-case-ui-toolkit/src/lib/shared/components/case-viewer/services/case.resolver.ts b/projects/ccd-case-ui-toolkit/src/lib/shared/components/case-viewer/services/case.resolver.ts index e26ab3fbfe..43ae25dcb0 100644 --- a/projects/ccd-case-ui-toolkit/src/lib/shared/components/case-viewer/services/case.resolver.ts +++ b/projects/ccd-case-ui-toolkit/src/lib/shared/components/case-viewer/services/case.resolver.ts @@ -114,7 +114,6 @@ export class CaseResolver implements Resolve { } private processErrorInCaseFetch(error: any, caseReference: string) { - this.logger.error('Error while fetching case view.', { caseReference, error }); // TODO Should be logged to remote logging infrastructure if (error.status === 400) { this.router.navigate(['/search/noresults']); diff --git a/projects/ccd-case-ui-toolkit/src/lib/shared/services/http/http-error.service.ts b/projects/ccd-case-ui-toolkit/src/lib/shared/services/http/http-error.service.ts index 4ef6298e00..f0ca0c3562 100644 --- a/projects/ccd-case-ui-toolkit/src/lib/shared/services/http/http-error.service.ts +++ b/projects/ccd-case-ui-toolkit/src/lib/shared/services/http/http-error.service.ts @@ -57,7 +57,6 @@ export class HttpErrorService { } public handle(error: HttpErrorResponse | any, redirectIfNotAuthorised = true): Observable { - HttpErrorService.logger.error('Handling error in http error service.', { error }); if (this.loadingService.hasSharedSpinner()){ this.loadingService.unregisterSharedSpinner(); } From 1a8e155f6c2b22eb8b00e0f736bf35ac68f01ce5 Mon Sep 17 00:00:00 2001 From: RiteshHMCTS Date: Thu, 4 Jun 2026 10:09:16 +0100 Subject: [PATCH 7/9] version updated --- package.json | 2 +- projects/ccd-case-ui-toolkit/package.json | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/package.json b/package.json index ae26055ccd..cb91480aec 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@hmcts/ccd-case-ui-toolkit", - "version": "7.3.53-exui-4648-rc1", + "version": "7.3.54-exui-4648-rc-1", "engines": { "node": ">=20.19.0" }, diff --git a/projects/ccd-case-ui-toolkit/package.json b/projects/ccd-case-ui-toolkit/package.json index 2a80eff8da..370fd0140c 100644 --- a/projects/ccd-case-ui-toolkit/package.json +++ b/projects/ccd-case-ui-toolkit/package.json @@ -1,6 +1,6 @@ { "name": "@hmcts/ccd-case-ui-toolkit", - "version": "7.3.53-exui-4648-rc1", + "version": "7.3.54-exui-4648-rc-1", "engines": { "node": ">=20.19.0" }, From cfa026c88c549a9a09a36e7cd961ce8bd5ba8537 Mon Sep 17 00:00:00 2001 From: RiteshHMCTS Date: Thu, 4 Jun 2026 10:25:31 +0100 Subject: [PATCH 8/9] removed logs --- .../components/case-viewer/case-view/case-view.component.ts | 5 ----- .../shared/components/case-viewer/case-viewer.component.ts | 4 +--- .../shared/components/case-viewer/services/case.resolver.ts | 6 +----- 3 files changed, 2 insertions(+), 13 deletions(-) diff --git a/projects/ccd-case-ui-toolkit/src/lib/shared/components/case-viewer/case-view/case-view.component.ts b/projects/ccd-case-ui-toolkit/src/lib/shared/components/case-viewer/case-view/case-view.component.ts index bdafc6b419..06ef2b92a4 100644 --- a/projects/ccd-case-ui-toolkit/src/lib/shared/components/case-viewer/case-view/case-view.component.ts +++ b/projects/ccd-case-ui-toolkit/src/lib/shared/components/case-viewer/case-view/case-view.component.ts @@ -6,7 +6,6 @@ import { CaseView } from '../../../domain/case-view/case-view.model'; import { Draft } from '../../../domain/draft.model'; import { AlertService } from '../../../services/alert/alert.service'; import { DraftService } from '../../../services/draft/draft.service'; -import { StructuredLoggerService } from '../../../services/logging'; import { NavigationNotifierService } from '../../../services/navigation/navigation-notifier.service'; import { CaseNotifier } from '../../case-editor/services/case.notifier'; import { CasesService } from '../../case-editor/services/cases.service'; @@ -18,9 +17,6 @@ import { CasesService } from '../../case-editor/services/cases.service'; standalone: false }) export class CaseViewComponent implements OnInit, OnDestroy { - private readonly logger = new StructuredLoggerService(); - - @Input() public case: string; @Input() @@ -82,7 +78,6 @@ export class CaseViewComponent implements OnInit, OnDestroy { private checkErrorGettingCaseView(error: any) { // TODO Should be logged to remote logging infrastructure - this.logger.error('Error while getting case view.', { error }); if (error.status !== 401 && error.status !== 403) { this.alertService.error(error.message); } diff --git a/projects/ccd-case-ui-toolkit/src/lib/shared/components/case-viewer/case-viewer.component.ts b/projects/ccd-case-ui-toolkit/src/lib/shared/components/case-viewer/case-viewer.component.ts index 1b6b5b63fc..abcd22406f 100644 --- a/projects/ccd-case-ui-toolkit/src/lib/shared/components/case-viewer/case-viewer.component.ts +++ b/projects/ccd-case-ui-toolkit/src/lib/shared/components/case-viewer/case-viewer.component.ts @@ -4,7 +4,7 @@ import { Subscription } from 'rxjs'; import { AbstractAppConfig } from '../../../app.config'; import { CaseTab, CaseView } from '../../domain'; import { CaseNotifier } from '../case-editor'; -import { OrderService, StructuredLoggerService } from '../../services'; +import { OrderService } from '../../services'; @Component({ selector: 'ccd-case-viewer', @@ -16,7 +16,6 @@ export class CaseViewerComponent implements OnInit, OnDestroy { public static readonly METADATA_FIELD_ACCESS_GRANTED_ID = '[ACCESS_GRANTED]'; public static readonly NON_STANDARD_USER_ACCESS_TYPES = ['CHALLENGED', 'SPECIFIC']; public static readonly BASIC_USER_ACCESS_TYPES = 'BASIC'; - private readonly logger = new StructuredLoggerService(); @Input() public hasPrint = true; @Input() public hasEventSelector = true; @@ -53,7 +52,6 @@ export class CaseViewerComponent implements OnInit, OnDestroy { this.setUserAccessType(this.caseDetails); } else { this.caseSubscription = this.caseNotifier.caseView.subscribe((caseDetails) => { - this.logger.info('Setting the case into case viewer component as retrieved from XHR request.'); this.caseDetails = caseDetails; this.setUserAccessType(this.caseDetails); }); diff --git a/projects/ccd-case-ui-toolkit/src/lib/shared/components/case-viewer/services/case.resolver.ts b/projects/ccd-case-ui-toolkit/src/lib/shared/components/case-viewer/services/case.resolver.ts index 43ae25dcb0..67af542748 100644 --- a/projects/ccd-case-ui-toolkit/src/lib/shared/components/case-viewer/services/case.resolver.ts +++ b/projects/ccd-case-ui-toolkit/src/lib/shared/components/case-viewer/services/case.resolver.ts @@ -5,7 +5,7 @@ import { of, throwError } from 'rxjs'; import { catchError, filter, map } from 'rxjs/operators'; import { AbstractAppConfig } from '../../../../app.config'; import { CaseView, Draft } from '../../../domain'; -import { DraftService, NavigationOrigin, SessionStorageService, StructuredLoggerService } from '../../../services'; +import { DraftService, NavigationOrigin, SessionStorageService } from '../../../services'; import { NavigationNotifierService } from '../../../services/navigation/navigation-notifier.service'; import { PUI_CASE_MANAGER, USER_DETAILS } from '../../../utils'; import { safeJsonParse } from '../../../json-utils'; @@ -21,7 +21,6 @@ export class CaseResolver implements Resolve { public static defaultWAPage = '/work/my-work/list'; public static defaultPage = '/cases'; - private readonly logger = new StructuredLoggerService(); // we need to run the CaseResolver on every child route of 'case/:jid/:ctid/:cid' // this is achieved with runGuardsAndResolvers: 'always' configuration @@ -44,12 +43,10 @@ export class CaseResolver implements Resolve { // Prevent resolving if eventId=queryManagementRespondQuery is in the URL if (currentUrl.includes(CaseResolver.EVENT_ID_QM_RESPOND_TO_QUERY)) { - this.logger.info('Skipping resolve for event queryManagementRespondQuery.'); this.goToDefaultPage(); } if (!cid) { - this.logger.info('No case ID available in the route. Will navigate to case list.'); // when redirected to case view after a case created, and the user has no READ access, // the post returns no id this.navigateToCaseList(); @@ -142,7 +139,6 @@ export class CaseResolver implements Resolve { // as discussed for EUI-5456, need functionality to go to default page private goToDefaultPage(): void { - this.logger.info('Going to default page.'); const userDetails = safeJsonParse(this.sessionStorage.getItem(USER_DETAILS)); userDetails && userDetails.roles && !userDetails.roles.includes(PUI_CASE_MANAGER) From ccbb71a8f8d6de2f3db17d33bdea3274728a7a7d Mon Sep 17 00:00:00 2001 From: RiteshHMCTS Date: Thu, 4 Jun 2026 10:58:29 +0100 Subject: [PATCH 9/9] release notes --- RELEASE-NOTES.md | 3 +++ 1 file changed, 3 insertions(+) diff --git a/RELEASE-NOTES.md b/RELEASE-NOTES.md index 8878e6c0d8..5d40ba9629 100644 --- a/RELEASE-NOTES.md +++ b/RELEASE-NOTES.md @@ -1,5 +1,8 @@ ## RELEASE NOTES +### Version 7.3.54-exui-4648-rc-1 +**EXUI-4648** Redact Sensitive Information Logged In through Console + ### Version 7.3.54 **EXUI-4636** Revert CME-220 and EXUI-2111 **EXUI-4637** Revert EXUI-2645