Merge branch 'dmp-refactoring' of https://code-repo.d4science.org/MaDgiK-CITE/argos into dmp-refactoring

This commit is contained in:
Sofia Papacharalampous 2024-06-21 15:52:45 +03:00
commit 7c4701cb22
32 changed files with 899 additions and 91 deletions

View File

@ -21,7 +21,8 @@ public class NotificationProperties {
private UUID descriptionTemplateInvitationType;
private UUID contactSupportType;
private UUID publicContactSupportType;
private UUID tenantSpecificInvitationUserType;
private UUID tenantSpecificInvitationExternalUserType;
private UUID tenantSpecificInvitationExistingUserType;
private int emailExpirationTimeSeconds;
private String contactSupportEmail;
@ -153,11 +154,19 @@ public class NotificationProperties {
this.descriptionAnnotationCreated = descriptionAnnotationCreated;
}
public UUID getTenantSpecificInvitationUserType() {
return tenantSpecificInvitationUserType;
public UUID getTenantSpecificInvitationExternalUserType() {
return tenantSpecificInvitationExternalUserType;
}
public void setTenantSpecificInvitationUserType(UUID tenantSpecificInvitationUserType) {
this.tenantSpecificInvitationUserType = tenantSpecificInvitationUserType;
public void setTenantSpecificInvitationExternalUserType(UUID tenantSpecificInvitationExternalUserType) {
this.tenantSpecificInvitationExternalUserType = tenantSpecificInvitationExternalUserType;
}
public UUID getTenantSpecificInvitationExistingUserType() {
return tenantSpecificInvitationExistingUserType;
}
public void setTenantSpecificInvitationExistingUserType(UUID tenantSpecificInvitationExistingUserType) {
this.tenantSpecificInvitationExistingUserType = tenantSpecificInvitationExistingUserType;
}
}

View File

@ -914,19 +914,23 @@ public class UserServiceImpl implements UserService {
public void sendUserToTenantInvitation(UserTenantUsersInviteRequest users) throws InvalidApplicationException, JAXBException {
String tenantName = null;
String tenantCode = null;
String tenantCode;
if (this.tenantScope.getTenantCode() != null && !this.tenantScope.getTenantCode().equals(this.tenantScope.getDefaultTenantCode())) {
TenantEntity tenantEntity = this.queryFactory.query(TenantQuery.class).disableTracking().authorize(AuthorizationFlags.AllExceptPublic).codes(this.tenantScope.getTenantCode()).isActive(IsActive.Active).first();
if (tenantEntity == null) throw new MyApplicationException("Tenant not found");
tenantName = tenantEntity.getName();
tenantCode = tenantEntity.getCode();
} else {
tenantName = "OpenCDMP";
tenantCode = this.tenantScope.getDefaultTenantCode();
}
for (UserInviteToTenantRequestPersist user: users.getUsers()) {
String token = this.createUserInviteToTenantConfirmation(user, tenantCode);
this.createTenantSpecificInvitationUserNotificationEvent(token, user.getEmail(), tenantName);
UserContactInfoEntity contactInfoEntity = this.queryFactory.query(UserContactInfoQuery.class).disableTracking().values(user.getEmail()).types(ContactInfoType.Email).first();
if (contactInfoEntity != null){
this.createTenantSpecificInvitationUserNotificationEvent(token, user.getEmail(), tenantName, contactInfoEntity.getUserId());
} else {
this.createTenantSpecificInvitationUserNotificationEvent(token, user.getEmail(), tenantName, null);
}
}
}
@ -952,26 +956,32 @@ public class UserServiceImpl implements UserService {
return persist.getToken();
}
private void createTenantSpecificInvitationUserNotificationEvent(String token, String email, String tenantName) throws InvalidApplicationException {
UserEntity currentUser = this.entityManager.find(UserEntity.class, this.userScope.getUserIdSafe());
if (currentUser == null) throw new MyNotFoundException(this.messageSource.getMessage("General_ItemNotFound", new Object[]{ this.userScope.getUserIdSafe(), User.class.getSimpleName()}, LocaleContextHolder.getLocale()));
private void createTenantSpecificInvitationUserNotificationEvent(String token, String email, String tenantName, UUID existingRecipient) throws InvalidApplicationException {
UserEntity sender = this.entityManager.find(UserEntity.class, this.userScope.getUserIdSafe());
if (sender == null) throw new MyNotFoundException(this.messageSource.getMessage("General_ItemNotFound", new Object[]{ this.userScope.getUserIdSafe(), User.class.getSimpleName()}, LocaleContextHolder.getLocale()));
NotifyIntegrationEvent event = new NotifyIntegrationEvent();
List<ContactPair> contactPairs = new ArrayList<>();
contactPairs.add(new ContactPair(ContactInfoType.Email, email));
if (existingRecipient == null) {
List<ContactPair> contactPairs = new ArrayList<>();
contactPairs.add(new ContactPair(ContactInfoType.Email, email));
NotificationContactData contactData = new NotificationContactData(contactPairs, null, null);
event.setContactHint(this.jsonHandlingService.toJsonSafe(contactData));
event.setContactTypeHint(NotificationContactType.EMAIL);
NotificationContactData contactData = new NotificationContactData(contactPairs, null, null);
event.setContactHint(this.jsonHandlingService.toJsonSafe(contactData));
event.setContactTypeHint(NotificationContactType.EMAIL);
event.setNotificationType(this.notificationProperties.getTenantSpecificInvitationExternalUserType());
} else {
event.setUserId(existingRecipient);
event.setNotificationType(this.notificationProperties.getTenantSpecificInvitationExistingUserType());
}
event.setNotificationType(this.notificationProperties.getTenantSpecificInvitationUserType());
NotificationFieldData data = new NotificationFieldData();
List<FieldInfo> fieldInfoList = new ArrayList<>();
fieldInfoList.add(new FieldInfo("{userName}", DataType.String, currentUser.getName()));
fieldInfoList.add(new FieldInfo("{userName}", DataType.String, sender.getName()));
fieldInfoList.add(new FieldInfo("{confirmationToken}", DataType.String, token));
fieldInfoList.add(new FieldInfo("{expiration_time}", DataType.String, this.secondsToTime(this.notificationProperties.getEmailExpirationTimeSeconds())));
fieldInfoList.add(new FieldInfo("{tenantName}", DataType.String, tenantName));
if (!this.conventionService.isNullOrEmpty(tenantName)) fieldInfoList.add(new FieldInfo("{tenantName}", DataType.String, tenantName));
data.setFields(fieldInfoList);
event.setData(this.jsonHandlingService.toJsonSafe(data));
@ -991,10 +1001,14 @@ public class UserServiceImpl implements UserService {
this.checkActionState(action);
UserInviteToTenantRequestEntity userInviteToTenantRequest = this.xmlHandlingService.fromXmlSafe(UserInviteToTenantRequestEntity.class, action.getData());
if (userInviteToTenantRequest == null) throw new MyNotFoundException(this.messageSource.getMessage("General_ItemNotFound", new Object[]{action.getId(), UserInviteToTenantRequestEntity.class.getSimpleName()}, LocaleContextHolder.getLocale()));
if (userInviteToTenantRequest == null)
throw new MyNotFoundException(this.messageSource.getMessage("General_ItemNotFound", new Object[]{action.getId(), UserInviteToTenantRequestEntity.class.getSimpleName()}, LocaleContextHolder.getLocale()));
TenantEntity tenantEntity = this.queryFactory.query(TenantQuery.class).disableTracking().authorize(AuthorizationFlags.AllExceptPublic).codes(userInviteToTenantRequest.getTenantCode()).isActive(IsActive.Active).first();
if (tenantEntity == null) throw new MyApplicationException("Tenant not found");
TenantEntity tenantEntity = null;
if (!userInviteToTenantRequest.getTenantCode().equals(this.tenantScope.getTenantCode())) {
tenantEntity = this.queryFactory.query(TenantQuery.class).disableTracking().authorize(AuthorizationFlags.AllExceptPublic).codes(userInviteToTenantRequest.getTenantCode()).isActive(IsActive.Active).first();
if (tenantEntity == null) throw new MyApplicationException("Tenant not found");
}
this.addUserToTenant(tenantEntity, userInviteToTenantRequest);
}
@ -1014,21 +1028,25 @@ public class UserServiceImpl implements UserService {
UserCredentialEntity userCredential = this.queryFactory.query(UserCredentialQuery.class).disableTracking().userIds(userId).first();
if (userCredential == null) throw new MyApplicationException();
TenantUserEntity tenantUserEntity = new TenantUserEntity();
tenantUserEntity.setId(UUID.randomUUID());
tenantUserEntity.setUserId(userId);
tenantUserEntity.setIsActive(IsActive.Active);
tenantUserEntity.setTenantId(tenant.getId());
tenantUserEntity.setCreatedAt(Instant.now());
tenantUserEntity.setUpdatedAt(Instant.now());
this.entityManager.persist(tenantUserEntity);
this.eventBroker.emit(new UserAddedToTenantEvent(tenantUserEntity.getUserId(), tenantUserEntity.getTenantId()));
if (tenant != null){
TenantUserEntity tenantUserEntity = new TenantUserEntity();
tenantUserEntity.setId(UUID.randomUUID());
tenantUserEntity.setUserId(userId);
tenantUserEntity.setIsActive(IsActive.Active);
tenantUserEntity.setTenantId(tenant.getId());
tenantUserEntity.setCreatedAt(Instant.now());
tenantUserEntity.setUpdatedAt(Instant.now());
this.entityManager.persist(tenantUserEntity);
this.eventBroker.emit(new UserAddedToTenantEvent(tenantUserEntity.getUserId(), tenantUserEntity.getTenantId()));
}
for (String role: userInviteToTenantRequest.getRoles()) {
UserRoleEntity item = new UserRoleEntity();
item.setId(UUID.randomUUID());
item.setUserId(userId);
item.setTenantId(tenant.getId());
if (tenant != null) item.setTenantId(tenant.getId());
else item.setTenantId(null);
item.setRole(role);
item.setCreatedAt(Instant.now());
this.entityManager.persist(item);
@ -1043,7 +1061,8 @@ public class UserServiceImpl implements UserService {
this.entityManager.flush();
for (String role: userInviteToTenantRequest.getRoles()) {
this.keycloakService.addUserToTenantRoleGroup(userCredential.getExternalId(), tenant.getCode(), role);
if (tenant != null && !this.conventionService.isNullOrEmpty(tenant.getCode())) this.keycloakService.addUserToTenantRoleGroup(userCredential.getExternalId(), tenant.getCode(), role);
else this.keycloakService.addUserToTenantRoleGroup(userCredential.getExternalId(), tenantScope.getDefaultTenantCode(), role);
}
}

View File

@ -13,5 +13,6 @@ notification:
descriptionTemplateInvitationType: 223BB607-EFA1-4CE7-99EC-4BEABFEF9A8B
contactSupportType: 5B1D6C52-88F9-418B-9B8A-6F1F963D9EAD
publicContactSupportType: B542B606-ACC6-4629-ADEF-4D8EE2F01222
tenantSpecificInvitationUserType: 497dada5-eccc-4bc0-9e0b-63e22b4eb0be
tenantSpecificInvitationExternalUserType: 497dada5-eccc-4bc0-9e0b-63e22b4eb0be
tenantSpecificInvitationExistingUserType: b3809c17-d1e4-420a-919c-828564114191
contactSupportEmail: support@dmp.com

View File

@ -84,3 +84,12 @@ export interface UserMergeRequestPersist {
export interface RemoveCredentialRequestPersist {
credentialId: Guid;
}
export interface UserTenantUsersInviteRequest {
users: UserInviteToTenantRequest[];
}
export interface UserInviteToTenantRequest {
email: string;
roles: string[];
}

View File

@ -1,7 +1,7 @@
import { HttpClient, HttpHeaders, HttpResponse } from '@angular/common/http';
import { Injectable } from '@angular/core';
import { IsActive } from '@app/core/common/enum/is-active.enum';
import { DmpAssociatedUser, RemoveCredentialRequestPersist, User, UserMergeRequestPersist, UserPersist, UserRolePatchPersist } from '@app/core/model/user/user';
import { DmpAssociatedUser, RemoveCredentialRequestPersist, User, UserMergeRequestPersist, UserPersist, UserRolePatchPersist, UserTenantUsersInviteRequest } from '@app/core/model/user/user';
import { UserLookup } from '@app/core/query/user.lookup';
import { MultipleAutoCompleteConfiguration } from '@app/library/auto-complete/multiple/multiple-auto-complete-configuration';
import { SingleAutoCompleteConfiguration } from '@app/library/auto-complete/single/single-auto-complete-configuration';
@ -139,6 +139,22 @@ export class UserService {
catchError((error: any) => throwError(error)));
}
inviteUsersToTenant(item: UserTenantUsersInviteRequest): Observable<boolean> {
const url = `${this.apiBase}/invite-users-to-tenant`;
return this.http
.post<boolean>(url, item).pipe(
catchError((error: any) => throwError(error)));
}
confirmInviteUser(token: Guid): Observable<boolean> {
const url = `${this.apiBase}/confirm-invite-user-to-tenant/token/${token}`;
return this.http
.get<boolean>(url).pipe(
catchError((error: any) => throwError(error)));
}
//
// Autocomplete Commons
//

View File

@ -0,0 +1,117 @@
import { UntypedFormArray, UntypedFormBuilder, UntypedFormGroup, Validators } from "@angular/forms";
import { UserInviteToTenantRequest, UserTenantUsersInviteRequest } from "@app/core/model/user/user";
import { BackendErrorValidator } from '@common/forms/validation/custom-validator';
import { ValidationErrorModel } from '@common/forms/validation/error-model/validation-error-model';
import { Validation, ValidationContext } from '@common/forms/validation/validation-context';
export class UserTenantUsersInviteRequestEditorModel implements UserTenantUsersInviteRequest {
users: UserInviteToTenantRequestEditorModel[] = [];
public validationErrorModel: ValidationErrorModel = new ValidationErrorModel();
protected formBuilder: UntypedFormBuilder = new UntypedFormBuilder();
constructor() { }
buildForm(context: ValidationContext = null, disabled: boolean = false): UntypedFormGroup {
if (context == null) { context = this.createValidationContext(); }
return this.formBuilder.group({
users: this.formBuilder.array(
(this.users ?? []).map(
(item, index) => item.buildForm({
rootPath: `userInviteToTenantRequest[${index}].`,
disabled: disabled
})
), context.getValidation('users').validators
),
});
}
createValidationContext(): ValidationContext {
const baseContext: ValidationContext = new ValidationContext();
const baseValidationArray: Validation[] = new Array<Validation>();
baseValidationArray.push({ key: 'users', validators: [Validators.required, BackendErrorValidator(this.validationErrorModel, 'userInviteToTenantRequest')] });
baseContext.validation = baseValidationArray;
return baseContext;
}
static reapplyValidators(params: {
formArray: UntypedFormArray,
validationErrorModel: ValidationErrorModel,
}): void {
const { validationErrorModel, formArray } = params;
formArray?.controls?.forEach(
(control, index) => UserInviteToTenantRequestEditorModel.reapplyValidators({
formGroup: control as UntypedFormGroup,
rootPath: `userInviteToTenantRequest[${index}].`,
validationErrorModel: validationErrorModel
})
);
}
}
export class UserInviteToTenantRequestEditorModel implements UserInviteToTenantRequest {
email: string;
roles: string[];
protected formBuilder: UntypedFormBuilder = new UntypedFormBuilder();
constructor(
public validationErrorModel: ValidationErrorModel = new ValidationErrorModel()
) { }
buildForm(params?: {
context?: ValidationContext,
disabled?: boolean,
rootPath?: string
}): UntypedFormGroup {
let { context = null, disabled = false, rootPath } = params ?? {}
if (context == null) {
context = UserInviteToTenantRequestEditorModel.createValidationContext({
validationErrorModel: this.validationErrorModel,
rootPath
});
}
return this.formBuilder.group({
email: [{ value: this.email, disabled: disabled }, context.getValidation('email').validators],
roles: [{ value: this.roles, disabled: disabled }, context.getValidation('roles').validators],
});
}
static createValidationContext(params: {
rootPath?: string,
validationErrorModel: ValidationErrorModel
}): ValidationContext {
const { rootPath = '', validationErrorModel } = params;
const baseContext: ValidationContext = new ValidationContext();
const baseValidationArray: Validation[] = new Array<Validation>();
baseValidationArray.push({ key: 'email', validators: [Validators.required, BackendErrorValidator(validationErrorModel, `${rootPath}email`)] });
baseValidationArray.push({ key: 'roles', validators: [Validators.required, BackendErrorValidator(validationErrorModel, `${rootPath}roles`)] });
baseContext.validation = baseValidationArray;
return baseContext;
}
static reapplyValidators(params: {
formGroup: UntypedFormGroup,
validationErrorModel: ValidationErrorModel,
rootPath: string
}): void {
const { formGroup, rootPath, validationErrorModel } = params;
const context = UserInviteToTenantRequestEditorModel.createValidationContext({
rootPath,
validationErrorModel
});
['email', 'roles'].forEach(keyField => {
const control = formGroup?.get(keyField);
control?.clearValidators();
control?.addValidators(context.getValidation(keyField).validators);
})
}
}

View File

@ -0,0 +1,57 @@
<div class="user-invite-to-tenant-dialog container-fluid" *ngIf="formGroup">
<div class="row">
<div class="col">
<h1 class="title">{{'USER-INVITE-TO-TENANT-DIALOG.TITLE' | translate}}</h1>
</div>
<div class="col-auto" (click)="closeDialog()">
<mat-icon class="close-icon">close</mat-icon>
</div>
</div>
<div mat-dialog-content class="row">
<div *ngFor="let user of formGroup.get('users').controls; let userIndex=index;" class="row mb-3">
<div class="col-12 col-xl mt-3">
<mat-form-field class="w-100">
<mat-label>{{'USER-INVITE-TO-TENANT-DIALOG.FIELDS.EMAIL' | translate}}</mat-label>
<input matInput type="text" name="email" [formControl]="user.get('email')">
<mat-error *ngIf="user.get('email').hasError('backendError')">{{user.get('email').getError('backendError').message}}</mat-error>
<mat-error *ngIf="user.get('email').hasError('required')">{{'GENERAL.VALIDATION.REQUIRED' | translate}}</mat-error>
</mat-form-field>
</div>
<div class="col-12 col-xl mt-3">
<mat-form-field class="w-100">
<mat-label>{{'USER-INVITE-TO-TENANT-DIALOG.FIELDS.ROLES' | translate}}</mat-label>
<mat-select multiple [formControl]="user.get('roles')">
<mat-option [value]="appRoleEnum.TenantAdmin">{{enumUtils.toAppRoleString(appRoleEnum.TenantAdmin)}}</mat-option>
<mat-option [value]="appRoleEnum.TenantPlanManager">{{enumUtils.toAppRoleString(appRoleEnum.TenantPlanManager)}}</mat-option>
<mat-option [value]="appRoleEnum.TenantConfigManager">{{enumUtils.toAppRoleString(appRoleEnum.TenantConfigManager)}}</mat-option>
<mat-option [value]="appRoleEnum.TenantUser">{{enumUtils.toAppRoleString(appRoleEnum.TenantUser)}}</mat-option>
</mat-select>
<mat-error *ngIf="user.get('roles').hasError('backendError')">{{user.get('roles').getError('backendError').message}}</mat-error>
<mat-error *ngIf="user.get('roles').hasError('required')">{{'GENERAL.VALIDATION.REQUIRED' | translate}}</mat-error>
</mat-form-field>
</div>
<div class="col-12 col-xl-auto">
<button mat-icon-button class="action-list-icon" matTooltip="{{'USER-INVITE-TO-TENANT-DIALOG.ACTIONS.REMOVE-USER' | translate}}" (click)="removeUser(userIndex)" [disabled]="formGroup.get('users').controls.length == 1">
<mat-icon>delete</mat-icon>
</button>
</div>
</div>
</div>
<mat-error *ngIf="formGroup.get('users').dirty && formGroup.get('users').hasError('required')">{{'USER-INVITE-TO-TENANT-DIALOG.USERS-REQUIRED' | translate}}</mat-error>
<mat-error *ngIf="formGroup.get('users').hasError('backendError')">{{formGroup.get('users').getError('backendError').message}}</mat-error>
<div class="p-2">
<div class="row">
<div class="col">
<button mat-icon-button (click)="addUser()">
<mat-icon>add</mat-icon>
</button>
</div>
</div>
</div>
<div class="col mt-2">
<button mat-raised-button [disabled]="inProgressSendButton" (click)="send()" type="button" class="invite-btn">{{'USER-INVITE-TO-TENANT-DIALOG.ACTIONS.INVITE' | translate}}</button>
<mat-error *ngIf="formGroup.get('users').hasError('backendError')">{{formGroup.get('users').getError('backendError').message}}</mat-error>
<mat-error *ngIf="formGroup.get('users').hasError('required')">{{'GENERAL.VALIDATION.REQUIRED' | translate}}</mat-error>
</div>
</div>

View File

@ -0,0 +1,150 @@
.user-invite-to-tenant-dialog {
padding: 2.5rem;
.form-container {
width: 33em;
min-height: 14em;
padding: 0.28em 0.34em 0em 1.125em;
}
.close-icon {
cursor: pointer;
// margin-right: 20px;
padding: .4rem;
width: auto !important;
height: auto !important;
}
.close-icon:hover {
background-color: #ECECED !important;
border-radius: 50%;
}
.title {
font-size: 2.375em;
font-weight: lighter;
color: #000000;
opacity: 0.8;
margin-bottom: 0.842em;
}
.content {
width: 31em;
margin: 0px;
padding: 0px;
}
// .mat-form-field {
// background: #fafafa;
// border: 1px solid #d1d1d1;
// border-radius: 4px;
// }
.hint {
font-size: 0.875rem;
font-weight: 500;
color: #212121;
opacity: 0.81;
}
::ng-deep .mat-dialog-container {
border-radius: 8px;
}
.search {
padding: 2px !important;
}
::ng-deep .search {
.mat-form-field-infix {
font-size: 1rem;
padding: 0.6em 0 1em 0 !important;
}
// .mat-form-field-underline {
// display: none;
// }
// .mat-form-field-flex {
// padding: 0em;
// }
// .align-arrow-right {
// display: none;
// }
}
.select-role {
width: 50% !important;
font-size: 14px;
color: #848484;
height: min-content;
margin-right: 2rem;
border: none;
background-color: transparent;
}
::ng-deep .select-role {
.mat-form-field-outline-start,
.mat-form-field-outline-gap,
.mat-form-field-outline-end {
border: none !important;
}
.mat-select-arrow-wrapper {
transform: none !important;
}
}
::ng-deep .select-role .mat-form-field-wrapper {
padding-bottom: 0 !important;
}
.invite-btn {
background: #ffffff 0% 0% no-repeat padding-box;
border: 1px solid var(--primary-color);
border-radius: 30px;
opacity: 1;
min-width: 101px;
height: 43px;
color: var(--primary-color);
font-weight: 500;
}
.invite-btn-disabled {
min-width: 6.64em;
height: 2.93em;
background: #ffffff;
border: 1px solid #b5b5b5;
border-radius: 30px;
font-weight: bold;
letter-spacing: -0.35px;
color: #b5b5b5;
margin-bottom: 0.25em;
cursor: default;
}
.invite-btn:hover {
background: var(--primary-color);
color: #ffffff;
}
@keyframes blink {
0% {
border: 1px solid rgba(255, 0, 0, 0.2);
}
50% {
border: 1px solid rgba(255, 0, 0, 0.5);
}
100% {
border: 1px solid rgba(255, 0, 0, 0.2);
}
}
:host ::ng-deep .invalid-email {
border: 1px solid red;
// animation: blink 1.4s infinite;
// animation-fill-mode: both;
}
}

View File

@ -0,0 +1,110 @@
import { COMMA, ENTER } from '@angular/cdk/keycodes';
import { HttpErrorResponse } from '@angular/common/http';
import { Component, Inject, OnInit } from '@angular/core';
import { UntypedFormArray, UntypedFormGroup } from '@angular/forms';
import { MatDialogRef } from '@angular/material/dialog';
import { ActivatedRoute, Router } from '@angular/router';
import { SnackBarNotificationLevel, UiNotificationService } from '@app/core/services/notification/ui-notification-service';
import { EnumUtils } from '@app/core/services/utilities/enum-utils.service';
import { BaseComponent } from '@common/base/base.component';
import { FormService } from '@common/forms/form-service';
import { HttpError, HttpErrorHandlingService } from '@common/modules/errors/error-handling/http-error-handling.service';
import { TranslateService } from '@ngx-translate/core';
import { takeUntil } from 'rxjs/operators';
import { UserInviteToTenantRequestEditorModel, UserTenantUsersInviteRequestEditorModel } from './user-invite-to-tenant-dialog-editor.model';
import { UserTenantUsersInviteRequest } from '@app/core/model/user/user';
import { UserService } from '@app/core/services/user/user.service';
import { AppRole } from '@app/core/common/enum/app-role';
@Component({
selector: 'app-user-invite-to-tenant-dialog.component',
templateUrl: 'user-invite-to-tenant-dialog.component.html',
styleUrls: ['./user-invite-to-tenant-dialog.component.scss']
})
export class UserInviteToTenantDialogComponent extends BaseComponent implements OnInit {
editorModel: UserTenantUsersInviteRequestEditorModel;
formGroup: any;
inProgressSendButton = false;
appRoleEnum = AppRole;
readonly separatorKeysCodes: number[] = [ENTER, COMMA];
constructor(
public enumUtils: EnumUtils,
public route: ActivatedRoute,
public router: Router,
private language: TranslateService,
public dialogRef: MatDialogRef<UserInviteToTenantDialogComponent>,
private uiNotificationService: UiNotificationService,
private httpErrorHandlingService: HttpErrorHandlingService,
private userService: UserService,
private formService: FormService,
) {
super();
}
ngOnInit() {
this.editorModel = new UserTenantUsersInviteRequestEditorModel();
this.formGroup = this.editorModel.buildForm();
this.addUser();
}
addUser(): void {
const formArray = this.formGroup.get("users") as UntypedFormArray;
const user: UserInviteToTenantRequestEditorModel = new UserInviteToTenantRequestEditorModel(this.editorModel.validationErrorModel);
formArray.push(user.buildForm({ rootPath: "userInviteToTenantRequest[" + formArray.length + "]." }));
}
removeUser(userIndex: number): void {
(this.formGroup.get("users") as UntypedFormArray).removeAt(userIndex);
UserTenantUsersInviteRequestEditorModel.reapplyValidators(
{
formArray: this.formGroup.get("users") as UntypedFormArray,
validationErrorModel: this.editorModel.validationErrorModel,
}
);
(this.formGroup.get("users") as UntypedFormArray).markAsDirty();
}
send() {
this.formService.removeAllBackEndErrors(this.formGroup);
this.formService.touchAllFormFields(this.formGroup);
if (!this.formGroup.valid) { return; }
this.inProgressSendButton = true;
const userFormData = this.formGroup.value as UserTenantUsersInviteRequest;
this.userService.inviteUsersToTenant(userFormData)
.pipe(takeUntil(this._destroyed))
.subscribe(
complete => {
this.dialogRef.close();
this.onCallbackSuccess();
},
error => this.onCallbackError(error)
);
}
closeDialog(): void {
this.dialogRef.close();
}
onCallbackSuccess(): void {
this.uiNotificationService.snackBarNotification(this.language.instant('DMP-USER-INVITATION-DIALOG.SUCCESS'), SnackBarNotificationLevel.Success);
}
onCallbackError(errorResponse: HttpErrorResponse) {
this.inProgressSendButton = false;
let errorOverrides = new Map<number, string>();
errorOverrides.set(-1, this.language.instant('DMP-USER-INVITATION-DIALOG.ERROR'));
this.httpErrorHandlingService.handleBackedRequestError(errorResponse, errorOverrides, SnackBarNotificationLevel.Error);
const error: HttpError = this.httpErrorHandlingService.getError(errorResponse);
if (error.statusCode === 400) {
this.editorModel.validationErrorModel.fromJSONObject(errorResponse.error);
this.formService.validateAllFormFields(this.formGroup);
}
}
}

View File

@ -0,0 +1,13 @@
import { NgModule } from '@angular/core';
import { FormsModule, ReactiveFormsModule } from '@angular/forms';
import { CommonUiModule } from '@common/ui/common-ui.module';
import { UserInviteToTenantDialogComponent } from './user-invite-to-tenant-dialog.component';
@NgModule({
imports: [CommonUiModule, FormsModule, ReactiveFormsModule],
declarations: [UserInviteToTenantDialogComponent],
exports: [UserInviteToTenantDialogComponent]
})
export class UserInviteToTenantDialogModule {
constructor() { }
}

View File

@ -6,6 +6,11 @@
<app-navigation-breadcrumb />
</div>
<div class="col-auto">
<button mat-raised-button class="create-btn" (click)="invite()" *ngIf="authService.hasPermission(authService.permissionEnum.ExportUsers)">
{{'USER-LISTING.ACTIONS.INVITE' | translate}}
</button>
</div>
<div class="col-auto">
<button mat-raised-button class="create-btn" (click)="export()" *ngIf="authService.hasPermission(authService.permissionEnum.ExportUsers)">
<mat-icon>download</mat-icon>

View File

@ -25,6 +25,7 @@ import { Observable } from 'rxjs';
import { takeUntil } from 'rxjs/operators';
import { nameof } from 'ts-simple-nameof';
import { RouterUtilsService } from '@app/core/services/router/router-utils.service';
import { UserInviteToTenantDialogComponent } from './user-invite-to-tenant-dialog/user-invite-to-tenant-dialog.component';
@Component({
templateUrl: './user-listing.component.html',
@ -216,4 +217,17 @@ export class UserListingComponent extends BaseListingComponent<User, UserLookup>
public setDefaultAvatar(ev: Event) {
(ev.target as HTMLImageElement).src = 'assets/images/profile-placeholder.png';
}
//
// Invite
//
invite() {
const dialogRef = this.dialog.open(UserInviteToTenantDialogComponent, {
autoFocus: false,
restoreFocus: false,
});
}
}

View File

@ -11,6 +11,7 @@ import { UserListingComponent } from './listing/user-listing.component';
import { UsersRoutingModule } from './user.routing';
import { UserRoleEditorComponent } from './listing/role-editor/user-role-editor.component';
import { AutoCompleteModule } from '@app/library/auto-complete/auto-complete.module';
import { UserInviteToTenantDialogModule } from './listing/user-invite-to-tenant-dialog/user-invite-to-tenant-dialog.module';
@NgModule({
declarations: [
@ -28,7 +29,8 @@ import { AutoCompleteModule } from '@app/library/auto-complete/auto-complete.mod
HybridListingModule,
AutoCompleteModule,
TextFilterModule,
UserSettingsModule
UserSettingsModule,
UserInviteToTenantDialogModule
]
})
export class UsersModule { }

View File

@ -6,6 +6,7 @@ import { CommonFormsModule } from '@common/forms/common-forms.module';
import { CommonUiModule } from '@common/ui/common-ui.module';
import { MergeEmailConfirmation } from './merge-email-confirmation/merge-email-confirmation.component';
import { UnlinkEmailConfirmation } from './unlink-email-confirmation/unlink-email-confirmation.component';
import { UserInviteConfirmation } from './user-invite-confirmation/user-invite-confirmation.component';
@NgModule({
imports: [
@ -16,7 +17,8 @@ import { UnlinkEmailConfirmation } from './unlink-email-confirmation/unlink-emai
declarations: [
LoginComponent,
MergeEmailConfirmation,
UnlinkEmailConfirmation
UnlinkEmailConfirmation,
UserInviteConfirmation
],
exports: [
LoginComponent

View File

@ -4,6 +4,7 @@ import { LoginComponent } from './login.component';
import { MergeEmailConfirmation } from './merge-email-confirmation/merge-email-confirmation.component';
import { UnlinkEmailConfirmation } from './unlink-email-confirmation/unlink-email-confirmation.component';
import { AuthGuard } from '@app/core/auth-guard.service';
import { UserInviteConfirmation } from './user-invite-confirmation/user-invite-confirmation.component';
// import { PostLoginComponent } from './post-login/post-login.component';
const routes: Routes = [
@ -18,6 +19,8 @@ const routes: Routes = [
// component: PostLoginComponent
// },
{ path: 'unlink/confirmation/:token', component: UnlinkEmailConfirmation },
{ path: 'invitation/confirmation/:token', component: UserInviteConfirmation },
];
@NgModule({

View File

@ -0,0 +1,21 @@
<div class="user-invite">
<div class="container-fluid">
<div class="row">
<div class="col user-invite-title">{{'USER-INVITE.TITLE' | translate}}</div>
</div>
<div *ngIf="showForm" class="row user-invite-content">
<div class="col">
<div class="row justify-content-center">
<div class="col-auto">
<span>
{{ 'USER-INVITE.IN-PROGRESS' | translate }}
</span>
</div>
</div>
</div>
</div>
<ng-template #loading>
</ng-template>
</div>
</div>

View File

@ -0,0 +1,19 @@
.user-invite {
height: fit-content;
//margin-top: 80px;
min-height: 100vh;
background-color: #ffffff;
}
.user-invite-title {
font-size: 1.25rem;
color: #212121;
padding-top: 4.1875rem;
padding-left: 3.75rem;
padding-bottom: 4.0625rem;
}
.user-invite-content {
margin-left: 9rem;
margin-right: 11rem;
}

View File

@ -0,0 +1,78 @@
import { HttpErrorResponse } from '@angular/common/http';
import { Component, OnInit } from '@angular/core';
import { ActivatedRoute, Router } from '@angular/router';
import { RouterUtilsService } from '@app/core/services/router/router-utils.service';
import { UserService } from '@app/core/services/user/user.service';
import { BaseComponent } from '@common/base/base.component';
import { HttpError, HttpErrorHandlingService } from '@common/modules/errors/error-handling/http-error-handling.service';
import { Guid } from '@common/types/guid';
import { TranslateService } from '@ngx-translate/core';
import { takeUntil } from "rxjs/operators";
@Component({
selector: 'app-user-invite-confirmation-component',
templateUrl: './user-invite-confirmation.component.html'
})
export class UserInviteConfirmation extends BaseComponent implements OnInit {
private token: Guid;
get showForm(): boolean {
return this.token != null;
}
constructor(
private userService: UserService,
private route: ActivatedRoute,
private router: Router,
private language: TranslateService,
private routerUtils: RouterUtilsService,
private httpErrorHandlingService: HttpErrorHandlingService
) { super(); }
ngOnInit() {
this.route.params
.pipe(takeUntil(this._destroyed))
.subscribe(params => {
const token = params['token']
if (token != null) {
this.token = token;
this.onConfirm();
}
});
}
onConfirm(): void {
if (this.showForm === false) return;
this.userService.confirmInviteUser(this.token)
.subscribe(result => {
if (result) {
this.onCallbackConfirmationSuccess();
}
},
error => this.onCallbackError(error));
}
onCallbackConfirmationSuccess() {
this.router.navigate([this.routerUtils.generateUrl('home')])
.then(() => {
localStorage.setItem('refreshPage', null);
localStorage.setItem('refreshPage', 'true');
window.location.reload();
});
}
onCallbackError(errorResponse: HttpErrorResponse) {
let errorOverrides = new Map<number, string>();
errorOverrides.set(302, this.language.instant('EMAIL-CONFIRMATION.EMAIL-FOUND'));
errorOverrides.set(-1, this.language.instant('EMAIL-CONFIRMATION.EXPIRED-EMAIL'));
this.httpErrorHandlingService.handleBackedRequestError(errorResponse, errorOverrides)
const error: HttpError = this.httpErrorHandlingService.getError(errorResponse);
if (error.statusCode === 302) {
this.router.navigate([this.routerUtils.generateUrl('home')]);
}
}
}

View File

@ -1768,7 +1768,8 @@
"ACTIONS": {
"EDIT": "Edit",
"SAVE": "Save",
"EXPORT": "Export users"
"EXPORT": "Export users",
"INVITE": "Invite users"
}
},
"REFERENCE-FIELD-COMPONENT": {
@ -2100,6 +2101,22 @@
"USER-PROFILE-SETTINGS": "Nire Profilaren Ezarpenak...",
"LOG-OUT": "Saioa itxi"
},
"USER-INVITE": {
"IN-PROGRESS": "User Invitation in progress..",
"TITLE": "User Invitation"
},
"USER-INVITE-TO-TENANT-DIALOG": {
"TITLE": "Invite Users",
"FIELDS": {
"EMAIL": "Email",
"ROLES": "Roles"
},
"ACTIONS": {
"REMOVE-USER":"Remove User",
"INVITE":"Invite"
},
"USERS-REQUIRED": "Required at leat on user"
},
"USER-PROFILE": {
"MERGING-EMAILS-DIALOG": {
"TITLE": "Lotutako kontua egiaztatu",

View File

@ -1768,7 +1768,8 @@
"ACTIONS": {
"EDIT": "Edit",
"SAVE": "Save",
"EXPORT": "Export users"
"EXPORT": "Export users",
"INVITE": "Invite users"
}
},
"REFERENCE-FIELD-COMPONENT": {
@ -2100,6 +2101,22 @@
"USER-PROFILE-SETTINGS": "My Profile Settings...",
"LOG-OUT": "Abmeldung"
},
"USER-INVITE": {
"IN-PROGRESS": "User Invitation in progress..",
"TITLE": "User Invitation"
},
"USER-INVITE-TO-TENANT-DIALOG": {
"TITLE": "Invite Users",
"FIELDS": {
"EMAIL": "Email",
"ROLES": "Roles"
},
"ACTIONS": {
"REMOVE-USER":"Remove User",
"INVITE":"Invite"
},
"USERS-REQUIRED": "Required at leat on user"
},
"USER-PROFILE": {
"MERGING-EMAILS-DIALOG": {
"TITLE": "Verify linked account",

View File

@ -1768,7 +1768,8 @@
"ACTIONS": {
"EDIT": "Edit",
"SAVE": "Save",
"EXPORT": "Export users"
"EXPORT": "Export users",
"INVITE": "Invite users"
}
},
"REFERENCE-FIELD-COMPONENT": {
@ -2100,6 +2101,22 @@
"USER-PROFILE-SETTINGS": "My Profile Settings...",
"LOG-OUT": "Log Out"
},
"USER-INVITE": {
"IN-PROGRESS": "User Invitation in progress..",
"TITLE": "User Invitation"
},
"USER-INVITE-TO-TENANT-DIALOG": {
"TITLE": "Invite Users",
"FIELDS": {
"EMAIL": "Email",
"ROLES": "Roles"
},
"ACTIONS": {
"REMOVE-USER":"Remove User",
"INVITE":"Invite"
},
"USERS-REQUIRED": "Required at leat on user"
},
"USER-PROFILE": {
"MERGING-EMAILS-DIALOG": {
"TITLE": "Verify linked account",

View File

@ -1768,7 +1768,8 @@
"ACTIONS": {
"EDIT": "Edit",
"SAVE": "Save",
"EXPORT": "Export users"
"EXPORT": "Export users",
"INVITE": "Invite users"
}
},
"REFERENCE-FIELD-COMPONENT": {
@ -2100,6 +2101,22 @@
"USER-PROFILE-SETTINGS": "Configuración de mi perfil...",
"LOG-OUT": "Cerrar la sesión"
},
"USER-INVITE": {
"IN-PROGRESS": "User Invitation in progress..",
"TITLE": "User Invitation"
},
"USER-INVITE-TO-TENANT-DIALOG": {
"TITLE": "Invite Users",
"FIELDS": {
"EMAIL": "Email",
"ROLES": "Roles"
},
"ACTIONS": {
"REMOVE-USER":"Remove User",
"INVITE":"Invite"
},
"USERS-REQUIRED": "Required at leat on user"
},
"USER-PROFILE": {
"MERGING-EMAILS-DIALOG": {
"TITLE": "Verify linked account",

View File

@ -1768,7 +1768,8 @@
"ACTIONS": {
"EDIT": "Edit",
"SAVE": "Save",
"EXPORT": "Export users"
"EXPORT": "Export users",
"INVITE": "Invite users"
}
},
"REFERENCE-FIELD-COMPONENT": {
@ -2100,6 +2101,22 @@
"USER-PROFILE-SETTINGS": "Οι ρυθμίσεις του προφίλ μου...",
"LOG-OUT": "Αποσύνδεση"
},
"USER-INVITE": {
"IN-PROGRESS": "User Invitation in progress..",
"TITLE": "User Invitation"
},
"USER-INVITE-TO-TENANT-DIALOG": {
"TITLE": "Invite Users",
"FIELDS": {
"EMAIL": "Email",
"ROLES": "Roles"
},
"ACTIONS": {
"REMOVE-USER":"Remove User",
"INVITE":"Invite"
},
"USERS-REQUIRED": "Required at leat on user"
},
"USER-PROFILE": {
"MERGING-EMAILS-DIALOG": {
"TITLE": "Verify linked account",

View File

@ -1768,7 +1768,8 @@
"ACTIONS": {
"EDIT": "Edit",
"SAVE": "Save",
"EXPORT": "Export users"
"EXPORT": "Export users",
"INVITE": "Invite users"
}
},
"REFERENCE-FIELD-COMPONENT": {
@ -2100,6 +2101,22 @@
"USER-PROFILE-SETTINGS": "Postavke profila...",
"LOG-OUT": "Odjava"
},
"USER-INVITE": {
"IN-PROGRESS": "User Invitation in progress..",
"TITLE": "User Invitation"
},
"USER-INVITE-TO-TENANT-DIALOG": {
"TITLE": "Invite Users",
"FIELDS": {
"EMAIL": "Email",
"ROLES": "Roles"
},
"ACTIONS": {
"REMOVE-USER":"Remove User",
"INVITE":"Invite"
},
"USERS-REQUIRED": "Required at leat on user"
},
"USER-PROFILE": {
"MERGING-EMAILS-DIALOG": {
"TITLE": "Potvrdi povezani korisnički račun",

View File

@ -1768,7 +1768,8 @@
"ACTIONS": {
"EDIT": "Edit",
"SAVE": "Save",
"EXPORT": "Export users"
"EXPORT": "Export users",
"INVITE": "Invite users"
}
},
"REFERENCE-FIELD-COMPONENT": {
@ -2100,6 +2101,22 @@
"USER-PROFILE-SETTINGS": "Ustawienia mojego Profilu...",
"LOG-OUT": "Wyloguj"
},
"USER-INVITE": {
"IN-PROGRESS": "User Invitation in progress..",
"TITLE": "User Invitation"
},
"USER-INVITE-TO-TENANT-DIALOG": {
"TITLE": "Invite Users",
"FIELDS": {
"EMAIL": "Email",
"ROLES": "Roles"
},
"ACTIONS": {
"REMOVE-USER":"Remove User",
"INVITE":"Invite"
},
"USERS-REQUIRED": "Required at leat on user"
},
"USER-PROFILE": {
"MERGING-EMAILS-DIALOG": {
"TITLE": "Zweryfikuj połączone konto",

View File

@ -1768,7 +1768,8 @@
"ACTIONS": {
"EDIT": "Edit",
"SAVE": "Save",
"EXPORT": "Export users"
"EXPORT": "Export users",
"INVITE": "Invite users"
}
},
"REFERENCE-FIELD-COMPONENT": {
@ -2100,6 +2101,22 @@
"USER-PROFILE-SETTINGS": "Definições do Meu Perfil...",
"LOG-OUT": "Terminar Sessão"
},
"USER-INVITE": {
"IN-PROGRESS": "User Invitation in progress..",
"TITLE": "User Invitation"
},
"USER-INVITE-TO-TENANT-DIALOG": {
"TITLE": "Invite Users",
"FIELDS": {
"EMAIL": "Email",
"ROLES": "Roles"
},
"ACTIONS": {
"REMOVE-USER":"Remove User",
"INVITE":"Invite"
},
"USERS-REQUIRED": "Required at leat on user"
},
"USER-PROFILE": {
"MERGING-EMAILS-DIALOG": {
"TITLE": "Verify linked account",

View File

@ -1768,7 +1768,8 @@
"ACTIONS": {
"EDIT": "Edit",
"SAVE": "Save",
"EXPORT": "Export users"
"EXPORT": "Export users",
"INVITE": "Invite users"
}
},
"REFERENCE-FIELD-COMPONENT": {
@ -2100,6 +2101,22 @@
"USER-PROFILE-SETTINGS": "Nastavenie profilu...",
"LOG-OUT": "Odhlásiť sa"
},
"USER-INVITE": {
"IN-PROGRESS": "User Invitation in progress..",
"TITLE": "User Invitation"
},
"USER-INVITE-TO-TENANT-DIALOG": {
"TITLE": "Invite Users",
"FIELDS": {
"EMAIL": "Email",
"ROLES": "Roles"
},
"ACTIONS": {
"REMOVE-USER":"Remove User",
"INVITE":"Invite"
},
"USERS-REQUIRED": "Required at leat on user"
},
"USER-PROFILE": {
"MERGING-EMAILS-DIALOG": {
"TITLE": "Verify linked account",

View File

@ -1768,7 +1768,8 @@
"ACTIONS": {
"EDIT": "Edit",
"SAVE": "Save",
"EXPORT": "Export users"
"EXPORT": "Export users",
"INVITE": "Invite users"
}
},
"REFERENCE-FIELD-COMPONENT": {
@ -2100,6 +2101,22 @@
"USER-PROFILE-SETTINGS": "Podešavanja mog profila...",
"LOG-OUT": "Odjavite se"
},
"USER-INVITE": {
"IN-PROGRESS": "User Invitation in progress..",
"TITLE": "User Invitation"
},
"USER-INVITE-TO-TENANT-DIALOG": {
"TITLE": "Invite Users",
"FIELDS": {
"EMAIL": "Email",
"ROLES": "Roles"
},
"ACTIONS": {
"REMOVE-USER":"Remove User",
"INVITE":"Invite"
},
"USERS-REQUIRED": "Required at leat on user"
},
"USER-PROFILE": {
"MERGING-EMAILS-DIALOG": {
"TITLE": "Verify linked account",

View File

@ -1768,7 +1768,8 @@
"ACTIONS": {
"EDIT": "Edit",
"SAVE": "Save",
"EXPORT": "Export users"
"EXPORT": "Export users",
"INVITE": "Invite users"
}
},
"REFERENCE-FIELD-COMPONENT": {
@ -2100,6 +2101,22 @@
"USER-PROFILE-SETTINGS": "Profil Ayarlarım...",
"LOG-OUT": ıkış Yap"
},
"USER-INVITE": {
"IN-PROGRESS": "User Invitation in progress..",
"TITLE": "User Invitation"
},
"USER-INVITE-TO-TENANT-DIALOG": {
"TITLE": "Invite Users",
"FIELDS": {
"EMAIL": "Email",
"ROLES": "Roles"
},
"ACTIONS": {
"REMOVE-USER":"Remove User",
"INVITE":"Invite"
},
"USERS-REQUIRED": "Required at leat on user"
},
"USER-PROFILE": {
"MERGING-EMAILS-DIALOG": {
"TITLE": "Verify linked account",

View File

@ -0,0 +1,19 @@
# stage1 as builder
FROM node:20-alpine AS builder
# copy the package.json to install dependencies
COPY package.json ./
# Install the dependencies and make the folder
RUN npm install --legacy-peer-deps && mkdir /src && mv ./node_modules ./src
WORKDIR /src
COPY . .
# Build the project and copy the files
RUN npm run build
#RUN node --max_old_space_size=8192 node_modules/@angular/cli/bin/ng build -- --deploy-url=/ --prod
EXPOSE 3000
## Run the production server.
CMD ["npm", "run", "serve", "--", "--host", "0.0.0.0", "--no-open"]

View File

@ -1,35 +0,0 @@
events {
worker_connections 4096; ## Default: 1024
}
http {
include ./mime.types;
server {
listen 4200;
sendfile on;
gzip on;
gzip_http_version 1.1;
gzip_disable "MSIE [1-6]\.";
gzip_min_length 1100;
gzip_vary on;
gzip_proxied expired no-cache no-store private auth;
gzip_types text/plain text/css application/json application/javascript application/x-javascript text/xml application/xml application/xml+rss text/javascript;
gzip_comp_level 9;
location / {
root /usr/share/nginx/html;
try_files $uri $uri/ /index.html =404;
add_header Cache-Control "no-store, no-cache, must-revalidate";
}
}
}

View File

@ -1,8 +0,0 @@
#!/bin/bash
if [[ ! -z "${WEBAPP_BASE_URL}" ]]; then
rm /usr/share/nginx/html/index.html
cp /usr/share/nginx/html/index_base.html /usr/share/nginx/html/index.html
find '/usr/share/nginx/html' -name 'index.html' -exec sed -i -e 's,<base href="/">,<base href="'"$WEBAPP_BASE_URL"'">,g' {} \;
fi
nginx -g "daemon off;"