From 2c7ac85f6eac65be4ab21152dffd91326d8decfb Mon Sep 17 00:00:00 2001 From: Marcel Haag Date: Wed, 29 Mar 2023 14:56:32 +0200 Subject: [PATCH] feat: As an user I want a retry dialog guard, in order to resend a failed request --- security-c4po-angular/src/app/app.module.ts | 2 + .../pentest-comments.component.ts | 85 +++++++------- .../pentest-findings.component.ts | 89 +++++++-------- .../project-overview.component.ts | 65 +++++------ .../src/assets/i18n/de-DE.json | 6 +- .../src/assets/i18n/en-US.json | 6 +- .../comment-dialog.component.ts | 89 +++++++++++++-- .../service/comment-dialog.service.ts | 13 ++- .../export-report-dialog.component.ts | 18 ++- .../finding-dialog.component.ts | 107 +++++++++++++++--- .../service/finding-dialog.service.ts | 10 +- .../project-dialog.component.ts | 81 +++++++++++-- .../service/project-dialog.service.ts | 6 +- .../retry-dialog/retry-dialog.component.html | 22 ++++ .../retry-dialog/retry-dialog.component.scss | 11 ++ .../retry-dialog.component.spec.ts | 58 ++++++++++ .../retry-dialog/retry-dialog.component.ts | 31 +++++ .../retry-dialog/retry-dialog.module.ts | 29 +++++ .../modules/retry-dialog/retry.function.ts | 20 ++++ .../dialog-service/dialog.service.mock.ts | 4 + .../services/dialog-service/dialog.service.ts | 16 +++ .../api/project/ProjectController.kt | 2 +- 22 files changed, 600 insertions(+), 170 deletions(-) create mode 100644 security-c4po-angular/src/shared/modules/retry-dialog/retry-dialog.component.html create mode 100644 security-c4po-angular/src/shared/modules/retry-dialog/retry-dialog.component.scss create mode 100644 security-c4po-angular/src/shared/modules/retry-dialog/retry-dialog.component.spec.ts create mode 100644 security-c4po-angular/src/shared/modules/retry-dialog/retry-dialog.component.ts create mode 100644 security-c4po-angular/src/shared/modules/retry-dialog/retry-dialog.module.ts create mode 100644 security-c4po-angular/src/shared/modules/retry-dialog/retry.function.ts diff --git a/security-c4po-angular/src/app/app.module.ts b/security-c4po-angular/src/app/app.module.ts index bc93689..04a8477 100644 --- a/security-c4po-angular/src/app/app.module.ts +++ b/security-c4po-angular/src/app/app.module.ts @@ -36,6 +36,7 @@ import {ProjectState} from '@shared/stores/project-state/project-state'; import {CustomOverlayContainer} from '@shared/modules/custom-overlay-container.component'; import {DialogService} from '@shared/services/dialog-service/dialog.service'; import {FormsModule, ReactiveFormsModule} from '@angular/forms'; +import {RetryDialogModule} from '@shared/modules/retry-dialog/retry-dialog.module'; @NgModule({ declarations: [ @@ -72,6 +73,7 @@ import {FormsModule, ReactiveFormsModule} from '@angular/forms'; }), HeaderModule, HomeModule, + RetryDialogModule ], providers: [ HttpClient, diff --git a/security-c4po-angular/src/app/pentest/pentest-content/pentest-comments/pentest-comments.component.ts b/security-c4po-angular/src/app/pentest/pentest-content/pentest-comments/pentest-comments.component.ts index b85f99e..5ea73d7 100644 --- a/security-c4po-angular/src/app/pentest/pentest-content/pentest-comments/pentest-comments.component.ts +++ b/security-c4po-angular/src/app/pentest/pentest-content/pentest-comments/pentest-comments.component.ts @@ -5,13 +5,11 @@ import * as FA from '@fortawesome/free-solid-svg-icons'; import {NbGetters, NbTreeGridDataSource, NbTreeGridDataSourceBuilder} from '@nebular/theme'; import {NotificationService, PopupType} from '@shared/services/toaster-service/notification.service'; import {UntilDestroy, untilDestroyed} from '@ngneat/until-destroy'; -import {catchError, filter, mergeMap, switchMap, tap} from 'rxjs/operators'; +import {filter, tap} from 'rxjs/operators'; import { Comment, - CommentDialogBody, CommentEntry, - transformCommentsToObjectiveEntries, - transformCommentToRequestBody + transformCommentsToObjectiveEntries } from '@shared/models/comment.model'; import {isNotNullOrUndefined} from 'codelyzer/util/isNotNullOrUndefined'; import {ProjectState} from '@shared/stores/project-state/project-state'; @@ -115,25 +113,13 @@ export class PentestCommentsComponent implements OnInit { hasScroll: false, autoFocus: true, closeOnBackdropClick: false - } + }, + this.pentestInfo$.getValue() ).pipe( - filter(value => !!value), - mergeMap((value: CommentDialogBody) => - this.commentService.saveComment( - this.pentestInfo$.getValue() ? this.pentestInfo$.getValue().id : '', - transformCommentToRequestBody(value) - ) - ), untilDestroyed(this) ).subscribe({ next: (newComment: Comment) => { - this.store.dispatch(new UpdatePentestComments(newComment.id)); this.loadCommentsData(); - this.notificationService.showPopup('comment.popup.save.success', PopupType.SUCCESS); - }, - error: err => { - console.error(err); - this.notificationService.showPopup('comment.popup.save.failed', PopupType.FAILURE); } }); } @@ -154,24 +140,13 @@ export class PentestCommentsComponent implements OnInit { hasScroll: false, autoFocus: true, closeOnBackdropClick: false - } + }, + this.pentestInfo$.getValue() ).pipe( - filter(value => !!value), - mergeMap((value: CommentDialogBody) => - this.commentService.updateComment( - commentEntry.data.commentId, - transformCommentToRequestBody(value) - ) - ), untilDestroyed(this) ).subscribe({ next: (updatedComment: Comment) => { this.loadCommentsData(); - this.notificationService.showPopup('comment.popup.update.success', PopupType.SUCCESS); - }, - error: err => { - console.error(err); - this.notificationService.showPopup('comment.popup.update.failed', PopupType.FAILURE); } }); } else { @@ -206,23 +181,10 @@ export class PentestCommentsComponent implements OnInit { this.dialogService.openConfirmDialog( message ).onClose.pipe( - filter((confirm) => !!confirm), - switchMap(() => this.commentService.deleteCommentByPentestAndCommentId( - this.pentestInfo$.getValue() ? this.pentestInfo$.getValue().id : '', - commentEntry.data.commentId) - ), - catchError(() => { - this.notificationService.showPopup('comment.popup.delete.failed', PopupType.FAILURE); - return []; - }), untilDestroyed(this) ).subscribe({ - next: (deletedComment: any) => { - this.store.dispatch(new UpdatePentestComments(deletedComment.id)); - this.loadCommentsData(); - this.notificationService.showPopup('comment.popup.delete.success', PopupType.SUCCESS); - }, error: error => { - console.error(error); + next: () => { + this.deleteComment(commentEntry); } }); } @@ -231,6 +193,37 @@ export class PentestCommentsComponent implements OnInit { isLoading(): Observable { return this.loading$.asObservable(); } + + private deleteComment(commentEntry): void { + this.commentService.deleteCommentByPentestAndCommentId( + this.pentestInfo$.getValue() ? this.pentestInfo$.getValue().id : '', + commentEntry.data.commentId) + .pipe( + untilDestroyed(this) + ).subscribe({ + next: (deletedComment: any) => { + this.store.dispatch(new UpdatePentestComments(deletedComment.id)); + this.loadCommentsData(); + this.notificationService.showPopup('comment.popup.delete.success', PopupType.SUCCESS); + }, error: error => { + console.error(error); + this.onRequestFailed(commentEntry); + this.notificationService.showPopup('comment.popup.delete.failed', PopupType.FAILURE); + } + }); + } + + private onRequestFailed(retryParameter: any): void { + this.dialogService.openRetryDialog({key: 'global.retry.dialog', data: null}).onClose + .pipe( + untilDestroyed(this) + ) + .subscribe((ref) => { + if (ref.retry) { + this.deleteComment(retryParameter); + } + }); + } } enum CommentColumns { diff --git a/security-c4po-angular/src/app/pentest/pentest-content/pentest-findings/pentest-findings.component.ts b/security-c4po-angular/src/app/pentest/pentest-content/pentest-findings/pentest-findings.component.ts index d62d837..775131c 100644 --- a/security-c4po-angular/src/app/pentest/pentest-content/pentest-findings/pentest-findings.component.ts +++ b/security-c4po-angular/src/app/pentest/pentest-content/pentest-findings/pentest-findings.component.ts @@ -2,14 +2,12 @@ import {Component, OnInit} from '@angular/core'; import {BehaviorSubject, Observable} from 'rxjs'; import {Pentest} from '@shared/models/pentest.model'; import {UntilDestroy, untilDestroyed} from '@ngneat/until-destroy'; -import {catchError, filter, mergeMap, switchMap, tap} from 'rxjs/operators'; +import { filter, tap} from 'rxjs/operators'; import {NotificationService, PopupType} from '@shared/services/toaster-service/notification.service'; import { Finding, - FindingDialogBody, FindingEntry, transformFindingsToObjectiveEntries, - transformFindingToRequestBody, } from '@shared/models/finding.model'; import {NbGetters, NbTreeGridDataSource, NbTreeGridDataSourceBuilder} from '@nebular/theme'; import * as FA from '@fortawesome/free-solid-svg-icons'; @@ -31,9 +29,9 @@ import {FindingService} from '@shared/services/api/finding.service'; }) export class PentestFindingsComponent implements OnInit { - constructor(private readonly findingService: FindingService, + constructor(private findingService: FindingService, private dataSourceBuilder: NbTreeGridDataSourceBuilder, - private notificationService: NotificationService, + private readonly notificationService: NotificationService, private dialogService: DialogService, private findingDialogService: FindingDialogService, private store: Store) { @@ -111,26 +109,13 @@ export class PentestFindingsComponent implements OnInit { hasScroll: false, autoFocus: true, closeOnBackdropClick: false - } + }, + this.pentestInfo$.getValue() ).pipe( - filter(value => !!value), - /*tap((value) => console.warn('FindingDialogBody: ', value)),*/ - mergeMap((value: FindingDialogBody) => - this.findingService.saveFinding( - this.pentestInfo$.getValue() ? this.pentestInfo$.getValue().id : '', - transformFindingToRequestBody(value) - ) - ), untilDestroyed(this) ).subscribe({ next: (newFinding: Finding) => { - this.store.dispatch(new UpdatePentestFindings(newFinding.id)); this.loadFindingsData(); - this.notificationService.showPopup('finding.popup.save.success', PopupType.SUCCESS); - }, - error: err => { - console.error(err); - this.notificationService.showPopup('finding.popup.save.failed', PopupType.FAILURE); } }); } @@ -150,25 +135,13 @@ export class PentestFindingsComponent implements OnInit { hasScroll: false, autoFocus: true, closeOnBackdropClick: false - } + }, + this.pentestInfo$.getValue() ).pipe( - filter(value => !!value), - /*tap((value) => console.warn('FindingDialogBody: ', value)),*/ - mergeMap((value: FindingDialogBody) => - this.findingService.updateFinding( - findingEntry.data.findingId, - transformFindingToRequestBody(value) - ) - ), untilDestroyed(this) ).subscribe({ next: (updatedFinding: Finding) => { this.loadFindingsData(); - this.notificationService.showPopup('finding.popup.update.success', PopupType.SUCCESS); - }, - error: err => { - console.error(err); - this.notificationService.showPopup('finding.popup.update.failed', PopupType.FAILURE); } }); } else { @@ -190,23 +163,10 @@ export class PentestFindingsComponent implements OnInit { this.dialogService.openConfirmDialog( message ).onClose.pipe( - filter((confirm) => !!confirm), - switchMap(() => this.findingService.deleteFindingByPentestAndFindingId( - this.pentestInfo$.getValue() ? this.pentestInfo$.getValue().id : '', - findingEntry.data.findingId) - ), - catchError(() => { - this.notificationService.showPopup('finding.popup.delete.failed', PopupType.FAILURE); - return []; - }), untilDestroyed(this) ).subscribe({ - next: (deletedFinding: any) => { - this.store.dispatch(new UpdatePentestFindings(deletedFinding.id)); - this.loadFindingsData(); - this.notificationService.showPopup('finding.popup.delete.success', PopupType.SUCCESS); - }, error: error => { - console.error(error); + next: () => { + this.deleteFinding(findingEntry); } }); } @@ -214,6 +174,37 @@ export class PentestFindingsComponent implements OnInit { isLoading(): Observable { return this.loading$.asObservable(); } + + private deleteFinding(findingEntry): void { + this.findingService.deleteFindingByPentestAndFindingId( + this.pentestInfo$.getValue() ? this.pentestInfo$.getValue().id : '', + findingEntry.data.findingId) + .pipe( + untilDestroyed(this) + ).subscribe({ + next: (deletedFinding: any) => { + this.store.dispatch(new UpdatePentestFindings(deletedFinding.id)); + this.loadFindingsData(); + this.notificationService.showPopup('finding.popup.delete.success', PopupType.SUCCESS); + }, error: error => { + console.error(error); + this.onRequestFailed(findingEntry); + this.notificationService.showPopup('finding.popup.delete.failed', PopupType.FAILURE); + } + }); + } + + private onRequestFailed(retryParameter: any): void { + this.dialogService.openRetryDialog({key: 'global.retry.dialog', data: null}).onClose + .pipe( + untilDestroyed(this) + ) + .subscribe((ref) => { + if (ref.retry) { + this.deleteFinding(retryParameter); + } + }); + } } enum FindingColumns { diff --git a/security-c4po-angular/src/app/project-overview/project-overview.component.ts b/security-c4po-angular/src/app/project-overview/project-overview.component.ts index 9865178..c30b800 100644 --- a/security-c4po-angular/src/app/project-overview/project-overview.component.ts +++ b/security-c4po-angular/src/app/project-overview/project-overview.component.ts @@ -1,11 +1,11 @@ import {Component, OnInit} from '@angular/core'; import * as FA from '@fortawesome/free-solid-svg-icons'; -import {Project, ProjectDialogBody} from '@shared/models/project.model'; +import {Project} from '@shared/models/project.model'; import {BehaviorSubject, Observable} from 'rxjs'; import {UntilDestroy, untilDestroyed} from '@ngneat/until-destroy'; import {ProjectService} from '@shared/services/api/project.service'; import {NotificationService, PopupType} from '@shared/services/toaster-service/notification.service'; -import {catchError, filter, mergeMap, switchMap, tap} from 'rxjs/operators'; +import {filter, tap} from 'rxjs/operators'; import {DialogService} from '@shared/services/dialog-service/dialog.service'; import {ProjectDialogComponent} from '@shared/modules/project-dialog/project-dialog.component'; import {ProjectDialogService} from '@shared/modules/project-dialog/service/project-dialog.service'; @@ -70,17 +70,10 @@ export class ProjectOverviewComponent implements OnInit { closeOnBackdropClick: false } ).pipe( - filter(value => !!value), - mergeMap((value: ProjectDialogBody) => this.projectService.saveProject(value)), untilDestroyed(this) ).subscribe({ next: () => { this.loadProjects(); - this.notificationService.showPopup('project.popup.save.success', PopupType.SUCCESS); - }, - error: err => { - console.error(err); - this.notificationService.showPopup('project.popup.save.failed', PopupType.FAILURE); } }); } @@ -96,17 +89,10 @@ export class ProjectOverviewComponent implements OnInit { closeOnBackdropClick: false } ).pipe( - filter(value => !!value), - mergeMap((value: ProjectDialogBody) => this.projectService.updateProject(project.id, value)), untilDestroyed(this) ).subscribe({ next: () => { this.loadProjects(); - this.notificationService.showPopup('project.popup.update.success', PopupType.SUCCESS); - }, - error: error => { - console.error(error); - this.notificationService.showPopup('project.popup.update.failed', PopupType.FAILURE); } }); } @@ -124,18 +110,10 @@ export class ProjectOverviewComponent implements OnInit { message ).onClose.pipe( filter((confirm) => !!confirm), - switchMap(() => this.projectService.deleteProjectById(project.id)), - catchError(() => { - this.notificationService.showPopup('project.popup.delete.failed', PopupType.FAILURE); - return []; - }), untilDestroyed(this) ).subscribe({ next: () => { - this.loadProjects(); - this.notificationService.showPopup('project.popup.delete.success', PopupType.SUCCESS); - }, error: error => { - console.error(error); + this.deleteProject(project); } }); } else { @@ -152,18 +130,10 @@ export class ProjectOverviewComponent implements OnInit { secMessage ).onClose.pipe( filter((confirm) => !!confirm), - switchMap(() => this.projectService.deleteProjectById(project.id)), - catchError(() => { - this.notificationService.showPopup('project.popup.delete.failed', PopupType.FAILURE); - return []; - }), untilDestroyed(this) ).subscribe({ next: () => { - this.loadProjects(); - this.notificationService.showPopup('project.popup.delete.success', PopupType.SUCCESS); - }, error: error => { - console.error(error); + this.deleteProject(project); } }); } @@ -185,4 +155,31 @@ export class ProjectOverviewComponent implements OnInit { isLoading(): Observable { return this.loading$.asObservable(); } + + private deleteProject(project: Project): void { + this.projectService.deleteProjectById(project.id).pipe( + untilDestroyed(this) + ).subscribe({ + next: () => { + this.loadProjects(); + this.notificationService.showPopup('project.popup.delete.success', PopupType.SUCCESS); + }, error: error => { + this.notificationService.showPopup('project.popup.delete.failed', PopupType.FAILURE); + this.onRequestFailed(project); + console.error(error); + } + }); + } + + private onRequestFailed(retryParameter: any): void { + this.dialogService.openRetryDialog({key: 'global.retry.dialog', data: null}).onClose + .pipe( + untilDestroyed(this) + ) + .subscribe((ref) => { + if (ref.retry) { + this.deleteProject(retryParameter); + } + }); + } } diff --git a/security-c4po-angular/src/assets/i18n/de-DE.json b/security-c4po-angular/src/assets/i18n/de-DE.json index 161496a..cd3dfeb 100644 --- a/security-c4po-angular/src/assets/i18n/de-DE.json +++ b/security-c4po-angular/src/assets/i18n/de-DE.json @@ -21,10 +21,14 @@ "username": "Nutzername", "password": "Passwort", "no.progress": "Kein Fortschritt", + "project": "Projekt", "validationMessage": { "inputNotMatching": "Eingabe stimmt nicht überein!" }, - "project": "Projekt" + "retry.dialog": { + "title": "Etwas ist schief gelaufen...", + "information": "Fehler beim verarbeiten Ihrer Anfrage. \nBitte versuchen Sie es erneut oder zu einem späteren Zeitpunkt." + } }, "languageKeys":{ "de-DE": "Deutsch", diff --git a/security-c4po-angular/src/assets/i18n/en-US.json b/security-c4po-angular/src/assets/i18n/en-US.json index a77c870..5491102 100644 --- a/security-c4po-angular/src/assets/i18n/en-US.json +++ b/security-c4po-angular/src/assets/i18n/en-US.json @@ -21,10 +21,14 @@ "username": "Username", "password": "Password", "no.progress": "No progress", + "project": "Project", "validationMessage": { "inputNotMatching": "Input does not match!" }, - "project": "Project" + "retry.dialog": { + "title": "Something went wrong...", + "information": "An error occured while processing your request. \nPlease retry or try it again later." + } }, "languageKeys":{ "de-DE": "German", diff --git a/security-c4po-angular/src/shared/modules/comment-dialog/comment-dialog.component.ts b/security-c4po-angular/src/shared/modules/comment-dialog/comment-dialog.component.ts index e8d43f3..ff43ac8 100644 --- a/security-c4po-angular/src/shared/modules/comment-dialog/comment-dialog.component.ts +++ b/security-c4po-angular/src/shared/modules/comment-dialog/comment-dialog.component.ts @@ -4,7 +4,13 @@ import {GenericDialogData, GenericFormFieldConfig} from '@shared/models/generic- import * as FA from '@fortawesome/free-solid-svg-icons'; import deepEqual from 'deep-equal'; import {NB_DIALOG_CONFIG, NbDialogRef} from '@nebular/theme'; -import {UntilDestroy} from '@ngneat/until-destroy'; +import {UntilDestroy, untilDestroyed} from '@ngneat/until-destroy'; +import {Comment, transformCommentToRequestBody} from '@shared/models/comment.model'; +import {UpdatePentestComments} from '@shared/stores/project-state/project-state.actions'; +import {NotificationService, PopupType} from '@shared/services/toaster-service/notification.service'; +import {DialogService} from '@shared/services/dialog-service/dialog.service'; +import {Store} from '@ngxs/store'; +import {CommentService} from '@shared/services/api/comment.service'; @Component({ selector: 'app-comment-dialog', @@ -26,7 +32,11 @@ export class CommentDialogComponent implements OnInit { constructor( @Inject(NB_DIALOG_CONFIG) private data: GenericDialogData, private fb: FormBuilder, - protected dialogRef: NbDialogRef + protected dialogRef: NbDialogRef, + private commentService: CommentService, + private readonly notificationService: NotificationService, + private dialogService: DialogService, + private store: Store ) { } @@ -50,11 +60,13 @@ export class CommentDialogComponent implements OnInit { } onClickSave(value: any): void { - this.dialogRef.close({ - title: value.commentTitle, - description: value.commentDescription, - // relatedFindings: this.selectedFindings ? this.selectedFindings : [] - }); + if (this.dialogData.options[0].headerLabelKey.includes('create')) { + // Save + this.saveComment(value); + } else { + // Update + this.updateComment(value); + } } onClickClose(): void { @@ -91,4 +103,67 @@ export class CommentDialogComponent implements OnInit { }); return commentData; } + + saveComment(value): void { + const dialogRes = { + title: value.commentTitle, + description: value.commentDescription, + // relatedFindings: this.selectedFindings ? this.selectedFindings : [] + }; + + this.commentService.saveComment( + this.dialogData.options[0].additionalData.id, + transformCommentToRequestBody(dialogRes) + ).pipe( + untilDestroyed(this) + ).subscribe({ + next: (newComment: Comment) => { + this.store.dispatch(new UpdatePentestComments(newComment.id)); + this.dialogRef.close(); + this.notificationService.showPopup('comment.popup.save.success', PopupType.SUCCESS); + }, + error: err => { + console.error(err); + this.onRequestFailed(value); + this.notificationService.showPopup('comment.popup.save.failed', PopupType.FAILURE); + } + }); + } + + updateComment(value): void { + const dialogRes = { + title: value.commentTitle, + description: value.commentDescription, + // relatedFindings: this.selectedFindings ? this.selectedFindings : [] + }; + + this.commentService.updateComment( + this.dialogData.options[0].additionalData.id, + transformCommentToRequestBody(dialogRes) + ).pipe( + untilDestroyed(this) + ).subscribe({ + next: (updatedComment: Comment) => { + this.dialogRef.close(); + this.notificationService.showPopup('comment.popup.update.success', PopupType.SUCCESS); + }, + error: err => { + console.error(err); + this.onRequestFailed(value); + this.notificationService.showPopup('comment.popup.update.failed', PopupType.FAILURE); + } + }); + } + + private onRequestFailed(retryParameter: any): void { + this.dialogService.openRetryDialog({key: 'global.retry.dialog', data: null}).onClose + .pipe( + untilDestroyed(this) + ) + .subscribe((ref) => { + if (ref.retry) { + this.onClickSave(retryParameter); + } + }); + } } diff --git a/security-c4po-angular/src/shared/modules/comment-dialog/service/comment-dialog.service.ts b/security-c4po-angular/src/shared/modules/comment-dialog/service/comment-dialog.service.ts index c6da978..e257749 100644 --- a/security-c4po-angular/src/shared/modules/comment-dialog/service/comment-dialog.service.ts +++ b/security-c4po-angular/src/shared/modules/comment-dialog/service/comment-dialog.service.ts @@ -6,6 +6,7 @@ import {Observable} from 'rxjs'; import {Validators} from '@angular/forms'; import {CommentDialogComponent} from '@shared/modules/comment-dialog/comment-dialog.component'; import {Comment} from '@shared/models/comment.model'; +import {Pentest} from '@shared/models/pentest.model'; @Injectable() export class CommentDialogService { @@ -31,16 +32,18 @@ export class CommentDialogService { public openCommentDialog(componentOrTemplateRef: ComponentType, findingIds: string[], comment?: Comment, - config?: Partial | string>>): Observable { + config?: Partial | string>>, + pentestInfo?: Pentest): Observable { let dialogOptions: Partial | string>>; let dialogData: GenericDialogData; // Preselect attachments const attachments: string[] = []; + /* ToDo: Use after file upload is implemented if (comment && comment.attachments.length > 0) { comment.attachments.forEach(attachment => { // Load attachment to show }); - } + }*/ // Setup CommentDialogBody dialogData = { form: { @@ -78,7 +81,8 @@ export class CommentDialogService { { headerLabelKey: 'comment.edit.header', buttonKey: 'global.action.update', - accentColor: 'warning' + accentColor: 'warning', + additionalData: comment }, ]; } else { @@ -86,7 +90,8 @@ export class CommentDialogService { { headerLabelKey: 'comment.create.header', buttonKey: 'global.action.save', - accentColor: 'info' + accentColor: 'info', + additionalData: pentestInfo }, ]; } diff --git a/security-c4po-angular/src/shared/modules/export-report-dialog/export-report-dialog.component.ts b/security-c4po-angular/src/shared/modules/export-report-dialog/export-report-dialog.component.ts index f8570d8..92025dc 100644 --- a/security-c4po-angular/src/shared/modules/export-report-dialog/export-report-dialog.component.ts +++ b/security-c4po-angular/src/shared/modules/export-report-dialog/export-report-dialog.component.ts @@ -14,6 +14,7 @@ import {shareReplay, tap} from 'rxjs/operators'; import {downloadFile} from '@shared/functions/download-file.function'; import {Loading, LoadingState} from '@shared/models/loading.model'; import {HttpEvent, HttpEventType} from '@angular/common/http'; +import {DialogService} from '@shared/services/dialog-service/dialog.service'; @Component({ selector: 'app-export-report-dialog', @@ -28,7 +29,8 @@ export class ExportReportDialogComponent implements OnInit { private projectService: ProjectService, private reportingService: ReportingService, private readonly notificationService: NotificationService, - protected dialogRef: NbDialogRef + protected dialogRef: NbDialogRef, + private dialogService: DialogService ) { } @@ -105,6 +107,7 @@ export class ExportReportDialogComponent implements OnInit { error: error => { console.error(error); this.loading$.next(false); + this.onRequestFailed(reportFormat, reportLanguage); this.notificationService.showPopup('report.popup.generation.failed', PopupType.FAILURE); } }); @@ -136,6 +139,19 @@ export class ExportReportDialogComponent implements OnInit { isLoading(): Observable { return this.loading$.asObservable(); } + + onRequestFailed(reportFormat: string, reportLanguage: string): void { + this.dialogService.openRetryDialog({key: 'global.retry.dialog', data: null}).onClose + .pipe( + untilDestroyed(this) + ) + .subscribe((ref) => { + if (ref.retry) { + // ToDo: Send same request again + this.onClickExport(reportFormat, reportLanguage); + } + }); + } } export enum ExportFormatOptions { diff --git a/security-c4po-angular/src/shared/modules/finding-dialog/finding-dialog.component.ts b/security-c4po-angular/src/shared/modules/finding-dialog/finding-dialog.component.ts index 41f6f0b..378ceb5 100644 --- a/security-c4po-angular/src/shared/modules/finding-dialog/finding-dialog.component.ts +++ b/security-c4po-angular/src/shared/modules/finding-dialog/finding-dialog.component.ts @@ -3,9 +3,15 @@ import {FormBuilder, FormGroup} from '@angular/forms'; import {GenericDialogData, GenericFormFieldConfig} from '@shared/models/generic-dialog-data'; import {NB_DIALOG_CONFIG, NbDialogRef, NbTagComponent} from '@nebular/theme'; import deepEqual from 'deep-equal'; -import {UntilDestroy} from '@ngneat/until-destroy'; +import {UntilDestroy, untilDestroyed} from '@ngneat/until-destroy'; import {Severity} from '@shared/models/severity.enum'; import * as FA from '@fortawesome/free-solid-svg-icons'; +import {Finding, transformFindingToRequestBody} from '@shared/models/finding.model'; +import {FindingService} from '@shared/services/api/finding.service'; +import {NotificationService, PopupType} from '@shared/services/toaster-service/notification.service'; +import {DialogService} from '@shared/services/dialog-service/dialog.service'; +import {UpdatePentestFindings} from '@shared/stores/project-state/project-state.actions'; +import {Store} from '@ngxs/store'; @UntilDestroy() @Component({ @@ -38,7 +44,11 @@ export class FindingDialogComponent implements OnInit { constructor( @Inject(NB_DIALOG_CONFIG) private data: GenericDialogData, private fb: FormBuilder, - protected dialogRef: NbDialogRef + protected dialogRef: NbDialogRef, + private findingService: FindingService, + private readonly notificationService: NotificationService, + private dialogService: DialogService, + private store: Store ) { } @@ -64,18 +74,6 @@ export class FindingDialogComponent implements OnInit { return this.fb.group(config); } - onClickSave(value: any): void { - this.dialogRef.close({ - title: value.findingTitle, - severity: this.formArray[1].controlsConfig[0].value, - description: value.findingDescription, - impact: value.findingImpact, - affectedUrls: this.affectedUrls ? this.affectedUrls : [], - reproduction: value.findingReproduction, - mitigation: value.findingMitigation - }); - } - renderAffectedUrls(affectedUrls: string[]): void { affectedUrls.forEach(url => this.initialAffectedUrls.push(url)); affectedUrls.forEach(url => this.affectedUrls.push(url)); @@ -95,6 +93,16 @@ export class FindingDialogComponent implements OnInit { this.affectedUrls = this.affectedUrls.filter(t => t !== tagToRemove.text); } + onClickSave(value: any): void { + if (this.dialogData.options[0].headerLabelKey.includes('create')) { + // Save + this.saveFinding(value); + } else { + // Update + this.updateFinding(value); + } + } + onClickClose(): void { this.dialogRef.close(); } @@ -168,6 +176,77 @@ export class FindingDialogComponent implements OnInit { }); return findingData; } + + private saveFinding(value): void { + const dialogRes = { + title: value.findingTitle, + severity: this.formArray[1].controlsConfig[0].value, + description: value.findingDescription, + impact: value.findingImpact, + affectedUrls: this.affectedUrls ? this.affectedUrls : [], + reproduction: value.findingReproduction, + mitigation: value.findingMitigation + }; + + this.findingService.saveFinding( + this.dialogData.options[0].additionalData.id, + transformFindingToRequestBody(dialogRes) + ).pipe( + untilDestroyed(this) + ).subscribe({ + next: (newFinding: Finding) => { + this.store.dispatch(new UpdatePentestFindings(newFinding.id)); + this.dialogRef.close(); + this.notificationService.showPopup('finding.popup.save.success', PopupType.SUCCESS); + }, + error: err => { + console.error(err); + this.onRequestFailed(value); + this.notificationService.showPopup('finding.popup.save.failed', PopupType.FAILURE); + } + }); + } + + private updateFinding(value): void { + const dialogRes = { + title: value.findingTitle, + severity: this.formArray[1].controlsConfig[0].value, + description: value.findingDescription, + impact: value.findingImpact, + affectedUrls: this.affectedUrls ? this.affectedUrls : [], + reproduction: value.findingReproduction, + mitigation: value.findingMitigation + }; + + this.findingService.updateFinding( + this.dialogData.options[0].additionalData.id, + transformFindingToRequestBody(dialogRes) + ).pipe( + untilDestroyed(this) + ).subscribe({ + next: (newFinding: Finding) => { + this.dialogRef.close(); + this.notificationService.showPopup('finding.popup.update.success', PopupType.SUCCESS); + }, + error: err => { + console.error(err); + this.onRequestFailed(value); + this.notificationService.showPopup('finding.popup.update.failed', PopupType.FAILURE); + } + }); + } + + private onRequestFailed(retryParameter: any): void { + this.dialogService.openRetryDialog({key: 'global.retry.dialog', data: null}).onClose + .pipe( + untilDestroyed(this) + ) + .subscribe((ref) => { + if (ref.retry) { + this.onClickSave(retryParameter); + } + }); + } } interface SeverityText { diff --git a/security-c4po-angular/src/shared/modules/finding-dialog/service/finding-dialog.service.ts b/security-c4po-angular/src/shared/modules/finding-dialog/service/finding-dialog.service.ts index 55a3fbf..40e8130 100644 --- a/security-c4po-angular/src/shared/modules/finding-dialog/service/finding-dialog.service.ts +++ b/security-c4po-angular/src/shared/modules/finding-dialog/service/finding-dialog.service.ts @@ -7,6 +7,7 @@ import {Validators} from '@angular/forms'; import {FindingDialogComponent} from '@shared/modules/finding-dialog/finding-dialog.component'; import {Finding} from '@shared/models/finding.model'; import {Severity} from '@shared/models/severity.enum'; +import {Pentest} from '@shared/models/pentest.model'; @Injectable() export class FindingDialogService { @@ -31,7 +32,8 @@ export class FindingDialogService { public openFindingDialog(componentOrTemplateRef: ComponentType, finding?: Finding, - config?: Partial | string>>): Observable { + config?: Partial | string>>, + pentestInfo?: Pentest): Observable { let dialogOptions: Partial | string>>; let dialogData: GenericDialogData; let severity; @@ -141,7 +143,8 @@ export class FindingDialogService { { headerLabelKey: 'finding.edit.header', buttonKey: 'global.action.update', - accentColor: 'warning' + accentColor: 'warning', + additionalData: finding }, ]; } else { @@ -149,7 +152,8 @@ export class FindingDialogService { { headerLabelKey: 'finding.create.header', buttonKey: 'global.action.save', - accentColor: 'info' + accentColor: 'info', + additionalData: pentestInfo }, ]; } diff --git a/security-c4po-angular/src/shared/modules/project-dialog/project-dialog.component.ts b/security-c4po-angular/src/shared/modules/project-dialog/project-dialog.component.ts index 3e755ee..a017b61 100644 --- a/security-c4po-angular/src/shared/modules/project-dialog/project-dialog.component.ts +++ b/security-c4po-angular/src/shared/modules/project-dialog/project-dialog.component.ts @@ -3,7 +3,10 @@ import {NB_DIALOG_CONFIG, NbDialogRef} from '@nebular/theme'; import {FormBuilder, FormGroup} from '@angular/forms'; import {GenericFormFieldConfig, GenericDialogData} from '@shared/models/generic-dialog-data'; import deepEqual from 'deep-equal'; -import {UntilDestroy} from '@ngneat/until-destroy'; +import {UntilDestroy, untilDestroyed} from '@ngneat/until-destroy'; +import {DialogService} from '@shared/services/dialog-service/dialog.service'; +import {ProjectService} from '@shared/services/api/project.service'; +import {NotificationService, PopupType} from '@shared/services/toaster-service/notification.service'; @UntilDestroy() @Component({ @@ -21,6 +24,9 @@ export class ProjectDialogComponent implements OnInit { constructor( @Inject(NB_DIALOG_CONFIG) private data: GenericDialogData, private fb: FormBuilder, + private dialogService: DialogService, + private projectService: ProjectService, + private readonly notificationService: NotificationService, protected dialogRef: NbDialogRef ) { } @@ -40,12 +46,13 @@ export class ProjectDialogComponent implements OnInit { } onClickSave(value): void { - this.dialogRef.close({ - title: value.projectTitle, - client: value.projectClient, - tester: value.projectTester, - summary: value.projectSummary - }); + if (this.dialogData.options[0].headerLabelKey.includes('create')) { + // Save + this.saveProject(value); + } else { + // Update + this.updateProject(value); + } } onClickClose(): void { @@ -85,4 +92,64 @@ export class ProjectDialogComponent implements OnInit { }); return projectData; } + + private saveProject(value): void { + const dialogRes = { + title: value.projectTitle, + client: value.projectClient, + tester: value.projectTester, + summary: value.projectSummary + }; + this.projectService.saveProject(dialogRes).pipe( + untilDestroyed(this) + ).subscribe( + { + next: () => { + this.notificationService.showPopup('project.popup.save.success', PopupType.SUCCESS); + this.dialogRef.close(); + }, + error: err => { + console.error(err); + this.onRequestFailed(value); + this.notificationService.showPopup('project.popup.save.failed', PopupType.FAILURE); + } + } + ); + } + + private updateProject(value): void { + const dialogRes = { + title: value.projectTitle, + client: value.projectClient, + tester: value.projectTester, + summary: value.projectSummary + }; + this.projectService.updateProject(this.dialogData.options[0].additionalData.id, dialogRes).pipe( + untilDestroyed(this) + ).subscribe( + { + next: () => { + this.notificationService.showPopup('project.popup.update.success', PopupType.SUCCESS); + this.dialogRef.close(); + }, + error: err => { + console.error(err); + this.onRequestFailed(value); + this.notificationService.showPopup('project.popup.update.failed', PopupType.FAILURE); + } + } + ); + } + + private onRequestFailed(retryParameter: any): void { + this.dialogService.openRetryDialog({key: 'global.retry.dialog', data: null}).onClose + .pipe( + untilDestroyed(this) + ) + .subscribe((ref) => { + if (ref.retry) { + this.onClickSave(retryParameter); + } + }); + } } diff --git a/security-c4po-angular/src/shared/modules/project-dialog/service/project-dialog.service.ts b/security-c4po-angular/src/shared/modules/project-dialog/service/project-dialog.service.ts index 0f2ca66..c1948db 100644 --- a/security-c4po-angular/src/shared/modules/project-dialog/service/project-dialog.service.ts +++ b/security-c4po-angular/src/shared/modules/project-dialog/service/project-dialog.service.ts @@ -96,7 +96,8 @@ export class ProjectDialogService { { headerLabelKey: 'project.edit.header', buttonKey: 'global.action.update', - accentColor: 'warning' + accentColor: 'warning', + additionalData: project }, ]; } else { @@ -104,7 +105,8 @@ export class ProjectDialogService { { headerLabelKey: 'project.create.header', buttonKey: 'global.action.save', - accentColor: 'info' + accentColor: 'info', + additionalData: project }, ]; } diff --git a/security-c4po-angular/src/shared/modules/retry-dialog/retry-dialog.component.html b/security-c4po-angular/src/shared/modules/retry-dialog/retry-dialog.component.html new file mode 100644 index 0000000..9d3f9e1 --- /dev/null +++ b/security-c4po-angular/src/shared/modules/retry-dialog/retry-dialog.component.html @@ -0,0 +1,22 @@ + + + {{ 'global.retry.dialog.title' | translate }} + + + {{ 'global.retry.dialog.information' | translate }} + + + + + + diff --git a/security-c4po-angular/src/shared/modules/retry-dialog/retry-dialog.component.scss b/security-c4po-angular/src/shared/modules/retry-dialog/retry-dialog.component.scss new file mode 100644 index 0000000..a9941bf --- /dev/null +++ b/security-c4po-angular/src/shared/modules/retry-dialog/retry-dialog.component.scss @@ -0,0 +1,11 @@ +@import "../../../assets/@theme/styles/_dialog.scss"; + +* { +} + +.retry-dialog-button { + // margin: 6rem 2rem 6rem 0; + .dialog-button-icon { + padding-right: 0.5rem; + } +} diff --git a/security-c4po-angular/src/shared/modules/retry-dialog/retry-dialog.component.spec.ts b/security-c4po-angular/src/shared/modules/retry-dialog/retry-dialog.component.spec.ts new file mode 100644 index 0000000..b403620 --- /dev/null +++ b/security-c4po-angular/src/shared/modules/retry-dialog/retry-dialog.component.spec.ts @@ -0,0 +1,58 @@ +import {ComponentFixture, TestBed} from '@angular/core/testing'; + +import {RetryDialogComponent} from './retry-dialog.component'; +import {CommonModule} from '@angular/common'; +import {NbButtonModule, NbCardModule, NbDialogRef, NbLayoutModule, NbStatusService} from '@nebular/theme'; +import {FlexLayoutModule} from '@angular/flex-layout'; +import {TranslateLoader, TranslateModule} from '@ngx-translate/core'; +import {HttpLoaderFactory} from '../../../app/common-app.module'; +import {HttpClient, HttpClientModule} from '@angular/common/http'; +import {HttpClientTestingModule} from '@angular/common/http/testing'; +import {MockProvider} from 'ng-mocks'; +import {DialogService} from '@shared/services/dialog-service/dialog.service'; +import {DialogServiceMock} from '@shared/services/dialog-service/dialog.service.mock'; + +describe('RetryDialogComponent', () => { + let component: RetryDialogComponent; + let fixture: ComponentFixture; + + beforeEach(async () => { + await TestBed.configureTestingModule({ + declarations: [ + RetryDialogComponent + ], + imports: [ + CommonModule, + NbLayoutModule, + NbCardModule, + NbButtonModule, + FlexLayoutModule, + TranslateModule.forRoot({ + loader: { + provide: TranslateLoader, + useFactory: HttpLoaderFactory, + deps: [HttpClient] + } + }), + HttpClientModule, + HttpClientTestingModule + ], + providers: [ + MockProvider(NbStatusService), + {provide: DialogService, useClass: DialogServiceMock}, + {provide: NbDialogRef, useValue: {}} + ] + }) + .compileComponents(); + }); + + beforeEach(() => { + fixture = TestBed.createComponent(RetryDialogComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/security-c4po-angular/src/shared/modules/retry-dialog/retry-dialog.component.ts b/security-c4po-angular/src/shared/modules/retry-dialog/retry-dialog.component.ts new file mode 100644 index 0000000..f3be71f --- /dev/null +++ b/security-c4po-angular/src/shared/modules/retry-dialog/retry-dialog.component.ts @@ -0,0 +1,31 @@ +import {Component, Input} from '@angular/core'; +import {NbDialogRef} from '@nebular/theme'; +import * as FA from '@fortawesome/free-solid-svg-icons'; + +@Component({ + selector: 'app-retry-dialog', + templateUrl: './retry-dialog.component.html', + styleUrls: ['./retry-dialog.component.scss'] +}) +export class RetryDialogComponent { + /** + * @param data contains all relevant information the dialog needs + * @param data.title The translation key for the dialog title + * @param data.key The translation key for the shown message + * @param data.data The data that may be used in the message translation key + */ + @Input() data: any; + // HTML only + readonly fa = FA; + + constructor(protected dialogRef: NbDialogRef) { + } + + onClickRetry(): void { + this.dialogRef.close({retry: true}); + } + + onClickCancel(): void { + this.dialogRef.close({retry: false}); + } +} diff --git a/security-c4po-angular/src/shared/modules/retry-dialog/retry-dialog.module.ts b/security-c4po-angular/src/shared/modules/retry-dialog/retry-dialog.module.ts new file mode 100644 index 0000000..8470e73 --- /dev/null +++ b/security-c4po-angular/src/shared/modules/retry-dialog/retry-dialog.module.ts @@ -0,0 +1,29 @@ +import { NgModule } from '@angular/core'; +import { CommonModule } from '@angular/common'; +import {CommonAppModule} from '../../../app/common-app.module'; +import {NbButtonModule, NbCardModule, NbLayoutModule, NbSelectModule} from '@nebular/theme'; +import {FlexLayoutModule} from '@angular/flex-layout'; +import {TranslateModule} from '@ngx-translate/core'; +import {RetryDialogComponent} from '@shared/modules/retry-dialog/retry-dialog.component'; +import {FontAwesomeModule} from '@fortawesome/angular-fontawesome'; + +@NgModule({ + declarations: [ + RetryDialogComponent + ], + imports: [ + CommonModule, + CommonAppModule, + NbCardModule, + NbButtonModule, + FlexLayoutModule, + TranslateModule, + NbLayoutModule, + NbSelectModule, + FontAwesomeModule + ], + exports: [ + RetryDialogComponent + ] +}) +export class RetryDialogModule { } diff --git a/security-c4po-angular/src/shared/modules/retry-dialog/retry.function.ts b/security-c4po-angular/src/shared/modules/retry-dialog/retry.function.ts new file mode 100644 index 0000000..ea0933a --- /dev/null +++ b/security-c4po-angular/src/shared/modules/retry-dialog/retry.function.ts @@ -0,0 +1,20 @@ +import {untilDestroyed} from '@ngneat/until-destroy'; + +// ToDo: PoC for handling failed requests +function onRequestFailed(retryParameter: any): void { + this.dialogService.openRetryDialog({key: 'global.retry.dialog', data: null}).onClose + .pipe( + untilDestroyed(this) + ) + .subscribe((ref) => { + console.warn(ref); + if (ref.retry) { + // ToDo: Send same request again + console.warn('Retry'); + this.METHODTHATNEEDSTOBEEXECUTED(retryParameter); + } else { + // ToDo: Cancel action + console.warn('Cancel'); + } + }); +} diff --git a/security-c4po-angular/src/shared/services/dialog-service/dialog.service.mock.ts b/security-c4po-angular/src/shared/services/dialog-service/dialog.service.mock.ts index 3358af0..70c3d5e 100644 --- a/security-c4po-angular/src/shared/services/dialog-service/dialog.service.mock.ts +++ b/security-c4po-angular/src/shared/services/dialog-service/dialog.service.mock.ts @@ -20,6 +20,10 @@ export class DialogServiceMock implements Required { return null; } + openRetryDialog(message: DialogMessage): NbDialogRef { + return null; + } + openSecurityConfirmDialog(message: SecurityDialogMessage): NbDialogRef { return undefined; } diff --git a/security-c4po-angular/src/shared/services/dialog-service/dialog.service.ts b/security-c4po-angular/src/shared/services/dialog-service/dialog.service.ts index b3002a3..d1c3382 100644 --- a/security-c4po-angular/src/shared/services/dialog-service/dialog.service.ts +++ b/security-c4po-angular/src/shared/services/dialog-service/dialog.service.ts @@ -4,6 +4,7 @@ import {ComponentType} from '@angular/cdk/overlay'; import {DialogMessage, SecurityDialogMessage} from '@shared/services/dialog-service/dialog-message'; import {ConfirmDialogComponent} from '@shared/modules/confirm-dialog/confirm-dialog.component'; import {SecurityConfirmDialogComponent} from '@shared/modules/security-confirm-dialog/security-confirm-dialog.component'; +import {RetryDialogComponent} from '@shared/modules/retry-dialog/retry-dialog.component'; @Injectable({ providedIn: 'root' @@ -44,6 +45,21 @@ export class DialogService { }); } + /** + * @param message.key The translation key for the shown message + * @param message.data The data that may be used in the message translation key (Set it null if it's not required in the key) + * @param message.title The translation key for the dialog title + */ + openRetryDialog(message: DialogMessage): NbDialogRef { + return this.dialog.open(RetryDialogComponent, { + closeOnEsc: true, + hasScroll: false, + autoFocus: true, + closeOnBackdropClick: false, + context: {data: message} + }); + } + /** * @param message.key The translation key for the shown message * @param message.data The data that may be used in the message translation key (Set it null if it's not required in the key) diff --git a/security-c4po-api/src/main/kotlin/com/securityc4po/api/project/ProjectController.kt b/security-c4po-api/src/main/kotlin/com/securityc4po/api/project/ProjectController.kt index 48fe89b..12e9665 100644 --- a/security-c4po-api/src/main/kotlin/com/securityc4po/api/project/ProjectController.kt +++ b/security-c4po-api/src/main/kotlin/com/securityc4po/api/project/ProjectController.kt @@ -71,7 +71,7 @@ class ProjectController(private val projectService: ProjectService, private val @DeleteMapping("/{id}") fun deleteProject(@PathVariable(value = "id") id: String): Mono> { return this.projectService.deleteProject(id).flatMap { project: Project -> - // If the project has pentest the will be deleted as well as all associated findings & comments + // If the project has pentest they will be deleted as well as all associated findings & comments if (project.projectPentests.isNotEmpty()) { this.pentestDeletionService.deletePentestsAndAllAssociatedFindingsAndComments(project).collectList() .flatMap { prunedProject: Any ->