mirror of
https://github.com/zitadel/zitadel.git
synced 2024-12-12 11:04:25 +00:00
fix: console v2 (#1454)
* some issues * passwordless, mfa * mfa, project fixes, login policy * user table, auth service, interceptor
This commit is contained in:
parent
d5f0c2375a
commit
7768759906
@ -97,9 +97,12 @@ export class MemberCreateDialogComponent {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public selectProject(project: Project.AsObject | GrantedProject.AsObject | any): void {
|
public selectProject(project: Project.AsObject | GrantedProject.AsObject | any): void {
|
||||||
this.projectId = project.projectId;
|
if (project.projectId && project.grantId) {
|
||||||
if (project.id) {
|
this.projectId = project.projectId;
|
||||||
this.grantId = project.id;
|
this.grantId = project.grantId;
|
||||||
|
}
|
||||||
|
else if (project.id) {
|
||||||
|
this.projectId = project.id;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -2,14 +2,15 @@
|
|||||||
<div mat-dialog-content>
|
<div mat-dialog-content>
|
||||||
<p class="desc">{{data.desc | translate}}</p>
|
<p class="desc">{{data.desc | translate}}</p>
|
||||||
|
|
||||||
<!-- <cnsl-form-field class="form-field" label="Access Code" required="true">
|
<cnsl-form-field class="form-field" label="Access Code" required="true">
|
||||||
<cnsl-label>{{'MFA.TYPE' | translate}}</cnsl-label>
|
<cnsl-label>{{'MFA.TYPE' | translate}}</cnsl-label>
|
||||||
<mat-select [(ngModel)]="newMfaType">
|
<mat-select [(ngModel)]="newMfaType">
|
||||||
<mat-option *ngFor="let mfa of []" [value]="mfa">
|
<mat-option *ngFor="let mfa of availableMfaTypes" [value]="mfa">
|
||||||
{{(data.componentType == LoginMethodComponentType.SecondFactor ? 'MFA.SECONDFACTORTYPES.': LoginMethodComponentType.MultiFactor ? 'MFA.MULTIFACTORTYPES.': '')+mfa | translate}}
|
{{(data.componentType == LoginMethodComponentType.SecondFactor ? 'MFA.SECONDFACTORTYPES.':
|
||||||
|
LoginMethodComponentType.MultiFactor ? 'MFA.MULTIFACTORTYPES.': '')+mfa | translate}}
|
||||||
</mat-option>
|
</mat-option>
|
||||||
</mat-select>
|
</mat-select>
|
||||||
</cnsl-form-field> -->
|
</cnsl-form-field>
|
||||||
</div>
|
</div>
|
||||||
<div mat-dialog-actions class="action">
|
<div mat-dialog-actions class="action">
|
||||||
<button mat-button (click)="closeDialog()"><span>{{'ACTIONS.CLOSE' | translate}}</span></button>
|
<button mat-button (click)="closeDialog()"><span>{{'ACTIONS.CLOSE' | translate}}</span></button>
|
||||||
|
@ -1,5 +1,6 @@
|
|||||||
import { Component, Inject } from '@angular/core';
|
import { Component, Inject } from '@angular/core';
|
||||||
import { MAT_DIALOG_DATA, MatDialogRef } from '@angular/material/dialog';
|
import { MAT_DIALOG_DATA, MatDialogRef } from '@angular/material/dialog';
|
||||||
|
import { MultiFactorType, SecondFactorType } from 'src/app/proto/generated/zitadel/policy_pb';
|
||||||
|
|
||||||
enum LoginMethodComponentType {
|
enum LoginMethodComponentType {
|
||||||
MultiFactor = 1,
|
MultiFactor = 1,
|
||||||
@ -13,10 +14,11 @@ enum LoginMethodComponentType {
|
|||||||
})
|
})
|
||||||
export class DialogAddTypeComponent {
|
export class DialogAddTypeComponent {
|
||||||
public LoginMethodComponentType: any = LoginMethodComponentType;
|
public LoginMethodComponentType: any = LoginMethodComponentType;
|
||||||
// public availableMfaTypes: Array<AdminMultiFactorType | MgmtMultiFactorType> = [];
|
public availableMfaTypes: Array<MultiFactorType | SecondFactorType> = [];
|
||||||
|
public newMfaType!: MultiFactorType | SecondFactorType;
|
||||||
constructor(public dialogRef: MatDialogRef<DialogAddTypeComponent>,
|
constructor(public dialogRef: MatDialogRef<DialogAddTypeComponent>,
|
||||||
@Inject(MAT_DIALOG_DATA) public data: any) {
|
@Inject(MAT_DIALOG_DATA) public data: any) {
|
||||||
// this.availableMfaTypes = data.types;
|
this.availableMfaTypes = data.types;
|
||||||
}
|
}
|
||||||
|
|
||||||
public closeDialog(): void {
|
public closeDialog(): void {
|
||||||
@ -24,6 +26,6 @@ export class DialogAddTypeComponent {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public closeDialogWithCode(): void {
|
public closeDialogWithCode(): void {
|
||||||
// this.dialogRef.close(this.newMfaType);
|
this.dialogRef.close(this.newMfaType);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -4,10 +4,14 @@ import { MatPaginator } from '@angular/material/paginator';
|
|||||||
import { TranslateService } from '@ngx-translate/core';
|
import { TranslateService } from '@ngx-translate/core';
|
||||||
import { BehaviorSubject, Observable } from 'rxjs';
|
import { BehaviorSubject, Observable } from 'rxjs';
|
||||||
import {
|
import {
|
||||||
|
AddMultiFactorToLoginPolicyRequest as AdminAddMultiFactorToLoginPolicyRequest,
|
||||||
|
AddSecondFactorToLoginPolicyRequest as AdminAddSecondFactorToLoginPolicyRequest,
|
||||||
RemoveMultiFactorFromLoginPolicyRequest as AdminRemoveMultiFactorFromLoginPolicyRequest,
|
RemoveMultiFactorFromLoginPolicyRequest as AdminRemoveMultiFactorFromLoginPolicyRequest,
|
||||||
RemoveSecondFactorFromLoginPolicyRequest as AdminRemoveSecondFactorFromLoginPolicyRequest,
|
RemoveSecondFactorFromLoginPolicyRequest as AdminRemoveSecondFactorFromLoginPolicyRequest,
|
||||||
} from 'src/app/proto/generated/zitadel/admin_pb';
|
} from 'src/app/proto/generated/zitadel/admin_pb';
|
||||||
import {
|
import {
|
||||||
|
AddMultiFactorToLoginPolicyRequest as MgmtAddMultiFactorToLoginPolicyRequest,
|
||||||
|
AddSecondFactorToLoginPolicyRequest as MgmtAddSecondFactorToLoginPolicyRequest,
|
||||||
RemoveMultiFactorFromLoginPolicyRequest as MgmtRemoveMultiFactorFromLoginPolicyRequest,
|
RemoveMultiFactorFromLoginPolicyRequest as MgmtRemoveMultiFactorFromLoginPolicyRequest,
|
||||||
RemoveSecondFactorFromLoginPolicyRequest as MgmtRemoveSecondFactorFromLoginPolicyRequest,
|
RemoveSecondFactorFromLoginPolicyRequest as MgmtRemoveSecondFactorFromLoginPolicyRequest,
|
||||||
} from 'src/app/proto/generated/zitadel/management_pb';
|
} from 'src/app/proto/generated/zitadel/management_pb';
|
||||||
@ -18,6 +22,7 @@ import { ToastService } from 'src/app/services/toast.service';
|
|||||||
|
|
||||||
import { PolicyComponentServiceType } from '../policies/policy-component-types.enum';
|
import { PolicyComponentServiceType } from '../policies/policy-component-types.enum';
|
||||||
import { WarnDialogComponent } from '../warn-dialog/warn-dialog.component';
|
import { WarnDialogComponent } from '../warn-dialog/warn-dialog.component';
|
||||||
|
import { DialogAddTypeComponent } from './dialog-add-type/dialog-add-type.component';
|
||||||
|
|
||||||
export enum LoginMethodComponentType {
|
export enum LoginMethodComponentType {
|
||||||
MultiFactor = 1,
|
MultiFactor = 1,
|
||||||
@ -116,57 +121,57 @@ export class MfaTableComponent implements OnInit {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
// const dialogRef = this.dialog.open(DialogAddTypeComponent, {
|
const dialogRef = this.dialog.open(DialogAddTypeComponent, {
|
||||||
// data: {
|
data: {
|
||||||
// title: 'MFA.CREATE.TITLE',
|
title: 'MFA.CREATE.TITLE',
|
||||||
// desc: 'MFA.CREATE.DESCRIPTION',
|
desc: 'MFA.CREATE.DESCRIPTION',
|
||||||
// componentType: this.componentType,
|
componentType: this.componentType,
|
||||||
// types: selection,
|
types: selection,
|
||||||
// },
|
},
|
||||||
// width: '400px',
|
width: '400px',
|
||||||
// });
|
});
|
||||||
|
|
||||||
// dialogRef.afterClosed().subscribe((mfaType: ) => {
|
dialogRef.afterClosed().subscribe((mfaType: MultiFactorType | SecondFactorType) => {
|
||||||
// if (mfaType) {
|
if (mfaType) {
|
||||||
// if (this.serviceType === PolicyComponentServiceType.MGMT) {
|
if (this.serviceType === PolicyComponentServiceType.MGMT) {
|
||||||
// if (this.componentType === LoginMethodComponentType.MultiFactor) {
|
if (this.componentType === LoginMethodComponentType.MultiFactor) {
|
||||||
// const req = new MgmtAddMultiFactorToLoginPolicyRequest();
|
const req = new MgmtAddMultiFactorToLoginPolicyRequest();
|
||||||
// req.setType(mfaType as MultiFactorType);
|
req.setType(mfaType as MultiFactorType);
|
||||||
// (this.service as ManagementService).addMultiFactorToLoginPolicy(req).then(() => {
|
(this.service as ManagementService).addMultiFactorToLoginPolicy(req).then(() => {
|
||||||
// this.refreshPageAfterTimout(2000);
|
this.refreshPageAfterTimout(2000);
|
||||||
// }).catch(error => {
|
}).catch(error => {
|
||||||
// this.toast.showError(error);
|
this.toast.showError(error);
|
||||||
// });
|
});
|
||||||
// } else if (this.componentType === LoginMethodComponentType.SecondFactor) {
|
} else if (this.componentType === LoginMethodComponentType.SecondFactor) {
|
||||||
// const req = new MgmtAddSecondFactorToLoginPolicyRequest();
|
const req = new MgmtAddSecondFactorToLoginPolicyRequest();
|
||||||
// req.setType(mfaType as SecondFactorType);
|
req.setType(mfaType as SecondFactorType);
|
||||||
// (this.service as ManagementService).addSecondFactorToLoginPolicy(req).then(() => {
|
(this.service as ManagementService).addSecondFactorToLoginPolicy(req).then(() => {
|
||||||
// this.refreshPageAfterTimout(2000);
|
this.refreshPageAfterTimout(2000);
|
||||||
// }).catch(error => {
|
}).catch(error => {
|
||||||
// this.toast.showError(error);
|
this.toast.showError(error);
|
||||||
// });
|
});
|
||||||
// }
|
}
|
||||||
// } else if (this.serviceType === PolicyComponentServiceType.ADMIN) {
|
} else if (this.serviceType === PolicyComponentServiceType.ADMIN) {
|
||||||
// if (this.componentType === LoginMethodComponentType.MultiFactor) {
|
if (this.componentType === LoginMethodComponentType.MultiFactor) {
|
||||||
// const req = new AdminAddMultiFactorToLoginPolicyRequest();
|
const req = new AdminAddMultiFactorToLoginPolicyRequest();
|
||||||
// req.setType(mfaType as MultiFactorType);
|
req.setType(mfaType as MultiFactorType);
|
||||||
// (this.service as AdminService).addMultiFactorToLoginPolicy(req).then(() => {
|
(this.service as AdminService).addMultiFactorToLoginPolicy(req).then(() => {
|
||||||
// this.refreshPageAfterTimout(2000);
|
this.refreshPageAfterTimout(2000);
|
||||||
// }).catch(error => {
|
}).catch(error => {
|
||||||
// this.toast.showError(error);
|
this.toast.showError(error);
|
||||||
// });
|
});
|
||||||
// } else if (this.componentType === LoginMethodComponentType.SecondFactor) {
|
} else if (this.componentType === LoginMethodComponentType.SecondFactor) {
|
||||||
// const req = new AdminAddSecondFactorToLoginPolicyRequest();
|
const req = new AdminAddSecondFactorToLoginPolicyRequest();
|
||||||
// req.setType(mfaType as SecondFactorType);
|
req.setType(mfaType as SecondFactorType);
|
||||||
// (this.service as AdminService).addSecondFactorToLoginPolicy(req).then(() => {
|
(this.service as AdminService).addSecondFactorToLoginPolicy(req).then(() => {
|
||||||
// this.refreshPageAfterTimout(2000);
|
this.refreshPageAfterTimout(2000);
|
||||||
// }).catch(error => {
|
}).catch(error => {
|
||||||
// this.toast.showError(error);
|
this.toast.showError(error);
|
||||||
// });
|
});
|
||||||
// }
|
}
|
||||||
// }
|
}
|
||||||
// }
|
}
|
||||||
// });
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
private async getData(): Promise<void> {
|
private async getData(): Promise<void> {
|
||||||
|
@ -8,7 +8,7 @@
|
|||||||
<mat-chip-list *ngIf="!singleOutput" #chipList aria-label="loginname selection">
|
<mat-chip-list *ngIf="!singleOutput" #chipList aria-label="loginname selection">
|
||||||
<mat-chip class="chip" *ngFor="let selecteduser of users" [selectable]="selectable" [removable]="removable"
|
<mat-chip class="chip" *ngFor="let selecteduser of users" [selectable]="selectable" [removable]="removable"
|
||||||
(removed)="remove(selecteduser)">
|
(removed)="remove(selecteduser)">
|
||||||
{{ selecteduser?.human ? (selecteduser.human.firstName + ' ' + selecteduser.human.lastName) :
|
{{ selecteduser?.human?.profile ? (selecteduser.human.profile.displayName) :
|
||||||
selecteduser?.machine?.name}}
|
selecteduser?.machine?.name}}
|
||||||
| <small>
|
| <small>
|
||||||
{{selecteduser.preferredLoginName}}</small>
|
{{selecteduser.preferredLoginName}}</small>
|
||||||
|
@ -62,6 +62,7 @@ export class SearchUserAutocompleteComponent implements OnInit, AfterContentChec
|
|||||||
} else if (this.target === UserTarget.SELF) {
|
} else if (this.target === UserTarget.SELF) {
|
||||||
this.getFilteredResults(); // new subscription
|
this.getFilteredResults(); // new subscription
|
||||||
}
|
}
|
||||||
|
console.log(this.users);
|
||||||
}
|
}
|
||||||
|
|
||||||
public ngAfterContentChecked(): void {
|
public ngAfterContentChecked(): void {
|
||||||
|
@ -128,6 +128,7 @@ export class UserGrantsDataSource extends DataSource<UserGrant.AsObject> {
|
|||||||
catchError(() => of([])),
|
catchError(() => of([])),
|
||||||
finalize(() => this.loadingSubject.next(false)),
|
finalize(() => this.loadingSubject.next(false)),
|
||||||
).subscribe(grants => {
|
).subscribe(grants => {
|
||||||
|
console.log(grants);
|
||||||
this.grantsSubject.next(grants);
|
this.grantsSubject.next(grants);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
@ -67,13 +67,13 @@
|
|||||||
<ng-container matColumnDef="dates">
|
<ng-container matColumnDef="dates">
|
||||||
<th mat-header-cell *matHeaderCellDef> DATES </th>
|
<th mat-header-cell *matHeaderCellDef> DATES </th>
|
||||||
<td mat-cell *matCellDef="let grant">
|
<td mat-cell *matCellDef="let grant">
|
||||||
<div class="date-block">
|
<div class="date-block" *ngIf="grant.details?.creationDate">
|
||||||
<span class="date-sub">{{ 'PROJECT.GRANT.CREATIONDATE' | translate }}:</span>
|
<span class="date-sub">{{ 'PROJECT.GRANT.CREATIONDATE' | translate }}:</span>
|
||||||
<span>{{grant.creationDate | timestampToDate | localizedDate: 'dd. MMM, HH:mm' }}</span>
|
<span>{{grant.details.creationDate | timestampToDate | localizedDate: 'dd. MMM, HH:mm' }}</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="date-block">
|
<div class="date-block" *ngIf="grant.details?.changeDate">
|
||||||
<span class="date-sub">{{ 'PROJECT.GRANT.CHANGEDATE' | translate }}</span>
|
<span class="date-sub">{{ 'PROJECT.GRANT.CHANGEDATE' | translate }}</span>
|
||||||
<span>{{grant.changeDate | timestampToDate | localizedDate: 'dd. MMM, HH:mm' }}</span>
|
<span>{{grant.details.changeDate | timestampToDate | localizedDate: 'dd. MMM, HH:mm' }}</span>
|
||||||
</div>
|
</div>
|
||||||
</td>
|
</td>
|
||||||
</ng-container>
|
</ng-container>
|
||||||
@ -86,14 +86,14 @@
|
|||||||
</th>
|
</th>
|
||||||
<td mat-cell *matCellDef="let grant; let i = index" class="role-data">
|
<td mat-cell *matCellDef="let grant; let i = index" class="role-data">
|
||||||
<ng-container
|
<ng-container
|
||||||
*ngIf="(context === UserGrantContext.USER || context === UserGrantContext.NONE) && (grant.grantId && grantToEdit !== grant.id) || (grantToEdit !== grant.id)">
|
*ngIf="(context === UserGrantContext.USER || context === UserGrantContext.NONE) && (grant.grantId && grantToEdit !== grant.grantId) || (grantToEdit !== grant.grantId)">
|
||||||
<div class="flex-row">
|
<div class="flex-row">
|
||||||
<div class="role">
|
<div class="role">
|
||||||
<span *ngFor="let role of grant.roleKeysList">{{ role }}</span>
|
<span *ngFor="let role of grant.roleKeysList">{{ role }}</span>
|
||||||
</div>
|
</div>
|
||||||
<span class="fill-space"></span>
|
<span class="fill-space"></span>
|
||||||
<button mat-stroked-button
|
<button mat-stroked-button
|
||||||
*ngIf="grant.grantId ? grantToEdit !== grant.id : grantToEdit !== grant.id"
|
*ngIf="grant.grantId ? grantToEdit !== grant.grantId : grantToEdit !== grant.grantId"
|
||||||
[disabled]="disableWrite || !((['user.grant.write$'] | hasRole | async) || ((context === UserGrantContext.OWNED_PROJECT ? ['user.grant.write:' + grant?.projectId] : context === UserGrantContext.GRANTED_PROJECT ? ['user.grant.write:' + grant?.grantId] : []) | hasRole | async))"
|
[disabled]="disableWrite || !((['user.grant.write$'] | hasRole | async) || ((context === UserGrantContext.OWNED_PROJECT ? ['user.grant.write:' + grant?.projectId] : context === UserGrantContext.GRANTED_PROJECT ? ['user.grant.write:' + grant?.grantId] : []) | hasRole | async))"
|
||||||
(click)="loadGrantOptions(grant)" matTooltip="{{'ACTIONS.CHANGE' | translate}}">
|
(click)="loadGrantOptions(grant)" matTooltip="{{'ACTIONS.CHANGE' | translate}}">
|
||||||
<i class="las la-edit"></i>
|
<i class="las la-edit"></i>
|
||||||
@ -104,7 +104,7 @@
|
|||||||
|
|
||||||
<div class="row-form">
|
<div class="row-form">
|
||||||
<ng-container
|
<ng-container
|
||||||
*ngIf="(context === UserGrantContext.OWNED_PROJECT || context === UserGrantContext.USER || context === UserGrantContext.NONE) && grantToEdit == grant.id && loadedProjectId && loadedProjectId === grant.projectId">
|
*ngIf="(context === UserGrantContext.OWNED_PROJECT || context === UserGrantContext.USER || context === UserGrantContext.NONE) && grantToEdit == grant.grantId && loadedProjectId && loadedProjectId === grant.projectId">
|
||||||
<cnsl-form-field class="form-field" appearance="outline">
|
<cnsl-form-field class="form-field" appearance="outline">
|
||||||
<!-- <cnsl-label>{{ 'PROJECT.GRANT.ROLENAMESLIST' | translate }}</cnsl-label> -->
|
<!-- <cnsl-label>{{ 'PROJECT.GRANT.ROLENAMESLIST' | translate }}</cnsl-label> -->
|
||||||
<mat-select [(ngModel)]="grant.roleKeysList" multiple
|
<mat-select [(ngModel)]="grant.roleKeysList" multiple
|
||||||
@ -123,7 +123,7 @@
|
|||||||
</ng-container>
|
</ng-container>
|
||||||
|
|
||||||
<ng-container
|
<ng-container
|
||||||
*ngIf="(context === UserGrantContext.GRANTED_PROJECT || context === UserGrantContext.USER || context === UserGrantContext.NONE) && loadedGrantId && loadedGrantId === grant.grantId && grantToEdit == grant.id">
|
*ngIf="(context === UserGrantContext.GRANTED_PROJECT || context === UserGrantContext.USER || context === UserGrantContext.NONE) && loadedGrantId && loadedGrantId === grant.grantId && grantToEdit == grant.grantId">
|
||||||
<cnsl-form-field class="form-field" appearance="outline">
|
<cnsl-form-field class="form-field" appearance="outline">
|
||||||
<!-- <cnsl-label>{{ 'PROJECT.GRANT.ROLENAMESLIST' | translate }}</cnsl-label> -->
|
<!-- <cnsl-label>{{ 'PROJECT.GRANT.ROLENAMESLIST' | translate }}</cnsl-label> -->
|
||||||
<mat-select [(ngModel)]="grant.roleKeysList" multiple
|
<mat-select [(ngModel)]="grant.roleKeysList" multiple
|
||||||
|
@ -182,12 +182,14 @@ export class UserGrantsComponent implements OnInit, AfterViewInit {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private getGrantRoleOptions(grantId: string, projectId: string): void {
|
private getGrantRoleOptions(grantId: string, projectId: string): void {
|
||||||
|
console.log(projectId, grantId);
|
||||||
this.mgmtService.getGrantedProjectByID(projectId, grantId).then(resp => {
|
this.mgmtService.getGrantedProjectByID(projectId, grantId).then(resp => {
|
||||||
if (resp.grantedProject) {
|
if (resp.grantedProject) {
|
||||||
this.loadedGrantId = grantId;
|
this.loadedGrantId = grantId;
|
||||||
this.grantRoleOptions = resp.grantedProject?.grantedRoleKeysList;
|
this.grantRoleOptions = resp.grantedProject?.grantedRoleKeysList;
|
||||||
}
|
}
|
||||||
}).catch(error => {
|
}).catch(error => {
|
||||||
|
this.grantToEdit = '';
|
||||||
this.toast.showError(error);
|
this.toast.showError(error);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
@ -112,8 +112,9 @@ export class OrgCreateComponent {
|
|||||||
|
|
||||||
this.adminService
|
this.adminService
|
||||||
.SetUpOrg(createOrgRequest, humanRequest)
|
.SetUpOrg(createOrgRequest, humanRequest)
|
||||||
.then(() => {
|
.then((resp) => {
|
||||||
this.router.navigate(['/org/overview']);
|
this.router.navigate(['/org/overview']);
|
||||||
|
|
||||||
// const orgResp = org.getOrg();
|
// const orgResp = org.getOrg();
|
||||||
// if (orgResp) {
|
// if (orgResp) {
|
||||||
// this.authService.setActiveOrg(orgResp.toObject());
|
// this.authService.setActiveOrg(orgResp.toObject());
|
||||||
@ -194,6 +195,8 @@ export class OrgCreateComponent {
|
|||||||
this.orgForm = this.fb.group({
|
this.orgForm = this.fb.group({
|
||||||
name: ['', [Validators.required]],
|
name: ['', [Validators.required]],
|
||||||
});
|
});
|
||||||
|
|
||||||
|
console.log(this.orgForm);
|
||||||
} else {
|
} else {
|
||||||
this.createSteps = 2;
|
this.createSteps = 2;
|
||||||
|
|
||||||
|
@ -70,6 +70,7 @@ export class OrgDetailComponent implements OnInit {
|
|||||||
public loadDomains(): void {
|
public loadDomains(): void {
|
||||||
this.mgmtService.listOrgDomains().then(result => {
|
this.mgmtService.listOrgDomains().then(result => {
|
||||||
this.domains = result.resultList;
|
this.domains = result.resultList;
|
||||||
|
console.log(this.domains);
|
||||||
this.primaryDomain = this.domains.find(domain => domain.isPrimary)?.domainName ?? '';
|
this.primaryDomain = this.domains.find(domain => domain.isPrimary)?.domainName ?? '';
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
@ -266,6 +266,6 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<app-changes *ngIf="app" [changeType]="ChangeType.APP" [id]="projectId" [secId]="app.id"></app-changes>
|
<app-changes *ngIf="app" [changeType]="ChangeType.APP" [id]="app.id" [secId]="projectId"></app-changes>
|
||||||
</div>
|
</div>
|
||||||
</app-meta-layout>
|
</app-meta-layout>
|
@ -9,17 +9,18 @@
|
|||||||
<p class="n-items" *ngIf="!loading && selection.selected.length > 0">{{'PROJECT.PAGES.PINNED' | translate}}</p>
|
<p class="n-items" *ngIf="!loading && selection.selected.length > 0">{{'PROJECT.PAGES.PINNED' | translate}}</p>
|
||||||
|
|
||||||
<div class="item card" *ngFor="let item of selection.selected; index as i"
|
<div class="item card" *ngFor="let item of selection.selected; index as i"
|
||||||
[ngClass]="{ inactive: item.state !== ProjectState.PROJECTSTATE_ACTIVE}"
|
[ngClass]="{ inactive: item.state !== ProjectState.PROJECT_STATE_ACTIVE}"
|
||||||
(click)="navigateToProject(item.projectId,item.id, $event)">
|
(click)="navigateToProject(item.projectId,item.grantId, $event)">
|
||||||
<div class="text-part">
|
<div class="text-part">
|
||||||
<span *ngIf="item.changeDate" class="top">{{'PROJECT.PAGES.LASTMODIFIED' | translate}}
|
<span *ngIf="item.details.changeDate" class="top">{{'PROJECT.PAGES.LASTMODIFIED' | translate}}
|
||||||
{{
|
{{
|
||||||
item.changeDate | timestampToDate | localizedDate: 'EEE dd. MMM, HH:mm'
|
item.details.changeDate | timestampToDate | localizedDate: 'EEE dd. MMM, HH:mm'
|
||||||
}}</span>
|
}}</span>
|
||||||
<span class="name" *ngIf="item.projectName">{{ item.projectName }}</span>
|
<span class="name" *ngIf="item.projectName">{{ item.projectName }}</span>
|
||||||
<span class="description" *ngIf="item.resourceOwnerName">{{item.resourceOwnerName}}</span>
|
<span class="description" *ngIf="item.resourceOwnerName">{{item.resourceOwnerName}}</span>
|
||||||
<span *ngIf="item.changeDate" class="created">{{'PROJECT.PAGES.CREATEDON' | translate}}
|
<span *ngIf="item.details.creationDate" class="created">{{'PROJECT.PAGES.CREATEDON' | translate}}
|
||||||
{{ item.creationDate | timestampToDate | localizedDate: 'EEE dd. MMM, HH:mm' }}</span>
|
{{ item.details.creationDate | timestampToDate | localizedDate: 'EEE dd. MMM, HH:mm'
|
||||||
|
}}</span>
|
||||||
<span class="fill-space"></span>
|
<span class="fill-space"></span>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@ -33,17 +34,17 @@
|
|||||||
<p class="n-items" *ngIf="!loading && notPinned.length > 0">{{'PROJECT.PAGES.ALL' | translate}}</p>
|
<p class="n-items" *ngIf="!loading && notPinned.length > 0">{{'PROJECT.PAGES.ALL' | translate}}</p>
|
||||||
|
|
||||||
<div class="item card" *ngFor="let item of notPinned; index as i"
|
<div class="item card" *ngFor="let item of notPinned; index as i"
|
||||||
(click)="navigateToProject(item.projectId,item.id, $event)"
|
(click)="navigateToProject(item.projectId,item.grantId, $event)"
|
||||||
[ngClass]="{ inactive: item.state !== ProjectState.PROJECTSTATE_ACTIVE}">
|
[ngClass]="{ inactive: item.state !== ProjectState.PROJECT_STATE_ACTIVE}">
|
||||||
<div class="text-part">
|
<div class="text-part">
|
||||||
<span *ngIf="item.changeDate" class="top">{{'PROJECT.PAGES.LASTMODIFIED' | translate}}
|
<span *ngIf="item.details.changeDate" class="top">{{'PROJECT.PAGES.LASTMODIFIED' | translate}}
|
||||||
{{
|
{{
|
||||||
item.changeDate | timestampToDate | localizedDate: 'EEE dd. MMM, HH:mm'
|
item.details.changeDate | timestampToDate | localizedDate: 'EEE dd. MMM, HH:mm'
|
||||||
}}</span>
|
}}</span>
|
||||||
<span class="name" *ngIf="item.projectName">{{ item.projectName }}</span>
|
<span class="name" *ngIf="item.projectName">{{ item.projectName }}</span>
|
||||||
<span class="description" *ngIf="item.resourceOwnerName">{{item.resourceOwnerName}}</span>
|
<span class="description" *ngIf="item.resourceOwnerName">{{item.resourceOwnerName}}</span>
|
||||||
<span *ngIf="item.changeDate" class="created">{{'PROJECT.PAGES.CREATEDON' | translate}}
|
<span *ngIf="item.details.creationDate" class="created">{{'PROJECT.PAGES.CREATEDON' | translate}}
|
||||||
{{ item.creationDate | timestampToDate | localizedDate: 'EEE dd. MMM, HH:mm' }}</span>
|
{{ item.details.creationDate | timestampToDate | localizedDate: 'EEE dd. MMM, HH:mm' }}</span>
|
||||||
<span class="fill-space"></span>
|
<span class="fill-space"></span>
|
||||||
</div>
|
</div>
|
||||||
<button [ngClass]="{ selected: selection.isSelected(item)}"
|
<button [ngClass]="{ selected: selection.isSelected(item)}"
|
||||||
|
@ -37,7 +37,7 @@
|
|||||||
position: relative;
|
position: relative;
|
||||||
z-index: 100;
|
z-index: 100;
|
||||||
margin: 1rem;
|
margin: 1rem;
|
||||||
flex-basis: 250px;
|
flex-basis: 260px;
|
||||||
display: flex;
|
display: flex;
|
||||||
text-decoration: none;
|
text-decoration: none;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
@ -65,7 +65,7 @@
|
|||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
min-height: 70px;
|
min-height: 70px;
|
||||||
padding: .5rem 0;
|
padding: .5rem 1rem 0 0;
|
||||||
|
|
||||||
.top {
|
.top {
|
||||||
font-size: .8rem;
|
font-size: .8rem;
|
||||||
|
@ -91,6 +91,7 @@ export class GrantedProjectListComponent implements OnInit, OnDestroy {
|
|||||||
this.loadingSubject.next(true);
|
this.loadingSubject.next(true);
|
||||||
this.mgmtService.listGrantedProjects(limit, offset).then(resp => {
|
this.mgmtService.listGrantedProjects(limit, offset).then(resp => {
|
||||||
this.grantedProjectList = resp.resultList;
|
this.grantedProjectList = resp.resultList;
|
||||||
|
console.log(this.grantedProjectList);
|
||||||
if (resp.details?.totalResult) {
|
if (resp.details?.totalResult) {
|
||||||
this.totalResult = resp.details.totalResult;
|
this.totalResult = resp.details.totalResult;
|
||||||
}
|
}
|
||||||
@ -101,6 +102,7 @@ export class GrantedProjectListComponent implements OnInit, OnDestroy {
|
|||||||
this.grid = false;
|
this.grid = false;
|
||||||
}
|
}
|
||||||
this.dataSource.data = this.grantedProjectList;
|
this.dataSource.data = this.grantedProjectList;
|
||||||
|
|
||||||
this.loadingSubject.next(false);
|
this.loadingSubject.next(false);
|
||||||
}).catch(error => {
|
}).catch(error => {
|
||||||
console.error(error);
|
console.error(error);
|
||||||
|
@ -13,15 +13,15 @@
|
|||||||
(click)="navigateToProject(item.id, $event)"
|
(click)="navigateToProject(item.id, $event)"
|
||||||
[ngClass]="{ inactive: item.state !== ProjectState.PROJECT_STATE_ACTIVE}">
|
[ngClass]="{ inactive: item.state !== ProjectState.PROJECT_STATE_ACTIVE}">
|
||||||
<div class="text-part">
|
<div class="text-part">
|
||||||
<span *ngIf="item.changeDate" class="top">{{'PROJECT.PAGES.LASTMODIFIED' | translate}}
|
<span *ngIf="item.details.changeDate" class="top">{{'PROJECT.PAGES.LASTMODIFIED' | translate}}
|
||||||
{{
|
{{
|
||||||
item.changeDate | timestampToDate | localizedDate: 'EEE dd. MMM, HH:mm'
|
item.details.changeDate | timestampToDate | localizedDate: 'EEE dd. MMM, HH:mm'
|
||||||
}}</span>
|
}}</span>
|
||||||
<span class="name" *ngIf="item.name">{{ item.name }}</span>
|
<span class="name" *ngIf="item.name">{{ item.name }}</span>
|
||||||
|
|
||||||
<span *ngIf="item.changeDate" class="created">{{'PROJECT.PAGES.CREATEDON' | translate}}
|
<span *ngIf="item.details.changeDate" class="created">{{'PROJECT.PAGES.CREATEDON' | translate}}
|
||||||
{{
|
{{
|
||||||
item.creationDate | timestampToDate | localizedDate: 'EEE dd. MMM, HH:mm'
|
item.details.creationDate | timestampToDate | localizedDate: 'EEE dd. MMM, HH:mm'
|
||||||
}}</span>
|
}}</span>
|
||||||
<span class="fill-space"></span>
|
<span class="fill-space"></span>
|
||||||
</div>
|
</div>
|
||||||
|
@ -37,7 +37,7 @@
|
|||||||
position: relative;
|
position: relative;
|
||||||
z-index: 100;
|
z-index: 100;
|
||||||
margin: 1rem;
|
margin: 1rem;
|
||||||
flex-basis: 250px;
|
flex-basis: 260px;
|
||||||
display: flex;
|
display: flex;
|
||||||
text-decoration: none;
|
text-decoration: none;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
@ -65,7 +65,7 @@
|
|||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
min-height: 70px;
|
min-height: 70px;
|
||||||
padding: .5rem 0;
|
padding: .5rem 1rem 0 0;
|
||||||
|
|
||||||
.top {
|
.top {
|
||||||
font-size: .8rem;
|
font-size: .8rem;
|
||||||
@ -168,7 +168,7 @@
|
|||||||
|
|
||||||
.add-project-button {
|
.add-project-button {
|
||||||
z-index: 100;
|
z-index: 100;
|
||||||
flex-basis: 250px;
|
flex-basis: 260px;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
display: flex;
|
display: flex;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
|
@ -16,7 +16,7 @@ const routes: Routes = [
|
|||||||
loadChildren: () => import('../project-create/project-create.module').then(m => m.ProjectCreateModule),
|
loadChildren: () => import('../project-create/project-create.module').then(m => m.ProjectCreateModule),
|
||||||
canActivate: [RoleGuard],
|
canActivate: [RoleGuard],
|
||||||
data: {
|
data: {
|
||||||
roles: ['project.write'],
|
roles: ['project.create'],
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
@ -1,26 +1,38 @@
|
|||||||
<h1 mat-dialog-title>
|
<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>
|
</h1>
|
||||||
<p class="desc">{{'USER.CODEDIALOG.DESCRIPTION' | translate}}</p>
|
|
||||||
<div mat-dialog-content>
|
<div mat-dialog-content>
|
||||||
<div class="type-selection">
|
<ng-container *ngIf="selectedType == undefined">
|
||||||
<button class="otp" (click)="selectType(AuthFactorType.OTP)">
|
<p class="desc">{{'USER.MFA.DIALOG.ADD_MFA_DESCRIPTION' | translate}}</p>
|
||||||
<mat-icon class="icon" svgIcon="mdi_radar"></mat-icon>
|
|
||||||
<span>{{'USER.MFA.OTP' | translate}}</span>
|
<div class="type-selection">
|
||||||
</button>
|
<button mat-raised-button [disabled]="data.otpDisabled" (click)="selectType(AuthFactorType.OTP)">
|
||||||
<button class="u2f" (click)="selectType(AuthFactorType.U2F)">
|
<div class="otp-btn">
|
||||||
<i class="las la-fingerprint"></i>
|
<mat-icon class="icon" svgIcon="mdi_radar"></mat-icon>
|
||||||
<span>{{'USER.MFA.U2F' | translate}}</span>
|
<span>{{'USER.MFA.OTP' | translate}}</span>
|
||||||
</button>
|
</div>
|
||||||
</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">
|
<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">
|
<div class="qrcode-wrapper">
|
||||||
<qrcode *ngIf="otpurl" class="qrcode" [qrdata]="otpurl" [width]="150" [errorCorrectionLevel]="'M'"></qrcode>
|
<qrcode *ngIf="otpurl" class="qrcode" [qrdata]="otpurl" [width]="150" [errorCorrectionLevel]="'M'"></qrcode>
|
||||||
</div>
|
</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>
|
<cnsl-label>Code</cnsl-label>
|
||||||
<input cnslInput [(ngModel)]="otpcode" />
|
<input cnslInput [(ngModel)]="otpcode" />
|
||||||
</cnsl-form-field>
|
</cnsl-form-field>
|
||||||
@ -44,7 +56,8 @@
|
|||||||
{{'ACTIONS.CLOSE' | translate}}
|
{{'ACTIONS.CLOSE' | translate}}
|
||||||
</button>
|
</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}}
|
{{'ACTIONS.CREATE' | translate}}
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
@ -2,21 +2,44 @@
|
|||||||
display: flex;
|
display: flex;
|
||||||
margin: 0 -0.5rem;
|
margin: 0 -0.5rem;
|
||||||
|
|
||||||
.otp,
|
.otp-btn,
|
||||||
.u2f {
|
.u2f-btn {
|
||||||
flex: 1;
|
flex: 1;
|
||||||
min-height: 100px;
|
min-height: 100px;
|
||||||
border-radius: .5rem;
|
border-radius: .5rem;
|
||||||
border: 1px solid var(--grey);
|
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
padding: 1rem;
|
margin: 1rem;
|
||||||
|
box-sizing: border-box;
|
||||||
|
|
||||||
|
.icon-row {
|
||||||
|
display: flex;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
button {
|
||||||
margin: .5rem;
|
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 {
|
.formfield {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
}
|
}
|
||||||
|
@ -42,6 +42,8 @@ export class AuthFactorDialogComponent {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public selectType(type: AuthFactorType): void {
|
public selectType(type: AuthFactorType): void {
|
||||||
|
this.selectedType = type;
|
||||||
|
|
||||||
if (type == AuthFactorType.OTP) {
|
if (type == AuthFactorType.OTP) {
|
||||||
this.authService.addMyMultiFactorOTP().then((otpresp) => {
|
this.authService.addMyMultiFactorOTP().then((otpresp) => {
|
||||||
this.otpurl = otpresp.url;
|
this.otpurl = otpresp.url;
|
||||||
@ -95,6 +97,7 @@ export class AuthFactorDialogComponent {
|
|||||||
(resp as any).response.clientDataJSON &&
|
(resp as any).response.clientDataJSON &&
|
||||||
(resp as any).rawId) {
|
(resp as any).rawId) {
|
||||||
|
|
||||||
|
console.log(resp);
|
||||||
const attestationObject = (resp as any).response.attestationObject;
|
const attestationObject = (resp as any).response.attestationObject;
|
||||||
const clientDataJSON = (resp as any).response.clientDataJSON;
|
const clientDataJSON = (resp as any).response.clientDataJSON;
|
||||||
const rawId = (resp as any).rawId;
|
const rawId = (resp as any).rawId;
|
||||||
|
@ -36,9 +36,9 @@
|
|||||||
</table>
|
</table>
|
||||||
</app-refresh-table>
|
</app-refresh-table>
|
||||||
<div class="add-row">
|
<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}}">
|
matTooltip="{{'ACTIONS.NEW' | translate}}">
|
||||||
<i class="las la-fingerprint"></i>
|
<i class="icon las la-fingerprint"></i>
|
||||||
{{'USER.PASSWORDLESS.U2F' | translate}}
|
{{'USER.PASSWORDLESS.U2F' | translate}}
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
@ -3,10 +3,13 @@
|
|||||||
display: flex;
|
display: flex;
|
||||||
margin: -.5rem;
|
margin: -.5rem;
|
||||||
flex-wrap: wrap;
|
flex-wrap: wrap;
|
||||||
|
justify-content: flex-end;
|
||||||
|
|
||||||
.button {
|
.button {
|
||||||
margin: .5rem;
|
margin: .5rem;
|
||||||
margin-top: 1rem;
|
margin-top: 1rem;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
|
||||||
.icon {
|
.icon {
|
||||||
margin-right: .5rem;
|
margin-right: .5rem;
|
||||||
|
@ -53,6 +53,7 @@ export class AuthPasswordlessComponent implements OnInit, OnDestroy {
|
|||||||
public addPasswordless(): void {
|
public addPasswordless(): void {
|
||||||
this.service.addMyPasswordless().then((resp) => {
|
this.service.addMyPasswordless().then((resp) => {
|
||||||
if (resp.key) {
|
if (resp.key) {
|
||||||
|
console.log(resp.key);
|
||||||
const credOptions: CredentialCreationOptions = JSON.parse(atob(resp.key.publicKey as string));
|
const credOptions: CredentialCreationOptions = JSON.parse(atob(resp.key.publicKey as string));
|
||||||
|
|
||||||
if (credOptions.publicKey?.challenge) {
|
if (credOptions.publicKey?.challenge) {
|
||||||
|
@ -27,10 +27,9 @@
|
|||||||
</div>
|
</div>
|
||||||
</app-card>
|
</app-card>
|
||||||
|
|
||||||
<app-card *ngIf="user && user.human?.profile?.userName" class=" app-card"
|
<app-card *ngIf="user && user.human?.profile" class=" app-card" title="{{ 'USER.PROFILE.TITLE' | translate }}">
|
||||||
title="{{ 'USER.PROFILE.TITLE' | translate }}">
|
<app-detail-form [genders]="genders" [languages]="languages" [username]="user.userName" [user]="user.human"
|
||||||
<app-detail-form [genders]="genders" [languages]="languages" [username]="user.human?.profile?.userName"
|
(changedLanguage)="changedLanguage($event)" (submitData)="saveProfile($event)">
|
||||||
[user]="user.human" (changedLanguage)="changedLanguage($event)" (submitData)="saveProfile($event)">
|
|
||||||
</app-detail-form>
|
</app-detail-form>
|
||||||
</app-card>
|
</app-card>
|
||||||
|
|
||||||
|
@ -69,6 +69,7 @@ export class AuthUserDetailComponent implements OnDestroy {
|
|||||||
this.user.human.profile?.firstName,
|
this.user.human.profile?.firstName,
|
||||||
this.user.human.profile?.lastName,
|
this.user.human.profile?.lastName,
|
||||||
this.user.human.profile?.nickName,
|
this.user.human.profile?.nickName,
|
||||||
|
this.user.human.profile?.displayName,
|
||||||
this.user.human.profile?.preferredLanguage,
|
this.user.human.profile?.preferredLanguage,
|
||||||
this.user.human.profile?.gender,
|
this.user.human.profile?.gender,
|
||||||
)
|
)
|
||||||
@ -169,6 +170,7 @@ export class AuthUserDetailComponent implements OnDestroy {
|
|||||||
titleKey: 'USER.LOGINMETHODS.PHONE.EDITTITLE',
|
titleKey: 'USER.LOGINMETHODS.PHONE.EDITTITLE',
|
||||||
descriptionKey: 'USER.LOGINMETHODS.PHONE.EDITDESC',
|
descriptionKey: 'USER.LOGINMETHODS.PHONE.EDITDESC',
|
||||||
value: this.user.human?.phone?.phone,
|
value: this.user.human?.phone?.phone,
|
||||||
|
type: type,
|
||||||
},
|
},
|
||||||
width: '400px',
|
width: '400px',
|
||||||
});
|
});
|
||||||
@ -180,6 +182,7 @@ export class AuthUserDetailComponent implements OnDestroy {
|
|||||||
});
|
});
|
||||||
break;
|
break;
|
||||||
case EditDialogType.EMAIL:
|
case EditDialogType.EMAIL:
|
||||||
|
console.log('email');
|
||||||
const dialogRefEmail = this.dialog.open(EditDialogComponent, {
|
const dialogRefEmail = this.dialog.open(EditDialogComponent, {
|
||||||
data: {
|
data: {
|
||||||
confirmKey: 'ACTIONS.SAVE',
|
confirmKey: 'ACTIONS.SAVE',
|
||||||
@ -188,6 +191,7 @@ export class AuthUserDetailComponent implements OnDestroy {
|
|||||||
titleKey: 'USER.LOGINMETHODS.EMAIL.EDITTITLE',
|
titleKey: 'USER.LOGINMETHODS.EMAIL.EDITTITLE',
|
||||||
descriptionKey: 'USER.LOGINMETHODS.EMAIL.EDITDESC',
|
descriptionKey: 'USER.LOGINMETHODS.EMAIL.EDITDESC',
|
||||||
value: this.user.human?.email?.email,
|
value: this.user.human?.email?.email,
|
||||||
|
type: type
|
||||||
},
|
},
|
||||||
width: '400px',
|
width: '400px',
|
||||||
});
|
});
|
||||||
|
@ -3,7 +3,10 @@
|
|||||||
<table class="table" mat-table [dataSource]="dataSource">
|
<table class="table" mat-table [dataSource]="dataSource">
|
||||||
<ng-container matColumnDef="type">
|
<ng-container matColumnDef="type">
|
||||||
<th mat-header-cell *matHeaderCellDef> {{ 'USER.MFA.TABLETYPE' | translate }} </th>
|
<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>
|
||||||
|
|
||||||
<ng-container matColumnDef="attr">
|
<ng-container matColumnDef="attr">
|
||||||
@ -39,10 +42,10 @@
|
|||||||
</table>
|
</table>
|
||||||
</app-refresh-table>
|
</app-refresh-table>
|
||||||
<div class="add-row">
|
<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}}">
|
matTooltip="{{'ACTIONS.NEW' | translate}}">
|
||||||
<mat-icon class="icon">add</mat-icon>
|
<mat-icon class="icon">add</mat-icon>
|
||||||
{{'USER.MFA.OTP' | translate}}
|
{{'USER.MFA.ADD' | translate}}
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
<div class="table-wrapper">
|
<div class="table-wrapper">
|
||||||
|
@ -3,6 +3,7 @@
|
|||||||
display: flex;
|
display: flex;
|
||||||
margin: -.5rem;
|
margin: -.5rem;
|
||||||
flex-wrap: wrap;
|
flex-wrap: wrap;
|
||||||
|
justify-content: flex-end;
|
||||||
|
|
||||||
.button {
|
.button {
|
||||||
margin: .5rem;
|
margin: .5rem;
|
||||||
|
@ -55,7 +55,9 @@ export class AuthUserMfaComponent implements OnInit, OnDestroy {
|
|||||||
|
|
||||||
public addAuthFactor(): void {
|
public addAuthFactor(): void {
|
||||||
const dialogRef = this.dialog.open(AuthFactorDialogComponent, {
|
const dialogRef = this.dialog.open(AuthFactorDialogComponent, {
|
||||||
width: '400px',
|
data: {
|
||||||
|
otpDisabled: !this.otpAvailable
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
dialogRef.afterClosed().subscribe((code) => {
|
dialogRef.afterClosed().subscribe((code) => {
|
||||||
@ -70,6 +72,7 @@ export class AuthUserMfaComponent implements OnInit, OnDestroy {
|
|||||||
public getMFAs(): void {
|
public getMFAs(): void {
|
||||||
this.service.listMyMultiFactors().then(mfas => {
|
this.service.listMyMultiFactors().then(mfas => {
|
||||||
const list = mfas.resultList;
|
const list = mfas.resultList;
|
||||||
|
console.log(list);
|
||||||
this.dataSource = new MatTableDataSource(list);
|
this.dataSource = new MatTableDataSource(list);
|
||||||
this.dataSource.sort = this.sort;
|
this.dataSource.sort = this.sort;
|
||||||
|
|
||||||
|
@ -4,9 +4,10 @@
|
|||||||
<p class="desc">{{data.descriptionKey | translate}}</p>
|
<p class="desc">{{data.descriptionKey | translate}}</p>
|
||||||
<div mat-dialog-content>
|
<div mat-dialog-content>
|
||||||
<cnsl-form-field class="formfield">
|
<cnsl-form-field class="formfield">
|
||||||
<cnsl-label>{{data.labelKey | translate }} <span *ngIf="phoneCountry">({{ phoneCountry }})</span></cnsl-label>
|
<cnsl-label>{{data.labelKey | translate }} <span *ngIf="isPhone && phoneCountry">({{ phoneCountry }})</span>
|
||||||
<input cnslInput [(ngModel)]="value" (change)="changeValue($event)"
|
</cnsl-label>
|
||||||
(keydown.enter)="value ? closeDialogWithValue(value) : null" />
|
<input [formControl]="valueControl" cnslInput
|
||||||
|
(keydown.enter)="valueControl.valid ? closeDialogWithValue() : null" />
|
||||||
</cnsl-form-field>
|
</cnsl-form-field>
|
||||||
</div>
|
</div>
|
||||||
<div mat-dialog-actions class="action">
|
<div mat-dialog-actions class="action">
|
||||||
@ -14,8 +15,8 @@
|
|||||||
{{data.cancelKey | translate}}
|
{{data.cancelKey | translate}}
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
<button [disabled]="!value" cdkFocusInitial color="primary" mat-raised-button class="ok-button"
|
<button [disabled]="valueControl.invalid" cdkFocusInitial color="primary" mat-raised-button class="ok-button"
|
||||||
(click)="closeDialogWithValue(value)">
|
(click)="closeDialogWithValue()">
|
||||||
{{data.confirmKey | translate}}
|
{{data.confirmKey | translate}}
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
@ -1,4 +1,5 @@
|
|||||||
import { Component, Inject } from '@angular/core';
|
import { Component, Inject } from '@angular/core';
|
||||||
|
import { FormControl, Validators } from '@angular/forms';
|
||||||
import { MAT_DIALOG_DATA, MatDialogRef } from '@angular/material/dialog';
|
import { MAT_DIALOG_DATA, MatDialogRef } from '@angular/material/dialog';
|
||||||
import { parsePhoneNumber } from 'libphonenumber-js';
|
import { parsePhoneNumber } from 'libphonenumber-js';
|
||||||
|
|
||||||
@ -13,34 +14,46 @@ export enum EditDialogType {
|
|||||||
styleUrls: ['./edit-dialog.component.scss'],
|
styleUrls: ['./edit-dialog.component.scss'],
|
||||||
})
|
})
|
||||||
export class EditDialogComponent {
|
export class EditDialogComponent {
|
||||||
public value: string = '';
|
|
||||||
public isPhone: boolean = false;
|
public isPhone: boolean = false;
|
||||||
public phoneCountry: string = 'CH';
|
public phoneCountry: string = 'CH';
|
||||||
|
public valueControl: FormControl = new FormControl(['', [Validators.required]]);
|
||||||
constructor(public dialogRef: MatDialogRef<EditDialogComponent>,
|
constructor(public dialogRef: MatDialogRef<EditDialogComponent>,
|
||||||
@Inject(MAT_DIALOG_DATA) public data: any) {
|
@Inject(MAT_DIALOG_DATA) public data: any) {
|
||||||
this.value = data.value;
|
this.valueControl.setValue(data.value);
|
||||||
if (data.type == EditDialogType.PHONE) {
|
if (data.type == EditDialogType.PHONE) {
|
||||||
this.isPhone = true;
|
this.isPhone = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
this.valueControl.valueChanges.subscribe(value => {
|
||||||
|
console.log(value);
|
||||||
|
if (value && value.length > 1) {
|
||||||
|
this.changeValue(value);
|
||||||
|
}
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
changeValue(change: any) {
|
changeValue(changedValue: string) {
|
||||||
const value = change.target.value;
|
if (this.isPhone && changedValue) {
|
||||||
if (this.isPhone && value) {
|
try {
|
||||||
const phoneNumber = parsePhoneNumber(value ?? '', 'CH');
|
const phoneNumber = parsePhoneNumber(changedValue ?? '', 'CH');
|
||||||
if (phoneNumber) {
|
if (phoneNumber) {
|
||||||
const formmatted = phoneNumber.formatInternational();
|
const formmatted = phoneNumber.formatInternational();
|
||||||
this.phoneCountry = phoneNumber.country || '';
|
this.phoneCountry = phoneNumber.country || '';
|
||||||
this.value = formmatted;
|
if (formmatted !== this.valueControl.value) {
|
||||||
|
this.valueControl.setValue(formmatted);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error(error);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
closeDialog(email: string = ''): void {
|
closeDialog(): void {
|
||||||
this.dialogRef.close(email);
|
this.dialogRef.close();
|
||||||
}
|
}
|
||||||
|
|
||||||
closeDialogWithValue(value: string = ''): void {
|
closeDialogWithValue(): void {
|
||||||
this.dialogRef.close(value);
|
this.dialogRef.close(this.valueControl.value);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -16,6 +16,10 @@
|
|||||||
<cnsl-label>{{ 'USER.PROFILE.NICKNAME' | translate }}</cnsl-label>
|
<cnsl-label>{{ 'USER.PROFILE.NICKNAME' | translate }}</cnsl-label>
|
||||||
<input cnslInput formControlName="nickName" />
|
<input cnslInput formControlName="nickName" />
|
||||||
</cnsl-form-field>
|
</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-form-field class="formfield">
|
||||||
<cnsl-label>{{ 'USER.PROFILE.GENDER' | translate }}</cnsl-label>
|
<cnsl-label>{{ 'USER.PROFILE.GENDER' | translate }}</cnsl-label>
|
||||||
<mat-select formControlName="gender">
|
<mat-select formControlName="gender">
|
||||||
@ -34,7 +38,7 @@
|
|||||||
</cnsl-form-field>
|
</cnsl-form-field>
|
||||||
</div>
|
</div>
|
||||||
<div class="btn-container">
|
<div class="btn-container">
|
||||||
<button [disabled]="disabled" class="submit-button" type="submit" color="primary"
|
<button [disabled]="disabled" class="submit-button" type="submit" color="primary" mat-raised-button>{{
|
||||||
mat-raised-button>{{ 'ACTIONS.SAVE' | translate }}</button>
|
'ACTIONS.SAVE' | translate }}</button>
|
||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
@ -30,6 +30,7 @@ export class DetailFormComponent implements OnDestroy, OnChanges {
|
|||||||
firstName: [{ value: '', disabled: this.disabled }, Validators.required],
|
firstName: [{ value: '', disabled: this.disabled }, Validators.required],
|
||||||
lastName: [{ value: '', disabled: this.disabled }, Validators.required],
|
lastName: [{ value: '', disabled: this.disabled }, Validators.required],
|
||||||
nickName: [{ value: '', disabled: this.disabled }],
|
nickName: [{ value: '', disabled: this.disabled }],
|
||||||
|
displayName: [{ value: '', disabled: this.disabled }, Validators.required],
|
||||||
gender: [{ value: 0, disabled: this.disabled }],
|
gender: [{ value: 0, disabled: this.disabled }],
|
||||||
preferredLanguage: [{ value: '', 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],
|
firstName: [{ value: '', disabled: this.disabled }, Validators.required],
|
||||||
lastName: [{ value: '', disabled: this.disabled }, Validators.required],
|
lastName: [{ value: '', disabled: this.disabled }, Validators.required],
|
||||||
nickName: [{ value: '', disabled: this.disabled }],
|
nickName: [{ value: '', disabled: this.disabled }],
|
||||||
|
displayName: [{ value: '', disabled: this.disabled }, Validators.required],
|
||||||
gender: [{ value: 0, disabled: this.disabled }],
|
gender: [{ value: 0, disabled: this.disabled }],
|
||||||
preferredLanguage: [{ value: '', disabled: this.disabled }],
|
preferredLanguage: [{ value: '', disabled: this.disabled }],
|
||||||
});
|
});
|
||||||
@ -77,6 +79,9 @@ export class DetailFormComponent implements OnDestroy, OnChanges {
|
|||||||
public get nickName(): AbstractControl | null {
|
public get nickName(): AbstractControl | null {
|
||||||
return this.profileForm.get('nickName');
|
return this.profileForm.get('nickName');
|
||||||
}
|
}
|
||||||
|
public get displayName(): AbstractControl | null {
|
||||||
|
return this.profileForm.get('displayName');
|
||||||
|
}
|
||||||
public get gender(): AbstractControl | null {
|
public get gender(): AbstractControl | null {
|
||||||
return this.profileForm.get('gender');
|
return this.profileForm.get('gender');
|
||||||
}
|
}
|
||||||
|
@ -38,7 +38,7 @@
|
|||||||
class="avatar" [name]="user.human.profile.displayName" [size]="32">
|
class="avatar" [name]="user.human.profile.displayName" [size]="32">
|
||||||
</app-avatar>
|
</app-avatar>
|
||||||
<ng-template #cog>
|
<ng-template #cog>
|
||||||
<div class="sa-icon">
|
<div class="sa-icon" *ngIf="user.machine">
|
||||||
<i class="las la-user-cog"></i>
|
<i class="las la-user-cog"></i>
|
||||||
</div>
|
</div>
|
||||||
</ng-template>
|
</ng-template>
|
||||||
|
@ -37,8 +37,6 @@ export class AuthenticationService {
|
|||||||
|
|
||||||
public async authenticate(
|
public async authenticate(
|
||||||
partialConfig?: Partial<AuthConfig>,
|
partialConfig?: Partial<AuthConfig>,
|
||||||
setState: boolean = true,
|
|
||||||
force: boolean = false,
|
|
||||||
): Promise<boolean> {
|
): Promise<boolean> {
|
||||||
if (partialConfig) {
|
if (partialConfig) {
|
||||||
Object.assign(this.authConfig, partialConfig);
|
Object.assign(this.authConfig, partialConfig);
|
||||||
@ -50,8 +48,8 @@ export class AuthenticationService {
|
|||||||
|
|
||||||
this._authenticated = this.oauthService.hasValidAccessToken();
|
this._authenticated = this.oauthService.hasValidAccessToken();
|
||||||
|
|
||||||
if (!this.oauthService.hasValidIdToken() || !this.authenticated || partialConfig || force) {
|
if (!this.oauthService.hasValidIdToken() || !this.authenticated || partialConfig) {
|
||||||
const newState = setState ? await this.statehandler.createState().toPromise() : undefined;
|
const newState = await this.statehandler.createState().toPromise();
|
||||||
this.oauthService.initCodeFlow(newState);
|
this.oauthService.initCodeFlow(newState);
|
||||||
}
|
}
|
||||||
this._authenticationChanged.next(this.authenticated);
|
this._authenticationChanged.next(this.authenticated);
|
||||||
|
@ -253,6 +253,7 @@ export class GrpcAuthService {
|
|||||||
firstName?: string,
|
firstName?: string,
|
||||||
lastName?: string,
|
lastName?: string,
|
||||||
nickName?: string,
|
nickName?: string,
|
||||||
|
displayName?: string,
|
||||||
preferredLanguage?: string,
|
preferredLanguage?: string,
|
||||||
gender?: Gender,
|
gender?: Gender,
|
||||||
): Promise<UpdateMyProfileResponse.AsObject> {
|
): Promise<UpdateMyProfileResponse.AsObject> {
|
||||||
@ -266,6 +267,9 @@ export class GrpcAuthService {
|
|||||||
if (nickName) {
|
if (nickName) {
|
||||||
req.setNickName(nickName);
|
req.setNickName(nickName);
|
||||||
}
|
}
|
||||||
|
if (displayName) {
|
||||||
|
req.setDisplayName(displayName);
|
||||||
|
}
|
||||||
if (gender) {
|
if (gender) {
|
||||||
req.setGender(gender);
|
req.setGender(gender);
|
||||||
}
|
}
|
||||||
|
@ -61,7 +61,7 @@ export class AuthInterceptor<TReq = unknown, TResp = unknown> implements UnaryIn
|
|||||||
|
|
||||||
dialogRef.afterClosed().pipe(take(1)).subscribe(resp => {
|
dialogRef.afterClosed().pipe(take(1)).subscribe(resp => {
|
||||||
if (resp) {
|
if (resp) {
|
||||||
this.authenticationService.authenticate(undefined, true, true);
|
this.authenticationService.authenticate(undefined);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
@ -3,5 +3,5 @@
|
|||||||
"mgmtServiceUrl": "https://api.zitadel.io",
|
"mgmtServiceUrl": "https://api.zitadel.io",
|
||||||
"adminServiceUrl":"https://api.zitadel.io",
|
"adminServiceUrl":"https://api.zitadel.io",
|
||||||
"issuer": "https://issuer.zitadel.io",
|
"issuer": "https://issuer.zitadel.io",
|
||||||
"clientid": "98804911164221523@zitadel"
|
"clientid": "100129365194565743@zitadel"
|
||||||
}
|
}
|
||||||
|
@ -222,10 +222,11 @@
|
|||||||
"TITLE": "Multifaktor-Authentisierung",
|
"TITLE": "Multifaktor-Authentisierung",
|
||||||
"DESCRIPTION": "Füge einen zusätzlichen Faktor hinzu, um Dein Konto optimal zu schützen.",
|
"DESCRIPTION": "Füge einen zusätzlichen Faktor hinzu, um Dein Konto optimal zu schützen.",
|
||||||
"MANAGE_DESCRIPTION": "Verwalte die Multifaktor-Merkmale Deiner Benutzer.",
|
"MANAGE_DESCRIPTION": "Verwalte die Multifaktor-Merkmale Deiner Benutzer.",
|
||||||
"OTP": "OTP konfigurieren",
|
"ADD":"Faktor hinzufügen",
|
||||||
|
"OTP": "OTP (One-Time Password)",
|
||||||
"OTP_DIALOG_TITLE": "OTP hinzufügen",
|
"OTP_DIALOG_TITLE": "OTP hinzufügen",
|
||||||
"OTP_DIALOG_DESCRIPTION": "Scanne den QR-Code mit einer Authenticator App und verifiziere den erhaltenen Code, um OTP zu aktivieren.",
|
"OTP_DIALOG_DESCRIPTION": "Scanne den QR-Code mit einer Authenticator App und verifiziere den erhaltenen Code, um OTP zu aktivieren.",
|
||||||
"U2F":"U2F hinzufügen",
|
"U2F":"U2F (Universal 2nd Factor)",
|
||||||
"U2F_DIALOG_TITLE": "U2F hinzufügen",
|
"U2F_DIALOG_TITLE": "U2F hinzufügen",
|
||||||
"U2F_DIALOG_DESCRIPTION": "Gib einen Namen für den von dir verwendeten Universellen Multifaktor an.",
|
"U2F_DIALOG_DESCRIPTION": "Gib einen Namen für den von dir verwendeten Universellen Multifaktor an.",
|
||||||
"U2F_SUCCESS":"U2F erfolgreich erstellt!",
|
"U2F_SUCCESS":"U2F erfolgreich erstellt!",
|
||||||
@ -244,7 +245,9 @@
|
|||||||
},
|
},
|
||||||
"DIALOG": {
|
"DIALOG": {
|
||||||
"MFA_DELETE_TITLE":"Zweiten Faktor entfernen",
|
"MFA_DELETE_TITLE":"Zweiten Faktor entfernen",
|
||||||
"MFA_DELETE_DESCRIPTION":"Sie sind im Begriff eine Zweitfaktormethode zu entfernen. Sind sie sicher?"
|
"MFA_DELETE_DESCRIPTION":"Sie sind im Begriff eine Zweitfaktormethode zu entfernen. Sind sie sicher?",
|
||||||
|
"ADD_MFA_TITLE":"Zweiten Faktor hinzufügen",
|
||||||
|
"ADD_MFA_DESCRIPTION":"Wählen Sie einen der verfügbaren Optionen."
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"EXTERNALIDP": {
|
"EXTERNALIDP": {
|
||||||
|
@ -222,10 +222,11 @@
|
|||||||
"TITLE": "Multifactor Authentication",
|
"TITLE": "Multifactor Authentication",
|
||||||
"DESCRIPTION": "Add a second factor to ensure optimal security for your account.",
|
"DESCRIPTION": "Add a second factor to ensure optimal security for your account.",
|
||||||
"MANAGE_DESCRIPTION": "Manage the second factor methods of your users.",
|
"MANAGE_DESCRIPTION": "Manage the second factor methods of your users.",
|
||||||
"OTP": "Configure OTP",
|
"ADD":"Add AuthFactor",
|
||||||
|
"OTP": "OTP (One-Time Password)",
|
||||||
"OTP_DIALOG_TITLE": "Add OTP",
|
"OTP_DIALOG_TITLE": "Add OTP",
|
||||||
"OTP_DIALOG_DESCRIPTION": "Scan the QR code with an authenticator app and enter the code below to verify and activate the OTP method.",
|
"OTP_DIALOG_DESCRIPTION": "Scan the QR code with an authenticator app and enter the code below to verify and activate the OTP method.",
|
||||||
"U2F":"Add U2F",
|
"U2F":"U2F (Universal 2nd Factor)",
|
||||||
"U2F_DIALOG_TITLE": "Verify U2F",
|
"U2F_DIALOG_TITLE": "Verify U2F",
|
||||||
"U2F_DIALOG_DESCRIPTION": "Enter a name for your used universal Multifactor.",
|
"U2F_DIALOG_DESCRIPTION": "Enter a name for your used universal Multifactor.",
|
||||||
"U2F_SUCCESS":"U2F created successfully!",
|
"U2F_SUCCESS":"U2F created successfully!",
|
||||||
|
Loading…
Reference in New Issue
Block a user