@@ -20,6 +20,7 @@ import {ErrorMessage} from "./error-message"; | |||
export class ErrorResult { | |||
error_messages: Array<ErrorMessage> | |||
status: number; | |||
constructor(errorMessages: Array<ErrorMessage>) { | |||
this.error_messages = errorMessages; |
@@ -0,0 +1,25 @@ | |||
/* | |||
* 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 { User } from './user'; | |||
describe('User', () => { | |||
it('should create an instance', () => { | |||
expect(new User()).toBeTruthy(); | |||
}); | |||
}); |
@@ -0,0 +1,24 @@ | |||
/* | |||
* 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 { UserInfo } from './user-info'; | |||
export class User extends UserInfo { | |||
password: string; | |||
confirm_password: string; | |||
} |
@@ -24,6 +24,9 @@ | |||
<li class="nav-item"> | |||
<a class="nav-link" routerLink="/user/users/add" routerLinkActive="active" href="#">{{'users.add.head' |translate }}</a> | |||
</li> | |||
<li class="nav-item"> | |||
<a class="nav-link" routerLink="/user/users/edit/guest" routerLinkActive="active" href="#">{{'users.edit.head' |translate }}</a> | |||
</li> | |||
</ul> | |||
<router-outlet ></router-outlet> |
@@ -18,47 +18,67 @@ | |||
--> | |||
<form class="mt-3 mb-3" [formGroup]="userForm" (ngSubmit)="onSubmit()"> | |||
<fieldset> | |||
<div class="form-group col-md-8"> | |||
<label for="userId">{{'users.attributes.user_id' |translate}}</label> | |||
<input type="text" class="form-control" formControlName="userId" id="userId" | |||
[ngClass]="valid('userId')" | |||
placeholder="{{'users.input.user_id'|translate}}"> | |||
<small>{{'users.input.small.user_id'|translate}}</small> | |||
<div class="form-group col-md-8"> | |||
<label for="user_id">{{'users.attributes.user_id' |translate}}</label> | |||
<input type="text" class="form-control" formControlName="user_id" id="user_id" | |||
[ngClass]="valid('user_id')" | |||
placeholder="{{'users.input.user_id'|translate}}"> | |||
<small>{{'users.input.small.user_id'|translate:{'minSize':this.minUserIdSize} }}</small> | |||
</div> | |||
<div class="form-group col-md-8"> | |||
<label for="full_name">{{'users.attributes.full_name' |translate}}</label> | |||
<input type="text" class="form-control" formControlName="full_name" id="full_name" | |||
[ngClass]="valid('full_name')" | |||
placeholder="{{'users.input.full_name'|translate}}"> | |||
<small>{{'users.input.small.full_name'|translate}}</small> | |||
</div> | |||
<div class="form-group col-md-8"> | |||
<label for="email">{{'users.attributes.email' |translate}}</label> | |||
<input type="text" class="form-control" formControlName="email" id="email" | |||
[ngClass]="valid('email')" | |||
placeholder="{{'users.input.email'|translate}}"> | |||
</div> | |||
<div class="form-group col-md-8"> | |||
<label for="password">{{'users.attributes.password' |translate}}</label> | |||
<input type="password" class="form-control" formControlName="password" id="password" | |||
[ngClass]="valid('password')" | |||
placeholder="{{'users.input.password'|translate}}"> | |||
</div> | |||
<div class="form-group col-md-8"> | |||
<label for="confirm_password">{{'users.attributes.confirm_password' |translate}}</label> | |||
<input type="password" class="form-control" formControlName="confirm_password" id="confirm_password" | |||
[ngClass]="valid('confirm_password')" | |||
placeholder="{{'users.input.confirm_password'|translate}}"> | |||
</div> | |||
<div class="form-group col-md-8"> | |||
<div class="form-check"> | |||
<input class="form-check-input" type="checkbox" value="" formControlName="locked" id="locked"> | |||
<label class="form-check-label" for="locked"> | |||
{{'users.attributes.locked'|translate}} | |||
</label> | |||
</div> | |||
<div class="form-group col-md-8"> | |||
<label for="fullName">{{'users.attributes.full_name' |translate}}</label> | |||
<input type="text" class="form-control" formControlName="fullName" id="fullName" | |||
[ngClass]="valid('fullName')" | |||
placeholder="{{'users.input.full_name'|translate}}"> | |||
<small>{{'users.input.small.full_name'|translate}}</small> | |||
<div class="form-check"> | |||
<input class="form-check-input" type="checkbox" value="" 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-group col-md-8"> | |||
<label for="email">{{'users.attributes.email' |translate}}</label> | |||
<input type="text" class="form-control" formControlName="email" id="email" | |||
[ngClass]="valid('email')" | |||
placeholder="{{'users.input.email'|translate}}"> | |||
</div> | |||
<div class="form-group col-md-8"> | |||
<div class="form-check"> | |||
<input class="form-check-input" type="checkbox" value="" 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="passwordChangeRequired" | |||
id="password_change_required" checked> | |||
<label class="form-check-label" for="password_change_required"> | |||
{{'users.attributes.password_change_required'|translate}} | |||
</label> | |||
</div> | |||
</div> | |||
<div class="form-group col-md-8"> | |||
</div> | |||
<div class="form-group col-md-8"> | |||
<button class="btn btn-primary" type="submit" | |||
[disabled]="!userForm.valid">{{'users.add.submit'|translate}}</button> | |||
</div> | |||
</fieldset> | |||
</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" > | |||
<h4 class="alert-heading">Errors</h4> | |||
<ng-container *ngFor="let message of errorResult?.error_messages; first as isFirst" > | |||
<hr *ngIf="!isFirst"> | |||
<p>{{message.message}}</p> | |||
</ng-container> | |||
</div> | |||
</form> |
@@ -18,8 +18,14 @@ | |||
*/ | |||
import {Component, OnInit} from '@angular/core'; | |||
import {FormControl, FormGroup, Validators, FormBuilder} from '@angular/forms'; | |||
import {Validators, FormBuilder, FormGroup} 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'; | |||
@Component({ | |||
selector: 'app-manage-users-add', | |||
@@ -28,12 +34,23 @@ import {UserService} from "../../../../services/user.service"; | |||
}) | |||
export class ManageUsersAddComponent implements OnInit { | |||
minUserIdSize=8; | |||
success:boolean=false; | |||
error:boolean=false; | |||
errorResult:ErrorResult; | |||
result:string; | |||
userid:string; | |||
userForm = this.fb.group({ | |||
userId: ['', [Validators.required, Validators.minLength(8)]], | |||
fullName: ['', Validators.required], | |||
user_id: ['', [Validators.required, Validators.minLength(this.minUserIdSize)]], | |||
full_name: ['', Validators.required], | |||
email: ['', [Validators.required,Validators.email]], | |||
locked: [false], | |||
passwordChangeRequired: [true] | |||
password_change_required: [true], | |||
password: [''], | |||
confirm_password: [''], | |||
}, { | |||
validator: MustMatch('password', 'confirm_password') | |||
}) | |||
constructor(private userService: UserService, private fb: FormBuilder) { | |||
@@ -45,13 +62,41 @@ export class ManageUsersAddComponent implements OnInit { | |||
onSubmit() { | |||
// Process checkout data here | |||
console.warn('Your order has been submitted', JSON.stringify(this.userForm.value)); | |||
this.result=null; | |||
if (this.userForm.valid) { | |||
let user = this.copyForm(['user_id','full_name','email','locked','password_change_required', | |||
'password','confirm_password']) | |||
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.errorResult = error; | |||
this.success=false; | |||
this.error=true; | |||
return throwError(error); | |||
})).subscribe((location : string ) => { | |||
this.result = location; | |||
this.success=true; | |||
this.error = false; | |||
this.userid = location.substring(location.lastIndexOf('/') + 1); | |||
}); | |||
} | |||
} | |||
get userId() { | |||
return this.userForm.get('userId'); | |||
private copyForm(properties:string[]) : User { | |||
let user : any = new User(); | |||
for (let prop of properties) { | |||
user[prop] = this.userForm.get(prop).value; | |||
} | |||
console.log("User " + user); | |||
return user; | |||
} | |||
valid(field:string) : string { | |||
let formField = this.userForm.get(field); | |||
if (formField.dirty||formField.touched) { | |||
@@ -65,4 +110,26 @@ export class ManageUsersAddComponent implements OnInit { | |||
} | |||
} | |||
} | |||
export function MustMatch(controlName: string, matchingControlName: string) { | |||
return (formGroup: FormGroup) => { | |||
const control = formGroup.controls[controlName]; | |||
const matchingControl = formGroup.controls[matchingControlName]; | |||
if (matchingControl.errors && !matchingControl.errors.mustMatch) { | |||
// return if another validator has already found an error on the matchingControl | |||
return; | |||
} | |||
// set error on matchingControl if validation fails | |||
if (control.value !== matchingControl.value) { | |||
matchingControl.setErrors({ mustMatch: true }); | |||
} else { | |||
matchingControl.setErrors(null); | |||
} | |||
} | |||
} |
@@ -50,7 +50,7 @@ | |||
[ngClass]="user.validated?'fa-check-circle':'fa-circle'"></span></td> | |||
<td><span class="far" [attr.aria-valuetext]="user.locked" | |||
[ngClass]="user.locked?'fa-check-circle':'fa-circle'"></span></td> | |||
<td><span class="far" [attr.aria-valuetext]="user.passwordChangeRequired" | |||
<td><span class="far" [attr.aria-valuetext]="user.password_change_required" | |||
[ngClass]="user.password_change_required?'fa-check-circle':'fa-circle'"></span></td> | |||
<td>{{user.timestamp_last_login | date:'yyyy-MM-ddTHH:mm:ss'}}</td> | |||
<td>{{user.timestamp_account_creation | date : 'yyyy-MM-ddTHH:mm:ss'}}</td> |
@@ -17,11 +17,12 @@ | |||
* under the License. | |||
*/ | |||
import {Injectable} from '@angular/core'; | |||
import {HttpClient} from "@angular/common/http"; | |||
import {HttpClient, HttpErrorResponse, HttpResponse} from "@angular/common/http"; | |||
import {environment} from "../../environments/environment"; | |||
import {Observable} from "rxjs"; | |||
import {ErrorMessage} from "../model/error-message"; | |||
import {TranslateService} from "@ngx-translate/core"; | |||
import {ErrorResult} from "../model/error-result"; | |||
@Injectable({ | |||
providedIn: 'root' | |||
@@ -42,6 +43,21 @@ export class ArchivaRequestService { | |||
* @param input the input data, if this is a POST or UPDATE request | |||
*/ | |||
executeRestCall<R>(type: string, module: string, service: string, input: object): Observable<R> { | |||
let httpArgs = this.getHttpOptions(type, module, service, input); | |||
httpArgs['options']['observe'] = 'body'; | |||
httpArgs['options']['responseType'] = 'json'; | |||
let lType = type.toLowerCase(); | |||
if (lType == "get") { | |||
return this.http.get<R>(httpArgs.url, httpArgs.options); | |||
} else if (lType == "head" ) { | |||
return this.http.head<R>(httpArgs.url, httpArgs.options); | |||
} else if (lType == "post") { | |||
return this.http.post<R>(httpArgs.url, input, httpArgs.options); | |||
} | |||
} | |||
private getHttpOptions(type: string, module: string, service: string, input: object) { | |||
let modulePath = environment.application.servicePaths[module]; | |||
let url = environment.application.baseUrl + environment.application.restPath + "/" + modulePath + "/" + service; | |||
let token = this.getToken(); | |||
@@ -53,14 +69,28 @@ export class ArchivaRequestService { | |||
} else { | |||
headers = {}; | |||
} | |||
if (type == "get") { | |||
let options = {'headers': headers} | |||
if (type.toLowerCase()=='get') { | |||
let params = {} | |||
if (input!=null) { | |||
params = input; | |||
} | |||
return this.http.get<R>(url, {"headers": headers,"params":params}); | |||
} else if (type == "post") { | |||
return this.http.post<R>(url, input, {"headers": headers}); | |||
options['params'] = params; | |||
} | |||
return {'url':url, 'options':options} | |||
} | |||
executeResponseCall<R>(type: string, module: string, service:string, input:object) : Observable<HttpResponse<R>> { | |||
let httpArgs = this.getHttpOptions(type, module, service, input); | |||
httpArgs['options']['observe'] = 'response'; | |||
httpArgs['options']['responseType'] = 'json'; | |||
let lType = type.toLowerCase(); | |||
if (lType == "get") { | |||
return this.http.get<HttpResponse<R>>(httpArgs.url, httpArgs.options); | |||
} else if (lType=='head') { | |||
return this.http.head<HttpResponse<R>>(httpArgs.url, httpArgs.options); | |||
} else if (lType == 'post') { | |||
return this.http.post<HttpResponse<R>>(httpArgs.url, input, httpArgs.options); | |||
} | |||
} | |||
@@ -97,4 +127,17 @@ export class ArchivaRequestService { | |||
return this.translator.instant('api.' + errorMsg.error_key, parms); | |||
} | |||
} | |||
public getTranslatedErrorResult(httpError : HttpErrorResponse) : ErrorResult { | |||
let errorResult = httpError.error as ErrorResult; | |||
errorResult.status = httpError.status; | |||
if (errorResult.error_messages!=null) { | |||
for (let message of errorResult.error_messages) { | |||
if (message.message==null || message.message=='') { | |||
message.message = this.translateError(message); | |||
} | |||
} | |||
} | |||
return errorResult; | |||
} | |||
} |
@@ -19,12 +19,14 @@ | |||
import {Injectable, OnDestroy, OnInit} from '@angular/core'; | |||
import {ArchivaRequestService} from "./archiva-request.service"; | |||
import {UserInfo} from '../model/user-info'; | |||
import {HttpErrorResponse} from "@angular/common/http"; | |||
import {HttpErrorResponse, HttpResponse} from "@angular/common/http"; | |||
import {ErrorResult} from "../model/error-result"; | |||
import {Observable} from "rxjs"; | |||
import {Observable, throwError} from "rxjs"; | |||
import {Permission} from '../model/permission'; | |||
import {PagedResult} from "../model/paged-result"; | |||
import {EntityService} from "../model/entity-service"; | |||
import { User } from '../model/user'; | |||
import {catchError, map} from "rxjs/operators"; | |||
@Injectable({ | |||
providedIn: 'root' | |||
@@ -269,4 +271,11 @@ export class UserService implements OnInit, OnDestroy { | |||
return this.rest.executeRestCall<PagedResult<UserInfo>>("get", "redback", "users", {'q':searchTerm, 'offset':offset,'limit':limit,'orderBy':orderBy,'order':order}); | |||
} | |||
public addUser(user : User) : Observable<string> { | |||
return this.rest.executeResponseCall<string>("post", "redback", "users", user).pipe( | |||
catchError( ( error: HttpErrorResponse) => { | |||
return throwError(this.rest.getTranslatedErrorResult(error)); | |||
}),map( (httpResponse : HttpResponse<string>) => httpResponse.headers.get('Location'))); | |||
} | |||
} |
@@ -47,7 +47,8 @@ | |||
} | |||
}, | |||
"api" : { | |||
"rb.auth.invalid_credentials": "Invalid credentials given" | |||
"rb.auth.invalid_credentials": "Invalid credentials given", | |||
"user.password.violation.numeric" : "Password must have at least {{arg0}} numeric characters." | |||
}, | |||
"users": { | |||
"attributes":{ | |||
@@ -61,16 +62,20 @@ | |||
"last_login": "Last Login", | |||
"created": "Created", | |||
"permanent": "Permanent", | |||
"last_password_change": "Last Password Change" | |||
"last_password_change": "Last Password Change", | |||
"password": "Password", | |||
"confirm_password": "Confirm Password" | |||
}, | |||
"input" : { | |||
"small": { | |||
"user_id": "Must be a unique key. No space 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", | |||
"full_name": "Enter full user name", | |||
"email": "email@example.org" | |||
"email": "email@example.org", | |||
"password": "Enter password", | |||
"confirm_password": "Confirm password" | |||
}, | |||
"list": { | |||
@@ -88,5 +93,10 @@ | |||
}, | |||
"form": { | |||
"submit": "Submit" | |||
}, | |||
"password": { | |||
"violations" : { | |||
} | |||
} | |||
} |