fix: console v2 (#1454)

* some issues

* passwordless, mfa

* mfa, project fixes, login policy

* user table, auth service, interceptor
This commit is contained in:
Max Peintner
2021-03-23 09:50:58 +01:00
committed by GitHub
parent d5f0c2375a
commit 7768759906
40 changed files with 261 additions and 157 deletions

View File

@@ -1,26 +1,38 @@
<h1 mat-dialog-title>
<span class="title">{{'USER.CODEDIALOG.TITLE' | translate}} {{data?.number}}</span>
<span class="title">{{'USER.MFA.DIALOG.ADD_MFA_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>
<ng-container *ngIf="selectedType == undefined">
<p class="desc">{{'USER.MFA.DIALOG.ADD_MFA_DESCRIPTION' | translate}}</p>
<div class="type-selection">
<button mat-raised-button [disabled]="data.otpDisabled" (click)="selectType(AuthFactorType.OTP)">
<div class="otp-btn">
<mat-icon class="icon" svgIcon="mdi_radar"></mat-icon>
<span>{{'USER.MFA.OTP' | translate}}</span>
</div>
</button>
<button mat-raised-button (click)="selectType(AuthFactorType.U2F)">
<div class="u2f-btn">
<div class="icon-row">
<i matTooltip="Fingerprint" class="las la-fingerprint"></i>
<i matTooltip="Security Key" class="lab la-usb"></i>
<mat-icon matTooltip="NFC">nfc</mat-icon>
</div>
<span>{{'USER.MFA.U2F' | translate}}</span>
</div>
</button>
</div>
</ng-container>
<div class="otp" *ngIf="selectedType == AuthFactorType.OTP">
<p>{{'USER.MFA.OTP_DIALOG_DESCRIPTION' | translate}}</p>
<p class="desc">{{'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-form-field class="formfield" label="Access Code" required="true">
<cnsl-label>Code</cnsl-label>
<input cnslInput [(ngModel)]="otpcode" />
</cnsl-form-field>
@@ -44,7 +56,8 @@
{{'ACTIONS.CLOSE' | translate}}
</button>
<button cdkFocusInitial color="primary" mat-raised-button class="ok-button" (click)="submitAuth()">
<button *ngIf="selectedType !== undefined" cdkFocusInitial color="primary" mat-raised-button class="ok-button"
(click)="submitAuth()">
{{'ACTIONS.CREATE' | translate}}
</button>
</div>

View File

@@ -2,21 +2,44 @@
display: flex;
margin: 0 -0.5rem;
.otp,
.u2f {
.otp-btn,
.u2f-btn {
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: 1rem;
box-sizing: border-box;
.icon-row {
display: flex;
}
}
button {
margin: .5rem;
}
}
.desc {
font-size: 14px;
color: var(--grey);
}
.otp {
max-width: 400px;
display: flex;
flex-direction: column;
align-items: center;
}
.u2f {
max-width: 400px;
}
.formfield {
width: 100%;
}

View File

@@ -42,6 +42,8 @@ export class AuthFactorDialogComponent {
}
public selectType(type: AuthFactorType): void {
this.selectedType = type;
if (type == AuthFactorType.OTP) {
this.authService.addMyMultiFactorOTP().then((otpresp) => {
this.otpurl = otpresp.url;
@@ -95,6 +97,7 @@ export class AuthFactorDialogComponent {
(resp as any).response.clientDataJSON &&
(resp as any).rawId) {
console.log(resp);
const attestationObject = (resp as any).response.attestationObject;
const clientDataJSON = (resp as any).response.clientDataJSON;
const rawId = (resp as any).rawId;

View File

@@ -36,9 +36,9 @@
</table>
</app-refresh-table>
<div class="add-row">
<button class="button" (click)="addPasswordless()" mat-stroked-button color="primary"
<button class="button" (click)="addPasswordless()" mat-raised-button color="primary"
matTooltip="{{'ACTIONS.NEW' | translate}}">
<i class="las la-fingerprint"></i>
<i class="icon las la-fingerprint"></i>
{{'USER.PASSWORDLESS.U2F' | translate}}
</button>
</div>

View File

@@ -3,10 +3,13 @@
display: flex;
margin: -.5rem;
flex-wrap: wrap;
justify-content: flex-end;
.button {
margin: .5rem;
margin-top: 1rem;
display: flex;
align-items: center;
.icon {
margin-right: .5rem;

View File

@@ -53,6 +53,7 @@ export class AuthPasswordlessComponent implements OnInit, OnDestroy {
public addPasswordless(): void {
this.service.addMyPasswordless().then((resp) => {
if (resp.key) {
console.log(resp.key);
const credOptions: CredentialCreationOptions = JSON.parse(atob(resp.key.publicKey as string));
if (credOptions.publicKey?.challenge) {

View File

@@ -27,10 +27,9 @@
</div>
</app-card>
<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-card *ngIf="user && user.human?.profile" 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-detail-form>
</app-card>

View File

@@ -69,6 +69,7 @@ export class AuthUserDetailComponent implements OnDestroy {
this.user.human.profile?.firstName,
this.user.human.profile?.lastName,
this.user.human.profile?.nickName,
this.user.human.profile?.displayName,
this.user.human.profile?.preferredLanguage,
this.user.human.profile?.gender,
)
@@ -169,6 +170,7 @@ export class AuthUserDetailComponent implements OnDestroy {
titleKey: 'USER.LOGINMETHODS.PHONE.EDITTITLE',
descriptionKey: 'USER.LOGINMETHODS.PHONE.EDITDESC',
value: this.user.human?.phone?.phone,
type: type,
},
width: '400px',
});
@@ -180,6 +182,7 @@ export class AuthUserDetailComponent implements OnDestroy {
});
break;
case EditDialogType.EMAIL:
console.log('email');
const dialogRefEmail = this.dialog.open(EditDialogComponent, {
data: {
confirmKey: 'ACTIONS.SAVE',
@@ -188,6 +191,7 @@ export class AuthUserDetailComponent implements OnDestroy {
titleKey: 'USER.LOGINMETHODS.EMAIL.EDITTITLE',
descriptionKey: 'USER.LOGINMETHODS.EMAIL.EDITDESC',
value: this.user.human?.email?.email,
type: type
},
width: '400px',
});

View File

@@ -3,7 +3,10 @@
<table class="table" mat-table [dataSource]="dataSource">
<ng-container matColumnDef="type">
<th mat-header-cell *matHeaderCellDef> {{ 'USER.MFA.TABLETYPE' | translate }} </th>
<td mat-cell *matCellDef="let mfa"> {{'USER.MFA.TYPE.'+ mfa.type | translate}} </td>
<td mat-cell *matCellDef="let mfa">
<span *ngIf="mfa.otp !== undefined">OTP (One-Time Password)</span>
<span *ngIf="mfa.u2f !== undefined">U2F (Universal 2nd Factor)</span>
</td>
</ng-container>
<ng-container matColumnDef="attr">
@@ -39,10 +42,10 @@
</table>
</app-refresh-table>
<div class="add-row">
<button class="button" *ngIf="otpAvailable" (click)="addAuthFactor()" mat-raised-button color="primary"
<button class="button" (click)="addAuthFactor()" mat-raised-button color="primary"
matTooltip="{{'ACTIONS.NEW' | translate}}">
<mat-icon class="icon">add</mat-icon>
{{'USER.MFA.OTP' | translate}}
{{'USER.MFA.ADD' | translate}}
</button>
</div>
<div class="table-wrapper">

View File

@@ -3,6 +3,7 @@
display: flex;
margin: -.5rem;
flex-wrap: wrap;
justify-content: flex-end;
.button {
margin: .5rem;

View File

@@ -55,7 +55,9 @@ export class AuthUserMfaComponent implements OnInit, OnDestroy {
public addAuthFactor(): void {
const dialogRef = this.dialog.open(AuthFactorDialogComponent, {
width: '400px',
data: {
otpDisabled: !this.otpAvailable
}
});
dialogRef.afterClosed().subscribe((code) => {
@@ -70,6 +72,7 @@ export class AuthUserMfaComponent implements OnInit, OnDestroy {
public getMFAs(): void {
this.service.listMyMultiFactors().then(mfas => {
const list = mfas.resultList;
console.log(list);
this.dataSource = new MatTableDataSource(list);
this.dataSource.sort = this.sort;

View File

@@ -4,9 +4,10 @@
<p class="desc">{{data.descriptionKey | translate}}</p>
<div mat-dialog-content>
<cnsl-form-field class="formfield">
<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-label>{{data.labelKey | translate }} <span *ngIf="isPhone && phoneCountry">({{ phoneCountry }})</span>
</cnsl-label>
<input [formControl]="valueControl" cnslInput
(keydown.enter)="valueControl.valid ? closeDialogWithValue() : null" />
</cnsl-form-field>
</div>
<div mat-dialog-actions class="action">
@@ -14,8 +15,8 @@
{{data.cancelKey | translate}}
</button>
<button [disabled]="!value" cdkFocusInitial color="primary" mat-raised-button class="ok-button"
(click)="closeDialogWithValue(value)">
<button [disabled]="valueControl.invalid" cdkFocusInitial color="primary" mat-raised-button class="ok-button"
(click)="closeDialogWithValue()">
{{data.confirmKey | translate}}
</button>
</div>

View File

@@ -1,4 +1,5 @@
import { Component, Inject } from '@angular/core';
import { FormControl, Validators } from '@angular/forms';
import { MAT_DIALOG_DATA, MatDialogRef } from '@angular/material/dialog';
import { parsePhoneNumber } from 'libphonenumber-js';
@@ -13,34 +14,46 @@ export enum EditDialogType {
styleUrls: ['./edit-dialog.component.scss'],
})
export class EditDialogComponent {
public value: string = '';
public isPhone: boolean = false;
public phoneCountry: string = 'CH';
public valueControl: FormControl = new FormControl(['', [Validators.required]]);
constructor(public dialogRef: MatDialogRef<EditDialogComponent>,
@Inject(MAT_DIALOG_DATA) public data: any) {
this.value = data.value;
this.valueControl.setValue(data.value);
if (data.type == EditDialogType.PHONE) {
this.isPhone = true;
}
this.valueControl.valueChanges.subscribe(value => {
console.log(value);
if (value && value.length > 1) {
this.changeValue(value);
}
});
}
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;
changeValue(changedValue: string) {
if (this.isPhone && changedValue) {
try {
const phoneNumber = parsePhoneNumber(changedValue ?? '', 'CH');
if (phoneNumber) {
const formmatted = phoneNumber.formatInternational();
this.phoneCountry = phoneNumber.country || '';
if (formmatted !== this.valueControl.value) {
this.valueControl.setValue(formmatted);
}
}
} catch (error) {
console.error(error);
}
}
}
closeDialog(email: string = ''): void {
this.dialogRef.close(email);
closeDialog(): void {
this.dialogRef.close();
}
closeDialogWithValue(value: string = ''): void {
this.dialogRef.close(value);
closeDialogWithValue(): void {
this.dialogRef.close(this.valueControl.value);
}
}

View File

@@ -16,6 +16,10 @@
<cnsl-label>{{ 'USER.PROFILE.NICKNAME' | translate }}</cnsl-label>
<input cnslInput formControlName="nickName" />
</cnsl-form-field>
<cnsl-form-field class="formfield">
<cnsl-label>{{ 'USER.PROFILE.DISPLAYNAME' | translate }}</cnsl-label>
<input cnslInput formControlName="displayName" />
</cnsl-form-field>
<cnsl-form-field class="formfield">
<cnsl-label>{{ 'USER.PROFILE.GENDER' | translate }}</cnsl-label>
<mat-select formControlName="gender">
@@ -34,7 +38,7 @@
</cnsl-form-field>
</div>
<div class="btn-container">
<button [disabled]="disabled" class="submit-button" type="submit" color="primary"
mat-raised-button>{{ 'ACTIONS.SAVE' | translate }}</button>
<button [disabled]="disabled" class="submit-button" type="submit" color="primary" mat-raised-button>{{
'ACTIONS.SAVE' | translate }}</button>
</div>
</form>

View File

@@ -30,6 +30,7 @@ export class DetailFormComponent implements OnDestroy, OnChanges {
firstName: [{ value: '', disabled: this.disabled }, Validators.required],
lastName: [{ value: '', disabled: this.disabled }, Validators.required],
nickName: [{ value: '', disabled: this.disabled }],
displayName: [{ value: '', disabled: this.disabled }, Validators.required],
gender: [{ value: 0, disabled: this.disabled }],
preferredLanguage: [{ value: '', disabled: this.disabled }],
});
@@ -43,6 +44,7 @@ export class DetailFormComponent implements OnDestroy, OnChanges {
firstName: [{ value: '', disabled: this.disabled }, Validators.required],
lastName: [{ value: '', disabled: this.disabled }, Validators.required],
nickName: [{ value: '', disabled: this.disabled }],
displayName: [{ value: '', disabled: this.disabled }, Validators.required],
gender: [{ value: 0, disabled: this.disabled }],
preferredLanguage: [{ value: '', disabled: this.disabled }],
});
@@ -77,6 +79,9 @@ export class DetailFormComponent implements OnDestroy, OnChanges {
public get nickName(): AbstractControl | null {
return this.profileForm.get('nickName');
}
public get displayName(): AbstractControl | null {
return this.profileForm.get('displayName');
}
public get gender(): AbstractControl | null {
return this.profileForm.get('gender');
}

View File

@@ -38,7 +38,7 @@
class="avatar" [name]="user.human.profile.displayName" [size]="32">
</app-avatar>
<ng-template #cog>
<div class="sa-icon">
<div class="sa-icon" *ngIf="user.machine">
<i class="las la-user-cog"></i>
</div>
</ng-template>