import {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 = new BehaviorSubject(null); @Output() userServiceError: BehaviorSubject = new BehaviorSubject(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, 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); this.userEmailControl.setValue(user.email ? user.email : null); }, 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: true}, [Validators.required, Validators.pattern(Patterns.NO_WHITESPACES)]], lastName: [{value: '', disabled: true}, [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'); } changePasswordInKeycloak(): void { this.userService.redirectToChangePasswordAction().then(r => { // tslint:disable-next-line:no-console console.info('Redirecting to Keycloak for password change...'); }); } onClickLanguage(language: string): void { this.translateService.use(language); // ToDo: Update userAccount language property const user = this.store.selectSnapshot(SessionState.userAccount); this.store.dispatch(new UpdateUserSettings({...user, interfaceLang: 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 { return this.userService.changeUserProperties(user) .pipe( tap({ next: resultingUser => { this.store.dispatch(new UpdateUserSettings(resultingUser)); this.store.dispatch(new UpdateUser(resultingUser, true)); }, error: error => { console.error(error); } }), mapTo(void 0), untilDestroyed(this) ); } 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 { 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.email = 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; }