You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@archiva.apache.org by ma...@apache.org on 2020/11/12 22:15:38 UTC

[archiva] 04/04: Improving localization. Adding user edit.

This is an automated email from the ASF dual-hosted git repository.

martin_s pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/archiva.git

commit ef97e905a3a1d01a22379ceef9f4399f13852002
Author: Martin Stockhammer <ma...@apache.org>
AuthorDate: Thu Nov 12 21:39:17 2020 +0100

    Improving localization. Adding user edit.
---
 .../src/main/archiva-web/package-lock.json         |  46 +++++-
 .../src/main/archiva-web/package.json              |   2 +
 .../src/main/archiva-web/src/app/app.module.ts     |  11 +-
 .../main/archiva-web/src/app/model/user-info.ts    |   1 +
 .../manage-users-add.component.html                |  29 +++-
 .../manage-users-add/manage-users-add.component.ts | 158 +++++++++++++++------
 .../manage-users-edit.component.html               | 118 ++++++++++-----
 .../manage-users-edit.component.ts                 |  18 ++-
 .../archiva-web/src/app/services/user.service.ts   |  35 ++++-
 .../src/main/archiva-web/src/assets/i18n/en.json   |  28 +++-
 10 files changed, 345 insertions(+), 101 deletions(-)

diff --git a/archiva-modules/archiva-web/archiva-webapp/src/main/archiva-web/package-lock.json b/archiva-modules/archiva-web/archiva-webapp/src/main/archiva-web/package-lock.json
index e3aaebc..f3e9dff 100644
--- a/archiva-modules/archiva-web/archiva-webapp/src/main/archiva-web/package-lock.json
+++ b/archiva-modules/archiva-web/archiva-webapp/src/main/archiva-web/package-lock.json
@@ -7758,6 +7758,14 @@
         }
       }
     },
+    "make-plural": {
+      "version": "4.3.0",
+      "resolved": "https://registry.npmjs.org/make-plural/-/make-plural-4.3.0.tgz",
+      "integrity": "sha512-xTYd4JVHpSCW+aqDof6w/MebaMVNTVYBZhbB/vi513xXdiPT92JMVCo0Jq8W2UZnzYRFeVbQiQ+I25l13JuKvA==",
+      "requires": {
+        "minimist": "^1.2.0"
+      }
+    },
     "map-cache": {
       "version": "0.2.2",
       "resolved": "https://registry.npmjs.org/map-cache/-/map-cache-0.2.2.tgz",
@@ -7841,6 +7849,26 @@
       "integrity": "sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==",
       "dev": true
     },
+    "messageformat": {
+      "version": "2.3.0",
+      "resolved": "https://registry.npmjs.org/messageformat/-/messageformat-2.3.0.tgz",
+      "integrity": "sha512-uTzvsv0lTeQxYI2y1NPa1lItL5VRI8Gb93Y2K2ue5gBPyrbJxfDi/EYWxh2PKv5yO42AJeeqblS9MJSh/IEk4w==",
+      "requires": {
+        "make-plural": "^4.3.0",
+        "messageformat-formatters": "^2.0.1",
+        "messageformat-parser": "^4.1.2"
+      }
+    },
+    "messageformat-formatters": {
+      "version": "2.0.1",
+      "resolved": "https://registry.npmjs.org/messageformat-formatters/-/messageformat-formatters-2.0.1.tgz",
+      "integrity": "sha512-E/lQRXhtHwGuiQjI7qxkLp8AHbMD5r2217XNe/SREbBlSawe0lOqsFb7rflZJmlQFSULNLIqlcjjsCPlB3m3Mg=="
+    },
+    "messageformat-parser": {
+      "version": "4.1.3",
+      "resolved": "https://registry.npmjs.org/messageformat-parser/-/messageformat-parser-4.1.3.tgz",
+      "integrity": "sha512-2fU3XDCanRqeOCkn7R5zW5VQHWf+T3hH65SzuqRvjatBK7r4uyFa5mEX+k6F9Bd04LVM5G4/BHBTUJsOdW7uyg=="
+    },
     "methods": {
       "version": "1.1.2",
       "resolved": "https://registry.npmjs.org/methods/-/methods-1.1.2.tgz",
@@ -7983,8 +8011,7 @@
     "minimist": {
       "version": "1.2.5",
       "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.5.tgz",
-      "integrity": "sha512-FM9nNUYrRBAELZQT3xeZQ7fmMOBg6nWNmJKTcgsJeaLstP/UODVpGsr5OhXhhXg6f+qtJ8uiZ+PUxkDWcgIXLw==",
-      "dev": true
+      "integrity": "sha512-FM9nNUYrRBAELZQT3xeZQ7fmMOBg6nWNmJKTcgsJeaLstP/UODVpGsr5OhXhhXg6f+qtJ8uiZ+PUxkDWcgIXLw=="
     },
     "minipass": {
       "version": "3.1.3",
@@ -8177,6 +8204,21 @@
       "integrity": "sha1-yobR/ogoFpsBICCOPchCS524NCw=",
       "dev": true
     },
+    "ngx-translate-messageformat-compiler": {
+      "version": "4.8.0",
+      "resolved": "https://registry.npmjs.org/ngx-translate-messageformat-compiler/-/ngx-translate-messageformat-compiler-4.8.0.tgz",
+      "integrity": "sha512-A1Zg2sC0uCc1r8siT1M2DFcLhgjX6aEIu2g5NGnPh51KGtGqQqXHiXx2qCxz1U9sKMlYrvCZzfxzJ2kaCTtw+A==",
+      "requires": {
+        "tslib": "^1.10.0"
+      },
+      "dependencies": {
+        "tslib": {
+          "version": "1.14.1",
+          "resolved": "https://registry.npmjs.org/tslib/-/tslib-1.14.1.tgz",
+          "integrity": "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg=="
+        }
+      }
+    },
     "nice-try": {
       "version": "1.0.5",
       "resolved": "https://registry.npmjs.org/nice-try/-/nice-try-1.0.5.tgz",
diff --git a/archiva-modules/archiva-web/archiva-webapp/src/main/archiva-web/package.json b/archiva-modules/archiva-web/archiva-webapp/src/main/archiva-web/package.json
index 113e3a1..a0935ae 100644
--- a/archiva-modules/archiva-web/archiva-webapp/src/main/archiva-web/package.json
+++ b/archiva-modules/archiva-web/archiva-webapp/src/main/archiva-web/package.json
@@ -27,6 +27,8 @@
     "bootstrap": "^4.5.0",
     "flag-icon-css": "^3.5.0",
     "jquery": "^3.5.1",
+    "messageformat": "^2.3.0",
+    "ngx-translate-messageformat-compiler": "^4.8.0",
     "popper.js": "^1.16.1",
     "rxjs": "~6.6.3",
     "service": "^0.1.4",
diff --git a/archiva-modules/archiva-web/archiva-webapp/src/main/archiva-web/src/app/app.module.ts b/archiva-modules/archiva-web/archiva-webapp/src/main/archiva-web/src/app/app.module.ts
index 6b7d2c4..bd46271 100644
--- a/archiva-modules/archiva-web/archiva-webapp/src/main/archiva-web/src/app/app.module.ts
+++ b/archiva-modules/archiva-web/archiva-webapp/src/main/archiva-web/src/app/app.module.ts
@@ -19,8 +19,9 @@
 import { BrowserModule } from '@angular/platform-browser';
 import { NgModule } from '@angular/core';
 import { HttpClient, HttpClientModule } from '@angular/common/http';
-import { TranslateLoader, TranslateModule } from '@ngx-translate/core';
+import { TranslateLoader, TranslateModule, TranslateCompiler } from '@ngx-translate/core';
 import { TranslateHttpLoader } from '@ngx-translate/http-loader';
+import { TranslateMessageFormatCompiler, MESSAGE_FORMAT_CONFIG } from 'ngx-translate-messageformat-compiler';
 
 import { AppRoutingModule } from './app-routing.module';
 import { AppComponent } from './app.component';
@@ -79,6 +80,10 @@ import { ManageUsersEditComponent } from './modules/user/users/manage-users-edit
     ReactiveFormsModule,
     HttpClientModule,
     TranslateModule.forRoot({
+      compiler: {
+        provide: TranslateCompiler,
+        useClass: TranslateMessageFormatCompiler
+      },
       loader: {
         provide: TranslateLoader,
         useFactory: httpTranslateLoader,
@@ -88,7 +93,9 @@ import { ManageUsersEditComponent } from './modules/user/users/manage-users-edit
       NgbPaginationModule,
       NgbTooltipModule
   ],
-  providers: [],
+  providers: [
+    { provide: MESSAGE_FORMAT_CONFIG, useValue: { locales: ['en', 'de'] }}
+  ],
   bootstrap: [AppComponent]
 })
 export class AppModule { }
diff --git a/archiva-modules/archiva-web/archiva-webapp/src/main/archiva-web/src/app/model/user-info.ts b/archiva-modules/archiva-web/archiva-webapp/src/main/archiva-web/src/app/model/user-info.ts
index 653a491..0f4a476 100644
--- a/archiva-modules/archiva-web/archiva-webapp/src/main/archiva-web/src/app/model/user-info.ts
+++ b/archiva-modules/archiva-web/archiva-webapp/src/main/archiva-web/src/app/model/user-info.ts
@@ -32,4 +32,5 @@ export class UserInfo {
     user_manager_id:string;
     validation_token:string;
     language:string;
+    location;
 }
diff --git a/archiva-modules/archiva-web/archiva-webapp/src/main/archiva-web/src/app/modules/user/users/manage-users-add/manage-users-add.component.html b/archiva-modules/archiva-web/archiva-webapp/src/main/archiva-web/src/app/modules/user/users/manage-users-add/manage-users-add.component.html
index ad538a8..13b764c 100644
--- a/archiva-modules/archiva-web/archiva-webapp/src/main/archiva-web/src/app/modules/user/users/manage-users-add/manage-users-add.component.html
+++ b/archiva-modules/archiva-web/archiva-webapp/src/main/archiva-web/src/app/modules/user/users/manage-users-add/manage-users-add.component.html
@@ -24,6 +24,18 @@
                [ngClass]="valid('user_id')"
                placeholder="{{'users.input.user_id'|translate}}">
         <small>{{'users.input.small.user_id'|translate:{'minSize':this.minUserIdSize} }}</small>
+        <div *ngIf="userForm.get('user_id').invalid" class="invalid-feedback">
+            <div *ngIf="userForm.get('user_id').errors.required">
+                {{'form.error.required'|translate}}
+            </div>
+            <div *ngIf="userForm.get('user_id').errors.containsWhitespace">
+                {{'form.error.containsWhitespace'|translate}}
+            </div>
+            <div *ngIf="userForm.get('user_id').errors.userexists">
+                {{'form.error.userexists'|translate}}
+            </div>
+        </div>
+
     </div>
     <div class="form-group col-md-8">
         <label for="full_name">{{'users.attributes.full_name' |translate}}</label>
@@ -52,28 +64,35 @@
     </div>
     <div class="form-group col-md-8">
         <div class="form-check">
-            <input class="form-check-input" type="checkbox" value="" formControlName="locked" id="locked">
+            <input class="form-check-input" type="checkbox" formControlName="locked" id="locked">
             <label class="form-check-label" for="locked">
                 {{'users.attributes.locked'|translate}}
             </label>
         </div>
         <div class="form-check">
-            <input class="form-check-input" type="checkbox" value="" formControlName="password_change_required"
+            <input class="form-check-input" type="checkbox" formControlName="password_change_required"
                    id="password_change_required" checked>
             <label class="form-check-label" for="password_change_required">
                 {{'users.attributes.password_change_required'|translate}}
             </label>
         </div>
+        <div class="form-check">
+            <input class="form-check-input" type="checkbox" formControlName="validated"
+                   id="validated" checked>
+            <label class="form-check-label" for="validated">
+                {{'users.attributes.validated'|translate}}
+            </label>
+        </div>
     </div>
     <div class="form-group col-md-8">
         <button class="btn btn-primary" type="submit"
-                [disabled]="!userForm.valid">{{'users.add.submit'|translate}}</button>
+                [attr.disabled]="userForm.valid?null:true">{{'users.add.submit'|translate}}</button>
     </div>
     <div *ngIf="success" class="alert alert-success" role="alert">
-        User <a [routerLink]="['user','users','edit',userid]">{{userid}}</a> was added to the list.
+        User <a [routerLink]="['user','users','edit',result?.user_id]">{{result?.userid}}</a> was added to the list.
     </div>
     <div *ngIf="error" class="alert alert-danger" role="alert" >
-        <h4 class="alert-heading">Errors</h4>
+        <h4 class="alert-heading">{{'users.add.errortitle'|translate}}</h4>
         <ng-container *ngFor="let message of errorResult?.error_messages; first as isFirst" >
             <hr *ngIf="!isFirst">
             <p>{{message.message}}</p>
diff --git a/archiva-modules/archiva-web/archiva-webapp/src/main/archiva-web/src/app/modules/user/users/manage-users-add/manage-users-add.component.ts b/archiva-modules/archiva-web/archiva-webapp/src/main/archiva-web/src/app/modules/user/users/manage-users-add/manage-users-add.component.ts
index 60649fc..f148d18 100644
--- a/archiva-modules/archiva-web/archiva-webapp/src/main/archiva-web/src/app/modules/user/users/manage-users-add/manage-users-add.component.ts
+++ b/archiva-modules/archiva-web/archiva-webapp/src/main/archiva-web/src/app/modules/user/users/manage-users-add/manage-users-add.component.ts
@@ -18,15 +18,23 @@
  */
 
 import {Component, OnInit} from '@angular/core';
-import {Validators, FormBuilder, FormGroup} from '@angular/forms';
+import {
+    FormBuilder,
+    FormGroup,
+    Validators,
+    FormControl,
+    AsyncValidator,
+    AbstractControl,
+    ValidationErrors,
+    ValidatorFn
+} from '@angular/forms';
 import {UserService} from "../../../../services/user.service";
 import {User} from "../../../../model/user";
-import { UserInfo } from 'src/app/model/user-info';
-import {HttpErrorResponse} from "@angular/common/http";
 import {ErrorResult} from "../../../../model/error-result";
-import {catchError} from "rxjs/operators";
-import {of, throwError} from 'rxjs';
+import {catchError, debounceTime, distinctUntilChanged, map, switchMap} from "rxjs/operators";
+import {throwError, Observable, of, pipe, timer} from 'rxjs';
 import {environment} from "../../../../../environments/environment";
+import {UserInfo} from "../../../../model/user-info";
 
 @Component({
     selector: 'app-manage-users-add',
@@ -35,21 +43,24 @@ import {environment} from "../../../../../environments/environment";
 })
 export class ManageUsersAddComponent implements OnInit {
 
-    minUserIdSize=environment.application.minUserIdLength;
-    success:boolean=false;
-    error:boolean=false;
-    errorResult:ErrorResult;
-    result:string;
-    userid:string;
+    editProperties = ['user_id', 'full_name', 'email', 'locked', 'password_change_required',
+        'password', 'confirm_password', 'validated'];
+    minUserIdSize = environment.application.minUserIdLength;
+    success: boolean = false;
+    error: boolean = false;
+    errorResult: ErrorResult;
+    result: UserInfo;
+    user: string;
 
     userForm = this.fb.group({
-        user_id: ['', [Validators.required, Validators.minLength(this.minUserIdSize)]],
+        user_id: ['', [Validators.required, Validators.minLength(this.minUserIdSize), whitespaceValidator()],this.userUidExistsValidator()],
         full_name: ['', Validators.required],
-        email: ['', [Validators.required,Validators.email]],
+        email: ['', [Validators.required, Validators.email]],
         locked: [false],
         password_change_required: [true],
         password: [''],
         confirm_password: [''],
+        validated: [true]
     }, {
         validator: MustMatch('password', 'confirm_password')
     })
@@ -63,33 +74,39 @@ export class ManageUsersAddComponent implements OnInit {
 
     onSubmit() {
         // Process checkout data here
-        this.result=null;
+        this.result = null;
         if (this.userForm.valid) {
-            let user = this.copyForm(['user_id','full_name','email','locked','password_change_required',
-            'password','confirm_password'])
+            let user = this.copyFromForm(this.editProperties);
             console.info('Adding user ' + user);
-            this.userService.addUser(user).pipe(catchError((error : ErrorResult)=> {
-                console.log("Error " + error + " - " + typeof (error) + " - " + JSON.stringify(error));
-                if (error.status==422) {
-                    console.warn("Validation error");
+            this.userService.addUser(user).pipe(catchError((error: ErrorResult) => {
+                // console.log("Error " + error + " - " + typeof (error) + " - " + JSON.stringify(error));
+                if (error.status == 422) {
+                    // console.warn("Validation error");
+                    let pwdErrors = {};
+                    for (let message of error.error_messages) {
+                        if (message.error_key.startsWith('user.password.violation')) {
+                            pwdErrors[message.error_key] = message.message;
+                        }
+                    }
+                    this.userForm.get('password').setErrors(pwdErrors);
 
                 }
                 this.errorResult = error;
-                this.success=false;
-                this.error=true;
-                return throwError(error);
-            })).subscribe((location : string ) => {
-                this.result = location;
-                this.success=true;
+                this.success = false;
+                this.error = true;
+                return [];
+                // return throwError(error);
+            })).subscribe((user: UserInfo) => {
+                this.result = user;
+                this.success = true;
                 this.error = false;
-                this.userid = location.substring(location.lastIndexOf('/') + 1);
             });
         }
     }
 
 
-    private copyForm(properties:string[]) : User {
-        let user : any  = new User();
+    public copyFromForm(properties: string[]): User {
+        let user: any = new User();
         for (let prop of properties) {
             user[prop] = this.userForm.get(prop).value;
         }
@@ -97,27 +114,83 @@ export class ManageUsersAddComponent implements OnInit {
         return user;
     }
 
+    public copyToForm(properties: string[], user: User): void {
+        let propMap = {};
+        for (let prop of properties) {
+            let propValue = user[prop] == null ? '' : user[prop];
+            propMap[prop] = propValue;
+        }
+        this.userForm.patchValue(propMap);
+        console.log("User " + user);
+    }
 
-    valid(field:string) : string[] {
-      let formField = this.userForm.get(field);
-      if (formField.dirty||formField.touched) {
-        if (formField.valid) {
-          return ['is-valid']
+
+    valid(field: string): string[] {
+        let formField = this.userForm.get(field);
+        if (formField.dirty || formField.touched) {
+            if (formField.valid) {
+                return ['is-valid']
+            } else {
+                return ['is-invalid']
+            }
         } else {
-          return ['is-invalid']
+            return ['']
+        }
+    }
+
+    getAllErrors(formGroup: FormGroup, errors: string[] = []) : string[] {
+        Object.keys(formGroup.controls).forEach(field => {
+            const control = formGroup.get(field);
+            if (control instanceof FormControl && control.errors != null) {
+                let keys = Object.keys(control.errors).map(errorKey=>field+'.'+errorKey);
+                errors = errors.concat(keys);
+            } else if (control instanceof FormGroup) {
+                errors = errors.concat(this.getAllErrors(control));
+            }
+        });
+        return errors;
+    }
+
+    getAttributeErrors(control:string):string[] {
+        return Object.keys(this.userForm.get(control).errors);
+    }
+
+    /**
+     * Async validator with debounce time
+     * @constructor
+     */
+    userUidExistsValidator() {
+
+        return (ctrl : FormControl) => {
+            // debounceTimer() does not work here, as the observable is created with each keystroke
+            // but angular does unsubscribe on previous started async observables.
+            return timer(500).pipe(
+                switchMap((userid) => this.userService.userExists(ctrl.value)),
+                catchError(() => of(null)),
+                map(exists => (exists ? {userexists: true} : null))
+            );
         }
-      } else {
-        return ['']
-      }
     }
 
+    forbiddenNameValidator(nameRe: RegExp): ValidatorFn {
+        return (control: AbstractControl): {[key: string]: any} | null => {
+            const forbidden = nameRe.test(control.value);
+            return forbidden ? {forbiddenName: {value: control.value}} : null;
+        };
+    }
 
 
 
 }
 
-export function MustMatch(controlName: string, matchingControlName: string) {
-    return (formGroup: FormGroup) => {
+export function whitespaceValidator(): ValidatorFn {
+    return (control: AbstractControl): ValidationErrors | null => {
+        const hasWhitespace =  /\s/g.test(control.value);
+        return hasWhitespace ? {containsWhitespace: {value: control.value}} : null;
+    };
+}
+export function MustMatch(controlName: string, matchingControlName: string) : ValidatorFn  {
+    return (formGroup: FormGroup): ValidationErrors | null => {
         const control = formGroup.controls[controlName];
         const matchingControl = formGroup.controls[matchingControlName];
 
@@ -128,9 +201,10 @@ export function MustMatch(controlName: string, matchingControlName: string) {
 
         // set error on matchingControl if validation fails
         if (control.value !== matchingControl.value) {
-            matchingControl.setErrors({ mustMatch: true });
+            matchingControl.setErrors({mustMatch: true});
         } else {
             matchingControl.setErrors(null);
         }
     }
-}
\ No newline at end of file
+}
+
diff --git a/archiva-modules/archiva-web/archiva-webapp/src/main/archiva-web/src/app/modules/user/users/manage-users-edit/manage-users-edit.component.html b/archiva-modules/archiva-web/archiva-webapp/src/main/archiva-web/src/app/modules/user/users/manage-users-edit/manage-users-edit.component.html
index f929de2..8fa520d 100644
--- a/archiva-modules/archiva-web/archiva-webapp/src/main/archiva-web/src/app/modules/user/users/manage-users-edit/manage-users-edit.component.html
+++ b/archiva-modules/archiva-web/archiva-webapp/src/main/archiva-web/src/app/modules/user/users/manage-users-edit/manage-users-edit.component.html
@@ -17,86 +17,126 @@
   -->
 
 <form class="mt-3 mb-3" [formGroup]="userForm" (ngSubmit)="onSubmit()">
-    <div class="form-group row col-md-8">
-        <div class="col-md-1">Edit  <span class="fas fa-edit"></span></div>
+    <div class="form-group row col-md-8" *ngIf="!editUser.permanent">
+        <div class="col-md-1">Edit <span class="fas fa-edit"></span></div>
         <div class="col-md-6">
-        <input class="form-check-input" type="checkbox" [value]="editMode"
-                                     (change)="editMode=!editMode"
-                                     >
+            <input class="form-check-input" type="checkbox" [value]="editMode"
+                   (change)="editMode=!editMode"
+            >
         </div>
     </div>
     <div class="form-group row col-md-8">
         <label class="col-md-2 col-form-label" for="user_id">{{'users.attributes.user_id' |translate}}</label>
-        <div class="col-md-6" >
-        <input type="text" formControlName="user_id" id="user_id"
-               [ngClass]="valid('user_id')"
-               value="{{editUser.user_id}}" [attr.readonly]="!editMode">
-        <small *ngIf="editMode">{{'users.input.small.user_id'|translate:{'minSize':this.minUserIdSize} }}</small>
+        <div class="col-md-6">
+            <input type="text" formControlName="user_id" id="user_id"
+                   [ngClass]="valid('user_id')"
+                   [attr.readonly]="true">
         </div>
     </div>
     <div class="form-group row col-md-8">
         <label class="col-md-2 col-form-label" for="full_name">{{'users.attributes.full_name' |translate}}</label>
-        <div class="col-md-6" >
-        <input type="text" formControlName="full_name" id="full_name"
-               [ngClass]="valid('full_name')" value="{{editUser.full_name}}" [attr.readonly]="!editMode" >
-        <small *ngIf="editMode">{{'users.input.small.full_name'|translate}}</small>
+        <div class="col-md-6">
+            <input type="text" formControlName="full_name" id="full_name"
+                   [ngClass]="valid('full_name')" [attr.readonly]="editMode?null:true">
+            <small *ngIf="editMode">{{'users.input.small.full_name'|translate}}</small>
         </div>
     </div>
     <div class="form-group row col-md-8">
         <label class="col-md-2 col-form-label" for="email">{{'users.attributes.email' |translate}}</label>
-        <div class="col-md-6" >
-        <input type="text" formControlName="email" id="email"
-               [ngClass]="valid('email')" value="{{editUser.email}}" [attr.readonly]="!editMode">
+        <div class="col-md-6">
+            <input type="text" formControlName="email" id="email"
+                   [ngClass]="valid('email')" value="{{editUser.email}}" [attr.readonly]="editMode?null:true">
         </div>
     </div>
     <div class="form-group row col-md-8" *ngIf="editMode">
         <label class="col-md-2 col-form-label" for="password">{{'users.attributes.password' |translate}}</label>
-        <div class="col-md-6" >
-        <input type="password" class="form-control" formControlName="password" id="password"
-               [ngClass]="valid('password')"
-               placeholder="{{'users.input.password'|translate}}">
+        <div class="col-md-6">
+            <input type="password" class="form-control" formControlName="password" id="password"
+                   [ngClass]="valid('password')"
+                   placeholder="{{'users.input.password'|translate}}">
+            <small>{{'users.edit.small.password'|translate}}</small>
         </div>
     </div>
     <div class="form-group row col-md-8" *ngIf="editMode">
-        <label class="col-md-2 col-form-label" for="confirm_password">{{'users.attributes.confirm_password' |translate}}</label>
+        <label class="col-md-2 col-form-label"
+               for="confirm_password">{{'users.attributes.confirm_password' |translate}}</label>
         <div class="col-md-6">
-        <input type="password" class="form-control" formControlName="confirm_password" id="confirm_password"
-               [ngClass]="valid('confirm_password')"
-               placeholder="{{'users.input.confirm_password'|translate}}">
+            <input type="password" class="form-control" formControlName="confirm_password" id="confirm_password"
+                   [ngClass]="valid('confirm_password')"
+                   placeholder="{{'users.input.confirm_password'|translate}}">
         </div>
     </div>
     <div class="form-group row col-md-8">
         <div class="col-md-2">Flags</div>
         <div class="col-md-6">
-        <div class="form-check">
-            <input class="form-check-input" type="checkbox" value="{{editUser.locked}}" formControlName="locked" id="locked" [attr.disabled]="editMode?null:true">
-            <label class="form-check-label" for="locked" >
-                {{'users.attributes.locked'|translate}}
-            </label>
+            <div class="form-check">
+                <input class="form-check-input" type="checkbox" formControlName="locked"
+                       id="locked" [attr.disabled]="editMode?null:true">
+                <label class="form-check-label " for="locked">
+                    {{'users.attributes.locked'|translate}}
+                </label>
+            </div>
+            <div class="form-check" >
+                <input class="form-check-input" type="checkbox"
+                       formControlName="password_change_required"
+                       id="password_change_required" [attr.disabled]="editMode?null:true">
+                <label class="form-check-label" for="password_change_required" >
+                    {{'users.attributes.password_change_required'|translate}}
+                </label>
+            </div>
+            <div class="form-check">
+                <input class="form-check-input" type="checkbox"
+                        formControlName="validated"
+                       id="validated" [attr.disabled]="editMode?null:true">
+                <label class="form-check-label" for="validated">
+                    {{'users.attributes.validated'|translate}}
+                </label>
+            </div>
         </div>
-        <div class="form-check">
-            <input class="form-check-input" type="checkbox" value="{{editUser.password_change_required}}" formControlName="password_change_required"
-                   id="password_change_required" [attr.disabled]="editMode?null:true">
-            <label class="form-check-label" for="password_change_required">
-                {{'users.attributes.password_change_required'|translate}}
-            </label>
+    </div>
+    <div class="form-group row col-md-8">
+        <label class="col-md-2 col-form-label" for="created">{{'users.attributes.created' |translate}}</label>
+        <div class="col-md-6">
+            <input type="text"  id="created" class="form-control-plaintext"
+                   value="{{editUser.timestamp_account_creation|date:'yyyy-MM-ddTHH:mm:ss'}}" [attr.readonly]="true">
+        </div>
+    </div>
+    <div class="form-group row col-md-8">
+        <label class="col-md-2 col-form-label" for="last_login">{{'users.attributes.last_login' |translate}}</label>
+        <div class="col-md-6">
+            <input type="text" id="last_login" class="form-control-plaintext"
+                   value="{{editUser.timestamp_last_login|date:'yyyy-MM-ddTHH:mm:ss'}}" [attr.readonly]="true">
         </div>
+    </div>
+    <div class="form-group row col-md-8">
+        <label class="col-md-2 col-form-label" for="email">{{'users.attributes.last_password_change' |translate}}</label>
+        <div class="col-md-6">
+            <input type="text" id="last_password_change" class="form-control-plaintext"
+                   value="{{editUser.timestamp_last_password_change|date:'yyyy-MM-ddTHH:mm:ss'}}" [attr.readonly]="true">
         </div>
     </div>
+
     <div class="form-group col-md-8" *ngIf="editMode">
         <button class="btn btn-primary" type="submit"
-                [disabled]="!userForm.valid">{{'users.edit.submit'|translate}}</button>
+                [disabled]="userForm.invalid || !userForm.dirty">{{'users.edit.submit'|translate}}</button>
     </div>
     <div *ngIf="success" class="alert alert-success" role="alert">
         User <a [routerLink]="['user','users','edit',userid]">{{userid}}</a> was added to the list.
     </div>
-    <div *ngIf="error" class="alert alert-danger" role="alert" >
+    <div *ngIf="editMode && error" class="alert alert-danger" role="alert">
         <h4 class="alert-heading">Errors</h4>
-        <ng-container *ngFor="let message of errorResult?.error_messages; first as isFirst" >
+        <ng-container *ngFor="let message of errorResult?.error_messages; first as isFirst">
             <hr *ngIf="!isFirst">
             <p>{{message.message}}</p>
         </ng-container>
     </div>
+    <div *ngIf="editMode && userForm.invalid" class="alert alert-danger" role="alert" >
+        <h4 class="alert-heading">Errors</h4>
+        <ng-container *ngFor="let message of getAllErrors(userForm); first as isFirst" >
+            <hr *ngIf="!isFirst">
+            <p>{{message}}</p>
+        </ng-container>
+    </div>
 
 
 </form>
diff --git a/archiva-modules/archiva-web/archiva-webapp/src/main/archiva-web/src/app/modules/user/users/manage-users-edit/manage-users-edit.component.ts b/archiva-modules/archiva-web/archiva-webapp/src/main/archiva-web/src/app/modules/user/users/manage-users-edit/manage-users-edit.component.ts
index adb5b91..7a01f25 100644
--- a/archiva-modules/archiva-web/archiva-webapp/src/main/archiva-web/src/app/modules/user/users/manage-users-edit/manage-users-edit.component.ts
+++ b/archiva-modules/archiva-web/archiva-webapp/src/main/archiva-web/src/app/modules/user/users/manage-users-edit/manage-users-edit.component.ts
@@ -19,7 +19,7 @@
 import { Component, OnInit } from '@angular/core';
 import { ActivatedRoute } from '@angular/router';
 import {UserService} from "../../../../services/user.service";
-import {FormBuilder, Validators} from "@angular/forms";
+import {FormBuilder, FormControl, Validators} from "@angular/forms";
 import {ManageUsersAddComponent, MustMatch} from "../manage-users-add/manage-users-add.component";
 import {environment} from "../../../../../environments/environment";
 import {map, switchMap} from 'rxjs/operators';
@@ -31,17 +31,23 @@ import {map, switchMap} from 'rxjs/operators';
 })
 export class ManageUsersEditComponent extends ManageUsersAddComponent implements OnInit {
 
+  editProperties = ['user_id', 'full_name', 'email', 'locked', 'password_change_required',
+    'password', 'confirm_password', 'validated'];
   editUser;
+  originUser;
   editMode:boolean=false;
+  minUserIdSize=0;
 
   constructor(private route: ActivatedRoute, public userService: UserService, public fb: FormBuilder) {
     super(userService, fb);
     this.editUser = this.route.params.pipe(map (params => params.userid ),  switchMap(userid => userService.getUser(userid))  ).subscribe(user => {
-      this.editUser = user;});
+      this.editUser = user;
+      this.originUser = user;
+      this.copyToForm(this.editProperties, this.editUser);});
   }
 
   ngOnInit(): void {
-
+    this.userForm.setControl('user_id', new FormControl());
   }
 
   valid(field: string): string[] {
@@ -54,4 +60,10 @@ export class ManageUsersEditComponent extends ManageUsersAddComponent implements
   }
 
 
+  onSubmit() {
+    this.copyFromForm(this.editProperties)
+
+  }
+
+
 }
diff --git a/archiva-modules/archiva-web/archiva-webapp/src/main/archiva-web/src/app/services/user.service.ts b/archiva-modules/archiva-web/archiva-webapp/src/main/archiva-web/src/app/services/user.service.ts
index 5e0f172..5205bfc 100644
--- a/archiva-modules/archiva-web/archiva-webapp/src/main/archiva-web/src/app/services/user.service.ts
+++ b/archiva-modules/archiva-web/archiva-webapp/src/main/archiva-web/src/app/services/user.service.ts
@@ -278,11 +278,19 @@ export class UserService implements OnInit, OnDestroy {
     }
 
 
-    public addUser(user: User): Observable<string> {
-        return this.rest.executeResponseCall<string>("post", "redback", "users", user).pipe(
+    public addUser(user: User): Observable<UserInfo> {
+        return this.rest.executeResponseCall<UserInfo>("post", "redback", "users", user).pipe(
             catchError((error: HttpErrorResponse) => {
                 return throwError(this.rest.getTranslatedErrorResult(error));
-            }), map((httpResponse: HttpResponse<string>) => httpResponse.headers.get('Location')));
+            }), map((httpResponse: HttpResponse<UserInfo>) => {
+                if (httpResponse.status==201) {
+                    let user = httpResponse.body;
+                    user.location = httpResponse.headers.get('Location');
+                    return user;
+                } else {
+                    throwError(new HttpErrorResponse({headers:httpResponse.headers,status:httpResponse.status,statusText:"Bad response code"}))
+                }
+            }));
     }
 
     public getUser(userid: string): Observable<UserInfo> {
@@ -291,4 +299,25 @@ export class UserService implements OnInit, OnDestroy {
                 return throwError(this.rest.getTranslatedErrorResult(error));
             }));
     }
+
+    public updateUser(user:User): Observable<UserInfo> {
+        return this.rest.executeRestCall<UserInfo>("put", "redback", "users/" + user.user_id, user).pipe(
+            catchError((error: HttpErrorResponse) => {
+                return throwError(this.rest.getTranslatedErrorResult(error));
+            }));
+    }
+
+    public userExists(userid:string): Observable<boolean> {
+        console.log("Checking user " + userid);
+        return this.rest.executeResponseCall<string>("head", "redback", "users/" + userid, null).pipe(
+            catchError((error: HttpErrorResponse) => {
+                if (error.status==404) {
+                    console.log("Status 404")
+                    return [false];
+                } else {
+                    return throwError(this.rest.getTranslatedErrorResult(error));
+                }
+            }), map((httpResponse: HttpResponse<string>) => httpResponse.status == 200));
+    }
+
 }
diff --git a/archiva-modules/archiva-web/archiva-webapp/src/main/archiva-web/src/assets/i18n/en.json b/archiva-modules/archiva-web/archiva-webapp/src/main/archiva-web/src/assets/i18n/en.json
index 05843c4..aaffa09 100644
--- a/archiva-modules/archiva-web/archiva-webapp/src/main/archiva-web/src/assets/i18n/en.json
+++ b/archiva-modules/archiva-web/archiva-webapp/src/main/archiva-web/src/assets/i18n/en.json
@@ -48,7 +48,12 @@
   },
   "api" : {
     "rb.auth.invalid_credentials": "Invalid credentials given",
-    "user.password.violation.numeric" : "Password must have at least {{arg0}} numeric characters."
+    "user.password.violation.length": "You must provide a password between {arg0} and {arg1} characters in length.",
+    "user.password.violation.alpha": "You must provide a password containing at least {arg0} alphabetic {arg0, plural, one {character} other {characters}}.",
+    "user.password.violation.numeric":"You must provide a password containing at least {arg0} numeric {arg0, plural, one {character} other {characters}}.",
+    "user.password.violation.reuse":"The password must not match any of the previous {arg0} {arg0, plural, one {password} other {passwords}}.",
+    "user.password.violation.alphanum.only":"You must provide a password containing all alpha-numeric characters.",
+    "user.password.violation.whitespace.detected":"You must provide a password without whitespace characters."
   },
   "users": {
     "attributes":{
@@ -64,11 +69,11 @@
         "permanent": "Permanent",
         "last_password_change": "Last Password Change",
         "password": "Password",
-      "confirm_password": "Confirm Password"
+        "confirm_password": "Confirm Password"
     },
     "input" : {
       "small": {
-        "user_id": "Must be a unique key with at least {{minSize}} characters. No spaces allowed.",
+        "user_id": "Must be a unique key with at least {minSize} characters. No spaces allowed.",
         "full_name": "This is the display name of the user"
       },
       "user_id": "Enter user ID",
@@ -83,7 +88,15 @@
     },
     "add": {
       "head": "Add User",
-      "submit": "Add User"
+      "submit": "Add User",
+      "errortitle": "Could not add the user. Please check the following error messages."
+    },
+    "edit": {
+      "submit": "Save Changes",
+      "head": "View/Edit User",
+      "small": {
+        "password": "If the password field is empty, it will not be updated."
+      }
     }
   },
   "search": {
@@ -92,7 +105,12 @@
     "input": "Search"
   },
   "form": {
-    "submit": "Submit"
+    "submit": "Submit",
+    "error": {
+      "required": "Value is empty. This is required.",
+      "containsWhitespace": "Value must not contain whitespace.",
+      "userexists": "This user exists already."
+    }
   },
   "password": {
     "violations" : {