diff --git a/src/frontend/app/administration/user/user.component.html b/src/frontend/app/administration/user/user.component.html index 950d65dfb67a617e371126733bf999c15841a38c..b3ac6e0056c445980aba4ef68eb9a2ced14656ef 100644 --- a/src/frontend/app/administration/user/user.component.html +++ b/src/frontend/app/administration/user/user.component.html @@ -1,50 +1,78 @@ <mat-sidenav-container autosize> - <mat-sidenav #snav [disableClose]="!signaturesService.mobileMode" [mode]="signaturesService.mobileMode ? 'over': 'side'" fixedInViewport="true" - [opened]="!signaturesService.mobileMode" [style.width.px]="350"> - <app-admin-sidebar [snavLeftComponent]="this.snav" [snavRightComponent]="this.snavRight"></app-admin-sidebar> - </mat-sidenav> - <mat-sidenav-content class="mainView"> - <header class="header"> - <div class="header-title"> - <button *ngIf="signaturesService.mobileMode" mat-icon-button (click)="this.snav.toggle();"> - <mat-icon fontSet="fas" fontIcon="fa-bars" style="font-size: 24px;"></mat-icon> - </button> - <span *ngIf="!loading">{{title}}</span> - </div> - <div *ngIf="!signaturesService.smartphoneMode" style="padding-right: 10px;"> - <div *ngIf="!loading && !creationMode" class="avatarProfile" - [ngStyle]="{'background': 'url(' + user.picture + ') no-repeat scroll center center / cover'}"> - </div> - </div> - </header> - <div class="container"> - <div *ngIf="loading" class="loader"> - <mat-spinner></mat-spinner> - </div> - <form class="admin-form" *ngIf="!loading" (ngSubmit)="onSubmit()" #adminForm="ngForm"> - <mat-form-field class="input-row"> - <input name="login" matInput placeholder="{{'lang.login' | translate}}" type="text" [(ngModel)]="user.login" - oninput="this.value = this.value.toLowerCase()" [disabled]="!creationMode" required pattern="^[\w.@-]*$"> - </mat-form-field> - <mat-form-field class="input-row"> - <input name="firstname" matInput placeholder="{{'lang.firstname' | translate}}" [(ngModel)]="user.firstname" - required> - </mat-form-field> - <mat-form-field class="input-row"> - <input name="nom" matInput placeholder="{{'lang.lastname' | translate}}" [(ngModel)]="user.lastname" required> - </mat-form-field> - <mat-form-field class="input-row"> - <input name="email" matInput placeholder="{{'lang.email' | translate}}" type="mail" [(ngModel)]="user.email" - pattern="(^[a-zA-Z0-9_.+-]+@[a-zA-Z0-9-]+\.[a-zA-Z0-9-.]+$)" required> - </mat-form-field> - <div class="actions-form"> - <button mat-stroked-button type="submit" class="btn blue" [disabled]="!adminForm.form.valid || !canValidate()">{{'lang.validate' | translate}}</button> - <button mat-stroked-button type="button" class="btn" (click)="cancel()">{{'lang.cancel' | translate}}</button> - <button *ngIf="!creationMode" mat-stroked-button type="button" class="btn red" (click)="delete()">{{'lang.delete' | translate}}</button> - </div> - </form> + <mat-sidenav #snav [disableClose]="!signaturesService.mobileMode" + [mode]="signaturesService.mobileMode ? 'over': 'side'" fixedInViewport="true" + [opened]="!signaturesService.mobileMode" [style.width.px]="350"> + <app-admin-sidebar [snavLeftComponent]="this.snav" [snavRightComponent]="this.snavRight"></app-admin-sidebar> + </mat-sidenav> + <mat-sidenav-content class="mainView"> + <header class="header"> + <div class="header-title"> + <button *ngIf="signaturesService.mobileMode" mat-icon-button (click)="this.snav.toggle();"> + <mat-icon fontSet="fas" fontIcon="fa-bars" style="font-size: 24px;"></mat-icon> + </button> + <span *ngIf="!loading">{{title}}</span> + </div> + <div *ngIf="!signaturesService.smartphoneMode" style="padding-right: 10px;"> + <div *ngIf="!loading && !creationMode" class="avatarProfile" + [ngStyle]="{'background': 'url(' + user.picture + ') no-repeat scroll center center / cover'}"> </div> - </mat-sidenav-content> - <mat-sidenav #snavRight disableClose [mode]="signaturesService.mobileMode ? 'over': 'side'" [opened]="false" fixedInViewport="true" position='end'> - </mat-sidenav> + </div> + </header> + <div class="container"> + <div *ngIf="loading" class="loader"> + <mat-spinner></mat-spinner> + </div> + <form class="admin-form" *ngIf="!loading" (ngSubmit)="onSubmit()" #adminForm="ngForm"> + <mat-form-field class="input-row"> + <input name="login" matInput placeholder="{{'lang.login' | translate}}" type="text" [(ngModel)]="user.login" + oninput="this.value = this.value.toLowerCase()" [disabled]="!creationMode" required pattern="^[\w.@-]*$"> + </mat-form-field> + <mat-form-field class="input-row"> + <input name="firstname" matInput placeholder="{{'lang.firstname' | translate}}" [(ngModel)]="user.firstname" + required> + </mat-form-field> + <mat-form-field class="input-row"> + <input name="nom" matInput placeholder="{{'lang.lastname' | translate}}" [(ngModel)]="user.lastname" required> + </mat-form-field> + <mat-form-field class="input-row"> + <input name="email" matInput placeholder="{{'lang.email' | translate}}" type="mail" [(ngModel)]="user.email" + pattern="(^[a-zA-Z0-9_.+-]+@[a-zA-Z0-9-]+\.[a-zA-Z0-9-.]+$)" required> + </mat-form-field> + <mat-checkbox [disabled]="!creationMode" name="isRest" color="primary" [(ngModel)]="user.isRest" [checked]="user.isRest" (change)="getPassRules($event)">Utilisateur REST</mat-checkbox> + <mat-form-field class="input-row" *ngIf="user.isRest"> + <input name="newPasswordRest" matInput [(ngModel)]="passwordRest.newPassword" + placeholder="{{'lang.newPassword' | translate}}" [type]="hideNewPassword ? 'password' : 'text'" + (keyup)="checkPasswordValidity(passwordRest.newPassword)"> + <mat-icon matSuffix (click)="hideNewPassword = !hideNewPassword" class="fa fa-2x" + [ngClass]="[hideNewPassword ? 'fa-eye-slash' : 'fa-eye']"></mat-icon> + <mat-hint style="color:red;">{{handlePassword.errorMsg}}</mat-hint> + </mat-form-field> + <mat-form-field class="input-row" *ngIf="user.isRest"> + <input name="passwordConfirmation" matInput + [(ngModel)]="passwordRest.passwordConfirmation" + placeholder="{{'lang.passwordConfirmation' | translate}}" + [type]="hideNewPasswordConfirm ? 'password' : 'text'"> + <mat-icon matSuffix (click)="hideNewPasswordConfirm = !hideNewPasswordConfirm" + class="fa fa-2x" + [ngClass]="[hideNewPasswordConfirm ? 'fa-eye-slash' : 'fa-eye']"></mat-icon> + <mat-hint style="color:red;" + *ngIf="passwordRest.passwordConfirmation !== passwordRest.newPassword"> + {{'lang.passwordNotMatch' | translate}}</mat-hint> + <mat-hint style="color:green;" + *ngIf="passwordRest.passwordConfirmation === passwordRest.newPassword && passwordRest.newPassword.length > 0 && passwordRest.passwordConfirmation.length> 0"> + {{'lang.samePassword' | translate}}</mat-hint> + </mat-form-field> + + <div class="actions-form"> + <button mat-stroked-button type="submit" class="btn blue" + [disabled]="!adminForm.form.valid || !canValidate()">{{'lang.validate' | translate}}</button> + <button mat-stroked-button type="button" class="btn" (click)="cancel()">{{'lang.cancel' | translate}}</button> + <button mat-stroked-button type="button" *ngIf="!creationMode" class="btn red" (click)="delete()">{{'lang.delete' | translate}}</button> + </div> + </form> + </div> + </mat-sidenav-content> + <mat-sidenav #snavRight disableClose [mode]="signaturesService.mobileMode ? 'over': 'side'" [opened]="false" + fixedInViewport="true" position='end'> + </mat-sidenav> </mat-sidenav-container> diff --git a/src/frontend/app/administration/user/user.component.ts b/src/frontend/app/administration/user/user.component.ts index 27b2d64c10486c9652c55301fe193c0c98604b93..f2fff2004ec9b968b9d6ebdd0d4b5a861569393c 100644 --- a/src/frontend/app/administration/user/user.component.ts +++ b/src/frontend/app/administration/user/user.component.ts @@ -1,12 +1,13 @@ import { Component, OnInit, ViewChild } from '@angular/core'; import { SignaturesContentService } from '../../service/signatures.service'; import { NotificationService } from '../../service/notification.service'; -import { HttpClient } from '@angular/common/http'; +import { HttpClient, HttpHeaders } from '@angular/common/http'; import { MatDialog } from '@angular/material'; import { map, tap, finalize } from 'rxjs/operators'; import { ActivatedRoute, Router } from '@angular/router'; import { ConfirmComponent } from '../../plugins/confirm.component'; import { TranslateService } from '@ngx-translate/core'; +import { AuthService } from '../../service/auth.service'; export interface User { @@ -16,6 +17,7 @@ export interface User { login: string; email: string; picture: string; + isRest: boolean; } @Component({ @@ -31,8 +33,34 @@ export class UserComponent implements OnInit { user: User; userClone: User; title: string = ''; + hideCurrentPassword: Boolean = true; + hideNewPassword: Boolean = true; + hideNewPasswordConfirm: Boolean = true; - constructor(public http: HttpClient, private translate: TranslateService, private route: ActivatedRoute, private router: Router, public signaturesService: SignaturesContentService, public notificationService: NotificationService, public dialog: MatDialog) { + // HANDLE PASSWORD + passwordRules: any = { + minLength: { enabled: false, value: 0 }, + complexityUpper: { enabled: false, value: 0 }, + complexityNumber: { enabled: false, value: 0 }, + complexitySpecial: { enabled: false, value: 0 }, + renewal: { enabled: false, value: 0 }, + historyLastUse: { enabled: false, value: 0 }, + }; + passwordRest: any = { + newPassword: '', + passwordConfirmation: '' + }; + + ruleText = ''; + otherRuleText = ''; + + showPassword = false; + handlePassword: any = { + error: false, + errorMsg: '' + }; + + constructor(public http: HttpClient, private translate: TranslateService, private route: ActivatedRoute, private router: Router, public signaturesService: SignaturesContentService, public notificationService: NotificationService, public dialog: MatDialog, public authService: AuthService) { } ngOnInit(): void { @@ -46,7 +74,8 @@ export class UserComponent implements OnInit { lastname: '', login: '', email: '', - picture: '' + picture: '', + isRest: true }; this.loading = false; } else { @@ -59,8 +88,14 @@ export class UserComponent implements OnInit { .subscribe({ next: data => { this.user = data; + + // FOR TEST + this.user.isRest = true; this.userClone = JSON.parse(JSON.stringify(this.user)); this.title = this.user.firstname + ' ' + this.user.lastname; + if (this.user.isRest) { + this.getPassRules({ checked: true }); + } }, }); } @@ -68,7 +103,9 @@ export class UserComponent implements OnInit { } canValidate() { - if (JSON.stringify(this.user) === JSON.stringify(this.userClone)) { + if (this.user.isRest && this.passwordRest.newPassword !== '' && (this.handlePassword.error || this.passwordRest.passwordConfirmation !== this.passwordRest.newPassword)) { + return false; + } else if (JSON.stringify(this.user) === JSON.stringify(this.userClone) && this.passwordRest.newPassword === '') { return false; } else { return true; @@ -91,12 +128,28 @@ export class UserComponent implements OnInit { ) .subscribe({ next: () => { + if (this.passwordRest.newPassword !== '') { + this.updateRestUser(); + } this.router.navigate(['/administration/users']); this.notificationService.success('lang.userUpdated'); }, }); } + updateRestUser() { + const headers = new HttpHeaders({ + 'Authorization': 'Bearer ' + this.authService.getToken() + }); + this.http.put('../rest/users/' + this.user.id + '/password', this.passwordRest, { headers: headers }) + .subscribe(() => { + this.passwordRest.newPassword = ''; + this.passwordRest.passwordConfirmation = ''; + }, (err) => { + this.notificationService.handleErrors(err); + }); + } + createUser() { this.loading = true; this.http.post('../rest/users', this.user) @@ -134,4 +187,90 @@ export class UserComponent implements OnInit { cancel() { this.router.navigate(['/administration/users']); } + + getPassRules(ev: any) { + if (ev.checked) { + this.handlePassword.error = false; + this.handlePassword.errorMsg = ''; + + this.http.get('../rest/passwordRules') + .subscribe((data: any) => { + const ruleTextArr: String[] = []; + const otherRuleTextArr: String[] = []; + + data.rules.forEach((rule: any) => { + if (rule.label === 'minLength') { + this.passwordRules.minLength.enabled = rule.enabled; + this.passwordRules.minLength.value = rule.value; + if (rule.enabled) { + this.translate.get('lang.minLengthChar', { charLength: rule.value }).subscribe((res: string) => { + ruleTextArr.push(res); + }); + } + + } else if (rule.label === 'complexityUpper') { + this.passwordRules.complexityUpper.enabled = rule.enabled; + this.passwordRules.complexityUpper.value = rule.value; + if (rule.enabled) { + ruleTextArr.push('lang.upperRequired'); + } + + } else if (rule.label === 'complexityNumber') { + this.passwordRules.complexityNumber.enabled = rule.enabled; + this.passwordRules.complexityNumber.value = rule.value; + if (rule.enabled) { + ruleTextArr.push('lang.numberRequired'); + } + + } else if (rule.label === 'complexitySpecial') { + this.passwordRules.complexitySpecial.enabled = rule.enabled; + this.passwordRules.complexitySpecial.value = rule.value; + if (rule.enabled) { + ruleTextArr.push('lang.specialCharRequired'); + } + } else if (rule.label === 'renewal') { + this.passwordRules.renewal.enabled = rule.enabled; + this.passwordRules.renewal.value = rule.value; + if (rule.enabled) { + this.translate.get('lang.renewalInfo', { time: rule.value }).subscribe((res: string) => { + otherRuleTextArr.push(res); + }); + } + } else if (rule.label === 'historyLastUse') { + this.passwordRules.historyLastUse.enabled = rule.enabled; + this.passwordRules.historyLastUse.value = rule.value; + if (rule.enabled) { + this.translate.get('lang.historyUseInfo', { countPwd: rule.value }).subscribe((res: string) => { + otherRuleTextArr.push(res); + }); + } + } + + }); + this.ruleText = ruleTextArr.join(', '); + this.otherRuleText = otherRuleTextArr.join('<br/>'); + }, (err) => { + this.notificationService.handleErrors(err); + }); + } + } + + checkPasswordValidity(password: string) { + this.handlePassword.error = true; + + if (!password.match(/[A-Z]/g) && this.passwordRules.complexityUpper.enabled) { + this.handlePassword.errorMsg = 'lang.upperRequired'; + } else if (!password.match(/[0-9]/g) && this.passwordRules.complexityNumber.enabled) { + this.handlePassword.errorMsg = 'lang.numberRequired'; + } else if (!password.match(/[^A-Za-z0-9]/g) && this.passwordRules.complexitySpecial.enabled) { + this.handlePassword.errorMsg = 'lang.specialCharRequired'; + } else if (password.length < this.passwordRules.minLength.value && this.passwordRules.minLength.enabled) { + this.translate.get('lang.minLengthChar', { charLength: this.passwordRules.minLength.value }).subscribe((res: string) => { + this.handlePassword.errorMsg = res; + }); + } else { + this.handlePassword.error = false; + this.handlePassword.errorMsg = ''; + } + } } diff --git a/src/frontend/app/app-material.module.ts b/src/frontend/app/app-material.module.ts index ea5730e532e41a1432f3ec3f03af06ae9c78859d..01a7f910dff5d0fff97abd0cf2336eaa451a1767 100755 --- a/src/frontend/app/app-material.module.ts +++ b/src/frontend/app/app-material.module.ts @@ -22,7 +22,8 @@ import { MatPaginatorModule, MatSortModule, MatPaginatorIntl, - MatAutocompleteModule + MatAutocompleteModule, + MatCheckboxModule } from '@angular/material'; import { MatMenuModule } from '@angular/material/menu'; @@ -55,7 +56,8 @@ import { getFrenchPaginatorIntl } from './plugins/paginator-fr-intl'; MatTableModule, MatPaginatorModule, MatSortModule, - MatAutocompleteModule + MatAutocompleteModule, + MatCheckboxModule ], exports: [ MatSidenavModule, @@ -80,7 +82,8 @@ import { getFrenchPaginatorIntl } from './plugins/paginator-fr-intl'; MatTableModule, MatPaginatorModule, MatSortModule, - MatAutocompleteModule + MatAutocompleteModule, + MatCheckboxModule ], providers: [ { provide: MatPaginatorIntl, useValue: getFrenchPaginatorIntl() }, diff --git a/src/frontend/app/profile/profile.component.html b/src/frontend/app/profile/profile.component.html index 62c76c09eefbb02bac1816b9dd2a08c4f763f16a..312cb1e4ea94f1dd87bda8e70faa54be30103213 100644 --- a/src/frontend/app/profile/profile.component.html +++ b/src/frontend/app/profile/profile.component.html @@ -198,39 +198,6 @@ </mat-tab> <mat-tab label="{{'lang.substitution' | translate}}"> <div class="profile-content"> - <div class="input-row" *ngIf="signaturesService.userLogged.canManageRestUsers"> - <fieldset> - <legend align="left">{{'lang.wsUser' | translate}}</legend> - <div class="form-container"> - <div class="form-col" style="width:35%;"> - <mat-form-field> - <mat-select name="usersRest" [(ngModel)]="currentUserRest"> - <mat-option *ngFor="let userRest of usersRest" [value]="userRest"> - {{userRest.firstname}} - {{userRest.lastname}}</mat-option> - </mat-select> - </mat-form-field> - </div> - <div class="form-col" style="width:35%;"> - <mat-form-field class="input-row"> - <input name="newPasswordRest" matInput [(ngModel)]="currentUserRestPassword" - placeholder="{{'lang.newPassword' | translate}}" - [type]="hideNewPassword ? 'password' : 'text'" - (keyup)="checkPasswordValidity(currentUserRestPassword)"> - <mat-icon matSuffix (click)="hideNewPassword = !hideNewPassword" - class="fa fa-2x" - [ngClass]="[hideNewPassword ? 'fa-eye-slash' : 'fa-eye']"></mat-icon> - <mat-hint style="color:red;">{{handlePassword.errorMsg}}</mat-hint> - </mat-form-field> - </div> - <div class="form-col" style="width:29%;"> - <button mat-raised-button type="button" color="primary" - [disabled]="handlePassword.error || currentUserRestPassword.length === 0" - (click)="updateRestUser()">{{'lang.update' | translate}}</button> - </div> - </div> - </fieldset> - </div> <div class="input-row"> <fieldset> <legend align="left">{{'lang.substitution' | translate}}</legend> @@ -240,9 +207,9 @@ </div> <div class="form-2-col"> <mat-form-field style="width:100%"> - <mat-select placeholder="{{'lang.chooseSubstitute' | translate}}" name="usersRest" + <mat-select placeholder="{{'lang.chooseSubstitute' | translate}}" name="usersList" [(ngModel)]="profileInfo.substitute"> - <ng-container *ngFor="let userRest of usersRest"> + <ng-container *ngFor="let userRest of usersList"> <mat-option *ngIf="userRest.id !== profileInfo.id && !userRest.substitute" [value]="userRest.id"> {{userRest.firstname}} diff --git a/src/frontend/app/profile/profile.component.ts b/src/frontend/app/profile/profile.component.ts index 097f3118c00cb0b652d8255ebe359f848af0401f..074ec4abcced1982d5e3b8cb7a006bc68a12776a 100644 --- a/src/frontend/app/profile/profile.component.ts +++ b/src/frontend/app/profile/profile.component.ts @@ -53,9 +53,7 @@ export class ProfileComponent implements OnInit { errorMsg: '' }; - usersRest: any = []; - currentUserRest: any = {}; - currentUserRestPassword = ''; + usersList: any = []; ruleText = ''; otherRuleText = ''; @@ -361,20 +359,10 @@ export class ProfileComponent implements OnInit { } swithToAdmin() { - /*if (this.usersRest.length === 0) { - this.getPassRules(); - this.http.get('../rest/users?mode=rest') - .subscribe((data: any) => { - this.usersRest = data.users; - this.currentUserRest = data.users[0]; - - }); - }*/ - - if (this.usersRest.length === 0) { + if (this.usersList.length === 0) { this.http.get('../rest/users') .subscribe((data: any) => { - this.usersRest = data.users; + this.usersList = data.users; }); } @@ -386,18 +374,6 @@ export class ProfileComponent implements OnInit { } } - updateRestUser() { - this.http.put('../rest/users/' + this.currentUserRest.id + '/password', { 'newPassword': this.currentUserRestPassword }) - .subscribe(() => { - this.currentUserRestPassword = ''; - this.translate.get('lang.passwordOfUserUpdated', { user: this.currentUserRest.firstname + ' ' + this.currentUserRest.lastname }).subscribe((res: string) => { - this.notificationService.success(res); - }); - }, (err) => { - this.notificationService.handleErrors(err); - }); - } - setLang(lang: any) { this.translate.use(lang); }