From 9a920f6842971411e86479a4febf2958975f2873 Mon Sep 17 00:00:00 2001 From: RIHTARSIC Joze <joze.rihtarsic@ext.ec.europa.eu> Date: Wed, 12 Apr 2023 18:59:41 +0200 Subject: [PATCH] Update user administration and add windows OS script for springboot --- smp-angular/src/app/app.module.ts | 10 +- smp-angular/src/app/app.routes.ts | 24 +- .../app/common/enums/application-role.enum.ts | 4 + .../enums}/member-type.enum.ts | 0 .../enums}/membership-role.enum.ts | 0 .../data-panel/data-panel.component.html | 4 +- .../panels/data-panel/data-panel.component.ts | 1 + .../search-table/search-table-entity.model.ts | 2 +- .../search-table/search-table.component.ts | 21 +- .../src/app/guards/authenticated.guard.ts | 32 --- ...{auth.guard.ts => authentication.guard.ts} | 2 +- .../src/app/security/security.service.ts | 2 + smp-angular/src/app/smp.constants.ts | 26 +- .../domain-member-panel.component.html | 9 +- .../domain-member-panel.component.ts | 10 +- .../member-dialog/member-dialog.component.ts | 4 +- .../domain-member-panel/member-ro.model.ts | 4 +- .../domain-member-panel/membership.service.ts | 2 +- .../admin-users/admin-user.component.css | 19 ++ .../admin-users/admin-user.component.html | 74 +++++ .../admin-users/admin-user.component.ts | 252 ++++++++++++++++++ .../admin-users/admin-user.service.ts | 74 +++++ .../user-profile-panel.component.html | 184 +++++++++++++ .../user-profile-panel.component.scss | 42 +++ .../user-profile-panel.component.ts | 231 ++++++++++++++++ .../system-settings/user/user-controller.ts | 5 +- .../user-details-dialog.component.ts | 7 +- .../app/system-settings/user/user-ro.model.ts | 14 +- .../user-profile/user-profile.component.html | 168 +----------- .../user-profile/user-profile.component.scss | 2 +- .../user-profile/user-profile.component.ts | 186 ++++--------- .../app/window/toolbar/toolbar.component.html | 12 +- .../ec/edelivery/smp/data/dao/DomainDao.java | 21 +- .../smp/data/dao/DomainMemberDao.java | 11 +- .../ec/edelivery/smp/data/model/DBDomain.java | 8 +- .../edelivery/smp/data/model/user/DBUser.java | 50 ++++ .../smp/services/ui/UIUserService.java | 48 +++- .../edelivery/smp/data/dao/DomainDaoTest.java | 76 ++++++ .../edelivery/smp/data/dao/TestUtilsDao.java | 47 +++- smp-springboot/README.md | 26 +- .../smp/auth/SMPAuthorizationService.java | 14 + .../smp/ui/external/UserResource.java | 32 ++- .../smp/ui/internal/UserAdminResource.java | 70 ++++- 43 files changed, 1389 insertions(+), 441 deletions(-) create mode 100644 smp-angular/src/app/common/enums/application-role.enum.ts rename smp-angular/src/app/{system-settings/admin-domain/domain-member-panel/member-dialog => common/enums}/member-type.enum.ts (100%) rename smp-angular/src/app/{system-settings/admin-domain/domain-member-panel => common/enums}/membership-role.enum.ts (100%) delete mode 100644 smp-angular/src/app/guards/authenticated.guard.ts rename smp-angular/src/app/guards/{auth.guard.ts => authentication.guard.ts} (95%) create mode 100644 smp-angular/src/app/system-settings/admin-users/admin-user.component.css create mode 100644 smp-angular/src/app/system-settings/admin-users/admin-user.component.html create mode 100644 smp-angular/src/app/system-settings/admin-users/admin-user.component.ts create mode 100644 smp-angular/src/app/system-settings/admin-users/admin-user.service.ts create mode 100644 smp-angular/src/app/system-settings/admin-users/user-settings-panel/user-profile-panel.component.html create mode 100644 smp-angular/src/app/system-settings/admin-users/user-settings-panel/user-profile-panel.component.scss create mode 100644 smp-angular/src/app/system-settings/admin-users/user-settings-panel/user-profile-panel.component.ts create mode 100644 smp-server-library/src/test/java/eu/europa/ec/edelivery/smp/data/dao/DomainDaoTest.java diff --git a/smp-angular/src/app/app.module.ts b/smp-angular/src/app/app.module.ts index dca9f8ac4..ca4b32bad 100644 --- a/smp-angular/src/app/app.module.ts +++ b/smp-angular/src/app/app.module.ts @@ -11,7 +11,6 @@ import {AlertComponent} from "./alert/alert.component"; import {AlertMessageComponent} from './common/alert-message/alert-message.component'; import {AlertMessageService} from './common/alert-message/alert-message.service'; import {AppComponent} from './app.component'; -import {AuthenticatedGuard} from './guards/authenticated.guard'; import {AuthorizedAdminGuard} from './guards/authorized-admin.guard'; import {AuthorizedGuard} from './guards/authorized.guard'; import {AutoFocusDirective} from "./common/directive/autofocus/auto-focus.directive"; @@ -134,6 +133,11 @@ import { } from "./system-settings/admin-domain/domain-member-panel/member-dialog/member-dialog.component"; import {MatAutocompleteModule} from "@angular/material/autocomplete"; import {MembershipService} from "./system-settings/admin-domain/domain-member-panel/membership.service"; +import {AdminUserComponent} from "./system-settings/admin-users/admin-user.component"; +import {AdminUserService} from "./system-settings/admin-users/admin-user.service"; +import { + UserProfilePanelComponent +} from "./system-settings/admin-users/user-settings-panel/user-profile-panel.component"; @NgModule({ @@ -143,6 +147,7 @@ import {MembershipService} from "./system-settings/admin-domain/domain-member-pa AdminDomainComponent, AdminTruststoreComponent, AdminKeystoreComponent, + AdminUserComponent, AlertComponent, AlertMessageComponent, AppComponent, @@ -204,6 +209,7 @@ import {MembershipService} from "./system-settings/admin-domain/domain-member-pa UserComponent, UserDetailsDialogComponent, UserProfileComponent, + UserProfilePanelComponent, ], imports: [ BrowserAnimationsModule, @@ -248,8 +254,8 @@ import {MembershipService} from "./system-settings/admin-domain/domain-member-pa AdminDomainService, AdminKeystoreService, AdminTruststoreService, + AdminUserService, AlertMessageService, - AuthenticatedGuard, AuthorizedAdminGuard, AuthorizedGuard, CertificateService, diff --git a/smp-angular/src/app/app.routes.ts b/smp-angular/src/app/app.routes.ts index f0f264360..4d2de6730 100644 --- a/smp-angular/src/app/app.routes.ts +++ b/smp-angular/src/app/app.routes.ts @@ -1,13 +1,11 @@ import {RouterModule, Routes} from '@angular/router'; import {LoginComponent} from './login/login.component'; import {ServiceGroupSearchComponent} from './service-group-search/service-group-search.component'; -import {ServiceGroupEditComponent} from './service-group-edit/service-group-edit.component'; -import {AuthenticatedGuard} from './guards/authenticated.guard'; import {UserComponent} from './system-settings/user/user.component'; import {AlertComponent} from "./alert/alert.component"; import {PropertyComponent} from "./system-settings/property/property.component"; import {UserProfileComponent} from "./user-settings/user-profile/user-profile.component"; -import {authGuard} from "./guards/auth.guard"; +import {authenticationGuard} from "./guards/authentication.guard"; import {UserAccessTokensComponent} from "./user-settings/user-access-tokens/user-access-tokens.component"; import {UserCertificatesComponent} from "./user-settings/user-certificates/user-certificates.component"; import {ExtensionComponent} from "./system-settings/admin-extension/extension.component"; @@ -15,25 +13,29 @@ import {AdminTruststoreComponent} from "./system-settings/admin-truststore/admin import {AdminKeystoreComponent} from "./system-settings/admin-keystore/admin-keystore.component"; import {AdminDomainComponent} from "./system-settings/admin-domain/admin-domain.component"; import {dirtyDeactivateGuard} from "./guards/dirty.guard"; +import {AdminUserComponent} from "./system-settings/admin-users/admin-user.component"; const appRoutes: Routes = [ {path: '', component: ServiceGroupSearchComponent}, {path: 'search', redirectTo: ''}, + {path: 'login', component: LoginComponent}, { - path: 'edit', - component: ServiceGroupEditComponent, - canActivate: [AuthenticatedGuard], - canDeactivate: [dirtyDeactivateGuard] + path: 'administration', + canActivateChild: [authenticationGuard], + children: [ + {path: 'admin-domain', component: AdminDomainComponent, canDeactivate: [dirtyDeactivateGuard]}, + {path: 'admin-group', component: UserComponent, canDeactivate: [dirtyDeactivateGuard]}, + {path: 'admin-resource', component: PropertyComponent, canDeactivate: [dirtyDeactivateGuard]} + ] }, - {path: 'login', component: LoginComponent}, { path: 'system-settings', - canActivateChild: [authGuard], + canActivateChild: [authenticationGuard], children: [ {path: 'domain', component: AdminDomainComponent, canDeactivate: [dirtyDeactivateGuard]}, - {path: 'user', component: UserComponent, canDeactivate: [dirtyDeactivateGuard]}, + {path: 'user', component: AdminUserComponent, canDeactivate: [dirtyDeactivateGuard]}, {path: 'properties', component: PropertyComponent, canDeactivate: [dirtyDeactivateGuard]}, {path: 'keystore', component: AdminKeystoreComponent, canDeactivate: [dirtyDeactivateGuard]}, {path: 'truststore', component: AdminTruststoreComponent, canDeactivate: [dirtyDeactivateGuard]}, @@ -43,7 +45,7 @@ const appRoutes: Routes = [ }, { path: 'user-settings', - canActivateChild: [authGuard], + canActivateChild: [authenticationGuard], children: [ {path: 'user-profile', component: UserProfileComponent, canDeactivate: [dirtyDeactivateGuard]}, {path: 'user-access-token', component: UserAccessTokensComponent, canDeactivate: [dirtyDeactivateGuard]}, diff --git a/smp-angular/src/app/common/enums/application-role.enum.ts b/smp-angular/src/app/common/enums/application-role.enum.ts new file mode 100644 index 000000000..f2b05c36d --- /dev/null +++ b/smp-angular/src/app/common/enums/application-role.enum.ts @@ -0,0 +1,4 @@ +export enum ApplicationRoleEnum { + USER = 'USER', + SYSTEM_ADMIN = 'SYSTEM_ADMIN' +} diff --git a/smp-angular/src/app/system-settings/admin-domain/domain-member-panel/member-dialog/member-type.enum.ts b/smp-angular/src/app/common/enums/member-type.enum.ts similarity index 100% rename from smp-angular/src/app/system-settings/admin-domain/domain-member-panel/member-dialog/member-type.enum.ts rename to smp-angular/src/app/common/enums/member-type.enum.ts diff --git a/smp-angular/src/app/system-settings/admin-domain/domain-member-panel/membership-role.enum.ts b/smp-angular/src/app/common/enums/membership-role.enum.ts similarity index 100% rename from smp-angular/src/app/system-settings/admin-domain/domain-member-panel/membership-role.enum.ts rename to smp-angular/src/app/common/enums/membership-role.enum.ts diff --git a/smp-angular/src/app/common/panels/data-panel/data-panel.component.html b/smp-angular/src/app/common/panels/data-panel/data-panel.component.html index 401712adb..84e0662ce 100644 --- a/smp-angular/src/app/common/panels/data-panel/data-panel.component.html +++ b/smp-angular/src/app/common/panels/data-panel/data-panel.component.html @@ -1,5 +1,5 @@ -<div class="panel smp-container-limited"> - <div class="smp-column-label"> +<div class="panel" [ngClass]="{ 'smp-container-limited': showTitle}"> + <div *ngIf="showTitle" class="smp-column-label"> <p>{{title}}</p> <div innerHTML={{text}}></div> <ng-container *ngTemplateOutlet="labelColumnContent"></ng-container> diff --git a/smp-angular/src/app/common/panels/data-panel/data-panel.component.ts b/smp-angular/src/app/common/panels/data-panel/data-panel.component.ts index fd7ab7b43..1e39b5fff 100644 --- a/smp-angular/src/app/common/panels/data-panel/data-panel.component.ts +++ b/smp-angular/src/app/common/panels/data-panel/data-panel.component.ts @@ -12,6 +12,7 @@ import { export class DataPanelComponent { @Input() title: string; + @Input() showTitle: boolean=true; @Input() text: string; @Input() labelColumnContent: TemplateRef<any>; diff --git a/smp-angular/src/app/common/search-table/search-table-entity.model.ts b/smp-angular/src/app/common/search-table/search-table-entity.model.ts index 10b37f09d..7f1d89cbd 100644 --- a/smp-angular/src/app/common/search-table/search-table-entity.model.ts +++ b/smp-angular/src/app/common/search-table/search-table-entity.model.ts @@ -3,7 +3,7 @@ import {EntityStatus} from '../model/entity-status.model'; export interface SearchTableEntity { id?: number; index?: number; - status: EntityStatus; + status?: EntityStatus; deleted?: boolean; actionMessage?: string; diff --git a/smp-angular/src/app/common/search-table/search-table.component.ts b/smp-angular/src/app/common/search-table/search-table.component.ts index bead1e97d..53451841d 100644 --- a/smp-angular/src/app/common/search-table/search-table.component.ts +++ b/smp-angular/src/app/common/search-table/search-table.component.ts @@ -17,7 +17,7 @@ import {ConfirmationDialogComponent} from "../dialogs/confirmation-dialog/confir import {SearchTableValidationResult} from "./search-table-validation-result.model"; import {ExtendedHttpClient} from "../../http/extended-http-client"; import {Router} from "@angular/router"; -import {AuthenticatedGuard} from "../../guards/authenticated.guard"; +import {authenticationGuard} from "../../guards/authentication.guard"; import ObjectUtils from "../utils/object-utils"; @Component({ @@ -77,7 +77,7 @@ export class SearchTableComponent implements OnInit { protected alertService: AlertMessageService, private downloadService: DownloadService, public dialog: MatDialog, - private router: Router, private authenticatedGuard: AuthenticatedGuard) { + private router: Router) { } ngOnInit(): void { @@ -243,11 +243,7 @@ export class SearchTableComponent implements OnInit { onNewButtonClicked() { - this.authenticatedGuard.canActivate(this.router.routerState.snapshot.root, this.router.routerState.snapshot).subscribe(authorized => { - if (authorized) { this.fireCreateNewEntityEvent(); - } - }) } fireCreateNewEntityEvent() { @@ -267,11 +263,7 @@ export class SearchTableComponent implements OnInit { } onDeleteButtonClicked() { - this.authenticatedGuard.canActivate(this.router.routerState.snapshot.root, this.router.routerState.snapshot).subscribe(authorized => { - if (authorized) { this.fireDeleteEntityEvent(); - } - }) } fireDeleteEntityEvent() { @@ -279,20 +271,11 @@ export class SearchTableComponent implements OnInit { } onDeleteRowActionClicked(row: SearchTableEntity) { - this.authenticatedGuard.canActivate(this.router.routerState.snapshot.root, this.router.routerState.snapshot).subscribe(authorized => { - if (authorized) { this.deleteSearchTableEntities([row]); - } - }) - } onEditButtonClicked() { - this.authenticatedGuard.canActivate(this.router.routerState.snapshot.root, this.router.routerState.snapshot).subscribe(authorized => { - if (authorized) { this.fireEditEntityEvent(); - } - }) } fireEditEntityEvent() { diff --git a/smp-angular/src/app/guards/authenticated.guard.ts b/smp-angular/src/app/guards/authenticated.guard.ts deleted file mode 100644 index 2772ce203..000000000 --- a/smp-angular/src/app/guards/authenticated.guard.ts +++ /dev/null @@ -1,32 +0,0 @@ -import {Injectable} from '@angular/core'; -import {Router, CanActivate, ActivatedRouteSnapshot, RouterStateSnapshot} from '@angular/router'; -import {SecurityService} from '../security/security.service'; -import {ReplaySubject} from 'rxjs'; -import {AlertMessageService} from "../common/alert-message/alert-message.service"; - -/** - * Authentication guard validates if user is logged in. If not it re - */ -@Injectable() -export class AuthenticatedGuard implements CanActivate { - - constructor(private router: Router, private securityService: SecurityService, private alertService: AlertMessageService) { - } - - canActivate(route: ActivatedRouteSnapshot, state: RouterStateSnapshot) { - const subject = new ReplaySubject<boolean>(); - this.securityService.isAuthenticated(true).subscribe((isAuthenticated: boolean) => { - if(isAuthenticated) { - subject.next(true); - } else { - console.log("User session is not active") - // not logged in so redirect to login page with the return url - this.router.navigate(['/login'], {queryParams: {returnUrl: state.url}}); - subject.next(false); - this.alertService.error('You have been logged out because of inactivity or missing access permissions.', true); - } - }); - return subject.asObservable(); - - } -} diff --git a/smp-angular/src/app/guards/auth.guard.ts b/smp-angular/src/app/guards/authentication.guard.ts similarity index 95% rename from smp-angular/src/app/guards/auth.guard.ts rename to smp-angular/src/app/guards/authentication.guard.ts index 4b8f9fc1e..d9bfd8769 100644 --- a/smp-angular/src/app/guards/auth.guard.ts +++ b/smp-angular/src/app/guards/authentication.guard.ts @@ -4,7 +4,7 @@ import {SecurityService} from "../security/security.service"; import {AlertMessageService} from "../common/alert-message/alert-message.service"; import {NavigationService} from "../window/sidenav/navigation-model.service"; -export const authGuard = () => { +export const authenticationGuard = () => { const navigationService = inject(NavigationService); const securityService = inject(SecurityService); const alertService = inject(AlertMessageService); diff --git a/smp-angular/src/app/security/security.service.ts b/smp-angular/src/app/security/security.service.ts index cd2e49f07..2a14888d9 100644 --- a/smp-angular/src/app/security/security.service.ts +++ b/smp-angular/src/app/security/security.service.ts @@ -14,6 +14,8 @@ export class SecurityService { readonly LOCAL_STORAGE_KEY_CURRENT_USER = 'currentUser'; + + constructor( private http: HttpClient, private alertService: AlertMessageService, diff --git a/smp-angular/src/app/smp.constants.ts b/smp-angular/src/app/smp.constants.ts index fcddea6f7..94438eac0 100644 --- a/smp-angular/src/app/smp.constants.ts +++ b/smp-angular/src/app/smp.constants.ts @@ -10,8 +10,10 @@ export class SmpConstants { public static readonly PATH_ACTION_DELETE = 'delete'; public static readonly PATH_ACTION_UPDATE = 'update'; public static readonly PATH_ACTION_CREATE = 'create'; - public static readonly PATH_ACTION_ADD = 'add'; + public static readonly PATH_ACTION_RETRIEVE = 'retrieve'; + + public static readonly PATH_ACTION_SEARCH = 'search'; public static readonly PATH_ACTION_UPDATE_RESOURCE_TYPES = 'update-resource-types'; public static readonly PATH_ACTION_UPDATE_SML_INTEGRATION = 'update-sml-integration-data'; @@ -31,7 +33,7 @@ export class SmpConstants { // public endpoints public static readonly REST_PUBLIC = 'public/rest/'; public static readonly REST_INTERNAL = 'internal/rest/'; - public static readonly REST_PUBLIC_SEARCH_SERVICE_GROUP = SmpConstants.REST_PUBLIC + 'search'; + public static readonly REST_PUBLIC_SEARCH_SERVICE_GROUP = SmpConstants.REST_PUBLIC + SmpConstants.PATH_ACTION_SEARCH; public static readonly REST_PUBLIC_DOMAIN_MANAGE = SmpConstants.REST_PUBLIC + 'domain'; public static readonly REST_PUBLIC_DOMAIN_MEMBERS = SmpConstants.REST_PUBLIC_DOMAIN_MANAGE + "/" + SmpConstants.PATH_PARAM_ENC_USER_ID + "/"+ SmpConstants.PATH_PARAM_ENC_DOMAIN_ID + "/"+ "members"; @@ -54,7 +56,7 @@ export class SmpConstants { public static readonly REST_PUBLIC_USER_GENERATE_ACCESS_TOKEN = SmpConstants.REST_PUBLIC_USER_UPDATE + 'generate-access-token'; public static readonly REST_PUBLIC_USER_CHANGE_PASSWORD = SmpConstants.REST_PUBLIC_USER_UPDATE + 'change-password'; - public static readonly REST_PUBLIC_USER_SEARCH = SmpConstants.REST_PUBLIC_USER + "/" + SmpConstants.PATH_PARAM_ENC_USER_ID + "/" + 'search' + public static readonly REST_PUBLIC_USER_SEARCH = SmpConstants.REST_PUBLIC_USER + "/" + SmpConstants.PATH_PARAM_ENC_USER_ID + "/" + SmpConstants.PATH_ACTION_SEARCH; // truststore public services public static readonly REST_PUBLIC_TRUSTSTORE = SmpConstants.REST_PUBLIC + "truststore/" + "/" + SmpConstants.PATH_PARAM_ENC_USER_ID + "/"; public static readonly REST_PUBLIC_TRUSTSTORE_CERT_VALIDATE = SmpConstants.REST_PUBLIC_TRUSTSTORE + 'validate-certificate'; @@ -104,12 +106,24 @@ export class SmpConstants { public static readonly REST_INTERNAL_PROPERTY_MANAGE = SmpConstants.REST_INTERNAL + 'property'; public static readonly REST_INTERNAL_PROPERTY_VALIDATE = SmpConstants.REST_INTERNAL_PROPERTY_MANAGE + '/validate'; public static readonly REST_INTERNAL_DOMAIN_VALIDATE_DELETE = SmpConstants.REST_INTERNAL_DOMAIN_MANAGE_DEPRECATED + '/validate-delete'; - public static readonly REST_INTERNAL_USER_MANAGE = SmpConstants.REST_INTERNAL + 'user'; + public static readonly REST_INTERNAL_USER_MANAGE = SmpConstants.REST_INTERNAL + 'user' +'/' + SmpConstants.PATH_PARAM_ENC_USER_ID; + + public static readonly REST_INTERNAL_USER_MANAGE_CREATE = SmpConstants.REST_INTERNAL_USER_MANAGE + '/' + SmpConstants.PATH_ACTION_CREATE; + public static readonly REST_INTERNAL_USER_MANAGE_UPDATE = SmpConstants.REST_INTERNAL_USER_MANAGE + '/' + SmpConstants.PATH_PARAM_ENC_MANAGED_USER_ID +'/' + SmpConstants.PATH_ACTION_UPDATE; + public static readonly REST_INTERNAL_USER_MANAGE_DELETE = SmpConstants.REST_INTERNAL_USER_MANAGE + '/' + SmpConstants.PATH_PARAM_ENC_MANAGED_USER_ID +'/' + SmpConstants.PATH_ACTION_DELETE; + + public static readonly INTERNAL_USER_MANAGE_SEARCH = SmpConstants.REST_INTERNAL_USER_MANAGE + '/' +SmpConstants.PATH_ACTION_SEARCH; + + public static readonly REST_INTERNAL_USER_MANAGE_DATA = SmpConstants.REST_INTERNAL_USER_MANAGE + + '/' + SmpConstants.PATH_PARAM_ENC_MANAGED_USER_ID + '/' + SmpConstants.PATH_ACTION_RETRIEVE; + + + public static readonly REST_INTERNAL_USER_GENERATE_ACCESS_TOKEN = SmpConstants.REST_INTERNAL_USER_MANAGE + - '/' + SmpConstants.PATH_PARAM_ENC_USER_ID + '/' + 'generate-access-token-for' + '/' + SmpConstants.PATH_PARAM_ENC_MANAGED_USER_ID; + '/' + 'generate-access-token-for' + '/' + SmpConstants.PATH_PARAM_ENC_MANAGED_USER_ID; public static readonly REST_INTERNAL_USER_CHANGE_PASSWORD = SmpConstants.REST_INTERNAL_USER_MANAGE + - '/' + SmpConstants.PATH_PARAM_ENC_USER_ID + '/' + 'change-password-for' + '/' + SmpConstants.PATH_PARAM_ENC_MANAGED_USER_ID; + '/' + 'change-password-for' + '/' + SmpConstants.PATH_PARAM_ENC_MANAGED_USER_ID; public static readonly REST_INTERNAL_USER_VALIDATE_DELETE = `${SmpConstants.REST_INTERNAL_USER_MANAGE}/validate-delete`; public static readonly REST_INTERNAL_KEYSTORE_DEPRECATED = SmpConstants.REST_INTERNAL + 'keystore'; diff --git a/smp-angular/src/app/system-settings/admin-domain/domain-member-panel/domain-member-panel.component.html b/smp-angular/src/app/system-settings/admin-domain/domain-member-panel/domain-member-panel.component.html index 21ab9c182..bcbf9dfe2 100644 --- a/smp-angular/src/app/system-settings/admin-domain/domain-member-panel/domain-member-panel.component.html +++ b/smp-angular/src/app/system-settings/admin-domain/domain-member-panel/domain-member-panel.component.html @@ -18,14 +18,10 @@ <mat-icon>delete</mat-icon> <span>Delete selected member</span> </button> - - - - </mat-toolbar-row> </mat-toolbar> <h3>Domain members</h3> - <div class="domain-member-container mat-elevation-z8"> + <div class="domain-member-container mat-elevation-z2"> <div class="domain-member-loading-shade" *ngIf="isLoadingResults"> <mat-spinner *ngIf="isLoadingResults"></mat-spinner> @@ -63,10 +59,7 @@ <td mat-cell *matCellDef="let row">{{row.memberOf}}</td> </ng-container> - <tr mat-header-row *matHeaderRowDef="displayedColumns"></tr> - - <tr mat-row *matRowDef="let odd = odd; let row; columns: displayedColumns;" (click)="memberSelected(row)" [ngClass]="{'datatable-row-selected': row==selectedMember,'datatable-row-odd': odd}" diff --git a/smp-angular/src/app/system-settings/admin-domain/domain-member-panel/domain-member-panel.component.ts b/smp-angular/src/app/system-settings/admin-domain/domain-member-panel/domain-member-panel.component.ts index 77ed57a39..b085e301d 100644 --- a/smp-angular/src/app/system-settings/admin-domain/domain-member-panel/domain-member-panel.component.ts +++ b/smp-angular/src/app/system-settings/admin-domain/domain-member-panel/domain-member-panel.component.ts @@ -11,8 +11,8 @@ import {finalize} from "rxjs/operators"; import {TableResult} from "./table-result.model"; import {MemberDialogComponent} from "./member-dialog/member-dialog.component"; import {MembershipService} from "./membership.service"; -import {MembershipRoleEnum} from "./membership-role.enum"; -import {MemberTypeEnum} from "./member-dialog/member-type.enum"; +import {MembershipRoleEnum} from "../../../common/enums/membership-role.enum"; +import {MemberTypeEnum} from "../../../common/enums/member-type.enum"; @Component({ @@ -28,15 +28,9 @@ export class DomainMemberPanelComponent implements BeforeLeaveGuard { domainForm: FormGroup; displayedColumns: string[] = ['username', 'fullName', 'roleType', 'memberOf']; - data: MemberRo[] = []; - selectedMember: MemberRo; - filter: any = {}; - - filterName: string; - resultsLength = 0; isLoadingResults = false; @ViewChild(MatPaginator) paginator: MatPaginator; diff --git a/smp-angular/src/app/system-settings/admin-domain/domain-member-panel/member-dialog/member-dialog.component.ts b/smp-angular/src/app/system-settings/admin-domain/domain-member-panel/member-dialog/member-dialog.component.ts index ae0887867..eafbf60c8 100644 --- a/smp-angular/src/app/system-settings/admin-domain/domain-member-panel/member-dialog/member-dialog.component.ts +++ b/smp-angular/src/app/system-settings/admin-domain/domain-member-panel/member-dialog/member-dialog.component.ts @@ -1,13 +1,13 @@ import {Component, Inject, Input, OnInit} from '@angular/core'; import {MAT_DIALOG_DATA, MatDialogRef} from '@angular/material/dialog'; import {FormBuilder, FormControl, FormGroup} from "@angular/forms"; -import {MembershipRoleEnum} from "../membership-role.enum"; +import {MembershipRoleEnum} from "../../../../common/enums/membership-role.enum"; import {Observable} from "rxjs"; import {SearchUserRo} from "./search-user-ro.model"; import {MembershipService} from "../membership.service"; import {MemberRo} from "../member-ro.model"; import {DomainRo} from "../../domain-ro.model"; -import {MemberTypeEnum} from "./member-type.enum"; +import {MemberTypeEnum} from "../../../../common/enums/member-type.enum"; import {AlertMessageService} from "../../../../common/alert-message/alert-message.service"; diff --git a/smp-angular/src/app/system-settings/admin-domain/domain-member-panel/member-ro.model.ts b/smp-angular/src/app/system-settings/admin-domain/domain-member-panel/member-ro.model.ts index d42464599..762f7f04e 100644 --- a/smp-angular/src/app/system-settings/admin-domain/domain-member-panel/member-ro.model.ts +++ b/smp-angular/src/app/system-settings/admin-domain/domain-member-panel/member-ro.model.ts @@ -1,7 +1,7 @@ -import {MembershipRoleEnum} from "./membership-role.enum"; +import {MembershipRoleEnum} from "../../../common/enums/membership-role.enum"; import {SearchTableEntity} from "../../../common/search-table/search-table-entity.model"; -import {MemberTypeEnum} from "./member-dialog/member-type.enum"; +import {MemberTypeEnum} from "../../../common/enums/member-type.enum"; export interface MemberRo extends SearchTableEntity { diff --git a/smp-angular/src/app/system-settings/admin-domain/domain-member-panel/membership.service.ts b/smp-angular/src/app/system-settings/admin-domain/domain-member-panel/membership.service.ts index 364f024dc..17a8531d0 100644 --- a/smp-angular/src/app/system-settings/admin-domain/domain-member-panel/membership.service.ts +++ b/smp-angular/src/app/system-settings/admin-domain/domain-member-panel/membership.service.ts @@ -9,7 +9,7 @@ import {AlertMessageService} from "../../../common/alert-message/alert-message.s import {MemberRo} from "./member-ro.model"; import {TableResult} from "./table-result.model"; import {SearchUserRo} from "./member-dialog/search-user-ro.model"; -import {MembershipRoleEnum} from "./membership-role.enum"; +import {MembershipRoleEnum} from "../../../common/enums/membership-role.enum"; @Injectable() diff --git a/smp-angular/src/app/system-settings/admin-users/admin-user.component.css b/smp-angular/src/app/system-settings/admin-users/admin-user.component.css new file mode 100644 index 000000000..b65965b4b --- /dev/null +++ b/smp-angular/src/app/system-settings/admin-users/admin-user.component.css @@ -0,0 +1,19 @@ + +#admin-user-panel { + display: flex; + flex-flow: column; + align-items: center; + height: 100%; + min-height: 600px; + padding: 0 2em; +} +#user-filter { + width: 100%; + padding-top: 1em; +} + + +#admin-user-table { + width: 100%; + padding-top: 1em; +} diff --git a/smp-angular/src/app/system-settings/admin-users/admin-user.component.html b/smp-angular/src/app/system-settings/admin-users/admin-user.component.html new file mode 100644 index 000000000..09f488c7f --- /dev/null +++ b/smp-angular/src/app/system-settings/admin-users/admin-user.component.html @@ -0,0 +1,74 @@ +<div id="admin-user-panel"> + <data-panel id="admin-user-data-panel" + title="System User administration" + text="System User administration panel is a tool for creating and removing users from DomiSMP" + [labelColumnContent]="searchUserPanel"> + <user-profile-panel + [showDataPanelTitles] =false + [managedUserData]="managedUserData" + (onSaveUserEvent)="onSaveUserEvent($event)" + (onDiscardNew)="onDiscardNew()" + ></user-profile-panel> + </data-panel> +</div> + +<ng-template #searchUserPanel> + <mat-form-field id="domain-filter"> + <mat-label>Filter Users</mat-label> + <input matInput (keyup)="applyUserFilter($event)" placeholder="User name or full name" #inputUserFilter> + </mat-form-field> + + <mat-toolbar> + <mat-toolbar-row class="mat-elevation-z5"> + <button mat-raised-button + mat-flat-button color="primary" + (click)="onCreateUserClicked()" + >Create User + </button> + + <button mat-raised-button + [disabled]="canNotDelete" + color="primary" + (click)="onDeleteSelectedUserClicked()"> + <mat-icon>delete</mat-icon> + <span>Delete selected</span> + </button> + </mat-toolbar-row> + </mat-toolbar> + <table class="mat-elevation-z2" id="admin-domain-table" mat-table [dataSource]="userData" > + <ng-container matColumnDef="username"> + <th mat-header-cell *matHeaderCellDef >Username</th> + <td mat-cell *matCellDef="let row" [matTooltip]="row.username">{{row.username}}</td> + </ng-container> + + <ng-container matColumnDef="fullName"> + <th mat-header-cell *matHeaderCellDef >Full name</th> + <td mat-cell *matCellDef="let row" [matTooltip]="row.username">{{row.fullName}}</td> + </ng-container> + + <tr mat-header-row *matHeaderRowDef="displayedColumns"></tr> + <tr mat-row *matRowDef="let odd = odd; let row; columns: displayedColumns;" + (click)="userSelected(row)" + [ngClass]="{'datatable-row-selected': row===selected,'datatable-row-odd': odd}" + ></tr> + + + <tr class="mat-row" *matNoDataRow> + <td *ngIf="inputUserFilter.value;else noDataFound" class="mat-cell" colspan="2">No users matching the filter + "{{inputUserFilter.value}}" + </td> + <ng-template #noDataFound> + <td class="mat-cell" colspan="2">No data</td> + </ng-template> + </tr> + </table> + + <mat-paginator class="mat-elevation-z2" id="extension-paginator" + [length]="resultsLength" + (page)="onPageChanged($event)" + [pageSize]="5" + [pageSizeOptions]="[5, 10, 25]" + [showFirstLastButtons]="true" + aria-label="Select page"></mat-paginator> + +</ng-template> diff --git a/smp-angular/src/app/system-settings/admin-users/admin-user.component.ts b/smp-angular/src/app/system-settings/admin-users/admin-user.component.ts new file mode 100644 index 000000000..0068df9f7 --- /dev/null +++ b/smp-angular/src/app/system-settings/admin-users/admin-user.component.ts @@ -0,0 +1,252 @@ +import {AfterViewInit, Component, ViewChild} from '@angular/core'; +import {MatPaginator, PageEvent} from "@angular/material/paginator"; +import {AlertMessageService} from "../../common/alert-message/alert-message.service"; +import {ConfirmationDialogComponent} from "../../common/dialogs/confirmation-dialog/confirmation-dialog.component"; +import {MatDialog, MatDialogConfig, MatDialogRef} from "@angular/material/dialog"; +import {BeforeLeaveGuard} from "../../window/sidenav/navigation-on-leave-guard"; +import {CancelDialogComponent} from "../../common/dialogs/cancel-dialog/cancel-dialog.component"; +import {SearchUserRo} from "../admin-domain/domain-member-panel/member-dialog/search-user-ro.model"; +import {AdminUserService} from "./admin-user.service"; +import {TableResult} from "../admin-domain/domain-member-panel/table-result.model"; +import {finalize} from "rxjs/operators"; +import {UserRo} from "../user/user-ro.model"; +import {SecurityService} from "../../security/security.service"; +import { + PasswordChangeDialogComponent +} from "../../common/dialogs/password-change-dialog/password-change-dialog.component"; +import {UserDetailsDialogMode} from "../user/user-details-dialog/user-details-dialog.component"; +import {ApplicationRoleEnum} from "../../common/enums/application-role.enum"; + + +@Component({ + moduleId: module.id, + templateUrl: './admin-user.component.html', + styleUrls: ['./admin-user.component.css'] +}) +export class AdminUserComponent implements AfterViewInit, BeforeLeaveGuard { + displayedColumns: string[] = ['username', 'fullName']; + + selected?: SearchUserRo; + + managedUserData?: UserRo; + + userData: SearchUserRo[]; + filter: string; + resultsLength: number = 0; + isLoadingResults: boolean = false; + + + @ViewChild(MatPaginator) paginator: MatPaginator; + + constructor(private adminUserService: AdminUserService, + private securityService: SecurityService, + private alertService: AlertMessageService, + private dialog: MatDialog) { + + + } + + ngAfterViewInit() { + this.loadTableData(); + } + + onPageChanged(page: PageEvent) { + this.loadTableData(); + } + + applyUserFilter(event: Event) { + const filterValue = (event.target as HTMLInputElement).value; + if (this.filter === filterValue) { + return; + } + this.filter = filterValue; + this.loadTableData(); + } + + loadTableData() { + + this.isLoadingResults = true; + + this.adminUserService.getUsersObservable(this.filter, this.paginator.pageIndex, this.paginator.pageSize) + .pipe( + finalize(() => { + this.isLoadingResults = false; + })) + .subscribe((result: TableResult<SearchUserRo>) => { + this.userData = [...result.serviceEntities]; + this.resultsLength = result.count; + this.isLoadingResults = false; + } + ); + } + + + onCreateUserClicked() { + this.selected = null; + this.managedUserData = { + active: true, + username: "", + role: ApplicationRoleEnum.USER + } + } + + + onDiscardNew() { + this.selected = null; + this.managedUserData = null; + } + + public userSelected(userSelected: SearchUserRo) { + if (this.selected === userSelected) { + return; + } + if (this.isDirty()) { + let canChangeTab = this.dialog.open(CancelDialogComponent).afterClosed().toPromise<boolean>(); + canChangeTab.then((canChange: boolean) => { + if (canChange) { + this.selectAndRetrieveUserData(userSelected); + } + }); + } else { + console.log("set selected 1 "); + this.selectAndRetrieveUserData(userSelected); + } + } + + + public selectAndRetrieveUserData(selectUser: SearchUserRo) { + // clear old data + this.managedUserData = null; + if (!selectUser) { + return; + + } + this.adminUserService.getUserDataObservable(selectUser.userId).subscribe((user: UserRo) => { + if (user) { + this.managedUserData = user; + this.selected = selectUser; + } + }, (error) => { + this.alertService.error(error.error?.errorDescription) + }); + } + + onSaveUserEvent(user: UserRo) { + if (!user.userId) { + this.createUserData(user); + } else { + this.updateUserData(user); + } + } + + updateUserData(user: UserRo) { + // change only allowed data + this.adminUserService.updateManagedUser(user).subscribe(user => { + if (user) { + this.selected = null; + this.managedUserData = null; + this.loadTableData(); + this.alertService.success("User [" + user.username + "] updated!"); + } + }, (error) => { + this.alertService.error(error.error?.errorDescription) + }); + } + + createUserData(user: UserRo) { + // change only allowed data + this.adminUserService.createManagedUser(user).subscribe(user => { + if (user) { + this.selected = null; + this.managedUserData = null; + this.loadTableData(); + this.alertService.success("User [" + user.username + "] created!"); + } + }, (error) => { + this.alertService.error(error.error?.errorDescription) + }); + } + + onDeleteSelectedUserClicked() { + + this.dialog.open(ConfirmationDialogComponent, { + data: { + title: "Delete user " + this.managedUserData?.username + " from DomiSMP", + description: "Action will permanently delete user! Do you wish to continue?" + } + }).afterClosed().subscribe(result => { + if (result) { + this.deleteUser(this.managedUserData); + } + }); + } + + deleteUser(user: UserRo) { + + // change only allowed data + this.adminUserService.deleteManagedUser(user).subscribe(user => { + if (user) { + this.selected = null; + this.managedUserData = null; + this.loadTableData(); + this.alertService.success("User [" + user.username + "] deleted!"); + } + }, (error) => { + this.alertService.error(error.error?.errorDescription) + }); + + } + + changeUserPasswordEvent(user: UserRo) { + const formRef: MatDialogRef<any> = this.changePasswordDialog({ + data: { + user: user, + adminUser: user.userId != this.securityService.getCurrentUser().userId + }, + }); + formRef.afterClosed().subscribe(result => { + if (result) { + //this.currentUserData.passwordExpireOn = result.passwordExpireOn; + //this.currentUserData = {...this.currentUserData} + } + }); + } + + public changePasswordDialog(config?: MatDialogConfig): MatDialogRef<PasswordChangeDialogComponent> { + return this.dialog.open(PasswordChangeDialogComponent, this.convertConfig(config)); + } + + + private convertConfig(config) { + return (config && config.data) + ? { + ...config, + data: { + ...config.data, + mode: config.data.mode || (config.data.edit ? UserDetailsDialogMode.EDIT_MODE : UserDetailsDialogMode.NEW_MODE) + } + } + : config; + } + + isDirty(): boolean { + return false; + } + + + isNew(): boolean { + return !this.selected && !this.selected?.userId + } + + get canNotDelete(): boolean { + return !this.selected || this.isLoggedInUser + } + + get editMode(): boolean { + return this.isDirty(); + } + + get isLoggedInUser() { + return this.securityService.getCurrentUser()?.userId == this.managedUserData?.userId + } +} diff --git a/smp-angular/src/app/system-settings/admin-users/admin-user.service.ts b/smp-angular/src/app/system-settings/admin-users/admin-user.service.ts new file mode 100644 index 000000000..a445a542f --- /dev/null +++ b/smp-angular/src/app/system-settings/admin-users/admin-user.service.ts @@ -0,0 +1,74 @@ +import {Injectable} from '@angular/core'; +import {Observable} from 'rxjs'; +import {HttpClient, HttpParams} from "@angular/common/http"; +import {SecurityService} from "../../security/security.service"; +import {AlertMessageService} from "../../common/alert-message/alert-message.service"; +import {SearchTableResult} from "../../common/search-table/search-table-result.model"; +import {User} from "../../security/user.model"; +import {TableResult} from "../admin-domain/domain-member-panel/table-result.model"; +import {MemberRo} from "../admin-domain/domain-member-panel/member-ro.model"; +import {SmpConstants} from "../../smp.constants"; +import {UserRo} from "../user/user-ro.model"; + + +@Injectable() +export class AdminUserService { + + + constructor( + private http: HttpClient, + private securityService: SecurityService) { + } + + getUsersObservable(filter: string, page: number, pageSize: number): Observable<SearchTableResult> { + const currentUser: User = this.securityService.getCurrentUser(); + + let params: HttpParams = new HttpParams() + .set('page', page.toString()) + .set('pageSize', pageSize.toString()) + .set('filter', !filter ? "" : filter); + + return this.http.get<TableResult<MemberRo>>(SmpConstants.INTERNAL_USER_MANAGE_SEARCH + .replace(SmpConstants.PATH_PARAM_ENC_USER_ID, currentUser.userId), {params}); + } + + getUserDataObservable(userId: string): Observable<UserRo> { + let user = this.securityService.getCurrentUser(); + if (!user) { + return null; + } + return this.http.get<UserRo>(SmpConstants.REST_INTERNAL_USER_MANAGE_DATA + .replace(SmpConstants.PATH_PARAM_ENC_USER_ID, user.userId) + .replace(SmpConstants.PATH_PARAM_ENC_MANAGED_USER_ID, userId)); + } + + updateManagedUser(managedUser: UserRo): Observable<UserRo> { + let user = this.securityService.getCurrentUser(); + if (!user) { + return null; + } + return this.http.post<UserRo>(SmpConstants.REST_INTERNAL_USER_MANAGE_UPDATE + .replace(SmpConstants.PATH_PARAM_ENC_USER_ID, user.userId) + .replace(SmpConstants.PATH_PARAM_ENC_MANAGED_USER_ID, managedUser.userId), managedUser); + } + + createManagedUser(managedUser: UserRo): Observable<UserRo> { + let user = this.securityService.getCurrentUser(); + if (!user) { + return null; + } + return this.http.put<UserRo>(SmpConstants.REST_INTERNAL_USER_MANAGE_CREATE + .replace(SmpConstants.PATH_PARAM_ENC_USER_ID, user.userId) + , managedUser); + } + + deleteManagedUser(managedUser: UserRo): Observable<UserRo> { + let user = this.securityService.getCurrentUser(); + if (!user) { + return null; + } + return this.http.delete<UserRo>(SmpConstants.REST_INTERNAL_USER_MANAGE_DELETE + .replace(SmpConstants.PATH_PARAM_ENC_USER_ID, user.userId) + .replace(SmpConstants.PATH_PARAM_ENC_MANAGED_USER_ID, managedUser.userId)); + } +} diff --git a/smp-angular/src/app/system-settings/admin-users/user-settings-panel/user-profile-panel.component.html b/smp-angular/src/app/system-settings/admin-users/user-settings-panel/user-profile-panel.component.html new file mode 100644 index 000000000..4450cc236 --- /dev/null +++ b/smp-angular/src/app/system-settings/admin-users/user-settings-panel/user-profile-panel.component.html @@ -0,0 +1,184 @@ +<div id="user-profile-panel"> + <form [formGroup]="userForm"> + <data-panel title="Account" + [showTitle]="showDataPanelTitles" + text="Account data"> + <div class="panel" *ngIf="isNewUser"><p style="font-weight: bold">Enter user and click 'Save' to create new user + </div> + <mat-form-field class="user-profile-pane-field"> + <mat-label>Username</mat-label> + <input id="username_id" matInput placeholder="Username" formControlName="username" #username + maxlength="255" required + auto-focus-directive> + </mat-form-field> + <div style="display:flex; flex-direction: row;align-items: center"> + <mat-form-field style="width:100%" class="user-profile-pane-field"> + <mat-label>Application role</mat-label> + <mat-select placeholder="Application role for the user" + formControlName="role" + matTooltip="application role for the user." + id="role_id" required> + <mat-option *ngFor="let role of applicationRoles" + [value]="role.value"> + {{role.key}} + </mat-option> + </mat-select> + <mat-hint> Chose member role</mat-hint> + </mat-form-field> + + <mat-checkbox + id="active_id" + formControlName="active">Active</mat-checkbox> + </div> + + </data-panel> + + <data-panel title="User profile" + [showTitle]="showDataPanelTitles" + text="User profile data and settings"> + <mat-form-field class="user-profile-pane-field"> + <mat-label>E-Mail Address</mat-label> + <input id="emailAddress_id" matInput placeholder="EMail Address" formControlName="emailAddress" + maxlength="127"> + <div + *ngIf="userForm.controls['emailAddress'].hasError('pattern') && userForm.controls['emailAddress'].touched" + class="has-error">Email is invalid! + </div> + </mat-form-field> + <mat-form-field class="user-profile-pane-field"> + <mat-label>Full name (name and last name)</mat-label> + <input id="fullName_id" matInput placeholder="Full name" formControlName="fullName" + maxlength="127"> + </mat-form-field> + + <mat-form-field class="user-profile-pane-field"> + <mat-label>Theme</mat-label> + <select id="smpTheme_id" matNativeControl + (change)="onThemeSelect($event.target.value)" + formControlName="smpTheme"> + <option *ngFor="let item of themeItems" [value]="item.className">{{item.name}}</option> + </select> + </mat-form-field> + <div class="user-profile-pane-field" style="display:flex;flex-direction: row"> + <mat-form-field style="flex-grow: 1"> + <mat-label>Locale (Date/time formatting)</mat-label> + <select id="smpLocale_id" matNativeControl id="moment-locale" + (change)="onLocaleSelect($event.target.value)" + formControlName="smpLocale" + > + <option value="bg">Bulgarian</option> + <option value="cs">Czech</option> + <option value="da">Danish</option> + <option value="de">German</option> + <option value="el">Greek</option> + <option value="en">English</option> + <option value="es">Spanish</option> + <option value="et">Estonian</option> + <option value="fi">Finnish</option> + <option value="fr">French</option> + <option value="hr">Croatian</option> + <option value="hu">Hungarian</option> + <option value="it">Italian</option> + <option value="lt">Lithuanian</option> + <option value="lv">Latvian</option> + <option value="mt">Maltese</option> + <option value="nl">Dutch</option> + <option value="pl">Polish</option> + <option value="pt">Portuguese</option> + <option value="ro">Romanian</option> + <option value="sk">Slovak</option> + <option value="sl">Slovenian</option> + <option value="sv">Swedish</option> + </select> + </mat-form-field> + <!-- show example only for logged-in user--> + <mat-form-field style="flex-grow: 1" *ngIf="isUserDataLoggedInUserData"> + <mat-label>Example of Date/time</mat-label> + <input id="exampleDate_id" matInput [ngxMatDatetimePicker]="picker" placeholder="Choose a date" + [value]="currentDate" + readonly> + <mat-datepicker-toggle matSuffix [for]="picker" style="visibility: hidden"></mat-datepicker-toggle> + <ngx-mat-datetime-picker #picker [showSpinners]="true" [showSeconds]="false" [stepHour]="1" + [stepMinute]="1" [stepSecond]="1" + [hideTime]="false" + > + + </ngx-mat-datetime-picker> + </mat-form-field> + </div> + <!-- buttons --> + <div id="user-profile-panel-toolbar" class="panel"> + <button id="cancelButton_id" mat-raised-button (click)="onResetButtonClicked()" color="primary" + [disabled]="!resetButtonEnabled"> + <mat-icon>refresh</mat-icon> + <span>Reset</span> + </button> + <button id="saveButton_id" mat-raised-button (click)="onSaveButtonClicked()" color="primary" + [disabled]="!submitButtonEnabled"> + <mat-icon>save</mat-icon> + <span>Save</span> + </button> + </div> + </data-panel> + </form> + <form [formGroup]="userCredentialForm"> + <data-panel *ngIf="true" title="Username/password credentials" + [showTitle]="showDataPanelTitles" + text="Reset username password for the UI login"> + <div style="display: flex;flex-flow: row wrap;"> + <mat-form-field style="flex-grow: 1"> + <mat-label>Last set</mat-label> + <input id="passwordUpdatedOn_id" matInput placeholder="Last set" + value="{{!userCredentialForm.get('passwordUpdatedOn').value?nullValue:userCredentialForm.get('passwordUpdatedOn').value | date:dateTimeFormat}}" + maxlength="255" disabled> + </mat-form-field> + + <mat-form-field style="flex-grow: 1"> + <mat-label>Password expire on</mat-label> + <input id="passwordExpireOn_id" + *ngIf="!!userCredentialForm.get('passwordExpireOn').value; else noPasswordExpirySet " matInput + placeholder="Valid until" + value="{{userCredentialForm.get('passwordExpireOn').value | date:dateTimeFormat}}" + maxlength="255" disabled> + <ng-template #noPasswordExpirySet> + <input id="passwordExpireOnMessage_id" matInput placeholder="Valid until" style="color: red" + matTooltip="Default password set by system admin! User must change password immediately!" + value="Default or null password" + maxlength="255" disabled> + </ng-template> + </mat-form-field> + </div> + <div style="display: flex;flex-flow: row wrap;"> + <mat-form-field style="flex-grow: 2"> + <mat-label>Seq. failed attempts</mat-label> + <input id="sequentialLoginFailureCount_id" matInput placeholder="Seq. failed attempts" + [value]="userCredentialForm.controls['sequentialLoginFailureCount'].value" + maxlength="255" disabled readonly> + </mat-form-field> + <mat-form-field style="flex-grow: 1"> + <mat-label>Last failed attempt</mat-label> + <input id="lastFailedLoginAttempt_id" matInput placeholder="Last failed attempt" + value="{{!userCredentialForm.get('lastFailedLoginAttempt').value?nullValue:userCredentialForm.get('lastFailedLoginAttempt').value | date:dateTimeFormat}}" + maxlength="255" disabled> + </mat-form-field> + </div> + <div style="display: flex;flex-flow: row wrap;"> + <mat-form-field style="flex-grow: 1"> + <mat-label>Suspended until</mat-label> + <input id="suspendedUtil_id" matInput placeholder="Suspended until" + value="{{!userCredentialForm.get('suspendedUtil').value?nullValue:userCredentialForm.get('suspendedUtil').value | date:dateTimeFormat}}" + maxlength="255" disabled> + </mat-form-field> + </div> + + + <div id="user-password-reset-panel-toolbar" class="panel"> + <button mat-flat-button color="primary" id="changePassword_id" + (click)="changeCurrentUserPassword()"> + <span>Set/change password</span> + </button> + </div> + </data-panel> + </form> +</div> + diff --git a/smp-angular/src/app/system-settings/admin-users/user-settings-panel/user-profile-panel.component.scss b/smp-angular/src/app/system-settings/admin-users/user-settings-panel/user-profile-panel.component.scss new file mode 100644 index 000000000..453a8c2bd --- /dev/null +++ b/smp-angular/src/app/system-settings/admin-users/user-settings-panel/user-profile-panel.component.scss @@ -0,0 +1,42 @@ + +.smp-container-limited { + display: flex; + flex-flow: row; + gap: 10px; + align-items: center; + + max-width: 1204px; + width: 1204px; + min-width: 120px; +} + +.smp-column-data { + min-height: 100px; + flex: 1 1 60%; + display: flex; + flex-direction: column; + align-self: stretch; +} + +.smp-data-panel-field { + width: 100% !important; +} + +.smp-column-label { + padding-top: 2em; + min-height: 100px; + max-width: 40%; + flex: 1 1 40%; + display: flex; + flex-direction: column; + align-self: stretch; + + +} +.smp-column-label p { + border-bottom: 1px solid black; + margin: 0em 2em 2em 1em; + font-weight: bold; + +} + diff --git a/smp-angular/src/app/system-settings/admin-users/user-settings-panel/user-profile-panel.component.ts b/smp-angular/src/app/system-settings/admin-users/user-settings-panel/user-profile-panel.component.ts new file mode 100644 index 000000000..333ed1aac --- /dev/null +++ b/smp-angular/src/app/system-settings/admin-users/user-settings-panel/user-profile-panel.component.ts @@ -0,0 +1,231 @@ +import {Component, ElementRef, EventEmitter, Input, Output, ViewChild,} from '@angular/core'; +import {SmpConstants} from "../../../smp.constants"; +import {FormBuilder, FormControl, FormGroup, Validators} from "@angular/forms"; +import {CredentialRo} from "../../../security/credential.model"; +import {UserController} from "../../user/user-controller"; +import {SecurityService} from "../../../security/security.service"; +import {ThemeService} from "../../../common/theme-service/theme.service"; +import {AlertMessageService} from "../../../common/alert-message/alert-message.service"; +import {UserService} from "../../user/user.service"; +import {MatDialog} from "@angular/material/dialog"; +import {HttpClient} from "@angular/common/http"; +import {GlobalLookups} from "../../../common/global-lookups"; +import {DateAdapter} from "@angular/material/core"; +import {NgxMatDateAdapter} from "@angular-material-components/datetime-picker"; +import {UserRo} from "../../user/user-ro.model"; +import {ApplicationRoleEnum} from "../../../common/enums/application-role.enum"; + + +@Component({ + selector: 'user-profile-panel', + templateUrl: './user-profile-panel.component.html', + styleUrls: ['./user-profile-panel.component.scss'] +}) +export class UserProfilePanelComponent { + + @Output() onSaveUserEvent: EventEmitter<UserRo> = new EventEmitter(); + @Output() onDiscardNew: EventEmitter<any> = new EventEmitter(); + @Output() onChangeUserPasswordEvent: EventEmitter<UserRo> = new EventEmitter(); + + + readonly emailPattern = '[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\\.[a-zA-Z]{2,}'; + readonly dateFormat: string = 'yyyy-MM-dd HH:mm:ssZ'; + readonly dateTimeFormat: string = SmpConstants.DATE_TIME_FORMAT; + readonly nullValue: string = SmpConstants.NULL_VALUE; + + readonly applicationRoles = Object.keys(ApplicationRoleEnum).map(el => { + return {key: el, value: ApplicationRoleEnum[el]} + }); + + + userForm: FormGroup; + userCredentialForm: FormGroup; + _managedUserData: UserRo; + + currentDate: Date = new Date(); + + currentPwdCredential: CredentialRo; + userController: UserController; + + @Input() showDataPanelTitles: boolean = true + + @ViewChild('username', {static: false}) usernameField: ElementRef; + + + constructor( + private securityService: SecurityService, + private themeService: ThemeService, + private alertService: AlertMessageService, + private formBuilder: FormBuilder, + private userService: UserService, + private dialog: MatDialog, + private http: HttpClient, + private lookups: GlobalLookups, + private dateAdapter: DateAdapter<Date>, + private ngxMatDateAdapter: NgxMatDateAdapter<Date>) { + + this.userController = new UserController(this.http, this.lookups, this.dialog); + + // set empty form ! do not bind it to current object ! + this.userForm = formBuilder.group({ + // common values + 'username': new FormControl({value: '', disabled: true}), + 'role': new FormControl({value: '', disabled: true}), + 'active': new FormControl({value: '', disabled: true}), + 'emailAddress': new FormControl({value: '', disabled: false}, [Validators.pattern(this.emailPattern), + Validators.maxLength(255)]), + 'fullName': new FormControl({value: '', disabled: false}), + 'smpTheme': new FormControl({value: 'default_theme', disabled: false}), + 'smpLocale': new FormControl({value: 'fr', disabled: false}), + + }); + + this.userCredentialForm = formBuilder.group({ + 'passwordUpdatedOn': new FormControl({value: '', disabled: true}), + 'passwordExpireOn': new FormControl({value: '', disabled: true}), + 'sequentialLoginFailureCount': new FormControl({value: '0', disabled: true}), + 'lastFailedLoginAttempt': new FormControl({value: '', disabled: true}), + 'suspendedUtil': new FormControl({value: '', disabled: true}), + }); + } + + + get managedUserData(): UserRo { + let userRo = {...this._managedUserData}; + userRo.active = this.userForm.get('active').value; + userRo.username = this.userForm.get('username').value; + userRo.role = this.userForm.get('role').value; + userRo.emailAddress = this.userForm.get('emailAddress').value; + userRo.fullName = this.userForm.get('fullName').value; + userRo.smpTheme = this.userForm.get('smpTheme').value; + userRo.smpLocale = this.userForm.get('smpLocale').value; + return userRo; + } + + @Input() set managedUserData(value: UserRo) { + this._managedUserData = value; + + if (!!this._managedUserData) { + this.userForm.controls['username'].setValue(this._managedUserData.username); + this.userForm.controls['active'].setValue(this._managedUserData.active); + this.userForm.controls['role'].setValue(this._managedUserData.role); + this.userForm.controls['emailAddress'].setValue(this._managedUserData.emailAddress); + this.userForm.controls['fullName'].setValue(this._managedUserData.fullName); + this.userForm.controls['smpTheme'].setValue(!this._managedUserData.smpTheme ? 'default_theme' : this._managedUserData.smpTheme); + this.userForm.controls['smpLocale'].setValue(!this._managedUserData.smpLocale ? 'fr' : this._managedUserData.smpLocale); + // mark form as pristine + this.userForm.enable(); + // disable fields + if (!this.isNewUser) { + this.userForm.controls['username'].disable(); + } else { + this.setFocus(); + } + if (this.isUserDataLoggedInUserData) { + this.userForm.controls['role'].disable(); + this.userForm.controls['active'].disable(); + } + } else { + this.userForm.controls['username'].setValue(""); + this.userForm.controls['role'].setValue(""); + this.userForm.controls['active'].setValue("false"); + this.userForm.controls['emailAddress'].setValue(""); + this.userForm.controls['fullName'].setValue(""); + this.userForm.controls['smpTheme'].setValue('default_theme'); + this.userForm.controls['smpLocale'].setValue('fr'); + this.userForm.disable(); + } + this.userForm.markAsPristine(); + } + + + + private updatePwdCredential(currentPwdCredential: CredentialRo) { + this.currentPwdCredential = { + ...currentPwdCredential + } + this.userCredentialForm.controls['passwordUpdatedOn'].setValue(this.currentPwdCredential.updatedOn); + this.userCredentialForm.controls['passwordExpireOn'].setValue(this.currentPwdCredential.expireOn); + this.userCredentialForm.controls['sequentialLoginFailureCount'].setValue(this.currentPwdCredential.sequentialLoginFailureCount); + this.userCredentialForm.controls['lastFailedLoginAttempt'].setValue(this.currentPwdCredential.lastFailedLoginAttempt); + this.userCredentialForm.controls['suspendedUtil'].setValue(this.currentPwdCredential.suspendedUtil); + // mark form as pristine + this.userCredentialForm.markAsPristine(); + } + + + onSaveButtonClicked() { + this.onSaveUserEvent.emit(this.managedUserData); + } + + onResetButtonClicked() { + if (this.isNewUser) { + this.onDiscardNew.emit(); + } + this.userForm.reset(this._managedUserData); + if (this.isUserDataLoggedInUserData) { + this.themeService.persistTheme(this._managedUserData.smpTheme); + this.dateAdapter.setLocale(this._managedUserData.smpLocale); + this.ngxMatDateAdapter.setLocale(this._managedUserData.smpLocale); + } + } + + changeCurrentUserPassword() { + this.onChangeUserPasswordEvent.emit(this._managedUserData) + } + + + get submitButtonEnabled(): boolean { + return this.userForm.valid && this.userForm.dirty; + } + + get resetButtonEnabled(): boolean { + return this.userForm.dirty; + } + + get safeRefresh(): boolean { + return true; + } + + + onThemeSelect(target: string) { + // save theme only for logged in user + if (this.isUserDataLoggedInUserData) { + this.themeService.persistTheme(target); + } + + } + + get themeItems() { + return this.themeService.themes; + } + + onLocaleSelect(target: string) { + // save locale only for logged-in user + if (this.isUserDataLoggedInUserData) { + this.dateAdapter.setLocale(target); + this.ngxMatDateAdapter.setLocale(target); + } + } + + isDirty(): boolean { + return this.userForm.dirty; + } + + get isNewUser(): boolean { + return !this._managedUserData?.userId; + } + + get canChangeRole ():boolean { + return !this.isUserDataLoggedInUserData + } + + get isUserDataLoggedInUserData(){ + return this.securityService.getCurrentUser()?.userId == this._managedUserData?.userId + } + + public setFocus() { + setTimeout(() => this.usernameField.nativeElement.focus()); + } + +} diff --git a/smp-angular/src/app/system-settings/user/user-controller.ts b/smp-angular/src/app/system-settings/user/user-controller.ts index 00b6456fc..ebf6103e1 100644 --- a/smp-angular/src/app/system-settings/user/user-controller.ts +++ b/smp-angular/src/app/system-settings/user/user-controller.ts @@ -11,6 +11,7 @@ import {HttpClient} from "@angular/common/http"; import {CertificateRo} from "./certificate-ro.model"; import {PasswordChangeDialogComponent} from "../../common/dialogs/password-change-dialog/password-change-dialog.component"; import {AccessTokenGenerationDialogComponent} from "../../common/dialogs/access-token-generation-dialog/access-token-generation-dialog.component"; +import {ApplicationRoleEnum} from "../../common/enums/application-role.enum"; export class UserController implements SearchTableController { @@ -70,10 +71,10 @@ export class UserController implements SearchTableController { index: null, username: '', emailAddress: '', - role: '', + role: ApplicationRoleEnum.USER, active: true, status: EntityStatus.NEW, - statusPassword: EntityStatus.NEW + } } diff --git a/smp-angular/src/app/system-settings/user/user-details-dialog/user-details-dialog.component.ts b/smp-angular/src/app/system-settings/user/user-details-dialog/user-details-dialog.component.ts index c36ca3258..20fa12941 100644 --- a/smp-angular/src/app/system-settings/user/user-details-dialog/user-details-dialog.component.ts +++ b/smp-angular/src/app/system-settings/user/user-details-dialog/user-details-dialog.component.ts @@ -24,6 +24,7 @@ import {UserController} from "../user-controller"; import {HttpClient} from "@angular/common/http"; import {CertificateDialogComponent} from "../../../common/dialogs/certificate-dialog/certificate-dialog.component"; import {SmpConstants} from "../../../smp.constants"; +import {ApplicationRoleEnum} from "../../../common/enums/application-role.enum"; @Component({ selector: 'user-details-dialog', @@ -124,7 +125,7 @@ export class UserDetailsDialogComponent { sequentialTokenLoginFailureCount: null, lastTokenFailedLoginAttempt: null, tokenSuspendedUtil: null, - role: '', + role: ApplicationRoleEnum.USER, encodedValue: '', crlUrl: '', status: EntityStatus.NEW, @@ -388,10 +389,10 @@ export class UserDetailsDialogComponent { index: null, username: '', emailAddress: '', - role: '', + role: ApplicationRoleEnum.USER, active: true, status: EntityStatus.NEW, - statusPassword: EntityStatus.NEW + } } diff --git a/smp-angular/src/app/system-settings/user/user-ro.model.ts b/smp-angular/src/app/system-settings/user/user-ro.model.ts index fbcfe70c9..81dbbce93 100644 --- a/smp-angular/src/app/system-settings/user/user-ro.model.ts +++ b/smp-angular/src/app/system-settings/user/user-ro.model.ts @@ -1,19 +1,25 @@ import {SearchTableEntity} from '../../common/search-table/search-table-entity.model'; import {CertificateRo} from './certificate-ro.model'; +import {ApplicationRoleEnum} from "../../common/enums/application-role.enum"; export interface UserRo extends SearchTableEntity { userId?: string username: string; fullName?: string; - emailAddress: string; + emailAddress?: string; + smpTheme?: string; + smpLocale?: string; + role: ApplicationRoleEnum; + active: boolean; + + + accessTokenId?: string; passwordExpireOn?: Date; accessTokenExpireOn?: Date; - role: string; - active: boolean; + suspended?: boolean; certificate?: CertificateRo; - statusPassword: number; casUserDataUrl?: string; sequentialLoginFailureCount?:number; lastFailedLoginAttempt?:Date; diff --git a/smp-angular/src/app/user-settings/user-profile/user-profile.component.html b/smp-angular/src/app/user-settings/user-profile/user-profile.component.html index 7b00ffa2d..e8986fd80 100644 --- a/smp-angular/src/app/user-settings/user-profile/user-profile.component.html +++ b/smp-angular/src/app/user-settings/user-profile/user-profile.component.html @@ -1,163 +1,7 @@ -<div id="user-profile-panel"> - <form [formGroup]="userForm" > - <data-panel title="Account" - text="Account data"> - <mat-form-field class="user-profile-pane-field"> - <mat-label>Username</mat-label> - <input matInput placeholder="Username" formControlName="username" - id="username_id" maxlength="255"> - </mat-form-field> - <mat-form-field class="user-profile-pane-field"> - <mat-label>Role</mat-label> - <input matInput placeholder="role" formControlName="role" - id="Role" maxlength="255"> - </mat-form-field> - </data-panel> - - <data-panel title="User profile" - text="User profile data and settings"> - <mat-form-field class="user-profile-pane-field"> - <mat-label>E-Mail Address</mat-label> - <input matInput placeholder="EMail Address" formControlName="emailAddress" - id="emailAddress_id" maxlength="255"> - <div - *ngIf="userForm.controls['emailAddress'].hasError('pattern') && userForm.controls['emailAddress'].touched" - class="has-error">Email is invalid! - </div> - </mat-form-field> - <mat-form-field class="user-profile-pane-field"> - <mat-label>Full name (name and last name)</mat-label> - <input matInput placeholder="Full name" formControlName="fullName" - id="fullName" maxlength="255"> - </mat-form-field> - - <mat-form-field class="user-profile-pane-field"> - <mat-label>Theme</mat-label> - <select matNativeControl - (change)="onThemeSelect($event.target.value)" - formControlName="smpTheme"> - <option *ngFor="let item of themeItems" [value]="item.className">{{item.name}}</option> - </select> - </mat-form-field> - <div class="user-profile-pane-field" style="display:flex;flex-direction: row"> - <mat-form-field style="flex-grow: 1"> - <mat-label>Locale (Date/time formatting)</mat-label> - <select matNativeControl id="moment-locale" - (change)="onLocaleSelect($event.target.value)" - formControlName="smpLocale" - > - <option value="bg">Bulgarian</option> - <option value="cs">Czech</option> - <option value="da">Danish</option> - <option value="de">German</option> - <option value="el">Greek</option> - <option value="en">English</option> - <option value="es">Spanish</option> - <option value="et">Estonian</option> - <option value="fi">Finnish</option> - <option value="fr">French</option> - <option value="hr">Croatian</option> - <option value="hu">Hungarian</option> - <option value="it">Italian</option> - <option value="lt">Lithuanian</option> - <option value="lv">Latvian</option> - <option value="mt">Maltese</option> - <option value="nl">Dutch</option> - <option value="pl">Polish</option> - <option value="pt">Portuguese</option> - <option value="ro">Romanian</option> - <option value="sk">Slovak</option> - <option value="sl">Slovenian</option> - <option value="sv">Swedish</option> - </select> - - - </mat-form-field> - <mat-form-field style="flex-grow: 1"> - <mat-label>Example of Date/time</mat-label> - <input matInput [ngxMatDatetimePicker]="picker" placeholder="Choose a date" - [value]="currentDate" - > - <mat-datepicker-toggle matSuffix [for]="picker" style="visibility: visible"></mat-datepicker-toggle> - <ngx-mat-datetime-picker #picker [showSpinners]="true" [showSeconds]="false" [stepHour]="1" - [stepMinute]="1" [stepSecond]="1" - [hideTime]="false" - > - - </ngx-mat-datetime-picker> - </mat-form-field> - </div> - <!-- buttons --> - <div id="user-profile-panel-toolbar" class="panel"> - <button id="cancelButton" mat-raised-button (click)="onResetButtonClicked()" color="primary" - [disabled]="!resetButtonEnabled"> - <mat-icon>refresh</mat-icon> - <span>Reset</span> - </button> - <button id="saveButton" mat-raised-button (click)="onSaveButtonClicked()" color="primary" - [disabled]="!submitButtonEnabled"> - <mat-icon>save</mat-icon> - <span>Save</span> - </button> - </div> - </data-panel> - </form> - <form [formGroup]="userCredentialForm"> - <data-panel *ngIf="true" title="Username/password credentials" - text="Reset username password for the UI login"> - <div style="display: flex;flex-flow: row wrap;"> - <mat-form-field style="flex-grow: 1"> - <mat-label>Last set</mat-label> - <input matInput placeholder="Last set" - value="{{!userCredentialForm.get('passwordUpdatedOn').value?nullValue:userCredentialForm.get('passwordUpdatedOn').value | date:dateTimeFormat}}" - maxlength="255" disabled> - </mat-form-field> - - <mat-form-field style="flex-grow: 1"> - <mat-label>Password expire on</mat-label> - <input *ngIf="!!userCredentialForm.get('passwordExpireOn').value; else noPasswordExpirySet " matInput - placeholder="Valid until" - value="{{userCredentialForm.get('passwordExpireOn').value | date:dateTimeFormat}}" - maxlength="255" disabled> - <ng-template #noPasswordExpirySet> - <input matInput placeholder="Valid until" style="color: red" - matTooltip="Default password set by system admin! User must change password immediately!" - value="Default or null password" - maxlength="255" disabled> - </ng-template> - </mat-form-field> - </div> - <div style="display: flex;flex-flow: row wrap;"> - <mat-form-field style="flex-grow: 2"> - <mat-label>Seq. failed attempts</mat-label> - <input matInput placeholder="Seq. failed attempts" - [value]="userCredentialForm.controls['sequentialLoginFailureCount'].value" - id="sequentialLoginFailureCount_id" maxlength="255" disabled readonly> - </mat-form-field> - <mat-form-field style="flex-grow: 1"> - <mat-label>Last failed attempt</mat-label> - <input matInput placeholder="Last failed attempt" - value="{{!userCredentialForm.get('lastFailedLoginAttempt').value?nullValue:userCredentialForm.get('lastFailedLoginAttempt').value | date:dateTimeFormat}}" - maxlength="255" disabled> - </mat-form-field> - </div> - <div style="display: flex;flex-flow: row wrap;"> - <mat-form-field style="flex-grow: 1"> - <mat-label>Suspended until</mat-label> - <input matInput placeholder="Suspended until" - value="{{!userCredentialForm.get('suspendedUtil').value?nullValue:userCredentialForm.get('suspendedUtil').value | date:dateTimeFormat}}" - maxlength="255" disabled> - </mat-form-field> - </div> - - - <div id="user-password-reset-panel-toolbar" class="panel"> - <button mat-flat-button color="primary" id="changePassword_id" - (click)="changeCurrentUserPassword()"> - <span>Set/change password</span> - </button> - </div> - </data-panel> - </form> -</div> +<user-profile-panel id="user-profile-component-panel" + #userProfilePanel + (onSaveUserEvent)="onSaveUserEvent($event)" + (onChangeUserPasswordEvent)="changeUserPasswordEvent($event)" + [managedUserData]="currentUserData"> +</user-profile-panel> diff --git a/smp-angular/src/app/user-settings/user-profile/user-profile.component.scss b/smp-angular/src/app/user-settings/user-profile/user-profile.component.scss index 88930e9e4..55184ddc7 100644 --- a/smp-angular/src/app/user-settings/user-profile/user-profile.component.scss +++ b/smp-angular/src/app/user-settings/user-profile/user-profile.component.scss @@ -1,4 +1,4 @@ -#user-profile-panel { +#user-profile-component-panel { display: flex; flex-flow: column; align-items: center; diff --git a/smp-angular/src/app/user-settings/user-profile/user-profile.component.ts b/smp-angular/src/app/user-settings/user-profile/user-profile.component.ts index a5b3fc42d..9bcf54826 100644 --- a/smp-angular/src/app/user-settings/user-profile/user-profile.component.ts +++ b/smp-angular/src/app/user-settings/user-profile/user-profile.component.ts @@ -1,184 +1,104 @@ -import {Component, Input,} from '@angular/core'; +import {Component, ViewChild,} from '@angular/core'; import {SecurityService} from "../../security/security.service"; -import {AlertMessageService} from "../../common/alert-message/alert-message.service"; -import {FormBuilder, FormControl, FormGroup, Validators,} from "@angular/forms"; -import {ThemeService} from "../../common/theme-service/theme.service"; import {User} from "../../security/user.model"; import {UserService} from "../../system-settings/user/user.service"; -import {SmpConstants} from "../../smp.constants"; -import {MatDialog, MatDialogRef} from "@angular/material/dialog"; -import {UserController} from "../../system-settings/user/user-controller"; -import {HttpClient} from "@angular/common/http"; -import {GlobalLookups} from "../../common/global-lookups"; -import {CredentialRo} from "../../security/credential.model"; -import {DateAdapter} from "@angular/material/core"; -import {NgxMatDateAdapter} from "@angular-material-components/datetime-picker"; import {BeforeLeaveGuard} from "../../window/sidenav/navigation-on-leave-guard"; +import {UserRo} from "../../system-settings/user/user-ro.model"; +import { + UserProfilePanelComponent +} from "../../system-settings/admin-users/user-settings-panel/user-profile-panel.component"; +import {MatDialog, MatDialogConfig, MatDialogRef} from "@angular/material/dialog"; +import { + PasswordChangeDialogComponent +} from "../../common/dialogs/password-change-dialog/password-change-dialog.component"; +import {UserDetailsDialogMode} from "../../system-settings/user/user-details-dialog/user-details-dialog.component"; @Component({ templateUrl: './user-profile.component.html', styleUrls: ['./user-profile.component.scss'] }) -export class UserProfileComponent implements BeforeLeaveGuard{ +export class UserProfileComponent implements BeforeLeaveGuard { - readonly emailPattern = '[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\\.[a-zA-Z]{2,}'; - readonly dateFormat: string = 'yyyy-MM-dd HH:mm:ssZ'; - readonly dateTimeFormat: string = SmpConstants.DATE_TIME_FORMAT; - readonly nullValue: string = SmpConstants.NULL_VALUE; - userForm: FormGroup; - userCredentialForm: FormGroup; - currentUserData: User; - currentDate: Date = new Date(); - - currentPwdCredential: CredentialRo; - userController: UserController; - @Input() showActionButtons: boolean = true; + @ViewChild('userProfilePanel') userProfilePanel: UserProfilePanelComponent; + currentUserData: UserRo; + loggedInUser: User; constructor( - private securityService: SecurityService, - private themeService: ThemeService, - private alertService: AlertMessageService, - private formBuilder: FormBuilder, private userService: UserService, - private dialog: MatDialog, - private http: HttpClient, - private lookups: GlobalLookups, - private dateAdapter: DateAdapter<Date>, - private ngxMatDateAdapter: NgxMatDateAdapter<Date>) { - - this.userController = new UserController(this.http, this.lookups, this.dialog); - - // set empty form ! do not bind it to current object ! - this.userForm = formBuilder.group({ - // common values - 'username': new FormControl({value: '', disabled: true}), - 'role': new FormControl({value: '', disabled: true}), - 'emailAddress': new FormControl({value: '', disabled: false}, [Validators.pattern(this.emailPattern), - Validators.maxLength(255)]), - 'fullName': new FormControl({value: '', disabled: false}), - 'smpTheme': new FormControl({value: 'default_theme', disabled: false}), - 'smpLocale': new FormControl({value: 'fr', disabled: false}), + private securityService: SecurityService, + public dialog: MatDialog) { - }); - this.userCredentialForm = formBuilder.group({ - 'passwordUpdatedOn': new FormControl({value: '', disabled: true}), - 'passwordExpireOn': new FormControl({value: '', disabled: true}), - 'sequentialLoginFailureCount': new FormControl({value: '0', disabled: true}), - 'lastFailedLoginAttempt': new FormControl({value: '', disabled: true}), - 'suspendedUtil': new FormControl({value: '', disabled: true}), - }); userService.onProfileDataChangedEvent().subscribe(updatedUser => { this.updateUserData(updatedUser); } ); - userService.onPwdCredentialsUpdateEvent().subscribe(pwdCredential => { - this.updatePwdCredential(pwdCredential); - } - ); - - userService.getUserPwdCredentialStatus(); - - this.updateUserData(securityService.getCurrentUser()) - } + this.updateUserData(this.securityService.getCurrentUser()) - private updateUserData(currentUser: User) { - this.currentUserData = { - ...currentUser - } - - this.userForm.controls['username'].setValue(this.currentUserData.username); - this.userForm.controls['role'].setValue(this.currentUserData.role); - this.userForm.controls['emailAddress'].setValue(this.currentUserData.emailAddress); - this.userForm.controls['fullName'].setValue(this.currentUserData.fullName); - this.userForm.controls['smpTheme'].setValue(!this.currentUserData.smpTheme ? 'default_theme' : this.currentUserData.smpTheme); - this.userForm.controls['smpLocale'].setValue(!this.currentUserData.smpLocale ? 'fr' : this.currentUserData.smpLocale); - - // set current user theme as persisted for the application - this.themeService.persistTheme(this.currentUserData.smpTheme); - // mark form as pristine - this.userForm.markAsPristine(); } - private updatePwdCredential(currentPwdCredential: CredentialRo) { - this.currentPwdCredential = { - ...currentPwdCredential - } - this.userCredentialForm.controls['passwordUpdatedOn'].setValue(this.currentPwdCredential.updatedOn); - this.userCredentialForm.controls['passwordExpireOn'].setValue(this.currentPwdCredential.expireOn); - this.userCredentialForm.controls['sequentialLoginFailureCount'].setValue(this.currentPwdCredential.sequentialLoginFailureCount); - this.userCredentialForm.controls['lastFailedLoginAttempt'].setValue(this.currentPwdCredential.lastFailedLoginAttempt); - this.userCredentialForm.controls['suspendedUtil'].setValue(this.currentPwdCredential.suspendedUtil); - // mark form as pristine - this.userCredentialForm.markAsPristine(); + private updateUserData(user: User) { + this.currentUserData = this.convert(user); + this.loggedInUser = user; } - - onSaveButtonClicked() { - let userData = {...this.currentUserData}; - userData.emailAddress = this.userForm.get('emailAddress').value; - userData.fullName = this.userForm.get('fullName').value; - userData.smpTheme = this.userForm.get('smpTheme').value; - userData.smpLocale = this.userForm.get('smpLocale').value; + onSaveUserEvent(user: UserRo) { + let userData: User = {...this.loggedInUser}; + // change only allowed data + userData.emailAddress = user.emailAddress; + userData.fullName = user.fullName; + userData.smpTheme = user.smpTheme; + userData.smpLocale = user.smpLocale; this.userService.updateUser(userData); + } - onResetButtonClicked() { - this.userForm.reset(this.currentUserData); + isDirty(): boolean { + return this.userProfilePanel.isDirty() } - changeCurrentUserPassword() { - const formRef: MatDialogRef<any> = this.userController.changePasswordDialog({ + changeUserPasswordEvent(user: UserRo) { + const formRef: MatDialogRef<any> = this.changePasswordDialog({ data: { - user: this.currentUserData, + user: user, adminUser: false }, }); formRef.afterClosed().subscribe(result => { if (result) { this.currentUserData.passwordExpireOn = result.passwordExpireOn; - this.userForm.controls['passwordExpireOn'].setValue(this.currentUserData.passwordExpireOn); + this.currentUserData = {...this.currentUserData} } }); - } - - get submitButtonEnabled(): boolean { - return this.userForm.valid && this.userForm.dirty; - } - - get resetButtonEnabled(): boolean { - return this.userForm.dirty; - } - - get safeRefresh(): boolean { - return true; - } - - - onThemeSelect(target: string) { - this.themeService.persistTheme(target); + public changePasswordDialog(config?: MatDialogConfig): MatDialogRef<PasswordChangeDialogComponent> { + return this.dialog.open(PasswordChangeDialogComponent, this.convertConfig(config)); } - get themeItems() { - return this.themeService.themes; - } - onLocaleSelect(target: string) { - console.log("set locale" + target) - this.dateAdapter.setLocale(target); - this.ngxMatDateAdapter.setLocale(target); + private convertConfig(config) { + return (config && config.data) + ? { + ...config, + data: { + ...config.data, + mode: config.data.mode || (config.data.edit ? UserDetailsDialogMode.EDIT_MODE : UserDetailsDialogMode.NEW_MODE) + } + } + : config; } - isDirty(): boolean { - return this.userForm.dirty; + private convert(user: User): UserRo { + return { + ...user, + active: true, + status: undefined, + statusPassword: 0 + } as UserRo; } - - - } diff --git a/smp-angular/src/app/window/toolbar/toolbar.component.html b/smp-angular/src/app/window/toolbar/toolbar.component.html index fc2701f0c..e21a663c4 100644 --- a/smp-angular/src/app/window/toolbar/toolbar.component.html +++ b/smp-angular/src/app/window/toolbar/toolbar.component.html @@ -10,7 +10,7 @@ </div--> </div> <span class="window-toolbar-spacer"></span> - <a class="window-toolbar-item" *ngIf="!currentUser" [routerLink]="['/login']" (click)="clearWarning()"> Login </a> + <a id="login_id" class="window-toolbar-item" *ngIf="!currentUser" [routerLink]="['/login']" (click)="clearWarning()"> Login </a> <span class="window-toolbar-item" *ngIf="currentUser">{{currentUserRoleDescription}}: {{currentUser}} </span> <button class="window-toolbar-item" [mat-menu-trigger-for]="settingsMenu" id="settingsmenu_id" matTooltip="Menu" > @@ -19,24 +19,24 @@ <mat-menu x-position="before" #settingsMenu="matMenu"> <div *ngIf="currentUser"> - <button mat-menu-item id="currentuser_id" (click)="editCurrentUser()"> + <button id="currentuser_id" mat-menu-item (click)="editCurrentUser()"> <mat-icon>person</mat-icon> <span>{{currentUser}}</span> </button> - <button *ngIf="isUserAuthPasswdEnabled" mat-menu-item id="changePassword_id" + <button id="changePassword_id" *ngIf="isUserAuthPasswdEnabled" mat-menu-item (click)="changeCurrentUserPassword()"> <span>Change password</span> </button> - <button *ngIf="isUserAuthSSOEnabled" mat-menu-item id="showSSODetails_id" + <button id="showSSODetails_id" *ngIf="isUserAuthSSOEnabled" mat-menu-item (click)="openCurrentCasUserData()"> <span>Open CAS user data</span> </button> - <button *ngIf="isWebServiceUserTokenAuthPasswdEnabled" mat-menu-item id="getAccessToken_id" + <button id="getAccessToken_id" *ngIf="isWebServiceUserTokenAuthPasswdEnabled" mat-menu-item (click)="regenerateCurrentUserAccessToken()"> <span>Generated access token</span> </button> <hr/> - <button mat-menu-item (click)="logout($event)" id="logout_id"> + <button id="logout_id" mat-menu-item (click)="logout($event)"> <mat-icon>power_settings_new</mat-icon> <span>Logout</span> </button> diff --git a/smp-server-library/src/main/java/eu/europa/ec/edelivery/smp/data/dao/DomainDao.java b/smp-server-library/src/main/java/eu/europa/ec/edelivery/smp/data/dao/DomainDao.java index 36c852157..e7aa71ec4 100644 --- a/smp-server-library/src/main/java/eu/europa/ec/edelivery/smp/data/dao/DomainDao.java +++ b/smp-server-library/src/main/java/eu/europa/ec/edelivery/smp/data/dao/DomainDao.java @@ -24,6 +24,7 @@ import javax.persistence.NoResultException; import javax.persistence.NonUniqueResultException; import javax.persistence.TypedQuery; import javax.transaction.Transactional; +import java.util.Arrays; import java.util.Collections; import java.util.List; import java.util.Optional; @@ -112,23 +113,23 @@ public class DomainDao extends BaseDao<DBDomain> { return query.getSingleResult(); } - public Long getDomainsByUserIdAndRolesCount(Long userId, List<MembershipRoleType> roleTypes) { - if (roleTypes.isEmpty()) { - return 0L; - } + + + public Long getDomainsByUserIdAndRolesCount(Long userId, MembershipRoleType ... roleTypes) { + + List<MembershipRoleType> list = Arrays.asList(roleTypes ==null || roleTypes.length==0 ?MembershipRoleType.values(): roleTypes); TypedQuery<Long> query = memEManager.createNamedQuery(QUERY_DOMAIN_BY_USER_ROLES_COUNT, Long.class); query.setParameter(PARAM_USER_ID, userId); - query.setParameter(PARAM_MEMBERSHIP_ROLES, roleTypes); + query.setParameter(PARAM_MEMBERSHIP_ROLES, list); return query.getSingleResult(); } - public List<DBDomain> getDomainsByUserIdAndRoles(Long userId, List<MembershipRoleType> roleTypes) { - if (roleTypes.isEmpty()) { - return Collections.emptyList(); - } + public List<DBDomain> getDomainsByUserIdAndRoles(Long userId, MembershipRoleType ... roleTypes) { + List<MembershipRoleType> list = Arrays.asList(roleTypes ==null || roleTypes.length==0 ?MembershipRoleType.values(): roleTypes); + TypedQuery<DBDomain> query = memEManager.createNamedQuery(QUERY_DOMAIN_BY_USER_ROLES, DBDomain.class); query.setParameter(PARAM_USER_ID, userId); - query.setParameter(PARAM_MEMBERSHIP_ROLES, roleTypes); + query.setParameter(PARAM_MEMBERSHIP_ROLES, list); return query.getResultList(); } diff --git a/smp-server-library/src/main/java/eu/europa/ec/edelivery/smp/data/dao/DomainMemberDao.java b/smp-server-library/src/main/java/eu/europa/ec/edelivery/smp/data/dao/DomainMemberDao.java index e4740a158..fe4c8183e 100644 --- a/smp-server-library/src/main/java/eu/europa/ec/edelivery/smp/data/dao/DomainMemberDao.java +++ b/smp-server-library/src/main/java/eu/europa/ec/edelivery/smp/data/dao/DomainMemberDao.java @@ -34,6 +34,11 @@ import static eu.europa.ec.edelivery.smp.data.dao.QueryNames.*; @Repository public class DomainMemberDao extends BaseDao<DBDomainMember> { + private final DomainDao domainDao; + + public DomainMemberDao(DomainDao domainDao) { + this.domainDao = domainDao; + } public boolean isUserDomainMember(DBUser user, DBDomain domain) { return isUserDomainsMember(user.getId(), Collections.singletonList(domain.getId())); @@ -60,14 +65,14 @@ public class DomainMemberDao extends BaseDao<DBDomainMember> { return query.getResultList().stream().anyMatch(member -> member.getRole() == roleType); } - public boolean isUserDomainAdministrator(Long userId){ - return false; + public boolean isUserAnyDomainAdministrator(Long userId){ + return domainDao.getDomainsByUserIdAndRolesCount(userId, MembershipRoleType.ADMIN)>0; } public boolean isUserGroupAdministrator(Long userId){ return false; } - public boolean isUserMemberAdministrator(Long userId){ + public boolean isUserResourceAdministrator(Long userId){ return false; } diff --git a/smp-server-library/src/main/java/eu/europa/ec/edelivery/smp/data/model/DBDomain.java b/smp-server-library/src/main/java/eu/europa/ec/edelivery/smp/data/model/DBDomain.java index 49c3c4c7e..0ef298c85 100644 --- a/smp-server-library/src/main/java/eu/europa/ec/edelivery/smp/data/model/DBDomain.java +++ b/smp-server-library/src/main/java/eu/europa/ec/edelivery/smp/data/model/DBDomain.java @@ -42,11 +42,11 @@ import static eu.europa.ec.edelivery.smp.data.dao.QueryNames.*; query = "update SMP_DOMAIN set SIGNATURE_KEY_ALIAS=:alias " + "WHERE SML_CLIENT_KEY_ALIAS IS null") -@NamedQuery(name = QUERY_DOMAIN_BY_USER_ROLES_COUNT, query = "SELECT count(c) FROM DBDomain c JOIN DBDomainMember dm " + - "WHERE c.id = dm.domain.id and dm.role in (:membership_roles) and dm.user.id= :user_id") +@NamedQuery(name = QUERY_DOMAIN_BY_USER_ROLES_COUNT, query = "SELECT count(c) FROM DBDomain c JOIN DBDomainMember dm ON c.id = dm.domain.id " + + " WHERE dm.role in (:membership_roles) and dm.user.id= :user_id") -@NamedQuery(name = QUERY_DOMAIN_BY_USER_ROLES, query = "SELECT c FROM DBDomain c JOIN DBDomainMember dm " + - "WHERE c.id = dm.domain.id and dm.role in (:membership_roles) and dm.user.id= :user_id") +@NamedQuery(name = QUERY_DOMAIN_BY_USER_ROLES, query = "SELECT c FROM DBDomain c JOIN DBDomainMember dm ON c.id = dm.domain.id " + + " WHERE dm.role in (:membership_roles) and dm.user.id= :user_id") @org.hibernate.annotations.Table(appliesTo = "SMP_DOMAIN", comment = "SMP can handle multiple domains. This table contains domain specific data") public class DBDomain extends BaseEntity { diff --git a/smp-server-library/src/main/java/eu/europa/ec/edelivery/smp/data/model/user/DBUser.java b/smp-server-library/src/main/java/eu/europa/ec/edelivery/smp/data/model/user/DBUser.java index f9fa64425..637a7581a 100644 --- a/smp-server-library/src/main/java/eu/europa/ec/edelivery/smp/data/model/user/DBUser.java +++ b/smp-server-library/src/main/java/eu/europa/ec/edelivery/smp/data/model/user/DBUser.java @@ -18,11 +18,14 @@ import eu.europa.ec.edelivery.smp.data.enums.ApplicationRoleType; import eu.europa.ec.edelivery.smp.data.model.BaseEntity; import eu.europa.ec.edelivery.smp.data.model.CommonColumnsLengths; import eu.europa.ec.edelivery.smp.data.model.DBUserDeleteValidation; +import eu.europa.ec.edelivery.smp.data.model.doc.DBResource; import org.apache.commons.lang3.StringUtils; import org.hibernate.annotations.GenericGenerator; import org.hibernate.envers.Audited; import javax.persistence.*; +import java.util.ArrayList; +import java.util.List; import java.util.Objects; import static eu.europa.ec.edelivery.smp.data.dao.QueryNames.*; @@ -151,6 +154,37 @@ public class DBUser extends BaseEntity { @ColumnDescription(comment = "DomiSMP settings: locale for the user") private String smpLocale; + + @OneToMany( + mappedBy = "user", + cascade = CascadeType.ALL, + orphanRemoval = true, + fetch = FetchType.LAZY + ) + private List<DBCredential> userCredentials = new ArrayList<>(); + + @OneToMany( + mappedBy = "user", + cascade = CascadeType.ALL, + orphanRemoval = true, + fetch = FetchType.LAZY + ) + private List<DBDomainMember> domainMembers = new ArrayList<>(); + @OneToMany( + mappedBy = "user", + cascade = CascadeType.ALL, + orphanRemoval = true, + fetch = FetchType.LAZY + ) + private List<DBGroupMember> groupMembers = new ArrayList<>(); + + @OneToMany( + mappedBy = "user", + cascade = CascadeType.ALL, + orphanRemoval = true, + fetch = FetchType.LAZY + ) + private List<DBResourceMember> resourceMembers = new ArrayList<>(); @Override public Long getId() { return id; @@ -216,6 +250,22 @@ public class DBUser extends BaseEntity { this.smpLocale = smpLocale; } + public List<DBCredential> getUserCredentials() { + return userCredentials; + } + + public List<DBDomainMember> getDomainMembers() { + return domainMembers; + } + + public List<DBGroupMember> getGroupMembers() { + return groupMembers; + } + + public List<DBResourceMember> getResourceMembers() { + return resourceMembers; + } + @Override public String toString() { return "DBUser{" + diff --git a/smp-server-library/src/main/java/eu/europa/ec/edelivery/smp/services/ui/UIUserService.java b/smp-server-library/src/main/java/eu/europa/ec/edelivery/smp/services/ui/UIUserService.java index f5861488c..ce65b7b26 100644 --- a/smp-server-library/src/main/java/eu/europa/ec/edelivery/smp/services/ui/UIUserService.java +++ b/smp-server-library/src/main/java/eu/europa/ec/edelivery/smp/services/ui/UIUserService.java @@ -336,6 +336,53 @@ public class UIUserService extends UIServiceBase<DBUser, UserRO> { dbUser.setSmpLocale(user.getSmpLocale()); } + @Transactional + public void adminUpdateUserData(Long userId, UserRO user) { + DBUser dbUser = userDao.find(userId); + if (dbUser == null) { + LOG.error("Can not update user because user for id [{}] does not exist!", userId); + throw new SMPRuntimeException(ErrorCode.INVALID_REQUEST, "UserId", "Can not find user id!"); + } + LOG.debug("Update user [{}]: email [{}], fullname [{}], smp theme [{}]", user.getUsername(), user.getEmailAddress(), user.getFullName(), user.getSmpTheme()); + // update user data by admin + dbUser.setActive(user.isActive()); + dbUser.setApplicationRole(user.getRole()); + dbUser.setEmailAddress(user.getEmailAddress()); + dbUser.setFullName(user.getFullName()); + dbUser.setSmpTheme(user.getSmpTheme()); + dbUser.setSmpLocale(user.getSmpLocale()); + } + + @Transactional + public UserRO adminCreateUserData(UserRO user) { + + Optional<DBUser> testUser = userDao.findUserByUsername(user.getUsername()); + if (testUser.isPresent()) { + throw new SMPRuntimeException(ErrorCode.INVALID_REQUEST, "CreateUser", "User with username ["+user.getUsername()+"] already exists!"); + } + DBUser dbUser = new DBUser(); + // update user data by admin + dbUser.setUsername(user.getUsername()); + dbUser.setApplicationRole(user.getRole()); + dbUser.setEmailAddress(user.getEmailAddress()); + dbUser.setFullName(user.getFullName()); + dbUser.setSmpTheme(user.getSmpTheme()); + dbUser.setSmpLocale(user.getSmpLocale()); + userDao.persistFlushDetach(dbUser); + return conversionService.convert(dbUser, UserRO.class); + } + + @Transactional + public UserRO adminDeleteUserData(Long userId) { + DBUser dbUser = userDao.find(userId); + if (dbUser == null) { + LOG.error("Can not delete user because user for id [{}] does not exist!", userId); + throw new SMPRuntimeException(ErrorCode.INVALID_REQUEST, "UserId", "Can not find user id!"); + } + userDao.remove(dbUser); + return conversionService.convert(dbUser, UserRO.class); + } + protected void createOrUpdateUser(UserRO userRO, OffsetDateTime passwordChange) { /* if (userRO.getStatus() == EntityROStatus.NEW.getStatusNumber()) { @@ -499,7 +546,6 @@ public class UIUserService extends UIServiceBase<DBUser, UserRO> { ServiceResult<SearchUserRO> result = new ServiceResult<>(); result.setPage(page); result.setPageSize(pageSize); - ; if (count < 1) { result.setCount(0L); return result; diff --git a/smp-server-library/src/test/java/eu/europa/ec/edelivery/smp/data/dao/DomainDaoTest.java b/smp-server-library/src/test/java/eu/europa/ec/edelivery/smp/data/dao/DomainDaoTest.java new file mode 100644 index 000000000..8524a9ee6 --- /dev/null +++ b/smp-server-library/src/test/java/eu/europa/ec/edelivery/smp/data/dao/DomainDaoTest.java @@ -0,0 +1,76 @@ +package eu.europa.ec.edelivery.smp.data.dao; + +import eu.europa.ec.edelivery.smp.data.enums.MembershipRoleType; +import eu.europa.ec.edelivery.smp.data.model.DBDomain; +import org.junit.Before; +import org.junit.Test; +import org.springframework.beans.factory.annotation.Autowired; + +import java.util.Collection; +import java.util.Collections; +import java.util.List; + +import static org.junit.Assert.*; + +/** + * The group of resources with shared resource management rights. The user with group admin has rights to create/delete + * resources for the group. + * + * @author Joze Rihtarsic + * @since 5.0 + */ +public class DomainDaoTest extends AbstractBaseDao { + + @Autowired + DomainDao testInstance; + + @Before + public void prepareDatabase() { + // setup initial data! + testUtilsDao.clearData(); + testUtilsDao.creatDomainMemberships(); + + } + @Test + public void getDomainsByUserIdAndRolesCount() { + // one for domain 1 + Long cnt = testInstance.getDomainsByUserIdAndRolesCount(testUtilsDao.getUser1().getId(), MembershipRoleType.ADMIN); + assertEquals(1, cnt.intValue()); + + // one for domain 2 + cnt = testInstance.getDomainsByUserIdAndRolesCount(testUtilsDao.getUser1().getId(), MembershipRoleType.VIEWER); + assertEquals(1, cnt.intValue()); + + // all + cnt = testInstance.getDomainsByUserIdAndRolesCount(testUtilsDao.getUser1().getId()); + assertEquals(2, cnt.intValue()); + + // all + cnt = testInstance.getDomainsByUserIdAndRolesCount(testUtilsDao.getUser1().getId(), MembershipRoleType.VIEWER, MembershipRoleType.ADMIN); + assertEquals(2, cnt.intValue()); + } + + @Test + public void getDomainsByUserIdAndRoles() { + // one for domain 1 + List<DBDomain> result = testInstance.getDomainsByUserIdAndRoles(testUtilsDao.getUser1().getId(), MembershipRoleType.ADMIN); + assertEquals(1, result.size()); + assertEquals(testUtilsDao.getD1(), result.get(0)); + + // one for domain 2 + result = testInstance.getDomainsByUserIdAndRoles(testUtilsDao.getUser1().getId(), MembershipRoleType.VIEWER); + assertEquals(1, result.size()); + assertEquals(testUtilsDao.getD2(), result.get(0)); + + result = testInstance.getDomainsByUserIdAndRoles(testUtilsDao.getUser2().getId(), MembershipRoleType.VIEWER); + assertEquals(0, result.size()); + + result = testInstance.getDomainsByUserIdAndRoles(testUtilsDao.getUser1().getId()); + assertEquals(2, result.size()); + + result = testInstance.getDomainsByUserIdAndRoles(testUtilsDao.getUser1().getId(), MembershipRoleType.VIEWER, MembershipRoleType.ADMIN); + assertEquals(2, result.size()); + + + } +} diff --git a/smp-server-library/src/test/java/eu/europa/ec/edelivery/smp/data/dao/TestUtilsDao.java b/smp-server-library/src/test/java/eu/europa/ec/edelivery/smp/data/dao/TestUtilsDao.java index 1f9646785..ee1557a6f 100644 --- a/smp-server-library/src/test/java/eu/europa/ec/edelivery/smp/data/dao/TestUtilsDao.java +++ b/smp-server-library/src/test/java/eu/europa/ec/edelivery/smp/data/dao/TestUtilsDao.java @@ -1,5 +1,6 @@ package eu.europa.ec.edelivery.smp.data.dao; +import eu.europa.ec.edelivery.smp.data.enums.MembershipRoleType; import eu.europa.ec.edelivery.smp.data.model.DBDomain; import eu.europa.ec.edelivery.smp.data.model.DBDomainResourceDef; import eu.europa.ec.edelivery.smp.data.model.DBGroup; @@ -9,6 +10,7 @@ import eu.europa.ec.edelivery.smp.data.model.doc.DBSubresource; import eu.europa.ec.edelivery.smp.data.model.ext.DBExtension; import eu.europa.ec.edelivery.smp.data.model.ext.DBResourceDef; import eu.europa.ec.edelivery.smp.data.model.ext.DBSubresourceDef; +import eu.europa.ec.edelivery.smp.data.model.user.DBDomainMember; import eu.europa.ec.edelivery.smp.data.model.user.DBUser; import eu.europa.ec.edelivery.smp.logging.SMPLogger; import eu.europa.ec.edelivery.smp.logging.SMPLoggerFactory; @@ -65,6 +67,9 @@ public class TestUtilsDao { DBSubresource subresourceD1G1RD1_S1; DBSubresource subresourceD2G1RD1_S1; + DBDomainMember domainMemberU1D1Admin; + DBDomainMember domainMemberU1D2Viewer; + DBExtension extension; /** @@ -93,7 +98,8 @@ public class TestUtilsDao { subresourceD2G1RD1_S1 = null; documentD1G1RD1_S1 = null; documentD2G1RD1_S1 = null; - + domainMemberU1D1Admin = null; + domainMemberU1D2Viewer = null; extension = null; } @@ -188,6 +194,37 @@ public class TestUtilsDao { assertNotNull(user3.getId()); } + /** + * Create domain members for + * user1 on domain 1 as Admin + * user1 on domain 2 as Viewer + */ + @Transactional + public void creatDomainMemberships() { + if (domainMemberU1D1Admin != null) { + LOG.trace("DomainMemberships are already initialized!"); + return; + } + createDomains(); + createUsers(); + domainMemberU1D1Admin = createDomainMembership(MembershipRoleType.ADMIN, user1, d1); + domainMemberU1D2Viewer = createDomainMembership(MembershipRoleType.VIEWER, user1, d2); + + persistFlushDetach(domainMemberU1D1Admin); + persistFlushDetach(domainMemberU1D2Viewer); + + assertNotNull(domainMemberU1D1Admin.getId()); + assertNotNull(domainMemberU1D2Viewer.getId()); + } + + public DBDomainMember createDomainMembership(MembershipRoleType roleType, DBUser user, DBDomain domain){ + DBDomainMember domainMember = new DBDomainMember(); + domainMember.setRole(roleType); + domainMember.setUser(user); + domainMember.setDomain(domain); + return domainMember; + } + /** * Create resources for ids: * TEST_SG_ID_1, TEST_SG_ID_2 @@ -470,4 +507,12 @@ public class TestUtilsDao { public DBExtension getExtension() { return extension; } + + public DBDomainMember getDomainMemberU1D1Admin() { + return domainMemberU1D1Admin; + } + + public DBDomainMember getDomainMemberU1D2Viewer() { + return domainMemberU1D2Viewer; + } } diff --git a/smp-springboot/README.md b/smp-springboot/README.md index 966025b15..c881a3f21 100644 --- a/smp-springboot/README.md +++ b/smp-springboot/README.md @@ -57,7 +57,7 @@ The script connect to mysql database using CLI tool 'mysql' and deletes databas and insert the init data from [PROJECT_HOME]/smp-soapui-tests/src/test/resources/init-data/init-test-mysql-soapui.sql - +Linux OS: ``` #!/bin/sh @@ -80,6 +80,30 @@ echo "init database for soapui tests" mysql -h localhost -u $DB_ADMIN --password=$DB_ADMIN_PASSWORD $DATABASE < "$PROJECT_HOME/smp-soapui-tests/groovy/mysql-4.1_integration_test_data.sql" ``` +Windows OS: +``` +@echo off + + +set PROJECT_HOME=C:\cef\code\smp +set DATABASE=smpdb +set DB_ADMIN=root +set DB_ADMIN_PASSWORD= +set DB_USERNAME=smp +set DB_PASSWORD=smp + +REM recreate database +echo "clean the database %DATABASE% if exists " +mysql -h localhost -u %DB_ADMIN% --password=%DB_ADMIN_PASSWORD% -e "drop schema if exists %DATABASE%;DROP USER IF EXISTS %DB_USERNAME%;create schema %DATABASE%;alter database %DATABASE% charset=utf8;create user %DB_USERNAME% identified by '%DB_PASSWORD%';grant all on %DATABASE%.* to %DB_USERNAME%;" + + +REM create new database +echo "create database" +mysql -h localhost -u %DB_ADMIN% --password=%DB_ADMIN_PASSWORD% %DATABASE% < "%PROJECT_HOME%\smp-webapp\src\main\smp-setup\database-scripts\mysql5innodb.ddl" +echo "init database for soapui tests" +mysql -h localhost -u %DB_ADMIN% --password=%DB_ADMIN_PASSWORD% %DATABASE% < "%PROJECT_HOME%\smp-soapui-tests\groovy\mysql-4.1_integration_test_data.sql" +``` + ### Prepare the DomiSMP database configuration. To set the DomiSMP database configuration, the following properties must be set. diff --git a/smp-webapp/src/main/java/eu/europa/ec/edelivery/smp/auth/SMPAuthorizationService.java b/smp-webapp/src/main/java/eu/europa/ec/edelivery/smp/auth/SMPAuthorizationService.java index 2f914ad1b..0684a30b2 100644 --- a/smp-webapp/src/main/java/eu/europa/ec/edelivery/smp/auth/SMPAuthorizationService.java +++ b/smp-webapp/src/main/java/eu/europa/ec/edelivery/smp/auth/SMPAuthorizationService.java @@ -75,6 +75,20 @@ public class SMPAuthorizationService { return domainMemberDao.isUserDomainMemberWithRole(userDetails.getUser().getId(), Collections.singletonList(domainId), MembershipRoleType.ADMIN); } + public boolean isAnyDomainAdministrator() { + SMPUserDetails userDetails = getAndValidateUserDetails(); + return domainMemberDao.isUserAnyDomainAdministrator(userDetails.getUser().getId()); + } + + public boolean isAnyGroupAdministrator() { + SMPUserDetails userDetails = getAndValidateUserDetails(); + return domainMemberDao.isUserGroupAdministrator(userDetails.getUser().getId()); + } + public boolean isAnyResourceAdministrator() { + SMPUserDetails userDetails = getAndValidateUserDetails(); + return domainMemberDao.isUserResourceAdministrator(userDetails.getUser().getId()); + } + public boolean isSMPAdministrator() { SMPUserDetails userDetails = getAndValidateUserDetails(); boolean hasRole = hasSessionUserRole(S_AUTHORITY_TOKEN_USER, userDetails); diff --git a/smp-webapp/src/main/java/eu/europa/ec/edelivery/smp/ui/external/UserResource.java b/smp-webapp/src/main/java/eu/europa/ec/edelivery/smp/ui/external/UserResource.java index 4cd776192..7adbf5dfa 100644 --- a/smp-webapp/src/main/java/eu/europa/ec/edelivery/smp/ui/external/UserResource.java +++ b/smp-webapp/src/main/java/eu/europa/ec/edelivery/smp/ui/external/UserResource.java @@ -120,7 +120,11 @@ public class UserResource { DBUser user = uiUserService.findUser(entityId); NavigationTreeNodeRO home = new NavigationTreeNodeRO("home", "Home", "home", ""); home.addChild(createPublicNavigationTreeNode()); - // home.addChild(createEditNavigationTreeNode()); + // create administration nodes for domains, groups and resources + NavigationTreeNodeRO adminNodes = createEditNavigationTreeNode(); + if (!adminNodes.getChildren().isEmpty()) { + home.addChild(adminNodes); + } if (user.getApplicationRole() == ApplicationRoleType.SYSTEM_ADMIN) { home.addChild(createSystemAdminNavigationTreeNode()); } @@ -252,7 +256,7 @@ public class UserResource { protected NavigationTreeNodeRO createPublicNavigationTreeNode() { NavigationTreeNodeRO node = new NavigationTreeNodeRO("search-tools", "Search", "search", "public"); node.addChild(new NavigationTreeNodeRO("search-resources", "Resources", "find_in_page", "search-resource","Search registered resources")); - // node.addChild(new NavigationTreeNodeRO("search-lookup", "DNS lookup", "dns", "dns-lookup" , "DNS lookup tools")); + node.addChild(new NavigationTreeNodeRO("search-lookup", "DNS lookup", "dns", "dns-lookup" , "DNS lookup tools")); return node; } @@ -267,29 +271,31 @@ public class UserResource { protected NavigationTreeNodeRO createSystemAdminNavigationTreeNode() { NavigationTreeNodeRO node = new NavigationTreeNodeRO("system-settings", "System settings", "admin_panel_settings", "system-settings"); + node.addChild(new NavigationTreeNodeRO("system-admin-user", "Users", "people", "user")); node.addChild(new NavigationTreeNodeRO("system-admin-domain", "Domains", "domain", "domain")); node.addChild(new NavigationTreeNodeRO("system-admin-keystore", "Keystore", "key", "keystore")); node.addChild(new NavigationTreeNodeRO("system-admin-truststore", "Truststore", "article", "truststore")); node.addChild(new NavigationTreeNodeRO("system-admin-extension", "Extensions", "extension", "extension")); - node.addChild(new NavigationTreeNodeRO("system-admin-user", "Users", "people", "user")); node.addChild(new NavigationTreeNodeRO("system-admin-properties", "Properties", "properties", "properties")); // node.addChild(new NavigationTreeNodeRO("system-admin-authentication", "Authentication", "shield", "authentication")); - - node.addChild(new NavigationTreeNodeRO("system-admin-alert", "Alerts", "notifications", "alert")); return node; } protected NavigationTreeNodeRO createEditNavigationTreeNode() { - NavigationTreeNodeRO node = new NavigationTreeNodeRO("admin-entities", "Administration", "settings", "admin-entities"); + NavigationTreeNodeRO node = new NavigationTreeNodeRO("administration", "Administration", "settings", "administration"); // is domain admin - - node.addChild(new NavigationTreeNodeRO("admin-domain", "Edit domains", "account_circle", "admin-domain")); - // is group admin - node.addChild(new NavigationTreeNodeRO("admin-group", "Edit groups", "key", "admin-group")); - // is resource admin - node.addChild(new NavigationTreeNodeRO("admin-resource", "Edit resources", "article", "admin-resource")); - // node.addChild(new NavigationTreeNodeRO("user-data-membership", "Membership", "person", "user-membership")); + if (authorizationService.isAnyDomainAdministrator()) { + node.addChild(new NavigationTreeNodeRO("admin-domain", "Edit domains", "account_circle", "admin-domain")); + } + if (authorizationService.isAnyGroupAdministrator()) { + // is group admin + node.addChild(new NavigationTreeNodeRO("admin-group", "Edit groups", "key", "admin-group")); + } + if (authorizationService.isAnyResourceAdministrator()) { + // is resource admin + node.addChild(new NavigationTreeNodeRO("admin-resource", "Edit resources", "article", "admin-resource")); + } return node; } } diff --git a/smp-webapp/src/main/java/eu/europa/ec/edelivery/smp/ui/internal/UserAdminResource.java b/smp-webapp/src/main/java/eu/europa/ec/edelivery/smp/ui/internal/UserAdminResource.java index 6cb7cfd38..5a2d92eed 100644 --- a/smp-webapp/src/main/java/eu/europa/ec/edelivery/smp/ui/internal/UserAdminResource.java +++ b/smp-webapp/src/main/java/eu/europa/ec/edelivery/smp/ui/internal/UserAdminResource.java @@ -12,6 +12,7 @@ import eu.europa.ec.edelivery.smp.services.ui.UIUserService; import eu.europa.ec.edelivery.smp.services.ui.filters.UserFilter; import eu.europa.ec.edelivery.smp.utils.SessionSecurityUtils; import org.springframework.security.access.annotation.Secured; +import org.springframework.security.access.prepost.PreAuthorize; import org.springframework.security.web.authentication.session.SessionAuthenticationException; import org.springframework.util.MimeTypeUtils; import org.springframework.web.bind.annotation.*; @@ -20,7 +21,8 @@ import java.util.Arrays; import java.util.List; import java.util.stream.Collectors; -import static eu.europa.ec.edelivery.smp.ui.ResourceConstants.CONTEXT_PATH_INTERNAL_USER; +import static eu.europa.ec.edelivery.smp.ui.ResourceConstants.*; +import static eu.europa.ec.edelivery.smp.ui.ResourceConstants.PARAM_PAGINATION_FILTER; import static eu.europa.ec.edelivery.smp.utils.SessionSecurityUtils.decryptEntityId; /** @@ -32,7 +34,6 @@ import static eu.europa.ec.edelivery.smp.utils.SessionSecurityUtils.decryptEntit public class UserAdminResource { private static final SMPLogger LOG = SMPLoggerFactory.getLogger(UserAdminResource.class); - protected UIUserService uiUserService; protected UITruststoreService uiTruststoreService; protected SMPAuthorizationService authorizationService; @@ -60,6 +61,71 @@ public class UserAdminResource { return uiUserService.getTableList(page, pageSize, orderBy, orderType, filter); } + @GetMapping(path = "/{user-enc-id}/search", produces = MimeTypeUtils.APPLICATION_JSON_VALUE) + @PreAuthorize("@smpAuthorizationService.isCurrentlyLoggedIn(#userEncId) and @smpAuthorizationService.isSystemAdministrator") + public ServiceResult<SearchUserRO> getDomainMemberList( + @PathVariable("user-enc-id") String userEncId, + @RequestParam(value = PARAM_PAGINATION_PAGE, defaultValue = "0") int page, + @RequestParam(value = PARAM_PAGINATION_PAGE_SIZE, defaultValue = "10") int pageSize, + @RequestParam(value = PARAM_PAGINATION_FILTER, defaultValue = "", required = false) String filter) { + + LOG.info("Search user with filter [{}], paging: [{}/{}], user: {}",filter, page, pageSize, userEncId); + return uiUserService.searchUsers(page, pageSize, filter); + } + + @GetMapping(path = "/{user-enc-id}/{managed-user-enc-id}/retrieve", produces = MimeTypeUtils.APPLICATION_JSON_VALUE) + @PreAuthorize("@smpAuthorizationService.isCurrentlyLoggedIn(#userEncId) and @smpAuthorizationService.isSystemAdministrator") + public UserRO getUserData(@PathVariable("user-enc-id") String userEncId, + @PathVariable("managed-user-enc-id") String managedUserEncId) { + Long managedUserId = decryptEntityId(managedUserEncId); + return uiUserService.getUserById(managedUserId); + } + + @PostMapping(path = "/{user-enc-id}/{managed-user-enc-id}/update", produces = MimeTypeUtils.APPLICATION_JSON_VALUE) + @PreAuthorize("@smpAuthorizationService.isCurrentlyLoggedIn(#userEncId) and @smpAuthorizationService.isSystemAdministrator") + public UserRO updateUser(@PathVariable("user-enc-id") String userEncId, + @PathVariable("managed-user-enc-id") String managedUserEncId, + @RequestBody UserRO user) { + + Long userId = decryptEntityId(userEncId); + Long managedUserId = decryptEntityId(managedUserEncId); + LOG.info("UpdateUserData adminId: [{}], managedUserId: [{}]", userId, managedUserId); + // Update the user and mark the password as changed at this very instant of time + uiUserService.adminUpdateUserData(managedUserId, user); + // refresh user from DB + UserRO userRO = uiUserService.getUserById(managedUserId); + // return clean user to UI + return authorizationService.sanitize(userRO); + } + + + @DeleteMapping(path = "/{user-enc-id}/{managed-user-enc-id}/delete", produces = MimeTypeUtils.APPLICATION_JSON_VALUE) + @PreAuthorize("@smpAuthorizationService.isCurrentlyLoggedIn(#userEncId) and @smpAuthorizationService.isSystemAdministrator") + public UserRO deleteUser(@PathVariable("user-enc-id") String userEncId, + @PathVariable("managed-user-enc-id") String managedUserEncId) { + + Long userId = decryptEntityId(userEncId); + Long managedUserId = decryptEntityId(managedUserEncId); + LOG.info("DeleteUserData adminId: [{}], managedUserId: [{}]", userId, managedUserId); + // Update the user and mark the password as changed at this very instant of time + UserRO deleted = uiUserService.adminDeleteUserData(managedUserId); + // return clean user to UI + return authorizationService.sanitize(deleted); + } + + @PutMapping(path = "/{user-enc-id}/create", produces = MimeTypeUtils.APPLICATION_JSON_VALUE) + @PreAuthorize("@smpAuthorizationService.isCurrentlyLoggedIn(#userEncId) and @smpAuthorizationService.isSystemAdministrator") + public UserRO createUser(@PathVariable("user-enc-id") String userEncId, + @RequestBody UserRO user) { + + Long userId = decryptEntityId(userEncId); + LOG.info("createUserData adminId: [{}], managedUserId: [{}]", userId); + // Update the user and mark the password as changed at this very instant of time + return uiUserService.adminCreateUserData(user); + } + + + @PutMapping(produces = MimeTypeUtils.APPLICATION_JSON_VALUE) @Secured({SMPAuthority.S_AUTHORITY_TOKEN_SYSTEM_ADMIN}) public void updateUserList(@RequestBody UserRO[] updateEntities) { -- GitLab