feat: rehauled console (#3525)

* new console

* move npm ci to angular build

* rel path for assets

* local grpc copy

* login policy, rm clear views, features rel path

* lock

Co-authored-by: Livio Amstutz <[email protected]>
This commit is contained in:
Max Peintner
2022-04-28 10:35:02 +00:00
committed by GitHub
co-authored by Livio Amstutz
parent 00f7dbe875
commit 08ae39ae19
668 changed files with 47746 additions and 19117 deletions
@@ -1,34 +1,45 @@
<cnsl-detail-layout [backRouterLink]="[ '/users/list/machines']" title="{{ 'USER.CREATE.TITLE' | translate }}"
description="{{ 'USER.CREATE.DESCRIPTION' | translate }}">
<mat-progress-bar *ngIf="loading" color="primary" mode="indeterminate"></mat-progress-bar>
<div class="max-width-container">
<div class="enlarged-container">
<div class="abort-container">
<a [routerLink]="[ '/users']" mat-icon-button>
<mat-icon>close</mat-icon>
</a>
<h1 class="abort">{{ 'USER.CREATE.TITLE' | translate }}</h1><span class="abort-2">Step
1 of 1</span>
</div>
<mat-progress-bar *ngIf="loading" color="primary" mode="indeterminate"></mat-progress-bar>
<form *ngIf="userForm" [formGroup]="userForm" (ngSubmit)="createUser()" class="form">
<div class="content">
<cnsl-form-field class="formfield">
<cnsl-label>{{ 'USER.MACHINE.USERNAME' | translate }}*</cnsl-label>
<input cnslInput formControlName="userName" required />
<span cnslError *ngIf="userName?.invalid && userName?.errors?.required">
{{ 'USER.VALIDATION.REQUIRED' | translate }}
</span>
<span cnslError *ngIf="userName?.invalid && userName?.errors?.noEmailValidator">
{{ 'USER.VALIDATION.NOEMAIL' | translate }}
</span>
</cnsl-form-field>
<cnsl-form-field class="formfield">
<cnsl-label>{{ 'USER.MACHINE.NAME' | translate }}*</cnsl-label>
<input cnslInput formControlName="name" required />
<span cnslError *ngIf="name?.invalid && name?.errors?.required">
{{ 'USER.VALIDATION.REQUIRED' | translate }}
</span>
</cnsl-form-field>
<cnsl-form-field class="formfield">
<cnsl-label>{{ 'USER.MACHINE.DESCRIPTION' | translate }}</cnsl-label>
<input cnslInput formControlName="description" />
</cnsl-form-field>
<div class="machine-create-main-content">
<form *ngIf="userForm" [formGroup]="userForm" (ngSubmit)="createUser()" class="machine-create-form">
<div class="machine-create-content">
<cnsl-form-field class="formfield">
<cnsl-label>{{ 'USER.MACHINE.USERNAME' | translate }}*</cnsl-label>
<input cnslInput formControlName="userName" required />
<span cnslError *ngIf="userName?.invalid && userName?.errors?.required">
{{ 'USER.VALIDATION.REQUIRED' | translate }}
</span>
<span cnslError *ngIf="userName?.invalid && userName?.errors?.noEmailValidator">
{{ 'USER.VALIDATION.NOEMAIL' | translate }}
</span>
</cnsl-form-field>
<cnsl-form-field class="formfield">
<cnsl-label>{{ 'USER.MACHINE.NAME' | translate }}*</cnsl-label>
<input cnslInput formControlName="name" required />
<span cnslError *ngIf="name?.invalid && name?.errors?.required">
{{ 'USER.VALIDATION.REQUIRED' | translate }}
</span>
</cnsl-form-field>
<cnsl-form-field class="formfield">
<cnsl-label>{{ 'USER.MACHINE.DESCRIPTION' | translate }}</cnsl-label>
<input cnslInput formControlName="description" />
</cnsl-form-field>
</div>
<div class="machine-btn-container">
<button color="primary" [attr.data-e2e]="'create-button'" [disabled]="userForm.invalid" type="submit"
mat-raised-button>{{ 'ACTIONS.CREATE' |
translate }}</button>
</div>
</form>
</div>
<div class="btn-container">
<button color="primary" [disabled]="userForm.invalid" type="submit" mat-raised-button>{{ 'ACTIONS.CREATE' |
translate }}</button>
</div>
</form>
</cnsl-detail-layout>
</div>
</div>
@@ -1,34 +1,48 @@
.abort-container {
display: flex;
align-items: center;
margin-bottom: 2rem;
.form {
width: 100%;
padding-top: 1rem;
.abort {
font-size: 1.2rem;
margin-left: 2rem;
}
.btn-container {
button {
margin-top: 3rem;
display: block;
padding: .5rem 4rem;
border-radius: .5rem;
.abort-2 {
font-size: 1.2rem;
margin-left: 2rem;
white-space: nowrap;
}
}
.machine-create-main-content {
padding-left: 4.5rem;
max-width: 35rem;
.machine-create-form {
width: 100%;
padding-top: 1rem;
.machine-btn-container {
button {
margin-top: 3rem;
display: block;
padding: 0.5rem 4rem;
border-radius: 0.5rem;
}
}
}
.machine-create-content {
width: 100%;
display: flex;
flex-wrap: wrap;
flex-direction: row;
margin: 0 -0.5rem;
.formfield {
flex: 1 0 33%;
margin: 0 0.5rem;
}
}
}
.content {
width: 100%;
display: flex;
flex-wrap: wrap;
flex-direction: row;
margin: 0 -.5rem;
.section {
padding: .5rem;
flex-basis: 100%;
color: var(--grey);
font-size: .9rem;
}
.formfield {
flex: 1 0 33%;
margin: 0 .5rem;
}
}
@@ -3,6 +3,7 @@ import { AbstractControl, FormBuilder, FormGroup, Validators } from '@angular/fo
import { Router } from '@angular/router';
import { Subscription } from 'rxjs';
import { AddMachineUserRequest } from 'src/app/proto/generated/zitadel/management_pb';
import { Breadcrumb, BreadcrumbService, BreadcrumbType } from 'src/app/services/breadcrumb.service';
import { ManagementService } from 'src/app/services/mgmt.service';
import { ToastService } from 'src/app/services/toast.service';
@@ -23,18 +24,25 @@ export class UserCreateMachineComponent implements OnDestroy {
private toast: ToastService,
public userService: ManagementService,
private fb: FormBuilder,
breadcrumbService: BreadcrumbService,
) {
breadcrumbService.setBreadcrumb([
new Breadcrumb({
type: BreadcrumbType.IAM,
name: 'IAM',
routerLink: ['/system'],
}),
new Breadcrumb({
type: BreadcrumbType.ORG,
routerLink: ['/org'],
}),
]);
this.initForm();
}
private initForm(): void {
this.userForm = this.fb.group({
userName: ['',
[
Validators.required,
Validators.minLength(2),
],
],
userName: ['', [Validators.required, Validators.minLength(2)]],
name: ['', [Validators.required]],
description: ['', []],
});
@@ -1,137 +1,149 @@
<cnsl-detail-layout [backRouterLink]="[ '/users/list/humans']" title="{{ 'USER.CREATE.TITLE' | translate }}"
description="{{ 'USER.CREATE.DESCRIPTION' | translate }}">
<mat-progress-bar *ngIf="loading" color="primary" mode="indeterminate"></mat-progress-bar>
<form *ngIf="userForm" [formGroup]="userForm" (ngSubmit)="createUser()" class="form">
<div class="content">
<p class="section">{{ 'USER.CREATE.NAMEANDEMAILSECTION' | translate }}</p>
<cnsl-form-field class="formfield">
<cnsl-label>{{ 'USER.PROFILE.EMAIL' | translate }}*</cnsl-label>
<input cnslInput matRipple formControlName="email" required />
<span cnslError *ngIf="email?.invalid && !email?.errors?.required">
{{ 'USER.VALIDATION.NOTANEMAIL' | translate }}
</span>
<span cnslError *ngIf="email?.invalid && email?.errors?.required">
{{ 'USER.VALIDATION.REQUIRED' | translate }}
</span>
</cnsl-form-field>
<cnsl-form-field class="formfield">
<cnsl-label>{{ 'USER.PROFILE.USERNAME' | translate }}*</cnsl-label>
<input cnslInput formControlName="userName" required
[ngStyle]="{'padding-right': suffixPadding ? suffixPadding : '10px'}" />
<span #suffix *ngIf="envSuffixLabel" cnslSuffix>{{envSuffixLabel}}</span>
<span cnslError *ngIf="userName?.invalid && userName?.errors?.required">
{{ 'USER.VALIDATION.REQUIRED' | translate }}
</span>
<span cnslError *ngIf="userName?.invalid && userName?.errors?.noEmailValidator">
{{ 'USER.VALIDATION.NOEMAIL' | translate }}
</span>
</cnsl-form-field>
<div class="max-width-container">
<div class="enlarged-container">
<div class="abort-container">
<a [routerLink]="[ '/users']" mat-icon-button>
<mat-icon>close</mat-icon>
</a>
<h1 class="abort">{{ 'USER.CREATE.TITLE' | translate }}</h1><span class="abort-2">Step
1 of 1</span>
</div>
<div class="content">
<cnsl-form-field class="formfield">
<cnsl-label>{{ 'USER.PROFILE.FIRSTNAME' | translate }}*</cnsl-label>
<input cnslInput formControlName="firstName" required />
<span cnslError *ngIf="firstName?.invalid && firstName?.errors?.required">
{{ 'USER.VALIDATION.REQUIRED' | translate }}
</span>
</cnsl-form-field>
<cnsl-form-field class="formfield">
<cnsl-label>{{ 'USER.PROFILE.LASTNAME' | translate }}*</cnsl-label>
<input cnslInput formControlName="lastName" required />
<span cnslError *ngIf="lastName?.invalid && lastName?.errors?.required">
{{ 'USER.VALIDATION.REQUIRED' | translate }}
</span>
</cnsl-form-field>
<cnsl-form-field class="formfield">
<cnsl-label>{{ 'USER.PROFILE.NICKNAME' | translate }}</cnsl-label>
<input cnslInput formControlName="nickName" />
<span cnslError *ngIf="nickName?.invalid && nickName?.errors?.required">
{{ 'USER.VALIDATION.REQUIRED' | translate }}
</span>
</cnsl-form-field>
<div class="email-is-verified">
<mat-checkbox class="block-checkbox" formControlName="isVerified">
{{'USER.LOGINMETHODS.EMAIL.ISVERIFIED' | translate}}
</mat-checkbox>
<mat-checkbox class="block-checkbox" [(ngModel)]="usePassword" [ngModelOptions]="{standalone: true}">
{{'ORG.PAGES.USEPASSWORD' | translate}}
</mat-checkbox>
<cnsl-info-section class="full-width desc">
<span>{{'USER.CREATE.INITMAILDESCRIPTION' | translate}}</span>
</cnsl-info-section>
</div>
<div class="user-create-main-content">
<mat-progress-bar *ngIf="loading" color="primary" mode="indeterminate"></mat-progress-bar>
<div class="pwd-section" *ngIf="usePassword && pwdForm">
<cnsl-password-complexity-view class="complexity-view" [policy]="this.policy" [password]="password">
</cnsl-password-complexity-view>
<form [formGroup]="pwdForm" class="user-create-pwd-form">
<cnsl-form-field class="pwd-field" *ngIf="password" appearance="outline">
<cnsl-label>{{ 'USER.PASSWORD.NEWINITIAL' | translate }}</cnsl-label>
<input cnslInput autocomplete="off" name="firstpassword" formControlName="password" type="password" />
<span cnslError *ngIf="password?.errors?.required">
<form *ngIf="userForm" [formGroup]="userForm" (ngSubmit)="createUser()" class="user-create-form">
<div class="user-create-content">
<p class="user-create-section cnsl-secondary-text">{{ 'USER.CREATE.NAMEANDEMAILSECTION' | translate }}</p>
<cnsl-form-field class="formfield">
<cnsl-label>{{ 'USER.PROFILE.EMAIL' | translate }}*</cnsl-label>
<input cnslInput matRipple formControlName="email" required />
<span cnslError *ngIf="email?.invalid && !email?.errors?.required">
{{ 'USER.VALIDATION.NOTANEMAIL' | translate }}
</span>
<span cnslError *ngIf="email?.invalid && email?.errors?.required">
{{ 'USER.VALIDATION.REQUIRED' | translate }}
</span>
</cnsl-form-field>
<cnsl-form-field class="pwd-field" *ngIf="confirmPassword" appearance="outline">
<cnsl-label>{{ 'USER.PASSWORD.CONFIRMINITIAL' | translate }}</cnsl-label>
<input cnslInput autocomplete="off" name="confirmPassword" formControlName="confirmPassword"
type="password" />
<cnsl-form-field class="formfield">
<cnsl-label>{{ 'USER.PROFILE.USERNAME' | translate }}*</cnsl-label>
<input cnslInput formControlName="userName" required
[ngStyle]="{'padding-right': suffixPadding ? suffixPadding : '10px'}" />
<span #suffix *ngIf="envSuffixLabel" cnslSuffix>{{envSuffixLabel}}</span>
<span cnslError *ngIf="confirmPassword?.errors?.required">
<span cnslError *ngIf="userName?.invalid && userName?.errors?.required">
{{ 'USER.VALIDATION.REQUIRED' | translate }}
</span>
<span cnslError *ngIf="confirmPassword?.errors?.notequal">
{{ 'USER.PASSWORD.NOTEQUAL' | translate }}
<span cnslError *ngIf="userName?.invalid && userName?.errors?.noEmailValidator">
{{ 'USER.VALIDATION.NOEMAIL' | translate }}
</span>
</cnsl-form-field>
</div>
<div class="user-create-content">
<cnsl-form-field class="formfield">
<cnsl-label>{{ 'USER.PROFILE.FIRSTNAME' | translate }}*</cnsl-label>
<input cnslInput formControlName="firstName" required />
<span cnslError *ngIf="firstName?.invalid && firstName?.errors?.required">
{{ 'USER.VALIDATION.REQUIRED' | translate }}
</span>
</cnsl-form-field>
<cnsl-form-field class="formfield">
<cnsl-label>{{ 'USER.PROFILE.LASTNAME' | translate }}*</cnsl-label>
<input cnslInput formControlName="lastName" required />
<span cnslError *ngIf="lastName?.invalid && lastName?.errors?.required">
{{ 'USER.VALIDATION.REQUIRED' | translate }}
</span>
</cnsl-form-field>
<cnsl-form-field class="formfield">
<cnsl-label>{{ 'USER.PROFILE.NICKNAME' | translate }}</cnsl-label>
<input cnslInput formControlName="nickName" />
<span cnslError *ngIf="nickName?.invalid && nickName?.errors?.required">
{{ 'USER.VALIDATION.REQUIRED' | translate }}
</span>
</cnsl-form-field>
</form>
</div>
<p class="section">{{ 'USER.CREATE.GENDERLANGSECTION' | translate }}</p>
<div class="email-is-verified">
<mat-checkbox class="block-checkbox" formControlName="isVerified">
{{'USER.LOGINMETHODS.EMAIL.ISVERIFIED' | translate}}
</mat-checkbox>
<mat-checkbox class="block-checkbox" [(ngModel)]="usePassword" [ngModelOptions]="{standalone: true}">
{{'ORG.PAGES.USEPASSWORD' | translate}}
</mat-checkbox>
<cnsl-info-section class="full-width desc">
<span>{{'USER.CREATE.INITMAILDESCRIPTION' | translate}}</span>
</cnsl-info-section>
</div>
<cnsl-form-field class="formfield">
<cnsl-label>{{ 'USER.PROFILE.GENDER' | translate }}</cnsl-label>
<mat-select formControlName="gender">
<mat-option *ngFor="let gender of genders" [value]="gender">
{{ 'GENDERS.'+gender | translate }}
</mat-option>
</mat-select>
<span cnslError *ngIf="gender?.invalid && gender?.errors?.required">
{{ 'USER.VALIDATION.REQUIRED' | translate }}
</span>
</cnsl-form-field>
<cnsl-form-field class="formfield">
<cnsl-label>{{ 'USER.PROFILE.PREFERRED_LANGUAGE' | translate }}</cnsl-label>
<mat-select formControlName="preferredLanguage">
<mat-option *ngFor="let language of languages" [value]="language">
{{ 'LANGUAGES.'+language | translate }}
</mat-option>
<span cnslError *ngIf="preferredLanguage?.invalid && preferredLanguage?.errors?.required">
{{ 'USER.VALIDATION.REQUIRED' | translate }}
</span>
</mat-select>
</cnsl-form-field>
<div class="pwd-section" *ngIf="usePassword && pwdForm">
<cnsl-password-complexity-view class="complexity-view" [policy]="this.policy" [password]="password">
</cnsl-password-complexity-view>
<p class="section">{{ 'USER.CREATE.ADDRESSANDPHONESECTION' | translate }}</p>
<form [formGroup]="pwdForm" class="user-create-pwd-form">
<cnsl-form-field class="pwd-field" *ngIf="password" appearance="outline">
<cnsl-label>{{ 'USER.PASSWORD.NEWINITIAL' | translate }}</cnsl-label>
<input cnslInput autocomplete="off" name="firstpassword" formControlName="password" type="password" />
<cnsl-form-field class="formfield">
<cnsl-label>{{ 'USER.PROFILE.PHONE' | translate }}</cnsl-label>
<input cnslInput formControlName="phone" />
<span cnslError *ngIf="phone?.invalid && phone?.errors?.required">
{{ 'USER.VALIDATION.REQUIRED' | translate }}
</span>
</cnsl-form-field>
<span cnslError *ngIf="password?.errors?.required">
{{ 'USER.VALIDATION.REQUIRED' | translate }}
</span>
</cnsl-form-field>
<cnsl-form-field class="pwd-field" *ngIf="confirmPassword" appearance="outline">
<cnsl-label>{{ 'USER.PASSWORD.CONFIRMINITIAL' | translate }}</cnsl-label>
<input cnslInput autocomplete="off" name="confirmPassword" formControlName="confirmPassword"
type="password" />
<span cnslError *ngIf="confirmPassword?.errors?.required">
{{ 'USER.VALIDATION.REQUIRED' | translate }}
</span>
<span cnslError *ngIf="confirmPassword?.errors?.notequal">
{{ 'USER.PASSWORD.NOTEQUAL' | translate }}
</span>
</cnsl-form-field>
</form>
</div>
<p class="user-create-section cnsl-secondary-text">{{ 'USER.CREATE.GENDERLANGSECTION' | translate }}</p>
<cnsl-form-field class="formfield">
<cnsl-label>{{ 'USER.PROFILE.GENDER' | translate }}</cnsl-label>
<mat-select formControlName="gender">
<mat-option *ngFor="let gender of genders" [value]="gender">
{{ 'GENDERS.'+gender | translate }}
</mat-option>
</mat-select>
<span cnslError *ngIf="gender?.invalid && gender?.errors?.required">
{{ 'USER.VALIDATION.REQUIRED' | translate }}
</span>
</cnsl-form-field>
<cnsl-form-field class="formfield">
<cnsl-label>{{ 'USER.PROFILE.PREFERRED_LANGUAGE' | translate }}</cnsl-label>
<mat-select formControlName="preferredLanguage">
<mat-option *ngFor="let language of languages" [value]="language">
{{ 'LANGUAGES.'+language | translate }}
</mat-option>
<span cnslError *ngIf="preferredLanguage?.invalid && preferredLanguage?.errors?.required">
{{ 'USER.VALIDATION.REQUIRED' | translate }}
</span>
</mat-select>
</cnsl-form-field>
<p class="user-create-section cnsl-secondary-text">{{ 'USER.CREATE.ADDRESSANDPHONESECTION' | translate }}</p>
<cnsl-form-field class="formfield">
<cnsl-label>{{ 'USER.PROFILE.PHONE' | translate }}</cnsl-label>
<input cnslInput formControlName="phone" />
<span cnslError *ngIf="phone?.invalid && phone?.errors?.required">
{{ 'USER.VALIDATION.REQUIRED' | translate }}
</span>
</cnsl-form-field>
</div>
<div class="user-create-btn-container">
<button [attr.data-e2e]="'create-button'" color="primary"
[disabled]="userForm.invalid || (this.usePassword && this.pwdForm.invalid)" type="submit"
mat-raised-button>{{ 'ACTIONS.CREATE' |
translate }}</button>
</div>
</form>
</div>
<div class="btn-container">
<button color="primary" [disabled]="userForm.invalid || (this.usePassword && this.pwdForm.invalid)" type="submit"
mat-raised-button>{{ 'ACTIONS.CREATE' |
translate }}</button>
</div>
</form>
</cnsl-detail-layout>
</div>
</div>
@@ -1,61 +1,80 @@
.form {
width: 100%;
padding-top: 1rem;
.btn-container {
button {
margin-top: 3rem;
display: block;
padding: 0.5rem 4rem;
border-radius: 0.5rem;
}
}
}
.content {
width: 100%;
.abort-container {
display: flex;
flex-wrap: wrap;
flex-direction: row;
margin: 0 -0.5rem;
align-items: center;
margin-bottom: 2rem;
.section {
padding: 0.5rem;
flex-basis: 100%;
color: var(--grey);
font-size: 0.9rem;
letter-spacing: 0.05em;
text-transform: uppercase;
.abort {
font-size: 1.2rem;
margin-left: 2rem;
}
.formfield {
flex: 1 0 33%;
margin: 0 0.5rem;
.abort-2 {
font-size: 1.2rem;
margin-left: 2rem;
white-space: nowrap;
}
}
.user-create-main-content {
padding-left: 4.5rem;
max-width: 35rem;
.user-create-form {
padding-top: 1rem;
.user-create-btn-container {
button {
margin-top: 3rem;
display: block;
padding: 0.5rem 4rem;
border-radius: 0.5rem;
}
}
}
.email-is-verified,
.use-password-block {
margin: 0 0.5rem;
flex-basis: 100%;
margin-top: 1.5rem;
.user-create-content {
display: flex;
flex-wrap: wrap;
flex-direction: row;
margin: 0 -0.5rem;
.block-checkbox {
display: block;
margin: 0.25rem 0;
.user-create-section {
padding: 0.5rem;
flex-basis: 100%;
font-size: 0.9rem;
letter-spacing: 0.05em;
text-transform: uppercase;
}
.formfield {
flex: 1 0 33%;
margin: 0 0.5rem;
}
.email-is-verified,
.use-password-block {
margin: 0 0.5rem;
flex-basis: 100%;
margin-top: 1.5rem;
.block-checkbox {
display: block;
margin: 0.25rem 0;
}
}
}
.pwd-section {
margin: 0 0.5rem;
.section {
padding: 0.5rem 0;
}
.user-create-pwd-form {
display: grid;
grid-template-columns: 1fr 1fr;
grid-gap: 1rem;
}
}
}
.pwd-section {
margin: 0 0.5rem;
.section {
padding: 0.5rem 0;
}
.user-create-pwd-form {
display: grid;
grid-template-columns: 1fr 1fr;
grid-gap: 1rem;
}
}
@@ -8,6 +8,7 @@ import { AddHumanUserRequest } from 'src/app/proto/generated/zitadel/management_
import { Domain } from 'src/app/proto/generated/zitadel/org_pb';
import { PasswordComplexityPolicy } from 'src/app/proto/generated/zitadel/policy_pb';
import { Gender } from 'src/app/proto/generated/zitadel/user_pb';
import { Breadcrumb, BreadcrumbService, BreadcrumbType } from 'src/app/services/breadcrumb.service';
import { ManagementService } from 'src/app/services/mgmt.service';
import { ToastService } from 'src/app/services/toast.service';
@@ -62,7 +63,20 @@ export class UserCreateComponent implements OnDestroy {
private fb: FormBuilder,
private mgmtService: ManagementService,
private changeDetRef: ChangeDetectorRef,
breadcrumbService: BreadcrumbService,
) {
breadcrumbService.setBreadcrumb([
new Breadcrumb({
type: BreadcrumbType.IAM,
name: 'IAM',
routerLink: ['/system'],
}),
new Breadcrumb({
type: BreadcrumbType.ORG,
routerLink: ['/org'],
}),
]);
this.loading = true;
this.loadOrg();
this.mgmtService
@@ -3,7 +3,7 @@
</h1>
<div mat-dialog-content>
<ng-container *ngIf="selectedType === undefined">
<p class="desc">{{'USER.MFA.DIALOG.ADD_MFA_DESCRIPTION' | translate}}</p>
<p class="desc cnsl-secondary-text">{{'USER.MFA.DIALOG.ADD_MFA_DESCRIPTION' | translate}}</p>
<div class="type-selection">
<button mat-raised-button color="primary" [disabled]="data.otpDisabled" (click)="selectType(AuthFactorType.OTP)">
@@ -26,7 +26,7 @@
</ng-container>
<div class="otp" *ngIf="selectedType === AuthFactorType.OTP">
<p class="desc">{{'USER.MFA.OTP_DIALOG_DESCRIPTION' | translate}}</p>
<p class="desc cnsl-secondary-text">{{'USER.MFA.OTP_DIALOG_DESCRIPTION' | translate}}</p>
<div class="qrcode-wrapper">
<qr-code *ngIf="otpurl" class="qrcode" [value]="otpurl" [size]="150" [errorCorrectionLevel]="'M'"></qr-code>
@@ -52,7 +52,7 @@
</div>
</div>
<div mat-dialog-actions class="action">
<button cdkFocusInitial color="primary" mat-button class="ok-button" (click)="closeDialog()">
<button mat-stroked-button class="ok-button" (click)="closeDialog()">
{{'ACTIONS.CLOSE' | translate}}
</button>
@@ -1,12 +1,12 @@
.type-selection {
display: flex;
margin: 0 -.5rem;
margin: 0 -0.5rem;
.otp-btn,
.u2f-btn {
flex: 1;
min-height: 100px;
border-radius: .5rem;
border-radius: 0.5rem;
display: flex;
flex-direction: column;
justify-content: center;
@@ -20,13 +20,12 @@
}
button {
margin: .5rem;
margin: 0.5rem;
}
}
.desc {
font-size: 14px;
color: var(--grey);
}
.otp {
@@ -49,10 +48,10 @@
justify-content: flex-end;
.ok-button {
margin-left: .5rem;
margin-left: 0.5rem;
}
button {
border-radius: .5rem;
border-radius: 0.5rem;
}
}
@@ -35,7 +35,7 @@ export class AuthFactorDialogComponent {
private translate: TranslateService,
public dialogRef: MatDialogRef<AuthFactorDialogComponent>,
@Inject(MAT_DIALOG_DATA) public data: any,
) { }
) {}
closeDialog(code: string = ''): void {
this.dialogRef.close(code);
@@ -45,31 +45,37 @@ export class AuthFactorDialogComponent {
this.selectedType = type;
if (type === AuthFactorType.OTP) {
this.authService.addMyMultiFactorOTP().then((otpresp) => {
this.otpurl = otpresp.url;
}, error => {
this.toast.showError(error);
});
this.authService.addMyMultiFactorOTP().then(
(otpresp) => {
this.otpurl = otpresp.url;
},
(error) => {
this.toast.showError(error);
},
);
} else if (type === AuthFactorType.U2F) {
this.authService.addMyMultiFactorU2F().then((u2fresp) => {
if (u2fresp.key) {
const credOptions: CredentialCreationOptions = JSON.parse(atob(u2fresp.key?.publicKey as string));
this.authService.addMyMultiFactorU2F().then(
(u2fresp) => {
if (u2fresp.key) {
const credOptions: CredentialCreationOptions = JSON.parse(atob(u2fresp.key?.publicKey as string));
if (credOptions.publicKey?.challenge) {
credOptions.publicKey.challenge = _base64ToArrayBuffer(credOptions.publicKey.challenge as any);
credOptions.publicKey.user.id = _base64ToArrayBuffer(credOptions.publicKey.user.id as any);
if (credOptions.publicKey.excludeCredentials) {
credOptions.publicKey.excludeCredentials.map(cred => {
cred.id = _base64ToArrayBuffer(cred.id as any);
return cred;
});
if (credOptions.publicKey?.challenge) {
credOptions.publicKey.challenge = _base64ToArrayBuffer(credOptions.publicKey.challenge as any);
credOptions.publicKey.user.id = _base64ToArrayBuffer(credOptions.publicKey.user.id as any);
if (credOptions.publicKey.excludeCredentials) {
credOptions.publicKey.excludeCredentials.map((cred) => {
cred.id = _base64ToArrayBuffer(cred.id as any);
return cred;
});
}
this.u2fCredentialOptions = credOptions;
}
this.u2fCredentialOptions = credOptions;
}
}
}, error => {
this.toast.showError(error);
});
},
(error) => {
this.toast.showError(error);
},
);
}
}
@@ -82,60 +88,77 @@ export class AuthFactorDialogComponent {
}
public submitOTP(): void {
this.authService.verifyMyMultiFactorOTP(this.otpcode).then(() => {
this.dialogRef.close(true);
}, error => {
this.dialogRef.close(false);
});
this.authService.verifyMyMultiFactorOTP(this.otpcode).then(
() => {
this.dialogRef.close(true);
},
(error) => {
this.toast.showError(error);
this.dialogRef.close(false);
},
);
}
public submitU2F(): void {
if (this.u2fname && this.u2fCredentialOptions.publicKey) {
// this.data.credOptions.publicKey.rp.id = 'localhost';
navigator.credentials.create(this.u2fCredentialOptions).then((resp) => {
if (resp &&
(resp as any).response.attestationObject &&
(resp as any).response.clientDataJSON &&
(resp as any).rawId) {
navigator.credentials
.create(this.u2fCredentialOptions)
.then((resp) => {
if (
resp &&
(resp as any).response.attestationObject &&
(resp as any).response.clientDataJSON &&
(resp as any).rawId
) {
const attestationObject = (resp as any).response.attestationObject;
const clientDataJSON = (resp as any).response.clientDataJSON;
const rawId = (resp as any).rawId;
const attestationObject = (resp as any).response.attestationObject;
const clientDataJSON = (resp as any).response.clientDataJSON;
const rawId = (resp as any).rawId;
const data = JSON.stringify({
id: resp.id,
rawId: _arrayBufferToBase64(rawId),
type: resp.type,
response: {
attestationObject: _arrayBufferToBase64(attestationObject),
clientDataJSON: _arrayBufferToBase64(clientDataJSON),
},
});
const base64 = btoa(data);
this.authService.verifyMyMultiFactorU2F(base64, this.u2fname).then(() => {
this.translate.get('USER.MFA.U2F_SUCCESS').pipe(take(1)).subscribe(msg => {
this.toast.showInfo(msg);
const data = JSON.stringify({
id: resp.id,
rawId: _arrayBufferToBase64(rawId),
type: resp.type,
response: {
attestationObject: _arrayBufferToBase64(attestationObject),
clientDataJSON: _arrayBufferToBase64(clientDataJSON),
},
});
const base64 = btoa(data);
this.authService
.verifyMyMultiFactorU2F(base64, this.u2fname)
.then(() => {
this.translate
.get('USER.MFA.U2F_SUCCESS')
.pipe(take(1))
.subscribe((msg) => {
this.toast.showInfo(msg);
});
this.dialogRef.close(true);
this.u2fLoading = false;
})
.catch((error) => {
this.u2fLoading = false;
this.toast.showError(error);
});
} else {
this.u2fLoading = false;
this.translate
.get('USER.MFA.U2F_ERROR')
.pipe(take(1))
.subscribe((msg) => {
this.toast.showInfo(msg);
});
this.dialogRef.close(true);
this.u2fLoading = false;
}).catch(error => {
this.u2fLoading = false;
this.toast.showError(error);
});
} else {
}
})
.catch((error) => {
this.u2fLoading = false;
this.translate.get('USER.MFA.U2F_ERROR').pipe(take(1)).subscribe(msg => {
this.toast.showInfo(msg);
});
this.dialogRef.close(true);
}
}).catch(error => {
this.u2fLoading = false;
this.u2fError = error;
this.toast.showInfo(error.message);
});
this.u2fError = error;
this.toast.showInfo(error.message);
});
}
}
}
@@ -1,53 +1,58 @@
<cnsl-card title="{{'USER.PASSWORDLESS.TITLE' | translate}}"
description="{{'USER.PASSWORDLESS.DESCRIPTION' | translate}}">
<button card-actions mat-icon-button (click)="getPasswordless()" class="icon-button" matTooltip="{{'ACTIONS.REFRESH' | translate}}">
<mat-icon class="icon">refresh</mat-icon>
description="{{'USER.PASSWORDLESS.DESCRIPTION' | translate}}">
<button card-actions mat-icon-button (click)="getPasswordless()" class="icon-button"
matTooltip="{{'ACTIONS.REFRESH' | translate}}">
<mat-icon class="icon">refresh</mat-icon>
</button>
<cnsl-refresh-table [hideRefresh]="true" [loading]="loading$ | async" (refreshed)="getPasswordless()"
[dataSize]="dataSource?.data?.length ?? 0">
<button actions class="cnsl-action-button button" (click)="addPasswordless()" mat-raised-button color="primary"
matTooltip="{{'ACTIONS.NEW' | translate}}">
<i class="icon las la-fingerprint"></i>
{{'USER.PASSWORDLESS.U2F' | translate}}
</button>
<cnsl-refresh-table [hideRefresh]="true" [loading]="loading$ | async" (refreshed)="getPasswordless()"
[dataSize]="dataSource?.data?.length ?? 0">
<table class="table" mat-table [dataSource]="dataSource">
<ng-container matColumnDef="name">
<th mat-header-cell *matHeaderCellDef> {{ 'USER.PASSWORDLESS.NAME' | translate }} </th>
<td mat-cell *matCellDef="let mfa"><span *ngIf="mfa?.name" class="centered">
{{ mfa?.name }}
</span>
</td>
</ng-container>
<table class="table" mat-table [dataSource]="dataSource">
<ng-container matColumnDef="name">
<th mat-header-cell *matHeaderCellDef> {{ 'USER.PASSWORDLESS.NAME' | translate }} </th>
<td mat-cell *matCellDef="let mfa"><span *ngIf="mfa?.name" class="centered">
{{ mfa?.name }}
</span>
</td>
</ng-container>
<ng-container matColumnDef="state">
<th mat-header-cell *matHeaderCellDef> {{ 'USER.PASSWORDLESS.TABLESTATE' | translate }} </th>
<td mat-cell *matCellDef="let mfa"><span class="centered">
{{'USER.PASSWORDLESS.STATE.'+ mfa.state | translate}}
<i matTooltip="verified" *ngIf="mfa.state === AuthFactorState.AUTH_FACTOR_STATE_READY"
class="verified las la-check-circle"></i>
</span>
</td>
</ng-container>
<ng-container matColumnDef="state">
<th mat-header-cell *matHeaderCellDef> {{ 'USER.PASSWORDLESS.TABLESTATE' | translate }} </th>
<td mat-cell *matCellDef="let mfa"><span class="centered">
<span class="state"
[ngClass]="{'active': mfa.state === AuthFactorState.AUTH_FACTOR_STATE_READY, 'inactive': mfa.state === AuthFactorState.AUTH_FACTOR_STATE_NOT_READY }">{{'USER.PASSWORDLESS.STATE.'+
mfa.state | translate}}</span>
</span>
</td>
</ng-container>
<ng-container matColumnDef="actions">
<th mat-header-cell *matHeaderCellDef> {{ 'USER.PASSWORDLESS.TABLEACTIONS' | translate }} </th>
<td mat-cell *matCellDef="let mfa">
<button matTooltip="{{'ACTIONS.REMOVE' | translate}}" color="warn" mat-icon-button
(click)="deletePasswordless(mfa.id)">
<i class="las la-trash"></i>
</button>
</td>
</ng-container>
<ng-container matColumnDef="actions" stickyEnd>
<th mat-header-cell *matHeaderCellDef></th>
<td mat-cell *matCellDef="let mfa">
<cnsl-table-actions>
<button actions matTooltip="{{'ACTIONS.REMOVE' | translate}}" color="warn" mat-icon-button
(click)="deletePasswordless(mfa.id)">
<i class="las la-trash"></i>
</button>
</cnsl-table-actions>
</td>
</ng-container>
<tr mat-header-row *matHeaderRowDef="displayedColumns"></tr>
<tr class="highlight" mat-row *matRowDef="let row; columns: displayedColumns;"></tr>
</table>
</cnsl-refresh-table>
<div class="add-row">
<button class="button" (click)="addPasswordless()" mat-raised-button color="primary"
matTooltip="{{'ACTIONS.NEW' | translate}}">
<i class="icon las la-fingerprint"></i>
{{'USER.PASSWORDLESS.U2F' | translate}}
</button>
<tr mat-header-row *matHeaderRowDef="displayedColumns"></tr>
<tr class="highlight" mat-row *matRowDef="let row; columns: displayedColumns;"></tr>
</table>
<div *ngIf="(loading$ | async) === false && !dataSource?.data?.length" class="no-content-row">
<i class="las la-exclamation"></i>
<span>{{'USER.PASSWORDLESS.EMPTY' | translate}}</span>
</div>
<div class="table-wrapper">
<div class="spinner-container" *ngIf="loading$ | async">
<mat-spinner diameter="50"></mat-spinner>
</div>
</cnsl-refresh-table>
<div class="table-wrapper">
<div class="spinner-container" *ngIf="loading$ | async">
<mat-spinner diameter="50"></mat-spinner>
</div>
</div>
</cnsl-card>
@@ -4,24 +4,6 @@
}
}
.add-row {
display: flex;
margin: -.5rem;
flex-wrap: wrap;
justify-content: flex-end;
.button {
margin: .5rem;
margin-top: 1rem;
display: flex;
align-items: center;
.icon {
margin-right: .5rem;
}
}
}
.centered {
display: flex;
align-items: center;
@@ -34,16 +16,4 @@
.table {
width: 100%;
td,
th {
&:first-child {
padding-left: 0;
padding-right: 1rem;
}
&:last-child {
padding-right: 0;
}
}
}
@@ -14,10 +14,10 @@ import { DialogPasswordlessComponent } from './dialog-passwordless/dialog-passwo
export interface WebAuthNOptions {
challenge: string;
rp: { name: string, id: string; };
user: { name: string, id: string, displayName: string; };
rp: { name: string; id: string };
user: { name: string; id: string; displayName: string };
pubKeyCredParams: any;
authenticatorSelection: { userVerification: string; };
authenticatorSelection: { userVerification: string };
timeout: number;
attestation: string;
}
@@ -39,9 +39,7 @@ export class AuthPasswordlessComponent implements OnInit, OnDestroy {
public AuthFactorState: any = AuthFactorState;
public error: string = '';
constructor(private service: GrpcAuthService,
private toast: ToastService,
private dialog: MatDialog) { }
constructor(private service: GrpcAuthService, private toast: ToastService, private dialog: MatDialog) {}
public ngOnInit(): void {
this.getPasswordless();
@@ -52,44 +50,50 @@ export class AuthPasswordlessComponent implements OnInit, OnDestroy {
}
public addPasswordless(): void {
this.service.addMyPasswordless().then((resp) => {
if (resp.key) {
const credOptions: CredentialCreationOptions = JSON.parse(atob(resp.key.publicKey as string));
this.service.addMyPasswordless().then(
(resp) => {
if (resp.key) {
const credOptions: CredentialCreationOptions = JSON.parse(atob(resp.key.publicKey as string));
if (credOptions.publicKey?.challenge) {
credOptions.publicKey.challenge = _base64ToArrayBuffer(credOptions.publicKey.challenge as any);
credOptions.publicKey.user.id = _base64ToArrayBuffer(credOptions.publicKey.user.id as any);
if (credOptions.publicKey.excludeCredentials) {
credOptions.publicKey.excludeCredentials.map(cred => {
cred.id = _base64ToArrayBuffer(cred.id as any);
return cred;
if (credOptions.publicKey?.challenge) {
credOptions.publicKey.challenge = _base64ToArrayBuffer(credOptions.publicKey.challenge as any);
credOptions.publicKey.user.id = _base64ToArrayBuffer(credOptions.publicKey.user.id as any);
if (credOptions.publicKey.excludeCredentials) {
credOptions.publicKey.excludeCredentials.map((cred) => {
cred.id = _base64ToArrayBuffer(cred.id as any);
return cred;
});
}
const dialogRef = this.dialog.open(DialogPasswordlessComponent, {
width: '400px',
data: {
credOptions,
type: U2FComponentDestination.PASSWORDLESS,
},
});
dialogRef.afterClosed().subscribe((done) => {
this.getPasswordless();
});
}
const dialogRef = this.dialog.open(DialogPasswordlessComponent, {
width: '400px',
data: {
credOptions,
type: U2FComponentDestination.PASSWORDLESS,
},
});
dialogRef.afterClosed().subscribe(done => {
this.getPasswordless();
});
}
}
}, error => {
this.toast.showError(error);
});
},
(error) => {
this.toast.showError(error);
},
);
}
public getPasswordless(): void {
this.service.listMyPasswordless().then(passwordless => {
this.dataSource = new MatTableDataSource(passwordless.resultList);
this.dataSource.sort = this.sort;
}).catch(error => {
this.error = error.message;
});
this.service
.listMyPasswordless()
.then((passwordless) => {
this.dataSource = new MatTableDataSource(passwordless.resultList);
this.dataSource.sort = this.sort;
})
.catch((error) => {
this.error = error.message;
});
}
public deletePasswordless(id?: string): void {
@@ -103,14 +107,17 @@ export class AuthPasswordlessComponent implements OnInit, OnDestroy {
width: '400px',
});
dialogRef.afterClosed().subscribe(resp => {
dialogRef.afterClosed().subscribe((resp) => {
if (resp && id) {
this.service.removeMyPasswordless(id).then(() => {
this.toast.showInfo('USER.TOAST.PASSWORDLESSREMOVED', true);
this.getPasswordless();
}).catch(error => {
this.toast.showError(error);
});
this.service
.removeMyPasswordless(id)
.then(() => {
this.toast.showInfo('USER.TOAST.PASSWORDLESSREMOVED', true);
this.getPasswordless();
})
.catch((error) => {
this.toast.showError(error);
});
}
});
}
@@ -5,7 +5,7 @@
<div class="desc">
<i class="icon las la-plus-circle"></i>
<p>{{'USER.PASSWORDLESS.DIALOG.NEW_DESCRIPTION' | translate}}</p>
<p class="cnsl-secondary-text">{{'USER.PASSWORDLESS.DIALOG.NEW_DESCRIPTION' | translate}}</p>
</div>
<cnsl-form-field class="form-field" label="Name" required="true">
@@ -22,7 +22,7 @@
<div class="desc">
<i class="icon las la-paper-plane"></i>
<p>{{'USER.PASSWORDLESS.DIALOG.SEND_DESCRIPTION' | translate}}</p>
<p class="cnsl-secondary-text">{{'USER.PASSWORDLESS.DIALOG.SEND_DESCRIPTION' | translate}}</p>
</div>
<button mat-raised-button class="send-button" color="primary"
@@ -32,7 +32,7 @@
<div class="desc">
<i class="icon las la-qrcode"></i>
<p>{{'USER.PASSWORDLESS.DIALOG.QRCODE_DESCRIPTION' | translate}}</p>
<p class="cnsl-secondary-text">{{'USER.PASSWORDLESS.DIALOG.QRCODE_DESCRIPTION' | translate}}</p>
</div>
<button mat-raised-button class="qr-button" color="primary"
@@ -63,5 +63,5 @@
<mat-spinner diameter="30" *ngIf="loading"></mat-spinner>
</div>
<div mat-dialog-actions class="action">
<button mat-button (click)="closeDialog()">{{'ACTIONS.CLOSE' | translate}}</button>
<button mat-stroked-button (click)="closeDialog()">{{'ACTIONS.CLOSE' | translate}}</button>
</div>
@@ -22,7 +22,6 @@
}
p {
color: var(--grey);
font-size: 14px;
}
}
@@ -37,7 +36,7 @@
justify-content: flex-end;
button {
margin-left: .5rem;
border-radius: .5rem;
margin-left: 0.5rem;
border-radius: 0.5rem;
}
}
@@ -27,9 +27,13 @@ export class DialogPasswordlessComponent {
public showQR: boolean = false;
public qrcodeLink: string = '';
constructor(public dialogRef: MatDialogRef<DialogPasswordlessComponent>,
@Inject(MAT_DIALOG_DATA) public data: { credOptions: any; type: U2FComponentDestination; },
private service: GrpcAuthService, private translate: TranslateService, private toast: ToastService) {
constructor(
public dialogRef: MatDialogRef<DialogPasswordlessComponent>,
@Inject(MAT_DIALOG_DATA) public data: { credOptions: any; type: U2FComponentDestination },
private service: GrpcAuthService,
private translate: TranslateService,
private toast: ToastService,
) {
this.type = data.type;
}
@@ -42,84 +46,106 @@ export class DialogPasswordlessComponent {
this.loading = true;
if (this.name && this.data.credOptions.publicKey) {
// this.data.credOptions.publicKey.rp.id = 'localhost';
navigator.credentials.create(this.data.credOptions).then((resp) => {
if (resp &&
(resp as any).response.attestationObject &&
(resp as any).response.clientDataJSON &&
(resp as any).rawId) {
navigator.credentials
.create(this.data.credOptions)
.then((resp) => {
if (
resp &&
(resp as any).response.attestationObject &&
(resp as any).response.clientDataJSON &&
(resp as any).rawId
) {
const attestationObject = (resp as any).response.attestationObject;
const clientDataJSON = (resp as any).response.clientDataJSON;
const rawId = (resp as any).rawId;
const attestationObject = (resp as any).response.attestationObject;
const clientDataJSON = (resp as any).response.clientDataJSON;
const rawId = (resp as any).rawId;
const data = JSON.stringify({
id: resp.id,
rawId: _arrayBufferToBase64(rawId),
type: resp.type,
response: {
attestationObject: _arrayBufferToBase64(attestationObject),
clientDataJSON: _arrayBufferToBase64(clientDataJSON),
},
});
const data = JSON.stringify({
id: resp.id,
rawId: _arrayBufferToBase64(rawId),
type: resp.type,
response: {
attestationObject: _arrayBufferToBase64(attestationObject),
clientDataJSON: _arrayBufferToBase64(clientDataJSON),
},
});
const base64 = btoa(data);
if (this.type === U2FComponentDestination.MFA) {
this.service.verifyMyMultiFactorU2F(base64, this.name).then(() => {
this.translate.get('USER.MFA.U2F_SUCCESS').pipe(take(1)).subscribe(msg => {
const base64 = btoa(data);
if (this.type === U2FComponentDestination.MFA) {
this.service
.verifyMyMultiFactorU2F(base64, this.name)
.then(() => {
this.translate
.get('USER.MFA.U2F_SUCCESS')
.pipe(take(1))
.subscribe((msg) => {
this.toast.showInfo(msg);
});
this.dialogRef.close(true);
this.loading = false;
})
.catch((error) => {
this.loading = false;
this.toast.showError(error);
});
} else if (this.type === U2FComponentDestination.PASSWORDLESS) {
this.service
.verifyMyPasswordless(base64, this.name)
.then(() => {
this.translate
.get('USER.PASSWORDLESS.U2F_SUCCESS')
.pipe(take(1))
.subscribe((msg) => {
this.toast.showInfo(msg);
});
this.dialogRef.close(true);
this.loading = false;
})
.catch((error) => {
this.loading = false;
this.toast.showError(error);
});
}
} else {
this.loading = false;
this.translate
.get('USER.MFA.U2F_ERROR')
.pipe(take(1))
.subscribe((msg) => {
this.toast.showInfo(msg);
});
this.dialogRef.close(true);
this.loading = false;
}).catch(error => {
this.loading = false;
this.toast.showError(error);
});
} else if (this.type === U2FComponentDestination.PASSWORDLESS) {
this.service.verifyMyPasswordless(base64, this.name).then(() => {
this.translate.get('USER.PASSWORDLESS.U2F_SUCCESS').pipe(take(1)).subscribe(msg => {
this.toast.showInfo(msg);
});
this.dialogRef.close(true);
this.loading = false;
}).catch(error => {
this.loading = false;
this.toast.showError(error);
});
this.dialogRef.close(true);
}
} else {
})
.catch((error) => {
this.loading = false;
this.translate.get('USER.MFA.U2F_ERROR').pipe(take(1)).subscribe(msg => {
this.toast.showInfo(msg);
});
this.dialogRef.close(true);
}
}).catch(error => {
this.loading = false;
this.error = error;
this.toast.showInfo(error.message);
});
this.error = error;
this.toast.showInfo(error.message);
});
}
}
public sendMyPasswordlessLink(): void {
this.service.sendMyPasswordlessLink().then(() => {
this.toast.showInfo('USER.TOAST.PASSWORDLESSREGISTRATIONSENT');
this.showSent = true;
}).catch(error => {
this.toast.showError(error);
});
this.service
.sendMyPasswordlessLink()
.then(() => {
this.toast.showInfo('USER.TOAST.PASSWORDLESSREGISTRATIONSENT');
this.showSent = true;
})
.catch((error) => {
this.toast.showError(error);
});
}
public addMyPasswordlessLink(): void {
this.service.addMyPasswordlessLink().then((resp) => {
console.log(resp);
this.showQR = true;
this.service
.addMyPasswordlessLink()
.then((resp) => {
this.showQR = true;
this.qrcodeLink = resp.link;
}).catch(error => {
this.toast.showError(error);
});
this.qrcodeLink = resp.link;
})
.catch((error) => {
this.toast.showError(error);
});
}
}
@@ -1,96 +1,94 @@
<cnsl-meta-layout>
<div class="max-width-container">
<div class="header-row">
<div class="text">
<h1 class="h1">{{ 'USER.TITLE' | translate }}</h1>
<p class="sub">{{'USER.DESCRIPTION' | translate}}</p>
</div>
<cnsl-top-view title="{{user?.human ? user.human?.profile?.displayName :
user?.machine?.name}}" sub="{{user?.preferredLoginName}}" [isActive]="user?.state === UserState.USER_STATE_ACTIVE"
[isInactive]="user?.state === UserState.USER_STATE_INACTIVE" stateTooltip="{{'USER.STATE.'+user?.state | translate}}"
[hasBackButton]="(['org.read'] | hasRole | async)" (backRouterLink)="['/']">
<div class="theme">
<cnsl-theme-setting></cnsl-theme-setting>
</div>
<span *ngIf="!loading && !user">{{ 'USER.PAGES.NOUSER' | translate }}</span>
<cnsl-info-row topContent *ngIf="user" [user]="user"></cnsl-info-row>
</cnsl-top-view>
<div *ngIf="loading" class="max-width-container">
<mat-progress-spinner diameter="25" color="primary" mode="indeterminate"></mat-progress-spinner>
</div>
<div class="max-width-container">
<cnsl-meta-layout>
<cnsl-sidenav [(ngModel)]="currentSetting" [settingsList]="settingsList">
<ng-container *ngIf="currentSetting === 'general'">
<cnsl-card *ngIf="user && user.human && user.human.profile" class=" app-card"
title="{{ 'USER.PROFILE.TITLE' | translate }}">
<cnsl-detail-form [showEditImage]="true" [preferredLoginName]="user.preferredLoginName" [genders]="genders"
[languages]="languages" [username]="user.userName" [user]="user.human" [disabled]="false"
(changedLanguage)="changedLanguage($event)" (changeUsernameClicked)="changeUsername()"
(submitData)="saveProfile($event)">
</cnsl-detail-form>
</cnsl-card>
<cnsl-card *ngIf="user" title="{{ 'USER.LOGINMETHODS.TITLE' | translate }}"
description="{{ 'USER.LOGINMETHODS.DESCRIPTION' | translate }}">
<button class="icon-button" card-actions mat-icon-button (click)="refreshUser()"
matTooltip="{{'ACTIONS.REFRESH' | translate}}">
<mat-icon class="icon">refresh</mat-icon>
</button>
<cnsl-contact *ngIf="user.human" [human]="user.human" [state]="user.state" [canWrite]="true"
(editType)="openEditDialog($event)" (enteredPhoneCode)="enteredPhoneCode($event)"
(deletedPhone)="deletePhone()" (resendEmailVerification)="resendEmailVerification()"
(resendPhoneVerification)="resendPhoneVerification()">
</cnsl-contact>
</cnsl-card>
<ng-template cnslHasRole [hasRole]="['user.self.delete']">
<cnsl-card title="{{'USER.PAGES.DELETEACCOUNT'| translate}}" [warn]="true">
<p>{{'USER.PAGES.DELETEACCOUNT_DESC'| translate}}</p>
<div class="delete-account-wrapper">
<button color="warn" mat-raised-button (click)="deleteAccount()">{{'USER.PAGES.DELETEACCOUNT_BTN' |
translate}}</button>
</div>
</cnsl-card>
</ng-template>
</ng-container>
<ng-container *ngIf="currentSetting === 'idp'">
<cnsl-external-idps *ngIf="user && user.id" [userId]="user.id" [service]="userService"></cnsl-external-idps>
</ng-container>
<ng-container *ngIf="currentSetting === 'passwordless'">
<cnsl-auth-passwordless *ngIf="user" #mfaComponent></cnsl-auth-passwordless>
</ng-container>
<ng-container *ngIf="currentSetting === 'mfa'">
<cnsl-auth-user-mfa *ngIf="user" #mfaComponent></cnsl-auth-user-mfa>
</ng-container>
<ng-container *ngIf="currentSetting === 'grants'">
<cnsl-card *ngIf="user?.id" title="{{ 'GRANTS.USER.TITLE' | translate }}"
description="{{'GRANTS.USER.DESCRIPTION' | translate }}">
<cnsl-user-grants [userId]="user.id" [context]="USERGRANTCONTEXT"
[displayedColumns]="['select', 'org','projectId', 'type','creationDate','changeDate', 'roleNamesList', 'actions']"
[disableWrite]="((['user.grant.write$'] | hasRole) | async) === false"
[disableDelete]="((['user.grant.delete$'] | hasRole) | async) === false">
</cnsl-user-grants>
</cnsl-card>
</ng-container>
<ng-container *ngIf="currentSetting === 'memberships'">
<cnsl-card *ngIf="user?.id" title="{{ 'USER.MEMBERSHIPS.TITLE' | translate }}"
description="{{'USER.MEMBERSHIPS.DESCRIPTION' | translate }}">
<cnsl-memberships-table></cnsl-memberships-table>
</cnsl-card>
</ng-container>
<ng-template cnslHasFeature [hasFeature]="['metadata.user']">
<ng-container *ngIf="currentSetting === 'metadata'">
<cnsl-metadata *ngIf="user?.id" [userId]="user.id"></cnsl-metadata>
</ng-container>
</ng-template>
</cnsl-sidenav>
<div metainfo>
<cnsl-changes class="changes" [refresh]="refreshChanges$" [changeType]="ChangeType.MYUSER">
</cnsl-changes>
</div>
<mat-progress-bar *ngIf="loading" color="primary" mode="indeterminate"></mat-progress-bar>
<span *ngIf="!loading && !user">{{ 'USER.PAGES.NOUSER' | translate }}</span>
<cnsl-info-row *ngIf="user" [user]="user"></cnsl-info-row>
<cnsl-card *ngIf="user && user.human && user.human.profile" class=" app-card"
title="{{ 'USER.PROFILE.TITLE' | translate }}">
<cnsl-detail-form [showEditImage]="true" [preferredLoginName]="user.preferredLoginName" [genders]="genders"
[languages]="languages" [username]="user.userName" [user]="user.human" [disabled]="false"
(changedLanguage)="changedLanguage($event)" (changeUsernameClicked)="changeUsername()"
(submitData)="saveProfile($event)">
</cnsl-detail-form>
</cnsl-card>
<cnsl-card *ngIf="user" title="{{ 'USER.LOGINMETHODS.TITLE' | translate }}"
description="{{ 'USER.LOGINMETHODS.DESCRIPTION' | translate }}">
<button class="icon-button" card-actions mat-icon-button (click)="refreshUser()"
matTooltip="{{'ACTIONS.REFRESH' | translate}}">
<mat-icon class="icon">refresh</mat-icon>
</button>
<cnsl-contact *ngIf="user.human" [human]="user.human" [state]="user.state" [canWrite]="true"
(editType)="openEditDialog($event)" (enteredPhoneCode)="enteredPhoneCode($event)" (deletedPhone)="deletePhone()"
(resendEmailVerification)="resendEmailVerification()" (resendPhoneVerification)="resendPhoneVerification()">
</cnsl-contact>
</cnsl-card>
<cnsl-external-idps *ngIf="user && user.id" [userId]="user.id" [service]="userService"></cnsl-external-idps>
<cnsl-auth-passwordless *ngIf="user" #mfaComponent></cnsl-auth-passwordless>
<cnsl-auth-user-mfa *ngIf="user" #mfaComponent></cnsl-auth-user-mfa>
<cnsl-card *ngIf="user?.id" title="{{ 'GRANTS.USER.TITLE' | translate }}"
description="{{'GRANTS.USER.DESCRIPTION' | translate }}">
<cnsl-user-grants [userId]="user.id" [context]="USERGRANTCONTEXT"
[displayedColumns]="['select', 'projectId', 'dates', 'roleNamesList']"
[disableWrite]="((['user.grant.write$'] | hasRole) | async) === false"
[disableDelete]="((['user.grant.delete$'] | hasRole) | async) === false">
</cnsl-user-grants>
</cnsl-card>
<ng-template cnslHasFeature [hasFeature]="['metadata.user']">
<cnsl-metadata *ngIf="user?.id" [userId]="user.id"></cnsl-metadata>
</ng-template>
<ng-template cnslHasRole [hasRole]="['user.self.delete']">
<cnsl-card title="{{'USER.PAGES.DELETEACCOUNT'| translate}}" [warn]="true">
<p>{{'USER.PAGES.DELETEACCOUNT_DESC'| translate}}</p>
<div class="delete-account-wrapper">
<button color="warn" mat-raised-button (click)="deleteAccount()">{{'USER.PAGES.DELETEACCOUNT_BTN' |
translate}}</button>
</div>
</cnsl-card>
</ng-template>
</div>
<div *ngIf="user" class="side" metainfo>
<div class="meta-details">
<div class="meta-row">
<span class="first">{{'RESOURCEID' | translate}}:</span>
<span *ngIf="user?.id" class="second">{{ user.id }}</span>
</div>
<div class="meta-row" *ngIf="user?.preferredLoginName">
<span class="first">{{'USER.PREFERRED_LOGINNAME' | translate}}</span>
<span class="second"><span style="display: block;">{{user.preferredLoginName}}</span></span>
</div>
</div>
<mat-tab-group mat-stretch-tabs class="tab-group" [disablePagination]="true">
<mat-tab label="Details">
<div class="side-padding">
<ng-template cnslHasRole [hasRole]="['user.membership.read']">
<cnsl-memberships [auth]="true" [user]="user"></cnsl-memberships>
</ng-template>
</div>
</mat-tab>
<mat-tab label="{{ 'CHANGES.PROJECT.TITLE' | translate }}" class="meta-flex-col">
<cnsl-changes class="changes" [refresh]="refreshChanges$" [changeType]="ChangeType.MYUSER" [id]="user.id">
</cnsl-changes>
</mat-tab>
</mat-tab-group>
</div>
</cnsl-meta-layout>
</cnsl-meta-layout>
</div>
@@ -1,49 +1,90 @@
.header-row {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 2rem;
flex-wrap: wrap;
@use '@angular/material' as mat;
.text {
flex: 1;
@mixin auth-user-detail-theme($theme) {
$foreground: map-get($theme, foreground);
$is-dark-theme: map-get($theme, is-dark);
.h1 {
margin: 0;
.user-container {
padding-top: 1rem;
display: grid;
position: relative;
grid-template-columns: 1fr;
.user-settings-list {
position: relative;
.user-sticky-rel {
display: flex;
flex-direction: column;
position: sticky;
top: 50px;
padding: 1rem 2rem 1rem 0;
.setting-list-element {
border: none;
background: none;
text-align: left;
padding: 0.75rem 0;
opacity: 0.6;
font-size: 15px;
cursor: pointer;
color: map-get($foreground, base);
display: flex;
align-items: center;
i {
font-size: 1.2rem;
height: 1.2rem;
line-height: 1.2rem;
}
&:hover {
opacity: 1;
}
&.mob-only {
display: none;
@media only screen and (max-width: 500px) {
display: flex;
}
}
&.hide-on-mobile {
@media only screen and (max-width: 500px) {
display: none;
}
}
&.show {
display: none;
@media only screen and (max-width: 500px) {
display: flex;
}
}
&.active {
font-weight: 600;
opacity: 1;
}
}
}
}
.sub {
color: var(--grey);
max-width: 500px;
font-size: 14px;
.user-settings {
width: 100%;
overflow-x: auto;
}
}
.theme {
min-width: 250px;
}
}
@media only screen and (min-width: 824px) {
grid-template-columns: 1fr 1fr 1fr 1fr;
.btn-container {
display: flex;
justify-content: flex-end;
margin: 0 -0.5rem;
button {
border-radius: 0.5rem;
margin: 0 0.5rem;
}
}
.col {
display: flex;
flex-wrap: wrap;
align-items: stretch;
margin: -1.5rem -0.5rem;
.app-card {
flex: 1;
margin: 0.5rem;
.user-settings {
grid-column-start: 2;
grid-column-end: 5;
}
}
}
}
@@ -1,12 +1,16 @@
import { MediaMatcher } from '@angular/cdk/layout';
import { Location } from '@angular/common';
import { Component, EventEmitter, OnDestroy } from '@angular/core';
import { MatDialog } from '@angular/material/dialog';
import { TranslateService } from '@ngx-translate/core';
import { Subscription } from 'rxjs';
import { ChangeType } from 'src/app/modules/changes/changes.component';
import { SidenavSetting } from 'src/app/modules/sidenav/sidenav.component';
import { UserGrantContext } from 'src/app/modules/user-grants/user-grants-datasource';
import { WarnDialogComponent } from 'src/app/modules/warn-dialog/warn-dialog.component';
import { Email, Gender, Phone, Profile, User, UserState } from 'src/app/proto/generated/zitadel/user_pb';
import { AuthenticationService } from 'src/app/services/authentication.service';
import { Breadcrumb, BreadcrumbService, BreadcrumbType } from 'src/app/services/breadcrumb.service';
import { GrpcAuthService } from 'src/app/services/grpc-auth.service';
import { ToastService } from 'src/app/services/toast.service';
@@ -33,13 +37,36 @@ export class AuthUserDetailComponent implements OnDestroy {
public USERGRANTCONTEXT: UserGrantContext = UserGrantContext.USER;
public refreshChanges$: EventEmitter<void> = new EventEmitter();
public settingsList: SidenavSetting[] = [
{ id: 'general', i18nKey: 'USER.SETTINGS.GENERAL', featureRequired: false },
{ id: 'idp', i18nKey: 'USER.SETTINGS.IDP', featureRequired: false },
{ id: 'passwordless', i18nKey: 'USER.SETTINGS.PASSWORDLESS', featureRequired: false },
{ id: 'mfa', i18nKey: 'USER.SETTINGS.MFA', featureRequired: false },
{ id: 'grants', i18nKey: 'USER.SETTINGS.USERGRANTS', featureRequired: false },
{ id: 'memberships', i18nKey: 'USER.SETTINGS.MEMBERSHIPS', featureRequired: false },
{ id: 'metadata', i18nKey: 'USER.SETTINGS.METADATA', featureRequired: ['metadata.user'] },
];
public currentSetting: string | undefined = this.settingsList[0].id;
constructor(
public translate: TranslateService,
private toast: ToastService,
public userService: GrpcAuthService,
private dialog: MatDialog,
private auth: AuthenticationService,
private breadcrumbService: BreadcrumbService,
private mediaMatcher: MediaMatcher,
private _location: Location,
) {
const mediaq: string = '(max-width: 500px)';
const small = this.mediaMatcher.matchMedia(mediaq).matches;
if (small) {
this.changeSelection(small);
}
this.mediaMatcher.matchMedia(mediaq).onchange = (small) => {
this.changeSelection(small.matches);
};
this.loading = true;
this.refreshUser();
@@ -48,6 +75,18 @@ export class AuthUserDetailComponent implements OnDestroy {
});
}
private changeSelection(small: boolean): void {
if (small) {
this.currentSetting = undefined;
} else {
this.currentSetting = this.currentSetting === undefined ? this.settingsList[0].id : this.currentSetting;
}
}
public navigateBack(): void {
this._location.back();
}
refreshUser(): void {
this.refreshChanges$.emit();
this.userService
@@ -55,6 +94,14 @@ export class AuthUserDetailComponent implements OnDestroy {
.then((resp) => {
if (resp.user) {
this.user = resp.user;
this.breadcrumbService.setBreadcrumb([
new Breadcrumb({
type: BreadcrumbType.AUTHUSER,
name: this.user.human?.profile?.displayName,
routerLink: ['/users', 'me'],
}),
]);
}
this.loading = false;
})
@@ -218,7 +265,7 @@ export class AuthUserDetailComponent implements OnDestroy {
data: {
confirmKey: 'ACTIONS.SAVE',
cancelKey: 'ACTIONS.CANCEL',
labelKey: 'ACTIONS.NEWVALUE',
labelKey: 'USER.LOGINMETHODS.PHONE.EDITVALUE',
titleKey: 'USER.LOGINMETHODS.PHONE.EDITTITLE',
descriptionKey: 'USER.LOGINMETHODS.PHONE.EDITDESC',
value: this.user.human?.phone?.phone,
@@ -1,60 +1,69 @@
<cnsl-card title="{{'USER.MFA.TITLE' | translate}}" description="{{'USER.MFA.DESCRIPTION' | translate}}">
<button card-actions mat-icon-button (click)="getMFAs()" class="icon-button" matTooltip="{{'ACTIONS.REFRESH' | translate}}">
<button card-actions mat-icon-button (click)="getMFAs()" class="icon-button"
matTooltip="{{'ACTIONS.REFRESH' | translate}}">
<mat-icon class="icon">refresh</mat-icon>
</button>
<cnsl-refresh-table [hideRefresh]="true" [loading]="loading$ | async" (refreshed)="getMFAs()" [dataSize]="dataSource?.data?.length ?? 0">
<table class="table" mat-table [dataSource]="dataSource">
<ng-container matColumnDef="type">
<th mat-header-cell *matHeaderCellDef> {{ 'USER.MFA.TABLETYPE' | translate }} </th>
<td mat-cell *matCellDef="let mfa">
<span *ngIf="mfa.otp !== undefined">OTP (One-Time Password)</span>
<span *ngIf="mfa.u2f !== undefined">U2F (Universal 2nd Factor)</span>
</td>
</ng-container>
<ng-container matColumnDef="name">
<th mat-header-cell *matHeaderCellDef> {{ 'USER.MFA.NAME' | translate }} </th>
<td mat-cell *matCellDef="let mfa">
<span *ngIf="mfa?.u2f?.name" class="centered">
{{ mfa.u2f.name }}
</span>
</td>
</ng-container>
<cnsl-refresh-table [hideRefresh]="true" [loading]="loading$ | async" (refreshed)="getMFAs()"
[dataSize]="dataSource?.data?.length ?? 0">
<button actions class="button cnsl-action-button" (click)="addAuthFactor()" mat-raised-button color="primary"
matTooltip="{{'ACTIONS.NEW' | translate}}">
<mat-icon class="icon">add</mat-icon>
<span>{{'USER.MFA.ADD' | translate}}</span>
</button>
<ng-container matColumnDef="state">
<th mat-header-cell *matHeaderCellDef> {{ 'USER.MFA.TABLESTATE' | translate }} </th>
<td mat-cell *matCellDef="let mfa"><span class="centered">
{{'USER.MFA.STATE.'+ mfa.state | translate}}
<i matTooltip="verified" *ngIf="mfa.state === AuthFactorState.AUTH_FACTOR_STATE_READY"
class="verified las la-check-circle"></i>
</span>
</td>
</ng-container>
<table class="table" mat-table [dataSource]="dataSource">
<ng-container matColumnDef="type">
<th mat-header-cell *matHeaderCellDef> {{ 'USER.MFA.TABLETYPE' | translate }} </th>
<td mat-cell *matCellDef="let mfa">
<span *ngIf="mfa.otp !== undefined">OTP (One-Time Password)</span>
<span *ngIf="mfa.u2f !== undefined">U2F (Universal 2nd Factor)</span>
</td>
</ng-container>
<ng-container matColumnDef="actions">
<th mat-header-cell *matHeaderCellDef> {{ 'USER.MFA.TABLEACTIONS' | translate }} </th>
<td mat-cell *matCellDef="let mfa">
<button matTooltip="{{'ACTIONS.REMOVE' | translate}}" color="warn" mat-icon-button
(click)="deleteMFA(mfa)">
<i class="las la-trash"></i>
</button>
</td>
</ng-container>
<ng-container matColumnDef="name">
<th mat-header-cell *matHeaderCellDef> {{ 'USER.MFA.NAME' | translate }} </th>
<td mat-cell *matCellDef="let mfa">
<span *ngIf="mfa?.u2f?.name" class="centered">
{{ mfa.u2f.name }}
</span>
</td>
</ng-container>
<tr mat-header-row *matHeaderRowDef="displayedColumns"></tr>
<tr class="highlight" mat-row *matRowDef="let row; columns: displayedColumns;"></tr>
</table>
</cnsl-refresh-table>
<div class="add-row">
<button class="button" (click)="addAuthFactor()" mat-raised-button color="primary"
matTooltip="{{'ACTIONS.NEW' | translate}}">
<mat-icon class="icon">add</mat-icon>
{{'USER.MFA.ADD' | translate}}
</button>
<ng-container matColumnDef="state">
<th mat-header-cell *matHeaderCellDef> {{ 'USER.MFA.TABLESTATE' | translate }} </th>
<td mat-cell *matCellDef="let mfa"><span class="centered">
<span class="state"
[ngClass]="{'active': mfa.state === AuthFactorState.AUTH_FACTOR_STATE_READY, 'inactive': mfa.state === AuthFactorState.AUTH_FACTOR_STATE_NOT_READY}">{{'USER.MFA.STATE.'+
mfa.state | translate}}</span>
</span>
</td>
</ng-container>
<ng-container matColumnDef="actions" stickyEnd>
<th mat-header-cell *matHeaderCellDef></th>
<td mat-cell *matCellDef="let mfa">
<cnsl-table-actions>
<button actions matTooltip="{{'ACTIONS.REMOVE' | translate}}" color="warn" mat-icon-button
(click)="deleteMFA(mfa)">
<i class="las la-trash"></i>
</button>
</cnsl-table-actions>
</td>
</ng-container>
<tr mat-header-row *matHeaderRowDef="displayedColumns"></tr>
<tr class="highlight" mat-row *matRowDef="let row; columns: displayedColumns;"></tr>
</table>
<div *ngIf="(loading$ | async) === false && !dataSource?.data?.length" class="no-content-row">
<i class="las la-exclamation"></i>
<span>{{'USER.MFA.EMPTY' | translate}}</span>
</div>
<div class="table-wrapper">
<div class="spinner-container" *ngIf="loading$ | async">
<mat-spinner diameter="50"></mat-spinner>
</div>
</cnsl-refresh-table>
<div class="table-wrapper">
<div class="spinner-container" *ngIf="loading$ | async">
<mat-spinner diameter="50"></mat-spinner>
</div>
</div>
</cnsl-card>
@@ -4,22 +4,6 @@
}
}
.add-row {
display: flex;
margin: -.5rem;
flex-wrap: wrap;
justify-content: flex-end;
.button {
margin: .5rem;
margin-top: 1rem;
.icon {
margin-right: .5rem;
}
}
}
.centered {
display: flex;
align-items: center;
@@ -32,16 +16,4 @@
.table {
width: 100%;
td,
th {
&:first-child {
padding-left: 0;
padding-right: 1rem;
}
&:last-child {
padding-right: 0;
}
}
}
@@ -12,10 +12,10 @@ import { AuthFactorDialogComponent } from '../auth-factor-dialog/auth-factor-dia
export interface WebAuthNOptions {
challenge: string;
rp: { name: string, id: string; };
user: { name: string, id: string, displayName: string; };
rp: { name: string; id: string };
user: { name: string; id: string; displayName: string };
pubKeyCredParams: any;
authenticatorSelection: { userVerification: string; };
authenticatorSelection: { userVerification: string };
timeout: number;
attestation: string;
}
@@ -26,7 +26,7 @@ export interface WebAuthNOptions {
styleUrls: ['./auth-user-mfa.component.scss'],
})
export class AuthUserMfaComponent implements OnInit, OnDestroy {
public displayedColumns: string[] = ['type', 'name', 'state', 'actions'];
public displayedColumns: string[] = ['name', 'type', 'state', 'actions'];
private loadingSubject: BehaviorSubject<boolean> = new BehaviorSubject<boolean>(false);
public loading$: Observable<boolean> = this.loadingSubject.asObservable();
@@ -39,11 +39,7 @@ export class AuthUserMfaComponent implements OnInit, OnDestroy {
public error: string = '';
public otpAvailable: boolean = false;
constructor(
private service: GrpcAuthService,
private toast: ToastService,
private dialog: MatDialog,
) { }
constructor(private service: GrpcAuthService, private toast: ToastService, private dialog: MatDialog) {}
public ngOnInit(): void {
this.getMFAs();
@@ -60,28 +56,27 @@ export class AuthUserMfaComponent implements OnInit, OnDestroy {
},
});
dialogRef.afterClosed().subscribe((code) => {
if (code) {
this.service.verifyMyMultiFactorOTP(code).then(() => {
this.getMFAs();
});
}
dialogRef.afterClosed().subscribe(() => {
this.getMFAs();
});
}
public getMFAs(): void {
this.service.listMyMultiFactors().then(mfas => {
const list = mfas.resultList;
this.dataSource = new MatTableDataSource(list);
this.dataSource.sort = this.sort;
this.service
.listMyMultiFactors()
.then((mfas) => {
const list = mfas.resultList;
this.dataSource = new MatTableDataSource(list);
this.dataSource.sort = this.sort;
const index = list.findIndex(mfa => mfa.otp);
if (index === -1) {
this.otpAvailable = true;
}
}).catch(error => {
this.error = error.message;
});
const index = list.findIndex((mfa) => mfa.otp);
if (index === -1) {
this.otpAvailable = true;
}
})
.catch((error) => {
this.error = error.message;
});
}
public deleteMFA(factor: AuthFactor.AsObject): void {
@@ -95,37 +90,40 @@ export class AuthUserMfaComponent implements OnInit, OnDestroy {
width: '400px',
});
dialogRef.afterClosed().subscribe(resp => {
dialogRef.afterClosed().subscribe((resp) => {
if (resp) {
if (factor.otp) {
this.service.removeMyMultiFactorOTP().then(() => {
this.toast.showInfo('USER.TOAST.OTPREMOVED', true);
this.service
.removeMyMultiFactorOTP()
.then(() => {
this.toast.showInfo('USER.TOAST.OTPREMOVED', true);
const index = this.dataSource.data.findIndex(mfa => !!mfa.otp);
if (index > -1) {
this.dataSource.data.splice(index, 1);
}
this.getMFAs();
}).catch(error => {
this.toast.showError(error);
});
} else
if (factor.u2f) {
this.service.removeMyMultiFactorU2F(factor.u2f.id).then(() => {
this.toast.showInfo('USER.TOAST.U2FREMOVED', true);
const index = this.dataSource.data.findIndex(mfa => !!mfa.u2f);
const index = this.dataSource.data.findIndex((mfa) => !!mfa.otp);
if (index > -1) {
this.dataSource.data.splice(index, 1);
}
this.getMFAs();
}).catch(error => {
})
.catch((error) => {
this.toast.showError(error);
});
}
} else if (factor.u2f) {
this.service
.removeMyMultiFactorU2F(factor.u2f.id)
.then(() => {
this.toast.showInfo('USER.TOAST.U2FREMOVED', true);
const index = this.dataSource.data.findIndex((mfa) => !!mfa.u2f);
if (index > -1) {
this.dataSource.data.splice(index, 1);
}
this.getMFAs();
})
.catch((error) => {
this.toast.showError(error);
});
}
}
});
}
}
@@ -1,20 +1,20 @@
<h1 mat-dialog-title>
<span>{{'USER.CODEDIALOG.TITLE' | translate}} {{data?.number}}</span>
<span>{{'USER.CODEDIALOG.TITLE' | translate}} {{data?.number}}</span>
</h1>
<p class="desc">{{'USER.CODEDIALOG.DESCRIPTION' | translate}}</p>
<p class="desc cnsl-secondary-text">{{'USER.CODEDIALOG.DESCRIPTION' | translate}}</p>
<div mat-dialog-content>
<cnsl-form-field class="formfield">
<cnsl-label>{{ 'USER.CODEDIALOG.CODE' | translate }}</cnsl-label>
<input cnslInput [(ngModel)]="code" />
</cnsl-form-field>
<cnsl-form-field class="formfield">
<cnsl-label>{{ 'USER.CODEDIALOG.CODE' | translate }}</cnsl-label>
<input cnslInput [(ngModel)]="code" />
</cnsl-form-field>
</div>
<div mat-dialog-actions class="action">
<button color="primary" mat-stroked-button class="ok-button" (click)="closeDialog()">
{{'ACTIONS.CLOSE' | translate}}
</button>
<button color="primary" mat-stroked-button class="ok-button" (click)="closeDialog()">
{{'ACTIONS.CLOSE' | translate}}
</button>
<button [disabled]="!code" cdkFocusInitial color="primary" mat-raised-button class="ok-button"
(click)="closeDialog(code)">
{{'ACTIONS.OK' | translate}}
</button>
<button [disabled]="!code" cdkFocusInitial color="primary" mat-raised-button class="ok-button"
(click)="closeDialog(code)">
{{'ACTIONS.OK' | translate}}
</button>
</div>
@@ -5,7 +5,6 @@ h1 {
.desc {
font-size: 14px;
color: var(--grey);
}
.formfield {
@@ -17,10 +16,10 @@ h1 {
justify-content: flex-end;
.ok-button {
margin-left: .5rem;
margin-left: 0.5rem;
}
button {
border-radius: .5rem;
border-radius: 0.5rem;
}
}
@@ -1,19 +1,19 @@
<h1 mat-dialog-title>{{'USER.MFA.U2F_DIALOG_TITLE' | translate}}</h1>
<div mat-dialog-content>
<p>{{'USER.MFA.U2F_DIALOG_DESCRIPTION' | translate}}</p>
<p>{{'USER.MFA.U2F_DIALOG_DESCRIPTION' | translate}}</p>
<cnsl-form-field class="form-field" label="Name" required="true">
<cnsl-label>{{'USER.MFA.U2F_NAME' | translate}}</cnsl-label>
<input cnslInput [(ngModel)]="name" required (keydown.enter)="name ? closeDialogWithCode() : null" />
</cnsl-form-field>
<cnsl-form-field class="form-field" label="Name" required="true">
<cnsl-label>{{'USER.MFA.U2F_NAME' | translate}}</cnsl-label>
<input cnslInput [(ngModel)]="name" required (keydown.enter)="name ? closeDialogWithCode() : null" />
</cnsl-form-field>
<mat-spinner diameter="30" *ngIf="loading"></mat-spinner>
<mat-spinner diameter="30" *ngIf="loading"></mat-spinner>
<p class="error">{{error}}</p>
<p class="error">{{error}}</p>
</div>
<div mat-dialog-actions class="action">
<button mat-button (click)="closeDialog()">{{'ACTIONS.CLOSE' | translate}}</button>
<button cdkFocusInitial [disabled]="!name" mat-raised-button class="ok-button" color="primary"
(click)="closeDialogWithCode()">{{'ACTIONS.VERIFY' | translate}}
</button>
<button mat-stroked-button (click)="closeDialog()">{{'ACTIONS.CLOSE' | translate}}</button>
<button cdkFocusInitial [disabled]="!name" mat-raised-button class="ok-button" color="primary"
(click)="closeDialogWithCode()">{{'ACTIONS.VERIFY' | translate}}
</button>
</div>
@@ -1,7 +1,7 @@
<h1 mat-dialog-title>
<span class="title">{{data.titleKey | translate}}</span>
</h1>
<p class="desc">{{data.descriptionKey | translate}}</p>
<p class="desc cnsl-secondary-text">{{data.descriptionKey | translate}}</p>
<div mat-dialog-content>
<cnsl-form-field class="formfield">
<cnsl-label>{{data.labelKey | translate }} <span *ngIf="isPhone && phoneCountry">({{ phoneCountry }})</span>
@@ -21,7 +21,7 @@
</ng-container>
</div>
<div mat-dialog-actions class="action">
<button cdkFocusInitial color="primary" mat-button class="ok-button" (click)="closeDialog()">
<button mat-stroked-button class="ok-button" (click)="closeDialog()">
{{data.cancelKey | translate}}
</button>
@@ -8,7 +8,6 @@
.desc {
font-size: 14px;
color: var(--grey);
}
.action {
@@ -1,19 +1,19 @@
<h1 mat-dialog-title>
<span class="title">{{'USER.SENDEMAILDIALOG.TITLE' | translate}} {{data?.number}}</span>
<span class="title">{{'USER.SENDEMAILDIALOG.TITLE' | translate}} {{data?.number}}</span>
</h1>
<p class="desc">{{'USER.SENDEMAILDIALOG.DESCRIPTION' | translate}}</p>
<p class="desc cnsl-secondary-text">{{'USER.SENDEMAILDIALOG.DESCRIPTION' | translate}}</p>
<div mat-dialog-content>
<cnsl-form-field class="formfield">
<cnsl-label>{{ 'USER.SENDEMAILDIALOG.NEWEMAIL' | translate }}</cnsl-label>
<input cnslInput [(ngModel)]="email" />
</cnsl-form-field>
<cnsl-form-field class="formfield">
<cnsl-label>{{ 'USER.SENDEMAILDIALOG.NEWEMAIL' | translate }}</cnsl-label>
<input cnslInput [(ngModel)]="email" />
</cnsl-form-field>
</div>
<div mat-dialog-actions class="action">
<button color="primary" mat-button class="ok-button" (click)="closeDialog()">
{{'ACTIONS.CLOSE' | translate}}
</button>
<button mat-stroked-button class="ok-button" (click)="closeDialog()">
{{'ACTIONS.CLOSE' | translate}}
</button>
<button cdkFocusInitial color="primary" mat-raised-button class="ok-button" (click)="closeDialogWithSend(email)">
{{'ACTIONS.SEND' | translate}}
</button>
<button cdkFocusInitial color="primary" mat-raised-button class="ok-button" (click)="closeDialogWithSend(email)">
{{'ACTIONS.SEND' | translate}}
</button>
</div>
@@ -4,7 +4,6 @@
.desc {
font-size: 14px;
color: var(--grey);
}
.action {
@@ -12,10 +11,10 @@
justify-content: flex-end;
.ok-button {
margin-left: .5rem;
margin-left: 0.5rem;
}
button {
border-radius: .5rem;
border-radius: 0.5rem;
}
}
@@ -1,16 +0,0 @@
@use '@angular/material' as mat;
@mixin theme-card($theme) {
/* stylelint-disable */
$primary: map-get($theme, primary);
$background: map-get($theme, background);
$is-dark-theme: map-get($theme, is-dark);
$back: map-get($background, background);
/* stylelint-enable */
.theme-conent,
.crescent {
background-color: $back;
transition: background-color .3s cubic-bezier(.645, .045, .355, 1); // cubic-bezier(.645, .045, .355, 1);
}
}
@@ -1,15 +0,0 @@
<input type="checkbox" id="switch" (change)="change($event)" [checked]="isDarkTheme"/>
<div class="theme-app">
<div class="theme-content">
<label for="switch">
<div class="toggle"></div>
<div class="names">
<p class="light">Light</p>
<div class="circle">
<div class="crescent"></div>
</div>
<p class="dark">Dark</p>
</div>
</label>
</div>
</div>
@@ -1,106 +0,0 @@
:root {
transition: none;
}
.theme-app {
position: relative;
}
.theme-content {
display: flex;
flex-direction: column;
text-align: center;
}
.circle {
z-index: 1;
position: relative;
border-radius: 100%;
width: 60px;
height: 60px;
background: linear-gradient(40deg, #ff0080, #ff8c00 70%);
box-shadow: 0 30px 60px rgba(0, 0, 0, .12);
}
.crescent {
position: absolute;
border-radius: 100%;
right: 0;
width: 40px;
height: 40px;
transform: scale(0);
transform-origin: top right;
transition: transform .2s cubic-bezier(.645, .045, .355, 1);
}
p {
font-size: 90%;
}
label,
.toggle {
height: 45px;
border-radius: 100px;
}
label {
width: 100%;
background-color: rgba(0, 0, 0, .1);
border-radius: 100px;
position: relative;
cursor: pointer;
}
.toggle {
position: absolute;
box-shadow: 0 2px 15px rgba(0, 0, 0, .15);
}
.names {
font-size: 90%;
font-weight: bolder;
width: 65%;
margin-left: 17.5%;
position: absolute;
display: flex;
justify-content: space-between;
align-items: center;
user-select: none;
height: 60px;
transform: translateY(-7.5px);
}
[type="checkbox"] {
display: none;
}
[type="checkbox"]:checked {
.toggle {
transform: translateX(100%);
}
.light {
opacity: .5;
}
.dark {
opacity: 1;
}
.theme-app {
background-color: inherit;
color: inherit;
}
}
[type="checkbox"]:checked + .theme-app .crescent {
transform: scale(1);
}
[type="checkbox"]:checked + .theme-app .circle {
background: linear-gradient(40deg, #8983f7, #a3dafb 70%);
}
[type="checkbox"]:checked + .theme-app .main-circle {
background: linear-gradient(40deg, #8983f7, #a3dafb 70%);
}
@@ -1,25 +0,0 @@
import { ComponentFixture, TestBed, waitForAsync } from '@angular/core/testing';
import { ThemeSettingComponent } from './theme-setting.component';
describe('ThemeSettingComponent', () => {
let component: ThemeSettingComponent;
let fixture: ComponentFixture<ThemeSettingComponent>;
beforeEach(waitForAsync(() => {
TestBed.configureTestingModule({
declarations: [ThemeSettingComponent],
})
.compileComponents();
}));
beforeEach(() => {
fixture = TestBed.createComponent(ThemeSettingComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should create', () => {
expect(component).toBeTruthy();
});
});
@@ -1,32 +0,0 @@
import { Component, OnDestroy, OnInit } from '@angular/core';
import { Subscription } from 'rxjs';
import { ThemeService } from 'src/app/services/theme.service';
@Component({
selector: 'cnsl-theme-setting',
templateUrl: './theme-setting.component.html',
styleUrls: ['./theme-setting.component.scss'],
})
export class ThemeSettingComponent implements OnInit, OnDestroy {
public isDarkTheme: boolean = true;
private sub: Subscription = new Subscription();
constructor(private themeService: ThemeService) {
const theme = localStorage.getItem('theme');
this.isDarkTheme = theme === 'dark-theme' ? true : theme === 'light-theme' ? false : true;
}
ngOnInit(): void {
this.sub = this.themeService.isDarkTheme.subscribe(d => this.isDarkTheme = d);
}
ngOnDestroy(): void {
this.sub.unsubscribe();
}
public change(event: any): void {
if (event.target) {
const checked = event.target.checked;
this.themeService.setDarkTheme(checked);
}
}
}
@@ -1,77 +1,79 @@
<div class="method-col">
<div class="method-row">
<div class="left">
<span class="label">{{ 'USER.PROFILE.PASSWORD' | translate }}</span>
<span class="name">*********</span>
<div class="method-row">
<div class="left">
<span class="label cnsl-secondary-text">{{ 'USER.PROFILE.PASSWORD' | translate }}</span>
<span class="name">*********</span>
<ng-content select="[pwdAction]"></ng-content>
</div>
<div class="right">
<a matTooltip="{{'USER.PASSWORD.SET' | translate}}" [disabled]="!canWrite" [routerLink]="['password']"
mat-icon-button>
<i class="las la-edit"></i>
</a>
</div>
<ng-content select="[pwdAction]"></ng-content>
</div>
<div class="method-row">
<div class="left">
<span class="label">{{ 'USER.EMAIL' | translate }}</span>
<span class="name">{{human?.email?.email}}</span>
<span *ngIf="human?.email?.isEmailVerified" class="contact-state verified">{{'USER.EMAILVERIFIED' |
translate}}</span>
<div *ngIf="!human?.email?.isEmailVerified" class="block">
<span class="contact-state notverified">{{'USER.NOTVERIFIED' | translate}}</span>
<div class="right">
<a matTooltip="{{'USER.PASSWORD.SET' | translate}}" [disabled]="!canWrite" [routerLink]="['password']"
mat-icon-button>
<i class="las la-pen"></i>
</a>
</div>
</div>
<ng-container *ngIf="human?.email">
<a *ngIf="canWrite" class="verify" matTooltip="{{'USER.LOGINMETHODS.EMAIL.RESEND' | translate}}"
(click)="emitEmailVerification()">{{'USER.LOGINMETHODS.RESENDCODE' | translate}}</a>
</ng-container>
</div>
<div class="method-row">
<div class="left">
<span class="label cnsl-secondary-text">{{ 'USER.EMAIL' | translate }}</span>
<span class="name">{{human?.email?.email}}</span>
<span *ngIf="human?.email?.isEmailVerified" class="contact-state verified">{{'USER.EMAILVERIFIED' |
translate}}</span>
<div *ngIf="!human?.email?.isEmailVerified" class="block">
<span class="contact-state notverified">{{'USER.NOTVERIFIED' | translate}}</span>
<ng-content select="[emailAction]"></ng-content>
</div>
<ng-container *ngIf="human?.email">
<a *ngIf="canWrite" class="verify cnsl-secondary-text"
matTooltip="{{'USER.LOGINMETHODS.EMAIL.RESEND' | translate}}"
(click)="emitEmailVerification()">{{'USER.LOGINMETHODS.RESENDCODE' | translate}}</a>
</ng-container>
</div>
<div class="right">
<button matTooltip="{{'ACTIONS.EDIT' | translate}}"
[disabled]="!canWrite || state === UserState.USER_STATE_INITIAL"
(click)="openEditDialog(EditDialogType.EMAIL)" mat-icon-button>
<i class="las la-edit"></i>
</button>
</div>
<ng-content select="[emailAction]"></ng-content>
</div>
<div class="method-row">
<div class="left">
<span class="label">{{ 'USER.PHONE' | translate }}</span>
<span class="name">{{human?.phone?.phone ? human.phone?.phone : ('USER.PHONEEMPTY' | translate)}}</span>
<span *ngIf="human?.phone?.isPhoneVerified" class="contact-state verified">{{'USER.PHONEVERIFIED' |
translate}}</span>
<div *ngIf="human.phone?.phone && !human?.phone?.isPhoneVerified" class="block">
<span class="contact-state notverified">{{'USER.NOTVERIFIED' | translate}}</span>
<ng-container *ngIf="human?.phone?.phone">
<a *ngIf="!disablePhoneCode && canWrite" class="verify"
matTooltip="{{'USER.LOGINMETHODS.ENTERCODE_DESC' | translate}}"
(click)="enterCode()">{{'USER.LOGINMETHODS.ENTERCODE' | translate}}</a>
<a *ngIf="canWrite" class="verify" matTooltip="{{'USER.LOGINMETHODS.PHONE.RESEND' | translate}}"
(click)="emitPhoneVerification()">{{'USER.LOGINMETHODS.RESENDCODE' | translate}}</a>
</ng-container>
</div>
<ng-content select="[phoneAction]"></ng-content>
</div>
<div class="right">
<button matTooltip="{{'ACTIONS.DELETE' | translate}}" *ngIf="human && human.phone?.phone" color="warn"
(click)="emitDeletePhone()" mat-icon-button>
<i class="las la-trash"></i>
</button>
<button matTooltip="{{'ACTIONS.EDIT' | translate}}" [disabled]="!canWrite"
(click)="openEditDialog(EditDialogType.PHONE)" mat-icon-button>
<i class="las la-edit"></i>
</button>
</div>
<div class="right">
<button matTooltip="{{'ACTIONS.EDIT' | translate}}"
[disabled]="!canWrite || state === UserState.USER_STATE_INITIAL" (click)="openEditDialog(EditDialogType.EMAIL)"
mat-icon-button>
<i class="las la-pen"></i>
</button>
</div>
</div>
<div class="method-row">
<div class="left">
<span class="label cnsl-secondary-text">{{ 'USER.PHONE' | translate }}</span>
<span class="name">{{human?.phone?.phone ? human.phone?.phone : ('USER.PHONEEMPTY' | translate)}}</span>
<span *ngIf="human?.phone?.isPhoneVerified" class="contact-state verified">{{'USER.PHONEVERIFIED' |
translate}}</span>
<div *ngIf="human.phone?.phone && !human?.phone?.isPhoneVerified" class="block">
<span class="contact-state notverified">{{'USER.NOTVERIFIED' | translate}}</span>
<ng-container *ngIf="human?.phone?.phone">
<a *ngIf="!disablePhoneCode && canWrite" class="verify cnsl-secondary-text"
matTooltip="{{'USER.LOGINMETHODS.ENTERCODE_DESC' | translate}}"
(click)="enterCode()">{{'USER.LOGINMETHODS.ENTERCODE' | translate}}</a>
<a *ngIf="canWrite" class="verify cnsl-secondary-text"
matTooltip="{{'USER.LOGINMETHODS.PHONE.RESEND' | translate}}"
(click)="emitPhoneVerification()">{{'USER.LOGINMETHODS.RESENDCODE' | translate}}</a>
</ng-container>
</div>
<ng-content select="[phoneAction]"></ng-content>
</div>
<div class="right">
<button matTooltip="{{'ACTIONS.DELETE' | translate}}" *ngIf="human && human.phone?.phone" color="warn"
(click)="emitDeletePhone()" mat-icon-button>
<i class="las la-trash"></i>
</button>
<button matTooltip="{{'ACTIONS.EDIT' | translate}}" [disabled]="!canWrite"
(click)="openEditDialog(EditDialogType.PHONE)" mat-icon-button>
<i class="las la-pen"></i>
</button>
</div>
</div>
</div>
@@ -1,13 +1,13 @@
.method-col {
display: flex;
flex-direction: column;
margin: -.5rem;
margin: -0.5rem;
.method-row {
display: flex;
justify-content: space-between;
align-items: center;
padding: .5rem;
padding: 0.5rem;
padding-right: 0;
border-bottom: 1px solid #ffffff20;
flex-wrap: wrap;
@@ -15,20 +15,19 @@
.left {
.label {
font-size: 13px;
margin-bottom: .5rem;
margin-bottom: 0.5rem;
min-width: 100px;
color: var(--grey);
display: block;
}
.name {
display: block;
margin-bottom: .5rem;
margin-bottom: 0.5rem;
}
.contact-state {
font-size: 14px;
margin-bottom: .5rem;
margin-bottom: 0.5rem;
&.verified {
color: #85d996;
@@ -61,9 +60,8 @@
.verify {
text-decoration: none;
font-size: .8rem;
color: var(--grey);
border-radius: .5rem;
font-size: 0.8rem;
border-radius: 0.5rem;
cursor: pointer;
word-wrap: none;
white-space: nowrap;
@@ -16,15 +16,15 @@ export class ContactComponent {
@Input() canWrite: boolean | null = false;
@Input() human!: Human.AsObject;
@Input() state!: UserState;
@Output() editType: EventEmitter<EditDialogType> = new EventEmitter();
@Output() resendEmailVerification: EventEmitter<void> = new EventEmitter();
@Output() resendPhoneVerification: EventEmitter<void> = new EventEmitter();
@Output() enteredPhoneCode: EventEmitter<string> = new EventEmitter();
@Output() deletedPhone: EventEmitter<void> = new EventEmitter();
@Output() editType: EventEmitter<EditDialogType> = new EventEmitter<EditDialogType>();
@Output() resendEmailVerification: EventEmitter<void> = new EventEmitter<void>();
@Output() resendPhoneVerification: EventEmitter<void> = new EventEmitter<void>();
@Output() enteredPhoneCode: EventEmitter<string> = new EventEmitter<string>();
@Output() deletedPhone: EventEmitter<void> = new EventEmitter<void>();
public UserState: any = UserState;
public EditDialogType: any = EditDialogType;
constructor(private dialog: MatDialog) { }
constructor(private dialog: MatDialog) {}
emitDeletePhone(): void {
const dialogRef = this.dialog.open(WarnDialogComponent, {
@@ -37,7 +37,7 @@ export class ContactComponent {
width: '400px',
});
dialogRef.afterClosed().subscribe(resp => {
dialogRef.afterClosed().subscribe((resp) => {
if (resp) {
this.deletedPhone.emit();
}
@@ -61,7 +61,7 @@ export class ContactComponent {
width: '400px',
});
dialogRef.afterClosed().subscribe(code => {
dialogRef.afterClosed().subscribe((code) => {
if (code) {
this.enteredPhoneCode.emit(code);
}
@@ -1,28 +1,28 @@
<form [formGroup]="machineForm" *ngIf="machineForm" (ngSubmit)="submitForm()">
<div class="content">
<cnsl-form-field class="formfield">
<cnsl-label>{{ 'USER.MACHINE.USERNAME' | translate }}</cnsl-label>
<input cnslInput formControlName="userName" required />
<span cnsl-error *ngIf="userName?.invalid && userName?.errors?.required">
{{ 'USER.VALIDATION.REQUIRED' | translate }}
</span>
<span cnsl-error *ngIf="userName?.invalid && userName?.errors?.noEmailValidator">
{{ 'USER.VALIDATION.NOEMAIL' | translate }}
</span>
</cnsl-form-field>
<cnsl-form-field class="formfield">
<cnsl-label>{{ 'USER.MACHINE.NAME' | translate }}</cnsl-label>
<input cnslInput formControlName="name" required />
<span cnsl-error *ngIf="name?.invalid && name?.errors?.required">
{{ 'USER.VALIDATION.REQUIRED' | translate }}
</span>
</cnsl-form-field>
<cnsl-form-field class="formfield">
<cnsl-label>{{ 'USER.MACHINE.DESCRIPTION' | translate }}</cnsl-label>
<input cnslInput formControlName="description" />
</cnsl-form-field>
</div>
<div class="btn-container">
<button type="submit" color="primary" mat-raised-button>{{ 'ACTIONS.SAVE' | translate }}</button>
</div>
<div class="content">
<cnsl-form-field class="formfield">
<cnsl-label>{{ 'USER.MACHINE.USERNAME' | translate }}</cnsl-label>
<input cnslInput formControlName="userName" required />
<span cnslError *ngIf="userName?.invalid && userName?.errors?.required">
{{ 'USER.VALIDATION.REQUIRED' | translate }}
</span>
<span cnslError *ngIf="userName?.invalid && userName?.errors?.noEmailValidator">
{{ 'USER.VALIDATION.NOEMAIL' | translate }}
</span>
</cnsl-form-field>
<cnsl-form-field class="formfield">
<cnsl-label>{{ 'USER.MACHINE.NAME' | translate }}</cnsl-label>
<input cnslInput formControlName="name" required />
<span cnslError *ngIf="name?.invalid && name?.errors?.required">
{{ 'USER.VALIDATION.REQUIRED' | translate }}
</span>
</cnsl-form-field>
<cnsl-form-field class="formfield">
<cnsl-label>{{ 'USER.MACHINE.DESCRIPTION' | translate }}</cnsl-label>
<input cnslInput formControlName="description" />
</cnsl-form-field>
</div>
<div class="btn-container">
<button type="submit" color="primary" mat-raised-button>{{ 'ACTIONS.SAVE' | translate }}</button>
</div>
</form>
@@ -17,7 +17,7 @@
<cnsl-label>{{ 'USER.PROFILE.USERNAME' | translate }}</cnsl-label>
<input cnslInput formControlName="userName" />
</cnsl-form-field>
<button [disabled]="user && disabled" type="button" color="primary" mat-stroked-button class="edit"
<button [disabled]="user && disabled" type="button" mat-stroked-button class="edit"
(click)="changeUsername()">{{'USER.PROFILE.CHANGEUSERNAME' |
translate}}</button>
</div>
@@ -1,16 +1,20 @@
<span class="title" mat-dialog-title>{{'USER.PROFILE.AVATAR.UPLOADTITLE' | translate}}</span>
<div mat-dialog-content>
<p class="desc">{{'USER.PROFILE.AVATAR.CURRENT' | translate}}</p>
<p class="desc cnsl-secondary-text">{{'USER.PROFILE.AVATAR.CURRENT' | translate}}</p>
<div class="current-pic-wrapper">
<img class="pic" [src]="data.profilePic" *ngIf="data.profilePic"/>
<img class="pic" [src]="data.profilePic" *ngIf="data.profilePic" />
<span class="fill-space"></span>
<input #selectedFile style="display: none;" class="file-input" type="file" (change)="onDrop($event)">
<button class="btn" mat-raised-button color="primary" type="button" (click)="selectedFile.click();">{{'USER.PROFILE.AVATAR.UPLOADBTN' | translate}}</button>
<button *ngIf="data.profilePic" matTooltip="{{'ACTIONS.DELETE' | translate}}" color="warn" (click)="deletePic()" mat-icon-button><mat-icon>remove_circle</mat-icon></button>
<input #selectedFile style="display: none;" class="file-input" type="file" (change)="onDrop($event)">
<button class="btn" mat-raised-button color="primary" type="button"
(click)="selectedFile.click();">{{'USER.PROFILE.AVATAR.UPLOADBTN' | translate}}</button>
<button *ngIf="data.profilePic" matTooltip="{{'ACTIONS.DELETE' | translate}}" color="warn" (click)="deletePic()"
mat-icon-button>
<mat-icon>remove_circle</mat-icon>
</button>
</div>
</div>
<div mat-dialog-actions class="action">
<button color="primary" mat-stroked-button class="ok-button" (click)="closeDialog()">
{{'ACTIONS.CLOSE' | translate}}
</button>
<button color="primary" mat-stroked-button class="ok-button" (click)="closeDialog()">
{{'ACTIONS.CLOSE' | translate}}
</button>
</div>
@@ -6,7 +6,6 @@
}
.desc {
color: var(--grey);
font-size: 14px;
}
@@ -52,7 +51,7 @@
.cropper {
margin: 1rem 0;
width: auto;
border-radius: .5rem;
border-radius: 0.5rem;
}
.action {
@@ -60,7 +59,7 @@
justify-content: flex-end;
.ok-button {
margin-left: .5rem;
margin-left: 0.5rem;
}
}
@@ -1,63 +1,71 @@
<cnsl-card title="{{ 'USER.EXTERNALIDP.TITLE' | translate }}" description="{{ 'USER.EXTERNALIDP.DESC' | translate }}">
<button card-actions mat-icon-button (click)="refreshPage()" class="icon-button" matTooltip="{{'ACTIONS.REFRESH' | translate}}">
<button card-actions mat-icon-button (click)="refreshPage()" class="icon-button"
matTooltip="{{'ACTIONS.REFRESH' | translate}}">
<mat-icon class="icon">refresh</mat-icon>
</button>
<cnsl-refresh-table [hideRefresh]="true" [loading]="loading$ | async" [dataSize]="dataSource.data.length"
[timestamp]="viewTimestamp" [selection]="selection">
[timestamp]="viewTimestamp" [selection]="selection">
<div class="table-wrapper">
<table class="table" mat-table [dataSource]="dataSource">
<ng-container matColumnDef="select">
<th mat-header-cell *matHeaderCellDef>
<mat-checkbox color="primary" (change)="$event ? masterToggle() : null"
[checked]="selection.hasValue() && isAllSelected()"
[indeterminate]="selection.hasValue() && !isAllSelected()">
</mat-checkbox>
</th>
<td mat-cell *matCellDef="let idp">
<mat-checkbox color="primary" (click)="$event.stopPropagation()"
(change)="$event ? selection.toggle(idp) : null" [checked]="selection.isSelected(idp)">
</mat-checkbox>
</td>
</ng-container>
<div class="table-wrapper">
<table class="table" mat-table [dataSource]="dataSource">
<ng-container matColumnDef="select">
<th mat-header-cell *matHeaderCellDef>
<mat-checkbox color="primary" (change)="$event ? masterToggle() : null"
[checked]="selection.hasValue() && isAllSelected()"
[indeterminate]="selection.hasValue() && !isAllSelected()">
</mat-checkbox>
</th>
<td mat-cell *matCellDef="let idp">
<mat-checkbox color="primary" (click)="$event.stopPropagation()"
(change)="$event ? selection.toggle(idp) : null" [checked]="selection.isSelected(idp)">
</mat-checkbox>
</td>
</ng-container>
<ng-container matColumnDef="idpConfigId">
<th mat-header-cell *matHeaderCellDef> {{ 'USER.EXTERNALIDP.IDPCONFIGID' | translate }} </th>
<td mat-cell *matCellDef="let idp"> {{idp?.idpId}} </td>
</ng-container>
<ng-container matColumnDef="idpConfigId">
<th mat-header-cell *matHeaderCellDef> {{ 'USER.EXTERNALIDP.IDPCONFIGID' | translate }} </th>
<td mat-cell *matCellDef="let idp"> {{idp?.idpId}} </td>
</ng-container>
<ng-container matColumnDef="idpName">
<th mat-header-cell *matHeaderCellDef> {{ 'USER.EXTERNALIDP.IDPNAME' | translate }} </th>
<td mat-cell *matCellDef="let idp"> {{idp?.idpName}} </td>
</ng-container>
<ng-container matColumnDef="idpName">
<th mat-header-cell *matHeaderCellDef> {{ 'USER.EXTERNALIDP.IDPNAME' | translate }} </th>
<td mat-cell *matCellDef="let idp"> {{idp?.idpName}} </td>
</ng-container>
<ng-container matColumnDef="externalUserDisplayName">
<th mat-header-cell *matHeaderCellDef> {{ 'USER.EXTERNALIDP.USERDISPLAYNAME' | translate }} </th>
<td mat-cell *matCellDef="let idp"> {{idp?.providedUserName}} </td>
</ng-container>
<ng-container matColumnDef="externalUserDisplayName">
<th mat-header-cell *matHeaderCellDef> {{ 'USER.EXTERNALIDP.USERDISPLAYNAME' | translate }} </th>
<td mat-cell *matCellDef="let idp"> {{idp?.providedUserName}} </td>
</ng-container>
<ng-container matColumnDef="externalUserId">
<th mat-header-cell *matHeaderCellDef> {{ 'USER.EXTERNALIDP.EXTERNALUSERID' | translate }} </th>
<td mat-cell *matCellDef="let idp"> {{idp?.providedUserId}} </td>
</ng-container>
<ng-container matColumnDef="externalUserId">
<th mat-header-cell *matHeaderCellDef> {{ 'USER.EXTERNALIDP.EXTERNALUSERID' | translate }} </th>
<td mat-cell *matCellDef="let idp"> {{idp?.providedUserId}} </td>
</ng-container>
<ng-container matColumnDef="actions" stickyEnd>
<th mat-header-cell *matHeaderCellDef></th>
<td mat-cell *matCellDef="let idp">
<button color="warn" mat-icon-button matTooltip="{{'ACTIONS.REMOVE' | translate}}"
(click)="removeExternalIdp(idp)">
<i class="las la-trash"></i>
</button>
</td>
</ng-container>
<ng-container matColumnDef="actions" stickyEnd>
<th mat-header-cell *matHeaderCellDef></th>
<td mat-cell *matCellDef="let idp">
<cnsl-table-actions>
<button actions color="warn" mat-icon-button matTooltip="{{'ACTIONS.REMOVE' | translate}}"
(click)="removeExternalIdp(idp)">
<i class="las la-trash"></i>
</button>
</cnsl-table-actions>
</td>
</ng-container>
<tr mat-header-row *matHeaderRowDef="displayedColumns"></tr>
<tr class="highlight" mat-row *matRowDef="let row; columns: displayedColumns;">
</tr>
<tr mat-header-row *matHeaderRowDef="displayedColumns"></tr>
<tr class="highlight" mat-row *matRowDef="let row; columns: displayedColumns;">
</tr>
</table>
</div>
</table>
<cnsl-paginator #paginator class="paginator" [timestamp]="viewTimestamp" [length]="totalResult || 0" [pageSize]="10"
[pageSizeOptions]="[5, 10, 20]" (page)="changePage($event)"></cnsl-paginator>
</div>
<div *ngIf="(loading$ | async) === false && !dataSource?.data?.length" class="no-content-row">
<i class="las la-exclamation"></i>
<span>{{'USER.EXTERNALIDP.EMPTY' | translate}}</span>
</div>
<cnsl-paginator #paginator class="paginator" [timestamp]="viewTimestamp" [length]="totalResult || 0" [pageSize]="20"
[pageSizeOptions]="[10, 20, 50, 100]" (page)="changePage($event)"></cnsl-paginator>
</cnsl-refresh-table>
</cnsl-card>
@@ -4,29 +4,6 @@
}
}
.table-wrapper {
overflow: auto;
.table,
.paginator {
width: 100%;
td,
th {
padding: 0 1rem;
&:first-child {
padding-left: 0;
padding-right: 1rem;
}
&:last-child {
padding-right: 0;
}
}
}
}
tr {
outline: none;
@@ -1,5 +1,5 @@
import { SelectionModel } from '@angular/cdk/collections';
import { Component, Input, OnInit, ViewChild } from '@angular/core';
import { Component, Input, OnDestroy, OnInit, ViewChild } from '@angular/core';
import { MatDialog } from '@angular/material/dialog';
import { MatTableDataSource } from '@angular/material/table';
import { Timestamp } from 'google-protobuf/google/protobuf/timestamp_pb';
@@ -17,26 +17,34 @@ import { ToastService } from '../../../../services/toast.service';
templateUrl: './external-idps.component.html',
styleUrls: ['./external-idps.component.scss'],
})
export class ExternalIdpsComponent implements OnInit {
export class ExternalIdpsComponent implements OnInit, OnDestroy {
@Input() service!: GrpcAuthService | ManagementService;
@Input() userId!: string;
@ViewChild(PaginatorComponent) public paginator!: PaginatorComponent;
public totalResult: number = 0;
public viewTimestamp!: Timestamp.AsObject;
public dataSource: MatTableDataSource<IDPUserLink.AsObject>
= new MatTableDataSource<IDPUserLink.AsObject>();
public selection: SelectionModel<IDPUserLink.AsObject>
= new SelectionModel<IDPUserLink.AsObject>(true, []);
public dataSource: MatTableDataSource<IDPUserLink.AsObject> = new MatTableDataSource<IDPUserLink.AsObject>();
public selection: SelectionModel<IDPUserLink.AsObject> = new SelectionModel<IDPUserLink.AsObject>(true, []);
private loadingSubject: BehaviorSubject<boolean> = new BehaviorSubject<boolean>(false);
public loading$: Observable<boolean> = this.loadingSubject.asObservable();
@Input() public displayedColumns: string[] = ['idpConfigId', 'idpName', 'externalUserId', 'externalUserDisplayName', 'actions'];
@Input() public displayedColumns: string[] = [
'idpConfigId',
'idpName',
'externalUserId',
'externalUserDisplayName',
'actions',
];
constructor(private toast: ToastService, private dialog: MatDialog) { }
constructor(private toast: ToastService, private dialog: MatDialog) {}
ngOnInit(): void {
this.getData(10, 0);
}
ngOnDestroy(): void {
this.loadingSubject.complete();
}
public isAllSelected(): boolean {
const numSelected = this.selection.selected.length;
const numRows = this.dataSource.data.length;
@@ -44,9 +52,7 @@ export class ExternalIdpsComponent implements OnInit {
}
public masterToggle(): void {
this.isAllSelected() ?
this.selection.clear() :
this.dataSource.data.forEach(row => this.selection.select(row));
this.isAllSelected() ? this.selection.clear() : this.dataSource.data.forEach((row) => this.selection.select(row));
}
public changePage(event: PageEvent): void {
@@ -64,21 +70,23 @@ export class ExternalIdpsComponent implements OnInit {
}
if (promise) {
promise.then(resp => {
this.dataSource.data = resp.resultList;
if (resp.details?.viewTimestamp) {
this.viewTimestamp = resp.details.viewTimestamp;
}
if (resp.details?.totalResult) {
this.totalResult = resp.details?.totalResult;
} else {
this.totalResult = 0;
}
this.loadingSubject.next(false);
}).catch((error: any) => {
this.toast.showError(error);
this.loadingSubject.next(false);
});
promise
.then((resp) => {
this.dataSource.data = resp.resultList;
if (resp.details?.viewTimestamp) {
this.viewTimestamp = resp.details.viewTimestamp;
}
if (resp.details?.totalResult) {
this.totalResult = resp.details?.totalResult;
} else {
this.totalResult = 0;
}
this.loadingSubject.next(false);
})
.catch((error: any) => {
this.toast.showError(error);
this.loadingSubject.next(false);
});
}
}
@@ -97,25 +105,25 @@ export class ExternalIdpsComponent implements OnInit {
width: '400px',
});
dialogRef.afterClosed().subscribe(resp => {
dialogRef.afterClosed().subscribe((resp) => {
if (resp) {
let promise;
if (this.service instanceof ManagementService) {
promise = (this.service as ManagementService)
.removeHumanLinkedIDP(idp.idpId, idp.providedUserId, idp.userId);
promise = (this.service as ManagementService).removeHumanLinkedIDP(idp.idpId, idp.providedUserId, idp.userId);
} else if (this.service instanceof GrpcAuthService) {
promise = (this.service as GrpcAuthService)
.removeMyLinkedIDP(idp.idpId, idp.providedUserId);
promise = (this.service as GrpcAuthService).removeMyLinkedIDP(idp.idpId, idp.providedUserId);
}
if (promise) {
promise.then(_ => {
setTimeout(() => {
this.refreshPage();
}, 1000);
}).catch((error: any) => {
this.toast.showError(error);
});
promise
.then((_) => {
setTimeout(() => {
this.refreshPage();
}, 1000);
})
.catch((error: any) => {
this.toast.showError(error);
});
}
}
});
@@ -1,77 +0,0 @@
import { DataSource } from '@angular/cdk/collections';
import { Timestamp } from 'google-protobuf/google/protobuf/timestamp_pb';
import { BehaviorSubject, from, Observable, of } from 'rxjs';
import { catchError, finalize, map } from 'rxjs/operators';
import { Membership } from 'src/app/proto/generated/zitadel/user_pb';
import { GrpcAuthService } from 'src/app/services/grpc-auth.service';
import { ManagementService } from 'src/app/services/mgmt.service';
export class MembershipDetailDataSource extends DataSource<Membership.AsObject> {
public totalResult: number = 0;
public viewTimestamp!: Timestamp.AsObject;
public membersSubject: BehaviorSubject<Membership.AsObject[]>
= new BehaviorSubject<Membership.AsObject[]>([]);
private loadingSubject: BehaviorSubject<boolean> = new BehaviorSubject<boolean>(false);
public loading$: Observable<boolean> = this.loadingSubject.asObservable();
constructor(private mgmtUserService: ManagementService, private authServce: GrpcAuthService) {
super();
}
public loadMemberships(userId: string, pageIndex: number, pageSize: number): void {
const offset = pageIndex * pageSize;
this.loadingSubject.next(true);
from(this.mgmtUserService.listUserMemberships(userId, pageSize, offset)).pipe(
map(resp => {
this.totalResult = resp.details?.totalResult || 0;
if (resp.details?.viewTimestamp) {
this.viewTimestamp = resp.details.viewTimestamp;
}
return resp.resultList;
}),
catchError(() => of([])),
finalize(() => this.loadingSubject.next(false)),
).subscribe(members => {
this.membersSubject.next(members);
});
}
public loadMyMemberships(pageIndex: number, pageSize: number): void {
const offset = pageIndex * pageSize;
this.loadingSubject.next(true);
from(this.authServce.listMyMemberships(pageSize, offset)).pipe(
map(resp => {
this.totalResult = resp.details?.totalResult || 0;
if (resp.details?.viewTimestamp) {
this.viewTimestamp = resp.details.viewTimestamp;
}
return resp.resultList;
}),
catchError(() => of([])),
finalize(() => this.loadingSubject.next(false)),
).subscribe(members => {
this.membersSubject.next(members);
});
}
/**
* Connect this data source to the table. The table will only update when
* the returned stream emits new items.
* @returns A stream of the items to be rendered.
*/
public connect(): Observable<Membership.AsObject[]> {
return this.membersSubject.asObservable();
}
/**
* Called when the table is being destroyed. Use this function, to clean up
* any open connections or free any held resources that were set up during connect.
*/
public disconnect(): void {
this.membersSubject.complete();
this.loadingSubject.complete();
}
}
@@ -1,90 +0,0 @@
<cnsl-detail-layout [backRouterLink]="[ '/users', user?.id]"
title="{{ 'USER.MEMBERSHIPS.TITLE' | translate }}"
description="{{ 'USER.MEMBERSHIPS.DESCRIPTION' | translate }}">
<cnsl-info-section class="context-wrapper">
<span *ngIf="isMe">{{'USER.MEMBERSHIPS.USERCONTEXT' | translate}}</span>
<span *ngIf="!isMe">{{'USER.MEMBERSHIPS.ORGCONTEXT' | translate}}</span>
</cnsl-info-section>
<cnsl-refresh-table class="refresh-table" (refreshed)="refreshPage()" [dataSize]="dataSource?.totalResult ?? 0"
[timestamp]="dataSource?.viewTimestamp" [selection]="selection" [loading]="dataSource?.loading$ | async">
<a actions color="primary" (click)="addMember()" color="primary" mat-raised-button>
<mat-icon class="icon">add</mat-icon>{{ 'ACTIONS.NEW' | translate }}
</a>
<div class="table-wrapper">
<table mat-table class="table" aria-label="Elements" [dataSource]="dataSource">
<ng-container matColumnDef="select">
<th class="selection" mat-header-cell *matHeaderCellDef>
<mat-checkbox color="primary" (change)="$event ? masterToggle() : null"
[checked]="selection.hasValue() && isAllSelected()"
[indeterminate]="selection.hasValue() && !isAllSelected()">
</mat-checkbox>
</th>
<td class="selection" mat-cell *matCellDef="let row">
<mat-checkbox color="primary" (click)="$event.stopPropagation()"
(change)="$event ? selection.toggle(row) : null" [checked]="selection.isSelected(row)">
</mat-checkbox>
</td>
</ng-container>
<ng-container matColumnDef="memberType">
<th mat-header-cell *matHeaderCellDef> {{ 'USER.MEMBERSHIPS.TYPE' | translate }} </th>
<td class="pointer" mat-cell *matCellDef="let member">
<span *ngIf="member.orgId && !member.projectGrantId && !member.projectId">
{{'USER.MEMBERSHIPS.TYPES.ORG' | translate }}</span>
<span *ngIf="member.projectId && !member.projectGrantId"> {{'USER.MEMBERSHIPS.TYPES.PROJECT' |
translate }}</span>
<span *ngIf="member.projectId && member.projectGrantId">
{{'USER.MEMBERSHIPS.TYPES.GRANTEDPROJECT' | translate }}</span>
</td>
</ng-container>
<ng-container matColumnDef="displayName">
<th mat-header-cell *matHeaderCellDef> {{ 'USER.MEMBERSHIPS.DISPLAYNAME' | translate }} </th>
<td class="pointer" mat-cell *matCellDef="let member">
{{member.displayName}} </td>
</ng-container>
<ng-container matColumnDef="creationDate">
<th mat-header-cell *matHeaderCellDef> {{ 'USER.MEMBERSHIPS.CREATIONDATE' | translate }} </th>
<td class="pointer" mat-cell *matCellDef="let member">
{{member.details?.creationDate | timestampToDate | localizedDate: 'EEE dd. MMM, HH:mm'}} </td>
</ng-container>
<ng-container matColumnDef="changeDate">
<th mat-header-cell *matHeaderCellDef> {{ 'USER.MEMBERSHIPS.CHANGEDATE' | translate }} </th>
<td class="pointer" mat-cell *matCellDef="let member">
{{member.details?.changeDate | timestampToDate | localizedDate: 'EEE dd. MMM, HH:mm'}}
</td>
</ng-container>
<ng-container matColumnDef="roles">
<th mat-header-cell *matHeaderCellDef> {{ 'PROJECT.MEMBER.ROLES' | translate }} </th>
<td class="pointer" mat-cell *matCellDef="let member">
{{member.rolesList.join(', ')}}
</td>
</ng-container>
<ng-container matColumnDef="actions" stickyEnd>
<th mat-header-cell *matHeaderCellDef></th>
<td mat-cell *matCellDef="let member">
<button [disabled]="member.projectId && member.projectGrantId ? (['project.grant.member.delete:' + member.projectGrantId, 'project.grant.member.delete'] | hasRole | async) === false : member.projectId ? (['project.member.delete:' + member.projectGrantId, 'project.member.delete'] | hasRole | async) === false : member.orgId ? (['org.member.delete:' + member.orgId, 'org.member.delete'] | hasRole | async) === false : (['iam.member.delete'] | hasRole | async) === false"
mat-icon-button color="warn" matTooltip="{{'ACTIONS.REMOVE' | translate}}"
(click)="removeMembership(member)">
<i class="las la-minus-circle"></i>
</button>
</td>
</ng-container>
<tr mat-header-row *matHeaderRowDef="displayedColumns"></tr>
<tr class="highlight" mat-row *matRowDef="let row; columns: displayedColumns;">
</tr>
</table>
<cnsl-paginator class="paginator" #paginator [timestamp]="dataSource?.viewTimestamp" [pageSize]="50" [pageSizeOptions]="[25, 50, 100, 250]">
</cnsl-paginator>
</div>
</cnsl-refresh-table>
</cnsl-detail-layout>
@@ -1,55 +0,0 @@
.context-wrapper {
margin-bottom: 1rem;
display: block;
}
.refresh-table {
width: 100%;
}
.table-wrapper {
width: 100%;
overflow: auto;
.table,
.paginator {
width: 100%;
td,
th {
padding: .5rem;
&:first-child {
padding-left: 0;
padding-right: 1rem;
}
&:last-child {
padding-right: 0;
}
}
.action {
width: 40px;
}
}
}
.pointer {
outline: none;
cursor: pointer;
}
tr {
outline: none;
button {
visibility: hidden;
}
&:hover {
button {
visibility: visible;
}
}
}
@@ -1,25 +0,0 @@
import { ComponentFixture, TestBed, waitForAsync } from '@angular/core/testing';
import { MembershipDetailComponent } from './membership-detail.component';
describe('MembershipDetailComponent', () => {
let component: MembershipDetailComponent;
let fixture: ComponentFixture<MembershipDetailComponent>;
beforeEach(waitForAsync(() => {
TestBed.configureTestingModule({
declarations: [MembershipDetailComponent],
})
.compileComponents();
}));
beforeEach(() => {
fixture = TestBed.createComponent(MembershipDetailComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should create', () => {
expect(component).toBeTruthy();
});
});
@@ -1,259 +0,0 @@
import { SelectionModel } from '@angular/cdk/collections';
import { AfterViewInit, Component, ViewChild } from '@angular/core';
import { MatDialog } from '@angular/material/dialog';
import { MatTable } from '@angular/material/table';
import { ActivatedRoute } from '@angular/router';
import { map, switchMap, tap } from 'rxjs/operators';
import { CreationType, MemberCreateDialogComponent } from 'src/app/modules/add-member-dialog/member-create-dialog.component';
import { PaginatorComponent } from 'src/app/modules/paginator/paginator.component';
import { Membership, User } from 'src/app/proto/generated/zitadel/user_pb';
import { AdminService } from 'src/app/services/admin.service';
import { GrpcAuthService } from 'src/app/services/grpc-auth.service';
import { ManagementService } from 'src/app/services/mgmt.service';
import { ToastService } from 'src/app/services/toast.service';
import { MembershipDetailDataSource } from './membership-detail-datasource';
@Component({
selector: 'cnsl-membership-detail',
templateUrl: './membership-detail.component.html',
styleUrls: ['./membership-detail.component.scss'],
})
export class MembershipDetailComponent implements AfterViewInit {
public user!: User.AsObject;
@ViewChild(PaginatorComponent) public paginator!: PaginatorComponent;
@ViewChild(MatTable) public table!: MatTable<Membership.AsObject>;
public dataSource!: MembershipDetailDataSource;
public selection: SelectionModel<Membership.AsObject>
= new SelectionModel<Membership.AsObject>(true, []);
public memberRoleOptions: string[] = [];
/** Columns displayed in the table. Columns IDs can be added, removed, or reordered. */
public displayedColumns: string[] = ['select', 'memberType', 'displayName', 'creationDate', 'changeDate', 'roles', 'actions'];
public loading: boolean = false;
public memberships!: Membership.AsObject[];
public isMe: boolean = false;
constructor(
authService: GrpcAuthService,
activatedRoute: ActivatedRoute,
private dialog: MatDialog,
private toast: ToastService,
private mgmtService: ManagementService,
private adminService: AdminService,
) {
activatedRoute.params
.pipe(switchMap(({ id }) => {
return authService.user.pipe(
map(user => user?.id === id),
tap((isMe) => {
this.isMe = isMe;
if (isMe) {
this.mgmtService.getUserByID(id).then(resp => {
if (resp.user) {
this.user = resp.user;
this.dataSource = new MembershipDetailDataSource(this.mgmtService, authService);
this.dataSource.loadMyMemberships(
0,
50,
);
}
}).catch(err => {
console.error(err);
});
} else {
this.mgmtService.getUserByID(id).then(resp => {
if (resp.user) {
this.user = resp.user;
this.dataSource = new MembershipDetailDataSource(this.mgmtService, authService);
this.dataSource.loadMemberships(
this.user.id,
0,
50,
);
}
}).catch(err => {
console.error(err);
});
}
}),
);
})).subscribe();
}
public ngAfterViewInit(): void {
this.paginator.page
.pipe(
tap(() => this.loadMembershipsPage()),
)
.subscribe();
}
private loadMembershipsPage(): void {
this.dataSource.loadMemberships(
this.user.id,
this.paginator.pageIndex,
this.paginator.pageSize,
);
}
public isAllSelected(): boolean {
const numSelected = this.selection.selected.length;
const numRows = this.dataSource.membersSubject.value.length;
return numSelected === numRows;
}
public masterToggle(): void {
this.isAllSelected() ?
this.selection.clear() :
this.dataSource.membersSubject.value.forEach(row => this.selection.select(row));
}
public addMember(): void {
const dialogRef = this.dialog.open(MemberCreateDialogComponent, {
width: '400px',
data: {
user: this.user,
},
});
dialogRef.afterClosed().subscribe(resp => {
if (resp && resp.creationType !== undefined) {
switch (resp.creationType) {
case CreationType.IAM:
this.createIamMember(resp);
break;
case CreationType.ORG:
this.createOrgMember(resp);
break;
case CreationType.PROJECT_OWNED:
this.createOwnedProjectMember(resp);
break;
case CreationType.PROJECT_GRANTED:
this.createGrantedProjectMember(resp);
break;
}
}
});
}
public async loadManager(userId: string): Promise<void> {
this.mgmtService.listUserMemberships(userId, 100, 0, []).then(response => {
this.memberships = response.resultList;
this.loading = false;
});
}
public createIamMember(response: any): void {
const users: User.AsObject[] = response.users;
const roles: string[] = response.roles;
if (users && users.length && roles && roles.length) {
Promise.all(users.map(user => {
return this.adminService.addIAMMember(user.id, roles);
})).then(() => {
this.toast.showInfo('IAM.TOAST.MEMBERADDED', true);
setTimeout(() => {
this.refreshPage();
}, 1000);
}).catch(error => {
this.toast.showError(error);
});
}
}
private createOrgMember(response: any): void {
const users: User.AsObject[] = response.users;
const roles: string[] = response.roles;
if (users && users.length && roles && roles.length) {
Promise.all(users.map(user => {
return this.mgmtService.addOrgMember(user.id, roles);
})).then(() => {
this.toast.showInfo('ORG.TOAST.MEMBERADDED', true);
setTimeout(() => {
this.refreshPage();
}, 1000);
}).catch(error => {
this.toast.showError(error);
});
}
}
private createGrantedProjectMember(response: any): void {
const users: User.AsObject[] = response.users;
const roles: string[] = response.roles;
if (users && users.length && roles && roles.length) {
users.forEach(user => {
return this.mgmtService.addProjectGrantMember(
response.projectId,
response.grantId,
user.id,
roles,
).then(() => {
this.toast.showInfo('PROJECT.TOAST.MEMBERADDED', true);
setTimeout(() => {
this.refreshPage();
}, 1000);
}).catch(error => {
this.toast.showError(error);
});
});
}
}
private createOwnedProjectMember(response: any): void {
const users: User.AsObject[] = response.users;
const roles: string[] = response.roles;
if (users && users.length && roles && roles.length) {
users.forEach(user => {
return this.mgmtService.addProjectMember(response.projectId, user.id, roles)
.then(() => {
this.toast.showInfo('PROJECT.TOAST.MEMBERADDED', true);
setTimeout(() => {
this.refreshPage();
}, 1000);
}).catch(error => {
this.toast.showError(error);
});
});
}
}
public removeMembership(membership: Membership.AsObject): void {
let prom;
if (membership.projectId && membership.projectGrantId && membership.userId) {
prom = this.mgmtService.removeProjectGrantMember(
membership.projectId,
membership.projectGrantId,
membership.userId,
);
} else if (membership.projectId && membership.userId) {
prom = this.mgmtService.removeProjectMember(membership.projectId, membership.userId);
} else if (membership.orgId && membership.userId) {
prom = this.mgmtService.removeOrgMember(membership.userId);
} else if (membership.userId) {
prom = this.adminService.removeIAMMember(membership.userId);
}
if (prom) {
prom.then(() => {
this.toast.showInfo('PROJECT.TOAST.MEMBERREMOVED', true);
this.refreshPage();
}).catch(error => this.toast.showError(error));
}
}
public refreshPage(): void {
this.selection.clear();
this.dataSource.loadMemberships(this.user.id, this.paginator.pageIndex, this.paginator.pageSize);
}
}
@@ -1,54 +0,0 @@
import { CommonModule } from '@angular/common';
import { NgModule } from '@angular/core';
import { MatButtonModule } from '@angular/material/button';
import { MatCheckboxModule } from '@angular/material/checkbox';
import { MatIconModule } from '@angular/material/icon';
import { MatProgressSpinnerModule } from '@angular/material/progress-spinner';
import { MatTableModule } from '@angular/material/table';
import { MatTooltipModule } from '@angular/material/tooltip';
import { RouterModule, Routes } from '@angular/router';
import { TranslateModule } from '@ngx-translate/core';
import { HasRoleModule } from 'src/app/directives/has-role/has-role.module';
import { DetailLayoutModule } from 'src/app/modules/detail-layout/detail-layout.module';
import { InfoSectionModule } from 'src/app/modules/info-section/info-section.module';
import { PaginatorModule } from 'src/app/modules/paginator/paginator.module';
import { RefreshTableModule } from 'src/app/modules/refresh-table/refresh-table.module';
import { HasRolePipeModule } from 'src/app/pipes/has-role-pipe/has-role-pipe.module';
import { LocalizedDatePipeModule } from 'src/app/pipes/localized-date-pipe/localized-date-pipe.module';
import { TimestampToDatePipeModule } from 'src/app/pipes/timestamp-to-date-pipe/timestamp-to-date-pipe.module';
import { MembershipDetailComponent } from './membership-detail.component';
const routes: Routes = [
{
path: '',
component: MembershipDetailComponent,
canActivate: [],
data: {
roles: ['user.write'],
},
},
];
@NgModule({
declarations: [MembershipDetailComponent],
imports: [
CommonModule,
RouterModule.forChild(routes),
TranslateModule,
DetailLayoutModule,
MatCheckboxModule,
MatTableModule,
PaginatorModule,
MatProgressSpinnerModule,
LocalizedDatePipeModule,
TimestampToDatePipeModule,
HasRoleModule,
MatIconModule,
MatButtonModule,
HasRolePipeModule,
RefreshTableModule,
MatTooltipModule,
InfoSectionModule,
],
})
export class MembershipDetailModule { }
@@ -1,40 +0,0 @@
<div class="membership-groups">
<span class="me-header">{{ 'USER.MEMBERSHIPS.TITLE' | translate }}</span>
<div class="people" *ngIf="memberships">
<div class="img-list" [@cardAnimation]="totalResult">
<mat-spinner class="spinner" diameter="20" *ngIf="loading"></mat-spinner>
<ng-container *ngIf="totalResult < 8 && memberships && memberships.length; else compact">
<ng-container *ngFor="let membership of memberships; index as i">
<div @animate class="avatar-circle" (click)="navigateToObject()"
matTooltip="{{ membership.displayName }} | {{membership.rolesList?.join(' ')}}"
[ngStyle]="{'z-index': 100 - i}">
<div class="membership-avatar" [ngStyle]="{'background-color': getColor(membership)}">
<i *ngIf="membership.orgId" class="las la-archway"></i>
<i *ngIf="membership.projectId && !membership.projectGrantId" class="icon las la-layer-group"></i>
<i *ngIf="membership.projectId && membership.projectGrantId" class="icon las la-layer-group"></i>
<span>{{membership.displayName}}</span>
</div>
</div>
</ng-container>
</ng-container>
<ng-template #compact>
<div class="avatar-circle" matTooltip="Click to show detail" (click)="navigateToObject()" role="button">
<div class="membership-avatar">
<span style="font-size: 16px;">{{totalResult}}</span>
</div>
</div>
</ng-template>
<button [disabled]="disabled" class="add-img" (click)="addMember()" mat-icon-button
matTooltip="{{'ACTIONS.ADD' | translate}}" aria-label="add membership">
<mat-icon>add</mat-icon>
</button>
<span class="fill-space"></span>
<button matTooltip="{{'ACTIONS.REFRESH' | translate}}" class="refresh-img" (click)="loadManager(user.id)"
[disabled]="disabled" mat-icon-button aria-label="refresh contributors">
<mat-icon class="icon">refresh</mat-icon>
</button>
</div>
</div>
</div>
@@ -1,124 +0,0 @@
@use '@angular/material' as mat;
@mixin membership-theme($theme) {
/* stylelint-disable */
$primary: map-get($theme, primary);
$is-dark-theme: map-get($theme, is-dark);
$primary-color: mat.get-color-from-palette($primary, 500);
/* stylelint-enable */
.membership-groups {
.me-header {
display: block;
margin-bottom: 1rem;
font-weight: 400;
margin-top: 0;
font-size: 14px;
letter-spacing: .05em;
text-transform: uppercase;
}
.sub-header {
font-size: .8rem;
color: var(--grey);
}
.people {
display: flex;
align-items: center;
flex-wrap: wrap;
margin-bottom: 1rem;
.owner {
margin-right: 1rem;
}
.img-list {
width: 100%;
margin-left: 1rem;
display: flex;
align-items: center;
.spinner {
margin-left: -15px;
margin-right: 20px;
}
.add-img {
float: left;
margin: 0 8px 0 -15px;
}
.fill-space {
flex: 1;
}
.refresh-img {
float: left;
margin: 0 0 0 -15px;
.icon {
font-size: 1.2rem;
}
}
.avatar-circle {
float: left;
margin: 0 8px 0 -12px;
height: 40px;
width: 40px;
border-radius: 50%;
-webkit-box-shadow: 2px 0 7px -1px rgba(33, 34, 36, .5);
-moz-box-shadow: 2px 0 7px -1px rgba(33, 34, 36, .5);
box-shadow: 2px 0 7px -1px rgba(33, 34, 36, .5);
display: flex;
align-items: center;
justify-content: center;
.membership-avatar {
display: flex;
align-items: center;
justify-content: center;
text-transform: uppercase;
color: white;
outline: none;
padding: 3px;
text-align: center;
height: 40px;
width: 40px;
box-sizing: border-box;
font-size: 8px;
border-radius: .5rem;
transition: background-color .3s cubic-bezier(.645, .045, .355, 1);
background-color: $primary-color;
cursor: pointer;
flex-direction: column;
overflow: hidden;
span {
max-width: 30px;
text-overflow: ellipsis;
font-weight: 300;
display: block;
white-space: nowrap;
overflow: hidden;
letter-spacing: .05em;
}
i {
font-size: 1.2rem;
flex-basis: 100%;
width: 100%;
height: 1.2rem;
max-height: 1.2rem;
}
}
}
.margin-neg {
margin-left: -1rem;
}
}
}
}
}
@@ -1,25 +0,0 @@
import { ComponentFixture, TestBed, waitForAsync } from '@angular/core/testing';
import { MembershipsComponent } from './memberships.component';
describe('MembershipsComponent', () => {
let component: MembershipsComponent;
let fixture: ComponentFixture<MembershipsComponent>;
beforeEach(waitForAsync(() => {
TestBed.configureTestingModule({
declarations: [MembershipsComponent],
})
.compileComponents();
}));
beforeEach(() => {
fixture = TestBed.createComponent(MembershipsComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should create', () => {
expect(component).toBeTruthy();
});
});
@@ -1,205 +0,0 @@
import { animate, animateChild, keyframes, query, stagger, style, transition, trigger } from '@angular/animations';
import { Component, Input, OnInit } from '@angular/core';
import { MatDialog } from '@angular/material/dialog';
import { Router } from '@angular/router';
import { CreationType, MemberCreateDialogComponent } from 'src/app/modules/add-member-dialog/member-create-dialog.component';
import { UserGrant } from 'src/app/proto/generated/zitadel/auth_pb';
import { Membership, User } from 'src/app/proto/generated/zitadel/user_pb';
import { AdminService } from 'src/app/services/admin.service';
import { GrpcAuthService } from 'src/app/services/grpc-auth.service';
import { ManagementService } from 'src/app/services/mgmt.service';
import { ToastService } from 'src/app/services/toast.service';
@Component({
selector: 'cnsl-memberships',
templateUrl: './memberships.component.html',
styleUrls: ['./memberships.component.scss'],
animations: [
trigger('cardAnimation', [
transition('* => *', [
query('@animate', stagger('40ms', animateChild()), { optional: true }),
]),
]),
trigger('animate', [
transition(':enter', [
animate('.2s ease-in', keyframes([
style({ opacity: 0, offset: 0 }),
style({ opacity: .5, transform: 'scale(1.05)', offset: 0.3 }),
style({ opacity: 1, transform: 'scale(1)', offset: 1 }),
])),
]),
]),
],
})
export class MembershipsComponent implements OnInit {
public loading: boolean = false;
public memberships: Membership.AsObject[] = [];
public totalResult: number = 0;
@Input() public auth: boolean = false;
@Input() public user!: User.AsObject;
@Input() public disabled: boolean = false;
constructor(
private authService: GrpcAuthService,
private mgmtService: ManagementService,
private adminService: AdminService,
private dialog: MatDialog,
private toast: ToastService,
private router: Router,
) { }
ngOnInit(): void {
this.loadManager(this.user.id);
}
public async loadManager(userId: string): Promise<void> {
if (this.auth) {
this.authService.listMyMemberships(100, 0, []).then(resp => {
this.memberships = resp.resultList;
this.totalResult = resp.details?.totalResult || 0;
this.loading = false;
});
} else {
this.mgmtService.listUserMemberships(userId, 100, 0, []).then(resp => {
this.memberships = resp.resultList;
this.totalResult = resp.details?.totalResult || 0;
this.loading = false;
});
}
}
public navigateToObject(): void {
if (!this.disabled) {
this.router.navigate(['/users', this.user.id, 'memberships']);
}
}
public addMember(): void {
const dialogRef = this.dialog.open(MemberCreateDialogComponent, {
width: '400px',
data: {
user: this.user,
},
});
dialogRef.afterClosed().subscribe(resp => {
if (resp && resp.creationType !== undefined) {
switch (resp.creationType) {
case CreationType.IAM:
this.createIamMember(resp);
break;
case CreationType.ORG:
this.createOrgMember(resp);
break;
case CreationType.PROJECT_OWNED:
this.createOwnedProjectMember(resp);
break;
case CreationType.PROJECT_GRANTED:
this.createGrantedProjectMember(resp);
break;
}
}
});
}
public createIamMember(response: any): void {
const users: User.AsObject[] = response.users;
const roles: string[] = response.roles;
if (users && users.length && roles && roles.length) {
Promise.all(users.map(user => {
return this.adminService.addIAMMember(user.id, roles);
})).then(() => {
this.toast.showInfo('IAM.TOAST.MEMBERADDED', true);
setTimeout(() => {
this.loadManager(this.user.id);
}, 1000);
}).catch(error => {
this.toast.showError(error);
});
}
}
private createOrgMember(response: any): void {
const users: User.AsObject[] = response.users;
const roles: string[] = response.roles;
if (users && users.length && roles && roles.length) {
Promise.all(users.map(user => {
return this.mgmtService.addOrgMember(user.id, roles);
})).then(() => {
this.toast.showInfo('ORG.TOAST.MEMBERADDED', true);
setTimeout(() => {
this.loadManager(this.user.id);
}, 1000);
}).catch(error => {
this.toast.showError(error);
});
}
}
private createGrantedProjectMember(response: any): void {
const users: User.AsObject[] = response.users;
const roles: string[] = response.roles;
if (users && users.length && roles && roles.length) {
users.forEach(user => {
return this.mgmtService.addProjectGrantMember(
response.projectId,
response.grantId,
user.id,
roles,
).then(() => {
this.toast.showInfo('PROJECT.TOAST.MEMBERADDED', true);
setTimeout(() => {
this.loadManager(this.user.id);
}, 1000);
}).catch(error => {
this.toast.showError(error);
});
});
}
}
private createOwnedProjectMember(response: any): void {
const users: User.AsObject[] = response.users;
const roles: string[] = response.roles;
if (users && users.length && roles && roles.length) {
users.forEach(user => {
return this.mgmtService.addProjectMember(response.projectId, user.id, roles)
.then(() => {
this.toast.showInfo('PROJECT.TOAST.MEMBERADDED', true);
setTimeout(() => {
this.loadManager(this.user.id);
}, 1000);
}).catch(error => {
this.toast.showError(error);
});
});
}
}
getColor(type: Membership.AsObject | UserGrant.AsObject): string {
const gen = type.toString();
const colors = [
'rgb(201, 115, 88)',
'rgb(226, 176, 50)',
'rgb(112, 89, 152)',
];
let hash = 0;
if (gen.length === 0) {
return colors[hash];
}
for (let i = 0; i < gen.length; i++) {
// eslint-disable-next-line no-bitwise
hash = gen.charCodeAt(i) + ((hash << 5) - hash);
// eslint-disable-next-line no-bitwise
hash = hash & hash;
}
hash = ((hash % colors.length) + colors.length) % colors.length;
return colors[hash];
}
}
@@ -1,40 +1,42 @@
<div mat-dialog-title class="title-row">
<h1 class="title">{{'USER.METADATA.TITLE' | translate}}</h1>
<span class="fill-space"></span>
<p *ngIf="ts" class="ts">{{ts | timestampToDate | localizedDate: 'dd. MMM, HH:mm' }}</p>
<p *ngIf="ts" class="ts cnsl-secondary-text">{{ts | timestampToDate | localizedDate: 'dd. MMM, HH:mm' }}</p>
<mat-spinner *ngIf="loading" diameter="20"></mat-spinner>
<button class="icon-button" mat-icon-button (click)="load()"><mat-icon class="icon">refresh</mat-icon></button>
<button class="icon-button" mat-icon-button (click)="load()">
<mat-icon class="icon">refresh</mat-icon>
</button>
</div>
<p class="desc">{{'USER.METADATA.DESCRIPTION' | translate}}</p>
<div mat-dialog-content>
<form *ngFor="let md of metadata; index as i" (ngSubmit)="saveElement(i)" >
<div class="content">
<cnsl-form-field #key id="key{{i}}" class="formfield">
<form *ngFor="let md of metadata; index as i" (ngSubmit)="saveElement(i)">
<div class="content">
<cnsl-form-field #key id="key{{i}}" class="formfield">
<cnsl-label>{{ 'USER.METADATA.KEY' | translate }}</cnsl-label>
<input cnslInput [(ngModel)]="md.key" [ngModelOptions]="{standalone: true}" />
</cnsl-form-field>
<cnsl-form-field #value id="value{{i}}" class="formfield">
<input cnslInput [(ngModel)]="md.key" [ngModelOptions]="{standalone: true}" />
</cnsl-form-field>
<cnsl-form-field #value id="value{{i}}" class="formfield">
<cnsl-label>{{ 'USER.METADATA.VALUE' | translate }}</cnsl-label>
<input cnslInput [(ngModel)]="md.value" [ngModelOptions]="{standalone: true}" />
</cnsl-form-field>
<input cnslInput [(ngModel)]="md.value" [ngModelOptions]="{standalone: true}" />
</cnsl-form-field>
<button mat-icon-button [disabled]="!(md.key && md.value)" class="set-button" type="submit" color="primary"
<button mat-icon-button [disabled]="!(md.key && md.value)" class="set-button" type="submit" color="primary"
matTooltip="{{ 'ACTIONS.SAVE' | translate }}">
<i class="las la-save"></i>
</button>
<button mat-icon-button (click)="removeEntry(i)" [disabled]="metadata.length < 2 && i === 0 && !md.key" class="rm-button" type="button" color="warn"
matTooltip="{{ 'ACTIONS.REMOVE' | translate }}">
</button>
<button mat-icon-button (click)="removeEntry(i)" [disabled]="metadata.length < 2 && i === 0 && !md.key"
class="rm-button" type="button" color="warn" matTooltip="{{ 'ACTIONS.REMOVE' | translate }}">
<i class="las la-trash"></i>
</button>
</div>
</form>
<button color="primary" (click)="addEntry()" mat-stroked-button color="primary" class="continue-button" type="button">
<mat-icon>add</mat-icon>
{{ 'ACTIONS.ADD' | translate }}
</button>
</button>
</div>
</form>
<button color="primary" (click)="addEntry()" mat-stroked-button color="primary" class="continue-button" type="button">
<mat-icon>add</mat-icon>
{{ 'ACTIONS.ADD' | translate }}
</button>
</div>
<div mat-dialog-actions class="action">
<button cdkFocusInitial color="primary" mat-stroked-button class="ok-button" (click)="closeDialog()">
{{'ACTIONS.CLOSE' | translate}}
{{'ACTIONS.CLOSE' | translate}}
</button>
</div>
@@ -14,7 +14,6 @@
.ts {
margin: 0;
font-size: 14px;
color: var(--grey);
}
.icon-button {
@@ -27,11 +26,11 @@
.content {
display: flex;
align-items: center;
margin: 0 -.5rem;
margin: 0 -0.5rem;
.formfield {
flex: 1;
margin: 0 .5rem;
margin: 0 0.5rem;
@media only screen and (max-width: 450px) {
flex-basis: 100%;
@@ -49,10 +48,10 @@
justify-content: flex-end;
.ok-button {
margin-left: .5rem;
margin-left: 0.5rem;
}
button {
border-radius: .5rem;
border-radius: 0.5rem;
}
}
@@ -5,7 +5,6 @@ import { Metadata } from 'src/app/proto/generated/zitadel/metadata_pb';
import { ManagementService } from 'src/app/services/mgmt.service';
import { ToastService } from 'src/app/services/toast.service';
@Component({
selector: 'cnsl-metadata-dialog',
templateUrl: './metadata-dialog.component.html',
@@ -21,31 +20,34 @@ export class MetadataDialogComponent {
private service: ManagementService,
private toast: ToastService,
public dialogRef: MatDialogRef<MetadataDialogComponent>,
@Inject(MAT_DIALOG_DATA) public data: any) {
@Inject(MAT_DIALOG_DATA) public data: any,
) {
this.injData = data;
this.load();
}
public load(): void {
this.loadMetadata().then(() => {
this.loading = false;
if (this.metadata.length === 0) {
this.addEntry();
}
}).catch(error => {
this.loading = false;
this.toast.showError(error);
if (this.metadata.length === 0) {
this.addEntry();
}
});
this.loadMetadata()
.then(() => {
this.loading = false;
if (this.metadata.length === 0) {
this.addEntry();
}
})
.catch((error) => {
this.loading = false;
this.toast.showError(error);
if (this.metadata.length === 0) {
this.addEntry();
}
});
}
public loadMetadata(): Promise<void> {
this.loading = true;
if (this.injData.userId) {
return this.service.listUserMetadata(this.injData.userId).then(resp => {
this.metadata = resp.resultList.map(md => {
return this.service.listUserMetadata(this.injData.userId).then((resp) => {
this.metadata = resp.resultList.map((md) => {
return {
key: md.key,
value: atob(md.value as string),
@@ -90,22 +92,25 @@ export class MetadataDialogComponent {
}
public setMetadata(key: string, value: string): void {
console.log(key, value, this.injData.userId);
if (key && value) {
this.service.setUserMetadata(key, btoa(value), this.injData.userId)
this.service
.setUserMetadata(key, btoa(value), this.injData.userId)
.then(() => {
this.toast.showInfo('USER.METADATA.SETSUCCESS', true);
}).catch(error => {
})
.catch((error) => {
this.toast.showError(error);
});
}
}
public removeMetadata(key: string): Promise<void> {
return this.service.removeUserMetadata(key, this.injData.userId)
return this.service
.removeUserMetadata(key, this.injData.userId)
.then((resp) => {
this.toast.showInfo('USER.METADATA.REMOVESUCCESS', true);
}).catch(error => {
})
.catch((error) => {
this.toast.showError(error);
});
}
@@ -1,21 +1,22 @@
<cnsl-card class="metadata-details" title="{{ 'USER.METADATA.TITLE' | translate }}">
<div card-actions class="actions">
<mat-spinner class="spinner" diameter="20" *ngIf="loading"></mat-spinner>
<button mat-raised-button color="primary" class="edit" (click)="editMetadata()">{{'ACTIONS.EDIT' | translate}}</button>
<button matTooltip="{{'ACTIONS.REFRESH' | translate}}" class="refresh-btn" (click)="loadMetadata()"
mat-icon-button aria-label="refresh contributors">
<mat-icon class="icon">refresh</mat-icon>
<button mat-raised-button color="primary" class="edit" (click)="editMetadata()">{{'ACTIONS.EDIT' |
translate}}</button>
<button matTooltip="{{'ACTIONS.REFRESH' | translate}}" class="refresh-btn" (click)="loadMetadata()" mat-icon-button
aria-label="refresh contributors">
<mat-icon class="icon">refresh</mat-icon>
</button>
</div>
<ng-container *ngIf="metadata?.length; else emptyList">
<div class="metadata-set" *ngFor="let md of metadata">
<span class="first">{{md.key}}</span>
<span class="second">{{ md.value }}</span>
<span class="first cnsl-secondary-text">{{md.key}}</span>
<span class="second">{{ md.value }}</span>
</div>
</ng-container>
<ng-template #emptyList>
<p class="empty-desc">{{'USER.METADATA.EMPTY' | translate}}</p>
<p class="empty-desc cnsl-secondary-text">{{'USER.METADATA.EMPTY' | translate}}</p>
</ng-template>
</cnsl-card>
</cnsl-card>
@@ -12,13 +12,13 @@
.meta-row {
display: flex;
margin-bottom: .5rem;
margin-bottom: 0.5rem;
align-items: center;
.first {
flex: 1;
font-size: 13px;
margin-right: .5rem;
margin-right: 0.5rem;
}
.fill-space {
@@ -33,11 +33,10 @@
.metadata-set {
display: flex;
flex-direction: column;
margin-bottom: .5rem;
margin-bottom: 0.5rem;
.first {
font-size: 14px;
color: var(--grey);
}
}
}
@@ -54,13 +53,6 @@
.empty-desc {
margin: 0;
font-size: 14px;
color: var(--grey);
}
.ts {
font-size: 14px;
color: var(--grey);
margin-top: 0;
}
.refresh-btn {
@@ -1,76 +1,73 @@
<cnsl-detail-layout [backRouterLink]="userId ? [ '/users',userId] : [ '/users/me']"
title="{{ 'USER.PASSWORD.TITLE' | translate }}" description="{{ 'USER.PASSWORD.DESCRIPTION' | translate }}">
<cnsl-detail-layout [hasBackButton]="true" title="{{ 'USER.PASSWORD.TITLE' | translate }}"
description="{{ 'USER.PASSWORD.DESCRIPTION' | translate }}">
<ng-container *ngIf="userId; else authUser">
<div class="validation" *ngIf="this.policy">
<cnsl-password-complexity-view [policy]="this.policy" [password]="password">
</cnsl-password-complexity-view>
<ng-container *ngIf="userId; else authUser">
<div class="validation" *ngIf="this.policy">
<cnsl-password-complexity-view [policy]="this.policy" [password]="password">
</cnsl-password-complexity-view>
</div>
<form *ngIf="passwordForm" autocomplete="new-password" [formGroup]="passwordForm"
(ngSubmit)="setInitialPassword(userId)">
<div class="content center">
<cnsl-form-field class="formfield">
<cnsl-label>{{ 'USER.PASSWORD.NEW' | translate }}</cnsl-label>
<input cnslInput autocomplete="off" name="password" formControlName="password" type="password" />
<span cnslError *ngIf="password?.errors?.required">
{{ 'USER.VALIDATION.REQUIRED' | translate }}
</span>
</cnsl-form-field>
<cnsl-form-field class="formfield">
<cnsl-label>{{ 'USER.PASSWORD.CONFIRM' | translate }}</cnsl-label>
<input cnslInput autocomplete="off" name="passwordRepeat" formControlName="confirmPassword" type="password" />
<span cnslError *ngIf="confirmPassword?.errors?.notequal">
{{ 'USER.PASSWORD.NOTEQUAL' | translate }}
</span>
</cnsl-form-field>
</div>
<div class="btn-container">
<button class="submit-button" [disabled]="passwordForm.invalid" mat-raised-button color="primary">{{
'USER.PASSWORD.SET' | translate }}</button>
</div>
</form>
</ng-container>
<ng-template #authUser>
<form *ngIf="passwordForm" [formGroup]="passwordForm" (ngSubmit)="setPassword()">
<div class="content">
<cnsl-form-field class="formfield">
<cnsl-label>{{ 'USER.PASSWORD.OLD' | translate }}</cnsl-label>
<input cnslInput autocomplete="off" name="password" type="password" formControlName="currentPassword" />
<span cnslError *ngIf="currentPassword?.errors?.required">
{{ 'USER.PASSWORD.REQUIRED' | translate }}
</span>
</cnsl-form-field>
<div class="validation between" *ngIf="this.policy">
<cnsl-password-complexity-view [policy]="this.policy" [password]="newPassword">
</cnsl-password-complexity-view>
</div>
<form *ngIf="passwordForm" autocomplete="new-password" [formGroup]="passwordForm"
(ngSubmit)="setInitialPassword(userId)">
<div class="content center">
<cnsl-form-field class="formfield" appearance="outline">
<cnsl-label>{{ 'USER.PASSWORD.NEW' | translate }}</cnsl-label>
<input cnslInput autocomplete="off" name="password" formControlName="password" type="password" />
<cnsl-form-field class="formfield">
<cnsl-label>{{ 'USER.PASSWORD.NEW' | translate }}</cnsl-label>
<input cnslInput autocomplete="off" name="new password" type="password" formControlName="newPassword" />
<span cnsl-error *ngIf="password?.errors?.required">
{{ 'USER.VALIDATION.REQUIRED' | translate }}
</span>
</cnsl-form-field>
<cnsl-form-field class="formfield" appearance="outline">
<cnsl-label>{{ 'USER.PASSWORD.CONFIRM' | translate }}</cnsl-label>
<input cnslInput autocomplete="off" name="passwordRepeat" formControlName="confirmPassword"
type="password" />
<span cnsl-error *ngIf="confirmPassword?.errors?.notequal">
{{ 'USER.PASSWORD.NOTEQUAL' | translate }}
</span>
</cnsl-form-field>
</div>
<div class="btn-container">
<button class="submit-button" [disabled]="passwordForm.invalid" mat-raised-button
color="primary">{{ 'USER.PASSWORD.SET' | translate }}</button>
</div>
</form>
</ng-container>
<ng-template #authUser>
<form *ngIf="passwordForm" [formGroup]="passwordForm" (ngSubmit)="setPassword()">
<div class="content">
<cnsl-form-field class="formfield" appearance="outline">
<cnsl-label>{{ 'USER.PASSWORD.OLD' | translate }}</cnsl-label>
<input cnslInput autocomplete="off" name="password" type="password"
formControlName="currentPassword" />
<span cnsl-error *ngIf="currentPassword?.errors?.required">
{{ 'USER.PASSWORD.REQUIRED' | translate }}
</span>
</cnsl-form-field>
<div class="validation between" *ngIf="this.policy">
<cnsl-password-complexity-view [policy]="this.policy" [password]="newPassword">
</cnsl-password-complexity-view>
</div>
<cnsl-form-field class="formfield" appearance="outline">
<cnsl-label>{{ 'USER.PASSWORD.NEW' | translate }}</cnsl-label>
<input cnslInput autocomplete="off" name="new password" type="password"
formControlName="newPassword" />
<span cnsl-error *ngIf="newPassword?.errors?.required">
{{ 'USER.VALIDATION.REQUIRED' | translate }}
</span>
</cnsl-form-field>
<cnsl-form-field class="formfield" appearance="outline">
<cnsl-label>{{ 'USER.PASSWORD.CONFIRM' | translate }}</cnsl-label>
<input cnslInput autocomplete="off" name="password repeating" type="password"
formControlName="confirmPassword" />
<span cnsl-error *ngIf="confirmPassword?.errors?.notequal">
{{ 'USER.PASSWORD.NOTEQUAL' | translate }}
</span>
</cnsl-form-field>
</div>
<button class="submit-button" [disabled]="passwordForm.invalid" mat-raised-button
color="primary">{{ 'USER.PASSWORD.RESET' | translate }}</button>
</form>
</ng-template>
<span cnslError *ngIf="newPassword?.errors?.required">
{{ 'USER.VALIDATION.REQUIRED' | translate }}
</span>
</cnsl-form-field>
<cnsl-form-field class="formfield">
<cnsl-label>{{ 'USER.PASSWORD.CONFIRM' | translate }}</cnsl-label>
<input cnslInput autocomplete="off" name="password repeating" type="password"
formControlName="confirmPassword" />
<span cnslError *ngIf="confirmPassword?.errors?.notequal">
{{ 'USER.PASSWORD.NOTEQUAL' | translate }}
</span>
</cnsl-form-field>
</div>
<button class="submit-button" [disabled]="passwordForm.invalid" mat-raised-button color="primary">{{
'USER.PASSWORD.RESET' | translate }}</button>
</form>
</ng-template>
</cnsl-detail-layout>
@@ -1,12 +1,11 @@
.content {
display: flex;
flex-direction: row;
flex-wrap: wrap;
margin: 0 -.5rem;
margin: 0 -0.5rem;
.formfield {
margin: 0 .5rem;
margin: 0 0.5rem;
width: 100%;
max-width: 400px;
}
@@ -18,16 +17,12 @@
.validation {
&.between {
margin: 0 .5rem;
margin: 0 0.5rem;
}
}
.submit-button {
margin-top: 100px;
display: block;
padding: .5rem 4rem;
}
.cnsl-error {
display: block;
padding: 0.5rem 4rem;
}
@@ -1,9 +1,10 @@
import { Component, OnDestroy } from '@angular/core';
import { AbstractControl, FormBuilder, FormGroup, Validators } from '@angular/forms';
import { ActivatedRoute } from '@angular/router';
import { Subscription } from 'rxjs';
import { Subscription, take } from 'rxjs';
import { lowerCaseValidator, numberValidator, symbolValidator, upperCaseValidator } from 'src/app/pages/validators';
import { PasswordComplexityPolicy } from 'src/app/proto/generated/zitadel/policy_pb';
import { Breadcrumb, BreadcrumbService, BreadcrumbType } from 'src/app/services/breadcrumb.service';
import { GrpcAuthService } from 'src/app/services/grpc-auth.service';
import { ManagementService } from 'src/app/services/mgmt.service';
import { ToastService } from 'src/app/services/toast.service';
@@ -42,42 +43,70 @@ export class PasswordComponent implements OnDestroy {
private authService: GrpcAuthService,
private mgmtUserService: ManagementService,
private toast: ToastService,
private breadcrumbService: BreadcrumbService,
) {
activatedRoute.params.subscribe(data => {
activatedRoute.params.subscribe((data) => {
const { id } = data;
if (id) {
this.userId = id;
breadcrumbService.setBreadcrumb([
new Breadcrumb({
type: BreadcrumbType.IAM,
name: 'IAM',
routerLink: ['/system'],
}),
new Breadcrumb({
type: BreadcrumbType.ORG,
routerLink: ['/org'],
}),
]);
} else {
this.authService.user.pipe(take(1)).subscribe((user) => {
if (user) {
this.breadcrumbService.setBreadcrumb([
new Breadcrumb({
type: BreadcrumbType.AUTHUSER,
name: user.human?.profile?.displayName,
routerLink: ['/users', 'me'],
}),
]);
}
});
}
const validators: Validators[] = [Validators.required];
this.authService.getMyPasswordComplexityPolicy().then(resp => {
if (resp.policy) {
this.policy = resp.policy;
}
if (this.policy.minLength) {
validators.push(Validators.minLength(this.policy.minLength));
}
if (this.policy.hasLowercase) {
validators.push(lowerCaseValidator);
}
if (this.policy.hasUppercase) {
validators.push(upperCaseValidator);
}
if (this.policy.hasNumber) {
validators.push(numberValidator);
}
if (this.policy.hasSymbol) {
validators.push(symbolValidator);
}
this.authService
.getMyPasswordComplexityPolicy()
.then((resp) => {
if (resp.policy) {
this.policy = resp.policy;
}
if (this.policy.minLength) {
validators.push(Validators.minLength(this.policy.minLength));
}
if (this.policy.hasLowercase) {
validators.push(lowerCaseValidator);
}
if (this.policy.hasUppercase) {
validators.push(upperCaseValidator);
}
if (this.policy.hasNumber) {
validators.push(numberValidator);
}
if (this.policy.hasSymbol) {
validators.push(symbolValidator);
}
this.setupForm(validators);
}).catch(error => {
this.setupForm(validators);
});
this.setupForm(validators);
})
.catch((error) => {
this.setupForm(validators);
});
});
}
public ngOnDestroy(): void {
ngOnDestroy(): void {
this.formSub.unsubscribe();
}
@@ -98,24 +127,34 @@ export class PasswordComponent implements OnDestroy {
public setInitialPassword(userId: string): void {
if (this.passwordForm.valid && this.password && this.password.value) {
this.mgmtUserService.setHumanInitialPassword(userId, this.password.value).then((data: any) => {
this.toast.showInfo('USER.TOAST.INITIALPASSWORDSET', true);
window.history.back();
}).catch(error => {
this.toast.showError(error);
});
this.mgmtUserService
.setHumanInitialPassword(userId, this.password.value)
.then((data: any) => {
this.toast.showInfo('USER.TOAST.INITIALPASSWORDSET', true);
window.history.back();
})
.catch((error) => {
this.toast.showError(error);
});
}
}
public setPassword(): void {
if (this.passwordForm.valid && this.currentPassword &&
if (
this.passwordForm.valid &&
this.currentPassword &&
this.currentPassword.value &&
this.newPassword && this.newPassword.value && this.newPassword.valid) {
this.authService.updateMyPassword(this.currentPassword.value, this.newPassword.value)
this.newPassword &&
this.newPassword.value &&
this.newPassword.valid
) {
this.authService
.updateMyPassword(this.currentPassword.value, this.newPassword.value)
.then((data: any) => {
this.toast.showInfo('USER.TOAST.PASSWORDCHANGED', true);
window.history.back();
}).catch(error => {
})
.catch((error) => {
this.toast.showError(error);
});
}
@@ -1,75 +0,0 @@
import { NgModule } from '@angular/core';
import { RouterModule, Routes } from '@angular/router';
import { AuthGuard } from 'src/app/guards/auth.guard';
import { RoleGuard } from 'src/app/guards/role.guard';
import { UserGuard } from 'src/app/guards/user.guard';
import { AuthUserDetailComponent } from './auth-user-detail/auth-user-detail.component';
import { PasswordComponent } from './password/password.component';
import { UserDetailComponent } from './user-detail/user-detail.component';
const routes: Routes = [
{
path: 'create',
loadChildren: () => import('../user-create/user-create.module').then(m => m.UserCreateModule),
canActivate: [AuthGuard, RoleGuard],
data: {
roles: ['user.write'],
},
},
{
path: 'create-machine',
loadChildren: () => import('../user-create-machine/user-create-machine.module').then(m => m.UserCreateMachineModule),
canActivate: [AuthGuard, RoleGuard],
data: {
roles: ['user.write'],
},
},
{
path: 'me',
component: AuthUserDetailComponent,
canActivate: [AuthGuard],
data: {
animation: 'HomePage',
},
},
{
path: 'me/password',
component: PasswordComponent,
canActivate: [AuthGuard],
data: { animation: 'AddPage' },
},
{
path: ':id',
component: UserDetailComponent,
canActivate: [AuthGuard, UserGuard, RoleGuard],
data: {
roles: ['user.read'],
animation: 'HomePage',
},
},
{
path: ':id/password',
component: PasswordComponent,
canActivate: [AuthGuard, RoleGuard],
data: {
roles: ['user.write'],
animation: 'AddPage',
},
},
{
path: ':id/memberships',
loadChildren: () => import('./membership-detail/membership-detail.module').then(m => m.MembershipDetailModule),
canActivate: [AuthGuard, RoleGuard],
data: {
roles: ['user.membership.read'],
animation: 'AddPage',
},
},
];
@NgModule({
imports: [RouterModule.forChild(routes)],
exports: [RouterModule],
})
export class UserDetailRoutingModule { }
@@ -6,11 +6,11 @@ import { MatCheckboxModule } from '@angular/material/checkbox';
import { MatDialogModule } from '@angular/material/dialog';
import { MatIconModule } from '@angular/material/icon';
import { MatMenuModule } from '@angular/material/menu';
import { MatProgressBarModule } from '@angular/material/progress-bar';
import { MatProgressSpinnerModule } from '@angular/material/progress-spinner';
import { MatTableModule } from '@angular/material/table';
import { MatTabsModule } from '@angular/material/tabs';
import { MatTooltipModule } from '@angular/material/tooltip';
import { RouterModule } from '@angular/router';
import { TranslateModule } from '@ngx-translate/core';
import { QrCodeModule } from 'ng-qrcode';
import { CopyToClipboardModule } from 'src/app/directives/copy-to-clipboard/copy-to-clipboard.module';
@@ -22,13 +22,16 @@ import { DetailLayoutModule } from 'src/app/modules/detail-layout/detail-layout.
import { InfoSectionModule } from 'src/app/modules/info-section/info-section.module';
import { InputModule } from 'src/app/modules/input/input.module';
import { MachineKeysModule } from 'src/app/modules/machine-keys/machine-keys.module';
import { MembershipsTableModule } from 'src/app/modules/memberships-table/memberships-table.module';
import { MetaLayoutModule } from 'src/app/modules/meta-layout/meta-layout.module';
import { PaginatorModule } from 'src/app/modules/paginator/paginator.module';
import { PasswordComplexityViewModule } from 'src/app/modules/password-complexity-view/password-complexity-view.module';
import { PersonalAccessTokensModule } from 'src/app/modules/personal-access-tokens/personal-access-tokens.module';
import { RefreshTableModule } from 'src/app/modules/refresh-table/refresh-table.module';
import { SharedModule } from 'src/app/modules/shared/shared.module';
import { ShowTokenDialogModule } from 'src/app/modules/show-token-dialog/show-token-dialog.module';
import { SidenavModule } from 'src/app/modules/sidenav/sidenav.module';
import { TableActionsModule } from 'src/app/modules/table-actions/table-actions.module';
import { TopViewModule } from 'src/app/modules/top-view/top-view.module';
import { UserGrantsModule } from 'src/app/modules/user-grants/user-grants.module';
import { WarnDialogModule } from 'src/app/modules/warn-dialog/warn-dialog.module';
import { HasRolePipeModule } from 'src/app/pipes/has-role-pipe/has-role-pipe.module';
@@ -48,16 +51,13 @@ import { CodeDialogComponent } from './auth-user-detail/code-dialog/code-dialog.
import { DialogU2FComponent } from './auth-user-detail/dialog-u2f/dialog-u2f.component';
import { EditDialogComponent } from './auth-user-detail/edit-dialog/edit-dialog.component';
import { ResendEmailDialogComponent } from './auth-user-detail/resend-email-dialog/resend-email-dialog.component';
import { ThemeSettingComponent } from './auth-user-detail/theme-setting/theme-setting.component';
import { ContactComponent } from './contact/contact.component';
import { DetailFormMachineModule } from './detail-form-machine/detail-form-machine.module';
import { DetailFormModule } from './detail-form/detail-form.module';
import { ExternalIdpsComponent } from './external-idps/external-idps.component';
import { MembershipsComponent } from './memberships/memberships.component';
import { MetadataDialogComponent } from './metadata-dialog/metadata-dialog.component';
import { MetadataComponent } from './metadata/metadata.component';
import { PasswordComponent } from './password/password.component';
import { UserDetailRoutingModule } from './user-detail-routing.module';
import { PasswordlessComponent } from './user-detail/passwordless/passwordless.component';
import { UserDetailComponent } from './user-detail/user-detail.component';
import { UserMfaComponent } from './user-detail/user-mfa/user-mfa.component';
@@ -71,10 +71,8 @@ import { UserMfaComponent } from './user-detail/user-mfa/user-mfa.component';
AuthPasswordlessComponent,
UserMfaComponent,
PasswordlessComponent,
ThemeSettingComponent,
PasswordComponent,
CodeDialogComponent,
MembershipsComponent,
ExternalIdpsComponent,
ContactComponent,
ResendEmailDialogComponent,
@@ -85,12 +83,13 @@ import { UserMfaComponent } from './user-detail/user-mfa/user-mfa.component';
MetadataComponent,
],
imports: [
UserDetailRoutingModule,
ChangesModule,
CommonModule,
SidenavModule,
MatTabsModule,
FormsModule,
ReactiveFormsModule,
MembershipsTableModule,
DetailFormModule,
DetailFormMachineModule,
WarnDialogModule,
@@ -99,6 +98,7 @@ import { UserMfaComponent } from './user-detail/user-mfa/user-mfa.component';
ShowTokenDialogModule,
MetaLayoutModule,
MatCheckboxModule,
TopViewModule,
HasRolePipeModule,
UserGrantsModule,
MatButtonModule,
@@ -106,7 +106,6 @@ import { UserMfaComponent } from './user-detail/user-mfa/user-mfa.component';
MatIconModule,
CardModule,
MatProgressSpinnerModule,
MatProgressBarModule,
HasFeatureModule,
MatTooltipModule,
HasRoleModule,
@@ -115,10 +114,11 @@ import { UserMfaComponent } from './user-detail/user-mfa/user-mfa.component';
InfoRowModule,
PaginatorModule,
MatMenuModule,
SharedModule,
RouterModule,
RefreshTableModule,
CopyToClipboardModule,
DetailLayoutModule,
TableActionsModule,
PasswordComplexityViewModule,
MemberCreateDialogModule,
TimestampToDatePipeModule,
@@ -1,10 +1,17 @@
<cnsl-card title="{{'USER.PASSWORDLESS.TITLE' | translate}}"
description="{{'USER.PASSWORDLESS.DESCRIPTION' | translate}}">
<button card-actions mat-icon-button (click)="getPasswordless()" class="icon-button"
matTooltip="{{'ACTIONS.REFRESH' | translate}}">
<mat-icon class="icon">refresh</mat-icon>
</button>
<cnsl-refresh-table [hideRefresh]="true" [loading]="loading$ | async" [dataSize]="dataSource?.data?.length ?? 0">
<button actions [disabled]="user && disabled" class="button cnsl-action-button"
(click)="sendPasswordlessRegistration()" mat-raised-button color="primary"
matTooltip="{{'ACTIONS.NEW' | translate}}">
<i class="icon las la-paper-plane"></i>
<span>{{'USER.PASSWORDLESS.SEND' | translate}}</span>
</button>
<table class="table" mat-table [dataSource]="dataSource">
<ng-container matColumnDef="name">
<th mat-header-cell *matHeaderCellDef> {{ 'USER.PASSWORDLESS.NAME' | translate }} </th>
@@ -16,35 +23,34 @@
<ng-container matColumnDef="state">
<th mat-header-cell *matHeaderCellDef> {{ 'USER.PASSWORDLESS.TABLESTATE' | translate }} </th>
<td mat-cell *matCellDef="let mfa"><span class="centered">
{{'USER.PASSWORDLESS.STATE.'+ mfa.state | translate}}
<i matTooltip="verified" *ngIf="mfa.state === AuthFactorState.AUTH_FACTOR_STATE_READY"
class="verified las la-check-circle"></i>
</span>
<td mat-cell *matCellDef="let mfa">
<span class="state"
[ngClass]="{'active': mfa.state === AuthFactorState.AUTH_FACTOR_STATE_READY, 'inactive': mfa.state === AuthFactorState.AUTH_FACTOR_STATE_NOT_READY }">{{'USER.PASSWORDLESS.STATE.'+
mfa.state | translate}}</span>
</td>
</ng-container>
<ng-container matColumnDef="actions">
<th mat-header-cell *matHeaderCellDef> {{ 'USER.PASSWORDLESS.TABLEACTIONS' | translate }} </th>
<th mat-header-cell *matHeaderCellDef></th>
<td mat-cell *matCellDef="let mfa">
<button matTooltip="{{'ACTIONS.REMOVE' | translate}}" color="warn" mat-icon-button
(click)="deletePasswordless(mfa.id)">
<i class="las la-trash"></i>
</button>
<cnsl-table-actions>
<button actions matTooltip="{{'ACTIONS.REMOVE' | translate}}" color="warn" mat-icon-button
(click)="deletePasswordless(mfa.id)">
<i class="las la-trash"></i>
</button>
</cnsl-table-actions>
</td>
</ng-container>
<tr mat-header-row *matHeaderRowDef="displayedColumns"></tr>
<tr class="highlight" mat-row *matRowDef="let row; columns: displayedColumns;"></tr>
</table>
<div *ngIf="(loading$ | async) === false && !dataSource?.data?.length" class="no-content-row">
<i class="las la-exclamation"></i>
<span>{{'USER.PASSWORDLESS.EMPTY' | translate}}</span>
</div>
</cnsl-refresh-table>
<div class="add-row">
<button [disabled]="user && disabled" class="button" (click)="sendPasswordlessRegistration()" mat-raised-button
color="primary" matTooltip="{{'ACTIONS.NEW' | translate}}">
{{'USER.PASSWORDLESS.SEND' | translate}}
<i class="icon las la-paper-plane"></i>
</button>
</div>
<div class="table-wrapper">
<div class="spinner-container" *ngIf="loading$ | async">
<mat-spinner diameter="50"></mat-spinner>
@@ -16,35 +16,4 @@
.table {
width: 100%;
td,
th {
&:first-child {
padding-left: 0;
padding-right: 1rem;
}
&:last-child {
padding-right: 0;
}
}
}
.add-row {
display: flex;
margin: -.5rem;
flex-wrap: wrap;
justify-content: flex-end;
.button {
margin: .5rem;
margin-top: 1rem;
display: flex;
align-items: center;
.icon {
margin-left: .5rem;
margin-bottom: 2px;
}
}
}
@@ -43,7 +43,7 @@ export class PasswordlessComponent implements OnInit, OnDestroy {
this.getPasswordless();
}
public ngOnDestroy(): void {
ngOnDestroy(): void {
this.loadingSubject.complete();
}
@@ -1,146 +1,147 @@
<div *ngIf="error" class="max-width-container">
<p>{{error}}</p>
<cnsl-top-view *ngIf="user" title="{{user?.human ? user.human?.profile?.displayName :
user?.machine?.name}}" docLink="https://docs.zitadel.ch/docs/guides/basics/projects"
sub="{{user?.preferredLoginName}}" [isActive]="user?.state === UserState.USER_STATE_ACTIVE"
[isInactive]="user?.state === UserState.USER_STATE_INACTIVE" stateTooltip="{{'USER.STATE.'+user?.state | translate}}"
(backClicked)="navigateBack()" [hasActions]="['user.write$', 'user.write:'+user?.id] | hasRole | async">
<ng-template topActions cnslHasRole [hasRole]="['user.write$', 'user.write:'+user?.id]">
<button mat-menu-item color="warn" *ngIf="user?.state === UserState.USER_STATE_LOCKED"
(click)="unlockUser()">{{'USER.PAGES.UNLOCK' |
translate}}</button>
<button mat-menu-item *ngIf="user?.state === UserState.USER_STATE_ACTIVE"
(click)="changeState(UserState.USER_STATE_INACTIVE)">{{'USER.PAGES.DEACTIVATE' |
translate}}</button>
<button mat-menu-item *ngIf="user?.state === UserState.USER_STATE_INACTIVE"
(click)="changeState(UserState.USER_STATE_ACTIVE)">{{'USER.PAGES.REACTIVATE' | translate}}</button>
<ng-template cnslHasRole [hasRole]="['user.delete$', 'user.delete:'+user?.id]">
<button mat-menu-item matTooltip="{{'USER.PAGES.DELETE' | translate}}" (click)="deleteUser()"><span
[style.color]="'var(--warn)'">{{'USER.PAGES.DELETE' | translate}}</span></button>
</ng-template>
</ng-template>
<cnsl-info-row topContent *ngIf="user" [user]="user"></cnsl-info-row>
</cnsl-top-view>
<div *ngIf="loading" class="max-width-container">
<mat-progress-spinner diameter="25" color="primary" mode="indeterminate"></mat-progress-spinner>
</div>
<cnsl-meta-layout *ngIf="user && (['user.write$','user.write:' + user.id] | hasRole) as canWrite$">
<div class="max-width-container">
<div class="head">
<a (click)="navigateBack()" mat-icon-button>
<mat-icon class="icon">arrow_back</mat-icon>
</a>
<div class="head-row">
<h1>{{user.human ? user.human?.profile?.displayName : user.machine?.name}}</h1>
<p *ngIf="user?.preferredLoginName">{{user?.preferredLoginName}}</p>
<p class="no-user-error" *ngIf="!loading && !user">{{ 'USER.PAGES.NOUSER' | translate }}</p>
<div class="max-width-container" *ngIf="user && (['user.write$','user.write:' + user.id] | hasRole) as canWrite$">
<cnsl-meta-layout>
<cnsl-sidenav [(ngModel)]="currentSetting" [settingsList]="settingsList">
<div *ngIf="error" class="max-width-container">
<p>{{error}}</p>
</div>
<span class="fill-space"></span>
<ng-template cnslHasRole [hasRole]="['user.write$', 'user.write:'+user?.id]">
<button class="actions-trigger" mat-raised-button color="primary" [matMenuTriggerFor]="actions">
<span>{{'ACTIONS.ACTIONS' | translate}}</span>
<mat-icon class="icon">keyboard_arrow_down</mat-icon>
</button>
<mat-menu #actions="matMenu" xPosition="before">
<button mat-menu-item color="warn" *ngIf="user?.state === UserState.USER_STATE_LOCKED"
(click)="unlockUser()">{{'USER.PAGES.UNLOCK' |
translate}}</button>
<button mat-menu-item *ngIf="user?.state === UserState.USER_STATE_ACTIVE"
(click)="changeState(UserState.USER_STATE_INACTIVE)">{{'USER.PAGES.DEACTIVATE' |
translate}}</button>
<button mat-menu-item *ngIf="user?.state === UserState.USER_STATE_INACTIVE"
(click)="changeState(UserState.USER_STATE_ACTIVE)">{{'USER.PAGES.REACTIVATE' | translate}}</button>
<ng-template cnslHasRole [hasRole]="['user.delete$', 'user.delete:'+user?.id]">
<button mat-menu-item matTooltip="{{'USER.PAGES.DELETE' | translate}}" (click)="deleteUser()"><span
[style.color]="'var(--warn)'">{{'USER.PAGES.DELETE' | translate}}</span></button>
<div class="max-width-container">
<cnsl-info-section class="locked" *ngIf="user?.state === UserState.USER_STATE_LOCKED"
[type]="InfoSectionType.WARN">
{{'USER.PAGES.LOCKEDDESCRIPTION' | translate}}</cnsl-info-section>
<span *ngIf="!loading && !user">{{ 'USER.PAGES.NOUSER' | translate }}</span>
<ng-container *ngIf="currentSetting === 'general'">
<ng-template cnslHasRole [hasRole]="['user.read$', 'user.read:'+user?.id]">
<cnsl-card *ngIf="user.human" title="{{ 'USER.PROFILE.TITLE' | translate }}">
<cnsl-detail-form [preferredLoginName]="user.preferredLoginName"
[disabled]="(canWrite$ | async) === false" [genders]="genders" [languages]="languages"
[username]="user.userName" [user]="user.human" (submitData)="saveProfile($event)"
(changeUsernameClicked)="changeUsername()">
</cnsl-detail-form>
</cnsl-card>
<cnsl-card *ngIf="user.human" title="{{ 'USER.LOGINMETHODS.TITLE' | translate }}"
description="{{ 'USER.LOGINMETHODS.DESCRIPTION' | translate }}">
<button card-actions class="icon-button" mat-icon-button (click)="refreshUser()"
matTooltip="{{'ACTIONS.REFRESH' | translate}}">
<mat-icon class="icon">refresh</mat-icon>
</button>
<cnsl-contact [disablePhoneCode]="true" [state]="user.state"
[canWrite]="(['user.write:' + user?.id, 'user.write$'] | hasRole | async)" *ngIf="user?.human"
[human]="user.human" (editType)="openEditDialog($event)" (deletedPhone)="deletePhone()"
(resendEmailVerification)="resendEmailVerification()"
(resendPhoneVerification)="resendPhoneVerification()">
<button pwdAction [disabled]="(canWrite$ | async) === false" (click)="sendSetPasswordNotification()"
mat-stroked-button color="primary"
*ngIf="user.state !== UserState.USER_STATE_LOCKED && user.state !== UserState.USER_STATE_INACTIVE && user.state !== UserState.USER_STATE_INITIAL">{{
'USER.PASSWORD.RESENDNOTIFICATION' | translate }}</button>
<button emailAction [disabled]="(canWrite$ | async) === false" class="resendemail"
*ngIf="user.state === UserState.USER_STATE_INITIAL" mat-stroked-button color="primary"
(click)="resendInitEmail()">{{'USER.RESENDINITIALEMAIL' |
translate}}</button>
</cnsl-contact>
</cnsl-card>
</ng-template>
</mat-menu>
</ng-template>
</div>
</ng-container>
<mat-progress-bar *ngIf="loading" color="primary" mode="indeterminate"></mat-progress-bar>
<ng-container *ngIf="currentSetting && currentSetting === 'idp'">
<cnsl-external-idps *ngIf="user && user.human && user.id" [userId]="user.id" [service]="mgmtUserService">
</cnsl-external-idps>
</ng-container>
<cnsl-info-section class="locked" *ngIf="user?.state === UserState.USER_STATE_LOCKED" [type]="InfoSectionType.WARN">
{{'USER.PAGES.LOCKEDDESCRIPTION' | translate}}</cnsl-info-section>
<span *ngIf="!loading && !user">{{ 'USER.PAGES.NOUSER' | translate }}</span>
<ng-container *ngIf="currentSetting && currentSetting === 'general'">
<cnsl-card *ngIf="user.machine" title="{{ 'USER.MACHINE.TITLE' | translate }}">
<cnsl-detail-form-machine [disabled]="(canWrite$ | async) === false" [username]="user.userName"
[user]="user.machine" (submitData)="saveMachine($event)">
</cnsl-detail-form-machine>
</cnsl-card>
</ng-container>
<cnsl-info-row *ngIf="user" [user]="user"></cnsl-info-row>
<ng-template cnslHasRole [hasRole]="['user.read$', 'user.read:'+user?.id]">
<cnsl-card *ngIf="user.human" title="{{ 'USER.PROFILE.TITLE' | translate }}">
<cnsl-detail-form [preferredLoginName]="user.preferredLoginName" [disabled]="(canWrite$ | async) === false"
[genders]="genders" [languages]="languages" [username]="user.userName" [user]="user.human"
(submitData)="saveProfile($event)" (changeUsernameClicked)="changeUsername()">
</cnsl-detail-form>
</cnsl-card>
<cnsl-card *ngIf="user.human" title="{{ 'USER.LOGINMETHODS.TITLE' | translate }}"
description="{{ 'USER.LOGINMETHODS.DESCRIPTION' | translate }}">
<button card-actions class="icon-button" mat-icon-button (click)="refreshUser()"
matTooltip="{{'ACTIONS.REFRESH' | translate}}">
<mat-icon class="icon">refresh</mat-icon>
</button>
<cnsl-contact [disablePhoneCode]="true"
[canWrite]="(['user.write:' + user?.id, 'user.write$'] | hasRole | async)" *ngIf="user?.human"
[human]="user.human" (editType)="openEditDialog($event)" (deletedPhone)="deletePhone()"
(resendEmailVerification)="resendEmailVerification()" (resendPhoneVerification)="resendPhoneVerification()">
<button pwdAction [disabled]="(canWrite$ | async) === false" (click)="sendSetPasswordNotification()"
mat-stroked-button color="primary" *ngIf="user.state === UserState.USER_STATE_INITIAL">{{
'USER.PASSWORD.RESENDNOTIFICATION' | translate }}</button>
<button emailAction [disabled]="(canWrite$ | async) === false" class="resendemail"
*ngIf="user.state === UserState.USER_STATE_INITIAL" mat-stroked-button color="primary"
(click)="resendInitEmail()">{{'USER.RESENDINITIALEMAIL' |
translate}}</button>
</cnsl-contact>
</cnsl-card>
<cnsl-external-idps *ngIf="user && user.human && user.id" [userId]="user.id" [service]="mgmtUserService">
</cnsl-external-idps>
<cnsl-card *ngIf="user.machine" title="{{ 'USER.MACHINE.TITLE' | translate }}">
<cnsl-detail-form-machine [disabled]="(canWrite$ | async) === false" [username]="user.userName"
[user]="user.machine" (submitData)="saveMachine($event)">
</cnsl-detail-form-machine>
</cnsl-card>
<cnsl-card *ngIf="user.machine && user.id" title="{{ 'USER.MACHINE.KEYSTITLE' | translate }}"
description="{{ 'USER.MACHINE.KEYSDESC' | translate }}">
<cnsl-machine-keys [userId]="user.id"></cnsl-machine-keys>
</cnsl-card>
<cnsl-card *ngIf="user.machine && user.id" title="{{ 'USER.MACHINE.TOKENSTITLE' | translate }}"
description="{{ 'USER.MACHINE.TOKENSDESC' | translate }}">
<cnsl-personal-access-tokens [userId]="user.id"></cnsl-personal-access-tokens>
</cnsl-card>
</ng-template>
<cnsl-passwordless *ngIf="user && user.human" [user]="user" [disabled]="(canWrite$ | async) === false">
</cnsl-passwordless>
<cnsl-user-mfa *ngIf="user && user.human" [user]="user"></cnsl-user-mfa>
<cnsl-card *ngIf="user?.id" title="{{ 'GRANTS.USER.TITLE' | translate }}"
description="{{'GRANTS.USER.DESCRIPTION' | translate }}">
<cnsl-user-grants [userId]="user.id" [context]="USERGRANTCONTEXT"
[displayedColumns]="['select', 'projectId', 'dates', 'roleNamesList']"
[disableWrite]="((['user.grant.write$'] | hasRole) | async) === false"
[disableDelete]="((['user.grant.delete$'] | hasRole) | async) === false">
</cnsl-user-grants>
</cnsl-card>
<ng-template cnslHasFeature [hasFeature]="['metadata.user']">
<cnsl-metadata *ngIf="user" [userId]="user.id"></cnsl-metadata>
</ng-template>
</div>
<div *ngIf="user" class="side" metainfo>
<div class="meta-details">
<div class="meta-row">
<span class="first">{{'RESOURCEID' | translate}}:</span>
<span *ngIf="user?.id" class="second">{{ user.id }}</span>
</div>
<div class="meta-row" *ngIf="user?.preferredLoginName">
<span class="first">{{'USER.PREFERRED_LOGINNAME' | translate}}</span>
<span class="second"><span style="display: block;">{{user.preferredLoginName}}</span></span>
</div>
<div class="meta-row">
<span class="first">{{'USER.PAGES.STATE' | translate}}</span>
<span *ngIf="user && user.state !== undefined" class="state"
[ngClass]="{'active': user.state === UserState.USER_STATE_ACTIVE, 'inactive': user.state === UserState.USER_STATE_INACTIVE}">{{'USER.DATA.STATE'+user.state
| translate}}</span>
</div>
</div>
<mat-tab-group mat-stretch-tabs class="tab-group" [disablePagination]="true">
<mat-tab label="Details">
<div class="side-padding">
<ng-template cnslHasRole [hasRole]="['user.membership.read']">
<cnsl-memberships [user]="user" [disabled]="(canWrite$ | async) === false"></cnsl-memberships>
<ng-container *ngIf="currentSetting && currentSetting === 'pat'">
<ng-template cnslHasRole [hasRole]="['user.read$', 'user.read:'+user?.id]">
<cnsl-card *ngIf="user.machine && user.id" title="{{ 'USER.MACHINE.TOKENSTITLE' | translate }}"
description="{{ 'USER.MACHINE.TOKENSDESC' | translate }}">
<cnsl-personal-access-tokens [userId]="user.id"></cnsl-personal-access-tokens>
</cnsl-card>
</ng-template>
</div>
</mat-tab>
<mat-tab label="{{ 'CHANGES.PROJECT.TITLE' | translate }}" class="meta-flex-col">
<cnsl-changes class="changes" [refresh]="refreshChanges$" [changeType]="ChangeType.USER" [id]="user.id">
</cnsl-changes>
</mat-tab>
</mat-tab-group>
</div>
</cnsl-meta-layout>
</ng-container>
<ng-container *ngIf="currentSetting && currentSetting === 'keys'">
<ng-template cnslHasRole [hasRole]="['user.read$', 'user.read:'+user?.id]">
<cnsl-card *ngIf="user.machine && user.id" title="{{ 'USER.MACHINE.KEYSTITLE' | translate }}"
description="{{ 'USER.MACHINE.KEYSDESC' | translate }}">
<cnsl-machine-keys [userId]="user.id"></cnsl-machine-keys>
</cnsl-card>
</ng-template>
</ng-container>
<ng-container *ngIf="currentSetting && currentSetting === 'passwordless'">
<cnsl-passwordless *ngIf="user && !!user.human" [user]="user" [disabled]="(canWrite$ | async) === false">
</cnsl-passwordless>
</ng-container>
<ng-container *ngIf="currentSetting && currentSetting === 'mfa'">
<cnsl-user-mfa *ngIf="user && user.human" [user]="user"></cnsl-user-mfa>
</ng-container>
<ng-container *ngIf="currentSetting && currentSetting === 'grants'">
<cnsl-card *ngIf="user?.id" title="{{ 'GRANTS.USER.TITLE' | translate }}"
description="{{'GRANTS.USER.DESCRIPTION' | translate }}">
<cnsl-user-grants [userId]="user.id" [context]="USERGRANTCONTEXT"
[displayedColumns]="['select', 'projectId', 'creationDate','changeDate', 'roleNamesList', 'actions']"
[disableWrite]="((['user.grant.write$'] | hasRole) | async) === false"
[disableDelete]="((['user.grant.delete$'] | hasRole) | async) === false">
</cnsl-user-grants>
</cnsl-card>
</ng-container>
<ng-container *ngIf="currentSetting && currentSetting === 'memberships'">
<cnsl-card *ngIf="user?.id" title="{{ 'USER.MEMBERSHIPS.TITLE' | translate }}"
description="{{'USER.MEMBERSHIPS.DESCRIPTION' | translate }}">
<cnsl-memberships-table [userId]="user.id"></cnsl-memberships-table>
</cnsl-card>
</ng-container>
<ng-container *ngIf="currentSetting && currentSetting === 'metadata'">
<ng-template cnslHasFeature [hasFeature]="['metadata.user']">
<cnsl-metadata *ngIf="user" [userId]="user.id"></cnsl-metadata>
</ng-template>
</ng-container>
</div>
</cnsl-sidenav>
<div metainfo>
<cnsl-changes class="changes" [refresh]="refreshChanges$" [changeType]="ChangeType.USER" [id]="user.id">
</cnsl-changes>
</div>
</cnsl-meta-layout>
</div>
@@ -1,43 +1,15 @@
.head {
display: flex;
align-items: flex-start;
flex-wrap: wrap;
padding-bottom: 2rem;
@use '@angular/material' as mat;
a {
display: block;
margin-right: 2rem;
}
@mixin user-detail-theme($theme) {
$foreground: map-get($theme, foreground);
$background: map-get($theme, background);
$is-dark-theme: map-get($theme, is-dark);
$warn: map-get($theme, warn);
$warn-color: map-get($warn, default);
.head-row {
display: flex;
flex-direction: column;
h1 {
margin: 0;
margin-right: 1rem;
}
p {
margin: 0.5rem 0;
font-size: 14px;
color: var(--grey);
}
}
.fill-space {
flex: 1;
}
.actions-trigger {
margin-top: 0.25rem;
display: flex;
align-items: center;
.icon {
margin-left: 0.5rem;
margin-right: -0.5rem;
}
.no-user-error {
color: $warn-color;
font-size: 14px;
}
}
@@ -1,3 +1,4 @@
import { MediaMatcher } from '@angular/cdk/layout';
import { Location } from '@angular/common';
import { Component, EventEmitter, OnInit } from '@angular/core';
import { MatDialog } from '@angular/material/dialog';
@@ -6,17 +7,29 @@ import { TranslateService } from '@ngx-translate/core';
import { take } from 'rxjs/operators';
import { ChangeType } from 'src/app/modules/changes/changes.component';
import { InfoSectionType } from 'src/app/modules/info-section/info-section.component';
import { SidenavSetting } from 'src/app/modules/sidenav/sidenav.component';
import { UserGrantContext } from 'src/app/modules/user-grants/user-grants-datasource';
import { WarnDialogComponent } from 'src/app/modules/warn-dialog/warn-dialog.component';
import { SendHumanResetPasswordNotificationRequest, UnlockUserRequest } from 'src/app/proto/generated/zitadel/management_pb';
import { Metadata } from 'src/app/proto/generated/zitadel/metadata_pb';
import { Email, Gender, Machine, Phone, Profile, User, UserState } from 'src/app/proto/generated/zitadel/user_pb';
import { Breadcrumb, BreadcrumbService, BreadcrumbType } from 'src/app/services/breadcrumb.service';
import { ManagementService } from 'src/app/services/mgmt.service';
import { ToastService } from 'src/app/services/toast.service';
import { EditDialogComponent, EditDialogType } from '../auth-user-detail/edit-dialog/edit-dialog.component';
import { ResendEmailDialogComponent } from '../auth-user-detail/resend-email-dialog/resend-email-dialog.component';
const GENERAL: SidenavSetting = { id: 'general', i18nKey: 'USER.SETTINGS.GENERAL', featureRequired: false };
const GRANTS: SidenavSetting = { id: 'grants', i18nKey: 'USER.SETTINGS.USERGRANTS', featureRequired: false };
const METADATA: SidenavSetting = { id: 'metadata', i18nKey: 'USER.SETTINGS.METADATA', featureRequired: ['metadata.user'] };
const IDP: SidenavSetting = { id: 'idp', i18nKey: 'USER.SETTINGS.IDP', featureRequired: false };
const PASSWORDLESS: SidenavSetting = { id: 'passwordless', i18nKey: 'USER.SETTINGS.PASSWORDLESS', featureRequired: false };
const MFA: SidenavSetting = { id: 'mfa', i18nKey: 'USER.SETTINGS.MFA', featureRequired: false };
const PERSONALACCESSTOKEN: SidenavSetting = { id: 'pat', i18nKey: 'USER.SETTINGS.PAT', featureRequired: false };
const KEYS: SidenavSetting = { id: 'keys', i18nKey: 'USER.SETTINGS.KEYS', featureRequired: false };
const MEMBERSHIPS: SidenavSetting = { id: 'memberships', i18nKey: 'USER.SETTINGS.MEMBERSHIPS', featureRequired: false };
@Component({
selector: 'cnsl-user-detail',
templateUrl: './user-detail.component.html',
@@ -29,7 +42,7 @@ export class UserDetailComponent implements OnInit {
public languages: string[] = ['de', 'en'];
public ChangeType: any = ChangeType;
public loading: boolean = false;
public loading: boolean = true;
public UserState: any = UserState;
public copied: string = '';
@@ -41,6 +54,9 @@ export class UserDetailComponent implements OnInit {
public error: string = '';
public settingsList: SidenavSetting[] = [GENERAL, GRANTS, MEMBERSHIPS, METADATA];
public currentSetting: string | undefined = 'general';
constructor(
public translate: TranslateService,
private route: ActivatedRoute,
@@ -49,21 +65,61 @@ export class UserDetailComponent implements OnInit {
private _location: Location,
private dialog: MatDialog,
private router: Router,
) {}
private mediaMatcher: MediaMatcher,
breadcrumbService: BreadcrumbService,
) {
breadcrumbService.setBreadcrumb([
new Breadcrumb({
type: BreadcrumbType.IAM,
name: 'IAM',
routerLink: ['/system'],
}),
new Breadcrumb({
type: BreadcrumbType.ORG,
routerLink: ['/org'],
}),
]);
const mediaq: string = '(max-width: 500px)';
const small = this.mediaMatcher.matchMedia(mediaq).matches;
if (small) {
this.changeSelection(small);
}
this.mediaMatcher.matchMedia(mediaq).onchange = (small) => {
this.changeSelection(small.matches);
};
}
private changeSelection(small: boolean): void {
if (small) {
this.currentSetting = undefined;
} else {
this.currentSetting = this.currentSetting === undefined ? 'general' : this.currentSetting;
}
}
refreshUser(): void {
this.refreshChanges$.emit();
this.route.params.pipe(take(1)).subscribe((params) => {
this.loading = true;
const { id } = params;
this.mgmtUserService
.getUserByID(id)
.then((resp) => {
this.loading = false;
if (resp.user) {
this.user = resp.user;
if (this.user.human) {
this.settingsList = [GENERAL, MFA, PASSWORDLESS, IDP, GRANTS, MEMBERSHIPS, METADATA];
} else if (this.user.machine) {
this.settingsList = [GENERAL, GRANTS, MEMBERSHIPS, PERSONALACCESSTOKEN, KEYS, METADATA];
}
}
})
.catch((err) => {
this.error = err.message ?? '';
this.loading = false;
this.toast.showError(err);
});
@@ -1,53 +1,62 @@
<cnsl-card title="{{'USER.MFA.TITLE' | translate}}" description="{{'USER.MFA.DESCRIPTION' | translate}}">
<button card-actions mat-icon-button (click)="getMFAs()" class="icon-button" matTooltip="{{'ACTIONS.REFRESH' | translate}}">
<button card-actions mat-icon-button (click)="getMFAs()" class="icon-button"
matTooltip="{{'ACTIONS.REFRESH' | translate}}">
<mat-icon class="icon">refresh</mat-icon>
</button>
<cnsl-refresh-table [hideRefresh]="true" [loading]="loading$ | async" (refreshed)="getMFAs()" [dataSize]="dataSource?.data?.length ?? 0">
<table class="table" mat-table [dataSource]="dataSource">
<ng-container matColumnDef="type">
<th mat-header-cell *matHeaderCellDef> {{ 'USER.MFA.TABLETYPE' | translate }} </th>
<td mat-cell *matCellDef="let mfa">
<span *ngIf="mfa.otp !== undefined">OTP (One-Time Password)</span>
<span *ngIf="mfa.u2f !== undefined">U2F (Universal 2nd Factor)</span>
</td>
</ng-container>
<cnsl-refresh-table [hideRefresh]="true" [loading]="loading$ | async" (refreshed)="getMFAs()"
[dataSize]="dataSource?.data?.length ?? 0">
<table class="table" mat-table [dataSource]="dataSource">
<ng-container matColumnDef="type">
<th mat-header-cell *matHeaderCellDef> {{ 'USER.MFA.TABLETYPE' | translate }} </th>
<td mat-cell *matCellDef="let mfa">
<span *ngIf="mfa.otp !== undefined">OTP (One-Time Password)</span>
<span *ngIf="mfa.u2f !== undefined">U2F (Universal 2nd Factor)</span>
</td>
</ng-container>
<ng-container matColumnDef="name">
<th mat-header-cell *matHeaderCellDef> {{ 'USER.MFA.NAME' | translate }} </th>
<td mat-cell *matCellDef="let mfa">
<span *ngIf="mfa?.u2f?.name" class="centered">
{{ mfa.u2f.name }}
</span>
</td>
</ng-container>
<ng-container matColumnDef="name">
<th mat-header-cell *matHeaderCellDef> {{ 'USER.MFA.NAME' | translate }} </th>
<td mat-cell *matCellDef="let mfa">
<span *ngIf="mfa?.u2f?.name" class="centered">
{{ mfa.u2f.name }}
</span>
</td>
</ng-container>
<ng-container matColumnDef="state">
<th mat-header-cell *matHeaderCellDef> {{ 'USER.MFA.TABLESTATE' | translate }} </th>
<td mat-cell *matCellDef="let mfa">
<span class="centered">
{{'USER.MFA.STATE.'+ mfa.state | translate}}
</span>
</td>
</ng-container>
<ng-container matColumnDef="state">
<th mat-header-cell *matHeaderCellDef> {{ 'USER.MFA.TABLESTATE' | translate }} </th>
<td mat-cell *matCellDef="let mfa">
<span class="state"
[ngClass]="{'active': mfa.state === AuthFactorState.AUTH_FACTOR_STATE_READY, 'inactive': mfa.state === AuthFactorState.AUTH_FACTOR_STATE_NOT_READY}">
{{'USER.MFA.STATE.'+ mfa.state | translate}}
</span>
</td>
</ng-container>
<ng-container matColumnDef="actions">
<th mat-header-cell *matHeaderCellDef> {{ 'USER.MFA.TABLEACTIONS' | translate }} </th>
<td mat-cell *matCellDef="let mfa">
<button matTooltip="{{'ACTIONS.REMOVE' | translate}}" color="warn" mat-icon-button
(click)="deleteMFA(mfa)">
<i class="las la-trash"></i>
</button>
</td>
</ng-container>
<ng-container matColumnDef="actions">
<th mat-header-cell *matHeaderCellDef></th>
<td mat-cell *matCellDef="let mfa">
<cnsl-table-actions>
<button actions matTooltip="{{'ACTIONS.REMOVE' | translate}}" color="warn" mat-icon-button
(click)="deleteMFA(mfa)">
<i class="las la-trash"></i>
</button>
</cnsl-table-actions>
</td>
</ng-container>
<tr mat-header-row *matHeaderRowDef="displayedColumns"></tr>
<tr class="highlight" mat-row *matRowDef="let row; columns: displayedColumns;">
</tr>
</table>
</cnsl-refresh-table>
<div class="table-wrapper">
<div class="spinner-container" *ngIf="loading$ | async">
<mat-spinner diameter="50"></mat-spinner>
</div>
<tr mat-header-row *matHeaderRowDef="displayedColumns"></tr>
<tr class="highlight" mat-row *matRowDef="let row; columns: displayedColumns;">
</tr>
</table>
<div *ngIf="(loading$ | async) === false && !dataSource?.data?.length" class="no-content-row">
<i class="las la-exclamation"></i>
<span>{{'USER.MFA.EMPTY' | translate}}</span>
</div>
</cnsl-refresh-table>
<div class="table-wrapper">
<div class="spinner-container" *ngIf="loading$ | async">
<mat-spinner diameter="50"></mat-spinner>
</div>
</div>
</cnsl-card>
@@ -13,7 +13,7 @@
.row {
display: flex;
justify-content: space-between;
padding: .5rem 0;
padding: 0.5rem 0;
align-items: center;
span {
@@ -34,16 +34,4 @@
.table {
width: 100%;
td,
th {
&:first-child {
padding-left: 0;
padding-right: 1rem;
}
&:last-child {
padding-right: 0;
}
}
}
@@ -8,7 +8,6 @@ import { AuthFactor, AuthFactorState, User } from 'src/app/proto/generated/zitad
import { ManagementService } from 'src/app/services/mgmt.service';
import { ToastService } from 'src/app/services/toast.service';
export interface MFAItem {
name: string;
verified: boolean;
@@ -33,24 +32,27 @@ export class UserMfaComponent implements OnInit, OnDestroy {
public AuthFactorState: any = AuthFactorState;
public error: string = '';
constructor(private mgmtUserService: ManagementService, private dialog: MatDialog, private toast: ToastService) { }
constructor(private mgmtUserService: ManagementService, private dialog: MatDialog, private toast: ToastService) {}
public ngOnInit(): void {
this.getMFAs();
}
public ngOnDestroy(): void {
ngOnDestroy(): void {
this.mfaSubject.complete();
this.loadingSubject.complete();
}
public getMFAs(): void {
this.mgmtUserService.listHumanMultiFactors(this.user.id).then(mfas => {
this.dataSource = new MatTableDataSource(mfas.resultList);
this.dataSource.sort = this.sort;
}).catch(error => {
this.error = error.message;
});
this.mgmtUserService
.listHumanMultiFactors(this.user.id)
.then((mfas) => {
this.dataSource = new MatTableDataSource(mfas.resultList);
this.dataSource.sort = this.sort;
})
.catch((error) => {
this.error = error.message;
});
}
public deleteMFA(factor: AuthFactor.AsObject): void {
@@ -64,32 +66,38 @@ export class UserMfaComponent implements OnInit, OnDestroy {
width: '400px',
});
dialogRef.afterClosed().subscribe(resp => {
dialogRef.afterClosed().subscribe((resp) => {
if (resp) {
if (factor.otp) {
this.mgmtUserService.removeHumanMultiFactorOTP(this.user.id).then(() => {
this.toast.showInfo('USER.TOAST.OTPREMOVED', true);
this.mgmtUserService
.removeHumanMultiFactorOTP(this.user.id)
.then(() => {
this.toast.showInfo('USER.TOAST.OTPREMOVED', true);
const index = this.dataSource.data.findIndex(mfa => !!mfa.otp);
if (index > -1) {
this.dataSource.data.splice(index, 1);
}
this.getMFAs();
}).catch(error => {
this.toast.showError(error);
});
const index = this.dataSource.data.findIndex((mfa) => !!mfa.otp);
if (index > -1) {
this.dataSource.data.splice(index, 1);
}
this.getMFAs();
})
.catch((error) => {
this.toast.showError(error);
});
} else if (factor.u2f) {
this.mgmtUserService.removeHumanAuthFactorU2F(this.user.id, factor.u2f.id).then(() => {
this.toast.showInfo('USER.TOAST.U2FREMOVED', true);
this.mgmtUserService
.removeHumanAuthFactorU2F(this.user.id, factor.u2f.id)
.then(() => {
this.toast.showInfo('USER.TOAST.U2FREMOVED', true);
const index = this.dataSource.data.findIndex(mfa => !!mfa.u2f);
if (index > -1) {
this.dataSource.data.splice(index, 1);
}
this.getMFAs();
}).catch(error => {
this.toast.showError(error);
});
const index = this.dataSource.data.findIndex((mfa) => !!mfa.u2f);
if (index > -1) {
this.dataSource.data.splice(index, 1);
}
this.getMFAs();
})
.catch((error) => {
this.toast.showError(error);
});
}
}
});
@@ -1,31 +0,0 @@
import { NgModule } from '@angular/core';
import { RouterModule, Routes } from '@angular/router';
import { Type } from 'src/app/proto/generated/zitadel/user_pb';
import { UserListComponent } from './user-list.component';
const routes: Routes = [
{
path: 'humans',
component: UserListComponent,
data: {
animation: 'HomePage',
type: Type.TYPE_HUMAN,
},
},
{
path: 'machines',
component: UserListComponent,
data: {
animation: 'HomePage',
type: Type.TYPE_MACHINE,
},
},
];
@NgModule({
imports: [RouterModule.forChild(routes)],
exports: [RouterModule],
})
export class UserListRoutingModule { }
@@ -1,19 +1,25 @@
<div class="enlarged-container" [ngSwitch]="type">
<div class="max-width-container">
<div class="enlarged-container" [ngSwitch]="type">
<ng-container *ngSwitchCase="Type.TYPE_HUMAN">
<div class="users-title-row">
<h1>{{ 'USER.PAGES.LIST' | translate }}</h1>
<p class="sub">{{ 'USER.PAGES.DESCRIPTION' | translate }}</p>
<a mat-icon-button href="https://docs.zitadel.ch/docs/concepts/structure/users" rel="noreferrer"
target="_blank">
<i class="las la-info-circle"></i>
</a>
</div>
<p class="user-list-sub cnsl-secondary-text">{{ 'USER.PAGES.DESCRIPTION' | translate }}</p>
<cnsl-user-table [type]="Type.TYPE_HUMAN" [disabled]="(['user.write$'] | hasRole | async) === false">
</cnsl-user-table>
<cnsl-user-table [type]="Type.TYPE_HUMAN" [disabled]="(['user.write$'] | hasRole | async) === false">
</cnsl-user-table>
</ng-container>
<ng-container *ngSwitchCase="Type.TYPE_MACHINE">
<h1>{{ 'USER.PAGES.LISTMACHINE' | translate }}</h1>
<p class="sub">{{ 'USER.PAGES.DESCRIPTIONMACHINE' | translate }}</p>
<h1>{{ 'USER.PAGES.LISTMACHINE' | translate }}</h1>
<p class="user-list-sub cnsl-secondary-text">{{ 'USER.PAGES.DESCRIPTIONMACHINE' | translate }}</p>
<cnsl-user-table [type]="Type.TYPE_MACHINE"
[displayedColumns]="['select','name', 'username', 'description','state', 'actions']"
[disabled]="(['user.write$'] | hasRole | async) === false">
</cnsl-user-table>
<cnsl-user-table [type]="Type.TYPE_MACHINE" [disabled]="(['user.write$'] | hasRole | async) === false">
</cnsl-user-table>
</ng-container>
</div>
</div>
@@ -1,9 +1,19 @@
h1 {
margin: 0;
.users-title-row {
display: flex;
align-items: center;
h1 {
margin: 0;
}
a i {
font-size: 1.2rem;
height: 1.2rem;
line-height: 1.2rem;
}
}
.sub {
color: var(--grey);
.user-list-sub {
margin-bottom: 2rem;
font-size: 14px;
}
@@ -1,8 +1,9 @@
import { Component } from '@angular/core';
import { ActivatedRoute } from '@angular/router';
import { ActivatedRoute, Params } from '@angular/router';
import { TranslateService } from '@ngx-translate/core';
import { take } from 'rxjs/operators';
import { Type } from 'src/app/proto/generated/zitadel/user_pb';
import { Breadcrumb, BreadcrumbService, BreadcrumbType } from 'src/app/services/breadcrumb.service';
@Component({
selector: 'cnsl-user-list',
@@ -13,10 +14,25 @@ export class UserListComponent {
public Type: any = Type;
public type: Type = Type.TYPE_HUMAN;
constructor(public translate: TranslateService, activatedRoute: ActivatedRoute) {
activatedRoute.data.pipe(take(1)).subscribe(params => {
constructor(public translate: TranslateService, activatedRoute: ActivatedRoute, breadcrumbService: BreadcrumbService) {
activatedRoute.queryParams.pipe(take(1)).subscribe((params: Params) => {
const { type } = params;
this.type = type;
if (type && type === 'human') {
this.type = Type.TYPE_HUMAN;
} else if (type && type === 'machine') {
this.type = Type.TYPE_MACHINE;
}
});
const iambread = new Breadcrumb({
type: BreadcrumbType.IAM,
name: 'IAM',
routerLink: ['/system'],
});
const bread: Breadcrumb = {
type: BreadcrumbType.ORG,
routerLink: ['/org'],
};
breadcrumbService.setBreadcrumb([iambread, bread]);
}
}
@@ -5,52 +5,58 @@ import { MatButtonModule } from '@angular/material/button';
import { MatCheckboxModule } from '@angular/material/checkbox';
import { MatDialogModule } from '@angular/material/dialog';
import { MatIconModule } from '@angular/material/icon';
import { MatMenuModule } from '@angular/material/menu';
import { MatProgressSpinnerModule } from '@angular/material/progress-spinner';
import { MatSortModule } from '@angular/material/sort';
import { MatTableModule } from '@angular/material/table';
import { MatTooltipModule } from '@angular/material/tooltip';
import { RouterModule } from '@angular/router';
import { TranslateModule } from '@ngx-translate/core';
import { HasRoleModule } from 'src/app/directives/has-role/has-role.module';
import { ActionKeysModule } from 'src/app/modules/action-keys/action-keys.module';
import { AvatarModule } from 'src/app/modules/avatar/avatar.module';
import { CardModule } from 'src/app/modules/card/card.module';
import { FilterUserModule } from 'src/app/modules/filter-user/filter-user.module';
import { InputModule } from 'src/app/modules/input/input.module';
import { PaginatorModule } from 'src/app/modules/paginator/paginator.module';
import { RefreshTableModule } from 'src/app/modules/refresh-table/refresh-table.module';
import { SharedModule } from 'src/app/modules/shared/shared.module';
import { TableActionsModule } from 'src/app/modules/table-actions/table-actions.module';
import { HasRolePipeModule } from 'src/app/pipes/has-role-pipe/has-role-pipe.module';
import { LocalizedDatePipeModule } from 'src/app/pipes/localized-date-pipe/localized-date-pipe.module';
import { TimestampToDatePipeModule } from 'src/app/pipes/timestamp-to-date-pipe/timestamp-to-date-pipe.module';
import { UserListRoutingModule } from './user-list-routing.module';
import { UserListComponent } from './user-list.component';
import { UserTableComponent } from './user-table/user-table.component';
@NgModule({
declarations: [
UserListComponent,
UserTableComponent,
],
imports: [
AvatarModule,
UserListRoutingModule,
CommonModule,
FormsModule,
MatButtonModule,
MatDialogModule,
HasRoleModule,
CardModule,
MatTableModule,
MatIconModule,
MatProgressSpinnerModule,
MatCheckboxModule,
MatTooltipModule,
HasRolePipeModule,
TranslateModule,
SharedModule,
RefreshTableModule,
InputModule,
PaginatorModule,
],
exports: [
UserListComponent,
],
declarations: [UserListComponent, UserTableComponent],
imports: [
AvatarModule,
CommonModule,
FormsModule,
MatButtonModule,
MatDialogModule,
HasRoleModule,
CardModule,
MatTableModule,
MatIconModule,
MatProgressSpinnerModule,
MatCheckboxModule,
MatTooltipModule,
TimestampToDatePipeModule,
LocalizedDatePipeModule,
HasRolePipeModule,
TranslateModule,
FilterUserModule,
RouterModule,
RefreshTableModule,
TableActionsModule,
ActionKeysModule,
MatMenuModule,
MatSortModule,
InputModule,
PaginatorModule,
],
exports: [UserListComponent],
})
export class UserListModule { }
export class UserListModule {}
@@ -1,141 +1,148 @@
<cnsl-refresh-table [loading]="loading$ | async" (refreshed)="refreshPage()" [dataSize]="totalResult"
[timestamp]="viewTimestamp" [selection]="selection" [emitRefreshOnPreviousRoutes]="refreshOnPreviousRoutes">
<cnsl-form-field @appearfade *ngIf="userSearchKey !== undefined" actions class="filtername">
<input class="filterinput" cnslInput (keyup)="applyFilter($event)"
[placeholder]="('USER.TABLE.FILTER.' + userSearchKey.toString()) | translate" #input>
</cnsl-form-field>
[hideRefresh]="true" [timestamp]="viewTimestamp" [selection]="selection"
[emitRefreshOnPreviousRoutes]="refreshOnPreviousRoutes" [showBorder]="true">
<ng-template cnslHasRole [hasRole]="['user.write']" actions>
<button (click)="deactivateSelectedUsers()" matTooltip="{{'USER.TABLE.DEACTIVATE' | translate}}"
class="icon-button" mat-icon-button *ngIf="selection.hasValue()" [disabled]="disabled">
<mat-icon svgIcon="mdi_account_cancel"></mat-icon>
</button>
<button (click)="reactivateSelectedUsers()" matTooltip="{{'USER.TABLE.ACTIVATE' | translate}}"
class="icon-button" mat-icon-button *ngIf="selection.hasValue()" [disabled]="disabled">
<mat-icon svgIcon="mdi_account_check_outline"></mat-icon>
</button>
<a [routerLink]="[ '/users',type === Type.TYPE_HUMAN ? 'create' : 'create-machine']" color="primary"
mat-raised-button [disabled]="disabled">
<mat-icon class="icon">add</mat-icon>{{ 'ACTIONS.NEW' | translate }}
</a>
</ng-template>
<div leftActions class="user-table-left-actions">
<button class="type-button" [ngClass]="{'active': type === Type.TYPE_HUMAN}"
(click)="setType(Type.TYPE_HUMAN)">{{'USER.TABLE.TYPES.HUMAN' | translate}}</button>
<button class="type-button" [ngClass]="{'active': type === Type.TYPE_MACHINE}"
(click)="setType(Type.TYPE_MACHINE)">{{'USER.TABLE.TYPES.MACHINE' | translate}}</button>
</div>
<div class="table-wrapper">
<table class="table" mat-table [dataSource]="dataSource">
<ng-container matColumnDef="select">
<th mat-header-cell *matHeaderCellDef class="selection">
<mat-checkbox [disabled]="disabled" color="primary" (change)="$event ? masterToggle() : null"
[checked]="selection.hasValue() && isAllSelected()"
[indeterminate]="selection.hasValue() && !isAllSelected()">
</mat-checkbox>
</th>
<td mat-cell *matCellDef="let user" class="selection">
<mat-checkbox [disabled]="disabled" color="primary" (click)="$event.stopPropagation()"
(change)="$event ? selection.toggle(user) : null" [checked]="selection.isSelected(user)">
<cnsl-avatar
*ngIf="user.human && user.human.profile.displayName && user.human?.profile.firstName && user.human?.profile.lastName; else cog"
class="avatar" [name]="user.human.profile.displayName" [avatarUrl]="user.human?.profile?.avatarUrl || ''" [forColor]="user?.preferredLoginName" [size]="32">
</cnsl-avatar>
<ng-template #cog>
<div class="sa-icon" *ngIf="user.machine">
<i class="las la-user-cog"></i>
</div>
</ng-template>
</mat-checkbox>
</td>
</ng-container>
<ng-container matColumnDef="displayName">
<th mat-header-cell *matHeaderCellDef
[ngClass]="{'search-active': this.userSearchKey === UserListSearchKey.DISPLAY_NAME}">
{{ 'USER.PROFILE.DISPLAYNAME' | translate }}
<template [ngTemplateOutlet]="templateRef"
[ngTemplateOutletContext]="{key: UserListSearchKey.DISPLAY_NAME}"></template>
</th>
<td mat-cell *matCellDef="let user" [routerLink]="user.id ? ['/users', user.id ]: null">
{{user.human?.profile?.displayName}}
</td>
</ng-container>
<ng-container matColumnDef="name">
<th mat-header-cell *matHeaderCellDef>
{{ 'USER.MACHINE.NAME' | translate }}
</th>
<td mat-cell *matCellDef="let user" [routerLink]="user.id ? ['/users', user.id ]: null">
<span *ngIf="user.human?.name">{{user.human?.name}}</span>
<span *ngIf="user.machine?.name">{{user.machine?.name}}</span>
</td>
</ng-container>
<ng-container matColumnDef="description">
<th mat-header-cell *matHeaderCellDef> {{ 'USER.MACHINE.DESCRIPTION' | translate }} </th>
<td mat-cell *matCellDef="let user" [routerLink]="user.id ? ['/users', user.id ]: null">
<span *ngIf="user.human?.description">{{user.human?.description}}</span>
<span *ngIf="user.machine?.description">{{user.machine?.description}}</span>
</td>
</ng-container>
<ng-container matColumnDef="username">
<th mat-header-cell *matHeaderCellDef
[ngClass]="{'search-active': this.userSearchKey === UserListSearchKey.USER_NAME}">
{{ 'USER.PROFILE.USERNAME' | translate }}
<template [ngTemplateOutlet]="templateRef"
[ngTemplateOutletContext]="{key: UserListSearchKey.USER_NAME}"></template>
</th>
<td mat-cell *matCellDef="let user" [routerLink]="user.id ? ['/users', user.id ]: null">
{{user.userName}} </td>
</ng-container>
<ng-container matColumnDef="email">
<th mat-header-cell *matHeaderCellDef
[ngClass]="{'search-active': this.UserListSearchKey === UserListSearchKey.EMAIL}">
{{ 'USER.EMAIL' | translate }}
<template [ngTemplateOutlet]="templateRef"
[ngTemplateOutletContext]="{key: UserListSearchKey.EMAIL}"></template>
</th>
<td mat-cell *matCellDef="let user" [routerLink]="user.id ? ['/users', user.id ]: null">
<span *ngIf="user.human?.email?.email">{{user.human?.email.email}}</span>
</td>
</ng-container>
<ng-container matColumnDef="state">
<th mat-header-cell *matHeaderCellDef> {{ 'USER.DATA.STATE' | translate }} </th>
<td mat-cell *matCellDef="let user" [routerLink]="user.id ? ['/users', user.id ]: null">
<span class="state"
[ngClass]="{'active': user.state === UserState.USER_STATE_ACTIVE, 'inactive': user.state === UserState.USER_STATE_INACTIVE}">
{{ 'USER.DATA.STATE'+user.state | translate }}
</span>
</td>
</ng-container>
<ng-container matColumnDef="actions" stickyEnd>
<th mat-header-cell *matHeaderCellDef></th>
<td mat-cell *matCellDef="let user">
<button class="dlt-button"
[disabled]="(['user.delete$', 'user.delete:'+user.id] | hasRole | async) === false" color="warn"
mat-icon-button matTooltip="{{'USER.PAGES.DELETE' | translate}}" (click)="deleteUser(user)">
<i class="las la-trash"></i>
</button>
</td>
</ng-container>
<tr mat-header-row *matHeaderRowDef="displayedColumns"></tr>
<tr class="highlight" mat-row *matRowDef="let row; columns: displayedColumns;"> </tr>
</table>
<div *ngIf="(loading$ | async) === false && !dataSource?.data?.length" class="no-content-row">
<i class="las la-exclamation"></i>
<span>{{'USER.TABLE.EMPTY' | translate}}</span>
</div>
<cnsl-paginator #paginator class="paginator" [timestamp]="viewTimestamp" [length]="totalResult || 0" [pageSize]="10" [timestamp]="viewTimestamp"
[pageSizeOptions]="[5, 10, 20]" (page)="changePage($event)"></cnsl-paginator>
</div>
</cnsl-refresh-table>
<ng-template #templateRef let-key="key">
<button class="search-button" mat-icon-button (click)="setFilter(key)">
<mat-icon class="icon" *ngIf="this.userSearchKey !== key">search</mat-icon>
<mat-icon class="icon" *ngIf="this.userSearchKey === key">search_off</mat-icon>
<ng-template cnslHasRole [hasRole]="['user.write']" actions>
<button (click)="deactivateSelectedUsers()" class="cnsl-action-button bg-state inactive" mat-raised-button
*ngIf="selection.hasValue() && multipleDeactivatePossible" [disabled]="disabled" color="primary">
<span class="">{{'USER.TABLE.DEACTIVATE' | translate}}</span>
<cnsl-action-keys (actionTriggered)="deactivateSelectedUsers()" [type]="ActionKeysType.DEACTIVATE">
</cnsl-action-keys>
</button>
</ng-template>
<button (click)="reactivateSelectedUsers()" class="cnsl-action-button bg-state active margin-left" mat-raised-button
*ngIf="selection.hasValue() && multipleActivatePossible" [disabled]="disabled" color="primary">
<span class="">{{'USER.TABLE.ACTIVATE' | translate}}</span>
<cnsl-action-keys (actionTriggered)="reactivateSelectedUsers()" [type]="ActionKeysType.REACTIVATE">
</cnsl-action-keys>
</button>
<cnsl-filter-user *ngIf="!selection.hasValue()" (filterChanged)="applySearchQuery($any($event))"
(filterOpen)="filterOpen = $event"></cnsl-filter-user>
<a *ngIf="!selection.hasValue()" [routerLink]="[ '/users',type === Type.TYPE_HUMAN ? 'create' : 'create-machine']"
color="primary" mat-raised-button [disabled]="disabled" class="cnsl-action-button">
<mat-icon class="icon">add</mat-icon>
<span>{{ 'ACTIONS.NEW' | translate }}</span>
<cnsl-action-keys *ngIf="!filterOpen"
(actionTriggered)="gotoRouterLink([ '/users',type === Type.TYPE_HUMAN ? 'create' : 'create-machine'])">
</cnsl-action-keys>
</a>
</ng-template>
<div class="table-wrapper">
<table class="table" mat-table [dataSource]="dataSource" matSort (matSortChange)="sortChange($event)">
<ng-container matColumnDef="select">
<th mat-header-cell *matHeaderCellDef class="selection">
<mat-checkbox [disabled]="disabled" color="primary" (change)="$event ? masterToggle() : null"
[checked]="selection.hasValue() && isAllSelected()"
[indeterminate]="selection.hasValue() && !isAllSelected()">
</mat-checkbox>
</th>
<td mat-cell *matCellDef="let user" class="selection">
<mat-checkbox [disabled]="disabled" color="primary" (click)="$event.stopPropagation()"
(change)="$event ? selection.toggle(user) : null" [checked]="selection.isSelected(user)">
<cnsl-avatar
*ngIf="user.human && user.human.profile.displayName && user.human?.profile.firstName && user.human?.profile.lastName; else cog"
class="avatar" [name]="user.human.profile.displayName" [avatarUrl]="user.human?.profile?.avatarUrl || ''"
[forColor]="user?.preferredLoginName" [size]="32">
</cnsl-avatar>
<ng-template #cog>
<cnsl-avatar [forColor]="user?.preferredLoginName" [isMachine]="true">
<i class="las la-robot"></i>
</cnsl-avatar>
</ng-template>
</mat-checkbox>
</td>
</ng-container>
<ng-container matColumnDef="displayName">
<th mat-header-cell *matHeaderCellDef mat-sort-header
[ngClass]="{'search-active': this.userSearchKey === UserListSearchKey.DISPLAY_NAME}">
{{ 'USER.PROFILE.DISPLAYNAME' | translate }}
</th>
<td mat-cell *matCellDef="let user" [routerLink]="user.id ? ['/users', user.id ]: null">
<span *ngIf="user.human">{{user.human.profile?.displayName}}</span>
<span *ngIf="user.machine">{{user.machine.name}}</span>
</td>
</ng-container>
<ng-container matColumnDef="username">
<th mat-header-cell *matHeaderCellDef mat-sort-header
[ngClass]="{'search-active': this.userSearchKey === UserListSearchKey.USER_NAME}">
{{ 'USER.PROFILE.USERNAME' | translate }}
</th>
<td mat-cell *matCellDef="let user" [routerLink]="user.id ? ['/users', user.id ]: null">
{{user.userName}} </td>
</ng-container>
<ng-container matColumnDef="email">
<th mat-header-cell *matHeaderCellDef mat-sort-header
[ngClass]="{'search-active': this.UserListSearchKey === UserListSearchKey.EMAIL}">
{{ 'USER.EMAIL' | translate }}
</th>
<td mat-cell *matCellDef="let user" [routerLink]="user.id ? ['/users', user.id ]: null">
<span *ngIf="user.human?.email?.email">{{user.human?.email.email}}</span>
</td>
</ng-container>
<ng-container matColumnDef="state">
<th mat-header-cell *matHeaderCellDef mat-sort-header> {{ 'USER.DATA.STATE' | translate }} </th>
<td mat-cell *matCellDef="let user" [routerLink]="user.id ? ['/users', user.id ]: null">
<span class="state"
[ngClass]="{'active': user.state === UserState.USER_STATE_ACTIVE, 'inactive': user.state === UserState.USER_STATE_INACTIVE}">
{{ 'USER.DATA.STATE'+user.state | translate }}
</span>
</td>
</ng-container>
<ng-container matColumnDef="creationDate">
<th mat-header-cell *matHeaderCellDef> {{'USER.TABLE.CREATIONDATE' | translate}} </th>
<td mat-cell *matCellDef="let user" [routerLink]="user.id ? ['/users', user.id ]: null">
<span class="no-break">{{user.details.creationDate | timestampToDate | localizedDate: 'fromNow'
}}</span>
</td>
</ng-container>
<ng-container matColumnDef="changeDate">
<th mat-header-cell *matHeaderCellDef> {{ 'USER.TABLE.CHANGEDATE' | translate }} </th>
<td mat-cell *matCellDef="let user" [routerLink]="user.id ? ['/users', user.id ]: null">
<span class="no-break">{{user.details.changeDate | timestampToDate | localizedDate: 'fromNow'
}}</span>
</td>
</ng-container>
<ng-container matColumnDef="actions" stickyEnd>
<th mat-header-cell *matHeaderCellDef class="user-tr-actions"></th>
<td mat-cell *matCellDef="let user" class="user-tr-actions">
<cnsl-table-actions>
<button actions matTooltip="{{'ACTIONS.REMOVE' | translate}}" color="warn" (click)="deleteUser(user)"
mat-icon-button>
<i class="las la-trash"></i>
</button>
</cnsl-table-actions>
</td>
</ng-container>
<tr mat-header-row *matHeaderRowDef="type === Type.TYPE_HUMAN ? displayedColumnsHuman : displayedColumnsMachine">
</tr>
<tr class="highlight pointer" mat-row
*matRowDef="let user; columns: (type === Type.TYPE_HUMAN ? displayedColumnsHuman : displayedColumnsMachine);">
</tr>
</table>
</div>
<div *ngIf="(loading$ | async) === false && !dataSource?.data?.length" class="no-content-row">
<i class="las la-exclamation"></i>
<span>{{'USER.TABLE.EMPTY' | translate}}</span>
</div>
<cnsl-paginator #paginator class="paginator" [timestamp]="viewTimestamp" [length]="totalResult || 0"
[pageSize]="INITIAL_PAGE_SIZE" [timestamp]="viewTimestamp" [pageSizeOptions]="[10, 20, 50, 100]"
(page)="changePage($event)"></cnsl-paginator>
</cnsl-refresh-table>
@@ -1,77 +1,43 @@
.table-wrapper {
overflow: auto;
@use '@angular/material' as mat;
.table,
.paginator {
width: 100%;
@mixin user-table-theme($theme) {
$foreground: map-get($theme, foreground);
$is-dark-theme: map-get($theme, is-dark);
td,
th {
padding-left: .5rem;
padding-right: .5rem;
.user-table-left-actions {
display: flex;
align-items: center;
.type-button {
border: none;
background: none;
text-align: left;
padding: 0.75rem 0;
opacity: 0.6;
font-size: 15px;
cursor: pointer;
color: map-get($foreground, base);
&:first-child {
padding-left: 0;
padding-right: .5rem;
}
&:last-child {
padding-right: 0;
}
}
th {
.search-button {
visibility: hidden;
width: 30px;
.icon {
font-size: 1.2rem;
}
}
&:hover,
&.search-active {
.search-button {
visibility: visible;
}
}
}
tr {
.dlt-button {
visibility: hidden;
margin-right: 1rem;
}
&:hover {
.dlt-button {
visibility: visible;
}
opacity: 1;
}
&.active {
font-weight: 600;
opacity: 1;
}
}
}
}
.filtername {
flex: 1;
margin-right: 1rem;
.filterinput {
height: 36px;
transform: translateY(1px);
}
.user-tr-actions {
width: 42px;
}
.sa-icon {
display: block;
width: 32px;
margin: 0 .5rem;
i {
margin: auto;
}
}
td {
outline: none;
.margin-left {
margin-left: 0.5rem;
}
@@ -1,28 +1,19 @@
import { LiveAnnouncer } from '@angular/cdk/a11y';
import { SelectionModel } from '@angular/cdk/collections';
import { Component, EventEmitter, Input, OnInit, Output, ViewChild } from '@angular/core';
import { MatDialog } from '@angular/material/dialog';
import { MatSort, Sort } from '@angular/material/sort';
import { MatTableDataSource } from '@angular/material/table';
import { ActivatedRoute } from '@angular/router';
import { ActivatedRoute, Router } from '@angular/router';
import { TranslateService } from '@ngx-translate/core';
import { BehaviorSubject, Observable } from 'rxjs';
import { take } from 'rxjs/operators';
import { enterAnimations } from 'src/app/animations';
import { ActionKeysType } from 'src/app/modules/action-keys/action-keys.component';
import { PageEvent, PaginatorComponent } from 'src/app/modules/paginator/paginator.component';
import { WarnDialogComponent } from 'src/app/modules/warn-dialog/warn-dialog.component';
import { Timestamp } from 'src/app/proto/generated/google/protobuf/timestamp_pb';
import { TextQueryMethod } from 'src/app/proto/generated/zitadel/object_pb';
import {
DisplayNameQuery,
EmailQuery,
FirstNameQuery,
LastNameQuery,
SearchQuery,
Type,
TypeQuery,
User,
UserNameQuery,
UserState,
} from 'src/app/proto/generated/zitadel/user_pb';
import { SearchQuery, Type, TypeQuery, User, UserFieldName, UserState } from 'src/app/proto/generated/zitadel/user_pb';
import { GrpcAuthService } from 'src/app/services/grpc-auth.service';
import { ManagementService } from 'src/app/services/mgmt.service';
import { ToastService } from 'src/app/services/toast.service';
@@ -44,11 +35,12 @@ enum UserListSearchKey {
export class UserTableComponent implements OnInit {
public userSearchKey: UserListSearchKey | undefined = undefined;
public Type: any = Type;
@Input() type: Type = Type.TYPE_HUMAN;
@Input() public type: Type = Type.TYPE_HUMAN;
@Input() refreshOnPreviousRoutes: string[] = [];
@Input() disabled: boolean = false;
@ViewChild(PaginatorComponent) public paginator!: PaginatorComponent;
@ViewChild('input') public filter!: Input;
@ViewChild(MatSort) public sort!: MatSort;
public INITIAL_PAGE_SIZE: number = 20;
public viewTimestamp!: Timestamp.AsObject;
public totalResult: number = 0;
@@ -56,20 +48,43 @@ export class UserTableComponent implements OnInit {
public selection: SelectionModel<User.AsObject> = new SelectionModel<User.AsObject>(true, []);
private loadingSubject: BehaviorSubject<boolean> = new BehaviorSubject<boolean>(false);
public loading$: Observable<boolean> = this.loadingSubject.asObservable();
@Input() public displayedColumns: string[] = ['select', 'displayName', 'username', 'email', 'state', 'actions'];
@Input() public displayedColumnsHuman: string[] = [
'select',
'displayName',
'username',
'email',
'state',
'creationDate',
'changeDate',
'actions',
];
@Input() public displayedColumnsMachine: string[] = [
'select',
'displayName',
'username',
'creationDate',
'changeDate',
'state',
'actions',
];
@Output() public changedSelection: EventEmitter<Array<User.AsObject>> = new EventEmitter();
public UserState: any = UserState;
public UserListSearchKey: any = UserListSearchKey;
public ActionKeysType: any = ActionKeysType;
public filterOpen: boolean = false;
constructor(
private router: Router,
public translate: TranslateService,
private authService: GrpcAuthService,
private userService: ManagementService,
private toast: ToastService,
private dialog: MatDialog,
private route: ActivatedRoute,
private _liveAnnouncer: LiveAnnouncer,
) {
this.selection.changed.subscribe(() => {
this.changedSelection.emit(this.selection.selected);
@@ -78,15 +93,28 @@ export class UserTableComponent implements OnInit {
ngOnInit(): void {
this.route.queryParams.pipe(take(1)).subscribe((params) => {
this.getData(10, 0, this.type);
this.getData(this.INITIAL_PAGE_SIZE, 0, this.type);
if (params.deferredReload) {
setTimeout(() => {
this.getData(10, 0, this.type);
this.getData(this.paginator.pageSize, this.paginator.pageIndex * this.paginator.pageSize, this.type);
}, 2000);
}
});
}
public setType(type: Type): void {
this.type = type;
this.router.navigate([], {
relativeTo: this.route,
queryParams: {
type: type === Type.TYPE_HUMAN ? 'human' : type === Type.TYPE_MACHINE ? 'machine' : 'human',
},
queryParamsHandling: 'merge',
skipLocationChange: false,
});
this.getData(this.paginator.pageSize, this.paginator.pageIndex * this.paginator.pageSize, this.type);
}
public isAllSelected(): boolean {
const numSelected = this.selection.selected.length;
const numRows = this.dataSource.data.length;
@@ -142,55 +170,42 @@ export class UserTableComponent implements OnInit {
});
}
private async getData(limit: number, offset: number, type: Type, searchValue?: string): Promise<void> {
public gotoRouterLink(rL: any): void {
this.router.navigate(rL);
}
private async getData(limit: number, offset: number, type: Type, searchQueries?: SearchQuery[]): Promise<void> {
this.loadingSubject.next(true);
const query = new SearchQuery();
let queryT = new SearchQuery();
const typeQuery = new TypeQuery();
typeQuery.setType(type);
query.setTypeQuery(typeQuery);
queryT.setTypeQuery(typeQuery);
if (searchValue && this.userSearchKey !== undefined) {
switch (this.userSearchKey) {
case UserListSearchKey.DISPLAY_NAME:
const dNQuery = new DisplayNameQuery();
dNQuery.setDisplayName(searchValue);
dNQuery.setMethod(TextQueryMethod.TEXT_QUERY_METHOD_CONTAINS_IGNORE_CASE);
query.setDisplayNameQuery(dNQuery);
let sortingField: UserFieldName | undefined = undefined;
if (this.sort?.active && this.sort?.direction)
switch (this.sort.active) {
case 'displayName':
sortingField = UserFieldName.USER_FIELD_NAME_DISPLAY_NAME;
break;
case UserListSearchKey.USER_NAME:
const uNQuery = new UserNameQuery();
uNQuery.setUserName(searchValue);
uNQuery.setMethod(TextQueryMethod.TEXT_QUERY_METHOD_CONTAINS_IGNORE_CASE);
query.setUserNameQuery(uNQuery);
case 'username':
sortingField = UserFieldName.USER_FIELD_NAME_USER_NAME;
break;
case UserListSearchKey.FIRST_NAME:
const fNQuery = new FirstNameQuery();
fNQuery.setFirstName(searchValue);
fNQuery.setMethod(TextQueryMethod.TEXT_QUERY_METHOD_CONTAINS_IGNORE_CASE);
query.setFirstNameQuery(fNQuery);
case 'email':
sortingField = UserFieldName.USER_FIELD_NAME_EMAIL;
break;
case UserListSearchKey.FIRST_NAME:
const lNQuery = new LastNameQuery();
lNQuery.setLastName(searchValue);
lNQuery.setMethod(TextQueryMethod.TEXT_QUERY_METHOD_CONTAINS_IGNORE_CASE);
query.setLastNameQuery(lNQuery);
break;
case UserListSearchKey.EMAIL:
const eQuery = new EmailQuery();
eQuery.setEmailAddress(searchValue);
eQuery.setMethod(TextQueryMethod.TEXT_QUERY_METHOD_CONTAINS_IGNORE_CASE);
query.setEmailQuery(eQuery);
case 'state':
sortingField = UserFieldName.USER_FIELD_NAME_STATE;
break;
}
}
this.userService
.listUsers(limit, offset, [query])
.listUsers(
limit,
offset,
searchQueries?.length ? [queryT, ...searchQueries] : [queryT],
sortingField,
this.sort?.direction,
)
.then((resp) => {
if (resp.details?.totalResult) {
this.totalResult = resp.details?.totalResult;
@@ -213,26 +228,20 @@ export class UserTableComponent implements OnInit {
this.getData(this.paginator.pageSize, this.paginator.pageIndex * this.paginator.pageSize, this.type);
}
public applyFilter(event: Event): void {
this.selection.clear();
const filterValue = (event.target as HTMLInputElement).value;
public sortChange(sortState: Sort) {
console.log(sortState.active, sortState.direction);
this.getData(this.paginator.pageSize, this.paginator.pageIndex * this.paginator.pageSize, this.type, filterValue);
if (sortState.direction && sortState.active) {
this._liveAnnouncer.announce(`Sorted ${sortState.direction}ending`);
this.refreshPage();
} else {
this._liveAnnouncer.announce('Sorting cleared');
}
}
public setFilter(key: UserListSearchKey): void {
setTimeout(() => {
if (this.filter) {
(this.filter as any).nativeElement.focus();
}
}, 100);
if (this.userSearchKey !== key) {
this.userSearchKey = key;
} else {
this.userSearchKey = undefined;
this.refreshPage();
}
public applySearchQuery(searchQueries: SearchQuery[]): void {
this.selection.clear();
this.getData(this.paginator.pageSize, this.paginator.pageIndex * this.paginator.pageSize, this.type, searchQueries);
}
public deleteUser(user: User.AsObject): void {
@@ -289,4 +298,14 @@ export class UserTableComponent implements OnInit {
});
}
}
public get multipleActivatePossible(): boolean {
const selected = this.selection.selected;
return selected ? selected.findIndex((user) => user.state !== UserState.USER_STATE_ACTIVE) > -1 : false;
}
public get multipleDeactivatePossible(): boolean {
const selected = this.selection.selected;
return selected ? selected.findIndex((user) => user.state !== UserState.USER_STATE_INACTIVE) > -1 : false;
}
}
@@ -0,0 +1,76 @@
import { NgModule } from '@angular/core';
import { RouterModule, Routes } from '@angular/router';
import { AuthGuard } from 'src/app/guards/auth.guard';
import { RoleGuard } from 'src/app/guards/role.guard';
import { UserGuard } from 'src/app/guards/user.guard';
import { Type } from 'src/app/proto/generated/zitadel/user_pb';
import { AuthUserDetailComponent } from './user-detail/auth-user-detail/auth-user-detail.component';
import { PasswordComponent } from './user-detail/password/password.component';
import { UserDetailComponent } from './user-detail/user-detail/user-detail.component';
import { UserListComponent } from './user-list/user-list.component';
const routes: Routes = [
{
path: '',
component: UserListComponent,
data: {
animation: 'HomePage',
type: Type.TYPE_HUMAN,
},
},
{
path: 'create',
loadChildren: () => import('./user-create/user-create.module').then((m) => m.UserCreateModule),
canActivate: [AuthGuard, RoleGuard],
data: {
roles: ['user.write'],
},
},
{
path: 'create-machine',
loadChildren: () => import('./user-create-machine/user-create-machine.module').then((m) => m.UserCreateMachineModule),
canActivate: [AuthGuard, RoleGuard],
data: {
roles: ['user.write'],
},
},
{
path: 'me',
component: AuthUserDetailComponent,
canActivate: [AuthGuard],
data: {
animation: 'HomePage',
},
},
{
path: 'me/password',
component: PasswordComponent,
canActivate: [AuthGuard],
data: { animation: 'AddPage' },
},
{
path: ':id',
component: UserDetailComponent,
canActivate: [AuthGuard, UserGuard, RoleGuard],
data: {
roles: ['user.read'],
animation: 'HomePage',
},
},
{
path: ':id/password',
component: PasswordComponent,
canActivate: [AuthGuard, RoleGuard],
data: {
roles: ['user.write'],
animation: 'AddPage',
},
},
];
@NgModule({
imports: [RouterModule.forChild(routes)],
exports: [RouterModule],
})
export class UsersRoutingModule {}
@@ -0,0 +1,14 @@
import { CommonModule } from '@angular/common';
import { NgModule } from '@angular/core';
import { ChangesModule } from 'src/app/modules/changes/changes.module';
import { SharedModule } from 'src/app/modules/shared/shared.module';
import { UserDetailModule } from './user-detail/user-detail.module';
import { UserListModule } from './user-list/user-list.module';
import { UsersRoutingModule } from './users-routing.module';
@NgModule({
declarations: [],
imports: [UsersRoutingModule, SharedModule, UserListModule, UserDetailModule, ChangesModule, CommonModule],
})
export class UsersModule {}