feat: As a user I want to add a summary to the project when editing on the main page & when inside a project

This commit is contained in:
Marcel Haag 2023-02-13 11:32:34 +01:00 committed by Cel
parent 3c3f005537
commit 88b3647295
25 changed files with 238 additions and 26 deletions

View File

@ -9,18 +9,28 @@
</button>
</div>
<h4>{{selectedProjectTitle$.getValue()}}</h4>
<h4>{{selectedProject$.getValue().title}}</h4>
<div class="export-button-container">
<div class="button-container">
<nb-actions size="medium">
<nb-action>
<button nbButton
status="button-outline-basic-text-color"
shape="round"
(click)="onClickEditPentestProject()">
<fa-icon [icon]="fa.faEdit"
class="element-icon fa-lg"></fa-icon>
<span class="element-text">{{ 'global.action.edit' | translate }}</span>
</button>
</nb-action>
<nb-action>
<button nbButton hero
status="info"
shape="round"
(click)="onClickExportPentest()">
<fa-icon [icon]="fa.faFileExport"
class="export-element-icon fa-lg"></fa-icon>
<span class="export-element-text">{{ 'global.action.export' | translate }}</span>
class="element-icon fa-lg"></fa-icon>
<span class="element-text">{{ 'global.action.export' | translate }}</span>
</button>
</nb-action>
</nb-actions>

View File

@ -6,16 +6,16 @@
}
}
.export-button-container {
.button-container {
display: flex;
align-content: flex-end;
// ToDo: Fix so that longer / shorter name won't change needed margin
margin-right: 2.25rem;
// margin-right: 2.25rem;
.export-element-icon {
.element-icon {
}
.export-element-text {
.element-text {
padding-left: 0.5rem;
font-size: 0.85rem;
}

View File

@ -8,14 +8,52 @@ import {TranslateLoader, TranslateModule} from '@ngx-translate/core';
import {HttpLoaderFactory} from '../../common-app.module';
import {HttpClient} from '@angular/common/http';
import {RouterTestingModule} from '@angular/router/testing';
import {NgxsModule} from '@ngxs/store';
import {ProjectState} from '@shared/stores/project-state/project-state';
import {NgxsModule, Store} from '@ngxs/store';
import {PROJECT_STATE_NAME, ProjectState, ProjectStateModel} from '@shared/stores/project-state/project-state';
import {FontAwesomeModule} from '@fortawesome/angular-fontawesome';
import {NbActionsModule, NbIconModule} from '@nebular/theme';
import {ProjectService} from '@shared/services/project.service';
import {ProjectServiceMock} from '@shared/services/project.service.mock';
import {ProjectDialogService} from '@shared/modules/project-dialog/service/project-dialog.service';
import {ProjectDialogServiceMock} from '@shared/modules/project-dialog/service/project-dialog.service.mock';
import {DialogService} from '@shared/services/dialog-service/dialog.service';
import {DialogServiceMock} from '@shared/services/dialog-service/dialog.service.mock';
import {NotificationService} from '@shared/services/notification.service';
import {NotificationServiceMock} from '@shared/services/notification.service.mock';
import {Category} from '@shared/models/category.model';
import {PentestStatus} from '@shared/models/pentest-status.model';
const DESIRED_PROJECT_STATE_SESSION: ProjectStateModel = {
selectedProject: {
id: '56c47c56-3bcd-45f1-a05b-c197dbd33111',
client: 'E Corp',
title: 'Some Mock API (v1.0) Scanning',
createdAt: new Date('2019-01-10T09:00:00'),
tester: 'Novatester',
summary: '',
testingProgress: 0,
createdBy: '11c47c56-3bcd-45f1-a05b-c197dbd33110'
},
// Manages Categories
disabledCategories: [],
selectedCategory: Category.INFORMATION_GATHERING,
// Manages Pentests of Category
disabledPentests: [],
selectedPentest: {
id: '56c47c56-3bcd-45f1-a05b-c197dbd33112',
category: Category.INFORMATION_GATHERING,
refNumber: 'OTF-001',
childEntries: [],
status: PentestStatus.NOT_STARTED,
findingIds: [],
commentIds: ['56c47c56-3bcd-45f1-a05b-c197dbd33112']
},
};
describe('ObjectiveHeaderComponent', () => {
let component: ObjectiveHeaderComponent;
let fixture: ComponentFixture<ObjectiveHeaderComponent>;
let store: Store;
beforeEach(async () => {
await TestBed.configureTestingModule({
@ -36,6 +74,12 @@ describe('ObjectiveHeaderComponent', () => {
}),
RouterTestingModule.withRoutes([]),
NgxsModule.forRoot([ProjectState])
],
providers: [
{provide: ProjectService, useValue: new ProjectServiceMock()},
{provide: ProjectDialogService, useClass: ProjectDialogServiceMock},
{provide: DialogService, useClass: DialogServiceMock},
{provide: NotificationService, useValue: new NotificationServiceMock()}
]
})
.compileComponents();
@ -43,6 +87,11 @@ describe('ObjectiveHeaderComponent', () => {
beforeEach(() => {
fixture = TestBed.createComponent(ObjectiveHeaderComponent);
store = TestBed.inject(Store);
store.reset({
...store.snapshot(),
[PROJECT_STATE_NAME]: DESIRED_PROJECT_STATE_SESSION
});
component = fixture.componentInstance;
fixture.detectChanges();
});

View File

@ -6,7 +6,14 @@ import {Router} from '@angular/router';
import {PROJECT_STATE_NAME, ProjectState} from '@shared/stores/project-state/project-state';
import {UntilDestroy, untilDestroyed} from '@ngneat/until-destroy';
import {BehaviorSubject} from 'rxjs';
import {Project} from '@shared/models/project.model';
import {Project, ProjectDialogBody} from '@shared/models/project.model';
import {ProjectDialogComponent} from '@shared/modules/project-dialog/project-dialog.component';
import {filter, mergeMap} from 'rxjs/operators';
import {NotificationService, PopupType} from '@shared/services/notification.service';
import {ProjectService} from '@shared/services/project.service';
import {DialogService} from '@shared/services/dialog-service/dialog.service';
import {ProjectDialogService} from '@shared/modules/project-dialog/service/project-dialog.service';
import {InitProjectState} from '@shared/stores/project-state/project-state.actions';
@UntilDestroy()
@Component({
@ -17,9 +24,13 @@ import {Project} from '@shared/models/project.model';
export class ObjectiveHeaderComponent implements OnInit {
readonly fa = FA;
selectedProjectTitle$: BehaviorSubject<string> = new BehaviorSubject<string>('');
selectedProject$: BehaviorSubject<Project> = new BehaviorSubject<Project>(null);
constructor(private store: Store,
private readonly notificationService: NotificationService,
private projectService: ProjectService,
private dialogService: DialogService,
private projectDialogService: ProjectDialogService,
private readonly router: Router) {
}
@ -28,7 +39,7 @@ export class ObjectiveHeaderComponent implements OnInit {
untilDestroyed(this)
).subscribe({
next: (selectedProject: Project) => {
this.selectedProjectTitle$.next(selectedProject?.title);
this.selectedProject$.next(selectedProject);
},
error: err => {
console.error(err);
@ -46,6 +57,36 @@ export class ObjectiveHeaderComponent implements OnInit {
).finally();
}
onClickEditPentestProject(): void {
this.projectDialogService.openProjectDialog(
ProjectDialogComponent,
this.selectedProject$.getValue(),
{
closeOnEsc: false,
hasScroll: false,
autoFocus: true,
closeOnBackdropClick: false
}
).pipe(
filter(value => !!value),
mergeMap((value: ProjectDialogBody) => this.projectService.updateProject(this.selectedProject$.getValue().id, value)),
untilDestroyed(this)
).subscribe({
next: (project: Project) => {
this.store.dispatch(new InitProjectState(
project,
[],
[]
)).pipe(untilDestroyed(this)).subscribe();
this.notificationService.showPopup('project.popup.update.success', PopupType.SUCCESS);
},
error: error => {
console.error(error);
this.notificationService.showPopup('project.popup.update.failed', PopupType.FAILURE);
}
});
}
onClickExportPentest(): void {
// tslint:disable-next-line:no-console
console.info('To be implemented..');

View File

@ -30,6 +30,7 @@ const DESIRED_PROJECT_STATE_SESSION: ProjectStateModel = {
title: 'Some Mock API (v1.0) Scanning',
createdAt: new Date('2019-01-10T09:00:00'),
tester: 'Novatester',
summary: '',
testingProgress: 0,
createdBy: '11c47c56-3bcd-45f1-a05b-c197dbd33110'
},

View File

@ -21,6 +21,7 @@ const DESIRED_PROJECT_STATE_SESSION: ProjectStateModel = {
title: 'Some Mock API (v1.0) Scanning',
createdAt: new Date('2019-01-10T09:00:00'),
tester: 'Novatester',
summary: '',
testingProgress: 0,
createdBy: '11c47c56-3bcd-45f1-a05b-c197dbd33110'
},

View File

@ -30,6 +30,7 @@ const DESIRED_PROJECT_STATE_SESSION: ProjectStateModel = {
title: 'Some Mock API (v1.0) Scanning',
createdAt: new Date('2019-01-10T09:00:00'),
tester: 'Novatester',
summary: '',
testingProgress: 0,
createdBy: '11c47c56-3bcd-45f1-a05b-c197dbd33110'
},

View File

@ -21,6 +21,7 @@ const DESIRED_PROJECT_STATE_SESSION: ProjectStateModel = {
title: 'Some Mock API (v1.0) Scanning',
createdAt: new Date('2019-01-10T09:00:00'),
tester: 'Novatester',
summary: '',
testingProgress: 0,
createdBy: '11c47c56-3bcd-45f1-a05b-c197dbd33110'
},

View File

@ -21,6 +21,7 @@ const DESIRED_PROJECT_STATE_SESSION: ProjectStateModel = {
title: 'Some Mock API (v1.0) Scanning',
createdAt: new Date('2019-01-10T09:00:00'),
tester: 'Novatester',
summary: '',
testingProgress: 0,
createdBy: '11c47c56-3bcd-45f1-a05b-c197dbd33110'
},

View File

@ -67,7 +67,7 @@ describe('ProjectOverviewComponent', () => {
{provide: ProjectService, useValue: new ProjectServiceMock()},
{provide: ProjectDialogService, useClass: ProjectDialogServiceMock},
{provide: DialogService, useClass: DialogServiceMock},
{provide: NotificationService, useValue: new NotificationServiceMock()}
{provide: NotificationService, useClass: NotificationServiceMock}
]
})
.compileComponents();

View File

@ -7,16 +7,55 @@ import {TranslateLoader, TranslateModule} from '@ngx-translate/core';
import {HttpLoaderFactory} from '../../common-app.module';
import {HttpClient, HttpClientModule} from '@angular/common/http';
import {RouterTestingModule} from '@angular/router/testing';
import {NgxsModule} from '@ngxs/store';
import {NgxsModule, Store} from '@ngxs/store';
import {SessionState} from '@shared/stores/session-state/session-state';
import {HttpClientTestingModule} from '@angular/common/http/testing';
import {NbCardModule, NbLayoutModule} from '@nebular/theme';
import {KeycloakService} from 'keycloak-angular';
import {ObjectiveOverviewModule} from '../../objective-overview';
import {NotificationService} from '@shared/services/notification.service';
import {NotificationServiceMock} from '@shared/services/notification.service.mock';
import {DialogService} from '@shared/services/dialog-service/dialog.service';
import {DialogServiceMock} from '@shared/services/dialog-service/dialog.service.mock';
import {ProjectService} from '@shared/services/project.service';
import {ProjectServiceMock} from '@shared/services/project.service.mock';
import {ProjectDialogService} from '@shared/modules/project-dialog/service/project-dialog.service';
import {ProjectDialogServiceMock} from '@shared/modules/project-dialog/service/project-dialog.service.mock';
import {PROJECT_STATE_NAME, ProjectState, ProjectStateModel} from '@shared/stores/project-state/project-state';
import {Category} from '@shared/models/category.model';
import {PentestStatus} from '@shared/models/pentest-status.model';
const DESIRED_PROJECT_STATE_SESSION: ProjectStateModel = {
selectedProject: {
id: '56c47c56-3bcd-45f1-a05b-c197dbd33111',
client: 'E Corp',
title: 'Some Mock API (v1.0) Scanning',
createdAt: new Date('2019-01-10T09:00:00'),
tester: 'Novatester',
summary: '',
testingProgress: 0,
createdBy: '11c47c56-3bcd-45f1-a05b-c197dbd33110'
},
// Manages Categories
disabledCategories: [],
selectedCategory: Category.INFORMATION_GATHERING,
// Manages Pentests of Category
disabledPentests: [],
selectedPentest: {
id: '56c47c56-3bcd-45f1-a05b-c197dbd33112',
category: Category.INFORMATION_GATHERING,
refNumber: 'OTF-001',
childEntries: [],
status: PentestStatus.NOT_STARTED,
findingIds: [],
commentIds: ['56c47c56-3bcd-45f1-a05b-c197dbd33112']
},
};
describe('ProjectComponent', () => {
let component: ProjectComponent;
let fixture: ComponentFixture<ProjectComponent>;
let store: Store;
beforeEach(async () => {
await TestBed.configureTestingModule({
@ -37,12 +76,16 @@ describe('ProjectComponent', () => {
}
}),
RouterTestingModule.withRoutes([]),
NgxsModule.forRoot([SessionState]),
NgxsModule.forRoot([ProjectState]),
HttpClientModule,
HttpClientTestingModule
],
providers: [
KeycloakService
KeycloakService,
{provide: ProjectService, useValue: new ProjectServiceMock()},
{provide: ProjectDialogService, useClass: ProjectDialogServiceMock},
{provide: DialogService, useClass: DialogServiceMock},
{provide: NotificationService, useClass: NotificationServiceMock}
]
})
.compileComponents();
@ -50,6 +93,11 @@ describe('ProjectComponent', () => {
beforeEach(() => {
fixture = TestBed.createComponent(ProjectComponent);
store = TestBed.inject(Store);
store.reset({
...store.snapshot(),
[PROJECT_STATE_NAME]: DESIRED_PROJECT_STATE_SESSION
});
component = fixture.componentInstance;
fixture.detectChanges();
});

View File

@ -9,6 +9,7 @@
"action.return": "Zurück",
"action.exit": "Beenden",
"action.update": "Speichern",
"action.edit": "Editieren",
"action.export": "Exportieren",
"action.reset": "Zurücksetzen",
"action.yes": "Ja",
@ -46,9 +47,12 @@
"title.label": "Projekt Titel",
"client.label": "Name des Auftraggebers",
"tester.label": "Name des Pentester",
"summary.label": "Zusammenfassung",
"summary.placeholder": "Sollte eine Zusammenfassung, einen Ansatz, einen Umfang und eine Bewertungsübersicht sowie allgemeine Empfehlungen enthalten",
"title": "Titel",
"client": "Klient",
"tester": "Tester",
"summary": "Zusammenfassung",
"createdAt": "Erstellt am",
"overview": {
"add.project": "Projekt hinzufügen",
@ -69,7 +73,8 @@
"validationMessage": {
"titleRequired": "Titel ist erforderlich.",
"clientRequired": "Klient ist erforderlich.",
"testerRequired": "Tester ist erforderlich."
"testerRequired": "Tester ist erforderlich.",
"summaryRequired": "Zusammenfassung ist erforderlich."
},
"popup": {
"not.found": "Keine Projekte gefunden",

View File

@ -9,6 +9,7 @@
"action.return": "Return",
"action.exit": "Exit",
"action.update": "Update",
"action.edit": "Edit",
"action.export": "Export",
"action.reset": "Reset",
"action.yes": "Yes",
@ -46,9 +47,12 @@
"title.label": "Project Title",
"client.label": "Name of Client",
"tester.label": "Name of Pentester",
"summary.label": "Summary",
"summary.placeholder": "Should include Executive Summary, Approach, Scope and Assessment Overview as well as General Recommendations",
"title": "Title",
"client": "Client",
"tester": "Tester",
"summary": "Summary",
"createdAt": "Created at",
"overview": {
"add.project": "Add Project",
@ -69,7 +73,8 @@
"validationMessage": {
"titleRequired": "Title is required.",
"clientRequired": "Client is required.",
"testerRequired": "Tester is required."
"testerRequired": "Tester is required.",
"summaryRequired": "Summary is required."
},
"popup": {
"not.found": "No projects found",

View File

@ -40,6 +40,7 @@ const DESIRED_PROJECT_STATE_SESSION: ProjectStateModel = {
title: 'Some Mock API (v1.0) Scanning',
createdAt: new Date('2019-01-10T09:00:00'),
tester: 'Novatester',
summary: '',
testingProgress: 0,
createdBy: '11c47c56-3bcd-45f1-a05b-c197dbd33110'
},

View File

@ -38,6 +38,7 @@ const DESIRED_PROJECT_STATE_SESSION: ProjectStateModel = {
title: 'Some Mock API (v1.0) Scanning',
createdAt: new Date('2019-01-10T09:00:00'),
tester: 'Novatester',
summary: '',
testingProgress: 0,
createdBy: '11c47c56-3bcd-45f1-a05b-c197dbd33110'
},

View File

@ -28,6 +28,27 @@
</span>
</ng-template>
</nb-form-field>
<!-- Textarea styles -->
<nb-form-field *ngSwitchCase="'formText'" class="project-form-field">
<label for="{{fieldConfig.fieldName}}" class="label">
{{fieldConfig.labelKey | translate}}
</label>
<textarea formControlName="{{fieldConfig.fieldName}}"
type="formText" required fullWidth
id="{{fieldConfig.fieldName}}" nbInput
class="form-field form-textarea"
[status]="projectFormGroup.get(fieldConfig.fieldName).dirty ? (projectFormGroup.get(fieldConfig.fieldName).invalid ? 'danger' : 'basic') : 'basic'"
placeholder="{{fieldConfig.placeholder | translate}}">
</textarea>
<!-- FIXME: when the bug (https://github.com/angular/components/issues/7739) is fixed -->
<ng-template ngFor let-error [ngForOf]="fieldConfig.errors"
*ngIf="projectFormGroup.get(fieldConfig.fieldName).dirty">
<span class="error-text"
*ngIf="projectFormGroup.get(fieldConfig.fieldName)?.hasError(error.errorCode) && error.errorCode === 'required'">
{{error.translationKey | translate}}
</span>
</ng-template>
</nb-form-field>
</ng-container>
</ng-template>
</form>

View File

@ -2,8 +2,8 @@
@import '../../../assets/@theme/styles/themes';
.project-dialog {
width: 25.25rem;
height: 35rem;
width: 40rem !important;
height: 42.25rem;
.project-dialog-header {
height: 8vh;
@ -21,10 +21,17 @@
}
.form-field {
width: 18rem;
width: 26.75rem;
// width: 30rem !important;
margin-bottom: 0.5rem;
}
.form-textarea {
width: 26.75rem !important;
// width: 30rem !important;
height: 8rem;
}
.error-text {
float: left;
color: nb-theme(color-danger-default);

View File

@ -91,6 +91,7 @@ export const mockProject: Project = {
title: 'Test Project',
client: 'Testclient',
tester: 'Testpentester',
summary: '',
createdAt: new Date(),
testingProgress: 0,
createdBy: 'UID-11-12-13'

View File

@ -43,7 +43,8 @@ export class ProjectDialogComponent implements OnInit {
this.dialogRef.close({
title: value.projectTitle,
client: value.projectClient,
tester: value.projectTester
tester: value.projectTester,
summary: value.projectSummary
});
}

View File

@ -74,6 +74,19 @@ export class ProjectDialogService {
errors: [
{errorCode: 'required', translationKey: 'project.validationMessage.testerRequired'}
]
},
projectSummary: {
fieldName: 'projectSummary',
type: 'formText',
labelKey: 'project.summary.label',
placeholder: 'project.summary.placeholder',
controlsConfig: [
{value: project ? project.summary : '', disabled: !project},
[project ? Validators.required : []]
],
errors: [
{errorCode: 'required', translationKey: 'project.validationMessage.summaryRequired'}
]
}
},
options: []

View File

@ -41,6 +41,7 @@ describe('ProjectService', () => {
title: 'Some Mock API (v1.0) Scanning',
createdAt: dummyDate,
tester: 'Novatester',
summary: '',
testingProgress: 0,
createdBy: '11c47c56-3bcd-45f1-a05b-c197dbd33110'
};
@ -90,6 +91,7 @@ describe('ProjectService', () => {
title: 'Some Mock API (v1.0) Scanning',
createdAt: dummyDate,
tester: 'Novatester',
summary: '',
testingProgress: 0,
createdBy: '11c47c56-3bcd-45f1-a05b-c197dbd33110'
};

View File

@ -41,11 +41,13 @@ class ReportController(private val apiService: APIService, private val reportSer
jacksonObjectMapper().readValue<ProjectReport>(jsonProjectReportString)
return this.reportService.createReport(jsonProjectReportCollection, "pdf").map { reportClassLoaderFilePatch ->
val reportRessourceStream = ReportController::class.java.getResourceAsStream(reportClassLoaderFilePatch)
// Todo: Fix Error with IOUtils.toByteArray(reportRessourceStream) on first start of application
val response = IOUtils.toByteArray(reportRessourceStream)
this.reportService.cleanUpFiles()
ResponseEntity.ok().body(response)
}.switchIfEmpty {
Mono.just(notFound().build<ByteArray>())
}.doOnSuccess {
this.reportService.cleanUpFiles()
}
}

View File

@ -163,7 +163,7 @@
<textElement>
<font size="12"/>
</textElement>
<textFieldExpression><![CDATA[$F{summary}]]></textFieldExpression>
<textFieldExpression><![CDATA[(($F{summary}.length() == 0) ? "" + $F{client} +" contracted " + $F{tester} + " to perform a Penetration Test to identify security weaknesses, determine the impact to " + $F{client} +", document all findings in a clear and repeatable manner, and provide remediation recommendations." : $F{summary})]]></textFieldExpression>
</textField>
<staticText>
<reportElement x="0" y="10" width="380" height="20" forecolor="#232B44" uuid="b508eb27-8cf7-40f3-86e8-6b7c9328d919"/>

Binary file not shown.

Before

Width:  |  Height:  |  Size: 231 KiB

After

Width:  |  Height:  |  Size: 220 KiB

File diff suppressed because one or more lines are too long