feat: As a user I want to see my profile in the header and log myself out manually

This commit is contained in:
Marcel Haag 2023-03-08 11:28:16 +01:00 committed by Cel
parent 3be43fa96e
commit a37e06f8ca
20 changed files with 186 additions and 93 deletions

View File

@ -12,7 +12,7 @@ import {
NbSelectModule,
NbThemeModule,
NbOverlayContainerAdapter,
NbDialogModule,
NbDialogModule, NbMenuModule,
} from '@nebular/theme';
import {TranslateLoader, TranslateModule} from '@ngx-translate/core';
import {HttpClient, HttpClientModule} from '@angular/common/http';
@ -58,6 +58,7 @@ import {FormsModule, ReactiveFormsModule} from '@angular/forms';
FontAwesomeModule,
BrowserAnimationsModule,
ThemeModule.forRoot(),
NbMenuModule.forRoot(),
NbSelectModule,
NgxsModule.forRoot([SessionState, ProjectState], {developmentMode: !environment.production}),
NgxsLoggerPluginModule.forRoot({developmentMode: !environment.production}),

View File

@ -7,7 +7,7 @@ import {FontAwesomeModule} from '@fortawesome/angular-fontawesome';
import {FlexLayoutModule, FlexModule} from '@angular/flex-layout';
import {MomentModule} from 'ngx-moment';
import {NotificationService} from '@shared/services/toaster-service/notification.service';
import {NbOverlayContainerAdapter, NbSpinnerModule, NbToastrModule} from '@nebular/theme';
import {NbMenuModule, NbOverlayContainerAdapter, NbSpinnerModule, NbToastrModule} from '@nebular/theme';
import {ThemeModule} from '@assets/@theme/theme.module';
import {LoadingSpinnerComponent} from '@shared/widgets/loading-spinner/loading-spinner.component';
@ -26,6 +26,7 @@ export function HttpLoaderFactory(http: HttpClient): TranslateHttpLoader {
FontAwesomeModule,
FlexLayoutModule,
ThemeModule.forRoot(),
NbMenuModule.forRoot(),
FlexModule,
HttpClientModule,
TranslateModule.forChild({

View File

@ -11,6 +11,7 @@
<div class="filler"></div>
<div fxLayoutGap="4rem">
<nb-actions size="medium">
<!--Theme Action-->
<nb-action class="toggle-theme">
<button nbButton
(click)="onClickGoToLink('https://owasp.org/www-project-web-security-testing-guide/v42/')">
@ -19,7 +20,6 @@
<span class="owasp-redirect-button">OWASP</span>
</button>
</nb-action>
<nb-action class="toggle-theme">
<button nbButton
(click)="onClickSwitchTheme()">
@ -30,8 +30,8 @@
</ng-template>
</button>
</nb-action>
</nb-actions>
</div>
<!--Language Action-->
<nb-action class="language-menu">
<div *ngIf="selectedLanguage && languages" class="languageContainer">
<nb-select selected="{{selectedLanguage}}" fullWidth>
<nb-option *ngFor="let language of languages"
@ -41,5 +41,18 @@
</nb-option>
</nb-select>
</div>
</nb-action>
<!--User Action-->
<nb-action class="user">
<!--<fa-icon [icon]="fa.faUser" class="user-icon">
</fa-icon>-->
<nb-user [nbContextMenu]="userMenu"
[picture]="FALLBACK_IMG"
name="{{user?.getValue()?.username}}"
title="Pentester">
</nb-user>
</nb-action>
</nb-actions>
</div>
</div>

View File

@ -3,17 +3,30 @@ import {ComponentFixture, TestBed} from '@angular/core/testing';
import {HeaderComponent} from './header.component';
import {CommonModule} from '@angular/common';
import {FontAwesomeTestingModule} from '@fortawesome/angular-fontawesome/testing';
import {NbActionsModule, NbSelectModule} from '@nebular/theme';
import {NbActionsModule, NbMenuModule, NbMenuService, NbSelectModule} from '@nebular/theme';
import {ThemeModule} from '@assets/@theme/theme.module';
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 {HttpClientTestingModule} from '@angular/common/http/testing';
import {NgxsModule, Store} from '@ngxs/store';
import {KeycloakService} from 'keycloak-angular';
import {SESSION_STATE_NAME, SessionState, SessionStateModel} from '@shared/stores/session-state/session-state';
import {User} from '@shared/models/user.model';
const DESIRED_STORE_STATE_SESSION: SessionStateModel = {
userAccount: {
...new User('ttt', 'test', 'user', 'default.user@test.de', 'en-US'),
id: '11c47c56-3bcd-45f1-a05b-c197dbd33110'
},
isAuthenticated: true
};
describe('HeaderComponent', () => {
let component: HeaderComponent;
let fixture: ComponentFixture<HeaderComponent>;
let store: Store;
beforeEach(async () => {
await TestBed.configureTestingModule({
@ -26,6 +39,7 @@ describe('HeaderComponent', () => {
NbSelectModule,
FontAwesomeTestingModule,
HttpClientTestingModule,
NbMenuModule,
ThemeModule.forRoot(),
TranslateModule.forRoot({
loader: {
@ -34,14 +48,23 @@ describe('HeaderComponent', () => {
deps: [HttpClient]
}
}),
RouterTestingModule.withRoutes([])
RouterTestingModule.withRoutes([]),
NgxsModule.forRoot([SessionState])
],
providers: [
NbMenuService,
KeycloakService
]
})
.compileComponents();
}).compileComponents();
});
beforeEach(() => {
fixture = TestBed.createComponent(HeaderComponent);
store = TestBed.inject(Store);
store.reset({
...store.snapshot(),
[SESSION_STATE_NAME]: DESIRED_STORE_STATE_SESSION
});
component = fixture.componentInstance;
fixture.detectChanges();
});

View File

@ -1,19 +1,29 @@
import {Component, OnDestroy, OnInit} from '@angular/core';
import {Component, OnInit} from '@angular/core';
import * as FA from '@fortawesome/free-solid-svg-icons';
import {NbThemeService} from '@nebular/theme';
import {NbMenuItem, NbMenuService, NbThemeService} from '@nebular/theme';
import {map} from 'rxjs/operators';
import {GlobalTitlesVariables} from '@shared/config/global-variables';
import {TranslateService} from '@ngx-translate/core';
import {UntilDestroy, untilDestroyed} from '@ngneat/until-destroy';
import {KeycloakService} from 'keycloak-angular';
import {Store} from '@ngxs/store';
import {ResetSession} from '@shared/stores/session-state/session-state.actions';
import {UserService} from '@shared/services/user-service/user.service';
import {User} from '@shared/models/user.model';
import {BehaviorSubject} from 'rxjs';
import {Route} from '@shared/models/route.enum';
import {environment} from '../../environments/environment';
import {Router} from '@angular/router';
@UntilDestroy()
@Component({
selector: 'app-header',
templateUrl: './header.component.html',
styleUrls: ['./header.component.scss']
})
@UntilDestroy()
export class HeaderComponent implements OnInit {
// HTML only
readonly fa = FA;
readonly SECURITYC4PO_TITLE: string = GlobalTitlesVariables.SECURITYC4PO_TITLE;
@ -21,16 +31,59 @@ export class HeaderComponent implements OnInit{
languages = ['en-US', 'de-DE'];
selectedLanguage = '';
constructor(private themeService: NbThemeService, private translateService: TranslateService) { }
// User Menu Properties
userPictureOnly = false;
user: BehaviorSubject<User> = new BehaviorSubject<User>(null);
userMenu: NbMenuItem[] = [{title: '', pathMatch: 'prefix'}];
readonly FALLBACK_IMG = 'assets/images/demo/anon-user-icon.png';
constructor(
private store: Store,
private router: Router,
private themeService: NbThemeService,
private translateService: TranslateService,
private menuService: NbMenuService,
private userService: UserService,
protected keycloakService: KeycloakService) {
}
ngOnInit(): void {
// Handle theme selection
this.themeService.onThemeChange()
.pipe(
map(({name}) => name),
untilDestroyed(this),
)
.subscribe(themeName => this.currentTheme = themeName);
).subscribe(themeName => this.currentTheme = themeName);
this.selectedLanguage = this.translateService.currentLang;
// Load user profile
this.userService.loadUserProfile().pipe(
untilDestroyed(this)
).subscribe({
next: (user: User) => {
this.user.next(user);
},
error: err => {
console.error(err);
}
});
// Handle user profile manu selection
this.menuService.onItemClick()
.pipe(
untilDestroyed(this)
)
.subscribe((menuBag) => {
if (menuBag.item.pathMatch === 'prefix') {
this.onClickLogOut();
}
});
// Setup stream to translate menu item
this.translateService.stream('global.action.logout')
.pipe(
untilDestroyed(this)
).subscribe((text: string) => {
this.userMenu[0].title = text;
});
}
// HTML only
@ -46,6 +99,22 @@ export class HeaderComponent implements OnInit{
}
}
onClickLogOut(): void {
// ToDo: Redirect user to Landing page from Issue #142 https://github.com/Marcel-Haag/security-c4po/issues/143
// ToDo: Fix Redirect URI in Keycloak Setting
this.keycloakService.logout(`http://auth-server/realms/${environment.keycloakclientId}/protocol/openid-connect/logout`).then(() => {
// Route user back to default page
this.router.navigate([Route.HOME]).then(() => {
// Reset User props from store
this.store.dispatch(new ResetSession());
}, err => {
console.error(err);
});
}, err => {
console.error(err);
});
}
onClickLanguage(language: string): void {
this.translateService.use(language);
}

View File

@ -1,7 +1,14 @@
import {NgModule} from '@angular/core';
import {CommonModule} from '@angular/common';
import {HeaderComponent} from './header.component';
import {NbActionsModule, NbButtonModule, NbCardModule, NbSelectModule} from '@nebular/theme';
import {
NbActionsModule,
NbButtonModule,
NbCardModule,
NbContextMenuModule, NbMenuModule,
NbSelectModule,
NbUserModule
} from '@nebular/theme';
import {FontAwesomeModule} from '@fortawesome/angular-fontawesome';
import {FlexLayoutModule} from '@angular/flex-layout';
import {TranslateModule} from '@ngx-translate/core';
@ -21,7 +28,11 @@ import {TranslateModule} from '@ngx-translate/core';
NbActionsModule,
FlexLayoutModule,
NbSelectModule,
TranslateModule
TranslateModule,
NbUserModule,
NbContextMenuModule
],
providers: [
]
})
export class HeaderModule {

View File

@ -81,7 +81,6 @@ describe('LoginComponent', () => {
...store.snapshot(),
[SESSION_STATE_NAME]: DESIRED_STORE_STATE_SESSION
});
fixture = TestBed.createComponent(LoginComponent);
component = fixture.componentInstance;
httpMock = TestBed.inject(HttpTestingController);

View File

@ -1,5 +1,5 @@
<div class="pentest-categories">
<nb-menu class="menu-style" tag="menu" [items]="categories"></nb-menu>
<nb-menu id="category-menu" class="menu-style" tag="menu" [items]="categories"></nb-menu>
</div>

View File

@ -54,10 +54,10 @@ export class ObjectiveCategoriesComponent implements OnInit, OnDestroy {
category.selected = false;
});
menuBag.item.selected = true;
if (this.selectedCategory) {
this.store.dispatch(new ChangeCategory(this.selectedCategory));
}
});
}
private initTranslation(): void {

View File

@ -37,7 +37,6 @@ import {CommentWidgetModule} from '@shared/widgets/comment-widget/comment-widget
CommonAppModule,
NbLayoutModule,
NbCardModule,
NbMenuModule.forRoot(),
NbButtonModule,
// nbTooltip crashes app right now if used in component,
// workaround: use title in html for now
@ -46,7 +45,6 @@ import {CommentWidgetModule} from '@shared/widgets/comment-widget/comment-widget
TranslateModule,
StatusTagModule,
RouterModule,
NbMenuModule,
FormsModule,
NbListModule,
FontAwesomeModule,
@ -57,7 +55,8 @@ import {CommentWidgetModule} from '@shared/widgets/comment-widget/comment-widget
ObjectiveOverviewRoutingModule,
// Table Widgets
FindigWidgetModule,
CommentWidgetModule
CommentWidgetModule,
NbMenuModule
],
exports: [
ObjectiveHeaderComponent,

View File

@ -25,7 +25,7 @@
status="success"
[disabled]="!pentestStatusChanged() || !pentestHasFindingsOrComments()"
title="{{ 'global.action.save' | translate }}"
(click)="onClickCompletePentestAndRouteBack()">
(click)="onClickCompletePentest()">
<fa-icon [icon]="fa.faSquare"></fa-icon>
<span class="action-element-text"> {{ 'global.action.complete' | translate }} </span>
</button>

View File

@ -95,7 +95,7 @@ export class PentestHeaderComponent implements OnInit, OnDestroy {
).finally();
}
onClickCompletePentestAndRouteBack(): void {
onClickCompletePentest(): void {
// Update existing Pentest
this.pentest$.next({...this.pentest$.getValue(), status: PentestStatus.COMPLETED, timeSpent: this.currentTimeSpent});
this.updatePentest();
@ -107,11 +107,11 @@ export class PentestHeaderComponent implements OnInit, OnDestroy {
next: (pentest: Pentest) => {
this.store.dispatch(new ChangePentest(pentest));
this.initialTimeSpent = pentest.timeSpent;
this.notificationService.showPopup('pentest.popup.update.success', PopupType.SUCCESS);
this.notificationService.showPopup('pentest.popup.complete.success', PopupType.SUCCESS);
},
error: err => {
console.log(err);
this.notificationService.showPopup('pentest.popup.update.failed', PopupType.FAILURE);
this.notificationService.showPopup('pentest.popup.complete.failed', PopupType.FAILURE);
}
});
}

View File

@ -1,6 +1,7 @@
{
"global": {
"action.login": "Einloggen",
"action.logout": "Ausloggen",
"action.retry": "Erneut Versuchen",
"action.info": "Info",
"action.save": "Speichern",
@ -233,6 +234,8 @@
"initial.save.failed": "Initialer Pentest konnte nicht aufgesetzt werden",
"save.success": "Pentest erfolgreich gespeichert",
"save.failed": "Pentest konnte nicht gespeichert werden",
"complete.success": "Pentest erfolgreich vervollständigt",
"complete.failed": "Pentest konnte nicht vervollständigt werden",
"update.success": "Pentest erfolgreich aktualisiert",
"update.failed": "Pentest konnte nicht aktualisiert werden",
"delete.success": "Pentest erfolgreich gelöscht",

View File

@ -1,6 +1,7 @@
{
"global": {
"action.login": "Login",
"action.logout": "Logout",
"action.retry": "Try again",
"action.info": "Info",
"action.confirm": "Confirm",
@ -233,6 +234,8 @@
"initial.save.failed": "Initial Pentest could not be setup",
"save.success": "Pentest saved successfully",
"save.failed": "Pentest could not be saved",
"complete.success": "Pentest completed successfully",
"complete.failed": "Pentest could not be completed",
"update.success": "Pentest updated successfully",
"update.failed": "Pentest could not be updated",
"delete.success": "Pentest deleted successfully",

Binary file not shown.

After

Width:  |  Height:  |  Size: 14 KiB

View File

@ -133,8 +133,7 @@ export const createSpyObj = (baseName, methodNames): { [key: string]: Mock<any>
export const mockComment: Comment = {
id: '11-22-33',
title: 'Test Finding',
description: 'Test Description',
relatedFindings: ['68c47c56-3bcd-45f1-a05b-c197dbd33224']
description: 'Test Description'
};
export const mockedCommentDialogData = {
@ -164,19 +163,6 @@ export const mockedCommentDialogData = {
errors: [
{errorCode: 'required', translationKey: 'comment.validationMessage.descriptionRequired'}
]
},
commentRelatedFindings: {
fieldName: 'commentRelatedFindings',
type: 'text',
labelKey: 'comment.relatedFindings.label',
placeholder: 'comment.relatedFindingsPlaceholder',
controlsConfig: [
{value: mockComment ? mockComment.relatedFindings : [], disabled: false},
[]
],
errors: [
{errorCode: 'required', translationKey: 'finding.validationMessage.relatedFindings'}
]
}
},
options: [

View File

@ -2,7 +2,7 @@ import {CommentDialogService} from '@shared/modules/comment-dialog/service/comme
import {ComponentType} from '@angular/cdk/overlay';
import {NbDialogConfig} from '@nebular/theme';
import {Observable, of} from 'rxjs';
import {Comment, RelatedFindingOption} from '@shared/models/comment.model';
import {Comment} from '@shared/models/comment.model';
export class CommentDialogServiceMock implements Required<CommentDialogService> {
@ -11,7 +11,6 @@ export class CommentDialogServiceMock implements Required<CommentDialogService>
openCommentDialog(
componentOrTemplateRef: ComponentType<any>,
findingIds: [],
relatedFindings: RelatedFindingOption[],
comment: Comment | undefined,
config: Partial<NbDialogConfig<Partial<any> | string>> | undefined): Observable<any> {
return of(undefined);

View File

@ -18,7 +18,7 @@ export class TimerDurationPipe implements PipeTransform {
let seconds: string | number = 0;
if (time) {
// tslint:disable-next-line:variable-name
const sec_num = parseInt(time, 10); // don't forget the second param
const sec_num = parseInt(time, 10);
hours = Math.floor(sec_num / 3600);
minutes = Math.floor((sec_num - (hours * 3600)) / 60);
seconds = sec_num - (hours * 3600) - (minutes * 60);

View File

@ -81,9 +81,7 @@ class CommentControllerDocumentationTest : BaseDocumentationIntTest() {
PayloadDocumentation.fieldWithPath("[].title").type(JsonFieldType.STRING)
.description("The title of the requested comment"),
PayloadDocumentation.fieldWithPath("[].description").type(JsonFieldType.STRING)
.description("The description number of the comment"),
PayloadDocumentation.fieldWithPath("[].relatedFindings").type(JsonFieldType.ARRAY)
.description("List of related Findings of the comment")
.description("The description number of the comment")
)
)
)
@ -93,7 +91,7 @@ class CommentControllerDocumentationTest : BaseDocumentationIntTest() {
id = "ab62d365-1b1d-4da1-89bc-5496616e220f",
title = "Found Bug",
description = "OTG-INFO-002 Bug",
relatedFindings = emptyList()
attachments = emptyList()
)
private fun getCommentsResponse() = listOf(
@ -133,9 +131,7 @@ class CommentControllerDocumentationTest : BaseDocumentationIntTest() {
PayloadDocumentation.fieldWithPath("title").type(JsonFieldType.STRING)
.description("The title of the requested comment"),
PayloadDocumentation.fieldWithPath("description").type(JsonFieldType.STRING)
.description("The description number of the comment"),
PayloadDocumentation.fieldWithPath("relatedFindings").type(JsonFieldType.ARRAY)
.description("List of related findings of the comment")
.description("The description number of the comment")
)
)
)
@ -145,7 +141,7 @@ class CommentControllerDocumentationTest : BaseDocumentationIntTest() {
id = "ab62d365-1b1d-4da1-89bc-5496616e220f",
title = "Found Bug",
description = "OTG-INFO-002 Bug",
relatedFindings = emptyList()
attachments = emptyList()
)
}
@ -182,9 +178,7 @@ class CommentControllerDocumentationTest : BaseDocumentationIntTest() {
PayloadDocumentation.fieldWithPath("title").type(JsonFieldType.STRING)
.description("The title of the comment"),
PayloadDocumentation.fieldWithPath("description").type(JsonFieldType.STRING)
.description("The description of the comment"),
PayloadDocumentation.fieldWithPath("relatedFindings").type(JsonFieldType.ARRAY)
.description("List of related findings of the comment")
.description("The description of the comment")
)
)
)
@ -192,8 +186,7 @@ class CommentControllerDocumentationTest : BaseDocumentationIntTest() {
private val commentBody = CommentRequestBody(
title = "Found another Bug",
description = "Another OTG-INFO-002 Bug",
relatedFindings = emptyList()
description = "Another OTG-INFO-002 Bug"
)
}
@ -230,9 +223,7 @@ class CommentControllerDocumentationTest : BaseDocumentationIntTest() {
PayloadDocumentation.fieldWithPath("title").type(JsonFieldType.STRING)
.description("The title of the requested comment"),
PayloadDocumentation.fieldWithPath("description").type(JsonFieldType.STRING)
.description("The description number of the comment"),
PayloadDocumentation.fieldWithPath("relatedFindings").type(JsonFieldType.ARRAY)
.description("List of related findings of the comment")
.description("The description number of the comment")
)
)
)
@ -240,8 +231,7 @@ class CommentControllerDocumentationTest : BaseDocumentationIntTest() {
private val commentBody = CommentRequestBody(
title = "Updated Comment",
description = "Updated Description",
relatedFindings = emptyList()
description = "Updated Description"
)
}
@ -330,7 +320,7 @@ class CommentControllerDocumentationTest : BaseDocumentationIntTest() {
id = "ab62d365-1b1d-4da1-89bc-5496616e220f",
title = "Found Bug",
description = "OTG-INFO-002 Bug",
relatedFindings = emptyList()
attachments = emptyList()
)
// persist test data in database
mongoTemplate.save(ProjectEntity(projectOne))

View File

@ -77,7 +77,7 @@ class CommentControllerIntTest : BaseIntTest() {
id = "ab62d365-1b1d-4da1-89bc-5496616e220f",
title = "Found Bug",
description = "OTG-INFO-002 Bug",
relatedFindings = emptyList()
attachments = emptyList()
)
private fun getComments() = listOf(
@ -103,7 +103,7 @@ class CommentControllerIntTest : BaseIntTest() {
id = "ab62d365-1b1d-4da1-89bc-5496616e220f",
title = "Found Bug",
description = "OTG-INFO-002 Bug",
relatedFindings = emptyList()
attachments = emptyList()
)
}
@ -122,13 +122,11 @@ class CommentControllerIntTest : BaseIntTest() {
.expectBody()
.jsonPath("$.title").isEqualTo("Found another Bug")
.jsonPath("$.description").isEqualTo("Another OTG-INFO-002 Bug")
.jsonPath("$.relatedFindings").isEmpty
}
private val commentBody = CommentRequestBody(
title = "Found another Bug",
description = "Another OTG-INFO-002 Bug",
relatedFindings = emptyList()
description = "Another OTG-INFO-002 Bug"
)
}
@ -147,13 +145,11 @@ class CommentControllerIntTest : BaseIntTest() {
.expectBody()
.jsonPath("$.title").isEqualTo("Updated Comment")
.jsonPath("$.description").isEqualTo("Updated Description")
.jsonPath("$.relatedFindings").isEmpty
}
private val commentBody = CommentRequestBody(
title = "Updated Comment",
description = "Updated Description",
relatedFindings = emptyList()
description = "Updated Description"
)
}
@ -221,7 +217,7 @@ class CommentControllerIntTest : BaseIntTest() {
id = "ab62d365-1b1d-4da1-89bc-5496616e220f",
title = "Found Bug",
description = "OTG-INFO-002 Bug",
relatedFindings = emptyList()
attachments = emptyList()
)
// persist test data in database
mongoTemplate.save(ProjectEntity(projectOne))