mirror of
https://github.com/andwati/zitadel.git
synced 2026-11-06 22:32:16 +00:00
fix(console): u2f, mfa, loginpolicy, auth and mgmt passwordless, clockskew, userinfo within idtoken (#1108)
* fix 2fa,mfa config, self management * u2f enable when otp * passwordless grpc auth * clockskew, passwordless container, util class * passwordless, i18n * passwordless auth and mgmt * lint ts * chore(deps-dev): bump ts-node from 9.1.0 to 9.1.1 in /console (#1089) Bumps [ts-node](https://github.com/TypeStrong/ts-node) from 9.1.0 to 9.1.1. - [Release notes](https://github.com/TypeStrong/ts-node/releases) - [Commits](https://github.com/TypeStrong/ts-node/compare/v9.1.0...v9.1.1) Signed-off-by: dependabot[bot] <[email protected]> Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> * chore(deps-dev): bump @angular/cli from 11.0.3 to 11.0.4 in /console (#1094) Bumps [@angular/cli](https://github.com/angular/angular-cli) from 11.0.3 to 11.0.4. - [Release notes](https://github.com/angular/angular-cli/releases) - [Commits](https://github.com/angular/angular-cli/compare/v11.0.3...v11.0.4) Signed-off-by: dependabot[bot] <[email protected]> Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> Co-authored-by: Max Peintner <[email protected]> * chore(deps): bump uuid from 8.3.1 to 8.3.2 in /console (#1098) Bumps [uuid](https://github.com/uuidjs/uuid) from 8.3.1 to 8.3.2. - [Release notes](https://github.com/uuidjs/uuid/releases) - [Changelog](https://github.com/uuidjs/uuid/blob/master/CHANGELOG.md) - [Commits](https://github.com/uuidjs/uuid/compare/v8.3.1...v8.3.2) Signed-off-by: dependabot[bot] <[email protected]> Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> Co-authored-by: Max Peintner <[email protected]> * chore(deps-dev): bump @angular/language-service in /console (#1099) Bumps [@angular/language-service](https://github.com/angular/angular/tree/HEAD/packages/language-service) from 11.0.3 to 11.0.4. - [Release notes](https://github.com/angular/angular/releases) - [Changelog](https://github.com/angular/angular/blob/master/CHANGELOG.md) - [Commits](https://github.com/angular/angular/commits/11.0.4/packages/language-service) Signed-off-by: dependabot[bot] <[email protected]> Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> * chore(deps-dev): bump @types/node from 14.14.10 to 14.14.13 in /console (#1100) Bumps [@types/node](https://github.com/DefinitelyTyped/DefinitelyTyped/tree/HEAD/types/node) from 14.14.10 to 14.14.13. - [Release notes](https://github.com/DefinitelyTyped/DefinitelyTyped/releases) - [Commits](https://github.com/DefinitelyTyped/DefinitelyTyped/commits/HEAD/types/node) Signed-off-by: dependabot[bot] <[email protected]> Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> * chore(deps-dev): bump @angular-devkit/build-angular in /console (#1088) Bumps [@angular-devkit/build-angular](https://github.com/angular/angular-cli) from 0.1100.3 to 0.1100.4. - [Release notes](https://github.com/angular/angular-cli/releases) - [Commits](https://github.com/angular/angular-cli/commits) Signed-off-by: dependabot[bot] <[email protected]> Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> Co-authored-by: Max Peintner <[email protected]> * fix: replace regex check for projectid (#1064) * update lock * fix app detail * logs * fix login policy update * fix error message * decode excluded cred id Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> Co-authored-by: Livio Amstutz <[email protected]>
This commit is contained in:
co-authored by
dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Livio Amstutz
parent
71df1bcd0e
commit
6aa0588fe0
+50
@@ -0,0 +1,50 @@
|
||||
<app-card title="{{'USER.PASSWORDLESS.TITLE' | translate}}"
|
||||
description="{{'USER.PASSWORDLESS.DESCRIPTION' | translate}}">
|
||||
<app-refresh-table [loading]="loading$ | async" (refreshed)="getPasswordless()"
|
||||
[dataSize]="dataSource?.data?.length">
|
||||
<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 === MFAState.MFASTATE_READY"
|
||||
class="verified las la-check-circle"></i>
|
||||
</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>
|
||||
|
||||
<tr mat-header-row *matHeaderRowDef="displayedColumns"></tr>
|
||||
<tr class="highlight" mat-row *matRowDef="let row; columns: displayedColumns;"></tr>
|
||||
</table>
|
||||
</app-refresh-table>
|
||||
<div class="add-row">
|
||||
<button class="button" (click)="addPasswordless()" mat-stroked-button color="primary"
|
||||
matTooltip="{{'ACTIONS.NEW' | translate}}">
|
||||
<i class="las la-fingerprint"></i>
|
||||
{{'USER.PASSWORDLESS.U2F' | translate}}
|
||||
</button>
|
||||
</div>
|
||||
<div class="table-wrapper">
|
||||
<div class="spinner-container" *ngIf="loading$ | async">
|
||||
<mat-spinner diameter="50"></mat-spinner>
|
||||
</div>
|
||||
</div>
|
||||
</app-card>
|
||||
+41
@@ -0,0 +1,41 @@
|
||||
|
||||
.add-row {
|
||||
display: flex;
|
||||
margin: -.5rem;
|
||||
flex-wrap: wrap;
|
||||
|
||||
.button {
|
||||
margin: .5rem;
|
||||
margin-top: 1rem;
|
||||
|
||||
.icon {
|
||||
margin-right: .5rem;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.centered {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
|
||||
i {
|
||||
margin-left: 1rem;
|
||||
color: var(--color-main);
|
||||
}
|
||||
}
|
||||
|
||||
.table {
|
||||
width: 100%;
|
||||
|
||||
td,
|
||||
th {
|
||||
&:first-child {
|
||||
padding-left: 0;
|
||||
padding-right: 1rem;
|
||||
}
|
||||
|
||||
&:last-child {
|
||||
padding-right: 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
+25
@@ -0,0 +1,25 @@
|
||||
import { ComponentFixture, TestBed, waitForAsync } from '@angular/core/testing';
|
||||
|
||||
import { AuthPasswordlessComponent } from './auth-passwordless.component';
|
||||
|
||||
describe('AuthPasswordlessComponent', () => {
|
||||
let component: AuthPasswordlessComponent;
|
||||
let fixture: ComponentFixture<AuthPasswordlessComponent>;
|
||||
|
||||
beforeEach(waitForAsync(() => {
|
||||
TestBed.configureTestingModule({
|
||||
declarations: [AuthPasswordlessComponent],
|
||||
})
|
||||
.compileComponents();
|
||||
}));
|
||||
|
||||
beforeEach(() => {
|
||||
fixture = TestBed.createComponent(AuthPasswordlessComponent);
|
||||
component = fixture.componentInstance;
|
||||
fixture.detectChanges();
|
||||
});
|
||||
|
||||
it('should create', () => {
|
||||
expect(component).toBeTruthy();
|
||||
});
|
||||
});
|
||||
+114
@@ -0,0 +1,114 @@
|
||||
import { Component, 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, WebAuthNResponse, WebAuthNToken } from 'src/app/proto/generated/auth_pb';
|
||||
import { GrpcAuthService } from 'src/app/services/grpc-auth.service';
|
||||
import { ToastService } from 'src/app/services/toast.service';
|
||||
|
||||
import { _base64ToArrayBuffer } from '../../u2f-util';
|
||||
import { DialogU2FComponent, U2FComponentDestination } from '../dialog-u2f/dialog-u2f.component';
|
||||
|
||||
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: 'app-auth-passwordless',
|
||||
templateUrl: './auth-passwordless.component.html',
|
||||
styleUrls: ['./auth-passwordless.component.scss'],
|
||||
})
|
||||
export class AuthPasswordlessComponent implements OnInit, OnDestroy {
|
||||
public displayedColumns: string[] = ['name', 'state', 'actions'];
|
||||
private loadingSubject: BehaviorSubject<boolean> = new BehaviorSubject<boolean>(false);
|
||||
public loading$: Observable<boolean> = this.loadingSubject.asObservable();
|
||||
|
||||
@ViewChild(MatTable) public table!: MatTable<WebAuthNToken.AsObject>;
|
||||
@ViewChild(MatSort) public sort!: MatSort;
|
||||
public dataSource!: MatTableDataSource<WebAuthNToken.AsObject>;
|
||||
|
||||
public MFAState: any = MFAState;
|
||||
public error: string = '';
|
||||
|
||||
constructor(private service: GrpcAuthService,
|
||||
private toast: ToastService,
|
||||
private dialog: MatDialog) { }
|
||||
|
||||
public ngOnInit(): void {
|
||||
this.getPasswordless();
|
||||
}
|
||||
|
||||
public ngOnDestroy(): void {
|
||||
this.loadingSubject.complete();
|
||||
}
|
||||
|
||||
public addPasswordless(): void {
|
||||
this.service.AddMyPasswordless().then((u2fresp) => {
|
||||
const webauthn: WebAuthNResponse.AsObject = u2fresp.toObject();
|
||||
const credOptions: CredentialCreationOptions = JSON.parse(atob(webauthn.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);
|
||||
const dialogRef = this.dialog.open(DialogU2FComponent, {
|
||||
width: '400px',
|
||||
data: {
|
||||
credOptions,
|
||||
type: U2FComponentDestination.PASSWORDLESS,
|
||||
},
|
||||
});
|
||||
|
||||
dialogRef.afterClosed().subscribe(done => {
|
||||
if (done) {
|
||||
this.getPasswordless();
|
||||
} else {
|
||||
this.getPasswordless();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
}, error => {
|
||||
this.toast.showError(error);
|
||||
});
|
||||
}
|
||||
|
||||
public getPasswordless(): void {
|
||||
this.service.GetMyPasswordless().then(passwordless => {
|
||||
this.dataSource = new MatTableDataSource(passwordless.toObject().tokensList);
|
||||
this.dataSource.sort = this.sort;
|
||||
}).catch(error => {
|
||||
this.error = error.message;
|
||||
});
|
||||
}
|
||||
|
||||
public deletePasswordless(id?: string): void {
|
||||
const dialogRef = this.dialog.open(WarnDialogComponent, {
|
||||
data: {
|
||||
confirmKey: 'ACTIONS.DELETE',
|
||||
cancelKey: 'ACTIONS.CANCEL',
|
||||
titleKey: 'USER.PASSWORDLESS.DIALOG.DELETE_TITLE',
|
||||
descriptionKey: 'USER.PASSWORDLESS.DIALOG.DELETE_DESCRIPTION',
|
||||
},
|
||||
width: '400px',
|
||||
});
|
||||
|
||||
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);
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
+2
@@ -51,6 +51,8 @@
|
||||
</app-contact>
|
||||
</app-card>
|
||||
|
||||
<app-auth-passwordless *ngIf="user" #mfaComponent></app-auth-passwordless>
|
||||
|
||||
<app-auth-user-mfa *ngIf="user" #mfaComponent></app-auth-user-mfa>
|
||||
|
||||
<app-card *ngIf="user?.id" title="{{ 'GRANTS.USER.TITLE' | translate }}"
|
||||
|
||||
+3
-3
@@ -8,8 +8,8 @@
|
||||
|
||||
<ng-container matColumnDef="attr">
|
||||
<th mat-header-cell *matHeaderCellDef> {{ 'USER.MFA.ATTRIBUTE' | translate }} </th>
|
||||
<td mat-cell *matCellDef="let mfa"><span *ngIf="mfa?.attr" class="centered">
|
||||
{{ mfa.attr }}
|
||||
<td mat-cell *matCellDef="let mfa"><span *ngIf="mfa?.attribute" class="centered">
|
||||
{{ mfa?.attribute }}
|
||||
</span>
|
||||
</td>
|
||||
</ng-container>
|
||||
@@ -43,7 +43,7 @@
|
||||
matTooltip="{{'ACTIONS.NEW' | translate}}">
|
||||
<mat-icon class="icon" svgIcon="mdi_radar"></mat-icon>{{'USER.MFA.OTP' | translate}}
|
||||
</button>
|
||||
<button class="button" *ngIf="otpAvailable" (click)="addU2F()" mat-stroked-button color="primary"
|
||||
<button class="button" (click)="addU2F()" mat-stroked-button color="primary"
|
||||
matTooltip="{{'ACTIONS.NEW' | translate}}">
|
||||
<i class="las la-fingerprint"></i>
|
||||
{{'USER.MFA.U2F' | translate}}
|
||||
|
||||
+5
-14
@@ -8,19 +8,10 @@ import { MfaOtpResponse, MFAState, MfaType, MultiFactor, WebAuthNResponse } from
|
||||
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 } from '../dialog-u2f/dialog-u2f.component';
|
||||
|
||||
export function _base64ToArrayBuffer(base64: string): any {
|
||||
const binaryString = atob(base64);
|
||||
const len = binaryString.length;
|
||||
const bytes = new Uint8Array(len);
|
||||
for (let i = 0; i < len; i++) {
|
||||
bytes[i] = binaryString.charCodeAt(i);
|
||||
}
|
||||
return bytes.buffer;
|
||||
}
|
||||
|
||||
export interface WebAuthNOptions {
|
||||
challenge: string;
|
||||
rp: { name: string, id: string; };
|
||||
@@ -50,6 +41,7 @@ export class AuthUserMfaComponent implements OnInit, OnDestroy {
|
||||
|
||||
public error: string = '';
|
||||
public otpAvailable: boolean = false;
|
||||
|
||||
constructor(private service: GrpcAuthService,
|
||||
private toast: ToastService,
|
||||
private dialog: MatDialog) { }
|
||||
@@ -82,10 +74,6 @@ export class AuthUserMfaComponent implements OnInit, OnDestroy {
|
||||
});
|
||||
}
|
||||
|
||||
public verifyU2f(): void {
|
||||
|
||||
}
|
||||
|
||||
public addU2F(): void {
|
||||
this.service.AddMyMfaU2F().then((u2fresp) => {
|
||||
const webauthn: WebAuthNResponse.AsObject = u2fresp.toObject();
|
||||
@@ -94,6 +82,9 @@ export class AuthUserMfaComponent implements OnInit, OnDestroy {
|
||||
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));
|
||||
}
|
||||
const dialogRef = this.dialog.open(DialogU2FComponent, {
|
||||
width: '400px',
|
||||
data: {
|
||||
|
||||
+32
-11
@@ -17,19 +17,27 @@ export function _arrayBufferToBase64(buffer: any): string {
|
||||
.replace(/=/g, '');
|
||||
}
|
||||
|
||||
export enum U2FComponentDestination {
|
||||
MFA = 'mfa',
|
||||
PASSWORDLESS = 'passwordless',
|
||||
}
|
||||
|
||||
@Component({
|
||||
selector: 'app-dialog-u2f',
|
||||
templateUrl: './dialog-u2f.component.html',
|
||||
styleUrls: ['./dialog-u2f.component.scss'],
|
||||
})
|
||||
export class DialogU2FComponent {
|
||||
private type!: U2FComponentDestination;
|
||||
public name: string = '';
|
||||
public error: string = '';
|
||||
public loading: boolean = false;
|
||||
|
||||
constructor(public dialogRef: MatDialogRef<DialogU2FComponent>,
|
||||
@Inject(MAT_DIALOG_DATA) public data: { credOptions: any; },
|
||||
private service: GrpcAuthService, private translate: TranslateService, private toast: ToastService) { }
|
||||
@Inject(MAT_DIALOG_DATA) public data: { credOptions: any; type: U2FComponentDestination; },
|
||||
private service: GrpcAuthService, private translate: TranslateService, private toast: ToastService) {
|
||||
this.type = data.type;
|
||||
}
|
||||
|
||||
public closeDialog(): void {
|
||||
this.dialogRef.close();
|
||||
@@ -61,16 +69,29 @@ export class DialogU2FComponent {
|
||||
});
|
||||
|
||||
const base64 = btoa(data);
|
||||
this.service.VerifyMyMfaU2F(base64, this.name).then(() => {
|
||||
this.translate.get('USER.MFA.U2F_SUCCESS').pipe(take(1)).subscribe(msg => {
|
||||
this.toast.showInfo(msg);
|
||||
if (this.type === U2FComponentDestination.MFA) {
|
||||
this.service.VerifyMyMfaU2F(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);
|
||||
});
|
||||
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 => {
|
||||
|
||||
@@ -0,0 +1,10 @@
|
||||
|
||||
export function _base64ToArrayBuffer(base64: string): any {
|
||||
const binaryString = atob(base64);
|
||||
const len = binaryString.length;
|
||||
const bytes = new Uint8Array(len);
|
||||
for (let i = 0; i < len; i++) {
|
||||
bytes[i] = binaryString.charCodeAt(i);
|
||||
}
|
||||
return bytes.buffer;
|
||||
}
|
||||
@@ -29,6 +29,7 @@ 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 { 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';
|
||||
@@ -47,6 +48,7 @@ import { ShowKeyDialogModule } from './machine-keys/show-key-dialog/show-key-dia
|
||||
import { MembershipsComponent } from './memberships/memberships.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';
|
||||
|
||||
@@ -57,7 +59,9 @@ import { UserMfaComponent } from './user-detail/user-mfa/user-mfa.component';
|
||||
DialogOtpComponent,
|
||||
EditDialogComponent,
|
||||
AuthUserMfaComponent,
|
||||
AuthPasswordlessComponent,
|
||||
UserMfaComponent,
|
||||
PasswordlessComponent,
|
||||
ThemeSettingComponent,
|
||||
PasswordComponent,
|
||||
CodeDialogComponent,
|
||||
|
||||
+43
@@ -0,0 +1,43 @@
|
||||
<app-card title="{{'USER.PASSWORDLESS.TITLE' | translate}}"
|
||||
description="{{'USER.PASSWORDLESS.DESCRIPTION' | translate}}">
|
||||
<app-refresh-table [loading]="loading$ | async" (refreshed)="getPasswordless()"
|
||||
[dataSize]="dataSource?.data?.length">
|
||||
<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 === MFAState.MFASTATE_READY"
|
||||
class="verified las la-check-circle"></i>
|
||||
</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>
|
||||
|
||||
<tr mat-header-row *matHeaderRowDef="displayedColumns"></tr>
|
||||
<tr class="highlight" mat-row *matRowDef="let row; columns: displayedColumns;"></tr>
|
||||
</table>
|
||||
</app-refresh-table>
|
||||
<div class="table-wrapper">
|
||||
<div class="spinner-container" *ngIf="loading$ | async">
|
||||
<mat-spinner diameter="50"></mat-spinner>
|
||||
</div>
|
||||
</div>
|
||||
</app-card>
|
||||
+26
@@ -0,0 +1,26 @@
|
||||
|
||||
.centered {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
|
||||
i {
|
||||
margin-left: 1rem;
|
||||
color: var(--color-main);
|
||||
}
|
||||
}
|
||||
|
||||
.table {
|
||||
width: 100%;
|
||||
|
||||
td,
|
||||
th {
|
||||
&:first-child {
|
||||
padding-left: 0;
|
||||
padding-right: 1rem;
|
||||
}
|
||||
|
||||
&:last-child {
|
||||
padding-right: 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
+25
@@ -0,0 +1,25 @@
|
||||
import { ComponentFixture, TestBed, waitForAsync } from '@angular/core/testing';
|
||||
|
||||
import { AuthPasswordlessComponent } from './auth-passwordless.component';
|
||||
|
||||
describe('AuthPasswordlessComponent', () => {
|
||||
let component: AuthPasswordlessComponent;
|
||||
let fixture: ComponentFixture<AuthPasswordlessComponent>;
|
||||
|
||||
beforeEach(waitForAsync(() => {
|
||||
TestBed.configureTestingModule({
|
||||
declarations: [AuthPasswordlessComponent],
|
||||
})
|
||||
.compileComponents();
|
||||
}));
|
||||
|
||||
beforeEach(() => {
|
||||
fixture = TestBed.createComponent(AuthPasswordlessComponent);
|
||||
component = fixture.componentInstance;
|
||||
fixture.detectChanges();
|
||||
});
|
||||
|
||||
it('should create', () => {
|
||||
expect(component).toBeTruthy();
|
||||
});
|
||||
});
|
||||
+84
@@ -0,0 +1,84 @@
|
||||
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, WebAuthNToken } from 'src/app/proto/generated/auth_pb';
|
||||
import { 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 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: 'app-passwordless',
|
||||
templateUrl: './passwordless.component.html',
|
||||
styleUrls: ['./passwordless.component.scss'],
|
||||
})
|
||||
export class PasswordlessComponent implements OnInit, OnDestroy {
|
||||
@Input() private user!: UserView.AsObject;
|
||||
public displayedColumns: string[] = ['name', 'state', 'actions'];
|
||||
private loadingSubject: BehaviorSubject<boolean> = new BehaviorSubject<boolean>(false);
|
||||
public loading$: Observable<boolean> = this.loadingSubject.asObservable();
|
||||
|
||||
@ViewChild(MatTable) public table!: MatTable<WebAuthNToken.AsObject>;
|
||||
@ViewChild(MatSort) public sort!: MatSort;
|
||||
public dataSource!: MatTableDataSource<WebAuthNToken.AsObject>;
|
||||
|
||||
public MFAState: any = MFAState;
|
||||
public error: string = '';
|
||||
|
||||
constructor(private service: ManagementService,
|
||||
private toast: ToastService,
|
||||
private dialog: MatDialog) { }
|
||||
|
||||
public ngOnInit(): void {
|
||||
this.getPasswordless();
|
||||
}
|
||||
|
||||
public ngOnDestroy(): void {
|
||||
this.loadingSubject.complete();
|
||||
}
|
||||
|
||||
public getPasswordless(): void {
|
||||
this.service.GetPasswordless(this.user.id).then(passwordless => {
|
||||
console.log(passwordless.toObject().tokensList);
|
||||
this.dataSource = new MatTableDataSource(passwordless.toObject().tokensList);
|
||||
this.dataSource.sort = this.sort;
|
||||
}).catch(error => {
|
||||
this.error = error.message;
|
||||
});
|
||||
}
|
||||
|
||||
public deletePasswordless(id?: string): void {
|
||||
const dialogRef = this.dialog.open(WarnDialogComponent, {
|
||||
data: {
|
||||
confirmKey: 'ACTIONS.DELETE',
|
||||
cancelKey: 'ACTIONS.CANCEL',
|
||||
titleKey: 'USER.PASSWORDLESS.DIALOG.DELETE_TITLE',
|
||||
descriptionKey: 'USER.PASSWORDLESS.DIALOG.DELETE_DESCRIPTION',
|
||||
},
|
||||
width: '400px',
|
||||
});
|
||||
|
||||
dialogRef.afterClosed().subscribe(resp => {
|
||||
if (resp && id) {
|
||||
this.service.RemovePasswordless(id, this.user.id).then(() => {
|
||||
this.toast.showInfo('USER.TOAST.PASSWORDLESSREMOVED', true);
|
||||
this.getPasswordless();
|
||||
}).catch(error => {
|
||||
this.toast.showError(error);
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -84,6 +84,8 @@
|
||||
</app-contact>
|
||||
</app-card>
|
||||
|
||||
<app-passwordless *ngIf="user && user.human" [user]="user"></app-passwordless>
|
||||
|
||||
<app-user-mfa *ngIf="user && user.human" [user]="user"></app-user-mfa>
|
||||
|
||||
<app-card *ngIf="user?.id" title="{{ 'GRANTS.USER.TITLE' | translate }}"
|
||||
|
||||
+2
-2
@@ -8,8 +8,8 @@
|
||||
|
||||
<ng-container matColumnDef="attr">
|
||||
<th mat-header-cell *matHeaderCellDef> {{ 'USER.MFA.ATTRIBUTE' | translate }} </th>
|
||||
<td mat-cell *matCellDef="let mfa"><span *ngIf="mfa?.attr" class="centered">
|
||||
{{ mfa.attr }}
|
||||
<td mat-cell *matCellDef="let mfa"><span *ngIf="mfa?.attribute" class="centered">
|
||||
{{ mfa.attribute }}
|
||||
</span>
|
||||
</td>
|
||||
</ng-container>
|
||||
|
||||
@@ -47,6 +47,7 @@ export class UserMfaComponent implements OnInit, OnDestroy {
|
||||
|
||||
public getMFAs(): void {
|
||||
this.mgmtUserService.getUserMfas(this.user.id).then(mfas => {
|
||||
console.log(mfas.toObject().mfasList);
|
||||
this.dataSource = new MatTableDataSource(mfas.toObject().mfasList);
|
||||
this.dataSource.sort = this.sort;
|
||||
}).catch(error => {
|
||||
@@ -81,7 +82,8 @@ export class UserMfaComponent implements OnInit, OnDestroy {
|
||||
this.toast.showError(error);
|
||||
});
|
||||
} else if (type === MfaType.MFATYPE_U2F && id) {
|
||||
this.mgmtUserService.RemoveMfaU2F(id).then(() => {
|
||||
console.log(this.user.id, id);
|
||||
this.mgmtUserService.RemoveMfaU2F(this.user.id, id).then(() => {
|
||||
this.toast.showInfo('USER.TOAST.U2FREMOVED', true);
|
||||
|
||||
const index = this.dataSource.data.findIndex(mfa => mfa.type === type);
|
||||
|
||||
Reference in New Issue
Block a user