feat: As a tester I want to edit my profile

This commit is contained in:
Marcel Haag 2023-04-12 14:49:34 +02:00 committed by Cel
parent e0e23f7383
commit b4bf6de8f8
34 changed files with 2833 additions and 1527 deletions

View File

@ -28,9 +28,12 @@
"src/assets"
],
"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": [
"buffer",
"crypto-js/hmac-sha256",

File diff suppressed because it is too large Load Diff

View File

@ -42,6 +42,7 @@
"keycloak-js": "^13.0.1",
"moment": "^2.29.1",
"moment-timezone": "latest",
"font-awesome": "^4.7.0",
"ng-mocks": "^13.4.2",
"ngx-moment": "^5.0.0",
"ngx-take-until-destroy": "^5.4.0",
@ -58,12 +59,12 @@
"@angular/cli": "^12.2.16",
"@angular/compiler-cli": "~12.2.16",
"@babel/preset-typescript": "^7.18.6",
"@briebug/jest-schematic": "^3.0.0",
"@fortawesome/fontawesome-free": "^6.4.0",
"@schematics/angular": "^10.2.4",
"@types/jest": "28.1.1",
"@types/node": "^12.20.47",
"@briebug/jest-schematic": "^3.0.0",
"codelyzer": "^6.0.2",
"font-awesome": "^4.7.0",
"jest": "28.1.1",
"protractor": "~7.0.0",
"ts-node": "~8.3.0",

View File

@ -9,6 +9,8 @@ import {SessionState, SessionStateModel} from '@shared/stores/session-state/sess
import {UntilDestroy, untilDestroyed} from '@ngneat/until-destroy';
import {isNotNullOrUndefined} from 'codelyzer/util/isNotNullOrUndefined';
import {filter} from 'rxjs/operators';
import {NbIconLibraries} from '@nebular/theme';
import {FaIconLibrary} from '@fortawesome/angular-fontawesome';
@UntilDestroy()
@Component({
@ -24,6 +26,8 @@ export class AppComponent implements OnInit, OnDestroy {
constructor(private translateService: TranslateService,
private store: Store,
private iconLibraries: FaIconLibrary,
private nebularIconLibraries: NbIconLibraries,
@Inject(LOCALE_ID) private localeId: string) {
this.initApp();
}
@ -44,10 +48,14 @@ export class AppComponent implements OnInit, OnDestroy {
initApp(): void {
// for global language
this.translateService.use(this.localeId);
// for number, date and time
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();
}

View File

@ -12,15 +12,12 @@ import {
NbSelectModule,
NbThemeModule,
NbOverlayContainerAdapter,
NbDialogModule, NbMenuModule,
NbDialogModule, NbMenuModule, NbIconLibraries,
} from '@nebular/theme';
import {TranslateLoader, TranslateModule} from '@ngx-translate/core';
import {HttpClient, HttpClientModule} from '@angular/common/http';
import {HttpLoaderFactory} from './common-app.module';
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 {SessionState} from '@shared/stores/session-state/session-state';
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 {FormsModule, ReactiveFormsModule} from '@angular/forms';
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({
declarations: [
@ -94,9 +94,11 @@ import {RetryDialogModule} from '@shared/modules/retry-dialog/retry-dialog.modul
]
})
export class AppModule {
constructor(library: FaIconLibrary, faConfig: FaConfig) {
library.addIconPacks(fas, far);
constructor(library: FaIconLibrary, faConfig: FaConfig, libraries: NbIconLibraries) {
library.addIconPacks(far, fas);
libraries.registerFontPack('solid', {packClass: 'fas', iconClassPrefix: 'fa'});
faConfig.defaultPrefix = 'fas';
libraries.setDefaultPack('solid');
}
}

View File

@ -2,7 +2,8 @@
<img *ngIf="currentTheme === 'corporate', else changeImage"
src="../../assets/images/favicons/favicon.ico" alt="logo dark" class="header-icon" width="60rem" height="60rem">
<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>
<div class="logo-container">
@ -11,41 +12,31 @@
<div class="filler"></div>
<div fxLayoutGap="4rem">
<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-->
<nb-action class="toggle-theme">
<button nbButton
(click)="onClickGoToLink('https://owasp.org/www-project-web-security-testing-guide/v42/')">
<fa-icon [icon]="fa.faFileInvoice" class="new-element-icon" href="https://www.google.com">
<nb-action>
<div (click)="onClickSwitchTheme()" class="action-element-icon">
<fa-icon *ngIf="currentTheme === 'corporate', else changeIcon"
title="Darktheme" [icon]="fa.faMoon" class="fa-2x">
</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>
<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>
</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>
</nb-action>
<!--User Action-->
<nb-action class="user">
<!--<fa-icon [icon]="fa.faUser" class="user-icon">
</fa-icon>-->
<nb-action class="user-action">
<nb-user [nbContextMenu]="userMenu"
[picture]="FALLBACK_IMG"
name="{{user?.getValue()?.username}}"

View File

@ -8,17 +8,26 @@
flex-grow: 1;
}
.action-element-icon:hover {
cursor: pointer;
}
.logo-container {
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.owasp-redirect-button {
margin-left: 0.5rem;
}
.languageContainer {
display: flex;
max-width: 8rem;
min-width: 8rem;
.user-action {
// width: 4rem;
z-index: 10;
// height: 3rem;
.user-action-accordion-header {
.flag {
object-fit: contain;
}
}
}
@ -33,14 +42,6 @@
align-items: center;
width: auto;
.logo-container {
font-style: oblique;
color: #e74c3c;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
nb-action {
height: auto;
display: flex;

View File

@ -14,6 +14,8 @@ 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';
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 = {
userAccount: {
@ -52,6 +54,7 @@ describe('HeaderComponent', () => {
NgxsModule.forRoot([SessionState])
],
providers: [
{provide: DialogService, useClass: DialogServiceMock},
NbMenuService,
KeycloakService
]

View File

@ -1,7 +1,7 @@
import {Component, OnInit} from '@angular/core';
import * as FA from '@fortawesome/free-solid-svg-icons';
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 {TranslateService} from '@ngx-translate/core';
import {UntilDestroy, untilDestroyed} from '@ngneat/until-destroy';
@ -14,6 +14,8 @@ import {BehaviorSubject} from 'rxjs';
import {Route} from '@shared/models/route.enum';
import {environment} from '../../environments/environment';
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({
selector: 'app-header',
@ -26,18 +28,21 @@ export class HeaderComponent implements OnInit {
// HTML only
readonly fa = FA;
readonly SECURITYC4PO_TITLE: string = GlobalTitlesVariables.SECURITYC4PO_TITLE;
// Menu only
readonly settingsIcon = 'gear';
readonly logoutIcon = 'right-from-bracket';
currentTheme = '';
languages = ['en-US', 'de-DE'];
selectedLanguage = '';
// User Menu Properties
userPictureOnly = false;
user: BehaviorSubject<User> = new BehaviorSubject<User>(null);
userMenu: NbMenuItem[] = [
{
title: '',
pathMatch: 'prefix'
title: 'settings',
icon: { icon: this.settingsIcon, pack: 'fas' }
},
{
title: 'logout',
icon: { icon: this.logoutIcon, pack: 'fas'}
}
];
readonly FALLBACK_IMG = 'assets/images/demo/anon-user-icon.png';
@ -47,6 +52,7 @@ export class HeaderComponent implements OnInit {
private router: Router,
private themeService: NbThemeService,
private translateService: TranslateService,
private dialogService: DialogService,
private menuService: NbMenuService,
private userService: UserService,
protected keycloakService: KeycloakService) {
@ -60,7 +66,6 @@ export class HeaderComponent implements OnInit {
untilDestroyed(this),
).subscribe(themeName => this.currentTheme = themeName);
this.selectedLanguage = this.translateService.currentLang;
// Load user profile
this.userService.loadUserProfile().pipe(
untilDestroyed(this)
@ -78,16 +83,44 @@ export class HeaderComponent implements OnInit {
untilDestroyed(this)
)
.subscribe((menuBag) => {
if (menuBag.item.pathMatch === 'prefix') {
this.onClickLogOut();
// Makes sure that other menus without icon won't trigger
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
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')
.pipe(
untilDestroyed(this)
).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');
}
onClickShowInfo(): void {
console.info('To be implemented..');
}
onClickSwitchTheme(): void {
if (this.currentTheme === 'corporate') {
this.themeService.changeTheme('dark');
@ -120,8 +157,4 @@ export class HeaderComponent implements OnInit {
console.error(err);
});*/
}
onClickLanguage(language: string): void {
this.translateService.use(language);
}
}

View File

@ -5,13 +5,14 @@ import {
NbActionsModule,
NbButtonModule,
NbCardModule,
NbContextMenuModule, NbMenuModule,
NbContextMenuModule,
NbSelectModule,
NbUserModule
} from '@nebular/theme';
import {FontAwesomeModule} from '@fortawesome/angular-fontawesome';
import {FlexLayoutModule} from '@angular/flex-layout';
import {TranslateModule} from '@ngx-translate/core';
import {ProfileSettingsModule} from '@shared/modules/profile-settings/profile-settings.module';
@NgModule({
declarations: [
@ -30,7 +31,8 @@ import {TranslateModule} from '@ngx-translate/core';
NbSelectModule,
TranslateModule,
NbUserModule,
NbContextMenuModule
NbContextMenuModule,
ProfileSettingsModule,
],
providers: [
]

View File

@ -42,7 +42,7 @@
<!--Actions for mobile devices-->
<nb-actions size="medium" fxHide fxShow.lt-lg>
<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-actions>
</div>

View File

@ -29,19 +29,18 @@ import {TranslateService} from '@ngx-translate/core';
export class ObjectiveHeaderComponent implements OnInit {
selectedProject$: BehaviorSubject<Project> = new BehaviorSubject<Project>(null);
// Menu only
readonly editIcon = 'edit';
readonly fileExportIcon = 'file-export';
// Mobile menu properties
objectiveActionItems: NbMenuItem[] = [
{
title: 'global.action.edit',
badge: {
status: 'warning'
}
icon: { icon: this.editIcon, pack: 'fas' }
},
{
title: 'global.action.report',
badge: {
status: 'info'
}
icon: { icon: this.fileExportIcon, pack: 'fas' }
},
];
// HTML only
@ -83,10 +82,16 @@ export class ObjectiveHeaderComponent implements OnInit {
untilDestroyed(this)
)
.subscribe((menuBag) => {
if (menuBag.item.badge && menuBag.item.badge.status === 'warning') {
this.onClickEditPentestProject();
} else if (menuBag.item.badge && menuBag.item.badge.status === 'info') {
this.onClickGeneratePentestReport();
// Makes sure that other menus without icon won't trigger
if (menuBag.item.icon) {
// tslint:disable-next-line:no-string-literal
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

View File

@ -5,6 +5,7 @@
.content {
height: 95%;
overflow: auto !important;
// overflow: hidden !important;
// ToDo: Fixes tab header but also disables scrolling for content
/*nb-tab {

View File

@ -2,8 +2,10 @@
<h4>
{{ getPentestHeaderForObjective(pentestInfo$.getValue().refNumber) | translate}}
</h4>
<p class="description">
{{ getPentestInfoForObjective(pentestInfo$.getValue().refNumber) | translate }}
</p>
<div class="description">
<div>
{{ getPentestInfoForObjective(pentestInfo$.getValue().refNumber) | translate }}
</div>
</div>
<!--ToDo: Add tooling hints after description (maybe in pentest-header component)-->
</div>

View File

@ -1,10 +1,14 @@
.pentest-info {
overflow-y: scroll;
overflow-x: hidden;
// overflow: auto !important;
overflow: hidden !important;
position: relative !important;
.description {
// ToDo: Make only description scrollable
// Scrollbar
overflow-y: scroll !important;
overflow-x: hidden;
scroll-behavior: smooth;
width: 60vw;
font-size: 1rem;
white-space: pre-line;

View File

@ -1,6 +1,7 @@
{
"global": {
"actions": "Aktionen",
"action.profile": "Profil",
"action.login": "Einloggen",
"action.logout": "Ausloggen",
"action.retry": "Erneut Versuchen",
@ -56,6 +57,35 @@
"failed": "Benutzername oder Passwort falsch",
"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": {
"new": "Neu",
"needs_more_info": "Benötigt mehr Informationen",
@ -64,7 +94,7 @@
"triaged": "Ausstehend",
"retesting": "Erneutes Testen",
"resolved": "Aufgeklärt",
"informative": "Informatif",
"informative": "Informativ",
"duplicate": "Duplikat",
"not_applicable": "Unzutreffend",
"spam": "Spam",

View File

@ -1,6 +1,7 @@
{
"global": {
"actions": "Actions",
"action.profile": "Profile",
"action.login": "Login",
"action.logout": "Logout",
"action.retry": "Try again",
@ -56,6 +57,35 @@
"failed": "Wrong username or password",
"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": {
"new": "New",
"needs_more_info": "Needs More Info",

View File

@ -31,7 +31,6 @@
float: left;
clear: none;
margin-left: 1rem;
}
nb-form-field {

View File

@ -32,15 +32,14 @@ export class ExportReportDialogComponent implements OnInit {
private dialogService: DialogService
) {
}
// HTML
readonly fa = FA;
// form control elements
exportReportFormatControl = new FormControl(ExportFormatOptions.PDF);
exportReportLanguageControl = new FormControl(ExportLanguageOptions.ENGLISH);
exportReportLanguageControl = new FormControl(LanguageOptions.ENGLISH);
// exports
exportFormats = ExportFormatOptions;
exportLanguages = ExportLanguageOptions;
exportLanguages = LanguageOptions;
dialogData: GenericDialogData;
@ -159,7 +158,7 @@ export enum ExportFormatOptions {
HTML = 'HTML'
}
export enum ExportLanguageOptions {
export enum LanguageOptions {
ENGLISH = 'en-US',
GERMAN = 'de-DE'
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -0,0 +1,5 @@
export interface ProfilePasswordFormData {
oldPassword: string;
newPassword: string;
confirmNewPassword: string;
}

View File

@ -0,0 +1,4 @@
export const Patterns = {
NOT_BLANK: '^(?!(?:\\s+)$).*$',
NO_WHITESPACES: '^(?!(?:\\s+)$).*$'
};

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -5,6 +5,7 @@ import {DialogMessage, SecurityDialogMessage} from '@shared/services/dialog-serv
import {ConfirmDialogComponent} from '@shared/modules/confirm-dialog/confirm-dialog.component';
import {SecurityConfirmDialogComponent} from '@shared/modules/security-confirm-dialog/security-confirm-dialog.component';
import {RetryDialogComponent} from '@shared/modules/retry-dialog/retry-dialog.component';
import {any} from 'codelyzer/util/function';
@Injectable({
providedIn: 'root'
@ -19,14 +20,15 @@ export class DialogService {
*/
openCustomDialog<T>(
componentOrTemplateRef: ComponentType<T> | TemplateRef<T>,
config?: Partial<NbDialogConfig<Partial<T> | string>>
additionalData: any
): NbDialogRef<T> {
return this.dialog.open<T>(componentOrTemplateRef, {
context: config?.context || undefined,
closeOnEsc: config?.closeOnEsc || false,
hasScroll: config?.hasScroll || false,
autoFocus: config?.autoFocus || true,
closeOnBackdropClick: config?.closeOnBackdropClick || false
closeOnEsc: false,
hasScroll: false,
autoFocus: true,
closeOnBackdropClick: false,
// @ts-ignore
context: {data: additionalData}
});
}

View File

@ -16,7 +16,7 @@
min-height: 6rem;
.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 {