diff --git a/console/src/app/pages/users/user-detail/user-detail/user-mfa/user-mfa.component.html b/console/src/app/pages/users/user-detail/user-detail/user-mfa/user-mfa.component.html
index 61c2b662c5..eb234e45d8 100644
--- a/console/src/app/pages/users/user-detail/user-detail/user-mfa/user-mfa.component.html
+++ b/console/src/app/pages/users/user-detail/user-detail/user-mfa/user-mfa.component.html
@@ -17,6 +17,16 @@
+
+ {{ 'USER.MFA.TABLEACTIONS' | translate }} |
+
+
+ |
+
+
diff --git a/console/src/app/pages/users/user-detail/user-detail/user-mfa/user-mfa.component.ts b/console/src/app/pages/users/user-detail/user-detail/user-mfa/user-mfa.component.ts
index 00b2e10f76..e91c170d77 100644
--- a/console/src/app/pages/users/user-detail/user-detail/user-mfa/user-mfa.component.ts
+++ b/console/src/app/pages/users/user-detail/user-detail/user-mfa/user-mfa.component.ts
@@ -1,9 +1,12 @@
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 { MFAState, MfaType, UserMultiFactor, UserView } from 'src/app/proto/generated/management_pb';
import { ManagementService } from 'src/app/services/mgmt.service';
+import { ToastService } from 'src/app/services/toast.service';
export interface MFAItem {
@@ -17,7 +20,7 @@ export interface MFAItem {
styleUrls: ['./user-mfa.component.scss'],
})
export class UserMfaComponent implements OnInit, OnDestroy {
- public displayedColumns: string[] = ['type', 'state'];
+ public displayedColumns: string[] = ['type', 'state', 'actions'];
@Input() private user!: UserView.AsObject;
public mfaSubject: BehaviorSubject = new BehaviorSubject([]);
private loadingSubject: BehaviorSubject = new BehaviorSubject(false);
@@ -31,7 +34,7 @@ export class UserMfaComponent implements OnInit, OnDestroy {
public MFAState: any = MFAState;
public error: string = '';
- constructor(private mgmtUserService: ManagementService) { }
+ constructor(private mgmtUserService: ManagementService, private dialog: MatDialog, private toast: ToastService) { }
public ngOnInit(): void {
this.getOTP();
@@ -50,4 +53,34 @@ export class UserMfaComponent implements OnInit, OnDestroy {
this.error = error.message;
});
}
+
+ public deleteMFA(type: MfaType): void {
+ const dialogRef = this.dialog.open(WarnDialogComponent, {
+ data: {
+ confirmKey: 'ACTIONS.DELETE',
+ cancelKey: 'ACTIONS.CANCEL',
+ titleKey: 'USER.MFA.DIALOG.OTP_DELETE_TITLE',
+ descriptionKey: 'USER.MFA.DIALOG.OTP_DELETE_DESCRIPTION',
+ },
+ width: '400px',
+ });
+
+ dialogRef.afterClosed().subscribe(resp => {
+ if (resp) {
+ if (type === MfaType.MFATYPE_OTP) {
+ this.mgmtUserService.removeMfaOTP(this.user.id).then(() => {
+ this.toast.showInfo('USER.TOAST.OTPREMOVED', true);
+
+ const index = this.dataSource.data.findIndex(mfa => mfa.type === type);
+ if (index > -1) {
+ this.dataSource.data.splice(index, 1);
+ }
+ this.getOTP();
+ }).catch(error => {
+ this.toast.showError(error);
+ });
+ }
+ }
+ });
+ }
}
diff --git a/console/src/app/services/mgmt.service.ts b/console/src/app/services/mgmt.service.ts
index e3e61180b4..f003e5c456 100644
--- a/console/src/app/services/mgmt.service.ts
+++ b/console/src/app/services/mgmt.service.ts
@@ -677,6 +677,12 @@ export class ManagementService {
return this.grpcService.mgmt.getUserMfas(req);
}
+ public removeMfaOTP(id: string): Promise {
+ const req = new UserID();
+ req.setId(id);
+ return this.grpcService.mgmt.removeMfaOTP(req);
+ }
+
public SaveUserProfile(
id: string,
firstName?: string,
diff --git a/internal/api/grpc/management/user.go b/internal/api/grpc/management/user.go
index f40f86317c..bf7f0abbd8 100644
--- a/internal/api/grpc/management/user.go
+++ b/internal/api/grpc/management/user.go
@@ -221,6 +221,11 @@ func (s *Server) GetUserMfas(ctx context.Context, userID *management.UserID) (*m
return &management.UserMultiFactors{Mfas: mfasFromModel(mfas)}, nil
}
+func (s *Server) RemoveMfaOTP(ctx context.Context, userID *management.UserID) (*empty.Empty, error) {
+ err := s.user.RemoveOTP(ctx, userID.Id)
+ return &empty.Empty{}, err
+}
+
func (s *Server) SearchUserMemberships(ctx context.Context, in *management.UserMembershipSearchRequest) (*management.UserMembershipSearchResponse, error) {
request := userMembershipSearchRequestsToModel(in)
request.AppendUserIDQuery(in.UserId)
diff --git a/internal/management/repository/eventsourcing/eventstore/user.go b/internal/management/repository/eventsourcing/eventstore/user.go
index c8579062ff..3ab8118411 100644
--- a/internal/management/repository/eventsourcing/eventstore/user.go
+++ b/internal/management/repository/eventsourcing/eventstore/user.go
@@ -223,6 +223,10 @@ func (repo *UserRepo) UserMfas(ctx context.Context, userID string) ([]*usr_model
return []*usr_model.MultiFactor{{Type: usr_model.MfaTypeOTP, State: user.OTPState}}, nil
}
+func (repo *UserRepo) RemoveOTP(ctx context.Context, userID string) error {
+ return repo.UserEvents.RemoveOTP(ctx, userID)
+}
+
func (repo *UserRepo) SetOneTimePassword(ctx context.Context, password *usr_model.Password) (*usr_model.Password, error) {
policy, err := repo.View.PasswordComplexityPolicyByAggregateID(authz.GetCtxData(ctx).OrgID)
if err != nil && caos_errs.IsNotFound(err) {
diff --git a/internal/management/repository/user.go b/internal/management/repository/user.go
index 58f4bdeda1..ae035368e7 100644
--- a/internal/management/repository/user.go
+++ b/internal/management/repository/user.go
@@ -31,6 +31,7 @@ type UserRepository interface {
ChangeProfile(ctx context.Context, profile *model.Profile) (*model.Profile, error)
UserMfas(ctx context.Context, userID string) ([]*model.MultiFactor, error)
+ RemoveOTP(ctx context.Context, userID string) error
SearchExternalIDPs(ctx context.Context, request *model.ExternalIDPSearchRequest) (*model.ExternalIDPSearchResponse, error)
RemoveExternalIDP(ctx context.Context, externalIDP *model.ExternalIDP) error
diff --git a/pkg/grpc/management/proto/management.proto b/pkg/grpc/management/proto/management.proto
index ceeece9f15..52f76c0611 100644
--- a/pkg/grpc/management/proto/management.proto
+++ b/pkg/grpc/management/proto/management.proto
@@ -388,6 +388,16 @@ rpc GetUserByID(UserID) returns (UserView) {
};
}
+ rpc RemoveMfaOTP(UserID) returns (google.protobuf.Empty) {
+ option (google.api.http) = {
+ delete: "/users/{id}/mfas/otp"
+ };
+
+ option (caos.zitadel.utils.v1.auth_option) = {
+ permission: "user.write"
+ };
+ }
+
// Sends an Notification (Email/SMS) with a password reset Link
rpc SendSetPasswordNotification(SetPasswordNotificationRequest) returns (google.protobuf.Empty) {
option (google.api.http) = {