Compare commits

...

1 Commits

Author SHA1 Message Date
Marcel Haag 7114f129cf feat: As an user I want a retry dialog guard, in order to resend a failed request 2023-03-29 21:57:23 +02:00
22 changed files with 600 additions and 170 deletions

View File

@ -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,

View File

@ -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<boolean> {
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 {

View File

@ -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<FindingEntry>,
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<boolean> {
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 {

View File

@ -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<boolean> {
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);
}
});
}
}

View File

@ -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",

View File

@ -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",

View File

@ -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<CommentDialogComponent>
protected dialogRef: NbDialogRef<CommentDialogComponent>,
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);
}
});
}
}

View File

@ -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<any>,
findingIds: string[],
comment?: Comment,
config?: Partial<NbDialogConfig<Partial<any> | string>>): Observable<any> {
config?: Partial<NbDialogConfig<Partial<any> | string>>,
pentestInfo?: Pentest): Observable<any> {
let dialogOptions: Partial<NbDialogConfig<Partial<any> | 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
},
];
}

View File

@ -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<ExportReportDialogComponent>
protected dialogRef: NbDialogRef<ExportReportDialogComponent>,
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<boolean> {
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 {

View File

@ -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<FindingDialogComponent>
protected dialogRef: NbDialogRef<FindingDialogComponent>,
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 {

View File

@ -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<any>,
finding?: Finding,
config?: Partial<NbDialogConfig<Partial<any> | string>>): Observable<any> {
config?: Partial<NbDialogConfig<Partial<any> | string>>,
pentestInfo?: Pentest): Observable<any> {
let dialogOptions: Partial<NbDialogConfig<Partial<any> | 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
},
];
}

View File

@ -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<ProjectDialogComponent>
) {
}
@ -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);
}
});
}
}

View File

@ -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
},
];
}

View File

@ -0,0 +1,22 @@
<nb-card accent="danger">
<nb-card-header fxLayoutAlign="start center" class="dialog-header confirm">
{{ 'global.retry.dialog.title' | translate }}
</nb-card-header>
<nb-card-body class="dialog-body">
{{ 'global.retry.dialog.information' | translate }}
</nb-card-body>
<nb-card-footer fxLayout="row" fxLayoutGap="1.5rem" fxLayoutAlign="end end">
<button nbButton size="small"
class="retry-dialog-button"
status="danger"
(click)="onClickRetry()">
<fa-icon [icon]="fa.faRedoAlt" class="dialog-button-icon"></fa-icon>
{{ 'global.action.retry' | translate }}
</button>
<button nbButton size="small"
class="cancel-dialog-button"
(click)="onClickCancel()">
{{ 'global.action.cancel' | translate }}
</button>
</nb-card-footer>
</nb-card>

View File

@ -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;
}
}

View File

@ -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<RetryDialogComponent>;
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();
});
});

View File

@ -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<any>) {
}
onClickRetry(): void {
this.dialogRef.close({retry: true});
}
onClickCancel(): void {
this.dialogRef.close({retry: false});
}
}

View File

@ -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 { }

View File

@ -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');
}
});
}

View File

@ -20,6 +20,10 @@ export class DialogServiceMock implements Required<DialogService> {
return null;
}
openRetryDialog(message: DialogMessage): NbDialogRef<any> {
return null;
}
openSecurityConfirmDialog(message: SecurityDialogMessage): NbDialogRef<SecurityConfirmDialogComponent> {
return undefined;
}

View File

@ -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<RetryDialogComponent> {
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)

View File

@ -71,7 +71,7 @@ class ProjectController(private val projectService: ProjectService, private val
@DeleteMapping("/{id}")
fun deleteProject(@PathVariable(value = "id") id: String): Mono<ResponseEntity<ResponseBody>> {
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 ->