mirror of
https://github.com/zitadel/zitadel.git
synced 2025-02-28 19: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 { ActivatedRoute, Router } from '@angular/router';
|
||||
import { take } from 'rxjs';
|
||||
@ -27,9 +27,10 @@ export class FilterOrgComponent extends FilterComponent implements OnInit {
|
||||
|
||||
constructor(
|
||||
router: Router,
|
||||
destroyRef: DestroyRef,
|
||||
protected override route: ActivatedRoute,
|
||||
) {
|
||||
super(router, route);
|
||||
super(router, route, destroyRef);
|
||||
}
|
||||
|
||||
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 { ActivatedRoute, Router } from '@angular/router';
|
||||
import { take } from 'rxjs';
|
||||
@ -23,8 +23,8 @@ export class FilterProjectComponent extends FilterComponent implements OnInit {
|
||||
public searchQueries: ProjectQuery[] = [];
|
||||
|
||||
public states: ProjectState[] = [ProjectState.PROJECT_STATE_ACTIVE, ProjectState.PROJECT_STATE_INACTIVE];
|
||||
constructor(router: Router, route: ActivatedRoute) {
|
||||
super(router, route);
|
||||
constructor(router: Router, route: ActivatedRoute, destroyRef: DestroyRef) {
|
||||
super(router, route, destroyRef);
|
||||
}
|
||||
|
||||
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 { ActivatedRoute, Router } from '@angular/router';
|
||||
import { take } from 'rxjs';
|
||||
@ -29,8 +29,8 @@ export class FilterUserGrantsComponent extends FilterComponent implements OnInit
|
||||
public SubQuery: any = SubQuery;
|
||||
public searchQueries: UserGrantQuery[] = [];
|
||||
|
||||
constructor(router: Router, route: ActivatedRoute) {
|
||||
super(router, route);
|
||||
constructor(router: Router, route: ActivatedRoute, destroyRef: DestroyRef) {
|
||||
super(router, route, destroyRef);
|
||||
}
|
||||
|
||||
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="name-query">
|
||||
<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 { ActivatedRoute, Router } from '@angular/router';
|
||||
import { take } from 'rxjs';
|
||||
@ -13,6 +13,7 @@ import {
|
||||
} from 'src/app/proto/generated/zitadel/user_pb';
|
||||
|
||||
import { FilterComponent } from '../filter/filter.component';
|
||||
import { filter, map } from 'rxjs/operators';
|
||||
|
||||
enum SubQuery {
|
||||
STATE,
|
||||
@ -28,25 +29,27 @@ enum SubQuery {
|
||||
})
|
||||
export class FilterUserComponent extends FilterComponent implements OnInit {
|
||||
public SubQuery: any = SubQuery;
|
||||
public searchQueries: UserSearchQuery[] = [];
|
||||
private searchQueries: UserSearchQuery[] = [];
|
||||
|
||||
public states: UserState[] = [
|
||||
UserState.USER_STATE_ACTIVE,
|
||||
UserState.USER_STATE_INACTIVE,
|
||||
UserState.USER_STATE_DELETED,
|
||||
UserState.USER_STATE_INITIAL,
|
||||
UserState.USER_STATE_LOCKED,
|
||||
UserState.USER_STATE_SUSPEND,
|
||||
UserState.USER_STATE_INITIAL,
|
||||
];
|
||||
constructor(router: Router, route: ActivatedRoute) {
|
||||
super(router, route);
|
||||
constructor(router: Router, route: ActivatedRoute, destroyRef: DestroyRef) {
|
||||
super(router, route, destroyRef);
|
||||
}
|
||||
|
||||
ngOnInit(): void {
|
||||
this.route.queryParams.pipe(take(1)).subscribe((params) => {
|
||||
const { filter } = params;
|
||||
if (filter) {
|
||||
const stringifiedFilters = filter as string;
|
||||
this.route.queryParamMap
|
||||
.pipe(
|
||||
take(1),
|
||||
map((params) => params.get('filter')),
|
||||
filter(Boolean),
|
||||
)
|
||||
.subscribe((stringifiedFilters) => {
|
||||
const filters: UserSearchQuery.AsObject[] = JSON.parse(stringifiedFilters) as UserSearchQuery.AsObject[];
|
||||
|
||||
const userQueries = filters.map((filter) => {
|
||||
@ -94,7 +97,6 @@ export class FilterUserComponent extends FilterComponent implements OnInit {
|
||||
this.filterChanged.emit(this.searchQueries ? this.searchQueries : []);
|
||||
// this.showFilter = true;
|
||||
// this.filterOpen.emit(true);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
|
@ -1,7 +1,6 @@
|
||||
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 { Observable, Subject, takeUntil } from 'rxjs';
|
||||
import { SearchQuery as MemberSearchQuery } from 'src/app/proto/generated/zitadel/member_pb';
|
||||
import { TextQueryMethod } from 'src/app/proto/generated/zitadel/object_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 { ActionKeysType } from '../action-keys/action-keys.component';
|
||||
import { takeUntilDestroyed } from '@angular/core/rxjs-interop';
|
||||
|
||||
type FilterSearchQuery = UserSearchQuery | MemberSearchQuery | UserGrantQuery | ProjectQuery | OrgQuery;
|
||||
type FilterSearchQueryAsObject =
|
||||
@ -23,7 +23,7 @@ type FilterSearchQueryAsObject =
|
||||
templateUrl: './filter.component.html',
|
||||
styleUrls: ['./filter.component.scss'],
|
||||
})
|
||||
export class FilterComponent implements OnDestroy {
|
||||
export class FilterComponent {
|
||||
@Output() public filterChanged: EventEmitter<FilterSearchQuery[]> = new EventEmitter();
|
||||
@Output() public filterOpen: EventEmitter<boolean> = new EventEmitter<boolean>(false);
|
||||
|
||||
@ -32,9 +32,6 @@ export class FilterComponent implements OnDestroy {
|
||||
|
||||
@Input() public queryCount: number = 0;
|
||||
|
||||
private destroy$: Subject<void> = new Subject();
|
||||
public filterChanged$: Observable<FilterSearchQuery[]> = this.filterChanged.asObservable();
|
||||
|
||||
public showFilter: boolean = false;
|
||||
public methods: TextQueryMethod[] = [
|
||||
TextQueryMethod.TEXT_QUERY_METHOD_CONTAINS_IGNORE_CASE,
|
||||
@ -59,17 +56,13 @@ export class FilterComponent implements OnDestroy {
|
||||
this.trigger.emit();
|
||||
}
|
||||
|
||||
public ngOnDestroy(): void {
|
||||
this.destroy$.next();
|
||||
this.destroy$.complete();
|
||||
}
|
||||
|
||||
constructor(
|
||||
private router: Router,
|
||||
protected route: ActivatedRoute,
|
||||
destroyRef: DestroyRef,
|
||||
) {
|
||||
const changes$ = this.filterChanged.asObservable();
|
||||
changes$.pipe(takeUntil(this.destroy$)).subscribe((queries) => {
|
||||
changes$.pipe(takeUntilDestroyed(destroyRef)).subscribe((queries) => {
|
||||
const filters: Array<FilterSearchQueryAsObject | {}> | undefined = queries
|
||||
?.map((q) => q.toObject())
|
||||
.map((query) =>
|
||||
@ -81,7 +74,8 @@ export class FilterComponent implements OnDestroy {
|
||||
);
|
||||
|
||||
if (filters && Object.keys(filters)) {
|
||||
this.router.navigate([], {
|
||||
this.router
|
||||
.navigate([], {
|
||||
relativeTo: this.route,
|
||||
queryParams: {
|
||||
['filter']: JSON.stringify(filters),
|
||||
@ -89,7 +83,8 @@ export class FilterComponent implements OnDestroy {
|
||||
replaceUrl: true,
|
||||
queryParamsHandling: 'merge',
|
||||
skipLocationChange: false,
|
||||
});
|
||||
})
|
||||
.then();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
@ -2,12 +2,11 @@
|
||||
<cnsl-top-view
|
||||
title="{{ userName$ | async }}"
|
||||
sub="{{ user(userQuery)?.preferredLoginName }}"
|
||||
[isActive]="user(userQuery)?.state === UserState.USER_STATE_ACTIVE"
|
||||
[isInactive]="user(userQuery)?.state === UserState.USER_STATE_INACTIVE"
|
||||
[isActive]="user(userQuery)?.state === UserState.ACTIVE"
|
||||
[isInactive]="user(userQuery)?.state === UserState.INACTIVE"
|
||||
stateTooltip="{{ 'USER.STATE.' + user(userQuery)?.state | translate }}"
|
||||
[hasBackButton]="['org.read'] | hasRole | async"
|
||||
>
|
||||
<span *ngIf="userQuery.state === 'notfound'">{{ 'USER.PAGES.NOUSER' | translate }}</span>
|
||||
<cnsl-info-row
|
||||
topContent
|
||||
*ngIf="user(userQuery) as user"
|
||||
@ -42,7 +41,7 @@
|
||||
[disabled]="false"
|
||||
[genders]="genders"
|
||||
[languages]="(langSvc.supported$ | async) || []"
|
||||
[username]="user.userName"
|
||||
[username]="user.username"
|
||||
[profile]="profile"
|
||||
[showEditImage]="true"
|
||||
(changedLanguage)="changedLanguage($event)"
|
||||
@ -93,7 +92,7 @@
|
||||
</ng-container>
|
||||
|
||||
<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 *ngIf="currentSetting === 'security'">
|
||||
@ -124,15 +123,14 @@
|
||||
<cnsl-auth-passwordless #mfaComponent></cnsl-auth-passwordless>
|
||||
|
||||
<cnsl-auth-user-mfa
|
||||
[phoneVerified]="humanUser(userQuery)?.type?.value?.phone?.isPhoneVerified ?? false"
|
||||
#mfaComponent
|
||||
[phoneVerified]="humanUser(userQuery)?.type?.value?.phone?.isVerified ?? false"
|
||||
></cnsl-auth-user-mfa>
|
||||
</ng-container>
|
||||
|
||||
<ng-container *ngIf="currentSetting === 'grants'">
|
||||
<cnsl-card title="{{ 'GRANTS.USER.TITLE' | translate }}" description="{{ 'GRANTS.USER.DESCRIPTION' | translate }}">
|
||||
<cnsl-user-grants
|
||||
[userId]="user.id"
|
||||
[userId]="user.userId"
|
||||
[context]="USERGRANTCONTEXT"
|
||||
[displayedColumns]="[
|
||||
'org',
|
||||
@ -165,7 +163,7 @@
|
||||
*ngIf="metadataQuery.state !== 'error'"
|
||||
[metadata]="metadataQuery.value"
|
||||
[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)"
|
||||
(refresh)="refreshMetadata$.next(true)"
|
||||
[loading]="metadataQuery.state === 'loading'"
|
||||
|
@ -36,11 +36,10 @@ import { ToastService } from 'src/app/services/toast.service';
|
||||
import { formatPhone } from 'src/app/utils/formatPhone';
|
||||
import { EditDialogComponent, EditDialogData, EditDialogResult, EditDialogType } from './edit-dialog/edit-dialog.component';
|
||||
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 { pairwiseStartWith } from 'src/app/utils/pairwiseStartWith';
|
||||
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 { NewMgmtService } from 'src/app/services/new-mgmt.service';
|
||||
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 { query } from '@angular/animations';
|
||||
|
||||
type UserQuery =
|
||||
| { state: 'success'; value: User }
|
||||
| { state: 'error'; value: string }
|
||||
| { state: 'loading'; value?: User }
|
||||
| { state: 'notfound' };
|
||||
type UserQuery = { state: 'success'; value: User } | { state: 'error'; value: string } | { state: 'loading'; value?: User };
|
||||
|
||||
type MetadataQuery =
|
||||
| { state: 'success'; value: Metadata[] }
|
||||
| { state: 'loading'; value: Metadata[] }
|
||||
| { 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({
|
||||
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'],
|
||||
})
|
||||
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 UserState: any = UserState;
|
||||
protected readonly UserState = UserState;
|
||||
|
||||
public USERGRANTCONTEXT: UserGrantContext = UserGrantContext.AUTHUSER;
|
||||
public refreshChanges$: EventEmitter<void> = new EventEmitter();
|
||||
public refreshMetadata$ = new Subject<true>();
|
||||
protected USERGRANTCONTEXT: UserGrantContext = UserGrantContext.AUTHUSER;
|
||||
protected readonly refreshChanges$: EventEmitter<void> = new EventEmitter();
|
||||
protected readonly refreshMetadata$ = new Subject<true>();
|
||||
|
||||
public settingsList: SidenavSetting[] = [
|
||||
protected readonly settingsList: SidenavSetting[] = [
|
||||
{ id: 'general', i18nKey: 'USER.SETTINGS.GENERAL' },
|
||||
{ id: 'security', i18nKey: 'USER.SETTINGS.SECURITY' },
|
||||
{ id: 'idp', i18nKey: 'USER.SETTINGS.IDP' },
|
||||
@ -92,9 +87,9 @@ export class AuthUserDetailComponent implements OnInit {
|
||||
protected readonly user$: Observable<UserQuery>;
|
||||
protected readonly metadata$: Observable<MetadataQuery>;
|
||||
private readonly savedLanguage$: Observable<string>;
|
||||
protected currentSetting$: Observable<string | undefined>;
|
||||
public loginPolicy$: Observable<LoginPolicy>;
|
||||
protected userName$: Observable<string>;
|
||||
protected readonly currentSetting$: Observable<string | undefined>;
|
||||
protected readonly loginPolicy$: Observable<LoginPolicy>;
|
||||
protected readonly userName$: Observable<string>;
|
||||
|
||||
constructor(
|
||||
public translate: TranslateService,
|
||||
@ -209,13 +204,8 @@ export class AuthUserDetailComponent implements OnInit {
|
||||
}
|
||||
|
||||
private getMyUser(): Observable<UserQuery> {
|
||||
return defer(() => this.newAuthService.getMyUser()).pipe(
|
||||
map(({ user }) => {
|
||||
if (user) {
|
||||
return { state: 'success', value: user } as const;
|
||||
}
|
||||
return { state: 'notfound' } as const;
|
||||
}),
|
||||
return defer(() => this.userService.getMyUser()).pipe(
|
||||
map((user) => ({ state: 'success' as const, value: user })),
|
||||
catchError((error) => of({ state: 'error', value: error.message ?? '' } as const)),
|
||||
startWith({ state: 'loading' } as const),
|
||||
);
|
||||
@ -232,7 +222,7 @@ export class AuthUserDetailComponent implements OnInit {
|
||||
if (!user.value) {
|
||||
return EMPTY;
|
||||
}
|
||||
return this.getMetadataById(user.value.id);
|
||||
return this.getMetadataById(user.value.userId);
|
||||
}),
|
||||
pairwiseStartWith(undefined),
|
||||
map(([prev, curr]) => {
|
||||
@ -259,7 +249,7 @@ export class AuthUserDetailComponent implements OnInit {
|
||||
labelKey: 'ACTIONS.NEWVALUE' as const,
|
||||
titleKey: 'USER.PROFILE.CHANGEUSERNAME_TITLE' 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, {
|
||||
data,
|
||||
@ -271,8 +261,8 @@ export class AuthUserDetailComponent implements OnInit {
|
||||
.pipe(
|
||||
map((value) => value?.value),
|
||||
filter(Boolean),
|
||||
filter((value) => user.userName != value),
|
||||
switchMap((username) => this.userService.updateUser({ userId: user.id, username })),
|
||||
filter((value) => user.username != value),
|
||||
switchMap((username) => this.userService.updateUser({ userId: user.userId, username })),
|
||||
)
|
||||
.subscribe({
|
||||
next: () => {
|
||||
@ -288,7 +278,7 @@ export class AuthUserDetailComponent implements OnInit {
|
||||
public saveProfile(user: User, profile: HumanProfile): void {
|
||||
this.userService
|
||||
.updateUser({
|
||||
userId: user.id,
|
||||
userId: user.userId,
|
||||
profile: {
|
||||
givenName: profile.givenName,
|
||||
familyName: profile.familyName,
|
||||
@ -350,7 +340,7 @@ export class AuthUserDetailComponent implements OnInit {
|
||||
|
||||
public resendEmailVerification(user: User): void {
|
||||
this.newMgmtService
|
||||
.resendHumanEmailVerification(user.id)
|
||||
.resendHumanEmailVerification(user.userId)
|
||||
.then(() => {
|
||||
this.toast.showInfo('USER.TOAST.EMAILVERIFICATIONSENT', true);
|
||||
this.refreshChanges$.emit();
|
||||
@ -362,7 +352,7 @@ export class AuthUserDetailComponent implements OnInit {
|
||||
|
||||
public resendPhoneVerification(user: User): void {
|
||||
this.newMgmtService
|
||||
.resendHumanPhoneVerification(user.id)
|
||||
.resendHumanPhoneVerification(user.userId)
|
||||
.then(() => {
|
||||
this.toast.showInfo('USER.TOAST.PHONEVERIFICATIONSENT', true);
|
||||
this.refreshChanges$.emit();
|
||||
@ -374,7 +364,7 @@ export class AuthUserDetailComponent implements OnInit {
|
||||
|
||||
public deletePhone(user: User): void {
|
||||
this.userService
|
||||
.removePhone(user.id)
|
||||
.removePhone(user.userId)
|
||||
.then(() => {
|
||||
this.toast.showInfo('USER.TOAST.PHONEREMOVED', true);
|
||||
this.refreshChanges$.emit();
|
||||
@ -417,7 +407,7 @@ export class AuthUserDetailComponent implements OnInit {
|
||||
filter((resp): resp is Required<EditDialogResult> => !!resp?.value),
|
||||
switchMap(({ value, isVerified }) =>
|
||||
this.userService.setEmail({
|
||||
userId: user.id,
|
||||
userId: user.userId,
|
||||
email: value,
|
||||
verification: isVerified ? { case: 'isVerified', value: isVerified } : { case: undefined },
|
||||
}),
|
||||
@ -453,7 +443,7 @@ export class AuthUserDetailComponent implements OnInit {
|
||||
.pipe(
|
||||
map((resp) => formatPhone(resp?.value)),
|
||||
filter(Boolean),
|
||||
switchMap(({ phone }) => this.userService.setPhone({ userId: user.id, phone })),
|
||||
switchMap(({ phone }) => this.userService.setPhone({ userId: user.userId, phone })),
|
||||
)
|
||||
.subscribe({
|
||||
next: () => {
|
||||
@ -482,7 +472,7 @@ export class AuthUserDetailComponent implements OnInit {
|
||||
.afterClosed()
|
||||
.pipe(
|
||||
filter(Boolean),
|
||||
switchMap(() => this.userService.deleteUser(user.id)),
|
||||
switchMap(() => this.userService.deleteUser(user.userId)),
|
||||
)
|
||||
.subscribe({
|
||||
next: () => {
|
||||
@ -498,9 +488,9 @@ export class AuthUserDetailComponent implements OnInit {
|
||||
this.newMgmtService.setUserMetadata({
|
||||
key,
|
||||
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, {
|
||||
data: {
|
||||
|
@ -2,7 +2,7 @@
|
||||
<div class="content">
|
||||
<cnsl-form-field class="formfield">
|
||||
<cnsl-label>{{ 'USER.MACHINE.USERNAME' | translate }}</cnsl-label>
|
||||
<input cnslInput formControlName="userName" required />
|
||||
<input cnslInput formControlName="username" required />
|
||||
</cnsl-form-field>
|
||||
<cnsl-form-field class="formfield">
|
||||
<cnsl-label>{{ 'USER.MACHINE.NAME' | translate }}</cnsl-label>
|
||||
|
@ -1,5 +1,5 @@
|
||||
<div class="max-width-container">
|
||||
<div class="enlarged-container" [ngSwitch]="type">
|
||||
<div class="enlarged-container">
|
||||
<div class="users-title-row">
|
||||
<h1>{{ 'DESCRIPTIONS.USERS.TITLE' | translate }}</h1>
|
||||
<a mat-icon-button href="https://zitadel.com/docs/concepts/structure/users" rel="noreferrer" target="_blank">
|
||||
@ -7,21 +7,6 @@
|
||||
</a>
|
||||
</div>
|
||||
<p class="user-list-sub cnsl-secondary-text">{{ 'DESCRIPTIONS.USERS.DESCRIPTION' | translate }}</p>
|
||||
<ng-container *ngSwitchCase="Type.TYPE_HUMAN">
|
||||
<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>
|
||||
<cnsl-user-table [canWrite$]="['user.write$'] | hasRole" [canDelete$]="['user.delete$'] | hasRole"> </cnsl-user-table>
|
||||
</div>
|
||||
</div>
|
||||
|
@ -1,8 +1,5 @@
|
||||
import { Component } from '@angular/core';
|
||||
import { ActivatedRoute, Params } from '@angular/router';
|
||||
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';
|
||||
|
||||
@Component({
|
||||
@ -11,23 +8,10 @@ import { Breadcrumb, BreadcrumbService, BreadcrumbType } from 'src/app/services/
|
||||
styleUrls: ['./user-list.component.scss'],
|
||||
})
|
||||
export class UserListComponent {
|
||||
public Type: any = Type;
|
||||
public type: Type = Type.TYPE_HUMAN;
|
||||
|
||||
constructor(
|
||||
public translate: TranslateService,
|
||||
activatedRoute: ActivatedRoute,
|
||||
protected readonly translate: TranslateService,
|
||||
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 = {
|
||||
type: BreadcrumbType.ORG,
|
||||
routerLink: ['/org'],
|
||||
|
@ -1,11 +1,11 @@
|
||||
<cnsl-refresh-table
|
||||
[loading]="loading$ | async"
|
||||
(refreshed)="refreshPage()"
|
||||
[dataSize]="totalResult"
|
||||
*ngIf="type$ | async as type"
|
||||
[loading]="loading()"
|
||||
(refreshed)="this.refresh$.next(true)"
|
||||
[dataSize]="dataSize()"
|
||||
[hideRefresh]="true"
|
||||
[timestamp]="viewTimestamp"
|
||||
[timestamp]="(users$ | async)?.details?.timestamp"
|
||||
[selection]="selection"
|
||||
[emitRefreshOnPreviousRoutes]="refreshOnPreviousRoutes"
|
||||
[showBorder]="true"
|
||||
>
|
||||
<div leftActions class="user-toggle-group">
|
||||
@ -60,12 +60,12 @@
|
||||
<cnsl-filter-user
|
||||
actions
|
||||
*ngIf="!selection.hasValue()"
|
||||
(filterChanged)="applySearchQuery($any($event))"
|
||||
(filterChanged)="this.searchQueries$.next($any($event))"
|
||||
(filterOpen)="filterOpen = $event"
|
||||
></cnsl-filter-user>
|
||||
<ng-template cnslHasRole [hasRole]="['user.write']" actions>
|
||||
<button
|
||||
(click)="gotoRouterLink(['/users', type === Type.HUMAN ? 'create' : 'create-machine'])"
|
||||
(click)="router.navigate(['/users', type === Type.HUMAN ? 'create' : 'create-machine'])"
|
||||
color="primary"
|
||||
mat-raised-button
|
||||
[disabled]="(canWrite$ | async) === false"
|
||||
@ -77,7 +77,7 @@
|
||||
<span>{{ 'ACTIONS.NEW' | translate }}</span>
|
||||
<cnsl-action-keys
|
||||
*ngIf="!filterOpen"
|
||||
(actionTriggered)="gotoRouterLink(['/users', type === Type.HUMAN ? 'create' : 'create-machine'])"
|
||||
(actionTriggered)="router.navigate(['/users', type === Type.HUMAN ? 'create' : 'create-machine'])"
|
||||
>
|
||||
</cnsl-action-keys>
|
||||
</div>
|
||||
@ -85,7 +85,7 @@
|
||||
</ng-template>
|
||||
|
||||
<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">
|
||||
<th mat-header-cell *matHeaderCellDef>
|
||||
<div class="selection">
|
||||
@ -133,12 +133,7 @@
|
||||
</ng-container>
|
||||
|
||||
<ng-container matColumnDef="displayName">
|
||||
<th
|
||||
mat-header-cell
|
||||
*matHeaderCellDef
|
||||
mat-sort-header
|
||||
[ngClass]="{ 'search-active': this.userSearchKey === UserListSearchKey.DISPLAY_NAME }"
|
||||
>
|
||||
<th mat-header-cell *matHeaderCellDef mat-sort-header>
|
||||
{{ 'USER.PROFILE.DISPLAYNAME' | translate }}
|
||||
</th>
|
||||
<td mat-cell *matCellDef="let user" [routerLink]="user.userId ? ['/users', user.userId] : null">
|
||||
@ -148,12 +143,7 @@
|
||||
</ng-container>
|
||||
|
||||
<ng-container matColumnDef="preferredLoginName">
|
||||
<th
|
||||
mat-header-cell
|
||||
*matHeaderCellDef
|
||||
mat-sort-header
|
||||
[ngClass]="{ 'search-active': this.userSearchKey === UserListSearchKey.DISPLAY_NAME }"
|
||||
>
|
||||
<th mat-header-cell *matHeaderCellDef mat-sort-header>
|
||||
{{ 'USER.PROFILE.PREFERREDLOGINNAME' | translate }}
|
||||
</th>
|
||||
<td mat-cell *matCellDef="let user" [routerLink]="user.userId ? ['/users', user.userId] : null">
|
||||
@ -162,12 +152,7 @@
|
||||
</ng-container>
|
||||
|
||||
<ng-container matColumnDef="username">
|
||||
<th
|
||||
mat-header-cell
|
||||
*matHeaderCellDef
|
||||
mat-sort-header
|
||||
[ngClass]="{ 'search-active': this.userSearchKey === UserListSearchKey.USER_NAME }"
|
||||
>
|
||||
<th mat-header-cell *matHeaderCellDef mat-sort-header>
|
||||
{{ 'USER.PROFILE.USERNAME' | translate }}
|
||||
</th>
|
||||
<td mat-cell *matCellDef="let user" [routerLink]="user.userId ? ['/users', user.userId] : null">
|
||||
@ -176,12 +161,7 @@
|
||||
</ng-container>
|
||||
|
||||
<ng-container matColumnDef="email">
|
||||
<th
|
||||
mat-header-cell
|
||||
*matHeaderCellDef
|
||||
mat-sort-header
|
||||
[ngClass]="{ 'search-active': this.UserListSearchKey === UserListSearchKey.EMAIL }"
|
||||
>
|
||||
<th mat-header-cell *matHeaderCellDef mat-sort-header>
|
||||
{{ 'USER.EMAIL' | translate }}
|
||||
</th>
|
||||
<td mat-cell *matCellDef="let user" [routerLink]="user.userId ? ['/users', user.userId] : null">
|
||||
@ -250,17 +230,16 @@
|
||||
</table>
|
||||
</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>
|
||||
<span>{{ 'USER.TABLE.EMPTY' | translate }}</span>
|
||||
</div>
|
||||
<cnsl-paginator
|
||||
#paginator
|
||||
class="paginator"
|
||||
[length]="totalResult || 0"
|
||||
[length]="dataSize()"
|
||||
[pageSize]="INITIAL_PAGE_SIZE"
|
||||
[timestamp]="viewTimestamp"
|
||||
[timestamp]="(users$ | async)?.details?.timestamp"
|
||||
[pageSizeOptions]="[10, 20, 50, 100]"
|
||||
(page)="changePage($event)"
|
||||
></cnsl-paginator>
|
||||
<!-- (page)="changePage($event)"-->
|
||||
</cnsl-refresh-table>
|
||||
|
@ -1,34 +1,45 @@
|
||||
import { LiveAnnouncer } from '@angular/cdk/a11y';
|
||||
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 { MatSort, Sort } from '@angular/material/sort';
|
||||
import { MatSort, SortDirection } from '@angular/material/sort';
|
||||
import { MatTableDataSource } from '@angular/material/table';
|
||||
import { ActivatedRoute, Router } from '@angular/router';
|
||||
import { TranslateService } from '@ngx-translate/core';
|
||||
import { BehaviorSubject, Observable, of } from 'rxjs';
|
||||
import { take } from 'rxjs/operators';
|
||||
import {
|
||||
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 { 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 { GrpcAuthService } from 'src/app/services/grpc-auth.service';
|
||||
import { ToastService } from 'src/app/services/toast.service';
|
||||
import { UserService } from 'src/app/services/user.service';
|
||||
import { toSignal } from '@angular/core/rxjs-interop';
|
||||
import { User } from 'src/app/proto/generated/zitadel/user_pb';
|
||||
import { SearchQuery, SearchQuerySchema, Type, UserFieldName } from '@zitadel/proto/zitadel/user/v2/query_pb';
|
||||
import { UserState, User as UserV2 } from '@zitadel/proto/zitadel/user/v2/user_pb';
|
||||
import { create } from '@bufbuild/protobuf';
|
||||
import { Timestamp } from '@bufbuild/protobuf/wkt';
|
||||
import { takeUntilDestroyed, toSignal } from '@angular/core/rxjs-interop';
|
||||
import { SearchQuery as UserSearchQuery } from 'src/app/proto/generated/zitadel/user_pb';
|
||||
import { Type, UserFieldName } from '@zitadel/proto/zitadel/user/v2/query_pb';
|
||||
import { UserState, User } from '@zitadel/proto/zitadel/user/v2/user_pb';
|
||||
import { MessageInitShape } from '@bufbuild/protobuf';
|
||||
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 {
|
||||
FIRST_NAME,
|
||||
LAST_NAME,
|
||||
DISPLAY_NAME,
|
||||
USER_NAME,
|
||||
EMAIL,
|
||||
}
|
||||
type Query = Exclude<
|
||||
Exclude<MessageInitShape<typeof ListUsersRequestSchema>['queries'], undefined>[number]['query'],
|
||||
undefined
|
||||
>;
|
||||
|
||||
@Component({
|
||||
selector: 'cnsl-user-table',
|
||||
@ -37,25 +48,33 @@ enum UserListSearchKey {
|
||||
animations: [enterAnimations],
|
||||
})
|
||||
export class UserTableComponent implements OnInit {
|
||||
public userSearchKey: UserListSearchKey | undefined = undefined;
|
||||
public Type = Type;
|
||||
@Input() public type: Type = Type.HUMAN;
|
||||
@Input() refreshOnPreviousRoutes: string[] = [];
|
||||
protected readonly Type = Type;
|
||||
protected readonly refresh$ = new ReplaySubject<true>(1);
|
||||
|
||||
@Input() public canWrite$: 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;
|
||||
@ViewChild(MatSort) public sort!: MatSort;
|
||||
public INITIAL_PAGE_SIZE: number = 20;
|
||||
private readonly paginator$ = new ReplaySubject<PaginatorComponent>(1);
|
||||
@ViewChild(PaginatorComponent) public set paginator(paginator: PaginatorComponent) {
|
||||
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[] = [
|
||||
'select',
|
||||
'displayName',
|
||||
@ -76,46 +95,56 @@ export class UserTableComponent implements OnInit {
|
||||
'actions',
|
||||
];
|
||||
|
||||
@Output() public changedSelection: EventEmitter<Array<UserV2>> = new EventEmitter();
|
||||
@Output() public changedSelection: EventEmitter<Array<User>> = new EventEmitter();
|
||||
|
||||
public UserState: any = UserState;
|
||||
public UserListSearchKey: any = UserListSearchKey;
|
||||
protected readonly UserState = UserState;
|
||||
|
||||
public ActionKeysType: any = ActionKeysType;
|
||||
public filterOpen: boolean = false;
|
||||
protected ActionKeysType = ActionKeysType;
|
||||
protected filterOpen: boolean = false;
|
||||
|
||||
private searchQueries: SearchQuery[] = [];
|
||||
constructor(
|
||||
private router: Router,
|
||||
public translate: TranslateService,
|
||||
private authService: GrpcAuthService,
|
||||
private userService: UserService,
|
||||
private toast: ToastService,
|
||||
private dialog: MatDialog,
|
||||
private route: ActivatedRoute,
|
||||
private _liveAnnouncer: LiveAnnouncer,
|
||||
protected readonly router: Router,
|
||||
public readonly translate: TranslateService,
|
||||
private readonly userService: UserService,
|
||||
private readonly toast: ToastService,
|
||||
private readonly dialog: MatDialog,
|
||||
private readonly route: ActivatedRoute,
|
||||
private readonly destroyRef: DestroyRef,
|
||||
private readonly authenticationService: AuthenticationService,
|
||||
private readonly authService: GrpcAuthService,
|
||||
) {
|
||||
this.selection.changed.subscribe(() => {
|
||||
this.changedSelection.emit(this.selection.selected);
|
||||
});
|
||||
this.type$ = this.getType$().pipe(shareReplay({ refCount: true, bufferSize: 1 }));
|
||||
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 {
|
||||
this.route.queryParams.pipe(take(1)).subscribe((params) => {
|
||||
if (!params['filter']) {
|
||||
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.selection.changed.pipe(takeUntilDestroyed(this.destroyRef)).subscribe(() => {
|
||||
this.changedSelection.emit(this.selection.selected);
|
||||
});
|
||||
|
||||
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 {
|
||||
this.type = type;
|
||||
setType(type: Type) {
|
||||
this.router
|
||||
.navigate([], {
|
||||
relativeTo: this.route,
|
||||
@ -127,12 +156,195 @@ export class UserTableComponent implements OnInit {
|
||||
skipLocationChange: false,
|
||||
})
|
||||
.then();
|
||||
this.getData(
|
||||
this.paginator.pageSize,
|
||||
this.paginator.pageIndex * this.paginator.pageSize,
|
||||
this.type,
|
||||
this.searchQueries,
|
||||
).then();
|
||||
}
|
||||
|
||||
private getMyUser() {
|
||||
return defer(() => this.userService.getMyUser()).pipe(
|
||||
catchError((error) => {
|
||||
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 {
|
||||
@ -145,147 +357,49 @@ export class UserTableComponent implements OnInit {
|
||||
this.isAllSelected() ? this.selection.clear() : this.dataSource.data.forEach((row) => this.selection.select(row));
|
||||
}
|
||||
|
||||
public changePage(event: PageEvent): void {
|
||||
this.selection.clear();
|
||||
this.getData(event.pageSize, event.pageIndex * event.pageSize, this.type, this.searchQueries).then();
|
||||
}
|
||||
|
||||
public deactivateSelectedUsers(): void {
|
||||
public async deactivateSelectedUsers(): Promise<void> {
|
||||
const usersToDeactivate = this.selection.selected
|
||||
.filter((u) => u.state === UserState.ACTIVE)
|
||||
.map((value) => {
|
||||
return this.userService.deactivateUser(value.userId);
|
||||
});
|
||||
|
||||
Promise.all(usersToDeactivate)
|
||||
.then(() => {
|
||||
try {
|
||||
await Promise.all(usersToDeactivate);
|
||||
} catch (error) {
|
||||
this.toast.showError(error);
|
||||
return;
|
||||
}
|
||||
|
||||
this.toast.showInfo('USER.TOAST.SELECTEDDEACTIVATED', true);
|
||||
this.selection.clear();
|
||||
setTimeout(() => {
|
||||
this.refreshPage();
|
||||
this.refresh$.next(true);
|
||||
}, 1000);
|
||||
})
|
||||
.catch((error) => {
|
||||
this.toast.showError(error);
|
||||
});
|
||||
}
|
||||
|
||||
public reactivateSelectedUsers(): void {
|
||||
public async reactivateSelectedUsers(): Promise<void> {
|
||||
const usersToReactivate = this.selection.selected
|
||||
.filter((u) => u.state === UserState.INACTIVE)
|
||||
.map((value) => {
|
||||
return this.userService.reactivateUser(value.userId);
|
||||
});
|
||||
|
||||
Promise.all(usersToReactivate)
|
||||
.then(() => {
|
||||
try {
|
||||
await Promise.all(usersToReactivate);
|
||||
} catch (error) {
|
||||
this.toast.showError(error);
|
||||
return;
|
||||
}
|
||||
|
||||
this.toast.showInfo('USER.TOAST.SELECTEDREACTIVATED', true);
|
||||
this.selection.clear();
|
||||
setTimeout(() => {
|
||||
this.refreshPage();
|
||||
this.refresh$.next(true);
|
||||
}, 1000);
|
||||
})
|
||||
.catch((error) => {
|
||||
this.toast.showError(error);
|
||||
});
|
||||
}
|
||||
|
||||
public gotoRouterLink(rL: any): Promise<boolean> {
|
||||
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 {
|
||||
public deleteUser(user: User): void {
|
||||
const authUserData = {
|
||||
confirmKey: 'ACTIONS.DELETE',
|
||||
cancelKey: 'ACTIONS.CANCEL',
|
||||
@ -309,8 +423,9 @@ export class UserTableComponent implements OnInit {
|
||||
};
|
||||
|
||||
if (user?.userId) {
|
||||
const authUser = this.user();
|
||||
const isMe = authUser?.id === user.userId;
|
||||
const authUser = this.myUser();
|
||||
console.log('my user', authUser);
|
||||
const isMe = authUser?.userId === user.userId;
|
||||
|
||||
let dialogRef;
|
||||
|
||||
@ -326,21 +441,21 @@ export class UserTableComponent implements OnInit {
|
||||
});
|
||||
}
|
||||
|
||||
dialogRef.afterClosed().subscribe((resp) => {
|
||||
if (resp) {
|
||||
this.userService
|
||||
.deleteUser(user.userId)
|
||||
.then(() => {
|
||||
dialogRef
|
||||
.afterClosed()
|
||||
.pipe(
|
||||
filter(Boolean),
|
||||
switchMap(() => this.userService.deleteUser(user.userId)),
|
||||
)
|
||||
.subscribe({
|
||||
next: () => {
|
||||
setTimeout(() => {
|
||||
this.refreshPage();
|
||||
this.refresh$.next(true);
|
||||
}, 1000);
|
||||
this.selection.clear();
|
||||
this.toast.showInfo('USER.TOAST.DELETED', true);
|
||||
})
|
||||
.catch((error) => {
|
||||
this.toast.showError(error);
|
||||
});
|
||||
}
|
||||
},
|
||||
error: (err) => this.toast.showError(err),
|
||||
});
|
||||
}
|
||||
}
|
||||
@ -354,4 +469,20 @@ export class UserTableComponent implements OnInit {
|
||||
const selected = this.selection.selected;
|
||||
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 {
|
||||
AddHumanUserRequestSchema,
|
||||
@ -11,7 +11,6 @@ import {
|
||||
DeactivateUserResponse,
|
||||
DeleteUserRequestSchema,
|
||||
DeleteUserResponse,
|
||||
GetUserByIDRequestSchema,
|
||||
GetUserByIDResponse,
|
||||
ListAuthenticationFactorsRequestSchema,
|
||||
ListAuthenticationFactorsResponse,
|
||||
@ -65,60 +64,87 @@ import {
|
||||
} from '@zitadel/proto/zitadel/user/v2/user_pb';
|
||||
import { create } from '@bufbuild/protobuf';
|
||||
import { Timestamp as TimestampV2, TimestampSchema } from '@bufbuild/protobuf/wkt';
|
||||
import { Details, DetailsSchema, ListQuerySchema } 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 { Details, DetailsSchema } from '@zitadel/proto/zitadel/object/v2/object_pb';
|
||||
import { Human, Machine, Phone, Profile, User } from '../proto/generated/zitadel/user_pb';
|
||||
import { ObjectDetails } from '../proto/generated/zitadel/object_pb';
|
||||
import { Timestamp } from '../proto/generated/google/protobuf/timestamp_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({
|
||||
providedIn: 'root',
|
||||
})
|
||||
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> {
|
||||
return this.grpcService.userNew.addHumanUser(create(AddHumanUserRequestSchema, req));
|
||||
}
|
||||
|
||||
public listUsers(
|
||||
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;
|
||||
}
|
||||
|
||||
public listUsers(req: MessageInitShape<typeof ListUsersRequestSchema>): Promise<ListUsersResponse> {
|
||||
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> {
|
||||
return this.grpcService.userNew.getUserByID(create(GetUserByIDRequestSchema, { userId }));
|
||||
return this.grpcService.userNew.getUserByID({ userId });
|
||||
}
|
||||
|
||||
public deactivateUser(userId: string): Promise<DeactivateUserResponse> {
|
||||
|
Loading…
x
Reference in New Issue
Block a user