Show existing keycloak user and Rollback on error
MODIFY
src/app/app.module.ts
src/app/app.module.ts
@@ -24,7 +24,7 @@ import { DeveloperListComponent } from './components/admin/developer-list.compon
import { CreateDeveloperComponent } from './components/admin/create-developer/create-developer.component';
import { ClientMappingComponent } from './components/admin/create-developer/client-mapping.component';
import { DragDropModule } from '@angular/cdk/drag-drop';
-import { MatButtonModule, MatInputModule } from '@angular/material';
+import { MatButtonModule, MatInputModule, MatOptionModule, MatAutocompleteModule } from '@angular/material';
import { EditDeveloperComponent } from './components/admin/edit-developer/edit-developer.component';
import { NotAuthorizedComponent } from './components/not-authorized/not-authorized.component';
import { ToasterModule } from 'angular2-toaster';
@@ -57,6 +57,8 @@ import { ToasterModule } from 'angular2-toaster';
MatButtonModule,
MatInputModule,
ToasterModule.forRoot(),
+ MatOptionModule,
+ MatAutocompleteModule,
],
providers: [
{

MODIFY
src/app/components/admin/create-developer/create-developer.component.html
src/app/components/admin/create-developer/create-developer.component.html
<div class="wrapper">
<form id="userForm" [formGroup]="userFormGroup">
<mat-form-field class="input-form-field">
- <input matInput placeholder="Username" formControlName="username" (input)="usernameKeyUp.next($event.target.value)" required>
+ <input matInput placeholder="Username" formControlName="username" [matAutocomplete]="auto" (input)="usernameKeyUp.next($event.target.value)" required>
</mat-form-field>
+ <mat-autocomplete #auto="matAutocomplete">
+ <mat-option *ngFor="let user of filteredKeycloakUsers" [value]="user.username">{{user.username}}</mat-option>
+ </mat-autocomplete>
<span [hidden]="this.checkDeveloperNotExists(this.userFormGroup.get('username').value)">(Developer already exists.)</span>
- <span [hidden]="!this.checkDeveloperNotExists(this.userFormGroup.get('username').value) || isPasswordRequiredForInsert">(You create a Developer for an existing API-Mgmt user.)</span>
- <span [hidden]="!isPasswordRequiredForInsert">
+ <span [hidden]="!this.checkDeveloperNotExists(this.userFormGroup.get('username').value) || this.isPasswordRequiredForInsert(this.userFormGroup.get('username').value)">(You create a Developer for an existing API-Mgmt user.)</span>
+ <span [hidden]="!this.isPasswordRequiredForInsert(this.userFormGroup.get('username').value)">
<mat-form-field class="input-form-field">
<!-- use attr.disabled here because of formGroup: https://angular.io/guide/template-syntax -->
- <input id="passwordField" matInput placeholder="Password" type="password" formControlName="password" [required]="isPasswordRequiredForInsert" [attr.disabled]="!isPasswordRequiredForInsert ? 'disabled' : null">
+ <input id="passwordField" matInput placeholder="Password" type="password" formControlName="password" [required]="this.isPasswordRequiredForInsert(this.userFormGroup.get('username').value)" [attr.disabled]="!this.isPasswordRequiredForInsert(this.userFormGroup.get('username').value) ? 'disabled' : null">
</mat-form-field>
<mat-form-field class="input-form-field">
- <input matInput placeholder="First name" formControlName="firstname" [required]="isPasswordRequiredForInsert" [attr.disabled]="!isPasswordRequiredForInsert ? 'disabled' : null">
+ <input matInput placeholder="First name" formControlName="firstname" [required]="this.isPasswordRequiredForInsert(this.userFormGroup.get('username').value)" [attr.disabled]="!this.isPasswordRequiredForInsert(this.userFormGroup.get('username').value) ? 'disabled' : null">
</mat-form-field>
<mat-form-field class="input-form-field">
- <input matInput placeholder="Last name" formControlName="lastname" [required]="isPasswordRequiredForInsert" [attr.disabled]="!isPasswordRequiredForInsert ? 'disabled' : null">
+ <input matInput placeholder="Last name" formControlName="lastname" [required]="this.isPasswordRequiredForInsert(this.userFormGroup.get('username').value)" [attr.disabled]="!this.isPasswordRequiredForInsert(this.userFormGroup.get('username').value) ? 'disabled' : null">
</mat-form-field>
<mat-form-field class="input-form-field">
- <input matInput placeholder="Email" formControlName="email" [required]="isPasswordRequiredForInsert" [attr.disabled]="!isPasswordRequiredForInsert ? 'disabled' : null">
+ <input matInput placeholder="Email" formControlName="email" [required]="this.isPasswordRequiredForInsert(this.userFormGroup.get('username').value)" [attr.disabled]="!this.isPasswordRequiredForInsert(this.userFormGroup.get('username').value) ? 'disabled' : null">
</mat-form-field>
</span>
</form>

MODIFY
src/app/components/admin/create-developer/create-developer.component.ts
src/app/components/admin/create-developer/create-developer.component.ts
@@ -8,8 +8,10 @@ import { Router } from '@angular/router';
import { KeycloakUserImpl } from '../edit-developer/keycloak-user-impl';
import { AdminService } from '../services/admin.service';
import { DeveloperDataCacheService } from '../services/developer-data-cache.service';
-import { Subject } from 'rxjs';
-import { debounceTime, map, mergeMap } from 'rxjs/operators';
+import {Observable, Subject} from 'rxjs';
+import {debounceTime, map, mergeMap, startWith} from 'rxjs/operators';
+import {Toast, ToasterService} from 'angular2-toaster';
+import UserRepresentation from 'keycloak-admin/lib/defs/userRepresentation';
@Component({
selector: 'app-create-developer',
@@ -18,8 +20,6 @@ import { debounceTime, map, mergeMap } from 'rxjs/operators';
})
export class CreateDeveloperComponent {
- public isPasswordRequiredForInsert = true;
-
// pattern allows only strings with characters a-z A-Z and 0-9
private nonSpecialCharacterPattern = Validators.pattern('[a-zA-Z0-9]+');
@@ -36,21 +36,30 @@ export class CreateDeveloperComponent {
public usernameKeyUp = new Subject<KeyboardEvent>();
private userNameInputSubscription;
- constructor(private adminService: AdminService, private router: Router, private developerDataCache: DeveloperDataCacheService) { }
+ public keycloakUsers: Array<UserRepresentation>;
+ public filteredKeycloakUsers: Array<UserRepresentation>;
+
+ constructor(private adminService: AdminService, private router: Router, private developerDataCache: DeveloperDataCacheService, private toasterService: ToasterService) { }
ngOnInit(): void {
+ this.adminService.getKeycloakUsers().subscribe(keycloakUsers => {
+ this.keycloakUsers = keycloakUsers;
+ this.filteredKeycloakUsers = keycloakUsers.filter((user => this.checkDeveloperNotExists(user.username)));
+ });
+
this.userNameInputSubscription = this.usernameKeyUp
- .pipe(
- debounceTime(300),
- mergeMap(username => this.adminService.isPasswordRequired(username)
- .pipe(map(isRequired => this.isPasswordRequiredForInsert = isRequired))
- )).subscribe();
+ .pipe(debounceTime(300), map(username => this.filterKeycloakUser(username)))
+ .subscribe();
}
ngOnDestroy(): void {
this.userNameInputSubscription.unsubscribe();
}
+ private filterKeycloakUser(value) {
+ this.filteredKeycloakUsers = this.keycloakUsers.filter((u) => this.checkDeveloperNotExists(u.username) && u.username.toLowerCase().includes(value.toLowerCase()));
+ }
+
insertDeveloper() {
const developerToCreate = new DeveloperImpl();
developerToCreate.name = this.userFormGroup.get('username').value;
@@ -71,13 +80,37 @@ export class CreateDeveloperComponent {
this.userFormGroup.reset();
this.clientMapping.reset();
this.developerDataCache.developers.push(createdDeveloper);
+ console.log('pushed developer to cache', createdDeveloper);
this.router.navigate(['/admin']);
+ }, error => {
+ this.adminService.rollbackDeveloperCreation(developerToCreate, keycloakUserToCreate)
+ .subscribe(rollbackResponse => console.log('Rollback executed', rollbackResponse));
+ this.developerDataCache.developers.splice(this.developerDataCache.developers
+ .findIndex(developer => developer.name.toLowerCase() === developerToCreate.name.toLowerCase()), 1);
+
+ console.error('Error creating developer', error);
+ const errorToast: Toast = {
+ type: 'error',
+ body: error.message ? error.message : error.error.message,
+ timeout: 30000,
+ showCloseButton: true
+ };
+ this.toasterService.pop(errorToast);
});
}
- checkDeveloperNotExists(userName: string) {
+ checkDeveloperNotExists(username: string) {
return this.developerDataCache.developers
- && this.developerDataCache.developers.find((d) => d.name.toLowerCase() === userName.toLowerCase()) === undefined;
+ && this.developerDataCache.developers.find((d) => d.name.toLowerCase() === username.toLowerCase()) === undefined;
+ }
+
+ checkKeycloakUserNotExists(username) {
+ return this.keycloakUsers
+ && this.keycloakUsers.find((u) => u.username.toLowerCase() === username.toLowerCase()) === undefined;
+ }
+
+ isPasswordRequiredForInsert(username) {
+ return this.checkKeycloakUserNotExists(username);
}
}

MODIFY
src/app/components/admin/services/admin.service.ts
src/app/components/admin/services/admin.service.ts
import { Inject, Injectable } from '@angular/core';
import { forkJoin, Observable } from 'rxjs';
-import { map, mergeMap } from 'rxjs/operators';
+import {map, mergeMap, share} from 'rxjs/operators';
import UserRepresentation from 'keycloak-admin/lib/defs/userRepresentation';
import { ClientSearchResult, Developer, KeycloakUser } from '../../../services/api-data.service';
import { HttpClient } from '@angular/common/http';
@@ -29,6 +29,14 @@ export class AdminService {
return this.http.get(url) as Observable<Array<Developer>>;
}
+ /**
+ * Get developer by name
+ * @param name the developer name
+ */
+ private getDeveloperByName(name: string) {
+ return this.getAllDevelopers().pipe(map(developers => developers.find(d => d.name.toLowerCase() === name.toLowerCase())));
+ }
+
/**
* Get developer by developer id
* @param developerId The developer id
@@ -38,11 +46,22 @@ export class AdminService {
return this.http.get(url) as Observable<Developer>;
}
+ /**
+ * Check if password is required (check if user exists already in keycloak)
+ * @param username The username
+ */
public isPasswordRequired(username) {
- return this.keycloak.searchUser(username)
+ return this.keycloak.findUser(username)
.pipe(map(users => users.findIndex((user => user.username === username)) === -1));
}
+ /**
+ * Get all keycloak users
+ */
+ public getKeycloakUsers() {
+ return this.keycloak.getAllUsers();
+ }
+
/**
* Create new developer
* @param developer the developer to create
@@ -66,6 +85,18 @@ export class AdminService {
return insertRequests;
}
+ public rollbackDeveloperCreation(developerToRollback: Developer, keycloakUserToRollback: KeycloakUser) {
+ const getDeveloperByName = this.getDeveloperByName(developerToRollback.name).pipe(share());
+
+ const deleteDeveloperFromApiman = getDeveloperByName
+ .pipe(mergeMap(developerToDelete => this.deleteDeveloperFromApiman(developerToDelete)));
+ const deleteKeycloakClientRole = getDeveloperByName
+ .pipe(mergeMap(developerToDelete => this.keycloak.deleteClientRole(developerToDelete.id)));
+ const deleteKeycloakUser = this.keycloak.deleteUser(keycloakUserToRollback.username);
+
+ return forkJoin(deleteDeveloperFromApiman, deleteKeycloakClientRole, deleteKeycloakUser);
+ }
+
/**
* Update a developer
* @param developer the developer to update
@@ -78,15 +109,22 @@ export class AdminService {
});
}
+ /**
+ * Delete developer from apiman
+ * @param developer the developer
+ */
+ private deleteDeveloperFromApiman(developer: Developer) {
+ const url = this.apimanUiRestUrl + '/developers/' + developer.id;
+ return this.http.delete(url);
+ }
+
/**
* Delete a developer
* @param developer the developer to update
*/
public deleteDeveloper(developer: Developer) {
- const url = this.apimanUiRestUrl + '/developers/' + developer.id;
- const deleteDeveloperFromApiman = this.http.delete(url);
return forkJoin(
- deleteDeveloperFromApiman,
+ this.deleteDeveloperFromApiman(developer),
this.keycloak.deleteClientRole(developer.id),
this.keycloak.deleteUser(developer.name)
);

MODIFY
src/app/components/admin/services/developer-data-cache.service.ts
src/app/components/admin/services/developer-data-cache.service.ts
@@ -14,6 +14,7 @@ export class DeveloperDataCacheService {
this.adminService.getAllDevelopers().subscribe((developers) => {
// set data to cache
this.developers = developers;
+ console.log('set developer list cache', developers);
});
}
}

MODIFY
src/app/components/admin/services/keycloak-interaction.service.ts
src/app/components/admin/services/keycloak-interaction.service.ts
@@ -52,11 +52,18 @@ export class KeycloakInteractionService {
.pipe(map(groups => groups.length > 0 ? groups[0] : undefined));
}
+ /**
+ * Get all keycloak users
+ */
+ public getAllUsers() {
+ return from(this.kcAdminClient.users.find());
+ }
+
/**
* Searchs a user from keycloak
* @param username the keycloak username
*/
- public searchUser(username) {
+ public findUser(username) {
return from(this.kcAdminClient.users.find({username}));
}
@@ -245,7 +252,7 @@ export class KeycloakInteractionService {
* @param user the user
*/
public findExistingOrCreateUser(user): Observable<UserRepresentation> {
- return this.searchUser(user.username).pipe(mergeMap(foundUsers => {
+ return this.findUser(user.username).pipe(mergeMap(foundUsers => {
let observer = null;
if (foundUsers.length === 0) {
observer = this.createUser(user).pipe(mergeMap((insertedUser => {
@@ -258,16 +265,6 @@ export class KeycloakInteractionService {
}));
}
- /**
- * Find user by user name
- * @param username The username
- */
- public findUser(username) {
- return from(this.kcAdminClient.users.find({
- username
- }));
- }
-
/**
* Determines if the user was generated from devportal admin
* @param user Keycloak user

GW-273: Show existing keycloak user and Rollback on error