Adding user info and password change

This commit is contained in:
Martin Stockhammer 2020-12-28 21:05:44 +01:00
parent cb0e4d19d7
commit c7344aa1c4
12 changed files with 347 additions and 37 deletions

View File

@ -0,0 +1,28 @@
package org.apache.archiva.rest.api.services.v2;/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
/**
*
* Service for configuration of redback and security related settings.
*
* @author Martin Stockhammer <martin_s@apache.org>
* @since 3.0
*/
public interface SecurityConfigurationService
{
}

View File

@ -29,6 +29,7 @@ import {BrowseComponent} from "./modules/repo/browse/browse.component";
import {UploadComponent} from "./modules/repo/upload/upload.component";
import {RoutingGuardService as Guard} from "./services/routing-guard.service";
import {SecurityConfigurationComponent} from "./modules/security/security-configuration/security-configuration.component";
import {UserInfoComponent} from "@app/modules/shared/user-info/user-info.component";
/**
* You can use Guard (RoutingGuardService) for permission checking. The service needs data with one parameter 'perm',
@ -55,6 +56,7 @@ const routes: Routes = [
]
},
{path: 'contact', component: ContactComponent},
{path: 'me/info', component: UserInfoComponent},
{path: 'about', component: AboutComponent},
{path: 'login', component: LoginComponent},
{path: 'logout', component: HomeComponent},

View File

@ -50,9 +50,11 @@ <h5 class="modal-title alert" id="modal-basic-title">{{'error.modal.title'|trans
<div class="collapse navbar-collapse" id="navbarsDefault">
<div class="navbar-nav ml-auto">
<span *ngIf="auth.authenticated" class="navbar-text border-right pr-2 mr-2">
<a class="nav-link" routerLink="/me/info" >
{{user.userInfo.full_name}}
</a>
</span>
<ul class="navbar-nav">
<ul class="navbar-nav d-lg-flex align-items-center">
<li class="nav-item active">
<a class="nav-link" routerLink="/">
<i class="fas fa-home mr-1"></i>{{ 'menu.home' |translate }}

View File

@ -37,6 +37,8 @@ import {RouterModule} from "@angular/router";
import { WithLoadingPipe } from './with-loading.pipe';
import { StripLoadingPipe } from './strip-loading.pipe';
import { ToastComponent } from './toast/toast.component';
import { UserInfoComponent } from './user-info/user-info.component';
import {FormsModule} from "@angular/forms";
export { LoadingValue } from './model/loading-value';
export { PageQuery } from './model/page-query';
@ -48,7 +50,8 @@ export { PageQuery } from './model/page-query';
SortedTableHeaderRowComponent,
WithLoadingPipe,
StripLoadingPipe,
ToastComponent
ToastComponent,
UserInfoComponent
],
exports: [
CommonModule,
@ -84,6 +87,7 @@ export { PageQuery } from './model/page-query';
deps: [HttpClient]
}
}),
FormsModule,
]
})
export class SharedModule {

View File

@ -41,9 +41,7 @@ import {AppNotification} from "@app/model/app-notification";
<ng-template #text>{{ toast.body }}</ng-template>
</ngb-toast>
`,
styles: [".ngb-toasts{margin:.5em;padding:0.5em;position:fixed;right:2px;top:20px;z-index:1200}"
],
host: {'[class.ngb-toasts]': 'true'}
styles: [':host { margin:.5em; padding:1em; position:fixed; right:10px; top:40px; z-index:1200; }']
})
export class ToastComponent implements OnInit {

View File

@ -0,0 +1,87 @@
<!--
~ Licensed to the Apache Software Foundation (ASF) under one
~ or more contributor license agreements. See the NOTICE file
~ distributed with this work for additional information
~ regarding copyright ownership. The ASF licenses this file
~ to you under the Apache License, Version 2.0 (the
~ "License"); you may not use this file except in compliance
~ with the License. You may obtain a copy of the License at
~
~ http://www.apache.org/licenses/LICENSE-2.0
~ Unless required by applicable law or agreed to in writing,
~ software distributed under the License is distributed on an
~ "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
~ KIND, either express or implied. See the License for the
~ specific language governing permissions and limitations
~ under the License.
-->
<div class="col-md-12">
<h2>{{'me.title'|translate}}</h2>
<form #passwordForm="ngForm" (ngSubmit)="changePassword()" class="mt-4">
<div class="form-group row" *ngFor="let att of ['user_id','full_name','email','language']">
<label for="{{att}}" class="col-md-1 col-form-label">{{'users.attributes.' + att|translate}}</label>
<div class="col-md-4">
<input id="{{att}}" class="form-control" value="{{userService.userInfo[att]}}" readonly>
</div>
</div>
<div class="form-group row"
*ngFor="let dateAtt of ['timestamp_account_creation','timestamp_last_password_change']">
<label for="{{dateAtt}}" class="col-md-1 col-form-label">{{'users.attributes.' + dateAtt|translate}}</label>
<ng-container *ngIf="userService.userInfo[dateAtt]">
<div class="col-md-4">
<input id="{{dateAtt}}" class="form-control"
value="{{userService.userInfo[dateAtt]|date:'YYYY-MM-dd HH:mm:ss'}}" readonly>
</div>
</ng-container>
</div>
<div class="form-group row">
<div class="col-md-1">&nbsp;</div>
<div class="col-md-4">
<div class="form-check" *ngFor="let checkAtt of ['password_change_required','validated','locked']">
<input id="{{checkAtt}}" type="checkbox" class="form-check-input disabled"
[checked]="userService.userInfo[checkAtt]?'true':null" disabled="disabled">
<label for="{{checkAtt}}"
class="form-check-label">{{'users.attributes.' + checkAtt|translate}}</label>
</div>
</div>
</div>
<hr class="mt-3" />
<h2>{{'users.edit.changePasswordTitle'|translate}}</h2>
<div class="form-group row mt-4" >
<label for="current_password" class="col-md-1 col-form-label">{{'users.attributes.current_password'|translate}}</label>
<div class="col-md-4">
<input id="current_password" name="current_password" type="password" class="form-control" required
[(ngModel)]="formData.current_password" #v_current_password="ngModel" [ngClass]="valid('current_password')">
<small class="invalid-feedback" *ngIf="v_current_password.errors?.required">{{'form.error.required'|translate}}</small>
</div>
</div>
<div class="form-group row" >
<label for="password" class="col-md-1 col-form-label">{{'users.attributes.new_password'|translate}}</label>
<div class="col-md-4">
<input id="password" name="password" type="password" class="form-control" required
[ngClass]="valid('password')"
[(ngModel)]="formData.password" #v_password="ngModel" >
<small class="invalid-feedback" *ngIf="v_password.errors?.required">{{'form.error.required'|translate}}</small>
<small class="invalid-feedback" *ngIf="v_password.errors?.invalidpassword">{{v_password.errors.invalidpassword}}</small>
</div>
</div>
<div class="form-group row" >
<label for="confirm_password" class="col-md-1 col-form-label">{{'users.attributes.confirm_password'|translate}}</label>
<div class="col-md-4">
<input id="confirm_password" name="confirm_password" type="password" class="form-control" required
[(ngModel)]="formData.confirm_password" [class.is-valid]="confirmIsValid()" [class.is-invalid]="confirmIsNotValid()">
</div>
</div>
<div class="form-group row" >
<div class="col-md-1">&nbsp;</div>
<div class="col-md-4">
<button class="btn btn-primary"
type="submit">{{'users.edit.submitPassword'|translate}}</button>
</div>
</div>
</form>
</div>

View File

@ -0,0 +1,18 @@
/*!
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/

View File

@ -0,0 +1,43 @@
/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
import { ComponentFixture, TestBed } from '@angular/core/testing';
import { UserInfoComponent } from './user-info.component';
describe('UserInfoComponent', () => {
let component: UserInfoComponent;
let fixture: ComponentFixture<UserInfoComponent>;
beforeEach(async () => {
await TestBed.configureTestingModule({
declarations: [ UserInfoComponent ]
})
.compileComponents();
});
beforeEach(() => {
fixture = TestBed.createComponent(UserInfoComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should create', () => {
expect(component).toBeTruthy();
});
});

View File

@ -0,0 +1,103 @@
/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
import {Component, OnInit, ViewChild} from '@angular/core';
import {NgForm} from '@angular/forms';
import {UserService} from "@app/services/user.service";
import {ToastService} from "@app/services/toast.service";
import {ErrorResult} from "@app/model/error-result";
@Component({
selector: 'app-user-info',
templateUrl: './user-info.component.html',
styleUrls: ['./user-info.component.scss']
})
export class UserInfoComponent implements OnInit {
@ViewChild('passwordForm') passwordForm: NgForm;
formData = {
current_password:"",
password: "",
confirm_password: ""
}
constructor(public userService: UserService, private toastService: ToastService) { }
ngOnInit(): void {
}
changePassword() {
console.log("Submit Password ")
if (!this.formData.current_password || this.formData.current_password.length==0) {
this.passwordForm.controls['current_password'].setErrors({'required':'true'})
this.passwordForm.controls['current_password'].markAsDirty()
}
if (!this.formData.password || this.formData.password.length==0) {
this.passwordForm.controls['password'].setErrors({'required':'true'})
this.passwordForm.controls['password'].markAsDirty()
}
if (this.passwordForm.valid) {
this.userService.changeOwnPassword(this.formData.current_password, this.formData.password, this.formData.confirm_password).subscribe(
val => {
this.toastService.showSuccessByKey('user-info','users.edit.passwordChanged')
this.formData.password=""
this.formData.current_password=""
this.formData.confirm_password=""
this.passwordForm.reset()
},
( error : ErrorResult)=>{
console.log("Error " + error.error_messages[0].message);
if (error.error_messages.length>0) {
if (error.error_messages[0].error_key.startsWith('user.password.violation')) {
this.toastService.showError('user-info', error.error_messages[0].message)
this.passwordForm.controls['password'].setErrors({'invalidpassword':error.error_messages[0].message})
}
}
}
)
} else {
this.toastService.showErrorByKey('user-info','form.error.invaliddata')
}
}
confirmIsValid() : boolean {
return (this.formData.password && this.formData.password.length >0 &&
this.formData.confirm_password && this.formData.confirm_password.length>0 && this.formData.password==this.formData.confirm_password )
}
confirmIsNotValid() : boolean {
return (this.formData.password && this.formData.password.length>0 &&
this.formData.confirm_password && this.formData.confirm_password.length>0 && this.formData.password!=this.formData.confirm_password )
}
valid(field:string) {
if (this.passwordForm) {
let ctrl = this.passwordForm.controls[field];
if (ctrl && ( ctrl.dirty || ctrl.touched ) && ctrl.valid) {
return 'is-valid'
}
if (ctrl && ( ctrl.dirty || ctrl.touched ) && ctrl.invalid) {
return 'is-invalid'
}
}
}
}

View File

@ -128,6 +128,7 @@ export class ArchivaRequestService {
* @param errorMsg the errorMsg as returned by a REST call
*/
public translateError(errorMsg: ErrorMessage): string {
console.log("Translating error "+errorMsg.error_key)
if (errorMsg.error_key != null && errorMsg.error_key != '') {
let parms = {};
if (errorMsg.args != null && errorMsg.args.length > 0) {

View File

@ -328,7 +328,7 @@ export class UserService implements OnInit, OnDestroy {
}), map((httpResponse: HttpResponse<string>) => httpResponse.status == 200));
}
public userRoleTree(userid:String): Observable<RoleTree> {
public userRoleTree(userid:string): Observable<RoleTree> {
return this.rest.executeResponseCall<RoleTree>("get", "redback","users/"+userid+"/roletree", null).pipe(
catchError((error: HttpErrorResponse)=>{
if (error.status==404) {
@ -341,4 +341,18 @@ export class UserService implements OnInit, OnDestroy {
).pipe(map((httpResponse:HttpResponse<RoleTree>)=>httpResponse.body))
}
public changeOwnPassword(current_password:string, password:string, confirm_password:string) {
let data = {
"user_id":this.userInfo.user_id,
"current_password":current_password,
"new_password":password,
"new_password_confirmation":confirm_password
}
return this.rest.executeRestCall<any>("post", "redback", "users/me/password/update", data).pipe(
catchError((error: HttpErrorResponse)=>{
return throwError(this.rest.getTranslatedErrorResult(error));
})
);
}
}

View File

@ -78,8 +78,14 @@
"created": "Created",
"permanent": "Permanent",
"last_password_change": "Last Password Change",
"timestamp_last_password_change": "Last Password Change",
"timestamp_account_creation": "Account Created",
"timestamp_last_login": "Last Login",
"password": "Password",
"confirm_password": "Confirm Password"
"new_password": "New Password",
"current_password": "Current Password",
"confirm_password": "Confirm Password",
"language": "UI Language"
},
"input": {
"small": {
@ -92,7 +98,6 @@
"password": "Enter password",
"confirm_password": "Confirm password"
},
"list": {
"head": "List Users"
},
@ -107,7 +112,10 @@
"head": "View/Edit User",
"small": {
"password": "If the password field is empty, it will not be updated."
}
},
"submitPassword": "Change Password",
"passwordChanged": "Password has been changed successfully",
"changePasswordTitle": "Change password"
},
"delete": {
"head": "Delete User",
@ -164,7 +172,6 @@
"assignable": "Assignable"
}
},
"permissions": {
"attributes": {
"permission": "Permission",
@ -182,7 +189,8 @@
"error": {
"required": "Value is empty. This is required.",
"containsWhitespace": "Value must not contain whitespace.",
"userexists": "This user exists already."
"userexists": "This user exists already.",
"invaliddata": "The data is not valid"
},
"button": {
"yes": "Yes",
@ -198,7 +206,9 @@
},
"password": {
"violations": {
}
}
},
"me": {
"title": "Current User Information"
}
}