diff --git a/console/src/app/pages/users/user-detail/metadata-dialog/metadata-dialog.component.html b/console/src/app/modules/metadata/metadata-dialog/metadata-dialog.component.html similarity index 81% rename from console/src/app/pages/users/user-detail/metadata-dialog/metadata-dialog.component.html rename to console/src/app/modules/metadata/metadata-dialog/metadata-dialog.component.html index 63e7b07dcf..5c04eb96d2 100644 --- a/console/src/app/pages/users/user-detail/metadata-dialog/metadata-dialog.component.html +++ b/console/src/app/modules/metadata/metadata-dialog/metadata-dialog.component.html @@ -1,22 +1,19 @@
-

{{ 'USER.METADATA.TITLE' | translate }}

+

{{ 'METADATA.TITLE' | translate }}

{{ ts | timestampToDate | localizedDate: 'dd. MMM, HH:mm' }}

-
-

{{ 'USER.METADATA.DESCRIPTION' | translate }}

+

{{ 'METADATA.DESCRIPTION' | translate }}

- {{ 'USER.METADATA.KEY' | translate }} + {{ 'METADATA.KEY' | translate }} - {{ 'USER.METADATA.VALUE' | translate }} + {{ 'METADATA.VALUE' | translate }} diff --git a/console/src/app/pages/users/user-detail/metadata-dialog/metadata-dialog.component.scss b/console/src/app/modules/metadata/metadata-dialog/metadata-dialog.component.scss similarity index 100% rename from console/src/app/pages/users/user-detail/metadata-dialog/metadata-dialog.component.scss rename to console/src/app/modules/metadata/metadata-dialog/metadata-dialog.component.scss diff --git a/console/src/app/pages/users/user-detail/metadata-dialog/metadata-dialog.component.spec.ts b/console/src/app/modules/metadata/metadata-dialog/metadata-dialog.component.spec.ts similarity index 100% rename from console/src/app/pages/users/user-detail/metadata-dialog/metadata-dialog.component.spec.ts rename to console/src/app/modules/metadata/metadata-dialog/metadata-dialog.component.spec.ts diff --git a/console/src/app/pages/users/user-detail/metadata-dialog/metadata-dialog.component.ts b/console/src/app/modules/metadata/metadata-dialog/metadata-dialog.component.ts similarity index 52% rename from console/src/app/pages/users/user-detail/metadata-dialog/metadata-dialog.component.ts rename to console/src/app/modules/metadata/metadata-dialog/metadata-dialog.component.ts index f4db7b7981..ff28df79b4 100644 --- a/console/src/app/pages/users/user-detail/metadata-dialog/metadata-dialog.component.ts +++ b/console/src/app/modules/metadata/metadata-dialog/metadata-dialog.component.ts @@ -3,7 +3,6 @@ import { MAT_DIALOG_DATA, MatDialogRef } from '@angular/material/dialog'; import { Buffer } from 'buffer'; import { Timestamp } from 'google-protobuf/google/protobuf/timestamp_pb'; import { Metadata } from 'src/app/proto/generated/zitadel/metadata_pb'; -import { GrpcAuthService } from 'src/app/services/grpc-auth.service'; import { ManagementService } from 'src/app/services/mgmt.service'; import { ToastService } from 'src/app/services/toast.service'; @@ -14,61 +13,15 @@ import { ToastService } from 'src/app/services/toast.service'; }) export class MetadataDialogComponent { public metadata: Partial[] = []; - public injData: any = {}; - public loading: boolean = true; + public loading: boolean = false; public ts!: Timestamp.AsObject | undefined; constructor( - private managementService: ManagementService, - private authService: GrpcAuthService, private toast: ToastService, public dialogRef: MatDialogRef, @Inject(MAT_DIALOG_DATA) public data: any, ) { - this.injData = data; - this.load(); - } - - public load(): void { - this.loadMetadata() - .then(() => { - this.loading = false; - if (this.metadata.length === 0) { - this.addEntry(); - } - }) - .catch((error) => { - this.loading = false; - this.toast.showError(error); - if (this.metadata.length === 0) { - this.addEntry(); - } - }); - } - - public loadMetadata(): Promise { - this.loading = true; - if (this.injData.userId) { - return this.managementService.listUserMetadata(this.injData.userId).then((resp) => { - this.metadata = resp.resultList.map((md) => { - return { - key: md.key, - value: Buffer.from(md.value as string, 'base64'), - }; - }); - this.ts = resp.details?.viewTimestamp; - }); - } else { - return this.authService.listMyMetadata().then((resp) => { - this.metadata = resp.resultList.map((md) => { - return { - key: md.key, - value: Buffer.from(md.value as string, 'base64'), - }; - }); - this.ts = resp.details?.viewTimestamp; - }); - } + this.metadata = data.metadata; } public addEntry(): void { @@ -104,24 +57,24 @@ export class MetadataDialogComponent { public setMetadata(key: string, value: string): void { if (key && value) { - this.managementService - .setUserMetadata(key, btoa(value), this.injData.userId) + this.data + .setFcn(key, value) .then(() => { - this.toast.showInfo('USER.METADATA.SETSUCCESS', true); + this.toast.showInfo('METADATA.SETSUCCESS', true); }) - .catch((error) => { + .catch((error: any) => { this.toast.showError(error); }); } } public removeMetadata(key: string): Promise { - return this.managementService - .removeUserMetadata(key, this.injData.userId) - .then((resp) => { - this.toast.showInfo('USER.METADATA.REMOVESUCCESS', true); + return this.data + .removeFcn(key) + .then((resp: any) => { + this.toast.showInfo('METADATA.REMOVESUCCESS', true); }) - .catch((error) => { + .catch((error: any) => { this.toast.showError(error); }); } diff --git a/console/src/app/modules/metadata/metadata.module.ts b/console/src/app/modules/metadata/metadata.module.ts new file mode 100644 index 0000000000..b5b8807d44 --- /dev/null +++ b/console/src/app/modules/metadata/metadata.module.ts @@ -0,0 +1,40 @@ +import { CommonModule } from '@angular/common'; +import { NgModule } from '@angular/core'; +import { FormsModule } from '@angular/forms'; +import { MatButtonModule } from '@angular/material/button'; +import { MatDialogModule } from '@angular/material/dialog'; +import { MatIconModule } from '@angular/material/icon'; +import { MatProgressSpinnerModule } from '@angular/material/progress-spinner'; +import { MatTableModule } from '@angular/material/table'; +import { MatTooltipModule } from '@angular/material/tooltip'; +import { TranslateModule } from '@ngx-translate/core'; +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 { CardModule } from '../card/card.module'; + +import { InputModule } from '../input/input.module'; +import { RefreshTableModule } from '../refresh-table/refresh-table.module'; +import { MetadataDialogComponent } from './metadata-dialog/metadata-dialog.component'; +import { MetadataComponent } from './metadata/metadata.component'; + +@NgModule({ + declarations: [MetadataComponent, MetadataDialogComponent], + imports: [ + CommonModule, + MatDialogModule, + MatProgressSpinnerModule, + CardModule, + MatButtonModule, + TranslateModule, + InputModule, + MatIconModule, + MatTooltipModule, + FormsModule, + LocalizedDatePipeModule, + TimestampToDatePipeModule, + RefreshTableModule, + MatTableModule, + ], + exports: [MetadataComponent, MetadataDialogComponent], +}) +export class MetadataModule {} diff --git a/console/src/app/modules/metadata/metadata/metadata.component.html b/console/src/app/modules/metadata/metadata/metadata.component.html new file mode 100644 index 0000000000..d94ac1cdfd --- /dev/null +++ b/console/src/app/modules/metadata/metadata/metadata.component.html @@ -0,0 +1,36 @@ + diff --git a/console/src/app/pages/users/user-detail/metadata/metadata.component.scss b/console/src/app/modules/metadata/metadata/metadata.component.scss similarity index 84% rename from console/src/app/pages/users/user-detail/metadata/metadata.component.scss rename to console/src/app/modules/metadata/metadata/metadata.component.scss index 5ac045e2e1..766bed97e5 100644 --- a/console/src/app/pages/users/user-detail/metadata/metadata.component.scss +++ b/console/src/app/modules/metadata/metadata/metadata.component.scss @@ -1,16 +1,19 @@ .metadata-details { padding-bottom: 1rem; - .metadata-actions { - display: flex; - align-items: center; - justify-content: flex-end; + .refresh { + margin-left: 0.5rem; + font-size: 1.2rem; - .edit { - font-size: 14px; + i { + font-size: 1.2rem; } } + .edit { + font-size: 14px; + } + .meta-row { display: flex; margin-bottom: 0.5rem; diff --git a/console/src/app/pages/users/user-detail/metadata/metadata.component.spec.ts b/console/src/app/modules/metadata/metadata/metadata.component.spec.ts similarity index 100% rename from console/src/app/pages/users/user-detail/metadata/metadata.component.spec.ts rename to console/src/app/modules/metadata/metadata/metadata.component.spec.ts diff --git a/console/src/app/modules/metadata/metadata/metadata.component.ts b/console/src/app/modules/metadata/metadata/metadata.component.ts new file mode 100644 index 0000000000..826bb1c992 --- /dev/null +++ b/console/src/app/modules/metadata/metadata/metadata.component.ts @@ -0,0 +1,34 @@ +import { Component, EventEmitter, Input, OnChanges, Output, SimpleChanges, ViewChild } from '@angular/core'; +import { MatSort } from '@angular/material/sort'; +import { MatTable, MatTableDataSource } from '@angular/material/table'; +import { BehaviorSubject, Observable } from 'rxjs'; +import { Metadata } from 'src/app/proto/generated/zitadel/metadata_pb'; + +@Component({ + selector: 'cnsl-metadata', + templateUrl: './metadata.component.html', + styleUrls: ['./metadata.component.scss'], +}) +export class MetadataComponent implements OnChanges { + @Input() public metadata: Metadata.AsObject[] = []; + @Input() public disabled: boolean = false; + @Input() public loading: boolean = false; + @Output() public editClicked: EventEmitter = new EventEmitter(); + @Output() public refresh: EventEmitter = new EventEmitter(); + + public displayedColumns: string[] = ['key', 'value']; + private loadingSubject: BehaviorSubject = new BehaviorSubject(false); + public loading$: Observable = this.loadingSubject.asObservable(); + + @ViewChild(MatTable) public table!: MatTable; + @ViewChild(MatSort) public sort!: MatSort; + public dataSource: MatTableDataSource = new MatTableDataSource([]); + + constructor() {} + + ngOnChanges(changes: SimpleChanges): void { + if (changes.metadata?.currentValue) { + this.dataSource = new MatTableDataSource(changes.metadata.currentValue); + } + } +} diff --git a/console/src/app/pages/actions/action-table/action-table.component.ts b/console/src/app/pages/actions/action-table/action-table.component.ts index 83a6b75a8f..38e1e84e4b 100644 --- a/console/src/app/pages/actions/action-table/action-table.component.ts +++ b/console/src/app/pages/actions/action-table/action-table.component.ts @@ -84,7 +84,8 @@ export class ActionTableComponent implements OnInit { .deleteAction(action.id) .then(() => { this.toast.showInfo('FLOWS.DIALOG.DELETEACTION.DELETE_SUCCESS', true); - this.getData(20, 0); + + this.refreshPage(); }) .catch((error: any) => { this.toast.showError(error); @@ -152,7 +153,9 @@ export class ActionTableComponent implements OnInit { } public refreshPage(): void { - this.getData(this.paginator.pageSize, this.paginator.pageIndex * this.paginator.pageSize); + setTimeout(() => { + this.getData(this.paginator.pageSize, this.paginator.pageIndex * this.paginator.pageSize); + }, 1000); } public deactivateSelection(): Promise { diff --git a/console/src/app/pages/orgs/org-detail/org-detail.component.html b/console/src/app/pages/orgs/org-detail/org-detail.component.html index cdc1407c6d..7c2a80ede9 100644 --- a/console/src/app/pages/orgs/org-detail/org-detail.component.html +++ b/console/src/app/pages/orgs/org-detail/org-detail.component.html @@ -46,6 +46,13 @@ + +
{{ diff --git a/console/src/app/pages/orgs/org-detail/org-detail.component.ts b/console/src/app/pages/orgs/org-detail/org-detail.component.ts index 44ade63be2..d1464829ec 100644 --- a/console/src/app/pages/orgs/org-detail/org-detail.component.ts +++ b/console/src/app/pages/orgs/org-detail/org-detail.component.ts @@ -1,20 +1,23 @@ import { Component, OnDestroy, OnInit } from '@angular/core'; import { MatDialog } from '@angular/material/dialog'; -import { ActivatedRoute, Router } from '@angular/router'; +import { Router } from '@angular/router'; import { BehaviorSubject, from, Observable, of, Subject, takeUntil } from 'rxjs'; -import { catchError, finalize, map, take } from 'rxjs/operators'; +import { catchError, finalize, map } from 'rxjs/operators'; import { CreationType, MemberCreateDialogComponent } from 'src/app/modules/add-member-dialog/member-create-dialog.component'; import { ChangeType } from 'src/app/modules/changes/changes.component'; import { InfoSectionType } from 'src/app/modules/info-section/info-section.component'; +import { MetadataDialogComponent } from 'src/app/modules/metadata/metadata-dialog/metadata-dialog.component'; import { PolicyComponentServiceType } from 'src/app/modules/policies/policy-component-types.enum'; import { WarnDialogComponent } from 'src/app/modules/warn-dialog/warn-dialog.component'; import { Member } from 'src/app/proto/generated/zitadel/member_pb'; +import { Metadata } from 'src/app/proto/generated/zitadel/metadata_pb'; import { Org, OrgState } from 'src/app/proto/generated/zitadel/org_pb'; import { User } from 'src/app/proto/generated/zitadel/user_pb'; import { Breadcrumb, BreadcrumbService, BreadcrumbType } from 'src/app/services/breadcrumb.service'; import { GrpcAuthService } from 'src/app/services/grpc-auth.service'; import { ManagementService } from 'src/app/services/mgmt.service'; import { ToastService } from 'src/app/services/toast.service'; +import { Buffer } from 'buffer'; @Component({ selector: 'cnsl-org-detail', @@ -28,6 +31,9 @@ export class OrgDetailComponent implements OnInit, OnDestroy { public OrgState: any = OrgState; public ChangeType: any = ChangeType; + public metadata: Metadata.AsObject[] = []; + public loadingMetadata: boolean = true; + // members private loadingSubject: BehaviorSubject = new BehaviorSubject(false); public loading$: Observable = this.loadingSubject.asObservable(); @@ -36,6 +42,7 @@ export class OrgDetailComponent implements OnInit, OnDestroy { private destroy$: Subject = new Subject(); public InfoSectionType: any = InfoSectionType; + constructor( auth: GrpcAuthService, private dialog: MatDialog, @@ -52,11 +59,13 @@ export class OrgDetailComponent implements OnInit, OnDestroy { auth.activeOrgChanged.pipe(takeUntil(this.destroy$)).subscribe((org) => { this.getData(); + this.loadMetadata(); }); } public ngOnInit(): void { this.getData(); + this.loadMetadata(); } public ngOnDestroy(): void { @@ -188,4 +197,40 @@ export class OrgDetailComponent implements OnInit, OnDestroy { this.membersSubject.next(members); }); } + + public loadMetadata(): Promise | void { + this.loadingMetadata = true; + return this.mgmtService + .listOrgMetadata() + .then((resp) => { + this.loadingMetadata = false; + this.metadata = resp.resultList.map((md) => { + return { + key: md.key, + value: Buffer.from(md.value as string, 'base64').toString('ascii'), + }; + }); + }) + .catch((error) => { + this.loadingMetadata = false; + this.toast.showError(error); + }); + } + + public editMetadata(): void { + const setFcn = (key: string, value: string): Promise => this.mgmtService.setOrgMetadata(key, btoa(value)); + const removeFcn = (key: string): Promise => this.mgmtService.removeOrgMetadata(key); + + const dialogRef = this.dialog.open(MetadataDialogComponent, { + data: { + metadata: this.metadata, + setFcn: setFcn, + removeFcn: removeFcn, + }, + }); + + dialogRef.afterClosed().subscribe(() => { + this.loadMetadata(); + }); + } } diff --git a/console/src/app/pages/orgs/org.module.ts b/console/src/app/pages/orgs/org.module.ts index e8fc9e3eff..a80c6b1854 100644 --- a/console/src/app/pages/orgs/org.module.ts +++ b/console/src/app/pages/orgs/org.module.ts @@ -18,6 +18,7 @@ import { InfoRowModule } from 'src/app/modules/info-row/info-row.module'; import { InfoSectionModule } from 'src/app/modules/info-section/info-section.module'; import { InputModule } from 'src/app/modules/input/input.module'; import { MetaLayoutModule } from 'src/app/modules/meta-layout/meta-layout.module'; +import { MetadataModule } from 'src/app/modules/metadata/metadata.module'; import { SettingsGridModule } from 'src/app/modules/settings-grid/settings-grid.module'; import { SharedModule } from 'src/app/modules/shared/shared.module'; import { TopViewModule } from 'src/app/modules/top-view/top-view.module'; @@ -53,6 +54,7 @@ import { OrgRoutingModule } from './org-routing.module'; MatMenuModule, ChangesModule, MatProgressSpinnerModule, + MetadataModule, TranslateModule, SharedModule, SettingsGridModule, diff --git a/console/src/app/pages/users/user-detail/auth-user-detail/auth-passwordless/auth-passwordless.component.html b/console/src/app/pages/users/user-detail/auth-user-detail/auth-passwordless/auth-passwordless.component.html index 253707be1a..76474b2242 100644 --- a/console/src/app/pages/users/user-detail/auth-user-detail/auth-passwordless/auth-passwordless.component.html +++ b/console/src/app/pages/users/user-detail/auth-user-detail/auth-passwordless/auth-passwordless.component.html @@ -25,7 +25,7 @@ color="primary" matTooltip="{{ 'ACTIONS.NEW' | translate }}" > - + add {{ 'USER.PASSWORDLESS.U2F' | translate }} diff --git a/console/src/app/pages/users/user-detail/auth-user-detail/auth-passwordless/dialog-passwordless/dialog-passwordless.component.html b/console/src/app/pages/users/user-detail/auth-user-detail/auth-passwordless/dialog-passwordless/dialog-passwordless.component.html index 50f18bb448..c34959e325 100644 --- a/console/src/app/pages/users/user-detail/auth-user-detail/auth-passwordless/dialog-passwordless/dialog-passwordless.component.html +++ b/console/src/app/pages/users/user-detail/auth-user-detail/auth-passwordless/dialog-passwordless/dialog-passwordless.component.html @@ -3,7 +3,7 @@

{{ 'USER.PASSWORDLESS.DIALOG.ADD_DESCRIPTION' | translate }}

-
+

{{ 'USER.PASSWORDLESS.DIALOG.NEW_DESCRIPTION' | translate }}

@@ -19,7 +19,7 @@

{{ error }}

-
+

{{ 'USER.PASSWORDLESS.DIALOG.SEND_DESCRIPTION' | translate }}

@@ -28,7 +28,7 @@ {{ 'USER.PASSWORDLESS.DIALOG.SEND' | translate }} -
+

{{ 'USER.PASSWORDLESS.DIALOG.QRCODE_DESCRIPTION' | translate }}

diff --git a/console/src/app/pages/users/user-detail/auth-user-detail/auth-passwordless/dialog-passwordless/dialog-passwordless.component.scss b/console/src/app/pages/users/user-detail/auth-user-detail/auth-passwordless/dialog-passwordless/dialog-passwordless.component.scss index 55eed0592e..0f4f837516 100644 --- a/console/src/app/pages/users/user-detail/auth-user-detail/auth-passwordless/dialog-passwordless/dialog-passwordless.component.scss +++ b/console/src/app/pages/users/user-detail/auth-user-detail/auth-passwordless/dialog-passwordless/dialog-passwordless.component.scss @@ -12,7 +12,7 @@ margin: 0; } -.desc { +.passwordless-desc { display: flex; align-items: center; padding-top: 1rem; diff --git a/console/src/app/pages/users/user-detail/auth-user-detail/auth-user-detail.component.html b/console/src/app/pages/users/user-detail/auth-user-detail/auth-user-detail.component.html index 9d58f480f4..0946bdef04 100644 --- a/console/src/app/pages/users/user-detail/auth-user-detail/auth-user-detail.component.html +++ b/console/src/app/pages/users/user-detail/auth-user-detail/auth-user-detail.component.html @@ -130,7 +130,13 @@ - + diff --git a/console/src/app/pages/users/user-detail/auth-user-detail/auth-user-detail.component.ts b/console/src/app/pages/users/user-detail/auth-user-detail/auth-user-detail.component.ts index 0fee13f046..521f95bcd7 100644 --- a/console/src/app/pages/users/user-detail/auth-user-detail/auth-user-detail.component.ts +++ b/console/src/app/pages/users/user-detail/auth-user-detail/auth-user-detail.component.ts @@ -6,15 +6,18 @@ import { ActivatedRoute, Params } from '@angular/router'; import { TranslateService } from '@ngx-translate/core'; import { Subscription, take } from 'rxjs'; import { ChangeType } from 'src/app/modules/changes/changes.component'; +import { MetadataDialogComponent } from 'src/app/modules/metadata/metadata-dialog/metadata-dialog.component'; import { SidenavSetting } from 'src/app/modules/sidenav/sidenav.component'; import { UserGrantContext } from 'src/app/modules/user-grants/user-grants-datasource'; import { WarnDialogComponent } from 'src/app/modules/warn-dialog/warn-dialog.component'; +import { Metadata } from 'src/app/proto/generated/zitadel/metadata_pb'; import { Email, Gender, Phone, Profile, User, UserState } from 'src/app/proto/generated/zitadel/user_pb'; import { AuthenticationService } from 'src/app/services/authentication.service'; import { Breadcrumb, BreadcrumbService, BreadcrumbType } from 'src/app/services/breadcrumb.service'; import { GrpcAuthService } from 'src/app/services/grpc-auth.service'; +import { ManagementService } from 'src/app/services/mgmt.service'; import { ToastService } from 'src/app/services/toast.service'; - +import { Buffer } from 'buffer'; import { EditDialogComponent, EditDialogType } from './edit-dialog/edit-dialog.component'; @Component({ @@ -30,6 +33,7 @@ export class AuthUserDetailComponent implements OnDestroy { private subscription: Subscription = new Subscription(); public loading: boolean = false; + public loadingMetadata: boolean = false; public ChangeType: any = ChangeType; public userLoginMustBeDomain: boolean = false; @@ -38,6 +42,8 @@ export class AuthUserDetailComponent implements OnDestroy { public USERGRANTCONTEXT: UserGrantContext = UserGrantContext.USER; public refreshChanges$: EventEmitter = new EventEmitter(); + public metadata: Metadata.AsObject[] = []; + public settingsList: SidenavSetting[] = [ { id: 'general', i18nKey: 'USER.SETTINGS.GENERAL' }, { id: 'idp', i18nKey: 'USER.SETTINGS.IDP' }, @@ -55,6 +61,7 @@ export class AuthUserDetailComponent implements OnDestroy { public userService: GrpcAuthService, private dialog: MatDialog, private auth: AuthenticationService, + private mgmt: ManagementService, private breadcrumbService: BreadcrumbService, private mediaMatcher: MediaMatcher, private _location: Location, @@ -104,6 +111,8 @@ export class AuthUserDetailComponent implements OnDestroy { if (resp.user) { this.user = resp.user; + this.loadMetadata(); + this.breadcrumbService.setBreadcrumb([ new Breadcrumb({ type: BreadcrumbType.AUTHUSER, @@ -337,4 +346,45 @@ export class AuthUserDetailComponent implements OnDestroy { } }); } + + public loadMetadata(): Promise | void { + if (this.user) { + this.loadingMetadata = true; + return this.mgmt + .listUserMetadata(this.user.id) + .then((resp) => { + this.loadingMetadata = false; + this.metadata = resp.resultList.map((md) => { + return { + key: md.key, + value: Buffer.from(md.value as string, 'base64').toString('ascii'), + }; + }); + }) + .catch((error) => { + this.loadingMetadata = false; + this.toast.showError(error); + }); + } + } + + public editMetadata(): void { + if (this.user && this.user.id) { + const setFcn = (key: string, value: string): Promise => + this.mgmt.setUserMetadata(key, Buffer.from(value).toString('base64'), this.user?.id ?? ''); + const removeFcn = (key: string): Promise => this.mgmt.removeUserMetadata(key, this.user?.id ?? ''); + + const dialogRef = this.dialog.open(MetadataDialogComponent, { + data: { + metadata: this.metadata, + setFcn: setFcn, + removeFcn: removeFcn, + }, + }); + + dialogRef.afterClosed().subscribe(() => { + this.loadMetadata(); + }); + } + } } diff --git a/console/src/app/pages/users/user-detail/metadata/metadata.component.html b/console/src/app/pages/users/user-detail/metadata/metadata.component.html deleted file mode 100644 index 88551c32aa..0000000000 --- a/console/src/app/pages/users/user-detail/metadata/metadata.component.html +++ /dev/null @@ -1,25 +0,0 @@ - diff --git a/console/src/app/pages/users/user-detail/metadata/metadata.component.ts b/console/src/app/pages/users/user-detail/metadata/metadata.component.ts deleted file mode 100644 index 6fce3c3075..0000000000 --- a/console/src/app/pages/users/user-detail/metadata/metadata.component.ts +++ /dev/null @@ -1,55 +0,0 @@ -import { Component, Input, OnInit } from '@angular/core'; -import { MatDialog } from '@angular/material/dialog'; -import { Metadata } from 'src/app/proto/generated/zitadel/metadata_pb'; -import { ManagementService } from 'src/app/services/mgmt.service'; -import { ToastService } from 'src/app/services/toast.service'; - -import { MetadataDialogComponent } from '../metadata-dialog/metadata-dialog.component'; - -@Component({ - selector: 'cnsl-metadata', - templateUrl: './metadata.component.html', - styleUrls: ['./metadata.component.scss'], -}) -export class MetadataComponent implements OnInit { - @Input() userId: string = ''; - public metadata: Metadata.AsObject[] = []; - public loading: boolean = false; - - constructor(private dialog: MatDialog, private service: ManagementService, private toast: ToastService) {} - - ngOnInit(): void { - this.loadMetadata(); - } - - public editMetadata(): void { - const dialogRef = this.dialog.open(MetadataDialogComponent, { - data: { - userId: this.userId, - }, - }); - - dialogRef.afterClosed().subscribe(() => { - this.loadMetadata(); - }); - } - - public loadMetadata(): Promise { - this.loading = true; - return (this.service as ManagementService) - .listUserMetadata(this.userId) - .then((resp) => { - this.loading = false; - this.metadata = resp.resultList.map((md) => { - return { - key: md.key, - value: atob(md.value as string), - }; - }); - }) - .catch((error) => { - this.loading = false; - this.toast.showError(error); - }); - } -} diff --git a/console/src/app/pages/users/user-detail/user-detail.module.ts b/console/src/app/pages/users/user-detail/user-detail.module.ts index e0980e8a20..2d40a29faf 100644 --- a/console/src/app/pages/users/user-detail/user-detail.module.ts +++ b/console/src/app/pages/users/user-detail/user-detail.module.ts @@ -52,12 +52,11 @@ import { ContactComponent } from './contact/contact.component'; import { DetailFormMachineModule } from './detail-form-machine/detail-form-machine.module'; import { DetailFormModule } from './detail-form/detail-form.module'; import { ExternalIdpsComponent } from './external-idps/external-idps.component'; -import { MetadataDialogComponent } from './metadata-dialog/metadata-dialog.component'; -import { MetadataComponent } from './metadata/metadata.component'; import { PasswordComponent } from './password/password.component'; 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'; +import { MetadataModule } from 'src/app/modules/metadata/metadata.module'; @NgModule({ declarations: [ @@ -76,8 +75,6 @@ import { UserMfaComponent } from './user-detail/user-mfa/user-mfa.component'; DialogU2FComponent, DialogPasswordlessComponent, AuthFactorDialogComponent, - MetadataDialogComponent, - MetadataComponent, ], imports: [ ChangesModule, @@ -95,6 +92,7 @@ import { UserMfaComponent } from './user-detail/user-mfa/user-mfa.component'; ShowTokenDialogModule, MetaLayoutModule, MatCheckboxModule, + MetadataModule, TopViewModule, HasRolePipeModule, UserGrantsModule, diff --git a/console/src/app/pages/users/user-detail/user-detail/user-detail.component.html b/console/src/app/pages/users/user-detail/user-detail/user-detail.component.html index 2bb025bb6b..8b9b4e30fc 100644 --- a/console/src/app/pages/users/user-detail/user-detail/user-detail.component.html +++ b/console/src/app/pages/users/user-detail/user-detail/user-detail.component.html @@ -206,7 +206,13 @@ - +
diff --git a/console/src/app/pages/users/user-detail/user-detail/user-detail.component.ts b/console/src/app/pages/users/user-detail/user-detail/user-detail.component.ts index a42a22f9be..2b54ccc6c7 100644 --- a/console/src/app/pages/users/user-detail/user-detail/user-detail.component.ts +++ b/console/src/app/pages/users/user-detail/user-detail/user-detail.component.ts @@ -7,6 +7,7 @@ import { TranslateService } from '@ngx-translate/core'; import { take } from 'rxjs/operators'; import { ChangeType } from 'src/app/modules/changes/changes.component'; import { InfoSectionType } from 'src/app/modules/info-section/info-section.component'; +import { MetadataDialogComponent } from 'src/app/modules/metadata/metadata-dialog/metadata-dialog.component'; import { SidenavSetting } from 'src/app/modules/sidenav/sidenav.component'; import { UserGrantContext } from 'src/app/modules/user-grants/user-grants-datasource'; import { WarnDialogComponent } from 'src/app/modules/warn-dialog/warn-dialog.component'; @@ -16,7 +17,7 @@ import { Email, Gender, Machine, Phone, Profile, User, UserState } from 'src/app import { Breadcrumb, BreadcrumbService, BreadcrumbType } from 'src/app/services/breadcrumb.service'; import { ManagementService } from 'src/app/services/mgmt.service'; import { ToastService } from 'src/app/services/toast.service'; - +import { Buffer } from 'buffer'; import { EditDialogComponent, EditDialogType } from '../auth-user-detail/edit-dialog/edit-dialog.component'; import { ResendEmailDialogComponent } from '../auth-user-detail/resend-email-dialog/resend-email-dialog.component'; @@ -42,7 +43,9 @@ export class UserDetailComponent implements OnInit { public languages: string[] = ['de', 'en', 'it', 'fr']; public ChangeType: any = ChangeType; + public loading: boolean = true; + public loadingMetadata: boolean = true; public UserState: any = UserState; public copied: string = ''; @@ -113,6 +116,7 @@ export class UserDetailComponent implements OnInit { this.mgmtUserService .getUserByID(id) .then((resp) => { + this.loadMetadata(id); this.loading = false; if (resp.user) { this.user = resp.user; @@ -129,17 +133,6 @@ export class UserDetailComponent implements OnInit { this.loading = false; this.toast.showError(err); }); - - this.mgmtUserService - .listUserMetadata(id, 0, 100, []) - .then((resp) => { - if (resp.resultList) { - this.metadata = resp.resultList; - } - }) - .catch((err) => { - console.error(err); - }); }); } @@ -448,4 +441,43 @@ export class UserDetailComponent implements OnInit { break; } } + + public loadMetadata(id: string): Promise | void { + this.loadingMetadata = true; + return this.mgmtUserService + .listUserMetadata(id) + .then((resp) => { + this.loadingMetadata = false; + this.metadata = resp.resultList.map((md) => { + return { + key: md.key, + value: Buffer.from(md.value as string, 'base64').toString('ascii'), + }; + }); + }) + .catch((error) => { + this.loadingMetadata = false; + this.toast.showError(error); + }); + } + + public editMetadata(): void { + if (this.user) { + const setFcn = (key: string, value: string): Promise => + this.mgmtUserService.setUserMetadata(key, Buffer.from(value).toString('base64'), this.user.id); + const removeFcn = (key: string): Promise => this.mgmtUserService.removeUserMetadata(key, this.user.id); + + const dialogRef = this.dialog.open(MetadataDialogComponent, { + data: { + metadata: this.metadata, + setFcn: setFcn, + removeFcn: removeFcn, + }, + }); + + dialogRef.afterClosed().subscribe(() => { + this.loadMetadata(this.user.id); + }); + } + } } diff --git a/console/src/app/services/mgmt.service.ts b/console/src/app/services/mgmt.service.ts index 0e1417cfc1..678db1fa68 100644 --- a/console/src/app/services/mgmt.service.ts +++ b/console/src/app/services/mgmt.service.ts @@ -219,6 +219,8 @@ import { ListOrgMemberRolesResponse, ListOrgMembersRequest, ListOrgMembersResponse, + ListOrgMetadataRequest, + ListOrgMetadataResponse, ListPersonalAccessTokensRequest, ListPersonalAccessTokensResponse, ListProjectChangesRequest, @@ -303,6 +305,8 @@ import { RemoveOrgIDPResponse, RemoveOrgMemberRequest, RemoveOrgMemberResponse, + RemoveOrgMetadataRequest, + RemoveOrgMetadataResponse, RemovePersonalAccessTokenRequest, RemovePersonalAccessTokenResponse, RemoveProjectGrantMemberRequest, @@ -371,6 +375,8 @@ import { SetCustomVerifyPhoneMessageTextRequest, SetCustomVerifyPhoneMessageTextResponse, SetHumanInitialPasswordRequest, + SetOrgMetadataRequest, + SetOrgMetadataResponse, SetPrimaryOrgDomainRequest, SetPrimaryOrgDomainResponse, SetTriggerActionsRequest, @@ -1374,6 +1380,26 @@ export class ManagementService { return this.grpcService.mgmt.listUserMetadata(req, null).then((resp) => resp.toObject()); } + public listOrgMetadata( + offset?: number, + limit?: number, + queryList?: MetadataQuery[], + ): Promise { + const req = new ListOrgMetadataRequest(); + + const metadata = new ListQuery(); + if (offset) { + metadata.setOffset(offset); + } + if (limit) { + metadata.setLimit(limit); + } + if (queryList) { + req.setQueriesList(queryList); + } + return this.grpcService.mgmt.listOrgMetadata(req, null).then((resp) => resp.toObject()); + } + public getUserMetadata(userId: string, key: string): Promise { const req = new GetUserMetadataRequest(); req.setId(userId); @@ -1389,6 +1415,13 @@ export class ManagementService { return this.grpcService.mgmt.setUserMetadata(req, null).then((resp) => resp.toObject()); } + public setOrgMetadata(key: string, value: string): Promise { + const req = new SetOrgMetadataRequest(); + req.setKey(key); + req.setValue(value); + return this.grpcService.mgmt.setOrgMetadata(req, null).then((resp) => resp.toObject()); + } + public bulkSetUserMetadata( list: BulkSetUserMetadataRequest.Metadata[], userId: string, @@ -1406,6 +1439,12 @@ export class ManagementService { return this.grpcService.mgmt.removeUserMetadata(req, null).then((resp) => resp.toObject()); } + public removeOrgMetadata(key: string): Promise { + const req = new RemoveOrgMetadataRequest(); + req.setKey(key); + return this.grpcService.mgmt.removeOrgMetadata(req, null).then((resp) => resp.toObject()); + } + public removeUser(id: string): Promise { const req = new RemoveUserRequest(); req.setId(id); diff --git a/console/src/assets/i18n/de.json b/console/src/assets/i18n/de.json index 4011953191..24038388c9 100644 --- a/console/src/assets/i18n/de.json +++ b/console/src/assets/i18n/de.json @@ -294,7 +294,7 @@ "TITLE": "Passwortlose Authentifizierungsmethoden", "DESCRIPTION": "Füge WebAuthn kompatible Authentifikatoren hinzu um dich passwortlos anzumelden.", "MANAGE_DESCRIPTION": "Verwalte die Multifaktor-Merkmale Deiner Benutzer.", - "U2F": "Authentifikator hinzufügen", + "U2F": "Methode hinzufügen", "U2F_DIALOG_TITLE": "Authentifikator hinzufügen", "U2F_DIALOG_DESCRIPTION": "Gib einen Namen für den von dir verwendeten Login an.", "U2F_SUCCESS": "Passwortlos erfolgreich erstellt!", @@ -326,17 +326,6 @@ "NEW": "Hinzufügen" } }, - "METADATA": { - "TITLE": "Metadata", - "DESCRIPTION": "", - "KEY": "Schlüssel", - "VALUE": "Wert", - "ADD": "Neues Element", - "SAVE": "Speichern", - "EMPTY": "Keine Metadaten", - "SETSUCCESS": "Element erfolgreich gespeichert", - "REMOVESUCCESS": "Element erfolgreich gelöscht" - }, "MFA": { "TABLETYPE": "Typ", "TABLESTATE": "Status", @@ -346,7 +335,7 @@ "DESCRIPTION": "Füge einen zusätzlichen Faktor hinzu, um Dein Konto optimal zu schützen.", "MANAGE_DESCRIPTION": "Verwalte die Multifaktor-Merkmale Deiner Benutzer.", "ADD": "Faktor hinzufügen", - "OTP": "OTP (One-Time Password)", + "OTP": "Authentikator App für OTP (One-Time Password)", "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.", "U2F": "Fingerabdruck, Security Key, Face ID oder andere", @@ -639,6 +628,17 @@ "DELETED": "Personal Access Token gelöscht." } }, + "METADATA": { + "TITLE": "Metadata", + "DESCRIPTION": "", + "KEY": "Schlüssel", + "VALUE": "Wert", + "ADD": "Neues Element", + "SAVE": "Speichern", + "EMPTY": "Keine Metadaten", + "SETSUCCESS": "Element erfolgreich gespeichert", + "REMOVESUCCESS": "Element erfolgreich gelöscht" + }, "FLOWS": { "TITLE": "Aktionen und Abläufe", "DESCRIPTION": "Hinterlege scripts die bei einem bestimmten Event ausgeführt werden.", diff --git a/console/src/assets/i18n/en.json b/console/src/assets/i18n/en.json index d247253316..28ed9d3a7c 100644 --- a/console/src/assets/i18n/en.json +++ b/console/src/assets/i18n/en.json @@ -294,7 +294,7 @@ "TITLE": "Passwordless Authentication", "DESCRIPTION": "Add WebAuthn based Authentication Methods to log onto ZITADEL passwordless.", "MANAGE_DESCRIPTION": "Manage the second factor methods of your users.", - "U2F": "Add authenticator", + "U2F": "Add method", "U2F_DIALOG_TITLE": "Verify authenticator", "U2F_DIALOG_DESCRIPTION": "Enter a name for your used passwordless Login", "U2F_SUCCESS": "Passwordless Auth created successfully!", @@ -326,17 +326,6 @@ "NEW": "Add New" } }, - "METADATA": { - "TITLE": "Metadata", - "DESCRIPTION": "", - "KEY": "Key", - "VALUE": "Value", - "ADD": "New Entry", - "SAVE": "Save", - "EMPTY": "No metadata", - "SETSUCCESS": "Element saved successfully", - "REMOVESUCCESS": "Element deleted successfully" - }, "MFA": { "TABLETYPE": "Type", "TABLESTATE": "Status", @@ -346,7 +335,7 @@ "DESCRIPTION": "Add a second factor to ensure optimal security for your account.", "MANAGE_DESCRIPTION": "Manage the second factor methods of your users.", "ADD": "Add Factor", - "OTP": "OTP (One-Time Password)", + "OTP": "Authenticator App for OTP (One-Time Password)", "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.", "U2F": "Fingerprint, Security Keys, Face ID and other", @@ -639,6 +628,17 @@ "DELETED": "Token deleted with success." } }, + "METADATA": { + "TITLE": "Metadata", + "DESCRIPTION": "", + "KEY": "Key", + "VALUE": "Value", + "ADD": "New Entry", + "SAVE": "Save", + "EMPTY": "No metadata", + "SETSUCCESS": "Element saved successfully", + "REMOVESUCCESS": "Element deleted successfully" + }, "FLOWS": { "TITLE": "Actions and Flows", "DESCRIPTION": "Define scripts to execute on a certain event.", diff --git a/console/src/assets/i18n/fr.json b/console/src/assets/i18n/fr.json index 8fe94a2465..b3416cc1ae 100644 --- a/console/src/assets/i18n/fr.json +++ b/console/src/assets/i18n/fr.json @@ -294,7 +294,7 @@ "TITLE": "Authentification sans mot de passe", "DESCRIPTION": "Ajoutez des méthodes d'authentification basées sur WebAuthn pour vous connecter à ZITADEL sans mot de passe.", "MANAGE_DESCRIPTION": "Gérez les méthodes de second facteur de vos utilisateurs.", - "U2F": "Ajouter un authentifiant", + "U2F": "Ajouter une méthode", "U2F_DIALOG_TITLE": "Vérifier l'authentifiant", "U2F_DIALOG_DESCRIPTION": "Entrez un nom pour votre connexion sans mot de passe utilisée", "U2F_SUCCESS": "Auth sans mot de passe créé avec succès !", @@ -326,17 +326,6 @@ "NEW": "Ajouter un nouveau" } }, - "METADATA": { - "TITLE": "Métadonnées", - "DESCRIPTION": "", - "KEY": "Clé", - "VALUE": "Valeur", - "ADD": "Nouvelle entrée", - "SAVE": "Enregistrer", - "EMPTY": "Pas de métadonnées", - "SETSUCCESS": "Élément sauvegardé avec succès", - "REMOVESUCCESS": "Élément supprimé avec succès" - }, "MFA": { "TABLETYPE": "Type", "TABLESTATE": "Statut", @@ -346,7 +335,7 @@ "DESCRIPTION": "Ajoutez un second facteur pour garantir une sécurité optimale de votre compte.", "MANAGE_DESCRIPTION": "Gérez les méthodes de second facteur de vos utilisateurs.", "ADD": "Ajouter un facteur", - "OTP": "OTP (mot de passe à usage unique)", + "OTP": "Application d'authentification pour OTP (One-time password)", "OTP_DIALOG_TITLE": "Ajouter un OTP", "OTP_DIALOG_DESCRIPTION": "Scannez le code QR avec une application d'authentification et saisissez le code ci-dessous pour vérifier et activer la méthode OTP.", "U2F": "Empreinte digitale, clés de sécurité, Face ID et autres", @@ -639,6 +628,17 @@ "DELETED": "Jeton supprimé avec succès." } }, + "METADATA": { + "TITLE": "Métadonnées", + "DESCRIPTION": "", + "KEY": "Clé", + "VALUE": "Valeur", + "ADD": "Nouvelle entrée", + "SAVE": "Enregistrer", + "EMPTY": "Pas de métadonnées", + "SETSUCCESS": "Élément sauvegardé avec succès", + "REMOVESUCCESS": "Élément supprimé avec succès" + }, "FLOWS": { "TITLE": "Actions et flux", "DESCRIPTION": "Définissez des scripts à exécuter lors d'un certain événement.", diff --git a/console/src/assets/i18n/it.json b/console/src/assets/i18n/it.json index 06338f826d..9ea0c260d5 100644 --- a/console/src/assets/i18n/it.json +++ b/console/src/assets/i18n/it.json @@ -294,7 +294,7 @@ "TITLE": "Autenticazione passwordless", "DESCRIPTION": "Aggiungi i metodi di autenticazione basati su WebAuthn per accedere a ZITADEL senza password.", "MANAGE_DESCRIPTION": "Gestisci i metodi del secondo fattore dei vostri utenti.", - "U2F": "Aggiungi autenticatore", + "U2F": "Aggiungi metodo", "U2F_DIALOG_TITLE": "Verifica autenticatore", "U2F_DIALOG_DESCRIPTION": "Inserisci un nome per il tuo authenticatore o dispositivo usato.", "U2F_SUCCESS": "Autorizzazione passwordless creata con successo!", @@ -326,17 +326,6 @@ "NEW": "Aggiungi nuovo" } }, - "METADATA": { - "TITLE": "Metadati", - "DESCRIPTION": "", - "KEY": "Chiave", - "VALUE": "Valore", - "ADD": "Nuova voce", - "SAVE": "Salva", - "EMPTY": "Nessun metadato", - "SETSUCCESS": "Salvato con successo", - "REMOVESUCCESS": "Rimosso con successo" - }, "MFA": { "TABLETYPE": "Tipo", "TABLESTATE": "Stato", @@ -346,7 +335,7 @@ "DESCRIPTION": "Aggiungi un secondo fattore per garantire la sicurezza ottimale del tuo account.", "MANAGE_DESCRIPTION": "Gestite i metodi del secondo fattore dei vostri utenti.", "ADD": "Aggiungi fattore", - "OTP": "OTP (One-Time Password)", + "OTP": "App di autenticazione per OTP (One-Time Password)", "OTP_DIALOG_TITLE": "Aggiungi OTP", "OTP_DIALOG_DESCRIPTION": "Scansiona il codice QR con un'app di autenticazione e inserisci il codice nel campo sottostante per verificare e attivare il metodo OTP.", "U2F": "Impronta digitale, chiave di sicurezza, Face ID e altri", @@ -639,6 +628,17 @@ "DELETED": "Token eliminato con successo." } }, + "METADATA": { + "TITLE": "Metadati", + "DESCRIPTION": "", + "KEY": "Chiave", + "VALUE": "Valore", + "ADD": "Nuova voce", + "SAVE": "Salva", + "EMPTY": "Nessun metadato", + "SETSUCCESS": "Salvato con successo", + "REMOVESUCCESS": "Rimosso con successo" + }, "FLOWS": { "TITLE": "Azioni e Processi", "DESCRIPTION": "Esegui processi su certi eventi.", diff --git a/console/src/assets/i18n/zh.json b/console/src/assets/i18n/zh.json index e8573d4049..63a7b02a3f 100644 --- a/console/src/assets/i18n/zh.json +++ b/console/src/assets/i18n/zh.json @@ -294,7 +294,7 @@ "TITLE": "无密码身份验证", "DESCRIPTION": "添加基于 WebAuthn 的身份验证方法以无密码登录 ZITADEL。", "MANAGE_DESCRIPTION": "管理用户的第二因素认证方式。", - "U2F": "添加验证器", + "U2F": "添加方法", "U2F_DIALOG_TITLE": "身份验证器", "U2F_DIALOG_DESCRIPTION": "输入您使用的无密码登录名", "U2F_SUCCESS": "无密码验证创建成功!", @@ -326,17 +326,6 @@ "NEW": "添加" } }, - "METADATA": { - "TITLE": "元数据", - "DESCRIPTION": "", - "KEY": "键", - "VALUE": "值", - "ADD": "新条目", - "SAVE": "保存", - "EMPTY": "暂无元数据", - "SETSUCCESS": "键值对保存成功", - "REMOVESUCCESS": "键值对删除成功" - }, "MFA": { "TABLETYPE": "类型", "TABLESTATE": "状态", @@ -346,7 +335,7 @@ "DESCRIPTION": "添加第二个因素以确保您帐户的最佳安全性。", "MANAGE_DESCRIPTION": "管理用户的第二因素身份认证方式。", "ADD": "添加因子", - "OTP": "一次性密码 (OTP)", + "OTP": "用于 OTP 的身份验证器应用程序", "OTP_DIALOG_TITLE": "添加 OTP", "OTP_DIALOG_DESCRIPTION": "使用验证器应用程序扫描二维码并输入下面的代码以验证并激活 OTP 方法。", "U2F": "指纹、安全密钥、Face ID 等", @@ -639,6 +628,17 @@ "DELETED": "成功删除令牌。" } }, + "METADATA": { + "TITLE": "元数据", + "DESCRIPTION": "", + "KEY": "键", + "VALUE": "值", + "ADD": "新条目", + "SAVE": "保存", + "EMPTY": "暂无元数据", + "SETSUCCESS": "键值对保存成功", + "REMOVESUCCESS": "键值对删除成功" + }, "FLOWS": { "TITLE": "动作和流程", "DESCRIPTION": "定义要在特定事件上需要执行的脚本。",