From 5e50497baba8d6988cd97139ad3cd090ecb4c243 Mon Sep 17 00:00:00 2001
From: Guillaume Heurtier <guillaume.heurtier@maarch.org>
Date: Mon, 8 Jun 2020 18:26:05 +0200
Subject: [PATCH] FEAT #8841 TIME 4:00 begin password rules front

---
 lang/fr.json                                  |  23 ++-
 .../securities-administration.component.html  | 135 ++++++++++++++++++
 .../securities-administration.component.ts    | 101 +++++++++++++
 src/frontend/app/app-routing.module.ts        |   8 +-
 src/frontend/app/app.module.ts                |   4 +-
 .../app/service/auth-interceptor.service.ts   |   2 +-
 6 files changed, 267 insertions(+), 6 deletions(-)
 create mode 100644 src/frontend/app/administration/security/securities-administration.component.html
 create mode 100644 src/frontend/app/administration/security/securities-administration.component.ts

diff --git a/lang/fr.json b/lang/fr.json
index be42d50935..c272218173 100755
--- a/lang/fr.json
+++ b/lang/fr.json
@@ -270,6 +270,27 @@
 		"note" : "Note",
 		"collapseNote" : "Réduire la note",
 		"expandNote" : "Ouvrir la note",
-		"convertingDocument" : "Document en cours de conversion"
+		"convertingDocument" : "Document en cours de conversion",
+		"manage_password_rulesAdmin" : "Administrer les règles de mots de passes",
+		"manage_password_rules" : "Sécurités",
+		"manage_password_rulesDesc" : "Administrer les règles de mots de passes",
+		"passwordRulesUpdated" : "Règle de mot de passe mise à jour",
+		"password_complexityNumberRequired":            "Chiffre requis",
+		"password_complexitySpecial":                   "1 caractère spécial au minimum",
+		"password_complexitySpecialRequired":           "Caractère spécial requis",
+		"password_complexityUpper":                     "1 majuscule au minimum",
+		"password_complexityUpperRequired":             "Majuscule requise",
+		"password_historyLastUseDesc":                  "Vous ne pouvez pas utiliser les",
+		"password_historyLastUseDesc2":                 "dernier(s) mot(s) de passe",
+		"password_historyLastUseRequired":              "Nombre de mots de passe sauvegardés",
+		"password_lockAttemptsRequired":                "Nombre de tentatives de connexions",
+		"password_lockTimeRequired":                    "Temps de blocage",
+		"password_minLength":                           "caractère(s) au minimum",
+		"password_minLengthRequired":                   "Longueur minimale",
+		"password_renewal":                             "Veuillez noter que ce nouveau mot de passe ne sera valide que",
+		"password_renewalRequired":                     "Expiration du mot de passe",
+		"chars":                                       "caractère(s)",
+		"days":                                        "jour(s)",
+		"minutes":                                     "minute(s)"
 	}
 }
diff --git a/src/frontend/app/administration/security/securities-administration.component.html b/src/frontend/app/administration/security/securities-administration.component.html
new file mode 100644
index 0000000000..363f4d80fc
--- /dev/null
+++ b/src/frontend/app/administration/security/securities-administration.component.html
@@ -0,0 +1,135 @@
+<mat-sidenav-container autosize class="maarch-container">
+    <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>
+        <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">{{'lang.manage_password_rules' | translate}}</span>
+            </div>
+        </header>
+        <div class="container">
+            <div *ngIf="loading" style="display:flex;height:100%;">
+                <mat-spinner style="margin:auto;"></mat-spinner>
+            </div>
+            <mat-card *ngIf="!loading" class="card-app-content">
+                <mat-tab-group>
+                    <mat-tab label="{{'lang.password' | translate}}">
+                        <form (ngSubmit)="onSubmit()" #passwordForm="ngForm">
+                            <mat-list>
+                                <p style="margin-bottom: 40px;text-align: center;">
+                                    <mat-slide-toggle [name]="passwordRules['complexityUpper'].label"
+                                        [checked]="passwordRules['complexityUpper'].enabled" color="primary"
+                                        (change)="toggleRule(passwordRules['complexityUpper']);"
+                                        style="padding-left:10px;padding-right:10px;">
+                                        {{passwordRules['complexityUpper'].label}}</mat-slide-toggle>
+                                    <mat-slide-toggle [name]="passwordRules['complexityNumber'].label"
+                                        [checked]="passwordRules['complexityNumber'].enabled" color="primary"
+                                        (change)="toggleRule(passwordRules['complexityNumber']);"
+                                        style="padding-left:10px;padding-right:10px;">
+                                        {{passwordRules['complexityNumber'].label}}</mat-slide-toggle>
+                                    <mat-slide-toggle [name]="passwordRules['complexitySpecial'].label"
+                                        [checked]="passwordRules['complexitySpecial'].enabled" color="primary"
+                                        (change)="toggleRule(passwordRules['complexitySpecial']);"
+                                        style="padding-left:10px;padding-right:10px;">
+                                        {{passwordRules['complexitySpecial'].label}}</mat-slide-toggle>
+                                </p>
+                                <mat-list-item style="margin-top: 15px;margin-bottom: 15px;">
+                                    <mat-icon mat-list-icon>
+                                        <mat-slide-toggle style="position: relative;top:-10px;"
+                                            [checked]="passwordRules['minLength'].enabled" color="primary"
+                                            (change)="toggleRule(passwordRules['minLength']);"></mat-slide-toggle>
+                                    </mat-icon>
+                                    <p mat-line>
+                                        <mat-form-field style="width: 100%">
+                                            <input type="number" [disabled]="!passwordRules['minLength'].enabled"
+                                                [name]="passwordRules['minLength'].label"
+                                                [(ngModel)]="passwordRules['minLength'].value" min="1"
+                                                pattern="^[1-9][0-9]*" matInput
+                                                placeholder="{{passwordRules['minLength'].label}}" required>
+                                            <span matSuffix>&nbsp;{{'lang.chars' | translate}}</span>
+                                        </mat-form-field>
+                                    </p>
+                                </mat-list-item>
+                                <mat-list-item style="margin-top: 15px;margin-bottom: 15px;">
+                                    <mat-icon mat-list-icon>
+                                        <mat-slide-toggle style="position: relative;top:-10px;"
+                                            [checked]="passwordRules['lockAttempts'].enabled" color="primary"
+                                            (change)="toggleRule(passwordRules['lockAttempts']);">
+                                        </mat-slide-toggle>
+                                    </mat-icon>
+                                    <p mat-line style="display:flex;">
+                                        <mat-form-field style="flex:1;padding-right: 10px;">
+                                            <input type="number" [disabled]="!passwordRules['lockAttempts'].enabled"
+                                                [name]="passwordRules['lockAttempts'].label"
+                                                [(ngModel)]="passwordRules['lockAttempts'].value" min="1"
+                                                pattern="^[1-9][0-9]*" matInput
+                                                placeholder="{{passwordRules['lockAttempts'].label}}" required>
+                                        </mat-form-field>
+                                        <mat-form-field style="flex:1;">
+                                            <input type="number" [disabled]="!passwordRules['lockTime'].enabled"
+                                                [name]="passwordRules['lockTime'].label"
+                                                [(ngModel)]="passwordRules['lockTime'].value" min="1"
+                                                pattern="^[1-9][0-9]*" matInput
+                                                placeholder="{{passwordRules['lockTime'].label}}" required>
+                                            <span matSuffix>&nbsp;{{'lang.minutes' | translate}}</span>
+                                        </mat-form-field>
+                                    </p>
+                                </mat-list-item>
+                                <mat-list-item style="margin-top: 15px;margin-bottom: 15px;">
+                                    <mat-icon mat-list-icon>
+                                        <mat-slide-toggle style="position: relative;top:-10px;"
+                                            [checked]="passwordRules['renewal'].enabled" color="primary"
+                                            (change)="toggleRule(passwordRules['renewal']);"></mat-slide-toggle>
+                                    </mat-icon>
+                                    <p mat-line>
+                                        <mat-form-field style="width: 100%">
+                                            <input type="number" [disabled]="!passwordRules['renewal'].enabled"
+                                                [name]="passwordRules['renewal'].label"
+                                                [(ngModel)]="passwordRules['renewal'].value" min="1"
+                                                pattern="^[1-9][0-9]*" matInput
+                                                placeholder="{{passwordRules['renewal'].label}}" required>
+                                            <span matSuffix>&nbsp;{{'lang.days' | translate}}</span>
+                                        </mat-form-field>
+                                    </p>
+                                </mat-list-item>
+                                <mat-list-item style="margin-top: 15px;margin-bottom: 15px;">
+                                    <mat-icon mat-list-icon>
+                                        <mat-slide-toggle style="position: relative;top:-10px;"
+                                            [checked]="passwordRules['historyLastUse'].enabled" color="primary"
+                                            (change)="toggleRule(passwordRules['historyLastUse']);">
+                                        </mat-slide-toggle>
+                                    </mat-icon>
+                                    <p mat-line>
+                                        <mat-form-field style="width: 100%;">
+                                            <input type="number"
+                                                [disabled]="!passwordRules['historyLastUse'].enabled"
+                                                [name]="passwordRules['historyLastUse'].label"
+                                                [(ngModel)]="passwordRules['historyLastUse'].value" min="1"
+                                                pattern="^[1-9][0-9]*" matInput
+                                                placeholder="{{passwordRules['historyLastUse'].label}}" required>
+                                        </mat-form-field>
+                                    </p>
+                                </mat-list-item>
+                            </mat-list>
+                            <div class="col-md-12 text-center" style="padding:10px; text-align: center">
+                                <button mat-raised-button type="submit" color="primary" style="margin: 10px"
+                                    [disabled]="(!passwordForm.valid && !disabledForm()) || checkModif()">{{'lang.validate' | translate}}</button>
+                                <button mat-raised-button type="button" color="default" [disabled]="checkModif()"
+                                    (click)="cancelModification()">{{'lang.cancel' | translate}}</button>
+                            </div>
+                        </form>
+                    </mat-tab>
+                </mat-tab-group>
+            </mat-card>
+        </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/security/securities-administration.component.ts b/src/frontend/app/administration/security/securities-administration.component.ts
new file mode 100644
index 0000000000..dc3e2d4cff
--- /dev/null
+++ b/src/frontend/app/administration/security/securities-administration.component.ts
@@ -0,0 +1,101 @@
+import { Component, OnInit } from '@angular/core';
+import { HttpClient } from '@angular/common/http';
+import {TranslateService} from '@ngx-translate/core';
+import {NotificationService} from '../../service/notification.service';
+import {SignaturesContentService} from '../../service/signatures.service';
+
+@Component({
+    templateUrl: 'securities-administration.component.html',
+    styleUrls: ['../administration.scss']
+})
+export class SecuritiesAdministrationComponent implements OnInit {
+
+    loading: boolean = false;
+
+    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 },
+        lockTime: { enabled: false, value: 0 },
+        lockAttempts: { enabled: false, value: 0 },
+    };
+    passwordRulesClone: any = {};
+
+    passwordRulesList: any[] = [];
+
+
+    constructor(
+        public http: HttpClient,
+        private translate: TranslateService,
+        private notify: NotificationService,
+        public signaturesService: SignaturesContentService
+    ) { }
+
+    ngOnInit(): void {
+        this.loading = true;
+
+        this.http.get('../rest/passwordRules')
+            .subscribe((data: any) => {
+                this.passwordRulesList = data.rules;
+
+                data.rules.forEach((rule: any) => {
+                    this.passwordRules[rule.label].enabled = rule.enabled;
+                    this.passwordRules[rule.label].value = rule.value;
+                    this.passwordRules[rule.label].label = this.translate.instant('lang.password_' + rule.label + 'Required');
+                    this.passwordRules[rule.label].id = rule.label;
+
+                    this.loading = false;
+                });
+
+                this.passwordRulesClone = JSON.parse(JSON.stringify(this.passwordRules));
+
+            }, (err) => {
+                this.notify.error(err.error.errors);
+            });
+    }
+
+    cancelModification() {
+        this.passwordRules = JSON.parse(JSON.stringify(this.passwordRulesClone));
+        this.passwordRulesList.forEach((rule: any) => {
+            rule.enabled = this.passwordRules[rule.label].enabled;
+            rule.value = this.passwordRules[rule.label].value;
+        });
+    }
+
+    checkModif() {
+        return JSON.stringify(this.passwordRules) === JSON.stringify(this.passwordRulesClone);
+    }
+
+    disabledForm() {
+        return !this.passwordRules['lockTime'].enabled && !this.passwordRules['minLength'].enabled && !this.passwordRules['lockAttempts'].enabled && !this.passwordRules['renewal'].enabled && !this.passwordRules['historyLastUse'].enabled;
+    }
+
+    toggleRule(rule: any) {
+        rule.enabled = !rule.enabled;
+        this.passwordRulesList.forEach((rule2: any) => {
+            if (rule.id === 'lockAttempts' && (rule2.label === 'lockTime' || rule2.label === 'lockAttempts')) {
+                rule2.enabled = rule.enabled;
+                this.passwordRules['lockTime'].enabled = rule.enabled;
+            } else if (rule.id === rule2.label) {
+                rule2.enabled = rule.enabled;
+            }
+        });
+    }
+
+    onSubmit() {
+        this.passwordRulesList.forEach((rule: any) => {
+            rule.enabled = this.passwordRules[rule.label].enabled;
+            rule.value = this.passwordRules[rule.label].value;
+        });
+        this.http.put('../rest/passwordRules', { rules: this.passwordRulesList })
+            .subscribe(() => {
+                this.passwordRulesClone = JSON.parse(JSON.stringify(this.passwordRules));
+                this.notify.success('lang.passwordRulesUpdated');
+            }, (err: any) => {
+                this.notify.error(err.error.errors);
+            });
+    }
+}
diff --git a/src/frontend/app/app-routing.module.ts b/src/frontend/app/app-routing.module.ts
index 6704cb994d..90a4a946e2 100755
--- a/src/frontend/app/app-routing.module.ts
+++ b/src/frontend/app/app-routing.module.ts
@@ -1,6 +1,6 @@
-import { NgModule }                         from '@angular/core';
-import { RouterModule }                     from '@angular/router';
-import { AuthGuard }                        from './service/auth.guard';
+import { NgModule } from '@angular/core';
+import { RouterModule } from '@angular/router';
+import { AuthGuard } from './service/auth.guard';
 
 import { AdministrationComponent } from './administration/home/administration.component';
 import { UsersListComponent } from './administration/user/users-list.component';
@@ -15,6 +15,7 @@ import { DocumentComponent } from './document/document.component';
 import { LoginComponent } from './login/login.component';
 import { ForgotPasswordComponent } from './login/forgotPassword/forgotPassword.component';
 import { UpdatePasswordComponent } from './login/updatePassword/updatePassword.component';
+import {SecuritiesAdministrationComponent} from './administration/security/securities-administration.component';
 
 @NgModule({
     imports: [
@@ -31,6 +32,7 @@ import { UpdatePasswordComponent } from './login/updatePassword/updatePassword.c
             { path: 'administration/connections/ldaps/new', canActivate: [AuthGuard], component: LdapComponent },
             { path: 'administration/connections/ldaps/:id', canActivate: [AuthGuard], component: LdapComponent },
             { path: 'administration/emailConfiguration', canActivate: [AuthGuard], component: SendmailComponent },
+            { path: 'administration/passwordRules', canActivate: [AuthGuard], component: SecuritiesAdministrationComponent },
             { path: 'documents/:id', canActivate: [AuthGuard], component: DocumentComponent },
             { path: 'documents', canActivate: [AuthGuard], component: DocumentComponent },
             { path: 'login', component: LoginComponent },
diff --git a/src/frontend/app/app.module.ts b/src/frontend/app/app.module.ts
index e5a955484c..6afb72a861 100755
--- a/src/frontend/app/app.module.ts
+++ b/src/frontend/app/app.module.ts
@@ -64,6 +64,7 @@ import { LdapComponent } from './administration/connection/ldap/ldap.component';
 import { SendmailComponent } from './administration/sendmail/sendmail.component';
 import { GroupsListComponent } from './administration/group/groups-list.component';
 import { GroupComponent } from './administration/group/group.component';
+import {SecuritiesAdministrationComponent} from './administration/security/securities-administration.component';
 
 
 // SERVICES
@@ -117,7 +118,8 @@ import { SortPipe } from './plugins/sorting.pipe';
     GroupsListComponent,
     GroupComponent,
     PluginAutocompleteComponent,
-    SortPipe
+    SortPipe,
+    SecuritiesAdministrationComponent
   ],
   imports: [
     FormsModule,
diff --git a/src/frontend/app/service/auth-interceptor.service.ts b/src/frontend/app/service/auth-interceptor.service.ts
index 8018640165..3910756db2 100644
--- a/src/frontend/app/service/auth-interceptor.service.ts
+++ b/src/frontend/app/service/auth-interceptor.service.ts
@@ -34,7 +34,7 @@ export class AuthInterceptor implements HttpInterceptor {
 
   intercept(request: HttpRequest<any>, next: HttpHandler): Observable<any> {
     // We don't want to intercept some routes
-    if (this.excludeUrls.indexOf(request.url) > -1 || request.url.indexOf('/password') > -1) {
+    if ((this.excludeUrls.indexOf(request.url) > -1 || request.url.indexOf('/password') > -1) && request.url.indexOf('/passwordRules') === -1 && request.method.indexOf('PUT') === -1) {
       return next.handle(request);
     } else {
       // Add current token in header request
-- 
GitLab