fix: part console ui (#1430)

* home, user create, list, app list

* org create fix

* fix user bindings, usertable, changes, projects

* fix app grid type, domains

* e.164 international phonenumber validation, create user, phone email dialog

* single authmethod create dialog

* fix timestamp to date pipe, app create secret dialog

* validate e164 number edit dialog

* fix machine keys, timestamp pipe

* rm comment

* projecttype circular deps

* downgrade protoc to 3.13.0 due to deserializer error, fix circular dep

* apptype controlname
This commit is contained in:
Max Peintner
2021-03-18 10:46:22 +01:00
committed by GitHub
parent 3cdaa8193d
commit bc7e650089
62 changed files with 530 additions and 355 deletions

View File

@@ -19,7 +19,7 @@
<cnsl-label>{{ 'USER.PROFILE.USERNAME' | translate }}*</cnsl-label>
<input cnslInput formControlName="userName" required
[ngStyle]="{'padding-right': suffixPadding ? suffixPadding : '10px'}" />
<span #suffix *ngIf="envSuffixLabel" cnslSuffix (click)="logsuff()">{{envSuffixLabel}}</span>
<span #suffix *ngIf="envSuffixLabel" cnslSuffix>{{envSuffixLabel}}</span>
<span cnsl-error *ngIf="userName?.invalid && userName?.errors?.required">
{{ 'USER.VALIDATION.REQUIRED' | translate }}
@@ -88,8 +88,8 @@
</cnsl-form-field>
</div>
<div class="btn-container">
<button color="primary" [disabled]="userForm.invalid" type="submit"
mat-raised-button>{{ 'ACTIONS.CREATE' | translate }}</button>
<button color="primary" [disabled]="userForm.invalid" type="submit" mat-raised-button>{{ 'ACTIONS.CREATE' |
translate }}</button>
</div>
</form>
</app-detail-layout>

View File

@@ -1,7 +1,9 @@
import { ChangeDetectorRef, Component, OnDestroy, ViewChild } from '@angular/core';
import { AbstractControl, FormBuilder, FormGroup, Validators } from '@angular/forms';
import { Router } from '@angular/router';
import { Subscription } from 'rxjs';
import parsePhoneNumber from 'libphonenumber-js';
import { Subject } from 'rxjs';
import { debounceTime, takeUntil } from 'rxjs/operators';
import { AddHumanUserRequest } from 'src/app/proto/generated/zitadel/management_pb';
import { Domain } from 'src/app/proto/generated/zitadel/org_pb';
import { Gender } from 'src/app/proto/generated/zitadel/user_pb';
@@ -37,7 +39,7 @@ export class UserCreateComponent implements OnDestroy {
public languages: string[] = ['de', 'en'];
public userForm!: FormGroup;
public envSuffixLabel: string = '';
private sub: Subscription = new Subscription();
private destroyed$: Subject<void> = new Subject();
public userLoginMustBeDomain: boolean = false;
public loading: boolean = false;
@@ -92,15 +94,23 @@ export class UserCreateComponent implements OnDestroy {
firstName: ['', Validators.required],
lastName: ['', Validators.required],
nickName: [''],
gender: [Gender.GENDER_UNSPECIFIED],
gender: [],
preferredLanguage: [''],
phone: [''],
});
}
public logsuff(): void {
console.log((this.suffix.nativeElement as HTMLElement), (this.suffix.nativeElement as HTMLElement).offsetWidth);
this.userForm.controls['phone'].valueChanges.pipe(
takeUntil(this.destroyed$),
debounceTime(300)).subscribe(value => {
const phoneNumber = parsePhoneNumber(value ?? '', 'CH');
if (phoneNumber) {
const formmatted = phoneNumber.formatInternational();
const country = phoneNumber.country;
if (this.phone && country && this.phone.value && this.phone.value !== formmatted) {
this.phone.setValue(formmatted);
}
}
});
}
public createUser(): void {
@@ -119,8 +129,11 @@ export class UserCreateComponent implements OnDestroy {
humanReq.setUserName(this.userName?.value);
humanReq.setProfile(profileReq);
humanReq.setEmail(this.email?.value);
humanReq.setPhone(this.phone?.value);
humanReq.setEmail(new AddHumanUserRequest.Email().setEmail(this.email?.value));
if (this.phone && this.phone.value) {
humanReq.setPhone(new AddHumanUserRequest.Phone().setPhone(this.phone.value));
}
this.mgmtService
.addHumanUser(humanReq)
@@ -136,7 +149,8 @@ export class UserCreateComponent implements OnDestroy {
}
ngOnDestroy(): void {
this.sub.unsubscribe();
this.destroyed$.next();
this.destroyed$.complete();
}
public get email(): AbstractControl | null {

View File

@@ -0,0 +1,50 @@
<h1 mat-dialog-title>
<span class="title">{{'USER.CODEDIALOG.TITLE' | translate}} {{data?.number}}</span>
</h1>
<p class="desc">{{'USER.CODEDIALOG.DESCRIPTION' | translate}}</p>
<div mat-dialog-content>
<div class="type-selection">
<button class="otp" (click)="selectType(AuthFactorType.OTP)">
<mat-icon class="icon" svgIcon="mdi_radar"></mat-icon>
<span>{{'USER.MFA.OTP' | translate}}</span>
</button>
<button class="u2f" (click)="selectType(AuthFactorType.U2F)">
<i class="las la-fingerprint"></i>
<span>{{'USER.MFA.U2F' | translate}}</span>
</button>
</div>
<div class="otp" *ngIf="selectedType == AuthFactorType.OTP">
<p>{{'USER.MFA.OTP_DIALOG_DESCRIPTION' | translate}}</p>
<div class="qrcode-wrapper">
<qrcode *ngIf="otpurl" class="qrcode" [qrdata]="otpurl" [width]="150" [errorCorrectionLevel]="'M'"></qrcode>
</div>
<cnsl-form-field class="form-field" label="Access Code" required="true">
<cnsl-label>Code</cnsl-label>
<input cnslInput [(ngModel)]="otpcode" />
</cnsl-form-field>
</div>
<div class="u2f" *ngIf="selectedType == AuthFactorType.U2F">
<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)]="u2fname" required (keydown.enter)="u2fname ? submitU2F() : null" />
</cnsl-form-field>
<mat-spinner diameter="30" *ngIf="u2fLoading"></mat-spinner>
<p class="error">{{u2fError}}</p>
</div>
</div>
<div mat-dialog-actions class="action">
<button cdkFocusInitial color="primary" mat-button class="ok-button" (click)="closeDialog()">
{{'ACTIONS.CLOSE' | translate}}
</button>
<button cdkFocusInitial color="primary" mat-raised-button class="ok-button" (click)="submitAuth()">
{{'ACTIONS.CREATE' | translate}}
</button>
</div>

View File

@@ -0,0 +1,35 @@
.type-selection {
display: flex;
margin: 0 -0.5rem;
.otp,
.u2f {
flex: 1;
min-height: 100px;
border-radius: .5rem;
border: 1px solid var(--grey);
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
padding: 1rem;
margin: .5rem;
}
}
.formfield {
width: 100%;
}
.action {
display: flex;
justify-content: flex-end;
.ok-button {
margin-left: .5rem;
}
button {
border-radius: .5rem;
}
}

View File

@@ -0,0 +1,25 @@
import { ComponentFixture, TestBed, waitForAsync } from '@angular/core/testing';
import { AuthFactorDialogComponent } from './auth-factor-dialog.component';
describe('CodeDialogComponent', () => {
let component: AuthFactorDialogComponent;
let fixture: ComponentFixture<AuthFactorDialogComponent>;
beforeEach(waitForAsync(() => {
TestBed.configureTestingModule({
declarations: [AuthFactorDialogComponent],
})
.compileComponents();
}));
beforeEach(() => {
fixture = TestBed.createComponent(AuthFactorDialogComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should create', () => {
expect(component).toBeTruthy();
});
});

View File

@@ -0,0 +1,138 @@
import { Component, Inject } from '@angular/core';
import { MAT_DIALOG_DATA, MatDialogRef } from '@angular/material/dialog';
import { TranslateService } from '@ngx-translate/core';
import { take } from 'rxjs/operators';
import { GrpcAuthService } from 'src/app/services/grpc-auth.service';
import { ToastService } from 'src/app/services/toast.service';
import { _base64ToArrayBuffer } from '../../u2f-util';
import { _arrayBufferToBase64 } from '../u2f_util';
export enum AuthFactorType {
OTP,
U2F,
}
@Component({
selector: 'app-auth-factor-dialog',
templateUrl: './auth-factor-dialog.component.html',
styleUrls: ['./auth-factor-dialog.component.scss'],
})
export class AuthFactorDialogComponent {
public otpurl: string = '';
public otpcode: string = '';
public u2fname: string = '';
public u2fCredentialOptions!: CredentialCreationOptions;
public u2fLoading: boolean = false;
public u2fError: string = '';
AuthFactorType: any = AuthFactorType;
selectedType!: AuthFactorType;
constructor(
private authService: GrpcAuthService,
private toast: ToastService,
private translate: TranslateService,
public dialogRef: MatDialogRef<AuthFactorDialogComponent>,
@Inject(MAT_DIALOG_DATA) public data: any,
) { }
closeDialog(code: string = ''): void {
this.dialogRef.close(code);
}
public selectType(type: AuthFactorType): void {
if (type == AuthFactorType.OTP) {
this.authService.addMyMultiFactorOTP().then((otpresp) => {
this.otpurl = otpresp.url;
}, error => {
this.toast.showError(error);
});
} else if (type == AuthFactorType.U2F) {
this.authService.addMyMultiFactorU2F().then((u2fresp) => {
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;
});
}
this.u2fCredentialOptions = credOptions;
}
}, error => {
this.toast.showError(error);
});
}
}
public submitAuth() {
if (this.selectedType == AuthFactorType.OTP) {
this.submitOTP();
} else if (this.selectedType == AuthFactorType.U2F) {
this.submitU2F();
}
}
public submitOTP(): void {
this.authService.verifyMyMultiFactorOTP(this.otpcode).then(() => {
this.dialogRef.close(true);
}, 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.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 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);
}
}).catch(error => {
this.u2fLoading = false;
this.u2fError = error;
this.toast.showInfo(error.message);
});
}
}
}

View File

@@ -27,9 +27,10 @@
</div>
</app-card>
<app-card *ngIf="user" class="app-card" title="{{ 'USER.PROFILE.TITLE' | translate }}">
<app-detail-form [genders]="genders" [languages]="languages" [username]="user.userName" [user]="user.human"
(changedLanguage)="changedLanguage($event)" (submitData)="saveProfile($event)">
<app-card *ngIf="user && user.human?.profile?.userName" class=" app-card"
title="{{ 'USER.PROFILE.TITLE' | translate }}">
<app-detail-form [genders]="genders" [languages]="languages" [username]="user.human?.profile?.userName"
[user]="user.human" (changedLanguage)="changedLanguage($event)" (submitData)="saveProfile($event)">
</app-detail-form>
</app-card>

View File

@@ -8,8 +8,7 @@ import { Email, Gender, Phone, Profile, User, UserState } from 'src/app/proto/ge
import { GrpcAuthService } from 'src/app/services/grpc-auth.service';
import { ToastService } from 'src/app/services/toast.service';
import { EditDialogType } from '../user-detail/user-detail.component';
import { EditDialogComponent } from './edit-dialog/edit-dialog.component';
import { EditDialogComponent, EditDialogType } from './edit-dialog/edit-dialog.component';
@Component({
selector: 'app-auth-user-detail',
@@ -169,7 +168,7 @@ export class AuthUserDetailComponent implements OnDestroy {
labelKey: 'ACTIONS.NEWVALUE',
titleKey: 'USER.LOGINMETHODS.PHONE.EDITTITLE',
descriptionKey: 'USER.LOGINMETHODS.PHONE.EDITDESC',
value: this.user.human?.phone,
value: this.user.human?.phone?.phone,
},
width: '400px',
});
@@ -188,7 +187,7 @@ export class AuthUserDetailComponent implements OnDestroy {
labelKey: 'ACTIONS.NEWVALUE',
titleKey: 'USER.LOGINMETHODS.EMAIL.EDITTITLE',
descriptionKey: 'USER.LOGINMETHODS.EMAIL.EDITDESC',
value: this.user.human?.email,
value: this.user.human?.email?.email,
},
width: '400px',
});

View File

@@ -39,14 +39,10 @@
</table>
</app-refresh-table>
<div class="add-row">
<button class="button" *ngIf="otpAvailable" (click)="addOTP()" mat-stroked-button color="primary"
<button class="button" *ngIf="otpAvailable" (click)="addAuthFactor()" mat-raised-button color="primary"
matTooltip="{{'ACTIONS.NEW' | translate}}">
<mat-icon class="icon" svgIcon="mdi_radar"></mat-icon>{{'USER.MFA.OTP' | translate}}
</button>
<button class="button" (click)="addU2F()" mat-stroked-button color="primary"
matTooltip="{{'ACTIONS.NEW' | translate}}">
<i class="las la-fingerprint"></i>
{{'USER.MFA.U2F' | translate}}
<mat-icon class="icon">add</mat-icon>
{{'USER.MFA.OTP' | translate}}
</button>
</div>
<div class="table-wrapper">

View File

@@ -8,9 +8,7 @@ import { AuthFactor, AuthFactorState } from 'src/app/proto/generated/zitadel/use
import { GrpcAuthService } from 'src/app/services/grpc-auth.service';
import { ToastService } from 'src/app/services/toast.service';
import { _base64ToArrayBuffer } from '../../u2f-util';
import { DialogOtpComponent } from '../dialog-otp/dialog-otp.component';
import { DialogU2FComponent, U2FComponentDestination } from '../dialog-u2f/dialog-u2f.component';
import { AuthFactorDialogComponent } from '../auth-factor-dialog/auth-factor-dialog.component';
export interface WebAuthNOptions {
challenge: string;
@@ -55,59 +53,17 @@ export class AuthUserMfaComponent implements OnInit, OnDestroy {
this.loadingSubject.complete();
}
public addOTP(): void {
this.service.addMyMultiFactorOTP().then((otpresp) => {
const otp = otpresp;
const dialogRef = this.dialog.open(DialogOtpComponent, {
data: otp.url,
width: '400px',
});
dialogRef.afterClosed().subscribe((code) => {
if (code) {
this.service.verifyMyMultiFactorOTP(code).then(() => {
this.getMFAs();
});
}
});
}, error => {
this.toast.showError(error);
public addAuthFactor(): void {
const dialogRef = this.dialog.open(AuthFactorDialogComponent, {
width: '400px',
});
}
public addU2F(): void {
this.service.addMyMultiFactorU2F().then((u2fresp) => {
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;
});
}
console.log(credOptions);
const dialogRef = this.dialog.open(DialogU2FComponent, {
width: '400px',
data: {
credOptions,
type: U2FComponentDestination.MFA,
},
});
dialogRef.afterClosed().subscribe(done => {
if (done) {
this.getMFAs();
} else {
this.getMFAs();
}
dialogRef.afterClosed().subscribe((code) => {
if (code) {
this.service.verifyMyMultiFactorOTP(code).then(() => {
this.getMFAs();
});
}
}, error => {
this.toast.showError(error);
});
}

View File

@@ -1,18 +0,0 @@
<h1 mat-dialog-title>{{'USER.MFA.OTP_DIALOG_TITLE' | translate}}</h1>
<div mat-dialog-content>
<p>{{'USER.MFA.OTP_DIALOG_DESCRIPTION' | translate}}</p>
<div class="qrcode-wrapper">
<qrcode *ngIf="data" class="qrcode" [qrdata]="data" [width]="150" [errorCorrectionLevel]="'M'"></qrcode>
</div>
<cnsl-form-field class="form-field" label="Access Code" required="true">
<cnsl-label>Code</cnsl-label>
<input cnslInput [(ngModel)]="code" />
</cnsl-form-field>
</div>
<div mat-dialog-actions class="action">
<button mat-button (click)="closeDialog()"><span translate>ACTIONS.CLOSE</span></button>
<button mat-raised-button class="ok-button" color="primary" (click)="closeDialogWithCode()"><span
translate>ACTIONS.OK</span>
</button>
</div>

View File

@@ -1,22 +0,0 @@
.qrcode-wrapper {
display: flex;
align-content: center;
.qrcode {
margin: 1rem auto;
}
}
.form-field {
width: 100%;
}
.action {
display: flex;
justify-content: flex-end;
button {
margin-left: .5rem;
border-radius: .5rem;
}
}

View File

@@ -1,21 +0,0 @@
import { Component, Inject } from '@angular/core';
import { MAT_DIALOG_DATA, MatDialogRef } from '@angular/material/dialog';
@Component({
selector: 'app-dialog-otp',
templateUrl: './dialog-otp.component.html',
styleUrls: ['./dialog-otp.component.scss'],
})
export class DialogOtpComponent {
public code: string = '';
constructor(public dialogRef: MatDialogRef<DialogOtpComponent>,
@Inject(MAT_DIALOG_DATA) public data: string) { }
public closeDialog(): void {
this.dialogRef.close();
}
public closeDialogWithCode(): void {
this.dialogRef.close(this.code);
}
}

View File

@@ -5,17 +5,7 @@ import { take } from 'rxjs/operators';
import { GrpcAuthService } from 'src/app/services/grpc-auth.service';
import { ToastService } from 'src/app/services/toast.service';
export function _arrayBufferToBase64(buffer: any): string {
let binary = '';
const bytes = new Uint8Array(buffer);
const len = bytes.byteLength;
for (let i = 0; i < len; i++) {
binary += String.fromCharCode(bytes[i]);
}
return btoa(binary).replace(/\+/g, '-')
.replace(/\//g, '_')
.replace(/=/g, '');
}
import { _arrayBufferToBase64 } from '../u2f_util';
export enum U2FComponentDestination {
MFA = 'mfa',

View File

@@ -4,8 +4,9 @@
<p class="desc">{{data.descriptionKey | translate}}</p>
<div mat-dialog-content>
<cnsl-form-field class="formfield">
<cnsl-label>{{data.labelKey | translate }}</cnsl-label>
<input cnslInput [(ngModel)]="value" (keydown.enter)="value ? closeDialogWithValue(value) : null" />
<cnsl-label>{{data.labelKey | translate }} <span *ngIf="phoneCountry">({{ phoneCountry }})</span></cnsl-label>
<input cnslInput [(ngModel)]="value" (change)="changeValue($event)"
(keydown.enter)="value ? closeDialogWithValue(value) : null" />
</cnsl-form-field>
</div>
<div mat-dialog-actions class="action">

View File

@@ -1,5 +1,11 @@
import { Component, Inject } from '@angular/core';
import { MAT_DIALOG_DATA, MatDialogRef } from '@angular/material/dialog';
import { parsePhoneNumber } from 'libphonenumber-js';
export enum EditDialogType {
PHONE = 1,
EMAIL = 2,
}
@Component({
selector: 'app-edit-email-dialog',
@@ -8,9 +14,26 @@ import { MAT_DIALOG_DATA, MatDialogRef } from '@angular/material/dialog';
})
export class EditDialogComponent {
public value: string = '';
public isPhone: boolean = false;
public phoneCountry: string = 'CH';
constructor(public dialogRef: MatDialogRef<EditDialogComponent>,
@Inject(MAT_DIALOG_DATA) public data: any) {
this.value = data.value;
if (data.type == EditDialogType.PHONE) {
this.isPhone = true;
}
}
changeValue(change: any) {
const value = change.target.value;
if (this.isPhone && value) {
const phoneNumber = parsePhoneNumber(value ?? '', 'CH');
if (phoneNumber) {
const formmatted = phoneNumber.formatInternational();
this.phoneCountry = phoneNumber.country || '';
this.value = formmatted;
}
}
}
closeDialog(email: string = ''): void {

View File

@@ -0,0 +1,12 @@
export function _arrayBufferToBase64(buffer: any): string {
let binary = '';
const bytes = new Uint8Array(buffer);
const len = bytes.byteLength;
for (let i = 0; i < len; i++) {
binary += String.fromCharCode(bytes[i]);
}
return btoa(binary).replace(/\+/g, '-')
.replace(/\//g, '_')
.replace(/=/g, '');
}

View File

@@ -18,10 +18,10 @@
<div class="method-row">
<div class="left">
<span class="label">{{ 'USER.EMAIL' | translate }}</span>
<span class="name">{{human?.email}}</span>
<span *ngIf="human?.profile.email.isEmailVerified" class="contact-state verified">{{'USER.EMAILVERIFIED' |
<span class="name">{{human?.email?.email}}</span>
<span *ngIf="human?.email?.isEmailVerified" class="contact-state verified">{{'USER.EMAILVERIFIED' |
translate}}</span>
<div *ngIf="!human?.profile.email.isEmailVerified" class="block">
<div *ngIf="!human?.email.isEmailVerified" class="block">
<span class="contact-state notverified">{{'USER.NOTVERIFIED' | translate}}</span>
<ng-container *ngIf="human?.email">
@@ -45,13 +45,13 @@
<div class="method-row">
<div class="left">
<span class="label">{{ 'USER.PHONE' | translate }}</span>
<span class="name">{{human?.phone ? human.phone : ('USER.PHONEEMPTY' | translate)}}</span>
<span *ngIf="human?.profile.phone.isPhoneVerified" class="contact-state verified">{{'USER.PHONEVERIFIED' |
<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?.profile.phone.isPhoneVerified" class="block">
<div *ngIf="human.phone?.phone && !human?.phone.isPhoneVerified" class="block">
<span class="contact-state notverified">{{'USER.NOTVERIFIED' | translate}}</span>
<ng-container *ngIf="human?.phone">
<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>
@@ -64,7 +64,7 @@
</div>
<div class="right">
<button matTooltip="{{'ACTIONS.DELETE' | translate}}" *ngIf="human && human.phone" color="warn"
<button matTooltip="{{'ACTIONS.DELETE' | translate}}" *ngIf="human && human.phone?.phone" color="warn"
(click)="emitDeletePhone()" mat-icon-button>
<i class="las la-trash"></i>
</button>

View File

@@ -4,7 +4,7 @@ import { WarnDialogComponent } from 'src/app/modules/warn-dialog/warn-dialog.com
import { Human, UserState } from 'src/app/proto/generated/zitadel/user_pb';
import { CodeDialogComponent } from '../auth-user-detail/code-dialog/code-dialog.component';
import { EditDialogType } from '../user-detail/user-detail.component';
import { EditDialogType } from '../auth-user-detail/edit-dialog/edit-dialog.component';
@Component({
selector: 'app-contact',

View File

@@ -1,7 +1,7 @@
import { Component, EventEmitter, Input, OnChanges, OnDestroy, Output } from '@angular/core';
import { AbstractControl, FormBuilder, FormGroup, Validators } from '@angular/forms';
import { Subscription } from 'rxjs';
import { Gender, User } from 'src/app/proto/generated/zitadel/user_pb';
import { Gender, Human, User } from 'src/app/proto/generated/zitadel/user_pb';
@Component({
@@ -11,7 +11,7 @@ import { Gender, User } from 'src/app/proto/generated/zitadel/user_pb';
})
export class DetailFormComponent implements OnDestroy, OnChanges {
@Input() public username!: string;
@Input() public user!: User;
@Input() public user!: Human.AsObject;
@Input() public disabled: boolean = false;
@Input() public genders: Gender[] = [];
@Input() public languages: string[] = ['de', 'en'];
@@ -47,7 +47,7 @@ export class DetailFormComponent implements OnDestroy, OnChanges {
preferredLanguage: [{ value: '', disabled: this.disabled }],
});
this.profileForm.patchValue({ userName: this.username, ...this.user });
this.profileForm.patchValue({ userName: this.username, ...this.user.profile });
if (this.preferredLanguage) {
this.sub = this.preferredLanguage.valueChanges.subscribe(value => {

View File

@@ -19,6 +19,7 @@ import { CardModule } from 'src/app/modules/card/card.module';
import { ChangesModule } from 'src/app/modules/changes/changes.module';
import { DetailLayoutModule } from 'src/app/modules/detail-layout/detail-layout.module';
import { InputModule } from 'src/app/modules/input/input.module';
import { MachineKeysModule } from 'src/app/modules/machine-keys/machine-keys.module';
import { MetaLayoutModule } from 'src/app/modules/meta-layout/meta-layout.module';
import { PasswordComplexityViewModule } from 'src/app/modules/password-complexity-view/password-complexity-view.module';
import { RefreshTableModule } from 'src/app/modules/refresh-table/refresh-table.module';
@@ -29,11 +30,11 @@ import { HasRolePipeModule } from 'src/app/pipes/has-role-pipe/has-role-pipe.mod
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 { AuthFactorDialogComponent } from './auth-user-detail/auth-factor-dialog/auth-factor-dialog.component';
import { AuthPasswordlessComponent } from './auth-user-detail/auth-passwordless/auth-passwordless.component';
import { AuthUserDetailComponent } from './auth-user-detail/auth-user-detail.component';
import { AuthUserMfaComponent } from './auth-user-detail/auth-user-mfa/auth-user-mfa.component';
import { CodeDialogComponent } from './auth-user-detail/code-dialog/code-dialog.component';
import { DialogOtpComponent } from './auth-user-detail/dialog-otp/dialog-otp.component';
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';
@@ -48,13 +49,11 @@ 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';
import { MachineKeysModule } from 'src/app/modules/machine-keys/machine-keys.module';
@NgModule({
declarations: [
AuthUserDetailComponent,
UserDetailComponent,
DialogOtpComponent,
EditDialogComponent,
AuthUserMfaComponent,
AuthPasswordlessComponent,
@@ -68,6 +67,7 @@ import { MachineKeysModule } from 'src/app/modules/machine-keys/machine-keys.mod
ContactComponent,
ResendEmailDialogComponent,
DialogU2FComponent,
AuthFactorDialogComponent,
],
imports: [
UserDetailRoutingModule,

View File

@@ -12,14 +12,9 @@ import { Email, Gender, Machine, Phone, Profile, User, UserState } from 'src/app
import { ManagementService } from 'src/app/services/mgmt.service';
import { ToastService } from 'src/app/services/toast.service';
import { EditDialogComponent } from '../auth-user-detail/edit-dialog/edit-dialog.component';
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';
export enum EditDialogType {
PHONE = 1,
EMAIL = 2,
}
@Component({
selector: 'app-user-detail',
templateUrl: './user-detail.component.html',
@@ -87,6 +82,7 @@ export class UserDetailComponent implements OnInit {
}
public saveProfile(profileData: Profile.AsObject): void {
console.log(profileData);
if (this.user.human) {
this.user.human.profile = profileData;
this.mgmtUserService
@@ -252,7 +248,8 @@ export class UserDetailComponent implements OnInit {
labelKey: 'ACTIONS.NEWVALUE',
titleKey: 'USER.LOGINMETHODS.PHONE.EDITTITLE',
descriptionKey: 'USER.LOGINMETHODS.PHONE.EDITDESC',
value: this.user.human?.phone,
value: this.user.human?.phone?.phone,
type: EditDialogType.PHONE,
},
width: '400px',
});
@@ -271,7 +268,8 @@ export class UserDetailComponent implements OnInit {
labelKey: 'ACTIONS.NEWVALUE',
titleKey: 'USER.LOGINMETHODS.EMAIL.EDITTITLE',
descriptionKey: 'USER.LOGINMETHODS.EMAIL.EDITDESC',
value: this.user.human?.email,
value: this.user.human?.email?.email,
type: EditDialogType.EMAIL,
},
width: '400px',
});

View File

@@ -32,9 +32,10 @@
<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)">
<app-avatar
*ngIf="user[type] && user[type].displayName && user[type]?.firstName && user[type]?.lastName; else cog"
class="avatar" [name]="user[type].displayName" [size]="32">
*ngIf="user.human && user.human.profile.displayName && user.human?.profile.firstName && user.human?.profile.lastName; else cog"
class="avatar" [name]="user.human.profile.displayName" [size]="32">
</app-avatar>
<ng-template #cog>
<div class="sa-icon">
@@ -45,28 +46,6 @@
</td>
</ng-container>
<ng-container matColumnDef="firstname">
<th mat-header-cell *matHeaderCellDef
[ngClass]="{'search-active': this.userSearchKey == UserListSearchKey.FIRST_NAME}">
{{ 'USER.PROFILE.FIRSTNAME' | translate }}
<template [ngTemplateOutlet]="templateRef"
[ngTemplateOutletContext]="{key: UserListSearchKey.FIRST_NAME}"></template>
</th>
<td mat-cell *matCellDef="let user" [routerLink]="user.id ? ['/users', user.id ]: null">
{{user[type]?.firstName}} </td>
</ng-container>
<ng-container matColumnDef="lastname">
<th mat-header-cell *matHeaderCellDef
[ngClass]="{'search-active': this.userSearchKey == UserListSearchKey.LAST_NAME}">
{{ 'USER.PROFILE.LASTNAME' | translate }}
<template [ngTemplateOutlet]="templateRef"
[ngTemplateOutletContext]="{key: UserListSearchKey.LAST_NAME}"></template>
</th>
<td mat-cell *matCellDef="let user" [routerLink]="user.id ? ['/users', user.id ]: null">
{{user[type]?.lastName}} </td>
</ng-container>
<ng-container matColumnDef="displayName">
<th mat-header-cell *matHeaderCellDef
[ngClass]="{'search-active': this.userSearchKey == UserListSearchKey.USERSEARCHKEY_DISPLAY_NAME}">
@@ -75,7 +54,8 @@
[ngTemplateOutletContext]="{key: UserListSearchKey.USERSEARCHKEY_DISPLAY_NAME}"></template>
</th>
<td mat-cell *matCellDef="let user" [routerLink]="user.id ? ['/users', user.id ]: null">
{{user[type]?.displayName}} </td>
{{user.human?.profile?.displayName}}
</td>
</ng-container>
<ng-container matColumnDef="name">
@@ -83,13 +63,17 @@
{{ 'USER.MACHINE.NAME' | translate }}
</th>
<td mat-cell *matCellDef="let user" [routerLink]="user.id ? ['/users', user.id ]: null">
{{user[type]?.name}} </td>
<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">
{{user[type]?.description}} </td>
<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">
@@ -111,15 +95,17 @@
[ngTemplateOutletContext]="{key: UserListSearchKey.EMAIL}"></template>
</th>
<td mat-cell *matCellDef="let user" [routerLink]="user.id ? ['/users', user.id ]: null">
{{user[type]?.email}} </td>
<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.USERSTATE_ACTIVE, 'inactive': user.state === UserState.USERSTATE_INACTIVE}">{{
'USER.DATA.STATE'+user.state | translate }}</span>
[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>

View File

@@ -180,6 +180,8 @@ export class UserTableComponent implements OnInit {
}
this.userService.listUsers(limit, offset, [query]).then(resp => {
console.log(resp);
if (resp.details?.totalResult) {
this.totalResult = resp.details?.totalResult;
}