mirror of
https://github.com/zitadel/zitadel.git
synced 2025-12-07 07:16:54 +00:00
# Which Problems Are Solved
- Hides for users MFA options are not allowed by org policy.
- Fix for "ng test" across "console"
# How the Problems Are Solved
- Before displaying MFA options we call "listMyMultiFactors" from parent
component to filter MFA allowed by org
# Additional Changes
- Dependency Injection was fixed around ng unit tests
# Additional Context
admin view
<img width="698" alt="Screenshot 2025-02-06 at 00 26 50"
src="https://github.com/user-attachments/assets/1b642c8a-a640-4bdd-a1ca-bde70c263567"
/>
user view
<img width="751" alt="Screenshot 2025-02-06 at 00 27 16"
src="https://github.com/user-attachments/assets/e1c99907-3226-46ce-b8bc-e993af4b4cae"
/>
test
<img width="1500" alt="Screenshot 2025-02-06 at 00 01 36"
src="https://github.com/user-attachments/assets/d2d8ead1-9f0f-4916-a2fc-f4db9c71cfa8"
/>
The issue: https://github.com/zitadel/zitadel/issues/9176
The bug report:
https://discord.com/channels/927474939156643850/1307006457815896094
---------
Co-authored-by: a k <rdyto1@macbook-pro-1.home>
Co-authored-by: a k <rdyto1@macbook-pro.home>
Co-authored-by: a k <rdyto1@macbook-pro-2.home>
Co-authored-by: Ramon <mail@conblem.me>
(cherry picked from commit 839c761357)
198 lines
6.8 KiB
TypeScript
198 lines
6.8 KiB
TypeScript
import { Component, Input, OnDestroy, OnInit, ViewChild } from '@angular/core';
|
|
import { MatDialog } from '@angular/material/dialog';
|
|
import { MatSort } from '@angular/material/sort';
|
|
import { MatTable, MatTableDataSource } from '@angular/material/table';
|
|
import { BehaviorSubject, Observable } from 'rxjs';
|
|
import { WarnDialogComponent } from 'src/app/modules/warn-dialog/warn-dialog.component';
|
|
import { AuthFactorState } from 'src/app/proto/generated/zitadel/user_pb';
|
|
import { NewAuthService } from 'src/app/services/new-auth.service';
|
|
import { ToastService } from 'src/app/services/toast.service';
|
|
import { AddAuthFactorDialogData, AuthFactorDialogComponent } from '../auth-factor-dialog/auth-factor-dialog.component';
|
|
import { AuthFactor } from '@zitadel/proto/zitadel/user_pb';
|
|
import { SecondFactorType } from '@zitadel/proto/zitadel/policy_pb';
|
|
export interface WebAuthNOptions {
|
|
challenge: string;
|
|
rp: { name: string; id: string };
|
|
user: { name: string; id: string; displayName: string };
|
|
pubKeyCredParams: any;
|
|
authenticatorSelection: { userVerification: string };
|
|
timeout: number;
|
|
attestation: string;
|
|
}
|
|
|
|
@Component({
|
|
selector: 'cnsl-auth-user-mfa',
|
|
templateUrl: './auth-user-mfa.component.html',
|
|
styleUrls: ['./auth-user-mfa.component.scss'],
|
|
})
|
|
export class AuthUserMfaComponent implements OnInit, OnDestroy {
|
|
public displayedColumns: string[] = ['name', 'type', 'state', 'actions'];
|
|
private loadingSubject: BehaviorSubject<boolean> = new BehaviorSubject<boolean>(false);
|
|
public loading$: Observable<boolean> = this.loadingSubject.asObservable();
|
|
|
|
@ViewChild(MatTable) public table!: MatTable<AuthFactor>;
|
|
@ViewChild(MatSort) public sort!: MatSort;
|
|
@Input() public phoneVerified: boolean = false;
|
|
public AuthFactorState: any = AuthFactorState;
|
|
public dataSource: MatTableDataSource<AuthFactor> = new MatTableDataSource<AuthFactor>([]);
|
|
|
|
protected error: string = '';
|
|
|
|
protected otpAvailable$ = new BehaviorSubject<boolean>(false);
|
|
protected u2fAvailable$ = new BehaviorSubject<boolean>(false);
|
|
protected otpSmsAvailable$ = new BehaviorSubject<boolean>(false);
|
|
protected otpEmailAvailable$ = new BehaviorSubject<boolean>(false);
|
|
protected otpDisabled$ = new BehaviorSubject<boolean>(true);
|
|
protected otpSmsDisabled$ = new BehaviorSubject<boolean>(true);
|
|
protected otpEmailDisabled$ = new BehaviorSubject<boolean>(true);
|
|
|
|
constructor(
|
|
private readonly service: NewAuthService,
|
|
private readonly toast: ToastService,
|
|
private readonly dialog: MatDialog,
|
|
) {}
|
|
|
|
public ngOnInit(): void {
|
|
this.getMFAs();
|
|
this.applyOrgPolicy();
|
|
}
|
|
|
|
public ngOnDestroy(): void {
|
|
this.loadingSubject.complete();
|
|
}
|
|
|
|
public addAuthFactor(): void {
|
|
const data: AddAuthFactorDialogData = {
|
|
otp$: this.otpAvailable$,
|
|
u2f$: this.u2fAvailable$,
|
|
otpSms$: this.otpSmsAvailable$,
|
|
otpEmail$: this.otpEmailAvailable$,
|
|
otpDisabled$: this.otpDisabled$,
|
|
otpSmsDisabled$: this.otpSmsDisabled$,
|
|
otpEmailDisabled$: this.otpEmailDisabled$,
|
|
phoneVerified: this.phoneVerified,
|
|
} as const;
|
|
|
|
const dialogRef = this.dialog.open(AuthFactorDialogComponent, {
|
|
data: data,
|
|
});
|
|
|
|
dialogRef.afterClosed().subscribe(() => {
|
|
this.getMFAs();
|
|
});
|
|
}
|
|
|
|
public getMFAs(): void {
|
|
this.service
|
|
.listMyMultiFactors()
|
|
.then((mfas) => {
|
|
const list: AuthFactor[] = mfas.result;
|
|
this.dataSource = new MatTableDataSource(list);
|
|
this.dataSource.sort = this.sort;
|
|
|
|
this.disableAuthFactor(list, 'otp', this.otpDisabled$);
|
|
this.disableAuthFactor(list, 'otpSms', this.otpSmsDisabled$);
|
|
this.disableAuthFactor(list, 'otpEmail', this.otpEmailDisabled$);
|
|
})
|
|
.catch((error) => {
|
|
this.error = error.message;
|
|
});
|
|
}
|
|
|
|
public applyOrgPolicy(): void {
|
|
this.service.getMyLoginPolicy().then((resp) => {
|
|
if (resp && resp.policy) {
|
|
const secondFactors = resp.policy?.secondFactors;
|
|
this.displayAuthFactorBasedOnPolicy(secondFactors, SecondFactorType.OTP, this.otpAvailable$);
|
|
this.displayAuthFactorBasedOnPolicy(secondFactors, SecondFactorType.U2F, this.u2fAvailable$);
|
|
this.displayAuthFactorBasedOnPolicy(secondFactors, SecondFactorType.OTP_EMAIL, this.otpEmailAvailable$);
|
|
this.displayAuthFactorBasedOnPolicy(secondFactors, SecondFactorType.OTP_SMS, this.otpSmsAvailable$);
|
|
}
|
|
});
|
|
}
|
|
|
|
public deleteMFA(factor: AuthFactor): void {
|
|
const dialogRef = this.dialog.open(WarnDialogComponent, {
|
|
data: {
|
|
confirmKey: 'ACTIONS.DELETE',
|
|
cancelKey: 'ACTIONS.CANCEL',
|
|
titleKey: 'USER.MFA.DIALOG.MFA_DELETE_TITLE',
|
|
descriptionKey: 'USER.MFA.DIALOG.MFA_DELETE_DESCRIPTION',
|
|
},
|
|
width: '400px',
|
|
});
|
|
|
|
dialogRef.afterClosed().subscribe((resp) => {
|
|
if (resp) {
|
|
if (factor.type.case === 'otp') {
|
|
this.service
|
|
.removeMyMultiFactorOTP()
|
|
.then(() => {
|
|
this.toast.showInfo('USER.TOAST.OTPREMOVED', true);
|
|
|
|
this.cleanupList();
|
|
this.getMFAs();
|
|
})
|
|
.catch((error) => {
|
|
this.toast.showError(error);
|
|
});
|
|
} else if (factor.type.case === 'u2f') {
|
|
this.service
|
|
.removeMyMultiFactorU2F(factor.type.value.id)
|
|
.then(() => {
|
|
this.toast.showInfo('USER.TOAST.U2FREMOVED', true);
|
|
|
|
this.cleanupList();
|
|
this.getMFAs();
|
|
})
|
|
.catch((error) => {
|
|
this.toast.showError(error);
|
|
});
|
|
} else if (factor.type.case === 'otpEmail') {
|
|
this.service
|
|
.removeMyAuthFactorOTPEmail()
|
|
.then(() => {
|
|
this.toast.showInfo('USER.TOAST.OTPREMOVED', true);
|
|
|
|
this.cleanupList();
|
|
this.getMFAs();
|
|
})
|
|
.catch((error) => {
|
|
this.toast.showError(error);
|
|
});
|
|
} else if (factor.type.case === 'otpSms') {
|
|
this.service
|
|
.removeMyAuthFactorOTPSMS()
|
|
.then(() => {
|
|
this.toast.showInfo('USER.TOAST.OTPREMOVED', true);
|
|
|
|
this.cleanupList();
|
|
this.getMFAs();
|
|
})
|
|
.catch((error) => {
|
|
this.toast.showError(error);
|
|
});
|
|
}
|
|
}
|
|
});
|
|
}
|
|
|
|
private cleanupList(): void {
|
|
this.dataSource.data = this.dataSource.data.filter((mfa: AuthFactor) => {
|
|
return mfa.type.case;
|
|
});
|
|
}
|
|
|
|
private disableAuthFactor(mfas: AuthFactor[], key: string, subject: BehaviorSubject<boolean>): void {
|
|
subject.next(mfas.some((mfa) => mfa.type.case === key));
|
|
}
|
|
|
|
private displayAuthFactorBasedOnPolicy(
|
|
factors: SecondFactorType[],
|
|
factor: SecondFactorType,
|
|
subject: BehaviorSubject<boolean>,
|
|
): void {
|
|
subject.next(factors.some((f) => f === factor));
|
|
}
|
|
}
|