mirror of
https://github.com/andwati/zitadel.git
synced 2026-11-15 10:25:11 +00:00
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:
co-authored by
Livio Amstutz
parent
00f7dbe875
commit
08ae39ae19
+43
-32
@@ -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>
|
||||
+43
-29
@@ -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
-3
@@ -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>
|
||||
|
||||
|
||||
+5
-6
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
+90
-67
@@ -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);
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+50
-45
@@ -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>
|
||||
-30
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+51
-44
@@ -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);
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
+4
-4
@@ -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>
|
||||
+2
-3
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
+94
-68
@@ -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);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
+92
-94
@@ -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>
|
||||
+80
-39
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+48
-1
@@ -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,
|
||||
|
||||
+59
-50
@@ -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>
|
||||
-28
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+45
-47
@@ -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);
|
||||
});
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
+13
-13
@@ -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>
|
||||
+2
-3
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
+11
-11
@@ -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>
|
||||
+2
-2
@@ -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>
|
||||
|
||||
|
||||
-1
@@ -8,7 +8,6 @@
|
||||
|
||||
.desc {
|
||||
font-size: 14px;
|
||||
color: var(--grey);
|
||||
}
|
||||
|
||||
.action {
|
||||
|
||||
+12
-12
@@ -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>
|
||||
+2
-3
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
-16
@@ -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);
|
||||
}
|
||||
}
|
||||
-15
@@ -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>
|
||||
-106
@@ -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%);
|
||||
}
|
||||
-25
@@ -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();
|
||||
});
|
||||
});
|
||||
-32
@@ -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);
|
||||
}
|
||||
|
||||
+26
-26
@@ -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>
|
||||
|
||||
+12
-8
@@ -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>
|
||||
+2
-3
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+57
-49
@@ -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);
|
||||
});
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
-77
@@ -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();
|
||||
}
|
||||
}
|
||||
-90
@@ -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>
|
||||
-55
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
-25
@@ -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();
|
||||
});
|
||||
});
|
||||
-259
@@ -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];
|
||||
}
|
||||
}
|
||||
+24
-22
@@ -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>
|
||||
+4
-5
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
+26
-21
@@ -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,
|
||||
|
||||
+23
-17
@@ -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>
|
||||
|
||||
-31
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+1
-1
@@ -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);
|
||||
});
|
||||
|
||||
|
||||
+53
-44
@@ -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>
|
||||
+1
-13
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+38
-30
@@ -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 {}
|
||||
Reference in New Issue
Block a user