feat: As a tester I want to edit my profile
This commit is contained in:
parent
e0e23f7383
commit
b4bf6de8f8
|
@ -28,9 +28,12 @@
|
||||||
"src/assets"
|
"src/assets"
|
||||||
],
|
],
|
||||||
"styles": [
|
"styles": [
|
||||||
"src/assets/@theme/styles/styles.scss"
|
"src/assets/@theme/styles/styles.scss",
|
||||||
|
"node_modules/@fortawesome/fontawesome-free/css/all.css"
|
||||||
|
],
|
||||||
|
"scripts": [
|
||||||
|
"node_modules/@fortawesome/fontawesome-free/js/all.js"
|
||||||
],
|
],
|
||||||
"scripts": [],
|
|
||||||
"allowedCommonJsDependencies": [
|
"allowedCommonJsDependencies": [
|
||||||
"buffer",
|
"buffer",
|
||||||
"crypto-js/hmac-sha256",
|
"crypto-js/hmac-sha256",
|
||||||
|
|
File diff suppressed because it is too large
Load Diff
|
@ -42,6 +42,7 @@
|
||||||
"keycloak-js": "^13.0.1",
|
"keycloak-js": "^13.0.1",
|
||||||
"moment": "^2.29.1",
|
"moment": "^2.29.1",
|
||||||
"moment-timezone": "latest",
|
"moment-timezone": "latest",
|
||||||
|
"font-awesome": "^4.7.0",
|
||||||
"ng-mocks": "^13.4.2",
|
"ng-mocks": "^13.4.2",
|
||||||
"ngx-moment": "^5.0.0",
|
"ngx-moment": "^5.0.0",
|
||||||
"ngx-take-until-destroy": "^5.4.0",
|
"ngx-take-until-destroy": "^5.4.0",
|
||||||
|
@ -58,12 +59,12 @@
|
||||||
"@angular/cli": "^12.2.16",
|
"@angular/cli": "^12.2.16",
|
||||||
"@angular/compiler-cli": "~12.2.16",
|
"@angular/compiler-cli": "~12.2.16",
|
||||||
"@babel/preset-typescript": "^7.18.6",
|
"@babel/preset-typescript": "^7.18.6",
|
||||||
|
"@briebug/jest-schematic": "^3.0.0",
|
||||||
|
"@fortawesome/fontawesome-free": "^6.4.0",
|
||||||
"@schematics/angular": "^10.2.4",
|
"@schematics/angular": "^10.2.4",
|
||||||
"@types/jest": "28.1.1",
|
"@types/jest": "28.1.1",
|
||||||
"@types/node": "^12.20.47",
|
"@types/node": "^12.20.47",
|
||||||
"@briebug/jest-schematic": "^3.0.0",
|
|
||||||
"codelyzer": "^6.0.2",
|
"codelyzer": "^6.0.2",
|
||||||
"font-awesome": "^4.7.0",
|
|
||||||
"jest": "28.1.1",
|
"jest": "28.1.1",
|
||||||
"protractor": "~7.0.0",
|
"protractor": "~7.0.0",
|
||||||
"ts-node": "~8.3.0",
|
"ts-node": "~8.3.0",
|
||||||
|
|
|
@ -9,6 +9,8 @@ import {SessionState, SessionStateModel} from '@shared/stores/session-state/sess
|
||||||
import {UntilDestroy, untilDestroyed} from '@ngneat/until-destroy';
|
import {UntilDestroy, untilDestroyed} from '@ngneat/until-destroy';
|
||||||
import {isNotNullOrUndefined} from 'codelyzer/util/isNotNullOrUndefined';
|
import {isNotNullOrUndefined} from 'codelyzer/util/isNotNullOrUndefined';
|
||||||
import {filter} from 'rxjs/operators';
|
import {filter} from 'rxjs/operators';
|
||||||
|
import {NbIconLibraries} from '@nebular/theme';
|
||||||
|
import {FaIconLibrary} from '@fortawesome/angular-fontawesome';
|
||||||
|
|
||||||
@UntilDestroy()
|
@UntilDestroy()
|
||||||
@Component({
|
@Component({
|
||||||
|
@ -24,6 +26,8 @@ export class AppComponent implements OnInit, OnDestroy {
|
||||||
|
|
||||||
constructor(private translateService: TranslateService,
|
constructor(private translateService: TranslateService,
|
||||||
private store: Store,
|
private store: Store,
|
||||||
|
private iconLibraries: FaIconLibrary,
|
||||||
|
private nebularIconLibraries: NbIconLibraries,
|
||||||
@Inject(LOCALE_ID) private localeId: string) {
|
@Inject(LOCALE_ID) private localeId: string) {
|
||||||
this.initApp();
|
this.initApp();
|
||||||
}
|
}
|
||||||
|
@ -44,10 +48,14 @@ export class AppComponent implements OnInit, OnDestroy {
|
||||||
initApp(): void {
|
initApp(): void {
|
||||||
// for global language
|
// for global language
|
||||||
this.translateService.use(this.localeId);
|
this.translateService.use(this.localeId);
|
||||||
|
|
||||||
// for number, date and time
|
// for number, date and time
|
||||||
registerLocaleData(localeDe, 'de-DE');
|
registerLocaleData(localeDe, 'de-DE');
|
||||||
|
// for font-awesome icons
|
||||||
|
this.nebularIconLibraries.registerFontPack('fas', { packClass: 'fas', iconClassPrefix: 'fa' });
|
||||||
|
this.nebularIconLibraries.registerFontPack('far', { packClass: 'far', iconClassPrefix: 'fa' });
|
||||||
|
this.nebularIconLibraries.registerFontPack('fab', { packClass: 'fab', iconClassPrefix: 'fa' });
|
||||||
|
this.nebularIconLibraries.setDefaultPack('far');
|
||||||
|
// for country codes
|
||||||
this.setupCountryCode();
|
this.setupCountryCode();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -12,15 +12,12 @@ import {
|
||||||
NbSelectModule,
|
NbSelectModule,
|
||||||
NbThemeModule,
|
NbThemeModule,
|
||||||
NbOverlayContainerAdapter,
|
NbOverlayContainerAdapter,
|
||||||
NbDialogModule, NbMenuModule,
|
NbDialogModule, NbMenuModule, NbIconLibraries,
|
||||||
} from '@nebular/theme';
|
} from '@nebular/theme';
|
||||||
import {TranslateLoader, TranslateModule} from '@ngx-translate/core';
|
import {TranslateLoader, TranslateModule} from '@ngx-translate/core';
|
||||||
import {HttpClient, HttpClientModule} from '@angular/common/http';
|
import {HttpClient, HttpClientModule} from '@angular/common/http';
|
||||||
import {HttpLoaderFactory} from './common-app.module';
|
import {HttpLoaderFactory} from './common-app.module';
|
||||||
import {RouterModule} from '@angular/router';
|
import {RouterModule} from '@angular/router';
|
||||||
import {FaConfig, FaIconLibrary, FontAwesomeModule} from '@fortawesome/angular-fontawesome';
|
|
||||||
import {fas} from '@fortawesome/free-solid-svg-icons';
|
|
||||||
import {far} from '@fortawesome/free-regular-svg-icons';
|
|
||||||
import {NgxsModule} from '@ngxs/store';
|
import {NgxsModule} from '@ngxs/store';
|
||||||
import {SessionState} from '@shared/stores/session-state/session-state';
|
import {SessionState} from '@shared/stores/session-state/session-state';
|
||||||
import {environment} from '../environments/environment';
|
import {environment} from '../environments/environment';
|
||||||
|
@ -37,6 +34,9 @@ import {CustomOverlayContainer} from '@shared/modules/custom-overlay-container.c
|
||||||
import {DialogService} from '@shared/services/dialog-service/dialog.service';
|
import {DialogService} from '@shared/services/dialog-service/dialog.service';
|
||||||
import {FormsModule, ReactiveFormsModule} from '@angular/forms';
|
import {FormsModule, ReactiveFormsModule} from '@angular/forms';
|
||||||
import {RetryDialogModule} from '@shared/modules/retry-dialog/retry-dialog.module';
|
import {RetryDialogModule} from '@shared/modules/retry-dialog/retry-dialog.module';
|
||||||
|
import {FaConfig, FaIconLibrary, FontAwesomeModule} from '@fortawesome/angular-fontawesome';
|
||||||
|
import {fas} from '@fortawesome/free-solid-svg-icons';
|
||||||
|
import {far} from '@fortawesome/free-regular-svg-icons';
|
||||||
|
|
||||||
@NgModule({
|
@NgModule({
|
||||||
declarations: [
|
declarations: [
|
||||||
|
@ -94,9 +94,11 @@ import {RetryDialogModule} from '@shared/modules/retry-dialog/retry-dialog.modul
|
||||||
]
|
]
|
||||||
})
|
})
|
||||||
export class AppModule {
|
export class AppModule {
|
||||||
constructor(library: FaIconLibrary, faConfig: FaConfig) {
|
constructor(library: FaIconLibrary, faConfig: FaConfig, libraries: NbIconLibraries) {
|
||||||
library.addIconPacks(fas, far);
|
library.addIconPacks(far, fas);
|
||||||
|
libraries.registerFontPack('solid', {packClass: 'fas', iconClassPrefix: 'fa'});
|
||||||
faConfig.defaultPrefix = 'fas';
|
faConfig.defaultPrefix = 'fas';
|
||||||
|
libraries.setDefaultPack('solid');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -2,7 +2,8 @@
|
||||||
<img *ngIf="currentTheme === 'corporate', else changeImage"
|
<img *ngIf="currentTheme === 'corporate', else changeImage"
|
||||||
src="../../assets/images/favicons/favicon.ico" alt="logo dark" class="header-icon" width="60rem" height="60rem">
|
src="../../assets/images/favicons/favicon.ico" alt="logo dark" class="header-icon" width="60rem" height="60rem">
|
||||||
<ng-template #changeImage>
|
<ng-template #changeImage>
|
||||||
<img src="../../assets/images/favicons/favicon_corporate.ico" alt="logo light" class="header-icon" width="60rem" height="60rem">
|
<img src="../../assets/images/favicons/favicon_corporate.ico" alt="logo light" class="header-icon" width="60rem"
|
||||||
|
height="60rem">
|
||||||
</ng-template>
|
</ng-template>
|
||||||
|
|
||||||
<div class="logo-container">
|
<div class="logo-container">
|
||||||
|
@ -11,41 +12,31 @@
|
||||||
<div class="filler"></div>
|
<div class="filler"></div>
|
||||||
<div fxLayoutGap="4rem">
|
<div fxLayoutGap="4rem">
|
||||||
<nb-actions size="medium">
|
<nb-actions size="medium">
|
||||||
|
<!--Info Action-->
|
||||||
|
<nb-action>
|
||||||
|
<fa-icon title="Info" [icon]="fa.faCircleInfo" (click)="onClickShowInfo()" class="action-element-icon fa-2x">
|
||||||
|
</fa-icon>
|
||||||
|
</nb-action>
|
||||||
|
<!--OWASP Action-->
|
||||||
|
<nb-action>
|
||||||
|
<fa-icon title="OWASP Testing Guide"
|
||||||
|
(click)="onClickGoToLink('https://owasp.org/www-project-web-security-testing-guide/v42/')"
|
||||||
|
[icon]="fa.faFileInvoice" class="action-element-icon fa-2x">
|
||||||
|
</fa-icon>
|
||||||
|
</nb-action>
|
||||||
<!--Theme Action-->
|
<!--Theme Action-->
|
||||||
<nb-action class="toggle-theme">
|
<nb-action>
|
||||||
<button nbButton
|
<div (click)="onClickSwitchTheme()" class="action-element-icon">
|
||||||
(click)="onClickGoToLink('https://owasp.org/www-project-web-security-testing-guide/v42/')">
|
<fa-icon *ngIf="currentTheme === 'corporate', else changeIcon"
|
||||||
<fa-icon [icon]="fa.faFileInvoice" class="new-element-icon" href="https://www.google.com">
|
title="Darktheme" [icon]="fa.faMoon" class="fa-2x">
|
||||||
</fa-icon>
|
</fa-icon>
|
||||||
<span class="owasp-redirect-button">OWASP</span>
|
|
||||||
</button>
|
|
||||||
</nb-action>
|
|
||||||
<nb-action class="toggle-theme">
|
|
||||||
<button nbButton
|
|
||||||
(click)="onClickSwitchTheme()">
|
|
||||||
<fa-icon *ngIf="currentTheme === 'corporate', else changeIcon" [icon]="fa.faMoon"
|
|
||||||
class="new-element-icon"></fa-icon>
|
|
||||||
<ng-template #changeIcon>
|
<ng-template #changeIcon>
|
||||||
<fa-icon [icon]="fa.faSun" class="new-element-icon"></fa-icon>
|
<fa-icon title="Lighttheme" [icon]="fa.faSun" class="fa-2x"></fa-icon>
|
||||||
</ng-template>
|
</ng-template>
|
||||||
</button>
|
|
||||||
</nb-action>
|
|
||||||
<!--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"
|
|
||||||
value="{{language}}" (click)="onClickLanguage(language)" fxLayoutAlign="start center">
|
|
||||||
<img src="../../assets/images/flags/{{language}}.svg" class="flag" width="25rem" height="16rem" alt="">
|
|
||||||
<span fxFlexOffset="0.5rem"> {{'languageKeys.' + language | translate}} </span>
|
|
||||||
</nb-option>
|
|
||||||
</nb-select>
|
|
||||||
</div>
|
</div>
|
||||||
</nb-action>
|
</nb-action>
|
||||||
<!--User Action-->
|
<!--User Action-->
|
||||||
<nb-action class="user">
|
<nb-action class="user-action">
|
||||||
<!--<fa-icon [icon]="fa.faUser" class="user-icon">
|
|
||||||
</fa-icon>-->
|
|
||||||
<nb-user [nbContextMenu]="userMenu"
|
<nb-user [nbContextMenu]="userMenu"
|
||||||
[picture]="FALLBACK_IMG"
|
[picture]="FALLBACK_IMG"
|
||||||
name="{{user?.getValue()?.username}}"
|
name="{{user?.getValue()?.username}}"
|
||||||
|
|
|
@ -8,17 +8,26 @@
|
||||||
flex-grow: 1;
|
flex-grow: 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.action-element-icon:hover {
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
.logo-container {
|
||||||
|
white-space: nowrap;
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
}
|
||||||
|
|
||||||
.owasp-redirect-button {
|
.owasp-redirect-button {
|
||||||
margin-left: 0.5rem;
|
margin-left: 0.5rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
.languageContainer {
|
.user-action {
|
||||||
display: flex;
|
// width: 4rem;
|
||||||
max-width: 8rem;
|
z-index: 10;
|
||||||
min-width: 8rem;
|
// height: 3rem;
|
||||||
|
.user-action-accordion-header {
|
||||||
|
|
||||||
.flag {
|
|
||||||
object-fit: contain;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -33,14 +42,6 @@
|
||||||
align-items: center;
|
align-items: center;
|
||||||
width: auto;
|
width: auto;
|
||||||
|
|
||||||
.logo-container {
|
|
||||||
font-style: oblique;
|
|
||||||
color: #e74c3c;
|
|
||||||
white-space: nowrap;
|
|
||||||
overflow: hidden;
|
|
||||||
text-overflow: ellipsis;
|
|
||||||
}
|
|
||||||
|
|
||||||
nb-action {
|
nb-action {
|
||||||
height: auto;
|
height: auto;
|
||||||
display: flex;
|
display: flex;
|
||||||
|
|
|
@ -14,6 +14,8 @@ import {NgxsModule, Store} from '@ngxs/store';
|
||||||
import {KeycloakService} from 'keycloak-angular';
|
import {KeycloakService} from 'keycloak-angular';
|
||||||
import {SESSION_STATE_NAME, SessionState, SessionStateModel} from '@shared/stores/session-state/session-state';
|
import {SESSION_STATE_NAME, SessionState, SessionStateModel} from '@shared/stores/session-state/session-state';
|
||||||
import {User} from '@shared/models/user.model';
|
import {User} from '@shared/models/user.model';
|
||||||
|
import {DialogService} from '@shared/services/dialog-service/dialog.service';
|
||||||
|
import {DialogServiceMock} from '@shared/services/dialog-service/dialog.service.mock';
|
||||||
|
|
||||||
const DESIRED_STORE_STATE_SESSION: SessionStateModel = {
|
const DESIRED_STORE_STATE_SESSION: SessionStateModel = {
|
||||||
userAccount: {
|
userAccount: {
|
||||||
|
@ -52,6 +54,7 @@ describe('HeaderComponent', () => {
|
||||||
NgxsModule.forRoot([SessionState])
|
NgxsModule.forRoot([SessionState])
|
||||||
],
|
],
|
||||||
providers: [
|
providers: [
|
||||||
|
{provide: DialogService, useClass: DialogServiceMock},
|
||||||
NbMenuService,
|
NbMenuService,
|
||||||
KeycloakService
|
KeycloakService
|
||||||
]
|
]
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
import {Component, OnInit} from '@angular/core';
|
import {Component, OnInit} from '@angular/core';
|
||||||
import * as FA from '@fortawesome/free-solid-svg-icons';
|
import * as FA from '@fortawesome/free-solid-svg-icons';
|
||||||
import {NbMenuItem, NbMenuService, NbThemeService} from '@nebular/theme';
|
import {NbMenuItem, NbMenuService, NbThemeService} from '@nebular/theme';
|
||||||
import {map} from 'rxjs/operators';
|
import {filter, map} from 'rxjs/operators';
|
||||||
import {GlobalTitlesVariables} from '@shared/config/global-variables';
|
import {GlobalTitlesVariables} from '@shared/config/global-variables';
|
||||||
import {TranslateService} from '@ngx-translate/core';
|
import {TranslateService} from '@ngx-translate/core';
|
||||||
import {UntilDestroy, untilDestroyed} from '@ngneat/until-destroy';
|
import {UntilDestroy, untilDestroyed} from '@ngneat/until-destroy';
|
||||||
|
@ -14,6 +14,8 @@ import {BehaviorSubject} from 'rxjs';
|
||||||
import {Route} from '@shared/models/route.enum';
|
import {Route} from '@shared/models/route.enum';
|
||||||
import {environment} from '../../environments/environment';
|
import {environment} from '../../environments/environment';
|
||||||
import {Router} from '@angular/router';
|
import {Router} from '@angular/router';
|
||||||
|
import {DialogService} from '@shared/services/dialog-service/dialog.service';
|
||||||
|
import {ProfileSettingsComponent} from '@shared/modules/profile-settings/profile-settings.component';
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'app-header',
|
selector: 'app-header',
|
||||||
|
@ -26,18 +28,21 @@ export class HeaderComponent implements OnInit {
|
||||||
// HTML only
|
// HTML only
|
||||||
readonly fa = FA;
|
readonly fa = FA;
|
||||||
readonly SECURITYC4PO_TITLE: string = GlobalTitlesVariables.SECURITYC4PO_TITLE;
|
readonly SECURITYC4PO_TITLE: string = GlobalTitlesVariables.SECURITYC4PO_TITLE;
|
||||||
|
// Menu only
|
||||||
|
readonly settingsIcon = 'gear';
|
||||||
|
readonly logoutIcon = 'right-from-bracket';
|
||||||
|
|
||||||
currentTheme = '';
|
currentTheme = '';
|
||||||
languages = ['en-US', 'de-DE'];
|
|
||||||
selectedLanguage = '';
|
|
||||||
|
|
||||||
// User Menu Properties
|
|
||||||
userPictureOnly = false;
|
|
||||||
user: BehaviorSubject<User> = new BehaviorSubject<User>(null);
|
user: BehaviorSubject<User> = new BehaviorSubject<User>(null);
|
||||||
userMenu: NbMenuItem[] = [
|
userMenu: NbMenuItem[] = [
|
||||||
{
|
{
|
||||||
title: '',
|
title: 'settings',
|
||||||
pathMatch: 'prefix'
|
icon: { icon: this.settingsIcon, pack: 'fas' }
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: 'logout',
|
||||||
|
icon: { icon: this.logoutIcon, pack: 'fas'}
|
||||||
}
|
}
|
||||||
];
|
];
|
||||||
readonly FALLBACK_IMG = 'assets/images/demo/anon-user-icon.png';
|
readonly FALLBACK_IMG = 'assets/images/demo/anon-user-icon.png';
|
||||||
|
@ -47,6 +52,7 @@ export class HeaderComponent implements OnInit {
|
||||||
private router: Router,
|
private router: Router,
|
||||||
private themeService: NbThemeService,
|
private themeService: NbThemeService,
|
||||||
private translateService: TranslateService,
|
private translateService: TranslateService,
|
||||||
|
private dialogService: DialogService,
|
||||||
private menuService: NbMenuService,
|
private menuService: NbMenuService,
|
||||||
private userService: UserService,
|
private userService: UserService,
|
||||||
protected keycloakService: KeycloakService) {
|
protected keycloakService: KeycloakService) {
|
||||||
|
@ -60,7 +66,6 @@ export class HeaderComponent implements OnInit {
|
||||||
untilDestroyed(this),
|
untilDestroyed(this),
|
||||||
).subscribe(themeName => this.currentTheme = themeName);
|
).subscribe(themeName => this.currentTheme = themeName);
|
||||||
|
|
||||||
this.selectedLanguage = this.translateService.currentLang;
|
|
||||||
// Load user profile
|
// Load user profile
|
||||||
this.userService.loadUserProfile().pipe(
|
this.userService.loadUserProfile().pipe(
|
||||||
untilDestroyed(this)
|
untilDestroyed(this)
|
||||||
|
@ -78,16 +83,44 @@ export class HeaderComponent implements OnInit {
|
||||||
untilDestroyed(this)
|
untilDestroyed(this)
|
||||||
)
|
)
|
||||||
.subscribe((menuBag) => {
|
.subscribe((menuBag) => {
|
||||||
if (menuBag.item.pathMatch === 'prefix') {
|
// Makes sure that other menus without icon won't trigger
|
||||||
this.onClickLogOut();
|
if (menuBag.item.icon) {
|
||||||
|
// tslint:disable-next-line:no-string-literal
|
||||||
|
if (menuBag.item.icon['icon'] === this.settingsIcon) {
|
||||||
|
console.warn('Profile');
|
||||||
|
this.dialogService.openCustomDialog(
|
||||||
|
ProfileSettingsComponent,
|
||||||
|
{
|
||||||
|
user: this.user.getValue(),
|
||||||
|
}
|
||||||
|
).onClose.pipe(
|
||||||
|
filter((confirm) => !!confirm),
|
||||||
|
untilDestroyed(this)
|
||||||
|
).subscribe({
|
||||||
|
next: () => {
|
||||||
|
console.warn('New Settings confirmed');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
// tslint:disable-next-line:no-string-literal
|
||||||
|
else if (menuBag.item.icon['icon'] === this.logoutIcon) {
|
||||||
|
this.onClickLogOut();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
// Setup stream to translate menu item
|
// Setup stream to translate menu item
|
||||||
|
this.translateService.stream('global.action.profile')
|
||||||
|
.pipe(
|
||||||
|
untilDestroyed(this)
|
||||||
|
).subscribe((text: string) => {
|
||||||
|
this.userMenu[0].title = text;
|
||||||
|
});
|
||||||
|
// Setup stream to translate menu item
|
||||||
this.translateService.stream('global.action.logout')
|
this.translateService.stream('global.action.logout')
|
||||||
.pipe(
|
.pipe(
|
||||||
untilDestroyed(this)
|
untilDestroyed(this)
|
||||||
).subscribe((text: string) => {
|
).subscribe((text: string) => {
|
||||||
this.userMenu[0].title = text;
|
this.userMenu[1].title = text;
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -96,6 +129,10 @@ export class HeaderComponent implements OnInit {
|
||||||
window.open(url, '_blank');
|
window.open(url, '_blank');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
onClickShowInfo(): void {
|
||||||
|
console.info('To be implemented..');
|
||||||
|
}
|
||||||
|
|
||||||
onClickSwitchTheme(): void {
|
onClickSwitchTheme(): void {
|
||||||
if (this.currentTheme === 'corporate') {
|
if (this.currentTheme === 'corporate') {
|
||||||
this.themeService.changeTheme('dark');
|
this.themeService.changeTheme('dark');
|
||||||
|
@ -120,8 +157,4 @@ export class HeaderComponent implements OnInit {
|
||||||
console.error(err);
|
console.error(err);
|
||||||
});*/
|
});*/
|
||||||
}
|
}
|
||||||
|
|
||||||
onClickLanguage(language: string): void {
|
|
||||||
this.translateService.use(language);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -5,13 +5,14 @@ import {
|
||||||
NbActionsModule,
|
NbActionsModule,
|
||||||
NbButtonModule,
|
NbButtonModule,
|
||||||
NbCardModule,
|
NbCardModule,
|
||||||
NbContextMenuModule, NbMenuModule,
|
NbContextMenuModule,
|
||||||
NbSelectModule,
|
NbSelectModule,
|
||||||
NbUserModule
|
NbUserModule
|
||||||
} from '@nebular/theme';
|
} from '@nebular/theme';
|
||||||
import {FontAwesomeModule} from '@fortawesome/angular-fontawesome';
|
import {FontAwesomeModule} from '@fortawesome/angular-fontawesome';
|
||||||
import {FlexLayoutModule} from '@angular/flex-layout';
|
import {FlexLayoutModule} from '@angular/flex-layout';
|
||||||
import {TranslateModule} from '@ngx-translate/core';
|
import {TranslateModule} from '@ngx-translate/core';
|
||||||
|
import {ProfileSettingsModule} from '@shared/modules/profile-settings/profile-settings.module';
|
||||||
|
|
||||||
@NgModule({
|
@NgModule({
|
||||||
declarations: [
|
declarations: [
|
||||||
|
@ -30,7 +31,8 @@ import {TranslateModule} from '@ngx-translate/core';
|
||||||
NbSelectModule,
|
NbSelectModule,
|
||||||
TranslateModule,
|
TranslateModule,
|
||||||
NbUserModule,
|
NbUserModule,
|
||||||
NbContextMenuModule
|
NbContextMenuModule,
|
||||||
|
ProfileSettingsModule,
|
||||||
],
|
],
|
||||||
providers: [
|
providers: [
|
||||||
]
|
]
|
||||||
|
|
|
@ -42,7 +42,7 @@
|
||||||
<!--Actions for mobile devices-->
|
<!--Actions for mobile devices-->
|
||||||
<nb-actions size="medium" fxHide fxShow.lt-lg>
|
<nb-actions size="medium" fxHide fxShow.lt-lg>
|
||||||
<nb-action>
|
<nb-action>
|
||||||
<nb-user [nbContextMenu]="objectiveActionItems" shape="rectangle" [picture]="BARS_IMG" name="" [onlyPicture]></nb-user> <!--[picture]="fa.faRedoAlt"-->
|
<nb-user [nbContextMenu]="objectiveActionItems" shape="rectangle" [picture]="BARS_IMG" name="" [onlyPicture]></nb-user>
|
||||||
</nb-action>
|
</nb-action>
|
||||||
</nb-actions>
|
</nb-actions>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -29,19 +29,18 @@ import {TranslateService} from '@ngx-translate/core';
|
||||||
export class ObjectiveHeaderComponent implements OnInit {
|
export class ObjectiveHeaderComponent implements OnInit {
|
||||||
|
|
||||||
selectedProject$: BehaviorSubject<Project> = new BehaviorSubject<Project>(null);
|
selectedProject$: BehaviorSubject<Project> = new BehaviorSubject<Project>(null);
|
||||||
|
// Menu only
|
||||||
|
readonly editIcon = 'edit';
|
||||||
|
readonly fileExportIcon = 'file-export';
|
||||||
// Mobile menu properties
|
// Mobile menu properties
|
||||||
objectiveActionItems: NbMenuItem[] = [
|
objectiveActionItems: NbMenuItem[] = [
|
||||||
{
|
{
|
||||||
title: 'global.action.edit',
|
title: 'global.action.edit',
|
||||||
badge: {
|
icon: { icon: this.editIcon, pack: 'fas' }
|
||||||
status: 'warning'
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: 'global.action.report',
|
title: 'global.action.report',
|
||||||
badge: {
|
icon: { icon: this.fileExportIcon, pack: 'fas' }
|
||||||
status: 'info'
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
// HTML only
|
// HTML only
|
||||||
|
@ -83,10 +82,16 @@ export class ObjectiveHeaderComponent implements OnInit {
|
||||||
untilDestroyed(this)
|
untilDestroyed(this)
|
||||||
)
|
)
|
||||||
.subscribe((menuBag) => {
|
.subscribe((menuBag) => {
|
||||||
if (menuBag.item.badge && menuBag.item.badge.status === 'warning') {
|
// Makes sure that other menus without icon won't trigger
|
||||||
this.onClickEditPentestProject();
|
if (menuBag.item.icon) {
|
||||||
} else if (menuBag.item.badge && menuBag.item.badge.status === 'info') {
|
// tslint:disable-next-line:no-string-literal
|
||||||
this.onClickGeneratePentestReport();
|
if (menuBag.item.icon['icon'] === this.editIcon) {
|
||||||
|
this.onClickEditPentestProject();
|
||||||
|
}
|
||||||
|
// tslint:disable-next-line:no-string-literal
|
||||||
|
else if (menuBag.item.icon['icon'] === this.fileExportIcon) {
|
||||||
|
this.onClickGeneratePentestReport();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
// Setup stream to translate menu action item
|
// Setup stream to translate menu action item
|
||||||
|
|
|
@ -5,6 +5,7 @@
|
||||||
.content {
|
.content {
|
||||||
height: 95%;
|
height: 95%;
|
||||||
overflow: auto !important;
|
overflow: auto !important;
|
||||||
|
// overflow: hidden !important;
|
||||||
|
|
||||||
// ToDo: Fixes tab header but also disables scrolling for content
|
// ToDo: Fixes tab header but also disables scrolling for content
|
||||||
/*nb-tab {
|
/*nb-tab {
|
||||||
|
|
|
@ -2,8 +2,10 @@
|
||||||
<h4>
|
<h4>
|
||||||
{{ getPentestHeaderForObjective(pentestInfo$.getValue().refNumber) | translate}}
|
{{ getPentestHeaderForObjective(pentestInfo$.getValue().refNumber) | translate}}
|
||||||
</h4>
|
</h4>
|
||||||
<p class="description">
|
<div class="description">
|
||||||
{{ getPentestInfoForObjective(pentestInfo$.getValue().refNumber) | translate }}
|
<div>
|
||||||
</p>
|
{{ getPentestInfoForObjective(pentestInfo$.getValue().refNumber) | translate }}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
<!--ToDo: Add tooling hints after description (maybe in pentest-header component)-->
|
<!--ToDo: Add tooling hints after description (maybe in pentest-header component)-->
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -1,10 +1,14 @@
|
||||||
.pentest-info {
|
.pentest-info {
|
||||||
overflow-y: scroll;
|
overflow: hidden !important;
|
||||||
overflow-x: hidden;
|
|
||||||
// overflow: auto !important;
|
|
||||||
position: relative !important;
|
position: relative !important;
|
||||||
|
|
||||||
.description {
|
.description {
|
||||||
|
// ToDo: Make only description scrollable
|
||||||
|
// Scrollbar
|
||||||
|
overflow-y: scroll !important;
|
||||||
|
overflow-x: hidden;
|
||||||
|
scroll-behavior: smooth;
|
||||||
|
|
||||||
width: 60vw;
|
width: 60vw;
|
||||||
font-size: 1rem;
|
font-size: 1rem;
|
||||||
white-space: pre-line;
|
white-space: pre-line;
|
||||||
|
|
|
@ -1,6 +1,7 @@
|
||||||
{
|
{
|
||||||
"global": {
|
"global": {
|
||||||
"actions": "Aktionen",
|
"actions": "Aktionen",
|
||||||
|
"action.profile": "Profil",
|
||||||
"action.login": "Einloggen",
|
"action.login": "Einloggen",
|
||||||
"action.logout": "Ausloggen",
|
"action.logout": "Ausloggen",
|
||||||
"action.retry": "Erneut Versuchen",
|
"action.retry": "Erneut Versuchen",
|
||||||
|
@ -56,6 +57,35 @@
|
||||||
"failed": "Benutzername oder Passwort falsch",
|
"failed": "Benutzername oder Passwort falsch",
|
||||||
"unauthorized": "Benutzer nicht gefunden. Bitte registrieren und erneut versuchen"
|
"unauthorized": "Benutzer nicht gefunden. Bitte registrieren und erneut versuchen"
|
||||||
},
|
},
|
||||||
|
"profile": {
|
||||||
|
"header": "Nutzerprofil",
|
||||||
|
"username": {
|
||||||
|
"title": "Nutzername",
|
||||||
|
"placeholder": "Nutzername"
|
||||||
|
},
|
||||||
|
"firstName": {
|
||||||
|
"title": "Vorname",
|
||||||
|
"placeholder": "Vorname"
|
||||||
|
},
|
||||||
|
"lastName": {
|
||||||
|
"title": "Nachname",
|
||||||
|
"placeholder": "Nachname"
|
||||||
|
},
|
||||||
|
"validationMessage": {
|
||||||
|
"firstNameRequired": "Vorname ist erforderlich.",
|
||||||
|
"lastNameRequired": "Nachname ist erforderlich."
|
||||||
|
},
|
||||||
|
"languageLabel": "Sprache ändern:",
|
||||||
|
"password": {
|
||||||
|
"title": "Passwort ändern:",
|
||||||
|
"old": "Altes Passwort",
|
||||||
|
"new": "Neues Passwort",
|
||||||
|
"confirmNew": "Neues Passwort bestätigen",
|
||||||
|
"validationMessage": {
|
||||||
|
"passwordRequired": "Passwort ist erforderlich."
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
"state": {
|
"state": {
|
||||||
"new": "Neu",
|
"new": "Neu",
|
||||||
"needs_more_info": "Benötigt mehr Informationen",
|
"needs_more_info": "Benötigt mehr Informationen",
|
||||||
|
@ -64,7 +94,7 @@
|
||||||
"triaged": "Ausstehend",
|
"triaged": "Ausstehend",
|
||||||
"retesting": "Erneutes Testen",
|
"retesting": "Erneutes Testen",
|
||||||
"resolved": "Aufgeklärt",
|
"resolved": "Aufgeklärt",
|
||||||
"informative": "Informatif",
|
"informative": "Informativ",
|
||||||
"duplicate": "Duplikat",
|
"duplicate": "Duplikat",
|
||||||
"not_applicable": "Unzutreffend",
|
"not_applicable": "Unzutreffend",
|
||||||
"spam": "Spam",
|
"spam": "Spam",
|
||||||
|
|
|
@ -1,6 +1,7 @@
|
||||||
{
|
{
|
||||||
"global": {
|
"global": {
|
||||||
"actions": "Actions",
|
"actions": "Actions",
|
||||||
|
"action.profile": "Profile",
|
||||||
"action.login": "Login",
|
"action.login": "Login",
|
||||||
"action.logout": "Logout",
|
"action.logout": "Logout",
|
||||||
"action.retry": "Try again",
|
"action.retry": "Try again",
|
||||||
|
@ -56,6 +57,35 @@
|
||||||
"failed": "Wrong username or password",
|
"failed": "Wrong username or password",
|
||||||
"unauthorized": "User not found. Please register and try again"
|
"unauthorized": "User not found. Please register and try again"
|
||||||
},
|
},
|
||||||
|
"profile": {
|
||||||
|
"header": "Userprofile",
|
||||||
|
"username": {
|
||||||
|
"title": "Username",
|
||||||
|
"placeholder": "Username"
|
||||||
|
},
|
||||||
|
"firstName": {
|
||||||
|
"title": "Firstname",
|
||||||
|
"placeholder": "Firstname"
|
||||||
|
},
|
||||||
|
"lastName": {
|
||||||
|
"title": "Lastname",
|
||||||
|
"placeholder": "Lastname"
|
||||||
|
},
|
||||||
|
"validationMessage": {
|
||||||
|
"firstNameRequired": "Firstname is required.",
|
||||||
|
"lastNameRequired": "Lastname is required."
|
||||||
|
},
|
||||||
|
"languageLabel": "Change language:",
|
||||||
|
"password": {
|
||||||
|
"title": "Change password:",
|
||||||
|
"old": "Old password",
|
||||||
|
"new": "New password",
|
||||||
|
"confirmNew": "Confirm new password",
|
||||||
|
"validationMessage": {
|
||||||
|
"passwordRequired": "Password is required."
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
"state": {
|
"state": {
|
||||||
"new": "New",
|
"new": "New",
|
||||||
"needs_more_info": "Needs More Info",
|
"needs_more_info": "Needs More Info",
|
||||||
|
|
|
@ -31,7 +31,6 @@
|
||||||
float: left;
|
float: left;
|
||||||
clear: none;
|
clear: none;
|
||||||
margin-left: 1rem;
|
margin-left: 1rem;
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
nb-form-field {
|
nb-form-field {
|
||||||
|
|
|
@ -32,15 +32,14 @@ export class ExportReportDialogComponent implements OnInit {
|
||||||
private dialogService: DialogService
|
private dialogService: DialogService
|
||||||
) {
|
) {
|
||||||
}
|
}
|
||||||
|
|
||||||
// HTML
|
// HTML
|
||||||
readonly fa = FA;
|
readonly fa = FA;
|
||||||
// form control elements
|
// form control elements
|
||||||
exportReportFormatControl = new FormControl(ExportFormatOptions.PDF);
|
exportReportFormatControl = new FormControl(ExportFormatOptions.PDF);
|
||||||
exportReportLanguageControl = new FormControl(ExportLanguageOptions.ENGLISH);
|
exportReportLanguageControl = new FormControl(LanguageOptions.ENGLISH);
|
||||||
// exports
|
// exports
|
||||||
exportFormats = ExportFormatOptions;
|
exportFormats = ExportFormatOptions;
|
||||||
exportLanguages = ExportLanguageOptions;
|
exportLanguages = LanguageOptions;
|
||||||
|
|
||||||
dialogData: GenericDialogData;
|
dialogData: GenericDialogData;
|
||||||
|
|
||||||
|
@ -159,7 +158,7 @@ export enum ExportFormatOptions {
|
||||||
HTML = 'HTML'
|
HTML = 'HTML'
|
||||||
}
|
}
|
||||||
|
|
||||||
export enum ExportLanguageOptions {
|
export enum LanguageOptions {
|
||||||
ENGLISH = 'en-US',
|
ENGLISH = 'en-US',
|
||||||
GERMAN = 'de-DE'
|
GERMAN = 'de-DE'
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,69 @@
|
||||||
|
<form [formGroup]="passwordFormGroup">
|
||||||
|
<div class="password-form" fxLayout="row" fxLayoutGap="2rem" fxLayoutAlign="start start">
|
||||||
|
<!--Old password-->
|
||||||
|
<nb-form-field class="password-form-field">
|
||||||
|
<label for="oldPassword" class="label">
|
||||||
|
{{'profile.password.old' | translate}}
|
||||||
|
</label>
|
||||||
|
<input formControlName="oldPassword"
|
||||||
|
type="{{getInputType('oldPassword')}}" required
|
||||||
|
id="oldPassword" nbInput
|
||||||
|
class="form-field"
|
||||||
|
[status]="passwordFormGroup.get('oldPassword').dirty ? (passwordFormGroup.get('oldPassword').invalid ? 'danger' : 'basic') : 'basic'"
|
||||||
|
placeholder="{{'******'}}">
|
||||||
|
<button nbSuffix nbButton ghost class="form-field-button" (click)="toggleShowOldPassword()">
|
||||||
|
<fa-icon [icon]="showOldPassword ? fa.faEye : fa.faEyeSlash"></fa-icon>
|
||||||
|
</button>
|
||||||
|
<!-- FIXME: when the bug (https://github.com/angular/components/issues/7739) is fixed -->
|
||||||
|
<ng-template *ngIf="passwordFormGroup.get('oldPassword').dirty">
|
||||||
|
<span class="error-text"
|
||||||
|
*ngIf="passwordFormGroup.get('oldPassword')?.hasError('required')">
|
||||||
|
{{'profile.password.validationMessage.passwordRequired' | translate}}
|
||||||
|
</span>
|
||||||
|
</ng-template>
|
||||||
|
</nb-form-field>
|
||||||
|
<div fxLayout="column" fxLayoutGap="2rem">
|
||||||
|
<!--New password-->
|
||||||
|
<nb-form-field class="password-form-field">
|
||||||
|
<label for="newPassword" class="label">
|
||||||
|
{{'profile.password.new' | translate}}
|
||||||
|
</label>
|
||||||
|
<input formControlName="newPassword"
|
||||||
|
type="{{getInputType('newPassword')}}" required
|
||||||
|
id="newPassword" nbInput
|
||||||
|
class="form-field"
|
||||||
|
[status]="passwordFormGroup.get('newPassword').dirty ? (passwordFormGroup.get('newPassword').invalid ? 'danger' : 'basic') : 'basic'"
|
||||||
|
placeholder="{{'******'}}">
|
||||||
|
<button nbSuffix nbButton ghost class="form-field-button" (click)="toggleShowNewPasswords()">
|
||||||
|
<fa-icon [icon]="showNewPasswords ? fa.faEye : fa.faEyeSlash"></fa-icon>
|
||||||
|
</button>
|
||||||
|
<!-- FIXME: when the bug (https://github.com/angular/components/issues/7739) is fixed -->
|
||||||
|
<ng-template *ngIf="passwordFormGroup.get('newPassword').dirty">
|
||||||
|
<span class="error-text"
|
||||||
|
*ngIf="passwordFormGroup.get('newPassword')?.hasError('required')">
|
||||||
|
{{'profile.password.validationMessage.passwordRequired' | translate}}
|
||||||
|
</span>
|
||||||
|
</ng-template>
|
||||||
|
</nb-form-field>
|
||||||
|
<!--New password-->
|
||||||
|
<nb-form-field class="password-form-field">
|
||||||
|
<label for="confirmNewPassword" class="label">
|
||||||
|
{{'profile.password.confirmNew' | translate}}
|
||||||
|
</label>
|
||||||
|
<input formControlName="confirmNewPassword"
|
||||||
|
type="{{getInputType('confirmNewPassword')}}" required
|
||||||
|
id="confirmNewPassword" nbInput
|
||||||
|
class="form-field"
|
||||||
|
[status]="passwordFormGroup.get('confirmNewPassword').dirty ? (passwordFormGroup.get('confirmNewPassword').invalid ? 'danger' : 'basic') : 'basic'"
|
||||||
|
placeholder="{{'******'}}">
|
||||||
|
<!-- FIXME: when the bug (https://github.com/angular/components/issues/7739) is fixed -->
|
||||||
|
<ng-template *ngIf="passwordFormGroup.get('confirmNewPassword').dirty">
|
||||||
|
<span class="error-text"
|
||||||
|
*ngIf="passwordFormGroup.get('confirmNewPassword')?.hasError('required')">
|
||||||
|
{{'profile.password.validationMessage.passwordRequired' | translate}}
|
||||||
|
</span>
|
||||||
|
</ng-template>
|
||||||
|
</nb-form-field>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</form>
|
|
@ -0,0 +1,29 @@
|
||||||
|
@import '../../../../assets/@theme/styles/themes';
|
||||||
|
|
||||||
|
.password-form {
|
||||||
|
padding-top: 1.5rem;
|
||||||
|
|
||||||
|
.password-form-field {
|
||||||
|
width: 20rem !important;
|
||||||
|
|
||||||
|
.label {
|
||||||
|
display: block;
|
||||||
|
font-size: 0.95rem;
|
||||||
|
padding-bottom: 0.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-field {
|
||||||
|
margin-bottom: 0.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-field-button {
|
||||||
|
margin-top: 1rem;
|
||||||
|
margin-right: 1rem;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.error-text {
|
||||||
|
float: left;
|
||||||
|
color: nb-theme(color-danger-default);
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,58 @@
|
||||||
|
import {ComponentFixture, TestBed} from '@angular/core/testing';
|
||||||
|
|
||||||
|
import {PasswordInputFromComponent} from './password-input-from.component';
|
||||||
|
import {NbFormFieldModule} from '@nebular/theme';
|
||||||
|
import {ReactiveFormsModule} from '@angular/forms';
|
||||||
|
import {BrowserAnimationsModule} from '@angular/platform-browser/animations';
|
||||||
|
import {ThemeModule} from '@assets/@theme/theme.module';
|
||||||
|
import {TranslateLoader, TranslateModule} from '@ngx-translate/core';
|
||||||
|
import {HttpLoaderFactory} from '../../../../app/common-app.module';
|
||||||
|
import {HttpClient, HttpClientModule} from '@angular/common/http';
|
||||||
|
import {NgxsModule} from '@ngxs/store';
|
||||||
|
import {SessionState} from '@shared/stores/session-state/session-state';
|
||||||
|
import {HttpClientTestingModule} from '@angular/common/http/testing';
|
||||||
|
import {KeycloakService} from 'keycloak-angular';
|
||||||
|
|
||||||
|
describe('PasswordInputFromComponent', () => {
|
||||||
|
let component: PasswordInputFromComponent;
|
||||||
|
let fixture: ComponentFixture<PasswordInputFromComponent>;
|
||||||
|
|
||||||
|
beforeEach(async () => {
|
||||||
|
await TestBed.configureTestingModule({
|
||||||
|
declarations: [
|
||||||
|
PasswordInputFromComponent
|
||||||
|
],
|
||||||
|
imports: [
|
||||||
|
NbFormFieldModule,
|
||||||
|
ReactiveFormsModule,
|
||||||
|
BrowserAnimationsModule,
|
||||||
|
ThemeModule.forRoot(),
|
||||||
|
TranslateModule.forRoot({
|
||||||
|
loader: {
|
||||||
|
provide: TranslateLoader,
|
||||||
|
useFactory: HttpLoaderFactory,
|
||||||
|
deps: [HttpClient]
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
NgxsModule.forRoot([SessionState]),
|
||||||
|
HttpClientModule,
|
||||||
|
HttpClientTestingModule
|
||||||
|
],
|
||||||
|
providers: [
|
||||||
|
KeycloakService
|
||||||
|
]
|
||||||
|
})
|
||||||
|
.compileComponents();
|
||||||
|
});
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
fixture = TestBed.createComponent(PasswordInputFromComponent);
|
||||||
|
component = fixture.componentInstance;
|
||||||
|
// ToDo: fix detectChanges() when from control accessor is defined
|
||||||
|
// fixture.detectChanges();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should create', () => {
|
||||||
|
expect(component).toBeTruthy();
|
||||||
|
});
|
||||||
|
});
|
|
@ -0,0 +1,130 @@
|
||||||
|
import {Component, EventEmitter, forwardRef, Input, OnInit, Output} from '@angular/core';
|
||||||
|
import {AbstractControl, FormBuilder, FormControl, FormGroup, NG_VALIDATORS, NG_VALUE_ACCESSOR, ValidationErrors} from '@angular/forms';
|
||||||
|
import {
|
||||||
|
OldPasswordEmptyError,
|
||||||
|
PasswordErrorStateMatcher
|
||||||
|
} from '@shared/modules/profile-settings/password-input-from/util/password-error-state-matcher';
|
||||||
|
import {passwordValidator} from '@shared/modules/profile-settings/password-input-from/util/password.validator';
|
||||||
|
import {BehaviorSubject} from 'rxjs';
|
||||||
|
import {ProfilePasswordFormData} from '@shared/modules/profile-settings/password-input-from/util/profile-password-form-data.model';
|
||||||
|
import * as FA from '@fortawesome/free-solid-svg-icons';
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
selector: 'app-password-input-from',
|
||||||
|
templateUrl: './password-input-from.component.html',
|
||||||
|
styleUrls: ['./password-input-from.component.scss'],
|
||||||
|
providers: [
|
||||||
|
{
|
||||||
|
provide: NG_VALUE_ACCESSOR,
|
||||||
|
useExisting: forwardRef(() => PasswordInputFromComponent),
|
||||||
|
multi: true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
provide: NG_VALIDATORS,
|
||||||
|
useExisting: forwardRef(() => PasswordInputFromComponent),
|
||||||
|
multi: true
|
||||||
|
}
|
||||||
|
]
|
||||||
|
})
|
||||||
|
export class PasswordInputFromComponent implements OnInit {
|
||||||
|
|
||||||
|
constructor(private fb: FormBuilder) { }
|
||||||
|
// Emits true if password is strong
|
||||||
|
@Output() passwordStrong: EventEmitter<boolean> = new EventEmitter();
|
||||||
|
// Handle erros
|
||||||
|
@Input() userServiceError$: BehaviorSubject<boolean> = new BehaviorSubject<boolean>(false);
|
||||||
|
|
||||||
|
public passwordErrorStateMatcher: PasswordErrorStateMatcher;
|
||||||
|
public oldPasswordEmptyError: OldPasswordEmptyError;
|
||||||
|
// show the criteria of the new password
|
||||||
|
showDetails = false;
|
||||||
|
|
||||||
|
passwordFormGroup: FormGroup;
|
||||||
|
// form control elements
|
||||||
|
oldPasswordCtrl: AbstractControl;
|
||||||
|
newPasswordCtrl: AbstractControl;
|
||||||
|
confirmNewPasswordCtrl: AbstractControl;
|
||||||
|
// HTML only
|
||||||
|
readonly fa = FA;
|
||||||
|
showOldPassword = false;
|
||||||
|
showNewPasswords = false;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* tests value of control for empty-spaces
|
||||||
|
* @param control FormControl
|
||||||
|
*/
|
||||||
|
static containsBlankSpaceValidator(control: FormControl): ValidationErrors | null {
|
||||||
|
const containsEmptySpacesRegex: RegExp = new RegExp(/\s+/);
|
||||||
|
return containsEmptySpacesRegex.test(control.value) ? {passwordContainsBlankSpace: true} : null;
|
||||||
|
}
|
||||||
|
|
||||||
|
ngOnInit(): void {
|
||||||
|
this.passwordFormGroup = this.fb.group(
|
||||||
|
{
|
||||||
|
oldPassword: ['', [PasswordInputFromComponent.containsBlankSpaceValidator]],
|
||||||
|
newPassword: ['', [PasswordInputFromComponent.containsBlankSpaceValidator]],
|
||||||
|
confirmNewPassword: ''
|
||||||
|
},
|
||||||
|
{validators: [passwordValidator]}
|
||||||
|
);
|
||||||
|
// get form controls
|
||||||
|
this.oldPasswordCtrl = this.passwordFormGroup.get('oldPassword');
|
||||||
|
this.newPasswordCtrl = this.passwordFormGroup.get('newPassword');
|
||||||
|
this.confirmNewPasswordCtrl = this.passwordFormGroup.get('confirmNewPassword');
|
||||||
|
|
||||||
|
this.passwordErrorStateMatcher = new PasswordErrorStateMatcher(this.userServiceError$);
|
||||||
|
this.oldPasswordEmptyError = new OldPasswordEmptyError();
|
||||||
|
}
|
||||||
|
|
||||||
|
registerOnChange(fn: any): void {
|
||||||
|
this.passwordFormGroup.valueChanges.subscribe(() => {
|
||||||
|
fn(this.passwordFormGroup.value);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
registerOnTouched(fn: any): void {
|
||||||
|
}
|
||||||
|
|
||||||
|
writeValue(value: ProfilePasswordFormData): void {
|
||||||
|
if (value) {
|
||||||
|
this.oldPasswordCtrl.setValue(value.oldPassword);
|
||||||
|
this.newPasswordCtrl.setValue(value.newPassword);
|
||||||
|
this.confirmNewPasswordCtrl.setValue(value.confirmNewPassword);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
validate(control: AbstractControl): ValidationErrors | null {
|
||||||
|
return this.passwordFormGroup.valid ? null : passwordValidator(this.passwordFormGroup);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This Function is triggered as soon as a change in the strength of the password happens
|
||||||
|
*
|
||||||
|
* @param strength the new strength as a percentage number. Only 100% denotes a strong password
|
||||||
|
*/
|
||||||
|
onStrengthChanged(strength: number): void {
|
||||||
|
this.passwordStrong.emit(strength === 100);
|
||||||
|
}
|
||||||
|
|
||||||
|
getInputType(passwordField: string): string {
|
||||||
|
if (passwordField === 'oldPassword') {
|
||||||
|
if (this.showOldPassword) {
|
||||||
|
return 'text';
|
||||||
|
}
|
||||||
|
return 'password';
|
||||||
|
} else {
|
||||||
|
if (this.showNewPasswords) {
|
||||||
|
return 'text';
|
||||||
|
}
|
||||||
|
return 'password';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
toggleShowOldPassword(): void {
|
||||||
|
this.showOldPassword = !this.showOldPassword;
|
||||||
|
}
|
||||||
|
|
||||||
|
toggleShowNewPasswords(): void {
|
||||||
|
this.showNewPasswords = !this.showNewPasswords;
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,33 @@
|
||||||
|
import {FormControl, FormGroupDirective, NgForm} from '@angular/forms';
|
||||||
|
import {BehaviorSubject} from 'rxjs';
|
||||||
|
|
||||||
|
export class PasswordErrorStateMatcher {
|
||||||
|
public passwordsDoNotMatch = true;
|
||||||
|
public userServiceError;
|
||||||
|
|
||||||
|
constructor(userServiceError: BehaviorSubject<boolean>) {
|
||||||
|
this.userServiceError = userServiceError;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return true if new and confirm password do not match.
|
||||||
|
*/
|
||||||
|
isErrorState(ctrl: FormControl | null, form: FormGroupDirective | NgForm | null): boolean {
|
||||||
|
const passwordCtrl = form.control.get('newPassword');
|
||||||
|
this.passwordsDoNotMatch = passwordCtrl.value !== ctrl.value;
|
||||||
|
return this.passwordsDoNotMatch || this.userServiceError.value;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export class OldPasswordEmptyError {
|
||||||
|
public oldPasswordEmpty = true;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return true if old password is empty and new password is not.
|
||||||
|
*/
|
||||||
|
isErrorState(ctrl: FormControl | null, form: FormGroupDirective | NgForm | null): boolean {
|
||||||
|
const passwordCtrl = form.control.get('newPassword');
|
||||||
|
this.oldPasswordEmpty = passwordCtrl.value.length !== 0 && ctrl.value.length === 0;
|
||||||
|
return this.oldPasswordEmpty;
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,23 @@
|
||||||
|
import {AbstractControl, ValidationErrors} from '@angular/forms';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* passwordValidator validates the password based on 3 conditions:
|
||||||
|
* - newPassword === confirmNewPassword
|
||||||
|
* - MIN_PASSWORD_LENGTH <= newPassword
|
||||||
|
* - oldPassword is NOT empty
|
||||||
|
*
|
||||||
|
* @param formGroup AbstractControl containing [oldPassword, newPassword, confirmPassword]
|
||||||
|
*/
|
||||||
|
export function passwordValidator(formGroup: AbstractControl): ValidationErrors | null {
|
||||||
|
const oldPasswordCtrl = formGroup.get('oldPassword');
|
||||||
|
const newPasswordCtrl = formGroup.get('newPassword');
|
||||||
|
const confirmNewPasswordCtrl = formGroup.get('confirmNewPassword');
|
||||||
|
if (newPasswordCtrl.value !== confirmNewPasswordCtrl.value) {
|
||||||
|
return {passwordsDoNotMatch: true};
|
||||||
|
} else if (oldPasswordCtrl.value === '') {
|
||||||
|
oldPasswordCtrl.setErrors({oldPasswordEmpty: true});
|
||||||
|
return {oldPasswordEmpty: true};
|
||||||
|
} else {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,5 @@
|
||||||
|
export interface ProfilePasswordFormData {
|
||||||
|
oldPassword: string;
|
||||||
|
newPassword: string;
|
||||||
|
confirmNewPassword: string;
|
||||||
|
}
|
|
@ -0,0 +1,4 @@
|
||||||
|
export const Patterns = {
|
||||||
|
NOT_BLANK: '^(?!(?:\\s+)$).*$',
|
||||||
|
NO_WHITESPACES: '^(?!(?:\\s+)$).*$'
|
||||||
|
};
|
|
@ -0,0 +1,135 @@
|
||||||
|
<nb-card accent="control" status="info" class="profile-setting-dialog">
|
||||||
|
<nb-card-header fxLayoutAlign="start center" class="dialog-header">
|
||||||
|
<fa-icon [icon]="fa.faUserCog"
|
||||||
|
class="header-icon fa-lg">
|
||||||
|
</fa-icon>
|
||||||
|
<span class="header-text"> {{ 'profile.header' | translate }} </span>
|
||||||
|
</nb-card-header>
|
||||||
|
<nb-card-body class="dialog-body">
|
||||||
|
<div class="user-properties" fxLayout="row" fxLayoutGap="2rem" fxLayoutAlign="center center">
|
||||||
|
<div fxFlex="20">
|
||||||
|
<img src="{{USER_IMG}}" alt="userimage" class="user-img">
|
||||||
|
</div>
|
||||||
|
<div fxFlex class="properties">
|
||||||
|
<form [formGroup]="userFormGroup" fxLayout="column" fxLayoutGap="1rem" fxLayoutAlign="start start">
|
||||||
|
<!--Username-->
|
||||||
|
<nb-form-field class="user-form-field">
|
||||||
|
<label for="username" class="label">
|
||||||
|
{{'profile.username.title' | translate}}
|
||||||
|
</label>
|
||||||
|
<fa-icon nbPrefix class="prefix-icon" [icon]="fa.faAt"></fa-icon>
|
||||||
|
<input formControlName="username"
|
||||||
|
type="text" required
|
||||||
|
id="username" nbInput
|
||||||
|
fullWidth
|
||||||
|
class="form-field untouchable"
|
||||||
|
status="control"
|
||||||
|
placeholder="{{'profile.username.placeholder' | translate}}">
|
||||||
|
</nb-form-field>
|
||||||
|
<div fxLayout="row" fxLayoutGap="1rem" fxLayoutAlign="start start">
|
||||||
|
<!--Firstname-->
|
||||||
|
<nb-form-field class="user-form-field">
|
||||||
|
<label for="firstName" class="label">
|
||||||
|
{{'profile.firstName.title' | translate}}
|
||||||
|
</label>
|
||||||
|
<input formControlName="firstName"
|
||||||
|
type="text" required
|
||||||
|
id="firstName" nbInput
|
||||||
|
fullWidth
|
||||||
|
class="form-field"
|
||||||
|
[status]="userFormGroup.get('firstName').dirty ? (userFormGroup.get('firstName').invalid ? 'danger' : 'basic') : 'basic'"
|
||||||
|
placeholder="{{'profile.firstName.placeholder' | translate}} *">
|
||||||
|
<!-- FIXME: when the bug (https://github.com/angular/components/issues/7739) is fixed -->
|
||||||
|
<ng-template *ngIf="userFormGroup.get('firstName').dirty">
|
||||||
|
<span class="error-text"
|
||||||
|
*ngIf="userFormGroup.get('firstName')?.hasError('required')">
|
||||||
|
{{'WWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWW'}}
|
||||||
|
{{'profile.validationMessage.firstNameRequired' | translate}}
|
||||||
|
</span>
|
||||||
|
<span class="error-text"
|
||||||
|
*ngIf="userFormGroup.get('firstName').invalid">
|
||||||
|
{{'Firstname is invalid' | translate}}
|
||||||
|
</span>
|
||||||
|
</ng-template>
|
||||||
|
</nb-form-field>
|
||||||
|
<!--Lastname-->
|
||||||
|
<nb-form-field class="user-form-field">
|
||||||
|
<label for="lastName" class="label">
|
||||||
|
{{'profile.lastName.title' | translate}}
|
||||||
|
</label>
|
||||||
|
<input formControlName="lastName"
|
||||||
|
type="text" required
|
||||||
|
id="lastName" nbInput
|
||||||
|
fullWidth
|
||||||
|
class="form-field"
|
||||||
|
[status]="userFormGroup.get('lastName').dirty ? (userFormGroup.get('lastName').invalid ? 'danger' : 'basic') : 'basic'"
|
||||||
|
placeholder="{{'profile.lastName.placeholder' | translate}} *">
|
||||||
|
<!-- FIXME: when the bug (https://github.com/angular/components/issues/7739) is fixed -->
|
||||||
|
<ng-template *ngIf="userFormGroup.get('lastName').dirty">
|
||||||
|
<span class="error-text"
|
||||||
|
*ngIf="userFormGroup.get('lastName')?.hasError('required')">
|
||||||
|
{{'profile.validationMessage.lastNameRequired' | translate}}
|
||||||
|
</span>
|
||||||
|
<span class="error-text"
|
||||||
|
*ngIf="userFormGroup.get('lastName').invalid">
|
||||||
|
{{'Lastname is invalid' | translate}}
|
||||||
|
</span>
|
||||||
|
</ng-template>
|
||||||
|
</nb-form-field>
|
||||||
|
</div>
|
||||||
|
<!--ToDo: Email?-->
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="language-settings">
|
||||||
|
<!--Language Radio Selection-->
|
||||||
|
<label class="language-selection-label">
|
||||||
|
{{ 'profile.languageLabel' | translate }}
|
||||||
|
</label>
|
||||||
|
<div fxLayout="row" fxLayoutGap="1rem" fxLayoutAlign="start center">
|
||||||
|
<nb-radio-group name="language" [formControl]="profileLanguageControl"
|
||||||
|
class="language-radio-buttons languageContainer" status="info">
|
||||||
|
<nb-radio value="{{profileLanguages.ENGLISH}}" (click)="onClickLanguage(profileLanguages.ENGLISH)">
|
||||||
|
<img src="../../assets/images/flags/{{profileLanguages.ENGLISH}}.svg" class="flag" width="25rem"
|
||||||
|
height="16rem"
|
||||||
|
alt="">
|
||||||
|
</nb-radio>
|
||||||
|
<nb-radio value="{{profileLanguages.GERMAN}}" (click)="onClickLanguage(profileLanguages.GERMAN)">
|
||||||
|
<img src="../../assets/images/flags/{{profileLanguages.GERMAN}}.svg" class="flag" width="25rem"
|
||||||
|
height="16rem"
|
||||||
|
alt="">
|
||||||
|
</nb-radio>
|
||||||
|
</nb-radio-group>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<!--ToDo: Add accordion for keycloak values-->
|
||||||
|
<div class="user-password-change">
|
||||||
|
<label class="password-selection-label">
|
||||||
|
{{'profile.password.title' | translate}}
|
||||||
|
</label>
|
||||||
|
<div>
|
||||||
|
<form class="password-form" [formGroup]="passwordFormGroup">
|
||||||
|
<app-password-input-from
|
||||||
|
formControlName="passwordInput"
|
||||||
|
[userServiceError$]="userServiceError"
|
||||||
|
(passwordStrong)="passwordStrong = $event">
|
||||||
|
</app-password-input-from>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</nb-card-body>
|
||||||
|
<nb-card-footer fxLayout="row" fxLayoutGap="1.5rem" fxLayoutAlign="end end">
|
||||||
|
<button nbButton size="small"
|
||||||
|
class="dialog-button"
|
||||||
|
status="info"
|
||||||
|
[disabled]="!allowConfirm()"
|
||||||
|
(click)="onClickConfirm()">
|
||||||
|
{{ 'global.action.confirm' | translate }}
|
||||||
|
</button>
|
||||||
|
<button nbButton size="small"
|
||||||
|
class="dialog-button"
|
||||||
|
(click)="onClickCancel()">
|
||||||
|
{{ 'global.action.cancel' | translate }}
|
||||||
|
</button>
|
||||||
|
</nb-card-footer>
|
||||||
|
</nb-card>
|
|
@ -0,0 +1,98 @@
|
||||||
|
@import "../../../assets/@theme/styles/_dialog.scss";
|
||||||
|
@import '../../../assets/@theme/styles/themes';
|
||||||
|
|
||||||
|
.profile-setting-dialog {
|
||||||
|
width: 45.25rem !important;
|
||||||
|
height: 46rem;
|
||||||
|
|
||||||
|
.dialog-header {
|
||||||
|
height: 8vh;
|
||||||
|
|
||||||
|
.header-text {
|
||||||
|
font-size: 1.5rem;
|
||||||
|
padding-left: 1rem;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.dialog-body {
|
||||||
|
|
||||||
|
.user-properties {
|
||||||
|
padding-top: 1rem;
|
||||||
|
|
||||||
|
.user-img {
|
||||||
|
width: 10rem;
|
||||||
|
height: 10rem;
|
||||||
|
border-radius: 50%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.user-img:hover {
|
||||||
|
cursor: not-allowed;
|
||||||
|
}
|
||||||
|
|
||||||
|
.properties {
|
||||||
|
padding-left: 2rem;
|
||||||
|
|
||||||
|
.prefix-icon {
|
||||||
|
margin-top: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.label {
|
||||||
|
display: block;
|
||||||
|
font-size: 0.95rem;
|
||||||
|
padding-bottom: 0.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-field {
|
||||||
|
margin-bottom: 0.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-field:disabled {
|
||||||
|
background-color: nb-theme(color-basic-transparent-focus);
|
||||||
|
}
|
||||||
|
|
||||||
|
.untouchable {
|
||||||
|
width: 30rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.error-text {
|
||||||
|
float: left;
|
||||||
|
color: nb-theme(color-danger-default);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.language-settings{
|
||||||
|
padding-top: 2rem;
|
||||||
|
|
||||||
|
.language-selection-label {
|
||||||
|
font-weight: bold;
|
||||||
|
}
|
||||||
|
|
||||||
|
.language-radio-buttons {
|
||||||
|
float: left;
|
||||||
|
clear: none;
|
||||||
|
padding-top: 0.5rem;
|
||||||
|
margin-left: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.languageContainer {
|
||||||
|
display: flex;
|
||||||
|
max-width: 8rem;
|
||||||
|
min-width: 8rem;
|
||||||
|
|
||||||
|
.flag {
|
||||||
|
object-fit: contain;
|
||||||
|
margin-top: 0.5rem;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.user-password-change{
|
||||||
|
padding-top: 1rem;
|
||||||
|
|
||||||
|
.password-selection-label {
|
||||||
|
font-weight: bold;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,65 @@
|
||||||
|
import { ComponentFixture, TestBed } from '@angular/core/testing';
|
||||||
|
|
||||||
|
import { ProfileSettingsComponent } from './profile-settings.component';
|
||||||
|
import {DialogService} from '@shared/services/dialog-service/dialog.service';
|
||||||
|
import {DialogServiceMock} from '@shared/services/dialog-service/dialog.service.mock';
|
||||||
|
import {NbDialogRef, NbFormFieldModule} from '@nebular/theme';
|
||||||
|
import {createSpyObj} from '@shared/modules/project-dialog/project-dialog.component.spec';
|
||||||
|
import {ReactiveFormsModule} from '@angular/forms';
|
||||||
|
import {BrowserAnimationsModule} from '@angular/platform-browser/animations';
|
||||||
|
import {ThemeModule} from '@assets/@theme/theme.module';
|
||||||
|
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 {NgxsModule} from '@ngxs/store';
|
||||||
|
import {SessionState} from '@shared/stores/session-state/session-state';
|
||||||
|
import {KeycloakService} from 'keycloak-angular';
|
||||||
|
|
||||||
|
describe('ProfileSettingsComponent', () => {
|
||||||
|
let component: ProfileSettingsComponent;
|
||||||
|
let fixture: ComponentFixture<ProfileSettingsComponent>;
|
||||||
|
|
||||||
|
beforeEach(async () => {
|
||||||
|
const dialogSpy = createSpyObj('NbDialogRef', ['close']);
|
||||||
|
|
||||||
|
await TestBed.configureTestingModule({
|
||||||
|
declarations: [
|
||||||
|
ProfileSettingsComponent
|
||||||
|
],
|
||||||
|
imports: [
|
||||||
|
NbFormFieldModule,
|
||||||
|
ReactiveFormsModule,
|
||||||
|
BrowserAnimationsModule,
|
||||||
|
ThemeModule.forRoot(),
|
||||||
|
TranslateModule.forRoot({
|
||||||
|
loader: {
|
||||||
|
provide: TranslateLoader,
|
||||||
|
useFactory: HttpLoaderFactory,
|
||||||
|
deps: [HttpClient]
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
NgxsModule.forRoot([SessionState]),
|
||||||
|
HttpClientModule,
|
||||||
|
HttpClientTestingModule
|
||||||
|
],
|
||||||
|
providers: [
|
||||||
|
{provide: DialogService, useClass: DialogServiceMock},
|
||||||
|
{provide: NbDialogRef, useValue: dialogSpy},
|
||||||
|
KeycloakService
|
||||||
|
]
|
||||||
|
})
|
||||||
|
.compileComponents();
|
||||||
|
});
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
fixture = TestBed.createComponent(ProfileSettingsComponent);
|
||||||
|
component = fixture.componentInstance;
|
||||||
|
// ToDo: fix detectChanges() when from control accessor is defined
|
||||||
|
// fixture.detectChanges();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should create', () => {
|
||||||
|
expect(component).toBeTruthy();
|
||||||
|
});
|
||||||
|
});
|
|
@ -0,0 +1,262 @@
|
||||||
|
import {ChangeDetectionStrategy, Component, Input, OnInit, Output} from '@angular/core';
|
||||||
|
import {NbDialogRef} from '@nebular/theme';
|
||||||
|
import * as FA from '@fortawesome/free-solid-svg-icons';
|
||||||
|
import {TranslateService} from '@ngx-translate/core';
|
||||||
|
import {AbstractControl, FormBuilder, FormControl, FormGroup, Validators} from '@angular/forms';
|
||||||
|
import {ProfilePasswordFormData} from '@shared/modules/profile-settings/password-input-from/util/profile-password-form-data.model';
|
||||||
|
import {Patterns} from '@shared/modules/profile-settings/patterns';
|
||||||
|
import {LanguageOptions} from '@shared/modules/export-report-dialog/export-report-dialog.component';
|
||||||
|
import {UntilDestroy, untilDestroyed} from '@ngneat/until-destroy';
|
||||||
|
import {User} from '@shared/models/user.model';
|
||||||
|
import {UserService} from '@shared/services/user-service/user.service';
|
||||||
|
import {BehaviorSubject, Observable, of} from 'rxjs';
|
||||||
|
import deepEqual from 'deep-equal';
|
||||||
|
import {UpdateUser, UpdateUserSettings} from '@shared/stores/session-state/session-state.actions';
|
||||||
|
import {mapTo, tap} from 'rxjs/operators';
|
||||||
|
import {Store} from '@ngxs/store';
|
||||||
|
import {SessionState} from '@shared/stores/session-state/session-state';
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
selector: 'app-profile-settings',
|
||||||
|
templateUrl: './profile-settings.component.html',
|
||||||
|
styleUrls: ['./profile-settings.component.scss']
|
||||||
|
})
|
||||||
|
@UntilDestroy()
|
||||||
|
export class ProfileSettingsComponent implements OnInit {
|
||||||
|
/**
|
||||||
|
* @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;
|
||||||
|
user: BehaviorSubject<User> = new BehaviorSubject<User>(null);
|
||||||
|
|
||||||
|
@Output() userServiceError: BehaviorSubject<boolean> = new BehaviorSubject<boolean>(false);
|
||||||
|
|
||||||
|
// HTML only
|
||||||
|
readonly fa = FA;
|
||||||
|
readonly USER_IMG = 'assets/images/demo/anon-user-icon.png';
|
||||||
|
|
||||||
|
// User form
|
||||||
|
userFormGroup: FormGroup;
|
||||||
|
userNameControl: AbstractControl;
|
||||||
|
userFirstNameControl: AbstractControl;
|
||||||
|
userLastNameControl: AbstractControl;
|
||||||
|
userEmailControl: AbstractControl;
|
||||||
|
oldUserData: UserProfileFormData;
|
||||||
|
|
||||||
|
// Password form
|
||||||
|
passwordFormGroup: FormGroup;
|
||||||
|
userPasswordInputControl: AbstractControl;
|
||||||
|
private readonly initialPasswordInput: ProfilePasswordFormData = {oldPassword: '', newPassword: '', confirmNewPassword: ''};
|
||||||
|
passwordStrong: boolean;
|
||||||
|
oldPasswordData: ProfilePasswordFormData;
|
||||||
|
|
||||||
|
|
||||||
|
// Language change
|
||||||
|
profileLanguageControl = new FormControl(LanguageOptions.ENGLISH);
|
||||||
|
profileLanguages = LanguageOptions;
|
||||||
|
|
||||||
|
constructor(protected dialogRef: NbDialogRef<any>,
|
||||||
|
private fb: FormBuilder,
|
||||||
|
private store: Store,
|
||||||
|
private userService: UserService,
|
||||||
|
private translateService: TranslateService) {
|
||||||
|
}
|
||||||
|
|
||||||
|
ngOnInit(): void {
|
||||||
|
this.profileLanguageControl.setValue(this.translateService.currentLang);
|
||||||
|
this.setupUserFormGroup();
|
||||||
|
// Load user profile
|
||||||
|
this.userService.loadUserProfile().pipe(
|
||||||
|
untilDestroyed(this)
|
||||||
|
).subscribe({
|
||||||
|
next: (user: User) => {
|
||||||
|
this.user.next(user);
|
||||||
|
this.userNameControl.setValue(user.username);
|
||||||
|
this.userFirstNameControl.setValue(user.firstName);
|
||||||
|
this.userLastNameControl.setValue(user.lastName);
|
||||||
|
console.warn(this.user.getValue());
|
||||||
|
},
|
||||||
|
error: err => {
|
||||||
|
console.error(err);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
this.oldUserData = Object.assign({}, this.userFormGroup.getRawValue());
|
||||||
|
// Setup password
|
||||||
|
this.setupPasswordFormGroup();
|
||||||
|
this.oldPasswordData = Object.assign({}, this.passwordFormGroup.getRawValue());
|
||||||
|
}
|
||||||
|
|
||||||
|
setupUserFormGroup(): void {
|
||||||
|
this.userFormGroup = this.fb.group({
|
||||||
|
username: [{value: '', disabled: true}, [Validators.required, Validators.pattern(Patterns.NO_WHITESPACES)]],
|
||||||
|
firstName: [{value: '', disabled: false}, [Validators.required, Validators.pattern(Patterns.NO_WHITESPACES)]],
|
||||||
|
lastName: [{value: '', disabled: false}, [Validators.required, Validators.pattern(Patterns.NO_WHITESPACES)]],
|
||||||
|
eMail: [{value: '', disabled: true}, [Validators.required, Validators.email, Validators.pattern(Patterns.NO_WHITESPACES)]],
|
||||||
|
avatarUploader: null
|
||||||
|
});
|
||||||
|
// Get form controls
|
||||||
|
this.userNameControl = this.userFormGroup.get('username');
|
||||||
|
this.userFirstNameControl = this.userFormGroup.get('firstName');
|
||||||
|
this.userLastNameControl = this.userFormGroup.get('lastName');
|
||||||
|
this.userEmailControl = this.userFormGroup.get('eMail');
|
||||||
|
}
|
||||||
|
|
||||||
|
setupPasswordFormGroup(): void {
|
||||||
|
this.passwordFormGroup = this.fb.group({
|
||||||
|
passwordInput: this.initialPasswordInput
|
||||||
|
});
|
||||||
|
// get password-form control
|
||||||
|
this.userPasswordInputControl = this.passwordFormGroup.get('passwordInput');
|
||||||
|
}
|
||||||
|
|
||||||
|
onClickLanguage(language: string): void {
|
||||||
|
this.translateService.use(language);
|
||||||
|
}
|
||||||
|
|
||||||
|
onClickConfirm(): void {
|
||||||
|
// ToDo: use handleUserUpdate() here
|
||||||
|
const userFormData: UserProfileFormData = this.userFormGroup.getRawValue();
|
||||||
|
// ToDo: use handlePasswordChange() here
|
||||||
|
const passwordFormData: ProfilePasswordFormData = this.passwordFormGroup.getRawValue();
|
||||||
|
// tslint:disable-next-line:no-console
|
||||||
|
console.info('User', userFormData);
|
||||||
|
// tslint:disable-next-line:no-console
|
||||||
|
console.info('Password', passwordFormData);
|
||||||
|
// ToDo: Fix?
|
||||||
|
if (this.allowChangeProfileData() && this.allowChangePassword()) {
|
||||||
|
const formAndUser: [UserProfileFormData, User] = this.extractFormAndUser();
|
||||||
|
this.handleUserUpdate(formAndUser['1']).subscribe({
|
||||||
|
complete: () => {
|
||||||
|
this.handlePasswordChange();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
// Changes occur only on the profile data without the password
|
||||||
|
else if (this.allowChangeProfileData()) {
|
||||||
|
const formAndUser: [UserProfileFormData, User] = this.extractFormAndUser();
|
||||||
|
this.handleUserUpdate(formAndUser['1']).subscribe({
|
||||||
|
complete: () => this.dialogRef.close({confirm: true})
|
||||||
|
});
|
||||||
|
}
|
||||||
|
// Changes occur only on the password
|
||||||
|
else if (this.allowChangePassword()) {
|
||||||
|
this.handlePasswordChange();
|
||||||
|
}
|
||||||
|
// this.dialogRef.close({confirm: true});
|
||||||
|
}
|
||||||
|
|
||||||
|
private handleUserUpdate(user: User): Observable<void> {
|
||||||
|
/* return this.userService.updateUser(user, this.timeOfChange)
|
||||||
|
.pipe(
|
||||||
|
tap({
|
||||||
|
next: resultingUser => {
|
||||||
|
this.store.dispatch(new UpdateUserSettings(resultingUser));
|
||||||
|
this.store.dispatch(new UpdateUser(resultingUser, true));
|
||||||
|
},
|
||||||
|
error: error => {
|
||||||
|
console.error(error);
|
||||||
|
this.onFailedUpdate(error);
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
mapTo(void 0),
|
||||||
|
untilDestroyed(this)
|
||||||
|
);*/
|
||||||
|
return of();
|
||||||
|
}
|
||||||
|
|
||||||
|
private handlePasswordChange(): void {
|
||||||
|
const formData = this.passwordFormGroup.getRawValue();
|
||||||
|
if (formData && formData.passwordInput) {
|
||||||
|
const oldPassword = formData.passwordInput.oldPassword;
|
||||||
|
const newPassword = formData.passwordInput.newPassword;
|
||||||
|
// ToDo: Fix connection to keycloak
|
||||||
|
/*this.userService.getCurrentAuthenticatedUser().pipe(
|
||||||
|
switchMap((currentUser: CognitoUser) => {
|
||||||
|
return this.userService.changePassword(currentUser, oldPassword, newPassword).pipe(
|
||||||
|
catchError((err) => {
|
||||||
|
if (err && 'message' in err) {
|
||||||
|
if (err.code === 'LimitExceededException') {
|
||||||
|
this.notificationService.showPopup('userProfile.password.limitExceeded', PopupType.FAILURE);
|
||||||
|
} else {
|
||||||
|
this.notificationService.showPopup('userProfile.password.invalidPassword', PopupType.FAILURE);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
this.userServiceError.next(true);
|
||||||
|
return throwError(err);
|
||||||
|
}),
|
||||||
|
tap(() => {
|
||||||
|
this.notificationService.showPopup('userProfile.password.changePasswordSuccess', PopupType.SUCCESS);
|
||||||
|
this.userDialogRef.close('confirm');
|
||||||
|
})
|
||||||
|
);
|
||||||
|
}),
|
||||||
|
).subscribe({
|
||||||
|
error: err => console.error(err)
|
||||||
|
});*/
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
onClickCancel(): void {
|
||||||
|
console.log(this.userFormGroup.get('firstName').dirty);
|
||||||
|
console.log(this.userFormGroup.get('firstName')?.hasError('required'));
|
||||||
|
console.log(this.userFirstNameControl.hasError('required'));
|
||||||
|
|
||||||
|
this.dialogRef.close();
|
||||||
|
}
|
||||||
|
|
||||||
|
extractFormAndUser(): [UserProfileFormData, User] {
|
||||||
|
const user: User = Object.assign(new User(), this.store.selectSnapshot(SessionState.userAccount));
|
||||||
|
const formData: UserProfileFormData = this.userFormGroup.getRawValue();
|
||||||
|
user.username = formData.username;
|
||||||
|
user.firstName = formData.firstName;
|
||||||
|
user.lastName = formData.lastName;
|
||||||
|
user.mailAddress = formData.email;
|
||||||
|
|
||||||
|
return [formData, user];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Checks for valid changes on both the change-profile-data and the change-password forms.
|
||||||
|
* @return true if we have one of the followings:
|
||||||
|
* 1. Valid changes on both forms
|
||||||
|
* 2. Valid changes on change-password form
|
||||||
|
* 3. Valid changes on change-profile-data form with empty old, new and confirm passwords
|
||||||
|
*/
|
||||||
|
allowConfirm(): boolean {
|
||||||
|
const userFormValid = this.allowChangeProfileData();
|
||||||
|
|
||||||
|
const passwordFormData: ProfilePasswordFormData = this.passwordFormGroup.getRawValue().passwordInput;
|
||||||
|
const passwordFormValid = this.allowChangePassword();
|
||||||
|
const passwordFormEmpty = (deepEqual(passwordFormData.oldPassword, '')
|
||||||
|
&& deepEqual(passwordFormData.newPassword, '')
|
||||||
|
&& deepEqual(passwordFormData.confirmNewPassword, ''));
|
||||||
|
|
||||||
|
return (userFormValid && passwordFormValid) || (passwordFormValid) || (userFormValid && passwordFormEmpty);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return true if the change-profile-data form is valid and there are actual changes.
|
||||||
|
*/
|
||||||
|
allowChangeProfileData(): boolean {
|
||||||
|
const formData: UserProfileFormData = this.userFormGroup.getRawValue();
|
||||||
|
return this.userFormGroup.dirty && this.userFormGroup.valid && !deepEqual(formData, this.oldUserData);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return true if the change-password form is valid and the password is strong.
|
||||||
|
*/
|
||||||
|
allowChangePassword(): boolean {
|
||||||
|
return this.passwordStrong && this.passwordFormGroup.valid && this.userFormGroup.dirty;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface UserProfileFormData {
|
||||||
|
username: string;
|
||||||
|
firstName: string;
|
||||||
|
lastName: string;
|
||||||
|
email: string;
|
||||||
|
avatarUploader: FileList;
|
||||||
|
}
|
|
@ -0,0 +1,35 @@
|
||||||
|
import {NgModule} from '@angular/core';
|
||||||
|
import {CommonModule} from '@angular/common';
|
||||||
|
import {ProfileSettingsComponent} from '@shared/modules/profile-settings/profile-settings.component';
|
||||||
|
import {NbButtonModule, NbCardModule, NbFormFieldModule, NbIconModule, NbInputModule, NbRadioModule, NbSelectModule} from '@nebular/theme';
|
||||||
|
import {TranslateModule} from '@ngx-translate/core';
|
||||||
|
import {FlexLayoutModule} from '@angular/flex-layout';
|
||||||
|
import {FontAwesomeModule} from '@fortawesome/angular-fontawesome';
|
||||||
|
import {ReactiveFormsModule} from '@angular/forms';
|
||||||
|
import { PasswordInputFromComponent } from './password-input-from/password-input-from.component';
|
||||||
|
|
||||||
|
@NgModule({
|
||||||
|
declarations: [
|
||||||
|
ProfileSettingsComponent,
|
||||||
|
PasswordInputFromComponent
|
||||||
|
],
|
||||||
|
imports: [
|
||||||
|
CommonModule,
|
||||||
|
NbCardModule,
|
||||||
|
TranslateModule,
|
||||||
|
FlexLayoutModule,
|
||||||
|
NbButtonModule,
|
||||||
|
FontAwesomeModule,
|
||||||
|
NbSelectModule,
|
||||||
|
ReactiveFormsModule,
|
||||||
|
NbFormFieldModule,
|
||||||
|
NbInputModule,
|
||||||
|
NbRadioModule,
|
||||||
|
NbIconModule
|
||||||
|
],
|
||||||
|
exports: [
|
||||||
|
ProfileSettingsComponent
|
||||||
|
]
|
||||||
|
})
|
||||||
|
export class ProfileSettingsModule {
|
||||||
|
}
|
|
@ -5,6 +5,7 @@ import {DialogMessage, SecurityDialogMessage} from '@shared/services/dialog-serv
|
||||||
import {ConfirmDialogComponent} from '@shared/modules/confirm-dialog/confirm-dialog.component';
|
import {ConfirmDialogComponent} from '@shared/modules/confirm-dialog/confirm-dialog.component';
|
||||||
import {SecurityConfirmDialogComponent} from '@shared/modules/security-confirm-dialog/security-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';
|
import {RetryDialogComponent} from '@shared/modules/retry-dialog/retry-dialog.component';
|
||||||
|
import {any} from 'codelyzer/util/function';
|
||||||
|
|
||||||
@Injectable({
|
@Injectable({
|
||||||
providedIn: 'root'
|
providedIn: 'root'
|
||||||
|
@ -19,14 +20,15 @@ export class DialogService {
|
||||||
*/
|
*/
|
||||||
openCustomDialog<T>(
|
openCustomDialog<T>(
|
||||||
componentOrTemplateRef: ComponentType<T> | TemplateRef<T>,
|
componentOrTemplateRef: ComponentType<T> | TemplateRef<T>,
|
||||||
config?: Partial<NbDialogConfig<Partial<T> | string>>
|
additionalData: any
|
||||||
): NbDialogRef<T> {
|
): NbDialogRef<T> {
|
||||||
return this.dialog.open<T>(componentOrTemplateRef, {
|
return this.dialog.open<T>(componentOrTemplateRef, {
|
||||||
context: config?.context || undefined,
|
closeOnEsc: false,
|
||||||
closeOnEsc: config?.closeOnEsc || false,
|
hasScroll: false,
|
||||||
hasScroll: config?.hasScroll || false,
|
autoFocus: true,
|
||||||
autoFocus: config?.autoFocus || true,
|
closeOnBackdropClick: false,
|
||||||
closeOnBackdropClick: config?.closeOnBackdropClick || false
|
// @ts-ignore
|
||||||
|
context: {data: additionalData}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -16,7 +16,7 @@
|
||||||
min-height: 6rem;
|
min-height: 6rem;
|
||||||
|
|
||||||
.header-title {
|
.header-title {
|
||||||
@include multiLineEllipsis($font-size: 1.5rem, $font-weight: bold, $line-height: 1.5rem, $lines-to-show: 2, $max-width: 14rem);
|
@include multiLineEllipsis($font-size: 1.5rem, $font-weight: bold, $line-height: 2rem, $lines-to-show: 4, $max-width: 14rem);
|
||||||
}
|
}
|
||||||
|
|
||||||
.state-tag {
|
.state-tag {
|
||||||
|
|
Loading…
Reference in New Issue