mirror of
https://github.com/zitadel/zitadel.git
synced 2025-02-28 22:37:24 +00:00
fix: v2 user console errors (#9386)
# Which Problems Are Solved - Fixed filtering in overview - Only get users from current organization - Use V2 api to get auth user # How the Problems Are Solved Added the organization filter to the List queries Get current User ID from ID Token to get auth user by id # Additional Changes Refactored the UserList # Additional Context - Closes #9382
This commit is contained in:
parent
9aad207ee4
commit
70234289cf
@ -1,4 +1,4 @@
|
|||||||
import { Component, OnInit } from '@angular/core';
|
import { Component, DestroyRef, OnInit } from '@angular/core';
|
||||||
import { MatCheckboxChange } from '@angular/material/checkbox';
|
import { MatCheckboxChange } from '@angular/material/checkbox';
|
||||||
import { ActivatedRoute, Router } from '@angular/router';
|
import { ActivatedRoute, Router } from '@angular/router';
|
||||||
import { take } from 'rxjs';
|
import { take } from 'rxjs';
|
||||||
@ -27,9 +27,10 @@ export class FilterOrgComponent extends FilterComponent implements OnInit {
|
|||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
router: Router,
|
router: Router,
|
||||||
|
destroyRef: DestroyRef,
|
||||||
protected override route: ActivatedRoute,
|
protected override route: ActivatedRoute,
|
||||||
) {
|
) {
|
||||||
super(router, route);
|
super(router, route, destroyRef);
|
||||||
}
|
}
|
||||||
|
|
||||||
ngOnInit(): void {
|
ngOnInit(): void {
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
import { Component, OnInit } from '@angular/core';
|
import { Component, DestroyRef, OnInit } from '@angular/core';
|
||||||
import { MatCheckboxChange } from '@angular/material/checkbox';
|
import { MatCheckboxChange } from '@angular/material/checkbox';
|
||||||
import { ActivatedRoute, Router } from '@angular/router';
|
import { ActivatedRoute, Router } from '@angular/router';
|
||||||
import { take } from 'rxjs';
|
import { take } from 'rxjs';
|
||||||
@ -23,8 +23,8 @@ export class FilterProjectComponent extends FilterComponent implements OnInit {
|
|||||||
public searchQueries: ProjectQuery[] = [];
|
public searchQueries: ProjectQuery[] = [];
|
||||||
|
|
||||||
public states: ProjectState[] = [ProjectState.PROJECT_STATE_ACTIVE, ProjectState.PROJECT_STATE_INACTIVE];
|
public states: ProjectState[] = [ProjectState.PROJECT_STATE_ACTIVE, ProjectState.PROJECT_STATE_INACTIVE];
|
||||||
constructor(router: Router, route: ActivatedRoute) {
|
constructor(router: Router, route: ActivatedRoute, destroyRef: DestroyRef) {
|
||||||
super(router, route);
|
super(router, route, destroyRef);
|
||||||
}
|
}
|
||||||
|
|
||||||
ngOnInit(): void {
|
ngOnInit(): void {
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
import { Component, OnInit } from '@angular/core';
|
import { Component, DestroyRef, OnInit } from '@angular/core';
|
||||||
import { MatCheckboxChange } from '@angular/material/checkbox';
|
import { MatCheckboxChange } from '@angular/material/checkbox';
|
||||||
import { ActivatedRoute, Router } from '@angular/router';
|
import { ActivatedRoute, Router } from '@angular/router';
|
||||||
import { take } from 'rxjs';
|
import { take } from 'rxjs';
|
||||||
@ -29,8 +29,8 @@ export class FilterUserGrantsComponent extends FilterComponent implements OnInit
|
|||||||
public SubQuery: any = SubQuery;
|
public SubQuery: any = SubQuery;
|
||||||
public searchQueries: UserGrantQuery[] = [];
|
public searchQueries: UserGrantQuery[] = [];
|
||||||
|
|
||||||
constructor(router: Router, route: ActivatedRoute) {
|
constructor(router: Router, route: ActivatedRoute, destroyRef: DestroyRef) {
|
||||||
super(router, route);
|
super(router, route, destroyRef);
|
||||||
}
|
}
|
||||||
|
|
||||||
ngOnInit(): void {
|
ngOnInit(): void {
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
<cnsl-filter (resetted)="resetFilter()" (trigger)="emitFilter()" [queryCount]="searchQueries.length">
|
<cnsl-filter (resetted)="resetFilter()" (trigger)="emitFilter()" [queryCount]="(filterChanged | async)?.length ?? 0">
|
||||||
<div class="filter-row" id="filtercomp">
|
<div class="filter-row" id="filtercomp">
|
||||||
<div class="name-query">
|
<div class="name-query">
|
||||||
<mat-checkbox
|
<mat-checkbox
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
import { Component, OnInit } from '@angular/core';
|
import { Component, DestroyRef, OnInit } from '@angular/core';
|
||||||
import { MatCheckboxChange } from '@angular/material/checkbox';
|
import { MatCheckboxChange } from '@angular/material/checkbox';
|
||||||
import { ActivatedRoute, Router } from '@angular/router';
|
import { ActivatedRoute, Router } from '@angular/router';
|
||||||
import { take } from 'rxjs';
|
import { take } from 'rxjs';
|
||||||
@ -13,6 +13,7 @@ import {
|
|||||||
} from 'src/app/proto/generated/zitadel/user_pb';
|
} from 'src/app/proto/generated/zitadel/user_pb';
|
||||||
|
|
||||||
import { FilterComponent } from '../filter/filter.component';
|
import { FilterComponent } from '../filter/filter.component';
|
||||||
|
import { filter, map } from 'rxjs/operators';
|
||||||
|
|
||||||
enum SubQuery {
|
enum SubQuery {
|
||||||
STATE,
|
STATE,
|
||||||
@ -28,25 +29,27 @@ enum SubQuery {
|
|||||||
})
|
})
|
||||||
export class FilterUserComponent extends FilterComponent implements OnInit {
|
export class FilterUserComponent extends FilterComponent implements OnInit {
|
||||||
public SubQuery: any = SubQuery;
|
public SubQuery: any = SubQuery;
|
||||||
public searchQueries: UserSearchQuery[] = [];
|
private searchQueries: UserSearchQuery[] = [];
|
||||||
|
|
||||||
public states: UserState[] = [
|
public states: UserState[] = [
|
||||||
UserState.USER_STATE_ACTIVE,
|
UserState.USER_STATE_ACTIVE,
|
||||||
UserState.USER_STATE_INACTIVE,
|
UserState.USER_STATE_INACTIVE,
|
||||||
UserState.USER_STATE_DELETED,
|
UserState.USER_STATE_DELETED,
|
||||||
UserState.USER_STATE_INITIAL,
|
|
||||||
UserState.USER_STATE_LOCKED,
|
UserState.USER_STATE_LOCKED,
|
||||||
UserState.USER_STATE_SUSPEND,
|
UserState.USER_STATE_INITIAL,
|
||||||
];
|
];
|
||||||
constructor(router: Router, route: ActivatedRoute) {
|
constructor(router: Router, route: ActivatedRoute, destroyRef: DestroyRef) {
|
||||||
super(router, route);
|
super(router, route, destroyRef);
|
||||||
}
|
}
|
||||||
|
|
||||||
ngOnInit(): void {
|
ngOnInit(): void {
|
||||||
this.route.queryParams.pipe(take(1)).subscribe((params) => {
|
this.route.queryParamMap
|
||||||
const { filter } = params;
|
.pipe(
|
||||||
if (filter) {
|
take(1),
|
||||||
const stringifiedFilters = filter as string;
|
map((params) => params.get('filter')),
|
||||||
|
filter(Boolean),
|
||||||
|
)
|
||||||
|
.subscribe((stringifiedFilters) => {
|
||||||
const filters: UserSearchQuery.AsObject[] = JSON.parse(stringifiedFilters) as UserSearchQuery.AsObject[];
|
const filters: UserSearchQuery.AsObject[] = JSON.parse(stringifiedFilters) as UserSearchQuery.AsObject[];
|
||||||
|
|
||||||
const userQueries = filters.map((filter) => {
|
const userQueries = filters.map((filter) => {
|
||||||
@ -94,7 +97,6 @@ export class FilterUserComponent extends FilterComponent implements OnInit {
|
|||||||
this.filterChanged.emit(this.searchQueries ? this.searchQueries : []);
|
this.filterChanged.emit(this.searchQueries ? this.searchQueries : []);
|
||||||
// this.showFilter = true;
|
// this.showFilter = true;
|
||||||
// this.filterOpen.emit(true);
|
// this.filterOpen.emit(true);
|
||||||
}
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1,7 +1,6 @@
|
|||||||
import { ConnectedPosition, ConnectionPositionPair } from '@angular/cdk/overlay';
|
import { ConnectedPosition, ConnectionPositionPair } from '@angular/cdk/overlay';
|
||||||
import { Component, EventEmitter, Input, OnDestroy, Output } from '@angular/core';
|
import { Component, DestroyRef, EventEmitter, Input, Output } from '@angular/core';
|
||||||
import { ActivatedRoute, Router } from '@angular/router';
|
import { ActivatedRoute, Router } from '@angular/router';
|
||||||
import { Observable, Subject, takeUntil } from 'rxjs';
|
|
||||||
import { SearchQuery as MemberSearchQuery } from 'src/app/proto/generated/zitadel/member_pb';
|
import { SearchQuery as MemberSearchQuery } from 'src/app/proto/generated/zitadel/member_pb';
|
||||||
import { TextQueryMethod } from 'src/app/proto/generated/zitadel/object_pb';
|
import { TextQueryMethod } from 'src/app/proto/generated/zitadel/object_pb';
|
||||||
import { OrgQuery } from 'src/app/proto/generated/zitadel/org_pb';
|
import { OrgQuery } from 'src/app/proto/generated/zitadel/org_pb';
|
||||||
@ -9,6 +8,7 @@ import { ProjectQuery } from 'src/app/proto/generated/zitadel/project_pb';
|
|||||||
import { SearchQuery as UserSearchQuery, UserGrantQuery } from 'src/app/proto/generated/zitadel/user_pb';
|
import { SearchQuery as UserSearchQuery, UserGrantQuery } from 'src/app/proto/generated/zitadel/user_pb';
|
||||||
|
|
||||||
import { ActionKeysType } from '../action-keys/action-keys.component';
|
import { ActionKeysType } from '../action-keys/action-keys.component';
|
||||||
|
import { takeUntilDestroyed } from '@angular/core/rxjs-interop';
|
||||||
|
|
||||||
type FilterSearchQuery = UserSearchQuery | MemberSearchQuery | UserGrantQuery | ProjectQuery | OrgQuery;
|
type FilterSearchQuery = UserSearchQuery | MemberSearchQuery | UserGrantQuery | ProjectQuery | OrgQuery;
|
||||||
type FilterSearchQueryAsObject =
|
type FilterSearchQueryAsObject =
|
||||||
@ -23,7 +23,7 @@ type FilterSearchQueryAsObject =
|
|||||||
templateUrl: './filter.component.html',
|
templateUrl: './filter.component.html',
|
||||||
styleUrls: ['./filter.component.scss'],
|
styleUrls: ['./filter.component.scss'],
|
||||||
})
|
})
|
||||||
export class FilterComponent implements OnDestroy {
|
export class FilterComponent {
|
||||||
@Output() public filterChanged: EventEmitter<FilterSearchQuery[]> = new EventEmitter();
|
@Output() public filterChanged: EventEmitter<FilterSearchQuery[]> = new EventEmitter();
|
||||||
@Output() public filterOpen: EventEmitter<boolean> = new EventEmitter<boolean>(false);
|
@Output() public filterOpen: EventEmitter<boolean> = new EventEmitter<boolean>(false);
|
||||||
|
|
||||||
@ -32,9 +32,6 @@ export class FilterComponent implements OnDestroy {
|
|||||||
|
|
||||||
@Input() public queryCount: number = 0;
|
@Input() public queryCount: number = 0;
|
||||||
|
|
||||||
private destroy$: Subject<void> = new Subject();
|
|
||||||
public filterChanged$: Observable<FilterSearchQuery[]> = this.filterChanged.asObservable();
|
|
||||||
|
|
||||||
public showFilter: boolean = false;
|
public showFilter: boolean = false;
|
||||||
public methods: TextQueryMethod[] = [
|
public methods: TextQueryMethod[] = [
|
||||||
TextQueryMethod.TEXT_QUERY_METHOD_CONTAINS_IGNORE_CASE,
|
TextQueryMethod.TEXT_QUERY_METHOD_CONTAINS_IGNORE_CASE,
|
||||||
@ -59,17 +56,13 @@ export class FilterComponent implements OnDestroy {
|
|||||||
this.trigger.emit();
|
this.trigger.emit();
|
||||||
}
|
}
|
||||||
|
|
||||||
public ngOnDestroy(): void {
|
|
||||||
this.destroy$.next();
|
|
||||||
this.destroy$.complete();
|
|
||||||
}
|
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
private router: Router,
|
private router: Router,
|
||||||
protected route: ActivatedRoute,
|
protected route: ActivatedRoute,
|
||||||
|
destroyRef: DestroyRef,
|
||||||
) {
|
) {
|
||||||
const changes$ = this.filterChanged.asObservable();
|
const changes$ = this.filterChanged.asObservable();
|
||||||
changes$.pipe(takeUntil(this.destroy$)).subscribe((queries) => {
|
changes$.pipe(takeUntilDestroyed(destroyRef)).subscribe((queries) => {
|
||||||
const filters: Array<FilterSearchQueryAsObject | {}> | undefined = queries
|
const filters: Array<FilterSearchQueryAsObject | {}> | undefined = queries
|
||||||
?.map((q) => q.toObject())
|
?.map((q) => q.toObject())
|
||||||
.map((query) =>
|
.map((query) =>
|
||||||
@ -81,7 +74,8 @@ export class FilterComponent implements OnDestroy {
|
|||||||
);
|
);
|
||||||
|
|
||||||
if (filters && Object.keys(filters)) {
|
if (filters && Object.keys(filters)) {
|
||||||
this.router.navigate([], {
|
this.router
|
||||||
|
.navigate([], {
|
||||||
relativeTo: this.route,
|
relativeTo: this.route,
|
||||||
queryParams: {
|
queryParams: {
|
||||||
['filter']: JSON.stringify(filters),
|
['filter']: JSON.stringify(filters),
|
||||||
@ -89,7 +83,8 @@ export class FilterComponent implements OnDestroy {
|
|||||||
replaceUrl: true,
|
replaceUrl: true,
|
||||||
queryParamsHandling: 'merge',
|
queryParamsHandling: 'merge',
|
||||||
skipLocationChange: false,
|
skipLocationChange: false,
|
||||||
});
|
})
|
||||||
|
.then();
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
@ -2,12 +2,11 @@
|
|||||||
<cnsl-top-view
|
<cnsl-top-view
|
||||||
title="{{ userName$ | async }}"
|
title="{{ userName$ | async }}"
|
||||||
sub="{{ user(userQuery)?.preferredLoginName }}"
|
sub="{{ user(userQuery)?.preferredLoginName }}"
|
||||||
[isActive]="user(userQuery)?.state === UserState.USER_STATE_ACTIVE"
|
[isActive]="user(userQuery)?.state === UserState.ACTIVE"
|
||||||
[isInactive]="user(userQuery)?.state === UserState.USER_STATE_INACTIVE"
|
[isInactive]="user(userQuery)?.state === UserState.INACTIVE"
|
||||||
stateTooltip="{{ 'USER.STATE.' + user(userQuery)?.state | translate }}"
|
stateTooltip="{{ 'USER.STATE.' + user(userQuery)?.state | translate }}"
|
||||||
[hasBackButton]="['org.read'] | hasRole | async"
|
[hasBackButton]="['org.read'] | hasRole | async"
|
||||||
>
|
>
|
||||||
<span *ngIf="userQuery.state === 'notfound'">{{ 'USER.PAGES.NOUSER' | translate }}</span>
|
|
||||||
<cnsl-info-row
|
<cnsl-info-row
|
||||||
topContent
|
topContent
|
||||||
*ngIf="user(userQuery) as user"
|
*ngIf="user(userQuery) as user"
|
||||||
@ -42,7 +41,7 @@
|
|||||||
[disabled]="false"
|
[disabled]="false"
|
||||||
[genders]="genders"
|
[genders]="genders"
|
||||||
[languages]="(langSvc.supported$ | async) || []"
|
[languages]="(langSvc.supported$ | async) || []"
|
||||||
[username]="user.userName"
|
[username]="user.username"
|
||||||
[profile]="profile"
|
[profile]="profile"
|
||||||
[showEditImage]="true"
|
[showEditImage]="true"
|
||||||
(changedLanguage)="changedLanguage($event)"
|
(changedLanguage)="changedLanguage($event)"
|
||||||
@ -93,7 +92,7 @@
|
|||||||
</ng-container>
|
</ng-container>
|
||||||
|
|
||||||
<ng-container *ngIf="currentSetting === 'idp'">
|
<ng-container *ngIf="currentSetting === 'idp'">
|
||||||
<cnsl-external-idps [userId]="user.id" [service]="grpcAuthService"></cnsl-external-idps>
|
<cnsl-external-idps [userId]="user.userId" [service]="grpcAuthService"></cnsl-external-idps>
|
||||||
</ng-container>
|
</ng-container>
|
||||||
|
|
||||||
<ng-container *ngIf="currentSetting === 'security'">
|
<ng-container *ngIf="currentSetting === 'security'">
|
||||||
@ -124,15 +123,14 @@
|
|||||||
<cnsl-auth-passwordless #mfaComponent></cnsl-auth-passwordless>
|
<cnsl-auth-passwordless #mfaComponent></cnsl-auth-passwordless>
|
||||||
|
|
||||||
<cnsl-auth-user-mfa
|
<cnsl-auth-user-mfa
|
||||||
[phoneVerified]="humanUser(userQuery)?.type?.value?.phone?.isPhoneVerified ?? false"
|
[phoneVerified]="humanUser(userQuery)?.type?.value?.phone?.isVerified ?? false"
|
||||||
#mfaComponent
|
|
||||||
></cnsl-auth-user-mfa>
|
></cnsl-auth-user-mfa>
|
||||||
</ng-container>
|
</ng-container>
|
||||||
|
|
||||||
<ng-container *ngIf="currentSetting === 'grants'">
|
<ng-container *ngIf="currentSetting === 'grants'">
|
||||||
<cnsl-card title="{{ 'GRANTS.USER.TITLE' | translate }}" description="{{ 'GRANTS.USER.DESCRIPTION' | translate }}">
|
<cnsl-card title="{{ 'GRANTS.USER.TITLE' | translate }}" description="{{ 'GRANTS.USER.DESCRIPTION' | translate }}">
|
||||||
<cnsl-user-grants
|
<cnsl-user-grants
|
||||||
[userId]="user.id"
|
[userId]="user.userId"
|
||||||
[context]="USERGRANTCONTEXT"
|
[context]="USERGRANTCONTEXT"
|
||||||
[displayedColumns]="[
|
[displayedColumns]="[
|
||||||
'org',
|
'org',
|
||||||
@ -165,7 +163,7 @@
|
|||||||
*ngIf="metadataQuery.state !== 'error'"
|
*ngIf="metadataQuery.state !== 'error'"
|
||||||
[metadata]="metadataQuery.value"
|
[metadata]="metadataQuery.value"
|
||||||
[description]="'DESCRIPTIONS.USERS.SELF.METADATA' | translate"
|
[description]="'DESCRIPTIONS.USERS.SELF.METADATA' | translate"
|
||||||
[disabled]="(['user.write:' + user.id, 'user.write'] | hasRole | async) === false"
|
[disabled]="(['user.write:' + user.userId, 'user.write'] | hasRole | async) === false"
|
||||||
(editClicked)="editMetadata(user, metadataQuery.value)"
|
(editClicked)="editMetadata(user, metadataQuery.value)"
|
||||||
(refresh)="refreshMetadata$.next(true)"
|
(refresh)="refreshMetadata$.next(true)"
|
||||||
[loading]="metadataQuery.state === 'loading'"
|
[loading]="metadataQuery.state === 'loading'"
|
||||||
|
@ -36,11 +36,10 @@ import { ToastService } from 'src/app/services/toast.service';
|
|||||||
import { formatPhone } from 'src/app/utils/formatPhone';
|
import { formatPhone } from 'src/app/utils/formatPhone';
|
||||||
import { EditDialogComponent, EditDialogData, EditDialogResult, EditDialogType } from './edit-dialog/edit-dialog.component';
|
import { EditDialogComponent, EditDialogData, EditDialogResult, EditDialogType } from './edit-dialog/edit-dialog.component';
|
||||||
import { LanguagesService } from 'src/app/services/languages.service';
|
import { LanguagesService } from 'src/app/services/languages.service';
|
||||||
import { Gender, HumanProfile } from '@zitadel/proto/zitadel/user/v2/user_pb';
|
import { Gender, HumanProfile, HumanUser, User, UserState } from '@zitadel/proto/zitadel/user/v2/user_pb';
|
||||||
import { catchError, filter, map, startWith, tap, withLatestFrom } from 'rxjs/operators';
|
import { catchError, filter, map, startWith, tap, withLatestFrom } from 'rxjs/operators';
|
||||||
import { pairwiseStartWith } from 'src/app/utils/pairwiseStartWith';
|
import { pairwiseStartWith } from 'src/app/utils/pairwiseStartWith';
|
||||||
import { NewAuthService } from 'src/app/services/new-auth.service';
|
import { NewAuthService } from 'src/app/services/new-auth.service';
|
||||||
import { Human, User, UserState } from '@zitadel/proto/zitadel/user_pb';
|
|
||||||
import { takeUntilDestroyed } from '@angular/core/rxjs-interop';
|
import { takeUntilDestroyed } from '@angular/core/rxjs-interop';
|
||||||
import { NewMgmtService } from 'src/app/services/new-mgmt.service';
|
import { NewMgmtService } from 'src/app/services/new-mgmt.service';
|
||||||
import { Metadata } from '@zitadel/proto/zitadel/metadata_pb';
|
import { Metadata } from '@zitadel/proto/zitadel/metadata_pb';
|
||||||
@ -48,18 +47,14 @@ import { UserService } from 'src/app/services/user.service';
|
|||||||
import { LoginPolicy } from '@zitadel/proto/zitadel/policy_pb';
|
import { LoginPolicy } from '@zitadel/proto/zitadel/policy_pb';
|
||||||
import { query } from '@angular/animations';
|
import { query } from '@angular/animations';
|
||||||
|
|
||||||
type UserQuery =
|
type UserQuery = { state: 'success'; value: User } | { state: 'error'; value: string } | { state: 'loading'; value?: User };
|
||||||
| { state: 'success'; value: User }
|
|
||||||
| { state: 'error'; value: string }
|
|
||||||
| { state: 'loading'; value?: User }
|
|
||||||
| { state: 'notfound' };
|
|
||||||
|
|
||||||
type MetadataQuery =
|
type MetadataQuery =
|
||||||
| { state: 'success'; value: Metadata[] }
|
| { state: 'success'; value: Metadata[] }
|
||||||
| { state: 'loading'; value: Metadata[] }
|
| { state: 'loading'; value: Metadata[] }
|
||||||
| { state: 'error'; value: string };
|
| { state: 'error'; value: string };
|
||||||
|
|
||||||
type UserWithHumanType = Omit<User, 'type'> & { type: { case: 'human'; value: Human } };
|
type UserWithHumanType = Omit<User, 'type'> & { type: { case: 'human'; value: HumanUser } };
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'cnsl-auth-user-detail',
|
selector: 'cnsl-auth-user-detail',
|
||||||
@ -67,17 +62,17 @@ type UserWithHumanType = Omit<User, 'type'> & { type: { case: 'human'; value: Hu
|
|||||||
styleUrls: ['./auth-user-detail.component.scss'],
|
styleUrls: ['./auth-user-detail.component.scss'],
|
||||||
})
|
})
|
||||||
export class AuthUserDetailComponent implements OnInit {
|
export class AuthUserDetailComponent implements OnInit {
|
||||||
public genders: Gender[] = [Gender.MALE, Gender.FEMALE, Gender.DIVERSE];
|
protected readonly genders: Gender[] = [Gender.MALE, Gender.FEMALE, Gender.DIVERSE];
|
||||||
|
|
||||||
public ChangeType: any = ChangeType;
|
protected readonly ChangeType = ChangeType;
|
||||||
public userLoginMustBeDomain: boolean = false;
|
public userLoginMustBeDomain: boolean = false;
|
||||||
public UserState: any = UserState;
|
protected readonly UserState = UserState;
|
||||||
|
|
||||||
public USERGRANTCONTEXT: UserGrantContext = UserGrantContext.AUTHUSER;
|
protected USERGRANTCONTEXT: UserGrantContext = UserGrantContext.AUTHUSER;
|
||||||
public refreshChanges$: EventEmitter<void> = new EventEmitter();
|
protected readonly refreshChanges$: EventEmitter<void> = new EventEmitter();
|
||||||
public refreshMetadata$ = new Subject<true>();
|
protected readonly refreshMetadata$ = new Subject<true>();
|
||||||
|
|
||||||
public settingsList: SidenavSetting[] = [
|
protected readonly settingsList: SidenavSetting[] = [
|
||||||
{ id: 'general', i18nKey: 'USER.SETTINGS.GENERAL' },
|
{ id: 'general', i18nKey: 'USER.SETTINGS.GENERAL' },
|
||||||
{ id: 'security', i18nKey: 'USER.SETTINGS.SECURITY' },
|
{ id: 'security', i18nKey: 'USER.SETTINGS.SECURITY' },
|
||||||
{ id: 'idp', i18nKey: 'USER.SETTINGS.IDP' },
|
{ id: 'idp', i18nKey: 'USER.SETTINGS.IDP' },
|
||||||
@ -92,9 +87,9 @@ export class AuthUserDetailComponent implements OnInit {
|
|||||||
protected readonly user$: Observable<UserQuery>;
|
protected readonly user$: Observable<UserQuery>;
|
||||||
protected readonly metadata$: Observable<MetadataQuery>;
|
protected readonly metadata$: Observable<MetadataQuery>;
|
||||||
private readonly savedLanguage$: Observable<string>;
|
private readonly savedLanguage$: Observable<string>;
|
||||||
protected currentSetting$: Observable<string | undefined>;
|
protected readonly currentSetting$: Observable<string | undefined>;
|
||||||
public loginPolicy$: Observable<LoginPolicy>;
|
protected readonly loginPolicy$: Observable<LoginPolicy>;
|
||||||
protected userName$: Observable<string>;
|
protected readonly userName$: Observable<string>;
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
public translate: TranslateService,
|
public translate: TranslateService,
|
||||||
@ -209,13 +204,8 @@ export class AuthUserDetailComponent implements OnInit {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private getMyUser(): Observable<UserQuery> {
|
private getMyUser(): Observable<UserQuery> {
|
||||||
return defer(() => this.newAuthService.getMyUser()).pipe(
|
return defer(() => this.userService.getMyUser()).pipe(
|
||||||
map(({ user }) => {
|
map((user) => ({ state: 'success' as const, value: user })),
|
||||||
if (user) {
|
|
||||||
return { state: 'success', value: user } as const;
|
|
||||||
}
|
|
||||||
return { state: 'notfound' } as const;
|
|
||||||
}),
|
|
||||||
catchError((error) => of({ state: 'error', value: error.message ?? '' } as const)),
|
catchError((error) => of({ state: 'error', value: error.message ?? '' } as const)),
|
||||||
startWith({ state: 'loading' } as const),
|
startWith({ state: 'loading' } as const),
|
||||||
);
|
);
|
||||||
@ -232,7 +222,7 @@ export class AuthUserDetailComponent implements OnInit {
|
|||||||
if (!user.value) {
|
if (!user.value) {
|
||||||
return EMPTY;
|
return EMPTY;
|
||||||
}
|
}
|
||||||
return this.getMetadataById(user.value.id);
|
return this.getMetadataById(user.value.userId);
|
||||||
}),
|
}),
|
||||||
pairwiseStartWith(undefined),
|
pairwiseStartWith(undefined),
|
||||||
map(([prev, curr]) => {
|
map(([prev, curr]) => {
|
||||||
@ -259,7 +249,7 @@ export class AuthUserDetailComponent implements OnInit {
|
|||||||
labelKey: 'ACTIONS.NEWVALUE' as const,
|
labelKey: 'ACTIONS.NEWVALUE' as const,
|
||||||
titleKey: 'USER.PROFILE.CHANGEUSERNAME_TITLE' as const,
|
titleKey: 'USER.PROFILE.CHANGEUSERNAME_TITLE' as const,
|
||||||
descriptionKey: 'USER.PROFILE.CHANGEUSERNAME_DESC' as const,
|
descriptionKey: 'USER.PROFILE.CHANGEUSERNAME_DESC' as const,
|
||||||
value: user.userName,
|
value: user.username,
|
||||||
};
|
};
|
||||||
const dialogRef = this.dialog.open<EditDialogComponent, typeof data, EditDialogResult>(EditDialogComponent, {
|
const dialogRef = this.dialog.open<EditDialogComponent, typeof data, EditDialogResult>(EditDialogComponent, {
|
||||||
data,
|
data,
|
||||||
@ -271,8 +261,8 @@ export class AuthUserDetailComponent implements OnInit {
|
|||||||
.pipe(
|
.pipe(
|
||||||
map((value) => value?.value),
|
map((value) => value?.value),
|
||||||
filter(Boolean),
|
filter(Boolean),
|
||||||
filter((value) => user.userName != value),
|
filter((value) => user.username != value),
|
||||||
switchMap((username) => this.userService.updateUser({ userId: user.id, username })),
|
switchMap((username) => this.userService.updateUser({ userId: user.userId, username })),
|
||||||
)
|
)
|
||||||
.subscribe({
|
.subscribe({
|
||||||
next: () => {
|
next: () => {
|
||||||
@ -288,7 +278,7 @@ export class AuthUserDetailComponent implements OnInit {
|
|||||||
public saveProfile(user: User, profile: HumanProfile): void {
|
public saveProfile(user: User, profile: HumanProfile): void {
|
||||||
this.userService
|
this.userService
|
||||||
.updateUser({
|
.updateUser({
|
||||||
userId: user.id,
|
userId: user.userId,
|
||||||
profile: {
|
profile: {
|
||||||
givenName: profile.givenName,
|
givenName: profile.givenName,
|
||||||
familyName: profile.familyName,
|
familyName: profile.familyName,
|
||||||
@ -350,7 +340,7 @@ export class AuthUserDetailComponent implements OnInit {
|
|||||||
|
|
||||||
public resendEmailVerification(user: User): void {
|
public resendEmailVerification(user: User): void {
|
||||||
this.newMgmtService
|
this.newMgmtService
|
||||||
.resendHumanEmailVerification(user.id)
|
.resendHumanEmailVerification(user.userId)
|
||||||
.then(() => {
|
.then(() => {
|
||||||
this.toast.showInfo('USER.TOAST.EMAILVERIFICATIONSENT', true);
|
this.toast.showInfo('USER.TOAST.EMAILVERIFICATIONSENT', true);
|
||||||
this.refreshChanges$.emit();
|
this.refreshChanges$.emit();
|
||||||
@ -362,7 +352,7 @@ export class AuthUserDetailComponent implements OnInit {
|
|||||||
|
|
||||||
public resendPhoneVerification(user: User): void {
|
public resendPhoneVerification(user: User): void {
|
||||||
this.newMgmtService
|
this.newMgmtService
|
||||||
.resendHumanPhoneVerification(user.id)
|
.resendHumanPhoneVerification(user.userId)
|
||||||
.then(() => {
|
.then(() => {
|
||||||
this.toast.showInfo('USER.TOAST.PHONEVERIFICATIONSENT', true);
|
this.toast.showInfo('USER.TOAST.PHONEVERIFICATIONSENT', true);
|
||||||
this.refreshChanges$.emit();
|
this.refreshChanges$.emit();
|
||||||
@ -374,7 +364,7 @@ export class AuthUserDetailComponent implements OnInit {
|
|||||||
|
|
||||||
public deletePhone(user: User): void {
|
public deletePhone(user: User): void {
|
||||||
this.userService
|
this.userService
|
||||||
.removePhone(user.id)
|
.removePhone(user.userId)
|
||||||
.then(() => {
|
.then(() => {
|
||||||
this.toast.showInfo('USER.TOAST.PHONEREMOVED', true);
|
this.toast.showInfo('USER.TOAST.PHONEREMOVED', true);
|
||||||
this.refreshChanges$.emit();
|
this.refreshChanges$.emit();
|
||||||
@ -417,7 +407,7 @@ export class AuthUserDetailComponent implements OnInit {
|
|||||||
filter((resp): resp is Required<EditDialogResult> => !!resp?.value),
|
filter((resp): resp is Required<EditDialogResult> => !!resp?.value),
|
||||||
switchMap(({ value, isVerified }) =>
|
switchMap(({ value, isVerified }) =>
|
||||||
this.userService.setEmail({
|
this.userService.setEmail({
|
||||||
userId: user.id,
|
userId: user.userId,
|
||||||
email: value,
|
email: value,
|
||||||
verification: isVerified ? { case: 'isVerified', value: isVerified } : { case: undefined },
|
verification: isVerified ? { case: 'isVerified', value: isVerified } : { case: undefined },
|
||||||
}),
|
}),
|
||||||
@ -453,7 +443,7 @@ export class AuthUserDetailComponent implements OnInit {
|
|||||||
.pipe(
|
.pipe(
|
||||||
map((resp) => formatPhone(resp?.value)),
|
map((resp) => formatPhone(resp?.value)),
|
||||||
filter(Boolean),
|
filter(Boolean),
|
||||||
switchMap(({ phone }) => this.userService.setPhone({ userId: user.id, phone })),
|
switchMap(({ phone }) => this.userService.setPhone({ userId: user.userId, phone })),
|
||||||
)
|
)
|
||||||
.subscribe({
|
.subscribe({
|
||||||
next: () => {
|
next: () => {
|
||||||
@ -482,7 +472,7 @@ export class AuthUserDetailComponent implements OnInit {
|
|||||||
.afterClosed()
|
.afterClosed()
|
||||||
.pipe(
|
.pipe(
|
||||||
filter(Boolean),
|
filter(Boolean),
|
||||||
switchMap(() => this.userService.deleteUser(user.id)),
|
switchMap(() => this.userService.deleteUser(user.userId)),
|
||||||
)
|
)
|
||||||
.subscribe({
|
.subscribe({
|
||||||
next: () => {
|
next: () => {
|
||||||
@ -498,9 +488,9 @@ export class AuthUserDetailComponent implements OnInit {
|
|||||||
this.newMgmtService.setUserMetadata({
|
this.newMgmtService.setUserMetadata({
|
||||||
key,
|
key,
|
||||||
value: Buffer.from(value),
|
value: Buffer.from(value),
|
||||||
id: user.id,
|
id: user.userId,
|
||||||
});
|
});
|
||||||
const removeFcn = (key: string): Promise<any> => this.newMgmtService.removeUserMetadata({ key, id: user.id });
|
const removeFcn = (key: string): Promise<any> => this.newMgmtService.removeUserMetadata({ key, id: user.userId });
|
||||||
|
|
||||||
const dialogRef = this.dialog.open<MetadataDialogComponent, MetadataDialogData>(MetadataDialogComponent, {
|
const dialogRef = this.dialog.open<MetadataDialogComponent, MetadataDialogData>(MetadataDialogComponent, {
|
||||||
data: {
|
data: {
|
||||||
|
@ -2,7 +2,7 @@
|
|||||||
<div class="content">
|
<div class="content">
|
||||||
<cnsl-form-field class="formfield">
|
<cnsl-form-field class="formfield">
|
||||||
<cnsl-label>{{ 'USER.MACHINE.USERNAME' | translate }}</cnsl-label>
|
<cnsl-label>{{ 'USER.MACHINE.USERNAME' | translate }}</cnsl-label>
|
||||||
<input cnslInput formControlName="userName" required />
|
<input cnslInput formControlName="username" required />
|
||||||
</cnsl-form-field>
|
</cnsl-form-field>
|
||||||
<cnsl-form-field class="formfield">
|
<cnsl-form-field class="formfield">
|
||||||
<cnsl-label>{{ 'USER.MACHINE.NAME' | translate }}</cnsl-label>
|
<cnsl-label>{{ 'USER.MACHINE.NAME' | translate }}</cnsl-label>
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
<div class="max-width-container">
|
<div class="max-width-container">
|
||||||
<div class="enlarged-container" [ngSwitch]="type">
|
<div class="enlarged-container">
|
||||||
<div class="users-title-row">
|
<div class="users-title-row">
|
||||||
<h1>{{ 'DESCRIPTIONS.USERS.TITLE' | translate }}</h1>
|
<h1>{{ 'DESCRIPTIONS.USERS.TITLE' | translate }}</h1>
|
||||||
<a mat-icon-button href="https://zitadel.com/docs/concepts/structure/users" rel="noreferrer" target="_blank">
|
<a mat-icon-button href="https://zitadel.com/docs/concepts/structure/users" rel="noreferrer" target="_blank">
|
||||||
@ -7,21 +7,6 @@
|
|||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
<p class="user-list-sub cnsl-secondary-text">{{ 'DESCRIPTIONS.USERS.DESCRIPTION' | translate }}</p>
|
<p class="user-list-sub cnsl-secondary-text">{{ 'DESCRIPTIONS.USERS.DESCRIPTION' | translate }}</p>
|
||||||
<ng-container *ngSwitchCase="Type.TYPE_HUMAN">
|
<cnsl-user-table [canWrite$]="['user.write$'] | hasRole" [canDelete$]="['user.delete$'] | hasRole"> </cnsl-user-table>
|
||||||
<cnsl-user-table
|
|
||||||
[type]="Type.TYPE_HUMAN"
|
|
||||||
[canWrite$]="['user.write$'] | hasRole"
|
|
||||||
[canDelete$]="['user.delete$'] | hasRole"
|
|
||||||
>
|
|
||||||
</cnsl-user-table>
|
|
||||||
</ng-container>
|
|
||||||
<ng-container *ngSwitchCase="Type.TYPE_MACHINE">
|
|
||||||
<cnsl-user-table
|
|
||||||
[type]="Type.TYPE_MACHINE"
|
|
||||||
[canWrite$]="['user.write$'] | hasRole"
|
|
||||||
[canDelete$]="['user.delete$'] | hasRole"
|
|
||||||
>
|
|
||||||
</cnsl-user-table>
|
|
||||||
</ng-container>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
@ -1,8 +1,5 @@
|
|||||||
import { Component } from '@angular/core';
|
import { Component } from '@angular/core';
|
||||||
import { ActivatedRoute, Params } from '@angular/router';
|
|
||||||
import { TranslateService } from '@ngx-translate/core';
|
import { TranslateService } from '@ngx-translate/core';
|
||||||
import { take } from 'rxjs/operators';
|
|
||||||
import { Type } from 'src/app/proto/generated/zitadel/user_pb';
|
|
||||||
import { Breadcrumb, BreadcrumbService, BreadcrumbType } from 'src/app/services/breadcrumb.service';
|
import { Breadcrumb, BreadcrumbService, BreadcrumbType } from 'src/app/services/breadcrumb.service';
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
@ -11,23 +8,10 @@ import { Breadcrumb, BreadcrumbService, BreadcrumbType } from 'src/app/services/
|
|||||||
styleUrls: ['./user-list.component.scss'],
|
styleUrls: ['./user-list.component.scss'],
|
||||||
})
|
})
|
||||||
export class UserListComponent {
|
export class UserListComponent {
|
||||||
public Type: any = Type;
|
|
||||||
public type: Type = Type.TYPE_HUMAN;
|
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
public translate: TranslateService,
|
protected readonly translate: TranslateService,
|
||||||
activatedRoute: ActivatedRoute,
|
|
||||||
breadcrumbService: BreadcrumbService,
|
breadcrumbService: BreadcrumbService,
|
||||||
) {
|
) {
|
||||||
activatedRoute.queryParams.pipe(take(1)).subscribe((params: Params) => {
|
|
||||||
const { type } = params;
|
|
||||||
if (type && type === 'human') {
|
|
||||||
this.type = Type.TYPE_HUMAN;
|
|
||||||
} else if (type && type === 'machine') {
|
|
||||||
this.type = Type.TYPE_MACHINE;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
const bread: Breadcrumb = {
|
const bread: Breadcrumb = {
|
||||||
type: BreadcrumbType.ORG,
|
type: BreadcrumbType.ORG,
|
||||||
routerLink: ['/org'],
|
routerLink: ['/org'],
|
||||||
|
@ -1,11 +1,11 @@
|
|||||||
<cnsl-refresh-table
|
<cnsl-refresh-table
|
||||||
[loading]="loading$ | async"
|
*ngIf="type$ | async as type"
|
||||||
(refreshed)="refreshPage()"
|
[loading]="loading()"
|
||||||
[dataSize]="totalResult"
|
(refreshed)="this.refresh$.next(true)"
|
||||||
|
[dataSize]="dataSize()"
|
||||||
[hideRefresh]="true"
|
[hideRefresh]="true"
|
||||||
[timestamp]="viewTimestamp"
|
[timestamp]="(users$ | async)?.details?.timestamp"
|
||||||
[selection]="selection"
|
[selection]="selection"
|
||||||
[emitRefreshOnPreviousRoutes]="refreshOnPreviousRoutes"
|
|
||||||
[showBorder]="true"
|
[showBorder]="true"
|
||||||
>
|
>
|
||||||
<div leftActions class="user-toggle-group">
|
<div leftActions class="user-toggle-group">
|
||||||
@ -60,12 +60,12 @@
|
|||||||
<cnsl-filter-user
|
<cnsl-filter-user
|
||||||
actions
|
actions
|
||||||
*ngIf="!selection.hasValue()"
|
*ngIf="!selection.hasValue()"
|
||||||
(filterChanged)="applySearchQuery($any($event))"
|
(filterChanged)="this.searchQueries$.next($any($event))"
|
||||||
(filterOpen)="filterOpen = $event"
|
(filterOpen)="filterOpen = $event"
|
||||||
></cnsl-filter-user>
|
></cnsl-filter-user>
|
||||||
<ng-template cnslHasRole [hasRole]="['user.write']" actions>
|
<ng-template cnslHasRole [hasRole]="['user.write']" actions>
|
||||||
<button
|
<button
|
||||||
(click)="gotoRouterLink(['/users', type === Type.HUMAN ? 'create' : 'create-machine'])"
|
(click)="router.navigate(['/users', type === Type.HUMAN ? 'create' : 'create-machine'])"
|
||||||
color="primary"
|
color="primary"
|
||||||
mat-raised-button
|
mat-raised-button
|
||||||
[disabled]="(canWrite$ | async) === false"
|
[disabled]="(canWrite$ | async) === false"
|
||||||
@ -77,7 +77,7 @@
|
|||||||
<span>{{ 'ACTIONS.NEW' | translate }}</span>
|
<span>{{ 'ACTIONS.NEW' | translate }}</span>
|
||||||
<cnsl-action-keys
|
<cnsl-action-keys
|
||||||
*ngIf="!filterOpen"
|
*ngIf="!filterOpen"
|
||||||
(actionTriggered)="gotoRouterLink(['/users', type === Type.HUMAN ? 'create' : 'create-machine'])"
|
(actionTriggered)="router.navigate(['/users', type === Type.HUMAN ? 'create' : 'create-machine'])"
|
||||||
>
|
>
|
||||||
</cnsl-action-keys>
|
</cnsl-action-keys>
|
||||||
</div>
|
</div>
|
||||||
@ -85,7 +85,7 @@
|
|||||||
</ng-template>
|
</ng-template>
|
||||||
|
|
||||||
<div class="table-wrapper">
|
<div class="table-wrapper">
|
||||||
<table class="table" mat-table [dataSource]="dataSource" matSort (matSortChange)="sortChange($event)">
|
<table class="table" mat-table [dataSource]="dataSource" matSort>
|
||||||
<ng-container matColumnDef="select">
|
<ng-container matColumnDef="select">
|
||||||
<th mat-header-cell *matHeaderCellDef>
|
<th mat-header-cell *matHeaderCellDef>
|
||||||
<div class="selection">
|
<div class="selection">
|
||||||
@ -133,12 +133,7 @@
|
|||||||
</ng-container>
|
</ng-container>
|
||||||
|
|
||||||
<ng-container matColumnDef="displayName">
|
<ng-container matColumnDef="displayName">
|
||||||
<th
|
<th mat-header-cell *matHeaderCellDef mat-sort-header>
|
||||||
mat-header-cell
|
|
||||||
*matHeaderCellDef
|
|
||||||
mat-sort-header
|
|
||||||
[ngClass]="{ 'search-active': this.userSearchKey === UserListSearchKey.DISPLAY_NAME }"
|
|
||||||
>
|
|
||||||
{{ 'USER.PROFILE.DISPLAYNAME' | translate }}
|
{{ 'USER.PROFILE.DISPLAYNAME' | translate }}
|
||||||
</th>
|
</th>
|
||||||
<td mat-cell *matCellDef="let user" [routerLink]="user.userId ? ['/users', user.userId] : null">
|
<td mat-cell *matCellDef="let user" [routerLink]="user.userId ? ['/users', user.userId] : null">
|
||||||
@ -148,12 +143,7 @@
|
|||||||
</ng-container>
|
</ng-container>
|
||||||
|
|
||||||
<ng-container matColumnDef="preferredLoginName">
|
<ng-container matColumnDef="preferredLoginName">
|
||||||
<th
|
<th mat-header-cell *matHeaderCellDef mat-sort-header>
|
||||||
mat-header-cell
|
|
||||||
*matHeaderCellDef
|
|
||||||
mat-sort-header
|
|
||||||
[ngClass]="{ 'search-active': this.userSearchKey === UserListSearchKey.DISPLAY_NAME }"
|
|
||||||
>
|
|
||||||
{{ 'USER.PROFILE.PREFERREDLOGINNAME' | translate }}
|
{{ 'USER.PROFILE.PREFERREDLOGINNAME' | translate }}
|
||||||
</th>
|
</th>
|
||||||
<td mat-cell *matCellDef="let user" [routerLink]="user.userId ? ['/users', user.userId] : null">
|
<td mat-cell *matCellDef="let user" [routerLink]="user.userId ? ['/users', user.userId] : null">
|
||||||
@ -162,12 +152,7 @@
|
|||||||
</ng-container>
|
</ng-container>
|
||||||
|
|
||||||
<ng-container matColumnDef="username">
|
<ng-container matColumnDef="username">
|
||||||
<th
|
<th mat-header-cell *matHeaderCellDef mat-sort-header>
|
||||||
mat-header-cell
|
|
||||||
*matHeaderCellDef
|
|
||||||
mat-sort-header
|
|
||||||
[ngClass]="{ 'search-active': this.userSearchKey === UserListSearchKey.USER_NAME }"
|
|
||||||
>
|
|
||||||
{{ 'USER.PROFILE.USERNAME' | translate }}
|
{{ 'USER.PROFILE.USERNAME' | translate }}
|
||||||
</th>
|
</th>
|
||||||
<td mat-cell *matCellDef="let user" [routerLink]="user.userId ? ['/users', user.userId] : null">
|
<td mat-cell *matCellDef="let user" [routerLink]="user.userId ? ['/users', user.userId] : null">
|
||||||
@ -176,12 +161,7 @@
|
|||||||
</ng-container>
|
</ng-container>
|
||||||
|
|
||||||
<ng-container matColumnDef="email">
|
<ng-container matColumnDef="email">
|
||||||
<th
|
<th mat-header-cell *matHeaderCellDef mat-sort-header>
|
||||||
mat-header-cell
|
|
||||||
*matHeaderCellDef
|
|
||||||
mat-sort-header
|
|
||||||
[ngClass]="{ 'search-active': this.UserListSearchKey === UserListSearchKey.EMAIL }"
|
|
||||||
>
|
|
||||||
{{ 'USER.EMAIL' | translate }}
|
{{ 'USER.EMAIL' | translate }}
|
||||||
</th>
|
</th>
|
||||||
<td mat-cell *matCellDef="let user" [routerLink]="user.userId ? ['/users', user.userId] : null">
|
<td mat-cell *matCellDef="let user" [routerLink]="user.userId ? ['/users', user.userId] : null">
|
||||||
@ -250,17 +230,16 @@
|
|||||||
</table>
|
</table>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div *ngIf="(loading$ | async) === false && !dataSource?.data?.length" class="no-content-row">
|
<div *ngIf="!loading() && !dataSource?.data?.length" class="no-content-row">
|
||||||
<i class="las la-exclamation"></i>
|
<i class="las la-exclamation"></i>
|
||||||
<span>{{ 'USER.TABLE.EMPTY' | translate }}</span>
|
<span>{{ 'USER.TABLE.EMPTY' | translate }}</span>
|
||||||
</div>
|
</div>
|
||||||
<cnsl-paginator
|
<cnsl-paginator
|
||||||
#paginator
|
|
||||||
class="paginator"
|
class="paginator"
|
||||||
[length]="totalResult || 0"
|
[length]="dataSize()"
|
||||||
[pageSize]="INITIAL_PAGE_SIZE"
|
[pageSize]="INITIAL_PAGE_SIZE"
|
||||||
[timestamp]="viewTimestamp"
|
[timestamp]="(users$ | async)?.details?.timestamp"
|
||||||
[pageSizeOptions]="[10, 20, 50, 100]"
|
[pageSizeOptions]="[10, 20, 50, 100]"
|
||||||
(page)="changePage($event)"
|
|
||||||
></cnsl-paginator>
|
></cnsl-paginator>
|
||||||
|
<!-- (page)="changePage($event)"-->
|
||||||
</cnsl-refresh-table>
|
</cnsl-refresh-table>
|
||||||
|
@ -1,34 +1,45 @@
|
|||||||
import { LiveAnnouncer } from '@angular/cdk/a11y';
|
|
||||||
import { SelectionModel } from '@angular/cdk/collections';
|
import { SelectionModel } from '@angular/cdk/collections';
|
||||||
import { Component, EventEmitter, Input, OnInit, Output, Signal, ViewChild } from '@angular/core';
|
import { Component, DestroyRef, EventEmitter, Input, OnInit, Output, signal, Signal, ViewChild } from '@angular/core';
|
||||||
import { MatDialog } from '@angular/material/dialog';
|
import { MatDialog } from '@angular/material/dialog';
|
||||||
import { MatSort, Sort } from '@angular/material/sort';
|
import { MatSort, SortDirection } from '@angular/material/sort';
|
||||||
import { MatTableDataSource } from '@angular/material/table';
|
import { MatTableDataSource } from '@angular/material/table';
|
||||||
import { ActivatedRoute, Router } from '@angular/router';
|
import { ActivatedRoute, Router } from '@angular/router';
|
||||||
import { TranslateService } from '@ngx-translate/core';
|
import { TranslateService } from '@ngx-translate/core';
|
||||||
import { BehaviorSubject, Observable, of } from 'rxjs';
|
import {
|
||||||
import { take } from 'rxjs/operators';
|
combineLatestWith,
|
||||||
|
defer,
|
||||||
|
delay,
|
||||||
|
distinctUntilChanged,
|
||||||
|
EMPTY,
|
||||||
|
from,
|
||||||
|
Observable,
|
||||||
|
of,
|
||||||
|
ReplaySubject,
|
||||||
|
shareReplay,
|
||||||
|
switchMap,
|
||||||
|
toArray,
|
||||||
|
} from 'rxjs';
|
||||||
|
import { catchError, filter, finalize, map, startWith, take } from 'rxjs/operators';
|
||||||
import { enterAnimations } from 'src/app/animations';
|
import { enterAnimations } from 'src/app/animations';
|
||||||
import { ActionKeysType } from 'src/app/modules/action-keys/action-keys.component';
|
import { ActionKeysType } from 'src/app/modules/action-keys/action-keys.component';
|
||||||
import { PageEvent, PaginatorComponent } from 'src/app/modules/paginator/paginator.component';
|
import { PaginatorComponent } from 'src/app/modules/paginator/paginator.component';
|
||||||
import { WarnDialogComponent } from 'src/app/modules/warn-dialog/warn-dialog.component';
|
import { WarnDialogComponent } from 'src/app/modules/warn-dialog/warn-dialog.component';
|
||||||
import { GrpcAuthService } from 'src/app/services/grpc-auth.service';
|
|
||||||
import { ToastService } from 'src/app/services/toast.service';
|
import { ToastService } from 'src/app/services/toast.service';
|
||||||
import { UserService } from 'src/app/services/user.service';
|
import { UserService } from 'src/app/services/user.service';
|
||||||
import { toSignal } from '@angular/core/rxjs-interop';
|
import { takeUntilDestroyed, toSignal } from '@angular/core/rxjs-interop';
|
||||||
import { User } from 'src/app/proto/generated/zitadel/user_pb';
|
import { SearchQuery as UserSearchQuery } from 'src/app/proto/generated/zitadel/user_pb';
|
||||||
import { SearchQuery, SearchQuerySchema, Type, UserFieldName } from '@zitadel/proto/zitadel/user/v2/query_pb';
|
import { Type, UserFieldName } from '@zitadel/proto/zitadel/user/v2/query_pb';
|
||||||
import { UserState, User as UserV2 } from '@zitadel/proto/zitadel/user/v2/user_pb';
|
import { UserState, User } from '@zitadel/proto/zitadel/user/v2/user_pb';
|
||||||
import { create } from '@bufbuild/protobuf';
|
import { MessageInitShape } from '@bufbuild/protobuf';
|
||||||
import { Timestamp } from '@bufbuild/protobuf/wkt';
|
import { ListUsersRequestSchema, ListUsersResponse } from '@zitadel/proto/zitadel/user/v2/user_service_pb';
|
||||||
|
import { AuthenticationService } from 'src/app/services/authentication.service';
|
||||||
|
import { GrpcAuthService } from 'src/app/services/grpc-auth.service';
|
||||||
|
import { UserState as UserStateV1 } from 'src/app/proto/generated/zitadel/user_pb';
|
||||||
|
|
||||||
enum UserListSearchKey {
|
type Query = Exclude<
|
||||||
FIRST_NAME,
|
Exclude<MessageInitShape<typeof ListUsersRequestSchema>['queries'], undefined>[number]['query'],
|
||||||
LAST_NAME,
|
undefined
|
||||||
DISPLAY_NAME,
|
>;
|
||||||
USER_NAME,
|
|
||||||
EMAIL,
|
|
||||||
}
|
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'cnsl-user-table',
|
selector: 'cnsl-user-table',
|
||||||
@ -37,25 +48,33 @@ enum UserListSearchKey {
|
|||||||
animations: [enterAnimations],
|
animations: [enterAnimations],
|
||||||
})
|
})
|
||||||
export class UserTableComponent implements OnInit {
|
export class UserTableComponent implements OnInit {
|
||||||
public userSearchKey: UserListSearchKey | undefined = undefined;
|
protected readonly Type = Type;
|
||||||
public Type = Type;
|
protected readonly refresh$ = new ReplaySubject<true>(1);
|
||||||
@Input() public type: Type = Type.HUMAN;
|
|
||||||
@Input() refreshOnPreviousRoutes: string[] = [];
|
|
||||||
@Input() public canWrite$: Observable<boolean> = of(false);
|
@Input() public canWrite$: Observable<boolean> = of(false);
|
||||||
@Input() public canDelete$: Observable<boolean> = of(false);
|
@Input() public canDelete$: Observable<boolean> = of(false);
|
||||||
|
|
||||||
private user: Signal<User.AsObject | undefined> = toSignal(this.authService.user, { requireSync: true });
|
protected readonly dataSize: Signal<number>;
|
||||||
|
protected readonly loading = signal(false);
|
||||||
|
|
||||||
@ViewChild(PaginatorComponent) public paginator!: PaginatorComponent;
|
private readonly paginator$ = new ReplaySubject<PaginatorComponent>(1);
|
||||||
@ViewChild(MatSort) public sort!: MatSort;
|
@ViewChild(PaginatorComponent) public set paginator(paginator: PaginatorComponent) {
|
||||||
public INITIAL_PAGE_SIZE: number = 20;
|
this.paginator$.next(paginator);
|
||||||
|
}
|
||||||
|
private readonly sort$ = new ReplaySubject<MatSort>(1);
|
||||||
|
@ViewChild(MatSort) public set sort(sort: MatSort) {
|
||||||
|
this.sort$.next(sort);
|
||||||
|
}
|
||||||
|
|
||||||
|
protected readonly INITIAL_PAGE_SIZE = 20;
|
||||||
|
|
||||||
|
protected readonly dataSource: MatTableDataSource<User> = new MatTableDataSource<User>();
|
||||||
|
protected readonly selection: SelectionModel<User> = new SelectionModel<User>(true, []);
|
||||||
|
protected readonly users$: Observable<ListUsersResponse>;
|
||||||
|
protected readonly type$: Observable<Type>;
|
||||||
|
protected readonly searchQueries$ = new ReplaySubject<UserSearchQuery[]>(1);
|
||||||
|
protected readonly myUser: Signal<User | undefined>;
|
||||||
|
|
||||||
public viewTimestamp!: Timestamp;
|
|
||||||
public totalResult: number = 0;
|
|
||||||
public dataSource: MatTableDataSource<UserV2> = new MatTableDataSource<UserV2>();
|
|
||||||
public selection: SelectionModel<UserV2> = new SelectionModel<UserV2>(true, []);
|
|
||||||
private loadingSubject: BehaviorSubject<boolean> = new BehaviorSubject<boolean>(false);
|
|
||||||
public loading$: Observable<boolean> = this.loadingSubject.asObservable();
|
|
||||||
@Input() public displayedColumnsHuman: string[] = [
|
@Input() public displayedColumnsHuman: string[] = [
|
||||||
'select',
|
'select',
|
||||||
'displayName',
|
'displayName',
|
||||||
@ -76,46 +95,56 @@ export class UserTableComponent implements OnInit {
|
|||||||
'actions',
|
'actions',
|
||||||
];
|
];
|
||||||
|
|
||||||
@Output() public changedSelection: EventEmitter<Array<UserV2>> = new EventEmitter();
|
@Output() public changedSelection: EventEmitter<Array<User>> = new EventEmitter();
|
||||||
|
|
||||||
public UserState: any = UserState;
|
protected readonly UserState = UserState;
|
||||||
public UserListSearchKey: any = UserListSearchKey;
|
|
||||||
|
|
||||||
public ActionKeysType: any = ActionKeysType;
|
protected ActionKeysType = ActionKeysType;
|
||||||
public filterOpen: boolean = false;
|
protected filterOpen: boolean = false;
|
||||||
|
|
||||||
private searchQueries: SearchQuery[] = [];
|
|
||||||
constructor(
|
constructor(
|
||||||
private router: Router,
|
protected readonly router: Router,
|
||||||
public translate: TranslateService,
|
public readonly translate: TranslateService,
|
||||||
private authService: GrpcAuthService,
|
private readonly userService: UserService,
|
||||||
private userService: UserService,
|
private readonly toast: ToastService,
|
||||||
private toast: ToastService,
|
private readonly dialog: MatDialog,
|
||||||
private dialog: MatDialog,
|
private readonly route: ActivatedRoute,
|
||||||
private route: ActivatedRoute,
|
private readonly destroyRef: DestroyRef,
|
||||||
private _liveAnnouncer: LiveAnnouncer,
|
private readonly authenticationService: AuthenticationService,
|
||||||
|
private readonly authService: GrpcAuthService,
|
||||||
) {
|
) {
|
||||||
this.selection.changed.subscribe(() => {
|
this.type$ = this.getType$().pipe(shareReplay({ refCount: true, bufferSize: 1 }));
|
||||||
this.changedSelection.emit(this.selection.selected);
|
this.users$ = this.getUsers(this.type$).pipe(shareReplay({ refCount: true, bufferSize: 1 }));
|
||||||
});
|
this.myUser = toSignal(this.getMyUser());
|
||||||
|
|
||||||
|
this.dataSize = toSignal(
|
||||||
|
this.users$.pipe(
|
||||||
|
map((users) => users.result.length),
|
||||||
|
distinctUntilChanged(),
|
||||||
|
),
|
||||||
|
{ initialValue: 0 },
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
ngOnInit(): void {
|
ngOnInit(): void {
|
||||||
this.route.queryParams.pipe(take(1)).subscribe((params) => {
|
this.selection.changed.pipe(takeUntilDestroyed(this.destroyRef)).subscribe(() => {
|
||||||
if (!params['filter']) {
|
this.changedSelection.emit(this.selection.selected);
|
||||||
this.getData(this.INITIAL_PAGE_SIZE, 0, this.type, this.searchQueries).then();
|
|
||||||
}
|
|
||||||
|
|
||||||
if (params['deferredReload']) {
|
|
||||||
setTimeout(() => {
|
|
||||||
this.getData(this.paginator.pageSize, this.paginator.pageIndex * this.paginator.pageSize, this.type).then();
|
|
||||||
}, 2000);
|
|
||||||
}
|
|
||||||
});
|
});
|
||||||
|
|
||||||
|
this.users$.pipe(takeUntilDestroyed(this.destroyRef)).subscribe((users) => (this.dataSource.data = users.result));
|
||||||
|
|
||||||
|
this.route.queryParamMap
|
||||||
|
.pipe(
|
||||||
|
map((params) => params.get('deferredReload')),
|
||||||
|
filter(Boolean),
|
||||||
|
take(1),
|
||||||
|
delay(2000),
|
||||||
|
takeUntilDestroyed(this.destroyRef),
|
||||||
|
)
|
||||||
|
.subscribe(() => this.refresh$.next(true));
|
||||||
}
|
}
|
||||||
|
|
||||||
public setType(type: Type): void {
|
setType(type: Type) {
|
||||||
this.type = type;
|
|
||||||
this.router
|
this.router
|
||||||
.navigate([], {
|
.navigate([], {
|
||||||
relativeTo: this.route,
|
relativeTo: this.route,
|
||||||
@ -127,12 +156,195 @@ export class UserTableComponent implements OnInit {
|
|||||||
skipLocationChange: false,
|
skipLocationChange: false,
|
||||||
})
|
})
|
||||||
.then();
|
.then();
|
||||||
this.getData(
|
}
|
||||||
this.paginator.pageSize,
|
|
||||||
this.paginator.pageIndex * this.paginator.pageSize,
|
private getMyUser() {
|
||||||
this.type,
|
return defer(() => this.userService.getMyUser()).pipe(
|
||||||
this.searchQueries,
|
catchError((error) => {
|
||||||
).then();
|
this.toast.showError(error);
|
||||||
|
return EMPTY;
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
private getType$(): Observable<Type> {
|
||||||
|
return this.route.queryParamMap.pipe(
|
||||||
|
map((params) => params.get('type')),
|
||||||
|
filter(Boolean),
|
||||||
|
map((type) => (type === 'machine' ? Type.MACHINE : Type.HUMAN)),
|
||||||
|
startWith(Type.HUMAN),
|
||||||
|
distinctUntilChanged(),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
private getDirection$() {
|
||||||
|
return this.sort$.pipe(
|
||||||
|
switchMap((sort) =>
|
||||||
|
sort.sortChange.pipe(
|
||||||
|
map(({ direction }) => direction),
|
||||||
|
startWith(sort.direction),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
distinctUntilChanged(),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
private getSortingColumn$() {
|
||||||
|
return this.sort$.pipe(
|
||||||
|
switchMap((sort) =>
|
||||||
|
sort.sortChange.pipe(
|
||||||
|
map(({ active }) => active),
|
||||||
|
startWith(sort.active),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
map((active) => {
|
||||||
|
switch (active) {
|
||||||
|
case 'displayName':
|
||||||
|
return UserFieldName.DISPLAY_NAME;
|
||||||
|
case 'username':
|
||||||
|
return UserFieldName.USER_NAME;
|
||||||
|
case 'preferredLoginName':
|
||||||
|
// TODO: replace with preferred username sorting once implemented
|
||||||
|
return UserFieldName.USER_NAME;
|
||||||
|
case 'email':
|
||||||
|
return UserFieldName.EMAIL;
|
||||||
|
case 'state':
|
||||||
|
return UserFieldName.STATE;
|
||||||
|
case 'creationDate':
|
||||||
|
return UserFieldName.CREATION_DATE;
|
||||||
|
default:
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
distinctUntilChanged(),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
private getQueries(type$: Observable<Type>): Observable<Query[]> {
|
||||||
|
const activeOrgId$ = this.getActiveOrgId();
|
||||||
|
|
||||||
|
return this.searchQueries$.pipe(
|
||||||
|
startWith([]),
|
||||||
|
combineLatestWith(type$, activeOrgId$),
|
||||||
|
switchMap(([queries, type, organizationId]) =>
|
||||||
|
from(queries).pipe(
|
||||||
|
map((query) => this.searchQueryToV2(query.toObject())),
|
||||||
|
startWith({ case: 'typeQuery' as const, value: { type } }),
|
||||||
|
startWith(organizationId ? { case: 'organizationIdQuery' as const, value: { organizationId } } : undefined),
|
||||||
|
filter(Boolean),
|
||||||
|
toArray(),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
private searchQueryToV2(query: UserSearchQuery.AsObject): Query | undefined {
|
||||||
|
if (query.userNameQuery) {
|
||||||
|
return {
|
||||||
|
case: 'userNameQuery' as const,
|
||||||
|
value: {
|
||||||
|
userName: query.userNameQuery.userName,
|
||||||
|
method: query.userNameQuery.method as unknown as any,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
} else if (query.displayNameQuery) {
|
||||||
|
return {
|
||||||
|
case: 'displayNameQuery' as const,
|
||||||
|
value: {
|
||||||
|
displayName: query.displayNameQuery.displayName,
|
||||||
|
method: query.displayNameQuery.method as unknown as any,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
} else if (query.emailQuery) {
|
||||||
|
return {
|
||||||
|
case: 'emailQuery' as const,
|
||||||
|
value: {
|
||||||
|
emailAddress: query.emailQuery.emailAddress,
|
||||||
|
method: query.emailQuery.method as unknown as any,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
} else if (query.stateQuery) {
|
||||||
|
return {
|
||||||
|
case: 'stateQuery' as const,
|
||||||
|
value: {
|
||||||
|
state: this.toV2State(query.stateQuery.state),
|
||||||
|
},
|
||||||
|
};
|
||||||
|
} else {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private toV2State(state: UserStateV1) {
|
||||||
|
switch (state) {
|
||||||
|
case UserStateV1.USER_STATE_ACTIVE:
|
||||||
|
return UserState.ACTIVE;
|
||||||
|
case UserStateV1.USER_STATE_INACTIVE:
|
||||||
|
return UserState.INACTIVE;
|
||||||
|
case UserStateV1.USER_STATE_DELETED:
|
||||||
|
return UserState.DELETED;
|
||||||
|
case UserStateV1.USER_STATE_LOCKED:
|
||||||
|
return UserState.LOCKED;
|
||||||
|
case UserStateV1.USER_STATE_INITIAL:
|
||||||
|
return UserState.INITIAL;
|
||||||
|
default:
|
||||||
|
throw new Error(`Invalid UserState ${state}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private getUsers(type$: Observable<Type>) {
|
||||||
|
const queries$ = this.getQueries(type$);
|
||||||
|
const direction$ = this.getDirection$();
|
||||||
|
const sortingColumn$ = this.getSortingColumn$();
|
||||||
|
|
||||||
|
const page$ = this.paginator$.pipe(switchMap((paginator) => paginator.page));
|
||||||
|
const pageSize$ = page$.pipe(
|
||||||
|
map(({ pageSize }) => pageSize),
|
||||||
|
startWith(this.INITIAL_PAGE_SIZE),
|
||||||
|
distinctUntilChanged(),
|
||||||
|
);
|
||||||
|
const pageIndex$ = page$.pipe(
|
||||||
|
map(({ pageIndex }) => pageIndex),
|
||||||
|
startWith(0),
|
||||||
|
distinctUntilChanged(),
|
||||||
|
);
|
||||||
|
|
||||||
|
return this.refresh$.pipe(
|
||||||
|
startWith(true),
|
||||||
|
combineLatestWith(queries$, direction$, sortingColumn$, pageSize$, pageIndex$),
|
||||||
|
switchMap(([_, queries, direction, sortingColumn, pageSize, pageIndex]) => {
|
||||||
|
return this.fetchUsers(queries, direction, sortingColumn, pageSize, pageIndex);
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
private fetchUsers(
|
||||||
|
queries: Query[],
|
||||||
|
direction: SortDirection,
|
||||||
|
sortingColumn: UserFieldName | undefined,
|
||||||
|
pageSize: number,
|
||||||
|
pageIndex: number,
|
||||||
|
) {
|
||||||
|
return defer(() => {
|
||||||
|
const req = {
|
||||||
|
query: {
|
||||||
|
limit: pageSize,
|
||||||
|
offset: BigInt(pageIndex * pageSize),
|
||||||
|
asc: direction === 'asc',
|
||||||
|
},
|
||||||
|
sortingColumn,
|
||||||
|
queries: queries.map((query) => ({ query })),
|
||||||
|
};
|
||||||
|
|
||||||
|
this.loading.set(true);
|
||||||
|
return this.userService.listUsers(req);
|
||||||
|
}).pipe(
|
||||||
|
catchError((error) => {
|
||||||
|
this.toast.showError(error);
|
||||||
|
return EMPTY;
|
||||||
|
}),
|
||||||
|
finalize(() => this.loading.set(false)),
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
public isAllSelected(): boolean {
|
public isAllSelected(): boolean {
|
||||||
@ -145,147 +357,49 @@ export class UserTableComponent implements OnInit {
|
|||||||
this.isAllSelected() ? this.selection.clear() : this.dataSource.data.forEach((row) => this.selection.select(row));
|
this.isAllSelected() ? this.selection.clear() : this.dataSource.data.forEach((row) => this.selection.select(row));
|
||||||
}
|
}
|
||||||
|
|
||||||
public changePage(event: PageEvent): void {
|
public async deactivateSelectedUsers(): Promise<void> {
|
||||||
this.selection.clear();
|
|
||||||
this.getData(event.pageSize, event.pageIndex * event.pageSize, this.type, this.searchQueries).then();
|
|
||||||
}
|
|
||||||
|
|
||||||
public deactivateSelectedUsers(): void {
|
|
||||||
const usersToDeactivate = this.selection.selected
|
const usersToDeactivate = this.selection.selected
|
||||||
.filter((u) => u.state === UserState.ACTIVE)
|
.filter((u) => u.state === UserState.ACTIVE)
|
||||||
.map((value) => {
|
.map((value) => {
|
||||||
return this.userService.deactivateUser(value.userId);
|
return this.userService.deactivateUser(value.userId);
|
||||||
});
|
});
|
||||||
|
|
||||||
Promise.all(usersToDeactivate)
|
try {
|
||||||
.then(() => {
|
await Promise.all(usersToDeactivate);
|
||||||
|
} catch (error) {
|
||||||
|
this.toast.showError(error);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
this.toast.showInfo('USER.TOAST.SELECTEDDEACTIVATED', true);
|
this.toast.showInfo('USER.TOAST.SELECTEDDEACTIVATED', true);
|
||||||
this.selection.clear();
|
this.selection.clear();
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
this.refreshPage();
|
this.refresh$.next(true);
|
||||||
}, 1000);
|
}, 1000);
|
||||||
})
|
|
||||||
.catch((error) => {
|
|
||||||
this.toast.showError(error);
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public reactivateSelectedUsers(): void {
|
public async reactivateSelectedUsers(): Promise<void> {
|
||||||
const usersToReactivate = this.selection.selected
|
const usersToReactivate = this.selection.selected
|
||||||
.filter((u) => u.state === UserState.INACTIVE)
|
.filter((u) => u.state === UserState.INACTIVE)
|
||||||
.map((value) => {
|
.map((value) => {
|
||||||
return this.userService.reactivateUser(value.userId);
|
return this.userService.reactivateUser(value.userId);
|
||||||
});
|
});
|
||||||
|
|
||||||
Promise.all(usersToReactivate)
|
try {
|
||||||
.then(() => {
|
await Promise.all(usersToReactivate);
|
||||||
|
} catch (error) {
|
||||||
|
this.toast.showError(error);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
this.toast.showInfo('USER.TOAST.SELECTEDREACTIVATED', true);
|
this.toast.showInfo('USER.TOAST.SELECTEDREACTIVATED', true);
|
||||||
this.selection.clear();
|
this.selection.clear();
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
this.refreshPage();
|
this.refresh$.next(true);
|
||||||
}, 1000);
|
}, 1000);
|
||||||
})
|
|
||||||
.catch((error) => {
|
|
||||||
this.toast.showError(error);
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public gotoRouterLink(rL: any): Promise<boolean> {
|
public deleteUser(user: User): void {
|
||||||
return this.router.navigate(rL);
|
|
||||||
}
|
|
||||||
|
|
||||||
private async getData(limit: number, offset: number, type: Type, searchQueries?: SearchQuery[]): Promise<void> {
|
|
||||||
this.loadingSubject.next(true);
|
|
||||||
|
|
||||||
let queryT = create(SearchQuerySchema, {
|
|
||||||
query: {
|
|
||||||
case: 'typeQuery',
|
|
||||||
value: {
|
|
||||||
type,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
let sortingField: UserFieldName | undefined = undefined;
|
|
||||||
if (this.sort?.active && this.sort?.direction)
|
|
||||||
switch (this.sort.active) {
|
|
||||||
case 'displayName':
|
|
||||||
sortingField = UserFieldName.DISPLAY_NAME;
|
|
||||||
break;
|
|
||||||
case 'username':
|
|
||||||
sortingField = UserFieldName.USER_NAME;
|
|
||||||
break;
|
|
||||||
case 'preferredLoginName':
|
|
||||||
// TODO: replace with preferred username sorting once implemented
|
|
||||||
sortingField = UserFieldName.USER_NAME;
|
|
||||||
break;
|
|
||||||
case 'email':
|
|
||||||
sortingField = UserFieldName.EMAIL;
|
|
||||||
break;
|
|
||||||
case 'state':
|
|
||||||
sortingField = UserFieldName.STATE;
|
|
||||||
break;
|
|
||||||
case 'creationDate':
|
|
||||||
sortingField = UserFieldName.CREATION_DATE;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
this.userService
|
|
||||||
.listUsers(
|
|
||||||
limit,
|
|
||||||
offset,
|
|
||||||
searchQueries?.length ? [queryT, ...searchQueries] : [queryT],
|
|
||||||
sortingField,
|
|
||||||
this.sort?.direction,
|
|
||||||
)
|
|
||||||
.then((resp) => {
|
|
||||||
if (resp.details?.totalResult) {
|
|
||||||
this.totalResult = Number(resp.details.totalResult);
|
|
||||||
} else {
|
|
||||||
this.totalResult = 0;
|
|
||||||
}
|
|
||||||
if (resp.details?.timestamp) {
|
|
||||||
this.viewTimestamp = resp.details?.timestamp;
|
|
||||||
}
|
|
||||||
this.dataSource.data = resp.result;
|
|
||||||
this.loadingSubject.next(false);
|
|
||||||
})
|
|
||||||
.catch((error) => {
|
|
||||||
this.toast.showError(error);
|
|
||||||
this.loadingSubject.next(false);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
public refreshPage(): void {
|
|
||||||
this.getData(
|
|
||||||
this.paginator.pageSize,
|
|
||||||
this.paginator.pageIndex * this.paginator.pageSize,
|
|
||||||
this.type,
|
|
||||||
this.searchQueries,
|
|
||||||
).then();
|
|
||||||
}
|
|
||||||
|
|
||||||
public sortChange(sortState: Sort) {
|
|
||||||
if (sortState.direction && sortState.active) {
|
|
||||||
this._liveAnnouncer.announce(`Sorted ${sortState.direction} ending`).then();
|
|
||||||
this.refreshPage();
|
|
||||||
} else {
|
|
||||||
this._liveAnnouncer.announce('Sorting cleared').then();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public applySearchQuery(searchQueries: SearchQuery[]): void {
|
|
||||||
this.selection.clear();
|
|
||||||
this.searchQueries = searchQueries;
|
|
||||||
this.getData(
|
|
||||||
this.paginator ? this.paginator.pageSize : this.INITIAL_PAGE_SIZE,
|
|
||||||
this.paginator ? this.paginator.pageIndex * this.paginator.pageSize : 0,
|
|
||||||
this.type,
|
|
||||||
searchQueries,
|
|
||||||
).then();
|
|
||||||
}
|
|
||||||
|
|
||||||
public deleteUser(user: UserV2): void {
|
|
||||||
const authUserData = {
|
const authUserData = {
|
||||||
confirmKey: 'ACTIONS.DELETE',
|
confirmKey: 'ACTIONS.DELETE',
|
||||||
cancelKey: 'ACTIONS.CANCEL',
|
cancelKey: 'ACTIONS.CANCEL',
|
||||||
@ -309,8 +423,9 @@ export class UserTableComponent implements OnInit {
|
|||||||
};
|
};
|
||||||
|
|
||||||
if (user?.userId) {
|
if (user?.userId) {
|
||||||
const authUser = this.user();
|
const authUser = this.myUser();
|
||||||
const isMe = authUser?.id === user.userId;
|
console.log('my user', authUser);
|
||||||
|
const isMe = authUser?.userId === user.userId;
|
||||||
|
|
||||||
let dialogRef;
|
let dialogRef;
|
||||||
|
|
||||||
@ -326,21 +441,21 @@ export class UserTableComponent implements OnInit {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
dialogRef.afterClosed().subscribe((resp) => {
|
dialogRef
|
||||||
if (resp) {
|
.afterClosed()
|
||||||
this.userService
|
.pipe(
|
||||||
.deleteUser(user.userId)
|
filter(Boolean),
|
||||||
.then(() => {
|
switchMap(() => this.userService.deleteUser(user.userId)),
|
||||||
|
)
|
||||||
|
.subscribe({
|
||||||
|
next: () => {
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
this.refreshPage();
|
this.refresh$.next(true);
|
||||||
}, 1000);
|
}, 1000);
|
||||||
this.selection.clear();
|
this.selection.clear();
|
||||||
this.toast.showInfo('USER.TOAST.DELETED', true);
|
this.toast.showInfo('USER.TOAST.DELETED', true);
|
||||||
})
|
},
|
||||||
.catch((error) => {
|
error: (err) => this.toast.showError(err),
|
||||||
this.toast.showError(error);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -354,4 +469,20 @@ export class UserTableComponent implements OnInit {
|
|||||||
const selected = this.selection.selected;
|
const selected = this.selection.selected;
|
||||||
return selected ? selected.findIndex((user) => user.state !== UserState.INACTIVE) > -1 : false;
|
return selected ? selected.findIndex((user) => user.state !== UserState.INACTIVE) > -1 : false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private getActiveOrgId() {
|
||||||
|
return this.authenticationService.authenticationChanged.pipe(
|
||||||
|
startWith(true),
|
||||||
|
filter(Boolean),
|
||||||
|
switchMap(() =>
|
||||||
|
from(this.authService.getActiveOrg()).pipe(
|
||||||
|
catchError((err) => {
|
||||||
|
this.toast.showError(err);
|
||||||
|
return of(undefined);
|
||||||
|
}),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
map((org) => org?.id),
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
import { Injectable } from '@angular/core';
|
import { DestroyRef, Injectable } from '@angular/core';
|
||||||
import { GrpcService } from './grpc.service';
|
import { GrpcService } from './grpc.service';
|
||||||
import {
|
import {
|
||||||
AddHumanUserRequestSchema,
|
AddHumanUserRequestSchema,
|
||||||
@ -11,7 +11,6 @@ import {
|
|||||||
DeactivateUserResponse,
|
DeactivateUserResponse,
|
||||||
DeleteUserRequestSchema,
|
DeleteUserRequestSchema,
|
||||||
DeleteUserResponse,
|
DeleteUserResponse,
|
||||||
GetUserByIDRequestSchema,
|
|
||||||
GetUserByIDResponse,
|
GetUserByIDResponse,
|
||||||
ListAuthenticationFactorsRequestSchema,
|
ListAuthenticationFactorsRequestSchema,
|
||||||
ListAuthenticationFactorsResponse,
|
ListAuthenticationFactorsResponse,
|
||||||
@ -65,60 +64,87 @@ import {
|
|||||||
} from '@zitadel/proto/zitadel/user/v2/user_pb';
|
} from '@zitadel/proto/zitadel/user/v2/user_pb';
|
||||||
import { create } from '@bufbuild/protobuf';
|
import { create } from '@bufbuild/protobuf';
|
||||||
import { Timestamp as TimestampV2, TimestampSchema } from '@bufbuild/protobuf/wkt';
|
import { Timestamp as TimestampV2, TimestampSchema } from '@bufbuild/protobuf/wkt';
|
||||||
import { Details, DetailsSchema, ListQuerySchema } from '@zitadel/proto/zitadel/object/v2/object_pb';
|
import { Details, DetailsSchema } from '@zitadel/proto/zitadel/object/v2/object_pb';
|
||||||
import { SearchQuery, UserFieldName } from '@zitadel/proto/zitadel/user/v2/query_pb';
|
|
||||||
import { SortDirection } from '@angular/material/sort';
|
|
||||||
import { Human, Machine, Phone, Profile, User } from '../proto/generated/zitadel/user_pb';
|
import { Human, Machine, Phone, Profile, User } from '../proto/generated/zitadel/user_pb';
|
||||||
import { ObjectDetails } from '../proto/generated/zitadel/object_pb';
|
import { ObjectDetails } from '../proto/generated/zitadel/object_pb';
|
||||||
import { Timestamp } from '../proto/generated/google/protobuf/timestamp_pb';
|
import { Timestamp } from '../proto/generated/google/protobuf/timestamp_pb';
|
||||||
import { HumanPhone, HumanPhoneSchema } from '@zitadel/proto/zitadel/user/v2/phone_pb';
|
import { HumanPhone, HumanPhoneSchema } from '@zitadel/proto/zitadel/user/v2/phone_pb';
|
||||||
|
import { OAuthService } from 'angular-oauth2-oidc';
|
||||||
|
import { firstValueFrom, Observable, shareReplay } from 'rxjs';
|
||||||
|
import { filter, map, startWith, tap, timeout } from 'rxjs/operators';
|
||||||
|
import { takeUntilDestroyed } from '@angular/core/rxjs-interop';
|
||||||
|
|
||||||
@Injectable({
|
@Injectable({
|
||||||
providedIn: 'root',
|
providedIn: 'root',
|
||||||
})
|
})
|
||||||
export class UserService {
|
export class UserService {
|
||||||
constructor(private readonly grpcService: GrpcService) {}
|
private readonly userId$: Observable<string>;
|
||||||
|
private user: UserV2 | undefined;
|
||||||
|
|
||||||
|
constructor(
|
||||||
|
private readonly grpcService: GrpcService,
|
||||||
|
private readonly oauthService: OAuthService,
|
||||||
|
destroyRef: DestroyRef,
|
||||||
|
) {
|
||||||
|
this.userId$ = this.getUserId().pipe(shareReplay({ refCount: true, bufferSize: 1 }));
|
||||||
|
|
||||||
|
// this preloads the userId and deletes the cache everytime the userId changes
|
||||||
|
this.userId$.pipe(takeUntilDestroyed(destroyRef)).subscribe(async () => {
|
||||||
|
this.user = undefined;
|
||||||
|
try {
|
||||||
|
await this.getMyUser();
|
||||||
|
} catch (error) {
|
||||||
|
console.warn(error);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private getUserId() {
|
||||||
|
return this.oauthService.events.pipe(
|
||||||
|
filter((event) => event.type === 'token_received'),
|
||||||
|
startWith(this.oauthService.getIdToken),
|
||||||
|
map(() => this.oauthService.getIdToken()),
|
||||||
|
filter(Boolean),
|
||||||
|
// split jwt and get base64 encoded payload
|
||||||
|
map((token) => token.split('.')[1]),
|
||||||
|
// decode payload
|
||||||
|
map(atob),
|
||||||
|
// parse payload
|
||||||
|
map((payload) => JSON.parse(payload)),
|
||||||
|
map((payload: unknown) => {
|
||||||
|
// check if sub is in payload and is a string
|
||||||
|
if (payload && typeof payload === 'object' && 'sub' in payload && typeof payload.sub === 'string') {
|
||||||
|
return payload.sub;
|
||||||
|
}
|
||||||
|
throw new Error('Invalid payload');
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
public addHumanUser(req: MessageInitShape<typeof AddHumanUserRequestSchema>): Promise<AddHumanUserResponse> {
|
public addHumanUser(req: MessageInitShape<typeof AddHumanUserRequestSchema>): Promise<AddHumanUserResponse> {
|
||||||
return this.grpcService.userNew.addHumanUser(create(AddHumanUserRequestSchema, req));
|
return this.grpcService.userNew.addHumanUser(create(AddHumanUserRequestSchema, req));
|
||||||
}
|
}
|
||||||
|
|
||||||
public listUsers(
|
public listUsers(req: MessageInitShape<typeof ListUsersRequestSchema>): Promise<ListUsersResponse> {
|
||||||
limit: number,
|
|
||||||
offset: number,
|
|
||||||
queriesList?: SearchQuery[],
|
|
||||||
sortingColumn?: UserFieldName,
|
|
||||||
sortingDirection?: SortDirection,
|
|
||||||
): Promise<ListUsersResponse> {
|
|
||||||
const query = create(ListQuerySchema);
|
|
||||||
|
|
||||||
if (limit) {
|
|
||||||
query.limit = limit;
|
|
||||||
}
|
|
||||||
if (offset) {
|
|
||||||
query.offset = BigInt(offset);
|
|
||||||
}
|
|
||||||
if (sortingDirection) {
|
|
||||||
query.asc = sortingDirection === 'asc';
|
|
||||||
}
|
|
||||||
|
|
||||||
const req = create(ListUsersRequestSchema, {
|
|
||||||
query,
|
|
||||||
});
|
|
||||||
|
|
||||||
if (sortingColumn) {
|
|
||||||
req.sortingColumn = sortingColumn;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (queriesList) {
|
|
||||||
req.queries = queriesList;
|
|
||||||
}
|
|
||||||
|
|
||||||
return this.grpcService.userNew.listUsers(req);
|
return this.grpcService.userNew.listUsers(req);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public async getMyUser(): Promise<UserV2> {
|
||||||
|
const userId = await firstValueFrom(this.userId$.pipe(timeout(2000)));
|
||||||
|
if (this.user) {
|
||||||
|
return this.user;
|
||||||
|
}
|
||||||
|
const resp = await this.getUserById(userId);
|
||||||
|
if (!resp.user) {
|
||||||
|
throw new Error("Couldn't find user");
|
||||||
|
}
|
||||||
|
|
||||||
|
this.user = resp.user;
|
||||||
|
return resp.user;
|
||||||
|
}
|
||||||
|
|
||||||
public getUserById(userId: string): Promise<GetUserByIDResponse> {
|
public getUserById(userId: string): Promise<GetUserByIDResponse> {
|
||||||
return this.grpcService.userNew.getUserByID(create(GetUserByIDRequestSchema, { userId }));
|
return this.grpcService.userNew.getUserByID({ userId });
|
||||||
}
|
}
|
||||||
|
|
||||||
public deactivateUser(userId: string): Promise<DeactivateUserResponse> {
|
public deactivateUser(userId: string): Promise<DeactivateUserResponse> {
|
||||||
|
Loading…
x
Reference in New Issue
Block a user