mirror of
https://github.com/zitadel/zitadel.git
synced 2025-12-13 18:32:10 +00:00
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:
@@ -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>
|
||||
@@ -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 {
|
||||
|
||||
@@ -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>
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
});
|
||||
});
|
||||
@@ -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);
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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>
|
||||
|
||||
|
||||
@@ -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',
|
||||
});
|
||||
|
||||
@@ -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">
|
||||
|
||||
@@ -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);
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -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>
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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',
|
||||
|
||||
@@ -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">
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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, '');
|
||||
}
|
||||
@@ -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>
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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 => {
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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',
|
||||
});
|
||||
|
||||
@@ -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>
|
||||
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user